diff --git a/.babelrc b/.babelrc deleted file mode 100644 index a9ce1369e61..00000000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["react-native"] -} diff --git a/.clj-kondo/babashka/fs/config.edn b/.clj-kondo/babashka/fs/config.edn new file mode 100644 index 00000000000..23f36094841 --- /dev/null +++ b/.clj-kondo/babashka/fs/config.edn @@ -0,0 +1 @@ +{:lint-as {babashka.fs/with-temp-dir clojure.core/let}} diff --git a/.clj-kondo/babashka/sci/config.edn b/.clj-kondo/babashka/sci/config.edn new file mode 100644 index 00000000000..60ea30d04f3 --- /dev/null +++ b/.clj-kondo/babashka/sci/config.edn @@ -0,0 +1 @@ +{:hooks {:macroexpand {sci.core/copy-ns sci.core/copy-ns}}} diff --git a/.clj-kondo/babashka/sci/sci/core.clj b/.clj-kondo/babashka/sci/sci/core.clj new file mode 100644 index 00000000000..ac324eecad5 --- /dev/null +++ b/.clj-kondo/babashka/sci/sci/core.clj @@ -0,0 +1,9 @@ +(ns sci.core) + +(defmacro copy-ns + ([ns-sym sci-ns] + `(copy-ns ~ns-sym ~sci-ns nil)) + ([ns-sym sci-ns opts] + `[(quote ~ns-sym) + ~sci-ns + (quote ~opts)])) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn new file mode 100644 index 00000000000..c653dc1048d --- /dev/null +++ b/.clj-kondo/config.edn @@ -0,0 +1,113 @@ +{:config-paths ["status-im"] + :output {:exclude-files ["src/user.cljs" "src/dev/user.cljs"]} + :lint-as {legacy.status-im.utils.views/defview clojure.core/defn + legacy.status-im.utils.views/letsubs clojure.core/let + reagent.core/with-let clojure.core/let + legacy.status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all + utils.re-frame/defn clojure.core/defn + quo.react/with-deps-check clojure.core/fn + quo.previews.preview/list-comp clojure.core/for + legacy.status-im.utils.styles/def clojure.core/def + legacy.status-im.utils.styles/defn clojure.core/defn + test-helpers.unit/deftest-sub clojure.core/defn + test-helpers.unit/deftest-event clojure.core/defn + + taoensso.tufte/defnp clojure.core/defn} + :linters {:clojure-lsp/unused-public-var {:exclude-when-defined-by #{utils.re-frame/defn} + :exclude #{ + ;; even if we are not + ;; using color now, we + ;; will need it later + quo.foundations.colors + + ;; same for funcs from + ;; native-module + native-module.core + + ;; some funcs used + ;; temporarily during + ;; development + test-helpers.component + test-helpers.unit + + ;; not sure if unused + ;; functions here should + ;; be removed: + keycard.keycard + react-native.fs}} + :case-duplicate-test {:level :error} + :case-quoted-test {:level :error} + :case-symbol-test {:level :error} + :clj-kondo-config {:level :error} + :cond-else {:level :error} + :condition-always-true {:level :error} + :conflicting-alias {:level :error} + :consistent-alias {:level :error + :aliases {clojure.set set + clojure.string string + clojure.walk walk + malli.core malli + malli.dev.pretty malli.pretty + malli.dev.virhe malli.virhe + malli.error malli.error + malli.generator malli.generator + malli.transform malli.transform + malli.util malli.util + promesa.core promesa + schema.core schema + status-im.feature-flags ff + taoensso.timbre log}} + :deprecated-namespace {:level :warning} + :docstring-blank {:level :error} + :equals-true {:level :error} + :inline-def {:level :error} + :invalid-arity {:skip-args [legacy.status-im.utils.fx/defn + utils.re-frame/defn + utils.re-frame/merge]} + :loop-without-recur {:level :error} + :minus-one {:level :error} + :misplaced-docstring {:level :error} + :missing-body-in-when {:level :error} + :missing-clause-in-try {:level :error} + :missing-else-branch {:level :error} + :multiple-async-in-deftest {:level :error} + :not-empty? {:level :error} + :plus-one {:level :error} + :redundant-do {:level :error} + :redundant-let {:level :error} + :refer-all {:level :error} + ;; ignore error in config itself saying that `:shadowed-fn-param` is unknown + #_{:clj-kondo/ignore [:clj-kondo-config]} + :shadowed-fn-param {:level :error} + :shadowed-var {:level :error + ;; We temporarily use :include to define an + ;; allowlist of core Clojure vars. In the + ;; future, as we progressively fix shadowed + ;; vars, we should be able to delete this + ;; option and lint all vars. + :exclude [type name]} + :self-requiring-namespace {:level :error} + :single-operand-comparison {:level :error} + :syntax {:level :error} + :unbound-destructuring-default {:level :error} + :underscore-in-namespace {:level :error} + :uninitialized-var {:level :error} + :unknown-require-option {:level :error} + :unreachable-code {:level :error} + :unresolved-namespace {:level :error} + ;; TODO remove number when this is fixed + ;; https://github.com/borkdude/clj-kondo/issues/867 + :unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY + number + legacy.status-im.test-helpers/restore-app-db + (cljs.test/is [match-strict?])]} + :unresolved-var {:level :error} + :unsorted-required-namespaces {:level :error} + :unused-alias {:level :warning} + :unused-binding {:level :error} + :unused-import {:level :error} + :unused-namespace {:level :error} + :unused-private-var {:level :error} + :unused-referred-var {:level :error} + :use {:level :error}} + :config-in-ns {mocks.js-dependencies {:linters {:clojure-lsp/unused-public-var {:level :off}}}}} diff --git a/.clj-kondo/funcool/promesa/config.edn b/.clj-kondo/funcool/promesa/config.edn new file mode 100644 index 00000000000..1844f77c5a6 --- /dev/null +++ b/.clj-kondo/funcool/promesa/config.edn @@ -0,0 +1,9 @@ +{:lint-as {promesa.core/-> clojure.core/-> + promesa.core/->> clojure.core/->> + promesa.core/as-> clojure.core/as-> + promesa.core/let clojure.core/let + promesa.core/plet clojure.core/let + promesa.core/loop clojure.core/loop + promesa.core/recur clojure.core/recur + promesa.core/with-redefs clojure.core/with-redefs + promesa.core/doseq clojure.core/doseq}} diff --git a/.clj-kondo/metosin/malli/config.edn b/.clj-kondo/metosin/malli/config.edn new file mode 100644 index 00000000000..0f8b25ccfd9 --- /dev/null +++ b/.clj-kondo/metosin/malli/config.edn @@ -0,0 +1,2 @@ +{:lint-as {malli.experimental/defn schema.core/defn} + :linters {:unresolved-symbol {:exclude [(malli.core/=>)]}}} diff --git a/.clj-kondo/nubank/matcher-combinators/config.edn b/.clj-kondo/nubank/matcher-combinators/config.edn new file mode 100644 index 00000000000..ea4ded7232f --- /dev/null +++ b/.clj-kondo/nubank/matcher-combinators/config.edn @@ -0,0 +1,4 @@ +{:linters + {:unresolved-symbol + {:exclude [(cljs.test/is [match? thrown-match?]) + (clojure.test/is [match? thrown-match?])]}}} diff --git a/.clj-kondo/rewrite-clj/rewrite-clj/config.edn b/.clj-kondo/rewrite-clj/rewrite-clj/config.edn new file mode 100644 index 00000000000..19ecae96a0f --- /dev/null +++ b/.clj-kondo/rewrite-clj/rewrite-clj/config.edn @@ -0,0 +1,5 @@ +{:lint-as + {rewrite-clj.zip/subedit-> clojure.core/-> + rewrite-clj.zip/subedit->> clojure.core/->> + rewrite-clj.zip/edit-> clojure.core/-> + rewrite-clj.zip/edit->> clojure.core/->>}} diff --git a/.clj-kondo/status-im/cljs/test.clj b/.clj-kondo/status-im/cljs/test.clj new file mode 100644 index 00000000000..6e3fa07f487 --- /dev/null +++ b/.clj-kondo/status-im/cljs/test.clj @@ -0,0 +1,21 @@ +(ns cljs.test + (:require [clj-kondo.hooks-api :as hooks])) + +(defn deftest + "Verify test name passed to `cljs.test/deftest` is suffixed with -test and not + prefixed with test-." + [{:keys [node]}] + (let [[_ test-name-node & _] (:children node) + test-name (str (hooks/sexpr test-name-node))] + (when (and (hooks/token-node? test-name-node) + (or (not (re-find #"^.*-test$" test-name)) + (re-find #"^test-.*$" test-name))) + (hooks/reg-finding! (assoc (meta test-name-node) + :message "Test name should be suffixed with -test" + :type :status-im.linter/inconsistent-test-name))))) + +(comment + ;; Invalid + (deftest {:node (hooks/parse-string "(deftest foo-tes (println :hello))")}) + (deftest {:node (hooks/parse-string "(deftest test-foo-test (println :hello))")}) +) diff --git a/.clj-kondo/status-im/config.edn b/.clj-kondo/status-im/config.edn new file mode 100644 index 00000000000..e2af52c1949 --- /dev/null +++ b/.clj-kondo/status-im/config.edn @@ -0,0 +1,11 @@ +{:hooks + {:analyze-call + {utils.i18n/label utils.i18n/label + test-helpers.component/get-all-by-translation-text utils.i18n/label + test-helpers.component/get-by-translation-text utils.i18n/label + test-helpers.component/query-all-by-translation-text utils.i18n/label + test-helpers.component/query-by-translation-text utils.i18n/label + + cljs.test/deftest cljs.test/deftest}} + :linters {:status-im.linter/invalid-translation-keyword {:level :error} + :status-im.linter/inconsistent-test-name {:level :error}}} diff --git a/.clj-kondo/status-im/utils/i18n.clj b/.clj-kondo/status-im/utils/i18n.clj new file mode 100644 index 00000000000..bde1603bae5 --- /dev/null +++ b/.clj-kondo/status-im/utils/i18n.clj @@ -0,0 +1,26 @@ +(ns utils.i18n + (:require [clj-kondo.hooks-api :as hooks])) + +(defn label + "Verify call to `utils.i18n/label` pass the translation keyword qualified with `t`." + [{:keys [node]}] + (let [[_ translation-key-node & _] (:children node)] + (when (and (hooks/keyword-node? translation-key-node) + (not= "t" (-> translation-key-node hooks/sexpr namespace))) + (hooks/reg-finding! (assoc (meta translation-key-node) + :message "Translation keyword should be qualified with \"t\"" + :type :status-im.linter/invalid-translation-keyword))))) + +(comment + ;; Valid + (label {:node (hooks/parse-string "(i18n/label :t/foo {:var \"hello\"})") + :cljc false + :lang :cljs + :filename "" + :config {} + :ns "" + :context nil}) + + ;; Invalid + (label {:node (hooks/parse-string "(i18n/label :foo)")}) +) diff --git a/.clj-kondo/taoensso/encore/config.edn b/.clj-kondo/taoensso/encore/config.edn new file mode 100644 index 00000000000..7b0ff3c2b7b --- /dev/null +++ b/.clj-kondo/taoensso/encore/config.edn @@ -0,0 +1 @@ +{:hooks {:analyze-call {taoensso.encore/defalias taoensso.encore/defalias}}} diff --git a/.clj-kondo/taoensso/encore/taoensso/encore.clj b/.clj-kondo/taoensso/encore/taoensso/encore.clj new file mode 100644 index 00000000000..7f6d30ac94e --- /dev/null +++ b/.clj-kondo/taoensso/encore/taoensso/encore.clj @@ -0,0 +1,16 @@ +(ns taoensso.encore + (:require + [clj-kondo.hooks-api :as hooks])) + +(defn defalias [{:keys [node]}] + (let [[sym-raw src-raw] (rest (:children node)) + src (if src-raw src-raw sym-raw) + sym (if src-raw + sym-raw + (symbol (name (hooks/sexpr src))))] + {:node (with-meta + (hooks/list-node + [(hooks/token-node 'def) + (hooks/token-node (hooks/sexpr sym)) + (hooks/token-node (hooks/sexpr src))]) + (meta src))})) diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100755 index 00000000000..e94d38dc945 --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,22 @@ +# https://dependabot.com/docs/config-file/ + +version: 1 +update_configs: + # Keep build.gradle files up to date, batching pull requests weekly + - package_manager: "java:gradle" + directory: "/android" + update_schedule: "weekly" + default_reviewers: + - "jakubgs" + allowed_updates: + - match: + update_type: "security" + # Keep /mobile/js_files/yarn.lock up to date, batching pull requests weekly + - package_manager: "javascript" + directory: "/" + update_schedule: "weekly" + allowed_updates: + - match: + update_type: security + default_reviewers: + - "jakubgs" diff --git a/.dockerignore b/.dockerignore index 895ecd84070..a6e9f8eeeac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -36,7 +36,6 @@ project.xcworkspace # .idea .gradle -local.properties *.iml # Atom @@ -46,7 +45,6 @@ local.properties # node_modules/ npm-debug.log -.nvmrc # BUCK buck-out/ @@ -88,7 +86,6 @@ doo-index.html # Status Statusgo.framework -status-go-local.aar status-dev-cli #ios diff --git a/.env b/.env index 749144116ee..75cb04aac3a 100644 --- a/.env +++ b/.env @@ -1,18 +1,42 @@ -TESTFAIRY_ENABLED=0 +CACHED_WEBVIEWS_ENABLED=1 +DEBUG_WEBVIEW=1 +DEV_BUILD=1 ETHEREUM_DEV_CLUSTER=1 -OFFLINE_INBOX_ENABLED=1 -RPC_NETWORKS_ONLY=1 -LOG_LEVEL=debug -LOG_LEVEL_STATUS_GO=info -QUEUE_MESSAGE_ENABLED=1 -RN_BRIDGE_THRESHOLD_WARNINGS=0 +EXTENSIONS=0 +GROUP_CHATS_ENABLED=1 +LOG_LEVEL=info +MAILSERVER_CONFIRMATIONS_ENABLED=1 +PFS_ENCRYPTION_ENABLED=1 +PFS_TOGGLE_VISIBLE=1 POW_TARGET=0.002 POW_TIME=1 -DEFAULT_NETWORK=mainnet_rpc -TESTFAIRY_TOKEN=969f6c921cb435cea1d41d1ea3f5b247d6026d55 -INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c -DEBUG_WEBVIEW=1 -INSTABUG_SURVEYS=1 -GROUP_CHATS_ENABLED=0 -USE_SYM_KEY=0 -SPAM_BUTTON_DETECTION_ENABLED=1 +SNOOPY=0 +RPC_NETWORKS_ONLY=0 +PARTITIONED_TOPIC=0 +CONTRACT_NODES=1 +STATUS_GO_PROTOCOL=0 +STATUS_GO_ENABLE_NIMBUS=0 +KEYCARD_TEST_MENU=0 +QR_READ_TEST_MENU=1 +ENABLE_ROOT_ALERT=1 +DEBUG_OR_PR_BUILD=1 +APN_TOPIC=im.status.ethereum.pr +COMMUNITIES_ENABLED=1 +DATABASE_MANAGEMENT_ENABLED=1 +DELETE_MESSAGE_ENABLED=1 +COMMANDS_ENABLED=1 +TWO_MINUTES_SYNCING=1 +SWAP_ENABLED=1 +STICKERS_TEST_ENABLED=1 +LOCAL_PAIRING_ENABLED=1 +TEST_STATEOFUS=1 +FAST_CREATE_COMMUNITY_ENABLED=1 +TEST_NETWORKS_ENABLED=1 +SHOW_NOT_IMPLEMENTED_FEATURES=0 +ENABLE_ALERT_BANNER=0 +FLAG_WALLET_CONNECT_ENABLED=1 +API_LOGGING_ENABLED=1 +SENTRY_ENABLED=0 +SENTRY_ENVIRONMENT=ci-main +USE_PUBLIC_LOG_DIR=0 +FLAG_MARKET_ENABLED=0 diff --git a/.env.e2e b/.env.e2e index f590c2d1e38..17e9982f543 100644 --- a/.env.e2e +++ b/.env.e2e @@ -1,17 +1,46 @@ -TESTFAIRY_ENABLED=1 +DEBUG_WEBVIEW=1 ETHEREUM_DEV_CLUSTER=1 -OFFLINE_INBOX_ENABLED=1 +EXTENSIONS=0 +FLEET=status.staging +GROUP_CHATS_ENABLED=1 LOG_LEVEL=debug -LOG_LEVEL_STATUS_GO=info -JSC_ENABLED=1 -QUEUE_MESSAGE_ENABLED=1 -RN_BRIDGE_THRESHOLD_WARNINGS=0 -COMPILE_VIEWS_ENABLED=0 +MAILSERVER_CONFIRMATIONS_ENABLED=0 +PAIRING_POPUP_DISABLED=1 +PFS_ENCRYPTION_ENABLED=1 +PFS_TOGGLE_VISIBLE=1 POW_TARGET=0.002 POW_TIME=1 -DEFAULT_NETWORK=testnet_rpc -INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c -DEBUG_WEBVIEW=1 -INSTABUG_SURVEYS=0 -GROUP_CHATS_ENABLED=1 -USE_SYM_KEY=0 +SNOOPY=0 +STICKERS_ENABLED=1 +PARTITIONED_TOPIC=0 +CONTRACT_NODES=1 +BLANK_PREVIEW=0 +TOOLTIP_EVENTS=1 +COMMANDS_ENABLED=1 +KEYCARD_TEST_MENU=1 +QR_READ_TEST_MENU=1 +ENABLE_ROOT_ALERT=0 +APN_TOPIC=im.status.ethereum.pr +VERIFY_TRANSACTION_CHAIN_ID=11155111 +VERIFY_ENS_CHAIN_ID=11155111 +TEST_STATEOFUS=1 +DATABASE_MANAGEMENT_ENABLED=1 +COMMUNITIES_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=1 +DELETE_MESSAGE_ENABLED=1 +TWO_MINUTES_SYNCING=1 +STICKERS_TEST_ENABLED=1 +LOCAL_PAIRING_ENABLED=1 +FAST_CREATE_COMMUNITY_ENABLED=1 +TEST_NETWORKS_ENABLED=1 +SHOW_NOT_IMPLEMENTED_FEATURES=1 +DELETE_MESSAGE_FOR_ME_UNDO_TIME_LIMIT=10000 +DELETE_MESSAGE_UNDO_TIME_LIMIT=10000 +ENABLE_ALERT_BANNER=1 +FLAG_WALLET_CONNECT_ENABLED=1 +MOBILE_DATA_SYNCING_TOGGLE_ENABLE=0 +API_LOGGING_ENABLED=1 +SENTRY_ENABLED=0 +SENTRY_ENVIRONMENT=ci-main +USE_PUBLIC_LOG_DIR=1 +VIEW_ID_TRACKER=1 diff --git a/.env.jenkins b/.env.jenkins index 114de888612..05d2b95ab84 100644 --- a/.env.jenkins +++ b/.env.jenkins @@ -1,19 +1,44 @@ -TESTFAIRY_ENABLED=1 +CACHED_WEBVIEWS_ENABLED=1 +DEBUG_WEBVIEW=1 ETHEREUM_DEV_CLUSTER=1 -OFFLINE_INBOX_ENABLED=1 +EXTENSIONS=0 +GROUP_CHATS_ENABLED=1 LOG_LEVEL=debug -LOG_LEVEL_STATUS_GO=info -JSC_ENABLED=1 -QUEUE_MESSAGE_ENABLED=1 -RN_BRIDGE_THRESHOLD_WARNINGS=0 -TESTFAIRY_TOKEN=969f6c921cb435cea1d41d1ea3f5b247d6026d55 +MAILSERVER_CONFIRMATIONS_ENABLED=1 +MAINNET_WARNING_ENABLED=1 +PFS_ENCRYPTION_ENABLED=1 +PFS_TOGGLE_VISIBLE=1 POW_TARGET=0.002 POW_TIME=1 -DEFAULT_NETWORK=mainnet_rpc -INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c -DEBUG_WEBVIEW=1 -INSTABUG_SURVEYS=1 -GROUP_CHATS_ENABLED=0 -USE_SYM_KEY=0 -SPAM_BUTTON_DETECTION_ENABLED=1 -MAINNET_WARNING_ENABLED=1 +SNOOPY=0 +RPC_NETWORKS_ONLY=0 +PARTITIONED_TOPIC=0 +CONTRACT_NODES=1 +STATUS_GO_ENABLE_NIMBUS=0 +COMMANDS_ENABLED=1 +KEYCARD_TEST_MENU=0 +ENABLE_ROOT_ALERT=1 +DISABLE_WALLET_ON_MOBILE_NETWORK=1 +APN_TOPIC=im.status.ethereum.pr +VERIFY_TRANSACTION_CHAIN_ID=11155111 +VERIFY_ENS_CHAIN_ID=11155111 +TEST_STATEOFUS=1 +BLANK_PREVIEW=0 +DATABASE_MANAGEMENT_ENABLED=1 +COMMUNITIES_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=1 +DELETE_MESSAGE_ENABLED=1 +TWO_MINUTES_SYNCING=1 +DEBUG_OR_PR_BUILD=1 +STICKERS_TEST_ENABLED=1 +LOCAL_PAIRING_ENABLED=1 +FAST_CREATE_COMMUNITY_ENABLED=1 +TEST_NETWORKS_ENABLED=1 +ENABLE_ALERT_BANNER=1 +FLAG_WALLET_CONNECT_ENABLED=1 +API_LOGGING_ENABLED=1 +SENTRY_ENABLED=0 +SENTRY_ENVIRONMENT=ci-main +USE_PUBLIC_LOG_DIR=0 +VIEW_ID_TRACKER=1 +GOOGLE_FREE=0 diff --git a/.env.nightly b/.env.nightly index 8cde67048ba..c6c4471d072 100644 --- a/.env.nightly +++ b/.env.nightly @@ -1,18 +1,29 @@ -TESTFAIRY_ENABLED=1 +DEBUG_WEBVIEW=1 ETHEREUM_DEV_CLUSTER=1 -OFFLINE_INBOX_ENABLED=1 +EXTENSIONS=0 +GROUP_CHATS_ENABLED=1 LOG_LEVEL=debug -LOG_LEVEL_STATUS_GO=info -JSC_ENABLED=1 -QUEUE_MESSAGE_ENABLED=1 -RN_BRIDGE_THRESHOLD_WARNINGS=0 -TESTFAIRY_TOKEN=969f6c921cb435cea1d41d1ea3f5b247d6026d55 +MAILSERVER_CONFIRMATIONS_ENABLED=1 +MAINNET_WARNING_ENABLED=1 +PFS_ENCRYPTION_ENABLED=1 +PFS_TOGGLE_VISIBLE=1 POW_TARGET=0.002 POW_TIME=1 -DEFAULT_NETWORK=mainnet_rpc -INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c -DEBUG_WEBVIEW=1 -INSTABUG_SURVEYS=1 -GROUP_CHATS_ENABLED=0 -SPAM_BUTTON_DETECTION_ENABLED=1 -MAINNET_WARNING_ENABLED=1 +SNOOPY=0 +KEYCARD_TEST_MENU=0 +RPC_NETWORKS_ONLY=0 +PARTITIONED_TOPIC=0 +CONTRACT_NODES=1 +ENABLE_ROOT_ALERT=1 +BLANK_PREVIEW=0 +COMMUNITIES_ENABLED=1 +DATABASE_MANAGEMENT_ENABLED=1 +DELETE_MESSAGE_ENABLED=1 +FAST_CREATE_COMMUNITY_ENABLED=0 +TEST_NETWORKS_ENABLED=0 +ENABLE_ALERT_BANNER=1 +FLAG_WALLET_CONNECT_ENABLED=1 +API_LOGGING_ENABLED=1 +SENTRY_ENABLED=1 +SENTRY_ENVIRONMENT=ci-nightly +USE_PUBLIC_LOG_DIR=0 diff --git a/.env.prod b/.env.prod deleted file mode 100644 index e5f98c4700c..00000000000 --- a/.env.prod +++ /dev/null @@ -1,20 +0,0 @@ -TESTFAIRY_ENABLED=0 -ETHEREUM_DEV_CLUSTER=0 -MAINNET_WARNING_ENABLED=1 -OFFLINE_INBOX_ENABLED=1 -RPC_NETWORKS_ONLY=1 -LOG_LEVEL=info -LOG_LEVEL_STATUS_GO= -QUEUE_MESSAGE_ENABLED=0 -RN_BRIDGE_THRESHOLD_WARNINGS=0 -POW_TARGET=0.002 -POW_TIME=1 -DEFAULT_NETWORK=mainnet_rpc -TESTFAIRY_TOKEN=969f6c921cb435cea1d41d1ea3f5b247d6026d55 -INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c -DEBUG_WEBVIEW=0 -INSTABUG_SURVEYS=1 -GROUP_CHATS_ENABLED=0 -USE_SYM_KEY=0 -MAINNET_WARNING_ENABLED=1 -SPAM_BUTTON_DETECTION_ENABLED=1 diff --git a/.env.release b/.env.release new file mode 100644 index 00000000000..74a88a0409a --- /dev/null +++ b/.env.release @@ -0,0 +1,26 @@ +DEBUG_WEBVIEW=0 +ETHEREUM_DEV_CLUSTER=0 +EXTENSIONS=0 +GROUP_CHATS_ENABLED=1 +LOG_LEVEL=error +MAILSERVER_CONFIRMATIONS_ENABLED=1 +MAINNET_WARNING_ENABLED=1 +PFS_ENCRYPTION_ENABLED=1 +PFS_TOGGLE_VISIBLE=0 +POW_TARGET=0.002 +POW_TIME=1 +SNOOPY=0 +KEYCARD_TEST_MENU=0 +RPC_NETWORKS_ONLY=1 +PARTITIONED_TOPIC=0 +ENABLE_ROOT_ALERT=1 +MAX_IMAGES_BATCH=1 +DELETE_MESSAGE_ENABLED=1 +FAST_CREATE_COMMUNITY_ENABLED=0 +TEST_NETWORKS_ENABLED=0 +STATUS_PROXY_STAGE_NAME=prod +FLAG_WALLET_CONNECT_ENABLED=1 +API_LOGGING_ENABLED=0 +SENTRY_ENABLED=1 +SENTRY_ENVIRONMENT=production +USE_PUBLIC_LOG_DIR=0 diff --git a/.envrc b/.envrc new file mode 100644 index 00000000000..a8a760658e1 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +layout node diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000..187894b6af2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native', +}; diff --git a/.flowconfig b/.flowconfig index c13c0d08499..9beb474135b 100644 --- a/.flowconfig +++ b/.flowconfig @@ -20,35 +20,64 @@ # Ignore duplicate module providers # For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root -.*/Libraries/react-native/React.js +node_modules/react-native/Libraries/react-native/React.js .*/Libraries/react-native/ReactNative.js .*/node_modules/jest-runtime/build/__tests__/.* ; Ignore polyfills -.*/Libraries/polyfills/.* +node_modules/react-native/Libraries/polyfills/.* -; Ignore metro -.*/node_modules/metro/.* +; These should not be required directly +; require from fbjs/lib instead: require('fbjs/lib/warning') +node_modules/warning/.* +; Flow doesn't support platforms +.*/Libraries/Utilities/LoadingView.js +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* [include] [libs] node_modules/react-native/Libraries/react-native/react-native-interface.js node_modules/react-native/flow/ -node_modules/react-native/flow-github/ [options] -module.system=haste +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable esproposal.class_static_fields=enable esproposal.class_instance_fields=enable +module.file_ext=.js +module.file_ext=.json +module.file_ext=.ios.js + +module.system=haste +module.system.haste.use_name_reducers=true +# get basename +# get basename +module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' +module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' +module.system.haste.paths.blacklist=.*/__tests__/.* +module.system.haste.paths.blacklist=.*/__tests__/.* +module.system.haste.paths.blacklist=.*/__mocks__/.* +module.system.haste.paths.blacklist=.*/__mocks__/.* +module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* +module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* +module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* +module.system.haste.paths.whitelist=/node_modules/react-native/RNTester/.* +module.system.haste.paths.whitelist=/node_modules/react-native/IntegrationTests/.* +module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/react-native/react-native-implementation.js +module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* + experimental.strict_type_args=true munge_underscores=true module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' -module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' +module.name_mapper='^react-native$' -> '/node_modules/react-native/Libraries/react-native/react-native-implementation' +module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' module.file_ext=.js module.file_ext=.jsx @@ -65,4 +94,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]* suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] -^0.63.0 +^0.122.0 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..5a2cd5ee9e0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,25 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +/ci @jakubgs +/fastlane @jakubgs +/scripts @jakubgs +/.env* @jakubgs +/.TOOLVERSIONS @jakubgs +/STATUS_GO_* @jakubgs + +# Order is important; the last matching pattern takes the most +# precedence. When someone opens a pull request that only +# modifies Nix files, only @js-owner and not the global +# owner(s) will be requested for a review. +*.nix @jakubgs +/nix @jakubgs +/.github/ @jakubgs + +/components @status-im/status-core-ui + +/test/appium/ @churik diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fafc70e4ca6..a7c860c6d29 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -8,10 +8,9 @@ These are just guidelines, not rules. Use your best judgment, and feel free to p #### What should I know before I get started? -* [Mission and Core Values](https://wiki.status.im/Mission_and_Core_Values) -* [Code of Conduct](https://wiki.status.im/Code_of_conduct) -* [Read our FAQ](https://wiki.status.im/Frequently_Asked_Questions) -* [Join our Riot](http://chat.status.im/) and chat in `#dev-status` -* [Developer Introduction](https://wiki.status.im/Developer_Documentation#Getting_Started) -* [How to Build Status](https://wiki.status.im/Building_Status) -* [Finding issues to work on](https://wiki.status.im/Developer_Documentation) +* [Mission and Principles](https://status.im/about) +* [Read our FAQ](https://status.im/docs/FAQs.html) +* [Join our Chat](http://join.status.im/chat/public/status-core-dev) +* [Developer Introduction](https://status.im/developer_tools/) +* [Build Status Yourself](https://status.im/technical/build_status) +* [Find issues to work on](https://github.com/status-im/) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index ce3ccf0f0b9..eb3ab87db83 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -43,4 +43,4 @@ As a , I want to so that . [comment]: # (if on Android please replicate bug whilst running adb logcat) ``` ... -``` \ No newline at end of file +``` diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000000..b8c3f5b25a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,34 @@ +--- +name: Bug Report +about: Bug Report +title: '' +type: 'Bug' +assignees: '' + +--- + +# Bug Report + + +## Problem +[comment]: # (A problem description) + + +#### Expected behavior + + +#### Actual behavior + + +### Reproduction + +1. + +### Additional Information + +- Status version: nightly +- Operating System: Android, iOS + + +[comment]: # (Please, add logs/notes if necessary) + diff --git a/.github/ISSUE_TEMPLATE/feature-issue.md b/.github/ISSUE_TEMPLATE/feature-issue.md new file mode 100644 index 00000000000..198b1a06453 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-issue.md @@ -0,0 +1,30 @@ +--- +name: Feature Issue +about: Feature Issue +title: '' +labels: feature +assignees: '' + +--- + +# Feature Issue + +[comment]: # (Please replace ... with your information. Remove < and >) + +### User Story + +As a , I want to **so that** . + +### Description + +[comment]: # (Describe the feature you would like and include designs and Figma links if available.) +[comment]: # (Describe the use cases the implementation should cover. E.g. New users, existing users, public chat, group chat, etc) +*Summary*: ... + +### Acceptance Criteria + +[comment]: # (Rules for the future PR to be accepted.) + +### Notes + +[comment]: # (For the developer who will implement. Include planning poker estimates, guidance, etc.) diff --git a/.github/ISSUE_TEMPLATE/release-issue.md b/.github/ISSUE_TEMPLATE/release-issue.md new file mode 100644 index 00000000000..cb095713d18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release-issue.md @@ -0,0 +1,77 @@ +--- +name: Release umbrella issue +about: Release Issue +title: 'Mobile release ' +labels: release +assignees: '' + +--- +Issue to manage mobile release + +### Scope +[comment]: # (Add milestone from github) +https://github.com/status-im/status-mobile/milestone/ + +- release notes: +- release branch: + + +### Testing info +[comment]: # (TestRail info, added by QA) +Main test suite: +Upgrade (e2e): + +### Builds in TestFlight and PlayStore to push as a release +[comment]: # (Will be added by QA after testing approving of build) + +### Process +#### 1) Kick-off +- [ ] 1. Translation PRs is merged before cutting branch (Jinho) +- [ ] 2. Prepare scope of visible changes based on commits history, started from previous release branch cut ([guide](https://notes.status.im/how-to-prepare-release-notes)) +- [ ] 3. Update [RELEASES.md](https://github.com/status-im/status-mobile/blob/develop/RELEASES.md) with release notes (make PR based on previous step) +- [ ] 4. Create release branch using this [guide](https://github.com/status-im/status-mobile/blob/develop/doc/RELEASE_GUIDE.md), bump VERSION (merge created PR ([example](https://github.com/status-im/status-mobile/pull/12504) ) to develop) and get successful release builds (so, testing can be started) +- [ ] 5. Based on release scope, ask for comms (Jonny) +- [ ] 6. Make sure that assets (screenshots, video) in stores are up-to-date, more info [here](https://notes.status.im/how-to-update-assets) + +#### 2) Testing +[comment]: # (Section will be maintained by QA) +- [ ] Dogfooding (add notes, if it was held) +- [ ] Regression testing for release candidate build +- [ ] Upgrade tests +- [ ] Fix known blocker(s) found during regression testing + +##### Blockers +[comment]: # (Section for blocker issues; should be fixed and merged by release-manager. Remove if no blockers found) +- [ ] 1 blocker: + +============================================================================================= + +- [ ] Cherry-pick fix(es) to the release branch +- [ ] Verify fix(es) on new release candidate build +- [ ] Retest +- [ ] Keep release build forever (button in Jenkins) + +#### 3) Submission + +- [ ] 1. Prepare PR for F-droid build ([instruction]( https://github.com/status-im/status-mobile/blob/develop/doc/FDROID.md)) +- [ ] 2. Release notes written and added (500 character limit for updates (Android) and PR for [RELEASES.md](https://github.com/status-im/status-mobile/blob/develop/RELEASES.md) is ready to be merged (from 1.3 step) +- [ ] 3. Submit beta version for Android (Open Testing in Play Store) +- [ ] 4. Submit iOS build to Apple review ([instruction](https://drive.google.com/file/d/10Cl7PBB7TFPkZiVbfzdFGpfMRP9bxXuq/view?usp=sharing), be careful - low quality of audio) +- [ ] 5. Play store content reviewed and updated +- [ ] 6. Privacy policy reviewed and updated (ask in channel) +- [ ] 7. Countries to be excluded (ask in channel) +- [ ] 8. Changes to our FAQ made (should be checked and if any changes needed, ask Jakub to made them) + + +#### 4) Before publishing +- [ ] 1. Translations for comms available (marketing, ask in the channel) +- [ ] 2. Promotional content ready (marketing, ask in channel) +- [ ] 3. Upload builds for DO store, https://dev.status.im/ (Jakub) + +#### 5) At time of publishing +- [ ] 1. Notify marketing to push comms and blog post (Jonny + get review on blog from QA team) +- [ ] 2. Publish release to app store and playstore +- [ ] 3. Final push to update prod site https://status.im/, [instructions](https://github.com/status-im/infra-docs/blob/master/articles/status_release_upload.md) (Jakub - requires credentials to upload) +- [ ] 4. Add the link to the post about the release and publish the release: https://github.com/status-im/status-mobile/releases (Jakub) +- [ ] 5. Merge PR for updating [RELEASES.md](https://github.com/status-im/status-mobile/blob/develop/RELEASES.md) (from 1.3 step) + diff --git a/.github/ISSUE_TEMPLATE/simple-issue.md b/.github/ISSUE_TEMPLATE/simple-issue.md new file mode 100644 index 00000000000..fa5afd64697 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/simple-issue.md @@ -0,0 +1,26 @@ +--- +name: Simple Issue Report +about: Simple issue report. +--- + +# Problem + +An overview of the background required to understand the problem. +A problem description. + +## Implementation + +Known steps towards feature implementation. +What needs further specifying and investigating. + +## Acceptance Criteria + +Rules for the future PR to be accepted. + +## Notes + +Random notes to keep in mind while implementing it. Mostly about related issues and future plans and thoughts. + +## Future Steps + +Steps which should be taken after this issue has been resolved. diff --git a/.github/ISSUE_TEMPLATE/wont-fix-bug.md b/.github/ISSUE_TEMPLATE/wont-fix-bug.md new file mode 100644 index 00000000000..d42b0b959dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/wont-fix-bug.md @@ -0,0 +1,30 @@ +--- +name: Wont fix Report +about: to facilitate future handling, identify and address non-urgent issues that won't be prioritized in the near future +title: '' +labels: likely_wont_fix +assignees: '' + +--- + +## Problem + + +#### Expected behavior + + +#### Actual behavior + + +### Reproduction + +1. + +### Additional Information + +- Status version: release +- Operating System: Android, iOS + + +[comment]: # (Please, add logs/notes if necessary) + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 64b040873be..e0c1425a1a7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,28 +1,70 @@ -[comment]: # (Please replace ... with your information. Remove < and >) [comment]: # (To auto-close issue on merge, please insert the related issue number after # i.e fixes #566) +fixes #... -If you submit PR for issue with bounty then write here Fixes #NN where NN is issue number +## Summary +[comment]: # (Summarise the problem and how the pull request solves it) +... -*otherwise* +## Review notes +[comment]: # (Optional. Specify if something in particular should be looked at, or ignored, during review) -fixes #... +## Testing notes +[comment]: # (Optional) -### Summary: +### Platforms +[comment]: # (Optional. Specify which platforms should be tested) -[comment]: # (Summarise the problem and how the pull request solves it) -... +- Android +- iOS + +### Areas that may be impacted +[comment]: # (Optional. Specify if some specific areas need to be tested, such as 1-1 chats) + +#### Functional + +- 1-1 chats +- public chats +- group chats +- wallet / transactions +- dapps / app browsing +- account recovery +- new account +- user profile updates +- networks +- mailservers +- fleet +- bootnodes -### Review notes (optional): - +#### Non-functional -### Testing notes (optional): - +- battery performance +- CPU performance / speed of the app +- network consumption + +## Steps to test +[comment]: # (Specify exact steps to test if there are such) -### Steps to test: - Open Status - ... - Step 3, etc. - -status: ready +[comment]: # (Can be ready or wip) +status: ready + + + diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 00000000000..67c74b27405 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,26 @@ +# Set to true to add reviewers to pull requests +addReviewers: true + +# Set to true to add assignees to pull requests +addAssignees: false + +# A list of reviewers to be added to pull requests (GitHub user name) +reviewers: + - status-im/status-core + +# A number of reviewers added to the pull request +# Set 0 to add all the reviewers (default: 0) +numberOfReviewers: 0 + +# A list of assignees, overrides reviewers if set +# assignees: +# - status-im/status-core + +# A number of assignees to add to the pull request +# Set to 0 to add all of the assignees. +# Uses numberOfReviewers if unset. +# numberOfAssignees: 2 + +# A list of keywords to be skipped the process that add reviewers if pull requests include it +skipKeywords: + - wip \ No newline at end of file diff --git a/.github/github-bot.yml b/.github/github-bot.yml index 73c4da92f35..a9398013a9f 100644 --- a/.github/github-bot.yml +++ b/.github/github-bot.yml @@ -2,14 +2,16 @@ _extends: probot-settings project-board: name: 'Pipeline for QA' + min-reviewers: 1 tested-pr-label-name: 'Tested - Issues' contributor-column-name: 'CONTRIBUTOR' review-column-name: 'REVIEW' - test-column-name: 'TO TEST' + test-column-name: 'E2E Tests' automated-tests: - repo-full-name: 'status-im/status-react' - job-full-name: 'end-to-end-tests/status-app-end-to-end-tests' + repo-full-name: 'status-im/status-mobile' + job-full-name: 'status-mobile/e2e/status-app-prs' + kickoff-column-name: 'E2E Tests' github-team: slug: 'clojure' diff --git a/.github/package-lock-snitch.config.json b/.github/package-lock-snitch.config.json new file mode 100644 index 00000000000..a1cc767d29d --- /dev/null +++ b/.github/package-lock-snitch.config.json @@ -0,0 +1,6 @@ +{ + "recipients": [ + "jakubgs", + "corpetty" + ] +} diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000000..4ebcca875a0 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,16 @@ +daysUntilStale: 90 +daysUntilClose: 7 +exemptLabels: + - 'wall of shame' + - security +staleLabel: stale +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +closeComment: > + This issue has been automatically closed. Please re-open if this issue + is important to you. + +pulls: + daysUntilStale: 14 diff --git a/.gitignore b/.gitignore index 3c633dc1081..41a0f621bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ # Xcode # +/ios/.xcode.env.local +/component-spec +result/ build/ *.pbxuser !default.pbxuser @@ -29,17 +32,29 @@ project.xcworkspace # .idea .gradle -local.properties *.iml +**/android/.project +**/android/.settings/org.eclipse.buildship.core.prefs +local.properties +*.hprof +.cxx/ +*.keystore +!debug.keystore # Atom .tags* +# VS Code +.vscode/ +workspace.code-workspace + # node.js # node_modules/ +node_modules.tmp/ npm-debug.log -.nvmrc +yarn-error.log +default.realm/ # BUCK buck-out/ @@ -47,6 +62,7 @@ buck-out/ android/app/libs android/app/obj android/keystores/debug.keystore +!debug.keystore # Generated by clj-rn # @@ -67,25 +83,35 @@ shim.js figwheel_server.log .nrepl-port +# FlowStorm (Clojure debugger) +repl-client-debug + # Lein # .lein-failures .lein-repl-history +# Shadow +.shadow-cljs + ## Doo # out doo-index.html +# Bundler +.bundle + # Status Statusgo.framework -status-go-local.aar -status-dev-cli +Statusgo.xcframework #ios -ios/Pods -ios/StatusIm.xcworkspace +/ios/Pods/ +/ios/logs/ +/ios/StatusIm.xcworkspace .ruby-version +status-e2e/ #python *.pyc @@ -104,12 +130,75 @@ fastlane/README.md # Clj .cpcache/ +src/user.cljs +src/dev/user.cljs # emacs .dir-locals.el -#ignore platform-specific files in the root since they are only symlinks to files in folders 'desktop_files' and 'mobile_files' -/VERSION +# We don't use NPM /package-lock.json -/package.json /.re-natal +/re-natal + +# Jenkins +pkg +/BUILD_NUMBER + +# Conan +conan*.txt +conanbuildinfo.* +conan.cmake + +# Env +.env.bkp + +# nix +/.ran-setup +/.nix-gcroots/ +/result + +# modules +status-modules/translations +status-modules/cljs +status-modules/resources + +## coverage +/.nyc_output +/coverage-report + +## fiddle +/fiddle/node_modules/ +/fiddle/target/ +/fiddle/resources/public/images/ + +trace.edn + +/app/ +project.clj + +##appium +test/appium/report/ +result.xml +test/appium/tests/users.py + +##node bindings +/bin/ +/lib/ + +## visual tests +/artifacts + +/.calva/ + +## component-tests +*.log + +## local xcode env introduced in react-native 0.69 +.xcode.env.local + +## git hooks +lefthook.yml + +## build time logs +/logs/*.log diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..560ca6c0c68 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "legal-docs"] + path = legal-docs + url = https://github.com/status-im/status-software-legal-documents.git diff --git a/.lsp/config.edn b/.lsp/config.edn new file mode 100644 index 00000000000..232387f7bf2 --- /dev/null +++ b/.lsp/config.edn @@ -0,0 +1 @@ +{:clean {:ns-inner-blocks-indentation :keep}} diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000000..21ddfa3c01c --- /dev/null +++ b/.mailmap @@ -0,0 +1,16 @@ +Andrea Maria Piana +Aleksandr Pantyukhov +Aleksandr Pantyukhov +Anton Danchenko +Chad Jackson +Dmitry Shulyak +Eric Dvorsak +Eric Dvorsak +Eric Dvorsak +George Lipov +Goran Jović +Jan Herich +Jan Herich +Jakub Sokołowski +Jarrad Hope +Oskar Thorén diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000000..e648fc1158d --- /dev/null +++ b/.nycrc @@ -0,0 +1,17 @@ +{ + "include": [ + "target/test/legacy/status_im/**" + ], + "exclude": [ + "target/test/legacy/status_im/test/**", + "target/test/legacy/status_im/ui/**" + ], + "report-dir": "./coverage-report", + "all": true, + "source-map": true, + "produce-source-map": true, + "instrument": true, + "exclude-after-remap": false, + "extension": [".js"], + "reporter": ["lcovonly"] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..238132a6987 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,15 @@ +/* + +# Format top-level js files. +!*.js + +# Ignore all except src/js/**/*.js +!/src/ +/src/* +!/src/js +!/src/js/**/*.js + +# Ignore all except translations/en.json +!/translations/ +/translations/* +!/translations/en.json diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000000..a000e79b5ca --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,13 @@ +module.exports = { + arrowParens: 'always', + printWidth: 120, + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: false, + + // JSON sorting + jsonSortOrder: '{ "/.*/": "caseInsensitiveLexical" } ', + plugins: ['prettier-plugin-sort-json'], +}; diff --git a/.vscode/settings.example.json b/.vscode/settings.example.json new file mode 100644 index 00000000000..f81330dbc5b --- /dev/null +++ b/.vscode/settings.example.json @@ -0,0 +1,12 @@ +{ + "editor.formatOnSave": true, + "clojure.format.enable": false, + "[clojure]": { + "editor.defaultFormatter": "rflagreca.vscode-clj-zprint" + }, + "vscode-clj-zprint.IgnoreExternalFiles": false, + "vscode-clj-zprint.CommunityFormatting": false, + "vscode-clj-zprint.OptionsMap": "", + "vscode-clj-zprint.width": "", + "vscode-clj-zprint.Styles.UseOnlyTheseStyles": false +} diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 00000000000..6712d024ced --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1,8 @@ +{ + "ignore_dirs": [ + "android/build", + "target", + ".clj-kondo", + ".shadow-cljs" + ] +} \ No newline at end of file diff --git a/.zprintrc b/.zprintrc new file mode 100644 index 00000000000..f7764fa3c4c --- /dev/null +++ b/.zprintrc @@ -0,0 +1,64 @@ +;; -*- mode: clojure -*- +;; vi: ft=clojure +{:width 105 + :remove {:fn-force-nl #{:noarg1-body}} + + :style + [;; community style https://github.com/kkinnear/zprint/blob/main/doc/reference.md#community + :community + + ;; no comma in map + :no-comma + + :custom-justify + + ;; respect all newlines https://github.com/kkinnear/zprint/blob/main/doc/reference.md#respect-nl + :respect-nl + + ;; respect blank line https://github.com/kkinnear/zprint/blob/main/doc/reference.md#respect-bl + :respect-bl + + ;; hang multiline left-hand-thing https://github.com/kkinnear/zprint/issues/273 + :multi-lhs-hang] + + :fn-map + {"reg-sub" :arg1-pair + "h/describe" :arg1-body + "h/describe-skip" :arg1-body + "h/describe-only" :arg1-body + "h/test" :arg1-body + "h/test-skip" :arg1-body + "h/test-only" :arg1-body + "test/async" :arg1-body + "test/use-fixtures" :arg1-body + "global.describe" :arg1-body + "global.test" :arg1-body + "list-comp" :binding + "defview" :arg1-body + "letsubs" :binding + "with-let" "let" + "reg-event-fx" :arg1-pair + "reg-fx" :arg1-pair + "testing" :arg1-body + "deftest-sub" :arg1-body + "deftest-event" :arg1-body + "test-async" :arg1-body + "wait-for" :arg1-body + "with-deps-check" :arg1-body + "schema/=>" :arg1-body + "->" [:noarg1-body + {:list {:constant-pair? false :force-nl? false} + :next-inner-restore [[:list :constant-pair?]]}] + "set!" "reset!" + "assoc-when" "assoc" + "assoc-some" "assoc" + "conj-when" "conj" + "conj-some" "conj"} + + :style-map + {:no-comma {:map {:comma? false}} + :custom-justify + {:doc "Justify everything using pre-1.1.2 approach" + :binding {:justify? true :justify {:max-variance 1000}} + :map {:justify? true :justify {:max-variance 1000}} + :pair {:justify? true :justify {:max-variance 1000}}}}} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 1053f4a4db2..00000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,73 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## [Unreleased] -### Added - -### Fixed - -### Changed - -## 0.9.23 - 2018-07-23 -### Added -- iPad support. Status is now displayed at full native resolution on iPad's -- Deep links support -- Persist browser history -- Added refresh button -- Added more dapps -- Allow for setting custom network id - -### Fixed -- Fixed Sign in: Cannot paste text within password field [#3931] -- Fixed chat message layout for right-to-left languages -- Fixed parsing of messages containing multiple dots (elipsis) -- Fixed Webview: Screen cut off when using ERC dEX DApp [#3131] -- Fixed links targeting new tabs [#4413] -- Fixed blinking token amounts in transaction history -- Fixed/corrected fiat values in /send and /request messages -- Automatically converts recovery phrase to lowercase during account recovery -- Fixed unread messages counter -- Fixed incoming messages timestamp -- Fixed gas validation: Showing message when total amount being sent plus the max gas cost exceed the balance [#3441] - -### Changed -- Removed Mixpanel integration - -## [0.9.22] - 2018-07-09 -### Added -- Added Farsi public #status channel -- Spam moderation -- Collectibles support (CryptoKitties, CryptoStrikers and Etheremon) -- Added more dapps -- Universal and deep links for public chats, browsing dapps, viewing profiles - -### Fixed -- Fixed mailservers connectivity issue -- Clear chat action correctly clear the unread messages counter -- Gracefully handle realm decryption failures by showing a pop up asking the user to reset the data - -### Changed -- Downgraded React Native to 0.53.3 for improved performance and decreased battery consumption -- When joining a chat only download one day of history - -## [0.9.21] - 2018-06-25 -### Added -- Mainnet is enabled by default on new installations. Upgrades will need to manually switch. -- Chat now supports sending SNT and other tokens in 1-1 chats -- New chat UI for /send and /receive commands -- Updated chat message styling -- Updated toolbar style -- Updated DApp list -- New help links on the Profile tab. Read our Beta FAQ, file a bug, or suggest and vote on features. -- Performance improvements when fetching old messages -- Added 5 new default public chats for our friends speaking Korean, Chinese, Japanese, Russian and Spanish. - -### Fixed -- Prevented empty commands from being sent in chat -- Fixed minimum amounts in transactions -- Fixed an issue where contacts names may not be shown -- Removed log files from release builds -- Fixed an issue with transactions when navigating back diff --git a/Gemfile b/Gemfile deleted file mode 100644 index cdd3a6b3491..00000000000 --- a/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index b74575c5a26..00000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,154 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.0) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) - atomos (0.1.2) - babosa (1.0.2) - claide (1.0.2) - colored (1.2) - colored2 (3.1.2) - commander-fastlane (4.4.6) - highline (~> 1.7.2) - declarative (0.0.10) - declarative-option (0.1.0) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.5.0) - emoji_regex (0.1.1) - excon (0.62.0) - faraday (0.15.2) - multipart-post (>= 1.2, < 3) - faraday-cookie_jar (0.0.6) - faraday (>= 0.7.4) - http-cookie (~> 1.0.0) - faraday_middleware (0.12.2) - faraday (>= 0.7.4, < 1.0) - fastimage (2.1.3) - fastlane (2.100.1) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.3, < 3.0.0) - babosa (>= 1.0.2, < 2.0.0) - bundler (>= 1.12.0, < 2.0.0) - colored - commander-fastlane (>= 4.4.6, < 5.0.0) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (~> 0.1) - excon (>= 0.45.0, < 1.0.0) - faraday (~> 0.9) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 0.9) - fastimage (>= 2.1.0, < 3.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.21.2, < 0.22.0) - highline (>= 1.7.2, < 2.0.0) - json (< 3.0.0) - mini_magick (~> 4.5.1) - multi_json - multi_xml (~> 0.5) - multipart-post (~> 2.0.0) - plist (>= 3.1.0, < 4.0.0) - public_suffix (~> 2.0.0) - rubyzip (>= 1.2.1, < 2.0.0) - security (= 0.1.3) - simctl (~> 1.6.3) - slack-notifier (>= 2.0.0, < 3.0.0) - terminal-notifier (>= 1.6.2, < 2.0.0) - terminal-table (>= 1.4.5, < 2.0.0) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.5.7, < 2.0.0) - xcpretty (~> 0.2.8) - xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-clean_testflight_testers (0.2.0) - gh_inspector (1.1.3) - google-api-client (0.21.2) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.5, < 0.7.0) - httpclient (>= 2.8.1, < 3.0) - mime-types (~> 3.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.0) - googleauth (0.6.2) - faraday (~> 0.12) - jwt (>= 1.4, < 3.0) - logging (~> 2.0) - memoist (~> 0.12) - multi_json (~> 1.11) - os (~> 0.9) - signet (~> 0.7) - highline (1.7.10) - http-cookie (1.0.3) - domain_name (~> 0.5) - httpclient (2.8.3) - json (2.1.0) - jwt (2.1.0) - little-plugger (1.1.4) - logging (2.2.2) - little-plugger (~> 1.1) - multi_json (~> 1.10) - memoist (0.16.0) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) - mini_magick (4.5.1) - multi_json (1.13.1) - multi_xml (0.6.0) - multipart-post (2.0.0) - nanaimo (0.2.6) - naturally (2.2.0) - os (0.9.6) - plist (3.4.0) - public_suffix (2.0.5) - representable (3.0.4) - declarative (< 0.1.0) - declarative-option (< 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rouge (2.0.7) - rubyzip (1.2.1) - security (0.1.3) - signet (0.8.1) - addressable (~> 2.3) - faraday (~> 0.9) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.5) - CFPropertyList - naturally - slack-notifier (2.3.2) - terminal-notifier (1.8.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - tty-cursor (0.6.0) - tty-screen (0.6.5) - tty-spinner (0.8.0) - tty-cursor (>= 0.5.0) - uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.5) - unicode-display_width (1.4.0) - word_wrap (1.0.0) - xcodeproj (1.5.9) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.2) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.2.5) - xcpretty (0.2.8) - rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.0) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - ruby - -DEPENDENCIES - fastlane - fastlane-plugin-clean_testflight_testers - -BUNDLED WITH - 1.16.3 diff --git a/Makefile b/Makefile index db1f97fc013..1a27ab282f9 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ -.PHONY: react-native test setup +.PHONY: nix-add-gcroots clean nix-clean run-metro test release _list _fix-node-perms _tmpdir-rm +help: SHELL := /bin/sh help: ##@other Show this help @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST) @@ -8,139 +9,509 @@ help: ##@other Show this help # To add new item into help output, simply add comments # starting with '##'. To add category, use @category. GREEN := $(shell tput -Txterm setaf 2) +RED := $(shell tput -Txterm setaf 1) WHITE := $(shell tput -Txterm setaf 7) YELLOW := $(shell tput -Txterm setaf 3) RESET := $(shell tput -Txterm sgr0) HELP_FUN = \ - %help; \ - while(<>) { push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \ - print "Usage: make [target]\n\n"; \ - for (sort keys %help) { \ - print "${WHITE}$$_:${RESET}\n"; \ - for (@{$$help{$$_}}) { \ - $$sep = " " x (32 - length $$_->[0]); \ - print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \ - }; \ - print "\n"; \ - } + %help; \ + while(<>) { push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \ + print "Usage: make [target]\n\nSee STARTING_GUIDE.md for more info.\n\n"; \ + for (sort keys %help) { \ + print "${WHITE}$$_:${RESET}\n"; \ + for (@{$$help{$$_}}) { \ + $$sep = " " x (32 - length $$_->[0]); \ + print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \ + }; \ + print "\n"; \ + } +HOST_OS := $(shell uname | tr '[:upper:]' '[:lower:]') + +# This can come from Jenkins +ifndef BUILD_TAG +export BUILD_TAG := $(shell git rev-parse --short HEAD) +endif -# Main targets +# We don't want to use /run/user/$UID because it runs out of space too easily. +export TMPDIR = /tmp/tmp-status-mobile-$(BUILD_TAG) +# This has to be specified for both the Node.JS server process and the Qt process. +export REACT_SERVER_PORT ?= 5001 +# Default metro port used by scripts/run-android.sh. +export RCT_METRO_PORT ?= 8081 +# Fix for ERR_OSSL_EVP_UNSUPPORTED error. +export NODE_OPTIONS += --openssl-legacy-provider +# The path can be anything, but home is usually safest. +export KEYSTORE_PATH ?= $(HOME)/.gradle/status-im.keystore + +# Location of symlinks to derivations that should not be garbage collected +export _NIX_GCROOTS = ./.nix-gcroots +# Defines which variables will be kept for Nix pure shell, use semicolon as divider +export _NIX_KEEP ?= TMPDIR,BUILD_ENV,\ + BUILD_TYPE,BUILD_NUMBER,COMMIT_HASH,\ + ANDROID_GRADLE_OPTS,ANDROID_ABI_SPLIT,ANDROID_ABI_INCLUDE,\ + STATUS_GO_SRC_OVERRIDE,STATUS_GO_IPFS_GATEWAY_URL + +# Useful for Android release builds +TMP_BUILD_NUMBER := $(shell ./scripts/version/gen_build_no.sh | cut -c1-10) -setup: ##@prepare Install all the requirements for status-react - ./scripts/setup +#---------------- +# Nix targets +#---------------- -prepare: ##@prepare Install dependencies and prepare workspace - scripts/prepare-for-platform.sh mobile - npm install +# WARNING: This has to be located right before all the targets. +SHELL := ./nix/scripts/shell.sh +shell: export TARGET ?= default +shell: ##@prepare Enter into a pre-configured shell +ifndef IN_NIX_SHELL + @ENTER_NIX_SHELL +else + @echo "${YELLOW}Nix shell is already active$(RESET)" +endif -prepare-ios: prepare ##@prepare Install iOS specific dependencies - mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack - cd ios && pod install && cd .. +nix-repl: SHELL := /bin/sh +nix-repl: ##@nix Start an interactive Nix REPL + nix repl default.nix -prepare-android: prepare ##@prepare Install Android specific dependencies - cd android; ./gradlew react-native-android:installArchives +nix-gc-protected: SHELL := /bin/sh +nix-gc-protected: + @echo -e "$(YELLOW)The following paths are protected:$(RESET)" && \ + ls -1 $(_NIX_GCROOTS) | sed 's/^/ - /' -#---------------- -# Release builds -#---------------- -release: release-android release-ios ##@build build release for Android and iOS +nix-upgrade: SHELL := /bin/sh +nix-upgrade: ##@nix Upgrade Nix interpreter to current version. + nix/scripts/upgrade.sh -release-android: prod-build-android ##@build build release for Android - react-native run-android --variant=release +nix-gc: export TARGET := nix +nix-gc: nix-gc-protected ##@nix Garbage collect all packages older than 20 days from /nix/store + nix-store --gc -release-ios: prod-build-ios ##@build build release for iOS release - @echo "Build in XCode, see https://wiki.status.im/TBD for instructions" +nix-clean: export TARGET := default +nix-clean: ##@nix Remove all status-mobile build artifacts from /nix/store + nix/scripts/clean.sh -prod-build: - lein prod-build +nix-purge: SHELL := /bin/sh +nix-purge: ##@nix Completely remove Nix setup, including /nix directory + nix/scripts/purge.sh -prod-build-android: - rm ./modules/react-native-status/android/libs/status-im/status-go/local/status-go-local.aar 2> /dev/null || true - lein prod-build-android +nix-update-gradle: export TARGET := gradle +nix-update-gradle: export ORG_GRADLE_PROJECT_hermesEnabled := false +nix-update-gradle: export ORG_GRADLE_PROJECT_universalApk := false +nix-update-gradle: ##@nix Update maven nix expressions based on current gradle setup + nix/deps/gradle/generate.sh -prod-build-ios: - rm -r ./modules/react-native-status/ios/RCTStatus/Statusgo.framework/ 2> /dev/null || true - lein prod-build-ios +nix-update-clojure: export TARGET := clojure +nix-update-clojure: ##@nix Update maven Nix expressions based on current clojure setup + nix/deps/clojure/generate.sh -full-prod-build: ##@build build prod for both Android and iOS - ./scripts/bundle-status-go.sh ios - ./scripts/bundle-status-go.sh android - $(MAKE) prod-build - rm -r ./modules/react-native-status/ios/RCTStatus/Statusgo.framework/ 2> /dev/null || true - rm ./modules/react-native-status/android/libs/status-im/status-go/local/status-go-local.aar 2> /dev/null +nix-update-gems: export TARGET := fastlane +nix-update-gems: ##@nix Update Ruby gems in fastlane/Gemfile.lock and fastlane/gemset.nix + fastlane/update.sh -#-------------- -# REPL -# ------------- +nix-update-pods: export TARGET := ios +nix-update-pods: ##@nix Update CocoaPods in ios/Podfile.lock + cd ios && pod update -watch-ios-real: ##@watch Start development for iOS real device - clj -R:dev build.clj watch --platform ios --ios-device real +#---------------- +# General targets +#---------------- -watch-ios-simulator: ##@watch Start development for iOS simulator - clj -R:dev build.clj watch --platform ios --ios-device simulator +_fix-node-perms: SHELL := /bin/sh +_fix-node-perms: ##@prepare Fix permissions so that directory can be cleaned + $(shell test -d node_modules && chmod -R 744 node_modules) + $(shell test -d node_modules.tmp && chmod -R 744 node_modules.tmp) + +$(TMPDIR): SHELL := /bin/sh +$(TMPDIR): ##@prepare Create a TMPDIR for temporary files + @mkdir -p "$(TMPDIR)" +# Make sure TMPDIR exists every time make is called +_tmpdir-mk: $(TMPDIR) +-include _tmpdir-mk + +_tmpdir-rm: SHELL := /bin/sh +_tmpdir-rm: ##@prepare Remove TMPDIR + rm -fr "$(TMPDIR)" + +_install-hooks: SHELL := /bin/sh +_install-hooks: ##@prepare Create prepare-commit-msg git hook symlink + @ln -s -f ../../scripts/hooks/prepare-commit-msg .git/hooks +-include _install-hooks + +# Remove directories and ignored files +clean: SHELL := /bin/sh +clean: _fix-node-perms _tmpdir-rm ios-clean android-clean ##@prepare Remove all output folders + git clean -dXf + +# Remove directories, ignored and non-ignored files +purge: SHELL := /bin/sh +purge: _fix-node-perms _tmpdir-rm ##@prepare Remove all output folders + git clean -dxf + + +watchman-clean: export TARGET := watchman +watchman-clean: ##@prepare Delete repo directory from watchman + watchman watch-del $${STATUS_MOBILE_HOME} + +pod-install: export TARGET := ios +pod-install: ##@prepare Run 'pod install' to install podfiles and update Podfile.lock + cd ios && pod install; cd -- + +FLEETS_FILE := ./resources/config/fleets.json +update-fleets: ##@prepare Download up-to-date JSON file with current fleets state + curl -s https://fleets.status.im/ \ + | sed 's/"warning": "/"warning": "DO NOT EDIT! /' \ + > $(FLEETS_FILE) + echo >> $(FLEETS_FILE) + +$(KEYSTORE_PATH): export TARGET := keytool +$(KEYSTORE_PATH): + @./scripts/generate-keystore.sh + +keystore: $(KEYSTORE_PATH) ##@prepare Generate a Keystore for signing Android APKs + +fdroid-max-watches: SHELL := /bin/sh +fdroid-max-watches: ##@prepare Bump max_user_watches to avoid ENOSPC errors + sysctl fs.inotify.max_user_watches=524288 + +fdroid-nix-dir: SHELL := /bin/sh +fdroid-nix-dir: ##@prepare Create /nix directory for F-Droid Vagrant builders + mkdir -m 0755 /nix + chown vagrant /nix + +fdroid-fix-tmp: SHELL := /bin/sh +fdroid-fix-tmp: ##@prepare Fix TMPDIR permissions so Vagrant user is the owner + chown -R vagrant "$(TMPDIR)" + +fdroid-build-env: fdroid-max-watches fdroid-nix-dir fdroid-fix-tmp ##@prepare Setup build environment for F-Droud build + +fdroid-pr: export TARGET := android-sdk +fdroid-pr: ##@prepare Create F-Droid release PR +ifndef APK + $(error APK env var not defined) +endif + scripts/fdroid-pr.sh "$(APK)" -watch-android-real: ##@watch Start development for Android real device - clj -R:dev build.clj watch --platform android --android-device real +xcode-clean: SHELL := /bin/sh +xcode-clean: XCODE_HOME := $(HOME)/Library/Developer/Xcode +xcode-clean: ##@prepare Clean XCode derived data and archives + rm -fr $(XCODE_HOME)/DerivedData/StatusIm-* $(XCODE_HOME)/Archives/*/StatusIm* -watch-android-avd: ##@watch Start development for Android AVD - clj -R:dev build.clj watch --platform android --android-device avd +ios-simulator-cache-clean: SHELL := /bin/sh +ios-simulator-cache-clean: ##@prepare Clean iOS Simulator Caches + rm -rf ~/Library/Developer/CoreSimulator/Cache -watch-android-genymotion: ##@watch Start development for Android Genymotion - clj -R:dev build.clj watch --platform android --android-device genymotion +#---------------- +# Release builds +#---------------- +release: release-android release-ios ##@build Build release for Android and iOS + +build-fdroid: export BUILD_ENV = prod +build-fdroid: export BUILD_TYPE = release +build-fdroid: export ANDROID_ABI_SPLIT = false +build-fdroid: export ANDROID_ABI_INCLUDE = armeabi-v7a;arm64-v8a;x86;x86_64 +build-fdroid: export ANDROID_STORE = fdroid +build-fdroid: ##@build Build release for F-Droid + @scripts/google-free.sh + @scripts/build-android.sh + +build-android: export BUILD_ENV ?= prod +build-android: export BUILD_TYPE ?= nightly +build-android: export ORG_GRADLE_PROJECT_versionCode ?= $(TMP_BUILD_NUMBER) +build-android: ##@build Build unsigned Android APK + @scripts/build-android.sh + +release-android: export TARGET := keytool +release-android: export KEYSTORE_PATH ?= $(HOME)/.gradle/status-im.keystore +release-android: keystore build-android ##@build Build signed Android APK + @scripts/sign-android.sh result/app-arm64-v8a-release-unsigned.apk + +release-ios: export TARGET := ios +release-ios: export IOS_STATUS_GO_TARGETS := ios/arm64 +release-ios: export BUILD_ENV ?= prod +release-ios: watchman-clean ios-clean jsbundle ##@build Build release for iOS release + xcodebuild \ + -scheme StatusIm \ + -configuration Release \ + -workspace ios/StatusIm.xcworkspace \ + -destination 'generic/platform=iOS' \ + -UseModernBuildSystem=N clean archive + +jsbundle: SHELL := /bin/sh +jsbundle: export BUILD_ENV ?= prod +jsbundle: ##@build Build JavaScript and Clojurescript bundle for iOS and Android + nix/scripts/build.sh targets.mobile.jsbundle #-------------- -# Run -# ------------- -run-android: ##@run Run Android build - react-native run-android --appIdSuffix debug - -SIMULATOR= -run-ios: ##@run Run iOS build -ifneq ("$(SIMULATOR)", "") - react-native run-ios --simulator="$(SIMULATOR)" -else - react-native run-ios -endif +# status-go lib +#-------------- + +status-go-android: SHELL := /bin/sh +status-go-android: ##@status-go Compile status-go for Android app + nix/scripts/build.sh targets.status-go.mobile.android + +status-go-ios: SHELL := /bin/sh +status-go-ios: ##@status-go Compile status-go for iOS app + nix/scripts/build.sh targets.status-go.mobile.ios + +status-go-library: SHELL := /bin/sh +status-go-library: ##@status-go Compile status-go for node-js + nix/scripts/build.sh targets.status-go.library + +#-------------- +# Watch, Build & Review changes +#-------------- + +run-clojure: export TARGET := clojure +run-clojure: ##@run Watch for and build Clojure changes for mobile + yarn shadow-cljs watch mobile + +run-metro: export TARGET := clojure +run-metro: ##@run Start Metro to build React Native changes + @scripts/run-metro.sh + +export RE_FRISK_PORT ?= 4567 + +run-re-frisk: export TARGET := clojure +run-re-frisk: ##@run Start re-frisk server + yarn shadow-cljs run re-frisk-remote.core/start ${RE_FRISK_PORT} + +# TODO: Migrate this to a Nix recipe, much the same way as nix/mobile/android/targets/release-android.nix +run-android: export TARGET := android +# Disabled for debug builds to avoid 'maximum call stack exceeded' errors. +# https://github.com/status-im/status-mobile/issues/18493 +run-android: export ORG_GRADLE_PROJECT_hermesEnabled := false +run-android: export ORG_GRADLE_PROJECT_universalApk := false +run-android: ##@run Build Android APK and start it on the device + @scripts/run-android.sh + +SIMULATOR=iPhone 13 +# TODO: fix IOS_STATUS_GO_TARGETS to be either amd64 or arm64 when RN is upgraded +run-ios: export TARGET := ios +run-ios: export IOS_STATUS_GO_TARGETS := ios/arm64;iossimulator/amd64 +run-ios: ##@run Build iOS app and start it in on the simulator + @scripts/run-ios.sh "${SIMULATOR}" + +show-ios-devices: ##@other shows connected ios device and its name + xcrun xctrace list devices + +# TODO: fix IOS_STATUS_GO_TARGETS to be either amd64 or arm64 when RN is upgraded +run-ios-device: export TARGET := ios +run-ios-device: export IOS_STATUS_GO_TARGETS := ios/arm64;iossimulator/amd64 +run-ios-device: ##@run iOS app and start it on the first connected iPhone + @scripts/run-ios-device.sh #-------------- # Tests #-------------- -test: ##@test Run tests once in NodeJS - lein with-profile test doo node test once +# Get all clojure files, including untracked, excluding removed +define find_all_clojure_files +$$(comm -23 <(sort <(git ls-files --cached --others --exclude-standard)) <(sort <(git ls-files --deleted)) | grep -E '((\.clj-kondo\/status-im)|(src).*\.clj[sc]?$$)|((\.clj-kondo\/(status-im|config))|(src).*\.edn$$)|shadow-cljs\.edn') +endef + +lint: export TARGET := clojure +lint: export CLJ_LINTER_PRINT_WARNINGS ?= false +lint: ##@test Run code style checks + @sh scripts/lint/re-frame-in-quo-components.sh && \ + sh scripts/lint/direct-require-component-outside-quo.sh && \ + sh scripts/lint/require-i18n-resource-first.sh && \ + clj-kondo --config .clj-kondo/config.edn --cache false --fail-level error --lint src $(if $(filter $(CLJ_LINTER_PRINT_WARNINGS),true),,| grep -v ': warning: ') && \ + ALL_CLOJURE_FILES=$(call find_all_clojure_files) && \ + scripts/lint/translations.clj && \ + zprint '{:search-config? true}' -sfc $$ALL_CLOJURE_FILES && \ + sh scripts/lint/trailing-newline.sh && \ + node_modules/.bin/prettier --check . + +# NOTE: We run the linter twice because of https://github.com/kkinnear/zprint/issues/271 +lint-fix: export TARGET := clojure +lint-fix: ##@test Run code style checks and fix issues + ALL_CLOJURE_FILES=$(call find_all_clojure_files) && \ + zprint '{:search-config? true}' -sw $$ALL_CLOJURE_FILES && \ + zprint '{:search-config? true}' -sw $$ALL_CLOJURE_FILES && \ + clojure-lsp --ns-exclude-regex ".*/\.clj-kondo/.*|.*/src/status_im/core\.cljs|.*/src/test_helpers/component_tests_preload\.cljs$$" clean-ns && \ + sh scripts/lint/trailing-newline.sh --fix && \ + node_modules/.bin/prettier --write . + +lint-commits: + @sh scripts/lint/donotmerge-git-commits.sh + +shadow-server: export TARGET := clojure +shadow-server:##@ Start shadow-cljs in server mode for watching + yarn shadow-cljs server + +_test-clojure: export TARGET := clojure +_test-clojure: export WATCH ?= false +_test-clojure: status-go-library +_test-clojure: +ifeq ($(WATCH), true) + yarn node-pre-gyp rebuild && \ + yarn shadow-cljs compile mocks && \ + nodemon --exec "yarn shadow-cljs compile test && node --require ./test-resources/override.js $$SHADOW_OUTPUT_TO" -e cljs +else + yarn node-pre-gyp rebuild && \ + yarn shadow-cljs compile mocks && \ + yarn shadow-cljs compile test && \ + node --require ./test-resources/override.js "$$SHADOW_OUTPUT_TO" +endif -test-auto: ##@test Run tests in interactive (auto) mode in NodeJS - lein with-profile test doo node test +test: export SHADOW_OUTPUT_TO := target/test/test.js +test: export SHADOW_NS_REGEXP := .*-test$$ +test: ##@test Run all Clojure tests +test: _test-clojure + +# Note: we need to override the :output-to and :ns-regexp options because +# shadow-cljs has a bug where it will not read from the env vars to expand the +# configuration when the shadow-cljs mobile target is already running. +test-watch-for-repl: export TARGET := default +test-watch-for-repl: export SHADOW_OUTPUT_TO := target/test/test.js +test-watch-for-repl: export SHADOW_NS_REGEXP := .*-test$$ +test-watch-for-repl: ##@test Watch all Clojure tests and support REPL connections + rm -f "$$SHADOW_OUTPUT_TO" && \ + yarn install && shadow-cljs compile mocks && \ + concurrently --kill-others --prefix-colors 'auto' --names 'build,repl' \ + "yarn shadow-cljs watch test --verbose --config-merge '{:output-to \"$(SHADOW_OUTPUT_TO)\" :ns-regexp \"$(SHADOW_NS_REGEXP)\"}'" \ + "until [ -f $$SHADOW_OUTPUT_TO ] ; do sleep 1 ; done ; node --require ./test-resources/override.js $$SHADOW_OUTPUT_TO --repl" + +test-unit: export SHADOW_OUTPUT_TO := target/unit_test/test.js +test-unit: export SHADOW_NS_REGEXP := ^(?!tests\.integration-test)(?!tests\.contract-test).*-test$$ +test-unit: ##@test Run unit tests +test-unit: _test-clojure + +test-integration: export SHADOW_OUTPUT_TO := target/integration_test/test.js +test-integration: export SHADOW_NS_REGEXP := ^tests\.integration-test.*$$ +test-integration: ##@test Run integration tests +test-integration: _test-clojure + +test-contract: export SHADOW_OUTPUT_TO := target/contract_test/test.js +test-contract: export SHADOW_NS_REGEXP := ^tests\.contract-test.*$$ +test-contract: ##@test Run contract tests +test-contract: _test-clojure + +test-android: jsbundle +test-android: export TARGET := android +test-android: ##@test Android Gradle test + cd android && ./gradlew test + +test-component-watch: export TARGET := clojure +test-component-watch: export COMPONENT_TEST := true +test-component-watch: export BABEL_ENV := test +test-component-watch: export JEST_USE_SILENT_REPORTER := false +test-component-watch: ##@ Watch tests and re-run no changes to cljs files + @scripts/check-metro-shadow-process.sh + rm -rf ./component-spec + nodemon --exec 'yarn shadow-cljs compile component-test && jest --config=test/jest/jest.config.js --testEnvironment node ' -e cljs + +test-component: export TARGET := clojure +test-component: export COMPONENT_TEST := true +test-component: export BABEL_ENV := test +test-component: export JEST_USE_SILENT_REPORTER := false +test-component: ##@test Run component tests once in NodeJS + @scripts/check-metro-shadow-process.sh + rm -rf ./component-spec + yarn shadow-cljs compile component-test && \ + jest --clearCache && jest --config=test/jest/jest.config.js --testEnvironment node + +# Reference: https://flow-storm.github.io/flow-storm-debugger/user_guide.html#_debugging_react_native_applications +run-flow-storm: export SHADOW_CLJS_BUILD_ID := :mobile +run-flow-storm: export TARGET := clojure +run-flow-storm: export GDK_DPI_SCALE := 1.0 +run-flow-storm: ##@run Start FlowStorm debugger + clj -Sforce -Sdeps '{:deps {com.github.jpmonettas/flow-storm-dbg {:mvn/version "3.7.5"}}}' \ + -X flow-storm.debugger.main/start-debugger \ + :port 7888 \ + :repl-type :shadow \ + :build-id $(SHADOW_CLJS_BUILD_ID) #-------------- # Other #-------------- -react-native: ##@other Start react native packager - react-native start +geth-connect: export TARGET := android-sdk geth-connect: ##@other Connect to Geth on the device - adb forward tcp:8545 tcp:8545 + adb forward tcp:8545 tcp:8545 && \ build/bin/geth attach http://localhost:8545 -android-ports-avd: ##@other Add reverse proxy to Android Device/Simulator - adb -e reverse tcp:8081 tcp:8081 - adb -e reverse tcp:3449 tcp:3449 - adb -e reverse tcp:4567 tcp:4567 +ios-clean: SHELL := /bin/sh +ios-clean: ios-simulator-cache-clean +ios-clean: ##@prepare Clean iOS build artifacts + git clean -dxf -f target/ios + +android-clean: SHELL := /bin/sh +android-clean: ##@prepare Clean Gradle state + git clean -dxf -f ./android/app/build; \ + rm -rf android/.gradle \ + rm -rf android/build \ + rm -rf ~/.gradle + + +android-ports: export FLOWSTORM_PORT ?= 7722 +android-ports: export TARGET := android-sdk +android-ports: export RCT_METRO_PORT ?= 8081 +android-ports: ##@other Add proxies to Android Device/Simulator + adb reverse tcp:$(RCT_METRO_PORT) tcp:$(RCT_METRO_PORT) && \ + adb reverse tcp:3449 tcp:3449 && \ + adb reverse tcp:$(RE_FRISK_PORT) tcp:$(RE_FRISK_PORT) && \ + adb reverse tcp:$(FLOWSTORM_PORT) tcp:$(FLOWSTORM_PORT) && \ + adb forward tcp:5561 tcp:5561 + +android-devices: export TARGET := android-sdk +android-devices: ##@other Invoke adb devices + adb devices + +android-pull-geth: export TARGET := android-sdk +android-pull-geth: export VERSION ?= debug +android-pull-geth: + adb pull "/storage/emulated/0/Android/data/im.status.ethereum$$( [ "$(VERSION)" = "release" ] || echo ".$(VERSION)" )/files/Download/geth.log" + +android-tail-geth: export TARGET := android-sdk +android-tail-geth: export VERSION ?= debug +android-tail-geth: + adb shell 'while true; do cat; sleep 1; done < /storage/emulated/0/Android/data/im.status.ethereum$$( [ "$(VERSION)" = "release" ] || echo ".$(VERSION)" )/files/Download/geth.log' + +android-clean-geth: export TARGET := android-sdk +android-clean-geth: export VERSION ?= debug +android-clean-geth: + adb shell 'rm /storage/emulated/0/Android/data/im.status.ethereum$$( [ "$(VERSION)" = "release" ] || echo ".$(VERSION)" )/files/Download/geth.log' + + +android-logcat: export TARGET := android-sdk +android-logcat: ##@other Read status-mobile logs from Android phone using adb + adb logcat | grep -e RNBootstrap -e ReactNativeJS -e ReactNative -e StatusModule -e StatusNativeLogs -e 'F DEBUG :' -e 'Go :' -e 'GoLog :' -e 'libc :' + +android-install: export TARGET := android-sdk +android-install: export BUILD_TYPE ?= release +android-install: ##@other Install APK on device using adb + adb install result/app-$(BUILD_TYPE).apk + +_list: SHELL := /bin/sh +_list: + @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' + +patch-file: export TARGET := default +patch-file: ##@other Generates patch file for npm deps + @scripts/patch-npm-lib.sh + +#-------------- +# REPLs +#-------------- + +repl-clojure: export TARGET := clojure +repl-clojure: ##@repl Start Clojure repl for mobile App + yarn shadow-cljs cljs-repl mobile -android-ports-genymotion: ##@other Add reverse proxy to Android Device/Simulator - adb -e reverse tcp:8081 tcp:8081 - adb -e reverse tcp:3449 tcp:3449 - adb -e reverse tcp:4567 tcp:4567 +repl-nix: nix-repl ##@repl Start an interactive Nix REPL -android-ports-real: ##@other Add reverse proxy to Android Device/Simulator - adb -d reverse tcp:8081 tcp:8081 - adb -d reverse tcp:3449 tcp:3449 - adb -d reverse tcp:4567 tcp:4567 +#-------------- +# Dev Automation Flows +#-------------- +auto-login: export TARGET := default +auto-login: ##@auto runs flow for login or onboarding app on simulator/emulator + maestro test "maestro/create-account-or-login.yaml" -startdev-%: - $(eval SYSTEM := $(word 2, $(subst -, , $@))) - $(eval DEVICE := $(word 3, $(subst -, , $@))) - ${MAKE} prepare-${SYSTEM} - ${MAKE} watch-$(SYSTEM)-$(DEVICE) +auto-custom: export TARGET := default +auto-custom: ##@auto runs any custom maestro automation flow on simulator/emulator +ifndef FLOW + $(error Usage: make automate FLOW=your-maestro-flow-file.yaml) +endif + maestro test "$(FLOW)" diff --git a/README.md b/README.md index 6cc5f9591f4..46dd4decd17 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,66 @@ +> [!WARNING] +> **This repository is archived and this codebase is deprecated.** +> The new mobile codebase lives at [status-im/status-app](https://github.com/status-im/status-app). # Status - a Mobile Ethereum Operating System -Join us in creating a browser, messenger, and gateway to a decentralized world. Status is a free (libre) open source mobile client targeting Android & iOS built entirely on [Ethereum](https://ethereum.org/) technologies. That's right, no middle men and `go-ethereum` running directly on your device. +[Get it on Google Play](https://play.google.com/store/apps/details?id=im.status.ethereum) +[Get it on F-Droid](https://f-droid.org/packages/im.status.ethereum/) + + +[Get it on Github](https://github.com/status-im/status-mobile/releases) + +Join us in creating a browser, messenger, and gateway to a decentralized world. Status is a free (libre) open source mobile client targeting Android & iOS built entirely on [Ethereum](https://ethereum.org/) technologies. That's right, no middlemen and `go-ethereum` running directly on your device. [![Getting started with Status](https://i.imgur.com/C0aZZEL.jpg)](https://www.youtube.com/watch?v=oDCSEmS9c3o "Getting started with Status") ## Why? -We believe in a medium of pure free trade, economies with fair, permission-less access and a world without intermediaries. We want to create policies that can exist between friends or scale globally, we want to communicate securely and be uninhibited by legacy systems. +We believe in a medium of pure free trade, economies with fair, permission-less access and a world without intermediaries. We want to create policies that can exist between friends or scale globally, we want to communicate securely and be uninhibited by legacy systems. -We want to take responsibility for our data, the way we conduct ourselves privately and promote this way of life to a mass audience. +We want to take responsibility for our data, and the way we conduct ourselves privately and promote this way of life to a mass audience. -We want deep insights into our own economies so we can make informed, data-driven decisions on how to make our lives better. The Ethereum blockchain, Smart Contracts, Swarm and Whisper provides us a path forward. +We want deep insights into our own economies so we can make informed, data-driven decisions on how to make our lives better. The Ethereum blockchain, Smart Contracts, Swarm and Whisper provide us with a path forward. If this interests you, **help us make Status a reality** - anyone can contribute and we need everyone at any skill level to participate. ## How to Contribute? -Go straight to the [docs](https://docs.status.im) or [join our Riot](http://chat.status.im/#/register) and choose what interests you: +Go straight to the [docs](doc/) and choose what interests you: -- **Developer** -Developers are the heart of software and to keep Status beating we need all the help we can get! If you're looking to code in ClojureScript or Golang then Status is the project for you! We use React Native and there is even some Java/Objective-C too! -Want to learn more about it? Start by reading our [Developer Introduction](https://docs.status.im/docs/introduction.html) which guides you through the technology stack and start browsing [beginner issues](https://github.com/status-im/status-react/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22good%20first%20issue%22%20). Then you can read how to [Build Status](https://docs.status.im/docs/build_status.html), which talks about managing project dependencies, coding guidelines and testing procedures. +- **Developer** Developers are the heart of software and to keep Status beating +we need all the help we can get! If you're looking to code in ClojureScript or +Golang then Status is the project for you! We use React Native and there is even +some Java/Objective-C too! Want to learn more about it? Start by reading our +[Developer Introduction](doc/starting-guide.md) which guides you +through the technology stack and start browsing [beginner +issues](https://github.com/status-im/status-mobile/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22good%20first%20issue%22%20). +Then you can read how to [Build +Status](doc/starting-guide.md), which talks about managing +project dependencies, coding guidelines and testing procedures. +Check out our [coding guidelines](doc/new-guidelines.md). -- **Community Management** -Metcalfe's law states that the value of a network is proportional to the square of the number of connected users of the system - without community Status is meaningless. We're looking to create a positive, fun environment to explore new ideas, experiment and grow the Status community. Building a community takes a lot of work but the people you'll meet and long lasting relationships you form will be well worth it, check out our [Community Principles](https://wiki.status.im/Read_Our_Principles) +- **Community Management** +Metcalfe's law states that the value of a network is proportional to the square of the number of connected users of the system - without community Status is meaningless. We're looking to create a positive, fun environment to explore new ideas, experiment and grow the Status community. Building a community takes a lot of work but the people you'll meet and the long-lasting relationships you form will be well worth it, check out our [Mission and Community Principles](https://status.app/manifesto) -- **Specification / Documentation** -John Dewey once said "Education is not preparation for life; education is life *itself* ". Developers & Designers need guidance and it all starts from documentation and specifications. Our software is only as good as its documentation, head over to our [docs](https://docs.status.im) and see how you can improve what we have. +- **Specification / Documentation** +John Dewey once said, "Education is not preparation for life; education is life *itself* ". Developers and Designers need guidance and it all starts from documentation and specifications. Our software is only as good as its documentation, check out our [docs](doc/) and see how you can improve what we have. -- **Blog Writing** -Content is King, keeping our blog up to date and informing the community of news helps keep everyone on the same page. [Jump on our Riot](http://chat.status.im/#/register) and discuss with the team how you can contribute! +- **Blog Writing** +Content is King, keeping our blog up to date and informing the community of news helps keep everyone on the same page. -- **Testers** -It's bug hunting season! Status is currently in Alpha and there is sure to be a bunch of learning, [build status from scratch](https://docs.status.im/docs/build_status.html) or if an android user check out our [nightly builds](http://artifacts.status.im:8081/artifactory/nightlies-local/). You can shake your phone to submit bug reports, or start browsing our [Github Issues](http://github.com/status-im/status-react/issues). Every bug you find brings Status closer to stable, usable software for everyone to enjoy! +- **Testers** +It's bug-hunting season! Status is currently under active development and there is sure to be a bunch of learning, [build status from scratch](doc/starting-guide.md) or if an android user checks out our [nightly builds](https://status.im/nightly). You can shake your phone to submit bug reports, or start browsing our [Github Issues](https://github.com/status-im/status-mobile/issues). Every bug you find brings Status closer to stable, usable software for everyone to enjoy! -- **Security** +- **Security** Status is a visual interface to make permanent changes on the Blockchain, it handles crypto-tokens that have real value and allows 3rd party code execution. Security is paramount to its success. You are given permission to break Status as hard as you can, as long as you share your findings with the community! -- **Evangelism** -Help us spread the word! Tell a friend *right now*, in fact tell **everyone** - yell from a mountain if you have to, every person counts! If you've got a great story to tell or have some interesting way you've spread the word about Status let us know about it in our [Riot](https://chat.status.im/#/register) - -## Status API -View our [API Docs](https://docs.status.im/docs/status_web_api.html) and learn how to integrate your DApp into Status. You can read more about how to add your DApp to Status [here](https://docs.status.im/docs/status_optimized.html) - +- **Evangelism** +Help us spread the word! Tell a friend *right now*, in fact, tell **everyone** - yell from a mountain if you have to, every person counts! If you've got a great story to tell or have some interesting way you've spread the word about Status let us know about it in our [chat](https://join.status.im/chat/public/status) + ## Give me Binaries! -Currently the fastest way to get your hands on a binary is to join our Early Access by submitting your email on our [website](https://status.im) or by [building it yourself](https://docs.status.im/docs/build_status.html). - -Alternatively, if you’re on Android you can try one of our nightly APK builds from [here](http://artifacts.status.im:8081/artifactory/nightlies-local/) +You can get our Beta builds for both Android and iOS on our [website](https://status.app), through our [nightly builds](https://status.im/nightly/), or by [building it yourself](doc/starting-guide.md). ## Core Contributors @@ -59,8 +71,8 @@ Without the dedication of these outstanding individuals, Status would not exist. ## Contact us -Feel free to email us at [support@status.im](mailto:support@status.im) or better yet, [join our Riot](http://chat.status.im/#/register). +Feel free to email us at [support@status.im](mailto:support@status.im). ## License -Licensed under the [Mozilla Public License v2.0](https://github.com/status-im/status-react/blob/develop/LICENSE.md) +Licensed under the [Mozilla Public License v2.0](https://github.com/status-im/status-mobile/blob/develop/LICENSE.md) diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 00000000000..528e9860bbc --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,1336 @@ +## 1.20 Release notes + +### Features + +* Add session management for wallet connect by @cammellos in https://github.com/status-im/status-mobile/pull/13799 +* [#13181] Cannot delete images or audio messages by @flexsurfer in https://github.com/status-im/status-mobile/pull/13210 +* [#13230] Show default syncing interval by @rasom in https://github.com/status-im/status-mobile/pull/13234 +* Add support for mutual contact requests by @cammellos in https://github.com/status-im/status-mobile/pull/13054 +* Implement Information box and ens banner view by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13753 +* Profile picture sync by @siphiuel in https://github.com/status-im/status-mobile/commit/fbfab9fd3bdbcfa6cdd9a24a6bce526da8252fd3 + +### Improvements + +* Improve notifications life-cycle by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13248 +* Simpledapp on Goerli by @churik in https://github.com/status-im/status-mobile/pull/13732 +* Include share action on longpress image menu by @jo-mut in https://github.com/status-im/status-mobile/pull/13359 +* fast image loading indicator by @flexsurfer in https://github.com/status-im/status-mobile/pull/13431 +* [#13355] Long time (5-15 sec) for loading chat messages by @flexsurfer in https://github.com/status-im/status-mobile/pull/13425 +* [Fixes: #13416] resize image in status-go as well, alert user if fails by @cammellos in https://github.com/status-im/status-mobile/pull/13422 +* [#12566] Remove legacy dependency on mailserver password by @erikseppanen in https://github.com/status-im/status-mobile/pull/13511 +* Wallet sync for generated accounts by @siphiuel in https://github.com/status-im/status-mobile/pull/13487 +* feat: lottie splash screen by @yqrashawn in https://github.com/status-im/status-mobile/pull/13714 +* [#13647] Update all settings on pairing with a new wallet account by @rasom in https://github.com/status-im/status-mobile/pull/13742 +* Improve ethereum.send by @qfrank in https://github.com/status-im/status-mobile/pull/13724 + +### Redesign + +* quo, Switcher Navigation by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13167 +* Switcher and Bottom Tabs Animations and UI Performance Improvements by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13470 +* feat: add token tag component (#13599) by @J-Son89 in https://github.com/status-im/status-mobile/pull/13644 +* [13565] icon-avatar component by @ibrkhalil in https://github.com/status-im/status-mobile/pull/13692 +* [13562] Wallet user avatar component by @ibrkhalil in https://github.com/status-im/status-mobile/pull/13681 +* Reply component in chat input & quoted messages redesign by @briansztamfater in https://github.com/status-im/status-mobile/pull/13706 +* Wallet redesign by @flexsurfer in https://github.com/status-im/status-mobile/pull/13172 +* [Potential: #13596] Add status-tags component by @cammellos in https://github.com/status-im/status-mobile/pull/13597 +* Chat views redesign by @briansztamfater in https://github.com/status-im/status-mobile/pull/13184 +* Implementation of React Native Blur view for android by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13663 +* feat: add token overview component (status-im#13555) by @J-Son89 in https://github.com/status-im/status-mobile/pull/13767 + +### Bug Fixes + +* fix screen orientation on android by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13218 +* Don't unmute contact while adding by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13237 +* fix chat image long press issue by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13371 +* [#13149] Timeline images stretch off screen by @siddarthkay in https://github.com/status-im/status-mobile/pull/13211 +* [#13233] Proper message after clearing history in pubchat by @rasom in https://github.com/status-im/status-mobile/pull/13241 +* [#13235] Fix opening of the last shown chat with local notifications by @rasom in https://github.com/status-im/status-mobile/pull/13252 +* [#13201] Fix handling of favourits syncing by @rasom in https://github.com/status-im/status-mobile/pull/13253 +* [#13257] Fix sync devices sorting by @rasom in https://github.com/status-im/status-mobile/pull/13282 +* [#13283] Fix syncing of adding contact after removal by @rasom in https://github.com/status-im/status-mobile/pull/13292 +* [#13263] Fix for ENS warning showing on custom domains by @daferna in https://github.com/status-im/status-mobile/pull/13312 +* fix can't share deep link from profile on ios by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13420 +* Fix sending logs from iOS by @bitgamma in https://github.com/status-im/status-mobile/pull/13619 +* fix #13642: Cannot sign in on claim.lens.xyz by @qfrank in https://github.com/status-im/status-mobile/pull/13668 +* [#13493] Can not switch to xDai chain by @flexsurfer in https://github.com/status-im/status-mobile/pull/13709 +* fix bookmarks not syncing by @Parveshdhull in https://github.com/status-im/status-mobile/pull/13727 +* fix: set max option not work when send from qrcode by @yqrashawn in https://github.com/status-im/status-mobile/pull/13733 +* [#13835] Fix Generate keys button by @rasom in https://github.com/status-im/status-mobile/pull/13841 +* Fix disappearing of the last notification when paired by @rasom in https://github.com/status-im/status-mobile/pull/13796 +* [#13766] custom networkid fix by @ flexsurfer in https://github.com/status-im/status-mobile/pull/13765 + +## New Contributors +* @siddarthkay made their first contribution in https://github.com/status-im/status-mobile/pull/13185 +* @daferna made their first contribution in https://github.com/status-im/status-mobile/pull/13312 +* @smohamedjavid made their first contribution in https://github.com/status-im/status-mobile/pull/13364 +* @ibrkhalil made their first contribution in https://github.com/status-im/status-mobile/pull/13450 +* @PEZ made their first contribution in https://github.com/status-im/status-mobile/pull/13494 +* @erikseppanen made their first contribution in https://github.com/status-im/status-mobile/pull/13511 +* @Rende11 made their first contribution in https://github.com/status-im/status-mobile/pull/13609 +* @yqrashawn made their first contribution in https://github.com/status-im/status-mobile/pull/13739 +* @ilmotta made their first contribution in https://github.com/status-im/status-mobile/pull/13747 +* @phrohdoh made their first contribution in https://github.com/status-im/status-mobile/pull/13772 +* @J-Son89 made their first contribution in https://github.com/status-im/status-mobile/pull/13767 + +**Full Changelog**: https://github.com/status-im/status-mobile/compare/1.19.0...1.20.0 + +## 1.19 Release notes + +### Features + +* 💬 Re-open app on the last open chat ([details](https://github.com/status-im/status-mobile/issues/12782)) +* 🪙 Support \$UBI token ([details](https://github.com/status-im/status-mobile/issues/12975)) +* 🔔 Implementation of remote android notifications ([details](https://github.com/status-im/status-mobile/issues/12866)) +* 🔁 Sync clear history on paired devices ([details](https://github.com/status-im/status-mobile/issues/13004)) +* 📑 Synchronize browser bookmarks/favorites across devices ([details](https://github.com/status-im/status-mobile/issues/12961)) +* 🔕 Add mute option to android ([details](https://github.com/status-im/status-mobile/issues/12890)) +* 👛 Wallet Connect 2.0 integration ([details](https://github.com/status-im/status-mobile/issues/12546)) +* 💰 Implement Wallet Connect 1.0 ([details](https://github.com/status-im/status-mobile/issues/12870)) +* ⚙️ Sync settings ([details](https://github.com/status-im/status-mobile/pull/13053)) + +### Improvements + +* Improve order of mailserver requests ([details](https://github.com/status-im/status-mobile/issues/12783)) +* Load images from an https server ([details](https://github.com/status-im/status-mobile/issues/12855)) +* Load audio messages from https server ([details](https://github.com/status-im/status-mobile/issues/13116)) +* Show message timestamps on tap, remove from bubbles 🕙 ([details](https://github.com/status-im/status-mobile/pull/12915)) +* support PUK and seed recovery during onboarding ([details](https://github.com/status-im/status-mobile/pull/12955)) +* update react-native-webview ([details](https://github.com/status-im/status-mobile/pull/12987)) +* Add group members to mentionable list ([details](https://github.com/status-im/status-mobile/pull/12994)) +* always use english for fallback text ([details](https://github.com/status-im/status-mobile/pull/13019)) +* Disable chat input for chats with a blocked user ([details](https://github.com/status-im/status-mobile/pull/13162)) +* Handle timeouts on mailserver requests ([details](https://github.com/status-im/status-mobile/issues/13022)) +* Human Readable data display when signing using eth_signTypedData_v3 ([details](https://github.com/status-im/status-mobile/issues/12535)) + +### Bug Fixes + +* Bug/fix opensea api ([details](https://github.com/status-im/status-mobile/pull/13021)) +* Show "Fetch more messages" only in public chats and communities ([details](https://github.com/status-im/status-mobile/pull/12927)) +* Chats are not synced if delete and re-add them on the first device while the second device is offline ([details](https://github.com/status-im/status-mobile/issues/12913)) +* fix distortion of app theme due to change in system theme ([details](https://github.com/status-im/status-mobile/pull/12934)) +* Unable to perform swap on 1inch.exchange if initial swap was cancelled by closing bottom sheet ([details](https://github.com/status-im/status-mobile/issues/12920)) +* Error when trying to edit 'per-gas price limit' on 1inch.exchange ([details](https://github.com/status-im/status-mobile/issues/12991)) +* Allow adding emoji in status input even if it exceeds limit ([details](https://github.com/status-im/status-mobile/pull/12949)) +* Allow adding emoji to group intro message if it exceeds limit ([details](https://github.com/status-im/status-mobile/pull/13024)) +* Use higher base fee value for default tx fee calculation ([details](https://github.com/status-im/status-mobile/pull/13020)) +* Profile QR code overlaps share menu during sharing (IOS) ([details](https://github.com/status-im/status-mobile/pull/13050)) +* No value in 'Gas amount limit' field in NFT dapps ([details](https://github.com/status-im/status-mobile/pull/13051)) +* Stop showing fetch more button for cleared history ([details](https://github.com/status-im/status-mobile/pull/13084)) +* fix ens resolve issue while following deep link ([details](https://github.com/status-im/status-mobile/pull/13080)) +* Fix deadlock on syncing all data ([details](https://github.com/status-im/status-mobile/pull/13128)) +* Fix broken username and timestamp layout for long usernames ([details](https://github.com/status-im/status-mobile/pull/13174)) + +## 1.18.0 Release notes :package: + +### Features +* 🚦Online status indicator +* 👛 Add possibility to select wallet for ENS names +* 🐞 Bug reporting and logs sharing improvements +* 🍳 Simplify Keycard onboarding by using defaults for pairing password +* 💾 Backup contacts via Waku +* 🪙 Added ENS token +* 🌠 Using react-native-fast-image for stickers +#### Message reliability improvements +* 𝌗 Show loading until all messages are processed +* 💪🏼 More robust message confirmations handling +* 📶 Allow sending messages when offline +#### Improvements on device pairing +* 🔔💻 Notification in activity centre now sync across devices +* ✅💻 Sync read messages across devices +#### Network settings improvements +* ✨ Add Binance Smart chain +* ♏︎ Added custom network symbol +* 💰 Network fee on xDai chain +* 📇 Support tx history (in a way) when on BSC chains +* 🙅🏻 Remove POA network from the list + +### Bug Fixes +* Fix activity center community invite preview +* Missing notifications for some contacts +* Fixes scrolling issues on the 'Max priority fee' bottom sheet +* Handle Keycard timeout on iOS 15 +* Allow user to be reinvited to the same group after leaving +* Fix for "TypeError: Network request failed" when switching to Goerli network +* Fix for Spin of death on adding custom NFT token contract +* Changing screen orientation results in breaking images / layout + + +## 1.17 + +### For iOS and Android + +### Features + +- 🖼️ Add ability to Share image from Status ([details](https://github.com/status-im/status-mobile/pull/12528)) +- 🕵️ Private Profile Photos ([details](https://github.com/status-im/status-mobile/pull/11768)) +- 🥥 Custom Emoji Thumbnails for Community Channels ([details](https://github.com/status-im/status-mobile/pull/12594)) +- 🔄 Sync deleted chats ([details](https://github.com/status-im/status-mobile/pull/12662)) +- 🔃 Sync removed/deleted/blocked contacts ([details](https://github.com/status-im/status-mobile/pull/12541)) +- 💸 Allow overriding nonce when sending transaction ([details](https://github.com/status-im/status-mobile/pull/12679)) +- 🗑️ Delete messages ([details](https://github.com/status-im/status-mobile/pull/12390)) +- 🎨 NFT Support via OpenSea ([details](https://github.com/status-im/status-mobile/pull/12485)) +- 🤳🏻 Enable collectibles and NFT as profile picture ([details](https://github.com/status-im/status-mobile/pull/12615)) + +### Improvements + +- Move pull to refresh to the account overview screen ([details](https://github.com/status-im/status-mobile/pull/12479)) +- Add $RARE token to our default list ([details](https://github.com/status-im/status-mobile/pull/12521)) +- Small UI Design Improvements ([details](https://github.com/status-im/status-mobile/pull/12544)) +- Chats list performance ([details](https://github.com/status-im/status-mobile/pull/12554)) +- Add push notification on group chat invite ([details](https://github.com/status-im/status-mobile/pull/12440)) +- Contacts performance ([details](https://github.com/status-im/status-mobile/pull/12643)) +- Add profile image in push notifications ([details](https://github.com/status-im/status-mobile/pull/12427)) +- Enable UI for signing legacy txs on networks without eip1559 ([details](https://github.com/status-im/status-mobile/pull/12692)) +- Doubled the character limit on timeline posts ([details](https://github.com/status-im/status-mobile/commit/a9333ad52ca71f512d5306f802927d0c20d63944)) + +### Bugfixes + +- Don't show notifications from self ([details](https://github.com/status-im/status-mobile/pull/12505)) +- Request permissions before saving images ([details](https://github.com/status-im/status-mobile/pull/12516)) +- Re-enable amount/fee validation on tx signing ([details](https://github.com/status-im/status-mobile/pull/12537)) +- Fix browser history sorting ([details](https://github.com/status-im/status-mobile/pull/12497)) +- Fix show/hide password on Android ([details](https://github.com/status-im/status-mobile/pull/12536)) +- Fix connecting autofarm.network dapps with status wallet freezes ([details](https://github.com/status-im/status-mobile/pull/12529)) +- RPC URL is lower-cased when entered in URL field when adding ([details](https://github.com/status-im/status-mobile/pull/12574)) +- Allow retrieving of profile updates after removing contact ([details](https://github.com/status-im/status-mobile/pull/12581)) +- Fetch history on receiving pubchat installation ([details](https://github.com/status-im/status-mobile/pull/12644)) +- Can't send funds to multisig - Out of gas ([details](https://github.com/status-im/status-mobile/pull/12648)) +- Fix handling errors on eth_gasPrice ([details](https://github.com/status-im/status-mobile/pull/12710)) + +## 1.16 + +### For iOS and Android + +### Features + +- EIP-1159 support ([#12369](https://github.com/status-im/status-mobile/pull/12369)) +- Hide/show option for accounts in the wallet ([#12438](https://github.com/status-im/status-mobile/pull/12438)) +- Delete accounts in wallet ([#12444](https://github.com/status-im/status-mobile/pull/12444)) +- Enable keeping database when migrating account to keycard ([#12306](https://github.com/status-im/status-mobile/pull/12306)) +- Replies from group chat in activity centre ([#12266](https://github.com/status-im/status-mobile/pull/12266)) + +### Improvements + +- Remove clear history from non-public chats ([#12384](https://github.com/status-im/status-mobile/pull/12384)) +- Clearing push notifications on read (Android) ([#12464](https://github.com/status-im/status-mobile/pull/12464)) +- Enable migration to keycard from signed-in state ([#11778](https://github.com/status-im/status-mobile/issues/11778)) + +### Bug Fixes + +#### Chat + +- Fix Error when opening 1-1 chat in Activity center for blocked user ([#12399](https://github.com/status-im/status-mobile/pull/12399)) +- Long press is long and it makes the message disappear entirely ([#12445](https://github.com/status-im/status-mobile/pull/12445)) +- Sort pinned messages by time of pinning ([#12402](https://github.com/status-im/status-mobile/pull/12402)) +- Screens with overlapping long usernames ([#12471](https://github.com/status-im/status-mobile/pull/12471)) + +#### Browser (Dapp) + +- Fix some token swap errors in Uniswap ([#12417](https://github.com/status-im/status-mobile/pull/12417)) +- Update react-native-webview for better SSL handling ([#12409](https://github.com/status-im/status-mobile/pull/12409)) + +#### Keycard + +- App freezes until reopening when signing tx with frozen Keycard ([#12473](https://github.com/status-im/status-mobile/pull/12473)) + +#### Wallet + +- Allow to add assets with same name ([#12466](https://github.com/status-im/status-mobile/pull/12466)) +- Update DATAcoin name ([#12454](https://github.com/status-im/status-mobile/pull/12454)) + +## 1.15 + +### For iOS and Android +### Features +- New improved native navigation, its very fast :dash: [[details](https://github.com/status-im/status-mobile/pull/12141)] +- Pinned messages in chats :pushpin: [[details](https://github.com/status-im/status-mobile/issues/11699)] +- Add support for ETH2x-FLI ERC20 token ⚫ [[details](https://github.com/status-im/status-mobile/issues/11885)] +- Add support for HEZ ERC20 token ⚫ [[details](https://github.com/status-im/status-mobile/issues/12233)] +- Show number of unread mentions in public and communities chats :bell: [[details](https://github.com/status-im/status-mobile/issues/12061)] +- QR code scanner for browser :ticket: [[details](https://github.com/status-im/status-mobile/issues/12195)] +- Edit messages after sending :pencil2: [[details](https://github.com/status-im/status-mobile/issues/12067)] +- Allow password reset :abcd: [[details](https://github.com/status-im/status-mobile/pull/12245)] + +### Improvements +- Native modals and tabs :house_with_garden: +- Improved time to show login screen :timer_clock: +- New, human friendly, terms of use :pencil: [[details](https://github.com/status-im/status-mobile/issues/12206)] + +### Bug Fixes +- No contact request in Activity Center after 1-1 chat deleted of non-contact user :no_good: [[details](https://github.com/status-im/status-mobile/issues/12218)] + +## 1.14 + +### For iOS and Android +### Features + +- Highlight posts that mention you ([details](https://github.com/status-im/status-mobile/pull/12146)) +- Remove PUK and Pairing Code screen from Keycard onboarding + [details](https://github.com/status-im/status-mobile/pull/12155)) +- Added unread messages count for messages with mentions in public chats ([details](https://github.com/status-im/status-mobile/pull/12152)) +- Sync history setting allowing to sync chat history up to 1 month ([details](https://github.com/status-im/status-mobile/pull/12166)) + + +### Improvements + +- "My profile" button for simplification of finding own public key ([details](https://github.com/status-im/status-mobile/pull/12044)) +- Factory reset option is available on account creation and recovery flows ([details](https://github.com/status-im/status-mobile/pull/12055)) +- Allow the creation of recovery/backup Keycard from the login interface ([details](https://github.com/status-im/status-mobile/pull/12115)) +- Reset blocked and/or frozen Keycards ([details](https://github.com/status-im/status-mobile/pull/12127)) +- Show cached balance ([details](https://github.com/status-im/status-mobile/pull/12134)) + + +### Bugfixes + +- Pending transactions are stored in the database and shown on history screen after re-login ([details](https://github.com/status-im/status-mobile/pull/12032)) +- Multiple fixes for keycard +- Fixes for resolving ENS name from chat key ([details](https://github.com/status-im/status-mobile/pull/12100)) + + +## 1.13 + +### For iOS and Android +### Features + +- New activity manager https://github.com/status-im/status-mobile/issues/11958 +New chats and group chat invitation will appear in a new section, the activity center, where a user will be able to accept or decline the request. +https://www.figma.com/file/r59kCQxl6tlC0dHkcAJOpP/Action-center-MVP---Mobile?node-id=2%3A8012 (not fully implemented) + +- Receive messages from contacts only option +Under privacy in settings a user will be able to select whether they want to receive messages and group chat invitations from contacts only. + +- Added delivered confirmation for private groups +Messages sent to private groups will now be marked as confirmed if at least one of the members received it. + +- New keycard functionalities: reset a keycard, changing PUK of a card, changing pairing code, create a new keycard backup card. + + +### Improvements + +- Improved UI for starting a new private chat + + +### Bugfixes + +- Fixed issue with crypto on ramp and requiring camera access +- Removed Wyre from list of services in buy crypto feature +- Added support for SOCKS token + + + +## 1.12 +### iOS +#### Excerpt +V1.12 Release - Keycard on iOS, Crypto Onramps, and More + +Keycard can now be used on iOS devices for hardware enforced security to your keys and accounts! Status v1.12 also introduces simpler access to obtaining crypto via onramps providers as well as some key updates to the messaging experience + +##### Added +Keycard iOS integration +Link previews for our.status.im and medium.om +List of Crypto onramps +Add “delivered” message confirmations to 1:1 messages +Keycard banner on login screen + +#### Fixed +UI issue showing up a chat on a paired device when it should not +UI issue was showing messages as pending if no one is part of the group chat* +Push notification is generated on paired account when you send message to 1-1 +UI issue for wrong notification when using two paired devices with same accounts +Improve keycard login time + +### Android +#### Excerpt +V1.12 Release - Crypto Onramps and Improved Messaging + +Status v1.12 introduces simpler access to obtaining crypto via onramp providers as well as some key updates to the messaging experience including delivered message confirmations in 1:1 private chats + +##### Added +Link previews for our.status.im and medium.om +List of Crypto onramps +Add “delivered” message confirmations to 1:1 messages +Keycard banner on login screen +Local notifications for Mentions + +#### Fixed +UI issue showing up a chat on a paired device when it should not +UI issue was showing messages as pending if no one is part of the group chat* +Push notification is generated on paired account when you send message to 1-1 +UI issue for wrong notification when using two paired devices with same accounts +Improve keycard login time + +## 1.11 +### iOS +#### Excerpt +V1.11 also makes some improvements to the overall Status experience. Improved compatibility with DApps creates a more seamless experience with your favorite DeFi, NFT, and other decentralized applications. Improvements to Status Nodes and fixes to Android notifications have been made for better performance with private, group chats and public chats. + +Update in the App Store or Google Play if you do not have auto updates enabled. + +For the full changelog, see Github https://github.com/status-im/status-mobile/milestone/49?closed=1 + +#### Added +- Add Giphy url support in chat +- The Graph (GRT) erc-20 token added to the default list + +#### Changed +- Improved compatibility with DApps +- Sync with Status Nodes improvements + +#### Fixed +- Fixes to Android notifications +- UI fixes +- Sluggish performance on private group chats bug + +### Android +#### Excerpt +V1.11 also makes some improvements to the overall Status experience. Improved compatibility with DApps creates a more seamless experience with your favorite DeFi, NFT, and other decentralized applications. Improvements to Status Nodes and fixes to Android notifications have been made for better performance with private, group chats and public chats. + +Update in the App Store or Google Play if you do not have auto updates enabled. + +For the full changelog, see Github https://github.com/status-im/status-mobile/milestone/49?closed=1 + +#### Added +- Migrate existing account to Keycard +- Add Giphy url support in chat +- The Graph (GRT) erc-20 token added to the default list + +#### Changed +- Improved compatibility with DApps +- Sync with Status Nodes improvements + +#### Fixed +- Fixes to Android notifications +- UI fixes +- Sluggish performance on private group chats bug + +## 1.10 +### iOS +#### Excerpt +Status version 1.10 is here and it is a perfect way to kick off 2021! Another feature rich update that makes the overall messaging, browsing, and transacting experience a whole lot better. Save your most frequently used DApps as favorites for one tap access to DeFi, NFTs, and more. Set a profile image to express yourself in chat. Speed up and cancel pending transactions and pull down to check for new transactions. + +#### Added +- Pull to refresh on individual account view +- Save dapps as favorite +- Switch between dapps in multiple tabs +- Banner to find dapps to buy crypto +- Sent message confirmation icon + +#### Changed +- Manage dapp permissions without leaving the browser +- Keycard pairing code updated to readible 5 symbol code +- Profile and settings back to most right position in tab bar + +#### Fixed +- Visibility of tab bar in dark mode +- Watching accounts for transaction in chat messages (update status) +- Multiple notifications for a single erc20 transfer +- Change group chat name when '/' character is included +- Show transaction in chat for small amounts +- Correction to transaction status in chat +- Rotate mailservers on error return +- Remove unused transport security entries +- Send chat messages in order so that text is always below image +- Performance of list rendering +- Alignment and image flickering issues in status updates +- Disallow multiple pop ups to be shown at the same time +- Add multiple custom history nodes without relogin + + +### Android +#### Excerpt +Status version 1.10 is here and it is a perfect way to kick off 2021! Another feature rich update that makes the overall messaging, browsing, and transacting experience a whole lot better. Bookmark your most frequently used DApps for one tap access to DeFi, NFTs, and more. Set a profile image to express yourself in chat. Speed up and cancel pending transactions and pull down to check for new transactions. + +#### Added +- Pull to refresh on individual account view +- Save dapps as favorite +- Switch between dapps in multiple tabs +- Banner to find dapps to buy crypto +- Sent message confirmations + +#### Changed +- Install with deeplink to public chat (Join to fetch messages) +- Manage dapp permissions without leaving the browser +- Keycard pairing code updated to readible 5 symbol code +- Profile and settings back to most right position in tab bar + +#### Fixed +- Visibility of tab bar in dark mode +- Watching accounts for transaction in chat messages (update status) +- Multiple notifications for a single erc20 transfer +- Change group chat name when '/' character is included +- Show transaction in chat for small amounts +- Correction to transaction status in chat +- Rotate mailservers on error return +- Remove unused transport security entries +- Send chat messages in order so that text is always below image +- Performance of list rendering +- Alignment and image flickering issues in status updates +- Disallow multiple pop ups to be shown at the same time +- Add multiple custom history nodes without relogin + + +## 1.9 +### iOS +#### Excerpt +With release 1.9, Status gives you the tools to better connect and engage with your friends and communities. You'll find a whole new tab for Status updates. Share what's on your mind and follow what your contacts are up to. + +Status updates, and any chat, are now more colorful with opt in link previews. + +#### Full + +#### Added +* Profile status updates +* Add unfurling of URLs in chats (YouTube) +* Onboarding into public chats + +#### Changed +* Add backwards compatibility for ENS Usernames +* Account cards design to accomodate high value accounts +* Rename Status account to Ethereum account for first account on onboarding +* Remove manual fetch 24h in bottom sheet +* Pinch to zoom on images + +#### Fixed +* Personal sign method +* Slow asset list +* Loading of older transactions (< April) +* Loading of message history after rejoining a public chat +* Unread message counter update when other user replies to own message +* App crash on navigation from and to generate keys in onboarding + + +### Android +#### Excerpt +With release 1.9, Status gives you the tools to better connect and engage with your friends and communities. You'll find a whole new tab for Status updates. Share what's on your mind and follow what your contacts are up to. + +Status updates, and any chat, are now more colorful with opt in link previews. + +#### Full + +#### Added +* Profile status updates +* Add unfurling of URLs in chats (YouTube) +* In-app local notifications for transactions(Beta) +* Onboarding into public chats + + +#### Changed +* Add backwards compatibility for ENS Usernames +* Account cards design to accomodate high value accounts +* Rename Status account to Ethereum account for first account on onboarding +* Remove manual fetch 24h in bottom sheet +* Pinch to zoom on images + +#### Fixed +* Personal sign method +* Slow asset list +* Loading of older transactions (< April) +* Loading of message history after rejoining a public chat +* Unread message counter update when other user replies to own message +* App crash on navigation from and to generate keys in onboarding + + + +## 1.8 +see https://notes.status.im/release-1.8-notes# +### Android + iOS +#### Excerpt + +#### Full +##### V1.8 Release - More people, more private & bug fixes + +This release includes updates that further improve private communication and your control over it. Group chats can now include up to 20 members (previously 10). With this increase, group chat preserves it's confidentiality, e2e encryption, and perfect forward secrecy that come with 1:1 private chats; Now usable with larger groups, without negatively impacting the network. Truly private group chats. + +Additionally, you can now opt out of 'History nodes' under Sync settings. With this option you control if you are ok sharing your IP address with a server in order to collect messages send while you were offline. In case you didn't know, you can also [run your own history node](https://status.im/technical/run_status_node.html) to connect to. + +Finally, when you receive a new ERC-20 token you can now add it to your wallet by tapping 'Scan tokens'. + + + +#### Added +* Increased private group chat size to 20 +* Allow users not to connect to history nodes +* Added scan button to fetch ERC-20 tokens + +#### Fixed +* Fix browser issues with logging in on github.com +* Fix bug with mentions not resolved in messages with markdown +* Fix blank view when open yearn.finance +* Fix entering “0x” in recipient field + +Update in the [App Store](https://apps.apple.com/us/app/status-private-communication/id1178893006) or [Google Play](https://play.google.com/store/apps/details?id=im.status.ethereum) if you do not have auto updates enabled. + +The APK available is [here](https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Mobile-v1.8.0.apk). + +For the full changelog, see our [Github](https://github.com/status-im/status-mobile/commits/release/1.8.x). +> [Verify APK link before publishing blog post] +> + +## 1.7.2 + +This release fixes an issue with fetching ERC20 balance. It was due to a contract that has been migrated to a different address which cause the whole request to fail. + + +## 1.7.1 +Full release commits: https://github.com/status-im/status-mobile/commits/release/1.7.1 +### Android + iOS +#### Excerpt +Several users have reported an issue in upgrading from release 1.6.1 to release 1.7. This update fixes the issue. + +#### Full +Several users have reported an issue in upgrading from release 1.6.1 to release 1.7. This update fixes the issue. + +The issue occurs when the application is killed during a migration that is part of the update. Killing the app causes the migration to fail and leaves a persistent database error. This error prevents users from unlocking their profile; Showing an error on password entry. The fix provided above resolves this issue by restarting the migration. It also prevents the issue from occuring if you haven’t updated yet. + +So far we’ve only received reports on this issue occurring on Android. We’re monitoring closely to see if further action is required and will take action to prevent this issue from occuring in future updates. Many thanks to our ambassador tbenr and others who reported this issue. + +#### Fixed +Retry failed migration + + +## 1.7 +Full release commits: https://github.com/status-im/status-mobile/commits/release/1.7.x + +### Android +#### Excerpt (446 characters including spaces) +Introducing the ability to mention and be mentioned (Beta). A host of other updates make it easier to recognize people and addresses. Status uses the infamous animal random names by default to provide pseudo-anonymity. You can now give 'Trusting Honeydew Panda' a nickname to recognize them. When sending transactions you can save and select favorite addresses. A safeguard will warn you when sending funds to a contract to prevent loss of funds. + +#### Full +Introducing the ability to mention and be mentioned. Note that this feature is in Beta, your feedback is much appreciated. Aside from Mentions, this release includes a host of other updates that are all about making it easier for you to recognize people and addresses. Status uses the infamous animal random names by default to provide pseudo-anonymity. While your best friend might be the 'Trusting Honeydew Panda' you can now give them a nickname to recognize them. Sending transactions got easier with the new feature to save and select favorite addresses. A safeguard will warn you when sending funds to a contract to prevent loss of funds. + + +#### Added +* Mentions (Beta) +* Local nicknames +* Wallet address selection (favorites, contacts, recent) +* Private group chat invite links +* Warning when attempting to send a transaction to a contract +* New tokens: UNI, COMP, BAL, AKRO, OXT, aUSDC +* Collapse long messages for more readible threads + +#### Changed +* Removed the ability to enable using Whisper instead of Waku + +#### Fixed +* Upgrade phishing detection library +* Decimal rounding when using 'Set max' +* ENS resolving for IPNS names +* SUPR support for old and new token contract + + + + +### iOS +#### Excerpt (496 characters incl spaces) +Introducing privacy preserving notifications on iOS. Paired with the ability mention and be mentioned (Beta). A host of other updates make it easier to recognize people and addresses. Status uses the infamous animal random names by default to provide pseudo-anonymity. You can now give 'Trusting Honeydew Panda' a nickname to recognize them. When sending transactions you can save and select favorite addresses. A safeguard will warn you when sending funds to a contract to prevent loss of funds. + +#### Full +Introducing privacy preserving, remote notifications on iOS. Paired with the ability mention and be mentioned. Note that this feature is in Beta, your feedback is much appreciated. Aside from Mentions, this release includes a host of other updates that are all about making it easier for you to recognize people and addresses. Status uses the infamous animal random names by default to provide pseudo-anonymity. While your best friend might be the 'Trusting Honeydew Panda' you can now give them a nickname to recognize them. Sending transactions got easier with the new feature to save and select favorite addresses. A safeguard will warn you when sending funds to a contract to prevent loss of funds. + +#### Added +* (Remote) Notifications on iOS +* Mentions (Beta) +* Local nicknames +* Wallet address selection (favorites, contacts, recent) +* Private group chat invite links +* Warning when attempting to send a transaction to a contract +* New tokens: UNI, COMP, BAL, AKRO, OXT, aUSDC +* Collapse long messages for more readible threads +#### Changed +* Removed the ability to enable using Whisper instead of Waku + +#### Fixed +* Upgrade phishing detection library +* Decimal rounding when using 'Set max' +* ENS resolving for IPNS names +* SUPR support for old and new token contract + + +## 1.6.1 +### Android + iOS +#### Excerpt +Introducing a spam mitigation +#### Full +Introducing a spam mitigation +##### Fixed +- Restrict message size on input +- Drop messages based on character length + + +## 1.6 +### Android +#### Excerpt +#### Full +This release includes a small, but important change to existing functionality: Onboard using a 'Referral URL'. You can confirm that you discovered the app through a partner of Status (Opt-in only). It also includes a fix to a persistant bug that resulted in messages not being marked as seen. + +##### Changed +- Attribute a referrer when installing and onboarding to the app using a Referral URL + +##### Fixed +- Mark 1:1 messages as seen (Badge kept reappearing when tapping 'Add as contact') + +### iOS +#### Excerpt +#### Full +This release includes a fix to a persistant bug that resulted in messages not being marked as seen as well as an issue that caused the app to crash. +##### Fixed +- Mark 1:1 messages as seen (Badge kept reappearing when tapping 'Add as contact') +- Crash on iOS when device passcode is switched off and 'Save password' is enabled inside the app + + +## 1.5 +### Excerpt (493 characters excl url) +Now including those features that make you feel connected while being miles apart. If there was a time you wondered if you could chat with friends and family, without giving up your privacy or security, wonder no more. Send images, audio messages, and show your love with emoji reactions ❤️; Not a word is sent to the cloud. #appdown is not a thing. E2E encryption is. If you have Internet you can send and receive messages. Securely. Privately. Always. +For the full changelog, see our Github + +### Full +Now including those features that make you feel connected while being miles apart. If there was a time you wondered if you could chat with friends and family, without giving up your privacy or security, wonder no more. Send images, audio messages, and show your love with emoji reactions ❤️; Not a word is sent to the cloud. #appdown is not a thing. E2E encryption is. If you have Internet you can send and receive messages. Securely. Privately. Always. + +Making new friends on open communities is now easier as well. We took the liberty of surfacing a few active public chats by category. + +For the full changelog, see our Github + + +#### Added +* Send images in 1:1 and group chats +* React with emojis to any message +* Send voice messages in 1:1 and group chats +* Discover public chats by category +* Ability to delete an account + +#### Changed +* Major interface improvements throughout, introducing our Quo component framework +* Auto-forwarding of the onboarding carousel +* Clarify and move 'Access existing keys' to import accounts +* Showing password entry and confirmation on the same screen +* Add contacts or invite friends from Contacts +* Searchbar includes search for ENS names + + +#### Fixed + + + +## 1.4.1 - Round of improvements +### Excerpt +This intermediate release, 1.4.1, includes bug fixes as well as UX and accessibility improvements. Most visibly, a gorgeous set of new icons, accessible input fields and toggling password entry visibility. Next to that the release includes updates to Notifications on Android, like being able to open Status, as well as close the notification service, directly from the notification banner. + +For the full changelog, see our [Github](https://github.com/status-im/status-mobile/commits/release/1.4.x) + + +### Full +While focused on building new features there are a few improvements we did not want to hold off on sharing. This intermediate release, 1.4.1, includes bug fixes as well as UX and accessibility improvements. Most visibly, a gorgeous set of new icons, accessible input fields and toggling password entry visibility. Next to that the release includes updates to Notifications on Android, like being able to open Status, as well as close the notification service, directly from the notification banner. + +For the full changelog, see our [Github](https://github.com/status-im/status-mobile/commits/release/1.4.x). + + + +#### Added +- Language support for Keycard use with Status (see our supported languages in [FAQ](https://status.im/faq/)) + +#### Changed +- Open the app and close the notification service from the notification banner +- More informative error messages for transaction overview contract and network fees +- Added a check for encoded characters in urls to increase browser security +- More accessible text input +- Replace icons with a more unified, carefully crafted set +- Update connectivity snackbar to explain messages can still be send and received when offline; only history does not sync + +#### Fixed +- ENS name is no longer infinitely pending when transaction has failed +- Added explainer that restart of the app is required to Disable logs +- Changed notification icon styling to follow convention of other apps +- Fixed critical issue that caused occasional app crashes +- Now giving feedback when blocking users is in progress +- Apply dark mode/light mode changes to System panel +- Update balance of custom tokens to be visible upon adding the token +- Show chat view after chat search gives no results (chat list showed up empty) +- Bring back numpad to sign with Keycard after dismissing an error (e.g. having tapped an unpaired card) +- Keyboard changes (include decimals for amount input and auto-capitalization) + + + + + +## 1.4 - Keycard, notifications DRAFT +In version 1.4, Status introduces a much anticipated integration with Keycard - the secure, contactless hardware wallet, also designed and developed by The Status Network. This integration enables Keycard holders to store their private keys offline on the Keycard device, add hardware-enforced authorizations to all of their transactions, and introduce two-factor authentication to log into their Status account. + +Another highly anticipated feature is the introduction of notifications. These notifications are [ANDRE to add detail].... + +The final addition to v1.4 is the introduction of browser support for EIP 1139 – A JavaScript Ethereum Provider API for consistency across clients and applications. + +Beyond that, version 1.4 includes some updates to the FAQ, extended translation support, and some fixes to ENS username displays, dark mode contrast, QR code scanner and more. + +For the full changelog, see our [Github](https://github.com/status-im/status-mobile/commits/release/1.4.x). + +#### Added +- Keycard integration on Android +- Browser support for eip-1193 (?) +- Notifications (Android) +- Content type for images in preparation of 'Images in chat' release + +#### Changed +- Extended and consolidated FAQ +- Extended translation support +- Animation in header (Profile + Wallet) + + +#### Fixed +- ENS in chat only when transaction is confirmed +- Dark mode contrast (increased) +- Updated and expanded token icons +- Resolved error on scrolling account view +- Parse markdown in subheader chat list +- Banner to back up seed phrase tappable +- [Chat performance improvements](https://github.com/status-im/status-mobile/pull/10711) +- QR code scanning issue when scanning from join.status.im + + +## 1.3 - Dark Mode, Group Chat + +In version 1.3, Status catches up to the most important design trend in recent history with the introduction of a dark mode. + +Hyperbolic? Perhaps, but we've heard your requests and we made it happen. Your app's appearance will now default to match your system settings, but if you'd like, you can head to `Profile` > `Appearance` to override it. Dark mode sure is easy on the eyes. + +Beyond that, we're bringing back private groups, another highly requested feature. Private groups allow you to add up to 9 friends to an invite-only chat. At the protocol level, group chats in Status function similarly to 1:1 chats. They're encrypted end-to-end and utilise perfect forward secrecy to protect your privacy. + +In the future, we'd like to expand these to host more than 10 members. We'll embark on further testing to determine the upper limit that our current protocol can support. + +Lastly, we've changed our webview implementation over to the react-native-community package, to remain in sync with the React Native community and benefit from upstream improvements. + +As always, we've also included a few nice fixes in this release. For the full changelog, [see our Github](https://github.com/status-im/status-mobile/commits/release/1.3.x). + + +#### Added +- Dark mode +- Group chat + +#### Changed +- Browser implementation now using react-native-community webview package + +#### Fixed +- Updates to back navigation +- Persistant wallet value +- Update to all text input, increasing pressable areas +- Improved seed phrase text input on small screens +- Easier to find user's public key for sharing (https://github.com/status-im/status-mobile/pull/10207#event-3163431514) +- Shortened universal links (https://github.com/status-im/status-mobile/issues/10083) + +## 1.2 + +### Short Version + +The change log is small, but significant! We've enabled mailservers that use our forked version of Whisper, known as Waku. Waku servers reduces bandwidth consumption by the app and enable Status to serve more concurrent users. + +A few bug fixes and optimizations are included, too. + +### Blog Post + +This release is one of the most important in Status' history. The change log is small, but significant. Today we're rolling out new mailservers that use our fork of the Whisper messaging protocol, known as Waku. + +Waku mailservers prevent Status from using excessive mobile bandwidth. With Waku, Status can support 10 times as many users as with Whisper servers alone. + +Regular Whisper mailservers use topic-based filters to surface only the chats that you want to see, but your device still receives all messages sent over the network. Filtration happens after the fact. + +To save on resources, Waku mailservers simply tell other nodes in the network which topics your device would like to receive content for, filtering before the messages reach your device. + +Heavy message traffic can be incredibly resource intensive, depending on how many topics you follow. For a heavy user, the difference can amount to several hundred kilobytes of data per minute. + +There is a potential privacy tradeoff in making your followed topics apparent to other nodes—it is easier to know if a given IP address belongs to a certain chat. + +In v1.2, we offer the legacy Whisper servers as an option under `Advanced` settings. You can disable Waku by toggling `Waku enabled`. Waku mailservers talk to existing mailservers without any disruption. + +With further improvements to Waku in the pipeline, Status becomes accessible to even more users and can deliver peer-to-peer messaging without draining unnecessary data. + + + +### Changelog + +#### Changed +- Waku mailservers +- Updating wallet fiat prices even more frequently (awaiting clarification) + +#### Fixed +- App crashing when webview is closed +- Messages not showing when received while on a different tab +- Token balances displayed as 0 after relogin + +## 1.1 + +Biggest changes: +- Status now support importing private key and seed phrase inside multiaccount! +(https://github.com/status-im/status-mobile/pull/10100) +- all chat dialogs moved to bottom sheet +- added "Mark all as read" option +- unread badge is changed for public chat + +More detailed: + +ENS name improvements: + +- https://github.com/status-im/status-mobile/pull/9952 - fixes ENS to new registry +- https://github.com/status-im/status-mobile/pull/9997 - Check for empty public key (for ENS names) +- https://github.com/status-im/status-mobile/pull/10007 - improve ENS display in Public chats +- https://github.com/status-im/status-mobile/pull/10022 - fix crash on long ENS names + + +General improvements: + +- https://github.com/status-im/status-mobile/pull/10027 - deep link handling (rename get.status.im to join.status.im) +- https://github.com/status-im/status-mobile/pull/9940 - changed navigation on logout +- https://github.com/status-im/status-mobile/pull/9917 - added search to currencies and assets +- https://github.com/status-im/status-mobile/pull/9941 - improvement on snackbar by considering login time (less "Offline", "Connected" after login) +- https://github.com/status-im/status-mobile/pull/9898 - added qr code reader for scanning QR when add watch-only address +- https://github.com/status-im/status-mobile/pull/9882 - Respond to FaceID failure callback +- https://github.com/status-im/status-mobile/pull/9915 - Make bottom sheet height dynamical +- https://github.com/status-im/status-mobile/pull/9931 - 2 lines for user name on intro screen + +Chat improvements: +- https://github.com/status-im/status-mobile/pull/10056 - added "Mark all as read" feature +- https://github.com/status-im/status-mobile/pull/9993, https://github.com/status-im/status-mobile/pull/9999 - better handling of ENS names / chat keys when starting new chat +- https://github.com/status-im/status-mobile/pull/10117 - reworked unread message indicator in public chats +- https://github.com/status-im/status-mobile/pull/9868 - all chat dialogs are moved to bottom sheets + +Performance improvements: +- https://github.com/status-im/status-mobile/pull/9995 - Offload chat messages +- https://github.com/status-im/status-mobile/pull/10120 - Android app UI is slow after background + +Issues fixed: +- https://github.com/status-im/status-mobile/issues/10002 +- https://github.com/status-im/status-mobile/issues/8797 +- https://github.com/status-im/status-mobile/issues/6839 +- https://github.com/status-im/status-mobile/issues/9754 +- https://github.com/status-im/status-mobile/issues/10044 +- https://github.com/status-im/status-mobile/issues/10041 +- https://github.com/status-im/status-mobile/issues/9886 + +## 1.0 + +### Blog Post + +Status version 1 is live! + +After nearly a year of quiet and focused development, we've restructured the core application, built out the promises of the white paper and prepared the app for a future in which the Status Network seeks to deliver on the promises of Ethereum at large. + +While the core mobile application is no longer the sole focus of the Status Network, it is integral to our dream of a decentralized web. With Status, we're building not just a tool for private and secure communication, but a gateway into an emerging ecosystem of distributed applications that embodies what is possible with decentralized technologies today. We believe apps like Status represent the future of the web—enabling an internet that can uphold human rights, and providing a path towards more open and equitable systems. + +Today, the underpinninngs of the core application are still tethered to the Ethereum stack. Whisper is still the communication layer of Status, and Status is still the only production use case of Whisper. + +The scalability of Whisper is not significantly improved from what it was roughly three years ago, when we first embarked on this project. We face immense challenge in building on this stack every day. + +But we also feel ready to share our progress with the world. Status version 1 launches with the Ethereum wallet, web3-enabled browser and peer-to-peer messenger that our community knows well. It includes new SNT utility features, like a sticker marketplace and a decentralized DApp directory (https://dap.ps). Other white paper features, such as the fiat-to-crypto Teller Network and incentivized messaging with Tribute to Talk, are well underway. + +Most pressingly, we're working to remedy the limitations of Whisper. In a forthcoming release, we'll introduce Waku mailservers, which will reduce the amount of bandwidth Status consumes—an important point for accessibility—and extend the capabilities of our nodes so that Status can support more concurrent users. + + +[Link to user guide] +[Link to roadmap] +[Shoutout contributors & team] +Huge shout out to our contributors: +Enrico +Yalu (user GH name) +Acolytec3 + +----- + +**Support comms** +- How to submit stickers for sticker marketplace ([google form](https://docs.google.com/forms/d/19itZB_V5bVW4cyV8a2ifDWHuQSr9V_dkVMCl6ARxOag/edit)) +- How to share your feedback with us (#status, primarily) +- Learn more about Status - Glossary, [FAQs up-to-date](https://status.im/docs/FAQs.html), [user guide by Jonny](https://docs.google.com/presentation/d/11o53KGcuzKouDU5VBeTB01mLCm41hKKuraz-AaudsEg/edit#slide=id.p) +- How to release your ENS name ([here](https://hackmd.io/QrfTRP6sQ9aQzjFyb_IfRQ?both)) +- How to set up your existing ENS name ([here](https://hackmd.io/QrfTRP6sQ9aQzjFyb_IfRQ?both)) + +----- + +**v1.0 Change Log** +**Added** +• Support for multiple Ethereum wallets within one Status account +• Generate unlimited additional BIP 44 wallets in Status +• Add watch-only addresses as well +• Brand new onboarding and key encryption flow +• Choose your random name and public chat key during onboarding +• You also can recover a seed phrase from any mnemonic compatible wallet +• Improvements to wallet transaction flow UI +• Sticker market and two initial sticker packs +• Choice of wallet to use when interacting with Dapps +• [Proper markdown support](https://github.com/status-im/status-mobile/pull/9409#issue-338045622) +• ENS username setting option for use in chat +• Improved chat intro screen and public chat list +• Updated chat command flow, with additional privacy layer to be compatible with multiaccount + +**Removed** +• Custom display names +• Custom profile photos, temporarily—photos will become a feature of ENS +• Push notifications—these will be improved and reintroduced for Android +• Group chat, temporarily—fixes to be prioritized for v1.1 +• Google Firebase database and Realm.js DB—performance gains, and more in line with our principles (fewer third party services) + +**Changed** +• Algorithm for deriving public chat key changed, performance. (andrey review: pubkey or 3 word name?, not sure if Algorithm for deriving pubkey can be changed) +• Storage of app state moved to Status-go—performance improvements in chat load times +• Browser privacy mode enabled by default, option removed (EIP1102) +• Updates to EIP1102 for compatibility (https://github.com/status-im/status-mobile/pull/9629) +• Protocol breaking changes: removed transit encoding in favor of protobuf, for better performance and interoperability with other languages +• Roughly 45% improvement in how quickly chats load +• Profile screen cleaned up + + +---- + +> Names and picture have changed as we use a different algorithm to generate it (linear > > feedback shift register instead of Mersienne Twister). +> There are breaking changing between protocol, as agreed before, so no compatibility at all. Previous version won't be able to receive/send messages to v1. + +> We confirmed the performance improvement with the test-team, roughly 45% improvement in how quickly chats load (maybe greater). +>indeed, true, good move to make together. +> but I would suggest to also add in the release changelog that the algorithm for deriving a > pubkey -> 3 word name changed, just because a standard was changed +> this is also good to say because there is performance gains. + + +## 0.13.2 +- V1 notification +- Bug fixes: + - Neverending loading indicator (#8550) + - Last message jumps after chat is opened (#8556) +- Sticker Market with two packs launched on mainnet +- Account explorer in preparation for multi-account +- Onboarding face lift (preparing for multiaccount/keycard) + +## 0.13.0 + +This release comes with a long change log and one popularly requested new feature: custom, user-added ERC20 token support. + +If you'd like to see a token that the Status wallet does not display by default, you can now add it yourself. From the wallet screen, open `Manage Assets` from the `...` menu and select `Add custom token`. Fill in the token's contract address, and the relevant information, and you will then be able to view this asset in your wallet. + +We've also added and altered our privacy settings: + +1) New `Preview privacy mode`: toggling this option in your profile will ensure that Status displays a blank screen when switching apps on your phone, to protect you from accidentally exposing your information while doing other things. Thanks to contributor [bitsikka](https://github.com/bitsikka) for this feature! +2) `Browser privacy mode` has been relocated from the profile to the browser, and reconfigured into a two option setting. Selecting `Require my permission` for this setting ensures that privacy mode is on, but still means that some DApps may be incompatible with Status. + +![](https://notes.status.im/uploads/upload_c9ea844cdcdc1ce5a786373ac08521bc.png) + +There are a few more important changes: + +- `Fetch more messages` was introduced in 0.12.0 to fill gaps in your message history; it now backfills gaps of up to 30 days in public chats, increased from 24 hours. +- We've removed the swipe functionality from the chat screen. To remove a chat, you can now tap on it and hold, pulling up a bottom menu. Another shout out to [bitsikka](https://github.com/bitsikka) for this change! +- We've reintroduced bug reporting on both iOS and Android: shake your phone to pull up an email report, or enable `Development mode` from your profile screen to `Report a bug` from there. + +Lastly, we've had some great contributions from the community for this release. They've added new features, refactored and even merged some changes to our developer tools. + +Special thanks to [tbenr](https://github.com/tbenr), [bitsikka](https://github.com/bitsikka), [m0ar](https://github.com/m0ar) and [alexanmtz](https://github.com/alexanmtz) for your recent [commits](https://notes.status.im/CgruSsRzQVGhsagaLxXGqQ?view#0130-mobile-release)! + +To get involved, you can find our current bounty issues on Gitcoin [LINK]. + + + +**Added** +- Custom, user-added ERC20 support +- New bug reporting options: shake device to send a report, with logs attached +- Bloom Protocol token - thanks to contributor [m0ar](https://github.com/m0ar)! +- Preview privacy mode to display blank Status screen when switching apps - thanks to contributor [bitsikka](https://github.com/bitsikka)! +- URLs for transactions open in Status (partial [EIP681](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-681.md) support) + +**Changed** +- Moved `Browser privacy Mode` toggle to the browser screen as new preference setting +- `Fetch more messages` button now retrieves up to 30 days of history in public chats +- Long press a chat for more options; no more swiping +- Fewer messages rendered before a chat is scrolled for better performance ([8191](https://github.com/status-im/status-mobile/pull/8191)) +- Using checksummed addresses in wallet (EIP55) ([8121](https://github.com/status-im/status-mobile/issues/8121) and [4959](https://github.com/status-im/status-mobile/issues/4959)) +- Scanning QR code from a DApp does not cause redirect + +**Fixed** +- Browser crash when incorrect domain entered ([8239](https://github.com/status-im/status-mobile/issues/8239)) +- ENS registration bug ([8249](https://github.com/status-im/status-mobile/issues/8249)) +- Wallet balance updates correctly after Tx received ([8107](https://github.com/status-im/status-mobile/issues/8107)) +- Support for EIP1577/`contenthash` field for ENS +- Numeric validation for `send`/`receive` chat commands ([1442](https://github.com/status-im/status-mobile/issues/1442)) +- Allow to select chat item below plus button ([7859](https://github.com/status-im/status-mobile/issues/7859)) + + +## 0.12.0 + +In this release, we're announcing the launch of Discover! + +Discover is our new portal for exploring the decentralized web. It has long been our vision for Status to act as a gateway to the new web, just like Chrome or Safari do for web2. + +Until this point, however, we've prioritized a curated list of DApps over an open-ended experience. + +With the launch of Discover, our browser becomes just that—a browser, with special capabilities for web3. + +Discover is its own web app and the new go-to for finding DApps. For now, we've ported over our existing list, with a few upgrades to the UI. + +But in the near future, we're decentralizing Discover, enabling anyone to add a DApp and giving the community control over curation through an SNT voting mechanism. More on that [here](https://our.status.im/discover-preparing-for-launch/). + +Discover can be accessed from any browser at https://dap.ps, so you can participate on desktop as well. + +This release also comes with a noteworthy chat improvement. Previously, a gap in connection to the mail servers—which happens if Status is closed for more than 24 hours or otherwise loses connection—meant that some messages might be lost. + +We now detect gaps where messages are missing and offer you the option to fetch the missing history. By tapping `Fetch messages` when it appears within a chat, you can backfill missing history. This is a big step forward for reliability in our peer-to-peer service. + +**Added** +- `Fetch messages` option to get missing message history +- Discover is live on https://dap.ps + +**Changed** +- `DApps` screen is now fully browser-centric with address bar, history and a link to Discover + + +**Fixed** +- Contact syncing between 0.11.0 and newer builds +- 503 error when accessing DApps +- More reliable history for token transactions + +## 0.11.0 + +Download 0.11.0 and you'll notice one thing right away: we have a new navigation! + +The home screen has been split into two tabs: Chats & DApps. From the chat view, you'll find the option to start or join a new chat has moved from the top right corner to bottom center. + +The DApp list also looks slightly different. We've done away with info screens for each DApp. Once you navigate to a URL, you'll find that your browser history appears under a `Recent` section at the top. + +This is phase one of bigger improvements to come, so we'd love to hear your feedback. + +We've also made preparations for the impending [Chaos Unicorn Day](https://chaos-unicorn-day.org/) in this release. + +Reminder: On April 1st, we'll experiment with eliminating all centralized, third party services from Status, including our own node clusters. To safeguard from the chaos, we've prepared [a guide for you](https://our.status.im/bulletproofing-against-chaos-unicorns-with-status-on-arm/). + + + +**Added** +- New DApps added +- New extensions features integrated within Status +- Chaos mode and preparation for Chaos Unicorn Day + +**Changed** +- New bottom navigation: chats & DApps are separated +- Improved UI performance (https://www.pivotaltracker.com/story/show/164245989) + +**Fixed** +- Universal link to public chat bug (https://github.com/status-im/status-mobile/issues/7549) +- Beta warning popup (https://github.com/status-im/status-mobile/issues/7771) +- DApp offline error (https://github.com/status-im/status-mobile/issues/7188#issuecomment-474308724) +- Bug with QR code scanner for custom bootnodes (https://github.com/status-im/status-mobile/pull/7782) +- Error message when entering an incorrect custom mailserver (https://github.com/status-im/status-mobile/issues/7752) + + +## 0.10.1 - Hot Fix + +**Added** +- Option for automatic mail server selection to optimize connection + +**Fixed** +- Copy errors corrected +- "Fetching messages" indicator appears on home screen + +## 0.10.0 + +A handful of exciting new features and fixes coming to you fresh in 0.10.0. + +First, a word about our new version numbers. Until now, our build versions have followed a convention of minor increments: 0.9.31, 0.9.32, 0.9.33. To better reflect the scope of our releases, we've [decided to level up](https://discuss.status.im/t/new-status-versioning-scheme/1066) to 0.10.0 and increment by 0.1.0 for typical releases, and 0.0.N for bug fixes. Our routine is business as usual otherwise. + +Onto the highlights from 0.10.0. + +The option to block a user will come in handy for those who participate in popular public chats. If someone is spamming a chat or otherwise bugging you, simply tap on their icon and hit `Block contact` from their profile. You'll no longer see any of their messages. + +From _your_ profile screen, you can undo this action via a new contact list feature. Open `Contacts` to find a list of users who are able to see your info—namely, photo and display name—and from there, `Blocked users` to see and unblock users as you wish. + +To better find users you _do_ want to hear from, we've added a search bar to the home screen. This allows you to filter through chats and browser history items. Text search within a chat is still to come. + +Back on your profile screen, you'll find another helpful new option to control message syncing. If on a mobile network, you can require Status to ask before syncing conversations, in order to protect against heavy data usage. You can also enable mobile network syncing by default, or turn these options off completely. + +You'll also find a new DApp permissions section here. DApps that adhere to browser privacy mode [ask permission](https://our.status.im/0-9-33/) to access your Ethereum account. You can now revoke access for a given DApp using these settings. + +And that concludes the most significant changes of this release! For more, check out the change log below. + +**Added** +- Ability to block specific users from profile view +- Contacts and blocked users list +- Search bar for chats and browser history +- Option to turn off syncing for chat on cell connection +- DApp web3 permissions management +- New DApps and WBTC token + +**Changed** +- Push notifications include sender name +- Contact updates pushed periodically to better maintain user data +- Public chats show first letter instead of # +- Minor tweaks to chat UI + +**Fixed** +- Status now works on restricted [ports](https://github.com/status-im/status-mobile/pull/7382) +- Overlap on small screens during log-in +- False mailserver connection error [removed](https://github.com/status-im/status-mobile/issues/7531) +- Premature sending of contact requests on Profile > Send message +- DApps on same host (i.e. IPFS) no longer share user-granted permissions + +Ready to update to 0.10.0? + +iOS +Within TestFlight, make sure you see version 0.10.0 and tap ‘INSTALL.’ +If you don’t currently have Status installed, access via TestFlight here: https://testflight.apple.com/join/J8EuJmey. + +Android +If you are not automatically asked to update, go to Status.im in the PlayStore and click ‘Update.’ +For more information, please join us in Status. +We're more than happy to answer your questions and help you out. + +--- +Social copy: +Release 0.10.0 is here! A whole bunch of exciting features, blocking contacts, searching, & more. Check out the blog for more details and update Status. our.status.im/0-10-0/ + + + +## 0.9.33 + +We're back in action! After a pause for the holidays and a transition back that saw us release 4 small updates to v.0.9.32, we've got a brand new mobile version for you. + +This release is a big one. + +First, we've made significant improvements to log-in, yielding a 10x increase in log-in speed. Android users will also be pleased to find a "Save password until logout" toggle to match iOS. This will save you significant time on launch of the app. + +Push notifications are improved by a number of fixes, including opening to the appropriate chat and no longer exposing your language settings to recipients using a different language than your own. + +Because of these fixes, anyone using an older version of the app **_will not receive notifications_** from users on v.0.9.33. Furthermore, iOS users will not receive push notifications while the app is closed, nor if they are on certain iOS versions (11.X). This is a known and temporary issue. + +Browser privacy mode is now enabled by default. This means that DApps will be required to ask permission before connecting to your wallet, and it may cause some DApps to break. If you find a DApp isn't working, you can go to your profile screen and toggle privacy mode off. In many cases, this should fix the issue. + +We're hopeful that more and more DApp developers will be up to speed with this new convention soon. + +Device pairing is no longer hidden under development mode. Visit the device section on your profile screen to "Pair this device" and sync messages and contacts from your phone to desktop. You can also name your devices to track which you've paired. + +For the full list of changes, see below. + +**Added** +- Device pairing and naming +- "Save password until logout" for Android +- New DApps including Aragon and Dragonereum +- BUFF token for ETHDenver +- Titles and favicons in browser +- Support for EIP1577, multihash interface for ENS names + +**Changed** +- Privacy mode enabled by default +- Log-in time improved 10x +- Private group chats limited to 10 members +- Connectivity loading indicator no longer appears as "Fetching messages" +- Chat bubble width increased +- Ethereum provider consistent with EIP1193 +- Upgraded styling for group chats + +**Fixed** +- Metadata leakage of language settings in push notifications +- Push notifications failing to open chat +- Loading indicator in browser +- Results displaying properly in Etheroll +- Issues with extensions installation +- Wrong password handling + +------- + +_Notes to call out_ +- Privacy mode by default instructions +- Breaking change: no PNs for old versions after next mobile release - https://github.com/status-im/status-mobile/pull/6893 +- Setting up device pairing + + +## 0.9.32 +Introducing beta support for Private group chats! +In this release you’ll find the ability to start private group chats with your friends. These group chats are e2e encrypted and utilize Signal's [double ratchet algorithm](https://en.wikipedia.org/wiki/Double_Ratchet_Algorithm) for guaranteed privacy. To start chatting go to the Home tab, tap "+", then Start group chat. + +There is also beta support for device pairing between all devices. To begin syncing messages and contacts across devices, go to Profile, tap advanced and turn on development mode, now go to devices and tap pair this device. + + + +Added +- Private group chats with increases security using Signal's double ratchet algorithm (In beta. Still being tested. Feedback welcome!) +- Device pairing and syncing with perfect forward secrecy (in dev mode) +- Added DApp Decentraland + + +Changed +- Support multi-extensions store +- Restyled wallet onboarding flow, main wallet screen, and sent-transaction screen +- Faster sign-in + +Fixed +- Transaction history fixes +- Updating enabled tokens before navigating to wallet + +[All changes](https://github.com/status-im/status-mobile/compare/release/0.9.31...release/0.9.32) + +## 0.9.31 + +Version 0.9.31 brings Status up to speed with the latest privacy standard for Ethereum browsers, introduced by EIP1102. + +You'll find a new privacy setting on your profile screen called "Browser privacy mode." + +If you toggle this setting ON, DApps will be required to ask your permission before accessing your Status wallet. DApps that do not currently follow this standard may not work! + +If you run into trouble, you can always toggle this setting OFF. With Browser privacy mode OFF, DApps can access your Status wallet without your permission. + +We're encouraging DApp builders to adopt this new standard and make web3 safer for users as soon as possible. Check the Status blog for details on how to comply. + + + +Added +- Browser privacy mode to toggle new web3 security setting on or off +- New DApps: Crypto Takeovers, Cryptographics, blockimmo and SNT Voting DApp +- ST to assets list + +Changed +- Changing mailserver no longer requires a logout +- Send button is inactive when there isn't an active internet or mailserver connection + +Fixed +- ENS addresses with unregistered chat IDs no longer resolve in chat +- CryptoKitties log-in issue +- Restored missing transaction error messages +- Granting profile access applies per DApp rather than per host + +[All changes](https://github.com/status-im/status-mobile/compare/release/0.9.30...release/0.9.31) + +## 0.9.30 + +Added + +- ENS registration link on profile screen +- Kudos tokens for the Status #CryptoLife Hackathon +- Reply and quote a message in chat. Long tap a message and tap Reply. +- Chat now resolves all .eth address +- New Farsi and Latin American Spanish translations +- Every Dapp gets its own built in chat room. Tap the chat icon from the Dapp browser to start chatting +- Added Kickback Dapp +- New custom RPC networks for POA Network and xDAI Chain + + +Changed +- Anonymous names now shown in chats and profile +- Disabled "sign transaction" button when offline +- Renamed default international public chats to ensure universal links would work. For example “status 中文” was changed to “status-chinese”. +- Removed Instabug for bug reports. In-app help is now available in #status + +Fixed +- Fixed an issue where messages are not saved to the database in some circumstances +- Fixed an error when fetching history on certain accounts +- Fixed logging in after logout when opening app from a notification +- Fixed profile QR code +- iPhone XS Max improvements + +Known Issues + +- POA and xDAI networks are only available for new accounts, not upgrades +- For more POA and xDAI network considerations see this issue post. + +## 0.9.29 + +This releases improves message validation to prevent potential crashes or malformed messages. We recommend all users upgrade. + +The fix makes these improvements: +- add validate method to StatusMessage protocol +- spec all message types for use in validate method +- use valid method after transit/decode step to reject invalid messages + + +## Older Versions + +- [Google docs](https://docs.google.com/document/d/1Z0mFlJJkNbRgv6qm80ob5g4PEvsI8cTmbwZpcRPWPho/edit#) + diff --git a/VERSION b/VERSION new file mode 100644 index 00000000000..78d1d9cf49e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.34.1 diff --git a/android/app/BUCK b/android/app/BUCK deleted file mode 100644 index 7e80564a8ba..00000000000 --- a/android/app/BUCK +++ /dev/null @@ -1,66 +0,0 @@ -import re - -# To learn about Buck see [Docs](https://buckbuild.com/). -# To run your application with Buck: -# - install Buck -# - `npm start` - to start the packager -# - `cd android` -# - `cp ~/.android/debug.keystore keystores/debug.keystore` -# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck -# - `buck install -r android/app` - compile, install and run application -# - -lib_deps = [] -for jarfile in glob(['libs/*.jar']): - name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) - lib_deps.append(':' + name) - prebuilt_jar( - name = name, - binary_jar = jarfile, - ) - -for aarfile in glob(['libs/*.aar']): - name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) - lib_deps.append(':' + name) - android_prebuilt_aar( - name = name, - aar = aarfile, - ) - -android_library( - name = 'all-libs', - exported_deps = lib_deps -) - -android_library( - name = 'app-code', - srcs = glob([ - 'src/main/java/**/*.java', - ]), - deps = [ - ':all-libs', - ':build_config', - ':res', - ], -) - -android_build_config( - name = 'build_config', - package = 'im.status.ethereum', -) - -android_resource( - name = 'res', - res = 'src/main/res', - package = 'im.status.ethereum', -) - -android_binary( - name = 'app', - package_type = 'debug', - manifest = 'src/main/AndroidManifest.xml', - keystore = '//android/keystores:debug', - deps = [ - ':app-code', - ], -) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9cfc4c02f9b..850f20752fc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,104 +1,103 @@ apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" import com.android.build.OutputFile +import com.sun.org.apache.xalan.internal.xsltc.compiler.Copy -/** - * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets - * and bundleReleaseJsAndAssets). - * These basically call `react-native bundle` with the correct arguments during the Android build - * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the - * bundle directly from the development server. Below you can see all the possible configurations - * and their defaults. If you decide to add a configuration block, make sure to add it before the - * `apply from: "../../node_modules/react-native/react.gradle"` line. - * - * project.ext.react = [ - * // the name of the generated asset file containing your JS bundle - * bundleAssetName: "index.android.bundle", - * - * // the entry file for bundle generation - * entryFile: "index.android.js", - * - * // whether to bundle JS and assets in debug mode - * bundleInDebug: false, - * - * // whether to bundle JS and assets in release mode - * bundleInRelease: true, - * - * // whether to bundle JS and assets in another build variant (if configured). - * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants - * // The configuration property can be in the following formats - * // 'bundleIn${productFlavor}${buildType}' - * // 'bundleIn${buildType}' - * // bundleInFreeDebug: true, - * // bundleInPaidRelease: true, - * // bundleInBeta: true, - * - * // whether to disable dev mode in custom build variants (by default only disabled in release) - * // for example: to disable dev mode in the staging build type (if configured) - * devDisabledInStaging: true, - * // The configuration property can be in the following formats - * // 'devDisabledIn${productFlavor}${buildType}' - * // 'devDisabledIn${buildType}' - * - * // the root of your project, i.e. where "package.json" lives - * root: "../../", - * - * // where to put the JS bundle asset in debug mode - * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", - * - * // where to put the JS bundle asset in release mode - * jsBundleDirRelease: "$buildDir/intermediates/assets/release", - * - * // where to put drawable resources / React Native assets, e.g. the ones you use via - * // require('./image.png')), in debug mode - * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", - * - * // where to put drawable resources / React Native assets, e.g. the ones you use via - * // require('./image.png')), in release mode - * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", - * - * // by default the gradle tasks are skipped if none of the JS files or assets change; this means - * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to - * // date; if you have any other folders that you want to ignore for performance reasons (gradle - * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ - * // for example, you might want to remove it from here. - * inputExcludes: ["android/**", "ios/**"], - * - * // override which node gets called and with what additional arguments - * nodeExecutableAndArgs: ["node"], - * - * // supply additional arguments to the packager - * extraPackagerArgs: [] - * ] +/* + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. */ -project.ext.react = [ - nodeExecutableAndArgs: ["node", "--max-old-space-size=4096"], - entryFile: "index.android.js" -] +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '..' + // root = file("../") + // The folder where the react-native NPM package is. Default is ../node_modules/react-native + // reactNativeDir = file("../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + // codegenDir = file("../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js + // cliFile = file("../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] -apply from: "../../node_modules/react-native/react.gradle" + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + nodeExecutableAndArgs = ["node", "--max-old-space-size=16384" ] + // + // The command to run when bundling. By default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] +} /** - * Set this to true to create two separate APKs instead of one: - * - An APK that only works on ARM devices - * - An APK that only works on x86 devices - * The advantage is the size of the APK is reduced by about 4MB. - * Upload all the APKs to the Play Store and people will download - * the correct one based on the CPU architecture of their device. + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = true + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. */ -def enableSeparateBuildPerCPUArchitecture = false +def jscFlavor = 'org.webkit:android-jsc:+' /** - * Run Proguard to shrink the Java bytecode in release builds. + * Whether to enable the Hermes VM. + * + * This should be set on project.ext.react and mirrored here. If it is not set + * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode + * and the benefits of using Hermes will therefore be sharply reduced. */ -def enableProguardInReleaseBuilds = false +def enableHermes = hermesEnabled.toBoolean(); + +def getCommitHash = { -> + if (project.hasProperty("commitHash")) { + return project.commitHash + } + return "unknown" +} def getVersionCode = { -> new ByteArrayOutputStream().withStream { stdOut -> + if (project.hasProperty("versionCode")) { + return project.versionCode.toString().toInteger() + } + exec { - commandLine "sh", "../../scripts/build_no.sh" + commandLine "bash", "../../scripts/version/build_no.sh" standardOutput = stdOut errorOutput = System.err } @@ -108,28 +107,85 @@ def getVersionCode = { -> def getVersionName = { -> new ByteArrayOutputStream().withStream { stdOut -> + // TODO: probably not used, cleanup if (project.hasProperty("releaseVersion")) { return project.releaseVersion } - version = new File('../VERSION').text + /* Necessary because Android Studio uses wrong PWD. + * Is actually absolute directory path of this file. */ + def configDir = project.projectDir.toString() + version = new File(configDir + '/../../VERSION').text return version.replaceAll("\\s","") } } +def getBuildUrl = { -> + new ByteArrayOutputStream().withStream { stdOut -> + if (project.hasProperty("buildUrl")) { + return project.buildUrl + } + return 'Local Build' + } +} + +/* check if environment variable exists for given variable name first */ +def getEnvOrConfig = { varName -> + def val = System.getenv(varName) ? System.getenv(varName) : project.property(varName) + return val +} + android { - compileSdkVersion 27 + ndkVersion rootProject.ext.ndkVersion + + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + // Disable default lint checks to remove redundant lintVitalReportRelease task + lintOptions { + checkReleaseBuilds false + } + + // https://developer.android.com/studio/projects/install-ndk#vanilla_cmake + externalNativeBuild { + cmake { + // This version must match cmakeVersions inside nix/pkgs/android-sdk/compose.nix + version "3.22.1" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + namespace "im.status.ethereum" defaultConfig { applicationId "im.status.ethereum" - minSdkVersion 23 - targetSdkVersion 27 + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled true versionCode getVersionCode() versionName getVersionName() - ndk { - abiFilters "armeabi-v7a", "arm64-v8a", "x86" + missingDimensionStrategy 'react-native-camera', 'general' + manifestPlaceholders = [commitHash: getCommitHash()] + /* this needs to be empty if we want APKs split by ABIs */ + if (!getEnvOrConfig('ANDROID_ABI_SPLIT').toBoolean()) { + ndk { + abiFilters getEnvOrConfig('ANDROID_ABI_INCLUDE').split(";") + } } } + + /** + * Arbitrary project metadata + * https://docs.gradle.org/current/dsl/org.gradle.api.plugins.ExtraPropertiesExtension.html + **/ + project.ext { + buildUrl = getBuildUrl() + } /** * Fix for: (https://github.com/ReactiveX/RxJava/issues/4445) * Execution failed for task ':app:transformResourcesWithMergeJavaResForDebug'. @@ -139,123 +195,152 @@ android { packagingOptions { exclude 'META-INF/rxjava.properties' exclude '/lib/mips64/**' - exclude '/lib/arm64-v8a/**' - exclude '/lib/x86_64/**' + exclude '/lib/armeabi/**' + + pickFirst '**/armeabi-v7a/libc++_shared.so' + pickFirst '**/x86/libc++_shared.so' + pickFirst '**/arm64-v8a/libc++_shared.so' + pickFirst '**/x86_64/libc++_shared.so' + pickFirst '**/x86/libjsc.so' + pickFirst '**/armeabi-v7a/libjsc.so' + /** Fix for: Execution failed for task ':app:transformNativeLibsWithStripDebugSymbolForDebug'. * with recent version of ndk (17.0.4754217) */ doNotStrip '*/mips/*.so' doNotStrip '*/mips64/*.so' } - dexOptions { - jumboMode true - javaMaxHeapSize "4g" - } - signingConfigs { - release { - storeFile file(STATUS_RELEASE_STORE_FILE.replaceAll("~", System.properties['user.home'])) - storePassword STATUS_RELEASE_STORE_PASSWORD - keyAlias STATUS_RELEASE_KEY_ALIAS - keyPassword STATUS_RELEASE_KEY_PASSWORD - } - } splits { abi { reset() - enable enableSeparateBuildPerCPUArchitecture - universalApk false // If true, also generate a universal APK - include "armeabi-v7a", "x86" + enable getEnvOrConfig('ANDROID_ABI_SPLIT').toBoolean() + include getEnvOrConfig('ANDROID_ABI_INCLUDE').split(";") + universalApk getEnvOrConfig('ORG_GRADLE_PROJECT_universalApk').toBoolean() + } + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' } } buildTypes { debug { applicationIdSuffix ".debug" - versionNameSuffix "-SNAPSHOT" + debuggable true + versionNameSuffix "-SNAPSHOT" resValue "string", "build_config_package", "im.status.ethereum" + signingConfig signingConfigs.debug } release { minifyEnabled enableProguardInReleaseBuilds + shrinkResources enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" - signingConfig signingConfigs.release + signingConfig null + } + pr { + initWith release + applicationIdSuffix ".pr" + versionNameSuffix ".pr" + debuggable false + matchingFallbacks = ["release"] + // necessary to make react-native-config's code generation work + resValue "string", "build_config_package", "im.status.ethereum" } } + // applicationVariants are e.g. debug, release applicationVariants.all { variant -> variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: - // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits - def versionCodes = ["armeabi-v7a":1, "x86":2] + + // https://developer.android.com/studio/build/configure-apk-splits.html + def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants - output.versionCodeOverride = - versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + output.versionCodeOverride = versionCodes.get(abi) + defaultConfig.versionCode } + } } - sourceSets { main { jniLibs.srcDirs 'libs' } } -} -configurations.all { - exclude group: 'com.facebook.react', module: 'react-native' + aaptOptions { + // disable PNG optimization as for some reason it cannot be trusted to provide deterministic output (see https://f-droid.org/en/docs/Reproducible_Builds/) + cruncherEnabled = false + } + + sourceSets { + main { jniLibs.srcDirs 'libs' } + } } dependencies { - implementation project(':react-native-background-timer') - implementation project(':react-native-svg') - implementation 'com.android.support:multidex:1.0.2' - implementation project(':react-native-http-bridge') - implementation project(':instabug-reactnative') - implementation project(':react-native-splash-screen') - implementation project(':react-native-image-resizer') - implementation project(':react-native-dialogs') - implementation project(':realm') - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "com.android.support:appcompat-v7:26.1.0" - // Force using exact RN version instead of relying on gradle dependency resolution - // https://docs.gradle.org/current/userguide/introduction_dependency_management.html#sec:dependency_resolution - //compile ("com.facebook.react:react-native:0.55.4") { force = true } // From node_modules - implementation project(':react-native-android') - implementation project(':react-native-i18n') - implementation project(':react-native-camera') + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + implementation("androidx.core:core-splashscreen:1.0.0") + + if (enableHermes) { + implementation("com.facebook.react:hermes-android") + } else { + // https://github.com/status-im/status-mobile/issues/18493 + // we don't use hermes for debug builds because it crashes too often + implementation(jscFlavor) + } + + // react-native-screens + implementation("androidx.appcompat:appcompat:1.1.0-rc01") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02") + implementation("androidx.multidex:multidex:2.0.1") // required by status-mobile/android/app/src/main/java/im/status/ethereum/MainApplication.java + implementation project(':react-native-blur') implementation project(':react-native-status') - implementation project(':react-native-fs') - implementation project(':react-native-image-crop-picker') - implementation project(':react-native-securerandom') - implementation project(':react-native-webview-bridge') - implementation project(':react-native-config') - implementation project(':react-native-fcm') - implementation 'com.google.firebase:firebase-core:10.0.1' //this decides your firebase SDK version - compile project(':react-native-testfairy') - implementation project(':react-native-keychain') - implementation project(':instabug-reactnative') - - implementation 'status-im:function:0.0.1' - implementation fileTree(dir: "node_modules/realm/android/libs", include: ["*.jar"]) -} -// Run this once to be able to run the application with BUCK -// puts all compile dependencies into folder libs for BUCK to use -task copyDownloadableDepsToLibs(type: Copy) { - from configurations.compile - into 'libs' + implementation project(':react-native-status-keycard') + implementation("com.github.status-im:function:0.0.1") + implementation("com.facebook.fresco:fresco:2.5.0") + implementation("com.facebook.fresco:animated-gif:2.5.0") + implementation("com.squareup.okhttp3:okhttp-tls:4.11.0") + implementation("com.google.prefab:cli:2.0.0") + implementation("com.android.tools.build:aapt2:8.1.1-10154469") } -// Must be at bottom to avoid dependency collision -apply plugin: "com.google.gms.google-services" -task hemroidBuild(type: Exec) { +def getLocalNDKDir = { -> def rootDir = project.rootDir def localProperties = new File(rootDir, "local.properties") + if (!localProperties.exists()) { + return null + } + Properties properties = new Properties() + localProperties.withInputStream { instr -> + properties.load(instr) + } + return properties.getProperty('ndk.dir') +} - def ndkDir = "$System.env.ANDROID_NDK" - if (localProperties.exists()) { - Properties properties = new Properties() - localProperties.withInputStream { instr -> - properties.load(instr) - } - ndkDir = properties.getProperty('ndk.dir') +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task hemroidBuild(type: Exec) { + def localNdkDir = getLocalNDKDir() + def ndkDir = System.env.ANDROID_NDK_ROOT + if (localNdkDir != null) { + ndkDir = localNdkDir } - executable "$ndkDir/ndk-build" + + def execPath = "$ndkDir/ndk-build" + def exec = new File(execPath) + if (!exec.exists()) { + throw new GradleException("No ndk-build binary found!") + } + executable execPath } preBuild.dependsOn hemroidBuild + +apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) + +def googleServicesConfigFile = "./googleServices.gradle"; +if (project.file(googleServicesConfigFile).exists()) { + apply from: file(googleServicesConfigFile) +} diff --git a/android/app/debug.keystore b/android/app/debug.keystore new file mode 100644 index 00000000000..364e105ed39 Binary files /dev/null and b/android/app/debug.keystore differ diff --git a/android/app/google-services.json b/android/app/google-services.json index ebe2374d0e4..220a6195f92 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -25,18 +25,95 @@ } ], "services": { - "analytics_service": { - "status": 1 - }, "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "ads_service": { - "status": 2 + "other_platform_oauth_client": [ + { + "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "im.status.ethereum" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:854811651919:android:15dbe4af1e06ca3e005f3a", + "android_client_info": { + "package_name": "im.status.ethereum.debug" + } + }, + "oauth_client": [ + { + "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "im.status.ethereum" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:854811651919:android:1d0d69fe8c1bb89b005f3a", + "android_client_info": { + "package_name": "im.status.ethereum.pr" + } + }, + "oauth_client": [ + { + "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "im.status.ethereum" + } + } + ] } } } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/android/app/googleServices.gradle b/android/app/googleServices.gradle new file mode 100644 index 00000000000..20dce0bd50d --- /dev/null +++ b/android/app/googleServices.gradle @@ -0,0 +1,25 @@ +/** + * Exclude google library so we can publish to external android stores + */ + +def googleFree = project.env.get("GOOGLE_FREE", "0") == '1' + +if (!googleFree) { + apply plugin: 'com.google.gms.google-services' +} + +dependencies { + if (googleFree) { + implementation(project(':react-native-firebase_app')) { + exclude group: 'com.google.firebase' + exclude group: 'com.google.gms' + } + + implementation(project(':react-native-firebase_messaging')) { + exclude group: 'com.google.firebase' + exclude group: 'com.google.gms' + } + } else { + implementation 'com.google.firebase:firebase-messaging:24.1.0' + } +} diff --git a/android/app/jni/Application.mk b/android/app/jni/Application.mk index 62989d870e5..df97d66fba4 100644 --- a/android/app/jni/Application.mk +++ b/android/app/jni/Application.mk @@ -1,4 +1,5 @@ APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 +APP_LDFLAGS := -Wl,--build-id=none APP_PLATFORM := android-18 -APP_STL := gnustl_static -NDK_TOOLCHAIN_VERSION=4.9 +APP_STL := c++_static +NDK_TOOLCHAIN_VERSION=clang diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 6e8516c8d6d..3904c4220ee 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -9,19 +9,15 @@ # Add any project specific keep options here: -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - # Disabling obfuscation is useful if you collect stack traces from production crashes # (unless you are using a system that supports de-obfuscate the stack traces). -dontobfuscate # React Native +-keep class com.facebook.hermes.unicode.** { *; } +-keep class com.facebook.jni.** { *; } + # Keep our interfaces so they can be used by other ProGuard rules. # See http://sourceforge.net/p/proguard/bugs/466/ -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip @@ -62,9 +58,33 @@ -keep interface okhttp3.** { *; } -dontwarn okhttp3.** +# to fix : java.security.KeyStoreException: BKS not found +-keep class org.bouncycastle.** { *; } + # okio -keep class sun.misc.Unsafe { *; } -dontwarn java.nio.file.* -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn okio.** + +# fast-image + +-keep public class com.dylanvann.fastimage.* {*;} +-keep public class com.dylanvann.fastimage.** {*;} +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class com.bumptech.glide.annotation.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +# react-native-config +# https://github.com/lugg/react-native-config?tab=readme-ov-file#problems-with-proguard +-keep class im.status.ethereum.BuildConfig {*;} + +# react-native-navigation +# to fix : java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType +# at com.reactnativenavigation.views.element.animators.PropertyAnimatorCreator.getChildClass(Unknown Source:13) +-keep class com.reactnativenavigation.** {*;} diff --git a/android/app/react.gradle b/android/app/react.gradle deleted file mode 100644 index 4b43bf910cc..00000000000 --- a/android/app/react.gradle +++ /dev/null @@ -1,96 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os - -def config = project.hasProperty("react") ? project.react : []; - -def bundleAssetName = config.bundleAssetName ?: "index.android.bundle" -def entryFile = config.entryFile ?: "index.android.js" - -// because elvis operator -def elvisFile(thing) { - return thing ? file(thing) : null; -} - -def reactRoot = elvisFile(config.root) ?: file("../../") -def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"] - -void runBefore(String dependentTaskName, Task task) { - Task dependentTask = tasks.findByPath(dependentTaskName); - if (dependentTask != null) { - dependentTask.dependsOn task - } -} - -gradle.projectsEvaluated { - // Grab all build types and product flavors - def buildTypes = android.buildTypes.collect { type -> type.name } - def productFlavors = android.productFlavors.collect { flavor -> flavor.name } - - // When no product flavors defined, use empty - if (!productFlavors) productFlavors.add('') - - productFlavors.each { productFlavorName -> - buildTypes.each { buildTypeName -> - // Create variant and source names - def sourceName = "${buildTypeName}" - def targetName = "${sourceName.capitalize()}" - if (productFlavorName) { - sourceName = "${productFlavorName}${targetName}" - } - - // React js bundle directories - def jsBundleDirConfigName = "jsBundleDir${targetName}" - def jsBundleDir = elvisFile(config."$jsBundleDirConfigName") ?: - file("$buildDir/intermediates/assets/${sourceName}") - - def resourcesDirConfigName = "jsBundleDir${targetName}" - def resourcesDir = elvisFile(config."${resourcesDirConfigName}") ?: - file("$buildDir/intermediates/res/merged/${sourceName}") - def jsBundleFile = file("$jsBundleDir/$bundleAssetName") - - // Bundle task name for variant - def bundleJsAndAssetsTaskName = "bundle${targetName}JsAndAssets" - - def currentBundleTask = tasks.create( - name: bundleJsAndAssetsTaskName, - type: Exec) { - group = "react" - description = "bundle JS and assets for ${targetName}." - - // Create dirs if they are not there (e.g. the "clean" task just ran) - doFirst { - jsBundleDir.mkdirs() - resourcesDir.mkdirs() - } - - // Set up inputs and outputs so gradle can cache the result - inputs.files fileTree(dir: reactRoot, excludes: inputExcludes) - outputs.dir jsBundleDir - outputs.dir resourcesDir - - // Set up the call to the react-native cli - workingDir reactRoot - - // Set up dev mode - def devEnabled = !targetName.toLowerCase().contains("release") - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - commandLine "cmd", "/c", "react-native", "bundle", "--platform", "android", "--dev", "${devEnabled}", - "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir - } else { - commandLine "react-native", "bundle", "--platform", "android", "--dev", "${devEnabled}", - "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir - } - - enabled config."bundleIn${targetName}" ?: targetName.toLowerCase().contains("release") - } - - // Hook bundle${productFlavor}${buildType}JsAndAssets into the android build process - currentBundleTask.dependsOn("merge${targetName}Resources") - currentBundleTask.dependsOn("merge${targetName}Assets") - - runBefore("processArmeabi-v7a${targetName}Resources", currentBundleTask) - runBefore("processX86${targetName}Resources", currentBundleTask) - runBefore("processUniversal${targetName}Resources", currentBundleTask) - runBefore("process${targetName}Resources", currentBundleTask) - } - } -} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000000..9b34ec89c86 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/android/app/src/debug/google-services.json b/android/app/src/debug/google-services.json deleted file mode 100644 index f05deec353f..00000000000 --- a/android/app/src/debug/google-services.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "project_info": { - "project_number": "854811651919", - "firebase_url": "https://status-react-app.firebaseio.com", - "project_id": "status-react-app", - "storage_bucket": "status-react-app.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:854811651919:android:11ee7444ded8a00a", - "android_client_info": { - "package_name": "im.status.ethereum.debug" - } - }, - "oauth_client": [ - { - "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} diff --git a/android/app/src/debug/res/raw/message.wav b/android/app/src/debug/res/raw/message.wav new file mode 100644 index 00000000000..13c9628295b Binary files /dev/null and b/android/app/src/debug/res/raw/message.wav differ diff --git a/android/app/src/debug/res/values/strings.xml b/android/app/src/debug/res/values/strings.xml index fd27483b602..c7db8572878 100644 --- a/android/app/src/debug/res/values/strings.xml +++ b/android/app/src/debug/res/values/strings.xml @@ -1,3 +1,3 @@ Status Debug - \ No newline at end of file + diff --git a/android/app/src/debug/res/xml/react_native_config.xml b/android/app/src/debug/res/xml/react_native_config.xml new file mode 100644 index 00000000000..8b11f2aee7d --- /dev/null +++ b/android/app/src/debug/res/xml/react_native_config.xml @@ -0,0 +1,8 @@ + + + + localhost + 10.0.3.2 + 10.0.2.2 + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4bf653238ca..da961875e71 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,47 +1,59 @@ + android:installLocation="auto"> + + + + + + + + + - + + + - - - - - - + + + - - + android:largeHeap="true" + android:usesCleartextTraffic="true" + android:extractNativeLibs="true" + android:requestLegacyExternalStorage="true"> + + @@ -59,34 +71,65 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + - + + + - - - - - - - - - - - - + + - + + + + + diff --git a/android/app/src/main/assets/fonts/Avenir-Roman.ttf b/android/app/src/main/assets/fonts/Avenir-Roman.ttf deleted file mode 100644 index f58eeaab873..00000000000 Binary files a/android/app/src/main/assets/fonts/Avenir-Roman.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Entypo.ttf b/android/app/src/main/assets/fonts/Entypo.ttf deleted file mode 100644 index 1c8f5e910bf..00000000000 Binary files a/android/app/src/main/assets/fonts/Entypo.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/EvilIcons.ttf b/android/app/src/main/assets/fonts/EvilIcons.ttf deleted file mode 100644 index b270f985192..00000000000 Binary files a/android/app/src/main/assets/fonts/EvilIcons.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/FontAwesome.ttf b/android/app/src/main/assets/fonts/FontAwesome.ttf deleted file mode 100644 index f221e50a2ef..00000000000 Binary files a/android/app/src/main/assets/fonts/FontAwesome.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Foundation.ttf b/android/app/src/main/assets/fonts/Foundation.ttf deleted file mode 100644 index 6cce217ddc2..00000000000 Binary files a/android/app/src/main/assets/fonts/Foundation.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Inter-Bold.ttf b/android/app/src/main/assets/fonts/Inter-Bold.ttf new file mode 100644 index 00000000000..728ab9be1f8 Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-Bold.ttf differ diff --git a/android/app/src/main/assets/fonts/Inter-BoldItalic.ttf b/android/app/src/main/assets/fonts/Inter-BoldItalic.ttf new file mode 100644 index 00000000000..9a7a216eb7c Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-BoldItalic.ttf differ diff --git a/android/app/src/main/assets/fonts/Inter-Italic.ttf b/android/app/src/main/assets/fonts/Inter-Italic.ttf new file mode 100644 index 00000000000..de23a382e65 Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-Italic.ttf differ diff --git a/android/app/src/main/assets/fonts/Inter-Medium.ttf b/android/app/src/main/assets/fonts/Inter-Medium.ttf new file mode 100644 index 00000000000..8913185821c Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-Medium.ttf differ diff --git a/android/app/src/main/assets/fonts/Inter-MediumItalic.ttf b/android/app/src/main/assets/fonts/Inter-MediumItalic.ttf new file mode 100644 index 00000000000..0c729891221 Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-MediumItalic.ttf differ diff --git a/android/app/src/main/assets/fonts/Inter-Regular.ttf b/android/app/src/main/assets/fonts/Inter-Regular.ttf new file mode 100644 index 00000000000..ffc482f0b87 Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-Regular.ttf differ diff --git a/android/app/src/main/assets/fonts/Inter-SemiBold.ttf b/android/app/src/main/assets/fonts/Inter-SemiBold.ttf new file mode 100644 index 00000000000..634e538d77c Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-SemiBold.ttf differ diff --git a/android/app/src/main/assets/fonts/Inter-SemiBoldItalic.ttf b/android/app/src/main/assets/fonts/Inter-SemiBoldItalic.ttf new file mode 100644 index 00000000000..2ae594bc840 Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-SemiBoldItalic.ttf differ diff --git a/android/app/src/main/assets/fonts/InterStatus-Regular.otf b/android/app/src/main/assets/fonts/InterStatus-Regular.otf new file mode 100644 index 00000000000..92c5dcb6e68 Binary files /dev/null and b/android/app/src/main/assets/fonts/InterStatus-Regular.otf differ diff --git a/android/app/src/main/assets/fonts/Ionicons.ttf b/android/app/src/main/assets/fonts/Ionicons.ttf deleted file mode 100644 index 307ad889aa8..00000000000 Binary files a/android/app/src/main/assets/fonts/Ionicons.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/MaterialIcons.ttf b/android/app/src/main/assets/fonts/MaterialIcons.ttf deleted file mode 100644 index 7015564ad16..00000000000 Binary files a/android/app/src/main/assets/fonts/MaterialIcons.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Octicons.ttf b/android/app/src/main/assets/fonts/Octicons.ttf deleted file mode 100644 index 0a62bb9ea69..00000000000 Binary files a/android/app/src/main/assets/fonts/Octicons.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Roboto-Bold.ttf b/android/app/src/main/assets/fonts/Roboto-Bold.ttf deleted file mode 100644 index aaf374d2cc0..00000000000 Binary files a/android/app/src/main/assets/fonts/Roboto-Bold.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Roboto-Light.ttf b/android/app/src/main/assets/fonts/Roboto-Light.ttf deleted file mode 100644 index 664e1b2f9db..00000000000 Binary files a/android/app/src/main/assets/fonts/Roboto-Light.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Roboto-Medium.ttf b/android/app/src/main/assets/fonts/Roboto-Medium.ttf deleted file mode 100644 index aa00de0ef92..00000000000 Binary files a/android/app/src/main/assets/fonts/Roboto-Medium.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Roboto-Regular.ttf b/android/app/src/main/assets/fonts/Roboto-Regular.ttf deleted file mode 100644 index 3e6e2e76134..00000000000 Binary files a/android/app/src/main/assets/fonts/Roboto-Regular.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/RobotoMono-Medium.ttf b/android/app/src/main/assets/fonts/RobotoMono-Medium.ttf deleted file mode 100644 index 0bcdc740c66..00000000000 Binary files a/android/app/src/main/assets/fonts/RobotoMono-Medium.ttf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/UbuntuMono-Regular.ttf b/android/app/src/main/assets/fonts/UbuntuMono-Regular.ttf new file mode 100644 index 00000000000..fdd309d7166 Binary files /dev/null and b/android/app/src/main/assets/fonts/UbuntuMono-Regular.ttf differ diff --git a/android/app/src/main/assets/fonts/Zocial.ttf b/android/app/src/main/assets/fonts/Zocial.ttf deleted file mode 100644 index e4ae46c6286..00000000000 Binary files a/android/app/src/main/assets/fonts/Zocial.ttf and /dev/null differ diff --git a/android/app/src/main/java/im/status/ethereum/MainActivity.java b/android/app/src/main/java/im/status/ethereum/MainActivity.java deleted file mode 100644 index 6b0172757b7..00000000000 --- a/android/app/src/main/java/im/status/ethereum/MainActivity.java +++ /dev/null @@ -1,229 +0,0 @@ -package im.status.ethereum; - -import android.content.Context; -import android.annotation.TargetApi; -import android.support.annotation.Nullable; -import android.app.AlertDialog; -import android.app.ActivityManager; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Looper; -import android.support.v4.app.ActivityCompat; -import android.util.Log; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.provider.Settings; -import android.os.Bundle; - -import com.facebook.react.ReactActivity; -import com.facebook.react.modules.core.PermissionListener; -import org.devio.rn.splashscreen.SplashScreen; -import com.testfairy.TestFairy; -import com.instabug.library.Instabug; - -import java.util.Properties; -import im.status.ethereum.module.StatusThreadPoolExecutor; - -public class MainActivity extends ReactActivity - implements ActivityCompat.OnRequestPermissionsResultCallback{ - - private static final int PERMISSION_REQUEST_CAMERA = 0; - - @Nullable private PermissionListener mPermissionListener; - - private static void registerUncaughtExceptionHandler(final Context context) { - final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); - Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(final Thread thread, final Throwable t) { - // High priority, so don't use StatusThreadPoolExecutor - new Thread() { - @Override - public void run() { - Looper.prepare(); - - new AlertDialog.Builder(context) - .setTitle("Error") - .setMessage(t.toString()) - .setNegativeButton("Exit", new DialogInterface.OnClickListener() { - public void onClick(final DialogInterface dialog, final int id) { - dialog.dismiss(); - defaultUncaughtExceptionHandler.uncaughtException(thread, t); - } - }).show(); - - Looper.loop(); - } - }.start(); - } - }); - } - - private ActivityManager getActivityManager() { - return (ActivityManager) this.getSystemService(ACTIVITY_SERVICE); - } - - private ActivityManager.MemoryInfo getAvailableMemory(final ActivityManager activityManager) { - final ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); - activityManager.getMemoryInfo(memoryInfo); - return memoryInfo; - } - - protected void configureStatus() { - // Required because of crazy APN settings redirecting localhost (found in GB) - Properties properties = System.getProperties(); - properties.setProperty("http.nonProxyHosts", "localhost|127.0.0.1"); - properties.setProperty("https.nonProxyHosts", "localhost|127.0.0.1"); - } - - private Intent createNotificationSettingsIntent() { - final Intent intent = new Intent(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); - } else { - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse("package:" + getPackageName())); - } - return intent; - } - - @Override - public void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - if (intent.getDataString() != null && intent.getData().getScheme().startsWith("app-settings")) { - startActivity(createNotificationSettingsIntent()); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - - // Make sure we get an Alert for every uncaught exceptions - registerUncaughtExceptionHandler(MainActivity.this); - - // Report memory details for this application - final ActivityManager activityManager = getActivityManager(); - Log.v("RNBootstrap", "Available system memory "+getAvailableMemory(activityManager).availMem + ", maximum usable application memory " + activityManager.getLargeMemoryClass()+"M"); - - - SplashScreen.show(this); - super.onCreate(savedInstanceState); - - Instabug.setIntroMessageEnabled(false); - if(BuildConfig.TESTFAIRY_ENABLED == "1") { - TestFairy.begin(this, "969f6c921cb435cea1d41d1ea3f5b247d6026d55"); - } - - if (!shouldShowRootedNotification()) { - configureStatus(); - } else { - AlertDialog dialog = new AlertDialog.Builder(MainActivity.this) - .setMessage(getResources().getString(R.string.root_warning)) - .setPositiveButton(getResources().getString(R.string.root_okay), new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - rejectRootedNotification(); - dialog.dismiss(); - configureStatus(); - } - }) - .setNegativeButton(getResources().getString(R.string.root_cancel), new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - MainActivity.this.finishAffinity(); - } - }) - .setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - dialog.dismiss(); - MainActivity.this.finishAffinity(); - } - }) - .create(); - - dialog.show(); - } - - Runnable r = new Runnable() { - @Override - public void run() { - System.loadLibrary("status-logs"); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - } - - /** - * Returns the name of the main component registered from JavaScript. - * This is used to schedule rendering of the component. - */ - @Override - protected String getMainComponentName() { - return "StatusIm"; - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - Intent intent = new Intent("onConfigurationChanged"); - intent.putExtra("newConfig", newConfig); - this.sendBroadcast(intent); - } - - private static final String REJECTED_ROOTED_NOTIFICATION = "rejectedRootedNotification"; - private static final Integer FREQUENCY_OF_REMINDER_IN_PERCENT = 5; - - private boolean shouldShowRootedNotification() { - if (RootUtil.isDeviceRooted()) { - if (userRejectedRootedNotification()) { - return ((Math.random() * 100) < FREQUENCY_OF_REMINDER_IN_PERCENT); - } else return true; - } else { - return false; - } - } - - private boolean userRejectedRootedNotification() { - SharedPreferences preferences = getPreferences(0); - return preferences.getBoolean(REJECTED_ROOTED_NOTIFICATION, false); - } - - private void rejectRootedNotification() { - SharedPreferences preferences = getPreferences(0); - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(REJECTED_ROOTED_NOTIFICATION, true); - editor.commit(); - } - - @TargetApi(Build.VERSION_CODES.M) - public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) { - mPermissionListener = listener; - requestPermissions(permissions, requestCode); - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) { - mPermissionListener = null; - } - if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Permission has been granted. Start camera preview Activity. - com.github.alinz.reactnativewebviewbridge.WebViewBridgeManager.grantAccess(requestCode); - } - } -} diff --git a/android/app/src/main/java/im/status/ethereum/MainActivity.kt b/android/app/src/main/java/im/status/ethereum/MainActivity.kt new file mode 100644 index 00000000000..c77da8d41c0 --- /dev/null +++ b/android/app/src/main/java/im/status/ethereum/MainActivity.kt @@ -0,0 +1,236 @@ +package im.status.ethereum + +import android.annotation.TargetApi +import android.app.ActivityManager +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.preference.PreferenceManager +import android.provider.Settings +import android.util.Log +import android.view.WindowManager +import androidx.annotation.Nullable +import androidx.core.app.ActivityCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint +import com.facebook.react.defaults.DefaultReactActivityDelegate +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.facebook.react.modules.core.PermissionListener +import com.reactnativenavigation.NavigationActivity +import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView +import im.status.ethereum.MainApplication +import im.status.ethereum.module.StatusThreadPoolExecutor +import java.util.Properties + +class MainActivity : NavigationActivity(), ActivityCompat.OnRequestPermissionsResultCallback { + + @Nullable + private var mPermissionListener: PermissionListener? = null + private var keepSplash = true + private val SPLASH_DELAY = 3200 + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + protected fun getMainComponentName(): String { + return "StatusIm" + } + + private fun registerUncaughtExceptionHandler(context: Context) { + val defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + // High priority, so don't use StatusThreadPoolExecutor + Thread.setDefaultUncaughtExceptionHandler { thread, t -> + Thread { + Looper.prepare() + AlertDialog.Builder(context) + .setTitle("Error") + .setMessage(t.toString()) + .setNegativeButton("Exit") { dialog, id -> + dialog.dismiss() + defaultUncaughtExceptionHandler.uncaughtException(thread, t) + }.show() + Looper.loop() + }.start() + } + } + + private fun getActivityManager(): ActivityManager { + return getSystemService(ACTIVITY_SERVICE) as ActivityManager + } + + private fun getAvailableMemory(activityManager: ActivityManager): ActivityManager.MemoryInfo { + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + return memoryInfo + } + + protected fun configureStatus() { + // Required because of crazy APN settings redirecting localhost (found in GB) + val properties = System.getProperties() + properties.setProperty("http.nonProxyHosts", "localhost|127.0.0.1") + properties.setProperty("https.nonProxyHosts", "localhost|127.0.0.1") + } + + private fun createNotificationSettingsIntent(): Intent { + val intent = Intent() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + } else { + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + intent.addCategory(Intent.CATEGORY_DEFAULT) + intent.data = Uri.parse("package:$packageName") + } + return intent + } + + private fun tryToEmit(eventName: String, event: WritableMap) { + try { + (getApplication() as MainApplication).reactNativeHost + .getReactInstanceManager() + .getCurrentReactContext() + ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + ?.emit("url", event) + } catch (e: Exception) { // we expect NPE on first start, which is OK because we have a fallback + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (intent.dataString != null && intent.data!!.scheme!!.startsWith("app-settings")) { + startActivity(createNotificationSettingsIntent()) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen(); + setTheme(R.style.DarkTheme) + // Make sure we get an Alert for every uncaught exceptions + registerUncaughtExceptionHandler(this) + + // Report memory details for this application + val activityManager = getActivityManager() + Log.v("RNBootstrap", "Available system memory " + getAvailableMemory(activityManager).availMem + ", maximum usable application memory " + activityManager.largeMemoryClass + "M") + + setSecureFlag() + + // NOTE: Try to not restore the state https://github.com/software-mansion/react-native-screens/issues/17 + super.onCreate(null) + + if (!shouldShowRootedNotification()) { + configureStatus() + } else { + val dialog = AlertDialog.Builder(this) + .setMessage(resources.getString(R.string.root_warning)) + .setPositiveButton(resources.getString(R.string.root_okay)) { dialog, which -> + rejectRootedNotification() + dialog.dismiss() + configureStatus() + } + .setNegativeButton(resources.getString(R.string.root_cancel)) { dialog, which -> + dialog.dismiss() + finishAffinity() + } + .setOnCancelListener { + it.dismiss() + finishAffinity() + } + .create() + + dialog.show() + } + + val r = Runnable { + System.loadLibrary("status-logs") + + // when app is started but the Activity has been destroyed, the deep linking url event is + // not emitted when coming back to foreground. This is a workaround. If the problem is + // resolved in react-native this code should be removed + if (intent.data != null) { + val event = Arguments.createMap() + event.putString("url", intent.dataString) + // on first start emit will (silently) fail, but the regular deep linking handler will work + tryToEmit("url", event) + } + } + + splashScreen.setKeepOnScreenCondition { keepSplash } + + val handler = Handler() + handler.postDelayed({ keepSplash = false }, SPLASH_DELAY.toLong()) + + StatusThreadPoolExecutor.getInstance().execute(r) + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + val intent = Intent("onConfigurationChanged") + intent.putExtra("newConfig", newConfig) + sendBroadcast(intent) + } + + private val REJECTED_ROOTED_NOTIFICATION = "rejectedRootedNotification" + private val FREQUENCY_OF_REMINDER_IN_PERCENT = 5 + + private fun shouldShowRootedNotification(): Boolean { + if (RootUtil.isDeviceRooted() && BuildConfig.ENABLE_ROOT_ALERT == "1") { + return if (userRejectedRootedNotification()) { + (Math.random() * 100) < FREQUENCY_OF_REMINDER_IN_PERCENT + } else true + } else { + return false + } + } + + private fun userRejectedRootedNotification(): Boolean { + val preferences = getPreferences(0) + return preferences.getBoolean(REJECTED_ROOTED_NOTIFICATION, false) + } + + private fun rejectRootedNotification() { + val preferences = getPreferences(0) + val editor = preferences.edit() + editor.putBoolean(REJECTED_ROOTED_NOTIFICATION, true) + editor.commit() + } + + private fun setSecureFlag() { + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) + val setSecure = sharedPrefs.getBoolean("BLANK_PREVIEW", false) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && setSecure) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + + @TargetApi(Build.VERSION_CODES.M) + override fun requestPermissions(permissions: Array, requestCode: Int, listener: PermissionListener) { + mPermissionListener = listener + super.requestPermissions(permissions, requestCode) + } + + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (mPermissionListener != null && mPermissionListener!!.onRequestPermissionsResult(requestCode, permissions, grantResults)) { + mPermissionListener = null + } + } +} diff --git a/android/app/src/main/java/im/status/ethereum/MainApplication.java b/android/app/src/main/java/im/status/ethereum/MainApplication.java deleted file mode 100644 index fc7a4c18db3..00000000000 --- a/android/app/src/main/java/im/status/ethereum/MainApplication.java +++ /dev/null @@ -1,103 +0,0 @@ -package im.status.ethereum; - -import android.support.multidex.MultiDexApplication; -import com.aakashns.reactnativedialogs.ReactNativeDialogsPackage; -import org.devio.rn.splashscreen.SplashScreenReactPackage; -import com.facebook.react.ReactApplication; -import net.rhogan.rnsecurerandom.RNSecureRandomPackage; -import com.instabug.reactlibrary.RNInstabugReactnativePackage; -import com.ocetnik.timer.BackgroundTimerPackage; -import com.horcrux.svg.SvgPackage; -import com.evollu.react.fcm.FIRMessagingPackage; -import com.lugg.ReactNativeConfig.ReactNativeConfigPackage; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactPackage; -import com.facebook.react.shell.MainReactPackage; -import com.facebook.soloader.SoLoader; -import com.github.alinz.reactnativewebviewbridge.WebViewBridgePackage; -import com.AlexanderZaytsev.RNI18n.RNI18nPackage; -import com.lwansbrough.RCTCamera.RCTCameraPackage; -import com.reactnative.ivpusic.imagepicker.PickerPackage; -import com.rnfs.RNFSPackage; -import fr.bamlab.rnimageresizer.ImageResizerPackage; -import im.status.ethereum.module.StatusPackage; -import io.realm.react.RealmReactPackage; -import me.alwx.HttpServer.HttpServerReactPackage; -import com.testfairy.react.TestFairyPackage; -import com.oblador.keychain.KeychainPackage; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import im.status.ethereum.function.Function; - -public class MainApplication extends MultiDexApplication implements ReactApplication { - - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { - @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - @Override - protected List getPackages() { - boolean devCluster = false; - if (BuildConfig.ETHEREUM_DEV_CLUSTER == "1") { - devCluster = true; - } - - boolean webViewDebugEnabled = false; - if (BuildConfig.DEBUG_WEBVIEW == "1") { - webViewDebugEnabled = true; - } - - StatusPackage statusPackage = new StatusPackage(BuildConfig.DEBUG, devCluster, BuildConfig.LOG_LEVEL_STATUS_GO); - Function callRPC = statusPackage.getCallRPC(); - List packages = new ArrayList(Arrays.asList( - new MainReactPackage(), - new RNSecureRandomPackage(), - new BackgroundTimerPackage(), - new SvgPackage(), - new FIRMessagingPackage(), - new HttpServerReactPackage(), - new SplashScreenReactPackage(), - statusPackage, - new RealmReactPackage(), - new RNI18nPackage(), - new RCTCameraPackage(), - new RNFSPackage(), - new ReactNativeDialogsPackage(), - new ImageResizerPackage(), - new PickerPackage(), - new TestFairyPackage(), - new WebViewBridgePackage(webViewDebugEnabled, callRPC), - new ReactNativeConfigPackage(), - new KeychainPackage(), - new RNInstabugReactnativePackage.Builder(BuildConfig.INSTABUG_TOKEN,MainApplication.this) - .setInvocationEvent("shake") - .setPrimaryColor("#1D82DC") - .setFloatingEdge("left") - .setFloatingButtonOffsetFromTop(250) - .build() - )); - - return packages; - } - - @Override - protected String getJSMainModuleName() { - return "index.android"; - } - }; - - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; - } - - @Override - public void onCreate() { - super.onCreate(); - SoLoader.init(this, /* native exopackage */ false); - } -} diff --git a/android/app/src/main/java/im/status/ethereum/MainApplication.kt b/android/app/src/main/java/im/status/ethereum/MainApplication.kt new file mode 100644 index 00000000000..8963c19a510 --- /dev/null +++ b/android/app/src/main/java/im/status/ethereum/MainApplication.kt @@ -0,0 +1,59 @@ +package im.status.ethereum + +import android.webkit.WebView +import androidx.multidex.MultiDexApplication +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.JSIModulePackage +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint +import com.facebook.react.modules.network.OkHttpClientProvider +import com.reactnativenavigation.NavigationApplication +import com.reactnativenavigation.react.NavigationReactNativeHost +import cl.json.RNSharePackage +import com.reactnativecommunity.blurview.BlurViewPackage +import im.status.ethereum.keycard.RNStatusKeycardPackage +import im.status.ethereum.module.StatusPackage +import im.status.ethereum.pushnotifications.PushNotificationPackage +import im.status.ethereum.StatusOkHttpClientFactory + +class MainApplication : NavigationApplication() { + + private val mReactNativeHost = object : NavigationReactNativeHost(this) { + override fun getUseDeveloperSupport(): Boolean { + return BuildConfig.DEBUG + } + + override fun getPackages(): List = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here + add(StatusPackage(RootUtil.isDeviceRooted())) + add(RNStatusKeycardPackage()) + add(PushNotificationPackage()) + add(BlurViewPackage()) + } + + override fun getJSMainModuleName(): String = "index" + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + + override val reactNativeHost: ReactNativeHost + get() = mReactNativeHost + + override fun onCreate() { + super.onCreate() + + OkHttpClientProvider.setOkHttpClientFactory(StatusOkHttpClientFactory()) + + WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG_WEBVIEW == "1") + + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + DefaultNewArchitectureEntryPoint.load() + } + } +} diff --git a/android/app/src/main/java/im/status/ethereum/RootUtil.java b/android/app/src/main/java/im/status/ethereum/RootUtil.java deleted file mode 100644 index 5c74f566df8..00000000000 --- a/android/app/src/main/java/im/status/ethereum/RootUtil.java +++ /dev/null @@ -1,40 +0,0 @@ -package im.status.ethereum; - -import java.io.File; -import java.io.BufferedReader; -import java.io.InputStreamReader; - -/** @author Kevin Kowalewski */ -public class RootUtil { - - public static boolean isDeviceRooted() { - return checkRootMethod1() || checkRootMethod2() || checkRootMethod3(); - } - - private static boolean checkRootMethod1() { - String buildTags = android.os.Build.TAGS; - return buildTags != null && buildTags.contains("test-keys"); - } - - private static boolean checkRootMethod2() { - String[] paths = { "/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", - "/system/bin/failsafe/su", "/data/local/su" }; - for (String path : paths) { - if (new File(path).exists()) return true; - } - return false; - } - - private static boolean checkRootMethod3() { - Process process = null; - try { - process = Runtime.getRuntime().exec(new String[] { "/system/xbin/which", "su" }); - BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream())); - return in.readLine() != null; - } catch (Throwable t) { - return false; - } finally { - if (process != null) process.destroy(); - } - } -} diff --git a/android/app/src/main/java/im/status/ethereum/RootUtil.kt b/android/app/src/main/java/im/status/ethereum/RootUtil.kt new file mode 100644 index 00000000000..c6729842778 --- /dev/null +++ b/android/app/src/main/java/im/status/ethereum/RootUtil.kt @@ -0,0 +1,42 @@ +package im.status.ethereum + +import android.os.Build +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader + +object RootUtil { + + fun isDeviceRooted(): Boolean = + checkRootMethod1() || checkRootMethod2() || checkRootMethod3() + + private fun checkRootMethod1(): Boolean = + Build.TAGS?.contains("test-keys") ?: false + + private fun checkRootMethod2(): Boolean { + val paths = arrayOf( + "/system/app/Superuser.apk", + "/sbin/su", + "/system/bin/su", + "/system/xbin/su", + "/data/local/xbin/su", + "/data/local/bin/su", + "/system/sd/xbin/su", + "/system/bin/failsafe/su", + "/data/local/su" + ) + return paths.any { File(it).exists() } + } + + private fun checkRootMethod3(): Boolean { + var process: Process? = null + return try { + process = Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su")) + BufferedReader(InputStreamReader(process.inputStream)).readLine() != null + } catch (t: Throwable) { + false + } finally { + process?.destroy() + } + } +} diff --git a/android/app/src/main/java/im/status/ethereum/StatusOKHttpClientFactory.kt b/android/app/src/main/java/im/status/ethereum/StatusOKHttpClientFactory.kt new file mode 100644 index 00000000000..a8458b8d38f --- /dev/null +++ b/android/app/src/main/java/im/status/ethereum/StatusOKHttpClientFactory.kt @@ -0,0 +1,88 @@ +package im.status.ethereum + +import android.util.Log +import com.facebook.react.modules.network.OkHttpClientFactory +import com.facebook.react.modules.network.OkHttpClientProvider +import okhttp3.OkHttpClient +import okhttp3.tls.HandshakeCertificates +import java.io.ByteArrayInputStream +import java.lang.RuntimeException +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import im.status.ethereum.module.StatusPackage + +class StatusOkHttpClientFactory : OkHttpClientFactory { + + companion object { + private const val TAG = "StatusOkHttpClientFactory" + private const val SLEEP_DURATION = 500L // milliseconds + } + + override fun createNewNetworkModuleClient(): OkHttpClient? { + val certPem = getCertificatePem().takeIf { it.isNotEmpty() } + ?: return logAndReturnNull("Certificate is empty, cannot create OkHttpClient without a valid certificate") + + val cert = try { + induceSleep() + // Convert PEM certificate string to X509Certificate object + CertificateFactory.getInstance("X.509") + .generateCertificate(ByteArrayInputStream(certPem.toByteArray())) as? X509Certificate + ?: return logAndReturnNull("Certificate could not be parsed as non-null") + } catch (e: Exception) { + return logAndReturnNull("Could not parse certificate", e) + } + + val clientCertificates = try { + induceSleep() + // Create HandshakeCertificates object with our certificate + HandshakeCertificates.Builder() + .addPlatformTrustedCertificates() + .addTrustedCertificate(cert) + .build() + } catch (e: Exception) { + return logAndReturnNull("Could not build HandshakeCertificates", e) + } + + return try { + OkHttpClientProvider.createClientBuilder() + .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) + .build() + } catch (e: Exception) { + logAndReturnNull("Could not create OkHttpClient", e) + } + } + + private fun getCertificatePem(): String { + return try { + // Create OkHttpClient with custom SSL socket factory and trust manager + StatusPackage.getImageTLSCert().takeIf { !it.isNullOrBlank() } + ?: logAndReturnEmpty("Certificate PEM string is null or empty") + } catch (e: Exception) { + logAndReturnEmpty("Could not getImageTLSCert", e) + } + } + + private fun induceSleep() { + try { + // induce half second sleep because sometimes a cert is not immediately available + // TODO : remove sleep if App no longer crashes on Android 10 devices with + // java.lang.RuntimeException: Could not invoke WebSocketModule.connect + Thread.sleep(SLEEP_DURATION) + } catch (e: InterruptedException) { + Log.e(TAG, "Sleep interrupted", e) + } + } + + private fun logAndReturnNull(message: String, e: Exception? = null): Nothing? { + Log.e(TAG, message, e) + return null + } + + private fun logAndReturnEmpty(message: String, e: Exception? = null): String { + Log.e(TAG, message, e) + return "" + } +} + diff --git a/android/app/src/main/res/drawable-hdpi/corner_left_bottom.png b/android/app/src/main/res/drawable-hdpi/corner_left_bottom.png deleted file mode 100644 index 1c9c9b446d2..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/corner_left_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/corner_left_top.png b/android/app/src/main/res/drawable-hdpi/corner_left_top.png deleted file mode 100644 index 1a0a4e57f82..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/corner_left_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/corner_right_bottom.png b/android/app/src/main/res/drawable-hdpi/corner_right_bottom.png deleted file mode 100644 index e6071d1ae10..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/corner_right_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/corner_right_top.png b/android/app/src/main/res/drawable-hdpi/corner_right_top.png deleted file mode 100644 index 6eaca3cd9a0..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/corner_right_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_background.png b/android/app/src/main/res/drawable-hdpi/ic_background.png new file mode 100644 index 00000000000..583635a1c9d Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_background.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_foreground.png new file mode 100644 index 00000000000..ca5e7ce1d20 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_status_notification.png b/android/app/src/main/res/drawable-hdpi/ic_stat_status_notification.png deleted file mode 100755 index a040aa06c8b..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/ic_stat_status_notification.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_action_back.png b/android/app/src/main/res/drawable-hdpi/icon_action_back.png deleted file mode 100644 index 89db7e29865..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_action_back.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_action_forward.png b/android/app/src/main/res/drawable-hdpi/icon_action_forward.png deleted file mode 100644 index b8abf47c489..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_action_forward.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_action_fullscreen_collapse.png b/android/app/src/main/res/drawable-hdpi/icon_action_fullscreen_collapse.png deleted file mode 100644 index 8bc5016f2ce..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_action_fullscreen_collapse.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_action_fullscreen_expand.png b/android/app/src/main/res/drawable-hdpi/icon_action_fullscreen_expand.png deleted file mode 100644 index d5b72bb7689..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_action_fullscreen_expand.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_arrow_top.png b/android/app/src/main/res/drawable-hdpi/icon_arrow_top.png deleted file mode 100644 index 193fe2261df..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_arrow_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_avatar.png b/android/app/src/main/res/drawable-hdpi/icon_avatar.png deleted file mode 100644 index d28594fcf10..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_avatar.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_check_on.png b/android/app/src/main/res/drawable-hdpi/icon_check_on.png deleted file mode 100644 index d8318e7fcb1..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_check_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_close_light_gray.png b/android/app/src/main/res/drawable-hdpi/icon_close_light_gray.png deleted file mode 100644 index 4b6d97ccefc..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_close_light_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_forward_gray.png b/android/app/src/main/res/drawable-hdpi/icon_forward_gray.png deleted file mode 100644 index 9b96935894e..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_forward_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_lock_gray.png b/android/app/src/main/res/drawable-hdpi/icon_lock_gray.png deleted file mode 100644 index 5e7c9580466..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_lock_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_lock_white.png b/android/app/src/main/res/drawable-hdpi/icon_lock_white.png deleted file mode 100644 index 700c8fdf815..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_lock_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_menu_group.png b/android/app/src/main/res/drawable-hdpi/icon_menu_group.png deleted file mode 100644 index 40e9b7dc416..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_menu_group.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_money_white.png b/android/app/src/main/res/drawable-hdpi/icon_money_white.png deleted file mode 100755 index 6510ff9c047..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_money_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_muted.png b/android/app/src/main/res/drawable-hdpi/icon_muted.png deleted file mode 100644 index 2c862042ba2..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_muted.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_notifications_on.png b/android/app/src/main/res/drawable-hdpi/icon_notifications_on.png deleted file mode 100644 index 207cb4c16f4..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_notifications_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_phone_white.png b/android/app/src/main/res/drawable-hdpi/icon_phone_white.png deleted file mode 100755 index d60a9ac7a20..00000000000 Binary files a/android/app/src/main/res/drawable-hdpi/icon_phone_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/splash_logo.png b/android/app/src/main/res/drawable-hdpi/splash_logo.png new file mode 100644 index 00000000000..ea5d96b73a6 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/corner_left_bottom.png b/android/app/src/main/res/drawable-mdpi/corner_left_bottom.png deleted file mode 100644 index da09ea7a24c..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/corner_left_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/corner_left_top.png b/android/app/src/main/res/drawable-mdpi/corner_left_top.png deleted file mode 100644 index 988d93651c4..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/corner_left_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/corner_right_bottom.png b/android/app/src/main/res/drawable-mdpi/corner_right_bottom.png deleted file mode 100644 index e74544e83ba..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/corner_right_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/corner_right_top.png b/android/app/src/main/res/drawable-mdpi/corner_right_top.png deleted file mode 100644 index 33726dfa018..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/corner_right_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_background.png b/android/app/src/main/res/drawable-mdpi/ic_background.png new file mode 100644 index 00000000000..a4e87e1d481 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_background.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_foreground.png new file mode 100644 index 00000000000..94fe86346bb Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_status_notification.png b/android/app/src/main/res/drawable-mdpi/ic_stat_status_notification.png deleted file mode 100755 index 7810cd58a1c..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/ic_stat_status_notification.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_action_back.png b/android/app/src/main/res/drawable-mdpi/icon_action_back.png deleted file mode 100644 index 8e31377a598..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_action_back.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_action_forward.png b/android/app/src/main/res/drawable-mdpi/icon_action_forward.png deleted file mode 100644 index af802ac9eb0..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_action_forward.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_action_fullscreen_collapse.png b/android/app/src/main/res/drawable-mdpi/icon_action_fullscreen_collapse.png deleted file mode 100644 index 09c4944904b..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_action_fullscreen_collapse.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_action_fullscreen_expand.png b/android/app/src/main/res/drawable-mdpi/icon_action_fullscreen_expand.png deleted file mode 100644 index a862a39fc51..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_action_fullscreen_expand.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_arrow_top.png b/android/app/src/main/res/drawable-mdpi/icon_arrow_top.png deleted file mode 100644 index dd47f4ff17a..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_arrow_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_avatar.png b/android/app/src/main/res/drawable-mdpi/icon_avatar.png deleted file mode 100644 index d76df40ebc0..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_avatar.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_check_on.png b/android/app/src/main/res/drawable-mdpi/icon_check_on.png deleted file mode 100644 index a44f891d2fa..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_check_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_close_light_gray.png b/android/app/src/main/res/drawable-mdpi/icon_close_light_gray.png deleted file mode 100644 index 4918a29a643..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_close_light_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_forward_gray.png b/android/app/src/main/res/drawable-mdpi/icon_forward_gray.png deleted file mode 100644 index 66dd050bda0..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_forward_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_lock_gray.png b/android/app/src/main/res/drawable-mdpi/icon_lock_gray.png deleted file mode 100644 index dce5f8dff8c..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_lock_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_lock_white.png b/android/app/src/main/res/drawable-mdpi/icon_lock_white.png deleted file mode 100644 index 7da52c18af8..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_lock_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_menu_group.png b/android/app/src/main/res/drawable-mdpi/icon_menu_group.png deleted file mode 100644 index f17ab21946f..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_menu_group.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_money_white.png b/android/app/src/main/res/drawable-mdpi/icon_money_white.png deleted file mode 100755 index 1584a09d0a2..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_money_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_muted.png b/android/app/src/main/res/drawable-mdpi/icon_muted.png deleted file mode 100644 index 21c7dc39622..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_muted.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_notifications_on.png b/android/app/src/main/res/drawable-mdpi/icon_notifications_on.png deleted file mode 100644 index 89a9ca8f15d..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_notifications_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_phone_white.png b/android/app/src/main/res/drawable-mdpi/icon_phone_white.png deleted file mode 100755 index 224f48c50de..00000000000 Binary files a/android/app/src/main/res/drawable-mdpi/icon_phone_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/logo.png b/android/app/src/main/res/drawable-mdpi/logo.png new file mode 100644 index 00000000000..5d415cc689a Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/notification_icon.png b/android/app/src/main/res/drawable-mdpi/notification_icon.png new file mode 100644 index 00000000000..a0f3fa51ee4 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/notification_icon.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash_logo.png b/android/app/src/main/res/drawable-mdpi/splash_logo.png new file mode 100644 index 00000000000..f7383cec215 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash_logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/corner_left_bottom.png b/android/app/src/main/res/drawable-xhdpi/corner_left_bottom.png deleted file mode 100644 index b43f25a58da..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/corner_left_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/corner_left_top.png b/android/app/src/main/res/drawable-xhdpi/corner_left_top.png deleted file mode 100644 index f8180a9c1c7..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/corner_left_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/corner_right_bottom.png b/android/app/src/main/res/drawable-xhdpi/corner_right_bottom.png deleted file mode 100644 index ce0a7ab9c09..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/corner_right_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/corner_right_top.png b/android/app/src/main/res/drawable-xhdpi/corner_right_top.png deleted file mode 100644 index 2ae2e61860b..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/corner_right_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_background.png b/android/app/src/main/res/drawable-xhdpi/ic_background.png new file mode 100644 index 00000000000..3d628c39623 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_background.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_foreground.png new file mode 100644 index 00000000000..a5a79d8d79a Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_status_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_status_notification.png deleted file mode 100755 index ab8aeaa8d9d..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/ic_stat_status_notification.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_action_back.png b/android/app/src/main/res/drawable-xhdpi/icon_action_back.png deleted file mode 100644 index 491112378ef..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_action_back.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_action_forward.png b/android/app/src/main/res/drawable-xhdpi/icon_action_forward.png deleted file mode 100644 index 453add7dec6..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_action_forward.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_action_fullscreen_collapse.png b/android/app/src/main/res/drawable-xhdpi/icon_action_fullscreen_collapse.png deleted file mode 100644 index 80948ff1915..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_action_fullscreen_collapse.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_action_fullscreen_expand.png b/android/app/src/main/res/drawable-xhdpi/icon_action_fullscreen_expand.png deleted file mode 100644 index 35fcdd6ae63..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_action_fullscreen_expand.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_arrow_top.png b/android/app/src/main/res/drawable-xhdpi/icon_arrow_top.png deleted file mode 100644 index 9125df76b51..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_arrow_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_avatar.png b/android/app/src/main/res/drawable-xhdpi/icon_avatar.png deleted file mode 100644 index 63e1078f8ee..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_avatar.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_check_on.png b/android/app/src/main/res/drawable-xhdpi/icon_check_on.png deleted file mode 100644 index ef90004718a..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_check_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_close_light_gray.png b/android/app/src/main/res/drawable-xhdpi/icon_close_light_gray.png deleted file mode 100644 index 03a45042681..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_close_light_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_forward_gray.png b/android/app/src/main/res/drawable-xhdpi/icon_forward_gray.png deleted file mode 100644 index de0eccda60c..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_forward_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_lock_gray.png b/android/app/src/main/res/drawable-xhdpi/icon_lock_gray.png deleted file mode 100644 index 40369d54240..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_lock_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_lock_white.png b/android/app/src/main/res/drawable-xhdpi/icon_lock_white.png deleted file mode 100644 index 827cd722424..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_lock_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_menu_group.png b/android/app/src/main/res/drawable-xhdpi/icon_menu_group.png deleted file mode 100644 index 28c99ec40f5..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_menu_group.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_money_white.png b/android/app/src/main/res/drawable-xhdpi/icon_money_white.png deleted file mode 100755 index da077578fa6..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_money_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_muted.png b/android/app/src/main/res/drawable-xhdpi/icon_muted.png deleted file mode 100644 index 41956196703..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_muted.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_notifications_on.png b/android/app/src/main/res/drawable-xhdpi/icon_notifications_on.png deleted file mode 100644 index 9322a10becb..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_notifications_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_phone_white.png b/android/app/src/main/res/drawable-xhdpi/icon_phone_white.png deleted file mode 100755 index f08867e855e..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/icon_phone_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/launch_logo.png b/android/app/src/main/res/drawable-xhdpi/launch_logo.png deleted file mode 100644 index 9765bd3a27a..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/launch_logo.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/launch_screen.png b/android/app/src/main/res/drawable-xhdpi/launch_screen.png deleted file mode 100644 index b5013c75f87..00000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/launch_screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/logo.png b/android/app/src/main/res/drawable-xhdpi/logo.png new file mode 100644 index 00000000000..f1d87020b90 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/notification_icon.png b/android/app/src/main/res/drawable-xhdpi/notification_icon.png new file mode 100644 index 00000000000..14530bdc2c5 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/notification_icon.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splash_logo.png b/android/app/src/main/res/drawable-xhdpi/splash_logo.png new file mode 100644 index 00000000000..44007cad485 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/corner_left_bottom.png b/android/app/src/main/res/drawable-xxhdpi/corner_left_bottom.png deleted file mode 100644 index 0fff3412209..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/corner_left_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/corner_left_top.png b/android/app/src/main/res/drawable-xxhdpi/corner_left_top.png deleted file mode 100644 index e25037ece32..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/corner_left_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/corner_right_bottom.png b/android/app/src/main/res/drawable-xxhdpi/corner_right_bottom.png deleted file mode 100644 index e542ec0d64d..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/corner_right_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/corner_right_top.png b/android/app/src/main/res/drawable-xxhdpi/corner_right_top.png deleted file mode 100644 index a6a65aedc96..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/corner_right_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_background.png b/android/app/src/main/res/drawable-xxhdpi/ic_background.png new file mode 100644 index 00000000000..f664414df47 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_background.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_foreground.png new file mode 100644 index 00000000000..9e49b93dce1 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_status_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_status_notification.png deleted file mode 100755 index 79708073580..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_stat_status_notification.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_avatar.png b/android/app/src/main/res/drawable-xxhdpi/icon_avatar.png deleted file mode 100644 index f3b8d0770d0..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_avatar.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_check_on.png b/android/app/src/main/res/drawable-xxhdpi/icon_check_on.png deleted file mode 100644 index 6388824eb66..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_check_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_close_light_gray.png b/android/app/src/main/res/drawable-xxhdpi/icon_close_light_gray.png deleted file mode 100644 index 8f9b0d0b809..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_close_light_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_forward_gray.png b/android/app/src/main/res/drawable-xxhdpi/icon_forward_gray.png deleted file mode 100644 index 629d017ffbe..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_forward_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_lock_gray.png b/android/app/src/main/res/drawable-xxhdpi/icon_lock_gray.png deleted file mode 100644 index f18aaa61e18..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_lock_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_lock_white.png b/android/app/src/main/res/drawable-xxhdpi/icon_lock_white.png deleted file mode 100644 index ccddc79dec7..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_lock_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_menu_group.png b/android/app/src/main/res/drawable-xxhdpi/icon_menu_group.png deleted file mode 100644 index a356f7ccdd3..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_menu_group.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_money_white.png b/android/app/src/main/res/drawable-xxhdpi/icon_money_white.png deleted file mode 100755 index 243469d9a98..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_money_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_muted.png b/android/app/src/main/res/drawable-xxhdpi/icon_muted.png deleted file mode 100644 index 674a9faa669..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_muted.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_notifications_on.png b/android/app/src/main/res/drawable-xxhdpi/icon_notifications_on.png deleted file mode 100644 index a0ee3c3fd5b..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_notifications_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_phone_white.png b/android/app/src/main/res/drawable-xxhdpi/icon_phone_white.png deleted file mode 100755 index b49ec0439d4..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/icon_phone_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/launch_logo.png b/android/app/src/main/res/drawable-xxhdpi/launch_logo.png deleted file mode 100644 index 1eb8bd8d616..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/launch_logo.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/launch_screen.png b/android/app/src/main/res/drawable-xxhdpi/launch_screen.png deleted file mode 100644 index 727d1fdae76..00000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/launch_screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/logo.png b/android/app/src/main/res/drawable-xxhdpi/logo.png new file mode 100644 index 00000000000..aa6f1830016 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/notification_icon.png b/android/app/src/main/res/drawable-xxhdpi/notification_icon.png new file mode 100644 index 00000000000..41d6814e024 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/notification_icon.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash_logo.png b/android/app/src/main/res/drawable-xxhdpi/splash_logo.png new file mode 100644 index 00000000000..b258ed1ace2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/corner_left_bottom.png b/android/app/src/main/res/drawable-xxxhdpi/corner_left_bottom.png deleted file mode 100644 index 79d8ff98d39..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/corner_left_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/corner_left_top.png b/android/app/src/main/res/drawable-xxxhdpi/corner_left_top.png deleted file mode 100644 index 8ccdd18d77b..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/corner_left_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/corner_right_bottom.png b/android/app/src/main/res/drawable-xxxhdpi/corner_right_bottom.png deleted file mode 100644 index 1fa347dffe8..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/corner_right_bottom.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/corner_right_top.png b/android/app/src/main/res/drawable-xxxhdpi/corner_right_top.png deleted file mode 100644 index a26ebe0e614..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/corner_right_top.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_background.png b/android/app/src/main/res/drawable-xxxhdpi/ic_background.png new file mode 100644 index 00000000000..4aeb9a15ba1 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_background.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_foreground.png new file mode 100644 index 00000000000..ea5d96b73a6 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_status_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_status_notification.png deleted file mode 100755 index c43ceadb5c9..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_status_notification.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_avatar.png b/android/app/src/main/res/drawable-xxxhdpi/icon_avatar.png deleted file mode 100644 index a0ab8e66571..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_avatar.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_check_on.png b/android/app/src/main/res/drawable-xxxhdpi/icon_check_on.png deleted file mode 100644 index 780956a86bd..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_check_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_close_light_gray.png b/android/app/src/main/res/drawable-xxxhdpi/icon_close_light_gray.png deleted file mode 100644 index cc04ce0d256..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_close_light_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_forward_gray.png b/android/app/src/main/res/drawable-xxxhdpi/icon_forward_gray.png deleted file mode 100644 index 78bf7faa089..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_forward_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_lock_gray.png b/android/app/src/main/res/drawable-xxxhdpi/icon_lock_gray.png deleted file mode 100644 index 8b3739e6a8b..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_lock_gray.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_lock_white.png b/android/app/src/main/res/drawable-xxxhdpi/icon_lock_white.png deleted file mode 100644 index 41875c78c23..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_lock_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_menu_group.png b/android/app/src/main/res/drawable-xxxhdpi/icon_menu_group.png deleted file mode 100644 index 7c51fd4dd03..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_menu_group.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_money_white.png b/android/app/src/main/res/drawable-xxxhdpi/icon_money_white.png deleted file mode 100755 index 9fcfca74965..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_money_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_muted.png b/android/app/src/main/res/drawable-xxxhdpi/icon_muted.png deleted file mode 100644 index bb46070875c..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_muted.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_notifications_on.png b/android/app/src/main/res/drawable-xxxhdpi/icon_notifications_on.png deleted file mode 100644 index cc4a37280a9..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_notifications_on.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_phone_white.png b/android/app/src/main/res/drawable-xxxhdpi/icon_phone_white.png deleted file mode 100755 index eb66d11f8d8..00000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/icon_phone_white.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/logo.png b/android/app/src/main/res/drawable-xxxhdpi/logo.png new file mode 100644 index 00000000000..b756ca0b651 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png b/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png new file mode 100644 index 00000000000..f82b908d9c7 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splash_logo.png new file mode 100644 index 00000000000..5d39707224e Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash_logo.png differ diff --git a/android/app/src/main/res/ic_launcher-web.png b/android/app/src/main/res/ic_launcher-web.png index ea08459f160..52c3837fe46 100644 Binary files a/android/app/src/main/res/ic_launcher-web.png and b/android/app/src/main/res/ic_launcher-web.png differ diff --git a/android/app/src/main/res/layout/launch_screen.xml b/android/app/src/main/res/layout/launch_screen.xml index b72e71736d6..2f37c5fe4fe 100644 --- a/android/app/src/main/res/layout/launch_screen.xml +++ b/android/app/src/main/res/layout/launch_screen.xml @@ -1,21 +1,21 @@ + android:layout_height="match_parent" + android:background="?attr/backgroundColor"> - - - - - \ No newline at end of file + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000000..f31858d7719 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000000..f31858d7719 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_background.png b/android/app/src/main/res/mipmap-hdpi/ic_background.png new file mode 100644 index 00000000000..cbf5ce6fee2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_background.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_foreground.png new file mode 100644 index 00000000000..d36823d2707 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 1926c282683..9d27512c3cd 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000000..09acba2aaf9 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000000..39f58863538 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png index cb7ae284fe1..4b1d945bae1 100644 Binary files a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png and b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png new file mode 100644 index 00000000000..a1191f67944 Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_background.png b/android/app/src/main/res/mipmap-mdpi/ic_background.png new file mode 100644 index 00000000000..b0221607704 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_foreground.png new file mode 100644 index 00000000000..61fff9f2344 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 3513e194971..1e4d9017606 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000000..c9142881511 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000000..e39eada7bc9 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_background.png new file mode 100644 index 00000000000..791a2fe2795 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_background.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png new file mode 100644 index 00000000000..d2fcaed1a78 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index a3beaa561df..c4b071d30d7 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000000..6ccb6c747ff Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..9faa82c14fe Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_background.png new file mode 100644 index 00000000000..f664414df47 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_background.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png new file mode 100644 index 00000000000..649230679ff Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 02039f3e03a..b5bf2cc1406 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000000..340b47dbbb3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..8e5765dcd61 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png new file mode 100644 index 00000000000..7af6e2cf4d5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png new file mode 100644 index 00000000000..ca890da1d60 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 19ec9084fdc..9d195a9116e 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000000..ad43de25986 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..8a883d4f277 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/raw/launching_animation.json b/android/app/src/main/res/raw/launching_animation.json new file mode 100644 index 00000000000..d45da7f722e --- /dev/null +++ b/android/app/src/main/res/raw/launching_animation.json @@ -0,0 +1 @@ +{"v":"5.7.8","fr":60,"ip":0,"op":91,"w":128,"h":128,"nm":"!!LOGO_lottie","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Merged Shape Layer 6","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[3.789]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[-192.857]},{"i":{"x":[0.833],"y":[0.762]},"o":{"x":[0.167],"y":[0.167]},"t":6.545,"s":[-193.857]},{"i":{"x":[0.833],"y":[0.691]},"o":{"x":[0.167],"y":[0.155]},"t":7.363,"s":[-191.765]},{"i":{"x":[0.833],"y":[0.797]},"o":{"x":[0.167],"y":[0.122]},"t":10.637,"s":[-178.897]},{"i":{"x":[0.833],"y":[0.855]},"o":{"x":[0.167],"y":[0.153]},"t":14.727,"s":[-138.079]},{"i":{"x":[0.833],"y":[0.814]},"o":{"x":[0.167],"y":[0.224]},"t":22.908,"s":[-29.589]},{"i":{"x":[0.833],"y":[0.801]},"o":{"x":[0.167],"y":[0.162]},"t":27.818,"s":[12.5]},{"i":{"x":[0.833],"y":[0.873]},"o":{"x":[0.167],"y":[0.107]},"t":45,"s":[181.996]},{"i":{"x":[0.833],"y":[0.923]},"o":{"x":[0.167],"y":[0.227]},"t":49.908,"s":[272.04]},{"i":{"x":[0.833],"y":[0.94]},"o":{"x":[0.167],"y":[0.514]},"t":55.637,"s":[330.762]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[-2.596]},"t":65.455,"s":[345.875]},{"t":81.818359375,"s":[345.292]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.791,"y":0.712},"o":{"x":0.652,"y":0.344},"t":8.182,"s":[-57.414,88.804,0],"to":[-266.586,66.696,0],"ti":[-471.296,0.211,0]},{"i":{"x":0.672,"y":0.664},"o":{"x":0.291,"y":0.351},"t":26.613,"s":[44.704,-314.789,0],"to":[74.296,1.789,0],"ti":[-24.095,-183.222,0]},{"i":{"x":0.414,"y":0.331},"o":{"x":0.136,"y":0.106},"t":34.889,"s":[318.5,-68,0],"to":[29.675,225.652,0],"ti":[174.57,-11.464,0]},{"i":{"x":0.288,"y":1},"o":{"x":0.288,"y":1},"t":44.047,"s":[-7.935,330.275,0],"to":[-375.415,-24.719,0],"ti":[-390,21,0]},{"t":81.818359375,"s":[53.652,-82.594,0]}],"ix":2,"l":2},"a":{"a":0,"k":[560,410.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.623,0.623,0.833],"y":[0.806,0.806,1.942]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8.182,"s":[100,100,100]},{"i":{"x":[0.615,0.615,0.833],"y":[-9.004,-9.004,-0.939]},"o":{"x":[0.32,0.32,0.167],"y":[-6.872,-6.872,-1.07]},"t":23.283,"s":[63.265,63.265,100]},{"i":{"x":[0.532,0.532,0.667],"y":[1,1,1]},"o":{"x":[0.407,0.407,0.167],"y":[0.429,0.429,3.289]},"t":40.443,"s":[63.265,63.265,100]},{"t":69.544921875,"s":[105,105,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8.182,"s":[{"i":[[29.256,-1.636],[12.654,-5.359],[8.617,-28.501],[-28.968,-8.636],[-24.783,-3.988],[-20.286,7.565],[3.491,32.586],[22.957,14.401]],"o":[[-19.802,1.107],[-62.447,26.447],[28.952,1.188],[19.806,5.905],[23.203,3.734],[32.599,-12.156],[-2.92,-27.253],[-17.795,-11.163]],"v":[[552.771,323.203],[505.624,334.015],[404.252,446.909],[473.294,460.187],[557.264,486.313],[622.075,480.875],[668.525,402.251],[624.928,339.765]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10.637,"s":[{"i":[[35.646,-6.61],[29.714,-25.56],[-5.458,-8.042],[-24.339,-5.662],[-31.956,-1.833],[-22.776,10.595],[3.231,33.642],[29.445,12.037]],"o":[[-29.737,6.28],[-33.566,28.873],[5.468,8.056],[24.339,5.662],[31.956,1.833],[30.472,-13.464],[-4.304,-33.253],[-24.528,-9.336]],"v":[[515.251,324.945],[432.694,362.132],[374.686,460.302],[435.7,469.78],[534.564,489.883],[624.004,480.477],[668.476,402.398],[607.104,332.181]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14.727,"s":[{"i":[[46.296,-14.902],[24.146,-47.639],[-14.286,-9.646],[-32.343,13.44],[-30.439,13.873],[-26.928,15.644],[2.798,35.403],[40.258,8.096]],"o":[[-46.296,14.902],[-32.901,64.912],[7.844,5.296],[32.343,-13.44],[41.324,-15.99],[26.928,-15.644],[-6.611,-43.252],[-35.748,-6.292]],"v":[[452.248,330.653],[340.333,427.05],[331.295,569.328],[388.653,559.038],[494.121,516.795],[621.12,482.157],[663.333,400.633],[576.423,320.768]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[46.329,-30.799],[9.206,-101.382],[-26.527,-8.11],[-23.981,21.663],[-27.026,18.669],[-27.972,10.72],[0.825,47.829],[49.937,10.873]],"o":[[-46.329,30.799],[-6.708,73.874],[23.569,6.669],[25.302,-22.856],[25.053,-17.306],[29.125,-11.006],[-2.651,-57.164],[-45.744,-9.256]],"v":[[379.868,359.18],[255.626,543.131],[308.895,689.081],[385.909,642.222],[491.706,538.051],[592.014,497.121],[656.681,411.345],[564.93,316.12]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20.455,"s":[{"i":[[64.565,-52.319],[10.188,-103.027],[-34.457,-9.254],[-24.784,40.268],[-45.418,34.675],[-26.649,9.512],[2.702,63.401],[53.773,2.903]],"o":[[-51.36,40.959],[-9.005,86.228],[32.953,8.587],[25.24,-40.488],[36.098,-27.559],[28.678,-10.033],[-3.84,-69.666],[-53.609,-2.058]],"v":[[318.842,389.727],[195.107,601.075],[271.374,775.287],[373.46,679.376],[465.38,554.672],[571.854,508.306],[654.682,407.541],[535.788,310.26]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22.092,"s":[{"i":[[79.222,-63.486],[15.85,-103.554],[-39.269,-11.688],[-27.26,51.436],[-59.282,43.184],[-26.212,7.402],[0.865,73.868],[56.15,0.205]],"o":[[-56.671,45.204],[-14.727,93.965],[38.768,11.464],[27.152,-50.985],[44.777,-32.618],[28.849,-7.981],[-1.239,-78.137],[-58.715,0.133]],"v":[[278.98,398.976],[145.383,622.417],[219.236,827.081],[321.65,722.651],[438.056,558.321],[546.812,508.94],[653.065,410.221],[527.368,313.788]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22.908,"s":[{"i":[[88.24,-66.875],[21.23,-103.3],[-41.334,-13.936],[-29.916,56.289],[-67.396,45.724],[-26.133,5.706],[-2.025,79.062],[57.349,0.274]],"o":[[-60.475,45.833],[-20.006,97.346],[41.334,13.936],[29.504,-55.514],[49.972,-33.903],[29.088,-6.239],[2.109,-82.33],[-61.277,-0.292]],"v":[[261.383,400.535],[115.172,622.607],[185.068,841.779],[296.59,729.919],[419.424,554.344],[533.802,511.454],[652.175,414.378],[525.102,315.747]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[79.206,-55.07],[34.985,-104.299],[-42.561,-16.337],[-35.547,56.801],[-74.269,39.628],[-26.803,6.098],[-2.508,70.574],[53.383,2.857]],"o":[[-71.711,40.918],[-33.75,99.227],[42.561,16.337],[44.194,-81.881],[51.209,-28.805],[29.168,-6.524],[2.444,-72.114],[-61.034,-4.458]],"v":[[259.091,379.56],[62.788,598.549],[117.611,837.642],[244.281,713.451],[395.457,545.595],[544.293,507.648],[652.581,414.871],[538.559,317.657]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[88.573,-35.243],[54.717,-109.178],[-44.347,-19.446],[-41.907,58.686],[-84.031,29.734],[-27.837,6.159],[-4.542,61.804],[47.75,7.066]],"o":[[-87.545,32.48],[-52.212,102.123],[44.346,19.446],[63.83,-104.145],[53.421,-18.892],[29.386,-6.448],[3.563,-57.999],[-60.415,-11.137]],"v":[[284.722,355.414],[25.649,572.493],[47.058,813.868],[189.834,694.714],[402.92,527.491],[557.36,502.833],[653.024,417.228],[562.218,320.949]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26.182,"s":[{"i":[[52.752,-17.839],[78.034,-114.944],[-46.456,-23.119],[-49.422,60.915],[-95.568,18.043],[-29.058,6.232],[-6.946,51.442],[41.094,12.038]],"o":[[-106.257,22.509],[-74.028,105.546],[46.456,23.119],[63.258,-77.968],[55.352,-11.602],[29.644,-6.358],[4.885,-41.321],[-59.684,-19.031]],"v":[[343.855,339.288],[0.542,548.387],[-20.386,790.87],[141.422,677.669],[416.79,520.423],[572.801,497.145],[653.549,420.013],[584.566,330.441]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[43.07,-7.849],[90.005,-108.293],[-47.472,-25.94],[-58.072,58.848],[-101.759,15.243],[-29.485,7.665],[-4.439,36.618],[37.52,13.193]],"o":[[-116.65,21.257],[-88.725,106.752],[47.472,25.94],[55.225,-55.962],[56.154,-8.412],[29.485,-7.665],[3.788,-31.249],[-60.064,-21.119]],"v":[[386.723,335.167],[7.232,529.027],[-44.498,775.809],[130.381,668.23],[427.379,517.498],[586.257,492.426],[654.202,416.845],[592.878,332.108]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[44.276,-9.501],[92.678,-97.204],[-43.839,-29.268],[-63.11,53.812],[-96.226,16.884],[-30.135,8.192],[-3.931,36.439],[39.485,12.843]],"o":[[-113.609,22.765],[-90.624,94.765],[43.702,29.176],[59.471,-50.709],[57.153,-11.132],[30.135,-8.192],[3.354,-31.081],[-60.976,-20.193]],"v":[[379.087,333.684],[21.384,525.463],[-45.868,757.813],[132.646,666.302],[426.199,511.645],[586.778,490.796],[654.789,415.852],[590.825,332.473]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":34.363,"s":[{"i":[[51.95,-20.013],[47.111,-81.464],[-37.667,-22.586],[-60.422,59.227],[-61.018,27.321],[-34.269,11.551],[-0.7,35.297],[51.989,10.621]],"o":[[-94.257,32.363],[-41.697,72.103],[36.628,21.963],[53.557,-52.765],[63.512,-28.438],[34.269,-11.551],[0.595,-30.014],[-66.778,-14.296]],"v":[[345.105,371.328],[91.668,574.153],[71.198,750.239],[211.554,652.871],[394.335,520.673],[590.09,480.427],[658.524,409.528],[577.761,334.795]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[60.597,-17.144],[55.738,-88.07],[-35.721,-21.835],[-52.845,48.263],[-52.868,16.415],[-40.067,1.439],[-7.978,34.246],[60.029,22.238]],"o":[[-79.624,19.63],[-59.491,90.469],[31.596,14.564],[59.618,-57.163],[53.386,-16.576],[43.14,-1.659],[7.69,-33.012],[-63.674,-23.588]],"v":[[375.49,339.43],[98.306,541.725],[47.861,762.978],[201.734,605.95],[401.05,491.915],[577.877,488.873],[660.255,431.731],[585.708,338.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45.818,"s":[{"i":[[59.539,-20.244],[47.714,-88.677],[-35.756,-18.463],[-47.228,49.234],[-57.954,15.189],[-40.35,2.243],[-7.563,34.736],[60.472,20.586]],"o":[[-104.048,33.408],[-52.426,93.275],[31.389,12.094],[52.583,-56.964],[52.241,-19.065],[43.423,-2.311],[7.257,-33.199],[-68.206,-22.475]],"v":[[373.478,351.274],[112.92,565.233],[83.609,785.614],[222.477,620.276],[408.233,501.25],[578.764,490.053],[661.094,430.613],[583.469,337.255]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46.637,"s":[{"i":[[57.712,-25.504],[36.433,-90.98],[-36.447,-13.777],[-39.793,51.902],[-62.468,19.926],[-40.634,3.047],[-7.148,35.225],[60.914,18.933]],"o":[[-94.357,38.947],[-41.933,97.946],[31.607,8.473],[43.446,-58.655],[52.271,-21.234],[43.705,-2.964],[6.824,-33.386],[-72.737,-21.362]],"v":[[369.092,363.635],[139.739,595.719],[129.151,823.514],[250.732,640.492],[419.312,512],[579.65,491.233],[661.932,429.495],[581.231,336.135]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47.455,"s":[{"i":[[55.886,-30.764],[25.152,-93.284],[-37.137,-9.091],[-32.357,54.571],[-54.109,23.906],[-40.917,3.851],[-6.733,35.714],[61.356,17.281]],"o":[[-84.667,44.486],[-31.441,102.617],[31.825,4.853],[34.308,-60.345],[49.456,-26.726],[43.987,-3.616],[6.391,-33.574],[-77.269,-20.249]],"v":[[364.707,375.996],[166.557,626.204],[179.604,848.983],[278.987,660.708],[431.33,522.675],[580.537,492.412],[662.77,428.377],[578.992,335.014]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":48.273,"s":[{"i":[[54.06,-36.024],[13.871,-95.586],[-37.828,-4.406],[-24.921,57.239],[-45.75,27.885],[-41.201,4.654],[-6.318,36.204],[61.799,15.629]],"o":[[-74.977,50.025],[-20.948,107.289],[32.043,1.232],[25.17,-62.036],[46.641,-32.218],[44.269,-4.268],[5.958,-33.761],[-81.8,-19.136]],"v":[[360.321,388.358],[193.375,656.689],[235.527,864.013],[307.241,680.924],[439.013,526.779],[581.423,493.592],[663.609,427.259],[576.753,333.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49.092,"s":[{"i":[[54.229,-36.905],[5.57,-101.387],[-36.641,-0.768],[-18.517,56.284],[-43.774,26.557],[-41.347,4.599],[-6.649,36.331],[63.855,15.952]],"o":[[-72.741,49.715],[-9.702,103.79],[30.91,-1.363],[17.66,-59.396],[43.888,-30.122],[44.357,-3.993],[6.15,-33.603],[-80.543,-18.707]],"v":[[362.232,386.897],[219.032,657.044],[282.368,849.948],[335.912,674.032],[446.191,523.631],[581.228,494.34],[664.433,427.968],[577.151,334.124]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49.908,"s":[{"i":[[55.851,-35.639],[1.081,-106.649],[-35.286,1.356],[-14.408,54.346],[-42.927,23.411],[-41.691,2.902],[-8.386,36.225],[65.306,18.743]],"o":[[-72.524,46.428],[-2.751,99.475],[29.734,-2.677],[12.638,-55.763],[42.437,-26.174],[44.627,-1.954],[7.69,-33.217],[-78.547,-21.547]],"v":[[366.116,380.681],[232.937,642.825],[309.173,823.03],[351.888,657.114],[446.876,515.689],[575.915,495.971],[662.811,432.816],[578.799,335.002]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50.727,"s":[{"i":[[60.314,-29.713],[5.377,-111.641],[-33.983,0.571],[-14.751,51.127],[-43.936,16.689],[-42.203,-2.234],[-13.085,35.369],[65.102,26.881]],"o":[[-76.009,37.087],[-3.991,94.765],[28.729,-1.537],[12.189,-50.993],[43.07,-18.685],[44.984,3.765],[11.954,-32.141],[-74.643,-30.821]],"v":[[376.485,357.42],[231.958,600.145],[306.12,773.627],[351.602,621.51],[442.789,497.128],[567.236,497.647],[662.876,444.974],[590.334,336.436]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":51.545,"s":[{"i":[[58.454,-25.382],[12.681,-98.321],[-31.559,-2.887],[-18.537,44.967],[-43.332,12.958],[-41.167,-3.588],[-14.185,34.151],[58.033,30.328]],"o":[[-69.015,30.489],[-12.424,86.472],[26.845,1.924],[15.157,-43.44],[43.664,-17.851],[43.766,5.393],[12.87,-30.749],[-63.143,-32.099]],"v":[[384.844,342.58],[232.615,547.383],[290,720.149],[345.565,591.838],[431.399,489.858],[566.257,498.124],[662.432,448.126],[599.974,337.45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":52.363,"s":[{"i":[[60.624,-26.033],[15.363,-79.978],[-27.604,-4.648],[-19.302,37.316],[-42.727,9.226],[-40.356,-0.588],[-11.64,34.294],[53.659,27.394]],"o":[[-83.841,33.567],[-16.149,74.105],[23.706,3.945],[15.658,-34.2],[41.582,-11.502],[42.942,2.391],[10.487,-30.583],[-54.449,-26.153]],"v":[[384.655,338.807],[231.988,523.839],[277.919,673.482],[335.725,573.116],[426.23,485.259],[573.209,496.029],[662.821,442.929],[599.313,337.255]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":53.182,"s":[{"i":[[60.048,-18.04],[30.218,-72.336],[-23.744,-10.961],[-26.815,27.74],[-59.275,9.159],[-41.895,-4.369],[-10.342,34.691],[48.819,25.855]],"o":[[-55.027,17.293],[-26.75,64.032],[20.738,9.824],[22.695,-23.478],[54.694,-8.451],[42.8,0.661],[9.135,-30.641],[-45.428,-20.735]],"v":[[400.303,331.387],[248.699,467.457],[260.73,611.694],[340.25,545.13],[442.156,481.453],[576.562,497.201],[665.233,438.89],[603.252,335.098]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54,"s":[{"i":[[56.897,-17.397],[38.265,-58.304],[-18.542,-13.031],[-28.19,18.585],[-56.997,10.729],[-39.901,-2.575],[-9.289,34.498],[47.944,23.807]],"o":[[-52.871,17.484],[-34.393,52.086],[16.577,12.127],[23.802,-15.741],[50.309,-5.992],[41.897,-0.884],[8.223,-30.629],[-44.847,-19.267]],"v":[[413.774,330.395],[284.179,421.614],[274.821,546.65],[356.095,516.769],[450.839,480.228],[580.534,495.137],[667.25,435.326],[606.323,335.728]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54.818,"s":[{"i":[[54.245,-16.24],[32.598,-43.781],[-13.881,-13.328],[-27.245,10.844],[-52.193,8.882],[-38.212,-1.181],[-8.763,34.48],[51.03,23.553]],"o":[[-51.284,17.177],[-34.856,42.266],[12.833,12.969],[22.946,-9.211],[46.029,-3.066],[41.384,-1.933],[7.793,-30.777],[-48.137,-21.126]],"v":[[428.16,331.264],[318.256,403.889],[293.813,516.44],[369.963,507.474],[462.486,480.365],[584.382,494.182],[667.934,433.567],[608.325,336.102]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":58.092,"s":[{"i":[[43.639,-11.614],[19.055,-21.071],[-3.889,-4.135],[-14.525,-1.576],[-29.412,-8.791],[-31.456,4.392],[-6.661,34.408],[44.084,17.289]],"o":[[-44.939,15.949],[-19.81,21.906],[6.308,6.708],[11.937,1.295],[28.909,8.641],[39.333,-6.127],[6.072,-31.368],[-42.526,-15.47]],"v":[[485.056,328.469],[396.287,387.701],[369.078,452.515],[403.714,458.848],[495.883,475.935],[599.772,490.363],[670.673,426.531],[607.6,332.794]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60.545,"s":[{"i":[[28.22,0.324],[12.906,-4.721],[9.327,-29.791],[-28.742,-9.153],[-24.553,-5.22],[-20.638,6.542],[2.522,32.675],[22.21,15.529]],"o":[[-19.831,-0.228],[-63.69,23.297],[28.857,2.631],[19.693,6.271],[22.988,4.887],[33.165,-10.514],[-1.99,-25.781],[-17.216,-12.037]],"v":[[557.533,321.811],[498.304,333.119],[393.386,442.624],[471.037,459.143],[553.818,485.938],[618.82,483.742],[668.474,407.752],[628.809,344.92]],"c":true}]},{"t":65.455078125,"s":[{"i":[[29.256,-1.636],[12.654,-5.359],[8.617,-28.501],[-28.968,-8.636],[-24.783,-3.988],[-20.286,7.565],[3.491,32.586],[22.957,14.401]],"o":[[-19.802,1.107],[-62.447,26.447],[28.952,1.188],[19.806,5.905],[23.203,3.734],[32.599,-12.156],[-2.92,-27.253],[-17.795,-11.163]],"v":[[552.771,323.203],[505.624,334.015],[404.252,446.909],[473.294,460.187],[557.264,486.313],[622.075,480.875],[668.525,402.251],[624.928,339.765]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.250979988248,0.721568986481,0.588234994926,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91,"st":-11,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"new_drop1","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.689]},"o":{"x":[0.167],"y":[0.167]},"t":6.545,"s":[-14.347]},{"i":{"x":[0.833],"y":[0.789]},"o":{"x":[0.167],"y":[0.114]},"t":10.637,"s":[0.326]},{"i":{"x":[0.833],"y":[0.87]},"o":{"x":[0.167],"y":[0.151]},"t":14.727,"s":[40.484]},{"i":{"x":[0.833],"y":[0.806]},"o":{"x":[0.167],"y":[0.186]},"t":22.908,"s":[153.117]},{"i":{"x":[0.833],"y":[0.873]},"o":{"x":[0.167],"y":[0.102]},"t":45,"s":[366.201]},{"i":{"x":[0.833],"y":[0.924]},"o":{"x":[0.167],"y":[0.228]},"t":49.908,"s":[456.075]},{"i":{"x":[0.833],"y":[0.945]},"o":{"x":[0.167],"y":[0.561]},"t":55.637,"s":[514.342]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[-1.414]},"t":65.455,"s":[527.802]},{"t":83.455078125,"s":[526.848]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.791,"y":0.711},"o":{"x":0.652,"y":0.346},"t":8.182,"s":[53.414,-82.804,0],"to":[266.586,-66.696,0],"ti":[471.296,-0.211,0]},{"i":{"x":0.672,"y":0.664},"o":{"x":0.291,"y":0.35},"t":26.613,"s":[-44.704,314.789,0],"to":[-74.296,-1.789,0],"ti":[24.095,183.222,0]},{"i":{"x":0.414,"y":0.331},"o":{"x":0.136,"y":0.106},"t":34.889,"s":[-318.5,68,0],"to":[-29.675,-225.652,0],"ti":[-174.57,11.464,0]},{"i":{"x":0.288,"y":1},"o":{"x":0.29,"y":1},"t":44.047,"s":[7.935,-330.275,0],"to":[375.415,24.719,0],"ti":[390,-21,0]},{"t":81.818359375,"s":[-57.824,89.667,0]}],"ix":2,"l":2},"a":{"a":0,"k":[560,410.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.623,0.623,0.833],"y":[0.793,0.793,1.942]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":8.182,"s":[105,105,100]},{"i":{"x":[0.615,0.615,0.833],"y":[-9.004,-9.004,-0.939]},"o":{"x":[0.32,0.32,0.167],"y":[-6.872,-6.872,-1.07]},"t":23.283,"s":[70.547,70.547,100]},{"i":{"x":[0.532,0.532,0.667],"y":[1,1,1]},"o":{"x":[0.407,0.407,0.167],"y":[0.591,0.591,3.196]},"t":40.443,"s":[70.547,70.547,100]},{"t":68.7265625,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8.182,"s":[{"i":[[29.256,-1.636],[12.654,-5.359],[8.617,-28.501],[-28.968,-8.636],[-24.783,-3.988],[-20.286,7.565],[3.491,32.586],[22.957,14.401]],"o":[[-19.802,1.107],[-62.447,26.447],[28.952,1.188],[19.806,5.905],[23.203,3.734],[32.599,-12.156],[-2.92,-27.253],[-17.795,-11.163]],"v":[[552.771,323.203],[505.624,334.015],[404.252,446.909],[473.294,460.187],[557.264,486.313],[622.075,480.875],[668.525,402.251],[624.928,339.765]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10.637,"s":[{"i":[[35.646,-6.61],[29.714,-25.56],[-5.458,-8.042],[-24.339,-5.662],[-31.956,-1.833],[-22.776,10.595],[3.231,33.642],[29.445,12.037]],"o":[[-29.737,6.28],[-33.566,28.873],[5.468,8.056],[24.339,5.662],[31.956,1.833],[30.472,-13.464],[-4.304,-33.253],[-24.528,-9.336]],"v":[[515.251,324.945],[435.35,364.682],[379.454,457.143],[436.661,468.177],[538.465,491.522],[618.412,480.135],[662.46,401.018],[603.745,333.35]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14.727,"s":[{"i":[[46.296,-14.902],[25.654,-41.975],[-12.411,-9.901],[-30.679,9.975],[-36.109,12.657],[-26.928,15.644],[2.798,35.403],[40.258,8.096]],"o":[[-46.296,14.902],[-34.955,57.193],[6.815,5.436],[30.679,-9.975],[41.324,-15.99],[26.928,-15.644],[-6.611,-43.252],[-35.748,-6.292]],"v":[[452.248,330.653],[349.544,418.17],[334.02,549.151],[385.979,543.95],[494.712,516.604],[609.938,480.878],[652.352,398.963],[565.911,321.557]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[46.299,-16.229],[25.223,-44.785],[-14.312,-8.764],[-29.132,13.264],[-35.087,17.189],[-26.668,15.692],[3.512,36.17],[41.001,7.348]],"o":[[-46.299,16.23],[-33.63,58.297],[8.585,5.04],[29.364,-13.44],[47.516,-22.057],[26.753,-15.736],[-7.266,-43.807],[-36.51,-5.645]],"v":[[446.17,335.328],[343.935,425.06],[336.74,568.517],[390.433,555.664],[494.669,518.468],[615.02,483.177],[658.058,401.022],[569.847,323.672]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[46.309,-21.086],[23.647,-55.061],[-29.449,-15.295],[-26.095,17.291],[-25.89,21.87],[-25.717,15.868],[6.123,38.977],[43.717,4.612]],"o":[[-46.309,21.086],[-28.781,62.334],[13.711,5.156],[27.142,-18.146],[56.674,-32.922],[26.112,-16.071],[-9.659,-45.837],[-39.296,-3.281]],"v":[[427.552,344.658],[320.257,451.253],[335.779,615.638],[396.852,587.851],[494.511,525.284],[614.116,480.305],[653.219,395.451],[554.114,323.076]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[50.874,-30.153],[20.495,-75.614],[-27.143,-14.724],[-23.602,18.606],[-16.627,16.893],[-31.675,11.482],[11.345,44.591],[55.689,-3.223]],"o":[[-47.858,28.366],[-19.084,70.409],[25.466,10.945],[27.119,-21.379],[29.344,-29.813],[29.775,-15.339],[-14.445,-49.896],[-41.559,5.601]],"v":[[392.332,367.062],[286.273,507.681],[328.632,672.3],[405.833,628.449],[494.195,538.917],[595.274,490.855],[646.745,388.536],[522.648,321.886]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[{"i":[[47.876,-39.067],[-1.493,-87.716],[-33.186,-6.385],[-20.836,38.763],[-32.797,26.281],[-22.697,14.619],[14.746,51.862],[55.358,-4.207]],"o":[[-47.876,39.067],[1.506,88.468],[31.64,5.908],[26.52,-39.942],[41.687,-43.224],[24.486,-15.507],[-16.763,-55.537],[-51.359,4.227]],"v":[[340.188,395.388],[225.533,612.286],[306.086,765.697],[392.682,674.77],[469.386,553.989],[593.181,490.089],[642.817,387.036],[511.322,321.364]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[51.265,-39.755],[0.551,-90.546],[-35.981,-6.35],[-20.3,47.041],[-41.532,29.872],[-23.105,12.196],[12.781,56.402],[58.621,-1.996]],"o":[[-51.265,39.755],[0.113,88.161],[35.176,6.272],[22.773,-47.833],[45.977,-38.344],[25.338,-13.139],[-13.994,-59.394],[-54.774,2.012]],"v":[[321.939,399.283],[196.493,623.914],[282.356,788.609],[369.309,686.696],[459.573,545.854],[579.65,494.411],[643.311,393.539],[509.611,324.553]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[{"i":[[54.655,-40.444],[21.671,-95.144],[-37.963,-13.705],[-28.782,51.483],[-50.267,33.464],[-23.513,9.772],[10.816,60.942],[61.884,0.215]],"o":[[-54.655,40.444],[-20.422,89.659],[37.963,13.705],[28.385,-50.774],[50.267,-33.464],[26.19,-10.771],[-11.226,-63.251],[-58.189,-0.202]],"v":[[296.919,402.702],[168.072,605.071],[227.024,801.31],[335.872,705.635],[436.307,547.324],[565.885,497.106],[643.57,398.416],[507.666,326.116]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[51.352,-33.861],[27.009,-88.13],[-39.938,-14.519],[-33.749,52.795],[-62.301,32.617],[-23.737,9.926],[8.332,53.733],[54.243,1.817]],"o":[[-69.052,39.888],[-31.555,92.567],[39.938,14.519],[30.003,-66.106],[51.275,-29.247],[25.745,-10.674],[-8.607,-54.288],[-57.202,-3.019]],"v":[[310.318,383.13],[130.903,588.955],[171.032,802.299],[289.305,702.667],[433.374,531.562],[573.674,492.141],[643.853,398.494],[525.74,328.775]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[48.049,-27.278],[32.346,-81.117],[-41.913,-15.333],[-38.716,54.106],[-74.334,31.77],[-23.962,10.079],[5.847,46.524],[46.602,3.419]],"o":[[-83.448,39.332],[-42.688,95.476],[41.913,15.333],[69.784,-113.901],[52.284,-25.031],[25.3,-10.578],[-5.987,-45.325],[-56.214,-5.836]],"v":[[325.781,370.853],[100.33,581.377],[121.894,802.211],[246.691,696.362],[431.548,522.436],[581.464,487.175],[644.135,398.573],[543.815,331.435]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[{"i":[[44.745,-20.695],[37.684,-74.103],[-43.889,-16.146],[-43.683,55.418],[-86.367,30.923],[-24.186,10.232],[3.362,39.315],[38.961,5.021]],"o":[[-119.264,43.039],[-53.821,98.384],[43.889,16.146],[53.717,-78.902],[53.292,-20.814],[24.855,-10.482],[-3.368,-36.362],[-55.227,-8.653]],"v":[[351.853,367.665],[83.444,578.458],[83.037,797.956],[218.71,689.28],[440.332,522.4],[589.253,482.21],[644.417,398.652],[561.889,334.094]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[41.442,-14.112],[43.022,-67.09],[-45.864,-16.96],[-48.65,56.729],[-98.4,30.077],[-24.41,10.386],[0.877,32.106],[31.32,6.623]],"o":[[-112.242,38.22],[-64.954,101.292],[45.864,16.96],[37.651,-43.904],[54.3,-16.598],[24.41,-10.386],[-0.749,-27.399],[-54.239,-11.47]],"v":[[377.924,364.478],[86.477,578.389],[64.098,796.551],[210.646,685.049],[449.115,522.363],[597.042,477.244],[644.699,398.73],[579.964,336.754]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":34,"s":[{"i":[[44.513,-23.712],[27.482,-56.256],[-37.784,-12.866],[-50.63,58.126],[-58.35,29.482],[-29.403,14.275],[3.378,32.034],[47.465,3.666]],"o":[[-83.032,40.247],[-28.361,58.056],[37.359,12.721],[44.616,-51.418],[56.174,-28.817],[29.403,-14.275],[-2.873,-27.245],[-61.731,-5.418]],"v":[[360.522,399.512],[167.659,595.984],[159.499,760.219],[281.433,660.858],[421.356,527.51],[592.399,468.7],[643.096,401.03],[557.766,341.618]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":34.363,"s":[{"i":[[44.672,-24.21],[31.158,-53.285],[-36.18,-15.729],[-55.363,53.834],[-56.271,29.451],[-29.662,14.476],[3.508,32.03],[48.303,3.512]],"o":[[-81.516,40.352],[-31.158,53.285],[35.747,15.541],[49.074,-47.961],[56.271,-29.451],[29.662,-14.476],[-2.983,-27.237],[-62.12,-5.104]],"v":[[353.995,392.968],[165.032,568.445],[141.322,727.462],[269.779,639.11],[419.915,527.777],[566.624,471.102],[623.232,402.797],[535.507,347.324]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[{"i":[[45.26,-24.45],[27.597,-58.817],[-37.262,-10.276],[-47.761,60.94],[-55.381,28.571],[-42.375,15.472],[3.29,32.053],[49.693,3.91]],"o":[[-79.368,39.383],[-29.194,62.221],[36.766,10.14],[42.806,-54.803],[55.381,-28.571],[31.516,-11.507],[-2.797,-27.247],[-62.693,-5.411]],"v":[[360.281,400.125],[170.977,597.431],[166.909,762.505],[282.798,653.18],[419.132,529.165],[591.161,470.697],[644.403,400.507],[556.486,342.254]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[52.639,-27.461],[57.444,-90.469],[-27.818,-6.701],[-52.811,60.226],[-44.19,17.504],[-43.428,1.434],[0.576,34.149],[70.929,9.404]],"o":[[-52.383,27.204],[-61.648,97.089],[26.814,6.459],[53.554,-61.074],[44.19,-17.504],[39.724,-1.312],[-0.488,-28.916],[-73.81,-9.786]],"v":[[337.627,393.675],[150.938,571.051],[113.78,790.198],[241.183,630.489],[415.018,508.533],[567.036,477.673],[651.931,411.796],[560.242,337.699]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":43.363,"s":[{"i":[[52.975,-27.598],[71.449,-82.568],[-26.119,-10.549],[-61.267,51.896],[-43.682,17.002],[-38.66,13.544],[0.42,32.348],[67.954,9.137]],"o":[[-51.157,26.651],[-76.706,88.642],[25.139,10.153],[62.435,-52.885],[43.681,-17.002],[38.66,-13.544],[-0.356,-27.386],[-70.216,-9.441]],"v":[[336.43,389.985],[165.007,532.087],[94.633,745.987],[244.636,604.296],[414.831,507.596],[559.622,473.279],[630.986,414.799],[536.565,346.563]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[52.475,-26.235],[56.355,-88.221],[-30.185,-10.492],[-53.423,57.161],[-42.414,16.625],[-57.784,7.045],[0.205,34.313],[70.43,10.482]],"o":[[-50.402,25.322],[-58.909,92.218],[26.307,6.588],[53.501,-59.257],[42.414,-16.625],[46.26,-10.266],[-0.174,-29.039],[-72.558,-10.799]],"v":[[337.76,388.794],[151.526,564.444],[110.692,784.489],[240.195,625.592],[405.777,506.621],[582.34,476.23],[653.568,408.95],[561.786,333.748]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[51.577,-25.66],[52.706,-87.785],[-30.618,-13.006],[-53.379,56.684],[-40.59,21.111],[-40.211,4.323],[-2.249,33.805],[67.004,15.657]],"o":[[-49.263,24.751],[-53.471,89.059],[27.013,6.224],[52.454,-59.802],[65.198,-33.909],[42.937,-6.667],[1.902,-28.598],[-67.829,-17.238]],"v":[[334.232,376.664],[151.617,546.553],[111.829,770.102],[245.559,611.006],[396.481,500.734],[579.493,475.824],[655.106,416.229],[569.69,335.104]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[{"i":[[50.679,-25.085],[44.071,-89.098],[-46.758,-23.261],[-50.159,58.35],[-39.774,16.193],[-40.206,5.305],[-1.298,33.563],[65.247,13.992]],"o":[[-48.123,24.179],[-43.227,87.392],[27.813,4.434],[47.993,-62.401],[64.986,-26.457],[43.11,-5.688],[1.098,-28.382],[-66.847,-14.049]],"v":[[349.01,382.282],[168.344,569.255],[155.055,803.191],[267.079,623.072],[416.579,501.275],[580.869,477.737],[655.02,412.524],[562.755,331.998]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[{"i":[[54.013,-35.45],[31.181,-97.419],[-43.069,-14.713],[-37.982,58.176],[-41.046,21.571],[-38.634,6.618],[0.037,33.154],[68.455,8.382]],"o":[[-46.017,33.376],[-31.464,101.162],[28.623,2.354],[45.782,-83.993],[58.103,-28.516],[47.059,-7.74],[-0.03,-28.022],[-64.25,-11.676]],"v":[[366.929,388.677],[191.905,611.177],[209.588,831.886],[294.473,653.961],[432.216,517.603],[579.867,478.292],[654.7,408.047],[561.596,330.44]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[54.111,-39.519],[6.329,-91.851],[-34.642,-2.992],[-18.209,52.194],[-43.589,32.329],[-36.365,1.728],[-0.196,32.447],[58.26,10.274]],"o":[[-71.666,47.302],[-8.274,120.077],[29.825,-0.297],[15.222,-53.379],[44.337,-32.634],[36.365,-1.728],[0.219,-36.155],[-58.627,-9.934]],"v":[[382.759,383.444],[250.15,636.941],[316.407,845.175],[356.09,671.488],[449.132,517.315],[581.972,480.508],[654.091,410.744],[567.259,331.944]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[55.613,-33.605],[-2.319,-104.947],[-33.372,0.013],[-11.332,50.116],[-43.975,27.16],[-36.725,3.231],[-1.296,32.438],[55.896,12.299]],"o":[[-67.316,38.794],[4.746,115.08],[28.468,-2.037],[9.012,-50.107],[44.439,-27.35],[37.425,-2.764],[1.183,-33.435],[-55.206,-10.869]],"v":[[391.276,368.929],[259.131,617.547],[343.337,809.45],[372.16,648.668],[449.357,507.798],[583.942,482.065],[655.642,413.828],[572.354,331.966]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":52,"s":[{"i":[[58.617,-21.778],[22.296,-90.194],[-29.972,-7.107],[-17,40.204],[-44.746,16.822],[-37.445,6.235],[-3.495,32.42],[51.168,16.349]],"o":[[-58.617,21.777],[-19.12,77.346],[25.817,5.735],[16.073,-38.719],[44.641,-16.782],[39.547,-4.836],[3.113,-27.994],[-48.364,-12.74]],"v":[[408.309,339.9],[251.256,509.005],[293.679,696.402],[351.615,577.292],[435.223,487.628],[587.884,485.178],[658.745,419.996],[582.544,332.009]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":53,"s":[{"i":[[53.658,-20.108],[35.383,-73.197],[-35.581,-11.521],[-21.277,28.439],[-40.437,12.211],[-35.307,6.585],[-3.296,32.76],[49.209,15.135]],"o":[[-53.978,20.738],[-31.828,64.285],[21.645,9.878],[26.197,-35.015],[40.361,-12.183],[38.856,-6.256],[2.951,-28.734],[-46.713,-12.085]],"v":[[416.241,335.883],[263.276,463.833],[273.739,629.436],[344.978,550.671],[443.616,481.103],[590.437,484.338],[658.853,418.224],[583.644,332.145]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":55.637,"s":[{"i":[[40.583,-15.705],[32.277,-43.48],[-9.048,-5.922],[-22.454,4.257],[-29.076,0.054],[-29.671,7.508],[-2.769,33.659],[44.042,11.934]],"o":[[-41.749,17.998],[-32.277,43.48],[14.817,9.699],[22.454,-4.257],[29.076,-0.054],[37.034,-9.997],[2.525,-30.685],[-42.359,-10.356]],"v":[[447.449,340.537],[353.348,411.355],[338.562,501.49],[403.265,496.366],[487.812,484.877],[597.168,482.124],[659.137,413.552],[586.547,332.506]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":58.092,"s":[{"i":[[41.14,-14.181],[16.857,-21.57],[-4.024,-3.706],[-14.07,-0.51],[-28.878,-6.415],[-29.93,6.397],[-4.022,33.532],[43.567,13.567]],"o":[[-42.09,18.438],[-17.525,22.426],[6.527,6.011],[11.563,0.419],[28.384,6.305],[37.381,-8.61],[3.666,-30.569],[-41.944,-11.927]],"v":[[481.538,335.535],[400.029,396.959],[378.702,461.784],[412.431,465.475],[500.339,477.428],[600.398,484.818],[664.879,418.602],[595.533,332.495]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60.545,"s":[{"i":[[28.201,-1.085],[12.654,-5.359],[7.828,-30.219],[-29.163,-7.707],[-24.783,-3.988],[-20.286,7.565],[4.15,32.508],[22.957,14.401]],"o":[[-19.818,0.762],[-62.447,26.447],[28.952,1.188],[19.981,5.281],[23.203,3.734],[32.599,-12.156],[-3.274,-25.65],[-17.795,-11.163]],"v":[[552.771,323.203],[494.19,336.456],[394.869,451.062],[473.248,463.684],[557.264,486.313],[622.075,480.875],[667.874,402.501],[624.119,342.216]],"c":true}]},{"t":65.455078125,"s":[{"i":[[29.256,-1.636],[12.654,-5.359],[8.617,-28.501],[-28.968,-8.636],[-24.783,-3.988],[-20.286,7.565],[3.491,32.586],[22.957,14.401]],"o":[[-19.802,1.107],[-62.447,26.447],[28.952,1.188],[19.806,5.905],[23.203,3.734],[32.599,-12.156],[-2.92,-27.253],[-17.795,-11.163]],"v":[[552.771,323.203],[505.624,334.015],[404.252,446.909],[473.294,460.187],[557.264,486.313],[622.075,480.875],[668.525,402.251],[624.928,339.765]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.250979988248,0.721568986481,0.588234994926,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91,"st":-11,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Null 6","hd":true,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.11],"y":[1.253]},"o":{"x":[0.234],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.619],"y":[1.175]},"o":{"x":[0.466],"y":[0.235]},"t":16.363,"s":[-23.464]},{"i":{"x":[0.495],"y":[1]},"o":{"x":[0.367],"y":[0.239]},"t":63.818,"s":[14.851]},{"t":90,"s":[0]}],"ix":10},"p":{"a":0,"k":[63.999999999999986,63.999999999999986,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.517,0.517,0.833],"y":[1.074,1.074,1.173]},"o":{"x":[0.571,0.571,0.333],"y":[0,0,0]},"t":0,"s":[12.799999999999997,12.799999999999997,100]},{"i":{"x":[0.636,0.636,0.667],"y":[0.588,0.588,0.493]},"o":{"x":[0.428,0.428,0.167],"y":[0.065,0.065,-0.129]},"t":31.908,"s":[10.323455999999998,10.323455999999998,100]},{"i":{"x":[0.472,0.472,0.667],"y":[1.033,1.033,1]},"o":{"x":[0.23,0.23,0.333],"y":[1.161,1.161,0.734]},"t":55.637,"s":[12.192767999999997,12.192767999999997,100]},{"t":90,"s":[12.799999999999997,12.799999999999997,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":91,"st":-10,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 4 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[63.99884799999998,63.999999999999986,0],"ix":2,"l":2},"a":{"a":0,"k":[20.25,20.25,0],"ix":1,"l":2},"s":{"a":0,"k":[255.99999999999994,255.99999999999994,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[11.045,0],[0,-11.046],[-11.046,0],[0,11.045]],"o":[[-11.046,0],[0,11.046],[11.045,0],[0,-11.046]],"v":[[0,-20],[-20,0],[0,20],[20,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.262745098039,0.376470618154,0.87450986376,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[20.25,20.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91,"st":0,"bm":0}],"markers":[]} diff --git a/android/app/src/main/res/raw/message.wav b/android/app/src/main/res/raw/message.wav new file mode 100644 index 00000000000..13c9628295b Binary files /dev/null and b/android/app/src/main/res/raw/message.wav differ diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 00000000000..eb75d8dd2b6 --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + + #141414 + #ffffff + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 44dee5a246e..a370d388ac1 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,5 +1,8 @@ + - #4360df - #5c6bc0 - #5c6bc0 + #ffffff + #000000 + #09101C + #ffffff + #4360DF diff --git a/android/app/src/main/res/values/splash_theme.xml b/android/app/src/main/res/values/splash_theme.xml new file mode 100644 index 00000000000..c61662059cd --- /dev/null +++ b/android/app/src/main/res/values/splash_theme.xml @@ -0,0 +1,13 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 428623ba43d..16d83f115d5 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - Status Your phone appears to be ROOTED, by pressing CONTINUE you understand and accept the risks in using this software. Continue Exit - \ No newline at end of file + status-im + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index b6c9d952be0..504077f1b02 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,10 +1,31 @@ - + + + + - @color/primary - @color/primary + diff --git a/android/app/src/main/res/values/theme.xml b/android/app/src/main/res/values/theme.xml new file mode 100644 index 00000000000..70d50d50307 --- /dev/null +++ b/android/app/src/main/res/values/theme.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_provider_paths.xml b/android/app/src/main/res/xml/file_provider_paths.xml new file mode 100644 index 00000000000..816c9d3f8c0 --- /dev/null +++ b/android/app/src/main/res/xml/file_provider_paths.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/pr/AndroidManifest.xml b/android/app/src/pr/AndroidManifest.xml new file mode 100644 index 00000000000..3571ea04c32 --- /dev/null +++ b/android/app/src/pr/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/android/app/src/pr/res/values/strings.xml b/android/app/src/pr/res/values/strings.xml new file mode 100644 index 00000000000..5bc6dcce61d --- /dev/null +++ b/android/app/src/pr/res/values/strings.xml @@ -0,0 +1,3 @@ + + Status PR + diff --git a/android/app/src/release/AndroidManifest.xml b/android/app/src/release/AndroidManifest.xml index 641bd0329a4..3571ea04c32 100644 --- a/android/app/src/release/AndroidManifest.xml +++ b/android/app/src/release/AndroidManifest.xml @@ -3,5 +3,7 @@ + + - \ No newline at end of file + diff --git a/android/app/src/release/google-services.json b/android/app/src/release/google-services.json deleted file mode 100644 index decba163d86..00000000000 --- a/android/app/src/release/google-services.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "project_info": { - "project_number": "854811651919", - "firebase_url": "https://status-react-app.firebaseio.com", - "project_id": "status-react-app", - "storage_bucket": "status-react-app.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:854811651919:android:11ee7444ded8a00a", - "android_client_info": { - "package_name": "im.status.ethereum" - } - }, - "oauth_client": [ - { - "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} diff --git a/android/app/src/release/res/values/strings.xml b/android/app/src/release/res/values/strings.xml new file mode 100644 index 00000000000..917ca04be88 --- /dev/null +++ b/android/app/src/release/res/values/strings.xml @@ -0,0 +1,3 @@ + + Status + diff --git a/android/build.gradle b/android/build.gradle index 5897663cf49..e7dbc681df4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,30 +1,88 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +/** +* Project-wide gradle configuration properties for use by all modules +*/ +ext { + // kotlin_version is needed for react-native-camera-kit library + kotlin_version = project.kotlinPluginVersion + RNNKotlinVersion = project.kotlinPluginVersion + RNGH_kotlinVersion = project.kotlinPluginVersion + buildToolsVersion = project.buildToolsVersion + minSdkVersion = Integer.valueOf(project.minSdkVersion) + compileSdkVersion = Integer.valueOf(project.compileSdkVersion) + targetSdkVersion = Integer.valueOf(project.targetSdkVersion) + supportLibVersion = project.supportLibVersion + gradlePluginVersion = project.gradlePluginVersion + kotlinVersion = project.kotlinPluginVersion + kotlinPluginVersion = project.kotlinPluginVersion + kotlinToolsVersion = project.kotlinPluginVersion + ndkVersion = "25.2.9519653" +} + buildscript { repositories { + flatDir { dirs "libs", "${rootDir}/app/libs" } google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' - classpath 'com.google.gms:google-services:3.0.0' - classpath 'de.undercouch:gradle-download-task:3.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${project.ext.kotlinPluginVersion}" + classpath("com.android.tools.build:gradle") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + classpath("com.google.gms:google-services:4.4.2") + } +} + +subprojects { + afterEvaluate { + if (project.hasProperty("android")) { + android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + targetSdkVersion rootProject.ext.targetSdkVersion + } - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files + // Speed up Tests Stage + tasks.withType(Test).configureEach { + // https://docs.gradle.org/current/userguide/performance.html#execute_tests_in_parallel + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + // https://docs.gradle.org/current/userguide/performance.html#fork_tests_into_multiple_processes + forkEvery = 100 + // https://docs.gradle.org/current/userguide/performance.html#disable_reports + reports.html.required = false + reports.junitXml.required = false + } + // Speed up Java Compile Stage + // https://docs.gradle.org/current/userguide/performance.html#run_the_compiler_as_a_separate_process + tasks.withType(JavaCompile).configureEach { + options.fork = true + } + + } + } } } +apply plugin: "com.facebook.react.rootproject" + allprojects { + beforeEvaluate { + if (System.env.STATUS_GO_ANDROID_LIBDIR == null || System.env.STATUS_GO_ANDROID_LIBDIR == "") { + throw new GradleException('STATUS_GO_ANDROID_LIBDIR environment variable is not valid!') + } + } repositories { - google() mavenLocal() - jcenter() - maven { url "$rootDir/../node_modules/react-native/android" } - maven { url "$rootDir/../modules/react-native-status/android/libs" } - // for geth - flatDir { dirs 'libs' } - maven { url "http://139.162.11.12:8081/artifactory/libs-release-local" } - maven { url "https://jitpack.io" } + // Android JSC is installed from npm + maven { url("$rootDir/../node_modules/jsc-android/dist") } + // For geth, function, and status-go + flatDir { dirs "libs", System.env.STATUS_GO_ANDROID_LIBDIR } + google() + mavenCentral() + maven { url 'https://mvnrepository.com/artifact/com.github.gundy/semver4j'} } } diff --git a/android/gradle.properties b/android/gradle.properties index 3cf751ad69b..6d587f756f9 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -17,18 +17,46 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -android.useDeprecatedNdk=true # To create a new keystore for development purposes (https://developer.android.com/studio/publish/app-signing.html) # $keytool -genkey -v -keystore ./status-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias status -STATUS_RELEASE_STORE_FILE=~/.gradle/status-im.keystore -STATUS_RELEASE_STORE_PASSWORD=password -STATUS_RELEASE_KEY_ALIAS=status -STATUS_RELEASE_KEY_PASSWORD=password -# Workaround for issue https://github.com/facebook/react-native/issues/16906 -android.enableAapt2=false - -# Disable Gradle Daemon https://stackoverflow.com/questions/38710327/jenkins-builds-fail-using-the-gradle-daemon -org.gradle.daemon=false - -org.gradle.jvmargs=-Xmx4608M \ No newline at end of file +# Version requirements used throughout the Gradle scripts +minSdkVersion=24 +compileSdkVersion=34 +targetSdkVersion=34 +buildToolsVersion=34.0.0 +supportLibVersion=28.0.0 +# This should match version from nix/pkgs/aapt2/default.nix +gradlePluginVersion=7.4.2 +kotlinPluginVersion=1.9.0 + +android.useAndroidX=true +android.enableJetifier=true + +KEYSTORE_PATH=~/.gradle/status-im.keystore +KEYSTORE_PASSWORD=password +KEYSTORE_ALIAS=status +KEYSTORE_KEY_PASSWORD=password + +# Splitting by CPU Architecture produces smaller APKs. +ANDROID_ABI_SPLIT=true +# By default its better to only build apk for most recent devices. +ANDROID_ABI_INCLUDE=arm64-v8a + +org.gradle.jvmargs=-Xmx8704M -XX:+UseParallelGC + +versionCode=9999 +commitHash=unknown + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=false + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +# Disabled for debug builds to avoid 'maximum call stack exceeded' errors. +# https://github.com/status-im/status-mobile/issues/18493 +hermesEnabled=false diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index b5166dad4d9..00000000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 6cd43a0b7aa..00000000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sun Jan 22 10:12:39 EET 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/android/gradlew b/android/gradlew index 91a7e269e19..7e5e936d819 100755 --- a/android/gradlew +++ b/android/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,159 +6,20 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac +## +## The default gradle wrapper script was replaced by a call to Nix's gradle package. +## Please revert to this script any time that it is upgraded by React Native. +## -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" fi -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi +# Map mavenLocal() to the Nix maven repo we set up in nix/mobile/android/maven-and-npm-deps/default.nix +if [ -n "${STATUS_NIX_MAVEN_REPO}" ]; then + repoOpts="-Dmaven.repo.local=${STATUS_NIX_MAVEN_REPO}" else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." + repoOpts='' fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec gradle $repoOpts "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index aec99730b4e..00000000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/init.gradle b/android/init.gradle new file mode 100644 index 00000000000..57b06eeaf87 --- /dev/null +++ b/android/init.gradle @@ -0,0 +1,9 @@ +initscript { + repositories { + gradlePluginPortal() + } + dependencies { + classpath "org.gradle:github-dependency-graph-gradle-plugin:+" + } +} +apply plugin: org.gradle.dependencygraph.simple.SimpleDependencyGraphPlugin diff --git a/android/keystores/BUCK b/android/keystores/BUCK deleted file mode 100644 index 88e4c31b28d..00000000000 --- a/android/keystores/BUCK +++ /dev/null @@ -1,8 +0,0 @@ -keystore( - name = "debug", - properties = "debug.keystore.properties", - store = "debug.keystore", - visibility = [ - "PUBLIC", - ], -) diff --git a/android/keystores/debug.keystore.properties b/android/keystores/debug.keystore.properties deleted file mode 100644 index 121bfb49f0d..00000000000 --- a/android/keystores/debug.keystore.properties +++ /dev/null @@ -1,4 +0,0 @@ -key.store=debug.keystore -key.alias=androiddebugkey -key.store.password=android -key.alias.password=android diff --git a/android/settings.gradle b/android/settings.gradle index 90863bfbc16..e9a8127c624 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,45 +1,21 @@ +pluginManagement { +includeBuild('../node_modules/@react-native/gradle-plugin') + repositories { + mavenLocal() // Let's prioritize local Maven repos so that Nix can provide them offline + gradlePluginPortal() + } +} +include ':react-native-share' +project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android') rootProject.name = 'StatusIm' -include ':react-native-securerandom' -project(':react-native-securerandom').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-securerandom/android') -include ':instabug-reactnative' -project(':instabug-reactnative').projectDir = new File(rootProject.projectDir, '../node_modules/instabug-reactnative/android') -include ':react-native-background-timer' -project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android') -include ':react-native-svg' -project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android') -include ':react-native-fcm' -project(':react-native-fcm').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fcm/android') +apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) +include ':react-native-nfc-manager' +project(':react-native-nfc-manager').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-nfc-manager/android') include ':app' -include ':react-native-http-bridge' -project(':react-native-http-bridge').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-http-bridge/android') -include ':instabug-reactnative' -project(':instabug-reactnative').projectDir = new File(rootProject.projectDir, '../node_modules/instabug-reactnative/android') -include ':react-native-splash-screen' -project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android') -include ':react-native-image-resizer' -project(':react-native-image-resizer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-resizer/android') -include ':react-native-dialogs' -project(':react-native-dialogs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-dialogs/android') -include ':react-native-i18n' -project(':react-native-i18n').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-i18n/android') -include ':realm' -project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android') include ':react-native-status' project(':react-native-status').projectDir = new File(rootProject.projectDir, '../modules/react-native-status/android') -include ':react-native-camera' -project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android') -include ':react-native-fs' -project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android') -include ':react-native-image-crop-picker' -project(':react-native-image-crop-picker').projectDir = new File(settingsDir, '../node_modules/react-native-image-crop-picker/android') -include ':react-native-keychain' -project(':react-native-keychain').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keychain/android') -include ':react-native-webview-bridge' -project(':react-native-webview-bridge').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview-bridge/android') -include ':react-native-config' -project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android') -include ':react-native-testfairy' -project(':react-native-testfairy').projectDir = new File(settingsDir, '../node_modules/react-native-testfairy/android') - -include ':react-native-android' -project(':react-native-android').projectDir = new File(rootProject.projectDir, '../node_modules/react-native/ReactAndroid') +include ':react-native-status-keycard' +project(':react-native-status-keycard').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-status-keycard/android') +include ':react-native-blur' +project(':react-native-blur').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/blur/android') +includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/app.json b/app.json index c24d965dac4..7e5f39bafdf 100644 --- a/app.json +++ b/app.json @@ -1,4 +1,4 @@ { "name": "StatusIm", "displayName": "StatusIm" -} \ No newline at end of file +} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000000..31de5f62fcd --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: ['react-native-reanimated/plugin', '@babel/plugin-transform-named-capturing-groups-regex'], +}; diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 00000000000..f161b329987 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,38 @@ +{ + "variables": { + "module_name%": "status_nodejs_addon", + "module_path%": "./lib/binding/" + }, + "targets": [{ + "target_name": "<(module_name)", + "sources": [ + "./modules/react-native-status/nodejs/status.cpp" + ], + "xcode_settings": { + "MACOSX_DEPLOYMENT_TARGET": "10.7" + }, + "libraries": [ + "> compiler-options :modules vals))))) - -(defn- compile-cljs-with-build-config [build-config build-fn env build-id] - (let [{:keys [source-paths compiler]} build-config - output-files (get-output-files compiler) - started-at (System/currentTimeMillis)] - (println (str "Compiling " (pr-str build-id) " for " (pr-str env) "...")) - (flush) - (build-fn (apply api/inputs source-paths) compiler) - (println-colorized (str "Successfully compiled " (pr-str output-files) " in " (elapsed started-at) ".") green-color))) - -(defn- compile-cljs [env & [build-id watch?]] - (let [build-fn (if watch? api/watch api/build)] - (if build-id - (compile-cljs-with-build-config (get-cljsbuild-config env build-id) build-fn env build-id) - (doseq [[build-id build-config] (get-cljsbuild-config env)] - (compile-cljs-with-build-config (get-cljsbuild-config env build-id) build-fn env build-id))))) - -(defn- show-help [] - (doseq [[task {:keys [desc usage]}] cli-tasks-info] - (println (format (str yellow-color "%-12s" reset-color green-color "%s" reset-color) - (name task) desc)) - (when usage - (println) - (->> usage - (map #(str " " %)) - (str/join "\n") - println) - (println)))) - -;;; Task dispatching - -(defmulti task first) - -(defmethod task :default [args] - (println (format "Unknown or missing task. Choose one of: %s\n" - (->> cli-tasks-info - keys - (map name) - (interpose ", ") - (apply str)))) - (show-help) - (System/exit 1)) - -;;; Compiling task - -(defmethod task "compile" [[_ env build-id type]] - (case type - (nil "once") (compile-cljs env build-id) - "watch" (compile-cljs env build-id true) - (do (println "Unknown argument to compile task:" type) - (System/exit 1)))) - -;;; Testing task - -(defmethod task "test" [[_ build-id]] - (with-namespaces [[doo.core :as doo]] - (compile-cljs :test build-id) - (doo/run-script :node (->> build-id (get-cljsbuild-config :test) :compiler)))) - -;;; :watch task - -(defn hawk-handler - [ctx e] - (let [path "src/status_im/utils/js_resources.cljs" - js-resourced (slurp path)] - (spit path (str js-resourced " ;;")) - (spit path js-resourced)) - ctx) - -(defmethod task "watch" [[_ & args]] - (with-namespaces [[hawk.core :as hawk] - [re-frisk-sidecar.core :as rfs] - [figwheel-sidecar.repl-api :as ra] - [clj-rn.core :as clj-rn] - [clj-rn.main :as main]] - (let [options (main/parse-cli-options args main/watch-task-options)] - (clj-rn/watch (assoc options :start-cljs-repl false)) - (rfs/-main) - (hawk/watch! [{:paths ["resources"] :handler hawk-handler}]) - (when (:start-cljs-repl options) (ra/cljs-repl))))) - -;;; Help - -(defmethod task "help" [_] - (show-help) - (System/exit 1)) - -;;; Build script entrypoint. - -(task *command-line-args*) diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile deleted file mode 100644 index 12b5dfda649..00000000000 --- a/ci/Jenkinsfile +++ /dev/null @@ -1,141 +0,0 @@ -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'scripts/prepare-for-platform.sh mobile' - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -timeout(90) { - node ('macos_pr') { - def apkUrl = '' - def ipaUrl = '' - def testPassed = true - def branch; - - load "$HOME/env.groovy" - - try { - stage('Git & Dependencies') { - slackSend color: 'good', message: BRANCH_NAME + '(' + env.CHANGE_BRANCH + ') build started. ' + env.BUILD_URL - - checkout scm - - try { - sh 'git rebase origin/develop' - } catch (e) { - sh 'git rebase --abort' - throw e - } - - sh 'rm -rf node_modules' - sh 'cp .env.jenkins .env' - - installJSDeps() - - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - - stage('Code style checks') { - sh 'lein cljfmt check' - } - - stage('Tests') { - sh 'lein test-cljs' - } - - stage('Build') { - sh 'lein prod-build' - } - - // Android - stage('Build (Android)') { - sh 'cd android && ./gradlew react-native-android:installArchives && ./gradlew assembleRelease' - } - - stage('Deploy (Android)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')] ) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@android/app/build/outputs/apk/release/app-release.apk -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - def hash = 'null' - for (int r = 0; r < 6 && hash == 'null'; r++) { - sh 'sleep 10' - hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - } - apkUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT Android: ' + apkUrl) - } - } - - // iOS - stage('Build (iOS)') { - env.RCT_NO_LAUNCH_PACKAGER = true - sh 'xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' - sh 'xcodebuild -exportArchive -exportPath status -archivePath status.xcarchive -exportOptionsPlist ios/archive-develop.plist' - } - - stage('Deploy (iOS)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@status/StatusIm.ipa -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - def hash = 'null' - for (int r = 0; r < 6 && hash == 'null'; r++) { - sh 'sleep 10' - hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - } - ipaUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT iOS: ' + ipaUrl) - } - } - - stage('Slack/GH Notifications') { - def c = (testPassed ? 'good' : 'warning' ) - - slackSend color: c, message: ('Branch: ' + BRANCH_NAME + '\nAndroid: ' + apkUrl + '\niOS: ' + ipaUrl); - - withCredentials([string(credentialsId: 'GIT_HUB_TOKEN', variable: 'githubToken')]) { - def commentMsg = ("branch " + BRANCH_NAME + ":\\napk uploaded to " + apkUrl + "\\nipa uploaded to " + ipaUrl) - def ghOutput = sh(returnStdout: true, script: "curl -u status-im:" + githubToken + " -H 'Content-Type: application/json' --data '{\"body\": \"" + commentMsg + "\"}' https://api.github.com/repos/status-im/status-react/issues/" + CHANGE_ID + "/comments") - println("Result of github comment curl: " + ghOutput); - } - } - - // Android for e2e - stage('Build (Android) for e2e tests') { - sh 'cd android && mv app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/app-release.original.apk && ENVFILE=.env.e2e ./gradlew assembleRelease' - } - - stage('Upload apk for e2e tests') { - if (env.CHANGE_ID != null){ - withCredentials([string(credentialsId: 'SAUCE_ACCESS_KEY', variable: 'key'), string(credentialsId: 'SAUCE_USERNAME', variable: 'username')]){ - def apk_name = env.CHANGE_ID + '.apk' - sh('curl -u ' + username+ ':' + key + ' -X POST -H "Content-Type: application/octet-stream" https://saucelabs.com/rest/v1/storage/' + username + '/' + apk_name + '?overwrite=true --data-binary @android/app/build/outputs/apk/release/app-release.apk') - } - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@android/app/build/outputs/apk/release/app-release.apk -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - apkUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT Android for e2e tests: ' + apkUrl) - } - } - } - - } catch (e) { - slackSend color: 'bad', message: BRANCH_NAME + ' failed to build. ' + env.BUILD_URL - throw e - } - } -} diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile new file mode 120000 index 00000000000..35f8dace385 --- /dev/null +++ b/ci/Jenkinsfile @@ -0,0 +1 @@ +Jenkinsfile.combined \ No newline at end of file diff --git a/ci/Jenkinsfile.android b/ci/Jenkinsfile.android new file mode 100644 index 00000000000..9f8cd46a3fe --- /dev/null +++ b/ci/Jenkinsfile.android @@ -0,0 +1,88 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +/* Options section can't access functions in objects. */ +def isPRBuild = utils.isPRBuild() + +pipeline { + agent { label 'linux && x86_64 && nix-2.24' } + + options { + timestamps() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 30, unit: 'MINUTES') + /* Limit builds retained */ + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '20', + artifactNumToKeepStr: '1', + )) + /* Allows combined build to copy */ + copyArtifactPermission('/status-mobile/*') + /* Abort old PR builds. */ + disableConcurrentBuilds( + abortPrevious: isPRBuild + ) + } + + parameters { + string( + name: 'BUILD_TYPE', + description: 'Specify build type. Values: pr / e2e / nightly / release', + defaultValue: 'pr', + ) + } + + environment { + LANG = 'en_US.UTF-8' + LC_ALL = 'en_US.UTF-8' + LANGUAGE = 'en_US.UTF-8' + PLATFORM = "android${utils.isE2EBuild() ? "-e2e" : ""}" + BUILD_ENV = 'prod' + NIX_CONF_DIR = "${env.WORKSPACE}/nix" + FASTLANE_DISABLE_COLORS = 1 + SENTRY_PRODUCTION = "${utils.isReleaseBuild() ? 'true' : 'false'}" + } + + stages { + stage('Prep') { + steps { + script { + utils.symlinkEnv() + println("Build Number: ${utils.genBuildNumber()}") + } + } + } + stage('Bundle') { + steps { + script { apks = android.bundle() } + } + } + stage('Sign') { + steps { + script { apks = android.sign(apks) } + } + } + stage('Parallel Upload') { + parallel { + stage('Archive') { + steps { script { + apks.each { archiveArtifacts it } + } } + } + stage('Upload') { + steps { script { + def urls = apks.collect { s5cmd.upload(it) } + env.PKG_URL = urls.first() + jenkins.setBuildDesc(APK: env.PKG_URL) + } } + } + } + } + } + post { + success { script { github.notifyPR(true) } } + failure { script { github.notifyPR(false) } } + always { sh 'make purge' } + } +} diff --git a/ci/Jenkinsfile.combined b/ci/Jenkinsfile.combined new file mode 100644 index 00000000000..1d21a717806 --- /dev/null +++ b/ci/Jenkinsfile.combined @@ -0,0 +1,136 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +import groovy.json.JsonBuilder + +pipeline { + agent { label 'linux' } + + options { + timestamps() + disableConcurrentBuilds() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 40, unit: 'MINUTES') + /* Limit builds retained */ + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '30', + artifactNumToKeepStr: '1', + )) + } + + /* WARNING: Defining parameters here with the ?: trick causes them to remember last value. */ + parameters { + choice( + name: 'BUILD_TYPE', + description: 'Makefile target to build. Optional Parameter.', + choices: genChoices(params.BUILD_TYPE, ['manual', 'nightly', 'release', 'pr', 'e2e']), + ) + booleanParam( + name: 'PUBLISH', + description: 'Trigger publishing of build results for nightly or release.', + defaultValue: getPublishDefault(params.PUBLISH), + ) + } + + stages { + stage('Prep') { + steps { script { + println("Current JOB: ${env.JOB_NAME}") + } } + } + stage('Build') { + parallel { + stage('iOS') { steps { script { + ios = jenkins.Build('status-mobile/platforms/ios') + } } } + stage('Android') { steps { script { + apk = jenkins.Build('status-mobile/platforms/android') + } } } + stage('Tests') { steps { script { + jenkins.Build('status-mobile/platforms/tests') + } } } + } + } + stage('Archive') { + steps { script { + sh('rm -f pkg/*') + jenkins.copyArts(ios) + jenkins.copyArts(apk) + sha = "pkg/${utils.pkgFilename(ext: 'sha256')}" + dir('pkg') { + /* generate sha256 checksums for upload */ + sh "sha256sum * | tee ../${sha}" + archiveArtifacts('*') + } + } } + } + stage('Upload') { + steps { script { + /* object for easier URLs handling */ + urls = [ + /* mobile */ + Apk: utils.pkgUrl(apk), + iOS: utils.pkgUrl(ios), + Diawi: utils.getEnv(ios, 'DIAWI_URL'), + /* upload the sha256 checksums file too */ + SHA: s5cmd.upload(sha), + ] + /* add URLs to the build description */ + jenkins.setBuildDesc(urls) + } } + } + stage('Publish') { + when { expression { params.PUBLISH } } + steps { script { + switch (utils.getBuildType()) { + case 'nightly': /* Create JSON file with newest build URLs */ + updateBucketJSON(urls, 'latest.json'); + break; + case 'release': + github.publishReleaseFiles(repo: 'status-mobile'); + break; + } + } } + } + } +} + +/* Helper that generates list of available choices for a parameter + * but re-orders them based on the currently set value. First is default. */ +def List genChoices(String previousChoice, List defaultChoices) { + if (previousChoice == null) { + return defaultChoices + } + choices = defaultChoices.minus(previousChoice) + choices.add(0, previousChoice) + return choices +} + +/* Helper that makes PUBLISH default to 'false' unless: + * - The build is for a release branch + * - A user explicitly specified a value + * Since release builds create and re-create GitHub drafts every time. */ +def Boolean getPublishDefault(Boolean previousValue) { + if (env.JOB_NAME.startsWith('status-mobile/release')) { return true } + if (previousValue != null) { return previousValue } + return false +} + +/* Helper for updating JSON with newest builds. */ +def updateBucketJSON(urls, fileName) { + /* latest.json has slightly different key names */ + def content = [ + DIAWI: urls.Diawi, + APK: urls.Apk, IOS: urls.iOS, + APP: urls.App, MAC: urls.Mac, + WIN: urls.Win, SHA: urls.SHA + ] + def filePath = "${pwd()}/pkg/${fileName}" + /* it might not exist */ + sh "mkdir -p ${pwd()}/pkg" + def contentJson = new JsonBuilder(content).toPrettyString() + println "${filePath}:\n${contentJson}" + writeFile(file: filePath, text: contentJson) + return s5cmd.upload(filePath) +} diff --git a/ci/Jenkinsfile.desktopbuild b/ci/Jenkinsfile.desktopbuild deleted file mode 100644 index f6d7c276cf2..00000000000 --- a/ci/Jenkinsfile.desktopbuild +++ /dev/null @@ -1,245 +0,0 @@ -properties([ - buildDiscarder(logRotator( - numToKeepStr: '2', - daysToKeepStr: '3', - artifactNumToKeepStr: '2', - artifactDaysToKeepStr: '3' - )) -]) - -env.LANG = 'en_US.UTF-8' -env.LANGUAGE = 'en_US.UTF-8' -env.LC_ALL = 'en_US.UTF-8' - -apkUrl = '' -ipaUrl = '' -testPassed = true -scriptOutput = '' -packageFolder = './StatusImPackage' -external_modules_dir = [ - 'node_modules/react-native-i18n/desktop', - 'node_modules/react-native-config/desktop', - 'node_modules/react-native-fs/desktop', - 'node_modules/react-native-http-bridge/desktop', - 'node_modules/react-native-webview-bridge/desktop', - 'node_modules/react-native-keychain/desktop', - 'node_modules/react-native-securerandom/desktop', - 'modules/react-native-status/desktop', -] - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - sh 'node -v' - sh 'npm -v' - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'scripts/prepare-for-platform.sh desktop' - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -def doGitRebase() { - try { - sh 'git rebase origin/develop' - } catch (e) { - sh 'git rebase --abort' - throw e - } -} - -def cleanupBuild() { - sh 'rm -rf node_modules' - sh "rm -rf ${packageFolder}" - sh 'rm -rf desktop/modules' - sh 'rm -rf desktop/node_modules' -} - -def cleanupAndDeps() { - cleanupBuild() - sh 'cp .env.jenkins .env' - sh 'lein deps' - installJSDeps() -} - -def slackNotify(message, color = 'good') { - slackSend( - color: color, - channel: '#jenkins-desktop', - message: "${BRANCH_NAME}(${env.CHANGE_BRANCH}) ${message} ${env.BUILD_URL}" - ) -} - -def buildClojureScript() { - sh 'rm -f index.desktop.js' - sh 'lein prod-build-desktop' - sh "mkdir ${packageFolder}" - sh """ - react-native bundle \\ - --entry-file index.desktop.js \\ - --dev false --platform desktop \\ - --bundle-output ${packageFolder}/StatusIm.jsbundle \\ - --assets-dest ${packageFolder}/assets - """ -} - -parallel( - 'MacOS build': { - timeout(90) { - node('macos1') { - load "$HOME/env.groovy" - - try { - stage('Git & Deps') { - slackNotify('MacOS build started.') - checkout scm - doGitRebase() - cleanupAndDeps() - } - - stage('Build ClojureScript') { - buildClojureScript() - } - - stage('Build MacOS binaries') { - /* add path for QT installation binaries */ - env.PATH = "/Users/administrator/qt/5.9.1/clang_64/bin:${env.PATH}" - dir('desktop') { - sh 'rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile' - sh """ - cmake -Wno-dev \\ - -DCMAKE_BUILD_TYPE=Release \\ - -DEXTERNAL_MODULES_DIR='${external_modules_dir.join(";")}' \\ - -DJS_BUNDLE_PATH='${workspace}/${packageFolder}/StatusIm.jsbundle' \\ - -DCMAKE_CXX_FLAGS:='-DBUILD_FOR_BUNDLE=1' - """ - sh 'make' - } - } - - stage('Create MacOS Bundle') { - dir(packageFolder) { - sh 'git clone https://github.com/vkjr/StatusAppFiles.git' - sh 'unzip StatusAppFiles/StatusIm.app.zip' - sh 'cp -r assets/share/assets StatusIm.app/Contents/MacOs' - sh 'chmod +x StatusIm.app/Contents/MacOs/ubuntu-server' - sh 'cp ../desktop/bin/StatusIm StatusIm.app/Contents/MacOs' - sh """ - macdeployqt StatusIm.app -verbose=1 -dmg \\ - -qmldir='${workspace}/node_modules/react-native/ReactQt/runtime/src/qml/' - """ - sh 'rm -fr StatusAppFiles' - } - } - - stage('Archive Artifact') { - archiveArtifacts 'StatusImPackage/*.dmg' - } - - slackNotify('MacOS build finished successfully.') - } catch (e) { - slackNotify('Failed to build on MacOS.', color: 'bad') - throw e - } finally { - cleanupBuild() - } - } - } - }, - 'Linux build': { - timeout(90) { - node ('linux-01') { - def qt_bin = '/opt/qt59/bin' - - try { - stage('Git & Deps') { - slackNotify('Linux build started.') - checkout scm - doGitRebase() - cleanupAndDeps() - } - - stage('Build ClojureScript') { - buildClojureScript() - } - - stage('Build Linux binaries') { - /* add path for QT installation binaries */ - env.PATH = "${qt_bin}:${env.PATH}" - dir('desktop') { - sh 'rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile' - sh """ - cmake -Wno-dev \\ - -DCMAKE_BUILD_TYPE=Release \\ - -DEXTERNAL_MODULES_DIR='${external_modules_dir.join(";")}' \\ - -DJS_BUNDLE_PATH='${workspace}/${packageFolder}/StatusIm.jsbundle' \\ - -DCMAKE_CXX_FLAGS:='-DBUILD_FOR_BUNDLE=1' - """ - sh 'make' - } - } - - stage('Create Linux AppImage') { - dir(packageFolder) { - sh 'rm -rf StatusImAppImage' - /* TODO this needs to be fixed: status-react/issues/5378 */ - sh 'cp /opt/StatusImAppImage.zip ./' - sh 'unzip ./StatusImAppImage.zip' - sh 'rm -rf AppDir' - sh 'mkdir AppDir' - } - sh "cp -r ./deployment/linux/usr ${packageFolder}/AppDir" - sh "cp ./deployment/linux/.env ${packageFolder}/AppDir" - sh "cp ./desktop/bin/StatusIm ${packageFolder}/AppDir/usr/bin" - sh 'wget https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage' - sh 'chmod a+x ./linuxdeployqt-continuous-x86_64.AppImage' - - sh 'rm -f Application-x86_64.AppImage' - sh 'rm -f StatusIm-x86_64.AppImage' - - sh "ldd ${packageFolder}/AppDir/usr/bin/StatusIm" - sh """ - ./linuxdeployqt-continuous-x86_64.AppImage \\ - ${packageFolder}/AppDir/usr/share/applications/StatusIm.desktop \\ - -verbose=3 -always-overwrite -no-strip \\ - -no-translations -bundle-non-qt-libs \\ - -qmake=${qt_bin}/qmake \\ - -extra-plugins=imageformats/libqsvg.so \\ - -qmldir='${workspace}/node_modules/react-native' - """ - dir(packageFolder) { - sh 'ldd AppDir/usr/bin/StatusIm' - sh 'cp -r assets/share/assets AppDir/usr/bin' - sh 'cp -rf StatusImAppImage/* AppDir/usr/bin' - sh 'rm -f AppDir/usr/bin/StatusIm.AppImage' - } - sh """ - ./linuxdeployqt-continuous-x86_64.AppImage \\ - ${packageFolder}/AppDir/usr/share/applications/StatusIm.desktop \\ - -verbose=3 -appimage -qmake=${qt_bin}/qmake - """ - dir(packageFolder) { - sh 'ldd AppDir/usr/bin/StatusIm' - sh 'rm -rf StatusIm.AppImage' - sh 'cp -f ../StatusIm-x86_64.AppImage StatusIm.AppImage' - } - } - - stage('Archive Artifact') { - archiveArtifacts 'StatusImPackage/*.AppImage' - } - - slackNotify('Linux build finished successfully.') - } catch (e) { - slackNotify('Failed to build on Linux.', color: 'bad') - throw e - } finally { - cleanupBuild() - } - } - } - } -) diff --git a/ci/Jenkinsfile.e2e-nightly b/ci/Jenkinsfile.e2e-nightly new file mode 100644 index 00000000000..0aa6d250afa --- /dev/null +++ b/ci/Jenkinsfile.e2e-nightly @@ -0,0 +1,52 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + agent { label 'linux' } + + triggers { + // Nightly at 0am + cron 'H 0 * * *' + } + + parameters { + string( + name: 'BRANCH', + description: 'Name of the branch to checkout and build.', + defaultValue: 'develop', + ) + } + + options { + timestamps() + disableConcurrentBuilds() + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '30', + artifactNumToKeepStr: '1', + )) + } + + stages { + stage('Build') { + steps { + script { + apk_build = jenkins.Build('status-mobile/platforms/android-e2e') + apk_build_number = apk_build.getNumber().toString() + } + } + } + stage('Run e2e') { + steps { + build( + job: 'status-mobile/e2e/status-app-nightly', + parameters: [ + string(name: 'APK_BUILD_NUMBER', value: apk_build_number), + string(name: 'BRANCH', value: env.BRANCH), + ] + ) + } + } + } +} + diff --git a/ci/Jenkinsfile.fastlane.clean b/ci/Jenkinsfile.fastlane.clean deleted file mode 100644 index 50cc1dfe7b3..00000000000 --- a/ci/Jenkinsfile.fastlane.clean +++ /dev/null @@ -1,25 +0,0 @@ -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -timeout(90) { - node ('macos'){ - - stage('Git & Dependencies'){ - - checkout([$class: 'GitSCM', branches: [[name: 'develop']], - doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], - submoduleCfg: [], userRemoteConfigs: [[url: 'https://github.com/status-im/status-react.git']]]) - - load "$HOME/env.groovy" - } - - stage('Clean Testflight Users'){ - - withCredentials([string(credentialsId: 'FASTLANE_PASSWORD', variable: 'FASTLANE_PASSWORD'), - string(credentialsId: 'APPLE_ID', variable: 'APPLE_ID')]) { - sh ('bundle install') - sh ('bundle exec fastlane ios clean')}} - } -} diff --git a/ci/Jenkinsfile.ios b/ci/Jenkinsfile.ios new file mode 100644 index 00000000000..21aafb6a341 --- /dev/null +++ b/ci/Jenkinsfile.ios @@ -0,0 +1,93 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +/* Options section can't access functions in objects. */ +def isPRBuild = utils.isPRBuild() + +pipeline { + agent { label 'macos && arm64 && nix-2.24 && xcode-16.2' } + + parameters { + string( + name: 'BUILD_TYPE', + description: 'Specify build type. Values: pr / e2e / nightly / release', + defaultValue: 'pr', + ) + } + + options { + timestamps() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 25, unit: 'MINUTES') + /* Limit builds retained */ + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '20', + artifactNumToKeepStr: '1', + )) + /* Allows combined build to copy */ + copyArtifactPermission('/status-mobile/*') + /* Abort old PR builds. */ + disableConcurrentBuilds( + abortPrevious: isPRBuild + ) + } + + environment { + LANG = 'en_US.UTF-8' + LC_ALL = 'en_US.UTF-8' + LANGUAGE = 'en_US.UTF-8' + PLATFORM = 'ios' + NIX_CONF_DIR = "${env.WORKSPACE}/nix" + FASTLANE_DISABLE_COLORS = 1 + BUNDLE_PATH = "${HOME}/.bundle" + SENTRY_PRODUCTION = "${utils.isReleaseBuild() ? 'true' : 'false'}" + } + + stages { + stage('Prep') { + steps { + script { + utils.symlinkEnv() + println("Build Number: ${utils.genBuildNumber()}") + } + } + } + stage('JSBundle') { + steps { + script { ios.jsbundle() } + } + } + stage('Bundle') { + steps { + script { api = ios.bundle() } + } + post { + failure { + archiveArtifacts 'logs/*' + } + } + } + stage('Parallel Upload') { + parallel { + stage('Archive') { + steps { + archiveArtifacts api + } + } + stage('Upload') { + steps { script { + env.DIAWI_URL = ios.uploadToDiawi() + env.PKG_URL = env.DIAWI_URL + jenkins.setBuildDesc(IPA: env.PKG_URL) + } } + } + } + } + } + post { + success { script { github.notifyPR(true) } } + failure { script { github.notifyPR(false) } } + always { sh 'make purge' } + } +} diff --git a/ci/Jenkinsfile.nightly b/ci/Jenkinsfile.nightly deleted file mode 100644 index 413731383d1..00000000000 --- a/ci/Jenkinsfile.nightly +++ /dev/null @@ -1,123 +0,0 @@ -// We need nightly builds for users who want to test apps, diawi removes old builds and limits downloads, hence the need for Artifactory. -// To see env: echo sh(returnStdout: true, script: 'env') - -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'scripts/prepare-for-platform.sh mobile' - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -timeout(90) { - node ('macos'){ - def apkUrl = '' - def ipaUrl = '' - def testPassed = true - - load "$HOME/env.groovy" - - try { - - stage('Git & Dependencies') { - slackSend color: 'good', message: 'Nightly build started. ' + env.BUILD_URL - - checkout scm - - sh 'rm -rf node_modules' - sh 'cp .env.jenkins .env' - - installJSDeps() - - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - - stage('Tests') { - sh 'lein test-cljs' - } - - stage('Build') { - sh 'lein prod-build' - } - - stage('Build (Android)') { - sh 'cd android && ./gradlew react-native-android:installArchives && ./gradlew assembleRelease' - } - - stage('Build (iOS)') { - env.RCT_NO_LAUNCH_PACKAGER = true - sh 'xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' - sh 'xcodebuild -exportArchive -exportPath status -archivePath status.xcarchive -exportOptionsPlist ios/archive-develop.plist' - } - - stage('Deploy (Android)') { - def artifact_dir = pwd() + '/android/app/build/outputs/apk/release/' - println (artifact_dir + 'app-release.apk') - sh ('ls -la ' + artifact_dir) - def artifact = (artifact_dir + 'app-release.apk') - def server = Artifactory.server('artifacts') - def shortCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim().take(6) - def filename = 'im.status.ethereum-' + shortCommit + '-nightly' + '.apk' - def newArtifact = (artifact_dir + filename) - sh ('mv ' + artifact + ' ' + newArtifact) - def uploadSpec = '{ "files": [ { "pattern": "*apk/release/' + filename + '", "target": "nightlies-local" }]}' - def buildInfo = server.upload(uploadSpec) - apkUrl = 'http://artifacts.status.im:8081/artifactory/nightlies-local/' + filename - - sh ('echo ARTIFACT Android: ' + apkUrl) - } - - stage('Deploy (iOS)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@status/StatusIm.ipa -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - ipaUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT iOS: ' + ipaUrl) - } - } - } catch (e) { - slackSend color: 'bad', message: 'Nightly build (develop) failed to build. ' + env.BUILD_URL - throw e - } - - stage('Slack Notification') { - def c = (testPassed ? 'good' : 'warning' ) - slackSend color: c, message: 'Nightly build (develop) \nTests: ' + (testPassed ? ':+1:' : ':-1:') + ')\nAndroid: ' + apkUrl + '\n iOS: ' + ipaUrl - build job: 'status-react/status-nightly-publish-link', parameters: [[$class: 'StringParameterValue', name: 'APK_URL', value: apkUrl], [$class: 'StringParameterValue', name: 'IOS_URL', value: ipaUrl]] - } - - stage('Build (Android) for e2e tests') { - sh 'cd android && mv app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/app-release.original.apk && ENVFILE=.env.e2e ./gradlew assembleRelease' - } - - stage('Upload apk for e2e tests') { - if (env.CHANGE_ID != null){ - withCredentials([string(credentialsId: 'SAUCE_ACCESS_KEY', variable: 'key'), string(credentialsId: 'SAUCE_USERNAME', variable: 'username')]){ - def apk_name = env.CHANGE_ID + '.apk' - sh('curl -u ' + username+ ':' + key + ' -X POST -H "Content-Type: application/octet-stream" https://saucelabs.com/rest/v1/storage/' + username + '/' + apk_name + '?overwrite=true --data-binary @android/app/build/outputs/apk/release/app-release.apk') - } - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@android/app/build/outputs/apk/release/app-release.apk -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - apkUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT Android for e2e tests: ' + apkUrl) - } - } - } - } -} diff --git a/ci/Jenkinsfile.nightly_desktop b/ci/Jenkinsfile.nightly_desktop deleted file mode 100644 index e35d13aab9c..00000000000 --- a/ci/Jenkinsfile.nightly_desktop +++ /dev/null @@ -1,306 +0,0 @@ -properties([ - buildDiscarder(logRotator( - numToKeepStr: '2', - daysToKeepStr: '3', - artifactNumToKeepStr: '2', - artifactDaysToKeepStr: '3' - )) -]) - -env.LANG = 'en_US.UTF-8' -env.LANGUAGE = 'en_US.UTF-8' -env.LC_ALL = 'en_US.UTF-8' - -app_url = '' -dmg_url = '' -app_file = '' -dmg_file = '' -commit = '' -testPassed = true -packageFolder = './StatusImPackage' -external_modules_dir = [ - 'node_modules/react-native-i18n/desktop', - 'node_modules/react-native-config/desktop', - 'node_modules/react-native-fs/desktop', - 'node_modules/react-native-http-bridge/desktop', - 'node_modules/react-native-webview-bridge/desktop', - 'node_modules/react-native-keychain/desktop', - 'node_modules/react-native-securerandom/desktop', - 'modules/react-native-status/desktop', -] - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - sh 'node -v' - sh 'npm -v' - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'scripts/prepare-for-platform.sh desktop' - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -def doGitRebase() { - try { - sh 'git rebase origin/develop' - } catch (e) { - sh 'git rebase --abort' - throw e - } -} - -def cleanupBuild() { - sh 'rm -rf node_modules' - sh "rm -rf ${packageFolder}" - sh 'rm -rf desktop/modules' - sh 'rm -rf desktop/node_modules' -} - -def cleanupAndDeps() { - cleanupBuild() - sh 'cp .env.jenkins .env' - sh 'lein deps' - installJSDeps() -} - -def slackNotify(message, color = 'good') { - slackSend( - color: color, - channel: '#jenkins-desktop', - message: "develop (${env.CHANGE_BRANCH}) ${message} ${env.BUILD_URL}" - ) -} - -def buildClojureScript() { - sh 'rm -f index.desktop.js' - sh 'lein prod-build-desktop' - sh "mkdir ${packageFolder}" - sh """ - react-native bundle \\ - --entry-file index.desktop.js \\ - --dev false --platform desktop \\ - --bundle-output ${packageFolder}/StatusIm.jsbundle \\ - --assets-dest ${packageFolder}/assets - """ -} - -def uploadArtifact(filename) { - def domain = 'ams3.digitaloceanspaces.com' - def bucket = 'status-im-desktop' - withCredentials([usernamePassword( - credentialsId: 'digital-ocean-access-keys', - usernameVariable: 'DO_ACCESS_KEY', - passwordVariable: 'DO_SECRET_KEY' - )]) { - sh """ - s3cmd \\ - --acl-public \\ - --host='${domain}' \\ - --host-bucket='%(bucket)s.${domain}' \\ - --access_key=${DO_ACCESS_KEY} \\ - --secret_key=${DO_SECRET_KEY} \\ - put ${filename} s3://${bucket}/ - """ - - } - def url = "https://${bucket}.${domain}/${filename}" - return url -} - -parallel( - 'MacOS build': { - timeout(90) { - node('macos') { - load "$HOME/env.groovy" - - try { - stage('Git & Deps') { - slackNotify('MacOS build started.') - checkout scm - doGitRebase() - cleanupAndDeps() - commit = sh( - returnStdout: true, - script: 'git rev-parse HEAD' - ).trim().take(6) - } - - stage('Build ClojureScript') { - buildClojureScript() - } - - stage('Build MacOS binaries') { - /* add path for QT installation binaries */ - env.PATH = "/Users/administrator/qt/5.9.1/clang_64/bin:${env.PATH}" - dir('desktop') { - sh 'rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile' - sh """ - cmake -Wno-dev \\ - -DCMAKE_BUILD_TYPE=Release \\ - -DEXTERNAL_MODULES_DIR='${external_modules_dir.join(";")}' \\ - -DJS_BUNDLE_PATH='${workspace}/${packageFolder}/StatusIm.jsbundle' \\ - -DCMAKE_CXX_FLAGS:='-DBUILD_FOR_BUNDLE=1' - """ - sh 'make' - } - } - - stage('Create MacOS Bundle') { - dir(packageFolder) { - sh 'git clone https://github.com/vkjr/StatusAppFiles.git' - sh 'unzip StatusAppFiles/StatusIm.app.zip' - sh 'cp -r assets/share/assets StatusIm.app/Contents/MacOs' - sh 'chmod +x StatusIm.app/Contents/MacOs/ubuntu-server' - sh 'cp ../desktop/bin/StatusIm StatusIm.app/Contents/MacOs' - sh """ - macdeployqt StatusIm.app -verbose=1 -dmg \\ - -qmldir='${workspace}/node_modules/react-native/ReactQt/runtime/src/qml/' - """ - sh 'rm -fr StatusAppFiles' - dmg_file = "StatusIm-${commit}.dmg" - sh "mv StatusIm.dmg ${dmg_file}" - } - } - - stage('Archive Artifact') { - archiveArtifacts "StatusImPackage/${dmg_file}" - } - - stage('Upload Artifacts') { - dir(packageFolder) { - dmg_url = uploadArtifact(dmg_file) - } - } - - slackNotify('MacOS build finished successfully.') - } catch (e) { - slackNotify('Failed to build on MacOS.', color: 'bad') - throw e - } finally { - cleanupBuild() - } - } - } - }, - 'Linux build': { - timeout(90) { - node ('linux-new') { - def qt_bin = '/opt/qt59/bin' - - try { - stage('Git & Deps') { - slackNotify('Linux build started.') - checkout scm - doGitRebase() - cleanupAndDeps() - commit = sh( - returnStdout: true, - script: 'git rev-parse HEAD' - ).trim().take(6) - } - - stage('Build ClojureScript') { - buildClojureScript() - } - - stage('Build Linux binaries') { - /* add path for QT installation binaries */ - env.PATH = "${qt_bin}:${env.PATH}" - dir('desktop') { - sh 'rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile' - sh """ - cmake -Wno-dev \\ - -DCMAKE_BUILD_TYPE=Release \\ - -DEXTERNAL_MODULES_DIR='${external_modules_dir.join(";")}' \\ - -DJS_BUNDLE_PATH='${workspace}/${packageFolder}/StatusIm.jsbundle' \\ - -DCMAKE_CXX_FLAGS:='-DBUILD_FOR_BUNDLE=1' - """ - sh 'make' - } - } - - stage('Create Linux AppImage') { - dir(packageFolder) { - sh 'rm -rf StatusImAppImage' - /* TODO this needs to be fixed: status-react/issues/5378 */ - sh 'cp /opt/StatusImAppImage.zip ./' - sh 'unzip ./StatusImAppImage.zip' - sh 'rm -rf AppDir' - sh 'mkdir AppDir' - } - sh "cp -r ./deployment/linux/usr ${packageFolder}/AppDir" - sh "cp ./deployment/linux/.env ${packageFolder}/AppDir" - sh "cp ./desktop/bin/StatusIm ${packageFolder}/AppDir/usr/bin" - sh 'wget https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage' - sh 'chmod a+x ./linuxdeployqt-continuous-x86_64.AppImage' - - sh 'rm -f Application-x86_64.AppImage' - sh 'rm -f StatusIm-x86_64.AppImage' - - sh "ldd ${packageFolder}/AppDir/usr/bin/StatusIm" - sh """ - ./linuxdeployqt-continuous-x86_64.AppImage \\ - ${packageFolder}/AppDir/usr/share/applications/StatusIm.desktop \\ - -verbose=3 -always-overwrite -no-strip \\ - -no-translations -bundle-non-qt-libs \\ - -qmake=${qt_bin}/qmake \\ - -extra-plugins=imageformats/libqsvg.so \\ - -qmldir='${workspace}/node_modules/react-native' - """ - dir(packageFolder) { - sh 'ldd AppDir/usr/bin/StatusIm' - sh 'cp -r assets/share/assets AppDir/usr/bin' - sh 'cp -rf StatusImAppImage/* AppDir/usr/bin' - sh 'rm -f AppDir/usr/bin/StatusIm.AppImage' - } - sh """ - ./linuxdeployqt-continuous-x86_64.AppImage \\ - ${packageFolder}/AppDir/usr/share/applications/StatusIm.desktop \\ - -verbose=3 -appimage -qmake=${qt_bin}/qmake - """ - dir(packageFolder) { - sh 'ldd AppDir/usr/bin/StatusIm' - sh 'rm -rf StatusIm.AppImage' - app_file = "StatusIm-${commit}.AppImage" - sh "mv ../StatusIm-x86_64.AppImage ${app_file}" - } - } - - stage('Archive Artifacts') { - archiveArtifacts "StatusImPackage/${app_file}" - } - - stage('Upload Artifacts') { - dir(packageFolder) { - app_url = uploadArtifact(app_file) - } - } - - slackNotify('Linux build finished successfully.') - } catch (e) { - slackNotify('Failed to build on Linux.', color: 'bad') - throw e - } finally { - cleanupBuild() - } - } - } - } -) - -stage('Update nightly links') { - println "Publishing URLs:" - println dmg_url - println app_url - build( - job: 'misc/status-im.github.io-update_env', - parameters: [ - [$class: 'StringParameterValue', name: 'DMG_URL', value: dmg_url], - [$class: 'StringParameterValue', name: 'NIX_URL', value: app_url] - ] - ) -} diff --git a/ci/Jenkinsfile.nightly_fastlane b/ci/Jenkinsfile.nightly_fastlane deleted file mode 100644 index 5d75a6075a9..00000000000 --- a/ci/Jenkinsfile.nightly_fastlane +++ /dev/null @@ -1,155 +0,0 @@ -// We need nightly builds for users who want to test apps, diawi removes old builds and limits downloads, hence the need for Artifactory. -// To see env: echo sh(returnStdout: true, script: 'env') - -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -timeout(90) { - node ('macos'){ - def apkUrl = '' - def ipaUrl = '' - def testPassed = true - def version - def build_no - - load "$HOME/env.groovy" - - try { - - stage('Git & Dependencies') { - slackSend color: 'good', message: 'Nightly build started. ' + env.BUILD_URL - - checkout scm - - sh 'git fetch --tags' - sh 'rm -rf node_modules' - sh 'cp .env.nightly .env' - - sh 'scripts/prepare-for-platform.sh mobile' - version = readFile("${env.WORKSPACE}/VERSION").trim() - installJSDeps() - - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - - stage('Tag Build') { - withCredentials([[ - $class: 'UsernamePasswordMultiBinding', - credentialsId: 'jenkins-status-im', - usernameVariable: 'GIT_USER', - passwordVariable: 'GIT_PASS' - ]]) { - build_no = sh( - returnStdout: true, - script: './scripts/build_no.sh --increment' - ).trim() - } - } - - stage('Tests') { - sh 'lein test-cljs' - } - - stage('Build') { - sh 'lein prod-build' - } - - stage('Build (Android)') { - sh 'cd android && ./gradlew react-native-android:installArchives && ./gradlew assembleRelease' - } - - stage('Build (iOS)') { - withCredentials([ - string(credentialsId: 'jenkins_pass', variable: 'password') - ]) { - sh "plutil -replace CFBundleShortVersionString -string ${version} ios/StatusIm/Info.plist" - sh "plutil -replace CFBundleVersion -string ${build_no} ios/StatusIm/Info.plist" - env.RCT_NO_LAUNCH_PACKAGER = true - sh 'xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' - sh "security unlock-keychain -p ${password} login.keychain" - sh 'xcodebuild -exportArchive -exportPath status_appstore -archivePath status.xcarchive -exportOptionsPlist ios/archive-release.plist' - sh 'xcodebuild -exportArchive -exportPath status -archivePath status.xcarchive -exportOptionsPlist ios/archive-develop.plist' - } - } - - stage('Deploy (Android)') { - def artifact_dir = pwd() + '/android/app/build/outputs/apk/release/' - println (artifact_dir + 'app-release.apk') - def artifact = (artifact_dir + 'app-release.apk') - def server = Artifactory.server('artifacts') - shortCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim().take(6) - def filename = 'im.status.ethereum-' + shortCommit + '-n-fl' + '.apk' - def newArtifact = (artifact_dir + filename) - sh ('cp ' + artifact + ' ' + newArtifact) - def uploadSpec = '{ "files": [ { "pattern": "*apk/release/' + filename + '", "target": "nightlies-local" }]}' - def buildInfo = server.upload(uploadSpec) - apkUrl = 'http://artifacts.status.im:8081/artifactory/nightlies-local/' + filename - - sh ('bundle exec fastlane android nightly') - sh ('echo ARTIFACT Android: ' + apkUrl) - } - - stage('Deploy (iOS)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@status/StatusIm.ipa -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - ipaUrl = 'https://i.diawi.com/' + hash - - sh ('bundle exec fastlane ios nightly') - sh ('echo ARTIFACT iOS: ' + ipaUrl) - } - } - } catch (e) { - slackSend color: 'bad', message: 'Nightly build (develop) failed to build. ' + env.BUILD_URL - throw e - } - - stage('Run status-nightly-publish-link job') { - build job: 'status-react/status-nightly-publish-link', parameters: [[$class: 'StringParameterValue', name: 'APK_URL', value: apkUrl], [$class: 'StringParameterValue', name: 'IOS_URL', value: ipaUrl]] - } - - stage('Build (Android) for e2e tests') { - sh 'cd android && mv app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/app-release.original.apk && ENVFILE=.env.e2e ./gradlew assembleRelease' - } - - stage('Upload apk for e2e tests') { - withCredentials([string(credentialsId: 'SAUCE_ACCESS_KEY', variable: 'key'), string(credentialsId: 'SAUCE_USERNAME', variable: 'username')]){ - apk_name = 'im.status.ethereum-e2e-' + shortCommit + '.apk' - sh('curl -u ' + username+ ':' + key + ' -X POST -H "Content-Type: application/octet-stream" https://saucelabs.com/rest/v1/storage/' + username + '/' + apk_name + '?overwrite=true --data-binary @android/app/build/outputs/apk/release/app-release.apk') - } - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@android/app/build/outputs/apk/release/app-release.apk -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - testApkUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT Android for e2e tests: ' + testApkUrl) - } - } - - stage('Slack Notification') { - def c = (testPassed ? 'good' : 'warning' ) - slackSend color: c, message: 'Nightly build (develop) \nTests: ' + (testPassed ? ':+1:' : ':-1:') + ')\nAndroid: ' + apkUrl + '\n iOS: ' + ipaUrl + '\n Android for e2e: ' + testApkUrl - } - - stage('Run extended e2e tests') { - build job: 'end-to-end-tests/status-app-nightly', parameters: [string(name: 'apk', value: '--apk=' + apk_name)], wait: false - } - } -} diff --git a/ci/Jenkinsfile.parameters b/ci/Jenkinsfile.parameters deleted file mode 100644 index 27909d1ec1f..00000000000 --- a/ci/Jenkinsfile.parameters +++ /dev/null @@ -1,127 +0,0 @@ -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'scripts/prepare-for-platform.sh mobile' - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -timeout(90) { - node ('macos') { - def apkUrl = '' - def ipaUrl = '' - def testPassed = true - def branch; - - load "$HOME/env.groovy" - - try { - stage('Git & Dependencies') { - slackSend color: 'good', message: REPO + ":" + BRANCH_NAME + ' build started. ' + env.BUILD_URL - - checkout scm - - sh 'rm -rf node_modules' - - sh 'test ${JENKINS_REBASE_DEVELOP} -eq 1 && git rebase origin/develop || echo "Not rebasing on develop."' - - // Assume all parameters are set in Jenkins 'Parameterized build' - // TODO(oskarth): Consider read/write from .env to avoid having to specify in Jenkins again - // sh 'cp .env.jenkins .env' - sh 'echo TESTFAIRY_ENABLED=' + TESTFAIRY_ENABLED + '>>' + '.env' - sh 'echo ETHEREUM_DEV_CLUSTER=' + ETHEREUM_DEV_CLUSTER + '>>' + '.env' - sh 'echo MAINNET_NETWORKS_ENABLED=' + MAINNET_NETWORKS_ENABLED + '>>' + '.env' - sh 'echo LOG_LEVEL=' + LOG_LEVEL + '>>' + '.env' - sh 'echo LOG_LEVEL_STATUS_GO=' + LOG_LEVEL_STATUS_GO + '>>' + '.env' - sh 'echo OFFLINE_INBOX_ENABLED=' + OFFLINE_INBOX_ENABLED + '>>' + '.env' - sh 'echo POW_TARGET=' + POW_TARGET + '>>' + '.env' - sh 'echo POW_TIME=' + POW_TIME + '>>' + '.env' - sh 'echo MAINNET_WARNING_ENABLED=' + MAINNET_WARNING_ENABLED + '>>' + '.env' - sh 'echo DEFAULT_NETWORK=' + DEFAULT_NETWORK + '>>' + '.env' - - sh 'echo "**********************************************************************"' - sh 'echo PARAMETERIZED BUILD - USING CUSTOM ENVIRONMENT' - sh 'cat .env' - sh 'echo "**********************************************************************"' - - installJSDeps() - - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - - stage('Tests') { - sh 'lein test-cljs' - } - - stage('Build') { - sh 'lein prod-build' - } - - // Android - stage('Build (Android)') { - sh 'cd android && ./gradlew react-native-android:installArchives && ./gradlew assembleRelease' - } - stage('Deploy (Android)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@android/app/build/outputs/apk/release/app-release.apk -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - apkUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT Android: ' + apkUrl) - } - } - - // try { - // stage('Test (Android)') { - // sauce('b9aded57-5cc1-4f6b-b5ea-42d989987852') { - // sh 'cd test/appium && mvn -DapkUrl=' + apkUrl + ' test' - // saucePublisher() - // } - // } - // } catch(e) { - // testPassed = false - // } - - // iOS - stage('Build (iOS)') { - env.RCT_NO_LAUNCH_PACKAGER = true - sh 'xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' - sh 'xcodebuild -exportArchive -exportPath status -archivePath status.xcarchive -exportOptionsPlist ios/archive-develop.plist' - } - stage('Deploy (iOS)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@status/StatusIm.ipa -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - ipaUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT iOS: ' + ipaUrl) - } - } - - stage('Slack Notification') { - def c = (testPassed ? 'good' : 'warning' ) - slackSend( - color: c, - message: "Branch: ${REPO}:${BRANCH_NAME}\nAndroid: ${apkUrl}\niOS: ${ipaUrl}" - ) - } - - } catch (e) { - slackSend color: 'bad', message: REPO + ":" + BRANCH_NAME + ' failed to build. ' + env.BUILD_URL - throw e - } - } -} diff --git a/ci/Jenkinsfile.release b/ci/Jenkinsfile.release deleted file mode 100644 index b5eb51aa4ee..00000000000 --- a/ci/Jenkinsfile.release +++ /dev/null @@ -1,146 +0,0 @@ -// We need release builds for users who want to test apps, diawi removes old builds and limits downloads, hence the need for Artifactory. -// To see env: echo sh(returnStdout: true, script: 'env') - -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -timeout(90) { - node ('macos'){ - def apkUrl = '' - def ipaUrl = '' - def testPassed = true - def version - def build_no - - load "$HOME/env.groovy" - - try { - - stage('Git & Dependencies') { - slackSend color: 'good', message: BRANCH_NAME + ' build started. ' + env.BUILD_URL - - checkout scm - - sh 'git fetch --tags' - - sh 'rm -rf node_modules' - sh 'cp .env.prod .env' - - sh 'scripts/prepare-for-platform.sh mobile' - version = readFile("${env.WORKSPACE}/VERSION").trim() - installJSDeps() - - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - - stage('Tag Build') { - withCredentials([[ - $class: 'UsernamePasswordMultiBinding', - credentialsId: 'jenkins-status-im', - usernameVariable: 'GIT_USER', - passwordVariable: 'GIT_PASS' - ]]) { - build_no = sh( - returnStdout: true, - script: './scripts/build_no.sh --increment' - ).trim() - } - } - - stage('Tests') { - sh 'lein test-cljs' - } - - stage('Build') { - sh 'lein prod-build' - } - - stage('Build (Android)') { - sh 'cd android && ./gradlew react-native-android:installArchives && ./gradlew assembleRelease -PreleaseVersion=' + version - } - - stage('Build (iOS)') { - sh "plutil -replace CFBundleShortVersionString -string ${version} ios/StatusIm/Info.plist" - sh "plutil -replace CFBundleVersion -string ${build_no} ios/StatusIm/Info.plist" - env.RCT_NO_LAUNCH_PACKAGER = true - sh 'xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' - sh 'xcodebuild -exportArchive -exportPath status -archivePath status.xcarchive -exportOptionsPlist ios/archive-develop.plist' - } - - stage('Deploy (Android)') { - def artifact_dir = pwd() + '/android/app/build/outputs/apk/release/' - println (artifact_dir + 'app-release.apk') - sh ('ls -la ' + artifact_dir) - def artifact = (artifact_dir + 'app-release.apk') - def server = Artifactory.server('artifacts') - shortCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim().take(6) - def filename = 'im.status.ethereum-' + shortCommit + '-rel' + '.apk' - def newArtifact = (artifact_dir + filename) - sh ('mv ' + artifact + ' ' + newArtifact) - def uploadSpec = '{ "files": [ { "pattern": "*apk/release/' + filename + '", "target": "nightlies-local" }]}' - def buildInfo = server.upload(uploadSpec) - apkUrl = 'http://artifacts.status.im:8081/artifactory/nightlies-local/' + filename - - sh ('echo ARTIFACT Android: ' + apkUrl) - } - - stage('Deploy (iOS)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@status/StatusIm.ipa -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - ipaUrl = 'https://i.diawi.com/' + hash - - sh ('echo ARTIFACT iOS: ' + ipaUrl) - } - } - - } catch (e) { - slackSend color: 'bad', message: 'Release build failed to build. ' + env.BUILD_URL - throw e - } - - stage('Slack Notification') { - def c = (testPassed ? 'good' : 'warning' ) - slackSend color: c, message: 'Release build \nTests: ' + (testPassed ? ':+1:' : ':-1:') + ')\nAndroid: ' + apkUrl + '\n iOS: ' + ipaUrl - } - - stage('Build (Android) for e2e tests') { - sh 'cd android && ENVFILE=.env.e2e ./gradlew assembleRelease' - } - - stage('Upload apk for e2e tests') { - withCredentials([string(credentialsId: 'SAUCE_ACCESS_KEY', variable: 'key'), string(credentialsId: 'SAUCE_USERNAME', variable: 'username')]){ - apk_name = 'release-e2e-' + shortCommit + '.apk' - sh('curl -u ' + username+ ':' + key + ' -X POST -H "Content-Type: application/octet-stream" https://saucelabs.com/rest/v1/storage/' + username + '/' + apk_name + '?overwrite=true --data-binary @android/app/build/outputs/apk/release/app-release.apk') - } - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@android/app/build/outputs/apk/release/app-release.apk -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - testApkUrl = 'https://i.diawi.com/' + hash - sh ('echo ARTIFACT Android for e2e tests: ' + testApkUrl) - } - } - - stage('Run extended e2e tests') { - build job: 'end-to-end-tests/status-app-nightly', parameters: [string(name: 'apk', value: '--apk=' + apk_name)], wait: false - } - - } -} diff --git a/ci/Jenkinsfile.tests b/ci/Jenkinsfile.tests new file mode 100644 index 00000000000..d043c846bdc --- /dev/null +++ b/ci/Jenkinsfile.tests @@ -0,0 +1,100 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +/* Options section can't access functions in objects. */ +def isPRBuild = utils.isPRBuild() + +pipeline { + agent { label 'linux && x86_64 && nix-2.24' } + + options { + timestamps() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 20, unit: 'MINUTES') + /* Limit builds retained */ + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '20', + artifactNumToKeepStr: '1', + )) + /* Abort old PR builds. */ + disableConcurrentBuilds( + abortPrevious: isPRBuild + ) + } + + parameters { + string( + name: 'BUILD_TYPE', + description: 'Specify build type. Values: pr / e2e / nightly / release', + defaultValue: 'pr', + ) + booleanParam( + name: 'RUN_CONTRACT_TESTS', + description: 'Whether to run optional and slow contract tests.', + defaultValue: false, + ) + } + + environment { + LANG = 'en_US.UTF-8' + LC_ALL = 'en_US.UTF-8' + LANGUAGE = 'en_US.UTF-8' + PLATFORM = 'tests' + BUILD_ENV = 'prod' + NIX_CONF_DIR = "${env.WORKSPACE}/nix" + LOG_FILE = utils.pkgFilename(ext: 'log', arch: 'tests') + CLJ_LINTER_PRINT_WARNINGS = 'true' + } + + stages { + stage('Test') { + parallel { + stage('Lint') { + steps { make('lint') } + } + stage('Unit') { + steps { make('test-unit') } + } + stage('Component') { + steps { make('test-component') } + } + } + } + stage('Heavy Test') { + parallel { + stage('Contract') { + when { expression { params.RUN_CONTRACT_TESTS } } + steps { make('test-contract') } + } + stage('Integration') { + steps { make('test-integration') } + } + } + } + stage('Upload') { + steps { + script { + env.PKG_URL = s5cmd.upload(LOG_FILE) + } + } + } + stage('Commit Checks') { + steps { + make('lint-commits') + } + } + } + post { + success { script { github.notifyPR(true) } } + failure { script { github.notifyPR(false) } } + always { sh 'make purge' } + } +} + +def make(target) { + sh """#!/bin/bash + set -eo pipefail + make ${target} 2>&1 | tee -a ${LOG_FILE} + """ +} diff --git a/ci/Jenkinsfile.upload_release_android b/ci/Jenkinsfile.upload_release_android deleted file mode 100644 index de63a6a4815..00000000000 --- a/ci/Jenkinsfile.upload_release_android +++ /dev/null @@ -1,104 +0,0 @@ -// We need release builds for users who want to test apps, diawi removes old builds and limits downloads, hence the need for Artifactory. -// To see env: echo sh(returnStdout: true, script: 'env') - -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -timeout(90) { - node ('macos'){ - def apkUrl = '' - def ipaUrl = '' - def testPassed = true - def version - def build_no - - load "$HOME/env.groovy" - - try { - - stage('Git & Dependencies') { - slackSend color: 'good', message: BRANCH_NAME + ' build started. ' + env.BUILD_URL - - if (!BRANCH_NAME.startsWith("release/")){ - error "Wrong branch name format: " + BRANCH_NAME + ", but it should be `release/version`" - } - - checkout scm - - sh 'git fetch --tags' - - sh 'rm -rf node_modules' - sh 'cp .env.prod .env' - - sh 'scripts/prepare-for-platform.sh mobile' - version = readFile("${env.WORKSPACE}/VERSION").trim() - installJSDeps() - - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - - stage('Tag Build') { - withCredentials([[ - $class: 'UsernamePasswordMultiBinding', - credentialsId: 'jenkins-status-im', - usernameVariable: 'GIT_USER', - passwordVariable: 'GIT_PASS' - ]]) { - build_no = sh( - returnStdout: true, - script: './scripts/build_no.sh --increment' - ).trim() - } - } - - stage('Tests') { - sh 'lein test-cljs' - } - - stage('Build') { - sh 'lein prod-build' - } - - stage('Build (Android)') { - sh 'cd android && ./gradlew react-native-android:installArchives && ./gradlew assembleRelease -PreleaseVersion=' + version - } - - stage('Deploy (Android)') { - sh ('bundle exec fastlane android release') - } - - stage('Push build tag') { - withCredentials([[ - $class: 'UsernamePasswordMultiBinding', - credentialsId: 'jenkins-status-im', - usernameVariable: 'GIT_USER', - passwordVariable: 'GIT_PASS' - ]]) { - sh ('git push --tags https://${GIT_USER}:${GIT_PASS}@github.com/status-im/status-react --tags') - } - } - } catch (e) { - slackSend color: 'bad', message: 'Release build failed uploading to the Play Market. ' + env.BUILD_URL - throw e - } - - stage('Slack Notification') { - slackSend color: 'good', message: 'Release build ' + version + ' succesfully aploade to the Play Market' - } - } -} diff --git a/ci/Jenkinsfile.upload_release_ios b/ci/Jenkinsfile.upload_release_ios deleted file mode 100644 index 3c272b4af56..00000000000 --- a/ci/Jenkinsfile.upload_release_ios +++ /dev/null @@ -1,110 +0,0 @@ -// We need release builds for users who want to test apps, diawi removes old builds and limits downloads, hence the need for Artifactory. -// To see env: echo sh(returnStdout: true, script: 'env') - -env.LANG="en_US.UTF-8" -env.LANGUAGE="en_US.UTF-8" -env.LC_ALL="en_US.UTF-8" -env.FASTLANE_DISABLE_COLORS=1 - -def installJSDeps() { - def attempt = 1 - def maxAttempts = 10 - def installed = false - while (!installed && attempt <= maxAttempts) { - println "#${attempt} attempt to install npm deps" - sh 'npm install' - installed = fileExists('node_modules/web3/index.js') - attemp = attempt + 1 - } -} - -timeout(90) { - node ('macos'){ - def apkUrl = '' - def ipaUrl = '' - def testPassed = true - def version - def build_no - - load "$HOME/env.groovy" - - try { - stage('Git & Dependencies') { - slackSend color: 'good', message: BRANCH_NAME + ' build started. ' + env.BUILD_URL - - if (!BRANCH_NAME.startsWith("release/")){ - error "Wrong branch name format: " + BRANCH_NAME + ", but it should be `release/version`" - } - - checkout scm - - sh 'git fetch --tags' - - sh 'rm -rf node_modules' - sh 'cp .env.prod .env' - - sh 'scripts/prepare-for-platform.sh mobile' - version = readFile("${env.WORKSPACE}/VERSION").trim() - installJSDeps() - - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - - stage('Tag Build') { - withCredentials([[ - $class: 'UsernamePasswordMultiBinding', - credentialsId: 'jenkins-status-im', - usernameVariable: 'GIT_USER', - passwordVariable: 'GIT_PASS' - ]]) { - build_no = sh( - returnStdout: true, - script: './scripts/build_no.sh --increment' - ).trim() - } - } - - stage('Tests') { - sh 'lein test-cljs' - } - - stage('Build') { - sh 'lein prod-build' - } - - stage('Build (iOS)') { - withCredentials([string(credentialsId: 'jenkins_pass', variable: 'password')]) { - sh "plutil -replace CFBundleShortVersionString -string ${version} ios/StatusIm/Info.plist" - sh "plutil -replace CFBundleVersion -string ${build_no} ios/StatusIm/Info.plist" - env.RCT_NO_LAUNCH_PACKAGER = true - sh 'xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' - sh "security unlock-keychain -p ${password} login.keychain" - sh 'xcodebuild -exportArchive -exportPath status_appstore -archivePath status.xcarchive -exportOptionsPlist ios/archive-release.plist' - } - } - - stage('Deploy (iOS)') { - sh ('bundle exec fastlane ios release') - } - - stage('Push build tag') { - withCredentials([[ - $class: 'UsernamePasswordMultiBinding', - credentialsId: 'jenkins-status-im', - usernameVariable: 'GIT_USER', - passwordVariable: 'GIT_PASS' - ]]) { - sh ('git push --tags https://${GIT_USER}:${GIT_PASS}@github.com/status-im/status-react --tags') - } - } - } catch (e) { - slackSend color: 'bad', message: 'Release build failed uploading to iTunes Connect. ' + env.BUILD_URL - throw e - } - - stage('Slack Notification') { - slackSend color: 'good', message: 'Release build ' + version + ' succesfully aploade to iTunes Connect' - } - } -} diff --git a/ci/README.md b/ci/README.md new file mode 100644 index 00000000000..6a087094855 --- /dev/null +++ b/ci/README.md @@ -0,0 +1,16 @@ +# Description + +This folder contains files defininf [Jenkins pipelines](https://jenkins.io/doc/book/pipeline/) that run on https://ci.status.im/. + +# Libraries + +All `Jenkinsfile`s contain the following line: +```groovy +library 'status-jenkins-lib@master' +``` + +Which loads the used methods - like `nix.shell()` - from a separate private repo: + +https://github.com/status-im/status-jenkins-lib + +This is done to improve security of our CI setup. diff --git a/ci/tests/Jenkinsfile.e2e-nightly b/ci/tests/Jenkinsfile.e2e-nightly new file mode 100644 index 00000000000..355d1c64673 --- /dev/null +++ b/ci/tests/Jenkinsfile.e2e-nightly @@ -0,0 +1,116 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + + agent { label 'linux' } + + parameters { + string( + name: 'APK_BUILD_NUMBER', + description: 'platform/e2e build number for apk artifact', + ) + string( + name: 'KEYWORD_EXPRESSION', + description: 'This will run tests which contain names that match the given string expression (Optional)', + defaultValue: '', + ) + string( + name: 'BRANCH', + description: 'Name of the branch to checkout and build.', + defaultValue: 'develop', + ) + string( + name: 'TR_CASE_IDS', + description: 'IDs of the TestRail case, separated by a comma (Optional)', + defaultValue: '', + ) + } + + options { + disableConcurrentBuilds() + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '30', + artifactNumToKeepStr: '1', + )) + } + + environment { + /* Avoid race conditions with other builds using virtualenv. */ + VIRTUAL_ENV = "${WORKSPACE_TMP}/venv" + PATH = "${VIRTUAL_ENV}/bin:${PATH}" + } + + stages { + stage('Fetch') { + steps { script { + copyArtifacts( + projectName: "status-mobile/platforms/android-e2e", + filter: 'result/*-x86_64.apk', + selector: specific(env.APK_BUILD_NUMBER), + ) + apk_path = "${env.WORKSPACE}/${utils.findFile('result/*-x86_64.apk')}" + + } } + } + + stage('Setup') { + steps { script { + dir('test/appium') { + sh "python3 -m venv ${VIRTUAL_ENV}" + sh 'pip3 install -r requirements.txt' + } + } } + } + + stage('Test') { + steps { + withCredentials([ + usernamePassword( + credentialsId: 'test-rail-api', + usernameVariable: 'TESTRAIL_USER', + passwordVariable: 'TESTRAIL_PASS' + ), + usernamePassword( + credentialsId: 'lambda-test-api', + usernameVariable: 'LAMBDA_TEST_USERNAME', + passwordVariable: 'LAMBDA_TEST_ACCESS_KEY' + ), + string( + credentialsId: 'etherscan-api-key', + variable: 'ETHERSCAN_API_KEY' + ), + string( + credentialsId: 'infura-e2e-token', + variable: 'WEB3_INFURA_PROJECT_ID' + ), + file( + credentialsId: "mobile-tests-eth-accounts", + variable: 'TEST_ETH_ACCOUNTS_FILE' + ), + ]) { + dir('test/appium/tests') { + /* Provide Eth test accounts secrets. */ + sh 'cp -f $TEST_ETH_ACCOUNTS_FILE users.py' + sh """ + python3 -m pytest \ + --numprocesses 9 \ + --testrail_report=True \ + -m testrail_id \ + -m \"nightly\" \ + -k \"${params.KEYWORD_EXPRESSION}\" \ + --apk=${params.APK_URL ?: apk_path} + """ + } + } + } + } + } + + post { + cleanup { + sh 'make purge' + } + } +} diff --git a/ci/tests/Jenkinsfile.e2e-prs b/ci/tests/Jenkinsfile.e2e-prs new file mode 100644 index 00000000000..909eafce2af --- /dev/null +++ b/ci/tests/Jenkinsfile.e2e-prs @@ -0,0 +1,154 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + + agent { label 'linux' } + + parameters { + string( + name: 'BRANCH_NAME', + description: 'Name of the branch to checkout and build.', + defaultValue: 'develop', + ) + string( + name: 'PR_ID', + description: 'ID of the Pull Request triggering this build.', + ) + string( + name: 'APK_URL', + description: 'Optional, set if job require APK to be downloaded from URL.', + ) + string( + name: 'KEYWORD_EXPRESSION', + description: 'This will run tests which contain names that match the given string expression (Optional)', + defaultValue: '', + ) + string( + name: 'TR_CASE_IDS', + description: 'IDs of the TestRail case, separated by a comma (Optional)', + defaultValue: '', + ) + /* FIXME: Remove this no longer relevant argument */ + string( + name: 'APK_NAME', + description: 'OBSOLETE ARGUMENT TO BE REMOVED', + defaultValue: 'DUMMY', + ) + string( + name: 'TEST_MARKERS', + description: 'Marker expression for matching tests to run.', + defaultValue: 'smoke', + ) + } + + options { + disableConcurrentBuilds() + timeout(time: 90, unit: 'MINUTES') + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '30', + artifactNumToKeepStr: '1', + )) + } + + environment { + /* Avoid race conditions with other builds using virtualenv. */ + VIRTUAL_ENV = "${WORKSPACE_TMP}/venv" + PATH = "${VIRTUAL_ENV}/bin:${PATH}" + } + + stages { + stage('Prep') { + steps { script { + currentBuild.displayName = "PR-${params.PR_ID}" + if (params.PR_ID == null) { + error("PR_ID parameter not set!") + } + } } + } + + stage('Fetch') { + when { expression { !params.APK_URL } } + steps { script { + copyArtifacts( + projectName: "status-mobile/prs/android-e2e/PR-${params.PR_ID}", + /* WARNING: This copies the latest available artifact. */ + selector: lastWithArtifacts(), + ) + apk_path = "${env.WORKSPACE}/${utils.findFile('result/*.apk')}" + } } + } + + stage('Setup') { + steps { script { + dir('test/appium') { + sh "python3 -m venv ${VIRTUAL_ENV}" + sh 'pip3 install -r requirements.txt' + } + } } + } + + stage('Test') { + steps { script { + /* for managing optional arguments */ + def extraPytestOpts = '' + if (params.TR_CASE_IDS != '') { + extraPytestOpts = "--run_testrail_ids='${params.TR_CASE_IDS}'" + } else { + extraPytestOpts = "-m \"${params.TEST_MARKERS}\"" + } + + withCredentials([ + usernamePassword( + credentialsId: 'status-im-auto', + usernameVariable: 'GIT_HUB_USER', /* ignored */ + passwordVariable: 'GIT_HUB_TOKEN' + ), + usernamePassword( + credentialsId: 'test-rail-api', + usernameVariable: 'TESTRAIL_USER', + passwordVariable: 'TESTRAIL_PASS' + ), + usernamePassword( + credentialsId: 'lambda-test-api', + usernameVariable: 'LAMBDA_TEST_USERNAME', + passwordVariable: 'LAMBDA_TEST_ACCESS_KEY' + ), + string( + credentialsId: 'etherscan-api-key', + variable: 'ETHERSCAN_API_KEY' + ), + string( + credentialsId: 'infura-e2e-token', + variable: 'WEB3_INFURA_PROJECT_ID' + ), + file( + credentialsId: "mobile-tests-eth-accounts", + variable: 'TEST_ETH_ACCOUNTS_FILE' + ), + ]) { + dir('test/appium/tests') { + /* Provide Eth test account secrets. */ + sh 'cp -f $TEST_ETH_ACCOUNTS_FILE users.py' + sh """ + python3 -m pytest \ + --numprocesses 9 \ + --testrail_report=True \ + -k \"${params.KEYWORD_EXPRESSION}\" \ + --apk=${params.APK_URL ?: apk_path} \ + --build=PR-${params.PR_ID}-${utils.timestamp()} \ + --pr_number=${params.PR_ID} \ + ${extraPytestOpts} + """ + } + } + } } + } + } + post { + cleanup { + sh 'make purge' + } + } +} diff --git a/ci/tests/Jenkinsfile.e2e-upgrade b/ci/tests/Jenkinsfile.e2e-upgrade new file mode 100644 index 00000000000..32ac6064eae --- /dev/null +++ b/ci/tests/Jenkinsfile.e2e-upgrade @@ -0,0 +1,136 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + + agent { label 'linux' } + + parameters { + string( + name: 'APK_URL', + description: 'URL of APK to be tested(base for upgrade, usually release build)', + ) + string( + name: 'APK_URL_UPGRADE', + description: 'URL of APK of upgraded application (installed on top of base)', + ) + string( + name: 'KEYWORD_EXPRESSION', + description: 'This will run tests which contain names that match the given string expression (Optional)', + defaultValue: '', + ) + string( + name: 'TR_CASE_IDS', + description: 'IDs of the TestRail case, separated by a comma (Optional)', + defaultValue: '', + ) + } + + options { + disableConcurrentBuilds() + buildDiscarder(logRotator( + numToKeepStr: '10', + daysToKeepStr: '30', + artifactNumToKeepStr: '1', + )) + } + + environment { + /* Avoid race conditions with other builds using virtualenv. */ + VIRTUAL_ENV = "${WORKSPACE_TMP}/venv" + PATH = "${VIRTUAL_ENV}/bin:${PATH}" + } + + stages { + stage('Prep') { + steps { script { + if (params.APK_URL == null) { + error("APK_URL parameter not set!") + } + if (params.APK_URL_UPGRADE == null) { + error("APK_URL_UPGRADE parameter not set!") + } + } } + } + + stage('Setup') { + steps { script { + dir('test/appium') { + sh "python3 -m venv ${VIRTUAL_ENV}" + sh 'pip3 install -r requirements.txt' + } + } } + } + + stage('Test') { + steps { script { + /* for managing optional arguments */ + def extraPytestOpts = '' + if (params.TR_CASE_IDS != '') { + extraPytestOpts = "--run_testrail_ids='${params.TR_CASE_IDS}'" + } + withCredentials([ + usernamePassword( + credentialsId: 'test-rail-api', + usernameVariable: 'TESTRAIL_USER', + passwordVariable: 'TESTRAIL_PASS' + ), + usernamePassword( + credentialsId: 'sauce-labs-api', + usernameVariable: 'SAUCE_USERNAME', + passwordVariable: 'SAUCE_ACCESS_KEY' + ), + string( + credentialsId: 'etherscan-api-key', + variable: 'ETHERSCAN_API_KEY' + ), + string( + credentialsId: 'infura-e2e-token', + variable: 'WEB3_INFURA_PROJECT_ID' + ), + file( + credentialsId: "mobile-tests-eth-accounts", + variable: 'TEST_ETH_ACCOUNTS_FILE' + ), + ]) { + dir('test/appium/tests') { + /* Provide Eth test accounts secrets. */ + sh 'cp -f $TEST_ETH_ACCOUNTS_FILE users.py' + sh """ + python3 -m pytest \ + -m "upgrade" \ + -k \"${params.KEYWORD_EXPRESSION}\" \ + --numprocesses 4 \ + --rerun_count=2 \ + --testrail_report=True \ + --apk=${params.APK_URL} \ + --apk_upgrade=${params.APK_URL_UPGRADE} \ + ${extraPytestOpts} + """ + } + } + } + } } + } + + post { + always { + script { + sauce('sauce-labs-cred') { + saucePublisher() + } + } + } + success { + script { + junit( + testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], + testResults: 'test/appium/tests/*.xml' + ) + } + } + cleanup { + sh 'make purge' + } + } +} diff --git a/ci/tools/Jenkinsfile.fastlane-clean b/ci/tools/Jenkinsfile.fastlane-clean new file mode 100644 index 00000000000..b5019384776 --- /dev/null +++ b/ci/tools/Jenkinsfile.fastlane-clean @@ -0,0 +1,54 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + agent { label 'macos' } + + environment { + LANG = 'en_US.UTF-8' + LANGUAGE = 'en_US.UTF-8' + LC_ALL = 'en_US.UTF-8' + PLATFORM = 'ios' + FASTLANE_DISABLE_COLORS = 1 + /* avoid writing to r/o /nix */ + GEM_HOME = '~/.rubygems' + } + + options { + timestamps() + /* Disable concurrent jobs */ + disableConcurrentBuilds() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 45, unit: 'MINUTES') + /* Don't keep more than 50 builds */ + buildDiscarder(logRotator(numToKeepStr: '50')) + } + + stages { + stage('Prep') { + steps { script { + nix.shell( + 'bundle install --gemfile=fastlane/Gemfile', + attr: 'shells.fastlane', + ) + } } + } + stage('Clean Users'){ + steps { script { + withCredentials([ + usernamePassword( + credentialsId: 'fastlane-match-apple-id', + usernameVariable: 'FASTLANE_APPLE_ID', + passwordVariable: 'FASTLANE_PASSWORD' + ), + ]) { + nix.shell( + 'bundle exec --gemfile=fastlane/Gemfile fastlane ios clean', + keepEnv: ['FASTLANE_APPLE_ID', 'FASTLANE_PASSWORD'], + attr: 'shells.fastlane', + ) + } + } } + } + } +} diff --git a/ci/tools/Jenkinsfile.nix-cache b/ci/tools/Jenkinsfile.nix-cache new file mode 100644 index 00000000000..f2ea808d9b4 --- /dev/null +++ b/ci/tools/Jenkinsfile.nix-cache @@ -0,0 +1,178 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + agent { label params.AGENT_LABEL } + + parameters { + string( + name: 'NIX_CACHE_HOST', + description: 'FQDN of Nix binary cache host.', + defaultValue: params.NIX_CACHE_HOST ?: 'cache-01.he-eu-hel1.ci.nix.status.im' + ) + string( + name: 'NIX_CACHE_USER', + description: 'Username for Nix binary cache host.', + defaultValue: params.NIX_CACHE_USER ?: 'nix-cache' + ) + booleanParam( + name: 'BUILD_COMPONENTS', + description: 'Whether to build Mobile components first.', + defaultValue: params.BUILD_COMPONENTS ?: true, + ) + } + + environment { + NIX_SSHOPTS = "-oStrictHostKeyChecking=accept-new" + NIX_CONF_DIR = "${env.WORKSPACE}/nix" + NIX_STORE_CMD = '/nix/var/nix/profiles/default/bin/nix-store' + NIX_SSH_REMOTE = "ssh://${params.NIX_CACHE_USER}@${params.NIX_CACHE_HOST}?remote-program=${env.NIX_STORE_CMD}" + } + + options { + timestamps() + disableConcurrentBuilds() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 120, unit: 'MINUTES') + /* Limit builds retained */ + buildDiscarder(logRotator( + numToKeepStr: '20', + daysToKeepStr: '30', + artifactNumToKeepStr: '1', + )) + } + + stages { + stage('Setup') { + steps { script { + nix.shell('nix-env -i openssh', sandbox: false, pure: false) + /* some build targets don't build on MacOS */ + os = sh(script: 'uname', returnStdout: true).trim() + arch = sh(script: 'arch', returnStdout: true).trim() + } } + } + + stage('Build status-go') { + when { expression { params.BUILD_COMPONENTS } } + steps { script { + def platforms = ['mobile.android', 'mobile.ios', 'library'] + if (os != 'Darwin') { platforms.removeAll { it == 'mobile.ios' } } + /* FIXME: "'x86_64-darwin' with features {} is required to build" */ + if (arch == 'arm64') { platforms.removeAll { it == 'mobile.android' } } + platforms.each { platform -> + /* Allow for Android builds on Apple ARM. */ + env.NIXPKGS_SYSTEM_OVERRIDE = nixSysOverride(os, arch, platform) + nix.build( + attr: "targets.status-go.${platform}", + sandbox: false, + link: false + ) + } + } } + } + + stage('Build android jsbundle') { + when { expression { params.BUILD_COMPONENTS } } + steps { script { + /* Build/fetch deps required for jsbundle build. */ + nix.build( + attr: 'targets.mobile.jsbundle', + sandbox: false, + pure: false, + link: false + ) + } } + } + + stage('Build android deps') { + when { expression { params.BUILD_COMPONENTS } } + steps { script { + /* Allow for Android builds on Apple ARM. */ + env.NIXPKGS_SYSTEM_OVERRIDE = nixSysOverride(os, arch, 'android') + /* Build/fetch deps required to build android release. */ + nix.build( + attr: 'targets.mobile.android.build.buildInputs', + sandbox: false, + pure: false, + link: false + ) + } } + } + + stage('Build nix shell deps') { + when { expression { params.BUILD_COMPONENTS } } + steps { script { + def shells = ['android', 'ios', 'fastlane', 'keytool', 'clojure', 'gradle'] + if (os != 'Darwin') { shells.removeAll { it == 'ios' } } + /* FIXME: "'x86_64-darwin' with features {} is required to build" */ + if (arch == 'arm64') { shells.removeAll { it == 'android' } } + /* Build/fetch deps required to start default Nix shell. */ + shells.each { shell -> + /* Allow for Android builds on Apple ARM. */ + env.NIXPKGS_SYSTEM_OVERRIDE = nixSysOverride(os, arch, shell) + nix.build( + attr: "shells.${shell}.buildInputs", + sandbox: false, + link: false + ) + } + } } + } + + stage('Collect') { + steps { script { + exitCode = nix.shell( + """ + find /nix/store/ -mindepth 1 -maxdepth 1 \ + | grep -v -e '.*.links\$' -e '.*.lock\$' -e '.*.drv\$' -e '.*.drv\$' -e '.*-status-mobile-.*' \ + | xargs nix path-info --recursive \ + > "${WORKSPACE_TMP}/store-paths.txt" + """, + pure: false, + returnStatus: true + ) + /* Some paths can throw 'path is not valid' errors, but it's not fatal. */ + if (exitCode != 0) { + currentBuild.result = 'UNSTABLE' + } + if (lineCount("${WORKSPACE_TMP}/store-paths.txt") == 0) { + throw new Exception("No Nix store paths found to copy.") + } + } } + } + + stage('Upload') { + steps { script { + sshagent(credentials: ['nix-cache-ssh']) { + nix.shell( + """ + cat "${WORKSPACE_TMP}/store-paths.txt" \ + | nix copy --stdin --to "${NIX_SSH_REMOTE}" + """, + pure: false + ) + } + } } + } + } + post { + always { script { + nix.shell('nix-store --optimize', pure: false) + } } + } +} + +def nixSysOverride(os, arch, target='android') { + return ( + os == 'Darwin' && + arch == 'arm64' && + target =~ /.*android$/ + ) ? 'x86_64-darwin' : '' +} + +def lineCount(filePath) { + return sh( + script: "wc -l ${filePath}", + returnStdout: true + ).trim() +} diff --git a/ci/tools/Jenkinsfile.playstore-meta b/ci/tools/Jenkinsfile.playstore-meta new file mode 100644 index 00000000000..fcdc6950aaa --- /dev/null +++ b/ci/tools/Jenkinsfile.playstore-meta @@ -0,0 +1,54 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + agent { label 'linux' } + + environment { + LANG = 'en_US.UTF-8' + LANGUAGE = 'en_US.UTF-8' + LC_ALL = 'en_US.UTF-8' + PLATFORM = 'ios' + FASTLANE_DISABLE_COLORS = 1 + /* avoid writing to r/o /nix */ + GEM_HOME = '~/.rubygems' + } + + options { + timestamps() + /* Disable concurrent jobs */ + disableConcurrentBuilds() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 5, unit: 'MINUTES') + /* Don't keep more than 50 builds */ + buildDiscarder(logRotator(numToKeepStr: '50')) + } + + stages { + stage('Prep') { + steps { script { + nix.shell( + 'bundle install --gemfile=fastlane/Gemfile', + attr: 'shells.fastlane', + ) + } } + } + stage('Play Store Update'){ + steps { script { + withCredentials([ + string( + credentialsId: 'google-play-api-key-json', + variable: 'GOOGLE_PLAY_JSON_KEY' + ), + ]) { + nix.shell( + 'bundle exec --gemfile=fastlane/Gemfile fastlane android upload_metadata', + keepEnv: ['FASTLANE_DISABLE_COLORS', 'GOOGLE_PLAY_JSON_KEY'], + attr: 'shells.fastlane', + pure: false + ) + } + } } + } + } +} diff --git a/ci/tools/Jenkinsfile.pod-repo-update b/ci/tools/Jenkinsfile.pod-repo-update new file mode 100644 index 00000000000..c212c3eda87 --- /dev/null +++ b/ci/tools/Jenkinsfile.pod-repo-update @@ -0,0 +1,44 @@ +#!/usr/bin/env groovy +/** + * This job runs daily and executes `pod repo update` on MacOS + * This is done to avoid issues with out of date repo causing errors like: + * + * Failed with exit code 1 (in target 'StatusImPR' from project 'StatusIm') + **/ + +pipeline { + agent { + label params.HOST_LABEL + } + + triggers { + /* Run daily at 2am */ + cron('H 2 * * *') + } + options { + timestamps() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 5, unit: 'MINUTES') + /* Limit builds retained */ + buildDiscarder(logRotator( + numToKeepStr: '20', + )) + } + + parameters { + string( + name: 'HOST_LABEL', + description: 'Label of host to run on', + /* Using startTimeInMillis to randomize which host gets the update. */ + defaultValue: "maci7-0${(currentBuild.startTimeInMillis % 3) + 1}", + ) + } + + stages { + stage('Update') { + steps { + sh 'pod repo update' + } + } + } +} diff --git a/ci/tools/Jenkinsfile.xcode-clean b/ci/tools/Jenkinsfile.xcode-clean new file mode 100644 index 00000000000..fbdfd0d4805 --- /dev/null +++ b/ci/tools/Jenkinsfile.xcode-clean @@ -0,0 +1,48 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.24' + +pipeline { + agent { + label 'linux' + } + + triggers { + cron('H 5 * * *') + } + + options { + timestamps() + /* Prevent Jenkins jobs from running forever */ + timeout(time: 15, unit: 'MINUTES') + /* Disable concurrent jobs */ + disableConcurrentBuilds() + /* Don't keep more than 50 builds */ + buildDiscarder(logRotator(numToKeepStr: '10')) + } + + stages { + stage('Get Nodes') { + steps { script { + stagePerNode = nodesByLabel('macos').collectEntries { + ["${it}" : generateNodeCleanupStage(it)] + } + } } + } + + stage('Clean Xcode') { + steps { script { + parallel stagePerNode + } } + } + } +} + +def generateNodeCleanupStage(nodeLabel) { + return { stage(nodeLabel) { + node(nodeLabel) { + dir('/Users/jenkins/Library/Developer/Xcode') { + sh 'rm -fr Archives DerivedData' + } + } + } } +} diff --git a/clj-rn.conf.edn b/clj-rn.conf.edn deleted file mode 100644 index 1777db66d80..00000000000 --- a/clj-rn.conf.edn +++ /dev/null @@ -1,73 +0,0 @@ -{:name "StatusIm" - - :run-options {:android {"appIdSuffix" "debug"}} - - :figwheel-bridge "figwheel-bridge.js" - ;; JS modules - :js-modules ["realm" - "react-native-i18n" - "realm/react-native" - "dismissKeyboard" - "react-native-splash-screen" - "react-native-status" - "react-native-camera" - "react-native-qrcode" - "identicon.js" - "react-native-fs" - "react-native-dialogs" - "react-native-image-resizer" - "react-native-image-crop-picker" - "react-native-securerandom" - "react-native-webview-bridge" - "react-native-fcm" - "homoglyph-finder" - "web3" - "chance" - "instabug-reactnative" - "react-native-http-bridge" - "emojilib" - "react-native-config" - "react-native-svg" - "react-native-keychain" - "rn-snoopy" - "rn-snoopy/stream/bars" - "rn-snoopy/stream/filter" - "rn-snoopy/stream/buffer" - "react-native/Libraries/vendor/emitter/EventEmitter" - "react-native-background-timer" - "react-native-fetch-polyfill" - "react-native-testfairy"] - - ;; Resoures - :resource-dirs ["resources/images"] - - :figwheel-options {:nrepl-port 7888 - :nrepl-middleware ["cider.nrepl/cider-middleware" - "refactor-nrepl.middleware/wrap-refactor" - "cemerick.piggieback/wrap-cljs-repl"]} - - :builds [{:id :desktop - :source-paths ["react-native/src" "src" "env/dev"] - :compiler {:output-to "target/ios/desktop.js" - :main "env.desktop.main" - :output-dir "target/desktop" - :npm-deps false - :optimizations :none} - :figwheel true} - {:id :ios - :source-paths ["react-native/src" "src" "env/dev"] - :compiler {:output-to "target/ios/app.js" - :main "env.ios.main" - :output-dir "target/ios" - :npm-deps false - :optimizations :none} - :figwheel true} - {:id :android - :source-paths ["react-native/src" "src" "env/dev"] - :compiler {:output-to "target/android/app.js" - :main "env.android.main" - :output-dir "target/android" - :npm-deps false - :optimizations :none} - :warning-handlers [status-im.utils.build/warning-handler] - :figwheel true}]} diff --git a/components/src/status_im/ui/components/react.cljs b/components/src/status_im/ui/components/react.cljs deleted file mode 100644 index c37a75c983e..00000000000 --- a/components/src/status_im/ui/components/react.cljs +++ /dev/null @@ -1,274 +0,0 @@ -(ns status-im.ui.components.react - (:require-macros [status-im.utils.views :as views]) - (:require [clojure.string :as string] - [goog.object :as object] - [reagent.core :as reagent] - [status-im.ui.components.styles :as styles] - [status-im.utils.utils :as utils] - [status-im.utils.platform :as platform] - [status-im.i18n :as i18n] - [status-im.react-native.js-dependencies :as js-dependencies])) - -(defn get-react-property [name] - (if js-dependencies/react-native - (object/get js-dependencies/react-native name) - #js {})) - -(defn adapt-class [class] - (when class - (reagent/adapt-react-class class))) - -(defn get-class [name] - (adapt-class (get-react-property name))) - -(def native-modules (.-NativeModules js-dependencies/react-native)) -(def device-event-emitter (.-DeviceEventEmitter js-dependencies/react-native)) -(def dismiss-keyboard! js-dependencies/dismiss-keyboard) -(def back-handler (get-react-property "BackHandler")) - -(def splash-screen (.-SplashScreen native-modules)) - -;; React Components - -(def app-registry (get-react-property "AppRegistry")) -(def app-state (get-react-property "AppState")) -(def net-info (get-react-property "NetInfo")) -(def view (get-class "View")) -(def safe-area-view (get-class "SafeAreaView")) - -(def status-bar (get-class (if platform/desktop? "View" "StatusBar"))) - -(def scroll-view (get-class "ScrollView")) -(def web-view (get-class "WebView")) -(def keyboard-avoiding-view-class (get-class "KeyboardAvoidingView")) - -(def refresh-control (get-class "RefreshControl")) - -(def text-class (get-class "Text")) -(def text-input-class (get-class "TextInput")) -(def image (get-class "Image")) -(def switch (get-class "Switch")) -(def check-box (get-class "CheckBox")) - -(def touchable-highlight-class (get-class "TouchableHighlight")) -(def touchable-without-feedback-class (get-class "TouchableWithoutFeedback")) -(def touchable-opacity (get-class "TouchableOpacity")) -(def activity-indicator (get-class "ActivityIndicator")) - -(def modal (get-class "Modal")) - -(def pan-responder (.-PanResponder js-dependencies/react-native)) -(def animated (.-Animated js-dependencies/react-native)) -(def animated-view (reagent/adapt-react-class (.-View animated))) -(def animated-text (reagent/adapt-react-class (.-Text animated))) - -(def dimensions (.-Dimensions js-dependencies/react-native)) -(def keyboard (.-Keyboard js-dependencies/react-native)) -(def linking (.-Linking js-dependencies/react-native)) - -(def slider (get-class "Slider")) -;; Accessor methods for React Components - -(defn add-font-style [style-key {:keys [font] :as opts :or {font :default}}] - (let [font (get-in platform/platform-specific [:fonts (keyword font)]) - style (get opts style-key)] - (-> opts - (dissoc :font) - (assoc style-key (merge style font))))) - - -(defn transform-to-uppercase [{:keys [uppercase? force-uppercase?] :as opts} ts] - (if (or force-uppercase? (and uppercase? platform/android?)) - (vec (map string/upper-case ts)) - ts)) - -(defn text - ([t] - [text-class t]) - ([opts t & ts] - (->> (conj ts t) - (transform-to-uppercase opts) - (concat [text-class (add-font-style :style opts)]) - (vec)))) - -(defn text-input [{:keys [font style] :as opts - :or {font :default}} text] - (let [font (get-in platform/platform-specific [:fonts (keyword font)])] - [text-input-class (merge - {:underline-color-android :transparent - :placeholder-text-color styles/text2-color - :placeholder (i18n/label :t/type-a-message) - :value text} - (-> opts - (dissoc :font) - (assoc :style (merge style font))))])) - -(defn i18n-text - [{:keys [style key]}] - (let [default-style {:letter-spacing -0.2 - :font-size 14}] - [text {:style (merge default-style style)} (i18n/label key)])) - -(defn icon - ([n] (icon n styles/icon-default)) - ([n style] - [image {:source {:uri (keyword (str "icon_" (name n)))} - :resizeMode "contain" - :style style}])) - -(defn touchable-highlight [props content] - [touchable-highlight-class - (merge {:underlay-color :transparent} props) - content]) - -(defn touchable-without-feedback [props content] - [touchable-without-feedback-class - props - content]) - -(defn get-dimensions [name] - (js->clj (.get dimensions name) :keywordize-keys true)) - -(defn list-item [component] - (reagent/as-element component)) - -;; Image picker - -(def image-picker-class js-dependencies/image-crop-picker) - -(defn show-access-error [o] - (when (= "ERROR_PICKER_UNAUTHORIZED_KEY" (object/get o "code")) ; Do not show error when user cancel selection - (utils/show-popup (i18n/label :t/error) - (i18n/label :t/photos-access-error)))) - -(defn show-image-picker [images-fn] - (let [image-picker (.-default image-picker-class)] - (-> image-picker - (.openPicker (clj->js {:multiple false})) - (.then images-fn) - (.catch show-access-error)))) - -;; Clipboard - -(def sharing - (.-Share js-dependencies/react-native)) - -(defn copy-to-clipboard [text] - (.setString (.-Clipboard js-dependencies/react-native) text)) - -(defn get-from-clipboard [clbk] - (let [clipboard-contents (.getString (.-Clipboard js-dependencies/react-native))] - (.then clipboard-contents #(clbk %)))) - -;; HTTP Bridge - -(def http-bridge js-dependencies/http-bridge) - -;; KeyboardAvoidingView - -(defn keyboard-avoiding-view [props & children] - (let [view-element (if platform/ios? - [keyboard-avoiding-view-class (merge {:behavior :padding} props)] - [view props])] - (vec (concat view-element children)))) - -(views/defview with-activity-indicator - [{:keys [timeout style enabled? preview]} comp] - (views/letsubs - [loading (reagent/atom true)] - {:component-did-mount (fn [] - (if (or (nil? timeout) - (> 100 timeout)) - (reset! loading false) - (utils/set-timeout #(reset! loading false) - timeout)))} - (if (and (not enabled?) @loading) - (or preview - [view {:style (or style {:justify-content :center - :align-items :center})} - [activity-indicator {:animating true}]]) - comp))) - -(defn navigation-wrapper - "Wraps component so that it will be shown only when current-screen is one of views" - [{:keys [component views current-view hide?] - :or {hide? false}}] - (let [current-view? (if (set? views) - (views current-view) - (= views current-view)) - - style (if current-view? - {:flex 1 - :zIndex 0} - {:opacity 0 - :flex 0 - :zIndex -1}) - - component' (if (fn? component) [component] component)] - - (when (or (not hide?) (and hide? current-view?)) - (if hide? - component' - [view style (if (fn? component) [component] component)])))) - -(defn with-empty-preview [comp] - [with-activity-indicator - {:preview [view {}]} - comp]) - -;; Platform-specific View - -(defmulti create-main-screen-view #(cond - platform/iphone-x? :iphone-x - platform/ios? :ios - platform/android? :android)) - -(defmethod create-main-screen-view :iphone-x [current-view] - (fn [props & children] - (let [props (merge props - {:background-color - (case current-view - (:wallet - :wallet-send-transaction - :wallet-transaction-sent - :wallet-request-transaction - :wallet-send-assets - :wallet-request-assets - :choose-recipient - :recent-recipients - :wallet-send-transaction-modal - :wallet-transaction-sent-modal - :wallet-send-transaction-request - :wallet-transaction-fee - :wallet-sign-message-modal - :contact-code) styles/color-blue4 - (:qr-viewer - :recipient-qr-code) "#2f3031" - (:accounts :login - :wallet-transactions-filter) styles/color-white - :transparent)}) - children (cond-> children - (#{:wallet - :recent-recipients - :wallet-send-assets - :wallet-request-assets} current-view) - (conj [view {:background-color styles/color-white - :position :absolute - :bottom 0 - :right 0 - :left 0 - :height 100 - :z-index -1000}]))] - (apply vector safe-area-view props children)))) - -(defmethod create-main-screen-view :default [_] - view) - -(views/defview main-screen-modal-view [current-view & components] - (views/letsubs [signing? [:get-in [:wallet :send-transaction :signing?]]] - (let [main-screen-view (create-main-screen-view current-view)] - [main-screen-view styles/flex - [keyboard-avoiding-view {:flex 1 :flex-direction :column} - (apply vector view styles/flex components) - (when (and platform/iphone-x? (not signing?)) - [view {:flex 0 :height 34}])]]))) diff --git a/components/src/status_im/ui/components/svg.clj b/components/src/status_im/ui/components/svg.clj deleted file mode 100644 index e4b093c7745..00000000000 --- a/components/src/status_im/ui/components/svg.clj +++ /dev/null @@ -1,41 +0,0 @@ -(ns status-im.ui.components.svg - (:require [clojure.string :as string] - [hickory.core :as hickory])) - -(def svg-tags #{:svg :g :rect :path :use :defs :circle}) - -(defmacro slurp-svg [file] - "Reads svg file, and return function (fn [color] ..), which returns hiccup structure for react-native-svg lib - - Example - - SVG: - \n - \n - - - Result: - (fn [color] - [path {:fill color :fill-rule \"evenodd\" :d \"M13.5416667...\"}] - - Attention!!!: Please make sure svg file has fill field, and has structure like above - " - (let [svg (-> (clojure.core/slurp file) - (string/replace #"[\n]\s*" "")) - svg-hiccup (hickory/as-hiccup (first (hickory/parse-fragment svg))) - color (gensym "args")] - `(fn [~color] - ~(into [] - (clojure.walk/postwalk-replace - {:viewbox :viewBox} ;; See https://github.com/jhy/jsoup/issues/272 - (clojure.walk/prewalk - (fn [node] - (if (svg-tags node) - (symbol (name node)) - (if (vector? node) - (let [[k v] node] - (if (and (= :fill k) v) - [k color] - node)) - node))) - svg-hiccup)))))) \ No newline at end of file diff --git a/default.nix b/default.nix new file mode 100644 index 00000000000..3b2898217c5 --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +# for passing build options, see nix/README.md +{ config ? { } }: + +let + main = import ./nix { inherit config; }; +in { + # this is where the --attr argument selects the shell or target + inherit (main) pkgs targets shells; + inherit (main.pkgs) config; +} diff --git a/deployment/linux/.env b/deployment/linux/.env deleted file mode 100644 index 8ecc7624a06..00000000000 --- a/deployment/linux/.env +++ /dev/null @@ -1,19 +0,0 @@ -TESTFAIRY_ENABLED=0 -STUB_STATUS_GO=0 -ETHEREUM_DEV_CLUSTER=1 -MAINNET_NETWORKS_ENABLED=1 -OFFLINE_INBOX_ENABLED=1 -OFFLINE_INBOX_MANY_ENABLED=1 -LOG_LEVEL=debug -LOG_LEVEL_STATUS_GO=info -JSC_ENABLED=1 -QUEUE_MESSAGE_ENABLED=1 -MANY_WHISPER_TOPICS_ENABLED=0 -RN_BRIDGE_THRESHOLD_WARNINGS=0 -COMPILE_VIEWS_ENABLED=0 -POW_TARGET=0.002 -POW_TIME=1 -MIXPANEL_TOKEN=3f2e1a8970f159aa2a3d5dc5d65eab38 -DEFAULT_NETWORK=mainnet_rpc -TESTFAIRY_TOKEN=969f6c921cb435cea1d41d1ea3f5b247d6026d55 -INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c diff --git a/deployment/linux/usr/share/applications/StatusIm.desktop b/deployment/linux/usr/share/applications/StatusIm.desktop deleted file mode 100644 index 93b6441f413..00000000000 --- a/deployment/linux/usr/share/applications/StatusIm.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Type=Application -Name=StatusIm -Comment=StatusIm Desktop -Exec=StatusIm -Icon=StatusIm -Categories=Network; \ No newline at end of file diff --git a/deployment/linux/usr/share/icons/hicolor/1024x1024/apps/StatusIm.png b/deployment/linux/usr/share/icons/hicolor/1024x1024/apps/StatusIm.png deleted file mode 100644 index c19a8246180..00000000000 Binary files a/deployment/linux/usr/share/icons/hicolor/1024x1024/apps/StatusIm.png and /dev/null differ diff --git a/deps.edn b/deps.edn deleted file mode 100644 index 11bc69363d0..00000000000 --- a/deps.edn +++ /dev/null @@ -1,35 +0,0 @@ -{:paths ["components/src" "src" "react-native/src/cljsjs" "react-native/src/mobile" "resources"] - :deps {org.clojure/clojure {:mvn/version "1.9.0"} - org.clojure/clojurescript {:mvn/version "1.10.238"} - org.clojure/core.async {:mvn/version "0.4.474"} - reagent {:mvn/version "0.7.0" - :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server cljsjs/create-react-class]} - re-frame {:git/url "https://github.com/status-im/re-frame" - :sha "b0d3b0f016f217c6a397a54e5eba9486471fb172" - :deps/manifest :deps} - com.andrewmcveigh/cljs-time {:mvn/version "0.5.2"} - com.taoensso/timbre {:mvn/version "4.10.0"} - hickory {:mvn/version "0.7.1"} - com.cognitect/transit-cljs {:mvn/version "0.8.248"}} - - :aliases - {:dev {:extra-deps - {clj-rn {:git/url "https://github.com/status-im/clj-rn" - :sha "d9b835701232cb1744a670e6dd21325712a43789"} - - ;; Figwheel ClojureScript REPL - com.cemerick/piggieback {:mvn/version "0.2.2" - :exclusions [com.google.javascript/closure-compiler]} - figwheel-sidecar {:mvn/version "0.5.16" - :exclusions [com.google.javascript/closure-compiler]} - re-frisk-remote {:mvn/version "0.5.5"} - re-frisk-sidecar {:mvn/version "0.5.7"} - hawk {:mvn/version "0.2.11"} - day8.re-frame/tracing {:mvn/version "0.5.0"} - - ;; CIDER compatible nREPL - cider/cider-nrepl {:mvn/version "0.16.0"} - org.clojure/tools.nrepl {:mvn/version "0.2.13"} - refactor-nrepl {:mvn/version "2.3.1"}}} - :test {:extra-deps {day8.re-frame/test {:mvn/version "0.1.5"} - doo {:mvn/version "0.1.9"}}}}} diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt deleted file mode 100644 index c3c8a4146c0..00000000000 --- a/desktop/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ - -# Copyright (C) 2016, Canonical Ltd. -# All rights reserved. - -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. An additional grant -# of patent rights can be found in the PATENTS file in the same directory. - -cmake_minimum_required(VERSION 2.8.11) - -set(APP_NAME StatusIm) -set(REACT_BUILD_STATIC_LIB ON) - -message(STATUS "EXTERNAL_MODULES_DIR: ${EXTERNAL_MODULES_DIR}") - -foreach(external_module ${EXTERNAL_MODULES_DIR}) - message(STATUS "external_module: ${external_module}") - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../${external_module} ${CMAKE_CURRENT_BINARY_DIR}/${external_module}) -endforeach(external_module) - -# APPLICATION_MAIN_CPP_PATH contains absolute path to generated template copy of main.cpp for application executable -get_filename_component(APPLICATION_MAIN_CPP_PATH main.cpp ABSOLUTE) - -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../node_modules/react-native/React/Layout) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../node_modules/react-native/ReactQt/runtime/src ${CMAKE_CURRENT_BINARY_DIR}/lib) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../node_modules/react-native/ReactQt/application/src ${CMAKE_CURRENT_BINARY_DIR}/bin) - -if (WIN32) - set(RUN_SCRIPT_FILE_NAME "run-app.bat") -else() - set(RUN_SCRIPT_FILE_NAME "run-app.sh") -endif() - -configure_file( - ${RUN_SCRIPT_FILE_NAME}.in - ${CMAKE_CURRENT_BINARY_DIR}/${RUN_SCRIPT_FILE_NAME} - @ONLY -) - diff --git a/desktop/build.bat b/desktop/build.bat deleted file mode 100644 index 998324cee80..00000000000 --- a/desktop/build.bat +++ /dev/null @@ -1,34 +0,0 @@ -@rem Copyright (c) 2017-present, Status Research and Development GmbH. -@rem All rights reserved. -@rem -@rem This source code is licensed under the BSD-style license found in the -@rem LICENSE file in the root directory of this source tree. An additional grant -@rem of patent rights can be found in the PATENTS file in the same directory. - -@echo off -setlocal EnableDelayedExpansion - -set "option=" -for %%a in (%*) do ( - if not defined option ( - set arg=%%a - if "!arg:~0,1!" equ "-" set "option=!arg!" - ) else ( - set "option!option!=%%a" - set "option=" - ) -) - -SET option -@echo on - -echo "build.bat external modules paths: "%option-e% -echo "build.bat JS bundle path: "%option-j% -echo "build.bat cmake generator: "%option-g% - -@rem Workaround -@rem rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile - -@rem Build project -echo %CD% -cmake -DCMAKE_BUILD_TYPE=Debug -G %option-g% -DEXTERNAL_MODULES_DIR=%option-e% -DJS_BUNDLE_PATH=%option-j% . && cmake --build . diff --git a/desktop/build.sh b/desktop/build.sh deleted file mode 100755 index 185a14918d3..00000000000 --- a/desktop/build.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2016, Canonical Ltd. -# All rights reserved. - -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. An additional grant -# of patent rights can be found in the PATENTS file in the same directory. - -# XXX: Don't move this script -cd $(dirname $0) - -while (( "$#" )); do -if [[ $1 == "-e" ]]; then - shift - ExternalModulesPaths="$1" -fi -if [[ $1 == "-j" ]]; then - shift - JsBundlePath="$1" -fi -shift -done - -echo "build.sh external modules paths: "$ExternalModulesPaths -echo "build.sh JS bundle path: "$JsBundlePath - -# Workaround -rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile - -# Build project -cmake -DCMAKE_BUILD_TYPE=Debug -DEXTERNAL_MODULES_DIR="$ExternalModulesPaths" -DJS_BUNDLE_PATH="$JsBundlePath" . && make diff --git a/desktop/main.cpp b/desktop/main.cpp deleted file mode 100644 index 8b61b219473..00000000000 --- a/desktop/main.cpp +++ /dev/null @@ -1,292 +0,0 @@ - -/** - * Copyright (C) 2016, Canonical Ltd. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -// #define BUILD_FOR_BUNDLE - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "attachedproperties.h" -#include "reactitem.h" -#include "rootview.h" -#include "utilities.h" - -#ifdef BUILD_FOR_BUNDLE -QStringList consoleOutputStrings; -bool ubuntuServerStarted = false; -#endif - -const int MAIN_WINDOW_WIDTH = 1024; -const int MAIN_WINDOW_HEIGHT = 768; - -// TODO: some way to change while running -class ReactNativeProperties : public QObject { - Q_OBJECT - Q_PROPERTY(bool liveReload READ liveReload WRITE setLiveReload NOTIFY - liveReloadChanged) - Q_PROPERTY(QUrl codeLocation READ codeLocation WRITE setCodeLocation NOTIFY - codeLocationChanged) - Q_PROPERTY(QString pluginsPath READ pluginsPath WRITE setPluginsPath NOTIFY - pluginsPathChanged) - Q_PROPERTY( - QString executor READ executor WRITE setExecutor NOTIFY executorChanged) -public: - ReactNativeProperties(QObject *parent = 0) : QObject(parent) { - m_codeLocation = m_packagerTemplate.arg(m_packagerHost).arg(m_packagerPort); - } - bool liveReload() const { return m_liveReload; } - void setLiveReload(bool liveReload) { - if (m_liveReload == liveReload) - return; - m_liveReload = liveReload; - Q_EMIT liveReloadChanged(); - } - QUrl codeLocation() const { return m_codeLocation; } - void setCodeLocation(const QUrl &codeLocation) { - if (m_codeLocation == codeLocation) - return; - m_codeLocation = codeLocation; - Q_EMIT codeLocationChanged(); - } - QString pluginsPath() const { return m_pluginsPath; } - void setPluginsPath(const QString &pluginsPath) { - if (m_pluginsPath == pluginsPath) - return; - m_pluginsPath = pluginsPath; - Q_EMIT pluginsPathChanged(); - } - QString executor() const { return m_executor; } - void setExecutor(const QString &executor) { - if (m_executor == executor) - return; - m_executor = executor; - Q_EMIT executorChanged(); - } - QString packagerHost() const { return m_packagerHost; } - void setPackagerHost(const QString &packagerHost) { - if (m_packagerHost == packagerHost) - return; - m_packagerHost = packagerHost; - setCodeLocation(m_packagerTemplate.arg(m_packagerHost).arg(m_packagerPort)); - } - QString packagerPort() const { return m_packagerPort; } - void setPackagerPort(const QString &packagerPort) { - if (m_packagerPort == packagerPort) - return; - m_packagerPort = packagerPort; - setCodeLocation(m_packagerTemplate.arg(m_packagerHost).arg(m_packagerPort)); - } - void setLocalSource(const QString &source) { - if (m_localSource == source) - return; - - // overrides packager* - if (source.startsWith("file:")) { - setCodeLocation(source); - } else { - QFileInfo fi(source); - if (!fi.exists()) { - qWarning() << "Attempt to set non-existent local source file"; - return; - } - setCodeLocation(QUrl::fromLocalFile(fi.absoluteFilePath())); - setLiveReload(false); - } - } -Q_SIGNALS: - void liveReloadChanged(); - void codeLocationChanged(); - void pluginsPathChanged(); - void executorChanged(); - -private: - bool m_liveReload = false; - QString m_packagerHost = "localhost"; - QString m_packagerPort = "8081"; - QString m_localSource; - QString m_packagerTemplate = - "http://%1:%2/index.desktop.bundle?platform=desktop&dev=true"; - QUrl m_codeLocation; - QString m_pluginsPath; -#ifdef BUILD_FOR_BUNDLE - QString m_executor = "RemoteServerConnection"; -#else - QString m_executor = "LocalServerConnection"; -#endif -}; - -#ifdef BUILD_FOR_BUNDLE -void runUbuntuServer(); -void saveMessage(QtMsgType type, const QMessageLogContext &context, - const QString &msg); - -void writeLogsToFile(); -#endif - -int main(int argc, char **argv) { - - QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QGuiApplication app(argc, argv); - - Q_INIT_RESOURCE(react_resources); - -#ifdef BUILD_FOR_BUNDLE - qInstallMessageHandler(saveMessage); - runUbuntuServer(); -#endif - - QQuickView view; - ReactNativeProperties *rnp = new ReactNativeProperties(&view); -#ifdef BUILD_FOR_BUNDLE - rnp->setCodeLocation("file:" + QGuiApplication::applicationDirPath() + - "/assets"); -#endif - - utilities::registerReactTypes(); - - QCommandLineParser p; - p.setApplicationDescription("React Native host application"); - p.addHelpOption(); - p.addOptions({ - {{"R", "live-reload"}, "Enable live reload."}, - {{"H", "host"}, "Set packager host address.", rnp->packagerHost()}, - {{"P", "port"}, "Set packager port number.", rnp->packagerPort()}, - {{"L", "local"}, "Set path to the local packaged source", "not set"}, - {{"M", "plugins-path"}, "Set path to node modules", "./plugins"}, - {{"E", "executor"}, "Set Javascript executor", rnp->executor()}, - }); - p.process(app); - rnp->setLiveReload(p.isSet("live-reload")); - if (p.isSet("host")) - rnp->setPackagerHost(p.value("host")); - if (p.isSet("port")) - rnp->setPackagerPort(p.value("port")); - if (p.isSet("local")) - rnp->setLocalSource(p.value("local")); - if (p.isSet("plugins-path")) - rnp->setPluginsPath(p.value("plugins-path")); - if (p.isSet("executor")) - rnp->setExecutor(p.value("executor")); - - view.rootContext()->setContextProperty("ReactNativeProperties", rnp); - view.setSource(QUrl("qrc:///main.qml")); - view.setResizeMode(QQuickView::SizeRootObjectToView); - view.resize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT); - view.show(); - -#ifdef BUILD_FOR_BUNDLE - QTimer t; - t.setInterval(500); - QObject::connect(&t, &QTimer::timeout, [=]() { writeLogsToFile(); }); - t.start(); -#endif - - return app.exec(); -} - -#ifdef BUILD_FOR_BUNDLE - -QString getDataStoragePath() { - QString dataStoragePath = - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - QDir dir(dataStoragePath); - if (!dir.exists()) { - dir.mkpath("."); - } - return dataStoragePath; -} - -void writeLogsToFile() { - QFile logFile(getDataStoragePath() + "/StatusIm.log"); - if (logFile.open(QIODevice::WriteOnly | QIODevice::Append)) { - for (QString message : consoleOutputStrings) { - logFile.write(message.toStdString().c_str()); - } - consoleOutputStrings.clear(); - - logFile.flush(); - logFile.close(); - } -} - -void runUbuntuServer() { - QProcess *process = new QProcess(); - process->setWorkingDirectory(getDataStoragePath()); - process->setProgram(QGuiApplication::applicationDirPath() + "/ubuntu-server"); - QObject::connect(process, &QProcess::errorOccurred, - [=](QProcess::ProcessError) { - qDebug() << "process name: " << process->program(); - qDebug() << "process error: " << process->errorString(); - }); - - QObject::connect(process, &QProcess::readyReadStandardOutput, [=] { - qDebug() << "ubuntu-server std: " - << process->readAllStandardOutput().trimmed(); - }); - QObject::connect(process, &QProcess::readyReadStandardError, [=] { - QString output = process->readAllStandardError().trimmed(); - qDebug() << "ubuntu-server err: " << output; - if (output.contains("Server starting")) { - ubuntuServerStarted = true; - } - }); - - QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, - [=]() { - qDebug() << "Kill ubuntu server"; - process->kill(); - }); - - qDebug() << "starting ubuntu server..."; - process->start(); - qDebug() << "wait for started..."; - - while (!ubuntuServerStarted) { - QGuiApplication::processEvents(); - } - - qDebug() << "waiting finished"; -} - -void saveMessage(QtMsgType type, const QMessageLogContext &context, - const QString &msg) { - - QByteArray localMsg = msg.toLocal8Bit(); - QString message = localMsg + "\n"; - - switch (type) { - case QtDebugMsg: - consoleOutputStrings << "Debug: " << message << "\n"; - break; - case QtInfoMsg: - consoleOutputStrings << "Info: " << message << "\n"; - break; - case QtWarningMsg: - consoleOutputStrings << "Warning: " << message << "\n"; - break; - case QtCriticalMsg: - consoleOutputStrings << "Critical: " << message << "\n"; - break; - case QtFatalMsg: - - consoleOutputStrings << "Fatal: " << message << "\n"; - writeLogsToFile(); - abort(); - } -} -#endif - -#include "main.moc" diff --git a/desktop/run-app.bat.in b/desktop/run-app.bat.in deleted file mode 100644 index f4dc903d550..00000000000 --- a/desktop/run-app.bat.in +++ /dev/null @@ -1,11 +0,0 @@ -@rem Copyright (c) 2017-present, Status Research and Development GmbH. -@rem All rights reserved. -@rem -@rem This source code is licensed under the BSD-style license found in the -@rem LICENSE file in the root directory of this source tree. An additional grant -@rem of patent rights can be found in the PATENTS file in the same directory. - - -@rem Run app locally -@CMAKE_BINARY_DIR@/bin/@APP_NAME@ - diff --git a/desktop/run-app.sh.in b/desktop/run-app.sh.in deleted file mode 100755 index 401251ea878..00000000000 --- a/desktop/run-app.sh.in +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2016, Canonical Ltd. -# All rights reserved. - -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. An additional grant -# of patent rights can be found in the PATENTS file in the same directory. - -args="" -on_device=0 -plugins_path="" -asset_path="share" -executor="" - -react_host=`hostname -I` - -# Parse args -for arg in "$@" -do - IFS="=" read -a parts <<< "$arg" - if [[ $parts == "--on-device" ]]; then - on_device=1 - elif [[ $parts == "--plugins-path" ]]; then - plugins_path=${parts[1]} - args=$args" --plugins-path=./plugins" - elif [[ $parts == "--asset-path" ]]; then - asset_path=${parts[1]} - elif [[ $parts == "--executor" ]]; then - if [[ $on_device == 1 ]]; then - # Force net executor for now - executor="ReactNetExecutor" - else - executor=${parts[1]} - fi - args=$args" --executor=$executor" - else - args=$args" $parts" - fi -done - -# Handle defaults -if [[ -z "$executor" ]]; then - if [[ $on_device == 1 ]]; then - executor="ReactNetExecutor" - args=$args" --executor=ReactNetExecutor" - fi - - # The RN application selects pipe executor by default -fi - -# For net case, try and run executor; it is probably OK if this fails - it's -# just running elsewhere -if [[ "$executor" == "ReactNetExecutor" ]]; then - (node @CMAKE_BINARY_DIR@/bin/ubuntu-server.js 2>&1 > /dev/null) & -fi - -if [[ $on_device == 1 ]]; then - app_path="/home/phablet/@APP_NAME@" - -# Push binaries - adb push @CMAKE_BINARY_DIR@/bin/@APP_NAME@ "$app_path/@APP_NAME@" - [ -d "$plugins_path" ] && adb push "$plugins_path" "$app_path/plugins/" - [ -d "$asset_path" ] && adb push "$asset_path" "$app_path/share/" -# adb reverse --no-rebind tcp:8081 tcp:808 - -# Run app on device - adb shell "cd $app_path && REACT_SERVER_HOST=$react_host ./@APP_NAME@ --host $react_host $args -- --desktop_file_hint=/usr/share/applications/webbrowser-app.desktop" -else -# Run app locally - @CMAKE_BINARY_DIR@/bin/@APP_NAME@ $args -fi - diff --git a/desktop_files/.re-natal b/desktop_files/.re-natal deleted file mode 100644 index f63ff5865d4..00000000000 --- a/desktop_files/.re-natal +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "StatusIm", - "interface": "reagent", - "platforms": { - "ios": { - "host": "localhost", - "modules": [ - "react-native-image-resizer", - "react-native-camera", - "instabug-reactnative", - "nfc-react-native", - "react-native-background-timer", - "react-native-testfairy" - ] - }, - "android": { - "host": "10.0.2.2", - "modules": [ - "react-native-image-resizer", - "react-native-camera", - "instabug-reactnative", - "nfc-react-native", - "react-native-background-timer", - "react-native-testfairy" - ] - }, - "desktop": { - "host": "localhost", - "modules": [] - } - }, - "modules": [ - "realm", - "react-native-i18n", - "realm/react-native", - "dismissKeyboard", - "react-native-splash-screen", - "react-native-status", - "react-native-qrcode", - "identicon.js", - "react-native-fs", - "react-native-dialogs", - "react-native-image-crop-picker", - "react-native-securerandom", - "react-native-webview-bridge", - "react-native-fcm", - "homoglyph-finder", - "web3", - "chance", - "react-native-http-bridge", - "emojilib", - "react-native-config", - "react-native-svg", - "react-native-keychain", - "rn-snoopy", - "rn-snoopy/stream/bars", - "rn-snoopy/stream/filter", - "rn-snoopy/stream/buffer", - "react-native/Libraries/vendor/emitter/EventEmitter", - "react-native-fetch-polyfill" - ], - "imageDirs": [ - "resources/images", - "resources/icons" - ], - "envRoots": { - "dev": "env/dev", - "prod": "env/prod" - } -} diff --git a/desktop_files/VERSION b/desktop_files/VERSION deleted file mode 100644 index 8acdd82b765..00000000000 --- a/desktop_files/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.1 diff --git a/desktop_files/package-lock.json b/desktop_files/package-lock.json deleted file mode 100644 index b192331e88f..00000000000 --- a/desktop_files/package-lock.json +++ /dev/null @@ -1,12602 +0,0 @@ -{ - "name": "StatusIm", - "version": "0.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", - "integrity": "sha1-mA0VYLhjV1v1o3eSUDfgEy71kh4=", - "requires": { - "@babel/highlight": "7.0.0-beta.53" - } - }, - "@babel/core": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.0.0-beta.53.tgz", - "integrity": "sha1-q2R8+7JyQf0i7DyhNC161Oa1T58=", - "requires": { - "@babel/code-frame": "7.0.0-beta.53", - "@babel/generator": "7.0.0-beta.53", - "@babel/helpers": "7.0.0-beta.53", - "@babel/parser": "7.0.0-beta.53", - "@babel/template": "7.0.0-beta.53", - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53", - "convert-source-map": "1.5.1", - "debug": "3.1.0", - "json5": "0.5.1", - "lodash": "4.17.10", - "micromatch": "2.3.11", - "resolve": "1.8.1", - "semver": "5.5.0", - "source-map": "0.5.7" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "@babel/generator": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.53.tgz", - "integrity": "sha1-uMrXLFcr4yNK/94ivm2sxCUOA0s=", - "requires": { - "@babel/types": "7.0.0-beta.53", - "jsesc": "2.5.1", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=" - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0-beta.53.tgz", - "integrity": "sha1-WZYGKDdcvu+WoH7f4co4t1bwGqg=", - "requires": { - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.0.0-beta.53.tgz", - "integrity": "sha1-RFZwliPX2vqivulPglUD9MDs6Fs=", - "requires": { - "@babel/helper-explode-assignable-expression": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0-beta.53.tgz", - "integrity": "sha1-e9fn419EOf03NfAC5hIyGGv5zs8=", - "requires": { - "@babel/types": "7.0.0-beta.53", - "esutils": "2.0.2" - } - }, - "@babel/helper-call-delegate": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.0.0-beta.53.tgz", - "integrity": "sha1-ld6Lq9A/nmz08rVkoDhwjBOP/jE=", - "requires": { - "@babel/helper-hoist-variables": "7.0.0-beta.53", - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-define-map": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.0.0-beta.53.tgz", - "integrity": "sha1-SOniJlRTeHl1BD76qx7a0jnqlpU=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53", - "lodash": "4.17.10" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.0.0-beta.53.tgz", - "integrity": "sha1-1bytK2tH9ATAruillk3/2TEkc6g=", - "requires": { - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-function-name": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.53.tgz", - "integrity": "sha1-USgEro6cvOVDHr6hnkdijC7WU/I=", - "requires": { - "@babel/helper-get-function-arity": "7.0.0-beta.53", - "@babel/template": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.53.tgz", - "integrity": "sha1-3tiKsp+bHbYch9G7jTijXdp3neY=", - "requires": { - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0-beta.53.tgz", - "integrity": "sha1-TCfjuHP6CcWtbpPrQHBMIA+EE3w=", - "requires": { - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0-beta.53.tgz", - "integrity": "sha1-D7Dviy07kD0cO/Qm2kp0V14BnOQ=", - "requires": { - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-module-imports": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.53.tgz", - "integrity": "sha1-5zXmqjClBLD52Fw4ptRwqfSqgdk=", - "requires": { - "@babel/types": "7.0.0-beta.53", - "lodash": "4.17.10" - } - }, - "@babel/helper-module-transforms": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.0.0-beta.53.tgz", - "integrity": "sha1-e6IUzcyPhiPy0Xl96v8f80mqzhM=", - "requires": { - "@babel/helper-module-imports": "7.0.0-beta.53", - "@babel/helper-simple-access": "7.0.0-beta.53", - "@babel/helper-split-export-declaration": "7.0.0-beta.53", - "@babel/template": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53", - "lodash": "4.17.10" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0-beta.53.tgz", - "integrity": "sha1-j8eO9MD2n4uzu980zSMsIBIEFMg=", - "requires": { - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", - "integrity": "sha1-1kRYY2/8JYtCcUqd2Trrb4uM8+0=" - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.0.0-beta.53.tgz", - "integrity": "sha1-uDSnVy3sF2OJ/6x+djV5WGSQySI=", - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.53", - "@babel/helper-wrap-function": "7.0.0-beta.53", - "@babel/template": "7.0.0-beta.53", - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-replace-supers": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.0.0-beta.53.tgz", - "integrity": "sha1-M5tb3BAilElbGifFWBMjBuG3vKc=", - "requires": { - "@babel/helper-member-expression-to-functions": "7.0.0-beta.53", - "@babel/helper-optimise-call-expression": "7.0.0-beta.53", - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-simple-access": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.0.0-beta.53.tgz", - "integrity": "sha1-cvbbmr5C+GgfpvAo79WdgVRHUrM=", - "requires": { - "@babel/template": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53", - "lodash": "4.17.10" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.53.tgz", - "integrity": "sha1-rvVLix+ZYW6jfJhHhxajeAJjMls=", - "requires": { - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helper-wrap-function": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.0.0-beta.53.tgz", - "integrity": "sha1-q/sr+pQBBCurJXwBkPWtbbjfFdU=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.53", - "@babel/template": "7.0.0-beta.53", - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/helpers": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.0.0-beta.53.tgz", - "integrity": "sha1-xDb/uOCTAU2olba7d5f/RuPRzM8=", - "requires": { - "@babel/template": "7.0.0-beta.53", - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53" - } - }, - "@babel/highlight": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.53.tgz", - "integrity": "sha1-9OlS2tF4fSBeGI0+OEzc5JyjaPs=", - "requires": { - "chalk": "2.4.1", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "1.9.2" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", - "integrity": "sha1-H0XrYXv5Rj1IKywE00nZ5O2/SJI=" - }, - "@babel/plugin-external-helpers": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-external-helpers/-/plugin-external-helpers-7.0.0-beta.53.tgz", - "integrity": "sha1-WMMIYn4cBXN34ehxpqJ904CNLbc=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.0.0-beta.53.tgz", - "integrity": "sha1-Jou4wNUKjqkeXIHgOtDNPMnNyVc=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.53", - "@babel/helper-member-expression-to-functions": "7.0.0-beta.53", - "@babel/helper-optimise-call-expression": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "@babel/helper-replace-supers": "7.0.0-beta.53", - "@babel/plugin-syntax-class-properties": "7.0.0-beta.53" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0-beta.53.tgz", - "integrity": "sha1-5rXwusUBg48W6PPG00sAs+pANdk=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "@babel/plugin-syntax-object-rest-spread": "7.0.0-beta.53" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.0.0-beta.53.tgz", - "integrity": "sha1-DGc/Om5RkvyAJ7ws9Nv69w37/3I=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.0.0-beta.53.tgz", - "integrity": "sha1-s+lA+H7oeq8UORsXHGNHb9Aa8yM=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-syntax-flow": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.0.0-beta.53.tgz", - "integrity": "sha1-bk7ovVeyDpVCX/7ZlYhJZ9VuKKo=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.0.0-beta.53.tgz", - "integrity": "sha1-IjzIl3IzOcsiCqghbGVQhv60U5g=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0-beta.53.tgz", - "integrity": "sha1-nb12jD8QnwKyT7oXNllp+iXrRYw=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0-beta.53.tgz", - "integrity": "sha1-p19fqEl6rBcp0DO/QcJQQWudHgQ=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.0.0-beta.53.tgz", - "integrity": "sha1-nv1uUMofo5jcqnEZYh2j8fu4IbY=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "lodash": "4.17.10" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.0.0-beta.53.tgz", - "integrity": "sha1-XcLsMb8emAZqzfDEiHt3RMFL7G4=", - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.53", - "@babel/helper-define-map": "7.0.0-beta.53", - "@babel/helper-function-name": "7.0.0-beta.53", - "@babel/helper-optimise-call-expression": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "@babel/helper-replace-supers": "7.0.0-beta.53", - "@babel/helper-split-export-declaration": "7.0.0-beta.53", - "globals": "11.7.0" - }, - "dependencies": { - "globals": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==" - } - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0-beta.53.tgz", - "integrity": "sha1-l0fiYIKulO2lMPmNLCBZ6NLbwAU=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.0.0-beta.53.tgz", - "integrity": "sha1-DwrbDhptzTWjZkEBYJ7AYv8SenY=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.0.0-beta.53.tgz", - "integrity": "sha1-PiZxeSBMd1GdhBepsZnyUiIejZU=", - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-flow-strip-types": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.0.0-beta.53.tgz", - "integrity": "sha1-1n+ltPeM/B+aRFVrBiOiL7LnvGM=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "@babel/plugin-syntax-flow": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0-beta.53.tgz", - "integrity": "sha1-+gZSFeGFacj3TdUktXIeEdzKlzs=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.0.0-beta.53.tgz", - "integrity": "sha1-Kzpbs2TB4cV+zL/iXGv1XygEET4=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0-beta.53.tgz", - "integrity": "sha1-vsTxROmpbvUSHRQwx+vl/QiGV8k=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.0.0-beta.53.tgz", - "integrity": "sha1-68P7ocWmyHQ7kJQD7NPn42gcr6U=", - "requires": { - "@babel/helper-module-transforms": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "@babel/helper-simple-access": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-object-assign": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.0.0-beta.53.tgz", - "integrity": "sha1-V0gAnshfrgVClAQFSkt4ujrxZwc=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.0.0-beta.53.tgz", - "integrity": "sha1-7+YM7IzsoNGdXG+hrnm8TjMnnVY=", - "requires": { - "@babel/helper-call-delegate": "7.0.0-beta.53", - "@babel/helper-get-function-arity": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.0.0-beta.53.tgz", - "integrity": "sha1-HOBO/1fsC/atM+9obPUStj2TxL4=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.0.0-beta.53.tgz", - "integrity": "sha1-ZYC3v2Zl8UyFgrn8JmygHwDgoEc=", - "requires": { - "@babel/helper-builder-react-jsx": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "@babel/plugin-syntax-jsx": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.0.0-beta.53.tgz", - "integrity": "sha1-1XYFUD3AcVTnuj5pPYr/ry1P8DY=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53", - "@babel/plugin-syntax-jsx": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0-beta.53.tgz", - "integrity": "sha1-T+u/YISvoMHJ7ISX3mjAaV/p2gs=", - "requires": { - "regenerator-transform": "0.13.3" - }, - "dependencies": { - "regenerator-transform": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz", - "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==", - "requires": { - "private": "0.1.8" - } - } - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0-beta.53.tgz", - "integrity": "sha1-38SIG2vXZYoAMew7gWPliPCJjUs=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0-beta.53.tgz", - "integrity": "sha1-g+j2Rsok8cmCKPnxREz2DL1JOLw=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0-beta.53.tgz", - "integrity": "sha1-+msLQXEA0j4tsUwd9HorGzl48dk=", - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.53", - "@babel/helper-plugin-utils": "7.0.0-beta.53" - } - }, - "@babel/register": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.0.0-beta.53.tgz", - "integrity": "sha1-o8Yr5SVrnOC5QLajTXk+i8l7dlQ=", - "requires": { - "core-js": "2.5.7", - "find-cache-dir": "1.0.0", - "home-or-tmp": "3.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "pirates": "3.0.2", - "source-map-support": "0.4.18" - }, - "dependencies": { - "home-or-tmp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-3.0.0.tgz", - "integrity": "sha1-V6j+JM8zzdUkhgoVgh3cJchmcfs=" - } - } - }, - "@babel/template": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.53.tgz", - "integrity": "sha1-MyIpCQDQsYewpxdDgeHzu3EFDS4=", - "requires": { - "@babel/code-frame": "7.0.0-beta.53", - "@babel/parser": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53", - "lodash": "4.17.10" - } - }, - "@babel/traverse": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.53.tgz", - "integrity": "sha1-ANMs2NC1j0wB0xFXvmIsZigm00Q=", - "requires": { - "@babel/code-frame": "7.0.0-beta.53", - "@babel/generator": "7.0.0-beta.53", - "@babel/helper-function-name": "7.0.0-beta.53", - "@babel/helper-split-export-declaration": "7.0.0-beta.53", - "@babel/parser": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53", - "debug": "3.1.0", - "globals": "11.7.0", - "invariant": "2.2.4", - "lodash": "4.17.10" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==" - } - } - }, - "@babel/types": { - "version": "7.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", - "integrity": "sha1-GaRhwNpRVZXftnQLS0Xce7Dms3U=", - "requires": { - "esutils": "2.0.2", - "lodash": "4.17.10", - "to-fast-properties": "2.0.0" - }, - "dependencies": { - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - } - } - }, - "Base64": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", - "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "absolute-path": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/absolute-path/-/absolute-path-0.0.0.tgz", - "integrity": "sha1-p4di+9rftSl76ZsV01p4Wy8JW/c=" - }, - "abstract-leveldown": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.1.tgz", - "integrity": "sha1-+QFKVmm3RkGOFFFo3qSaBErhWQA=", - "requires": { - "xtend": "4.0.1" - } - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "2.1.18", - "negotiator": "0.6.1" - }, - "dependencies": { - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "1.33.0" - } - } - } - }, - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "requires": { - "acorn": "3.3.0" - } - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - }, - "dependencies": { - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - } - } - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "ansi": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz", - "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=" - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "2.1.1" - } - }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" - }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "3.1.10", - "normalize-path": "2.1.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - } - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "arch": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", - "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" - }, - "archive-type": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-3.2.0.tgz", - "integrity": "sha1-nNnABpV+vpX62tW9YJiUKoE3N/Y=", - "requires": { - "file-type": "3.9.0" - } - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "argsarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/argsarray/-/argsarray-0.0.1.tgz", - "integrity": "sha1-bnIHtOzbObCviDA/pa4ivajfYcs=" - }, - "aria-query": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", - "integrity": "sha1-Jsu1r/ZBRLCoJb4YRuCxbPoAsR4=", - "requires": { - "ast-types-flow": "0.0.7", - "commander": "2.13.0" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "requires": { - "typical": "2.6.1" - } - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" - }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" - }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.12.0" - } - }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=" - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=" - }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=" - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "1.0.3" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "art": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/art/-/art-0.10.2.tgz", - "integrity": "sha512-0F3cb+pWScVwrbAi3b/GINGTZ4DKMcaKqzBIt57whlpkgCiJXA0vXR9fdlcvCnA/UzWJYSAFEslRXZQDypiW6A==" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "requires": { - "bn.js": "4.11.8", - "inherits": "2.0.1", - "minimalistic-assert": "1.0.1" - } - }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "requires": { - "util": "0.10.3" - } - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "ast-query": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ast-query/-/ast-query-2.0.0.tgz", - "integrity": "sha1-NYjnmtjeB85Q3x54HMK9of1ppFM=", - "requires": { - "acorn-jsx": "3.0.1", - "class-extend": "0.1.2", - "escodegen-wallaby": "1.6.18", - "lodash": "4.17.10", - "traverse": "0.6.6" - } - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "4.17.10" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "asyncstorage-down": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/asyncstorage-down/-/asyncstorage-down-4.0.1.tgz", - "integrity": "sha1-OAtISrYpqths1rcj4E8IwForAGc=", - "requires": { - "abstract-leveldown": "2.6.1", - "argsarray": "0.0.1", - "d64": "1.0.0", - "ltgt": "2.2.1", - "tiny-queue": "0.2.0" - } - }, - "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "axobject-query": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", - "integrity": "sha1-YvWdvFnJ+SQnWco0mWDnov48NsA=", - "requires": { - "ast-types-flow": "0.0.7" - } - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "babel-core": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz", - "integrity": "sha1-jEKFZNzh4fQfszfsNPTDsCK1rYM=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.24.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - }, - "dependencies": { - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - } - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - } - } - } - } - }, - "babel-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz", - "integrity": "sha1-5xX0hsWN7SVknYiJRNUqoHxdlJc=", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "requires": { - "babel-helper-explode-assignable-expression": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.18.0.tgz", - "integrity": "sha1-qwLxmi63rOk23Yf6VYltAr5Zv3E=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.10" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "requires": { - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "requires": { - "babel-helper-optimise-call-expression": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-external-helpers": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz", - "integrity": "sha1-IoX0iwK9Xe3oUXXK+MYuhq3M76E=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-react-transform": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-react-transform/-/babel-plugin-react-transform-3.0.0.tgz", - "integrity": "sha512-4vJGddwPiHAOgshzZdGwYy4zRjjIr5SMY7gkOaCyIASjgpcsyLTlZNuB5rHOFoaTvGlhfo8/g4pobXPyHqm/3w==", - "requires": { - "lodash": "4.17.10" - } - }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=" - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=" - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=" - }, - "babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=" - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.16.0.tgz", - "integrity": "sha1-Gew2yxSGtZ+fRorfpCzhOQjKKZk=", - "requires": { - "babel-helper-remap-async-to-generator": "6.24.1", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.15.0.tgz", - "integrity": "sha1-W0Q8oUK+jR22qMKuQvUZWLZrcPY=", - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "requires": { - "babel-helper-define-map": "6.26.0", - "babel-helper-function-name": "6.24.1", - "babel-helper-optimise-call-expression": "6.24.1", - "babel-helper-replace-supers": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", - "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", - "requires": { - "babel-plugin-transform-strict-mode": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "requires": { - "babel-helper-replace-supers": "6.24.1", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "requires": { - "babel-helper-call-delegate": "6.24.1", - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "regexpu-core": "2.0.0" - } - }, - "babel-plugin-transform-es3-member-expression-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz", - "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es3-property-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.22.0.tgz", - "integrity": "sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", - "babel-plugin-syntax-exponentiation-operator": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "requires": { - "babel-plugin-syntax-flow": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-object-assign": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-assign/-/babel-plugin-transform-object-assign-6.22.0.tgz", - "integrity": "sha1-+Z0vZvGgsNSY40bFNZaEdAyqILo=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "requires": { - "babel-helper-builder-react-jsx": "6.26.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - }, - "dependencies": { - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "2.0.2" - } - } - } - }, - "babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "requires": { - "regenerator-transform": "0.10.1" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-preset-es2015-node": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-preset-es2015-node/-/babel-preset-es2015-node-6.1.1.tgz", - "integrity": "sha1-YLIxVwJLDP6/OmNVTLBe4DW05V8=", - "requires": { - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "6.24.1", - "babel-plugin-transform-es2015-unicode-regex": "6.24.1", - "semver": "5.5.0" - } - }, - "babel-preset-fbjs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz", - "integrity": "sha512-6XVQwlO26V5/0P9s2Eje8Epqkv/ihaMJ798+W98ktOA8fCn2IFM6wEi7CDW3fTbKFZ/8fDGvGZH01B6GSuNiWA==", - "requires": { - "babel-plugin-check-es2015-constants": "6.22.0", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-plugin-syntax-flow": "6.18.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-es2015-arrow-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoping": "6.15.0", - "babel-plugin-transform-es2015-classes": "6.24.1", - "babel-plugin-transform-es2015-computed-properties": "6.24.1", - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-for-of": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-literals": "6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-plugin-transform-es2015-object-super": "6.24.1", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-template-literals": "6.22.0", - "babel-plugin-transform-es3-member-expression-literals": "6.22.0", - "babel-plugin-transform-es3-property-literals": "6.22.0", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-plugin-transform-react-display-name": "6.25.0", - "babel-plugin-transform-react-jsx": "6.24.1" - } - }, - "babel-preset-react-native": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/babel-preset-react-native/-/babel-preset-react-native-4.0.0.tgz", - "integrity": "sha512-Wfbo6x244nUbBxjr7hQaNFdjj7FDYU+TVT7cFVPEdVPI68vhN52iLvamm+ErhNdHq6M4j1cMT6AJBYx7Wzdr0g==", - "requires": { - "babel-plugin-check-es2015-constants": "6.22.0", - "babel-plugin-react-transform": "3.0.0", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-plugin-syntax-dynamic-import": "6.18.0", - "babel-plugin-syntax-flow": "6.18.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-es2015-arrow-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoping": "6.15.0", - "babel-plugin-transform-es2015-classes": "6.24.1", - "babel-plugin-transform-es2015-computed-properties": "6.24.1", - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-for-of": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-literals": "6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-template-literals": "6.22.0", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-plugin-transform-object-assign": "6.22.0", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-plugin-transform-react-display-name": "6.25.0", - "babel-plugin-transform-react-jsx": "6.24.1", - "babel-plugin-transform-react-jsx-source": "6.22.0", - "babel-plugin-transform-regenerator": "6.26.0", - "babel-template": "6.26.0", - "react-transform-hmr": "1.0.4" - } - }, - "babel-register": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.18.0.tgz", - "integrity": "sha1-iS4uA4ZQeN2QrSxxURHsREmzKmg=", - "requires": { - "babel-core": "6.24.1", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.7", - "regenerator-runtime": "0.11.1" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.10" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.4", - "lodash": "4.17.10" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.10", - "to-fast-properties": "1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "pascalcase": "0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" - }, - "base64-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" - }, - "basic-auth": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", - "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", - "requires": { - "safe-buffer": "5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=" - }, - "big-integer": { - "version": "1.6.32", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.32.tgz", - "integrity": "sha512-ljKJdR3wk9thHfLj4DtrNiOSTxvGFaMjWrG4pW75juXC4j7+XuKJVFdg4kgFMYp85PVkO05dFMj2dk2xVsH4xw==" - }, - "bignumber.js": { - "version": "github:status-im/bignumber.js#cc066a0a3d6bfe0c436c9957f4ea8344bf963c89" - }, - "binaryextensions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", - "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=" - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "2.3.6", - "safe-buffer": "5.1.2" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "2.0.1" - } - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.16.3" - } - }, - "bower-config": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bower-config/-/bower-config-1.4.1.tgz", - "integrity": "sha1-hf2d82fCuNu9DKpMXyutQM2Ewsw=", - "requires": { - "graceful-fs": "4.1.11", - "mout": "1.1.0", - "optimist": "0.6.1", - "osenv": "0.1.5", - "untildify": "2.1.0" - } - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "2.0.0", - "camelcase": "4.1.0", - "chalk": "2.4.1", - "cli-boxes": "1.0.0", - "string-width": "2.1.1", - "term-size": "1.2.0", - "widest-line": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "1.9.2" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "bplist-creator": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", - "integrity": "sha1-N98VNgkoJLh8QvlXsBNEEXNyrkU=", - "requires": { - "stream-buffers": "2.2.0" - } - }, - "bplist-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.1.tgz", - "integrity": "sha1-1g1dzCDLptx+HymbNdPh+V2vuuY=", - "requires": { - "big-integer": "1.6.32" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "1.0.3", - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "inherits": "2.0.1", - "safe-buffer": "5.1.2" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "1.2.0", - "browserify-des": "1.0.2", - "evp_bytestokey": "1.0.3" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "requires": { - "cipher-base": "1.0.4", - "des.js": "1.0.0", - "inherits": "2.0.1", - "safe-buffer": "5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "requires": { - "bn.js": "4.11.8", - "randombytes": "2.0.6" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "elliptic": "6.4.0", - "inherits": "2.0.1", - "parse-asn1": "5.1.1" - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "requires": { - "node-int64": "0.4.0" - } - }, - "buffer": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", - "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", - "requires": { - "base64-js": "0.0.8", - "ieee754": "1.1.12", - "isarray": "1.0.0" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "1.1.0", - "buffer-fill": "1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-from": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", - "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" - }, - "buffer-to-vinyl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-to-vinyl/-/buffer-to-vinyl-1.1.0.tgz", - "integrity": "sha1-APFfruOreh3aLN5tkSG//dB7ImI=", - "requires": { - "file-type": "3.9.0", - "readable-stream": "2.3.6", - "uuid": "2.0.3", - "vinyl": "1.2.0" - }, - "dependencies": { - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" - } - } - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.2.1", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.0", - "to-object-path": "0.3.0", - "union-value": "1.0.0", - "unset-value": "1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - } - } - }, - "capture-exit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", - "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", - "requires": { - "rsvp": "3.6.2" - } - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "caw": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/caw/-/caw-1.2.0.tgz", - "integrity": "sha1-/7Im/n78VHKI3GLuPpcHPCEtEDQ=", - "requires": { - "get-proxy": "1.1.0", - "is-obj": "1.0.1", - "object-assign": "3.0.0", - "tunnel-agent": "0.4.3" - }, - "dependencies": { - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" - } - } - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "optional": true, - "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "chance": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.0.12.tgz", - "integrity": "sha512-W99uMuboG5CT1iToDmizEH6yQYqICzZnrSRbbXPuJErzFWLPaoiEDvwnKbESjDo/8st1n3pyh70VBMmfqPmf+Q==" - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" - }, - "check-dependencies": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/check-dependencies/-/check-dependencies-1.1.0.tgz", - "integrity": "sha512-GDrbGzzJ6Gc6tQh87HBMGhrJ4UWIlR9MKJwgvlrJyj/gWvTYYb2jQetKbajt/EYK5Y8/4g7gH2LEvq8GdUWTag==", - "requires": { - "bower-config": "1.4.1", - "chalk": "2.4.1", - "findup-sync": "2.0.0", - "lodash.camelcase": "4.3.0", - "minimist": "1.2.0", - "semver": "5.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "1.9.2" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "cheerio": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", - "integrity": "sha1-dy5wFfLuKZZQltcepBdbdas1SSU=", - "requires": { - "css-select": "1.0.0", - "dom-serializer": "0.1.0", - "entities": "1.1.1", - "htmlparser2": "3.8.3", - "lodash": "3.10.1" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, - "ci-info": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", - "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "2.0.1", - "safe-buffer": "5.1.2" - } - }, - "class-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/class-extend/-/class-extend-0.1.2.tgz", - "integrity": "sha1-gFeoKwD1P4Kl1ixQ74z/3sb6vDQ=", - "requires": { - "object-assign": "2.1.1" - }, - "dependencies": { - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" - } - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "2.0.0" - } - }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "requires": { - "colors": "1.0.3" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "clipboardy": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.3.tgz", - "integrity": "sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA==", - "requires": { - "arch": "2.1.1", - "execa": "0.8.0" - }, - "dependencies": { - "execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - } - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "clone": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.1.19.tgz", - "integrity": "sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU=" - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" - }, - "co": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", - "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "coffee-script": { - "version": "1.12.7", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", - "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" - } - }, - "color": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz", - "integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==", - "requires": { - "color-convert": "1.9.2", - "color-string": "1.5.2" - } - }, - "color-convert": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", - "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "requires": { - "color-name": "1.1.1" - } - }, - "color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" - }, - "color-string": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.2.tgz", - "integrity": "sha1-JuRYFLw8mny9Z1FkikFDRRSnc6k=", - "requires": { - "color-name": "1.1.1", - "simple-swizzle": "0.2.2" - } - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" - }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "command-line-args": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.7.tgz", - "integrity": "sha512-aUdPvQRAyBvQd2n7jXcsMDz68ckBJELXNzBybCHOibUWEg0mWTnaYCSRU8h9R+aNRSvDihJtssSRCiDRpLaezA==", - "requires": { - "array-back": "2.0.0", - "find-replace": "1.0.3", - "typical": "2.6.1" - } - }, - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "compressible": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", - "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", - "requires": { - "mime-db": "1.34.0" - }, - "dependencies": { - "mime-db": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.34.0.tgz", - "integrity": "sha1-RS0Oz/XDA0am3B5kseruDTcZ/5o=" - } - } - }, - "compression": { - "version": "1.7.2", - "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", - "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", - "requires": { - "accepts": "1.3.5", - "bytes": "3.0.0", - "compressible": "2.0.14", - "debug": "2.6.9", - "on-headers": "1.0.1", - "safe-buffer": "5.1.1", - "vary": "1.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "1.1.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "4.2.0", - "graceful-fs": "4.1.11", - "make-dir": "1.3.0", - "unique-string": "1.0.0", - "write-file-atomic": "2.3.0", - "xdg-basedir": "3.0.0" - }, - "dependencies": { - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "signal-exit": "3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - } - } - }, - "connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "1.3.2", - "utils-merge": "1.0.1" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "requires": { - "bn.js": "4.11.8", - "elliptic": "6.4.0" - } - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "1.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.1", - "md5.js": "1.3.4", - "ripemd160": "2.0.2", - "sha.js": "2.4.11" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "inherits": "2.0.1", - "ripemd160": "2.0.2", - "safe-buffer": "5.1.2", - "sha.js": "2.4.11" - } - }, - "create-react-class": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", - "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.4.0", - "object-assign": "4.1.1" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "4.1.3", - "shebang-command": "1.2.0", - "which": "1.3.1" - } - }, - "cross-spawn-async": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz", - "integrity": "sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=", - "requires": { - "lru-cache": "4.1.3", - "which": "1.3.1" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.10.1" - } - }, - "crypto-js": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz", - "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU=" - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "css-select": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", - "integrity": "sha1-sRIcpRhI3SZOIkTQWM7iVN7rRLA=", - "requires": { - "boolbase": "1.0.0", - "css-what": "1.0.0", - "domutils": "1.4.3", - "nth-check": "1.0.1" - } - }, - "css-what": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", - "integrity": "sha1-18wt9FGAZm+Z0rFEYmOUaeAPc2w=" - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "requires": { - "array-find-index": "1.0.2" - } - }, - "d64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz", - "integrity": "sha1-QAKofoUMv8n52XBrYPymE6MzbpA=" - }, - "damerau-levenshtein": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", - "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=" - }, - "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "dateformat": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", - "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-3.0.0.tgz", - "integrity": "sha1-rx3VDQbjv8QyRh033hGzjA2ZG+0=", - "requires": { - "buffer-to-vinyl": "1.1.0", - "concat-stream": "1.6.2", - "decompress-tar": "3.1.0", - "decompress-tarbz2": "3.1.0", - "decompress-targz": "3.1.0", - "decompress-unzip": "3.4.0", - "stream-combiner2": "1.1.1", - "vinyl-assign": "1.2.1", - "vinyl-fs": "2.4.4" - } - }, - "decompress-tar": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-3.1.0.tgz", - "integrity": "sha1-IXx4n5uURQ76rcXF5TeXj8MzxGY=", - "requires": { - "is-tar": "1.0.0", - "object-assign": "2.1.1", - "strip-dirs": "1.1.1", - "tar-stream": "1.6.1", - "through2": "0.6.5", - "vinyl": "0.4.6" - }, - "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "requires": { - "clone": "0.2.0", - "clone-stats": "0.0.1" - } - } - } - }, - "decompress-tarbz2": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-3.1.0.tgz", - "integrity": "sha1-iyOTVoE1X58YnYclag+L3ZbZZm0=", - "requires": { - "is-bzip2": "1.0.0", - "object-assign": "2.1.1", - "seek-bzip": "1.0.5", - "strip-dirs": "1.1.1", - "tar-stream": "1.6.1", - "through2": "0.6.5", - "vinyl": "0.4.6" - }, - "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "requires": { - "clone": "0.2.0", - "clone-stats": "0.0.1" - } - } - } - }, - "decompress-targz": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-3.1.0.tgz", - "integrity": "sha1-ssE9+YFmJomRtxXWRH9kLpaW9aA=", - "requires": { - "is-gzip": "1.0.0", - "object-assign": "2.1.1", - "strip-dirs": "1.1.1", - "tar-stream": "1.6.1", - "through2": "0.6.5", - "vinyl": "0.4.6" - }, - "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "requires": { - "clone": "0.2.0", - "clone-stats": "0.0.1" - } - } - } - }, - "decompress-tarxz": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarxz/-/decompress-tarxz-2.1.1.tgz", - "integrity": "sha512-U7LniyhLTpCc07lAmNXMX4NBW60ONkpGTwo6KxIDk9zk22GYh5NKjEI09F8I1oYkGr9WGOA6OH7scqLo7Xu5lA==", - "requires": { - "decompress-tar": "4.1.1", - "file-type": "3.9.0", - "is-stream": "1.1.0", - "lzma-native": "3.0.8" - }, - "dependencies": { - "decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "requires": { - "file-type": "5.2.0", - "is-stream": "1.1.0", - "tar-stream": "1.6.1" - }, - "dependencies": { - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=" - } - } - } - } - }, - "decompress-unzip": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-3.4.0.tgz", - "integrity": "sha1-YUdbQVIGa74/7hL51inRX+ZHjus=", - "requires": { - "is-zip": "1.0.0", - "read-all-stream": "3.1.0", - "stat-mode": "0.2.2", - "strip-dirs": "1.1.1", - "through2": "2.0.3", - "vinyl": "1.2.0", - "yauzl": "2.10.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==" - }, - "deferred-leveldown": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz", - "integrity": "sha1-LO8fER4cV4cNi7uK8mUOWHzS9bQ=", - "requires": { - "abstract-leveldown": "0.12.4" - }, - "dependencies": { - "abstract-leveldown": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz", - "integrity": "sha1-KeGOYy5g5OIh1YECR4UqY9ey5BA=", - "requires": { - "xtend": "3.0.0" - } - }, - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" - } - } - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.12" - }, - "dependencies": { - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" - } - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "requires": { - "inherits": "2.0.1", - "minimalistic-assert": "1.0.1" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-conflict": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/detect-conflict/-/detect-conflict-1.0.1.tgz", - "integrity": "sha1-CIZXpmqWHAUBnbfEIwiDsca0F24=" - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "requires": { - "repeating": "2.0.1" - } - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" - }, - "diff": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", - "integrity": "sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=" - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "4.11.8", - "miller-rabin": "4.0.1", - "randombytes": "2.0.6" - } - }, - "dns.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dns.js/-/dns.js-1.0.1.tgz", - "integrity": "sha1-v8CVDXl3dyjGdQCxxBgHXmU5Ve4=" - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" - } - } - }, - "dom-walk": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", - "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" - }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "requires": { - "domelementtype": "1.3.0" - } - }, - "domutils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", - "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", - "requires": { - "domelementtype": "1.3.0" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "1.0.1" - } - }, - "download": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/download/-/download-4.4.3.tgz", - "integrity": "sha1-qlX9rTktldS2jowr4D4MKqIbqaw=", - "requires": { - "caw": "1.2.0", - "concat-stream": "1.6.2", - "each-async": "1.1.1", - "filenamify": "1.2.1", - "got": "5.7.1", - "gulp-decompress": "1.2.0", - "gulp-rename": "1.3.0", - "is-url": "1.2.4", - "object-assign": "4.1.1", - "read-all-stream": "3.1.0", - "readable-stream": "2.3.6", - "stream-combiner2": "1.1.1", - "vinyl": "1.2.0", - "vinyl-fs": "2.4.4", - "ware": "1.3.0" - } - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "2.3.6" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", - "requires": { - "end-of-stream": "1.4.1", - "inherits": "2.0.1", - "readable-stream": "2.3.6", - "stream-shift": "1.0.0" - } - }, - "each-async": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/each-async/-/each-async-1.1.1.tgz", - "integrity": "sha1-3uUim98KtrogEqOV4bhpq/iBNHM=", - "requires": { - "onetime": "1.1.0", - "set-immediate-shim": "1.0.1" - }, - "dependencies": { - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" - } - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "ejs": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", - "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" - }, - "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.5", - "hmac-drbg": "1.0.1", - "inherits": "2.0.1", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "emoji-regex": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", - "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==" - }, - "emojilib": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.2.9.tgz", - "integrity": "sha512-RbYMz3G8OFBZYsqHQp8kiM/Hsg/0bSRWG1iRPqvQ04s4RUQH+iBLFjybSM6fv2rV/vQrywOEL5m31BjnfyDUlw==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "0.4.23" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "1.4.0" - } - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" - }, - "envinfo": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-3.11.1.tgz", - "integrity": "sha512-hKkh7aKtont6Zuv4RmE4VkOc96TkBj9NXj7Ghsd/qCA9LuJI0Dh+ImwA1N5iORB9Vg+sz5bq9CHJzs51BILNCQ==", - "requires": { - "clipboardy": "1.2.3", - "glob": "7.1.2", - "minimist": "1.2.0", - "os-name": "2.0.1", - "which": "1.3.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "requires": { - "prr": "1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "0.2.1" - } - }, - "errorhandler": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.0.tgz", - "integrity": "sha1-6rpkyl1UKjEayUX1gt78M2Fl2fQ=", - "requires": { - "accepts": "1.3.5", - "escape-html": "1.0.3" - } - }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "is-callable": "1.1.4", - "is-regex": "1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "requires": { - "is-callable": "1.1.4", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen-wallaby": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/escodegen-wallaby/-/escodegen-wallaby-1.6.18.tgz", - "integrity": "sha512-3UvR14JRNh8VfKJixTDHWmhPNKAJiVZS807KUjECBk6f05WMe8ZeWL1gbrswNYhDiAUeDBQccyTWR91fayx3og==", - "requires": { - "esprima": "2.7.3", - "estraverse": "1.9.3", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.2.0" - }, - "dependencies": { - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "optional": true, - "requires": { - "amdefine": "1.0.1" - } - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.1.tgz", - "integrity": "sha512-5I9SpoP7gT4wBFOtXT8/tXNPYohHBVfyVfO17vkbC7r9kEIxYJF12D3pKqhk8+xnk12rfxKClS3WCFpVckFTPQ==", - "requires": { - "aria-query": "0.7.1", - "array-includes": "3.0.3", - "ast-types-flow": "0.0.7", - "axobject-query": "0.1.0", - "damerau-levenshtein": "1.0.4", - "emoji-regex": "6.5.1", - "jsx-ast-utils": "1.4.1" - } - }, - "eslint-plugin-react-native": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-3.2.1.tgz", - "integrity": "sha512-1AnJO3JUCAoLpyOEsWCwN9hPJ0aQ72OT+JvLMuHjEWYb6QWxiNOszp24CEwegMzbREtJKI9OoRqYYDYxMxmjgQ==", - "requires": { - "eslint-plugin-react-native-globals": "0.1.2" - } - }, - "eslint-plugin-react-native-globals": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", - "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==" - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "event-target-shim": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz", - "integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE=" - }, - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "1.3.4", - "safe-buffer": "5.1.2" - } - }, - "exec-sh": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", - "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", - "requires": { - "merge": "1.2.0" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=" - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "requires": { - "fill-range": "2.2.4" - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "requires": { - "homedir-polyfill": "1.0.1" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "requires": { - "chardet": "0.4.2", - "iconv-lite": "0.4.23", - "tmp": "0.0.33" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "requires": { - "is-extglob": "1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fancy-log": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", - "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", - "requires": { - "ansi-gray": "0.1.1", - "color-support": "1.1.3", - "time-stamp": "1.1.0" - } - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "requires": { - "bser": "2.0.0" - } - }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "requires": { - "core-js": "1.2.7", - "isomorphic-fetch": "2.2.1", - "loose-envify": "1.4.0", - "object-assign": "4.1.1", - "promise": "7.3.1", - "setimmediate": "1.0.5", - "ua-parser-js": "0.7.18" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - } - } - }, - "fbjs-scripts": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fbjs-scripts/-/fbjs-scripts-0.8.3.tgz", - "integrity": "sha512-aUJ/uEzMIiBYuj/blLp4sVNkQQ7ZEB/lyplG1IzzOmZ83meiWecrGg5jBo4wWrxXmO4RExdtsSV1QkTjPt2Gag==", - "requires": { - "ansi-colors": "1.1.0", - "babel-core": "6.24.1", - "babel-preset-fbjs": "2.1.4", - "core-js": "2.5.7", - "cross-spawn": "5.1.0", - "fancy-log": "1.3.2", - "object-assign": "4.1.1", - "plugin-error": "0.1.2", - "semver": "5.5.0", - "through2": "2.0.3" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "requires": { - "pend": "1.2.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "1.0.5" - } - }, - "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=" - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" - }, - "filename-reserved-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", - "integrity": "sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q=" - }, - "filenamify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", - "integrity": "sha1-qfL/0RxQO+0wABUCknI3jx8TZaU=", - "requires": { - "filename-reserved-regex": "1.0.0", - "strip-outer": "1.0.1", - "trim-repeated": "1.0.0" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "3.0.0", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "requires": { - "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" - } - }, - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", - "requires": { - "commondir": "1.0.1", - "make-dir": "1.3.0", - "pkg-dir": "2.0.0" - } - }, - "find-replace": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", - "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=", - "requires": { - "array-back": "1.0.4", - "test-value": "2.1.0" - }, - "dependencies": { - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "requires": { - "typical": "2.6.1" - } - } - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "2.0.0" - } - }, - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "requires": { - "detect-file": "1.0.0", - "is-glob": "3.1.0", - "micromatch": "3.1.10", - "resolve-dir": "1.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - } - } - }, - "first-chunk-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", - "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", - "requires": { - "readable-stream": "2.3.6" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "requires": { - "for-in": "1.0.2" - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - }, - "dependencies": { - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "1.33.0" - } - } - } - }, - "formatio": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", - "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", - "requires": { - "samsam": "1.1.2" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0", - "klaw": "1.3.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "optional": true, - "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true - } - } - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.1", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.1", - "minimatch": "3.0.4" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "fwd-stream": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fwd-stream/-/fwd-stream-1.0.4.tgz", - "integrity": "sha1-7Sgcq+1G/uz5Ie4y3ExQs3KsfPo=", - "requires": { - "readable-stream": "1.0.34" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "gauge": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", - "integrity": "sha1-6c7FSD09TuDvRLYKfZnkk14TbZM=", - "requires": { - "ansi": "0.3.1", - "has-unicode": "2.0.1", - "lodash.pad": "4.5.1", - "lodash.padend": "4.6.1", - "lodash.padstart": "4.6.1" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "get-proxy": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-1.1.0.tgz", - "integrity": "sha1-iUhUSRvFkbDxR9euVw9cZ4tyVus=", - "requires": { - "rc": "1.2.8" - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "gh-got": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/gh-got/-/gh-got-2.4.0.tgz", - "integrity": "sha1-qlFBiRHKXk+SQ3EUzRIJODpKoBk=", - "requires": { - "got": "5.7.1", - "object-assign": "4.1.1", - "pinkie-promise": "2.0.1" - } - }, - "github-username": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/github-username/-/github-username-2.1.0.tgz", - "integrity": "sha1-IA5aEEr0K6CKVAlscI1LbsL6JWs=", - "requires": { - "gh-got": "2.4.0", - "meow": "3.7.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.1", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "requires": { - "is-glob": "2.0.1" - } - }, - "glob-stream": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", - "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", - "requires": { - "extend": "3.0.1", - "glob": "5.0.15", - "glob-parent": "3.1.0", - "micromatch": "2.3.11", - "ordered-read-streams": "0.3.0", - "through2": "0.6.5", - "to-absolute-glob": "0.1.1", - "unique-stream": "2.2.1" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.1", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "3.1.0", - "path-dirname": "1.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "2.1.1" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - } - } - }, - "global": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", - "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", - "requires": { - "min-document": "2.19.0", - "process": "0.5.2" - }, - "dependencies": { - "process": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" - } - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "1.3.5" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "requires": { - "global-prefix": "1.0.2", - "is-windows": "1.0.2", - "resolve-dir": "1.0.1" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "requires": { - "expand-tilde": "2.0.2", - "homedir-polyfill": "1.0.1", - "ini": "1.3.5", - "is-windows": "1.0.2", - "which": "1.3.1" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" - }, - "globby": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz", - "integrity": "sha1-CA9UVJ7BuCpsYOYx/ILhIR2+lfg=", - "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "6.0.4", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.1", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "glogg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", - "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", - "requires": { - "sparkles": "1.0.1" - } - }, - "got": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", - "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", - "requires": { - "create-error-class": "3.0.2", - "duplexer2": "0.1.4", - "is-redirect": "1.0.0", - "is-retry-allowed": "1.1.0", - "is-stream": "1.1.0", - "lowercase-keys": "1.0.1", - "node-status-codes": "1.0.0", - "object-assign": "4.1.1", - "parse-json": "2.2.0", - "pinkie-promise": "2.0.1", - "read-all-stream": "3.1.0", - "readable-stream": "2.3.6", - "timed-out": "3.1.3", - "unzip-response": "1.0.2", - "url-parse-lax": "1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, - "grouped-queue": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/grouped-queue/-/grouped-queue-0.3.3.tgz", - "integrity": "sha1-wWfSpTGcWg4JZO9qJbfC34mWyFw=", - "requires": { - "lodash": "4.17.10" - } - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" - }, - "gruntfile-editor": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/gruntfile-editor/-/gruntfile-editor-1.2.1.tgz", - "integrity": "sha1-Nm/B+Ty/BFgT4USK7x2p8YKJ1es=", - "requires": { - "ast-query": "2.0.0", - "lodash": "4.17.10" - } - }, - "gulp-decompress": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gulp-decompress/-/gulp-decompress-1.2.0.tgz", - "integrity": "sha1-jutlpeAV+O2FMsr+KEVJYGJvDcc=", - "requires": { - "archive-type": "3.2.0", - "decompress": "3.0.0", - "gulp-util": "3.0.8", - "readable-stream": "2.3.6" - } - }, - "gulp-rename": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.3.0.tgz", - "integrity": "sha512-nEuZB7/9i0IZ8AXORTizl2QLP9tcC9uWc/s329zElBLJw1CfOhmMXBxwVlCRKjDyrWuhVP0uBKl61KeQ32TiCg==" - }, - "gulp-sourcemaps": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", - "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", - "requires": { - "convert-source-map": "1.5.1", - "graceful-fs": "4.1.11", - "strip-bom": "2.0.0", - "through2": "2.0.3", - "vinyl": "1.2.0" - }, - "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "0.2.1" - } - } - } - }, - "gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "requires": { - "array-differ": "1.0.0", - "array-uniq": "1.0.3", - "beeper": "1.1.1", - "chalk": "1.1.3", - "dateformat": "2.2.0", - "fancy-log": "1.3.2", - "gulplog": "1.0.0", - "has-gulplog": "0.1.0", - "lodash._reescape": "3.0.0", - "lodash._reevaluate": "3.0.0", - "lodash._reinterpolate": "3.0.0", - "lodash.template": "3.6.2", - "minimist": "1.2.0", - "multipipe": "0.1.2", - "object-assign": "3.0.0", - "replace-ext": "0.0.1", - "through2": "2.0.3", - "vinyl": "0.5.3" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=" - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" - }, - "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", - "requires": { - "clone": "1.0.4", - "clone-stats": "0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "requires": { - "glogg": "1.0.1" - } - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "optional": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "optional": true, - "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - } - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": "1.0.1" - } - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "optional": true, - "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "optional": true - } - } - }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "optional": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "optional": true, - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } - } - } - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "requires": { - "sparkles": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "2.0.1", - "safe-buffer": "5.1.2" - } - }, - "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", - "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "1.1.5", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "homedir-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", - "requires": { - "parse-passwd": "1.0.0" - } - }, - "homoglyph-finder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/homoglyph-finder/-/homoglyph-finder-1.1.1.tgz", - "integrity": "sha1-U8NnGmGOADom+j+H3CsXnpVWj6g=" - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" - }, - "html-wiring": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/html-wiring/-/html-wiring-1.2.0.tgz", - "integrity": "sha1-xfkKd24KJyQdxt+QIsNxhtAnD54=", - "requires": { - "cheerio": "0.19.0", - "detect-newline": "1.0.3" - }, - "dependencies": { - "detect-newline": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-1.0.3.tgz", - "integrity": "sha1-6XsQA4d9cMCa8a81v63/Fo3kkg0=", - "requires": { - "get-stdin": "4.0.1", - "minimist": "1.2.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.3.0", - "domutils": "1.5.1", - "entities": "1.0.0", - "readable-stream": "1.1.14" - }, - "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "http-basic": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-2.5.1.tgz", - "integrity": "sha1-jORHvbW2xXf4pj4/p4BW7Eu02/s=", - "requires": { - "caseless": "0.11.0", - "concat-stream": "1.6.2", - "http-response-object": "1.1.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - } - } - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": "1.5.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - } - } - }, - "http-response-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-1.1.0.tgz", - "integrity": "sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM=" - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": "2.1.2" - } - }, - "identicon.js": { - "version": "github:status-im/identicon.js#4cd179daaf6f01384502b4b2c1876f982db0f57e" - }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" - }, - "image-size": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", - "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==" - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "requires": { - "repeating": "2.0.1" - } - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "requires": { - "ansi-escapes": "3.1.0", - "chalk": "2.4.1", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "2.2.0", - "figures": "2.0.0", - "lodash": "4.17.10", - "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rx-lite": "4.0.8", - "rx-lite-aggregates": "4.0.8", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "1.9.2" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "3.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "instabug-reactnative": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/instabug-reactnative/-/instabug-reactnative-2.12.0.tgz", - "integrity": "sha512-aD2JLofdXfZWvlcg3t3lEoUM+HN7qLaKry68Ypjuh+cFGCXtblbKz4ozEfEFBw4rvEm31s8BSd1e/aFiHPXBmQ==" - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "1.4.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "ip-regex": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", - "integrity": "sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=" - }, - "is": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/is/-/is-0.2.7.tgz", - "integrity": "sha1-OzSixI81mXLzUEKEkZOucmS2NWI=" - }, - "is-absolute": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz", - "integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=", - "requires": { - "is-relative": "0.1.3" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-bzip2": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-bzip2/-/is-bzip2-1.0.0.tgz", - "integrity": "sha1-XuWOqlounIDiFAe+3yOuWsCRs/w=" - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" - }, - "is-ci": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", - "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", - "dev": true, - "requires": { - "ci-info": "1.1.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "1.0.0" - } - }, - "is-gzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", - "integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=" - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "0.1.1", - "is-path-inside": "1.0.1" - } - }, - "is-natural-number": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-2.1.1.tgz", - "integrity": "sha1-fUxXKDd+84bD4ZSpkRv1fG3DNec=" - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "requires": { - "kind-of": "3.2.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-object": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-0.1.2.tgz", - "integrity": "sha1-AO+8CIFsM8/ErIJR0TLhDcZQmNc=" - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "1.0.2" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "requires": { - "has": "1.0.3" - } - }, - "is-relative": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz", - "integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=" - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" - }, - "is-tar": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-tar/-/is-tar-1.0.0.tgz", - "integrity": "sha1-L2suF5LB9bs2UZrKqdZcDSb+hT0=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-valid-glob": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", - "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "is-zip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-zip/-/is-zip-1.0.0.tgz", - "integrity": "sha1-R7Co/004p2QxzP2ZqOFaTIa6IyU=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "1.7.3", - "whatwg-fetch": "2.0.4" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istextorbinary": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", - "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", - "requires": { - "binaryextensions": "1.0.1", - "textextensions": "1.0.2" - } - }, - "jest-docblock": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-22.4.0.tgz", - "integrity": "sha512-lDY7GZ+/CJb02oULYLBDj7Hs5shBhVpDYpIm8LUyqw9X2J22QRsM19gmGQwIFqGSJmpc/LRrSYudeSrG510xlQ==", - "requires": { - "detect-newline": "2.1.0" - } - }, - "jest-haste-map": { - "version": "22.4.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-22.4.2.tgz", - "integrity": "sha512-EdQADHGXRqHJYAr7q9B9YYHZnrlcMwhx1+DnIgc9uN05nCW3RvGCxJ91MqWXcC1AzatLoSv7SNd0qXMp2jKBDA==", - "requires": { - "fb-watchman": "2.0.0", - "graceful-fs": "4.1.11", - "jest-docblock": "22.4.0", - "jest-serializer": "22.4.3", - "jest-worker": "22.2.2", - "micromatch": "2.3.11", - "sane": "2.5.2" - } - }, - "jest-serializer": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-22.4.3.tgz", - "integrity": "sha512-uPaUAppx4VUfJ0QDerpNdF43F68eqKWCzzhUlKNDsUPhjOon7ZehR4C809GCqh765FoMRtTVUVnGvIoskkYHiw==" - }, - "jest-worker": { - "version": "22.2.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-22.2.2.tgz", - "integrity": "sha512-ZylDXjrFNt/OP6cUxwJFWwDgazP7hRjtCQbocFHyiwov+04Wm1x5PYzMGNJT53s4nwr0oo9ocYTImS09xOlUnw==", - "requires": { - "merge-stream": "1.0.1" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "jsx-ast-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", - "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=" - }, - "keymirror": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz", - "integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU=" - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "klaw-sync": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-2.1.0.tgz", - "integrity": "sha1-PTvNhgDnv971MjHHOf8FOu1WDkQ=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "4.0.1" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "optional": true - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "2.3.6" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "1.0.0" - } - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" - }, - "level-blobs": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/level-blobs/-/level-blobs-0.1.7.tgz", - "integrity": "sha1-mrm5e7mfHtv594o0M+Ie1WOGva8=", - "requires": { - "level-peek": "1.0.6", - "once": "1.4.0", - "readable-stream": "1.1.14" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "level-filesystem": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/level-filesystem/-/level-filesystem-1.2.0.tgz", - "integrity": "sha1-oArKmRnEpN+v3KaoEI0iWq3/Y7M=", - "requires": { - "concat-stream": "1.6.2", - "errno": "0.1.7", - "fwd-stream": "1.0.4", - "level-blobs": "0.1.7", - "level-peek": "1.0.6", - "level-sublevel": "5.2.3", - "octal": "1.0.0", - "once": "1.4.0", - "xtend": "2.2.0" - }, - "dependencies": { - "xtend": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz", - "integrity": "sha1-7vax8ZjByN6vrYsXZaBNrUoBxak=" - } - } - }, - "level-fix-range": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/level-fix-range/-/level-fix-range-1.0.2.tgz", - "integrity": "sha1-vxW5Fa422EcMgh6IPd95zRZCCCg=" - }, - "level-hooks": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/level-hooks/-/level-hooks-4.5.0.tgz", - "integrity": "sha1-G5rmGSKTDzMF0aYfxNg8gQLA3ZM=", - "requires": { - "string-range": "1.2.2" - } - }, - "level-peek": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/level-peek/-/level-peek-1.0.6.tgz", - "integrity": "sha1-vsUccqgu5GTTNkNMfIdsP8vM538=", - "requires": { - "level-fix-range": "1.0.2" - } - }, - "level-sublevel": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/level-sublevel/-/level-sublevel-5.2.3.tgz", - "integrity": "sha1-dEwSxy0ucr543eO5tc2E1iGRQTo=", - "requires": { - "level-fix-range": "2.0.0", - "level-hooks": "4.5.0", - "string-range": "1.2.2", - "xtend": "2.0.6" - }, - "dependencies": { - "level-fix-range": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/level-fix-range/-/level-fix-range-2.0.0.tgz", - "integrity": "sha1-xBfWIVlEIVGhnZojZ4aPFyTC1Ug=", - "requires": { - "clone": "0.1.19" - } - }, - "xtend": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.0.6.tgz", - "integrity": "sha1-XqZXptukRwacLlnFihE4ywxebO4=", - "requires": { - "is-object": "0.1.2", - "object-keys": "0.2.0" - } - } - } - }, - "levelup": { - "version": "0.18.6", - "resolved": "https://registry.npmjs.org/levelup/-/levelup-0.18.6.tgz", - "integrity": "sha1-5qAcsIlhbI7MApHCqb0/DETj5es=", - "requires": { - "bl": "0.8.2", - "deferred-leveldown": "0.2.0", - "errno": "0.1.7", - "prr": "0.0.0", - "readable-stream": "1.0.34", - "semver": "2.3.2", - "xtend": "3.0.0" - }, - "dependencies": { - "bl": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-0.8.2.tgz", - "integrity": "sha1-yba8oI0bwuoA/Ir7Txpf0eHGbk4=", - "requires": { - "readable-stream": "1.0.34" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "prr": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", - "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "semver": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-2.3.2.tgz", - "integrity": "sha1-uYSPJdbPNjMwc+ye+IVtQvEjPlI=" - }, - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" - } - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" - }, - "lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=" - }, - "lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=" - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" - }, - "lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=" - }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" - }, - "lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "requires": { - "lodash._root": "3.0.1" - } - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" - } - }, - "lodash.pad": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", - "integrity": "sha1-QzCUmoM6fI2iLMIPaibE1Z3runA=" - }, - "lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" - }, - "lodash.padstart": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.6.1.tgz", - "integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=" - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" - }, - "lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", - "requires": { - "lodash._basecopy": "3.0.1", - "lodash._basetostring": "3.0.1", - "lodash._basevalues": "3.0.0", - "lodash._isiterateecall": "3.0.9", - "lodash._reinterpolate": "3.0.0", - "lodash.escape": "3.2.0", - "lodash.keys": "3.1.2", - "lodash.restparam": "3.6.1", - "lodash.templatesettings": "3.1.1" - } - }, - "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", - "requires": { - "lodash._reinterpolate": "3.0.0", - "lodash.escape": "3.2.0" - } - }, - "lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "requires": { - "chalk": "1.1.3" - } - }, - "lolex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", - "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=" - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "3.0.2" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "requires": { - "currently-unhandled": "0.4.1", - "signal-exit": "3.0.2" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "ltgt": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", - "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" - }, - "lzma-native": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-3.0.8.tgz", - "integrity": "sha512-X/2yK+b4aDKZ217m3LJF3fKfzl8X9duDmGyLq1pm/Q+/P8e4pJzmMaP+Z+sRLwtLaUsyvSzvdzk1EAP23cWvvQ==", - "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.6.39", - "readable-stream": "2.3.6", - "rimraf": "2.6.2" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true - }, - "aws4": { - "version": "1.7.0", - "bundled": true - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "caseless": { - "version": "0.12.0", - "bundled": true - }, - "co": { - "version": "4.6.0", - "bundled": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "combined-stream": { - "version": "1.0.6", - "bundled": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true - }, - "extsprintf": { - "version": "1.3.0", - "bundled": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.14.1" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "ini": { - "version": "1.3.5", - "bundled": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true - }, - "jsprim": { - "version": "1.4.1", - "bundled": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "mime-db": { - "version": "1.33.0", - "bundled": true - }, - "mime-types": { - "version": "2.1.18", - "bundled": true, - "requires": { - "mime-db": "1.33.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "requires": { - "detect-libc": "1.0.3", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.7", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "2.2.1", - "tar-pack": "3.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true - }, - "qs": { - "version": "6.4.0", - "bundled": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.2", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true - }, - "semver": { - "version": "5.5.0", - "bundled": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.14.1", - "bundled": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.1", - "bundled": true, - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.6", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.4", - "bundled": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true - }, - "uuid": { - "version": "3.2.1", - "bundled": true - }, - "verror": { - "version": "1.10.0", - "bundled": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - } - } - }, - "macos-release": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-1.1.0.tgz", - "integrity": "sha512-mmLbumEYMi5nXReB9js3WGsB8UE6cDBWyIO62Z4DNx6GbRhDxHNjA1MlzSpJ2S2KM1wyiPRA0d19uHWYYvMHjA==" - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "3.0.0" - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "requires": { - "tmpl": "1.0.4" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "1.0.1" - } - }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" - }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.1" - } - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "requires": { - "mimic-fn": "1.2.0" - } - }, - "mem-fs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-1.1.3.tgz", - "integrity": "sha1-uK6NLj/Lb10/kWXBLUVRoGXZicw=", - "requires": { - "through2": "2.0.3", - "vinyl": "1.2.0", - "vinyl-file": "2.0.0" - } - }, - "mem-fs-editor": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-2.3.0.tgz", - "integrity": "sha1-QqCuH1Xnb9A/CefHsVtjB731yxM=", - "requires": { - "commondir": "1.0.1", - "deep-extend": "0.4.2", - "ejs": "2.6.1", - "glob": "7.1.2", - "globby": "4.1.0", - "mkdirp": "0.5.1", - "multimatch": "2.1.0", - "rimraf": "2.6.2", - "through2": "2.0.3", - "vinyl": "1.2.0" - }, - "dependencies": { - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - } - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.6.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.4.0", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "2.0.1" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "0.2.1" - } - } - } - }, - "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "requires": { - "readable-stream": "2.3.6" - } - }, - "metro": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.30.2.tgz", - "integrity": "sha512-wmdkh4AsfZjWaMM++KMDswQHdyo5L9a0XAaQBL4XTJdQIRG+x+Rmjixe7tDki5jKwe9XxsjjbpbdYKswOANuiw==", - "requires": { - "@babel/core": "7.0.0-beta.53", - "@babel/generator": "7.0.0-beta.53", - "@babel/helper-remap-async-to-generator": "7.0.0-beta.53", - "@babel/plugin-external-helpers": "7.0.0-beta.53", - "@babel/plugin-proposal-class-properties": "7.0.0-beta.53", - "@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.53", - "@babel/plugin-syntax-dynamic-import": "7.0.0-beta.53", - "@babel/plugin-transform-arrow-functions": "7.0.0-beta.53", - "@babel/plugin-transform-block-scoping": "7.0.0-beta.53", - "@babel/plugin-transform-classes": "7.0.0-beta.53", - "@babel/plugin-transform-computed-properties": "7.0.0-beta.53", - "@babel/plugin-transform-destructuring": "7.0.0-beta.53", - "@babel/plugin-transform-exponentiation-operator": "7.0.0-beta.53", - "@babel/plugin-transform-flow-strip-types": "7.0.0-beta.53", - "@babel/plugin-transform-for-of": "7.0.0-beta.53", - "@babel/plugin-transform-function-name": "7.0.0-beta.53", - "@babel/plugin-transform-literals": "7.0.0-beta.53", - "@babel/plugin-transform-modules-commonjs": "7.0.0-beta.53", - "@babel/plugin-transform-object-assign": "7.0.0-beta.53", - "@babel/plugin-transform-parameters": "7.0.0-beta.53", - "@babel/plugin-transform-react-display-name": "7.0.0-beta.53", - "@babel/plugin-transform-react-jsx": "7.0.0-beta.53", - "@babel/plugin-transform-react-jsx-source": "7.0.0-beta.53", - "@babel/plugin-transform-regenerator": "7.0.0-beta.53", - "@babel/plugin-transform-shorthand-properties": "7.0.0-beta.53", - "@babel/plugin-transform-spread": "7.0.0-beta.53", - "@babel/plugin-transform-template-literals": "7.0.0-beta.53", - "@babel/register": "7.0.0-beta.53", - "@babel/template": "7.0.0-beta.53", - "@babel/traverse": "7.0.0-beta.53", - "@babel/types": "7.0.0-beta.53", - "absolute-path": "0.0.0", - "async": "2.6.1", - "babel-core": "6.24.1", - "babel-generator": "6.26.1", - "babel-plugin-external-helpers": "6.22.0", - "babel-plugin-react-transform": "3.0.0", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-preset-es2015-node": "6.1.1", - "babel-preset-fbjs": "2.1.4", - "babel-preset-react-native": "4.0.0", - "babel-register": "6.26.0", - "babel-template": "6.26.0", - "babylon": "6.18.0", - "chalk": "1.1.3", - "concat-stream": "1.6.2", - "connect": "3.6.6", - "core-js": "2.5.7", - "debug": "2.6.9", - "denodeify": "1.2.1", - "eventemitter3": "3.1.0", - "fbjs": "0.8.17", - "fs-extra": "1.0.0", - "graceful-fs": "4.1.11", - "image-size": "0.6.3", - "jest-docblock": "22.4.0", - "jest-haste-map": "22.4.2", - "jest-worker": "22.2.2", - "json-stable-stringify": "1.0.1", - "json5": "0.4.0", - "left-pad": "1.3.0", - "lodash.throttle": "4.1.1", - "merge-stream": "1.0.1", - "metro-babylon7": "0.30.2", - "metro-cache": "0.30.2", - "metro-core": "0.30.2", - "metro-minify-uglify": "0.30.2", - "metro-resolver": "0.30.2", - "metro-source-map": "0.30.2", - "mime-types": "2.1.11", - "mkdirp": "0.5.1", - "node-fetch": "1.7.3", - "resolve": "1.8.1", - "rimraf": "2.6.2", - "serialize-error": "2.1.0", - "source-map": "0.5.7", - "temp": "0.8.3", - "throat": "4.1.0", - "wordwrap": "1.0.0", - "write-file-atomic": "1.3.4", - "ws": "1.1.5", - "xpipe": "1.0.5", - "yargs": "9.0.1" - }, - "dependencies": { - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - } - } - }, - "json5": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", - "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=" - } - } - }, - "metro-babylon7": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-babylon7/-/metro-babylon7-0.30.2.tgz", - "integrity": "sha512-ZI0h4/3raGnzA6fFXwLUMidGOG4jkDi9fgFkoI8I4Ack3TDMabmZATu9RD6DaSolu3lylhfPd8DeAAMeopX9CA==", - "requires": { - "babylon": "7.0.0-beta.47" - }, - "dependencies": { - "babylon": { - "version": "7.0.0-beta.47", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz", - "integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==" - } - } - }, - "metro-cache": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.30.2.tgz", - "integrity": "sha512-XYd07OwgtZRHFXyip40wdNJ8abPJRziuE5bb3jjf8wvyHxCpzlZlvbe0ZhcR8ChBwFUjHMuVyoou52AC3a0f+g==", - "requires": { - "jest-serializer": "22.4.3", - "mkdirp": "0.5.1" - } - }, - "metro-core": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.30.2.tgz", - "integrity": "sha512-2Y89PpD9sE/8QaHhYxaI21WFxkVmjbxdphiOPdsC9t7A3kQHMYOTQPYFon3bkYM7tL8k9YVBimXSv20JGglqUA==", - "requires": { - "lodash.throttle": "4.1.1", - "wordwrap": "1.0.0" - } - }, - "metro-minify-uglify": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-minify-uglify/-/metro-minify-uglify-0.30.2.tgz", - "integrity": "sha512-xwqMqYYKZEqJ66Wpf5OpyPJhApOQDb8rYiO94VInlDeHpN7eKGCVILclnx9AmVM3dStmebvXa5jrdgsbnJ1bSg==", - "requires": { - "uglify-es": "3.3.9" - } - }, - "metro-resolver": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.30.2.tgz", - "integrity": "sha512-bODCys/WYpqJ+KYbCIENZu1eqdQu3g/d2fXfhAROhutqojMqrT1eIGhzWpk3G1k/J6vlaf69uW6xrVuheg0ktg==", - "requires": { - "absolute-path": "0.0.0" - } - }, - "metro-source-map": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.30.2.tgz", - "integrity": "sha512-9tW3B1JOdXhyDJnR4wOPOsOlYWSL+xh6J+N5/DADGEK/X/+Up/lEHdEfpB+/+yGk1LHaRHcKCahtLPNl/to7Sg==", - "requires": { - "source-map": "0.5.7" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", - "integrity": "sha1-oxtAcK2uon1zLqMzdApk0OyaZlk=" - }, - "mime-types": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", - "integrity": "sha1-wlnEcb2oCKhdbNGTtDCl+uRHOzw=", - "requires": { - "mime-db": "1.23.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "requires": { - "dom-walk": "0.1.1" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "morgan": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", - "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", - "requires": { - "basic-auth": "2.0.0", - "debug": "2.6.9", - "depd": "1.1.2", - "on-finished": "2.3.0", - "on-headers": "1.0.1" - } - }, - "mout": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mout/-/mout-1.1.0.tgz", - "integrity": "sha512-XsP0vf4As6BfqglxZqbqQ8SR6KQot2AgxvR0gG+WtUkf90vUXchMOZQtPf/Hml1rEffJupqL/tIrU6EYhsUQjw==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "requires": { - "array-differ": "1.0.0", - "array-union": "1.0.2", - "arrify": "1.0.1", - "minimatch": "3.0.4" - } - }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", - "requires": { - "duplexer2": "0.0.2" - }, - "dependencies": { - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "requires": { - "readable-stream": "1.1.14" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-windows": "1.0.2", - "kind-of": "6.0.2", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "nfc-react-native": { - "version": "github:status-im/nfc-react-native#ecdb55e470bb9fa569d26fcc8af4e5169511a4d3" - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "0.1.12", - "is-stream": "1.1.0" - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" - }, - "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", - "requires": { - "growly": "1.3.0", - "semver": "5.5.0", - "shellwords": "0.1.1", - "which": "1.3.1" - } - }, - "node-pre-gyp": { - "version": "0.6.39", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "requires": { - "detect-libc": "1.0.3", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.8", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "2.2.1", - "tar-pack": "3.4.1" - }, - "dependencies": { - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.3" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "1.1.5", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.11", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.2", - "stringstream": "0.0.6", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.2" - } - } - } - }, - "node-status-codes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", - "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1.1.1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "requires": { - "hosted-git-info": "2.7.1", - "is-builtin-module": "1.0.0", - "semver": "5.5.0", - "validate-npm-package-license": "3.0.3" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "2.0.1" - } - }, - "npmlog": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz", - "integrity": "sha1-mLUlMPJRTKkNCexbIsiEZyI3VpI=", - "requires": { - "ansi": "0.3.1", - "are-we-there-yet": "1.1.5", - "gauge": "1.2.7" - } - }, - "nth-check": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "requires": { - "boolbase": "1.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - } - } - }, - "object-keys": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.2.0.tgz", - "integrity": "sha1-zd7AKZiwkb5CvxA1rjLknxy26mc=", - "requires": { - "foreach": "2.0.5", - "indexof": "0.0.1", - "is": "0.2.7" - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "octal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/octal/-/octal-1.0.0.tgz", - "integrity": "sha1-Y+cWKmjvvrniE1iNWOmJ0eXEUws=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "1.2.0" - } - }, - "opn": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz", - "integrity": "sha1-ttmec5n3jWXDuq/+8fsojpuFJDo=", - "requires": { - "object-assign": "4.1.1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "0.0.8", - "wordwrap": "0.0.3" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - } - }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" - }, - "ordered-read-streams": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", - "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", - "requires": { - "is-stream": "1.1.0", - "readable-stream": "2.3.6" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" - } - }, - "os-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-2.0.1.tgz", - "integrity": "sha1-uaOGNhwXrjohc27wWZQFyajF3F4=", - "requires": { - "macos-release": "1.1.0", - "win-release": "1.1.1" - } - }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "1.3.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "6.7.1", - "registry-auth-token": "3.3.2", - "registry-url": "3.1.0", - "semver": "5.5.0" - }, - "dependencies": { - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "3.0.2", - "duplexer3": "0.1.4", - "get-stream": "3.0.0", - "is-redirect": "1.0.0", - "is-retry-allowed": "1.1.0", - "is-stream": "1.1.0", - "lowercase-keys": "1.0.1", - "safe-buffer": "5.1.2", - "timed-out": "4.0.1", - "unzip-response": "2.0.1", - "url-parse-lax": "1.0.0" - } - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - } - } - }, - "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", - "requires": { - "asn1.js": "4.10.1", - "browserify-aes": "1.2.0", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "pbkdf2": "3.0.8" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "1.3.2" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "patch-package": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-5.1.1.tgz", - "integrity": "sha512-bO+vfFGgTVTtv89kXWGEMIPnrYhhhMtbOnJKStfIhNWUxperVjlI++1ixksi0YPCMPGuFy9W3zsKHxIITV2r2A==", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cross-spawn": "5.1.0", - "fs-extra": "4.0.3", - "minimist": "1.2.0", - "rimraf": "2.6.2", - "slash": "1.0.0", - "tmp": "0.0.31", - "update-notifier": "2.5.0" - }, - "dependencies": { - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.2" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "tmp": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", - "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", - "dev": true, - "requires": { - "os-tmpdir": "1.0.2" - } - } - } - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "requires": { - "pify": "2.3.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "pbkdf2": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.8.tgz", - "integrity": "sha1-L4q/FuvsyCJ3lF10irodeHYfYeI=", - "requires": { - "create-hmac": "1.1.7" - } - }, - "pegjs": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", - "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=" - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "2.0.4" - } - }, - "pirates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-3.0.2.tgz", - "integrity": "sha512-c5CgUJq6H2k6MJz72Ak1F5sN9n9wlSlJyEnwvpm9/y3WB4E3pHBDT2c6PEiS1vyJvq2bUxUAIu0EGf8Cx4Ic7Q==", - "requires": { - "node-modules-regexp": "1.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "requires": { - "find-up": "2.1.0" - } - }, - "plist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz", - "integrity": "sha1-CEtQk93JJQbiWfh0uNmxr7jHlZM=", - "requires": { - "base64-js": "0.0.8", - "util-deprecate": "1.0.2", - "xmlbuilder": "4.0.0", - "xmldom": "0.1.27" - } - }, - "plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", - "requires": { - "ansi-cyan": "0.1.1", - "ansi-red": "0.1.1", - "arr-diff": "1.1.0", - "arr-union": "2.1.0", - "extend-shallow": "1.1.4" - }, - "dependencies": { - "arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", - "requires": { - "arr-flatten": "1.1.0", - "array-slice": "0.2.3" - } - }, - "arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=" - }, - "extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", - "requires": { - "kind-of": "1.1.0" - } - }, - "kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=" - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" - }, - "pretty-bytes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-2.0.1.tgz", - "integrity": "sha1-FV7E0ANvQTkecEXW2+SWPVJdJk8=", - "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0", - "number-is-nan": "1.0.1" - } - }, - "pretty-format": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-4.3.1.tgz", - "integrity": "sha1-UwvlxCs8BbNkFKeipDN6qArNDo0=" - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "2.0.6" - } - }, - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.4.0", - "object-assign": "4.1.1" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "public-encrypt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", - "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "parse-asn1": "5.1.1", - "randombytes": "2.0.6" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qr.js": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", - "integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=" - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "querystringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", - "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==" - }, - "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", - "requires": { - "is-number": "4.0.0", - "kind-of": "6.0.2", - "math-random": "1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "0.6.0", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "re-natal": { - "version": "git+https://github.com/status-im/re-natal.git#860f8b58fcaf0f08870ddc1174cedd401b598b6e", - "requires": { - "chalk": "1.1.3", - "check-dependencies": "1.1.0", - "coffee-script": "1.12.7", - "commander": "2.13.0", - "deepmerge": "1.5.2", - "fs-extra": "0.26.7", - "handlebars": "4.0.11", - "klaw-sync": "2.1.0", - "semver": "5.5.0" - }, - "dependencies": { - "fs-extra": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0", - "klaw": "1.3.1", - "path-is-absolute": "1.0.1", - "rimraf": "2.6.2" - } - } - } - }, - "react": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.3.1.tgz", - "integrity": "sha512-NbkxN9jsZ6+G+ICsLdC7/wUD26uNbvKU/RAxEWgc9kcdKvROt+5d5j2cNQm5PSFTQ4WNGsR3pa4qL2Q0/WSy1w==", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.4.0", - "object-assign": "4.1.1", - "prop-types": "15.6.0" - } - }, - "react-clone-referenced-element": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-clone-referenced-element/-/react-clone-referenced-element-1.0.1.tgz", - "integrity": "sha1-K7qMaUBMXkqUQ5hgC8xMlB+GBoI=" - }, - "react-deep-force-update": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz", - "integrity": "sha1-vNMUeAJ7ZLMznxCJIatSC0MT3Cw=" - }, - "react-devtools-core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-3.1.0.tgz", - "integrity": "sha512-fO6SmpW16E9u6Lb6zQOHrjhJXGBNz+cJ0/a9cSF55nXfL0sQLlvYJR8DpU7f4rMUrVnVineg4XQyYYBZicmhJg==", - "requires": { - "shell-quote": "1.6.1", - "ws": "2.3.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" - }, - "ws": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz", - "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA=", - "requires": { - "safe-buffer": "5.0.1", - "ultron": "1.1.1" - } - } - } - }, - "react-dom": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.1.tgz", - "integrity": "sha512-2Infg89vzahq8nfVi1GkjPqq0vrBvf0f3T0+dTtyjq4f6HKOqKixAK25Vr593O3QTx4kw/vmUtAJwerlevNWOA==", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.4.0", - "object-assign": "4.1.1", - "prop-types": "15.6.0" - } - }, - "react-native": { - "version": "git+https://github.com/status-im/react-native-desktop.git#ce75ef7b3293009e163624e611e6782c8f545924", - "requires": { - "absolute-path": "0.0.0", - "art": "0.10.2", - "babel-core": "6.24.1", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-async-to-generator": "6.16.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-exponentiation-operator": "6.24.1", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "base64-js": "1.3.0", - "chalk": "1.1.3", - "commander": "2.13.0", - "compression": "1.7.2", - "connect": "3.6.6", - "create-react-class": "15.6.3", - "debug": "2.6.9", - "denodeify": "1.2.1", - "envinfo": "3.11.1", - "errorhandler": "1.5.0", - "eslint-plugin-react-native": "3.2.1", - "event-target-shim": "1.1.1", - "fbjs": "0.8.17", - "fbjs-scripts": "0.8.3", - "fs-extra": "1.0.0", - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "inquirer": "3.3.0", - "lodash": "4.17.10", - "metro": "0.30.2", - "metro-core": "0.30.2", - "mime": "1.6.0", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "morgan": "1.9.0", - "node-fetch": "1.7.3", - "node-notifier": "5.2.1", - "npmlog": "2.0.4", - "opn": "3.0.3", - "optimist": "0.6.1", - "plist": "1.2.0", - "pretty-format": "4.3.1", - "promise": "7.3.1", - "prop-types": "15.6.0", - "react-clone-referenced-element": "1.0.1", - "react-devtools-core": "3.1.0", - "react-timer-mixin": "0.13.3", - "regenerator-runtime": "0.11.1", - "rimraf": "2.6.2", - "semver": "5.5.0", - "serve-static": "1.13.2", - "shell-quote": "1.6.1", - "stacktrace-parser": "0.1.4", - "whatwg-fetch": "1.1.1", - "ws": "1.1.5", - "xcode": "0.9.3", - "xmldoc": "0.4.0", - "yargs": "9.0.1", - "yeoman-environment": "1.6.6", - "yeoman-generator": "0.20.3" - }, - "dependencies": { - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - } - } - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "create-react-class": { - "version": "15.6.3", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", - "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.4.0", - "object-assign": "4.1.1" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "whatwg-fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz", - "integrity": "sha1-rDydOfMgxtzlM5lp0FTvQ90zMxk=" - } - } - }, - "react-native-background-timer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz", - "integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw==" - }, - "react-native-camera": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-0.10.0.tgz", - "integrity": "sha512-qum5zdJ3GQZ/mB+SYdT8EhQVnIfmyLzwyIK9Ss4vsTGLKxKmt9tf7oGigT2t52aOy6tNglKcgYuU0cZREKGDDA==", - "requires": { - "prop-types": "15.6.0" - } - }, - "react-native-config": { - "version": "git+https://github.com/status-im/react-native-config.git#99e60ac4695966e25012e0506974e78a2d754d87" - }, - "react-native-crypto": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/react-native-crypto/-/react-native-crypto-2.1.1.tgz", - "integrity": "sha512-JHwX0zobeYqCAwdSffRoivXeqlZR54WpCj8ICGcoNh7bvOh8khr63zvNk+fcqdRH3eI9Jhglygswijza0f3D4Q==", - "requires": { - "browserify-cipher": "1.0.1", - "browserify-sign": "4.0.4", - "create-ecdh": "4.0.3", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "diffie-hellman": "5.0.3", - "inherits": "2.0.1", - "pbkdf2": "3.0.8", - "public-encrypt": "4.0.2" - } - }, - "react-native-dialogs": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/react-native-dialogs/-/react-native-dialogs-0.0.20.tgz", - "integrity": "sha512-HeoU9d7wWUUAtxjcIJLaeIs/eYS2ZHTNh3kboeCKngLvfvgptd/7vZXwXdQ+xeOEek+cm95kDfMJQd2xnb+4wA==" - }, - "react-native-fcm": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/react-native-fcm/-/react-native-fcm-10.0.3.tgz", - "integrity": "sha1-HcU47YifkLelvB0FMMxRbmmwnow=" - }, - "react-native-fetch-polyfill": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/react-native-fetch-polyfill/-/react-native-fetch-polyfill-1.1.2.tgz", - "integrity": "sha1-JWtaCr14zEmS96fPglQ9ovISSnM=" - }, - "react-native-fs": { - "version": "git+https://github.com/status-im/react-native-fs.git#e60e5cd6062e8c33379dd50f09b76f4f5024d5c0", - "requires": { - "base-64": "0.1.0", - "utf8": "2.1.2" - } - }, - "react-native-http": { - "version": "github:tradle/react-native-http#834492dbc792b1dec8ec8f0a1c9671f640339f01", - "requires": { - "Base64": "0.2.1", - "inherits": "2.0.1" - } - }, - "react-native-http-bridge": { - "version": "git+https://github.com/status-im/react-native-http-bridge.git#612f6a7b5e36d72df10c0fcb40cd124268e5b4df" - }, - "react-native-i18n": { - "version": "git+https://github.com/status-im/react-native-i18n.git#e4361722d5fcaaa52d561d948dc7ed2a5e709d38" - }, - "react-native-image-crop-picker": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.18.1.tgz", - "integrity": "sha512-63Yr+Mzl/CzDze1eqMg/GGFOODgGrixSMO+28TVGCX3982f76w/op0JKoL7zuSKz/P/mVsbi9RnHA0z6Ih/RHQ==" - }, - "react-native-image-resizer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/react-native-image-resizer/-/react-native-image-resizer-1.0.0.tgz", - "integrity": "sha1-1H4UlDw3k44of71jnk23zrf9iRc=" - }, - "react-native-invertible-scroll-view": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/react-native-invertible-scroll-view/-/react-native-invertible-scroll-view-1.1.0.tgz", - "integrity": "sha1-v9UKP11myhJjm3x6mETO3dHRaJA=", - "requires": { - "create-react-class": "15.6.2", - "prop-types": "15.6.0", - "react-clone-referenced-element": "1.0.1", - "react-native-scrollable-mixin": "1.0.1" - } - }, - "react-native-keychain": { - "version": "git+https://github.com/status-im/react-native-keychain.git#61fb404b9b9a38c3a2fa8b0b730576686ed303cc" - }, - "react-native-level-fs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-native-level-fs/-/react-native-level-fs-3.0.0.tgz", - "integrity": "sha1-uefSeEhbaWY0pifMYEV3yyr5698=", - "requires": { - "level-filesystem": "1.2.0", - "levelup": "0.18.6" - } - }, - "react-native-os": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/react-native-os/-/react-native-os-1.1.0.tgz", - "integrity": "sha1-v74cRNilsUpvOjpAXYrab1R6UW4=" - }, - "react-native-qrcode": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/react-native-qrcode/-/react-native-qrcode-0.2.6.tgz", - "integrity": "sha1-3ZLxUcEYhgCSr5+lvAmV8vnd2xY=", - "requires": { - "create-react-class": "15.6.2", - "prop-types": "15.6.0", - "qr.js": "0.0.0" - } - }, - "react-native-scrollable-mixin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-native-scrollable-mixin/-/react-native-scrollable-mixin-1.0.1.tgz", - "integrity": "sha1-NKMhZ7ZCSFlBVP0NaosD8idAVI4=" - }, - "react-native-securerandom": { - "version": "git+https://github.com/status-im/react-native-securerandom.git#58aeb7e581bbccf3bfa54e1fbe48c3d5dca9a675", - "requires": { - "base64-js": "0.0.8" - } - }, - "react-native-splash-screen": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/react-native-splash-screen/-/react-native-splash-screen-3.0.6.tgz", - "integrity": "sha512-yaTnGAHRyhduLSfD85gP3Vsf0BRePHW3aNNtDXbkbUhwIIeafu2cJH86U/qKFuKLMYLnFOXteOkP80gaYVGAYg==" - }, - "react-native-svg": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-6.3.1.tgz", - "integrity": "sha512-0kmfUwKUBWnPuJpy+bdGIYKkXHg/M/X57ji8b3d3ZFB2rRTWMRkwI1D+AJ6FQRX109+FJn6L6hsIokDj1lckzA==", - "requires": { - "color": "2.0.1", - "lodash": "4.17.10", - "pegjs": "0.10.0" - } - }, - "react-native-tcp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/react-native-tcp/-/react-native-tcp-3.3.0.tgz", - "integrity": "sha512-WAiL3IGhVxvmM0iw7/5ZRzdkiJBfBbLoFKNaHLJIsTZVqD/N5ZHUlJMjIQ+RrmB/f5nJ82bUEZ2vPCxe+rlNqQ==", - "requires": { - "base64-js": "0.0.8", - "buffer": "5.1.0", - "events": "1.1.1", - "ip-regex": "1.0.3", - "process": "0.11.10", - "util": "0.10.3" - }, - "dependencies": { - "buffer": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", - "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", - "requires": { - "base64-js": "1.3.0", - "ieee754": "1.1.12" - }, - "dependencies": { - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - } - } - } - } - }, - "react-native-testfairy": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/react-native-testfairy/-/react-native-testfairy-2.10.0.tgz", - "integrity": "sha512-l9Ywc69WTnraPuq5wZS7/GWcSuBxVKxaFzCIhsrIzWhByIh/1o/UqwRb8Eb06h8/e3IP7fd1LD5hXF2NMDRg3g==" - }, - "react-native-udp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-native-udp/-/react-native-udp-2.2.1.tgz", - "integrity": "sha1-Y1NsYVCzborhi870AMT66iT5QDU=", - "requires": { - "base64-js": "0.0.8", - "events": "1.1.1", - "inherits": "2.0.1", - "ip-regex": "1.0.3", - "util": "0.10.3" - } - }, - "react-native-webview-bridge": { - "version": "github:status-im/react-native-webview-bridge#da824f59731af37b877e325204637d7c52e170f6", - "requires": { - "invariant": "2.2.0", - "keymirror": "0.1.1" - }, - "dependencies": { - "invariant": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.0.tgz", - "integrity": "sha1-yNfoRzZqScwYtiLwWKaJ1IHolfI=", - "requires": { - "loose-envify": "1.4.0" - } - } - } - }, - "react-proxy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz", - "integrity": "sha1-nb/Z2SdSjDqp9ETkVYw3gwq4wmo=", - "requires": { - "lodash": "4.17.10", - "react-deep-force-update": "1.1.1" - } - }, - "react-timer-mixin": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/react-timer-mixin/-/react-timer-mixin-0.13.3.tgz", - "integrity": "sha1-Dai5+AfsB9w+hU0ILHN8ZWBbPSI=" - }, - "react-transform-hmr": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz", - "integrity": "sha1-4aQL0Krvxy6N/Xp82gmvhQZjl7s=", - "requires": { - "global": "4.3.2", - "react-proxy": "1.1.8" - } - }, - "read-all-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", - "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", - "requires": { - "pinkie-promise": "2.0.1", - "readable-stream": "2.3.6" - } - }, - "read-chunk": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-1.0.1.tgz", - "integrity": "sha1-X2jKswfmY/GZk1J9m1icrORmEZQ=" - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.4.0", - "path-type": "2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.2" - } - } - } - }, - "readline2": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-0.1.1.tgz", - "integrity": "sha1-mUQ7pug7gw7zBRv9fcJBqCco1Wg=", - "requires": { - "mute-stream": "0.0.4", - "strip-ansi": "2.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz", - "integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=" - }, - "mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha1-qSGZYKbV1dBGWXruUSUsZlX3F34=" - }, - "strip-ansi": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", - "integrity": "sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=", - "requires": { - "ansi-regex": "1.1.1" - } - } - } - }, - "realm": { - "version": "git+https://github.com/status-im/realm-js.git#65d4fb77474c797ebb13ae8ef4748a4cad8c4824", - "requires": { - "command-line-args": "4.0.7", - "decompress": "4.2.0", - "decompress-tarxz": "2.1.1", - "fs-extra": "4.0.3", - "ini": "1.3.5", - "lzma-native": "3.0.8", - "nan": "2.10.0", - "node-fetch": "1.7.3", - "node-pre-gyp": "0.6.39", - "progress": "2.0.0", - "prop-types": "15.6.0", - "request": "2.87.0", - "stream-counter": "1.0.0", - "sync-request": "3.0.1", - "url-parse": "1.4.1" - }, - "dependencies": { - "decompress": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", - "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", - "requires": { - "decompress-tar": "4.1.1", - "decompress-tarbz2": "4.1.1", - "decompress-targz": "4.1.1", - "decompress-unzip": "4.0.1", - "graceful-fs": "4.1.11", - "make-dir": "1.3.0", - "pify": "2.3.0", - "strip-dirs": "2.1.0" - } - }, - "decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "requires": { - "file-type": "5.2.0", - "is-stream": "1.1.0", - "tar-stream": "1.6.1" - } - }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "requires": { - "decompress-tar": "4.1.1", - "file-type": "6.2.0", - "is-stream": "1.1.0", - "seek-bzip": "1.0.5", - "unbzip2-stream": "1.2.5" - }, - "dependencies": { - "file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==" - } - } - }, - "decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "requires": { - "decompress-tar": "4.1.1", - "file-type": "5.2.0", - "is-stream": "1.1.0" - } - }, - "decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", - "requires": { - "file-type": "3.9.0", - "get-stream": "2.3.1", - "pify": "2.3.0", - "yauzl": "2.10.0" - }, - "dependencies": { - "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=" - } - } - }, - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=" - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.2" - } - }, - "get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", - "requires": { - "object-assign": "4.1.1", - "pinkie-promise": "2.0.1" - } - }, - "is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "requires": { - "is-natural-number": "4.0.1" - } - } - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" - } - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "private": "0.1.8" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "requires": { - "is-equal-shallow": "0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" - } - }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", - "requires": { - "regenerate": "1.4.0", - "regjsgen": "0.2.0", - "regjsparser": "0.1.5" - } - }, - "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "dev": true, - "requires": { - "rc": "1.2.8", - "safe-buffer": "5.1.2" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "1.2.8" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "requires": { - "jsesc": "0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "1.0.2" - } - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" - } - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "1.33.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", - "requires": { - "path-parse": "1.0.5" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "requires": { - "expand-tilde": "2.0.2", - "global-modules": "1.0.0" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "2.0.1", - "signal-exit": "3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "optional": true, - "requires": { - "align-text": "0.1.4" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.1" - } - }, - "rn-snoopy": { - "version": "github:status-im/rn-snoopy#f23dc13469c6c2a694649f658cdc1d1eaf8def64", - "requires": { - "eslint-plugin-jsx-a11y": "5.1.1", - "lodash": "3.10.1", - "rxjs": "5.5.2" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==" - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "2.1.0" - } - }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=" - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=" - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "requires": { - "rx-lite": "4.0.8" - } - }, - "rxjs": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.2.tgz", - "integrity": "sha512-oRYoIKWBU3Ic37fLA5VJu31VqQO4bWubRntcHSJ+cwaDQBwdnZ9x4zmhJfm/nFQ2E82/I4loSioHnACamrKGgA==", - "requires": { - "symbol-observable": "1.2.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "0.1.15" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "samsam": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", - "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=" - }, - "sane": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", - "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", - "requires": { - "anymatch": "2.0.0", - "capture-exit": "1.2.0", - "exec-sh": "0.2.2", - "fb-watchman": "2.0.0", - "fsevents": "1.2.4", - "micromatch": "3.1.10", - "minimist": "1.2.0", - "walker": "1.0.7", - "watch": "0.18.0" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "sax": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.6.tgz", - "integrity": "sha1-XWFr6KXmB9VOEUr65Vt+ry/MMkA=" - }, - "seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", - "requires": { - "commander": "2.8.1" - }, - "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "requires": { - "graceful-readlink": "1.0.1" - } - } - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "5.5.0" - } - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.2", - "destroy": "1.0.4", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.3", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.4.0" - }, - "dependencies": { - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "serialize-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", - "integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=" - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.2" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "2.0.1", - "safe-buffer": "5.1.2" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "requires": { - "array-filter": "0.0.1", - "array-map": "0.0.0", - "array-reduce": "0.0.0", - "jsonify": "0.0.0" - } - }, - "shelljs": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.5.3.tgz", - "integrity": "sha1-xUmCuZbHbvDB5rWfvcWCX1txMRM=" - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-plist": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-0.2.1.tgz", - "integrity": "sha1-cXZts1IyaSjPOoByQrp2IyJjZyM=", - "requires": { - "bplist-creator": "0.0.7", - "bplist-parser": "0.1.1", - "plist": "2.0.1" - }, - "dependencies": { - "base64-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.1.2.tgz", - "integrity": "sha1-1kAMrBxMZgl22Q0HoENR2JOV9eg=" - }, - "plist": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/plist/-/plist-2.0.1.tgz", - "integrity": "sha1-CjLKlIGxw2TpLhjcVch23p0B2os=", - "requires": { - "base64-js": "1.1.2", - "xmlbuilder": "8.2.2", - "xmldom": "0.1.27" - } - }, - "xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=" - } - } - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "0.3.2" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, - "sinon": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", - "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", - "requires": { - "formatio": "1.1.1", - "lolex": "1.3.2", - "samsam": "1.1.2", - "util": "0.10.3" - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.2", - "use": "3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "3.2.2" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "requires": { - "hoek": "2.16.3" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "2.1.1", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" - } - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "requires": { - "source-map": "0.5.7" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==" - }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", - "requires": { - "concat-stream": "1.6.2", - "os-shim": "0.1.3" - } - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "2.1.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "3.0.2" - } - }, - "sprintf-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", - "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" - }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "stacktrace-parser": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.4.tgz", - "integrity": "sha1-ATl5IuX2Ls8whFUiyVxP4dJefU4=" - }, - "stat-mode": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", - "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - } - } - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "stream-buffers": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", - "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=" - }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "requires": { - "duplexer2": "0.1.4", - "readable-stream": "2.3.6" - } - }, - "stream-counter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-1.0.0.tgz", - "integrity": "sha1-kc8lac5NxQYf6816yyY5SloRR1E=" - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "string-range": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/string-range/-/string-range-1.2.2.tgz", - "integrity": "sha1-qJPtNH5yKZvIO++78qaSqNI51d0=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-bom-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", - "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", - "requires": { - "first-chunk-stream": "2.0.0", - "strip-bom": "2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "0.2.1" - } - } - } - }, - "strip-dirs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz", - "integrity": "sha1-lgu9EoeETzl1pFWKoQOoJV4kVqA=", - "requires": { - "chalk": "1.1.3", - "get-stdin": "4.0.1", - "is-absolute": "0.1.7", - "is-natural-number": "2.1.1", - "minimist": "1.2.0", - "sum-up": "1.0.3" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "requires": { - "get-stdin": "4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", - "requires": { - "escape-string-regexp": "1.0.5" - } - }, - "sum-up": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", - "integrity": "sha1-HGYfZnBX9jvLeHWqFDi8FiUlFW4=", - "requires": { - "chalk": "1.1.3" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, - "sync-request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-3.0.1.tgz", - "integrity": "sha1-yqEjWq+Im6UBB2oYNMQ2gwqC+3M=", - "requires": { - "concat-stream": "1.6.2", - "http-response-object": "1.1.0", - "then-request": "2.2.0" - } - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.1" - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.6", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tar-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", - "requires": { - "bl": "1.2.2", - "buffer-alloc": "1.2.0", - "end-of-stream": "1.4.1", - "fs-constants": "1.0.0", - "readable-stream": "2.3.6", - "to-buffer": "1.1.1", - "xtend": "4.0.1" - } - }, - "temp": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", - "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", - "requires": { - "os-tmpdir": "1.0.2", - "rimraf": "2.2.8" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" - } - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "0.7.0" - } - }, - "test-value": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=", - "requires": { - "array-back": "1.0.4", - "typical": "2.6.1" - }, - "dependencies": { - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "requires": { - "typical": "2.6.1" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "textextensions": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", - "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=" - }, - "then-request": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/then-request/-/then-request-2.2.0.tgz", - "integrity": "sha1-ZnizL6DKIY/laZgbvYhxtZQGDYE=", - "requires": { - "caseless": "0.11.0", - "concat-stream": "1.6.2", - "http-basic": "2.5.1", - "http-response-object": "1.1.0", - "promise": "7.3.1", - "qs": "6.4.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - } - } - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "requires": { - "readable-stream": "2.3.6", - "xtend": "4.0.1" - } - }, - "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", - "requires": { - "through2": "2.0.3", - "xtend": "4.0.1" - } - }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" - }, - "timed-out": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-3.1.3.tgz", - "integrity": "sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=" - }, - "tiny-queue": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.0.tgz", - "integrity": "sha1-xJ/LXIdVW+G0pd9+uHEB1beLydw=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" - }, - "to-absolute-glob": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", - "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", - "requires": { - "extend-shallow": "2.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "3.2.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - } - } - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "requires": { - "punycode": "1.4.1" - } - }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" - }, - "trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", - "requires": { - "escape-string-regexp": "1.0.5" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" - }, - "ua-parser-js": { - "version": "0.7.18", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", - "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==" - }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "requires": { - "commander": "2.13.0", - "source-map": "0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "ultron": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" - }, - "unbzip2-stream": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", - "integrity": "sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==", - "requires": { - "buffer": "3.6.0", - "through": "2.3.8" - } - }, - "underscore.string": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz", - "integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=", - "requires": { - "sprintf-js": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" - } - } - } - }, - "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "requires": { - "json-stable-stringify": "1.0.1", - "through2-filter": "2.0.0" - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "1.0.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "untildify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha1-F+soB5h/dpUunASF/DEdBqgmouA=", - "requires": { - "os-homedir": "1.0.2" - } - }, - "unzip-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", - "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=" - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "1.3.0", - "chalk": "2.4.1", - "configstore": "3.1.2", - "import-lazy": "2.1.0", - "is-ci": "1.1.0", - "is-installed-globally": "0.1.0", - "is-npm": "1.0.0", - "latest-version": "3.1.0", - "semver-diff": "2.1.0", - "xdg-basedir": "3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "1.9.2" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "url-parse": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.1.tgz", - "integrity": "sha512-x95Td74QcvICAA0+qERaVkRpTGKyBHHYdwL2LXZm5t/gBtCB9KQSO/0zQgSTYEV1p0WcvSg79TLNPSvd5IDJMQ==", - "requires": { - "querystringify": "2.0.0", - "requires-port": "1.0.0" - } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "1.0.4" - } - }, - "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "requires": { - "kind-of": "6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "requires": { - "os-homedir": "1.0.2" - } - }, - "utf8": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", - "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=" - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" - }, - "vali-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=" - }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", - "requires": { - "spdx-correct": "3.0.0", - "spdx-expression-parse": "3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "requires": { - "clone": "1.0.4", - "clone-stats": "0.0.1", - "replace-ext": "0.0.1" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - } - } - }, - "vinyl-assign": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vinyl-assign/-/vinyl-assign-1.2.1.tgz", - "integrity": "sha1-TRmIkbVRWRHXcajNnFSApGoHSkU=", - "requires": { - "object-assign": "4.1.1", - "readable-stream": "2.3.6" - } - }, - "vinyl-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", - "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0", - "strip-bom-stream": "2.0.0", - "vinyl": "1.2.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "0.2.1" - } - } - } - }, - "vinyl-fs": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", - "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", - "requires": { - "duplexify": "3.6.0", - "glob-stream": "5.3.5", - "graceful-fs": "4.1.11", - "gulp-sourcemaps": "1.6.0", - "is-valid-glob": "0.3.0", - "lazystream": "1.0.0", - "lodash.isequal": "4.5.0", - "merge-stream": "1.0.1", - "mkdirp": "0.5.1", - "object-assign": "4.1.1", - "readable-stream": "2.3.6", - "strip-bom": "2.0.0", - "strip-bom-stream": "1.0.0", - "through2": "2.0.3", - "through2-filter": "2.0.0", - "vali-date": "1.0.0", - "vinyl": "1.2.0" - }, - "dependencies": { - "first-chunk-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", - "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=" - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "0.2.1" - } - }, - "strip-bom-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", - "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", - "requires": { - "first-chunk-stream": "1.0.0", - "strip-bom": "2.0.0" - } - } - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "requires": { - "makeerror": "1.0.11" - } - }, - "ware": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz", - "integrity": "sha1-0bFPOdLiy0q4xAmPdW/ksWTkc9Q=", - "requires": { - "wrap-fn": "0.1.5" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "requires": { - "exec-sh": "0.2.2", - "minimist": "1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "web3": { - "version": "github:status-im/web3.js#ff09ce57e9574a1c42138832a7d6bf6c2e699048", - "requires": { - "bignumber.js": "github:status-im/bignumber.js#cc066a0a3d6bfe0c436c9957f4ea8344bf963c89", - "crypto-js": "3.1.8", - "utf8": "2.1.2", - "xhr2": "0.1.4", - "xmlhttprequest": "1.8.0" - } - }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "2.1.1" - } - }, - "widest-line": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", - "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", - "dev": true, - "requires": { - "string-width": "2.1.1" - } - }, - "win-release": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz", - "integrity": "sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk=", - "requires": { - "semver": "5.5.0" - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "optional": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "wrap-fn": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz", - "integrity": "sha1-8htuQQFv9KfjFyDbxjoJAWvfmEU=", - "requires": { - "co": "3.1.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", - "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "slide": "1.1.6" - } - }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "requires": { - "options": "0.0.6", - "ultron": "1.0.2" - } - }, - "xcode": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/xcode/-/xcode-0.9.3.tgz", - "integrity": "sha1-kQqJwWrubMC0LKgFptC0z4chHPM=", - "requires": { - "pegjs": "0.10.0", - "simple-plist": "0.2.1", - "uuid": "3.0.1" - } - }, - "xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", - "requires": { - "os-homedir": "1.0.2" - } - }, - "xhr2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", - "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" - }, - "xmlbuilder": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz", - "integrity": "sha1-mLj2UcowqmJANvEn0RzGbce5B6M=", - "requires": { - "lodash": "3.10.1" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, - "xmldoc": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-0.4.0.tgz", - "integrity": "sha1-0lciS+g5PqrL+DfvIn/Y7CWzaIg=", - "requires": { - "sax": "1.1.6" - } - }, - "xmldom": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", - "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" - }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" - }, - "xpipe": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/xpipe/-/xpipe-1.0.5.tgz", - "integrity": "sha1-jdi/Rfw/f1Xw4FS4ePQ6YmFNr98=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", - "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", - "requires": { - "camelcase": "4.1.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.3", - "os-locale": "2.1.0", - "read-pkg-up": "2.0.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "7.0.0" - } - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "requires": { - "camelcase": "4.1.0" - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "requires": { - "buffer-crc32": "0.2.13", - "fd-slicer": "1.1.0" - } - }, - "yeoman-assert": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/yeoman-assert/-/yeoman-assert-2.2.3.tgz", - "integrity": "sha1-pWgqg2MsUKwO6EFzpaEP1vMgZHQ=" - }, - "yeoman-environment": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-1.6.6.tgz", - "integrity": "sha1-zYX6Z9FWBg5EDXgH1+988NLR1nE=", - "requires": { - "chalk": "1.1.3", - "debug": "2.6.9", - "diff": "2.2.3", - "escape-string-regexp": "1.0.5", - "globby": "4.1.0", - "grouped-queue": "0.3.3", - "inquirer": "1.2.3", - "lodash": "4.17.10", - "log-symbols": "1.0.2", - "mem-fs": "1.1.3", - "text-table": "0.2.0", - "untildify": "2.1.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "requires": { - "restore-cursor": "1.0.1" - } - }, - "external-editor": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-1.1.1.tgz", - "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=", - "requires": { - "extend": "3.0.1", - "spawn-sync": "1.0.15", - "tmp": "0.0.29" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - } - }, - "inquirer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-1.2.3.tgz", - "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=", - "requires": { - "ansi-escapes": "1.4.0", - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "cli-width": "2.2.0", - "external-editor": "1.1.1", - "figures": "1.7.0", - "lodash": "4.17.10", - "mute-stream": "0.0.6", - "pinkie-promise": "2.0.1", - "run-async": "2.3.0", - "rx": "4.1.0", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "through": "2.3.8" - } - }, - "mute-stream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.6.tgz", - "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=" - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "requires": { - "exit-hook": "1.1.1", - "onetime": "1.1.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", - "requires": { - "os-tmpdir": "1.0.2" - } - } - } - }, - "yeoman-generator": { - "version": "0.20.3", - "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-0.20.3.tgz", - "integrity": "sha1-fm3AaMpQ95Fg/tGiRp7Q55UotnE=", - "requires": { - "async": "1.5.2", - "chalk": "1.1.3", - "class-extend": "0.1.2", - "cli-table": "0.3.1", - "cross-spawn": "2.2.3", - "dargs": "4.1.0", - "dateformat": "1.0.12", - "debug": "2.6.9", - "detect-conflict": "1.0.1", - "diff": "2.2.3", - "download": "4.4.3", - "findup-sync": "0.2.1", - "github-username": "2.1.0", - "glob": "5.0.15", - "gruntfile-editor": "1.2.1", - "html-wiring": "1.2.0", - "inquirer": "0.8.5", - "istextorbinary": "1.0.2", - "lodash": "3.10.1", - "mem-fs-editor": "2.3.0", - "mime": "1.6.0", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "path-exists": "1.0.0", - "path-is-absolute": "1.0.1", - "pretty-bytes": "2.0.1", - "read-chunk": "1.0.1", - "rimraf": "2.6.2", - "run-async": "0.1.0", - "shelljs": "0.5.3", - "sinon": "1.17.7", - "text-table": "0.2.0", - "through2": "2.0.3", - "underscore.string": "3.3.4", - "user-home": "2.0.0", - "xdg-basedir": "2.0.0", - "yeoman-assert": "2.2.3", - "yeoman-environment": "1.6.6", - "yeoman-welcome": "1.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz", - "integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=" - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "cli-width": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz", - "integrity": "sha1-pNKT72frt7iNSk1CwMzwDE0eNm0=" - }, - "cross-spawn": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-2.2.3.tgz", - "integrity": "sha1-+sViAt/T0N2GF3jy2iA79DS7ghw=", - "requires": { - "cross-spawn-async": "2.2.5", - "spawn-sync": "1.0.15" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - } - }, - "findup-sync": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.2.1.tgz", - "integrity": "sha1-4KkKRQB1xJRm7lE3MgV1FLgeh4w=", - "requires": { - "glob": "4.3.5" - }, - "dependencies": { - "glob": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-4.3.5.tgz", - "integrity": "sha1-gPuwjKVA8jiszl0R0em8QedRc9M=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.1", - "minimatch": "2.0.10", - "once": "1.4.0" - } - } - } - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.1", - "minimatch": "2.0.10", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "inquirer": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.8.5.tgz", - "integrity": "sha1-29dAz2yjtzEpamPOb22WGFHzNt8=", - "requires": { - "ansi-regex": "1.1.1", - "chalk": "1.1.3", - "cli-width": "1.1.1", - "figures": "1.7.0", - "lodash": "3.10.1", - "readline2": "0.1.1", - "rx": "2.5.3", - "through": "2.3.8" - } - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - }, - "minimatch": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", - "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=" - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "requires": { - "once": "1.4.0" - } - }, - "rx": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/rx/-/rx-2.5.3.tgz", - "integrity": "sha1-Ia3H2A8CACr1Da6X/Z2/JIdV9WY=" - } - } - }, - "yeoman-welcome": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/yeoman-welcome/-/yeoman-welcome-1.0.1.tgz", - "integrity": "sha1-9s8Zj9T7qKdxZywmzfuKZHlchOw=", - "requires": { - "chalk": "1.1.3" - } - } - } -} diff --git a/desktop_files/package.json b/desktop_files/package.json deleted file mode 100644 index 0dc30daf8c6..00000000000 --- a/desktop_files/package.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "name": "StatusIm", - "version": "0.0.1", - "private": true, - "scripts": { - "start": "node node_modules/react-native/local-cli/cli.js start", - "prepare": "patch-package" - }, - "desktopExternalModules": [ - "node_modules/react-native-i18n/desktop", - "node_modules/react-native-config/desktop", - "node_modules/react-native-fs/desktop", - "node_modules/react-native-http-bridge/desktop", - "node_modules/react-native-webview-bridge/desktop", - "node_modules/react-native-keychain/desktop", - "node_modules/react-native-securerandom/desktop", - "modules/react-native-status/desktop" - ], - "dependencies": { - "assert": "1.4.1", - "asyncstorage-down": "4.0.1", - "babel-core": "6.24.1", - "babel-generator": "6.24.1", - "babel-helper-builder-react-jsx": "6.18.0", - "babel-plugin-transform-es2015-block-scoping": "6.15.0", - "babel-preset-react-native": "4.0.0", - "babel-register": "6.18.0", - "bignumber.js": "github:status-im/bignumber.js#master", - "buffer": "3.6.0", - "chance": "1.0.12", - "create-react-class": "15.6.2", - "dns.js": "1.0.1", - "emojilib": "2.2.9", - "events": "1.1.1", - "homoglyph-finder": "1.1.1", - "identicon.js": "github:status-im/identicon.js", - "instabug-reactnative": "2.12.0", - "level-filesystem": "1.2.0", - "metro": "^0.30.2", - "nfc-react-native": "github:status-im/nfc-react-native", - "process": "0.11.10", - "prop-types": "15.6.0", - "punycode": "1.4.1", - "querystring-es3": "0.2.1", - "re-natal": "git+https://github.com/status-im/re-natal.git#master", - "react": "16.3.1", - "react-dom": "16.3.1", - "react-native": "git+https://github.com/status-im/react-native-desktop.git", - "react-native-background-timer": "2.0.0", - "react-native-camera": "0.10.0", - "react-native-config": "git+https://github.com/status-im/react-native-config.git", - "react-native-crypto": "2.1.1", - "react-native-dialogs": "0.0.20", - "react-native-fcm": "10.0.3", - "react-native-fetch-polyfill": "1.1.2", - "react-native-fs": "git+https://github.com/status-im/react-native-fs.git", - "react-native-http": "github:tradle/react-native-http#834492d", - "react-native-http-bridge": "git+https://github.com/status-im/react-native-http-bridge.git#desktop", - "react-native-i18n": "git+https://github.com/status-im/react-native-i18n.git#version_0.0.8_desktop", - "react-native-image-crop-picker": "0.18.1", - "react-native-image-resizer": "1.0.0", - "react-native-invertible-scroll-view": "1.1.0", - "react-native-keychain": "git+https://github.com/status-im/react-native-keychain.git", - "react-native-level-fs": "3.0.0", - "react-native-os": "1.1.0", - "react-native-qrcode": "0.2.6", - "react-native-securerandom": "git+https://github.com/status-im/react-native-securerandom.git", - "react-native-splash-screen": "3.0.6", - "react-native-svg": "6.3.1", - "react-native-tcp": "3.3.0", - "react-native-testfairy": "2.10.0", - "react-native-udp": "2.2.1", - "react-native-webview-bridge": "github:status-im/react-native-webview-bridge#react-native-0.49-desktop", - "realm": "git+https://github.com/status-im/realm-js.git", - "rn-snoopy": "github:status-im/rn-snoopy", - "string_decoder": "0.10.31", - "url": "0.10.3", - "web3": "github:status-im/web3.js#feature/shhext" - }, - "devDependencies": { - "patch-package": "^5.1.1" - } -} diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000000..0d769b6c036 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,45 @@ +## Getting Started + +- [Starting Guide](starting-guide.md) +- [IDE Setup](ide-setup.md) + +## Development Process + +- [Coding guidelines](new-guidelines.md) +- [Layered architecture](architecture-layers.md) +- [UI components coding guidelines](ui-guidelines.md) +- [Release Checklist](release-checklist.md) +- [Release Guide](release-guide.md) +- [Merging PR process](merging-pr-process.md) +- [PR Review Policy](pr-review-policy.md) +- [Working on PR together with QA team](pipeline_process.md) +- [Debugging](debugging.md) +- [Patching](patching.md) +- [Creating a pixel perfect UI](pixel-perfection.md) +- [Contributing to status-go](status-go-changes.md) +- [Status Backend Server](use-status-backend-server.md) +- [Malli schemas (recorded demo)](https://www.youtube.com/watch?v=SlRio70aYVI) ([slides](files/forging-code-with-schemas-sep-2023-slides.pdf)) + +## Testing + +- [Overview](tests/tests-overview.md) +- [How to run local tests](tests/how-to-run-local-tests.md) +- [End-to-end tests (e2e) overview](tests/how-to-launch-e2e.md) +- [Component tests (jest) overview](tests/component-tests-overview.md) + +## Project details + +- [status-go introduction (recorded meeting)](https://drive.google.com/file/d/1B7TljmTZ8fHkqJH8ChU1Cp4FGDFM03gq/view) +- [re-frame usage (recorded meeting)](https://drive.google.com/file/d/1qv_E0CEGzQpu_zGXD0gCTU5EvhC2k8Jy/view) +- [status app functionality demo](https://drive.google.com/file/u/0/d/1PmwsMLTLDNNIdv5_6wvMOwoj2PfT50c6/view) + +## Misc + +- [Importing assets from Figma into project](import-assets.md) +- [Updating Status APK builds for the F-Droid Android application catalogue](fdroid.md) +- [Troubleshooting for known errors](troubleshooting.md) + +## Outdated: + +- [Old guidelines](codebase-structure-and-guidelines.md) +- [Post mortem analysis](post-mortem.md) diff --git a/doc/adding-new-chains.md b/doc/adding-new-chains.md new file mode 100644 index 00000000000..54c0db38c78 --- /dev/null +++ b/doc/adding-new-chains.md @@ -0,0 +1,18 @@ +# Adding new chains + +This doc only describes how to add new chains on the client side, so make sure the chain is added on `status-go` first (see [eth-rpc-proxy:ADD_NEW_CHAINS.md](https://github.com/status-im/eth-rpc-proxy/blob/master/ADD_NEW_CHAINS.md)) + +## Steps +1. Add the chain image (.png) to `resources/images/networks/` +2. Add the chain name (keyword) and the path to the chain image to `quo.foundations.resources/networks` +3. Add the mainnet (and/or testnet) `chain-id` constant(s) to `status-im.contexts.wallet.networks.config` +4. Add additional chain information in the same namespace, under `mainnets` and `testnets` respectively, including the chain name keyword added for the image. +5. In case the network should be highlighted as _new_ in the UI, add the chain id to `status-im.contexts.wallet.networks.config/new-networks` + +## Future steps to improve addition of new chains and facilitate the addition of custom chains by the user +1. Remove reliance on the `network-name` and refer to a chain solely by its `chain-id` +2. Remove the definition of the network images from `quo.foundations.resources/networks` by relying on the images provided by `status-go` +3. `quo` components that should render a network image should get the image source as a prop +4. The chain data added in `status-im.contexts.wallet.networks.config` should be received from `status-go` or derived on the client, instead of being added manually. +5. Ideally, when adding a new chain, we shouln't bring new changes on the client. All the changes should happen on the `status-go` side +6. Remove deprecated namespaces dealing with chains e.g. `utils.ethereum.chains` diff --git a/doc/architecture-layers.md b/doc/architecture-layers.md new file mode 100644 index 00000000000..0bb9cac7e49 --- /dev/null +++ b/doc/architecture-layers.md @@ -0,0 +1,240 @@ +# Mobile app architecture - WIP + +## Problems we are trying to solve + +- Mixed layers of abstraction. +in the same event we sometimes process data from UI, perform application logic and send data to the backend. +- Inconsistent re-frame `app-db`. +UI state, data from backend, and application flow data are mixed, making it hard to track what belongs where. + +**Why is this bad?** +Intertwining different levels of abstraction in the same code increases cognitive load for developers and also makes code less testable. + +## Layered architecture +Architecture described below was inspired by [clean architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) but does not strictly folow it. +The core idea is to highlight 4 logical layers, each responsible for own set of tasks and to organize the code in a way that each layer is clearly separated. + +![layers](images/architecture.png) + + +## Layers + +### Gateway +`Gateway` is a layer that responsible for communication with "outer" services. +The main "outer" service that we depend on is `status-go` so most of the code here deals with connections to `status-go`. + +> **Reasoning for considering `status-go` an "outer" service?** +> - It’s accessed via remote procedure calls +> - It is a subject to change () +> - pieces of `status-go` functionality could be theoretically replaced by a another providers which confirms it "outer" nature. + +`Gateway` layer abstracts the rest of the application from changes that happen in outer services. +Which means it: +- hides implementation details of outer services. +- transforms data that arrives from outer services to those used in the app. + +#### Example +Market data is provided by `status-go` via api call to `FetchMarketTokenPageAsync` function. That function **uses pagination** (has `page` and `page-size` arguments). +After that function called `status-go` starts to regularly emit signals that **in json format** contain information about tokens: their name, price, image, market cap, position in "tokens leaderboard", price change in last 24h, etc. + +It is `gateway` layer responsibility to abstract other parts of app of such implementation details as using pagination and using json format. And it is up to gateway to dissect the incoming big ball of data to smaller and more coherent structures to be used in other layers. + +Which means `gateway` should: +- maintain the `current-page` variable and increase/reset it as needed to pass to `FetchMarketTokenPageAsync` +- maintain `page-size` variable +- convert incoming structure from json to edn +- split incoming structure to smaller and more coherent ones: token-info, token-price, tokens-leaderboard + +### Domain +`Domain` is a layer where business logic lives. It contains the entities of our knowledge domain and operations on them. +For example for `messenger` part of our app such entities are: message, chat, contact, etc. Those are concepts that all messenger apps share. + +For `wallet` part of our app domain entities are: accout, token, network, address. All wallets use those because their are part of "crypto" knowledge domain. + +So in the `domain` level of our app we define data structures for such concepts and functions to operate on them. + +**What entities belong to `domain`?** + +There is a simple question you can ask yourself to check if some data belongs to `domain` layer: "If I will write console version of our application (which means different UI and different user flows), can I reuse the same structures and functions?". +If answer is "yes" - most probably it is a domain-level entities. + +#### Example +`network` is a concept from "blockchain" domain. Representations of network can be found in code of any wallet. + +In our code `network` is represented by datastructure with fields: +```clojure +{:testnet? + :chain-id + :short-name + :full-name + :native-currency-symbol + ...} +``` + +And there are functions that work with `network` datastructure +```clojure +(defn ethereum-network? + [network] + ...) + +(defn get-networks-for-layer + "Returns networks for the layer (`1` or `2`)" + [networks layer] + ...) +``` + +If we will make a console version of the app we can reuse that datastructure and functions which means they should be stored in `domain` level. + +### Application +`Application` is a level that manages **user flows**. It is responsible for coordination between inner `domain` layer and outer layers. +When user interacts with `UI` it is up to the `application` layer to make calls to `domain`, `gateway` or `UI`. + +#### Example +Status mobile app has user flow "Send crypto". +That flow consists of few screens and on each screen the data required for sending (network, token, amount) is partially filled. + +What `application` layer **DOES** in this flow: +- when user goes from step to step in user flow, `application` layer knows what UI should be presented. For example if user starting the `send` flow `application` layer checks if user is already inside a wallet account screen. +If yes - next screen will be the token selection. If user is not within an account - next screen will be an account selection. +- when user goes through steps in user flow, `application` layer stores intermediate data acquired on steps. When user selected `token` to send `application` layer should remember the selection to use it in the next screen. +- `application` layer knows when to call `domain` to perform checks. If user entered address to send funds to, `application` layer should call the business logic in `domain` layer to check if address is valid. +If amount of crypto entered - `application` layer redirects to `domain` to check if the amount is less than user owns. +- `application` layer knows when to call `gateway` to communicate with outer services. When user confirms the sending `application` layer should call `gateway` and pass all the collected data to perform sending. + +What `application` layer **DOES NOT** in this flow: +- it does not check if address is correct "ethereum" address - it is up to `domain` layer. +- it does not check if user has sufficient amount of crypto to send - it is up to `domain` layer. +- it does not call `status-go` directly - it is up to `gateway` layer. + +### UI +Here belongs everything related to user interface - views, components, styles, animation. +All logic here should be UI-specific and not application-specific. + +#### Example +Lets say user is in "enter crypto amount" screen of "send crypto" flow. +That screen has the input field for entering amount of crypto, button for switching between crypto and fiat, button for confirming the amount. + +What **ARE** parts of `UI` layer on this screen: +- all the styles, components, animations belong to the `UI` layer +- state of the input field +- state of the fiat/crypto toggle + +What **ARE NOT** parts of `UI` layer on this screen: +- logic of converting between fiat and crypto - should be part of the `domain` +- logic of displaying crypto and fiat values (amount of decimals, prepending with fiat symbol) - should be part of the `domain` + + +## Data management + +Each layer has logic and data. This section clarifies which data belons to which layer. + +### Gateway data +Here goes data related to the **mechanics** of interaction with outer services: +- connection states +- timeouts values +- current page in paginated request + +If outer service does not respond and we need to make two more tries to reach it before call it "unreachable", the counter of tries should be treated as `gateway` data. + +### Domain data +Here goes all the data related to knowledge domain. Distinctive feature of this data is that it is "persistent" which means it stays actual even if we restart the application. +Wallet part of our application works with user profiles, accounts, balances. When we restart the application all of them stay actual. On contrary with user flow data which could be safely discarded when app closed. + +If user had 4 accounts in wallet and 10 saved contacts in chat they should stay intact after app reloading. `accounts` and `contacts` are concepts from our knowledge domain, plus they are presistent - that means it is a `domain` level data. + +Note: There are other types of data that should survive data restart. For example user visual settings like light/dark mode. +This data belongs to `UI` layer because it would survive app restart but wouldn't survive rewriting our mobile app to console app. + + +### Application data +`Application` layer data contains the state of current "user flow". +This data can be safely discarded after user has finished the flow. + +If user entered some chat we store `current-chat` as piece of `application` data. +If user is in "send crypto" flow that consist of few consecutive screens, the selections made on all screens (account, token, receiver address) is stored as `application`-layer data. + +### UI data +`UI` data contains UI settings or states of the particular screen. + +User can switch between fiat and crypto on `enter amount` screen of `send crypto` user flow. But the state of that toggle does not influence any other screens, it is solely UI feature and should be stored under `UI` layer. + +### re-frame db organization + +Data stored in re-frame db should be normalized, which means: +- each entity is stored only once +- relationships between entities handled using IDs +- redundancy is minimized + +`rf-db` keys names should have the structure: `:layer.context/entity`. Where `layer` is a logical layer where data belongs. `context` is an area of application and `entity` is a name of stored data. + +Layer names are shortened for convenience `application`->`app`, `gateway`->`gate`. + +From previous examples: +- last page fetched from `FetchMarketTokenPageAsync` call to `status-go` can be stored in rf-db under the name `:gate.market/last-fetched-page`. +- token prices extracted from status-go signal payload could be stored under the name `:domain.market/prices` +- `:app.wallet/send-flow` can contain structure the whole state of the "send-flow" - selected account, selected token, destination address + + +## Communications between layers + +![layers](images/layers-relationships.png) + +- `Domain` layer does not communicate with any other layer and not aware about their existence +- `Application` layer makes calls to `Domain` via events or functions +- `Application` layer makes calls to `UI` via events or functions +- `UI` layer makes calls to `Application` layer via events or functions +- `UI` layer uses subscriptions defined in `Domain` layer +- `UI` layer uses subscriptions defined in `Application` layer +- `UI` layer uses subscriptions defined in `UI` layer + + +### Subscriptions +Subscriptions can be declared on `Domain`, `Application`, or `UI` level. +- `Domain` layer subscriptions provide access to domain data (most likely without any data transformation) +- `Application` layer subscriptions provide access to state of the current user flows +- `UI` layer subscriptions provide access to ui screen states and also consumes `domain` and `application` layer subscriptions to extract data needed for particular ui elements. + +Subscriptions are named using `:layer.context/entity` pattern. + +### Examples: +Domain-level subscriptions to access normalized tokens data: +```clojure +(rf/reg-root-key-sub :domain.market/leaderboard :domain.market/leaderboard) +(rf/reg-root-key-sub :domain.market/prices :domain.market/prices) +(rf/reg-root-key-sub :domain.market/tokens :domain.market/tokens) +``` + +UI-level subscription that consumes domain data and "massages" it for UI: +```clojure +(rf/reg-sub + :ui.market/token + :<- [:domain.market/prices] + :<- [:domain.market/tokens] + :<- [:domain.market/leaderboard] + (fn [[token-prices token-details leaderboard] [_ token-id]] + (let [{:keys [current-price price-change-percentage-24h market-cap]} (get token-prices token-id) + {short-name :symbol name :name image :image} (get token-details token-id) + position (get leaderboard token-id)] + {:price (wallet.utils/prettify-balance "$" current-price) + :percentage-change (utils.number/naive-round price-change-percentage-24h 2) + :market-cap (utils.number/format-number-with-si-suffix market-cap) + :short-name (when short-name (string/upper-case short-name)) + :token-rank position + :token-name (utils.string/truncate name 18) + :image image}))) +``` + + + + +### Events +Events are named using `:layer.context/entity` pattern. + +#### Example +```clojure +(rf/reg-event-fx :app.market/close-leaderboard + (fn [{:keys [_db]}] + {:fx [[:dispatch [:gate.market/stop-fetching-tokens]] + [:dispatch [:domain.market/clear-market-data]]]})) +``` + diff --git a/doc/codebase-structure-and-guidelines.md b/doc/codebase-structure-and-guidelines.md new file mode 100644 index 00000000000..e4ec779c4ce --- /dev/null +++ b/doc/codebase-structure-and-guidelines.md @@ -0,0 +1,114 @@ +# Codebase Structure and Guidelines + +As of Nov/2022, we're starting to document and migrate the codebase to [new +guidelines](new-guidelines.md), which will supersede some of the guidelines in +this document. + +## Starting the app + +The app is bundled and run in a Nix environment. Information regarding how to run it locally can be found [here](https://status.im/technical/build_status/). + +## Business Logic Modules + +Each business logic module is managed in a module directory. + +**Only the core and db namespace of a module can be required outside of the module directory. Other namespaces must never be required outside of the module directory** + +*There is no rigid structure on how to organize code inside modules outside of core and db namespaces* + +```txt +- events.cljs +- subs.cljs +- notifications + - core.cljs +- init + - core.cljs +- accounts + - core.cljs + - db.cljs +- node + - core.cljs + - db.cljs + - config.cljs +- ui + - screens + - login + - views.cljs +``` + +Rationale and how to get there: + +Currently a lot of business logic is lost in utils and screens namespaces and should be moved to dedicated top level directories. +This should make core logic of the app more accessible and organized + +### core.cljs + +Core namespace must only contain functions that can be called outside of the module (i.e. in events or other modules): + +- fx producing functions called by events and other modules + + ```clojure + (def get-current-account + module.db/get-current-account) + + (defn set-current-account [{db :db :as cofx}] + {:db (module.db/set-current-account db)}) + ``` + +### db.cljs + +- must contain getter and setter functions used by fx producing functions and subscriptions +- db logic called by other modules + +Rationale: + +These guidelines make db.cljs namespaces the place to go when making changes to the db layout and minimize breaking changes when adding/refactoring features + +## re-frame + +### Subscriptions + +- all subscriptions must be defined in the single `status-im.subs` namespace +- subscriptions must subscribe only on other subscriptions, and never on app-db itself +- `reg-root-key-sub` should be used for the root keys subscriptions + +### Events + +- all events must be defined in the `core.cljs` namespaces +- events must always be declared with `fx/defn` macro + +## Testing flow +- All PRs automatically go to "REVIEW" column on [Pipeline for QA](https://github.com/status-im/status-mobile/projects/7) project on Github. This is our main board for QA / devs interaction +- After the PR gets at least 1 approval, it should be moved to "E2E Tests" column. Some PRs may need approvals from more than one person. +- Critical path tests are automatically run for all PRs in "E2E Tests" column. +- If E2E tests pass: + - If PR doesn't need manual QA (it is small change or change doesn't relate to app) - after passing e2e tests it can be merged (don't use the Github UI to merge, check section on merging PRs). + + - If manual QA needed - add label 'request manual qa'. The QA team will process it (make sure the summary and test instructions are clear). QA team will review results and test PR on different platforms and assign one of the three labels: + - No issues - label 'TESTED-OK' and then you can merge it. + - If issues - label 'TESTED-ISSUES' and comment from QA with bugs. + - After fix and/or discussion the process is repeated. + + - If manual QA is not needed but all tests don't pass, you can ping @churik or @Serhy to confirm that failed E2E tests are not unrelated. + +## Translations +The app relies on system locale to select a language from the [list of supported languages](https://github.com/status-im/status-mobile/blob/bda73867471cf2bb8a68b1cc27c9f94b92d9a58b/src/status_im/i18n_resources.cljs#L9). It falls back to English in cash the system locale is not supported. + +We use Lokalise App to manage [translations](https://translate.status.im/). In case you need to add/remove a key to translations, you only need to change `en.json`. Missing keys fallback to `en.json`. The actual translations will be added by Lokalise. + +## Merging approved PRs + +We don't Github's UI to merge. Instead `./scripts/merge-pr.sh` is used to sign and merge PR to `develop`. You first need to enable [GPG signing on you commits](https://github.com/status-im/status-mobile/blob/develop/STARTING_GUIDE.md#configure-gpg-keys-for-signing-commits). + +Once your commits are verified and PR approved, you can run the script like so: + +```bash +$ git checkout develop +$ git fetch +$ git reset --hard origin/develop +$ ./scripts/merge-pr.sh 11370 +``` +Replace 11370 with your PR id. + +## Release process +TODO(shivekkhurana): Compile release notes diff --git a/doc/debugging.md b/doc/debugging.md new file mode 100644 index 00000000000..a9fde18b011 --- /dev/null +++ b/doc/debugging.md @@ -0,0 +1,169 @@ +# Debugging + +## Inspecting re-frame with re-frisk +`re-frisk` is a state visualization tool written by our very own Andrey (@flexsurfer). With its help you can inspect the current state of app-db, watch event, etc. + +![re-frisk](images/debugging/re-frisk.png) + +To start `re-frisk`, execute the following command: +```bash +$ yarn shadow-cljs run re-frisk-remote.core/start +``` + +or you can also use make: + +```bash +$ make run-re-frisk +``` + +A server will be started at http://localhost:4567. It might show "not connected" at first. Don't worry and just start using the app. The events and state will populate. + +More details about re-frisk are on the [project page](https://github.com/flexsurfer/re-frisk). + +## Debugging with FlowStorm + +[FlowStorm](http://www.flow-storm.org/) is a free and open-source time-travel +debugger for Clojure and ClojureScript. It is capable of recording almost any +code execution path in the Status' mobile app. + +FlowStorm is independent of editor/IDE, but a tighter [integration with +Emacs](https://github.com/jpmonettas/cider-storm) is available. + +If you are developing with Android, run `make android-ports` first. If you are +behind a firewall, allow TCP traffic on port `7722`, then: + +```bash +make run-flow-storm +``` + +You will know FlowStorm is correctly running if you see the status bar with +green `REPL` and `RUNTIME` indicators. + +With FlowStorm running, you can now add `#trace` on any top-level form, reload +the code, or evaluate the traced form in the REPL, and FlowStorm will start +recording data. Avoid `#rtrace` (with `r`) because it can cause massive spikes +in CPU which will make the app hang and you might need to forcefully kill +processes. + +```clojure +#trace ; => Add this line to tell FlowStorm to instrument the function. +(defn say-hello [] + (str "Hello world")) +``` + +There are other ways to debug, but this is a good start. + +## Enabling debug logs +Calls to `log/debug` will not be printed to the console by default. It can be enabled under "Advanced settings" in the app: + +![Enable Debug Logs](images/debugging/log-settings.png) + + +## Checking status-go logs +While status mobile works it saves logs from `status-go` to `geth.log` file. + + +### Checking logs from physical device +To obtain `geth.log` from physical device you need to shake it and in an opened menu select "Share logs". + +![Share logs](images/debugging/share-logs.jpeg) + + +### Checking logs from iOS Simulator +When developing with iOS simulator it is more convenient to see the `geth.log` updates in real-time. +To do this: +- open Activity Monitor +- find the "StatusIm" app and doubleclick it +- in the opened window select "Open files and ports" and find the full path to `geth.log` (note that it won't appear until you login to Status app) + +![geth.log path](images/debugging/geth-path.png) + +## Inspecting database content + +Encrypted database can be found using commands: +``` +cd ~/Library/Developer/CoreSimulator/Devices +find ./ -name accounts.sql +``` + +To get unencrypted database you need to export it first: +- open the status app in simulator +- on login screen enter the correct password without logging in +- using repl execute statement to export db: + ``` + (re-frame.core/dispatch [:multiaccounts.login.ui/export-db-submitted]) + ``` +- save generated `export.db` file + +Now you can locate the `export.db` and open it with preferred db viewer. + +**Android:** + +```sh +adb root +adb pull /storage/emulated/0/Android/data/im.status.ethereum.debug/files/Download/export.db /path/to/store/export.db +``` + +If you're using a release build, change the path to `/storage/emulated/0/Android/data/im.status.ethereum/files/Download` + +**iOS** + +``` +cd ~/Library/Developer/CoreSimulator/Devices +find ./ -name export.db +``` + + + +## Tips +### From @ilmotta: + +Something I find extremely convenient for Android is to use `adb` to tail logs. I don't use macOS so I don't know if the iOS simulator offers a CLI interface with the same capabilities. + +But here's what I use for example: + +``` + adb shell tail -n 10 -f /storage/emulated/0/Android/data/im.status.ethereum.debug/files/Download/geth.log | grep 'waku.relay' +``` + +Also to inspect logs in a more flexible manner, instead of the strict output from `make run-metro`, I prefer `adb logcat`. Combined with enabling status-mobile logs in debug by default plus filtering the logs to only what I care during development, I find this helps me inspect the app without running re-frisk because with the debug log level I can already see which events are dispatched (one of the features I like the most from re-frisk). + +``` +adb logcat | grep 'ReactNativeJS\|StatusModule\|GoLog' +``` + +### From @clauxx (visual debugging): + +When building/fixing/refactoring components, it can be annoying to set-up the component for instant visual feedback (i.e. finding it in the UI when fixing/refactoring; finding a place for it when building from scratch). Very often if the location of the screen containing your component nested deeply in the app, you might have to do a lot of repeated navigation, nevermind logging in every time the app reloads (even with fast refresh disabled this happens quite often). + +To alleviate this discomfort and make the development cycle more *pleasant*, you can use one of the following options (*_both of which involve the ... REPL ༼ つ ╹ ╹ ༽つ, so make sure you have that set-up with your IDE/editor_*) + +#### REPL-ing to a screen + +This one is straightforward, just evaluate a navigation dispatch form from anywhere: + +```cljs +(comment (rf/dispatch [:navigate-to :your-favorite-buggy-screen])) +``` + +> TIP: you can run this from an untracked user namespace `(ns user)`, from which you can experiment or interact with the REPL. This is where I usually keep my development navigation forms. + +#### REPL-ing a component + +This is also straightforward, but there are some small differences. Just like above, we only have to evaluate a dispatch form, as follows: + +```cljs +;; example debugging a quo component, although it could be any other component e.g. from the `status-im2` ns. +(comment (re-frame/dispatch [:dev/preview-component + [quo/slide-button + {:track-icon :face-id + :track-text "preview component example" + :customization-color :blue + :on-complete identity}]])) +``` + +When evaluated, a full-screen bottom sheet will appear with your component inside. You can make changes to the props or the component internals and re-evaluate the changed forms, after which the component will be re-mounted without running a reload (unless you save the file). Other times it's quicker to just hit save and the component will be reloaded as usual (except for the props you evaluated, which have to be re-evaluated when changed). + +> TIP: You can have multiple versions of the component as separate comments to check for the component variations + +> TIP: You can leave `comment` blocks at the end of component files (other than quo, since can be previewed in other ways) with usages of components, which other developers can use for visualisation/testing. diff --git a/doc/decisions/0003-geth-node.md b/doc/decisions/0003-geth-node.md index 83a9ad1e1f8..1f663c5d957 100644 --- a/doc/decisions/0003-geth-node.md +++ b/doc/decisions/0003-geth-node.md @@ -49,7 +49,7 @@ decentralization and security. Q2 we will provide two ways of accessing the Ethereum network: - Upstream RPC with Infura (default) -- Custom upstream node (https://github.com/status-im/status-react/issues/3994) +- Custom upstream node (https://github.com/status-im/status-mobile/issues/3994) From a decentralization point of view we need the latter before we can release the beta. diff --git a/doc/decisions/0004-disable-group-chats.md b/doc/decisions/0004-disable-group-chats.md index 3b7bd0c198b..6bbec4aec96 100644 --- a/doc/decisions/0004-disable-group-chats.md +++ b/doc/decisions/0004-disable-group-chats.md @@ -20,11 +20,11 @@ chance to talk to strangers and it is open by default. Whether group chats are in scope or not has been a recurring discussing and a lot of effort has been wasted related to this. -There are currently a lot of outstanding regressions related to group chat: https://github.com/status-im/status-react/labels/group-chat Additionally, since group chats are private the encryption and security of those are generally harder than 1-1 and public chat. +There are currently a lot of outstanding regressions related to group chat: https://github.com/status-im/status-mobile/labels/group-chat Additionally, since group chats are private the encryption and security of those are generally harder than 1-1 and public chat. ## Decision -Disable group chat for beta and don't work on bugs related to it until after. See https://github.com/status-im/status-react/issues/3995 +Disable group chat for beta and don't work on bugs related to it until after. See https://github.com/status-im/status-mobile/issues/3995 This ensures we can release beta without blockers, and then we can take some time to fix group chats properly. diff --git a/doc/decisions/0007-masking-sensitive-data.md b/doc/decisions/0007-masking-sensitive-data.md index f7e55495bf8..14518570b0f 100644 --- a/doc/decisions/0007-masking-sensitive-data.md +++ b/doc/decisions/0007-masking-sensitive-data.md @@ -13,13 +13,15 @@ Proposed We have some data that we don't want to appear in the logs (user passwords are a good example). Currently, they are passed around as strings, that could be -printed out by mistake in a log entry (see https://github.com/status-im/status-react/issues/4053) +printed out by mistake in a log entry (see https://github.com/status-im/status-mobile/issues/4053) ## Decision To minimize the risk of leaking passwords through logs, we should not pass passwords as strings in our codebase. We introduced a new type `MaskedData` in `status-im.utils.security`. +update (16-Dec-2022) `status-im.utils.security` is now moved over to `utils.security.core` + We use `(security/mask-data ` to wrap sensitive data into this type and then use `(security/unmask )` to get the plaintext back. diff --git a/doc/decisions/0009-release-process-mobile.md b/doc/decisions/0009-release-process-mobile.md index 9fb5bb80805..fb0aba66232 100644 --- a/doc/decisions/0009-release-process-mobile.md +++ b/doc/decisions/0009-release-process-mobile.md @@ -95,7 +95,7 @@ We also have an ongoing document with them. #### iOS Test Flight Release **Upload to AppStore Connect** -Use [this Jenkins job](https://jenkins.status.im/job/status-react/job/upload_release_ios/) +Use [this Jenkins job](https://jenkins.status.im/job/status-mobile/job/upload_release_ios/) **“What to test” field** When AppStore Connect asks you to fill in the field called “what to test”, just copy the release notes there. @@ -114,7 +114,7 @@ If reviewed successfully, we can share it to our users by adding “testflight-b The uploaded release is **immediately available**! Do it only if the iOS build is approved by Apple! -Use [this Jenkins job](https://jenkins.status.im/job/status-react/job/upload_release_android/) +Use [this Jenkins job](https://jenkins.status.im/job/status-mobile/job/upload_release_android/) Don’t forget to update the screenshots if necessary! diff --git a/doc/decisions/0010-remove-jail-and-status-api.md b/doc/decisions/0010-remove-jail-and-status-api.md new file mode 100644 index 00000000000..2590047a672 --- /dev/null +++ b/doc/decisions/0010-remove-jail-and-status-api.md @@ -0,0 +1,51 @@ +# 0010. Remove jail and Status API + +| Date | Tags | +|---|---| +| Thu Aug 16 | jail, 3rd-party-code, extensions | + +## Status + +accepted + +## Context + +Originally, we supported 3rd party status extensions in form of javascript jailed execution environment (eq Jail) + small js library exposing chat commands API to js code. + +While neat in theory, it has some serious downsides: + +- Design of the API was quite poor, preferring mutable semantics over stateless API leveraging strength of the host application (cljs/re-frame). + As a result of that, more dynamic/state requiring things (like the live tx detail in `/send` command messages) were very hard to do, + so instead of "eating our own dogfood", we decided to side-step the API and implement such things as hard-coded logic in the app, while partly + retaining the js code for "easier" things (like parameter declaration). + Needles to say, such efforts produced code of very poor quality, riddling our app with hard-coded "magic" everywhere in the codebase, completely + defeating the point of "dogfooding" while still requiring more effort and being much more error prone (no way to unit test jail logic) because + of the need to asynchronously communicate with jail for leftover logic in command messages (the parts not hardcoded in app). + +- We were in a state where there was not even one command defined completely in jail, with all of them requiring custom hardcoded logic in app to + actually work. + Due to numerous changes and rush to get things working, half of the API was obsolete and was not working/working differently then described in + the API documentation. + +- We are quite tight stretched regarding performance and the Jail didn't particularly help with that - whenever some command message was defined in + Jail, it required constant RN Bridge "ping-pong" whenever sending the message or receiving the message, eating more resources + breaking otherwise + quite simple logic into harder to reason asynchronous call chains (execute method defined in Jail, wait for the result, execute next method in Jail...). + +- Till now, there was no real interest in 3rd party bots anyway - the only "real" DAPPs submitted to status were "normal" DAPPs utilising just the `web3.js` + API and built on top of regular web technologies, so they could be ran in any Ethereum client like Metamask or Mist, besides Status. + +- There is a very promising new concept of extensions (project pluto), which will enable to extend status with much nicer and declarative extension + definitions. + Such extensions will not be able to only hook-in into the command messages logic, but also other application (host) hooks, like wallet assets/collectibles, + chat contacts, etc. + +## Decision + +In the light of points above, we decided to remove Jail and `Status.js` API from the application and re-work command messages in set of nicely +encapsulated app level protocols, so command related logic will be defined in one place, commands will be able to leverage strength of our platform +and development of any new functionality related to them will be much faster and more pleasant. +We will address the 3rd party code issue separately by extensions. + +## Consequences + +`Status.js` is deprecated and there is no way to define custom commands using javascript only. diff --git a/doc/decisions/0011-tweak-pr-process.md b/doc/decisions/0011-tweak-pr-process.md new file mode 100644 index 00000000000..30ff27e8978 --- /dev/null +++ b/doc/decisions/0011-tweak-pr-process.md @@ -0,0 +1,46 @@ +# 0011. Tweak PR process + +| Date | Tags | +|---|---| +| 2019-02-26 | process, pull-requests | + + +## Status + +accepted + +## Context + +There was a generally dissatisfaction with our PR flow process from multiple stakeholders, including devs, QA and design. These largely centered around size, speed of integration and quality of PRs. + +For more details, please see pain points in the meeting notes held end of February here: https://notes.status.im/C5pj8g7gQOu9Wo8PtDZsMw?edit# as well as the preceding Discuss thread: https://discuss.status.im/t/better-pull-requests-process/1044 + +Also see conversations in Core Dev Call #12 and #13: https://github.com/status-im/pm/ + +## Decision + +These specific policy changes were agreed upon (a la 'rough consensus' model). These are heuristics, and should be more of a guide than strict rules. Exception to these should be minimal though. + +**1. Reject PRs that don't have tests** +As a rule of thumb, coverage should increase. + +**2. Don't use or promote nightly as something for end users.** + +**3. Reject PRs that don't write what tests the author have done and what platforms you used to test, including screenshots, videos or logs** +This means QA and dev have to spend less time catching obvious problems. + +**4. Run tests when a PR is at "Review" stage on https://github.com/status-im/status-mobile/projects/7 (or add new 'e2e stage' between review and `TO TEST`)** + +**5. Dev asks for 2-3 reviewers, including a designer if the change affects UI, and these reviewers review within a day.** +This ensures we don't have *diffusion of responsibility* and that PRs are reviewed quickly. + +## Consequences + +This means we need to some form of release schedule up for Desktop. It also means we need to standardize tests, at least on Clojure side. + +Additionally, the following two medium term projects can't be fixed immediately but will be started: + +- Look into integration tests, headless tests. Andrea is looking into. +- Look into iOS automated test and more platform. Anton will look into. + +Follow up on this will happen at a later Core Dev call. diff --git a/doc/decisions/0012-using-pivotal-tracker.md b/doc/decisions/0012-using-pivotal-tracker.md new file mode 100644 index 00000000000..17f0ab52f33 --- /dev/null +++ b/doc/decisions/0012-using-pivotal-tracker.md @@ -0,0 +1,48 @@ +Using Pivotal Tracker +=== + +## Abstract + +The Core Improvements swarm tracks its workflow in a Pivotal project. Although GitHub items can still be created to facilitate communication, the official way to track progress of a functionality/bug fix is in Pivotal. + +[Project board link](https://www.pivotaltracker.com/n/projects/2232205) + +## Creating Pivotal Stories + +Fill in `Story Type`, `Requester`, `Owner` (if already assigned), and `Points` (if an estimate is available). Add a description and optionally group it using a label. As a rule of thumb, Pivotal stories should be grouped in epics for better high-level visibility. Although labels can be used to group stories, the prescribed way to do that is through epics. + +You can add tasks under a Story for smaller items that don't warrant its own Story. + +Do add the link to the Pull Request to the Story when one is available. + +## Lifetime of a Pivotal story + +The `State` field in the Pivotal story is used to track the progress of a Pivotal Story, and has the following interpretation: + +| State | Meaning | +| ---------- | -------- | +| `Unstarted` | In backlog | +| `Started` | Developer has started work on story | +| `Finished` | Developer has finished work on story (Pull Request for story is in [REVIEW](https://github.com/status-im/status-mobile/projects/7#column-1843024) column in [Pipeline for QA](https://github.com/status-im/status-mobile/projects/7) board) | +| `Delivered` | Pull Request for story is in testing in [Pipeline for QA](https://github.com/status-im/status-mobile/projects/7) board) board | +| `Accepted` | PR has been merged | +| `Rejected` | PR has been aborted | + +### Rules of thumb + +- **Q:** _When to create a Pivotal story?_ + **A:** Stories should be created whenever estimated work exceeds half a day, or when part of a bigger epic. + +- **Q:** _What do points equate to?_ + **A:** We've agreed to make 3 points equate roughly to 1 day of work. From there, we can extrapolate: + + | Points | Estimated Time | + | -------- | ---------------- | + | 0 points | < 1 hour | + | 1 points | 1 hour - 0.5 days| + | 2 points | 0.5 - 1 days | + | 3 points | ~1 day | + | 5 points | 2 - 3 days | + | 8 points | ~1 week | + + We should avoid 8 point stories by breaking them down into smaller stories as much as possible. diff --git a/doc/decisions/0013-tribute-to-talk.md b/doc/decisions/0013-tribute-to-talk.md new file mode 100644 index 00000000000..744205417a9 --- /dev/null +++ b/doc/decisions/0013-tribute-to-talk.md @@ -0,0 +1,113 @@ +# 0013. Tribute-to-Talk + +| Date | Tags | +|---|---| +| 2019-04-02 | spam filtering, blocking, chat | + + +## Status + +proposed + +## Context + +The whitepaper describes Tribute to Talk (TtT) as an economics-based anti-spam filter for receiving messages and "cold" contact requests from user. Stakeholders would stake a minimum amount of SNT that another Status stakeholder must deposit in order to contact him directly. +The whitepaper also proposes that the deposit is only forfeited to the recipient if he replies. + +## Decision + +Considering: +- the absence of efficient ways to perform anonymous transactions (zk-snarks could be used in the future for that) +- the impossibility to prove that a recipient has made an actual reply and not some kind of automated reply (captcha solution was proposed, but wouldn't be practical until we can use a solution such as swarm feeds that allow users to make free updates to their captcha without on-chain transactions) +- the limited time to develop the feature + +We opted for a solution that: +- doesn't establish a direct correlation between the tribute payment and an on-chain transaction, so it +- uses regular transactions for tributes payment, giving users plausible deniability about the nature of the payment +- doesn't store tribute information on-chain + +## Manifests + +Since TtT related information aren't stored on chain, they need to be stored somewhere else. +For the first iteration of TtT we opted for IPFS, which is already used across the app for other forms of content, through Infura IPFS gateway. + +On IPFS we store what we call a ttt manifest, which is a json file with the following format: + +```json +{ + "tribute-to-talk": + "snt-amount": "snt amount in wei that a user has to pay as tribute", + "message": "a personalized message to display to users trying to contact stakeholder" +} +``` + +When a user sets TtT in his profile, the manifest contenthash is added to the registry contract (which requires a transaction). +The TtT contract points ethereum addresses to contenthash manifests. In the current implementation, the manifest is not encrypted, but we think it would be a privacy improvement to have symmetric key encryption for the manifest using the user public-key. That way, parsing the contract event log is useless to collect TtT manifests because there is no way to find the public key from the address, but legit users will know the public-key already. + +### Future implementation + +We were considering Swarm feeds to upload manifests but we were worried about the current instabilities, particularly a bug that was returning stale reads very often and made it unsuitable for our usage. + +Swarm feeds are the ideal solution as they allows users to set a manifest without any transaction which makes the registry contract completely obsolete. +The format of the manifest will stay the same, but could be extended with other top-level keys to serve as a public profile as well. + +## Implementation + +### Checking manifest + +In status app, we check a user manifest when: +- the app starts (user checks his own manifest) +- a user's profile is opened +- a chat with a user is opened +If there is no match in the registry for the user address or if the manifest is not available, the client will consider that there is no tribute to talk set for the user. + +Relevant code is located in `status-im.tribute-to-talk.core` ns + +### Whitelisting rules + +A is whitelisted by B: +- A received a contact request from B +- A has paid the tribute to B +- B has paid the tribute to A + +A whitelists B if: +- A sent a contact request to B +- A has paid the tribute to B +- B has paid the tribute to A + +### Paying tribute + +The tribute payment is a regular SNT transaction greater or equal to the value of the snt-amount key in the manifest. + +The id of the transaction is sent along messages in the payload top level `:tribute-transaction` key as long as the sender isn't sure that he is whitelisted by the recipient (see `whitelisting rules` above). + +The UI in status app waits for 1 confirmation on chain before allowing the user to send a message to ensure the recipient will see the payment and not ignore the message. + +## Upgradability + +The registry contract has a `stopped` attribute that needs to be checked on start-up by clients. If the contract is stopped, this means that a new version of tribute to talk that is incompatible with the current one has been released. + +Old clients MUST check if the tribute to talk contract is stopped and MUST disable tribute to talk if it is. +Old clients MUST warn the user when they open a chat with a user that may have not whitelisted them that their messages will be ignored if the recipient has tribute to talk enabled. + +New client will be using a different contract + +## Consequences + +### Data collection of manifests' data + +- if we don't encrypt the manifests with the users public key, the registry contract logs can be used to collect manifest contenthashes which can then be downloaded and the data aggregated +- SNT transactions could then be correlated to have an estimate of which SNT transactions are like to be tributes payments (it would give the "attacker" an additional filter on transaction amount, transaction recipient being already known through the registry contract without the manifest) +- this would not be possible with swarm feeds as there wouldn't be a registry contract to look at. + +### Impact of token transactions +- currently there is a wallet limitation that limits the token transactions list to the past 2 weeks, this means that if a user pays and doesn't send any message after the payment for 2 weeks, he will silently be ignored. +- this will be solved by fixing the transaction list in the wallet + +### Edge cases (implementation specific) + +- if A and B have a conversation, and B suddenly sets TtT on, if B didn't add A as a contact, A messages will be filtered and A will not now it until he leaves and returns to the chat + +### Switching networks + +When switching between networks, the app uses the settings on the current network. This means that if one has TtT enabled on Mainnet and goes on Testnet, and one has no TtT settings on Testnet, TtT will be considered disabled and no messages will be filtered. diff --git a/doc/decisions/0014-wallet-team.md b/doc/decisions/0014-wallet-team.md new file mode 100644 index 00000000000..ae014bb3167 --- /dev/null +++ b/doc/decisions/0014-wallet-team.md @@ -0,0 +1,30 @@ +# 0014. Team-structure + +| Date | Tags | +|---|---| +| 2023-06-16 | wallet team | + +## Status + +Proposed + +## Context: + +In order to try out vertical team, which is a structure that is wanted by the company, we will be creating a wallet team within the mobile team. +Some member of the mobile team will join the current wallet team, which is composed by developers from desktop and lead by @alaibe, on a voluntary basis. +The responsibilities of the team are to implement the new wallet UI, with the support of the current wallet team, which will be assisting with the status-go work. + +## Decision + +The team members for the initial wallet team are: + +- @rasom +- @OmarBasem +- @vkjr +- @J-Son89 +- @briansztamfater +- @smohamedjavid +- @ulisesmac +- @erikseppanen +- @mmilad75 +- @tumanov-alex diff --git a/doc/decisions/0015-team-structure.md b/doc/decisions/0015-team-structure.md new file mode 100644 index 00000000000..8bbceee5361 --- /dev/null +++ b/doc/decisions/0015-team-structure.md @@ -0,0 +1,46 @@ +# 0015. Team-structure + +| Date | Tags | +|---|---| +| 2023-06-16 | changes in team structure | + +## Status + +Proposed/partially implemented + +## Context: + +Currently, there is only a single team lead in the mobile development team, and after a meeting with the design team, it has been identified that the mobile app lacks polishness. One of the main issues identified is a lack of oversight in certain areas. To address this problem, it has been decided to create an additional role within the team. For each section of the mobile app (communities, syncing & onboarding, activity center & shell, wallet, messaging), a person will be selected to fulfill the responsibilities outlined below. + +## Decision + +The decision is to create an additional role within the mobile development team for each section. The individuals selected for these roles will have the following responsibilities: + +- Ensure GitHub issues are created for new features/designs of the section. +- Ensure issues are adequately described and linked as best as possible. +- Ensure issues have a grooming session where other developers can provide their feedback before working on the set of issues. +- Ensure all relevant parties are invited, such as designers, QA, Product Owner, leads, etc., as needed (including the designer who created the feature). +- Ensure there is consensus among the developers working on the section about the decided strategy approach. Ideally, this consensus should be documented. +- Ensure UI quality by organizing designer reviews and guaranteeing pixel perfection. +- Ensure overall implementation is satisfactory based on the predefined requirements. +- Ensure alignment between desktop, web, and mobile versions for the respective section in collaboration with the mobile developers and design team. +- Act as a point of contact for the design team with respect to the section. +- Ensure that someone goes through the designs once they are created with the respective designer. +- Assist the design team with technical feasibility analysis and address any product concerns. +- Facilitate the growth of the next team member into the role. + +The people identified for each sections are as follow: + +- Messaging @flexsurfer, followed by @alwx +- Syncing & Onboarding @siddarthkay +- Communities @ilmotta, followed by @ajayesivan +- Activity Center & Shell @J-Son89, followed by @Parveshdhull +- Wallet (once the vertical team is created) @J-Son89, followed by @briansztamfater + +This will be a rotating post, every 4 months it will go to the next in line. + +The wallet team might have longer times for rotation, as stability might be more important. That's still to be decided. + +# Follow-up Actions + +- Find suitable next in line for Messaging, Onboarding, Communities diff --git a/doc/decisions/UI-related/7-august-2023-paste-button-ux.md b/doc/decisions/UI-related/7-august-2023-paste-button-ux.md new file mode 100644 index 00000000000..122f1f50263 --- /dev/null +++ b/doc/decisions/UI-related/7-august-2023-paste-button-ux.md @@ -0,0 +1,31 @@ +### How should the paste button behave when clipboard is empty? + +This question first came up as a review comment to this PR +https://github.com/status-im/status-mobile/pull/16852 + +There were 2 considerations : +- Either keep the paste button disabled when there is nothing in the clipboard OR +- Always keep the paste button enabled + +There were positives and negatives for both approaches. + +Positives of keeping paste button disabled when there is nothing in clipboard would require us to +check the value of clipboard as soon as the component is mounted (i.e when the user first sees +the screen). In iOS this means a native permissions dialog would appear requesting for permissions +to paste from the clipboard. + +Negatives of this approach is that as soon as any user navigates to this screen they are greeted with +this popup which can be annoying sometimes. + +Positives of keeping paste button always enabled is that we can trigger a request to the clipboard on +tap of the paste button which would trigger the native permissions dialog requesting for permissions +to paste from the clipboard. +In this case seeing this dialog is okay because the user has initiated a paste action. + +Negatives of this approach is that in the event the clipboard is empty the user will still see the +system dialog and on approving nothing will be pasted (because the clipboard was empty). +This behaviour can be confusing. + +On consulting the Design Team via discord it was concluded that out of the two approaches +having the paste button always enabled is a better UX overall. + diff --git a/doc/dependencies.md b/doc/dependencies.md new file mode 100644 index 00000000000..9bb6deee3f7 --- /dev/null +++ b/doc/dependencies.md @@ -0,0 +1,156 @@ +## "@react-native-community/netinfo": "^3.2.1" + +no known issue while updating version + +used to monitor network information (connected, expensive, connection type) + +## "bignumber.js": "git+https://github.com/status-im/bignumber.js.git#v4.0.2-status" + +used to work with big numbers in JS +BN.js (used in web3-utils) doesn't support decimals, though we could work without decimals + +should be replaced by JSBI https://github.com/GoogleChromeLabs/jsbi/blob/master/README.md +see web-utils issue: https://github.com/ethereum/web3.js/issues/2171 + +## "buffer": "^5.4.2" + +required by bignumber.js otherwise there is a compilation error + +## "chance": "^1.1.0" + +used to generate guids and random values in `status-im.utils.random` + +## "emojilib": "^2.4.0" + +used for emojis + +## "eth-phishing-detect": "^1.1.13" + +used to check if URL is known for phishing + +## "hi-base32": "^0.5.0" + +used only in `status-im.browser.core` to convert base58 hash into base32 hash +TODO: could be replaced by a simple implementation of ascii to base32 + +## "i18n-js": "^3.3.0" + +used to manage translations + +## "identicon.js": "git+https://github.com/status-im/identicon.js.git#v1.2.1-status" + +TODO: switch to latest upstream version, fork is seriously outdated and changes have been merged to master long time ago +used to generate identicons + +## "qrcode": "^1.4.1" + +used to generate QR code + +## "react": "16.8.3" + +react library, update based on react-native recommendation + +## "react-dom": "16.4.2" + +react library, update based on react-native recommendation + +## "react-native": "git+https://github.com/status-im/react-native.git#v0.59.10" + +react-native + +TODO: forked because of specific markdown handling, should be bountied to be turned into a library so we can go back to upstream version + +## "react-native-background-timer": "^2.1.1" + +used to have setTimeout and setInterval that don't trigger yellow warning about long timeouts + +## "react-native-camera": "git+https://github.com/status-im/react-native-camera.git#v1.1.5-1-status" + +used to read QR code and take profile picture +TODO: update version, this one doesn't work with Android (done in react-native upgrade branch) + +## "react-native-config": "git+https://github.com/status-im/react-native-config.git#0.11.2-1" + +used to fetch config values from env +TODO: not sure fork usage is justified + +## "react-native-dialogs": "^1.0.5" + +used to show dialogs but only in android in list selection `status-im.ui.components.list-selection`, iOS uses action-sheet +TODO: see if we should actually remove it + +## "react-native-fetch-polyfill": "^1.1.3" + +used to be able to put a timeout on fetch request +TODO: check if the fetch timeout is actually aborting the request or only ignoring the callback on the response, if it +doesn't then this is useless and we are better of using a simple seTimeout + +## "react-native-fs": "^2.14.1" + +used for some filesystem related functions, for instance to get no-backup directory in `status-im.utils.platform` + +## "react-native-gesture-handler": "1.3.0" + +library used by react-navigation +use version recommended by react-navigation + +## "react-native-image-crop-picker": "^0.18.2" + +used for profile picture + +## "react-native-image-resizer": "git+https://github.com/status-im/react-native-image-resizer.git#1.0.##-status" + +used for profile picture + +## "react-native-keychain": "git+https://github.com/status-im/react-native-keychain.git#v.3.0.0-status" + +used for storing password when user saves password +TODO: check if fork is necessary, merge with upstream? + +## "react-native-mail": "git+https://github.com/status-im/react-native-mail.git#v3.0.8_status" + +used to send email to support when phone is shaked + +## "react-native-screens": "1.0.0-alpha.22" + +used by react-navigation + +TODO: update once bug is fixed for Android that causes crash when app is back from background after being put in background with hardware backbutton press + +## "react-native-shake": "^3.3.1" + +used to send email to support when phone is shaked + +## "react-native-lottie-splash-screen": "^1.0.1" + +used to make splash screen with lottie animation during initial loading for android, also used as replacement of "react-native-splash-screen" for iOS + +## "react-native-status-keycard": "^2.5.7" + +used for keycard + +## "react-native-svg": "9.7.1" + +used for svg icons, mostly collectibles are using svg +TODO: updating requires a pod update on iOS + +## "react-native-biometrics": "^3.0.1" + +used for biometric authentication on Android and iOS + +## "react-native-webview": "git+https://github.com/status-im/react-native-webview.git#v8.0.7_3" + +used for browser + +## "react-navigation": "3.11.0" + +used for native navigation +fixed because even bugfix version upgrade causes runtime errors with current version of react-native + +## "react-native-draggable-flatlist": "https://github.com/computerjazz/react-native-draggable-flatlist" + +A drag-and-drop-enabled FlatList component for React Native + +## "react-native-shadow-2": "https://github.com/SrBrahma/react-native-shadow-2" + +A shadow component that is consistent across both platforms diff --git a/doc/desktop/DevEnvSetup.md b/doc/desktop/DevEnvSetup.md deleted file mode 100644 index 4b0506df543..00000000000 --- a/doc/desktop/DevEnvSetup.md +++ /dev/null @@ -1,60 +0,0 @@ -# Prerequisites: -lein, node.js v.8 , cmake, Qt 5.9.1 (with QtWebEngine components installed), Qt's qmake available in PATH - -Note: add qmake to PATH via -`export PATH=/clang_64/bin:$PATH` - -Caveats: - - if npm hangs at some step, check the version. If it's 5.6.0, try downgrading to 5.5.1 via `npm install -g npm@5.5.1` - -# To install react-native-cli with desktop commands support: -1. git clone https://github.com/status-im/react-native-desktop.git -2. cd react-native-desktop/react-native-cli -3. npm update -4. npm install -g - -# To setup re-natal dev builds of status-react for Desktop: -1. git clone https://github.com/status-im/status-react.git -2. cd status-react -3. git checkout desktop -4. npm install -5. lein deps -6. ./re-natal use-figwheel -7. ./re-natal enable-source-maps -8. In separate terminal tab: `npm start` (note: it starts react-native packager ) -9. In separate terminal tab: node ./ubuntu-server.js -10. In separate terminal tab: lein figwheel-repl desktop (note: wait until sources compiled) -11. In separate terminal tab: react-native run-desktop - -# Editor setup -Running `lein figwheel-repl desktop` will run a REPL on port 7888 by default. Some additional steps might be needed to connect to it. - -## emacs-cider -In order to get REPL working, use the below elisp code: -``` -(defun custom-cider-jack-in () - (interactive) - (let ((status-desktop-params "with-profile +figwheel repl")) - (set-variable 'cider-lein-parameters status-desktop-params) - (message "setting 'cider-lein-parameters") - (cider-jack-in))) - -(defun start-figwheel-cljs-repl () - (interactive) - (set-buffer "*cider-repl status-react*") - (goto-char (point-max)) - (insert "(do (use 'figwheel-api) - (start [:desktop]) - (start-cljs-repl))") - (cider-repl-return)) -``` - -`custom-cider-jack-in` sets the correct profile for leiningen, and can be run as soon as emacs is open. -run `start-figwheel-cljs-repl` once you already have a cider repl session from the jack-in - -## vim-fireplace -For some reason there is no `.nrepl-port` file in project root, so `vim-fireplace` will not be able to connect automatically. You can either: - - run `:Connect` and answer a couple of interactive prompts - - create `.nrepl-port` file manually and add a single line containing `7888` (or whatever port REPL is running on) - -After Figwheel has connected to the app, run `:Piggieback (figwheel-sidecar.repl-api/repl-env)` inside Vim, and you should be all set. diff --git a/doc/desktop/Troubleshooting.md b/doc/desktop/Troubleshooting.md deleted file mode 100644 index 6d6b4669134..00000000000 --- a/doc/desktop/Troubleshooting.md +++ /dev/null @@ -1,136 +0,0 @@ -These are some common issues you may run into while setting up React Native Qt. - -## Initial setup issues - -### `npm install` hangs -Downgrade to version 5.5.1: `npm install -g npm@5.5.1`. - -### `re-natal` missing -Create a link: -`ln -sf node_modules/re-natal/index.js re-natal` - - -### `react-native run desktop` complaining about missing `qmldir`: -```Command failed: ./build.sh -e "node_modules/react-native-i18n/desktop;node_modules/react-native-config/desktop;node_modules/react-native-fs/desktop;node_modules/react-native-http-bridge/desktop;node_modules/react-native-webview-bridge/desktop;modules/react-native-status/desktop" -Error copying directory from "/path-to-status-react/node_modules/react-native/ReactQt/runtime/src/qmldir" to "/path-to-status-react/desktop/lib/React". -make[2]: *** [lib/CMakeFiles/copy-qmldir] Error 1 -make[1]: *** [lib/CMakeFiles/copy-qmldir.dir/all] Error 2 -make: *** [all] Error 2 -``` -Can be solved by re-running `npm install react-native` which put the `ReactQt/runtime/src/qmldir` file back. - -### Missing web3 package issue - -After last upgrade of react-native-desktop to the v.0.53.3 of original react-native appeared some incompatibility between `react-native` and `web3` packages on npm install. Initially it installed usually fine, but after `react-native desktop` command execution `web3` package is get removed from `node_modules`. Manual install of web3 by `npm install web3` installs `web3` package, but removes `react-native` package. Workaround or solution? - -### Go problem -``` -panic: runloop has just unexpectedly stopped - -goroutine 50 [running]: -github.com/status-im/status-go/vendor/github.com/rjeczalik/notify.init.0.func1() - /path-to-status-react/desktop/modules/react-native-status/desktop/StatusGo/src/github.com/status-im/status-go/vendor/github.com/rjeczalik/notify/watcher_fsevents_cgo.go:69 +0x79 -created by github.com/status-im/status-go/vendor/github.com/rjeczalik/notify.init.0 - /path-to-status-react/desktop/modules/react-native-status/desktop/StatusGo/src/github.com/status-im/status-go/vendor/github.com/rjeczalik/notify/watcher_fsevents_cgo.go:65 +0x4e -events.js:183 - throw er; // Unhandled 'error' event -``` -Related to https://github.com/rjeczalik/notify/issues/139. Solution: re-run. - -## App issues - -### Eth node crashing -`node ./ubuntu_server.js` log: -``` -DEBUG [status-im.utils.handlers:36] - Handling re-frame event: :signal-event {"type":"node.crashed","event":{"error":"node is already running"}} -DEBUG [status-im.ui.screens.events:350] - :event-str {"type":"node.crashed","event":{"error":"node is already running"}} -DEBUG [status-im.utils.instabug:8] - Signal event: {"type":"node.crashed","event":{"error":"node is already running"}} -DEBUG [status-im.ui.screens.events:362] - Event node.crashed not handled -``` -Solution: prevent starting Ethereum local node when there is an instance already running. - -### Reload JS - blank screen -Console log for `react-native run-desktop` shows error 533. -Solution: reload again. Still, might hang at `Signing you in...` step (due to node attempted to be restarted). Re-run Figwheel and `react-native run-desktop` - -### ReactButton.qml non-existent property "elide" error upon startup -``` -qrc:/qml/ReactButton.qml:33: Error: Cannot assign to non-existent property "elide" -"Component for qrc:/qml/ReactWebView.qml is not loaded" -QQmlComponent: Component is not ready -"Unable to construct item from component qrc:/qml/ReactWebView.qml" -"Can't create QML item for componenet qrc:/qml/ReactWebView.qml" -"RCTWebViewView" has no view for inspecting! -``` -Reload JS does not help, restarting Figwheel/react-native might not as well. Restarting Metro bundler solved it for me. - -### After login when several contacts are available: realm errors -1. `attempting to create an object of type 'chat'...` -2. `attempting to create an object of type 'transport'...` -3. Error text containing only the public key. -The realm stack trace follows. - -### Error: spawn gnome-terminal ENOENT -In node server log: -``` -ignoring exception: Error: read ECONNRESET -``` -In react-native log: -``` -./run-app.sh: line 72: 56660 Segmentation fault: 11 /path-to-status-react/desktop/bin/StatusIm $args -events.js:183 - throw er; // Unhandled 'error' event - ^ - -Error: spawn gnome-terminal ENOENT - at _errnoException (util.js:992:11) - at Process.ChildProcess._handle.onexit (internal/child_process.js:190:19) - at onErrorNT (internal/child_process.js:372:16) - at _combinedTickCallback (internal/process/next_tick.js:138:11) - at process._tickCallback (internal/process/next_tick.js:180:9) -``` -or -``` -StatusIm(7924,0x70000c1cd000) malloc: *** error for object 0x7f8b1539bd10: incorrect checksum for freed object - object was probably modified after being freed. -*** set a breakpoint in malloc_error_break to debug -./run-app.sh: line 72: 7924 Abort trap: 6 /path-to-status-react/desktop/bin/StatusIm $args -events.js:183 - throw er; // Unhandled 'error' event - ^ - -Error: spawn gnome-terminal ENOENT - at _errnoException (util.js:992:11) - at Process.ChildProcess._handle.onexit (internal/child_process.js:190:19) - at onErrorNT (internal/child_process.js:372:16) - at _combinedTickCallback (internal/process/next_tick.js:138:11) - at process._tickCallback (internal/process/next_tick.js:180:9) -``` - -### statusgo error during `react-native run-desktop` - -``` -Command failed: build(.)sh -e "node_modules/react-native-i18n/desktop;node_modules/react-native-config/desktop;node_modules/react-native-fs/desktop;node_modules/react-native-http-bridge/desktop;node_modules/react-native-webview-bridge/desktop;modules/react-native-status/desktop" -# github.com/status-im/status-go/vendor/github.com/ethereum/go-ethereum/crypto/bn256 -../vendor/github.com/ethereum/go-ethereum/crypto/bn256/bn256_fast.go:26: syntax error: unexpected = in type declaration -../vendor/github.com/ethereum/go-ethereum/crypto/bn256/bn256_fast.go:30: syntax error: unexpected = in type declaration -# github.com/status-im/status-go/vendor/github.com/ethereum/go-ethereum/crypto/bn256 -vendor/github.com/ethereum/go-ethereum/crypto/bn256/bn256_fast.go:26: syntax error: unexpected = in type declaration -vendor/github.com/ethereum/go-ethereum/crypto/bn256/bn256_fast.go:30: syntax error: unexpected = in type declaration -make[3]: *** [statusgo-library] Error 2 -make[2]: *** [modules/react-native-status/desktop/StatusGo/src/github.com/status-im/src/StatusGo_ep-stamp/StatusGo_ep-configure] Error 2 -make[1]: *** [modules/react-native-status/desktop/CMakeFiles/StatusGo_ep(.)dir/all] Error 2 -make: *** [all] Error 2 -``` - -### inotify errors - -upon running `npm start` on linux, watchman may indicate: "The user limit on the total number of inotify watches was reached" - -This can be fixed by running the below command. Note, changes will only be as valid as the current terminal session. - -``` -echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_user_watches && echo 999999 | sudo tee -a -/proc/sys/fs/inotify/max_queued_events && echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_user_instances && -watchman shutdown-server && sudo sysctl -p -``` - diff --git a/doc/fdroid.md b/doc/fdroid.md new file mode 100644 index 00000000000..1a80c8770b0 --- /dev/null +++ b/doc/fdroid.md @@ -0,0 +1,214 @@ +# Description + +This document describes how to update Status APK builds for the [F-Droid](https://f-droid.org/) Android application catalogue. + +# Intro + +In simplest terms F-Droid requires a YAML file that defines the steps necessary to create a universal unsigned APK build. This is achieved by submitting a new app version into the `metadata/im.status.ethereum.yml` file in the [fdroiddata](https://gitlab.com/fdroid/fdroiddata) repository. + +The app builds defined this way run on servers that generate the unsigned APKs using the [fdroidserver](https://gitlab.com/fdroid/fdroidserver) software. The [server setup](https://f-droid.org/en/docs/Build_Server_Setup/) is quite involved but is not necessary unless you want to run your own instance. Normally the applications defined in `fdroiddata` are built by servers maintained by [the F-Droid volunteers](https://f-droid.org/en/contribute/). + +First release of Status app was merged in [fdroid/fdroiddata#7179](https://gitlab.com/fdroid/fdroiddata/-/merge_requests/7179). + +:warning: __WARNING__: Once changes are committed into `fdroiddata` repo they __cannot be changed__. + +# Adding New Versions + +There are two ways - automated and manual - described below. + +## Automated + +The script will analyze a provided APK, update the metadata file based on that information, and commit the change to [`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo. The creation of merge request is manual. + +#### Requirements: + +- GitLab account +- Forked copy of the [`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo +- Link to the release published + +#### Steps + +1. Use the F-Droid PR update script via `make`: + ```sh + make fdroid-pr APK=StatusIm-Mobile-v1.16.0-ef34af.apk + ``` + The script also accepts a URL. +2. Add a fork repo: + ```sh + git remote add john https://gitlab.com/john/fdroiddata.git + ``` +3. Push: + ```sh + git push john status-im/v1.16.0 + ``` +4. [Create a PR via the GitLab interface.](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) + + +## Manual + +You can find our configuration file at [`metadata/im.status.ethereum.yml`](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/im.status.ethereum.yml) + +The file defines all the necessary metadata like `SourceCode`, `Website`, or `License`, but the most important key is `Builds`, which looks like this: +```yml +Builds: + - versionName: 1.12.0 + versionCode: 2021022512 + commit: cfb825a11b61d312af8cb5d36686af540c31f481 + sudo: + - cd build/im.status.ethereum + - make fdroid-build-env + init: nix/scripts/setup.sh + output: result/app-release-unsigned.apk + scanignore: + - android/build.gradle + scandelete: + - ios + build: make release-fdroid BUILD_NUMBER=$$VERCODE$$ +``` +It contains a list of objects defining each release of the application. In order to add a new release simply copy a previous release object and adjust the following values: + +* `versionName` - String version like `1.12.0`. +* `versionCode` - Android `versionCode`. Normally it is generated from timestamp at build time. +* `commit` - Specific commit SHA1 from which the given release was built. + +The `versionCode` should be the same as the one in build that was uploaded to Play Store. +It can be found in the build logs or by using: +``` + > make shell TARGET=android + > apkanalyzer manifest version-code StatusIm-Mobile-v1.12.0.apk +2021022512 +``` + +At the bottom of the file you should also update the following keys: + +* `CurrentVersion` - Same as the new `versionName` added +* `CurrentVersionCode` - Same as the `versionCode` added + +Then submit a merge request with `Draft: ` prefix to the [fdroid/fdroiddata](https://gitlab.com/fdroid/fdroiddata) repository. +Prefix is necessary to avoid F-Droid people merging the PR before it's ready. + +:warning: __WARNING__: Currently GitLab PR builds will fail due to running as `root` instead of `vagrant` and failing to install Nix. + +# Checking Builds + +The simplest way to test if the app builds via F-Droid is to use the Docker image: +https://gitlab.com/fdroid/docker-executable-fdroidserver + +Which is available under `registry.gitlab.com/fdroid/docker-executable-fdroidserver:latest`. + +Because we use Nix to build the mobile app we need to slightly modify the image with this `Dockerfile`: +```Dockerfile +FROM registry.gitlab.com/fdroid/docker-executable-fdroidserver:latest +ARG BUILDER_UID=1000 +ENV BUILDER_USER=vagrant +RUN useradd -u $BUILDER_UID $BUILDER_USER +RUN mkdir -m 0755 /nix /home/$BUILDER_USER \ + && chown -R $BUILDER_USER /nix /home/$BUILDER_USER +``` +Build it using: +``` +docker build --build-arg=BUILDER_UID=$UID -t statusteam/docker-executable-fdroidserver:latest . +``` +Then clone the [fdroiddata](https://gitlab.com/fdroid/fdroiddata) and [fdroidserver](https://gitlab.com/fdroid/fdroidserver) repos and use it to test the build of a specific Status Android app version: `1.12.0` +``` +git clone https://gitlab.com/fdroid/fdroidserver +git clone https://gitlab.com/fdroid/fdroiddata +docker run --rm \ + -u $UID:$GID -e USER=vagrant \ + -v $PWD/fdroiddata:/repo \ + -v $PWD/fdroidserver:/fdroidserver \ + statusteam/docker-executable-fdroidserver:latest \ + build im.status.ethereum +``` +We have to create a user and specify the UID because Nix cannot run as `root` and that is the default user for the F-Droid Docker image. By adding our own user and setting the UID we also make it possible to mount folders like `fdroiddata` and `fdroidserver`. + +You can specify a `--verbose` flag for `build` command for additional information. +You can provide `--storage-opt size=30G` flag if Docker complains about running out of space. + +You should also run `lint` and `rewritemeta` for the App ID to verify and fix the YAML metadata formatting. + +# Scanning Builds Locally + +Based on the documentation provided here: https://gitlab.com/fdroid/wiki/-/wikis/Tips-for-fdroiddata-contributors/Setting-up-app-submission-development-environment-locally + +We can locally scan our source code and pre-built APKS by using the FDroid CLI tool. We can prepare a FDroid environment by +following [the steps mentioned in these instructions](https://gitlab.com/fdroid/wiki/-/wikis/Tips-for-fdroiddata-contributors/Setting-up-app-submission-development-environment-locally). + * Note, at the time of writing (March 2025), the easiest way to follow these instructions involved: + * Creating a new empty directory outside of the status-mobile repository. + * For this example, let's refer to this directory as `status-fdroid-server` + * Then git cloning the two referenced git repos inside the `status-fdroid-server` directory: + ```shell + git clone https://gitlab.com/fdroid/fdroiddata.git --depth=1 + git clone https://gitlab.com/fdroid/fdroidserver.git --depth=1 + ``` + * Then create a subdirectory inside `status-fdroid-server` named `apks` + * Then create a subdirectory inside `status-fdroid-server` named `scripts` + * Create a file inside the `scripts` folder named `env.sh` + ```shell + source /etc/profile.d/bsenv.sh + export GRADLE_USER_HOME=$home_vagrant/.gradle + export fdroid="sudo --preserve-env --user vagrant + env PATH=$fdroidserver:$PATH + env PYTHONPATH=$fdroidserver:$fdroidserver/examples + env PYTHONUNBUFFERED=true + env TERM=$TERM + env HOME=$home_vagrant + fdroid" + + export build=im.status.ethereum + ``` + * Create a file inside the `scripts` folder named `prep.sh` + ```shell + apt-get update && apt-get install -y make xz-utils + ``` + * Create a file inside the `scripts` folder named `run.sh` + ```shell + docker run --rm -i -t --privileged --pid=host --entrypoint "/bin/bash" \ + -v ${ANDROID_SDK_ROOT}:/opt/android-sdk:Z \ + -v $(pwd)/fdroidserver:/home/vagrant/fdroidserver:Z \ + -v $(pwd)/scripts:/scripts:z \ + -v $(pwd)/fdroiddata:/build:z \ + -v $(pwd)/apks:/apks:z \ + registry.gitlab.com/fdroid/fdroidserver:buildserver + ``` + * Create a file inside the `scripts` folder named `latest-version-code.sh` + ```shell + prefix="versionCode: " + resultLine=$(grep $prefix ./metadata/$build.yml | tail -n 1) + echo $resultLine | sed -e "s/^$prefix//" + ``` + * Then inside a terminal shell run the following command: + * `env ANDROID_SDK_ROOT= sh scripts/run.sh` + * Ensure you replace `` with the path to Android SDK folder. + * On macOS, the value could be: `$HOME/Library/Android/sdk` + * Once inside the Docker shell environment run the following commands: + * `sh /scripts/prep.sh` + * This command should install `xz` utils packages for installing the Nix package manager. + * `source /scripts/env.sh` + * This command should populate the Docker shell environment with the `$fdroid` command. + * Next we can run `cd /build` to enter the `fdroiddata` directory that has been mounted inside the docker environment. + * And now we can run `$fdroid scanner $build:$(sh /scripts/latest-version-code.sh)` to scan the latest version of the status app that has been registered in the `fdroiddata/metadata/im.status.ethereum.yml` file. + * Alternatively, we can also copy an APK into the `apks` folder and run `$fdroid scanner /apks/`. + * Ensure that you replace the `` with the correct name of the pre-built APK. + * For example, after running `make release-fdroid`, you can copy the `app-arm64-v8a-release-unsigned.apk` file into the `apks` folder and run `$fdroid scanner /apks/app-arm64-v8arelease-unsigned.apk` inside the docker shell environment. + +# Details + +The original research was done in [#8512](https://github.com/status-im/status-mobile/issues/8512). + +Normally F-Droid server wants to run Gradle itself, but we do not specify the `gradle` key in order to run `make release-fdroid` ourselves in `build` step. We also add `android/build.gradle` to `scanignore` to avoid F-Droid trying to use Gradle directly. + +The Android metadata like description or screenshots is [loaded from our repository](https://f-droid.org/en/docs/All_About_Descriptions_Graphics_and_Screenshots/#fastlane-structure) based on the Fastlane [`supply`](https://docs.fastlane.tools/actions/supply/) tool folder structure for updating Google Play store metadata. + +Once the PR is merged it may take a few days for the F-Droid server farm to build and deploy the new version to their site and app. You can look up the current state of builds [here](https://f-droid.org/wiki/index.php?title=Special:RecentChanges&days=7&from=&hidebots=0&hideanons=1&hideliu=1&limit=500) and look for your App ID and a `deploy` change after it. + +# F-Droid Build + +F-Droid requires submitted applications to be free of non Open Source software. For this reason we need to exclude certain Google libraries at build time. + +This is achieved by setting `GOOGLE_FREE=1` in the `.env.release` file. This affects two things: + +1. [Clojure code conditional](../src/status_im/acquisition/core.cljs) which stops use of `get-referrer`. +2. [Gradle build configuration](../android/app/build.gradle) by excluding Google libraries. + +Both are affected by the `.env` file and both are required. diff --git a/doc/files/forging-code-with-schemas-sep-2023-slides.pdf b/doc/files/forging-code-with-schemas-sep-2023-slides.pdf new file mode 100644 index 00000000000..7d8f65c7f22 Binary files /dev/null and b/doc/files/forging-code-with-schemas-sep-2023-slides.pdf differ diff --git a/doc/how-to-catch-crash-on-ios.md b/doc/how-to-catch-crash-on-ios.md new file mode 100644 index 00000000000..336ec2904f1 --- /dev/null +++ b/doc/how-to-catch-crash-on-ios.md @@ -0,0 +1,40 @@ +# How to catch crash on ios use xcode + +## Overview +Sometimes, we can't reproduce crash with local dev build, probably because your built version differs from the one QA team used. +The QA team mostly used the PR build version. In this guide we will try to make a build as close as possible to the PR build for iOS. + +You may notice various configuration files starting with .env (such as .env, .env.e2e, .env.jenkins) in the project. +The usage of these files and environment variables injected at build time on CI will not be covered in this guide. + +This guide covers how to configure Xcode to capture a crash stack trace with a physical iOS device. This allows debugging crashes by inspecting the stack trace. +I will use [issue #17255](https://github.com/status-im/status-mobile/issues/17255) as an example to reproduce and analyze a crash. + +## Prerequisites +- A physical ios device (simulator is not considered in this doc) +- Xcode installed + +## Steps +1. Prepare Status Mobile + 1. Open Terminal, cd to project root + 2. Run `make run-clojure` in one tab + 3. Run `make run-metro` in a second tab + 4. Run `make pod-install` in a third tab +2. Configure Xcode Project + 1. Open Xcode + 2. Open a project or file, navigate to folder `path_to_root_directory_of_status_mobile/ios` + 3. Set Scheme `StatusImPR` as current scheme ![](images/how-to-catch-crash-on-ios/choose-scheme-StatusImPR.png) + 4. Edit scheme ![](images/how-to-catch-crash-on-ios/edit-scheme.png) + 5. Change `Info` ![](images/how-to-catch-crash-on-ios/config-release.png) + 6. Change `Diagnostics` ![](images/how-to-catch-crash-on-ios/change-diagnostics.png) + 7. Set automatically manage signing ![](images/how-to-catch-crash-on-ios/automatically-manage-signing.png) +3. Update Code + 1. Edit `src/status_im2/setup/dev.cljs`, comment out lines 47-52 ![](images/how-to-catch-crash-on-ios/edit-dev.png) + 2. Edit `shadow-cljs.edn`, comment out `re-frisk-remote.preload`, otherwise it will try to connect `localhost:4567` on physical device which could slow down performance ![](images/how-to-catch-crash-on-ios/edit-shadow-cljs.png) +4. Reproduce Crash And Analyze + 1. Choose your physical ios device as target device and Run + 2. Follow reproduce steps from [issue #17255](https://github.com/status-im/status-mobile/issues/17255) mentioned and xcode will halt at crash point ![](images/how-to-catch-crash-on-ios/located-stack-trace.png) + 3. Inspect stack trace to identify root cause. From the stack trace, we can locate the crash happened in `api/geth_backend.go:1048`(createTempDBFile) clearly! ![](images/how-to-catch-crash-on-ios/located-go-code.png) + +## Links +- [changes](https://github.com/status-im/status-mobile/commit/4306d419de15e737e8ded3a749cc5e8d6a06e627) to source code diff --git a/doc/ide-setup.md b/doc/ide-setup.md new file mode 100644 index 00000000000..aa400509d7c --- /dev/null +++ b/doc/ide-setup.md @@ -0,0 +1,226 @@ +# Setup your Editor/IDE + +You can use any Clojure enabled editor. Here are instructions for developing the status-mobile app using: + +* IntelliJ IDEA and Cursive +* VS Code and Calva +* Emacs/Cider + +(Please add instructions for your favorite editor.) + +## Table of Contents + +- [Using Cursive](#using-cursive) + - [Install Cursive](#install-cursive) + - [Getting Cursive to understand `status-mobile`](#getting-cursive-to-understand-status-mobile) + - [I get a lot of `cannot be resolved`](#i-get-a-lot-of-cannot-be-resolved) + - [REPL!](#repl) + - [Connecting to REPL to IntelliJ](#connecting-to-repl-to-intellij) + - [Connecting REPL and IntelliJ to `status-mobile`](#connecting-repl-and-intellij-to-status-mobile) +- [Visual Studio Code](#visual-studio-code) + - [Set up the REPL](#set-up-the-repl) + - [Auto-format files on save](#auto-format-files-on-save) +- [Using Emacs with CIDER](#using-emacs-with-cider) + +## Using Cursive + +### Install Cursive + +See https://cursive-ide.com/userguide/index.html + +### Getting Cursive to understand `status-mobile` + +- Add this file to the root of the `status-mobile` project dir + - https://gist.github.com/Samyoul/f71a0593ba7a12d24dd0d5ef986ebbec +- Right click and "add as leiningen project" + + + +## I get a lot of `cannot be resolved` + +Are you getting problems where you get a lot of `cannot be resolved` on everything? + + + +See https://cursive-ide.com/userguide/macros.html + +- opt+enter (on macOS) +- resolve defview as fn and letsubs as let +- move selection on resolve and hit enter +- and select defn for defview and let for letsubs + +### REPL! + +#### Connecting to REPL to IntelliJ + +I had a number of problems connecting to REPL, the solution is as follows: + +At the top of IntelliJ IDEA click on the `Add Configuration...` option: + + + +This will load the following menu: + + + +Click on the `+` icon in the top left corner of the menu. + +Select `Clojure REPL > Remote` + + + +Which will load the following menu + + + +Enter the below options: + +- Name = status-mobile +- Display Name = status-mobile +- Connection type = nREPL +- Connection details + - Host = 127.0.0.1 + - Port = 7888 + + + +Press `OK` + +Now the below option will be visible. +Press the green run button + + + +You should now see a dialog with the following message: + +```shell +Connecting to remote nREPL server... +Clojure 1.10.1 +``` + +To confirm you have a working connection with REPL enter the following command in the input box below the output: + +```shell +(prn 1) +``` + +Which should output + +```shell +(prn 1) +1 +=> nil +``` + +See below: + + + +#### Connecting REPL and IntelliJ to `status-mobile` + +**The important next step is telling REPL the context in which to interact with the code.** + +Do the following: + +Ensure you have 2 terminals running the following + +- `make run-clojure` +- `make run-ios` / `make run-android` + +[See the STARTING GUIDE for details](STARTING_GUIDE.md#development) + +Next go back to the REPL input and enter the following commands: + +```clojure +(shadow/watch :mobile) +(shadow/repl :mobile) +``` + +See Below + + + +Which should switch the clj file type target to cljs as shown above + +Finally you are ready to test REPL. + +Create a sample function to evaluate something simple like `(prn "I'm working")`, move your cursor to one of the outer parentheses. Right or `control` click and select the `REPL` option. From there select `Sync files in REPL` and then `Send '...' to REPL'`. + + + +Alternatively you can use the shortcut commands `⇧⌘M` to sync your files and `⇧⌘P` to send the statement to REPL. You may also need to switch the REPL namespace to match the current file, which can be done manually from the dialogue box or using the `⇧⌘N` shortcut key. + +Following the above should give you the below result: + + + +🎉 Tada! Working! 🎉 + +--- + +For additional details on issues you may face when setting up REPL with Cursive [see this document](https://notes.status.im/9Gr7kqF8SzC_SmYK0eB7uQ?view#Connecting-Cursive--IntelliJ-IDEA-to-REPL-Problems) + +## Visual Studio Code + +### Set up the REPL + +1. Install Calva extension. +2. Open the `status-mobile` folder. +3. Start [Status development](starting-guide.md#development) (Starting the + `run-clojure` and `run-metro` jobs in split view in the VS Code integrated + terminal works great). +4. Run the VS Code command: **Calva: Connect to a running REPL Server in the + project**. + - Select the project type `shadow-cljs`. + - Accept the suggested connection `host:port`. + - Select to connect to the `:mobile` build. + +Open any `.cljs` file in the project and evaluate forms in it. See +https://calva.io/try-first/ for some starter tips and links. Confirm that your +REPL is connected to the app by evaluating: + +```clojure +(js/alert "Hello from Status App!") +``` + +🎉 Tada! You are ready to use the REPL to improve Status.im! 🎉 + +Consider bookmarking [calva.io](https://calva.io/) for quick access to the Calva +documentation. + +### Auto-format files on save + +We format Clojure code with [zprint](https://github.com/kkinnear/zprint). We +recommend configuring VSCode to auto-format Clojure files on every save, +otherwise you'll need to constantly rely on the much slower `make lint-fix` +command. + +1. Create a file `~/.zprintrc` and enable the `:search-config?` option. This is + necessary so the VSCode extension finds the project's `.zprintrc`. + +```bash +echo "{:search-config? true}" > ~/.zprintrc +``` + +2. Install the extension + [vscode-clj-zprint](https://github.com/rflagreca/vscode-clj-zprint). By the + way, did you know this extension is written in ClojureScript? +3. Copy the recommended settings from our repository in + [.vscode/settings.example.json](/.vscode/settings.example.json) and merge it + with your workspace settings in `.vscode/settings.json`. + +The extension `vscode-clj-zprint` is only available for VSCode. If you are using +VSCodium, you'll need to build the `vsix` (extension) yourself (it's easy, only +`yarn` and `vsce` are required). + +## Using Emacs with CIDER + +1. Install Emacs/Cider/etc. (there is a lot of variability in how to manage things in emacs, so please google for help with this) +2. Add a local ~/.shadow-cljs/config.edn file like below (corresponding to the version numbers of the packages you are using): + +```clojure +{:dependencies + [[nrepl/nrepl "0.9.0"] + [cider/cider-nrepl "0.28.4"] + [cider/piggieback "0.5.2"]]} +``` diff --git a/doc/images/architecture.png b/doc/images/architecture.png new file mode 100644 index 00000000000..2cc86240d5d Binary files /dev/null and b/doc/images/architecture.png differ diff --git a/doc/images/debugging/geth-path.png b/doc/images/debugging/geth-path.png new file mode 100644 index 00000000000..0fa34ec11f0 Binary files /dev/null and b/doc/images/debugging/geth-path.png differ diff --git a/doc/images/debugging/log-settings.png b/doc/images/debugging/log-settings.png new file mode 100644 index 00000000000..8821d8c218c Binary files /dev/null and b/doc/images/debugging/log-settings.png differ diff --git a/doc/images/debugging/re-frisk.png b/doc/images/debugging/re-frisk.png new file mode 100644 index 00000000000..a34a100728b Binary files /dev/null and b/doc/images/debugging/re-frisk.png differ diff --git a/doc/images/debugging/share-logs.jpeg b/doc/images/debugging/share-logs.jpeg new file mode 100644 index 00000000000..b56d3579b12 Binary files /dev/null and b/doc/images/debugging/share-logs.jpeg differ diff --git a/doc/images/export-icons/export-icons.gif b/doc/images/export-icons/export-icons.gif new file mode 100644 index 00000000000..f60c12a03c2 Binary files /dev/null and b/doc/images/export-icons/export-icons.gif differ diff --git a/doc/images/figma-properties.png b/doc/images/figma-properties.png new file mode 100644 index 00000000000..50fc9ca1ffe Binary files /dev/null and b/doc/images/figma-properties.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/automatically-manage-signing.png b/doc/images/how-to-catch-crash-on-ios/automatically-manage-signing.png new file mode 100644 index 00000000000..fba99af587c Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/automatically-manage-signing.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/change-diagnostics.png b/doc/images/how-to-catch-crash-on-ios/change-diagnostics.png new file mode 100644 index 00000000000..38120cff49f Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/change-diagnostics.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/choose-scheme-StatusImPR.png b/doc/images/how-to-catch-crash-on-ios/choose-scheme-StatusImPR.png new file mode 100644 index 00000000000..f7f99b44679 Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/choose-scheme-StatusImPR.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/config-release.png b/doc/images/how-to-catch-crash-on-ios/config-release.png new file mode 100644 index 00000000000..355cdeb15ba Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/config-release.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/edit-dev.png b/doc/images/how-to-catch-crash-on-ios/edit-dev.png new file mode 100644 index 00000000000..ee06ba0a89b Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/edit-dev.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/edit-scheme.png b/doc/images/how-to-catch-crash-on-ios/edit-scheme.png new file mode 100644 index 00000000000..8ec6096a4e1 Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/edit-scheme.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/edit-shadow-cljs.png b/doc/images/how-to-catch-crash-on-ios/edit-shadow-cljs.png new file mode 100644 index 00000000000..4704d3d395c Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/edit-shadow-cljs.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/located-go-code.png b/doc/images/how-to-catch-crash-on-ios/located-go-code.png new file mode 100644 index 00000000000..b8138ce8f0a Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/located-go-code.png differ diff --git a/doc/images/how-to-catch-crash-on-ios/located-stack-trace.png b/doc/images/how-to-catch-crash-on-ios/located-stack-trace.png new file mode 100644 index 00000000000..c0b06d15d8d Binary files /dev/null and b/doc/images/how-to-catch-crash-on-ios/located-stack-trace.png differ diff --git a/doc/images/how-to-launch-e2e/how-to-launch-e2e-1.png b/doc/images/how-to-launch-e2e/how-to-launch-e2e-1.png new file mode 100644 index 00000000000..13bd065b646 Binary files /dev/null and b/doc/images/how-to-launch-e2e/how-to-launch-e2e-1.png differ diff --git a/doc/images/how-to-launch-e2e/how-to-launch-e2e-2.png b/doc/images/how-to-launch-e2e/how-to-launch-e2e-2.png new file mode 100644 index 00000000000..bdde50b4fee Binary files /dev/null and b/doc/images/how-to-launch-e2e/how-to-launch-e2e-2.png differ diff --git a/doc/images/how-to-launch-e2e/how-to-launch-e2e-3.png b/doc/images/how-to-launch-e2e/how-to-launch-e2e-3.png new file mode 100644 index 00000000000..04acac2f9a4 Binary files /dev/null and b/doc/images/how-to-launch-e2e/how-to-launch-e2e-3.png differ diff --git a/doc/images/how-to-launch-e2e/how-to-launch-e2e-4.png b/doc/images/how-to-launch-e2e/how-to-launch-e2e-4.png new file mode 100644 index 00000000000..775364ab787 Binary files /dev/null and b/doc/images/how-to-launch-e2e/how-to-launch-e2e-4.png differ diff --git a/doc/images/ide-setup/10_REPL_8.png b/doc/images/ide-setup/10_REPL_8.png new file mode 100644 index 00000000000..cddbb129fc5 Binary files /dev/null and b/doc/images/ide-setup/10_REPL_8.png differ diff --git a/doc/images/ide-setup/11_REPL_9.png b/doc/images/ide-setup/11_REPL_9.png new file mode 100644 index 00000000000..6237894d5a5 Binary files /dev/null and b/doc/images/ide-setup/11_REPL_9.png differ diff --git a/doc/images/ide-setup/12_REPL_10.png b/doc/images/ide-setup/12_REPL_10.png new file mode 100644 index 00000000000..1b2b56d2d8c Binary files /dev/null and b/doc/images/ide-setup/12_REPL_10.png differ diff --git a/doc/images/ide-setup/1_fake_project_file.png b/doc/images/ide-setup/1_fake_project_file.png new file mode 100644 index 00000000000..e96e9b9c9f1 Binary files /dev/null and b/doc/images/ide-setup/1_fake_project_file.png differ diff --git a/doc/images/ide-setup/2_resolve.jpeg b/doc/images/ide-setup/2_resolve.jpeg new file mode 100644 index 00000000000..a6a79891225 Binary files /dev/null and b/doc/images/ide-setup/2_resolve.jpeg differ diff --git a/doc/images/ide-setup/3_REPL_1.png b/doc/images/ide-setup/3_REPL_1.png new file mode 100644 index 00000000000..585f3ddb7e9 Binary files /dev/null and b/doc/images/ide-setup/3_REPL_1.png differ diff --git a/doc/images/ide-setup/4_REPL_2.png b/doc/images/ide-setup/4_REPL_2.png new file mode 100644 index 00000000000..1cbd4bb81cb Binary files /dev/null and b/doc/images/ide-setup/4_REPL_2.png differ diff --git a/doc/images/ide-setup/5_REPL_3.png b/doc/images/ide-setup/5_REPL_3.png new file mode 100644 index 00000000000..94b60c1bf13 Binary files /dev/null and b/doc/images/ide-setup/5_REPL_3.png differ diff --git a/doc/images/ide-setup/6_REPL_4.png b/doc/images/ide-setup/6_REPL_4.png new file mode 100644 index 00000000000..4ffd02c3264 Binary files /dev/null and b/doc/images/ide-setup/6_REPL_4.png differ diff --git a/doc/images/ide-setup/7_REPL_5.png b/doc/images/ide-setup/7_REPL_5.png new file mode 100644 index 00000000000..5ede5f14c34 Binary files /dev/null and b/doc/images/ide-setup/7_REPL_5.png differ diff --git a/doc/images/ide-setup/8_REPL_6.png b/doc/images/ide-setup/8_REPL_6.png new file mode 100644 index 00000000000..979d47f7f80 Binary files /dev/null and b/doc/images/ide-setup/8_REPL_6.png differ diff --git a/doc/images/ide-setup/9_REPL_7.png b/doc/images/ide-setup/9_REPL_7.png new file mode 100644 index 00000000000..6f5669b45ee Binary files /dev/null and b/doc/images/ide-setup/9_REPL_7.png differ diff --git a/doc/images/layers-relationships.png b/doc/images/layers-relationships.png new file mode 100644 index 00000000000..abc7763716e Binary files /dev/null and b/doc/images/layers-relationships.png differ diff --git a/doc/images/pipeline-process/automation-checks.png b/doc/images/pipeline-process/automation-checks.png new file mode 100644 index 00000000000..cf88b43608c Binary files /dev/null and b/doc/images/pipeline-process/automation-checks.png differ diff --git a/doc/images/pixel-perfection/layer-width.png b/doc/images/pixel-perfection/layer-width.png new file mode 100644 index 00000000000..bebba1e2739 Binary files /dev/null and b/doc/images/pixel-perfection/layer-width.png differ diff --git a/doc/images/quo-component.png b/doc/images/quo-component.png new file mode 100644 index 00000000000..048b077766a Binary files /dev/null and b/doc/images/quo-component.png differ diff --git a/doc/import-assets.md b/doc/import-assets.md new file mode 100644 index 00000000000..66ad0c3cd6e --- /dev/null +++ b/doc/import-assets.md @@ -0,0 +1,23 @@ +# Importing assets from Figma into project + +## Icons + +![](images/export-icons/export-icons.gif) + +1. Export from figma 2 pngs 2x and 3x put them in `./resources/images/icons2` +2. if necessary, rename file so that filename contains only lower case chars, e.g. `"Icon-Name@2x.png"` should be renamed to `"icon_name@2x.png"`. +3. In the app `icon_name.png` still can be accessed as `icon-name`, so in order to use can add the next code: + ```clojure + ;; icon_name.png + [vector-icons/icon :icon-name {:color ...}] + ``` + 2x@2x +4. If you want platform specific icon use `.android` or `.ios` suffixes. Example `icon_name@2x.android.png`.:w + +## Images + +Make sure to compress images before using into project. +``` +make shell +./scripts/compress_image.sh image_path +``` diff --git a/doc/logging-and-sharing-flow.md b/doc/logging-and-sharing-flow.md new file mode 100644 index 00000000000..a95403b8b7c --- /dev/null +++ b/doc/logging-and-sharing-flow.md @@ -0,0 +1,45 @@ +# Status App Logging and Sharing Flow + +## Overview +The Status app maintains four log files to track different aspects of its operation. This document outlines the purpose of each log file, their configuration, storage, and the process for sharing logs. + +## Log Files + +- **api.log** + Records interactions between the frontend and the backend(status-go). + +- **pre-login.log** + Captures logs generated before user login. + +- **Status.log** + Stores logs output by the frontend. + +- **Profile log** + Dynamically named log file that records user-specific logs after successful login. The log file name is generated based on the user's keyUID which is truncated to 8 characters. Each user profile has its own log file. + +## Log Level Configuration + +### pre-login.log +- **Setting Method:** Shake the phone before login to open a dialog box. +- **Options:** Includes a log level setting option (e.g., DEBUG, INFO). +- **Behavior:** The log level setting is only available pre-login. After login, shaking the phone does not display this option. +- **Impact:** The configured log level is inherited by the Profile log for newly created users. + +### Profile Log +- **Setting Method:** Can be configured after user login through app advanced settings. +- **Note:** Inherits the log level from pre-login.log for new users. + +## Log File Storage +- **Default Behavior:** All log files are stored in a protected folder named "logs" within the app to ensure privacy and security. +- **Exception:** On Android e2e build, logs are stored in the publicly accessible Download folder. + +## Log setting storage +- pre-login setting is stored with [mmkv](https://github.com/mrousavy/react-native-mmkv), we have some pre-configured values for log level in files `.env.xxx` e.g `.env` / `.env.e2e` , corresponding field name is `LOG_LEVEL` +- profile log setting is stored in db with status-go + +## Log Sharing +- **Method:** + - Shake the phone to open a dialog box. + - Select the "Share logs" option from the menu. +- **Note:** Be careful when sharing logs with others, as it may leak sensitive information. + diff --git a/doc/merging-pr-process.md b/doc/merging-pr-process.md new file mode 100644 index 00000000000..96f75e1500d --- /dev/null +++ b/doc/merging-pr-process.md @@ -0,0 +1,51 @@ +## PR process + +1) Create a PR in status-mobile +2) Add some reviewers to the PR and wait for feedback +3) Address feedback +4) Make sure builds and tests are green (run `make test` locally, `make lint-fix` to fix any indentation issue and `make lint`) +5) Once the PR has been reviewed by the dev team, run e2e tests on it by going to https://github.com/status-im/status-mobile/projects/7 and move the pr under the column E2E tests. This will trigger tests. **Running E2E tests is required for all PRs.** + 1) Read our [PR Review Policy](pr-review-policy.md) for more details on our approach to PR reviews. + 2) Learn more about our [Pipeline for QA](pipeline_process.md) process +6) Once e2e tests have run, they will report the result on the PR, if it's less than 100%, ask QA to take a look to make sure everything is in order (some might fail for legitimate reasons) +7) Ask QA for manual testing if the PR requires it (more details [here](pipeline_process.md)) +8) Once it has been tested successfully, squash everything into one commit. rebase and merge. The commands we use: +``` + git checkout develop + git pull develop + git checkout your-feature-branch + git rebase develop + git checkout develop + git rebase your-feature-branch + git push +``` + + +## Status-go changes + +If you are introducing status-go changes, the PR process is pretty similar, with some differences. + +The most important thing is that + +status-mobile code that makes it to the `develop` branch, should always point to a tagged version of status-go in the `develop` branch of status-go. + +In practice, this means that sometimes they need to be merged in lockstep. +1) Create a PR in status-go and status-mobile. Update the status-go version to the PR revision `scripts/update-status-go.sh $git_revision`. +2) Get both PRs reviewed and approved. Once status-mobile PR has been approved, go through manual QA testing if necessary. Don't merge status-mobile PR just yet. +3) Now that you know the integration between client & server is working, merge the status-go PR first. +4) Once merged, tag the merged commit with the new version and push the tag: +``` + git checkout develop + git pull develop + make tag-version + git push origin vx.y.z +``` +:warning: If a specific commit need to tagged in status-go, use `make tag-version TARGET_COMMIT={hash}`. More info about tagging in [status-go](https://github.com/status-im/status-go/blob/develop/RELEASING.md#tagging-versions). + +5) Update status-mobile with the new status-go version, using the new tag `scripts/update-status-go.sh "vx.y.z"` +6) If you had to rebase status-go to include new changes, e2e test MUST be re-run. If there's any issue you will + have to fix in status-go with another PR and follow the same process. +7) In status-mobile, push, rebase against `develop` and merge it 🚀 + + +important note : make sure your status-go PRs get a tested-ok by QA before merging them in. diff --git a/doc/new-guidelines.md b/doc/new-guidelines.md new file mode 100644 index 00000000000..93c1a20c5ed --- /dev/null +++ b/doc/new-guidelines.md @@ -0,0 +1,958 @@ +# Code Style Guidelines + +> [!IMPORTANT] +> The goal of this document is to help all contributors (core and external) to +> write code in _unison_ and help establish good practices that serve the Status +> Mobile contributors well. + +We don't want to turn this document into an exhaustive list of rules to be +followed that nobody will read. As much as possible, we'll try to document only +what we consider important for Status Mobile. In other words, we don't want to +maintain a general Clojure convention/style guide, nor do we want to turn this +document into a long tutorial. + +> [!WARNING] +> This is a **work in progress**, and not all conventions are properly +> implemented in the codebase yet. The project structure is also undergoing +> major changes, and it will take a considerable amount of time until we migrate +> the existing code to the new structure. + +If you find out anything is outdated or missing, please, share with us or even +better, create a pull-request! 🤸 + +## Style guide + +We follow the [Clojure Style +Guide](https://github.com/bbatsov/clojure-style-guide) and we use +[zprint](https://github.com/kkinnear/zprint) to format Clojure code. Running +`make lint-fix` should fix most formatting issues, but not all of them. + +## Dos and don'ts + +### Hiccup + +Never use anonymous inline function in hiccup, this will lead to +reinitialization of component on each render of parent component. + +```clojure +;; bad +(defn checkbox-view + [{:keys [size]}] + [rn/view + [(fn [] [rn/view])]]) + +;; good +(defn comp [] + [rn/view]) + +(defn checkbox-view + [{:keys [size]}] + [rn/view + [comp]]) +``` + +This mistake mostly happens with functional components. + +```clojure +;; bad +(fn [] + (let [atom (rf/sub [:sub])] + (fn [] + [:f> + (fn [] + [rn/text atom] + +;; good +(defn f-comp [atom] + [rn/text atom]) + +(fn [] + (let [atom (rf/sub [:sub])] + (fn [] + [:f> f-comp atom]))) +``` + +It's important to name functional components with `f-` prefix. + +### Component props and API scheme to match Figma as closely as possible + +Ideally, the prop names for components (particularly in quo Design System) +should match the Figma properties as best as possible. This makes it easier for +the developer using that component to configure it correctly for the screen it +is being used on and avoids unnecessary overwrites and adjustments being made. + +#### Avoid unnecessarily grouping categories to reduce the number of props + +For example in Figma if there is a component and it has the following variants: + +|theme: "light" blur: "False"|theme: "dark" blur: "False"|theme: "light" blur: "True"|theme: "dark" blur: "True"| +|----------------------------|---------------------------|---------------------------|---------------------------| +| type :neutral label "ABC" | type :neutral label "ABC" | | | +| type :active label "ABC" | type :active label "ABC" | | | +| type :danger label "ABC" | type :danger label "ABC" | type :danger label "ABC" | type :danger label "ABC" | + +```clojure +;; bad +"theme - :light or :dark +type - can be :neutral :active :danger :danger-blur" +(defn my-component [{:keys [theme type]} label]) + +;; good +"theme - :light or :dark + type - can be :neutral :active :danger + blur? - boolean +" +(defn my-component [{:keys [theme blur? type]} label]) +``` + +Please note this is only for the external API of the component and there should +be no restriction of how the component manages its internal API as that will not +affect the developer using the component with the issues described above. + +In some cases this is not always possible or does not make sense. However the +thought process should be how easy will it be for another developer to use this +component with the correct configuration given the screen designs for Figma. + +#### Avoid unnecessarily renaming props + +In general it can be helpful to avoid renaming props from their counterpart in +Figma. + +For example if Figma has sizes `:small`, `:medium` and `:large` + +```clojure +;; bad +":size - :little, :default or :big" +(defn my-component [{:keys [size]}]) + +;; good +":size - :small, :medium or :large" +(defn my-component [{:keys [size]}]) +``` + +### Component styles + +Prefer to define styles in a separate file named `style.cljs`, colocated with +the source file. For a real example, see +[src/quo/components/record_audio/record_audio/style.cljs](../src/quo/components/record_audio/record_audio/style.cljs). + +```clojure +;; bad +(defn checkbox-view + [{:keys [size]}] + [rn/view + {:style {:width size + :height size + :border-radius 4 + :justify-content :center + :align-items :center}} + [rn/view (do-something)]]) + +;; good +(defn checkbox-view + [{:keys [size]}] + [rn/view {:style (style/checkbox size)} + [rn/view (do-something)]]) +``` + +### Always add styles inside the `:style` key + +Although when compiling ReactNative for mobile some components are able work with +their styles in the top-level of the properties map, prefer to add them inside the +`:style` key in order to separate styles from properties: + +```clojure +;; bad +[rn/button {:flex 1 + :padding-vertical 10 + :padding-horizontal 20 + :on-press #(js/alert "Hi!") + :title "Button"}] + +;; good +[rn/button {:style {:flex 1 + :padding-vertical 10 + :padding-horizontal 20} + :on-press #(js/alert "Hi!") + :title "Button"}] + +;; better +;; (define them in a style ns & place them inside `:style` key) +[rn/button {:style (style/button) + :on-press #(js/alert "Hi!") + :title "Button"}] +``` + +Also its fine to keep one liner styles in view + +```clojure +;; ok +[rn/view {:style {:flex 1 :padding-top 5}}] +``` + +### Abstract styles and use the vector syntax + +We modified reagent to support passing styles in the [React Native way](https://reactnative.dev/docs/0.73/style): +by using Clojure vectors (arrays in JS). + +```clojure +;; ...style.cljs +(def absolute-position + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0}) + +(defn padding [amount] + {:background-color :red + :padding amount}) + +;; ...view.cljs + +;; bad +[rn/view {:style style/absolute-position} + [rn/view {:style (merge style/absolute-position ;; <- merge is not performant + (style/padding 20))}]] + +;; good +[rn/view {:style style/absolute-position} + [rn/view {:style [style/absolute-position ;; <- vector syntax, RN merges the styles + (style/padding 20)]}]] + +;; better +;; Reference an already existing JS obj with the required styles, by doing this, Reagent +;; skips the transformation of styles and uses the obj directly. +[rn/view {:style rn/stylesheet-absolute-fill} ;; <- Already provided by RN, used directly + [rn/view {:style [rn/stylesheet-absolute-fill ;; <- Reagent skips this transformation + (style/padding 20)]}]] ;; <- Reagent transforms this map +``` + +### Don't define properties in styles ns + +Properties must be set on view level + +```clojure +;; bad +{:style {:position :absolute + :left 0 + :right 0 + :bottom 0} + :blur-amount 30 + :blur-radius 25 + :blur-type :transparent + :overlay-color :transparent} + +;; good +{:position :absolute + :left 0 + :right 0 + :bottom 0} +``` + +### Animated styles in the style file + +```clojure +;; bad +(defn circle + [] + (let [opacity (reanimated/use-shared-value 1)] + [reanimated/view {:style [{:opacity opacity} + style/circle-container]}])) + +;; good +(defn circle + [] + (let [opacity (reanimated/use-shared-value 1)] + [reanimated/view {:style (style/circle-container opacity)}])) +``` + +### Pass a vector of styles to Reanimated view + +Prefer to pass a vector of styles to `react-native.reanimated/view` `:style` +prop instead of using `apply-animations-to-style` directly. For more details, check out Reanimated docs about [inline +styles](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/your-first-animation/#defining-a-shared-value) +and [useAnimatedStyle](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/animating-styles-and-props/#animating-styles). + +```clojure +(defn f-view [] + (let [scroll-x (reanimated/use-shared-value 0) + opacity (reanimated/interpolate scroll-x [0 45 50] [1 1 0])] + [reanimated/view + ;; bad + {:style (reanimated/apply-animations-to-style + {:opacity opacity + :transform [{:translate-x scroll-x}]} + {:flex-direction :row})} + + ;; good + {:style [{:opacity opacity + :transform [{:translate-x scroll-x}]} + {:flex-direction :row}]} + + ;; other valid and good variants + {:style [{:opacity opacity} + {:transform [{:translate-x scroll-x}]} + {:flex-direction :row}]} + + {:style {:opacity opacity + :transform [{:translate-x scroll-x}] + :flex-direction :row}}])) +``` +### Don't use percents to define width/height + +In ReactNative, all layouts use the [flexbox +model](https://reactnative.dev/docs/flexbox), so percentages are unnecessary the +vast majority of the time, don't use them. Check out this great [interactive +flexbox guide](https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/) by +Joshua Comeau. + +```clojure +;; bad +[rn/view {:style {:width "80%"}}] + +;; good +[rn/view {:style {:padding-horizontal 20}}] +``` + +### Use a question mark to convey the value is a boolean + +The Clojure Style Guide suggests using a question mark only in [predicate +functions](https://guide.clojure.style/#naming-predicates), but nothing is +mentioned about other symbols and keywords. We prefer to extend the convention +to all boolean references. + +```clojure +;; bad +(let [is-open? true] ...) +(def flag-is-enabled false) + +;; good +(let [open? true] ...) +(def flag-enabled? false) +``` + +And for keywords too: + +```clojure +;; bad +[some-component {:logged-in true}] + +;; good +[some-component {:logged-in? true}] +``` + +### Styles def vs defn + +Always use `def` over `defn` if there are no dynamic values. This helps cut the +cost of function calls. + +```clojure +;; bad +(defn title-column [] + {:height 56}) + +;; good +(def title-column + {:height 56}) +``` + +```clojure +;; bad +(def community-card + {:background-color (colors/theme-colors colors/white colors/neutral-90)}) + +;; good +(defn community-card [] + {:background-color (colors/theme-colors colors/white colors/neutral-90)}) +``` + +### Custom Colors + +The Status designs have a lot of customization of user and group colors. For +consistency it is best to use `customization-color` as the prop key on pages and +components. This will help easily identify what pages and components in the +application are using customized colors. + +```clojure +;; bad +(defn community-card [{keys [custom-color]}] + ...) + +;; good +(defn community-card [{keys [customization-color]}] + ...) +``` + +### Using TODOs comments + +_TODO_ comments are used extensively in the codebase, but prefer to use them +only when strictly necessary and when an issue is not enough to track the work +left to be done. + +These are all good examples: + +```clojure +;; TODO(@username): +;; TODO(@username): , +;; TODO(YYYY-MM-DD): +;; TODO(@username,YYYY-MM-DD): +``` + +### Subscription names and event names + +Always register events and subscriptions using a meaningful namespace, but don't +namespace them with `::`. We understand it's a controversial decision because +there are both pros and cons to such practice. + +Whenever appropriate, it's also recommended to use _fake_ namespaces to convey +more knowledge in the keyword about which bounded context (domain) it refers to. +You may also use dots to convey hierarchical structures. + +```clojure +;; bad +;; Don't use real namespaced keywords. +(re-frame/reg-sub + ::profile-pictures-visibility + :<- [:multiaccount] + (fn [multiaccount] + (:profile-pictures-visibility multiaccount))) + +;; good +;; Uses a fake namespaced keyword. +(re-frame/reg-sub + :profile/pictures-visibility + :<- [:multiaccount] + (fn [multiaccount] + (:profile-pictures-visibility multiaccount))) + +;; better +;; Uses a fake namespaced keyword with a parent namespace (multiaccount). +(re-frame/reg-sub + :multiaccount.profile/pictures-visibility + :<- [:multiaccount] + (fn [multiaccount] + (:profile-pictures-visibility multiaccount))) +``` + +### Declaring view components + +Use the simple `defn` to declare components. Don't use `utils.views/defview` and +`utils.views/letsubs`. + +```clojure +;; bad +(utils.views/defview browser [] + (utils.views/letsubs [window-width [:dimensions/window-width]] + (do-something window-width))) + +;; good +(defn browser [] + (let [window-width (rf/sub [:dimensions/window-width])] + (do-something window-width))) +``` + +### Use `[]` instead of `()` in Reagent components + +- The `()` version [does NOT work with Form-2 and + Form-3](https://github.com/reagent-project/reagent/blob/master/doc/UsingSquareBracketsInsteadOfParens.md#a-further-significant-why) + components. +- Components defined with `[]` will be [more efficient at re-render + time](https://github.com/reagent-project/reagent/blob/master/doc/UsingSquareBracketsInsteadOfParens.md#which-and-why) + because they're interpreted by Reagent and transformed into distinct React + components, with their own lifecycle. + +```clojure +;; bad +[rn/view + (message-card message)] + +;; good +[rn/view + [message-card message]] +``` + +### Using re-frame subscriptions and dispatching events + +Use the `utils.re-frame` namespace instead of `re-frame.core` to subscribe and +dispatch. + +```clojure +;; bad +(ns my-namespace + (:require [re-frame.core :as rf])) + +(let [username @(rf/subscribe [:username])] + [pressable/pressable {:on-press #(rf/dispatch [:do-something])} + [rn/view + (str "Hello " username)]]) + +;; good +(ns my-namespace + (:require [utils.re-frame :as rf])) + +(let [username (rf/sub [:username])] + [pressable/pressable {:on-press #(rf/dispatch [:do-something])} + [rn/view + (str "Hello " username)]]) +``` + +### Registering effects + +When registering re-frame effects (`reg-fx`), prefer to expose a data-only +interface because that will allow event handlers to stay pure. + +For instance, if an effect needs a `on-success` callback, allow it to receive a +*re-frame event vector*. This approach is used by us in the [json-rpc/call +effect](src/status_im2/common/json_rpc/events.cljs), but also by third-party +effects, such as https://github.com/Day8/re-frame-http-fx. For the complete +rationale, see [PR #15936](https://github.com/status-im/status-mobile/pull/15936). + +### Using the effect `:json-rpc/call` + +Prefer the pure version of `:json-rpc/call` (no callbacks). + +```clojure +;; not as good +(rf/defn accept-contact-request + {:events [:activity-center.contact-requests/accept]} + [_ contact-id] + {:json-rpc/call + [{:method "wakuext_acceptContactRequest" + :params [{:id contact-id}] + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(rf/dispatch [:activity-center.contact-requests/accept-error contact-id %])}]}) + +;; better +(rf/defn accept-contact-request + {:events [:activity-center.contact-requests/accept]} + [_ contact-id] + {:json-rpc/call + [{:method "wakuext_acceptContactRequest" + :params [{:id contact-id}] + :on-success [:sanitize-messages-and-process-response] + :on-error [:activity-center.contact-requests/accept-error contact-id]}]}) +``` + +### Registering event handlers + +Register events with `utils.re-frame/reg-event-fx` and follow [re-frame's best +practice](https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/FAQs/BestPractice.md#use-the-fx-effect) +so use only `:db` and `:fx` effects. `utils.re-frame/merge` and `utils.re-frame/defn` are deprecated and should not be +used in the new code in `src/status_im2/`. Don't use +`re-frame.core/reg-event-db`. + +```clojure +;; bad +(rf/defn invite-people-pressed + {:events [:communities/invite-people-pressed]} + [cofx id] + (rf/merge cofx + (reset-community-id-input id) + (bottom-sheet/hide-bottom-sheet) + (navigation/open-modal :screen/invite-people-community {:invite? true}))) + +;; good +(re-frame/reg-event-fx :communities/invite-people-pressed + (fn [{:keys [db]} [id]] + {:db (assoc db :communities/community-id-input id) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch [:open-modal :screen/invite-people-community {:invite? true}]]]})) +``` + +### Registering top-level re-frame subscriptions + +Use `subs.root/reg-root-key-sub` to register top-level (root) subscriptions. +Additionally, register root subscriptions in the `subs.root` namespace. + +```clojure +;; bad +(re-frame/reg-sub + :view-id + (fn [db] + (:view-id db))) + +;; good +(reg-root-key-sub :view-id :view-id) +``` + +### Registering layer-3 subscriptions + +The majority of the subscriptions should be defined as [layer-3 +subscriptions](https://day8.github.io/re-frame/subscriptions/#the-four-layer) +due to performance constraints. + +```clojure +;; bad +(re-frame/reg-sub + :ens/preferred-name + (fn [db] + (get-in db [:multiaccount :preferred-name]))) + +;; good +(re-frame/reg-sub + :ens/preferred-name + :<- [:multiaccount] + (fn [multiaccount] + (:preferred-name multiaccount))) +``` + +### Requiring quo components + +Consume `quo` components from `quo.core`, unless the namespace is also inside +the `quo/` directory. + +```clojure +;; bad +(ns my-namespace + (:require [quo.components.icon :as icon])) + +(icon/icon :i/verified) + +;; good +(ns my-namespace + (:require [quo.core :as quo])) + +(quo/icon :i/verified) + +;; also good because both namespaces are inside quo/ +(ns quo.components.tabs.account-selector + (:require [quo.components.markdown.text :as text])) +``` + +### Require/import + +Prefer `:as` instead of `:refer`. There are exceptions to this rule, e.g. the +test macros `deftest` and `is`, which are ubiquitous in the Clojure community. + +```clojure +;; bad +(ns status-im.utils.datetime + (:require [cljs-time.coerce :refer [from-long]])) + +;; good +(ns status-im.utils.datetime + (:require [cljs-time.coerce :as time.coerce])) +``` + +### Javascript interop + +Use [binaryage/oops](https://github.com/binaryage/cljs-oops) macros instead of +core interop macros. + +```clojure +;; bad +(fn [^js event] + (.-width (.-nativeEvent event))) + +;; good +(require '[oops.core :as oops]) +(fn [event] + (oops/oget event "nativeEvent.width")) +``` + +### Accessibility labels + +Accessibility labels are currently used only for end-to-end tests. Use keywords +instead of strings (remember keywords are cached). + +```clojure +;; bad +[text/text {:accessibility-label "profile-nickname"} + "Markov"] + +;; good +[text/text {:accessibility-label :profile-nickname} + "Markov"] +``` + +Avoid dynamic labels, for example to specify an element's index because +[Appium](https://appium.io/) already supports element selection based on +indices. + +```clojure +;; bad +[button {:accessibility-label (str "do-something" index)}] + +;; good +[button {:accessibility-label :do-something}] +``` + +### Icons + +Use the appropriate keyword qualification/namespace. + +```clojure +;; bad +(require '[quo.components.icon :as icons]) +(icons/icon :main-icons2/verified) + +;; good +(require '[quo.core :as quo]) +(quo/icon :i/verified) +``` + +### Translations + +Prefer to use translation placeholders instead of creating multiple translation +keywords and concatenating them into a single string. + +```clojure +;; bad +;; Assume the translation key is: +;; "biometric-auth-error": "Unable perform biometric authentication" +(str (i18n/label :t/biometric-auth-error) "(" error-code ")") + +;; good +;; Assume the translation key is: +;; "biometric-auth-error": "Unable perform biometric authentication ({{code}})" +(i18n/label :t/biometric-auth-error {:code error-code}) +``` + +### Tests +#### Prefer `match?` over `=` when comparing data structures + +Prefer the `match?` directive over `=` when comparing data structures, otherwise +when the check fails the output can be too difficult to read. `match?` is +defined by library https://github.com/nubank/matcher-combinators. + +```clojure +;; bad +(deftest some-test + (let [expected {...} + actual {...}] + (is (= expected actual)))) + +;; good +(deftest some-test + (let [expected {...} + actual {...}] + (is (match? expected actual)))) +``` + +#### Subscription tests + +Test [layer-3 subscriptions](https://day8.github.io/re-frame/subscriptions/) by +actually subscribing to them, so reframe's signal graph gets validated too. + +```clojure +;; bad +(defn user-recipes + [[current-user all-recipes location]] + ...) + +(re-frame/reg-sub + :user/recipes + :<- [:current-user] + :<- [:all-recipes] + :<- [:location] + user-recipes) + +(deftest user-recipes-test + (testing "builds list of recipes" + (let [current-user {...} + all-recipes {...} + location [...]] + (is (= expected (recipes [current-user all-recipes location])))))) + +;; good +(require '[test-helpers.unit :as h]) + +(re-frame/reg-sub + :user/recipes + :<- [:current-user] + :<- [:all-recipes] + :<- [:location] + (fn [[current-user all-recipes location]] + ...)) + +(h/deftest-sub :user/recipes + [sub-name] + (testing "builds list of recipes" + (swap! rf-db/app-db assoc + :current-user {...} + :all-recipes {...} + :location [...]) + (is (= expected (rf/sub [sub-name]))))) +``` + +## Project Structure + +First, the bird's-eye view with some example ClojureScript files: + +``` +src +├── js/ +├── mocks/ +├── quo +│ ├── components/ +│ ├── foundations/ +│ └── theme.cljs +├── react_native +│ ├── gesture.cljs +│ └── platform.cljs +├── status_im/ +├── status_im2 +│ ├── common +│ │ └── components +│ │ └── bottom_sheet.cljs +│ ├── contexts/ +│ ├── setup/ +│ └── subs/ +├── test_helpers/ +└── utils.cljs +``` + +- `src/js`: Raw Javascript files, e.g. React Native Reanimated worklets. +- `src/mocks`: Plumbing configuration to be able to run tests. +- `src/quo/`: The component library for Status Mobile. [Read more...](../src/quo/README.md) +- `src/react_native/`: Contains only low-level constructs to help React Native + work in tandem with Clojure(Script). +- `src/status_im2/`: Directory where we try to be as strict as possible about + our guidelines and where we prefer to write code for the new, redesigned + mobile app. +- `src/status_im/`: Directory containing what we call "old code", not yet + migrated to new guidelines for the new mobile app. +- `src/status_im2/common/`: Directories named `common` can appear at any level + of the directory tree. Just like directories named `utils`, their directory + nesting level communicates their applicable limits. +- `src/status_im2/common/components/`: Contains reusable components that are not + part of the design system (quo). +- `src/status_im2/contexts/`: Contains [bounded contexts](#glossary), like + `browser/`, `messaging/`, etc. As much as possible, _bounded contexts_ should + not directly require each other's namespaces. +- `src/status_im2/setup/`: Contains namespaces that are mostly used to + initialize the application, configure test runners, etc. In general, such + namespaces should not be required from the outside. +- `src/test_helpers/`: Reusable utilities for writing all kinds of tests. +- `src/status_im/subs/`: All subscriptions should live inside it. + +Directories named `utils/` can appear at any level of the directory tree. The +directory nesting level precisely indicates its boundaries. For example, a +`contexts/user_settings/utils/datetime.cljs` file communicates that it should +only be used in the `user_settings` context. + +### src/quo + +The `src/quo/` directory holds all components for the new design system. As +much as possible, its sub-directories and component names should reflect the +same language used by designers. + +Even though the directory lives alongside the rest of the codebase, we should +think of it as an external entity that abstracts away particular Status domain +knowledge. + +Components inside `src/quo/` should not rely on re-frame, i.e. they should not +dispatch events or use subscriptions. + +Example structure: + +``` +src +└── quo + ├── components + │ └── dropdown + │ ├── style.cljs + │ ├── test.cljs + │ └── view.cljs + └── screens + └── dropdown + └── view.cljs +``` + +### Re-frame events + +Event handlers should be defined in files named `events.cljs`, and they should +be _close_ to other _things_, like view files, components, etc. + +For example: + +``` +src +└── contexts + └── browser + ├── bookmarks/ + ├── options/ + ├── permissions/ + ├── events.cljs + ├── events_test.cljs + ├── style.cljs + └── view.cljs +``` + +## Deprecation process + +To deprecate a var, add the `:deprecated` metadata and, if necessary, suggest an +alternative. + +```clojure +;; Good if there's no better alternative yet, but we want to deprecate it anyway. +(defn ^:deprecated foo + [] + (bar)) + +;; Good +(defn foo + {:deprecated "Use some.namespace/var-name instead."} + [] + (bar)) +``` + +Please check the [Clojure Style](https://guide.clojure.style/#deprecated) documentation + +To reduce visual clutter from deprecated methods in your text editor, consult this [example](https://rider-support.jetbrains.com/hc/en-us/community/posts/4419728641810-How-to-disable-the-the-strike-thru-for-deprecated-methods-in-Javascript-#:~:text=Try%20disabling%20%22Preferences%20%7C%20Editor%20%7C,It%20works%20for%20me). The approach can be adapted for settings in VSCode, Emacs, VIM, and others. + +### Test structure + +[Unit tests](#glossary) should be created alongside their respective source +implementation. We prefer them colocated with the source and not like most +Clojure (JVM) codebases which mirror the sources in a top-level test directory. + +``` +├── models +│ ├── message.cljs +│ └── message_test.cljs +├── models.cljs +└── models_test.cljs +``` + +Component tests should be created in the same directory as the source component, +and named as `component_spec.cljs`. + + +``` +└── filter + ├── component_spec.cljs + ├── style.cljs + └── view.cljs +``` + +There's no hard rule on how integration test namespaces should be split, but +we're at least striving to define them under appropriate bounded contexts that +mirror the source code. + +``` +test +├── appium/ +└── integration + ├── browser/ + ├── communities/ + ├── messaging/ + ├── user_settings/ + └── wallet + └── payment_test.cljs +``` + +## Glossary + +**Unit test**: The smallest atomic unit that's meaningful to test. For example, +tests for utility functions and event handlers are considered unit tests in the +mobile codebase. They should be completely deterministic, _fast_, and they +should work flawlessly in the REPL. + +**Bounded context**: A logical separation between different domains. It's an +important concept in the [Domain-Driven +Design](https://en.wikipedia.org/wiki/Domain-driven_design) literature. See +[Bounded Context, by Martin +Fowler](https://martinfowler.com/bliki/BoundedContext.html) for an introduction +to the topic. diff --git a/doc/patching.md b/doc/patching.md new file mode 100644 index 00000000000..f0de38639d7 --- /dev/null +++ b/doc/patching.md @@ -0,0 +1,13 @@ +# Patching + +## Libraries +If 3rd party library has an issue and fix is not yet released (or we can't switch to a new release), we use forks. Fix should be committed to the fork, tagged and referenced from package.json. + +Example: [`react-native-hole-view`](https://github.com/status-im/react-native-hole-view#refs/tags/v2.1.1-status) + +## React Native +When patch need to be applied to React Native itself Status does patching with Nix instead of doing it nodejs-way. + +Patches should be added to [this file](https://github.com/status-im/status-mobile/blob/develop/nix/deps/nodejs-patched/default.nix). + +Example: [patching `react-native/Yoga` to build app with XCode 14.3](https://github.com/status-im/status-mobile/pull/15589) diff --git a/doc/pipeline_process.md b/doc/pipeline_process.md new file mode 100644 index 00000000000..6324639fc7b --- /dev/null +++ b/doc/pipeline_process.md @@ -0,0 +1,125 @@ +# Pipeline process + +[Pipeline for QA](https://github.com/status-im/status-mobile/projects/7) is a project board for developers and testers used to track the status of a pull request, get reviews and manual testing, and run autotests. + +The generally accepted requirements for its use are described below: + +## Opening a PR +- Once a PR is created, it moves to the ```REVIEW``` column where a review will be requested automatically. +- You can also request a review inside the PR from a particular person if needed. +- When creating a PR, do not forget to assign it to yourself. +- Also in case the PR adds new functionality, a description **MUST** be added. + +### What if the work is still in progress? + +- If PR work is not finished yet, please mark it as a draft or add [WIP] to the title and keep it in the `CONTRIBUTOR` column until it's ready to be reviewed/tested. +- Alternatively, you can open a `draft`. + +### When is a PR considered ready to move forward? + +Ready for testing, a PR should meet the following criteria: + +1. Reviewed and has at least 1 approval. +2. Rebased to the `develop` branch (both `status-mobile` and `status-go` if needed, depending on what part has changes). + - **NOTE:** QAs can use the `Update with rebase` button from GitHub if: + - The PR has no `status-go` changes. + - The PR has no conflict with the develop branch. +3. All possible conflicts have been resolved. +4. Has the label: `request-manual-qa` or `skip-manual-qa`. +5. PRs **MUST** identify what area is affected and should have a description. + + +### E2E tests and analyzing the results + +The PR **MUST** be moved to the E2E column when it is ready for testing (**mandatory for all PRs**). +That will also trigger e2e tests run. QAs are monitoring PRs from E2E column and take it into test. +This step cannot be skipped. So, at least one comment from the `status-im-auto` bot with results is a prerequisite for moving forward. +Information on how to analyze tests can be found [here](https://github.com/status-im/status-mobile/blob/develop/doc/tests/how-to-launch-e2e.md). +Tests might be flaky, as they depend on infrastructure - SauceLabs and Waku. + +If there are `Failed tests` and you are not sure about the reason, you can always ping the mobile QAs for help (preferably in PRs by `@status-im/mobile-qa`). + +**For now, it is the only reliable source of regression in the app because, due to time constraints, it is not possible to test thoroughly each PR. +Please, respect this rule.** + +## Testing PR + +### Adding `skip-manual-qa` + +**Do not hesitate to use a `skip-manual-qa`** if you're sure that it is a simple flow and you checked it. +- Please ask another team member before adding the `skip-manual-qa` label (PR/Status community/DMs) so that there's a second opinion. +- The PR MUST have a proper reasoning why manual QA is skipped. +- The PR MUST include the steps of testing that has been done by the developer prior to moving it forward. + +**NOTE:** Make sure that QAs are OK with that; + +Before merging PRs, please make sure that information is added about how you tested the PRs, that e2s have been passed and their results have been reviewed. + +The QA team appreciates your help! + + +### Manual testing + +#### Prerequisites for manual testing +1. PR has a description of the change and details the area of the application affected by the change. +2. PR has automation results from the previous E2E test run step. +3. PR includes a demo, test steps, and any possible regression(s) +4. If appropriate, PR details what elements of a feature are out of scope. + +**If one of the prerequisites is missing, the PR will be moved to the contributor folder with a corresponding comment.** + +- If you think a PR needs and is ready for manual testing, please add the `request-manual-qa` label. +- QA engineer picks up one of PRs with the ```request-manual-qa``` label, drags the item to the ```IN TESTING``` column and assigns it to themselves. +- During testing, QA will add comments describing the issues found, and also review automation tests results. +Usually found issues are numbered as "Issue 1, Issue 2", etc. +When the first round of testing is completed and all issues for this stage are found, the QA can add the ```Tested - Issues``` label and drag the card to the ```CONTRIBUTOR``` column. These two actions are optional. + +**IMPORTANT NOTE:** when the issues are fixed, developer **MUST** notify the QA that it is ready to be re-tested again by mention them in the PR. + +- When manual testing of the PR is fully completed and all the issues are fixed, the QA adds the ```Tested - OK``` label and drags the card to the ```Design review``` column (the cases when design review is necessary are described below). +- If design review is not required, the QA drags the PR to the ```MERGE``` column. After that the developer merges PR into develop. +- If design review has been done, the designer (```@Francesca-G```) drags the PR to the ```MERGE``` column. +After that the developer merges PR into develop. +- If manual testing/design review was not carried out, the developer drags PR to the ```MERGE``` column themselves. + +### Design review +_**How do I know if a design review is needed?**_ + +There are three cases here depending on the changes in the PR: +1. **Functional PRs with UI changes:** after the ```Tested - OK``` label is added, the QA moves the PR to the ```Design review``` column + mentions ```@Francesca-G``` in comments. +2. **Component PRs:** once the PR has received a review from developers and e2e tests results, it can be moved directly to the ```Design review``` column by the developer (manual testing step can be skipped) + the developer mentions ```@Francesca-G``` in comments. +3. **Functional PRs changes in which are not related to UI (e.g. a crash fix):** skip the ```Design review``` step (the PR should only be manually tested by QA). + +There are three possible scenarios when the design review is completed: +1. **Approved by design** > the PR is moved to the ```MERGE``` column by Francesca +2. **Some changes requested, but can be fixed as followups** > need to be negotiated with Francesca > Francesca moves PR to the ```MERGE``` column and adds the ```Follow-up required``` label > follow-ups should be added **by the dev** right after the PR is merged +3. **Some changes requested, they should be fixed before merge** > Francesca reviews the PR again after the fixes and moves PR to the ```MERGE``` column from ```Design review``` + +--- +**Notes:** +- If your PR has a long story and started from `develop` branch several days ago, please rebase it to current develop before adding label +- if PR can be tested by developer (in case of small changes) and/or developer is sure that the changes made cannot introduce a regression, then PR can be merged without manual testing. Also, currently, PRs are not manually tested if the changes relate only the design (creation of components, etc.) and do not affect the functionality (see `skip-manual-qa` label) +--- + +#### Why my PR is in `Contributor` column? +PR can be moved to this column by the ```status-github-bot``` or by QA engineer with label `Tested-issues` or if one of the requirements for manual QA was not met. +In the first case most often this happens due to conflicting files in PR. +In the second case - after fixing of all found issues, the developer should ping the QA in the PR comments for retesting. + +## Merging a PR +**Merge conditions:** +1. Required number of reviews received +2. E2E results are received and reviewed +3. All commits are squashed into one. +4. No conflicting files in PR +5. No issues from lint +6. Pay attention to automation checks (some of them are not blockers, best to check before merge anyway) +![](images/pipeline-process/automation-checks.png) + + +6. In case of manual testing - the label ```Tested - OK``` from QA +7. In case of design review - the approval from the designer + +You can merge your PR into develop - some useful clues you can find [here](https://notes.status.im/setup-e2e#3-Merging-PR) + +HAPPY DEVELOPMENT! :tada: diff --git a/doc/pixel-perfection.md b/doc/pixel-perfection.md new file mode 100644 index 00000000000..49a4bd45252 --- /dev/null +++ b/doc/pixel-perfection.md @@ -0,0 +1,23 @@ +# Pixel Perfection +The Status Mobile team aims to align the design implementation perfectly according to a given screens respective Figma design spec. + +Currently, the Figma Designs match up with both "iPhone 11 Pro" and "iPhone 13" screen size. + +The Dimension reference for iPhone 11 Pro is 375 x 812 (width x height) and for iPhone 13 is 390 x 844. + +To test your implementation is correct you can take the following steps + +- Open a new Figma file so that you have write privileges. + +- Copy in the component or screen from the Figma file. + +- Using the "iPhone 11 Pro" or the "iPhone 13" simulator (it is the default now) you take a screenshot of your component and paste it into your Figma file. + +- To get the right size, set your screenshot to width according to your simulator: + - for iPhone 13 set 390 + - for iPhone 11 Pro set 375 + + +![](images/pixel-perfection/layer-width.png) + +- Lower the opacity so you can see it sitting on top of the Figma mock ups. diff --git a/doc/post-mortem.md b/doc/post-mortem.md new file mode 100644 index 00000000000..b88e496506b --- /dev/null +++ b/doc/post-mortem.md @@ -0,0 +1,42 @@ +This document MUST be updated everytime a PR has been identified has the source of a bug in develop. The post mortem analysis MUST include: the CAUSE of the bug, the RESOLUTION PROCESS and PREVENTION MEASURES. + + +# "sql: no rows in result set" error on iOS login + +## Cause + +There were errors in the iOS native code: +- "loginWithKeycard" method was not implemented +- "createAndLoginWithKeycard" method was actually used instead of "createAndLogin** which is used for regular account creation without keycard and doesn't have the same final argument + +QA process was focused on account creation and login with keycard, and this bug would only appear specifically on iOS when creating an account without keycard and only during the next login. It was therefore a more complex path to reach than what could have been expected from this PR, and it was caused in develop by a developer working on iOS simulator because that is a common path during development. + +## Resolution process + +- the commit was reverted locally by the developer which fixed the issue locally and confirmed the faulty commit +- the bug was only reproducible on iOS which strongly hinted at a potential issue with native code +- the missing method was not the origin of the problem but was found at this point and further analysis lead to the shadowing error + +## Prevention measures + +- PRs that change native code should be checked for platform parity: + - same methods are created/modified for each platform + - test cases in the PR should take these methods in consideration + +# Missing labels for en translation + +https://github.com/status-im/status-mobile/issues/9003 + +## Cause + +Unused labels in the application have been listed by QA and an issue created for their removal. The PR was made by an external contributor and it had to be rebased many time because it touched many files and other PRs got merged before this one could be tested. After a last rebase the PR was merged with 100% e2e test but no manual testing. + +The reason some translations got deleted despite the fact that they were used is because the `message status` related labels were not explicitly mentioned in the code unlike every other labels. They were built from the actual `message status` and the prefix `status-`. + +## Resolution process + +The mistake was quickly noticed in develop and an issue created. The fix was to recover the 3 labels that were accidentally deleted. + +## Prevention measures + +The code has been changed so that the labels are no longer generated and appear in the code like other labels. diff --git a/doc/pr-review-policy.md b/doc/pr-review-policy.md new file mode 100644 index 00000000000..3769c025ba6 --- /dev/null +++ b/doc/pr-review-policy.md @@ -0,0 +1,96 @@ +## Pull Request Review Policy + +This document details the pull request policy of the Status Mobile repository / team. + +- [Terms](#terms) +- [Policy](#policy) + - [Overview](#overview) + - [Full Policy](#fully-policy) +- [Rationale](#rationale) + - [Front pressure and Demand control](#front-pressure-and-demand-control) + - [Back pressure and Supply control](#back-pressure-and-supply-control) + - [Honour System](#honour-system) +- [Review Support](#review-support) +- [Background](#background) + +### Terms: + +| Term | Definition | +|----------------------|---------------------------------------------------------------------------------------------------------------| +| **Requester** | The team member submitting a pull request. | +| **Reviewer** | A team member asked by a requester to review the requester’s pull request. | +| **Review bandwidth** | The number of reviews a team member has self-limited themselves to giving within their defined review period. | +| **Review period** | The period of time over which a reviewer’s bandwidth is consumed | + +### Policy: + +#### Overview + +- 🔀 - You are responsible for getting your PR reviewed and approved. +- ✅ - You must give a review on a PR if you are asked to do so, unless your review limit has been reached. +- ✨ - The honour system underpins this policy, without acting in good faith it will not work. + +#### Fully policy + +1) The requester has the responsibility for getting a PR reviewed and approved. + 1) The requester has the right and responsibility to pursue code reviews. + 2) The requester has the right and responsibility to make multiple requests of the same reviewer. + 3) The requester will select a limited number of reviewers to review any of the requester’s PRs. +2) Reviewers are obligated to give a review for all PRs they are requested to review, except when their review bandwidth has been exhausted: + 1) Reviewers have the right to refuse a requester a review if the reviewer’s review bandwidth has been exhausted. + 2) Reviewers are expected to manage and track their review bandwidth. + 3) Reviewers can manage their review bandwidth via any means they feel works best for them. Examples include: + 1) Using the GitHub busy feature + 2) Declining a review on the PR + 3) Declining a review when pursued by a code reviewer + 4) Proactively declining to review a PR +3) All team members will set a review bandwidth that is reasonable. +4) All team members acknowledge that giving reviews is a requirement of membership of the team. +5) All team members will manage their review bandwidth honestly. + 1) The right to refuse a PR review is only eligible if a reviewer’s review bandwidth has actually been exhausted. + 2) Breaching the team’s honour is very bad form. Do not do it (please). +6) All team members acknowledge that without acting with honesty this review process will not work and jeopardises their own future ability to receive reviews. + +### Rationale + +#### Front pressure and Demand control + +Foremost in the rationale for the above policy is that giving a PR review is a requirement. If a requester requests + a PR review of a team member, the reviewer is now obligated to review the PR within a reasonable timeframe. + +This principle enforces that there is a pressure and demand that PR reviews must be given. + +#### Back pressure and Supply control + +Front pressure alone is not enough to balance the PR review load across the team. Because of this an exception is granted +to reviewers that have exhausted their review bandwidth, the exemption permits these reviewers to refuse a PR review. + +This principle gives the reviewer pool back-pressure, relieving the exhausted reviewer and increasing pressure / demand on +the other reviewers. + +#### Honour system + +The two principles of demand and supply control are underpinned by an honour system and are useless without honest action. +If a team member does not honestly manage their review bandwidth this action seriously damages the collective balance of +PR review load. Because of this potential damage a breach of the honour system is considered a deeply shameful act. + +The honour system is the mechanism of control and enforcement, without it the policy would need to be controlled and enforced +using less dynamic and forgiving mechanisms. An example is a GitHub review counting tools with quotas and metrics. So just +be cool. + +### Review Support + +Sometimes reviewing is time-consuming or generally painful, to help make reviews as simple as possible consider the following suggestions: +- Paired reviews: Ask an experienced reviewer to review with you. +- If a PR is the product of multiple developers only ask 1 or 2 other developers for their review. + +### Background + +The contents of this policy document is the product of 2 councils of the Status Mobile team, the below documents summarise the topics and outcomes of the councils: + +- [2023-05-15 - Council of Deadwater](https://docs.google.com/document/d/1TtUZCke2OzumjdWGu6TIECHEOUPwvaLrvQ8Re2nEGCo) +- [2023-06-13 - Mauve Council of Deadwater](https://docs.google.com/document/d/19hfr8xXrShLk9ROdQfOwb9_vwbTah9nOCqKSLIrgoSI) + +The aims of the council were to allow developers that are relied on heavily for reviews to better balance their time +between reviews and other responsibilities. Equally, the council aimed to give other team members better opportunity and +understanding that giving PR reviews is a requirement and will help the team generally if more review burden is shared. diff --git a/doc/react_tree.png b/doc/react_tree.png new file mode 100644 index 00000000000..e5ade680dd4 Binary files /dev/null and b/doc/react_tree.png differ diff --git a/doc/release-checklist.md b/doc/release-checklist.md new file mode 100644 index 00000000000..a40fca996f0 --- /dev/null +++ b/doc/release-checklist.md @@ -0,0 +1,21 @@ +#### Before submission +- [ ] Feature or improvements release? (Feature release needs more lead time and assets for comms) +- [ ] Sign off by QA +- [ ] App store listing content reviewed and updated ([copy](https://docs.google.com/spreadsheets/d/1fLDr3BD1dcg0dolvLiozXoZU27IQirjBzn0h4_QS2u0/edit#gid=0), [images](https://www.dropbox.com/sh/6baklctvxy3nuoo/AACG9kYIVdt2qREW76rP8SP-a?dl=0) ) +- [ ] Play store content reviewed and updated +- [ ] Release notes written and added (500 character limit for updates (Android), no Android references for App store) +- [ ] Comms timeframe agreed on (Mon/Tue ping when both submissions are in) +- [ ] Privacy policy reviewed and updated +- [ ] App translations for key features merged and tested +- [ ] Countries to be excluded +- [ ] Changes to our FAQ made + +#### Before publishing +- [ ] Translations for comms available +- [ ] Promotional content ready +- [ ] Update Draft with release on https://github.com/status-im/status-mobile/releases +- [ ] Add release to DO store and [update site](https://notes.status.im/s/status-release-upload#) (Jakub - requires credentials to upload) + +#### At time of publishing +- [ ] validate draft release on github release page https://github.com/status-im/status-mobile/releases +- [ ] update https://github.com/status-im/status-mobile/blob/develop/RELEASES.md with release notes diff --git a/doc/release-guide.md b/doc/release-guide.md new file mode 100644 index 00000000000..44b9b2cdbf6 --- /dev/null +++ b/doc/release-guide.md @@ -0,0 +1,117 @@ +# Description + +This document explains some of the steps that are involved in releases. + +# Release process + +## Coordination + +All the coordination is done in the `#release` channel on discord. + +Release readiness can be tracked by watching an appropriate milestone at [milestones](https://github.com/status-im/status-mobile/milestones) section in github. + +When release is building you can check the progress in a testrail. Ask @churik for access if you don't have it + +## Preparing branches +Special branches should be created at the start of a release process. That can be done manually or with the script + +### Prepare branches with the script + +You can use the script to cut a release branch. No arguments needed, the new release version will be deducted automatically: + +`scripts/cut-release.sh` + +Upon script call two branches will be created: +- `release/a.b.x` +- `chore/update-release-to-$version` + +The first one is the main release branch. All bugfixes should be cherry-picked from `develop` to it. Jenkins treats that branch specially and prepares release builds from it. + +The second branch only updates the `VERSION` file and all you need to do is to merge it to `develop`. If you have the `gh` command installed, `cut-release` script will automatically create a PR for this. + +### Prepare branches manually + +#### Create a branch + +First, we need to create a release branch cut from develop. +The name of the branch should be `release/a.b.x` where `x` is actually `x`, not a variable, and `a` and `b` are the `MAJOR` and `MINOR` version of the app. + +For example, valid branch names are: + +`release/1.12.x` or `release/3.12.x` + +#### Update version + +We need to update the `VERSION` file to reflect the release we want to build. + + +## Building + +Jenkins will automatically build this branch and create release candidates. +Those will go through the QA process and the QA team will let us know when +tested successfully. + +You can see the state at https://ci.infra.status.im/ -> status-mobile -> release -> pick the right version number + +## Bugfixes before an app is released + +Because both `status-mobile` and `status-go` might have newer commits that we don't want to include, bug fixes should be applied separately to develop and the release branch. +The best workflow is to send a PR to develop, get it merged, and the cherry-pick on the +release branch. + +It is a release manager's responsibility to cherry-pick bugfix from `develop` to release branch. In order to do this, you should be a part of a `release group` on github. Contact @jakubgs to be added. + +If cherry-picking not practical for some reason (i.e develop has diverged significantly from release) +other workflows are possible. + +To change status-go, a similar workflow applies. +First checkout the tag used in `status-go-version.json` of release branch. + +`git checkout v0.62.3` + +Create a branch on status-go named: `release/vx.y.z+hotfix.w` where (make sure branch name is not the same as the tag) + +`x` is the `MAJOR` version +`y` is the `MINOR` version +`z` is the `PATCH` version +`w` is the `HOTFIX` version + +For example valid names are: + +`release/v0.62.3+hotfix.2` +`release/v1.3.4+hotfix.4` + +Once the branch is ready to use and tested successfully, tag the branch: + +`git tag v0.62.3+hotfix.2` + +Push the branch and then the tag to origin: +`git push --set-upstream origin release/v0.62.3+hotfix.2` +`git push origin v0.62.3+hotfix.2` + +Switch to status-mobile release branch, and cherry pick the commit you need. + +Once that's done, update the status-mobile release branch with the new tag: + +`make shell` + +`scripts/update-status-go.sh v0.62.3+hotfix.2` + +And commit and push the changes + +# Hotfix process + +The hotfix process is basically identical to the workflow of a bugfix: + +1) Add the changes on status-mobile +2) Make sure you use a hotfix branch for status-go (unless we are happy to release from develop) +3) Update the `VERSION` in status-mobile + +# PlayStore Metadata Updates +This section is relevant only for the marketing team, no developer participation is required. + +You can update Play Store release metadata using `fastlane android upload_metadata`. + +But that requires credentials necessary for accessing Play Store API. The simpler way is to edit files contained within [`fastlane/metadata`](metadata) and run the following CI job: + +https://ci.status.im/job/status-mobile/job/tools/job/update-playstore-metadata/ diff --git a/doc/sign_in/readme.md b/doc/sign_in/readme.md new file mode 100644 index 00000000000..c18c0f82086 --- /dev/null +++ b/doc/sign_in/readme.md @@ -0,0 +1,70 @@ +# Sign in flow + +TODO: this document is outdated + +## tl;dr +In general, at first we try to encrypt realm database using password +and then proceed with the rest of operations. We do not wait for +`Statusgo.Login` callback in order to show `:home` screen with chats +as this call may add more than 1s to the process. + +![img](sign_in_diagram.jpg) + +## In details + +#### Main flow: +1. User enters password +2. Presses "Sign In" +3. Attempt to open account's db +4. + _a._ If DB file doesn't exist go to [**Empty DB flow**](#empty-db-flow) + + _b._ Otherwise continue +5. + _a._ If db can't be encrypted go to [**Verify password flow**](#verify-password-flow) + + _b._ Otherwise continue +6. + _a._ If db can't be migrate DB schema **show migrations failure popup with + option to erase DB** + + _b._ Otherwise continue +7. Proceed with [**UI flow**](#ui-flow) and [**Statusgo.Login flow**] +(#statusgologin-flow) + +#### Empty DB flow: +1. Verify password +2. + _a._ If password is wrong **show "Wrong password" label** + + _b._ Otherwise continue +3. Create account's DB (by opening it with correct encryption key) +4. Proceed with [**UI flow**](#ui-flow) and [**Statusgo.Login flow**] +(#statusgologin-flow) + +#### Verify password flow: +1. Verify password +2. + _a._ If password is correct **show encryption failure popup with option to + erase DB** + + _b._ Otherwise **show "Wrong password" label** + +#### Statusgo.Login flow: +1. Start node +2. "node.ready" event received +3. Statusgo.Login is called +4. Statusgo.Login callback +5. + _a._ If password is incorrect **show error popup (passwords used for db + encryption and for account do not match)** + + _b._ Otherwise continue +6. Done + +#### UI flow: +1. Load necessary data for last 10 chats +2. Show `:home` screen +2. Load the rest of chats +3. Show them too +4. Done diff --git a/doc/sign_in/sign_in_diagram.jpg b/doc/sign_in/sign_in_diagram.jpg new file mode 100644 index 00000000000..548fccc6dd7 Binary files /dev/null and b/doc/sign_in/sign_in_diagram.jpg differ diff --git a/doc/sign_in/sign_in_diagram.xml b/doc/sign_in/sign_in_diagram.xml new file mode 100644 index 00000000000..d9537608ab5 --- /dev/null +++ b/doc/sign_in/sign_in_diagram.xml @@ -0,0 +1 @@ +7V1dd6M2Gv41PtteTA4gwHA5idNu2+lMutnNdPZmjwwC02LkCjyJ++tXMsgGIduaGAHO8VxkbAEC3s/n/ZA8AXfLlx8JXC1+xSFKJ5YRvkzAbGJZvmPQv2xgUw5MTb8ciEkSlkPmfuAx+RtVg9V18ToJUd44scA4LZJVczDAWYaCojEGCcHPzdMinDbvuoIxag08BjBtj35OwmJRjnr8tdj4P1ESL/idTaM6soT85GogX8AQP9eGwP0E3BGMi/LT8uUOpYx2nC7ldT8cOLp7MIKyQuWC+ebp68/LX/3ZE948PcysF+Ljd5ZXPVyx4W+MQkqA6ismxQLHOIPp/X70luB1FiI2rUG/LYplSj+a9CN6SYrf2fCNU337wo9kBdnUDrGvX6oJ/kBFsak4D9cFpkP7+37AeFXN0X7higY5XpMAHXlLLjiQxKg4Ro3yPEaB2g0qcv6I8BLRx6YnEJTCIvnaFBFYSVq8O2/PDPqh4sc38Kaa9ytM19WdHgjK8wnjs/vXmsnN7WMSZ3Tgp2w/1GLonl2Mis+LpECPK7gl2DPV2SYLoyRN73CKyfZaEELkRQEdzwuC/0S1I27goXl0jC1fESnQy1FCVkeB6ZSXbLgWTW+qkee90plckxY1hQOGJupbLep/WiFGaxgElKDFP/KtIMB0Sf+fvQmqe9bgVLdbVGekNSh9EFPMlyQv6C1/aFN7gZfzdX6a0p3QzWjQjTo1Gd2AhG5eB3QDP01/u/3fu/XP//plMbv9+PnJ/c+Xd06XZly/QXYUDbIPRmWRnZZ03kFmElAWkM2qKA3B0NJpNaUTTM22bJoS2ZwCXZZ02inG2CGJBo7YwgpVwRXNrYO80JaZW8+aA9c9S9RdRVHnLOlO1LeXvicEbmonrHCSURO6n/mBDdTEZ9p0Cq7tCAJQzrgXh92jnWH1py3FGsKeCWIReQEKpF547jm2Y/QiFnbnYnEeJjX0KLP5tpR56o1EmV1BmV39yuy2dPlxHQQ0cInWabopo5eUBSpzQj/F7NMyiQksqIBQFgQLtISDO1GjaQVtTwrxbIkbdXVBYxUbmYXvWc6DfgtSmOdJoBqfv14lVGPmGt0cCdn42LcpRFviBcY5e8bxSco3qq6r50tOeMIazOdTlXagNVVXmuTp5HifZtNWNJvOqGXJBa+XJeekWGqWJV+nLB3L7vUpZ6phpXuVM11yxsOYuvunt2TPmOEQtb366NNktt0MqF0eYJ9Kkjm6kIDZzk2OAgpw3p+OdVXTOv0ooO03OTwV05uq6ue4wkSWMJFu5QMSweB1AaZ+NwTBcFOvFRh5EtN46g3o5dRQ1EttCN1sZ6/HoZdAVS/HBcBEvfREdVLWS1FUpj3rJbeMXadABymmqqIsMFWUJr1JEEeAV55rHBeW4+frSZqY7dIChU3FOo/xzQccJ1k7afIWTLY/tMnevUMHJtusGeyaZh6oVfSZbFaNv/nLj8X+AyED4746MLL9k1Np9gHgDbkALienBarz+u3rXIDd5D6v4B/GC0fP1+MCOFFrpugJkSTavmit52RFTdAzJiH9+F2+9RHvYvz9JXoDMcEKlLt+tPkDVyXn2nslUmvSzFKNDtzBmjGkLS9ml4w5ZE/HWj5WZpppqPp0vSbYFU2q7/VgUtv5kDtMCOsTtly4ZEYwm+erLSdEeL0zswOXIm2viZmB02u3mTzP3imW0Y9XgCpeMUehK2Ll0T+OVo6frkezOEWlmcaqKZkFrQZlp2WUoWwz7RgQhDqPajvQN6G5k+faa7rmybrnukAkUjdna3JzvtPouTFvDAD4wAPFnfTpEektogCqISoXvJH0Q4F2yvdxgZ/ZAxJC/T2lBl6tV+dJdYfAQl0PdrULrgiOcne4NnTu+VqU4UKaz3g+9aSOeCOJuoVEqmk4PXgmv9O4YIzYX1kOuHEai61sd7B9wHG8bfvbYoW83ic4NOy2hCqALFXt9gq6wRCCfYaY+oO59FeZK1N0eOA47D5xvibcLevjKjF0vmJrUUoUzTFIuS4lwUy7Ipika8KWUQU4ixKyhNWBCqEY3z0nVLItA/MrttgdEZizi2a33/M70Sev3+wCwY3TZJ01HT7z6HSaaKwBfYPa3SbQN3cD/QN9XgG8FKsgVCyBd9woHD9d0+qZdsvdEZtQ9tpfTYJoEgRrfqDjv1+TIOsnqhIme0qXAxHeimGwI84+1wKiCNB/9aHy2o+fauwrJ2hOSofnB1lK6Vo0+dbkT4YzJDCzGoIpSxCBWUC5xUzPLeNSEsD0fXVgmYTh1r7JBKVp87rgvC8qrSthvWyth+np4nx7Ec2V8xo4L7RL2/7wnJct87lyvmvO20BAgNLac7+cl5Wer5zvmvOO0AkEXG9wzh8O6r6J864bBIbR5vyX+8cr6yftQpktTWnLWC82HnfXb2JdWd+L1tvjY70M411Sgs9V70GZjCGUF2XA8ew6EyX1C+PY+WcH87d/xvefivzpv58XfznZ0+8P/34Iuu1qGpEMjGRHhLYMnEjy2qIM9JDQcSVtSwTBguVc6p2gM0nbUkt8LqAP1FPabKHX1Mv0cE7tCsb1uWXPt4cG4xwj6t4R82zb3LElbZu+adP0uY4wherai/ZERt8bachQ9iW52anqxkNe51DrPGVqY9wPGIbsTRZo+xQ5ExQc0T8ZYsV/uH26EBaQ/hcRvBR8Xji/QA8ntPDulp8Ot1LZvDCcyeHjaZypurdHTwpwOKknqc1NwPsFvfFk3x5rVAW4KCFbTWGhlBEsIMO4+itvg2iHy43zKe2wdUEAT2Ud/5i1hVN0gE7B8wgv2Tym1AzILm4u2ai04NLF3TeHdgZeO9ycffp43xb5NE1W+aHY4AhZz2iKfTVZTUOVrtoa9vyOYshrbvdEWl9YJu7wTsmhQkhfFm5cGd99EVdoMPfdgVP6fre/ptHBmtTXYwhfNbPrj6vL3G97s7e9jN70d+nS3ZbvsgQqr3v3gil8V4smXMhKHa4Sp5tc/XHpjqad+i9jTb0y17gmjYVr7XD14la5W0KttbY/T92E9QnfTUPPakPjxvInzYZ8w50M1pDPhfnSVt7y527nCmRFUQ6hPxOcxQ3vX1uTnsI56nYnzCiKLLm9Ct2563QU+lriQktJAOT1ml42rqFvLxGQWFqzPGfotibTkG2pdy2dd967LpTOLdvvsXQu3aqi0wWqNX859WwRPw7kLHe7KV2at9w9+NVdUren8muTMofp6MoYmtIt3a8OU3sLODBU1328wmHSr/uf9y17Wfa/kQzu/w8= \ No newline at end of file diff --git a/doc/starting-guide.md b/doc/starting-guide.md new file mode 100644 index 00000000000..959f098c9a1 --- /dev/null +++ b/doc/starting-guide.md @@ -0,0 +1,131 @@ +# Description + +This document provides information on how to start developing Status App. + +# Getting Started + +To start developing clone the status-mobile repo in the directory of your choice. +``` +git clone https://github.com/status-im/status-mobile.git +``` + +Then open a terminal and cd into this directory. + +``` +cd status-mobile +``` + +Then build the clojure terminal + +``` +make run-clojure +``` + +note: ⚠️ This might take a while if this is your first time. + +This command installs `nix` and pulls in all the dependencies. +Do answer with "Y" to all the prompts and press "Enter" when `nix` setup asks you to Acknowledge. +This command builds the `jsbundle` and then compiles `Clojure` into `JavaScript`, watches for changes on `cljs` files, and hot-reloads code in the app. + +wait till you see the following message : + +``` +[:mobile] Build completed. (1801 files, 52 compiled, 0 warnings, 9.52s) +``` + +Once the clojure terminal is running you need to run the appropriate command next for your platform in a separate terminal : + +`make run-android` or `make run-ios` + +These commands will build the app, start a metro bundler and deploy the app on your simulator OR connected device (android only). For building and deploying to connected iPhones use `make run-ios-device` instead of `make run-ios` +Also check [developing on a physical iOS Device](#Additional-requirements-for-developing-on-physical-ios-device). + +## Simulators and Devices +### Android + +You need to have an emulator like [AVD](https://developer.android.com/studio/run/emulator), or [Genymotion](#genymotion-virtualization), or a real device running and visible to [adb](https://developer.android.com/studio/command-line/adb), before you run `make run-android`. + +### iOS + +#### Running on a simulator + +We highly recommend using either the `iPhone 11 Pro` or `iPhone 13` simulator as its screen dimensions match with our design. + +If you have Xcode `v12.x` (and above) installed in your system, you need to follow the below steps to add `iPhone 13` simulator: + +1. Open Xcode +2. Menu `>` Window `>` Devices and Simulators +3. Tap `+` button on bottom left +4. Select **Device Type** as `iPhone 13` +5. Leave the **Simulator Name** empty and tap on **Create** + +##### NOTE ⚠️ + +Running `make run-ios` will target `iPhone 13` by default. + +If you need to run on any other simulator, you can specify the simulator type by adding the `SIMULATOR` flag: +```sh +make run-ios SIMULATOR="iPhone 15" +``` + +# Build release + +To build the app, you can simply run on of the following: +``` +make release-android +make release-ios +``` + +For more `make` targets run `make help`. + +# Updating Dependencies + +* `make nix-update-pods` - iOS CocoaPods dependencies (updates `ios/Podfile` and `ios/Podfile.loc`) +* `make nix-update-gradle` - Android Gradle/Maven dependencies (updates `nix/deps/gradle/deps.json`) +* `make nix-update-clojure` - Clojure Maven dependencies (updates `nix/deps/clojure/deps.json`) +* `make nix-update-gems` - Fastlane Ruby dependencies (updates `fastlane/Gemfile.lock` and `fastlane/gemset.nix`) + +# Manual Steps + +There are a few manual steps you might want to do in order to start contributing. + +## Genymotion Virtualization + +Optionally set up Genymotion if you don't want to use Android Virtual Device: + +https://www.genymotion.com + +## Android Development Environment + +You can also setup Android Development Environment + Simulator: + +https://facebook.github.io/react-native/docs/getting-started.html + +## Configure GitHub Account + +The optimal way of pushing to GitHub is using SSH instead of user/pass auth. + +It's recommended that you [add your public SSH key to your GitHub account](https://help.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account). + +## Configure GPG Keys for signing commits + +In order to increase security we require all commits in `status-mobile` repo to be signed with a GPG key. + +Steps: +1. [Generate a new GPG key](https://help.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key) +2. [Setup Git to use your GPG key](https://help.github.com/en/github/authenticating-to-github/telling-git-about-your-signing-key) +3. [Setup Git to sign commits](https://help.github.com/en/github/authenticating-to-github/signing-commits) +4. [Setup GitHub to validate commits](https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account) + +## Additional requirements for developing on Physical iOS Device + +To use a physical iPhone your device UDID must be added to provisioning profiles and your Apple account invited as Developer to Status team. + +1. [Get your UDID of your iPhone.](https://www.extentia.com/post/finding-the-udid-of-an-ios-device) +2. Request from someone with access like @cammellos or @jakubgs to +- Add the UDID to development devices on Apple Developer Portal. +- Invite your Apple account to be Developer in Status team. +3. Open XCode using the project from `status-mobile/ios` directory. +- You might see error: `Select a development team in the Signing & Capabilities editor` +- Select `Status Research & Development GmbH` as the development team. +4. In a new terminal execute `make clean` and then `make xcode-clean` diff --git a/doc/status-go-changes.md b/doc/status-go-changes.md new file mode 100644 index 00000000000..c6e9d48ef9e --- /dev/null +++ b/doc/status-go-changes.md @@ -0,0 +1,31 @@ +# How to make changes to status-go + + +## Overview + +To make changes to status-go, the best approach is to first identify the issue/feature and the changes to be made directly in status-go. +For any bugs we want to most likely write a failing test in status-go, and make sure that it passes, before we test on mobile. + +Once you are quite confident that the status-go code is ready to be tested on mobile, you can test it locally by running the following command: + +``` +env STATUS_GO_SRC_OVERRIDE={your-status-go-directory} make run-{android/ios} +``` + +This will recompile status-go and run status-mobile on your device with that updated status-go version. + +Once you have tested the status-go changes locally and you think it's ready for a PR: + +1) Create a PR in status-go + +2) Point your local status-mobile branch to that PR by running the script: + +``` +scripts/update-status-go.sh {status-go-branch-name} +``` + +This will update `status-go-version.json` + +Commit updated status-go-version.json and create a status-mobile PR + +Once both PRs have been approved and are good to merge, you can follow the [merge guidelines](./merging-pr-process.md) diff --git a/doc/tests/component-tests-overview.md b/doc/tests/component-tests-overview.md new file mode 100644 index 00000000000..1e336729068 --- /dev/null +++ b/doc/tests/component-tests-overview.md @@ -0,0 +1,55 @@ +# Component Tests + +The component tests are using React Native Testing Library - https://callstack.github.io/react-native-testing-library/ +and Jest - https://jestjs.io/ + + +It is highly recommended to read some advice from Kent C.Dodds on how to write tests and use these tools correctly. + + +https://kentcdodds.com/blog/common-mistakes-with-react-testing-library + +https://www.youtube.com/watch?v=ahrvE062Kv4 + +Both of these links are showing it for React-Testing-Library (not Native) however the approach is for the most part considered the same. + +## Running the tests +To run these tests there are two methods. + +`make test-component` +setups and runs the test suite once. + +`make test-component-watch` +setups and runs the test suite and watches for code changes will then retrigger the test suite. + +## Writing Tests +New test files will need their namespace added to either the file "src/quo/core_spec.cljs" or "src/status_im2/core_spec.cljs. These locations may update overtime but it is dependent on the entrypoint in shadow-cljs config discussed below. + + +### Best practices +For the moment we will keep best practices for tests in our other guidelines document: + +To that point these guidelines will follow the conventions of Jest and React Native Testing Library recommendations and Status mobile will just stack their preferences on top. + +### Utilities +There is a file of utility functions defined in "src/test_helpers/component.cljs" and "src/test_helpers/component.clj". It will be great to use these utilities and to add any common testing tools to these files as it should make writing tests easier and faster. + + +## Configuration +Status Mobile has a bespoke tech stack, as such there is more complexities to configuring the tests. + +### Shadow-CLJS +the configuration for compiling our tests are defined in the "shadow-cljs.edn" file. +The three main parts of this are +`:target :npm-module` +Needed for the configuration we are using +`:entries` +a vector of entry points for the test files. +and the `ns-regexp` to specify what tests to find. Since we have multiple forms of tests we decided that "component-spec" is the least likely to detect the wrong file type. + +It's worth knowing that our tests are compiled to JS and then run in the temporary folder `component-tests`. + +### Jest +There is also further configuration for Jest in "test/jest". There is a jest config file which has some mostly standard configuration pieces, where the tests live, what environment variables are set etc. This is documented by Jest here: https://jestjs.io/docs/configuration + +There is also a setup file which is used to set some global and default values. Additionally this file is used to mock some of the react native (among other) dependencies. diff --git a/doc/tests/how-to-launch-e2e.md b/doc/tests/how-to-launch-e2e.md new file mode 100644 index 00000000000..a536042f3a2 --- /dev/null +++ b/doc/tests/how-to-launch-e2e.md @@ -0,0 +1,103 @@ +How to Launch E2E +=== + +## Overview for how automated test structured for Status app + +As a part of CI for Status mobile app and in order to ensure there are no regressions appear after the changes in code (bug fix, new/updated feature) we are using automated tests (e2e tests). + +- Automated tests written on Python 3.9 and pytest. +- Appium (server) and Selenium WebDriver (protocol) are the base of test automation framework. + +TestRail is a test case management system tool where we have test cases. + + +Each of the test case gets a priority (Critical/High/Medium) + +**SauceLabs** - is a cloud based mobile application test platform. We are using Android emulators (Android 10.0) for test script execution there. We have 16 session be running at the same time max. +For now we support e2e for Android only. + +## What's happening when any e2e job is running +Whenever we need to push set of test scripts we create 16 parallel sessions (max, but depending on amount of cases that are included in job) and each thread: 1) uploads Android .apk file to SauceLabs -> 2) runs through the test steps -> 3) receives results whether test failed on particular step or succeeded with no errors -> 3) Parse test results and push them as a Github comment (if the suite ran against respective PR) and into TestRail. + +We push **whole automation test suite (currently 155, amount is changing)** against each nightly build (if the nightly builds job succeeded). Results of the test run are saved in TestRail. +And also we push set of autotests whenever PR with successful builds got moved in to `E2E Tests` column from [Pipeline for QA dashboard ](https://github.com/status-im/status-react/projects/7). +In that case we save results in TestRail as well and push a comment with test results in a respective PR. + +For example: https://github.com/status-im/status-react/pull/9147#issuecomment-540008770 + +![](../images/how-to-launch-e2e/how-to-launch-e2e-1.png) + + +The test_send_stt_from_wallet opens link in TestRail https://ethstatus.testrail.net/index.php?/tests/view/890885 where performed steps could be found +List of all runs performed by test jobs could be found here https://ethstatus.testrail.net/index.php?/runs/overview/14 + +**For credentials for TestRail to see results ping Chu in DM**: + +Opening any test run navigates you to list of test cases with results: +![](../images/how-to-launch-e2e/how-to-launch-e2e-2.png) + + +## What about launching e2e manually + +To manage e2e there are several jobs in https://ci.status.im/job/status-mobile/job/e2e : +1) [nightly](https://ci.status.im/job/status-mobile/job/e2e/job/status-app-nightly/) - running automatically after building nightly apk e2e build. QA running it manually for results of e2e when testing release. +2) [upgrade](https://ci.status.im/job/status-mobile/job/e2e/job/status-app-upgrade/) - running manually by QA in release testing for smoke upgrade tests. +3) [prs](https://ci.status.im/job/status-mobile/job/e2e/job/status-app-prs/) - running **only automatically** when PR moves into `e2e column` +4) [prs-rerun](https://ci.status.im/job/status-mobile/job/e2e/job/status-app-prs-rerun/) - for manual run, can be run on request by anyone. **If you need to launch e2e against your build, use this job.** +Params to specify: + +- apk: [url_to_apk_build_here] +- pr_id: pull request number (e.g. 1234) +- branch: branch name from which the test are taken (in most of cases `develop`) +- keyword expression: tests by area (let's say `ens` or `chat`, they can be combined`ens or chat or send_tx`. All keywords can be found in testrail, ping Chu for details) +- test_marks: tests by priorities (by default: `critical or high or medium`, which corresponds the whole suite; to launch the same suite as in PRs, use `critical or high`) +- testrail_case_id: here is the list of test cases which you may find in test rail (4-digit value) + +For easier access you can hit `Rerun tests` in GH comment and testrail_case_id/ apk_name/ pr_id will be filled automatically. For making sure that tests are being rerun on most recent e2e build it is recommended to paste link to the last e2e build in apk_name field. The list of PR builds can be found in Jenkins Builds block on PR page. +![](../images/how-to-launch-e2e/how-to-launch-e2e-3.png) +And then hit ‘Build’. +Once the job starts it picks up specified tests, runs them against provided apk and sends results to pull request. + +Even we have 16 parallel sessions for testing it’s a time consuming operation (whole test suite we have automated at the moment takes ~140 minutes to finish). +So for PRs we pick only set of `critical or high` (you can also use this in TEST_MARKS param for job) +tests (otherwise some PRs could wait their turn of the scheduled Jenkins job till the next day). + +## Analysing test results (and why test fails to pass) + +After automated test run finished test results could be found in GH comment (if the test suite ran against PR) and TestRail. There are two states of the test: Passed and Failed. Test failure happens when certain condition of test step has not met or automated test can not proceed execution because it can not find the respective element on screen it expects should be there. + +Several examples of when test fails to succeed: + +- Test clicked on element which should load new screen (or pop-up) and awaits some element on this screen. But test did not wait enough allowing the new screen to appear and so it fails with “Could not find element XYZ” (this case is more app issue in our opinion rather then test issue, but we just can not spend our and dev time with too specific random places which happens once causing app lags in different moments) + +- Test sent transaction to address but it was not mined in time (we have a limit to wait until balance is changed on recipient side up to ~6 mins now). We classify this as False Fail, because it’s not the app issue but more network issue. + +- Test infrastructure issues, - anything related to infrastructure including SauceLabs side issues (apk failed to install - rare case, or LTE connection was set by default instead WiFi or unexpected pop-up appeared preventing test to going further) + +- Failure due to changed feature which has not been taken into account in some test after code merge (for instance: some element on screen has been removed, and we want to locate another element on this screen via XPath which is different now) + +- **Valid issue in the automated test scripts** - that's what we're looking for + +Example: here is the test results https://github.com/status-im/status-react/pull/13015#issuecomment-1016495043 where one test failed. + 1. Open the test in TestRail and open session recorded for this test in SauceLabs +![](../images/how-to-launch-e2e/how-to-launch-e2e-4.png) + + +In TestRail you may find all the steps performed by the test. +In SauceLabs testrun page you may find useful: video of the session, step logs, logcat.log of the session + 2. Analyze step where test was failed +For particular example it was failed on `Recover access(password:qwerty, keycard:False)` and unexpected error appeared. + +## Limits for e2e tests coverage +Not all features of the app could be covered by e2e at the moment: + +- Colours or place of an element on UI. +- Real ETH/token transactions. That’s the main reason we have separate .apk build for automation needs - it defaults to Sepolia network. Also it has enabled keycard test menu, ENS names and chat commands are also on Sepolia network (the same in PR builds, but not in nightlies / release) +- Autologin/Biometric related actions (autologin available when device meets certain conditions like the it has set unlock password and device is not rooted: all emulators are rooted in SauceLabs) + +## Brief flow for test to be automated + +Whenever there is a need to have a new test: +1) Create a test scenario in TestRail. +2) If certain item could be checked in scope of existing test case we update existing one (otherwise we may have thousands of test cases which is overkill to manage in TestRail as well as in automated test scripts). And also complex autotests increase probability to not catch regressions by stopping test execution (due to valid bug or changed feature) keeping the rest test steps uncovered. So here we need to balance when it makes sense to update existing test case with more checks. +3) Then we create test script based on the test case, ensure test passes for the build and pushing the changes to repo. diff --git a/doc/tests/how-to-run-local-tests.md b/doc/tests/how-to-run-local-tests.md new file mode 100644 index 00000000000..317a38b493d --- /dev/null +++ b/doc/tests/how-to-run-local-tests.md @@ -0,0 +1,94 @@ +# Local testing + +## Unit & integration tests + +To run all tests: + +``` +make test +``` + +Also test watcher can be launched. It will re-run the entire test suite when any file is modified + +``` +make test WATCH=true +``` + +Developers can also manually change the shadow-cljs option `:ns-regex` to control which namespaces the test runner should pick. + +## Testing with REPL + +The most convenient way to develop and run test locally is using REPL: + +1. Run command `make test-watch-for-repl`. +3. Once you see the message `[repl] shadow-cljs - #3 ready!` you can connect a REPL to the `:test` target from VS Code, Emacs, etc. +4. In any test namespace, run [cljs.test/run-tests](https://cljs.github.io/api/cljs.test/#run-tests) or your preferred method to run tests in the current namespace. + +You can run single test in REPL like this + +```clojure +(require 'cljs.test) +(cljs.test/test-var #'status-im.data-store.chats-test/normalize-chat-test) +``` + + + +Tests will use the bindings in `modules/react-native-status/nodejs`, if you make any changes to these you will need to restart the watcher. + + + +### Example in Emacs + +In the video below, you can see two buffers side-by-side. On the left the source implementation, on the right the REPL buffer. Whenever a keybinding is pressed, **tests in the current namespace instantly run**. You can achieve this exact flow in VS Code, IntelliJ, Vim, etc. + +[2022-12-19 12-46.webm](https://user-images.githubusercontent.com/46027/208465927-4ad9a935-5494-45e7-85b0-8134dc32d1a1.webm) + +### Example in terminal emulator + +Here I'm showing a terminal-only experience using Tmux (left pane Emacs, right pane the output coming from running the make target). + +[2022-12-19 13-17.webm](https://user-images.githubusercontent.com/46027/208471199-1909c446-c82d-42a0-9350-0c15ca562713.webm) + +## Component tests only + +To run tests: + +``` +make test-component +``` + +Also test watcher can be launched. It will re-run the entire test suite when any file is modified + +``` +make test-component-watch +``` + +Check [component tests doc](./component-tests-overview.md) for more. + +## Unit tests only + +To run unit tests: + +``` +make test-unit +``` + +Also test watcher can be launched. It will re-run the entire test suite when any file is modified + +``` +make test-unit WATCH=true +``` + +## Integration tests only + +To run integration tests: + +``` +make test-integration +``` + +Also test watcher can be launched. It will re-run the entire test suite when any file is modified + +``` +make test-integration WATCH=true +``` diff --git a/doc/tests/tests-overview.md b/doc/tests/tests-overview.md new file mode 100644 index 00000000000..6a60149affe --- /dev/null +++ b/doc/tests/tests-overview.md @@ -0,0 +1,167 @@ +# Tests + +## Introduction + +This document provides a general overview of the types of tests we use and when +to use them. It is not meant to be a tutorial or a detailed documentation about +testing in software development. + +## Types of tests + +Tests in `status-mobile` are comprised of: + +- Unit tests + - Subscription tests + - Event tests + - Tests for various utilities +- [Component tests](./component-tests-overview.md) +- Integration/contract tests +- [End-to-end tests](./how-to-launch-e2e.md) + +We apply the [test +pyramid](https://en.wikipedia.org/wiki/Test_automation#Testing_at_different_levels) +strategy, which means we want the majority of tests at the bottom of the +pyramid. Those should be fast and deterministic and support REPL-Driven +development (RDD). Slightly above them, we have component tests, then +integration/contract tests and finally end-to-end tests. The closer to the top +of the pyramid, the more valuable a test can be, but also more difficult to +pinpoint why it failed and harder to make it dependable. + +*Note*: there are literally dozens of [types of +tests](https://en.wikipedia.org/wiki/Software_testing), each with its strengths +and weaknesses. + +We tend not to stub or mock implementations in our tests, which means our tests +are [sociable](https://martinfowler.com/bliki/UnitTest.html). + +## What to test? + +The UI is driven by global & local state changes caused by events. Global state +is managed by re-frame and local state by Reagent atoms or React hooks. Except +for component and end-to-end tests, we test only non-UI code in `status-mobile`. +Given that the UI is greatly derived from global state, by guaranteeing the +state is correct we can prevent bugs and, more importantly, reduce the [cost of +change](https://www.pmi.org/disciplined-agile/agile/costofchange). + +We strive to minimize the amount of _business logic_ in views (UI code). We +achieve this by moving capabilities to status-go and also by adhering to +re-frame's architecture. + +Whenever appropriate (see section `When to test?`), we _may_ test: + +- Re-frame events. +- Re-frame subscriptions. +- Utility functions. +- User journeys through integration/contract tests. + +Interestingly, we don't test re-frame _effects_ in isolation. + +### What are status-mobile integration and contract tests? + +The mobile _integration tests_ can be used to "simulate" user interactions and +make actual calls to status-go via the RPC layer and actually receive signals. +We can also use these tests to verify the app-db and multiple subscriptions are +correct. We use the word _simulate_ because there is no UI. Basically, any flow +that can be driven by re-frame events is possible to automatically test. There +is no way to change or inspect local state managed by React. + +A _contract test_ has the same capabilities as an integration test, but we want +to draw the line that they should focus more on a particupar RPC endpoint or +signal, and not on a user journey (e.g. create a wallet account). In the future, +we may consider running them automatically in status-go. + +**Note:** integration tests and contract tests are currently overlapping in +their responsibilities and still require a clearer distinction. + +## When to test? + +(Automated) tests basically exist to support rapid software changes, but not +every piece of code should be tested. The following are general recommendations, +not rules. + +- What would be the consequences to the user of a bug in the implementation you + are working on? +- Can a QA exercise all the branches in the code you changed? Not surprisingly, + usually QAs can't test many code paths (it may be nearly impossible), and + because PRs are not often tested by reviewers, many PRs can get into `develop` + without the necessary quality assurance. +- How costly was it for you to verify a function/event/etc was correct? Now + consider that this cost will be dispersed to every developer who needs to + change the implementation if there are no tests. +- Check the number of conditionals, and if they nest as well. Every conditional + may require two different assertions, and the number of assertions can grow + exponentially. +- How complicated are the arguments to the function? If they contain nested maps + or data that went through a few transformations, it may be tricky to decipher + what they are, unless you are familiar with the code. A test would be able to + capture the data, however complex they are. + +### When to unit-test subscriptions? + +Only test [layer-3 +subscriptions](https://day8.github.io/re-frame/subscriptions/#the-four-layers), +i.e. don't bother testing extractor subscriptions (check the related +[guideline](https://github.com/status-im/status-mobile/blob/7774c4eac16fdee950a17bf5d07630c45a980f41/doc/new-guidelines.md#subscription-tests)). +Some layer-3 subscriptions can still be straightforward and may not be worth +testing. + +- Check the number of _inputs_ to the sub (from the graph). The higher this + number, the greater the chance the subscription can break if any of the + input's implementation changes. + +**Note**: if a tested subscription changes inadvertently, even if its own tests +still pass, other subscriptions that depend on it and have tests may still fail. +This is why we don't directly test the subscription handler, but instead, use +the macro `test-helpers.unit/deftest-sub`. + +### When to unit-test events? + +A good hint is to ask if you and other CCs need to rely on re-frisk, UI, REPL, +or FlowStorm to understand the event. If the answer is yes or probably, then a +test would be prudent. + +- Many events only receive arguments and pass them along without much or any + transformation to an RPC call. These are straightforward and usually don't + need tests ([example](https://github.com/status-im/status-mobile/blob/7774c4eac16fdee950a17bf5d07630c45a980f41/src/status_im/contexts/contact/blocking/events.cljs#L79-L85)). +- Overall, every event basically returns two effects at most, `:fx` and/or + `:db`. Usually, the complicated part lies in the computation to return the new + app-db. If the event doesn't perform transformations in the app-db or just + does a trivial `assoc`, for example, it may not be worth testing. + +For reference, the re-frame author particularly [suggests testing events and +subscriptions](https://github.com/day8/re-frame/blob/09e2d7132c479aa43f2a64164e54e42bf8511902/docs/Testing.md#what-to-test). + +### When to unit-test utility functions? + +Most utility functions in `status-mobile` are pure and can be readily and +cheaply tested. + +- If the utility is used in an event/subscription and if the event/subscription + has tests, you may prefer to test the event/subscription and not the utility, + or the other way around sometimes. +- If the utility is tricky to verify, such as functions manipulating time, write + tests ([example](https://github.com/status-im/status-mobile/blob/7774c4eac16fdee950a17bf5d07630c45a980f41/src/utils/datetime.cljs#L1)). +- Utilities can be particularly hard to verify by QAs because they can be lower + level and require very particular inputs. In such cases, consider writing + tests. + +### When to write integration/contract tests? + +- You want to make real calls to status-go because you think the unit tests are + not enough (test pyramid strategy). +- You constantly need to retest the same things on the UI, sometimes over + multiple screens. +- The flow is too important to rely only on manual QA, which can't always be + done due to resource limits, so an integration/contract test fills this gap. +- You want to rely less on end-to-end tests, which can be more unreliable and + slower to change. +- You want automatic verifications for some area of the mobile app whenever + status-go is upgraded. + +**Note**: the feedback cycle to write integration tests is longer than unit +tests because they are slower and harder to debug. Using the REPL with them is +difficult due to their stateful nature. + +### When to test Quo components? + +This is covered in [quo/README.md#component-tests](https://github.com/status-im/status-mobile/blob/7774c4eac16fdee950a17bf5d07630c45a980f41/src/quo/README.md#component-tests). diff --git a/doc/troubleshooting.md b/doc/troubleshooting.md new file mode 100644 index 00000000000..9e203f396a4 --- /dev/null +++ b/doc/troubleshooting.md @@ -0,0 +1,146 @@ +## `yarn add` is not working + +While running any yarn add command like `yarn add react-native-share@7.0.2`, it is showing error + +``` +error status-mobile/node_modules/better-sqlite3: Command failed. +``` + +### Cause + +Local `node` version can be different from the one needed by Status project. + +### Solution + +Before running `yarn add`, nix shell should be started: + +``` +make shell +``` + + +## [DEPRECATED] Undefined is not an object evaluating `register_handler_fx` + +### Deprecation note + +This type of error should not occur anymore now that we require the namespace in the `fx.cljs` file. + +It can however happen with other macros requiring a cljs namespace. + +The general fix for that type of issue is to have two files for the namespace where your macros are defined, let's say for `my-project.my-macro` namespace you would have: +- my_macro.cljs in which you need `(:require-macros my-project.my-macro)` and `(:require my-project.the-namespace-used-in-the-macro)` +- my_macro.clj in which you define the macro + +That way you don't need to use any magical call like `find-ns` or inline `require` with some kind of call only once switch (which was the root cause of another bug in ``defstyle`` macro because the compilation phase at which the evaluation of the switch is done was not properly considered). + +You also want to make sure users are using the macro by using a aliased namespace defined in require statement rather than require-macro and refer to the macro directly. Otherwise it won't require the cljs file and the require statement of the namespace in the macroexpension might not be there. + +### Stacktrace + +``` +13:25:22, Requiring: hi-base32 +13:25:23, Possible Unhandled Promise Rejection (id: 0): +TypeError: undefined is not an object (evaluating 'status_im.utils.handlers.register_handler_fx') +eval code +eval@[native code] +asyncImportScripts$@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:200728:21 +tryCatch@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:26567:23 +invoke@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:26742:32 +tryCatch@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:26567:23 +invoke@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:26643:30 +http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:26653:21 +tryCallOne@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3725:16 +http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3826:27 +_callTimer@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:28405:17 +_callImmediatesPass@http://localhost:8081/index.bundle?pla<…> +``` + +### Cause + +- stacktrace mentions `register_handler_fx`, +- common cause is when requires have been cleaned up and a require of `status-im.utils.handlers` namespace was removed because it looked like it was unused but was actually used through a fx/defn macro + +### Solution + +go through known faulty commit looking for deleted requires + + +## Git "unable to access" errors during `yarn install` + +### Description +Developer updates `package.json` file with a new dependency using a GitHub URL. So it looks like this: +``` + "react-native-status-keycard": "git+https://github.com/status-im/react-native-status-keycard.git#feature/exportKeyWithPath", +``` +Afterwards, when running e.g. `make run-metro`, they might see the following confusing error: +``` +# macOS +fatal: unable to access 'https://github.com/siphiuel/react-native-status-keycard.git/': SSL certificate problem: unable to get local issuer certificate +# Linux +fatal: unable to access 'https://github.com/status-im/react-native-status-keycard.git/': Could not resolve host: github.com +info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. +``` + +### Cause +`yarn.lock` is not updated to be in sync with `package.json`. + +### Solution +Update yarn.lock file. In order to do this, perform the following steps on a clean `status-mobile` repo: +``` +cd status-mobile +make shell +yarn install +``` +and don't forget to commit updated `yarn.lock` together with `package.json`. + + +## adb server/client version mismatch errors + +### Description +Running some adb commands, e.g. `adb devices` or `make android-ports` (in turn invokes `adb reverse`/`adb forward` commands) may display the following message: +``` +adb server version (40) doesn't match this client (41); killing... +``` +or the reverse +``` +adb server version (41) doesn't match this client (40); killing... +``` + +This might cause all kinds of difficult-to-debug errors, e.g.: + - not being able to find the device through `adb devices` + - `make run-android` throwing `com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: device 'device-id' not found.` + - `make run-android` throwing `- Error: Command failed: ./gradlew app:installDebug -PreactNativeDevServerPort=8081 Unable to install /status-mobile/android/app/build/outputs/apk/debug/app-debug.apk com.android.ddmlib.InstallException: EOF` + - dropped CLJS repl connections (that have been enabled previously with the help of `make android-ports`) + +### Cause +System's local adb and Nix's adb differ. As adb include of server/client processes, this can cause subtle version errors that cause adb to kill mismatching server processes. + +### Solution +Always use respective `make` commands, e.g. `make android-ports`, `make android-devices`, etc. + +Alternatively, run adb commands only from `make shell TARGET=android` shell. Don't forget the `TARGET=android` env var setting - otherwise `adb` will still be selected from the system's default location. You can double-check this by running `which adb`. + +# Hot Reloading Crashes +## APP Crashes on reloading changes + +### Cause +Status-mobile uses `shadow-cljs` for hot reloading changes and uses its own [reloader](https://github.com/status-im/status-mobile/blob/develop/src/status_im/reloader.cljs) for updating them in the running app. If react-native's fast refresh is also enabled then it creates conflicts and crashes the app. + +### Solution +Open react native's [In-App Developer Menu](https://reactnative.dev/docs/debugging#accessing-the-in-app-developer-menu) and press "Disable Fast Refresh" or "Disable Hot Reloading" + +# App crashing after running `make run-ios` + +### Cause +It's possible that installing XCode from a `.xip` file might cause XCode to act funny +Since it's not installed from the App Store, Or you might be missing a Rosetta installation. + +[Reference/Similar issue](https://github.com/expo/expo-cli/issues/3197) + +You might see something like this after running `make run-ios`: +``` +Underlying error (domain=FBSOpenApplicationServiceErrorDomain, code=1): +``` +### Solution +Run `softwareupdate --install-rosetta --agree-to-license` +to accept XCode's license and install Rosetta. diff --git a/doc/ui-guidelines.md b/doc/ui-guidelines.md new file mode 100644 index 00000000000..cd659c6a061 --- /dev/null +++ b/doc/ui-guidelines.md @@ -0,0 +1,254 @@ +# UI components coding guidelines + +> [!IMPORTANT] +> React apps are made out of components. A component is a piece of the UI (user interface) that has its own logic and appearance. A component can be as small as a button, or as large as an entire screen. +> React components are JavaScript functions that return markup +> This document will provide best practices on how to write efficient React components in ClojureScript + + +At the time of creating the Status app, the Reagent library was a solid choice. Back then, hooks didn't exist, and there weren't any libraries providing effective global state management. After Reagent's emergence, another library called Re-frame built upon Reagent. Together, they offered powerful tools for developing React applications with ClojureScript. However, as React evolved, significant changes occurred. Class components, utilized in Reagent, became deprecated. Instead, functional components and hooks emerged for state management. In Status 2.0, we began incorporating more functional components and hooks, resulting in a blend of both approaches. To simplify matters and reduce confusion, we opted to transition to functional components and hooks for local state management. + +BEFORE: +```clojure +(defn- view-internal + [_ _] + (let [pressed? (reagent/atom false)] + (fn + [{:keys [theme on-press on-long-press icon]}] + [rn/pressable + {:style (style/main @pressed? theme) + :on-press on-press + :on-press-in #(reset! pressed? true) + :on-press-out #(reset! pressed? nil) + :on-long-press on-long-press} + [quo.icons/icon icon]]))) + +(def view (theme/with-theme view-internal)) +``` + +NOW: +```clojure +(defn view + [{:keys [on-press on-long-press icon]}] + (let [[pressed? set-pressed] (rn/use-state false) + theme (context/use-theme) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed nil))] + [rn/pressable + {:style (style/main pressed? theme) + :on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out + :on-long-press on-long-press} + [quo.icons/icon icon]])) +``` + + +- We no longer need to create an anonymous function for rendering. This removes unnecessary confusion and the need for specific knowledge on how it works and why it was needed. +- `rn/use-state` is used instead of `reagent/atom` +- State values no longer need to be dereferenced; they are accessible as regular symbols. This eliminates a common bug where the "@" symbol was inadvertently omitted. +- `theme/with-theme` wrapper is not needed anymore, `(theme/use-theme)` hook can be used directly in the components +- `:f>` not needed anymore, all components are functional by default +- `rn/use-callback` hook should be used for anon callback functions + +> [!IMPORTANT] +> DO NOT USE anon functions directly in the props + +BAD +```clojure +(defn view + [] + (let [[pressed? set-pressed] (rn/use-state false)] + [rn/pressable + {:style (style/main pressed?) + :on-press-in #(set-pressed true) + :on-press-out #(set-pressed nil)}])) +``` + +GOOD: +```clojure +(defn view + [] + (let [[pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed nil))] + [rn/pressable + {:style (style/main pressed?) + :on-press-in on-press-in + :on-press-out on-press-out}])) +``` + +## Global state and subscriptions + +For global state management, we utilize Re-frame subscriptions. They can be likened to React state. To obtain the state, `(rf/sub [])` is employed, and to modify it, `(rf/dispatch [])` is utilized. However, they update components in a similar manner to React states. + +```clojure +(defn view + [{:keys [selected-tab]}] + (let [collectible-list (rf/sub [:wallet/all-collectibles]) + on-collectible-press (rn/use-callback + (fn [{:keys [id]}] + (rf/dispatch [:wallet/get-collectible-details id])))] + [rn/view {:style style/container} + (case selected-tab + :assets [assets/view] + :collectibles [collectibles/view {:collectibles collectible-list + :on-collectible-press on-collectible-press}]) + [activity/view]])) +``` + +## Regular atoms + +In certain instances, components utilized regular atoms; however, they should now be used with `rn/use-ref-atom` + +BEFORE: +```clojure +(defn view + [] + (let [focused? (atom false)] + (fn [] + (let [on-clear #(reset! status (if @focused? :active :default)) + on-focus #(reset! focused? true) + on-blur #(reset! focused? false)])))) +``` + +NOW: +```clojure +(defn view + [] + (let [focused? (rn/use-ref-atom false) + on-clear (rn/use-callback #(set-status (if @focused? :active :default))) + on-focus (rn/use-callback #(reset! focused? true)) + on-blur (rn/use-callback #(reset! focused? false))])) +``` + +## Effects + +LIFECYCLE: + +```clojure +(defn view + [{:keys []}] + (let [opacity (reanimated/use-shared-value 0)] + (rn/use-mount #(reanimated/animate opacity 1)) + [rn/view + {:style (style/opacity opacity)}])) +``` + +```clojure +(defn view + [{:keys []}] + (let [] + (rn/use-unmount #(rn/dispatch [:unmounted])) + [rn/view])) +``` + +> [!IMPORTANT] +> Effects should NOT be utilized as a response to state changes for modifying logic. If you're unsure how to achieve this without using effects, please consult the team in the general chat. There may be instances where using effects is appropriate, so we can explore a solution together and enhance our guidelines. + +BAD: +```clojure +(defn f-zoom-button + [{:keys [selected? current-zoom]}] + (let [size (reanimated/use-shared-value (if selected? 37 25))] + (rn/use-effect #(reanimated/animate size (if selected? 37 25)) [current-zoom]) + [rn/touchable-opacity + {:style (style/zoom-button-container size)}])) +``` + +BAD: + +```clojure +(defn view + [collectible-list (rf/sub [:wallet/all-collectibles])] + (let [] + (rn/use-effect #(rn/dispatch [:all-collectibles-changed]) [collectible-list]) + [rn/view])) +``` + +Instead `:all-collectibles-changed` should be used in the handler which changes `collectible-list` state + + + +## Performance tips + +To begin with, we need to understand that there are two distinct stages for a component: creation and update. React creates a render tree, a UI tree, composed of the rendered components. + +![react_tree.png](react_tree.png) + +### Component creation + +For component creation, the most critical factor is the number of elements involved, so we should strive to minimize them. For instance, it's advisable to avoid using unnecessary wrappers or containers. + +BAD: + +```clojure +(defn view + [] + (let [] + [rn/view {:style {:padding-top 20}} + [quo/button]])) +``` + +GOOD: +```clojure +(defn view + [] + (let [] + [quo/button {:container-style {:padding-top 20}}])) +``` + +### Component updates + +For component updates, it's crucial to recognize that React will invoke the function where state is utilized. Therefore, if you utilize state in the root component, React will execute the root function and re-render the entire root component along with all its children (unless optimizations like memoization are implemented). + +BAD: + +```clojure +(defn component + [{:keys [label]}] + (let [] + [rn/text label])) + +(defn component2 + [{:keys [label2]}] + (let [] + [rn/text label2])) + +(defn screen + [] + (let [screen-params (rf/sub [:screen-params])] + [component screen-params] + [component1] + [component2 screen-params] + [component3] + [rn/view {:padding-top 20} + [quo/button]])) +``` + +Here, we have lost control over the `screen-params` map. It can contain any data, and if any field within this map changes, the entire screen function will be executed, resulting in the re-rendering of both `component` and `component2`. + +GOOD: +```clojure +(defn component + [] + (let [label (rf/sub [:screen-params-label])] + [rn/text label])) + +(defn component2 + [] + (let [label2 (rf/sub [:screen-params-label2])] + [rn/text label2])) + +(defn screen + [] + (let [] + [component] + [component1] + [component2] + [component3] + [rn/view {:padding-top 20} + [quo/button]])) +``` + +So, now the screen component function will never be invoked, and `component` and `component2` will be re-rendered only when `label` or `label2` have changed. diff --git a/doc/use-status-backend-server.md b/doc/use-status-backend-server.md new file mode 100644 index 00000000000..db297aa9a3c --- /dev/null +++ b/doc/use-status-backend-server.md @@ -0,0 +1,61 @@ +## What is Status Backend Server +Status Backend Server is a http server that runs status-go, it exposed endpoints come from [mobile/status.go](https://github.com/status-im/status-go/blob/master/mobile/status.go) so that you can call them through http requests. It brings the following benefits: +- When verifying changes made in status-go, you only need to run these commands once (unless modifying Kotlin/Objective-C code): + - `make clean && make run-clojure` + - `make run-android` or `make run-ios` + This avoids repeatedly rebuilding and saves development time. +- Debug status-go while running status mobile app + +## Solution to use Status Backend Server for mobile development +`StatusBackendClient` is the entry point to use Status Backend Server. We need always to call `status-im.setup.status-backend-client/init` whether `STATUS_BACKEND_SERVER_ENABLED` is `1` or not. If it's not enabled, the invocation to functions in `native-module.core` will be delegated to built-in status-go library, otherwise it will be delegated to status-go running in status-backend server. Currently, all functions has usages in `native-module.core` should be supported delegated to. +NOTE: not all the native functions used `StatusBackendClient`, only the corresponding functions in `native-module.core` has usages should be supported delegated to ATM. + +related [PR](https://github.com/status-im/status-mobile/pull/21550) + +## Usage +### Add environment variables to your local machine: +```shell +# enable using status backend server or not, otherwise it will use built-in status-go library +export STATUS_BACKEND_SERVER_ENABLED=1 + +#The host should contain an IP address and a port separated by a colon. +#The port comes from your running status backend server. +#If you run it by PORT=60000 make run-status-backend , then host will likely be 127.0.0.1:60000 +export STATUS_BACKEND_SERVER_HOST="127.0.0.1:60000" + +export STATUS_BACKEND_SERVER_ROOT_DATA_DIR="/path/to/your/root/data/dir" +``` +You need to change `STATUS_BACKEND_SERVER_ROOT_DATA_DIR` to your preferred directory and ensure it exists, it should be in absolute path. +All the db files and log files(api.log/geth.log) and keystore files etc will be stored in this directory. + +### Start the status backend server: +```shell +PORT=60000 make run-status-backend +``` +MAKE SURE the status-backend is checked out to a revision that's at least compatible with the revision in status-mobile/status-go-version.json before starting the server. + +For the Android simulator, you need to reverse the port: +```shell +adb reverse tcp:60000 tcp:60000 +``` +However, there is restriction when use adb reverse, we use random port for media server. So I'd suggest use "10.0.2.2:60000" as `STATUS_BACKEND_SERVER_HOST`. + +### Debug status-go using IDEA +Assume you've already set up the development environment for status-go, open status-go project use IDEA, run `make generate` with terminal in the status-go project root directory to ensure all the generated files are up to date, then open `cmd/status-backend/main.go` file, navigate to function `main()`, click the green play button on the left of `main()`, choose `Modify Run Configuration`, in `Program arguments` section, add `--address=localhost:60000 --media-https=false`, then click `OK`, finally click the green play button on the left of `main()` again and choose `Debug ...`. +Basically, you don't have to run `make generate` again and again, just run it once at the beginning. So you can re-run and debug it faster! + +## Known Android simulator issues +- Issue#1: Android simulator may not display images due to TLS certificate validation issues with the image server + - solution: use http instead of https for media server with set env: `export STATUS_BACKEND_SERVER_MEDIA_SERVER_ENABLE_TLS=0`, you also need to set env variable `STATUS_BACKEND_SERVER_IMAGE_SERVER_URI_PREFIX` to "http://10.0.2.2:" so that `image_server.cljs` can work, and to make `/accountInitials` work, you need to copy `Inter-Medium.ttf` to your host machine from the android simulator, let's say you store it in `/Users/mac/Downloads/Inter-Medium.ttf`, then you need to update `get-font-file-ready` manually in `image_server.cljs` to return the correct path so that status backend server can access it. +- Issue#2: exportUnencryptedDatabaseV2/import-multiaccount does not work for android, probably cause of tech debt, I found it during creating the draft PR. +- Issue#3: unable to invoke `multiaccounts_storeIdentityImage` to change avatar image. + - The reason is that we path the absolute path of the image to the backend server, but the image file is stored in the android simulator. the backend server cannot access it as it runs in the host machine. + +If you're using ios simulator, you can skip above issues! + +## Details for issue#1 if you're interested +- we use `react-native-fast-image` which use okhttpclient behind +- we were using custom cert for https +- we fetch the custom cert through endpoint `ImageServerTLSCert` +- we fetched it through built-in status-go before, now we need to fetch it through status backend server +- we expect `OkHttpClientProvider.setOkHttpClientFactory(StatusOkHttpClientFactory())` to be invoked early(will trigger fetching wrong custom cert via built-in status-go since StatusBackendClient is not initialised yet!) in `MainApplication.kt` before executing clojure code, otherwise we will get black screen after `make run-android` . After deep research, I found there's no way to update the cert okhttpclient used to the correct one return from status backend server diff --git a/docker-build/Dockerfile b/docker-build/Dockerfile deleted file mode 100644 index 01a320a0eec..00000000000 --- a/docker-build/Dockerfile +++ /dev/null @@ -1,103 +0,0 @@ -FROM openjdk:8-jdk - -ENV DEBIAN_FRONTEND noninteractive -ENV ANDROID_HOME /opt/android-sdk-linux -ENV ANDROID_SDK_HOME /opt/android-sdk-linux -ENV PATH ${PATH}:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools:${PATH}:${ANDROID_HOME}/tools -ENV ANDROID_NDK /opt/android-ndk-linux -ENV ANDROID_NDK_HOME /opt/android-ndk-linux - -WORKDIR /opt - -RUN mkdir -p /opt/android-sdk-linux && mkdir -p ~/.android && touch ~/.android/repositories.cfg - -# Install dependencies -RUN apt-get update && \ - apt-get install -yq supervisor curl unzip wget sudo maven libc6 libstdc++6 zlib1g libncurses5 make file --no-install-recommends && \ - apt-get clean - -RUN cd /opt/android-sdk-linux && \ - wget -q --output-document=sdk-tools.zip https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip && \ - unzip sdk-tools.zip && \ - rm -f sdk-tools.zip && \ - sdkmanager "cmake;3.6.4111459" - -RUN cd /opt/android-sdk-linux && \ - wget -q --output-document=sdk-platform-tools.zip 'https://dl.google.com/android/repository/platform-tools-latest-linux.zip' && \ - unzip sdk-platform-tools.zip && \ - rm -f sdk-platform-tools.zip - -RUN wget -q --output-document=android-ndk.zip http://dl.google.com/android/repository/android-ndk-r10e-linux-x86_64.zip && \ - unzip android-ndk.zip && \ - rm -f android-ndk.zip && \ - mv android-ndk-r10e android-ndk-linux - -RUN yes | sdkmanager --licenses - -# install Node JS (https://github.com/nodejs/docker-node/blob/90d5e3df903b830d039d3fe8f30e3a62395db37e/7.5/Dockerfile) - -RUN groupadd --gid 1000 node \ - && useradd --uid 1000 --gid node --shell /bin/bash --create-home node - -ENV NPM_CONFIG_LOGLEVEL info -# that's the only version that doesn't hang on installing react-native-http inside docker -ENV NODE_VERSION 8.9.1 -ENV NODE_ENV development - -RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ - && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ - && rm "node-v$NODE_VERSION-linux-x64.tar.xz" \ - && ln -s /usr/local/bin/node /usr/local/bin/nodejs - -# END install Node JS - -# Support Gradle -ENV TERM dumb - -# Install Lein -# Check out latest releases from https://github.com/technomancy/leiningen/releases -ENV LEIN_VERSION=2.8.1 -ENV LEIN_INSTALL=/usr/local/bin/ - -RUN mkdir -p $LEIN_INSTALL \ - && wget -q https://raw.githubusercontent.com/technomancy/leiningen/$LEIN_VERSION/bin/lein-pkg \ - && mv lein-pkg $LEIN_INSTALL/lein \ - && chmod 0755 $LEIN_INSTALL/lein \ - && wget -q https://github.com/technomancy/leiningen/releases/download/$LEIN_VERSION/leiningen-$LEIN_VERSION-standalone.zip \ - && mkdir -p /usr/share/java \ - && mv leiningen-$LEIN_VERSION-standalone.zip /usr/share/java/leiningen-$LEIN_VERSION-standalone.jar - -# Install React-Native -RUN npm install -g react-native-cli - -# Add files needed for installing dependencies -# Use directory /build for that purpose -WORKDIR /build - -ADD .env.jenkins .env - -ADD ./project.clj ./ - -ADD ./package.json package-lock.json ./ - -RUN npm install - -ADD . ./ - -RUN make setup -RUN make prepare - -COPY supervisord.conf /etc/supervisord.conf - -RUN keytool -genkey -v -keystore status-im.keystore -storepass password -alias status -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=mqttserver.ibm.com, OU=ID, O=IBM, L=Hursley, S=Hants, C=GB" -keypass password -RUN mkdir -p /root/.gradle -RUN mv status-im.keystore /root/.gradle/ - - -RUN echo "sdk.dir=/opt/android-sdk-linux" >> android/local.properties -RUN echo "ndk.dir=/opt/android-ndk-linux" >> android/local.properties - -RUN cd ./android && ./gradlew react-native-android:installArchives -RUN cd ./android && ./gradlew assembleRelease - -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/docker-build/README.md b/docker-build/README.md deleted file mode 100644 index 3715b2261dc..00000000000 --- a/docker-build/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Build - -``` -docker-compose -f docker-build/docker-compose.yml build -``` - -This will install all the required sdks, depending on the connection will take some time. - -# Run - -``` -docker-compose -f docker-build/docker-compose.yml up -``` - -This will start `adbd` and `fighwheel` listening on port `4567`. - -You need to connect your device and accept the key. - -After the figwheel prompt you can install the application running: - -``` -docker-compose -f docker-build/docker-compose.yml exec adbd make run-android -``` - -# Notes - -## Adbd key - -The adbd key is generated every time you start/stop the container, so you will have to accept it if that happens. - -## Security - -This will give the docker image access to your USB devices and run in priviliged mode, so run at your own risk. - -## Multiple adbd - -You will need to make sure no `adbd` is running on your local machine, as that would interfer with the one running in docker. diff --git a/docker-build/docker-compose.yml b/docker-build/docker-compose.yml deleted file mode 100644 index 2a930caa475..00000000000 --- a/docker-build/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3' -services: - adbd: - privileged: true - build: - context: $PWD - dockerfile: docker-build/Dockerfile - ports: - - 4567:4567 - volumes: - - /dev/bus/usb:/dev/bus/usb - - ../src:/build/src - - ../test:/build/test - - ../scripts:/build/scripts - - ../android/app/src/:/build/android/app/src - diff --git a/env/dev/env/android/main.cljs b/env/dev/env/android/main.cljs deleted file mode 100644 index bb19159cf71..00000000000 --- a/env/dev/env/android/main.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns ^:figwheel-no-load env.android.main - (:require [reagent.core :as r] - [status-im.android.core :as core] - [figwheel.client :as figwheel] - [re-frisk-remote.core :as rr] - [env.config :as conf] - [env.utils] - [status-im.utils.handlers :as utils.handlers])) - -(enable-console-print!) - -(assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?") -(assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?") - -(def cnt (r/atom 0)) -(defn reloader [] @cnt [core/app-root]) - -;; Do not delete, root-el is used by the figwheel-bridge.js -(def root-el (r/as-element [reloader])) - -(figwheel/start {:websocket-url (:android conf/figwheel-urls) - :heads-up-display false - :jsload-callback #(swap! cnt inc)}) - -(utils.handlers/add-pre-event-callback rr/pre-event-callback) - -(rr/enable-re-frisk-remote! {:host (env.utils/re-frisk-url (:android conf/figwheel-urls)) - :on-init core/init}) diff --git a/env/dev/env/config.cljs b/env/dev/env/config.cljs deleted file mode 100644 index b67b984552b..00000000000 --- a/env/dev/env/config.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns env.config) - - -(def figwheel-urls {:android "ws://192.168.10.203:3449/figwheel-ws", - :ios "ws://localhost:3449/figwheel-ws", - :desktop "ws://localhost:3449/figwheel-ws"}) diff --git a/env/dev/env/desktop/main.cljs b/env/dev/env/desktop/main.cljs deleted file mode 100644 index 72c57914465..00000000000 --- a/env/dev/env/desktop/main.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns ^:figwheel-no-load env.desktop.main - (:require [reagent.core :as r] - [re-frisk-remote.core :as rr] - [status-im.desktop.core :as core] - [status-im.utils.handlers :as utils.handlers] - [figwheel.client :as figwheel] - [env.config :as conf] - [env.utils])) - -(enable-console-print!) - -(assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?") -(assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?") - -(def cnt (r/atom 0)) -(defn reloader [] @cnt [core/app-root]) - -;; Do not delete, root-el is used by the figwheel-bridge.js -(def root-el (r/as-element [reloader])) - -(figwheel/start {:websocket-url (:desktop conf/figwheel-urls) - :heads-up-display false - :jsload-callback #(swap! cnt inc)}) - -(utils.handlers/add-pre-event-callback rr/pre-event-callback) - -(rr/enable-re-frisk-remote! {:host (env.utils/re-frisk-url (:desktop conf/figwheel-urls)) - :on-init core/init}) diff --git a/env/dev/env/ios/main.cljs b/env/dev/env/ios/main.cljs deleted file mode 100644 index 887650871ff..00000000000 --- a/env/dev/env/ios/main.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns ^:figwheel-no-load env.ios.main - (:require [reagent.core :as r] - [re-frisk-remote.core :as rr] - [status-im.ios.core :as core] - [status-im.utils.handlers :as utils.handlers] - [figwheel.client :as figwheel] - [env.config :as conf] - [env.utils])) - -(enable-console-print!) - -(assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?") -(assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?") - -(def cnt (r/atom 0)) -(defn reloader [] @cnt [core/app-root]) - -;; Do not delete, root-el is used by the figwheel-bridge.js -(def root-el (r/as-element [reloader])) - -(figwheel/start {:websocket-url (:ios conf/figwheel-urls) - :heads-up-display false - :jsload-callback #(swap! cnt inc)}) - -(utils.handlers/add-pre-event-callback rr/pre-event-callback) - -(rr/enable-re-frisk-remote! {:host (env.utils/re-frisk-url (:ios conf/figwheel-urls)) - :on-init core/init}) diff --git a/env/dev/env/utils.cljs b/env/dev/env/utils.cljs deleted file mode 100644 index 1b957b2db84..00000000000 --- a/env/dev/env/utils.cljs +++ /dev/null @@ -1,21 +0,0 @@ -(ns env.utils - (:require [clojure.string :as string])) - -(defn get-host - "Expects the input url to be in the form protocol://host:port - Returns host or an empty string upon failure" - [url] - (-> - url - (string/split #"/") - (get 2 "") - (string/split #":") - (get 0 ""))) - -(defn re-frisk-url - "Expects the input url to be in the form ws://host:port/figwheel-ws" - [url] - (let [host (get-host url)] - (if (string/blank? host) - (throw (js/Error. "Failed to parse figwheel url. re-frisk url cannot be blank")) - (str host ":" 4567)))) diff --git a/env/dev/figwheel.clj b/env/dev/figwheel.clj deleted file mode 100644 index ec285848d44..00000000000 --- a/env/dev/figwheel.clj +++ /dev/null @@ -1,31 +0,0 @@ -(ns figwheel - (:require [clojure.string :as s])) - -(defn system-options [builds-to-start] - {:nrepl-port 7888 - :builds [{:id :desktop - :source-paths ["react-native/src/cljsjs" "react-native/src/desktop" "src" "env/dev"] - :compiler {:output-to "target/desktop/app.js" - :main "env.desktop.main" - :output-dir "target/desktop" - :npm-deps false - :optimizations :none} - :figwheel true} - {:id :ios - :source-paths ["react-native/src/cljsjs" "react-native/src/mobile" "src" "env/dev"] - :compiler {:output-to "target/ios/app.js" - :main "env.ios.main" - :output-dir "target/ios" - :npm-deps false - :optimizations :none} - :figwheel true} - {:id :android - :source-paths ["react-native/src/cljsjs" "react-native/src/mobile" "src" "env/dev"] - :compiler {:output-to "target/android/app.js" - :main "env.android.main" - :output-dir "target/android" - :npm-deps false - :optimizations :none} - :warning-handlers '[status-im.utils.build/warning-handler] - :figwheel true}] - :builds-to-start builds-to-start}) diff --git a/env/dev/figwheel_api.clj b/env/dev/figwheel_api.clj deleted file mode 100644 index 5045ed7959f..00000000000 --- a/env/dev/figwheel_api.clj +++ /dev/null @@ -1,36 +0,0 @@ -(ns figwheel-api - (:use [figwheel-sidecar.repl-api :as ra]) - (:require [hawk.core :as hawk] - [re-frisk-sidecar.core :as rfs] - [status-im.utils.core :as utils] - [figwheel :as config])) - -(defn start-figwheel - "Start figwheel for one or more builds" - [build-ids] - (ra/start-figwheel! (config/system-options build-ids))) - -(defn stop-figwheel - "Stops figwheel" - [] - (ra/stop-figwheel!)) - -(defn start - ([] - (start (if *command-line-args* - (map keyword *command-line-args*) - [:android]))) - ([build-ids] - (hawk/watch! [{:paths ["resources"] - :handler (fn [ctx e] - (let [path "src/status_im/utils/js_resources.cljs" - js-resourced (slurp path)] - (spit path (str js-resourced " ;;")) - (spit path js-resourced)) - ctx)}]) - (start-figwheel build-ids) - (rfs/-main))) - -(def stop ra/stop-figwheel!) - -(def start-cljs-repl ra/cljs-repl) diff --git a/env/dev/run.clj b/env/dev/run.clj deleted file mode 100644 index e1b46f903da..00000000000 --- a/env/dev/run.clj +++ /dev/null @@ -1,5 +0,0 @@ -(ns ^:figwheel-no-load run - (:use [figwheel-api])) - -(start) -(start-cljs-repl) diff --git a/env/prod/env/android/main.cljs b/env/prod/env/android/main.cljs deleted file mode 100644 index b1e717e0781..00000000000 --- a/env/prod/env/android/main.cljs +++ /dev/null @@ -1,6 +0,0 @@ - (ns env.android.main - (:require [status-im.android.core :as core])) - - (core/init) - - diff --git a/env/prod/env/desktop/main.cljs b/env/prod/env/desktop/main.cljs deleted file mode 100644 index e23508305bb..00000000000 --- a/env/prod/env/desktop/main.cljs +++ /dev/null @@ -1,4 +0,0 @@ - (ns env.desktop.main - (:require [status-im.desktop.core :as core])) - - (core/init) \ No newline at end of file diff --git a/env/prod/env/ios/main.cljs b/env/prod/env/ios/main.cljs deleted file mode 100644 index 67664f36de6..00000000000 --- a/env/prod/env/ios/main.cljs +++ /dev/null @@ -1,6 +0,0 @@ - (ns env.ios.main - (:require [status-im.ios.core :as core])) - - (core/init) - - diff --git a/fastlane/.env b/fastlane/.env new file mode 100644 index 00000000000..055dffbdba2 --- /dev/null +++ b/fastlane/.env @@ -0,0 +1 @@ +RCT_NO_LAUNCH_PACKAGER=true diff --git a/fastlane/Appfile b/fastlane/Appfile index 4341a2d9dd0..f01e56d20d5 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,13 +1,19 @@ # iOS -app_identifier("im.status.ethereum") # The bundle identifier of your app -apple_id(ENV["APPLE_ID"]) # Your Apple email address -itc_team_id("118430139") # iTunes Connect Team ID +app_identifier('im.status.ethereum') # The bundle identifier of your app +apple_id(ENV['FASTLANE_APPLE_ID']) # Your Apple email address +itc_team_id('118430139') # iTunes Connect Team ID + +for_platform :ios do + for_lane :pr do + app_identifier('im.status.ethereum.pr') + end +end # Android -# json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one -package_name("im.status.ethereum") # e.g. com.krausefx.app +# json_key_file('') # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +package_name('im.status.ethereum') # e.g. com.krausefx.app # For more information about the Appfile, see: # https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 40fe85d425b..a9b039fd824 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,50 +1,322 @@ # This file contains the fastlane.tools configuration + # You can find the documentation at https://docs.fastlane.tools # # For a list of all available actions, check out # # https://docs.fastlane.tools/actions # - -# Uncomment the line if you want fastlane to automatically update itself +# Fastlane is updated quite frequently with security patches # update_fastlane +# There are a few env variables defined in the .env file in +# this directory (fastlane/.env) + +def curl_upload(url, file, auth, conn_timeout = 5, timeout = 60, retries = 3) + rval = sh( + 'curl', + '--silent', + '--user', auth, + '--write-out', "\nHTTP_CODE:%{http_code}", + '--request', 'POST', + '--header', 'Content-Type: application/octet-stream', + # we retry few times if upload doesn't succeed in sensible time + '--retry-connrefused', # consider ECONNREFUSED as error too retry + '--data-binary', "@../#{file}", # `fastlane` is the cwd so we go one folder up + '--connect-timeout', conn_timeout.to_s, # max time in sec. for establishing connection + '--max-time', timeout.to_s, # max time in sec. for whole transfer to take + '--retry', retries.to_s, # number of retries to attempt + '--retry-max-time', timeout.to_s, # same as --max-time but for retries + '--retry-delay', '0', # an exponential backoff algorithm in sec. + url + ) + # We're not using --fail because it suppresses server response + raise "Error:\n#{rval}" unless rval.include? 'HTTP_CODE:200' + + rval +end + +def retry_curl_upload(url, file, auth, conn_timeout = 5, timeout = 60, retries = 3) + # since curl doesn't retry on connection and operation timeouts we roll our own + (1..retries).each do |try| + begin + return curl_upload(url, file, auth, conn_timeout, timeout, retries) + rescue StandardError => e + if try == retries + UI.error "Error:\n#{e}" + raise + end + UI.important "Warning: Retrying cURL upload! (attempt #{try}/#{retries})" + end + end +end + +# Creates and unlocks a keychain into which Fastlane match imports signing keys and certs. +class Keychain + attr_accessor :name, :pass + + def initialize(name) + # Local devs will not have KEYCHAIN_PASSWORD set, and will be prompted for password. + return "login.keychain-db" unless ENV['KEYCHAIN_PASSWORD'] + # We user the same keychain every time because we need to set a default. + @name = "#{name}.keychain-db" + @pass = ENV['KEYCHAIN_PASSWORD'] + Fastlane::Actions::CreateKeychainAction.run( + name: @name, + password: @pass, + unlock: true, + # Fastlane can't find the signing cert without setting a default. + default_keychain: true, + # Deleting the keychain would cause race condition for parallel jobs. + require_create: false, + # Lock it up after 25 minutes just in case we don't delete it. + lock_when_sleeps: true, + lock_after_timeout: true, + timeout: 1500 + ) + end +end + +# App Store Connect API is an official public API used to manage Apps. +# This includes metadata, pricing and availability, provisioning, and more. +# It provides a JSON API and auth using API Keys to generate a JSON Web Token (JWT). +def asc_api_key() + app_store_connect_api_key( + key_id: ENV['FASTLANE_ASC_API_KEY_ID'], + issuer_id: ENV['FASTLANE_ASC_API_ISSUER_ID'], + key_filepath: ENV['FASTLANE_ASC_API_KEY_FILE_PATH'], + duration: 1200, # seconds, session length + in_house: false, + ) +end + +# builds an ios app with ad-hoc configuration and put it +# to "status-ios" output folder +# `readonly`: +# if true - only fetch existing certificates and profiles, don't upgrade from AppStoreConnect +# if false - read list of devices from AppStoreConnect, and upgrade the provisioning profiles from it +# `pr_build`: +# if true - uses StatusImPR scheme and postfixed app id with `.pr` to build an app, which can be used in parallel with release +# if false - uses StatusIm scheme to build the release app + +def build_ios_adhoc(readonly: false, pr_build: false) + # PR builds should appear as a separate App on iOS + scheme = pr_build ? 'StatusImPR' : 'StatusIm' + app_id = pr_build ? 'im.status.ethereum.pr' : 'im.status.ethereum' + + kc = Keychain.new('fastlane') + + match( + type: 'adhoc', + readonly: readonly, + api_key: asc_api_key(), + app_identifier: app_id, + force_for_new_devices: true, + keychain_name: kc.name, + keychain_password: kc.pass + ) + + build_ios_app( + scheme: scheme, + workspace: 'ios/StatusIm.xcworkspace', + configuration: 'Release', + clean: true, + export_method: 'ad-hoc', + output_name: 'StatusIm', + output_directory: 'status-ios', + buildlog_path: 'logs', + export_options: { + signingStyle: 'manual', + provisioningProfiles: { + "im.status.ethereum": "match AdHoc im.status.ethereum", + "im.status.ethereum.pr": "match AdHoc im.status.ethereum.pr" + } + } + ) +end + +# builds an ios app with e2e configuration and put it +# to "status-ios" output folder +def build_ios_e2e + # determine a simulator SDK installed + showsdks_output = sh('xcodebuild', '-showsdks') + simulator_sdk = showsdks_output.scan(/iphonesimulator\d\d?\.\d\d?/).first + + kc = Keychain.new('fastlane') + + match( + type: 'adhoc', + readonly: true, + api_key: asc_api_key(), + force_for_new_devices: true, + keychain_name: kc.name, + keychain_password: kc.pass + ) + + build_ios_app( + # Creating a build for the iOS Simulator + # 1. https://medium.com/rocket-fuel/fastlane-to-the-simulator-87549b2601b9 + sdk: simulator_sdk, + destination: 'generic/platform=iOS Simulator', + # 2. fixing compilations issues as stated in https://stackoverflow.com/a/20505258 + # it looks like i386 isn't supported by React Native + xcargs: 'ARCHS="x86_64" ONLY_ACTIVE_ARCH=NO', + # 3. directory where to up StatusIm.app + derived_data_path: 'status-ios', + output_name: 'StatusIm.app', + buildlog_path: 'logs', + # ------------------------------------- + # Normal stuff + scheme: 'StatusIm', + workspace: 'ios/StatusIm.xcworkspace', + configuration: 'Release', + # Simulator apps can't be archived... + skip_archive: true, + # ...and we don't need an .ipa file for them, because we use .app directly + skip_package_ipa: true + ) + + zip( + path: 'status-ios/Build/Products/Release-iphonesimulator/StatusIm.app', + output_path: 'status-ios/StatusIm.app.zip', + verbose: false + ) +end platform :ios do - desc "Deploy a new internal build to iTunes Connect" + desc '`fastlane ios adhoc` - ad-hoc lane for iOS.' + desc 'This lane is used for PRs, Releases, etc.' + desc 'It creates an .ipa that can be used by a list of devices, registeded in the App Store Connect.' + desc 'This .ipa is ready to be distibuted through diawi.com' + lane :adhoc do + build_ios_adhoc(readonly: true) + end + + desc '`fastlane ios e2e` - e2e lane for iOS.' + desc 'This lane is used for SauceLabs end-to-end testing.' + desc 'It creates an .app that can be used inside of a iPhone simulator.' + lane :e2e do + build_ios_e2e + end + + desc '`fastlane ios pr` - makes a new pr build' + desc 'This lane builds a new adhoc build and leaves an .ipa that is ad-hoc signed (can be uploaded to diawi)' + lane :pr do + build_ios_adhoc(pr_build: true) + end + + desc '`fastlane ios nightly` - makes a new nightly' + desc 'This lane builds a new nightly and leaves an .ipa that is ad-hoc signed (can be uploaded to diawi)' lane :nightly do - upload_to_testflight( - ipa: "status_appstore/StatusIm.ipa" - ) - slack(message: "New nightly build uploaded to iTunes Connect") + build_ios_adhoc() end + + desc '`fastlane ios release` builds a release & uploads it to TestFlight' lane :release do + kc = Keychain.new('fastlane') + + # recreate keychain + sh("security delete-keychain #{kc.name}", error_callback: ->(result) { }) + sh("security create-keychain -p #{kc.pass} #{kc.name}") + sh("security set-keychain-settings -t 3600 -l #{kc.name}") + sh("security unlock-keychain -p #{kc.pass} #{kc.name}") + sh("security list-keychains -d user -s login.keychain #{kc.name}") + + match( + type: 'appstore', + readonly: true, + api_key: asc_api_key(), + app_identifier: 'im.status.ethereum', + keychain_name: kc.name, + keychain_password: kc.pass + ) + + # Fix certificate access permissions + sh("security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k #{kc.pass} #{kc.name}") + + # Debug + sh("security find-identity -v -p codesigning #{kc.name}") + + build_ios_app( + scheme: 'StatusIm', + workspace: 'ios/StatusIm.xcworkspace', + configuration: 'Release', + clean: true, + export_method: 'app-store', + output_directory: 'status-ios', + buildlog_path: 'logs', + include_symbols: false, + export_options: { + "combileBitcode": true, + "uploadBitcode": false, + "ITSAppUsesNonExemptEncryption": false + } + ) + upload_to_testflight( - ipa: "status_appstore/StatusIm.ipa" + ipa: 'status-ios/StatusIm.ipa', + skip_waiting_for_build_processing: true ) - slack(message: "New release build uploaded to iTunes Connect") end - desc "Remove inactive TestFlight users" - lane :clean do - clean_testflight_testers + + desc '`fastlane ios saucelabs` - upload .app to sauce labs' + desc 'also notifies in a GitHub comments' + desc 'expects to have an .apk prepared: `result/app.apk`' + desc 'expects to have a saucelabs access key as SAUCE_ACCESS_KEY env variable' + desc 'expects to have a saucelabs username token as SAUCE_USERNAME env variable' + desc 'expects to have a saucelabs destination name as SAUCE_LABS_NAME env variable' + desc "will fails if file isn't there" + lane :saucelabs do + upload_to_saucelabs( + 'status-ios/StatusIm.app.zip' + ) end -end + desc 'This fastlane step cleans up XCode DerivedData folder' + lane :cleanup do + clear_derived_data + end +end platform :android do - desc "Deploy a new internal build to Google Play" + # Optional env variables + APK_PATHS = ENV["APK_PATHS"]&.split(";") or ["result/app.apk"] + + desc 'Deploy a new internal build to Google Play' + desc 'expects GOOGLE_PLAY_JSON_KEY environment variable' lane :nightly do upload_to_play_store( - track: "internal", - apk: "android/app/build/outputs/apk/release/app-release.apk" + track: 'internal', + apk_paths: APK_PATHS, + json_key_data: ENV['GOOGLE_PLAY_JSON_KEY'] ) - slack(message: "New nightly build uploaded to Google Play") end + + desc 'Deploy a new alpha (public) build to Google Play' + desc 'expects GOOGLE_PLAY_JSON_KEY environment variable' lane :release do upload_to_play_store( - track: "alpha", - apk: "android/app/build/outputs/apk/release/app-release.apk" + track: 'alpha', + apk_paths: APK_PATHS, + json_key_data: ENV['GOOGLE_PLAY_JSON_KEY'] + ) + end + + desc 'Upload metadata to Google Play.' + desc 'Metadata is always updated when builds are uploaded,' + desc 'but this action can update metadata without uploading a build.' + desc 'expects GOOGLE_PLAY_JSON_KEY environment variable' + lane :upload_metadata do + upload_to_play_store( + skip_upload_apk: true, + skip_upload_changelogs: true, + json_key_data: ENV['GOOGLE_PLAY_JSON_KEY'], + # These don't matter much as we're not uploading any new builds + # and indeed, we're skipping changelogs. This is just so that + # the library can find what it thinks it needs and continue with + # the work we actually want it to do. + track: 'production', + version_code: '2020042307' ) - slack(message: "New release build uploaded to Google Play") end end diff --git a/fastlane/Gemfile b/fastlane/Gemfile new file mode 100644 index 00000000000..a0be0a1ee5f --- /dev/null +++ b/fastlane/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' +gem 'fastlane', '>= 2.131.0' diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock new file mode 100644 index 00000000000..4e4e1e8e3f3 --- /dev/null +++ b/fastlane/Gemfile.lock @@ -0,0 +1,228 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.6) + rexml + activesupport (7.0.8) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.872.0) + aws-sdk-core (3.190.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.75.0) + aws-sdk-core (~> 3, >= 3.188.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.141.0) + aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.2.2) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20231109) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.108.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.7) + fastlane (2.217.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.29.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.3.1) + google-cloud-storage (1.45.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.29.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.7.1) + jwt (2.7.1) + mini_magick (4.12.0) + mini_mime (1.1.5) + minitest (5.20.0) + multi_json (1.15.0) + multipart-post (2.3.0) + nanaimo (0.3.0) + naturally (2.2.1) + optparse (0.1.1) + os (1.1.4) + plist (3.7.0) + public_suffix (5.0.4) + rake (13.1.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.18.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (2.5.0) + webrick (1.8.1) + word_wrap (1.0.0) + xcodeproj (1.23.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-22 + arm64-darwin-23 + x86_64-darwin-22 + +DEPENDENCIES + activesupport (>= 6.1.7.3, < 7.1.0) + fastlane (>= 2.131.0) + +BUNDLED WITH + 2.4.13 diff --git a/fastlane/Matchfile b/fastlane/Matchfile new file mode 100644 index 00000000000..0d17f94ad31 --- /dev/null +++ b/fastlane/Matchfile @@ -0,0 +1,9 @@ +git_url('git@github.com:status-im/ios-certificates.git') + +type('development') # The default type, can be: appstore, adhoc, enterprise or development + +app_identifier(['im.status.ethereum', 'im.status.ethereum.pr']) +username(ENV['FASTLANE_APPLE_ID']) # Your Apple Developer Portal username + +# For all available options run `fastlane match --help` +# Remove the # in the beginning of the line to enable the other options diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile deleted file mode 100644 index 0ee9978e51a..00000000000 --- a/fastlane/Pluginfile +++ /dev/null @@ -1,5 +0,0 @@ -# Autogenerated by fastlane -# -# Ensure this file is checked in to source control! - -gem 'fastlane-plugin-clean_testflight_testers' diff --git a/fastlane/default.nix b/fastlane/default.nix new file mode 100644 index 00000000000..745831b388a --- /dev/null +++ b/fastlane/default.nix @@ -0,0 +1,40 @@ +{ stdenv, lib, bundlerEnv, ruby, makeWrapper }: + +let + fastlane = stdenv.mkDerivation rec { + name = "${pname}-${version}"; + pname = "fastlane"; + version = (import ./gemset.nix).fastlane.version; + + nativeBuildInputs = [ makeWrapper ]; + + env = bundlerEnv { + name = "${name}-gems"; + inherit pname ruby; + gemdir = ./.; + }; + + phases = [ "installPhase" ]; + + installPhase = '' + mkdir -p $out/bin + makeWrapper ${env}/bin/fastlane $out/bin/fastlane \ + --set FASTLANE_SKIP_UPDATE_CHECK 1 + ''; + + shellHook = '' + [ -z "$STATUS_MOBILE_HOME" ] && echo "STATUS_MOBILE_HOME is empty!" && exit 1 + ''; + + meta = with lib; { + description = "A tool to automate building and releasing iOS and Android apps"; + longDescription = "fastlane is a tool for iOS and Android developers to automate tedious tasks like generating screenshots, dealing with provisioning profiles, and releasing your application."; + homepage = https://github.com/fastlane/fastlane; + license = licenses.mit; + maintainers = with maintainers; [ + peterromfeldhk + ]; + }; + }; + +in fastlane diff --git a/fastlane/gemset.nix b/fastlane/gemset.nix new file mode 100644 index 00000000000..4f96bbf178c --- /dev/null +++ b/fastlane/gemset.nix @@ -0,0 +1,946 @@ +{ + activesupport = { + dependencies = ["concurrent-ruby" "i18n" "minitest" "tzinfo"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "188kbwkn1lbhz40ala8ykp20jzqphgc68g3d8flin8cqa2xid0s5"; + type = "gem"; + }; + version = "7.0.8"; + }; + addressable = { + dependencies = ["public_suffix"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0irbdwkkjwzajq1ip6ba46q49sxnrl2cw7ddkdhsfhb6aprnm3vr"; + type = "gem"; + }; + version = "2.8.6"; + }; + artifactory = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0wify8rhjwr5bw5y6ary61vba290vk766cxw9a9mg05yswmaisls"; + type = "gem"; + }; + version = "3.0.15"; + }; + atomos = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "17vq6sjyswr5jfzwdccw748kgph6bdw30bakwnn6p8sl4hpv4hvx"; + type = "gem"; + }; + version = "0.1.3"; + }; + aws-eventstream = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0gvdg4yx4p9av2glmp7vsxhs0n8fj1ga9kq2xdb8f95j7b04qhzi"; + type = "gem"; + }; + version = "1.3.0"; + }; + aws-partitions = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1ddbcz8p3abbw8d8pn796z1ry1mbapl6ayhh37ziwal6bd047kvm"; + type = "gem"; + }; + version = "1.872.0"; + }; + aws-sdk-core = { + dependencies = ["aws-eventstream" "aws-partitions" "aws-sigv4" "jmespath"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1ansagfl5irx1y6b9xf4xpi9j6q6k5pbd2aw80hn0p4m3ycafamh"; + type = "gem"; + }; + version = "3.190.1"; + }; + aws-sdk-kms = { + dependencies = ["aws-sdk-core" "aws-sigv4"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1qzxqfgrhnl5rdc39a1gl2pgrdxgnsj12zycpxnsx8lg6arfmnr1"; + type = "gem"; + }; + version = "1.75.0"; + }; + aws-sdk-s3 = { + dependencies = ["aws-sdk-core" "aws-sdk-kms" "aws-sigv4"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0bnhpmi0iiaj88rqc5lhhnp2gyrk4fs8xz51lj36wwzng94qinya"; + type = "gem"; + }; + version = "1.141.0"; + }; + aws-sigv4 = { + dependencies = ["aws-eventstream"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1g3w27wzjy4si6kp49w10as6ml6g6zl3xrfqs5ikpfciidv9kpc4"; + type = "gem"; + }; + version = "1.8.0"; + }; + babosa = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "16dwqn33kmxkqkv51cwiikdkbrdjfsymlnc0rgbjwilmym8a9phq"; + type = "gem"; + }; + version = "1.0.4"; + }; + CFPropertyList = { + dependencies = ["rexml"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1a36zn77yyibqsfpka0i8vgf3yv98ic2b9wwlbc29566y8wpa2bq"; + type = "gem"; + }; + version = "3.0.6"; + }; + claide = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0bpqhc0kqjp1bh9b7ffc395l9gfls0337rrhmab4v46ykl45qg3d"; + type = "gem"; + }; + version = "1.1.0"; + }; + colored = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0b0x5jmsyi0z69bm6sij1k89z7h0laag3cb4mdn7zkl9qmxb90lx"; + type = "gem"; + }; + version = "1.2"; + }; + colored2 = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0jlbqa9q4mvrm73aw9mxh23ygzbjiqwisl32d8szfb5fxvbjng5i"; + type = "gem"; + }; + version = "3.1.2"; + }; + commander = { + dependencies = ["highline"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1n8k547hqq9hvbyqbx2qi08g0bky20bbjca1df8cqq5frhzxq7bx"; + type = "gem"; + }; + version = "4.6.0"; + }; + concurrent-ruby = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0krcwb6mn0iklajwngwsg850nk8k9b35dhmc2qkbdqvmifdi2y9q"; + type = "gem"; + }; + version = "1.2.2"; + }; + declarative = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1yczgnqrbls7shrg63y88g7wand2yp9h6sf56c9bdcksn5nds8c0"; + type = "gem"; + }; + version = "0.0.20"; + }; + digest-crc = { + dependencies = ["rake"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "09114ndpnnyamc2q07bmpzw7kp3rbbfv7plmxcbzzi9d6prmd92w"; + type = "gem"; + }; + version = "0.6.5"; + }; + domain_name = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1gpciaifmxql8h01ci12qq08dnqrdlzkkz6fmia9v9yc3r9a29si"; + type = "gem"; + }; + version = "0.6.20231109"; + }; + dotenv = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1n0pi8x8ql5h1mijvm8lgn6bhq4xjb5a500p5r1krq4s6j9lg565"; + type = "gem"; + }; + version = "2.8.1"; + }; + emoji_regex = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0jsnrkfy345v66jlm2xrz8znivfnamg3mfzkddn414bndf2vxn7c"; + type = "gem"; + }; + version = "3.2.3"; + }; + excon = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1q9yz7x36mpzpz04bg6hdyrzzk4bri9jg3h2ygw26bf6v3axq53q"; + type = "gem"; + }; + version = "0.108.0"; + }; + faraday = { + dependencies = ["faraday-em_http" "faraday-em_synchrony" "faraday-excon" "faraday-httpclient" "faraday-multipart" "faraday-net_http" "faraday-net_http_persistent" "faraday-patron" "faraday-rack" "faraday-retry" "ruby2_keywords"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1c760q0ks4vj4wmaa7nh1dgvgqiwaw0mjr7v8cymy7i3ffgjxx90"; + type = "gem"; + }; + version = "1.10.3"; + }; + faraday-cookie_jar = { + dependencies = ["faraday" "http-cookie"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "00hligx26w9wdnpgsrf0qdnqld4rdccy8ym6027h5m735mpvxjzk"; + type = "gem"; + }; + version = "0.0.7"; + }; + faraday-em_http = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "12cnqpbak4vhikrh2cdn94assh3yxza8rq2p9w2j34bqg5q4qgbs"; + type = "gem"; + }; + version = "1.0.0"; + }; + faraday-em_synchrony = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1vgrbhkp83sngv6k4mii9f2s9v5lmp693hylfxp2ssfc60fas3a6"; + type = "gem"; + }; + version = "1.0.0"; + }; + faraday-excon = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0h09wkb0k0bhm6dqsd47ac601qiaah8qdzjh8gvxfd376x1chmdh"; + type = "gem"; + }; + version = "1.1.0"; + }; + faraday-httpclient = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0fyk0jd3ks7fdn8nv3spnwjpzx2lmxmg2gh4inz3by1zjzqg33sc"; + type = "gem"; + }; + version = "1.0.1"; + }; + faraday-multipart = { + dependencies = ["multipart-post"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "09871c4hd7s5ws1wl4gs7js1k2wlby6v947m2bbzg43pnld044lh"; + type = "gem"; + }; + version = "1.0.4"; + }; + faraday-net_http = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1fi8sda5hc54v1w3mqfl5yz09nhx35kglyx72w7b8xxvdr0cwi9j"; + type = "gem"; + }; + version = "1.0.1"; + }; + faraday-net_http_persistent = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0dc36ih95qw3rlccffcb0vgxjhmipsvxhn6cw71l7ffs0f7vq30b"; + type = "gem"; + }; + version = "1.2.0"; + }; + faraday-patron = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "19wgsgfq0xkski1g7m96snv39la3zxz6x7nbdgiwhg5v82rxfb6w"; + type = "gem"; + }; + version = "1.0.0"; + }; + faraday-rack = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1h184g4vqql5jv9s9im6igy00jp6mrah2h14py6mpf9bkabfqq7g"; + type = "gem"; + }; + version = "1.0.0"; + }; + faraday-retry = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "153i967yrwnswqgvnnajgwp981k9p50ys1h80yz3q94rygs59ldd"; + type = "gem"; + }; + version = "1.0.3"; + }; + faraday_middleware = { + dependencies = ["faraday"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1bw8mfh4yin2xk7138rg3fhb2p5g2dlmdma88k82psah9mbmvlfy"; + type = "gem"; + }; + version = "1.2.0"; + }; + fastimage = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1pd7pamzhdz2w0fbcvsfn2nyslznvphnwj16zw35g2b28zd2xyzx"; + type = "gem"; + }; + version = "2.2.7"; + }; + fastlane = { + dependencies = ["CFPropertyList" "addressable" "artifactory" "aws-sdk-s3" "babosa" "colored" "commander" "dotenv" "emoji_regex" "excon" "faraday" "faraday-cookie_jar" "faraday_middleware" "fastimage" "gh_inspector" "google-apis-androidpublisher_v3" "google-apis-playcustomapp_v1" "google-cloud-storage" "highline" "http-cookie" "json" "jwt" "mini_magick" "multipart-post" "naturally" "optparse" "plist" "rubyzip" "security" "simctl" "terminal-notifier" "terminal-table" "tty-screen" "tty-spinner" "word_wrap" "xcodeproj" "xcpretty" "xcpretty-travis-formatter"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0p2b92csayzlj60v15fz3mq4h23kvvr7swhaavi6fpaqzf70iafi"; + type = "gem"; + }; + version = "2.217.0"; + }; + gh_inspector = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0f8r9byajj3bi2c7c5sqrc7m0zrv3nblfcd4782lw5l73cbsgk04"; + type = "gem"; + }; + version = "1.1.3"; + }; + google-apis-androidpublisher_v3 = { + dependencies = ["google-apis-core"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "046j100lrh5dhb8p3gr38fyqrw8vcif97pqb55ysipy874lafw49"; + type = "gem"; + }; + version = "0.54.0"; + }; + google-apis-core = { + dependencies = ["addressable" "googleauth" "httpclient" "mini_mime" "representable" "retriable" "rexml" "webrick"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1cly6ycryjhk15d60v3nqvhqpjk9f0nznnslbdnin90f5r54sbpd"; + type = "gem"; + }; + version = "0.11.2"; + }; + google-apis-iamcredentials_v1 = { + dependencies = ["google-apis-core"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0ysil0bkh755kmf9xvw5szhk1yyh3gqzwfsrbwsrl77gsv7jarcs"; + type = "gem"; + }; + version = "0.17.0"; + }; + google-apis-playcustomapp_v1 = { + dependencies = ["google-apis-core"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1mlgwiid5lgg41y7qk8ca9lzhwx5njs25hz5fbf1mdal0kwm37lm"; + type = "gem"; + }; + version = "0.13.0"; + }; + google-apis-storage_v1 = { + dependencies = ["google-apis-core"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1k432qgxf41c4m6d68rascm0gyj18r7ypmrnyzmxh7k7nh543awx"; + type = "gem"; + }; + version = "0.29.0"; + }; + google-cloud-core = { + dependencies = ["google-cloud-env" "google-cloud-errors"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "00wfvdvdv9m4l5ydn6xp65n68mgmpqr3n09hzvxs7gp8fly3j17v"; + type = "gem"; + }; + version = "1.6.1"; + }; + google-cloud-env = { + dependencies = ["faraday"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "056r1p8vhjswnx2cy3mzhwc5gj03whmpz8m4p2ph37gag5bpnxmf"; + type = "gem"; + }; + version = "2.1.0"; + }; + google-cloud-errors = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0flpj7v196c3xsqx4yjb7rjcj8p0by4rhj6qf5zanw4p1i41ssf0"; + type = "gem"; + }; + version = "1.3.1"; + }; + google-cloud-storage = { + dependencies = ["addressable" "digest-crc" "google-apis-iamcredentials_v1" "google-apis-storage_v1" "google-cloud-core" "googleauth" "mini_mime"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0033bi8qwm0ksxsv5zhz4nzwsiaapq3xk79z8f8rx3v09vdap07j"; + type = "gem"; + }; + version = "1.45.0"; + }; + googleauth = { + dependencies = ["faraday" "google-cloud-env" "jwt" "multi_json" "os" "signet"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0spv89di017rg25psiv4w3pn6f67y2w6vv8w910i83b5yii84rl1"; + type = "gem"; + }; + version = "1.9.1"; + }; + highline = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0yclf57n2j3cw8144ania99h1zinf8q3f5zrhqa754j6gl95rp9d"; + type = "gem"; + }; + version = "2.0.3"; + }; + http-cookie = { + dependencies = ["domain_name"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "13rilvlv8kwbzqfb644qp6hrbsj82cbqmnzcvqip1p6vqx36sxbk"; + type = "gem"; + }; + version = "1.0.5"; + }; + httpclient = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "19mxmvghp7ki3klsxwrlwr431li7hm1lczhhj8z4qihl2acy8l99"; + type = "gem"; + }; + version = "2.8.3"; + }; + i18n = { + dependencies = ["concurrent-ruby"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0qaamqsh5f3szhcakkak8ikxlzxqnv49n2p7504hcz2l0f4nj0wx"; + type = "gem"; + }; + version = "1.14.1"; + }; + jmespath = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1cdw9vw2qly7q7r41s7phnac264rbsdqgj4l0h4nqgbjb157g393"; + type = "gem"; + }; + version = "1.6.2"; + }; + json = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0r9jmjhg2ly3l736flk7r2al47b5c8cayh0gqkq0yhjqzc9a6zhq"; + type = "gem"; + }; + version = "2.7.1"; + }; + jwt = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "16z11alz13vfc4zs5l3fk6n51n2jw9lskvc4h4prnww0y797qd87"; + type = "gem"; + }; + version = "2.7.1"; + }; + mini_magick = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0slh78f9z6n0l1i2km7m48yz7l4fjrk88sj1f4mh1wb39sl2yc37"; + type = "gem"; + }; + version = "4.12.0"; + }; + mini_mime = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1vycif7pjzkr29mfk4dlqv3disc5dn0va04lkwajlpr1wkibg0c6"; + type = "gem"; + }; + version = "1.1.5"; + }; + minitest = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0bkmfi9mb49m0fkdhl2g38i3xxa02d411gg0m8x0gvbwfmmg5ym3"; + type = "gem"; + }; + version = "5.20.0"; + }; + multi_json = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0pb1g1y3dsiahavspyzkdy39j4q377009f6ix0bh1ag4nqw43l0z"; + type = "gem"; + }; + version = "1.15.0"; + }; + multipart-post = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0lgyysrpl50wgcb9ahg29i4p01z0irb3p9lirygma0kkfr5dgk9x"; + type = "gem"; + }; + version = "2.3.0"; + }; + nanaimo = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0xi36h3f7nm8bc2k0b6svpda1lyank2gf872lxjbhw3h95hdrbma"; + type = "gem"; + }; + version = "0.3.0"; + }; + naturally = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "04x1nkx6gkqzlc4phdvq05v3vjds6mgqhjqzqpcs6vdh5xyqrf59"; + type = "gem"; + }; + version = "2.2.1"; + }; + optparse = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0j9l5a1zszvrlggp1ldx82i4kkqx34g4g3amwp488s499w5l1cvj"; + type = "gem"; + }; + version = "0.1.1"; + }; + os = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0gwd20smyhxbm687vdikfh1gpi96h8qb1x28s2pdcysf6dm6v0ap"; + type = "gem"; + }; + version = "1.1.4"; + }; + plist = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0wzhnbzraz60paxhm48c50fp9xi7cqka4gfhxmiq43mhgh5ajg3h"; + type = "gem"; + }; + version = "3.7.0"; + }; + public_suffix = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1bni4qjrsh2q49pnmmd6if4iv3ak36bd2cckrs6npl111n769k9m"; + type = "gem"; + }; + version = "5.0.4"; + }; + rake = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1ilr853hawi09626axx0mps4rkkmxcs54mapz9jnqvpnlwd3wsmy"; + type = "gem"; + }; + version = "13.1.0"; + }; + representable = { + dependencies = ["declarative" "trailblazer-option" "uber"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1kms3r6w6pnryysnaqqa9fsn0v73zx1ilds9d1c565n3xdzbyafc"; + type = "gem"; + }; + version = "3.2.0"; + }; + retriable = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1q48hqws2dy1vws9schc0kmina40gy7sn5qsndpsfqdslh65snha"; + type = "gem"; + }; + version = "3.1.2"; + }; + rexml = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "05i8518ay14kjbma550mv0jm8a6di8yp5phzrd8rj44z9qnrlrp0"; + type = "gem"; + }; + version = "3.2.6"; + }; + rouge = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0sfikq1q8xyqqx690iiz7ybhzx87am4w50w8f2nq36l3asw4x89d"; + type = "gem"; + }; + version = "2.0.7"; + }; + ruby2_keywords = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1vz322p8n39hz3b4a9gkmz9y7a5jaz41zrm2ywf31dvkqm03glgz"; + type = "gem"; + }; + version = "0.0.5"; + }; + rubyzip = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0grps9197qyxakbpw02pda59v45lfgbgiyw48i0mq9f2bn9y6mrz"; + type = "gem"; + }; + version = "2.3.2"; + }; + security = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1ryjxs0j66wrbky2c08yf0mllwalvpg12rpxzbdx2rdhj3cbrlxa"; + type = "gem"; + }; + version = "0.1.3"; + }; + signet = { + dependencies = ["addressable" "faraday" "jwt" "multi_json"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0fzakk5y7zzii76zlkynpp1c764mzkkfg4mpj18f5pf2xp1aikb6"; + type = "gem"; + }; + version = "0.18.0"; + }; + simctl = { + dependencies = ["CFPropertyList" "naturally"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0sr3z4kmp6ym7synicyilj9vic7i9nxgaszqx6n1xn1ss7s7g45r"; + type = "gem"; + }; + version = "1.6.10"; + }; + terminal-notifier = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1slc0y8pjpw30hy21v8ypafi8r7z9jlj4bjbgz03b65b28i2n3bs"; + type = "gem"; + }; + version = "2.0.0"; + }; + terminal-table = { + dependencies = ["unicode-display_width"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "14dfmfjppmng5hwj7c5ka6qdapawm3h6k9lhn8zj001ybypvclgr"; + type = "gem"; + }; + version = "3.0.2"; + }; + trailblazer-option = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "18s48fndi2kfvrfzmq6rxvjfwad347548yby0341ixz1lhpg3r10"; + type = "gem"; + }; + version = "0.1.2"; + }; + tty-cursor = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0j5zw041jgkmn605ya1zc151bxgxl6v192v2i26qhxx7ws2l2lvr"; + type = "gem"; + }; + version = "0.7.1"; + }; + tty-screen = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0l4vh6g333jxm9lakilkva2gn17j6gb052626r1pdbmy2lhnb460"; + type = "gem"; + }; + version = "0.8.2"; + }; + tty-spinner = { + dependencies = ["tty-cursor"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0hh5awmijnzw9flmh5ak610x1d00xiqagxa5mbr63ysggc26y0qf"; + type = "gem"; + }; + version = "0.9.3"; + }; + tzinfo = { + dependencies = ["concurrent-ruby"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "16w2g84dzaf3z13gxyzlzbf748kylk5bdgg3n1ipvkvvqy685bwd"; + type = "gem"; + }; + version = "2.0.6"; + }; + uber = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1p1mm7mngg40x05z52md3mbamkng0zpajbzqjjwmsyw0zw3v9vjv"; + type = "gem"; + }; + version = "0.1.0"; + }; + unicode-display_width = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1d0azx233nags5jx3fqyr23qa2rhgzbhv8pxp46dgbg1mpf82xky"; + type = "gem"; + }; + version = "2.5.0"; + }; + webrick = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "13qm7s0gr2pmfcl7dxrmq38asaza4w0i2n9my4yzs499j731wh8r"; + type = "gem"; + }; + version = "1.8.1"; + }; + word_wrap = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1iyc5bc7dbgsd8j3yk1i99ral39f23l6wapi0083fbl19hid8mpm"; + type = "gem"; + }; + version = "1.0.0"; + }; + xcodeproj = { + dependencies = ["CFPropertyList" "atomos" "claide" "colored2" "nanaimo" "rexml"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "176ndahc5fssyx04q176vy6wngs1av4vrsdrkdpjij700hqll8hn"; + type = "gem"; + }; + version = "1.23.0"; + }; + xcpretty = { + dependencies = ["rouge"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1xq47q2h5llj7b54rws4796904vnnjz7qqnacdv7wlp3gdbwrivm"; + type = "gem"; + }; + version = "0.3.0"; + }; + xcpretty-travis-formatter = { + dependencies = ["xcpretty"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "14rg4f70klrs910n7rsgfa4dn8s2qyny55194ax2qyyb2wpk7k5a"; + type = "gem"; + }; + version = "1.0.1"; + }; +} diff --git a/fastlane/metadata/android/ar-SA/full_description.txt b/fastlane/metadata/android/ar-SA/full_description.txt new file mode 100644 index 00000000000..71c07d40163 --- /dev/null +++ b/fastlane/metadata/android/ar-SA/full_description.txt @@ -0,0 +1,21 @@ +يجمع Status بين برنامج مراسلة مجهول الهوية يركز على الخصوصية ومحفظة العملات المشفرة الآمنة في أداة اتصال قوية واحدة. يمكنك الدردشة مع الأصدقاء والمجتمعات المتنامية. وشراء وتخزين وتبادل الأصول الرقمية. + +Status هو نظام تشغيل Ethereum الخاص بك. + +محفظة Ethereum آمنة +تتيح لك محفظة العملات المشفرة Status إرسال وتخزين وتبادل أصول Ethereum بأمان مثل ETH وSNT والعملات المستقرة مثل DAI، بالإضافة إلى المقتنيات. تحكم بثقة في عملاتك المشفرة والأصول الرقمية من خلال تطبيق محفظة Ethereum متعددة السلاسل، والذي يدعم الشبكة الرئيسية و Base Arbitrum وOptimism. تدعم محفظة blockchain Status حاليًا أصول ETH وERC-20 وERC-721 وERC-1155 فقط؛ ولا تدعم Bitcoin. + +PRIVATE MESSENGER +أرسل محادثات فردية وجماعية خاصة دون أن يتجسس أحد على اتصالاتك. Status هو تطبيق مراسلة يلغي عمليات نقل الرسائل المركزية لمزيد من الخصوصية والمراسلة الآمنة. يتم تشفير جميع الرسائل باستخدام تشفير من البداية إلى النهاية. بالإضافة إلى ذلك، لا تكشف أي رسالة عن هوية المؤلف أو المستلم المقصود، لذلك لا أحد، ولا حتى Status، يعرف من يتحدث إلى من أو ما قيل. + +اربح مع DEFI + +استخدم عملتك المشفرة مع أحدث تطبيقات التمويل اللامركزية والبورصات اللامركزية (DEX) مثل Maker وAave وUniswap وSynthetix وPoolTogether وZerion وKyber والمزيد. + +تواصل مع مجتمعك + +استكشف مجتمعاتك وأصدقائك المفضلين وتواصل معهم ودردش معهم. سواء كانت مجموعة صغيرة من الأصدقاء أو مجموعة فنانين أو تجار عملات مشفرة أو منظمة كبيرة قادمة - أرسل رسائل نصية وتواصل مع مجتمعات Status. + +إنشاء حساب خاص + +ابق خاصًا من خلال إنشاء حساب مجهول الهوية. عند إنشاء حسابك المجاني، لن تضطر أبدًا إلى إدخال رقم هاتف أو عنوان بريد إلكتروني أو حساب مصرفي. يتم إنشاء مفاتيح المحفظة الخاصة بك محليًا وتخزينها بشكل آمن لضمان حصولك أنت فقط على حق الوصول إلى أموالك ومعاملاتك المالية. diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar-SA/short_description.txt new file mode 100644 index 00000000000..282c5de3145 --- /dev/null +++ b/fastlane/metadata/android/ar-SA/short_description.txt @@ -0,0 +1 @@ +ربط Private Finance وWeb3 diff --git a/fastlane/metadata/android/ar-SA/title.txt b/fastlane/metadata/android/ar-SA/title.txt new file mode 100644 index 00000000000..27f0018a91d --- /dev/null +++ b/fastlane/metadata/android/ar-SA/title.txt @@ -0,0 +1 @@ +Status: محفظة إيثريوم كريبتو diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 00000000000..1a6a7a4c839 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,18 @@ +Status combines pseudonymous privacy-focused messenger and secure crypto wallet into one powerful communication tool. Chat with friends and growing communities. Buy, store, and exchange digital assets. + +Status is your Ethereum operating system. + +SECURE ETHEREUM WALLET +The Status crypto wallet allows you to securely send, store, and exchange Ethereum assets such as ETH, SNT, stable coins such as DAI, as well as collectibles. Confidently take control of your cryptocurrency and digital assets with our multichain Ethereum wallet app, supporting Ethereum Mainnet, Base, Arbitrum, and Optimism. The Status blockchain wallet currently only supports ETH, ERC-20, ERC-721, and ERC-1155 assets; it does not support Bitcoin. + +PRIVATE MESSENGER +Send private 1:1 and private group chats without anyone snooping on your communications. Status is a messenger app that eliminates centralised message relays for greater privacy and secure messaging. All messages are encrypted with end-to-end encryption. Additionally, no message exposes who the author or the intended recipient is, so nobody, not even Status, knows who is talking to who or what was said. + +EARN WITH DEFI +Put your crypto to work with the latest decentralised finance apps and decentralised exchanges (DEX) such as Maker, Aave, Uniswap, Synthetix, PoolTogether, Zerion, Kyber, and more. + +CONNECT WITH YOUR COMMUNITY +Explore, connect, and chat with your favourite communities and friends. Whether it is a small group of friends, an artist collective, crypto traders, or the next big organization - text and communicate with Status communities. + +PRIVATE ACCOUNT CREATION +Stay private with pseudo-anonymous account creation. When creating your free account, you will NEVER have to enter a phone number, email address, or bank account. Your wallet private keys are locally generated and securely stored to ensure only you have access to your funds and financial transactions. diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.jpg b/fastlane/metadata/android/en-US/images/featureGraphic.jpg new file mode 100644 index 00000000000..d7d889b8337 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.jpg differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 00000000000..1e95f046ba6 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png new file mode 100644 index 00000000000..42493df3008 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png new file mode 100644 index 00000000000..a720dc24d1d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png new file mode 100644 index 00000000000..edccd39c120 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png new file mode 100644 index 00000000000..eb8611516ad Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png new file mode 100644 index 00000000000..2ea43e57c96 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png new file mode 100644 index 00000000000..12125b30646 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png new file mode 100644 index 00000000000..e573b05cdf2 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png new file mode 100644 index 00000000000..aa112f3f5f5 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 00000000000..873fc4371ea --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Connect Private Finance & Web3 diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 00000000000..fef6ccbb055 --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +Status: Ethereum Crypto Wallet diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt new file mode 100644 index 00000000000..b7bcbe00c50 --- /dev/null +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -0,0 +1,18 @@ +Status combina una mensajería seudónima centrada en la privacidad y una Billetera de criptomonedas segura en una poderosa herramienta de comunicación. Chatea con amigos y comunidades en crecimiento. Compra, almacena e intercambia activos digitales. + +Status es tu sistema operativo Ethereum. + +BILLETERA ETHEREUM SEGURA +La Billetera de criptomonedas Status te permite enviar, almacenar e intercambiar de forma segura activos Ethereum como ETH, SNT, stablecoins como DAI, así como coleccionables. Toma el control de tu criptomoneda y activos digitales con confianza con nuestra Billetera multicadena, compatible con Ethereum Mainnet, Base, Arbitrum y Optimism. La Billetera Status actualmente solo admite activos ETH, ERC-20, ERC-721 y ERC-1155; no admite Bitcoin. + +MENSAJERÍA PRIVADA +Envía chats privados 1:1 y grupales sin que nadie espíe tus comunicaciones. Status es una aplicación de mensajería que elimina los servidores de mensajes centralizados para una mayor privacidad y una mensajería segura. Todos los mensajes están encriptados de extremo a extremo. Además, ningún mensaje expone quién es el autor o el destinatario previsto, por lo que nadie, ni siquiera Status, sabe quién está hablando con quién o qué se dijo. + +GANA CON DEFI +Pon tus criptomonedas a trabajar con las últimas aplicaciones financieras descentralizadas y plataformas de intercambio descentralizadas (DEX) como Maker, Aave, Uniswap, Synthetix, PoolTogether, Zerion, Kyber y más. + +CONECTA CON TU COMUNIDAD +Explora, conéctate y chatea con tus comunidades y amigos. Ya sea un pequeño grupo de amigos, un colectivo de artistas, traders de criptomonedas o la próxima gran organización, envía mensajes de texto y comunícate con las comunidades de Status. + +CREACIÓN DE CUENTAS PRIVADAS +Mantén la privacidad con la creación de cuentas seudónimas. Al crear tu cuenta gratuita, NUNCA tendrás que compartir tu número de teléfono, tu dirección de correo electrónico o una cuenta bancaria. Las claves privadas de tu billetera se generan localmente y se almacenan de forma segura para garantizar que solo tú tienes acceso a tus fondos y transacciones financieras. diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt new file mode 100644 index 00000000000..55a7c3b1545 --- /dev/null +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -0,0 +1 @@ +Conecta tus finanzas y la web3 diff --git a/fastlane/metadata/android/es-ES/title.txt b/fastlane/metadata/android/es-ES/title.txt new file mode 100644 index 00000000000..941e46c1de1 --- /dev/null +++ b/fastlane/metadata/android/es-ES/title.txt @@ -0,0 +1 @@ +Status: Billetera Ethereum diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 00000000000..74162b0f257 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,18 @@ +Status combine une messagerie pseudonyme axée sur la confidentialité et un portefeuille cryptographique sécurisé en un seul outil de communication puissant. Discutez avec des amis et des communautés en pleine croissance. Achetez, stockez et échangez des actifs numériques. + +Status est votre système d'exploitation Ethereum. + +PORTEFEUILLE ETHEREUM SÉCURISÉ +Le portefeuille cryptographique Status vous permet d'envoyer, de stocker et d'échanger en toute sécurité des actifs Ethereum tels que ETH, SNT, des pièces stables telles que DAI, ainsi que des objets de collection. Prenez le contrôle en toute confiance de votre cryptomonnaie et de vos actifs numériques avec notre application de portefeuille Ethereum multichaîne, prenant en charge le réseau principal, Base, Arbitrum et Optimism. Le portefeuille blockchain Status ne prend actuellement en charge que les actifs ETH, ERC-20, ERC-721 et ERC-1155 ; il ne prend pas en charge Bitcoin. + +MESSAGERIE PRIVÉE +Envoyez des conversations privées en tête-à-tête et en groupe sans que personne ne surveille vos communications. Status est une application de messagerie qui élimine les relais de messages centralisés pour une plus grande confidentialité et une messagerie sécurisée. Tous les messages sont cryptés avec un cryptage de bout en bout. De plus, aucun message n'expose l'identité de l'auteur ou du destinataire prévu, de sorte que personne, pas même Status, ne sait qui parle à qui ou ce qui a été dit. + +GAGNEZ DE L'ARGENT AVEC DEFI +Mettez votre crypto au travail avec les dernières applications financières décentralisées et les échanges décentralisés (DEX) tels que Maker, Aave, Uniswap, Synthetix, PoolTogether, Zerion, Kyber, et plus encore. + +CONNECTEZ-VOUS AVEC VOTRE COMMUNAUTÉ +Explorez, connectez-vous et discutez avec vos communautés et amis préférés. Qu'il s'agisse d'un petit groupe d'amis, d'un collectif d'artistes, de traders de crypto-monnaies ou de la prochaine grande organisation, envoyez des SMS et communiquez avec les communautés Status. + +CRÉATION DE COMPTE PRIVÉ +Restez privé grâce à la création de compte pseudo-anonyme. Lors de la création de votre compte gratuit, vous n'aurez JAMAIS besoin de saisir un numéro de téléphone, une adresse e-mail ou un compte bancaire. Les clés privées de votre portefeuille sont générées localement et stockées en toute sécurité pour garantir que vous seul avez accès à vos fonds et à vos transactions financières. diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 00000000000..209638df37a --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Finance privée + Web3 diff --git a/fastlane/metadata/android/fr-FR/title.txt b/fastlane/metadata/android/fr-FR/title.txt new file mode 100644 index 00000000000..d5ae8d762f7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/title.txt @@ -0,0 +1 @@ +Status: Portefeuille Ethereum diff --git a/fastlane/metadata/android/ja-JP/full_description.txt b/fastlane/metadata/android/ja-JP/full_description.txt new file mode 100644 index 00000000000..a9efa0992f8 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/full_description.txt @@ -0,0 +1,21 @@ +Statusは、匿名のプライバシー重視のメッセンジャーと安全な暗号通貨ウォレットを1つの強力なコミュニケーションツールに組み合わせています。友達や成長中のコミュニティとチャットしましょう。デジタル資産を購入、保管、交換しましょう。 + +StatusはあなたのEthereumオペレーティングシステムです。 + +安全なEthereumウォレット +Status暗号通貨ウォレットを使用すると、ETH、SNT、DAIなどのステーブルコイン、収集品などのEthereum資産を安全に送信、保管、交換できます。メインネット、Base、 Arbitrum、OptimismをサポートするマルチチェーンEthereumウォレットアプリを使用して、暗号通貨とデジタル資産を自信を持って管理できます。 Status ブロックチェーン ウォレットは現在、ETH、ERC-20、ERC-721、ERC-1155 アセットのみをサポートしており、ビットコインはサポートしていません。 + +プライベート メッセンジャー +誰にも通信を覗かれることなく、プライベートな 1:1 チャットやプライベート グループ チャットを送信できます。Status は、集中型のメッセージ リレーを排除してプライバシーと安全なメッセージングを実現するメッセンジャー アプリです。すべてのメッセージはエンドツーエンドの暗号化で暗号化されます。さらに、メッセージの作成者や宛先の受信者が誰であるかは明らかにされないため、Status でさえ、誰が誰と話しているのか、何が話されたのかを知ることはできません。 + +DEFI で稼ぐ + +Maker、Aave、Uniswap、Synthetix、PoolTogether、Zerion、Kyber などの最新の分散型金融アプリや分散型取引所 (DEX) で暗号通貨を活用しましょう。 + +コミュニティとつながる + +お気に入りのコミュニティや友達を探索、接続、チャットしましょう。少人数の友人グループ、アーティスト集団、暗号通貨トレーダー、または次の大きな組織であっても、Status コミュニティとテキストでやり取りできます。 + +プライベート アカウントの作成 + +疑似匿名アカウントの作成でプライバシーを確保します。無料アカウントを作成するときに、電話番号、メール アドレス、銀行口座を入力する必要はありません。ウォレットの秘密鍵はローカルで生成され、安全に保管されるため、資金と金融取引にアクセスできるのはあなただけです。 diff --git a/fastlane/metadata/android/ja-JP/short_description.txt b/fastlane/metadata/android/ja-JP/short_description.txt new file mode 100644 index 00000000000..825f8aadeeb --- /dev/null +++ b/fastlane/metadata/android/ja-JP/short_description.txt @@ -0,0 +1 @@ +プライベート ファイナンスと Web3 を接続 diff --git a/fastlane/metadata/android/ja-JP/title.txt b/fastlane/metadata/android/ja-JP/title.txt new file mode 100644 index 00000000000..efd647343d6 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/title.txt @@ -0,0 +1 @@ +Status: イーサリアム クリプト ウォレット diff --git a/fastlane/metadata/android/ko-KR/full_description.txt b/fastlane/metadata/android/ko-KR/full_description.txt new file mode 100644 index 00000000000..02529c62b34 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/full_description.txt @@ -0,0 +1,21 @@ +Status는 익명의 개인 정보 보호 중심 메신저와 안전한 암호화폐 지갑을 하나의 강력한 커뮤니케이션 도구로 결합합니다. 친구 및 성장하는 커뮤니티와 채팅하세요. 디지털 자산을 구매, 저장 및 교환하세요. + +Status는 귀하의 Ethereum 운영 체제입니다. + +안전한 이더리움 지갑 +Status 암호화폐 지갑을 사용하면 ETH, SNT, DAI와 같은 스테이블 코인, 수집품과 같은 이더리움 자산을 안전하게 보내고, 저장하고, 교환할 수 있습니다. 메인넷, Base Arbitrum 및 Optimism을 지원하는 멀티체인 이더리움 지갑 앱으로 암호화폐와 디지털 자산을 자신 있게 제어하세요. Status 블록체인 지갑은 현재 ETH, ERC-20, ERC-721 및 ERC-1155 자산만 지원하며 비트코인은 지원하지 않습니다. + +개인 메신저 +아무도 귀하의 커뮤니케이션을 엿보지 않고 개인 1:1 및 개인 그룹 채팅을 보내세요. Status는 중앙 집중식 메시지 릴레이를 제거하여 더 큰 프라이버시와 안전한 메시징을 제공하는 메신저 앱입니다. 모든 메시지는 종단 간 암호화로 암호화됩니다. 또한, 메시지에는 작성자 또는 의도된 수신자가 노출되지 않으므로 Status를 포함하여 아무도 누가 누구와 대화하고 있는지 또는 무슨 말을 했는지 알 수 없습니다. + +DEFI로 수익 창출 + +Maker, Aave, Uniswap, Synthetix, PoolTogether, Zerion, Kyber 등과 같은 최신 분산 금융 앱 및 분산 거래소(DEX)에서 암호화폐를 활용하세요. + +커뮤니티와 연결 + +좋아하는 커뮤니티와 친구를 탐색하고, 연결하고, 채팅하세요. 소규모 친구 그룹, 아티스트 집단, 암호화폐 거래자 또는 차세대 대기업이든 Status 커뮤니티에 문자를 보내고 소통하세요. + +개인 계정 생성 + +익명 계정을 생성하여 개인 정보를 보호하세요. 무료 계정을 만들 때 전화번호, 이메일 주소 또는 은행 계좌를 입력할 필요가 없습니다. 지갑 개인 키는 로컬에서 생성되고 안전하게 저장되어 본인만 자금과 금융 거래에 액세스할 수 있습니다. diff --git a/fastlane/metadata/android/ko-KR/short_description.txt b/fastlane/metadata/android/ko-KR/short_description.txt new file mode 100644 index 00000000000..197b6757827 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/short_description.txt @@ -0,0 +1 @@ +개인 금융 및 Web3 연결 diff --git a/fastlane/metadata/android/ko-KR/title.txt b/fastlane/metadata/android/ko-KR/title.txt new file mode 100644 index 00000000000..053cc6ca04f --- /dev/null +++ b/fastlane/metadata/android/ko-KR/title.txt @@ -0,0 +1 @@ +Status: 이더리움 크립토 지갑 diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt new file mode 100644 index 00000000000..13921edcec1 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -0,0 +1,18 @@ +Status é um app de mensagem e uma carteira de criptomoedas focado em privacidade e anonimato. Você pode conversar com seus amigos e interagir com comunidades. Compre, armazene e troque ativos digitais. + +Status é o seu sistema operacional para Ethereum. + +CARTEIRA ETHEREUM SEGURA +A carteira de criptomoedas Status permite que você envie, armazene e troque ativos Ethereum, como ETH, SNT, stablecoins como DAI, além de colecionáveis, de forma segura. Controle com confiança suas criptomoedas e ativos digitais com nosso aplicativo de carteira Ethereum multichain, que suporta Ethereum Mainnet, Base, Arbitrum e Optimism. A carteira blockchain da Status atualmente suporta apenas ativos ETH, ERC-20, ERC-721 e ERC-1155; não suporta Bitcoin. + +APP DE MENSAGEM PRIVADO +Envie mensagens privadas em chats 1:1 ou em grupos. Status é um aplicativo de mensagem descentralizado que foca em privacidade e anonimato. Todas as mensagens são protegidas com criptografia de ponta a ponta. Além disso, nenhuma mensagem revela quem é o(a) autor(a) ou o(a) destinatário(a) pretendido(a), então ninguém, nem mesmo a Status, sabe quem está conversando com quem ou o que foi dito. + +MONETIZE COM DEFI E STAKING +Coloque suas criptomoedas para trabalhar (staking) com os mais recentes aplicativos de finanças descentralizadas e exchanges descentralizadas (DEX), como Maker, Aave, Uniswap, Synthetix, PoolTogether, Zerion, Kyber e mais. + +CONECTE-SE COM SUA COMUNIDADE +Explore, conecte-se e converse com seus amigos e comunidades favoritas. Seja um grupo de amigos, de artistas, de traders de criptomoedas ou a próxima grande empresa - envie textos e comunique-se através das comunidades da Status. + +CRIAÇÃO DE CONTA PRIVADA +Mantenha sua privacidade com a criação de conta anônima. Ao criar sua conta gratuita, você NUNCA precisará inserir um número de telefone, endereço de e-mail ou conta bancária. Suas chaves privadas da carteira são geradas localmente e armazenadas com segurança para garantir que apenas você tenha acesso aos seus fundos e transações financeiras. diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt new file mode 100644 index 00000000000..634d8b43559 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/short_description.txt @@ -0,0 +1 @@ +Privacidade Financeira e Web3 diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt new file mode 100644 index 00000000000..47e98e79eb2 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/title.txt @@ -0,0 +1 @@ +Status: Carteira Ethereum diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000000..5c21acb08b1 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,18 @@ +Status объединяет анонимный мессенджер, ориентированный на конфиденциальность, и защищенный криптокошелек в один мощный инструмент для финансов и общения. Общайтесь с друзьями, находите новые сообщества, а также покупайте, храните и обменивайте цифровые активы. + +Status — это ваша операционная система для Ethereum. + +БЕЗОПАСНЫЙ КОШЕЛЕК ETHEREUM +Крипто-кошелек Status позволяет вам безопасно отправлять, хранить и обменивать активы Ethereum, такие как ETH, SNT, стейблкоины, такие как DAI, а также невзаимозаменяемые токены. Контролируйте свою криптовалюту с помощью нашего криптокошелека для Ethereum, поддерживающего Ethereum, Base, Arbitrum и Optimism. В настоящее время криптокошелек Status поддерживает такие активы ETH, ERC-20, ERC-721 и ERC-1155. + +ПРИВАТНИЙ МЕСЕНДЖЕР +Отправляйте приватные сообщения друзьям или группам. Status — это приложение для обмена сообщениями, которое поддерживает децентрализованную систему для повышения конфиденциальности и безопасного обмена сообщениями. Все сообщения зашифрованы с помощью сквозного шифрования. Более того, ни одно сообщение не содержит информации об авторе или получателе, поэтому никто, даже Status, не знает, кто с кем общается или что было отправлено. + +ЗАРАБАТЫВАЙТЕ С DEFI +Используйте свою криптовалюту для работы с новейшими приложениями для децентрализованных финансов и децентрализованными криптобиржами (DEX), такими как Maker, Aave, Uniswap, Synthetix, PoolTogether, Kyber и другие. + +НАЙДИТЕ ВАШЕ СООБЩЕСТВО +Находите, исследуйте и общайтесь с любимыми сообществами и друзьями. Независимо от того, является ли это небольшой группой друзей, коллективом художников, криптотрейдеров или следующим большим сообществом единомышленников — отправляйте текстовые сообщения и общайтесь с сообществами в Status. + +СОЗДАНИЕ ПРИВАТНОГО АККАУНТА +Оставайтесь конфиденциальными с помощью создания анонимного аккаунта. При создании аккаунта вам НИКОГДА не нужно будет вводить номер телефона, адрес электронной почты или банковский счет. Приватные ключи вашего криптокошелька генерируются локально на вашем устройстве и надежно хранятся, чтобы только вы имели доступ к своим средствам и финансовым транзакциям. diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000000..5811246908c --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Web3 финансы и приват чат diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000000..1f01ef2483d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Status: Ethereum Криптокошелек diff --git a/fastlane/metadata/android/tr-TR/full_description.txt b/fastlane/metadata/android/tr-TR/full_description.txt new file mode 100644 index 00000000000..1869c0f50f3 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/full_description.txt @@ -0,0 +1,18 @@ +Status, takma adlı gizlilik odaklı mesajlaşmayı ve güvenli kripto cüzdanını tek bir güçlü iletişim aracında birleştiriyor. Arkadaşlarınızla ve büyüyen topluluklarla sohbet edin. Dijital varlıkları satın alın, saklayın ve değiştirin. + +Status, Ethereum işletim sisteminizdir. + +GÜVENLİ ETHEREUM CÜZDANI +Status kripto cüzdanı, ETH, SNT, DAI gibi sabit paralar ve koleksiyonluk eşyalar gibi Ethereum varlıklarını güvenli bir şekilde göndermenize, saklamanıza ve değiştirmenize olanak tanır. Ana ağ, Base, Arbitrum ve Optimism'i destekleyen çok zincirli Ethereum cüzdan uygulamamızla kripto paranızın ve dijital varlıklarınızın kontrolünü güvenle ele alın. Status blok zinciri cüzdanı şu anda yalnızca ETH, ERC-20, ERC-721 ve ERC-1155 varlıklarını destekliyor; Bitcoin'i desteklemiyor. + +ÖZEL MESAJLAŞMACI +İletişimlerinizi kimse gözetlemeden özel 1:1 ve özel grup sohbetleri gönderin. Status, daha fazla gizlilik ve güvenli mesajlaşma için merkezi mesaj iletimlerini ortadan kaldıran bir mesajlaşma uygulamasıdır. Tüm mesajlar uçtan uca şifrelemeyle şifrelenir. Ayrıca, hiçbir mesaj yazarının veya hedeflenen alıcının kim olduğunu ifşa etmez, bu nedenle hiç kimse, Status bile kimin kiminle konuştuğunu veya ne söylendiğini bilmez. + +DEFI İLE KAZANIN +Kripto paranızı Maker, Aave, Uniswap, Synthetix, PoolTogether, Zerion, Kyber ve daha fazlası gibi en son merkezi olmayan finans uygulamaları ve merkezi olmayan borsalarla (DEX) çalıştırın. + +TOPLULUĞUNUZLA BAĞLANTI KURUN +En sevdiğiniz toplulukları ve arkadaşlarınızla keşfedin, bağlantı kurun ve sohbet edin. İster küçük bir arkadaş grubu, ister bir sanatçı kolektifi, kripto tüccarları veya bir sonraki büyük organizasyon olsun - Status topluluklarıyla mesajlaşın ve iletişim kurun. + +ÖZEL HESAP OLUŞTURMA +Sahte anonim hesap oluşturma ile gizli kalın. Ücretsiz hesabınızı oluştururken ASLA bir telefon numarası, e-posta adresi veya banka hesabı girmeniz gerekmeyecek. Cüzdan özel anahtarlarınız yerel olarak oluşturulur ve yalnızca sizin fonlarınıza ve finansal işlemlerinize erişebilmenizi sağlamak için güvenli bir şekilde saklanır. diff --git a/fastlane/metadata/android/tr-TR/short_description.txt b/fastlane/metadata/android/tr-TR/short_description.txt new file mode 100644 index 00000000000..62d38cb14af --- /dev/null +++ b/fastlane/metadata/android/tr-TR/short_description.txt @@ -0,0 +1 @@ +Özel Finans ve Web3'ü Bağlayın diff --git a/fastlane/metadata/android/tr-TR/title.txt b/fastlane/metadata/android/tr-TR/title.txt new file mode 100644 index 00000000000..b40a6bf0f13 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/title.txt @@ -0,0 +1 @@ +Status Ethereum Kripto Cüzdanı diff --git a/fastlane/metadata/android/uk-UA/full_description.txt b/fastlane/metadata/android/uk-UA/full_description.txt new file mode 100644 index 00000000000..098f9976041 --- /dev/null +++ b/fastlane/metadata/android/uk-UA/full_description.txt @@ -0,0 +1,18 @@ +Status поєднує анонімний месенджер, орієнтований на конфіденційність та захищений криптогаманець в один потужний інструмент для фінансів та спілкування. Спілкуйтеся з друзями та спільнотами. Купуйте, зберігайте та обмінюйте крипто активи. + +Status — це ваша операційна система для Ethereum. + +БЕЗПЕЧНИЙ КРИПТОГАМАНЕЦЬ ДЛЯ ETHEREUM +Криптогаманець Status дозволяє безпечно надсилати, зберігати та обмінювати крипто активи на Ethereum, такі як ETH, SNT, стейблкоїни та DAI, а також невзаємозамінні токени. Впевнено контролюйте свої крипто активи за допомогою нашого блокчейн гаманця для Ethereum, який підтримує Ethereum, Base, Arbitrum та Optimism. Криптогаманець Status наразі підтримує такі кприпто активи як ETH, ERC-20, ERC-721 та ERC-1155. + +ПРИВАТНИЙ МЕСЕНДЖЕР +Надсилайте приватні повідомлення друзям або групам. Status — це програма для обміну повідомленнями, яка підтримує децентралізовану систему повідомлень для більшої конфіденційності та безпечного обміну повідомленнями. Усі повідомлення зашифровано за допомогою наскрізного шифрування. Крім того, жодне повідомлення не містить інформації про автора чи одержувача, тому ніхто, навіть Status, не знає, хто з ким спілкувється або що було надіслано. + +ЗАРОБЛЯЙТЕ З DEFI +Налаштуйте свою криптовалюту на роботу з найновішими програмами для децентралізованих фінансів і децентралізованими крипто біржами (DEX), такими як Maker, Aave, Uniswap, Synthetix, PoolTogether, Kyber тощо. + +ЗНАЙДІТЬ ВАШУ СПІЛЬНОТУ +Знаходьте, досліджуйте та спілкуйтеся з улюбленими спільнотами та друзями. Незалежно від того, чи це невелика група друзів, колектив художників, криптотрейдерів чи наступна велика спільнота однодумців – надсилайте текстові повідомлення та спілкуйтеся зі спільнотами у Status. + +СТВОРЕННЯ ПРИВАТНОГО ОБЛІКОВОГО ЗАПИСУ +Залишайтеся конфіденційними за допомогою створення анонімного облікового запису. Під час створення облікового запису вам НІКОЛИ не доведеться вводити номер телефону, адресу електронної пошти чи банківський рахунок. Приватні ключі вашого крипто гаманця генеруються локально на вашому пристрої та надійно зберігаються, щоб лише Ви мали доступ до своїх коштів і фінансових транзакцій. diff --git a/fastlane/metadata/android/uk-UA/short_description.txt b/fastlane/metadata/android/uk-UA/short_description.txt new file mode 100644 index 00000000000..34bd9af60d8 --- /dev/null +++ b/fastlane/metadata/android/uk-UA/short_description.txt @@ -0,0 +1 @@ +Web3 фінанси та приват чат diff --git a/fastlane/metadata/android/uk-UA/title.txt b/fastlane/metadata/android/uk-UA/title.txt new file mode 100644 index 00000000000..3741b63365c --- /dev/null +++ b/fastlane/metadata/android/uk-UA/title.txt @@ -0,0 +1 @@ +Status Ethereum Криптогаманець diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt new file mode 100644 index 00000000000..28d310190b3 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -0,0 +1,18 @@ +Status 将保护匿名私密的消息应用,与安全的加密货币钱包结合成一个强大的通信工具。您可以与朋友或在社群中聊天,也可以购买、存储和交换数字资产。 + +Status 是你的以太坊操作系统。 + +安全的以太坊钱包 +Status 加密货币钱包支持安全地发送、存储和交换以太坊资产(例如 ETH、SNT)、稳定币(例如 DAI)以及数字藏品。Status 多链以太坊钱包支持主网、Base Arbitrum 和 Optimism,让您安心掌控加密货币和数字资产。Status 区块链钱包目前仅支持 ETH、ERC-20、ERC-721 和 ERC-1155 资产;不支持 Bitcoin。 + +私密通信 +一对一或同多人发送私密消息,没有人能窥探你的通信内容。Status 不使用中心化消息中继,提升消息的隐私和安全。所有消息都经过端到端加密。此外,消息不含发送或接收者的身份信息,包括 Status 在内的任何人都无法获知消息内容或交流者身份。 + +赚取 DeFi 收益 +使用最新的去中心化金融应用和去中心化交易所 (DEX),如 Maker、Aave、Uniswap、Synthetix、PoolTogether、Zerion、Kyber 等,充分发挥加密货币的作用。 + +连接社群 +探索、连接并与你最喜欢的社群和朋友聊天。无论是朋友群聊、艺术家团体、加密货币交易者,还是潜在大型组织,都可以通过在 Status 社群中发送信息,进行交流。 + +创建私密账号 +免费创建匿名账号,保护隐私。Status 不会向您索要电话号码、电子邮件地址或银行账户。钱包私钥在本地生成并安全存储,只有您自己可以获取资金和金融交易记录。 diff --git a/fastlane/metadata/android/zh-CN/short_description.txt b/fastlane/metadata/android/zh-CN/short_description.txt new file mode 100644 index 00000000000..1f735456a30 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/short_description.txt @@ -0,0 +1 @@ +连接个人金融和Web3 diff --git a/fastlane/metadata/android/zh-CN/title.txt b/fastlane/metadata/android/zh-CN/title.txt new file mode 100644 index 00000000000..d1152f6c389 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/title.txt @@ -0,0 +1 @@ +Status: 以太坊加密钱包 diff --git a/fastlane/update.sh b/fastlane/update.sh new file mode 100755 index 00000000000..91f10dd6e4b --- /dev/null +++ b/fastlane/update.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env nix-shell +#! nix-shell -i bash -p bash ruby bundler bundix +set -x +set -e + +SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) + +rm -f "${GIT_ROOT}/fastlane/Gemfile.lock" + +bundler install \ + --gemfile="${GIT_ROOT}/fastlane/Gemfile" \ + --path "${GIT_ROOT}/fastlane/.bundle/vendor" +bundix \ + --gemfile="${GIT_ROOT}/fastlane/Gemfile" \ + --lockfile="${GIT_ROOT}/fastlane/Gemfile.lock" \ + --gemset="${GIT_ROOT}/fastlane/gemset.nix" + +rm -rf "${GIT_ROOT}/fastlane/.bundle/vendor" + +if [ "clean" == "$1" ]; then + rm -r ~/.gem +fi diff --git a/figwheel-bridge.js b/figwheel-bridge.js deleted file mode 100644 index ecbd394e922..00000000000 --- a/figwheel-bridge.js +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Originally taken from https://github.com/decker405/figwheel-react-native - * - * @providesModule figwheel-bridge - */ - -var CLOSURE_UNCOMPILED_DEFINES = null; -var debugEnabled = true; - -var config = { - basePath: "target/", - googBasePath: 'goog/', - serverPort: 8081 -}; - -var React = require('react'); -var createReactClass = require('create-react-class'); -var ReactNative = require('react-native'); -var WebSocket = require('WebSocket'); -var self; -var evaluate = eval; // This is needed, direct calls to eval does not work (RN packager???) -var externalModules = {}; -var evalListeners = {}; -var asyncImportChain = new Promise(function (succ,fail) {succ(true);}); - -function fireEvalListenters(url) { - Object.values(evalListeners).forEach(function (listener) { - listener(url) - }); -} - -function formatCompileError(msg) { - var errorStr = "Figwheel Compile Exception: " - var data = msg['exception-data']; - if(data['message']) { - errorStr += data['message'] + " "; - } - if(data['file']) { - errorStr += "in file " + data['file'] + " "; - } - if(data['line']) { - errorStr += "at line " + data['line']; - } - if(data['column']) { - errorStr += ", column " + data['column']; - } - return errorStr; -} - -/* This is simply demonstrating that we can receive and react to - * arbitrary messages from Figwheel this will enable creating a nicer - * feedback system in the Figwheel top level React component. - */ -function figwheelMessageHandler(msg) { - if(msg["msg-name"] == "compile-failed") { - console.warn(formatCompileError(msg)); - } -} - -function listenToFigwheelMessages() { - if(figwheel.client.add_json_message_watch) { - figwheel.client.add_json_message_watch("ReactNativeMessageIntercept", - figwheelMessageHandler); - } -} - -var figwheelApp = function (platform, devHost) { - return createReactClass({ - getInitialState: function () { - return {loaded: false} - }, - render: function () { - if (!this.state.loaded) { - var plainStyle = {flex: 1, alignItems: 'center', justifyContent: 'center'}; - return ( - - Waiting for Figwheel to load files. - - ); - } - return this.state.root; - }, - - componentDidMount: function () { - var app = this; - if (typeof goog === "undefined") { - loadApp(platform, devHost, function (appRoot) { - app.setState({root: appRoot, loaded: true}); - listenToFigwheelMessages(); - }); - } - } - }) -}; - -function logDebug(msg) { - if (debugEnabled) { - console.log(msg); - } -} - -var isChrome = function () { - return typeof importScripts === "function" -}; - -async function getUrlText(url) { - const text = await fetch(url).then(response => { - if(!response.ok) { - throw new Error("Failed to Fetch: " + url + " - Perhaps your project was cleaned and you haven't recompiled?"); - } - return response.text() - }); - return text; -} - -var ATTEMPTS_COUNT = 3; -async function asyncImportScripts(url, success, error) { - - var attempt = 0; - var text = await getUrlText(url); - - while(attempt < ATTEMPTS_COUNT && text.length === 0) - { - text = await getUrlText(url); - ++attempt; - } - - if(!text || 0 === text.length) { - console.log("Can't fetch file: ", url) - return; - } - evaluate(text); - fireEvalListenters(url); - success(); -} - -function syncImportScripts(url, success, error) { - try { - importScripts(url); - logDebug('Evaluated: ' + url); - fireEvalListenters(url); - success(); - } catch (e) { - console.error(e); - error() - } -} - -// Loads js file sync if possible or async. -function importJs(src, success, error) { - var noop = function(){}; - success = (typeof success == 'function') ? success : noop; - error = (typeof error == 'function') ? error : noop; - logDebug('(importJs) Importing: ' + src); - if (isChrome()) { - syncImportScripts(src, success, error); - } else { - asyncImportChain = asyncImportChain.then(function (v) {return asyncImportScripts(src, success, error);}) - } -} - -function interceptRequire() { - var oldRequire = window.require; - console.info("Shimming require"); - window.require = function (id) { - console.info("Requiring: " + id); - if (externalModules[id]) { - return externalModules[id]; - } - return oldRequire(id); - }; -} - -function serverBaseUrl(host) { - return "http://" + host + ":" + config.serverPort -} - -function isUnDefined(x) { - return typeof x == "undefined"; -} - -// unlikely to happen but it happened to me a couple of times so ... -function assertRootElExists(platform) { - var basicMessage = "ClojureScript project didn't compile, or didn't load correctly."; - if(isUnDefined(env)) { - throw new Error("Critical Error: env namespace not defined - " + basicMessage); - } else if(isUnDefined(env[platform])) { - throw new Error("Critical Error: env." + platform + " namespace not defined - " + basicMessage); - } else if(isUnDefined(env[platform].main)) { - throw new Error("Critical Error: env." + platform + ".main namespace not defined - " + basicMessage); - } else if(isUnDefined(env[platform].main.root_el)) { - throw new Error("Critical Error: env." + - platform + ".main namespace doesn't define a root-el which should hold the root react node of your app."); - } - } - -function loadApp(platform, devHost, onLoadCb) { - var fileBasePath = serverBaseUrl((isChrome() ? "localhost" : devHost)) + "/" + config.basePath + platform; - - // callback when app is ready to get the reloadable component - var mainJs = `/env/${platform}/main.js`; - evalListeners.waitForFinalEval = function (url) { - if (url.indexOf(mainJs) > -1) { - assertRootElExists(platform); - onLoadCb(env[platform].main.root_el); - console.info('Done loading Clojure app'); - delete evalListeners.waitForFinalEval; - } - }; - - if (typeof goog === "undefined") { - console.info('Loading Closure base.'); - interceptRequire(); - // need to know base path here - importJs(fileBasePath + '/goog/base.js', function () { - shimBaseGoog(fileBasePath); - importJs(fileBasePath + '/cljs_deps.js'); - importJs(fileBasePath + '/goog/deps.js', function () { - // This is needed because of RN packager - // seriously React packager? why. - var googreq = goog.require; - // NOTE: :closure-defines is not working with RN - goog.global.CLOSURE_UNCOMPILED_DEFINES = { - "re_frame.trace.trace_enabled_QMARK_":true, - "day8.re_frame.tracing.trace_enabled_QMARK_": true - }; - googreq(`env.${platform}.main`); - }); - }); - } -} - -function startApp(appName, platform, devHost) { - ReactNative.AppRegistry.registerComponent( - appName, () => figwheelApp(platform, devHost)); -} - -function withModules(moduleById) { - externalModules = moduleById; - return self; -} - -function figwheelImportScript(uri, callback) { - importJs(uri.toString(), - function () {callback(true);}, - function () {callback(false);}) -} - -// Goog fixes -function shimBaseGoog(basePath) { - console.info('Shimming goog functions.'); - goog.basePath = basePath + '/' + config.googBasePath; - goog.global.FIGWHEEL_WEBSOCKET_CLASS = WebSocket; - goog.global.FIGWHEEL_IMPORT_SCRIPT = figwheelImportScript; - goog.writeScriptSrcNode = importJs; - goog.writeScriptTag_ = function (src, optSourceText) { - importJs(src); - return true; - }; -} - -self = { - withModules: withModules, - start: startApp -}; - -module.exports = self; diff --git a/findSymlinkedModules.js.patch b/findSymlinkedModules.js.patch deleted file mode 100644 index 5bd9e6dfdfa..00000000000 --- a/findSymlinkedModules.js.patch +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @format - * @flow - */ - -const path = require('path'); -const fs = require('fs'); - -/** - * Find symlinked modules inside "node_modules." - * - * Naively, we could just perform a depth-first search of all folders in - * node_modules, recursing when we find a symlink. - * - * We can be smarter than this due to our knowledge of how npm/Yarn lays out - * "node_modules" / how tools that build on top of npm/Yarn (such as Lerna) - * install dependencies. - * - * Starting from a given root node_modules folder, this algorithm will look at - * both the top level descendants of the node_modules folder or second level - * descendants of folders that start with "@" (which indicates a scoped - * package). If any of those folders is a symlink, it will recurse into the - * link, and perform the same search in the linked folder. - * - * The end result should be a list of all resolved module symlinks for a given - * root. - */ -module.exports = function findSymlinkedModules( - projectRoot: string, - ignoredRoots: Array = [], -) { - const timeStart = Date.now(); - const nodeModuleRoot = path.join(projectRoot, 'node_modules'); - const resolvedSymlinks = findModuleSymlinks(nodeModuleRoot, [ - ...ignoredRoots, - projectRoot, - ]); - const timeEnd = Date.now(); - - console.log( - `Scanning folders for symlinks in ${nodeModuleRoot} (${timeEnd - - timeStart}ms)`, - ); - - return resolvedSymlinks; -}; - -function findModuleSymlinks( - modulesPath: string, - ignoredPaths: Array = [], -): Array { - if (!fs.existsSync(modulesPath)) { - return []; - } - - // Find module symlinks - const moduleFolders = fs.readdirSync(modulesPath); - const symlinks = moduleFolders.reduce((links, folderName) => { - const folderPath = path.join(modulesPath, folderName); - const maybeSymlinkPaths = []; - if (folderName.startsWith('@')) { - const scopedModuleFolders = fs.readdirSync(folderPath); - maybeSymlinkPaths.push( - ...scopedModuleFolders.map(name => path.join(folderPath, name)), - ); - } else { - maybeSymlinkPaths.push(folderPath); - } - return links.concat(resolveSymlinkPaths(maybeSymlinkPaths, ignoredPaths)); - }, []); - - // For any symlinks found, look in _that_ modules node_modules directory - // and find any symlinked modules - const nestedSymlinks = symlinks.reduce( - (links, symlinkPath) => - links.concat( - // We ignore any found symlinks or anything from the ignored list, - // to prevent infinite recursion - findModuleSymlinks(path.join(symlinkPath, 'node_modules'), [ - ...ignoredPaths, - ...symlinks, - ]), - ), - [], - ); - - return [...new Set([...symlinks, ...nestedSymlinks])]; -} - -function resolveSymlinkPaths(maybeSymlinkPaths, ignoredPaths) { - return maybeSymlinkPaths.reduce((links, maybeSymlinkPath) => { - if (fs.lstatSync(maybeSymlinkPath).isSymbolicLink()) { - const resolved = path.resolve( - path.dirname(maybeSymlinkPath), - fs.readlinkSync(maybeSymlinkPath), - ); - if (ignoredPaths.indexOf(resolved) === -1 && fs.existsSync(resolved)) { - links.push(resolved); - } - } - return links; - }, []); -} diff --git a/index.js b/index.js new file mode 100644 index 00000000000..bc4004a365e --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +import 'node-libs-react-native/globals'; +import './result/index.js'; diff --git a/ios/.xcode.env b/ios/.xcode.env new file mode 100644 index 00000000000..48e2635ebba --- /dev/null +++ b/ios/.xcode.env @@ -0,0 +1,12 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY="${NODE_BINARY:-node}" +export NODE_ARGS="${NODE_ARGS:- --max-old-space-size=16384 }" diff --git a/ios/Bridge.swift b/ios/Bridge.swift new file mode 100644 index 00000000000..56b71f2f9ab --- /dev/null +++ b/ios/Bridge.swift @@ -0,0 +1,9 @@ +// +// Bridge.swift +// StatusIm +// +// Created by Andrea Franz on 02/12/2020. +// Copyright © 2020 Status. All rights reserved. +// + +import Foundation diff --git a/ios/Dummy.swift b/ios/Dummy.swift new file mode 100644 index 00000000000..ee5c8184cda --- /dev/null +++ b/ios/Dummy.swift @@ -0,0 +1,9 @@ +// +// Dummy.swift +// StatusIm +// +// Created by Gheorghe on 01.05.2020. +// Copyright © 2020 Status. All rights reserved. +// + +import Foundation diff --git a/ios/FirebaseCore.framework/FirebaseCore b/ios/FirebaseCore.framework/FirebaseCore deleted file mode 100755 index a11fcb343f4..00000000000 Binary files a/ios/FirebaseCore.framework/FirebaseCore and /dev/null differ diff --git a/ios/FirebaseCore.framework/Headers/FIRAnalyticsConfiguration.h b/ios/FirebaseCore.framework/Headers/FIRAnalyticsConfiguration.h deleted file mode 100755 index f42eaf54777..00000000000 --- a/ios/FirebaseCore.framework/Headers/FIRAnalyticsConfiguration.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -#import "FIRCoreSwiftNameSupport.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * This class provides configuration fields for Firebase Analytics. - */ -FIR_SWIFT_NAME(AnalyticsConfiguration) -@interface FIRAnalyticsConfiguration : NSObject - -/** - * Returns the shared instance of FIRAnalyticsConfiguration. - */ -+ (FIRAnalyticsConfiguration *)sharedInstance FIR_SWIFT_NAME(shared()); - -/** - * Sets the minimum engagement time in seconds required to start a new session. The default value - * is 10 seconds. - */ -- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval; - -/** - * Sets the interval of inactivity in seconds that terminates the current session. The default - * value is 1800 seconds (30 minutes). - */ -- (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval; - -/** - * Sets whether analytics collection is enabled for this app on this device. This setting is - * persisted across app sessions. By default it is enabled. - */ -- (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/FirebaseCore.framework/Headers/FIRApp.h b/ios/FirebaseCore.framework/Headers/FIRApp.h deleted file mode 100755 index dbec0ac30df..00000000000 --- a/ios/FirebaseCore.framework/Headers/FIRApp.h +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -#if TARGET_OS_IOS -// TODO: Remove UIKit import on next breaking change release -#import -#endif - -#import "FIRCoreSwiftNameSupport.h" - -@class FIROptions; - -NS_ASSUME_NONNULL_BEGIN - -/** A block that takes a BOOL and has no return value. */ -typedef void (^FIRAppVoidBoolCallback)(BOOL success) FIR_SWIFT_NAME(FirebaseAppVoidBoolCallback); - -/** - * The entry point of Firebase SDKs. - * - * Initialize and configure FIRApp using +[FIRApp configure] - * or other customized ways as shown below. - * - * The logging system has two modes: default mode and debug mode. In default mode, only logs with - * log level Notice, Warning and Error will be sent to device. In debug mode, all logs will be sent - * to device. The log levels that Firebase uses are consistent with the ASL log levels. - * - * Enable debug mode by passing the -FIRDebugEnabled argument to the application. You can add this - * argument in the application's Xcode scheme. When debug mode is enabled via -FIRDebugEnabled, - * further executions of the application will also be in debug mode. In order to return to default - * mode, you must explicitly disable the debug mode with the application argument -FIRDebugDisabled. - * - * It is also possible to change the default logging level in code by calling setLoggerLevel: on - * the FIRConfiguration interface. - */ -FIR_SWIFT_NAME(FirebaseApp) -@interface FIRApp : NSObject - -/** - * Configures a default Firebase app. Raises an exception if any configuration step fails. The - * default app is named "__FIRAPP_DEFAULT". This method should be called after the app is launched - * and before using Firebase services. This method is thread safe. - */ -+ (void)configure; - -/** - * Configures the default Firebase app with the provided options. The default app is named - * "__FIRAPP_DEFAULT". Raises an exception if any configuration step fails. This method is thread - * safe. - * - * @param options The Firebase application options used to configure the service. - */ -+ (void)configureWithOptions:(FIROptions *)options FIR_SWIFT_NAME(configure(options:)); - -/** - * Configures a Firebase app with the given name and options. Raises an exception if any - * configuration step fails. This method is thread safe. - * - * @param name The application's name given by the developer. The name should should only contain - Letters, Numbers and Underscore. - * @param options The Firebase application options used to configure the services. - */ -// clang-format off -+ (void)configureWithName:(NSString *)name - options:(FIROptions *)options FIR_SWIFT_NAME(configure(name:options:)); -// clang-format on - -/** - * Returns the default app, or nil if the default app does not exist. - */ -+ (nullable FIRApp *)defaultApp FIR_SWIFT_NAME(app()); - -/** - * Returns a previously created FIRApp instance with the given name, or nil if no such app exists. - * This method is thread safe. - */ -+ (nullable FIRApp *)appNamed:(NSString *)name FIR_SWIFT_NAME(app(name:)); - -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 -/** - * Returns the set of all extant FIRApp instances, or nil if there are no FIRApp instances. This - * method is thread safe. - */ -@property(class, readonly, nullable) NSDictionary *allApps; -#else -/** - * Returns the set of all extant FIRApp instances, or nil if there are no FIRApp instances. This - * method is thread safe. - */ -+ (nullable NSDictionary *)allApps FIR_SWIFT_NAME(allApps()); -#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - -/** - * Cleans up the current FIRApp, freeing associated data and returning its name to the pool for - * future use. This method is thread safe. - */ -- (void)deleteApp:(FIRAppVoidBoolCallback)completion; - -/** - * FIRApp instances should not be initialized directly. Call +[FIRApp configure], - * +[FIRApp configureWithOptions:], or +[FIRApp configureWithNames:options:] directly. - */ -- (instancetype)init NS_UNAVAILABLE; - -/** - * Gets the name of this app. - */ -@property(nonatomic, copy, readonly) NSString *name; - -/** - * Gets a copy of the options for this app. These are non-modifiable. - */ -@property(nonatomic, copy, readonly) FIROptions *options; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/FirebaseCore.framework/Headers/FIRConfiguration.h b/ios/FirebaseCore.framework/Headers/FIRConfiguration.h deleted file mode 100755 index e2c85e5a1de..00000000000 --- a/ios/FirebaseCore.framework/Headers/FIRConfiguration.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -#import "FIRAnalyticsConfiguration.h" -#import "FIRCoreSwiftNameSupport.h" -#import "FIRLoggerLevel.h" - -/** - * The log levels used by FIRConfiguration. - */ -typedef NS_ENUM(NSInteger, FIRLogLevel) { - /** Error */ - kFIRLogLevelError __deprecated = 0, - /** Warning */ - kFIRLogLevelWarning __deprecated, - /** Info */ - kFIRLogLevelInfo __deprecated, - /** Debug */ - kFIRLogLevelDebug __deprecated, - /** Assert */ - kFIRLogLevelAssert __deprecated, - /** Max */ - kFIRLogLevelMax __deprecated = kFIRLogLevelAssert -} DEPRECATED_MSG_ATTRIBUTE( - "Use -FIRDebugEnabled and -FIRDebugDisabled or setLoggerLevel. See FIRApp.h for more details."); - -NS_ASSUME_NONNULL_BEGIN - -/** - * This interface provides global level properties that the developer can tweak, and the singleton - * of the Firebase Analytics configuration class. - */ -FIR_SWIFT_NAME(FirebaseConfiguration) -@interface FIRConfiguration : NSObject - -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 -/** Returns the shared configuration object. */ -@property(class, nonatomic, readonly) FIRConfiguration *sharedInstance FIR_SWIFT_NAME(shared); -#else -/** Returns the shared configuration object. */ -+ (FIRConfiguration *)sharedInstance FIR_SWIFT_NAME(shared()); -#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - -/** The configuration class for Firebase Analytics. */ -@property(nonatomic, readwrite) FIRAnalyticsConfiguration *analyticsConfiguration; - -/** Global log level. Defaults to kFIRLogLevelError. */ -@property(nonatomic, readwrite, assign) FIRLogLevel logLevel DEPRECATED_MSG_ATTRIBUTE( - "Use -FIRDebugEnabled and -FIRDebugDisabled or setLoggerLevel. See FIRApp.h for more details."); - -/** - * Sets the logging level for internal Firebase logging. Firebase will only log messages - * that are logged at or below loggerLevel. The messages are logged both to the Xcode - * console and to the device's log. Note that if an app is running from AppStore, it will - * never log above FIRLoggerLevelNotice even if loggerLevel is set to a higher (more verbose) - * setting. - * - * @param loggerLevel The maximum logging level. The default level is set to FIRLoggerLevelNotice. - */ -- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/FirebaseCore.framework/Headers/FIRCoreSwiftNameSupport.h b/ios/FirebaseCore.framework/Headers/FIRCoreSwiftNameSupport.h deleted file mode 100755 index 216c047c0e5..00000000000 --- a/ios/FirebaseCore.framework/Headers/FIRCoreSwiftNameSupport.h +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef FIR_SWIFT_NAME - -#import - -// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. -// Wrap it in our own macro if it's a non-compatible SDK. -#ifdef __IPHONE_9_3 -#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) -#else -#define FIR_SWIFT_NAME(X) // Intentionally blank. -#endif // #ifdef __IPHONE_9_3 - -#endif // FIR_SWIFT_NAME diff --git a/ios/FirebaseCore.framework/Headers/FIRLoggerLevel.h b/ios/FirebaseCore.framework/Headers/FIRLoggerLevel.h deleted file mode 100755 index 70e99eea487..00000000000 --- a/ios/FirebaseCore.framework/Headers/FIRLoggerLevel.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "FIRCoreSwiftNameSupport.h" - -/** - * The log levels used by internal logging. - */ -typedef NS_ENUM(NSInteger, FIRLoggerLevel) { - /** Error level, matches ASL_LEVEL_ERR. */ - FIRLoggerLevelError = 3, - /** Warning level, matches ASL_LEVEL_WARNING. */ - FIRLoggerLevelWarning = 4, - /** Notice level, matches ASL_LEVEL_NOTICE. */ - FIRLoggerLevelNotice = 5, - /** Info level, matches ASL_LEVEL_NOTICE. */ - FIRLoggerLevelInfo = 6, - /** Debug level, matches ASL_LEVEL_DEBUG. */ - FIRLoggerLevelDebug = 7, - /** Minimum log level. */ - FIRLoggerLevelMin = FIRLoggerLevelError, - /** Maximum log level. */ - FIRLoggerLevelMax = FIRLoggerLevelDebug -} FIR_SWIFT_NAME(FirebaseLoggerLevel); diff --git a/ios/FirebaseCore.framework/Headers/FIROptions.h b/ios/FirebaseCore.framework/Headers/FIROptions.h deleted file mode 100755 index e1d114ae574..00000000000 --- a/ios/FirebaseCore.framework/Headers/FIROptions.h +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -#import "FIRCoreSwiftNameSupport.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * This class provides constant fields of Google APIs. - */ -FIR_SWIFT_NAME(FirebaseOptions) -@interface FIROptions : NSObject - -/** - * Returns the default options. - */ -+ (nullable FIROptions *)defaultOptions FIR_SWIFT_NAME(defaultOptions()); - -/** - * An iOS API key used for authenticating requests from your app, e.g. - * @"AIzaSyDdVgKwhZl0sTTTLZ7iTmt1r3N2cJLnaDk", used to identify your app to Google servers. - */ -@property(nonatomic, copy, nullable) NSString *APIKey FIR_SWIFT_NAME(apiKey); - -/** - * The bundle ID for the application. Defaults to `[[NSBundle mainBundle] bundleID]` when not set - * manually or in a plist. - */ -@property(nonatomic, copy) NSString *bundleID; - -/** - * The OAuth2 client ID for iOS application used to authenticate Google users, for example - * @"12345.apps.googleusercontent.com", used for signing in with Google. - */ -@property(nonatomic, copy, nullable) NSString *clientID; - -/** - * The tracking ID for Google Analytics, e.g. @"UA-12345678-1", used to configure Google Analytics. - */ -@property(nonatomic, copy, nullable) NSString *trackingID; - -/** - * The Project Number from the Google Developer's console, for example @"012345678901", used to - * configure Google Cloud Messaging. - */ -@property(nonatomic, copy) NSString *GCMSenderID FIR_SWIFT_NAME(gcmSenderID); - -/** - * The Project ID from the Firebase console, for example @"abc-xyz-123". - */ -@property(nonatomic, copy, nullable) NSString *projectID; - -/** - * The Android client ID used in Google AppInvite when an iOS app has its Android version, for - * example @"12345.apps.googleusercontent.com". - */ -@property(nonatomic, copy, nullable) NSString *androidClientID; - -/** - * The Google App ID that is used to uniquely identify an instance of an app. - */ -@property(nonatomic, copy) NSString *googleAppID; - -/** - * The database root URL, e.g. @"http://abc-xyz-123.firebaseio.com". - */ -@property(nonatomic, copy, nullable) NSString *databaseURL; - -/** - * The URL scheme used to set up Durable Deep Link service. - */ -@property(nonatomic, copy, nullable) NSString *deepLinkURLScheme; - -/** - * The Google Cloud Storage bucket name, e.g. @"abc-xyz-123.storage.firebase.com". - */ -@property(nonatomic, copy, nullable) NSString *storageBucket; - -/** - * Initializes a customized instance of FIROptions with keys. googleAppID, bundleID and GCMSenderID - * are required. Other keys may required for configuring specific services. - */ -- (instancetype)initWithGoogleAppID:(NSString *)googleAppID - bundleID:(NSString *)bundleID - GCMSenderID:(NSString *)GCMSenderID - APIKey:(NSString *)APIKey - clientID:(NSString *)clientID - trackingID:(NSString *)trackingID - androidClientID:(NSString *)androidClientID - databaseURL:(NSString *)databaseURL - storageBucket:(NSString *)storageBucket - deepLinkURLScheme:(NSString *)deepLinkURLScheme - DEPRECATED_MSG_ATTRIBUTE( - "Use `-[[FIROptions alloc] initWithGoogleAppID:GCMSenderID:]` " - "(`FirebaseOptions(googleAppID:gcmSenderID:)` in Swift)` and property " - "setters instead."); - -/** - * Initializes a customized instance of FIROptions from the file at the given plist file path. - * For example, - * NSString *filePath = - * [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"]; - * FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:filePath]; - * Returns nil if the plist file does not exist or is invalid. - */ -- (nullable instancetype)initWithContentsOfFile:(NSString *)plistPath; - -/** - * Initializes a customized instance of FIROptions with required fields. Use the mutable properties - * to modify fields for configuring specific services. - */ -// clang-format off -- (instancetype)initWithGoogleAppID:(NSString *)googleAppID - GCMSenderID:(NSString *)GCMSenderID - FIR_SWIFT_NAME(init(googleAppID:gcmSenderID:)); -// clang-format on - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/FirebaseCore.framework/Headers/FirebaseCore.h b/ios/FirebaseCore.framework/Headers/FirebaseCore.h deleted file mode 100755 index 20816292eb5..00000000000 --- a/ios/FirebaseCore.framework/Headers/FirebaseCore.h +++ /dev/null @@ -1,6 +0,0 @@ -#import "FIRAnalyticsConfiguration.h" -#import "FIRApp.h" -#import "FIRConfiguration.h" -#import "FIRCoreSwiftNameSupport.h" -#import "FIRLoggerLevel.h" -#import "FIROptions.h" diff --git a/ios/FirebaseCore.framework/Modules/module.modulemap b/ios/FirebaseCore.framework/Modules/module.modulemap deleted file mode 100755 index 2f85309a9a9..00000000000 --- a/ios/FirebaseCore.framework/Modules/module.modulemap +++ /dev/null @@ -1,5 +0,0 @@ -framework module FirebaseCore { - umbrella header "FirebaseCore.h" - export * - module * { export *} - link "z"} diff --git a/ios/FirebaseCoreDiagnostics.framework/FirebaseCoreDiagnostics b/ios/FirebaseCoreDiagnostics.framework/FirebaseCoreDiagnostics deleted file mode 100755 index c51fd37f51b..00000000000 Binary files a/ios/FirebaseCoreDiagnostics.framework/FirebaseCoreDiagnostics and /dev/null differ diff --git a/ios/FirebaseCoreDiagnostics.framework/Modules/module.modulemap b/ios/FirebaseCoreDiagnostics.framework/Modules/module.modulemap deleted file mode 100644 index 5bd208fe7a6..00000000000 --- a/ios/FirebaseCoreDiagnostics.framework/Modules/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -framework module FirebaseCoreDiagnostics { - export * - module * { export *} - link "z"} diff --git a/ios/FirebaseInstanceID.framework/FirebaseInstanceID b/ios/FirebaseInstanceID.framework/FirebaseInstanceID deleted file mode 100755 index 14e65bea079..00000000000 Binary files a/ios/FirebaseInstanceID.framework/FirebaseInstanceID and /dev/null differ diff --git a/ios/FirebaseInstanceID.framework/Headers/FIRInstanceID.h b/ios/FirebaseInstanceID.framework/Headers/FIRInstanceID.h deleted file mode 100755 index 680125df4ad..00000000000 --- a/ios/FirebaseInstanceID.framework/Headers/FIRInstanceID.h +++ /dev/null @@ -1,281 +0,0 @@ -#import - -// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. -// Wrap it in our own macro if it's a non-compatible SDK. -#ifndef FIR_SWIFT_NAME -#ifdef __IPHONE_9_3 -#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) -#else -#define FIR_SWIFT_NAME(X) // Intentionally blank. -#endif // #ifdef __IPHONE_9_3 -#endif // #ifndef FIR_SWIFT_NAME - -/** - * @memberof FIRInstanceID - * - * The scope to be used when fetching/deleting a token for Firebase Messaging. - */ -FOUNDATION_EXPORT NSString * __nonnull const kFIRInstanceIDScopeFirebaseMessaging - FIR_SWIFT_NAME(InstanceIDScopeFirebaseMessaging); - -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 -/** - * Called when the system determines that tokens need to be refreshed. - * This method is also called if Instance ID has been reset in which - * case, tokens and FCM topic subscriptions also need to be refreshed. - * - * Instance ID service will throttle the refresh event across all devices - * to control the rate of token updates on application servers. - */ -FOUNDATION_EXPORT const NSNotificationName __nonnull kFIRInstanceIDTokenRefreshNotification - FIR_SWIFT_NAME(InstanceIDTokenRefresh); -#else -/** - * Called when the system determines that tokens need to be refreshed. - * This method is also called if Instance ID has been reset in which - * case, tokens and FCM topic subscriptions also need to be refreshed. - * - * Instance ID service will throttle the refresh event across all devices - * to control the rate of token updates on application servers. - */ -FOUNDATION_EXPORT NSString * __nonnull const kFIRInstanceIDTokenRefreshNotification - FIR_SWIFT_NAME(InstanceIDTokenRefreshNotification); -#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - -/** - * @related FIRInstanceID - * - * The completion handler invoked when the InstanceID token returns. If - * the call fails we return the appropriate `error code` as described below. - * - * @param token The valid token as returned by InstanceID backend. - * - * @param error The error describing why generating a new token - * failed. See the error codes below for a more detailed - * description. - */ -typedef void(^FIRInstanceIDTokenHandler)( NSString * __nullable token, NSError * __nullable error) - FIR_SWIFT_NAME(InstanceIDTokenHandler); - - -/** - * @related FIRInstanceID - * - * The completion handler invoked when the InstanceID `deleteToken` returns. If - * the call fails we return the appropriate `error code` as described below - * - * @param error The error describing why deleting the token failed. - * See the error codes below for a more detailed description. - */ -typedef void(^FIRInstanceIDDeleteTokenHandler)(NSError * __nullable error) - FIR_SWIFT_NAME(InstanceIDDeleteTokenHandler); - -/** - * @related FIRInstanceID - * - * The completion handler invoked when the app identity is created. If the - * identity wasn't created for some reason we return the appropriate error code. - * - * @param identity A valid identity for the app instance, nil if there was an error - * while creating an identity. - * @param error The error if fetching the identity fails else nil. - */ -typedef void(^FIRInstanceIDHandler)(NSString * __nullable identity, NSError * __nullable error) - FIR_SWIFT_NAME(InstanceIDHandler); - -/** - * @related FIRInstanceID - * - * The completion handler invoked when the app identity and all the tokens associated - * with it are deleted. Returns a valid error object in case of failure else nil. - * - * @param error The error if deleting the identity and all the tokens associated with - * it fails else nil. - */ -typedef void(^FIRInstanceIDDeleteHandler)(NSError * __nullable error) - FIR_SWIFT_NAME(InstanceIDDeleteHandler); - -/** - * @enum FIRInstanceIDError - */ -typedef NS_ENUM(NSUInteger, FIRInstanceIDError) { - // Http related errors. - - /// Unknown error. - FIRInstanceIDErrorUnknown = 0, - - /// Auth Error -- GCM couldn't validate request from this client. - FIRInstanceIDErrorAuthentication = 1, - - /// NoAccess -- InstanceID service cannot be accessed. - FIRInstanceIDErrorNoAccess = 2, - - /// Timeout -- Request to InstanceID backend timed out. - FIRInstanceIDErrorTimeout = 3, - - /// Network -- No network available to reach the servers. - FIRInstanceIDErrorNetwork = 4, - - /// OperationInProgress -- Another similar operation in progress, - /// bailing this one. - FIRInstanceIDErrorOperationInProgress = 5, - - /// InvalidRequest -- Some parameters of the request were invalid. - FIRInstanceIDErrorInvalidRequest = 7, -} FIR_SWIFT_NAME(InstanceIDError); - -/** - * The APNS token type for the app. If the token type is set to `UNKNOWN` - * InstanceID will implicitly try to figure out what the actual token type - * is from the provisioning profile. - */ -typedef NS_ENUM(NSInteger, FIRInstanceIDAPNSTokenType) { - /// Unknown token type. - FIRInstanceIDAPNSTokenTypeUnknown, - /// Sandbox token type. - FIRInstanceIDAPNSTokenTypeSandbox, - /// Production token type. - FIRInstanceIDAPNSTokenTypeProd, -} FIR_SWIFT_NAME(InstanceIDAPNSTokenType) - __deprecated_enum_msg("Use FIRMessaging's APNSToken property instead."); - -/** - * Instance ID provides a unique identifier for each app instance and a mechanism - * to authenticate and authorize actions (for example, sending an FCM message). - * - * Instance ID is long lived but, may be reset if the device is not used for - * a long time or the Instance ID service detects a problem. - * If Instance ID is reset, the app will be notified via - * `kFIRInstanceIDTokenRefreshNotification`. - * - * If the Instance ID has become invalid, the app can request a new one and - * send it to the app server. - * To prove ownership of Instance ID and to allow servers to access data or - * services associated with the app, call - * `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. - */ -FIR_SWIFT_NAME(InstanceID) -@interface FIRInstanceID : NSObject - -/** - * FIRInstanceID. - * - * @return A shared instance of FIRInstanceID. - */ -+ (nonnull instancetype)instanceID FIR_SWIFT_NAME(instanceID()); - -/** - * Unavailable. Use +instanceID instead. - */ -- (nonnull instancetype)init __attribute__((unavailable("Use +instanceID instead."))); - -/** - * Set APNS token for the application. This APNS token will be used to register - * with Firebase Messaging using `token` or - * `tokenWithAuthorizedEntity:scope:options:handler`. If the token type is set to - * `FIRInstanceIDAPNSTokenTypeUnknown` InstanceID will read the provisioning profile - * to find out the token type. - * - * @param token The APNS token for the application. - * @param type The APNS token type for the above token. - */ -- (void)setAPNSToken:(nonnull NSData *)token - type:(FIRInstanceIDAPNSTokenType)type - __deprecated_msg("Use FIRMessaging's APNSToken property instead."); - -#pragma mark - Tokens - -/** - * Returns a Firebase Messaging scoped token for the firebase app. - * - * @return Null Returns null if the device has not yet been registerd with - * Firebase Message else returns a valid token. - */ -- (nullable NSString *)token; - -/** - * Returns a token that authorizes an Entity (example: cloud service) to perform - * an action on behalf of the application identified by Instance ID. - * - * This is similar to an OAuth2 token except, it applies to the - * application instance instead of a user. - * - * This is an asynchronous call. If the token fetching fails for some reason - * we invoke the completion callback with nil `token` and the appropriate - * error. - * - * Note, you can only have one `token` or `deleteToken` call for a given - * authorizedEntity and scope at any point of time. Making another such call with the - * same authorizedEntity and scope before the last one finishes will result in an - * error with code `OperationInProgress`. - * - * @see FIRInstanceID deleteTokenWithAuthorizedEntity:scope:handler: - * - * @param authorizedEntity Entity authorized by the token. - * @param scope Action authorized for authorizedEntity. - * @param options The extra options to be sent with your token request. The - * value for the `apns_token` should be the NSData object - * passed to the UIApplicationDelegate's - * `didRegisterForRemoteNotificationsWithDeviceToken` method. - * The value for `apns_sandbox` should be a boolean (or an - * NSNumber representing a BOOL in Objective C) set to true if - * your app is a debug build, which means that the APNs - * device token is for the sandbox environment. It should be - * set to false otherwise. - * @param handler The callback handler which is invoked when the token is - * successfully fetched. In case of success a valid `token` and - * `nil` error are returned. In case of any error the `token` - * is nil and a valid `error` is returned. The valid error - * codes have been documented above. - */ -- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity - scope:(nonnull NSString *)scope - options:(nullable NSDictionary *)options - handler:(nonnull FIRInstanceIDTokenHandler)handler; - -/** - * Revokes access to a scope (action) for an entity previously - * authorized by `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. - * - * This is an asynchronous call. Call this on the main thread since InstanceID lib - * is not thread safe. In case token deletion fails for some reason we invoke the - * `handler` callback passed in with the appropriate error code. - * - * Note, you can only have one `token` or `deleteToken` call for a given - * authorizedEntity and scope at a point of time. Making another such call with the - * same authorizedEntity and scope before the last one finishes will result in an error - * with code `OperationInProgress`. - * - * @param authorizedEntity Entity that must no longer have access. - * @param scope Action that entity is no longer authorized to perform. - * @param handler The handler that is invoked once the unsubscribe call ends. - * In case of error an appropriate error object is returned - * else error is nil. - */ -- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity - scope:(nonnull NSString *)scope - handler:(nonnull FIRInstanceIDDeleteTokenHandler)handler; - -#pragma mark - Identity - -/** - * Asynchronously fetch a stable identifier that uniquely identifies the app - * instance. If the identifier has been revoked or has expired, this method will - * return a new identifier. - * - * - * @param handler The handler to invoke once the identifier has been fetched. - * In case of error an appropriate error object is returned else - * a valid identifier is returned and a valid identifier for the - * application instance. - */ -- (void)getIDWithHandler:(nonnull FIRInstanceIDHandler)handler - FIR_SWIFT_NAME(getID(handler:)); - -/** - * Resets Instance ID and revokes all tokens. - */ -- (void)deleteIDWithHandler:(nonnull FIRInstanceIDDeleteHandler)handler - FIR_SWIFT_NAME(deleteID(handler:)); - -@end diff --git a/ios/FirebaseInstanceID.framework/Headers/FirebaseInstanceID.h b/ios/FirebaseInstanceID.framework/Headers/FirebaseInstanceID.h deleted file mode 100755 index 053ec2b1cf3..00000000000 --- a/ios/FirebaseInstanceID.framework/Headers/FirebaseInstanceID.h +++ /dev/null @@ -1 +0,0 @@ -#import "FIRInstanceID.h" diff --git a/ios/FirebaseInstanceID.framework/Modules/module.modulemap b/ios/FirebaseInstanceID.framework/Modules/module.modulemap deleted file mode 100755 index 7344d9bdd85..00000000000 --- a/ios/FirebaseInstanceID.framework/Modules/module.modulemap +++ /dev/null @@ -1,5 +0,0 @@ -framework module FirebaseInstanceID { - umbrella header "FirebaseInstanceID.h" - export * - module * { export *} - link "z"} diff --git a/ios/FirebaseMessaging.framework/FirebaseMessaging b/ios/FirebaseMessaging.framework/FirebaseMessaging deleted file mode 100755 index 44c6424debf..00000000000 Binary files a/ios/FirebaseMessaging.framework/FirebaseMessaging and /dev/null differ diff --git a/ios/FirebaseMessaging.framework/Headers/FIRMessaging.h b/ios/FirebaseMessaging.framework/Headers/FIRMessaging.h deleted file mode 100755 index 7313f2339ab..00000000000 --- a/ios/FirebaseMessaging.framework/Headers/FIRMessaging.h +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - - -// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. -// Wrap it in our own macro if it's a non-compatible SDK. -#ifndef FIR_SWIFT_NAME -#ifdef __IPHONE_9_3 -#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) -#else -#define FIR_SWIFT_NAME(X) // Intentionally blank. -#endif // #ifdef __IPHONE_9_3 -#endif // #ifndef FIR_SWIFT_NAME - -/** - * @related FIRMessaging - * - * The completion handler invoked when the registration token returns. - * If the call fails we return the appropriate `error code`, described by - * `FIRMessagingError`. - * - * @param FCMToken The valid registration token returned by FCM. - * @param error The error describing why a token request failed. The error code - * will match a value from the FIRMessagingError enumeration. - */ -typedef void(^FIRMessagingFCMTokenFetchCompletion)(NSString * _Nullable FCMToken, - NSError * _Nullable error) - FIR_SWIFT_NAME(MessagingFCMTokenFetchCompletion); - - -/** - * @related FIRMessaging - * - * The completion handler invoked when the registration token deletion request is - * completed. If the call fails we return the appropriate `error code`, described - * by `FIRMessagingError`. - * - * @param error The error describing why a token deletion failed. The error code - * will match a value from the FIRMessagingError enumeration. - */ -typedef void(^FIRMessagingDeleteFCMTokenCompletion)(NSError * _Nullable error) - FIR_SWIFT_NAME(MessagingDeleteFCMTokenCompletion); - -/** - * The completion handler invoked once the data connection with FIRMessaging is - * established. The data connection is used to send a continous stream of - * data and all the FIRMessaging data notifications arrive through this connection. - * Once the connection is established we invoke the callback with `nil` error. - * Correspondingly if we get an error while trying to establish a connection - * we invoke the handler with an appropriate error object and do an - * exponential backoff to try and connect again unless successful. - * - * @param error The error object if any describing why the data connection - * to FIRMessaging failed. - */ -typedef void(^FIRMessagingConnectCompletion)(NSError * __nullable error) - FIR_SWIFT_NAME(MessagingConnectCompletion) - __deprecated_msg("Please listen for the FIRMessagingConnectionStateChangedNotification " - "NSNotification instead."); - -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 -/** - * Notification sent when the upstream message has been delivered - * successfully to the server. The notification object will be the messageID - * of the successfully delivered message. - */ -FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendSuccessNotification - FIR_SWIFT_NAME(MessagingSendSuccess); - -/** - * Notification sent when the upstream message was failed to be sent to the - * server. The notification object will be the messageID of the failed - * message. The userInfo dictionary will contain the relevant error - * information for the failure. - */ -FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendErrorNotification - FIR_SWIFT_NAME(MessagingSendError); - -/** - * Notification sent when the Firebase messaging server deletes pending - * messages due to exceeded storage limits. This may occur, for example, when - * the device cannot be reached for an extended period of time. - * - * It is recommended to retrieve any missing messages directly from the - * server. - */ -FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingMessagesDeletedNotification - FIR_SWIFT_NAME(MessagingMessagesDeleted); - -/** - * Notification sent when Firebase Messaging establishes or disconnects from - * an FCM socket connection. You can query the connection state in this - * notification by checking the `isDirectChannelEstablished` property of FIRMessaging. - */ -FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingConnectionStateChangedNotification - FIR_SWIFT_NAME(MessagingConnectionStateChanged); - -/** - * Notification sent when the FCM registration token has been refreshed. You can also - * receive the FCM token via the FIRMessagingDelegate method - * `-messaging:didRefreshRegistrationToken:` - */ -FOUNDATION_EXPORT const NSNotificationName __nonnull - FIRMessagingRegistrationTokenRefreshedNotification - FIR_SWIFT_NAME(MessagingRegistrationTokenRefreshed); -#else -/** - * Notification sent when the upstream message has been delivered - * successfully to the server. The notification object will be the messageID - * of the successfully delivered message. - */ -FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendSuccessNotification - FIR_SWIFT_NAME(MessagingSendSuccessNotification); - -/** - * Notification sent when the upstream message was failed to be sent to the - * server. The notification object will be the messageID of the failed - * message. The userInfo dictionary will contain the relevant error - * information for the failure. - */ -FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendErrorNotification - FIR_SWIFT_NAME(MessagingSendErrorNotification); - -/** - * Notification sent when the Firebase messaging server deletes pending - * messages due to exceeded storage limits. This may occur, for example, when - * the device cannot be reached for an extended period of time. - * - * It is recommended to retrieve any missing messages directly from the - * server. - */ -FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingMessagesDeletedNotification - FIR_SWIFT_NAME(MessagingMessagesDeletedNotification); - -/** - * Notification sent when Firebase Messaging establishes or disconnects from - * an FCM socket connection. You can query the connection state in this - * notification by checking the `isDirectChannelEstablished` property of FIRMessaging. - */ -FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingConnectionStateChangedNotification - FIR_SWIFT_NAME(MessagingConnectionStateChangedNotification); - -/** - * Notification sent when the FCM registration token has been refreshed. You can also - * receive the FCM token via the FIRMessagingDelegate method - * `-messaging:didRefreshRegistrationToken:` - */ -FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingRegistrationTokenRefreshedNotification - FIR_SWIFT_NAME(MessagingRegistrationTokenRefreshedNotification); -#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - -/** - * @enum FIRMessagingError - */ -typedef NS_ENUM(NSUInteger, FIRMessagingError) { - /// Unknown error. - FIRMessagingErrorUnknown = 0, - - /// FIRMessaging couldn't validate request from this client. - FIRMessagingErrorAuthentication = 1, - - /// InstanceID service cannot be accessed. - FIRMessagingErrorNoAccess = 2, - - /// Request to InstanceID backend timed out. - FIRMessagingErrorTimeout = 3, - - /// No network available to reach the servers. - FIRMessagingErrorNetwork = 4, - - /// Another similar operation in progress, bailing this one. - FIRMessagingErrorOperationInProgress = 5, - - /// Some parameters of the request were invalid. - FIRMessagingErrorInvalidRequest = 7, -} FIR_SWIFT_NAME(MessagingError); - -/// Status for the downstream message received by the app. -typedef NS_ENUM(NSInteger, FIRMessagingMessageStatus) { - /// Unknown status. - FIRMessagingMessageStatusUnknown, - /// New downstream message received by the app. - FIRMessagingMessageStatusNew, -} FIR_SWIFT_NAME(MessagingMessageStatus); - -/** - * The APNS token type for the app. If the token type is set to `UNKNOWN` - * Firebase Messaging will implicitly try to figure out what the actual token type - * is from the provisioning profile. - * Unless you really need to specify the type, you should use the `APNSToken` - * property instead. - */ -typedef NS_ENUM(NSInteger, FIRMessagingAPNSTokenType) { - /// Unknown token type. - FIRMessagingAPNSTokenTypeUnknown, - /// Sandbox token type. - FIRMessagingAPNSTokenTypeSandbox, - /// Production token type. - FIRMessagingAPNSTokenTypeProd, -} FIR_SWIFT_NAME(MessagingAPNSTokenType); - -/// Information about a downstream message received by the app. -FIR_SWIFT_NAME(MessagingMessageInfo) -@interface FIRMessagingMessageInfo : NSObject - -/// The status of the downstream message -@property(nonatomic, readonly, assign) FIRMessagingMessageStatus status; - -@end - -/** - * A remote data message received by the app via FCM (not just the APNs interface). - * - * This is only for devices running iOS 10 or above. To support devices running iOS 9 or below, use - * the local and remote notifications handlers defined in UIApplicationDelegate protocol. - */ -FIR_SWIFT_NAME(MessagingRemoteMessage) -@interface FIRMessagingRemoteMessage : NSObject - -/// The downstream message received by the application. -@property(nonatomic, readonly, strong, nonnull) NSDictionary *appData; -@end - -@class FIRMessaging; -/** - * A protocol to handle events from FCM for devices running iOS 10 or above. - * - * To support devices running iOS 9 or below, use the local and remote notifications handlers - * defined in UIApplicationDelegate protocol. - */ -FIR_SWIFT_NAME(MessagingDelegate) -@protocol FIRMessagingDelegate - -/// This method will be called whenever FCM receives a new, default FCM token for your -/// Firebase project's Sender ID. -/// You can send this token to your application server to send notifications to this device. -- (void)messaging:(nonnull FIRMessaging *)messaging - didRefreshRegistrationToken:(nonnull NSString *)fcmToken - FIR_SWIFT_NAME(messaging(_:didRefreshRegistrationToken:)); - -@optional -/// This method is called on iOS 10 devices to handle data messages received via FCM through its -/// direct channel (not via APNS). For iOS 9 and below, the FCM data message is delivered via the -/// UIApplicationDelegate's -application:didReceiveRemoteNotification: method. -- (void)messaging:(nonnull FIRMessaging *)messaging - didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage - FIR_SWIFT_NAME(messaging(_:didReceive:)) - __IOS_AVAILABLE(10.0); - -/// The callback to handle data message received via FCM for devices running iOS 10 or above. -- (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage - FIR_SWIFT_NAME(application(received:)) - __deprecated_msg("Use FIRMessagingDelegate’s -messaging:didReceiveMessage:"); - -@end - -/** - * Firebase Messaging lets you reliably deliver messages at no cost. - * - * To send or receive messages, the app must get a - * registration token from FIRInstanceID. This token authorizes an - * app server to send messages to an app instance. - * - * In order to receive FIRMessaging messages, declare `application:didReceiveRemoteNotification:`. - */ -FIR_SWIFT_NAME(Messaging) -@interface FIRMessaging : NSObject - -/** - * Delegate to handle FCM token refreshes, and remote data messages received via FCM for devices - * running iOS 10 or above. - */ -@property(nonatomic, weak, nullable) id delegate; - - -/** - * Delegate to handle remote data messages received via FCM for devices running iOS 10 or above. - */ -@property(nonatomic, weak, nullable) id remoteMessageDelegate - __deprecated_msg("Use 'delegate' property"); - -/** - * When set to `YES`, Firebase Messaging will automatically establish a socket-based, direct - * channel to the FCM server. Enable this only if you are sending upstream messages or - * receiving non-APNS, data-only messages in foregrounded apps. - * Default is `NO`. - */ -@property(nonatomic) BOOL shouldEstablishDirectChannel; - -/** - * Returns `YES` if the direct channel to the FCM server is active, and `NO` otherwise. - */ -@property(nonatomic, readonly) BOOL isDirectChannelEstablished; - -/** - * FIRMessaging - * - * @return An instance of FIRMessaging. - */ -+ (nonnull instancetype)messaging FIR_SWIFT_NAME(messaging()); - -/** - * Unavailable. Use +messaging instead. - */ -- (nonnull instancetype)init __attribute__((unavailable("Use +messaging instead."))); - -#pragma mark - APNS - -/** - * This property is used to set the APNS Token received by the application delegate. - * - * FIRMessaging uses method swizzling to ensure that the APNS token is set - * automatically. However, if you have disabled swizzling by setting - * `FirebaseAppDelegateProxyEnabled` to `NO` in your app's - * Info.plist, you should manually set the APNS token in your application - * delegate's `-application:didRegisterForRemoteNotificationsWithDeviceToken:` - * method. - * - * If you would like to set the type of the APNS token, rather than relying on - * automatic detection, see: `-setAPNSToken:type:`. - */ -@property(nonatomic, copy, nullable) NSData *APNSToken FIR_SWIFT_NAME(apnsToken); - -/** - * Set APNS token for the application. This APNS token will be used to register - * with Firebase Messaging using `FCMToken` or - * `tokenWithAuthorizedEntity:scope:options:handler`. - * - * @param apnsToken The APNS token for the application. - * @param type The type of APNS token. Debug builds should use - * FIRMessagingAPNSTokenTypeSandbox. Alternatively, you can supply - * FIRMessagingAPNSTokenTypeUnknown to have the type automatically - * detected based on your provisioning profile. - */ -- (void)setAPNSToken:(nonnull NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type; - -#pragma mark - FCM Tokens - -/** - * The FCM token is used to identify this device so that FCM can send notifications to it. - * It is associated with your APNS token when the APNS token is supplied, so that sending - * messages to the FCM token will be delivered over APNS. - * - * The FCM token is sometimes refreshed automatically. You can be notified of these changes - * via the FIRMessagingDelegate method `-message:didRefreshRegistrationToken:`, or by - * listening for the `FIRMessagingRegistrationTokenRefreshedNotification` notification. - * - * Once you have an FCM token, you should send it to your application server, so it can use - * the FCM token to send notifications to your device. - */ -@property(nonatomic, readonly, nullable) NSString *FCMToken FIR_SWIFT_NAME(fcmToken); - - -/** - * Retrieves an FCM registration token for a particular Sender ID. This can be used to allow - * multiple senders to send notifications to the same device. By providing a different Sender - * ID than your default when fetching a token, you can create a new FCM token which you can - * give to a different sender. Both tokens will deliver notifications to your device, and you - * can revoke a token when you need to. - * - * This registration token is not cached by FIRMessaging. FIRMessaging should have an APNS - * token set before calling this to ensure that notifications can be delivered via APNS using - * this FCM token. You may re-retrieve the FCM token once you have the APNS token set, to - * associate it with the FCM token. The default FCM token is automatically associated with - * the APNS token, if the APNS token data is available. - * - * @param senderID The Sender ID for a particular Firebase project. - * @param completion The completion handler to handle the token request. - */ -- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID - completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion - FIR_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:)); - - -/** - * Invalidates an FCM token for a particular Sender ID. That Sender ID cannot no longer send - * notifications to that FCM token. - * - * @param senderID The senderID for a particular Firebase project. - * @param completion The completion handler to handle the token deletion. - */ -- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID - completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion - FIR_SWIFT_NAME(deleteFCMToken(forSenderID:completion:)); - - -#pragma mark - Connect - -/** - * Create a FIRMessaging data connection which will be used to send the data notifications - * sent by your server. It will also be used to send ACKS and other messages based - * on the FIRMessaging ACKS and other messages based on the FIRMessaging protocol. - * - * - * @param handler The handler to be invoked once the connection is established. - * If the connection fails we invoke the handler with an - * appropriate error code letting you know why it failed. At - * the same time, FIRMessaging performs exponential backoff to retry - * establishing a connection and invoke the handler when successful. - */ -- (void)connectWithCompletion:(nonnull FIRMessagingConnectCompletion)handler - FIR_SWIFT_NAME(connect(handler:)) - __deprecated_msg("Please use the shouldEstablishDirectChannel property instead."); - -/** - * Disconnect the current FIRMessaging data connection. This stops any attempts to - * connect to FIRMessaging. Calling this on an already disconnected client is a no-op. - * - * Call this before `teardown` when your app is going to the background. - * Since the FIRMessaging connection won't be allowed to live when in the background, it is - * prudent to close the connection. - */ -- (void)disconnect - __deprecated_msg("Please use the shouldEstablishDirectChannel property instead."); - -#pragma mark - Topics - -/** - * Asynchronously subscribes to a topic. - * - * @param topic The name of the topic, for example, @"sports". - */ -- (void)subscribeToTopic:(nonnull NSString *)topic FIR_SWIFT_NAME(subscribe(toTopic:)); - -/** - * Asynchronously unsubscribe from a topic. - * - * @param topic The name of the topic, for example @"sports". - */ -- (void)unsubscribeFromTopic:(nonnull NSString *)topic FIR_SWIFT_NAME(unsubscribe(fromTopic:)); - -#pragma mark - Upstream - -/** - * Sends an upstream ("device to cloud") message. - * - * The message is queued if we don't have an active connection. - * You can only use the upstream feature if your FCM implementation - * uses the XMPP server protocol. - * - * @param message Key/Value pairs to be sent. Values must be String, any - * other type will be ignored. - * @param receiver A string identifying the receiver of the message. For FCM - * project IDs the value is `SENDER_ID@gcm.googleapis.com`. - * @param messageID The ID of the message. This is generated by the application. It - * must be unique for each message generated by this application. - * It allows error callbacks and debugging, to uniquely identify - * each message. - * @param ttl The time to live for the message. In case we aren't able to - * send the message before the TTL expires we will send you a - * callback. If 0, we'll attempt to send immediately and return - * an error if we're not connected. Otherwise, the message will - * be queued. As for server-side messages, we don't return an error - * if the message has been dropped because of TTL; this can happen - * on the server side, and it would require extra communication. - */ -- (void)sendMessage:(nonnull NSDictionary *)message - to:(nonnull NSString *)receiver - withMessageID:(nonnull NSString *)messageID - timeToLive:(int64_t)ttl; - -#pragma mark - Analytics - -/** - * Use this to track message delivery and analytics for messages, typically - * when you receive a notification in `application:didReceiveRemoteNotification:`. - * However, you only need to call this if you set the `FirebaseAppDelegateProxyEnabled` - * flag to `NO` in your Info.plist. If `FirebaseAppDelegateProxyEnabled` is either missing - * or set to `YES` in your Info.plist, the library will call this automatically. - * - * @param message The downstream message received by the application. - * - * @return Information about the downstream message. - */ -- (nonnull FIRMessagingMessageInfo *)appDidReceiveMessage:(nonnull NSDictionary *)message; - -@end diff --git a/ios/FirebaseMessaging.framework/Headers/FirebaseMessaging.h b/ios/FirebaseMessaging.framework/Headers/FirebaseMessaging.h deleted file mode 100755 index ef49e7ff7a8..00000000000 --- a/ios/FirebaseMessaging.framework/Headers/FirebaseMessaging.h +++ /dev/null @@ -1 +0,0 @@ -#import "FIRMessaging.h" diff --git a/ios/FirebaseMessaging.framework/Modules/module.modulemap b/ios/FirebaseMessaging.framework/Modules/module.modulemap deleted file mode 100755 index 7fd2aab4287..00000000000 --- a/ios/FirebaseMessaging.framework/Modules/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module FirebaseMessaging { - umbrella header "FirebaseMessaging.h" - export * - module * { export *} - link "sqlite3" - link "z"} diff --git a/ios/FirebaseNanoPB.framework/FirebaseNanoPB b/ios/FirebaseNanoPB.framework/FirebaseNanoPB deleted file mode 100755 index ff9bef2d0f7..00000000000 Binary files a/ios/FirebaseNanoPB.framework/FirebaseNanoPB and /dev/null differ diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist index b5744a35f77..19ddb96668f 100644 --- a/ios/GoogleService-Info.plist +++ b/ios/GoogleService-Info.plist @@ -2,38 +2,32 @@ - AD_UNIT_ID_FOR_BANNER_TEST - ca-app-pub-3940256099942544/2934735716 - AD_UNIT_ID_FOR_INTERSTITIAL_TEST - ca-app-pub-3940256099942544/4411468910 CLIENT_ID 854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com REVERSED_CLIENT_ID com.googleusercontent.apps.854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o API_KEY - AIzaSyCwlaE5cnsT4rmGyOFxE39FlPVyKQ_2Mdo + AIzaSyB_ZCi76uSX1RBqGiLIllUnJ8D6_cKrRGQ GCM_SENDER_ID 854811651919 PLIST_VERSION 1 BUNDLE_ID - $(PRODUCT_BUNDLE_IDENTIFIER)$(BUNDLE_ID_SUFFIX) + im.status.ethereum PROJECT_ID status-react-app STORAGE_BUCKET status-react-app.appspot.com IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - - FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID 1:854811651919:ios:11ee7444ded8a00a DATABASE_URL diff --git a/ios/Podfile b/ios/Podfile index 6e2524763f0..fbbc60dc0ad 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,39 +1,108 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -# Need to explicitly declare this avoid downgrade -# https://github.com/evollu/react-native-fcm/issues/420 -#pod 'Firebase/Core', '4.0.0' - -# react-native-fcm should support FB 4, but latest breaks builds, hence older version -# TODO(oskarth): Awaiting RN 0.47 upgrade, and/or PN working on iOS device before deciding which version to use -#pod 'Firebase/Messaging' -#pod 'Firebase/Messaging', '3.17.0' - -target 'StatusIm' do - platform :ios, '8.0' - # Uncomment the next line if you're using Swift or would like to use dynamic frameworks - # use_frameworks! - - # Pods for StatusIm - pod 'FirebaseMessaging' - - pod 'React', :path => '../node_modules/react-native' - pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga' - pod 'react-native-background-timer', :path => '../node_modules/react-native-background-timer' - pod 'RNKeychain', :path => '../node_modules/react-native-keychain' - - target 'StatusImTests' do - inherit! :search_paths - # Pods for testing - end +def node_require(script) + # Resolve script with node to allow for hoisting + require Pod::Executable.execute_command('node', ['-p', + "require.resolve( + '#{script}', + {paths: [process.argv[1]]}, + )", __dir__]).strip +end + +node_require('react-native/scripts/react_native_pods.rb') +node_require('react-native-permissions/scripts/setup.rb') +platform :ios, min_ios_version_supported +prepare_react_native_project! + +setup_permissions([ + 'Camera', + 'FaceID', + 'MediaLibrary', + 'Microphone', + 'Notifications', + 'PhotoLibrary', + 'PhotoLibraryAddOnly', +]) + +linkage = ENV['USE_FRAMEWORKS'] +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym end -post_install do |installer| - installer.pods_project.targets.each do |target| - if target.name == "React" - target.remove_from_project +# If inside a Nix shell, allow pod install +inside_nix_shell = ENV['IN_NIX_SHELL'] +allow_pod_install = !inside_nix_shell.nil? + +unless allow_pod_install + puts "ERROR: 'pod install' must be run inside a Nix shell. Please ensure you're inside a nix shell and try again." + exit 1 +end + +abstract_target 'Status' do + # Pods for StatusQuo + config = use_native_modules! + + # Flags change depending on the env values. + flags = get_default_flags() + + use_react_native!( + :path => config[:reactNativePath], + :fabric_enabled => flags[:fabric_enabled], + :hermes_enabled => true, + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) + + pod 'react-native-image-resizer', :path => '../node_modules/react-native-image-resizer' + pod 'react-native-config', :path => '../node_modules/react-native-config' + + pod 'SSZipArchive', '2.4.3' + # to fix [!] [!] CocoaPods could not find compatible versions for pod "libwebp" + pod 'libwebp', '1.0.0' + # to fix [!] CocoaPods could not find compatible versions for pod "CryptoSwift" + pod 'CryptoSwift', git: "https://github.com/krzyzanowskim/CryptoSwift", tag: '1.8.0' + pod "react-native-status-keycard", path: "../node_modules/react-native-status-keycard" + pod "react-native-status", path: "../modules/react-native-status" + pod "Keycard", git: "https://github.com/status-im/Keycard.swift.git", tag: '3.1.1' + pod 'secp256k1', git: "https://github.com/status-im/secp256k1.swift.git", submodules: true + + pod 'FirebaseCore', :modular_headers => true + pod 'GoogleUtilities', :modular_headers => true + + target 'StatusIm' do + + target 'StatusImTests' do + inherit! :complete + # Pods for testing + end + end + + target 'StatusImPR' do + end + + post_install do |installer| + # as per react-native 0.73.2 template + # https://github.com/facebook/react-native/blob/c5c0563f209ead8f884204763e02115bd66d1960/packages/react-native/template/ios/Podfile#L49-L52 + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + ) + # some of libs wouldn't be build for x86_64 otherwise and that is + # necessary for ios simulators + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ONLY_ACTIVE_ARCH'] = 'NO' + end + # We need to remove the predefinition of IPHONEOS_DEPLOYMENT_TARGET from RNCK lib to use our own. + case target.name + when 'ReactNativeCameraKit' + target.build_configurations.each do |config| + config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' + end + end end end + + use_native_modules! end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0535b77ebbb..373780e1749 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,72 +1,1744 @@ PODS: - - FirebaseAnalytics (4.0.1): - - FirebaseCore (~> 4.0) - - FirebaseInstanceID (~> 2.0) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - FirebaseCore (4.0.2): - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - FirebaseInstanceID (2.0.0): - - FirebaseCore (~> 4.0) - - FirebaseMessaging (2.0.0): - - FirebaseAnalytics (~> 4.0) - - FirebaseCore (~> 4.0) - - FirebaseInstanceID (~> 2.0) - - GoogleToolboxForMac/Logger (~> 2.1) - - Protobuf (~> 3.1) - - GoogleToolboxForMac/Defines (2.1.1) - - GoogleToolboxForMac/Logger (2.1.1): - - GoogleToolboxForMac/Defines (= 2.1.1) - - "GoogleToolboxForMac/NSData+zlib (2.1.1)": - - GoogleToolboxForMac/Defines (= 2.1.1) - - Protobuf (3.3.0) - - React (0.55.4): - - React/Core (= 0.55.4) - - react-native-background-timer (2.0.0): + - BigInt (5.2.0) + - boost (1.83.0) + - CryptoSwift (1.8.0) + - DoubleConversion (1.1.6) + - FBLazyVector (0.73.5) + - FBReactNativeSpec (0.73.5): + - RCT-Folly (= 2022.05.16.00) + - RCTRequired (= 0.73.5) + - RCTTypeSafety (= 0.73.5) + - React-Core (= 0.73.5) + - React-jsi (= 0.73.5) + - ReactCommon/turbomodule/core (= 0.73.5) + - Firebase/CoreOnly (11.7.0): + - FirebaseCore (~> 11.7.0) + - Firebase/Messaging (11.7.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 11.7.0) + - FirebaseCore (11.7.0): + - FirebaseCoreInternal (~> 11.7.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.7.0): + - FirebaseCore (~> 11.7.0) + - FirebaseCoreInternal (11.7.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.7.0): + - FirebaseCore (~> 11.7.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.7.0): + - FirebaseCore (~> 11.7.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) + - fmt (6.2.1) + - glog (0.3.5) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities (8.0.2): + - GoogleUtilities/AppDelegateSwizzler (= 8.0.2) + - GoogleUtilities/Environment (= 8.0.2) + - GoogleUtilities/Logger (= 8.0.2) + - GoogleUtilities/MethodSwizzler (= 8.0.2) + - GoogleUtilities/Network (= 8.0.2) + - "GoogleUtilities/NSData+zlib (= 8.0.2)" + - GoogleUtilities/Privacy (= 8.0.2) + - GoogleUtilities/Reachability (= 8.0.2) + - GoogleUtilities/SwizzlerTestHelpers (= 8.0.2) + - GoogleUtilities/UserDefaults (= 8.0.2) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/SwizzlerTestHelpers (8.0.2): + - GoogleUtilities/MethodSwizzler + - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - hermes-engine (0.73.5): + - hermes-engine/Pre-built (= 0.73.5) + - hermes-engine/Pre-built (0.73.5) + - HMSegmentedControl (1.5.6) + - Keycard (3.1.0): + - BigInt + - CryptoSwift + - secp256k1 + - SSZipArchive + - libevent (2.1.12) + - libwebp (1.0.0): + - libwebp/core (= 1.0.0) + - libwebp/dec (= 1.0.0) + - libwebp/demux (= 1.0.0) + - libwebp/dsp (= 1.0.0) + - libwebp/enc (= 1.0.0) + - libwebp/mux (= 1.0.0) + - libwebp/utils (= 1.0.0) + - libwebp/webp (= 1.0.0) + - libwebp/core (1.0.0): + - libwebp/webp + - libwebp/dec (1.0.0): + - libwebp/core + - libwebp/demux (1.0.0): + - libwebp/core + - libwebp/dsp (1.0.0): + - libwebp/core + - libwebp/enc (1.0.0): + - libwebp/core + - libwebp/mux (1.0.0): + - libwebp/core + - libwebp/utils (1.0.0): + - libwebp/core + - libwebp/webp (1.0.0) + - MMKV (2.1.0): + - MMKVCore (~> 2.1.0) + - MMKVCore (2.1.1) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - PromisesObjC (2.4.0) + - RCT-Folly (2022.05.16.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Default (= 2022.05.16.00) + - RCT-Folly/Default (2022.05.16.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Fabric (2022.05.16.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Futures (2022.05.16.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - libevent + - RCTRequired (0.73.5) + - RCTTypeSafety (0.73.5): + - FBLazyVector (= 0.73.5) + - RCTRequired (= 0.73.5) + - React-Core (= 0.73.5) + - React (0.73.5): + - React-Core (= 0.73.5) + - React-Core/DevSupport (= 0.73.5) + - React-Core/RCTWebSocket (= 0.73.5) + - React-RCTActionSheet (= 0.73.5) + - React-RCTAnimation (= 0.73.5) + - React-RCTBlob (= 0.73.5) + - React-RCTImage (= 0.73.5) + - React-RCTLinking (= 0.73.5) + - React-RCTNetwork (= 0.73.5) + - React-RCTSettings (= 0.73.5) + - React-RCTText (= 0.73.5) + - React-RCTVibration (= 0.73.5) + - React-callinvoker (0.73.5) + - React-Codegen (0.73.5): + - DoubleConversion + - FBReactNativeSpec + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-rncore + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-Core (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default (= 0.73.5) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/CoreModulesHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/Default (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/DevSupport (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default (= 0.73.5) + - React-Core/RCTWebSocket (= 0.73.5) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector (= 0.73.5) + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTActionSheetHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTAnimationHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTBlobHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTImageHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTLinkingHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTNetworkHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTSettingsHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTTextHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTVibrationHeaders (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTWebSocket (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Core/Default (= 0.73.5) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-CoreModules (0.73.5): + - RCT-Folly (= 2022.05.16.00) + - RCTTypeSafety (= 0.73.5) + - React-Codegen + - React-Core/CoreModulesHeaders (= 0.73.5) + - React-jsi (= 0.73.5) + - React-NativeModulesApple + - React-RCTBlob + - React-RCTImage (= 0.73.5) + - ReactCommon + - SocketRocket (= 0.6.1) + - React-cxxreact (0.73.5): + - boost (= 1.83.0) + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-callinvoker (= 0.73.5) + - React-debug (= 0.73.5) + - React-jsi (= 0.73.5) + - React-jsinspector (= 0.73.5) + - React-logger (= 0.73.5) + - React-perflogger (= 0.73.5) + - React-runtimeexecutor (= 0.73.5) + - React-debug (0.73.5) + - React-Fabric (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/animations (= 0.73.5) + - React-Fabric/attributedstring (= 0.73.5) + - React-Fabric/componentregistry (= 0.73.5) + - React-Fabric/componentregistrynative (= 0.73.5) + - React-Fabric/components (= 0.73.5) + - React-Fabric/core (= 0.73.5) + - React-Fabric/imagemanager (= 0.73.5) + - React-Fabric/leakchecker (= 0.73.5) + - React-Fabric/mounting (= 0.73.5) + - React-Fabric/scheduler (= 0.73.5) + - React-Fabric/telemetry (= 0.73.5) + - React-Fabric/templateprocessor (= 0.73.5) + - React-Fabric/textlayoutmanager (= 0.73.5) + - React-Fabric/uimanager (= 0.73.5) + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/animations (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/attributedstring (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistry (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistrynative (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/components/inputaccessory (= 0.73.5) + - React-Fabric/components/legacyviewmanagerinterop (= 0.73.5) + - React-Fabric/components/modal (= 0.73.5) + - React-Fabric/components/rncore (= 0.73.5) + - React-Fabric/components/root (= 0.73.5) + - React-Fabric/components/safeareaview (= 0.73.5) + - React-Fabric/components/scrollview (= 0.73.5) + - React-Fabric/components/text (= 0.73.5) + - React-Fabric/components/textinput (= 0.73.5) + - React-Fabric/components/unimplementedview (= 0.73.5) + - React-Fabric/components/view (= 0.73.5) + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/inputaccessory (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/legacyviewmanagerinterop (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/modal (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/rncore (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/root (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/safeareaview (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/scrollview (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/text (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/textinput (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/unimplementedview (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/view (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric/core (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/imagemanager (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/leakchecker (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/mounting (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/scheduler (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/telemetry (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/templateprocessor (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/textlayoutmanager (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/uimanager + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-FabricImage (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - RCTRequired (= 0.73.5) + - RCTTypeSafety (= 0.73.5) + - React-Fabric + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.73.5) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - Yoga + - React-graphics (0.73.5): + - glog + - RCT-Folly/Fabric (= 2022.05.16.00) + - React-Core/Default (= 0.73.5) + - React-utils + - React-hermes (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCT-Folly/Futures (= 2022.05.16.00) + - React-cxxreact (= 0.73.5) + - React-jsi + - React-jsiexecutor (= 0.73.5) + - React-jsinspector (= 0.73.5) + - React-perflogger (= 0.73.5) + - React-ImageManager (0.73.5): + - glog + - RCT-Folly/Fabric + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - React-jserrorhandler (0.73.5): + - RCT-Folly/Fabric (= 2022.05.16.00) + - React-debug + - React-jsi + - React-Mapbuffer + - React-jsi (0.73.5): + - boost (= 1.83.0) + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-jsiexecutor (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-cxxreact (= 0.73.5) + - React-jsi (= 0.73.5) + - React-perflogger (= 0.73.5) + - React-jsinspector (0.73.5) + - React-logger (0.73.5): + - glog + - React-Mapbuffer (0.73.5): + - glog + - React-debug + - react-native-background-timer (2.2.0): - React - - React/Core (0.55.4): - - yoga (= 0.55.4.React) - - RNKeychain (3.0.0-rc.3): + - react-native-biometrics (3.0.1): + - React-Core + - react-native-blob-util (0.13.18): + - React-Core + - react-native-blur (4.4.1): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - react-native-cameraroll (7.5.2): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - react-native-compat (2.17.3): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - react-native-config (1.5.0): + - react-native-config/App (= 1.5.0) + - react-native-config/App (1.5.0): + - React-Core + - react-native-get-random-values (1.11.0): + - React-Core + - react-native-hole-view (3.0.0-alpha4): + - React-Core + - react-native-image-resizer (1.2.3): - React - - yoga (0.55.4.React) + - react-native-lottie-splash-screen (1.1.2): + - React + - react-native-mmkv (2.12.2): + - glog + - MMKV (>= 1.3.3) + - RCT-Folly (= 2022.05.16.00) + - React-Core + - react-native-netinfo (4.7.0): + - React + - react-native-orientation-locker (1.5.0): + - React-Core + - react-native-pdf (6.7.5): + - React-Core + - react-native-safe-area-context (4.14.1): + - React-Core + - react-native-shake (3.4.0): + - React + - react-native-slider (3.0.0): + - React + - react-native-status (1.0.0): + - React + - react-native-status-keycard (2.6.6): + - Keycard + - React + - react-native-webview (13.6.3): + - React-Core + - React-nativeconfig (0.73.5) + - React-NativeModulesApple (0.73.5): + - glog + - hermes-engine + - React-callinvoker + - React-Core + - React-cxxreact + - React-jsi + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-perflogger (0.73.5) + - React-RCTActionSheet (0.73.5): + - React-Core/RCTActionSheetHeaders (= 0.73.5) + - React-RCTAnimation (0.73.5): + - RCT-Folly (= 2022.05.16.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTAnimationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-RCTAppDelegate (0.73.5): + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-CoreModules + - React-hermes + - React-nativeconfig + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-RCTNetwork + - React-runtimescheduler + - ReactCommon + - React-RCTBlob (0.73.5): + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-Codegen + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-NativeModulesApple + - React-RCTNetwork + - ReactCommon + - React-RCTFabric (0.73.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2022.05.16.00) + - React-Core + - React-debug + - React-Fabric + - React-FabricImage + - React-graphics + - React-ImageManager + - React-jsi + - React-nativeconfig + - React-RCTImage + - React-RCTText + - React-rendererdebug + - React-runtimescheduler + - React-utils + - Yoga + - React-RCTImage (0.73.5): + - RCT-Folly (= 2022.05.16.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTNetwork + - ReactCommon + - React-RCTLinking (0.73.5): + - React-Codegen + - React-Core/RCTLinkingHeaders (= 0.73.5) + - React-jsi (= 0.73.5) + - React-NativeModulesApple + - ReactCommon + - ReactCommon/turbomodule/core (= 0.73.5) + - React-RCTNetwork (0.73.5): + - RCT-Folly (= 2022.05.16.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTNetworkHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-RCTSettings (0.73.5): + - RCT-Folly (= 2022.05.16.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-RCTText (0.73.5): + - React-Core/RCTTextHeaders (= 0.73.5) + - Yoga + - React-RCTVibration (0.73.5): + - RCT-Folly (= 2022.05.16.00) + - React-Codegen + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-rendererdebug (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - RCT-Folly (= 2022.05.16.00) + - React-debug + - React-rncore (0.73.5) + - React-runtimeexecutor (0.73.5): + - React-jsi (= 0.73.5) + - React-runtimescheduler (0.73.5): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-callinvoker + - React-cxxreact + - React-debug + - React-jsi + - React-rendererdebug + - React-runtimeexecutor + - React-utils + - React-utils (0.73.5): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-debug + - ReactCommon (0.73.5): + - React-logger (= 0.73.5) + - ReactCommon/turbomodule (= 0.73.5) + - ReactCommon/turbomodule (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-callinvoker (= 0.73.5) + - React-cxxreact (= 0.73.5) + - React-jsi (= 0.73.5) + - React-logger (= 0.73.5) + - React-perflogger (= 0.73.5) + - ReactCommon/turbomodule/bridging (= 0.73.5) + - ReactCommon/turbomodule/core (= 0.73.5) + - ReactCommon/turbomodule/bridging (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-callinvoker (= 0.73.5) + - React-cxxreact (= 0.73.5) + - React-jsi (= 0.73.5) + - React-logger (= 0.73.5) + - React-perflogger (= 0.73.5) + - ReactCommon/turbomodule/core (0.73.5): + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - React-callinvoker (= 0.73.5) + - React-cxxreact (= 0.73.5) + - React-jsi (= 0.73.5) + - React-logger (= 0.73.5) + - React-perflogger (= 0.73.5) + - ReactNativeAudioToolkit (2.0.3): + - React + - ReactNativeCameraKit (14.0.0-beta15): + - React-Core + - ReactNativeNavigation (7.43.0): + - HMSegmentedControl + - React-Core + - React-CoreModules + - React-RCTImage + - React-RCTText + - ReactNativeNavigation/Core (= 7.43.0) + - ReactNativeNavigation/Core (7.43.0): + - HMSegmentedControl + - React-Core + - React-CoreModules + - React-RCTImage + - React-RCTText + - RNCAsyncStorage (1.19.3): + - React-Core + - RNCClipboard (1.13.2): + - React-Core + - RNCMaskedView (0.1.9): + - React + - RNCPushNotificationIOS (1.4.1): + - React + - RNFastImage (8.5.11): + - React-Core + - SDWebImage (~> 5.11.1) + - SDWebImageWebPCoder (~> 0.8.4) + - RNFBApp (21.7.2): + - Firebase/CoreOnly (= 11.7.0) + - React-Core + - RNFBMessaging (21.7.2): + - Firebase/Messaging (= 11.7.0) + - FirebaseCoreExtension + - React-Core + - RNFBApp + - RNFS (2.16.6): + - React + - RNGestureHandler (2.14.1): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - RNImageCropPicker (0.41.2): + - React-Core + - React-RCTImage + - RNImageCropPicker/QBImagePickerController (= 0.41.2) + - TOCropViewController (~> 2.7.4) + - RNImageCropPicker/QBImagePickerController (0.41.2): + - React-Core + - React-RCTImage + - TOCropViewController (~> 2.7.4) + - RNKeychain (8.1.2): + - React-Core + - RNLinearGradient (3.0.0-alpha.1): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - RNPermissions (4.1.5): + - React-Core + - RNReanimated (3.6.1): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - ReactCommon/turbomodule/core + - RNShare (10.0.2): + - React-Core + - RNSVG (13.10.0): + - React-Core + - SDWebImage (5.11.1): + - SDWebImage/Core (= 5.11.1) + - SDWebImage/Core (5.11.1) + - SDWebImageWebPCoder (0.8.5): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.10) + - secp256k1 (0.1.6) + - SocketRocket (0.6.1) + - SSZipArchive (2.4.3) + - TOCropViewController (2.7.4) + - Yoga (1.14.0) DEPENDENCIES: - - FirebaseMessaging - - React (from `../node_modules/react-native`) + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - CryptoSwift (from `https://github.com/krzyzanowskim/CryptoSwift`, tag `1.8.0`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) + - FirebaseCore + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - GoogleUtilities + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - Keycard (from `https://github.com/status-im/Keycard.swift.git`, tag `3.1.1`) + - libevent (~> 2.1.12) + - libwebp (= 1.0.0) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Codegen (from `build/generated/ios`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - react-native-background-timer (from `../node_modules/react-native-background-timer`) + - react-native-biometrics (from `../node_modules/react-native-biometrics`) + - react-native-blob-util (from `../node_modules/react-native-blob-util`) + - "react-native-blur (from `../node_modules/@react-native-community/blur`)" + - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" + - "react-native-compat (from `../node_modules/@walletconnect/react-native-compat`)" + - react-native-config (from `../node_modules/react-native-config`) + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - react-native-hole-view (from `../node_modules/react-native-hole-view`) + - react-native-image-resizer (from `../node_modules/react-native-image-resizer`) + - react-native-lottie-splash-screen (from `../node_modules/react-native-lottie-splash-screen`) + - react-native-mmkv (from `../node_modules/react-native-mmkv`) + - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" + - react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`) + - react-native-pdf (from `../node_modules/react-native-pdf`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-shake (from `../node_modules/react-native-shake`) + - "react-native-slider (from `../node_modules/@react-native-community/slider`)" + - react-native-status (from `../modules/react-native-status`) + - react-native-status-keycard (from `../node_modules/react-native-status-keycard`) + - react-native-webview (from `../node_modules/react-native-webview`) + - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-rncore (from `../node_modules/react-native/ReactCommon`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "ReactNativeAudioToolkit (from `../node_modules/@react-native-community/audio-toolkit`)" + - ReactNativeCameraKit (from `../node_modules/react-native-camera-kit`) + - ReactNativeNavigation (from `../node_modules/react-native-navigation`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" + - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" + - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" + - RNFastImage (from `../node_modules/react-native-fast-image`) + - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" + - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" + - RNFS (from `../node_modules/react-native-fs`) + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNKeychain (from `../node_modules/react-native-keychain`) - - yoga (from `../node_modules/react-native/ReactCommon/yoga`) + - RNLinearGradient (from `../node_modules/react-native-linear-gradient`) + - RNPermissions (from `../node_modules/react-native-permissions`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNShare (from `../node_modules/react-native-share`) + - RNSVG (from `../node_modules/react-native-svg`) + - secp256k1 (from `https://github.com/status-im/secp256k1.swift.git`) + - SSZipArchive (= 2.4.3) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: - https://github.com/cocoapods/specs.git: - - FirebaseAnalytics + trunk: + - BigInt + - Firebase - FirebaseCore - - FirebaseInstanceID + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseInstallations - FirebaseMessaging - - GoogleToolboxForMac - - Protobuf + - fmt + - GoogleDataTransport + - GoogleUtilities + - HMSegmentedControl + - libevent + - libwebp + - MMKV + - MMKVCore + - nanopb + - PromisesObjC + - SDWebImage + - SDWebImageWebPCoder + - SocketRocket + - SSZipArchive + - TOCropViewController EXTERNAL SOURCES: + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + CryptoSwift: + :git: https://github.com/krzyzanowskim/CryptoSwift + :tag: 1.8.0 + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + FBReactNativeSpec: + :path: "../node_modules/react-native/React/FBReactNativeSpec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-2024-02-20-RNv0.73.5-18f99ace4213052c5e7cdbcd39ee9766cd5df7e4 + Keycard: + :git: https://github.com/status-im/Keycard.swift.git + :tag: 3.1.1 + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTRequired: + :path: "../node_modules/react-native/Libraries/RCTRequired" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" React: - :path: "../node_modules/react-native" + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Codegen: + :path: build/generated/ios + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" react-native-background-timer: :path: "../node_modules/react-native-background-timer" + react-native-biometrics: + :path: "../node_modules/react-native-biometrics" + react-native-blob-util: + :path: "../node_modules/react-native-blob-util" + react-native-blur: + :path: "../node_modules/@react-native-community/blur" + react-native-cameraroll: + :path: "../node_modules/@react-native-camera-roll/camera-roll" + react-native-compat: + :path: "../node_modules/@walletconnect/react-native-compat" + react-native-config: + :path: "../node_modules/react-native-config" + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" + react-native-hole-view: + :path: "../node_modules/react-native-hole-view" + react-native-image-resizer: + :path: "../node_modules/react-native-image-resizer" + react-native-lottie-splash-screen: + :path: "../node_modules/react-native-lottie-splash-screen" + react-native-mmkv: + :path: "../node_modules/react-native-mmkv" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" + react-native-orientation-locker: + :path: "../node_modules/react-native-orientation-locker" + react-native-pdf: + :path: "../node_modules/react-native-pdf" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + react-native-shake: + :path: "../node_modules/react-native-shake" + react-native-slider: + :path: "../node_modules/@react-native-community/slider" + react-native-status: + :path: "../modules/react-native-status" + react-native-status-keycard: + :path: "../node_modules/react-native-status-keycard" + react-native-webview: + :path: "../node_modules/react-native-webview" + React-nativeconfig: + :path: "../node_modules/react-native/ReactCommon" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-rncore: + :path: "../node_modules/react-native/ReactCommon" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + ReactNativeAudioToolkit: + :path: "../node_modules/@react-native-community/audio-toolkit" + ReactNativeCameraKit: + :path: "../node_modules/react-native-camera-kit" + ReactNativeNavigation: + :path: "../node_modules/react-native-navigation" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" + RNCClipboard: + :path: "../node_modules/@react-native-clipboard/clipboard" + RNCMaskedView: + :path: "../node_modules/@react-native-community/masked-view" + RNCPushNotificationIOS: + :path: "../node_modules/@react-native-community/push-notification-ios" + RNFastImage: + :path: "../node_modules/react-native-fast-image" + RNFBApp: + :path: "../node_modules/@react-native-firebase/app" + RNFBMessaging: + :path: "../node_modules/@react-native-firebase/messaging" + RNFS: + :path: "../node_modules/react-native-fs" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" + RNImageCropPicker: + :path: "../node_modules/react-native-image-crop-picker" RNKeychain: :path: "../node_modules/react-native-keychain" - yoga: + RNLinearGradient: + :path: "../node_modules/react-native-linear-gradient" + RNPermissions: + :path: "../node_modules/react-native-permissions" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNShare: + :path: "../node_modules/react-native-share" + RNSVG: + :path: "../node_modules/react-native-svg" + secp256k1: + :git: https://github.com/status-im/secp256k1.swift.git + :submodules: true + Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + CryptoSwift: + :git: https://github.com/krzyzanowskim/CryptoSwift + :tag: 1.8.0 + Keycard: + :git: https://github.com/status-im/Keycard.swift.git + :tag: 3.1.1 + secp256k1: + :commit: 4ab977cc2b2d7319be858bcb30a5d189bb149884 + :git: https://github.com/status-im/secp256k1.swift.git + :submodules: true + SPEC CHECKSUMS: - FirebaseAnalytics: 4d7040fefc3cd8b291cde35f12cf063d7963f15d - FirebaseCore: 1a8bf6c795ad07d2918278657954ffd2552e9c17 - FirebaseInstanceID: 9fbf536668f4d3f0880e7438456dabd1376e294b - FirebaseMessaging: 227406c05b0dc9290702d2e9f18ab5528f0c2cf2 - GoogleToolboxForMac: 8e329f1b599f2512c6b10676d45736bcc2cbbeb0 - Protobuf: d582fecf68201eac3d79ed61369ef45734394b9c - React: aa2040dbb6f317b95314968021bd2888816e03d5 - react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594 - RNKeychain: 627c6095cef215dd3d9804a9a9cf45ab96aa3997 - yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a + BigInt: f668a80089607f521586bbe29513d708491ef2f7 + boost: d3f49c53809116a5d38da093a8aa78bf551aed09 + CryptoSwift: 52aaf3fce7337552863b1d952e408085f0e65030 + DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 + FBLazyVector: 56e0e498dbb513b96c40bac6284729ba4e62672d + FBReactNativeSpec: 146c741a3f40361f6bc13a4ba284678cbedb5881 + Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4 + FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4 + FirebaseCoreExtension: 206c1b399f0d103055207c16f299b28e3dbd1949 + FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881 + FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9 + FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c + fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + glog: c0eca6ffed0a6fde1965ef26dcc7bf528f052af4 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + hermes-engine: 1d1835b2cc54c381909d94d1b3c8e0a2f1a94a0e + HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 + Keycard: 3bd24405b9a875b3b3cf1ab1be69b5c30c5f28bc + libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + libwebp: d7e0c95fe97245c97e08101eba10702ebb0f6101 + MMKV: ce484c1ac40bf76d5f09a0195d2ec5b3d3840d55 + MMKVCore: 1eb661c6c498ab88e3df9ce5d8ff94d05fcc0567 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 + RCTRequired: 2544c0f1081a5fa12e108bb8cb40e5f4581ccd87 + RCTTypeSafety: 50efabe2b115c11ed03fbf3fd79e2f163ddb5d7c + React: 84221d5e0ce297bc57c4b6af539a62d812d89f10 + React-callinvoker: 5d17577ecc7f784535ebedf3aad4bcbf8f4b5117 + React-Codegen: 857e7984fc277aadde2a7a427288b6918ece7b2b + React-Core: 8e782e7e24c7843871a0d9c3c8d7c5b3ebb73832 + React-CoreModules: 7875ee247e3e6e0e683b52cd1cdda1b71618bd55 + React-cxxreact: 788cd771c6e94d44f8d472fdfae89b67226067ea + React-debug: 55c7f2b8463bfe85567c9f4ede904085601130c9 + React-Fabric: 8cb43853496bb8032420edf62e7281c53109e682 + React-FabricImage: fbdc0ef7ed58a87c77600017c19a751932de3e47 + React-graphics: dc8307b615f14e13f1081ac23ea66697808bcd29 + React-hermes: d9acaa4ebf2118d9bd8a541af8c620c467b356b6 + React-ImageManager: 2a97ddc9b1f459121697d629cfbe69712997d76f + React-jserrorhandler: b97b16674258ccaeff5a70047a097a140e76d12d + React-jsi: 1d59d0a148c76641ac577729e0268bafa494152c + React-jsiexecutor: 262b66928ad948491d03fd328bb5b822cce94647 + React-jsinspector: 32db5e364bcae8fca8cdf8891830636275add0c5 + React-logger: 0331362115f0f5b392bd7ed14636d1a3ea612479 + React-Mapbuffer: 7c35cd53a22d0be04d3f26f7881c7fb7dd230216 + react-native-background-timer: 1f7d560647b40e6a60b01c452ba29c54bf581fc4 + react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc + react-native-blob-util: 600972b1782380a5a7d5db61a3817ea32349dae9 + react-native-blur: b2b140d3d65077a1d5d26be62d9415f895592344 + react-native-cameraroll: af8eec1e585d053ff485d98ec837f9a8a11b5745 + react-native-compat: fd639220cca206df6c74b95df56a1809d78ba5a3 + react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 + react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 + react-native-hole-view: 6935448993bac79f2b5a4ad7e9741094cf810679 + react-native-image-resizer: 2f1577efa3bc762597681f530c8e8d05ce0ceeb3 + react-native-lottie-splash-screen: 4e1b1fd9d6633f9cd2106d6877eb5ba0147f3e2b + react-native-mmkv: 1fdc81aa70c1aba09370718e6a63a09cbbbac8d2 + react-native-netinfo: ddaca8bbb9e6e914b1a23787ccb879bc642931c9 + react-native-orientation-locker: 851f6510d8046ea2f14aa169b1e01fcd309a94ba + react-native-pdf: 103940c90d62adfd259f63cca99c7c0c306b514c + react-native-safe-area-context: 141eca0fd4e4191288dfc8b96a7c7e1c2983447a + react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c + react-native-slider: 12bd76d3d568c9c5500825db54123d44b48e4ad4 + react-native-status: 21f75d492fd311dc111303da38a7a2b23a8a8466 + react-native-status-keycard: 2facee29633e4f754475731007915d76fda36e12 + react-native-webview: 88293a0f23eca8465c0433c023ec632930e644d0 + React-nativeconfig: 1166714a4f7ea57a0df5c2cb44fbc70f98d580f9 + React-NativeModulesApple: 726664e9829eb5eed8170241000e46ead269a05f + React-perflogger: 0dd9f1725d55f8264b81efadd373fe1d9cca7dc2 + React-RCTActionSheet: 05656d2102b0d0a2676d58bad4d80106af5367b2 + React-RCTAnimation: 6c66beae98730fb7615df28caf651e295f2401e5 + React-RCTAppDelegate: 891b80c596fffcb3f90431739495d606a9a0d610 + React-RCTBlob: 8ecee445ec5fa9ed8a8621a136183c1045165100 + React-RCTFabric: f291e06bc63fef26cdd105537bae5c6a8d3bdca8 + React-RCTImage: 585b16465146cb839da02f3179ce7cb19d332642 + React-RCTLinking: 09ba11f7df62946e7ddca1b51aa3bf47b230e008 + React-RCTNetwork: e070f8d2fca60f1e9571936ce54d165e77129e76 + React-RCTSettings: b08c7ff191f0a5421aab198ea1086c9a8d513bde + React-RCTText: f6cc5a3cf0f1a4f3d1256657dca1025e4cfe45e0 + React-RCTVibration: d9948962139f9924ef87f23ab240e045e496213b + React-rendererdebug: ee05480666415f7a76e6cf0a7a50363423f44809 + React-rncore: 010565651e9cf2e4fac9517a348446789dd55e01 + React-runtimeexecutor: 56f562a608056fb0c1711d900a992e26f375d817 + React-runtimescheduler: 814b644a5f456c7df1fba7bcd9914707152527c6 + React-utils: 987a4526a2fc0acdfaf87888adfe0bf9d0452066 + ReactCommon: 2947b0bffd82ea0e58ca7928881152d4c6dae9af + ReactNativeAudioToolkit: de9610f323e855ac6574be8c99621f3d57c5df06 + ReactNativeCameraKit: 71343efc1256720184ce980f164c7eedb78d5c16 + ReactNativeNavigation: 6ef5f2d34477e0c50e0969e4c6f88d2ad7e866ae + RNCAsyncStorage: c913ede1fa163a71cea118ed4670bbaaa4b511bb + RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d + RNCMaskedView: 71fc32d971f03b7f03d6ab6b86b730c4ee64f5b6 + RNCPushNotificationIOS: c145c6253ea016e5efeff604f2720736b4a596f7 + RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7 + RNFBApp: a5395f4df3ea5d19697bdc99f5a0400903e5fff6 + RNFBMessaging: 3a01ad804d6cef7266794b922545b03161672a27 + RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df + RNGestureHandler: 15c6ef51acba34c49ff03003806cf5dd6098f383 + RNImageCropPicker: 771e2ca319d2cf92e04ebf334ece892ee9a6728f + RNKeychain: a65256b6ca6ba6976132cc4124b238a5b13b3d9c + RNLinearGradient: ccaaebce083b54180da61d992e7fa56abfe000d6 + RNPermissions: 08e619529cced22695f4b6d0efcc0a233e278903 + RNReanimated: dee37576492f1a375017515f5c77e66e5eec696b + RNShare: 859ff710211285676b0bcedd156c12437ea1d564 + RNSVG: 80584470ff1ffc7994923ea135a3e5ad825546b9 + SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d + SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d + secp256k1: f61d67e6fdcb85fd727acf1bf35ace6036db540c + SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 + SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef + TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 + Yoga: a716eea57d0d3430219c0a5a233e1e93ee931eb7 -PODFILE CHECKSUM: 58fa7be75df3ece53c570dbd30d721f4b6d77137 +PODFILE CHECKSUM: 6eb8a78392f0dc3cb4232c5743a61ed9f17ba267 -COCOAPODS: 1.5.3 +COCOAPODS: 1.15.2 diff --git a/ios/RobotoMono-Medium.ttf b/ios/RobotoMono-Medium.ttf deleted file mode 100644 index 0bcdc740c66..00000000000 Binary files a/ios/RobotoMono-Medium.ttf and /dev/null differ diff --git a/ios/SF-UI-Display-Medium.otf b/ios/SF-UI-Display-Medium.otf deleted file mode 100644 index dc2df0571ef..00000000000 Binary files a/ios/SF-UI-Display-Medium.otf and /dev/null differ diff --git a/ios/SF-UI-Display-Regular.otf b/ios/SF-UI-Display-Regular.otf deleted file mode 100644 index 14c96528c4f..00000000000 Binary files a/ios/SF-UI-Display-Regular.otf and /dev/null differ diff --git a/ios/SF-UI-Display-Semibold.otf b/ios/SF-UI-Display-Semibold.otf deleted file mode 100644 index 4ea7ea73eca..00000000000 Binary files a/ios/SF-UI-Display-Semibold.otf and /dev/null differ diff --git a/ios/SF-UI-Display-Thin.otf b/ios/SF-UI-Display-Thin.otf deleted file mode 100644 index f02dcda5e43..00000000000 Binary files a/ios/SF-UI-Display-Thin.otf and /dev/null differ diff --git a/ios/SF-UI-Text-Bold.otf b/ios/SF-UI-Text-Bold.otf deleted file mode 100644 index 4e9fc0492a6..00000000000 Binary files a/ios/SF-UI-Text-Bold.otf and /dev/null differ diff --git a/ios/SF-UI-Text-Light.otf b/ios/SF-UI-Text-Light.otf deleted file mode 100644 index 313f439f2cb..00000000000 Binary files a/ios/SF-UI-Text-Light.otf and /dev/null differ diff --git a/ios/SF-UI-Text-Medium.otf b/ios/SF-UI-Text-Medium.otf deleted file mode 100644 index a6383853096..00000000000 Binary files a/ios/SF-UI-Text-Medium.otf and /dev/null differ diff --git a/ios/SF-UI-Text-Regular.otf b/ios/SF-UI-Text-Regular.otf deleted file mode 100644 index a36c8f62b0a..00000000000 Binary files a/ios/SF-UI-Text-Regular.otf and /dev/null differ diff --git a/ios/SF-UI-Text-Semibold.otf b/ios/SF-UI-Text-Semibold.otf deleted file mode 100644 index c11fa416de9..00000000000 Binary files a/ios/SF-UI-Text-Semibold.otf and /dev/null differ diff --git a/ios/StatusIm-Bridging-Header.h b/ios/StatusIm-Bridging-Header.h new file mode 100644 index 00000000000..1b2cb5d6d09 --- /dev/null +++ b/ios/StatusIm-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/ios/StatusIm.xcodeproj/project.pbxproj b/ios/StatusIm.xcodeproj/project.pbxproj index c83a02db282..9de5e1783df 100644 --- a/ios/StatusIm.xcodeproj/project.pbxproj +++ b/ios/StatusIm.xcodeproj/project.pbxproj @@ -3,115 +3,70 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; - 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; - 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; - 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; - 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; 00E356F31AD99517003FC87E /* StatusImTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* StatusImTests.m */; }; - 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; - 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; - 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; - 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; - 2028DFF91D4275B600227DCD /* SF-UI-Display-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 2028DFF51D4275B600227DCD /* SF-UI-Display-Medium.otf */; }; - 2028DFFA1D4275B600227DCD /* SF-UI-Display-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 2028DFF61D4275B600227DCD /* SF-UI-Display-Regular.otf */; }; - 2028DFFB1D4275B600227DCD /* SF-UI-Display-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 2028DFF71D4275B600227DCD /* SF-UI-Display-Semibold.otf */; }; - 2028DFFC1D4275B600227DCD /* SF-UI-Display-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = 2028DFF81D4275B600227DCD /* SF-UI-Display-Thin.otf */; }; - 20AB9EC61D47CC0300E7FD9C /* libRCTStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 201067C41D4789F700FA83B6 /* libRCTStatus.a */; }; - 20B6B6841D92C42600CC5C6A /* RSKImageCropper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20B6B6831D92C42600CC5C6A /* RSKImageCropper.framework */; }; - 20B6B6851D92C42600CC5C6A /* RSKImageCropper.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 20B6B6831D92C42600CC5C6A /* RSKImageCropper.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 20B6B6871D92C42600CC5C6A /* QBImagePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20B6B6861D92C42600CC5C6A /* QBImagePicker.framework */; }; - 20B6B6881D92C42600CC5C6A /* QBImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 20B6B6861D92C42600CC5C6A /* QBImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 213311F38CA74CE280FD09AD /* libRNI18n.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 52F6ED6465184513A082652B /* libRNI18n.a */; }; - 22118DE1207A419FBFE7B62D /* libRealmReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CD48A32459B64E96843BB238 /* libRealmReact.a */; }; 25DC9C9DC25846BD8D084888 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B9A886A2CB448B1ABA0EB62 /* libc++.tbd */; }; - 3A8BF31C42291E324219071C /* InstabugCore.framework in Embed Instabug Framework */ = {isa = PBXBuildFile; fileRef = EA36FD18BC2ECACF0A8B83DE /* InstabugCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 3E15DFEC1F6F4D7CAE088F49 /* libTcpSockets.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C2A4E93F6B154AEFA3485B45 /* libTcpSockets.a */; }; - 4C16DE0C1F89508700AA10DB /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C16DE0B1F89508700AA10DB /* JavaScriptCore.framework */; }; - 4CA4DA1B206D105D006A98B0 /* libRNInstabug.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E3F1C121DDAE781005E4779 /* libRNInstabug.a */; }; - 5974D2035B8B47E0946B63B6 /* libRNFIRMessaging.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F752F17B1E04216B1337A72 /* libRNFIRMessaging.a */; }; - 63AE4174241B852A045FAE1F /* InstabugCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA36FD18BC2ECACF0A8B83DE /* InstabugCore.framework */; }; + 2BE1270E2D59242300023E5E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2BE1270D2D59242300023E5E /* GoogleService-Info.plist */; }; + 2BE1270F2D59242300023E5E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2BE1270D2D59242300023E5E /* GoogleService-Info.plist */; }; + 2BE127102D59242300023E5E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2BE1270D2D59242300023E5E /* GoogleService-Info.plist */; }; + 2D32CCEDFF64E1C3F1FB79AF /* libPods-Status-StatusIm-StatusImTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6081A369F49E04EC54F77E1B /* libPods-Status-StatusIm-StatusImTests.a */; }; + 3870E1E692E24133A80B07DE /* Inter-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 693A62DB37BC4CD5A30E5C96 /* Inter-SemiBold.otf */; }; + 393D26E3080B443A998F4A2F /* Inter-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = B07176ACDAA1422E8F0A3D6B /* Inter-Italic.otf */; }; + 3A2626CF245C3F2200D5F94B /* Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2626CE245C3F2200D5F94B /* Dummy.swift */; }; + 3A8F8EAA24A4D31600BF206D /* GameKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A8F8EA924A4D31600BF206D /* GameKit.framework */; }; + 3AAD2ABC24A3A60E0075D594 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 3AAD2ABD24A3A60E0075D594 /* Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2626CE245C3F2200D5F94B /* Dummy.swift */; }; + 3AAD2ABE24A3A60E0075D594 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 3AAD2AC024A3A60E0075D594 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B24FC7FE1DE7195F00D694FF /* MessageUI.framework */; }; + 3AAD2AC124A3A60E0075D594 /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B24FC7FC1DE7195700D694FF /* Social.framework */; }; + 3AAD2AC224A3A60E0075D594 /* Statusgo.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4E31B21D8695250033ED64 /* Statusgo.xcframework */; }; + 3AAD2AC524A3A60E0075D594 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B9A886A2CB448B1ABA0EB62 /* libc++.tbd */; }; + 3AAD2AC624A3A60E0075D594 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E586E1B0E544F64AA9F5BD1 /* libz.tbd */; }; + 3AAD2ACA24A3A60E0075D594 /* launch-image-universal.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74B758FB20D7C00B003343C3 /* launch-image-universal.storyboard */; }; + 3AAD2ACB24A3A60E0075D594 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B2F2D1BB1D9D531B00B7B453 /* Images.xcassets */; }; + 3AAD2ACC24A3A60E0075D594 /* Inter-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CD4A2C27D6D5473184DC1F7E /* Inter-Bold.otf */; }; + 3AAD2ACD24A3A60E0075D594 /* Inter-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = B321D25F4493470980039457 /* Inter-BoldItalic.otf */; }; + 3AAD2ACE24A3A60E0075D594 /* InterStatus-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9C76AF5A418D4D65A4CAD1D9 /* InterStatus-Regular.otf */; }; + 3AAD2ACF24A3A60E0075D594 /* Inter-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = B07176ACDAA1422E8F0A3D6B /* Inter-Italic.otf */; }; + 3AAD2AD024A3A60E0075D594 /* Inter-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = B2A38FC3D3954DE7B2B171F8 /* Inter-Medium.otf */; }; + 3AAD2AD124A3A60E0075D594 /* Inter-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = C6B1215047604CD59A4C74D6 /* Inter-MediumItalic.otf */; }; + 3AAD2AD224A3A60E0075D594 /* Inter-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 1426DF592BA248FC81D955CB /* Inter-Regular.otf */; }; + 3AAD2AD324A3A60E0075D594 /* Inter-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 693A62DB37BC4CD5A30E5C96 /* Inter-SemiBold.otf */; }; + 3AAD2AD424A3A60E0075D594 /* Inter-SemiBoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = A4F2BBE8D4DD4140A6CCAC39 /* Inter-SemiBoldItalic.otf */; }; + 3ABC7AF8245FF85900612C45 /* InterStatus-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9C76AF5A418D4D65A4CAD1D9 /* InterStatus-Regular.otf */; }; + 57C854A7993C47A3B1AECD32 /* Inter-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = C6B1215047604CD59A4C74D6 /* Inter-MediumItalic.otf */; }; + 65F6941925780A4F00A45E76 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F6941825780A4F00A45E76 /* Bridge.swift */; }; + 65F6941A25780A4F00A45E76 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F6941825780A4F00A45E76 /* Bridge.swift */; }; + 65F6941B25780A4F00A45E76 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F6941825780A4F00A45E76 /* Bridge.swift */; }; + 70ADBB5ECF934DCF8A0E4919 /* Inter-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 1426DF592BA248FC81D955CB /* Inter-Regular.otf */; }; + 715D8132290BE850006F5C88 /* UbuntuMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 715D8131290BE850006F5C88 /* UbuntuMono-Regular.ttf */; }; + 715D8133290BE850006F5C88 /* UbuntuMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 715D8131290BE850006F5C88 /* UbuntuMono-Regular.ttf */; }; 74B758FC20D7C00B003343C3 /* launch-image-universal.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74B758FB20D7C00B003343C3 /* launch-image-universal.storyboard */; }; - 81C6E6AE0AA739BE9D87C1D0 /* libPods-StatusImTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FC1CBCFE6C906043D6CCEEE1 /* libPods-StatusImTests.a */; }; - 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; - 8E55E6877F950B81C8D711C5 /* libPods-StatusIm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 101A4045637A2ADF57D28EF5 /* libPods-StatusIm.a */; }; - 91446A820DA5E1C15C24D2A7 /* Instabug.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22D72275B7ADF9FC3304DCE5 /* Instabug.framework */; }; - 925C1F471F7B73B20063DFA0 /* FirebaseCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 925C1F401F7B73B20063DFA0 /* FirebaseCore.framework */; }; - 925C1F481F7B73B20063DFA0 /* FirebaseCoreDiagnostics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 925C1F411F7B73B20063DFA0 /* FirebaseCoreDiagnostics.framework */; }; - 925C1F491F7B73B20063DFA0 /* FirebaseInstanceID.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 925C1F421F7B73B20063DFA0 /* FirebaseInstanceID.framework */; }; - 925C1F4A1F7B73B20063DFA0 /* FirebaseNanoPB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 925C1F431F7B73B20063DFA0 /* FirebaseNanoPB.framework */; }; - 925C1F4C1F7B73B20063DFA0 /* nanopb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 925C1F451F7B73B20063DFA0 /* nanopb.framework */; }; - 925C1F811F7B73C00063DFA0 /* FirebaseMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 925C1F7F1F7B73C00063DFA0 /* FirebaseMessaging.framework */; }; - 92A0DF7D1F4DE3A4002051BC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 92A0DF491F4DE3A0002051BC /* GoogleService-Info.plist */; }; - 98482A6F2065257B00263651 /* libReactNativeTestFairy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9880471C20650CBC00CEBFE0 /* libReactNativeTestFairy.a */; }; - 9E0B01A11DDC5DA7002B0359 /* SF-UI-Text-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9E0B01A01DDC5DA7002B0359 /* SF-UI-Text-Light.otf */; }; - 9E3AB6D01D87DB2B008846B4 /* libReact-Native-Webview-Bridge.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E3AB6C61D87DA2B008846B4 /* libReact-Native-Webview-Bridge.a */; }; - 9E7C64731E03FDDE004C7042 /* libRCTCamera.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20B7D1151D3F74CD00B70F14 /* libRCTCamera.a */; }; - 9ED2F45E1D9D535A00B36508 /* SF-UI-Text-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9ED2F45D1D9D52DD00B36508 /* SF-UI-Text-Regular.otf */; }; - 9ED2F45F1D9D535A00B36508 /* SF-UI-Text-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9ED2F45C1D9D52C100B36508 /* SF-UI-Text-Medium.otf */; }; - 9ED2F4611D9D579900B36508 /* SF-UI-Text-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9ED2F4601D9D577B00B36508 /* SF-UI-Text-Bold.otf */; }; - 9EE89E271E03FCB7007D3C25 /* libSplashScreen.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B24FC7F21DE718EF00D694FF /* libSplashScreen.a */; }; - 9EE89E2D1E03FD9F007D3C25 /* libimageCropPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20A5C9531D927137002C4965 /* libimageCropPicker.a */; }; - 9EF0836B1F3B53AB00876A8F /* libReactNativeConfig.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9EF083611F3B538B00876A8F /* libReactNativeConfig.a */; }; - AB96C730942E49178F59AE6F /* libRNSecureRandom.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1464A9A9E12F45068947C28F /* libRNSecureRandom.a */; }; - ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; }; - B23B48FF1E76917B006D4535 /* RobotoMono-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B23B48FE1E76917B006D4535 /* RobotoMono-Medium.ttf */; }; + 8391E8E0E93C41A98AAA6631 /* Inter-SemiBoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = A4F2BBE8D4DD4140A6CCAC39 /* Inter-SemiBoldItalic.otf */; }; B24FC7FD1DE7195700D694FF /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B24FC7FC1DE7195700D694FF /* Social.framework */; }; B24FC7FF1DE7195F00D694FF /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B24FC7FE1DE7195F00D694FF /* MessageUI.framework */; }; - B2DEA0D01E49E33300FA28D6 /* libRCTHttpServer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B2DEA0B11E49E32000FA28D6 /* libRCTHttpServer.a */; }; B2F2D1BC1D9D531B00B7B453 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B2F2D1BB1D9D531B00B7B453 /* Images.xcassets */; }; - B957A49EB0DE44D9A31CAF2D /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF2BF381BC7B4EB0887F0091 /* libRNSVG.a */; }; BA68A2377A20496EA737000D /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E586E1B0E544F64AA9F5BD1 /* libz.tbd */; }; - C93242561FE1C68C00FE7099 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C93242531FE1C68C00FE7099 /* libRCTAnimation.a */; }; - CE4E31B31D8695250033ED64 /* Statusgo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4E31B21D8695250033ED64 /* Statusgo.framework */; }; - DA0D1ABA19458751CC2840C3 /* Instabug.framework in Embed Instabug Framework */ = {isa = PBXBuildFile; fileRef = 22D72275B7ADF9FC3304DCE5 /* Instabug.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - E0AD9E8F495A4907B65104BF /* libRCTImageResizer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2BEE3436791D42248F853999 /* libRCTImageResizer.a */; }; - F9238D6C1E5F055900C047B9 /* SF-UI-Text-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = F9238D6B1E5F055900C047B9 /* SF-UI-Text-Semibold.otf */; }; - FD4F213C3873473CB703B1D2 /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 674B3D9595A047AB8D518F4E /* libRNFS.a */; }; + C14C5F8D29C0A149005C58A7 /* launch-icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C14C5F8C29C0A149005C58A7 /* launch-icon@3x.png */; }; + C14C5F9129C0AD9C005C58A7 /* launch-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C14C5F9029C0AD9C005C58A7 /* launch-icon@2x.png */; }; + C14C5F9329C0ADB5005C58A7 /* launch-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = C14C5F9229C0ADB5005C58A7 /* launch-icon.png */; }; + C1715FFA29C0BCE10088FA8B /* launch-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C14C5F9029C0AD9C005C58A7 /* launch-icon@2x.png */; }; + C1715FFB29C0BCE50088FA8B /* launch-icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C14C5F8C29C0A149005C58A7 /* launch-icon@3x.png */; }; + C1715FFC29C0BCE80088FA8B /* launch-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = C14C5F9229C0ADB5005C58A7 /* launch-icon.png */; }; + CE4E31B31D8695250033ED64 /* Statusgo.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4E31B21D8695250033ED64 /* Statusgo.xcframework */; }; + D1786306E0184916B11F4C37 /* Inter-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = B2A38FC3D3954DE7B2B171F8 /* Inter-Medium.otf */; }; + D791AB0467C983D226FF5713 /* libPods-Status-StatusIm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E42DDCDBECE60E763EAA5042 /* libPods-Status-StatusIm.a */; }; + D84616FB563A48EBB1678699 /* Inter-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CD4A2C27D6D5473184DC1F7E /* Inter-Bold.otf */; }; + D99C50E5E18942A39C8DDF61 /* Inter-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = B321D25F4493470980039457 /* Inter-BoldItalic.otf */; }; + F9C9188A9398D58C286A2F4B /* libPods-Status-StatusImPR.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C929A693823978C12D329338 /* libPods-Status-StatusImPR.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RCTActionSheet; - }; - 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RCTGeolocation; - }; - 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 58B5115D1A9E6B3D00147676; - remoteInfo = RCTImage; - }; - 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 58B511DB1A9E6C8500147676; - remoteInfo = RCTNetwork; - }; - 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; - remoteInfo = RCTVibration; - }; 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; @@ -119,181 +74,6 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = StatusIm; }; - 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RCTSettings; - }; - 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3C86DF461ADF2C930047B81A; - remoteInfo = RCTWebSocket; - }; - 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; - remoteInfo = React; - }; - 2010676C1D477F5E00FA83B6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 38E1A2C8D0734EE99E2B16CE /* TcpSockets.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = TcpSockets; - }; - 201067C31D4789F700FA83B6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 439B6B4B407A4E2AACAFE5BE /* RCTStatus.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 206C9F3A1D474E910063E3E6; - remoteInfo = RCTStatus; - }; - 20A5C9521D927137002C4965 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 20A5C94B1D927137002C4965 /* imageCropPicker.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3400A8081CEB54A6008A0BC7; - remoteInfo = imageCropPicker; - }; - 20B7D10D1D3F74CD00B70F14 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 46E2F6052EB44C698C680894 /* RNI18n.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CDD7BF781B2D5125006FDA75; - remoteInfo = RNI18n; - }; - 20B7D1141D3F74CD00B70F14 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9F1854E6D9654226B1FC8308 /* RCTCamera.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 4107012F1ACB723B00C6AA39; - remoteInfo = RCTCamera; - }; - 20B7D1191D3F74CD00B70F14 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 305F194186D848FDB07AF34C /* RNFS.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = F12AFB9B1ADAF8F800E0535D; - remoteInfo = RNFS; - }; - 20B7D1281D3F74CD00B70F14 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F090E261B9854867A728CE4F /* RealmReact.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = F60690131CA2766F0003FB26; - remoteInfo = RealmReact; - }; - 5537E2A920650E640085CED4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = EBF21BDC1FC498900052F4D5; - remoteInfo = jsinspector; - }; - 5537E2AB20650E640085CED4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = EBF21BFA1FC4989A0052F4D5; - remoteInfo = "jsinspector-tvOS"; - }; - 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RCTLinking; - }; - 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 58B5119B1A9E6C1200147676; - remoteInfo = RCTText; - }; - 922C4C4A1F4D5C3F0033C753 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F89A8F8005874B86B63C22E3 /* RNFIRMessaging.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RNFIRMessaging; - }; - 92925B671F571DE600203EEB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 1E74DC52A1E449A2BA858B14 /* RNSVG.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 0CF68AC11AF0540F00FF9E5C; - remoteInfo = RNSVG; - }; - 9880471B20650CBC00CEBFE0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9880471720650CBC00CEBFE0 /* ReactNativeTestFairy.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 42959838203B471F00E34882; - remoteInfo = ReactNativeTestFairy; - }; - 9E3AB6C51D87DA2B008846B4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9E3AB6B21D87DA2A008846B4 /* React-Native-Webview-Bridge.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 4114DC4C1C187C3A003CD988; - remoteInfo = "React-Native-Webview-Bridge"; - }; - 9E3F1BED1DDAE781005E4779 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A283A1D9B042B00D4039D; - remoteInfo = "RCTImage-tvOS"; - }; - 9E3F1BF21DDAE781005E4779 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A28471D9B043800D4039D; - remoteInfo = "RCTLinking-tvOS"; - }; - 9E3F1BF61DDAE781005E4779 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A28541D9B044C00D4039D; - remoteInfo = "RCTNetwork-tvOS"; - }; - 9E3F1BFB1DDAE781005E4779 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A28611D9B046600D4039D; - remoteInfo = "RCTSettings-tvOS"; - }; - 9E3F1C001DDAE781005E4779 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A287B1D9B048500D4039D; - remoteInfo = "RCTText-tvOS"; - }; - 9E3F1C051DDAE781005E4779 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A28881D9B049200D4039D; - remoteInfo = "RCTWebSocket-tvOS"; - }; - 9E3F1C111DDAE781005E4779 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F3548417D8DA4362B6796A54 /* RNInstabug.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 24DF11ED1DA3A2B90056F77C; - remoteInfo = RNInstabug; - }; 9EC013781E06FB1900155B5C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9EC0135C1E06FB1900155B5C /* RCTWKWebView.xcodeproj */; @@ -301,202 +81,6 @@ remoteGlobalIDString = 0974579A1D2A440A000D9368; remoteInfo = RCTWKWebView; }; - 9EF083601F3B538B00876A8F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9EF083381F3B538A00876A8F /* ReactNativeConfig.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = EB2648DF1C7BE17A00B8F155; - remoteInfo = ReactNativeConfig; - }; - ADBDB9261DFEBF0700ED6528 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 358F4ED71D1E81A9004DF814; - remoteInfo = RCTBlob; - }; - B24FC7F11DE718EF00D694FF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 3A7EB0491DD9CABC00A4FCC8 /* SplashScreen.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D7682761D8E76B80014119E; - remoteInfo = SplashScreen; - }; - B2DEA0B01E49E32000FA28D6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B2DEA0A41E49E32000FA28D6 /* RCTHttpServer.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = B29EC9CC1E48BED600704A36; - remoteInfo = RCTHttpServer; - }; - C20F3A6E20E5165B00757214 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = AF409BB755B94FB1BBA01927 /* RNSecureRandom.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RNSecureRandom; - }; - C90047CC1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = ADD01A681E09402E00F6D226; - remoteInfo = "RCTBlob-tvOS"; - }; - C90047E81FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3DBE0D001F3B181A0099AA32; - remoteInfo = fishhook; - }; - C90047EA1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3DBE0D0D1F3B181C0099AA32; - remoteInfo = "fishhook-tvOS"; - }; - C90047FB1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; - remoteInfo = React; - }; - C90047FD1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A28131D9B038B00D4039D; - remoteInfo = "React-tvOS"; - }; - C90047FF1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D3C059A1DE3340900C268FA; - remoteInfo = yoga; - }; - C90048011FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D3C06751DE3340C00C268FA; - remoteInfo = "yoga-tvOS"; - }; - C90048031FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D3CD9251DE5FBEC00167DC4; - remoteInfo = cxxreact; - }; - C90048051FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D3CD9321DE5FBEE00167DC4; - remoteInfo = "cxxreact-tvOS"; - }; - C90048071FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D3CD90B1DE5FBD600167DC4; - remoteInfo = jschelpers; - }; - C90048091FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D3CD9181DE5FBD800167DC4; - remoteInfo = "jschelpers-tvOS"; - }; - C900480B1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 139D7ECE1E25DB7D00323FB7; - remoteInfo = "third-party"; - }; - C900480D1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D383D3C1EBD27B6005632C8; - remoteInfo = "third-party-tvOS"; - }; - C900480F1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 139D7E881E25C6D100323FB7; - remoteInfo = "double-conversion"; - }; - C90048111FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 3D383D621EBD27B9005632C8; - remoteInfo = "double-conversion-tvOS"; - }; - C90048131FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 9936F3131F5F2E4B0010BF04; - remoteInfo = privatedata; - }; - C90048151FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 9936F32F1F5F2E5B0010BF04; - remoteInfo = "privatedata-tvOS"; - }; - C900481C1FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 305F194186D848FDB07AF34C /* RNFS.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 6456441F1EB8DA9100672408; - remoteInfo = "RNFS-tvOS"; - }; - C90048221FC47AF50002B8EA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 1E74DC52A1E449A2BA858B14 /* RNSVG.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 94DDAC5C1F3D024300EED511; - remoteInfo = "RNSVG-tvOS"; - }; - C93242521FE1C68C00FE7099 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B2A5F42F1DEC36B200174F4D /* RCTAnimation.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RCTAnimation; - }; - C93242541FE1C68C00FE7099 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B2A5F42F1DEC36B200174F4D /* RCTAnimation.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 2D2A28201D9B03D100D4039D; - remoteInfo = "RCTAnimation-tvOS"; - }; - C9B824E81FC713C800D277C6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 46E2F6052EB44C698C680894 /* RNI18n.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 6476C4051EEAA69700B10F51; - remoteInfo = "RNI18n-tvOS"; - }; - CE4E319D1D8693090033ED64 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 5E5A7625B76441D984EA8C0D /* RCTImageResizer.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 5D72D2E81C16249000E22EC1; - remoteInfo = RCTImageResizer; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -506,116 +90,74 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 20B6B6851D92C42600CC5C6A /* RSKImageCropper.framework in Embed Frameworks */, - 20B6B6881D92C42600CC5C6A /* QBImagePicker.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - C5A3444E637B7B715148AD1B /* Embed Instabug Framework */ = { + 3AAD2AD624A3A60E0075D594 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - DA0D1ABA19458751CC2840C3 /* Instabug.framework in Embed Instabug Framework */, - 3A8BF31C42291E324219071C /* InstabugCore.framework in Embed Instabug Framework */, ); - name = "Embed Instabug Framework"; + name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; - 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; - 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; }; - 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; - 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = "../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj"; sourceTree = ""; }; - 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = ""; }; 00E356EE1AD99517003FC87E /* StatusImTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StatusImTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* StatusImTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StatusImTests.m; sourceTree = ""; }; - 064351D2FD901F8B6C9AE2A5 /* Pods-StatusImTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StatusImTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-StatusImTests/Pods-StatusImTests.debug.xcconfig"; sourceTree = ""; }; - 101A4045637A2ADF57D28EF5 /* libPods-StatusIm.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-StatusIm.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 11632AA0A5F84F029DD91797 /* libRNVectorIcons.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNVectorIcons.a; sourceTree = ""; }; - 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; - 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* StatusIm.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StatusIm.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = StatusIm/AppDelegate.h; sourceTree = ""; }; - 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = StatusIm/AppDelegate.m; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = StatusIm/AppDelegate.mm; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = StatusIm/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = StatusIm/main.m; sourceTree = ""; }; - 1464A9A9E12F45068947C28F /* libRNSecureRandom.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSecureRandom.a; sourceTree = ""; }; - 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; - 1E74DC52A1E449A2BA858B14 /* RNSVG.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSVG.xcodeproj; path = "../node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = ""; }; - 2028DFF51D4275B600227DCD /* SF-UI-Display-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Display-Medium.otf"; sourceTree = ""; }; - 2028DFF61D4275B600227DCD /* SF-UI-Display-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Display-Regular.otf"; sourceTree = ""; }; - 2028DFF71D4275B600227DCD /* SF-UI-Display-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Display-Semibold.otf"; sourceTree = ""; }; - 2028DFF81D4275B600227DCD /* SF-UI-Display-Thin.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Display-Thin.otf"; sourceTree = ""; }; - 20A5C94B1D927137002C4965 /* imageCropPicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = imageCropPicker.xcodeproj; path = "../node_modules/react-native-image-crop-picker/ios/imageCropPicker.xcodeproj"; sourceTree = ""; }; - 20A5C96C1D92715E002C4965 /* RSKImageCropper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RSKImageCropper.framework; path = "../node_modules/react-native-image-crop-picker/ios/RSKImageCropper/build/Debug-iphoneos/RSKImageCropper.framework"; sourceTree = ""; }; - 20A5C96E1D92716C002C4965 /* QBImagePicker.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QBImagePicker.framework; path = "../node_modules/react-native-image-crop-picker/ios/QBImagePicker/build/Debug-iphoneos/QBImagePicker.framework"; sourceTree = ""; }; - 20B6B6831D92C42600CC5C6A /* RSKImageCropper.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSKImageCropper.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 20B6B6861D92C42600CC5C6A /* QBImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = QBImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 22D72275B7ADF9FC3304DCE5 /* Instabug.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Instabug.framework; path = "../node_modules/instabug-reactnative/ios/Instabug.framework"; sourceTree = ""; }; - 2BEE3436791D42248F853999 /* libRCTImageResizer.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTImageResizer.a; sourceTree = ""; }; - 305F194186D848FDB07AF34C /* RNFS.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNFS.xcodeproj; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = ""; }; - 38A44830EC5708E89387F641 /* Pods-StatusIm.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StatusIm.release.xcconfig"; path = "Pods/Target Support Files/Pods-StatusIm/Pods-StatusIm.release.xcconfig"; sourceTree = ""; }; - 38E1A2C8D0734EE99E2B16CE /* TcpSockets.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = TcpSockets.xcodeproj; path = "../node_modules/react-native-tcp/ios/TcpSockets.xcodeproj"; sourceTree = ""; }; - 3A7EB0491DD9CABC00A4FCC8 /* SplashScreen.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SplashScreen.xcodeproj; path = "../node_modules/react-native-splash-screen/ios/SplashScreen.xcodeproj"; sourceTree = ""; }; - 439B6B4B407A4E2AACAFE5BE /* RCTStatus.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTStatus.xcodeproj; path = "../modules/react-native-status/ios/RCTStatus/RCTStatus.xcodeproj"; sourceTree = ""; }; - 46E2F6052EB44C698C680894 /* RNI18n.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNI18n.xcodeproj; path = "../node_modules/react-native-i18n/ios/RNI18n.xcodeproj"; sourceTree = ""; }; + 1426DF592BA248FC81D955CB /* Inter-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Regular.otf"; path = "../resources/fonts/Inter-Regular.otf"; sourceTree = ""; }; + 203C46547A6E99D27529A8AA /* Pods-Status-StatusIm-StatusImTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status-StatusIm-StatusImTests.release.xcconfig"; path = "Target Support Files/Pods-Status-StatusIm-StatusImTests/Pods-Status-StatusIm-StatusImTests.release.xcconfig"; sourceTree = ""; }; + 24F63BCFEB33B4C969305A46 /* Pods-Status-StatusIm.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status-StatusIm.debug.xcconfig"; path = "Target Support Files/Pods-Status-StatusIm/Pods-Status-StatusIm.debug.xcconfig"; sourceTree = ""; }; + 2BE1270D2D59242300023E5E /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 344F43B59CCE090C3271FF08 /* Pods-Status-StatusIm.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status-StatusIm.release.xcconfig"; path = "Target Support Files/Pods-Status-StatusIm/Pods-Status-StatusIm.release.xcconfig"; sourceTree = ""; }; + 3A2626CE245C3F2200D5F94B /* Dummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dummy.swift; sourceTree = ""; }; + 3A6406FB24A3ADF90046ED37 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3A8F8EA924A4D31600BF206D /* GameKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameKit.framework; path = System/Library/Frameworks/GameKit.framework; sourceTree = SDKROOT; }; + 3AAD2ADC24A3A60E0075D594 /* Status PR.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Status PR.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AB1C3AD245C043900098F67 /* StatusIm-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StatusIm-Bridging-Header.h"; sourceTree = ""; }; + 415E72A662B5ABF086AB7D7B /* Pods-Status-StatusImPR.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status-StatusImPR.release.xcconfig"; path = "Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR.release.xcconfig"; sourceTree = ""; }; 4C16DE0B1F89508700AA10DB /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 4E586E1B0E544F64AA9F5BD1 /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; - 4EC426A98043452BB6F9C134 /* libRNInstabug.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNInstabug.a; sourceTree = ""; }; - 52F6ED6465184513A082652B /* libRNI18n.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNI18n.a; sourceTree = ""; }; - 5535217F57E44D77AA9CF083 /* libRCTOrientation.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTOrientation.a; sourceTree = ""; }; - 5E5A7625B76441D984EA8C0D /* RCTImageResizer.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTImageResizer.xcodeproj; path = "../node_modules/react-native-image-resizer/ios/RCTImageResizer.xcodeproj"; sourceTree = ""; }; - 674B3D9595A047AB8D518F4E /* libRNFS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFS.a; sourceTree = ""; }; + 6081A369F49E04EC54F77E1B /* libPods-Status-StatusIm-StatusImTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Status-StatusIm-StatusImTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 65F693BD2578002500A45E76 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; }; + 65F693BF2578003600A45E76 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/iOSSupport/System/Library/Frameworks/CoreNFC.framework; sourceTree = DEVELOPER_DIR; }; + 65F6941725780A4E00A45E76 /* StatusImTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StatusImTests-Bridging-Header.h"; sourceTree = ""; }; + 65F6941825780A4F00A45E76 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = ""; }; + 693A62DB37BC4CD5A30E5C96 /* Inter-SemiBold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-SemiBold.otf"; path = "../resources/fonts/Inter-SemiBold.otf"; sourceTree = ""; }; + 70748FF05FFD3CACB6F64CE0 /* Pods-Status-StatusImPR.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status-StatusImPR.debug.xcconfig"; path = "Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR.debug.xcconfig"; sourceTree = ""; }; + 715D8131290BE850006F5C88 /* UbuntuMono-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "UbuntuMono-Regular.ttf"; path = "../resources/fonts/UbuntuMono-Regular.ttf"; sourceTree = ""; }; 74B758FB20D7C00B003343C3 /* launch-image-universal.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "launch-image-universal.storyboard"; sourceTree = ""; }; - 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; - 7F752F17B1E04216B1337A72 /* libRNFIRMessaging.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFIRMessaging.a; sourceTree = ""; }; - 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; 8B9A886A2CB448B1ABA0EB62 /* libc++.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; 922C4CA61F4D5F8B0033C753 /* StatusIm.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = StatusIm.entitlements; path = StatusIm/StatusIm.entitlements; sourceTree = ""; }; - 925C1F401F7B73B20063DFA0 /* FirebaseCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseCore.framework; sourceTree = ""; }; - 925C1F411F7B73B20063DFA0 /* FirebaseCoreDiagnostics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseCoreDiagnostics.framework; sourceTree = ""; }; - 925C1F421F7B73B20063DFA0 /* FirebaseInstanceID.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseInstanceID.framework; sourceTree = ""; }; - 925C1F431F7B73B20063DFA0 /* FirebaseNanoPB.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseNanoPB.framework; sourceTree = ""; }; - 925C1F451F7B73B20063DFA0 /* nanopb.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = nanopb.framework; sourceTree = ""; }; - 925C1F7F1F7B73C00063DFA0 /* FirebaseMessaging.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseMessaging.framework; sourceTree = ""; }; - 92A0DF491F4DE3A0002051BC /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 9880471720650CBC00CEBFE0 /* ReactNativeTestFairy.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeTestFairy.xcodeproj; path = "../node_modules/react-native-testfairy/ios/ReactNativeTestFairy.xcodeproj"; sourceTree = ""; }; - 9E0B01A01DDC5DA7002B0359 /* SF-UI-Text-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Text-Light.otf"; sourceTree = ""; }; - 9E3AB6B21D87DA2A008846B4 /* React-Native-Webview-Bridge.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "React-Native-Webview-Bridge.xcodeproj"; path = "../node_modules/react-native-webview-bridge/ios/React-Native-Webview-Bridge.xcodeproj"; sourceTree = ""; }; + 9C76AF5A418D4D65A4CAD1D9 /* InterStatus-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "InterStatus-Regular.otf"; path = "../resources/fonts/InterStatus-Regular.otf"; sourceTree = ""; }; 9EC0135C1E06FB1900155B5C /* RCTWKWebView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWKWebView.xcodeproj; path = "../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView.xcodeproj"; sourceTree = ""; }; - 9ED2F45C1D9D52C100B36508 /* SF-UI-Text-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Text-Medium.otf"; sourceTree = ""; }; - 9ED2F45D1D9D52DD00B36508 /* SF-UI-Text-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Text-Regular.otf"; sourceTree = ""; }; - 9ED2F4601D9D577B00B36508 /* SF-UI-Text-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Text-Bold.otf"; sourceTree = ""; }; - 9EF083381F3B538A00876A8F /* ReactNativeConfig.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeConfig.xcodeproj; path = "../node_modules/react-native-config/ios/ReactNativeConfig.xcodeproj"; sourceTree = ""; }; - 9F1854E6D9654226B1FC8308 /* RCTCamera.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTCamera.xcodeproj; path = "../node_modules/react-native-camera/ios/RCTCamera.xcodeproj"; sourceTree = ""; }; - ACA66A8F16CD2FE21F38738B /* Pods-StatusIm.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StatusIm.debug.xcconfig"; path = "Pods/Target Support Files/Pods-StatusIm/Pods-StatusIm.debug.xcconfig"; sourceTree = ""; }; - ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = "../node_modules/react-native/Libraries/Blob/RCTBlob.xcodeproj"; sourceTree = ""; }; - AF2BF381BC7B4EB0887F0091 /* libRNSVG.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSVG.a; sourceTree = ""; }; - AF409BB755B94FB1BBA01927 /* RNSecureRandom.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSecureRandom.xcodeproj; path = "../node_modules/react-native-securerandom/ios/RNSecureRandom.xcodeproj"; sourceTree = ""; }; - B23B48FE1E76917B006D4535 /* RobotoMono-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "RobotoMono-Medium.ttf"; sourceTree = ""; }; + A4F2BBE8D4DD4140A6CCAC39 /* Inter-SemiBoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-SemiBoldItalic.otf"; path = "../resources/fonts/Inter-SemiBoldItalic.otf"; sourceTree = ""; }; + B07176ACDAA1422E8F0A3D6B /* Inter-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Italic.otf"; path = "../resources/fonts/Inter-Italic.otf"; sourceTree = ""; }; B24FC7FC1DE7195700D694FF /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; B24FC7FE1DE7195F00D694FF /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; - B2A5F42F1DEC36B200174F4D /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; - B2DEA0A41E49E32000FA28D6 /* RCTHttpServer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTHttpServer.xcodeproj; path = "../node_modules/react-native-http-bridge/ios/RCTHttpServer.xcodeproj"; sourceTree = ""; }; + B2A38FC3D3954DE7B2B171F8 /* Inter-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Medium.otf"; path = "../resources/fonts/Inter-Medium.otf"; sourceTree = ""; }; B2F2D1BB1D9D531B00B7B453 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = StatusIm/Images.xcassets; sourceTree = ""; }; - C2A4E93F6B154AEFA3485B45 /* libTcpSockets.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libTcpSockets.a; sourceTree = ""; }; - CD48A32459B64E96843BB238 /* libRealmReact.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRealmReact.a; sourceTree = ""; }; - CE4E31B21D8695250033ED64 /* Statusgo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Statusgo.framework; path = "../modules/react-native-status/ios/RCTStatus/Statusgo.framework"; sourceTree = ""; }; - D489EE8D5F52DA10AC715727 /* Pods-StatusImTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StatusImTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-StatusImTests/Pods-StatusImTests.release.xcconfig"; sourceTree = ""; }; - DF1CD4C3D1254774ACCAE4E8 /* libBVLinearGradient.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libBVLinearGradient.a; sourceTree = ""; }; - EA36FD18BC2ECACF0A8B83DE /* InstabugCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InstabugCore.framework; path = "../node_modules/instabug-reactnative/ios/InstabugCore.framework"; sourceTree = ""; }; - F090E261B9854867A728CE4F /* RealmReact.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RealmReact.xcodeproj; path = "../node_modules/realm/react-native/ios/RealmReact.xcodeproj"; sourceTree = ""; }; - F3548417D8DA4362B6796A54 /* RNInstabug.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNInstabug.xcodeproj; path = "../node_modules/instabug-reactnative/ios/RNInstabug.xcodeproj"; sourceTree = ""; }; - F89A8F8005874B86B63C22E3 /* RNFIRMessaging.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNFIRMessaging.xcodeproj; path = "../node_modules/react-native-fcm/ios/RNFIRMessaging.xcodeproj"; sourceTree = ""; }; - F9238D6B1E5F055900C047B9 /* SF-UI-Text-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-UI-Text-Semibold.otf"; sourceTree = ""; }; - FC1CBCFE6C906043D6CCEEE1 /* libPods-StatusImTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-StatusImTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + B321D25F4493470980039457 /* Inter-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-BoldItalic.otf"; path = "../resources/fonts/Inter-BoldItalic.otf"; sourceTree = ""; }; + C14C5F8C29C0A149005C58A7 /* launch-icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "launch-icon@3x.png"; path = "StatusIm/launch-icon@3x.png"; sourceTree = ""; }; + C14C5F9029C0AD9C005C58A7 /* launch-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "launch-icon@2x.png"; path = "StatusIm/launch-icon@2x.png"; sourceTree = ""; }; + C14C5F9229C0ADB5005C58A7 /* launch-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "launch-icon.png"; path = "StatusIm/launch-icon.png"; sourceTree = ""; }; + C6B1215047604CD59A4C74D6 /* Inter-MediumItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-MediumItalic.otf"; path = "../resources/fonts/Inter-MediumItalic.otf"; sourceTree = ""; }; + C929A693823978C12D329338 /* libPods-Status-StatusImPR.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Status-StatusImPR.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + CD4A2C27D6D5473184DC1F7E /* Inter-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Bold.otf"; path = "../resources/fonts/Inter-Bold.otf"; sourceTree = ""; }; + CE4E31B21D8695250033ED64 /* Statusgo.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Statusgo.xcframework; path = "../modules/react-native-status/ios/RCTStatus/Statusgo.xcframework"; sourceTree = ""; }; + E42DDCDBECE60E763EAA5042 /* libPods-Status-StatusIm.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Status-StatusIm.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F12F9820A9DF3FE08C615B90 /* Pods-Status-StatusIm-StatusImTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status-StatusIm-StatusImTests.debug.xcconfig"; path = "Target Support Files/Pods-Status-StatusIm-StatusImTests/Pods-Status-StatusIm-StatusImTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -623,7 +165,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 81C6E6AE0AA739BE9D87C1D0 /* libPods-StatusImTests.a in Frameworks */, + 2D32CCEDFF64E1C3F1FB79AF /* libPods-Status-StatusIm-StatusImTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -631,100 +173,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4CA4DA1B206D105D006A98B0 /* libRNInstabug.a in Frameworks */, - 98482A6F2065257B00263651 /* libReactNativeTestFairy.a in Frameworks */, - C93242561FE1C68C00FE7099 /* libRCTAnimation.a in Frameworks */, - 4C16DE0C1F89508700AA10DB /* JavaScriptCore.framework in Frameworks */, - B2DEA0D01E49E33300FA28D6 /* libRCTHttpServer.a in Frameworks */, - 925C1F481F7B73B20063DFA0 /* FirebaseCoreDiagnostics.framework in Frameworks */, - 9EE89E271E03FCB7007D3C25 /* libSplashScreen.a in Frameworks */, - 925C1F811F7B73C00063DFA0 /* FirebaseMessaging.framework in Frameworks */, - 925C1F4A1F7B73B20063DFA0 /* FirebaseNanoPB.framework in Frameworks */, B24FC7FF1DE7195F00D694FF /* MessageUI.framework in Frameworks */, B24FC7FD1DE7195700D694FF /* Social.framework in Frameworks */, - 9EE89E2D1E03FD9F007D3C25 /* libimageCropPicker.a in Frameworks */, - 9E3AB6D01D87DB2B008846B4 /* libReact-Native-Webview-Bridge.a in Frameworks */, - 20B6B6841D92C42600CC5C6A /* RSKImageCropper.framework in Frameworks */, - CE4E31B31D8695250033ED64 /* Statusgo.framework in Frameworks */, - 20AB9EC61D47CC0300E7FD9C /* libRCTStatus.a in Frameworks */, - 925C1F471F7B73B20063DFA0 /* FirebaseCore.framework in Frameworks */, - 20B6B6871D92C42600CC5C6A /* QBImagePicker.framework in Frameworks */, - ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */, - 146834051AC3E58100842450 /* libReact.a in Frameworks */, - 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, - 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, - 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, - 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, - 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, - 925C1F4C1F7B73B20063DFA0 /* nanopb.framework in Frameworks */, - 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, - 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, - 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, - 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, - 9E7C64731E03FDDE004C7042 /* libRCTCamera.a in Frameworks */, - FD4F213C3873473CB703B1D2 /* libRNFS.a in Frameworks */, - 213311F38CA74CE280FD09AD /* libRNI18n.a in Frameworks */, - 22118DE1207A419FBFE7B62D /* libRealmReact.a in Frameworks */, + CE4E31B31D8695250033ED64 /* Statusgo.xcframework in Frameworks */, 25DC9C9DC25846BD8D084888 /* libc++.tbd in Frameworks */, BA68A2377A20496EA737000D /* libz.tbd in Frameworks */, - 3E15DFEC1F6F4D7CAE088F49 /* libTcpSockets.a in Frameworks */, - E0AD9E8F495A4907B65104BF /* libRCTImageResizer.a in Frameworks */, - 8E55E6877F950B81C8D711C5 /* libPods-StatusIm.a in Frameworks */, - 925C1F491F7B73B20063DFA0 /* FirebaseInstanceID.framework in Frameworks */, - 9EF0836B1F3B53AB00876A8F /* libReactNativeConfig.a in Frameworks */, - B957A49EB0DE44D9A31CAF2D /* libRNSVG.a in Frameworks */, - 5974D2035B8B47E0946B63B6 /* libRNFIRMessaging.a in Frameworks */, - 91446A820DA5E1C15C24D2A7 /* Instabug.framework in Frameworks */, - 63AE4174241B852A045FAE1F /* InstabugCore.framework in Frameworks */, - AB96C730942E49178F59AE6F /* libRNSecureRandom.a in Frameworks */, + D791AB0467C983D226FF5713 /* libPods-Status-StatusIm.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AAD2ABF24A3A60E0075D594 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A8F8EAA24A4D31600BF206D /* GameKit.framework in Frameworks */, + 3AAD2AC024A3A60E0075D594 /* MessageUI.framework in Frameworks */, + 3AAD2AC124A3A60E0075D594 /* Social.framework in Frameworks */, + 3AAD2AC224A3A60E0075D594 /* Statusgo.xcframework in Frameworks */, + 3AAD2AC524A3A60E0075D594 /* libc++.tbd in Frameworks */, + 3AAD2AC624A3A60E0075D594 /* libz.tbd in Frameworks */, + F9C9188A9398D58C286A2F4B /* libPods-Status-StatusImPR.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 00C302A81ABCB8CE00DB3ED1 /* Products */ = { - isa = PBXGroup; - children = ( - 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, - ); - name = Products; - sourceTree = ""; - }; - 00C302B61ABCB90400DB3ED1 /* Products */ = { - isa = PBXGroup; - children = ( - 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, - ); - name = Products; - sourceTree = ""; - }; - 00C302BC1ABCB91800DB3ED1 /* Products */ = { - isa = PBXGroup; - children = ( - 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, - 9E3F1BEE1DDAE781005E4779 /* libRCTImage-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; - 00C302D41ABCB9D200DB3ED1 /* Products */ = { - isa = PBXGroup; - children = ( - 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, - 9E3F1BF71DDAE781005E4779 /* libRCTNetwork-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; - 00C302E01ABCB9EE00DB3ED1 /* Products */ = { - isa = PBXGroup; - children = ( - 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, - ); - name = Products; - sourceTree = ""; - }; 00E356EF1AD99517003FC87E /* StatusImTests */ = { isa = PBXGroup; children = ( @@ -742,392 +216,127 @@ name = "Supporting Files"; sourceTree = ""; }; - 139105B71AF99BAD00B5F7CC /* Products */ = { - isa = PBXGroup; - children = ( - 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, - 9E3F1BFC1DDAE781005E4779 /* libRCTSettings-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; - 139FDEE71B06529A00C62182 /* Products */ = { - isa = PBXGroup; - children = ( - 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, - 9E3F1C061DDAE781005E4779 /* libRCTWebSocket-tvOS.a */, - C90047E91FC47AF50002B8EA /* libfishhook.a */, - C90047EB1FC47AF50002B8EA /* libfishhook-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; 13B07FAE1A68108700A75B9A /* StatusIm */ = { isa = PBXGroup; children = ( + C14C5F9229C0ADB5005C58A7 /* launch-icon.png */, + C14C5F9029C0AD9C005C58A7 /* launch-icon@2x.png */, + C14C5F8C29C0A149005C58A7 /* launch-icon@3x.png */, 922C4CA61F4D5F8B0033C753 /* StatusIm.entitlements */, B2F2D1BB1D9D531B00B7B453 /* Images.xcassets */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, - 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, + 3AB1C3AD245C043900098F67 /* StatusIm-Bridging-Header.h */, + 3A2626CE245C3F2200D5F94B /* Dummy.swift */, ); name = StatusIm; sourceTree = ""; }; - 146834001AC3E56700842450 /* Products */ = { - isa = PBXGroup; - children = ( - C90047FC1FC47AF50002B8EA /* libReact.a */, - C90047FE1FC47AF50002B8EA /* libReact.a */, - C90048001FC47AF50002B8EA /* libyoga.a */, - C90048021FC47AF50002B8EA /* libyoga.a */, - C90048041FC47AF50002B8EA /* libcxxreact.a */, - C90048061FC47AF50002B8EA /* libcxxreact.a */, - C90048081FC47AF50002B8EA /* libjschelpers.a */, - C900480A1FC47AF50002B8EA /* libjschelpers.a */, - 5537E2AA20650E640085CED4 /* libjsinspector.a */, - 5537E2AC20650E640085CED4 /* libjsinspector-tvOS.a */, - C900480C1FC47AF50002B8EA /* libthird-party.a */, - C900480E1FC47AF50002B8EA /* libthird-party.a */, - C90048101FC47AF50002B8EA /* libdouble-conversion.a */, - C90048121FC47AF50002B8EA /* libdouble-conversion.a */, - C90048141FC47AF50002B8EA /* libprivatedata.a */, - C90048161FC47AF50002B8EA /* libprivatedata-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; 1E7837547A9A40E18AD63CF3 /* Resources */ = { isa = PBXGroup; children = ( - 2028E0111D4275BD00227DCD /* SF */, - B23B48FE1E76917B006D4535 /* RobotoMono-Medium.ttf */, 74B758FB20D7C00B003343C3 /* launch-image-universal.storyboard */, + CD4A2C27D6D5473184DC1F7E /* Inter-Bold.otf */, + B321D25F4493470980039457 /* Inter-BoldItalic.otf */, + B07176ACDAA1422E8F0A3D6B /* Inter-Italic.otf */, + B2A38FC3D3954DE7B2B171F8 /* Inter-Medium.otf */, + C6B1215047604CD59A4C74D6 /* Inter-MediumItalic.otf */, + 1426DF592BA248FC81D955CB /* Inter-Regular.otf */, + 693A62DB37BC4CD5A30E5C96 /* Inter-SemiBold.otf */, + A4F2BBE8D4DD4140A6CCAC39 /* Inter-SemiBoldItalic.otf */, + 9C76AF5A418D4D65A4CAD1D9 /* InterStatus-Regular.otf */, + 715D8131290BE850006F5C88 /* UbuntuMono-Regular.ttf */, ); name = Resources; sourceTree = ""; }; - 201067551D477F5E00FA83B6 /* Products */ = { + 3AAD2AB624A3A5F10075D594 /* StatusImPR */ = { isa = PBXGroup; children = ( - 2010676D1D477F5E00FA83B6 /* libTcpSockets.a */, + 3A6406FB24A3ADF90046ED37 /* Info.plist */, ); - name = Products; + path = StatusImPR; sourceTree = ""; }; - 201067BA1D4789F700FA83B6 /* Products */ = { + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( - 201067C41D4789F700FA83B6 /* libRCTStatus.a */, + 9EC0135C1E06FB1900155B5C /* RCTWKWebView.xcodeproj */, ); - name = Products; - sourceTree = ""; - }; - 2028E0111D4275BD00227DCD /* SF */ = { - isa = PBXGroup; - children = ( - F9238D6B1E5F055900C047B9 /* SF-UI-Text-Semibold.otf */, - 9ED2F4601D9D577B00B36508 /* SF-UI-Text-Bold.otf */, - 9E0B01A01DDC5DA7002B0359 /* SF-UI-Text-Light.otf */, - 9ED2F45D1D9D52DD00B36508 /* SF-UI-Text-Regular.otf */, - 9ED2F45C1D9D52C100B36508 /* SF-UI-Text-Medium.otf */, - 2028DFF51D4275B600227DCD /* SF-UI-Display-Medium.otf */, - 2028DFF61D4275B600227DCD /* SF-UI-Display-Regular.otf */, - 2028DFF71D4275B600227DCD /* SF-UI-Display-Semibold.otf */, - 2028DFF81D4275B600227DCD /* SF-UI-Display-Thin.otf */, - ); - name = SF; + name = Libraries; sourceTree = ""; }; - 20A5C94C1D927137002C4965 /* Products */ = { + 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( - 20A5C9531D927137002C4965 /* libimageCropPicker.a */, + 2BE1270D2D59242300023E5E /* GoogleService-Info.plist */, + 65F6941825780A4F00A45E76 /* Bridge.swift */, + 3AAD2AB624A3A5F10075D594 /* StatusImPR */, + 13B07FAE1A68108700A75B9A /* StatusIm */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* StatusImTests */, + 83CBBA001A601CBA00E9B192 /* Products */, + A97BA941B2FB44B4B66EE6D3 /* Frameworks */, + 1E7837547A9A40E18AD63CF3 /* Resources */, + 65F6941725780A4E00A45E76 /* StatusImTests-Bridging-Header.h */, + D0D5C8D06825D33BA2D2121E /* Pods */, ); - name = Products; + indentWidth = 2; sourceTree = ""; + tabWidth = 2; + usesTabs = 0; }; - 20B7D1041D3F74CD00B70F14 /* Products */ = { + 83CBBA001A601CBA00E9B192 /* Products */ = { isa = PBXGroup; children = ( - 20B7D10E1D3F74CD00B70F14 /* libRNI18n.a */, - C9B824E91FC713C800D277C6 /* libRNI18n-tvOS.a */, + 13B07F961A680F5B00A75B9A /* StatusIm.app */, + 00E356EE1AD99517003FC87E /* StatusImTests.xctest */, + 3AAD2ADC24A3A60E0075D594 /* Status PR.app */, ); name = Products; sourceTree = ""; }; - 20B7D1111D3F74CD00B70F14 /* Products */ = { + 9EC0135D1E06FB1900155B5C /* Products */ = { isa = PBXGroup; children = ( - 20B7D1151D3F74CD00B70F14 /* libRCTCamera.a */, + 9EC013791E06FB1900155B5C /* libRCTWKWebView.a */, ); name = Products; sourceTree = ""; }; - 20B7D1161D3F74CD00B70F14 /* Products */ = { - isa = PBXGroup; - children = ( - 20B7D11A1D3F74CD00B70F14 /* libRNFS.a */, - C900481D1FC47AF50002B8EA /* libRNFS.a */, - ); - name = Products; - sourceTree = ""; - }; - 20B7D1251D3F74CD00B70F14 /* Products */ = { - isa = PBXGroup; - children = ( - 20B7D1291D3F74CD00B70F14 /* libRealmReact.a */, - ); - name = Products; - sourceTree = ""; - }; - 5C1C8762251D6EF495FB2384 /* Pods */ = { - isa = PBXGroup; - children = ( - ACA66A8F16CD2FE21F38738B /* Pods-StatusIm.debug.xcconfig */, - 38A44830EC5708E89387F641 /* Pods-StatusIm.release.xcconfig */, - 064351D2FD901F8B6C9AE2A5 /* Pods-StatusImTests.debug.xcconfig */, - D489EE8D5F52DA10AC715727 /* Pods-StatusImTests.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 78C398B11ACF4ADC00677621 /* Products */ = { - isa = PBXGroup; - children = ( - 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, - 9E3F1BF31DDAE781005E4779 /* libRCTLinking-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; - 832341AE1AAA6A7D00B99B32 /* Libraries */ = { - isa = PBXGroup; - children = ( - 9880471720650CBC00CEBFE0 /* ReactNativeTestFairy.xcodeproj */, - 9EF083381F3B538A00876A8F /* ReactNativeConfig.xcodeproj */, - B2DEA0A41E49E32000FA28D6 /* RCTHttpServer.xcodeproj */, - 9EC0135C1E06FB1900155B5C /* RCTWKWebView.xcodeproj */, - B2A5F42F1DEC36B200174F4D /* RCTAnimation.xcodeproj */, - 3A7EB0491DD9CABC00A4FCC8 /* SplashScreen.xcodeproj */, - 20A5C94B1D927137002C4965 /* imageCropPicker.xcodeproj */, - 9E3AB6B21D87DA2A008846B4 /* React-Native-Webview-Bridge.xcodeproj */, - 146833FF1AC3E56700842450 /* React.xcodeproj */, - 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, - ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */, - 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, - 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, - 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, - 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, - 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, - 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, - 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, - 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, - 9F1854E6D9654226B1FC8308 /* RCTCamera.xcodeproj */, - 305F194186D848FDB07AF34C /* RNFS.xcodeproj */, - 46E2F6052EB44C698C680894 /* RNI18n.xcodeproj */, - F090E261B9854867A728CE4F /* RealmReact.xcodeproj */, - 38E1A2C8D0734EE99E2B16CE /* TcpSockets.xcodeproj */, - 439B6B4B407A4E2AACAFE5BE /* RCTStatus.xcodeproj */, - 5E5A7625B76441D984EA8C0D /* RCTImageResizer.xcodeproj */, - F3548417D8DA4362B6796A54 /* RNInstabug.xcodeproj */, - 1E74DC52A1E449A2BA858B14 /* RNSVG.xcodeproj */, - F89A8F8005874B86B63C22E3 /* RNFIRMessaging.xcodeproj */, - AF409BB755B94FB1BBA01927 /* RNSecureRandom.xcodeproj */, - ); - name = Libraries; - sourceTree = ""; - }; - 832341B11AAA6A8300B99B32 /* Products */ = { - isa = PBXGroup; - children = ( - 832341B51AAA6A8300B99B32 /* libRCTText.a */, - 9E3F1C011DDAE781005E4779 /* libRCTText-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; - 83CBB9F61A601CBA00E9B192 = { - isa = PBXGroup; - children = ( - 92A0DF491F4DE3A0002051BC /* GoogleService-Info.plist */, - 20B6B6861D92C42600CC5C6A /* QBImagePicker.framework */, - 20B6B6831D92C42600CC5C6A /* RSKImageCropper.framework */, - 13B07FAE1A68108700A75B9A /* StatusIm */, - 832341AE1AAA6A7D00B99B32 /* Libraries */, - 00E356EF1AD99517003FC87E /* StatusImTests */, - 83CBBA001A601CBA00E9B192 /* Products */, - A97BA941B2FB44B4B66EE6D3 /* Frameworks */, - 1E7837547A9A40E18AD63CF3 /* Resources */, - 5C1C8762251D6EF495FB2384 /* Pods */, - C90047C11FC47AF30002B8EA /* Recovered References */, - ); - indentWidth = 2; - sourceTree = ""; - tabWidth = 2; - usesTabs = 0; - }; - 83CBBA001A601CBA00E9B192 /* Products */ = { - isa = PBXGroup; - children = ( - 13B07F961A680F5B00A75B9A /* StatusIm.app */, - 00E356EE1AD99517003FC87E /* StatusImTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 922C4C211F4D5C3F0033C753 /* Products */ = { - isa = PBXGroup; - children = ( - 922C4C4B1F4D5C3F0033C753 /* libRNFIRMessaging.a */, - ); - name = Products; - sourceTree = ""; - }; - 92925B361F571DE600203EEB /* Products */ = { - isa = PBXGroup; - children = ( - 92925B681F571DE600203EEB /* libRNSVG.a */, - C90048231FC47AF50002B8EA /* libRNSVG-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; - 9880471820650CBC00CEBFE0 /* Products */ = { - isa = PBXGroup; - children = ( - 9880471C20650CBC00CEBFE0 /* libReactNativeTestFairy.a */, - ); - name = Products; - sourceTree = ""; - }; - 9E3AB6B31D87DA2A008846B4 /* Products */ = { - isa = PBXGroup; - children = ( - 9E3AB6C61D87DA2B008846B4 /* libReact-Native-Webview-Bridge.a */, - ); - name = Products; - sourceTree = ""; - }; - 9E3F1BE31DDAE781005E4779 /* Products */ = { - isa = PBXGroup; - children = ( - 9E3F1C121DDAE781005E4779 /* libRNInstabug.a */, - ); - name = Products; - sourceTree = ""; - }; - 9EC0135D1E06FB1900155B5C /* Products */ = { - isa = PBXGroup; - children = ( - 9EC013791E06FB1900155B5C /* libRCTWKWebView.a */, - ); - name = Products; - sourceTree = ""; - }; - 9EF083391F3B538A00876A8F /* Products */ = { - isa = PBXGroup; - children = ( - 9EF083611F3B538B00876A8F /* libReactNativeConfig.a */, - ); - name = Products; - sourceTree = ""; - }; - A97BA941B2FB44B4B66EE6D3 /* Frameworks */ = { + A97BA941B2FB44B4B66EE6D3 /* Frameworks */ = { isa = PBXGroup; children = ( + 65F693BD2578002500A45E76 /* CoreNFC.framework */, + 65F693BF2578003600A45E76 /* CoreNFC.framework */, + 3A8F8EA924A4D31600BF206D /* GameKit.framework */, 4C16DE0B1F89508700AA10DB /* JavaScriptCore.framework */, - 925C1F7F1F7B73C00063DFA0 /* FirebaseMessaging.framework */, - 925C1F401F7B73B20063DFA0 /* FirebaseCore.framework */, - 925C1F411F7B73B20063DFA0 /* FirebaseCoreDiagnostics.framework */, - 925C1F421F7B73B20063DFA0 /* FirebaseInstanceID.framework */, - 925C1F431F7B73B20063DFA0 /* FirebaseNanoPB.framework */, - 925C1F451F7B73B20063DFA0 /* nanopb.framework */, B24FC7FE1DE7195F00D694FF /* MessageUI.framework */, B24FC7FC1DE7195700D694FF /* Social.framework */, - 20A5C96E1D92716C002C4965 /* QBImagePicker.framework */, - 20A5C96C1D92715E002C4965 /* RSKImageCropper.framework */, - CE4E31B21D8695250033ED64 /* Statusgo.framework */, + CE4E31B21D8695250033ED64 /* Statusgo.xcframework */, 8B9A886A2CB448B1ABA0EB62 /* libc++.tbd */, 4E586E1B0E544F64AA9F5BD1 /* libz.tbd */, - 101A4045637A2ADF57D28EF5 /* libPods-StatusIm.a */, - FC1CBCFE6C906043D6CCEEE1 /* libPods-StatusImTests.a */, - 22D72275B7ADF9FC3304DCE5 /* Instabug.framework */, - EA36FD18BC2ECACF0A8B83DE /* InstabugCore.framework */, + E42DDCDBECE60E763EAA5042 /* libPods-Status-StatusIm.a */, + 6081A369F49E04EC54F77E1B /* libPods-Status-StatusIm-StatusImTests.a */, + C929A693823978C12D329338 /* libPods-Status-StatusImPR.a */, ); name = Frameworks; sourceTree = ""; }; - ADBDB9201DFEBF0600ED6528 /* Products */ = { - isa = PBXGroup; - children = ( - ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */, - C90047CD1FC47AF50002B8EA /* libRCTBlob-tvOS.a */, - ); - name = Products; - sourceTree = ""; - }; - B24FC7BB1DE718EA00D694FF /* Products */ = { + D0D5C8D06825D33BA2D2121E /* Pods */ = { isa = PBXGroup; children = ( - B24FC7F21DE718EF00D694FF /* libSplashScreen.a */, - ); - name = Products; - sourceTree = ""; - }; - B2A5F4301DEC36B200174F4D /* Products */ = { - isa = PBXGroup; - children = ( - C93242531FE1C68C00FE7099 /* libRCTAnimation.a */, - C93242551FE1C68C00FE7099 /* libRCTAnimation.a */, - ); - name = Products; - sourceTree = ""; - }; - B2DEA0A51E49E32000FA28D6 /* Products */ = { - isa = PBXGroup; - children = ( - B2DEA0B11E49E32000FA28D6 /* libRCTHttpServer.a */, - ); - name = Products; - sourceTree = ""; - }; - C20F3A6B20E5165A00757214 /* Products */ = { - isa = PBXGroup; - children = ( - C20F3A6F20E5165B00757214 /* libRNSecureRandom.a */, - ); - name = Products; - sourceTree = ""; - }; - C90047C11FC47AF30002B8EA /* Recovered References */ = { - isa = PBXGroup; - children = ( - 146834041AC3E56700842450 /* libReact.a */, - 674B3D9595A047AB8D518F4E /* libRNFS.a */, - 52F6ED6465184513A082652B /* libRNI18n.a */, - DF1CD4C3D1254774ACCAE4E8 /* libBVLinearGradient.a */, - 5535217F57E44D77AA9CF083 /* libRCTOrientation.a */, - 11632AA0A5F84F029DD91797 /* libRNVectorIcons.a */, - CD48A32459B64E96843BB238 /* libRealmReact.a */, - C2A4E93F6B154AEFA3485B45 /* libTcpSockets.a */, - 2BEE3436791D42248F853999 /* libRCTImageResizer.a */, - 4EC426A98043452BB6F9C134 /* libRNInstabug.a */, - AF2BF381BC7B4EB0887F0091 /* libRNSVG.a */, - 7F752F17B1E04216B1337A72 /* libRNFIRMessaging.a */, - 1464A9A9E12F45068947C28F /* libRNSecureRandom.a */, - ); - name = "Recovered References"; - sourceTree = ""; - }; - CE4E31941D8693090033ED64 /* Products */ = { - isa = PBXGroup; - children = ( - CE4E319E1D8693090033ED64 /* libRCTImageResizer.a */, - ); - name = Products; + 24F63BCFEB33B4C969305A46 /* Pods-Status-StatusIm.debug.xcconfig */, + 344F43B59CCE090C3271FF08 /* Pods-Status-StatusIm.release.xcconfig */, + F12F9820A9DF3FE08C615B90 /* Pods-Status-StatusIm-StatusImTests.debug.xcconfig */, + 203C46547A6E99D27529A8AA /* Pods-Status-StatusIm-StatusImTests.release.xcconfig */, + 70748FF05FFD3CACB6F64CE0 /* Pods-Status-StatusImPR.debug.xcconfig */, + 415E72A662B5ABF086AB7D7B /* Pods-Status-StatusImPR.release.xcconfig */, + ); + path = Pods; sourceTree = ""; }; /* End PBXGroup section */ @@ -1137,10 +346,12 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "StatusImTests" */; buildPhases = ( - 49A69B853BD9751F84878340 /* [CP] Check Pods Manifest.lock */, + D357C5313A85261FF3DAB74E /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, + 36051AE6B742BD851884AB9D /* [CP] Embed Pods Frameworks */, + 3506F0F77B5DE03815A5414A /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1156,42 +367,65 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "StatusIm" */; buildPhases = ( - 2EAC54E16AB243C3EBBFE1BA /* [CP] Check Pods Manifest.lock */, + D47DE8479514FAAC6B26ACD2 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 20B6B6891D92C42700CC5C6A /* Embed Frameworks */, E3914A731DF919ED00EBB515 /* Run Script */, - C5A3444E637B7B715148AD1B /* Embed Instabug Framework */, - D286FF71BA61530E740B7C82 /* Strip Frameworks */, + 82219E0CC0028DA612FCC0E3 /* [CP] Embed Pods Frameworks */, + 389504FC147AAF0616332C3A /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = StatusIm; - productName = "Hello World"; + productName = StatusIm; productReference = 13B07F961A680F5B00A75B9A /* StatusIm.app */; productType = "com.apple.product-type.application"; }; + 3AAD2AB924A3A60E0075D594 /* StatusImPR */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AAD2AD924A3A60E0075D594 /* Build configuration list for PBXNativeTarget "StatusImPR" */; + buildPhases = ( + E507D5CEF6BDEB9FBEBFA1C3 /* [CP] Check Pods Manifest.lock */, + 3AAD2ABB24A3A60E0075D594 /* Sources */, + 3AAD2ABF24A3A60E0075D594 /* Frameworks */, + 3AAD2AC924A3A60E0075D594 /* Resources */, + 3AAD2AD524A3A60E0075D594 /* Bundle React Native code and images */, + 3AAD2AD624A3A60E0075D594 /* Embed Frameworks */, + 3AAD2AD724A3A60E0075D594 /* Run Script */, + 13D9129EE9E3EFD2436F82FD /* [CP] Embed Pods Frameworks */, + E374B6B7AF37774C15097B39 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StatusImPR; + productName = StatusIm; + productReference = 3AAD2ADC24A3A60E0075D594 /* Status PR.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 830; - ORGANIZATIONNAME = Facebook; + LastUpgradeCheck = 1140; + ORGANIZATIONNAME = Status; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; - DevelopmentTeam = DTX7Z4U3YA; + DevelopmentTeam = 8B5X2M6H2Y; + ProvisioningStyle = Manual; TestTargetID = 13B07F861A680F5B00A75B9A; }; 13B07F861A680F5B00A75B9A = { - DevelopmentTeam = DTX7Z4U3YA; - ProvisioningStyle = Automatic; + LastSwiftMigration = 1140; SystemCapabilities = { com.apple.BackgroundModes = { enabled = 1; @@ -1207,561 +441,44 @@ }; }; }; + 3AAD2AB924A3A60E0075D594 = { + DevelopmentTeam = 8B5X2M6H2Y; + ProvisioningStyle = Manual; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "StatusIm" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); - mainGroup = 83CBB9F61A601CBA00E9B192; - productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; - projectDirPath = ""; - projectReferences = ( - { - ProductGroup = 20A5C94C1D927137002C4965 /* Products */; - ProjectRef = 20A5C94B1D927137002C4965 /* imageCropPicker.xcodeproj */; - }, - { - ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; - ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; - }, - { - ProductGroup = B2A5F4301DEC36B200174F4D /* Products */; - ProjectRef = B2A5F42F1DEC36B200174F4D /* RCTAnimation.xcodeproj */; - }, - { - ProductGroup = ADBDB9201DFEBF0600ED6528 /* Products */; - ProjectRef = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; - }, - { - ProductGroup = 20B7D1111D3F74CD00B70F14 /* Products */; - ProjectRef = 9F1854E6D9654226B1FC8308 /* RCTCamera.xcodeproj */; - }, - { - ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; - ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; - }, - { - ProductGroup = B2DEA0A51E49E32000FA28D6 /* Products */; - ProjectRef = B2DEA0A41E49E32000FA28D6 /* RCTHttpServer.xcodeproj */; - }, - { - ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; - ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; - }, - { - ProductGroup = CE4E31941D8693090033ED64 /* Products */; - ProjectRef = 5E5A7625B76441D984EA8C0D /* RCTImageResizer.xcodeproj */; - }, - { - ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; - ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; - }, - { - ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; - ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; - }, - { - ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; - ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; - }, - { - ProductGroup = 201067BA1D4789F700FA83B6 /* Products */; - ProjectRef = 439B6B4B407A4E2AACAFE5BE /* RCTStatus.xcodeproj */; - }, - { - ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; - ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; - }, - { - ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; - ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; - }, - { - ProductGroup = 139FDEE71B06529A00C62182 /* Products */; - ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; - }, - { - ProductGroup = 9EC0135D1E06FB1900155B5C /* Products */; - ProjectRef = 9EC0135C1E06FB1900155B5C /* RCTWKWebView.xcodeproj */; - }, - { - ProductGroup = 9E3AB6B31D87DA2A008846B4 /* Products */; - ProjectRef = 9E3AB6B21D87DA2A008846B4 /* React-Native-Webview-Bridge.xcodeproj */; - }, - { - ProductGroup = 146834001AC3E56700842450 /* Products */; - ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; - }, - { - ProductGroup = 9EF083391F3B538A00876A8F /* Products */; - ProjectRef = 9EF083381F3B538A00876A8F /* ReactNativeConfig.xcodeproj */; - }, - { - ProductGroup = 9880471820650CBC00CEBFE0 /* Products */; - ProjectRef = 9880471720650CBC00CEBFE0 /* ReactNativeTestFairy.xcodeproj */; - }, - { - ProductGroup = 20B7D1251D3F74CD00B70F14 /* Products */; - ProjectRef = F090E261B9854867A728CE4F /* RealmReact.xcodeproj */; - }, - { - ProductGroup = 922C4C211F4D5C3F0033C753 /* Products */; - ProjectRef = F89A8F8005874B86B63C22E3 /* RNFIRMessaging.xcodeproj */; - }, - { - ProductGroup = 20B7D1161D3F74CD00B70F14 /* Products */; - ProjectRef = 305F194186D848FDB07AF34C /* RNFS.xcodeproj */; - }, - { - ProductGroup = 20B7D1041D3F74CD00B70F14 /* Products */; - ProjectRef = 46E2F6052EB44C698C680894 /* RNI18n.xcodeproj */; - }, - { - ProductGroup = 9E3F1BE31DDAE781005E4779 /* Products */; - ProjectRef = F3548417D8DA4362B6796A54 /* RNInstabug.xcodeproj */; - }, - { - ProductGroup = C20F3A6B20E5165A00757214 /* Products */; - ProjectRef = AF409BB755B94FB1BBA01927 /* RNSecureRandom.xcodeproj */; - }, - { - ProductGroup = 92925B361F571DE600203EEB /* Products */; - ProjectRef = 1E74DC52A1E449A2BA858B14 /* RNSVG.xcodeproj */; - }, - { - ProductGroup = B24FC7BB1DE718EA00D694FF /* Products */; - ProjectRef = 3A7EB0491DD9CABC00A4FCC8 /* SplashScreen.xcodeproj */; - }, - { - ProductGroup = 201067551D477F5E00FA83B6 /* Products */; - ProjectRef = 38E1A2C8D0734EE99E2B16CE /* TcpSockets.xcodeproj */; - }, - ); - projectRoot = ""; - targets = ( - 13B07F861A680F5B00A75B9A /* StatusIm */, - 00E356ED1AD99517003FC87E /* StatusImTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXReferenceProxy section */ - 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTActionSheet.a; - remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTGeolocation.a; - remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTImage.a; - remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTNetwork.a; - remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTVibration.a; - remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTSettings.a; - remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTWebSocket.a; - remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 146834041AC3E56700842450 /* libReact.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libReact.a; - remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 2010676D1D477F5E00FA83B6 /* libTcpSockets.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libTcpSockets.a; - remoteRef = 2010676C1D477F5E00FA83B6 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 201067C41D4789F700FA83B6 /* libRCTStatus.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTStatus.a; - remoteRef = 201067C31D4789F700FA83B6 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 20A5C9531D927137002C4965 /* libimageCropPicker.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libimageCropPicker.a; - remoteRef = 20A5C9521D927137002C4965 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 20B7D10E1D3F74CD00B70F14 /* libRNI18n.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNI18n.a; - remoteRef = 20B7D10D1D3F74CD00B70F14 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 20B7D1151D3F74CD00B70F14 /* libRCTCamera.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTCamera.a; - remoteRef = 20B7D1141D3F74CD00B70F14 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 20B7D11A1D3F74CD00B70F14 /* libRNFS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNFS.a; - remoteRef = 20B7D1191D3F74CD00B70F14 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 20B7D1291D3F74CD00B70F14 /* libRealmReact.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRealmReact.a; - remoteRef = 20B7D1281D3F74CD00B70F14 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 5537E2AA20650E640085CED4 /* libjsinspector.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libjsinspector.a; - remoteRef = 5537E2A920650E640085CED4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 5537E2AC20650E640085CED4 /* libjsinspector-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libjsinspector-tvOS.a"; - remoteRef = 5537E2AB20650E640085CED4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTLinking.a; - remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTText.a; - remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 922C4C4B1F4D5C3F0033C753 /* libRNFIRMessaging.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNFIRMessaging.a; - remoteRef = 922C4C4A1F4D5C3F0033C753 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 92925B681F571DE600203EEB /* libRNSVG.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNSVG.a; - remoteRef = 92925B671F571DE600203EEB /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9880471C20650CBC00CEBFE0 /* libReactNativeTestFairy.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libReactNativeTestFairy.a; - remoteRef = 9880471B20650CBC00CEBFE0 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3AB6C61D87DA2B008846B4 /* libReact-Native-Webview-Bridge.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libReact-Native-Webview-Bridge.a"; - remoteRef = 9E3AB6C51D87DA2B008846B4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3F1BEE1DDAE781005E4779 /* libRCTImage-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRCTImage-tvOS.a"; - remoteRef = 9E3F1BED1DDAE781005E4779 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3F1BF31DDAE781005E4779 /* libRCTLinking-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRCTLinking-tvOS.a"; - remoteRef = 9E3F1BF21DDAE781005E4779 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3F1BF71DDAE781005E4779 /* libRCTNetwork-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRCTNetwork-tvOS.a"; - remoteRef = 9E3F1BF61DDAE781005E4779 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3F1BFC1DDAE781005E4779 /* libRCTSettings-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRCTSettings-tvOS.a"; - remoteRef = 9E3F1BFB1DDAE781005E4779 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3F1C011DDAE781005E4779 /* libRCTText-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRCTText-tvOS.a"; - remoteRef = 9E3F1C001DDAE781005E4779 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3F1C061DDAE781005E4779 /* libRCTWebSocket-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRCTWebSocket-tvOS.a"; - remoteRef = 9E3F1C051DDAE781005E4779 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9E3F1C121DDAE781005E4779 /* libRNInstabug.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNInstabug.a; - remoteRef = 9E3F1C111DDAE781005E4779 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9EC013791E06FB1900155B5C /* libRCTWKWebView.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTWKWebView.a; - remoteRef = 9EC013781E06FB1900155B5C /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9EF083611F3B538B00876A8F /* libReactNativeConfig.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libReactNativeConfig.a; - remoteRef = 9EF083601F3B538B00876A8F /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTBlob.a; - remoteRef = ADBDB9261DFEBF0700ED6528 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - B24FC7F21DE718EF00D694FF /* libSplashScreen.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libSplashScreen.a; - remoteRef = B24FC7F11DE718EF00D694FF /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - B2DEA0B11E49E32000FA28D6 /* libRCTHttpServer.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTHttpServer.a; - remoteRef = B2DEA0B01E49E32000FA28D6 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C20F3A6F20E5165B00757214 /* libRNSecureRandom.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNSecureRandom.a; - remoteRef = C20F3A6E20E5165B00757214 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90047CD1FC47AF50002B8EA /* libRCTBlob-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRCTBlob-tvOS.a"; - remoteRef = C90047CC1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90047E91FC47AF50002B8EA /* libfishhook.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libfishhook.a; - remoteRef = C90047E81FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90047EB1FC47AF50002B8EA /* libfishhook-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libfishhook-tvOS.a"; - remoteRef = C90047EA1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90047FC1FC47AF50002B8EA /* libReact.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libReact.a; - remoteRef = C90047FB1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90047FE1FC47AF50002B8EA /* libReact.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libReact.a; - remoteRef = C90047FD1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048001FC47AF50002B8EA /* libyoga.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libyoga.a; - remoteRef = C90047FF1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048021FC47AF50002B8EA /* libyoga.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libyoga.a; - remoteRef = C90048011FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048041FC47AF50002B8EA /* libcxxreact.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libcxxreact.a; - remoteRef = C90048031FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048061FC47AF50002B8EA /* libcxxreact.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libcxxreact.a; - remoteRef = C90048051FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048081FC47AF50002B8EA /* libjschelpers.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libjschelpers.a; - remoteRef = C90048071FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C900480A1FC47AF50002B8EA /* libjschelpers.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libjschelpers.a; - remoteRef = C90048091FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C900480C1FC47AF50002B8EA /* libthird-party.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libthird-party.a"; - remoteRef = C900480B1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C900480E1FC47AF50002B8EA /* libthird-party.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libthird-party.a"; - remoteRef = C900480D1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048101FC47AF50002B8EA /* libdouble-conversion.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libdouble-conversion.a"; - remoteRef = C900480F1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048121FC47AF50002B8EA /* libdouble-conversion.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libdouble-conversion.a"; - remoteRef = C90048111FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048141FC47AF50002B8EA /* libprivatedata.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libprivatedata.a; - remoteRef = C90048131FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048161FC47AF50002B8EA /* libprivatedata-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libprivatedata-tvOS.a"; - remoteRef = C90048151FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C900481D1FC47AF50002B8EA /* libRNFS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNFS.a; - remoteRef = C900481C1FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C90048231FC47AF50002B8EA /* libRNSVG-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRNSVG-tvOS.a"; - remoteRef = C90048221FC47AF50002B8EA /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C93242531FE1C68C00FE7099 /* libRCTAnimation.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTAnimation.a; - remoteRef = C93242521FE1C68C00FE7099 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C93242551FE1C68C00FE7099 /* libRCTAnimation.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTAnimation.a; - remoteRef = C93242541FE1C68C00FE7099 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C9B824E91FC713C800D277C6 /* libRNI18n-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libRNI18n-tvOS.a"; - remoteRef = C9B824E81FC713C800D277C6 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 9EC0135D1E06FB1900155B5C /* Products */; + ProjectRef = 9EC0135C1E06FB1900155B5C /* RCTWKWebView.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* StatusIm */, + 00E356ED1AD99517003FC87E /* StatusImTests */, + 3AAD2AB924A3A60E0075D594 /* StatusImPR */, + ); }; - CE4E319E1D8693090033ED64 /* libRCTImageResizer.a */ = { +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 9EC013791E06FB1900155B5C /* libRCTWKWebView.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; - path = libRCTImageResizer.a; - remoteRef = CE4E319D1D8693090033ED64 /* PBXContainerItemProxy */; + path = libRCTWKWebView.a; + remoteRef = 9EC013781E06FB1900155B5C /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ @@ -1771,6 +488,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2BE1270F2D59242300023E5E /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1778,19 +496,45 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9ED2F4611D9D579900B36508 /* SF-UI-Text-Bold.otf in Resources */, - 9E0B01A11DDC5DA7002B0359 /* SF-UI-Text-Light.otf in Resources */, - 9ED2F45E1D9D535A00B36508 /* SF-UI-Text-Regular.otf in Resources */, + C14C5F9329C0ADB5005C58A7 /* launch-icon.png in Resources */, 74B758FC20D7C00B003343C3 /* launch-image-universal.storyboard in Resources */, - 9ED2F45F1D9D535A00B36508 /* SF-UI-Text-Medium.otf in Resources */, - 2028DFFA1D4275B600227DCD /* SF-UI-Display-Regular.otf in Resources */, - 2028DFF91D4275B600227DCD /* SF-UI-Display-Medium.otf in Resources */, - F9238D6C1E5F055900C047B9 /* SF-UI-Text-Semibold.otf in Resources */, - 2028DFFC1D4275B600227DCD /* SF-UI-Display-Thin.otf in Resources */, - 2028DFFB1D4275B600227DCD /* SF-UI-Display-Semibold.otf in Resources */, - B23B48FF1E76917B006D4535 /* RobotoMono-Medium.ttf in Resources */, + 715D8132290BE850006F5C88 /* UbuntuMono-Regular.ttf in Resources */, + C14C5F9129C0AD9C005C58A7 /* launch-icon@2x.png in Resources */, B2F2D1BC1D9D531B00B7B453 /* Images.xcassets in Resources */, - 92A0DF7D1F4DE3A4002051BC /* GoogleService-Info.plist in Resources */, + D84616FB563A48EBB1678699 /* Inter-Bold.otf in Resources */, + D99C50E5E18942A39C8DDF61 /* Inter-BoldItalic.otf in Resources */, + 3ABC7AF8245FF85900612C45 /* InterStatus-Regular.otf in Resources */, + 393D26E3080B443A998F4A2F /* Inter-Italic.otf in Resources */, + 2BE1270E2D59242300023E5E /* GoogleService-Info.plist in Resources */, + D1786306E0184916B11F4C37 /* Inter-Medium.otf in Resources */, + 57C854A7993C47A3B1AECD32 /* Inter-MediumItalic.otf in Resources */, + 70ADBB5ECF934DCF8A0E4919 /* Inter-Regular.otf in Resources */, + 3870E1E692E24133A80B07DE /* Inter-SemiBold.otf in Resources */, + 8391E8E0E93C41A98AAA6631 /* Inter-SemiBoldItalic.otf in Resources */, + C14C5F8D29C0A149005C58A7 /* launch-icon@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AAD2AC924A3A60E0075D594 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AAD2ACA24A3A60E0075D594 /* launch-image-universal.storyboard in Resources */, + 715D8133290BE850006F5C88 /* UbuntuMono-Regular.ttf in Resources */, + 3AAD2ACB24A3A60E0075D594 /* Images.xcassets in Resources */, + 3AAD2ACC24A3A60E0075D594 /* Inter-Bold.otf in Resources */, + 3AAD2ACD24A3A60E0075D594 /* Inter-BoldItalic.otf in Resources */, + C1715FFA29C0BCE10088FA8B /* launch-icon@2x.png in Resources */, + 3AAD2ACE24A3A60E0075D594 /* InterStatus-Regular.otf in Resources */, + 3AAD2ACF24A3A60E0075D594 /* Inter-Italic.otf in Resources */, + 3AAD2AD024A3A60E0075D594 /* Inter-Medium.otf in Resources */, + 2BE127102D59242300023E5E /* GoogleService-Info.plist in Resources */, + 3AAD2AD124A3A60E0075D594 /* Inter-MediumItalic.otf in Resources */, + 3AAD2AD224A3A60E0075D594 /* Inter-Regular.otf in Resources */, + C1715FFC29C0BCE80088FA8B /* launch-icon.png in Resources */, + 3AAD2AD324A3A60E0075D594 /* Inter-SemiBold.otf in Resources */, + 3AAD2AD424A3A60E0075D594 /* Inter-SemiBoldItalic.otf in Resources */, + C1715FFB29C0BCE50088FA8B /* launch-icon@3x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1808,57 +552,266 @@ outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; + shellPath = "/usr/bin/env sh"; + shellScript = "set -o errexit\nexport NODE_BINARY=\"${NODE_BINARY:-node}\"\nexport NODE_ARGS=\"${NODE_ARGS:- --max-old-space-size=16384 }\"\n\n\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n"; + }; + 13D9129EE9E3EFD2436F82FD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3506F0F77B5DE03815A5414A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm-StatusImTests/Pods-Status-StatusIm-StatusImTests-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseMessaging/FirebaseMessaging_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNPermissions/RNPermissionsPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseMessaging_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNPermissionsPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm-StatusImTests/Pods-Status-StatusIm-StatusImTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 36051AE6B742BD851884AB9D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm-StatusImTests/Pods-Status-StatusIm-StatusImTests-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm-StatusImTests/Pods-Status-StatusIm-StatusImTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 389504FC147AAF0616332C3A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm/Pods-Status-StatusIm-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseMessaging/FirebaseMessaging_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNPermissions/RNPermissionsPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseMessaging_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNPermissionsPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm/Pods-Status-StatusIm-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3AAD2AD524A3A60E0075D594 /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = "/usr/bin/env sh"; + shellScript = "set -o errexit\nexport NODE_BINARY=\"${NODE_BINARY:-node}\"\nexport NODE_ARGS=\"${NODE_ARGS:- --max-old-space-size=16384 }\"\n\n\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n"; + }; + 3AAD2AD724A3A60E0075D594 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 8; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 1; + shellPath = "/usr/bin/env sh"; + shellScript = "\"${PROJECT_DIR}/scripts/set_xcode_version.sh\" > ../logs/set_xcode_version.log 2>&1\n"; + }; + 82219E0CC0028DA612FCC0E3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm/Pods-Status-StatusIm-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -o errexit\nexport NODE_BINARY=\"node --max-old-space-size=4096\"\n../node_modules/react-native/scripts/react-native-xcode.sh"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm/Pods-Status-StatusIm-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 2EAC54E16AB243C3EBBFE1BA /* [CP] Check Pods Manifest.lock */ = { + D357C5313A85261FF3DAB74E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-StatusIm-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Status-StatusIm-StatusImTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 49A69B853BD9751F84878340 /* [CP] Check Pods Manifest.lock */ = { + D47DE8479514FAAC6B26ACD2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-StatusImTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Status-StatusIm-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - D286FF71BA61530E740B7C82 /* Strip Frameworks */ = { + E374B6B7AF37774C15097B39 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - ); - name = "Strip Frameworks"; + "${PODS_ROOT}/Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseMessaging/FirebaseMessaging_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNPermissions/RNPermissionsPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseMessaging_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNPermissionsPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "bash \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/InstabugCore.framework/strip-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR-resources.sh\"\n"; + showEnvVarsInLog = 0; }; E3914A731DF919ED00EBB515 /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -1871,8 +824,30 @@ outputPaths = ( ); runOnlyForDeploymentPostprocessing = 1; + shellPath = "/usr/bin/env sh"; + shellScript = "\"${PROJECT_DIR}/scripts/set_xcode_version.sh\" > ../logs/set_xcode_version.log 2>&1\n"; + }; + E507D5CEF6BDEB9FBEBFA1C3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Status-StatusImPR-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#!/bin/bash\n\nsource \"${PROJECT_DIR}/scripts/set_xcode_version.sh\""; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -1881,6 +856,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 65F6941A25780A4F00A45E76 /* Bridge.swift in Sources */, 00E356F31AD99517003FC87E /* StatusImTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1889,11 +865,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 65F6941925780A4F00A45E76 /* Bridge.swift in Sources */, + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 3A2626CF245C3F2200D5F94B /* Dummy.swift in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 3AAD2ABB24A3A60E0075D594 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 65F6941B25780A4F00A45E76 /* Bridge.swift in Sources */, + 3AAD2ABC24A3A60E0075D594 /* AppDelegate.mm in Sources */, + 3AAD2ABD24A3A60E0075D594 /* Dummy.swift in Sources */, + 3AAD2ABE24A3A60E0075D594 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1907,11 +896,14 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 064351D2FD901F8B6C9AE2A5 /* Pods-StatusImTests.debug.xcconfig */; + baseConfigurationReference = F12F9820A9DF3FE08C615B90 /* Pods-Status-StatusIm-StatusImTests.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_ID_SUFFIX = .debug; BUNDLE_LOADER = "$(TEST_HOST)"; - DEVELOPMENT_TEAM = DTX7Z4U3YA; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", @@ -1921,160 +913,161 @@ "$(inherited)", ); INFOPLIST_FILE = StatusImTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", + "@executable_path/Frameworks", + "@loader_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "StatusImTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StatusIm.app/StatusIm"; }; name = Debug; }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D489EE8D5F52DA10AC715727 /* Pods-StatusImTests.release.xcconfig */; + baseConfigurationReference = 203C46547A6E99D27529A8AA /* Pods-Status-StatusIm-StatusImTests.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_ID_SUFFIX = ""; BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; COPY_PHASE_STRIP = NO; - DEVELOPMENT_TEAM = DTX7Z4U3YA; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", ); INFOPLIST_FILE = StatusImTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", + "@executable_path/Frameworks", + "@loader_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "StatusImTests-Bridging-Header.h"; + SWIFT_VERSION = 5.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StatusIm.app/StatusIm"; }; name = Release; }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = ACA66A8F16CD2FE21F38738B /* Pods-StatusIm.debug.xcconfig */; + baseConfigurationReference = 24F63BCFEB33B4C969305A46 /* Pods-Status-StatusIm.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; BUNDLE_ID_SUFFIX = .debug; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = StatusIm/StatusIm.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CUSTOM_PRODUCT_NAME = "Status Debug"; - DEAD_CODE_STRIPPING = NO; - DEVELOPMENT_TEAM = DTX7Z4U3YA; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; + EXCLUDED_ARCHS = ""; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; FRAMEWORK_SEARCH_PATHS = ( "$(PROJECT_DIR)/../modules/react-native-status/ios/RCTStatus", "$(inherited)", "$(PROJECT_DIR)/Pods/**", "$(PROJECT_DIR)", - "../node_modules/instabug-reactnative/ios", ); GCC_PRECOMPILE_PREFIX_HEADER = NO; GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "$(inherited)", + "SQLITE_HAS_CODEC=1", + ); HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", "$(SRCROOT)/../node_modules/react-native-camera/ios", "$(SRCROOT)/../node_modules/react-native-fs/**", - "$(SRCROOT)/../node_modules/react-native-i18n/RNI18n", - "$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient", - "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", - "$(SRCROOT)/../node_modules/realm/src/**", - "$(SRCROOT)/../node_modules/react-native-tcp/ios/**", + "$(SRCROOT)/../node_modules/react-native-languages/RNLanguages", "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", - "$(SRCROOT)/../node_modules/instabug-reactnative/ios/RNInstabug", - "$(SRCROOT)/../node_modules/react-native-splash-screen/ios", + "$(SRCROOT)/../node_modules/react-native-lottie-splash-screen/ios", "$(SRCROOT)/../node_modules/react-native-config/ios/**", - "$(SRCROOT)/../node_modules/react-native-svg/ios/**", - "$(SRCROOT)/../node_modules/react-native-fcm/ios", - "$(SRCROOT)/../node_modules/react-native-testfairy/ios", - "$(SRCROOT)/../node_modules/react-native-securerandom/ios", + "$(SRCROOT)/../node_modules/react-native-webview/ios", "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS", ); INFOPLIST_FILE = StatusIm/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", "$(inherited)", "$(PROJECT_DIR)", ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); OTHER_LDFLAGS = ( "$(inherited)", "-lc++", "-ObjC", + "-lresolv", ); PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum; PRODUCT_NAME = StatusIm; - TARGETED_DEVICE_FAMILY = "1,2"; - VALID_ARCHS = "arm64 armv7 armv7s"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "StatusIm-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.2; + TARGETED_DEVICE_FAMILY = 1; + VALID_ARCHS = "x86_64 arm64"; + "VALID_ARCHS[sdk=iphonesimulator*]" = "x86_64 "; }; name = Debug; }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 38A44830EC5708E89387F641 /* Pods-StatusIm.release.xcconfig */; + baseConfigurationReference = 344F43B59CCE090C3271FF08 /* Pods-Status-StatusIm.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; BUNDLE_ID_SUFFIX = ""; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = StatusIm/StatusIm.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; CUSTOM_PRODUCT_NAME = Status; - DEAD_CODE_STRIPPING = NO; - DEVELOPMENT_TEAM = DTX7Z4U3YA; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; + EXCLUDED_ARCHS = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; FRAMEWORK_SEARCH_PATHS = ( "$(PROJECT_DIR)/../modules/react-native-status/ios/RCTStatus", "$(inherited)", "$(PROJECT_DIR)/Pods/**", "$(PROJECT_DIR)", - "../node_modules/instabug-reactnative/ios", ); GCC_PRECOMPILE_PREFIX_HEADER = NO; GCC_PREFIX_HEADER = ""; @@ -2083,68 +1076,245 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", "$(SRCROOT)/../node_modules/react-native-camera/ios", + "$(SRCROOT)/../node_modules/react-native-webview/ios", "$(SRCROOT)/../node_modules/react-native-fs/**", - "$(SRCROOT)/../node_modules/react-native-i18n/RNI18n", - "$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient", - "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", - "$(SRCROOT)/../node_modules/realm/src/**", - "$(SRCROOT)/../node_modules/react-native-tcp/ios/**", + "$(SRCROOT)/../node_modules/react-native-languages/RNLanguages", "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", - "$(SRCROOT)/../node_modules/instabug-reactnative/ios/RNInstabug", - "$(SRCROOT)/../node_modules/react-native-splash-screen/ios", + "$(SRCROOT)/../node_modules/react-native-lottie-splash-screen/ios", "$(SRCROOT)/../node_modules/react-native-config/ios/**", - "$(SRCROOT)/../node_modules/react-native-svg/ios/**", - "$(SRCROOT)/../node_modules/react-native-fcm/ios", - "$(SRCROOT)/../node_modules/react-native-testfairy/ios", - "$(SRCROOT)/../node_modules/react-native-securerandom/ios", "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS", ); INFOPLIST_FILE = StatusIm/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", "$(inherited)", "$(PROJECT_DIR)", ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); OTHER_LDFLAGS = ( "$(inherited)", "-lc++", "-ObjC", + "-lresolv", ); PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum; PRODUCT_NAME = StatusIm; - TARGETED_DEVICE_FAMILY = "1,2"; - VALID_ARCHS = "armv7 armv7s arm64"; + PROVISIONING_PROFILE = "e2202b12-7a66-4ff7-af3c-a52e35f32dc1"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc im.status.ethereum"; + SWIFT_OBJC_BRIDGING_HEADER = "StatusIm-Bridging-Header.h"; + SWIFT_VERSION = 5.2; + TARGETED_DEVICE_FAMILY = 1; + VALID_ARCHS = arm64; + }; + name = Release; + }; + 3AAD2ADA24A3A60E0075D594 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 70748FF05FFD3CACB6F64CE0 /* Pods-Status-StatusImPR.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; + BUNDLE_ID_SUFFIX = .debug; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = StatusImPR/StatusImPR.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CUSTOM_PRODUCT_NAME = "Status Debug"; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../modules/react-native-status/ios/RCTStatus", + "$(inherited)", + "$(PROJECT_DIR)/Pods/**", + "$(PROJECT_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "$(inherited)", + "SQLITE_HAS_CODEC=1", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../node_modules/react-native-camera/ios", + "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-languages/RNLanguages", + "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", + "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", + "$(SRCROOT)/../node_modules/react-native-lottie-splash-screen/ios", + "$(SRCROOT)/../node_modules/react-native-config/ios/**", + "$(SRCROOT)/../node_modules/react-native-webview/ios", + "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS", + ); + INFOPLIST_FILE = StatusImPR/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "$(inherited)", + "$(PROJECT_DIR)", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lc++", + "-ObjC", + "-lresolv", + ); + PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum.pr; + PRODUCT_NAME = "Status PR"; + PROVISIONING_PROFILE_SPECIFIER = "match Development im.status.ethereum.pr"; + SWIFT_OBJC_BRIDGING_HEADER = "StatusIm-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.2; + TARGETED_DEVICE_FAMILY = 1; + VALID_ARCHS = arm64; + }; + name = Debug; + }; + 3AAD2ADB24A3A60E0075D594 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 415E72A662B5ABF086AB7D7B /* Pods-Status-StatusImPR.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIconPR$(BUNDLE_ID_SUFFIX)"; + BUNDLE_ID_SUFFIX = ""; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = StatusImPR/StatusImPR.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CUSTOM_PRODUCT_NAME = Status; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../modules/react-native-status/ios/RCTStatus", + "$(inherited)", + "$(PROJECT_DIR)/Pods/**", + "$(PROJECT_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../node_modules/react-native-camera/ios", + "$(SRCROOT)/../node_modules/react-native-webview/ios", + "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-languages/RNLanguages", + "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", + "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", + "$(SRCROOT)/../node_modules/react-native-lottie-splash-screen/ios", + "$(SRCROOT)/../node_modules/react-native-config/ios/**", + "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS", + ); + INFOPLIST_FILE = StatusImPR/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "$(inherited)", + "$(PROJECT_DIR)", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lc++", + "-ObjC", + "-lresolv", + ); + PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum.pr; + PRODUCT_NAME = "Status PR"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc im.status.ethereum.pr"; + SWIFT_OBJC_BRIDGING_HEADER = "StatusIm-Bridging-Header.h"; + SWIFT_VERSION = 5.2; + TARGETED_DEVICE_FAMILY = 1; + VALID_ARCHS = arm64; }; name = Release; }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - DEVELOPMENT_TEAM = DTX7Z4U3YA; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + EXCLUDED_ARCHS = ""; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -2153,6 +1323,7 @@ GCC_PREFIX_HEADER = ""; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", + "FB_SONARKIT_ENABLED=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; @@ -2167,51 +1338,81 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", "$(SRCROOT)/../node_modules/react-native-camera/ios", + "$(SRCROOT)/../node_modules/react-native-webview/ios", "$(SRCROOT)/../node_modules/react-native-fs/**", - "$(SRCROOT)/../node_modules/react-native-i18n/RNI18n", - "$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient", - "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", - "$(SRCROOT)/../node_modules/realm/src/**", - "$(SRCROOT)/../node_modules/react-native-tcp/ios/**", - "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", + "$(SRCROOT)/../node_modules/react-native-languages/RNLanguages", "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", - "$(SRCROOT)/../node_modules/instabug-reactnative/ios/RNInstabug", - "$(SRCROOT)/../node_modules/react-native-splash-screen/ios", + "$(SRCROOT)/../node_modules/react-native-lottie-splash-screen/ios/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "$(inherited)", + " ", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); + OTHER_LDFLAGS = "$(inherited)"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + USE_HERMES = true; + VALID_ARCHS = arm64; + "VALID_ARCHS[sdk=iphonesimulator*]" = x86_64; }; name = Debug; }; 83CBBA211A601CBA00E9B192 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = YES; - DEVELOPMENT_TEAM = DTX7Z4U3YA; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8B5X2M6H2Y; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + EXCLUDED_ARCHS = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_PRECOMPILE_PREFIX_HEADER = NO; @@ -2227,24 +1428,38 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", "$(SRCROOT)/../node_modules/react-native-camera/ios", + "$(SRCROOT)/../node_modules/react-native-webview/ios", "$(SRCROOT)/../node_modules/react-native-fs/**", - "$(SRCROOT)/../node_modules/react-native-i18n/RNI18n", - "$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient", - "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", - "$(SRCROOT)/../node_modules/realm/src/**", - "$(SRCROOT)/../node_modules/react-native-tcp/ios/**", - "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", + "$(SRCROOT)/../node_modules/react-native-languages/RNLanguages", "$(SRCROOT)/../modules/react-native-status/ios/RCTStatus/**", "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", - "$(SRCROOT)/../node_modules/instabug-reactnative/ios/RNInstabug", - "$(SRCROOT)/../node_modules/react-native-splash-screen/ios", + "$(SRCROOT)/../node_modules/react-native-lottie-splash-screen/ios/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "$(inherited)", + " ", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); + OTHER_LDFLAGS = "$(inherited)"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + USE_HERMES = true; VALIDATE_PRODUCT = YES; - VALID_ARCHS = "armv7 armv7s arm64"; + VALID_ARCHS = arm64; }; name = Release; }; @@ -2269,6 +1484,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 3AAD2AD924A3A60E0075D594 /* Build configuration list for PBXNativeTarget "StatusImPR" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AAD2ADA24A3A60E0075D594 /* Debug */, + 3AAD2ADB24A3A60E0075D594 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "StatusIm" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/StatusIm.xcodeproj/xcshareddata/xcschemes/StatusIm.xcscheme b/ios/StatusIm.xcodeproj/xcshareddata/xcschemes/StatusIm.xcscheme index 9dfd2ede6ef..399f89fdd62 100644 --- a/ios/StatusIm.xcodeproj/xcshareddata/xcschemes/StatusIm.xcscheme +++ b/ios/StatusIm.xcodeproj/xcshareddata/xcschemes/StatusIm.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -67,17 +76,6 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/StatusIm.xcworkspace/contents.xcworkspacedata b/ios/StatusIm.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..04a8ab389e5 --- /dev/null +++ b/ios/StatusIm.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/StatusIm/AppDelegate.h b/ios/StatusIm/AppDelegate.h index 14ef57dbd26..590610423b6 100644 --- a/ios/StatusIm/AppDelegate.h +++ b/ios/StatusIm/AppDelegate.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7,12 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import #import +#import -@import UserNotifications; - -@interface AppDelegate : UIResponder - -@property (nonatomic, strong) UIWindow *window; +@interface AppDelegate : RCTAppDelegate @end diff --git a/ios/StatusIm/AppDelegate.m b/ios/StatusIm/AppDelegate.m deleted file mode 100644 index afaed3a5207..00000000000 --- a/ios/StatusIm/AppDelegate.m +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "AppDelegate.h" - -#import -#import "ReactNativeConfig.h" -#import "React/RCTLog.h" -#import "RCTBundleURLProvider.h" -#import "RCTRootView.h" -#import "SplashScreen.h" -#import "TestFairy.h" -#import "RNFIRMessaging.h" -#import "RCTLinkingManager.h" - -@implementation AppDelegate - -/* Modified version of RCTDefaultLogFunction that also directs all app logs to TestFairy. */ -RCTLogFunction RCTTestFairyLogFunction = ^( - RCTLogLevel level, - __unused RCTLogSource source, - NSString *fileName, - NSNumber *lineNumber, - NSString *message - ) -{ - NSString *log = RCTFormatLog([NSDate date], level, fileName, lineNumber, message); - fprintf(stderr, "%s\n", log.UTF8String); - fflush(stderr); - - /* Only custom part */ - TFLog(log); - - int aslLevel; - switch(level) { - case RCTLogLevelTrace: - aslLevel = ASL_LEVEL_DEBUG; - break; - case RCTLogLevelInfo: - aslLevel = ASL_LEVEL_NOTICE; - break; - case RCTLogLevelWarning: - aslLevel = ASL_LEVEL_WARNING; - break; - case RCTLogLevelError: - aslLevel = ASL_LEVEL_ERR; - break; - case RCTLogLevelFatal: - aslLevel = ASL_LEVEL_CRIT; - break; - } - asl_log(NULL, NULL, aslLevel, "%s", message.UTF8String); - -}; - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ - signal(SIGPIPE, SIG_IGN); - NSURL *jsCodeLocation; - - /* Set logging level from React Native */ - NSString *logLevel = [ReactNativeConfig envFor:@"LOG_LEVEL"]; - if([logLevel isEqualToString:@"error"]){ - RCTSetLogThreshold(RCTLogLevelError); - } - else if([logLevel isEqualToString:@"warn"]){ - RCTSetLogThreshold(RCTLogLevelWarning); - } - else if([logLevel isEqualToString:@"info"]){ - RCTSetLogThreshold(RCTLogLevelInfo); - } - else if([logLevel isEqualToString:@"debug"]){ - RCTSetLogThreshold(RCTLogLevelTrace); - RCTSetLogFunction(RCTTestFairyLogFunction); - } - - jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; - - RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation - moduleName:@"StatusIm" - initialProperties:nil - launchOptions:launchOptions]; - rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; - - self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [UIViewController new]; - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; - [self.window makeKeyAndVisible]; - [SplashScreen show]; - - [FIRApp configure]; - [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self]; - - return YES; -} - - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler -{ -[RNFIRMessaging willPresentNotification:notification withCompletionHandler:completionHandler]; -} - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler -{ -[RNFIRMessaging didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; -} - --(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { -[RNFIRMessaging didReceiveLocalNotification:notification]; -} - -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{ -[RNFIRMessaging didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; -} - -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options -{ - return [RCTLinkingManager application:application openURL:url options:options]; -} - - -- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler -{ - return [RCTLinkingManager application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; -} - -@end diff --git a/ios/StatusIm/AppDelegate.mm b/ios/StatusIm/AppDelegate.mm new file mode 100644 index 00000000000..f917e900985 --- /dev/null +++ b/ios/StatusIm/AppDelegate.mm @@ -0,0 +1,245 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "AppDelegate.h" +#import +#import + +#import +#import "RNCConfig.h" +#import "React/RCTLog.h" +#import "RCTBundleURLProvider.h" +#import "RNSplashScreen.h" +#import "RCTLinkingManager.h" + +#import +#import + +#import +#import + +#import +#import + +#import +#import + +#import "StatusBackendClient.h" + +//TODO: properly import the framework +extern "C" NSString* StatusgoImageServerTLSCert(); + +@interface StatusDownloaderOperation : SDWebImageDownloaderOperation + + (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler; +@end + +@implementation AppDelegate +{ + UIView *_blankView; +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + [FIRApp configure]; + + if (!self.bridge) { + self.bridge = [self createBridgeWithDelegate:self launchOptions:launchOptions]; + } + + [ReactNativeNavigation bootstrapWithBridge:self.bridge]; + + signal(SIGPIPE, SIG_IGN); + NSURL *jsCodeLocation; + + /* Set logging level from React Native */ + NSString *logLevel = [RNCConfig envFor:@"LOG_LEVEL"]; + if([logLevel isEqualToString:@"error"]){ + RCTSetLogThreshold(RCTLogLevelError); + } + else if([logLevel isEqualToString:@"warn"]){ + RCTSetLogThreshold(RCTLogLevelWarning); + } + else if([logLevel isEqualToString:@"info"]){ + RCTSetLogThreshold(RCTLogLevelInfo); + } + else if([logLevel isEqualToString:@"debug"]){ + RCTSetLogThreshold(RCTLogLevelTrace); + } + + NSDictionary *appDefaults = [NSDictionary + dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:@"BLANK_PREVIEW"]; + [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults]; + + [RNSplashScreen show]; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = self; + + SDWebImageDownloaderConfig.defaultDownloaderConfig.operationClass = [StatusDownloaderOperation class]; + + return YES; +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options +{ + return [RCTLinkingManager application:application openURL:url options:options]; +} + + +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler +{ + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge { + return [ReactNativeNavigation extraModulesForBridge:bridge]; +} + + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ + return [self getBundleURL]; +} + +- (NSURL *)getBundleURL +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +- (void)applicationWillResignActive:(UIApplication *)application { + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"BLANK_PREVIEW"]) { + [self.window addSubview:_blankView]; + [self.window bringSubviewToFront:_blankView]; + + [UIView animateWithDuration:0.5 animations:^{ + _blankView.alpha = 1; + }]; + } +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"BLANK_PREVIEW"]) { + [UIView animateWithDuration:0.5 animations:^{ + _blankView.alpha = 0; + } completion:^(BOOL finished) { + [_blankView removeFromSuperview]; + }]; + } +} + +// Required to register for notifications +- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings +{ + [RNCPushNotificationIOS didRegisterUserNotificationSettings:notificationSettings]; +} +// Required for the register event. +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} +// Required for the registrationError event. +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error]; +} +// IOS 10+ Required for localNotification event +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler +{ + [RNCPushNotificationIOS didReceiveNotificationResponse:response]; + completionHandler(); +} +// IOS 4-10 Required for the localNotification event. +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification +{ + [RNCPushNotificationIOS didReceiveLocalNotification:notification]; +} +// Manage notifications while app is in the foreground +- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler +{ + NSDictionary *userInfo = notification.request.content.userInfo; + + NSString *notificationType = userInfo[@"notificationType"]; // check your notification type + if (![notificationType isEqual: @"local-notification"]) { // we silence all notifications which are not local + completionHandler(UNNotificationPresentationOptionNone); + return; + } + + completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge); +} + +@end + +@implementation StatusDownloaderOperation + ++ (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { + NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge; + __block NSURLCredential *credential = nil; + + NSString *pemCert = [StatusBackendClient executeStatusGoRequestWithResult:@"ImageServerTLSCert" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoImageServerTLSCert(); + }]; + + pemCert = [pemCert stringByReplacingOccurrencesOfString:@"-----BEGIN CERTIFICATE-----\n" withString:@""]; + pemCert = [pemCert stringByReplacingOccurrencesOfString:@"\n-----END CERTIFICATE-----" withString:@""]; + NSData *derCert = [[NSData alloc] initWithBase64EncodedString:pemCert options:NSDataBase64DecodingIgnoreUnknownCharacters]; + SecCertificateRef certRef = SecCertificateCreateWithData(NULL, (__bridge_retained CFDataRef) derCert); + CFArrayRef certArrayRef = CFArrayCreate(NULL, (const void **)&(certRef), 1, NULL); + SecTrustSetAnchorCertificates(challenge.protectionSpace.serverTrust, certArrayRef); + + SecTrustResultType trustResult; + SecTrustEvaluate(challenge.protectionSpace.serverTrust, &trustResult); + + if ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)) { + disposition = NSURLSessionAuthChallengeUseCredential; + credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + } + + if (completionHandler) { + completionHandler(disposition, credential); + } +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust] && + [challenge.protectionSpace.host isEqualToString:@"localhost"]) { + [StatusDownloaderOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler]; + } else { + [super URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler]; + } +} + +@end + +@implementation RCTHTTPRequestHandler (SelfSigned) + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust] && + [challenge.protectionSpace.host isEqualToString:@"localhost"]) { + [StatusDownloaderOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler]; + } else { + if (completionHandler) { + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + } + } +} + +@end diff --git a/ios/StatusIm/Base.lproj/LaunchScreen.xib b/ios/StatusIm/Base.lproj/LaunchScreen.xib deleted file mode 100644 index 99ee1ebed32..00000000000 --- a/ios/StatusIm/Base.lproj/LaunchScreen.xib +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png index 638c51af6b4..c81c9cbfb7e 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png index ec2c5fde961..3a2885b83ad 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72.png index b2864f400a8..ce463f35774 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png index 2256b38421f..b5bf2cc1406 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76.png index 227521586f9..89453b3ad7c 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png index 864f5d0d552..477732abae0 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png index 6b1c808ad78..e74d474bc89 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification.png index 62b4937c7ac..71100052c04 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png index a8ec2099b0d..d3ba5367d25 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png index c30dd099246..22b0999665e 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png index a8ec2099b0d..d3ba5367d25 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png index cf84c965f54..fa7118195d3 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png index 638c51af6b4..c81c9cbfb7e 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png index 91e5c1665c5..e8c8ad471a7 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png index e35be622848..bb53dec3086 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small.png index db77f76dd37..b0ee8b85570 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png index 47ffb7c8f94..812f19f7506 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png index e6f3f308b59..a6efd854a85 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon.png index a0c6da14acb..12a2cdea346 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon@2x.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon@2x.png index 2d82458228e..d724b9c19e9 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon@2x.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/Icon@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/ios-marketing.png b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/ios-marketing.png index 79f4c97fdae..e9b4a160903 100644 Binary files a/ios/StatusIm/Images.xcassets/AppIcon.appiconset/ios-marketing.png and b/ios/StatusIm/Images.xcassets/AppIcon.appiconset/ios-marketing.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/Contents.json b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/Contents.json new file mode 100644 index 00000000000..97c36fda18b --- /dev/null +++ b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/Contents.json @@ -0,0 +1,62 @@ +{ + "images" : [ + { + "filename" : "icon-ios-20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-ios-20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "icon-ios-29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-ios-29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon-ios-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-ios-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "icon-ios-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "icon-ios-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "icon-ios-1024@1x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-1024@1x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-1024@1x.png new file mode 100644 index 00000000000..078848763d4 Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-1024@1x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-20@2x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-20@2x.png new file mode 100644 index 00000000000..97c3be4c95e Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-20@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-20@3x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-20@3x.png new file mode 100644 index 00000000000..0c5efe1cad2 Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-20@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-29@2x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-29@2x.png new file mode 100644 index 00000000000..2ee13d28d9b Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-29@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-29@3x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-29@3x.png new file mode 100644 index 00000000000..2d4ed45ea1d Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-29@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-40@2x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-40@2x.png new file mode 100644 index 00000000000..6f7451862dd Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-40@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-40@3x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-40@3x.png new file mode 100644 index 00000000000..8770f70891c Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-40@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-60@2x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-60@2x.png new file mode 100644 index 00000000000..8770f70891c Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-60@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-60@3x.png b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-60@3x.png new file mode 100644 index 00000000000..5984ce7ff67 Binary files /dev/null and b/ios/StatusIm/Images.xcassets/AppIconPR.appiconset/icon-ios-60@3x.png differ diff --git a/ios/StatusIm/Images.xcassets/corner_left_bottom.imageset/Contents.json b/ios/StatusIm/Images.xcassets/corner_left_bottom.imageset/Contents.json deleted file mode 100644 index 8f7abaebbbd..00000000000 --- a/ios/StatusIm/Images.xcassets/corner_left_bottom.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "corner_left_bottom.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/corner_left_bottom.imageset/corner_left_bottom.png b/ios/StatusIm/Images.xcassets/corner_left_bottom.imageset/corner_left_bottom.png deleted file mode 100644 index 0fff3412209..00000000000 Binary files a/ios/StatusIm/Images.xcassets/corner_left_bottom.imageset/corner_left_bottom.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/corner_left_top.imageset/Contents.json b/ios/StatusIm/Images.xcassets/corner_left_top.imageset/Contents.json deleted file mode 100644 index 8a4261dfffc..00000000000 --- a/ios/StatusIm/Images.xcassets/corner_left_top.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "corner_left_top.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/corner_left_top.imageset/corner_left_top.png b/ios/StatusIm/Images.xcassets/corner_left_top.imageset/corner_left_top.png deleted file mode 100644 index e25037ece32..00000000000 Binary files a/ios/StatusIm/Images.xcassets/corner_left_top.imageset/corner_left_top.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/corner_right_bottom.imageset/Contents.json b/ios/StatusIm/Images.xcassets/corner_right_bottom.imageset/Contents.json deleted file mode 100644 index 077dc665070..00000000000 --- a/ios/StatusIm/Images.xcassets/corner_right_bottom.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "corner_right_bottom.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/corner_right_bottom.imageset/corner_right_bottom.png b/ios/StatusIm/Images.xcassets/corner_right_bottom.imageset/corner_right_bottom.png deleted file mode 100644 index e542ec0d64d..00000000000 Binary files a/ios/StatusIm/Images.xcassets/corner_right_bottom.imageset/corner_right_bottom.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/corner_right_top.imageset/Contents.json b/ios/StatusIm/Images.xcassets/corner_right_top.imageset/Contents.json deleted file mode 100644 index 4a70ee39484..00000000000 --- a/ios/StatusIm/Images.xcassets/corner_right_top.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "corner_right_top.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/corner_right_top.imageset/corner_right_top.png b/ios/StatusIm/Images.xcassets/corner_right_top.imageset/corner_right_top.png deleted file mode 100644 index a6a65aedc96..00000000000 Binary files a/ios/StatusIm/Images.xcassets/corner_right_top.imageset/corner_right_top.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_action_back.imageset/Contents.json deleted file mode 100644 index 55aef57a63d..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_action_back.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_back@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_back@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back.png b/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back.png deleted file mode 100644 index 8e31377a598..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back@2x.png b/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back@2x.png deleted file mode 100644 index 89db7e29865..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back@2x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back@3x.png b/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back@3x.png deleted file mode 100644 index 491112378ef..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_back.imageset/icon_action_back@3x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/Contents.json deleted file mode 100644 index 71b9f10b5e1..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_action_forward.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_forward@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_forward@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward.png b/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward.png deleted file mode 100644 index af802ac9eb0..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward@2x.png b/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward@2x.png deleted file mode 100644 index b8abf47c489..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward@2x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward@3x.png b/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward@3x.png deleted file mode 100644 index 453add7dec6..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_forward.imageset/icon_action_forward@3x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/Contents.json deleted file mode 100644 index b69df6a5e08..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_action_fullscreen_collapse.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_fullscreen_collapse@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_fullscreen_collapse@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse.png b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse.png deleted file mode 100644 index 09c4944904b..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse@2x.png b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse@2x.png deleted file mode 100644 index 8bc5016f2ce..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse@2x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse@3x.png b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse@3x.png deleted file mode 100644 index 80948ff1915..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_collapse.imageset/icon_action_fullscreen_collapse@3x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/Contents.json deleted file mode 100644 index 561ff97a3b3..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_action_fullscreen_expand.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_fullscreen_expand@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_action_fullscreen_expand@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand.png b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand.png deleted file mode 100644 index a862a39fc51..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand@2x.png b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand@2x.png deleted file mode 100644 index d5b72bb7689..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand@2x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand@3x.png b/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand@3x.png deleted file mode 100644 index 35fcdd6ae63..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_action_fullscreen_expand.imageset/icon_action_fullscreen_expand@3x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/Contents.json deleted file mode 100644 index 067327c77bc..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_arrow_top.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_arrow_top@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_arrow_top@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top.png b/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top.png deleted file mode 100644 index dd47f4ff17a..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top@2x.png b/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top@2x.png deleted file mode 100644 index 193fe2261df..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top@2x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top@3x.png b/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top@3x.png deleted file mode 100644 index 9125df76b51..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_arrow_top.imageset/icon_arrow_top@3x.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_avatar.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_avatar.imageset/Contents.json deleted file mode 100644 index d7258658d9e..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_avatar.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_avatar.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_avatar.imageset/icon_avatar.png b/ios/StatusIm/Images.xcassets/icon_avatar.imageset/icon_avatar.png deleted file mode 100644 index f3b8d0770d0..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_avatar.imageset/icon_avatar.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_check_on.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_check_on.imageset/Contents.json deleted file mode 100644 index a5f8b13cf15..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_check_on.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_check_on.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_check_on.imageset/icon_check_on.png b/ios/StatusIm/Images.xcassets/icon_check_on.imageset/icon_check_on.png deleted file mode 100644 index ef90004718a..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_check_on.imageset/icon_check_on.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/Contents.json deleted file mode 100644 index c9e06951764..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_close_gray.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_close_gray-2.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_close_gray-1.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray-1.png b/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray-1.png deleted file mode 100644 index 8f9b0d0b809..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray-1.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray-2.png b/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray-2.png deleted file mode 100644 index 03a45042681..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray-2.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray.png b/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray.png deleted file mode 100644 index 4918a29a643..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_close_light_gray.imageset/icon_close_gray.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_forward_gray.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_forward_gray.imageset/Contents.json deleted file mode 100644 index b9246a710af..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_forward_gray.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_forward_gray.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/StatusIm/Images.xcassets/icon_forward_gray.imageset/icon_forward_gray.png b/ios/StatusIm/Images.xcassets/icon_forward_gray.imageset/icon_forward_gray.png deleted file mode 100644 index de0eccda60c..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_forward_gray.imageset/icon_forward_gray.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_lock_gray.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_lock_gray.imageset/Contents.json deleted file mode 100644 index ced131029f3..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_lock_gray.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_lock_gray.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_lock_gray.imageset/icon_lock_gray.png b/ios/StatusIm/Images.xcassets/icon_lock_gray.imageset/icon_lock_gray.png deleted file mode 100644 index f18aaa61e18..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_lock_gray.imageset/icon_lock_gray.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_lock_white.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_lock_white.imageset/Contents.json deleted file mode 100644 index 1ceca537cf1..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_lock_white.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_lock_white.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_lock_white.imageset/icon_lock_white.png b/ios/StatusIm/Images.xcassets/icon_lock_white.imageset/icon_lock_white.png deleted file mode 100644 index ccddc79dec7..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_lock_white.imageset/icon_lock_white.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_menu_group.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_menu_group.imageset/Contents.json deleted file mode 100644 index e499cb9f091..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_menu_group.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_menu_group.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_menu_group.imageset/icon_menu_group.png b/ios/StatusIm/Images.xcassets/icon_menu_group.imageset/icon_menu_group.png deleted file mode 100644 index a356f7ccdd3..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_menu_group.imageset/icon_menu_group.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_money_white.imageset/Contents.json deleted file mode 100644 index 6ad2e9bcdcd..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_money_white.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_money_white-1.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_money_white-2.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white-1.png b/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white-1.png deleted file mode 100644 index da077578fa6..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white-1.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white-2.png b/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white-2.png deleted file mode 100644 index 243469d9a98..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white-2.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white.png b/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white.png deleted file mode 100644 index 1584a09d0a2..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_money_white.imageset/icon_money_white.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_muted.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_muted.imageset/Contents.json deleted file mode 100644 index 052a64996d9..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_muted.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_muted.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_muted.imageset/icon_muted.png b/ios/StatusIm/Images.xcassets/icon_muted.imageset/icon_muted.png deleted file mode 100644 index 674a9faa669..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_muted.imageset/icon_muted.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_notifications_on.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_notifications_on.imageset/Contents.json deleted file mode 100644 index fecfd53e278..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_notifications_on.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_notifications_on.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_notifications_on.imageset/icon_notifications_on.png b/ios/StatusIm/Images.xcassets/icon_notifications_on.imageset/icon_notifications_on.png deleted file mode 100644 index a0ee3c3fd5b..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_notifications_on.imageset/icon_notifications_on.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/Contents.json deleted file mode 100644 index 5f10533309f..00000000000 --- a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_phone_white.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_phone_white-1.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_phone_white-2.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white-1.png b/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white-1.png deleted file mode 100644 index f08867e855e..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white-1.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white-2.png b/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white-2.png deleted file mode 100644 index b49ec0439d4..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white-2.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white.png b/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white.png deleted file mode 100644 index 224f48c50de..00000000000 Binary files a/ios/StatusIm/Images.xcassets/icon_phone_white.imageset/icon_phone_white.png and /dev/null differ diff --git a/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image.png b/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image.png index e4855dc4e09..23bb6bad604 100644 Binary files a/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image.png and b/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image.png differ diff --git a/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@2x.png b/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@2x.png index e5e2bf9193b..ef4aeeb761c 100644 Binary files a/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@2x.png and b/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@2x.png differ diff --git a/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@3x.png b/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@3x.png index 03baea0a6e5..e8bec099f0b 100644 Binary files a/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@3x.png and b/ios/StatusIm/Images.xcassets/launch_image.imageset/launch_image@3x.png differ diff --git a/ios/StatusIm/Info.plist b/ios/StatusIm/Info.plist index eb2f4d3c644..94904c11f89 100644 --- a/ios/StatusIm/Info.plist +++ b/ios/StatusIm/Info.plist @@ -2,17 +2,12 @@ - CFBundleURLTypes - - - CFBundleURLName - im.status.ethereum.applink - CFBundleURLSchemes - - status-im - - - + CADisableMinimumFrameDurationOnPhone + + CFBundleBuildUrl + ???? + CFBundleCommitHash + unknown CFBundleDevelopmentRegion en CFBundleDisplayName @@ -31,66 +26,91 @@ 1.0 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleURLName + im.status.ethereum.applink + CFBundleURLSchemes + + status-app + + + + CFBundleTypeRole + Editor + CFBundleURLName + im.status.ethereum.eip681 + CFBundleURLSchemes + + ethereum + + + CFBundleVersion 1 + ITSAppUsesNonExemptEncryption + + ITSEncryptionExportComplianceCode + 37d5ac79-0eb2-4e66-9117-8a8515185a7f LSApplicationQueriesSchemes whatsapp LSRequiresIPhoneOS + NFCReaderUsageDescription + Enable Keycard NSAppTransportSecurity NSAllowsArbitraryLoads + + NSAllowsLocalNetworking - NSExceptionDomains - - api.status.im - - NSTemporaryExceptionAllowsInsecureHTTPLoads - - - localhost - - NSTemporaryExceptionAllowsInsecureHTTPLoads - - - NSCameraUsageDescription - We need to access your camera - NSContactsUsageDescription - We need to access your contacts + Status uses camera to take pictures and scan QR codes. + NSFaceIDUsageDescription + Enabling Face ID allows you quick and secure access to your account. + NSLocationAlwaysUsageDescription + Location access is required for some DApps to function properly. NSLocationWhenInUseUsageDescription - + Location access is required for some DApps to function properly. NSMicrophoneUsageDescription - Need microphone access for Instabug and Audio Messages + Need microphone access for sending audio messages. + NSPhotoLibraryAddUsageDescription + Photos access is required to give you the ability to save images. NSPhotoLibraryUsageDescription - We need to access your photo storage to give you an ability to select photos + Photos access is required to give you the ability to send images. + NSAppleMusicUsageDescription + Status uses Media Library to save and send Images. The Media Library module internally requires permissions to Apple Music UIAppFonts - RobotoMono-Medium.ttf - SF-UI-Text-Semibold.otf - SF-UI-Text-Bold.otf - SF-UI-Text-Regular.otf - SF-UI-Text-Medium.otf - SF-UI-Text-Light.otf - SF-UI-Display-Medium.otf - SF-UI-Display-Regular.otf - SF-UI-Display-Semibold.otf - SF-UI-Display-Thin.otf - Entypo.ttf - EvilIcons.ttf - FontAwesome.ttf - Foundation.ttf - Ionicons.ttf - MaterialIcons.ttf - Octicons.ttf - Zocial.ttf + Inter-Bold.otf + Inter-Medium.otf + Inter-Regular.otf + Inter-Black.otf + Inter-BlackItalic.otf + Inter-BoldItalic.otf + Inter-ExtraBold.otf + Inter-ExtraBoldItalic.otf + Inter-ExtraLight-BETA.otf + Inter-ExtraLightItalic-BETA.otf + Inter-Italic.otf + Inter-Light-BETA.otf + Inter-LightItalic-BETA.otf + Inter-MediumItalic.otf + Inter-SemiBold.otf + Inter-SemiBoldItalic.otf + Inter-Thin-BETA.otf + Inter-ThinItalic-BETA.otf + InterStatus-Regular.otf + UbuntuMono-Regular.ttf UIBackgroundModes remote-notification + fetch UILaunchStoryboardName launch-image-universal @@ -112,6 +132,12 @@ UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance - + + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A00000080400010101 + A00000080400010301 + A000000151000000 + diff --git a/ios/StatusIm/LaunchScreen.storyboard b/ios/StatusIm/LaunchScreen.storyboard new file mode 100644 index 00000000000..e117d8bb6e6 --- /dev/null +++ b/ios/StatusIm/LaunchScreen.storyboard @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/StatusIm/StatusIm.entitlements b/ios/StatusIm/StatusIm.entitlements index 0af9283b444..b1f10375030 100644 --- a/ios/StatusIm/StatusIm.entitlements +++ b/ios/StatusIm/StatusIm.entitlements @@ -2,11 +2,15 @@ + com.apple.developer.nfc.readersession.formats + + TAG + aps-environment development com.apple.developer.associated-domains - applinks:get.status.im + applinks:status.app keychain-access-groups diff --git a/ios/StatusIm/launch-icon.png b/ios/StatusIm/launch-icon.png new file mode 100644 index 00000000000..23bb6bad604 Binary files /dev/null and b/ios/StatusIm/launch-icon.png differ diff --git a/ios/StatusIm/launch-icon@2x.png b/ios/StatusIm/launch-icon@2x.png new file mode 100644 index 00000000000..ef4aeeb761c Binary files /dev/null and b/ios/StatusIm/launch-icon@2x.png differ diff --git a/ios/StatusIm/launch-icon@3x.png b/ios/StatusIm/launch-icon@3x.png new file mode 100644 index 00000000000..e8bec099f0b Binary files /dev/null and b/ios/StatusIm/launch-icon@3x.png differ diff --git a/ios/StatusIm/main.m b/ios/StatusIm/main.m index 3d767fcbb9f..8bfe50c3354 100644 --- a/ios/StatusIm/main.m +++ b/ios/StatusIm/main.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11,7 +11,8 @@ #import "AppDelegate.h" -int main(int argc, char * argv[]) { +int main(int argc, char *argv[]) +{ @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/ios/StatusImPR/Info.plist b/ios/StatusImPR/Info.plist new file mode 100644 index 00000000000..4b1118d0b94 --- /dev/null +++ b/ios/StatusImPR/Info.plist @@ -0,0 +1,152 @@ + + + + + CFBundleBuildUrl + ???? + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLName + im.status.ethereum.applink + CFBundleURLSchemes + + status-app + + + + CFBundleTypeRole + Editor + CFBundleURLName + im.status.ethereum.eip681 + CFBundleURLSchemes + + ethereum + + + + CFBundleVersion + 1 + ITSAppUsesNonExemptEncryption + + ITSEncryptionExportComplianceCode + 37d5ac79-0eb2-4e66-9117-8a8515185a7f + LSApplicationQueriesSchemes + + whatsapp + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + api.status.im + + NSTemporaryExceptionAllowsInsecureHTTPLoads + + + localhost + + NSTemporaryExceptionAllowsInsecureHTTPLoads + + + + + NSCameraUsageDescription + Status uses camera to take pictures and scan QR codes. + NSFaceIDUsageDescription + Enabling Face ID allows you quick and secure access to your account. + NSLocationAlwaysUsageDescription + Location access is required for some DApps to function properly. + NSLocationWhenInUseUsageDescription + Location access is required for some DApps to function properly. + NSPhotoLibraryUsageDescription + Photos access is required to give you the ability to send images. + NSPhotoLibraryAddUsageDescription + Photos access is required to give you the ability to save images. + NSMicrophoneUsageDescription + Need microphone access for sending audio messages. + UIAppFonts + + Inter-Bold.otf + Inter-Medium.otf + Inter-Regular.otf + Inter-Black.otf + Inter-BlackItalic.otf + Inter-BoldItalic.otf + Inter-ExtraBold.otf + Inter-ExtraBoldItalic.otf + Inter-ExtraLight-BETA.otf + Inter-ExtraLightItalic-BETA.otf + Inter-Italic.otf + Inter-Light-BETA.otf + Inter-LightItalic-BETA.otf + Inter-MediumItalic.otf + Inter-SemiBold.otf + Inter-SemiBoldItalic.otf + Inter-Thin-BETA.otf + Inter-ThinItalic-BETA.otf + InterStatus-Regular.otf + UbuntuMono-Regular.ttf + + UIBackgroundModes + + remote-notification + fetch + + UILaunchStoryboardName + launch-image-universal + UIMainStoryboardFile + launch-image-universal + UIRequiredDeviceCapabilities + + armv7 + gamekit + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A00000080400010101 + A00000080400010301 + A000000151000000 + + NFCReaderUsageDescription + Enable Keycard + CADisableMinimumFrameDurationOnPhone + + diff --git a/ios/StatusImPR/StatusImPR.entitlements b/ios/StatusImPR/StatusImPR.entitlements new file mode 100644 index 00000000000..e568b09f0ea --- /dev/null +++ b/ios/StatusImPR/StatusImPR.entitlements @@ -0,0 +1,21 @@ + + + + + com.apple.developer.nfc.readersession.formats + + NDEF + TAG + + aps-environment + development + com.apple.developer.associated-domains + + applinks:status.app + + keychain-access-groups + + $(AppIdentifierPrefix)im.status.ethereum.pr + + + diff --git a/ios/StatusImTests-Bridging-Header.h b/ios/StatusImTests-Bridging-Header.h new file mode 100644 index 00000000000..1b2cb5d6d09 --- /dev/null +++ b/ios/StatusImTests-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/ios/StatusImTests/Info.plist b/ios/StatusImTests/Info.plist index 48e83604358..db4d420079f 100644 --- a/ios/StatusImTests/Info.plist +++ b/ios/StatusImTests/Info.plist @@ -20,5 +20,13 @@ ???? CFBundleVersion 1 + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A00000080400010101 + A00000080400010301 + A000000151000000 + + NFCReaderUsageDescription + Enable Keycard diff --git a/ios/StatusImTests/StatusImTests.m b/ios/StatusImTests/StatusImTests.m index 7e2349d94a5..f43300954a1 100644 --- a/ios/StatusImTests/StatusImTests.m +++ b/ios/StatusImTests/StatusImTests.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22,7 +22,7 @@ @interface StatusImTests : XCTestCase @implementation StatusImTests -- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test +- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test { if (test(view)) { return YES; @@ -42,10 +42,11 @@ - (void)testRendersWelcomeScreen BOOL foundElement = NO; __block NSString *redboxError = nil; - RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { - if (level >= RCTLogLevelError) { - redboxError = message; - } + RCTSetLogFunction( + ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + redboxError = message; + } }); while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { diff --git a/ios/archive-develop.plist b/ios/archive-develop.plist deleted file mode 100644 index fbed5e8ee15..00000000000 --- a/ios/archive-develop.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - teamID - DTX7Z4U3YA - method - development - - diff --git a/ios/archive-release.plist b/ios/archive-release.plist deleted file mode 100644 index 4d2930c9667..00000000000 --- a/ios/archive-release.plist +++ /dev/null @@ -1,16 +0,0 @@ - - - - - teamID - DTX7Z4U3YA - method - app-store - uploadBitcode - - compileBitcode - - ITSAppUsesNonExemptEncryption - - - diff --git a/ios/fonts/Inter-UI-Bold.otf b/ios/fonts/Inter-UI-Bold.otf new file mode 100644 index 00000000000..8ad8834884d Binary files /dev/null and b/ios/fonts/Inter-UI-Bold.otf differ diff --git a/ios/fonts/Inter-UI-Medium.otf b/ios/fonts/Inter-UI-Medium.otf new file mode 100644 index 00000000000..9bec8934bb6 Binary files /dev/null and b/ios/fonts/Inter-UI-Medium.otf differ diff --git a/ios/fonts/Inter-UI-Regular.otf b/ios/fonts/Inter-UI-Regular.otf new file mode 100644 index 00000000000..287135d4009 Binary files /dev/null and b/ios/fonts/Inter-UI-Regular.otf differ diff --git a/ios/launch-image-universal.storyboard b/ios/launch-image-universal.storyboard index e2834bddeb4..a986b4b020e 100644 --- a/ios/launch-image-universal.storyboard +++ b/ios/launch-image-universal.storyboard @@ -1,11 +1,10 @@ - - - - + + - - + + + @@ -17,13 +16,20 @@ - + - + - - + + + + + + + + + @@ -32,6 +38,9 @@ - + + + + diff --git a/ios/scripts/set_xcode_version.sh b/ios/scripts/set_xcode_version.sh index c3fa745b190..1cda42c8f8c 100755 --- a/ios/scripts/set_xcode_version.sh +++ b/ios/scripts/set_xcode_version.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # This script automatically sets the version and short version string of # an Xcode project from the Git repository containing the project. @@ -6,29 +6,29 @@ # To use this script in Xcode, add the script's path to a "Run Script" build # phase for your application target. +set -x set -o errexit set -o nounset # First, check for git in $PATH hash git 2>/dev/null || { echo >&2 "Git required, not installed. Aborting build number update script."; exit 0; } -# Alternatively, we could use Xcode's copy of the Git binary, -# but old Xcodes don't have this. -#GIT=$(xcrun -find git) - -# Run Script build phases that operate on product files of the target that defines them should use the value of this build setting [TARGET_BUILD_DIR]. But Run Script build phases that operate on product files of other targets should use ?BUILT_PRODUCTS_DIR? instead. +# Run Script build phases that operate on product files of the target +# that defines them should use the value of this build setting [TARGET_BUILD_DIR]. +# But Run Script build phases that operate on product files of other targets should use ?BUILT_PRODUCTS_DIR? instead. INFO_PLIST="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}" if [[ $(git ls-files -m "StatusIm/Info.plist") = *"Info.plist"* ]]; then echo "version was set in Info.plist" -else - RELEASE_VERSION=$(cat ../VERSION) - BUILD_NO=$(sh ../scripts/build_no.sh) + exit 0 +fi - # For debugging: - echo "SHORT VERSION: $RELEASE_VERSION" - echo "BUILD NO: $BUILD_NO" +RELEASE_VERSION=$(cat ../VERSION) +BUILD_NO=$(bash ../scripts/version/build_no.sh) - /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string $BUILD_NO" "$INFO_PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NO" "$INFO_PLIST" - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $RELEASE_VERSION" "$INFO_PLIST" -fi +# For debugging: +echo "SHORT VERSION: ${RELEASE_VERSION}" +echo "BUILD NO: ${BUILD_NO}" + +/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${BUILD_NO}" "${INFO_PLIST}" +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${RELEASE_VERSION}" "${INFO_PLIST}" diff --git a/legal-docs b/legal-docs new file mode 160000 index 00000000000..144f64af0d1 --- /dev/null +++ b/legal-docs @@ -0,0 +1 @@ +Subproject commit 144f64af0d13a97f793120f589b9d9297653a2a8 diff --git a/logs/README.md b/logs/README.md new file mode 100644 index 00000000000..18ef8efdd0e --- /dev/null +++ b/logs/README.md @@ -0,0 +1,29 @@ +# Description + +This directory is the destination of logs created during build time of debug builds and logs created by Fastlane on CI. + +# Logs + +* `xcrun_device_install.log` - Output from `status-mobile/scripts/run-ios-device.sh`. + - Created by redirecting output of `xcrun simctl install "$UDID" "$APP_PATH"`. +* `xcrun_device_process_launch.log` - Output from `status-mobile/scripts/run-ios-device.sh`. + - Created by specifying `--json-output` flag for `xcrun devicectl device process launch --no-activate --verbose --device "${DEVICE_UUID}" "${INSTALLATION_URL}"`. +* `xcrun_device_process_resume.log` - Output from `status-mobile/scripts/run-ios-device.sh`. + - Created by redirecting output of `xcrun devicectl device process resume --device "${DEVICE_UUID}" --pid "${STATUS_PID}"`. +* `adb_install.log` - Output from `scripts/run-android.sh`. + - Created by redirecting output of `adb install -r ./result/app-debug.apk`. +* `adb_shell_monkey.log` - Output from `status-mobile/scripts/run-android.sh`. + - Created by redirecting output of `adb shell monkey -p im.status.ethereum.debug 1 >`. +* `ios_simulators_list.log` - Output from `status-mobile/scripts/run-ios.sh`. + - Created by redirecting output of `xcrun simctl list devices -j`. + +# CI + +These log files are uploaded to Jenkins as CI job artifacts on failure using `archiveArtifacts`. + +* `react-native-xcode.log` - Output from `node_modules/react-native/scripts/react-native-xcode.sh`. + - Created by redirecting output of `shellScript` in `ios/StatusIm.xcodeproj/project.pbxproj` for "Bundle React Native code and images" build Phase. +* `set_xcode_version.log` - Output from `node_modules/react-native/scripts/react-native-xcode.sh`. + - Created by redirecting output of `shellScript` in `ios/StatusIm.xcodeproj/project.pbxproj` for "Run Script" build Phase. +* `Status PR-StatusImPR.log - Created by [Fastlane Gym](https://docs.fastlane.tools/actions/gym/). + - Configured via [`Fastfile`](../Fastfile) using `buildlog_path` argument for `build_ios_app`. diff --git a/maestro/create-account-or-login.yaml b/maestro/create-account-or-login.yaml new file mode 100644 index 00000000000..8a24ec95d1d --- /dev/null +++ b/maestro/create-account-or-login.yaml @@ -0,0 +1,11 @@ +appId: im.status.ethereum.debug +--- +- runFlow: # to run onboarding flow only when device is in onboarding state + when: + visible: "I’m new to Status" + file: onboarding.yaml + +- runFlow: # to run login flow only when device is on login screen + when: + visible: "Profile password" + file: login.yaml diff --git a/maestro/login.yaml b/maestro/login.yaml new file mode 100644 index 00000000000..48a0ad2a00f --- /dev/null +++ b/maestro/login.yaml @@ -0,0 +1,5 @@ +appId: im.status.ethereum.debug +--- +- tapOn: "password-input" +- inputText: "password123" # update your account password here +- tapOn: login-button diff --git a/maestro/onboarding.yaml b/maestro/onboarding.yaml new file mode 100644 index 00000000000..d6d78f282d8 --- /dev/null +++ b/maestro/onboarding.yaml @@ -0,0 +1,15 @@ +appId: im.status.ethereum.debug +--- +- tapOn: "I’m new to Status" +- tapOn: "small-option-card" +- tapOn: "Your name" +- inputText: "DeviceA" +- tapOn: "submit-create-profile-button" +- inputText: "password123" +- tapOn: "Repeat password" +- inputText: "password123" +- tapOn: "disclaimer-touchable-opacity" +- tapOn: "Confirm password" +- tapOn: "skip-identifiers" +- tapOn: "enable-notifications-later-button" +- tapOn: "welcome-button" diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 00000000000..dccedbf8d05 --- /dev/null +++ b/metro.config.js @@ -0,0 +1,24 @@ +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +/** + * Metro configuration + * https://facebook.github.io/metro/docs/configuration + * + * @type {import('metro-config').MetroConfig} + */ +const config = { + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + }, + resolver: { + resolverMainFields: ['react-native', 'browser', 'main'], + nodeModulesPaths: ['./node_modules'], + extraNodeModules: require('node-libs-react-native'), + unstable_enableSymlinks: true, + }, +}; +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/mobile_files/VERSION b/mobile_files/VERSION deleted file mode 100644 index 46e7a712842..00000000000 --- a/mobile_files/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.9.26 diff --git a/mobile_files/package-lock.json b/mobile_files/package-lock.json deleted file mode 100644 index e82247932e9..00000000000 --- a/mobile_files/package-lock.json +++ /dev/null @@ -1,8455 +0,0 @@ -{ - "name": "StatusIm", - "version": "0.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.54.tgz", - "integrity": "sha1-ACT5b99wKKIdaOJzr9TpUyFKHq0=", - "requires": { - "@babel/highlight": "7.0.0-beta.54" - } - }, - "@babel/core": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.0.0-beta.54.tgz", - "integrity": "sha1-JTxU0AlUA6XPp2Tn2bRYGUaS0Cs=", - "requires": { - "@babel/code-frame": "7.0.0-beta.54", - "@babel/generator": "7.0.0-beta.54", - "@babel/helpers": "7.0.0-beta.54", - "@babel/parser": "7.0.0-beta.54", - "@babel/template": "7.0.0-beta.54", - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54", - "convert-source-map": "1.5.1", - "debug": "3.1.0", - "json5": "0.5.1", - "lodash": "4.17.10", - "resolve": "1.8.1", - "semver": "5.5.0", - "source-map": "0.5.7" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "@babel/generator": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.54.tgz", - "integrity": "sha1-wEPH7r7r/X5mXZXCgaSq/IPU4ck=", - "requires": { - "@babel/types": "7.0.0-beta.54", - "jsesc": "2.5.1", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=" - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0-beta.54.tgz", - "integrity": "sha1-FiYSaj+fxO0oCslCNyx9OWU9cSE=", - "requires": { - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.0.0-beta.54.tgz", - "integrity": "sha1-0KGWdjW57ryv26gEkZF+5JgcEvo=", - "requires": { - "@babel/helper-explode-assignable-expression": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0-beta.54.tgz", - "integrity": "sha1-ytWCqxy4MfTcNOm8tMT6Lx+2yYY=", - "requires": { - "@babel/types": "7.0.0-beta.54", - "esutils": "2.0.2" - } - }, - "@babel/helper-call-delegate": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.0.0-beta.54.tgz", - "integrity": "sha1-9rcs/YMvsm6yqAbhjeBfiNOo8wI=", - "requires": { - "@babel/helper-hoist-variables": "7.0.0-beta.54", - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-define-map": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.0.0-beta.54.tgz", - "integrity": "sha1-IDbXxJNlmH8JHblwLOLztV9ndzA=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54", - "lodash": "4.17.10" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.0.0-beta.54.tgz", - "integrity": "sha1-zwZ/MzCWXCBIvwh+oG9ix22Up5I=", - "requires": { - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-function-name": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.54.tgz", - "integrity": "sha1-MHh1UHoe2iSCoJqaTfaiVjL/s0s=", - "requires": { - "@babel/helper-get-function-arity": "7.0.0-beta.54", - "@babel/template": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.54.tgz", - "integrity": "sha1-dXvRibB3B0oAQCjP3l8IPDBsxsQ=", - "requires": { - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0-beta.54.tgz", - "integrity": "sha1-hjW+gJUTX/c/dT7RieRJ9otPQ8s=", - "requires": { - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0-beta.54.tgz", - "integrity": "sha1-vOndxIQxexPSYVuv4rUk0NVtmd8=", - "requires": { - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-module-imports": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.54.tgz", - "integrity": "sha1-wtjhT/A0Ilv0MTVtt370Z7jTWqw=", - "requires": { - "@babel/types": "7.0.0-beta.54", - "lodash": "4.17.10" - } - }, - "@babel/helper-module-transforms": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.0.0-beta.54.tgz", - "integrity": "sha1-jMV+sNtfCUXYZlJNVVq9CE4wzDU=", - "requires": { - "@babel/helper-module-imports": "7.0.0-beta.54", - "@babel/helper-simple-access": "7.0.0-beta.54", - "@babel/helper-split-export-declaration": "7.0.0-beta.54", - "@babel/template": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54", - "lodash": "4.17.10" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0-beta.54.tgz", - "integrity": "sha1-SvjdT/kNvSmzvPhf/0OVLirhAW4=", - "requires": { - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.54.tgz", - "integrity": "sha1-YdKpoPmj4xg4pFjeu57r173SSbQ=" - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.0.0-beta.54.tgz", - "integrity": "sha1-OaUAUqrddNQMc7fFjrljuQ+sVtM=", - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.54", - "@babel/helper-wrap-function": "7.0.0-beta.54", - "@babel/template": "7.0.0-beta.54", - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-replace-supers": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.0.0-beta.54.tgz", - "integrity": "sha1-kB9aFJOkEHmf06s+DA0p0YBxyJ8=", - "requires": { - "@babel/helper-member-expression-to-functions": "7.0.0-beta.54", - "@babel/helper-optimise-call-expression": "7.0.0-beta.54", - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-simple-access": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.0.0-beta.54.tgz", - "integrity": "sha1-X3YKGViam28H6Apl70vL1PuowlM=", - "requires": { - "@babel/template": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54", - "lodash": "4.17.10" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.54.tgz", - "integrity": "sha1-ic2IM8lUgaCCesahv8zduSt1oQk=", - "requires": { - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helper-wrap-function": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.0.0-beta.54.tgz", - "integrity": "sha1-3Bt6SDowdKNTGzZSPgMVbZEKOio=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.54", - "@babel/template": "7.0.0-beta.54", - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/helpers": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.0.0-beta.54.tgz", - "integrity": "sha1-uGqZqA79gWaMrvMHYQuWEZdEanQ=", - "requires": { - "@babel/template": "7.0.0-beta.54", - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54" - } - }, - "@babel/highlight": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.54.tgz", - "integrity": "sha1-FV1Qc1gym45waJcAF8P9dKmwhYQ=", - "requires": { - "chalk": "2.4.1", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "1.9.2" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.54.tgz", - "integrity": "sha1-wBqmO1fJyNzodEeWyB2d8SHyDbQ=" - }, - "@babel/plugin-external-helpers": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-external-helpers/-/plugin-external-helpers-7.0.0-beta.54.tgz", - "integrity": "sha1-FLHq6aW0kayth45fu6Wage06xM4=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.0.0-beta.54.tgz", - "integrity": "sha1-WVPwSZweaecy0zpVC86HmaprdvM=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.54", - "@babel/helper-member-expression-to-functions": "7.0.0-beta.54", - "@babel/helper-optimise-call-expression": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "@babel/helper-replace-supers": "7.0.0-beta.54", - "@babel/plugin-syntax-class-properties": "7.0.0-beta.54" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0-beta.54.tgz", - "integrity": "sha1-VIEmmgIN0NOHFagJT+0BXTDvTCo=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "@babel/plugin-syntax-object-rest-spread": "7.0.0-beta.54" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.0.0-beta.54.tgz", - "integrity": "sha1-XnDyLcNijB01QCtj/xqPjgBb2HE=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.0.0-beta.54.tgz", - "integrity": "sha1-yWJif04aFdptCEIwbUIeex5S9Yc=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-syntax-flow": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.0.0-beta.54.tgz", - "integrity": "sha1-jTj/+m2hbi0yf1/uT5CROxTUPRQ=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.0.0-beta.54.tgz", - "integrity": "sha1-cRca7pArlMzy4iqBDRQmtRZbjYQ=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0-beta.54.tgz", - "integrity": "sha1-4PRFYSCBq1c+JTWturx7cQ0XlAw=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0-beta.54.tgz", - "integrity": "sha1-RKl3uOYeTvzHZYu74mDyBMobz3I=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.0.0-beta.54.tgz", - "integrity": "sha1-vK4cL/rkzDt7PlRV8KmNrswJo8Y=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "lodash": "4.17.10" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.0.0-beta.54.tgz", - "integrity": "sha1-sVeB0uSZziVDjnP+ovpaCYWFaP8=", - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.54", - "@babel/helper-define-map": "7.0.0-beta.54", - "@babel/helper-function-name": "7.0.0-beta.54", - "@babel/helper-optimise-call-expression": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "@babel/helper-replace-supers": "7.0.0-beta.54", - "@babel/helper-split-export-declaration": "7.0.0-beta.54", - "globals": "11.7.0" - }, - "dependencies": { - "globals": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==" - } - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0-beta.54.tgz", - "integrity": "sha1-soSUlCuU+4bQGZR2PStcQ73Zhq8=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.0.0-beta.54.tgz", - "integrity": "sha1-gfZJo+T8tiwrKtSX94OoALmURy8=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.0.0-beta.54.tgz", - "integrity": "sha1-EBcJY2b7Q+vKjtjY0M3R69ZP67I=", - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-flow-strip-types": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.0.0-beta.54.tgz", - "integrity": "sha1-NhL6OTXmDfbqrmajPST8MbWMyRk=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "@babel/plugin-syntax-flow": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0-beta.54.tgz", - "integrity": "sha1-Jh0pkgWKngkjS5/2eCAFT/xV95w=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.0.0-beta.54.tgz", - "integrity": "sha1-zHIvmXOTEzfe89HmxVE4WB7dNx4=", - "requires": { - "@babel/helper-function-name": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0-beta.54.tgz", - "integrity": "sha1-cPB+zC87e8n1QqV46C7sGKVQQJg=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.0.0-beta.54.tgz", - "integrity": "sha1-B9kSp6JNrS2b9dRM4yLdxFeo2zc=", - "requires": { - "@babel/helper-module-transforms": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "@babel/helper-simple-access": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-object-assign": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.0.0-beta.54.tgz", - "integrity": "sha1-zbcLQhmYH7IspkAUojITWRikFb4=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.0.0-beta.54.tgz", - "integrity": "sha1-djBvGbmsrGzxNyGvFey584KGT/c=", - "requires": { - "@babel/helper-call-delegate": "7.0.0-beta.54", - "@babel/helper-get-function-arity": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.0.0-beta.54.tgz", - "integrity": "sha1-h8tqK7wl2xb61/bshqEHh/SCEYw=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.0.0-beta.54.tgz", - "integrity": "sha1-fptqYktkBtQ4tEqPrSPUN6dhO2Y=", - "requires": { - "@babel/helper-builder-react-jsx": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "@babel/plugin-syntax-jsx": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.0.0-beta.54.tgz", - "integrity": "sha1-HW7uZf93L7ACuZXlOMDJYV7Wo/c=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54", - "@babel/plugin-syntax-jsx": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0-beta.54.tgz", - "integrity": "sha1-i0bhkvO/4Ja7v4bid2TnZi5fmg8=", - "requires": { - "regenerator-transform": "0.13.3" - }, - "dependencies": { - "regenerator-transform": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz", - "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==", - "requires": { - "private": "0.1.8" - } - } - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0-beta.54.tgz", - "integrity": "sha1-UOc8KvxYmLEFVRDd9g7hOmMBUX8=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0-beta.54.tgz", - "integrity": "sha1-TwhS3w9LHbJCbED6zY/l8Cij28k=", - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0-beta.54.tgz", - "integrity": "sha1-yx9jA8r7hEKmxsaaDfu2BpnzJ7w=", - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.54", - "@babel/helper-plugin-utils": "7.0.0-beta.54" - } - }, - "@babel/register": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.0.0-beta.54.tgz", - "integrity": "sha1-EIKEqzlHiMuGaHCHhxrY6Umr77g=", - "requires": { - "core-js": "2.5.7", - "find-cache-dir": "1.0.0", - "home-or-tmp": "3.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "pirates": "4.0.0", - "source-map-support": "0.4.18" - }, - "dependencies": { - "home-or-tmp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-3.0.0.tgz", - "integrity": "sha1-V6j+JM8zzdUkhgoVgh3cJchmcfs=" - } - } - }, - "@babel/template": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.54.tgz", - "integrity": "sha1-1bDS0tVcDniwSMYaBY82z9fZGvM=", - "requires": { - "@babel/code-frame": "7.0.0-beta.54", - "@babel/parser": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54", - "lodash": "4.17.10" - } - }, - "@babel/traverse": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.54.tgz", - "integrity": "sha1-LBf5jc2/GaqRj94Sjw4aC8CJ4Fo=", - "requires": { - "@babel/code-frame": "7.0.0-beta.54", - "@babel/generator": "7.0.0-beta.54", - "@babel/helper-function-name": "7.0.0-beta.54", - "@babel/helper-split-export-declaration": "7.0.0-beta.54", - "@babel/parser": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54", - "debug": "3.1.0", - "globals": "11.7.0", - "lodash": "4.17.10" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==" - } - } - }, - "@babel/types": { - "version": "7.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.54.tgz", - "integrity": "sha1-AlrWhJL+1ULBPxTFeaRMhI5TEGM=", - "requires": { - "esutils": "2.0.2", - "lodash": "4.17.10", - "to-fast-properties": "2.0.0" - }, - "dependencies": { - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - } - } - }, - "@tradle/react-native-http": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tradle/react-native-http/-/react-native-http-2.0.1.tgz", - "integrity": "sha1-rxniQOHlgL+iSVY5JNG+RyaG9Is=", - "requires": { - "Base64": "0.2.1", - "inherits": "2.0.3" - } - }, - "Base64": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", - "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "absolute-path": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/absolute-path/-/absolute-path-0.0.0.tgz", - "integrity": "sha1-p4di+9rftSl76ZsV01p4Wy8JW/c=" - }, - "abstract-leveldown": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.1.tgz", - "integrity": "sha1-+QFKVmm3RkGOFFFo3qSaBErhWQA=", - "requires": { - "xtend": "4.0.1" - } - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "2.1.18", - "negotiator": "0.6.1" - } - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz", - "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=" - }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" - }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "3.1.10", - "normalize-path": "2.1.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - } - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "arch": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", - "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "argsarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/argsarray/-/argsarray-0.0.1.tgz", - "integrity": "sha1-bnIHtOzbObCviDA/pa4ivajfYcs=" - }, - "aria-query": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", - "integrity": "sha1-Jsu1r/ZBRLCoJb4YRuCxbPoAsR4=", - "requires": { - "ast-types-flow": "0.0.7", - "commander": "2.16.0" - } - }, - "arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", - "requires": { - "arr-flatten": "1.1.0", - "array-slice": "0.2.3" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=" - }, - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "requires": { - "typical": "2.6.1" - } - }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" - }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.12.0" - } - }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=" - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=" - }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=" - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" - }, - "art": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/art/-/art-0.10.3.tgz", - "integrity": "sha512-HXwbdofRTiJT6qZX/FnchtldzJjS3vkLJxQilc3Xj+ma2MXjY4UAyQ0ls1XZYVnDvVIBiFZbC6QsvtW86TD6tQ==" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "requires": { - "bn.js": "4.11.8", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" - } - }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "requires": { - "util": "0.10.3" - } - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "asyncstorage-down": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/asyncstorage-down/-/asyncstorage-down-4.0.1.tgz", - "integrity": "sha1-OAtISrYpqths1rcj4E8IwForAGc=", - "requires": { - "abstract-leveldown": "2.6.1", - "argsarray": "0.0.1", - "d64": "1.0.0", - "ltgt": "2.2.1", - "tiny-queue": "0.2.0" - } - }, - "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "axobject-query": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", - "integrity": "sha1-YvWdvFnJ+SQnWco0mWDnov48NsA=", - "requires": { - "ast-types-flow": "0.0.7" - } - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "babel-core": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz", - "integrity": "sha1-jEKFZNzh4fQfszfsNPTDsCK1rYM=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.24.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - }, - "dependencies": { - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - } - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - } - } - } - } - }, - "babel-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz", - "integrity": "sha1-5xX0hsWN7SVknYiJRNUqoHxdlJc=", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "requires": { - "babel-helper-explode-assignable-expression": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.18.0.tgz", - "integrity": "sha1-qwLxmi63rOk23Yf6VYltAr5Zv3E=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.10" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "requires": { - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "requires": { - "babel-helper-optimise-call-expression": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-external-helpers": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz", - "integrity": "sha1-IoX0iwK9Xe3oUXXK+MYuhq3M76E=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-react-transform": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-react-transform/-/babel-plugin-react-transform-3.0.0.tgz", - "integrity": "sha512-4vJGddwPiHAOgshzZdGwYy4zRjjIr5SMY7gkOaCyIASjgpcsyLTlZNuB5rHOFoaTvGlhfo8/g4pobXPyHqm/3w==", - "requires": { - "lodash": "4.17.10" - } - }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=" - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=" - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=" - }, - "babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=" - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.16.0.tgz", - "integrity": "sha1-Gew2yxSGtZ+fRorfpCzhOQjKKZk=", - "requires": { - "babel-helper-remap-async-to-generator": "6.24.1", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.15.0.tgz", - "integrity": "sha1-W0Q8oUK+jR22qMKuQvUZWLZrcPY=", - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "requires": { - "babel-helper-define-map": "6.26.0", - "babel-helper-function-name": "6.24.1", - "babel-helper-optimise-call-expression": "6.24.1", - "babel-helper-replace-supers": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", - "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", - "requires": { - "babel-plugin-transform-strict-mode": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "requires": { - "babel-helper-replace-supers": "6.24.1", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "requires": { - "babel-helper-call-delegate": "6.24.1", - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "regexpu-core": "2.0.0" - } - }, - "babel-plugin-transform-es3-member-expression-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz", - "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es3-property-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.22.0.tgz", - "integrity": "sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", - "babel-plugin-syntax-exponentiation-operator": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "requires": { - "babel-plugin-syntax-flow": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-object-assign": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-assign/-/babel-plugin-transform-object-assign-6.22.0.tgz", - "integrity": "sha1-+Z0vZvGgsNSY40bFNZaEdAyqILo=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "requires": { - "babel-helper-builder-react-jsx": "6.26.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - }, - "dependencies": { - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "2.0.2" - } - } - } - }, - "babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "requires": { - "regenerator-transform": "0.10.1" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-preset-es2015-node": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-preset-es2015-node/-/babel-preset-es2015-node-6.1.1.tgz", - "integrity": "sha1-YLIxVwJLDP6/OmNVTLBe4DW05V8=", - "requires": { - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "6.24.1", - "babel-plugin-transform-es2015-unicode-regex": "6.24.1", - "semver": "5.5.0" - } - }, - "babel-preset-fbjs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz", - "integrity": "sha512-6XVQwlO26V5/0P9s2Eje8Epqkv/ihaMJ798+W98ktOA8fCn2IFM6wEi7CDW3fTbKFZ/8fDGvGZH01B6GSuNiWA==", - "requires": { - "babel-plugin-check-es2015-constants": "6.22.0", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-plugin-syntax-flow": "6.18.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-es2015-arrow-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoping": "6.15.0", - "babel-plugin-transform-es2015-classes": "6.24.1", - "babel-plugin-transform-es2015-computed-properties": "6.24.1", - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-for-of": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-literals": "6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-plugin-transform-es2015-object-super": "6.24.1", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-template-literals": "6.22.0", - "babel-plugin-transform-es3-member-expression-literals": "6.22.0", - "babel-plugin-transform-es3-property-literals": "6.22.0", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-plugin-transform-react-display-name": "6.25.0", - "babel-plugin-transform-react-jsx": "6.24.1" - } - }, - "babel-preset-react-native": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/babel-preset-react-native/-/babel-preset-react-native-4.0.0.tgz", - "integrity": "sha512-Wfbo6x244nUbBxjr7hQaNFdjj7FDYU+TVT7cFVPEdVPI68vhN52iLvamm+ErhNdHq6M4j1cMT6AJBYx7Wzdr0g==", - "requires": { - "babel-plugin-check-es2015-constants": "6.22.0", - "babel-plugin-react-transform": "3.0.0", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-plugin-syntax-dynamic-import": "6.18.0", - "babel-plugin-syntax-flow": "6.18.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-es2015-arrow-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoping": "6.15.0", - "babel-plugin-transform-es2015-classes": "6.24.1", - "babel-plugin-transform-es2015-computed-properties": "6.24.1", - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-for-of": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-literals": "6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-template-literals": "6.22.0", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-plugin-transform-object-assign": "6.22.0", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-plugin-transform-react-display-name": "6.25.0", - "babel-plugin-transform-react-jsx": "6.24.1", - "babel-plugin-transform-react-jsx-source": "6.22.0", - "babel-plugin-transform-regenerator": "6.26.0", - "babel-template": "6.26.0", - "react-transform-hmr": "1.0.4" - } - }, - "babel-register": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.18.0.tgz", - "integrity": "sha1-iS4uA4ZQeN2QrSxxURHsREmzKmg=", - "requires": { - "babel-core": "6.24.1", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.7", - "regenerator-runtime": "0.11.1" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.10" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.4", - "lodash": "4.17.10" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.10", - "to-fast-properties": "1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "pascalcase": "0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" - }, - "base64-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" - }, - "basic-auth": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", - "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", - "requires": { - "safe-buffer": "5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "big-integer": { - "version": "1.6.32", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.32.tgz", - "integrity": "sha512-ljKJdR3wk9thHfLj4DtrNiOSTxvGFaMjWrG4pW75juXC4j7+XuKJVFdg4kgFMYp85PVkO05dFMj2dk2xVsH4xw==" - }, - "bignumber.js": { - "version": "git+https://github.com/status-im/bignumber.js.git#cc066a0a3d6bfe0c436c9957f4ea8344bf963c89" - }, - "bl": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-0.8.2.tgz", - "integrity": "sha1-yba8oI0bwuoA/Ir7Txpf0eHGbk4=", - "requires": { - "readable-stream": "1.0.34" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "2.0.3" - } - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.16.3" - } - }, - "bplist-creator": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", - "integrity": "sha1-N98VNgkoJLh8QvlXsBNEEXNyrkU=", - "requires": { - "stream-buffers": "2.2.0" - } - }, - "bplist-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.1.tgz", - "integrity": "sha1-1g1dzCDLptx+HymbNdPh+V2vuuY=", - "requires": { - "big-integer": "1.6.32" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "1.0.3", - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "inherits": "2.0.3", - "safe-buffer": "5.1.2" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "1.2.0", - "browserify-des": "1.0.1", - "evp_bytestokey": "1.0.3" - } - }, - "browserify-des": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", - "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", - "requires": { - "cipher-base": "1.0.4", - "des.js": "1.0.0", - "inherits": "2.0.3" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "requires": { - "bn.js": "4.11.8", - "randombytes": "2.0.6" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "elliptic": "6.4.0", - "inherits": "2.0.3", - "parse-asn1": "5.1.1" - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "requires": { - "node-int64": "0.4.0" - } - }, - "buffer": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", - "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", - "requires": { - "base64-js": "0.0.8", - "ieee754": "1.1.12", - "isarray": "1.0.0" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "1.1.0", - "buffer-fill": "1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-from": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", - "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.2.1", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.0", - "to-object-path": "0.3.0", - "union-value": "1.0.0", - "unset-value": "1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "capture-exit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", - "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", - "requires": { - "rsvp": "3.6.2" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "chance": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.0.12.tgz", - "integrity": "sha512-W99uMuboG5CT1iToDmizEH6yQYqICzZnrSRbbXPuJErzFWLPaoiEDvwnKbESjDo/8st1n3pyh70VBMmfqPmf+Q==" - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.2" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" - }, - "dependencies": { - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "2.0.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "clipboardy": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.3.tgz", - "integrity": "sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA==", - "requires": { - "arch": "2.1.1", - "execa": "0.8.0" - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "clone": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.1.19.tgz", - "integrity": "sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" - } - }, - "color": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz", - "integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==", - "requires": { - "color-convert": "1.9.2", - "color-string": "1.5.2" - } - }, - "color-convert": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", - "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "requires": { - "color-name": "1.1.1" - } - }, - "color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" - }, - "color-string": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.2.tgz", - "integrity": "sha1-JuRYFLw8mny9Z1FkikFDRRSnc6k=", - "requires": { - "color-name": "1.1.1", - "simple-swizzle": "0.2.2" - } - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "command-line-args": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.7.tgz", - "integrity": "sha512-aUdPvQRAyBvQd2n7jXcsMDz68ckBJELXNzBybCHOibUWEg0mWTnaYCSRU8h9R+aNRSvDihJtssSRCiDRpLaezA==", - "requires": { - "array-back": "2.0.0", - "find-replace": "1.0.3", - "typical": "2.6.1" - } - }, - "commander": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", - "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "compressible": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", - "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", - "requires": { - "mime-db": "1.35.0" - }, - "dependencies": { - "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" - } - } - }, - "compression": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", - "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", - "requires": { - "accepts": "1.3.5", - "bytes": "3.0.0", - "compressible": "2.0.14", - "debug": "2.6.9", - "on-headers": "1.0.1", - "safe-buffer": "5.1.2", - "vary": "1.1.2" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "1.1.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" - } - }, - "connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "1.3.2", - "utils-merge": "1.0.1" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "requires": { - "bn.js": "4.11.8", - "elliptic": "6.4.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.3", - "md5.js": "1.3.4", - "ripemd160": "2.0.2", - "sha.js": "2.4.11" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "inherits": "2.0.3", - "ripemd160": "2.0.2", - "safe-buffer": "5.1.2", - "sha.js": "2.4.11" - } - }, - "create-react-class": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", - "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "4.1.3", - "shebang-command": "1.2.0", - "which": "1.3.1" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.10.1" - } - }, - "crypto-js": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz", - "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU=" - }, - "d64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz", - "integrity": "sha1-QAKofoUMv8n52XBrYPymE6MzbpA=" - }, - "damerau-levenshtein": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", - "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", - "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", - "requires": { - "decompress-tar": "4.1.1", - "decompress-tarbz2": "4.1.1", - "decompress-targz": "4.1.1", - "decompress-unzip": "4.0.1", - "graceful-fs": "4.1.11", - "make-dir": "1.3.0", - "pify": "2.3.0", - "strip-dirs": "2.1.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "requires": { - "file-type": "5.2.0", - "is-stream": "1.1.0", - "tar-stream": "1.6.1" - } - }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "requires": { - "decompress-tar": "4.1.1", - "file-type": "6.2.0", - "is-stream": "1.1.0", - "seek-bzip": "1.0.5", - "unbzip2-stream": "1.2.5" - }, - "dependencies": { - "file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==" - } - } - }, - "decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "requires": { - "decompress-tar": "4.1.1", - "file-type": "5.2.0", - "is-stream": "1.1.0" - } - }, - "decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", - "requires": { - "file-type": "3.9.0", - "get-stream": "2.3.1", - "pify": "2.3.0", - "yauzl": "2.10.0" - }, - "dependencies": { - "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=" - }, - "get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", - "requires": { - "object-assign": "4.1.1", - "pinkie-promise": "2.0.1" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deferred-leveldown": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz", - "integrity": "sha1-LO8fER4cV4cNi7uK8mUOWHzS9bQ=", - "requires": { - "abstract-leveldown": "0.12.4" - }, - "dependencies": { - "abstract-leveldown": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz", - "integrity": "sha1-KeGOYy5g5OIh1YECR4UqY9ey5BA=", - "requires": { - "xtend": "3.0.0" - } - }, - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" - } - } - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.12" - }, - "dependencies": { - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" - } - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "requires": { - "repeating": "2.0.1" - } - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "4.11.8", - "miller-rabin": "4.0.1", - "randombytes": "2.0.6" - } - }, - "dns.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dns.js/-/dns.js-1.0.1.tgz", - "integrity": "sha1-v8CVDXl3dyjGdQCxxBgHXmU5Ve4=" - }, - "dom-walk": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", - "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.5", - "hmac-drbg": "1.0.1", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "emoji-regex": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", - "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==" - }, - "emojilib": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.2.9.tgz", - "integrity": "sha512-RbYMz3G8OFBZYsqHQp8kiM/Hsg/0bSRWG1iRPqvQ04s4RUQH+iBLFjybSM6fv2rV/vQrywOEL5m31BjnfyDUlw==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "0.4.23" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "1.4.0" - } - }, - "envinfo": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-3.11.1.tgz", - "integrity": "sha512-hKkh7aKtont6Zuv4RmE4VkOc96TkBj9NXj7Ghsd/qCA9LuJI0Dh+ImwA1N5iORB9Vg+sz5bq9CHJzs51BILNCQ==", - "requires": { - "clipboardy": "1.2.3", - "glob": "7.1.2", - "minimist": "1.2.0", - "os-name": "2.0.1", - "which": "1.3.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "requires": { - "prr": "1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "0.2.1" - } - }, - "errorhandler": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.0.tgz", - "integrity": "sha1-6rpkyl1UKjEayUX1gt78M2Fl2fQ=", - "requires": { - "accepts": "1.3.5", - "escape-html": "1.0.3" - } - }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "is-callable": "1.1.4", - "is-regex": "1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "requires": { - "is-callable": "1.1.4", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint-plugin-jsx-a11y": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.1.tgz", - "integrity": "sha512-5I9SpoP7gT4wBFOtXT8/tXNPYohHBVfyVfO17vkbC7r9kEIxYJF12D3pKqhk8+xnk12rfxKClS3WCFpVckFTPQ==", - "requires": { - "aria-query": "0.7.1", - "array-includes": "3.0.3", - "ast-types-flow": "0.0.7", - "axobject-query": "0.1.0", - "damerau-levenshtein": "1.0.4", - "emoji-regex": "6.5.1", - "jsx-ast-utils": "1.4.1" - } - }, - "eslint-plugin-react-native": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-3.2.1.tgz", - "integrity": "sha512-1AnJO3JUCAoLpyOEsWCwN9hPJ0aQ72OT+JvLMuHjEWYb6QWxiNOszp24CEwegMzbREtJKI9OoRqYYDYxMxmjgQ==", - "requires": { - "eslint-plugin-react-native-globals": "0.1.2" - } - }, - "eslint-plugin-react-native-globals": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", - "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==" - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "event-target-shim": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz", - "integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE=" - }, - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "1.3.4", - "safe-buffer": "5.1.2" - } - }, - "exec-sh": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", - "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", - "requires": { - "merge": "1.2.0" - } - }, - "execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "requires": { - "fill-range": "2.2.4" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", - "requires": { - "kind-of": "1.1.0" - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "requires": { - "chardet": "0.4.2", - "iconv-lite": "0.4.23", - "tmp": "0.0.33" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "requires": { - "is-extglob": "1.0.0" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fancy-log": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", - "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", - "requires": { - "ansi-gray": "0.1.1", - "color-support": "1.1.3", - "time-stamp": "1.1.0" - } - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "requires": { - "bser": "2.0.0" - } - }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "requires": { - "core-js": "1.2.7", - "isomorphic-fetch": "2.2.1", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "promise": "7.3.1", - "setimmediate": "1.0.5", - "ua-parser-js": "0.7.18" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - } - } - }, - "fbjs-scripts": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fbjs-scripts/-/fbjs-scripts-0.8.3.tgz", - "integrity": "sha512-aUJ/uEzMIiBYuj/blLp4sVNkQQ7ZEB/lyplG1IzzOmZ83meiWecrGg5jBo4wWrxXmO4RExdtsSV1QkTjPt2Gag==", - "requires": { - "ansi-colors": "1.1.0", - "babel-core": "6.24.1", - "babel-preset-fbjs": "2.1.4", - "core-js": "2.5.7", - "cross-spawn": "5.1.0", - "fancy-log": "1.3.2", - "object-assign": "4.1.1", - "plugin-error": "0.1.2", - "semver": "5.5.0", - "through2": "2.0.3" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "requires": { - "pend": "1.2.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "1.0.5" - } - }, - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=" - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "3.0.0", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "requires": { - "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" - } - }, - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", - "requires": { - "commondir": "1.0.1", - "make-dir": "1.3.0", - "pkg-dir": "2.0.0" - } - }, - "find-replace": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", - "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=", - "requires": { - "array-back": "1.0.4", - "test-value": "2.1.0" - }, - "dependencies": { - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "requires": { - "typical": "2.6.1" - } - } - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "2.0.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "requires": { - "for-in": "1.0.2" - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0", - "klaw": "1.3.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "fwd-stream": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fwd-stream/-/fwd-stream-1.0.4.tgz", - "integrity": "sha1-7Sgcq+1G/uz5Ie4y3ExQs3KsfPo=", - "requires": { - "readable-stream": "1.0.34" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "gauge": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", - "integrity": "sha1-6c7FSD09TuDvRLYKfZnkk14TbZM=", - "requires": { - "ansi": "0.3.1", - "has-unicode": "2.0.1", - "lodash.pad": "4.5.1", - "lodash.padend": "4.6.1", - "lodash.padstart": "4.6.1" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "1.0.0" - } - } - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "requires": { - "is-glob": "2.0.1" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "1.0.0" - } - } - } - }, - "global": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", - "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", - "requires": { - "min-document": "2.19.0", - "process": "0.5.2" - }, - "dependencies": { - "process": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" - } - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.2" - } - }, - "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", - "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "1.1.5", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "homoglyph-finder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/homoglyph-finder/-/homoglyph-finder-1.1.1.tgz", - "integrity": "sha1-U8NnGmGOADom+j+H3CsXnpVWj6g=" - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" - }, - "http-basic": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-2.5.1.tgz", - "integrity": "sha1-jORHvbW2xXf4pj4/p4BW7Eu02/s=", - "requires": { - "caseless": "0.11.0", - "concat-stream": "1.6.2", - "http-response-object": "1.1.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - } - } - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": "1.5.0" - }, - "dependencies": { - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - } - } - }, - "http-response-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-1.1.0.tgz", - "integrity": "sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM=" - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" - } - }, - "i18n-js": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/i18n-js/-/i18n-js-3.0.11.tgz", - "integrity": "sha512-v7dG3kYJTQTyox3NqDabPDE/ZotWntyMI9kh4cYi+XlCSnsIR+KBTS2opPyObL8WndnklcLzbNU92FP/mLge3Q==" - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": "2.1.2" - } - }, - "identicon.js": { - "version": "git+https://github.com/status-im/identicon.js.git#4cd179daaf6f01384502b4b2c1876f982db0f57e" - }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" - }, - "image-size": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", - "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "requires": { - "ansi-escapes": "3.1.0", - "chalk": "2.4.1", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "2.2.0", - "figures": "2.0.0", - "lodash": "4.17.10", - "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rx-lite": "4.0.8", - "rx-lite-aggregates": "4.0.8", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "1.9.2" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "3.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "instabug-reactnative": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/instabug-reactnative/-/instabug-reactnative-2.12.0.tgz", - "integrity": "sha512-aD2JLofdXfZWvlcg3t3lEoUM+HN7qLaKry68Ypjuh+cFGCXtblbKz4ozEfEFBw4rvEm31s8BSd1e/aFiHPXBmQ==" - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "1.3.1" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "ip-regex": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", - "integrity": "sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=" - }, - "is": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/is/-/is-0.2.7.tgz", - "integrity": "sha1-OzSixI81mXLzUEKEkZOucmS2NWI=" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=" - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-object": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-0.1.2.tgz", - "integrity": "sha1-AO+8CIFsM8/ErIJR0TLhDcZQmNc=" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "requires": { - "has": "1.0.3" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "1.7.3", - "whatwg-fetch": "2.0.4" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jest-docblock": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-22.4.0.tgz", - "integrity": "sha512-lDY7GZ+/CJb02oULYLBDj7Hs5shBhVpDYpIm8LUyqw9X2J22QRsM19gmGQwIFqGSJmpc/LRrSYudeSrG510xlQ==", - "requires": { - "detect-newline": "2.1.0" - } - }, - "jest-haste-map": { - "version": "22.4.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-22.4.2.tgz", - "integrity": "sha512-EdQADHGXRqHJYAr7q9B9YYHZnrlcMwhx1+DnIgc9uN05nCW3RvGCxJ91MqWXcC1AzatLoSv7SNd0qXMp2jKBDA==", - "requires": { - "fb-watchman": "2.0.0", - "graceful-fs": "4.1.11", - "jest-docblock": "22.4.0", - "jest-serializer": "22.4.3", - "jest-worker": "22.2.2", - "micromatch": "2.3.11", - "sane": "2.5.2" - } - }, - "jest-serializer": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-22.4.3.tgz", - "integrity": "sha512-uPaUAppx4VUfJ0QDerpNdF43F68eqKWCzzhUlKNDsUPhjOon7ZehR4C809GCqh765FoMRtTVUVnGvIoskkYHiw==" - }, - "jest-worker": { - "version": "22.2.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-22.2.2.tgz", - "integrity": "sha512-ZylDXjrFNt/OP6cUxwJFWwDgazP7hRjtCQbocFHyiwov+04Wm1x5PYzMGNJT53s4nwr0oo9ocYTImS09xOlUnw==", - "requires": { - "merge-stream": "1.0.1" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "jsx-ast-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", - "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=" - }, - "keymirror": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz", - "integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU=" - }, - "kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=" - }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "1.0.0" - } - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" - }, - "level-blobs": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/level-blobs/-/level-blobs-0.1.7.tgz", - "integrity": "sha1-mrm5e7mfHtv594o0M+Ie1WOGva8=", - "requires": { - "level-peek": "1.0.6", - "once": "1.4.0", - "readable-stream": "1.1.14" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "level-filesystem": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/level-filesystem/-/level-filesystem-1.2.0.tgz", - "integrity": "sha1-oArKmRnEpN+v3KaoEI0iWq3/Y7M=", - "requires": { - "concat-stream": "1.6.2", - "errno": "0.1.7", - "fwd-stream": "1.0.4", - "level-blobs": "0.1.7", - "level-peek": "1.0.6", - "level-sublevel": "5.2.3", - "octal": "1.0.0", - "once": "1.4.0", - "xtend": "2.2.0" - }, - "dependencies": { - "xtend": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz", - "integrity": "sha1-7vax8ZjByN6vrYsXZaBNrUoBxak=" - } - } - }, - "level-fix-range": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/level-fix-range/-/level-fix-range-1.0.2.tgz", - "integrity": "sha1-vxW5Fa422EcMgh6IPd95zRZCCCg=" - }, - "level-hooks": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/level-hooks/-/level-hooks-4.5.0.tgz", - "integrity": "sha1-G5rmGSKTDzMF0aYfxNg8gQLA3ZM=", - "requires": { - "string-range": "1.2.2" - } - }, - "level-peek": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/level-peek/-/level-peek-1.0.6.tgz", - "integrity": "sha1-vsUccqgu5GTTNkNMfIdsP8vM538=", - "requires": { - "level-fix-range": "1.0.2" - } - }, - "level-sublevel": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/level-sublevel/-/level-sublevel-5.2.3.tgz", - "integrity": "sha1-dEwSxy0ucr543eO5tc2E1iGRQTo=", - "requires": { - "level-fix-range": "2.0.0", - "level-hooks": "4.5.0", - "string-range": "1.2.2", - "xtend": "2.0.6" - }, - "dependencies": { - "level-fix-range": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/level-fix-range/-/level-fix-range-2.0.0.tgz", - "integrity": "sha1-xBfWIVlEIVGhnZojZ4aPFyTC1Ug=", - "requires": { - "clone": "0.1.19" - } - }, - "xtend": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.0.6.tgz", - "integrity": "sha1-XqZXptukRwacLlnFihE4ywxebO4=", - "requires": { - "is-object": "0.1.2", - "object-keys": "0.2.0" - } - } - } - }, - "levelup": { - "version": "0.18.6", - "resolved": "https://registry.npmjs.org/levelup/-/levelup-0.18.6.tgz", - "integrity": "sha1-5qAcsIlhbI7MApHCqb0/DETj5es=", - "requires": { - "bl": "0.8.2", - "deferred-leveldown": "0.2.0", - "errno": "0.1.7", - "prr": "0.0.0", - "readable-stream": "1.0.34", - "semver": "2.3.2", - "xtend": "3.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "prr": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", - "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "semver": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-2.3.2.tgz", - "integrity": "sha1-uYSPJdbPNjMwc+ye+IVtQvEjPlI=" - }, - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" - } - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash.pad": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", - "integrity": "sha1-QzCUmoM6fI2iLMIPaibE1Z3runA=" - }, - "lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" - }, - "lodash.padstart": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.6.1.tgz", - "integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=" - }, - "lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "3.0.2" - } - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "ltgt": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", - "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" - }, - "macos-release": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-1.1.0.tgz", - "integrity": "sha512-mmLbumEYMi5nXReB9js3WGsB8UE6cDBWyIO62Z4DNx6GbRhDxHNjA1MlzSpJ2S2KM1wyiPRA0d19uHWYYvMHjA==" - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "3.0.0" - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "requires": { - "tmpl": "1.0.4" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "1.0.1" - } - }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" - }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.3" - } - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "requires": { - "mimic-fn": "1.2.0" - } - }, - "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "requires": { - "readable-stream": "2.3.6" - } - }, - "metro": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.30.2.tgz", - "integrity": "sha512-wmdkh4AsfZjWaMM++KMDswQHdyo5L9a0XAaQBL4XTJdQIRG+x+Rmjixe7tDki5jKwe9XxsjjbpbdYKswOANuiw==", - "requires": { - "@babel/core": "7.0.0-beta.54", - "@babel/generator": "7.0.0-beta.54", - "@babel/helper-remap-async-to-generator": "7.0.0-beta.54", - "@babel/plugin-external-helpers": "7.0.0-beta.54", - "@babel/plugin-proposal-class-properties": "7.0.0-beta.54", - "@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.54", - "@babel/plugin-syntax-dynamic-import": "7.0.0-beta.54", - "@babel/plugin-transform-arrow-functions": "7.0.0-beta.54", - "@babel/plugin-transform-block-scoping": "7.0.0-beta.54", - "@babel/plugin-transform-classes": "7.0.0-beta.54", - "@babel/plugin-transform-computed-properties": "7.0.0-beta.54", - "@babel/plugin-transform-destructuring": "7.0.0-beta.54", - "@babel/plugin-transform-exponentiation-operator": "7.0.0-beta.54", - "@babel/plugin-transform-flow-strip-types": "7.0.0-beta.54", - "@babel/plugin-transform-for-of": "7.0.0-beta.54", - "@babel/plugin-transform-function-name": "7.0.0-beta.54", - "@babel/plugin-transform-literals": "7.0.0-beta.54", - "@babel/plugin-transform-modules-commonjs": "7.0.0-beta.54", - "@babel/plugin-transform-object-assign": "7.0.0-beta.54", - "@babel/plugin-transform-parameters": "7.0.0-beta.54", - "@babel/plugin-transform-react-display-name": "7.0.0-beta.54", - "@babel/plugin-transform-react-jsx": "7.0.0-beta.54", - "@babel/plugin-transform-react-jsx-source": "7.0.0-beta.54", - "@babel/plugin-transform-regenerator": "7.0.0-beta.54", - "@babel/plugin-transform-shorthand-properties": "7.0.0-beta.54", - "@babel/plugin-transform-spread": "7.0.0-beta.54", - "@babel/plugin-transform-template-literals": "7.0.0-beta.54", - "@babel/register": "7.0.0-beta.54", - "@babel/template": "7.0.0-beta.54", - "@babel/traverse": "7.0.0-beta.54", - "@babel/types": "7.0.0-beta.54", - "absolute-path": "0.0.0", - "async": "2.6.1", - "babel-core": "6.24.1", - "babel-generator": "6.26.1", - "babel-plugin-external-helpers": "6.22.0", - "babel-plugin-react-transform": "3.0.0", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-preset-es2015-node": "6.1.1", - "babel-preset-fbjs": "2.1.4", - "babel-preset-react-native": "4.0.0", - "babel-register": "6.26.0", - "babel-template": "6.26.0", - "babylon": "6.18.0", - "chalk": "1.1.3", - "concat-stream": "1.6.2", - "connect": "3.6.6", - "core-js": "2.5.7", - "debug": "2.6.9", - "denodeify": "1.2.1", - "eventemitter3": "3.1.0", - "fbjs": "0.8.17", - "fs-extra": "1.0.0", - "graceful-fs": "4.1.11", - "image-size": "0.6.3", - "jest-docblock": "22.4.0", - "jest-haste-map": "22.4.2", - "jest-worker": "22.2.2", - "json-stable-stringify": "1.0.1", - "json5": "0.4.0", - "left-pad": "1.3.0", - "lodash.throttle": "4.1.1", - "merge-stream": "1.0.1", - "metro-babylon7": "0.30.2", - "metro-cache": "0.30.2", - "metro-core": "0.30.2", - "metro-minify-uglify": "0.30.2", - "metro-resolver": "0.30.2", - "metro-source-map": "0.30.2", - "mime-types": "2.1.11", - "mkdirp": "0.5.1", - "node-fetch": "1.7.3", - "resolve": "1.8.1", - "rimraf": "2.6.2", - "serialize-error": "2.1.0", - "source-map": "0.5.7", - "temp": "0.8.3", - "throat": "4.1.0", - "wordwrap": "1.0.0", - "write-file-atomic": "1.3.4", - "ws": "1.1.5", - "xpipe": "1.0.5", - "yargs": "9.0.1" - }, - "dependencies": { - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "4.17.10" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - } - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0", - "klaw": "1.3.1" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "json5": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", - "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=" - }, - "mime-db": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", - "integrity": "sha1-oxtAcK2uon1zLqMzdApk0OyaZlk=" - }, - "mime-types": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", - "integrity": "sha1-wlnEcb2oCKhdbNGTtDCl+uRHOzw=", - "requires": { - "mime-db": "1.23.0" - } - }, - "yargs": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", - "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", - "requires": { - "camelcase": "4.1.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.3", - "os-locale": "2.1.0", - "read-pkg-up": "2.0.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "7.0.0" - } - } - } - }, - "metro-babylon7": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-babylon7/-/metro-babylon7-0.30.2.tgz", - "integrity": "sha512-ZI0h4/3raGnzA6fFXwLUMidGOG4jkDi9fgFkoI8I4Ack3TDMabmZATu9RD6DaSolu3lylhfPd8DeAAMeopX9CA==", - "requires": { - "babylon": "7.0.0-beta.47" - }, - "dependencies": { - "babylon": { - "version": "7.0.0-beta.47", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz", - "integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==" - } - } - }, - "metro-cache": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.30.2.tgz", - "integrity": "sha512-XYd07OwgtZRHFXyip40wdNJ8abPJRziuE5bb3jjf8wvyHxCpzlZlvbe0ZhcR8ChBwFUjHMuVyoou52AC3a0f+g==", - "requires": { - "jest-serializer": "22.4.3", - "mkdirp": "0.5.1" - } - }, - "metro-core": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.30.2.tgz", - "integrity": "sha512-2Y89PpD9sE/8QaHhYxaI21WFxkVmjbxdphiOPdsC9t7A3kQHMYOTQPYFon3bkYM7tL8k9YVBimXSv20JGglqUA==", - "requires": { - "lodash.throttle": "4.1.1", - "wordwrap": "1.0.0" - } - }, - "metro-minify-uglify": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-minify-uglify/-/metro-minify-uglify-0.30.2.tgz", - "integrity": "sha512-xwqMqYYKZEqJ66Wpf5OpyPJhApOQDb8rYiO94VInlDeHpN7eKGCVILclnx9AmVM3dStmebvXa5jrdgsbnJ1bSg==", - "requires": { - "uglify-es": "3.3.9" - } - }, - "metro-resolver": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.30.2.tgz", - "integrity": "sha512-bODCys/WYpqJ+KYbCIENZu1eqdQu3g/d2fXfhAROhutqojMqrT1eIGhzWpk3G1k/J6vlaf69uW6xrVuheg0ktg==", - "requires": { - "absolute-path": "0.0.0" - } - }, - "metro-source-map": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.30.2.tgz", - "integrity": "sha512-9tW3B1JOdXhyDJnR4wOPOsOlYWSL+xh6J+N5/DADGEK/X/+Up/lEHdEfpB+/+yGk1LHaRHcKCahtLPNl/to7Sg==", - "requires": { - "source-map": "0.5.7" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - }, - "dependencies": { - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "requires": { - "arr-flatten": "1.1.0" - } - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "1.0.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "1.33.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "requires": { - "dom-walk": "0.1.1" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "morgan": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", - "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", - "requires": { - "basic-auth": "2.0.0", - "debug": "2.6.9", - "depd": "1.1.2", - "on-finished": "2.3.0", - "on-headers": "1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-windows": "1.0.2", - "kind-of": "6.0.2", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "0.1.12", - "is-stream": "1.1.0" - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" - }, - "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", - "requires": { - "growly": "1.3.0", - "semver": "5.5.0", - "shellwords": "0.1.1", - "which": "1.3.1" - } - }, - "node-pre-gyp": { - "version": "0.6.39", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "requires": { - "detect-libc": "1.0.3", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.8", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "2.2.1", - "tar-pack": "3.4.1" - }, - "dependencies": { - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.3" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "1.1.5", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.2", - "stringstream": "0.0.6", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "requires": { - "hosted-git-info": "2.7.1", - "is-builtin-module": "1.0.0", - "semver": "5.5.0", - "validate-npm-package-license": "3.0.3" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "2.0.1" - } - }, - "npmlog": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz", - "integrity": "sha1-mLUlMPJRTKkNCexbIsiEZyI3VpI=", - "requires": { - "ansi": "0.3.1", - "are-we-there-yet": "1.1.5", - "gauge": "1.2.7" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "object-keys": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.2.0.tgz", - "integrity": "sha1-zd7AKZiwkb5CvxA1rjLknxy26mc=", - "requires": { - "foreach": "2.0.5", - "indexof": "0.0.1", - "is": "0.2.7" - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "octal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/octal/-/octal-1.0.0.tgz", - "integrity": "sha1-Y+cWKmjvvrniE1iNWOmJ0eXEUws=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "1.2.0" - } - }, - "opn": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz", - "integrity": "sha1-ttmec5n3jWXDuq/+8fsojpuFJDo=", - "requires": { - "object-assign": "4.1.1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "0.0.8", - "wordwrap": "0.0.3" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - } - } - }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" - }, - "dependencies": { - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - } - } - }, - "os-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-2.0.1.tgz", - "integrity": "sha1-uaOGNhwXrjohc27wWZQFyajF3F4=", - "requires": { - "macos-release": "1.1.0", - "win-release": "1.1.1" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "1.3.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", - "requires": { - "asn1.js": "4.10.1", - "browserify-aes": "1.2.0", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "pbkdf2": "3.0.8" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "1.0.0" - } - } - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "1.3.2" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "requires": { - "pify": "2.3.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "pbkdf2": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.8.tgz", - "integrity": "sha1-L4q/FuvsyCJ3lF10irodeHYfYeI=", - "requires": { - "create-hmac": "1.1.7" - } - }, - "pegjs": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", - "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=" - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "2.0.4" - } - }, - "pirates": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.0.tgz", - "integrity": "sha512-8t5BsXy1LUIjn3WWOlOuFDuKswhQb/tkak641lvBgmPOBUQHXveORtlMCp6OdPV1dtuTaEahKA8VNz6uLfKBtA==", - "requires": { - "node-modules-regexp": "1.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "requires": { - "find-up": "2.1.0" - } - }, - "plist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz", - "integrity": "sha1-CEtQk93JJQbiWfh0uNmxr7jHlZM=", - "requires": { - "base64-js": "0.0.8", - "util-deprecate": "1.0.2", - "xmlbuilder": "4.0.0", - "xmldom": "0.1.27" - } - }, - "plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", - "requires": { - "ansi-cyan": "0.1.1", - "ansi-red": "0.1.1", - "arr-diff": "1.1.0", - "arr-union": "2.1.0", - "extend-shallow": "1.1.4" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" - }, - "pretty-format": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-4.3.1.tgz", - "integrity": "sha1-UwvlxCs8BbNkFKeipDN6qArNDo0=" - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "2.0.6" - } - }, - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "public-encrypt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", - "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "parse-asn1": "5.1.1", - "randombytes": "2.0.6" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qr.js": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", - "integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=" - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "querystringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", - "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==" - }, - "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", - "requires": { - "is-number": "4.0.0", - "kind-of": "6.0.2", - "math-random": "1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "0.6.0", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "react": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz", - "integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "prop-types": "15.6.0" - } - }, - "react-clone-referenced-element": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-clone-referenced-element/-/react-clone-referenced-element-1.0.1.tgz", - "integrity": "sha1-K7qMaUBMXkqUQ5hgC8xMlB+GBoI=" - }, - "react-deep-force-update": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz", - "integrity": "sha1-vNMUeAJ7ZLMznxCJIatSC0MT3Cw=" - }, - "react-devtools-core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-3.1.0.tgz", - "integrity": "sha512-fO6SmpW16E9u6Lb6zQOHrjhJXGBNz+cJ0/a9cSF55nXfL0sQLlvYJR8DpU7f4rMUrVnVineg4XQyYYBZicmhJg==", - "requires": { - "shell-quote": "1.6.1", - "ws": "2.3.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" - }, - "ws": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz", - "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA=", - "requires": { - "safe-buffer": "5.0.1", - "ultron": "1.1.1" - } - } - } - }, - "react-dom": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz", - "integrity": "sha512-zpGAdwHVn9K0091d+hr+R0qrjoJ84cIBFL2uU60KvWBPfZ7LPSrfqviTxGHWN0sjPZb2hxWzMexwrvJdKePvjg==", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "prop-types": "15.6.0" - } - }, - "react-native": { - "version": "github:status-im/react-native#786b022d814b6a4b89db863a1dbb7e7ac8ac8c50", - "requires": { - "absolute-path": "0.0.0", - "art": "0.10.3", - "babel-core": "6.24.1", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-async-to-generator": "6.16.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-exponentiation-operator": "6.24.1", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "base64-js": "1.3.0", - "chalk": "1.1.3", - "commander": "2.16.0", - "compression": "1.7.3", - "connect": "3.6.6", - "create-react-class": "15.6.3", - "debug": "2.6.9", - "denodeify": "1.2.1", - "envinfo": "3.11.1", - "errorhandler": "1.5.0", - "eslint-plugin-react-native": "3.2.1", - "event-target-shim": "1.1.1", - "fbjs": "0.8.17", - "fbjs-scripts": "0.8.3", - "fs-extra": "1.0.0", - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "inquirer": "3.3.0", - "lodash": "4.17.10", - "metro": "0.30.2", - "metro-core": "0.30.2", - "mime": "1.6.0", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "morgan": "1.9.0", - "node-fetch": "1.7.3", - "node-notifier": "5.2.1", - "npmlog": "2.0.4", - "opn": "3.0.3", - "optimist": "0.6.1", - "plist": "1.2.0", - "pretty-format": "4.3.1", - "promise": "7.3.1", - "prop-types": "15.6.0", - "react-clone-referenced-element": "1.0.1", - "react-devtools-core": "3.1.0", - "react-timer-mixin": "0.13.4", - "regenerator-runtime": "0.11.1", - "rimraf": "2.6.2", - "semver": "5.5.0", - "serve-static": "1.13.2", - "shell-quote": "1.6.1", - "stacktrace-parser": "0.1.4", - "whatwg-fetch": "1.1.1", - "ws": "1.1.5", - "xcode": "0.9.3", - "xmldoc": "0.4.0", - "yargs": "9.0.1" - }, - "dependencies": { - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - } - } - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "create-react-class": { - "version": "15.6.3", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", - "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", - "requires": { - "fbjs": "0.8.17", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "whatwg-fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz", - "integrity": "sha1-rDydOfMgxtzlM5lp0FTvQ90zMxk=" - } - } - }, - "react-native-background-timer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz", - "integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw==" - }, - "react-native-camera": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-0.10.0.tgz", - "integrity": "sha512-qum5zdJ3GQZ/mB+SYdT8EhQVnIfmyLzwyIK9Ss4vsTGLKxKmt9tf7oGigT2t52aOy6tNglKcgYuU0cZREKGDDA==", - "requires": { - "prop-types": "15.6.0" - } - }, - "react-native-config": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/react-native-config/-/react-native-config-0.9.0.tgz", - "integrity": "sha512-ImdtUf1mgCm6nn4up3ig9j6tax2Tz0TK8j0SsZ/OUks0pokPRe7DH6pQRrerp47q63Ad+biPJs69ap2xczTFAg==" - }, - "react-native-crypto": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/react-native-crypto/-/react-native-crypto-2.1.1.tgz", - "integrity": "sha512-JHwX0zobeYqCAwdSffRoivXeqlZR54WpCj8ICGcoNh7bvOh8khr63zvNk+fcqdRH3eI9Jhglygswijza0f3D4Q==", - "requires": { - "browserify-cipher": "1.0.1", - "browserify-sign": "4.0.4", - "create-ecdh": "4.0.3", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "diffie-hellman": "5.0.3", - "inherits": "2.0.3", - "pbkdf2": "3.0.8", - "public-encrypt": "4.0.2" - } - }, - "react-native-dialogs": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/react-native-dialogs/-/react-native-dialogs-0.0.20.tgz", - "integrity": "sha512-HeoU9d7wWUUAtxjcIJLaeIs/eYS2ZHTNh3kboeCKngLvfvgptd/7vZXwXdQ+xeOEek+cm95kDfMJQd2xnb+4wA==" - }, - "react-native-fcm": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/react-native-fcm/-/react-native-fcm-10.0.3.tgz", - "integrity": "sha1-HcU47YifkLelvB0FMMxRbmmwnow=" - }, - "react-native-fetch-polyfill": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/react-native-fetch-polyfill/-/react-native-fetch-polyfill-1.1.2.tgz", - "integrity": "sha1-JWtaCr14zEmS96fPglQ9ovISSnM=" - }, - "react-native-fs": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.8.1.tgz", - "integrity": "sha512-1v7NrbYlx47VkWaemcbXdzJHoG5GVKdkNIwwH8vJVFQTc5jJYaUxXdn+g+duc6+DD08Yt5Of6eFh3ILc88mNMw==", - "requires": { - "base-64": "0.1.0", - "utf8": "2.1.2" - } - }, - "react-native-http-bridge": { - "version": "git+https://github.com/status-im/react-native-http-bridge.git#86c56aa05dfb35ac2956c85dfadf77b9ca1da59c" - }, - "react-native-i18n": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/react-native-i18n/-/react-native-i18n-2.0.9.tgz", - "integrity": "sha512-aHaXLvvEHwsSLLuK+IeE2plWyAXFwbCIEDsTrQZAN73iUFetZZfD97GkCWi+/yWfLQPlFHaMGJbGkd6fIUDaxA==", - "requires": { - "i18n-js": "3.0.11" - } - }, - "react-native-image-crop-picker": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.18.1.tgz", - "integrity": "sha512-63Yr+Mzl/CzDze1eqMg/GGFOODgGrixSMO+28TVGCX3982f76w/op0JKoL7zuSKz/P/mVsbi9RnHA0z6Ih/RHQ==" - }, - "react-native-image-resizer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/react-native-image-resizer/-/react-native-image-resizer-1.0.0.tgz", - "integrity": "sha1-1H4UlDw3k44of71jnk23zrf9iRc=" - }, - "react-native-invertible-scroll-view": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/react-native-invertible-scroll-view/-/react-native-invertible-scroll-view-1.1.0.tgz", - "integrity": "sha1-v9UKP11myhJjm3x6mETO3dHRaJA=", - "requires": { - "create-react-class": "15.6.2", - "prop-types": "15.6.0", - "react-clone-referenced-element": "1.0.1", - "react-native-scrollable-mixin": "1.0.1" - } - }, - "react-native-keychain": { - "version": "3.0.0-rc.3", - "resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-3.0.0-rc.3.tgz", - "integrity": "sha512-ijWfHmxTPKnrHtPJiDbKW3D6lRH8O9wbCNEE3xlxEg1WZT+VhP6iiF+HUansNYuxL7Hh7k41GSFfvr3xumfmXA==" - }, - "react-native-level-fs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-native-level-fs/-/react-native-level-fs-3.0.0.tgz", - "integrity": "sha1-uefSeEhbaWY0pifMYEV3yyr5698=", - "requires": { - "level-filesystem": "1.2.0", - "levelup": "0.18.6" - } - }, - "react-native-os": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/react-native-os/-/react-native-os-1.1.0.tgz", - "integrity": "sha1-v74cRNilsUpvOjpAXYrab1R6UW4=" - }, - "react-native-qrcode": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/react-native-qrcode/-/react-native-qrcode-0.2.6.tgz", - "integrity": "sha1-3ZLxUcEYhgCSr5+lvAmV8vnd2xY=", - "requires": { - "create-react-class": "15.6.2", - "prop-types": "15.6.0", - "qr.js": "0.0.0" - } - }, - "react-native-scrollable-mixin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-native-scrollable-mixin/-/react-native-scrollable-mixin-1.0.1.tgz", - "integrity": "sha1-NKMhZ7ZCSFlBVP0NaosD8idAVI4=" - }, - "react-native-securerandom": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/react-native-securerandom/-/react-native-securerandom-0.1.1.tgz", - "integrity": "sha1-8TBiOkEsM4sK+t7bwgTFy7i/IHA=", - "requires": { - "base64-js": "0.0.8" - } - }, - "react-native-splash-screen": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/react-native-splash-screen/-/react-native-splash-screen-3.0.6.tgz", - "integrity": "sha512-yaTnGAHRyhduLSfD85gP3Vsf0BRePHW3aNNtDXbkbUhwIIeafu2cJH86U/qKFuKLMYLnFOXteOkP80gaYVGAYg==" - }, - "react-native-svg": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-6.3.1.tgz", - "integrity": "sha512-0kmfUwKUBWnPuJpy+bdGIYKkXHg/M/X57ji8b3d3ZFB2rRTWMRkwI1D+AJ6FQRX109+FJn6L6hsIokDj1lckzA==", - "requires": { - "color": "2.0.1", - "lodash": "4.17.10", - "pegjs": "0.10.0" - } - }, - "react-native-tcp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/react-native-tcp/-/react-native-tcp-3.3.0.tgz", - "integrity": "sha512-WAiL3IGhVxvmM0iw7/5ZRzdkiJBfBbLoFKNaHLJIsTZVqD/N5ZHUlJMjIQ+RrmB/f5nJ82bUEZ2vPCxe+rlNqQ==", - "requires": { - "base64-js": "0.0.8", - "buffer": "5.1.0", - "events": "1.1.1", - "ip-regex": "1.0.3", - "process": "0.11.10", - "util": "0.10.3" - }, - "dependencies": { - "buffer": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", - "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", - "requires": { - "base64-js": "1.3.0", - "ieee754": "1.1.12" - }, - "dependencies": { - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - } - } - } - } - }, - "react-native-testfairy": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/react-native-testfairy/-/react-native-testfairy-2.10.0.tgz", - "integrity": "sha512-l9Ywc69WTnraPuq5wZS7/GWcSuBxVKxaFzCIhsrIzWhByIh/1o/UqwRb8Eb06h8/e3IP7fd1LD5hXF2NMDRg3g==" - }, - "react-native-udp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-native-udp/-/react-native-udp-2.2.1.tgz", - "integrity": "sha1-Y1NsYVCzborhi870AMT66iT5QDU=", - "requires": { - "base64-js": "0.0.8", - "events": "1.1.1", - "inherits": "2.0.3", - "ip-regex": "1.0.3", - "util": "0.10.3" - } - }, - "react-native-webview-bridge": { - "version": "git+https://github.com/status-im/react-native-webview-bridge.git#80a798c4004bf38bf4e965e38a6f6bc59557278d", - "requires": { - "invariant": "2.2.0", - "keymirror": "0.1.1" - }, - "dependencies": { - "invariant": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.0.tgz", - "integrity": "sha1-yNfoRzZqScwYtiLwWKaJ1IHolfI=", - "requires": { - "loose-envify": "1.3.1" - } - } - } - }, - "react-proxy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz", - "integrity": "sha1-nb/Z2SdSjDqp9ETkVYw3gwq4wmo=", - "requires": { - "lodash": "4.17.10", - "react-deep-force-update": "1.1.1" - } - }, - "react-timer-mixin": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/react-timer-mixin/-/react-timer-mixin-0.13.4.tgz", - "integrity": "sha512-4+ow23tp/Tv7hBM5Az5/Be/eKKF7DIvJ09voz5LyHGQaqqz9WV8YMs31eFvcYQs7d451LSg7kDJV70XYN/Ug/Q==" - }, - "react-transform-hmr": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz", - "integrity": "sha1-4aQL0Krvxy6N/Xp82gmvhQZjl7s=", - "requires": { - "global": "4.3.2", - "react-proxy": "1.1.8" - } - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.4.0", - "path-type": "2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - }, - "dependencies": { - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.2" - } - } - } - }, - "realm": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/realm/-/realm-2.3.3.tgz", - "integrity": "sha1-3Z3AE3+VsXAWVLqRk5KNUurEI50=", - "requires": { - "command-line-args": "4.0.7", - "decompress": "4.2.0", - "fs-extra": "4.0.3", - "ini": "1.3.5", - "nan": "2.8.0", - "node-fetch": "1.7.3", - "node-pre-gyp": "0.6.39", - "progress": "2.0.0", - "prop-types": "15.6.0", - "request": "2.87.0", - "stream-counter": "1.0.0", - "sync-request": "3.0.1", - "url-parse": "1.4.1" - }, - "dependencies": { - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.2" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" - } - } - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "private": "0.1.8" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "requires": { - "is-equal-shallow": "0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", - "requires": { - "regenerate": "1.4.0", - "regjsgen": "0.2.0", - "regjsparser": "0.1.5" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "requires": { - "jsesc": "0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "1.0.2" - } - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", - "requires": { - "path-parse": "1.0.5" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "2.0.1", - "signal-exit": "3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.3" - } - }, - "rn-snoopy": { - "version": "git+https://github.com/status-im/rn-snoopy.git#f23dc13469c6c2a694649f658cdc1d1eaf8def64", - "requires": { - "eslint-plugin-jsx-a11y": "5.1.1", - "lodash": "3.10.1", - "rxjs": "5.5.2" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==" - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "2.1.0" - } - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=" - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "requires": { - "rx-lite": "4.0.8" - } - }, - "rxjs": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.2.tgz", - "integrity": "sha512-oRYoIKWBU3Ic37fLA5VJu31VqQO4bWubRntcHSJ+cwaDQBwdnZ9x4zmhJfm/nFQ2E82/I4loSioHnACamrKGgA==", - "requires": { - "symbol-observable": "1.2.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "0.1.15" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sane": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", - "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", - "requires": { - "anymatch": "2.0.0", - "capture-exit": "1.2.0", - "exec-sh": "0.2.2", - "fb-watchman": "2.0.0", - "micromatch": "3.1.10", - "minimist": "1.2.0", - "walker": "1.0.7", - "watch": "0.18.0" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "sax": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.6.tgz", - "integrity": "sha1-XWFr6KXmB9VOEUr65Vt+ry/MMkA=" - }, - "seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", - "requires": { - "commander": "2.8.1" - }, - "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "requires": { - "graceful-readlink": "1.0.1" - } - } - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.2", - "destroy": "1.0.4", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.3", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.4.0" - }, - "dependencies": { - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "serialize-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", - "integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=" - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.2" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.2" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "requires": { - "array-filter": "0.0.1", - "array-map": "0.0.0", - "array-reduce": "0.0.0", - "jsonify": "0.0.0" - } - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-plist": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-0.2.1.tgz", - "integrity": "sha1-cXZts1IyaSjPOoByQrp2IyJjZyM=", - "requires": { - "bplist-creator": "0.0.7", - "bplist-parser": "0.1.1", - "plist": "2.0.1" - }, - "dependencies": { - "base64-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.1.2.tgz", - "integrity": "sha1-1kAMrBxMZgl22Q0HoENR2JOV9eg=" - }, - "plist": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/plist/-/plist-2.0.1.tgz", - "integrity": "sha1-CjLKlIGxw2TpLhjcVch23p0B2os=", - "requires": { - "base64-js": "1.1.2", - "xmlbuilder": "8.2.2", - "xmldom": "0.1.27" - } - }, - "xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=" - } - } - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "0.3.2" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.2", - "use": "3.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "requires": { - "hoek": "2.16.3" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "2.1.1", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" - } - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "requires": { - "source-map": "0.5.7" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "2.1.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "stacktrace-parser": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.4.tgz", - "integrity": "sha1-ATl5IuX2Ls8whFUiyVxP4dJefU4=" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - } - } - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "stream-buffers": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", - "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=" - }, - "stream-counter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-1.0.0.tgz", - "integrity": "sha1-kc8lac5NxQYf6816yyY5SloRR1E=" - }, - "string-range": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/string-range/-/string-range-1.2.2.tgz", - "integrity": "sha1-qJPtNH5yKZvIO++78qaSqNI51d0=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "requires": { - "is-natural-number": "4.0.1" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, - "sync-request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-3.0.1.tgz", - "integrity": "sha1-yqEjWq+Im6UBB2oYNMQ2gwqC+3M=", - "requires": { - "concat-stream": "1.6.2", - "http-response-object": "1.1.0", - "then-request": "2.2.0" - } - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.6", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tar-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", - "requires": { - "bl": "1.2.2", - "buffer-alloc": "1.2.0", - "end-of-stream": "1.4.1", - "fs-constants": "1.0.0", - "readable-stream": "2.3.6", - "to-buffer": "1.1.1", - "xtend": "4.0.1" - }, - "dependencies": { - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "2.3.6", - "safe-buffer": "5.1.2" - } - } - } - }, - "temp": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", - "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", - "requires": { - "os-tmpdir": "1.0.2", - "rimraf": "2.2.8" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" - } - } - }, - "test-value": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=", - "requires": { - "array-back": "1.0.4", - "typical": "2.6.1" - }, - "dependencies": { - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "requires": { - "typical": "2.6.1" - } - } - } - }, - "then-request": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/then-request/-/then-request-2.2.0.tgz", - "integrity": "sha1-ZnizL6DKIY/laZgbvYhxtZQGDYE=", - "requires": { - "caseless": "0.11.0", - "concat-stream": "1.6.2", - "http-basic": "2.5.1", - "http-response-object": "1.1.0", - "promise": "7.3.1", - "qs": "6.4.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - } - } - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "requires": { - "readable-stream": "2.3.6", - "xtend": "4.0.1" - } - }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" - }, - "tiny-queue": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.0.tgz", - "integrity": "sha1-xJ/LXIdVW+G0pd9+uHEB1beLydw=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "requires": { - "punycode": "1.4.1" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" - }, - "ua-parser-js": { - "version": "0.7.18", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", - "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==" - }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "requires": { - "commander": "2.13.0", - "source-map": "0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "ultron": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" - }, - "unbzip2-stream": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", - "integrity": "sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==", - "requires": { - "buffer": "3.6.0", - "through": "2.3.8" - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" - }, - "dependencies": { - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" - } - } - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "url-parse": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.1.tgz", - "integrity": "sha512-x95Td74QcvICAA0+qERaVkRpTGKyBHHYdwL2LXZm5t/gBtCB9KQSO/0zQgSTYEV1p0WcvSg79TLNPSvd5IDJMQ==", - "requires": { - "querystringify": "2.0.0", - "requires-port": "1.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "utf8": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", - "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=" - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" - }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", - "requires": { - "spdx-correct": "3.0.0", - "spdx-expression-parse": "3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "requires": { - "makeerror": "1.0.11" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "requires": { - "exec-sh": "0.2.2", - "minimist": "1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "web3": { - "version": "git+https://github.com/status-im/web3.js.git#ff09ce57e9574a1c42138832a7d6bf6c2e699048", - "requires": { - "bignumber.js": "github:status-im/bignumber.js#cc066a0a3d6bfe0c436c9957f4ea8344bf963c89", - "crypto-js": "3.1.8", - "utf8": "2.1.2", - "xhr2": "0.1.4", - "xmlhttprequest": "1.8.0" - }, - "dependencies": { - "bignumber.js": { - "version": "github:status-im/bignumber.js#cc066a0a3d6bfe0c436c9957f4ea8344bf963c89" - } - } - }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "2.1.1" - } - }, - "win-release": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz", - "integrity": "sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk=", - "requires": { - "semver": "5.5.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", - "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "slide": "1.1.6" - } - }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "requires": { - "options": "0.0.6", - "ultron": "1.0.2" - } - }, - "xcode": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/xcode/-/xcode-0.9.3.tgz", - "integrity": "sha1-kQqJwWrubMC0LKgFptC0z4chHPM=", - "requires": { - "pegjs": "0.10.0", - "simple-plist": "0.2.1", - "uuid": "3.0.1" - } - }, - "xhr2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", - "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" - }, - "xmlbuilder": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz", - "integrity": "sha1-mLj2UcowqmJANvEn0RzGbce5B6M=", - "requires": { - "lodash": "3.10.1" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, - "xmldoc": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-0.4.0.tgz", - "integrity": "sha1-0lciS+g5PqrL+DfvIn/Y7CWzaIg=", - "requires": { - "sax": "1.1.6" - } - }, - "xmldom": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", - "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" - }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" - }, - "xpipe": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/xpipe/-/xpipe-1.0.5.tgz", - "integrity": "sha1-jdi/Rfw/f1Xw4FS4ePQ6YmFNr98=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", - "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", - "requires": { - "camelcase": "4.1.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.3", - "os-locale": "2.1.0", - "read-pkg-up": "2.0.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "7.0.0" - } - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "requires": { - "camelcase": "4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - } - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "requires": { - "buffer-crc32": "0.2.13", - "fd-slicer": "1.1.0" - } - } - } -} diff --git a/mobile_files/package.json b/mobile_files/package.json deleted file mode 100644 index c197a7d4352..00000000000 --- a/mobile_files/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "name": "StatusIm", - "version": "0.0.1", - "private": true, - "scripts": { - "start": "node node_modules/react-native/local-cli/cli.js start" - }, - "dependencies": { - "@tradle/react-native-http": "^2.0.1", - "assert": "1.4.1", - "asyncstorage-down": "4.0.1", - "babel-core": "6.24.1", - "babel-generator": "6.24.1", - "babel-helper-builder-react-jsx": "6.18.0", - "babel-plugin-transform-es2015-block-scoping": "6.15.0", - "babel-preset-react-native": "4.0.0", - "babel-register": "6.18.0", - "bignumber.js": "https://github.com/status-im/bignumber.js.git", - "buffer": "3.6.0", - "chance": "1.0.12", - "create-react-class": "15.6.2", - "dns.js": "1.0.1", - "emojilib": "2.2.9", - "events": "1.1.1", - "homoglyph-finder": "1.1.1", - "identicon.js": "https://github.com/status-im/identicon.js.git", - "instabug-reactnative": "2.12.0", - "level-filesystem": "1.2.0", - "process": "0.11.10", - "prop-types": "15.6.0", - "punycode": "1.4.1", - "querystring-es3": "0.2.1", - "react": "16.2.0", - "react-dom": "16.2.0", - "react-native": "github:status-im/react-native#status-0.55.4", - "react-native-background-timer": "2.0.0", - "react-native-camera": "0.10.0", - "react-native-config": "0.9.0", - "react-native-crypto": "2.1.1", - "react-native-dialogs": "0.0.20", - "react-native-fcm": "10.0.3", - "react-native-fetch-polyfill": "1.1.2", - "react-native-fs": "2.8.1", - "react-native-http-bridge": "https://github.com/status-im/react-native-http-bridge.git", - "react-native-i18n": "2.0.9", - "react-native-image-crop-picker": "0.18.1", - "react-native-image-resizer": "1.0.0", - "react-native-invertible-scroll-view": "1.1.0", - "react-native-keychain": "3.0.0-rc.3", - "react-native-level-fs": "3.0.0", - "react-native-os": "1.1.0", - "react-native-qrcode": "0.2.6", - "react-native-securerandom": "0.1.1", - "react-native-splash-screen": "3.0.6", - "react-native-svg": "6.3.1", - "react-native-tcp": "3.3.0", - "react-native-testfairy": "2.10.0", - "react-native-udp": "2.2.1", - "react-native-webview-bridge": "https://github.com/status-im/react-native-webview-bridge.git", - "realm": "2.3.3", - "rn-snoopy": "https://github.com/status-im/rn-snoopy.git", - "string_decoder": "0.10.31", - "url": "0.10.3", - "web3": "https://github.com/status-im/web3.js.git#feature/shhext" - } -} diff --git a/modules/react-native-status/android/build.gradle b/modules/react-native-status/android/build.gradle index 444c35f7478..112739ce879 100644 --- a/modules/react-native-status/android/build.gradle +++ b/modules/react-native-status/android/build.gradle @@ -1,30 +1,44 @@ apply plugin: 'com.android.library' +apply plugin: "org.jetbrains.kotlin.android" + +def getStatusGoSHA1 = { -> + def statusgoOverridePath = System.getenv("STATUS_GO_SRC_OVERRIDE") + if (statusgoOverridePath != null && statusgoOverridePath != "") { + logger.info("build.gradle: Using local version of status-go from ${statusgoOverridePath}") + return "unknown" // This value is defined in https://github.com/status-im/status-mobile/blob/develop/nix/status-go/default.nix, in `srcData.shortRev` + } + + def jsonSlurper = new groovy.json.JsonSlurper() + def content = new File('../status-go-version.json').text + def object = jsonSlurper.parseText(content) + + assert object instanceof Map + + return object.get('commit-sha1').substring(0, 7) +} android { - compileSdkVersion 24 + compileSdkVersion rootProject.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } defaultConfig { - minSdkVersion 18 - targetSdkVersion 23 + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" } } dependencies { - implementation 'com.facebook.react:react-native:+' - implementation 'com.instabug.library:instabug:3+' - implementation 'status-im:function:0.0.1' - - String statusGoVersion = 'develop-g03bf6e37' - final String statusGoGroup = 'status-im', statusGoName = 'status-go' - - // Check if the local status-go jar exists, and compile against that if it does - final String localStatusLibOutputDir = "${rootDir}/../modules/react-native-status/android/libs", localVersion = 'local' - if ( new File("${localStatusLibOutputDir}/${statusGoGroup}/${statusGoName}/${localVersion}/${statusGoName}-${localVersion}.aar").exists() ) { - // Use the local version - statusGoVersion = localVersion - } + implementation 'com.facebook.react:react-native:+' // from node_modules + implementation 'com.github.status-im:function:0.0.1' + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation(group: 'status-im', name: 'status-go', version: getStatusGoSHA1(), ext: 'aar') - implementation(group: statusGoGroup, name: statusGoName, version: statusGoVersion, ext: 'aar') + testImplementation 'junit:junit:4.13.2' + testImplementation "org.mockito:mockito-core:3.+" } diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/AccountManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/AccountManager.kt new file mode 100644 index 00000000000..661ca3ce254 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/AccountManager.kt @@ -0,0 +1,283 @@ +package im.status.ethereum.module + +import android.app.Activity +import android.content.Context +import android.util.Log +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import org.json.JSONException +import org.json.JSONObject +import statusgo.Statusgo +import java.io.* + +class AccountManager(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val utils = Utils(reactContext) + private val logManager = LogManager(reactContext) + + override fun getName() = "AccountManager" + + private fun getTestnetDataDir(absRootDirPath: String) = utils.pathCombine(absRootDirPath, "ethereum/testnet") + + @ReactMethod + fun createAccountAndLogin(createAccountRequest: String) { + Log.d(TAG, "createAccountAndLogin") + + StatusBackendClient.executeStatusGoRequest( + endpoint = "CreateAccountAndLogin", + requestBody = createAccountRequest, + statusgoFunction = { Statusgo.createAccountAndLogin(createAccountRequest) } + ) + } + + @ReactMethod + fun restoreAccountAndLogin(restoreAccountRequest: String) { + Log.d(TAG, "restoreAccountAndLogin") + + StatusBackendClient.executeStatusGoRequest( + endpoint = "RestoreAccountAndLogin", + requestBody = restoreAccountRequest, + statusgoFunction = { Statusgo.restoreAccountAndLogin(restoreAccountRequest) } + ) + } + + private fun copyDirectory(sourceLocation: File, targetLocation: File) { + if (sourceLocation.isDirectory) { + if (!targetLocation.exists() && !targetLocation.mkdirs()) { + throw IOException("Cannot create dir ${targetLocation.absolutePath}") + } + + val children = sourceLocation.list() + children?.forEach { child -> + copyDirectory(File(sourceLocation, child), File(targetLocation, child)) + } + } else { + val directory = targetLocation.parentFile + if (directory != null && !directory.exists() && !directory.mkdirs()) { + throw IOException("Cannot create dir ${directory.absolutePath}") + } + + sourceLocation.inputStream().use { input -> + targetLocation.outputStream().use { output -> + input.copyTo(output) + } + } + } + } + + @ReactMethod + fun loginWithKeycard(accountData: String, password: String, chatKey: String, nodeConfigJSON: String) { + Log.d(TAG, "loginWithKeycard") + utils.migrateKeyStoreDir(accountData, password) + val result = Statusgo.loginWithKeycard(accountData, password, chatKey, nodeConfigJSON) + utils.handleStatusGoResponse(result, "loginWithKeycard") + } + + @ReactMethod + fun loginWithConfig(accountData: String, password: String, configJSON: String) { + Log.d(TAG, "loginWithConfig") + utils.migrateKeyStoreDir(accountData, password) + val result = Statusgo.loginWithConfig(accountData, password, configJSON) + utils.handleStatusGoResponse(result, "loginWithConfig") + } + + @ReactMethod + fun loginAccount(request: String) { + Log.d(TAG, "loginAccount") + StatusBackendClient.executeStatusGoRequest( + endpoint = "LoginAccount", + requestBody = request, + statusgoFunction = { Statusgo.loginAccount(request) } + ) + } + + @ReactMethod + fun verifyDatabasePassword(keyUID: String, password: String, callback: Callback) { + val jsonParams = JSONObject() + jsonParams.put("keyUID", keyUID) + jsonParams.put("password", password) + + val jsonString = jsonParams.toString() + + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "VerifyDatabasePasswordV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.verifyDatabasePasswordV2(jsonString) }, + callback + ) + } + + @ReactMethod + private fun openAccounts(callback: Callback) { + Log.d(TAG, "openAccounts") + val rootDir = utils.getNoBackupDirectory() + Log.d(TAG, "[Opening accounts $rootDir") + utils.executeRunnableStatusGoMethod({ Statusgo.openAccounts(rootDir) }, callback) + } + + @ReactMethod + private fun initializeApplication(request: String, callback: Callback) { + Log.d(TAG, "initializeApplication") + StatusBackendClient.executeStatusGoRequestWithCallback( + "InitializeApplication", + request, + { Statusgo.initializeApplication(request) }, + callback + ) + } + + @ReactMethod + private fun acceptTerms(callback: Callback) { + Log.d(TAG, "acceptTerms") + StatusBackendClient.executeStatusGoRequestWithCallback( + "AcceptTerms", + "", + { Statusgo.acceptTerms() }, + callback + ) + } + + @ReactMethod + fun logout() { + Log.d(TAG, "logout") + StatusBackendClient.executeStatusGoRequest( + endpoint = "Logout", + requestBody = "", + statusgoFunction = { Statusgo.logout() } + ) + } + + @ReactMethod + fun multiAccountStoreAccount(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "MultiAccountStoreAccount", + requestBody = json, + statusgoFunction = { Statusgo.multiAccountStoreAccount(json) }, + callback = callback + ) + } + + @ReactMethod + fun multiAccountLoadAccount(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "MultiAccountLoadAccount", + requestBody = json, + statusgoFunction = { Statusgo.multiAccountLoadAccount(json) }, + callback = callback + ) + } + + @ReactMethod + fun multiAccountDeriveAddresses(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "MultiAccountDeriveAddresses", + requestBody = json, + statusgoFunction = { Statusgo.multiAccountDeriveAddresses(json) }, + callback = callback + ) + } + + @ReactMethod + fun multiAccountGenerateAndDeriveAddresses(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "MultiAccountGenerateAndDeriveAddresses", + requestBody = json, + statusgoFunction = { Statusgo.multiAccountGenerateAndDeriveAddresses(json) }, + callback = callback + ) + } + + @ReactMethod + fun multiAccountStoreDerived(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "MultiAccountStoreDerivedAccounts", + requestBody = json, + statusgoFunction = { Statusgo.multiAccountStoreDerivedAccounts(json) }, + callback = callback + ) + } + + @ReactMethod + fun multiAccountImportMnemonic(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "MultiAccountImportMnemonic", + requestBody = json, + statusgoFunction = { Statusgo.multiAccountImportMnemonic(json) }, + callback = callback + ) + } + + @ReactMethod + fun multiAccountImportPrivateKey(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "MultiAccountImportPrivateKey", + requestBody = json, + statusgoFunction = { Statusgo.multiAccountImportPrivateKey(json) }, + callback = callback + ) + } + + @ReactMethod + fun deleteMultiaccount(keyUID: String, callback: Callback) { + val keyStoreDir = utils.getKeyStorePath(keyUID) + val params = JSONObject().apply { + put("keyUID", keyUID) + put("keyStoreDir", keyStoreDir) + } + val jsonString = params.toString() + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "DeleteMultiaccountV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.deleteMultiaccountV2(jsonString) }, + callback + ) + } + + @ReactMethod + fun getRandomMnemonic(callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + "GetRandomMnemonic", + "", + { Statusgo.getRandomMnemonic() }, + callback + ) + } + + @ReactMethod + fun createAccountFromMnemonicAndDeriveAccountsForPaths(mnemonic: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + "CreateAccountFromMnemonicAndDeriveAccountsForPaths", + mnemonic, + { Statusgo.createAccountFromMnemonicAndDeriveAccountsForPaths(mnemonic) }, + callback + ) + } + + @ReactMethod + fun createAccountFromPrivateKey(json: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "CreateAccountFromPrivateKey", + requestBody = json, + statusgoFunction = { Statusgo.createAccountFromPrivateKey(json) }, + callback = callback + ) + } + + companion object { + private const val TAG = "AccountManager" + + private fun prettyPrintConfig(config: String) { + Log.d(TAG, "startNode() with config (see below)") + var configOutput = config + val maxOutputLen = 4000 + Log.d(TAG, "********************** NODE CONFIG ****************************") + while (configOutput.isNotEmpty()) { + Log.d(TAG, "Node config:${configOutput.take(maxOutputLen)}") + configOutput = configOutput.drop(maxOutputLen) + } + Log.d(TAG, "******************* ENDOF NODE CONFIG *************************") + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/ConnectorHandler.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/ConnectorHandler.java deleted file mode 100644 index 3dbba9b6625..00000000000 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/ConnectorHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package im.status.ethereum.module; - - -import android.os.Message; - -public interface ConnectorHandler { - boolean handleMessage(Message message); - void onConnectorConnected(); - void onConnectorDisconnected(); -} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/DatabaseManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/DatabaseManager.kt new file mode 100644 index 00000000000..5188a260b0f --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/DatabaseManager.kt @@ -0,0 +1,95 @@ +package im.status.ethereum.module + +import android.content.Context +import android.os.Environment +import android.util.Log +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import statusgo.Statusgo +import java.io.File +import org.json.JSONObject +import org.json.JSONException + +class DatabaseManager(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val utils = Utils(reactContext) + + override fun getName() = "DatabaseManager" + + private fun getExportDBFile(): File { + StatusBackendClient.getInstance()?.let { + if (it.serverEnabled) { + return File(it.rootDataDir, exportDBFileName) + } + } + val pubDirectory = reactContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + return File(pubDirectory, exportDBFileName) + } + + @ReactMethod + fun exportUnencryptedDatabase(accountData: String, password: String, callback: Callback) { + Log.d(TAG, "exportUnencryptedDatabase") + + val newFile = getExportDBFile() + + utils.migrateKeyStoreDir(accountData, password) + + try { + val accountJson = JSONObject(accountData) + + val params = JSONObject().apply { + put("account", accountJson) + put("password", password) + put("databasePath", newFile.absolutePath) + } + + val jsonParams = params.toString() + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "ExportUnencryptedDatabaseV2", + requestBody = jsonParams, + statusgoFunction = { Statusgo.exportUnencryptedDatabaseV2(jsonParams) }, + callback = null + ) + callback.invoke(newFile.absolutePath) + + } catch (e: JSONException) { + Log.e(TAG, "Error parsing account data: ${e.message}") + } + } + + @ReactMethod + fun importUnencryptedDatabase(accountData: String, password: String) { + Log.d(TAG, "importUnencryptedDatabase") + + val newFile = getExportDBFile() + + utils.migrateKeyStoreDir(accountData, password) + + try { + val accountJson = JSONObject(accountData) + + val params = JSONObject().apply { + put("account", accountJson) + put("password", password) + put("databasePath", newFile.absolutePath) + } + + val jsonParams = params.toString() + + StatusBackendClient.executeStatusGoRequest( + endpoint = "ImportUnencryptedDatabaseV2", + requestBody = jsonParams, + statusgoFunction = { Statusgo.importUnencryptedDatabaseV2(jsonParams) } + ) + } catch (e: JSONException) { + Log.e(TAG, "Error parsing account data: ${e.message}") + } + } + + companion object { + private const val TAG = "DatabaseManager" + private const val exportDBFileName = "export.db" + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/EncryptionUtils.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/EncryptionUtils.kt new file mode 100644 index 00000000000..2d373894cb6 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/EncryptionUtils.kt @@ -0,0 +1,258 @@ +package im.status.ethereum.module + +import android.app.Activity +import android.os.Build +import android.preference.PreferenceManager +import android.util.Log +import android.view.WindowManager +import com.facebook.react.bridge.* +import org.json.JSONException +import org.json.JSONObject +import statusgo.Statusgo +import java.lang.Exception +import java.lang.RuntimeException + +class EncryptionUtils(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + private val utils = Utils(reactContext) + + companion object { + private const val TAG = "EncryptionUtils" + } + + override fun getName(): String = "EncryptionUtils" + + @ReactMethod + @Throws(Exception::class) + private fun initKeystore(keyUID: String, callback: Callback) { + Log.d(TAG, "initKeystore") + val commonKeydir = utils.pathCombine(utils.getNoBackupDirectory(), "/keystore") + val keydir = utils.pathCombine(commonKeydir, keyUID) + + StatusBackendClient.executeStatusGoRequestWithCallback( + "InitKeystore", + keydir, + { Statusgo.initKeystore(keydir) }, + callback + ) + } + + @ReactMethod + @Throws(Exception::class) + fun reEncryptDbAndKeystore(keyUID: String, password: String, newPassword: String, callback: Callback) { + val params = JSONObject().apply { + put("keyUID", keyUID) + put("oldPassword", password) + put("newPassword", newPassword) + } + StatusBackendClient.executeStatusGoRequestWithCallback( + "ChangeDatabasePasswordV2", + params.toString(), + { Statusgo.changeDatabasePasswordV2(params.toString()) }, + callback + ) + } + + @ReactMethod + @Throws(Exception::class) + fun convertToKeycardAccount( + keyUID: String, + accountData: String, + options: String, + keycardUID: String, + password: String, + newPassword: String, + callback: Callback + ) { + val keyStoreDir = utils.getKeyStorePath(keyUID) + val params = JSONObject().apply { + put("keyUID", keyUID) + put("account", JSONObject(accountData)) + put("settings", JSONObject(options)) + put("keycardUID", keycardUID) + put("oldPassword", password) + put("newPassword", newPassword) + } + val jsonParams = params.toString() + + StatusBackendClient.executeStatusGoRequest( + "InitKeystore", + keyStoreDir, + { Statusgo.initKeystore(keyStoreDir) } + ) + StatusBackendClient.executeStatusGoRequestWithCallback( + "ConvertToKeycardAccountV2", + jsonParams, + { Statusgo.convertToKeycardAccountV2(jsonParams) }, + callback + ) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun decodeParameters(decodeParamJSON: String): String = + StatusBackendClient.executeStatusGoRequestWithResult( + "DecodeParameters", + decodeParamJSON, + { Statusgo.decodeParameters(decodeParamJSON) } + ) + + @ReactMethod(isBlockingSynchronousMethod = true) + fun hexToNumber(hex: String): String = + StatusBackendClient.executeStatusGoRequestWithResult( + "HexToNumber", + hex, + { Statusgo.hexToNumber(hex) } + ) + + @ReactMethod(isBlockingSynchronousMethod = true) + fun numberToHex(numString: String): String = + StatusBackendClient.executeStatusGoRequestWithResult( + "NumberToHex", + numString, + { Statusgo.numberToHex(numString) } + ) + + @ReactMethod(isBlockingSynchronousMethod = true) + fun sha3(str: String): String = + StatusBackendClient.executeStatusGoRequestWithResult( + "Sha3", + str, + { Statusgo.sha3(str) } + ) + + @ReactMethod(isBlockingSynchronousMethod = true) + fun utf8ToHex(str: String): String = + StatusBackendClient.executeStatusGoRequestWithResult( + "Utf8ToHex", + str, + { Statusgo.utf8ToHex(str) } + ) + + @ReactMethod(isBlockingSynchronousMethod = true) + fun hexToUtf8(str: String): String = + StatusBackendClient.executeStatusGoRequestWithResult( + "HexToUtf8", + str, + { Statusgo.hexToUtf8(str) } + ) + + @ReactMethod(isBlockingSynchronousMethod = true) + fun serializeLegacyKey(publicKey: String): String = + StatusBackendClient.executeStatusGoRequestWithResult( + "SerializeLegacyKey", + publicKey, + { Statusgo.serializeLegacyKey(publicKey) } + ) + + @ReactMethod + fun setBlankPreviewFlag(blankPreview: Boolean) { + PreferenceManager.getDefaultSharedPreferences(reactContext) + .edit() + .putBoolean("BLANK_PREVIEW", blankPreview) + .commit() + setSecureFlag() + } + + private fun setSecureFlag() { + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(reactContext) + val setSecure = sharedPrefs.getBoolean("BLANK_PREVIEW", true) + + reactContext.currentActivity?.run { + runOnUiThread { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && setSecure) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + } + } + + @ReactMethod + @Throws(Exception::class) + fun hashTransaction(txArgsJSON: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + "HashTransaction", + txArgsJSON, + { Statusgo.hashTransaction(txArgsJSON) }, + callback + ) + } + + @ReactMethod + @Throws(Exception::class) + fun hashMessage(message: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + "HashMessage", + message, + { Statusgo.hashMessage(message) }, + callback + ) + } + + @ReactMethod + @Throws(Exception::class) + fun multiformatDeserializePublicKey(multiCodecKey: String, base58btc: String, callback: Callback) { + val params = JSONObject().apply { + put("key", multiCodecKey) + put("outBase", base58btc) + } + StatusBackendClient.executeStatusGoRequestWithCallback( + "MultiformatDeserializePublicKeyV2", + params.toString(), + { Statusgo.multiformatDeserializePublicKeyV2(params.toString()) }, + callback + ) + } + + @ReactMethod + @Throws(Exception::class) + fun deserializeAndCompressKey(desktopKey: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + "DeserializeAndCompressKey", + desktopKey, + { Statusgo.deserializeAndCompressKey(desktopKey) }, + callback + ) + } + + @ReactMethod + @Throws(Exception::class) + fun hashTypedData(data: String, callback: Callback) { + utils.executeRunnableStatusGoMethod({ Statusgo.hashTypedData(data) }, callback) + } + + @ReactMethod + @Throws(Exception::class) + fun hashTypedDataV4(data: String, callback: Callback) { + utils.executeRunnableStatusGoMethod({ Statusgo.hashTypedDataV4(data) }, callback) + } + + @ReactMethod + @Throws(Exception::class) + fun signMessage(rpcParams: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + "SignMessage", + rpcParams, + { Statusgo.signMessage(rpcParams) }, + callback + ) + } + + @ReactMethod + @Throws(Exception::class) + fun signTypedData(data: String, account: String, password: String, callback: Callback) { + utils.executeRunnableStatusGoMethod({ Statusgo.signTypedData(data, account, password) }, callback) + } + + @ReactMethod + @Throws(Exception::class) + fun signTypedDataV4(data: String, account: String, password: String, callback: Callback) { + utils.executeRunnableStatusGoMethod({ Statusgo.signTypedDataV4(data, account, password) }, callback) + } + + @ReactMethod + @Throws(Exception::class) + fun signGroupMembership(content: String, callback: Callback) { + utils.executeRunnableStatusGoMethod({ Statusgo.signGroupMembership(content) }, callback) + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/LogManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/LogManager.kt new file mode 100644 index 00000000000..fb519f32585 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/LogManager.kt @@ -0,0 +1,249 @@ +package im.status.ethereum.module + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.util.Log +import androidx.core.content.FileProvider +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import org.json.JSONException +import org.json.JSONObject +import statusgo.Statusgo +import java.io.* +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class LogManager(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val utils = Utils(reactContext) + + override fun getName() = "LogManager" + + private fun showErrorMessage(message: String) { + val activity = currentActivity + + AlertDialog.Builder(activity) + .setTitle("Error") + .setMessage(message) + .setNegativeButton("Exit") { dialog, _ -> + dialog.dismiss() + }.show() + } + + private fun dumpAdbLogsTo(statusLogStream: FileOutputStream) { + val filter = "logcat -d -b main ReactNativeJS:D StatusModule:D StatusService:D StatusNativeLogs:D *:S" + val p = Runtime.getRuntime().exec(filter) + val input = BufferedReader(InputStreamReader(p.inputStream)) + val output = BufferedWriter(OutputStreamWriter(statusLogStream)) + var line: String? + while (input.readLine().also { line = it } != null) { + output.write(line) + output.newLine() + } + output.close() + input.close() + } + + private fun zip(files: Array, zipFile: File, errorList: Stack): Boolean { + val BUFFER = 0x8000 + + try { + var origin: BufferedInputStream? = null + val dest = FileOutputStream(zipFile) + val out = ZipOutputStream(BufferedOutputStream(dest)) + val data = ByteArray(BUFFER) + + for (file in files) { + if (file == null || !file.exists()) { + continue + } + + Log.v("Compress", "Adding: ${file.absolutePath}") + try { + val fi = FileInputStream(file) + origin = BufferedInputStream(fi, BUFFER) + + val entry = ZipEntry(file.name) + out.putNextEntry(entry) + var count: Int + + while (origin.read(data, 0, BUFFER).also { count = it } != -1) { + out.write(data, 0, count) + } + origin.close() + } catch (e: IOException) { + Log.e(TAG, e.message!!) + errorList.push(e.message!!) + } + } + + out.close() + + return true + } catch (e: Exception) { + Log.e(TAG, e.message!!) + e.printStackTrace() + return false + } + } + + @ReactMethod + fun sendLogs(dbJson: String, jsLogs: String, usePublicLogDir: Boolean, callback: Callback) { + Log.d(TAG, "sendLogs") + if (!utils.checkAvailability()) { + return + } + + val context = reactApplicationContext + val logsTempDir = File(context.cacheDir, Utils.LOGS_DIRECTORY_NAME) // This path needs to be in sync with android/app/src/main/res/xml/file_provider_paths.xml + logsTempDir.mkdir() + + val dbFile = File(logsTempDir, "db.json") + try { + val outputStreamWriter = OutputStreamWriter(FileOutputStream(dbFile)) + outputStreamWriter.write(dbJson) + outputStreamWriter.close() + } catch (e: IOException) { + Log.e(TAG, "File write failed: ${e}") + showErrorMessage(e.localizedMessage!!) + } + + val zipFile = File(logsTempDir, logsZipFileName) + val statusLogFile = File(logsTempDir, statusLogFileName) + + try { + if (zipFile.exists() || zipFile.createNewFile()) { + val usableSpace = zipFile.usableSpace + if (usableSpace < 20 * 1024 * 1024) { + val message = "Insufficient space available on device (${android.text.format.Formatter.formatShortFileSize(context, usableSpace)}) to write logs.\nPlease free up some space." + Log.e(TAG, message) + showErrorMessage(message) + return + } + } + + dumpAdbLogsTo(FileOutputStream(statusLogFile)) + + val errorList = Stack() + val filesToZip = mutableListOf(dbFile, statusLogFile) + + // Get all files from the log directory + val logDirectory = utils.getLogDirectory(usePublicLogDir) + if (logDirectory != null && logDirectory.exists()) { + val logFiles = logDirectory.listFiles() + if (logFiles != null) { + for (file in logFiles) { + if (file.isFile) { + Log.d(TAG, "Adding log file: ${file.name}") + filesToZip.add(file) + } + } + } + } + + val zipped = zip(filesToZip.toTypedArray(), zipFile, errorList) + if (zipped && zipFile.exists()) { + zipFile.setReadable(true, false) + val extUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", zipFile) + callback.invoke(extUri.toString()) + } else { + Log.d(TAG, "File ${zipFile.absolutePath} does not exist") + } + } catch (e: Exception) { + Log.e(TAG, e.message!!) + showErrorMessage(e.localizedMessage!!) + e.printStackTrace() + return + } finally { + dbFile.delete() + statusLogFile.delete() + zipFile.deleteOnExit() + } + } + + // workaround for android since react-native-share is not working for zip files, for iOS we use react-native-share + @ReactMethod + fun shareLogs(fileUri: String, callback: Callback) { + Log.d(TAG, "shareLogs: $fileUri") + + try { + val uri = Uri.parse(fileUri) + val intent = Intent(Intent.ACTION_SEND) + intent.type = "application/zip" + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + val chooser = Intent.createChooser(intent, "Share Logs") + chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + reactContext.startActivity(chooser) + + } catch (e: Exception) { + Log.e(TAG, "Error sharing file: ${e.message}") + callback.invoke(e.message) + } + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun logFileDirectory(usePublicLogDir: Boolean): String? { + Log.d(TAG, "logFileDirectory: usePublicLogDir=$usePublicLogDir") + return utils.getLogDirectory(usePublicLogDir)?.absolutePath + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun setProfileLogLevel(setProfileLogLevelRequest: String): String { + Log.d(TAG, "setProfileLogLevel: $setProfileLogLevelRequest") + + return StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "SetProfileLogLevel", + requestBody = setProfileLogLevelRequest, + statusgoFunction = { Statusgo.setProfileLogLevel(setProfileLogLevelRequest) } + ) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun setProfileLogEnabled(setProfileLogEnabledRequest: String): String { + Log.d(TAG, "setProfileLogEnabled: $setProfileLogEnabledRequest") + + return StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "SetLogEnabled", + requestBody = setProfileLogEnabledRequest, + statusgoFunction = { Statusgo.setProfileLogEnabled(setProfileLogEnabledRequest) } + ) + } + + @ReactMethod + fun setPreLoginLogLevel(setPreLoginLogLevelRequest: String) { + Log.d(TAG, "setPreLoginLogLevel: $setPreLoginLogLevelRequest") + + StatusBackendClient.executeStatusGoRequest( + endpoint = "SetPreLoginLogLevel", + requestBody = setPreLoginLogLevelRequest, + statusgoFunction = { Statusgo.setPreLoginLogLevel(setPreLoginLogLevelRequest) } + ) + } + + @ReactMethod + fun setPreLoginLogEnabled(setPreLoginLogEnabledRequest: String) { + Log.d(TAG, "setPreLoginLogEnabled: $setPreLoginLogEnabledRequest") + + StatusBackendClient.executeStatusGoRequest( + endpoint = "SetPreLoginLogEnabled", + requestBody = setPreLoginLogEnabledRequest, + statusgoFunction = { Statusgo.setPreLoginLogEnabled(setPreLoginLogEnabledRequest) } + ) + } + + companion object { + private const val TAG = "LogManager" + private const val statusLogFileName = "Status.log" + private const val logsZipFileName = "Status-debug-logs.zip" + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/MailManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/MailManager.kt new file mode 100644 index 00000000000..c5173bb9dc5 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/MailManager.kt @@ -0,0 +1,133 @@ +package im.status.ethereum.module + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.Callback +import android.util.Log +import android.content.Intent +import android.net.Uri +import android.content.pm.PackageManager +import java.io.File +import androidx.core.content.FileProvider +import android.text.Html + +class MailManager(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val utils = Utils(reactContext) + + override fun getName() = "MailManager" + + @ReactMethod + fun mail(options: ReadableMap, callback: Callback) { + Log.d(TAG, "attempting to send email") + val i = Intent(Intent.ACTION_SEND_MULTIPLE) + val selectorIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) + i.selector = selectorIntent + + if (options.hasKey("subject") && !options.isNull("subject")) { + i.putExtra(Intent.EXTRA_SUBJECT, options.getString("subject")) + } + + if (options.hasKey("body") && !options.isNull("body")) { + val body = options.getString("body") + if (options.hasKey("isHTML") && options.getBoolean("isHTML")) { + i.putExtra(Intent.EXTRA_TEXT, Html.fromHtml(body)) + } else { + i.putExtra(Intent.EXTRA_TEXT, body) + } + } + + if (options.hasKey("recipients") && !options.isNull("recipients")) { + val recipients = options.getArray("recipients") + i.putExtra(Intent.EXTRA_EMAIL, this.utils.readableArrayToStringArray(recipients!!)) + } + + if (options.hasKey("ccRecipients") && !options.isNull("ccRecipients")) { + val ccRecipients = options.getArray("ccRecipients") + i.putExtra(Intent.EXTRA_CC, this.utils.readableArrayToStringArray(ccRecipients!!)) + } + + if (options.hasKey("bccRecipients") && !options.isNull("bccRecipients")) { + val bccRecipients = options.getArray("bccRecipients") + i.putExtra(Intent.EXTRA_BCC, this.utils.readableArrayToStringArray(bccRecipients!!)) + } + + if (options.hasKey("attachments") && !options.isNull("attachments")) { + val r = options.getArray("attachments") + val length = r?.size() ?: 0 + + val provider = reactContext.applicationContext.packageName + ".rnmail.provider" + val resolvedIntentActivities = reactContext.packageManager.queryIntentActivities(i, + PackageManager.MATCH_DEFAULT_ONLY) + + val uris = ArrayList() + for (keyIndex in 0 until length) { + val clip = r?.getMap(keyIndex) + val uri: Uri + if (clip?.hasKey("path") == true && !clip.isNull("path")) { + val path = clip.getString("path") + val file = File(path) + uri = FileProvider.getUriForFile(reactContext, provider, file) + } else if (clip?.hasKey("uri") == true && !clip.isNull("uri")) { + val uriPath = clip.getString("uri") + uri = Uri.parse(uriPath) + } else { + callback.invoke("not_found") + return + } + uris.add(uri) + + for (resolvedIntentInfo in resolvedIntentActivities) { + val packageName = resolvedIntentInfo.activityInfo.packageName + reactContext.grantUriPermission(packageName, uri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + } + + i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + i.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris) + } + + val manager = reactContext.packageManager + val list = manager.queryIntentActivities(i, 0) + + if (list == null || list.isEmpty()) { + Log.d(TAG, "not_available") + callback.invoke("not_available") + return + } + + if (list.size == 1) { + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + reactContext.startActivity(i) + } catch (ex: Exception) { + Log.e(TAG, ex.message!!) + callback.invoke("error") + } + } else { + var chooserTitle = "Send Mail" + + if (options.hasKey("customChooserTitle") && !options.isNull("customChooserTitle")) { + chooserTitle = options.getString("customChooserTitle") ?: "" + } + + val chooser = Intent.createChooser(i, chooserTitle) + chooser.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + try { + reactContext.startActivity(chooser) + } catch (ex: Exception) { + Log.e(TAG, ex.message!!) + callback.invoke("error") + } + } + } + + companion object { + private const val TAG = "MailManager" + } + +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NetworkManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NetworkManager.kt new file mode 100644 index 00000000000..2392632a958 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NetworkManager.kt @@ -0,0 +1,151 @@ +package im.status.ethereum.module + +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import org.json.JSONException +import org.json.JSONObject +import statusgo.Statusgo + +class NetworkManager(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val utils = Utils(reactContext) + + override fun getName() = "NetworkManager" + + @ReactMethod + fun startSearchForLocalPairingPeers(callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "StartSearchForLocalPairingPeers", + requestBody = "", + statusgoFunction = { Statusgo.startSearchForLocalPairingPeers() }, + callback = callback + ) + } + + @ReactMethod + fun getConnectionStringForBootstrappingAnotherDevice(configJSON: String, callback: Callback) { + val jsonConfig = JSONObject(configJSON) + val senderConfig = jsonConfig.getJSONObject("senderConfig") + val keyUID = senderConfig.getString("keyUID") + val keyStorePath = utils.getKeyStorePath(keyUID) + senderConfig.put("keystorePath", keyStorePath) + val jsonString = jsonConfig.toString() + + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "GetConnectionStringForBootstrappingAnotherDevice", + requestBody = jsonString, + statusgoFunction = { Statusgo.getConnectionStringForBootstrappingAnotherDevice(jsonString) }, + callback + ) + } + + @ReactMethod + fun inputConnectionStringForBootstrapping(connectionString: String, configJSON: String, callback: Callback) { + val receiverClientConfig = JSONObject(configJSON) + val params = JSONObject().apply { + put("connectionString", connectionString) + put("receiverClientConfig", receiverClientConfig) + } + val jsonString = params.toString() + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "InputConnectionStringForBootstrappingV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.inputConnectionStringForBootstrappingV2(jsonString) }, + callback + ) + } + + @ReactMethod + fun sendTransactionWithSignature(txArgsJSON: String, signature: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "SendTransactionWithSignature", + requestBody = txArgsJSON, + statusgoFunction = { Statusgo.sendTransactionWithSignature(txArgsJSON, signature) }, + callback + ) + } + + @ReactMethod + fun sendTransaction(txArgsJSON: String, password: String, callback: Callback) { + val jsonParams = JSONObject().apply { + put("txArgs", JSONObject(txArgsJSON)) + put("password", password) + } + val jsonString = jsonParams.toString() + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "SendTransactionV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.sendTransactionV2(jsonString) }, + callback + ) + } + + @ReactMethod + fun callRPC(payload: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "CallRPC", + requestBody = payload, + statusgoFunction = { Statusgo.callRPC(payload) }, + callback = callback + ) + } + + @ReactMethod + fun callPrivateRPC(payload: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "CallPrivateRPC", + requestBody = payload, + statusgoFunction = { Statusgo.callPrivateRPC(payload) }, + callback = callback + ) + } + + @ReactMethod + fun recover(rpcParams: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "Recover", + requestBody = rpcParams, + statusgoFunction = { Statusgo.recover(rpcParams) }, + callback + ) + } + + @ReactMethod + fun getConnectionStringForExportingKeypairsKeystores(configJSON: String, callback: Callback) { + val jsonConfig = JSONObject(configJSON) + val senderConfig = jsonConfig.getJSONObject("senderConfig") + val keyUID = senderConfig.getString("loggedInKeyUid") + val keyStorePath = utils.getKeyStorePath(keyUID) + senderConfig.put("keystorePath", keyStorePath) + val jsonString = jsonConfig.toString() + + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "GetConnectionStringForExportingKeypairsKeystores", + requestBody = jsonString, + statusgoFunction = { Statusgo.getConnectionStringForExportingKeypairsKeystores(jsonString) }, + callback = callback + ) + } + + @ReactMethod + fun inputConnectionStringForImportingKeypairsKeystores(connectionString: String, configJSON: String, callback: Callback) { + val keystoreFilesReceiverClientConfig = JSONObject(configJSON) + val receiverConfig = keystoreFilesReceiverClientConfig.getJSONObject("receiverConfig") + val keyStorePath = utils.pathCombine(utils.getNoBackupDirectory(), "/keystore") + receiverConfig.put("keystorePath", keyStorePath) + + val params = JSONObject().apply { + put("connectionString", connectionString) + put("keystoreFilesReceiverClientConfig", keystoreFilesReceiverClientConfig) + } + val jsonString = params.toString() + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "InputConnectionStringForImportingKeypairsKeystoresV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.inputConnectionStringForImportingKeypairsKeystoresV2(jsonString) }, + callback = callback + ) + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputModule.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputModule.kt new file mode 100644 index 00000000000..56b753ae6ec --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputModule.kt @@ -0,0 +1,72 @@ +package im.status.ethereum.module + +import android.view.ActionMode +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.uimanager.NativeViewHierarchyManager +import com.facebook.react.uimanager.UIBlock +import com.facebook.react.uimanager.UIManagerModule +import com.facebook.react.views.textinput.ReactEditText + +class RNSelectableTextInputModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private var lastActionMode: ActionMode? = null + + override fun getName(): String { + return "RNSelectableTextInputManager" + } + + @ReactMethod + fun setupMenuItems(selectableTextViewReactTag: Int?, textInputReactTag: Int?) { + val reactContext = reactApplicationContext + val uiManager = reactContext.getNativeModule(UIManagerModule::class.java) + selectableTextViewReactTag?.let { selectableTag -> + textInputReactTag?.let { inputTag -> + uiManager?.addUIBlock(UIBlock { nvhm: NativeViewHierarchyManager -> + val rnSelectableTextManager = nvhm.resolveViewManager(selectableTag) as RNSelectableTextInputViewManager + val reactTextView = nvhm.resolveView(inputTag) as ReactEditText + rnSelectableTextManager.registerSelectionListener(reactTextView) + }) + } + } + } + + @ReactMethod + fun startActionMode(textInputReactTag: Int?) { + val reactContext = reactApplicationContext + val uiManager = reactContext.getNativeModule(UIManagerModule::class.java) + textInputReactTag?.let { inputTag -> + uiManager?.addUIBlock(UIBlock { nvhm: NativeViewHierarchyManager -> + val reactTextView = nvhm.resolveView(inputTag) as ReactEditText + lastActionMode = reactTextView.startActionMode(reactTextView.customSelectionActionModeCallback, ActionMode.TYPE_FLOATING) + }) + } + } + + @ReactMethod + fun hideLastActionMode() { + val reactContext = reactApplicationContext + val uiManager = reactContext.getNativeModule(UIManagerModule::class.java) + uiManager?.addUIBlock(UIBlock { _ -> + lastActionMode?.finish() + lastActionMode = null + }) + } + + @ReactMethod + fun setSelection(textInputReactTag: Int?, start: Int?, end: Int?) { + val reactContext = reactApplicationContext + val uiManager = reactContext.getNativeModule(UIManagerModule::class.java) + textInputReactTag?.let { inputTag -> + start?.let { s -> + end?.let { e -> + uiManager?.addUIBlock(UIBlock { nvhm: NativeViewHierarchyManager -> + val reactTextView = nvhm.resolveView(inputTag) as ReactEditText + reactTextView.setSelection(s, e) + }) + } + } + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputViewManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputViewManager.kt new file mode 100644 index 00000000000..6ffe4f54d4f --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputViewManager.kt @@ -0,0 +1,85 @@ +package im.status.ethereum.module + +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.common.MapBuilder +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.uimanager.events.RCTEventEmitter +import com.facebook.react.views.textinput.ReactEditText +import com.facebook.react.views.view.ReactViewGroup +import com.facebook.react.views.view.ReactViewManager + +class RNSelectableTextInputViewManager : ReactViewManager() { + companion object { + const val REACT_CLASS = "RNSelectableTextInput" + } + + private var _menuItems = arrayOf() + + override fun getName(): String { + return REACT_CLASS + } + + override fun createViewInstance(context: ThemedReactContext): ReactViewGroup { + return ReactViewGroup(context) + } + + @ReactProp(name = "menuItems") + fun setMenuItems(reactViewGroup: ReactViewGroup, items: ReadableArray?) { + _menuItems = items?.let { + Array(items.size()) { i -> items.getString(i) } + } ?: arrayOf() + } + + fun registerSelectionListener(view: ReactEditText) { + view.customSelectionActionModeCallback = object : ActionMode.Callback { + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + menu.clear() + _menuItems.forEachIndexed { i, item -> + menu.add(0, i, 0, item) + } + return true + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + return true + } + + override fun onDestroyActionMode(mode: ActionMode) {} + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val selectionStart = view.selectionStart + val selectionEnd = view.selectionEnd + val selectedText = view.text.toString().substring(selectionStart, selectionEnd) + + onSelectNativeEvent(view, item.itemId, selectedText, selectionStart, selectionEnd) + mode.finish() + return true + } + } + } + + private fun onSelectNativeEvent(view: ReactEditText, eventType: Int, content: String, selectionStart: Int, selectionEnd: Int) { + val event: WritableMap = Arguments.createMap().apply { + putInt("eventType", eventType) + putString("content", content) + putInt("selectionStart", selectionStart) + putInt("selectionEnd", selectionEnd) + } + + val reactContext = view.context as ReactContext + reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(view.id, "topSelection", event) + } + + override fun getExportedCustomDirectEventTypeConstants(): Map? { + return MapBuilder.builder() + .put("topSelection", MapBuilder.of("registrationName", "onSelection")) + .build() + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/ServiceConnector.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/ServiceConnector.java deleted file mode 100644 index 43afd901185..00000000000 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/ServiceConnector.java +++ /dev/null @@ -1,153 +0,0 @@ -package im.status.ethereum.module; - - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.*; -import android.util.Log; - -import java.util.ArrayList; - -public class ServiceConnector { - - private static final String TAG = "ServiceConnector"; - /** Context of the activity from which this connector was launched */ - private Context context; - - /** The class of the service we want to connect to */ - private Class serviceClass; - - /** Flag indicating if the service is bound. */ - boolean isBound; - - /** Sends messages to the service. */ - Messenger serviceMessenger = null; - - /** Receives messages from the service. */ - Messenger clientMessenger = null; - - private ArrayList handlers = new ArrayList<>(); - - /** Handles incoming messages from service. */ - private class IncomingHandler extends Handler { - - IncomingHandler(HandlerThread thread) { - - super(thread.getLooper()); - } - - @Override - public void handleMessage(Message message) { - - boolean isClaimed = false; - //if (message.obj != null) { - // String identifier = ((Bundle) message.obj).getString("identifier"); - //if (identifier != null) { - - for (ConnectorHandler handler : handlers) { - // if (identifier.equals(handler.getID())) { - isClaimed = handler.handleMessage(message); - // } - } - // } - //} - if (!isClaimed) { - super.handleMessage(message); - } - } - } - - /** - * Class for interacting with the main interface of the service. - */ - private ServiceConnection serviceConnection = new ServiceConnection() { - - public void onServiceConnected(ComponentName className, IBinder service) { - - // This is called when the connection with the service has been - // established, giving us the object we can use to - // interact with the service. We are communicating with the - // service using a Messenger, so here we get a client-side - // representation of that from the raw IBinder object. - - serviceMessenger = new Messenger(service); - isBound = true; - for (ConnectorHandler handler: handlers) { - handler.onConnectorConnected(); - } - } - - public void onServiceDisconnected(ComponentName className) { - - // This is called when the connection with the service has been - // unexpectedly disconnected -- that is, its process crashed. - serviceMessenger = null; - isBound = false; - for (ConnectorHandler handler: handlers) { - handler.onConnectorDisconnected(); - } - } - }; - - ServiceConnector(Context context, Class serviceClass) { - this.context = context; - this.serviceClass = serviceClass; - // Handler thread to avoid running on the main UI thread - HandlerThread handlerThread = new HandlerThread("HandlerThread"); - handlerThread.start(); - // Incoming message handler. Calls to its binder are sequential! - IncomingHandler handler = new IncomingHandler(handlerThread); - clientMessenger = new Messenger(handler); - } - - /** Bind to the service */ - public boolean bindService() { - - if (serviceConnection != null) { - Intent intent = new Intent(context, serviceClass); - context.getApplicationContext().startService(intent); - return context.getApplicationContext().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); - } else { - return false; - } - } - - /** Unbind from the service */ - public void unbindService() { - - if (isBound && serviceConnection != null) { - context.getApplicationContext().unbindService(serviceConnection); - isBound = false; -/* - Intent intent = new Intent(context, serviceClass); - context.getApplicationContext().stopService(intent); -*/ - } - } - - public void registerHandler(ConnectorHandler handler) { - - if (!handlers.contains(handler)) { - handlers.add(handler); - } - } - - public void removeHandler(ConnectorHandler handler) { - - handlers.remove(handler); - } - - public void sendMessage() { - - Message msg = Message.obtain(null, 0, 0, 0); - msg.replyTo = clientMessenger; - try { - Log.d(TAG, "Sending message to service: "); - serviceMessenger.send(msg); - } catch (RemoteException e) { - Log.e(TAG, "Exception sending message(" + msg.toString() + ") to service: ", e); - } - } -} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusBackendClient.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusBackendClient.kt new file mode 100644 index 00000000000..f2829100100 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusBackendClient.kt @@ -0,0 +1,209 @@ +package im.status.ethereum.module + +import android.util.Log +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.Callback +import java.net.SocketException +import java.net.SocketTimeoutException +import java.util.concurrent.TimeUnit + +class StatusBackendClient(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + companion object { + private const val TAG = "StatusBackendClient" + private val JSON = "application/json; charset=utf-8".toMediaType() + private const val TIMEOUT_SECONDS = 30L + @Volatile private var instance: StatusBackendClient? = null + private lateinit var utils: Utils + + fun getInstance(): StatusBackendClient? = instance + + @JvmStatic + fun executeStatusGoRequest( + endpoint: String, + requestBody: String, + statusgoFunction: () -> String + ) { + val statusBackendClient = getInstance() + if (statusBackendClient?.serverEnabled == true) { + val result = statusBackendClient.request(endpoint, requestBody) + result.onSuccess { response -> + utils.handleStatusGoResponse(response, endpoint) + }.onFailure { error -> + Log.e(TAG, "request to $endpoint failed", error) + } + } else { + val result = statusgoFunction() + utils.handleStatusGoResponse(result, endpoint) + } + } + + @JvmStatic + fun executeStatusGoRequestWithCallback( + endpoint: String, + requestBody: String, + statusgoFunction: () -> String, + callback: Callback? + ) { + val statusBackendClient = getInstance() + if (statusBackendClient?.serverEnabled == true) { + val runnable = Runnable { + val result = statusBackendClient.request(endpoint, requestBody) + result.onSuccess { response -> + callback?.invoke(response) + }.onFailure { error -> + Log.e(TAG, "request to $endpoint failed", error) + callback?.invoke(false) + } + } + StatusThreadPoolExecutor.getInstance().execute(runnable) + } else { + utils.executeRunnableStatusGoMethod(statusgoFunction, callback) + } + } + + @JvmStatic + fun executeStatusGoRequestWithResult( + endpoint: String, + requestBody: String, + statusgoFunction: () -> String + ): String { + val statusBackendClient = getInstance() + return if (statusBackendClient?.serverEnabled == true) { + val result = statusBackendClient.request(endpoint, requestBody) + result.getOrElse { error -> + Log.e(TAG, "request to $endpoint failed", error) + "" + } + } else { + statusgoFunction() + } + } + } + + init { + instance = this + utils = Utils(reactContext) + } + + override fun getName(): String = "StatusBackendClient" + + private val httpClient = OkHttpClient.Builder() + .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build() + + private val wsClient = OkHttpClient.Builder() + .retryOnConnectionFailure(true) + .build() + + private var webSocket: WebSocket? = null + + @Volatile var serverEnabled = false + @Volatile private var statusGoEndpoint: String? = null + @Volatile private var signalEndpoint: String? = null + @Volatile var rootDataDir: String? = null + + @ReactMethod + fun configStatusBackendServer( + serverEnabled: Boolean, + statusGoEndpoint: String, + signalEndpoint: String, + rootDataDir: String + ) { + configure(serverEnabled, statusGoEndpoint, signalEndpoint, rootDataDir) + } + + private fun configure( + serverEnabled: Boolean, + statusGoEndpoint: String, + signalEndpoint: String, + rootDataDir: String + ) { + Log.d(TAG, "configure: serverEnabled=$serverEnabled, statusGoEndpoint=$statusGoEndpoint, " + + "signalEndpoint=$signalEndpoint, rootDataDir=$rootDataDir") + + this.serverEnabled = serverEnabled + if (serverEnabled) { + this.statusGoEndpoint = statusGoEndpoint + this.signalEndpoint = signalEndpoint + this.rootDataDir = rootDataDir + connectWebSocket() + } else { + disconnectWebSocket() + this.statusGoEndpoint = null + this.signalEndpoint = null + this.rootDataDir = null + } + } + + private fun connectWebSocket() { + if (!serverEnabled || signalEndpoint == null) { + return + } + + val request = Request.Builder() + .url("$signalEndpoint") + .build() + + webSocket = wsClient.newWebSocket(request, object : WebSocketListener() { + override fun onMessage(webSocket: WebSocket, text: String) { + StatusModule.getInstance()?.handleSignal(text) + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.e(TAG, "WebSocket error: ${t.message}") + } + }) + } + + private fun disconnectWebSocket() { + webSocket?.cancel() + webSocket = null + } + + fun request(endpoint: String, body: String): Result { + if (!serverEnabled || statusGoEndpoint == null) { + return Result.failure(IllegalStateException("Status backend server is not enabled")) + } + + val fullUrl = "$statusGoEndpoint$endpoint" + + return try { + val request = Request.Builder() + .url(fullUrl) + .post(body.toRequestBody(JSON)) + .build() + + httpClient.newCall(request).execute().use { response -> + val responseBody = response.body?.string() ?: "" + + if (response.isSuccessful) { + Log.d(TAG, "Request to $endpoint succeeded: $responseBody") + Result.success(responseBody) + } else { + val errorMsg = "Request failed with code ${response.code}: $responseBody" + Log.e(TAG, "Request to $endpoint failed: $errorMsg") + Result.failure(Exception(errorMsg)) + } + } + } catch (e: Exception) { + when (e) { + is SocketTimeoutException -> Log.e(TAG, "Request to $endpoint timed out", e) + is SocketException -> Log.e(TAG, "Socket error for $endpoint", e) + else -> Log.e(TAG, "Request to $endpoint failed with exception", e) + } + Result.failure(e) + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java deleted file mode 100644 index 7805154389c..00000000000 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java +++ /dev/null @@ -1,725 +0,0 @@ -package im.status.ethereum.module; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.*; -import android.view.WindowManager; -import android.text.TextUtils; -import android.util.Log; -import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; -import android.webkit.WebStorage; - -import com.facebook.react.bridge.*; -import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.github.status_im.status_go.Statusgo; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.FileInputStream; -import java.io.OutputStream; -import java.io.FileOutputStream; -import java.util.HashMap; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.json.JSONObject; -import org.json.JSONException; -import com.instabug.library.Instabug; - -class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventListener, ConnectorHandler { - - private static final String TAG = "StatusModule"; - - private HashMap callbacks = new HashMap<>(); - - private static StatusModule module; - private ServiceConnector status = null; - private ExecutorService executor = null; - private boolean debug; - private boolean devCluster; - private String logLevel; - - StatusModule(ReactApplicationContext reactContext, boolean debug, boolean devCluster, String logLevel) { - super(reactContext); - if (executor == null) { - executor = Executors.newCachedThreadPool(); - } - this.debug = debug; - this.devCluster = devCluster; - this.logLevel = logLevel; - reactContext.addLifecycleEventListener(this); - } - - @Override - public String getName() { - return "Status"; - } - - @Override - public void onHostResume() { // Actvity `onResume` - module = this; - Activity currentActivity = getCurrentActivity(); - if (currentActivity == null) { - Log.d(TAG, "On host Activity doesn't exist"); - return; - } - - if (status == null) { - status = new ServiceConnector(currentActivity, StatusService.class); - status.registerHandler(this); - } - - status.bindService(); - - signalEvent("{\"type\":\"module.initialized\"}"); - } - - @Override - public void onHostPause() { - if (status != null) { - status.unbindService(); - } - } - - @Override - public void onHostDestroy() { - - } - - private boolean checkAvailability() { - - Activity currentActivity = getCurrentActivity(); - if (currentActivity == null) { - Log.d(TAG, "Activity doesn't exist"); - return false; - } - - return true; - } - - - void signalEvent(String jsonEvent) { - Log.d(TAG, "Signal event: " + jsonEvent); - WritableMap params = Arguments.createMap(); - params.putString("jsonEvent", jsonEvent); - this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params); - } - - private String prepareLogsFile() { - String gethLogFileName = "geth.log"; - File pubDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - File logFile = new File(pubDirectory, gethLogFileName); - - try { - logFile.setReadable(true); - File parent = logFile.getParentFile(); - if (!parent.canWrite()) { - return null; - } - if (!parent.exists()) { - parent.mkdirs(); - } - logFile.createNewFile(); - logFile.setWritable(true); - Log.d(TAG, "Can write " + logFile.canWrite()); - Uri gethLogUri = Uri.fromFile(logFile); - try { - Log.d(TAG, "Attach to geth.log to instabug " + gethLogUri.getPath()); - Instabug.setFileAttachment(gethLogUri, gethLogFileName); - } catch (NullPointerException e) { - Log.d(TAG, "Instabug is not initialized!"); - } - - String gethLogFilePath = logFile.getAbsolutePath(); - Log.d(TAG, gethLogFilePath); - - return gethLogFilePath; - } catch (Exception e) { - Log.d(TAG, "Can't create geth.log file! " + e.getMessage()); - } - - return null; - } - - private void doStartNode(final String defaultConfig) { - - Activity currentActivity = getCurrentActivity(); - - String root = currentActivity.getApplicationInfo().dataDir; - String dataFolder = root + "/ethereum/testnet"; - Log.d(TAG, "Starting Geth node in folder: " + dataFolder); - - try { - final File newFile = new File(dataFolder); - // todo handle error? - newFile.mkdir(); - } catch (Exception e) { - Log.e(TAG, "error making folder: " + dataFolder, e); - } - - final String ropstenFlagPath = root + "/ropsten_flag"; - final File ropstenFlag = new File(ropstenFlagPath); - if (!ropstenFlag.exists()) { - try { - final String chaindDataFolderPath = dataFolder + "/StatusIM/lightchaindata"; - final File lightChainFolder = new File(chaindDataFolderPath); - if (lightChainFolder.isDirectory()) { - String[] children = lightChainFolder.list(); - for (int i = 0; i < children.length; i++) { - new File(lightChainFolder, children[i]).delete(); - } - } - lightChainFolder.delete(); - ropstenFlag.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - - int testnetNetworkId = 3; - String testnetDataDir = root + "/ethereum/testnet"; - String oldKeystoreDir = testnetDataDir + "/keystore"; - String newKeystoreDir = root + "/keystore"; - final File oldKeystore = new File(oldKeystoreDir); - if (oldKeystore.exists()) { - try { - final File newKeystore = new File(newKeystoreDir); - copyDirectory(oldKeystore, newKeystore); - - if (oldKeystore.isDirectory()) { - String[] children = oldKeystore.list(); - for (int i = 0; i < children.length; i++) { - new File(oldKeystoreDir, children[i]).delete(); - } - } - oldKeystore.delete(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - String config; - try { - JSONObject customConfig = new JSONObject(defaultConfig); - - String dataDir = root + customConfig.get("DataDir"); - - config = Statusgo.GenerateConfig(dataDir, customConfig.getInt("NetworkId")); - - JSONObject jsonConfig = new JSONObject(config); - - String gethLogFilePath = TextUtils.isEmpty(this.logLevel) ? null : prepareLogsFile(); - boolean logsEnabled = (gethLogFilePath != null); - - jsonConfig.put("LogEnabled", logsEnabled); - jsonConfig.put("LogFile", gethLogFilePath); - jsonConfig.put("LogLevel", TextUtils.isEmpty(this.logLevel) ? "ERROR" : this.logLevel.toUpperCase()); - jsonConfig.put("DataDir", dataDir); - jsonConfig.put("NetworkId", customConfig.get("NetworkId")); - try { - Object upstreamConfig = customConfig.get("UpstreamConfig"); - if (upstreamConfig != null) { - Log.d(TAG, "UpstreamConfig is not null"); - jsonConfig.put("UpstreamConfig", upstreamConfig); - } - } catch (Exception e) { - - } - try { - JSONObject whisperConfig = (JSONObject) jsonConfig.get("WhisperConfig"); - if (whisperConfig == null) { - whisperConfig = new JSONObject(); - } - whisperConfig.put("LightClient", true); - jsonConfig.put("WhisperConfig", whisperConfig); - } catch (Exception e) { - - } - try { - Object clusterConfig = customConfig.get("ClusterConfig"); - if (clusterConfig != null) { - Log.d(TAG, "ClusterConfig is not null"); - jsonConfig.put("ClusterConfig", clusterConfig); - } - } catch (Exception e) { - Log.w(TAG, "Something went wrong parsing cluster config" + e.getMessage()); - } - - - jsonConfig.put("KeyStoreDir", newKeystoreDir); - - config = jsonConfig.toString(); - } catch (JSONException e) { - config = Statusgo.GenerateConfig(testnetDataDir, testnetNetworkId); - Log.d(TAG, "Something went wrong " + e.getMessage()); - Log.d(TAG, "Default configuration will be used"); - } - - String configOutput = config; - final int maxOutputLen = 4000; - while (!configOutput.isEmpty()) { - Log.d(TAG, "Node config:" + configOutput.substring(0, Math.min(maxOutputLen, configOutput.length()))); - if (configOutput.length() > maxOutputLen) { - configOutput = configOutput.substring(maxOutputLen); - } else { - break; - } - } - - String res = Statusgo.StartNode(config); - if (res.startsWith("{\"error\":\"\"")) { - Log.d(TAG, "StartNode result: " + res); - } - else { - Log.e(TAG, "StartNode failed: " + res); - } - Log.d(TAG, "Geth node started"); - status.sendMessage(); - } - - private String getOldExternalDir() { - File extStore = Environment.getExternalStorageDirectory(); - return extStore.exists() ? extStore.getAbsolutePath() + "/ethereum/testnet" : getNewInternalDir(); - } - - private String getNewInternalDir() { - Activity currentActivity = getCurrentActivity(); - return currentActivity.getApplicationInfo().dataDir + "/ethereum/testnet"; - } - - private void deleteDirectory(File folder) { - File[] files = folder.listFiles(); - if (files != null) { - for (File f : files) { - if (f.isDirectory()) { - deleteDirectory(f); - } else { - f.delete(); - } - } - } - folder.delete(); - } - - private void copyDirectory(File sourceLocation, File targetLocation) throws IOException { - if (sourceLocation.isDirectory()) { - if (!targetLocation.exists() && !targetLocation.mkdirs()) { - throw new IOException("Cannot create dir " + targetLocation.getAbsolutePath()); - } - - String[] children = sourceLocation.list(); - for (int i = 0; i < children.length; i++) { - copyDirectory(new File(sourceLocation, children[i]), new File(targetLocation, children[i])); - } - } else { - File directory = targetLocation.getParentFile(); - if (directory != null && !directory.exists() && !directory.mkdirs()) { - throw new IOException("Cannot create dir " + directory.getAbsolutePath()); - } - - InputStream in = new FileInputStream(sourceLocation); - OutputStream out = new FileOutputStream(targetLocation); - - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - in.close(); - out.close(); - } - } - - @ReactMethod - public void shouldMoveToInternalStorage(Callback callback) { - String oldDir = getOldExternalDir(); - String newDir = getNewInternalDir(); - - File oldDirFile = new File(oldDir); - File newDirFile = new File(newDir); - - callback.invoke(oldDirFile.exists() && !newDirFile.exists()); - } - - @ReactMethod - public void moveToInternalStorage(Callback callback) { - String oldDir = getOldExternalDir(); - String newDir = getNewInternalDir(); - - try { - File oldDirFile = new File(oldDir); - copyDirectory(oldDirFile, new File(newDir)); - deleteDirectory(oldDirFile); - } catch (IOException e) { - Log.d(TAG, "Moving error: " + e); - } - - callback.invoke(); - } - - @ReactMethod - public void startNode(final String config) { - Log.d(TAG, "startNode"); - if (!checkAvailability()) { - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - doStartNode(config); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void stopNode() { - - Runnable r = new Runnable() { - @Override - public void run() { - Log.d(TAG, "stopNode"); - String res = Statusgo.StopNode(); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void login(final String address, final String password, final Callback callback) { - Log.d(TAG, "login"); - if (!checkAvailability()) { - callback.invoke(false); - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - String result = Statusgo.Login(address, password); - - callback.invoke(result); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void createAccount(final String password, final Callback callback) { - Log.d(TAG, "createAccount"); - if (!checkAvailability()) { - callback.invoke(false); - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.CreateAccount(password); - - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void notifyUsers(final String message, final String payloadJSON, final String tokensJSON, final Callback callback) { - Log.d(TAG, "notifyUsers"); - if (!checkAvailability()) { - callback.invoke(false); - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.NotifyUsers(message, payloadJSON, tokensJSON); - - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void addPeer(final String enode, final Callback callback) { - Log.d(TAG, "addPeer"); - if (!checkAvailability()) { - callback.invoke(false); - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.AddPeer(enode); - - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - - @ReactMethod - public void recoverAccount(final String passphrase, final String password, final Callback callback) { - Log.d(TAG, "recoverAccount"); - if (!checkAvailability()) { - callback.invoke(false); - return; - } - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.RecoverAccount(password, passphrase); - - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - private String createIdentifier() { - return UUID.randomUUID().toString(); - } - - @ReactMethod - public void approveSignRequest(final String id, final String password, final Callback callback) { - Log.d(TAG, "approveSignRequest"); - if (!checkAvailability()) { - callback.invoke(false); - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.ApproveSignRequest(id, password); - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void approveSignRequestWithArgs(final String id, final String password, final String gas, final String gasPrice, final Callback callback) { - Log.d(TAG, "approveSignRequestWithArgs"); - if (!checkAvailability()) { - callback.invoke(false); - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.ApproveSignRequestWithArgs(id, password, Long.parseLong(gas), Long.parseLong(gasPrice)); - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - - @ReactMethod - public void discardSignRequest(final String id) { - Log.d(TAG, "discardSignRequest"); - if (!checkAvailability()) { - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - Statusgo.DiscardSignRequest(id); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void setAdjustResize() { - Log.d(TAG, "setAdjustResize"); - final Activity activity = getCurrentActivity(); - if (activity == null) { - return; - } - - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - } - }); - } - - @ReactMethod - public void setAdjustPan() { - Log.d(TAG, "setAdjustPan"); - final Activity activity = getCurrentActivity(); - if (activity == null) { - return; - } - - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - }); - } - - @ReactMethod - public void setSoftInputMode(final int mode) { - Log.d(TAG, "setSoftInputMode"); - final Activity activity = getCurrentActivity(); - if (activity == null) { - return; - } - - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - activity.getWindow().setSoftInputMode(mode); - } - }); - } - - @SuppressWarnings("deprecation") - @ReactMethod - public void clearCookies() { - Log.d(TAG, "clearCookies"); - final Activity activity = getCurrentActivity(); - if (activity == null) { - return; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - CookieManager.getInstance().removeAllCookies(null); - CookieManager.getInstance().flush(); - } else { - CookieSyncManager cookieSyncManager = CookieSyncManager.createInstance(activity); - cookieSyncManager.startSync(); - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.removeAllCookie(); - cookieManager.removeSessionCookie(); - cookieSyncManager.stopSync(); - cookieSyncManager.sync(); - } - } - - @ReactMethod - public void clearStorageAPIs() { - Log.d(TAG, "clearStorageAPIs"); - final Activity activity = getCurrentActivity(); - if (activity == null) { - return; - } - - WebStorage storage = WebStorage.getInstance(); - if (storage != null) { - storage.deleteAllData(); - } - } - - @Override - public boolean handleMessage(Message message) { - - Log.d(TAG, "Received message: " + message.toString()); - Bundle bundle = message.getData(); - - String event = bundle.getString("event"); - signalEvent(event); - - return true; - } - - @Override - public void onConnectorConnected() { - - } - - @Override - public void onConnectorDisconnected() { - - } - - @ReactMethod - public void sendWeb3Request(final String payload, final Callback callback) { - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.CallRPC(payload); - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void sendWeb3PrivateRequest(final String payload, final Callback callback) { - Runnable r = new Runnable() { - @Override - public void run() { - String res = Statusgo.CallPrivateRPC(payload); - callback.invoke(res); - } - }; - - StatusThreadPoolExecutor.getInstance().execute(r); - } - - @ReactMethod - public void closeApplication() { - System.exit(0); - } - - @ReactMethod - public void connectionChange(final String type, final boolean isExpensive) { - Log.d(TAG, "ConnectionChange: " + type + ", is expensive " + isExpensive); - Statusgo.ConnectionChange(type, isExpensive ? 1 : 0); - } - - @ReactMethod - public void appStateChange(final String type) { - Log.d(TAG, "AppStateChange: " + type); - Statusgo.AppStateChange(type); - } - - private static String uniqueID = null; - private static final String PREF_UNIQUE_ID = "PREF_UNIQUE_ID"; - - @ReactMethod - public void getDeviceUUID(final Callback callback) { - if (uniqueID == null) { - SharedPreferences sharedPrefs = this.getReactApplicationContext().getSharedPreferences( - PREF_UNIQUE_ID, Context.MODE_PRIVATE); - uniqueID = sharedPrefs.getString(PREF_UNIQUE_ID, null); - if (uniqueID == null) { - uniqueID = UUID.randomUUID().toString(); - SharedPreferences.Editor editor = sharedPrefs.edit(); - editor.putString(PREF_UNIQUE_ID, uniqueID); - editor.commit(); - } - } - callback.invoke(uniqueID); - } -} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.kt new file mode 100644 index 00000000000..df2f2206f23 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.kt @@ -0,0 +1,189 @@ +package im.status.ethereum.module + +import android.app.Activity +import android.os.Build +import android.util.Log +import com.facebook.react.bridge.* +import com.facebook.react.modules.core.DeviceEventManagerModule +import statusgo.SignalHandler +import statusgo.Statusgo +import org.json.JSONException +import android.view.WindowManager +import org.json.JSONObject + +class StatusModule(private val reactContext: ReactApplicationContext, private val rootedDevice: Boolean) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener, SignalHandler { + + companion object { + private const val TAG = "StatusModule" + private var module: StatusModule? = null + + fun getInstance(): StatusModule? { + return module + } + } + + private val utils: Utils = Utils(reactContext) + private var background: Boolean = false + + init { + reactContext.addLifecycleEventListener(this) + } + + override fun getName(): String { + return "Status" + } + + override fun onHostResume() { + module = this + background = false + Statusgo.setMobileSignalHandler(this) + } + + override fun onHostPause() { + background = true + } + + override fun onHostDestroy() { + Log.d(TAG, "******************* ON HOST DESTROY *************************") + } + + override fun handleSignal(jsonEventString: String) { + val params = Arguments.createMap() + params.putString("jsonEvent", jsonEventString) + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit("gethEvent", params) + } + + @ReactMethod + fun closeApplication() { + System.exit(0) + } + + @ReactMethod + fun connectionChange(type: String, isExpensive: Boolean) { + Log.d(TAG, "ConnectionChange: $type, is expensive $isExpensive") + val params = JSONObject().apply { + put("type", type) + put("expensive", isExpensive) + } + + val jsonString = params.toString() + StatusBackendClient.executeStatusGoRequest( + endpoint = "ConnectionChangeV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.connectionChangeV2(jsonString) } + ) + } + + @ReactMethod + fun appStateChange(state: String) { + Log.d(TAG, "AppStateChange: $state") + val params = JSONObject().apply { + put("state", state) + } + val jsonString = params.toString() + StatusBackendClient.executeStatusGoRequest( + endpoint = "AppStateChangeV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.appStateChangeV2(jsonString) } + ) + } + + @ReactMethod + fun startLocalNotifications() { + Log.d(TAG, "startLocalNotifications") + StatusBackendClient.executeStatusGoRequest( + endpoint = "StartLocalNotifications", + requestBody = "", + statusgoFunction = { Statusgo.startLocalNotifications() } + ) + } + + @ReactMethod + fun getNodeConfig(callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "GetNodeConfig", + requestBody = "", + statusgoFunction = { Statusgo.getNodeConfig() }, + callback = callback + ) + } + + @ReactMethod + fun intendedPanic(message: String) { + StatusBackendClient.executeStatusGoRequest( + endpoint = "IntendedPanic", + requestBody = message, + statusgoFunction = { Statusgo.intendedPanic(message) }, + ) + } + + @ReactMethod + fun addCentralizedMetric(request: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "AddCentralizedMetric", + requestBody = request, + statusgoFunction = { Statusgo.addCentralizedMetric(request) }, + callback + ) + } + + @ReactMethod + fun toggleCentralizedMetrics(request: String, callback: Callback) { + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "ToggleCentralizedMetrics", + requestBody = request, + statusgoFunction = { Statusgo.toggleCentralizedMetrics(request) }, + callback + ) + } + + @ReactMethod + fun deleteImportedKey(keyUID: String, address: String, password: String, callback: Callback) { + val keyStoreDir = utils.getKeyStorePath(keyUID) + val params = JSONObject().apply { + put("address", address) + put("password", password) + put("keyStoreDir", keyStoreDir) + } + val jsonString = params.toString() + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "DeleteImportedKeyV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.deleteImportedKeyV2(jsonString) }, + callback + ) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun fleets(): String { + return StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "Fleets", + requestBody = "", + statusgoFunction = { Statusgo.fleets() } + ) + } + + override fun getConstants(): Map? { + return hashMapOf( + "is24Hour" to utils.is24Hour(), + "model" to Build.MODEL, + "brand" to Build.BRAND, + "buildId" to Build.ID, + "deviceId" to Build.BOARD + ) + } + + @ReactMethod + fun isDeviceRooted(callback: Callback) { + callback.invoke(rootedDevice) + } + + @ReactMethod + fun deactivateKeepAwake() { + val activity = currentActivity + + activity?.runOnUiThread { + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.java deleted file mode 100644 index 0307c57459d..00000000000 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.java +++ /dev/null @@ -1,54 +0,0 @@ -package im.status.ethereum.module; - -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.JavaScriptModule; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; -import com.github.status_im.status_go.Statusgo; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import im.status.ethereum.function.Function; - -public class StatusPackage implements ReactPackage { - - private boolean debug; - private boolean devCluster; - private String logLevel; - - public StatusPackage (boolean debug, boolean devCluster, String logLevel) { - this.debug = debug; - this.devCluster = devCluster; - this.logLevel = logLevel; - } - - @Override - public List createNativeModules(ReactApplicationContext reactContext) { - List modules = new ArrayList<>(); - System.loadLibrary("statusgoraw"); - System.loadLibrary("statusgo"); - modules.add(new StatusModule(reactContext, this.debug, this.devCluster, this.logLevel)); - - return modules; - } - - public List> createJSModules() { - return Collections.emptyList(); - } - - @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); - } - - public Function getCallRPC() { - return new Function() { - @Override - public String apply(String payload) { - return Statusgo.CallRPC(payload); - } - }; - } -} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.kt new file mode 100644 index 00000000000..fb8220c0b66 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.kt @@ -0,0 +1,45 @@ +package im.status.ethereum.module + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager +import statusgo.Statusgo + +class StatusPackage(private val rootedDevice: Boolean) : ReactPackage { + + companion object { + fun getImageTLSCert(): String = + StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "ImageServerTLSCert", + requestBody = "", + statusgoFunction = { Statusgo.imageServerTLSCert() } + ) + } + + override fun createNativeModules(reactContext: ReactApplicationContext): List { + val modules = mutableListOf() + + modules.apply { + add(StatusModule(reactContext, rootedDevice)) + add(AccountManager(reactContext)) + add(EncryptionUtils(reactContext)) + add(DatabaseManager(reactContext)) + add(UIHelper(reactContext)) + add(LogManager(reactContext)) + add(Utils(reactContext)) + add(NetworkManager(reactContext)) + add(MailManager(reactContext)) + add(RNSelectableTextInputModule(reactContext)) + add(StatusBackendClient(reactContext)) + } + + return modules + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return listOf( + RNSelectableTextInputViewManager() + ) + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusService.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusService.java deleted file mode 100644 index fec769145b0..00000000000 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusService.java +++ /dev/null @@ -1,106 +0,0 @@ -package im.status.ethereum.module; - -import android.app.Service; -import android.content.Intent; -import android.os.*; -import android.support.annotation.Nullable; -import android.util.Log; -import java.util.concurrent.CountDownLatch; - -import java.lang.ref.WeakReference; - -public class StatusService extends Service { - - private static final String TAG = "StatusService"; - - public StatusService() { - super(); - } - - private static class IncomingHandler extends Handler { - - private final WeakReference service; - - IncomingHandler(StatusService service) { - - this.service = new WeakReference<>(service); - } - - @Override - public void handleMessage(Message message) { - - StatusService service = this.service.get(); - if (service != null) { - if (!service.handleMessage(message)) { - super.handleMessage(message); - } - } - } - } - - private static CountDownLatch applicationMessengerIsSet = new CountDownLatch(1); - - private final Messenger serviceMessenger = new Messenger(new IncomingHandler(this)); - - private static Messenger applicationMessenger = null; - - private boolean handleMessage(Message message) { - Log.d(TAG, "Received service message." + message.toString()); - applicationMessenger = message.replyTo; - applicationMessengerIsSet.countDown(); - - return true; - } - - public static void signalEvent(String jsonEvent) { - - Log.d(TAG, "Signal event: " + jsonEvent); - Bundle replyData = new Bundle(); - replyData.putString("event", jsonEvent); - - Message replyMessage = Message.obtain(null, 0, 0, 0, null); - replyMessage.setData(replyData); - try { - applicationMessengerIsSet.await(); - sendReply(applicationMessenger, replyMessage); - } catch(InterruptedException e) { - Log.d(TAG, "Interrupted during event signalling."); - } - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return serviceMessenger.getBinder(); - } - - @Override - public void onCreate() { - super.onCreate(); - Log.d(TAG, "Status Service created!"); - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "Status Service stopped!"); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return Service.START_STICKY; - } - - private static void sendReply(Messenger messenger, Message message) { - try { - boolean ex = false; - if (messenger != null) { - ex = true; - } - Log.d(TAG, "before sendReply " + ex); - messenger.send(message); - } catch (Exception e) { - Log.e(TAG, "Exception sending message id: " + message.what, e); - } - } -} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusThreadPoolExecutor.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusThreadPoolExecutor.java deleted file mode 100644 index 7e7cadad2a7..00000000000 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusThreadPoolExecutor.java +++ /dev/null @@ -1,37 +0,0 @@ -package im.status.ethereum.module; - -import java.util.concurrent.*; - -public class StatusThreadPoolExecutor { - private static final int NUMBER_OF_CORES = - Runtime.getRuntime().availableProcessors(); - private static final int KEEP_ALIVE_TIME = 1; - private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; - - private final BlockingQueue mQueue; - private final ThreadPoolExecutor mThreadPool; - - private StatusThreadPoolExecutor() { - mQueue = new LinkedBlockingQueue<>(); - - mThreadPool = new ThreadPoolExecutor( - NUMBER_OF_CORES, - NUMBER_OF_CORES, - KEEP_ALIVE_TIME, - KEEP_ALIVE_TIME_UNIT, - mQueue); - } - - /** Pugh singleton */ - private static class Holder { - private static StatusThreadPoolExecutor instance = new StatusThreadPoolExecutor(); - } - - public static StatusThreadPoolExecutor getInstance() { - return Holder.instance; - } - - public void execute(final Runnable r) { - mThreadPool.execute(r); - } -} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusThreadPoolExecutor.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusThreadPoolExecutor.kt new file mode 100644 index 00000000000..684f142abf2 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusThreadPoolExecutor.kt @@ -0,0 +1,47 @@ +package im.status.ethereum.module + +import java.util.concurrent.* + +/** + * Uses an unbounded queue but allows timeout of core threads + * (modified case 2 in + * https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html ) + */ +class StatusThreadPoolExecutor private constructor() { + private val NUMBER_OF_CORES: Int = Runtime.getRuntime().availableProcessors() + private val THREADS_TO_CORES_RATIO: Int = 100 + private val KEEP_ALIVE_TIME: Int = 1 + private val KEEP_ALIVE_TIME_UNIT: TimeUnit = TimeUnit.SECONDS + + private val mQueue: BlockingQueue = LinkedBlockingQueue() + private val mThreadPool: ThreadPoolExecutor + + init { + mThreadPool = ThreadPoolExecutor( + THREADS_TO_CORES_RATIO * NUMBER_OF_CORES, + THREADS_TO_CORES_RATIO * NUMBER_OF_CORES, + KEEP_ALIVE_TIME.toLong(), + KEEP_ALIVE_TIME_UNIT, + mQueue + ) + + // Allow pool to drain + mThreadPool.allowCoreThreadTimeOut(true) + } + + /** Singleton holder */ + private object Holder { + val instance = StatusThreadPoolExecutor() + } + + companion object { + @JvmStatic + fun getInstance(): StatusThreadPoolExecutor { + return Holder.instance + } + } + + fun execute(r: Runnable) { + mThreadPool.execute(r) + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/UIHelper.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/UIHelper.kt new file mode 100644 index 00000000000..a661670901d --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/UIHelper.kt @@ -0,0 +1,97 @@ +package im.status.ethereum.module + +import android.app.Activity +import android.content.Context +import android.os.Build +import android.util.Log +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import android.webkit.CookieManager +import android.webkit.CookieSyncManager +import android.webkit.WebStorage +import android.webkit.WebView +import android.widget.EditText +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.uimanager.NativeViewHierarchyManager +import com.facebook.react.uimanager.UIBlock +import com.facebook.react.uimanager.UIManagerModule + +class UIHelper(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + companion object { + private const val TAG = "UIHelper" + } + + override fun getName(): String = "UIHelper" + + @ReactMethod + fun setSoftInputMode(mode: Int) { + Log.d(TAG, "setSoftInputMode") + getCurrentActivity()?.run { + runOnUiThread { + window.setSoftInputMode(mode) + } + } + } + + @Suppress("DEPRECATION") + @ReactMethod + fun clearCookies() { + Log.d(TAG, "clearCookies") + getCurrentActivity()?.let { activity -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + CookieManager.getInstance().apply { + removeAllCookies(null) + flush() + } + } else { + CookieSyncManager.createInstance(activity).also { cookieSyncManager -> + cookieSyncManager.startSync() + CookieManager.getInstance().apply { + removeAllCookie() + removeSessionCookie() + } + cookieSyncManager.stopSync() + cookieSyncManager.sync() + } + } + } + } + + @ReactMethod + fun toggleWebviewDebug(enabled: Boolean) { + Log.d(TAG, "toggleWebviewDebug") + getCurrentActivity()?.run { + runOnUiThread { + WebView.setWebContentsDebuggingEnabled(enabled) + } + } + } + + @ReactMethod + fun clearStorageAPIs() { + Log.d(TAG, "clearStorageAPIs") + getCurrentActivity()?.let { + WebStorage.getInstance()?.deleteAllData() + } + } + + @ReactMethod + fun resetKeyboardInputCursor(reactTagToReset: Int, selection: Int) { + reactApplicationContext.getNativeModule(UIManagerModule::class.java)?.addUIBlock(object : UIBlock { + override fun execute(nativeViewHierarchyManager: NativeViewHierarchyManager) { + (reactApplicationContext.baseContext.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)?.let { imm -> + val viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset) + imm.restartInput(viewToReset) + try { + (viewToReset as? EditText)?.setSelection(selection) + } catch (e: Exception) { + // Ignore exceptions during selection setting + } + } + } + }) + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/Utils.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/Utils.kt new file mode 100644 index 00000000000..dbba600a344 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/Utils.kt @@ -0,0 +1,219 @@ +package im.status.ethereum.module + +import android.content.Context +import android.os.Environment +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray; +import android.util.Log +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.util.function.Supplier +import statusgo.Statusgo + +class Utils(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + companion object { + private const val TAG = "Utils" + const val LOGS_DIRECTORY_NAME = "logs" + } + + override fun getName(): String { + return "Utils" + } + + fun getNoBackupDirectory(): String { + StatusBackendClient.getInstance()?.let { client -> + if (client.serverEnabled && client.rootDataDir != null) { + return client.rootDataDir!! + } + } + return reactContext.noBackupFilesDir.absolutePath + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun backupDisabledDataDir(): String { + return getNoBackupDirectory() + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun getMMKVStoragePath(): String { + return pathCombine(reactContext.noBackupFilesDir.absolutePath, "/mmkv") + } + + fun getLogDirectory(usePublicLogDir: Boolean): File? { + return if (usePublicLogDir) { + StatusBackendClient.getInstance()?.let { client -> + if (client.serverEnabled && client.rootDataDir != null) { + return File(client.rootDataDir!!, LOGS_DIRECTORY_NAME) + } + } + reactContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + } else { + File(getNoBackupDirectory(), LOGS_DIRECTORY_NAME) + } + } + + fun getKeyUID(json: String): String { + val jsonObj = JSONObject(json) + return jsonObj.getString("key-uid") + } + + fun pathCombine(path1: String, path2: String): String { + val file = File(path1, path2) + return file.absolutePath + } + + fun getKeyStorePath(keyUID: String): String { + val commonKeydir = pathCombine(getNoBackupDirectory(), "/keystore") + return pathCombine(commonKeydir, keyUID) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun keystoreDir(): String { + val absRootDirPath = getNoBackupDirectory() + return pathCombine(absRootDirPath, "keystore") + } + + fun migrateKeyStoreDir(accountData: String, password: String) { + try { + val commonKeydir = pathCombine(getNoBackupDirectory(), "/keystore") + val keydir = getKeyStorePath(getKeyUID(accountData)) + Log.d(TAG, "before migrateKeyStoreDir $keydir") + + val keydirFile = File(keydir) + if (!keydirFile.exists() || keydirFile.list().isEmpty()) { + Log.d(TAG, "migrateKeyStoreDir") + val jsonParams = JSONObject() + jsonParams.put("account", JSONObject(accountData)) // Remove 'new' keyword + jsonParams.put("password", password) + jsonParams.put("oldDir", commonKeydir) + jsonParams.put("newDir", keydir) + + StatusBackendClient.executeStatusGoRequest( + endpoint = "MigrateKeyStoreDirV2", + requestBody = jsonParams.toString(), + statusgoFunction = { Statusgo.migrateKeyStoreDirV2(jsonParams.toString()) } + ) + StatusBackendClient.executeStatusGoRequest( + endpoint = "InitKeystore", + requestBody = keydir, + statusgoFunction = { Statusgo.initKeystore(keydir) } + ) + } + } catch (e: JSONException) { + Log.e(TAG, "JSON conversion failed: ${e.message}") + } + } + + fun checkAvailability(): Boolean { + // We wait at least 10s for getCurrentActivity to return a value, + // otherwise we give up + for (attempts in 0 until 100) { + if (currentActivity != null) { + return true + } + try { + Thread.sleep(100) + } catch (ex: InterruptedException) { + if (currentActivity != null) { + return true + } + Log.d(TAG, "Activity doesn't exist") + return false + } + } + + Log.d(TAG, "Activity doesn't exist") + return false + } + + fun executeRunnableStatusGoMethod(method: Supplier, callback: Callback?) { + if (!checkAvailability()) { + callback?.invoke(false) + return + } + + val runnableTask = Runnable { + val res = method.get() + callback?.invoke(res) + } + + StatusThreadPoolExecutor.getInstance().execute(runnableTask) + } + + @ReactMethod + fun validateMnemonic(seed: String, callback: Callback) { + val jsonParams = JSONObject() + jsonParams.put("mnemonic", seed) + val jsonString = jsonParams.toString() + StatusBackendClient.executeStatusGoRequestWithCallback( + endpoint = "ValidateMnemonicV2", + requestBody = jsonString, + statusgoFunction = { Statusgo.validateMnemonicV2(jsonString) }, + callback + ) + } + + fun is24Hour(): Boolean { + return android.text.format.DateFormat.is24HourFormat(reactContext.applicationContext) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun checkAddressChecksum(address: String): String { + return StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "CheckAddressChecksum", + requestBody = address, + statusgoFunction = { Statusgo.checkAddressChecksum(address) } + ) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun isAddress(address: String): String { + return StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "IsAddress", + requestBody = address, + statusgoFunction = { Statusgo.isAddress(address) } + ) + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun toChecksumAddress(address: String): String { + return StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "ToChecksumAddress", + requestBody = address, + statusgoFunction = { Statusgo.toChecksumAddress(address) } + ) + } + + fun readableArrayToStringArray(r: ReadableArray): Array { + val length = r.size() + val strArray = Array(length) { "" } + + for (keyIndex in 0 until length) { + strArray[keyIndex] = r.getString(keyIndex) ?: "" + } + + return strArray + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun validateConnectionString(connectionString: String): String { + return StatusBackendClient.executeStatusGoRequestWithResult( + endpoint = "ValidateConnectionString", + requestBody = connectionString, + statusgoFunction = { Statusgo.validateConnectionString(connectionString) } + ) + } + + fun handleStatusGoResponse(response: String, source: String) { + if (response.startsWith("{\"error\":\"\"")) { + Log.d(TAG, "$source success: $response") + } else { + Log.e(TAG, "$source failed: $response") + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/ForegroundService.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/ForegroundService.kt new file mode 100644 index 00000000000..753a241940e --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/ForegroundService.kt @@ -0,0 +1,122 @@ +package im.status.ethereum.pushnotifications + +import android.content.Context +import android.content.Intent +import android.app.Service +import android.content.pm.ServiceInfo +import android.os.IBinder +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.provider.Settings +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import android.os.Build +import im.status.ethereum.module.R + +class ForegroundService : Service() { + companion object { + private const val CHANNEL_ID = "status-service" + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(i: Intent?, flags: Int, startId: Int): Int { + // NOTE: recent versions of Android require the service to display + // a sticky notification to inform the user that the service is running + val context = applicationContext + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + + // Create notification channel for Android O and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = context.getSystemService(NotificationManager::class.java) + val channel = NotificationChannel( + CHANNEL_ID, + context.resources.getString(R.string.status_service), + NotificationManager.IMPORTANCE_HIGH + ).apply { + setShowBadge(false) + } + notificationManager.createNotificationChannel(channel) + } + + // Initialize intent and notification text based on Android version + val notificationIntent = createNotificationIntent(context) + val notificationText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.resources.getString(R.string.tap_to_hide_notification) + } else { + context.resources.getString(R.string.keep_status_running) + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + notificationIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + val stopIntent = Intent(PushNotificationHelper.ACTION_TAP_STOP) + val stopPendingIntent = PendingIntent.getBroadcast( + context, + 0, + stopIntent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_stat_notify_status) + .setContentTitle(context.resources.getString(R.string.background_service_opened)) + .setContentText(notificationText) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setContentIntent(pendingIntent) + .setNumber(0) + .addAction( + R.drawable.ic_stat_notify_status, + context.resources.getString(R.string.stop), + stopPendingIntent + ) + .build() + + // the id of the foreground notification MUST NOT be 0 + if (Build.VERSION.SDK_INT >= 33) { + startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + } else { + startForeground(1, notification) + } + + return START_STICKY + } + + private fun createNotificationIntent(context: Context): Intent { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create intent that takes the user to the notification channel settings + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID) + } + } else { + // For older versions of android, intent takes the user to the Status app + val packageName = context.packageName + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + val className = launchIntent?.component?.className + + try { + val intentClass = Class.forName(className!!) + Intent(context, intentClass).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + action = Intent.ACTION_VIEW + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + } catch (e: ClassNotFoundException) { + e.printStackTrace() + // Return an empty intent as fallback + Intent() + } + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.kt new file mode 100644 index 00000000000..918e088eb47 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.kt @@ -0,0 +1,118 @@ +package im.status.ethereum.pushnotifications + +import android.app.Activity +import android.app.Application +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.util.Log +import androidx.annotation.NonNull +import androidx.core.app.NotificationManagerCompat +import com.facebook.react.bridge.* +import im.status.ethereum.pushnotifications.PushNotificationJsDelivery +import java.security.SecureRandom + +class PushNotification(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), ActivityEventListener { + + companion object { + const val LOG_TAG = "PushNotification" + } + + private val mRandomNumberGenerator = SecureRandom() + private val pushNotificationHelper: PushNotificationHelper + private val delivery: PushNotificationJsDelivery + private val reactContext: ReactApplicationContext = reactContext + private var started: Boolean = false + + init { + reactContext.addActivityEventListener(this) + val applicationContext = reactContext.applicationContext as Application + + val intentFilter = IntentFilter() + pushNotificationHelper = PushNotificationHelper(applicationContext, intentFilter) + + delivery = PushNotificationJsDelivery(reactContext) + } + + override fun getName(): String = "PushNotification" + + // Primary implementation of ActivityEventListener interface + override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { + // Call the helper method with the correct parameter order + handleActivityResult(requestCode, resultCode, data) + } + + // Private helper method for handling activity results + private fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + // Ignored, required to implement ActivityEventListener for RN 0.33 + } + + private fun getBundleFromIntent(intent: Intent): Bundle? { + return when { + intent.hasExtra("notification") -> intent.getBundleExtra("notification") + intent.hasExtra("google.message_id") -> Bundle().apply { + putBundle("data", intent.extras) + } + else -> null + }?.apply { + if (!getBoolean("foreground", false) && !containsKey("userInteraction")) { + putBoolean("userInteraction", true) + } + } + } + + override fun onNewIntent(intent: Intent) { + getBundleFromIntent(intent)?.let { bundle -> + delivery.notifyNotification(bundle) + } + } + + @ReactMethod + fun createChannel(channelInfo: ReadableMap, callback: Callback?) { + val created = pushNotificationHelper.createChannel(channelInfo) + callback?.invoke(created) + } + + @ReactMethod + fun presentLocalNotification(details: ReadableMap) { + if (!started) return + + Arguments.toBundle(details)?.apply { + // If notification ID is not provided by the user, generate one at random + if (getString("id") == null) { + putString("id", mRandomNumberGenerator.nextInt().toString()) + } + pushNotificationHelper.sendToNotificationCentre(this) + } + } + + @ReactMethod + fun clearMessageNotifications(conversationId: String) { + if (started) { + pushNotificationHelper.clearMessageNotifications(conversationId) + } + } + + @ReactMethod + fun clearAllMessageNotifications() { + pushNotificationHelper.clearAllMessageNotifications() + } + + @ReactMethod + fun enableNotifications() { + started = true + pushNotificationHelper.start() + } + + @ReactMethod + fun disableNotifications() { + if (started) { + started = false + pushNotificationHelper.stop() + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationActions.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationActions.kt new file mode 100644 index 00000000000..9bcbc3dae71 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationActions.kt @@ -0,0 +1,100 @@ +package im.status.ethereum.pushnotifications + +import android.os.Build +import android.app.Application +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.facebook.react.ReactApplication +import com.facebook.react.ReactInstanceManager +import com.facebook.react.bridge.ReactContext +import im.status.ethereum.pushnotifications.PushNotificationJsDelivery +import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG + +/** + * BroadcastReceiver responsible for handling push notification actions. + * This class processes notification interactions and manages their lifecycle. + */ +class PushNotificationActions : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val intentActionPrefix = "${context.packageName}.ACTION_" + + Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications") + + if (intent.action == null || !intent.action!!.startsWith(intentActionPrefix)) { + return + } + + val bundle = intent.getBundleExtra("notification") ?: return + + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val notificationID = bundle.getString("id")?.toInt() ?: return + + // Dismiss the notification popup. + val autoCancel = bundle.getBoolean("autoCancel", true) + if (autoCancel) { + bundle.getString("tag")?.let { tag -> + manager.cancel(tag, notificationID) + } ?: run { + manager.cancel(notificationID) + } + } + + // Notify the action. + val invokeApp = bundle.getBoolean("invokeApp", true) + + if (invokeApp) { + val intentFilter = IntentFilter() + val helper = PushNotificationHelper( + context.applicationContext as Application, + intentFilter + ) + helper.invokeApp(bundle) + } else { + // We need to run this on the main thread, as the React code assumes that is true. + // Namely, DevServerHelper constructs a Handler() without a Looper, which triggers: + // "Can't create handler inside thread that has not called Looper.prepare()" + Handler(Looper.getMainLooper()).post { + // Construct and load our normal React JS code bundle + val reactInstanceManager = (context.applicationContext as ReactApplication) + .reactNativeHost + .reactInstanceManager + + val currentContext = reactInstanceManager.currentReactContext + + when { + // If context exists, deliver notification immediately + currentContext != null -> { + PushNotificationJsDelivery(currentContext) + .notifyNotificationAction(bundle) + } + // Otherwise wait for construction, then send the notification + else -> { + val listener = object : ReactInstanceManager.ReactInstanceEventListener { + override fun onReactContextInitialized(reactContext: ReactContext) { + PushNotificationJsDelivery(reactContext) + .notifyNotificationAction(bundle) + reactInstanceManager.removeReactInstanceEventListener(this) + } + } + + // Add the listener and create context if needed + reactInstanceManager.addReactInstanceEventListener(listener) + if (!reactInstanceManager.hasStartedCreatingInitialContext()) { + // Construct it in the background + reactInstanceManager.createReactContextInBackground() + } + } + } + } + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.kt new file mode 100644 index 00000000000..2869f1be9cb --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.kt @@ -0,0 +1,750 @@ +// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/ +// java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase +package im.status.ethereum.pushnotifications + +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.app.AlarmManager +import android.app.Application +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.BroadcastReceiver +import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Color +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.RectF +import android.media.AudioAttributes +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.service.notification.StatusBarNotification +import android.util.Log +import android.util.Base64 +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import org.json.JSONArray +import org.json.JSONException +import im.status.ethereum.module.R +import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG + +class PushNotificationHelper(private val context: Application, private val intentFilter: IntentFilter) { + + companion object { + private const val DEFAULT_VIBRATION: Long = 300L + private const val CHANNEL_ID = "status-im-notifications" + const val ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION" + const val ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP" + } + + private val flag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_CANCEL_CURRENT + } + + private val notificationManager: NotificationManager by lazy { + context.getSystemService(NotificationManager::class.java) + } + + private val persons: MutableMap = mutableMapOf() + private val messageGroups: MutableMap = mutableMapOf() + + init { + registerBroadcastReceiver() + } + + private val notificationActionReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + ACTION_DELETE_NOTIFICATION -> { + intent.extras?.getString("im.status.ethereum.groupId")?.let { groupId -> + cleanGroup(groupId) + } + } + ACTION_TAP_STOP -> { + stop() + System.exit(0) + } + } + Log.e(LOG_TAG, "intent received: ${intent.action}") + } + } + + private fun registerBroadcastReceiver() { + intentFilter.apply { + addAction(ACTION_DELETE_NOTIFICATION) + addAction(ACTION_TAP_STOP) + } + context.registerReceiver(notificationActionReceiver, intentFilter, Context.RECEIVER_EXPORTED) + Log.e(LOG_TAG, "Broadcast Receiver registered") + } + + fun getOpenAppIntent(deepLink: String?): Intent? { + val packageName = context.packageName + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + val className = launchIntent?.component?.className ?: return null + + return try { + val intentClass = Class.forName(className) + Intent(context, intentClass).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + action = Intent.ACTION_VIEW + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + deepLink?.let { data = Uri.parse(it) } + } + } catch (e: ClassNotFoundException) { + e.printStackTrace() + null + } + } + + fun invokeApp(bundle: Bundle?) { + val packageName = context.packageName + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + val className = launchIntent?.component?.className ?: return + + try { + val activityClass = Class.forName(className) + Intent(context, activityClass).apply { + bundle?.let { putExtra("notification", it) } + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(this) + } + } catch (e: ClassNotFoundException) { + Log.e(LOG_TAG, "Class not found", e) + } + } + + fun getMainActivityClass(): Class<*>? { + val packageName = context.packageName + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + val className = launchIntent?.component?.className ?: return null + + return try { + Class.forName(className) + } catch (e: ClassNotFoundException) { + e.printStackTrace() + null + } + } + + fun isApplicationInForeground(): Boolean { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val processInfos = activityManager.runningAppProcesses ?: return false + + return processInfos.any { processInfo -> + processInfo.processName == context.packageName && + processInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && + processInfo.pkgList.isNotEmpty() + } + } + + private fun getCircleBitmap(bitmap: Bitmap): Bitmap { + val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + val paint = Paint().apply { + isAntiAlias = true + color = Color.RED + } + val rect = Rect(0, 0, bitmap.width, bitmap.height) + val rectF = RectF(rect) + + canvas.drawARGB(0, 0, 0, 0) + canvas.drawOval(rectF, paint) + + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + canvas.drawBitmap(bitmap, rect, rect, paint) + + bitmap.recycle() + return output + } + + // Represents a message in a Status conversation + data class StatusMessage( + val id: String, + val author: Person, + val timestamp: Long, + val text: String + ) + + // Represents a group of messages in a Status conversation + inner class StatusMessageGroup(val id: String) { + private val _messages = mutableListOf() + val messages: List get() = _messages.toList() + + fun addMessage(message: StatusMessage) { + _messages.add(message) + } + + fun removeMessage(id: String) { + _messages.removeAll { it.id == id } + } + } + + private fun getPerson(bundle: Bundle): Person { + return Person.Builder() + .setName(bundle.getString("name")) + .build() + } + + private fun createMessage(data: Bundle): StatusMessage { + val author = getPerson(data.getBundle("notificationAuthor") ?: Bundle()) + val timeStampLongValue = data.getDouble("timestamp").toLong() + return StatusMessage( + id = data.getString("id") ?: "", + author = author, + timestamp = timeStampLongValue, + text = data.getString("message") ?: "" + ) + } + + private fun createGroupOnDismissedIntent( + context: Context, + notificationId: Int, + groupId: String, + deepLink: String? + ): PendingIntent { + return Intent(ACTION_DELETE_NOTIFICATION).apply { + putExtra("im.status.ethereum.deepLink", deepLink) + putExtra("im.status.ethereum.groupId", groupId) + }.let { intent -> + PendingIntent.getBroadcast( + context.applicationContext, + notificationId, + intent, + flag + ) + } + } + + private fun createGroupOnTapIntent( + context: Context, + notificationId: Int, + groupId: String, + deepLink: String? + ): PendingIntent { + return getOpenAppIntent(deepLink)?.let { intent -> + PendingIntent.getActivity( + context.applicationContext, + notificationId, + intent, + flag + ) + } ?: throw IllegalStateException("Could not create open app intent") + } + + private fun createOnTapIntent( + context: Context, + notificationId: Int, + deepLink: String? + ): PendingIntent { + return getOpenAppIntent(deepLink)?.let { intent -> + PendingIntent.getActivity( + context.applicationContext, + notificationId, + intent, + flag + ) + } ?: throw IllegalStateException("Could not create open app intent") + } + + private fun createOnDismissedIntent( + context: Context, + notificationId: Int, + deepLink: String? + ): PendingIntent { + return Intent(ACTION_DELETE_NOTIFICATION).apply { + putExtra("im.status.ethereum.deepLink", deepLink) + }.let { intent -> + PendingIntent.getBroadcast( + context.applicationContext, + notificationId, + intent, + flag + ) + } + } + + fun removeStatusMessage(bundle: Bundle) { + val conversationId = bundle.getString("conversationId") ?: return + val group = messageGroups.getOrPut(conversationId) { + StatusMessageGroup(conversationId) + } + + bundle.getString("id")?.let { id -> + group.removeMessage(id) + } + + showMessages(bundle) + } + + fun getMessageGroup(conversationId: String): StatusMessageGroup? { + return messageGroups[conversationId] + } + + fun addStatusMessage(bundle: Bundle) { + val conversationId = bundle.getString("conversationId") ?: return + val group = messageGroups.getOrPut(conversationId) { + StatusMessageGroup(conversationId) + } + + group.addMessage(createMessage(bundle)) + showMessages(bundle) + } + + fun showMessages(bundle: Bundle) { + val conversationId = bundle.getString("conversationId") ?: return + val group = messageGroups[conversationId] ?: return + + val messagingStyle = NotificationCompat.MessagingStyle("Me") + + // If there are no messages, cancel the notification and return + if (group.messages.isEmpty()) { + notificationManager.cancel(conversationId.hashCode()) + return + } + + group.messages.forEach { message -> + messagingStyle.addMessage( + message.text, + message.timestamp, + message.author + ) + } + + // Set conversation title if available + bundle.getString("title")?.let { title -> + messagingStyle.conversationTitle = title + } + + val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply { + setSmallIcon(R.drawable.ic_stat_notify_status) + setPriority(NotificationCompat.PRIORITY_HIGH) + setCategory(NotificationCompat.CATEGORY_MESSAGE) + setStyle(messagingStyle) + setGroup(conversationId) + setOnlyAlertOnce(true) + setGroupSummary(true) + setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink"))) + setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink"))) + setNumber(group.messages.size + 1) + setAutoCancel(true) + + // Set empty vibration pattern for Android 5.0 and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setVibrate(LongArray(0)) + } + } + + // Show the notification + notificationManager.notify(conversationId.hashCode(), builder.build()) + } + + fun checkOrCreateChannel( + manager: NotificationManager?, + channelId: String, + channelName: String?, + channelDescription: String?, + soundUri: Uri?, + importance: Int, + vibratePattern: LongArray?, + showBadge: Boolean + ): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || manager == null) { + return false + } + + val existingChannel = manager.getNotificationChannel(channelId) + val shouldCreateOrUpdate = when { + // Channel doesn't exist and we have required info + existingChannel == null && channelName != null && channelDescription != null -> true + + // Channel exists but needs update + existingChannel != null && ( + (channelName != null && existingChannel.name != channelName) || + (channelDescription != null && existingChannel.description != channelDescription) + ) -> true + + else -> false + } + + if (shouldCreateOrUpdate) { + NotificationChannel(channelId, channelName, importance).apply { + description = channelDescription + enableLights(true) + enableVibration(vibratePattern != null) + vibrationPattern = vibratePattern + setShowBadge(showBadge) + + // Configure sound + if (soundUri != null) { + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build() + setSound(soundUri, audioAttributes) + } else { + setSound(null, null) + } + + manager.createNotificationChannel(this) + } + return true + } + + return false + } + + fun createChannel(channelInfo: ReadableMap): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return false + } + + return checkOrCreateChannel( + manager = notificationManager, + channelId = channelInfo.getString("channelId") ?: return false, + channelName = channelInfo.getString("channelName"), + channelDescription = if (channelInfo.hasKey("channelDescription")) { + channelInfo.getString("channelDescription") + } else "", + soundUri = getSoundUri( + if (channelInfo.hasKey("soundName")) { + channelInfo.getString("soundName") + } else "default" + ), + importance = if (channelInfo.hasKey("importance")) { + channelInfo.getInt("importance") + } else 4, + vibratePattern = if (channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate")) { + longArrayOf(DEFAULT_VIBRATION) + } else null, + showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge") + ) + } + + fun getNotificationDefaultChannelId(): String = CHANNEL_ID + + private fun getSoundUri(soundName: String?): Uri { + return when { + soundName == null || soundName.equals("default", ignoreCase = true) -> { + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } + else -> { + val resources = context.resources + val packageName = context.packageName + val resId = when { + // Try to find the resource as is + resources.getIdentifier(soundName, "raw", packageName) != 0 -> { + resources.getIdentifier(soundName, "raw", packageName) + } + // Try without file extension + else -> { + val nameWithoutExtension = soundName.substringBeforeLast('.') + resources.getIdentifier(nameWithoutExtension, "raw", packageName) + } + } + Uri.parse("android.resource://$packageName/$resId") + } + } + } + +private fun removeGroup(groupId: String) { + messageGroups.remove(groupId) + } + + private fun cleanGroup(groupId: String) { + removeGroup(groupId) + if (messageGroups.isEmpty()) { + notificationManager.cancelAll() + } + } + + fun start() { + Log.e(LOG_TAG, "Starting Foreground Service") + Intent(context, ForegroundService::class.java).also { serviceIntent -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent) + } else { + context.startService(serviceIntent) + } + } + registerBroadcastReceiver() + } + + fun stop() { + Log.e(LOG_TAG, "Stopping Foreground Service") + // Cancel all current notifications since their intents won't be valid + // after the broadcast receiver is killed + notificationManager.cancelAll() + + Intent(context, ForegroundService::class.java).also { serviceIntent -> + context.stopService(serviceIntent) + } + context.unregisterReceiver(notificationActionReceiver) + } + + fun sendToNotificationCentre(bundle: Bundle) { + val aggregator = PushNotificationPicturesAggregator { largeIconImage, bigPictureImage -> + sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage) + } + + aggregator.apply { + setLargeIconUrl(context, bundle.getString("largeIconUrl")) + setBigPictureUrl(context, bundle.getString("bigPictureUrl")) + } + } + + fun handleConversation(bundle: Bundle) { + if (bundle.getBoolean("deleted")) { + removeStatusMessage(bundle) + } else { + addStatusMessage(bundle) + } + } + + fun clearMessageNotifications(conversationId: String) { + notificationManager.cancel(conversationId.hashCode()) + cleanGroup(conversationId) + } + + fun clearAllMessageNotifications() { + notificationManager.cancelAll() + } + + fun sendToNotificationCentreWithPicture( + bundle: Bundle, + largeIconBitmap: Bitmap?, + bigPictureBitmap: Bitmap? + ) { + try { + val intentClass = getMainActivityClass() ?: run { + Log.e(LOG_TAG, "No activity class found for the notification") + return + } + + if (bundle.getBoolean("isConversation")) { + handleConversation(bundle) + return + } + + if (bundle.getString("message") == null) { + Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. " + + "Cannot send to notification centre because there is no 'message' field in: $bundle") + return + } + + val notificationIdString = bundle.getString("id") ?: run { + Log.e(LOG_TAG, "No notification ID specified for the notification") + return + } + + val resources = context.resources + val packageName = context.packageName + + val title = bundle.getString("title") ?: run { + val appInfo = context.applicationInfo + context.packageManager.getApplicationLabel(appInfo).toString() + } + + val priority = when (bundle.getString("priority")?.toLowerCase()) { + "max" -> NotificationCompat.PRIORITY_MAX + "high" -> NotificationCompat.PRIORITY_HIGH + "low" -> NotificationCompat.PRIORITY_LOW + "min" -> NotificationCompat.PRIORITY_MIN + "default" -> NotificationCompat.PRIORITY_DEFAULT + else -> NotificationCompat.PRIORITY_HIGH + } + + val visibility = when (bundle.getString("visibility")?.toLowerCase()) { + "private" -> NotificationCompat.VISIBILITY_PRIVATE + "public" -> NotificationCompat.VISIBILITY_PUBLIC + "secret" -> NotificationCompat.VISIBILITY_SECRET + else -> NotificationCompat.VISIBILITY_PRIVATE + } + + val channelId = bundle.getString("channelId") ?: getNotificationDefaultChannelId() + + NotificationCompat.Builder(context, channelId).apply { + setContentTitle(title) + setTicker(bundle.getString("ticker")) + setVisibility(visibility) + setPriority(priority) + setAutoCancel(bundle.getBoolean("autoCancel", true)) + setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false)) + + // Configure notification based on Android version + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + setShowWhen(bundle.getBoolean("showWhen", true)) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setDefaults(Notification.DEFAULT_LIGHTS) + } + + // Handle group settings for Android KitKat Watch and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + bundle.getString("group")?.let { group -> + setGroup(group) + } + + if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) { + setGroupSummary(bundle.getBoolean("groupSummary")) + } + } + + // Set notification number if provided + bundle.getString("number")?.toIntOrNull()?.let { number -> + setNumber(number) + } + + val smallIconResId = findSmallIconResourceId(resources, packageName, bundle) + setSmallIcon(smallIconResId) + + configureLargeIcon(this, largeIconBitmap, bundle, resources, packageName) + + val message = bundle.getString("message") ?: "" + setContentText(message) + + bundle.getString("subText")?.let { subText -> + setSubText(subText) + } + + val bigText = bundle.getString("bigText") ?: message + val style = when { + bigPictureBitmap != null -> NotificationCompat.BigPictureStyle() + .bigPicture(bigPictureBitmap) + .setBigContentTitle(title) + .setSummaryText(message) + else -> NotificationCompat.BigTextStyle().bigText(bigText) + } + setStyle(style) + + val notificationId = notificationIdString.hashCode() + setContentIntent(createOnTapIntent(context, notificationId, bundle.getString("deepLink"))) + setDeleteIntent(createOnDismissedIntent(context, notificationId, bundle.getString("deepLink"))) + + configureSoundAndVibration(this, bundle) + + setOngoing(bundle.getBoolean("ongoing", false)) + + // Handle color for Lollipop and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setCategory(NotificationCompat.CATEGORY_CALL) + bundle.getString("color")?.let { color -> + setColor(Color.parseColor(color)) + } + } + + // Finally show the notification if conditions are met + if (!(isApplicationInForeground() && bundle.getBoolean("ignoreInForeground"))) { + val notification = build() + notification.defaults = notification.defaults or Notification.DEFAULT_LIGHTS + + if (bundle.containsKey("tag")) { + notificationManager.notify(bundle.getString("tag"), notificationId, notification) + } else { + notificationManager.notify(notificationId, notification) + } + } + } + + } catch (e: Exception) { + Log.e(LOG_TAG, "Failed to send push notification", e) + } + } + + private fun findSmallIconResourceId(resources: Resources, packageName: String, bundle: Bundle): Int { + val smallIcon = bundle.getString("smallIcon") + return when { + !smallIcon.isNullOrEmpty() -> + resources.getIdentifier(smallIcon, "mipmap", packageName) + smallIcon == null -> + resources.getIdentifier("ic_stat_notify_status", "drawable", packageName) + else -> { + val defaultIcon = resources.getIdentifier("ic_launcher", "mipmap", packageName) + defaultIcon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info + } + } + } + + private fun configureLargeIcon( + builder: NotificationCompat.Builder, + largeIconBitmap: Bitmap?, + bundle: Bundle, + resources: Resources, + packageName: String + ) { + var finalLargeIcon = largeIconBitmap + + if (finalLargeIcon == null) { + val largeIcon = bundle.getString("largeIcon") + val largeIconResId = when { + !largeIcon.isNullOrEmpty() -> + resources.getIdentifier(largeIcon, "mipmap", packageName) + largeIcon == null -> + resources.getIdentifier("ic_launcher", "mipmap", packageName) + else -> 0 + } + + if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) { + finalLargeIcon = BitmapFactory.decodeResource(resources, largeIconResId) + } + } + + bundle.getBundle("notificationAuthor")?.getString("icon")?.let { icon -> + val base64Image = icon.split(",")[1] + val decodedString = Base64.decode(base64Image, Base64.DEFAULT) + val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size) + builder.setLargeIcon(getCircleBitmap(decodedByte)) + } ?: finalLargeIcon?.let { icon -> + builder.setLargeIcon(icon) + } + } + + private fun configureSoundAndVibration( + builder: NotificationCompat.Builder, + bundle: Bundle + ) { + var soundUri: Uri? = null + + if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) { + val soundName = bundle.getString("soundName") ?: "default" + soundUri = getSoundUri(soundName) + builder.setSound(soundUri) + } + + if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setSound(null) + } + + if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) { + val vibration = if (bundle.containsKey("vibration")) { + bundle.getDouble("vibration").toLong() + } else DEFAULT_VIBRATION + + val finalVibration = if (vibration == 0L) DEFAULT_VIBRATION else vibration + builder.setVibrate(longArrayOf(0, finalVibration)) + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationJsDelivery.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationJsDelivery.kt new file mode 100644 index 00000000000..cb22cf4c3ff --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationJsDelivery.kt @@ -0,0 +1,93 @@ +package im.status.ethereum.pushnotifications + +import android.os.Build +import android.app.Application +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.modules.core.DeviceEventManagerModule +import org.json.JSONException +import org.json.JSONObject +import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG + +class PushNotificationJsDelivery( + private val reactContext: ReactContext +) { + /** + * Converts a Bundle to its JSON string representation. + * Returns null if conversion fails. + */ + fun convertJSON(bundle: Bundle): String? { + return try { + convertJSONObject(bundle).toString() + } catch (e: JSONException) { + null + } + } + + /** + * Converts a Bundle to a JSONObject, handling nested bundles and different value types. + * This explicit conversion is necessary because Bundle is not a standard map. + */ + @Throws(JSONException::class) + private fun convertJSONObject(bundle: Bundle): JSONObject { + return JSONObject().apply { + // Using Kotlin's for loop syntax and 'apply' scope function + bundle.keySet().forEach { key -> + bundle.get(key)?.let { value -> + when (value) { + is Bundle -> put(key, convertJSONObject(value)) + else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + put(key, JSONObject.wrap(value)) + } else { + put(key, value) + } + } + } + } + } + } + + /** + * Notifies about a received remote notification by sending an event to JavaScript + */ + fun notifyNotification(bundle: Bundle) { + convertJSON(bundle)?.let { bundleString -> + Arguments.createMap().apply { + putString("dataJSON", bundleString) + sendEvent("remoteNotificationReceived", this) + } + } + } + + /** + * Notifies about a notification action by sending an event to JavaScript + */ + fun notifyNotificationAction(bundle: Bundle) { + convertJSON(bundle)?.let { bundleString -> + Arguments.createMap().apply { + putString("dataJSON", bundleString) + sendEvent("notificationActionReceived", this) + } + } + } + + /** + * Sends an event to JavaScript if there's an active Catalyst instance + */ + fun sendEvent(eventName: String, params: Any) { + if (reactContext.hasActiveCatalystInstance()) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(eventName, params) + } + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPackage.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPackage.kt new file mode 100644 index 00000000000..cd97ca09ff1 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPackage.kt @@ -0,0 +1,24 @@ +package im.status.ethereum.pushnotifications + +import im.status.ethereum.pushnotifications.PushNotification +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.JavaScriptModule +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class PushNotificationPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(PushNotification(reactContext)) + } + + // Note: This function is deprecated in newer versions of React Native + // but kept for backwards compatibility + fun createJSModules(): List> { + return emptyList() + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPicturesAggregator.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPicturesAggregator.kt new file mode 100644 index 00000000000..f2a46b90c21 --- /dev/null +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPicturesAggregator.kt @@ -0,0 +1,109 @@ +package im.status.ethereum.pushnotifications + +import androidx.annotation.Nullable +import com.facebook.common.executors.CallerThreadExecutor +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSource +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.common.Priority +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.request.ImageRequest +import com.facebook.imagepipeline.request.ImageRequestBuilder +import android.util.Log +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import java.util.concurrent.atomic.AtomicInteger +import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG + +class PushNotificationPicturesAggregator( + private val callback: (Bitmap?, Bitmap?) -> Unit +) { + private val count = AtomicInteger(0) + private var largeIconImage: Bitmap? = null + private var bigPictureImage: Bitmap? = null + + fun setBigPicture(bitmap: Bitmap?) { + bigPictureImage = bitmap + finished() + } + + fun setBigPictureUrl(context: Context, url: String?) { + if (url == null) { + setBigPicture(null) + return + } + + val uri = try { + Uri.parse(url) + } catch (ex: Exception) { + Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex) + setBigPicture(null) + return + } + + downloadRequest(context, uri, object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(@Nullable bitmap: Bitmap?) { + setBigPicture(bitmap) + } + + override fun onFailureImpl(dataSource: DataSource>) { + setBigPicture(null) + } + }) + } + + fun setLargeIcon(bitmap: Bitmap?) { + largeIconImage = bitmap + finished() + } + + fun setLargeIconUrl(context: Context, url: String?) { + if (url == null) { + setLargeIcon(null) + return + } + + val uri = try { + Uri.parse(url) + } catch (ex: Exception) { + Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex) + setLargeIcon(null) + return + } + + downloadRequest(context, uri, object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(@Nullable bitmap: Bitmap?) { + setLargeIcon(bitmap) + } + + override fun onFailureImpl(dataSource: DataSource>) { + setLargeIcon(null) + } + }) + } + + private fun downloadRequest(context: Context, uri: Uri, subscriber: BaseBitmapDataSubscriber) { + val imageRequest = ImageRequestBuilder + .newBuilderWithSource(uri) + .setRequestPriority(Priority.HIGH) + .setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH) + .build() + + if (!Fresco.hasBeenInitialized()) { + Fresco.initialize(context) + } + + val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context) + dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance()) + } + + private fun finished() { + synchronized(count) { + if (count.incrementAndGet() >= 2) { + callback(largeIconImage, bigPictureImage) + } + } + } +} diff --git a/modules/react-native-status/android/src/main/res/drawable-hdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-hdpi/ic_stat_notify_status.png new file mode 100644 index 00000000000..22fdbc41f4f Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-hdpi/ic_stat_notify_status.png differ diff --git a/modules/react-native-status/android/src/main/res/drawable-mdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-mdpi/ic_stat_notify_status.png new file mode 100644 index 00000000000..c75e35988f3 Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-mdpi/ic_stat_notify_status.png differ diff --git a/modules/react-native-status/android/src/main/res/drawable-xhdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-xhdpi/ic_stat_notify_status.png new file mode 100644 index 00000000000..2785bee4bae Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-xhdpi/ic_stat_notify_status.png differ diff --git a/modules/react-native-status/android/src/main/res/drawable-xxhdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-xxhdpi/ic_stat_notify_status.png new file mode 100644 index 00000000000..39ae7d94ad1 Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-xxhdpi/ic_stat_notify_status.png differ diff --git a/modules/react-native-status/android/src/main/res/drawable-xxxhdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-xxxhdpi/ic_stat_notify_status.png new file mode 100644 index 00000000000..94d30f43d43 Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-xxxhdpi/ic_stat_notify_status.png differ diff --git a/modules/react-native-status/android/src/main/res/drawable/notification_icon.png b/modules/react-native-status/android/src/main/res/drawable/notification_icon.png new file mode 100644 index 00000000000..22fdbc41f4f Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable/notification_icon.png differ diff --git a/modules/react-native-status/android/src/main/res/raw/notification_sound.mp3 b/modules/react-native-status/android/src/main/res/raw/notification_sound.mp3 new file mode 100644 index 00000000000..81b702fd43a Binary files /dev/null and b/modules/react-native-status/android/src/main/res/raw/notification_sound.mp3 differ diff --git a/modules/react-native-status/android/src/main/res/values/strings.xml b/modules/react-native-status/android/src/main/res/values/strings.xml new file mode 100644 index 00000000000..4b5de7c7805 --- /dev/null +++ b/modules/react-native-status/android/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + Status Background Service + STOP + Tap to hide this notification permanently + Keep Status running to receive notifications + Background service for notifications is running + Get notifications on new messages and mentions + + diff --git a/modules/react-native-status/android/src/main/res/xml/provider_paths.xml b/modules/react-native-status/android/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000000..97384e12915 --- /dev/null +++ b/modules/react-native-status/android/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/react-native-status/android/src/test/java/android/util/Log.kt b/modules/react-native-status/android/src/test/java/android/util/Log.kt new file mode 100644 index 00000000000..d44e6732f8e --- /dev/null +++ b/modules/react-native-status/android/src/test/java/android/util/Log.kt @@ -0,0 +1,27 @@ +package android.util + +object Log { + @JvmStatic + fun d(tag: String, msg: String): Int { + println("DEBUG: $tag: $msg") + return 0 + } + + @JvmStatic + fun i(tag: String, msg: String): Int { + println("INFO: $tag: $msg") + return 0 + } + + @JvmStatic + fun w(tag: String, msg: String): Int { + println("WARN: $tag: $msg") + return 0 + } + + @JvmStatic + fun e(tag: String, msg: String): Int { + println("ERROR: $tag: $msg") + return 0 + } +} diff --git a/modules/react-native-status/android/src/test/java/im/status/ethereum/pushnotifications/PushNotificationHelperTest.kt b/modules/react-native-status/android/src/test/java/im/status/ethereum/pushnotifications/PushNotificationHelperTest.kt new file mode 100644 index 00000000000..5477ab27955 --- /dev/null +++ b/modules/react-native-status/android/src/test/java/im/status/ethereum/pushnotifications/PushNotificationHelperTest.kt @@ -0,0 +1,49 @@ +package im.status.ethereum.pushnotifications + +import android.app.Application +import android.app.NotificationManager +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import org.junit.Assert.assertNotNull +import org.junit.Test +import org.mockito.Mockito.* +import org.mockito.Spy + +class PushNotificationHelperTest { + + private lateinit var helper: PushNotificationHelper + + @Test + fun testAddStatusMessage() { + val context = mock(Application::class.java) + val manager = mock(NotificationManager::class.java) + val filter = mock(IntentFilter::class.java) + + `when`(context.getSystemService(NotificationManager::class.java)).thenReturn(manager) + + // Create a spy for PushNotificationHelper + val realHelper = PushNotificationHelper(context, filter) + helper = spy(realHelper) + + val bundle = mock(Bundle::class.java) + val personBundle = mock(Bundle::class.java) + + val conversationId = "conversation-id" + val deepLink = "deep-link" + + `when`(bundle.getString("conversationId")).thenReturn(conversationId) + `when`(bundle.getString("id")).thenReturn("id") + `when`(bundle.getString("message")).thenReturn("message") + `when`(bundle.getDouble("timestamp")).thenReturn(100000.0) + `when`(bundle.getString("deepLink")).thenReturn(deepLink) + + `when`(personBundle.getString("name")).thenReturn("name") + `when`(bundle.getBundle("notificationAuthor")).thenReturn(personBundle) + + doNothing().`when`(helper).showMessages(bundle) + + helper.addStatusMessage(bundle) + assertNotNull(helper.getMessageGroup(conversationId)) + } +} diff --git a/modules/react-native-status/desktop/CMakeLists.txt b/modules/react-native-status/desktop/CMakeLists.txt deleted file mode 100755 index 66d328e04ce..00000000000 --- a/modules/react-native-status/desktop/CMakeLists.txt +++ /dev/null @@ -1,64 +0,0 @@ -set(CMAKE_INCLUDE_CURRENT_DIR ON) - -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}) -find_package(Go REQUIRED) - -set(REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_TYPE_NAMES ${REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_TYPE_NAMES} - \"RCTStatus\" PARENT_SCOPE) - -set(REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_SRC ${REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_SRC} - ${CMAKE_CURRENT_SOURCE_DIR}/rctstatus.cpp PARENT_SCOPE) - -include(${CMAKE_ROOT}/Modules/ExternalProject.cmake) - -if (WIN32 AND NOT CUSTOM_STATUSGO_BUILD_DIR_PATH) - set(CUSTOM_STATUSGO_BUILD_DIR_PATH "C:/srd-build/StatusGo") -endif() -if (CUSTOM_STATUSGO_BUILD_DIR_PATH) - set(StatusGo_ROOT ${CUSTOM_STATUSGO_BUILD_DIR_PATH}) -else() - set(StatusGo_ROOT "${CMAKE_CURRENT_BINARY_DIR}/StatusGo") -endif() -set(StatusGo_PREFIX "${StatusGo_ROOT}/src/github.com/status-im") -set(StatusGo_SOURCE_DIR "${StatusGo_PREFIX}/status-go") -set(StatusGo_INCLUDE_DIR "${StatusGo_SOURCE_DIR}/build/bin") -set(StatusGo_STATIC_LIB - "${StatusGo_SOURCE_DIR}/build/bin/${CMAKE_STATIC_LIBRARY_PREFIX}status${CMAKE_STATIC_LIBRARY_SUFFIX}") - -include_directories(${StatusGo_INCLUDE_DIR}) - -if (WIN32) - set(CONFIGURE_SCRIPT build-status-go.bat) -else() - set(CONFIGURE_SCRIPT build-status-go.sh) -endif() - -ExternalProject_Add(StatusGo_ep - PREFIX ${StatusGo_PREFIX} - SOURCE_DIR ${StatusGo_SOURCE_DIR} - GIT_REPOSITORY https://github.com/status-im/status-go.git - GIT_TAG 03bf6e37 - BUILD_BYPRODUCTS ${StatusGo_STATIC_LIB} - CONFIGURE_COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/${CONFIGURE_SCRIPT} ${GO_ROOT_PATH} ${StatusGo_ROOT} ${StatusGo_SOURCE_DIR} - BUILD_COMMAND "" - INSTALL_COMMAND "" -) - -set(REACT_NATIVE_DESKTOP_EXTERNAL_PROJECT_DEPS ${REACT_NATIVE_DESKTOP_EXTERNAL_PROJECT_DEPS} StatusGo_ep PARENT_SCOPE) - -if (APPLE) - set(STATUSGO_DEPS_LIBS "-framework Foundation" - "-framework CoreServices" - "-framework IOKit" - "-framework Security" pthread) -elseif (WIN32) - set(STATUSGO_DEPS_LIBS -lWinMM -lWS2_32 -lsetupapi) -else() - set(STATUSGO_DEPS_LIBS pthread) -endif() - -set(REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_LIBS ${REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_LIBS} - ${StatusGo_STATIC_LIB} ${STATUSGO_DEPS_LIBS} PARENT_SCOPE) - -set(REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_INCLUDE_DIRS ${REACT_NATIVE_DESKTOP_EXTERNAL_MODULES_INCLUDE_DIRS} - ${StatusGo_INCLUDE_DIR} PARENT_SCOPE) diff --git a/modules/react-native-status/desktop/FindGo.cmake b/modules/react-native-status/desktop/FindGo.cmake deleted file mode 100644 index 96de42dd882..00000000000 --- a/modules/react-native-status/desktop/FindGo.cmake +++ /dev/null @@ -1,35 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -# The module defines the following variables: -# GO_FOUND - true if the Go was found -# GO_EXECUTABLE - path to the executable -# GO_VERSION - Go version number -# GO_PLATFORM - i.e. linux -# GO_ARCH - i.e. amd64 -# Example usage: -# find_package(Go 1.2 REQUIRED) - - -find_program(GO_EXECUTABLE go PATHS ENV GOROOT GOPATH GOBIN PATH_SUFFIXES bin) -if (GO_EXECUTABLE) - get_filename_component(GO_ROOT_PATH ${GO_EXECUTABLE} REALPATH) - get_filename_component(GO_ROOT_PATH ${GO_ROOT_PATH}/../.. REALPATH) - message(STATUS "GO_ROOT_PATH is set to: ${GO_ROOT_PATH}") - execute_process(COMMAND ${GO_EXECUTABLE} version OUTPUT_VARIABLE GO_VERSION_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE) - if(GO_VERSION_OUTPUT MATCHES "go([0-9]+\\.[0-9]+\\.?[0-9]*)[a-zA-Z0-9]* ([^/]+)/(.*)") - set(GO_VERSION ${CMAKE_MATCH_1}) - set(GO_PLATFORM ${CMAKE_MATCH_2}) - set(GO_ARCH ${CMAKE_MATCH_3}) - elseif(GO_VERSION_OUTPUT MATCHES "go version devel .* ([^/]+)/(.*)$") - set(GO_VERSION "99-devel") - set(GO_PLATFORM ${CMAKE_MATCH_1}) - set(GO_ARCH ${CMAKE_MATCH_2}) - message("WARNING: Development version of Go being used, can't determine compatibility.") - endif() -endif() -mark_as_advanced(GO_EXECUTABLE) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Go REQUIRED_VARS GO_EXECUTABLE GO_VERSION GO_PLATFORM GO_ARCH VERSION_VAR GO_VERSION) \ No newline at end of file diff --git a/modules/react-native-status/desktop/build-status-go.bat b/modules/react-native-status/desktop/build-status-go.bat deleted file mode 100755 index e48e8d7a50f..00000000000 --- a/modules/react-native-status/desktop/build-status-go.bat +++ /dev/null @@ -1,8 +0,0 @@ - set GOROOT=%1 - set GOPATH=%2 - set PATH=%GOROOT%/bin;%GOROOT%;%GOPATH%;%PATH% - - cd %3/lib - go get . - cd .. - mingw32-make statusgo-library \ No newline at end of file diff --git a/modules/react-native-status/desktop/build-status-go.sh b/modules/react-native-status/desktop/build-status-go.sh deleted file mode 100755 index 10bec83affc..00000000000 --- a/modules/react-native-status/desktop/build-status-go.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -export GOROOT=$1 -export GOPATH=$2 -export PATH=$GOROOT/bin:$GOROOT:$GOPATH:$PATH - -cd $3/lib -go get ./ -cd .. -make statusgo-library \ No newline at end of file diff --git a/modules/react-native-status/desktop/rctstatus.cpp b/modules/react-native-status/desktop/rctstatus.cpp deleted file mode 100644 index 30c880736b0..00000000000 --- a/modules/react-native-status/desktop/rctstatus.cpp +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Copyright (c) 2017-present, Status Research and Development GmbH. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - */ - -#include "rctstatus.h" -#include "bridge.h" -#include "eventdispatcher.h" - -#include -#include -#include -#include -#include -#include - -#include "libstatus.h" - -namespace { -struct RegisterQMLMetaType { - RegisterQMLMetaType() { - qRegisterMetaType(); - } -} registerMetaType; -} // namespace - -class RCTStatusPrivate { -public: - static Bridge* bridge; - static RCTStatus* rctStatus; -}; - -Bridge* RCTStatusPrivate::bridge = nullptr; -RCTStatus* RCTStatusPrivate::rctStatus = nullptr; - -RCTStatus::RCTStatus(QObject* parent) : QObject(parent), d_ptr(new RCTStatusPrivate) { - RCTStatusPrivate::rctStatus = this; - SetSignalEventCallback((void*)&RCTStatus::jailSignalEventCallback); - connect(this, &RCTStatus::jailSignalEvent, this, &RCTStatus::onJailSignalEvent); -} - -RCTStatus::~RCTStatus() {} - -void RCTStatus::setBridge(Bridge* bridge) { - Q_D(RCTStatus); - d->bridge = bridge; -} - -QString RCTStatus::moduleName() { - return "Status"; -} - -QList RCTStatus::methodsToExport() { - return QList{}; -} - -QVariantMap RCTStatus::constantsToExport() { - return QVariantMap(); -} - -void RCTStatus::getDeviceUUID(double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::getDeviceUUID"; - - d->bridge->invokePromiseCallback(callbackId, QVariantList{"com.status.StatusIm"}); -} - - -void RCTStatus::startNode(QString configString) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::startNode with param configString:" << configString; - - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(configString.toUtf8(), &jsonError); - if (jsonError.error != QJsonParseError::NoError){ - qDebug() << jsonError.errorString(); - } - - qDebug() << " RCTStatus::startNode configString: " << jsonDoc.toVariant().toMap(); - QVariantMap configJSON = jsonDoc.toVariant().toMap(); - - int networkId = configJSON["NetworkId"].toInt(); - QString dataDir = configJSON["DataDir"].toString(); - - QString rootDirPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; - QString networkDir = rootDirPath + dataDir; - QString keyStoreDir = rootDirPath + "keystore"; - QDir dir(networkDir); - if (!dir.exists()) { - dir.mkpath("."); - } - qDebug()<<"RCTStatus::startNode networkDir: "<bridge->invokePromiseCallback(callbackId, QVariantList{}); -} - - -void RCTStatus::moveToInternalStorage(double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::moveToInternalStorage with param callbackId: " << callbackId; - d->bridge->invokePromiseCallback(callbackId, QVariantList{ "{\"result\":\"\"}" }); -} - - -void RCTStatus::stopNode() { - qDebug() << "call of RCTStatus::stopNode"; - const char* result = StopNode(); - qDebug() << "RCTStatus::stopNode StopNode result: " << result; -} - - -void RCTStatus::createAccount(QString password, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::createAccount with param callbackId: " << callbackId; - const char* result = CreateAccount(password.toUtf8().data()); - qDebug() << "RCTStatus::createAccount CreateAccount result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - - -void RCTStatus::notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::notifyUsers with param callbackId: " << callbackId; - const char* result = NotifyUsers(token.toUtf8().data(), payloadJSON.toUtf8().data(), tokensJSON.toUtf8().data()); - qDebug() << "RCTStatus::notifyUsers Notify result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - - -void RCTStatus::addPeer(QString enode, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::addPeer with param callbackId: " << callbackId; - const char* result = AddPeer(enode.toUtf8().data()); - qDebug() << "RCTStatus::addPeer AddPeer result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - - -void RCTStatus::recoverAccount(QString passphrase, QString password, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::recoverAccount with param callbackId: " << callbackId; - const char* result = RecoverAccount(password.toUtf8().data(), passphrase.toUtf8().data()); - qDebug() << "RCTStatus::recoverAccount RecoverAccount result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - - -void RCTStatus::login(QString address, QString password, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::login with param callbackId: " << callbackId; - const char* result = Login(address.toUtf8().data(), password.toUtf8().data()); - qDebug() << "RCTStatus::login Login result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - - -void RCTStatus::approveSignRequests(QString hashes, QString password, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::approveSignRequests with param callbackId: " << callbackId; - const char* result = ApproveSignRequests(hashes.toUtf8().data(), password.toUtf8().data()); - qDebug() << "RCTStatus::approveSignRequests CompleteTransactions result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - -void RCTStatus::discardSignRequest(QString id) { - qDebug() << "call of RCTStatus::discardSignRequest with id: " << id; - DiscardSignRequest(id.toUtf8().data()); -} - -void RCTStatus::setAdjustResize() { -} - - -void RCTStatus::setAdjustPan() { -} - - -void RCTStatus::setSoftInputMode(int i) { -} - - - -void RCTStatus::clearCookies() { -} - - -void RCTStatus::clearStorageAPIs() { -} - - -void RCTStatus::sendWeb3Request(QString payload, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::sendWeb3Request with param callbackId: " << callbackId; - const char* result = CallRPC(payload.toUtf8().data()); - qDebug() << "RCTStatus::sendWeb3Request CallRPC result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - -void RCTStatus::sendWeb3PrivateRequest(QString payload, double callbackId) { - Q_D(RCTStatus); - qDebug() << "call of RCTStatus::sendWeb3PrivateRequest with param callbackId: " << callbackId; - const char* result = CallPrivateRPC(payload.toUtf8().data()); - qDebug() << "RCTStatus::sendWeb3PrivateRequest CallPrivateRPC result: " << result; - d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); -} - -void RCTStatus::closeApplication() { -} - -bool RCTStatus::JSCEnabled() { - qDebug() << "call of RCTStatus::JSCEnabled"; - return false; -} - -void RCTStatus::jailSignalEventCallback(const char* signal) { - qDebug() << "call of RCTStatus::jailSignalEventCallback ... signal: " << signal; - RCTStatusPrivate::rctStatus->emitSignalEvent(signal); -} - -void RCTStatus::emitSignalEvent(const char* signal) { - qDebug() << "call of RCTStatus::emitSignalEvent ... signal: " << signal; - Q_EMIT jailSignalEvent(signal); -} - -void RCTStatus::onJailSignalEvent(const char* signal) { - qDebug() << "call of RCTStatus::onJailSignalEvent ... signal: " << signal; - RCTStatusPrivate::bridge->eventDispatcher()->sendDeviceEvent("gethEvent", QVariantMap{{"jsonEvent", signal}}); -} diff --git a/modules/react-native-status/desktop/rctstatus.h b/modules/react-native-status/desktop/rctstatus.h deleted file mode 100644 index c3d584d1665..00000000000 --- a/modules/react-native-status/desktop/rctstatus.h +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2017-present, Status Research and Development GmbH. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - */ - -#ifndef RCTSTATUS_H -#define RCTSTATUS_H - -#include "moduleinterface.h" - -#include - -class RCTStatusPrivate; -class RCTStatus : public QObject, public ModuleInterface { - Q_OBJECT - Q_INTERFACES(ModuleInterface) - - Q_DECLARE_PRIVATE(RCTStatus) - -public: - Q_INVOKABLE RCTStatus(QObject* parent = 0); - ~RCTStatus(); - - void setBridge(Bridge* bridge) override; - - QString moduleName() override; - QList methodsToExport() override; - QVariantMap constantsToExport() override; - - Q_INVOKABLE void startNode(QString configString); - Q_INVOKABLE void shouldMoveToInternalStorage(double callbackId); - Q_INVOKABLE void moveToInternalStorage(double callbackId); - Q_INVOKABLE void stopNode(); - Q_INVOKABLE void createAccount(QString password, double callbackId); - Q_INVOKABLE void notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId); - Q_INVOKABLE void addPeer(QString enode, double callbackId); - Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId); - Q_INVOKABLE void login(QString address, QString password, double callbackId); - Q_INVOKABLE void approveSignRequests(QString hashes, QString password, double callbackId); - Q_INVOKABLE void discardSignRequest(QString id); - - Q_INVOKABLE void setAdjustResize(); - Q_INVOKABLE void setAdjustPan(); - Q_INVOKABLE void setSoftInputMode(int i); - - Q_INVOKABLE void clearCookies(); - Q_INVOKABLE void clearStorageAPIs(); - Q_INVOKABLE void sendWeb3Request(QString payload, double callbackId); - Q_INVOKABLE void sendWeb3PrivateRequest(QString payload, double callbackId); - Q_INVOKABLE void closeApplication(); - Q_INVOKABLE void getDeviceUUID(double callbackId); - - Q_INVOKABLE static bool JSCEnabled(); - Q_INVOKABLE static void jailSignalEventCallback(const char* signal); - - void emitSignalEvent(const char* signal); - -Q_SIGNALS: - void jailSignalEvent(const char* signal); - -private Q_SLOTS: - void onJailSignalEvent(const char* signal); - -private: - QScopedPointer d_ptr; -}; - -#endif // RCTSTATUS_H diff --git a/modules/react-native-status/ios/RCTStatus/AccountManager.h b/modules/react-native-status/ios/RCTStatus/AccountManager.h new file mode 100644 index 00000000000..7698eebe029 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/AccountManager.h @@ -0,0 +1,9 @@ +#import +#import +#import +#import "Statusgo.h" +#import "RCTLog.h" + +@interface AccountManager : NSObject + +@end diff --git a/modules/react-native-status/ios/RCTStatus/AccountManager.m b/modules/react-native-status/ios/RCTStatus/AccountManager.m new file mode 100644 index 00000000000..721db92368f --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/AccountManager.m @@ -0,0 +1,298 @@ +#import "AccountManager.h" +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" +#import "Statusgo.h" +#import "Utils.h" +#import "StatusBackendClient.h" + +@implementation AccountManager + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(createAccountAndLogin:(NSString *)request) { +#if DEBUG + NSLog(@"createAccountAndLogin() method called"); +#endif + [StatusBackendClient executeStatusGoRequest:@"CreateAccountAndLogin" + body:request + statusgoFunction:^NSString *{ + return StatusgoCreateAccountAndLogin(request); + }]; +} + +RCT_EXPORT_METHOD(restoreAccountAndLogin:(NSString *)request) { +#if DEBUG + NSLog(@"restoreAccountAndLogin() method called"); +#endif + [StatusBackendClient executeStatusGoRequest:@"RestoreAccountAndLogin" + body:request + statusgoFunction:^NSString *{ + return StatusgoRestoreAccountAndLogin(request); + }]; +} + +RCT_EXPORT_METHOD(deleteMultiaccount:(NSString *)keyUID + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"DeleteMultiaccount() method called"); +#endif + NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID]; + NSDictionary *params = @{ + @"keyUID": keyUID, + @"keyStoreDir": multiaccountKeystoreDir.path + }; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"DeleteMultiaccountV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoDeleteMultiaccountV2(jsonString); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(loginWithKeycard:(NSString *)accountData + password:(NSString *)password + chatKey:(NSString *)chatKey + nodeConfigJSON:(NSString *)nodeConfigJSON) { +#if DEBUG + NSLog(@"LoginWithKeycard() method called"); +#endif + [Utils getExportDbFilePath]; + [Utils migrateKeystore:accountData password:password]; + + NSString *result = StatusgoLoginWithKeycard(accountData, password, chatKey, nodeConfigJSON); + + NSLog(@"%@", result); +} + +RCT_EXPORT_METHOD(loginWithConfig:(NSString *)accountData + password:(NSString *)password + configJSON:(NSString *)configJSON) { +#if DEBUG + NSLog(@"LoginWithConfig() method called"); +#endif + [Utils getExportDbFilePath]; + [Utils migrateKeystore:accountData password:password]; + NSString *result = StatusgoLoginWithConfig(accountData, password, configJSON); + NSLog(@"%@", result); +} + +RCT_EXPORT_METHOD(loginAccount:(NSString *)request) { +#if DEBUG + NSLog(@"loginAccount() method called"); +#endif + [StatusBackendClient executeStatusGoRequest:@"LoginAccount" + body:request + statusgoFunction:^NSString *{ + return StatusgoLoginAccount(request); + }]; +} + +RCT_EXPORT_METHOD(verifyDatabasePassword:(NSString *)keyUID + password:(NSString *)password + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"VerifyDatabasePasswordV2() method called"); +#endif + NSDictionary *params = @{ + @"keyUID": keyUID, + @"password": password + }; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"VerifyDatabasePasswordV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoVerifyDatabasePasswordV2(jsonString); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(initializeApplication:(NSString *)request + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"initializeApplication() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"InitializeApplication" + body:request + statusgoFunction:^NSString *{ + return StatusgoInitializeApplication(request); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(acceptTerms:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"acceptTerms() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"AcceptTerms" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoAcceptTerms(); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(openAccounts:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"OpenAccounts() method called"); +#endif + NSURL *rootUrl =[Utils getRootUrl]; + NSString *result = StatusgoOpenAccounts(rootUrl.path); + callback(@[result]); +} + +RCT_EXPORT_METHOD(logout) { +#if DEBUG + NSLog(@"Logout() method called"); +#endif + [StatusBackendClient executeStatusGoRequest:@"Logout" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoLogout(); + }]; +} + +RCT_EXPORT_METHOD(multiAccountStoreAccount:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountStoreAccount() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiAccountStoreAccount" + body:json + statusgoFunction:^NSString *{ + return StatusgoMultiAccountStoreAccount(json); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(multiAccountLoadAccount:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountLoadAccount() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiAccountLoadAccount" + body:json + statusgoFunction:^NSString *{ + return StatusgoMultiAccountLoadAccount(json); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(multiAccountDeriveAddresses:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"multiAccountDeriveAddresses() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiAccountDeriveAddresses" + body:json + statusgoFunction:^NSString *{ + return StatusgoMultiAccountDeriveAddresses(json); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(multiAccountGenerateAndDeriveAddresses:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountGenerateAndDeriveAddresses() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiAccountGenerateAndDeriveAddresses" + body:json + statusgoFunction:^NSString *{ + return StatusgoMultiAccountGenerateAndDeriveAddresses(json); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(multiAccountStoreDerived:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountStoreDerived() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiAccountStoreDerivedAccounts" + body:json + statusgoFunction:^NSString *{ + return StatusgoMultiAccountStoreDerivedAccounts(json); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(multiAccountImportMnemonic:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountImportMnemonic() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiAccountImportMnemonic" + body:json + statusgoFunction:^NSString *{ + return StatusgoMultiAccountImportMnemonic(json); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(multiAccountImportPrivateKey:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountImportPrivateKey() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiAccountImportPrivateKey" + body:json + statusgoFunction:^NSString *{ + return StatusgoMultiAccountImportPrivateKey(json); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(getRandomMnemonic:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"GetRandomMnemonic() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"GetRandomMnemonic" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoGetRandomMnemonic(); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(createAccountFromMnemonicAndDeriveAccountsForPaths:(NSString *)mnemonic + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"createAccountFromMnemonicAndDeriveAccountsForPaths() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"CreateAccountFromMnemonicAndDeriveAccountsForPaths" + body:mnemonic + statusgoFunction:^NSString *{ + return StatusgoCreateAccountFromMnemonicAndDeriveAccountsForPaths(mnemonic); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(createAccountFromPrivateKey:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"createAccountFromPrivateKey() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"CreateAccountFromPrivateKey" + body:json + statusgoFunction:^NSString *{ + return StatusgoCreateAccountFromPrivateKey(json); + } + callback:callback]; +} + +@end diff --git a/modules/react-native-status/ios/RCTStatus/DatabaseManager.h b/modules/react-native-status/ios/RCTStatus/DatabaseManager.h new file mode 100644 index 00000000000..7ff6e7dac55 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/DatabaseManager.h @@ -0,0 +1,9 @@ +#import +#import +#import +#import "Statusgo.h" +#import "RCTLog.h" + +@interface DatabaseManager : NSObject + +@end diff --git a/modules/react-native-status/ios/RCTStatus/DatabaseManager.m b/modules/react-native-status/ios/RCTStatus/DatabaseManager.m new file mode 100644 index 00000000000..9a27215725f --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/DatabaseManager.m @@ -0,0 +1,80 @@ +#import "DatabaseManager.h" +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" +#import "Statusgo.h" +#import "Utils.h" +#import "StatusBackendClient.h" +@implementation DatabaseManager + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(exportUnencryptedDatabase:(NSString *)accountData + password:(NSString *)password + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"exportUnencryptedDatabase() method called"); +#endif + + NSString *filePath = [Utils getExportDbFilePath]; + + NSDictionary *params = @{ + @"account": [NSJSONSerialization JSONObjectWithData:[accountData dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil], + @"password": password, + @"databasePath": filePath + }; + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + + if (error) { + NSLog(@"Error creating JSON: %@", [error localizedDescription]); + callback(@[filePath]); + return; + } + + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"ExportUnencryptedDatabaseV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoExportUnencryptedDatabaseV2(jsonString); + } + callback:nil]; + + callback(@[filePath]); +} + +RCT_EXPORT_METHOD(importUnencryptedDatabase:(NSString *)accountData + password:(NSString *)password) { +#if DEBUG + NSLog(@"importUnencryptedDatabase() method called"); +#endif + + NSString *filePath = [Utils getExportDbFilePath]; + [Utils migrateKeystore:accountData password:password]; + + NSDictionary *params = @{ + @"account": [NSJSONSerialization JSONObjectWithData:[accountData dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil], + @"password": password, + @"databasePath": filePath + }; + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + + if (error) { + NSLog(@"Error creating JSON: %@", [error localizedDescription]); + return; + } + + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + [StatusBackendClient executeStatusGoRequest:@"ImportUnencryptedDatabaseV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoImportUnencryptedDatabaseV2(jsonString); + }]; +} + + +@end diff --git a/modules/react-native-status/ios/RCTStatus/EncryptionUtils.h b/modules/react-native-status/ios/RCTStatus/EncryptionUtils.h new file mode 100644 index 00000000000..29fa9a1703d --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/EncryptionUtils.h @@ -0,0 +1,9 @@ +#import +#import +#import +#import "Statusgo.h" +#import "RCTLog.h" + +@interface EncryptionUtils : NSObject + +@end diff --git a/modules/react-native-status/ios/RCTStatus/EncryptionUtils.m b/modules/react-native-status/ios/RCTStatus/EncryptionUtils.m new file mode 100644 index 00000000000..039ab523e15 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/EncryptionUtils.m @@ -0,0 +1,296 @@ +#import "EncryptionUtils.h" +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" +#import "Statusgo.h" +#import "Utils.h" +#import "StatusBackendClient.h" + +@implementation EncryptionUtils + +RCT_EXPORT_MODULE(); + +#pragma mark - InitKeystore method + +RCT_EXPORT_METHOD(initKeystore:(NSString *)keyUID + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"initKeystore() method called"); +#endif + NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"InitKeystore" + body:multiaccountKeystoreDir.path + statusgoFunction:^NSString *{ + return StatusgoInitKeystore(multiaccountKeystoreDir.path); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(reEncryptDbAndKeystore:(NSString *)keyUID + currentPassword:(NSString *)currentPassword + newPassword:(NSString *)newPassword + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"reEncryptDbAndKeystore() method called"); +#endif + // Construct params into JSON string + NSDictionary *params = @{ + @"keyUID": keyUID, + @"oldPassword": currentPassword, + @"newPassword": newPassword + }; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"ChangeDatabasePasswordV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoChangeDatabasePasswordV2(jsonString); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(convertToKeycardAccount:(NSString *)keyUID + accountData:(NSString *)accountData + settings:(NSString *)settings + keycardUID:(NSString *)keycardUID + currentPassword:(NSString *)currentPassword + newPassword:(NSString *)newPassword + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"convertToKeycardAccount() method called"); +#endif + NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID]; + + // First initialize keystore + [StatusBackendClient executeStatusGoRequest:@"InitKeystore" + body:multiaccountKeystoreDir.path + statusgoFunction:^NSString *{ + return StatusgoInitKeystore(multiaccountKeystoreDir.path); + }]; + + // Prepare parameters for conversion + NSDictionary *params = @{ + @"keyUID": keyUID, + @"account": [NSJSONSerialization JSONObjectWithData:[accountData dataUsingEncoding:NSUTF8StringEncoding] + options:0 + error:nil], + @"settings": [NSJSONSerialization JSONObjectWithData:[settings dataUsingEncoding:NSUTF8StringEncoding] + options:0 + error:nil], + @"keycardUID": keycardUID, + @"oldPassword": currentPassword, + @"newPassword": newPassword + }; + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", [error localizedDescription]); + return; + } + + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"ConvertToKeycardAccountV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoConvertToKeycardAccountV2(jsonString); + } + callback:callback]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(decodeParameters:(NSString *)decodeParamJSON) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"DecodeParameters" + body:decodeParamJSON + statusgoFunction:^NSString *{ + return StatusgoDecodeParameters(decodeParamJSON); + }]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(hexToNumber:(NSString *)hex) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"HexToNumber" + body:hex + statusgoFunction:^NSString *{ + return StatusgoHexToNumber(hex); + }]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(numberToHex:(NSString *)numString) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"NumberToHex" + body:numString + statusgoFunction:^NSString *{ + return StatusgoNumberToHex(numString); + }]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(sha3:(NSString *)str) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"Sha3" + body:str + statusgoFunction:^NSString *{ + return StatusgoSha3(str); + }]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(utf8ToHex:(NSString *)str) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"Utf8ToHex" + body:str + statusgoFunction:^NSString *{ + return StatusgoUtf8ToHex(str); + }]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(hexToUtf8:(NSString *)str) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"HexToUtf8" + body:str + statusgoFunction:^NSString *{ + return StatusgoHexToUtf8(str); + }]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(serializeLegacyKey:(NSString *)str) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"SerializeLegacyKey" + body:str + statusgoFunction:^NSString *{ + return StatusgoSerializeLegacyKey(str); + }]; +} + +RCT_EXPORT_METHOD(setBlankPreviewFlag:(BOOL *)newValue) +{ + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + + [userDefaults setBool:newValue forKey:@"BLANK_PREVIEW"]; + + [userDefaults synchronize]; +} + +RCT_EXPORT_METHOD(hashTransaction:(NSString *)txArgsJSON + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"hashTransaction() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"HashTransaction" + body:txArgsJSON + statusgoFunction:^NSString *{ + return StatusgoHashTransaction(txArgsJSON); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(hashMessage:(NSString *)message + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"hashMessage() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"HashMessage" + body:message + statusgoFunction:^NSString *{ + return StatusgoHashMessage(message); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(localPairingPreflightOutboundCheck:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"LocalPairingPreflightOutboundCheck() method called"); +#endif + NSString *result = StatusgoLocalPairingPreflightOutboundCheck(); + callback(@[result]); +} + +RCT_EXPORT_METHOD(multiformatDeserializePublicKey:(NSString *)multiCodecKey + base58btc:(NSString *)base58btc + callback:(RCTResponseSenderBlock)callback) { + NSDictionary *params = @{ + @"key": multiCodecKey, + @"outBase": base58btc + }; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"MultiformatDeserializePublicKeyV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoMultiformatDeserializePublicKeyV2(jsonString); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(deserializeAndCompressKey:(NSString *)desktopKey + callback:(RCTResponseSenderBlock)callback) { + [StatusBackendClient executeStatusGoRequestWithCallback:@"DeserializeAndCompressKey" + body:desktopKey + statusgoFunction:^NSString *{ + return StatusgoDeserializeAndCompressKey(desktopKey); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(hashTypedData:(NSString *)data + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"hashTypedData() method called"); +#endif + NSString *result = StatusgoHashTypedData(data); + callback(@[result]); +} + +RCT_EXPORT_METHOD(hashTypedDataV4:(NSString *)data + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"hashTypedDataV4() method called"); +#endif + NSString *result = StatusgoHashTypedDataV4(data); + callback(@[result]); +} + +#pragma mark - SignMessage + +RCT_EXPORT_METHOD(signMessage:(NSString *)rpcParams + callback:(RCTResponseSenderBlock)callback) { + [StatusBackendClient executeStatusGoRequestWithCallback:@"SignMessage" + body:rpcParams + statusgoFunction:^NSString *{ + return StatusgoSignMessage(rpcParams); + } + callback:callback]; +} + +#pragma mark - SignTypedData + +RCT_EXPORT_METHOD(signTypedData:(NSString *)data + account:(NSString *)account + password:(NSString *)password + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"SignTypedData() method called"); +#endif + NSString *result = StatusgoSignTypedData(data, account, password); + callback(@[result]); +} + +#pragma mark - SignTypedDataV4 + +RCT_EXPORT_METHOD(signTypedDataV4:(NSString *)data + account:(NSString *)account + password:(NSString *)password + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"SignTypedDataV4() method called"); +#endif + NSString *result = StatusgoSignTypedDataV4(data, account, password); + callback(@[result]); +} + +@end diff --git a/modules/react-native-status/ios/RCTStatus/LogManager.h b/modules/react-native-status/ios/RCTStatus/LogManager.h new file mode 100644 index 00000000000..a7a264f9882 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/LogManager.h @@ -0,0 +1,9 @@ +#import +#import +#import +#import "Statusgo.h" +#import "RCTLog.h" + +@interface LogManager : NSObject + +@end diff --git a/modules/react-native-status/ios/RCTStatus/LogManager.m b/modules/react-native-status/ios/RCTStatus/LogManager.m new file mode 100644 index 00000000000..d7980ea4511 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/LogManager.m @@ -0,0 +1,124 @@ +#import "LogManager.h" +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" +#import "Statusgo.h" +#import "Utils.h" +#import "SSZipArchive.h" +#import "StatusBackendClient.h" + +@implementation LogManager + +RCT_EXPORT_MODULE(); + +#pragma mark - SendLogs method + +RCT_EXPORT_METHOD(sendLogs:(NSString *)dbJson + jsLogs:(NSString *)jsLogs + usePublicLogDir:(BOOL)usePublicLogDir + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"SendLogs() method called"); +#endif + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl = [Utils getRootUrl]; + + // Prepare output paths + NSURL *zipFilePath = [rootUrl URLByAppendingPathComponent:@"logs.zip"]; + NSURL *logsFolderPath = [rootUrl URLByAppendingPathComponent:@"logs_"]; + + [fileManager removeItemAtPath:zipFilePath.path error:nil]; + + [self ensureDirectoryExists:logsFolderPath withFileManager:fileManager]; + + [self saveProvidedLogs:dbJson jsLogs:jsLogs toFolder:logsFolderPath]; + + [self copyExistingLogsToFolder:logsFolderPath withFileManager:fileManager]; + + // Create zip archive and clean up + [SSZipArchive createZipFileAtPath:zipFilePath.path withContentsOfDirectory:logsFolderPath.path]; + [fileManager removeItemAtPath:logsFolderPath.path error:nil]; + + callback(@[zipFilePath.absoluteString]); +} + +- (void)ensureDirectoryExists:(NSURL *)directoryPath withFileManager:(NSFileManager *)fileManager { + if (![fileManager fileExistsAtPath:directoryPath.path]) { + NSError *error = nil; + [fileManager createDirectoryAtPath:directoryPath.path + withIntermediateDirectories:YES + attributes:nil + error:&error]; + } +} + +- (void)saveProvidedLogs:(NSString *)dbJson jsLogs:(NSString *)jsLogs toFolder:(NSURL *)logsFolderPath { + NSURL *dbFile = [logsFolderPath URLByAppendingPathComponent:@"db.json"]; + NSURL *jsLogsFile = [logsFolderPath URLByAppendingPathComponent:@"Status.log"]; + + [dbJson writeToFile:dbFile.path atomically:YES encoding:NSUTF8StringEncoding error:nil]; + [jsLogs writeToFile:jsLogsFile.path atomically:YES encoding:NSUTF8StringEncoding error:nil]; +} + +- (void)copyExistingLogsToFolder:(NSURL *)logsFolderPath withFileManager:(NSFileManager *)fileManager { + NSString *logDirectory = [self logFileDirectory:NO]; + + // Get all files in the log directory + NSError *error = nil; + NSArray *fileList = [fileManager contentsOfDirectoryAtPath:logDirectory error:&error]; + if (error) { + NSLog(@"Error reading log directory: %@", error); + return; + } + + for (NSString *fileName in fileList) { + NSString *sourcePath = [logDirectory stringByAppendingPathComponent:fileName]; + BOOL isDirectory; + + // Only copy files, skip directories + [fileManager fileExistsAtPath:sourcePath isDirectory:&isDirectory]; + if (!isDirectory) { + NSString *destinationPath = [logsFolderPath.path stringByAppendingPathComponent:fileName]; + [fileManager copyItemAtPath:sourcePath toPath:destinationPath error:nil]; + } + } +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(logFileDirectory:(BOOL)usePublicLogDir) { + NSURL *rootUrl = [Utils getRootUrl]; + NSURL *logsUrl = [rootUrl URLByAppendingPathComponent:@"logs"]; + return logsUrl.path; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(setProfileLogLevel:(NSString *)setProfileLogLevelRequest) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"SetProfileLogLevel" + body:setProfileLogLevelRequest + statusgoFunction:^NSString *{ + return StatusgoSetProfileLogLevel(setProfileLogLevelRequest); + }]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(setProfileLogEnabled:(NSString *)setProfileLogEnabledRequest) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"SetProfileLogEnabled" + body:setProfileLogEnabledRequest + statusgoFunction:^NSString *{ + return StatusgoSetProfileLogEnabled(setProfileLogEnabledRequest); + }]; +} + +RCT_EXPORT_METHOD(setPreLoginLogLevel:(NSString *)setPreLoginLogLevelRequest) { + [StatusBackendClient executeStatusGoRequest:@"SetPreLoginLogLevel" + body:setPreLoginLogLevelRequest + statusgoFunction:^NSString *{ + return StatusgoSetPreLoginLogLevel(setPreLoginLogLevelRequest); + }]; +} + +RCT_EXPORT_METHOD(setPreLoginLogEnabled:(NSString *)setPreLoginLogEnabledRequest) { + [StatusBackendClient executeStatusGoRequest:@"SetPreLoginLogEnabled" + body:setPreLoginLogEnabledRequest + statusgoFunction:^NSString *{ + return StatusgoSetPreLoginLogEnabled(setPreLoginLogEnabledRequest); + }]; +} + +@end diff --git a/modules/react-native-status/ios/RCTStatus/MailManager.h b/modules/react-native-status/ios/RCTStatus/MailManager.h new file mode 100644 index 00000000000..98a4fa1303e --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/MailManager.h @@ -0,0 +1,9 @@ +#import +#import +#import +#import +#import "RCTLog.h" + +@interface MailManager : NSObject + +@end diff --git a/modules/react-native-status/ios/RCTStatus/MailManager.m b/modules/react-native-status/ios/RCTStatus/MailManager.m new file mode 100644 index 00000000000..bc0a36b7a9d --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/MailManager.m @@ -0,0 +1,207 @@ +#import +#import "MailManager.h" +#import +#import +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" + +@implementation MailManager +{ + NSMutableDictionary *_callbacks; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _callbacks = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(mail:(NSDictionary *)options + callback: (RCTResponseSenderBlock)callback) +{ + if ([MFMailComposeViewController canSendMail]) + { + MFMailComposeViewController *mail = [[MFMailComposeViewController alloc] init]; + mail.mailComposeDelegate = self; + _callbacks[RCTKeyForInstance(mail)] = callback; + + if (options[@"subject"]){ + NSString *subject = [RCTConvert NSString:options[@"subject"]]; + [mail setSubject:subject]; + } + + BOOL isHTML = NO; + + if (options[@"isHTML"]){ + isHTML = [options[@"isHTML"] boolValue]; + } + + if (options[@"body"]){ + NSString *body = [RCTConvert NSString:options[@"body"]]; + [mail setMessageBody:body isHTML:isHTML]; + } + + if (options[@"recipients"]){ + NSArray *recipients = [RCTConvert NSArray:options[@"recipients"]]; + [mail setToRecipients:recipients]; + } + + if (options[@"ccRecipients"]){ + NSArray *ccRecipients = [RCTConvert NSArray:options[@"ccRecipients"]]; + [mail setCcRecipients:ccRecipients]; + } + + if (options[@"bccRecipients"]){ + NSArray *bccRecipients = [RCTConvert NSArray:options[@"bccRecipients"]]; + [mail setBccRecipients:bccRecipients]; + } + if (options[@"attachments"]) { + NSArray *attachments = [RCTConvert NSArray:options[@"attachments"]]; + for (NSDictionary *attachment in attachments) { + if ((attachment[@"path"] || attachment[@"uri"]) && (attachment[@"type"] || attachment[@"mimeType"])) { + NSString *attachmentPath = [RCTConvert NSString:attachment[@"path"]]; + NSString *attachmentUri = [RCTConvert NSString:attachment[@"uri"]]; + NSString *attachmentType = [RCTConvert NSString:attachment[@"type"]]; + NSString *attachmentName = [RCTConvert NSString:attachment[@"name"]]; + NSString *attachmentMimeType = [RCTConvert NSString:attachment[@"mimeType"]]; + + // Set default filename if not specificed + if (!attachmentName) { + attachmentName = [[attachmentPath lastPathComponent] stringByDeletingPathExtension]; + } + + NSData *fileData; + if (attachmentPath) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:attachmentPath]){ + callback(@[[NSString stringWithFormat: @"attachment file with path '%@' does not exist", attachmentPath]]); + return; + } + // Get the resource path and read the file using NSData + fileData = [NSData dataWithContentsOfFile:attachmentPath]; + } else if (attachmentUri) { + // Get the URI and read it using NSData + NSURL *attachmentURL = [[NSURLComponents componentsWithString:attachmentUri] URL]; + NSError *error = nil; + fileData = [NSData dataWithContentsOfURL:attachmentURL options:0 error:&error]; + if (!fileData) { + callback(@[[NSString stringWithFormat: @"attachment file with uri '%@' does not exist", attachmentUri]]); + return; + } + } + + // Determine the MIME type + NSString *mimeType; + if (attachmentType) { + /* + * Add additional mime types and PR if necessary. Find the list + * of supported formats at http://www.iana.org/assignments/media-types/media-types.xhtml + */ + NSDictionary *supportedMimeTypes = @{ + @"jpeg" : @"image/jpeg", + @"jpg" : @"image/jpeg", + @"png" : @"image/png", + @"doc" : @"application/msword", + @"docx" : @"application/vnd.openxmlformats-officedocument.wordprocessingml.document", + @"ppt" : @"application/vnd.ms-powerpoint", + @"pptx" : @"application/vnd.openxmlformats-officedocument.presentationml.presentation", + @"html" : @"text/html", + @"csv" : @"text/csv", + @"pdf" : @"application/pdf", + @"vcard" : @"text/vcard", + @"json" : @"application/json", + @"zip" : @"application/zip", + @"text" : @"text/*", + @"mp3" : @"audio/mpeg", + @"wav" : @"audio/wav", + @"aiff" : @"audio/aiff", + @"flac" : @"audio/flac", + @"ogg" : @"audio/ogg", + @"xls" : @"application/vnd.ms-excel", + @"ics" : @"text/calendar", + @"xlsx" : @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + }; + if([supportedMimeTypes objectForKey:attachmentType]) { + mimeType = [supportedMimeTypes objectForKey:attachmentType]; + } else { + callback(@[[NSString stringWithFormat: @"Mime type '%@' for attachment is not handled", attachmentType]]); + return; + } + } else if (attachmentMimeType) { + mimeType = attachmentMimeType; + } + + // Add attachment + [mail addAttachmentData:fileData mimeType:mimeType fileName:attachmentName]; + } + } + } + + UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + + while (root.presentedViewController) { + root = root.presentedViewController; + } + [root presentViewController:mail animated:YES completion:nil]; + } else { + callback(@[@"not_available"]); + } +} + +#pragma mark MFMailComposeViewControllerDelegate Methods + +- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error +{ + NSString *key = RCTKeyForInstance(controller); + RCTResponseSenderBlock callback = _callbacks[key]; + if (callback) { + switch (result) { + case MFMailComposeResultSent: + callback(@[[NSNull null] , @"sent"]); + break; + case MFMailComposeResultSaved: + callback(@[[NSNull null] , @"saved"]); + break; + case MFMailComposeResultCancelled: + callback(@[[NSNull null] , @"cancelled"]); + break; + case MFMailComposeResultFailed: + callback(@[@"failed"]); + break; + default: + callback(@[@"error"]); + break; + } + [_callbacks removeObjectForKey:key]; + } else { + RCTLogWarn(@"No callback registered for mail: %@", controller.title); + } + UIViewController *ctrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + while (ctrl.presentedViewController && ctrl != controller) { + ctrl = ctrl.presentedViewController; + } + [ctrl dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark Private + +static NSString *RCTKeyForInstance(id instance) +{ + return [NSString stringWithFormat:@"%p", instance]; +} + +@end diff --git a/modules/react-native-status/ios/RCTStatus/NetworkManager.h b/modules/react-native-status/ios/RCTStatus/NetworkManager.h new file mode 100644 index 00000000000..cc8ce42f98e --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/NetworkManager.h @@ -0,0 +1,9 @@ +#import +#import +#import +#import "Statusgo.h" +#import "RCTLog.h" + +@interface NetworkManager : NSObject + +@end diff --git a/modules/react-native-status/ios/RCTStatus/NetworkManager.m b/modules/react-native-status/ios/RCTStatus/NetworkManager.m new file mode 100644 index 00000000000..6b76182dc6b --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/NetworkManager.m @@ -0,0 +1,214 @@ +#import "NetworkManager.h" +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" +#import "Statusgo.h" +#import "Utils.h" +#import "StatusBackendClient.h" +@implementation NetworkManager + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(startSearchForLocalPairingPeers:(RCTResponseSenderBlock)callback) { + [StatusBackendClient executeStatusGoRequestWithCallback:@"StartSearchForLocalPairingPeers" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoStartSearchForLocalPairingPeers(); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(getConnectionStringForBootstrappingAnotherDevice:(NSString *)configJSON + callback:(RCTResponseSenderBlock)callback) { + + NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSMutableDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:&error]; + if (error) { + NSLog(@"Error parsing JSON: %@", error); + return; + } + + NSMutableDictionary *senderConfig = configDict[@"senderConfig"]; + NSString *keyUID = senderConfig[@"keyUID"]; + NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID]; + NSString *keystoreDir = multiaccountKeystoreDir.path; + + [senderConfig setValue:keystoreDir forKey:@"keystorePath"]; + NSString *modifiedConfigJSON = [Utils jsonStringWithPrettyPrint:NO fromDictionary:configDict]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"GetConnectionStringForBootstrappingAnotherDevice" + body:modifiedConfigJSON + statusgoFunction:^NSString *{ + return StatusgoGetConnectionStringForBootstrappingAnotherDevice(modifiedConfigJSON); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(inputConnectionStringForBootstrapping:(NSString *)cs + configJSON:(NSString *)configJSON + callback:(RCTResponseSenderBlock)callback) { + + NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *jsonError; + NSDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:0 error:&jsonError]; + if (jsonError) { + NSLog(@"Error parsing JSON: %@", jsonError); + return; + } + + NSDictionary *params = @{ + @"connectionString": cs, + @"receiverClientConfig": configDict + }; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [StatusBackendClient executeStatusGoRequestWithCallback:@"InputConnectionStringForBootstrappingV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoInputConnectionStringForBootstrappingV2(jsonString); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(sendTransactionWithSignature:(NSString *)txArgsJSON + signature:(NSString *)signature + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"sendTransactionWithSignature() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"SendTransactionWithSignature" + body:txArgsJSON + statusgoFunction:^NSString *{ + return StatusgoSendTransactionWithSignature(txArgsJSON, signature); + } + callback:callback]; +} + +#pragma mark - SendTransaction + +RCT_EXPORT_METHOD(sendTransaction:(NSString *)txArgsJSON + password:(NSString *)password + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"SendTransactionV2() method called"); +#endif + NSData *txArgsData = [txArgsJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *jsonError; + NSDictionary *txArgsDict = [NSJSONSerialization JSONObjectWithData:txArgsData options:0 error:&jsonError]; + if (jsonError) { + NSLog(@"Error parsing JSON: %@", jsonError); + return; + } + + NSDictionary *params = @{ + @"txArgs": txArgsDict, + @"password": password + }; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [StatusBackendClient executeStatusGoRequestWithCallback:@"SendTransactionV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoSendTransactionV2(jsonString); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(callRPC:(NSString *)payload + callback:(RCTResponseSenderBlock)callback) { + [StatusBackendClient executeStatusGoRequestWithCallback:@"CallRPC" + body:payload + statusgoFunction:^NSString *{ + return StatusgoCallRPC(payload); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(callPrivateRPC:(NSString *)payload + callback:(RCTResponseSenderBlock)callback) { + [StatusBackendClient executeStatusGoRequestWithCallback:@"CallPrivateRPC" + body:payload + statusgoFunction:^NSString *{ + return StatusgoCallPrivateRPC(payload); + } + callback:callback]; +} + +#pragma mark - Recover + +RCT_EXPORT_METHOD(recover:(NSString *)message + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"Recover() method called"); +#endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"Recover" + body:message + statusgoFunction:^NSString *{ + return StatusgoRecover(message); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(getConnectionStringForExportingKeypairsKeystores:(NSString *)configJSON + callback:(RCTResponseSenderBlock)callback) { + + NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSMutableDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:&error]; + NSMutableDictionary *senderConfig = configDict[@"senderConfig"]; + NSString *keyUID = senderConfig[@"loggedInKeyUid"]; + NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID]; + NSString *keystoreDir = multiaccountKeystoreDir.path; + + [senderConfig setValue:keystoreDir forKey:@"keystorePath"]; + NSString *modifiedConfigJSON = [Utils jsonStringWithPrettyPrint:NO fromDictionary:configDict]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"GetConnectionStringForExportingKeypairsKeystores" + body:modifiedConfigJSON + statusgoFunction:^NSString *{ + return StatusgoGetConnectionStringForExportingKeypairsKeystores(modifiedConfigJSON); + } + callback:callback]; +} + +RCT_EXPORT_METHOD(inputConnectionStringForImportingKeypairsKeystores:(NSString *)cs + configJSON:(NSString *)configJSON + callback:(RCTResponseSenderBlock)callback) { + + NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSMutableDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:&error]; + NSMutableDictionary *receiverConfig = configDict[@"receiverConfig"]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl =[Utils getRootUrl]; + NSURL *multiaccountKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"]; + NSString *keystoreDir = multiaccountKeystoreDir.path; + + [receiverConfig setValue:keystoreDir forKey:@"keystorePath"]; + NSString *modifiedConfigJSON = [Utils jsonStringWithPrettyPrint:NO fromDictionary:configDict]; + + NSDictionary *params = @{ + @"connectionString": cs, + @"keystoreFilesReceiverClientConfig": modifiedConfigJSON + }; + NSString *paramsJSON = [Utils jsonStringWithPrettyPrint:NO fromDictionary:params]; + + [StatusBackendClient executeStatusGoRequestWithCallback:@"InputConnectionStringForImportingKeypairsKeystoresV2" + body:paramsJSON + statusgoFunction:^NSString *{ + return StatusgoInputConnectionStringForImportingKeypairsKeystoresV2(paramsJSON); + } + callback:callback]; +} + +@end diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.h b/modules/react-native-status/ios/RCTStatus/RCTStatus.h index 9ce5e5b22a3..54b63e14eaf 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.h +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.h @@ -1,10 +1,10 @@ +#import #import #import +#import "Statusgo.h" #import "RCTLog.h" -@interface Status : NSObject -+ (void)signalEvent:(const char *)signal; -+ (void)jailEvent:(NSString *)chatId - data:(NSString *)data; -+ (BOOL)JSCEnabled; +@interface Status : NSObject ++ (Status *)sharedInstance; +- (void)handleSignal:(NSString *)signal; @end diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index 2b5a11e5216..73b9902f171 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -1,53 +1,31 @@ #import "RCTStatus.h" -#import "ReactNativeConfig.h" #import "React/RCTBridge.h" #import "React/RCTEventDispatcher.h" -#import +#import "Statusgo.h" +#import "StatusBackendClient.h" +#import "Utils.h" -@interface NSDictionary (BVJSONString) --(NSString*) bv_jsonStringWithPrettyPrint:(BOOL) prettyPrint; -@end - -@implementation NSDictionary (BVJSONString) +static RCTBridge *bridge; --(NSString*) bv_jsonStringWithPrettyPrint:(BOOL) prettyPrint { - NSError *error; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self - options:(NSJSONWritingOptions) (prettyPrint ? NSJSONWritingPrettyPrinted : 0) - error:&error]; +@implementation Status - if (! jsonData) { - NSLog(@"bv_jsonStringWithPrettyPrint: error: %@", error.localizedDescription); - return @"{}"; - } else { - return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - } ++ (Status *)sharedInstance { + static Status *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; } -@end - -@interface NSArray (BVJSONString) -- (NSString *)bv_jsonStringWithPrettyPrint:(BOOL)prettyPrint; -@end - -@implementation NSArray (BVJSONString) --(NSString*) bv_jsonStringWithPrettyPrint:(BOOL) prettyPrint { - NSError *error; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self - options:(NSJSONWritingOptions) (prettyPrint ? NSJSONWritingPrettyPrinted : 0) - error:&error]; - if (! jsonData) { - NSLog(@"bv_jsonStringWithPrettyPrint: error: %@", error.localizedDescription); - return @"[]"; - } else { - return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; } -} -@end - -static bool isStatusInitialized; -static RCTBridge *bridge; -@implementation Status{ + // Subscribing to the signals from Status-Go + StatusgoSetMobileSignalHandler(self); + return self; } -(RCTBridge *)bridge @@ -60,342 +38,240 @@ -(void)setBridge:(RCTBridge *)newBridge bridge = newBridge; } -RCT_EXPORT_MODULE(); - -//////////////////////////////////////////////////////////////////// -#pragma mark - startNode -//////////////////////////////////////////////////////////////////// startNode -RCT_EXPORT_METHOD(startNode:(NSString *)configString) { +- (void)handleSignal:(NSString *)signal +{ + if(!signal){ #if DEBUG - NSLog(@"StartNode() method called"); + NSLog(@"SignalEvent nil"); #endif - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSError *error = nil; - NSURL *rootUrl =[[fileManager - URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] - lastObject]; - NSURL *testnetFolderName = [rootUrl URLByAppendingPathComponent:@"ethereum/testnet"]; - - if (![fileManager fileExistsAtPath:testnetFolderName.path]) - [fileManager createDirectoryAtPath:testnetFolderName.path withIntermediateDirectories:YES attributes:nil error:&error]; - - NSURL *flagFolderUrl = [rootUrl URLByAppendingPathComponent:@"ropsten_flag"]; - - if(![fileManager fileExistsAtPath:flagFolderUrl.path]){ - NSLog(@"remove lightchaindata"); - NSURL *lightChainData = [testnetFolderName URLByAppendingPathComponent:@"StatusIM/lightchaindata"]; - if([fileManager fileExistsAtPath:lightChainData.path]) { - [fileManager removeItemAtPath:lightChainData.path - error:nil]; - } - [fileManager createDirectoryAtPath:flagFolderUrl.path - withIntermediateDirectories:NO - attributes:nil - error:&error]; - } - - NSLog(@"after remove lightchaindata"); - - NSURL *oldKeystoreUrl = [testnetFolderName URLByAppendingPathComponent:@"keystore"]; - NSURL *newKeystoreUrl = [rootUrl URLByAppendingPathComponent:@"keystore"]; - if([fileManager fileExistsAtPath:oldKeystoreUrl.path]){ - NSLog(@"copy keystore"); - [fileManager copyItemAtPath:oldKeystoreUrl.path toPath:newKeystoreUrl.path error:nil]; - [fileManager removeItemAtPath:oldKeystoreUrl.path error:nil]; - } - - NSLog(@"after lightChainData"); - - NSLog(@"preconfig: %@", configString); - NSData *configData = [configString dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *configJSON = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:nil]; - int networkId = [configJSON[@"NetworkId"] integerValue]; - NSString *dataDir = [configJSON objectForKey:@"DataDir"]; - NSString *upstreamURL = [configJSON valueForKeyPath:@"UpstreamConfig.URL"]; - NSArray *bootnodes = [configJSON valueForKeyPath:@"ClusterConfig.BootNodes"]; - NSString *networkDir = [rootUrl.path stringByAppendingString:dataDir]; - NSString *devCluster = [ReactNativeConfig envFor:@"ETHEREUM_DEV_CLUSTER"]; - NSString *logLevel = [[ReactNativeConfig envFor:@"LOG_LEVEL_STATUS_GO"] uppercaseString]; - char *configChars = GenerateConfig((char *)[networkDir UTF8String], networkId); - NSString *config = [NSString stringWithUTF8String: configChars]; - configData = [config dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *resultingConfigJson = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:nil]; - NSURL *networkDirUrl = [NSURL fileURLWithPath:networkDir]; - NSURL *logUrl = [networkDirUrl URLByAppendingPathComponent:@"geth.log"]; - [resultingConfigJson setValue:newKeystoreUrl.path forKey:@"KeyStoreDir"]; - [resultingConfigJson setValue:[NSNumber numberWithBool:[logLevel length] != 0] forKey:@"LogEnabled"]; - [resultingConfigJson setValue:([logLevel length] == 0 ? [NSNull null] : logUrl.path) forKey:@"LogFile"]; - [resultingConfigJson setValue:([logLevel length] == 0 ? [NSString stringWithUTF8String: "ERROR"] : logLevel) forKey:@"LogLevel"]; - - [resultingConfigJson setValue:[NSNumber numberWithBool:YES] forKeyPath:@"WhisperConfig.LightClient"]; - - if(upstreamURL != nil) { - [resultingConfigJson setValue:[NSNumber numberWithBool:YES] forKeyPath:@"UpstreamConfig.Enabled"]; - [resultingConfigJson setValue:upstreamURL forKeyPath:@"UpstreamConfig.URL"]; - } - - if(bootnodes != nil) { - [resultingConfigJson setValue:[NSNumber numberWithBool:YES] forKeyPath:@"ClusterConfig.Enabled"]; - [resultingConfigJson setValue:bootnodes forKeyPath:@"ClusterConfig.BootNodes"]; - } - - - NSString *resultingConfig = [resultingConfigJson bv_jsonStringWithPrettyPrint:NO]; - NSLog(@"node config %@", resultingConfig); - - if(![fileManager fileExistsAtPath:networkDirUrl.path]) { - [fileManager createDirectoryAtPath:networkDirUrl.path withIntermediateDirectories:YES attributes:nil error:nil]; + return; } - NSLog(@"logUrlPath %@", logUrl.path); - if(![fileManager fileExistsAtPath:logUrl.path]) { - NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - [dict setObject:[NSNumber numberWithInt:511] forKey:NSFilePosixPermissions]; - [fileManager createFileAtPath:logUrl.path contents:nil attributes:dict]; - } + [bridge.eventDispatcher sendAppEventWithName:@"gethEvent" + body:@{@"jsonEvent": signal}]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), - ^(void) - { - char *res = StartNode((char *) [resultingConfig UTF8String]); - NSLog(@"StartNode result %@", [NSString stringWithUTF8String: res]); }); + return; } -//////////////////////////////////////////////////////////////////// +RCT_EXPORT_MODULE(); + #pragma mark - shouldMoveToInternalStorage -//////////////////////////////////////////////////////////////////// shouldMoveToInternalStorage + RCT_EXPORT_METHOD(shouldMoveToInternalStorage:(RCTResponseSenderBlock)onResultCallback) { // Android only onResultCallback(@[[NSNull null]]); } -//////////////////////////////////////////////////////////////////// #pragma mark - moveToInternalStorage -//////////////////////////////////////////////////////////////////// moveToInternalStorage + RCT_EXPORT_METHOD(moveToInternalStorage:(RCTResponseSenderBlock)onResultCallback) { // Android only onResultCallback(@[[NSNull null]]); } -//////////////////////////////////////////////////////////////////// -#pragma mark - StopNode method -//////////////////////////////////////////////////////////////////// StopNode -RCT_EXPORT_METHOD(stopNode) { +RCT_EXPORT_METHOD(exportLogs:(RCTResponseSenderBlock)callback) { #if DEBUG - NSLog(@"StopNode() method called"); + NSLog(@"exportLogs() method called"); #endif - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), - ^(void) - { - char *res = StopNode(); - NSLog(@"StopNode result %@", [NSString stringWithUTF8String: res]); - }); + NSString *result = StatusgoExportNodeLogs(); + callback(@[result]); } -//////////////////////////////////////////////////////////////////// -#pragma mark - Accounts method -//////////////////////////////////////////////////////////////////// createAccount -RCT_EXPORT_METHOD(createAccount:(NSString *)password +RCT_EXPORT_METHOD(deleteImportedKey:(NSString *)keyUID + address:(NSString *)address + password:(NSString *)password callback:(RCTResponseSenderBlock)callback) { #if DEBUG - NSLog(@"CreateAccount() method called"); + NSLog(@"DeleteImportedKeyV2() method called"); #endif - char * result = CreateAccount((char *) [password UTF8String]); - callback(@[[NSString stringWithUTF8String: result]]); + NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID]; + NSString *jsonParams = [NSString stringWithFormat:@"{\"address\":\"%@\",\"password\":\"%@\",\"keyStoreDir\":\"%@\"}", + address, password, multiaccountKeystoreDir.path]; + [StatusBackendClient executeStatusGoRequestWithCallback:@"DeleteImportedKeyV2" + body:jsonParams + statusgoFunction:^NSString *{ + return StatusgoDeleteImportedKeyV2(jsonParams); + } + callback:callback]; } -//////////////////////////////////////////////////////////////////// -#pragma mark - NotifyUsers method -//////////////////////////////////////////////////////////////////// notifyUsers -RCT_EXPORT_METHOD(notifyUsers:(NSString *)message - payloadJSON:(NSString *)payloadJSON - tokensJSON:(NSString *)tokensJSON - callback:(RCTResponseSenderBlock)callback) { - char * result = NotifyUsers((char *) [message UTF8String], (char *) [payloadJSON UTF8String], (char *) [tokensJSON UTF8String]); - callback(@[[NSString stringWithUTF8String: result]]); -#if DEBUG - NSLog(@"NotifyUsers() method called"); -#endif -} +#pragma mark - GetNodeConfig -//////////////////////////////////////////////////////////////////// addPeer -RCT_EXPORT_METHOD(addPeer:(NSString *)enode - callback:(RCTResponseSenderBlock)callback) { - char * result = AddPeer((char *) [enode UTF8String]); - callback(@[[NSString stringWithUTF8String: result]]); +RCT_EXPORT_METHOD(getNodeConfig:(RCTResponseSenderBlock)callback) { #if DEBUG - NSLog(@"AddPeer() method called"); + NSLog(@"GetNodeConfig() method called"); #endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"GetNodeConfig" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoGetNodeConfig(); + } + callback:callback]; } -//////////////////////////////////////////////////////////////////// recoverAccount -RCT_EXPORT_METHOD(recoverAccount:(NSString *)passphrase - password:(NSString *)password - callback:(RCTResponseSenderBlock)callback) { +RCT_EXPORT_METHOD(intendedPanic:(NSString *)message) { #if DEBUG - NSLog(@"RecoverAccount() method called"); + NSLog(@"IntendedPanic() method called"); #endif - char * result = RecoverAccount((char *) [password UTF8String], (char *) [passphrase UTF8String]); - callback(@[[NSString stringWithUTF8String: result]]); + [StatusBackendClient executeStatusGoRequest:@"IntendedPanic" + body:message + statusgoFunction:^NSString *{ + return StatusgoIntendedPanic(message); + }]; } -//////////////////////////////////////////////////////////////////// login -RCT_EXPORT_METHOD(login:(NSString *)address - password:(NSString *)password - callback:(RCTResponseSenderBlock)callback) { -#if DEBUG - NSLog(@"Login() method called"); -#endif - char * result = Login((char *) [address UTF8String], (char *) [password UTF8String]); - callback(@[[NSString stringWithUTF8String: result]]); +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(fleets) { + return [StatusBackendClient executeStatusGoRequestWithResult:@"Fleets" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoFleets(); + }]; } -//////////////////////////////////////////////////////////////////// -#pragma mark - Approve Sign Request -//////////////////////////////////////////////////////////////////// approveSignRequests -RCT_EXPORT_METHOD(approveSignRequest:(NSString *)id - password:(NSString *)password - callback:(RCTResponseSenderBlock)callback) { -#if DEBUG - NSLog(@"ApproveSignRequest() method called"); -#endif - char * result = ApproveSignRequest((char *) [id UTF8String], (char *) [password UTF8String]); - callback(@[[NSString stringWithUTF8String: result]]); +RCT_EXPORT_METHOD(closeApplication) { + exit(0); } -//////////////////////////////////////////////////////////////////// -#pragma mark - Approve Sign Request With Args -//////////////////////////////////////////////////////////////////// approveSignRequestWithArgs -RCT_EXPORT_METHOD(approveSignRequestWithArgs:(NSString *)id - password:(NSString *)password - gas:(NSString *)gas - gasPrice:(NSString *)gasPrice - callback:(RCTResponseSenderBlock)callback) { +RCT_EXPORT_METHOD(connectionChange:(NSString *)type + isExpensive:(BOOL)isExpensive) { #if DEBUG - NSLog(@"ApproveSignRequestWithArgs() method called"); + NSLog(@"ConnectionChange() method called"); #endif - char * result = ApproveSignRequestWithArgs((char *) [id UTF8String], (char *) [password UTF8String], (long long) [gas longLongValue], (long long) [gasPrice longLongValue]); - callback(@[[NSString stringWithUTF8String: result]]); + NSDictionary *params = @{ + @"type": type, + @"expensive": @(isExpensive) + }; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params + options:0 + error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + + if (jsonData) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [StatusBackendClient executeStatusGoRequest:@"ConnectionChangeV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoConnectionChangeV2(jsonString); + }]; + } else { + NSLog(@"Failed to create JSON data"); + } } -//////////////////////////////////////////////////////////////////// -#pragma mark - Discard Sign Request -//////////////////////////////////////////////////////////////////// discardSignRequest -RCT_EXPORT_METHOD(discardSignRequest:(NSString *)id) { +RCT_EXPORT_METHOD(appStateChange:(NSString *)state) { #if DEBUG - NSLog(@"DiscardSignRequest() method called"); + NSLog(@"AppStateChange() method called"); #endif - DiscardSignRequest((char *) [id UTF8String]); + NSDictionary *params = @{@"state": state}; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params + options:0 + error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + + if (jsonData) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [StatusBackendClient executeStatusGoRequest:@"AppStateChangeV2" + body:jsonString + statusgoFunction:^NSString *{ + return StatusgoAppStateChangeV2(jsonString); + }]; + } else { + NSLog(@"Failed to create JSON data"); + } } -//////////////////////////////////////////////////////////////////// -#pragma mark - only android methods -//////////////////////////////////////////////////////////////////// -RCT_EXPORT_METHOD(setAdjustResize) { +RCT_EXPORT_METHOD(addCentralizedMetric:(NSString *)request + callback:(RCTResponseSenderBlock)callback) { #if DEBUG - NSLog(@"setAdjustResize() works only on Android"); + NSLog(@"addCentralizedMetric() method called"); #endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"AddCentralizedMetric" + body:request + statusgoFunction:^NSString *{ + return StatusgoAddCentralizedMetric(request); + } + callback:callback]; } -RCT_EXPORT_METHOD(setAdjustPan) { +RCT_EXPORT_METHOD(toggleCentralizedMetrics:(NSString *)request + callback:(RCTResponseSenderBlock)callback) { #if DEBUG - NSLog(@"setAdjustPan() works only on Android"); + NSLog(@"toggleCentralizedMetrics() method called"); #endif + [StatusBackendClient executeStatusGoRequestWithCallback:@"ToggleCentralizedMetrics" + body:request + statusgoFunction:^NSString *{ + return StatusgoToggleCentralizedMetrics(request); + } + callback:callback]; } -RCT_EXPORT_METHOD(setSoftInputMode: (NSInteger) i) { +RCT_EXPORT_METHOD(startLocalNotifications) { #if DEBUG - NSLog(@"setSoftInputMode() works only on Android"); + NSLog(@"StartLocalNotifications() method called"); #endif + [StatusBackendClient executeStatusGoRequest:@"StartLocalNotifications" + body:@"" + statusgoFunction:^NSString *{ + return StatusgoStartLocalNotifications(); + }]; } -RCT_EXPORT_METHOD(clearCookies) { - NSHTTPCookie *cookie; - NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) { - [storage deleteCookie:cookie]; - } -} - -RCT_EXPORT_METHOD(clearStorageAPIs) { - [[NSURLCache sharedURLCache] removeAllCachedResponses]; +#pragma mark - deviceinfo - NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; - NSArray *array = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil]; - for (NSString *string in array) { - NSLog(@"Removing %@", [path stringByAppendingPathComponent:string]); - if ([[string pathExtension] isEqualToString:@"localstorage"]) - [[NSFileManager defaultManager] removeItemAtPath:[path stringByAppendingPathComponent:string] error:nil]; - } +- (bool) is24Hour +{ + NSString *format = [NSDateFormatter dateFormatFromTemplate:@"j" options:0 locale:[NSLocale currentLocale]]; + return ([format rangeOfString:@"a"].location == NSNotFound); } -RCT_EXPORT_METHOD(sendWeb3Request:(NSString *)payload - callback:(RCTResponseSenderBlock)callback) { - dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - char * result = CallRPC((char *) [payload UTF8String]); - dispatch_async( dispatch_get_main_queue(), ^{ - callback(@[[NSString stringWithUTF8String: result]]); - }); - }); +- (NSString *)getBuildId { + return @"not available"; } -RCT_EXPORT_METHOD(sendWeb3PrivateRequest:(NSString *)payload - callback:(RCTResponseSenderBlock)callback) { - dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - char * result = CallPrivateRPC((char *) [payload UTF8String]); - dispatch_async( dispatch_get_main_queue(), ^{ - callback(@[[NSString stringWithUTF8String: result]]); - }); - }); -} +- (NSString*) deviceId +{ + struct utsname systemInfo; -RCT_EXPORT_METHOD(closeApplication) { - exit(0); -} + uname(&systemInfo); + NSString* deviceId = [NSString stringWithCString:systemInfo.machine + encoding:NSUTF8StringEncoding]; -RCT_EXPORT_METHOD(connectionChange:(NSString *)type - isExpensive:(BOOL)isExpensive) { -#if DEBUG - NSLog(@"ConnectionChange() method called"); -#endif - ConnectionChange((char *) [type UTF8String], isExpensive? 1 : 0); -} + if ([deviceId isEqualToString:@"i386"] || [deviceId isEqualToString:@"x86_64"] ) { + deviceId = [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")]; + } -RCT_EXPORT_METHOD(appStateChange:(NSString *)type) { -#if DEBUG - NSLog(@"AppStateChange() method called"); -#endif - AppStateChange((char *) [type UTF8String]); + return deviceId; } -RCT_EXPORT_METHOD(getDeviceUUID:(RCTResponseSenderBlock)callback) { -#if DEBUG - NSLog(@"getDeviceUUID() method called"); -#endif - NSString* Identifier = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; - - callback(@[Identifier]); +- (NSString*) deviceName +{ + return [[UIDevice currentDevice] name]; } -+ (void)signalEvent:(const char *) signal +- (NSDictionary *)constantsToExport { - if(!signal){ -#if DEBUG - NSLog(@"SignalEvent nil"); -#endif - return; - } - - NSString *sig = [NSString stringWithUTF8String:signal]; -#if DEBUG - NSLog(@"SignalEvent"); - NSLog(sig); -#endif - [bridge.eventDispatcher sendAppEventWithName:@"gethEvent" - body:@{@"jsonEvent": sig}]; + return @{ + @"is24Hour": @(self.is24Hour), + @"model": self.deviceName ?: [NSNull null], + @"brand": @"Apple", + @"buildId": [self getBuildId], + @"deviceId": self.deviceId ?: [NSNull null], + }; +} - return; ++ (BOOL)requiresMainQueueSetup +{ + return NO; } @end diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.xcodeproj/project.pbxproj b/modules/react-native-status/ios/RCTStatus/RCTStatus.xcodeproj/project.pbxproj index b5088210cd4..cc3cb7e6fe5 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.xcodeproj/project.pbxproj +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.xcodeproj/project.pbxproj @@ -3,14 +3,21 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 206C9F3E1D474E910063E3E6 /* RCTStatus.h in Copy Files */ = {isa = PBXBuildFile; fileRef = 206C9F3D1D474E910063E3E6 /* RCTStatus.h */; }; 206C9F401D474E910063E3E6 /* RCTStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 206C9F3F1D474E910063E3E6 /* RCTStatus.m */; }; - 9806BE77206D044D00548CC4 /* Instabug.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9806BE76206D044D00548CC4 /* Instabug.framework */; }; - CE4E31B11D86951A0033ED64 /* Statusgo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4E31B01D86951A0033ED64 /* Statusgo.framework */; }; + CE4E31B11D86951A0033ED64 /* Statusgo.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4E31B01D86951A0033ED64 /* Statusgo.xcframework */; }; + E92244EB2B485F2400915F4C /* UIHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = E92244E92B485F2400915F4C /* UIHelper.m */; }; + E967A3AC2B47BD5A00FB19B2 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = E967A3AB2B47BD5A00FB19B2 /* Utils.m */; }; + E9AEB5FE2BD8EEB100FB2926 /* MailManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9AEB5FD2BD8EEB100FB2926 /* MailManager.m */; }; + E9BEF3602B470BF1001F6755 /* NetworkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9BEF35E2B470BF1001F6755 /* NetworkManager.m */; }; + E9C33AA62B4828A60074B1C5 /* DatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9C33AA52B4828A60074B1C5 /* DatabaseManager.m */; }; + E9DB08932B4858B400F51053 /* LogManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9DB08912B4858B400F51053 /* LogManager.m */; }; + E9F5C3322B483B6C001A7F40 /* EncryptionUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E9F5C3312B483B6C001A7F40 /* EncryptionUtils.m */; }; + E9FC4ED12B47EEFF00E834DB /* AccountManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9FC4ECF2B47EEFF00E834DB /* AccountManager.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -31,8 +38,23 @@ 206C9F3A1D474E910063E3E6 /* libRCTStatus.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTStatus.a; sourceTree = BUILT_PRODUCTS_DIR; }; 206C9F3D1D474E910063E3E6 /* RCTStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTStatus.h; sourceTree = ""; }; 206C9F3F1D474E910063E3E6 /* RCTStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTStatus.m; sourceTree = ""; }; - 9806BE76206D044D00548CC4 /* Instabug.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Instabug.framework; path = "../../../../node_modules/instabug-reactnative/ios/Instabug.framework"; sourceTree = ""; }; - CE4E31B01D86951A0033ED64 /* Statusgo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Statusgo.framework; sourceTree = ""; }; + CE4E31B01D86951A0033ED64 /* Statusgo.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Statusgo.xcframework; sourceTree = ""; }; + E92244E92B485F2400915F4C /* UIHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIHelper.m; sourceTree = ""; }; + E92244EA2B485F2400915F4C /* UIHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIHelper.h; sourceTree = ""; }; + E967A3AA2B47BD5A00FB19B2 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = ""; }; + E967A3AB2B47BD5A00FB19B2 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Utils.m; sourceTree = ""; }; + E9AEB5FC2BD8EEB100FB2926 /* MailManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MailManager.h; sourceTree = ""; }; + E9AEB5FD2BD8EEB100FB2926 /* MailManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MailManager.m; sourceTree = ""; }; + E9BEF35E2B470BF1001F6755 /* NetworkManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NetworkManager.m; sourceTree = ""; }; + E9BEF35F2B470BF1001F6755 /* NetworkManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetworkManager.h; sourceTree = ""; }; + E9C33AA42B4828A60074B1C5 /* DatabaseManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DatabaseManager.h; sourceTree = ""; }; + E9C33AA52B4828A60074B1C5 /* DatabaseManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DatabaseManager.m; sourceTree = ""; }; + E9DB08912B4858B400F51053 /* LogManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LogManager.m; sourceTree = ""; }; + E9DB08922B4858B400F51053 /* LogManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LogManager.h; sourceTree = ""; }; + E9F5C3302B483B6C001A7F40 /* EncryptionUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EncryptionUtils.h; sourceTree = ""; }; + E9F5C3312B483B6C001A7F40 /* EncryptionUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EncryptionUtils.m; sourceTree = ""; }; + E9FC4ECF2B47EEFF00E834DB /* AccountManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AccountManager.m; sourceTree = ""; }; + E9FC4ED02B47EEFF00E834DB /* AccountManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AccountManager.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -40,8 +62,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CE4E31B11D86951A0033ED64 /* Statusgo.framework in Frameworks */, - 9806BE77206D044D00548CC4 /* Instabug.framework in Frameworks */, + CE4E31B11D86951A0033ED64 /* Statusgo.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -51,10 +72,9 @@ 206C9F311D474E910063E3E6 = { isa = PBXGroup; children = ( - CE4E31B01D86951A0033ED64 /* Statusgo.framework */, + CE4E31B01D86951A0033ED64 /* Statusgo.xcframework */, 206C9F3C1D474E910063E3E6 /* Status */, 206C9F3B1D474E910063E3E6 /* Products */, - 9E3F8AF11ED2CCBD0016D874 /* Frameworks */, ); sourceTree = ""; }; @@ -69,20 +89,28 @@ 206C9F3C1D474E910063E3E6 /* Status */ = { isa = PBXGroup; children = ( + E9AEB5FC2BD8EEB100FB2926 /* MailManager.h */, + E9AEB5FD2BD8EEB100FB2926 /* MailManager.m */, + E92244EA2B485F2400915F4C /* UIHelper.h */, + E92244E92B485F2400915F4C /* UIHelper.m */, + E9DB08922B4858B400F51053 /* LogManager.h */, + E9DB08912B4858B400F51053 /* LogManager.m */, + E9F5C3302B483B6C001A7F40 /* EncryptionUtils.h */, + E9F5C3312B483B6C001A7F40 /* EncryptionUtils.m */, + E9C33AA42B4828A60074B1C5 /* DatabaseManager.h */, + E9C33AA52B4828A60074B1C5 /* DatabaseManager.m */, + E9FC4ED02B47EEFF00E834DB /* AccountManager.h */, + E9FC4ECF2B47EEFF00E834DB /* AccountManager.m */, + E967A3AA2B47BD5A00FB19B2 /* Utils.h */, + E967A3AB2B47BD5A00FB19B2 /* Utils.m */, + E9BEF35F2B470BF1001F6755 /* NetworkManager.h */, + E9BEF35E2B470BF1001F6755 /* NetworkManager.m */, 206C9F3D1D474E910063E3E6 /* RCTStatus.h */, 206C9F3F1D474E910063E3E6 /* RCTStatus.m */, ); name = Status; sourceTree = ""; }; - 9E3F8AF11ED2CCBD0016D874 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 9806BE76206D044D00548CC4 /* Instabug.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -109,7 +137,7 @@ 206C9F321D474E910063E3E6 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0730; + LastUpgradeCheck = 1140; ORGANIZATIONNAME = Status.im; TargetAttributes = { 206C9F391D474E910063E3E6 = { @@ -119,7 +147,7 @@ }; buildConfigurationList = 206C9F351D474E910063E3E6 /* Build configuration list for PBXProject "RCTStatus" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -139,7 +167,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E9DB08932B4858B400F51053 /* LogManager.m in Sources */, + E9AEB5FE2BD8EEB100FB2926 /* MailManager.m in Sources */, + E9F5C3322B483B6C001A7F40 /* EncryptionUtils.m in Sources */, + E967A3AC2B47BD5A00FB19B2 /* Utils.m in Sources */, + E92244EB2B485F2400915F4C /* UIHelper.m in Sources */, + E9BEF3602B470BF1001F6755 /* NetworkManager.m in Sources */, 206C9F401D474E910063E3E6 /* RCTStatus.m in Sources */, + E9C33AA62B4828A60074B1C5 /* DatabaseManager.m in Sources */, + E9FC4ED12B47EEFF00E834DB /* AccountManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -150,18 +186,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -183,6 +230,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "$(SRCROOT)/../../../../ios/Pods/Headers/Public/SSZipArchive"; IPHONEOS_DEPLOYMENT_TARGET = 9.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -194,18 +242,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -221,8 +280,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "$(SRCROOT)/../../../../ios/Pods/Headers/Public/SSZipArchive"; IPHONEOS_DEPLOYMENT_TARGET = 9.3; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; VALID_ARCHS = "armv7 armv7s arm64"; @@ -233,21 +294,20 @@ 206C9F441D474E910063E3E6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", - "$(PROJECT_DIR)/../../../../ios/Pods/Instabug", ); FRAMEWORK_VERSION = A; HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../../../node_modules/React/**", + "$(SRCROOT)/../../../../ios/Pods/Headers/Public/React-Core", "$(SRCROOT)/../../../../node_modules/react-native/React/**", "$(SRCROOT)/../../../../node_modules/react-native-config/ios/ReactNativeConfig", ); - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "-ObjC", @@ -255,29 +315,29 @@ ); PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - VALID_ARCHS = "armv7 armv7s arm64"; + VALID_ARCHS = "armv7 armv7s arm64 "; + "VALID_ARCHS[sdk=iphonesimulator14.3]" = "armv7 armv7s x86_64"; }; name = Debug; }; 206C9F451D474E910063E3E6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", - "$(PROJECT_DIR)/../../../../ios/Pods/Instabug", ); FRAMEWORK_VERSION = A; HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../../../node_modules/React/**", + "$(SRCROOT)/../../../../ios/Pods/Headers/Public/React-Core", "$(SRCROOT)/../../../../node_modules/react-native/React/**", "$(SRCROOT)/../../../../node_modules/react-native-config/ios/ReactNativeConfig", ); - IPHONEOS_DEPLOYMENT_TARGET = 7.0; - ONLY_ACTIVE_ARCH = NO; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "-ObjC", "$(inherited)", diff --git a/modules/react-native-status/ios/RCTStatus/StatusBackendClient.h b/modules/react-native-status/ios/RCTStatus/StatusBackendClient.h new file mode 100644 index 00000000000..6a4bee04ea9 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/StatusBackendClient.h @@ -0,0 +1,31 @@ +#import +#import + +@interface StatusBackendClient : NSObject + +@property (nonatomic) BOOL serverEnabled; +@property (nonatomic, strong) NSString *statusGoEndpoint; +@property (nonatomic, strong) NSString *signalEndpoint; +@property (nonatomic, strong) NSString *rootDataDir; +@property (nonatomic, strong) NSURLSessionWebSocketTask *webSocket; + +// Add sharedInstance class method declaration ++ (instancetype)sharedInstance; +- (void)request:(NSString *)endpoint + body:(NSString *)body + callback:(void (^)(NSString *response, NSError *error))callback; + ++ (void)executeStatusGoRequest:(NSString *)endpoint + body:(NSString *)body + statusgoFunction:(NSString * (^)(void))statusgoFunction; + ++ (void)executeStatusGoRequestWithCallback:(NSString *)endpoint + body:(NSString *)body + statusgoFunction:(NSString * (^)(void))statusgoFunction + callback:(RCTResponseSenderBlock)callback; + ++ (NSString *)executeStatusGoRequestWithResult:(NSString *)endpoint + body:(NSString *)body + statusgoFunction:(NSString * (^)(void))statusgoFunction; + +@end diff --git a/modules/react-native-status/ios/RCTStatus/StatusBackendClient.m b/modules/react-native-status/ios/RCTStatus/StatusBackendClient.m new file mode 100644 index 00000000000..ab7329f6be4 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/StatusBackendClient.m @@ -0,0 +1,279 @@ +#import "StatusBackendClient.h" +#import "RCTStatus.h" +#import "Utils.h" + +@implementation StatusBackendClient { + NSURLSessionWebSocketTask *_webSocket; + BOOL _serverEnabled; + NSString *_statusGoEndpoint; + NSString *_signalEndpoint; + NSString *_rootDataDir; + NSTimer *_pingTimer; +} + +RCT_EXPORT_MODULE(); + ++ (BOOL)requiresMainQueueSetup { + return YES; +} + ++ (instancetype)allocWithZone:(NSZone *)zone { + static StatusBackendClient *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [super allocWithZone:zone]; + }); + return sharedInstance; +} + ++ (instancetype)sharedInstance { + return [[self alloc] init]; +} + +- (instancetype)init { + static StatusBackendClient *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [super init]; + if (sharedInstance) { + sharedInstance->_serverEnabled = NO; + sharedInstance->_statusGoEndpoint = nil; + sharedInstance->_signalEndpoint = nil; + sharedInstance->_rootDataDir = nil; + sharedInstance->_webSocket = nil; + } + }); + return sharedInstance; +} + +- (id)copyWithZone:(NSZone *)zone { + return self; +} + +- (BOOL)serverEnabled { + return _serverEnabled; +} + +- (NSString *)statusGoEndpoint { + return _statusGoEndpoint; +} + +- (NSString *)signalEndpoint { + return _signalEndpoint; +} + +- (NSString *)rootDataDir { + return _rootDataDir; +} + +- (void)connectWebSocket { + if (!self.serverEnabled || !self.signalEndpoint) { + return; + } + + NSString *fullUrl = [NSString stringWithFormat:@"%@", self.signalEndpoint]; + NSURL *url = [NSURL URLWithString:fullUrl]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + self.webSocket = [session webSocketTaskWithURL:url]; + + [self.webSocket resume]; + [self receiveMessage]; + + [self startPingTimer]; +} + +- (void)receiveMessage { + __weak typeof(self) weakSelf = self; + [self.webSocket receiveMessageWithCompletionHandler:^(NSURLSessionWebSocketMessage *message, NSError *error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf || !strongSelf.webSocket) { + return; + } + + if (error) { + NSLog(@"WebSocket error: %@", error); + // Attempt to reconnect after error + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [strongSelf connectWebSocket]; + }); + return; + } + + if (message) { + if (message.type == NSURLSessionWebSocketMessageTypeString && message.string) { + [[Status sharedInstance] handleSignal:message.string]; + } + // Continue receiving messages only if the connection is still active + if (strongSelf.webSocket) { + [strongSelf receiveMessage]; + } + } + }]; +} + +- (void)disconnectWebSocket { + [self stopPingTimer]; + if (self.webSocket) { + [self.webSocket cancel]; + self.webSocket = nil; + } +} + +- (void)request:(NSString *)endpoint + body:(NSString *)body + callback:(void (^)(NSString *response, NSError *error))callback { + NSString *fullUrlString = [NSString stringWithFormat:@"%@%@", self.statusGoEndpoint, endpoint]; + NSLog(@"StatusGo server is enabled, executing request, endpoint: %@, body: %@", fullUrlString, body); + NSURL *url = [NSURL URLWithString:fullUrlString]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; + + NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding]; + request.HTTPBody = bodyData; + + NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + NSLog(@"request error: %@", error); + callback(nil, error); + return; + } + + NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + callback(responseString, nil); + }]; + + [task resume]; +} + +RCT_EXPORT_METHOD(configStatusBackendServer:(BOOL)serverEnabled + statusGoEndpoint:(NSString *)statusGoEndpoint + signalEndpoint:(NSString *)signalEndpoint + rootDataDir:(NSString *)rootDataDir) { + [self configureWithEnabled:serverEnabled + statusGoEndpoint:statusGoEndpoint + signalEndpoint:signalEndpoint + rootDataDir:rootDataDir]; +} + +- (void)configureWithEnabled:(BOOL)serverEnabled + statusGoEndpoint:(NSString *)statusGoEndpoint + signalEndpoint:(NSString *)signalEndpoint + rootDataDir:(NSString *)rootDataDir { + _serverEnabled = serverEnabled; + + if (serverEnabled) { + _statusGoEndpoint = statusGoEndpoint; + _signalEndpoint = signalEndpoint; + _rootDataDir = rootDataDir; + [self connectWebSocket]; + } else { + [self disconnectWebSocket]; + _statusGoEndpoint = nil; + _signalEndpoint = nil; + _rootDataDir = nil; + } +} + ++ (void)executeStatusGoRequest:(NSString *)endpoint + body:(NSString *)body + statusgoFunction:(NSString * (^)(void))statusgoFunction { + StatusBackendClient *client = [StatusBackendClient sharedInstance]; + if (client.serverEnabled) { + [client request:endpoint body:body callback:^(NSString *response, NSError *error) { + [Utils handleStatusGoResponse:response source:endpoint error:error]; + }]; + } else { + NSString *result = statusgoFunction(); + [Utils handleStatusGoResponse:result source:endpoint error:nil]; + } +} + ++ (void)executeStatusGoRequestWithCallback:(NSString *)endpoint + body:(NSString *)body + statusgoFunction:(NSString * (^)(void))statusgoFunction + callback:(RCTResponseSenderBlock)callback { + StatusBackendClient *client = [StatusBackendClient sharedInstance]; + if (client.serverEnabled) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [client request:endpoint body:body callback:^(NSString *response, NSError *error) { + if (error) { + NSLog(@"request to %@ failed: %@", endpoint, error); + if (callback) { + callback(@[@(NO)]); + } + } else { + if (callback) { + callback(@[response]); + } + } + }]; + }); + } else { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *result = statusgoFunction(); + if (callback) { + callback(@[result]); + } + }); + } +} + ++ (NSString *)executeStatusGoRequestWithResult:(NSString *)endpoint + body:(NSString *)body + statusgoFunction:(NSString * (^)(void))statusgoFunction { + StatusBackendClient *client = [StatusBackendClient sharedInstance]; + if (client.serverEnabled) { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block NSString *resultString = @""; + [client request:endpoint body:body callback:^(NSString *response, NSError *error) { + if (error) { + NSLog(@"request to %@ failed: %@", endpoint, error); + resultString = @""; + } else { + resultString = response; + } + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + return resultString; + } else { + return statusgoFunction(); + } +} + +- (void)startPingTimer { + [self stopPingTimer]; + _pingTimer = [NSTimer scheduledTimerWithTimeInterval:30.0 + target:self + selector:@selector(sendPing) + userInfo:nil + repeats:YES]; +} + +- (void)stopPingTimer { + if (_pingTimer) { + [_pingTimer invalidate]; + _pingTimer = nil; + } +} + +- (void)sendPing { + if (self.webSocket) { + [self.webSocket sendPingWithPongReceiveHandler:^(NSError * _Nullable error) { + if (error) { + NSLog(@"WebSocket ping failed: %@", error); + // Reconnect on ping failure + [self disconnectWebSocket]; + [self connectWebSocket]; + } + }]; + } +} + +@end diff --git a/modules/react-native-status/ios/RCTStatus/UIHelper.h b/modules/react-native-status/ios/RCTStatus/UIHelper.h new file mode 100644 index 00000000000..2f5b16284d0 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/UIHelper.h @@ -0,0 +1,8 @@ +#import +#import +#import +#import "RCTLog.h" + +@interface UIHelper : NSObject + +@end diff --git a/modules/react-native-status/ios/RCTStatus/UIHelper.m b/modules/react-native-status/ios/RCTStatus/UIHelper.m new file mode 100644 index 00000000000..00e8ee0862f --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/UIHelper.m @@ -0,0 +1,39 @@ +#import "UIHelper.h" +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" + +@implementation UIHelper + +RCT_EXPORT_MODULE(); + +#pragma mark - only android methods + +RCT_EXPORT_METHOD(setSoftInputMode: (NSInteger) i) { +#if DEBUG + NSLog(@"setSoftInputMode() works only on Android"); +#endif +} + +RCT_EXPORT_METHOD(clearCookies) { + NSHTTPCookie *cookie; + NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + + for (cookie in [storage cookies]) { + [storage deleteCookie:cookie]; + } +} + +RCT_EXPORT_METHOD(clearStorageAPIs) { + [[NSURLCache sharedURLCache] removeAllCachedResponses]; + + NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; + NSArray *array = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil]; + for (NSString *string in array) { + NSLog(@"Removing %@", [path stringByAppendingPathComponent:string]); + if ([[string pathExtension] isEqualToString:@"localstorage"]) + [[NSFileManager defaultManager] removeItemAtPath:[path stringByAppendingPathComponent:string] error:nil]; + } +} + + +@end diff --git a/modules/react-native-status/ios/RCTStatus/Utils.h b/modules/react-native-status/ios/RCTStatus/Utils.h new file mode 100644 index 00000000000..9bca89d9ff5 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/Utils.h @@ -0,0 +1,18 @@ +#import +#import +#import +#import "Statusgo.h" +#import "RCTLog.h" + +@interface Utils : NSObject + ++ (NSURL *)getRootUrl; ++ (NSString *)jsonStringWithPrettyPrint:(BOOL)prettyPrint fromDictionary:(NSDictionary *)dictionary; ++ (NSString *)jsonStringWithPrettyPrint:(BOOL)prettyPrint fromArray:(NSArray *)array; ++ (NSURL *)getKeyStoreDirForKeyUID:(NSString *)keyUID; ++ (NSString *)getExportDbFilePath; ++ (NSString *)getKeyUID:(NSString *)jsonString; ++ (void)migrateKeystore:(NSString *)accountData password:(NSString *)password; ++ (void)handleStatusGoResponse:(NSString *)response source:(NSString *)source error:(NSError *)error; + +@end diff --git a/modules/react-native-status/ios/RCTStatus/Utils.m b/modules/react-native-status/ios/RCTStatus/Utils.m new file mode 100644 index 00000000000..3c6e63537d1 --- /dev/null +++ b/modules/react-native-status/ios/RCTStatus/Utils.m @@ -0,0 +1,204 @@ +#import "Utils.h" +#import "React/RCTBridge.h" +#import "React/RCTEventDispatcher.h" +#import "Statusgo.h" +#import "Utils.h" +#import "StatusBackendClient.h" + +@implementation Utils + +RCT_EXPORT_MODULE(); + +#pragma mark - Private Methods + ++ (NSURL *)getRootUrl { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl; + + StatusBackendClient *client = [StatusBackendClient sharedInstance]; + if (client.serverEnabled && client.rootDataDir) { + rootUrl = [NSURL fileURLWithPath:client.rootDataDir]; + } else { + rootUrl = [[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject]; + } + + return rootUrl; +} + ++ (NSString *)jsonStringWithPrettyPrint:(BOOL)prettyPrint fromDictionary:(NSDictionary *)dictionary { + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary + options:(NSJSONWritingOptions)(prettyPrint ? NSJSONWritingPrettyPrinted : 0) + error:&error]; + + if (!jsonData) { + NSLog(@"jsonStringWithPrettyPrint: error: %@", error.localizedDescription); + return @"{}"; + } else { + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } +} + ++ (NSString *)jsonStringWithPrettyPrint:(BOOL)prettyPrint fromArray:(NSArray *)array { + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:array + options:(NSJSONWritingOptions)(prettyPrint ? NSJSONWritingPrettyPrinted : 0) + error:&error]; + + if (!jsonData) { + NSLog(@"jsonStringWithPrettyPrint: error: %@", error.localizedDescription); + return @"[]"; + } else { + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } +} + ++ (NSURL *)getKeyStoreDirForKeyUID:(NSString *)keyUID { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl = [self getRootUrl]; + + NSURL *oldKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"]; + NSURL *multiaccountKeystoreDir = [oldKeystoreDir URLByAppendingPathComponent:keyUID]; + + return multiaccountKeystoreDir; +} + ++ (NSString *) getKeyUID:(NSString *)jsonString { + NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization + JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:nil]; + + return [json valueForKey:@"key-uid"]; +} + ++ (NSString *) getExportDbFilePath { + StatusBackendClient *client = [StatusBackendClient sharedInstance]; + if (client.serverEnabled && client.rootDataDir) { + return [client.rootDataDir stringByAppendingPathComponent:@"export.db"]; + } + + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"export.db"]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + if ([fileManager fileExistsAtPath:filePath]) { + [fileManager removeItemAtPath:filePath error:nil]; + } + + return filePath; +} + ++ (void) migrateKeystore:(NSString *)accountData + password:(NSString *)password { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl =[self getRootUrl]; + + NSData *jsonData = [accountData dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSDictionary *accountJson = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + if (error) { + NSLog(@"Error parsing accountData: %@", error); + return; + } + + NSString *keyUID = [self getKeyUID:accountData]; + NSURL *oldKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"]; + NSURL *multiaccountKeystoreDir = [self getKeyStoreDirForKeyUID:keyUID]; + + NSArray *keys = [fileManager contentsOfDirectoryAtPath:multiaccountKeystoreDir.path error:nil]; + if (keys.count == 0) { + NSDictionary *params = @{ + @"account": accountJson, + @"password": password, + @"oldDir": oldKeystoreDir.path, + @"newDir": multiaccountKeystoreDir.path + }; + NSData *paramsJsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating params JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:paramsJsonData encoding:NSUTF8StringEncoding]; + + NSString *migrationResult = StatusgoMigrateKeyStoreDirV2(jsonString); + NSLog(@"keystore migration result %@", migrationResult); + + NSString *initKeystoreResult = StatusgoInitKeystore(multiaccountKeystoreDir.path); + NSLog(@"InitKeyStore result %@", initKeystoreResult); + } +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(backupDisabledDataDir) { + StatusBackendClient *client = [StatusBackendClient sharedInstance]; + if (client.serverEnabled && client.rootDataDir) { + return client.rootDataDir; + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl = [Utils getRootUrl]; + return rootUrl.path; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getMMKVStoragePath) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl = [[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject]; + return [rootUrl.path stringByAppendingPathComponent:@"mmkv"]; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(keystoreDir) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootUrl =[Utils getRootUrl]; + + NSURL *commonKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"]; + + return commonKeystoreDir.path; +} + +RCT_EXPORT_METHOD(validateMnemonic:(NSString *)seed + callback:(RCTResponseSenderBlock)callback) { + #if DEBUG + NSLog(@"validateMnemonicV2() method called"); + #endif + NSDictionary *params = @{@"mnemonic": seed}; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error]; + if (error) { + NSLog(@"Error creating JSON: %@", error); + return; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + NSString *result = StatusgoValidateMnemonicV2(jsonString); + callback(@[result]); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(checkAddressChecksum:(NSString *)address) { + return StatusgoCheckAddressChecksum(address); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isAddress:(NSString *)address) { + return StatusgoIsAddress(address); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(toChecksumAddress:(NSString *)address) { + return StatusgoToChecksumAddress(address); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(validateConnectionString:(NSString *)cs) { + return StatusgoValidateConnectionString(cs); +} + ++ (void)handleStatusGoResponse:(NSString *)response source:(NSString *)source error:(NSError *)error { + if (error) { + NSLog(@"%@ failed: %@", source, error); + return; + } + + if ([response hasPrefix:@"{\"error\":\"\""]) { + NSLog(@"%@ success: %@", source, response); + } else { + NSLog(@"%@ failed: %@", source, response); + } +} + +@end diff --git a/modules/react-native-status/ios/RCTStatus/pom.xml b/modules/react-native-status/ios/RCTStatus/pom.xml deleted file mode 100644 index 03629fb6772..00000000000 --- a/modules/react-native-status/ios/RCTStatus/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - 4.0.0 - - org.reactjs.native.example - Messenger - 1.0-SNAPSHOT - - - - ci.status.im - http://139.162.11.12:8081/artifactory/libs-release-local/ - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 2.10 - - - - status-im - status-go-ios-simulator - develop-g03bf6e37 - zip - true - ./ - - - - - - - diff --git a/modules/react-native-status/nodejs/bindings.js b/modules/react-native-status/nodejs/bindings.js new file mode 100644 index 00000000000..072c96855d5 --- /dev/null +++ b/modules/react-native-status/nodejs/bindings.js @@ -0,0 +1,5 @@ +var binary = require('@mapbox/node-pre-gyp'); +var path = require('path'); +var binding_path = binary.find(path.resolve(path.join(__dirname, './../../../package.json'))); + +module.exports = require(binding_path); diff --git a/modules/react-native-status/nodejs/status.cpp b/modules/react-native-status/nodejs/status.cpp new file mode 100644 index 00000000000..18d2a411d33 --- /dev/null +++ b/modules/react-native-status/nodejs/status.cpp @@ -0,0 +1,1878 @@ +// This code has pieces that were autogenerated but is now managed manually +// Here lies the node bindings used for integration tests +// These are basically C calls to exported status-go functions +// Along 1 additional Poll function to get signals to work +// function names registered here in init() are used in various places +// ref -> status-mobile/src/tests/test_utils.cljs + +#include +#include +#include + +#include "../../../result/libstatus.h" + +namespace status { + +using v8::Exception; +using v8::FunctionCallbackInfo; +using v8::Function; +using v8::Persistent; +using v8::Context; +using v8::NewStringType; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Number; +using v8::Value; + + +void _MultiAccountGenerateAndDeriveAddresses(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountGenerateAndDeriveAddresses"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountGenerateAndDeriveAddresses(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _MultiAccountImportPrivateKey(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountImportPrivateKey"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountImportPrivateKey(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _MultiAccountLoadAccount(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountLoadAccount"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountLoadAccount(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _MultiAccountReset(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + if (args.Length() != 0) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountReset"))); + return; + } + + // Check the argument types + + + // Call exported Go function, which returns a C string + char *c = MultiAccountReset(); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _MultiAccountDeriveAddresses(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountDeriveAddresses"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountDeriveAddresses(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _MultiAccountImportMnemonic(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountImportMnemonic"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountImportMnemonic(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _MultiAccountGenerate(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountGenerate"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountGenerate(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _MultiAccountStoreDerivedAccounts(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountStoreDerivedAccounts"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountStoreDerivedAccounts(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _MultiAccountStoreAccount(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiAccountStoreAccount"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = MultiAccountStoreAccount(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _InitializeApplication(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for InitializeApplication"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for request"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = InitializeApplication(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _InitKeystore(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for InitKeystore"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'keydir'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = InitKeystore(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _StopCPUProfiling(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + if (args.Length() != 0) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for StopCPUProfiling"))); + return; + } + + // Check the argument types + + + + + // Call exported Go function, which returns a C string + char *c = StopCPUProfiling(); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _DecodeParameters(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for DecodeParameters"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'decodeParamJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = DecodeParameters(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _HexToNumber(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for HexToNumber"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'hex'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = HexToNumber(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _NumberToHex(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for NumberToHex"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'numString'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = NumberToHex(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _CheckAddressChecksum(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for CheckAddressChecksum"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'address'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = CheckAddressChecksum(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _Fleets(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + // Call exported Go function, which returns a C string + char *c = Fleets(); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + + +void _IsAddress(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for IsAddress"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'address'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = IsAddress(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _Sha3(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for Sha3"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'str'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = Sha3(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _SerializeLegacyKey(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SerializeLegacyKey"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'str'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = SerializeLegacyKey(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _ToChecksumAddress(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for ToChecksumAddress"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'address'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = ToChecksumAddress(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _Logout(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + if (args.Length() != 0) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for Logout"))); + return; + } + + // Check the argument types + + + + + // Call exported Go function, which returns a C string + char *c = Logout(); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _AcceptTerms(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + if (args.Length() != 0) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for AcceptTerms"))); + return; + } + + // Check the argument types + + // Call exported Go function, which returns a C string + char *c = AcceptTerms(); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _HashMessage(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for HashMessage"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'message'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = HashMessage(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _ResetChainData(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + if (args.Length() != 0) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for ResetChainData"))); + return; + } + + // Check the argument types + + + + + // Call exported Go function, which returns a C string + char *c = ResetChainData(); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _SaveAccountAndLogin(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 5) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SaveAccountAndLogin"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'accountData'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'password'"))); + return; + } + if (!args[2]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'settingsJSON'"))); + return; + } + if (!args[3]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'configJSON'"))); + return; + } + if (!args[4]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'subaccountData'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + String::Utf8Value arg2Obj(isolate, args[2]->ToString(context).ToLocalChecked()); + char *arg2 = *arg2Obj; + String::Utf8Value arg3Obj(isolate, args[3]->ToString(context).ToLocalChecked()); + char *arg3 = *arg3Obj; + String::Utf8Value arg4Obj(isolate, args[4]->ToString(context).ToLocalChecked()); + char *arg4 = *arg4Obj; + + // Call exported Go function, which returns a C string + char *c = SaveAccountAndLogin(arg0, arg1, arg2, arg3, arg4); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _CreateAccountAndLogin(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SaveAccountAndLogin"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'settingsJSON'"))); + return; + } + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = CreateAccountAndLogin(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _ValidateMnemonic(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for ValidateMnemonic"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'mnemonic'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = ValidateMnemonic(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _MultiformatSerializePublicKey(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 2) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiformatSerializePublicKey"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'key'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'outBase'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + + // Call exported Go function, which returns a C string + char *c = MultiformatSerializePublicKey(arg0, arg1); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + + +void _SaveAccountAndLoginWithKeycard(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 6) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SaveAccountAndLoginWithKeycard"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'accountData'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'password'"))); + return; + } + if (!args[2]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'settingsJSON'"))); + return; + } + if (!args[3]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'configJSON'"))); + return; + } + if (!args[4]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'subaccountData'"))); + return; + } + if (!args[5]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'keyHex'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + String::Utf8Value arg2Obj(isolate, args[2]->ToString(context).ToLocalChecked()); + char *arg2 = *arg2Obj; + String::Utf8Value arg3Obj(isolate, args[3]->ToString(context).ToLocalChecked()); + char *arg3 = *arg3Obj; + String::Utf8Value arg4Obj(isolate, args[4]->ToString(context).ToLocalChecked()); + char *arg4 = *arg4Obj; + String::Utf8Value arg5Obj(isolate, args[5]->ToString(context).ToLocalChecked()); + char *arg5 = *arg5Obj; + + // Call exported Go function, which returns a C string + char *c = SaveAccountAndLoginWithKeycard(arg0, arg1, arg2, arg3, arg4, arg5); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _LoginWithKeycard(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 4) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for LoginWithKeycard"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'accountData'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'password'"))); + return; + } + if (!args[2]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'keyHex'"))); + return; + } + if (!args[3]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'nodeConfigJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + String::Utf8Value arg2Obj(isolate, args[2]->ToString(context).ToLocalChecked()); + char *arg2 = *arg2Obj; + String::Utf8Value arg3Obj(isolate, args[3]->ToString(context).ToLocalChecked()); + char *arg3 = *arg3Obj; + + // Call exported Go function, which returns a C string + char *c = LoginWithKeycard(arg0, arg1, arg2, arg3); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _MultiformatDeserializePublicKey(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 2) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for MultiformatDeserializePublicKey"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'key'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'outBase'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + + // Call exported Go function, which returns a C string + char *c = MultiformatDeserializePublicKey(arg0, arg1); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _Login(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 2) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for Login"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'accountData'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'password'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + + // Call exported Go function, which returns a C string + char *c = Login(arg0, arg1); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +Local runCallbackCB; +Local runCallbackContext; +Isolate* runCallbackIsolate; + +Persistent r_call; + +std::queue q; + +void run(char *json) { + std::string str(json); + q.push(str); +} + +// Poll signals and process queue, one at a time +void _PollSignal(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SetSignalEventCallback"))); + return; + } + + + v8::Local func = v8::Local::Cast(args[0]); + if (!q.empty()) { + const unsigned argc = 1; + v8::Local argv[argc] = {v8::String::NewFromUtf8(isolate, q.front().c_str()).ToLocalChecked()}; + v8::MaybeLocal result = func->Call(isolate->GetCurrentContext(), v8::Null(isolate), argc, argv); + if (result.IsEmpty()) { + isolate->ThrowException(Exception::Error( + String::NewFromUtf8Literal(isolate, "Error calling the callback function"))); + return; + } + q.pop(); + } +} + +void _SetSignalEventCallback(const FunctionCallbackInfo& args) { + SetSignalEventCallback((void *)&run); +} + + + +void _StartCPUProfile(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for StartCPUProfile"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'dataDir'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = StartCPUProfile(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _OpenAccounts(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for OpenAccounts"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'datadir'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = OpenAccounts(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _ExtractGroupMembershipSignatures(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for ExtractGroupMembershipSignatures"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'signaturePairsStr'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = ExtractGroupMembershipSignatures(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _CallPrivateRPC(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for CallPrivateRPC"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'inputJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = CallPrivateRPC(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + +} + +void _SendTransactionWithSignature(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 2) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SendTransactionWithSignature"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'txArgsJSON'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'sigString'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + + // Call exported Go function, which returns a C string + char *c = SendTransactionWithSignature(arg0, arg1); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _WriteHeapProfile(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for WriteHeapProfile"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'dataDir'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = WriteHeapProfile(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _SignHash(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SignHash"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'hexEncodedHash'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = SignHash(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _SignGroupMembership(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SignGroupMembership"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'content'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = SignGroupMembership(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _CallRPC(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for CallRPC"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'inputJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = CallRPC(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _SignMessage(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SignMessage"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'rpcParams'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = SignMessage(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _SignTypedData(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 3) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SignTypedData"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'data'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'address'"))); + return; + } + if (!args[2]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'password'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + String::Utf8Value arg2Obj(isolate, args[2]->ToString(context).ToLocalChecked()); + char *arg2 = *arg2Obj; + + // Call exported Go function, which returns a C string + char *c = SignTypedData(arg0, arg1, arg2); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _SendTransaction(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 2) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for SendTransaction"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'txArgsJSON'"))); + return; + } + if (!args[1]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'password'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked()); + char *arg1 = *arg1Obj; + + // Call exported Go function, which returns a C string + char *c = SendTransaction(arg0, arg1); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _AppStateChange(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for AppStateChange"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'state'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + AppStateChange(arg0); + + + + +} + +void _ValidateNodeConfig(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for ValidateNodeConfig"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'configJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = ValidateNodeConfig(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _HashTypedData(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for HashTypedData"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'data'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = HashTypedData(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _Recover(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for Recover"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'rpcParams'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = Recover(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; + + +} + +void _HashTransaction(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for HashTransaction"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'txArgsJSON'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = HashTransaction(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + +void _ConnectionChange(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 2) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for ConnectionChange"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'typ'"))); + return; + } + if (!args[1]->IsNumber()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'expensive'"))); + return; + } + + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + int arg1 = args[1]->Int32Value(context).ToChecked(); + + // Call exported Go function, which returns a C string + ConnectionChange(arg0, arg1); +} + +void _RestoreAccountAndLogin(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong number of arguments for RestoreAccountAndLogin"))); + return; + } + + // Check the argument types + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8Literal(isolate, "Wrong argument type for 'RestoreAccountAndLogin'"))); + return; + } + + String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked()); + char *arg0 = *arg0Obj; + + // Call exported Go function, which returns a C string + char *c = RestoreAccountAndLogin(arg0); + + Local ret = String::NewFromUtf8(isolate, c).ToLocalChecked(); + args.GetReturnValue().Set(ret); + delete c; +} + + +void init(Local exports) { + NODE_SET_METHOD(exports, "multiAccountGenerateAndDeriveAddresses", _MultiAccountGenerateAndDeriveAddresses); + NODE_SET_METHOD(exports, "multiAccountImportPrivateKey", _MultiAccountImportPrivateKey); + NODE_SET_METHOD(exports, "multiAccountLoadAccount", _MultiAccountLoadAccount); + NODE_SET_METHOD(exports, "multiAccountReset", _MultiAccountReset); + NODE_SET_METHOD(exports, "multiAccountDeriveAddresses", _MultiAccountDeriveAddresses); + NODE_SET_METHOD(exports, "multiAccountImportMnemonic", _MultiAccountImportMnemonic); + NODE_SET_METHOD(exports, "multiAccountGenerate", _MultiAccountGenerate); + NODE_SET_METHOD(exports, "multiAccountStoreDerivedAccounts", _MultiAccountStoreDerivedAccounts); + NODE_SET_METHOD(exports, "multiAccountStoreAccount", _MultiAccountStoreAccount); + NODE_SET_METHOD(exports, "initKeystore", _InitKeystore); + NODE_SET_METHOD(exports, "initializeApplication", _InitializeApplication); + NODE_SET_METHOD(exports, "acceptTerms", _AcceptTerms); + NODE_SET_METHOD(exports, "fleets", _Fleets); + NODE_SET_METHOD(exports, "stopCPUProfiling", _StopCPUProfiling); + NODE_SET_METHOD(exports, "decodeParameters", _DecodeParameters); + NODE_SET_METHOD(exports, "hexToNumber", _HexToNumber); + NODE_SET_METHOD(exports, "numberToHex", _NumberToHex); + NODE_SET_METHOD(exports, "checkAddressChecksum", _CheckAddressChecksum); + NODE_SET_METHOD(exports, "isAddress", _IsAddress); + NODE_SET_METHOD(exports, "sha3", _Sha3); + NODE_SET_METHOD(exports, "serializeLegacyKey", _SerializeLegacyKey); + NODE_SET_METHOD(exports, "toChecksumAddress", _ToChecksumAddress); + NODE_SET_METHOD(exports, "logout", _Logout); + NODE_SET_METHOD(exports, "hashMessage", _HashMessage); + NODE_SET_METHOD(exports, "resetChainData", _ResetChainData); + NODE_SET_METHOD(exports, "saveAccountAndLogin", _SaveAccountAndLogin); + NODE_SET_METHOD(exports, "createAccountAndLogin", _CreateAccountAndLogin); + NODE_SET_METHOD(exports, "validateMnemonic", _ValidateMnemonic); + NODE_SET_METHOD(exports, "multiformatSerializePublicKey", _MultiformatSerializePublicKey); + NODE_SET_METHOD(exports, "loginWithKeycard", _LoginWithKeycard); + NODE_SET_METHOD(exports, "multiformatDeserializePublicKey", _MultiformatDeserializePublicKey); + NODE_SET_METHOD(exports, "login", _Login); + NODE_SET_METHOD(exports, "startCPUProfile", _StartCPUProfile); + NODE_SET_METHOD(exports, "openAccounts", _OpenAccounts); + NODE_SET_METHOD(exports, "extractGroupMembershipSignatures", _ExtractGroupMembershipSignatures); + NODE_SET_METHOD(exports, "callPrivateRPC", _CallPrivateRPC); + NODE_SET_METHOD(exports, "sendTransactionWithSignature", _SendTransactionWithSignature); + NODE_SET_METHOD(exports, "writeHeapProfile", _WriteHeapProfile); + NODE_SET_METHOD(exports, "signHash", _SignHash); + NODE_SET_METHOD(exports, "signGroupMembership", _SignGroupMembership); + NODE_SET_METHOD(exports, "callRPC", _CallRPC); + NODE_SET_METHOD(exports, "signMessage", _SignMessage); + NODE_SET_METHOD(exports, "signTypedData", _SignTypedData); + NODE_SET_METHOD(exports, "sendTransaction", _SendTransaction); + NODE_SET_METHOD(exports, "appStateChange", _AppStateChange); + NODE_SET_METHOD(exports, "setSignalEventCallback", _SetSignalEventCallback); + NODE_SET_METHOD(exports, "validateNodeConfig", _ValidateNodeConfig); + NODE_SET_METHOD(exports, "hashTypedData", _HashTypedData); + NODE_SET_METHOD(exports, "recover", _Recover); + NODE_SET_METHOD(exports, "hashTransaction", _HashTransaction); + NODE_SET_METHOD(exports, "connectionChange", _ConnectionChange); + NODE_SET_METHOD(exports, "pollSignal", _PollSignal); + NODE_SET_METHOD(exports, "restoreAccountAndLogin", _RestoreAccountAndLogin); +} + +NODE_MODULE(NODE_GYP_MODULE_NAME, init) + +} // namespace status diff --git a/modules/react-native-status/package.json b/modules/react-native-status/package.json index 1df0d0b103f..b67939dd9f1 100644 --- a/modules/react-native-status/package.json +++ b/modules/react-native-status/package.json @@ -18,4 +18,4 @@ "url": "https://github.com/status-im/react-native-status/issues" }, "homepage": "https://github.com/status-im/react-native-status#readme" -} \ No newline at end of file +} diff --git a/modules/react-native-status/react-native-status.podspec b/modules/react-native-status/react-native-status.podspec new file mode 100644 index 00000000000..d5c734b840a --- /dev/null +++ b/modules/react-native-status/react-native-status.podspec @@ -0,0 +1,22 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "react-native-status" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => "10.0" } + s.source = { :git => "https://github.com/status-im/react-native-status.git", :tag => "#{s.version}" } + + + s.source_files = "ios/**/*.{h,m,mm,swift}" + + + s.dependency "React" + s.vendored_frameworks = "Statusgo.xcframework" +end diff --git a/nix/DETAILS.md b/nix/DETAILS.md new file mode 100644 index 00000000000..b295bf97f54 --- /dev/null +++ b/nix/DETAILS.md @@ -0,0 +1,93 @@ +# Description + +This document describes the layout of our Nix setup. + +# Folders + +There are four main folders in the `nix` directory: + +* [`nix/scripts`](./scripts) - Bash scripts for easier usage of Nix +* [`nix/pkgs`](./pkgs) - Packages we add to or modify in `nixpkgs` +* [`nix/deps`](./deps) - Project dependencies managed by Nix +* [`nix/lib`](./lib) - Our tools we merge into `pkgs.lib` +* [`nix/tools`](./tools) - Various tools used by our derivations and shells +* [`nix/status-go`](./status-go) - Derivations for building [`status-go`](https://github.com/status-im/status-go) repo + +# Files + +There are a few main files that define the whole build environment: + +* [`nix/nix.conf`](./nix.conf) - Binary cache configuration +* [`nix/default.nix`](./default.nix) - Entry point for both shells and targets +* [`nix/shell.nix`](./shell.nix) - Definition of the default Nix shell +* [`nix/shells.nix`](./shells.nix) - Definitions of other Nix shells used in builds +* [`nix/targets.nix`](./targets.nix) - Hierarchy of main build targets +* [`nix/pkgs.nix`](./pkgs.nix) - Definition of a custom `nixpkgs` repo +* [`nix/overlay.nix`](./overlay.nix) - Overrides for `nixpkgs`, custom packages + +The [`default.nix`](../default.nix) and [`shell.nix`](../shell.nix) files at th repo root are just a gateway into the `nix` sub folder. + +# Scripts + +There's a few scripts in [`nix/scripts`](./scripts) that make use of Nix simpler: + +* [`nix/scripts/setup.sh`](./scripts/setup.sh) - Installs Nix Package manager +* [`nix/scripts/source.sh`](./scripts/source.sh) - Sources the Nix profile or installs Nix +* [`nix/scripts/build.sh`](./scripts/build.sh) - A wrapper around `nix-build` with sane defaults +* [`nix/scripts/shell.sh`](./scripts/shell.sh) - A wrapper around `nix-shell` for `Makefile` +* [`nix/scripts/clean.sh`](./scripts/clean.sh) - For cleaning Nix store after builds +* [`nix/scripts/purge.sh`](./scripts/purge.sh) - For purging everything Nix related from system + +# Start + +The starting point for using our Nix shells and targets is the [`default.nix`](/default.nix) file. + +It pulls in all the `pkgs`, `targets` and `shells` defined in [`nix/default.nix`](/nix/default.nix). The point is easy access to them via commands like `nix-build` or `nix-shell`, which you'll see next. + +# Shells + +Normally shells are started using `make shell TARGET=default`, but that is essentially the same as calling: +```bash +nix-shell -A shells.default default.nix +``` +The [`nix/scripts/shell.sh`](./scripts/shell.sh) script is essentially a wrapper around that command to make it usable as shell for the `Makefile`. + +# Building + +We will use the `make jsbundle` target as an example of a derivation you can build using Nix: + +1. `make jsbundle` is called by developer +2. `make` calls `nix/scripts/build.sh targets.mobile.jsbundle` +3. [`build.sh`](/nix/scripts/build.sh) calls `nix-build --attr targets.mobile.jsbundle` with extra arguments +4. `nix-build` builds the derivation from [`nix/mobile/jsbundle/default.nix`](/nix/mobile/jsbundle/default.nix) + +The same can be done for other targets like `targets.mobile.android.build`. +Except in that case extra arguments are required which is why the [`scripts/release-android.sh`](/scripts/release-android.sh) is used in the `make release-android` target. + +If you run `make release-android` you'll see the `nix-build` command used: +``` +nix-build \ + --pure \ + --fallback \ + --no-out-link \ + --show-trace \ + --attr targets.mobile.android.build \ + --argstr secrets-file '/tmp/tmp-status-mobile-559a3a441/tmp.xAnrPuNtAP' \ + --option extra-sandbox-paths '/home/joe/.gradle/status-im.keystore /tmp/tmp-status-mobile-559a3a441/tmp.xAnrPuNtAP' \ + default.nix +``` +Some of those are required which is why just calling: +``` +nix-build --attr targets.mobile.android.build +``` +Would fail. + +# Garbage Collection + +The `make nix-gc` target calls `nix-store --gc` and normally would remove almost everything, but to prevent that we place symlinks to protected derivations in `.nix-gcroots` folder. Specifically: +```sh +_NIX_GCROOTS="${_NIX_GCROOTS:-${GIT_ROOT}/.nix-gcroots} +``` +These symlinks in turn will be symlinked from `/nix/var/nix/gcroots/auto` through use of `nix-store --add-gcroots`. + +Whenever `nix/scripts/build.sh` or `nix/scripts/shell.sh` are called they update symlinks named after given targets in that folder. This in combination with `keep-outputs = true` set in `nix/nix.conf` prevents garbage collection from removing too much. diff --git a/nix/KNOWN_ISSUES.md b/nix/KNOWN_ISSUES.md new file mode 100644 index 00000000000..9320bf16712 --- /dev/null +++ b/nix/KNOWN_ISSUES.md @@ -0,0 +1,86 @@ +# Known Issues + +## Ignoring Untrusted Substituter on NixOS + +When using our setup on NixOS users can see this warning: +``` +warning: ignoring untrusted substituter 'https://nix-cache.status.im/', you are not a trusted user. +``` +Which is due to incorrect `trusted-users` setting: +``` +> grep trusted-users /etc/nix/nix.conf +trusted-users = root +``` +Which can be extended using the [`nix.settings.trusted-users`](https://search.nixos.org/options?channel=23.05&show=nix.settings.trusted-users) option. + +## Too many open files + +### Single-User Installation + +Nix can open a lot of files when fetching things for `/nix/store` which can cause +``` +Too many open files" +``` +The temporary way to fix this is set a new limit for current session using `ulimit`: +```sh +ulimit -n 131072 +``` +To increase limit permanently system-wide edit `/etc/sysctl.conf`: +``` +fs.file-max = 131072 +``` +And use `sudo sysctl --system` to load these new settings. + +### Multi-User Installation + +This has been fixed in Nix `2.9.0`: + +* https://github.com/NixOS/nix/issues/6007 +* https://github.com/NixOS/nix/pull/6553 + +## Cache Downloads Timing Out + +If copying from Nix Cache times out you can adjust the timeout by changing [`nix/nix.conf`](/nix/nix.conf): +```conf +stalled-download-timeout = 9001 +``` + +## NDK Bundle Download Failures + +There is a known issue([#11111](https://github.com/status-im/status-mobile/issues/11111)) where the fetching of Android NDK Bundle from our own cache host fails in various different ways: +``` +unable to download 'https://nix-cache.status.im/nar/64g5wharwjj040rg1v8jnc5qhv1hkgds.nar': + HTTP error 200 (curl error: Stream error in the HTTP/2 framing layer) +``` +``` +unable to download 'https://nix-cache.status.im/nar/64g5wharwjj040rg1v8jnc5qhv1hkgds.nar': + HTTP error 200 (curl error: Transferred a partial file) +``` +``` +NAR for '/nix/store/psx79cnz1khfjlzkw3j8x9sb1jhk1v1a-ndk-bundle-21.0.6113669' fetched from 'https://nix-cache.status.im' is incomplete +``` +Currently the simplest temporary solution is to edit `nix/nix.conf` and comment out the `extra-substituters` line. +This should allow Nix to download the NDK Bundle directly from Google servers rather than from our own cache. + +## `extra-sandbox-paths` Is a Restricted Setting + +When building Android on NixOS you might encounter the following error: +``` +ignoring the user-specified setting 'extra-sandbox-paths', because it is a restricted setting and you are not a trusted user +``` +You can mitigate this by setting the [`nix.trustedUsers`](https://nixos.org/nixos/options.html#nix.trustedusers) property. + +## NixOS Prioritizes System Config + +Currently on NixOS `NIX_CONF_DIR` is being ignored in favor of the default `/etc/nix/nix.conf`. +This will be possible to fix once Nix `2.4` comes out with support for `NIX_USER_CONF_FILES`. + +For more details see https://github.com/NixOS/nix/issues/3723. + +## Extra Sandbox Files Not Available + +It's possible that on a multi-user installation files provided to the build sandbox using the `extra-sandbox-paths` option will ne be available, unless the current user is added to `trusted-users` in `nix.conf` file. + +Issues: +* https://github.com/NixOS/nix/issues/6115 +* https://github.com/NixOS/nix/issues/6217 diff --git a/nix/README.md b/nix/README.md new file mode 100644 index 00000000000..86057e71886 --- /dev/null +++ b/nix/README.md @@ -0,0 +1,72 @@ +# Description + +This folder contains configuration for [Nix](https://nixos.org/), a purely functional package manager used by the Status app for its build process. + +## Configuration + +The main config file is [`nix/nix.conf`](/nix/nix.conf) and its main purpose is defining the [binary caches](https://nixos.org/nix/manual/#ch-basic-package-mgmt) which allow download of packages to avoid having to compile them yourself locally. + +## Build arguments + +We leverage the environment variables for parameterization of the builds (e.g. to pass a build number or build type). + +Here is the list of variables you can use to override defaults: +```shell +# Build type (influences which .env file gets used for feature flags) +export BUILD_TYPE="pr" +# Used for versionCode and CFBundleVersion in Android and iOS respectively +export BUILD_NUMBER=9999 +# Gradle options passed for Android builds +export ANDROID_GRADLE_OPTS="" +# If APKs should be split based on architectures +export ANDROID_ABI_SPLIT=false +# Android architectures to build for +export ANDROID_ABI_INCLUDE="x86" +# Local source override, see below +export STATUS_GO_SRC_OVERRIDE="$HOME/my/source/status-go" +export STATUS_GO_IPFS_GATEWAY_URL="https://ipfs.status.im/" +``` +You can see the defaults in code: +- [`nix/status-go/default.nix`](./status-go/default.nix) +- [`nix/mobile/android/release.nix`](./mobile/android/build.nix) + +## Shell + +In order to access an interactive Nix shell a user should run `make shell`. + +The Nix shell is started in this repo via the [`nix/scripts/shell.sh`](/nix/scripts/shell.sh) script, which is a wrapper around the `nix-shell` command and is intended for use with our main [`Makefile`](/Makefile). This allows for an implicit use of `nix-shell` as the default shell in the `Makefile`. + +Normally the shell starts without any specific target platform, if you want to change that you should export the `TARGET` env variable with appropriate value: + +```bash +make shell TARGET=android +``` + +This way your shell and all other nix commands should run in a setup that is tailored towards Android development. + +For valid values you can check the [`nix/shells.nix`](/nix/shells.nix) file. + +:warning: __WARNING__: To have Nix pick up all changes a new `nix-shell` needs to be spawned. + +## Using a local status-go repository + +If you need to use a locally checked-out status-go repository, you can achieve that by defining the `STATUS_GO_SRC_OVERRIDE` +environment variable: + +```sh +export STATUS_GO_SRC_OVERRIDE=$GOPATH/src/github.com/status-im/status-go +make release-android +``` + +## Resources + +You can learn more about Nix by watching these presentations: + +* [Nix Fundamentals](https://www.youtube.com/watch?v=m4sv2M9jRLg) ([PDF](https://drive.google.com/file/d/1Tt5R7QOubudGiSuZIGxuFWB1OYgcThcL/view?usp=sharing), [src](https://github.com/status-im/infra-docs/tree/master/presentations/nix_basics)) +* [Nix in Status](https://www.youtube.com/watch?v=rEQ1EvRG8Wc) ([PDF](https://drive.google.com/file/d/1Ti0wppMoj40icCPdHy7mJcQj__DeaYBE/view?usp=sharing), [src](https://github.com/status-im/infra-docs/tree/master/presentations/nix_in_status)) + +And you can read [`nix/DETAILS.md`](./DETAILS.md) for more information. + +## Known Issues + +See [`KNOWN_ISSUES.md`](./KNOWN_ISSUES.md). diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 00000000000..2da6f2b8332 --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,12 @@ +{ + config ? {}, + pkgs ? import ./pkgs.nix { inherit config; } +}: + +let + # put all main targets and shells together for easy import + shells = pkgs.callPackage ./shells.nix { }; + targets = pkgs.callPackage ./targets.nix { }; +in { + inherit pkgs targets shells; +} diff --git a/nix/deps/clojure/README.md b/nix/deps/clojure/README.md new file mode 100644 index 00000000000..5e532669f65 --- /dev/null +++ b/nix/deps/clojure/README.md @@ -0,0 +1,40 @@ +# Description + +This folder contains the scripts and Nix derivations necessary to generate Clojure dependencies for this project. + +# Usage + +Running [`generate.sh`](./generate.sh) in a shell that has available `shadow-cljs` and `clojure` should update the `deps.list` and `deps.json` files. + +# Details + +By using the following command: +```sh +shadow-cljs classpath --force-spawn +``` +We download the necessary JARs into `~/.m2` folder, but also get the classpath printed into standard output. +We skip POM files since they are not necessary, and add edge cases we don't want to handle. + +We then use the classpath in combination with contents of `~/.m2` folder to generate the following files: + +* `deps.list` - List of JARs relative to the `~/.m2` cache folder. +* `deps.json` - Full list of JARs including their SHAs. + +The `deps.list` file is just intermediate and for debugging purposes. +The `deps.json` is loaded by the derivation in [`default.nix`](./default.nix) and used to produce a derivation that contains all the necessary dependencies: + +``` + > grep cljfmt nix/deps/clojure/deps.list +cljfmt/cljfmt/0.6.7/cljfmt-0.6.7.jar + + > nix-build --no-out-link -A pkgs.deps.clojure +/nix/store/d7p8r9vhq6z2jmxwakxpcb262wgkc86s-status-mobile-clojure-deps + + > DEPS=$(nix-build --no-out-link -A pkgs.deps.clojure) + + > find $DEPS -iname 'cljfmt-0.6.7.*' +/nix/store/d7p...86s-status-mobile-clojure-deps/cljfmt/cljfmt/0.6.7/cljfmt-0.6.7.pom.sha1 +/nix/store/d7p...86s-status-mobile-clojure-deps/cljfmt/cljfmt/0.6.7/cljfmt-0.6.7.pom +/nix/store/d7p...86s-status-mobile-clojure-deps/cljfmt/cljfmt/0.6.7/cljfmt-0.6.7.jar.sha1 +/nix/store/d7p...86s-status-mobile-clojure-deps/cljfmt/cljfmt/0.6.7/cljfmt-0.6.7.jar +``` diff --git a/nix/deps/clojure/default.nix b/nix/deps/clojure/default.nix new file mode 100644 index 00000000000..9bea02a99aa --- /dev/null +++ b/nix/deps/clojure/default.nix @@ -0,0 +1,44 @@ +{ stdenv, lib, fetchurl, writeShellScriptBin }: + +let + inherit (lib) + removeSuffix optionalString splitString concatMapStrings + attrByPath attrValues last makeOverridable importJSON; + + # load dependencies + deps = importJSON ./deps.json; + + script = writeShellScriptBin "create-local-maven-repo" ('' + mkdir -p $out + cd $out + '' + + (concatMapStrings (dep: + let + url = "${dep.host}/${dep.path}"; + jar = { + sha1 = attrByPath [ "jar" "sha1" ] "" dep; + sha256 = attrByPath [ "jar" "sha256" ] "" dep; + }; + jarFile = optionalString (jar.sha256 != "") ( + fetchurl { url = "${url}.jar"; inherit (jar) sha256; } + ); + fileName = last (splitString "/" dep.path); + directory = removeSuffix fileName dep.path; + in + '' + mkdir -p ${directory} + + ${optionalString (jarFile != "") '' + ln -s "${jarFile}" "${dep.path}.jar" + ''} + ${optionalString (jar.sha1 != "") '' + echo "${jar.sha1}" > "${dep.path}.jar.sha1" + ''} + '') + deps)); + +in makeOverridable stdenv.mkDerivation { + name = "status-mobile-clojure-deps"; + phases = [ "buildPhase" ]; + buildPhase = "${script}/bin/create-local-maven-repo"; +} diff --git a/nix/deps/clojure/deps.json b/nix/deps/clojure/deps.json new file mode 100644 index 00000000000..7428227a14d --- /dev/null +++ b/nix/deps/clojure/deps.json @@ -0,0 +1,1576 @@ +[ + { + "path": "aopalliance/aopalliance/1.0/aopalliance-1.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8", + "sha256": "023a6xwv1kd9c4dq9jrsbvvj6398hgbr302w7h8kzkgd1xkyrp8a" + } + }, + + { + "path": "args4j/args4j/2.33/args4j-2.33", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "bd87a75374a6d6523de82fef51fc3cfe9baf9fc9", + "sha256": "1mlyqrqyhijwkjx4sv2zfn2ciqfwpc08qq8w55rcxb941fxfmpci" + } + }, + + { + "path": "babashka/fs/0.2.16/fs-0.2.16", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "4e7ad43c9d5ab8907ef0064105e788e0e84f282a", + "sha256": "1zhz4hnrzpnrz3d222py42xlhybwsk94bipmnm7ypb9vlf0p4m8y" + } + }, + + { + "path": "bidi/bidi/2.1.6/bidi-2.1.6", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "e17fa1c05ff99e99543c6d5328e293e933e15e06", + "sha256": "1gld043c5qz7v9bp5s61vf1s5f8f1pbda9nzwqhy893dpm8xv0qb" + } + }, + + { + "path": "binaryage/env-config/0.2.2/env-config-0.2.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "ac36173f1802a5d7225be41faebedbf12949ae59", + "sha256": "11j1bls84d8hn8gviawvxkbbnb0hcg1lvw6qqcjj356ap6xzxfic" + } + }, + + { + "path": "binaryage/oops/0.7.2/oops-0.7.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "2f47298f9aa41ef6f2c4f2c49f472eb113e94ae7", + "sha256": "0c9f7wylwl0lxcn4vqncprlzmdddqhxzb3vdaawdm0da0xh010hb" + } + }, + + { + "path": "borkdude/dynaload/0.3.5/dynaload-0.3.5", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "accd696ba364b850b4d92e38f5a34d0e828a0ad1", + "sha256": "0k62m1f29xfh3cp67w7kcvkp5aj35simi8kf95ycvkmgp76w11q8" + } + }, + + { + "path": "borkdude/edamame/1.3.23/edamame-1.3.23", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "254d023e97ed438f0f44532b5a06d928d031ede4", + "sha256": "0l7mxza2nimslhg0qh6jp7gmb8sd6l89fk5d1zvzq2xskscq2vly" + } + }, + + { + "path": "borkdude/sci.impl.reflector/0.0.1/sci.impl.reflector-0.0.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "33dfc86102e0ea400498cbca47572459c1c43b00", + "sha256": "0a5gxmj8kzc01y9bn7l4x7c1v5q9wcbvw5hdr525d3ylsyl6xfkw" + } + }, + + { + "path": "camel-snake-kebab/camel-snake-kebab/0.4.3/camel-snake-kebab-0.4.3", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "5ae08f83ceb8959971e6334596bff0214bf6fdf2", + "sha256": "1j627a99ccc4v0v83c8670vdnsp69cjk7ba0ga2xf433fwsz74c1" + } + }, + + { + "path": "cheshire/cheshire/5.11.0/cheshire-5.11.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "1a1231c65bfd6a2033148e88dcbd1ed8dede12a4", + "sha256": "0iv2nidrz07qjsqhyh8r9n59hxc52jpagggj9ivxl7bbcyg0daqz" + } + }, + + { + "path": "cider/cider-nrepl/0.31.0/cider-nrepl-0.31.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "1f28a4a834a7a46cf24def971b1a705a6795c73a", + "sha256": "0h4pyzy8rzzh567khvimw2cs8jr640kqxgaxvbfx1s5yfp56fka2" + } + }, + + { + "path": "cider/piggieback/0.4.1/piggieback-0.4.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "0a02a3e2ecd7a126ab60d8a44793342f20ced79b", + "sha256": "142vl5np33akcrnn6pksi0rjfsmmi528villxsj6cwcndvybiw4m" + } + }, + + { + "path": "clj-kondo/clj-kondo/2024.03.13/clj-kondo-2024.03.13", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "adec398a95322f3a27baf0a92b658493335464d1", + "sha256": "0r97hsj7m3qc12xwyxx6m1a71gqp5aiy99imcf8r0nz2dnjmj0kz" + } + }, + + { + "path": "cljs-bean/cljs-bean/1.9.0/cljs-bean-1.9.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "ac78135240a56cdfb104256b73f398bac5bea4d2", + "sha256": "129bvlbq3x92p4y1pyikzg774wxf28kxz1qvqqd7mj7bqilwc2gq" + } + }, + + { + "path": "clout/clout/2.1.2/clout-2.1.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "87cc1bd24ec39a8572e66103039955d7570ce077", + "sha256": "1y3pp542ml5za3iyc5szqh2xn65dqmd8d6621mznmzg8bng1yscx" + } + }, + + { + "path": "com/andrewmcveigh/cljs-time/0.5.2/cljs-time-0.5.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "7d4274be169f54a03d4afcc40ea95f40d44ca0a2", + "sha256": "061cjh2a6qpkib5v5mdrsbwhvcbqvh1igvp3b7jhcfj05pgplm1x" + } + }, + + { + "path": "com/bhauman/cljs-test-display/0.1.1/cljs-test-display-0.1.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "64735aaf0401970524db37053b5dca19aa01f0a6", + "sha256": "13099h797xp6wh0f4yly480q59xwkm63fba43y99ilzc647ipwn1" + } + }, + + { + "path": "com/cognitect/aws/api/0.8.612/api-0.8.612", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "6bf53a6a318693e086bec6246a63b193f981b386", + "sha256": "174csbi2whfqqw1pnna1a7pxwfa3q39marrs0sqjrwsvzqkwakvl" + } + }, + + { + "path": "com/cognitect/aws/endpoints/1.1.12.321/endpoints-1.1.12.321", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "3bec2d7c7fe3b22b709e3ef8244d8f34406f3a8d", + "sha256": "18mz7n8f07ki35h5v7v60rw9fh02fayda9l9cfa7bjy7w4a1yl1f" + } + }, + + { + "path": "com/cognitect/aws/s3/822.2.1145.0/s3-822.2.1145.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "0c4d7c1bc3baa4a05c7ebdf6b672259f56a16d7e", + "sha256": "0fhgikpn4ns494gaq1y8gch95silx1snvkg1n62ysnf1zyk42ckh" + } + }, + + { + "path": "com/cognitect/http-client/1.0.115/http-client-1.0.115", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "1a84a105c286d00557cc6c3e96932e40439b387a", + "sha256": "0k2dnyx4jywgzfizz9i9yb2v04cvzbqvyhyp2nv1pll0blv8h38v" + } + }, + + { + "path": "com/cognitect/transit-clj/1.0.329/transit-clj-1.0.329", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "e3bc004c0ca6bef0a0249147f57d5d741521cb11", + "sha256": "0sn9m8sfmm3p5dr9gz95j8fbkk7xip0iqs8ld6j0pkrzvff476l1" + } + }, + + { + "path": "com/cognitect/transit-cljs/0.8.280/transit-cljs-0.8.280", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "98d11d81caee82f97b9417025750b785fb6ecb16", + "sha256": "06hwd00wh0qh5dynf69jnk20az97xmnz5flpk7118qj11v1x8yhm" + } + }, + + { + "path": "com/cognitect/transit-java/1.0.362/transit-java-1.0.362", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "93775c7f592ccca35e1eba3a24ac807650dedc74", + "sha256": "0m6bywis7l7g4vl049g9fsgfidgyhz1b3nb3rh0mda6x8qymfs7b" + } + }, + + { + "path": "com/cognitect/transit-js/0.8.874/transit-js-0.8.874", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f8b4f570ca4d41649190efecac27a5932cc11429", + "sha256": "0289pwxxmdhb6gw8djrrdc6qa8jxcv4j105mkskmsmlmsv94nvmd" + } + }, + + { + "path": "com/fasterxml/jackson/core/jackson-core/2.13.3/jackson-core-2.13.3", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "a27014716e4421684416e5fa83d896ddb87002da", + "sha256": "0gbara9dbk2khk1ksqbxsmm57gpvkf20p1qfphp4fsfclf79l4db" + } + }, + + { + "path": "com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.13.3/jackson-dataformat-cbor-2.13.3", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "bf43eed9de0031521107dfea41d1e5d6bf1b9639", + "sha256": "0q78lxy2sh9gdscnbqrjb3gkgjy1gf76gyf3yfqj353kb5vnhsla" + } + }, + + { + "path": "com/fasterxml/jackson/dataformat/jackson-dataformat-smile/2.13.3/jackson-dataformat-smile-2.13.3", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "b4e03e361e2388e3a8a0b68e3b9988d3a07ee3f3", + "sha256": "1d4zhxvr9zc01lzsa3fq1bww2bmwc06p213sr058z3g85j4gzm1j" + } + }, + + { + "path": "com/github/javaparser/javaparser-core/3.25.3/javaparser-core-3.25.3", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "55a960eea36e9ae20e48c500c3dd356b33331f1f", + "sha256": "09rca8alzi5av62sjsd4m0j6wpa0nprml0zjas87xb8dh8cbq93k" + } + }, + + { + "path": "com/github/jpmonettas/flow-storm-inst/3.7.5/flow-storm-inst-3.7.5", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "c76d1e07f3914d3f1c669d1aadc469037e7cff00", + "sha256": "0xzygzibqslhsl90zxfm6k2izmvzrm5wixrb0xzfcihf9bwn9nya" + } + }, + + { + "path": "com/github/jpmonettas/hansel/0.1.78/hansel-0.1.78", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "a4d916f9882f748201003880d2ee440e448b912f", + "sha256": "0d3x5pab6p4mykmxhspz17w4zwcp52g7gmzma3vqsn4ym24xr5qm" + } + }, + + { + "path": "com/google/auto/value/auto-value-annotations/1.6/auto-value-annotations-1.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "da725083ee79fdcd86d9f3d8a76e38174a01892a", + "sha256": "0sdf3y01nmj6kixvfqd8ljxm1vvw7r1ngaza3dkzqaig8dn975fh" + } + }, + + { + "path": "com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "25ea2e8b0c338a877313bd4672d3fe056ea78f0d", + "sha256": "1iyh53li6y4b8gp8bl52fagqp8iqrkp4rmwa5jb8f9izg2hd4skn" + } + }, + + { + "path": "com/google/code/gson/gson/2.9.1/gson-2.9.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "02cc2131b98ebfb04e2b2c7dfb84431f4045096b", + "sha256": "00x67pi14r2kdpn3rhglwcdmvhgifsxkmyrn2w5xbrp677ik919p" + } + }, + + { + "path": "com/googlecode/json-simple/json-simple/1.1.1/json-simple-1.1.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "c9ad4a0850ab676c5c64461a05ca524cdfff59f1", + "sha256": "170rflxnqnah0265ik2aylmxkshyqbf2zas9bp2l32xqj9l6jsaf" + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.15.0/error_prone_annotations-2.15.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "38c8485a652f808c8c149150da4e5c2b0bd17f9a", + "sha256": "1sy40pwq5rk87zpa0mccn8g3m7xgq38xkynvbfd7irs98dqlfw06" + } + }, + + { + "path": "com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "1dcf1de382a0bf95a3d8b0849546c88bac1292c9", + "sha256": "09na6vwxmpw4xcqszba15avzl6k6yjfvw5jbgs1xmljdfd6fwwd1" + } + }, + + { + "path": "com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "119ea2b2bc205b138974d351777b20f02b92704b", + "sha256": "1fc7y1dan9jqfg7j27f9iywa6mdagd8x2fhrnfgj3gc7bvb99gnm" + } + }, + + { + "path": "com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "b421526c5f297295adef1c886e5246c39d4ac629", + "sha256": "169zydsbk48cs370lpdq5l69qgqjsq7z7ppzprzsa2i3shvs0wmk" + } + }, + + { + "path": "com/google/inject/guice/4.2.2/guice-4.2.2-no_aop", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "fa13659f9128f4c011c8e1d06f137083b4876377", + "sha256": "1zxg79l9jv86ar71pmw3abr43dgrgjz2hwbzifrx5909hsr5ykqg" + } + }, + + { + "path": "com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "ba035118bc8bac37d7eff77700720999acd9986d", + "sha256": "0ysaws2dawf41raccmprx8vilr5nrh6d5d70q0i63gb74b4k1br1" + } + }, + + { + "path": "com/google/javascript/closure-compiler-unshaded/v20230802/closure-compiler-unshaded-v20230802", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "0a2498a4d45bf5896042f31da0cd30c5e7b415af", + "sha256": "0jvgqq0gnvm4jw2zac4677ds05klvsjalszdzcq3nw8g7fjyjv2x" + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.21.12/protobuf-java-3.21.12", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "5589e79a33cb6509f7e681d7cf4fc59d47c51c71", + "sha256": "11yzx7m9qq682n8r1xh820gjnnhddgfn3xgayf060946jbddngiz" + } + }, + + { + "path": "com/google/re2j/re2j/1.3/re2j-1.3", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "dc7de2b32fa8cc569ab44fb849abadbbc6983b91", + "sha256": "06fypacl4jsbiddgby40fxxz6bpck7jvc5ch344f472cqnhhy16q" + } + }, + + { + "path": "commons-codec/commons-codec/1.15/commons-codec-1.15", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "49d94806b6e3dc933dacbd8acb0fdbab8ebd1e5d", + "sha256": "0qzd8v96j4x7jjcfpvvdh9ar1xhwxpxi2rh51nzhj0br7bbgdsdk" + } + }, + + { + "path": "commons-fileupload/commons-fileupload/1.4/commons-fileupload-1.4", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f95188e3d372e20e7328706c37ef366e5d7859b0", + "sha256": "1xyyl54sfxsdcwxdyq6b0azmr31b4dwqns850jjkw9a9dwrh5v54" + } + }, + + { + "path": "commons-io/commons-io/2.11.0/commons-io-2.11.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "a2503f302b11ebde7ebc3df41daebe0e4eea3689", + "sha256": "020946yakki3qzc652arfndzi594drxanidz9bawbb6vhxnjy6wn" + } + }, + + { + "path": "compojure/compojure/1.5.2/compojure-1.5.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "0b5258d0616ffc5f64c2b6d95f09de56d24df439", + "sha256": "1s2k05lwnlm9a66mxnsss437i9gp70dny8y2rlfkl090s6mdqsaf" + } + }, + + { + "path": "com/taoensso/encore/3.68.0/encore-3.68.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "b098b58e27a7cf9ab5e46aedbb5ababc12805170", + "sha256": "1i5bmm83y9wpgvl5h6ysjsw58qrqz4b4v5vqnsprq6f1pmakjfz2" + } + }, + + { + "path": "com/taoensso/timbre/6.3.1/timbre-6.3.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "e1b04be262f38bbd9af8f1193b6b3f2c6a37129d", + "sha256": "0p6rws5gpab5qldp739lyqvwdbak28vaxbjw2bg2slflkkahszc7" + } + }, + + { + "path": "com/taoensso/truss/1.11.0/truss-1.11.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "4784f43191d5839448b58128bb455d62616086fd", + "sha256": "16xqav4cv3cnfik2d8v3wn7zb2a97ybnrbp6v6nj8m1nmdhr16k7" + } + }, + + { + "path": "com/taoensso/tufte/2.6.3/tufte-2.6.3", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "8862c6113ca743ebdcfa17bf4779a21fc08a7c7e", + "sha256": "0i0wsv2g2hswgdik6zlgcs212clj1d7fvckxfcsmyfr0vvpc1dmd" + } + }, + + { + "path": "crypto-equality/crypto-equality/1.0.1/crypto-equality-1.0.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "26f76ad46f4a9881992c158118419dd9e7846b52", + "sha256": "1psbxljxvqvjvvlz1cj0df50l5npzvpyj0kdr27kzxywfq5wq5gf" + } + }, + + { + "path": "crypto-random/crypto-random/1.2.1/crypto-random-1.2.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "ded0350f88e6f0bcca276c73f3aaadde94dc09f3", + "sha256": "0b75799a2lilbrm9j6k5zx22iq7pfaw76rvjx72m6vdnsx38h4jw" + } + }, + + { + "path": "day8/re-frame/test/0.1.5/test-0.1.5", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "9e2d06c8564f0983098ddcc046a329c4faca46c2", + "sha256": "11qsa9y006f6cg8s08zzsbbgxnpv0504xns9r9xdyvkjwi280wxl" + } + }, + + { + "path": "expound/expound/0.9.0/expound-0.9.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "5294f6b31a2cfa6ffbe5021d9390c738fb471927", + "sha256": "0p7r33hglnl93v0sxbvspbl9khcbs69xd2vaz8dkbq0qk5h758yn" + } + }, + + { + "path": "fipp/fipp/0.6.26/fipp-0.6.26", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "fde761cd9f5c9bd13e8c91e8b9724573a87f1449", + "sha256": "10vjhnb9g9yzcgjsi1gi394nspvyki0l2m4dhd6dgbsmrrn6kjzp" + } + }, + + { + "path": "funcool/promesa/11.0.678/promesa-11.0.678", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "0ee1690ba74b2609c2d85b51edacad13046a18e2", + "sha256": "1wif8nwdbpchwg56fz47ymjnflmva0fzy0bw1s6bacnac472pmza" + } + }, + + { + "path": "hiccup/hiccup/1.0.5/hiccup-1.0.5", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "75940a400111bbb8f80e43325e23100b6e2227bc", + "sha256": "1z07dh3qg9zzcwa8x31mnqxnkfsf2sbv315n43kxmnv1fkjagm0g" + } + }, + + { + "path": "http-kit/http-kit/2.2.0/http-kit-2.2.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "70b17515f43a0e74937cdc0dea270d4ef13c7f32", + "sha256": "1nnvadcqc7mabyp0gp2kbic85ipnxx499w36984is3ajw8k2c47p" + } + }, + + { + "path": "instaparse/instaparse/1.4.0/instaparse-1.4.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "ade13d743d1ec8a6c5103fd0f5701b0f997e2cc4", + "sha256": "188325lqyc6p9fmrcsx5vwp4cydnaxf32034jqf2ch29mnax1gvh" + } + }, + + { + "path": "io/aviso/pretty/1.4.4/pretty-1.4.4", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "ab0c981501a6a24066478b724a926a036217d5fd", + "sha256": "14mz49f0dn06cs9s4gka32m7n5y49h2vvkgkgdrh4dm3dyslzy5c" + } + }, + + { + "path": "io/github/clojure/tools.build/0.9.4/tools.build-0.9.4", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "5d0a9ffeae76e1afa5b14ecaf6faffadea77dffd", + "sha256": "1vivf7p05f30bcsiqn175df9vlxfvfpyx0z1snnfgin5mf6nhniq" + } + }, + + { + "path": "io/methvin/directory-watcher/0.17.1/directory-watcher-0.17.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "cf173a2fbca13eea5de68ea7b3434ce2c627fdeb", + "sha256": "176sa5hglp358nswyfafqh98l79971h8qxncpmbb5y227v4qx4xd" + } + }, + + { + "path": "io/replikativ/datalog-parser/0.2.25/datalog-parser-0.2.25", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "4d59fde5929044463b0385e9161709a64a4f3d32", + "sha256": "0lbwrpsgfg7ri7bqrh23w3fjkfc9jlh4s1nim8rd284pc42xhnhg" + } + }, + + { + "path": "io/undertow/undertow-core/2.3.10.Final/undertow-core-2.3.10.Final", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "96d1d5ca6406a75a285b95ea8f4b39e0fb2d707e", + "sha256": "1s1m5hfhmfr7jsp4bn4c6fhdbgiz72mz0a96494nyhfa0b4lql8l" + } + }, + + { + "path": "javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "934c04d3cfef185a8008e7bf34331b79730a9d43", + "sha256": "16xbi6mz75r1maw116njab6ln7hm9mhwrxshav4msmfdbccsajz0" + } + }, + + { + "path": "javax/annotation/jsr250-api/1.0/jsr250-api-1.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "5025422767732a1ab45d93abfea846513d742dcf jsr250-api-1.0.jar", + "sha256": "07wl9bsxxh9id5rr8vwc1sgibsz1s40srpq073nq7ldnv7825ad1" + } + }, + + { + "path": "javax/inject/javax.inject/1/javax.inject-1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "6975da39a7040257bd51d21a231b76c915872d38 /home/maven/repository-staging/to-ibiblio/maven2/javax/inject/javax.inject/1/javax.inject-1.jar", + "sha256": "1zz7gnahy2352345411rjlhsf64ikkc6z49dqcv1cj0clm271iwi" + } + }, + + { + "path": "javax/servlet/servlet-api/2.5/servlet-api-2.5", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "5959582d97d8b61f4d154ca9e495aafd16726e34", + "sha256": "1p6lk86qwrr8k2pgjs3pmfh44h9ff05ckcvgnsnyxykh18vfln66" + } + }, + + { + "path": "javax/xml/bind/jaxb-api/2.3.0/jaxb-api-2.3.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "99f802e0cb3e953ba3d6e698795c4aeb98d37c48", + "sha256": "00rxpc0m30d3jc572ni01ryxq8gcbnr955xsabrijg9pknc0fc48" + } + }, + + { + "path": "medley/medley/0.8.2/medley-0.8.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "0c05ef36ae49925af44c781108ecf8b704a83a8f", + "sha256": "1hf1jd60jshd1p8yknfnimfsbiz9x1kg8x1gb6939xxx6sk953c8" + } + }, + + { + "path": "metosin/malli/0.13.0/malli-0.13.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "286c52d26a3a9d613b5f692971bd19c581cdc4b0", + "sha256": "0vaq9d6cln5j4ww302fwlgg7m647cf2j16gs2i1p1d4klbn4jddz" + } + }, + + { + "path": "mvxcvi/arrangement/2.1.0/arrangement-2.1.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "1bc8f3bba7a28de85f319b7d464aa8d955f44918", + "sha256": "1j1rkwrs4wm8zk9v7ilgpnyav5sipdd2bmb83sazh392blw52wpk" + } + }, + + { + "path": "net/cgrand/macrovich/0.2.1/macrovich-0.2.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "abcb26cbfe0f8eef363b25525210c0fe1eb1f693", + "sha256": "119rmznkfsk1df3q9408dkd9kcqsbpffni19dzrjr7k05ijcl487" + } + }, + + { + "path": "net/java/dev/jna/jna/5.12.1/jna-5.12.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "b1e93a735caea94f503e95e6fe79bf9cdc1e985d", + "sha256": "1cskrxarxlrh7h73sh44g4cn4k47mnlf2hnqj7p0vmj09yn19a4i" + } + }, + + { + "path": "nrepl/bencode/1.1.0/bencode-1.1.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "48e0674aeb221294c8728ad68571c01b95df4f5b", + "sha256": "0syyqsx376lxirv3prx27klvz1x98vxg6rqsaniz4ddi45vxlm2p" + } + }, + + { + "path": "nrepl/nrepl/1.0.0/nrepl-1.0.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "f47774c43493efdc879d36b95ebd67ea0d9c890a", + "sha256": "1fx5ssmixgqmklliw0ng8fjz41kkhys56x8dbwv9yqrfzws9f2x3" + } + }, + + { + "path": "nubank/matcher-combinators/3.9.1/matcher-combinators-3.9.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "f0830a112cae8ee931a90d9f39214a0ed4d44150", + "sha256": "1rjcgqhms84xmnhs7sqcd3b2dpa37mmzgz2yz33yz2rdb9skl96g" + } + }, + + { + "path": "org/apache/ant/ant/1.10.11/ant-1.10.11", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "b875cd48a0bc955ae9c5c477ad991e1f26fb24d2", + "sha256": "0m07pifkdpwghpp8wvqh14sbxazmjbkkpsfakw6ixq5apfdvih48" + } + }, + + { + "path": "org/apache/ant/ant-launcher/1.10.11/ant-launcher-1.10.11", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "ea0a0475fb6dfcdcf48b30410fd9d4f5c80df07e", + "sha256": "0mnj5v660qvmrsi1m6z0dnykw3df8f1213byzp45l2wqgbgk1dfs" + } + }, + + { + "path": "org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "c6842c86792ff03b9f1d1fe2aab8dc23aa6c6f0e", + "sha256": "0kp9qwqmqp45axxddf9h8ai9yap2j86dl4il366py0vc902dj6fr" + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "e5f6cae5ca7ecaac1ec2827a9e2d65ae2869cada", + "sha256": "0hzp3vrxbnyc6w86v671wp0zchb634rgrwwcc00m0skcarm05sbg" + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.4.15/httpcore-4.4.15", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "7f2e0c573eaa7a74bac2e89b359e1f73d92a0a1d", + "sha256": "0hmi3c0p5a2m2v53i0fqmdqm3677q2fz6n6ydpwi16j9ih4fvfiw" + } + }, + + { + "path": "org/apache/maven/maven-artifact/3.8.6/maven-artifact-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "1637b7e8fc392e389752e79b827b883629285626", + "sha256": "0ar5p715427zbqdl6bvkkr926s7xmp9vpc93m1v15qsgyp3a88ny" + } + }, + + { + "path": "org/apache/maven/maven-builder-support/3.8.6/maven-builder-support-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "4d22a3faa8880efef2e960bb8a00c2a0b351c46a", + "sha256": "1kll3ln7mx7mvbnc8s718fadcc1qckz7nxgqcfg0723fyd9rfg9x" + } + }, + + { + "path": "org/apache/maven/maven-core/3.8.6/maven-core-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f945f1f19452214d360d453d9357275313f1cfd9", + "sha256": "1qaqyrxr1b2sz6d6amllynxw8c6cviydy1k8pi6ghlalipd515a3" + } + }, + + { + "path": "org/apache/maven/maven-model/3.8.6/maven-model-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "8ad31867c493c63ac60a3fa42356a72d20f8457b", + "sha256": "1c129h6n5daibjksa652ywbzji0fy4pd53qwk46bvsp7s326v60k" + } + }, + + { + "path": "org/apache/maven/maven-model-builder/3.8.6/maven-model-builder-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "8323a726c657becbffc8ea807970935dde3e3903", + "sha256": "1fyf717l3aa1jzwq0qss2i4d1m1rrpg6chq0sw6fr5319vmp98sw" + } + }, + + { + "path": "org/apache/maven/maven-plugin-api/3.8.6/maven-plugin-api-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "4138e2e9b39f364902ac263888feb7b2407a298e", + "sha256": "02ppxjnx9zly3dq7hwvh3x7jsmapirhsfznl9grid7r185h7yc9c" + } + }, + + { + "path": "org/apache/maven/maven-repository-metadata/3.8.6/maven-repository-metadata-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "c60a7640e1b829e60f8791b5edf3cf56a6556aca", + "sha256": "1zgasjbx3k0l3k7x2m2bkjsbgwnp5z3ywa4d8vmp46x85xk1y3m7" + } + }, + + { + "path": "org/apache/maven/maven-resolver-provider/3.8.6/maven-resolver-provider-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "c6dadedc9f6b5c1c02d0a93afd1857460b0f501d", + "sha256": "1a9ymv2hy7509hgib75g95my1xh6i9l7pjzzl14l3a3b6kqapcgv" + } + }, + + { + "path": "org/apache/maven/maven-settings/3.8.6/maven-settings-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "9ba5114c6c61c66fbc6b93c73085dc5b15585524", + "sha256": "0qcakn4g2h68s4s8blllw2889bhiqs8cr5056ywj8i954yhyzp36" + } + }, + + { + "path": "org/apache/maven/maven-settings-builder/3.8.6/maven-settings-builder-3.8.6", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "e5c92138dffd1f9e5454cd8224cf8e18253b1803", + "sha256": "1f44w8kz8v8k8an2n3i6lk48pb6sgjal6lcprzywash5qn4khnar" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-api/1.8.2/maven-resolver-api-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "af6c5d8660e3c127c3987c385069d301848800a2", + "sha256": "0pziii8ys35i4zi0sxw652pd04d2chr3hilpagf0qqpnah2qdf7n" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-connector-basic/1.8.2/maven-resolver-connector-basic-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "013b77ee054cd269a7e14667419d202799db18a2", + "sha256": "0w7im7wimjlzsdnvwsfvbji2i9sjaqc9xydb86qk3l3mg3cg2cbn" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-impl/1.8.2/maven-resolver-impl-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "5f08b1e7d0bc89d8254cd46881a5e228c98303df", + "sha256": "0slzdrhjhxl41lghrg3b0cxj07zqmmgkf5kqh1g5hksbplyy00n7" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-named-locks/1.8.2/maven-resolver-named-locks-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "6d81987c220cdb9672f15a1f2ee3e4ba9f80d50f", + "sha256": "1wf3xzv4xd7d21km0328s519kivaw1953mc33yq0mpdyarwhb6il" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-spi/1.8.2/maven-resolver-spi-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "0d0768a341173c2ef88e8dbcab2f30ebfe7ef526", + "sha256": "111vahiwcbaa28w98ksy8q3r23m7gdkqrr4bpy2jxd3vrazaq0d3" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-transport-file/1.8.2/maven-resolver-transport-file-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "b9a7004de9850fe9dcff8325c3d51ef4e387ef70", + "sha256": "0li1332vq03w11wdbni52qgkl2k6qdhskmfsdycqdqh9xm7c3f4c" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-transport-http/1.8.2/maven-resolver-transport-http-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "dbc0b6f022c51857fb801ba135c30af056b4c071", + "sha256": "0q0hhvwwfilzkvrza6czv4qhvcjh9shxaych2yah0nd6kvmlslsh" + } + }, + + { + "path": "org/apache/maven/resolver/maven-resolver-util/1.8.2/maven-resolver-util-1.8.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "cfb9b2e4c71d2a819aa4519800c9687c74b71584", + "sha256": "0jkvz7z07qsyp4i8p36n6zb53vmkh765fj4d3q7p8rbhnc601k52" + } + }, + + { + "path": "org/apache/maven/shared/maven-shared-utils/3.3.4/maven-shared-utils-3.3.4", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f87a61adb1e12a00dcc6cc6005a51e693aa7c4ac", + "sha256": "1nkdvyywfzbpd2ikpcrqb3zbz71rxc9gdqzsp0j0s172l32xj9br" + } + }, + + { + "path": "org/babashka/sci/0.8.41/sci-0.8.41", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "e8d59dc588a1e1f0c62c21fb47c093eeed043f04", + "sha256": "1lhv283fs0bnqbj1jhfgyy2rv6hspnhdgsiklqy3rqxrqbnrww6w" + } + }, + + { + "path": "org/babashka/sci.impl.types/0.0.2/sci.impl.types-0.0.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "45a05ece33609c3ad26a6ea4e05130560da82306", + "sha256": "0pwwqq11rcknpcwbwsbw7pgbgnd7hqiawn0r8yvk14qfwa6p7z46" + } + }, + + { + "path": "org/checkerframework/checker-qual/3.12.0/checker-qual-3.12.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "d5692f0526415fcc6de94bb5bfbd3afd9dd3b3e5", + "sha256": "1jzkwzdwd6wvrg0lrsh90df61frc5accp4y2x5fyqmx3q9d7h47z" + } + }, + + { + "path": "org/clojure/clojure/1.11.1/clojure-1.11.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "2896bc72c90da8125026c0e61df0470a084f9ec3", + "sha256": "1pml1iqzix0vzi51kf86c0yj8miss41lk52m2hanbd1s8blvd093" + } + }, + + { + "path": "org/clojure/clojurescript/1.11.60/clojurescript-1.11.60", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "bc14df6666853ed869d7b88aaa864111c65d0c7f", + "sha256": "104mwhda4k9mw7qdszxrzha1idy9yqsidssw4ljf4m58l3rpgkhl" + } + }, + + { + "path": "org/clojure/core.async/1.5.648/core.async-1.5.648", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "134b1e0eac15fdd1718f8a8ddacbb0902961558f", + "sha256": "0b1krpckkc6ai85h32mfs3v7awgjcld38s7nwbklmjf7pxpv1fjd" + } + }, + + { + "path": "org/clojure/core.cache/1.0.225/core.cache-1.0.225", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "ddd58c0d29cf1515d13351cc0770634ecac884f0", + "sha256": "1cd5yrlm80fqpjs0461isx57s0ymmgxwi0iqm7cdnp6sgsaally1" + } + }, + + { + "path": "org/clojure/core.memoize/1.0.253/core.memoize-1.0.253", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "bfa3ac940d93d50a14e4301b4cf8295e451b97c4", + "sha256": "1mhy3s8yjzyx47042za3b1d3nmp1bcqk83d2s30jdcra322hb4aa" + } + }, + + { + "path": "org/clojure/core.rrb-vector/0.1.2/core.rrb-vector-0.1.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "0404feea925608b921b56acd11d3b187a0d33fe4", + "sha256": "13hkx1285f2imqlj6wbgyxki2yg8rmfr49iq1zijxm1cgfx8xyai" + } + }, + + { + "path": "org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "a2a7ea21a695561924bc8506f3feb5d8c8f894d5", + "sha256": "1j6bsr1blcps3gw18d0jx538rg41jr1l7r37hlamrr5vf30aivh6" + } + }, + + { + "path": "org/clojure/data.int-map/1.2.1/data.int-map-1.2.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "9e7e4e17329ecd46e92384d9e8475d77800d204e", + "sha256": "055ip8l2y9004cydzz4i08p1ihlm5dz5fpknrln87ds9z9i5hiqh" + } + }, + + { + "path": "org/clojure/data.json/2.4.0/data.json-2.4.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "d779823f78d614897df79cd1823cb1cef840fa5b", + "sha256": "1pva908ndg2havnxyljipsbmqpwca6jjni9w64hd9v8y9scjygzc" + } + }, + + { + "path": "org/clojure/data.priority-map/1.1.0/data.priority-map-1.1.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "fc412d06788c1ea186117f8ea656d44fba654788", + "sha256": "0k3gxah05i1pgfqvqx2sc7v2yh3na3jiv1zkcvyin3zsf92aylgy" + } + }, + + { + "path": "org/clojure/data.xml/0.2.0-alpha8/data.xml-0.2.0-alpha8", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "c3dd8907b0a63a67082bc3091e304d9e1676d4b0", + "sha256": "159spfgsn0grlfszzybycyrg6apmv2622aqgc5nc6m7ndm7hrcdm" + } + }, + + { + "path": "org/clojure/google-closure-library/0.0-20230227-c7c0a541/google-closure-library-0.0-20230227-c7c0a541", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "533ce2bdbb7925db781449abb6527af1e6c5e782", + "sha256": "0js19lw8bp9gym3pn47h867vhf65j18qc6x1pfn883vkwyasm18l" + } + }, + + { + "path": "org/clojure/google-closure-library-third-party/0.0-20230227-c7c0a541/google-closure-library-third-party-0.0-20230227-c7c0a541", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f5ea82eb1309b81ada6a14371bb848323c65e38b", + "sha256": "0jk9v4bfrxvz6wq1s86msry2mf47nwcjfplnn41yabqc44g82hva" + } + }, + + { + "path": "org/clojure/java.classpath/1.0.0/java.classpath-1.0.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "015d06d1b304ac23104333bc8492a11bcc2e87e4", + "sha256": "0vvxzv3vagwhx3zw4w30xhz2kxf1zbggnhn75kymqpja6080wkn1" + } + }, + + { + "path": "org/clojure/math.combinatorics/0.2.0/math.combinatorics-0.2.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "aa95d25ea98bfe1152ea540f6e58a303c8cacc86", + "sha256": "0qvdsr5ryjmrpz622ivpyq5aayn42y4bk531vbasyj2yj7gcg35h" + } + }, + + { + "path": "org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "a7dad492f8d6cf657d82dcd6b31bda0899f1ac0e", + "sha256": "1q5ax2bkpsz11lmqnrl7pnabjsrps62xsyajlmbsjrjwnn78kv37" + } + }, + + { + "path": "org/clojure/test.check/1.1.1/test.check-1.1.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f33d988fd57bc9c11af1952db81c10f319c91416", + "sha256": "0y2hpkj7zl4yrpsl35ifpdaja5c72b8fpjcnmdgmld9c7cb1hlcl" + } + }, + + { + "path": "org/clojure/tools.analyzer/1.1.0/tools.analyzer-1.1.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "692882a35d7b50947d6e4852fba8a51d8d5e3646", + "sha256": "08fzw3srrppgq1d11sh1ghnyvi24ixa10kbqsncc7xyx7fybcs0k" + } + }, + + { + "path": "org/clojure/tools.analyzer.jvm/1.2.2/tools.analyzer.jvm-1.2.2", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "449691b55d7d526258ce02c69b4699f2897c494d", + "sha256": "0phfs1z1scvdi00348zjh223xncmgrkmlrnbca4dh7lk701gy34i" + } + }, + + { + "path": "org/clojure/tools.cli/1.0.206/tools.cli-1.0.206", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "5341b1ff68ec84e4ecff14c3611f81d36dba1041", + "sha256": "05pns6pzb9kkpknicabk2wdbiv4hi9wyr3w99aafmf8r35lx55vb" + } + }, + + { + "path": "org/clojure/tools.deps/0.17.1297/tools.deps-0.17.1297", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "7f27f66575190ddd5a343a77bba6911b0b4ec620", + "sha256": "0mlf7k0s26v4dx25hmix79002ghzar7jynnvcm9pcp56dsg4k78n" + } + }, + + { + "path": "org/clojure/tools.gitlibs/2.5.190/tools.gitlibs-2.5.190", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f300f872ac84711ba6df61cbba923c515c289068", + "sha256": "1i3jqzgfqiysy7np21gwrds32aay6a5p1xfnc3xp1jv30nnqd4bs" + } + }, + + { + "path": "org/clojure/tools.logging/1.2.4/tools.logging-1.2.4", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "3a85764aa30c434a5b0375a2ee72924aa040fa66", + "sha256": "0a8riw23pfkiiyz98ccmljpbw1z13mxl73pdgyz80j93s0y0mzj6" + } + }, + + { + "path": "org/clojure/tools.macro/0.1.5/tools.macro-0.1.5", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "925e200c906052e462e34a2c7e78a48ffec1dec4", + "sha256": "0j428ic8aazgv9s27820ybnsmgwfv7j8ywpkxs72dych9hlxf517" + } + }, + + { + "path": "org/clojure/tools.namespace/1.4.4/tools.namespace-1.4.4", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "ba1078e3fe6ebc33119a7c09722afa9f708aef1f", + "sha256": "01b3v2mii51krravn2rh1b9ysg9ph0mlb9c2dfbv0k6mckynhy04" + } + }, + + { + "path": "org/clojure/tools.reader/1.3.7/tools.reader-1.3.7", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "c60dfd3d79cbc5ec6264ba45c14b8d4c9b6708b2", + "sha256": "1mr0qx3gw1myfls07vrx3arwsi5n95b2lwk74ijpj0z3lzpzlj7m" + } + }, + + { + "path": "org/codehaus/plexus/plexus-cipher/2.0/plexus-cipher-2.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "425ea8e534716b4bff1ea90f39bd76be951d651b", + "sha256": "0fi0x8dq1skmxy8w7yhij7kshxig592k31zxmlgddzwyb9f1nzws" + } + }, + + { + "path": "org/codehaus/plexus/plexus-classworlds/2.6.0/plexus-classworlds-2.6.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "8587e80fcb38e70b70fae8d5914b6376bfad6259", + "sha256": "0j9m9zbw152gdqvigwh2l922z4nrxzbdbgky86f7qy4zqig7rxsj" + } + }, + + { + "path": "org/codehaus/plexus/plexus-component-annotations/2.1.0/plexus-component-annotations-2.1.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "2f2147a6cc6a119a1b51a96f31d45c557f6244b9", + "sha256": "1b5c6ay7nbphafqs6h7apars9xis0j06011685cgkg5mx5y63qxx" + } + }, + + { + "path": "org/codehaus/plexus/plexus-interpolation/1.26/plexus-interpolation-1.26", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "25b919c664b79795ccde0ede5cee0fd68b544197", + "sha256": "0qi633bwjwwaadmsrzjg6i0abpxkkz7xzg34llz112bqw4n43ddk" + } + }, + + { + "path": "org/codehaus/plexus/plexus-sec-dispatcher/2.0/plexus-sec-dispatcher-2.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "f89c5080614ffd0764e49861895dbedde1b47237", + "sha256": "1szwmkvlw8jf4fcvprfwicc85gy4l81v1055vmv02y2c1jb3jcc7" + } + }, + + { + "path": "org/codehaus/plexus/plexus-utils/3.3.1/plexus-utils-3.3.1", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "9b41b2b76b1bfe3774411fe22f5868058a9fc822", + "sha256": "0aig48q1jihh02l2glj8wp1qi74a6llvkf9fklj4z2aspv6hymsb" + } + }, + + { + "path": "org/eclipse/jetty/jetty-client/9.4.48.v20220622/jetty-client-9.4.48.v20220622", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "fc2e953a7030045e1ca467d57e200856a170848e", + "sha256": "058x9p1k8r5miwyn2za86nz27mbnmnk956crfmi2jsyk004zx2bz" + } + }, + + { + "path": "org/eclipse/jetty/jetty-http/9.4.48.v20220622/jetty-http-9.4.48.v20220622", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "8cb235e70bda0c5e97a41e7ee0ea33ee7f5bcc6a", + "sha256": "0phpz22cj5n8950pdbb97fwbmr4f4l8h8lvh0kg8ya159j0196f9" + } + }, + + { + "path": "org/eclipse/jetty/jetty-io/9.4.48.v20220622/jetty-io-9.4.48.v20220622", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "b09b55209d0a304e542f779750a01f6914dc55e7", + "sha256": "0iwb1d3a18yl2x6sbawi2cl5k6d24gmx2rmj1fks01c96jh60bsd" + } + }, + + { + "path": "org/eclipse/jetty/jetty-util/9.4.48.v20220622/jetty-util-9.4.48.v20220622", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "7efc06f7ec0ff33d8c219bcc8c7415280c103669", + "sha256": "11rbr3mayczff851l34ccbmkpi71dzy2havrqalvxd54ki2gvji4" + } + }, + + { + "path": "org/eclipse/sisu/org.eclipse.sisu.inject/0.3.5/org.eclipse.sisu.inject-0.3.5", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "d4265dd4f0f1d7a06d80df5a5f475d5ff9c17140", + "sha256": "04ys5z436vd2m8ipn5fifmwbvpcif7250k9sc2yx5qfwph8416f5" + } + }, + + { + "path": "org/eclipse/sisu/org.eclipse.sisu.plexus/0.3.5/org.eclipse.sisu.plexus-0.3.5", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "d71996bb2e536f966b3b70e647067fff3b73d32f", + "sha256": "1157yqjxvr24wpgy4zi4mnjpm3jjlmcmrmd7ywh6z0khdl4n2k3y" + } + }, + + { + "path": "org/javassist/javassist/3.18.1-GA/javassist-3.18.1-GA", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "d9a09f7732226af26bf99f19e2cffe0ae219db5b", + "sha256": "0qk19ja8zv4jdxcmw4krcdax1j4iham9gszmjc7vp66hmwqi5drz" + } + }, + + { + "path": "org/java-websocket/Java-WebSocket/1.5.3/Java-WebSocket-1.5.3", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "9c26b6a6e732a1242db576a50dc3a12e446e2717", + "sha256": "0gvsbr49lr8ylv4k8gsnd9wyfc4gs5wr15klrmnyfmxnr4fw4vsd" + } + }, + + { + "path": "org/jboss/logging/jboss-logging/3.4.3.Final/jboss-logging-3.4.3.Final", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "c4bd7e12a745c0e7f6cf98c45cdcdf482fd827ea", + "sha256": "0jdcziqbdzmah981zv3q88k2zrnclr2h1k3h3vjn002m9p54qchb" + } + }, + + { + "path": "org/jboss/threads/jboss-threads/3.5.0.Final/jboss-threads-3.5.0.Final", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "dd23d4788b3eafe9597ef3fe028e46ceb293ba8d", + "sha256": "1cchp54dw1578my27siim3h5jbf240aq8q6xd3z2b1ajgxxbcl71" + } + }, + + { + "path": "org/jboss/xnio/xnio-api/3.8.8.Final/xnio-api-3.8.8.Final", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "1ba9c8b9a8dea1c6cd656155943e6d4c2c631fa7", + "sha256": "0wvqdcbardi55l77aw5404qgw70384hw155xvc66shn7l6z8h6bh" + } + }, + + { + "path": "org/jboss/xnio/xnio-nio/3.8.8.Final/xnio-nio-3.8.8.Final", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "a5d2faf8d02e0a0bb9e9eabca8e38173cb640331", + "sha256": "0pxw2iv1q1yzk39knc650rg4vflsyjzhf07mwm2s5aqn5h82sk3i" + } + }, + + { + "path": "org/jspecify/jspecify/0.2.0/jspecify-0.2.0", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "89ca55e02b85c959bd0c4c0c13a0b1885829af44", + "sha256": "1bn0nw88v70i4sqxlfl3jsmfi0180426kh77dx07955ysbl9k8vx" + } + }, + + { + "path": "org/msgpack/msgpack/0.6.12/msgpack-0.6.12", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "6a0c88fe022993c490011c3dce7127b29f9a9b3b", + "sha256": "0plvpp9ra9848sb3psx2yi0gvk5gm146hhnwln08wj10hmfsd770" + } + }, + + { + "path": "org/ow2/asm/asm/9.4/asm-9.4", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "b4e0e2d2e023aa317b7cfcfc916377ea348e07d1", + "sha256": "10gk2l71sfj4d0sgj971abh2d8cl19slay89kfh6bbs5vjry5l1r" + } + }, + + { + "path": "org/slf4j/jcl-over-slf4j/1.7.36/jcl-over-slf4j-1.7.36", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "d877e195a05aca4a2f1ad2ff14bfec1393af4b5e", + "sha256": "15ci04f0g0avrfih736hb6pg1jsfx5ciy4jx6qbjqxr3sa7wlmxb" + } + }, + + { + "path": "org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "7cf2726fdcfbc8610f9a71fb3ed639871f315340", + "sha256": "0v1b7iydpnh2rvyzqvqbqm19vr2qva8icq84690bppnpr06r6608" + } + }, + + { + "path": "org/slf4j/slf4j-nop/2.0.9/slf4j-nop-2.0.9", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "fa0627049304bf501bf6a0e4d1416bcaec9a72a9", + "sha256": "10vdg9qkailjbzmf5mfnv328675swmnf1x3g9v7ymhxs29xkc4jn" + } + }, + + { + "path": "org/wildfly/client/wildfly-client-config/1.0.1.Final/wildfly-client-config-1.0.1.Final", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "2a803b23c40a0de0f03a90d1fd3755747bc05f4b", + "sha256": "1crv5kxfsbkmcfw504yqhzf26zrjgpkw1wmhxi1v1swlrriyk940" + } + }, + + { + "path": "org/wildfly/common/wildfly-common/1.5.4.Final/wildfly-common-1.5.4.Final", + "host": "https://repo1.maven.org/maven2", + "jar": { + "sha1": "735ceee5616d5143bac1bc740e444697073c002f", + "sha256": "0sp48y3q7bmp3bjb7a6fpz8gb7ssizvxlw5wdv2xwa6mifpkrnlz" + } + }, + + { + "path": "prismatic/schema/1.1.7/schema-1.1.7", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "289b571f1cbab59aef436fc0af56e7efc5322685", + "sha256": "01zc13qhkyghhr4s1if9agbgz38gvrg8x62gn1g8fjll9nq7aca9" + } + }, + + { + "path": "reagent/reagent/1.2.0/reagent-1.2.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "1b9a181b5c7ed3557768d2ea0c66f5616aef5e97", + "sha256": "0scvkzfqjs613z10rngh7427v3pxdqablf0fcl65pbpkzz16wgav" + } + }, + + { + "path": "re-com/re-com/2.8.0/re-com-2.8.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "fd038eef3c36c460613e94f2c70fc84c6eb35a66", + "sha256": "0r431yq5hf8zc4j2k3lsi3vni60kk8ncinqrb7xv2qcyjw4qh178" + } + }, + + { + "path": "refactor-nrepl/refactor-nrepl/3.9.1/refactor-nrepl-3.9.1", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "f949af92dd1d6fef59d7447bd0cb62118e34eca1", + "sha256": "0dml9yvjmji6xk2sk3cdmkvvnh13rw29szxxl363hap8yg28xcs1" + } + }, + + { + "path": "re-frame/re-frame/1.4.3/re-frame-1.4.3", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "74e132cc593ed2d72a5d4874954c58677a905f0e", + "sha256": "1xdmbjb0gv6dby98y1nx3brz9sdkfzxz9cjk8ajcr1f4zabcaapb" + } + }, + + { + "path": "re-frisk-remote/re-frisk-remote/1.6.0/re-frisk-remote-1.6.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "dea48c4be6421c50c4bbf2c1be2ea3a6b2418d8d", + "sha256": "1rkfyc5fwbafx56a6zhy0k9ygmlxp9asqzby09a5xzjsmkrx2jf1" + } + }, + + { + "path": "re-frisk/sente/1.15.0/sente-1.15.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "d54aa873a848f624a282c2c2f7df947c976a77b5", + "sha256": "0yhsqpi3d8x11pl1aq8z23fdyjspvq07dsyvrlkpbyx7rm64i21h" + } + }, + + { + "path": "ring-cors/ring-cors/0.1.8/ring-cors-0.1.8", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "4788dcaca6b2429bf823c1235dbb44cd5689584a", + "sha256": "1382hxpgfpjdn0lcgq512yvbvq661skwd7lrakpnq9zs827jq9mc" + } + }, + + { + "path": "ring/ring-codec/1.2.0/ring-codec-1.2.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "fbcc4a141c638a3bd386df8ed04c05d0481be209", + "sha256": "1hk58ln4vijf5zk2c61x8is5fhwgyrqhc49qnxbmn1b2002svn3g" + } + }, + + { + "path": "ring/ring-core/1.9.6/ring-core-1.9.6", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "8ca97618f914401c4112e2fd28d24d47c4fc2815", + "sha256": "1zj5dpcyvivvf7zsggvrd75ykr98pblv3bpfr01jbkwjwx1s2d4a" + } + }, + + { + "path": "thheller/shadow-client/1.3.3/shadow-client-1.3.3", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "cb2034364161b68d1994d374eb59d6d2b0613989", + "sha256": "13avr26yxns3kcab46anf5z4lxmyn72qydvgppvv26vcf77acx0p" + } + }, + + { + "path": "thheller/shadow-cljs/2.26.2/shadow-cljs-2.26.2-aot", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "ab6b31649cb03e352369698781207db4f6da6d5f", + "sha256": "0mh28nykpillkym7hvplrr7vy5hn6a2ymafyf4yfl5y2kfx4yq42" + } + }, + + { + "path": "thheller/shadow-cljsjs/0.0.22/shadow-cljsjs-0.0.22", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "4323f8e603a952cae34c4c6db04141e97928434f", + "sha256": "1bljcig3hkn1nhfbg2w6apz8lwm8qk74qcwd8l2mbw1plfxa0fzn" + } + }, + + { + "path": "thheller/shadow-undertow/0.3.3/shadow-undertow-0.3.3", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "ba377c3b70fd942b3ca7e38f055412a441626711", + "sha256": "10w2slj4znm93j0qq3ldj9hj0i7a50vbnmn6kv8i77fakikdjc48" + } + }, + + { + "path": "thheller/shadow-util/0.7.0/shadow-util-0.7.0", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "61a374c204d797a92d9daeb4b8d9effeb0d81183", + "sha256": "10fdbqrz7zcfan8x4aikl2i532dk9dz0dfxrww91mgynf0g90rp5" + } + }, + + { + "path": "tigris/tigris/0.1.2/tigris-0.1.2", + "host": "https://repo.clojars.org", + "jar": { + "sha1": "a122db758561d995a83cbb40f252b64d8b0f506e", + "sha256": "184p1wqcc6ikj9gpaygv4f1mf1p6mqg3j6x1jmqfa53cvf769aj9" + } + } +] diff --git a/nix/deps/clojure/deps.list b/nix/deps/clojure/deps.list new file mode 100644 index 00000000000..82c73717da3 --- /dev/null +++ b/nix/deps/clojure/deps.list @@ -0,0 +1,175 @@ +aopalliance/aopalliance/1.0/aopalliance-1.0.jar +args4j/args4j/2.33/args4j-2.33.jar +babashka/fs/0.2.16/fs-0.2.16.jar +bidi/bidi/2.1.6/bidi-2.1.6.jar +binaryage/env-config/0.2.2/env-config-0.2.2.jar +binaryage/oops/0.7.2/oops-0.7.2.jar +borkdude/dynaload/0.3.5/dynaload-0.3.5.jar +borkdude/edamame/1.3.23/edamame-1.3.23.jar +borkdude/sci.impl.reflector/0.0.1/sci.impl.reflector-0.0.1.jar +camel-snake-kebab/camel-snake-kebab/0.4.3/camel-snake-kebab-0.4.3.jar +cheshire/cheshire/5.11.0/cheshire-5.11.0.jar +cider/cider-nrepl/0.31.0/cider-nrepl-0.31.0.jar +cider/piggieback/0.4.1/piggieback-0.4.1.jar +clj-kondo/clj-kondo/2024.03.13/clj-kondo-2024.03.13.jar +cljs-bean/cljs-bean/1.9.0/cljs-bean-1.9.0.jar +clout/clout/2.1.2/clout-2.1.2.jar +com/andrewmcveigh/cljs-time/0.5.2/cljs-time-0.5.2.jar +com/bhauman/cljs-test-display/0.1.1/cljs-test-display-0.1.1.jar +com/cognitect/aws/api/0.8.612/api-0.8.612.jar +com/cognitect/aws/endpoints/1.1.12.321/endpoints-1.1.12.321.jar +com/cognitect/aws/s3/822.2.1145.0/s3-822.2.1145.0.jar +com/cognitect/http-client/1.0.115/http-client-1.0.115.jar +com/cognitect/transit-clj/1.0.329/transit-clj-1.0.329.jar +com/cognitect/transit-cljs/0.8.280/transit-cljs-0.8.280.jar +com/cognitect/transit-java/1.0.362/transit-java-1.0.362.jar +com/cognitect/transit-js/0.8.874/transit-js-0.8.874.jar +com/fasterxml/jackson/core/jackson-core/2.13.3/jackson-core-2.13.3.jar +com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.13.3/jackson-dataformat-cbor-2.13.3.jar +com/fasterxml/jackson/dataformat/jackson-dataformat-smile/2.13.3/jackson-dataformat-smile-2.13.3.jar +com/github/javaparser/javaparser-core/3.25.3/javaparser-core-3.25.3.jar +com/github/jpmonettas/flow-storm-inst/3.7.5/flow-storm-inst-3.7.5.jar +com/github/jpmonettas/hansel/0.1.78/hansel-0.1.78.jar +com/google/auto/value/auto-value-annotations/1.6/auto-value-annotations-1.6.jar +com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar +com/google/code/gson/gson/2.9.1/gson-2.9.1.jar +com/googlecode/json-simple/json-simple/1.1.1/json-simple-1.1.1.jar +com/google/errorprone/error_prone_annotations/2.15.0/error_prone_annotations-2.15.0.jar +com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar +com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.jar +com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar +com/google/inject/guice/4.2.2/guice-4.2.2-no_aop.jar +com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar +com/google/javascript/closure-compiler-unshaded/v20230802/closure-compiler-unshaded-v20230802.jar +com/google/protobuf/protobuf-java/3.21.12/protobuf-java-3.21.12.jar +com/google/re2j/re2j/1.3/re2j-1.3.jar +commons-codec/commons-codec/1.15/commons-codec-1.15.jar +commons-fileupload/commons-fileupload/1.4/commons-fileupload-1.4.jar +commons-io/commons-io/2.11.0/commons-io-2.11.0.jar +compojure/compojure/1.5.2/compojure-1.5.2.jar +com/taoensso/encore/3.68.0/encore-3.68.0.jar +com/taoensso/timbre/6.3.1/timbre-6.3.1.jar +com/taoensso/truss/1.11.0/truss-1.11.0.jar +com/taoensso/tufte/2.6.3/tufte-2.6.3.jar +crypto-equality/crypto-equality/1.0.1/crypto-equality-1.0.1.jar +crypto-random/crypto-random/1.2.1/crypto-random-1.2.1.jar +day8/re-frame/test/0.1.5/test-0.1.5.jar +expound/expound/0.9.0/expound-0.9.0.jar +fipp/fipp/0.6.26/fipp-0.6.26.jar +funcool/promesa/11.0.678/promesa-11.0.678.jar +hiccup/hiccup/1.0.5/hiccup-1.0.5.jar +http-kit/http-kit/2.2.0/http-kit-2.2.0.jar +instaparse/instaparse/1.4.0/instaparse-1.4.0.jar +io/aviso/pretty/1.4.4/pretty-1.4.4.jar +io/github/clojure/tools.build/0.9.4/tools.build-0.9.4.jar +io/methvin/directory-watcher/0.17.1/directory-watcher-0.17.1.jar +io/replikativ/datalog-parser/0.2.25/datalog-parser-0.2.25.jar +io/undertow/undertow-core/2.3.10.Final/undertow-core-2.3.10.Final.jar +javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar +javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar +javax/inject/javax.inject/1/javax.inject-1.jar +javax/servlet/servlet-api/2.5/servlet-api-2.5.jar +javax/xml/bind/jaxb-api/2.3.0/jaxb-api-2.3.0.jar +medley/medley/0.8.2/medley-0.8.2.jar +metosin/malli/0.13.0/malli-0.13.0.jar +mvxcvi/arrangement/2.1.0/arrangement-2.1.0.jar +net/cgrand/macrovich/0.2.1/macrovich-0.2.1.jar +net/java/dev/jna/jna/5.12.1/jna-5.12.1.jar +nrepl/bencode/1.1.0/bencode-1.1.0.jar +nrepl/nrepl/1.0.0/nrepl-1.0.0.jar +nubank/matcher-combinators/3.9.1/matcher-combinators-3.9.1.jar +org/apache/ant/ant/1.10.11/ant-1.10.11.jar +org/apache/ant/ant-launcher/1.10.11/ant-launcher-1.10.11.jar +org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar +org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar +org/apache/httpcomponents/httpcore/4.4.15/httpcore-4.4.15.jar +org/apache/maven/maven-artifact/3.8.6/maven-artifact-3.8.6.jar +org/apache/maven/maven-builder-support/3.8.6/maven-builder-support-3.8.6.jar +org/apache/maven/maven-core/3.8.6/maven-core-3.8.6.jar +org/apache/maven/maven-model/3.8.6/maven-model-3.8.6.jar +org/apache/maven/maven-model-builder/3.8.6/maven-model-builder-3.8.6.jar +org/apache/maven/maven-plugin-api/3.8.6/maven-plugin-api-3.8.6.jar +org/apache/maven/maven-repository-metadata/3.8.6/maven-repository-metadata-3.8.6.jar +org/apache/maven/maven-resolver-provider/3.8.6/maven-resolver-provider-3.8.6.jar +org/apache/maven/maven-settings/3.8.6/maven-settings-3.8.6.jar +org/apache/maven/maven-settings-builder/3.8.6/maven-settings-builder-3.8.6.jar +org/apache/maven/resolver/maven-resolver-api/1.8.2/maven-resolver-api-1.8.2.jar +org/apache/maven/resolver/maven-resolver-connector-basic/1.8.2/maven-resolver-connector-basic-1.8.2.jar +org/apache/maven/resolver/maven-resolver-impl/1.8.2/maven-resolver-impl-1.8.2.jar +org/apache/maven/resolver/maven-resolver-named-locks/1.8.2/maven-resolver-named-locks-1.8.2.jar +org/apache/maven/resolver/maven-resolver-spi/1.8.2/maven-resolver-spi-1.8.2.jar +org/apache/maven/resolver/maven-resolver-transport-file/1.8.2/maven-resolver-transport-file-1.8.2.jar +org/apache/maven/resolver/maven-resolver-transport-http/1.8.2/maven-resolver-transport-http-1.8.2.jar +org/apache/maven/resolver/maven-resolver-util/1.8.2/maven-resolver-util-1.8.2.jar +org/apache/maven/shared/maven-shared-utils/3.3.4/maven-shared-utils-3.3.4.jar +org/babashka/sci/0.8.41/sci-0.8.41.jar +org/babashka/sci.impl.types/0.0.2/sci.impl.types-0.0.2.jar +org/checkerframework/checker-qual/3.12.0/checker-qual-3.12.0.jar +org/clojure/clojure/1.11.1/clojure-1.11.1.jar +org/clojure/clojurescript/1.11.60/clojurescript-1.11.60.jar +org/clojure/core.async/1.5.648/core.async-1.5.648.jar +org/clojure/core.cache/1.0.225/core.cache-1.0.225.jar +org/clojure/core.memoize/1.0.253/core.memoize-1.0.253.jar +org/clojure/core.rrb-vector/0.1.2/core.rrb-vector-0.1.2.jar +org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar +org/clojure/data.int-map/1.2.1/data.int-map-1.2.1.jar +org/clojure/data.json/2.4.0/data.json-2.4.0.jar +org/clojure/data.priority-map/1.1.0/data.priority-map-1.1.0.jar +org/clojure/data.xml/0.2.0-alpha8/data.xml-0.2.0-alpha8.jar +org/clojure/google-closure-library/0.0-20230227-c7c0a541/google-closure-library-0.0-20230227-c7c0a541.jar +org/clojure/google-closure-library-third-party/0.0-20230227-c7c0a541/google-closure-library-third-party-0.0-20230227-c7c0a541.jar +org/clojure/java.classpath/1.0.0/java.classpath-1.0.0.jar +org/clojure/math.combinatorics/0.2.0/math.combinatorics-0.2.0.jar +org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar +org/clojure/test.check/1.1.1/test.check-1.1.1.jar +org/clojure/tools.analyzer/1.1.0/tools.analyzer-1.1.0.jar +org/clojure/tools.analyzer.jvm/1.2.2/tools.analyzer.jvm-1.2.2.jar +org/clojure/tools.cli/1.0.206/tools.cli-1.0.206.jar +org/clojure/tools.deps/0.17.1297/tools.deps-0.17.1297.jar +org/clojure/tools.gitlibs/2.5.190/tools.gitlibs-2.5.190.jar +org/clojure/tools.logging/1.2.4/tools.logging-1.2.4.jar +org/clojure/tools.macro/0.1.5/tools.macro-0.1.5.jar +org/clojure/tools.namespace/1.4.4/tools.namespace-1.4.4.jar +org/clojure/tools.reader/1.3.7/tools.reader-1.3.7.jar +org/codehaus/plexus/plexus-cipher/2.0/plexus-cipher-2.0.jar +org/codehaus/plexus/plexus-classworlds/2.6.0/plexus-classworlds-2.6.0.jar +org/codehaus/plexus/plexus-component-annotations/2.1.0/plexus-component-annotations-2.1.0.jar +org/codehaus/plexus/plexus-interpolation/1.26/plexus-interpolation-1.26.jar +org/codehaus/plexus/plexus-sec-dispatcher/2.0/plexus-sec-dispatcher-2.0.jar +org/codehaus/plexus/plexus-utils/3.3.1/plexus-utils-3.3.1.jar +org/eclipse/jetty/jetty-client/9.4.48.v20220622/jetty-client-9.4.48.v20220622.jar +org/eclipse/jetty/jetty-http/9.4.48.v20220622/jetty-http-9.4.48.v20220622.jar +org/eclipse/jetty/jetty-io/9.4.48.v20220622/jetty-io-9.4.48.v20220622.jar +org/eclipse/jetty/jetty-util/9.4.48.v20220622/jetty-util-9.4.48.v20220622.jar +org/eclipse/sisu/org.eclipse.sisu.inject/0.3.5/org.eclipse.sisu.inject-0.3.5.jar +org/eclipse/sisu/org.eclipse.sisu.plexus/0.3.5/org.eclipse.sisu.plexus-0.3.5.jar +org/javassist/javassist/3.18.1-GA/javassist-3.18.1-GA.jar +org/java-websocket/Java-WebSocket/1.5.3/Java-WebSocket-1.5.3.jar +org/jboss/logging/jboss-logging/3.4.3.Final/jboss-logging-3.4.3.Final.jar +org/jboss/threads/jboss-threads/3.5.0.Final/jboss-threads-3.5.0.Final.jar +org/jboss/xnio/xnio-api/3.8.8.Final/xnio-api-3.8.8.Final.jar +org/jboss/xnio/xnio-nio/3.8.8.Final/xnio-nio-3.8.8.Final.jar +org/jspecify/jspecify/0.2.0/jspecify-0.2.0.jar +org/msgpack/msgpack/0.6.12/msgpack-0.6.12.jar +org/ow2/asm/asm/9.4/asm-9.4.jar +org/slf4j/jcl-over-slf4j/1.7.36/jcl-over-slf4j-1.7.36.jar +org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.jar +org/slf4j/slf4j-nop/2.0.9/slf4j-nop-2.0.9.jar +org/wildfly/client/wildfly-client-config/1.0.1.Final/wildfly-client-config-1.0.1.Final.jar +org/wildfly/common/wildfly-common/1.5.4.Final/wildfly-common-1.5.4.Final.jar +prismatic/schema/1.1.7/schema-1.1.7.jar +reagent/reagent/1.2.0/reagent-1.2.0.jar +re-com/re-com/2.8.0/re-com-2.8.0.jar +refactor-nrepl/refactor-nrepl/3.9.1/refactor-nrepl-3.9.1.jar +re-frame/re-frame/1.4.3/re-frame-1.4.3.jar +re-frisk-remote/re-frisk-remote/1.6.0/re-frisk-remote-1.6.0.jar +re-frisk/sente/1.15.0/sente-1.15.0.jar +ring-cors/ring-cors/0.1.8/ring-cors-0.1.8.jar +ring/ring-codec/1.2.0/ring-codec-1.2.0.jar +ring/ring-core/1.9.6/ring-core-1.9.6.jar +thheller/shadow-client/1.3.3/shadow-client-1.3.3.jar +thheller/shadow-cljs/2.26.2/shadow-cljs-2.26.2-aot.jar +thheller/shadow-cljsjs/0.0.22/shadow-cljsjs-0.0.22.jar +thheller/shadow-undertow/0.3.3/shadow-undertow-0.3.3.jar +thheller/shadow-util/0.7.0/shadow-util-0.7.0.jar +tigris/tigris/0.1.2/tigris-0.1.2.jar diff --git a/nix/deps/clojure/generate.sh b/nix/deps/clojure/generate.sh new file mode 100755 index 00000000000..42d34f73a71 --- /dev/null +++ b/nix/deps/clojure/generate.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +set -e + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" + +if [[ -z "${IN_NIX_SHELL}" ]]; then + echo "Remember to call 'make shell'!" + exit 1 +fi + +CLASSPATH_FILE="${GIT_ROOT}/nix/deps/clojure/deps.list" +JSON_DEPS_FILE="${GIT_ROOT}/nix/deps/clojure/deps.json" +MAVEN_CACHE_PATH="${HOME}/.m2/repository" + +declare -A REPOS=( + [central]="https://repo1.maven.org/maven2" + [clojars]="https://repo.clojars.org" +) + +function gen_deps_list() { + # split into separate lines + CLASSPATH_LINES=$(yarn shadow-cljs classpath --force-spawn | tr ':' '\n') + # remove unnecessary lines + CLASSPATH_LINES=$(echo "${CLASSPATH_LINES}" | grep -vE '^(\$|yarn|Done|shadow-cljs|src|test)') + # remove local home path + CLASSPATH_LINES=$(echo "${CLASSPATH_LINES}" | sed "s#${MAVEN_CACHE_PATH}/##") + # print cleaned up entries from classpath + echo "${CLASSPATH_LINES}" | sort | uniq +} + +function get_repo_for_dir() { + # This file has different name depending on Maven version + if [[ -f "${1}/_remote.repositories" ]]; then + REPO_FILE="${1}/_remote.repositories" + elif [[ -f "${1}/_maven.repositories" ]]; then + REPO_FILE="${1}/_maven.repositories" + else + echo -e "${RED}Cannot find Maven repo file for:${RST} ${1}" >&2 + exit 1 + fi + grep -oP '.*>\K\w+' "${REPO_FILE}" | uniq +} + +function get_nix_sha() { + nix hash-file --base32 --type sha256 "$1" 2> /dev/null +} + +function nix_entry_from_jar() { + JAR_REL_PATH="${1}" + JAR_REL_NAME="${JAR_REL_PATH%.jar}" + JAR_PATH="${MAVEN_CACHE_PATH}/${JAR_REL_PATH}" + JAR_NAME=$(basename "${JAR_PATH}") + JAR_DIR=$(dirname "${JAR_PATH}") + + REPO_NAME=$(get_repo_for_dir "${JAR_DIR}") + REPO_URL=${REPOS[${REPO_NAME}]} + + JAR_SHA1=$(cat "${JAR_PATH}.sha1") + JAR_SHA256=$(get_nix_sha "${JAR_PATH}") + + # Format into a Nix attrset entry + echo -n " + { + \"path\": \"${JAR_REL_NAME}\", + \"host\": \"${REPO_URL}\", + \"jar\": { + \"sha1\": \"${JAR_SHA1}\", + \"sha256\": \"${JAR_SHA256}\" + } + }" +} + +# generate the deps.list file with relative paths +gen_deps_list > "${CLASSPATH_FILE}" +echo "Saved Clojure deps list to: ${CLASSPATH_FILE}" >&2 + +# add the header which defines repo URLs +echo -en "[" > "${JSON_DEPS_FILE}" + +# for each dependency generate an attrset with metadata +DEPS=$(cat "${CLASSPATH_FILE}") +LAST=$(tail -n1 "${CLASSPATH_FILE}") +for DEP in ${DEPS}; do + nix_entry_from_jar "${DEP}" >> "${JSON_DEPS_FILE}" + # JSON is stupid and requires no comma on last element + [[ "${DEP}" != "${LAST}" ]] && echo "," >> "${JSON_DEPS_FILE}" +done + +# close the attrset +echo -e "\n]" >> "${JSON_DEPS_FILE}" +echo "Generated Clojure deps for Nix: ${JSON_DEPS_FILE}" >&2 diff --git a/nix/deps/gradle/README.md b/nix/deps/gradle/README.md new file mode 100644 index 00000000000..4d8a47ec8d8 --- /dev/null +++ b/nix/deps/gradle/README.md @@ -0,0 +1,80 @@ +# Description + +This directory contains the tools and the data that allows Nix to manage Gradle project dependencies for the app. + +# Usage + +Simply calling `generate.sh` should result in a `deps.json` file which is used in the derivation that provides Gradle dependencies when building the Android app. + +You can see in [`nix/mobile/android/release.nix`](../../mobile/android/build.nix) that it's used via the `-Dmaven.repo.local='${deps.gradle}'` Gradle flag. + +# Files + +First we have the files that store data on Gradle dependencies: + +* `proj.list` - List of projects of which our Gradle project consists. +* `deps.list` - List of dependencies for our project and sub-projects. +* `deps.urls` - List of URLs to POMs for all of the dependencies. +* `deps.json` - Final file containing all of the necessary data for Nix. + +Generating scripts: + +- `generate.sh` - Main script which generates all the above files. +- `get_projects.sh` - Calls Gradle to get a list of sub-projects. +- `get_deps.sh` - Calls Gradle to get all the dependencies of sub-projects. +- `gradle_parser.awk` - An AWK script that parses above Gradle output. +- `url2json.sh` - Converts the list of URLs into a format consumable by Nix. +- `add_package.sh` - Allows for adding a missing package manually. + +Finally we have the Nix derivation in `default.nix` which produces a derivation with all of the Gradle project dependencies: +``` + $ nix-build --no-out-link --attr pkgs.deps.gradle default.nix +/nix/store/57g95ik19k6gs5w68yid3hzhsax60i3m-status-mobile-maven-deps + + $ ls -l /nix/store/57g95ik19k6gs5w68yid3hzhsax60i3m-status-mobile-maven-deps | head -n 5 +total 32 +dr-xr-xr-x 3 root root 3 Jan 1 1970 android +dr-xr-xr-x 35 root root 35 Jan 1 1970 androidx +dr-xr-xr-x 15 root root 15 Jan 1 1970 com +dr-xr-xr-x 3 root root 3 Jan 1 1970 commons-cli +... +``` + +# Dependencies + +One dependency these scripts require is the [go-maven-resolver](https://github.com/status-im/go-maven-resolver) which turns a list of packages into list of all URLs of POMs for them and their dependencies. + +# Known Issues + +One edge case that is currently not automatically handled by this setup is an addition of a new dependency that requires a version of Gradle we don't have. In that case running `make nix-update-gradle` would fail with: +``` +AILURE: Build failed with an exception. + +* What went wrong: +A problem occurred configuring project ':react-native-example-project'. +> Could not resolve all artifacts for configuration ':react-native-example-project:classpath'. + > Could not find com.android.tools.build:gradle:3.4.0. + Searched in the following locations: + - file:/nix/store/34a4qd5qhg2a9kq6a0q9lp7hgmi48q4x-status-mobile-maven-deps/com/android/tools/build/gradle/3.4.0/gradle-3.4.0.pom + - file:/nix/store/34a4qd5qhg2a9kq6a0q9lp7hgmi48q4x-status-mobile-maven-deps/com/android/tools/build/gradle/3.4.0/gradle-3.4.0.jar + Required by: + project :react-native-example-project +``` +This happens because `get_projects.sh` and `get_deps.sh` depend on running Gradle to generate their output, and that will not work because Gradle cannot find the matching JAR for the new dependency. + +You can use the `add_package.sh` script to add missing Gradle dependencies: +``` + > make shell TARGET=gradle +Configuring Nix shell for target 'gradle'... + +[nix-shell:~/work/status-mobile]$ nix/deps/gradle/add_package.sh com.android.tools.build:gradle:3.5.3 +Changes made: + nix/deps/gradle/deps.urls | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +Regenerating Nix files... +Successfully added: com.android.tools.build:gradle:3.5.3 + +NOTICE: Running 'make nix-update-gradle' in a new shell is recommended. +``` +Keep in mind that the changes made by this script do not affect the already spawned shell. diff --git a/nix/deps/gradle/add_package.sh b/nix/deps/gradle/add_package.sh new file mode 100755 index 00000000000..59ffbbc27f5 --- /dev/null +++ b/nix/deps/gradle/add_package.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# This script allows for adding a package by providing a Maven full name. +# Such name consists of 3 sections separated by the colon character. +# Example: com.android.tools.build:gradle:3.5.3 + +set -Eeuo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: add_package.sh " >&2 + exit 1 +fi +if ! command -v go-maven-resolver &> /dev/null; then + echo "Use 'make shell TARGET=gradle' for this script" >&2 + exit 1 +fi + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" + +# Add missing package to dependencies. +echo "${1}" | go-maven-resolver >> nix/deps/gradle/deps.urls + +# Remove duplicates and sort. +sort -uVo nix/deps/gradle/deps.urls nix/deps/gradle/deps.urls + +echo -e "${GRN}Changes made:${RST}" >&2 +git diff --stat nix/deps/gradle/deps.urls +echo + +# Re-generate dependencies JSON. +"${GIT_ROOT}/nix/deps/gradle/generate.sh" gen_deps_json + +echo -e "${GRN}Successfully added:${RST} ${BLD}${1}${RST}" >&2 +echo +echo -e "${YLW}NOTICE:${RST} Running '${BLD}make nix-update-gradle${RST}' in a new shell is recommended." diff --git a/nix/deps/gradle/default.nix b/nix/deps/gradle/default.nix new file mode 100644 index 00000000000..bdcaa055695 --- /dev/null +++ b/nix/deps/gradle/default.nix @@ -0,0 +1,44 @@ +{ stdenv, lib, pkgs, fetchurl, writeShellScriptBin }: + +let + inherit (lib) concatStrings concatMapStrings mapAttrsToList makeOverridable; + + deps = lib.importJSON ./deps.json; + + script = writeShellScriptBin "create-local-maven-repo" ('' + mkdir -p $out + cd $out + '' + + # For each dependency in deps.json. + (concatMapStrings (dep: concatStrings + # And for each file in the 'files' dict of given dependency. + (mapAttrsToList (filename: hashes: let + # Download given file(POM, JAR, nodeps JAR, or AAR). + download = fetchurl { + url = "${dep.repo}/${dep.path}/${filename}"; + inherit (hashes) sha256; + }; + # And symlink it in the correct folder along with SHA1. + in '' + mkdir -p "${dep.path}" + ln -s "${download}" "${dep.path}/${filename}" + echo "${hashes.sha1}" > "${dep.path}/${filename}.sha1" + '') + dep.files) + ) deps) + ); + +in makeOverridable stdenv.mkDerivation { + name = "status-mobile-maven-deps"; + + buildInputs = [ pkgs.aapt2 ]; + + phases = [ "buildPhase" "patchPhase" ]; + buildPhase = "${script}/bin/create-local-maven-repo"; + # Replace AAPT2 package only with our patched version from overlay. + patchPhase = '' + aapt2_dir=$out/com/android/tools/build/aapt2/${pkgs.aapt2.version} + mkdir -p $aapt2_dir + ln -sf ${pkgs.aapt2}/* $aapt2_dir + ''; +} diff --git a/nix/deps/gradle/deps.json b/nix/deps/gradle/deps.json new file mode 100644 index 00000000000..ae5603d3973 --- /dev/null +++ b/nix/deps/gradle/deps.json @@ -0,0 +1,25138 @@ +[ + { + "path": "androidx/activity/activity/1.0.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "activity-1.0.0-rc01.pom": { + "sha1": "05bfab882943363153ae9eefbaba3c6065f43922", + "sha256": "sha256-Y6zsFBqCEZWgNxWKXkkwAHi5n4e/ITk75eK0M7QdjrY=" + }, + "activity-1.0.0-rc01.aar": { + "sha1": "3339ce77cea7349781e82681a532b671ea7dde59", + "sha256": "sha256-zXg+CXrv3H/s7I1W21lT4FVfWagFKDw3MgQy4EpPiKA=" + } + } + }, + + { + "path": "androidx/activity/activity/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "activity-1.0.0.pom": { + "sha1": "f94af7350c14b899596aefd6c50e381a96d933ba", + "sha256": "sha256-J6S+dGJinDEtoLgxoJeNIHb8NAdBRfth3U6G18hGm4I=" + }, + "activity-1.0.0.aar": { + "sha1": "ed7a64df6e3fbebf7d3d3dfc30b0f47efcc707f7", + "sha256": "sha256-0byYQkVcLlNEFdiMRN9NUkE7R425CTobo2Mk9wX0TD0=" + } + } + }, + + { + "path": "androidx/activity/activity/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "activity-1.1.0.pom": { + "sha1": "c133421ae3bcd7a3c341124b2821223015f0f617", + "sha256": "sha256-NpAAH8WletF2ys96E/ViGb/7c7k8eXjbrB4eXbCC1sA=" + }, + "activity-1.1.0.aar": { + "sha1": "67c7a207d26ea46621e261ca2295ed2e2d311f8f", + "sha256": "sha256-Tys1kWdoAy99DCDiUOKLKQN+1M6evz2k/NUbywxgZ+8=" + } + } + }, + + { + "path": "androidx/activity/activity/1.2.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "activity-1.2.4.pom": { + "sha1": "c14da44e34d4fb2dba4ad4099e1b6e7b84ff6ad6", + "sha256": "sha256-kCTdmHPZ5uKa84xOKqCWCHcJ23rz680yqNF9e/xT9GY=" + }, + "activity-1.2.4.aar": { + "sha1": "bebd083d1486ed7f033b2dcc06e7cdefede4445f", + "sha256": "sha256-ro6cfeV+OH0q2Q5z86Wl39UCvU8DTB3M/bNQbR0t+Bo=" + }, + "activity-1.2.4.module": { + "sha1": "d29c97c31e7fb6526818b662c95bc0b48237ed4f", + "sha256": "sha256-IPVjT3ZxeRDltCmZCdaZioB48Qa9ueFbLddfz8G9QrU=" + } + } + }, + + { + "path": "androidx/activity/activity/1.5.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "activity-1.5.1.pom": { + "sha1": "7d703cb03cbb2ab533ab863244786eb663e60d71", + "sha256": "sha256-0pzNzkb0xxoBruYLr4MjRihvOB2dEbzHPuvtXRr3LN0=" + }, + "activity-1.5.1.aar": { + "sha1": "fd9bedd4fa5b13501bc5ff75b537ba9fa07185de", + "sha256": "sha256-SwS0LSwfgcAvrw97bpzJ/t4Q/e6PZhNs1LmfiOj0jA8=" + }, + "activity-1.5.1.module": { + "sha1": "e8401db0d903a5723f6b827bea9804bddc21bf04", + "sha256": "sha256-xDF/uVzicWuI8fSlM0eV79oIQJej8kR//MsQpBLIW+Q=" + } + } + }, + + { + "path": "androidx/activity/activity/1.6.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "activity-1.6.0.pom": { + "sha1": "6e416a37d326a5eabe9c828e73b05e2e6c642c08", + "sha256": "sha256-1YKpAf26v8E4sMQIJ2PD5Q2nceGN0Ss5Q0HjjlzBTxk=" + }, + "activity-1.6.0.aar": { + "sha1": "4bee9777c02790cb99e68cd397036b1d5508a630", + "sha256": "sha256-fwhyPsq++6YW1gxxSw6aMTAb1NB5L8x5RsFHnFf9LSg=" + }, + "activity-1.6.0.module": { + "sha1": "373d5f07fbb547702dd94a2b6c12c5243252956c", + "sha256": "sha256-tWHkHBvdshYPebm8dUGtntEQFHqyzPY3GaI9SYRwsEM=" + } + } + }, + + { + "path": "androidx/annotation/annotation-experimental/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-experimental-1.0.0.pom": { + "sha1": "ebc8cc79af3841ffa6a44ab02f31df2973c9b916", + "sha256": "sha256-a3P/Zgj0sdbLq2ILZXCKOC0LOZAc9Oaw0W+EobBNdzI=" + }, + "annotation-experimental-1.0.0.aar": { + "sha1": "fe9ba8247a591ebf8e16f0cf80af7a2045c981e0", + "sha256": "sha256-shnStWjn5LpTTgn4wv0kI0PfbMvfu+k4hG9ddA5rCxE=" + } + } + }, + + { + "path": "androidx/annotation/annotation-experimental/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-experimental-1.1.0.pom": { + "sha1": "390754e0425d3b182589223fcf7c4c02113dc1ca", + "sha256": "sha256-VQpmAeGvR8+ZUBzGzBXR6+nO8XtR5d1YtqtsI1aO96k=" + }, + "annotation-experimental-1.1.0.aar": { + "sha1": "5481c9aca18271c977d67b30e26f484f3f8f1c02", + "sha256": "sha256-AVfeYaIGQEeJagWAgPP9Z7pXrZqUhXs/ejY2YCQ+P5A=" + }, + "annotation-experimental-1.1.0.module": { + "sha1": "2af10f6e4d0ae8629b9e00151df3db2450e9d572", + "sha256": "sha256-A2HRUmpNdQElXhl3ngnpPNvQf+4OL1xQt6E3Qy1RARk=" + } + } + }, + + { + "path": "androidx/annotation/annotation-experimental/1.3.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-experimental-1.3.0.pom": { + "sha1": "25698c04f4da4d67254962b1d09ee4e5cf3a317a", + "sha256": "sha256-ui/kb3T2iFHiNCRhk6uJbAwB+glM03LBBRtr8E15sLs=" + }, + "annotation-experimental-1.3.0.aar": { + "sha1": "5087c6f545117dcd474e69e1a93cacec9d7334af", + "sha256": "sha256-q/0pyFVuW9AyWp92mrnp0VT/SlUVxHbN1aKoKFsbGdw=" + }, + "annotation-experimental-1.3.0.module": { + "sha1": "c31709adc46c574eb71546ac11b65c3483eb4907", + "sha256": "sha256-Xuvq/wHQQuBtzykqv4lkrTkeSwFZ8AkPFiU9YEXTjaA=" + } + } + }, + + { + "path": "androidx/annotation/annotation-jvm/1.6.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-jvm-1.6.0.pom": { + "sha1": "4f2f0d097957ae582b2e67e5ba2aa04af79fc2f4", + "sha256": "sha256-K+ZlunzbBGtBtAnHmj3gxtnhJVJCwIf2uUssdJO+CPQ=" + }, + "annotation-jvm-1.6.0.jar": { + "sha1": "a7257339a052df0f91433cf9651231bbb802b502", + "sha256": "sha256-YLELXvV2m3lXAXLgFbgVlAXJLwNLqIuTkal3WJyd604=" + }, + "annotation-jvm-1.6.0.module": { + "sha1": "2461465166ff8ca842e725bbbfd06d7e551a514e", + "sha256": "sha256-P1qPqhneZn5j3KlzD/jvDkeOS6+1/uuCWOXAhiRtyQw=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.0.0.pom": { + "sha1": "1ec1fd6918db230c4b2e8ffa78f724267fb6328c", + "sha256": "sha256-oXnBLbQ9nAMAydtj9IEdtJZQS+VAG5UdQit4SQrR5bQ=" + }, + "annotation-1.0.0.jar": { + "sha1": "45599f2cd5965ac05a1488fa2a5c0cdd7c499ead", + "sha256": "sha256-C6rpdV98r1KqgM0EMkuRupOvVdTR0X3MmntT2Z73wBY=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.0.1.pom": { + "sha1": "08f7df7cd012aeb0458703f5698237d122599384", + "sha256": "sha256-yJ0j+YBCgqR5kv9cpke3hJIcFsqmaafprzTBX4GqdEI=" + }, + "annotation-1.0.1.jar": { + "sha1": "2dfd8f6b2a8fc466a1ae4e329fb79cd580f6393f", + "sha256": "sha256-D4b0C+VwmKr1PLWhXpdNEj7B3A0aIZSr9CJrN/gCbKo=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.0.2.pom": { + "sha1": "f1dc2b36a23233b355a06839d55d7b4f0765b724", + "sha256": "sha256-XIaFSZJaWI4RehherYZYB62IImduDXyATCdH2N1YZvk=" + }, + "annotation-1.0.2.jar": { + "sha1": "02f1d597d48e5309e935ce1212eedf5ae69d3f97", + "sha256": "sha256-m830zFADEh24e1HeX1HbWAXfM11Co2pwpat0BcIxY/I=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.1.0-rc01.pom": { + "sha1": "86bc67f5ec6534c96b1adae802b747742dc04a38", + "sha256": "sha256-DMHcvVrJ6P3rZUAxekOI8NC6/3Sf/T5BzLw58A2TFCE=" + }, + "annotation-1.1.0-rc01.jar": { + "sha1": "e3a6fb2f40e3a3842e6b7472628ba4ce416ea4c8", + "sha256": "sha256-041j7bMPFGeBjVCq8F+KaS3qizE5KgSb+pkbFZrVtpI=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.1.0.pom": { + "sha1": "404526f88e3a7efa04f1c99074dfc10aff9061ee", + "sha256": "sha256-LpNyuneA70SVKtv4a2bh8IaCweUnfJJhhfZWShN5nv4=" + }, + "annotation-1.1.0.jar": { + "sha1": "e3a6fb2f40e3a3842e6b7472628ba4ce416ea4c8", + "sha256": "sha256-041j7bMPFGeBjVCq8F+KaS3qizE5KgSb+pkbFZrVtpI=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.2.0.pom": { + "sha1": "b1d9e8a4bbffcb85580a101f2032d68a9f426606", + "sha256": "sha256-Yvttyid37+COcHfWuHLWkRBhnff8Icmab1QGZJnMA4M=" + }, + "annotation-1.2.0.jar": { + "sha1": "57136ff68ee784c6e19db34ed4a175338fadfde1", + "sha256": "sha256-kCkmK93OEW5tAr5Jnkr9uiHyTCOQh7drO1fX6YtJCjY=" + }, + "annotation-1.2.0.module": { + "sha1": "26f06565ebe0e525e1a183b59a9ad2e2eb799b2a", + "sha256": "sha256-Lvyrge+RshG6zSBuqs2ZWlH2M6Lpa1eo/AAUTF+cVrM=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.3.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.3.0.pom": { + "sha1": "8e41994f7c27b40c14f53bf3fa40c84d5d00b13d", + "sha256": "sha256-BJUXxSVqS7yFKfzckMTqrzWBgKwgXJFmB2ffJkZe+7A=" + }, + "annotation-1.3.0.jar": { + "sha1": "021f49f5f9b85fc49de712539f79123119740595", + "sha256": "sha256-l9xFr+/joeQh2kK4tun5BJFHfEX8YXggPjpeigXuhVM=" + }, + "annotation-1.3.0.module": { + "sha1": "6adc685f78686901844153d9d7f41774e649eb4b", + "sha256": "sha256-lRbCrkQoTqC9PQ6t4O5jiHm3CMvjHjr5K6lsMAYE68M=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.5.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.5.0.pom": { + "sha1": "213890462e7f573cc6895f20d54a32ce4eec5138", + "sha256": "sha256-4pMlmZmt4HAXXEqreA0H1V0BhbtDpRWiT1iQ0CLhzAU=" + }, + "annotation-1.5.0.jar": { + "sha1": "857678d6b4ca7b28571ef7935c668bdb57e15027", + "sha256": "sha256-Jh+3wCEIWFALq2bTQ1SXKnUWarQYKt0oN4CwVRPW7Eo=" + }, + "annotation-1.5.0.module": { + "sha1": "a06a58f332414cfa6165376ff668ab69644f4fce", + "sha256": "sha256-TIT+7i24kf9rl9YToNQKuWzil7A0ppJ8qEefCegtfC4=" + } + } + }, + + { + "path": "androidx/annotation/annotation/1.6.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.6.0.pom": { + "sha1": "fb1e233a6072d024c05fd2802aadd9146575e4ef", + "sha256": "sha256-3YgrTBlFrRMSHs32UeumZKmyJrVduE+nVTkt5eENHls=" + }, + "annotation-1.6.0.jar": { + "sha1": "b94fe605ea8ff0f68f3feab065ae0b7d67860964", + "sha256": "sha256-+8ZPXESnrd7Ytuq1F899cFVeJRU79dRKbtmw5TEvfek=" + }, + "annotation-1.6.0.module": { + "sha1": "cf3899ddc31f0a48c7189b1a0c943e917ec927d4", + "sha256": "sha256-YUa2E4ZDsqwFkN9QndUauup2nHn9dgLrIXFo/lr3jNI=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat-resources/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-resources-1.1.0-rc01.pom": { + "sha1": "ede5b258a8f866119f39a17a2daf749e76be42a6", + "sha256": "sha256-CdoKEmxurM6DWKaGEsKazxrhEJc1DTndk71AfBlaxJo=" + }, + "appcompat-resources-1.1.0-rc01.aar": { + "sha1": "1c8796ad736cbcb539e89630931ca886b2e75212", + "sha256": "sha256-Y3l3/Ymf9kF8a+b+uF0uzAuQgDCR0ej1pyStIU+2r7c=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat-resources/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-resources-1.1.0.pom": { + "sha1": "04d25219b17a53da24285e9560bbf5c587abee38", + "sha256": "sha256-BGAR4WywG28UhCVlZhVREQ7xtkJ0g/XZBoST9MSWkPI=" + }, + "appcompat-resources-1.1.0.aar": { + "sha1": "a573b2ab146d686244721ef1038d08043f18c67f", + "sha256": "sha256-GZRNMrRlUaF8NH4hiUuVg3+9e6qvyeIIJ5Q0TyIvc2E=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat-resources/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-resources-1.2.0.pom": { + "sha1": "2211657bde99e604557ac3cd44464d390c4c26f1", + "sha256": "sha256-FJ3YzsNmS++P/ehsOWuh4qsVbqaHk9KYANAIusvJwPg=" + }, + "appcompat-resources-1.2.0.aar": { + "sha1": "ad67e51b92364585bb335177664959001644327a", + "sha256": "sha256-xHApfAP/PeHD0V2s8L4MrmOrwQtS8CHdB64o2qMQD+U=" + }, + "appcompat-resources-1.2.0.module": { + "sha1": "82dd6ee9bf383eac85017a007d0cfa250229ee27", + "sha256": "sha256-ddmGW8aw93kEO09lI6lEvSzLdSb1wfXvJGJKx43sO9M=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat-resources/1.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-resources-1.3.1.pom": { + "sha1": "fbf12448e003ff14e3aad1a2c9fd3c84af05cb1e", + "sha256": "sha256-WPscma3gYskU7kEFs8CZoMHI9IQjLp6Z2bq1bNU8Hz0=" + }, + "appcompat-resources-1.3.1.aar": { + "sha1": "32ad26fa0a18f5df7c7bff16aeaa1ecfef593877", + "sha256": "sha256-4zBs0+mhmiil3l7Ds3lYDyN8TYHBXD15VAS+kpGJCnU=" + }, + "appcompat-resources-1.3.1.module": { + "sha1": "f7e98e064eed98c706088dc2d4ff407bd6b2c652", + "sha256": "sha256-dXu0fYQXHQVf5nz2uI78ekLAW9tnO4qJoaPCWlidSEg=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat-resources/1.6.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-resources-1.6.1.pom": { + "sha1": "9f7e2a43a9a3e8fea938f0d0da224ab5e30c9c91", + "sha256": "sha256-2f0zmytjcFG6WTTI2Hx7nH02jUe1+oR9bCadooUs/GM=" + }, + "appcompat-resources-1.6.1.aar": { + "sha1": "03fe43025e50b556f319b1ff82a730d5a376a31e", + "sha256": "sha256-25Fdv0k1eGPeFmn/n92OkAjWX+NXr2zOmuYwQ61fZhc=" + }, + "appcompat-resources-1.6.1.module": { + "sha1": "d8daa06427e6a710a8bd0262eada00384bd66aeb", + "sha256": "sha256-yBTBXC/zTwlPi2mUss1R5ujQYrz8hpEvVd1uCPM6zuc=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-1.0.0.pom": { + "sha1": "5ce716d352f95b3c0ea0ecf9f579691c1a79410f", + "sha256": "sha256-ICw/zbgIMrIuLSffFNnnGy9xbwj1co6Cg0njNFTMTSs=" + }, + "appcompat-1.0.0.aar": { + "sha1": "155b5a7193b5b87c3ece2ec444c85cd8de2347dd", + "sha256": "sha256-mUJU4SA2y9MyaygZpNi8O+8Tn0UCLPsLEIbcWNNtVs4=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-1.1.0-rc01.pom": { + "sha1": "49c937cd4c8edaa42aefa8eb7b9adffc3c5362c3", + "sha256": "sha256-/ej7WpHHa1qzgcxvGh2qYjWfICXjQRNjljQiwjPxwT8=" + }, + "appcompat-1.1.0-rc01.aar": { + "sha1": "62955e0a3c6f4566648f30da6e40e44b01221422", + "sha256": "sha256-lRE7OiTbTy6Smr2xVaoqGv04N/lK94bg6qVmJNwrZ08=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-1.1.0.pom": { + "sha1": "83765254bf5e6aa04bb1f35960db460afd70b048", + "sha256": "sha256-NA1hcSH4744CpmgMjzV6o+VCJ20Mihzctv2YmEsst7k=" + }, + "appcompat-1.1.0.aar": { + "sha1": "351d3409fe51f3d862bd2b1bcc0f3b6ded29460e", + "sha256": "sha256-jXKZvKRMs73xf1WVdmrL9Fn8gf7iI+hobMas06QqtcA=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-1.2.0.pom": { + "sha1": "c599cadd6e3ef8d628737532dcb341704b3a4261", + "sha256": "sha256-jrPL6CO2CYU7SBZG49LBqjnb3lPdJpcS/YRP/e8uu0I=" + }, + "appcompat-1.2.0.aar": { + "sha1": "6487c8711252a8680baaa854247141cc1d2098f4", + "sha256": "sha256-PSExpVphp3cyLiEm4AGAEe+mM55TtEFT62UbFgIMynA=" + }, + "appcompat-1.2.0.module": { + "sha1": "3da28bc2a27608c3a10a71e93b184fa9827cb9f4", + "sha256": "sha256-JnCQL8bCYEfELg9g7jRlb6BxhB2zcN6VgZhBO1q1j8M=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat/1.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-1.3.1.pom": { + "sha1": "080c26399978d9c495e3b29e81b85d91683b10a2", + "sha256": "sha256-KL3KNxXvqB1g+4jEBTNtt3nV4ngV7JaVYqrQeJ8Ueg8=" + }, + "appcompat-1.3.1.aar": { + "sha1": "b9bc6aeb85e9bd2bc7ce829960b07d47eda7a1a9", + "sha256": "sha256-lZsdrv5A1efu0QIvl3MLIrxf1S5qYIProoT6hsKXEwM=" + }, + "appcompat-1.3.1.module": { + "sha1": "9b9b5740f6ff2f3167b05251d4755df6d27a62ee", + "sha256": "sha256-oc/rdg5RqjNjpyeW7Vvte6UB2U3zQvCpFgtn1PITzF4=" + } + } + }, + + { + "path": "androidx/appcompat/appcompat/1.6.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-1.6.1.pom": { + "sha1": "0d37a2c50e23b15925fdb15464965ddcd8556f48", + "sha256": "sha256-li3am+r40cLMUf/Szivik+vKcXKnasDPNHARM2XPgIg=" + }, + "appcompat-1.6.1.aar": { + "sha1": "6c7577004b7ebbee5ed87d512b578dd20e3c8c31", + "sha256": "sha256-fqVXO5Orq9O9MjEkUcbqSKZisDoUDdqBrr51d2ogpCI=" + }, + "appcompat-1.6.1.module": { + "sha1": "3256e42830ca15e6665f40c577cbfedad1e25797", + "sha256": "sha256-91U4DYBWPuzKfbC/Y2i3lq++/jgVVMh7dSv54pHzwaY=" + } + } + }, + + { + "path": "androidx/arch/core/core-common/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-common-2.0.0.pom": { + "sha1": "f99012a3ab10fd604a8631aaa8d525aa44df5da5", + "sha256": "sha256-S28dRZ3dFGtOhe1tRuhuuMJjnF3keQTm201phyEzQiA=" + }, + "core-common-2.0.0.jar": { + "sha1": "bb21b9a11761451b51624ac428d1f1bb5deeac38", + "sha256": "sha256-S4CzN3ebUm5ksO4MqeDfQ7gINE0UX46bHEKhNNrFetg=" + } + } + }, + + { + "path": "androidx/arch/core/core-common/2.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-common-2.0.1.pom": { + "sha1": "8ebce13650b574c9e486fba87f1f948edd41949f", + "sha256": "sha256-JejXhewsfJnis7TojQwNpwg1MKesqAyFg1NKktrZOTU=" + }, + "core-common-2.0.1.jar": { + "sha1": "3e8d84e4fe526be0ca1832a518bf323e729a79fd", + "sha256": "sha256-5zFqhLiZ6yr7FVF4TpgH+2S9/MEFY2/gVRzQNoAfl8g=" + } + } + }, + + { + "path": "androidx/arch/core/core-common/2.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-common-2.1.0-rc01.pom": { + "sha1": "ee73e3edda236d1e1114b333d4e7dfac924c3f79", + "sha256": "sha256-BlJbd/hzjn59nTOErPA3+xSOnWLs87odlbq+Dn9R3Y8=" + }, + "core-common-2.1.0-rc01.jar": { + "sha1": "b3152fc64428c9354344bd89848ecddc09b6f07e", + "sha256": "sha256-/hI3vwKdBj5/Kf45rq9z73TIsKNlhIb8KdPFQyZlOIk=" + } + } + }, + + { + "path": "androidx/arch/core/core-common/2.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-common-2.1.0.pom": { + "sha1": "2b3b788454b0aa77266f2ce191eba3a7768678ca", + "sha256": "sha256-g7uzlg6qvGAKw2bJTLWUFORBUyodaqk4iwuL/6zlzwE=" + }, + "core-common-2.1.0.jar": { + "sha1": "b3152fc64428c9354344bd89848ecddc09b6f07e", + "sha256": "sha256-/hI3vwKdBj5/Kf45rq9z73TIsKNlhIb8KdPFQyZlOIk=" + } + } + }, + + { + "path": "androidx/arch/core/core-runtime/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-runtime-2.0.0.pom": { + "sha1": "d3572f4d27cca318471a0005ce6c93740ad5e9cb", + "sha256": "sha256-4gB9Qd3GnuKnf+17XB3FpB4BEikmullpaqt7GNImRBE=" + }, + "core-runtime-2.0.0.aar": { + "sha1": "c5be9edf9ca9135a465d23939f6e7d0e1cf90b41", + "sha256": "sha256-h+Zfx2fHErQ3ZJx87iQx67S+1trvguUB1BJbPtP2X44=" + } + } + }, + + { + "path": "androidx/arch/core/core-runtime/2.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-runtime-2.1.0.pom": { + "sha1": "a86136badfffa9d668106a80ff4cda9ee9c1cc1b", + "sha256": "sha256-wMTtAWDNLKGDkAFd6LOStpfBczJ8aywJR9TmL2lYwF0=" + }, + "core-runtime-2.1.0.aar": { + "sha1": "2df0e03029caae7863ccb4825addaadc8ab6780c", + "sha256": "sha256-3XdhW9PdJ1r7EbYt8luuRrELShF803lDr0W9y/h1WFI=" + } + } + }, + + { + "path": "androidx/asynclayoutinflater/asynclayoutinflater/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "asynclayoutinflater-1.0.0.pom": { + "sha1": "fddbfd83af5c2af0884a3a36a91ad8113d7bd3a4", + "sha256": "sha256-SBZ+7tyNp5xNKd6vDQzZtdj+3K4B8abvs/KPCOiYL3E=" + }, + "asynclayoutinflater-1.0.0.aar": { + "sha1": "5ffa788d19a6863799f25cb50d4fdfb0ec649037", + "sha256": "sha256-9+q2DFet3ZS7BidYMv52AGEb6qrhoexZfCMZVvr5bIs=" + } + } + }, + + { + "path": "androidx/autofill/autofill/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "autofill-1.1.0.pom": { + "sha1": "a2b5e0bf6a1f4e9e254a7c4cf8609233d5485919", + "sha256": "sha256-xQODutr2zFpMf8iBGAVOJ16kldsvH334xDxgWA8YtNs=" + }, + "autofill-1.1.0.aar": { + "sha1": "d9cdaa22b9c373ba75a35dcc33ee8871543164e5", + "sha256": "sha256-Z864JnwnRVfDV1jrvkSJCiA6VXm9KwhN801wdjxy+A8=" + }, + "autofill-1.1.0.module": { + "sha1": "f21598b35f918abd1589bb6fad4f675a5fd9b849", + "sha256": "sha256-PUfCoN2wkehOKkpXrus9JumBOsBsK83IjFrKEwcEGGY=" + } + } + }, + + { + "path": "androidx/biometric/biometric/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "biometric-1.1.0.pom": { + "sha1": "b531efe87c20ed8b07e055c4a872f404586c5065", + "sha256": "sha256-rraedpiPQ7BshPyiXvqy5Bi7mLWqvNA/p5E70+0byqk=" + }, + "biometric-1.1.0.aar": { + "sha1": "3d4d3a881cc8cbeee5bf9dd6a4e8326f0e4b44cd", + "sha256": "sha256-Jwx7fZmULV7B3YhZTkZI/rM9jjHYw8KrIyHUnZq9/B8=" + }, + "biometric-1.1.0.module": { + "sha1": "d973835f168796264f1887dffa369f1af9329497", + "sha256": "sha256-SZnyBbKzjjEYCtWBEeMtVkGQwgJPv9WFJ+Lai/tn/2U=" + } + } + }, + + { + "path": "androidx/camera/camera-camera2/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "camera-camera2-1.1.0.pom": { + "sha1": "6bf92d6f3ca6bcf6177e39a1ad20a687fa33adca", + "sha256": "sha256-wTyfsIPJQwh0ljA5KaQTCHISCAgquQnvYLe3YxuF3QQ=" + }, + "camera-camera2-1.1.0.aar": { + "sha1": "dda35e13f2bb2c7907523aa9f5bd70576589fcd6", + "sha256": "sha256-q/B8I/a+lj/jaUPOTxOAcRrSK60ytuYr3d0WCtfZ3iI=" + }, + "camera-camera2-1.1.0.module": { + "sha1": "4ded67412273ea37f40c5fcba4604c36bb7da971", + "sha256": "sha256-Wz4tTfFrFQbLoTevfTa38Q7ClZQdpnnTMdAiDbe4t1M=" + } + } + }, + + { + "path": "androidx/camera/camera-core/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "camera-core-1.1.0.pom": { + "sha1": "829e3d2da0093a5266f2fc544692fa19e6e268a4", + "sha256": "sha256-UlIWWiDFa3poCuL0lPxVgE5jgSIsoh8fNzRRxZbamAs=" + }, + "camera-core-1.1.0.aar": { + "sha1": "7c3b5218f57adab60a7ff744ec5a0befa2f5d7f4", + "sha256": "sha256-xvrmgIHZ8zRMSuKznQldL21/Zw+fSzflRXTTGDy/nwg=" + }, + "camera-core-1.1.0.module": { + "sha1": "4a844e5e2ae61ca1a8b313b66fe1ae935f1b6328", + "sha256": "sha256-rbuVcfjtXjjpdRz5J1vKvt51ksO9AJdT0QtwPcxxBqE=" + } + } + }, + + { + "path": "androidx/camera/camera-lifecycle/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "camera-lifecycle-1.1.0.pom": { + "sha1": "3a706bf3632687ea72faf7b55ffd9b5b5a9c44b1", + "sha256": "sha256-nkiPWzYdHbQEZwEy+qIWhyYSSox5CIRB/bnP+s30oQM=" + }, + "camera-lifecycle-1.1.0.aar": { + "sha1": "04304bb6a036d3961e20cdf7bd6ca1e2537ff354", + "sha256": "sha256-ncRoVypzYnrkA+Op/6rXG8/ACvnQKqV6ulpXJtBiRrI=" + }, + "camera-lifecycle-1.1.0.module": { + "sha1": "e7d2429f9f6999ab844621d874560a35ef7c26bf", + "sha256": "sha256-sq6KYrVSuuqv/B/3/KmIT/cL0MOJkolE/oXTb+UjYqI=" + } + } + }, + + { + "path": "androidx/camera/camera-view/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "camera-view-1.1.0.pom": { + "sha1": "a1f65af7f7a7853b770ed66bb660da1afe56be37", + "sha256": "sha256-rzhQ3joch82axk6O3qpuADyZQHEJ8E7JXh0FIDsI3no=" + }, + "camera-view-1.1.0.aar": { + "sha1": "0a993004e0bae677ea88067dd7291c5701690f6b", + "sha256": "sha256-YS3kGZMd5LsrPW9PaUNXWSEdqXURmaNqUmZLKKJb2ow=" + }, + "camera-view-1.1.0.module": { + "sha1": "71ed6d8998fad2c9e6b9a62620b168b2e4627f58", + "sha256": "sha256-f/Ajx6C2q4mvT/1XQg59Y7zUt1Z7IUD/6a2UGpX7LZo=" + } + } + }, + + { + "path": "androidx/cardview/cardview/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "cardview-1.0.0.pom": { + "sha1": "93a261514381247a8a6bebe171338f9f2f883f27", + "sha256": "sha256-5k704ItYNY/ie1meb+gKGxU9sBTGRL7uYwqycQYcPmw=" + }, + "cardview-1.0.0.aar": { + "sha1": "158dbc2e2bc502815821191b04446b8f663c1874", + "sha256": "sha256-EZPATCKj1rWUba6fToxZ1q3eanG2vV2H+5nYLdoa/sc=" + } + } + }, + + { + "path": "androidx/collection/collection/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "collection-1.0.0.pom": { + "sha1": "6b6d5f96527a79bce10e9e9e8c4704f661b2cdc8", + "sha256": "sha256-p5E6UnWtaOVV0mEuvowUw2exU+FMpIoYcqZImQIOVO8=" + }, + "collection-1.0.0.jar": { + "sha1": "42858b26cafdaa69b6149f45dfc2894007bc2c7a", + "sha256": "sha256-nI0Re1wrwSChzf64V+BbSVsWw2ATVwNypwj3gn46yfk=" + } + } + }, + + { + "path": "androidx/collection/collection/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "collection-1.1.0.pom": { + "sha1": "e1eba7bcd8a98744b7850e5c474edebaafba6fbb", + "sha256": "sha256-Z+kGbKSs/cbjzFCCk8MboDmAV/8Rjk9wseGBPJo0VtE=" + }, + "collection-1.1.0.jar": { + "sha1": "1f27220b47669781457de0d600849a5de0e89909", + "sha256": "sha256-YyoOVAdGHed0QJNSlA4pKikQN3JCB6eHggx32vfTO3I=" + } + } + }, + + { + "path": "androidx/concurrent/concurrent-futures/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "concurrent-futures-1.0.0.pom": { + "sha1": "83560cc5e7d6146245a05a4837ac6ba6474de92e", + "sha256": "sha256-RQW5peMKlBi1mprWcCw+QZOupuaRo9A88iDHZArQg+I=" + }, + "concurrent-futures-1.0.0.jar": { + "sha1": "c1e77e3ee6f4643b77496a1ddf7a2eef1aefdaa1", + "sha256": "sha256-VZWkDieKezn6eKCUkOPX8/qpXHsBRHFIvTi1reBgXDU=" + } + } + }, + + { + "path": "androidx/concurrent/concurrent-futures/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "concurrent-futures-1.1.0.pom": { + "sha1": "1a2c7df73a77c58e30bd0b20cbc23e9854515438", + "sha256": "sha256-dIo0610T0Z0Yet7K6oJmf97V8bC5imVeE+0uSos9iuY=" + }, + "concurrent-futures-1.1.0.jar": { + "sha1": "50b7fb98350d5f42a4e49704b03278542293ba48", + "sha256": "sha256-DOBnxRSg0QSdG+vfcJ40TtMmb+l0QnVoKTfNyxMzTp4=" + }, + "concurrent-futures-1.1.0.module": { + "sha1": "3f5fb4ebdb2b2b4bc5d6c2de76d90d27efa9b963", + "sha256": "sha256-d2OaCwUeIlELrZOv/OoOvXge8SS/m3YhqVdJk3vPzf0=" + } + } + }, + + { + "path": "androidx/constraintlayout/constraintlayout-solver/2.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "constraintlayout-solver-2.0.4.pom": { + "sha1": "ce642efe9f0d3ebbd1a43630140b3a85029dac20", + "sha256": "sha256-fzP0sQavm+FMOeUPElNPllH1ZLuUOgLwjXikXV1bHJc=" + }, + "constraintlayout-solver-2.0.4.jar": { + "sha1": "1f001d7db280a89a6c26b26a66eb064bb6d5efeb", + "sha256": "sha256-nKGfVEhwkwHHVjSI75Qb6d+lXINTjKegWbIRPoNSe0Y=" + } + } + }, + + { + "path": "androidx/constraintlayout/constraintlayout/2.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "constraintlayout-2.0.4.pom": { + "sha1": "5a6761d954eaef83dafd69eddd77df09e59bcd8d", + "sha256": "sha256-cXfbrhmkhBrIJSPfpC/pMmK2oMmh2QpfV8Mx+R8j6pc=" + }, + "constraintlayout-2.0.4.aar": { + "sha1": "fc11f9c864670e5e2d1b02d2092c8811804f3ec7", + "sha256": "sha256-MHp5pKHM/0QknHKiv39H2gn6G2sfqyolgIyoiTgrc44=" + } + } + }, + + { + "path": "androidx/coordinatorlayout/coordinatorlayout/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "coordinatorlayout-1.0.0.pom": { + "sha1": "7136717d5fb751f308e9f9acf25a1f89d897c67f", + "sha256": "sha256-PkTk3i/ubevDPSNibukG5U4bs8F0mxngK7ZnthSrk5I=" + }, + "coordinatorlayout-1.0.0.aar": { + "sha1": "7664385a7e39112b780baf8819ee880dcd3c4094", + "sha256": "sha256-5QjGlUiUkzdNlCv3tO4Cq/dXHSWqxMYi5X1s1c0p63M=" + } + } + }, + + { + "path": "androidx/coordinatorlayout/coordinatorlayout/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "coordinatorlayout-1.1.0.pom": { + "sha1": "2ccdc41c50a15a5cbf1bf0e40e46a63241b1c4ea", + "sha256": "sha256-pnxSyd36/y/7L9S5fNlPo4LoN+qKWHTQKeCgT6Y+XK8=" + }, + "coordinatorlayout-1.1.0.aar": { + "sha1": "8eeb7baf75b9595d017642a460df5af3bb9fa4e1", + "sha256": "sha256-RKnjCr9WrxAlxSoK9Qb+6cQTGqVe/aUvn9lFEhHF6Ms=" + } + } + }, + + { + "path": "androidx/core/core-ktx/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-ktx-1.2.0.pom": { + "sha1": "ca8eda9cff2258e9705357282c6f86bc80737594", + "sha256": "sha256-Kerduk5qNeaKEo5ZTrrjty7352fxsnPvk9wu0Bn/l/Y=" + }, + "core-ktx-1.2.0.aar": { + "sha1": "6756534f262c845d4d06b55aaf8b751db0e48822", + "sha256": "sha256-3LdNUQ1VKzXv9zsN0nuClklTXzkC5bWh8mBAODwQqUA=" + } + } + }, + + { + "path": "androidx/core/core-ktx/1.6.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-ktx-1.6.0.pom": { + "sha1": "c4966cf606f084c4edea617d45043e8fe1aaacfe", + "sha256": "sha256-yRTWLel+tWackWkErr1f9VMrsSCsxntRNkodLS/9hSg=" + }, + "core-ktx-1.6.0.aar": { + "sha1": "75bce93ea8fc2c82b012352a5672a18b9ebde366", + "sha256": "sha256-ce8CzufroekFp/N/MTtQuoo77g7Fmg+HbVp4hg4d6WM=" + }, + "core-ktx-1.6.0.module": { + "sha1": "28fcca0ea8bb99be985d3db68209ef8a2c2dc27e", + "sha256": "sha256-6Te/IsDaKOhyMA/GuWApuD/3WqjQqx7AOarj2K9lE90=" + } + } + }, + + { + "path": "androidx/core/core-ktx/1.8.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-ktx-1.8.0.pom": { + "sha1": "d4ab65db0a1cfd583cf7a655492e24fabd5b9dc4", + "sha256": "sha256-jQesWFVMKRbo0z3/u3sTtKdXhpAeUIrig6dbjC6wK68=" + }, + "core-ktx-1.8.0.aar": { + "sha1": "4c72a0f8fa6cd11a21ef86be9c1d5d257068b225", + "sha256": "sha256-F5C7mws+/mon+xujo1MLC5qmZU5Yv4qe94P3Z+UNMbQ=" + }, + "core-ktx-1.8.0.module": { + "sha1": "7b8fd94270bc14f0eb4f5658867c1866e81b3fbb", + "sha256": "sha256-qRvD4C8gn2Q92CdTRanjADziDWT8B2Dsz0ecFwmEL3I=" + } + } + }, + + { + "path": "androidx/core/core-ktx/1.9.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-ktx-1.9.0.pom": { + "sha1": "ec014f2abe3006fd3c854222f2d3a8463372c32f", + "sha256": "sha256-Gu0iT4f3ZkHsoC95qONl1y6cy5AphPj1MYPEQgyUVQA=" + }, + "core-ktx-1.9.0.aar": { + "sha1": "b56f6b1bcb7882a9933c963907818da2094ae3a4", + "sha256": "sha256-FUKh33vpCMlf7pG428A/2XT1PxHYSoXYgflaUlUcQFE=" + }, + "core-ktx-1.9.0.module": { + "sha1": "fd85d06987f548d1b48ade231d073f931a9940b8", + "sha256": "sha256-TUZkYGbHlPKBLVszqUItN+T5GMTYCc0fsleccCLvKBg=" + } + } + }, + + { + "path": "androidx/core/core-ktx/1.12.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-ktx-1.12.0.pom": { + "sha1": "98358fbb20e71d472adc3a26d01a8a0a445ef612", + "sha256": "sha256-1xnEKyqtwG6db1OsxUD7gAUBgkEloBZXLMbP/a2TCZo=" + }, + "core-ktx-1.12.0.aar": { + "sha1": "1b80a40d6f34eb52c8de87c186feb57aa2a43ada", + "sha256": "sha256-lbPtr3gie3zI2ALRH4eTqa7SkuUrj7+MCNYTTMsXFtM=" + }, + "core-ktx-1.12.0.aar.asc": { + "sha1": "f0b148451012d382dee8477494242b26aceb0d5c", + "sha256": "sha256-dUDyw21famtz5Dp6J4vYtYBH+XQsW9O8AyEFhjYmx6E=" + }, + "core-ktx-1.12.0.module": { + "sha1": "31c9935faf7fab82d6d148f3ca26ba66af0aee1f", + "sha256": "sha256-5MzDxKoDRbAr8pP7i5/8xCwDkJAXyg1WGixDE8Y4A30=" + } + } + }, + + { + "path": "androidx/core/core-splashscreen/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-splashscreen-1.0.0.pom": { + "sha1": "763acf4fa60a08a3c059a4c50d3d54ac34a0cfa2", + "sha256": "sha256-A9f/13OTY3fjmKuedtPR4zq5Hv2vdoQgyJR/+lP3PSs=" + }, + "core-splashscreen-1.0.0.aar": { + "sha1": "6b1eccca966811faafb13f6c2f88351504dc9eae", + "sha256": "sha256-PoayPcHKP8SrwhHunKfIifR4dD4kgkWKYmfmAj3PKnI=" + }, + "core-splashscreen-1.0.0.module": { + "sha1": "58ca9fe3061afe95f052d5f4b15de381b655dc11", + "sha256": "sha256-Lqb/glAO4WSQktvcqYgkPDrfOML/u5rsKBsOXxGJVVY=" + } + } + }, + + { + "path": "androidx/core/core/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.0.0.pom": { + "sha1": "a0587cee2a95fb9027f0b6d4a110a2072a7f6015", + "sha256": "sha256-OE1SmtZoraSAjeKbKaY0jwAHwoQa15yumloIm9aIEPU=" + }, + "core-1.0.0.aar": { + "sha1": "48ad5ac253f1478cd7643ab4eef497a376098886", + "sha256": "sha256-12cjWPwNRt5niWi83f073+FhhXQpmnU0d7FhFs/sdPU=" + } + } + }, + + { + "path": "androidx/core/core/1.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.0.1.pom": { + "sha1": "4ca2f7d0a090f143876b04f4be8e746ac5921a39", + "sha256": "sha256-wLJZjSYlR5Tg/USpYKyf8LxZMp98OjYzKdIwl0oZ+to=" + }, + "core-1.0.1.aar": { + "sha1": "263deba7f9c24bd0cefb93c0aaaf402cc50828ee", + "sha256": "sha256-sakFIsIsrYxft6T5Ekk9vN5GPGo3tBSN+5QjdjRg+Zg=" + } + } + }, + + { + "path": "androidx/core/core/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.1.0-rc01.pom": { + "sha1": "f4e3a51f6c998a50f95a4b82a626f9ecee5a7762", + "sha256": "sha256-pO+w1UwMj0SKE+IwwV5eiyZo/VAuI2PBEKjOvWD+wdU=" + }, + "core-1.1.0-rc01.aar": { + "sha1": "1f464723e68cbe1a5adad0f761b163ba8ff8bc3c", + "sha256": "sha256-RcelCtHzZuYttJbYzvdzDV7haBIVAH0aGea22AChKEI=" + } + } + }, + + { + "path": "androidx/core/core/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.1.0.pom": { + "sha1": "2a6d529fdea6d3525bd70f46b6f1fc1a580a8513", + "sha256": "sha256-2uRhMs3NRreYQl98t4/WWJCGm20mEBzNzUNGGk9RdUw=" + }, + "core-1.1.0.aar": { + "sha1": "b9addd897d6b9c8634fe789bdb82f993171432ae", + "sha256": "sha256-dsfPvllv48CaaYO/HInoiSmcCKyaO1LOUYKgiNBWZH4=" + } + } + }, + + { + "path": "androidx/core/core/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.2.0.pom": { + "sha1": "01d5f5eb5224254e598a939cceb7e03aa7c209eb", + "sha256": "sha256-PR9ON7d92SNTh5oECrTOL3BnmLy98GYUdJHDZCs/eaY=" + }, + "core-1.2.0.aar": { + "sha1": "745ff33409507e50cae5be800dea412470f8af5a", + "sha256": "sha256-UkuLiM62p0p+ROa1Z6E1Zg8hF5mQTLIYv+5b4RZoILI=" + } + } + }, + + { + "path": "androidx/core/core/1.3.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.3.0.pom": { + "sha1": "5b07a2d6dcf49b2a3080b5163d9d8f690947c7a6", + "sha256": "sha256-PqXWDbE52fvFxfesBtd5+3Xa/nGwkxvJ24YyMSlLwtU=" + }, + "core-1.3.0.aar": { + "sha1": "7b02249f15f9366a6d98dd5d6bf613c34810982e", + "sha256": "sha256-HGtmJvFRhdj0vHyqx1lBKhq26FHs91Jjh9m5+tyr22M=" + } + } + }, + + { + "path": "androidx/core/core/1.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.3.1.pom": { + "sha1": "094f0ae90fc01f22fe2ee64545e280d351d4c3cb", + "sha256": "sha256-rIUPU2fQh3xY0/yUObRT6ik89fhA/AQ9U1CFoM306vs=" + }, + "core-1.3.1.aar": { + "sha1": "33ef11f8b162514f0520ebb7f454664eea7911eb", + "sha256": "sha256-6S6mWjfViZQ9QFpqVNG+nRKiJZSPJsTkHlEd1V6B77Y=" + } + } + }, + + { + "path": "androidx/core/core/1.3.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.3.2.pom": { + "sha1": "f2330ce1c60b69f99d55a498f32c0216807214e5", + "sha256": "sha256-r7XqSU3Qg+1ATNUfWA0hjjc2L4rjJuiTvuUhKQ7TSSA=" + }, + "core-1.3.2.aar": { + "sha1": "18a4755efb834c487bac7062e508465f0c853218", + "sha256": "sha256-lN4ZbNZ5UM/27z4axZAV+OqvYYQL3COPLPVN3vjdC+k=" + } + } + }, + + { + "path": "androidx/core/core/1.5.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.5.0.pom": { + "sha1": "57d35949ae075717fb6bea8bf103a2bc35400523", + "sha256": "sha256-2aNOM4hns9t1qqzgndRawtmkJuezcilt0OdT6DeQzJ8=" + }, + "core-1.5.0.aar": { + "sha1": "592da26c6f1263a51134546204033e0964e3c540", + "sha256": "sha256-KyeXEnlWiQac+2Pkizq2PDKlZJvdpExILrj4HKGnIWE=" + }, + "core-1.5.0.module": { + "sha1": "7b59e736c4e9d817654bd8247cbaf1fa0a3b915d", + "sha256": "sha256-44d/pSn+KRd/NKJuB5DtNVRISLDHUDv+0wslOfFobWU=" + } + } + }, + + { + "path": "androidx/core/core/1.6.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.6.0.pom": { + "sha1": "94a35cc5a7ff820b8fabad1283164da40e6a6909", + "sha256": "sha256-MglHXj3eLNDcxghAQw9RwZzPLAjGVuEj1sxQxkb6Zug=" + }, + "core-1.6.0.aar": { + "sha1": "10775f024a338475a5e374033f4532a7b3d8994f", + "sha256": "sha256-h12+yIjJGwVSFXX9GAzAiCTSxNErELAmG0+daCUhTvE=" + }, + "core-1.6.0.module": { + "sha1": "cbe6011cd3b72dd4d289344a0d769a5de3adcc45", + "sha256": "sha256-g0jitcDH4oM3C4UDfOyLMrSsq41zp6UyNaNyRGsTh8g=" + } + } + }, + + { + "path": "androidx/core/core/1.8.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.8.0.pom": { + "sha1": "6f245aaecd759215e2ae1ad7e12e95036e4fd8ba", + "sha256": "sha256-eezHl6XSAMS+P2dW0su2+kb+WCGSMEN36KZBD2Bm6z4=" + }, + "core-1.8.0.aar": { + "sha1": "56215072292d2db400ba7bfec758a980c0ecc9a4", + "sha256": "sha256-SMZKFew+sRv7MzOeXOtw7H+CG9LfouuGdevVNTMX55I=" + }, + "core-1.8.0.module": { + "sha1": "9d0f0776c1a6f204eb5409a72b80b81661064e0b", + "sha256": "sha256-UF8YOIaWEVGdZex/62UOiIVuNoLjfkKioktz5lWpjXQ=" + } + } + }, + + { + "path": "androidx/core/core/1.9.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.9.0.pom": { + "sha1": "18b045c91161c607687c27bd3e55ecd28bd13391", + "sha256": "sha256-naZhCFuyo8pEwHEl0M773UoA1luEGlO9stVgC2Qh4Oo=" + }, + "core-1.9.0.aar": { + "sha1": "aa21c91d72e5d2a8dcc00c029ec65fc8d804ce02", + "sha256": "sha256-i9o+46iIh9VPZnn7a2zXiGKfcyNKyRyLvtkk5yHshbg=" + }, + "core-1.9.0.module": { + "sha1": "373f58888308cab13941cc415cdb9bdf4f2402aa", + "sha256": "sha256-hnSt4dEpYkad0tHm7cA7Rl8g0YCwhAoSm8QLNR6GUdE=" + } + } + }, + + { + "path": "androidx/core/core/1.12.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.12.0.pom": { + "sha1": "13e7da54eaddca269cf27c7fc339c187f0c46ef3", + "sha256": "sha256-R19uIhDrB5RxYHaT5l9lszPAgAMNWqIqGXLFEwFaolU=" + }, + "core-1.12.0.aar": { + "sha1": "5aa3088ed93ba8ebaea2b8a83befdfa829339e7a", + "sha256": "sha256-Qv+nykfXuo/h2HTFfvnHERvEGisMDCFRijngfSIt7Ys=" + }, + "core-1.12.0.aar.asc": { + "sha1": "1db1e7109b2ac2100f73578243c703ed671cd4e2", + "sha256": "sha256-TQXPZ0mRWTP4TcfdDHjLbIqdEgrkEps9WfdpZR58mtU=" + }, + "core-1.12.0.module": { + "sha1": "0ec1292c59a175ab0b9ec906831750a7ca4d0ced", + "sha256": "sha256-L2P77aI8oJGXONCeQG3mYfIbrFg9bgSheX3Ld+O2rpU=" + } + } + }, + + { + "path": "androidx/cursoradapter/cursoradapter/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "cursoradapter-1.0.0.pom": { + "sha1": "b1d8c36333871395c98e157e903881d44a509899", + "sha256": "sha256-YtlciYUK8hAwsZ8U1ffs1ti8yaMBTFkALsmWJMqsgQA=" + }, + "cursoradapter-1.0.0.aar": { + "sha1": "74014983a86b83cbce534dec4e7aa9312f5f5d82", + "sha256": "sha256-qByP54gV+kffW3Sd61JyetEfk5faWLFgF/TrLBHihWQ=" + } + } + }, + + { + "path": "androidx/customview/customview/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "customview-1.0.0.pom": { + "sha1": "53c54220d41f17dcb79d4d7f39432a328df544b2", + "sha256": "sha256-zp5HuHGE9b1eE56b7NWyZHbULXjDG/L97cN6y0G5rUk=" + }, + "customview-1.0.0.aar": { + "sha1": "30f5ff6075d112f8076e733b24410e68159735b6", + "sha256": "sha256-IOW49lJqNFlaYE9WcY2oEWfAtAp6lKV9qjVWY/JZTfI=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-3.2.1.pom": { + "sha1": "9191d0e28f6349d5754483edbab6e60513666176", + "sha256": "sha256-hey2+lIPgANJtSJgAVT4E+7tmWx2F6ffpLO3GagJ/6Y=" + }, + "databinding-common-3.2.1.jar": { + "sha1": "91c10c7ce255ab29ec4aea8a469d6c8ffa60bd53", + "sha256": "sha256-TESd55PGYmSu+55V4XupLlsL0diEQK6JgTvluzzUxpM=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-3.5.3.pom": { + "sha1": "0f85f7500d68d4eaed465ec194e18779374a7a68", + "sha256": "sha256-paY7t0Iz/AxIVcvQLT4sAPgqTvvMChxgS3SmWRod2tw=" + }, + "databinding-common-3.5.3.jar": { + "sha1": "72bb6cb779ae61710a01ff76d9d3cab355c44a4f", + "sha256": "sha256-WRoV+qBtmUWoqA8m8n2vBosrLirqakvhN7/SixHZVgA=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-3.5.4.pom": { + "sha1": "77802aaa543731f35a9779eba2a2809e1df2bb42", + "sha256": "sha256-iQeamXWtPsbgP6cL95jCL9Ym6+ZW4LWBwkMWOdAW/Zs=" + }, + "databinding-common-3.5.4.jar": { + "sha1": "6c02f6a4a55b3ee69345055fb9c63acf931bc972", + "sha256": "sha256-hQhnKMcNLrdivoyZHZNWzE6H3Mn6yiNRkFkyslpg3m0=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-4.1.0.pom": { + "sha1": "f47c001ca18d95099651af8aa71ad3941280cf1a", + "sha256": "sha256-G3s8yXOhVYHS+rITkqAdhBI7/+IPcsrMLoSNTaV1Rz4=" + }, + "databinding-common-4.1.0.jar": { + "sha1": "e0c4fa410ea245a70a9dbd140a164d50b10e55fe", + "sha256": "sha256-MqN6PLEaKwkco7SgikDmYFzd4ZoH9taf+HRERfmqr7o=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-7.0.4.pom": { + "sha1": "2e1a290309576b9ac273becfe79e79ecca2a60e0", + "sha256": "sha256-Ddoz+X3Acc7j4rLbiY7SYk0crJEbhfe4titcPSb2J4U=" + }, + "databinding-common-7.0.4.jar": { + "sha1": "64f538bb8479deadd08795fc9baf57a67c2f4432", + "sha256": "sha256-f10+jBqLMzmohYDWW80LORZvgY4f0AyQN4cVmqAQrO0=" + }, + "databinding-common-7.0.4.module": { + "sha1": "611f5415ea682395e0dcd6a959797fbd71763162", + "sha256": "sha256-9RXrGhFXY/W2/Bh8VR2G/7jjmEwH182CQUr9IKbJDiQ=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-7.2.1.pom": { + "sha1": "53f208fbbb5674a3793c8bcd6ddcbc22822dc410", + "sha256": "sha256-D212TluG+hroCk28ImgEpJJ4iTvY+pBSrxNbB88Pw/U=" + }, + "databinding-common-7.2.1.jar": { + "sha1": "0cb53bd625b7a99cf218f46634d07d1886441288", + "sha256": "sha256-hS5GWkknHbx/ar++NnNorrbTEwofqvlhfMud5hGNHKI=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-7.3.1.pom": { + "sha1": "ccaf78f6493b04ce564e0ed51e2a63e479fd39ca", + "sha256": "sha256-6aEw+4Ncel2z/2/XhZg91KF2xTG5DG0z9LIdahLRy2Q=" + }, + "databinding-common-7.3.1.jar": { + "sha1": "9ce4d7d086454caee5620765f7aa39115aeffb47", + "sha256": "sha256-a0oSHF0nTTTgOGqgGYSKLpRuvZjkpfaTsd9op3GgMb4=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-7.4.2.pom": { + "sha1": "eb3967aa265e1a06f1362fe1288b330c62737ef6", + "sha256": "sha256-icyCIUaHwMeZr15JOnLBRwuAlAOnWPajQPJvTlj++zo=" + }, + "databinding-common-7.4.2.jar": { + "sha1": "84ed411631d2c4113f4ad992669fe3f50a83f072", + "sha256": "sha256-n6IeIDAnPuPLjEcBw0GvjPk1l5GjtFsrwL4HhwwDscg=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-8.1.1.pom": { + "sha1": "4a90995d94c322e8b3aeb8b368120b06edb60c4e", + "sha256": "sha256-UDlY+Wx8LgsoR5RvoLbKh4uXG924euaC/Gl92G590a4=" + }, + "databinding-common-8.1.1.jar": { + "sha1": "2be3530b78477835e69dce953b17adc1dbc5c6fa", + "sha256": "sha256-Zsq4JjnawPbCQzRkwJOwdNYIxLuIfsOKm4vErJgSZzI=" + } + } + }, + + { + "path": "androidx/databinding/databinding-common/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-common-8.2.2.pom": { + "sha1": "7a26d8361d8ac0df0770f5ace6354f84b429b904", + "sha256": "sha256-LaV4nA/pvJk1gOoEZyK46YCCvgNfoh2cAnr4eSXUUFM=" + }, + "databinding-common-8.2.2.jar": { + "sha1": "2be3530b78477835e69dce953b17adc1dbc5c6fa", + "sha256": "sha256-Zsq4JjnawPbCQzRkwJOwdNYIxLuIfsOKm4vErJgSZzI=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-3.2.1.pom": { + "sha1": "4bae47f040933df0c02a0dbd783a0cd852b4cc91", + "sha256": "sha256-0UISpF0A3GfKR2qfVzfhSfTdc76IxBfEtJabd2pFKG4=" + }, + "databinding-compiler-common-3.2.1.jar": { + "sha1": "dee402009966fb2d3bb03e3186f3f16b9c503c86", + "sha256": "sha256-ikWq3acE1DvjUuBIldHfoAFI3t1aGCIlg2PV6/Sdl7Y=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-3.5.3.pom": { + "sha1": "345b6f4cea120faa14f118c08150de5d8afccce1", + "sha256": "sha256-LRdv8FuX6tdlmxdHuhK/a/bhc8zIONVFAniG0toDu+A=" + }, + "databinding-compiler-common-3.5.3.jar": { + "sha1": "32bfc30c00fdc550723ffd9bff3ef2471fbd4a3a", + "sha256": "sha256-yDZslWiT00cwjhF5Be1v2xq+gFW54gEhOrjyMO3GNRg=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-3.5.4.pom": { + "sha1": "062e8d8f0b6efd6bd452f10e707d3ed59ae94b57", + "sha256": "sha256-gmXJh+jGS8As2y9sGEzovrYoVtE2Mupk0tOQShzcIxQ=" + }, + "databinding-compiler-common-3.5.4.jar": { + "sha1": "e3a2ee3bc2c0295d9d2b4519c1bba5786e8a5435", + "sha256": "sha256-nGkauevkhYx2l6RnXmJ7FWEw4iQhGM650+HKBo86vEg=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-4.1.0.pom": { + "sha1": "7fbfbe352effc935d5e0a5040f8d7e49030b337f", + "sha256": "sha256-+9hzgVPT8c/lD8CAAG8rW3ebfJO/2q7RaBSa9hGp1eM=" + }, + "databinding-compiler-common-4.1.0.jar": { + "sha1": "ce7160dc1ad2cb4144d24da6b81a4bc97ec5c78b", + "sha256": "sha256-Fg2wq2Ku0644e9kJQTsm0vL1jgUgf5a28DYuwE97PCo=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-7.0.4.pom": { + "sha1": "ad95a8d33a1b39110f3c96b036ed80df4137790d", + "sha256": "sha256-+TUCaLTEh4xLukG7I+XUSaubonlOM1nPBzQrssV+7Sw=" + }, + "databinding-compiler-common-7.0.4.jar": { + "sha1": "dfc271a04c341c5199c09d088dfad5b7e0cead2f", + "sha256": "sha256-aJVgKiXbw8jlNZwBuOjj4ALSFA/QGB0S6URblJdgaOQ=" + }, + "databinding-compiler-common-7.0.4.module": { + "sha1": "8e42398adc4d8023e84bd1777a72a9b8a67f0437", + "sha256": "sha256-ziS9VuBXyx4QZku+1/C+8X+byh7Nrqr/Gp8PFWAHEaE=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-7.2.1.pom": { + "sha1": "4e770350098a205dcb3c1971ab00b6aad0efd1f3", + "sha256": "sha256-vOEGO5uOym4lT4hHLfHiqB+Wryx6doDqEmuukucGMrA=" + }, + "databinding-compiler-common-7.2.1.jar": { + "sha1": "4a85b96494288c49d88bb1c521be45f7c36cdbea", + "sha256": "sha256-g2bhoINk4MUmEriaT6Yd0sdYrRkk+AKf32Ee3FBGp48=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-7.3.1.pom": { + "sha1": "d5eeb4fff5058764b22a715dad444198d30afcd5", + "sha256": "sha256-71VKG6HyO6o8EG6KtCtJs1vdN8ad6gvaQuFqeX7GZeI=" + }, + "databinding-compiler-common-7.3.1.jar": { + "sha1": "7d9048591c2f0932195a8f9019cf7f7f86afd043", + "sha256": "sha256-qPEPZCL6Rd3aKwgv87zvraZrmaJpi6+qNrAun4XcGzw=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-7.4.2.pom": { + "sha1": "0da169d4c2d841ffe7877d7327bb57039c8648b4", + "sha256": "sha256-JBtCivRdoFu8BLUkej+Wb54L+g0pXxBLdVtiSFCO43s=" + }, + "databinding-compiler-common-7.4.2.jar": { + "sha1": "a4261dd0ef1ef53b45a2be27e059b1b6b7dd87e1", + "sha256": "sha256-b3kJ77cbrRV5lNl17LFsgEGiVqnNLhW7vXEjOe+zjp0=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-8.1.1.pom": { + "sha1": "13607f99fd17f490c972a63f9e6086c5218b6754", + "sha256": "sha256-4IXXzrwI57OFMDyRBD4950f5+N4Eb0mPG1m3hxqI+z8=" + }, + "databinding-compiler-common-8.1.1.jar": { + "sha1": "63d39b93de85c2a0c8407c5ab906ffe39f41f734", + "sha256": "sha256-Ws4UXH4GKJcUh3FRM5xV2f+2hjUXvfW887lVX4yNlD4=" + } + } + }, + + { + "path": "androidx/databinding/databinding-compiler-common/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "databinding-compiler-common-8.2.2.pom": { + "sha1": "4d1f0bd913d55d11c058c7da0d42f3b144f2f400", + "sha256": "sha256-h8THM2ojnPmyfFC4CDggRb/AtdBVb6b/5SrdDt0wcqg=" + }, + "databinding-compiler-common-8.2.2.jar": { + "sha1": "2d572c6e5813359baa393caf8305e99fdbc062a8", + "sha256": "sha256-J9hMNNAn9gFVFCuJrgRdq1+cHXITFk2F63DIQsHqmHk=" + } + } + }, + + { + "path": "androidx/documentfile/documentfile/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "documentfile-1.0.0.pom": { + "sha1": "8cb912559da493946f05d1b5aa17a4b409a3e6eb", + "sha256": "sha256-ATKIqTF6VScGzmJfskST6CIyiFKSI+xXjPhVpa6cFuU=" + }, + "documentfile-1.0.0.aar": { + "sha1": "66104345c90cd8c2fd5ad2d3aad692b280e10c32", + "sha256": "sha256-hloGHvL60WUi+EM1NrjUcgjEb/fHdFGX36HutIGGlIc=" + } + } + }, + + { + "path": "androidx/drawerlayout/drawerlayout/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "drawerlayout-1.0.0.pom": { + "sha1": "436d14f0b503b411431a0c38e5368ba4ec4b1e62", + "sha256": "sha256-2mczQlqD9c6FCHj6cgEII0X+18Zo3VhVD90ZwDlsb6Q=" + }, + "drawerlayout-1.0.0.aar": { + "sha1": "dd02c7e207136e1272b33815cc61e57676ed13a2", + "sha256": "sha256-lAJELNxaQ89i+xT4z5jGM0LU2dm4BcgDPGz36AJ0msE=" + } + } + }, + + { + "path": "androidx/emoji2/emoji2-views-helper/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "emoji2-views-helper-1.2.0.pom": { + "sha1": "3fc7535bd4d186fd2c447a711217e5a9fa03e491", + "sha256": "sha256-21kFLonRO3lGSwS9H4dNeX8dl9g/STf2aaU+H2ioAjc=" + }, + "emoji2-views-helper-1.2.0.aar": { + "sha1": "4a22802fc9e88ad232522f04a35d1fdc3e5b4df0", + "sha256": "sha256-f/pNRk2dsln8oM21D71Ktj1ocrzaWUaLn3VVUEx9WsQ=" + }, + "emoji2-views-helper-1.2.0.module": { + "sha1": "73b187d55e6322910dbcb9d3f6f8e4353a7a2035", + "sha256": "sha256-o6nbWBq/F4ewH/FcQPBZUw6OZPOTfKoteI9C6zmJMmg=" + } + } + }, + + { + "path": "androidx/emoji2/emoji2/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "emoji2-1.2.0.pom": { + "sha1": "c929e0180adab54696b517e34b1a16ab1b153f45", + "sha256": "sha256-+vWVXSybCbupLlFWnyuShBne0FtclknARTySXH2mI2A=" + }, + "emoji2-1.2.0.aar": { + "sha1": "2c95c9aa87788e4d9ce692ecfc961db004e57480", + "sha256": "sha256-8xoGwVDssDBz9Vpvewt0okCmqNcnwUznZybQIFcN+ow=" + }, + "emoji2-1.2.0.module": { + "sha1": "2b4a0059ae2b65ffa8204a574ada56402fc98498", + "sha256": "sha256-nRmWzKA3d7qh8nzRVTHbmDpjPa43uQ+FvVNQGstWaZ0=" + } + } + }, + + { + "path": "androidx/exifinterface/exifinterface/1.1.0-beta01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "exifinterface-1.1.0-beta01.pom": { + "sha1": "113ca4bcf7feed9865496b8980cb92dec0d99d12", + "sha256": "sha256-UOlTruqZnZJ0YxoNyjz/AinX5LFFHcb/3wcxyOApr8M=" + }, + "exifinterface-1.1.0-beta01.aar": { + "sha1": "3a3ae85030468e63b28989401fe69db581e40c82", + "sha256": "sha256-/Jgpzzgb7Mf4jS5I4WgLBWgMJWSDbiZrzr7gjALNmZw=" + } + } + }, + + { + "path": "androidx/exifinterface/exifinterface/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "exifinterface-1.1.0-rc01.pom": { + "sha1": "5aeae1be812a6eda52d9f2794b0bab6da4bfa40b", + "sha256": "sha256-HXB8fuQ3zxQNq3sRiNYuy9ocLxiF5Dg0GQ0mnbsYHVg=" + }, + "exifinterface-1.1.0-rc01.aar": { + "sha1": "00dc0a481d0dea36957eab478d53e98c378144ed", + "sha256": "sha256-FG+rRgVvQu0o6vD5A6ihB2sS9jD9HZDZs0iC1EtNDiU=" + } + } + }, + + { + "path": "androidx/exifinterface/exifinterface/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "exifinterface-1.2.0.pom": { + "sha1": "34832395bc9a4fe2aafdd73605ddf9da9f842e7a", + "sha256": "sha256-b36Rv6bMAvsxaiyq4TpYDxJ57Z6qWuAvBw3xVm24KeI=" + }, + "exifinterface-1.2.0.aar": { + "sha1": "e8a2f661f33bbfa8e9d226aa5dacae90aedf2fc9", + "sha256": "sha256-quaOUTCV5HWnZwVW6suncuwrtZLRcYcJFXjT/vlHrqc=" + } + } + }, + + { + "path": "androidx/exifinterface/exifinterface/1.3.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "exifinterface-1.3.2.pom": { + "sha1": "2f7b38976ad64276465c1cb69a1e37e2fdaa4186", + "sha256": "sha256-jZqxpyQvwUP6YvQrXhYA3rPOV9af40I9lXqTgpFX7jc=" + }, + "exifinterface-1.3.2.aar": { + "sha1": "ce8e52ca0749a3b6d07758a26e1cc642180bb4d7", + "sha256": "sha256-h3DBgBA+C4wEoH60xZFTr2ObCeyiXerpvc2vhp0eW2s=" + }, + "exifinterface-1.3.2.module": { + "sha1": "b98d44cf74b2d9d59f49fdaf54163622c2ad164b", + "sha256": "sha256-ELpbXL6n9ch1i+T9rsYKNUXokaETDYMKRCuIz1M2qIU=" + } + } + }, + + { + "path": "androidx/fragment/fragment/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "fragment-1.0.0.pom": { + "sha1": "3f74765ebe6789c38bcfb2e06640a43a0d7d5252", + "sha256": "sha256-4ynWczYelNLo9NTRTh8FhjaL1D+xnv1XZs50mLzM0WI=" + }, + "fragment-1.0.0.aar": { + "sha1": "0b40f6a2ae814f72d1e71a5df6dc1283c00cd52f", + "sha256": "sha256-Zd0y1x/mWjLneYmmz7GtCTBwOJJ/gqdAx2ERYtC1GPg=" + } + } + }, + + { + "path": "androidx/fragment/fragment/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "fragment-1.1.0-rc01.pom": { + "sha1": "6cab14f710c74d04aef6f4b34b734fdc5db014b5", + "sha256": "sha256-jL6M3jGmtjwDZBbxBZz5BkwLKZWHFvXR+NMwOFn/x9U=" + }, + "fragment-1.1.0-rc01.aar": { + "sha1": "5369151f89369269c30897029bed5cf258509f60", + "sha256": "sha256-avUj7uDxPIgChXMHfS30hpb3nfFzASdULo6CQzg8q74=" + } + } + }, + + { + "path": "androidx/fragment/fragment/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "fragment-1.1.0.pom": { + "sha1": "753e03826c6efdace71d94ed9d04bf008fa6ad3d", + "sha256": "sha256-73jrJ6wC3fNUXV+KOFfHOig3oBhT+NX893JRAR21JUQ=" + }, + "fragment-1.1.0.aar": { + "sha1": "5f9efc7569e651415a0958d6b3e6226ef2825c24", + "sha256": "sha256-oUyLjyFT8SjoAPvSZqa+qxwoOYKinsVw0swF0wfYFJY=" + } + } + }, + + { + "path": "androidx/fragment/fragment/1.2.5", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "fragment-1.2.5.pom": { + "sha1": "15c5ddb5d62f1ab369c268679d5dda05c8a194a3", + "sha256": "sha256-3wrKRrYrtHzGYsvO5jNy220qKFlHjuOLWU+6NDP+MKU=" + }, + "fragment-1.2.5.aar": { + "sha1": "9517ddcc4f298876b8af1ed4bc8cab94ee40d2e4", + "sha256": "sha256-0Z6C0ULe9sThNtpwv5LxlMDsxh0Uq06EVnss7Qkg+pM=" + } + } + }, + + { + "path": "androidx/fragment/fragment/1.3.6", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "fragment-1.3.6.pom": { + "sha1": "27c5fdad4300b48d2fd1ebc0e5694081f3f52472", + "sha256": "sha256-pHD2dEy/irCbR3GTAwoNJ/1JOM9xe1uYL35qCrboXFU=" + }, + "fragment-1.3.6.aar": { + "sha1": "6391fc504e7c0f24051ac8b9da3f0ba4d1241dff", + "sha256": "sha256-EvCDG08ICS1d2icsGSPBGgIv8gzv/tPoAXUeIbuNHB4=" + }, + "fragment-1.3.6.module": { + "sha1": "8a3460656d01b95b99914ca756cd603a9abc14a4", + "sha256": "sha256-oUCYGj5l88qzVGUmggYdI9GbAeAO5xK8OKXpsPae7c8=" + } + } + }, + + { + "path": "androidx/fragment/fragment/1.5.7", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "fragment-1.5.7.pom": { + "sha1": "c99d49f425f673bbc8479bfe15ac76820ae3b2be", + "sha256": "sha256-YboRvJYY0s/AjVbdOZmlxpm/iozT/0ovqLAugg//dKM=" + }, + "fragment-1.5.7.aar": { + "sha1": "049fe69bb50cdf391fd3b823611672371a6bb476", + "sha256": "sha256-WcdBELmTeIhe0GJC+KJtowXqTcpT7WAWNUDdDi48ZnU=" + }, + "fragment-1.5.7.module": { + "sha1": "a79be0eb29a74327194e677f19aa1c3a5c762e3b", + "sha256": "sha256-8PB3//dK+BWMsvCOHBmGBl8xYaKfZO86wN8Q0QSA/Tw=" + } + } + }, + + { + "path": "androidx/interpolator/interpolator/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "interpolator-1.0.0.pom": { + "sha1": "dfa9c6b5826fbf2f910e1720ce2b0421d7aafec3", + "sha256": "sha256-DdwHzDlpn0js2eyJS1gwwPCeIugpWSlO3zchciTIi3s=" + }, + "interpolator-1.0.0.aar": { + "sha1": "8a01fa254a23b9388571eb6334b03707c7d122d7", + "sha256": "sha256-MxkxNaZP4h+iw17sZojxp25RJgbA/IPcG2ieN63Xcyo=" + } + } + }, + + { + "path": "androidx/legacy/legacy-support-core-ui/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "legacy-support-core-ui-1.0.0.pom": { + "sha1": "6bda132303af80266ef7f6d8e86b0712f76f8d28", + "sha256": "sha256-K6r38XVRdlUkvR5NL5VKKgF4oRIDnPVJuot9S0HmaTE=" + }, + "legacy-support-core-ui-1.0.0.aar": { + "sha1": "61a264f996046e059f889914050fae1e75d3b702", + "sha256": "sha256-DRJgxufmozf4dd9xtRaTHnA/cW6QiJgXzTog+lrD2Uc=" + } + } + }, + + { + "path": "androidx/legacy/legacy-support-core-utils/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "legacy-support-core-utils-1.0.0.pom": { + "sha1": "838c7839cc6df6d56f594ddc2050bd36f7f0c1f3", + "sha256": "sha256-j9CTAIs+58BuUseNoq+YCntHtpuWf6kdrXr0ZvegCjg=" + }, + "legacy-support-core-utils-1.0.0.aar": { + "sha1": "9b9570042115da8629519090dfeb71df75da59fc", + "sha256": "sha256-p+3PAdW1KzA0BzAnvEd1t4pHZLtiAruR1hyCmt2N0cc=" + } + } + }, + + { + "path": "androidx/legacy/legacy-support-v4/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "legacy-support-v4-1.0.0.pom": { + "sha1": "a0c16ebb4d57f6ec356dd8756285537c0d8b67dd", + "sha256": "sha256-7Yj7Ekz7cPK7ko7eE1eHQ3SqRYgby22XRMHcZoD6fFg=" + }, + "legacy-support-v4-1.0.0.aar": { + "sha1": "03e1271b351e1209661b9fd769cd6681a31678fe", + "sha256": "sha256-eP7BSF8POIpHSQIt1RQWhXEnzSVErhw/0LFliQVUgLA=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.0.0.pom": { + "sha1": "23b760e7668836e3afa3e63608dde9e747a3e1cc", + "sha256": "sha256-BNUlBzRpIU0Mmegaqodd1Ui6MrgpRavYMmvFAinfcA0=" + }, + "lifecycle-common-2.0.0.jar": { + "sha1": "e070ffae07452331bc5684734fce6831d531785c", + "sha256": "sha256-e616GIgErepvofNdXvmbcF8gvZPsrd5IR2D/hrU1/vw=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.1.0-rc01.pom": { + "sha1": "3787cc1eed8c7e6a822fa60215549387e4a0ccce", + "sha256": "sha256-6jngWmjqBhWDw/5kFjUhjK9hiKTByYfo1wlL7tJrjPk=" + }, + "lifecycle-common-2.1.0-rc01.jar": { + "sha1": "c67e7807d9cd6c329b9d0218b2ec4e505dd340b7", + "sha256": "sha256-dttr5TO9cw+zYcL+sSosJtmVKCR0aEfagmAe+B8IJkM=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.1.0.pom": { + "sha1": "de2b915adafa394edd5eadcab6b5089d08fb71f6", + "sha256": "sha256-aSwqYEN6lSIMj7tuiUMZbyVfU10afFhxTmxGUweC8nY=" + }, + "lifecycle-common-2.1.0.jar": { + "sha1": "c67e7807d9cd6c329b9d0218b2ec4e505dd340b7", + "sha256": "sha256-dttr5TO9cw+zYcL+sSosJtmVKCR0aEfagmAe+B8IJkM=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.2.0.pom": { + "sha1": "0d038e279eff9df92cdf3e324314ca06d2de3250", + "sha256": "sha256-zDCooql9N2e9Fpt0fAeHJzCAlt06t5F9gLLx9C0Ri9Y=" + }, + "lifecycle-common-2.2.0.jar": { + "sha1": "4ef09a745007778eef83b92f8f23987a8ea59496", + "sha256": "sha256-Y4mNq/fP5exdfti4wlZMFCe+h24UlurZXCcDz1nTc0s=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.3.1.pom": { + "sha1": "0ed381b5d87316483120f3328b489056246e6730", + "sha256": "sha256-jNI9iJoUCVxs4WhA0psaY4j6XhFRRMEwnU1tRpwbw1E=" + }, + "lifecycle-common-2.3.1.jar": { + "sha1": "fc466261d52f4433863642fb40d12441ae274a98", + "sha256": "sha256-FYSPtW2zL0x83HKzJAAxg9UqSITWvwm+cIrH9YfRObU=" + }, + "lifecycle-common-2.3.1.module": { + "sha1": "b5e5c27519d0931380c5985dcd0d9b9547888d1f", + "sha256": "sha256-X7fIUU2MVsraXinvidwCiecZQqtMsLLm3KE3udy4/dQ=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.4.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.4.0.pom": { + "sha1": "6d87ae16a2fcb198d436c62bc19088a132debf1e", + "sha256": "sha256-9kuiN1KUEE3V6PrnNFph1SGeCpTMIvJs72YwuwqQQjY=" + }, + "lifecycle-common-2.4.0.jar": { + "sha1": "1fdb7349701e9cf2f0a69fc10642b6fef6bb3e12", + "sha256": "sha256-IK0VIPYlz0Veav1ykJiDBtOpiG76mT4IYPur9Ls/e9o=" + }, + "lifecycle-common-2.4.0.module": { + "sha1": "d9277df62050569d37d8fbc9a087e354e878cddf", + "sha256": "sha256-WtXq/CLosE5Y+oHRjSVwVilxl34Y8AlQC1vUSbxjN7w=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.4.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.4.1.pom": { + "sha1": "8ab6312080ec6652c17a3b90870bf0327693eb9b", + "sha256": "sha256-ZylrB5jEJzbCAWd2CW9uwjNfnwbCJP1/xeKqipccuvQ=" + }, + "lifecycle-common-2.4.1.jar": { + "sha1": "1fdb7349701e9cf2f0a69fc10642b6fef6bb3e12", + "sha256": "sha256-IK0VIPYlz0Veav1ykJiDBtOpiG76mT4IYPur9Ls/e9o=" + }, + "lifecycle-common-2.4.1.module": { + "sha1": "64e98ba47fae3ac3c286f288a89882ad27bdfac8", + "sha256": "sha256-1bkR+jiuuXTWUoA+moWV7GEv6KGhE+jsT5lHnVxQqHg=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-common/2.5.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-common-2.5.1.pom": { + "sha1": "37ed1700b5b91beccd2a924f2175e6b4d91bc90e", + "sha256": "sha256-gtIZLj3p/df0W5CI4nMV59vsAtCUYy+qajklFpDSjMA=" + }, + "lifecycle-common-2.5.1.jar": { + "sha1": "1fdb7349701e9cf2f0a69fc10642b6fef6bb3e12", + "sha256": "sha256-IK0VIPYlz0Veav1ykJiDBtOpiG76mT4IYPur9Ls/e9o=" + }, + "lifecycle-common-2.5.1.module": { + "sha1": "99ee1f67ed0b59ddca92d495451acb776d06d313", + "sha256": "sha256-fUvClhzVvTmeNiHUNPDEU91srfiR+RepRswpGr3ajxo=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-livedata-core/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-livedata-core-2.0.0.pom": { + "sha1": "b78b5eea709c488e36f0d4cb8932bbb5757649f5", + "sha256": "sha256-ZQ/aGiMvVml0IUBXFXDMrS9HpxIlqw8CF5vI0US5gsg=" + }, + "lifecycle-livedata-core-2.0.0.aar": { + "sha1": "1a7cee84b43fa935231b016f0665cd56a72fa9db", + "sha256": "sha256-/eM07H4idEwPW/58rxqEydcXMnBEQAV3vfm9kh7E97w=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-livedata-core/2.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-livedata-core-2.1.0.pom": { + "sha1": "2a4b17102be06191a3f30047e9467dc483f0b1ee", + "sha256": "sha256-aDnS5oyP7kzDu/BOaykaauhCQD/NX42UTCeBTSlt7/M=" + }, + "lifecycle-livedata-core-2.1.0.aar": { + "sha1": "897d534c89162cb35f14e17d1d421e70667564eb", + "sha256": "sha256-oVB0PobDDt3xZgrUVLH4YEHv3vzRoDkyDEwm24f3EZo=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-livedata-core/2.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-livedata-core-2.2.0.pom": { + "sha1": "962f3b9666f789eebafa7b5cdfd0af8f8aed1ab9", + "sha256": "sha256-qlFiG+OBfR9dITNrDAUl/pauiZzsZWwT2OoYpR6jLC8=" + }, + "lifecycle-livedata-core-2.2.0.aar": { + "sha1": "4bcd5721a6358c312b86d219ff57d4a4a841fc50", + "sha256": "sha256-VWwfOvkKqdfQ0zBWWtv22nGyQpFIuskeB8SF9Pmr9hQ=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-livedata-core/2.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-livedata-core-2.3.1.pom": { + "sha1": "33a214022e333d8cd6d8c94b759eeaf2b7e3de79", + "sha256": "sha256-+5liIuPeR/G964s2WQuMEYmKT3L5E42hhzZdMQOhQ/s=" + }, + "lifecycle-livedata-core-2.3.1.aar": { + "sha1": "d6d9279495dc234dc37cecf461b3b626825086ef", + "sha256": "sha256-5V04w3JGDwoDmX3clQxnInURNA/XT4Y02Z0pZTzYGrE=" + }, + "lifecycle-livedata-core-2.3.1.module": { + "sha1": "30c43515ee01e54dae9477a18e5f14b18cc445e8", + "sha256": "sha256-seCV1VDTmn1sgVdh1tvj/WTrMaOdwoFG54u/LAG6j0E=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-livedata-core/2.5.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-livedata-core-2.5.1.pom": { + "sha1": "43192e508a07c50f8faf06768576002ff82c9d93", + "sha256": "sha256-ZC8Oldo/OGux0TbvihFIHczThMKKCT2Utce82qLvpbM=" + }, + "lifecycle-livedata-core-2.5.1.aar": { + "sha1": "a55defc29b52baf41304c058c91dba38b5c7651c", + "sha256": "sha256-7nkhA8oki/rxUMRak4ceTPfozrq5kOD2L33l1P8vIJ8=" + }, + "lifecycle-livedata-core-2.5.1.module": { + "sha1": "5b28be9eee128e14c41b82d552a208def9badd3a", + "sha256": "sha256-PziOngeJAZcMK/z8Av7K6UjeS0a+UhGRmuB9ASyimA0=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-livedata/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-livedata-2.0.0.pom": { + "sha1": "b1987c65a98cb0d6f43048be0c20f20f932fc1aa", + "sha256": "sha256-qEhC/8DxTlGNt1wFzBEmgKikoWT6eDlb4y2IMEpDlCM=" + }, + "lifecycle-livedata-2.0.0.aar": { + "sha1": "c17007cd0b21d6401910b0becdd16c438c68a9af", + "sha256": "sha256-yCYJztjEmPCnAaMPtncbt0gIYNruhNguCoHuhu33ujk=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-livedata/2.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-livedata-2.1.0.pom": { + "sha1": "d9ce2eab5e7f47299fd0653cab6862152257e10e", + "sha256": "sha256-HkxYDJleQc0YRVz7h+PkOVW+nbnsY6j7PHJZ8Bt8Tjw=" + }, + "lifecycle-livedata-2.1.0.aar": { + "sha1": "454b4bcf4ce0b860b4b4d6a16cba132bb7bbae8f", + "sha256": "sha256-JC5Ea+09s28N8Kqwy3+RBgvS2retytERet9U5yTNHSY=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-process/2.4.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-process-2.4.1.pom": { + "sha1": "c7664ba7375aa54116d482246563c4a5a21f23d2", + "sha256": "sha256-2n4uEMrn12JaCuRUAXoSV0nheRoTnC13feka3YlBCvU=" + }, + "lifecycle-process-2.4.1.aar": { + "sha1": "3e9686bbec67987b55502f4e4a2d662734d5781c", + "sha256": "sha256-22SbPvok4xBSFFMQsALbkdo0az+JwJPsOMMEbbReeU4=" + }, + "lifecycle-process-2.4.1.module": { + "sha1": "1fb9c6e7014d52a8bdb18efd96b1b0acb8324857", + "sha256": "sha256-46rj7QS0dE/zFFLpj9KZ4639KNO1cjZh2WeLkvoJzrQ=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-runtime/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-runtime-2.0.0.pom": { + "sha1": "46f0c189fc05bc4a80ab5399919d5ba0b5d71a33", + "sha256": "sha256-qSpG+nrsisMmpdV4c0otW2MgaXaZa54GyuFxs1sKtt4=" + }, + "lifecycle-runtime-2.0.0.aar": { + "sha1": "ea27e9e79e9a0fbedfa4dbbef5ddccf0e1d9d73f", + "sha256": "sha256-5K/J5jYYP28+Dt8c9GEhpJL/0sZzB1uwf1XHqZ3UPPs=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-runtime/2.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-runtime-2.1.0-rc01.pom": { + "sha1": "37d279635d4095bc4a11c476399154343e8f76cb", + "sha256": "sha256-z2Ki8QyEf5DAgGbRKF3RBiJZj5MYxoIFh4lPzYzc4SI=" + }, + "lifecycle-runtime-2.1.0-rc01.aar": { + "sha1": "9adcac4dbf51391798e5dbeb1c414408d7ff6558", + "sha256": "sha256-+z3/ItgoT/B5tMyMkZsSaV2JZjCAg9XRW0zZYdQaEho=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-runtime/2.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-runtime-2.1.0.pom": { + "sha1": "87fefa0cd4644709a2afc1f2da09397694bfe8b1", + "sha256": "sha256-r43PAtstebzUtT1r6NvzfXmjKzih38rru9h2bg2Vn8s=" + }, + "lifecycle-runtime-2.1.0.aar": { + "sha1": "24af6c5162c83b5bf073e16608fd87821422fe48", + "sha256": "sha256-5Rc4l7ll6HBlHoPZ1a8XQtP1MtWIYyI6OQzjoZTIMSs=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-runtime/2.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-runtime-2.2.0.pom": { + "sha1": "283a21b2c7ef1c44075724edd605ccaab55fc293", + "sha256": "sha256-fxVAZv7RyRYocPcosgg1KDGz0fjyP104iVilDKTp9EE=" + }, + "lifecycle-runtime-2.2.0.aar": { + "sha1": "62fab579508713b8bbb6d251b0abff6e538e8f46", + "sha256": "sha256-L4ZsB6HzOoybtpqVRdTyC08GKM0KFVQyOG18sIHh4Lw=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-runtime/2.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-runtime-2.3.1.pom": { + "sha1": "06991b67903c152737d17c12fa53610f1e3e049c", + "sha256": "sha256-tEQqhPw5ftsukIof33E8aufTqHZBoJ7hPRDsjuELMx8=" + }, + "lifecycle-runtime-2.3.1.aar": { + "sha1": "5786c819df8bcc1f050e9a752d2d0d85f87c4aed", + "sha256": "sha256-3SlPSmiccf+Hf9QfO2ejpi93YNRM5CDmEw8fw1adjwA=" + }, + "lifecycle-runtime-2.3.1.module": { + "sha1": "d459c7a0e1758ee24f5dae6386ed9d8b90483c4c", + "sha256": "sha256-KnuQ5QSbZ0s2vM/WhnezoLMXiz98Lvfd9hjTiVWYxM4=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-runtime/2.4.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-runtime-2.4.1.pom": { + "sha1": "816c5dfbed5c5c9d3eb82e0d81d146b15f41b65c", + "sha256": "sha256-R/vXbNQ37pWY2trrtRoQYlEMTMmE3/qGwZoCCv5oVh8=" + }, + "lifecycle-runtime-2.4.1.aar": { + "sha1": "5091ed6ca0f0274035de2d41621fe4c0b512d695", + "sha256": "sha256-uigymPx9PRcPVnbvFSpy48FWbVtGs+rdf1W5DDtU32A=" + }, + "lifecycle-runtime-2.4.1.module": { + "sha1": "06a9f671ae592337f625d86405f189ee55a3a81c", + "sha256": "sha256-JHmFOJrlTvQIkSt0OqglJih1xXMf5XaWfVgrvvatVtU=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-runtime/2.5.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-runtime-2.5.1.pom": { + "sha1": "82386f01f805f3fbf7ee22eeb6c62c2b0bf2e137", + "sha256": "sha256-ajkLp6wCyQBKCkVO/Cf5JZNswmUn6CvGLKr7T8sPDbI=" + }, + "lifecycle-runtime-2.5.1.aar": { + "sha1": "26e3d5564fe875473320ad0c15cab765099be8c2", + "sha256": "sha256-M7DXPcLwKPzrNZm6yr5WPD220m81E9iJWVhjU2pKyMA=" + }, + "lifecycle-runtime-2.5.1.module": { + "sha1": "82664f917f2b470277bf583274991e057afa3d72", + "sha256": "sha256-ACfciH+jskPCPbQKsPdj/H76yq+hXfkj0jM0Ss/oGhY=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel-savedstate/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-savedstate-1.0.0.pom": { + "sha1": "cc2f5146a3911e5bce103f06f7fccdb8632b62c0", + "sha256": "sha256-N6RjlfszT11UhD1YI/N1E8gEhdyubPFhiLq8SevmXhc=" + }, + "lifecycle-viewmodel-savedstate-1.0.0.aar": { + "sha1": "9a8d575935acee563f6ac9a069d5863abf59d198", + "sha256": "sha256-9Mzq+/hqz8fzumph1txoQqZzjBJ0YQdn06uPihFMupc=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel-savedstate/2.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-savedstate-2.2.0.pom": { + "sha1": "f296a3abc6e55f6da5323ffee00b084503ac8b97", + "sha256": "sha256-nzElTlR7IUJXBnYR8Y/Io8CPjfnrpjT24hutyEhZbbE=" + }, + "lifecycle-viewmodel-savedstate-2.2.0.aar": { + "sha1": "50d7a240847eb488a740939a6c19f689ee1a4a04", + "sha256": "sha256-POhm+4IrIP4vGI+XSZKGmgpiM/5ArL78/wkNbe9efzM=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel-savedstate/2.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-savedstate-2.3.1.pom": { + "sha1": "67c0adcbc0ca42eb784b7eeffb3f23cdcf7e8593", + "sha256": "sha256-IV5A7oT9r7Ke8liXexlrro+lpsejo0EUJ8eHsnHk9Fw=" + }, + "lifecycle-viewmodel-savedstate-2.3.1.aar": { + "sha1": "ef1df1036fc803ab3eaf0c298ee256e0b9fdad01", + "sha256": "sha256-lxN6ivajF3ahTkhmq4CMfAp5G0hL28eIu9g+ZkB1ZMA=" + }, + "lifecycle-viewmodel-savedstate-2.3.1.module": { + "sha1": "7b9b255844885273d694b2e3dee317dcc996add0", + "sha256": "sha256-gINxC3WKwJaJHpH1HZHuVqRFsmXXvs8jA1U3cyfAQYs=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel-savedstate/2.5.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-savedstate-2.5.1.pom": { + "sha1": "202abc723c1c99c5ef731cff79becac37cc068e6", + "sha256": "sha256-PXf6Ow4CbYa3I7vBNHQuIy6Pmc4YoR31t4lWLppsCm8=" + }, + "lifecycle-viewmodel-savedstate-2.5.1.aar": { + "sha1": "35860ecab7b49f43ab38d9afffbf37847192e156", + "sha256": "sha256-hIEUH5fw5iE90z/MiaeExL0Rpv99R3mhz2oOn72/JOA=" + }, + "lifecycle-viewmodel-savedstate-2.5.1.module": { + "sha1": "1a25f3b79cd01a918f837c1e1ca3871818411228", + "sha256": "sha256-KazV/mFLP4kSPrg49ojWJeqotCLI0ZBbSK2OdgzXrYs=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-2.0.0.pom": { + "sha1": "5ff80c640672babaf445983797eaefc3104bd1af", + "sha256": "sha256-YLdY/RxmQIn4LRwBjtT/eVXBSisWIK96oQZZH3M+CCc=" + }, + "lifecycle-viewmodel-2.0.0.aar": { + "sha1": "6417c576c458137456d996914c50591e7f4acc24", + "sha256": "sha256-1kYK6htrrYCrFM+IKX6eQ7/ejYfD5cKPLFCCM/+8wGI=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel/2.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-2.1.0-rc01.pom": { + "sha1": "de93bcc0e90b4d795415dc2435de27e51d38f440", + "sha256": "sha256-ZfUDwMFgpELJ62Uk2084YlcWZEtn1Fx+RbpNiGv6NMQ=" + }, + "lifecycle-viewmodel-2.1.0-rc01.aar": { + "sha1": "b55fd435828b78efe4866ed28bb922161597c7ac", + "sha256": "sha256-YzOBdmtwECh2Z59VxyhOPTICQ8FJ0QqdFxDptcZJO0c=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel/2.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-2.1.0.pom": { + "sha1": "b95d38b7236e5bb4249d74fdab5044c292158342", + "sha256": "sha256-Kapy4znD4ifnTJc6TIXHt5ySbw03th7ZT02ZoTfJpLQ=" + }, + "lifecycle-viewmodel-2.1.0.aar": { + "sha1": "682d06cc95e0632efdb9cfcc18828840ac941148", + "sha256": "sha256-ulX7esGygo1TJ82orPcIXZkLK0xD7zNsqmdoYkm4Uj0=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel/2.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-2.2.0.pom": { + "sha1": "5b85f9eedca6f1e45f62c6d3ca150cae66acf366", + "sha256": "sha256-qqOvqkn/R+K8myOJ+TnT6BN2FsRhZoTi37U7qDZqoR0=" + }, + "lifecycle-viewmodel-2.2.0.aar": { + "sha1": "695c5e9ee89ee73d69d0981303f138d7680acc01", + "sha256": "sha256-ln76sk1sSd1BSowKxKHNCbAY8Li7Q7c5rTYMQVjr3ic=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel/2.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-2.3.1.pom": { + "sha1": "319f1b60575b6ba81aa588084b903aece6b33aa5", + "sha256": "sha256-I/RerQuaA4OC2cOq+ctR9QvrzLY4q4PfnYQqO/CMQRo=" + }, + "lifecycle-viewmodel-2.3.1.aar": { + "sha1": "9b96f5276783647e1436154c7ae393b2a4e9c2c4", + "sha256": "sha256-tttMJ0oS/4WkdH4eZmnH6Yrvolcazp0fGm+mvkF86Dg=" + }, + "lifecycle-viewmodel-2.3.1.module": { + "sha1": "3304a3b6edee953359cde68b0f92dca43ff1769d", + "sha256": "sha256-pTGFPf4xbJC3Rm0kvpTb5gpg71SlLJBMhjgZhiAuUfQ=" + } + } + }, + + { + "path": "androidx/lifecycle/lifecycle-viewmodel/2.5.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lifecycle-viewmodel-2.5.1.pom": { + "sha1": "74072201fbebe52b6dba96083bdacdb4041f9099", + "sha256": "sha256-k+XPzLyf0vihCS7AawdXvYNOY9zSqAAbWUI5qx5KbeE=" + }, + "lifecycle-viewmodel-2.5.1.aar": { + "sha1": "81fe3e2d11ed12be941b71dbea223aa3e64c6903", + "sha256": "sha256-FKJ9X7ihQ2sbfewYvrqmbIMM27yOKKgc5fhbfDOzrp0=" + }, + "lifecycle-viewmodel-2.5.1.module": { + "sha1": "020a15b8ff54ccf158b51c54c5a5620fabce1dfd", + "sha256": "sha256-AeQTtzy+OMtxTcW9shvYYJMRJMjl8jaYA/SqzEkIHJ8=" + } + } + }, + + { + "path": "androidx/loader/loader/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "loader-1.0.0.pom": { + "sha1": "4b1418669a3392afe77045f85ba870f0c3148f67", + "sha256": "sha256-yXjVUICLR0NKpJpjFkEQpQtVsLzGFgqTouN9URDfjF4=" + }, + "loader-1.0.0.aar": { + "sha1": "8af8b6cec0da85c207d03e15840e0722cbc71e70", + "sha256": "sha256-Efc1yztVxFjUcL7Z4lJUN1tRi0sbrWkmeDpwJtsPUCU=" + } + } + }, + + { + "path": "androidx/loader/loader/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "loader-1.1.0.pom": { + "sha1": "f6977dbb6324d7cfbb78887f1acc10594f4ea560", + "sha256": "sha256-LHTswTvZO4wqSDQAV/P9CDuI/1HpFprnDWP5mNaGotw=" + }, + "loader-1.1.0.aar": { + "sha1": "49591a915750c6483e1791daad039e0af71cb91c", + "sha256": "sha256-qDtZfNJAncF39/ky98QILve1UItptsS6cWB+ddvZmXE=" + } + } + }, + + { + "path": "androidx/localbroadcastmanager/localbroadcastmanager/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "localbroadcastmanager-1.0.0.pom": { + "sha1": "afb1a15f5669db4cc43e19ec2e67e6fb77500234", + "sha256": "sha256-oAAEH1ofeSg8UXXhu2DPNoN4D0Acap00++l1ElP6b/k=" + }, + "localbroadcastmanager-1.0.0.aar": { + "sha1": "2734f31c8321e83ce6b60570d14777fc33cc2ece", + "sha256": "sha256-5xwyjO71xKfXby2G3xtl1l/irPhosaTv2Eo/NDNhhtg=" + } + } + }, + + { + "path": "androidx/media/media/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "media-1.0.0.pom": { + "sha1": "86e15c4ec92837de49486f1ad9827d689a6186d8", + "sha256": "sha256-nN/l94SQaa2E3E+wQHs2d/uMItPWb9K4/bjeyMXcdBE=" + }, + "media-1.0.0.aar": { + "sha1": "7f92bbaf670497a9a1105124122fb20cee61e58c", + "sha256": "sha256-sjtSeyushwxKdFHmmC1xMuQT6I1/J9vrH8dkCnIM2e4=" + } + } + }, + + { + "path": "androidx/multidex/multidex/2.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "multidex-2.0.1.pom": { + "sha1": "afb1726bf3ab700c7a2147342125a69812bf1c95", + "sha256": "sha256-DxC2PP1SktFmeAd723g2NEe4MoAZNsQB/CbmgpPyEG8=" + }, + "multidex-2.0.1.aar": { + "sha1": "c5c10c06f82ec4b4fcccc5ee4ea64ecc48254f33", + "sha256": "sha256-Qt0y/5+X+FdxuCogADqNcPaKt7S6MolkMSzgcyaT2wk=" + } + } + }, + + { + "path": "androidx/print/print/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "print-1.0.0.pom": { + "sha1": "f0031001c95427f3e6a85661f5c3560131ef8145", + "sha256": "sha256-YkgsBZSEG+4ku5lqu2y3syCmo7d9yp8KC6T+O+VTCqc=" + }, + "print-1.0.0.aar": { + "sha1": "7722094652c48ebe27acc94d74a55e759e4635ff", + "sha256": "sha256-HVx/MTWhu6Zh/Dc/1y4R6wpK27M5Z4eCbdjkGQ1dnt0=" + } + } + }, + + { + "path": "androidx/recyclerview/recyclerview/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "recyclerview-1.0.0.pom": { + "sha1": "f703fc85e864a92fb72e4bd5996b8024a824e20a", + "sha256": "sha256-iDHAZBf6tqlpDCX25LNHsujAPPbUcze+nFfnW3+yPmo=" + }, + "recyclerview-1.0.0.aar": { + "sha1": "ee3a2ad35bcfa91ab9eeebe991b0893ab40009cf", + "sha256": "sha256-BpVvsawBQCfKnStARppLQqphtJV7sRhI4f81JwGrRUg=" + } + } + }, + + { + "path": "androidx/recyclerview/recyclerview/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "recyclerview-1.1.0.pom": { + "sha1": "a058fa06eb843f4ef55d18eea26c0afb7b39213e", + "sha256": "sha256-/jIQYqbk4Wi5wrOaETdWTIeEouWElAJyl2jV18TZcu8=" + }, + "recyclerview-1.1.0.aar": { + "sha1": "8b4f97752ddcbc712af46d208aebd46585cde63b", + "sha256": "sha256-8NK1pn0Kke4bHHPvK2NqgfNWOSXd0VodThxB7Cjeek8=" + } + } + }, + + { + "path": "androidx/resourceinspection/resourceinspection-annotation/1.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "resourceinspection-annotation-1.0.1.pom": { + "sha1": "2c264bd97530601a68f3952fe040e2f7786ebc60", + "sha256": "sha256-uOtJOO2V1/zaonEuvKNj5GoPkii4vn8jFU3vVoRoFdw=" + }, + "resourceinspection-annotation-1.0.1.jar": { + "sha1": "8c21f8ff5d96d5d52c948707f7e4d6ca6773feef", + "sha256": "sha256-jP+HDsb7MdtIpS9KeSM1tL+N4H4DvTeCMYFSZDPM1cs=" + }, + "resourceinspection-annotation-1.0.1.module": { + "sha1": "75fe8bc237ef8047b526ba5ec28dff7d755b9b82", + "sha256": "sha256-NSoRqNikwb1s0sL+//nJTKlU17UgKgZWlZ25Upf2orc=" + } + } + }, + + { + "path": "androidx/savedstate/savedstate/1.0.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "savedstate-1.0.0-rc01.pom": { + "sha1": "7cff5957aca1ee17fc49b1d6601b8e3dccf67dde", + "sha256": "sha256-T75dgztaxblFTG0Yf7xHq5vvG7IySJjJ9i+Yxf7CB/g=" + }, + "savedstate-1.0.0-rc01.aar": { + "sha1": "8b8f9e98e359be8d0466c15a682ba246100e3edc", + "sha256": "sha256-LNUDum8NdiP+L/1Df+wZbF3uyOLhVwDg3kEQLKxmfVs=" + } + } + }, + + { + "path": "androidx/savedstate/savedstate/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "savedstate-1.0.0.pom": { + "sha1": "3efaae1f3b3dc6c389a390ef716ede40ec63ef06", + "sha256": "sha256-hE19IvzqeYx4v1VZIp2viOycrYE29e6gopqnaz+P3nw=" + }, + "savedstate-1.0.0.aar": { + "sha1": "6d39721808cd67e3d3d0d60f194907615fcaa69c", + "sha256": "sha256-JRClYZw3V5yc4aBFdPqvMjzQ/+L8TiD6j48B5btALoM=" + } + } + }, + + { + "path": "androidx/savedstate/savedstate/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "savedstate-1.1.0.pom": { + "sha1": "a1223b1e7d6c7a4a351376c3756eb44816346178", + "sha256": "sha256-SXhLdctJm7n4E32COocvDG2lym26fJrPLeOmg8t9ttw=" + }, + "savedstate-1.1.0.aar": { + "sha1": "8d4785f699f93e4020b185e2b7de37cb1f58753c", + "sha256": "sha256-1gu+RMLAgIOhfF3GeKbWtNCi1mSFgBarXAScvqkKY7c=" + }, + "savedstate-1.1.0.module": { + "sha1": "00de5d62f526744b999b521664ecff3ad474795a", + "sha256": "sha256-buorwVCCjI/Lp3fpMDcDji7j7EQcQ9as7PLFzZ3cU3Q=" + } + } + }, + + { + "path": "androidx/savedstate/savedstate/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "savedstate-1.2.0.pom": { + "sha1": "599cb28d21299cf6cb3adca50b4b354507766b69", + "sha256": "sha256-PhHAGf/mBTs7mHmiMQfW1vEFnpIzm6Uk4ZZ0cg+n3b4=" + }, + "savedstate-1.2.0.aar": { + "sha1": "bcaeef765339b976946cc9e891536713af651ebc", + "sha256": "sha256-LeUo1omOle8CDSLZ/9+dH3fL3ZP5LTnfql1cQ7DDEcg=" + }, + "savedstate-1.2.0.module": { + "sha1": "4e57bce9369ddbcca5bf31f930bb6cdf8d73e0e3", + "sha256": "sha256-QkfCMwiry10P+M2vff1YPzwqEBb2jRPxpBwhYAtvr9c=" + } + } + }, + + { + "path": "androidx/slidingpanelayout/slidingpanelayout/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "slidingpanelayout-1.0.0.pom": { + "sha1": "aa7576cffbc8658b758a4b5e0f80dfc44e7af8d4", + "sha256": "sha256-QOkPloOMKoFWq1GxgUAHZwSfOHzsjGleQS09kgWwdFs=" + }, + "slidingpanelayout-1.0.0.aar": { + "sha1": "37eba9ccbf09b75cc4aa78a5e182d5b8ba79ad6a", + "sha256": "sha256-dr/7fO+/eAeU2IFwAtrRVi8+J8Cp90bWJAHI7bMK7t4=" + } + } + }, + + { + "path": "androidx/startup/startup-runtime/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "startup-runtime-1.0.0.pom": { + "sha1": "c11c09082438d9d77d62299a73da939f95a4d02b", + "sha256": "sha256-GQtlQlHxEEUvZ/ahPXZuj+4IEfB+bwsPd9WJBiJqy6g=" + }, + "startup-runtime-1.0.0.aar": { + "sha1": "2b09d1715e2c98428ea7420deaf65b73f6ba8212", + "sha256": "sha256-/wgdLbfdKK7Fn3STTFFPuvSuWqxSWElf4Q1hKjYi+HY=" + }, + "startup-runtime-1.0.0.module": { + "sha1": "3c21be3ca39b8fa8a9f9c26b4502fc7f4b1c0744", + "sha256": "sha256-QO/8oNbuH94yvClol+VOu8xM9KopsMUxA2y9KoJKPCQ=" + } + } + }, + + { + "path": "androidx/startup/startup-runtime/1.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "startup-runtime-1.1.1.pom": { + "sha1": "1ee5722679a9f73d27bb12d0ed4711745d4345b9", + "sha256": "sha256-9BFLXGhZuxvDyvKBy21vJZmPp/cpLGTOrqdKkyEOdGs=" + }, + "startup-runtime-1.1.1.aar": { + "sha1": "9d4ba67056aff98b69eadc1477c19a1e67cb9470", + "sha256": "sha256-4KYymjcSYv5MRQNytw/a8zt2nvaRcJRyN4fPzolrHdM=" + }, + "startup-runtime-1.1.1.module": { + "sha1": "748e65734e8d257ca6c4ff7b57a97682dea6786b", + "sha256": "sha256-z9ls9kUMbitpdZiSRymtmgSVxaT89Ovufi+BsH5BWGU=" + } + } + }, + + { + "path": "androidx/swiperefreshlayout/swiperefreshlayout/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "swiperefreshlayout-1.0.0.pom": { + "sha1": "96828e94bba119dabf4bff993f79f2fa45351efa", + "sha256": "sha256-j+S12zMq8zzeiZulce6Gatl3vjgRTTq2AO2spFTqgpk=" + }, + "swiperefreshlayout-1.0.0.aar": { + "sha1": "4fd265b80a2b0fbeb062ab2bc4b1487521507762", + "sha256": "sha256-l2GzqAnJsJP9BqPEu8ZFdW3sDpW1ydpBm8nyo/MCbo0=" + } + } + }, + + { + "path": "androidx/swiperefreshlayout/swiperefreshlayout/1.1.0-alpha02", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "swiperefreshlayout-1.1.0-alpha02.pom": { + "sha1": "a46229bf11dbdf4d2952357d050b14b6d432c628", + "sha256": "sha256-KIMerCEZzxk6deSR+q5ulAQR9+O7VkNX0aHPPCCbRCo=" + }, + "swiperefreshlayout-1.1.0-alpha02.aar": { + "sha1": "5cb9e619bcf4b8d4c0fae0d8530abcaeee292110", + "sha256": "sha256-APgDY+p+iKI/PzgplKAwoNP0ic2ZMr2OQevGAG37AeY=" + } + } + }, + + { + "path": "androidx/swiperefreshlayout/swiperefreshlayout/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "swiperefreshlayout-1.1.0.pom": { + "sha1": "73f3b16b59f1a72ad4a71424490e2340d6acce19", + "sha256": "sha256-C4xVzmkQFVooO6mKbQlL5uDvXnCnNxzZ/p9xv4/9JK0=" + }, + "swiperefreshlayout-1.1.0.aar": { + "sha1": "2aeac64056fe6cc0420afad0b699a7f9257b8ebe", + "sha256": "sha256-LOeQbNHeoFrsgZddsi1UOCNZwFohslJ62Ei8YPaycpM=" + } + } + }, + + { + "path": "androidx/test/annotation/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotation-1.0.0.pom": { + "sha1": "dcf5ae6cf8a52294379ecbf2ed3656610b4e887b", + "sha256": "sha256-Yb8nIHfOIzyHx+2TC9uYCfzbL4vnZC3/rZzwKPevv1I=" + }, + "annotation-1.0.0.aar": { + "sha1": "5b8f891800db5da96485d08b42b1e435bc0c43da", + "sha256": "sha256-wHVJKO/+GWjDqae1XR38fOseHnyfPwn5iv1CQx9xJJI=" + } + } + }, + + { + "path": "androidx/test/core/1.4.1-alpha05", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-1.4.1-alpha05.pom": { + "sha1": "6f45599d898e26bcfb92d83597d0d766399c5be5", + "sha256": "sha256-JzKYzoXpkrUJeoRDPsdA7tTWaK8JzNAUHufBqiFSG3k=" + }, + "core-1.4.1-alpha05.aar": { + "sha1": "ff49d9bcc1d852804d73ac32969d78c1064342be", + "sha256": "sha256-O3ClI0rsm4hJnv2vCTJSK20ede4rxouhD12stGT7IX4=" + } + } + }, + + { + "path": "androidx/test/ext/junit/1.1.4-alpha05", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "junit-1.1.4-alpha05.pom": { + "sha1": "ab13fb7d61fae76c05b5a59f3247f84989b93104", + "sha256": "sha256-HV5UR8nU6M64cT90KJ3wLsclTtt3fUbo5vbZuEoWzYs=" + }, + "junit-1.1.4-alpha05.aar": { + "sha1": "52eb7c242ee816df049216f786c68e1872463a76", + "sha256": "sha256-ZKF16Y9qEbvWxGuUu2fTSCmduYXK/C3OToOXlfUrmg8=" + } + } + }, + + { + "path": "androidx/test/monitor/1.4.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "monitor-1.4.0.pom": { + "sha1": "22d313df94e46b3441206ad46ad657e2e8db447e", + "sha256": "sha256-0fZCM/KfDt4Ha2flcqKEICiog429q1kZPiv7pN3yav4=" + }, + "monitor-1.4.0.aar": { + "sha1": "4f3c15d0a1e0c943b233b0dc5d8f1e690cfe7c0a", + "sha256": "sha256-RqkSoeF18nqXUhrz9Q5a+HwixJJ13SxXwEN0ABKAYyU=" + } + } + }, + + { + "path": "androidx/test/monitor/1.6.0-alpha02", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "monitor-1.6.0-alpha02.pom": { + "sha1": "6dbc2e999150408518bea832a91fc54c8f13dd60", + "sha256": "sha256-5Kci/ByUPG0XXRwBnUvXf12lgYH+FFYzvCg0KZg7U2I=" + }, + "monitor-1.6.0-alpha02.aar": { + "sha1": "8b62ee338feab6fb680a8ea79241c0d0b63f27e9", + "sha256": "sha256-vUu8EiSkzs5156DgzVv2EqkgAHjU4UFTT2NNm/HPNHY=" + } + } + }, + + { + "path": "androidx/test/runner/1.5.0-alpha02", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "runner-1.5.0-alpha02.pom": { + "sha1": "630488868703605980675d6c5fa0be32864f90b5", + "sha256": "sha256-tcsxE3pOJgFatgPuVGca7tunyKqRcxfloorRQjGEC14=" + }, + "runner-1.5.0-alpha02.aar": { + "sha1": "40c986399afead7121233eff04546191e32340c4", + "sha256": "sha256-wKgbsxf2ZWfe3FVh//1ZNBfSzXx7W8mGXhbNViPr5Pk=" + } + } + }, + + { + "path": "androidx/test/services/storage/1.4.2-alpha02", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "storage-1.4.2-alpha02.pom": { + "sha1": "dbc64bc59ccfc059de59470f36bf9764b09aabaa", + "sha256": "sha256-rrjrFIOnrVrwsDK05OOn41nWpM3vKO8HIjGj3cSlTMs=" + }, + "storage-1.4.2-alpha02.aar": { + "sha1": "e056752f7bdbc3fe7cfd6d7d4fd9fc2f057b8a6a", + "sha256": "sha256-T1820afDVOgG5vxZKSIHvLIdViHJWiSUrBktTTLK4gw=" + } + } + }, + + { + "path": "androidx/tracing/tracing/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracing-1.0.0.pom": { + "sha1": "8699319903509b20b3ffa718d36b742fd11493c0", + "sha256": "sha256-zQKZqQ1HINePHPtf91BfTbwacNBf4j/Z9NS3fqWcoF4=" + }, + "tracing-1.0.0.aar": { + "sha1": "69cef34711dffbc517e7a0a419ec4a98f84f496a", + "sha256": "sha256-B7i2E5ZluIShYuzPl4kcpQ9/VoMSM78lForgT3tWhhI=" + }, + "tracing-1.0.0.module": { + "sha1": "a43d7ec71f20cf31d11f9aa264dc46d75a7ed450", + "sha256": "sha256-/Ish6+X6OnyW7gmLzc0A8HfrznPyQ/qFjisGcWFfddg=" + } + } + }, + + { + "path": "androidx/tracing/tracing/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracing-1.1.0.pom": { + "sha1": "f47db550fed0a0bbe853d1230422fef98a9bc546", + "sha256": "sha256-8dtQK11TnZTftsZZ3rdocrHp2CZteqQIOGiinDPCKRc=" + }, + "tracing-1.1.0.aar": { + "sha1": "cdc41b1335c3857e4136d399f6a7962663d05e5e", + "sha256": "sha256-W3jixhj8ELPRTezAHfdhWPFZVK10aqzwYHdmch2ggfY=" + }, + "tracing-1.1.0.module": { + "sha1": "f6a10a67bbf47708d53d84346fedb65f7a4cbb00", + "sha256": "sha256-sf7UMJYjtvILyBfY+9cOTqcIXkBkdpTNOZrljS8ASeM=" + } + } + }, + + { + "path": "androidx/transition/transition/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transition-1.0.0.pom": { + "sha1": "cba38d22419c84dcf737d666ae0c980f1e521e65", + "sha256": "sha256-50uKnjrJTi8wO6Sf/4Cx6oHSRaKx9OE+HDW/PaERaIs=" + }, + "transition-1.0.0.aar": { + "sha1": "f3556ce8f251984acb24014c3ba055ff235929ff", + "sha256": "sha256-oAoPdj9AGrzs2psOr8tziSnFgBsRGppBS4Ghk9D0AI0=" + } + } + }, + + { + "path": "androidx/transition/transition/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transition-1.1.0.pom": { + "sha1": "0c354a924354ceed91673412fb5b6ce326735a63", + "sha256": "sha256-XUyh6MYz+TYWhJeyUkTsqy930LTzTBWGaRP2EBlFSl0=" + }, + "transition-1.1.0.aar": { + "sha1": "ee403adc3ae340af6461cdd8151b4518c9f5c1b0", + "sha256": "sha256-vY5M/EKHUWWflb3Ox+SPgKufak/8cW8AzBo+j4bo83g=" + } + } + }, + + { + "path": "androidx/transition/transition/1.2.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transition-1.2.0-rc01.pom": { + "sha1": "7fe296c768d13fdd86a0dcd5e15083d5b6f5290f", + "sha256": "sha256-pF15rLfkPMw64yaP4XS6b87Q74JJmt/KLOsa5Sr5fb4=" + }, + "transition-1.2.0-rc01.aar": { + "sha1": "40bdbe5a245edd0393b7b0e1d9dd6c446dd9008d", + "sha256": "sha256-cuUfliAYf4cLmrGysoyglGboJRRWkHja8dPIey3+bro=" + } + } + }, + + { + "path": "androidx/transition/transition/1.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transition-1.2.0.pom": { + "sha1": "1ceda042cf551bdede6fdb82adeda0cb6a9b63d2", + "sha256": "sha256-JH8SdXrKvXBrBpRF/THIPAFsx/5nsEaMWC0EPqfSDeE=" + }, + "transition-1.2.0.aar": { + "sha1": "28003577e8b32f5417bfe899bbdc7a333dc81c9f", + "sha256": "sha256-oeBZs7wLQ6WN7A7+zcqonILSvKVS6lus9mVsRuhTFX4=" + } + } + }, + + { + "path": "androidx/vectordrawable/vectordrawable-animated/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "vectordrawable-animated-1.0.0.pom": { + "sha1": "539d391719865644a3003742c0b3537d9ed4fc73", + "sha256": "sha256-1kau6t61FjpflmzAjx3+pLeiwbkY75o5q3kNMvM31H4=" + }, + "vectordrawable-animated-1.0.0.aar": { + "sha1": "0a41681ac4e1747f87237e489699089ad46b7a5e", + "sha256": "sha256-JsOgzwqamn0jWgsA8vN+Qx1S2ZUnUePrfJC0tSwjbPE=" + } + } + }, + + { + "path": "androidx/vectordrawable/vectordrawable-animated/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "vectordrawable-animated-1.1.0-rc01.pom": { + "sha1": "0306fff46604ec21f43f2953c85351e82785cf77", + "sha256": "sha256-0tagLEu07RBQNJPqrYjdVk9bBZrpBoEvQCXHrfZTaZk=" + }, + "vectordrawable-animated-1.1.0-rc01.aar": { + "sha1": "d574d647c1305a96791edaf097f04fe4fc6db5f5", + "sha256": "sha256-AbIN8LjxCO0SGX3tDXqDscSUAREjv2SKlehumRne5vk=" + } + } + }, + + { + "path": "androidx/vectordrawable/vectordrawable-animated/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "vectordrawable-animated-1.1.0.pom": { + "sha1": "2c40e16427f4e94ea471526868ba5ac4bd13de6d", + "sha256": "sha256-J2ogEWtwX7dbkAPulJbFb2/TsyN1+yMkcoEeumCgQL0=" + }, + "vectordrawable-animated-1.1.0.aar": { + "sha1": "fcda1161354501471c30a4e077af6b5c4d4eddc6", + "sha256": "sha256-dtosUCNx2cOAVN9eKySNANqHgJ7QWPM2Pq6Hzl4kA/g=" + } + } + }, + + { + "path": "androidx/vectordrawable/vectordrawable/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "vectordrawable-1.0.0.pom": { + "sha1": "ba44dcbfcb2c76959be21748e8b1a1e99712df85", + "sha256": "sha256-17Zv6ihU3I/hYrzyS5/p1eC4t6QVFc4HOR6ymEE3yKo=" + }, + "vectordrawable-1.0.0.aar": { + "sha1": "eabb75f549c6a6ee4011e15adf04bf8ca33d3762", + "sha256": "sha256-UHWRkmWDh146DmxGJyI18yO0V0dZtcAvCrelHSrnMg0=" + } + } + }, + + { + "path": "androidx/vectordrawable/vectordrawable/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "vectordrawable-1.1.0-rc01.pom": { + "sha1": "c1ffad7b1e9fac67051c8d56c5e7db6a59fbfc50", + "sha256": "sha256-G3JkzQK7GUcoAF3gg3lLJjX1q+mIDeJCWREP0WL5q8I=" + }, + "vectordrawable-1.1.0-rc01.aar": { + "sha1": "b3612e242f283b5cc510b842997bf20daf7345ec", + "sha256": "sha256-0GIOI69Mm0G4nr9Jb4kuUFW0WmTqNVm7AnAdZohFQqk=" + } + } + }, + + { + "path": "androidx/vectordrawable/vectordrawable/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "vectordrawable-1.1.0.pom": { + "sha1": "8b15e03d94f7d9a07cbea653ad7f524315d1d507", + "sha256": "sha256-Ww4tWyF55UgEeFy8Ic5fRzteHd1VpX2kgulNzTlJK7I=" + }, + "vectordrawable-1.1.0.aar": { + "sha1": "eac7a364fff534035a2a6cb17770a1288315f69f", + "sha256": "sha256-Rv1jOsAbSbf8q8JjvwmMWouemml3TSNO3MoE+wLfjiY=" + } + } + }, + + { + "path": "androidx/versionedparcelable/versionedparcelable/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "versionedparcelable-1.0.0.pom": { + "sha1": "198bc6d7ad619b76e317edcec0fdc71343d387a6", + "sha256": "sha256-R+58VUMjmmUfj7gTEMre6quOOKLYRNe7kt0Ibk/7syA=" + }, + "versionedparcelable-1.0.0.aar": { + "sha1": "52718baf7e51ccba173b468a1034caba8140752e", + "sha256": "sha256-9kOKk+2AFszdyg4UCnC+CwEQ4EJO2qFHL4T5j+0uHOM=" + } + } + }, + + { + "path": "androidx/versionedparcelable/versionedparcelable/1.1.0-rc01", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "versionedparcelable-1.1.0-rc01.pom": { + "sha1": "ae43a9ce1dbd010f44ef03242c38dc1a597a442e", + "sha256": "sha256-POKLlo2x+0bnEArdVKN9MMIgHPqeKR3aSE3Nm3I8vbY=" + }, + "versionedparcelable-1.1.0-rc01.aar": { + "sha1": "89b237116bff8661335573b5786ae14c722a91ad", + "sha256": "sha256-lIx1H2NS1MD5PxX6G/UGxZCDvHdUJk3ZoyWm2g4u7AU=" + } + } + }, + + { + "path": "androidx/versionedparcelable/versionedparcelable/1.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "versionedparcelable-1.1.0.pom": { + "sha1": "5860ab66f74bffbfcfd51883ddabd477a0b80dc8", + "sha256": "sha256-xynHvgzAYyO9qCnUYGZuedvUO3maIQiaRL07KT3CU7U=" + }, + "versionedparcelable-1.1.0.aar": { + "sha1": "91acce73e00a17b524f6697f6c3893efb8ea349f", + "sha256": "sha256-mh13FArCIreGa1BU7n0Vm8GACYftLUbdav3RRau3EME=" + } + } + }, + + { + "path": "androidx/versionedparcelable/versionedparcelable/1.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "versionedparcelable-1.1.1.pom": { + "sha1": "301263729256e9f19b144ce00f83d600ba14c38b", + "sha256": "sha256-X1HmWHPKYS3jg4+pDS7pW40EDv0xucOQoZv5TWFc2y8=" + }, + "versionedparcelable-1.1.1.aar": { + "sha1": "18e18071e7715679e0593c570217edeb5ca48b9e", + "sha256": "sha256-V+jZMmDRjVuQB8nu08ZK0VnekMhgnr/HSjR8vVFFNaQ=" + } + } + }, + + { + "path": "androidx/viewpager2/viewpager2/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "viewpager2-1.0.0.pom": { + "sha1": "6c206c3aa4ba7917be9ccf200e11aa83caf01c50", + "sha256": "sha256-QGO8p/6U/mXJj0Fo+XrhDgLaAkhZitOsIcQyx/YIoXo=" + }, + "viewpager2-1.0.0.aar": { + "sha1": "91c378a09ddff66e1bb73e90edeac53487d2832b", + "sha256": "sha256-6VwAMdTMJHzUgZbGKH5Y0s7lTZx5uFr+p8kJIDMCda8=" + } + } + }, + + { + "path": "androidx/viewpager/viewpager/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "viewpager-1.0.0.pom": { + "sha1": "42452223b3d46c0e3bf2c67f226144ce64551be9", + "sha256": "sha256-H3L4NjOdA8brAT9lB152yocHWld1eOtPlfdKOl0lMSg=" + }, + "viewpager-1.0.0.aar": { + "sha1": "1f90e13820f96c2fb868f9674079a551678d68b2", + "sha256": "sha256-FHr04UoZhAENjxVeXhnXgfA8HXDf7QKo4NGEKLj8hoI=" + } + } + }, + + { + "path": "androidx/webkit/webkit/1.4.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "webkit-1.4.0.pom": { + "sha1": "c67e70da5ad98b587cc030a053e3eb45b57ae2a6", + "sha256": "sha256-y1UR2P7UutQVebx/Df6i1iWm/A7Tqa0NHqjXJsDptV0=" + }, + "webkit-1.4.0.aar": { + "sha1": "48e4cd9c63f7dc9fc62631d8dfbb77e890b459aa", + "sha256": "sha256-GhGyCqwTUt5y2lNmt2aCzoumubC+8Wp2uz30Gp24kZc=" + }, + "webkit-1.4.0.module": { + "sha1": "942c029f644b30d2589b9ef5b67e8e110b7e6497", + "sha256": "sha256-unCWUCSRzydkvnDbO28936yRIL26MBAj6KzrSZq+1rM=" + } + } + }, + + { + "path": "android/arch/core/common/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-1.0.0.pom": { + "sha1": "914227d3f5f7829f1677917b5438dd64984fac3f", + "sha256": "sha256-oqqr4Ut8iNxVzpsHuKe/9bxsbXKctAT1kb3T1rTXA14=" + }, + "common-1.0.0.jar": { + "sha1": "a2d487452376193fc8c103dd2b9bd5f2b1b44563", + "sha256": "sha256-UZKTTNc98y4sFXIu1/xIjd6Quq7JrgMAEN0agPtOdOE=" + } + } + }, + + { + "path": "android/arch/lifecycle/common/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-1.0.0.pom": { + "sha1": "c026e9d477378c67e6de0b5775a79672d68383de", + "sha256": "sha256-2C4k5ywVgxXOyfbvy3omhRZVvZh9cF/IFpn/ZxBwPuQ=" + }, + "common-1.0.0.jar": { + "sha1": "e414a4cb28434e25c4f6aa71426eb20cf4874ae9", + "sha256": "sha256-hr8wGiCtDNCjkeIqUub7+QV1wJb/gyM/qf0NUrMhkSE=" + } + } + }, + + { + "path": "android/arch/lifecycle/runtime/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "runtime-1.0.0.pom": { + "sha1": "1404cc3858b2271ca32a6f5259e8ea056d5f23b0", + "sha256": "sha256-M2klDWiTzeD+2gqXw8cPea2di4qC8IqGiZeHOxVMunE=" + }, + "runtime-1.0.0.aar": { + "sha1": "30c60a8a357ee1321ffd0c9f08ef54b24045cd10", + "sha256": "sha256-5ONOXQK9EC6NOd28Kfnq2KFaYeNnmT0CI4GWrEhQmtg=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-3.0.1.pom": { + "sha1": "270fb083c20514f1fe06795051dafeb48762678d", + "sha256": "sha256-i4cLPy7ji/FWgay8i2KN8cz3xTMZC1x5r5k0ts1m1FE=" + }, + "baseLibrary-3.0.1.jar": { + "sha1": "cc0483d84342e82c261aeb5fd0ef2f876b587489", + "sha256": "sha256-H4yQbQ3pjd+oGxzlIO5S+9KAwiI4O2bLqJ4W6vtiYAE=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-3.2.1.pom": { + "sha1": "6c8596d33ec70bc00aeb92c6740a74c627be9693", + "sha256": "sha256-cH7W9tYbcibyvM/sD/UR1J7fJDI15mm00X+MDwoY0z4=" + }, + "baseLibrary-3.2.1.jar": { + "sha1": "62174ecf31e2e3e4dd7a21cbd05399e11d0a9699", + "sha256": "sha256-5pk9Q6SOgOSXGUGfxTJDfGKAb7kER8NcKufBN2T4K58=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-3.5.3.pom": { + "sha1": "81372394c257edc4595cf19c33f7460892eeaa3e", + "sha256": "sha256-knlU7d5unzROdh7YkGliIzSrDWc3i9B2rDIkfhX/Bmk=" + }, + "baseLibrary-3.5.3.jar": { + "sha1": "5da8b7daa2f10ec359a13ee778eeabf15c1758b1", + "sha256": "sha256-zebU7vY+5eBbiqtY/4Xq85zD5u2E/RhL9lWNgvjZwGc=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-3.5.4.pom": { + "sha1": "86ef5c0145af0c31feca51140da31655c96d8d57", + "sha256": "sha256-Fefir74rPVxCFiDEmUl/yU9toIOZrujcmBIAXexf2lw=" + }, + "baseLibrary-3.5.4.jar": { + "sha1": "8d6e308beba7242cdae35cf4fd27e642d9c973ca", + "sha256": "sha256-IT6zkxCxDfXV0xhWAg0n8Fi/A725aaBYGC5xrLPVj+s=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-4.1.0.pom": { + "sha1": "bce3b5dbe634fc8351ca85381984501a63d5e2a3", + "sha256": "sha256-Xg71WojQd1bxr8F/resHe0leTb58lcuhsudAQisaIGs=" + }, + "baseLibrary-4.1.0.jar": { + "sha1": "2697e7689bbde7051db61cb6baeaf307975090db", + "sha256": "sha256-Jjkj/pb6FHgd1ifA9CBJjyeZrLre4MmGRu5l5vOLaXI=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-7.0.4.pom": { + "sha1": "46e8e214a070372087a270256705b0b5973a83df", + "sha256": "sha256-peVr0bveFk/AikahZTqAavdDLz0J/pvVwh9oITJjHk8=" + }, + "baseLibrary-7.0.4.jar": { + "sha1": "d4c47076828144a525b98cca1bdd3893f965bcd3", + "sha256": "sha256-rYDCyWpl5ZkKUtqmEPEuEziPqoQkyTsQwqrpzFwyYM0=" + }, + "baseLibrary-7.0.4.module": { + "sha1": "429b5bb473788d38b6efbeb935d5b21f6d69fd15", + "sha256": "sha256-ZXj+px2gKm/XNlcLM7ogd0y3IGvzk3ANbYnB/4hVTto=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-7.2.1.pom": { + "sha1": "f24d754aac61dd6be091a67d67ddfb75d5487f79", + "sha256": "sha256-ZAUKf8HcLEBYTpFlkhrOiqKnkG5I8xTbBHGDoQ7NQGU=" + }, + "baseLibrary-7.2.1.jar": { + "sha1": "64ab5b22730b44026db99a1403ceec3275468edd", + "sha256": "sha256-uZAFd9Ihl4RJJBTTYTA+7ksUlxrlLi16i9Z6M8Fjb5U=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-7.3.1.pom": { + "sha1": "688941edf0b614b398cb9c5e256464b96f93c6ba", + "sha256": "sha256-eCZTkVWkYhH/83jZqptvUJMuYosSxRWS+VmGJ3f6cP0=" + }, + "baseLibrary-7.3.1.jar": { + "sha1": "d8866960f7a5beb7cda6c6c53a770f00bbcf4496", + "sha256": "sha256-f/Y700yuAp5Hn8pSdRzGeuI+APKkxxwwIDlo8zcEqpY=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-7.4.2.pom": { + "sha1": "beedcb6ae17f8e29770579e5d9cd55d909acb4fa", + "sha256": "sha256-abaQ0vOWv7mqc4G3sx6wrHxA1LyrIG/mxroEqEp6AdE=" + }, + "baseLibrary-7.4.2.jar": { + "sha1": "816af75743910bbee1f2a4e0620683bc3ec6e453", + "sha256": "sha256-UwshEzF/9ND2n/37STh7pLhqrBaeHHff+UNAW3mtz4s=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-8.1.1.pom": { + "sha1": "a81104ced74bdd21e64d8179330ce6379020e22d", + "sha256": "sha256-eeLM2bUSictusz0SeRSzR+t8c4gAeaXZ3s+wYCXo29g=" + }, + "baseLibrary-8.1.1.jar": { + "sha1": "7e7399239e52d4069896277615e005add7ae8e5a", + "sha256": "sha256-eUETcJ2rIbBsJis3lec8twj7rK5hcV80Nh4a9iN6GHA=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "baseLibrary-8.2.2.pom": { + "sha1": "3bbd129e030a44eee7f18bbcbd468d46f8ab1bbc", + "sha256": "sha256-TysbW3qU1f8Ey+8LG602luXtTnGdCgtAjPaBJJO8AYc=" + }, + "baseLibrary-8.2.2.jar": { + "sha1": "7e7399239e52d4069896277615e005add7ae8e5a", + "sha256": "sha256-eUETcJ2rIbBsJis3lec8twj7rK5hcV80Nh4a9iN6GHA=" + } + } + }, + + { + "path": "com/android/databinding/compilerCommon/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "compilerCommon-3.0.1.pom": { + "sha1": "d312442e8d1fe2271e45bce271150005173a98a0", + "sha256": "sha256-yaVUY5QhKPC3+tYri+5YhtHLHHM/s7eFB0ZbAl5qQDE=" + }, + "compilerCommon-3.0.1.jar": { + "sha1": "7f51b3e1636063e491131a0b0436512131cb1bdc", + "sha256": "sha256-6CSTXCZLBB6Ee2eAiu5wT4zUfWmCAyWQV+QvRz4fbzA=" + } + } + }, + + { + "path": "com/android/signflinger/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "signflinger-4.1.0.pom": { + "sha1": "cfeea815292e0e15a56371c58cb3e06c2504d63c", + "sha256": "sha256-wHmgrfsSVizwQv+PAyO8offPjjoVfNiw1TpoBUKO/HM=" + }, + "signflinger-4.1.0.jar": { + "sha1": "8f9580b81acbbf2978bb7bba45c30eb1ea552b88", + "sha256": "sha256-7EokjR5byrNlnoScali/wWt0bPVMh64QNExHGsQwfT8=" + } + } + }, + + { + "path": "com/android/signflinger/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "signflinger-7.0.4.pom": { + "sha1": "39d84d96c507bf39f1aef241d7ce5939813031a5", + "sha256": "sha256-kCA8ewZB52YL+BRNWQRJeMqvV3pcfLJpaH92+9gWUiY=" + }, + "signflinger-7.0.4.jar": { + "sha1": "b45f0abc92a5c66a4543077491793e116f6d8fa9", + "sha256": "sha256-y9dSE5RWSWn7BBRtqF6u4SU5TUel4SvVqBRdvP2RpX0=" + }, + "signflinger-7.0.4.module": { + "sha1": "c870140211ac7fda03486e31968a089fdca1dddb", + "sha256": "sha256-kI7sr7SPoFah/LYtpO5BMHc7tUpInC04sxpKCcNVo3Q=" + } + } + }, + + { + "path": "com/android/signflinger/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "signflinger-7.2.1.pom": { + "sha1": "43aba4a4462674d941ed35f2bb1afce2292c322c", + "sha256": "sha256-sAetVxmMoQK72INMOPXLAcPegdjoSADR6LLfo6BprJE=" + }, + "signflinger-7.2.1.jar": { + "sha1": "92b0db3c63249ae6ba21f9543b34e7e6a76c92a3", + "sha256": "sha256-H8E5eXa2kKnaDysswDePwwuOOJ+rZYB0/mvOqouXGuc=" + } + } + }, + + { + "path": "com/android/signflinger/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "signflinger-7.3.1.pom": { + "sha1": "9e07706173dded4743cc16183c840cbd5066b9d4", + "sha256": "sha256-nulFeTmNHxKBdyrmfk01pc6chvwAG4yX47yC9EKkJmA=" + }, + "signflinger-7.3.1.jar": { + "sha1": "d8982f3c1b697c0b6b03340152cb9711bccd6de5", + "sha256": "sha256-QjLkw8h6BmkLfipJWPt0lfy3Xuo59l62VzN32FLVemM=" + } + } + }, + + { + "path": "com/android/signflinger/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "signflinger-7.4.2.pom": { + "sha1": "e8b88d6aa2207f8349a9064a6b38dd6e0b208d4a", + "sha256": "sha256-/JSR+Iixjbn6z8RUb/oz4tl7AGWQQ7wE5GmnomRKmTs=" + }, + "signflinger-7.4.2.jar": { + "sha1": "eadca001587305c0d2498104183497eaa7013ce2", + "sha256": "sha256-HyXz+cXXBv17wTOkW/NVEQkxl9VAY1phPRquESYOMUo=" + } + } + }, + + { + "path": "com/android/signflinger/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "signflinger-8.1.1.pom": { + "sha1": "a9a20f5790234d6e404dc6646a4b68a3a1541baf", + "sha256": "sha256-XfZCBs3vlMUbvTKOl30F32KIXnt+VRI0aBFQNNdVYOQ=" + }, + "signflinger-8.1.1.jar": { + "sha1": "13f035dd8e7e470ff8f3216a0b46333187f26df5", + "sha256": "sha256-wdyixoNjTuGilCmPnHF5V4r2qG4IC9xA+WGRW8XIFC8=" + } + } + }, + + { + "path": "com/android/signflinger/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "signflinger-8.2.2.pom": { + "sha1": "190adf35117b6260f2fc480581c9230cb01f6bd5", + "sha256": "sha256-RRbL7uoiqX2kSli0O+dltGohQ2mLHmkEurHg6ZZIGNI=" + }, + "signflinger-8.2.2.jar": { + "sha1": "13f035dd8e7e470ff8f3216a0b46333187f26df5", + "sha256": "sha256-wdyixoNjTuGilCmPnHF5V4r2qG4IC9xA+WGRW8XIFC8=" + } + } + }, + + { + "path": "com/android/support/animated-vector-drawable/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "animated-vector-drawable-26.0.2.pom": { + "sha1": "b45042835d6565fef6bb45395618ada3c068f082", + "sha256": "sha256-HmnBarTOTeyGU+JlAOEWUm89N9iAVXBIgtPd1weCk2Q=" + }, + "animated-vector-drawable-26.0.2.aar": { + "sha1": "b9b56e503d08c4246a30f281e3fe8511c6cfb430", + "sha256": "sha256-KnqP5fcb5vlnYqHwBq4O/MJAj1jnJ52xEDQ4IREH7FY=" + } + } + }, + + { + "path": "com/android/support/animated-vector-drawable/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "animated-vector-drawable-27.0.1.pom": { + "sha1": "aa9b6a87ba80a1a9c54f352e1f2330445c9279e8", + "sha256": "sha256-Fb5YLa72BPF5S62BAYfjH10p7gKIa7g6pGskEfBOlWQ=" + }, + "animated-vector-drawable-27.0.1.aar": { + "sha1": "09da101c8b10d3f54997b3af65943d4f3256da79", + "sha256": "sha256-NlBQEQQRyGx+7IYQG0mrU1V//mZn9gsZBV8dNcOKV3s=" + } + } + }, + + { + "path": "com/android/support/appcompat-v7/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-v7-26.0.2.pom": { + "sha1": "58184c7ab6bff1fbc4957f15feff8bf95fc9fafb", + "sha256": "sha256-azscV3XAXxM9Top7pvUw9ugh/06NFFlxsKUevS8PmWk=" + }, + "appcompat-v7-26.0.2.aar": { + "sha1": "2950c96bbe00b54626a0b00439e9b1f767518ede", + "sha256": "sha256-wZ7YF5QuJHLdQrg8g+dAmP88HsM6WZf0QiGCsZAME2A=" + } + } + }, + + { + "path": "com/android/support/appcompat-v7/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "appcompat-v7-27.0.1.pom": { + "sha1": "0b082299e66b082b025f93ca37aa55fbeea4408e", + "sha256": "sha256-2EcNerHHAhQJvYGMROAV50yJimA6R7bWZp8CN4Dg64Q=" + }, + "appcompat-v7-27.0.1.aar": { + "sha1": "2fc21de3a59a1183bead10473a551e43c6af8cef", + "sha256": "sha256-FALCmknbMDRsIafUBjRGF2Wzq4JvXdlbxNzHZ4eyGFE=" + } + } + }, + + { + "path": "com/android/support/recyclerview-v7/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "recyclerview-v7-27.0.1.pom": { + "sha1": "d1e9affc536f1f7adfbd9f00f12448b4b3d03931", + "sha256": "sha256-DemZ2OF+QZ18xe6/tsvzOd2vuEjXo/H+VeX6sZYhhQM=" + }, + "recyclerview-v7-27.0.1.aar": { + "sha1": "a8eb866818e214fe71244b31ffea07dbeac3fc2a", + "sha256": "sha256-ma4oYyh9+ADyOWF9Ccr8WLlfelNl2zLjdhOU+gGrEsk=" + } + } + }, + + { + "path": "com/android/support/support-annotations/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-annotations-26.0.2.pom": { + "sha1": "064ba98589b894a0d04f2b112061befbc77e03fa", + "sha256": "sha256-xfSZ72QKt785GzJiIsPeUwpQxXPH7DBGQ8qFTEGnZLE=" + }, + "support-annotations-26.0.2.jar": { + "sha1": "8b68a849722b44f584e2d68c451c5e3844c10380", + "sha256": "sha256-MvIg7hxL/T2rmVVIvho1mJk6YelFGW0EqlDOxwL6OOQ=" + } + } + }, + + { + "path": "com/android/support/support-annotations/26.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-annotations-26.1.0.pom": { + "sha1": "649798a9fc69aa94d4f31b528d81c05f22e44678", + "sha256": "sha256-Pq5y/PAqF176E3eIbmUA5hR/khBP9D7pK5tp1ZNXuQ0=" + }, + "support-annotations-26.1.0.jar": { + "sha1": "0814258103cf26a15fcc26ecce35f5b7d24b73f8", + "sha256": "sha256-mdYZmtWgmg5eikmkzAj4GEg93P1+7eovmSNBLa+YIwk=" + } + } + }, + + { + "path": "com/android/support/support-annotations/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-annotations-27.0.1.pom": { + "sha1": "ed47a79d643e78d1445913d22d5d1d289e08d6c8", + "sha256": "sha256-qkAkHEC+JE5HXQCBaw6GDX4RHqgiaWoxfTaZtdhvn18=" + }, + "support-annotations-27.0.1.jar": { + "sha1": "287742f1c6cea6d9126670e9f031890b0462362a", + "sha256": "sha256-JAkSt3n+dBO92XyUrCqS0fspvVPE1QxpguWUKwEnisA=" + } + } + }, + + { + "path": "com/android/support/support-compat/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-compat-26.0.2.pom": { + "sha1": "cf3024436b9ebdabe6e7c3799967a7d9a2eac6cf", + "sha256": "sha256-4Z3Tuj8zim9xwcfcpr3gaonTr/hmZqR+TVzxewSH+IM=" + }, + "support-compat-26.0.2.aar": { + "sha1": "342fa9af0dbea4a4d1d92fabd6fcf55a00528db5", + "sha256": "sha256-zPuvraFhE2DJOL5I+kTs21PnAHRdKf2Nl9ZlbLD9pCU=" + } + } + }, + + { + "path": "com/android/support/support-compat/26.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-compat-26.1.0.pom": { + "sha1": "fdb7266853fa71155bab2e696e4ace2f44f8dfc3", + "sha256": "sha256-JuJZgqA4JjPJoq1S2kg79E0GvtcuCbmM3OYeB4DruSg=" + }, + "support-compat-26.1.0.aar": { + "sha1": "1e1c8ffc01d277d8f01dfd11d5d2ce3a2af4b98c", + "sha256": "sha256-fW2gHPl2axcFxsgM/BInSolbQGxMKHkAsHpWFFymwDA=" + } + } + }, + + { + "path": "com/android/support/support-compat/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-compat-27.0.1.pom": { + "sha1": "05bcca92e852f205f2b64dfa04280c108164999d", + "sha256": "sha256-fybTCmhBsynN4Zr3WLDj8VQw+G3h3GH99htt1b0n1Z0=" + }, + "support-compat-27.0.1.aar": { + "sha1": "df3dabe358f5f29b02cd3eb182f0094fd43c7d53", + "sha256": "sha256-cN/ZJWFCHlrhE9/EBF/JTJDKypkSg5s9CMeTqjka7cw=" + } + } + }, + + { + "path": "com/android/support/support-core-ui/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-core-ui-26.0.2.pom": { + "sha1": "5aa07c751f9593c5fe7094ed941b29b46d9a9031", + "sha256": "sha256-tE9pScXlxn9cfzEPy2fv2jv2QqZysrJkuLZOGsYLP9I=" + }, + "support-core-ui-26.0.2.aar": { + "sha1": "0c55b19b6a499c4a78227ebc8390475d594e08d0", + "sha256": "sha256-evatPgaiIxkAHfk0vPp9+50n/i6WnmF9c9m1Oeye/WM=" + } + } + }, + + { + "path": "com/android/support/support-core-ui/26.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-core-ui-26.1.0.pom": { + "sha1": "3b5b814f4fd0cf809db6922009daa2842009c84e", + "sha256": "sha256-fDzRWBtXPTyIeX9NSDEtFx5uJIgfBkJx8Z0+8cm0gNA=" + }, + "support-core-ui-26.1.0.aar": { + "sha1": "e306308d54052a1ded7bf9c2f5c2fdf5152a1f22", + "sha256": "sha256-gvU4BRWZM16ogewmRAdUfKtSvnUPFs4JnPsndU/HVf8=" + } + } + }, + + { + "path": "com/android/support/support-core-ui/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-core-ui-27.0.1.pom": { + "sha1": "3005122800b90643d9c400f07fb96feb5d96f021", + "sha256": "sha256-dhELM5L1R39i29Bz1Aqs3bOZH58YX2aItW6EAXZ5lwk=" + }, + "support-core-ui-27.0.1.aar": { + "sha1": "5b078bf46dc9a209977ecbc805e26378c70da96d", + "sha256": "sha256-kWFPgp+Kbx/C0o78WOonNSEiJasCzfdos92a9yNx1fA=" + } + } + }, + + { + "path": "com/android/support/support-core-utils/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-core-utils-26.0.2.pom": { + "sha1": "b1d45292e499eea2ccea51e7caf204fd878eca88", + "sha256": "sha256-CQBFZW0o9muiju9bnUChCUYLrk8blmoX9rOw/oS40IM=" + }, + "support-core-utils-26.0.2.aar": { + "sha1": "641bb56a309f50fe37ca6c5e48e7665f739e7c19", + "sha256": "sha256-Ur3CqC028z9VZcabUqbj16WDTwOcmsAbx2iuMikIdOA=" + } + } + }, + + { + "path": "com/android/support/support-core-utils/26.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-core-utils-26.1.0.pom": { + "sha1": "04751a3bb1a065130c1618549318c9a8b0328ab2", + "sha256": "sha256-qCS376n25mN4450+t5uIV85pVWY+QYbKayrYUu6mR0M=" + }, + "support-core-utils-26.1.0.aar": { + "sha1": "1bfaae21c4d5c5532c5e732071e9ce234cd58cff", + "sha256": "sha256-T9ptTrQwlx47Ha10VpiDM/N0sPS6FfmYOcoaCrUVXIo=" + } + } + }, + + { + "path": "com/android/support/support-core-utils/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-core-utils-27.0.1.pom": { + "sha1": "9787db3f91ff0ef98c6ed255e06d722c5c56d877", + "sha256": "sha256-K5SP2nkTh7Id9sSRk5XzsuOsooOlzUz5gPG9kCS2fDI=" + }, + "support-core-utils-27.0.1.aar": { + "sha1": "810bdd1d47d61e537bba0c1252f92e5f564b1edc", + "sha256": "sha256-szxaXWe4v6bIgph1Zhd5J5FXvyPmBCF5gg6vF0UDSnI=" + } + } + }, + + { + "path": "com/android/support/support-fragment/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-fragment-26.0.2.pom": { + "sha1": "af31d1e645360844fe83db8a0927b3a7f8f4edda", + "sha256": "sha256-qkmt+c8HX8AtD2j26QaNFHgbC3VW3oClDFjAUXwclKY=" + }, + "support-fragment-26.0.2.aar": { + "sha1": "26cf0203b3dbb19862c462b7a711b759580a3393", + "sha256": "sha256-YhfnRN1XUNCTeOZS8Aq/GeDSIMUREZAfMijuyBVTitk=" + } + } + }, + + { + "path": "com/android/support/support-fragment/26.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-fragment-26.1.0.pom": { + "sha1": "3db6198de6ef0c0178e33fc53a6d873d30d53b79", + "sha256": "sha256-AnfqNYKSslchxe265mxqC1V7JUl4d9Gq5fLKwr9rPmo=" + }, + "support-fragment-26.1.0.aar": { + "sha1": "ddacf369bb98dd6558606558de8ddcd53895cf91", + "sha256": "sha256-oKszae9A/hmRYGkvBGOl9j8Sd+v7ZN1YfHb9sSjXazI=" + } + } + }, + + { + "path": "com/android/support/support-fragment/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-fragment-27.0.1.pom": { + "sha1": "b9266f3eecaf9e8c0c02b8f2607bbd79832a6429", + "sha256": "sha256-/kFy5CNItPU/tA9+MCmTIHMTtIR+6In5QPjTKuJv2wE=" + }, + "support-fragment-27.0.1.aar": { + "sha1": "6bdb260a2a656c2ed593dcf841f1518890ee4b7a", + "sha256": "sha256-BahZeDrkooXG1cmGXhJCuG9b+2mjBFXKbISV8kwJ2e0=" + } + } + }, + + { + "path": "com/android/support/support-media-compat/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-media-compat-26.0.2.pom": { + "sha1": "f65317b491650d9e2e7af18aad837bd54a65778e", + "sha256": "sha256-40jP0P7BfFTvuf5PW3ul3D4SnYCUzlhC8i5Dw0auph0=" + }, + "support-media-compat-26.0.2.aar": { + "sha1": "c7912532b36588f54933bc03b109e5b5f24e0b0d", + "sha256": "sha256-cLVgHuG4r/ruLTnzwIPk75wMgkpn5Nbl09ki/F6j/bE=" + } + } + }, + + { + "path": "com/android/support/support-media-compat/26.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-media-compat-26.1.0.pom": { + "sha1": "f1b57b8b374dcd9878853973d6bb030f2dea610f", + "sha256": "sha256-7q/AsvX/zpz9hI9Pf2apJ9QCuoOWGxLwBFf894BurI8=" + }, + "support-media-compat-26.1.0.aar": { + "sha1": "09fb587f27cde19aa8f2e50c5c9ee645d9aec44d", + "sha256": "sha256-nYzufNQO/yLr3rkMjnD17pbFvSXLLD47OUDicoWj6Yo=" + } + } + }, + + { + "path": "com/android/support/support-v4/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-v4-26.0.2.pom": { + "sha1": "f3417d10effa8d6bafae4e78a7fd09b16067f699", + "sha256": "sha256-3XE/of96+sOXVih9itpjzCVEqixFiF0otqwKgP0lklU=" + }, + "support-v4-26.0.2.aar": { + "sha1": "762902b1aa5eb48bdb1a1f84b1be19a7b1285708", + "sha256": "sha256-uX1rm5dLJDcj1WgwI3ZJx5Ek/Z5JY5bnQPXGwTMULE8=" + } + } + }, + + { + "path": "com/android/support/support-v4/26.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-v4-26.1.0.pom": { + "sha1": "2560ebaad416fb32fc979550b83f47f1e1ad58e4", + "sha256": "sha256-by6/2qVXH/YUsALaP6yPGM+Yed1MXqgUS1X4f85jwzg=" + }, + "support-v4-26.1.0.aar": { + "sha1": "444114b772e5eee3e66f9236ace4acc1964a33b8", + "sha256": "sha256-Ntg4XeG+d5EjGsuTO3VxmPl8tTvH0EboxLxAPSFMqso=" + } + } + }, + + { + "path": "com/android/support/support-vector-drawable/26.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-vector-drawable-26.0.2.pom": { + "sha1": "c55de7d06fcd692a786f190056dfca8b394ba56c", + "sha256": "sha256-VAyj2GU4jJh1F/k4MmIfvhVb2QpIVlyvMsVOVPABsQg=" + }, + "support-vector-drawable-26.0.2.aar": { + "sha1": "89ebac92d36ee264233136fb473d4f61d17d570d", + "sha256": "sha256-tPV5PxrJpp+d3JenY/tN/4vBpRIlG2y2zX2Svmzz3RQ=" + } + } + }, + + { + "path": "com/android/support/support-vector-drawable/27.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "support-vector-drawable-27.0.1.pom": { + "sha1": "70faf3e360e770cfb5adea12a901ac671322c7bc", + "sha256": "sha256-tRkybMJvqPmSJKvHYThSEVMYtUeNRfSuff2w3wzo6eY=" + }, + "support-vector-drawable-27.0.1.aar": { + "sha1": "ad85b9cd7f3a8095e9d011f7467ba7f47da79724", + "sha256": "sha256-ZxVOoZH3NUllAiFLxtphlXOIKajp1Z6AQqHgMKLH8Zw=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-26.2.1.pom": { + "sha1": "e74c00c94a05aca89f4e1515718e886f47e87a9b", + "sha256": "sha256-n3PXkFX8B7hlCQthJpP4fd+eT1PXD4xL8BsE3F6Ufls=" + }, + "crash-26.2.1.jar": { + "sha1": "ca7e89eebc7c27af8f7aea2ec070873fcd90602a", + "sha256": "sha256-E9NiyGTOi14rZYOJouC/y9J8n2wL3oIHc4ch1z8BZb0=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-26.5.3.pom": { + "sha1": "2b6f117c8542a9132bbd1982960ff93be002eb77", + "sha256": "sha256-ZrYgjqqWCNZ7K84UQIlBptcRC4ePlCVTmsIeuEWnqBU=" + }, + "crash-26.5.3.jar": { + "sha1": "d199e964be1fa3e7162344b91336afae6c7ea789", + "sha256": "sha256-86x29qjj+M7mPiqew2n87T3przHhRQzZOdiHXWcT758=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-26.5.4.pom": { + "sha1": "ebe45156d5b63fd80ab2a31fefd9af18ef243cd6", + "sha256": "sha256-AkXJpkCEHleGadczsGucCAy3vzQvtC1Z0ARxS6kRq40=" + }, + "crash-26.5.4.jar": { + "sha1": "04020841b8009414df8663161527d035528686e2", + "sha256": "sha256-u5yJn6vcHSz6KO/OTM9HAyDBMAQ2jagJS152n5qXXn8=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-27.1.0.pom": { + "sha1": "72942829fdd039ec7fa3a0abdc25df513dcd0348", + "sha256": "sha256-ZNPDPNV0vQrjNP7fgxDPul/8Noc+Z+KhDvyyasf8t2M=" + }, + "crash-27.1.0.jar": { + "sha1": "2eaed8e603076e9d6d2e0ddfccc2260c6bce7abc", + "sha256": "sha256-RaHd/CUtgCFUZlSp3eLLwg4rMOG9hO+tZH4BZWCpeuE=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-30.0.4.pom": { + "sha1": "3935b22db02850255d60e543d191b997ce70b27e", + "sha256": "sha256-LegdwtGzNlfTm0e5GM/akGq6tg0SwUxBIcmFxvXFVcU=" + }, + "crash-30.0.4.jar": { + "sha1": "22789a36994b27935c2bc294262488626ff37496", + "sha256": "sha256-Kt8G4rshr6qKHeoYftk+WZ2YKuBcNpyW+JbOSVxrpxw=" + }, + "crash-30.0.4.module": { + "sha1": "f110e6755601cb60ff08e2fb0872ddd63bf301cd", + "sha256": "sha256-z/s8mHRng1KA0C9FiDh3/OzRTTKmJfGChX88NHko4F8=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-30.2.1.pom": { + "sha1": "f5082b4ddeb7a688ca75ff999b0ed0a280cf2a33", + "sha256": "sha256-kHGYg4uvVtJQTQPy5GR3yh0yTUj7Gs+QzXomi+hfrxU=" + }, + "crash-30.2.1.jar": { + "sha1": "cc206cb0ead9f1a882d29aeb070437c5bf5eed63", + "sha256": "sha256-baLqK1OCRoRlYNi/HTJRcEDCl+3voNz9witKmpVPMe0=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-30.3.1.pom": { + "sha1": "b8d0288bc3bfa668c942cb2eab3d625b6451bbdd", + "sha256": "sha256-eardWzaaVxNCKDwtHkJfzOjMZHdGGTTWH47DKcITE9s=" + }, + "crash-30.3.1.jar": { + "sha1": "528046bc127bdb59fec52d3ca3997bee71151bcc", + "sha256": "sha256-yQRITEGA7zdqfwzsXXpXtseF8ZnxW3ay9aKoWg3oHSA=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-30.4.2.pom": { + "sha1": "cf31442dee2648daf81657e8fd3ca14eb6838912", + "sha256": "sha256-+AIsbkItXR5+ueAv7qTPYie0fVSa3cKvxVyX48O64vw=" + }, + "crash-30.4.2.jar": { + "sha1": "cb084b6a8d0e52c4cd9b05362010674bd732875c", + "sha256": "sha256-qgUpP/bvyg8Wh8+qLesgGYQBYyC4vTY2p+Uau/kPLF0=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-31.1.1.pom": { + "sha1": "eeee0d6e43b21baa711c6ccf683715ede95e29b2", + "sha256": "sha256-tspKk+lK2W89k8UZ6eVWNVA2XHoYl05rp9EJq9TIBYc=" + }, + "crash-31.1.1.jar": { + "sha1": "745eed5bd3b46fecd9a285aaf033e64b570b1d68", + "sha256": "sha256-zF8CisT9A31XwLsM3ITKN2cYUuojcCFrXOb0VHnwMmE=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/crash/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "crash-31.2.2.pom": { + "sha1": "7540f15fff234133b962e64b2f857048b0e26b44", + "sha256": "sha256-hnsYWcNu9fKzEkcDVDmmqEvQ3JnGKxTSCAUzu212mGw=" + }, + "crash-31.2.2.jar": { + "sha1": "61d28c3941a95d40b9571d3efcd31b3404e7ffd3", + "sha256": "sha256-1b55v6kDEdOd12dRZTmvoSL51AvB5L2Px24HuBUTefM=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-26.0.1.pom": { + "sha1": "cf5b7d86e8a5c471cdb6c6615f6e26589b6b3e63", + "sha256": "sha256-jUAGF4/t70JVeXuAYWfPSmd0FY7tq/lkIGk6+70A/uM=" + }, + "protos-26.0.1.jar": { + "sha1": "07719e7ed7498e25ec555bdfcb7a4de0e88a47ea", + "sha256": "sha256-319eS20t3N5J7VpOfjvyy3+H4NmJLlsGb5lcTXA7Io8=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-26.2.1.pom": { + "sha1": "5ba0bf99a124c6370336fa345b871f05e610bf64", + "sha256": "sha256-x3WKfKOh0Dsm/k7xRTL0/76Dx/KO48kUaYZXP8i5Lj4=" + }, + "protos-26.2.1.jar": { + "sha1": "db637f20d2c56d74a1b504d4e39a0bc8817fcaf4", + "sha256": "sha256-LzcfWx9VHoWrCL5NaihzRxs9RK/R6/aqMpjzt5a/aR8=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-26.5.3.pom": { + "sha1": "a2452eb3233ef51f1dda9b5508cfe340134b4978", + "sha256": "sha256-FYV7IPvj+TmJoQA2xD32xOTygrtjh07jAghoOH7gRuY=" + }, + "protos-26.5.3.jar": { + "sha1": "16d70f0d597eac5d39180b1ce2d9b274f8802cf9", + "sha256": "sha256-8uut8euJZzFc/sTTw3UkQFSh8W15+N4l0eiYPVq0D70=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-26.5.4.pom": { + "sha1": "448847b74ed14339ce2f4a225b38e71df5a640a8", + "sha256": "sha256-HYwxeLrT/dmouBBbqLcHKoRNlo3HWDViDGdFjSLdsoQ=" + }, + "protos-26.5.4.jar": { + "sha1": "60c319aa5d271e400787987ae15e61e1e73dc4c5", + "sha256": "sha256-SLCJ2kM04KNqfQQkf5lMI8jGWtvp0Htxov64NLm5P9I=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-27.1.0.pom": { + "sha1": "53682c375de53a6baa44cba2f21f91b7fae0fad8", + "sha256": "sha256-Ql7CNamS/UOrx6HObxtiHo4BcZH8eWw2X7W/wgx4XwQ=" + }, + "protos-27.1.0.jar": { + "sha1": "b2ea7accb70541360fc44eb953dec4a35e0ffc8f", + "sha256": "sha256-votKXFUzpsryaEfnj2hTu4n0pfMy5xgGYuYqdicSIBU=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-30.0.4.pom": { + "sha1": "8f6768aad7274b471b3300add4454513429e55a3", + "sha256": "sha256-bl2WxashMYd4QUKCEGDZ3/mk+n8oNBNqyZsYswBqWY0=" + }, + "protos-30.0.4.jar": { + "sha1": "fdc7837cc71e27ce3f2c08c24e842c27e30c967c", + "sha256": "sha256-wfAnDHWPYdsxHRNlry6qtGMYwOzeHRWu1sINWzLtOt4=" + }, + "protos-30.0.4.module": { + "sha1": "cae0cdbde04b51654175001144caa8e6b24967e7", + "sha256": "sha256-2CdxG8cu6GoXb/oj/UpUsQjqs7M+up1QQ1JkV48U6wU=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-30.2.1.pom": { + "sha1": "1010ea5cc0b87add02c0419dd6fddfdf7a572795", + "sha256": "sha256-Vnh5AOvCSTl1g76MNkG5ubCemyaj8PucTL5cq4p0MZM=" + }, + "protos-30.2.1.jar": { + "sha1": "7a4b54b151a2def037f1c384de4cb4af9c5ecdb4", + "sha256": "sha256-/rKTpNF1okzn+Y1/eDZw2B2rYSYM+bvT10jpT5XjiJI=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-30.3.1.pom": { + "sha1": "7833695cbe81a539e6fe15342d23408d2f268c3c", + "sha256": "sha256-oWjXnpTKd4xueScY4g4zND+3zSW0obTSqhiIcNBop2k=" + }, + "protos-30.3.1.jar": { + "sha1": "4078d8e29f3868ef54788d45269c51cb533755ca", + "sha256": "sha256-ZpX+S+jFgnTvvw+gq55GXOr08heSlw1o03WRKH7nvtE=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-30.4.2.pom": { + "sha1": "0a7992102716c47c7b3adf0cf0ee1c0c1015668c", + "sha256": "sha256-bzAygI7DYvfNNoK5/wHPnwtDUs0TLoD0pCk6l45oUGI=" + }, + "protos-30.4.2.jar": { + "sha1": "ffa08a769e27c0b4f0f9bc3151daef0f8d280a2e", + "sha256": "sha256-ugMDuVktzTiRthYGPz62cEhfGg/VrSEtkt7VBYQ+qHw=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-31.1.1.pom": { + "sha1": "cb053977fc173dd259171efe64d0bc6de4d42ba0", + "sha256": "sha256-QNQBbFRHaZy4uiFmzpG8MHC5/6x5KsBw6rec0fD7ohg=" + }, + "protos-31.1.1.jar": { + "sha1": "8e0699e5e6b59a622048b0b57237e9466740b84f", + "sha256": "sha256-BNxEGDtKK38UFZCsGy3FPWvmFXI6GBpIGP2VUK0dAmM=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/protos/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "protos-31.2.2.pom": { + "sha1": "e7e5fb1241bb67729eaebe426126117b027355bb", + "sha256": "sha256-oWn0dX9ba/7eEZw30K+K3da4LYHJs/KLo5sHjS+cpkw=" + }, + "protos-31.2.2.jar": { + "sha1": "2ed40fab84972734c47bbce32a52d8912551d70c", + "sha256": "sha256-AiAhxwY/KWTM9HoRdYOkXV8Pw7jVldaHevf4FQmyIxc=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-26.0.1.pom": { + "sha1": "6702994a364e97efd9f8d98c9cb3a6759a6efb46", + "sha256": "sha256-ONtpK1y69p2Jsf5bD8OKXMg/iYcW+LxkCG+hfCXqV9Y=" + }, + "shared-26.0.1.jar": { + "sha1": "5cfc3811c2fd6cddfce2ec1bb5fbbec87e0fc462", + "sha256": "sha256-56elgQgp3ukkzsZUpWmkgwH87dSH97iHEOgW0Sc4X1E=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-26.2.1.pom": { + "sha1": "7dbecc3aa0a180f166d671c9324535ecf4644b43", + "sha256": "sha256-TqzKBUPLUgmMejIrdutDHh4nzqryCUoJnd5sBjmj7wE=" + }, + "shared-26.2.1.jar": { + "sha1": "2e79154b4a5d2e6c1816811db6ab00cf41886737", + "sha256": "sha256-TB5OcF+k1F8jquojBVf2UIFVAS2cKWM3eHwdeyapf1o=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-26.5.3.pom": { + "sha1": "14c908921dd73687f66f496dfc73fa807d57e40d", + "sha256": "sha256-FkUuC4D4/OiS5yBlJvWnupvlSungNjArA0ktU240P54=" + }, + "shared-26.5.3.jar": { + "sha1": "594abc68f0518057cdb150e4c8cc73bc5c94183e", + "sha256": "sha256-dpzYinI6GR6dmPsEGULf5Irzo+8kqP6wCI2NTNfhurA=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-26.5.4.pom": { + "sha1": "12791d9597c430c22a10c0ea9dda65e17f7ad1fe", + "sha256": "sha256-CKkNUratLT1qfYEqfgPbteghupet0+h/VtrJjNGziuo=" + }, + "shared-26.5.4.jar": { + "sha1": "1a0f988f591d492d82b1bc30d5771f074037bd58", + "sha256": "sha256-Gy+5K0kzaGkwHC17wiz1NIB6wZ3QeizYslACSSC5cnM=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-27.1.0.pom": { + "sha1": "d24f50bbd3859f7d13cc920b7cbdfb0113d197be", + "sha256": "sha256-gBkzW3sgqBj5ppHV0BbXwxnG19KKS3iGVew+VQTC9d0=" + }, + "shared-27.1.0.jar": { + "sha1": "1ffbad68155c565413b98df687b1e0f7651bff9c", + "sha256": "sha256-PoGHtvpkf019MPWgqTJR54XN23u6DxzKxx/7iJJoPU8=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-30.0.4.pom": { + "sha1": "5f43553b6bb264f3b0b5589da1673d93cb29aae9", + "sha256": "sha256-H+3xu8FuDWjhCBr2gwNoDWrqa+9crPt2zBJs9U7XZFM=" + }, + "shared-30.0.4.jar": { + "sha1": "d02bc8a370cf76be821686ac47afc4621951535c", + "sha256": "sha256-kcraN/UDW10xPM5k/eKjw6P1iyZ+24Cw4+lDcuO88tE=" + }, + "shared-30.0.4.module": { + "sha1": "73fc4d22f1a2fecf866fbb9584e335f8cebbf800", + "sha256": "sha256-EOrBmNWflNpdIMxSQujpY6tGouKh9ACkTq8rgmkFM9o=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-30.2.1.pom": { + "sha1": "73bbed7b64e639b932f254c96ef36ff2da040e47", + "sha256": "sha256-4lti/yAun1uEwquKn7dePxalVrq0geAXVOy9/QK9cBE=" + }, + "shared-30.2.1.jar": { + "sha1": "3b8a85f5ff132c28e9f4382e61a1e57618919b89", + "sha256": "sha256-1LnBV3xnpbp8AKAWf+qp8Kl6BxfRquwvmEFuHG7paBc=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-30.3.1.pom": { + "sha1": "2535e714661fedefc5988e79fa441937cc431b50", + "sha256": "sha256-yQMRZw8RO4gXEpCPvKEDF81nON+UjTE9uPmvMphGHDE=" + }, + "shared-30.3.1.jar": { + "sha1": "e1c363b7347d680e674c12eb21ecdafbb49b2fe6", + "sha256": "sha256-Sn7oqtV5bgWKL0FJk0JBVRcmiawzLNGpmduGGUni4d4=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-30.4.2.pom": { + "sha1": "e78cf4245dcb3aaf0771a39de825a656ac7ea634", + "sha256": "sha256-CnHigN31lfa8DLEg6xMJ89StrQD2reY47fHR2ALD1bA=" + }, + "shared-30.4.2.jar": { + "sha1": "990ae0af7a3c8bb0c52f50e55992c89d8c4802a7", + "sha256": "sha256-VW68/sYL4iUlTQPRmn11twf9xF9xtED2j0jWQKWTBdQ=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-31.1.1.pom": { + "sha1": "ce5d47732456b55949df4ebff1774b457a4e5dcb", + "sha256": "sha256-V3W6HuHNkdlXmfaqeIkENDPW/Co4M3cOi3+eKAhp4GM=" + }, + "shared-31.1.1.jar": { + "sha1": "02f264944222d494cc74fa2feb2b13e08f4b61c7", + "sha256": "sha256-HABZR13axUTp+PQ64BgJLbtEbIaXDitf4eZZMceA1Co=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/shared/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "shared-31.2.2.pom": { + "sha1": "4690284e3f3da489279ef139c61a8f57fd81b5a9", + "sha256": "sha256-snVhY58baQ8IRCPHBqiATLgfUNaRD82OpZ5YGO/VPzY=" + }, + "shared-31.2.2.jar": { + "sha1": "1ee49c37374d86a840c49b82adb94ac33733ee06", + "sha256": "sha256-e9RVvaKeFSjNpBUzMrcI7S0AZtzmj2ClG+EfUp+r3Xc=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-26.0.1.pom": { + "sha1": "4f77d3c5867728241b7b71115a8dc147611d8441", + "sha256": "sha256-EBB4mJkRKe/yBCpBsb/C1yxOqOS6vkKa466zLGnFaN4=" + }, + "tracker-26.0.1.jar": { + "sha1": "15f0c0560c8a97700651d2dd5219937baa80c166", + "sha256": "sha256-aJXjaPpZFoQox4zfMAuyk1IHPzl9jyYK896C3yyLJEg=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-26.2.1.pom": { + "sha1": "9de8a29b54977e7fb674fa9d783799a5d4112912", + "sha256": "sha256-/RkjxjKQ5s2xNiSDIY/RQMsVuC7FN9GlBEe7I6zQX5A=" + }, + "tracker-26.2.1.jar": { + "sha1": "6b9eca42be508a8d87c287ad280a27f802f0e40a", + "sha256": "sha256-SmJOzJdlOfdV3bC7jfwt09CDJs/sWaCY29cPcByn+3U=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-26.5.3.pom": { + "sha1": "8551b232bde0e0cd7a9983f4acc716ed2fa8d59b", + "sha256": "sha256-fAbjPrxxjgXSaoM5kD9ZoVTedhcXTPha/bxi0Nsgnkc=" + }, + "tracker-26.5.3.jar": { + "sha1": "6a84a345f5155eca774cd238c233a2f6fad551b9", + "sha256": "sha256-dqhsU6BlaXcW4tXGEAyd4LrpHVacu5vRn6U+Pw5n63M=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-26.5.4.pom": { + "sha1": "871a2762da5f41af5544d48a9aa10ee5dab8a21d", + "sha256": "sha256-6gE4k9aluK33G3IScw7PzjBlFeHYoIcp2U0c1gvKNs8=" + }, + "tracker-26.5.4.jar": { + "sha1": "7ebda91f709dcd3234d948ad3f5c04c1be38fe83", + "sha256": "sha256-WMV6VHCkVqtV08ndFXTDpo6VtPVenLSkCvkJtrZWaX8=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-27.1.0.pom": { + "sha1": "5241b6f8bb394b0e8d7f7ad3d5a9ed84c863da01", + "sha256": "sha256-egnjGaUEQ7HvMMDr1Efv3yBuKEp3sRsDql+QN5cDmuc=" + }, + "tracker-27.1.0.jar": { + "sha1": "e5acd478607d3d13d80e4a47073b0797be97bac4", + "sha256": "sha256-+UusQc316Kh6+Sf0hoOXkHcgaauKl+jLr8qK4M/FDWw=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-30.0.4.pom": { + "sha1": "61745e7da2f09406900630dcf6ec98072fc46a46", + "sha256": "sha256-dF933VfS3SrbmgCnKQ4v7AKIBVR+6pKE0dOV3N+Xi6I=" + }, + "tracker-30.0.4.jar": { + "sha1": "70f5c858ada5d72c47d333ac583feee86b6afb8f", + "sha256": "sha256-xapqoZljINU/wlSluWyaEymYha99Q4qHfJ+a5wBL5TQ=" + }, + "tracker-30.0.4.module": { + "sha1": "e96176bc2e0ead20e3e7703de1933cc89144370d", + "sha256": "sha256-wofQkDaM/t1dSb20B60tUkFPGrznZqa/ox1Zmq92BYU=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-30.2.1.pom": { + "sha1": "a4fbc9c6c0bf5a34b1c2ea8d9429a74d2ce0cb62", + "sha256": "sha256-I5oFTCEyIM0Uf36kD7sNkOmPrkFK5yAvAOhIha+FkEg=" + }, + "tracker-30.2.1.jar": { + "sha1": "f2256fa76371dd154b7b83a2b431635d7d981538", + "sha256": "sha256-f0BDZXP7/YFIl+djkxUcqKRgKoteZ2gMZS1+nHMVwVE=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-30.3.1.pom": { + "sha1": "c69411cb11f354bad196c5d8962ba5e5b1589323", + "sha256": "sha256-V0iO5rM9GmvXbtv7RluU+jIhKmKLygg2Kuy2Euvwz2E=" + }, + "tracker-30.3.1.jar": { + "sha1": "5e65e94c9dcd4a9e3b88e6d022c3db8a52248bc9", + "sha256": "sha256-7EPbxs8ek/WZkmK0g8tuyTs3IBDAbDgAxQ2fypA90cI=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-30.4.2.pom": { + "sha1": "34bb46f432abece5dbdc25da70ab7f0b7ca99067", + "sha256": "sha256-dpkmtnuMTnLgcSnpboYotSBenfb4QXNKGDAsOMrBXZo=" + }, + "tracker-30.4.2.jar": { + "sha1": "93e267ae33339e3e4c31513a481d4e5da25a3f3c", + "sha256": "sha256-pivPitOBa9wnaZnoDI7dUr2xogv9Vx2PX36R/OHHRUU=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-31.1.1.pom": { + "sha1": "37703a62773f4f5a5ac374cbefe665903a5b5b24", + "sha256": "sha256-uV29Hl8pEQ+2hiECb8DCkEiNwhHJFbD8GmLqkYnE30c=" + }, + "tracker-31.1.1.jar": { + "sha1": "4defc338e33c908b6910577a18b4000987325e9f", + "sha256": "sha256-AsyTL0gBLY/q4dCflJaXxuIVToIdbUZJ4eOdyRHEaXc=" + } + } + }, + + { + "path": "com/android/tools/analytics-library/tracker/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "tracker-31.2.2.pom": { + "sha1": "5377f7c8b1afa0bba9121faa7574f7e55817be72", + "sha256": "sha256-SogXQRbuTzMJ3sjT6ur1az/Oq79iMfREjqst2mjLX8A=" + }, + "tracker-31.2.2.jar": { + "sha1": "4b8f807ccf3691d0d9ee8226dc074f596e70eb4a", + "sha256": "sha256-MMpTQKVgH5ctuzguvSlALZz7nG2O08SUafha17vy8zg=" + } + } + }, + + { + "path": "com/android/tools/annotations/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-26.0.1.pom": { + "sha1": "d4d92709c9b15a5a1fb33a78e041533fdfdd1577", + "sha256": "sha256-T4iokULGS/A5BvhGCVlGZulpTfVuPm8F7fq4pos52Dc=" + }, + "annotations-26.0.1.jar": { + "sha1": "29012758503f56c7dc0714eda494f7f140fd3db8", + "sha256": "sha256-qdZdrMxa/KlzcpNiRqxJ+1h0I+AFYG55gbDvxmsOk4U=" + } + } + }, + + { + "path": "com/android/tools/annotations/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-26.2.1.pom": { + "sha1": "63dc441eca9b0d9aeb259ed2e33f080760ec651a", + "sha256": "sha256-ZNeXg+Lf/B/sWBRgUb3ZOi/KIxE6W4RD226x133t/ew=" + }, + "annotations-26.2.1.jar": { + "sha1": "5f9b112634e53890e16cc2947989bc2bc048734e", + "sha256": "sha256-c5HGoeCAF0uW5kzrB42t0xzk2KLS/uDsZb4gISb5DyQ=" + } + } + }, + + { + "path": "com/android/tools/annotations/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-26.5.3.pom": { + "sha1": "03c6890f54f0b7c91aaeb7d66e87c52e53b10d79", + "sha256": "sha256-4vEHB5aOWAXrLslhHTRa1GVrKLeEpApil0rlXPIQzh4=" + }, + "annotations-26.5.3.jar": { + "sha1": "87a36f2086b41d1ec162a6e74c4f999d58eb1310", + "sha256": "sha256-/js3c6HdNGlY2KFUess2UN/74HfTmtdMX3b7f6k/NYw=" + } + } + }, + + { + "path": "com/android/tools/annotations/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-26.5.4.pom": { + "sha1": "754b6bad3451ae01952f028ad1810c7ce3394cf9", + "sha256": "sha256-0bwL1GC8pQb4zvgxGQ8C30Rr6/6tOSHCPrLrMPXFuEM=" + }, + "annotations-26.5.4.jar": { + "sha1": "f3165077d2dc34a2fed93ed248f3b958fec162af", + "sha256": "sha256-Crf1G7aInPkpQSTCxCOK0+rU44i2c/yhWbv07/jlsTw=" + } + } + }, + + { + "path": "com/android/tools/annotations/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-27.1.0.pom": { + "sha1": "5cfc76a3491124ab871d8f70fa2f80c41ac267c1", + "sha256": "sha256-/g7JomZ/WVtkk0AReD4u0O19WmSY6sftD7IbL5vhqVY=" + }, + "annotations-27.1.0.jar": { + "sha1": "c5e76d6e7078808655107373703dda7c58fec517", + "sha256": "sha256-7wUaA12ys3A+YCRTaH0Bj/i8Zfz1aXEykisKWRex53Q=" + } + } + }, + + { + "path": "com/android/tools/annotations/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-30.0.4.pom": { + "sha1": "8412ee379366574a6aa0cf2b2010b180cf6552a9", + "sha256": "sha256-57CEFANunNMS1hdizbgY+10BGmxqNbLwCWKjDHfQwJg=" + }, + "annotations-30.0.4.jar": { + "sha1": "824f88bfaf5ebdac7eb478bd434de74429634723", + "sha256": "sha256-3xMrTaortzdwzUWcTGubknaV3YqJWSqxSezbfg2FBu0=" + }, + "annotations-30.0.4.module": { + "sha1": "ad1824a9fc5ce121eb2cb8d92191fdd3bc173f53", + "sha256": "sha256-1e9VoFmrpl9gVINxdQJyhdI5ueo9tRHuguWyBseFBhw=" + } + } + }, + + { + "path": "com/android/tools/annotations/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-30.2.1.pom": { + "sha1": "26bd540d2ae469f5e0d38659d8edafa9aae4a65a", + "sha256": "sha256-PafOm8i2L0mnKhc2Ev45eqKRqZTzC8jk3lOodvP0lwg=" + }, + "annotations-30.2.1.jar": { + "sha1": "e0254d7202df74359763abadb4cb48e2ad0365e6", + "sha256": "sha256-Ywq0xvIR+hwPXIhBUstjETYPG3lkQhlsKHplhkWplkU=" + } + } + }, + + { + "path": "com/android/tools/annotations/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-30.3.1.pom": { + "sha1": "418c1139eed0d5cf284c7bcd1cdc5c8d6716bb6e", + "sha256": "sha256-Qwer/FKTwxFgAxYGEc0VzhlevNkUTq09DVf5AeegXzc=" + }, + "annotations-30.3.1.jar": { + "sha1": "bf7c0de4844ebdce4eec08b202d24d8f1f7ac520", + "sha256": "sha256-d+UZa8r8KDxemH8JRPBJ7uDmi1MuvrUw2FX28ZbbTyo=" + } + } + }, + + { + "path": "com/android/tools/annotations/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-30.4.2.pom": { + "sha1": "fa4af32b6516b2c21daadffe90d8888836c4f782", + "sha256": "sha256-lpAph3O7bPheW2UanO/LAOGnNY5hwYCP+A2Is1+Uexw=" + }, + "annotations-30.4.2.jar": { + "sha1": "9251cabb9830e0f59e30e8e99f58af7bbc97c34f", + "sha256": "sha256-1Lp91gVabx/W3bbc704glsFZCLx9xgV8suBvOg25Scc=" + } + } + }, + + { + "path": "com/android/tools/annotations/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-31.1.1.pom": { + "sha1": "9475599f5954b4ebb3b9fb4f400bb61f720565b0", + "sha256": "sha256-XcDnAJTFiIHnQzsR367CdyJ5nSD/z2yDwSoE3rUJBDU=" + }, + "annotations-31.1.1.jar": { + "sha1": "d70720d1aed8e56163636f82782985acfe66eba8", + "sha256": "sha256-7jv9nNtQEr22FSD4ZUp4VXfpuzN+WTnFxhSaRGaE7hY=" + } + } + }, + + { + "path": "com/android/tools/annotations/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "annotations-31.2.2.pom": { + "sha1": "b335dea7d4c1f9d0bbf3b044fc66afe8a9262600", + "sha256": "sha256-IMQ2oAFFOzZaBupC1/BtIWP4q2gpe3sx0NpeIbkNG7w=" + }, + "annotations-31.2.2.jar": { + "sha1": "d70720d1aed8e56163636f82782985acfe66eba8", + "sha256": "sha256-7jv9nNtQEr22FSD4ZUp4VXfpuzN+WTnFxhSaRGaE7hY=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/0.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-0.3.1.pom": { + "sha1": "235a6eb105ebf8b411e4d75926814cd34abd0d22", + "sha256": "sha256-MeXw5WtZp8FDOrSgOW/WF47yje9s9utY4jXXz0Dv0B0=" + }, + "aapt2-proto-0.3.1.jar": { + "sha1": "8311800abb3dcd84426a0de834a6b884a173d864", + "sha256": "sha256-pQMhm4mwqve3xsmzsX1p5lWmxhkHK2hKHqT78dfcSdo=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/0.4.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-0.4.0.pom": { + "sha1": "7dfca013cd859905a574a0c484e5c022eeccb3c3", + "sha256": "sha256-okvdTo43T9zYzvjXfqcj8UfM0PJdxt5PvikAOb6QQzk=" + }, + "aapt2-proto-0.4.0.jar": { + "sha1": "d4db96aa0d2e161d08a038731e08c3ebf612cd12", + "sha256": "sha256-+sBDXgiJj4nu65yiNr6nBxVf+BbBIgXO0oWtU2BBM8o=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/4.1.0-6503028", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-4.1.0-6503028.pom": { + "sha1": "d0affb06f179cb5f521c127cf43660b0f5b56fcb", + "sha256": "sha256-znJmjPzwnaG3JLYXMIm6YnvYeM+LZP2R8dFMEm5pFqk=" + }, + "aapt2-proto-4.1.0-6503028.jar": { + "sha1": "858d5d4c84fba77dc8ce69feaa3c90457f06e6dc", + "sha256": "sha256-TiaW33lAf9+x81us6+CHnVVKw53fBQzCW80hXngXZ3o=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/4.1.0-alpha01-6193524", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-4.1.0-alpha01-6193524.pom": { + "sha1": "f98ad12c2eeb644c995ca18fc9a5a41dc8d7f8c7", + "sha256": "sha256-FApAa8xSviyZk3xR9ikHYTTHT7Bgru259U6Ry3CR14Y=" + }, + "aapt2-proto-4.1.0-alpha01-6193524.jar": { + "sha1": "fadd481b02a88ce9b256c11533ef1270c0c847ec", + "sha256": "sha256-F+dVI+HpLdTyIsc2juQd+elkpQgjL1keJl0MSZuvnco=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/7.0.0-beta04-7396180", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-7.0.0-beta04-7396180.pom": { + "sha1": "40411736b83aa6648190297407c6e5c8b73c612c", + "sha256": "sha256-YTkg2FmOoU1DxeTH1Lt9ioaoyzjbhv+aAj+Ut+5ukvk=" + }, + "aapt2-proto-7.0.0-beta04-7396180.jar": { + "sha1": "bab609213f260c15cd22ff646a8a8f99e05c2778", + "sha256": "sha256-HKTxsPVQxsJfY8GRbahPbnqSxmt604qx1dSaIFUqWYQ=" + }, + "aapt2-proto-7.0.0-beta04-7396180.module": { + "sha1": "cb38595c4e7bf301971049ea86b38a9179e87cca", + "sha256": "sha256-IrXWkpbAa5kmHCtUhZlK8ebn4Cvrao0hPOiNrnZmB1U=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/7.0.4-7396180", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-7.0.4-7396180.pom": { + "sha1": "4376661e318218c3cae5b66d103b358570866980", + "sha256": "sha256-b2JGjit+sODVyWUq149LwHkquei1+cTL3fwejBkgWEo=" + }, + "aapt2-proto-7.0.4-7396180.jar": { + "sha1": "d309db52c6cec920b5360caa3444b8719aba26aa", + "sha256": "sha256-Jx/+dTeSOQRjfUyem1Pp3cvjm1vXvsgkp0AAS+eumok=" + }, + "aapt2-proto-7.0.4-7396180.module": { + "sha1": "43414c2f8ca8d0051e4eeeb7a2993cd7a33b9b8a", + "sha256": "sha256-sZq5Hy43HftjdzrQMykQrSWWKU1xi6e2QIV9h0/n5Qg=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/7.2.1-7984345", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-7.2.1-7984345.pom": { + "sha1": "fc09fcbb72cdba495d981421dde833bf0dcf504a", + "sha256": "sha256-hXM85IlKtL8F8HCp08CcWJdWd42agjXOpIvpzS2T/lg=" + }, + "aapt2-proto-7.2.1-7984345.jar": { + "sha1": "38a91dd7b31ebc87e3e44d2b9e267f90f90c1bb7", + "sha256": "sha256-9LgMuNWep8PW2ZIpUrZEFW/FpuIj0IJf/FqPfsf/jdY=" + }, + "aapt2-proto-7.2.1-7984345.module": { + "sha1": "0ea61bfa60fde00a964805a81afc8d423cc4ba0f", + "sha256": "sha256-T/mQq7fMPrFtie/R+880YNQTkxxhzjo36QnuLy4HzzI=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/7.3.0-alpha07-8248216", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-7.3.0-alpha07-8248216.pom": { + "sha1": "e3f04d98df70e2b3cb5618838e6d3575d8ceb63f", + "sha256": "sha256-pTdQb9UtDZMnlMqEkETqEO7b4k2lGDgHSsdF3XMRPIk=" + }, + "aapt2-proto-7.3.0-alpha07-8248216.jar": { + "sha1": "0a4570674c55f4373019fb4f02ccc695999acf89", + "sha256": "sha256-bkVQOpZewArNfDsV3zwP2tKiXYfQC0rjgAMMPOa1JvQ=" + }, + "aapt2-proto-7.3.0-alpha07-8248216.module": { + "sha1": "80b56a873508be3014eb23fb403b1f0d616d85dc", + "sha256": "sha256-KBNxL6kZLPQ23TXb/pN1/FkIpySE8aPkSbSRpuzQygU=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/7.3.1-8691043", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-7.3.1-8691043.pom": { + "sha1": "0297ae8323859d6090a1159ac5ba468682f83765", + "sha256": "sha256-90SV2eV+uTBEowjJ0SDPlOucHbxK9gieeWPwVgf0OWI=" + }, + "aapt2-proto-7.3.1-8691043.jar": { + "sha1": "c5ced5de396317a62df687ed0ba4404898bdc266", + "sha256": "sha256-1eLz4eHrBiJLaHX15RPHKmUYI0J0VxgWDK8ZHUapZmQ=" + }, + "aapt2-proto-7.3.1-8691043.module": { + "sha1": "312309652191db1508028ca3d98e693ff2b7477e", + "sha256": "sha256-XHobthipLvWcPWDPgEmPFkYKa/L+qvxK6EfT+2xZ7aw=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/7.4.2-8841542", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-7.4.2-8841542.pom": { + "sha1": "404365ad84841faf9f15c8e809a817bd952ea7ec", + "sha256": "sha256-scn1dAsR/yEnBw5OApl7o/PqTv1T5NejGJ1wkx+DnDw=" + }, + "aapt2-proto-7.4.2-8841542.jar": { + "sha1": "d66fdd8f8469bad0933d3ab52e107177df6a3f2f", + "sha256": "sha256-8LESAJJGfaEDuZ5UumCFgICf/KKc67+J1kWNtUTb8lM=" + }, + "aapt2-proto-7.4.2-8841542.module": { + "sha1": "2cb366d2e71047e1b8dde3fdee8d89010bfb55aa", + "sha256": "sha256-FfHJ2kIdX0Qg3JHh9e8agL7H3tFlsOtPuV8r7eLLHhM=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/8.1.1-10154469", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-8.1.1-10154469.pom": { + "sha1": "6a5cc8c79b47076dea99166e4d635742f096b41d", + "sha256": "sha256-Nt5XFCAh57/eodoP+yDLJhHXmHHNDS1eK0YgIyUzAEg=" + }, + "aapt2-proto-8.1.1-10154469.jar": { + "sha1": "e38bb50410bed03e4e29bb8772535b46a26fad17", + "sha256": "sha256-xh3afdhum7W8w+WCq8nRHaojqexIze2+JXgib0Mvdn0=" + }, + "aapt2-proto-8.1.1-10154469.module": { + "sha1": "20d8007f0c80abc0b098d75fd79bb077e17d44c6", + "sha256": "sha256-83wevQJGl0HedIlfqIOYSOvikIXrTeOx6iF+YOkoOHI=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2-proto/8.2.2-10154469", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-proto-8.2.2-10154469.pom": { + "sha1": "f24aa8f0e08091c48acd31120369dfe865dc8abd", + "sha256": "sha256-y/aOvJ5w6HC1lYOLVHjQD7za3MnXS34ZAxAjHTdm0mk=" + }, + "aapt2-proto-8.2.2-10154469.jar": { + "sha1": "8ce15106ed14b44732c5753cb6ca4609fcf987d7", + "sha256": "sha256-KLJgZqvX+tOwhxR30qamf4bNpSXiPU6BYgm5ZqmNriw=" + }, + "aapt2-proto-8.2.2-10154469.module": { + "sha1": "6898033d4ec7e37d88736cc77fd9a79ce20d6000", + "sha256": "sha256-lQq9fFVghgns16mslLzHWUIeWdyyJl3ifm/R5zDFofM=" + } + } + }, + + { + "path": "com/android/tools/build/aapt2/8.1.1-10154469", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aapt2-8.1.1-10154469.pom": { + "sha1": "2b62faca353d3e97e1937bc1dc1c1a1c9971db6d", + "sha256": "sha256-UdUGCoMmEfJxBS2demYKGdG4lzd2DqQbDUCHt/h7Ohg=" + }, + "aapt2-8.1.1-10154469-linux.jar": { + "sha1": "9521deb131442a36ce93bbc9e1ab7558da7c09bb", + "sha256": "sha256-p54GGvEfAo0yk8euVO7QTu/c3zuityZhyGdhFSV6w+E=" + }, + "aapt2-8.1.1-10154469-osx.jar": { + "sha1": "7ba1292279ab3fa6214f021f6457d2c8ecbc21b8", + "sha256": "sha256-bO4ljdUEfbuns7EyT1FKGLqNGz+0bms5XsplXvzD2T0=" + }, + "aapt2-8.1.1-10154469-windows.jar": { + "sha1": "a574696d7c58b61a7a86eaedd3d9754c1dcb99e4", + "sha256": "sha256-ktb2p1ycwcicZd7TNDviET9IynHgQf8L+anBeLce3A8=" + } + } + }, + + { + "path": "com/android/tools/build/aaptcompiler/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aaptcompiler-4.1.0.pom": { + "sha1": "135f4de109871c96242c6e0fd3a69dfb816d923f", + "sha256": "sha256-sOqmXjW0ED0NW0SCCy+Xd+/m+PhLALtsbUyMWr7a+nA=" + }, + "aaptcompiler-4.1.0.jar": { + "sha1": "daed124c5fcc8b7cb56fb1278ccf0611b32394a1", + "sha256": "sha256-Yzx9SZ+Osg/erCNcQXXzrfC6MiHY+9Tu/kX1dYNl2uw=" + } + } + }, + + { + "path": "com/android/tools/build/aaptcompiler/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aaptcompiler-7.0.4.pom": { + "sha1": "4f59079c4046ecb9fba6016bd03784d1171f2e4f", + "sha256": "sha256-iM5JmEbwF+6yDb6rxAtveMe60or9ENP0+ZQKO1GFuc0=" + }, + "aaptcompiler-7.0.4.jar": { + "sha1": "adb56bd73b0e9514373aaf42ce7c0c639492188c", + "sha256": "sha256-4QgaRMGdMtI5bHPn+Oks/hVtll8I7Skt8CuHGn/XRhE=" + }, + "aaptcompiler-7.0.4.module": { + "sha1": "7db08efc781069297c9596c2202e2f59ccb09cc6", + "sha256": "sha256-CKNYPqjJFqHd/KlwYYgYG6oIVQuHyUP5ui4HN9XSQfU=" + } + } + }, + + { + "path": "com/android/tools/build/aaptcompiler/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aaptcompiler-7.2.1.pom": { + "sha1": "1875562c3087df822d24cbf76722f38687a94630", + "sha256": "sha256-U3MSvFzibQBY+0BU/x3v62yFdi1j6mgxO/M4oWqVb6k=" + }, + "aaptcompiler-7.2.1.jar": { + "sha1": "95487f5d7ec14ae3c3d300940a6df4ada056c9f4", + "sha256": "sha256-xfygLNc6a8ZgbfufPBVf95BN3l8/+3boL9FHoKn9KUs=" + }, + "aaptcompiler-7.2.1.module": { + "sha1": "c72ba29d531e68a4ef8cfe95fd81325f432d9194", + "sha256": "sha256-5tOVzqVWAbUrUKgnRj710MtPN7T/pOhHwGY2V97l2po=" + } + } + }, + + { + "path": "com/android/tools/build/aaptcompiler/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aaptcompiler-7.3.1.pom": { + "sha1": "1514b9462437d9c0b5181e48de057b490df1a11d", + "sha256": "sha256-+nqCmMnVOGrMkzPYt9rp5MOFPDn3DN8yW0Qqm6fVYA4=" + }, + "aaptcompiler-7.3.1.jar": { + "sha1": "de743c148d7ecd5b896c0911a98f7d3eb41d1b1e", + "sha256": "sha256-LOCTff4ovL4+DCDKAwP7X6KHPwstGHXXdQmvH77lBpg=" + }, + "aaptcompiler-7.3.1.module": { + "sha1": "34b970cad1c31818cf406365f891ca30ff80b45e", + "sha256": "sha256-3soXIHSif+7axiMcHtfelwtZEPVYkfySjL/GVmRhhy8=" + } + } + }, + + { + "path": "com/android/tools/build/aaptcompiler/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aaptcompiler-7.4.2.pom": { + "sha1": "14c0fea0aa326d7d4d92174479d2a8247e20bf45", + "sha256": "sha256-Iww6MPjIr4vBa8XyYpad1WeesmKDFX+VMtkm3Bq4nrs=" + }, + "aaptcompiler-7.4.2.jar": { + "sha1": "2bf0f040dad19c66b661d973c6c9f135355fd072", + "sha256": "sha256-l8cdJl0j4LAqHQh2ju46qeegCE/10LaIAZ7ItLVllcU=" + }, + "aaptcompiler-7.4.2.module": { + "sha1": "6d54dd56a97098e3123d163fd7d494d689e3c6d8", + "sha256": "sha256-CQnrS6aWlFYZ2FicrPTrC2Dn2zM8mYqb+7VFcs5S8e0=" + } + } + }, + + { + "path": "com/android/tools/build/aaptcompiler/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aaptcompiler-8.1.1.pom": { + "sha1": "8de8c5190c979c047aaa4db5d73a50adeef9dcca", + "sha256": "sha256-wgTqscIi1F2XcvtAgXvltcGd85DAU6RnLDUJ9VHmzYg=" + }, + "aaptcompiler-8.1.1.jar": { + "sha1": "7f64699804514b64d14abd79a12c1d6902de7530", + "sha256": "sha256-M3UjB7C5t+vjuVdXPjmxjIx3FlMw/5c769xjuaxvLCQ=" + }, + "aaptcompiler-8.1.1.module": { + "sha1": "86f74f8c0afd2683464d728d2651109a2f6f04af", + "sha256": "sha256-coIQCS6/WKgADagIqbsouDdDMbHtJsikhnRpcjaDmok=" + } + } + }, + + { + "path": "com/android/tools/build/aaptcompiler/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "aaptcompiler-8.2.2.pom": { + "sha1": "7cd5c77db99f0cd77eb332601998a371ae16c962", + "sha256": "sha256-BikfU7umKMYbX/qe9TjVUEVfl/bnGAv2uU95VF34lLk=" + }, + "aaptcompiler-8.2.2.jar": { + "sha1": "6a30c90a76c95c771d64e3640790ec7a36e7e903", + "sha256": "sha256-+r+noB/AM3YRvvXk2MYixLWb6OojHBlDkZ4f9OQecdQ=" + }, + "aaptcompiler-8.2.2.module": { + "sha1": "8a254f3efe1882d2a6e2f7c86cb40a9465edcea3", + "sha256": "sha256-HTaOiG9UFuL5nK3w7gscWPXIwqJrRuh7QsoC7vLwysc=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-3.0.1.pom": { + "sha1": "e5218e97587ad80f7945e20cd6db112b7aa00385", + "sha256": "sha256-Zo62rrPQOCZ+cZ3rVfOKQjOhJgKvuX6J2mCcCVOUQAo=" + }, + "apksig-3.0.1.jar": { + "sha1": "535b9323a1bd47e3a9c860d11f4e11a80db13f2b", + "sha256": "sha256-cS18l00gtxznMJOKPdqNI+h8/RZQDhwaLJ7MLJ/w0yM=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-3.2.1.pom": { + "sha1": "6bc273f7c6759c88a5233bcf10c2b11e12b07b67", + "sha256": "sha256-4jVg9Hng/KKpFe1yjsfr/8xkgZLL54OQjHxYhE1vPqY=" + }, + "apksig-3.2.1.jar": { + "sha1": "0ad6d930fceb350753d645f4bf66b3eb4d2ad566", + "sha256": "sha256-K0by/v/qZgN6qynkJhskM8GQGUpu+XuVhRHrFX8sy6U=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-3.5.3.pom": { + "sha1": "fe080d599c11357971f21a2b7475d9915515d363", + "sha256": "sha256-5sSQBWPiQJf82rf++W6oPPf6sfSTn6teEVRL8Ng0LZ0=" + }, + "apksig-3.5.3.jar": { + "sha1": "d5ffda89f909743ad8e77b3c28ed351695037543", + "sha256": "sha256-SMfd+nhnEtdmpzyt1+3JGGhsGv1s6FcRyvUQYx+0CQ8=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-3.5.4.pom": { + "sha1": "5140271d412bd3d8b90f5b7c347d1d708700bb46", + "sha256": "sha256-75WOL7NkxkWED20lZ5BrHB46XdycsZtzCKS61IuFfcE=" + }, + "apksig-3.5.4.jar": { + "sha1": "25e48bdb40ac1e627248575dba64ab5fda63389a", + "sha256": "sha256-ZI+GEkuyALBNvMI7sy95LfaT3fVgcX6s4cVfEuy9aQw=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-4.1.0.pom": { + "sha1": "9e0cb1f3ef74aa08d1bc972ecb90fbd4524e51d0", + "sha256": "sha256-2StxhlTm27NSxhlmPLT1ptTUUugkMtKz1joOfYrHiQU=" + }, + "apksig-4.1.0.jar": { + "sha1": "65068a3496ad4f7a2f2ef53fb4af3f1fcd6448ff", + "sha256": "sha256-dXkl3sYXITVokVrOSl87WsOvkPDZpK3voT9H746xfXY=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-7.0.4.pom": { + "sha1": "7c58a5a7a0289773da39ab37b3b2422370aaf530", + "sha256": "sha256-c9Y0tC0SAG8JVqanwDm9aY39uj0jM+Dhs+OdhJICQa4=" + }, + "apksig-7.0.4.jar": { + "sha1": "55069f1c25da805fdeb367612f8e94788de80966", + "sha256": "sha256-s9TcJ08X2EvvNja9S0fBp1qV2VRAisf42T5etKfty5k=" + }, + "apksig-7.0.4.module": { + "sha1": "74e7804b7088919f7fa8007b0e8567ac50672c7f", + "sha256": "sha256-C4fN/bz27ZpCF0ho1Ja8iBOdOIZeNyPiFfMToE0YEUE=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-7.2.1.pom": { + "sha1": "e0618cd072903641b25c40b15108b71295e92fba", + "sha256": "sha256-CANJmrbvASk4i7HLt813nShEHePuu5snrNjaZvaOM34=" + }, + "apksig-7.2.1.jar": { + "sha1": "735df93a1e8f80050ce1e23caf72edd493273970", + "sha256": "sha256-QXvkt71+lPAyc3fELkC87ZkyXK5gg0EBAZlgJ1dIoC8=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-7.3.1.pom": { + "sha1": "226dca1560f017d5f78b80386e984dd0ba40e5b1", + "sha256": "sha256-fTJrnB2Xjlop5FP+w2htz5VyHLE85sA24kgU+dZJYKU=" + }, + "apksig-7.3.1.jar": { + "sha1": "46b1d756b1dd2f41714f24c84b611306d39ec065", + "sha256": "sha256-bWoTpRrDgGwRr18JZwkgVeNaZ99qdL/xVE2fVRjFrVU=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-7.4.2.pom": { + "sha1": "d2997ce9921c12cf1c462535fd7a1cda341f1a23", + "sha256": "sha256-IWiJwK5k4XB1UD5KLVbiQBGNMi5St7bBkk4DI8tjQEw=" + }, + "apksig-7.4.2.jar": { + "sha1": "46b1d756b1dd2f41714f24c84b611306d39ec065", + "sha256": "sha256-bWoTpRrDgGwRr18JZwkgVeNaZ99qdL/xVE2fVRjFrVU=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-8.1.1.pom": { + "sha1": "e7ffa86a9b24ad23095854e72cc47dd8a11a7cf8", + "sha256": "sha256-0IEAgv+FYDPQOREtlTwoMsdfC11cVDwZ/iH2EMqIcLk=" + }, + "apksig-8.1.1.jar": { + "sha1": "9125e1bb9607abb6d2d582f6b551360b0258658a", + "sha256": "sha256-Ro7hhS+hJGHGYpYI/ke+cOUPYL58R0S7f2LKSOhcCmw=" + } + } + }, + + { + "path": "com/android/tools/build/apksig/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apksig-8.2.2.pom": { + "sha1": "9dbf46bf7c86d4ed9276dba87d36e7797fada389", + "sha256": "sha256-9ZqkmZZvgycqAzFxMe7IK2MKqpFwJ2pXahgoYunD9Bs=" + }, + "apksig-8.2.2.jar": { + "sha1": "9125e1bb9607abb6d2d582f6b551360b0258658a", + "sha256": "sha256-Ro7hhS+hJGHGYpYI/ke+cOUPYL58R0S7f2LKSOhcCmw=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-3.2.1.pom": { + "sha1": "22a184ca4c2bb74b1b55f4ba8f5ed33516e4d6e8", + "sha256": "sha256-VDH5LNePKzl/ZXVFr5/KDwkP5R0yXnIUW6146Gbu57E=" + }, + "apkzlib-3.2.1.jar": { + "sha1": "fd288c2a8a84c8acb4e43cbbd9dc34fd442b8bfb", + "sha256": "sha256-w5rQMTkFkyQx/oHIiZws85pNkq1sTtyqSyVDL0YUUqo=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-3.5.3.pom": { + "sha1": "d41d21481aa9ea302638d5008d1c54e391981843", + "sha256": "sha256-unLu4SDrvKUNUxJUZlaH2INOewFTQfQfdi2GKoiPDrI=" + }, + "apkzlib-3.5.3.jar": { + "sha1": "8e17d7b8bb5f756c139619d86facca1675bc6164", + "sha256": "sha256-aNpGeSdH6yIHK7PygeptifcYAB6Mmaa75oYJM/73qdw=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-3.5.4.pom": { + "sha1": "18b3e978362e7383a0137de5829706eb94c4fbc3", + "sha256": "sha256-L6lrugh+MoiU5F9/msFDjYLiMFkbh1W9cErZeZv9EsY=" + }, + "apkzlib-3.5.4.jar": { + "sha1": "a590af8131b6b03b6b638d716cd46c6529c3dd40", + "sha256": "sha256-vFAi41YWLv1LPTyFuBtermxiWFJchTL7ZdEFDUae+RM=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-4.1.0.pom": { + "sha1": "c399247d5b0d8a9f88f5c7b9eecb415d05a52994", + "sha256": "sha256-jRSMIt9O3+pftyLQiqd62rW9q+y4c0Ss8VwcnfHhvWg=" + }, + "apkzlib-4.1.0.jar": { + "sha1": "d5b6769410dff70448dfac18387fcea05ef11073", + "sha256": "sha256-Nu8SUl1x64mvval0nHSD4O8jZ21S2Ir3O6ycSd1cYXE=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-7.0.4.pom": { + "sha1": "d82d8c2e63729ffd58bec4d50b2b163df15ffc0b", + "sha256": "sha256-Ei2MI8ABltgOdbnh4LRdefAUuXQnEm5oLxF/XSaVoIk=" + }, + "apkzlib-7.0.4.jar": { + "sha1": "756f0d262f94ae1de42c1ece5e8c43623ab5068e", + "sha256": "sha256-Xd3aDF+HKNO/O+MDEyYRdudaDClejoz0uaXtKqGsCTM=" + }, + "apkzlib-7.0.4.module": { + "sha1": "98c7a9fcde090b73a42192d607b370fc73a420d8", + "sha256": "sha256-RfnHIKj71M+R8XdvSs80fszT0rkOZI4niBm0z/Llvp0=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-7.2.1.pom": { + "sha1": "03571351f11874f310e060da4211a48e7b87622e", + "sha256": "sha256-DOew8vDt2cttydirJK8cusgMLx68/5L/piJq5RXmDRE=" + }, + "apkzlib-7.2.1.jar": { + "sha1": "dea3c50cf709649fccb03ed2f936901e7b8d1c8f", + "sha256": "sha256-xwXYBhsE6RTPdabSie4ETolcsf0qXJrzQ7yMEZrKbeI=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-7.3.1.pom": { + "sha1": "09c5eda498dde83a6748057c080fedc372ceb23a", + "sha256": "sha256-w4uHS1dmeCuMVckNG48Kzm8IHZDikuYvzZRTvWLY6QQ=" + }, + "apkzlib-7.3.1.jar": { + "sha1": "5b1abe37db589a8c57e1023c0c709d41424d87dd", + "sha256": "sha256-QVxdWt/2iw69cA4nwEwVBoPmXVcx1H/bAY6uOR9wdow=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-7.4.2.pom": { + "sha1": "9be106c10dbdc3c9486c0eccfe39ab47c3339677", + "sha256": "sha256-WglCZ3SrtpxJeHIPo3YCLLL0hbWeczojMsPYCs/GEyc=" + }, + "apkzlib-7.4.2.jar": { + "sha1": "ad7136410ad4c88b1d1dea24ddeb5f9379e157e5", + "sha256": "sha256-eUdNu3UFUVp/4IEUmhGapMZYCsTajcPqfTZ1lWsA//w=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-8.1.1.pom": { + "sha1": "c7de768fdb7b5df67e206e6e1f13ec3370e3e00e", + "sha256": "sha256-stgVllE9LK9HE96F2qBzB/O15PpBCAmi1o70pEG+HRI=" + }, + "apkzlib-8.1.1.jar": { + "sha1": "cac4447eae289138e1cbbfe13a0d8bb6b4255e56", + "sha256": "sha256-7GzdBkXJkjxzPTigpowbR5Gagi8TjVbcmbqceMNh+gA=" + } + } + }, + + { + "path": "com/android/tools/build/apkzlib/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "apkzlib-8.2.2.pom": { + "sha1": "f27726c41993e79497528a285daba74bc362ae33", + "sha256": "sha256-9Pbmz7NPh6WMasdzcX4VmEkoSFjlZySq7FLht740UAs=" + }, + "apkzlib-8.2.2.jar": { + "sha1": "d656b52facbb301b8bdc1e40a5ae008bcf335fe9", + "sha256": "sha256-HBpn1vTxhkJ6wWbrqg3YZ/WV1RRPySUlKwX/udGhVrc=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-3.0.1.pom": { + "sha1": "a7ac3d40d7a7c001067e92869c2784bfc1b013dd", + "sha256": "sha256-6JngLsisanP7nJx8uevZv8TOgmNBIoXV7G8t+SCICQU=" + }, + "builder-model-3.0.1.jar": { + "sha1": "e9de7e34339a8eb90fda30ac6bf580d5f2c4f282", + "sha256": "sha256-Y7RlzGKPjDBXArErJlAc6iNqMXXbFbWnNW6YSEqmPbU=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-3.2.1.pom": { + "sha1": "5d5177ffd5079fdb4c671ca61ecc63d9a8ff2cc7", + "sha256": "sha256-TSo1gzy5r+5NN7aMFigPivxk3g587NrBAi26+iRz4x8=" + }, + "builder-model-3.2.1.jar": { + "sha1": "3984b3e65c34b5d278e434270b63802451ec3e9d", + "sha256": "sha256-qfaOarzsEi+cta01LT8Fo+sDrLzcqV5NJcFjEMLJZf8=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-3.5.3.pom": { + "sha1": "64d0223622a2c59fb67f8fb5c22b6535efef29b5", + "sha256": "sha256-8kaKB3hYve1NA/D8BkGjtpr5zskjdqqjht3XoSTIQQM=" + }, + "builder-model-3.5.3.jar": { + "sha1": "775377503072e1b6a701c3ea923c468cedd4e2eb", + "sha256": "sha256-U1xpawkp6LOZLwiCPpTFtci+UzCVThX8TuFhCW+gT6I=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-3.5.4.pom": { + "sha1": "e6a529e2090de8d642b697b4067702b628e8ca83", + "sha256": "sha256-+hp/Fk3EtpNAs4AXTcLCZR72ZUTkQN9Xtd+VP4QzSvQ=" + }, + "builder-model-3.5.4.jar": { + "sha1": "ce9181b0c254dba36f8a1d5ae57fd0ec56c9584c", + "sha256": "sha256-ZaQgWyYfeJ6FwpF5uX/aI7jx6E5doJmgXFDCIsk8xN0=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-4.1.0.pom": { + "sha1": "4a0a8140b4445207a37e583b85c87fc246f053e8", + "sha256": "sha256-nNrjZWMeBCcLmp/ioyRwPRzZ+YW3Kn/jPPN4M8M/OAU=" + }, + "builder-model-4.1.0.jar": { + "sha1": "18c4f4036b1737bf2108f5e0d33e363866c2c34b", + "sha256": "sha256-YTxdwnnlPsPayCLLuHSuwziWbLzGZhR+vCOnZLhjj6k=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-7.0.4.pom": { + "sha1": "aee44be20c37bf3d97ffcd0b330caab9f0d4bd99", + "sha256": "sha256-USVRojVQmkejy6TMu+VhSLS7xQHcsFkT0L9qClXlNqE=" + }, + "builder-model-7.0.4.jar": { + "sha1": "33db284973b01be0323c76583a3bf82fc5136a9a", + "sha256": "sha256-arLyNx8rzUijpiNhPQvACDi1v7TmxOSeMk68+I9/6w4=" + }, + "builder-model-7.0.4.module": { + "sha1": "b3dfdfe2dd049f44dffcc5cc676934a55ebf4366", + "sha256": "sha256-miu7hLr8ijznSUsR0q+mJT1gRTiU6pyez24IJ3YhdCI=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-7.2.1.pom": { + "sha1": "bd155c2561440904a21d2d340eecc5e74b4b14d9", + "sha256": "sha256-CSNKYHIRNo6sV4E3K4JVneZ0gR03LF08jFPQ+gAvEqI=" + }, + "builder-model-7.2.1.jar": { + "sha1": "35f0f5aa396d2a3daa0ab20221d200375784a2aa", + "sha256": "sha256-fwhUeOstLIQxrHb4iYsG0hifWzZDEySVirvo1ZcLbMM=" + }, + "builder-model-7.2.1.module": { + "sha1": "e84495335b2506aa2a25007cd5356724a7cf96e1", + "sha256": "sha256-VoR82ulgZptfv4AbIJmqA1vakbw/zCldVkHXo52nxV0=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-7.3.1.pom": { + "sha1": "b53bf05627f9007383d61f850e16c18cb49cfe41", + "sha256": "sha256-YBkErgndAtVVTZ7VHRe97FCkBrtvA17J0ZzuHVlGvu4=" + }, + "builder-model-7.3.1.jar": { + "sha1": "fddc6058293b89467794f8fcd6bfb35e37739c48", + "sha256": "sha256-lzfgsYcxgAJ/WOmQKy/IMuDOItNt2dePyTpcoytspbE=" + }, + "builder-model-7.3.1.module": { + "sha1": "ad5c4c7ea01a2c3d4aa1914e7a34284e1c2e8b84", + "sha256": "sha256-GHOaABpMvv150ia+49Q9tw0wsQ+sdDkSx7iexTg5/v0=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-7.4.2.pom": { + "sha1": "2f49db123b2b9874280b93ec0f59a0c96d1cf59b", + "sha256": "sha256-ASRhJaAT5Uerk0CPw9RJMf75Hu7KIA4N/ux9kTeUC5U=" + }, + "builder-model-7.4.2.jar": { + "sha1": "80c54f681d4ddf8fe2ff1df80bb767983032b7b8", + "sha256": "sha256-kKiCQFf9YTB4qNpC2wxbDqHPqvCPcnEIfV20RpFnuoY=" + }, + "builder-model-7.4.2.module": { + "sha1": "96336e1b2398ba49cb5c61c93853036c4a543a94", + "sha256": "sha256-IsaZIHYBjYXgRF/Dw6nJP+pSsH/+8Ar19kCtB/R9qJ0=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-8.1.1.pom": { + "sha1": "6e607f43ae3e72297a79fe282ddae76b2a15f124", + "sha256": "sha256-xIgs5eEHhIzR7SQYkE8wwA8gLHKLd74Kk9R9tp7GiwY=" + }, + "builder-model-8.1.1.jar": { + "sha1": "c7c2b321263e7d8e9b6868aaab53e1f72e636e7c", + "sha256": "sha256-VbpaOVF9HgBH0leg7w4cgyQQimZ+oglsAYy40Mn6VMY=" + }, + "builder-model-8.1.1.module": { + "sha1": "d8a304e0c0bea20477125a27ede9fcdd1990e2ff", + "sha256": "sha256-BiLMS0CBS809ndYPH9nkcpto8QdZMduX/ciT0nUa6GA=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-model-8.2.2.pom": { + "sha1": "03a01ee05383b5748b6f512ef0e799ce0cbcc738", + "sha256": "sha256-C7WpChgbyO4dElO9t5vbDKEQBI8CZSCyCU/SdXfp0jw=" + }, + "builder-model-8.2.2.jar": { + "sha1": "2335b64f5856414571f74898885d8ad86a6bcb3b", + "sha256": "sha256-ShDJ9O1vu1djOfjKfHeGeSPHVm71orEi9D81huWWS04=" + }, + "builder-model-8.2.2.module": { + "sha1": "b7a8ea9847115f8e5a44f3054dc88b466364e1ea", + "sha256": "sha256-1RRMgP6eHkeoOS7zlatPM1ZQHt4zsv5Vm4InKEOHb9A=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-3.0.1.pom": { + "sha1": "ef4ababa08f89af4c1b0a1217780ec5abcfbbea0", + "sha256": "sha256-iwqop06ZDKjWM6t70ZOFMOkhVI3xP9QCAiSmEGU3iHY=" + }, + "builder-test-api-3.0.1.jar": { + "sha1": "05f084a2888a7f638c4c89c3278760345b1fb4c8", + "sha256": "sha256-52ehJIrbSEMRBFfoOeUEwlEWSe548FpqMR6n8jV4iU0=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-3.2.1.pom": { + "sha1": "7491225517d80b5dde854365e2d299926b3d7962", + "sha256": "sha256-FsTibejJzlCaLoHrTFcdXWpueS2UJdvHENypN5Ok4S0=" + }, + "builder-test-api-3.2.1.jar": { + "sha1": "09c0b9848b34d79d71f911ebfaf6e0d606e7a3e6", + "sha256": "sha256-UzrGwrWIS7VJZ6M3kfJijf36x5ga85QXozO0PUN5tr4=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-3.5.3.pom": { + "sha1": "f910b8864ae8e3e3c39823ca7ca6ba99a09d7564", + "sha256": "sha256-9IV0lJ6wAgzUWwuGES1aMXxpH+0OPxPYzY7VNbmksUI=" + }, + "builder-test-api-3.5.3.jar": { + "sha1": "cf9ce50cd19a816c5bc7d4c6362699aa98c8ac0f", + "sha256": "sha256-euZFcKEsjav85tiA+KSNsVqoMRHbcC2iTAWvv3H8FUc=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-3.5.4.pom": { + "sha1": "b201367cc7edf91447be7cda74f26cc6b5ed3fd0", + "sha256": "sha256-By4nSqwYo/hXe0VW1Xrtyih8a9SS3M9Xa69V9Fo+c/g=" + }, + "builder-test-api-3.5.4.jar": { + "sha1": "d7c771aebbdcf2e07dde87cec1339c6f28e12636", + "sha256": "sha256-WIfg0QZ3Ue/qAaL9Or5caNU18rJV+aiqsQSiLzJ7BeU=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-4.1.0.pom": { + "sha1": "9a77c7e214ad27bb5a1bd6d82cfc4c56fd924b9e", + "sha256": "sha256-VLrSHbL2se1dUp0kuSuPNWAzqhIch7t5mkKLj3KGtAA=" + }, + "builder-test-api-4.1.0.jar": { + "sha1": "dd08119a1cfe1789b8749127033470cdf919e412", + "sha256": "sha256-QVyUHJwULfKRJI/bTiTwSrH6P4+ZDNoV+hk25gdgLbc=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-7.0.4.pom": { + "sha1": "256f101bc57742ac1dc5ac78b7f0382dc8456c25", + "sha256": "sha256-2QpTTEq9KzyC8CK7GmoS9TD37/sKpzXu7fGQgRrjm3A=" + }, + "builder-test-api-7.0.4.jar": { + "sha1": "fcc18ce9ef2347c04ca686c8f9f17b166f6e491b", + "sha256": "sha256-z8+TMinjaCMalrebHsOU18vPgi6vijukTJK6UtlZyQ4=" + }, + "builder-test-api-7.0.4.module": { + "sha1": "36694ec661d2ab47d6e64cea50d3155a099fcc95", + "sha256": "sha256-7F/C4maDgvHMR4saGAteVA+MPF0MOU9u+H1bAbiQJBw=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-7.2.1.pom": { + "sha1": "fb239391a41db719d1eb110d29b01a52d0e4188a", + "sha256": "sha256-5fWFCD+6wZAeRJKn4TyCmeroBbtezH7NQlSEIUBZTsM=" + }, + "builder-test-api-7.2.1.jar": { + "sha1": "11cecda6e79a9b65996c90c65c5f8f5c06bddfe2", + "sha256": "sha256-4kASHB4GmQUi8YAtxPPoU1hOYmboxNnfl/QHW08LJXU=" + }, + "builder-test-api-7.2.1.module": { + "sha1": "6b321f9310bfc0b47e6c0d393091d663af0a829c", + "sha256": "sha256-RjZrRnTahJ/evSq3SNRDRSI9Wv1TJvxx0q2Vu7Oxjx0=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-7.3.1.pom": { + "sha1": "ce2b9f4c98b5eee1a3468075a0673f94fb12d67b", + "sha256": "sha256-7fYyG8x9s8p9xr9eckKHjxfnJULb7ACJGbC61rkmP5s=" + }, + "builder-test-api-7.3.1.jar": { + "sha1": "5f4b0978a05b1ff995ca255da73fdc76f5b99949", + "sha256": "sha256-1SzCaYJsjynagRaH5ooB2Z5+oHIndJ9e4N8IeZshjyY=" + }, + "builder-test-api-7.3.1.module": { + "sha1": "5651cd12c87d15618a9a123419cbf747b034f2ad", + "sha256": "sha256-ZsaAxrcO1wBNdVSCFmtLHAEQVHFOgbuP/IbU70lXpC4=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-7.4.2.pom": { + "sha1": "f25ec3b17205f4408e6d5c3952a2c3835331eb2f", + "sha256": "sha256-Vzvbu/UQtdITa8HWz6ub2GvrRg0Jf44JOLfpRhYlf6s=" + }, + "builder-test-api-7.4.2.jar": { + "sha1": "dc8df869e33c39136f0dc390ae6b207b39402820", + "sha256": "sha256-KVgZUklAY73vHwBKzNhjK+drF/2ev7wM4xKsS839mRQ=" + }, + "builder-test-api-7.4.2.module": { + "sha1": "092317a662d2d637c1be3169a981b724f7c9957d", + "sha256": "sha256-6W0Ef+pMlu7g6rTfGpH1m6sNa4yvbjpp03ka/aUMuW8=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-8.1.1.pom": { + "sha1": "6f4d5e88a99ff0c6c6f695dff14ea6e47412505c", + "sha256": "sha256-0A/j4H8lSME80di+yh7V735ruYhDVrlkRrvwmIznBf8=" + }, + "builder-test-api-8.1.1.jar": { + "sha1": "5669c77c28e07cfcdaa65f4615c7d3ea3f2da649", + "sha256": "sha256-mpscZRpuYTZin4DSB5X1WYCNxY641VQZ5UNXlDqubX8=" + }, + "builder-test-api-8.1.1.module": { + "sha1": "13dd4fc0ace60c3fdc6a79a6fc28388bfca59c07", + "sha256": "sha256-qdSGv3FTaMS1yETH0hiGJcZXfrbbFNqMkFG8lTmDSdE=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-test-api-8.2.2.pom": { + "sha1": "16d7eb99c9ddc4a0eba32bb413c1628dd4bb8043", + "sha256": "sha256-pwjqkMf32jNbZun7QeJPRjrgKve5Lx8MPo4G1TsbFcc=" + }, + "builder-test-api-8.2.2.jar": { + "sha1": "4f45dab84a8696041770ec2f7998455f04d771db", + "sha256": "sha256-OyYKGLfXHNLieD9OtKYg/O7jQQDpGc7UZF5oB/YQO9s=" + }, + "builder-test-api-8.2.2.module": { + "sha1": "3bd40bb4a7586e1e154a30ea3a95448b8d1f02f9", + "sha256": "sha256-mSOlmMbhx8hukPudqcQ+i5xwN/IyiwnWCgenC2PVat4=" + } + } + }, + + { + "path": "com/android/tools/build/builder/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-3.0.1.pom": { + "sha1": "00bdc803975a0159610829a018c5e19fd4cb193d", + "sha256": "sha256-XcMnxpDQMaEaqPn0zcOR+5OPu3x8FDzEvGDdVz0f0ck=" + }, + "builder-3.0.1.jar": { + "sha1": "1f896967507729bb35ac727c03d3b9306fe87b7d", + "sha256": "sha256-gDvjlO243X0eZ4aD7YWu5/hqFyyQ8D5hmMiknU80K5k=" + } + } + }, + + { + "path": "com/android/tools/build/builder/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-3.2.1.pom": { + "sha1": "0b2116384a9cbf80be799fa327c808ac5e9e42ad", + "sha256": "sha256-Y7OsUg17OqThBUiS+TgOeskgr7vw0x6gq5rTywwr6E8=" + }, + "builder-3.2.1.jar": { + "sha1": "1303a2feb969ac0896e7c83c1f5a0fd2496b71bc", + "sha256": "sha256-rty/0RXb6R0JtBE+Zu9QWJtVjQqjsvEzsdhnybh/roM=" + } + } + }, + + { + "path": "com/android/tools/build/builder/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-3.5.3.pom": { + "sha1": "b327e842abd63da2281e74c59de69ab2b5998443", + "sha256": "sha256-dNWTs6MgZG6PVaytZxU/2aWQk8a3lqkGYmbXPzO99ZA=" + }, + "builder-3.5.3.jar": { + "sha1": "39bba1af533b25f6c1140e3c795ff815a6ad86ba", + "sha256": "sha256-BcvZGDubmg4SisvDkSVAwXLg11pcNY2kZcb3F5JMV0E=" + } + } + }, + + { + "path": "com/android/tools/build/builder/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-3.5.4.pom": { + "sha1": "ef68ad9d71bd9336d25de233d96b1b0927233d5c", + "sha256": "sha256-XZpBbpABlu/SDriSfT0DlYK4ryoIDR5fhYri+367v9w=" + }, + "builder-3.5.4.jar": { + "sha1": "287ffb45bab7531f677cb1411b1f157b43d1f4ab", + "sha256": "sha256-zXtfqbVLprqAK95xWFm6BQsgE5Aqcr9cdDvKwBl+qLg=" + } + } + }, + + { + "path": "com/android/tools/build/builder/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-4.1.0.pom": { + "sha1": "60cc7860788cb8f4cd2a98786b38e03ba6ac25fc", + "sha256": "sha256-rBjgsdkwXF7dRDByiB2xDFIU9pDHQJHHuGgslMylR+s=" + }, + "builder-4.1.0.jar": { + "sha1": "aec0494498d0c8b9b46c226bf0ff02d73a299cac", + "sha256": "sha256-UBDsvxcb7iSeXw325ONCRCnFk5QST5Au9XmIdDPqPH4=" + } + } + }, + + { + "path": "com/android/tools/build/builder/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-7.0.4.pom": { + "sha1": "44fbdb1614c87020abf2628dd9b175a4e90901d6", + "sha256": "sha256-Mkttu3EyJBZdXaz3E2y0aONIBzK25sfjr2kzFupX1R0=" + }, + "builder-7.0.4.jar": { + "sha1": "5d69ea9607caf9156a8bd0d95fa53598d1bbb59a", + "sha256": "sha256-JjMs/JVAaU+JBL9SQOtS7atmggzM1hfyEsbm3xQlk5c=" + }, + "builder-7.0.4.module": { + "sha1": "81c264c631331a30d7c92186698d285f46cb1b12", + "sha256": "sha256-fEt6nXhwiYaeFFtnLPzVa+nzk9KzDcjioeRy+52Ih+A=" + } + } + }, + + { + "path": "com/android/tools/build/builder/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-7.2.1.pom": { + "sha1": "fe118f9cdb6b95cc20166630374674dd51e43042", + "sha256": "sha256-oCDfomXlPruBd7tBEN/1VZy8CgjyV57GfYV+MyUPW3Q=" + }, + "builder-7.2.1.jar": { + "sha1": "37e17c1fbf53d8aed49942725d20e60af7fbfe27", + "sha256": "sha256-QbeFfqXjDBUQq5t8YQ3y4fY29Bl4NRtRMTQdnvuM71Y=" + }, + "builder-7.2.1.module": { + "sha1": "edf4f2dd46860a9d341c1cfc9c2b5be0539261e8", + "sha256": "sha256-86dNd7exp4Vm+FQ+o2h3gEpyMJ+XHi/PZ/f1V0XM1+c=" + } + } + }, + + { + "path": "com/android/tools/build/builder/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-7.3.1.pom": { + "sha1": "ffe1d078db3481b478c089a1e50435bd43c72dd3", + "sha256": "sha256-rSxF9toU0zWW30JFLeR/tpRVvDSb82XHSK3Ixy9U+Hc=" + }, + "builder-7.3.1.jar": { + "sha1": "3e23401ece1637cecbae6be5c02cc18ab7e452e5", + "sha256": "sha256-U+VVYstC/xdMSQd2GO2POFewqaXj1AVf3+yyh/Za5Pg=" + }, + "builder-7.3.1.module": { + "sha1": "d891b27986186ad40dfa2a38a8d065e7873272cd", + "sha256": "sha256-UTu+Icro2gup/j9GpnG/o262g/IAFqn9Nlfk2gus24Y=" + } + } + }, + + { + "path": "com/android/tools/build/builder/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-7.4.2.pom": { + "sha1": "b643b56201ed209c09df8b3e83e1e8a968f6b428", + "sha256": "sha256-bn7RcxAaGvcCAs5ldnUCz1JpVCACAMJ+zJG1I2Pvj5Q=" + }, + "builder-7.4.2.jar": { + "sha1": "e0cb985a4d385b6c8e8155e13df8205f459ecac0", + "sha256": "sha256-NGykWb07SzoRSPbIrTG2kEnoTyaRMu3dbSji1RPdNg4=" + }, + "builder-7.4.2.module": { + "sha1": "28dfc9b83c71dd2a70cf67fdfbc7ce410a387cb3", + "sha256": "sha256-hAQFm90J6AgdbL25RcbgyijWOnNwzQNv5GpUHnsPt04=" + } + } + }, + + { + "path": "com/android/tools/build/builder/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-8.1.1.pom": { + "sha1": "fe8fac455630ee91f84019a5df64340cda55d60c", + "sha256": "sha256-a52JEcSvoJ0xRtKXeBqhyr8cYkkBa9U+GxDuy979Q8g=" + }, + "builder-8.1.1.jar": { + "sha1": "5594d3763845845e708896806fdc639a145f47c9", + "sha256": "sha256-cPqydxzndAIvxUGbgtSNTJoDCfxyfMaq1OV64WT3jeg=" + }, + "builder-8.1.1.module": { + "sha1": "eb48a025e33df770e23bee988b218849cb1f94b7", + "sha256": "sha256-m4nTQoaLQ+yVvArkbk9itxosc7xirqzyX0bDs7yTpNQ=" + } + } + }, + + { + "path": "com/android/tools/build/builder/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "builder-8.2.2.pom": { + "sha1": "4c13f2f3afe26d0522ab96e820efaa43bfe54987", + "sha256": "sha256-Ysb92C/ubl34NG+LAezJjkTxxoV27Nqkp1BiYTkcUjU=" + }, + "builder-8.2.2.jar": { + "sha1": "fc78d5edb7ef5ad6012ecd1f96db39ed87180594", + "sha256": "sha256-kE0RjhsdiJklGLxjKbsvWWAWZB9bHZfJvZP3h6uGTCE=" + }, + "builder-8.2.2.module": { + "sha1": "dfba407412e7a813182d570fe3f6560f2240c334", + "sha256": "sha256-jYOfpCJ89WmamQHJmVW5x9+nR2QUsbdwJNrb0DRHhQg=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/0.5.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-0.5.0.pom": { + "sha1": "37b0f576d869aab168033ee8b21a54ffc307667e", + "sha256": "sha256-fJu/q5YqzULoUmr5bkpFSipEyERBVDia6/973gVCHC8=" + }, + "bundletool-0.5.0.jar": { + "sha1": "ffd6077d13937213b860c0022174ece60000eabc", + "sha256": "sha256-H95zgKEY5FS110nPGyq91WuzGZw6z49h9QhHdOCSuDE=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/0.9.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-0.9.0.pom": { + "sha1": "c54b8b912afb54d6393182adc99489917e3a1a7e", + "sha256": "sha256-Ek4SQieANom3HadEDw29/3HLtMZhoY4PTi4NOoDTZ8I=" + }, + "bundletool-0.9.0.jar": { + "sha1": "6c7492aedf12ff02ed7c634246026ac7f7372dd2", + "sha256": "sha256-UMRFV8p1sGjjZKw1pki4HuunY5q16wQDbjkMcnrS+j0=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/0.14.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-0.14.0.pom": { + "sha1": "804cae85e2510716c9811045e1cf21ae842a4010", + "sha256": "sha256-kXAzHPe17ttA03O2qjpDPehAujgO3+RoPCLTiupvky0=" + }, + "bundletool-0.14.0.jar": { + "sha1": "e7ecfb48a6d43f4ec23475c9d04b96c97ecc101b", + "sha256": "sha256-lsXOqbO304Q6EG5oG4d6D6kkG/gb4UqqflQAgccsiMQ=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/1.6.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-1.6.0.pom": { + "sha1": "761051e82f824dce8bc2952487951b4290ec5551", + "sha256": "sha256-0lGuXF/41LzS+hitdAg7tryQV5TM8rFoGjgbe6yD8E8=" + }, + "bundletool-1.6.0.jar": { + "sha1": "6c112c1d563b30e817e4185ce46d139a5faccad1", + "sha256": "sha256-ThaMn9WrhrJVP0P9QIkctwuY9PD1P9UVUtcTB5BE3mQ=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/1.8.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-1.8.2.pom": { + "sha1": "d9a48d38ff609ca572d87fb0b36ead5ecb8a3a38", + "sha256": "sha256-DyFeztbFrauefPfQ9clJeW0nPxnPQY1B2cMsGcw+2QI=" + }, + "bundletool-1.8.2.jar": { + "sha1": "e2b55df817264021b21208fcf01b93b8d2cd67ce", + "sha256": "sha256-Kq0FYc3vP+KGEH3/APTowCikUabBaPst0wLbdjr9T8w=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/1.9.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-1.9.0.pom": { + "sha1": "11887c6139361a6f5abd709cb3504631ff0fbb49", + "sha256": "sha256-gDGq1ykM9MJ8TFND1y51l/1EoNUiadrAJxSTO2atYz4=" + }, + "bundletool-1.9.0.jar": { + "sha1": "e144fc87490d12ee601c79dbcb60798a23d1b702", + "sha256": "sha256-XdUBIv0xwhe/9foJTdFkcFhTQSDYtwkZSjyouYTZwJk=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/1.11.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-1.11.4.pom": { + "sha1": "013449e8b780a0edfafb5e3985a1966a8b5510b7", + "sha256": "sha256-lInY4+4SMKBqLXfUXy16D7eG9zdjCjGNMDmR16G4nxE=" + }, + "bundletool-1.11.4.jar": { + "sha1": "7834f285553cf91a2a36f5919be1ffc4ce28ffda", + "sha256": "sha256-J/mFA44CCZfH0MI6bNu1s9HenQkfkmdI3usmWqYqriU=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/1.14.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-1.14.0.pom": { + "sha1": "62cc91c21513f464f7de9f41b1878d6704a7a28b", + "sha256": "sha256-096nX31DnZKoKQ5UGpnd7O062KQse6K6AsDp4GGIR9A=" + }, + "bundletool-1.14.0.jar": { + "sha1": "2fcb426b5e62cc6e879369fdbbe915bb0a43c3e1", + "sha256": "sha256-jkTks5bxLcL6VdQJhc6wixvtIiB3pd7br960baj7nQY=" + } + } + }, + + { + "path": "com/android/tools/build/bundletool/1.15.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "bundletool-1.15.2.pom": { + "sha1": "79561bd95cd47a829cbc722b58885a4678372aca", + "sha256": "sha256-LIzgBJJuvZ9nopHAzh8BEyv1/Kp+sJPKKIFXLQU8DCQ=" + }, + "bundletool-1.15.2.jar": { + "sha1": "35993207f8440e64f2c9db15a5926a85fb17e533", + "sha256": "sha256-IDSudU8K1vVEqx8gO133Hl0n9hE+VQmjGF98IkEAWys=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-3.0.1.pom": { + "sha1": "f690377f3de82dfb540ee80dd6b4cd86d9cda1c1", + "sha256": "sha256-4vexdhD58KK1XAGbdie+PULsFC/eHbXK+KNNvju4280=" + }, + "gradle-api-3.0.1.jar": { + "sha1": "92640ae3c543aa3faee7b954ce641d03949d4401", + "sha256": "sha256-JGM/seYqnksaJeLBdR1ovongULSIZSRW5UW4AZU5E30=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-3.2.1.pom": { + "sha1": "e1fdffe59af4366be13486055a56e95c5be928b5", + "sha256": "sha256-EjTrZepcL8yTzO9mfOMTWtv9uaw1aPENEdikuYlHTu0=" + }, + "gradle-api-3.2.1.jar": { + "sha1": "825438df08a0c323b7d315071d402ff8fe700a42", + "sha256": "sha256-V88Kxawdyor9s/YrlCZed259z6ZBzDhE+1OgUZPeII0=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-3.5.3.pom": { + "sha1": "ef22e3a71efd8cb517d4b039076f4b69013c0208", + "sha256": "sha256-1T+FRJWgpHkt+LVOWXFAdtk3HW9ah5AABwdUoYDS7kU=" + }, + "gradle-api-3.5.3.jar": { + "sha1": "c1d6a6b3e5b744caaf6eea2452d8db62c8e2882f", + "sha256": "sha256-yOi/eI7S+LRswAqWJOAKivQLwx/x2CmL10DxhAC9PS4=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-3.5.4.pom": { + "sha1": "6522e0a98fdfff19d417c714957c9b8f3a8e9398", + "sha256": "sha256-SCaIlLlCiZvDBff6nAfmPGtof7toBkfgs96y7ieb6z0=" + }, + "gradle-api-3.5.4.jar": { + "sha1": "b7e85ab17eee5cc049702b127512c1993a85b5bb", + "sha256": "sha256-uxs+kpirHLiM5tra+E2Hnurtl+3XSWaSb3eQopZMFm8=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-4.1.0.pom": { + "sha1": "97e0350f42820f9a228c67895e334b142d1a0b0a", + "sha256": "sha256-u7y3OxJCktzdz0N1GwDk/S9XAv5UfXt+W77tpHbvqDk=" + }, + "gradle-api-4.1.0.jar": { + "sha1": "83d3f04ecfd6c6870ba511c817837868a6993f3a", + "sha256": "sha256-ed+G5v1aq+w9RZaPBPiXf0PsRV2QxJI/mqSI4j+TpuU=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-7.0.4.pom": { + "sha1": "cf2e47ad5e48aad28d95a414565495e6ec6e77f4", + "sha256": "sha256-cowLRQ7j+HzG1tO6tHTLs6uGz0N9rGWjfgs85jkt3C0=" + }, + "gradle-api-7.0.4.jar": { + "sha1": "da92e074b345da0557a37db65042ab9bf6068c21", + "sha256": "sha256-BX8tOM2zWfPJyC7E62hump/lCcprSgpfmVZezhrNiH8=" + }, + "gradle-api-7.0.4.module": { + "sha1": "f7da6025ab82ba3ab640accf8759a491ee14c811", + "sha256": "sha256-bjQb2aFdEEta7Tit9AS6LjipcV+q4jxBt9ewlZfwb14=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-7.2.1.pom": { + "sha1": "f7d8b5859515795b52b95470f8e29ff52f524b8a", + "sha256": "sha256-u+wnPR/AgFgumnB6qysI9m1f+x7TMohmKNYU/hTdLDs=" + }, + "gradle-api-7.2.1.jar": { + "sha1": "1c2fdf9606aadaa7f7c837db74b99fa781d84a8f", + "sha256": "sha256-CwgFPJHdOzl2MG5PVzrRmfkECA9CDG09bnF6aCOUDoE=" + }, + "gradle-api-7.2.1.module": { + "sha1": "dcb374dae9c2119c70618ac485aad7b33cd8aada", + "sha256": "sha256-gm5U7mV1RBTk6ilqjXkngJ8J5ShzIpnhO2DPpa+IngI=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-7.3.1.pom": { + "sha1": "d55d6182fce83ab5e880643e3cd45408600a4137", + "sha256": "sha256-ztxRVD//QtuoyMR0Q0n5HAm+JUCNZ+ispPsUmzJF34s=" + }, + "gradle-api-7.3.1.jar": { + "sha1": "a2723750277a10ca522477752d98f996653ebf14", + "sha256": "sha256-WmYTFdG3rdyr+pgcql3awMWbBDWArRiIZyTfFioKZD8=" + }, + "gradle-api-7.3.1.module": { + "sha1": "4111d793865d0c3b8cef83fbd35ca65e9401554e", + "sha256": "sha256-FqiNo1wMryJjs4iJO4zIKqNkYqfExnihpu2iaIilMio=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-7.4.2.pom": { + "sha1": "e4a792e07feb15797d13be76e4d0f02c1c5e064b", + "sha256": "sha256-Mwtq4R/ztMbCULDSjYqeuo2NWVI688OLfJRKj758lFs=" + }, + "gradle-api-7.4.2.jar": { + "sha1": "5f81693f719b0472898c69dafb42befbb18de887", + "sha256": "sha256-jjZmZBtZJ4UbgwqKuA75DM7DeAMFx8xOaXIpERRASus=" + }, + "gradle-api-7.4.2.module": { + "sha1": "8bfa5b9053d4e0e85a5615122c674c87661688e8", + "sha256": "sha256-a9nrzYbXCA5vUlZ0eNYF9iDkKuVD4HySmY+0Aq+ADuI=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-8.1.1.pom": { + "sha1": "972c2ab3b4b32ee9f4110584d316ac2ec52ad60d", + "sha256": "sha256-DXD0uqUXuhGa2APh+0iO+Q3ItpG3w45mtOqRf+PCsQI=" + }, + "gradle-api-8.1.1.jar": { + "sha1": "d36f6beb6e0b46efbe7ea40179e2e7952c5c7e66", + "sha256": "sha256-73fskWn1Nt/VUOzxgWc1qeRb01jpXPWsnyXvOSrsV+o=" + }, + "gradle-api-8.1.1.module": { + "sha1": "61e90f9cc0d1001f854e83e36e6a670f0e4b1c7c", + "sha256": "sha256-/YzHAkzNZvCWYAnLrCKn6pv8RadP1dWyoOhX9vBphhc=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-api/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-api-8.2.2.pom": { + "sha1": "cce375d0aff3d5f260618dd1265340deecb603a0", + "sha256": "sha256-7RwEc9syuSi46qLyWyOk6ZzQDozK4kfSuGNhp7J1qro=" + }, + "gradle-api-8.2.2.jar": { + "sha1": "ff8f3ca6cca706ab812699276be6c64a6a9dd28d", + "sha256": "sha256-FdH7XoktC/A/NxJxz2e66V246JCC0SqLWBZdSfQohkM=" + }, + "gradle-api-8.2.2.module": { + "sha1": "b75dcbebc06fad428d7dec4d61a400bc0aea2066", + "sha256": "sha256-BMFsCluOkpQ6TqC3JBdRe6j2dHuH4ReCziYrEgpRuM4=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-core/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-core-3.0.1.pom": { + "sha1": "13a9e82c7724e138dc6cb8cf958ca9fcbf0db8ab", + "sha256": "sha256-I+bQBvfQe7Q+vICy5TDIzdZburyq+RHD8S4uomFnF4w=" + }, + "gradle-core-3.0.1.jar": { + "sha1": "c2945119335b491ca56e4042f7c8c55dccf5c9c2", + "sha256": "sha256-CmQPlW9M3ei100WKZ8pnWZ7rb0c0kWej8a/OfWCXgCc=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-settings-api/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-settings-api-7.4.2.pom": { + "sha1": "0a8e8acbebd14ff3c15889d1cba9ad75ffff8088", + "sha256": "sha256-/pZglgLJjkj0JiwNbaif1+smf0HDL+7aZMjWRFLvO9k=" + }, + "gradle-settings-api-7.4.2.jar": { + "sha1": "bb22600dccee35b401ff17c5ca1bfc6d853d15fd", + "sha256": "sha256-AmCLjqIXGBrdNTv6MfVr7Y+yrmtcvqe+x3IoLUY+S2I=" + }, + "gradle-settings-api-7.4.2.module": { + "sha1": "1a486f6f403553b7d0d9c8c7157ffab744a7889a", + "sha256": "sha256-ipoHZ/+ifPLxUFxa4+Z0gP7lKx8YModl/ejI/z7v+KY=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-settings-api/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-settings-api-8.1.1.pom": { + "sha1": "6b6e57bd13be0edf27d03d448d998a34aafcace2", + "sha256": "sha256-zlJL5ZJrPNynDNCPdX6zNKifCJvtTuokgRTmmxPs228=" + }, + "gradle-settings-api-8.1.1.jar": { + "sha1": "f2cc372826954967fa44fc51b4e53124559b5e01", + "sha256": "sha256-GzWgye/V+rribI30jDBu9KgCxW4yrQ+JrGbDIuilYcg=" + }, + "gradle-settings-api-8.1.1.module": { + "sha1": "53f1056c60a7913a394c879e3086e7f67bf9ac45", + "sha256": "sha256-T4eTyX+dy+d0wvTHsKUyURUJcipbWKcpSyGD0dvvm6A=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-settings-api/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-settings-api-8.2.2.pom": { + "sha1": "b2cb9162c5564680e928ce0c0fe150f6e1f6279c", + "sha256": "sha256-u9IdSbSHbj5QW7NPkXGU8clXSkbRINBARZsDB3dSjpk=" + }, + "gradle-settings-api-8.2.2.jar": { + "sha1": "c89092f3baf068ac86f33b3681d1a353d9f043cb", + "sha256": "sha256-IIlHPI1NSAqQZ0EgwI+kkYjViaKDhr34iKSTvuIlP6w=" + }, + "gradle-settings-api-8.2.2.module": { + "sha1": "13adc16c41d41a4fceafcffa6a4bf7fcf4bf7766", + "sha256": "sha256-gmi7gHcsXKSJxZEJGU3kKHBjNHRIfS3XoQ/6xI+pPVU=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/3.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-3.0.1.pom": { + "sha1": "f6941bdcc20f1efd54b55db56be1085bbe24e554", + "sha256": "sha256-HvccJanUauDOAj0CpXNOCwb9+TT7xZSOIEglJrIrQXk=" + }, + "gradle-3.0.1.jar": { + "sha1": "cfa107d4db0c11c11f445a11e768adf61c72ce06", + "sha256": "sha256-/Nd8uk6jiTQphguT2l1D8uCJ8rqL36l+9yT0EPQEFSo=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/3.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-3.2.1.pom": { + "sha1": "15d3336376c1847b8c51a1d398599eb7f4aa68c4", + "sha256": "sha256-LgYrUSYVYGN0kB6vJTomxGp7j9p6BA9+IKwBpMW22Ms=" + }, + "gradle-3.2.1.jar": { + "sha1": "1ce0d907aa7805e19f553807b9bbdc9bb9841dbf", + "sha256": "sha256-EIofeCMBBSHDSEjaM2pn1ggkbbjoYXSEN/UuClqNzHw=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/3.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-3.5.3.pom": { + "sha1": "6bfb2b4c34c37c4f2addcb6d99db19fa7a1a5810", + "sha256": "sha256-k8NC3tdmWdVBdyaKn9lBLf4HLozxd26vtHxZCBmOZdI=" + }, + "gradle-3.5.3.jar": { + "sha1": "12e36e5f81cc2face28e0c14cb98941d773cae6a", + "sha256": "sha256-Zyk97dM5uJypz2xEAJvNj4Bb9CnCVYYuq/LeuBDYuRA=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/3.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-3.5.4.pom": { + "sha1": "69e3655fd7c0b419cfb40b07e5cb5e9dcabfb8ac", + "sha256": "sha256-uh5ckGwXFW8s+Tg9h+3ZAvhevFi2G3Hr1GjsWkoc1D8=" + }, + "gradle-3.5.4.jar": { + "sha1": "a72c4b804b2bf0bcee270adf0bdf2e83fa9ad50f", + "sha256": "sha256-wrht/NNKMeXh1NFa8wAsWr6u1mwHaRCRZjFPKVK8rz8=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-4.1.0.pom": { + "sha1": "dd32facae8d761b1cff013273422aecb5a441662", + "sha256": "sha256-JmAmsxTudQvsaQXDRvDqMdPi/KtsfitWeLruA9ZsGfY=" + }, + "gradle-4.1.0.jar": { + "sha1": "b505c73b43e30569c32589b8638164cb650282d2", + "sha256": "sha256-VfIaEzwIenMvECgI6IHWRqVRc4/Qgb8f1Q63MUHJS1g=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-7.0.4.pom": { + "sha1": "b4ade81e077ca22131addd218b6f4d2a99dec393", + "sha256": "sha256-iQ2BEiqtcwiDe3nEmnzTelK68gdiOH9W93hMPIfn1JE=" + }, + "gradle-7.0.4.jar": { + "sha1": "2f429abf8c8fe6ef67b7a96174b2073f9cc11f5d", + "sha256": "sha256-wcg9LfrZRmf79OugJZPBFRxkrfSix3u5cGCQjqtgTxY=" + }, + "gradle-7.0.4.module": { + "sha1": "acb9681744abb0f00156d51a1d45b11776014b26", + "sha256": "sha256-Ul0pULKDfv8ymTW3+KUnZAaDnniO+SfgduAEn7wdSt4=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-7.2.1.pom": { + "sha1": "656e1e9c7756e2a32ae4105320fdaed0b5d993b0", + "sha256": "sha256-W9P0kc8gtbtNiMR8M1nX60eZR+7Yg/9P4W0NowcXOJg=" + }, + "gradle-7.2.1.jar": { + "sha1": "51c17e0a7eea8eaaeb39b7e7a1e85a84867dcf2e", + "sha256": "sha256-fxxTXP8WlTfrXTsw/vXwDxu6kvvCDLJIbEzg3qQ9Nls=" + }, + "gradle-7.2.1.module": { + "sha1": "0095ab666877d40f86be4f8b4532e26341b9f7c8", + "sha256": "sha256-bu01k6TZN6p7KWm+6C8HAiE+YCKLnMiDATIeWQH/uJw=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-7.3.1.pom": { + "sha1": "543598c32290c69cf88e272e6a074b8fd9b764cd", + "sha256": "sha256-ZVe+j/XZUHodOh06X0NFh9BzwkZ/n7MXZ/J16vUpDnw=" + }, + "gradle-7.3.1.jar": { + "sha1": "e05fdef1532d40d90ef6d2f80f76dfc78d098bb9", + "sha256": "sha256-k+AxdL8Y9BVUt+52fJCAk3pe/lAV7JvfjMl1UNQn96U=" + }, + "gradle-7.3.1.module": { + "sha1": "58015a2cc37e8f7a8885c9aea93d4eda3232820f", + "sha256": "sha256-WB0R7MT7s0FCtejAetMMVhpKOM3D48vhWA6w7kjHIz4=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-7.4.2.pom": { + "sha1": "6753122b63a7fe68117e025bca305649cc537460", + "sha256": "sha256-sjiy6E9wR81iNmbh8BuP4UtuN6o14BgbzatNUx+FJU0=" + }, + "gradle-7.4.2.jar": { + "sha1": "087cb2d87280562820906f2a1ee3a12b047359a7", + "sha256": "sha256-1CZHivljvSJ2/rhA/Je4WQ7bE5JnD1h+g4/WP4KfvSQ=" + }, + "gradle-7.4.2.module": { + "sha1": "ced302bccf59e346c56b1aa63931d9a23952c7c6", + "sha256": "sha256-4YboI7e/gobXVz1TT3o1YusRu0VojfzfufGHisAMEV4=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-8.1.1.pom": { + "sha1": "aa69b6a9fffc015aab25184d09235dc5e924b540", + "sha256": "sha256-39NEMLA6cpB3m9be1L4Zkb57rgT6Kn6OsYhAlxdhFAc=" + }, + "gradle-8.1.1.jar": { + "sha1": "8c2d62f29b154bd15bf4cb5122a8f5a52f183855", + "sha256": "sha256-Vhd3NtPxVxYG1klatkGQzwM5ShRSa8TAzLy2F4XTopg=" + }, + "gradle-8.1.1.module": { + "sha1": "23d98b835675924b5f790d7efb2618f572271fbd", + "sha256": "sha256-RX24vL3ZnbfY6iCQsEvfpbjXE9PyFryTp09yVlInPSE=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "gradle-8.2.2.pom": { + "sha1": "ae8bc4c25b01042f1a1695d38519a833aa524ee9", + "sha256": "sha256-0mrNNU2P1H3bnfffgrz+23MzsQq/fBLw5HTYpMpaNQ4=" + }, + "gradle-8.2.2.jar": { + "sha1": "d5251ce31f2502b1ae5109ccab1b244f60b22368", + "sha256": "sha256-ylD0fNE9NH3nyJ7LK9Bkh7A6iGh8RsK3drwKkyOViVU=" + }, + "gradle-8.2.2.module": { + "sha1": "143b939218cfa59a51f4cfae4026c557e7d501a8", + "sha256": "sha256-KOkot64vlZy+MSEMM7doYKFZ3DdonYYibg2mWjcKIh8=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-core/1.0.0-alpha10", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-core-1.0.0-alpha10.pom": { + "sha1": "c81a50e473499d158969ec0e041aedbce0bf49e4", + "sha256": "sha256-CJe92DonFWAsnok8rw/nocQMmKmRWxmJgNmwY5jl+74=" + }, + "jetifier-core-1.0.0-alpha10.jar": { + "sha1": "9eb7027c383061de12f93aae7a22cbeb97832d2a", + "sha256": "sha256-QAc+QE3kQc2QTWBjC+SBIv5GgxQBCM5qGemnDSmDo5Y=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-core/1.0.0-beta04", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-core-1.0.0-beta04.pom": { + "sha1": "d9d79b2e0b66e7ab107f5e07fc021c6a7ff8c365", + "sha256": "sha256-2wJa0bvGiW7fD2fNoSIrqbfsmgGH339P0KMYR1x1jk4=" + }, + "jetifier-core-1.0.0-beta04.jar": { + "sha1": "92ae18564fc833685f30f5a20297dd32349a9f6e", + "sha256": "sha256-yfiwFhRM+zHFruktR/NN4jKJFnysXo72g2Wy3QV2bxE=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-core/1.0.0-beta09", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-core-1.0.0-beta09.pom": { + "sha1": "6393d821ad5c2917a0182717ae8b8eee72c667de", + "sha256": "sha256-Roi0Qvs0eCCoWo4PAZl4+eqnYvu7rbd0q5RRGdcwTaU=" + }, + "jetifier-core-1.0.0-beta09.jar": { + "sha1": "c98ee0e5579aed97e17f605a89b101115a2f5a61", + "sha256": "sha256-m7OpHoceeERG5N0eHR3qXDIuJxy6gnjB3sNmmmau6Fg=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-core-1.0.0-beta10.pom": { + "sha1": "a05601d691cb5b3175469dad16d7b507e122161f", + "sha256": "sha256-XJ1C5rfjXU2NAuCjIs8maTs+w2QrEHyPC+WnIdRaDG0=" + }, + "jetifier-core-1.0.0-beta10.jar": { + "sha1": "6e621022a9c95af12a7004d6b55805bbd1126837", + "sha256": "sha256-Jqu0oTkn2QYhacUEyelP6A6a46T3tauIdasAdTapH14=" + }, + "jetifier-core-1.0.0-beta10.module": { + "sha1": "df2cc641b26756fd3daf9dd8f69ff81a886b2610", + "sha256": "sha256-8JF1iaQtJ2Fj8QBAq1hC6RiD3L2x1Iv9Hx/Kpywcp7c=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-processor/1.0.0-alpha10", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-processor-1.0.0-alpha10.pom": { + "sha1": "2a59ae1bc830ad76a588eb20b6d58be93ca97399", + "sha256": "sha256-oM4OWwvQ35DUo7ZqtLVLDTRUZbVcraYQQnHyCjrgrhg=" + }, + "jetifier-processor-1.0.0-alpha10.jar": { + "sha1": "6d494a04e0af27ecd4ec7a35c93759dd9c7443bc", + "sha256": "sha256-fGHwU/zaY9+fNK3CZQzeRTdni9x/fbcr2UGi+wF5dgI=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta04", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-processor-1.0.0-beta04.pom": { + "sha1": "39f80028bc2d7c4fe6740c80d68f3ff7dce9c62d", + "sha256": "sha256-SXOmxtf4kpehz+pWxiPOpys9s7SkmZKvoA3XesBSuJ8=" + }, + "jetifier-processor-1.0.0-beta04.jar": { + "sha1": "8ed1fee7b1d03709dd8bd0224f21ef243e5c4a4c", + "sha256": "sha256-HdSB52BsyzCGiQjZroDyfRxbqIL3klMr2R7XMcVPTvs=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta09", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-processor-1.0.0-beta09.pom": { + "sha1": "c5b7c0ddbdfdeb69b842bcd9672f88bae14238c7", + "sha256": "sha256-OIQI+ZQFkDGSnRRyPsAUEfhcaDRghGV2PUx5r+Igpzg=" + }, + "jetifier-processor-1.0.0-beta09.jar": { + "sha1": "fb2a015ff56e24939a88593ac73b84e627864476", + "sha256": "sha256-GDj57nASDmT0VhLKBxlyrIPdwRgYWr/7zjkypHEDJes=" + } + } + }, + + { + "path": "com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "jetifier-processor-1.0.0-beta10.pom": { + "sha1": "0ef909fba0e8e31b04e88b96ac2a01b46c395c9f", + "sha256": "sha256-M7F/OWmJQEpJF0dIVpvI7fTjmmKkKjXOk9ylwOS6CEI=" + }, + "jetifier-processor-1.0.0-beta10.jar": { + "sha1": "946c39eb0c052ff4f60a8939648a8b99af20d604", + "sha256": "sha256-xQZ6e5KCN6EnGl6ctXEOn4C0lzKTlFvFHjpMhk6kv+0=" + }, + "jetifier-processor-1.0.0-beta10.module": { + "sha1": "e2bc65203be6634db858f565b4318d5eb2798e64", + "sha256": "sha256-NsJVdrGZk982AXBSjMYrckbDd3bWFYFUpnzfj8LVjhM=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-26.0.1.pom": { + "sha1": "2ad9aa17965bac57ce06539375ffdb5905943ffb", + "sha256": "sha256-BSsNK1jl308w5PZL31QURz9s+hAUf4+NicsLYzFrqPM=" + }, + "manifest-merger-26.0.1.jar": { + "sha1": "0700cbb1ed4066f48d4970f5669c44a51bd03142", + "sha256": "sha256-vt5zCE7IErkW9obMbkdiW9B1YVS+9YmkwrQZ5Uo5O/U=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-26.2.1.pom": { + "sha1": "fe714f42f14c16ac5f75820a0e6c239e50aa1b64", + "sha256": "sha256-VsRL8LetwtF07FqTUCyRiKw+XEhminMvuxYdCNVNvhk=" + }, + "manifest-merger-26.2.1.jar": { + "sha1": "6a6542caf30fbc4091dffea76c602e97bc9d8949", + "sha256": "sha256-iDBXMmM2EDXTjP3LUeLblAKck4ZbITNPX7+KJ5hCgaY=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-26.5.3.pom": { + "sha1": "4cc40406dfe779b3b575e66c970ae0e9986b0c94", + "sha256": "sha256-ZjxoTo8jLgEYXLnqBwjqCRwxAhYiDHlQthqQZlX0Vj4=" + }, + "manifest-merger-26.5.3.jar": { + "sha1": "36674041b9d2644a30b75b6d8b7106bc16eac78a", + "sha256": "sha256-tGBtBgLyH5GhdM+4UWtabpj/rJWNPzzYmTuVDvSbgEk=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-26.5.4.pom": { + "sha1": "1f2b10961ea26a90e51148d8ef9d85448984bb10", + "sha256": "sha256-HR7dIAvWhbElsTt+q6VSSM2KIGBmTaRpbVHKw7XtA0s=" + }, + "manifest-merger-26.5.4.jar": { + "sha1": "ee71136e9023cd90b3e431e506e4f5d0386ad55d", + "sha256": "sha256-ChDiaftKXZbEV5aHccAWfoS7cmSVGPFklzDVUEZX1fY=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-27.1.0.pom": { + "sha1": "4786c7970474769c178f7e39219e4a92d7003514", + "sha256": "sha256-dRusGyEZx0eMEfX4Rbt35Pe/mOcGbQ1vvFMmJ6SyB5g=" + }, + "manifest-merger-27.1.0.jar": { + "sha1": "f7c161c4f308ebc76ccc4915333eae4997f1b4a0", + "sha256": "sha256-XDxiQg7LLhPenGB0aCfwiVzDCChCj06SQfbSvBXVbHg=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-30.0.4.pom": { + "sha1": "b5ac2e7b772b0e414b3bf78613c451028b1f37e0", + "sha256": "sha256-q9dqshB0E7BDPypTAIrCIsMQ8NaHTlerNbJLNFFQDIM=" + }, + "manifest-merger-30.0.4.jar": { + "sha1": "3da7e87c25dc7bd65622079e81b5933aa8eac31a", + "sha256": "sha256-RDOe0wmVdC+WFHiRNWE1IQWZyWoKAKYtgUZc4rjsH5g=" + }, + "manifest-merger-30.0.4.module": { + "sha1": "0b535770e310a72aee2fb2ecfd45c4fa6da2db57", + "sha256": "sha256-Hx20XqStGPzf57SnKpKaJbTjV+yFfVcJhiFWMwBM+j0=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-30.2.1.pom": { + "sha1": "8a6b45ac60ba79772a432dead7a9a8a08ff86294", + "sha256": "sha256-+hDPCXgTv/RshaW1HV/44ZBgLfEp8fWrbsKITivXGI8=" + }, + "manifest-merger-30.2.1.jar": { + "sha1": "1756d0ad1b6a704a32f73a09f3fa1836243a1586", + "sha256": "sha256-Tpn626MxUVAHg6ZvbRLFFTsbQJJ6ma9XWrOsRRc+swE=" + }, + "manifest-merger-30.2.1.module": { + "sha1": "5c8c76e4ee519e2e1f345dd32ec2f311a44ff3fb", + "sha256": "sha256-EtAi/D6KMtQEF26ZUFjazNfNV1WC3kSby8Kfa7EemBM=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-30.3.1.pom": { + "sha1": "64e3793915ed005355d09f0feadaa2856361c015", + "sha256": "sha256-3eu5uMjcZt+PD6ArwefWju51lDe7UkrUpL7ogEHAPy4=" + }, + "manifest-merger-30.3.1.jar": { + "sha1": "71f4364c86bbe1ff328c6e5eac880d07c8c92943", + "sha256": "sha256-U0rFDUXRu8gPQSLH+iIUAkQoNQKe8F7AVvTt8ndsjJ8=" + }, + "manifest-merger-30.3.1.module": { + "sha1": "618fea94f91e136108ce02a7235c1210f2011761", + "sha256": "sha256-UOE10sw6kW542Ht7nx+peosdp+RixHknFq/2/3FtNkw=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-30.4.2.pom": { + "sha1": "158ac051cc71420b05f5acbcc2c00438609ec6e5", + "sha256": "sha256-QT1r3wdtdMWwECD7R4SYMmT1IWjd+EGqV+bKKzzwuXs=" + }, + "manifest-merger-30.4.2.jar": { + "sha1": "49581a754311a2f7011ca141b3022c60d508504b", + "sha256": "sha256-2YtqHQA2JDH4SkqLy6FC+N1AEKGROoIDRpYBecsITXY=" + }, + "manifest-merger-30.4.2.module": { + "sha1": "ad359e6ac143f1b912fdf70a0bb9a81a4720f590", + "sha256": "sha256-G4nv54OrJpQw535LButTR76SCsv56iYe5g+KL7DW8fg=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-31.1.1.pom": { + "sha1": "5d06a2351befabf83ae415d46c639d95828b0394", + "sha256": "sha256-vCgRhiIlxkcQxCCwe2bf/JJYYeek4vLGe0qheK/3q48=" + }, + "manifest-merger-31.1.1.jar": { + "sha1": "d5f15475323231ed1506561184e7483f837f27f8", + "sha256": "sha256-h6GJaBqMavIXG60czPFsWnQ78ngRJq3i74zdBMbkiYg=" + }, + "manifest-merger-31.1.1.module": { + "sha1": "8fb73cca01f935c444076e5eba14decb32f4d561", + "sha256": "sha256-/RmTiJnz1hIAB62Gp9OiQjpHKBcmX5QBYIteE4pNCW8=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "manifest-merger-31.2.2.pom": { + "sha1": "569aacdd0f5eb1cd540eb738fe20e857d8f75f94", + "sha256": "sha256-mVZBVHy3QuOdyoDq4OcA/f8XJnjSTAxya95PaOA+TQ8=" + }, + "manifest-merger-31.2.2.jar": { + "sha1": "0628d0293afda936759cf13554040e02fc0a4617", + "sha256": "sha256-z0EJsiwVnVa0WtOC1VDfT9A2lLix3dE3jvGgnqN4pMA=" + }, + "manifest-merger-31.2.2.module": { + "sha1": "96ab00b782b28aa524a70e604bd174cac845336a", + "sha256": "sha256-jW58M6j7+aNuh/fDb/egbkVicPqf7v+i+QXypcdDT8s=" + } + } + }, + + { + "path": "com/android/tools/common/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-26.0.1.pom": { + "sha1": "29ca7df177eab0ed8dba3f3d22802f216ad29ab2", + "sha256": "sha256-4w8dtgWPdF8mES7zCfib7LJ8YXx9nkxuAhZadiCpLmo=" + }, + "common-26.0.1.jar": { + "sha1": "38e362a43a905b6745d609bea37faddb0c5bffab", + "sha256": "sha256-snu7nSaal76P8KxIDA8JyalT9i5v8F2Q/LsN203pfHc=" + } + } + }, + + { + "path": "com/android/tools/common/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-26.2.1.pom": { + "sha1": "e952e8a80fcb28ce040645f27b75e116a9faf120", + "sha256": "sha256-xb2DpZe6+ZVnxEaXldFVwZKNxzjg7/OY2W44+K14sh8=" + }, + "common-26.2.1.jar": { + "sha1": "b1b9e1a6efa7b64cba484e9f6562b5f63bf23e22", + "sha256": "sha256-pQqrLWQR/2j0AEqHx+k9h9jpgKDsOzUiRlSYl+oteOU=" + } + } + }, + + { + "path": "com/android/tools/common/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-26.5.3.pom": { + "sha1": "ace0f2945bba30c12223d7634a69da21a4242fd7", + "sha256": "sha256-9ikVDGRk14qviKUSEpiyeycF0HdW2W5hH6/yuJgdgeQ=" + }, + "common-26.5.3.jar": { + "sha1": "47409edcd2331596b42c33160101a86046e1c6f5", + "sha256": "sha256-YAVo9B+xn8eWSHtvxTnlKr0Jj60cZ4vONcAqMVGeJPY=" + } + } + }, + + { + "path": "com/android/tools/common/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-26.5.4.pom": { + "sha1": "3e47b19f02478c9dea70fba422429a12447d7ce6", + "sha256": "sha256-XM9Rpvz9rDBcLPJ9ADreEnuYGyth6jf2mxyhqJzolx8=" + }, + "common-26.5.4.jar": { + "sha1": "354eee31a9c52abc71a9b49969ef40da9afc5bd8", + "sha256": "sha256-Ui7nNEyzEo0PTzG+2uSd0nyQKrpUB69+4+IUQR2hYhY=" + } + } + }, + + { + "path": "com/android/tools/common/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-27.1.0.pom": { + "sha1": "d873551e1518e8d0b9e530f97c97c51771659142", + "sha256": "sha256-BLN1IBRJR0Kohnr9gI3HoZVsgxtHPq5+oMaShXAy9hY=" + }, + "common-27.1.0.jar": { + "sha1": "2999a3827833da7c3258167566cd042d60cf533f", + "sha256": "sha256-jhxjjDP4x38iEU6nxoEI+x9wKlO1xJTPpxZT/K7SBFI=" + } + } + }, + + { + "path": "com/android/tools/common/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-30.0.4.pom": { + "sha1": "57942a9f7e13c8e32d12b12a8a89a9eca4cbf66c", + "sha256": "sha256-+zZNrOvW8H0Me2DtQ7BKDLArr1cc0PKDq8PHsJQZ1dY=" + }, + "common-30.0.4.jar": { + "sha1": "8ad211d914acb75405897e95d3a43748747195c1", + "sha256": "sha256-0aorakOEPDhd3SRvsBGKz0WvrVMX7bPFCKRgSiiPqck=" + }, + "common-30.0.4.module": { + "sha1": "621643cd62e77ae672de81d49031726ea710d71a", + "sha256": "sha256-0cu/AUaHSnnn9rZt7kpAR2k/584BR3m+PlbcKDYg2cY=" + } + } + }, + + { + "path": "com/android/tools/common/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-30.2.1.pom": { + "sha1": "6b149f62a91c31e10b572323f7d4cdca084d1736", + "sha256": "sha256-69cVuF76B7sI26WS1SFmtYuSD/7ar3kKYDaV0TGQ+WU=" + }, + "common-30.2.1.jar": { + "sha1": "2243ffc2818633cea65ed2da911d393908016591", + "sha256": "sha256-Dx1UHktxFJSAI3Relxxk4Q99zH3PefTTAXICxRcXdkk=" + } + } + }, + + { + "path": "com/android/tools/common/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-30.3.1.pom": { + "sha1": "56d57dbaee9042973a611ef9c34036968313e6db", + "sha256": "sha256-7TVveYppjlMg6oYWEIMT333tl3I8B6MV13KH/CXMmxs=" + }, + "common-30.3.1.jar": { + "sha1": "6d00d4eaf6e80712b579a71e50f574483e6aef31", + "sha256": "sha256-VZ8UhCItiUfs9NGFOClb5tEv8rdNwiw8hZIse6M2eOk=" + } + } + }, + + { + "path": "com/android/tools/common/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-30.4.2.pom": { + "sha1": "234f6f77ed2bb825c849196b099512220da53272", + "sha256": "sha256-vZh0cRCL1+FVWovuIE8Wg8I/KpyXzabFOYM1cDSSdFQ=" + }, + "common-30.4.2.jar": { + "sha1": "605a8083db5a6d178e3fc6810c4046c198a4f4b6", + "sha256": "sha256-GtyKcVZSZhj+LTJDBnodYckMQ/a1JRXtqv6GK/sVbyg=" + } + } + }, + + { + "path": "com/android/tools/common/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-31.1.1.pom": { + "sha1": "5f949413a6fe83b71911eab7d1df145c48cb61c5", + "sha256": "sha256-JCo+lppmXi3md3tQVQEDHoF/ezA6j97qDYQxOLRpSn4=" + }, + "common-31.1.1.jar": { + "sha1": "71050720fc42680f95e9751305b80cfd84a46755", + "sha256": "sha256-A0ReEMSzYIOOq3ryOJGCczhxOQ+6uk+pnxZj5uBI0GM=" + } + } + }, + + { + "path": "com/android/tools/common/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "common-31.2.2.pom": { + "sha1": "40eb4af68a15cae532da8392d658fdb678a61275", + "sha256": "sha256-5b+SfV///FUEOjfp+SLS/4ly7Gp58apI4IrNdMP/jEs=" + }, + "common-31.2.2.jar": { + "sha1": "c4bd098aab97fa95da10c4098c0238437dc1c4ef", + "sha256": "sha256-6b8fw5MLtRGylYSRXF5qanKdRRujrCXvOWUo1IJ5ZBY=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-26.0.1.pom": { + "sha1": "9ba40aa301f8ad2ba19b56d0948954d7bdd6cdef", + "sha256": "sha256-B/CLMVyBJAbp1JfhtZJol0L+s5UMGnE/leJpLT4Vthk=" + }, + "ddmlib-26.0.1.jar": { + "sha1": "d4bcdb3a578812688ccc34f1359702601fea955d", + "sha256": "sha256-gfc/iyW0MPWkCjlye7zvZTodCGjMaSM7ROQoLOWpT/k=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-26.2.1.pom": { + "sha1": "66a2a3ef7c801b5dd27a083e82b8e778c468add5", + "sha256": "sha256-ebqdPh8dW5daninZJhvUSccw8VLtTF67Pil3Ge/wUPA=" + }, + "ddmlib-26.2.1.jar": { + "sha1": "088fbd27836d660bbbc3823bda8da3085cef41f6", + "sha256": "sha256-pL8KKaGZgL8nJpRlzHggZGVnULd8JnKPgvnhSLcFIYs=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-26.5.3.pom": { + "sha1": "f69c10111005d9ed625cc0b75fed9c5c2fcd1bcb", + "sha256": "sha256-y79URq2NFK1PQ/kl063FlIr3hP7NdEvk04bf50sXe1s=" + }, + "ddmlib-26.5.3.jar": { + "sha1": "804a61e67286eeec17d195c78a135f0ed2fbdb8a", + "sha256": "sha256-IlrK31Fopg4wIKM81mm6TYatse+ePnEhcREd4t+RrCs=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-26.5.4.pom": { + "sha1": "6911c3e30fe3e41a31b47e7e5947dce86f1e620f", + "sha256": "sha256-GLx5yaaXpHKUHr6pLztbT890Ofr0anUwB9OFPVFIBFE=" + }, + "ddmlib-26.5.4.jar": { + "sha1": "befefae046c5666818dab0d597a878960bd42c56", + "sha256": "sha256-9I/fnMvAJ0eknzLF3vOcqwW7O50szOfK5J8kXgzL9uM=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-27.1.0.pom": { + "sha1": "189dbc302b8654a932c5b59a27c3b1e64f892bab", + "sha256": "sha256-C1677lP4OtjGjitPuVsN7Mx+kjBhogRDOSWjstKECiE=" + }, + "ddmlib-27.1.0.jar": { + "sha1": "41b9d5aacf5f56e792d6bf881fb931e12c9e047d", + "sha256": "sha256-HG5B3mSuaPkVZzBvwFyxtgeYo3gTxGZ4wWoN5pdU9wo=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-30.0.4.pom": { + "sha1": "48120f184850eab33acbfa842de82024b29bdc9f", + "sha256": "sha256-1620QL/EjLbkmSP3AGnZt/jThiFe0r8g9aCdgcsEUaY=" + }, + "ddmlib-30.0.4.jar": { + "sha1": "1b3da17287756321b45bbfd3145e9f2695fb8685", + "sha256": "sha256-f3BqmV9bMEfYDii07bWEsjOLe0e5Yp7RYHAR85wX4Uw=" + }, + "ddmlib-30.0.4.module": { + "sha1": "2839ad6e03d2253696155f4fec035c866292d66e", + "sha256": "sha256-cJ8JRXt4j3cj8JUa2hY4cDLnADncA80YQByHP8aqc20=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-30.2.1.pom": { + "sha1": "8ed87a8175b7630d90d7b8e08dcd8a04e7ac6818", + "sha256": "sha256-W8xtpgtd377vpEIH4b0bucMdsqyRgy1/B7sPBjuZGGA=" + }, + "ddmlib-30.2.1.jar": { + "sha1": "242ed3329eb3b74d57c6a375550f3dfbaf879a12", + "sha256": "sha256-qMwpKbxLXNRcLgUwT9ZSTJGu2BmAkEST27zJ8+LGhrU=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-30.3.1.pom": { + "sha1": "f8f1c83c4da30b4f36b85b551ec7d25b5430834b", + "sha256": "sha256-jjDsuGqNjRp2ZXpvJ+9F7aTv6yUSHPw1djIK6I82UCc=" + }, + "ddmlib-30.3.1.jar": { + "sha1": "f271cb990dc62f580b608a75c87c509683c75da9", + "sha256": "sha256-WMTbWpSl+MkzQJM19mV6kv+ch0iKJxsmAxX70CDAf+A=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-30.4.2.pom": { + "sha1": "431026f55032ede33d7dbfb3decffa97bae9dffe", + "sha256": "sha256-WreMA2M9my0RKbfZu0BLPynU++9wPDAVu4nwk85hXS0=" + }, + "ddmlib-30.4.2.jar": { + "sha1": "b87b79d91290578ba458ce588a4c6c6f3c031621", + "sha256": "sha256-cVf6/Uky3p54c3lyy9Veree6Fehf4/UiypOa90tcxIk=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-31.1.1.pom": { + "sha1": "5da31bf25cf02adb4f3c5a20d30e52ea05cb5b21", + "sha256": "sha256-OZckmHKQWCiyDhsd7EvHnuEib9qFHHEqNMbaxUnvaqI=" + }, + "ddmlib-31.1.1.jar": { + "sha1": "8c5f283b9aa267c37b9b632484a2ab4ccd1205d7", + "sha256": "sha256-jC+xROYcp5qrjtx8P/4sK3S8ayPLDKxsi34Ga1FR6O0=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "ddmlib-31.2.2.pom": { + "sha1": "b80d354529f985b3e29ffb5d4147408d86039290", + "sha256": "sha256-Wb3/fofuqWjNeFRrmK5aQWiN6eykbhUlg8eGJhOISVU=" + }, + "ddmlib-31.2.2.jar": { + "sha1": "281833d08fbead59de281fc0b0eb2e127e9d7482", + "sha256": "sha256-X8H3CzsLgGnyXjoin0qtpJXlkspf4/uIsCVjtHxnTI4=" + } + } + }, + + { + "path": "com/android/tools/dvlib/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-26.0.1.pom": { + "sha1": "2d0e9cdbc51c243de640d2513447c8ff0a1c4d91", + "sha256": "sha256-Lsd5dB9yMT/GYj0PDkKyRQ3SQl3WrTGWl+ZYqumuF3s=" + }, + "dvlib-26.0.1.jar": { + "sha1": "46f3eeaf812811374b4ea2c49ad52687a7ff6a87", + "sha256": "sha256-cMRY3PYEAxe0Hd9Qp9U6ArioWwcjs2zelWLFnjzRHa4=" + } + } + }, + + { + "path": "com/android/tools/dvlib/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-26.2.1.pom": { + "sha1": "726bd01d91e395377f71a4b1cff2bf24dd612589", + "sha256": "sha256-HKGxTAKBJ1pYtnzV468YDWWhBTQNifB2bmvQ7uYEcoE=" + }, + "dvlib-26.2.1.jar": { + "sha1": "fa3cc125cd311f65dcae6310e381b20d4b2ec0fe", + "sha256": "sha256-cqg78oObHfmx+/Z7pF0b+5+WbNd02kMgx2Kyvo8WiKo=" + } + } + }, + + { + "path": "com/android/tools/dvlib/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-26.5.3.pom": { + "sha1": "1983f43ea2ef12519c4ac4579930af7494b4fb1c", + "sha256": "sha256-HNYT321rRp1fKslux7VIkAzONv7xDrPJAZVIITehh5c=" + }, + "dvlib-26.5.3.jar": { + "sha1": "9caa13fe83508ba5687199c02b7ddb885abd8f14", + "sha256": "sha256-+f3/TUDUJ1vhgAWJ2GlS33ErcQa7MD2o5f5zaqIogGU=" + } + } + }, + + { + "path": "com/android/tools/dvlib/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-26.5.4.pom": { + "sha1": "5d3f9c05edec8b02436ddf39b2901339a6ddfb66", + "sha256": "sha256-wfsAKQSxciYuIPbXcvQzZfCh3fojuDPOwD/J7EreUsw=" + }, + "dvlib-26.5.4.jar": { + "sha1": "fc9828a5c4a02bf510b8bde2733e2eb81c34bf64", + "sha256": "sha256-akGcdBJaTpHorupGHR/ySuHpAXVnPn0oyqSwkZ9WbWE=" + } + } + }, + + { + "path": "com/android/tools/dvlib/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-27.1.0.pom": { + "sha1": "0c15e6b7f60b0966af80b4b3d1abbc793275ae92", + "sha256": "sha256-R3gAQbGsAkcAbI/xebQ+1bb3423+A6XP3/IrYYgqn6E=" + }, + "dvlib-27.1.0.jar": { + "sha1": "5345927c80b40fcdae47735726db23b5c9f20c0a", + "sha256": "sha256-pIN8JZxFG6QNW1jswL0NMDuuYU5jX0GqJX2RI97ncQI=" + } + } + }, + + { + "path": "com/android/tools/dvlib/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-30.0.4.pom": { + "sha1": "1a011bb6e869c58bcdd0ff906ece0ad6bd42620c", + "sha256": "sha256-eFplpT7Etc76fnasPJYNYyVAULHBPoyW+ho902wxKPY=" + }, + "dvlib-30.0.4.jar": { + "sha1": "0edb2f58bd08f8ce19cfbf79a87d1e12bbfbdd45", + "sha256": "sha256-Ye3MzOzB/8pH9EIya9fB1z9yVu+a2qyDTB1jmhQuSbk=" + }, + "dvlib-30.0.4.module": { + "sha1": "16ba6678d60a24469800236cb6c0becf021ff666", + "sha256": "sha256-zvalBDpfQ/XFJF4xfZLhhMC8HyLuRZyv+emXGJBUW6Q=" + } + } + }, + + { + "path": "com/android/tools/dvlib/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-30.2.1.pom": { + "sha1": "c5d27511e3c8a85985d7c132a699f2684fa1184d", + "sha256": "sha256-4GQhAPIIHKNhNOYROwyRq9g6FTo2cD7cWMrQ9hXkS9c=" + }, + "dvlib-30.2.1.jar": { + "sha1": "957cccd8d81c52fb48d30226233a2d6dd01348a0", + "sha256": "sha256-jx+gFLcLqsf8QBnXt4goDautztdHzvQeUvTQxgPn4Lg=" + } + } + }, + + { + "path": "com/android/tools/dvlib/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-30.3.1.pom": { + "sha1": "c1e5015259b56f91e85b83b39bb63bf4acf70c45", + "sha256": "sha256-LSBo//D6lVBl4i/LP0kV6ipTAm1kTpzTOD8SHxZ9fYA=" + }, + "dvlib-30.3.1.jar": { + "sha1": "2053a677dd70d49240d711929ba11f5955ecba7e", + "sha256": "sha256-1LFBvWF6+vKS29fE9ItrOgtCRN7RKw4kHnvfdV2xWXw=" + } + } + }, + + { + "path": "com/android/tools/dvlib/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-30.4.2.pom": { + "sha1": "e1aed526b893668ef0fa6f3837f3514563c1887c", + "sha256": "sha256-DqW8FwL6vKKLWHOpAfNJuWeilvSzscw+yYvXjAlp79E=" + }, + "dvlib-30.4.2.jar": { + "sha1": "ef8dfce1e50dc4b683cdb8ce9507cc87878ebcb5", + "sha256": "sha256-2UatOaQLZnyC2odyRSeBVTbeYtDbDo6jM0Sf25BQkAE=" + } + } + }, + + { + "path": "com/android/tools/dvlib/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-31.1.1.pom": { + "sha1": "5b569f920aa9c63a7aef25c89ab96e2d7aecd53e", + "sha256": "sha256-dH7Q/h2/gK1ASnPDAI72OaRzliqWzPpDqzoqltjXhNo=" + }, + "dvlib-31.1.1.jar": { + "sha1": "73f9e701a49ce55a5efc5ade01238f5fa9b2655d", + "sha256": "sha256-9uMaK32htipAHl4/AwRCb6oRnc/RZR+PZ6mkwlTVn68=" + } + } + }, + + { + "path": "com/android/tools/dvlib/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "dvlib-31.2.2.pom": { + "sha1": "30149fc810001a472bcb924fc3e4ebabc2906ab6", + "sha256": "sha256-G+ZdAN8ZdjCYm+RzvXLydjsu3wjQgmrg/IhgC1W+wdE=" + }, + "dvlib-31.2.2.jar": { + "sha1": "a1a4e33042e1e450bed44a65bc8798c9783bedca", + "sha256": "sha256-DLV8Em7ZVleuHgl0rwkrQBpSt21Wg/fMvh+PIa8GeU4=" + } + } + }, + + { + "path": "com/android/tools/emulator/proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "proto-31.1.1.pom": { + "sha1": "8d63d0d59b38f0001c784a7e0f2a30a5f53b5dc1", + "sha256": "sha256-O1zWdSvyLBrwfhDYvWnjKeC6Jh8LjdsP4+AOLy1/k50=" + }, + "proto-31.1.1.jar": { + "sha1": "346f58323eceeaee3ddd80cb7847958be3a5e5cd", + "sha256": "sha256-bAd1TEBR47HLvsvg8UxunFE3pj/z6ir+UpI2Q1/Jcb0=" + } + } + }, + + { + "path": "com/android/tools/external/com-intellij/intellij-core/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "intellij-core-26.0.1.pom": { + "sha1": "d100cfbdf7500f2006ecfeeacce519a0d8e9d875", + "sha256": "sha256-7gQbO+toLou/LcFrbtwjNIPLw2veJ/0TmWYXpme2FaU=" + }, + "intellij-core-26.0.1.jar": { + "sha1": "a373ed549821cb9a932b87c79792790f6d018727", + "sha256": "sha256-WKpN+PqukOSEwLxHSUoeyX3FH+U+ritKcNYOp0ic34w=" + } + } + }, + + { + "path": "com/android/tools/external/com-intellij/intellij-core/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "intellij-core-31.1.1.pom": { + "sha1": "e74a7533b6d6e0db54ea6bc35b15efe2fd00da27", + "sha256": "sha256-V495OImIIeT/dCkdYBymlYn5rTRz0uFvcAiMq20H4dM=" + }, + "intellij-core-31.1.1.jar": { + "sha1": "ed4e36f3c2c41cf488aaf1303a96ba2064d6bf8f", + "sha256": "sha256-VY235L4/CactFcnsGt9MeteLa4r5J7Nrw+GxCT6Gt+o=" + } + } + }, + + { + "path": "com/android/tools/external/com-intellij/kotlin-compiler/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "kotlin-compiler-31.1.1.pom": { + "sha1": "d3836527cf8b72fa3cc43df621a15eb823cbc226", + "sha256": "sha256-UDMcEDSty4XUa9VqYX2bmAZD14JqkhEjfx3xc14nH10=" + }, + "kotlin-compiler-31.1.1.jar": { + "sha1": "ca679200f5902b43a2a7bf3ebaf27c4c50a60010", + "sha256": "sha256-Wa9fMRuIZN8hEpP+a6OoAZu0D1/CJuzn7LzQoGFKMms=" + } + } + }, + + { + "path": "com/android/tools/external/org-jetbrains/uast/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "uast-26.0.1.pom": { + "sha1": "6ecc2c2b816f6e23f5edf12e496d51b27198beee", + "sha256": "sha256-K6nBz+xrShg7cqXMxsssg0edatfpMVQ8eSKSwWeaf0M=" + }, + "uast-26.0.1.jar": { + "sha1": "663906c592ce867fa2e9bcbffbe2130397078132", + "sha256": "sha256-7NRtxp9SErNesh1k22gHL5VsAACDAw/1PgnR+BSHxYA=" + } + } + }, + + { + "path": "com/android/tools/external/org-jetbrains/uast/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "uast-31.1.1.pom": { + "sha1": "564979086715d09649bb66f3d40276c1fff8c3ff", + "sha256": "sha256-l0xuVJOGRG34wCrqeI8PgoBNWFLoV5Itx+6KDkA8jAE=" + }, + "uast-31.1.1.jar": { + "sha1": "362abd636073c3e0f20fb708053f178cbbf9279a", + "sha256": "sha256-PLzgbYhZttOfSYMWuzL1QviWMSUUR7WTjleDoabq158=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-26.0.1.pom": { + "sha1": "f28f7a66758d9b562b4a2f07ecf5af6bb924302c", + "sha256": "sha256-4rSTXnbZG1q+xE3jCfLIu40dLmzAuHkLQ8tlTx4ixyI=" + }, + "layoutlib-api-26.0.1.jar": { + "sha1": "4eb1f46803873ff996f2457d1dd64c5c305818d8", + "sha256": "sha256-a1rZEaE6XIFMBL5Ddlhu7BWDgVjLWbnwYq4Tjd/xuGE=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-26.2.1.pom": { + "sha1": "597e3e5c746e2eaefb90a86a3dd0af1745442087", + "sha256": "sha256-IeBtjE+5XTibZqBhPdjbtsyIozaTEDW2HGpx2xdKvKc=" + }, + "layoutlib-api-26.2.1.jar": { + "sha1": "164c58ac8cf4b2bb08ad03acc5f963f4f70cfcaa", + "sha256": "sha256-3b9PyhI3M/oBFZWxzB9KwpN+0ye2CZBxH6/DPHdcKt4=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-26.5.3.pom": { + "sha1": "75263963e8b5baa185153cbdf1a5347628a71927", + "sha256": "sha256-Qs1kefYxk/LNrFGbBIi08IbYhToLabRA84yCWHE09eE=" + }, + "layoutlib-api-26.5.3.jar": { + "sha1": "6549e729f3f27c7d05049494f25cfd01890c00e0", + "sha256": "sha256-/7VaULd7u4vTgnr7gpGhRVzZU7EmBYzP3db/rvkwM9A=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-26.5.4.pom": { + "sha1": "d492e7833d19ddfd31e5efbe54ab4729431c2bef", + "sha256": "sha256-gLEzIb//XVAQ35WLPpeQEi+E+xsVDJX1S6e0eeRDplg=" + }, + "layoutlib-api-26.5.4.jar": { + "sha1": "50e2abfb1721624c03e3f48f08f08403a425a919", + "sha256": "sha256-ZRA2lehZ1q9SDMfzWQGvu9c716c1TRUwxofbZafRKTs=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-27.1.0.pom": { + "sha1": "c824c4c543da3f1c97172ab93636fac468f0b040", + "sha256": "sha256-3TLJPUHdF9N7Q5SI/j3XP5mR4wGxRjrA+zLK6rhnPcE=" + }, + "layoutlib-api-27.1.0.jar": { + "sha1": "cb0701115c2227b223ac65f0a158f0e80cc197d3", + "sha256": "sha256-spDsTKDDaGfqmoHdhWddRYlcF6ZEbkDUF7jDeZwBzuM=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-30.0.4.pom": { + "sha1": "2b6107eabdecae37b98a762f8cdcdcbda3f7e60e", + "sha256": "sha256-jArJBZarTIRTZKzoXHT2XD0RJ7HUONbacskS17LDTh8=" + }, + "layoutlib-api-30.0.4.jar": { + "sha1": "5a7d5ff9ba2185bda1b10294e96945f3da79889d", + "sha256": "sha256-m6IxU91Kp5UxPYuQjNCs1VPsXi17SlxZRGCbakenaNQ=" + }, + "layoutlib-api-30.0.4.module": { + "sha1": "02ca23994eed3e4e124ff3da7dae85aa19435d13", + "sha256": "sha256-6uHY9OtwDF2hfqAk52mY/e/1DlC+wDKHl77Dw59yTnM=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-30.2.1.pom": { + "sha1": "498630d86d0727a3248356127ffa3ba62f4c687f", + "sha256": "sha256-eCW0p85L55mAT2YKc33KIriUuw/7Suh58utNRxQgqkw=" + }, + "layoutlib-api-30.2.1.jar": { + "sha1": "f2b86f9214ba98e5b44a75b6863e8548885e3634", + "sha256": "sha256-LH15RbovjUpILY6KXpBpIzUCVfuLJ15dxv0KSm7gmrc=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-30.3.1.pom": { + "sha1": "7c3cb92a304d1a3664b005908a39b63944429c41", + "sha256": "sha256-DR9LTvL9KG6ok97RPjaXRP/OeCManoRR7l7L+DGdyCs=" + }, + "layoutlib-api-30.3.1.jar": { + "sha1": "5f0a18661da3c0f524b42fddb0d801d224e19a84", + "sha256": "sha256-f/ssE6yS4tLUVMYX7FN607iGiphxGMTTxiAYEl1lZwc=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-30.4.2.pom": { + "sha1": "b7d1c300dc17bbefed51c6897eea0bf5957ecf3e", + "sha256": "sha256-rGm/J4D0xgO+Bi9e9n5w9Y8E5xYNQRqmSMDOEW7h39s=" + }, + "layoutlib-api-30.4.2.jar": { + "sha1": "2ca558949f442088c4c997a9cf1b6c7fae8b270e", + "sha256": "sha256-a2aOsYdUYtVY1RMAYTIbUEOFppxMEwMDbaR5N2jtWSs=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-31.1.1.pom": { + "sha1": "07d8bdfdfd70ad8f39c52fa259cf50715057efe8", + "sha256": "sha256-4odDUvZrmAq+x8JMcQ3koLl6G7nOK80JOdPUcD0FUow=" + }, + "layoutlib-api-31.1.1.jar": { + "sha1": "3803332e9fa1f20ac5b4368c54fa9ba830a4e49f", + "sha256": "sha256-K4s1ohJbnhHEULzbV3SqaYsZm0AmrknyiwfBwjJbWGg=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "layoutlib-api-31.2.2.pom": { + "sha1": "7d79a239b417933b672246bb166606eb031d7feb", + "sha256": "sha256-PkQsDa4nEWLULfcpRqWBE1Un1Yq7TiU4ZS7j8ngQxgw=" + }, + "layoutlib-api-31.2.2.jar": { + "sha1": "dea9952b540a160c7906348a58e145bfa9db7410", + "sha256": "sha256-KxNlIa2TLxqccAolGp/1tuvUZAQj2mjOQ7pMf3YCwiU=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-api/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-api-26.0.1.pom": { + "sha1": "939c08ec1fd74659d4a47d849d0376e1b1b88988", + "sha256": "sha256-3bD8VAK0zY8x1fKcgweT0+wA/YzmB4YlTl8PEy8L3Y0=" + }, + "lint-api-26.0.1.jar": { + "sha1": "2d4dd9f4676fbb152e4baf6f6f4cbbb868521832", + "sha256": "sha256-X+ihHpEwhvt6tQW5MJh/MM+j9LIm1Lu8aAA8pDh65A4=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-api/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-api-31.1.1.pom": { + "sha1": "19bd73fd700a6a44e7938b2243e80bee63530acc", + "sha256": "sha256-Rec29/CMq3IbVzWQV6gAratV97+5yqqznZZdAtSjilQ=" + }, + "lint-api-31.1.1.jar": { + "sha1": "7aecc68b4129bb13377d63d9ec2ca5f9837ebe83", + "sha256": "sha256-0Lc7UJRSwstgcNKhHl3c0La8rUUq96UxvLCVeXJXkws=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-checks/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-checks-26.0.1.pom": { + "sha1": "12d4cfbea1a4815866c8edb6a9e598df0b335125", + "sha256": "sha256-XSUlhdRxKc1mfZbJIjc4MTikLt7FqJY6fdRM13D67ZE=" + }, + "lint-checks-26.0.1.jar": { + "sha1": "a5df32c64598e190808accaa03820fb9db9f8201", + "sha256": "sha256-lJJGAgwIoFQziQgq84Jozc5O56eoIYCXNpuYRXUoZss=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-checks/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-checks-31.1.1.pom": { + "sha1": "ea747cad48b1cf70bb68fa35ab99ea152855a8fa", + "sha256": "sha256-Phxnpsiak9TjgPzC+/9MkKjTeQcyEzggJ+ImKVNFzzA=" + }, + "lint-checks-31.1.1.jar": { + "sha1": "429af1de53157365fff26f815c766aa25beeeb58", + "sha256": "sha256-OHVtwpKdX9bezZBoDEIupXc2OS7fPB80sgkhaN2beVI=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-gradle-api/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-gradle-api-26.2.1.pom": { + "sha1": "ba277763a9f51be5bacd587b3cc3451f45d3b9ce", + "sha256": "sha256-mFN3XvrKSJWhKyXs0+dg7Nw93Xm6wySDpj+P5lZblPY=" + }, + "lint-gradle-api-26.2.1.jar": { + "sha1": "62f8362369c570266bcd4f030908ec1d237df331", + "sha256": "sha256-IoPnrzLjAVZfKnl+Ux8PyMZIB31Fevs//d2+5jiXbC8=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-gradle-api/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-gradle-api-26.5.3.pom": { + "sha1": "ce7c04f4c18db0fe372667092500726dc1436b2b", + "sha256": "sha256-tD6Ht965r/ZdBmPvIpAUoOyV9OsgUpxwTbEXrkq7I00=" + }, + "lint-gradle-api-26.5.3.jar": { + "sha1": "34f6edb87863c7e9f89f24c36485374ad350166a", + "sha256": "sha256-+xQk+cpYS2WRw5znSR5tiXYAVx4XPPkogrqTQWPGDMQ=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-gradle-api/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-gradle-api-26.5.4.pom": { + "sha1": "59dca214745003f3932114b776df8310f058c4b9", + "sha256": "sha256-b8NexLZvhNdXielZ+0ATNnGIgDPfTGZIgstwWSQSxxs=" + }, + "lint-gradle-api-26.5.4.jar": { + "sha1": "5f3ddac230dc3f2cd241b3cd2d1f3fddb995bba4", + "sha256": "sha256-u/D5/waeHn28JYcdnRwl0zawADp7eBNQmQt54rh1Dm8=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-gradle-api/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-gradle-api-27.1.0.pom": { + "sha1": "ffca4a65f15ee528835e0da5da551af2f7c8a255", + "sha256": "sha256-Cg2rofTqLVQP3YamVpmMZg8xrShmHMPTf36l9tWW7cs=" + }, + "lint-gradle-api-27.1.0.jar": { + "sha1": "2ed2e2ea08ef9bc4705153e5742209edc81344fd", + "sha256": "sha256-bv/uEsK4wXR8WC1CiWoE8sTi4lmJzrxBPbHX0iXAOaM=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-gradle/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-gradle-31.1.1.pom": { + "sha1": "2bf09746f723163bc69b2a57baf6b7c1a6f58ccd", + "sha256": "sha256-5KmUW6sAnQIZovEHzzbgR+p8lwXz1yQd+ex3NtZ7d9c=" + }, + "lint-gradle-31.1.1.jar": { + "sha1": "684709eff4ce13cd8acb0864e0331947e0f95f3a", + "sha256": "sha256-47+tF26FuDMG9IfKH3C2Ow4uJ+ldMoC4cwBRyYkNJLI=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-model/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-model-27.1.0.pom": { + "sha1": "743ed546fdb25e66e25495ec819a968f5f49742c", + "sha256": "sha256-qVCQHBuS6Pxl2YJJqbBHpFSaeFk8z8boHD9HkhchSVc=" + }, + "lint-model-27.1.0.jar": { + "sha1": "18a262fd95b383b04f181bea7e8726701d026e9b", + "sha256": "sha256-ULdaIy/iKsjw2xrQCubRLl/hXijL4TJNvmFHoyzfh/0=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-model/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-model-30.0.4.pom": { + "sha1": "9eba9cb7f7049d4cc8d6a61591f1142db6058395", + "sha256": "sha256-gEHfjVZCMkOJ6waHe49eKd5c2LMT0uwueJtIzaRsEVQ=" + }, + "lint-model-30.0.4.jar": { + "sha1": "6debe32b9318c70e5d1f82cecefa4f6a65d9a1b6", + "sha256": "sha256-+3VhVit9PT7fAVwSNn8meASpgkCDqeEJRNr3FHPOm8U=" + }, + "lint-model-30.0.4.module": { + "sha1": "895fcd5a02d156bdc08a965b1d2259b4d60d3502", + "sha256": "sha256-Vc1urha6D3jKUg6Y8gb1tCeuqo9EP0s/bOhOgSQYIx8=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-model/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-model-30.2.1.pom": { + "sha1": "7e32c3acd56462d4f0fb74099f631b7d67d3567e", + "sha256": "sha256-ue8gSZKz4+ckgE6N+6Ng5VatKkx7OG7IzavY8rfrGTg=" + }, + "lint-model-30.2.1.jar": { + "sha1": "4b6bc3603507b9ed592b99b7c4d26d4f675fb1fe", + "sha256": "sha256-nTgSGctdTbOYTpgn0y/GSLfi/1+v2VL19MdGMHld7sc=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-model/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-model-30.3.1.pom": { + "sha1": "0de967ee5cb08e52bea923569e8dc25fbb2957d2", + "sha256": "sha256-C/rzW31XCqmBFV6uvx3ckDXZO+2IuVkTeZ9aqZCLDqY=" + }, + "lint-model-30.3.1.jar": { + "sha1": "99d568ce7c22b37aeae3568889364a33a540db76", + "sha256": "sha256-S2/ACinI3HFvs6U7IAr3ETGNKo21+mw9h0ttbWXkRUE=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-model/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-model-30.4.2.pom": { + "sha1": "2e9ec4bdcb10076f124f1909f787722e9e92c469", + "sha256": "sha256-ptVu8qAJne/XDlYj89VR2ASkaA3zfhTtF/YPKDpAbb4=" + }, + "lint-model-30.4.2.jar": { + "sha1": "a615607c33cfcb27cf49f84250e59d722390f034", + "sha256": "sha256-MVok9HzHuY1YxYLl4btJBnMwHs32uQbXFwKEw59GLBs=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-model/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-model-31.1.1.pom": { + "sha1": "cbfca2987afdee7020a79cded52cad3965c72545", + "sha256": "sha256-aiYIJh3MDeOPR9IsQe0toioP23dVDqlVPfmk45+PAek=" + }, + "lint-model-31.1.1.jar": { + "sha1": "2a8baf9946bcb0d2fe3d171930fddd414447926f", + "sha256": "sha256-o361RHokL8PI71Ti+mJb5DJPzHgHB23cgNtqCAgdMdY=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-model/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-model-31.2.2.pom": { + "sha1": "56ce34d82bc0636685e4fc3ec285e196c4263704", + "sha256": "sha256-BE1U1IeS5QPkfM2a2E6xrkE+3ceAUIYdrJOf2gCwq7A=" + }, + "lint-model-31.2.2.jar": { + "sha1": "06bbe3204dfd21a5d22d223f17486b5a4f7b4669", + "sha256": "sha256-PJass13jGUNZ+P1Ni17RKdo1iPMqHwENVElvjbkyU8E=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-typedef-remover/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-typedef-remover-30.2.1.pom": { + "sha1": "50380f716679bc36e82f5e4b19cb531fd7f9d53d", + "sha256": "sha256-7LUptoAEsDNDxy9PllxWRNJg/vF9Womu3eDUYdmdgAI=" + }, + "lint-typedef-remover-30.2.1.jar": { + "sha1": "58f701ab23acf04b423b7553fc4f4f8b8b824367", + "sha256": "sha256-9oVJBc/VbEUl6wIvwRZIBU76gS+hYVlkqOyMEXrzksY=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-typedef-remover/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-typedef-remover-30.3.1.pom": { + "sha1": "e721cfcab67d6becc5ad9001be6bfce9536ad712", + "sha256": "sha256-jWiS19fZz3MCbtrNlUBH+ZfRgga3KOlvso5q8fW9r/M=" + }, + "lint-typedef-remover-30.3.1.jar": { + "sha1": "2c7bb3fcd0052f8311973c915913fb1979979f40", + "sha256": "sha256-AnWnYz1Vly/kbjOVgZuQFM8E4gNb6J5KbrpqyVWTUf8=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-typedef-remover/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-typedef-remover-30.4.2.pom": { + "sha1": "f6a80f7a05f5125bbafd786e018251d1d494f649", + "sha256": "sha256-WyryHlCuu24Et7C2ryYcKsyGanmcZFU9QMyqEmKebu4=" + }, + "lint-typedef-remover-30.4.2.jar": { + "sha1": "dc294605db32e2a07fd9bba72a4e337eba837a03", + "sha256": "sha256-Jsk4TNDMcnwJhQl7r0+82Q1Vbga7D1hsF1ypkS+8Zv8=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-typedef-remover/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-typedef-remover-31.1.1.pom": { + "sha1": "d243453ef528a2a8a8a05b0fae2ed7be57414fea", + "sha256": "sha256-0x8UFw7cBk76Skxv4W0EX5qY3p131e4w2VClUP7Fp9k=" + }, + "lint-typedef-remover-31.1.1.jar": { + "sha1": "644ab9b8a697ba83702c4e32eaaf95c504e1a522", + "sha256": "sha256-W09IUhXKTYbvIxn8OYtfIlHmL1RGvF/Q4AZTZI3d4xg=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-typedef-remover/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-typedef-remover-31.2.2.pom": { + "sha1": "122535cd0c930c46fa746a809e2ab6184e76a980", + "sha256": "sha256-aORAhH36aWimcC3nXQ8GcMJV9Vhd64ca1EonaYz5nrE=" + }, + "lint-typedef-remover-31.2.2.jar": { + "sha1": "644ab9b8a697ba83702c4e32eaaf95c504e1a522", + "sha256": "sha256-W09IUhXKTYbvIxn8OYtfIlHmL1RGvF/Q4AZTZI3d4xg=" + } + } + }, + + { + "path": "com/android/tools/lint/lint/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-26.0.1.pom": { + "sha1": "fbca22e39c4f0b04feec26b3ac78622bf39ae6d2", + "sha256": "sha256-E8FEj367YfZ+b71s1cGIgEwXxNfnfEt5sxbt5O+c2bM=" + }, + "lint-26.0.1.jar": { + "sha1": "541bfb082a95a3b55d0158d69b067f1d7c623122", + "sha256": "sha256-rrGXFYovtr0suSM+IodPgFH4KJHb2CtrCJ9J4wn4LkA=" + } + } + }, + + { + "path": "com/android/tools/lint/lint/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "lint-31.1.1.pom": { + "sha1": "fef414797b25887e835a341ccd65f08293ad1a0c", + "sha256": "sha256-b4X4Fig2gy3tAXr6EezaqNMTeklziLR+I21vbETbsvg=" + }, + "lint-31.1.1.jar": { + "sha1": "1a8e67dded8fd4c1079005c10f3812b888e8800e", + "sha256": "sha256-SXpKXJ6rBgPIZlGqHjVanw/QUXGwavZy0R6pmbT0vxY=" + } + } + }, + + { + "path": "com/android/tools/play-sdk-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-sdk-proto-31.1.1.pom": { + "sha1": "78d2bf75f979fb8389500ae11a23c94e640aaece", + "sha256": "sha256-Tiyq9PdGXZcAlzvXwzihHn5ATQbzZpzjvJrSwAaW5e8=" + }, + "play-sdk-proto-31.1.1.jar": { + "sha1": "95e818a5c126927c34d12190deb8e55f5c8ba306", + "sha256": "sha256-XHfNX6vj0ognLAiLBVdgheu/0d2qwpm767k85TsP/n4=" + } + } + }, + + { + "path": "com/android/tools/repository/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-26.0.1.pom": { + "sha1": "1ce1944d6e8602c7fd60ee462522a18f1b93fa5c", + "sha256": "sha256-og+ybI3lsP96MGnmgfzwHr79Lz0kuDLDryLZgdfUN2s=" + }, + "repository-26.0.1.jar": { + "sha1": "bc0dfe02af8d075f6b7ad63a202ed4c63a6a647d", + "sha256": "sha256-b1K6kzoRe9ZxMbKBIUUQ6jAjK5gHv+VpvAwX5qLUAYw=" + } + } + }, + + { + "path": "com/android/tools/repository/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-26.2.1.pom": { + "sha1": "ed8a2e0c85f79d8961ddc4afb0b92e0924c4c221", + "sha256": "sha256-6weK37II6udSTQDQzgMZaiyBNT23jH+WE4hcGWkM8t4=" + }, + "repository-26.2.1.jar": { + "sha1": "7a72673cd6b44aeafab3ccf831fc0b95cb61f3b5", + "sha256": "sha256-+nTa4JED+u9wPfOFUK2PokTFttG/kNYZi+kyKSs9nME=" + } + } + }, + + { + "path": "com/android/tools/repository/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-26.5.3.pom": { + "sha1": "f8f4198066a0abb5dd28ddf7e7bbb9ce9c6df714", + "sha256": "sha256-1VAgYQtJuNTY+UgjN7nkcyAEjcxoEYb4tLuJAxaMxUE=" + }, + "repository-26.5.3.jar": { + "sha1": "91466e99a63da0267b17bdfb617f54582e311618", + "sha256": "sha256-X8tsUpwFkmiZtGzyUA46ChpjOV59RY6na8sHXKoF1Lg=" + } + } + }, + + { + "path": "com/android/tools/repository/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-26.5.4.pom": { + "sha1": "dbc9c0fdda79c10f8c236bfbb2edf70352144c6d", + "sha256": "sha256-SLRX4bzjoInUaNoXu3RVHLGjsk7x5bplTufAgeHLsl0=" + }, + "repository-26.5.4.jar": { + "sha1": "41dc1986340a058bcee2124128c5ba9e1b52cb1f", + "sha256": "sha256-xQ1chRSpO8aDs5mGoQ8515uEussKn0xp34cETKhMQEI=" + } + } + }, + + { + "path": "com/android/tools/repository/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-27.1.0.pom": { + "sha1": "df89c2efd1ccf7da130c0fd6f7846ffe1e95edf2", + "sha256": "sha256-e90nWAIy2Kkznf5sKmScDErUc5tKLXatWvJGYIHcyFA=" + }, + "repository-27.1.0.jar": { + "sha1": "cda08c985c2de2c3da0701e696b7b4cd088d3cba", + "sha256": "sha256-yi7ry7LJdQeAVu+KVdF8KiYrvpRsgSu8TUoO+K4eVHk=" + } + } + }, + + { + "path": "com/android/tools/repository/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-30.0.4.pom": { + "sha1": "bf8e314c90821bbc47055be1c6bad14d956357b9", + "sha256": "sha256-8IMjoq2XMonJyZ8jsTtRZKJOyRTJyCw03pFItPGOM7Y=" + }, + "repository-30.0.4.jar": { + "sha1": "979e072ae320ac9d14477fa3820e6ecdbc88b921", + "sha256": "sha256-/3fdp6CUprKuXWMeyspZwmwGc/WbHl+HQ1LBfpLrmh8=" + }, + "repository-30.0.4.module": { + "sha1": "a01a7ccf2e615a2b3e163ac4b8c6e9c19906f446", + "sha256": "sha256-UmPePq76UWcGzD+gH9MrPsc6dYrpOAd94v2DRd+P/V4=" + } + } + }, + + { + "path": "com/android/tools/repository/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-30.2.1.pom": { + "sha1": "3420f36ee3baf9d835964c31af4ab0630bd3aec5", + "sha256": "sha256-D1wkMN3G9RAmgkbEmeZ5XlLrgtPOL6lN3JzNgGNDnhA=" + }, + "repository-30.2.1.jar": { + "sha1": "bc22606d2fc0e0d840cb906c3ec90d14e4f377cd", + "sha256": "sha256-5cSCYYUQ6/edS6nIlo+YSj0+itvFHb+oKI/RR+tYfoo=" + } + } + }, + + { + "path": "com/android/tools/repository/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-30.3.1.pom": { + "sha1": "de69354e4d4a7ba2fd14ef53397c865ccfdeca52", + "sha256": "sha256-7E1txtjP8zsUHdxo8ajkvxO21xbG7lMSQi0CuWhhlvk=" + }, + "repository-30.3.1.jar": { + "sha1": "87b80c1a37843e2db9a3356d2bb5b6f59e4feb0f", + "sha256": "sha256-o70RtDTM3l35Ded/2FM2HZnBUHeMzlY40c4l+CC54kM=" + } + } + }, + + { + "path": "com/android/tools/repository/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-30.4.2.pom": { + "sha1": "e1875e1d589a81f21cc99ccbcb1f7ce0ac9660ed", + "sha256": "sha256-ePZamukF2lJ3vtiqPQfxtf7hr2dcm8RLn78hHSufxYo=" + }, + "repository-30.4.2.jar": { + "sha1": "0cfffc97211eb7c5c36ef16c41d2db746c0ef1b2", + "sha256": "sha256-bDlUnRiHosfgi3PWbAxPEDZXsGRxdg+2WFjJQ3VsyUA=" + } + } + }, + + { + "path": "com/android/tools/repository/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-31.1.1.pom": { + "sha1": "a3e76e4384b784dfc73d750ad56f5f8082261bd5", + "sha256": "sha256-sJIKVyTMVIqkqDAIlwW6zU+QSutJOj+j1xKYiKMLYLw=" + }, + "repository-31.1.1.jar": { + "sha1": "371ee5bee7cb5fbd24569a2c07944be4ee2b8ce7", + "sha256": "sha256-j+jp1rzr6S3U7uPo+OEitSHwOo9J4o9qgUtG44IuSqo=" + } + } + }, + + { + "path": "com/android/tools/repository/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "repository-31.2.2.pom": { + "sha1": "a32c13648da5ae844c17fa554a23c04e58051aca", + "sha256": "sha256-EhMCNh92oYvwx4ssXToYtu/zvYZGWm9p44PASaNtqOk=" + }, + "repository-31.2.2.jar": { + "sha1": "31525288e1be940e531a46a4d8392542c66649d9", + "sha256": "sha256-Dxd4qYp5+286j6URa38WCSlgv0lQmAJlbBEXx1zHh1w=" + } + } + }, + + { + "path": "com/android/tools/sdklib/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-26.0.1.pom": { + "sha1": "220be342eb8692d2d1b864cdf2d0c275c7aa5c19", + "sha256": "sha256-KgR1wmJfwn+A08UCETBQm0Bo4PmE8lEeenegl95ItB0=" + }, + "sdklib-26.0.1.jar": { + "sha1": "bdb4723f8e3791f1d3911b1f191acf25840f352a", + "sha256": "sha256-zyUSeVBZYIibaygcJq8EOGVuusOEXlJ5GSXPdknLFaY=" + } + } + }, + + { + "path": "com/android/tools/sdklib/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-26.2.1.pom": { + "sha1": "0a5d07fb6e3ed7ebc33032538f275aa75171213e", + "sha256": "sha256-cDHS0JaDmWrBJgdL495t+bPegPfg8QuApuh1T2nIACg=" + }, + "sdklib-26.2.1.jar": { + "sha1": "6bedd85cbf10816816e71846600ffda8d5037c8b", + "sha256": "sha256-JI33rV6sSutvlsOUx2dg3kt7iawFblTQwhpzk2i5G0U=" + } + } + }, + + { + "path": "com/android/tools/sdklib/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-26.5.3.pom": { + "sha1": "08fa943d8253d6301e2dd02bb953fe73bd1406f7", + "sha256": "sha256-UQ3bott3wc/Uw37/g/L25qPv4gXqt8Zkgu7QAUgtsTs=" + }, + "sdklib-26.5.3.jar": { + "sha1": "8c305bc044ad81115859eb964c2829cbea70b036", + "sha256": "sha256-H8X6i5qnJUBBwF5s8fPrqfR7BJH3p4ZDwhy+eylFHkI=" + } + } + }, + + { + "path": "com/android/tools/sdklib/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-26.5.4.pom": { + "sha1": "40ed09458a36fa35cdf2efc58acebc65e9edc3f6", + "sha256": "sha256-rTjk0Bn1eWnRpHC3b/hO98MJ7ntuh/YTr662EmpyRYU=" + }, + "sdklib-26.5.4.jar": { + "sha1": "c1a4303d0e148a08be80c9ac1d9ea1c88ee0a85f", + "sha256": "sha256-u3FT60VR/fqAxvioFWxbgZsByBLnNzTxVweJST0MP1w=" + } + } + }, + + { + "path": "com/android/tools/sdklib/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-27.1.0.pom": { + "sha1": "7186a84cb4cbf9626b7bb536a18180f9d38c9aee", + "sha256": "sha256-tbZHzUiN9YT2tKEXv5bdw//dHfwsXEtTUqC+vuLcP3s=" + }, + "sdklib-27.1.0.jar": { + "sha1": "a1ceeaa51eab6500bfa772e183d252903ae6b767", + "sha256": "sha256-vguQymPh89JBRpjRgleiHst5oMq0l/38uXbzCqMytM8=" + } + } + }, + + { + "path": "com/android/tools/sdklib/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-30.0.4.pom": { + "sha1": "905d5d68a4ccec545bc30a953c4a3f258da00087", + "sha256": "sha256-JVvi3cSild7hjl0lWmxKsdoi2u2ZgR6p1wBMaaB+Fx8=" + }, + "sdklib-30.0.4.jar": { + "sha1": "220c06fad8feb0e8d62515ace398caaecd63360f", + "sha256": "sha256-vP5o0uwCVp9Al4yTgKMwLWD2d3LA3qlYOPgX9c81Bag=" + }, + "sdklib-30.0.4.module": { + "sha1": "ddb6f011fb4fae7fc3d15e4a0420a27088e0e633", + "sha256": "sha256-yf++U8b4Dh5+Cc48IGjqjD3jJKEazFzqR4xBuUJQ5Ag=" + } + } + }, + + { + "path": "com/android/tools/sdklib/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-30.2.1.pom": { + "sha1": "25d36a77372dd462cf82d6bd85db673d2102c571", + "sha256": "sha256-uQSC+0J/OQaGsAh3mAR/VwZaK/vOfrw0V2gGUs73cWc=" + }, + "sdklib-30.2.1.jar": { + "sha1": "a96285f990a3ef4e4931f79f811dc3bebb8e9ebe", + "sha256": "sha256-aiBVKeB8eDHStgu/xWuCwONUyGKPWhEb/7fF8rkwoRw=" + } + } + }, + + { + "path": "com/android/tools/sdklib/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-30.3.1.pom": { + "sha1": "4348f79a48d98ac33b7d48181a6a0e8d483287d7", + "sha256": "sha256-PUMqUs/CkGj43GBsLPSnYgri7sITKZHXih+w0YSIRJk=" + }, + "sdklib-30.3.1.jar": { + "sha1": "c7d208823ca1a0b5c11c1b94329f517d9582b346", + "sha256": "sha256-V+FH5cOt1b+hxT64GqJO2+IFb70iEKAMmnFAmuSYNWs=" + } + } + }, + + { + "path": "com/android/tools/sdklib/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-30.4.2.pom": { + "sha1": "032c8f7c15c9d514f39c68742b7f8a1caab5a6cb", + "sha256": "sha256-cUub6MJvEAjcRDaW0bVueAkFEggEak7wfB41x7xy6Vc=" + }, + "sdklib-30.4.2.jar": { + "sha1": "35a703c17c59fbf4c6563e35c4149b1522a82ad3", + "sha256": "sha256-h3hhNlPFe0KavvWLZ/PvC+FqCwGPjqRCKY14OU9CKl8=" + } + } + }, + + { + "path": "com/android/tools/sdklib/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-31.1.1.pom": { + "sha1": "33fba3b4263ae6ce1fba67fb32f0c42915991e83", + "sha256": "sha256-T8tDfb3B00hD3bgK5j0cicZQ5xBkYtQdJkuw0fApAT4=" + }, + "sdklib-31.1.1.jar": { + "sha1": "1ee002c6e1a7c207e2f03eba15864479ad8afe6f", + "sha256": "sha256-mzRPZYfokL0NS2hmfI+2Ur4SIn++TCHNiCTSkuTtbds=" + } + } + }, + + { + "path": "com/android/tools/sdklib/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdklib-31.2.2.pom": { + "sha1": "adfc68ac2ed04a7aa9a13a9892f3d30b36174723", + "sha256": "sha256-V8oWJsVPkiQ0+F7epEj2L8tn8yrQOU2el6YCwNZbLGU=" + }, + "sdklib-31.2.2.jar": { + "sha1": "fe33505316b16e3f8874b4746a7992e97a08b3c0", + "sha256": "sha256-T8by1G+XkMn5tYVz5vX51R0gaUFJugFc7XIBButmaqI=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/26.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-26.0.1.pom": { + "sha1": "4a2198af4193975c9d22884503b7bed6e42d2460", + "sha256": "sha256-qWYPi2S3T0dCJscKUBh9jvysRI0yzpd1b0CJdapSzao=" + }, + "sdk-common-26.0.1.jar": { + "sha1": "9ddd2fa552e7b49548c421c0be4e65dc9a245f69", + "sha256": "sha256-fovcooG9y22tSKgIVIN/HAq0bzGgspI3DjIPRx9bnP0=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/26.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-26.2.1.pom": { + "sha1": "e84475bfabf0dcb1d9b587a847c183d23b13fa85", + "sha256": "sha256-2r9INQjzZwToodfCnNpRNa2c22Fp9VaQgYKVTuiOCx8=" + }, + "sdk-common-26.2.1.jar": { + "sha1": "bcc236baac9d05aee927370ccaf5785d92ed5e2b", + "sha256": "sha256-dZ1LKSymmjXPlh/KN3tUFY/GyIEIl4AGmZRC6AoBHPQ=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/26.5.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-26.5.3.pom": { + "sha1": "3fda12d2fca4442d791813f7a73399ff5f6e1973", + "sha256": "sha256-RenED9V2i7mYpkVhbqjSVGv15o9fBkL34FpKhwZElZk=" + }, + "sdk-common-26.5.3.jar": { + "sha1": "cce8ff081940bb7c9b1368a4052f987091996887", + "sha256": "sha256-kDfQjhoo5b8rEzc6Dyo/hxRsRG/gkYeNNOtvHxyIBgk=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/26.5.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-26.5.4.pom": { + "sha1": "f148513aeeeadae52d1ed5fba5c22fcfb262a0f2", + "sha256": "sha256-EvrrD+l5DZ+oqipXnAUHv3qELl7oa/9N1lIxiTLI98s=" + }, + "sdk-common-26.5.4.jar": { + "sha1": "79236d1cf1374b157a92e2fd473e572279f2551d", + "sha256": "sha256-J+NBifGV15ZWunqq9da+b45mlHsTwqQcFWttedqhYjw=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/27.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-27.1.0.pom": { + "sha1": "1d27bbdb632e5926090d333972d1b35fb573dbf5", + "sha256": "sha256-1vI6mzbE051D/9NyiOLGnhhBTpTFk4Mjrz+fNrqNb9Q=" + }, + "sdk-common-27.1.0.jar": { + "sha1": "8afcbb45547380b94b589b48058b7a0127f60b82", + "sha256": "sha256-/7iL3VtFFG8+ktte047MLsCUX98jRFu/NU7wv2/51uY=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-30.0.4.pom": { + "sha1": "3371f37867b60f697f6cf18ab1209e06d2715ca9", + "sha256": "sha256-hyivjA+EVLNakuew07LX5LAHCGGumW+7vIZSZsxgfPQ=" + }, + "sdk-common-30.0.4.jar": { + "sha1": "181552edd229caa539812529d58b596db1d427f3", + "sha256": "sha256-tGF2MPnJzIrYDxqrmAzFYNj3a9JuhEIepU844Dazvkg=" + }, + "sdk-common-30.0.4.module": { + "sha1": "5653148e7d9fee6b4d2b0369b825bc3c09b71079", + "sha256": "sha256-Vg1L64GWhieAgqBLk8oN0j81yJ29SLTpTDPQD1+qpMU=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-30.2.1.pom": { + "sha1": "e913b187815192056ad3e9234f9432d60e3c8a1a", + "sha256": "sha256-zXayh8UKgdWRsfM8KzfVv9i8GVnasGegI4m7nKb2klg=" + }, + "sdk-common-30.2.1.jar": { + "sha1": "a7e1e2886a37c26c11e89802708c51f5e5052b26", + "sha256": "sha256-SkX+p8A6zUT2G6Fp4Mm9XA1pH9PhjjvUxLAqlttdk00=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-30.3.1.pom": { + "sha1": "65b950f846c0097e4c48c007cd13201272f32341", + "sha256": "sha256-ugbHYaolfL6YIZQfmi233OqB0y48ZzOfzhyqR9mWrE0=" + }, + "sdk-common-30.3.1.jar": { + "sha1": "10deeec417af159c070f839d6eb4dae29a293593", + "sha256": "sha256-++a337MLOKsdF8XGtmCoRGrCAj+wz8oyMRJvrO2sK3w=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-30.4.2.pom": { + "sha1": "419f995a26f33ffc0145748ca4509419f904182a", + "sha256": "sha256-VNZKa7iHQn8+VCcv3eU/f//v1DxYKF/LUAeL9mnjRt8=" + }, + "sdk-common-30.4.2.jar": { + "sha1": "161759948f92f7b7fe44e6e01049db349451d34c", + "sha256": "sha256-PFUXuaZf0zwqHK6reHMG1OO7IzcT57tHus3c2E2y6g0=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-31.1.1.pom": { + "sha1": "0a394020b25ae399f47b4fccc4b7d33752969635", + "sha256": "sha256-Kg/G2/sjSgfnqRwPcHvlJEayI17R65N6+NXfuZwLpFU=" + }, + "sdk-common-31.1.1.jar": { + "sha1": "a7450a5a1fc9574099f661f44dd056637de82dc6", + "sha256": "sha256-R6Y/PJV8x6EIAA+UX9z/k1FI1cITy1iHRyzAkVmaRMM=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "sdk-common-31.2.2.pom": { + "sha1": "0f766bd837a64596fafb7d7800df6206b27f0e24", + "sha256": "sha256-nMRVZo2tsLhHW8ljpouTnhdmBVoeKspX/4kuK73+5NI=" + }, + "sdk-common-31.2.2.jar": { + "sha1": "432fd187a7f19f9670169918c8c01d1af08b70cd", + "sha256": "sha256-YhAm4Sdy3UStXL1YCPkf5a8PO8l2zZsrPPrZI2hIY30=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-ddmlib-proto/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-ddmlib-proto-30.2.1.pom": { + "sha1": "1427c83e642cdd9aab6f3a335a2a3de7feb2e957", + "sha256": "sha256-hJzmRkbfK9qpicAI/w/1bA06tQTReLkzxGHCes/HhQQ=" + }, + "android-device-provider-ddmlib-proto-30.2.1.jar": { + "sha1": "0bebb1d3ccf4362611d1bf979f28e5cf9031a663", + "sha256": "sha256-sDbxWcBTXGyBk5ItQwynac5BTByW/cWw4L0S0ny3oXk=" + }, + "android-device-provider-ddmlib-proto-30.2.1.module": { + "sha1": "4453ddeb0633da971a421abb23acc71fd29905ab", + "sha256": "sha256-a94yr1AhCwBkGhibDfuWUC4atf6e1MTWLhrTFfy3nq0=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-ddmlib-proto/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-ddmlib-proto-30.3.1.pom": { + "sha1": "d6faada1ccf7f24c8e779924cd8a2ae4ff36d538", + "sha256": "sha256-7cIg+/t/3f6gjh1RfqJHhXBOMQxBSk5vHYuhoYq91tU=" + }, + "android-device-provider-ddmlib-proto-30.3.1.jar": { + "sha1": "bb5d5b14533aad60deca6766fa403f9e335a9cf9", + "sha256": "sha256-6nYV6m23YC6TekWD/GgYFisQj7PK1v5hWo9bqCzSja0=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-ddmlib-proto/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-ddmlib-proto-30.4.2.pom": { + "sha1": "4d2eaf0decbf49a7807e4599a4851b18f4d80dfe", + "sha256": "sha256-lXcH8LY5Ujd+xeclCzf2270RDRZSJXdmMAxzc72g4g4=" + }, + "android-device-provider-ddmlib-proto-30.4.2.jar": { + "sha1": "7e8d418bf45f4f9f8cc1708429aa996d34b938b2", + "sha256": "sha256-MyeU2x53AslonIB1Uy8jXMy/V9cKuinL19N454G6038=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-ddmlib-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-ddmlib-proto-31.1.1.pom": { + "sha1": "4bc1293e5b3a7bad67001637342a2c9d5b5e1db7", + "sha256": "sha256-Gb89ldbzRJxJt5KoWFShsu2aD5dEEt9vsse3BdzBb+8=" + }, + "android-device-provider-ddmlib-proto-31.1.1.jar": { + "sha1": "70f460be43fe32ed535b1d34a9966ac955191ce1", + "sha256": "sha256-GiiN/SL1uqA2v/3906x1BGZLlMhtv2vjZjDlWHwi6P4=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-ddmlib-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-ddmlib-proto-31.2.2.pom": { + "sha1": "4c55f2e09e9aae49caf048879f0425cabca5a61e", + "sha256": "sha256-kenLogCxWsYw2c/6x7vpMjKA2eCGAOmfKaoEDLqRjZc=" + }, + "android-device-provider-ddmlib-proto-31.2.2.jar": { + "sha1": "70f460be43fe32ed535b1d34a9966ac955191ce1", + "sha256": "sha256-GiiN/SL1uqA2v/3906x1BGZLlMhtv2vjZjDlWHwi6P4=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-ddmlib/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-ddmlib-31.1.1.pom": { + "sha1": "5a93dcc8c4b945d8d18d9614394c26f6e22cefdb", + "sha256": "sha256-Uxo0l7u7qMKOHdbyZ1B8aMaMkbGj+8uraw/ENttUpQU=" + }, + "android-device-provider-ddmlib-31.1.1.jar": { + "sha1": "d2d85ef7f7ee4b5e00edf6c8d031a35499f88498", + "sha256": "sha256-ooF1h72X2tSBlr2VhknwqUH+5ejJOjq2I6LpuOGJ8tw=" + }, + "android-device-provider-ddmlib-31.1.1.module": { + "sha1": "d78f4f7992f5cfdfb5d253be0be5b2c4d1cd3b28", + "sha256": "sha256-xgl/VY+OwGXhFohR8AQ0jJtPQH9XtI83600N+g0e78o=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-gradle-proto/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-gradle-proto-30.0.4.pom": { + "sha1": "11af4ff0575ad2942becc28558ca35434711d394", + "sha256": "sha256-EwAREmTwfnGLl5BVUni2AIsGnhhnZ8yOT41/eMvT76w=" + }, + "android-device-provider-gradle-proto-30.0.4.jar": { + "sha1": "9681c2de741c515493b056d3384201e5184ae758", + "sha256": "sha256-6OgTGOKFr0gNm1uewwbO3zB7Vs+ButdJI/uD8FwNcu0=" + }, + "android-device-provider-gradle-proto-30.0.4.module": { + "sha1": "c83d324062704a5b36de081ec553923da822b830", + "sha256": "sha256-USDG58CHnzOKRyZbjNK1IpH/yhhGHui30cOcF2yRdYU=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-gradle-proto/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-gradle-proto-30.2.1.pom": { + "sha1": "65c8a247199bcd1d1fc253137085353a38771f88", + "sha256": "sha256-y3oNwkcNAhTzvBYPqzrKD8bOiHHkIB03XiBRMXgG684=" + }, + "android-device-provider-gradle-proto-30.2.1.jar": { + "sha1": "5f52519d313aa596d36cad23f366955ad543d71f", + "sha256": "sha256-LDYWTMKhAszk89xgJXg5aaTosM/vfQPKxkWREXh1xtk=" + }, + "android-device-provider-gradle-proto-30.2.1.module": { + "sha1": "5c18c38f5eed9daa3a225780fcd5e09e1359179f", + "sha256": "sha256-Kj3Pc4f91+8A+jYdm0IvViNCXH4x0gsVlX7AKS2pjs8=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-gradle-proto/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-gradle-proto-30.3.1.pom": { + "sha1": "2078aa1506edd1f46e936204231c14365f03db20", + "sha256": "sha256-jo/J2raisD1lMPykkP2PKBY7mh07FCGFOkHdILbap3E=" + }, + "android-device-provider-gradle-proto-30.3.1.jar": { + "sha1": "e1427d46551936e6d3c2a0d6be7e4469bbe257ae", + "sha256": "sha256-pxy0Ylo5y4zgS8CDOu/vuVY4+ccCn2ZNpqufkb47nNk=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-gradle-proto/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-gradle-proto-30.4.2.pom": { + "sha1": "5868248dca0e39731c8f02a0d8222ddb956a423e", + "sha256": "sha256-W0RlAvSJOWb6OaZm93hRUfztYvUuwznjDUrD2WYatE4=" + }, + "android-device-provider-gradle-proto-30.4.2.jar": { + "sha1": "2bab341f7cfb927f41ed2fe379a32a1805a14bfd", + "sha256": "sha256-2OnN1gdXUu2IQipcNzJNmqaioOp5q8vGHcyJuOR/jpg=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-gradle-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-gradle-proto-31.1.1.pom": { + "sha1": "86de0ad33981a3541f354633ec68297b00208f88", + "sha256": "sha256-3se5RgHpRQA+pQk6kYy3BOHqet34jvMfkUO83eFhHrk=" + }, + "android-device-provider-gradle-proto-31.1.1.jar": { + "sha1": "622ec6748a5fd9804d1f1a1239826cbd73a960bc", + "sha256": "sha256-EPm2IKhAP9apyenGNcY6eXUzex3rXDdbeaaWDrWviNQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-gradle-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-gradle-proto-31.2.2.pom": { + "sha1": "0e4fee521edcf916d5462dd2e2fbbd7ec059b97b", + "sha256": "sha256-Eker0ZoktQxYJanXLxYg93cSPf6Ig/8rfeR5d2KxwhI=" + }, + "android-device-provider-gradle-proto-31.2.2.jar": { + "sha1": "622ec6748a5fd9804d1f1a1239826cbd73a960bc", + "sha256": "sha256-EPm2IKhAP9apyenGNcY6eXUzex3rXDdbeaaWDrWviNQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-device-provider-gradle/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-gradle-31.1.1.pom": { + "sha1": "ed509001d928123866e9fab49acba53b21bb3eb7", + "sha256": "sha256-ojgpQhfiv8CBwxiHE81PubbFV/BYddsfW6Mrb8sj1yA=" + }, + "android-device-provider-gradle-31.1.1.jar": { + "sha1": "151da3d432338841b7948a1466072219381b7378", + "sha256": "sha256-xa5Cx7wvIAxmRW+zrz8V78XWOTGH0tg8QQBUPn+Sy50=" + }, + "android-device-provider-gradle-31.1.1.module": { + "sha1": "00c8de468126b4ccf35de6736b9ed1a9073fd46d", + "sha256": "sha256-frh/1jZfWckd/pwxCqWjpe4jIHiImgyh520inayMj3M=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-additional-test-output-proto-30.2.1.pom": { + "sha1": "ac78ab72bc6fa8e935fd8ccaf9801573bb6d555e", + "sha256": "sha256-K+qQ8CrPTEZQab2TaPzSS2tGEGmcVeOELEFlm0zceeI=" + }, + "android-test-plugin-host-additional-test-output-proto-30.2.1.jar": { + "sha1": "3ef1d596205c37cbc0914f6e0ad38c9fbb7ed6f9", + "sha256": "sha256-LYqcLYh5I/EbEpwjh3bYhI0Yff8QPGI7uq7UAUjjQRY=" + }, + "android-test-plugin-host-additional-test-output-proto-30.2.1.module": { + "sha1": "35495ca9595acfb24bcd4b6d991910b648049551", + "sha256": "sha256-V5gU77khEHAnxjBpETxO6/Ai3J5+Pa6Ebi5tRdk7auU=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-additional-test-output-proto-30.3.1.pom": { + "sha1": "ae7ee92f46572a79877d6550bfc877ab1c0544e6", + "sha256": "sha256-31ccybUnhTCe4NeBARTbqzNMmn/U3ct/orlvMLmZB6A=" + }, + "android-test-plugin-host-additional-test-output-proto-30.3.1.jar": { + "sha1": "f970263cd7f02e515393506fe846bdda43fd718c", + "sha256": "sha256-McWgs6EyOXDqrNdxcn7BOGO22+YAeeDA0CmBa2QTdm0=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-additional-test-output-proto-30.4.2.pom": { + "sha1": "1bef89e8a6555e3d15a5384f5cd87d857752de1d", + "sha256": "sha256-W5EXN/1G6SLG6DmzF0b3tNZgT3s/lcoNvy/RfFNPUJM=" + }, + "android-test-plugin-host-additional-test-output-proto-30.4.2.jar": { + "sha1": "3d36dd35dd38c03783614f8e47d39ef942adf757", + "sha256": "sha256-6cp2VjB7/zFspGEqMemWs+S7PKVHEkjpmt1Cb/11mTY=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-additional-test-output-proto-31.1.1.pom": { + "sha1": "22c399aaff7e75598fe6a97370426195784e73aa", + "sha256": "sha256-eZTLs9edKHqHzgCT3Q4ZR3DUTM616FO+0uvZGYMLvII=" + }, + "android-test-plugin-host-additional-test-output-proto-31.1.1.jar": { + "sha1": "94f0a14f0c6cceff5a0374a2d4e0e50bac122899", + "sha256": "sha256-CNEuVfqRq4PdG3V828o9dGgJsKUCaPdc9SGU9GosZnM=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-additional-test-output-proto-31.2.2.pom": { + "sha1": "da4dabb1e69f62ce059056d30891cd8395074ba2", + "sha256": "sha256-FVnF3dMdzfVRqcYGLdKh+KCIg+TwzzjRnJqeX/t3hsQ=" + }, + "android-test-plugin-host-additional-test-output-proto-31.2.2.jar": { + "sha1": "94f0a14f0c6cceff5a0374a2d4e0e50bac122899", + "sha256": "sha256-CNEuVfqRq4PdG3V828o9dGgJsKUCaPdc9SGU9GosZnM=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-additional-test-output/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-additional-test-output-31.1.1.pom": { + "sha1": "b0238a1f48147540a3fd0ace38fd97a59d9a648a", + "sha256": "sha256-wigsQZo7fHISmfDTBqmC5I0H6HbvEme50PzltgwwZyA=" + }, + "android-test-plugin-host-additional-test-output-31.1.1.jar": { + "sha1": "76c98ceb7ae31cb927de5950defa85b8c31f648d", + "sha256": "sha256-S13qjGveeBjNJsWlo8BHTdnYjk2655fC4Pd6FA+LmwI=" + }, + "android-test-plugin-host-additional-test-output-31.1.1.module": { + "sha1": "55dfc900b82407e179c7a119ad8bccaf8d7dd0be", + "sha256": "sha256-ukAuwJXv68mH+7gCtkg/9b5eyzA7M/arzyQfK4Z6zXA=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-apk-installer-proto-31.1.1.pom": { + "sha1": "a50cc0d58a363749305adea12b502c58a728793f", + "sha256": "sha256-5+T7C5TkhF0AJiYA01V3NlPOh3PnHUvq6/zpChHnbvU=" + }, + "android-test-plugin-host-apk-installer-proto-31.1.1.jar": { + "sha1": "c14a7d0b4e1166a2035b26be175fcd52ce167844", + "sha256": "sha256-q9KqyBaoHir3cF8+3/PeIrzYBiEqHRWPQyo5sGrlROQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-apk-installer-proto-31.2.2.pom": { + "sha1": "efdcfae23cb84987ba817e809c75b124663ba0b2", + "sha256": "sha256-L3Q9yl855otvO3d4TWUaw+LG0flKjxCcul58qQzSjoI=" + }, + "android-test-plugin-host-apk-installer-proto-31.2.2.jar": { + "sha1": "c14a7d0b4e1166a2035b26be175fcd52ce167844", + "sha256": "sha256-q9KqyBaoHir3cF8+3/PeIrzYBiEqHRWPQyo5sGrlROQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-apk-installer/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-apk-installer-31.1.1.pom": { + "sha1": "78ced250bf6d0b637929b0bf00e48e06baad0022", + "sha256": "sha256-mwGIXRC8xdAQHLzZCiseQQZCszw0syIpRLuUHc9ZFa0=" + }, + "android-test-plugin-host-apk-installer-31.1.1.jar": { + "sha1": "43aaf429a440d796073491b982d06c081378be71", + "sha256": "sha256-NjGrunFwe7q6IeTqeuQHyLCn8SvX4KbGl1i5+Q935oQ=" + }, + "android-test-plugin-host-apk-installer-31.1.1.module": { + "sha1": "b621aa3535fb2d704b84f3271141f9046cc4a384", + "sha256": "sha256-GM+G5+g5OSPODkvR6lKeHwsGsg+tq68ELy9z/A+9iF0=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-coverage-proto/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-coverage-proto-30.2.1.pom": { + "sha1": "3d57990741e6547c65d71aa80a34ecf127ffeb98", + "sha256": "sha256-zdbS+Anb1z2p9YI8Ec+4EuBunn8x4DtPNDB/HodsLr4=" + }, + "android-test-plugin-host-coverage-proto-30.2.1.jar": { + "sha1": "a7ede88b7bcca1d232cf177e4e609b2b56f3a22a", + "sha256": "sha256-AqY90slZGy93ubu1ZTFrsD+3d6kCkTBD6+ZhA7Mkzkw=" + }, + "android-test-plugin-host-coverage-proto-30.2.1.module": { + "sha1": "13358255e5b67968e194b1176ad661df563c73cf", + "sha256": "sha256-wzi2/Nkiw4u6ux6t3pjXkbgksm+JruOPOch779bf5Vc=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-coverage-proto/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-coverage-proto-30.3.1.pom": { + "sha1": "50eb20da70e41a3e837d759dcbbc5ab7b2ae8f47", + "sha256": "sha256-Xk4Vsh+Qx1g5vDMhe7Vq6rWVkKG6ZEn4p40qO1jQtu8=" + }, + "android-test-plugin-host-coverage-proto-30.3.1.jar": { + "sha1": "e9fdeb8417e35e6528fa3e352e4677c8e898639c", + "sha256": "sha256-GJWKKRkL6MaF5a5bJdmUKA89jgGbPP2SHwtJLv0YiTs=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-coverage-proto/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-coverage-proto-30.4.2.pom": { + "sha1": "287c8289747347786d3f018803b771e6d5222403", + "sha256": "sha256-4b2JmT4mqrX3qt3lybx3x8s1X2jXCYhs3uaSaS+KKhk=" + }, + "android-test-plugin-host-coverage-proto-30.4.2.jar": { + "sha1": "414b98ba9375a8c21f4c3a659a628601f2000c32", + "sha256": "sha256-L4ZctyK+Zfh3KyGK/vQ4GldFBjWbj+15LAd7QCAu8hA=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-coverage-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-coverage-proto-31.1.1.pom": { + "sha1": "b67e1d57199632f897f32a0e5b52388fe05e0505", + "sha256": "sha256-l9Wwn67C63YrMzHYsqSPc82ooKAsXIHHo0nn2wzJRCM=" + }, + "android-test-plugin-host-coverage-proto-31.1.1.jar": { + "sha1": "7b3dfe6654185ce95c2e815c3dcbc488a3ea567e", + "sha256": "sha256-nrycYnmjKkU8kM9rcys6P4ioBJjnRkAIUPLGbqeTNYA=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-coverage-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-coverage-proto-31.2.2.pom": { + "sha1": "f90553c5a79a9c7162308c8e3174ca365b5395fa", + "sha256": "sha256-XwO+Cs/MFe+FcQvWJfZ8E7unED/Vezi7LjfPDBLZ7us=" + }, + "android-test-plugin-host-coverage-proto-31.2.2.jar": { + "sha1": "7b3dfe6654185ce95c2e815c3dcbc488a3ea567e", + "sha256": "sha256-nrycYnmjKkU8kM9rcys6P4ioBJjnRkAIUPLGbqeTNYA=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-coverage/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-coverage-31.1.1.pom": { + "sha1": "8308b7301f3b2c4dca346c8156468aa14f31730d", + "sha256": "sha256-5k1h7htSX220mTkt6qV/QEU4kKHGi/zJIGyzBDuevEY=" + }, + "android-test-plugin-host-coverage-31.1.1.jar": { + "sha1": "8b05bf0a8aa635172cec6ad6acd06be95a21ff54", + "sha256": "sha256-Ix1EOPPLqJcqC/vHb+o0kf32mmPkeZ12OsYg76FHA6E=" + }, + "android-test-plugin-host-coverage-31.1.1.module": { + "sha1": "5293bcc6dcf94bdf2b31fce219c69d1cf709bbb0", + "sha256": "sha256-FKvQPDcLMNRaFi7oirf56g0IZzwbvDMccDSRKTkKG2k=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-device-info-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-device-info-proto-31.1.1.pom": { + "sha1": "e431e0b44d45d8183fc5bf15ba9bcc5eaa8dcef1", + "sha256": "sha256-EIsBpHV3rxifthNWYse7llurxDsu05XtRVkXFuNl57Q=" + }, + "android-test-plugin-host-device-info-proto-31.1.1.jar": { + "sha1": "7390ae04ab150219e54b9903e1daf46a94491be8", + "sha256": "sha256-PViByYS6b3rr8/efaYQmVfe6yZY9pooPx/HpQHWLTzE=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-device-info/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-device-info-31.1.1.pom": { + "sha1": "d48027e88b85bd9f77c002aebd9baecd4a67fcb8", + "sha256": "sha256-UjNB08vHsYMMEjF6vR3GDgE/Z9EpnYuR74SXA0BrVIY=" + }, + "android-test-plugin-host-device-info-31.1.1.jar": { + "sha1": "2664dd4c4e5f9cf263f734cfc1aa2115c7d9e4bb", + "sha256": "sha256-U9Xkuz4EJX+knTO7cgpyTFkf8C/ztcQkarMwnmcer/s=" + }, + "android-test-plugin-host-device-info-31.1.1.module": { + "sha1": "e97faabcb70405dacb7788ea143eb756209728ba", + "sha256": "sha256-WCeCXGTA2FMC1Ec/++8rpC2tXr49mue3xmh8li7rs6g=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-emulator-control-proto-31.1.1.pom": { + "sha1": "fefbd087c85bddbaea95ad31d936457746baf3c4", + "sha256": "sha256-zG1W6z8FRAboysNo6VkMbEA7gSVicVcjZV2139H0xUw=" + }, + "android-test-plugin-host-emulator-control-proto-31.1.1.jar": { + "sha1": "11cb514e23daa4b9e3b8bd395014aa58ada6bcec", + "sha256": "sha256-Q1Q4vIQBbsXSjDTJ0BewE2XzeUkWjP/IESbUN1fFPKQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-emulator-control-proto-31.2.2.pom": { + "sha1": "4491ff3a5ceffe3d6580b5ba95b07171d268d2bd", + "sha256": "sha256-3FOV8DXljpnAb0geGcNL6LP1ikcpFMOPcZx90kH3g+E=" + }, + "android-test-plugin-host-emulator-control-proto-31.2.2.jar": { + "sha1": "11cb514e23daa4b9e3b8bd395014aa58ada6bcec", + "sha256": "sha256-Q1Q4vIQBbsXSjDTJ0BewE2XzeUkWjP/IESbUN1fFPKQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-emulator-control/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-emulator-control-31.1.1.pom": { + "sha1": "215385ff445af80a44f08174a4b28b496ad67014", + "sha256": "sha256-JHhS9eE29CuIwRTO0SSjo7Ndk7e2TqiyKRUAqt6lkxA=" + }, + "android-test-plugin-host-emulator-control-31.1.1.jar": { + "sha1": "05e8fd4c2d96a69e4c0e239611119127a216990f", + "sha256": "sha256-SWaY9dLN5cjqMSlzABZY3CjonqBqCvDPwcSsx0iaMvQ=" + }, + "android-test-plugin-host-emulator-control-31.1.1.module": { + "sha1": "513f1c03879afd1596f8f8f8457a54c3dfd65e5e", + "sha256": "sha256-PxEClC6x6zYP8/kCkRmfeZ7CST7bzrh7dTGHxikG/oc=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-logcat-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-logcat-proto-31.1.1.pom": { + "sha1": "dbe6283b78ae80fbdfdd4545b459149f3f57db12", + "sha256": "sha256-W1IpG3ahcah6GLAOM0CtKRG+JB7E1YEtnaQkvRkl3lI=" + }, + "android-test-plugin-host-logcat-proto-31.1.1.jar": { + "sha1": "fd1c47673952b6b7a50173005322d40a3a106604", + "sha256": "sha256-0xypevv81xpUdJ0mXaEaQksSLPHPvtv/LLPXNixXnTs=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-logcat-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-logcat-proto-31.2.2.pom": { + "sha1": "46d82dbfd4b2ec23508fb54fa225c595c62649ef", + "sha256": "sha256-0ojYjEnJv2WATo6P4wMOvt8zYeNdpMoqIlt3msIlLk4=" + }, + "android-test-plugin-host-logcat-proto-31.2.2.jar": { + "sha1": "fd1c47673952b6b7a50173005322d40a3a106604", + "sha256": "sha256-0xypevv81xpUdJ0mXaEaQksSLPHPvtv/LLPXNixXnTs=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-logcat/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-logcat-31.1.1.pom": { + "sha1": "f05ee8b55fbea903000f4594c62884f8e19aaa24", + "sha256": "sha256-tpwl6m0gpldBzhRm40YrTQh5DHduov2Yh7+7RTZDxNs=" + }, + "android-test-plugin-host-logcat-31.1.1.jar": { + "sha1": "e949e3c8128d45e232d0856f5a51eb7bcc453f33", + "sha256": "sha256-14v6aNi7lF4hbWgM4eGLrhW0vB9kie+pQ5IDzLkSxZQ=" + }, + "android-test-plugin-host-logcat-31.1.1.module": { + "sha1": "b4e9abf92d6e5c44e5caff5ceb5d77a94aa4b29b", + "sha256": "sha256-MmFfMQ/jMfwiY4ctjfLBnBrwnTy0fHhfvcxWRIebUYA=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-retention-proto/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-retention-proto-30.0.4.pom": { + "sha1": "f857fa2a08d13ac6b139864966d1d0c7537a1fe2", + "sha256": "sha256-/AJMOzj9bACj0/B5MEtG8NMDqWMrW9Khs9esb+3vyFI=" + }, + "android-test-plugin-host-retention-proto-30.0.4.jar": { + "sha1": "7f5cf49bb3c07664d4112aec6d4702e22e788394", + "sha256": "sha256-Fs1uB2yz5xEbX+JV46IiF6/qdKBWbmrr9HW9m8YF6C8=" + }, + "android-test-plugin-host-retention-proto-30.0.4.module": { + "sha1": "a0de6a55c83e0e835dc1cd87d05950907fc3d451", + "sha256": "sha256-QNPYRxAFo3jRSNjzlILPsAR6iCqrtXSgV/Q3B70j6aU=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-retention-proto/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-retention-proto-30.2.1.pom": { + "sha1": "490734394468879e397c6e4c95d0f39669411560", + "sha256": "sha256-gHyf/EIr8PxSm08mkyVzF098NtQfBBW3feZ8rQss53Y=" + }, + "android-test-plugin-host-retention-proto-30.2.1.jar": { + "sha1": "6c12af8ece914157353f46176e8b1d94de25e711", + "sha256": "sha256-gmzVDS5AhIf6CAQkGmZin5Z5AYba6M6znDmlpg38SVM=" + }, + "android-test-plugin-host-retention-proto-30.2.1.module": { + "sha1": "78bf67d6806ec4ba7d447b551cad07d873b0cb64", + "sha256": "sha256-CyEwFZFy4sgPGKT0u02WECVcRtjcAaEa7G99zJOBmHI=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-retention-proto/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-retention-proto-30.3.1.pom": { + "sha1": "c43e22366034f2bc79f72cb5755918c275e43368", + "sha256": "sha256-BEEm+57E3wNWUuMPDF+pG+Tu6X3l5khOsZ4yOHLZqUY=" + }, + "android-test-plugin-host-retention-proto-30.3.1.jar": { + "sha1": "8ee698bf4845058318330a48d13950f2d9c56430", + "sha256": "sha256-w09X1vSJce5xth6m8XlWk0zC3jHdg8hK57/saB4fmdQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-retention-proto/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-retention-proto-30.4.2.pom": { + "sha1": "09da25c9d4ed11ca20b4893c97de5436dcb1f11e", + "sha256": "sha256-n7m8ShlbltOBjHtXn0yCIukYxUdi9TZ5R6BdR1TtZkI=" + }, + "android-test-plugin-host-retention-proto-30.4.2.jar": { + "sha1": "712055bf1c7bea94ff84cf144cd36d73cee54800", + "sha256": "sha256-sF5kKak6pUjR3cizojr/rYUKesenQkB0UXv0LAt28RA=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-retention-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-retention-proto-31.1.1.pom": { + "sha1": "7122cd4a2be89f9e569d3bdee7aaf8ee3801c43b", + "sha256": "sha256-dgXa9CSGTdUh7AUwnsQaEja6oo2361eZg3XOaSlR9Ho=" + }, + "android-test-plugin-host-retention-proto-31.1.1.jar": { + "sha1": "4319b864edee0328f75afbc17ac4ae8997679420", + "sha256": "sha256-TRw/qv9aOLKUJvCC6OjD+LhakCCGuQ0Y9wZ1Stk94E0=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-retention-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-retention-proto-31.2.2.pom": { + "sha1": "85e51ee5ce9e123154db03a60ab66876b4afe372", + "sha256": "sha256-Uz3aAMXXmbzRtt82JV9JSJcOggaF3QB+TZxuoRnZ608=" + }, + "android-test-plugin-host-retention-proto-31.2.2.jar": { + "sha1": "4319b864edee0328f75afbc17ac4ae8997679420", + "sha256": "sha256-TRw/qv9aOLKUJvCC6OjD+LhakCCGuQ0Y9wZ1Stk94E0=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-host-retention/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-host-retention-31.1.1.pom": { + "sha1": "9d3b7186c83c05ccb576a8186f2603ca2f7f4a4e", + "sha256": "sha256-In1wb4JAsDlxusSpL+mKeC1iA2Uwaf0fTrdICsTUifQ=" + }, + "android-test-plugin-host-retention-31.1.1.jar": { + "sha1": "3e5f67258a739804fc6762f53178d6e774c7a5fb", + "sha256": "sha256-8UQy9ZIdo31Wxa/aCeGU9ffSxQU8FNyadbpcsRFbKxA=" + }, + "android-test-plugin-host-retention-31.1.1.module": { + "sha1": "d5b1faf9c8d65cc22ce4bc6eb892e8cbf8d4edac", + "sha256": "sha256-IWG940G42tcH4bMbnWU3j0ij/G3i4FrL1FNKwfLi9TQ=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-result-listener-gradle-proto-30.0.4.pom": { + "sha1": "56bd1a2eb9eb6addcf3ae5a457cff1f726fdbdd8", + "sha256": "sha256-kjV9EtU+X88jTIi2lesXxIq7k22rdpf/5gwZEHyJEn8=" + }, + "android-test-plugin-result-listener-gradle-proto-30.0.4.jar": { + "sha1": "cb6fb365ca84d67297244b8638b5791ddb6d2545", + "sha256": "sha256-lJv8VdGazwCjx9IrIu1n9zg8mqs/q//crP1W1941LEY=" + }, + "android-test-plugin-result-listener-gradle-proto-30.0.4.module": { + "sha1": "0776f5e69617a6a250325cf0bc68008c3645996a", + "sha256": "sha256-SmZPifaXUOnxBjJjE0Y7NpoCHClUWwFIkYBmu7rXKJI=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-result-listener-gradle-proto-30.2.1.pom": { + "sha1": "8e73d88cd5ff74d77949282269fc27d3f4b6b939", + "sha256": "sha256-zYnPfvWZcNfHFTWgXF5lT8Rhjdhc0BISKUhRtAQ3vLM=" + }, + "android-test-plugin-result-listener-gradle-proto-30.2.1.jar": { + "sha1": "76359cbcc3f244fb7894d6041ef2ee0750cf5a35", + "sha256": "sha256-Y7CHR1FytBp7TkuvUv/dQwS03+Rj34N424Pb/kPYUKI=" + }, + "android-test-plugin-result-listener-gradle-proto-30.2.1.module": { + "sha1": "53aabba3541a4ea91a4b4255cdae2ab9e4b57150", + "sha256": "sha256-Rxe1muh8kwwawrJsRUfoF/gPQYzscb/hEabiVroVgIE=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-result-listener-gradle-proto-30.3.1.pom": { + "sha1": "377abe062fe0fbf86a2e8fbdccc4474d1e650f17", + "sha256": "sha256-8YvBZiovuPlQEYGOe5zAU5mXHZJNIHkcXImPy5A85k8=" + }, + "android-test-plugin-result-listener-gradle-proto-30.3.1.jar": { + "sha1": "262cca037fd00f58dd5ca7dd7a32a6a837b4b993", + "sha256": "sha256-ONa4+yyleMhq7bZrFw7GG/8edO7yODSXAv3V2rsac9g=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-result-listener-gradle-proto-30.4.2.pom": { + "sha1": "b9d9d824c9e157c5e1d9a2fc4068f3f3c66e4761", + "sha256": "sha256-SPZ8kSY10vNUMwcVxOiQeTtc9z3iPU/qhRMx/tKWEKY=" + }, + "android-test-plugin-result-listener-gradle-proto-30.4.2.jar": { + "sha1": "aa3bc3b4e9a97960af10991183a4c71e5c06d23f", + "sha256": "sha256-1P2JlqyaA+5l0bGCPr1rcPsMmwclKmzSo4YUg1slZwc=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-result-listener-gradle-proto-31.1.1.pom": { + "sha1": "915320f6bcb45f1763a01ad69d4c7e82eb024063", + "sha256": "sha256-RhU/pTJduHWR1MTC+PnhqvEFx7tjLwRG79/8RXaG6Ow=" + }, + "android-test-plugin-result-listener-gradle-proto-31.1.1.jar": { + "sha1": "12e0c415c599bd8768dc1e3db076bbe921db1e08", + "sha256": "sha256-seSf8j+jV/hwKu8WMrBEBSJKOEb0/nZlTHbvm9e1pgE=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-result-listener-gradle-proto-31.2.2.pom": { + "sha1": "d50b62bd13fb9be168bfe4ee620220706b6b4839", + "sha256": "sha256-ChiX2vI+a0RdF0ht1NgL5rtM9Ejst/q+Hs49EGwiLOE=" + }, + "android-test-plugin-result-listener-gradle-proto-31.2.2.jar": { + "sha1": "12e0c415c599bd8768dc1e3db076bbe921db1e08", + "sha256": "sha256-seSf8j+jV/hwKu8WMrBEBSJKOEb0/nZlTHbvm9e1pgE=" + } + } + }, + + { + "path": "com/android/tools/utp/android-test-plugin-result-listener-gradle/31.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-result-listener-gradle-31.1.1.pom": { + "sha1": "0e7674766fc3c5b3f687d78f269def098f15af91", + "sha256": "sha256-/99PJ+50L9CFclApB8NdAGqqDo7DobUj4BOBHJFuDnk=" + }, + "android-test-plugin-result-listener-gradle-31.1.1.jar": { + "sha1": "4d3e99736da354b799507fc44d24b1165c8b4dde", + "sha256": "sha256-xAaGncP4UPmHahXEWMxs24Yob1HnnZxB+0KZzNEOB88=" + }, + "android-test-plugin-result-listener-gradle-31.1.1.module": { + "sha1": "9fbd2061c684f3fe9b03aedc3914d798a0c5b801", + "sha256": "sha256-FWn8Dl232qh9iAnBmfv5oYxABNWLaQW6aSP5IOtTg+g=" + } + } + }, + + { + "path": "com/android/zipflinger/4.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "zipflinger-4.1.0.pom": { + "sha1": "79cf7686aba1c0bed2e2aba3239b1cd8c037dc00", + "sha256": "sha256-GrcX0xkoLPS7i0IJigiCEybNkFiV70pUvprdVmiRUvk=" + }, + "zipflinger-4.1.0.jar": { + "sha1": "001318a26e3ef874686dd0e4d2a7f3a735b932d5", + "sha256": "sha256-kEARbNs+0SbTZ9tMhox2fRcXtNST7XxDbPq4iAZz/gg=" + } + } + }, + + { + "path": "com/android/zipflinger/7.0.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "zipflinger-7.0.4.pom": { + "sha1": "0c8c67d0e86857861587e1674cde9f5ef8a6ff62", + "sha256": "sha256-xuTtXruNtQ9BZc1SJPzk/bY+jgmWUaSKad/uYcSEEhw=" + }, + "zipflinger-7.0.4.jar": { + "sha1": "784467cfb5c987c3fdb358c66946d8fc21f2a873", + "sha256": "sha256-ojZXzJZ12bYH1Bnfzd8LhbbsnScopdFgQAFxbT6P8gM=" + }, + "zipflinger-7.0.4.module": { + "sha1": "d680fc45c5f949ade2709e27dbdb9fd1cafcd4be", + "sha256": "sha256-/S/J9LVewOrivlR/MH730DRcwjarkDW+hY3bsd0j9mc=" + } + } + }, + + { + "path": "com/android/zipflinger/7.2.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "zipflinger-7.2.1.pom": { + "sha1": "9cee2dbc6d70ae9d2495e874a2b0ab4d23b676c9", + "sha256": "sha256-lsVRNUcsxtqLOTZwLYmhFkCGpZjFQnFMjuLwJr3T4W0=" + }, + "zipflinger-7.2.1.jar": { + "sha1": "3a0ab31aa56c06b9839bc081b30a2c8c6e6a89ac", + "sha256": "sha256-ByB/Fh6TbNlyQy1KCfN4g2twHkncMBYun1HEH4dV0ow=" + } + } + }, + + { + "path": "com/android/zipflinger/7.3.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "zipflinger-7.3.1.pom": { + "sha1": "2422e174cf1c851617e74a84b7245c116aa0e464", + "sha256": "sha256-YvtAdwnDKnJQkFLgwf/YKnEq8nui4go9JWpXa+Yy/SY=" + }, + "zipflinger-7.3.1.jar": { + "sha1": "16999dcec6ec960f9dc53c76081936bd545b4dc9", + "sha256": "sha256-rM9AfHAmfPfrWD7VqqE1jC2P29+cd2dQEBGK9rVcDVw=" + } + } + }, + + { + "path": "com/android/zipflinger/7.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "zipflinger-7.4.2.pom": { + "sha1": "937ee64a1e7b37034d6b779161f15228f6913fe8", + "sha256": "sha256-kMNWLdSS+dzNB366rZzGpkYA4tMZ+PcfHEGvDhy+z0Q=" + }, + "zipflinger-7.4.2.jar": { + "sha1": "c8905e73d011fc1683db2dfd08397ab250672d4e", + "sha256": "sha256-2CsjlJTdJi5lzZSy7flsm1zxnHr7/ITyYBdJ9Q4Vx2U=" + } + } + }, + + { + "path": "com/android/zipflinger/8.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "zipflinger-8.1.1.pom": { + "sha1": "d7dc37d311fabbb8f0fcd44feac9e0bda4a55246", + "sha256": "sha256-wjyHzn43LLwWDPc9uCSsAvr3k3e80ZIJpxuSizxBjW8=" + }, + "zipflinger-8.1.1.jar": { + "sha1": "b450a19b332659b4190f0af34bc734a7e21d464e", + "sha256": "sha256-Vliav5Za9Y6h0xwV1nb+DWK525KYP73+jnLJg+pXfbs=" + } + } + }, + + { + "path": "com/android/zipflinger/8.2.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "zipflinger-8.2.2.pom": { + "sha1": "dfd120196062f0ef6b999743041b37229a34d012", + "sha256": "sha256-S/yIFhx6w6OHJz/8dqgknVGrrWaS+fB8GlJOcqlSrxo=" + }, + "zipflinger-8.2.2.jar": { + "sha1": "de8e3267995699af356740cf562cbedb485f5763", + "sha256": "sha256-gd1IVhilCaMjWSm56xMJHYhEUmYd5s5aRcw4scVVQhw=" + } + } + }, + + { + "path": "com/google/android/datatransport/transport-api/3.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transport-api-3.0.0.pom": { + "sha1": "153cf38690003fe8e553852f791d363324e02df9", + "sha256": "sha256-FTe+vUTaLrfjvnP8QlnhEW8qaKUwX0/iPGzqmm+E95E=" + }, + "transport-api-3.0.0.aar": { + "sha1": "7421bab19ed712c5592694e807ba72ccf39d42d4", + "sha256": "sha256-TmmDwHA7NX328cbOrLG138LFAGp4nHmf7CKYsrUzdGY=" + } + } + }, + + { + "path": "com/google/android/datatransport/transport-api/3.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transport-api-3.1.0.pom": { + "sha1": "2ba09d3090a90822cef1eae1125deebaf9bdc22b", + "sha256": "sha256-JWDCRWk9Hy1hmQO23Rt8+d8R93QYC8PwnCFZjhhvhkg=" + }, + "transport-api-3.1.0.aar": { + "sha1": "baaedf05b7d61114c4e46cdba296dee2f031895c", + "sha256": "sha256-fa/Dnw6oNUczZqzDRvfmf8hEP0xkXZmcPph7yta4jHs=" + } + } + }, + + { + "path": "com/google/android/datatransport/transport-backend-cct/3.1.8", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transport-backend-cct-3.1.8.pom": { + "sha1": "30b50332e6459a8373936d6064b42c6d80fc2d85", + "sha256": "sha256-QmmGluvVrx6zP5F+WCuqQW4omiHNg+4ynCVYUiFring=" + }, + "transport-backend-cct-3.1.8.aar": { + "sha1": "0cfd0b63e154b80207d36ef3199ae06c8fe088e5", + "sha256": "sha256-4X7dHvf9R1yQuqTjlCIzLycIfTS8tGy0jOhq+aVKYS4=" + } + } + }, + + { + "path": "com/google/android/datatransport/transport-backend-cct/3.1.9", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transport-backend-cct-3.1.9.pom": { + "sha1": "a50693077f8060bf6b89cd2f600c0f0c74beb7af", + "sha256": "sha256-POnrVvUsFelmgwMP/8jbVTvEHR6ZniB8uQTL7EXU/Uk=" + }, + "transport-backend-cct-3.1.9.aar": { + "sha1": "fcabe9fa92663233c35b6d39ca6e6c8687cc9232", + "sha256": "sha256-B6MgJaZbCO5+EdFNxTmnWLZnE/DB0TkAqBWZzq269E0=" + } + } + }, + + { + "path": "com/google/android/datatransport/transport-runtime/3.1.8", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transport-runtime-3.1.8.pom": { + "sha1": "75b152503c942438971e378871936f6e915a8fd1", + "sha256": "sha256-1v92IlH7NVlK/l7+hgtYcQZOGMC9G9t3CE41c/kOTo8=" + }, + "transport-runtime-3.1.8.aar": { + "sha1": "5632cd0e73a5e28a50761beae0b999ca8a3d69ce", + "sha256": "sha256-y5NT7xeRrhcJfYeMpxHiWpwyzskEKtxJsAyt/uGnKQs=" + } + } + }, + + { + "path": "com/google/android/datatransport/transport-runtime/3.1.9", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "transport-runtime-3.1.9.pom": { + "sha1": "070add7697b2e6d0c693f42ceff979e773b57d19", + "sha256": "sha256-hvRSt0VRBiYj+tCwu2vE3CprqfRKmYNNDlD7BHBRNXs=" + }, + "transport-runtime-3.1.9.aar": { + "sha1": "3aca9b6851513285cd500c03c94a0876cf222ac2", + "sha256": "sha256-QXRcW49CfSRDkBW4T2Ua1CBxiZGGfSr8JiwmQAm4AsE=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-auth-api-phone/18.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-auth-api-phone-18.0.2.pom": { + "sha1": "722e493bc46d182f05a240c370a783a176dbf006", + "sha256": "sha256-ngtHpWio3rnLC4LzwXbWnBTWA7yni7NnuzcBC0pksYQ=" + }, + "play-services-auth-api-phone-18.0.2.aar": { + "sha1": "7761647f560910ff979281f93d486fef2d2a0083", + "sha256": "sha256-v984DnQAqdlQy6Te3BUrG85hSoc1MAnoUQh0KJJ8wkA=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-auth-base/18.0.10", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-auth-base-18.0.10.pom": { + "sha1": "5956997efffb28851f0735c425e91e4bb0beccdd", + "sha256": "sha256-hT6lxGmZ1UNckj9wIbAzvV7AovN/mVubehOlZVGNcAo=" + }, + "play-services-auth-base-18.0.10.aar": { + "sha1": "d7b135c179bafd097e074ee1738b30414ed73d9b", + "sha256": "sha256-2L/yyY0jljP7VQIp5Sd3lCW/tR/wS5asDxLTrLoLH0I=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-auth/21.3.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-auth-21.3.0.pom": { + "sha1": "b1e26e77489a301e79cdb0443573cc00cf68205c", + "sha256": "sha256-BAKVPwZ1OIGtHlqYheRKSIRI6aMLSpNP/rZsaFTyUnA=" + }, + "play-services-auth-21.3.0.aar": { + "sha1": "82970b450dc32e97b14a51185cd06e53c65f73df", + "sha256": "sha256-XTJQDlwsIkCkky26TV/9BsKkpKiGb9T0vvspfSlfYyw=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/16.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-16.0.1.pom": { + "sha1": "324f657b74d96b21711a95ea726bdbb9600779c3", + "sha256": "sha256-o4AdCEGzv3ee90Nw4YOZw6JAHEBaBGpSjNfmCugHNUI=" + }, + "play-services-basement-16.0.1.aar": { + "sha1": "924c32f9c032821afc94dec82c0f40e81310f67e", + "sha256": "sha256-4Iv9HofE5Q73YWHXrHa4c665dTZ+6zr6Sr5i6hiHx8Y=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/17.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-17.0.0.pom": { + "sha1": "45c1c43219354319ea73c7cc7376b4f83adaf240", + "sha256": "sha256-/+kpXZ6y7C14S7TDY09qEBdSJNIREzL6JzUyC1KiS2Y=" + }, + "play-services-basement-17.0.0.aar": { + "sha1": "f94e0b893e0d5d2a922577986c8b2a31fd79f634", + "sha256": "sha256-0ySheFu8SL/jY5/IR8/Tz0PUnpZ7XK8nlCQKhUVXo5w=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/18.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-18.0.0.pom": { + "sha1": "1a4fdb03eefb2ab366885414496749b003fb6948", + "sha256": "sha256-egPcMQWShulJf0NoMUnZEm0jRxft89265xB0f1f1q6k=" + }, + "play-services-basement-18.0.0.aar": { + "sha1": "1e804f5288f6cd645a872b17556f8f375bcbc274", + "sha256": "sha256-VcF3dGeQGi05nzJSOExJdihKo1/d/VmVRm2+rLSfndY=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/18.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-18.0.2.pom": { + "sha1": "366c64a986ef9bccec67c491e7b0db1abb260faa", + "sha256": "sha256-fMoh2JXIvk/QdBlkBr81DnTTMFU1LTimioTc+ilVY6k=" + }, + "play-services-basement-18.0.2.aar": { + "sha1": "5288a9922158670d945a749d62c3b2524a1dd6d7", + "sha256": "sha256-3C+jbku+NCNy5DkxDEQVckb44y3Wx4dUJic3MGeQRPA=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/18.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-18.1.0.pom": { + "sha1": "2f77416451b95799af0867fc0b94ed2f25d767d0", + "sha256": "sha256-xjPkLa+U3xC34ddc1u+4JHySS4o1w4X1zR88uu7hv30=" + }, + "play-services-basement-18.1.0.aar": { + "sha1": "e2accf5782dedb91dddef0e86cb960cc0961ac2f", + "sha256": "sha256-A7oV84bPJgvecvxAEPuJqCDEfVHOY7wJKX6r5szgm90=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/18.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-18.2.0.pom": { + "sha1": "c9e076da5d16e31f210084fb9c26142eefeb0409", + "sha256": "sha256-4gp1pMdW4qOZL2pM4DrhV1qPg54pVMjWWegS1NE+Kms=" + }, + "play-services-basement-18.2.0.aar": { + "sha1": "726e058af3c0d8c592ebf1d9b7b68b57b1d710a3", + "sha256": "sha256-70Pr/GQdcUgVQ1JPRtEmeTtMtXv0ZsTfTOQ9DLXhG5E=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/18.3.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-18.3.0.pom": { + "sha1": "3aeb3cc4ddff06527ebf43687d3e9676ae7a1132", + "sha256": "sha256-nO9dyaaVD/Cahf9SK0dvhV637yNzqkwXM5yxFKxTl+I=" + }, + "play-services-basement-18.3.0.aar": { + "sha1": "ac6d9f53fb09f9f0b6b2d3175a38d886e06bfccb", + "sha256": "sha256-bBGuPrLdfxc3P5GcTFV6cOTPiRvAybZpJqCmRF1lQ1I=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-basement/18.4.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-basement-18.4.0.pom": { + "sha1": "e02644ad28f1a509f2b52aa4c76ebfd4de261986", + "sha256": "sha256-Bcp8Cs4NYmCTH5ftMsYM5ZgHH/Vg0/pE9J5vBpXStoc=" + }, + "play-services-basement-18.4.0.aar": { + "sha1": "bdc0da0600b30e6baed1d6b0875cdc3ae411ad69", + "sha256": "sha256-zlyTb9ZoFLNgL1xqXpKZEf+XPUsF9t6ZltpZa++XytI=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-base/18.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-base-18.0.1.pom": { + "sha1": "2515339950ea48ef3700b1b1073af9b431ff0182", + "sha256": "sha256-7Fwb6C1CX8qm2lTGICvCW8Y/1hUZ02w+6BP/rn0gPyU=" + }, + "play-services-base-18.0.1.aar": { + "sha1": "1db00193b2879dc27bd914d9d8596a080569a7c4", + "sha256": "sha256-KJbXb0Mr5SFnKVu5zkWt4lwxCu/8BNKM+NtqFYaOg94=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-base/18.5.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-base-18.5.0.pom": { + "sha1": "bea849333b5255e10aba738e7ba15f40b70fbf51", + "sha256": "sha256-JXC1FcJxevOGyJDpf2RHguP4bae2d6T/EDYUfn6mIqQ=" + }, + "play-services-base-18.5.0.aar": { + "sha1": "9b3fc5c98a0a082ce2804aa97d1f1bf39b01ce76", + "sha256": "sha256-WaXAwtoSMR112WXOH0GUmFNrGhZ/so/338Lf2c76QVc=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-cloud-messaging/17.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-cloud-messaging-17.2.0.pom": { + "sha1": "d060f8f83b19e2163b2a9029b0a369776ca10c2a", + "sha256": "sha256-ZfWDPmITaN+w6yA9yvzwcMXOzlpgyhiELOcIHUQOBBk=" + }, + "play-services-cloud-messaging-17.2.0.aar": { + "sha1": "388fac100c9074b38f67a1365c10fdbd2cb42b23", + "sha256": "sha256-JyVef+lwZIOBaxWNslzzGfaiagVm/v9BWXzogHo1Djc=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-fido/20.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-fido-20.0.1.pom": { + "sha1": "1e7769755638ac07a5455c1bc10befea32c3d8cc", + "sha256": "sha256-8uOMOdMAlhyat3Gk48eBqrGOfPVHRQhGaCoKgeswzog=" + }, + "play-services-fido-20.0.1.aar": { + "sha1": "b34ea85f12fbd3b458571081eac7417e7d6731f4", + "sha256": "sha256-s1JvDK7aqWxSbMTJupSyKdrVr+MfMvRvbNZooDYk/UI=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-stats/17.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-stats-17.0.2.pom": { + "sha1": "b7f93aa44314bffafb4b86a0b0b79bdc28acf00c", + "sha256": "sha256-aLsrwTHAk5hY4xZvd3xjkDuZDH/K48HepwoxKTf2pz8=" + }, + "play-services-stats-17.0.2.aar": { + "sha1": "43b1c2623e3915d91c06ed1f95b042b390ecd498", + "sha256": "sha256-3UMUpT9Jo3jsFGED02IyuWx1RU0pUmM2zL3xMpQXZNM=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-tasks/16.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-tasks-16.0.1.pom": { + "sha1": "9d2270f8d3cc0cca2c73f9b0a0ce7bc43c88fce4", + "sha256": "sha256-oO34IFOPDRyuwxT7Talr634gvJuqbYopZrji5DpjHko=" + }, + "play-services-tasks-16.0.1.aar": { + "sha1": "565e6572641c926a370f1eaa1e0b963172ef8516", + "sha256": "sha256-sxwY2NHMjZgU8pXudDVHEzPzcLpb2QTKFPjyvsTzXDU=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-tasks/17.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-tasks-17.0.0.pom": { + "sha1": "ab81ab94f07c15697696e3da7fb026046403e0b9", + "sha256": "sha256-/dkzAsLrlLvmAHyqKNByJG0odc2ly3dLtimUFrlNdKY=" + }, + "play-services-tasks-17.0.0.aar": { + "sha1": "18299a334d48bc5abfe3c9446c2a9e1a3b3f38e1", + "sha256": "sha256-Lm0XOLc2R/P+egOLl4C5dxezdG6uJYAJGX42578xEqU=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-tasks/18.0.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-tasks-18.0.1.pom": { + "sha1": "bdca6948d70ec65d184b9de3df90217b85b4fff6", + "sha256": "sha256-8eDKIj7Obk9TM0p3cUkT5TZmeA9obo/wot6t7qtm6mQ=" + }, + "play-services-tasks-18.0.1.aar": { + "sha1": "319f361d7486eadccf0c3a3a28c27c50d8047655", + "sha256": "sha256-8QbbSMbM+o4TFaetxErs0C/3NV6z+n3Ny6jCg6jrFoE=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-tasks/18.0.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-tasks-18.0.2.pom": { + "sha1": "ba9c33d90c90f37503783cbcfc7dcc87bd7795f0", + "sha256": "sha256-4Hzbjp5j8+lFlrzYmExzecR9nyiey5peNfy58vVzup8=" + }, + "play-services-tasks-18.0.2.aar": { + "sha1": "07742ff703c42a8e78e4b1b548c8abf0f2020170", + "sha256": "sha256-A0Zc9TaA2iPT1KOZXAMpt/c7IR04eR7LNuWR/ruJZmQ=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-tasks/18.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-tasks-18.1.0.pom": { + "sha1": "21ae318bba62176cc0e1a86dcf2aeabf4e862b09", + "sha256": "sha256-zynthGEI16jywX2O9bYzmXNXV8Wgm7Zj1+eE4CrYS80=" + }, + "play-services-tasks-18.1.0.aar": { + "sha1": "09841a7984efd142b88e8563a5f99131f24e22e1", + "sha256": "sha256-1gV16uOTUOYjSFi8nX13U3VweugqaE5sr38+QaEuJaI=" + } + } + }, + + { + "path": "com/google/android/gms/play-services-tasks/18.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "play-services-tasks-18.2.0.pom": { + "sha1": "3e21a0ece933e1b3415a7a9cd90397852bed2735", + "sha256": "sha256-a5nEioldFV5Yq87mbMIhRtuDq9XYTK9sj3oq6psbzSE=" + }, + "play-services-tasks-18.2.0.aar": { + "sha1": "75cd01884bac2768b8ab1b530c6510334bb2330c", + "sha256": "sha256-fyqqj1AgaOr1Q1bKkq7AQnHW58QWxSxFwNI0QPy9FlQ=" + } + } + }, + + { + "path": "com/google/android/gms/strict-version-matcher-plugin/1.2.4", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "strict-version-matcher-plugin-1.2.4.pom": { + "sha1": "64382e5666d083a8f09f353a5d3b385474223112", + "sha256": "sha256-Ihy7TNpqqRBzw7xi31oNpgXA/nF5qLdPCVWC0cKclMw=" + }, + "strict-version-matcher-plugin-1.2.4.jar": { + "sha1": "37f8b2ab56ecab699697602acdd598cb6d809b38", + "sha256": "sha256-3xtItno8X+pyZ+nlHSDeR1eBi26O1XZITHTJbqKVEgA=" + }, + "strict-version-matcher-plugin-1.2.4.module": { + "sha1": "72de58fcbf744d0b071aec262ba21b6e000845f0", + "sha256": "sha256-W/ThPqqW+o8eWxxG6epdvdOORWUyR7RJ1jvkn1Z3kqw=" + } + } + }, + + { + "path": "com/google/android/material/material/1.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "material-1.0.0.pom": { + "sha1": "2bcf783c3bc66bafc0e1c9ca721d7544659bb6c3", + "sha256": "sha256-yf/1QdUcERld4wXFC7/k0d/JeEGYPZX21xAREmA1Fas=" + }, + "material-1.0.0.aar": { + "sha1": "f83e012c22d2fac8fcf23880d7167832b099fa94", + "sha256": "sha256-doDjgaPAN5jZmbLkQcqt2KVqCoCOEIAkpnr5/ibBGtw=" + } + } + }, + + { + "path": "com/google/android/material/material/1.2.0-alpha03", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "material-1.2.0-alpha03.pom": { + "sha1": "b19834fc7dba44949d5f906136911bb271a708ae", + "sha256": "sha256-NMYdzjlxcSwpWuxT+mUb5jaVDE9zD7MSYTKQvV6inr0=" + }, + "material-1.2.0-alpha03.aar": { + "sha1": "05bb1d391bbed501d9ec0de96dbdb3fa1464f255", + "sha256": "sha256-/THQQCGTX1gQASNBd1y7D/DYew5RIXr1zGnkplkFawA=" + } + } + }, + + { + "path": "com/google/firebase/firebase-annotations/16.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-annotations-16.0.0.pom": { + "sha1": "e233c7dae7102b50652662ed4e88ec6ee55c7383", + "sha256": "sha256-gsbQQO3LPBWLW5hlYB0r10Kry9l8vZiQO94Fnajt0Ic=" + }, + "firebase-annotations-16.0.0.jar": { + "sha1": "dbeae20d6c97b747b59ef47b6dcf770ba1a60fa6", + "sha256": "sha256-iDDy6KXkNMvF5L0YEzBztuhRiozo7Czm87JzyR2WZMI=" + } + } + }, + + { + "path": "com/google/firebase/firebase-annotations/16.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-annotations-16.1.0.pom": { + "sha1": "3eda81647455bbab57dab71898743f04783a3b8e", + "sha256": "sha256-au7RUn/7i6EzGm5GSq2KEyclAhC42dRLw0qp6dteMrA=" + }, + "firebase-annotations-16.1.0.jar": { + "sha1": "64bf465fd5aa693067bfd6bc28efb260e1ad29ea", + "sha256": "sha256-A5NHjPEkchx1Aup2ooEppCDEYvxNXApCdNueMJeFvX4=" + } + } + }, + + { + "path": "com/google/firebase/firebase-annotations/16.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-annotations-16.2.0.pom": { + "sha1": "4ca34ea7d81a600b23af972d0597f17075950b9d", + "sha256": "sha256-CFsRHeSXHLO5OP+QAMyAeQEE/WmlE0doTftybmnoUko=" + }, + "firebase-annotations-16.2.0.jar": { + "sha1": "ba0806703ca285d03fa9c888b5868f101134a501", + "sha256": "sha256-RvbV390szzxA3ol6FL2XeTFMMxn0S/0x5+CiDZNaXj4=" + } + } + }, + + { + "path": "com/google/firebase/firebase-bom/33.8.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-bom-33.8.0.pom": { + "sha1": "9acc6ead36b7d03d5a2fe0ae53092558a496e644", + "sha256": "sha256-5rdkiyH4WXSbvvlBzppktI1rjAvsLkJwJwmaOpLWqSc=" + } + } + }, + + { + "path": "com/google/firebase/firebase-common-ktx/20.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-common-ktx-20.4.2.pom": { + "sha1": "9ab1c46c75e7f07a264153bcfd5943d4780e07a8", + "sha256": "sha256-dJEB/nA3Wu/U2H6gElXcmQ1kd3Rbp1LxjTG5YKKjB0s=" + }, + "firebase-common-ktx-20.4.2.aar": { + "sha1": "e68c639c6166dfc8b7a3c125e7f55960443c1bcf", + "sha256": "sha256-FwtEaQ5IDR3eeajRY1hqECWrVULuCqw4dLdufRwncY8=" + } + } + }, + + { + "path": "com/google/firebase/firebase-common-ktx/21.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-common-ktx-21.0.0.pom": { + "sha1": "44cd788c09008d164d78039a22bbfff26d2a3139", + "sha256": "sha256-wn7MtIuViBFtb9MvRle8Wd+FUAJDIpNVjbuX6YeK3rg=" + }, + "firebase-common-ktx-21.0.0.aar": { + "sha1": "332a03e56b0c03ac7ed3590fbd256bb307784288", + "sha256": "sha256-JfyAybueyxZykIcYwpSvzErB5HRzZ3vgYMeV4E8SAGY=" + } + } + }, + + { + "path": "com/google/firebase/firebase-common/20.3.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-common-20.3.3.pom": { + "sha1": "cb6a0248752d54e4c67b455f80649d70c5f39a56", + "sha256": "sha256-dRMuY+h4r+LUnoD1inpL798yswmSSIKioWb2wym6fyc=" + }, + "firebase-common-20.3.3.aar": { + "sha1": "4614a17fd1b31c36815a00f46015ef02e1ea1674", + "sha256": "sha256-KR6GfJcpkbdBrBANOfQkijdyeAWmw3zlk3YzT7FO/pM=" + } + } + }, + + { + "path": "com/google/firebase/firebase-common/20.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-common-20.4.2.pom": { + "sha1": "4c7967d37a82375e261927a2eb86129f20495d31", + "sha256": "sha256-2OIiMifyZ9lfVYD3RsoAk0LkZpOJZMUVuNZvXaDkk7Y=" + }, + "firebase-common-20.4.2.aar": { + "sha1": "f3814a9749517424cdd1c648f29f1d475e9c46a3", + "sha256": "sha256-N3guPzYbbEJLVDmj8z1WlDPX4mYFWAH1H/ygut2omNo=" + } + } + }, + + { + "path": "com/google/firebase/firebase-common/21.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-common-21.0.0.pom": { + "sha1": "0cd82433652faf17b9d038a61ea3bd906138d4b2", + "sha256": "sha256-EcJkYba/8lSadVT9qqWMBwVQq/RyVem9oy3SODJMxz8=" + }, + "firebase-common-21.0.0.aar": { + "sha1": "0e18024afd3371be7db91e00a28e36d3d2d0f144", + "sha256": "sha256-N5KH1xcTcVEkk2gbOY54w0GzM8+XTugaGLHVbnwuOF0=" + } + } + }, + + { + "path": "com/google/firebase/firebase-components/17.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-components-17.1.0.pom": { + "sha1": "1763b199bf6f4b9e8feb9251b9ae40b213ab356e", + "sha256": "sha256-gJq38W2OchDzDTiNVYURBMUZvgSIscVQRiZgvnTLOXk=" + }, + "firebase-components-17.1.0.aar": { + "sha1": "ea168c6188a104fa22dffe599c7a350500c92580", + "sha256": "sha256-NSxx+JmUy0SB2utF7TYwu/F4m98b5FY5iOLBhhivT2A=" + } + } + }, + + { + "path": "com/google/firebase/firebase-components/17.1.3", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-components-17.1.3.pom": { + "sha1": "9afce27f9312fffa4b915f56473dd2125a5b468a", + "sha256": "sha256-ad6QRJn/ZH3AP9BelhieSwSDPvNO72z8/K2N1jaA3t0=" + }, + "firebase-components-17.1.3.aar": { + "sha1": "d4d9e1668f6957e60e0e5591f58a3f5ff5b9f6bf", + "sha256": "sha256-QiUbluMeHidmMr9LgnS8Ig7ei6+/dukssVpDi43qukg=" + } + } + }, + + { + "path": "com/google/firebase/firebase-components/17.1.5", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-components-17.1.5.pom": { + "sha1": "281f172f098849b90a0c29e3100d7e0f48a8d928", + "sha256": "sha256-54FoS9HSEyqzK6KcQZuiF0/4mD7etNk3gheOi23G+AQ=" + }, + "firebase-components-17.1.5.aar": { + "sha1": "564a24061437251d8754791a281944e817ef93e3", + "sha256": "sha256-5MEz+AVbGJSBStFpAo4X9rIUg40LZ4cmjc79wJ1Onb8=" + } + } + }, + + { + "path": "com/google/firebase/firebase-components/18.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-components-18.0.0.pom": { + "sha1": "02a6b008c6ffefccbb31b51ec7cd49a0826c7b08", + "sha256": "sha256-bm3TTUUSxzk+iezdi4KevN0x5r4skx59EQCofVaIAEk=" + }, + "firebase-components-18.0.0.aar": { + "sha1": "bb3038eb5d7807378715a14e92745c387bdad239", + "sha256": "sha256-x8SKOoD0SkmevWRzvPx91aRe9SP6viQUpiUV/zxkCXI=" + } + } + }, + + { + "path": "com/google/firebase/firebase-datatransport/18.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-datatransport-18.2.0.pom": { + "sha1": "50fbbda5ec5911b1c44203b2c61717c0757a2580", + "sha256": "sha256-uL4RglulbATeg3QJSEC1Y4PfJGNJMYAmaNcCaNDHu+g=" + }, + "firebase-datatransport-18.2.0.aar": { + "sha1": "e1300ce0aa8bfca59bd1ebf007167a279ec3e239", + "sha256": "sha256-srKPa6Fz9eDE/j02otfuUjnHrL9BQ7VJ7PAxo0hbNbs=" + } + } + }, + + { + "path": "com/google/firebase/firebase-encoders-json/18.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-encoders-json-18.0.0.pom": { + "sha1": "b2d4da0cbd5f4b53da51d5aaa25011da98e4e198", + "sha256": "sha256-On1ZeVp5loOvpkNZMQZsW7Y0rf69KIOgi0dl84zdPqE=" + }, + "firebase-encoders-json-18.0.0.aar": { + "sha1": "96bb325983a231881f1e195ed9d713189f421dea", + "sha256": "sha256-gK7Ofh71iVfKL8GVe8kgjskqOpUoIBMx08Y+MYJXD5c=" + } + } + }, + + { + "path": "com/google/firebase/firebase-encoders-proto/16.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-encoders-proto-16.0.0.pom": { + "sha1": "3b39278f0d341e0045dc42c86133175d5b6c8c3b", + "sha256": "sha256-X1+3SvP/8OH5Yfy18/mSZ8EDsWojyJtVbIy0pxGo3LI=" + }, + "firebase-encoders-proto-16.0.0.jar": { + "sha1": "a42d5fd83b96ae7b73a8617d29c94703e18c9992", + "sha256": "sha256-KT25ag0dQ/AzFniBtjjY/ehE5OVJX1EBz1IpV2UpXg4=" + } + } + }, + + { + "path": "com/google/firebase/firebase-encoders/17.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-encoders-17.0.0.pom": { + "sha1": "860a97137bed444e73ab31478797fa5415eaf564", + "sha256": "sha256-QjV141AOmRDjqoP516bXVbX3asWRgjuvZ1cPts5+qBY=" + }, + "firebase-encoders-17.0.0.jar": { + "sha1": "26f52dc549c42575b155f8c720e84059ee600a85", + "sha256": "sha256-KCpacD+bfrVlCN3pfqkY6V1zMYsVcFD0V/eobcp1AVA=" + } + } + }, + + { + "path": "com/google/firebase/firebase-iid-interop/17.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-iid-interop-17.1.0.pom": { + "sha1": "e5dc43e465b8703fe65dba14037eea4575044337", + "sha256": "sha256-U/Jp9eEnrCHrotgKmFq8jSyPJM3Km+AoREjunqND40s=" + }, + "firebase-iid-interop-17.1.0.aar": { + "sha1": "a15abe6089504f291984e999e84745a4f3ee6440", + "sha256": "sha256-C3w3IchLYucEFTByOe1Kf5mJiQhL8oM/kLn1vqMJWgU=" + } + } + }, + + { + "path": "com/google/firebase/firebase-installations-interop/17.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-installations-interop-17.1.0.pom": { + "sha1": "bbfe56b9041afff1278c2e071bdc5968dee83446", + "sha256": "sha256-0Tb8CCaVluUIWK2nPd1qSAlbvorWmncY1i3XOJn7RMo=" + }, + "firebase-installations-interop-17.1.0.aar": { + "sha1": "da970092a7a27ea95609d94e871c679c2a5d71d3", + "sha256": "sha256-iR8+p5m7PzmyS/lLttlRQk3eeEtcm1rx3vS6t5hwXY0=" + } + } + }, + + { + "path": "com/google/firebase/firebase-installations-interop/17.1.1", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-installations-interop-17.1.1.pom": { + "sha1": "c6d6456109b27284f4fdcf3c4c56296cc09b3ab7", + "sha256": "sha256-nhH6EtHjY1H6sYUn0Y+Kdxyztebnk/IMz6yhVyYOyqg=" + }, + "firebase-installations-interop-17.1.1.aar": { + "sha1": "0c7b5bf3ade22e81023cb90de2fa857016dfa441", + "sha256": "sha256-+sZQaA95IfSrkvC7IaCSNLEs2u+rLPcBiCgdfqUrja0=" + } + } + }, + + { + "path": "com/google/firebase/firebase-installations/17.2.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-installations-17.2.0.pom": { + "sha1": "ad80e07953c5d108dc84e883b8d8aa6869ba8fa0", + "sha256": "sha256-phcK45N9JwQBSZRO7LJB8E2v3QF3ye/jwCOvmhwUOgM=" + }, + "firebase-installations-17.2.0.aar": { + "sha1": "35dcca50378836565ed4967bb682bcad9758af13", + "sha256": "sha256-3gEeajt5Yd5jjxcvLLZrowTqqYlmKp4rD0jf93TMwWY=" + } + } + }, + + { + "path": "com/google/firebase/firebase-installations/18.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-installations-18.0.0.pom": { + "sha1": "326a7bb8f56fdcc1eabb2a726be5b26c6c47a8eb", + "sha256": "sha256-E8ZMA8w8bENbPYUh48hfy8/v8i87fwF4qcpiaYHs2mY=" + }, + "firebase-installations-18.0.0.aar": { + "sha1": "02e187f194e9650bc6a2f50e960685ab483b44e5", + "sha256": "sha256-lQY637F//kkrEvaOcwIIc4FNgXeqVTBpytZOEdjHkpI=" + } + } + }, + + { + "path": "com/google/firebase/firebase-measurement-connector/19.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-measurement-connector-19.0.0.pom": { + "sha1": "ba468776dbaf3aa578fafcc041a54ed765373e62", + "sha256": "sha256-lUQjVEKpW1Qu7V9j49/nhuPtC/JqH2Ax2rTfqWWkzBo=" + }, + "firebase-measurement-connector-19.0.0.aar": { + "sha1": "3f8a798ae1e716d98f5412e049476ce1df435d57", + "sha256": "sha256-26dNa/lGR+45e/evsqsH9v6NExV+VnhfpUCioT7YLJk=" + } + } + }, + + { + "path": "com/google/firebase/firebase-messaging/24.1.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "firebase-messaging-24.1.0.pom": { + "sha1": "e7707bb6b8413da7923f167f63dc152faac04533", + "sha256": "sha256-wtVzmqwneVDccbHdfvFBNxCMVqg8MawjhV6r3SJLCYg=" + }, + "firebase-messaging-24.1.0.aar": { + "sha1": "17f55f61bc79d5b7e9b793c458f85b76962e462c", + "sha256": "sha256-1e8+6hcgjPIU/ewxH30cR47RwZw5o7BjE/ESiMcjBNk=" + } + } + }, + + { + "path": "com/google/gms/google-services/4.4.2", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "google-services-4.4.2.pom": { + "sha1": "41d48f536514092907df10c78f8e6482da2a6cdb", + "sha256": "sha256-a76JZLzWRJhZBaoRsvY7iHgQ7gFK13cSY9KvFxTQLrk=" + }, + "google-services-4.4.2.jar": { + "sha1": "4d61599d56846c0cf3c6b466260a3d08b9032d55", + "sha256": "sha256-M6xbjCDHycyB6JjZ9Ncvjl8Xo1UFGCaDzYFexpOS7I0=" + }, + "google-services-4.4.2.module": { + "sha1": "72a56653f39c4195a8d49a5f81444ed3ad159219", + "sha256": "sha256-tom98xESY+QuQd13QS5i2ekNttWrm+sYeA5jNF5G6Sc=" + } + } + }, + + { + "path": "com/google/prefab/cli/2.0.0", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "cli-2.0.0.pom": { + "sha1": "c551056d34273f78ce483e40a36516c46bf2de35", + "sha256": "sha256-SFZAGiY7OcU5Szahbg2ZYozwXGgAigzalpHHK7EB4d8=" + }, + "cli-2.0.0-all.jar": { + "sha1": "f2702b5ca13df54e3ca92f29d6b403fb6285d8df", + "sha256": "sha256-2b2J9oRGuCvgOKrndHca2Fki0LN1IJsXYlonNLUxfik=" + } + } + }, + + { + "path": "com/google/testing/platform/android-device-provider-local/0.0.8-alpha08", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-device-provider-local-0.0.8-alpha08.pom": { + "sha1": "7ad0da63dcd2c26aea263586da23c0460c65faba", + "sha256": "sha256-0VXgZ0WJLz9awljHxROrInshyxPlH1tGw8mE1sNCWuA=" + }, + "android-device-provider-local-0.0.8-alpha08.jar": { + "sha1": "e86d506e61ebf46364cc49b8f2c9954cd2def0c7", + "sha256": "sha256-qUXH0ET23W+ufjFbAtxc+nVDLfQcYkw/sLruy4Qale8=" + } + } + }, + + { + "path": "com/google/testing/platform/android-driver-instrumentation/0.0.8-alpha08", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-driver-instrumentation-0.0.8-alpha08.pom": { + "sha1": "5bc328cf4879e406876a24782dcf7273c528b0e1", + "sha256": "sha256-6ldfu72BTDyChdjs5D71A/az7qP7bW/dBTe3uNYl8Yo=" + }, + "android-driver-instrumentation-0.0.8-alpha08.jar": { + "sha1": "282ecf24946b51fa3810c0d4f3efac79f0008be2", + "sha256": "sha256-xENqvouD5fxzQbIxT9azJ/64p7ZMKU+LnQi4s74hSvw=" + } + } + }, + + { + "path": "com/google/testing/platform/android-test-plugin/0.0.8-alpha08", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "android-test-plugin-0.0.8-alpha08.pom": { + "sha1": "2c42948c5384d62dc30d844b1a57df0a70ccd325", + "sha256": "sha256-hwPkWxXkevzfID7eXwogC+uITnPmLLK2GW9uU3nISrw=" + }, + "android-test-plugin-0.0.8-alpha08.jar": { + "sha1": "c9485307a108389faa4f9aeb5738c0b0625aa3dc", + "sha256": "sha256-J6fKgNeOVo8p4uWUhyAtGRjnuib/URx1bjXodGuqbpM=" + } + } + }, + + { + "path": "com/google/testing/platform/core-proto/0.0.8-alpha04", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-proto-0.0.8-alpha04.pom": { + "sha1": "ed0058e91fb2abe566f4e0453b545ef770f03195", + "sha256": "sha256-7awJ/HBAaH60VcU218hc8KGNwcfVZx20m7YOqgZYgQ8=" + }, + "core-proto-0.0.8-alpha04.jar": { + "sha1": "655f18fa3952ed1441e8d032006bee81b85ba4db", + "sha256": "sha256-0rx6RhNR50hETsgWyDUN2QgEb6NGr2A5lpSpgyfWAkU=" + } + } + }, + + { + "path": "com/google/testing/platform/core-proto/0.0.8-alpha07", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-proto-0.0.8-alpha07.pom": { + "sha1": "dc9139e0be79ce2147e79e4282b6e76e5b687477", + "sha256": "sha256-kIdIJb5QR47VdyHt0KxNSWWT3zzuC4gg+QxkkFPlKQo=" + }, + "core-proto-0.0.8-alpha07.jar": { + "sha1": "901b629a7684aa0cae060a7f23f385297add1e26", + "sha256": "sha256-RkWDPK9c9hXV3bAaSPIYjsmINw8QOWni+5Mfgxm/PAw=" + } + } + }, + + { + "path": "com/google/testing/platform/core-proto/0.0.8-alpha08", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-proto-0.0.8-alpha08.pom": { + "sha1": "fec8c436cb221cf938294b043444f3fc4b2514c0", + "sha256": "sha256-ku8nGFWHHB1GMpE4yl4rxpJvHZvgYH0v/Ayl/vJcegs=" + }, + "core-proto-0.0.8-alpha08.jar": { + "sha1": "72ca52065789993f2f029b34d797813c467d5d74", + "sha256": "sha256-OSBXWnTxQ3l1zfRSdvm3XnrkG6+43rHWDn1jnyHRsGw=" + } + } + }, + + { + "path": "com/google/testing/platform/core/0.0.8-alpha08", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-0.0.8-alpha08.pom": { + "sha1": "ef6d7246aa48c378f53e829554e875ab78f0adcb", + "sha256": "sha256-5aLSW7MtuWP9xGLsiSEa37VjO1Q+nuWk94euzUPmSqA=" + }, + "core-0.0.8-alpha08.jar": { + "sha1": "762b033d6dcf24e44af8ccc5428ceb3eef540e37", + "sha256": "sha256-iPJ4Ao0ksVRDXdhWPf9MYYu3N7ZTnSd+RjJ9iYJD5yI=" + } + } + }, + + { + "path": "com/google/testing/platform/launcher/0.0.8-alpha08", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "launcher-0.0.8-alpha08.pom": { + "sha1": "2834047ab9cd43599311df5f6e7e3add0a7f7d37", + "sha256": "sha256-Mh+uPcuTDaE5NSuQgM2hY3sGml4G8V5SzibF0aWQRRE=" + }, + "launcher-0.0.8-alpha08.jar": { + "sha1": "de8664b74f9b70fea224c610fd41b802805c72ab", + "sha256": "sha256-sSmC58NMVuSAsY1J8UBaiWWpt5gjw4BaSIxbxkLledk=" + } + } + }, + + { + "path": "com/google/test/platform/core-proto/0.0.2-dev", + "repo": "https://dl.google.com/dl/android/maven2", + "files": { + "core-proto-0.0.2-dev.pom": { + "sha1": "c29cc8ce0adb73f7d3fd06436520f188884bc810", + "sha256": "sha256-6SZGLDuVO9+ntYDiqZWbhzJryjQa+6GO0A5vYrgvC4g=" + }, + "core-proto-0.0.2-dev.jar": { + "sha1": "56452fc5ddd0dc8e0e2f56b86910d97373a4b75c", + "sha256": "sha256-nIleorp/KrSL0V/4RezdfR0Wg2s8f/grih79G60LYpA=" + } + } + }, + + { + "path": "com/github/Dimezis/BlurView/version-2.0.4", + "repo": "https://jitpack.io", + "files": { + "BlurView-version-2.0.4.pom": { + "sha1": "48eab3169b9a4ee3167ed1ed5f9c35135e9e4dad", + "sha256": "sha256-DjaiZmuM2CoTmZ4C+6HnNcsjc31ZYaWZSv6A0SCmmjk=" + }, + "BlurView-version-2.0.4.aar": { + "sha1": "c7d8a6464a50b430b01ce1a25f7949b75ba359b8", + "sha256": "sha256-w+KXiNSn0WoszuUpcxIp/M162Z+4vB30FeFvWKd1mcg=" + } + } + }, + + { + "path": "com/github/status-im/function/0.0.1", + "repo": "https://jitpack.io", + "files": { + "function-0.0.1.pom": { + "sha1": "a8732bd3d863516447a2fa335ade1ae05c01fd4d", + "sha256": "sha256-Xnjkm6xzSZOQBYeI4jVGL8/tuA+9ix22se40jzp4Nu0=" + }, + "function-0.0.1.jar": { + "sha1": "b68f81d53884a85fe2c0a8386e53cc0d6c980d1b", + "sha256": "sha256-uPHqxp01O6iyo+7iVAsWZforakqTYw8E6fwOGxcHhx8=" + } + } + }, + + { + "path": "com/github/status-im/status-keycard-java/android/3.1.2", + "repo": "https://jitpack.io", + "files": { + "android-3.1.2.pom": { + "sha1": "9afce579939bad3c2b6d15787a0c861f6dbd9afa", + "sha256": "sha256-hNYvQQKfHNmbGQw5P0sssZAI11wfRhLir46QdukK0bs=" + }, + "android-3.1.2.jar": { + "sha1": "46fe72fb8fca5935c73e0df5c10ccdd5c64fca8e", + "sha256": "sha256-QNpDWQ1TWvCWg3YdL4UWSEiafpoHavXSY3VWCN45rtE=" + } + } + }, + + { + "path": "com/github/status-im/status-keycard-java/lib/3.1.2", + "repo": "https://jitpack.io", + "files": { + "lib-3.1.2.pom": { + "sha1": "0c0d8ef7318ba054457dea1e6624e0284aec91ca", + "sha256": "sha256-Vs9p287YNgMD76/egyeHTqlIsENjwbZsspQbzs8QkeY=" + }, + "lib-3.1.2.jar": { + "sha1": "5df9eef589a72c5a3143f59b0f547092aeb0c309", + "sha256": "sha256-M0P6r96RkgaIjC912Z/VxJgHmfm/ftl7rqYOto8A0Ic=" + } + } + }, + + { + "path": "com/github/wix-playground/ahbottomnavigation/3.3.0", + "repo": "https://jitpack.io", + "files": { + "ahbottomnavigation-3.3.0.pom": { + "sha1": "3bd510fdc1a32189fdd7dd78f023abbef70d9653", + "sha256": "sha256-yGMc30nTrlxDFB85iNq65sw/QIGq1RkhKNDBkf0UuXs=" + }, + "ahbottomnavigation-3.3.0.aar": { + "sha1": "d9a85263fb7033898294b64bdd936b2d95b2c771", + "sha256": "sha256-VQ9z6iJ97CE8cMDxDP6LtlUC++Spqdu93xvS9TZ5Xu4=" + } + } + }, + + { + "path": "com/github/wix-playground/reflow-animator/1.0.6", + "repo": "https://jitpack.io", + "files": { + "reflow-animator-1.0.6.pom": { + "sha1": "d2c5089ae74a366676d9432c401e373b4895858e", + "sha256": "sha256-uanUNC0CwC2YnlQ4rpZBMdp+Is6UF36Y/Iwn6YLLHno=" + }, + "reflow-animator-1.0.6.aar": { + "sha1": "36074a3fcb77c2ba12bd2de7dde81441e0c7504f", + "sha256": "sha256-RriEmXGOnCDY2257VY6Dl91ZOYr6fnXsasDHxgj0Y6I=" + } + } + }, + + { + "path": "com/github/yalantis/ucrop/2.2.6-native", + "repo": "https://jitpack.io", + "files": { + "ucrop-2.2.6-native.pom": { + "sha1": "46554329fff3cae14a7fcafb2c8388cb7fe216e7", + "sha256": "sha256-WApaf2JoUtRwyEGBy0q1f8UkNgsJy7I11unHTLRq80s=" + }, + "ucrop-2.2.6-native.aar": { + "sha1": "a248e1b729dbb512215c845ec77e890a7b63d501", + "sha256": "sha256-wSlQchfOmNHW/2NViapdQnF+abq0xbtQb7HBC7OZO0o=" + } + } + }, + + { + "path": "com/github/zacharee/AndroidPdfViewer/4.0.1", + "repo": "https://jitpack.io", + "files": { + "AndroidPdfViewer-4.0.1.pom": { + "sha1": "e4e91cd5feaef974c568cc6bb8a73fba9eec4850", + "sha256": "sha256-Bb6gyzftM1QgZ1Zk9us4/aVMor1ItQbd5lWriMpFBkA=" + }, + "AndroidPdfViewer-4.0.1.aar": { + "sha1": "6ba402769abaa10061af8991ad6a20160191509f", + "sha256": "sha256-ZGW4/r2/dRcokKzNLdGeRa6rnC/zKLwRvY2ykqdEVNw=" + } + } + }, + + { + "path": "com/adarshr/gradle-test-logger-plugin/2.0.0", + "repo": "https://plugins.gradle.org/m2", + "files": { + "gradle-test-logger-plugin-2.0.0.pom": { + "sha1": "fe5c29ddcae80215fd7ec74476880de67d66c894", + "sha256": "sha256-kVCmaVqB+SN+4958dClo5XcMHyWCGsS3c5kCFh9lUu4=" + }, + "gradle-test-logger-plugin-2.0.0-groovydoc.jar": { + "sha1": "6d8bed5c5b1e6daef6ac6cc7fa1ee3ba514ad96b", + "sha256": "sha256-jtiWgKriVFvUvRKe9AGdEc+NGIf6xdECTr1uVOD3Vys=" + }, + "gradle-test-logger-plugin-2.0.0.jar": { + "sha1": "957ca7f6f4390ea8cf4551fff43a96da50844b98", + "sha256": "sha256-lZFMdk+bW0P59xiCWMxr9BsLr2dnlhqjQIJtgHyAjp0=" + } + } + }, + + { + "path": "gradle/plugin/io/invertase/gradle/react-native/1.5", + "repo": "https://plugins.gradle.org/m2", + "files": { + "react-native-1.5.pom": { + "sha1": "44ff05f78af2deeb6b43e8113ba28b35345d1789", + "sha256": "sha256-o23sP5/WANcHoOjURP5m02F+B3XzWJ/GYH+67qFPnIw=" + }, + "react-native-1.5-groovydoc.jar": { + "sha1": "69acfcf9335199888b5bd2360ff397b5392b5f62", + "sha256": "sha256-4k5u10TsvAtHU8YikPc6WiMAGzhnLPIA6O9ARdfKtk0=" + }, + "react-native-1.5.jar": { + "sha1": "edf0eab0867863c35b058d36c9fdf389d2033aaa", + "sha256": "sha256-A82HSyPJYyaoeGQTpZAi1grdwUdR2pVlcTRXKw1Pw5U=" + } + } + }, + + { + "path": "io/invertase/gradle/build/io.invertase.gradle.build.gradle.plugin/1.5", + "repo": "https://plugins.gradle.org/m2", + "files": { + "io.invertase.gradle.build.gradle.plugin-1.5.pom": { + "sha1": "63467114309109a4eeb1cecaf5407b1549280837", + "sha256": "sha256-w6K0/v4mAD087tgbifsU1QCwSkHsO9lx1wRgARXdOPU=" + } + } + }, + + { + "path": "org/gradle/github-dependency-graph-gradle-plugin/1.3.2", + "repo": "https://plugins.gradle.org/m2", + "files": { + "github-dependency-graph-gradle-plugin-1.3.2.pom": { + "sha1": "0740a44f4c99d3e2d9fbc2c89afcb195ec66dbdf", + "sha256": "sha256-xaVDODswVgtoaoIAtgCELvnd8IAx8AYdJA+FE3a0Jxs=" + }, + "github-dependency-graph-gradle-plugin-1.3.2.jar": { + "sha1": "ba3d0995475c6aa79048e865d7923bbed20f36a0", + "sha256": "sha256-VdNKG2xBu75P6hSlZaERE3ZxOSSwAaPMjD2EnGSejQA=" + }, + "github-dependency-graph-gradle-plugin-1.3.2.module": { + "sha1": "879866cccedf304d8c484d3bb70eb6d699e9e118", + "sha256": "sha256-nBqz4Xbqelt3YhEAEG9aPzYC0+2MBcM1M+1Ys5PumhQ=" + } + } + }, + + { + "path": "org/gradle/toolchains/foojay-resolver-convention/org.gradle.toolchains.foojay-resolver-convention.gradle.plugin/0.5.0", + "repo": "https://plugins.gradle.org/m2", + "files": { + "org.gradle.toolchains.foojay-resolver-convention.gradle.plugin-0.5.0.pom": { + "sha1": "eee0c2b09319bd0288afcb9238e7b129ed80966b", + "sha256": "sha256-jLnbP0b+vVsPmB9eHODk+L/+dMHjdQTBuiLWs1gj3Tc=" + } + } + }, + + { + "path": "org/gradle/toolchains/foojay-resolver/0.5.0", + "repo": "https://plugins.gradle.org/m2", + "files": { + "foojay-resolver-0.5.0.pom": { + "sha1": "5ca1aef0e385ac06911462ec88cac36d7acdc831", + "sha256": "sha256-gk6IGdaFWBkX62IRkEsD8YFSJHM4vF3yMaPCUUOCErA=" + }, + "foojay-resolver-0.5.0.jar": { + "sha1": "ea9de2dd6593295f79b4eb723dc8b1a68d04c627", + "sha256": "sha256-UxYINJpuXWsFOHaxHcHBmH0LAS1kLP/bKOOAMuXn+Bs=" + }, + "foojay-resolver-0.5.0.module": { + "sha1": "2820ee176a7abf60e999c749079ed35605f7102e", + "sha256": "sha256-JLVV+EVrjBDljbM8qLvJmOZnk/KY6SjgAzDPD74ySZ4=" + } + } + }, + + { + "path": "antlr/antlr/2.7.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-2.7.1.pom": { + "sha1": "22aff158409d056a24ee49717ecb32fc9142edbe", + "sha256": "sha256-CcuO2cMiWqsgXWVnsvVoV5K6gjCLRCeQcKPXJ1WRp8E=" + }, + "antlr-2.7.1.jar": { + "sha1": "9f391a14266a640b029a990aa5da4a26830cae98", + "sha256": "sha256-JrKmLkaGYezB1F5GBryQcafKdXtgkQOnCGWOESaXAdY=" + } + } + }, + + { + "path": "antlr/antlr/2.7.7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-2.7.7.pom": { + "sha1": "52f15b99911ab8b8bc8744675f5cf1994a626fb8", + "sha256": "sha256-EA95O6J/i05CBO20YXHr825U4PlM/AJSf+oHoLsfzrc=" + }, + "antlr-2.7.7.jar": { + "sha1": "83cd2cd674a217ade95a4bb83a8a14f351f48bd0", + "sha256": "sha256-iPvaS5Ellrn1bo4S5YDMlUus+1F3bs/d0+GPwc9W3Ew=" + } + } + }, + + { + "path": "commons-codec/commons-codec/1.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-codec-1.4.pom": { + "sha1": "393db4ae967c6e831025d432632d1f72f7108b01", + "sha256": "sha256-9fMAaUIboBPna+znYEB9zIACJGHclVXcvXX9sG2aQI8=" + }, + "commons-codec-1.4.jar": { + "sha1": "4216af16d38465bbab0f3dff8efa14204f7a399a", + "sha256": "sha256-aqQjTHTzoQNXUaJYIlRYZ8jDcnElpkK24ElmXRhjYxs=" + } + } + }, + + { + "path": "commons-codec/commons-codec/1.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-codec-1.6.pom": { + "sha1": "9499f0c87ab43a74c456b9847acbcb5e67fe9f32", + "sha256": "sha256-oG410//zprgT2UiU6/PkmPlUDIZMWzmueDkH46bHKIk=" + }, + "commons-codec-1.6.jar": { + "sha1": "b7f0fc8f61ecadeb3695f0b9464755eee44374d4", + "sha256": "sha256-VLNOlBuOFBS9PkDXNu/TSBdy3CbbMpb2qkXOyfYgPYY=" + } + } + }, + + { + "path": "commons-codec/commons-codec/1.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-codec-1.9.pom": { + "sha1": "f5357ff0f308600af3660bf00a8be3415a335723", + "sha256": "sha256-5e/PA5zZCWiMIB3FR5sUT9bwHw5AJSt/xefS4bXAeZA=" + }, + "commons-codec-1.9.jar": { + "sha1": "9ce04e34240f674bc72680f8b843b1457383161a", + "sha256": "sha256-rRnSYBw6vwuUa1w6QRPiJqjB4zBeOVuQATt43ZSnI84=" + } + } + }, + + { + "path": "commons-codec/commons-codec/1.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-codec-1.10.pom": { + "sha1": "44b9477418d2942d45550f7e7c66c16262062d0e", + "sha256": "sha256-vbjbcBLREqbj6o/bfFELMA2Z7/CBnSfd26nEM5fqTPs=" + }, + "commons-codec-1.10.jar": { + "sha1": "4b95f4897fa13f2cd904aee711aeafc0c5295cd8", + "sha256": "sha256-QkHfqU5xHUNfKaRgSj4t5cSqPBZeI70Ga+b8H8QwlWk=" + } + } + }, + + { + "path": "commons-codec/commons-codec/1.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-codec-1.11.pom": { + "sha1": "093ee1760aba62d6896d578bd7d247d0fa52f0e7", + "sha256": "sha256-wecUDR3qj981KLwePFRErAtUEpcxH0X5gGwhPsPumhA=" + }, + "commons-codec-1.11.jar": { + "sha1": "3acb4705652e16236558f0f4f2192cc33c3bd189", + "sha256": "sha256-5ZnVMY6Xqkj0ITaikn5t+k6Igd/w5sjjEJ3bv/Ude30=" + } + } + }, + + { + "path": "commons-codec/commons-codec/1.18.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-codec-1.18.0.pom": { + "sha1": "2aa7395e4e4d63be2fd4439da7a01527235f76bf", + "sha256": "sha256-dLkW2ksDhMYZ5t1MGN7+iqQ4f3lSBSU8+0u7L0WM3c4=" + }, + "commons-codec-1.18.0.jar": { + "sha1": "ee45d1cf6ec2cc2b809ff04b4dc7aec858e0df8f", + "sha256": "sha256-ugBfMEzvkqPe3iSjitWsm4r8zw2PdYOdbBM4Y0z39uQ=" + } + } + }, + + { + "path": "commons-io/commons-io/2.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-io-2.4.pom": { + "sha1": "9ece23effe8bce3904f3797a76b1ba6ab681e1b9", + "sha256": "sha256-srXdRs+Zj6Ym62+KHBFPYWfI05JpQWTmJTPliY6bMfI=" + }, + "commons-io-2.4.jar": { + "sha1": "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad", + "sha256": "sha256-zGpB3D6qzJ5ECmvQ0okLINNrTuQI/i1nEi8yi7bgFYE=" + } + } + }, + + { + "path": "commons-io/commons-io/2.18.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-io-2.18.0.pom": { + "sha1": "cbd7a6fd54081f8e3d2a90c3cb74a91a31662a86", + "sha256": "sha256-Y9lpQetE35yQ0q2yrYw/aZwuBl5wcEXF2vcT/KUrz8o=" + }, + "commons-io-2.18.0.jar": { + "sha1": "44084ef756763795b31c578403dd028ff4a22950", + "sha256": "sha256-88oPjWPEDiOlbVQQHGDV7e4Ta0LYS/uFvHljCTEJz4s=" + } + } + }, + + { + "path": "commons-logging/commons-logging/1.1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-logging-1.1.1.pom": { + "sha1": "76672afb562b9e903674ad3a544cdf2092f1faa3", + "sha256": "sha256-0PLhbQVOi7l63ZyiZSXrI0b2koCfzSooeH2ozrPDXug=" + }, + "commons-logging-1.1.1-api.jar": { + "sha1": "eb3b72ff88dba201bbf878cdc6fdbe8e63c73ac0", + "sha256": "sha256-0w5N476WL+hizdXUZVD2nUpa/4zixgBV1mN8PtFMElc=" + }, + "commons-logging-1.1.1.jar": { + "sha1": "5043bfebc3db072ed80fbd362e7caf00e885d8ae", + "sha256": "sha256-zm+RPK0fDbOq1wGG1lxbx//Mmpnj/o4LE3MSgZ98Ni8=" + } + } + }, + + { + "path": "commons-logging/commons-logging/1.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-logging-1.2.pom": { + "sha1": "075c03ba4b01932842a996ef8d3fc1ab61ddeac2", + "sha256": "sha256-yRq1qlcNhvb9B8wVjsa8LFAIBAKXLukXn+JBAHOfuyA=" + }, + "commons-logging-1.2-api.jar": { + "sha1": "bb0e26afcdce195f68e1373a72709d7a01b55498", + "sha256": "sha256-3xwy+u325f0oUGYMYAG8eHfzjZlvhtoFK+xCdLTmGXk=" + }, + "commons-logging-1.2.jar": { + "sha1": "4bfc12adfe4842bf07b657f0369c4cb522955686", + "sha256": "sha256-2t3qHqC+D1aXirMAa4rJKDSv7vvZt+TmMW/KV98PpjY=" + } + } + }, + + { + "path": "commons-logging/commons-logging/1.3.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-logging-1.3.5.pom": { + "sha1": "378e7ead6461eabd50a97f6503d061cad0dcae9d", + "sha256": "sha256-zPSjA0b1AnuUlqvVYpCsJtFZq/K+ZN8SZWEdyUALnWg=" + }, + "commons-logging-1.3.5-api.jar": { + "sha1": "2e234baf9b0fd82e4e2f0db3b9083900fbfe2f36", + "sha256": "sha256-6WjRzl2r6cQzxHbTd+ZSJzD2FttdtdKO2sfEWMtVMdw=" + }, + "commons-logging-1.3.5.jar": { + "sha1": "a3fcc5d3c29b2b03433aa2d2f2d2c1b1638924a1", + "sha256": "sha256-bXp0TkAnZJ+7UIld+Ul9EJ+Yx2amNwYv6NLqu7MUC6Q=" + } + } + }, + + { + "path": "com/adobe/xmp/xmpcore/6.1.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "xmpcore-6.1.11.pom": { + "sha1": "9f2a56e38f04aa222f8d5436cb07529d80cf89d2", + "sha256": "sha256-cZEYGCECwlM+kqL2fANRAmTmFgVxpzishj51cSQXMj0=" + }, + "xmpcore-6.1.11.jar": { + "sha1": "852f14101381e527e6d43339d7db1698c970436c", + "sha256": "sha256-j3AzxXm5n6DZ1t3LlEiHW15LV3w1AAInjORpl9Z4tzc=" + } + } + }, + + { + "path": "com/afollestad/material-dialogs/commons/0.9.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-0.9.6.0.pom": { + "sha1": "aa1ed023a8ac8b5df9b17e41c2406010bd38bda1", + "sha256": "sha256-IKJBT3IgreiU3lLTJO8SKiuml7aM4aCWlIOsKmmFjLE=" + }, + "commons-0.9.6.0.aar": { + "sha1": "21f4607036dc455fbe6c67ecbe5234c8257c33dc", + "sha256": "sha256-1c+D2Ar30gaOpweGFAK3oCep7LmQ/yqESZj7C/o/yX8=" + }, + "commons-0.9.6.0.aar.asc": { + "sha1": "aa2a557e64d140997e36c3cf633db2ef364ee908", + "sha256": "sha256-llD0cVkCUmVfPm7EOoDcBMAD0KnJrdCkTuAlIuNZY5w=" + } + } + }, + + { + "path": "com/afollestad/material-dialogs/core/0.9.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "core-0.9.6.0.pom": { + "sha1": "d2640552c87f4803f40a8f526b1cf84c29608727", + "sha256": "sha256-W48oHURq59nhq50ycEPlFdgIXjbyFY+e4hv0Zk9j4p0=" + }, + "core-0.9.6.0.aar": { + "sha1": "8f7a3101248ff835d06374a890d6037ee12c8c52", + "sha256": "sha256-KxqlvpOAvE6VInFw0o6oiYRmR769J6UvBpyDOJvtRIQ=" + }, + "core-0.9.6.0.aar.asc": { + "sha1": "7153e77763c8e25c422eb882ca7b295442b873cc", + "sha256": "sha256-Fcru73lCMQdh/N0BidqXqPtjRO2cB0YdgDxFvx/wgbc=" + } + } + }, + + { + "path": "com/airbnb/android/lottie/3.4.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lottie-3.4.4.pom": { + "sha1": "cc31f1b29d1432093566c8ab31d830c20339aaa0", + "sha256": "sha256-O9+GIXZPTBIZ76ApBfZMuU5BkeZSFXesVsGdjB61e/A=" + }, + "lottie-3.4.4.aar": { + "sha1": "213d6753c5580a5ef8c935da845779e1cd350e60", + "sha256": "sha256-I9I4/pL6y7exBYQF+Ppt8Q9okxfE2JOpQeqQ6FJ56FM=" + }, + "lottie-3.4.4.aar.asc": { + "sha1": "1b8a4efb92c57abc41a2b0bcb52483d05e5269c9", + "sha256": "sha256-DsaaehRrIrRuyACf+KUrVJhX7CMAq5Kzct/i4+7gxv0=" + } + } + }, + + { + "path": "com/almworks/sqlite4java/sqlite4java/1.0.392", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "sqlite4java-1.0.392.pom": { + "sha1": "96723d1426f8d8ad5044c2e26718da5ba3fc6ea8", + "sha256": "sha256-E5VSxYale/bZj4fWt+I/7021PPdAl76WL3ho42BsedI=" + }, + "sqlite4java-1.0.392.jar": { + "sha1": "d6234e08ff4e1607ff5321da2579571f05ff778d", + "sha256": "sha256-JDpkRw/aDoam/d6wr0x6qUJs6E5oy/4Y117l2kt+C5I=" + } + } + }, + + { + "path": "com/android/databinding/baseLibrary/1.0-rc5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "baseLibrary-1.0-rc5.pom": { + "sha1": "e956f9975bff435c6fc95aab976ff3b1032f3794", + "sha256": "sha256-CkKgxwGhebjuqzny32uEu3qrmOIbbcWbakt6mk+hAHE=" + }, + "baseLibrary-1.0-rc5.jar": { + "sha1": "4d15bfec5956b906a26527dbde26a4fc84326611", + "sha256": "sha256-7EPDD2CiRYou7iabh5+YO3XicJXlKxBUj+fi4duc06E=" + } + } + }, + + { + "path": "com/android/databinding/compilerCommon/1.0-rc5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "compilerCommon-1.0-rc5.pom": { + "sha1": "167cd6131d81c21ed0582c157339425d33b6afbd", + "sha256": "sha256-90r5hkzMHTLDz9EgiztPNlJTFfLY9nomhYntixa2rq8=" + }, + "compilerCommon-1.0-rc5.jar": { + "sha1": "3152caf6b493a20e5b0d63bb36f5322dbce6d10f", + "sha256": "sha256-LMYi44NiPgyDADffx8o78jA3acosx80qjI4D59pRQk8=" + } + } + }, + + { + "path": "com/android/tools/annotations/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-24.3.1.pom": { + "sha1": "ad6e2d7013950d4b3d6290420fc145c5107821be", + "sha256": "sha256-fgjLmee01SHFb53wOv42qwdpaJrshWBUlvoTLfE/cxg=" + }, + "annotations-24.3.1.jar": { + "sha1": "3c0f015e74a277e90ca34229458dd767aa39bb30", + "sha256": "sha256-ygR8CG/kak6Mfq0RcU/7PAFhip1uUeS8/ZUhbrbheIg=" + } + } + }, + + { + "path": "com/android/tools/annotations/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-24.5.0.pom": { + "sha1": "16ae9945d5f0ed5fb1aff1d9aaa627b82096b56e", + "sha256": "sha256-w8mb9YGCiJ/oYxXpoBRz7iyVVAud7e+JjOxkVU2SXFQ=" + }, + "annotations-24.5.0.jar": { + "sha1": "7f2bcdf40b008f15481324327b7f9e377b0b1697", + "sha256": "sha256-Kfuvjp7ny9u/PiPClVwEu3Utg10sTJCTbongI1F92SI=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/1.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "builder-model-1.3.1.pom": { + "sha1": "ac4b0c04b2ad8d2549370600443b0790c9633f7b", + "sha256": "sha256-a+/h4zAAkEK/S2OAqzwV3TKsppCDmnTE+OjvXdWz5qk=" + }, + "builder-model-1.3.1.jar": { + "sha1": "16c0a3e34af44f31f757144d7c1bf045777dff88", + "sha256": "sha256-AWbJ7CaUyltQE42No3aoVhuCDPmtK+3Mw3r6ty/pSWw=" + } + } + }, + + { + "path": "com/android/tools/build/builder-model/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "builder-model-1.5.0.pom": { + "sha1": "669b49337dfbcc7cbfc0c12d3bff4e179bf160ad", + "sha256": "sha256-zwf/Kqj6sR2okIPv69L3RxAw579DN0XzyScQGw1u41c=" + }, + "builder-model-1.5.0.jar": { + "sha1": "51a48a2f4c1da923b0b8bb174e57bc68ee040889", + "sha256": "sha256-QEoz3yQmpcbY5EFmFOz9oilt9/XS03rMddXUsO1H1FY=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/1.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "builder-test-api-1.3.1.pom": { + "sha1": "0a5740c225133609a72df078483677bbd52ab5d5", + "sha256": "sha256-ifzgMjFfAL1Go7RVlQKsTpunIN3+IqL8CjdpzISKvAA=" + }, + "builder-test-api-1.3.1.jar": { + "sha1": "1954d436ec3eeea11070e91e6c808232837728b8", + "sha256": "sha256-rAPEDH2xxTB3JTb+4aYALLi54WnQ9FdEBVZ4JiAEvxg=" + } + } + }, + + { + "path": "com/android/tools/build/builder-test-api/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "builder-test-api-1.5.0.pom": { + "sha1": "c72342d5d41bc45f97e42f74f887a2c88e3c69c5", + "sha256": "sha256-jfkEbz8/ZHvCdHFYMfD0o+ZWsRZ8QJaZau0G/Oj7gCg=" + }, + "builder-test-api-1.5.0.jar": { + "sha1": "0d9240c30b1bb243b4f57c48f734df2635fbd07e", + "sha256": "sha256-kHaopDfqkjxGHF7Q6V4okCc3GepnVyLCSQgaOZv+dH8=" + } + } + }, + + { + "path": "com/android/tools/build/builder/1.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "builder-1.3.1.pom": { + "sha1": "dae3278eebf0589048e229b0142fcca0799bc170", + "sha256": "sha256-d1Z1NFEfPqeLNDNMo9HzL+hEOMiKZCDZRPkiNdBHcDQ=" + }, + "builder-1.3.1.jar": { + "sha1": "83e6307a43477f2a5ff0b5b654b131425c3dcbc5", + "sha256": "sha256-p3N4hN1qqdy626aZAI40wnEmHRwTRA14Nt9B0e1hLcA=" + } + } + }, + + { + "path": "com/android/tools/build/builder/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "builder-1.5.0.pom": { + "sha1": "9ebf618447bc810b57feb2fd4635b556826696cb", + "sha256": "sha256-suSFIE8wmXGhFHn/ULow0VC+8BmAqWCHHRNglTNo6uA=" + }, + "builder-1.5.0.jar": { + "sha1": "1dcd59f6f3c90b2a8bc7156d091ad1b87a41beed", + "sha256": "sha256-t41TvhMo2/tm1kxpe8Jz0R6pBaFEkSfm5mhycv5WbA4=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-core/1.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gradle-core-1.3.1.pom": { + "sha1": "d25a6df467931a378d376e430604076753bbb1f3", + "sha256": "sha256-qh2bNv/KCSN3+C2KLA9Gks9tXXKpKqv6/QVAzwOodQ0=" + }, + "gradle-core-1.3.1.jar": { + "sha1": "70731f5b83a22faa2f25d6281f6812977f64f520", + "sha256": "sha256-yWCmKw6IATa3Doftgtw0s3Eb6FhBNYclvf4ew5GZ2Mo=" + } + } + }, + + { + "path": "com/android/tools/build/gradle-core/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gradle-core-1.5.0.pom": { + "sha1": "38fc0646f9f74f90543167609f80583c69d03393", + "sha256": "sha256-9CBoYj2Sn6CPgGuWMaTa2CEMWgtf/qRPJCNqMn+CbjU=" + }, + "gradle-core-1.5.0.jar": { + "sha1": "70915a3f0ef4243d4630ea23219f5445fac82700", + "sha256": "sha256-yOzQL0WKdPUCzQcsW3fztZkHVdhxcMAbcaphru80lnQ=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/1.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gradle-1.3.1.pom": { + "sha1": "3c8a4b12eeed68097bc8470fb80504e1243a483c", + "sha256": "sha256-tiqqIr97rAuc1APgDY6UWoe2Q8qyvxFlssdYgE19c2g=" + }, + "gradle-1.3.1.jar": { + "sha1": "f9f3d53c24efd3b9176ec36aeed9c518954c2fd9", + "sha256": "sha256-da3lmFFDjto4i/dZzGG4887NEpiJgAjUThWeNKgPf1w=" + } + } + }, + + { + "path": "com/android/tools/build/gradle/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gradle-1.5.0.pom": { + "sha1": "1c3d2b23212349ffbe6479fcb04732c5cddaf98b", + "sha256": "sha256-OijJ07ShRqM29TRsJHwh7u4mLkwGOTfBcj6GfWux3tE=" + }, + "gradle-1.5.0.jar": { + "sha1": "ca6476d93e374e70d0b7ca45ddc86151da840aa6", + "sha256": "sha256-zaFbZwmz+KGxAClIEOohIoMsOwVZDTRAbizDEAqfBns=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "manifest-merger-24.3.1.pom": { + "sha1": "d92b94e91d3a7e12d513c8aff3099b61d1fd274e", + "sha256": "sha256-BcxHJcwWS59LD55jnFR1QS+Kw3yezP/SU7POb54vUWc=" + }, + "manifest-merger-24.3.1.jar": { + "sha1": "b9b0fc1064f963774beb573469ac371bf0c2c461", + "sha256": "sha256-h/qAbGryO3+pIDgXmUnQiFRzRHwkle2yVyP8dvdGx+M=" + } + } + }, + + { + "path": "com/android/tools/build/manifest-merger/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "manifest-merger-24.5.0.pom": { + "sha1": "4ed7f2f4e41e4dae51810f14d34c09e64ffae831", + "sha256": "sha256-09KCyJ9daCskqw2YBso2JjjvA8lOptkWRlAIf3Rxv3Y=" + }, + "manifest-merger-24.5.0.jar": { + "sha1": "dc521cf0bc3565f990d4da7fac4aabc73b6d62cf", + "sha256": "sha256-mU9Mctp9UdtevgewYtfOHNN8jNxoLwjtzIQyYzMFs2o=" + } + } + }, + + { + "path": "com/android/tools/build/transform-api/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "transform-api-1.5.0.pom": { + "sha1": "48a97797e6ee321d516e5ef4844449660f09adb2", + "sha256": "sha256-YXgdVW5+u4840dIsfNjT3dR8jCExp3Z2p7RgqKFzlNU=" + }, + "transform-api-1.5.0.jar": { + "sha1": "090f914a7972579a4cc726c9e0022878f6c675e2", + "sha256": "sha256-rrTXhsJZoHLR8P1aEpfdBpFcbiS7LtFRUocqex3AdiY=" + } + } + }, + + { + "path": "com/android/tools/build/transform-api/2.0.0-deprecated-use-gradle-api", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "transform-api-2.0.0-deprecated-use-gradle-api.pom": { + "sha1": "d2a89c5569f53a8c8adfe599ede88cb6f38a8a6e", + "sha256": "sha256-1J0Xn3B9PzoAsqfTYTa1SqjUT6IncHA82C/lL7OeIus=" + }, + "transform-api-2.0.0-deprecated-use-gradle-api.jar": { + "sha1": "085bee1acea9e27152b920746c68133b30b11431", + "sha256": "sha256-6LQVGuFnnxq+ehTuNxrJs8ZRrntjKQ0fWGvdD3j6zpo=" + } + } + }, + + { + "path": "com/android/tools/common/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "common-24.3.1.pom": { + "sha1": "23de0b66e531bd1e140b1121ff66edff6e17a5aa", + "sha256": "sha256-ajq+e1vOe83l/xGJ7x1pMQyxk6rMT3zE0MB/rkAM9w0=" + }, + "common-24.3.1.jar": { + "sha1": "92f1a60ebb004e2969661315de200232f90e0a80", + "sha256": "sha256-9J75Z4+l+Zjkb/vId4JiDDum2xCedwE0KT4jf2ViB3o=" + } + } + }, + + { + "path": "com/android/tools/common/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "common-24.5.0.pom": { + "sha1": "db31e9ab2e791b2eb4a90319b101aff1c333f75c", + "sha256": "sha256-pFEJsHZpDyBmC3A8iLceybEOzzn4s/ah4frKsrfgbbw=" + }, + "common-24.5.0.jar": { + "sha1": "55436737b893bac95c978b184dd35c76002d2eef", + "sha256": "sha256-CpDC2xtVQZVBqLr6hYARIurjnr0ls2pTb0sAVyp3VbM=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ddmlib-24.3.1.pom": { + "sha1": "82d64d7df960709b51584d4c2cb8c011a384bd7d", + "sha256": "sha256-yGPKtK1hm6a681FOiaHMOj8tGBHwdF+/y9e/JkgCGj4=" + }, + "ddmlib-24.3.1.jar": { + "sha1": "2018ea863b14fe3aeaf2b49316ee98567e1d9c11", + "sha256": "sha256-/5Wa1Honr+lHEucKxqG9B+mnB2MQ83gSFRZvfbbxqlI=" + } + } + }, + + { + "path": "com/android/tools/ddms/ddmlib/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ddmlib-24.5.0.pom": { + "sha1": "cd0a918c94d6f5745a8f8a3be1cf4e5da24228a9", + "sha256": "sha256-SftLmOgeUWXUrBomqi46nJx87hQYIDBZs7cWeIcQV+E=" + }, + "ddmlib-24.5.0.jar": { + "sha1": "d45670b5511da60d2939a1eece4c7c4407fee95b", + "sha256": "sha256-9Y2jMMkzJi2V4EQsigRMqiijk/L/0UWLpVhDp6R2JBc=" + } + } + }, + + { + "path": "com/android/tools/dvlib/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "dvlib-24.3.1.pom": { + "sha1": "f6b49646e055630844bd5e1a93affd62887ce613", + "sha256": "sha256-mZsw08MtgejLsti7OTFrZVi0V12JZWtgVmiMgBTtJ+0=" + }, + "dvlib-24.3.1.jar": { + "sha1": "1d78ec584fb99277e54e849b8ab57b860d590c17", + "sha256": "sha256-xCqNqHRiDljFuBPR//dhWTZnoHPcjo1cdKFDEOMLJZc=" + } + } + }, + + { + "path": "com/android/tools/dvlib/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "dvlib-24.5.0.pom": { + "sha1": "ce0a9bdd22c7f8ddd0072526f4d0e3c5e565b107", + "sha256": "sha256-ZnwqVYE9BwBR5GCDDw+j3gRYtuA9VTPY+XFvR4GLkhY=" + }, + "dvlib-24.5.0.jar": { + "sha1": "0bb3cda7db1fa6c9543e887a8c451d0449e76fa4", + "sha256": "sha256-cXP2oDo3AqwscaDfu4ahtjlOvg6ekoTKS7B3XlFftv0=" + } + } + }, + + { + "path": "com/android/tools/external/lombok/lombok-ast/0.2.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lombok-ast-0.2.3.pom": { + "sha1": "ac2e013f1621e1456de3ac77aeae0437237c736c", + "sha256": "sha256-jGGR4N2L/chiMyy3mwgYWsW2NWXUBdrCYFvkwrGXdL0=" + }, + "lombok-ast-0.2.3.jar": { + "sha1": "0528b6f8bde3157f17530aa366631f2aad2a6cf9", + "sha256": "sha256-ResNbklDAnG1+zhcU/rguB1q0+rkZaeo2Ughe8rh6G0=" + } + } + }, + + { + "path": "com/android/tools/jack/jack-api/0.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jack-api-0.9.0.pom": { + "sha1": "2abc263cbda683515f7a62a4e78cbb418ce1d336", + "sha256": "sha256-kgwnT+dCgP0UqlSZV+5thVW2mw4N/Ef9ZkbkGStInYU=" + }, + "jack-api-0.9.0.jar": { + "sha1": "6b474acfffc1e9d9e03df4ab3579b5fef852ed23", + "sha256": "sha256-uAIkK33x0Oj0iw0K02fJ1UxS/cCrGk0JFLlvKglAFCo=" + } + } + }, + + { + "path": "com/android/tools/jill/jill-api/0.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jill-api-0.9.0.pom": { + "sha1": "f2b3545b3b7149a5e27c95a815ee6d7781ecd485", + "sha256": "sha256-o29dlD0P0aYjzfo8iw0WsWBtJ6Ub8DEnw01U2I46yjk=" + }, + "jill-api-0.9.0.jar": { + "sha1": "09a83e898b15c853f6c92c74791064ca400cd3f3", + "sha256": "sha256-uBzvrUBbabyz/T4BVOAp+/aqXftzBS2hPEf1PzOQTxw=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "layoutlib-api-24.3.1.pom": { + "sha1": "f34f004d8e5c2a38fabe6323815f94ace633853a", + "sha256": "sha256-q5D7wE1QN3E7XBFQibw+152MqWV2cgdmKRKly1MCi84=" + }, + "layoutlib-api-24.3.1.jar": { + "sha1": "982783a222369254fce34336cc48155bbd87e3fd", + "sha256": "sha256-eRvmh1iSZZDSmkPqOeAPPUQXhtsOVttbKcBWOOccXJk=" + } + } + }, + + { + "path": "com/android/tools/layoutlib/layoutlib-api/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "layoutlib-api-24.5.0.pom": { + "sha1": "fa97b9a79f6122105ba672a6fa02d1a1577dcdf5", + "sha256": "sha256-7OzPt/5hjeZJ4A8n9OP7HPly/3tf8def4vY1IY5eT8E=" + }, + "layoutlib-api-24.5.0.jar": { + "sha1": "387949832720a36855711d2033cd4122e945f36e", + "sha256": "sha256-uxrzMRMygzSr17DoDPZqW1661z6GP6RYdWJ1Yv68tQ0=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-api/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lint-api-24.3.1.pom": { + "sha1": "47bb5e97941016323d8191f45651371ca6ed3dcf", + "sha256": "sha256-nF3vvfL0NTiSMESa3PXCeX9OkxwEmwmmf7mxDjmhnyU=" + }, + "lint-api-24.3.1.jar": { + "sha1": "f5666b0d8408e79273a17cbe89c673c98f37b9fa", + "sha256": "sha256-xMH/TvrVfjUkOyHuzDH32tJb1ALC9mDBhnGUZRkL3cg=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-api/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lint-api-24.5.0.pom": { + "sha1": "a9683a32abea8a508c6ed7c3c1b5eb3dc0b75557", + "sha256": "sha256-ciYPPcVFzLgvYtBDIazjQoxRrYOxphzQEVzSmmyZFXs=" + }, + "lint-api-24.5.0.jar": { + "sha1": "7d93eb96a2f07860ab8d9ae761ebe5c416777fcf", + "sha256": "sha256-5jqnImySSo2ijfKNzERkBdVOlSW0UlzE4I+47EEHEJQ=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-checks/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lint-checks-24.3.1.pom": { + "sha1": "c5fa1b16386424e0a2fee9830554cf1214d9ac8f", + "sha256": "sha256-RtuZEQ+mNZ2elSqB7WbZ14F5d32d3nyLYIBlovz4IGQ=" + }, + "lint-checks-24.3.1.jar": { + "sha1": "ee1edb630d923511bb46bef4da148dcb0a74a029", + "sha256": "sha256-5OuemToySOBIeW0FSYx1WW5yTlao3NlsdCN3HUZtInE=" + } + } + }, + + { + "path": "com/android/tools/lint/lint-checks/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lint-checks-24.5.0.pom": { + "sha1": "489b0c0e5c956150a7cf29cd2749d6e505db3917", + "sha256": "sha256-rx2IxIiL2DZyJNW1EluBa1/TxWb0H5YX6S8JCFNebgo=" + }, + "lint-checks-24.5.0.jar": { + "sha1": "dbdca0447c2333481823e088a356393e653276b4", + "sha256": "sha256-Wdeyg0ELn8pU1AvnWgPNDAwKCbzgmJdA6ogOHttL3kY=" + } + } + }, + + { + "path": "com/android/tools/lint/lint/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lint-24.3.1.pom": { + "sha1": "0ee9c4bf792910e6dff319f2602f1b0ca99940d6", + "sha256": "sha256-0iaH0Vt0ADrHc7a9CKs908f+kL7uEQttJ3ypebqXlYQ=" + }, + "lint-24.3.1.jar": { + "sha1": "57cb2a6361f32753bee3670f072f097840219e34", + "sha256": "sha256-bbsouAPoTfBI5gWOi+Pi05kOd9LBiihtij9IPQAKp90=" + } + } + }, + + { + "path": "com/android/tools/lint/lint/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "lint-24.5.0.pom": { + "sha1": "3759044c677888f526044f3ee25d77de19dfb4f8", + "sha256": "sha256-yyd5fLpkdbwrHoZ5ZNljL89V27jkqCkO+yUVXA6C62E=" + }, + "lint-24.5.0.jar": { + "sha1": "461d7d801b822cbeb7daf001924fe1c616ce9535", + "sha256": "sha256-N3A/Wt2o6E3DcDr26L5qglbF6UycN3zozmcTSqxNFCc=" + } + } + }, + + { + "path": "com/android/tools/sdklib/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "sdklib-24.3.1.pom": { + "sha1": "045ca04b7749735b99d854d3abddf56cb49b3f10", + "sha256": "sha256-XzyLfzi2yXiSsSqzjOMqy2BGH9kmkONXzScZhiGImlE=" + }, + "sdklib-24.3.1.jar": { + "sha1": "1a4226d7db1381552b94fee30453a08fbfd678a5", + "sha256": "sha256-SssK0doISbYNaN8muyoRRjjDUSexgiY8olz35cckPl4=" + } + } + }, + + { + "path": "com/android/tools/sdklib/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "sdklib-24.5.0.pom": { + "sha1": "bff3e933089e1c17d494ea2ade8b9690803b26f3", + "sha256": "sha256-L1zV7ooFMokTtjbKb1WWijtZjpPrE+OKzel/qM7N7Ok=" + }, + "sdklib-24.5.0.jar": { + "sha1": "473c60ca5786363b6b3ea944446cf41676207ca7", + "sha256": "sha256-FtaozRxaTnEO32tN3mS9sgx7F2EX8aAzE8nwq+ufJfE=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/24.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "sdk-common-24.3.1.pom": { + "sha1": "b1614d578635b1badd25f11b64d658b5fe1b1b26", + "sha256": "sha256-XDurYBdGte+5T7l+koMHcxa7hxtwtU6weKHfuDzf53A=" + }, + "sdk-common-24.3.1.jar": { + "sha1": "5c28ab2dda1600eddaa978e095f0d8c044cac33b", + "sha256": "sha256-yALG4FsVcjrr4RJgbRnooRL52kD4E9kfrd1OTM2d/tM=" + } + } + }, + + { + "path": "com/android/tools/sdk-common/24.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "sdk-common-24.5.0.pom": { + "sha1": "7c9c1a690245a44d024cc8700211c39c1ec900ef", + "sha256": "sha256-Ccfi+Nmqh59DBwFrmGpC6Sr8vq5q8uDpjaUK7TzCoWI=" + }, + "sdk-common-24.5.0.jar": { + "sha1": "e9b9c057d8df82ee7bc682558fe21b2110b3e40a", + "sha256": "sha256-xyYmthraLilAAmwJG6ZKF4EN/plUeGnmhr/iExM14iY=" + } + } + }, + + { + "path": "com/diffplug/durian/durian-collect/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "durian-collect-1.2.0.pom": { + "sha1": "0bd196532b47f195e61a179ea7be66ca4e5b12d8", + "sha256": "sha256-i7diCGoKT9KmRzu/kFx0R2OvodWaVjD3O7BLeHLAn/M=" + }, + "durian-collect-1.2.0.jar": { + "sha1": "eee2085a0f894527ac841b05b442656115e2877c", + "sha256": "sha256-sZTAuIAhzBFsIcHcdvScLB/hda9by3TIume527+aSMw=" + } + } + }, + + { + "path": "com/diffplug/durian/durian-core/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "durian-core-1.2.0.pom": { + "sha1": "ca659370ff47a81844e968ffc4d001f2d56911cd", + "sha256": "sha256-hwMg6QdVNxsBeW/oG6Ul/R3ui3A0b1VFUe7dQonwtmI=" + }, + "durian-core-1.2.0.jar": { + "sha1": "ddf0b08f6dccae07bcb993645a7634cbe8f9b2f8", + "sha256": "sha256-F+0KrLOjwWMjMyFou96thpTzKACytH1p1KTEmxFNXa4=" + } + } + }, + + { + "path": "com/diffplug/durian/durian-io/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "durian-io-1.2.0.pom": { + "sha1": "eeeb5dbc5add4ef406c597b785e91986787ea17c", + "sha256": "sha256-NQkZQkMk4nUKPdwvobzmqQrIziklaYpgqbTR1uSSL/4=" + }, + "durian-io-1.2.0.jar": { + "sha1": "fc9c103d454d04f7c5b78b437190ea3bd15fb72c", + "sha256": "sha256-CV/R3HeIjAc/C+OaAYFW7lJnInmLCd6eKF7yE14W6sQ=" + } + } + }, + + { + "path": "com/diffplug/spotless/spotless-lib-extra/2.26.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "spotless-lib-extra-2.26.2.pom": { + "sha1": "1de01592f56268f58007d91da47df8fb94993c5e", + "sha256": "sha256-qnWNc4mzh55xA/eNtNfj7CF8AWq8rlGsG9Eu4MEmoVo=" + }, + "spotless-lib-extra-2.26.2.jar": { + "sha1": "4f3fe4e991b2e386be0bc9753fc5018f5fe08beb", + "sha256": "sha256-m0zpiqT128dYUP5tmnnoeNMYvnGPmX5epAUsiFYhuqs=" + }, + "spotless-lib-extra-2.26.2.module": { + "sha1": "9256ffa10bd872e22ad46b136878859457b707d5", + "sha256": "sha256-02HDw5v0BosWXuv0wQIhUSCnbp9NL8iKYbvbjfSQdQQ=" + } + } + }, + + { + "path": "com/diffplug/spotless/spotless-lib-extra/2.30.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "spotless-lib-extra-2.30.0.pom": { + "sha1": "f12c6c0a73e7db1b2e00d671741512c5ff6bbfc5", + "sha256": "sha256-/aZTxHtda3bliX/u2ebt0n7WtDsqzw0WAwFDHdFhXN0=" + }, + "spotless-lib-extra-2.30.0.jar": { + "sha1": "4f3fe4e991b2e386be0bc9753fc5018f5fe08beb", + "sha256": "sha256-m0zpiqT128dYUP5tmnnoeNMYvnGPmX5epAUsiFYhuqs=" + }, + "spotless-lib-extra-2.30.0.module": { + "sha1": "b9463de43d9f9e52ad49e4dcbc4dc619a0be9961", + "sha256": "sha256-hRX3JbKhNxn2Ww81yFUf6IBx3VUONlw6OdYkqyqBEpI=" + } + } + }, + + { + "path": "com/diffplug/spotless/spotless-lib/2.26.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "spotless-lib-2.26.2.pom": { + "sha1": "045eb2262fd6118ec48049eed83b3e9fab277792", + "sha256": "sha256-rP7Qc7tEKTVFhftyVc9m/01uH0TdPF+9PBpnUIb4VjQ=" + }, + "spotless-lib-2.26.2.jar": { + "sha1": "054779b48f9f745a688a0ed85f82503ee00ba5f2", + "sha256": "sha256-7ILKA5XLvbUrJZsEAc2YSTL3pIafrQOhxCZKumO1oXk=" + }, + "spotless-lib-2.26.2.module": { + "sha1": "24a7710b2e8b954d9002213341a8205d4064e09b", + "sha256": "sha256-WIN5rauYDCikhDDlPCrY0Rckn4/THcY0QCCttnYmO7Y=" + } + } + }, + + { + "path": "com/diffplug/spotless/spotless-lib/2.30.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "spotless-lib-2.30.0.pom": { + "sha1": "5fa9d7789f4c24053fd1df6050f4e3a93297e1c9", + "sha256": "sha256-Qh7qoUJrVb/lMmy0UD5Vjc2suG0t7HJWiD4EQm45Qj4=" + }, + "spotless-lib-2.30.0.jar": { + "sha1": "8aa538fd70f8a141e8cc6ca288f05445548ad058", + "sha256": "sha256-IfTX/AMr+tpC8p5inOfYlOGpvPfdWqmyQ/7dDH0k8KE=" + }, + "spotless-lib-2.30.0.module": { + "sha1": "2b985a0ab76a338e666f608a58a26d026291a66a", + "sha256": "sha256-tEP7lj0UwzViIWbzYBgEW5cMxm1BO96ttc5xZYRE05k=" + } + } + }, + + { + "path": "com/diffplug/spotless/spotless-plugin-gradle/6.7.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "spotless-plugin-gradle-6.7.2.pom": { + "sha1": "dc6ef9dcfa11ec7f69100a1df8a83e160651f99a", + "sha256": "sha256-7bLH0z8XRkXBBzdCqsBXGSJ+C6HMPh2NNKOeZ6NFq9s=" + }, + "spotless-plugin-gradle-6.7.2.jar": { + "sha1": "a94824f0d6dfdebc8ef1378af6f32ae514cc8930", + "sha256": "sha256-OWJ8h6L1icutVSVQnXdVoKZOzB79nygALCe6ReoVwR0=" + }, + "spotless-plugin-gradle-6.7.2.module": { + "sha1": "f5c08633e213cc774d50230fbcb7b75e61d6e3b3", + "sha256": "sha256-2nrK4GmiP6/TXxGlhzzRFZbHpKxhua0YvlHy9zBof1c=" + } + } + }, + + { + "path": "com/diffplug/spotless/spotless-plugin-gradle/6.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "spotless-plugin-gradle-6.11.0.pom": { + "sha1": "8c83606c3d3f5b1005c519a50e7531f0d9c4a6c1", + "sha256": "sha256-WTL5RMFQ2jo4JHw//uDhbtAuvPAt9TdQr8ZWU89ELdw=" + }, + "spotless-plugin-gradle-6.11.0.jar": { + "sha1": "4bace71cc3201f9de81752b32f94532004bdaa4b", + "sha256": "sha256-VCk7hhcLghEVdyCQu5CutGbNpr+eCLBrrL07uUpxTh8=" + }, + "spotless-plugin-gradle-6.11.0.module": { + "sha1": "d7330f25897a207af97a31886b66cbb1c57c74de", + "sha256": "sha256-IanBGRH06NtFAqb2iCUD7q9SaJYlkCnhXbNSthtIwmI=" + } + } + }, + + { + "path": "com/drewnoakes/metadata-extractor/2.18.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "metadata-extractor-2.18.0.pom": { + "sha1": "8a253b5c301e2b71ddb0ad28f3d8656d34f8781f", + "sha256": "sha256-p9E4Id81LFQz1QjswN6KDwSWOE6kggfYEhR8pZgqFZ4=" + }, + "metadata-extractor-2.18.0.jar": { + "sha1": "fa9fd43a28b10333108c603819810d5176d2b092", + "sha256": "sha256-R4k2H9Bji9skFVS3oMyuIF7SOWl+K3D6nK2t7WmEtWU=" + } + } + }, + + { + "path": "com/facebook/conceal/conceal/1.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "conceal-1.1.3.pom": { + "sha1": "7a127807f09f8dfd511c539d42608f585ebd64fa", + "sha256": "sha256-dwRKFAO3MvyYEL2yOyNWrs6aiB86y9A21KiRucOqrnI=" + }, + "conceal-1.1.3.aar": { + "sha1": "5d117a4a5dc63f52575fe16cca1b1605cbd8f86d", + "sha256": "sha256-gfZLig+qxnIhBuXV2fKJu0CgK/2XkzdlcdTX5XH8Ns0=" + }, + "conceal-1.1.3.aar.asc": { + "sha1": "f88925f7e40e0c99b86cbd0ee060fd3393f0a875", + "sha256": "sha256-39weWPJyt2BxZUoDlIXFwH1N4MoaJ5euOck7d1YlYik=" + } + } + }, + + { + "path": "com/facebook/fbjni/fbjni/0.5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fbjni-0.5.1.pom": { + "sha1": "079b7085cbd84f8f596e6baf022ef20182876afb", + "sha256": "sha256-OKEai/wJ8pXPteyxRIVVbAUWhEgTKYLNykERC5sF9vM=" + }, + "fbjni-0.5.1.aar": { + "sha1": "03c2e028858e79f35837cf34a7701f1156ceb81b", + "sha256": "sha256-4Q/n/JztmJxBsFzgRopOu8XR04YW7f6KAWqgf7i6Tt4=" + }, + "fbjni-0.5.1.aar.asc": { + "sha1": "e6b5efe31f4bcb5e7032accaaad61d8f7849c93f", + "sha256": "sha256-ZsVIWGxqpiAWLuNdsE11geWhwFbLgugJsARzj/Jbgiw=" + }, + "fbjni-0.5.1.module": { + "sha1": "f15be15e5f3ed32a6be74be25c141b5bdb045e69", + "sha256": "sha256-279MAZyxLzJFQ0TVdftTDJ3lBUobMIlDBtmCZ5V60sI=" + } + } + }, + + { + "path": "com/facebook/fresco/animated-base/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animated-base-2.5.0.pom": { + "sha1": "f83fed83c5ff9bcd1593d5206436472c4c42a69a", + "sha256": "sha256-00P/OqFr7JaNfJvjwOxw5DJVuSTyasS2cSwjJRj9vGo=" + }, + "animated-base-2.5.0.aar": { + "sha1": "8c9afdf062a681646dcc467b9124257886c6548b", + "sha256": "sha256-oWh+dbR94eVu0FDw6pDGw5401lnW/zbYgcJibMhZulI=" + }, + "animated-base-2.5.0.aar.asc": { + "sha1": "5ebe59a1ee6a28a7aba7df93fe98ef21c3596700", + "sha256": "sha256-GWqLcmJ8yRci33R3G1ZQTR6y9LLkSJVvIOdr3LLhSLo=" + }, + "animated-base-2.5.0.module": { + "sha1": "0c9d917a160bf3eb7774cc94985b8e8bfe960dff", + "sha256": "sha256-lYK1z4/tjKyoNl+0LU9ccp1GqsJM4/qr3sqj+NHLITU=" + } + } + }, + + { + "path": "com/facebook/fresco/animated-drawable/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animated-drawable-2.5.0.pom": { + "sha1": "6963e3b9fbfe3c91cb947576b27bdcbae6707c60", + "sha256": "sha256-eYFVC/rwNvt3GVJhJEbOZQZRHEimeY7jEDEjMSsq5NY=" + }, + "animated-drawable-2.5.0.aar": { + "sha1": "885c83e3b0a95f7550cd2f554a8713b08f8c73c6", + "sha256": "sha256-RwjBmS5Qa5c4MAZdJUtj3D9MoANix8zzD3twWZl3EXI=" + }, + "animated-drawable-2.5.0.aar.asc": { + "sha1": "910e9dd6a8a0840c205e7874d03e0fe92d726faa", + "sha256": "sha256-srBc+NLVouPFioGx7JdRn9cE7qo4yvFn5glyNdYrS0M=" + }, + "animated-drawable-2.5.0.module": { + "sha1": "b9bd47dd024b25186f8369f4283dab3e4ae086df", + "sha256": "sha256-QqlFbZVrPIK3Jh8cnrA8eKEmo3EBkdH3WePI/Rk2aH8=" + } + } + }, + + { + "path": "com/facebook/fresco/animated-gif/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animated-gif-2.5.0.pom": { + "sha1": "c8fcb6b5161f31695be7f69e9a3b30023c612e70", + "sha256": "sha256-shp3Jh75BjiwhWColoxGccGAP7qUisjgqH8z6NwzoyM=" + }, + "animated-gif-2.5.0.aar": { + "sha1": "f08f7ad3abf216f674b636176d45d59fd503d19b", + "sha256": "sha256-xGXMWZPoIzw53cXjL90vMWq4L1JKX+DlxFjhQoBGcbg=" + }, + "animated-gif-2.5.0.aar.asc": { + "sha1": "bbfcf75f25998d3034070583063684d739026182", + "sha256": "sha256-P7xdfdGIeEdiwsKvSHOM3Pt2GwJzTohAGWt+stoUrEE=" + }, + "animated-gif-2.5.0.module": { + "sha1": "69389ac8e7ca5e4823d31d95a388afb5c29ac084", + "sha256": "sha256-95QrefEBpe6bg71QOsLq19WfRelKw4FG9EJTjwNU6VY=" + } + } + }, + + { + "path": "com/facebook/fresco/drawee/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "drawee-2.5.0.pom": { + "sha1": "0b9549e54345d2632976e3038e8ff5c830c056f2", + "sha256": "sha256-MdJM7JvRlaI2GBSANiKnJ4pf63JmQRFtMJYCWQXjPmM=" + }, + "drawee-2.5.0.aar": { + "sha1": "06971aca0134eafa61c1b81714c0954cb4eb4e10", + "sha256": "sha256-5M0dyod5O+hQILyQg1B86bkq/7yJiC12dMD4O6BG8N4=" + }, + "drawee-2.5.0.aar.asc": { + "sha1": "406e041c926839af4502bab919f56d30134b1e57", + "sha256": "sha256-UvhHzOSvypF2MUaqlyGa7CheV2lcI2sY4EzwEDmLxBQ=" + }, + "drawee-2.5.0.module": { + "sha1": "8bac510ae8eb096fa85dc1c2668ab042dfda331e", + "sha256": "sha256-H8JKusEKqrJXy5DUO0dlE5DZ4NWMk29DlhXmfukXqe4=" + } + } + }, + + { + "path": "com/facebook/fresco/drawee/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "drawee-3.1.3.pom": { + "sha1": "a1a6053ad88ace6aed4e3d844de61e07e24e63c5", + "sha256": "sha256-dxLcGHR1QFQ1D4CilDObyHCvzVEmSheWUp4IQ6IKHB8=" + }, + "drawee-3.1.3.aar": { + "sha1": "e40081962f2d677c57aef3921b923bd974b157f1", + "sha256": "sha256-GZbMGfyk1xymkPx2bkV7Lc7Zxmk/jTXzt3YOFSkTzls=" + }, + "drawee-3.1.3.aar.asc": { + "sha1": "97998a0d5f4f558a5bae14eb5b6e08ef73de9d04", + "sha256": "sha256-MfZyVmrY0mE322YnU9Cdw7PCDEtl3+fI0SKNfvQHacM=" + }, + "drawee-3.1.3.module": { + "sha1": "7b69c4e618ecf4856a6e032622978a7885e4e210", + "sha256": "sha256-WPhcRI85KxfQMEa2z0RgUIleTIXl+mdkgQJ9VKb6nM8=" + } + } + }, + + { + "path": "com/facebook/fresco/fbcore/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fbcore-2.5.0.pom": { + "sha1": "a5d444cbedff4a20e15f95bfa18c553885030835", + "sha256": "sha256-swHXNt9xSenk+r9191qpgZrpPusTS4InwjMyrcmPHQE=" + }, + "fbcore-2.5.0.aar": { + "sha1": "ffe378b572055e0600377c000d390fb46bf278a5", + "sha256": "sha256-p4RiaweKcVnv9r4o/+lqV+dhl1us5ZVloNV1e7bMvMQ=" + }, + "fbcore-2.5.0.aar.asc": { + "sha1": "50534b2792f64a385eec1c8ece8c606ea2bc3414", + "sha256": "sha256-hXnZT5xzpE6gqw88sU6MWyfDwxhGlQjMr72GD6/hVSA=" + }, + "fbcore-2.5.0.module": { + "sha1": "1648969fe720453007ba582a8f4ce29e3927cab3", + "sha256": "sha256-1UFASHJb+76PFdew4Vtwf5JlZPxi0J0oZbK2Wre9W2M=" + } + } + }, + + { + "path": "com/facebook/fresco/fbcore/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fbcore-3.1.3.pom": { + "sha1": "04e95c719d3c8fe0b794b2b69e3e521d261d2926", + "sha256": "sha256-ofBgMJobXvzZVgO43XzmSicME9PK93Pii8xJ0DyIimc=" + }, + "fbcore-3.1.3.aar": { + "sha1": "1d8b6068ba733355b691679db4014ae78d08b70a", + "sha256": "sha256-NGMNsZFm0y1PbxSKZ9KZ/6/K4C+qEgEjg7SfTbqkge4=" + }, + "fbcore-3.1.3.aar.asc": { + "sha1": "462eaea3677e152846a2f1f0fe3a2dfc592e4e0a", + "sha256": "sha256-As7DAueaEVp0Zih8NiQtYaK/0zZbUsulgDSvO724G/4=" + }, + "fbcore-3.1.3.module": { + "sha1": "49b461c4ca204ed78cb39c67a2b46b01465844e5", + "sha256": "sha256-mXXIdu8b/K9+XF3By3plg3vx9tPQ/lij2mWR6qb/FFY=" + } + } + }, + + { + "path": "com/facebook/fresco/fresco/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fresco-2.5.0.pom": { + "sha1": "b5fe959c59f1973d703cc3f23e7ee00403f6a3ad", + "sha256": "sha256-V6/2t7vB0U5hhiWzgJB/2/YkSXLZzLX2mAy2UGn/F3U=" + }, + "fresco-2.5.0.aar": { + "sha1": "b768459446d166d148aaf3b8edcdcecd59381190", + "sha256": "sha256-A9oyITby4A37cDAyXs3jMNpfSHMjy4N3pgIIhSSgXAg=" + }, + "fresco-2.5.0.aar.asc": { + "sha1": "f099dc1e41f04bf2631a7a3ba09d589ba6847fc0", + "sha256": "sha256-ZcCzVeUhpdN4DNQbDVjWpO2V7/doeb4/mCXK8OdRQZ0=" + }, + "fresco-2.5.0.module": { + "sha1": "d1d02333c99b0e161a984fbd52af12fcaf728c14", + "sha256": "sha256-gj6vO+cqzs1iXrUGYGJcgVO61Zki2KMkBE2Mf6/Zqxw=" + } + } + }, + + { + "path": "com/facebook/fresco/fresco/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fresco-3.1.3.pom": { + "sha1": "abdd0ba7d0f5a87d1a355fe62308e09f5dbf8d2d", + "sha256": "sha256-n7I2UVj6xQEHUx57AFRTTpFIV33UV1N1l2CfcV9AgWc=" + }, + "fresco-3.1.3.aar": { + "sha1": "fc0f945952cf0e6d627425ebabf322aae8df2e3f", + "sha256": "sha256-3/Hbk90ba9vW0aJboCxDtPaTmmDtFciKEfhTPMbUMPk=" + }, + "fresco-3.1.3.aar.asc": { + "sha1": "2d5e2a1f746456ae7f56d3e54d504ee70b8787c4", + "sha256": "sha256-RxdPO8Lc+OAkx6KjbAiH+GQrXJ6fxa4YrsJAnfaxq1Q=" + }, + "fresco-3.1.3.module": { + "sha1": "82f4c255dcdcc56741e5c97f5918d71c8ed42c11", + "sha256": "sha256-JU5FPqfOcIEct7bPuEy9nfzoCbTcBfO5uflp5la1vkY=" + } + } + }, + + { + "path": "com/facebook/fresco/imagepipeline-base/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "imagepipeline-base-2.5.0.pom": { + "sha1": "ab1b7014ddde427400feb40906fd9b2cf19f7732", + "sha256": "sha256-jgkYoemZGAS4JsXyFzU8nxYoxLxTSqmSiey8cgOzvys=" + }, + "imagepipeline-base-2.5.0.aar": { + "sha1": "6839693f5d3b6697cfabbf159d004dae2f465126", + "sha256": "sha256-k8bQv82OuVkokfGzKkMUqYLyJRDfRJsxcS6XI9CXte0=" + }, + "imagepipeline-base-2.5.0.aar.asc": { + "sha1": "e2e1fef04dd18fa97b1fc76b32bbfea8b5367820", + "sha256": "sha256-7aCu7q4i/iIEZ3jZunPv4lKhoePM5WWLUXidDD1gBLg=" + }, + "imagepipeline-base-2.5.0.module": { + "sha1": "b5aa00a5406ee507acdd66f21daecf28b53e0caa", + "sha256": "sha256-MVMTHhcfS6PZItvXcL/az9uGdprKR/S/lhZccoBgcEo=" + } + } + }, + + { + "path": "com/facebook/fresco/imagepipeline-base/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "imagepipeline-base-3.1.3.pom": { + "sha1": "b4714a1a610f5f78f76266aa99737738693b784c", + "sha256": "sha256-xhNxGVE7IeVzzpIAQCaZQKQBMxJEKzvlAuCaKB14eoU=" + }, + "imagepipeline-base-3.1.3.aar": { + "sha1": "2fdc3c44cd54fc7711c09e5921ea952000602459", + "sha256": "sha256-kLzoOxQzknOyOrZ2rzZbTcW9zZlMLkGiZaPCROVgWrk=" + }, + "imagepipeline-base-3.1.3.aar.asc": { + "sha1": "de6b13f2f2be26b09945f56e64cacae837db6e36", + "sha256": "sha256-LOgnWft4dfdLGytDCxWGt81ix54fpwFuMsnAqvLJkqk=" + }, + "imagepipeline-base-3.1.3.module": { + "sha1": "b970064e7b95f51ad2be0f9074183f0b51e3c4f7", + "sha256": "sha256-IdXtpYc7nVW5saAg3OHGCzZ4IhLmvnxU3Dxx7z8Kq2I=" + } + } + }, + + { + "path": "com/facebook/fresco/imagepipeline-native/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "imagepipeline-native-2.5.0.pom": { + "sha1": "d445704694201141671afef229178f074fab7b99", + "sha256": "sha256-Yr6jCTV6aEb4di1KuSYzEQJX6jnVu3xbNsn03Khs8K0=" + }, + "imagepipeline-native-2.5.0.aar": { + "sha1": "f8e65e897a883ac4f754d57897aaba0016c8beba", + "sha256": "sha256-RUCXZyUA3eQDwI00WtlrMBGsdV+4D603L6K46Cqz7+o=" + }, + "imagepipeline-native-2.5.0.aar.asc": { + "sha1": "ac2af46dc4981e88c8b6c49dee136421958687b3", + "sha256": "sha256-s6LPqpGCA+lqbs9QFuv1dhUXZSxq3sZinDblLFAmDgY=" + }, + "imagepipeline-native-2.5.0.module": { + "sha1": "64fe3eb05427d5b82c186d43eadb516c758cddf0", + "sha256": "sha256-2GCD7lgeVidnjthVVeZvnfMFZ4qEMSl8clcn3bsO8Ow=" + } + } + }, + + { + "path": "com/facebook/fresco/imagepipeline-native/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "imagepipeline-native-3.1.3.pom": { + "sha1": "a9173914e9d15592b807742a27a0bcaf2a56cc40", + "sha256": "sha256-h4KKoKF9f4ra4RrKj5CJ12Kd6CHdQD3ImJvdPlho0/4=" + }, + "imagepipeline-native-3.1.3.aar": { + "sha1": "19026f410da3392d60643644390563cf1ab34556", + "sha256": "sha256-ozgLb+sZp49T+P0qJbgGx/NilsUHW7ghKXLyXrq/1oo=" + }, + "imagepipeline-native-3.1.3.aar.asc": { + "sha1": "9ec1f812cd94341925acc7935b91aff706dae357", + "sha256": "sha256-o0h4lsIjyIGWj3l43NswWxb8oMIhzidMTX59vecNvxc=" + }, + "imagepipeline-native-3.1.3.module": { + "sha1": "44375907ce6bb60eacbe16a444ee3c4fa01a2351", + "sha256": "sha256-PvBOCnsCa8+rGVccA7vQ1Jtk5vjQkQhR6/Kj1DwVqzE=" + } + } + }, + + { + "path": "com/facebook/fresco/imagepipeline-okhttp3/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "imagepipeline-okhttp3-3.1.3.pom": { + "sha1": "15bdf9aa20d8b766389f60aee869396f9cc47384", + "sha256": "sha256-EyVQ7xzuhCAkfQ8CK2abWWe6kPuqVqu6YYYxa9GNJBY=" + }, + "imagepipeline-okhttp3-3.1.3.aar": { + "sha1": "455b433027f246a46cac0b9832c0bc9d513b804a", + "sha256": "sha256-HNh6za9wvX/BJglrSKw74V8/sSof3A15R35J16YZCJo=" + }, + "imagepipeline-okhttp3-3.1.3.aar.asc": { + "sha1": "b3f9a94cf7f55e567f7e9b933cc2e832f434993c", + "sha256": "sha256-wgPBxUh+ispCwGu3c66AYoQroaS06bePDafrgb1tc9E=" + }, + "imagepipeline-okhttp3-3.1.3.module": { + "sha1": "ee5c8b5535cb1f07d7e5026151fb1e61ebb34678", + "sha256": "sha256-SI3Db7ZteVN5xve1hVzX1gcc0jLdGEX/R09PQCA274g=" + } + } + }, + + { + "path": "com/facebook/fresco/imagepipeline/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "imagepipeline-2.5.0.pom": { + "sha1": "4deb58ada67afd4d7015e18d214716cb1b5382b6", + "sha256": "sha256-OIPZcMbaDfNe93nv/4sdc3XpvpKnf3+TrPwzezURZ+k=" + }, + "imagepipeline-2.5.0.aar": { + "sha1": "dd62dd1607ade4532c1240f137ac3c2d7920c134", + "sha256": "sha256-otcJLJvCFQFZznvnl+32qsCmkTzlq4sCSSFv/Ua3Uyc=" + }, + "imagepipeline-2.5.0.aar.asc": { + "sha1": "07aa62ce2d43b01098e44e4dc95ac82941cd8046", + "sha256": "sha256-wUHvsbzRKcjEHWXl36hvQTHT2oltTSqWp7wwaF9Pr4c=" + }, + "imagepipeline-2.5.0.module": { + "sha1": "c1e42d68e91a166b4a3967dafde284b3964d7944", + "sha256": "sha256-zzxMlf0K4+JglUCl1D3TlDBTCOfG9ELqwviOMuvtDvE=" + } + } + }, + + { + "path": "com/facebook/fresco/imagepipeline/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "imagepipeline-3.1.3.pom": { + "sha1": "e3cf5b6d93f268a9da04a4a03a1fb407f9e31101", + "sha256": "sha256-PPWMy0IuZkBjB4l+7ga7EBTyEl/PHJCcTNFLhzcQUMo=" + }, + "imagepipeline-3.1.3.aar": { + "sha1": "0a68c241ae4078de1b8b46f24dc0f11b0a5a7b59", + "sha256": "sha256-a4++cdrKxcGhkVnpbGkgKThu4b6prNCDUxiZGgOcn+M=" + }, + "imagepipeline-3.1.3.aar.asc": { + "sha1": "9dc83c97306ad902f906cc832901613c736910f7", + "sha256": "sha256-Csha10eAIKWEcRnLO/N+BakFCWmr04k3FHhio6UDTic=" + }, + "imagepipeline-3.1.3.module": { + "sha1": "dc47f5cb4c964dec73c95e5c796d0fbfd14a0c34", + "sha256": "sha256-n0IBgFkbTOCHc6oFpkZjaJubSqxWWuCVzvIRLUpNbPk=" + } + } + }, + + { + "path": "com/facebook/fresco/memory-type-ashmem/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "memory-type-ashmem-2.5.0.pom": { + "sha1": "618d681fec1ea5c82efc23c27827f302f012e78d", + "sha256": "sha256-PVoGpYzaMdWVXToiCn7vOvjwuqkWjP2sN5m8wS6Ox6U=" + }, + "memory-type-ashmem-2.5.0.aar": { + "sha1": "b43b53aca89569fd2e8e964b71dc7afbf62204e8", + "sha256": "sha256-7YDEIBjE/TGSGLwjluOLJzN7VBxFwe/qq862wqzh9+g=" + }, + "memory-type-ashmem-2.5.0.aar.asc": { + "sha1": "51238df1bc2941cb6a1de43db1dd4509e76a0b9b", + "sha256": "sha256-/fHWL65w4gmQgi685NRCyt3rbjCxKaLzZSP/f02rW5Q=" + }, + "memory-type-ashmem-2.5.0.module": { + "sha1": "e59fa447fef1fae959fa8c0d49a5eeb9656b7e0b", + "sha256": "sha256-40ibhbtUQk1gUI2mPRyJeol5xwWxZSSyeiRkS1BzRHg=" + } + } + }, + + { + "path": "com/facebook/fresco/memory-type-ashmem/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "memory-type-ashmem-3.1.3.pom": { + "sha1": "69fb9c1b8d5c13a2d0802dc4faac411151581c57", + "sha256": "sha256-ycH+9SKBj9jcKyPZA1QGGed1xta/yBD7JUU0d8NQgBo=" + }, + "memory-type-ashmem-3.1.3.aar": { + "sha1": "c3f85c564fddaa7a77ffcae2403bf913340a0aa6", + "sha256": "sha256-UTO3Oq0OML+y/2vPZTVnbwymW6e98n8gbk09OUmcaF8=" + }, + "memory-type-ashmem-3.1.3.aar.asc": { + "sha1": "1ee0a848d18e9a83894f19910a31a04c9440603f", + "sha256": "sha256-m1RDohP56EG9O5aZxfKTlKqLUjAUMT4uRhQOTv0BNR4=" + }, + "memory-type-ashmem-3.1.3.module": { + "sha1": "a14209b1e10c9db60cbd49523841b2a1363e9cfe", + "sha256": "sha256-jqUNWjlUdPzhy5qAMDLyp37tJ1s2u53e3VffrGlp5kA=" + } + } + }, + + { + "path": "com/facebook/fresco/memory-type-java/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "memory-type-java-2.5.0.pom": { + "sha1": "804fc0d5c758d795ced5a9baf5f6194c0308d71e", + "sha256": "sha256-5w/t1eiC4vh1gc6OqokHWw+sOyvodmtcUWofa/EqMG8=" + }, + "memory-type-java-2.5.0.aar": { + "sha1": "57537a8b69dbad44fafdaea3067a776e11586465", + "sha256": "sha256-iwxpMqeLFfdbjxTgNdg/+GIbhP57MyRaTTV8GqWrr5A=" + }, + "memory-type-java-2.5.0.aar.asc": { + "sha1": "614ba8279882d1b06c6246845f19524d945df8f6", + "sha256": "sha256-P795lf0poAmVyZpxB4AhD4JstDZJjaSZio/h1Ru016Y=" + }, + "memory-type-java-2.5.0.module": { + "sha1": "ccc48102961687eb03e2bffcd37c642bf45b50f4", + "sha256": "sha256-LAVHnDt0ujuE1fgDJp5cB5RJQYBoP4YKGYqx2TLyNms=" + } + } + }, + + { + "path": "com/facebook/fresco/memory-type-java/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "memory-type-java-3.1.3.pom": { + "sha1": "657ae19c78782cab55d3e3b9e1bfef49d4cf26f0", + "sha256": "sha256-R94o44fxtSxUMD+EeXbeVY9kKVAIm0loZ/3ZMvkP118=" + }, + "memory-type-java-3.1.3.aar": { + "sha1": "456c580809f8a70de844217d5a3ed14f70033d32", + "sha256": "sha256-je7i52XY4j33nRDAIG+k0jF5XlzkmgFRwOwHA3SzpPk=" + }, + "memory-type-java-3.1.3.aar.asc": { + "sha1": "09048f7efea878c358531ebae078c4b09db594fa", + "sha256": "sha256-62Y5JSEx7bEgJ/x5XEkgQKL9t20zPBtZDHcGYthrSQc=" + }, + "memory-type-java-3.1.3.module": { + "sha1": "72b4aefff3ddea3f52240d6188d47c082687ed25", + "sha256": "sha256-FdHKlyKoh2hXpqHe0kV2Z5WG0Puy4HVPY4z0aLjQ/WI=" + } + } + }, + + { + "path": "com/facebook/fresco/memory-type-native/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "memory-type-native-2.5.0.pom": { + "sha1": "e9a4eb5cddd8be98120e4358818672cbf941aeee", + "sha256": "sha256-TFIN8d0UaDL0lwiS2ETABkh2HGt6ECEvud7O5VAzIIg=" + }, + "memory-type-native-2.5.0.aar": { + "sha1": "7eb7c52ee2a4d40b7f706c90069549410236aaa5", + "sha256": "sha256-q1iHvIGyAjUgSno7BgYw1Opmjvb/q/Hqp5nHarPKKd0=" + }, + "memory-type-native-2.5.0.aar.asc": { + "sha1": "8b8627588c16a7dbdcfe41518b461017b9e58fd6", + "sha256": "sha256-K1QT1peRJacB2MqujgmMRD7lgec3hyo/7o94kRsXeXg=" + }, + "memory-type-native-2.5.0.module": { + "sha1": "fd80f3c6294741b83963f98e7c780ecb8bce075c", + "sha256": "sha256-afxdx+pyy03GkFvAu0Boy6YKsYUIIzAL+4QKMskJFQM=" + } + } + }, + + { + "path": "com/facebook/fresco/memory-type-native/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "memory-type-native-3.1.3.pom": { + "sha1": "1ef54b0de4d3359833588ee39ed88ef51196e848", + "sha256": "sha256-A1pDKnjHUo06ZSQykzLeGVBue9VHZCkQaMhsewW+uC0=" + }, + "memory-type-native-3.1.3.aar": { + "sha1": "7db16f971dbbcdbd0c337d88e376d0b09fb699ef", + "sha256": "sha256-x6O5GWAULEYg4jDcGnkJFWhaU7Hf8XMV86stFPB0J3w=" + }, + "memory-type-native-3.1.3.aar.asc": { + "sha1": "7603b9e87255b73a6616da233fc1ff4a59cf3a94", + "sha256": "sha256-C5V+EjNHDYQbmPQ+KqyQlJLiYcFeQ56y/lxqOZ4aHcM=" + }, + "memory-type-native-3.1.3.module": { + "sha1": "ccb742cdf2aad6c9308298cf9c3adf4524af9b29", + "sha256": "sha256-7BKqMaeYFB7ez81suupJ8PsFaDime2gm38FcT+5Rr4k=" + } + } + }, + + { + "path": "com/facebook/fresco/middleware/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "middleware-2.5.0.pom": { + "sha1": "d92046020539d1c4a2e658b41d19cf32b418a33b", + "sha256": "sha256-X5+rHeAlsWQiYzjbA2BC5q/NHCOO3dK+vKl0pXciL8M=" + }, + "middleware-2.5.0.aar": { + "sha1": "06666d80bd9f242a0a532f895ef56511a2439c26", + "sha256": "sha256-BJE5mh81FlOHUhelAOOx4zVuqXsHm9Ej5YoscMW0ShQ=" + }, + "middleware-2.5.0.aar.asc": { + "sha1": "163216907236746c719a6090c25134747c8eb65c", + "sha256": "sha256-t9sa7x/kQxiPFMgllpFWMD74Fb5MzNJGJdqn4wxs8Gc=" + }, + "middleware-2.5.0.module": { + "sha1": "caf537286d6bbbf6dd82ad0dfeacc5e45da77421", + "sha256": "sha256-1l5GYQupCpxGibySKOxkr/BOmL4zOqtIZeLTQMX5CHk=" + } + } + }, + + { + "path": "com/facebook/fresco/middleware/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "middleware-3.1.3.pom": { + "sha1": "e1b051480c9a912070813a8c2527b50a7e873cca", + "sha256": "sha256-pdYWdFTVsPX2fj/wiowWDyG6TmbLUQQDR3dqAe9aQoQ=" + }, + "middleware-3.1.3.aar": { + "sha1": "80650d4b0a58f9f156e74a9d942a87ee64c107b3", + "sha256": "sha256-amcSnZLnyzpVvtehmaWKX5Yt06eYkSJeVbpKg+RfD+c=" + }, + "middleware-3.1.3.aar.asc": { + "sha1": "f59c25a9f6a90e003e97b79a1a553ca9af048647", + "sha256": "sha256-Vo4MpL3NMaumkXL41jm1ZvJxnZZksagsAR4exdcu0kk=" + }, + "middleware-3.1.3.module": { + "sha1": "cbd733257abdfc65547e68ea7e59fa60e83ff425", + "sha256": "sha256-6OQ5vjB9oS0rSF6z7qaBdksj0QDPzDGN/oqQP4653Fs=" + } + } + }, + + { + "path": "com/facebook/fresco/nativeimagefilters/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "nativeimagefilters-2.5.0.pom": { + "sha1": "569b8566e8f2d003e2a3f45ca303a85ab4467244", + "sha256": "sha256-bTiWNq18DiHqYp8CtkjZTOeObeUUjpdQdNixhdxsWek=" + }, + "nativeimagefilters-2.5.0.aar": { + "sha1": "4a30438df8d960000b37b1f8cdba9d7996d57ea9", + "sha256": "sha256-gMjMDP2ZoPJuc30Jx6m6bQxJzcAL4tz2rWatF0u8M/w=" + }, + "nativeimagefilters-2.5.0.aar.asc": { + "sha1": "49ce6d54a568ce88de94615433704fe59cbb88a3", + "sha256": "sha256-9JR2vkDv1MR28fxbqb4dL4tpEQJ6iRNy+VQhaNvaMfA=" + }, + "nativeimagefilters-2.5.0.module": { + "sha1": "d4c34604021f16b07667e371481d426b04a5a354", + "sha256": "sha256-ko+1/s604FyBDiBloC6W46MMtrwj7PvomCZ4HneTIC4=" + } + } + }, + + { + "path": "com/facebook/fresco/nativeimagefilters/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "nativeimagefilters-3.1.3.pom": { + "sha1": "982e44873bee9f8f1cf5b55ffa860532281b6f12", + "sha256": "sha256-V3DobsTLyoC7ujv4Uue/+7+Xj1NtbPcib1D4BYFZ3Yg=" + }, + "nativeimagefilters-3.1.3.aar": { + "sha1": "2f48c3f7a1575d8aad19732890de10a25154321a", + "sha256": "sha256-RPmyhbpckTZgpVTR640vCZJoj1B49X1382zGIiazAWM=" + }, + "nativeimagefilters-3.1.3.aar.asc": { + "sha1": "13c9a1539dfdddc54944158d87a78556d0ae4df0", + "sha256": "sha256-lddZ97VHprcgyi0db2mwCxmq3TohiUHWYHNMffmkWqM=" + }, + "nativeimagefilters-3.1.3.module": { + "sha1": "3840c0e30f4a648006c83c8bdec366f4b63d8ecf", + "sha256": "sha256-zpDM5S5JTsZKeX1XjTYzic4S1g7yWS7wZuPsoSpBYz0=" + } + } + }, + + { + "path": "com/facebook/fresco/nativeimagetranscoder/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "nativeimagetranscoder-2.5.0.pom": { + "sha1": "f9ff1669e903b745f17de838d541920ffadfafc3", + "sha256": "sha256-eWY8uILPG/X7PCiYcsUmI7Q5aS8CObdPsZF/lAf6O0k=" + }, + "nativeimagetranscoder-2.5.0.aar": { + "sha1": "7db353c68b41e2ad502a959c554387d59e75eb75", + "sha256": "sha256-tRlI7Cquc6vqN83+hUYf8sSz0lyiKxOuilYrgybEJVI=" + }, + "nativeimagetranscoder-2.5.0.aar.asc": { + "sha1": "d6d9c01e5a7daa063cb649d93164b81bc7680805", + "sha256": "sha256-epgxwzcVZvPjqT6A4Fapn+8P1zLyjXJRQtAEcjXFM/s=" + }, + "nativeimagetranscoder-2.5.0.module": { + "sha1": "a0469bf07b4ab0904340968db6f04d4abf5ae638", + "sha256": "sha256-/5aaezl3yAQVE06MmSJb7JAuqHbWSNQFzWjfNkHRINs=" + } + } + }, + + { + "path": "com/facebook/fresco/nativeimagetranscoder/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "nativeimagetranscoder-3.1.3.pom": { + "sha1": "d329e7d9ba9d968185aabb5ed2ecb578904eba00", + "sha256": "sha256-3IUCIs/kAl+FmudaLdhowda+9wVGXxT0j1dpmWzlM2E=" + }, + "nativeimagetranscoder-3.1.3.aar": { + "sha1": "7bebc51666c6fc79d52299102486a40451faf972", + "sha256": "sha256-hKE7qh1Cm9RWFfM19pEp3JNdJWJDrjUdelNOuw/we34=" + }, + "nativeimagetranscoder-3.1.3.aar.asc": { + "sha1": "60640867daa1fa7c6fe9ca212ed6e63cb8ddb04d", + "sha256": "sha256-RWVjjbkB1YySDYTBnGaJU+UL71P+bKFDAJ7WKnBJ49k=" + }, + "nativeimagetranscoder-3.1.3.module": { + "sha1": "49e148ced5beab46c6eb05a7a7493310656210fe", + "sha256": "sha256-48RAOPU5JAwhafLvhd/eUPmKLylPaUjHfRGdGHD/YWM=" + } + } + }, + + { + "path": "com/facebook/fresco/soloader/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "soloader-2.5.0.pom": { + "sha1": "e8849cf5020591cc0bb3196e6a7b399910d09e91", + "sha256": "sha256-tTNJi68BSLRYxu5uLlgpI9lblcgHEU6Y09y7mFp/yGM=" + }, + "soloader-2.5.0.aar": { + "sha1": "8829543f2720836579d4f0f944a90b57720e2792", + "sha256": "sha256-ol//x1Q375w2viSN8EsTceSwEin5RAFPdWxeGCxRmoI=" + }, + "soloader-2.5.0.aar.asc": { + "sha1": "d7f3c0a0406b77304b3255496685ae59f0ea92d8", + "sha256": "sha256-V0PiwPHmI1XplfTWyEz4JcE5Ig8LeJ57PERfazLBV9c=" + }, + "soloader-2.5.0.module": { + "sha1": "e0544005851d1416a4ecacd2936a88a4fa4ef281", + "sha256": "sha256-yOYNxdttBv3RtFO6GizpYzZQ3D6VkX9exyx+ymSvto0=" + } + } + }, + + { + "path": "com/facebook/fresco/soloader/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "soloader-3.1.3.pom": { + "sha1": "b47f3d16009aee02cbba5dd18860900ac6f0050a", + "sha256": "sha256-OwfHYoRWLOKTpF+b+T+aKdKwQUUql9dDgpnOnTSsPsI=" + }, + "soloader-3.1.3.aar": { + "sha1": "a5965b164f6dedd337df2fb86ed395d059cf4e3f", + "sha256": "sha256-Tk4erPqE10H5siKld1VvzcsNpf04TB/DpB+EkQtdzSA=" + }, + "soloader-3.1.3.aar.asc": { + "sha1": "5dbba384a832bd37337d6f09f3818c37941dbbe5", + "sha256": "sha256-NnLUZ85MLZO4r0z3U0Vk5CpJfNZ63CO/eOSPZ//8LfE=" + }, + "soloader-3.1.3.module": { + "sha1": "b6128ff9c77daab3ad4c770c6673476e78534ac1", + "sha256": "sha256-9owXzwWdA7uyCbBYubWLj0K0p7MN4EPUYel+pofCMjw=" + } + } + }, + + { + "path": "com/facebook/fresco/ui-common/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ui-common-2.5.0.pom": { + "sha1": "56d316bbf5d15721aa54ac491818b957f2f42a5e", + "sha256": "sha256-hMxuteBGTeN9KP1dQ3c2wFVszGujJ7nTWPDrVydvJ5U=" + }, + "ui-common-2.5.0.aar": { + "sha1": "224851cf4c074aeff5dc42861b557870459a9d28", + "sha256": "sha256-Ttui1aT724Yv2J0hDmQGA9Ss4fw106DYOfFue/ClIi0=" + }, + "ui-common-2.5.0.aar.asc": { + "sha1": "387159642149a6c278d5fa86afa516fbd45aebb5", + "sha256": "sha256-BO6AFwiaIHQ16A9QKCiN4Ng05NhUJYh2ZAAVORhmjVc=" + }, + "ui-common-2.5.0.module": { + "sha1": "78525bc37ba6e4a053d39a93a9ad2e25b9288116", + "sha256": "sha256-HKf7CaiJzHxZJ5T+pXv721bmQBF+gOgpvewbMgv7xdE=" + } + } + }, + + { + "path": "com/facebook/fresco/ui-common/3.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ui-common-3.1.3.pom": { + "sha1": "342dbde3bee451a2295a3ae647cf4eb8df06d184", + "sha256": "sha256-Bi5zbL9bT7ooP9SKvnQUVB0WsMBKrg0SP7pFOZPoGFQ=" + }, + "ui-common-3.1.3.aar": { + "sha1": "3c4cff99fc8a3f9f167e4d598c6be08b5a992251", + "sha256": "sha256-UThdMDfkadhuZkIN7C2eA+T2sQgkDjsyQ0WvTWoiuyM=" + }, + "ui-common-3.1.3.aar.asc": { + "sha1": "60f4e108f6d9233fc3937f2a44671ef5863ba170", + "sha256": "sha256-Jr91t5rpJ1CyOTAlQUn1FHMrfBRA+gusItVD0Cokc30=" + }, + "ui-common-3.1.3.module": { + "sha1": "f3932e9dbd1cd2aa51cb0874057c425883082648", + "sha256": "sha256-priHJdiMJyha20vrdroF5Ife5TEbQwGePJ6zW2Qz65Q=" + } + } + }, + + { + "path": "com/facebook/infer/annotation/infer-annotation/0.18.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "infer-annotation-0.18.0.pom": { + "sha1": "9bfab08829c9e56f3af6403da5abb2f220fa1a85", + "sha256": "sha256-1Enr0EiaGgDQPk/HgrU0WgBi9iUZ4fHda9ti1xDjRb0=" + }, + "infer-annotation-0.18.0.jar": { + "sha1": "27539793fe93ed7d92b6376281c16cda8278ab2f", + "sha256": "sha256-yu+6k1ZD3JYnWKdg8WJ6QCIVrwNNlsD0hNY7f93KmS8=" + } + } + }, + + { + "path": "com/facebook/react/hermes-android/0.73.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "hermes-android-0.73.5.pom": { + "sha1": "955db2fa2ae0372c8a8a0b6842e7f0cab1cc6221", + "sha256": "sha256-9B2asyhFoQ0R4Vf1vLu7dPLb31++xnjYNhgd0j+jORg=" + }, + "hermes-android-0.73.5-debug.aar": { + "sha1": "6f643357f12dea27120e33a3b13f05e41da24bf0", + "sha256": "sha256-K29yemB5WljvOu9WxYkO8g9wukrVaFYlVWoy7t57g0k=" + }, + "hermes-android-0.73.5-debug.aar.asc": { + "sha1": "65ab5673bc871ab015095284e5c6c42104a422fc", + "sha256": "sha256-XbxZVSr7mZ396VCvC9viJE6en9LllCfR2wWRDKqv2NE=" + }, + "hermes-android-0.73.5-release.aar": { + "sha1": "866acdef2fd2e78e27eba2de672252e39e4a2a36", + "sha256": "sha256-PLR2N9gsEcB82NvjbZN9jRSNk2NWO704Npan9x6TS7w=" + }, + "hermes-android-0.73.5-release.aar.asc": { + "sha1": "ca9e27e0b490e323ac7e82882da3ee0cd27a479b", + "sha256": "sha256-qn7gIMuvHXCRwzLkXuc+zuvMvTUh577P10okSYvMkA8=" + }, + "hermes-android-0.73.5.module": { + "sha1": "cff268fe33444a1facce09f138d2b82c9c8fe0cd", + "sha256": "sha256-8sBAOyQYkzeMA73Scg7/NZtOz3t0Pn+PBeWpCXecM+s=" + } + } + }, + + { + "path": "com/facebook/react/react-android/0.73.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "react-android-0.73.5.pom": { + "sha1": "db442050834390cb54cd8162d4caf4924ee407ac", + "sha256": "sha256-OJOXiLrb/zhqyxdUtAPByZIHKHkyXPliWNyB7pDaZdI=" + }, + "react-android-0.73.5-debug.aar": { + "sha1": "3b66d3a3de9db61b22a1ecaf7c6dd9e1864d887e", + "sha256": "sha256-A6/jBejJVxBnCk6ESbjRH5GsmVPiwKik18orxGUFGX4=" + }, + "react-android-0.73.5-debug.aar.asc": { + "sha1": "bd11129f0ef7cadfe4dcf68b2a6e4112c9d2f6a5", + "sha256": "sha256-H99ZwfTDMh+sIjhynGPtwg192wYDAHo54i99KaXD5Kc=" + }, + "react-android-0.73.5-release.aar": { + "sha1": "de12b19fbd9fc3c3da9ae2bbffe7e9c3b5d36023", + "sha256": "sha256-pLPp0ep+enzCEgAZwPF8S7+PsdhGECwMYrrdEfoaSRA=" + }, + "react-android-0.73.5-release.aar.asc": { + "sha1": "1233cb3f6613bbd4debf387c3000b767719561a2", + "sha256": "sha256-7ZIpbOFHWO2EhS2+C3KjESJyz2UlHYi5cnmVH6/lRh0=" + }, + "react-android-0.73.5.module": { + "sha1": "9805ec3c968f6954da72baeae08806fdf2ab5677", + "sha256": "sha256-7BSpyf6KD3Zerv94krLDW8QIF0QQV6GtHNkuzdIZa5U=" + } + } + }, + + { + "path": "com/facebook/soloader/annotation/0.10.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotation-0.10.1.pom": { + "sha1": "f89dde641bdc595800fb880f9509ab58d26d98c2", + "sha256": "sha256-EPxoZEzDXNvgt8SOL1UYYqyFdJA6eyybxpo1ifcmk+U=" + }, + "annotation-0.10.1.jar": { + "sha1": "dc58463712cb3e5f03d8ee5ac9743b9ced9afa77", + "sha256": "sha256-rQyHrvcCm2TJ9H/QkEsUOy1dBfSUU7L4AaI1GUOx1Os=" + } + } + }, + + { + "path": "com/facebook/soloader/annotation/0.10.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotation-0.10.5.pom": { + "sha1": "0fab507032364f29287386399dfec67a39185516", + "sha256": "sha256-w1Tl81JCV7V1ZV2B9JAFU5CAmLLC3OLHrU32K4oUZgU=" + }, + "annotation-0.10.5.jar": { + "sha1": "dc58463712cb3e5f03d8ee5ac9743b9ced9afa77", + "sha256": "sha256-rQyHrvcCm2TJ9H/QkEsUOy1dBfSUU7L4AaI1GUOx1Os=" + } + } + }, + + { + "path": "com/facebook/soloader/nativeloader/0.10.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "nativeloader-0.10.1.pom": { + "sha1": "266cb4a9a8978a44c6c7b828e7adbbfde427d995", + "sha256": "sha256-fSqocBv/udASksSTuHhM613ham7YroBk72uROmJoX6o=" + }, + "nativeloader-0.10.1.jar": { + "sha1": "ba1b31b4b9f65494a90de7c2728b57155344a858", + "sha256": "sha256-gsqcNI9gWYT6cuCK7gw156oQPsaw7M9ZLkmDksTWAbg=" + } + } + }, + + { + "path": "com/facebook/soloader/nativeloader/0.10.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "nativeloader-0.10.5.pom": { + "sha1": "8075528e55fee32f7d1072b9829a0dcad42549c1", + "sha256": "sha256-iUaslfY8FPS6AsFX6cnQYFqeO6XLQqBxA6qltAFCCVo=" + }, + "nativeloader-0.10.5.jar": { + "sha1": "492cc5082540e19b29328f2f56c53255cb6e7cc6", + "sha256": "sha256-lx3thgCcCsVvsideEo+FLn3LxBpAzXmWiPihrr3lGV0=" + } + } + }, + + { + "path": "com/facebook/soloader/soloader/0.10.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "soloader-0.10.1.pom": { + "sha1": "ff4e5ff3fd52ad178d95a9c16ba4e33c2f32f075", + "sha256": "sha256-pE2ueH4eqPlCsKGPHrSGqVbz7CbuhiBdS0MBET6acLg=" + }, + "soloader-0.10.1.aar": { + "sha1": "78e5537c0cdf7c190f6cad9a2490f9f84ee8f041", + "sha256": "sha256-DgqC+FAvgaCgf5HBLwfsVFIuXqSFEmBMaiSfVfNJCVc=" + }, + "soloader-0.10.1.aar.asc": { + "sha1": "e48052202209158045d62f91be043ca4b4ca7273", + "sha256": "sha256-YkXSMhVMH8bwLGKHyIqOsgIl6984q540WlXUrO2aMPg=" + } + } + }, + + { + "path": "com/facebook/soloader/soloader/0.10.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "soloader-0.10.5.pom": { + "sha1": "aac7f28093b38755ff727b05cea490646cea7777", + "sha256": "sha256-GFoUITryiFKQlu6TBUPQF0H29J/Cg1w5sy+M6Z3Pzrg=" + }, + "soloader-0.10.5.aar": { + "sha1": "56c5199825ebfe8638af6912fb00fa2f120846e1", + "sha256": "sha256-rB1tl+UsP+QP9t/3uKuO18tYEis/Iyvq4AWMv2oh6c4=" + }, + "soloader-0.10.5.aar.asc": { + "sha1": "596eded364a07dcd319ae928b18fd6d205f36825", + "sha256": "sha256-LVdut4LOKFG0H7BHcxc2qxH9xImbu0oJV73TGKLbd6w=" + } + } + }, + + { + "path": "com/facebook/yoga/proguard-annotations/1.19.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-annotations-1.19.0.pom": { + "sha1": "d43f43c470ae9e21d21a6e53a99ef04c8786128a", + "sha256": "sha256-m6+HeH6sVK7jheN0wtaIpFGQSTr6XaZFkgL1qyOrSWY=" + }, + "proguard-annotations-1.19.0.jar": { + "sha1": "fcbbb39052e6490eaaf6a6959c49c3a4fbe87c63", + "sha256": "sha256-+2X3Bu6gW1ZA+2uT0h1sQfnitOem5tnyjOnSpXRHSZU=" + }, + "proguard-annotations-1.19.0.module": { + "sha1": "53bd20373bc22c340ecc6b4faa501c7a9b9a0083", + "sha256": "sha256-FLjkmKMQAVRZ0C8sfFnCDPzmLCkNAmjr6aGSNOmgOPI=" + } + } + }, + + { + "path": "com/fasterxml/jackson/core/jackson-annotations/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-annotations-2.11.1.pom": { + "sha1": "b746e11b97ca52619ade1effdb75a786f495f4f7", + "sha256": "sha256-aHfVzp6yEzMJz57lqxkX97vpMvJBavwQsW9mF3Z7aqc=" + }, + "jackson-annotations-2.11.1.jar": { + "sha1": "f083c4ac0fb8b3c6b8d5b62cd54122228ef62cee", + "sha256": "sha256-BF5hsLSjR9Erhs8tDndt6mp5IErhIJXCsIkG0p5ZSlo=" + } + } + }, + + { + "path": "com/fasterxml/jackson/core/jackson-annotations/3.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-annotations-3.0-rc2.pom": { + "sha1": "0fd09eefce48f4d11a897a1d9a6c9ef20f652630", + "sha256": "sha256-VHcuTXWW1j4jZfdC49tNLnfiCgLWR5XR0YdveBFYi48=" + }, + "jackson-annotations-3.0-rc2.jar": { + "sha1": "16a509fe9aab5b995fd45ee107ce989451c332b5", + "sha256": "sha256-hIZWpc+18OnnxUanm5I1iHZqmYOXe9Ff1lUI1mfLvLo=" + }, + "jackson-annotations-3.0-rc2.module": { + "sha1": "fcc5d85363ed063b4388e65f9bcaf4d69cde1e85", + "sha256": "sha256-T6JYbdUHNwvXGprbpmb2mo9dsnoT0vNM1viez5eFm4g=" + } + } + }, + + { + "path": "com/fasterxml/jackson/core/jackson-core/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-core-2.11.1.pom": { + "sha1": "0a0d8a4a161004cf6d19eaeef9c95c7682da6152", + "sha256": "sha256-Ffe5Awsewg1KPCRDF6IOedidUtAt2Zmye8Z8WhBfCoI=" + }, + "jackson-core-2.11.1.jar": { + "sha1": "8b02908d53183fdf9758e7e20f2fdee87613a962", + "sha256": "sha256-JUYdv7Qxy0jjARigJ3Zx3o6KVqOxGYSS4ksvtgXXV14=" + } + } + }, + + { + "path": "com/fasterxml/jackson/core/jackson-core/2.19.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-core-2.19.0-rc2.pom": { + "sha1": "7df6ff9be9701055175612f576ba758dea968452", + "sha256": "sha256-Sk7QX8U5V2SP+0+3aMbyvh1P9bT83oeORbcsGE5Avwg=" + }, + "jackson-core-2.19.0-rc2.jar": { + "sha1": "ea7c49f2dda2d93b7ec157e5d287db2728b85a32", + "sha256": "sha256-vjKXb6jnayawqhZFXB/Mbd8HPZfXnQWConAbTdwNZUA=" + }, + "jackson-core-2.19.0-rc2.module": { + "sha1": "eb0e67eae2561c8ac36285a9db4f63c7b50aac9e", + "sha256": "sha256-LshfzEk7bm4vAEiqPDDVp0G6svfp0atmoCMJeKjrcRo=" + } + } + }, + + { + "path": "com/fasterxml/jackson/core/jackson-databind/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-databind-2.11.1.pom": { + "sha1": "51a50898a694504b98853f909f068d4aa17dc782", + "sha256": "sha256-R/cX2AHkV3EH3fYRcu11h5Auus5SSOBzN7kLr4qrJAA=" + }, + "jackson-databind-2.11.1.jar": { + "sha1": "f5d24a1dcf46000316d40c8c61196c48dd5677c5", + "sha256": "sha256-tIa+Odn/Sui8d4J5Ly6nDzp72u5b0TeA0pXXZ0Ixr9Y=" + } + } + }, + + { + "path": "com/fasterxml/jackson/core/jackson-databind/2.19.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-databind-2.19.0-rc2.pom": { + "sha1": "9f2fd01ecaeb85faa5cc7b4a3987f270aa03b6ee", + "sha256": "sha256-Bj5/lHFl6sL2G7I0SN8xnKCWwnx3ELQwEMw6bzxqiQo=" + }, + "jackson-databind-2.19.0-rc2.jar": { + "sha1": "52d9ffc5973607f1833d078332e71e0d46b00415", + "sha256": "sha256-EQ/nyV7ZIZEb1IOlMJVBoPNWf7yr4QbgtogqwEHfA3s=" + }, + "jackson-databind-2.19.0-rc2.module": { + "sha1": "3e27dc800e9fff544d73f8f8483a400113ffbcff", + "sha256": "sha256-l0bGX6ZZJE2CfW3batb6RD4oDMOKrNEJwAmauh9m7V8=" + } + } + }, + + { + "path": "com/fasterxml/jackson/dataformat/jackson-dataformat-xml/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-dataformat-xml-2.11.1.pom": { + "sha1": "53ea9b6fe5c842557e0f671f5ad735e0fbd12af2", + "sha256": "sha256-iDCMyi/T0BymUj9UFNlfx+quaQPo55GSigz1tw8wbb8=" + }, + "jackson-dataformat-xml-2.11.1.jar": { + "sha1": "656cda13be9698863e77064d33689cfc6a71b380", + "sha256": "sha256-t2dMT8pfB8f72PCrkGYmqQmqDXxj5DIanSFda0MwV2s=" + } + } + }, + + { + "path": "com/fasterxml/jackson/jackson-base/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-base-2.11.1.pom": { + "sha1": "3f035aea01d74bae7a0bdb90064095454eca3730", + "sha256": "sha256-4FLlRTuEmXidZdXaimrBmRRJ+sJ0CugBueUqJn1PBMw=" + } + } + }, + + { + "path": "com/fasterxml/jackson/jackson-base/2.19.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-base-2.19.0-rc2.pom": { + "sha1": "5b397720ce1330673208c3a0dabbaed5acf07700", + "sha256": "sha256-XreQnxmG5dTxBE1FdZ3jP36iAVsEgzEyIfWgisCeOOY=" + } + } + }, + + { + "path": "com/fasterxml/jackson/jackson-bom/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-bom-2.11.1.pom": { + "sha1": "619e312d377367fcf8d704c2f575fe62922b7426", + "sha256": "sha256-T9YaleWh3rpO8RxpaBWq7A8Q8NmOEH18fQrHkP6M7t8=" + } + } + }, + + { + "path": "com/fasterxml/jackson/jackson-bom/2.19.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-bom-2.19.0-rc2.pom": { + "sha1": "1c22be24bb5ea2c9b30d957f9453736d613f703c", + "sha256": "sha256-EWrClE0XCWRpWG513QCMcquRzO/TdzRpMSx+fMPu61M=" + } + } + }, + + { + "path": "com/fasterxml/jackson/jackson-parent/2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-parent-2.11.pom": { + "sha1": "89f0f8a7f21a54125e0ae1f72e6cdd50b51c87f3", + "sha256": "sha256-wbChPYz+Y7QLD66EWLmUYxae0MYPf8lj7z8FJXFQy+g=" + } + } + }, + + { + "path": "com/fasterxml/jackson/jackson-parent/2.19-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-parent-2.19-rc2.pom": { + "sha1": "b9aacc8e4d5b20524fa9ff969273b1e46ceff973", + "sha256": "sha256-PZiWsPV//2EYq//JEDulQ3nXNg5+oyB7AITJqFiWHl0=" + } + } + }, + + { + "path": "com/fasterxml/jackson/module/jackson-modules-base/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-modules-base-2.11.1.pom": { + "sha1": "01c50f48159f43e7e2309452e1c06b84b0db3ba9", + "sha256": "sha256-7Nm5O8YNTgz8dmhT3f/3ryqz7ku/ZptVRcFPYDSZcSI=" + } + } + }, + + { + "path": "com/fasterxml/jackson/module/jackson-modules-base/2.19.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-modules-base-2.19.0-rc2.pom": { + "sha1": "8838c701d7dbb82d988cc237e457e2a5fa3974db", + "sha256": "sha256-W9YZOmt8/YCgFsvDtKbgODz+9UicWwtfZnelo9XwVtQ=" + } + } + }, + + { + "path": "com/fasterxml/jackson/module/jackson-module-jaxb-annotations/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-module-jaxb-annotations-2.11.1.pom": { + "sha1": "c49cfafbb34f2f54015caf99fcab1698c40f680a", + "sha256": "sha256-iXalf04+k5SczvgecgCeiUo12zadU5mMpJYPKkVyuHs=" + }, + "jackson-module-jaxb-annotations-2.11.1.jar": { + "sha1": "bdc322415a167c6b90f77fef535f24a61abefb67", + "sha256": "sha256-kD+RAir43k1AvynOCgvfmMMnQOX3ULLXYwYD/J4+7Yc=" + } + } + }, + + { + "path": "com/fasterxml/jackson/module/jackson-module-jaxb-annotations/2.19.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-module-jaxb-annotations-2.19.0-rc2.pom": { + "sha1": "d92f9b87833e5bd5cecddebe564492a8557a6fd3", + "sha256": "sha256-HKyCv+kHj4frdz6ku7sM1bAhE/0PQiNAD/F8YuM4Aq0=" + }, + "jackson-module-jaxb-annotations-2.19.0-rc2.jar": { + "sha1": "1110b50675c55f220b739546be3f93a2b7f8887b", + "sha256": "sha256-zd2Z0QpwESaUFQaMfBinxZfyiUit2/dEnA6AHCz6LEw=" + }, + "jackson-module-jaxb-annotations-2.19.0-rc2.module": { + "sha1": "061513ae836b4bdbbc0a3732d1f568387e3e38bb", + "sha256": "sha256-33yI5TT33i+Xx8VsfFd6MmCnl+qKZ/NtTxJ6jiKuRR8=" + } + } + }, + + { + "path": "com/fasterxml/jackson/module/jackson-module-kotlin/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-module-kotlin-2.11.1.pom": { + "sha1": "08f9a08430cc5bf5dd63628e72e1720c8b6b8bf4", + "sha256": "sha256-vgsyV0RK22ycxpSB1JW4ijEw1FGTDtEkAIn2c7whqgk=" + }, + "jackson-module-kotlin-2.11.1.jar": { + "sha1": "3e427205246f26dac2d2f2c9ba763cbeb7b919b3", + "sha256": "sha256-twe2AYLlHYycfU1PDonVct7e0A4o/hOnq/D4UGK54fk=" + } + } + }, + + { + "path": "com/fasterxml/oss-parent/38", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "oss-parent-38.pom": { + "sha1": "ff6b9e9e40c23235f87a44b463995b47adca8dc9", + "sha256": "sha256-yD+PRd/cqNC2s2YcYLP4R4D2cbEuBvka1dHBodH5Zug=" + } + } + }, + + { + "path": "com/fasterxml/oss-parent/64", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "oss-parent-64.pom": { + "sha1": "968074901581fdc2b48ef4b8f2975fefe000975e", + "sha256": "sha256-coKMT0mQ19qo40+pQ8qiKlzzgQWysM5hJlGZIdJ272A=" + } + } + }, + + { + "path": "com/fasterxml/woodstox/woodstox-core/6.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "woodstox-core-6.2.1.pom": { + "sha1": "979823754d4d774267043fb4e24f489d078897a7", + "sha256": "sha256-L2rXsKp/LWvJxMKW/NO+bcdDbjSbmp1C4e91QfxFIM8=" + }, + "woodstox-core-6.2.1.jar": { + "sha1": "1600925b1ec7c7a3403c8f068ad62feead13132f", + "sha256": "sha256-3nDQ8elxtmL1tb8TikZ7Zx/VCy2/fepcHMOzOtfV0Gs=" + } + } + }, + + { + "path": "com/github/bumptech/glide/annotations/4.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-4.12.0.pom": { + "sha1": "94d5a495b107bd421804a296c9073439402c0996", + "sha256": "sha256-5CdDmiE29CW4swywd8XsrSn0V0D985EXqAdmOIA1LmQ=" + }, + "annotations-4.12.0.jar": { + "sha1": "298d793db124809d74ad9a90de5eddc408898c84", + "sha256": "sha256-+kMMv4+TH82Jw4GVq1VE701iyKxsLm9J+PSueGD63o4=" + } + } + }, + + { + "path": "com/github/bumptech/glide/compiler/4.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "compiler-4.12.0.pom": { + "sha1": "4a090fcf1fe6ec3782581efc517a8f621e85a804", + "sha256": "sha256-7Kki9JOhNc1I1gZrrPqOjAkawmDARXwmGs6EHDfNPYg=" + }, + "compiler-4.12.0.jar": { + "sha1": "75cf5e43fd6e6c6e769e060014feb985284cfae7", + "sha256": "sha256-AwKXczqlLdUu+dRYleAtdEa1N9YI/mlyUUufJhwjyZo=" + } + } + }, + + { + "path": "com/github/bumptech/glide/disklrucache/4.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "disklrucache-4.12.0.pom": { + "sha1": "0c90a83f7490992222d8bf7227b35e6bc0e4f552", + "sha256": "sha256-ZNJlhZ9swclEHEH5jQB4GmiIu/2q4rcJeslnp6I/CtQ=" + }, + "disklrucache-4.12.0.aar": { + "sha1": "1a9f7c8e74713cbef4889af389c38acb18f0cbb6", + "sha256": "sha256-ysV2SJXlVkRlSOMzO67EXOsRNhgVv+hf7Ye246JlBcM=" + }, + "disklrucache-4.12.0.aar.asc": { + "sha1": "106dcfb240f0430de7dafeedfcdb3546b56ec0da", + "sha256": "sha256-XBePEgjXWcheWsSpiYyVWBTrH2qsvceDDt3OJsMx8VY=" + }, + "disklrucache-4.12.0.jar": { + "sha1": "6d44c9103551e3ff51f00ac26077c143a8fb74f9", + "sha256": "sha256-bwzJBpZGrFgu542/Yh6QMD6iALnOQcIKw2jpJe1FY3k=" + } + } + }, + + { + "path": "com/github/bumptech/glide/gifdecoder/4.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gifdecoder-4.12.0.pom": { + "sha1": "9105e854c39f7d5dbcfd04f644a243f31e48033d", + "sha256": "sha256-gZO9Bwe+0CZxbbuixrG9ofQtJLv3qFjUyNlurpDTOOk=" + }, + "gifdecoder-4.12.0.aar": { + "sha1": "a00cc6741d9f7cf025dd93293c724a688c47631e", + "sha256": "sha256-GXoc1bdoVaoCsjDBOXTikyKbkB3CuW+rQxUgHni6qAQ=" + }, + "gifdecoder-4.12.0.aar.asc": { + "sha1": "5eace550ebbdcf473d6cb4f1c68df872ca33ed67", + "sha256": "sha256-NHmBZlgsA/SY8/gxYwOqFaCWsAdR8/RiQqBtDonRMnE=" + }, + "gifdecoder-4.12.0.jar": { + "sha1": "880194d896cf782586f292a3c340d461bdc63933", + "sha256": "sha256-rDYpkcUaxxF7OgrOuT1lyOiVHNp21Wdml1WHuOdf84c=" + } + } + }, + + { + "path": "com/github/bumptech/glide/glide/4.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "glide-4.12.0.pom": { + "sha1": "74ef469a4966c69ec7346df8b3895ac11474f8be", + "sha256": "sha256-/W1IchOekzIZDaKfkJEZxHh7Ff0B7KYqKo6qbUBcuMA=" + }, + "glide-4.12.0.aar": { + "sha1": "3f57db6cc954212739bb0d693ec48ecbc8ab73c4", + "sha256": "sha256-auKUSttil3/lP0LE+GoivTJsGCi2k0QczOQw6S8Ug4Q=" + }, + "glide-4.12.0.aar.asc": { + "sha1": "8171f43cb0b0c40b44d1f36697b16bc7a23709dc", + "sha256": "sha256-F8mA2KbVfu/ch8uaFlTVdrN6lImzNbISoAs3K9XEzCM=" + }, + "glide-4.12.0.jar": { + "sha1": "33c523a0853351c1484d23628020c90cc3c20251", + "sha256": "sha256-/0LHIOtJ/h2MWhWVa2dpdwmFg0QQAr3WDB4I7l8cY9Q=" + } + } + }, + + { + "path": "com/github/bumptech/glide/okhttp3-integration/4.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okhttp3-integration-4.12.0.pom": { + "sha1": "86bab5a64ff1b3492ca0a416b59de2ed89d46608", + "sha256": "sha256-zx7uo18pEKBUotLTIElnGi5ez7R+n7+94QMNTu2fYHQ=" + }, + "okhttp3-integration-4.12.0.aar": { + "sha1": "8235c8ea4484c142ac058b736e807f9c56346e59", + "sha256": "sha256-DXIu9NqXHz0J6gc1HdOp/3MPsXtDix1NdE+aKKbN1iA=" + }, + "okhttp3-integration-4.12.0.aar.asc": { + "sha1": "aca3b7ea70455bfcfd2c7f8ef2e697d6f9ca0135", + "sha256": "sha256-MiEHIb9irIoCO+1m1rDAjNhANCz2KmGbOiurG1fj1Js=" + }, + "okhttp3-integration-4.12.0.jar": { + "sha1": "62392244f50db7c61dcd701345d39f0bcf106ef4", + "sha256": "sha256-Nvzw04rmw3DX1stq4103rn7sox1bzrENrxGZlovisPI=" + } + } + }, + + { + "path": "com/github/clans/fab/1.6.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fab-1.6.4.pom": { + "sha1": "1211e25e0611566427a6fc5fdcee00a1ddda3dd2", + "sha256": "sha256-xiM7gvUj/9nOa/m9xEkXYkxaSKgKmesWgjk3AYEcq80=" + }, + "fab-1.6.4.aar": { + "sha1": "8e468a9d9ba05bdb2612eb22d5cd5add9a238c8c", + "sha256": "sha256-UxKTFfj+s+zRBXhQAcYlEUdbkHWqDUX2Ir7XfVMq1Eg=" + }, + "fab-1.6.4.aar.asc": { + "sha1": "609710d7a9c665f6ab19200c6027a6acf33f9f26", + "sha256": "sha256-NKLCbjYqunifbElERUPxWtu2AVMkXFZz2g41Be8DO00=" + } + } + }, + + { + "path": "com/github/gundy/semver4j/0.16.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "semver4j-0.16.4.pom": { + "sha1": "ca8df209029884f283afdcd7b104fb88576a18b1", + "sha256": "sha256-MgAdskQ7M53SH1t5/ynRreci0boIDCFL3oGfD3LRYE0=" + }, + "semver4j-0.16.4-nodeps.jar": { + "sha1": "273cc1869ddf7ebbac8176402e29f41b1c83d7df", + "sha256": "sha256-P1nspRY3TM1P01UWJb9Q+KSxkfcAUI985IZkYKYSivA=" + }, + "semver4j-0.16.4.jar": { + "sha1": "de8c77583e97bbbd3cdbe5f406b583e9b081ff56", + "sha256": "sha256-3vm0Il+jchnhj4HQHw5S1z3KElejj1R1vp3Vj4dzZRA=" + } + } + }, + + { + "path": "com/googlecode/concurrent-trees/concurrent-trees/2.6.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "concurrent-trees-2.6.1.pom": { + "sha1": "ffd029b087e3132fb021f64841e103e4e813944d", + "sha256": "sha256-Q8K5sULnBV0fKlgn8QlEkl0idH2XVrMlDAeqtHU4qXE=" + }, + "concurrent-trees-2.6.1.jar": { + "sha1": "9b647240522ab67c003de9b6702ca81ac0c15efc", + "sha256": "sha256-BONySYTipcv1VgbPo3KlvT08XSohUzpwBOPN5Tl2H6U=" + } + } + }, + + { + "path": "com/googlecode/javaewah/JavaEWAH/1.1.12", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "JavaEWAH-1.1.12.pom": { + "sha1": "15223fb6adf95c0ecb031a3b430e82d2f26c2c56", + "sha256": "sha256-BhhOmwWeCAkRgnE2r17Hj1fuTDvBn2MutWGw34+HiM4=" + }, + "JavaEWAH-1.1.12.jar": { + "sha1": "9feecc2b24d6bc9ff865af8d082f192238a293eb", + "sha256": "sha256-sZIEMxrG4gS++vUIpo9S7GtGONnZus3b69Q1+cTVAPI=" + } + } + }, + + { + "path": "com/googlecode/javaewah/JavaEWAH/1.1.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "JavaEWAH-1.1.13.pom": { + "sha1": "c0e2224902ce244ef82d72c9287f933394255452", + "sha256": "sha256-lyWx/pxoENl3dQu4RBXqEILEtIjUqDn5cEu09ej8F/Q=" + }, + "JavaEWAH-1.1.13.jar": { + "sha1": "32cd724a42dc73f99ca08453d11a4bb83e0034c7", + "sha256": "sha256-TA/aKx0xd1DX6jJONscLK8SDEMCqrme5jfCRXWltcRE=" + } + } + }, + + { + "path": "com/googlecode/javaewah/JavaEWAH/1.2.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "JavaEWAH-1.2.3.pom": { + "sha1": "24485416d388ffe422bbbbc8ffbc742eb35e2b70", + "sha256": "sha256-5O1sZpYgNm+ZOSBln+CsfLyD11PbwNwOseUplzr5byM=" + }, + "JavaEWAH-1.2.3.jar": { + "sha1": "13a27c856e0c8808cee9a64032c58eee11c3adc9", + "sha256": "sha256-1lImlJcTxMYaeE9BxRFn57Axb5N2Q5jrup5DNrPZVMI=" + } + } + }, + + { + "path": "com/googlecode/json-simple/json-simple/1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "json-simple-1.1.pom": { + "sha1": "a2c3a73d894b86ac979b88be6537b284eb4bf916", + "sha256": "sha256-R6ib4PoP7dR221/SyDSHZU0qEZw5H4OhQr6HZmfPfas=" + }, + "json-simple-1.1.jar": { + "sha1": "5e303a03d04e6788dddfa3655272580ae0fc13bb", + "sha256": "sha256-LZSE9MZJ9wj0f5pHlGX8cpdw7mVhfcowEYNmAiZPZDk=" + } + } + }, + + { + "path": "com/googlecode/juniversalchardet/juniversalchardet/1.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "juniversalchardet-1.0.3.pom": { + "sha1": "be6da8320adedafc712d645ddaaad00357b55408", + "sha256": "sha256-eEY5mzXHzWQqmzoADD4tYtBOs3pFR7aTPMixi8wvCGs=" + }, + "juniversalchardet-1.0.3.jar": { + "sha1": "cd49678784c46aa8789c060538e0154013bb421b", + "sha256": "sha256-dXv+kGGTuLZR553CbNZ9a1XQdwos37A4FZFQT3edSnY=" + } + } + }, + + { + "path": "com/google/android/annotations/4.1.1.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-4.1.1.4.pom": { + "sha1": "c5a23d7076f3c7fd4b8f39b59ff02b1e164edc28", + "sha256": "sha256-5LtUdTw2onoOXXAVSlA0/t2P6sQoIpUDS/1IPWx6rng=" + }, + "annotations-4.1.1.4.jar": { + "sha1": "a1678ba907bf92691d879fef34e1a187038f9259", + "sha256": "sha256-unNOHoTAnWFa9qCdMwNLTwRC+Hct7BIO+zdthqVlrhU=" + } + } + }, + + { + "path": "com/google/api/grpc/proto-google-common-protos/1.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proto-google-common-protos-1.12.0.pom": { + "sha1": "583c87bb5828d170c2ae2334fb24711653e8fa20", + "sha256": "sha256-FWK9RN8nIx1esWQWJd9ysP4T4p/ur1VSfIXmF97PDjo=" + }, + "proto-google-common-protos-1.12.0.jar": { + "sha1": "1140cc74df039deb044ed0e320035e674dc13062", + "sha256": "sha256-vWDNekI7APuCTCe90Ck6r0eBvh2rpu0lYxEQP7S4QQg=" + } + } + }, + + { + "path": "com/google/api/grpc/proto-google-common-protos/2.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proto-google-common-protos-2.0.1.pom": { + "sha1": "ae5815e509e885a4aaa42e01fc3aafe3c3f28716", + "sha256": "sha256-i2K/2F1pPrz/2CgjKvdCTzT9ISxeKOXtmbDcXSvH1PU=" + }, + "proto-google-common-protos-2.0.1.jar": { + "sha1": "20827628ea2b9f69ae22987b2aedb0050e9c470d", + "sha256": "sha256-XOcWVhGGGHMeNKXUxhqjoDG+I0Rtx96LWl53tm681u8=" + } + } + }, + + { + "path": "com/google/auto/auto-parent/3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-parent-3.pom": { + "sha1": "8e22bd67b5aeb5f03fd50efd5910ba6dfb7f4c86", + "sha256": "sha256-yY8bCXjP0kcSwnOI4EDFDOBpIlKzqpRIYD2e+gXr6Kg=" + } + } + }, + + { + "path": "com/google/auto/auto-parent/6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-parent-6.pom": { + "sha1": "0f29db646babbca0049636b6724613035bbcedff", + "sha256": "sha256-BfdAxmSBZdsAz2GN1WwgDEcl41jm1U9YU+C+wVc06go=" + } + } + }, + + { + "path": "com/google/auto/auto-parent/7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-parent-7.pom": { + "sha1": "02ab24442738fb975a81595ef033f67a72358c1c", + "sha256": "sha256-pGQm/MtdMnBa2cu8mW94a9BIzIy90h2wRlABafFaQ1Y=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-annotations/1.6.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-annotations-1.6.2.pom": { + "sha1": "ae1ff98291be961d9e1b14eb26bedaf0b5ac9d15", + "sha256": "sha256-HHbNRi/JbnqpbccM6C8NVAY9bfFts1ycfZzA0amdP/8=" + }, + "auto-value-annotations-1.6.2.jar": { + "sha1": "ed193d86e0af90cc2342aedbe73c5d86b03fa09b", + "sha256": "sha256-tIsE3bpA6KwzvwNvBvxDmV/FCEvZS9qs6AfOJ9O+o/s=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-annotations/1.6.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-annotations-1.6.3.pom": { + "sha1": "8ccd7ba6d7ea8de204f17f6e509d77a76f06d298", + "sha256": "sha256-4fx4D37gJeZis9pycj2+KsjawKL4kg8mUxXE4b49dlw=" + }, + "auto-value-annotations-1.6.3.jar": { + "sha1": "b88c1bb7f149f6d2cc03898359283e57b08f39cc", + "sha256": "sha256-DpUf7owx9gJwvEZVOoWGABt7k9uxKuwGNzqpmhUDksA=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-annotations/1.7.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-annotations-1.7.4.pom": { + "sha1": "f737dd3756969b2fdc91d3d8a71e68386949b6ba", + "sha256": "sha256-Fs1vEf/zk5pbBBSAYhteF7ZjapSaS2WAdmCcYdJTqYU=" + }, + "auto-value-annotations-1.7.4.jar": { + "sha1": "eff48ed53995db2dadf0456426cc1f8700136f86", + "sha256": "sha256-/t1ZsLSYbDQvarLRgvKk7p/Ossfi1b3E3HZMkjlKI9M=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-annotations/1.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-annotations-1.11.0.pom": { + "sha1": "a6b3d6cd083f5e03aad810d4bc2b08755cf91753", + "sha256": "sha256-KuwW406j4BFiGgMi9PNvj5v5iLtURitVcJduieoHsSI=" + }, + "auto-value-annotations-1.11.0.jar": { + "sha1": "f0d047931d07cfbc6fa4079854f181ff62891d6f", + "sha256": "sha256-WgVc5CVTM7M0bhqHA9pb+P8ElTIob9zTFxLWJKvhEd0=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-parent/1.6.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-parent-1.6.2.pom": { + "sha1": "5eb0685cabce285064a62a7b9c6c256343a58285", + "sha256": "sha256-J7ZAyCF59c/2IAnAtyAz2bxg9g6ZAqZoAidLf+N/yBw=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-parent/1.6.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-parent-1.6.3.pom": { + "sha1": "7e21ec842ec2e34f4d8e277ef98be79401dce4c6", + "sha256": "sha256-5Z31cytMs01XJxgURvne2c5EJRMaChBiUZ7qGW3k2KE=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-parent/1.7.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-parent-1.7.4.pom": { + "sha1": "a75e195146275e9ff1a18cd3804cf30cce990cc8", + "sha256": "sha256-YYssv7OZ5FErMip3tbT2qMyB8w83em1sUt7IKjwzGqM=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value-parent/1.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-parent-1.11.0.pom": { + "sha1": "4d1249846df8ae67b8ad313bc9a6174516fa49b3", + "sha256": "sha256-Wg0dcYVS6KRdzOASjRtrliP6lxqCzSRXUyM7pyCMsp0=" + } + } + }, + + { + "path": "com/google/auto/value/auto-value/1.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "auto-value-1.5.2.pom": { + "sha1": "e9ba039f98da8c1ea01ad9715befc4a6cf4fdc7c", + "sha256": "sha256-kB+xRAoO+ODAVTp7tEwLiWR79TyIImQu6gtXFzx8aI0=" + }, + "auto-value-1.5.2.jar": { + "sha1": "1b94ab7ec707e2220a0d1a7517488d1843236345", + "sha256": "sha256-bKyUIUObObn0qnZzjO0DlGKmmRHEEPLDcsGWsPS39M8=" + } + } + }, + + { + "path": "com/google/code/findbugs/jsr305/1.3.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jsr305-1.3.9.pom": { + "sha1": "67ea333a3244bc20a17d6f0c29498071dfa409fc", + "sha256": "sha256-/quRkTEcPXru8rZtYGSvyA09HVLZgPsHrkPHjJh7qTo=" + }, + "jsr305-1.3.9.jar": { + "sha1": "40719ea6961c0cb6afaeb6a921eaa1f6afd4cfdf", + "sha256": "sha256-kFchoO6pCoFTSrt+5u9OouXmRfod7wpc2IQC3xtGye0=" + } + } + }, + + { + "path": "com/google/code/findbugs/jsr305/2.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jsr305-2.0.1.pom": { + "sha1": "95efa8cea662452bb74b34abe09a93ff47625c8f", + "sha256": "sha256-AsEsPCrhLdR1IZ/2kcgqTZ6iH0S8WUoYEpW/bUPc+7A=" + }, + "jsr305-2.0.1.jar": { + "sha1": "516c03b21d50a644d538de0f0369c620989cd8f0", + "sha256": "sha256-Hn9T+luLXIB+mGujNWZdoD8Y1mCALYvwYYIwidG+5Gg=" + } + } + }, + + { + "path": "com/google/code/findbugs/jsr305/3.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jsr305-3.0.1.pom": { + "sha1": "d04690f71f3393e23f30998d9534365274fa5f9f", + "sha256": "sha256-QXCnYdxb/TmBqOb3qrnirNzoLTT9Wqm7EePAkNJTFM4=" + }, + "jsr305-3.0.1.jar": { + "sha1": "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d", + "sha256": "sha256-yIXONCSWgrwCNrSn1W78wSBI5hNaW696nN6K2M2hP80=" + } + } + }, + + { + "path": "com/google/code/findbugs/jsr305/3.0.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jsr305-3.0.2.pom": { + "sha1": "8d93cdf4d84d7e1de736df607945c6df0730a10f", + "sha256": "sha256-GYidvfGyVLJgGl7mRbgUepdGRIgil2hMeYr+XWPXjf4=" + }, + "jsr305-3.0.2.jar": { + "sha1": "25ea2e8b0c338a877313bd4672d3fe056ea78f0d", + "sha256": "sha256-dmrSoHg/JoeWLIrXTO7MOKKLn3Ki0IXuQ4t4E+ko0Mc=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.7.pom": { + "sha1": "3e147803ee87007ec82a1f625a0034be35c9b8a8", + "sha256": "sha256-hx6Mc3MiMB8MyKz+OaKWDEV52dhmau6lKYu2/MnkJys=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.8.0.pom": { + "sha1": "df4e302623519c6b801aee5281bada262b9ed876", + "sha256": "sha256-Dx2DabHp8bxLRnyNz92LWXM6rVtchBnDpZsvmrrwTNI=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.8.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.8.5.pom": { + "sha1": "40f295fd74fbe91f1589481c967b14f176d6e549", + "sha256": "sha256-jx/scrkaceo57Dn193jE0RJLawl8bVWzpQtVSlIjeyc=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.8.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.8.6.pom": { + "sha1": "88d12a7aeaeaa9bbc773bc5f32690edf4d3ff9e6", + "sha256": "sha256-NzZGOFnsGSZyleiUlAroKo9oRBMDESL+Nc58/34wp3Q=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.8.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.8.9.pom": { + "sha1": "0c3fdad2fa3239f928ad9c77a9f9c9379f6bde7a", + "sha256": "sha256-sW4CbmNCfBlyrQ/GhwPsN5sVduQRuknDL6mjGrC7z/s=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.9.1.pom": { + "sha1": "066fdfacdd5cac19a21e4cd2fc94ac8587f3a2a5", + "sha256": "sha256-fKCEXnNoVhjePka9NDTQOko3PVIPq5OmgDGK1sjLKnk=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.10.pom": { + "sha1": "ed3f42b8b29f94e917769344aaeeb78639ecbd0b", + "sha256": "sha256-+1OsCwbBkRbKYaw0S03+inwpzE+Bs1POiJSTpQOQBPs=" + } + } + }, + + { + "path": "com/google/code/gson/gson-parent/2.12.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-parent-2.12.1.pom": { + "sha1": "660107b2e76095ef86bbd15c503afe11e5260bfb", + "sha256": "sha256-yeewt+Mb574iaEl5wGgAHGUssRPE5u2JTjm2Q97gf8E=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.2.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.2.4.pom": { + "sha1": "06252c690921ee1dc719594a5d4da1829194f8b3", + "sha256": "sha256-rphNXRmJTObcRomGbqoS+PwxQJET5g7jtEhTuKxP04A=" + }, + "gson-2.2.4.jar": { + "sha1": "a60a5e993c98c864010053cb901b7eab25306568", + "sha256": "sha256-wDKM0Hyp42OlrNAMHPSv6M9VS9bTc4NJgboFzr7Gh/s=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.3.pom": { + "sha1": "9bdcf3053cba435cadc5fa575cc835429a0fb0c2", + "sha256": "sha256-GlaZL8UPg4/Qkba5cBsw2mSwC9L+dq0sgAIS60WbSeI=" + }, + "gson-2.3.jar": { + "sha1": "5fc52c41ef0239d1093a1eb7c3697036183677ce", + "sha256": "sha256-lGZRLNjPC9ZqijZhaFNz97Per97REJbgRJJv9cgsekw=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.7.pom": { + "sha1": "09f9e39f9b791aeb73ba428ad30872f1a703edb3", + "sha256": "sha256-cjk27Ae4eyEKRO+AWPMvVuPVZEIhjfv49OU4qScWDt4=" + }, + "gson-2.7.jar": { + "sha1": "751f548c85fa49f330cecbb1875893f971b33c4e", + "sha256": "sha256-LUPrXqnhM9LuJAXMFPXuCJUbg2EwL92TSUo6mXtQjTI=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.8.0.pom": { + "sha1": "36e74e4b4e2a699b4dc43e722cd0ad436f6b21fd", + "sha256": "sha256-pTMaDstUj5lCq1uTx6xDw4oh6Jd2Pd4bzb8HdPQWym8=" + }, + "gson-2.8.0.jar": { + "sha1": "c4ba5371a29ac9b2ad6129b1d39ea38750043eff", + "sha256": "sha256-xiIXY715xPHD3H91C18poLs4s2e4ExTE9xiW40DECCU=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.8.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.8.5.pom": { + "sha1": "2c18fa1082aa1c422aece88244c084be50c76500", + "sha256": "sha256-uDCFV6f8zJLZ/nyM0FmSWLNhKF0uzedontqYhDJVoJI=" + }, + "gson-2.8.5.jar": { + "sha1": "f645ed69d595b24d4cf8b3fbb64cc505bede8829", + "sha256": "sha256-IzoBSfw2XJ9u29aDz+JmsZvcdzvpjqva9rPJJLSOfYE=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.8.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.8.6.pom": { + "sha1": "b9d7e274612598f1e8835d8bbe30baa5b970ca32", + "sha256": "sha256-IXRBWmRzMtMP2gS9HPxwij7MhOr3UX9ZYYjYJE4QORE=" + }, + "gson-2.8.6.jar": { + "sha1": "9180733b7df8542621dc12e21e87557e8c99b8cb", + "sha256": "sha256-yPtIOQVNKAswM/gA0fWpfeLwKOuLoutFitKH5Tbz8l8=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.8.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.8.9.pom": { + "sha1": "e40b03e4cc2b52efb19af75c07596e9d15a52d82", + "sha256": "sha256-r97W5qaQ+/OtSuZa2jl/CpCl9jCzA9G3QbnJeSb91N4=" + }, + "gson-2.8.9.jar": { + "sha1": "8a432c1d6825781e21a02db2e2c33c5fde2833b9", + "sha256": "sha256-05mSkYVd5JXJTHQ3YbirUXbP6r4oGlqw2OjUUyb9cD4=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.9.1.pom": { + "sha1": "f0cf3edcef8dcb74d27cb427544a309eb718d772", + "sha256": "sha256-5ZZjI9cUJXCzekvpeeIbwtroSBB+TcQW2PRNmqPwKQM=" + }, + "gson-2.9.1.jar": { + "sha1": "02cc2131b98ebfb04e2b2c7dfb84431f4045096b", + "sha256": "sha256-N4U04znm5tULFzb7Ort28cFdG+P0wTzsbVNkEuI9pgM=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.10.pom": { + "sha1": "03c9221549841dd251d2f80ffe3bfd60c663b5af", + "sha256": "sha256-rGnZ8lQmDK6rOZjqrWDzVVmcJRIeGVFWv9/8ijVfxr0=" + }, + "gson-2.10.jar": { + "sha1": "dd9b193aef96e973d5a11ab13cd17430c2e4306b", + "sha256": "sha256-DN0WPONZiiD8BO7nGxQLJPbyo7NfCkmdu92YUug/v68=" + } + } + }, + + { + "path": "com/google/code/gson/gson/2.12.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gson-2.12.1.pom": { + "sha1": "d2c3993ff96e5da39a57e5e0b695eda560949b57", + "sha256": "sha256-C1c17IX0UoLx4sdpd5gAQnsVCoFj9AUJOpKAtxyrGXg=" + }, + "gson-2.12.1.buildinfo": { + "sha1": "c2e219d4a63eebf78ce409ee8303eb930248f486", + "sha256": "sha256-fNm4d7ep72MLsQq/BeBRdEuam/9hzGJafTURb48yeuY=" + }, + "gson-2.12.1.buildinfo.asc": { + "sha1": "01ba63402708ab4efbd2fa2f4dc5ee7f136b9479", + "sha256": "sha256-M+At44sk0y+mblm7UnhRzYzeO36NPGTpP3rTID/wJjU=" + }, + "gson-2.12.1.jar": { + "sha1": "4e773a317740b83b43cfc3d652962856041697cb", + "sha256": "sha256-6+4T1ft0d81/HMAQ4MNW34yoBwlxUkjal/eeNcy0++w=" + } + } + }, + + { + "path": "com/google/crypto/tink/tink/1.3.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "tink-1.3.0-rc2.pom": { + "sha1": "5a666d168e1b821be33321a858f90baa5ab4b6cc", + "sha256": "sha256-W5oRwRGD0BHemppUkJYteKTUv+dXmwxb5K+xiiIuP0E=" + }, + "tink-1.3.0-rc2.jar": { + "sha1": "c7efb1ecc3b667b8a0789a1b019b06269037e19b", + "sha256": "sha256-OqV2tWcdDibYPX/bcYaBC1Zy8Av45yGEJgvMfC19wHo=" + } + } + }, + + { + "path": "com/google/crypto/tink/tink/1.7.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "tink-1.7.0.pom": { + "sha1": "a8cba5b72a24f8adbd86d4e3172c01337ed0d4e0", + "sha256": "sha256-Ku41I3FfjyzRCyYDyNGeVhrHWDELfiyYU5RtLF57S/c=" + }, + "tink-1.7.0.jar": { + "sha1": "668b57f109d32349b2870448f06ae6f202713edc", + "sha256": "sha256-iJcKRWoIukxmsBsj5YRsoQlcwU5Uy0g2Pl0uFaEwcwg=" + } + } + }, + + { + "path": "com/google/dagger/dagger/2.28.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "dagger-2.28.3.pom": { + "sha1": "6a11b6b6db0c740dbd150e11f54c6f219fdc2891", + "sha256": "sha256-JlupWajhPDoGEz8EtTkWnBAY2v/U0z9TxFOrTLOG9XA=" + }, + "dagger-2.28.3.jar": { + "sha1": "10d83810ef9e19714116ed518896c90c6606d633", + "sha256": "sha256-8d0j+K40qOkTZnI5kerQ1kmdGj6RY85VDCALAtdqhys=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.0.18", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.0.18.pom": { + "sha1": "8e0351439081cc11563b09019302926addadaa25", + "sha256": "sha256-kUQScZLW9hLCNmgl3OrrI7DVMTC4Pgvx/+EH0UcKhIc=" + }, + "error_prone_annotations-2.0.18.jar": { + "sha1": "5f65affce1684999e2f4024983835efc3504012e", + "sha256": "sha256-y0z62HC/VjoHGZ8+vqV2Pw3sRA/NoLMYZAsf6qeIZWs=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.2.0.pom": { + "sha1": "e28b5b546020f18ea29e2b4756023f53ee91492b", + "sha256": "sha256-XgJY6huk5RoTN0JoC8IkSPerIUvkBz6GGfZF7xvkLdU=" + }, + "error_prone_annotations-2.2.0.jar": { + "sha1": "88e3c593e9b3586e1c6177f89267da6fc6986f0c", + "sha256": "sha256-br0iyhudjsBtQd6NZOBZaYHZYHtCA1+e03T53icaSBo=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.3.1.pom": { + "sha1": "19c878e6870c8382864dcc459de1c6bfe7f36e54", + "sha256": "sha256-PtzmtxG6No7+Frm3qssCFPvWSEFMublllTouftiagZo=" + }, + "error_prone_annotations-2.3.1.jar": { + "sha1": "a6a2b2df72fd13ec466216049b303f206bd66c5d", + "sha256": "sha256-EKWUmqD5XI3k/Uft/iBTTSrO/YwiT4r+ofYH4RKBYSA=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.3.2.pom": { + "sha1": "f9aadf833282cfd743aa5d330f927489ca9c8734", + "sha256": "sha256-jRdVYWGSiaUnVzz73Phy61wSFs+atpKg7z26bXk/Y7Q=" + }, + "error_prone_annotations-2.3.2.jar": { + "sha1": "d1a0c5032570e0f64be6b4d9c90cdeb103129029", + "sha256": "sha256-NXzWz7BnyWkibEQkUVAq7hOACiTpUP3953vNtFZaZo0=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.3.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.3.4.pom": { + "sha1": "9a23fcb83bc8ed502506a8e6c648bf763dc5bcf9", + "sha256": "sha256-EyZziktPfMrPYHuGahH7hRk+9g9qWUYRh85yZfm+W+0=" + }, + "error_prone_annotations-2.3.4.jar": { + "sha1": "dac170e4594de319655ffb62f41cbd6dbb5e601e", + "sha256": "sha256-uvfW6pfOYGxT4RtoVLpfLOfvXCTd3wr6GNEmC9JbACw=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.4.0.pom": { + "sha1": "efece50811944f4b355130bc01f5eb2739d28e58", + "sha256": "sha256-fqVL+iYOoD1PCEFY0oSshzelUH93yA+WNNqL3MAYR08=" + }, + "error_prone_annotations-2.4.0.jar": { + "sha1": "32ecccc595e4e4d813a80ee9e3ab5813d65874eb", + "sha256": "sha256-XyoGSCMKZi6L4EnfMI1YPXNp8Tr2g+RN31gpttdBoig=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.5.1.pom": { + "sha1": "d759b1b643583e30d8bb6bb55f4ad434f26c44a4", + "sha256": "sha256-mDrX/g+m9vkeLSBHf1CJ7Mw/TY7GxR1utFWDlw4nnTw=" + }, + "error_prone_annotations-2.5.1.jar": { + "sha1": "562d366678b89ce5d6b6b82c1a073880341e3fba", + "sha256": "sha256-/4Bia6rxKgk0K+/U6Ey6nVBmL1/Nf3qbNJCmt8+H5mw=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.7.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.7.1.pom": { + "sha1": "0b0fa22131b64b3985fba347927252af21898984", + "sha256": "sha256-Mahy4RScXzqLwF+03kVeXqYI7PrRryIst2N8psdi7iU=" + }, + "error_prone_annotations-2.7.1.jar": { + "sha1": "458d9042f7aa6fa9a634df902b37f544e15aacac", + "sha256": "sha256-zVJXwIokbPhiiBeuccuCK+GS75H2iByko/z/Tx3hz/M=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.9.0.pom": { + "sha1": "ffe29062ff5242287b65c16ec45e6a65bb946231", + "sha256": "sha256-pI7gVcn0dYRuIbhwKd+Jgrn3tek8wmVvyo0b2kcUCM4=" + }, + "error_prone_annotations-2.9.0.jar": { + "sha1": "74fe3b8b4f3fc84dc940d0ca4c4b270dbc902764", + "sha256": "sha256-+Ue9wzriemtKpEeZ5sIeGUR5e9ABC6Q+uC0RRG4WNpQ=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.10.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.10.0.pom": { + "sha1": "972fdb73175fce135e78555269ede8c8d3b36f74", + "sha256": "sha256-1oDcn1eKd6bc2ZKywyUOsWinIUzyf48MrB8GUmcQwMw=" + }, + "error_prone_annotations-2.10.0.jar": { + "sha1": "9bc20b94d3ac42489cf6ce1e42509c86f6f861a1", + "sha256": "sha256-oknU0l37htQebIL8M131gBifDJ/uq9xTIz/B5QYHJKE=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.11.0.pom": { + "sha1": "ced1afa200bbf344ccdef14b92aa84f7863f395c", + "sha256": "sha256-AmHKAfLS6awq4uznXULFYyOzhfspS2vJQ/Yu9Okt3wg=" + }, + "error_prone_annotations-2.11.0.jar": { + "sha1": "c5a0ace696d3f8b1c1d8cc036d8c03cc0cbe6b69", + "sha256": "sha256-chy5GEK0b6BWhH0QTVIlyLjh6LYiY7mTBR4eWgE3t+w=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.26.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.26.0.pom": { + "sha1": "b9b8335987b9ca7b231d89cbb622247122131f43", + "sha256": "sha256-w1ongEI/x+U1kioab351Ryjm+a2qJq2kw399nTVrnkM=" + }, + "error_prone_annotations-2.26.0.jar": { + "sha1": "c513866fd91bb46587500440a80fa943e95d12d9", + "sha256": "sha256-U838C+stdm/gO3jwsR0CBVTNQZh50gOAw4+h3PK6G1A=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.36.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.36.0.pom": { + "sha1": "daf188a28315b31852ebd550f78f6b764972ee1f", + "sha256": "sha256-15z9N8hfdta3VMdQHuHchEe3smQsI4LXeCUhZr0zHpw=" + }, + "error_prone_annotations-2.36.0.jar": { + "sha1": "227d4d4957ccc3dc5761bd897e3a0ee587e750a7", + "sha256": "sha256-d0QOJwsLyaJJkDxaB2w2pyLEiGyk9CZ18pA6HFPtYaU=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_annotations/2.37.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_annotations-2.37.0.pom": { + "sha1": "c1c9d609aebc7216aad5bc6e9cb97a313af2b093", + "sha256": "sha256-nBm0OV6yKL6yEJXdMdVa2V+bcnKYNpuShL0jMOi2LaI=" + }, + "error_prone_annotations-2.37.0.jar": { + "sha1": "8512660d1269d166fad497f51de35da61447f063", + "sha256": "sha256-0ppiY7SNRtTHwotkcXptEFs+Kj5kJWCS+EXo53T8pro=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.0.18", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.0.18.pom": { + "sha1": "16ffdb67ed91d9d87a943a3127da3900d83cc81d", + "sha256": "sha256-zxSZVSebB9TxHoF5hcEWSmnpMNc9t0QbQ6bvU7vShsQ=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.2.0.pom": { + "sha1": "72e2f31cb1952eada003d2ba7e82874cfe4b738c", + "sha256": "sha256-xGCQLd9ezmiDLGsnHOUqCSiwXPOmrIGo9UjHPL1UETg=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.3.1.pom": { + "sha1": "a32c199defa0dcf9c42a32c26fec4d1fbb408e58", + "sha256": "sha256-dnUl2agRKc0IGWg4KYAzYye+QWKx4iUaGCkR2qczwSM=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.3.2.pom": { + "sha1": "ea30b94538922c99fb231a2c9859790831eb0fb0", + "sha256": "sha256-jTtbn6IRl56cQl6YmCXAd0PS603Iwg7GHeD1s9waYzM=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.3.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.3.4.pom": { + "sha1": "a9b9dd42d174a5f96d6c195525877fc6d0b2028a", + "sha256": "sha256-QElbQ3pg0jmPD9/AVLidnDlKgjR6J0oHIcLpUKQwIYY=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.4.0.pom": { + "sha1": "b9ca052d865d3ed214b61462960923998be720e8", + "sha256": "sha256-3t2gzaTadSrIDeDSHIKc0/uRm9IlaBQmDrOrzi9Ohpo=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.5.1.pom": { + "sha1": "3cce581619abb2d892618745ef2f7f19087ecbc7", + "sha256": "sha256-oXp2d0gqOlGkX14IFAByrNMyzLEvI4mbAFLYCnKcLlI=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.7.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.7.1.pom": { + "sha1": "cca3ba79de44febe5a196510393e1011bd554e0b", + "sha256": "sha256-Cm4kLigQToCTQFrjeWlmCkOLccTBtz/E/3FtuJ2ojeY=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.9.0.pom": { + "sha1": "a4f56c023d5284d3161dac51af134af6856e1fe9", + "sha256": "sha256-UgsSPQhZdbVj400EQAxiY4lvhYhnzn/EeN039JHq47Q=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.10.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.10.0.pom": { + "sha1": "0d70888ba27b15543d639b0be077b1d0d6d38c40", + "sha256": "sha256-B4kah8SQ+QDBIJe+6V7By24HHIp9+BBmY4fgx3Lwifo=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.11.0.pom": { + "sha1": "bd585f378139da5b5e670840892c48e75d03cf7b", + "sha256": "sha256-goPwy0TGJKedMwtv2AuLinFaaLNoXJqVHD3oN9RUBVE=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.26.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.26.0.pom": { + "sha1": "442d80101ef468172d4fb920ee2b5efa3f5b536a", + "sha256": "sha256-LGKyqmiomYIdh66DvgQQut1OdIgXLezos04iRnXW6TY=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.36.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.36.0.pom": { + "sha1": "f280159378f00e9f428f10887e22f84ebb2c7e9f", + "sha256": "sha256-Okz8imvtYetI6Wl5b8MeoNJwtj5nBZmUamGIOttwlNw=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_parent/2.37.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_parent-2.37.0.pom": { + "sha1": "0c06731f64d33ecb250ce82552473b642e739d7b", + "sha256": "sha256-SmWUVg9cwGyJ2vzEn4spHxJ83bon1CYFliKhmrjWzkc=" + } + } + }, + + { + "path": "com/google/errorprone/error_prone_type_annotations/2.37.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "error_prone_type_annotations-2.37.0.pom": { + "sha1": "95a6d81b5930b1fd45bb96c25e3f2a0aed11005c", + "sha256": "sha256-69Fv9A5HxZg71f1iL0ym71cWPowYU+OilsOtKUYnUdY=" + }, + "error_prone_type_annotations-2.37.0.jar": { + "sha1": "fcfcfaee56c933a2dd44e2a870818ce090131fc5", + "sha256": "sha256-yg99yo1mQ1Wh0sr2HuaqFQiLHm/sPh1SXgnZsvtkhqs=" + } + } + }, + + { + "path": "com/google/flatbuffers/flatbuffers-java/1.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "flatbuffers-java-1.12.0.pom": { + "sha1": "f93bdcc0b38ffbec623e6610e0cddacae758cb54", + "sha256": "sha256-yyJrr1RiYHcPIegVKmqoi6FSMNc591DfSA8qZo1D4Os=" + }, + "flatbuffers-java-1.12.0.jar": { + "sha1": "8201cc7b511177a37071249e891f2f2fea4b32e9", + "sha256": "sha256-P4wIi03QSphYch8uFiUIyU2w3Yb5YeMG7mPvLtqHG/c=" + } + } + }, + + { + "path": "com/google/google/1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "google-1.pom": { + "sha1": "c35a5268151b7a1bbb77f7ee94a950f00e32db61", + "sha256": "sha256-zW2xehGjHt55TMvR3w5Nl1D2QCNHMfIc/4hamZcnfoE=" + } + } + }, + + { + "path": "com/google/guava/failureaccess/1.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "failureaccess-1.0.1.pom": { + "sha1": "e8160e78fdaaf7088621dc1649d9dd2dfcf8d0e8", + "sha256": "sha256-6WBCznj+y6DaK+lkUilHyHtAopG1/TzWcqQ0kkEDxLk=" + }, + "failureaccess-1.0.1.jar": { + "sha1": "1dcf1de382a0bf95a3d8b0849546c88bac1292c9", + "sha256": "sha256-oXHuTHNN0tqDfksWvp30Zhr6typBra8x64Tf2vk2yiY=" + } + } + }, + + { + "path": "com/google/guava/failureaccess/1.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "failureaccess-1.0.3.pom": { + "sha1": "12732d7aa78945d26029ab0dd54d5f824b6fe740", + "sha256": "sha256-xUvv839tQtQ+FHItVKUiya1R75f8W3knfmKj6/iC87s=" + }, + "failureaccess-1.0.3.jar": { + "sha1": "aeaffd00d57023a2c947393ed251f0354f0985fc", + "sha256": "sha256-y/w5BrGbj1XdfP1t/gqkUy6DQlDX8IC9jSEaPiRrWcs=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/17.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-17.0.pom": { + "sha1": "f8ba48b925d1c925d0fc0379ffa14a06e44eb464", + "sha256": "sha256-n7pTWFVR08d1F53oWXYKc8S+q0B9/eFrVRoJqHtQUyo=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/22.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-22.0.pom": { + "sha1": "52822d0abaa6bc42a26ea0d26a83abad05d938a8", + "sha256": "sha256-Hq+RguGXfBxQpw7b+/cFNjmMaJkL+q/J8OmJkEEgFTk=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/23.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-23.0.pom": { + "sha1": "79d7a4be1ccf3691ee279c5ee165385b7132f71c", + "sha256": "sha256-Zsw6DTKfn+EwYF/bLJAeWpuyYS8C8jp2dXtG5omzWU0=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/26.0-android", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-26.0-android.pom": { + "sha1": "a2c0df489614352b7e8e503e274bd1dee5c42a64", + "sha256": "sha256-+GmKtGypls6InBr8jKTyXrisawNNyJjUWDdCNgAWzAQ=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/27.0.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-27.0.1-jre.pom": { + "sha1": "8f7a96c144da567471d3d763a240c25ba374ebb1", + "sha256": "sha256-MX6IKRJi4M8oBelWwYhQ8dRWXIXC4REvXZ0Iqxcy5pY=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/28.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-28.1-jre.pom": { + "sha1": "cf90295394521cc63bf7cfdd4b81236531ccefdb", + "sha256": "sha256-8xpYRr2eFKTDdNobmxRqVxXs3Kkl4HHh0r9Notqt7ZA=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/29.0-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-29.0-jre.pom": { + "sha1": "bd26677407fe1a325335f7c0b96ed42417198250", + "sha256": "sha256-alf54C9436L0vaNBYGWmRCauG2beIoz24Zbi4ZElU78=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/30.1-android", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-30.1-android.pom": { + "sha1": "12791f126f39664cfaac1b0ca13411952c2080fa", + "sha256": "sha256-6ZvGLVWlZAfE3dwXhSIJFKU1+9kfnNxotcyHHVZwro8=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/30.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-30.1-jre.pom": { + "sha1": "b732bbf7c4ceb14dfd6af514a8b8b71dd952e53b", + "sha256": "sha256-4q+3R+vE/iMo1qkPqIxdioO7HjIGG7mxD/Q+LEetbnM=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/31.0.1-android", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-31.0.1-android.pom": { + "sha1": "72b166027ed42668cdef102402fbb05b8b981722", + "sha256": "sha256-MKUdNLB1ybTrai2j+6bB9QR7RSJaOgOuvO/At4gOVzU=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/31.0.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-31.0.1-jre.pom": { + "sha1": "eb78b5264bd92cca5655396c44420eef3ad3ec0f", + "sha256": "sha256-s7a2qnCZwRgXrO6FsyL9kffuMq6mn+CD7jbIc17AZ4g=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/31.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-31.1-jre.pom": { + "sha1": "99dae234b84eeaafa621086b6fff3530fb7e45d3", + "sha256": "sha256-RDliZ4O0StJe8F/wdiHdS7eWzE608pZqSkYf6kEw4Pw=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/33.4.0-android", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-33.4.0-android.pom": { + "sha1": "b58bc4ee7ad508a20cc41c776e8eef8fa78dfa5d", + "sha256": "sha256-ciDt5hAmWW+8cg7kuTJG+i0U8ygFhTK1nvBT3jl8fYM=" + } + } + }, + + { + "path": "com/google/guava/guava-parent/33.4.7-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-parent-33.4.7-jre.pom": { + "sha1": "ff1416b7eceb77e54c674c93a8dde9d1b63bed03", + "sha256": "sha256-HP4J7RFykrfaKJfiraET1uB+Jx4rTctKtqk6Yaq8bTE=" + } + } + }, + + { + "path": "com/google/guava/guava-testlib/33.4.7-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-testlib-33.4.7-jre.pom": { + "sha1": "67db329fefcf1951b77cf1bcf76cccec3a62cd92", + "sha256": "sha256-GW7oakfCaynpmiOJThsgoTL4TLKkdn7XMFtJeZtqJWc=" + }, + "guava-testlib-33.4.7-jre.jar": { + "sha1": "d77f409771aabb312c9cd383dbaf504c5c8c8ceb", + "sha256": "sha256-qB1UafMUUo3M/NDR3SJWOwiafGEtZWoyjGsAWlsmSNo=" + } + } + }, + + { + "path": "com/google/guava/guava/17.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-17.0.pom": { + "sha1": "0f534dabee0b40b25715869c5e1287e38c7e1e4a", + "sha256": "sha256-Kg0WAQ04JccyrqsdJfGBB0lFBj2PdsKABOfUI9Zst1s=" + }, + "guava-17.0.jar": { + "sha1": "9c6ef172e8de35fd8d4d8783e4821e57cdef7445", + "sha256": "sha256-jDaoDqYT0La4BAoXz4N8W742d7wbBqBYpsF0/beH67w=" + } + } + }, + + { + "path": "com/google/guava/guava/22.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-22.0.pom": { + "sha1": "b87878db57d5cfc2ca7d3972cc8f7486bf02fbca", + "sha256": "sha256-v62ztA9l3W3hZm1rKfi7VAMTlsdu7vQUbPnyglX4vzM=" + }, + "guava-22.0.jar": { + "sha1": "3564ef3803de51fb0530a8377ec6100b33b0d073", + "sha256": "sha256-EVjpTH3k2kgIc/C0q0odoUwNI9SxkCzJSlim8PmrV54=" + } + } + }, + + { + "path": "com/google/guava/guava/23.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-23.0.pom": { + "sha1": "05ce65d93433db6ef45746bcfbcef6656698851f", + "sha256": "sha256-rCrEKnprTOUJfGyD4vu6y7t/mRPxtPmT6xcjoUR2Mk4=" + }, + "guava-23.0.jar": { + "sha1": "c947004bb13d18182be60077ade044099e4f26f1", + "sha256": "sha256-e6qA3yhBF+W5RbGbmNNnqF6nt4Ab01j/ZXlGw70bZZY=" + } + } + }, + + { + "path": "com/google/guava/guava/26.0-android", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-26.0-android.pom": { + "sha1": "93ab8d808bf758a89db28cd7d021e5b011883545", + "sha256": "sha256-9W1rX3ochpewh+BdqPY5Xtpm/kH+Jf5SBtS3SIMv8Ac=" + }, + "guava-26.0-android.jar": { + "sha1": "ef69663836b339db335fde0df06fb3cd84e3742b", + "sha256": "sha256-HQROu4Zu8It9BOmYtCYMm1L6tubWto0geFlIa7NobNU=" + } + } + }, + + { + "path": "com/google/guava/guava/27.0.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-27.0.1-jre.pom": { + "sha1": "a6ccf3af1d34509e5fb1f5bff675b9ae707ad628", + "sha256": "sha256-ao3QQfI6a7FKhuRA/MuZNTe2InE1eg2sCjyw/zkVjzY=" + }, + "guava-27.0.1-jre.jar": { + "sha1": "bd41a290787b5301e63929676d792c507bbc00ae", + "sha256": "sha256-4cgU/QRJKifDjgMX6r6qGz6VDsgBAjnkAP6QrWyRB7Q=" + } + } + }, + + { + "path": "com/google/guava/guava/28.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-28.1-jre.pom": { + "sha1": "1207be0906dcc3e4984e8daa60b389e2ebd0a7cd", + "sha256": "sha256-dK8Ojdk1iUzpvLjihC3QABWBuEy2p3UcPvkxQBE5m6Q=" + }, + "guava-28.1-jre.jar": { + "sha1": "b0e91dcb6a44ffb6221b5027e12a5cb34b841145", + "sha256": "sha256-ML64uFJ70HxudH538akhIsLynVfONHRhpKVesm44LaQ=" + } + } + }, + + { + "path": "com/google/guava/guava/29.0-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-29.0-jre.pom": { + "sha1": "e40cdee0d70244df1e963daac53a16241aea4585", + "sha256": "sha256-kCfpNAmJA9KH8bphyLZfAdHR4dp6b7zAS/PeBUQBRCY=" + }, + "guava-29.0-jre.jar": { + "sha1": "801142b4c3d0f0770dd29abea50906cacfddd447", + "sha256": "sha256-sixftm1h57lSJTHQSy+RW1FY6AqgtA7nKCyL+wew2iU=" + } + } + }, + + { + "path": "com/google/guava/guava/30.1-android", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-30.1-android.pom": { + "sha1": "ebdad8309f46923e5a55cae98b7ef4fe62f9b44b", + "sha256": "sha256-R8xHfi++cHUmD8AcU7D46rSo7ANnQhyaLP2cF9VfbqQ=" + }, + "guava-30.1-android.jar": { + "sha1": "1e904cca24cdf3dc84d5fe4f73e12f1186e04380", + "sha256": "sha256-6gtaYrcHSC7uXPMFyPNe+Rz0zq/1BPARpcScQTVfV4E=" + } + } + }, + + { + "path": "com/google/guava/guava/30.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-30.1-jre.pom": { + "sha1": "4056c9385578a442117d1a68118def38087164da", + "sha256": "sha256-lkbUzVAJTUq+UH5VXT921340pMVWayL7Ew71XU676Sc=" + }, + "guava-30.1-jre.jar": { + "sha1": "00d0c3ce2311c9e36e73228da25a6e99b2ab826f", + "sha256": "sha256-5t0HL50/4CpGAGiDgL1CK9rBhMr2/iQYz90JNPCUMqo=" + } + } + }, + + { + "path": "com/google/guava/guava/31.0.1-android", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-31.0.1-android.pom": { + "sha1": "3865d0b312d464d20f04cc4093a6e18554676aa7", + "sha256": "sha256-E9TimYqsZS/G17nq8+SV+ilSv5jvaKLEmqXqoLvnMak=" + }, + "guava-31.0.1-android.jar": { + "sha1": "b400db548e748a1a60ba18543ad4fbb44a675a69", + "sha256": "sha256-lCWkI6TLnZ2wNWMAci2b2OY0z1OfKdl7uE9FfMzRbrg=" + } + } + }, + + { + "path": "com/google/guava/guava/31.0.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-31.0.1-jre.pom": { + "sha1": "d0ec1628dcc04e4835721416103672384ea3136f", + "sha256": "sha256-K+VmkgwhxgxcyvKCeGfK/3ZmRuIRO3/MPunCSkCy85Y=" + }, + "guava-31.0.1-jre.jar": { + "sha1": "119ea2b2bc205b138974d351777b20f02b92704b", + "sha256": "sha256-1b6U1l6HvSGfsxk60VF7qlWjuI/JHSHPc1gmq1rwh7k=" + } + } + }, + + { + "path": "com/google/guava/guava/31.1-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-31.1-jre.pom": { + "sha1": "03a6ac93765fbbc416179f7c7127b9ddddbf38d9", + "sha256": "sha256-kZPQe/T2YBCNc1jliyfSG0TjToDWc06Y4hkWN28nDeI=" + }, + "guava-31.1-jre.jar": { + "sha1": "60458f877d055d0c9114d9e1a2efb737b4bc282c", + "sha256": "sha256-pC7cnKt5Ljn+ObuU8/ymVe0Vf/h6iveOHWulsHxKAKs=" + } + } + }, + + { + "path": "com/google/guava/guava/33.4.7-jre", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "guava-33.4.7-jre.pom": { + "sha1": "5ad5843a58f352d42f92b4354d6509d2b7fd3c19", + "sha256": "sha256-hko9FWlOLeNWLLlEIjyG2iew+jaVsmDADEPCTQbM7N4=" + }, + "guava-33.4.7-jre.jar": { + "sha1": "c1f6ad95476208ef852f92919e7a9e22abd83a98", + "sha256": "sha256-46DHQLcu4O55odhvu4Ep0Rd2gi/BfGJFFP8d3AGGQrI=" + }, + "guava-33.4.7-jre.module": { + "sha1": "da818db22618951612505cd9f333b6a876a1a125", + "sha256": "sha256-YjqhR3cIq97mrRl0MWOxlxbNeEe9hqkmJMkER2Loi1c=" + } + } + }, + + { + "path": "com/google/guava/listenablefuture/1.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "listenablefuture-1.0.pom": { + "sha1": "3e71124fc28b44e29eaa31abbc267faef7ee7d40", + "sha256": "sha256-U4c8rya8HtilZ+psk5qyqqP0el4y1creld31CA0jI4o=" + }, + "listenablefuture-1.0.jar": { + "sha1": "c949a840a6acbc5268d088e47b04177bf90b3cad", + "sha256": "sha256-5K12B+XAR3xviQ7yaknLjRu03/tlC6tFAq/uZGROMGk=" + } + } + }, + + { + "path": "com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.pom": { + "sha1": "1b77ba79f9b2b7dfd4e15ea7bb0d568d5eb9cb8d", + "sha256": "sha256-GNSx2yYVPU5VB5zh92ux/gXNuGLvmVSojLzE/zi4Z5s=" + }, + "listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar": { + "sha1": "b421526c5f297295adef1c886e5246c39d4ac629", + "sha256": "sha256-s3KgN9QjCqV/vv/e8w/WEj+cDC24XQrO0AyRuXTzP5k=" + } + } + }, + + { + "path": "com/google/j2objc/j2objc-annotations/1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "j2objc-annotations-1.1.pom": { + "sha1": "b964a9414771661bdf35a3f10692a2fb0dd2c866", + "sha256": "sha256-8MmMVx6Tp8tN0Y3w+jCPCWPnoGIKwtQkTmHnCdA61r4=" + }, + "j2objc-annotations-1.1.jar": { + "sha1": "ed28ded51a8b1c6b112568def5f4b455e6809019", + "sha256": "sha256-KZSn63jycQvT07+2ObLJTiGc7awNTQhNUW54wW3d7PY=" + } + } + }, + + { + "path": "com/google/j2objc/j2objc-annotations/1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "j2objc-annotations-1.3.pom": { + "sha1": "47e0dd93285dcc6b33181713bc7e8aed66742964", + "sha256": "sha256-X6yoJLoRW+5FhzAzff2y/OpGui/XdNQwTtvzD6aj8FU=" + }, + "j2objc-annotations-1.3.jar": { + "sha1": "ba035118bc8bac37d7eff77700720999acd9986d", + "sha256": "sha256-Ia8wySJnvWEiwOC00gzMtmQaN+r5VsZUDsRx1YTmSns=" + } + } + }, + + { + "path": "com/google/j2objc/j2objc-annotations/3.0.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "j2objc-annotations-3.0.0.pom": { + "sha1": "45cbaa9d129a696907d1e5bc56c707b6a0043181", + "sha256": "sha256-I7PQOeForYndEUaY5t1744P0osV3uId9gsc6ZRXnShc=" + }, + "j2objc-annotations-3.0.0.jar": { + "sha1": "7399e65dd7e9ff3404f4535b2f017093bdb134c7", + "sha256": "sha256-iCQVc0Z93KRP/U10qgTCu/0Rv3wX4MNCyUyd56cKfGQ=" + } + } + }, + + { + "path": "com/google/jimfs/jimfs-parent/1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jimfs-parent-1.1.pom": { + "sha1": "2283f99a6aa73205190a20cfd35078e9f7d6cec9", + "sha256": "sha256-xxVVdR5X4O+RKHDorJYlrnglAqalucGcz4OyqX2LJr0=" + } + } + }, + + { + "path": "com/google/jimfs/jimfs/1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jimfs-1.1.pom": { + "sha1": "380866e1e495f899631b6e1d437e4b0a69e4faa3", + "sha256": "sha256-76huXNki8XtHL9/K5XI02NSsPhSLYlBzffzkVK96ekQ=" + }, + "jimfs-1.1.jar": { + "sha1": "8fbd0579dc68aba6186935cc1bee21d2f3e7ec1c", + "sha256": "sha256-xIKOKNfAqTCvk4dRCzutp9qlwE18Jadce4sIHxwlfd0=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-bom/3.7.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-bom-3.7.1.pom": { + "sha1": "7282c1af14379f0787f401e67c09c4b3ecf10469", + "sha256": "sha256-VO/ZoEaIbUfFmcAY4Q0g9UQE/NFewkKHnlO/lJPN2No=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-bom/3.10.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-bom-3.10.0.pom": { + "sha1": "4414dc69051a62cfdc674d840525f94f8979f408", + "sha256": "sha256-Mv8jB9r8ZY0LVbKthB1iWupWBrubAxZgUWXNaYBQMkM=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-bom/3.13.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-bom-3.13.0.pom": { + "sha1": "e4a331d742de7ff35e4b8f33489d330dde1182f0", + "sha256": "sha256-MqMXb3O+qXsSaI3x83x0ZZv9mADjFspspATubfolJZ8=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-bom/3.17.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-bom-3.17.2.pom": { + "sha1": "a76cc840d98f58670195da4c1ef10db4b9bc6bda", + "sha256": "sha256-825bwLwOawFR+oOxfNlNy1prjJ4bFJncCG7cqZ6ZuAU=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-bom/3.19.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-bom-3.19.2.pom": { + "sha1": "64293bb00ea01eb8f529139c1f750d44f5f035a9", + "sha256": "sha256-qErTHgcaxk2Xkszu8bs+pgvCyqnrlPMBhze8MuyP/+I=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-bom/3.19.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-bom-3.19.3.pom": { + "sha1": "da916302bb00f1b636836ed7e22e07a926a20565", + "sha256": "sha256-reWXGPSUmSZbLENaDxhQHyhc2cMQ3fBgQbXzkiiIykQ=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-bom/4.30.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-bom-4.30.2.pom": { + "sha1": "a57f9d18a5bc534a5adb37245106c51b95fb96f5", + "sha256": "sha256-61x34DXYID9qvZ9YiDKLg/zNOUcVDOYUbSpBQ8e+GTA=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-javalite/3.17.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-javalite-3.17.2.pom": { + "sha1": "f7e52b45910fac1344cc2a0f46f2e47381d49c64", + "sha256": "sha256-J+bAwzePT7YZB0sNtNd5x7/PY1KnRQu6C4qbY5wqJBI=" + }, + "protobuf-javalite-3.17.2.jar": { + "sha1": "cbd767e6363fa10925fb2e5497d4922f1726df2b", + "sha256": "sha256-pbDUljS0IJ/G5QBX8yz6S6lVR3ZE9Pzlcs5x1ml855I=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-javalite/3.19.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-javalite-3.19.2.pom": { + "sha1": "08d6de7fa9f0666e961aacf6f0250eb129f8abca", + "sha256": "sha256-abFFj6bMulbfyHGFk2S+trHnpJl0oR4auN0K0FAt66k=" + }, + "protobuf-javalite-3.19.2.jar": { + "sha1": "552a872042d49951a83b96b6eb5bcd9e8e4e42fd", + "sha256": "sha256-v8WYR0OSoBXAUNNL44u4TrOYaAk3TqxDdCDVFoAVDpQ=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java-util/3.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-util-3.4.0.pom": { + "sha1": "72b93eb17c1d5337ec0f4497313a14b315b9e76c", + "sha256": "sha256-icQwc+fqoOq6cqSjauG2v9/l2Bu50OFWruBeSnLePLg=" + }, + "protobuf-java-util-3.4.0.jar": { + "sha1": "96aba8ab71c16018c6adf66771ce15c6491bc0fe", + "sha256": "sha256-QYngvlqxXPIzD3CyT73HXKN1FPGIOI/OhYDOFqmmgFI=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java-util/3.10.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-util-3.10.0.pom": { + "sha1": "dd9dbd5ae7e5c6978bb725c387aaeb2dc07dfddb", + "sha256": "sha256-FGZgGC0HeYztBvqOiuO5+qkKpIzzxq38Z9uTcRiLpQI=" + }, + "protobuf-java-util-3.10.0.jar": { + "sha1": "a68c906db83e93babbb4024ce91e7441bb7598dd", + "sha256": "sha256-YZsLDcNEyxQeSTy+3FaHyPt8mF5gmhsDXmIb+rL4kCE=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java-util/3.17.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-util-3.17.2.pom": { + "sha1": "f35b7d98a11c9b4011698429ae78573b6a43e80d", + "sha256": "sha256-pdbSx0z4PI70vkUyuG8V7LHa+6rWTm2kJ8CQ3Uv2x70=" + }, + "protobuf-java-util-3.17.2.jar": { + "sha1": "88801cfefba20827bf5cd9f2cc4866bd8c766242", + "sha256": "sha256-hJklMzqsI/RvmhVKUuRBEXAX1Wc+BOVjaZO8w88ZJNk=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java-util/3.19.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-util-3.19.2.pom": { + "sha1": "ffbe4aa7661b9496311630efac0d29d801fec70d", + "sha256": "sha256-aPwVL+0eJZVEMPhSTV/T7hsxz1sRQcHcr8QJ9wPgnOw=" + }, + "protobuf-java-util-3.19.2.jar": { + "sha1": "fd73ae9d9f8121eca362d54279f73bb56219a594", + "sha256": "sha256-lYjWfbORsZz/tOxljZNlrFNisgiWYcFX/SfyE4DN0eg=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java-util/3.19.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-util-3.19.3.pom": { + "sha1": "bd870efcd988b538d6bafdeb59118b7d9813ad51", + "sha256": "sha256-ppBHoWNK/Z8Bz4fpsS1IWD2fnHF4ABYOSmdEz2mRq/A=" + }, + "protobuf-java-util-3.19.3.jar": { + "sha1": "3e6812cbbb7e6faffa7b56438740dec510e1fc1a", + "sha256": "sha256-HsBzPzLUn4GU+1RSYTA5NwYjcVc5VoptPJh9mngJwWw=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.0.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.0.0.pom": { + "sha1": "cd049bdc1680b9419359a73be694bac35a12942c", + "sha256": "sha256-5MevuPDXqhWTGNQqARzCybBSXYIJc8vKUxi+xsZJzsw=" + }, + "protobuf-java-3.0.0.jar": { + "sha1": "6d325aa7c921661d84577c0a93d82da4df9fa4c8", + "sha256": "sha256-oefWs+/BcDx8jxsV7Ph1fxfQrEHzLJpISBpcr2OMVds=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.4.0.pom": { + "sha1": "100272b91d96ed5b41df4bb11ba543140c36bd30", + "sha256": "sha256-g/F7qGxfoaFaOjyAMNTOQu8hwdObZdtswASo7rLFlAY=" + }, + "protobuf-java-3.4.0.jar": { + "sha1": "b32aba0cbe737a4ca953f71688725972e3ee927c", + "sha256": "sha256-3OfmazJFahsRmNoMr/OorLcVSGWDkeeYx5NpJB5kkKQ=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.5.1.pom": { + "sha1": "572120a85fac0f666c59e7ced0ced43a147e3f1b", + "sha256": "sha256-E4pcdWW55HBMnMT2LbuybOlxcYdwntrNoJCcSdSVDUc=" + }, + "protobuf-java-3.5.1.jar": { + "sha1": "8c3492f7662fa1cbf8ca76a0f5eb1146f7725acd", + "sha256": "sha256-teLZGBLRg8nwU//uvLzaA01N5meVIZQKGQZHFJZsLNQ=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.7.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.7.1.pom": { + "sha1": "2d2e87204d85c9f4ddb7cfe0cd798e91652d7ada", + "sha256": "sha256-sChElntxt/qpi3w284d/+a/nvWgalCG2CvpyydqZBJE=" + }, + "protobuf-java-3.7.1.jar": { + "sha1": "0bce1b6dc9e4531169542ab37a1c8641bcaa8afb", + "sha256": "sha256-IneerP4PM7nozv+jze9ZNcrk5T9zbgJ9kStwdSL+pkU=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.10.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.10.0.pom": { + "sha1": "db94f4481e9ef506c7dfdbb0d059892aa283ab1a", + "sha256": "sha256-tATBsJPsneqIjgLI3+hmJ1lYa5TvpPlwYc38G7+hWvA=" + }, + "protobuf-java-3.10.0.jar": { + "sha1": "410b61dd0088aab4caa05739558d43df248958c9", + "sha256": "sha256-Fh19YajLOXCJHCmVeHAv0HlkbgMjKdbCyr+ZjRkUN8k=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.13.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.13.0.pom": { + "sha1": "e51cac4aec96b0d1b06b31ae2f623e5edf8e3140", + "sha256": "sha256-4qMGgQJLFzgMHoafjhMUlB2KsugSuXCvKjPsO9oHkUs=" + }, + "protobuf-java-3.13.0.jar": { + "sha1": "c913f2b6021ca9606efba56d1a0d03e91e725e4c", + "sha256": "sha256-l9WydYQIaQwNwnYjhwdJKgtqTXEgYxG2xELNwmxZc/8=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.17.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.17.2.pom": { + "sha1": "a6581839d4a656adc852b8d2af28480847873ae4", + "sha256": "sha256-ch7vhL3SiO2jVir2Ju4ss0Jr6vWMU2lxC1T1HUhFaPo=" + }, + "protobuf-java-3.17.2.jar": { + "sha1": "d7eec2ad499b605f24d07f49bdcb41c801aafbfc", + "sha256": "sha256-z2l0Uvvn9r399lBDeXoWzWJ7HdQki+S1cvy3P4wDLMA=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.19.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.19.2.pom": { + "sha1": "31e701dc5d5daef432ef645933e3f7417dbd7fe7", + "sha256": "sha256-Cm6l31fYfjBClQUlIEMQuidYHPNR0kan7/LSG3tOFkE=" + }, + "protobuf-java-3.19.2.jar": { + "sha1": "e958ce38f96b612d3819ff1c753d4d70609aea74", + "sha256": "sha256-NEbL+hN5Uii8ZUm5GkCfJ832kT0cjwPg+ZVymIYjoEs=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/3.19.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-3.19.3.pom": { + "sha1": "5e964f99b80254406fb4de10904cdea7499097fb", + "sha256": "sha256-aPOpzESWPjEIOZPBCknr6xZS1+9jz1Fvyq/MMllfOo4=" + }, + "protobuf-java-3.19.3.jar": { + "sha1": "4b57f1b1b9e281231c3fcfc039ce3021e29ff570", + "sha256": "sha256-RAZC47UmZj0cFbSiBxwmlvKiXwMTMqw+tD9jlp2Bmz0=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-java/4.30.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-java-4.30.2.pom": { + "sha1": "ef21d90610cf5333b2803371564b5b39c20e13a1", + "sha256": "sha256-eeMkDT4MXFhgm42n09gtcmUTa9pq6pkfkvxNrHzNoVI=" + }, + "protobuf-java-4.30.2.jar": { + "sha1": "e4d6256d911424343d05b3c0b853446b16e908a4", + "sha256": "sha256-DzpOkmTbB87EKfKmimYDDpt0hyd7doY83Q6SOM7OJJs=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-lite/3.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-lite-3.0.1.pom": { + "sha1": "6b899d982947441869b3653aaf9942ec9198db8e", + "sha256": "sha256-1ihqSVUCHIzdnVHCrg2gRPdrLsraTyBuR5XUPAvrSI0=" + }, + "protobuf-lite-3.0.1.jar": { + "sha1": "59b5b9c6e1a3054696d23492f888c1f8b583f5fc", + "sha256": "sha256-FBM5PbhOSt73mymX2d/rR5PY+T0Zb4NHgI0VcR8Lxpw=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.0.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.0.0.pom": { + "sha1": "fa9a080c3ed2997cd84df8effd6cb8af9e4ed020", + "sha256": "sha256-ky5rq5okp7yVi7234p4E0IO0c9EcS6P6sem3FJV58nI=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.4.0.pom": { + "sha1": "5d9f2b22310a535c4597b4ed3691e000221b3a17", + "sha256": "sha256-JJCcVShCwOt6THadYxpDy+9amhDBZA8r29HqFJxXOkc=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.5.1.pom": { + "sha1": "649fbc4b83872f8af5453f9d47a09b8017c2ce57", + "sha256": "sha256-jNQ0OEcD9eVbyTCS6Qu9+kGyAYLoE4FKAEmdLBZ3Oug=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.7.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.7.1.pom": { + "sha1": "3467c91a6e547f39c9d6208fa830d0f7036a1005", + "sha256": "sha256-okoYlEoTlHIfIfgBjzHLuArffGT9vTv7+RsyhSJLGvI=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.10.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.10.0.pom": { + "sha1": "92e4373db52230866c0352b343e3f83f25812c77", + "sha256": "sha256-bdhKUIEl//3vvVg/rhK/FmqpAlEbVwylT6nvpF9t/oA=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.13.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.13.0.pom": { + "sha1": "a0090ecff82d3fb70b6e026faa6a8979d081a769", + "sha256": "sha256-boZZFGt/A68V4lhnW186agRRILpoiF7A2en69gM/c9I=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.17.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.17.2.pom": { + "sha1": "20e0334d443e10f0b065b4ee9c638b0f208a1b28", + "sha256": "sha256-jZdmS7ws+FO6qJwo4HCC4uSufE0tyommOyi9oJPN7oY=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.19.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.19.2.pom": { + "sha1": "2b5c5e43c61559a58f1c74a527d0cbf6f905a9bd", + "sha256": "sha256-JTlYGQl7ysg6Q+rIvCp2aByFhprJeZERIE1lDX3wwIY=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/3.19.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-3.19.3.pom": { + "sha1": "5d1e98e81b70b56dc077188c92c3eafd164ad1c4", + "sha256": "sha256-PWC8x41sTz2fdjEAxLUtbV8auO0cL9W1byU0JDhUaZM=" + } + } + }, + + { + "path": "com/google/protobuf/protobuf-parent/4.30.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "protobuf-parent-4.30.2.pom": { + "sha1": "eab909747594276b922185b3f06a31c9f5d3e189", + "sha256": "sha256-mzxGtPTObVkwrFIQxwb0GvE+zzCOKJib9x8RnpjZwi8=" + } + } + }, + + { + "path": "com/google/truth/truth-parent/1.4.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "truth-parent-1.4.4.pom": { + "sha1": "9367b3ee84de3d245ab3ad7ee34061d9cbe0b84c", + "sha256": "sha256-Kclz8PzjFysbGLx8m+L73H6h8PoIAWOeR36ltm7rtJ0=" + } + } + }, + + { + "path": "com/google/truth/truth/1.4.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "truth-1.4.4.pom": { + "sha1": "c7b43cfaf8861dcd0ec37c7f36f08440253a861b", + "sha256": "sha256-3vXdxr+jSkfcM2ldtzCbisoU/VVp39bRGum8CEMY7iY=" + }, + "truth-1.4.4.jar": { + "sha1": "33810058273a2a3b6ce6d1f8c8621bfc85493f67", + "sha256": "sha256-Ushs3a3DG8hFfB4VaJ/Gt14ul84qg9i1S3ldVW1In4w=" + } + } + }, + + { + "path": "com/google/zxing/core/3.4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "core-3.4.1.pom": { + "sha1": "5e394e62598655ec542a69b8426abe8fd6fef182", + "sha256": "sha256-saD67kJKPHdVNMgfoW9aXVqmXKQOathX9pYbMz6jn9M=" + }, + "core-3.4.1.jar": { + "sha1": "1869da97e9b2b60b5ff2fcaf55899174b93ae25d", + "sha256": "sha256-rPowM4wmJntsq2ZVCcIlG7lug1C/ytidWblJuncBH40=" + } + } + }, + + { + "path": "com/google/zxing/zxing-parent/3.4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "zxing-parent-3.4.1.pom": { + "sha1": "e95c34647f02558c0e02d79dc6784bae52c89085", + "sha256": "sha256-ICgf4RuOS9TBfe7FYURNFXW9Uy/stz7Pa4nv2tfNiTg=" + } + } + }, + + { + "path": "com/ibm/icu/icu4j/53.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "icu4j-53.1.pom": { + "sha1": "d13d24dbfb9d48175ebf4fae9cd7d5b7c5937f64", + "sha256": "sha256-oHANhO/nzDEDVXvQUi+UQ2gblrqVWcS7f53JvzUkNIU=" + }, + "icu4j-53.1.jar": { + "sha1": "786d9055d4ca8c1aab4a7d4ac8283f973fd7e41f", + "sha256": "sha256-43pEZ7rFzesCxcS45QY9L05ntp48ffbWthDxMYVXK6s=" + } + } + }, + + { + "path": "com/intellij/annotations/12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-12.0.pom": { + "sha1": "aaa1796465aa46f478176c06456397418b34d2b3", + "sha256": "sha256-+vgt4NwC4MCuMnzWU/NyVUlrLlP84oCzq0yzRVOokIY=" + }, + "annotations-12.0-excludingjsr305overlaps.jar": { + "sha1": "fc258d8bd79fb63d0914a56508f2a0d344212094", + "sha256": "sha256-kMbINkyMlIqZg3WcqfQenka2AiJm+F+wiKmhVDEc1FY=" + }, + "annotations-12.0.jar": { + "sha1": "bbcf6448f6d40abe506e2c83b70a3e8bfd2b4539", + "sha256": "sha256-+KsTsUvggP4vYX+Q5VWZdg5KG03u6lxZXfY9DWN17W0=" + } + } + }, + + { + "path": "com/parse/bolts/bolts-tasks/1.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bolts-tasks-1.4.0.pom": { + "sha1": "db60026ffef8aae5e2bac952e74a106a784ea5ae", + "sha256": "sha256-jLAHhhFiwbnlDdjsB7fOev/qj+duB6xAjO2BzcuDEuA=" + }, + "bolts-tasks-1.4.0.jar": { + "sha1": "d85884acf6810a3bbbecb587f239005cbc846dc4", + "sha256": "sha256-m8UePsqFOp3y+6xiwjF9ssnqYZuVtVMcMEQCQ9qszuI=" + } + } + }, + + { + "path": "com/squareup/javapoet/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javapoet-1.8.0.pom": { + "sha1": "60f3a32fabfe6c4b7572d8e2d94010ea44af4843", + "sha256": "sha256-s3YPQPGec1ss1Bj3ZWM12SbR2UE+EJgqHJaVtk7btkc=" + }, + "javapoet-1.8.0.jar": { + "sha1": "e858dc62ef484048540d27d36f3ec2177a3fa9b1", + "sha256": "sha256-jhCMkgJ7tCgZbxD6Ec/75Yn3ZIpq8gFtZSJ5OF/f14k=" + } + } + }, + + { + "path": "com/squareup/javapoet/1.10.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javapoet-1.10.0.pom": { + "sha1": "111612d623e9d2047798f13324fcb29966f080e3", + "sha256": "sha256-FpA0CiIiefLLrfNz6Igm+iD388w+wCUvNoGP7TJwGrE=" + }, + "javapoet-1.10.0.jar": { + "sha1": "712c178d35185d8261295913c9f2a7d6867a6007", + "sha256": "sha256-IO9LguQ/98ZSKBohMTzzuUEJJGet0/pzUJwm9pae/as=" + } + } + }, + + { + "path": "com/squareup/javapoet/1.13.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javapoet-1.13.0.pom": { + "sha1": "17b682b96d0d2abfdd92f6a99fbab40dd4f4d360", + "sha256": "sha256-VKNPqFAqRryQ79tJJiYAWR+oC/mjT1pMeYMRrsFsqXc=" + }, + "javapoet-1.13.0.jar": { + "sha1": "d6562d385049f35eb50403fa86bb11cce76b866a", + "sha256": "sha256-THUX6EinGzbQadErs79Gpw/UzaMQXYIrDtLhnAC2kpE=" + } + } + }, + + { + "path": "com/squareup/javawriter/2.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javawriter-2.5.0.pom": { + "sha1": "d932f2476f65ecd95dcd6fd8c568b3f466f6a482", + "sha256": "sha256-4avX8RFs9eDFmUdpPiGJII7JQpayozlMlZ41EdOZp7A=" + }, + "javawriter-2.5.0.jar": { + "sha1": "81241ff7078ef14f42ea2a8995fa09c096256e6b", + "sha256": "sha256-/PsJ+w6gqpfTz+fqeSOYCBNI5GjxJrNgPLOAPyQBl/A=" + } + } + }, + + { + "path": "com/squareup/okhttp3/okhttp-tls/4.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okhttp-tls-4.11.0.pom": { + "sha1": "93518ebb24213d6dcbde8e0c21913e40672f80c3", + "sha256": "sha256-0SYiMcXxtb/bVe/7+SXas1/dWlNFEvhsSd3HIGY/IEg=" + }, + "okhttp-tls-4.11.0.jar": { + "sha1": "5ac74e5e234dc2d5cd434ac87df377858a2dcf40", + "sha256": "sha256-SuTHCSwBeBhSRQabkSdGX9V8dkSF81ev7USlxIoFNf0=" + }, + "okhttp-tls-4.11.0.module": { + "sha1": "e790e6e5d0e88f9fc0c70df981e86daf29f411fa", + "sha256": "sha256-2m5O5qfdybnUZusr5ScBAmsXms2wClyso08uXTdeSMI=" + } + } + }, + + { + "path": "com/squareup/okhttp3/okhttp-urlconnection/4.9.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okhttp-urlconnection-4.9.2.pom": { + "sha1": "b704c6fcaf3d004756d12e06a283a5c692f5d6bb", + "sha256": "sha256-UBmysFh3r2xOCXbsLvBlbOYXYFSfFT9u9pAhWbLbNsQ=" + }, + "okhttp-urlconnection-4.9.2.jar": { + "sha1": "3b9e64d3d56370bc7488ed8b336d17a8013cb336", + "sha256": "sha256-Dhw5qolmj5YlySB6zUqHhNyGitDVogEdnhXgZ087fRE=" + }, + "okhttp-urlconnection-4.9.2.module": { + "sha1": "9a011fa1f74d9e8821940a4908e8bc57dbd4687e", + "sha256": "sha256-pFI5YUSIoTl60VGA29RWy1egbqoKmuvlTlEqIZWbBwM=" + } + } + }, + + { + "path": "com/squareup/okhttp3/okhttp/3.9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okhttp-3.9.1.pom": { + "sha1": "7a34bfe0899e4b1a4b84f4e9e79c221bd37e9808", + "sha256": "sha256-vMGh4M3Bz97N3l/nsYKFf8sfQk1H6bVvYQ4TX5z6XO4=" + }, + "okhttp-3.9.1.jar": { + "sha1": "84b4b7d1c4a238e7899972b7446c250691e65f1f", + "sha256": "sha256-oNAQF6QruiblB/xtRIuzblNvS25hL3xC3jC72sK3eF4=" + } + } + }, + + { + "path": "com/squareup/okhttp3/okhttp/3.12.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okhttp-3.12.1.pom": { + "sha1": "975e0606bfccdffb6dcf5ccb6a823f70be6be18d", + "sha256": "sha256-lAZ2sSSo7qitdXb5vgcsgtfcf+NQrYlaNggfDJOumaY=" + }, + "okhttp-3.12.1.jar": { + "sha1": "dc6d02e4e68514eff5631963e28ca7742ac69efe", + "sha256": "sha256-B8PYLKfq9HIvALLagH3Hhg9haa5gz+3PXUAhj5CICkY=" + } + } + }, + + { + "path": "com/squareup/okhttp3/okhttp/4.9.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okhttp-4.9.2.pom": { + "sha1": "84e07aab64e999d6f956e27e528f776882adbda6", + "sha256": "sha256-cqIz6uiltaDX3WJnmMbwBN/Suu8hbxOW6CF9nLh24m4=" + }, + "okhttp-4.9.2.jar": { + "sha1": "5302714ee9320b64cf65ed865e5f65981ef9ba46", + "sha256": "sha256-Oy7ht2jB3yjDDS/ms43aTSxRkhDjDKPSeVBhjFY8kt4=" + }, + "okhttp-4.9.2.module": { + "sha1": "b4819aff8283ee285c4d026cfd274fa56ea7d8a7", + "sha256": "sha256-wcHBXp2HkulalwuwT39PsaCc2WUhpwpUVKTUy1U7cYw=" + } + } + }, + + { + "path": "com/squareup/okhttp3/okhttp/4.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okhttp-4.11.0.pom": { + "sha1": "723ab52cf1ac14363cb36ab084b0af810c095bef", + "sha256": "sha256-ei1Cezixfgdtpk7o0hAuZIiNyyOK7l4tukp3UslKP94=" + }, + "okhttp-4.11.0.jar": { + "sha1": "436932d695b2c43f2c86b8111c596179cd133d56", + "sha256": "sha256-7o9r1s0SVwE9dIMw9MoUdjip+8tS+ziNWsk89TQIdF0=" + }, + "okhttp-4.11.0.module": { + "sha1": "db24464b688785ba9c5765df8b5508a9b8994b40", + "sha256": "sha256-VnwltR13eWF0Q5GE11JBK6l+2f22X8cYQNvFVjvrj6g=" + } + } + }, + + { + "path": "com/squareup/okhttp3/parent/3.9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "parent-3.9.1.pom": { + "sha1": "e55ee2b70a7a64431236eac139096e369db69bb6", + "sha256": "sha256-9RXgvmt8mjVDBQ0qoWJ8jOSTuxTZQTCKPXy6vu4DFVM=" + } + } + }, + + { + "path": "com/squareup/okhttp3/parent/3.12.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "parent-3.12.1.pom": { + "sha1": "a118bcb30283e6df0fa33574d3eeb69804e0f3dd", + "sha256": "sha256-9XxvCJvu7bW3PNrKcWeZjOdFBa6aPZrqzXLg67D79Lw=" + } + } + }, + + { + "path": "com/squareup/okio/okio-jvm/3.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-jvm-3.2.0.pom": { + "sha1": "749720d97fe3dc16e0d84a05628f4c818b375a85", + "sha256": "sha256-XEUflKdr6oYbbvK/hOj1cgBUWWjIZVWr3+0Tx8otSJ0=" + }, + "okio-jvm-3.2.0.jar": { + "sha1": "332d1c5dc82b0241cb1d35bb0901d28470cc89ca", + "sha256": "sha256-tkK670xXAFXeTLPRZnsrFtztkB/4BmNFoGNpGqBgJaQ=" + }, + "okio-jvm-3.2.0.module": { + "sha1": "09f966bc7d27d5151fa3367e637e098cf7cf4179", + "sha256": "sha256-p3jzkIXtar/NaHESmGxjhapXrC2IQLIdlGs8IJXzDqQ=" + } + } + }, + + { + "path": "com/squareup/okio/okio-jvm/3.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-jvm-3.11.0.pom": { + "sha1": "bdec62df95cb5df0d8470ccc4294523de61e022a", + "sha256": "sha256-GnMR4mRILliufUvE1wgIfIhIz+RGC3Zyv19Qs09RS6k=" + }, + "okio-jvm-3.11.0.jar": { + "sha1": "e19f243ee46335ab089613a25f3ea1f65df285b1", + "sha256": "sha256-LSJUGhkvkBpFyVJ0Vn1pV6we1hPUnkHbTl5dhhOc8CI=" + }, + "okio-jvm-3.11.0.module": { + "sha1": "336ee51298320b6f97545c38320382d1e97df9ed", + "sha256": "sha256-AOYfaQETdeeCYvf1PnoKcfjro+ScCUHVI9tQfumvdQY=" + } + } + }, + + { + "path": "com/squareup/okio/okio-parent/1.17.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-parent-1.17.4.pom": { + "sha1": "18eae90824681bf12cb8359b0a1ecef9149de1e3", + "sha256": "sha256-qw4527w6z4u806DLf0++HV+PAXTVHXZRwjGRIlh75hE=" + } + } + }, + + { + "path": "com/squareup/okio/okio/1.17.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-1.17.4.pom": { + "sha1": "cda5234d77b44144201f4c77ab46f79422884d81", + "sha256": "sha256-IK30Thh7VXt2Bab4sbqCuljg1Hl7w5f2oOBeTtlUlcY=" + }, + "okio-1.17.4.jar": { + "sha1": "50077892fbc179bc69c0ac488442fd3a864e897f", + "sha256": "sha256-14+sWIRY/AmebILpH+XwN1xnQ0YmRRo6d3csZdnu6Fs=" + } + } + }, + + { + "path": "com/squareup/okio/okio/2.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-2.8.0.pom": { + "sha1": "db3559887f215bc3cdbb4ad37d246d04fbbdff8e", + "sha256": "sha256-JiFAaP1Mf0N6yP6FdoAqmp3BUcRXWCgHrZryry6MlIE=" + }, + "okio-2.8.0.jar": { + "sha1": "49b64e09d81c0cc84b267edd0c2fd7df5a64c78c", + "sha256": "sha256-RJawbnOYL83YpTk/RuXfLOL6RGXfWJVFTKxooy8Ju8g=" + }, + "okio-2.8.0.module": { + "sha1": "8ad44b1be256f93994505ebed213be0d8780aab2", + "sha256": "sha256-F7qrcnA4ml+mOrEoEYZNCgDzgWEbxOsEL6G9WRjtCWU=" + } + } + }, + + { + "path": "com/squareup/okio/okio/2.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-2.9.0.pom": { + "sha1": "eaa6725bd15c851530d55f235208568dfb399bb8", + "sha256": "sha256-8uE4WwBL178AhUXDQFuWcDXxJkJUroom+1Pynhxd9/Y=" + }, + "okio-2.9.0.jar": { + "sha1": "0dcc813b08ce5933f8bdfd1dfbab4ad4bd170e7a", + "sha256": "sha256-uSVcFjt+3AuEBlWdZlecbDLeoB9pGUO6xdP9vRD2m0Q=" + }, + "okio-2.9.0.module": { + "sha1": "520c235dba7958456a273ae5fdf3d925cbc8a81a", + "sha256": "sha256-ZbeMGrIdx2QHmNx2fFgJkJFteozPQY2Ksi/jrKC0iO4=" + } + } + }, + + { + "path": "com/squareup/okio/okio/3.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-3.2.0.pom": { + "sha1": "85284557b21661be1c4983910fef4e35532e1e34", + "sha256": "sha256-i0b1jZua6xF4Nh1YpoZfTa1mWTDF/3tV4LqmHvOpcqE=" + }, + "okio-3.2.0-all.jar": { + "sha1": "22e2a4f865203033fc0f35fd5892a213d21a4710", + "sha256": "sha256-Uf+6hylix9O+0SjfKbDeqz7qMfwjfAoMgVjcmQqEh44=" + }, + "okio-3.2.0.jar": { + "sha1": "afd4b7185a6976c2ac6bfb7b126c257ee9b3e765", + "sha256": "sha256-3KkyyyAptsniZ3D4fbCLFNSB/+gTGlnzaaI4XBG+Ti0=" + }, + "okio-3.2.0.module": { + "sha1": "32c2edb224d6eeaee24f8086e3ac41b92138fadb", + "sha256": "sha256-aB9c7BcN5FuVST6e5wWGjrNa34mO4G+W4i0ZclDBsQQ=" + } + } + }, + + { + "path": "com/squareup/okio/okio/3.11.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "okio-3.11.0.pom": { + "sha1": "a7c64ef7befbce5a774ec44151a30468d5bb6e80", + "sha256": "sha256-ehJQ9xiWx+cJPIDhNvgl4dl4kjHyPaHHk8TirhKrjA0=" + }, + "okio-3.11.0.jar": { + "sha1": "b9eacde07c931796546811f87107c973e2243831", + "sha256": "sha256-WVmIAinYpCJlKUnXxrTcNaTVOdsIU7HVz68oa2a7cts=" + }, + "okio-3.11.0.module": { + "sha1": "bb485afb7bc7e114a545b561f9db7770b48e2274", + "sha256": "sha256-lQBY+qz8BqADxPIBUt+FR0InnLNh4EJ1FNjE5DdA3ew=" + } + } + }, + + { + "path": "com/sun/activation/all/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "all-1.2.0.pom": { + "sha1": "9b1023e38195ea19d1a0ac79192d486da1904f97", + "sha256": "sha256-HYUY46x1MqEE5Pe+d97zfJguUwcjxr2z1ncIzOKwwsQ=" + } + } + }, + + { + "path": "com/sun/activation/all/1.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "all-1.2.1.pom": { + "sha1": "efca912f55f71e63ab2eb82552519b66dd123f98", + "sha256": "sha256-NgiDv2RIbs7xYbjygvZQNTbdGmcNU6Coccj7IBcOZ5U=" + } + } + }, + + { + "path": "com/sun/activation/all/1.2.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "all-1.2.2.pom": { + "sha1": "ed2bb1aab38540cd4262061c5ea5fc6ed0933e94", + "sha256": "sha256-GXPUmcwsEmSv8tbQUqHHFq5hPQGK4cL2EN1qTRwkV44=" + } + } + }, + + { + "path": "com/sun/activation/javax.activation/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javax.activation-1.2.0.pom": { + "sha1": "bdb776ae9b888b7ad8f9f424b9e67837eae916c5", + "sha256": "sha256-+Hm26UWFTGkAsNvuHIOE16s95+FX/XrISTdAXEFtKl4=" + }, + "javax.activation-1.2.0.jar": { + "sha1": "bf744c1e2776ed1de3c55c8dac1057ec331ef744", + "sha256": "sha256-mTMCsWzXBW8h53nMV30XWoELtJAO9zzY+/K1D5KLqc4=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons-runtime/2.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-runtime-2.21.pom": { + "sha1": "04c234cf684a202c5c9bb7f0a198ba97e958f8f4", + "sha256": "sha256-6+cTe1+/0FBUX5p/PzOa5VvrC1N1UHG0/WKqAkxibRw=" + }, + "istack-commons-runtime-2.21.jar": { + "sha1": "c969d8f15c467f0ef7d7b04889afbe7b5d48e22f", + "sha256": "sha256-wz5noIBwlfAqDi2hOUEt18T5zBpMBUs+Q0+WgxupUPQ=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons-runtime/3.0.7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-runtime-3.0.7.pom": { + "sha1": "8eb4c6b0e9b0a1fadf53fce8b3fc8415b00469ef", + "sha256": "sha256-bXBORQqBakW86Aa6IsIv6D2Ojc96cVF2A95jChcmgJ8=" + }, + "istack-commons-runtime-3.0.7.jar": { + "sha1": "c197c86ceec7318b1284bffb49b54226ca774003", + "sha256": "sha256-ZEPhC6LiWfuCHZtr7PENtTFihfwwxTzsnXsZo4d+f98=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons-runtime/3.0.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-runtime-3.0.8.pom": { + "sha1": "fd5dfcff9a1f1e7e4371c6a998b74ee0e52bdc4a", + "sha256": "sha256-wuAU00y4TtKH0GSYbEXDBaQSQiinM37M9sQh0U1wjxw=" + }, + "istack-commons-runtime-3.0.8.jar": { + "sha1": "d6a97364045aa6b99bf2d3c566a3f98599c2d296", + "sha256": "sha256-T/q7Br5FSgXkOY4gx3+itjCNS4jfvvfKMKdrW31VBe8=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons-runtime/4.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-runtime-4.2.0.pom": { + "sha1": "e00df2d2db8d5459e86a5ef542269ed56c278c66", + "sha256": "sha256-G4ib6z0u5EuS9ydgDSaDnlvjKma9uIwUscGpksqxpXM=" + }, + "istack-commons-runtime-4.2.0.jar": { + "sha1": "131351109ff27c4aa0958a988bb273e879687232", + "sha256": "sha256-IQJbeglu+T903mWcG+WZD6DCTlmg+YpwbjkucIhyX/Y=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons/2.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-2.21.pom": { + "sha1": "125168cc27946f32374cef253dbe607486aa3919", + "sha256": "sha256-wwcSd/ibFimCYGtOZckgdyEu+2y/G9w2XFG9C1esgYw=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons/3.0.7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-3.0.7.pom": { + "sha1": "1d26fac59c5bbccf2666921ea76d24778b7df5dd", + "sha256": "sha256-b4PTyF/cqe8kAQyy9lKqsaUIv/YzHAh7YNAwF4K3jG8=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons/3.0.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-3.0.8.pom": { + "sha1": "3835dae3b9e529039b70bce883a1c7438713c295", + "sha256": "sha256-oPBRfoUS8PvMe4KVwS9lZqPQwthtZVY53GYu+MDH6+U=" + } + } + }, + + { + "path": "com/sun/istack/istack-commons/4.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "istack-commons-4.2.0.pom": { + "sha1": "ddebb110777b1340dae08ba3779042ea5b45144e", + "sha256": "sha256-ZnNmONuyxXtoqua8CxLPe8Yq7w1pMedWfMV7zinYSXw=" + } + } + }, + + { + "path": "com/sun/xml/bind/jaxb-bom-ext/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-ext-2.2.11.pom": { + "sha1": "158223fd61f720cf172e1984147b62aa8f2bdf4e", + "sha256": "sha256-sl4Gk94hy5KwOaLkYI85ZZD7h3MQjxDW3J8UZfA7W+k=" + } + } + }, + + { + "path": "com/sun/xml/bind/jaxb-bom-ext/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-ext-2.3.1.pom": { + "sha1": "1f45034ee74332e54404adc6a539307c879fad5f", + "sha256": "sha256-wiBPVLQ1k4CMmvZQKGXucWeYIxVtq97zQecdNWYseqA=" + } + } + }, + + { + "path": "com/sun/xml/bind/jaxb-bom-ext/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-ext-2.3.2.pom": { + "sha1": "7a92d12e31a67bae498f840c8f7c334a4fee0fbb", + "sha256": "sha256-Gn3sKyfn4FV0TNuM8bkN70/Uc6zRuATv8JgTk1iVm9c=" + } + } + }, + + { + "path": "com/sun/xml/bind/jaxb-bom-ext/4.0.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-ext-4.0.5.pom": { + "sha1": "651882f97b92c0ae166ecdfbb23678993c9e3229", + "sha256": "sha256-vDnKsUxxStmKeyoG96ybM3bgeVVCaa03KhfXEd9Yp6E=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-parent/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-parent-2.2.11.pom": { + "sha1": "9496f6747e6d5ab5da869941dea4143969122b91", + "sha256": "sha256-tTAbcRwBVH5XG2Ffmggy7lJdNKTOM3L1OQeofPILBIA=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-parent/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-parent-2.3.1.pom": { + "sha1": "7882377e82eb8f5c89164b71b49e8b7f33820b92", + "sha256": "sha256-9pnvN+x5ZuKEdC38qDB1IhF5BBqaSa73mRKAGSYERi0=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-parent/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-parent-2.3.2.pom": { + "sha1": "05d96ada92105844d8a2eedfb63bf782a712cc0a", + "sha256": "sha256-IN1tw0q3VJrEDaHYLpIiLsQ0etDsDLEY72xXA77VOhg=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-parent/4.0.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-parent-4.0.5.pom": { + "sha1": "537859c01d9f1f80266cda7eb0f46c7e16a7fd7d", + "sha256": "sha256-xrVu/pzFnzx4NT0oTFXo3PFivoX4IrJs+BAjxvkI9/c=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-runtime-parent/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-runtime-parent-2.2.11.pom": { + "sha1": "dd7834f63d08408fde8fbb58f1ea557bbb31d444", + "sha256": "sha256-IZGMuOTtpn8kJR6Qml2BZyIB2TYExP+/M1Iq2DaljpA=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-runtime-parent/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-runtime-parent-2.3.1.pom": { + "sha1": "503a774712f50e6b6e6ffc76f6e73f51e9e2f889", + "sha256": "sha256-tWOD601DSYsUXTeeKpPV/NzY/5KR+JtYuCy5Fljb8Uw=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-runtime-parent/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-runtime-parent-2.3.2.pom": { + "sha1": "b9916bbbf37042807a649a2a7fcb6b97e193c28a", + "sha256": "sha256-sk+NUfGEpovBuG1IwOPP7+shpE7eHF9zA8WK4EiFM+w=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-txw-parent/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-txw-parent-2.2.11.pom": { + "sha1": "3fdc8adc054c5418bd76858dce64350bb8655ac5", + "sha256": "sha256-mjmMaZpdPztW8QVsaIkO3iXZShLZjqvCpHNzPmqyNiw=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-txw-parent/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-txw-parent-2.3.1.pom": { + "sha1": "dbac3b31b97442b12772e4ff1fbe00e903161e43", + "sha256": "sha256-eoRz6TVQSEHGBmhthOnAF6c5rCwUT95oeqADp91E3n8=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-txw-parent/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-txw-parent-2.3.2.pom": { + "sha1": "1f872b3cb7c49c35501651aab063659a9775f06a", + "sha256": "sha256-tV0++psVj0g6MOkseMy2APkzFHM9CJ66m3RDbwGzFKQ=" + } + } + }, + + { + "path": "com/sun/xml/bind/mvn/jaxb-txw-parent/4.0.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-txw-parent-4.0.5.pom": { + "sha1": "5826d93f394ec154ea14512ed0b8bfa98c4b4b2c", + "sha256": "sha256-yu7EM19YqOOcl+oK6JvqnCtJro2bD82SUjjgKNOESU8=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/FastInfoset/1.2.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "FastInfoset-1.2.13.pom": { + "sha1": "bc1ac953addb710ec08dcca6465bb1f6fcfd7ee9", + "sha256": "sha256-t1BeDs99SVtNqj5lafcWEdGnidxTHL2SogJZIv8mVdM=" + }, + "FastInfoset-1.2.13.jar": { + "sha1": "098f56b9354e27bd2941cc5d461344e240ae51ae", + "sha256": "sha256-J6d9uQnzwoM8Cxo3xVrx2wYEURitLu2WzlZ7ZjK84Dg=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/FastInfoset/1.2.15", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "FastInfoset-1.2.15.pom": { + "sha1": "945cf1f4467c72add88309fb05cdf5e340b569f9", + "sha256": "sha256-u8eWq4Smd4p1HC7/ETYHir0rTTW1BHBigE81gu88Qsg=" + }, + "FastInfoset-1.2.15.jar": { + "sha1": "bb7b7ec0379982b97c62cd17465cb6d9155f68e8", + "sha256": "sha256-eFhh2xHKG9DRlWaCuXStc+sZzT4BpLP6gtYuypchCuw=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/FastInfoset/1.2.16", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "FastInfoset-1.2.16.pom": { + "sha1": "e4d2eed02e0e0a9f4c892e6d43bdf3dfdc1fa2ee", + "sha256": "sha256-4UfSWKtuZpH3BZmpUkAObmx1WPjJwCjb4b4jF4MI6DA=" + }, + "FastInfoset-1.2.16.jar": { + "sha1": "4eb6a0adad553bf759ffe86927df6f3b848c8bea", + "sha256": "sha256-BW86HhRECfIe0Wr8JoBfWOmiHz/OFUPELUAHGdJQxRE=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/FastInfoset/2.1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "FastInfoset-2.1.1.pom": { + "sha1": "85420a8093b72919bd47f0e4ca8abf2274c3797c", + "sha256": "sha256-Sf3LDhtzh1Ln2kuljnpenkhskI8ZuFNTVQadrvq3On4=" + }, + "FastInfoset-2.1.1.jar": { + "sha1": "650c84348bbcf4461bd24d38e67f7a7ce687de24", + "sha256": "sha256-ddZjXgkQHvFUXZpZu02VJOxr8VJGVAr1Q1dQ8nFhJ2U=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/fastinfoset-project/1.2.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fastinfoset-project-1.2.13.pom": { + "sha1": "db60407ed97748ffda8b69aee0dd10a82851640d", + "sha256": "sha256-/QhXiZ8QZ+AofC/9keOPlnomvUBbg6NGUMJ0LO6fwmE=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/fastinfoset-project/1.2.15", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fastinfoset-project-1.2.15.pom": { + "sha1": "a40c0d8a2f104dd9ae803f086014ed0c010ca5f7", + "sha256": "sha256-z7jNrUwN0F7YysvhRr8XGHZEA5R7neg0jhv9QvYupz4=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/fastinfoset-project/1.2.16", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fastinfoset-project-1.2.16.pom": { + "sha1": "86113f7ee9a5d995429bf75294b57b31f0a17d29", + "sha256": "sha256-kFgkJa3B9AtBNi2vuVFzkxIlrKpeeWINXmvVL2Rikro=" + } + } + }, + + { + "path": "com/sun/xml/fastinfoset/fastinfoset-project/2.1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fastinfoset-project-2.1.1.pom": { + "sha1": "64c42818f61c961fd357788a0e8287369358258d", + "sha256": "sha256-HoDvsaCRWw+GJrp11ylrXxuwJpoKL51zh62Zh/duTAk=" + } + } + }, + + { + "path": "com/tunnelvisionlabs/antlr4-annotations/4.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-annotations-4.5.pom": { + "sha1": "3dfac370b3fca6f90861f3a10fd5445ca6f8cc1b", + "sha256": "sha256-GlGhYs9HVzQpkn8LKY/+osu22EopFlWilP6wBD9K+ak=" + }, + "antlr4-annotations-4.5.jar": { + "sha1": "2c5996120a0ac690de575bd8ac36250e6720a6b8", + "sha256": "sha256-Cy20gXadUUAmJKlZLoBRi6Teg1tAPMztdz/T2taQicA=" + } + } + }, + + { + "path": "com/tunnelvisionlabs/antlr4-annotations/4.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-annotations-4.9.0.pom": { + "sha1": "a14e662f4a9928d9242c9bc9fbbe512bb561121e", + "sha256": "sha256-/RFLtzGoRYoVaFF2Efdw61AbP1gxR+z+lwZwv26rsU0=" + }, + "antlr4-annotations-4.9.0.jar": { + "sha1": "ef4a4a6528a74cc24b6f7f0f1711876e32ab54b7", + "sha256": "sha256-B9BT1MMXi7QGt3Iov/5a8jFHNmboS/gBhrHOpDmLy3g=" + } + } + }, + + { + "path": "com/tunnelvisionlabs/antlr4-master/4.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-master-4.5.pom": { + "sha1": "6dadb90adc9b3879a4fb9625b08711e578252dfb", + "sha256": "sha256-u9IDQVdyb0yleNnx9uqlT7GPyCIyCVjqjpNC3IXB0DM=" + } + } + }, + + { + "path": "com/tunnelvisionlabs/antlr4-master/4.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-master-4.9.0.pom": { + "sha1": "97a80ecc34d75a66b47753108406a0a1ef00a192", + "sha256": "sha256-PGDW41sFnx1xFiZQuyKCPZE8xbH2dxQiYptTGBckDIE=" + } + } + }, + + { + "path": "com/tunnelvisionlabs/antlr4-runtime/4.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-runtime-4.5.pom": { + "sha1": "4c01c62d899d8bd5112a182624fca9ae62d42c8e", + "sha256": "sha256-/K7AmQ4gBh0pNr64wlykVpBNNZGkUiWEg4QFgcptF54=" + }, + "antlr4-runtime-4.5-complete.jar": { + "sha1": "281bc3c7d3a25101df33873c11d6978e5797b451", + "sha256": "sha256-snk3RqxHbmAkyPpWt7EjFtgF+fJ1DcGrpdnh9x07bj4=" + }, + "antlr4-runtime-4.5.jar": { + "sha1": "5067478827a98f5ab77d9fc577903edc57af3da4", + "sha256": "sha256-uHGWd9LRT79vnnm+W7IpkFRKZSjca0idiiUCoIcg170=" + } + } + }, + + { + "path": "com/tunnelvisionlabs/antlr4-runtime/4.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-runtime-4.9.0.pom": { + "sha1": "97e6da6d8bc827b5d9f6590fbfa8565511a8da5e", + "sha256": "sha256-rrWtT7mfiGvPWaqtrMB2l/6VRswGCKs/KA/T6/Jibx8=" + }, + "antlr4-runtime-4.9.0-complete.jar": { + "sha1": "d2be64097877f0dfd88a844ae59cef0549120a71", + "sha256": "sha256-zzVfXa9YFybVCx1i20/79qhC6DFflExOcgpn1qcQ3QM=" + }, + "antlr4-runtime-4.9.0.jar": { + "sha1": "17e19535a875a2a1014197d2bb35faeecf4e50f4", + "sha256": "sha256-5Bk7qANM7fkgkFvbMYuKUJfXZ/mZ+HupekKY3mnz/6s=" + } + } + }, + + { + "path": "com/tunnelvisionlabs/antlr4/4.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-4.5.pom": { + "sha1": "2370f47fd57fbea37385e241dd7292bdcfbe8353", + "sha256": "sha256-P3iRz4cESH2jwwdKDcBNHd0Bk7t7jto6hoQJR1HZArs=" + }, + "antlr4-4.5-complete.jar": { + "sha1": "ed9ad7c5daf965143384ef80154aa4461e78f49a", + "sha256": "sha256-0xNoAWHtHY4o6vw2FKotq0lZAgnr4uVs7Yijx/EjVww=" + }, + "antlr4-4.5.jar": { + "sha1": "a0e860e317147848e69ac145bc5196901a9993bf", + "sha256": "sha256-kCd4fPB5RRjschjuiVATzvLzrTcQ3M/0n/tpByNbmG4=" + } + } + }, + + { + "path": "de/undercouch/gradle-download-task/4.1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gradle-download-task-4.1.1.pom": { + "sha1": "526528997aef6e1ed2b5ddd990c3692aa7e7b364", + "sha256": "sha256-heZgcmDbvbXoXxmIhAsNmsS+kRnd6QobrKjqd1ZGQVo=" + }, + "gradle-download-task-4.1.1.jar": { + "sha1": "7a57e30e285bd972cd73e91c0ad16155e29b18ee", + "sha256": "sha256-6wi1cOQI1GRnBecKlJYU1DnqKxFFXxZSqwMw3olU2rk=" + } + } + }, + + { + "path": "de/undercouch/gradle-download-task/5.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "gradle-download-task-5.0.1.pom": { + "sha1": "ca19885c858f0239c57c36e67c8bc3f4f987250e", + "sha256": "sha256-Pos7wzs3Vgzic+Pp+EZKtfdb9E5jlmEwMpdGrROyAX8=" + }, + "gradle-download-task-5.0.1.jar": { + "sha1": "0625245c5e761ee3eb64ff91aad6fdb60b1985aa", + "sha256": "sha256-7VONXLvRug+MUcCRaEVg0S4JPuJwyFvDN6emaj97Rcc=" + } + } + }, + + { + "path": "io/grpc/grpc-api/1.21.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-api-1.21.1.pom": { + "sha1": "504b90430e48910d71e1598077338da6d54140a5", + "sha256": "sha256-DaGcU+Yg6aiVcnluQEkwoGtypnU3cD2938oW4LfDYtw=" + }, + "grpc-api-1.21.1.jar": { + "sha1": "5b4ffc280fc4dd7b4ee2357a901b1a9f15404200", + "sha256": "sha256-AJpdC92B+h1gAlpYExcq4hoLZK4jxjmGSnVoOuW6yZo=" + } + } + }, + + { + "path": "io/grpc/grpc-api/1.39.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-api-1.39.0.pom": { + "sha1": "f6244501ee958174e8b93823e38f1b88c8f24e2b", + "sha256": "sha256-6cU/UvOi1IavwcsDaqonRMD52lKz/nzcHczyV70MkgU=" + }, + "grpc-api-1.39.0.jar": { + "sha1": "706f68087b8fae59f8949b4302ac9807131c0112", + "sha256": "sha256-ToAHTcLGJzcgRFmc+HO/ZERQohjFivSMA9W7CGzO1IQ=" + } + } + }, + + { + "path": "io/grpc/grpc-api/1.45.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-api-1.45.1.pom": { + "sha1": "cd3e633a81b14642aa6eae750cec62829dcf1139", + "sha256": "sha256-W2EAJ7n2kfY1QkefucNRBAD9EcHuExgvMZdN8XAcBWs=" + }, + "grpc-api-1.45.1.jar": { + "sha1": "521df70b1f69648d5f418015ee594fc15a065dfc", + "sha256": "sha256-3Dgf4Bj7ELuozGb5jbEFCnDO5JqCcAF8Iuxvd7EPE+U=" + } + } + }, + + { + "path": "io/grpc/grpc-context/1.19.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-context-1.19.0.pom": { + "sha1": "054c2f2244451ffb36656a404fc2095a836fe18c", + "sha256": "sha256-fhPUOJ0+91mkhdt91BjelyFmu0inSikZ186B+8+2j4k=" + }, + "grpc-context-1.19.0.jar": { + "sha1": "bb73958187106ef1300b9e47ce5333f40cb913eb", + "sha256": "sha256-j034YYxQDzwf34i3VcrrFP4oRupZqedi9hSFIXi2Qxg=" + } + } + }, + + { + "path": "io/grpc/grpc-context/1.21.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-context-1.21.1.pom": { + "sha1": "78cafcd57253e02a2b93bd7ce34ee78494866a2f", + "sha256": "sha256-ocIxfLzFJMBPVsAdqpgLaNMOaO5LoNORm+TmUgYUOgY=" + }, + "grpc-context-1.21.1.jar": { + "sha1": "aef9db74ded2d9ab229f9e184e35bebf6ac6fbdf", + "sha256": "sha256-qT/qEy1AH1q5D1v1IMVQvIFCO/z0p5mK3p24edzXggc=" + } + } + }, + + { + "path": "io/grpc/grpc-context/1.39.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-context-1.39.0.pom": { + "sha1": "4eed2350a7e5b55f34e349aceab52c875acd0832", + "sha256": "sha256-/fXj7X4v5WwgIWjIG2A6aanhQ0+oe8W4idTnIRIXQl8=" + }, + "grpc-context-1.39.0.jar": { + "sha1": "637f453f3654aa29bf085ae7ddc86f9f80c937dd", + "sha256": "sha256-l/5wtOfahu1P496xbYP8icflt2TrrIyd1dUd1LWqkgw=" + } + } + }, + + { + "path": "io/grpc/grpc-context/1.45.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-context-1.45.1.pom": { + "sha1": "25e8145ed8f219c0248d1db8cccd30949cfe1a81", + "sha256": "sha256-tW+fRqEUE2cqWLA0FiIR1hq+EO8cEzx/4lQoDzDxwRk=" + }, + "grpc-context-1.45.1.jar": { + "sha1": "485a08c019cc78914a477b1dfc7052820b8d822c", + "sha256": "sha256-JiuNDSNVHMsMgU+JeUxz6bUzPOceqyjuxqn8UwYT4V4=" + } + } + }, + + { + "path": "io/grpc/grpc-core/1.21.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-core-1.21.1.pom": { + "sha1": "62baa94560346b8781f83e4e964ffc99f6d9f325", + "sha256": "sha256-OmM2+4gvMF2y5eFOmlDVgTgZtDBPbrOfDuBIP0pw+Zg=" + }, + "grpc-core-1.21.1.jar": { + "sha1": "ba6f9c6cc943e9f88e8716ede7ceaad3b8b14f8f", + "sha256": "sha256-FQbQUWFoto7t+qa+FDPBN6HplqRlSWz7VNVIU8vJAo4=" + } + } + }, + + { + "path": "io/grpc/grpc-core/1.39.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-core-1.39.0.pom": { + "sha1": "7ec5caa2962f3945cb2a8702722e4ccb8ab04354", + "sha256": "sha256-MGFYawP72Eiaj2ggF18HMrnvHDZpLNW2xH0l5Bxd4p4=" + }, + "grpc-core-1.39.0.jar": { + "sha1": "df5bbb97602a059eee86e22e09f6524901a62cfa", + "sha256": "sha256-P+vrGeGIczae+pvwjt1OScT6Pr2tROcErG6jUrYra+I=" + } + } + }, + + { + "path": "io/grpc/grpc-core/1.45.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-core-1.45.1.pom": { + "sha1": "dadbd6216d9f370100b97db0c358511d00dcd875", + "sha256": "sha256-YDn8XzyJfqFAlAwSkHXIKIM84xM08TQ0fbFZkc2iHsk=" + }, + "grpc-core-1.45.1.jar": { + "sha1": "9d886fc7d591fac00cd1bbd2347ba66efb31ef20", + "sha256": "sha256-jdg1PipVZ4WB7JJBXCuSzI2vtDdkfe5XpJ420eg0W54=" + } + } + }, + + { + "path": "io/grpc/grpc-netty/1.21.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-netty-1.21.1.pom": { + "sha1": "add76d3c47a8f2b821e6073f936bb79805895182", + "sha256": "sha256-uoQA8Y1XvbcY0EpkO6RgJVfXmp7IC4rkUVd3ZzV1QIU=" + }, + "grpc-netty-1.21.1.jar": { + "sha1": "7d21fd499ac6856fec0754d6f3c1df03065e82e0", + "sha256": "sha256-EENdv17ePKfdckMqgf99UI9rkwv9X+Io0aWbV//Om8c=" + } + } + }, + + { + "path": "io/grpc/grpc-netty/1.39.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-netty-1.39.0.pom": { + "sha1": "b4c208ed0c82654502fa9552edf000e9c480b0e0", + "sha256": "sha256-ISjUfxJ+Ow/K3MDRm5tHcJdd++jH/ixx6Tpgf633yMI=" + }, + "grpc-netty-1.39.0.jar": { + "sha1": "d161bd637e2174cd6a73178117ff1774604fd2e0", + "sha256": "sha256-Zy2gjJRAEElMKc5+6DIxlJSdhSDk6k/g5tROZblIlwA=" + } + } + }, + + { + "path": "io/grpc/grpc-netty/1.45.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-netty-1.45.1.pom": { + "sha1": "99e303d9aded62bd49bdd8c126649159c0254f1c", + "sha256": "sha256-NRA/5lz81d3I++2u6tkjFQ1TnTGohCNccW7Fybu5oZY=" + }, + "grpc-netty-1.45.1.jar": { + "sha1": "e2a1bc48548213144f00aa709c4da92626b35cd8", + "sha256": "sha256-D+88pnRyQmJMrSGf3RO128oSHeBOoSL1yxy1CC2ZgxM=" + } + } + }, + + { + "path": "io/grpc/grpc-protobuf-lite/1.21.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-protobuf-lite-1.21.1.pom": { + "sha1": "917f3b38b16e3ca4d638525958979b1a49d976b4", + "sha256": "sha256-qCBXVrr2zxInJbKVNCI7keXCxFi85OCfueATTKHIr/E=" + }, + "grpc-protobuf-lite-1.21.1.jar": { + "sha1": "fc62b16f89160ec81cfe0b4882bfd17a88148bde", + "sha256": "sha256-EgAjbiP6UcKDiQHU+SOLvselLv1TfegyrZJhyB3N2r0=" + } + } + }, + + { + "path": "io/grpc/grpc-protobuf-lite/1.39.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-protobuf-lite-1.39.0.pom": { + "sha1": "2b56a83a586d8eff46bbd3761959a4ec018deb93", + "sha256": "sha256-TUSWGTO5svjSNbdj13gDIxy1vzNZ+OwBPjw0M4iyLAk=" + }, + "grpc-protobuf-lite-1.39.0.jar": { + "sha1": "64ccdfc8a5a7c545247381f0ac42a5b8fc936e56", + "sha256": "sha256-lNnF7oir4Gp0G05i0F32JRQJ4szI5RBXWJHOPXgMFk4=" + } + } + }, + + { + "path": "io/grpc/grpc-protobuf-lite/1.45.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-protobuf-lite-1.45.1.pom": { + "sha1": "b675f731b35d519e046e72f9304191f2e566e46c", + "sha256": "sha256-viFHK0eJfvaGwal2julMbArXbfVHls97shJEWDedXAo=" + }, + "grpc-protobuf-lite-1.45.1.jar": { + "sha1": "0a842605124e6542550b2da550a9acae1443a945", + "sha256": "sha256-h0r8TKwRYf3TgSH/1W5Y6MhfUUnBOVSrZy1rSuJMi58=" + } + } + }, + + { + "path": "io/grpc/grpc-protobuf/1.21.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-protobuf-1.21.1.pom": { + "sha1": "a144482ff250e2f357379c30694e71bc6c2e67dd", + "sha256": "sha256-Zvh2dfYXjL559KaV4CbWKkwyc/nmo2YI8WmEdKtVLaM=" + }, + "grpc-protobuf-1.21.1.jar": { + "sha1": "e2aa7a68b565e2180c934951cff7903d6d4fd3a0", + "sha256": "sha256-ycAHDDn5Xp16ytYP3MPUyRcppMEs3hVwUJmwniAavAE=" + } + } + }, + + { + "path": "io/grpc/grpc-protobuf/1.39.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-protobuf-1.39.0.pom": { + "sha1": "2e1e00e1bd33a3e512461c85a1b8ea8571f44062", + "sha256": "sha256-onwNhxmms3/ZeHlVn8LdCwJhxw3b5XhXG+iEPcZZLZs=" + }, + "grpc-protobuf-1.39.0.jar": { + "sha1": "b7789ed9dd819a4fb9f5e3cf44d02f16fa49031e", + "sha256": "sha256-mp2UuT++jTTKN1EIyhR9ezWlPaBRCfQS0YdUSPRIgkQ=" + } + } + }, + + { + "path": "io/grpc/grpc-protobuf/1.45.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-protobuf-1.45.1.pom": { + "sha1": "ff6b3e1de46a47cd973452e9e2b953675762d047", + "sha256": "sha256-g66A3aN4skdB2iOA7dSwqGkyVwE3qMHnwUBQ6EDMntI=" + }, + "grpc-protobuf-1.45.1.jar": { + "sha1": "480d2a8c2592c7067d2fe9cc773d3a36402bc463", + "sha256": "sha256-9Z/mYgMOMJI6kcXYC/OiVIa+J0DQi2dXV158QhN4JpQ=" + } + } + }, + + { + "path": "io/grpc/grpc-stub/1.21.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-stub-1.21.1.pom": { + "sha1": "cda5896a90ac55a06da1ada8a551decb2c2d6682", + "sha256": "sha256-7dDNw8IhG7LxNg7nZHuU4Edj7FGbf95yeqFDYOjKjnU=" + }, + "grpc-stub-1.21.1.jar": { + "sha1": "a5646c3bfd04c9e40d6680cf474f8a64611cf3f6", + "sha256": "sha256-ZCwyiRYpEofpZwLMWtH31gGgw0fJ3g8TAhmqD9x9WpA=" + } + } + }, + + { + "path": "io/grpc/grpc-stub/1.39.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-stub-1.39.0.pom": { + "sha1": "59c69fc74dc5075c95d80702668e1235246c04d8", + "sha256": "sha256-3w/yTFzz7EcCh4QPRaH/224C+tbd3v3u3VhPZJndmOc=" + }, + "grpc-stub-1.39.0.jar": { + "sha1": "0598232e1142fb0d09067e1fd1fc524580ee9425", + "sha256": "sha256-yFeC4g2m6tvwdTfzXVK/Krr9iRrhTCe302pNRQV17rM=" + } + } + }, + + { + "path": "io/grpc/grpc-stub/1.45.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "grpc-stub-1.45.1.pom": { + "sha1": "1e11baab2824973e2ae19f3205fede1ae78ef0bd", + "sha256": "sha256-Y9AI9+QeYpR4B9BGDGwWlx5zzy3Lq2C4izYC3sqQu0o=" + }, + "grpc-stub-1.45.1.jar": { + "sha1": "3cf1d4ef88808bbabea5d929b78854ccc7f95242", + "sha256": "sha256-TZ2CxZuQODwX8VK6AxI8a0taiF+TPamgWgX18snltKA=" + } + } + }, + + { + "path": "io/legere/pdfiumandroid/1.0.19", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "pdfiumandroid-1.0.19.pom": { + "sha1": "030b82d68e00d32fe0fbf6b77c0bf2aecc3f854c", + "sha256": "sha256-vXKo6GDEFs6hhL6Bp7Yh0ibOkxj9LwnsSNCUpYR2Gvs=" + }, + "pdfiumandroid-1.0.19.aar": { + "sha1": "60b52dc0372fbe17693a199a3bad1249e2be0279", + "sha256": "sha256-G4lIaMyTCb6G9ki1W9gFNcy4RcrO09SOfUl07DScUs0=" + }, + "pdfiumandroid-1.0.19.aar.asc": { + "sha1": "ca81280b8a51a77b74cc2834a8ec5f0fc8b183b3", + "sha256": "sha256-gbYf5pG2Xj/R6DFGRzOlmJr7BxAAG4x5j5UhqT7Qu5A=" + }, + "pdfiumandroid-1.0.19.module": { + "sha1": "86a405ac275ce86db1769ad37e3b3742292b6f14", + "sha256": "sha256-aWDgX5fFPT6JpiY8OeRrV2Loz3RGVkpZANRdXbRiX2s=" + } + } + }, + + { + "path": "io/netty/netty-buffer/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-buffer-4.1.34.Final.pom": { + "sha1": "a3ca3438b1e607a25f913e44f8f8bdbb2c5b0d9d", + "sha256": "sha256-r4QPS6cbZkLeWMjtyQFdC9+yLcmbvIpAc3hCGig+mSc=" + }, + "netty-buffer-4.1.34.Final.jar": { + "sha1": "8afc2027eadc7848127553ebb490ea0e2b199d4e", + "sha256": "sha256-Od/ojfhQX9Afv5wdu2tvqbApfkU8PcTOA56leK6i6qM=" + } + } + }, + + { + "path": "io/netty/netty-buffer/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-buffer-4.1.52.Final.pom": { + "sha1": "533ef2b7c0e36272b5ca855e1df36e78f8ba735c", + "sha256": "sha256-WFJmy/bhR+P+IJMApw1fDE22TM1wK8AGkMQqM97+AGA=" + }, + "netty-buffer-4.1.52.Final.jar": { + "sha1": "fdf681985f50d84fac625df52f34b6d6cb699bda", + "sha256": "sha256-QcoQNa7m4PgXdZfQp2EKWqLG5vrHRKyB/eTl5Y9jlnU=" + } + } + }, + + { + "path": "io/netty/netty-buffer/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-buffer-4.1.72.Final.pom": { + "sha1": "14db354f78e0ff68f0f59b2119ef12152fb767a9", + "sha256": "sha256-RZMB2nScter3IbXKw5wGYLLVn8ZiZAkVNuEhyxjYtBM=" + }, + "netty-buffer-4.1.72.Final.jar": { + "sha1": "f306eec3f79541f9b8af9c471a0d5b63b7996272", + "sha256": "sha256-Vo/3zZ2OIoTsmAcwyIkk9oZkKSn48hmnRRi05kdV86E=" + } + } + }, + + { + "path": "io/netty/netty-buffer/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-buffer-5.0.0.Alpha2.pom": { + "sha1": "0c145d670f23ccf322fcaaf6e4611e85c9d4493b", + "sha256": "sha256-/XVHx3sKb08Y+oV2z1EKOrsj2XORfW+VhSw+vBNx504=" + }, + "netty-buffer-5.0.0.Alpha2.jar": { + "sha1": "3468ac1b15b4037afaac36933c4f8513b013514f", + "sha256": "sha256-5b18+P5REmlzcsbD1GiAcjTIDSJ9nI6sd6ev4f5cTXo=" + } + } + }, + + { + "path": "io/netty/netty-codec-http2/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-http2-4.1.34.Final.pom": { + "sha1": "256a7c4c25b5cd1e3e360d0d99a1bfc853a00c98", + "sha256": "sha256-YydnN8pyWcgKpcF91V3Bb8/DQfDyd6GeNryGcrHpRpU=" + }, + "netty-codec-http2-4.1.34.Final.jar": { + "sha1": "66268942ed80beadaf6802e2fcf8983eef4fb7ec", + "sha256": "sha256-MZ9m86sNOqw0d/6/GcJZmQ7oxjn8faiCLfpY59qxvc8=" + } + } + }, + + { + "path": "io/netty/netty-codec-http2/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-http2-4.1.52.Final.pom": { + "sha1": "badba3fbd74fbebb29c89ffdd698b6323fa1183d", + "sha256": "sha256-H5l0Qz2GELmHsluj6+n5G1meIg4NRKzCmM858FXo89M=" + }, + "netty-codec-http2-4.1.52.Final.jar": { + "sha1": "1e06a41e2b67cd6f91bf97935f4729c843ac1a57", + "sha256": "sha256-61J6QtIonRLe5tR1PKJxWXije7tBxxkVzk6VOcSfk6E=" + } + } + }, + + { + "path": "io/netty/netty-codec-http2/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-http2-4.1.72.Final.pom": { + "sha1": "822fd3bc9889fc099dbdc658ad0c9df308514ad7", + "sha256": "sha256-v7rDcekD48ciBtmFpiH9yFBws4PNlUxCb/lj5RJnWRQ=" + }, + "netty-codec-http2-4.1.72.Final.jar": { + "sha1": "4b269e666fbace27d2c1efa57703e99b09655822", + "sha256": "sha256-yJpwUA9Z6FY+cgqqgIJjpRS9nivZG6hOq4wsy0XyNLI=" + } + } + }, + + { + "path": "io/netty/netty-codec-http/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-http-4.1.34.Final.pom": { + "sha1": "7f628ef0c77449ec695649617b2c492dc5d7ffea", + "sha256": "sha256-wWBf+0M6wSPJSf993hCPFT4giuAdFgduR1u1lKbcvrM=" + }, + "netty-codec-http-4.1.34.Final.jar": { + "sha1": "02887d87fbc1b057657348f61dc538f7296daf79", + "sha256": "sha256-XfVVbvaw5858cqNZ5Mp3T8342P4S8LYzJxXqpEz+Qfg=" + } + } + }, + + { + "path": "io/netty/netty-codec-http/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-http-4.1.52.Final.pom": { + "sha1": "05c384973b385aa25231a8e84239ce63b265b261", + "sha256": "sha256-THwiCueAN0x4HILR4recjrdE2pA70q2C1KJrsUkYDAs=" + }, + "netty-codec-http-4.1.52.Final.jar": { + "sha1": "e0d75d03edf6e4d0682c85e8b26762bcde34697d", + "sha256": "sha256-tdoo0OqHiwfTERrPXk+vQSiO8jArJkFBkP3Rp9lzyAw=" + } + } + }, + + { + "path": "io/netty/netty-codec-http/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-http-4.1.72.Final.pom": { + "sha1": "84ad0a0bbd43a62df88b74749b8763da5124e0a4", + "sha256": "sha256-qM0VZZpXIAE5GIQ/yL8p84qBuD40gW+C45ek2zyGT1M=" + }, + "netty-codec-http-4.1.72.Final.jar": { + "sha1": "a8f062d67303a5e4b2bc2ad48fb4fd8c99108e45", + "sha256": "sha256-+m/siAEL+vanQVtTZGcbaxj/trNamGq5e0I/2MOgF0s=" + } + } + }, + + { + "path": "io/netty/netty-codec-http/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-http-5.0.0.Alpha2.pom": { + "sha1": "b05554ab2c08cf9c121ad04cfaf6be13814b1511", + "sha256": "sha256-pNhtBP/BsZPlBJkY6dd/b2nmSpKLQDjhgLM5O0xaAPA=" + }, + "netty-codec-http-5.0.0.Alpha2.jar": { + "sha1": "8bdccec21ddb3db3a5804af120ee94061df6b6c8", + "sha256": "sha256-6a8NH5gWokK0GTzOFgOlZ3zaTwUTz65NkFvN9oT20zE=" + } + } + }, + + { + "path": "io/netty/netty-codec-socks/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-socks-4.1.34.Final.pom": { + "sha1": "c1cdb87e69a616bc081319110ec6b8764af2026a", + "sha256": "sha256-6KkHpl7qO3s1z1HVy98W2wPRNafc0sXjfUpFvC1Etpo=" + }, + "netty-codec-socks-4.1.34.Final.jar": { + "sha1": "345a10b91e4876cb6ec642a3c010ca187f361fa0", + "sha256": "sha256-nE/1i2SBk5QmVNsg8XLQF0QWJXVLkCOU9iDwQHSDA0Y=" + } + } + }, + + { + "path": "io/netty/netty-codec-socks/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-socks-4.1.52.Final.pom": { + "sha1": "abcfd50101ed27c92d1cf93f6629784819527512", + "sha256": "sha256-HZaSGuleZKA8TtdenqV7J64wq1J++SISz9pZcmfOOfk=" + }, + "netty-codec-socks-4.1.52.Final.jar": { + "sha1": "93170f664ae83ec70c35752b91cb733f9b80bf90", + "sha256": "sha256-/sxXfgu3ch1naM/BhG7hcpEdFquA372ZldllelUdTqc=" + } + } + }, + + { + "path": "io/netty/netty-codec-socks/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-socks-4.1.72.Final.pom": { + "sha1": "f4ced36d3bbeaf48e89c33400f8ffa7b7742157a", + "sha256": "sha256-s+tGfmS3210J9VxYuH0bMa62qfQsKMLWjh8TH9mqFns=" + }, + "netty-codec-socks-4.1.72.Final.jar": { + "sha1": "d3bc427b6e2dc4bb6dc9d18d1cc47f8530970a8b", + "sha256": "sha256-M8TNE0hYqrxa/l3EQMiIAYYBfcTcVB80yOFr/8VUbQc=" + } + } + }, + + { + "path": "io/netty/netty-codec-socks/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-socks-5.0.0.Alpha2.pom": { + "sha1": "c113d4433edbecf2f34baab45da61a99b88865ef", + "sha256": "sha256-CO06wkoMg7NCgm0jU9WcvrGkqyGDVtXjX9bgTZeQ89Q=" + }, + "netty-codec-socks-5.0.0.Alpha2.jar": { + "sha1": "a68cbb83da20ac74b92848902b9f85426f37d461", + "sha256": "sha256-g9S8bOT+l8SzpjS6jXL1KuAiZjppmtrNXEaN9GmAb98=" + } + } + }, + + { + "path": "io/netty/netty-codec/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-4.1.34.Final.pom": { + "sha1": "9f0655ef901aff9c12392a1902f8d3bb6aa15f7a", + "sha256": "sha256-+9h+ortXrbmQdoA8TapOjRdOZJAPoWD7zgafbzzG4q4=" + }, + "netty-codec-4.1.34.Final.jar": { + "sha1": "0cb53baf46cc55768f2e28d15cde399063555ea8", + "sha256": "sha256-Uunus2OKjtCRHHKlCMBfpPnTOREl6uRvKH06igd2IR0=" + } + } + }, + + { + "path": "io/netty/netty-codec/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-4.1.52.Final.pom": { + "sha1": "2260f6195e4194153168a3b4bdea5b7d850c3715", + "sha256": "sha256-PkqniwyDCL7eb24X7P7mEmsqverbDj2iWq5ndWlm+lQ=" + }, + "netty-codec-4.1.52.Final.jar": { + "sha1": "d98ad5bb86707c3c1a7105a346119dac332bdf23", + "sha256": "sha256-PH543M5+NT2GrH26LCUatc3dTrhuqoic/kIpeCuCjlI=" + } + } + }, + + { + "path": "io/netty/netty-codec/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-4.1.72.Final.pom": { + "sha1": "31bbec5c37902fd2d728969353a0f658b768d4e5", + "sha256": "sha256-LYjRojCsykeXRUeCxSJuFhJyHU5zknwUL96Vop1JVCo=" + }, + "netty-codec-4.1.72.Final.jar": { + "sha1": "613c4019d687db4e9a5532564e442f83c4474ed7", + "sha256": "sha256-XYWRyicaHpwiTo3jhzqpk2rLWB7g21FOfcGFI9820Ww=" + } + } + }, + + { + "path": "io/netty/netty-codec/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-codec-5.0.0.Alpha2.pom": { + "sha1": "2b4218526aba38a9d068515fbbd47db4620dfba6", + "sha256": "sha256-CIyljWUUzHf377sLESPzzh8KdrYjlmx7HU62C4MllGg=" + }, + "netty-codec-5.0.0.Alpha2.jar": { + "sha1": "01cb2a913926418fe973af72046ae40b3728083b", + "sha256": "sha256-5KBUtrnWQMqBtMg3cHwva7yNTCb8PiB0UL9+lY1nMpI=" + } + } + }, + + { + "path": "io/netty/netty-common/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-common-4.1.34.Final.pom": { + "sha1": "c8c470bae99389c3267db4d53a2041e36f77aadd", + "sha256": "sha256-g1wVWCIsilc+7jYyEo4Hplvok4LmThpRj+SmRKvJKxo=" + }, + "netty-common-4.1.34.Final.jar": { + "sha1": "2dffa21967d36cae446b6905bd5fb39750fcba43", + "sha256": "sha256-EikxEX6s83CwVNDookEe+oHeSVamw/k4sPDrkVlppCU=" + } + } + }, + + { + "path": "io/netty/netty-common/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-common-4.1.52.Final.pom": { + "sha1": "bdd347dea9b3fcc5f7f8f5be1b78df7e30bcbc0b", + "sha256": "sha256-NljreSmkZytZr+4iK656EidFTG+jkznKTKx6XvMPOqc=" + }, + "netty-common-4.1.52.Final.jar": { + "sha1": "715bfadb9f3bf86da38fa12b97b4aaa9828b4789", + "sha256": "sha256-DR8QHk231TDtBNfKvVfZXAdQ8Fm7az+t/tDIDUc0xxc=" + } + } + }, + + { + "path": "io/netty/netty-common/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-common-4.1.72.Final.pom": { + "sha1": "34e72a4aa7e41b8f7e9461aca29abded92eb87b6", + "sha256": "sha256-1tMCSR85hIvxiJvyE35ufnfZbDlK9br4rGoJAVcs2nU=" + }, + "netty-common-4.1.72.Final.jar": { + "sha1": "a55bac9c3af5f59828207b551a96ac19bbfc341e", + "sha256": "sha256-ittMKRJgzrKFmmjEnwre7Ta/SVh2COK4Hs/2qvBgJek=" + } + } + }, + + { + "path": "io/netty/netty-common/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-common-5.0.0.Alpha2.pom": { + "sha1": "214d9773a826b6737ff37207530c3ce2cd08df01", + "sha256": "sha256-c2/Zu9x2H32KjjklMTS2fwPU/PghhCv+kBuSo8NiQZg=" + }, + "netty-common-5.0.0.Alpha2.jar": { + "sha1": "3cc855271fd64b464c47ba69a200b446c9e9efc7", + "sha256": "sha256-n3wOG3FAEK72lBhqoCurql78RQ3/KntGNQxVKW+WjPI=" + } + } + }, + + { + "path": "io/netty/netty-handler-proxy/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-handler-proxy-4.1.34.Final.pom": { + "sha1": "c03a4c4484bd745ee1803090012753c89ca349fa", + "sha256": "sha256-Huo9FnThxkRPFm8okGY3SIOweOKoDfZbcCIrPRgvXPs=" + }, + "netty-handler-proxy-4.1.34.Final.jar": { + "sha1": "42b5a57a4bb895f4489a724cdc9e454bc5afe108", + "sha256": "sha256-9QbGrLl7PgsHlc+fCXHYC7q3wXCGMS+iJbmMzJS+bf8=" + } + } + }, + + { + "path": "io/netty/netty-handler-proxy/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-handler-proxy-4.1.52.Final.pom": { + "sha1": "cec8bc994c63ca374d3c7229207654717a4fbf9a", + "sha256": "sha256-aW41AdGsc6o4JVXRvWwsaDGtX+PbEG5cgugIKS9b6bg=" + }, + "netty-handler-proxy-4.1.52.Final.jar": { + "sha1": "e5701741e851d83b5ef15738f84c825ba2c0688e", + "sha256": "sha256-6NB4Y860vUgG4lWh1TLIZQ8POy1NAGrW0hgS6UG0L9w=" + } + } + }, + + { + "path": "io/netty/netty-handler-proxy/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-handler-proxy-4.1.72.Final.pom": { + "sha1": "3c2586b23902c259410b4357e4f0b26132c0f1b4", + "sha256": "sha256-QddvkZtHOCN9o2jJDQrvv0SGO2u6dIfsuX1sI/dLkzg=" + }, + "netty-handler-proxy-4.1.72.Final.jar": { + "sha1": "90de0fe610454d4296052ae36acb8a6a1d0333f1", + "sha256": "sha256-zhEjJQRQ0rF4FXSBd/02hDaQuM1wO0fm3BLRWdhtow0=" + } + } + }, + + { + "path": "io/netty/netty-handler/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-handler-4.1.34.Final.pom": { + "sha1": "41ace5597164d88c1dd5fe6ed9f65f9cf66513d5", + "sha256": "sha256-63V9rhoTrxhoMeerbmJU/rNmWl2c4ZqvSq/+LafSrSY=" + }, + "netty-handler-4.1.34.Final.jar": { + "sha1": "96169098bebda3d1432f5421c338958063b3da52", + "sha256": "sha256-A1YWgB/piUyiSQgyz5l2U22sdA9B6Q3hzdS6RvBCY9E=" + } + } + }, + + { + "path": "io/netty/netty-handler/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-handler-4.1.52.Final.pom": { + "sha1": "e984652e31a81af5e1e67f96db139c41b2a08e72", + "sha256": "sha256-s3S7oLymMS/m2++xoBOsdFfBqAyfGOvMBZvcLesWMUc=" + }, + "netty-handler-4.1.52.Final.jar": { + "sha1": "eb7d4fa554f4fdc8dacb80dce7e3f806fd1dc2db", + "sha256": "sha256-LNwh+/rgSUDIWSkDl3nJP1H2GPXvshYHkxnc/PMqVcE=" + } + } + }, + + { + "path": "io/netty/netty-handler/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-handler-4.1.72.Final.pom": { + "sha1": "9b6e10d98a9d8eb82f7225c0691984bbb727de75", + "sha256": "sha256-KEnrx1zAfK0Tkd50/P9HtC8FW7Z2eFhcacLBsSsyPuk=" + }, + "netty-handler-4.1.72.Final.jar": { + "sha1": "9feee089fee606c64be90c0332db9aef1f7d8e46", + "sha256": "sha256-nLYBKvfgY2HXOKxOO9xJoVj4z4fZ3uDydEBWt9mcKNU=" + } + } + }, + + { + "path": "io/netty/netty-handler/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-handler-5.0.0.Alpha2.pom": { + "sha1": "29ff1937ec235692e87a49939d558ba867053c6d", + "sha256": "sha256-Nf1n2YmudJxHdPdBzTRthuIpyDp9sMGDLd5odGjA9JE=" + }, + "netty-handler-5.0.0.Alpha2.jar": { + "sha1": "5b9267a9f949e82b04a2613a44dbc1134661517f", + "sha256": "sha256-8jCgo41dhS5Fk0H7/BpWCZPGAQQqADF+p0tfgVOlU/E=" + } + } + }, + + { + "path": "io/netty/netty-parent/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-parent-4.1.34.Final.pom": { + "sha1": "1246042bbf6b658c718fb3724fb217cf5cc81a4e", + "sha256": "sha256-fp5rf3Cuipq+bReuo5G4H2fiDLj6xVuInPpRnSo+Sx8=" + } + } + }, + + { + "path": "io/netty/netty-parent/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-parent-4.1.52.Final.pom": { + "sha1": "01075921cdfdb2aabb1988fdaddd45be58c0b727", + "sha256": "sha256-vkvqb2BtR6ilw1cwR9BEJtM7nbWRuqa84eIw/iI/kD4=" + } + } + }, + + { + "path": "io/netty/netty-parent/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-parent-4.1.72.Final.pom": { + "sha1": "6320a6a690cac6b98a7666d2fe98b98df9b1b295", + "sha256": "sha256-7UEy427amB+nmQN22vx9fWgDcSYO2SCywEHWse0jDYU=" + } + } + }, + + { + "path": "io/netty/netty-parent/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-parent-5.0.0.Alpha2.pom": { + "sha1": "9766944a6b42805ad9a3db93b711ead720892d28", + "sha256": "sha256-EN+aaKkwmwjuE/KLc57dfUICEd2Wgu4PgDtSR3rpjXE=" + } + } + }, + + { + "path": "io/netty/netty-resolver/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-resolver-4.1.34.Final.pom": { + "sha1": "2f795deaa337a2cfacf16c8684d48b3a6e8c7a41", + "sha256": "sha256-rLPG09ViPq8LsQCD9TT6HKou/nDCvxpo02UYq+lyO9Q=" + }, + "netty-resolver-4.1.34.Final.jar": { + "sha1": "528789e98eef519960a56e44569ecb2260782fd7", + "sha256": "sha256-d0Ih7UwTC1MoZXcLEGMLwS0NQAEn2mF+4KyN4qesIJc=" + } + } + }, + + { + "path": "io/netty/netty-resolver/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-resolver-4.1.52.Final.pom": { + "sha1": "bc49f9619f5d4ada316b574b45e6e7e1b4721de2", + "sha256": "sha256-mwguVfLycRqEqsZwLOcAt5/JBFA4EjjmArOWrHZRAVI=" + }, + "netty-resolver-4.1.52.Final.jar": { + "sha1": "1fd5a58ff5df4dc9bb3027a6fbf114767d9278d5", + "sha256": "sha256-butyojOXnLRbC01tRsWkEzLFfAz4qAOx78YNu+y6CaY=" + } + } + }, + + { + "path": "io/netty/netty-resolver/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-resolver-4.1.72.Final.pom": { + "sha1": "dd29c40d6fd0272a5b5acb13de71dbdd76828b67", + "sha256": "sha256-5QFMXh5/GEXCxvWgYPo43r17Z/ARYS38AjmqKOAF/hQ=" + }, + "netty-resolver-4.1.72.Final.jar": { + "sha1": "4ff458458ea32ed1156086820b624a815fcbf2c0", + "sha256": "sha256-ZHRZiqt8ydjWz6BsBb0bGa2/f4RR291zBwszpsYLG5A=" + } + } + }, + + { + "path": "io/netty/netty-resolver/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-resolver-5.0.0.Alpha2.pom": { + "sha1": "2ccea58f7748ac25d641cac9d9c48a0f7ec534b3", + "sha256": "sha256-/l9fovSuWQjjWbj+BCNxca1JEOaIh1S5JSAs1S8qZ5g=" + }, + "netty-resolver-5.0.0.Alpha2.jar": { + "sha1": "917a91bf0f44fe73ce0d15651afb909c1b11b0f5", + "sha256": "sha256-frNirQ2gPzn4YCzA4kXiQivarDr+kBnLpPutIwP6S5k=" + } + } + }, + + { + "path": "io/netty/netty-tcnative-classes/2.0.46.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-tcnative-classes-2.0.46.Final.pom": { + "sha1": "a7320b070f2c6a163d8c4c2950ff17082a2b33d4", + "sha256": "sha256-QKcxnAeUbUeZf4KzTP6m2yDUWDyKyZ3B/4wSpNtoVyY=" + }, + "netty-tcnative-classes-2.0.46.Final.jar": { + "sha1": "9937a832d9c19861822d345b48ced388b645aa5f", + "sha256": "sha256-0+yIjcxKx5Fb+ItBfF4E/TVPQxEDKnSKaILfCTR+7Zo=" + } + } + }, + + { + "path": "io/netty/netty-tcnative-classes/2.0.70.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-tcnative-classes-2.0.70.Final.pom": { + "sha1": "c1e1a8fdfb72675d90886ad28a6949529f798b1a", + "sha256": "sha256-QRtGiEv20l66H2RTU5HsWxCH6Rjg5T0iTkIgoE+zJnU=" + }, + "netty-tcnative-classes-2.0.70.Final.jar": { + "sha1": "5f75accc769e69f2e6c1e56ba3a08b6c4bcc25f8", + "sha256": "sha256-p5wVeTE9StSKPswdAaJdoG0i1kScO8w2nCMYdJvPVbw=" + } + } + }, + + { + "path": "io/netty/netty-tcnative-parent/2.0.46.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-tcnative-parent-2.0.46.Final.pom": { + "sha1": "5247a6d71a79156c8dc1ef097a0ad9493ee8b3dd", + "sha256": "sha256-xrXuOgGkTZabGKR4qHZGGfM6aOT7XAu/bd3F9qYRN24=" + } + } + }, + + { + "path": "io/netty/netty-tcnative-parent/2.0.70.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-tcnative-parent-2.0.70.Final.pom": { + "sha1": "b5573ffca02a2c092bdb0f7e5efb7da495523ea4", + "sha256": "sha256-gHWl5UlncBGgcXXZuNqWOli4hLMujhAbdeeBUhc/3DU=" + } + } + }, + + { + "path": "io/netty/netty-transport/4.1.34.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-transport-4.1.34.Final.pom": { + "sha1": "10f67f4c6669fcd7024b67cf8fd0cd33e26a2a56", + "sha256": "sha256-o3Izc3uBW8VNgISxMolI7MILAxemaRHVzV6viElGMyk=" + }, + "netty-transport-4.1.34.Final.jar": { + "sha1": "f57be6509efa6766d90e537a775930101f26b35c", + "sha256": "sha256-Kz99OllRAd731BF5Omdb8qMllkR1/XvbvkSOkI3glEU=" + } + } + }, + + { + "path": "io/netty/netty-transport/4.1.52.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-transport-4.1.52.Final.pom": { + "sha1": "aca4eccbb984a9e4e5b7815415b8ed0f144e69dc", + "sha256": "sha256-WKiqplZtlWYARs5MjsjExxEYK+p9i36ZSWjghBjds38=" + }, + "netty-transport-4.1.52.Final.jar": { + "sha1": "970ee3790f504452bd32692f6b208a590c51f0ee", + "sha256": "sha256-mj5vjA5V3jY+seoQ/ngXl+yjlOYhht8q4LTrK84LRUE=" + } + } + }, + + { + "path": "io/netty/netty-transport/4.1.72.Final", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-transport-4.1.72.Final.pom": { + "sha1": "5cfee79648d225627c6739ced2a86982030f5f48", + "sha256": "sha256-zym9hyqMKvvNt0snqekOZxlWhfVwEdXfxHWk8mF5TbQ=" + }, + "netty-transport-4.1.72.Final.jar": { + "sha1": "99138b436a584879355aca8fe3c64b46227d5d79", + "sha256": "sha256-xfto6aZbbopRat/Ln6MjR57ntNlEnYpSnS7Ks9NxHVo=" + } + } + }, + + { + "path": "io/netty/netty-transport/5.0.0.Alpha2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "netty-transport-5.0.0.Alpha2.pom": { + "sha1": "f7db0ccc93971fd0fde3295dec00e3d268395d16", + "sha256": "sha256-pffwjP2RWElt3ku6CaUClgOBq6k/IDlSC8lNH7OBpF4=" + }, + "netty-transport-5.0.0.Alpha2.jar": { + "sha1": "340af2e29f04c00a4bc54e9be3f058f2abb51c87", + "sha256": "sha256-ZqXPTrIdh7PdUCjl+3dnYGMHB+1xL6NOdKx+j1jyy+0=" + } + } + }, + + { + "path": "io/opencensus/opencensus-api/0.21.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "opencensus-api-0.21.0.pom": { + "sha1": "22833c1b793344ae7701dd74506d86df2af5d1c0", + "sha256": "sha256-4vHozbSYIgMVzw/N2J8cCr87K8z5pOwfXlnMvnhIQ5s=" + }, + "opencensus-api-0.21.0.jar": { + "sha1": "73c07fe6458840443f670b21c7bf57657093b4e1", + "sha256": "sha256-jiyw9jkdjrChvNAed0iIPwAzsZQXVPTtPxnSw+Qnb8g=" + } + } + }, + + { + "path": "io/opencensus/opencensus-contrib-grpc-metrics/0.21.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "opencensus-contrib-grpc-metrics-0.21.0.pom": { + "sha1": "075621314e97c29e7c43cb94edd6f796e1dae5be", + "sha256": "sha256-kIiFoeY81u6MXSgUBNQe09W8saZC4cCjqyhdN47wdY4=" + }, + "opencensus-contrib-grpc-metrics-0.21.0.jar": { + "sha1": "f07d3a325f1fe69ee40d6b409086964edfef4e69", + "sha256": "sha256-Kfx5QBCCMBVCyridcFTS8IJfGESSZUyVACBVPvT/Dvg=" + } + } + }, + + { + "path": "io/perfmark/perfmark-api/0.23.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "perfmark-api-0.23.0.pom": { + "sha1": "c0750154030a99c1771178f8b0e1bd0de3f41bab", + "sha256": "sha256-XjuOBAYVmLz0rUiktEZZgFZW1rUoKmhT2ALxyctw50k=" + }, + "perfmark-api-0.23.0.jar": { + "sha1": "0b813b7539fae6550541da8caafd6add86d4e22f", + "sha256": "sha256-xwW1wQwY/zAyuegXQrwvaw5WB/am38DIrQz/ddSRMEI=" + }, + "perfmark-api-0.23.0.module": { + "sha1": "7641298029badb3631f6471d0a597fc7ee3b71a5", + "sha256": "sha256-hKEvNIF9Hy9534ELy6rLKZAGeDhNPdBQLfC9ljpPP9s=" + } + } + }, + + { + "path": "it/unimi/dsi/fastutil/7.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fastutil-7.2.0.pom": { + "sha1": "e068a0be19991231f5020d2748e709325bb745f2", + "sha256": "sha256-lTsRZSGnNXXu6ZDj8sNqiS+wiLstmjAnyCGTy3oBPvc=" + }, + "fastutil-7.2.0.jar": { + "sha1": "5ad3a2bb04143f70aa0765fc29fc29571a7d6b34", + "sha256": "sha256-dPoggEN0BkL35usJ+roVllIYrS9QzjAg77EAE25LWRw=" + } + } + }, + + { + "path": "it/unimi/dsi/fastutil/8.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fastutil-8.4.0.pom": { + "sha1": "7ec92f6e71ce4abebde5cebd574a6f27528959d7", + "sha256": "sha256-EQ5vZsfV7TyRQI6wtJ1AeRYU3syELrktdUeuf4hIKCI=" + }, + "fastutil-8.4.0.jar": { + "sha1": "60fbeaa50799581f6244fcff1cebba0353ec0141", + "sha256": "sha256-KtKCSkoKDrg2tS7i/IS6ITT0S857+lQBWuPzHHEKMHE=" + } + } + }, + + { + "path": "jakarta/activation/jakarta.activation-api/1.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jakarta.activation-api-1.2.1.pom": { + "sha1": "b9c1b2502949970360efe8d75ec5268d87d38a82", + "sha256": "sha256-QlhcsH3afyOqBOteCUAGGUSiRqZ609FpQvvlaf8DzTE=" + }, + "jakarta.activation-api-1.2.1.jar": { + "sha1": "562a587face36ec7eff2db7f2fc95425c6602bc1", + "sha256": "sha256-iwoPUvqLBcVDGSGgY+2GbvqkHa3y46fuPhlh8rDZZFs=" + } + } + }, + + { + "path": "jakarta/activation/jakarta.activation-api/1.2.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jakarta.activation-api-1.2.2.pom": { + "sha1": "9653afc910af5c200113efac90e205f9b36b8d39", + "sha256": "sha256-XlD+k4BoRx9QSn78QHGCNCW1IOq8b4DHLZNevVRoMJE=" + }, + "jakarta.activation-api-1.2.2.jar": { + "sha1": "99f53adba383cb1bf7c3862844488574b559621f", + "sha256": "sha256-oYepORA671hJp6+EvX4nvi0SDEEK8pFDc3X/4GH08J0=" + } + } + }, + + { + "path": "jakarta/activation/jakarta.activation-api/2.1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jakarta.activation-api-2.1.3.pom": { + "sha1": "d50eecb3b12aa729c1bd58ca3a4a61b22d98563d", + "sha256": "sha256-slSZQMF7aGWjT2E1t3Iu2Mv+9tC2wNs3LDDwNGvIzVg=" + }, + "jakarta.activation-api-2.1.3.jar": { + "sha1": "fa165bd70cda600368eee31555222776a46b881f", + "sha256": "sha256-AbF21xihaSY+eCkGkfxHmXcYa8xrMzSHMlCE1lhvRic=" + } + } + }, + + { + "path": "jakarta/xml/bind/jakarta.xml.bind-api-parent/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jakarta.xml.bind-api-parent-2.3.2.pom": { + "sha1": "7be72e8345e33f747d5d813e313abd8df25f259e", + "sha256": "sha256-FaVbfVN8n5lwrq0o0q+XwFn2X/YQL3a70p8SR92Kbfs=" + } + } + }, + + { + "path": "jakarta/xml/bind/jakarta.xml.bind-api-parent/4.0.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jakarta.xml.bind-api-parent-4.0.2.pom": { + "sha1": "5d3186bca50baf84bb24db19bafcff2d131d02cb", + "sha256": "sha256-SCzbRtTdL1r33IhMim3nPd/+ba9UnvNqbAd/1uQe7Qg=" + } + } + }, + + { + "path": "jakarta/xml/bind/jakarta.xml.bind-api/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jakarta.xml.bind-api-2.3.2.pom": { + "sha1": "6142021e7186bf29f5f7639b968cfe250ce086dd", + "sha256": "sha256-tTeziNurTMBpC50vsMdBJNZyUxc0VnrPblMTDqsTGtY=" + }, + "jakarta.xml.bind-api-2.3.2.jar": { + "sha1": "8d49996a4338670764d7ca4b85a1c4ccf7fe665d", + "sha256": "sha256-aRVjBAeb3u2fwK47OTifGbPMS6REO8gFCJlTlOrXQuo=" + } + } + }, + + { + "path": "jakarta/xml/bind/jakarta.xml.bind-api/4.0.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jakarta.xml.bind-api-4.0.2.pom": { + "sha1": "da6b967605200a3f7ed3b5874159046cc9e8ecc5", + "sha256": "sha256-q3Jz4mpUgks7czPUlW5uzSbeE6XrPZDuYwIxk6ktcr4=" + }, + "jakarta.xml.bind-api-4.0.2.jar": { + "sha1": "6cd5a999b834b63238005b7144136379dc36cad2", + "sha256": "sha256-DWvP5Hdj6FBHrPfDmDNtyE/4XrytCny287nT6YEkVAY=" + } + } + }, + + { + "path": "javax/activation/activation/1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "activation-1.1.pom": { + "sha1": "fd9dd0faa8f03f3ce0dc4eec22e57e818d8b9897", + "sha256": "sha256-1JDlQKEVBLnXFxixyF/vez3mgCNhKQgkU5sHbVj6qKA=" + }, + "activation-1.1.jar": { + "sha1": "e6cb541461c2834bdea3eb920f1884d1eb508b50", + "sha256": "sha256-KIHHnJ1u8BxY5ivuoT6dGsi4uqFvL8GYrW5ndt79zdM=" + } + } + }, + + { + "path": "javax/activation/javax.activation-api/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javax.activation-api-1.2.0.pom": { + "sha1": "1aa9ef58e50ba6868b2e955d61fcd73be5b4cea5", + "sha256": "sha256-2ikm88i+iYZDzBCs3sbeCwNRpX+yc1dw+gF3sGrecbk=" + }, + "javax.activation-api-1.2.0.jar": { + "sha1": "85262acf3ca9816f9537ca47d5adeabaead7cb16", + "sha256": "sha256-Q/3vC1ts6zGwQksgi5MMdKtY+sLO63s/b9OuuLXKQ5M=" + } + } + }, + + { + "path": "javax/annotation/javax.annotation-api/1.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javax.annotation-api-1.3.2.pom": { + "sha1": "302fe96ef206b17f82893083b51b479541fa25ab", + "sha256": "sha256-RqSiUcpAbnjkhT16K66DKChEpJkoUUOe6aHyNxbwa5c=" + }, + "javax.annotation-api-1.3.2.jar": { + "sha1": "934c04d3cfef185a8008e7bf34331b79730a9d43", + "sha256": "sha256-4EulGVvNVV3JVlD3zGFNFR5LzVLSmhC4qiGX86uJq5s=" + } + } + }, + + { + "path": "javax/inject/javax.inject/1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "javax.inject-1.pom": { + "sha1": "b8e00a8a0deb0ebef447570e37ff8146ccd92cbe", + "sha256": "sha256-lD4SsQBieARjj6KFgFoKt4imgCZlMeZQkh6/5GIai/o=" + }, + "javax.inject-1.jar": { + "sha1": "6975da39a7040257bd51d21a231b76c915872d38", + "sha256": "sha256-kcdwRKUMSBY2wy2Rb9ickRinIZU5BFLIEGUID5V95/8=" + } + } + }, + + { + "path": "javax/xml/bind/jaxb-api-parent/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-api-parent-2.3.1.pom": { + "sha1": "26101e8a1a09e16cdf277a4591f6d29917b2e06e", + "sha256": "sha256-zRvqpFYNxN/bgmudgJ5GTbIlJt+1QmS654pv9++wjh8=" + } + } + }, + + { + "path": "javax/xml/bind/jaxb-api-parent/2.4.0-b180830.0359", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-api-parent-2.4.0-b180830.0359.pom": { + "sha1": "0dba9e0dbd65de0095f5be19cf10bd838b11a4c6", + "sha256": "sha256-ctEy4shY0iMPFdBI8ek6J5xAxOnshLxW+fLz61r0tLg=" + } + } + }, + + { + "path": "javax/xml/bind/jaxb-api/2.2.12-b140109.1041", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-api-2.2.12-b140109.1041.pom": { + "sha1": "a400fbd62fa68fae267b5641c3e4bb740a0f7335", + "sha256": "sha256-IJKn2Nm8VpjFmwlL3qRmIpFcSMg65m1bWtVJx78WFVs=" + }, + "jaxb-api-2.2.12-b140109.1041.jar": { + "sha1": "7ed0e0d01198614194d56dfb03d9d95aa311824c", + "sha256": "sha256-teYM2Le1/wHOSnTF3QCPT70UztNJXQtHuFz+3BgiEfI=" + } + } + }, + + { + "path": "javax/xml/bind/jaxb-api/2.2.12", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-api-2.2.12.pom": { + "sha1": "650e118d32cfcbbe5da8727ccda4023e4b2331bc", + "sha256": "sha256-Gs08hdTP48Sa4eKsKXdCujnaW7swo60OQXbqbGNI/O8=" + }, + "jaxb-api-2.2.12.jar": { + "sha1": "4c83805595b15acf41d71d49e3add7c0e85baaed", + "sha256": "sha256-aKYh7BhIX5UdCax29D5X7uOU2+QsuPKkxZyTKW+p3MY=" + } + } + }, + + { + "path": "javax/xml/bind/jaxb-api/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-api-2.3.1.pom": { + "sha1": "c42c51ae84892b73ef7de5351188908e673f5c69", + "sha256": "sha256-ErIM+SJ3NEXDRFwog8v2cfqYIRHpv5+HUCD5MTs4FLE=" + }, + "jaxb-api-2.3.1.jar": { + "sha1": "8531ad5ac454cc2deb9d4d32c40c4d7451939b5d", + "sha256": "sha256-iLlVoN9XiAomp0cIvDT3Tcr46/TniEOii1Dq6UVzKwY=" + } + } + }, + + { + "path": "javax/xml/bind/jaxb-api/2.4.0-b180830.0359", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-api-2.4.0-b180830.0359.pom": { + "sha1": "8094057c54ba7d482d78c7a5bfe2c9e316713808", + "sha256": "sha256-sck/wwHX9f5M3hPRlTKZJR2jfv/8kfUjg1UEw/+HNwc=" + }, + "jaxb-api-2.4.0-b180830.0359.jar": { + "sha1": "b54184b7dcab2031add3f525550c7f1b7e12209d", + "sha256": "sha256-VrnpcCdTdjAHQ1Fi6niAVe/P78hquSDwMsBBHcVHuDY=" + } + } + }, + + { + "path": "javax/xml/stream/stax-api/1.0-2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stax-api-1.0-2.pom": { + "sha1": "5379b69f557c5ab7c144d22bf7c3768bd2adb93d", + "sha256": "sha256-KGTxnahP1Sdj11oZenF3my3svMqsPrTkdg/8iExa9KI=" + }, + "stax-api-1.0-2.jar": { + "sha1": "d6337b0de8b25e53e81b922352fbea9f9f57ba0b", + "sha256": "sha256-6McOvXb5gslYKoLvgs9s4Up9WKSk3KXLe3/JiMgAibc=" + } + } + }, + + { + "path": "junit/junit/4.12", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "junit-4.12.pom": { + "sha1": "35fb238baee3f3af739074d723279ebea2028398", + "sha256": "sha256-kPFj944/+28cetl96efrpO6iWAcUG4XW0SvmfKJUScQ=" + }, + "junit-4.12.jar": { + "sha1": "2973d150c0dc1fefe998f834810d68f278ea58ec", + "sha256": "sha256-WXIfCAXiI9hLkGd4h9n/Vn3FNNfFAsqQPAwrF/BcEWo=" + } + } + }, + + { + "path": "junit/junit/4.13.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "junit-4.13.2.pom": { + "sha1": "73bc5be628edeb297a1caf421a5a2e494798b92f", + "sha256": "sha256-Vptpd+5GA8llwcRsMFj6bpaSkbAWDraWTdCSzYnq3ZQ=" + }, + "junit-4.13.2.jar": { + "sha1": "8ac9e16d933b6fb43bc7f576336b8f4d7eb5ba12", + "sha256": "sha256-jklbY0Rp1k+4rPo0laBly6zIoP/1XOHjEAe+TBbcV9M=" + } + } + }, + + { + "path": "me/zhanghai/android/materialprogressbar/library/1.4.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "library-1.4.2.pom": { + "sha1": "fdab48c3bb74d27b46bceba3dde7b3a65752ebb2", + "sha256": "sha256-y7D9pnA67VSOindUupFgfK5l+Ach+XjEsjNxedzStic=" + }, + "library-1.4.2.aar": { + "sha1": "6bd6cd6e9107e22a5d3b4d3d3713a6864aae854e", + "sha256": "sha256-91gbP3GaJ0riTwJZifORpoyUKZzrhuOye0Hy1jSiKNA=" + }, + "library-1.4.2.aar.asc": { + "sha1": "2e757827ed3e246f012b75ae15503d9d599e1c2f", + "sha256": "sha256-c3HCoQozdznND0gmNUmItVkz3ivYayS9w0jNPZqUMWc=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy-agent/1.11.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-agent-1.11.13.pom": { + "sha1": "462542bd3a41955bedcb1b86512cf10d1280575d", + "sha256": "sha256-sDhEB2p1yahs4azSrqxPu3kEJ0AzlrbGi7/KlTZLbrQ=" + }, + "byte-buddy-agent-1.11.13.jar": { + "sha1": "8c7aaa0ef9863fa89a711bfc5d8e2e0affa0d67f", + "sha256": "sha256-SbQ7DRD4ux2ADVYTe98PRGKEEuvh+9gE5F82PUlYYPo=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy-agent/1.11.19", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-agent-1.11.19.pom": { + "sha1": "40e756adf87ec6c7ec8895f786857d94e1af864a", + "sha256": "sha256-BIgsNByoXV/NM4g3zNXmK3PF4mKvCEVTfWVrs3spVs8=" + }, + "byte-buddy-agent-1.11.19.jar": { + "sha1": "25c27513103bd4a166778b20a0be0c84dc5364f6", + "sha256": "sha256-hxunAh+MM1oUtHAS8b/iz4yqfAtaV90eAO4rnh8UP8A=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy-agent/1.15.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-agent-1.15.11.pom": { + "sha1": "4015ca3530951c4336155d578de83f0e6b900f8c", + "sha256": "sha256-tfoTlvFHl7jYCIJ+d0O6il8gO0iJvjLklj1EvV7XWag=" + }, + "byte-buddy-agent-1.15.11.jar": { + "sha1": "a38b16385e867f59a641330f0362ebe742788ed8", + "sha256": "sha256-MW0sB5XCpNTEdW8ub5NJg3x0MKw04Ed+rYdNBfXMGeU=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy-parent/1.11.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-parent-1.11.13.pom": { + "sha1": "e3334dfff8e86e2caf5c632bc98e6fca2f1a4222", + "sha256": "sha256-BZo8mkiOF2lhULG5budtAJyWs6pTVp5jxyS6s6SIAQo=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy-parent/1.11.19", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-parent-1.11.19.pom": { + "sha1": "96088ca3c8dfa274938bfa10dff039bc482ee2fc", + "sha256": "sha256-G9uEjn7AGSXfj1HBn4Mg7dRuctPf/TDgOvgOoB4c/AI=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy-parent/1.15.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-parent-1.15.10.pom": { + "sha1": "6f8c0f39f52c09fbf4df945ff563f5fd81130724", + "sha256": "sha256-ZX1HwTcRTN8u0plv73gJYTgX3v9k2L4TI/QJiaO6BG4=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy-parent/1.15.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-parent-1.15.11.pom": { + "sha1": "80e27b5d9cf8ba40dfd0b8ee8f7915d7237aec6d", + "sha256": "sha256-jcUZ16PnkhEqfNhB6vvsTwDbxjPQha3SDEXwq0dspJY=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy/1.11.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-1.11.13.pom": { + "sha1": "3d4916e320cae88ae58704cedc347c1eebca0ca8", + "sha256": "sha256-Krna5rnDCPcpvGMb1osG0i+X0ky2Ao85/lRMqpDWrJc=" + }, + "byte-buddy-1.11.13.jar": { + "sha1": "0a85d4d74de5ce7a4dd5cbbd337ced6af2740acd", + "sha256": "sha256-4p+nW5A0Mqxk0FwYwZ0OO5Am50q9pSu9b5Bl5V9KKfU=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy/1.11.19", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-1.11.19.pom": { + "sha1": "57af7507207663daddc2dabb327cdbcb321f8e1c", + "sha256": "sha256-gQK23PoKt5S3NLcCUm6IgcX+tIgZBVb2li+rnNkUQIg=" + }, + "byte-buddy-1.11.19.jar": { + "sha1": "3ee8778b8e5287c57c9c3f3fd4c1c23e93e8779a", + "sha256": "sha256-TNEDjKVFBzI78EdCY09hi7FiSRTfR6i0pNz6eXvWqNU=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy/1.15.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-1.15.10.pom": { + "sha1": "c968dc18f092176badbc9bd389ce53cbb6f13aa5", + "sha256": "sha256-BCz/E2+5FPl2I7oremhdmEuGnb+1tFIdx8kiejJZznA=" + }, + "byte-buddy-1.15.10.jar": { + "sha1": "635c873fadd853c084f84fdc3cbd58c5dd8537f9", + "sha256": "sha256-2DkNIGhaQaK9ymQPlYlCzZG8vyHEJHBJS99XUtmgexQ=" + } + } + }, + + { + "path": "net/bytebuddy/byte-buddy/1.15.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "byte-buddy-1.15.11.pom": { + "sha1": "c625e547e59a502e99eba31c1a5b1c514e8c64f5", + "sha256": "sha256-IFuLJUGWcX6B2tZyu4aacZr8lt8pf5fYEe/+H0NlPa4=" + }, + "byte-buddy-1.15.11.jar": { + "sha1": "f61886478e0f9ee4c21d09574736f0ff45e0a46c", + "sha256": "sha256-+giZiq4ee9roO94HEsUOhETXHA4MGWuyJHrejUrQ65A=" + } + } + }, + + { + "path": "net/java/dev/jna/jna-platform/5.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jna-platform-5.6.0.pom": { + "sha1": "f86d5b088d311a7dee895e06cb2a08ccada7cbde", + "sha256": "sha256-G+s1y0GE5skGp+Murr2FLdPaCiY5YumRNKuUWDI5Tig=" + }, + "jna-platform-5.6.0.jar": { + "sha1": "d18424ffb8bbfd036d71bcaab9b546858f2ef986", + "sha256": "sha256-ns6ovysbOZY5OdGLcEZO72DFCP7Ygg+dyroMNVGOq/c=" + } + } + }, + + { + "path": "net/java/dev/jna/jna/5.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jna-5.6.0.pom": { + "sha1": "e542808a6326ec0302c0b09011cb4363e1ece380", + "sha256": "sha256-X+gbAlWXjyRhbTexBgi3lJil8wc+HZsgONhzaoMfJgg=" + }, + "jna-5.6.0.aar": { + "sha1": "a3879b46f9067438eacc1b43e3cc93c6690d0865", + "sha256": "sha256-K580lyGLMsOZ0ey+oLANT0VT7c9mmRntHhqH2qd6z4c=" + }, + "jna-5.6.0.aar.asc": { + "sha1": "b1e22214259467c3b42b0947512d6eb6e6a0a0ed", + "sha256": "sha256-DZPp+ErKJ90+I2V1c4hRDzVtxLnKjboLpx5VxZYaRSQ=" + }, + "jna-5.6.0.jar": { + "sha1": "330f2244e9030119ab3030fc3fededc86713d9cc", + "sha256": "sha256-VVfiNaiqL5dm1dxgnWeUjyqIMsLXls6p7x1svgs7fq8=" + } + } + }, + + { + "path": "net/java/jvnet-parent/1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jvnet-parent-1.pom": { + "sha1": "b55a1b046dbe82acdee8edde7476eebcba1e57d8", + "sha256": "sha256-KBRAgRJo5l2eJms8yJgpfiFOBPCXQNA4bO60qJI9Y78=" + } + } + }, + + { + "path": "net/java/jvnet-parent/3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jvnet-parent-3.pom": { + "sha1": "f8f3be3e980551a39b5679411e171aeb6931aaec", + "sha256": "sha256-MPV4nvo53b+WCVqto/wSYMRWH68vcUaGcXyy3FBJR1o=" + } + } + }, + + { + "path": "net/java/jvnet-parent/4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jvnet-parent-4.pom": { + "sha1": "a80cde31667f91784a6c68a06b5c6a77418f7822", + "sha256": "sha256-RxOVc1VJSVKXyP+Tm5oy4IuRMCAg/3c1htJ+SXq7j7s=" + } + } + }, + + { + "path": "net/java/jvnet-parent/5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jvnet-parent-5.pom": { + "sha1": "5343c954d21549d039feebe5fadef023cdfc1388", + "sha256": "sha256-GvaZ+Nndq2f5oNIC+9eRXrA2Klpt/V/8VMr6NGXJywo=" + } + } + }, + + { + "path": "net/sf/jopt-simple/jopt-simple/4.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jopt-simple-4.9.pom": { + "sha1": "ea3cd0a93e4e8adc1cdadd544c9168bc5aa985a8", + "sha256": "sha256-evfi2LJLR5jwTCt9okyfvRt1V7TgF8IFRIFWWRYHkJI=" + }, + "jopt-simple-4.9.jar": { + "sha1": "ee9e9eaa0a35360dcfeac129ff4923215fd65904", + "sha256": "sha256-JsWFbpVLX4ZNt28TuGkZtZxu7Pn9kwuWuqiIRia68vU=" + } + } + }, + + { + "path": "net/sf/kxml/kxml2/2.3.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kxml2-2.3.0.pom": { + "sha1": "8efa75f9cdc57687076b2125b1a098e6f42e737d", + "sha256": "sha256-Mc5gb06VGJNimbsNJ8l4+mHhhf0d58mHT+lZpT40poU=" + }, + "kxml2-2.3.0.jar": { + "sha1": "ccbc77a5fd907ef863c29f3596c6f54ffa4e9442", + "sha256": "sha256-8mTdn3mh/eEM5ezFMiHv8kvkyTMcgwt9UvLwintjPeI=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-base/5.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-base-5.2.1.pom": { + "sha1": "03e04f64ce189b9cfa7e08baae0d6f657a0204fd", + "sha256": "sha256-b4HsdG4NhL7o30WGI5iDIk92r6cf/5iXDsF6OV3aOsM=" + }, + "proguard-base-5.2.1.jar": { + "sha1": "4f61348b4e7c943b85679dcb697f3a5fc3101921", + "sha256": "sha256-8WO91zxYy8m3iasU9dYCtJlNvYLqy11Us70qld5j878=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-base/5.3.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-base-5.3.3.pom": { + "sha1": "6216930869e6faab2954ce7075727f64c606f3a8", + "sha256": "sha256-5R77LsFAsoSbmID7Fhy24w3WcjaKU0wfR6tG1/U32co=" + }, + "proguard-base-5.3.3.jar": { + "sha1": "988b6b0636ce343d4962b3b37f6319dcc6e99a61", + "sha256": "sha256-HS6INxe4q30/zJ+gEDTRZYCanJTapjA6g48R5NNq3IY=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-base/6.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-base-6.0.3.pom": { + "sha1": "d53b864b20f6ab75c66017adcb06c44a912a2076", + "sha256": "sha256-KoI9efJnVYKpiNU5Rm0bF16Tl4Lvvg9uYfBssWXtSQc=" + }, + "proguard-base-6.0.3.jar": { + "sha1": "7135739d2d3834964c543ed21e2936ce34747aca", + "sha256": "sha256-dbyJaV+mbnU3Ts2kObgCQG8Ksdgm3Thu7QojsfCp0mQ=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-base/6.3.0beta1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-base-6.3.0beta1.pom": { + "sha1": "6ec223be63e45327a5ec8fdd4620e25f6ce4c82a", + "sha256": "sha256-xlJ2MUXixMgBKD2i/iArA9XzogNQe2h0W1vTSl38d6U=" + }, + "proguard-base-6.3.0beta1.jar": { + "sha1": "997b026bff2518c12194d5cd967bd28970d3c3a9", + "sha256": "sha256-xAuDnHPLortaxUGDDFhqj9kwZi1zaKPhHh5Vqguo+Qs=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-gradle/5.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-gradle-5.2.1.pom": { + "sha1": "3f835379d233d3c8dc6076dcfc5e38d077b8ac4b", + "sha256": "sha256-b109T1fqMuQGOXUS/JsYWMTYLe5jUn/9eeGyj70KLWE=" + }, + "proguard-gradle-5.2.1.jar": { + "sha1": "5e9956c050fd84fa043517e77c4c0ff175a6b002", + "sha256": "sha256-77BMSgXzZf0UjjBqgh41SppevX1Q+fQnlTi01ZqJx7s=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-gradle/5.3.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-gradle-5.3.3.pom": { + "sha1": "57a3eda3d39f004a8cfc724f1a98af89b91f79c2", + "sha256": "sha256-8mvewrlJZHDAzmOw0pCBboAA7wGNqqJZWX666Uf9aQ8=" + }, + "proguard-gradle-5.3.3.jar": { + "sha1": "ad23a0505f58d0dfc95bb1472decc397460406c9", + "sha256": "sha256-80s6C2LNtGjrfoM13k7ch5WNwpkwbCtLqOoV5qS+EGo=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-gradle/6.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-gradle-6.0.3.pom": { + "sha1": "6583a613d9b6d02a699d4dd24d8b8825da3d8e8b", + "sha256": "sha256-WlxzF9aM6A0dQMnYvU44gU1C0bFsJlFG4zNjSDOjWlc=" + }, + "proguard-gradle-6.0.3.jar": { + "sha1": "e5becf2356695a396b788110e386c38bad523bfc", + "sha256": "sha256-zBKxFo5SFJHdDmh8/r7BGkr4dLIq9w4QzyoFtHygDI8=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-parent/5.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-parent-5.2.1.pom": { + "sha1": "01bc4a2b3094f3e2112e61398e15bfb067841eed", + "sha256": "sha256-LiUC2q3X9ciFCs0GqkZOIPtLwTbKpyYNpxrlgmUEgwY=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-parent/5.3.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-parent-5.3.3.pom": { + "sha1": "45c24fb869b3c809c0a1a00081fc353184c2472c", + "sha256": "sha256-AohBRQnGJbjdVEOE+9KfUCvci0+Gz8JmJaoj7vn6YFc=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-parent/6.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-parent-6.0.3.pom": { + "sha1": "f76c32555b227be8f5d8c753ccc7f49c5196345e", + "sha256": "sha256-2HJmv9IxLDsDbErHCTEK+jXESM6xgCfDuHoz0Dxt4KA=" + } + } + }, + + { + "path": "net/sf/proguard/proguard-parent/6.3.0beta1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "proguard-parent-6.3.0beta1.pom": { + "sha1": "6a0ab47c2c7ca00006ca9e69048beb25907daa31", + "sha256": "sha256-vog8dxc7EDZ8Z/A6r20PVMJfGdB8di8/9A1rJn2g4t8=" + } + } + }, + + { + "path": "org/abego/treelayout/org.abego.treelayout.core/1.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.abego.treelayout.core-1.0.1.pom": { + "sha1": "e8da72b4e31c6610ca57fde5f73d5ee4d1d5f957", + "sha256": "sha256-fIy/ymTpcEQNTyh/NRapbABpKgOU/KHVexKpepbw6hE=" + }, + "org.abego.treelayout.core-1.0.1.jar": { + "sha1": "e31e79cba7a5414cf18fa69f3f0a2cf9ee997b61", + "sha256": "sha256-grnjYxrMcIwRUhb6ePl3gNioHbt/FfjYIpGh3DqDsdA=" + } + } + }, + + { + "path": "org/antlr/ST4/4.0.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ST4-4.0.8.pom": { + "sha1": "116663d33389525e932a4ff7adaf66eb06caf277", + "sha256": "sha256-PAiQ3scRdOs7o9QEyp40GQH/awQhgIsAcTsNuxMGwXw=" + }, + "ST4-4.0.8-complete.jar": { + "sha1": "e970d1a0b3c1680d48dc9f9c86026bfe8d875dee", + "sha256": "sha256-i5Mgkm5vDOMShPhgqX9sLW0Scz7rCxaeUfgRKfVW9Sc=" + }, + "ST4-4.0.8.jar": { + "sha1": "0a1c55e974f8a94d78e2348fa6ff63f4fa1fae64", + "sha256": "sha256-WMqrxAyfdLC1mT/YaOD2SlDAdZCU5qJRqq+tmO38ejs=" + } + } + }, + + { + "path": "org/antlr/ST4/4.3.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ST4-4.3.4.pom": { + "sha1": "24c80ba529f26698567f91773d24f8c01e1bfa73", + "sha256": "sha256-nnwfPkiZGUQOjBMInlljcp1bf4D3AjO/uuMJxkmryj4=" + }, + "ST4-4.3.4.jar": { + "sha1": "bf68d049dd4e6e104055a79ac3bf9e6307d29258", + "sha256": "sha256-+SesOExG10n4texolypTrtIeADE1CSmWFu23O/oV/zM=" + } + } + }, + + { + "path": "org/antlr/antlr4-master/4.5.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-master-4.5.3.pom": { + "sha1": "c7e4123f86c15b492adba2fc5377949c4ea43946", + "sha256": "sha256-QBh31ecK1Znpts/xhDTqAzL2N7UfjsaDUmRsg2+bsqQ=" + } + } + }, + + { + "path": "org/antlr/antlr4-master/4.13.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-master-4.13.2.pom": { + "sha1": "c16aa4679ed71d485c700238d9b4730dd3d5e82e", + "sha256": "sha256-Ct2gJmhYc/ZRNgF4v/xEbO7kgzCBc5466dbo8H6NkCo=" + } + } + }, + + { + "path": "org/antlr/antlr4-runtime/4.13.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-runtime-4.13.2.pom": { + "sha1": "d07a9559966ddaa848df4dd2099f15ceaab173ca", + "sha256": "sha256-A84HonlsURsMlNwU/YbM3W44KMV5Z60jg94wTg0Runk=" + }, + "antlr4-runtime-4.13.2.jar": { + "sha1": "fc3db6d844df652a3d5db31c87fa12757f13691d", + "sha256": "sha256-3T6KE6LWab+E+42DTeNc5IdfJxV2mNIGJB7ISIqtyvc=" + } + } + }, + + { + "path": "org/antlr/antlr4/4.5.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr4-4.5.3.pom": { + "sha1": "9ecf07c69a057cb13cea8a153d242007f5cc8003", + "sha256": "sha256-ik5LMu7apyl2p1fhLPHf50JyXbC3MRvxdt2Te6QjY4Q=" + }, + "antlr4-4.5.3.jar": { + "sha1": "f35db7e4b2446e4174ba6a73db7bd6b3e6bb5da1", + "sha256": "sha256-oy3nOc/fUVd05pb5GqlpfS53MeXLUEXKiktlf4sbT7Q=" + } + } + }, + + { + "path": "org/antlr/antlr-master/3.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-master-3.3.pom": { + "sha1": "c6eef5b4a8e7af1c43979108eadd3a6fbb4ac3d2", + "sha256": "sha256-vX8O/IHH1lZOLglg4bMrmzRzYv5YeARXNvuG1t7Drho=" + } + } + }, + + { + "path": "org/antlr/antlr-master/3.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-master-3.5.2.pom": { + "sha1": "0e9d18b3d8c228ff9786ee977f44800897fe15be", + "sha256": "sha256-QtkaUx6lEA6wm1QaoALDuQjo8oK9c7bi9S83HvEzG9Y=" + } + } + }, + + { + "path": "org/antlr/antlr-master/3.5.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-master-3.5.3.pom": { + "sha1": "a0f2d06c86cd02cb9ff7fb5a1c93fb0ba3eb5cec", + "sha256": "sha256-6p43JQ9cTC52tlOL6XtX8zSb2lhe31PzypfiB7OFuJU=" + } + } + }, + + { + "path": "org/antlr/antlr-runtime/3.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-runtime-3.3.pom": { + "sha1": "2fc09acfad331cd0939b058947a87294762f39c8", + "sha256": "sha256-0JzcLaVSnr/myueMlXq3NThoFx85abfMbmQEs4XehNE=" + }, + "antlr-runtime-3.3.jar": { + "sha1": "ccd65b08cbc9b7e90b9facd4d125a133c6f87228", + "sha256": "sha256-NsA8jgi+BBoPESBzsdg8w7Oht8qAG3kklSHL9OuuRZE=" + } + } + }, + + { + "path": "org/antlr/antlr-runtime/3.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-runtime-3.5.2.pom": { + "sha1": "af8ae5172f0c499d932d465673c9833c8777c1dd", + "sha256": "sha256-RqnCIAu4sSvXEkqnpQl/9JCZkIMpyFGgTLIFFCCqfyU=" + }, + "antlr-runtime-3.5.2.jar": { + "sha1": "cd9cd41361c155f3af0f653009dcecb08d8b4afd", + "sha256": "sha256-zj/I7LEPOemjzdy7LONQ0nLZzT0LHhjm/nPDuTichzQ=" + } + } + }, + + { + "path": "org/antlr/antlr-runtime/3.5.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-runtime-3.5.3.pom": { + "sha1": "1a980d8984cb2d929dbd11d765fbc03ab6092b2a", + "sha256": "sha256-EymODgqvr0FP99RAZCfKtuxPv6NkJ/bXEDxDLzLAfSU=" + }, + "antlr-runtime-3.5.3.jar": { + "sha1": "9011fb189c5ed6d99e5f3322514848d1ec1e1416", + "sha256": "sha256-aL+fWjPfyzQDNJXFh+Yja+9ON6pmEpGfWx6EO5Bmn7k=" + } + } + }, + + { + "path": "org/antlr/antlr/3.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "antlr-3.5.2.pom": { + "sha1": "d6830744a9a30a9c0afebfb84a5fdd6cc7e9d4ab", + "sha256": "sha256-Bl5egGYv64WHldPAH3cUJHvdMZRZcF7hOxpLGWj6IuQ=" + }, + "antlr-3.5.2.jar": { + "sha1": "c4a65c950bfc3e7d04309c515b2177c00baf7764", + "sha256": "sha256-WsNsKs+woPPTfa/iC1tXDyZD4tAAxkjURQPCc4vmQ98=" + } + } + }, + + { + "path": "org/antlr/stringtemplate/3.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stringtemplate-3.2.1.pom": { + "sha1": "88562344bdb06d01a8f410aa624538e345086595", + "sha256": "sha256-tF6CZVqlpI8z0TpD5DRUJrFWM1s14kta6hLbWCPBahY=" + }, + "stringtemplate-3.2.1.jar": { + "sha1": "59ec8083721eae215c6f3caee944c410d2be34de", + "sha256": "sha256-9mznLpZeUwHLDwIOVNK6atdv65Gzy/ww278AwGpt9tc=" + } + } + }, + + { + "path": "org/antlr/stringtemplate/4.0.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stringtemplate-4.0.2.pom": { + "sha1": "fdf5de19491ec50c9f621ab93c3d3e36879bcfa3", + "sha256": "sha256-Ak9tLOcl6a0x0ALB1DvqoJKgeGTvYNvxaemXizk2SRY=" + }, + "stringtemplate-4.0.2.jar": { + "sha1": "e28e09e2d44d60506a7bcb004d6c23ff35c6ac08", + "sha256": "sha256-gFbVWG4bGNPe9jR7XQIKhXIthQu59Nepqv5PhCxlHvk=" + } + } + }, + + { + "path": "org/apache/apache/4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-4.pom": { + "sha1": "602b647986c1d24301bc3d70e5923696bc7f1401", + "sha256": "sha256-npMjomuo6yOU7+8MltMbcN9XCAhjDcFHyrHnNUHMUZQ=" + } + } + }, + + { + "path": "org/apache/apache/9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-9.pom": { + "sha1": "de55d73a30c7521f3d55e8141d360ffbdfd88caa", + "sha256": "sha256-SUbmClR8jtpp87wjxbbw2tz4Rp6kmx0dp940rs/PGN0=" + } + } + }, + + { + "path": "org/apache/apache/13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-13.pom": { + "sha1": "15aff1faaec4963617f07dbe8e603f0adabc3a12", + "sha256": "sha256-/1E9sDYf1BI3vvR4SWi8FarkeNTsCpSW+BEHLMrzhB0=" + } + } + }, + + { + "path": "org/apache/apache/15", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-15.pom": { + "sha1": "95c70374817194cabfeec410fe70c3a6b832bafe", + "sha256": "sha256-NsLy+XmsZ7RQwMtIDk6br2tA86aB8iupaSKH0ROa1JQ=" + } + } + }, + + { + "path": "org/apache/apache/16", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-16.pom": { + "sha1": "8a90e31780e5cd0685ccaf25836c66e3b4e163b7", + "sha256": "sha256-n4X/L9fWyzCXqkf7QZ7n8OvoaRCfmKup9Oyj9J50pA4=" + } + } + }, + + { + "path": "org/apache/apache/18", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-18.pom": { + "sha1": "bd408bbea3840f2c7f914b29403e39a90f84fd5f", + "sha256": "sha256-eDEwcoX9R1u8NrIK4454gvEcMVOx1ZMPhS1E7ajzPBc=" + } + } + }, + + { + "path": "org/apache/apache/21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-21.pom": { + "sha1": "649b700a1b2b4a1d87e7ae8e3f47bfe101b2a4a5", + "sha256": "sha256-rxDBCNoBTxfK+se1KytLWjocGCZfoq+XoyXZFDU3s4A=" + } + } + }, + + { + "path": "org/apache/apache/23", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-23.pom": { + "sha1": "0404949e96725e63a10a6d8f9d9b521948d170d5", + "sha256": "sha256-vBBiTgYj82V3+sVjnKKTbTJA7RUvttjVM6tNJwVDSRw=" + } + } + }, + + { + "path": "org/apache/apache/33", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "apache-33.pom": { + "sha1": "f7b7b5a9e84395f17d7e6a136e5bfc4b9973566d", + "sha256": "sha256-14vYUkxfg4ChkKZSVoZimpXf5RLfIRETg6bYwJI6RBU=" + } + } + }, + + { + "path": "org/apache/commons/commons-compress/1.8.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-compress-1.8.1.pom": { + "sha1": "2fb3ea9182911315ee79aa2fb4853c7bdd347407", + "sha256": "sha256-EBN62xToUgsqvN3DCAA+Qy64NsTR+aAPaGAdY1g7n7g=" + }, + "commons-compress-1.8.1.jar": { + "sha1": "a698750c16740fd5b3871425f4cb3bbaa87f529d", + "sha256": "sha256-X8oTZQP4bsxsth+9F7E31Z5WtFx6VJTmuP08q9Rpf70=" + } + } + }, + + { + "path": "org/apache/commons/commons-compress/1.12", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-compress-1.12.pom": { + "sha1": "d4ba952b054eef846e7255cc3ac6d4ca76ead72c", + "sha256": "sha256-t4fVdMhRUF52ISloua4WQep5gErvf1os7ioBzUBVITo=" + }, + "commons-compress-1.12.jar": { + "sha1": "84caa68576e345eb5e7ae61a0e5a9229eb100d7b", + "sha256": "sha256-LBVC+vNDGFt8q5w9VciuVHHW0JXTiHpK3v298phNwLY=" + } + } + }, + + { + "path": "org/apache/commons/commons-compress/1.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-compress-1.20.pom": { + "sha1": "a63d6a48347a2848f6a798c9efad976829011f7c", + "sha256": "sha256-2VZ4469WsXx9ts/5ZF761etZvp88HKqvXwFG7fBGkdc=" + }, + "commons-compress-1.20.jar": { + "sha1": "b8df472b31e1f17c232d2ad78ceb1c84e00c641b", + "sha256": "sha256-CutiXJSMaX6nsgUVbhEjY7We1eJVEhLNTkYL23LHwG4=" + } + } + }, + + { + "path": "org/apache/commons/commons-compress/1.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-compress-1.21.pom": { + "sha1": "f9f4f26a1ea08778cc818c1555587741605bb4da", + "sha256": "sha256-Z1uwI8m+7d4yMpSZebl0Kl/qlGKApVobRi1Mp4AQiM0=" + }, + "commons-compress-1.21.jar": { + "sha1": "4ec95b60d4e86b5c95a0e919cb172a0af98011ef", + "sha256": "sha256-auz9VFlyillWAc+gcljRMZcv/Dm0kutIvdWWV3ovJEo=" + } + } + }, + + { + "path": "org/apache/commons/commons-lang3/3.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-lang3-3.3.2.pom": { + "sha1": "932ed8226f371b204d04a8c4d3d5fe0f2b26339f", + "sha256": "sha256-4Ydb9XWZnrxi3sNDBKDRCMMKT/gQHo6lyakzJxUneUA=" + }, + "commons-lang3-3.3.2.jar": { + "sha1": "90a3822c38ec8c996e84c16a3477ef632cbc87a3", + "sha256": "sha256-a4HRB1Ta3xhNOGARSG5lCcLMDD0zVlztT7RAK5QT1H0=" + } + } + }, + + { + "path": "org/apache/commons/commons-lang3/3.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-lang3-3.9.pom": { + "sha1": "2b7f0896fc2f13bbe7b0022c85738e3b6a3f201a", + "sha256": "sha256-pAIkKbmEJbQwGBkVchJ5pS9hDzRki9rEh9TKy76N/rU=" + }, + "commons-lang3-3.9.jar": { + "sha1": "0122c7cee69b53ed4a7681c03d4ee4c0e2765da5", + "sha256": "sha256-3i4dzc8++ReozoWGYaBnJqmpRPKOM61/ngi+pE3DwjA=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-5.pom": { + "sha1": "a0a168281558e7ae972f113fa128bc46b4973edd", + "sha256": "sha256-i9YywAvfgKfeNsIrYPEkUsFH2Oyi8A151maZ6+faoCo=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-11.pom": { + "sha1": "3f29657e1e3d6856344728ddbcf696477e943d59", + "sha256": "sha256-ueAwbzk0YBBbij+lEFJQxSkbHvqpmVSs4OwceDEJoCo=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-22.pom": { + "sha1": "0e895fa7ed472b3b2081ef77e2d5ece78c139d54", + "sha256": "sha256-+4xeVeMKet20/yEIWKDo0klO1nV7vhkBLamdUVhsPLs=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/25", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-25.pom": { + "sha1": "67b84199ca4acf0d8fbc5256d90b80f746737e94", + "sha256": "sha256-RnrmUEQuh2hnN5CU51GN/dZ9IsU1Lr05gIyEJZ6XkLo=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/32", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-32.pom": { + "sha1": "0e51c4223003c2c7c63f38d7b8823e40eb06bd1f", + "sha256": "sha256-5NJYr4sv9AMhSNQVN53veHB4mmAD6AV28VBLEPJrS+g=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/33", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-33.pom": { + "sha1": "a9bd6ae1e11cb313ec4a4c9bcd58c7a9ea60a5cd", + "sha256": "sha256-U9ABE1Li5RBvN52vzNrHdU7G8PeCQ8AwXklp9azd+Ps=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/34", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-34.pom": { + "sha1": "1f6be162a806d8343e3cd238dd728558532473a5", + "sha256": "sha256-Oi5p0G1kHR87KTEm3J4uTqZWO/jDbIfgq2+kKS0Et5w=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/35", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-35.pom": { + "sha1": "d88c24ebb385e5404f34573f24362b17434e3f33", + "sha256": "sha256-cJihq4M27NTJ3CHLvKyGn4LGb2S4rE95iNQbT8tE5Jo=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/39", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-39.pom": { + "sha1": "4bc32d3cda9f07814c548492af7bf19b21798d46", + "sha256": "sha256-h80n4aAqXD622FBZzphpa7G0TCuLZQ8FZ8ht9g+mHac=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/42", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-42.pom": { + "sha1": "35d45eda74fe511d3d60b68e1dac29ed55043354", + "sha256": "sha256-zTE0lMZwtIPsJWlyrxaYszDlmPgHACNU63ZUefYEsJw=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/48", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-48.pom": { + "sha1": "1cdeb626cf4f0cec0f171ec838a69922efc6ef95", + "sha256": "sha256-Hh996TcKe3kB8Sjx2s0UIr504/R/lViw954EwGN8oLQ=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/52", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-52.pom": { + "sha1": "004ee86dedc66d0010ccdc29e5a4ce014c057854", + "sha256": "sha256-ddvo806Y5MP/QtquSi+etMvNO18QR9VEYKzpBtu0UC4=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/78", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-78.pom": { + "sha1": "c59670d5fff6efd8b31013cb579732c75a250b4c", + "sha256": "sha256-Ai0gLmVe3QTyoQ7L5FPZKXeSTTg4Ckyow1nxgXqAMg4=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/79", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-79.pom": { + "sha1": "a2f13e0fa1f22588976e02c39b840653986ba685", + "sha256": "sha256-Yo3zAUis08SRz8trc8euS1mJ5VJqsTovQo3qXUrRDXo=" + } + } + }, + + { + "path": "org/apache/commons/commons-parent/81", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "commons-parent-81.pom": { + "sha1": "4a342190fb243a221636ac0e0b02d3eed07a9908", + "sha256": "sha256-NI1OfBMb5hFMhUpxnOekQwenw5vTZghJd7JP0prQ7bQ=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpclient-4.1.1.pom": { + "sha1": "1468927fa5fbea4d3037422f45eb12cef173ba16", + "sha256": "sha256-rboX2TSKl9TOBfKCZ5cP2BlcYaVssG1eDdFfr/YP4qc=" + }, + "httpclient-4.1.1.jar": { + "sha1": "3d1d918f32709e33ba7ddb2c4e8d1c543ebe713e", + "sha256": "sha256-6uUm0Ipmeb9soTjUWgAFsgum7EpAJ4i+gQlwcTyOR1E=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.2.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpclient-4.2.6.pom": { + "sha1": "fc51b4b649a2f9ab2588e9c1703288479591ade8", + "sha256": "sha256-9aCh7qoQDtNqqD7KVi1du36bBFyYtE232kdF6dmiAyg=" + }, + "httpclient-4.2.6.jar": { + "sha1": "e4ca30a6a3a075053a61c6fc850d2432dc012ba7", + "sha256": "sha256-Ni6TJO58aX4hJ54gB3tSc33e8/Gywaer5a00tGUUVVA=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpclient-4.5.2.pom": { + "sha1": "56f6338b324e438307e1f2c2b33bd02268310fc2", + "sha256": "sha256-SIABuiGCmksosO++0Y3MsTaJ9Y8JhUU4YyVwSffsGfA=" + }, + "httpclient-4.5.2.jar": { + "sha1": "733db77aa8d9b2d68015189df76ab06304406e50", + "sha256": "sha256-Df/GIUANbGMvVXh9mWuK7KNrMHRqcW4HmphfJNgHQFc=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.5.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpclient-4.5.6.pom": { + "sha1": "db40edda8b95d880d2a810560fd5e46eb4fa6909", + "sha256": "sha256-fvwSQec+f7smi/0zJC0R69PKBwYdfYXyli3DKg8LiFU=" + }, + "httpclient-4.5.6.jar": { + "sha1": "1afe5621985efe90a92d0fbc9be86271efbe796f", + "sha256": "sha256-wD+BMZXnqA42CNDd2NqAshaWpMkqaiKYhlvxSQcVUcc=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.5.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpclient-4.5.9.pom": { + "sha1": "2b382e49ec08a138a7f0b36916632517f6a6838e", + "sha256": "sha256-o6HH5fHBYICMjGD4iOtIhN6C8kTdaBamVChHHAZLxz4=" + }, + "httpclient-4.5.9.jar": { + "sha1": "a25c1be5ce99d0ce99aa43eb982868c796dd0775", + "sha256": "sha256-bH47tCPYxVdPKBV/5CtMONajR3v6KVTP5fMwsU7K2Kk=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.5.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpclient-4.5.13.pom": { + "sha1": "e5b134e5cd3e28dc431ca5397e9b53d28d1cfa74", + "sha256": "sha256-eOua2nSSn81j0HrcT0kjaEGkXMKdX4F79FgB9RP9fmw=" + }, + "httpclient-4.5.13.jar": { + "sha1": "e5f6cae5ca7ecaac1ec2827a9e2d65ae2869cada", + "sha256": "sha256-b+kCalZsalABYIzz/DIZZkH2weXhmG0QN8zb1fMe90M=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpclient/4.5.14", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpclient-4.5.14.pom": { + "sha1": "f62c6a311407cc4b47d0ea9eec6cf97ed25b8cee", + "sha256": "sha256-8YNVr0z4CopO8E69dCpH6Qp+rwgMclsgldvE/F2977c=" + }, + "httpclient-4.5.14.jar": { + "sha1": "1194890e6f56ec29177673f2f12d0b8e627dec98", + "sha256": "sha256-yLx+HFGm1M5y9A0uu6vxxLaL/nbnMhBLBDgbSTR46dY=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.1.1.pom": { + "sha1": "3e1c598ddde5619cc602d32fffdf34668be071a3", + "sha256": "sha256-R1KE5emVUm1+dxUTSgdBLOctWYUEY0I4P13kGIoCKlM=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.1.pom": { + "sha1": "ac363bbcf41af1153745f561aa0feac2b373a017", + "sha256": "sha256-UhW2SHmMbI2bnN42QtO/taTMiUt8hBEAQ1Ln3l0RmCc=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.2.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.2.6.pom": { + "sha1": "e40911efc7a573f049475a2beeaf83690bf5ad62", + "sha256": "sha256-Urb9Lq8iQOFNvnUvH6iEJwzGaNbzUOjn9l9gM6gXLWk=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.5.2.pom": { + "sha1": "55d500e6c1e98ffbb00c35bee72fabc255c22108", + "sha256": "sha256-HejLpsHlxGsoYZ4zWm+3IEw1Lbo+ixzvtLWVdeK+7QE=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.5.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.5.6.pom": { + "sha1": "135a0a91d1ad2909b08196580ef2c363932bb87e", + "sha256": "sha256-sEK0HyOR7bANNff05Qmu0hI2SMHSRs5Y0Pe5Bcn+H3M=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.5.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.5.9.pom": { + "sha1": "ec94d21b5dacc56fac902390c7e37ca810ab6cff", + "sha256": "sha256-sU04xit53R+laPHyeNmRCAHTnz2AtjZj6tV3iaLTcoU=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.5.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.5.13.pom": { + "sha1": "61045e5bac5f6b0a7e3301053de0d78fc92f09db", + "sha256": "sha256-nLpZTAjbcnHQwg6YRdYiuznmlYORC0Xn1d+C9gWNTdk=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-client/4.5.14", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-client-4.5.14.pom": { + "sha1": "e36b5c26d9012cce0ba214e06462596585475217", + "sha256": "sha256-W60d5PEBRHZZ+J0ImGjMutZKaMxQPS1lQQtR9pBKoGE=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.1.pom": { + "sha1": "166e36966c2cbd6c1867a6eded3f56d70958338c", + "sha256": "sha256-T3l//Zw9FW3g2+wf0eY+n9hYSpPHBDV2VT38twb2TeQ=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.2.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.2.5.pom": { + "sha1": "1230a8404ed1cb3710ac59734fa2bcad69c166d9", + "sha256": "sha256-Fk56nPI6TmO7yWH4232WXVPWhXEGYKiCSWQfkdNvg6Q=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.4.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.4.5.pom": { + "sha1": "020c728c32a780a0a41ce5ba6e61891aadf25cf1", + "sha256": "sha256-GNkkNUHICEjVN4utifzMXhv1wEZbJIHZSLDAaFEJHNw=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.4.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.4.10.pom": { + "sha1": "43dd1af3c408840fa4d19679da7db9c8dec9f7fa", + "sha256": "sha256-YelCfUvjJsMHp/FrqCjRyzsUcTybBPyLqZKljzdsMTY=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.4.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.4.11.pom": { + "sha1": "3cae168cde302e64df8c7f640102af6c58d2d867", + "sha256": "sha256-EjYLoW2AJcxKvWOTpipOLi6syQBsjsfkiMQIUqXN7Rw=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.4.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.4.13.pom": { + "sha1": "221f9ef52d6147e7c193f540c495db26f25d64b6", + "sha256": "sha256-xVTnAI5FF8fvVOAFzIt09Mh6VKDqLG9Xvl0Fad9Rk2s=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.4.15", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.4.15.pom": { + "sha1": "3533a4d4bbf8fb489bc20795ba2826d3a95baa7d", + "sha256": "sha256-YNQ3J6YXSATIrhf5PpzGMuR/PEEQpMVLn6/IzZqMpQk=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-core/4.4.16", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-core-4.4.16.pom": { + "sha1": "e0cbfde0384168cdd90e2633c1f101416d799090", + "sha256": "sha256-8tdaLC1COtGFOb8hZW1W+IpAkZRKZi/K8VnVrig9t/c=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-parent/10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-parent-10.pom": { + "sha1": "1ba3c5940f24525444492ae12b6a1015df8a477b", + "sha256": "sha256-yq+WfZSvshdT82CCxghiBr0fSIJf9ZaTLM66crZdOfo=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcomponents-parent/11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcomponents-parent-11.pom": { + "sha1": "3ee7a841cc49326e8681089ea7ad6a3b81b88581", + "sha256": "sha256-qQH4exFcVQcMfuQ+//Y+IOewLTCvJEOuKSvx9OUy06o=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.1.pom": { + "sha1": "28b423b7882a5c0d5b383903c6ea05d19b3c7c91", + "sha256": "sha256-T8hq+jjpyfqwmcz0XCvHQ9RT5qsiJJCr/oZxl1w8cyc=" + }, + "httpcore-4.1.jar": { + "sha1": "33fc26c02f8043ab0ede19eadc8c9885386b255c", + "sha256": "sha256-POON5R9OJGaMbRhAV6jQhUH56BXS0xnQ9GLwgwkrKc8=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.2.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.2.5.pom": { + "sha1": "cbae058677f9e1abfd9060223eb68224f96a7559", + "sha256": "sha256-tuuZ8TYrg0TKVADVejXEnjUE8j897jBP24edTZU/S8Y=" + }, + "httpcore-4.2.5.jar": { + "sha1": "472f0f5f8dba5d1962cb9d7739feed739a31c30d", + "sha256": "sha256-5egtpMxmyNkXu/dD48B1Lv6FInNef8nb3bZbzOqBz+k=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.4.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.4.5.pom": { + "sha1": "06137e1c916f6467c943847418f39b1685aadf4e", + "sha256": "sha256-FH+hjfdTU05QBU0BijYPeJkCrx5cJDIqPwsgDQaE/tI=" + }, + "httpcore-4.4.5.jar": { + "sha1": "e7501a1b34325abb00d17dde96150604a0658b54", + "sha256": "sha256-ZNVFOHTKt+QKcGXLAampyhBThFqXhrR4h4tnngWAzsM=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.4.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.4.10.pom": { + "sha1": "cbbf1989463d9f3e284c1348e50ea8bf426e4e1c", + "sha256": "sha256-xcEgZt8rO4iomiyGArgeqaYWJ+l25RKe6hiZ67rqOSs=" + }, + "httpcore-4.4.10.jar": { + "sha1": "acc54d9b28bdffe4bbde89ed2e4a1e86b5285e2b", + "sha256": "sha256-eLoQllYZV9sbVSAKFZtkiHZDA0LRXUYSd+YjYNoZ9v0=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.4.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.4.11.pom": { + "sha1": "f055c410b7d91bd8fdcc2efef1fbce44843f2404", + "sha256": "sha256-1WEbRtjJqHPjFgh91raE3fabdoyAhDraiyKiOuvGIwE=" + }, + "httpcore-4.4.11.jar": { + "sha1": "de748cf874e4e193b42eceea9fe5574fabb9d4df", + "sha256": "sha256-15lSLVearAaxcGA/jwgPbjJI2twB+WUs3X6nvDGMIc4=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.4.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.4.13.pom": { + "sha1": "7d4610db34bf2175d0d3813d7faac9cf7ca7c0e5", + "sha256": "sha256-j4Etn6e3Kj1Kp/glJ4kypd80S0Km2DmJBYeUMaG/mpc=" + }, + "httpcore-4.4.13.jar": { + "sha1": "853b96d3afbb7bf8cc303fe27ee96836a10c1834", + "sha256": "sha256-4G6J1AlDJF/Po57FN82/zjdirs3o+cWXeA0rAMK0NCQ=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.4.15", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.4.15.pom": { + "sha1": "fcd592279d3c26678aad344894e20f9a01102a16", + "sha256": "sha256-Kaz+qoqIu2IPw0Nxows9QDKNxaecx0kCz0RsCUPBvms=" + }, + "httpcore-4.4.15.jar": { + "sha1": "7f2e0c573eaa7a74bac2e89b359e1f73d92a0a1d", + "sha256": "sha256-PLrtCIxJmhD5bd5Y853A55hRcavYgTjKFlWocgEbsUI=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpcore/4.4.16", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpcore-4.4.16.pom": { + "sha1": "fdcd45abd94151f990a359e1d367e325cfb50286", + "sha256": "sha256-PLrYSbNdrP5s7DGtraLGI8AmwyYRQbDSbux+OZxs1/o=" + }, + "httpcore-4.4.16.jar": { + "sha1": "51cf043c87253c9f58b539c9f7e44c8894223850", + "sha256": "sha256-bJs90UKgncRo4jrTmq1vdaDyuFElEERp8CblKkdORk8=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpmime/4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpmime-4.1.pom": { + "sha1": "a9e269c38cd5736d1db3a0e50adf87fafa8504a8", + "sha256": "sha256-2zzZW+wQAWBeFlPyhD6FQucS+iu2cv2cjBrOhsV2c9E=" + }, + "httpmime-4.1.jar": { + "sha1": "9ba2dcdf94ce35c8a8e9bff242db0618ca932e92", + "sha256": "sha256-MWKVZhSOikdoiuQ7Qgq8Ps14PtFbM768AIJL8kybFao=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpmime/4.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpmime-4.5.2.pom": { + "sha1": "9bfedb5d7d9aab32d9ed2377ed8bde2e49ef9e51", + "sha256": "sha256-AEtbYnLYIAKa3vyqqSGG7EakhZkLVNA1CeRB7ahbN4Q=" + }, + "httpmime-4.5.2.jar": { + "sha1": "22b4c53dd9b6761024258de8f9240c3dce6ea368", + "sha256": "sha256-Ixo/fkliBT2yvoRh1UIuaPxFijp919itqAOjSOIfjwc=" + } + } + }, + + { + "path": "org/apache/httpcomponents/httpmime/4.5.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "httpmime-4.5.6.pom": { + "sha1": "b9dcad423fba1978379049cf0cf3cf535c667af7", + "sha256": "sha256-37/W/+KnhMqYF8RjZap/ileDILgFveOdb1WgsJ2KqMo=" + }, + "httpmime-4.5.6.jar": { + "sha1": "164343da11db817e81e24e0d9869527e069850c9", + "sha256": "sha256-CysRAsGNPH4Fp3IUubdQGm9gVhdK5WBODiVndu2nVT4=" + } + } + }, + + { + "path": "org/apache/httpcomponents/project/4.1.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "project-4.1.1.pom": { + "sha1": "34c04480e3e97bccf10777c94651479b5ff55278", + "sha256": "sha256-IbtNRN/1TjOjfBGvaYWacUICrgCWmqtUU+unJ2aI+Ow=" + } + } + }, + + { + "path": "org/apache/httpcomponents/project/7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "project-7.pom": { + "sha1": "c486760d8e0eafe8d4932450e386c2805364f782", + "sha256": "sha256-PW66QoVVpVjeBGtddurMH1pUtPXyC4TWNu16/xiqSMM=" + } + } + }, + + { + "path": "org/bitbucket/b_c/jose4j/0.7.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jose4j-0.7.0.pom": { + "sha1": "75a113b082e019173123f818deb57e1b12c25c85", + "sha256": "sha256-ExKMz5D7MfGNgQm0hFFmSdXbDkcoTYYOcaTPMUgVPtg=" + }, + "jose4j-0.7.0.jar": { + "sha1": "478ee8518687f9b01f20912fc7edd9d7b46d06da", + "sha256": "sha256-6xT2nAOV1KEGxsRv5t/wgMRgjMq8mbHwOTPTdDg9m74=" + } + } + }, + + { + "path": "org/bouncycastle/bcpkix-jdk15on/1.48", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcpkix-jdk15on-1.48.pom": { + "sha1": "8fc7cecdba5ea3df0e29b659613dc21222a910fa", + "sha256": "sha256-v3z/mqxILUKuuKFVHQDrZ81DbsjNThJuyKHgnITdIX0=" + }, + "bcpkix-jdk15on-1.48.jar": { + "sha1": "28b7614b908a47844bb27e3c94b45b6893656265", + "sha256": "sha256-U0czNrTlqtd5aKGLFsVNFc28Q/Plehh4eZ7faUHQujc=" + } + } + }, + + { + "path": "org/bouncycastle/bcpkix-jdk15on/1.56", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcpkix-jdk15on-1.56.pom": { + "sha1": "2e2e47a19e0c53e7d204a6ad071c9e64c7c4c43f", + "sha256": "sha256-MhZo/EdKcxQOucxzx3nx67n2CUiq3jvt3uCC7O/PAlk=" + }, + "bcpkix-jdk15on-1.56.jar": { + "sha1": "4648af70268b6fdb24674fb1fd7c1fcc73db1231", + "sha256": "sha256-cEPe5OnnF16T4LNvRbHsHsuJPF91VmfouRbrjdIBxso=" + } + } + }, + + { + "path": "org/bouncycastle/bcpkix-jdk15on/1.67", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcpkix-jdk15on-1.67.pom": { + "sha1": "2e8437c0c4f64198b37d21ecec695e6cf3cc86d0", + "sha256": "sha256-qbswvZh9wwWwXuU+nyfm6QL8EM48bplhjP8DYteWsE0=" + }, + "bcpkix-jdk15on-1.67.jar": { + "sha1": "5f48020a2a60a8d6bcbecceca23529d225b28efb", + "sha256": "sha256-77ynVIgM45IspHpDwfC3LEVzFFCg7xk7nbM79Ls4zl8=" + } + } + }, + + { + "path": "org/bouncycastle/bcprov-jdk15on/1.48", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcprov-jdk15on-1.48.pom": { + "sha1": "645352132b8d239bcc737c3c164f435af97cd9bb", + "sha256": "sha256-KRIr50qOwbT8VB49opmL62Irw4wEuovx9Vk4aReCeYI=" + }, + "bcprov-jdk15on-1.48.jar": { + "sha1": "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", + "sha256": "sha256-gEt+Ljuax3Hf07Q94WZmrGAI+GAPSPKN3JTjmhFOIog=" + } + } + }, + + { + "path": "org/bouncycastle/bcprov-jdk15on/1.56", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcprov-jdk15on-1.56.pom": { + "sha1": "16220ab4e1b41786b0396e908ed91437c620ee3c", + "sha256": "sha256-j9wzNuewGHMZO6nEi4fefXiNwJVNHrRcMiSSYnpLXG4=" + }, + "bcprov-jdk15on-1.56.jar": { + "sha1": "a153c6f9744a3e9dd6feab5e210e1c9861362ec7", + "sha256": "sha256-lj4e4U+Aj/uZiX2EjdzbKPqR3dqGfrGNMD6Cco+Hg0k=" + } + } + }, + + { + "path": "org/bouncycastle/bcprov-jdk15on/1.60", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcprov-jdk15on-1.60.pom": { + "sha1": "2e5c6ffc32983b3bf170a97d3370b277b9b2ea34", + "sha256": "sha256-cXQxerHmw9iaJS1efRxdD6/CfD7SIR7vxgzGF4LcorY=" + }, + "bcprov-jdk15on-1.60.jar": { + "sha1": "bd47ad3bd14b8e82595c7adaa143501e60842a84", + "sha256": "sha256-fxoOa62rOGZvhGepoO6WZWsvjshiOGftNPPNwXO37gc=" + } + } + }, + + { + "path": "org/bouncycastle/bcprov-jdk15on/1.67", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcprov-jdk15on-1.67.pom": { + "sha1": "c622f23fc3d1acf7c0cf66755ad7455781c4d489", + "sha256": "sha256-pHOndHbdYxYw67OjezSBL5TmpyRcOXZaFNcjG5DgZFs=" + }, + "bcprov-jdk15on-1.67.jar": { + "sha1": "8c0998045da87dbc2f1d4b6480458ed811ca7b82", + "sha256": "sha256-+gBBo2+fIK88a42/brSalp4snMApBJ1hrMUmujJHs+8=" + } + } + }, + + { + "path": "org/bouncycastle/bcprov-jdk15on/1.68", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "bcprov-jdk15on-1.68.pom": { + "sha1": "be65feffe917b1b1e9c566e0cfab739c41f93345", + "sha256": "sha256-2QfezWhfnNlx5xvfbOWXzzqVt8j/ajSCNRv9irwXz+A=" + }, + "bcprov-jdk15on-1.68.jar": { + "sha1": "46a080368d38b428d237a59458f9bc915222894d", + "sha256": "sha256-9zKkbI3n4iMvIAfGgqIdH0zIqKAUm2t71qoa/cZaD40=" + } + } + }, + + { + "path": "org/checkerframework/checker-compat-qual/2.5.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-compat-qual-2.5.6.pom": { + "sha1": "ab73e5d62a66f6a4485a10a0e2c7234da2889f1d", + "sha256": "sha256-FxNsR1rgzd9LMvf66tVIbCzEWgZI69MH6uDbNUuWCLE=" + }, + "checker-compat-qual-2.5.6.jar": { + "sha1": "435dc33e3019c9f019e15f01aa111de9d6b2b79c", + "sha256": "sha256-EdE0skXpysxHRRTS1mtbhhj4A5oUZc3FW7wLNOAAi3o=" + } + } + }, + + { + "path": "org/checkerframework/checker-qual/2.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-qual-2.5.2.pom": { + "sha1": "7cee753353b0fc94e0300ad3dbf155069260c4d7", + "sha256": "sha256-3EzUOKNkYtATwjOMjiBtECoyKgDzNynolV7iGYWcnt4=" + }, + "checker-qual-2.5.2.jar": { + "sha1": "cea74543d5904a30861a61b4643a5f2bb372efc4", + "sha256": "sha256-ZLAmkci51OdwD47i50Lc5+osboHmYrdSLJ7jv1aMBAo=" + } + } + }, + + { + "path": "org/checkerframework/checker-qual/2.5.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-qual-2.5.8.pom": { + "sha1": "5eb0c9262e891ee4dfa3410b9be611e47c79779f", + "sha256": "sha256-M6xqDxNBrpZkfH1EZfSqPST+l9Jpe87izq5vyLXvLDw=" + }, + "checker-qual-2.5.8.jar": { + "sha1": "41c6fa63cba0fea55ebd3581d42b3d2c5cca98b2", + "sha256": "sha256-vWVdpTXfHuuW/Ih00F0PcCkgnelQluT4S9B8ZmiKIQ0=" + } + } + }, + + { + "path": "org/checkerframework/checker-qual/2.8.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-qual-2.8.1.pom": { + "sha1": "c7e7cde72ffc19ab0f453b6fcc184638a8f3bb11", + "sha256": "sha256-8jN2tY7XlfL/R0Jqx2uZbUnjkYRC5dhxPjkl+Imnd5k=" + }, + "checker-qual-2.8.1.jar": { + "sha1": "eb2e8ab75598548cc8acf9a1ca227e480e01881e", + "sha256": "sha256-kQNJkAi87NTpSNopsXhkq7ZDBOFXBkRK4gnRfr4Fdd8=" + } + } + }, + + { + "path": "org/checkerframework/checker-qual/2.11.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-qual-2.11.1.pom": { + "sha1": "ebc1b44da8503640e2ed42627b299272c8c72824", + "sha256": "sha256-zy4MkNj3V0VfSiWOpglzkFNmO9XaannZvVP5NaR955w=" + }, + "checker-qual-2.11.1.jar": { + "sha1": "8c43bf8f99b841d23aadda6044329dad9b63c185", + "sha256": "sha256-AVIkpLHcbebaBTJz1Np9Oc/qIOYwOBafxFrA0dycWTg=" + } + } + }, + + { + "path": "org/checkerframework/checker-qual/3.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-qual-3.5.0.pom": { + "sha1": "408a4451ff5bdef60400a49657867db100ea0f83", + "sha256": "sha256-KDazuKeO2zGhgDWS5g/HZ7IfLRkHZGMbpu+gg3uzVyE=" + }, + "checker-qual-3.5.0.jar": { + "sha1": "2f50520c8abea66fbd8d26e481d3aef5c673b510", + "sha256": "sha256-cpmQs/GKlWBvwlc4NraVi820TLUr+9G3qpwznP81paQ=" + } + } + }, + + { + "path": "org/checkerframework/checker-qual/3.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-qual-3.12.0.pom": { + "sha1": "fb8dca6f40fcb30f7b89de269940bf3316fb9845", + "sha256": "sha256-d1t6425iggs7htwao5rzfArEuF/0j3/khakionkPRrk=" + }, + "checker-qual-3.12.0.jar": { + "sha1": "d5692f0526415fcc6de94bb5bfbd3afd9dd3b3e5", + "sha256": "sha256-/xB4WsKjV+xd6cKTy5gqLLtgXAMJ6kzBy5ubxtvn88s=" + }, + "checker-qual-3.12.0.module": { + "sha1": "7f3b31c801fe87a77142f4fdd07bd4cb6f28cc9b", + "sha256": "sha256-0EeUnBuBCRwsORN3H6wvMqL6VJuj1dVIzIwLbfpJN3c=" + } + } + }, + + { + "path": "org/checkerframework/checker-qual/3.49.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "checker-qual-3.49.2.pom": { + "sha1": "687fe10fc21e361bb1830b51095364bf21b7c10d", + "sha256": "sha256-CX6h/cRnRIxon1YUr26j0+ZZvz9LQuBiJuYPKuV7vfk=" + }, + "checker-qual-3.49.2.jar": { + "sha1": "98ac669ccce59dba8ca360d3e07891d62b6b946a", + "sha256": "sha256-M7HYssSwD31PS0kxQkJ08/cpR0zjqHzqCah5l2odyus=" + }, + "checker-qual-3.49.2.module": { + "sha1": "3e9e799363f6097e0898db328376fbb1750cd01d", + "sha256": "sha256-2qMZqqMe/EhdFuOrYw48uP5PQTz91EYisQBJxURdGQQ=" + } + } + }, + + { + "path": "org/codehaus/codehaus-parent/4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "codehaus-parent-4.pom": { + "sha1": "8b133202d50bec1e59bddc9392cb44d1fe5facc8", + "sha256": "sha256-a4cjfejC4XQM+AYnx/POPhXeGTC7JQxVoeypT6PgFN8=" + } + } + }, + + { + "path": "org/codehaus/groovy/groovy-xml/3.0.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "groovy-xml-3.0.9.pom": { + "sha1": "756049db683d199c198abdbed122681ed3b78d01", + "sha256": "sha256-FIz0tXtqo/61JZo78lsohKbAazE/xYg412rNC6LT2i0=" + }, + "groovy-xml-3.0.9-groovydoc.jar": { + "sha1": "7adc05f5667bbc35e31d0907f5b0e0fb13e4f48f", + "sha256": "sha256-/G0Jg66CNyg3J4DloBFhPwE+V9f2qO26QSRlG0/bdMc=" + }, + "groovy-xml-3.0.9-indy.jar": { + "sha1": "58767441b51f524645b2ff0d642d50b2ec42267d", + "sha256": "sha256-ZbimAbsSlBZfKJNckRWYusjDEdhuKdv3TxR0lteGE0c=" + }, + "groovy-xml-3.0.9.jar": { + "sha1": "dae68f2e9f5e12dc5cc0a27ee7003f18702a2bcd", + "sha256": "sha256-K87NhIyu05dTWKXFVqpJFUstZvDIpcFwQ917fYmFtv4=" + } + } + }, + + { + "path": "org/codehaus/groovy/groovy-xml/3.0.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "groovy-xml-3.0.10.pom": { + "sha1": "365e519c10d64937dbec5aa2d81cef99eefb31b6", + "sha256": "sha256-Zwf5O1jO4BxBc/uuhNLh2DDKQc00LZBgbNW3GKn/fSk=" + }, + "groovy-xml-3.0.10-groovydoc.jar": { + "sha1": "de0696f962461e3c056c7008f7d922c45e87a8e9", + "sha256": "sha256-ywtCe0dU7FFAMG1t10P0Vk1u2XOPrwV6dkjX04+JHzY=" + }, + "groovy-xml-3.0.10-indy.jar": { + "sha1": "67c08709717882d84c2cd1791049af54a1476335", + "sha256": "sha256-ThhoA1V3m1n2OmFNu+W1MObSo0cG2JWruLjdR1MhzPI=" + }, + "groovy-xml-3.0.10.jar": { + "sha1": "5367241b0c5d4638c0e8371028a2d19a0af35156", + "sha256": "sha256-vCrVETWWo6m7ZQ2mRx8biRUFQheCF0p/5g15DttinoU=" + } + } + }, + + { + "path": "org/codehaus/groovy/groovy/3.0.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "groovy-3.0.9.pom": { + "sha1": "6e2f4baa2df2329e9b0950c92b1683de15196a7a", + "sha256": "sha256-PiaGTCM+AEQdDq39gvuVq7y+xRa46VErxcXpS+hG3P8=" + }, + "groovy-3.0.9-grooid.jar": { + "sha1": "bf48423e10d6a94edec3dfa9e90fb03838972a49", + "sha256": "sha256-xQLhZ33+UiahexO3gzMPwG7iYgn5Pl5FVP2W0POzS4A=" + }, + "groovy-3.0.9-groovydoc.jar": { + "sha1": "d0c5a5634117a98666553506a682187e4e3e7b88", + "sha256": "sha256-RrQQGSb7hafXJCzAXsIyyXRCPvpFxbXuLrSrgT1069Q=" + }, + "groovy-3.0.9-indy.jar": { + "sha1": "0f3982de28eef40dbed77231dc872e721a207f96", + "sha256": "sha256-d2OoneB+11EWez6E1wqGHXUsKAulOoBMG5tW+AFVd+Y=" + }, + "groovy-3.0.9.jar": { + "sha1": "bd159803bedc434e7af0f714693fb5578a719e95", + "sha256": "sha256-d7+GiX8pX4yuLh9GseyhCfSHuoG2bvJKK23Loet9bOc=" + } + } + }, + + { + "path": "org/codehaus/groovy/groovy/3.0.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "groovy-3.0.10.pom": { + "sha1": "9b1123b5959b49abeb8a90ec110f3ee9b7b8c9f6", + "sha256": "sha256-SYKt9KbGxQVnsL489JSCXmwqJ9izhx2eGIIqdCcreao=" + }, + "groovy-3.0.10-grooid.jar": { + "sha1": "86dd48157b3f62e89e5353789d890f1ae245bc1a", + "sha256": "sha256-Zj6VjH68WOBpik6idZnFHO1q1uM5xi53z+aeC/SfaZI=" + }, + "groovy-3.0.10-groovydoc.jar": { + "sha1": "e9e2128d3569af30bc789c27ba8234fd3cc0225a", + "sha256": "sha256-I6xQYdpkfvrbVxPevmSzMJ7pG+NRvBYbpyvf8t4f81M=" + }, + "groovy-3.0.10-indy.jar": { + "sha1": "4bb654e20b0771d88b98b4e75906fc7394e2f745", + "sha256": "sha256-I/1Gjlzq0Mku9zqFH3XZkOgzYORHOioTGeSvQ660jRI=" + }, + "groovy-3.0.10.jar": { + "sha1": "b92c72a758f468e64b55e38abe06afa873decdba", + "sha256": "sha256-3/762oKUhlvlA1UVwDIPtnlEPShfSl+K2tzwDBEw6Ew=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-annotations/1.14", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-annotations-1.14.pom": { + "sha1": "99a56e3312f8ece1d457c8ca0a3c4b69d173d000", + "sha256": "sha256-GHnxmgWZHj7ZWRC5ZokzM5awxGeiFdxNH5ABhAS3KiY=" + }, + "animal-sniffer-annotations-1.14.jar": { + "sha1": "775b7e22fb10026eed3f86e8dc556dfafe35f2d5", + "sha256": "sha256-IGgyC9a610TDZzqwSPZ+ML749RiZb6OAAzVWYAZpkF0=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-annotations/1.17", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-annotations-1.17.pom": { + "sha1": "80948bd07c5db60753d8d5a9164b8b2272e0842a", + "sha256": "sha256-6VarXS60j6uuEjANDNLTKU1KKkGrwgaMI8tNYK12y+U=" + }, + "animal-sniffer-annotations-1.17.jar": { + "sha1": "f97ce6decaea32b36101e37979f8b647f00681fb", + "sha256": "sha256-kmVPST7P7FIILnY1Tw6/h2SNw9XOwuPDzblHwBZ0elM=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-annotations/1.18", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-annotations-1.18.pom": { + "sha1": "82452242e8e2d0e058182cb01f29c3c30391dac3", + "sha256": "sha256-rfUi9IOcNfUynql8QHrr6/qIB7ZEhS3E1c18l7em0uA=" + }, + "animal-sniffer-annotations-1.18.jar": { + "sha1": "f7aa683ea79dc6681ee9fb95756c999acbb62f5d", + "sha256": "sha256-R/BYUrSO6brv74D6PYzqYO+kdTwAExId1/5e7y5ccp0=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-annotations/1.19", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-annotations-1.19.pom": { + "sha1": "45a124d1de95705d6d8e7547e131ebdb3a99fada", + "sha256": "sha256-Op1RE9gNzBnFyOZBvXBougot+AQgH1aIrSfQCUxs8dI=" + }, + "animal-sniffer-annotations-1.19.jar": { + "sha1": "1536e1a8fd552dc05f12b5f0827fbb2ee3d5a89b", + "sha256": "sha256-5n7CfO6vE6tdVM9f28xUTEG0240C2fAGZ4zKLHwT7p0=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-annotations/1.24", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-annotations-1.24.pom": { + "sha1": "a671a0325694922bebf4cf062dde813827c82b4b", + "sha256": "sha256-iEhPYKatQjipf+us8rMz6eCMF4uPGAoFo+2/9KOKg24=" + }, + "animal-sniffer-annotations-1.24.jar": { + "sha1": "aa9ba58d30e0aad7f1808fce9c541ea3760678d8", + "sha256": "sha256-xyDm5by+ay9I3tdaR7zNt2Pu3nnRQzAQLg01Lj2J7ZI=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-parent/1.14", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-parent-1.14.pom": { + "sha1": "c1e91a9c2f36d9e6d3c7087242d8d9ec56052e5d", + "sha256": "sha256-9RVQoGsUEL1JYssOcd8Lkhpgp+9Hv6nEgloUvnIxbuo=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-parent/1.17", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-parent-1.17.pom": { + "sha1": "eea4b805793494ebb025e5a9926ea3cf1ee8b34d", + "sha256": "sha256-GKA98W4qGExYLbexJWM8Fft3FAJ6hMG1MtcpM9wIuB8=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-parent/1.18", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-parent-1.18.pom": { + "sha1": "dd9080d0b2019e3cbb8b97dc7c057787062172f2", + "sha256": "sha256-Tp31RqR89jBKExfEaHAQCocm++oRsN0YMi+VfkBwlzw=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-parent/1.19", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-parent-1.19.pom": { + "sha1": "dd4912ea50c7ed0941f713e49457212e83eab926", + "sha256": "sha256-Z6tXJ/etOO8b5yoFOauQNqdkTz678BBofm3ZdaIelbg=" + } + } + }, + + { + "path": "org/codehaus/mojo/animal-sniffer-parent/1.24", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "animal-sniffer-parent-1.24.pom": { + "sha1": "3c8a9968c0b8ccdc8fe7dc77a97e54e406cd0c56", + "sha256": "sha256-Sd2rQ8g2HcLvDB/4fLWQ+nIxcCq59i4m1RLcGKHxzQQ=" + } + } + }, + + { + "path": "org/codehaus/mojo/mojo-parent/34", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mojo-parent-34.pom": { + "sha1": "803dc5cf36e504c5a48aa9a321f7fba1d6396733", + "sha256": "sha256-Pjldb7xDwJo3dMrIaUzlJzmDBeo/1UktgOJa8n04Kpw=" + } + } + }, + + { + "path": "org/codehaus/mojo/mojo-parent/40", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mojo-parent-40.pom": { + "sha1": "d2fa7c95447827e9bbcb8c60bd9484c51202732e", + "sha256": "sha256-/GSNzcQE+L9m4Fg5FOz5gBdmGCASJ76hFProUEPLdV4=" + } + } + }, + + { + "path": "org/codehaus/mojo/mojo-parent/50", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mojo-parent-50.pom": { + "sha1": "8caf7f67d94e762418df68f034c88b6f18433860", + "sha256": "sha256-+BnK0bFbaneRyLYB6WveM3ZeRoE5WAfbRTfS8N7dSTs=" + } + } + }, + + { + "path": "org/codehaus/mojo/mojo-parent/84", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mojo-parent-84.pom": { + "sha1": "f7511e802abf301d927dd6085f2ee1826692b08f", + "sha256": "sha256-L+UQYYsvYPzV8vuCvEssLDRASNdPML5xn8uGgp7orDA=" + }, + "mojo-parent-84-config.jar": { + "sha1": "f93f72bef2edb3fd5335463c8c4b63b8aa5e8f2c", + "sha256": "sha256-uhHUHE/fK1JVLEM2T02t9kQgcl1BUinRUztUyPbudrw=" + } + } + }, + + { + "path": "org/codehaus/woodstox/stax2-api/4.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stax2-api-4.2.1.pom": { + "sha1": "052c4497300ff75147fade804f05351ded9e9eec", + "sha256": "sha256-edpBDIwPRqP46K2zDWwkzNYGW272v96HvZfpiB6gouc=" + }, + "stax2-api-4.2.1.jar": { + "sha1": "a3f7325c52240418c2ba257b103c3c550e140c83", + "sha256": "sha256-Z4Vn5ItRpCxlxpnyZlOa09Z21LGlsK19iezoudV3JXk=" + } + } + }, + + { + "path": "org/easymock/easymockclassextension/3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "easymockclassextension-3.2.pom": { + "sha1": "7a11e8397359652228c0d359de72f8238dafd758", + "sha256": "sha256-ymIC5wQSHhWGaXYGCSbK7OjK1duVnBtA44rDny3wQEA=" + }, + "easymockclassextension-3.2.jar": { + "sha1": "9c922ad77729f27eb7872aa570287e239a8346da", + "sha256": "sha256-4q6z7OyH2Fmy8wcphdSxWHNVi89kEPQi2wwMUZTHbIc=" + } + } + }, + + { + "path": "org/easymock/easymock-parent/3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "easymock-parent-3.2.pom": { + "sha1": "1d130e585b385841b0ac06f546e0fcd859074620", + "sha256": "sha256-pi/34MnrpdXxVmh0U+RqJYpiBr4zIutlA0jVgPTes44=" + } + } + }, + + { + "path": "org/easymock/easymock-parent/5.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "easymock-parent-5.5.0.pom": { + "sha1": "a30937cb15aa72370d3841ca217dd04cd24d506c", + "sha256": "sha256-e7FiABT3ApRLbKcywPIh6jLHwyfQ9fM6zuGf/gtAJ6A=" + } + } + }, + + { + "path": "org/easymock/easymock/5.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "easymock-5.5.0.pom": { + "sha1": "a711c875746fa00d8c929824b0f99ed7f103ac75", + "sha256": "sha256-0TuotyX47NbHANkl9v78MwXLkIGtzLN97cUxr9HzfQY=" + }, + "easymock-5.5.0.jar": { + "sha1": "b07e2bcbc7b6bcae190a18b9ced742b6fd9ceec3", + "sha256": "sha256-vwW1axAr6Rx80LutFoTVgJrCzNA4DmhPlEW/3hRBj38=" + } + } + }, + + { + "path": "org/eclipse/angus/angus-activation-project/2.0.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "angus-activation-project-2.0.2.pom": { + "sha1": "94be80476a7ca9a1b9ca1357be5823829a2bb328", + "sha256": "sha256-r5GIoQy4qk61/+bTkfHuIVnx6kp/2JDuaYYj5vN52PY=" + } + } + }, + + { + "path": "org/eclipse/angus/angus-activation/2.0.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "angus-activation-2.0.2.pom": { + "sha1": "7e58bd2f7b207783653d04efc9d2db4cb4ac884c", + "sha256": "sha256-deViGn3IWMmW7nDGtNiE2QHRh4Ns5sZxIMr5VH5vxXE=" + }, + "angus-activation-2.0.2.jar": { + "sha1": "41f1e0ddd157c856926ed149ab837d110955a9fc", + "sha256": "sha256-bdO8/8IrzoOwc3ag4uCU5JZKMZXUEY+0PjgO81Q2zB4=" + } + } + }, + + { + "path": "org/eclipse/ee4j/project/1.0.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "project-1.0.2.pom": { + "sha1": "5730bedee1faaeeb62b0afc4ed1c4571f87a8eb4", + "sha256": "sha256-dJWgenl+iOQ8O8GodCG9ix/FXjIpH6GOTjLYAx3chz8=" + } + } + }, + + { + "path": "org/eclipse/ee4j/project/1.0.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "project-1.0.5.pom": { + "sha1": "d391c5ed15d8fb1dadba9c5d1017006d56c50332", + "sha256": "sha256-kWtHlNjYIgpZo/32pk2+eUrrIzleiIuBrjaptaLFkaY=" + } + } + }, + + { + "path": "org/eclipse/ee4j/project/1.0.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "project-1.0.6.pom": { + "sha1": "9adda6d67ddf76eb9ed4e1c331d705d03dc2a94b", + "sha256": "sha256-Tn2DKdjafc8wd52CQkG+FF8nEIky9aWiTrkHZ3vI1y0=" + } + } + }, + + { + "path": "org/eclipse/ee4j/project/1.0.7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "project-1.0.7.pom": { + "sha1": "b0f6c4bc691a0b694a377edc9bc4777871647253", + "sha256": "sha256-IFwDmkLLrjVW776wSkg+s6PPlVC9db+EJg3I8oIY8QU=" + } + } + }, + + { + "path": "org/eclipse/ee4j/project/1.0.9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "project-1.0.9.pom": { + "sha1": "5c02bae4f34e508f6e22b0f7beb0fef77880ad02", + "sha256": "sha256-glN5k0oc8pJJ80ny0Yra95p7LLLb4jFRiXTh7nCUHBc=" + } + } + }, + + { + "path": "org/eclipse/jdt/core/compiler/ecj/4.4.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ecj-4.4.2.pom": { + "sha1": "b0bbf104bd2030ae98a2ec99ff4940cfd0106423", + "sha256": "sha256-D6igIpN1QRKbddAj9YVAlnNs83CAeLTUu/9YMZNVVZE=" + }, + "ecj-4.4.2.jar": { + "sha1": "71d67f5bab9465ec844596ef844f40902ae25392", + "sha256": "sha256-LW7iFVS7ugErawODvm5lh/o1NwEE5BwQo+tHA5+j5tE=" + } + } + }, + + { + "path": "org/eclipse/jdt/core/compiler/ecj/4.6.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ecj-4.6.1.pom": { + "sha1": "bdc645b0c5c9c6535844192844245b8de465e8d3", + "sha256": "sha256-U2xF6IBN4BMhxHjPuZQ3f4Hx4yiXY2YW+iG4d/0TfG8=" + }, + "ecj-4.6.1.jar": { + "sha1": "5555607add54eb79c5a0729134e663fe2e6300c2", + "sha256": "sha256-nN3adfShtEaec/ROe2Gj6JfQ9lffR5f5EG/+iMTureA=" + } + } + }, + + { + "path": "org/eclipse/jgit/org.eclipse.jgit-parent/5.13.0.202109080827-r", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.eclipse.jgit-parent-5.13.0.202109080827-r.pom": { + "sha1": "a2e71115828f17f51090c4a0d3a511e7e50eefad", + "sha256": "sha256-oY/X0MQf2o2PHEoktQAKhmRWFHokdG7mzEcx54ihje4=" + } + } + }, + + { + "path": "org/eclipse/jgit/org.eclipse.jgit-parent/5.13.1.202206130422-r", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.eclipse.jgit-parent-5.13.1.202206130422-r.pom": { + "sha1": "b1ddb45d726f9720db298988b67d8654fc707aef", + "sha256": "sha256-3mX8du2PRKV3he4WSRjcPU9V7j/qake8fLG6oZ7xvIM=" + } + } + }, + + { + "path": "org/eclipse/jgit/org.eclipse.jgit/5.13.0.202109080827-r", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.eclipse.jgit-5.13.0.202109080827-r.pom": { + "sha1": "4659260c98ae15e350f1ee7d3d31aed67f0ac2f8", + "sha256": "sha256-qEF3Rc+i2V1qlxHpQT/KmE/FZmt2J7rRVAzyfUYq6BM=" + }, + "org.eclipse.jgit-5.13.0.202109080827-r.jar": { + "sha1": "32badd5adebd660424509344259ad3e409699ba2", + "sha256": "sha256-2r+DafDN+M8Xt/faS9qTIMVwu3VMiOC+t7hSgSz1zSU=" + } + } + }, + + { + "path": "org/eclipse/jgit/org.eclipse.jgit/5.13.1.202206130422-r", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.eclipse.jgit-5.13.1.202206130422-r.pom": { + "sha1": "849ac18b1db0534ab42bf842a0efd144b9d01dd6", + "sha256": "sha256-PMS+v5yTZXjq4rbST7KpEtiZk6VP0OL7wcirwEmHhRU=" + }, + "org.eclipse.jgit-5.13.1.202206130422-r.jar": { + "sha1": "841d1ae74e4bc77ac7d4b106f15d0468dc7ac7f2", + "sha256": "sha256-1nk2WmyOVcFJZwEJn15XZUM/aNy2WXWUFrpbIi61BVw=" + } + } + }, + + { + "path": "org/fusesource/fusesource-pom/1.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "fusesource-pom-1.11.pom": { + "sha1": "f5547b0657be718d1ec0d2816cbcd5c398d6fccb", + "sha256": "sha256-aV24M14z3j1AOz0fBCWXDzcQqdtKUo7EYBmQVe8eirg=" + } + } + }, + + { + "path": "org/fusesource/jansi/jansi-project/1.18", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jansi-project-1.18.pom": { + "sha1": "5142f25c525a3184def79c5f690e5638971c50ff", + "sha256": "sha256-Asg2iTz9H1RZklohY1pdLEEHVEL7lnOtPNrtVzuegA0=" + } + } + }, + + { + "path": "org/fusesource/jansi/jansi/1.18", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jansi-1.18.pom": { + "sha1": "0e5be937c76095ab5b16f2cc6747cbb58b2414ce", + "sha256": "sha256-3Y9hnCwqTXtvgViUKNt3BRRSjlnOBTLwcY/uM6Jl+0s=" + }, + "jansi-1.18.jar": { + "sha1": "d9205bbcd4b5f9cd1effe752d18f73bd942d783f", + "sha256": "sha256-EJ5k/GV2fHoaO9ZUcJ128Qewo7Odsyy/EROeE6b1Ips=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-bom/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-2.2.11.pom": { + "sha1": "eed2bcd59f35f07507c553bbd7da20415cc7f63e", + "sha256": "sha256-pS5oUPBw3J96KlHQsl36r9SbC5v4GW22wLPFt8lNjTg=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-bom/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-2.3.1.pom": { + "sha1": "a5a4f48c2e08c00b5bea5dd8ada4ccf864c3f9cf", + "sha256": "sha256-bMEmbPMGVXtPLQnL2M1udbXvDFdzyk73Y9T3MN+Ue2Q=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-bom/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-2.3.2.pom": { + "sha1": "c7cd28b5d3871feba8cebf6d4587043fb0d6fd0a", + "sha256": "sha256-oQGLtUZ47Z9ayy96QITjhf9RAgH06dv1913GpnX2a+c=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-bom/4.0.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-bom-4.0.5.pom": { + "sha1": "a86fce599edf93050d83862d319078bcc8298dc2", + "sha256": "sha256-7JfsQtk308iVGXl+RCRvgN4IUIGax6euZ1xEl7cHXDk=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-core/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-core-2.2.11.pom": { + "sha1": "f3208abdc61be827cf28838c3881213648807821", + "sha256": "sha256-7DFAnyA7yr+ZU09ZIx7AV22HXU1Lc0mwlWanqMgXmyQ=" + }, + "jaxb-core-2.2.11.jar": { + "sha1": "f5745049f5fb9cb9d9b5f513c207727f475983e9", + "sha256": "sha256-N7yu6OuwQ2LINSpb9iIbhpZ+zatRZMaWsQuaK7WHsqo=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-core/4.0.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-core-4.0.5.pom": { + "sha1": "ad142d8f32e7e7cc200363ae469d6df5029ec286", + "sha256": "sha256-S1OSE3b7/exg46j+8FjMPcmdn8o+nJUOhZKYeYRghXI=" + }, + "jaxb-core-4.0.5.jar": { + "sha1": "007b4b11ea5542eea4ad55e1080b23be436795b3", + "sha256": "sha256-rT/ZvwDePtqYWfcLbPsBHi/pkEgE4WomZQkoiOzg/co=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-runtime/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-runtime-2.2.11.pom": { + "sha1": "6a1651361e4c2392aff30da0df648187f670f8cb", + "sha256": "sha256-5TJ7MbWVq4FD6Xg21czfhf65Hn/1Zm97JpE2MvrMpKo=" + }, + "jaxb-runtime-2.2.11.jar": { + "sha1": "0065510afc78679e347b0d774617a97fedac94f8", + "sha256": "sha256-qHTyNRz7qOKUa+MALRDBim2o8htSuirPUvK4XVUg7XA=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-runtime/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-runtime-2.3.1.pom": { + "sha1": "1856da23a80b9b1374d925d6dcb4a21db2144204", + "sha256": "sha256-+BAbhhV/v8AZSf/cfFnqccojt+ziX2p5Bh/gHLdQ/wA=" + }, + "jaxb-runtime-2.3.1.jar": { + "sha1": "dd6dda9da676a54c5b36ca2806ff95ee017d8738", + "sha256": "sha256-Rf7PpcghfOHzZSq5UXl5DsjMDewDhLylHL65Sik9ny8=" + } + } + }, + + { + "path": "org/glassfish/jaxb/jaxb-runtime/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jaxb-runtime-2.3.2.pom": { + "sha1": "f46a0f023205138c366f0b0c9316f8663bb32a4f", + "sha256": "sha256-lEilrX+mimCD375PQsjIPggrkgKhBUAfxo6UTCZUizQ=" + }, + "jaxb-runtime-2.3.2.jar": { + "sha1": "5528bc882ea499a09d720b42af11785c4fc6be2a", + "sha256": "sha256-5uCh6J+2/3hieeagCC1c71LcLr5nBT0EGABzdlK0/Rs=" + } + } + }, + + { + "path": "org/glassfish/jaxb/txw2/2.2.11", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "txw2-2.2.11.pom": { + "sha1": "4be03527dbf2428f7ea99fb9c2f50f089dffad5e", + "sha256": "sha256-hRTLcktPylmlzycrYy5Tm9Cg88rPGEQILQoXOoZAa9g=" + }, + "txw2-2.2.11.jar": { + "sha1": "2df047d8c187a62f2177bf6013f1f9786cdfc8a2", + "sha256": "sha256-Jyo8ytRaRRE1GSDNKoYzxTyrjVIgx6kpVNpVJrter+o=" + } + } + }, + + { + "path": "org/glassfish/jaxb/txw2/2.3.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "txw2-2.3.1.pom": { + "sha1": "c78aa440484eab1a6e2104e4fe69d0945a3cb3da", + "sha256": "sha256-RxTUbKz3Aqss1HjWBIxdnMV6vbVfTAe2t6pyeZ4C02w=" + }, + "txw2-2.3.1.jar": { + "sha1": "a09d2c48d3285f206fafbffe0e50619284e92126", + "sha256": "sha256-NJdd3hxpIPGjl5EUIjVom8PNNX4k0F7dj/k7iFvWjWA=" + } + } + }, + + { + "path": "org/glassfish/jaxb/txw2/2.3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "txw2-2.3.2.pom": { + "sha1": "22d24b523eb02a955e8bfa7af6572207a3490482", + "sha256": "sha256-p53QAvsDgYP/KGomNb4uaMEDuH4OZHF9jUS/0Bf9M+o=" + }, + "txw2-2.3.2.jar": { + "sha1": "ce5be7da2e442c25ec14c766cb60cb802741727b", + "sha256": "sha256-SmqfSDOI1GG4GqmijGhbi3TAWXmTvxiEsE7dvKlfSP4=" + } + } + }, + + { + "path": "org/glassfish/jaxb/txw2/4.0.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "txw2-4.0.5.pom": { + "sha1": "d5ec8832a8653dbf2c03ef6baca0337e6978d849", + "sha256": "sha256-a2gzdWAy4YOaQBRoFaWY7IsFg6Lv+9Rd311UMB9Ky6s=" + }, + "txw2-4.0.5.jar": { + "sha1": "f36a4ef12120a9bb06d766d6a0e54b144fd7ed98", + "sha256": "sha256-kXNVvEUUgfMNBDsk0SMRBReWavNDg5AXc4goENykgOU=" + } + } + }, + + { + "path": "org/hamcrest/hamcrest-core/1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "hamcrest-core-1.3.pom": { + "sha1": "872e413497b906e7c9fa85ccc96046c5d1ef7ece", + "sha256": "sha256-/eOGp5BRc6GxA95quCBydYS1DQ4yKC4nl3h8IKZP+pM=" + }, + "hamcrest-core-1.3.jar": { + "sha1": "42a25dc3219429f0e5d060061f71acb49bf010a0", + "sha256": "sha256-Zv3vkelzk0jfeglqo4SlaF9Oh1WEzOiThqekclHE2Ok=" + } + } + }, + + { + "path": "org/hamcrest/hamcrest-core/3.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "hamcrest-core-3.0.pom": { + "sha1": "e419abe63f1e41cb1aa41225f0bb532b2c6e6e1a", + "sha256": "sha256-1kyZW1baUn0+fhhmuRkNmL0QuYanfYET7EVQeZ44WqQ=" + }, + "hamcrest-core-3.0.jar": { + "sha1": "254f69f4b0ca22198acfc19fcdd5f96140431c3e", + "sha256": "sha256-t4o6gWkvQhzAH8F97ZpF6ftvOUnHEvjsTQHaa4wGvG4=" + }, + "hamcrest-core-3.0.module": { + "sha1": "796d1cd3d6308a0d7338827644448450dd9b12f0", + "sha256": "sha256-gweKVn1bT3j27cznGvQfDnRjmIELze3N/QLiRVe8mmA=" + } + } + }, + + { + "path": "org/hamcrest/hamcrest-parent/1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "hamcrest-parent-1.3.pom": { + "sha1": "80391bd32bfa4837a15215d5e9f07c60555c379a", + "sha256": "sha256-bVNflO+2Y722gsnyelAzU5RogAlkK6epZ3UEvBvkEps=" + } + } + }, + + { + "path": "org/hamcrest/hamcrest/2.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "hamcrest-2.2.pom": { + "sha1": "98624f676c4d263b568816b0af5d4f552a2d0501", + "sha256": "sha256-s2E3N2xLP8923DN+KhvFtpGirBqpZqtdJiCak4EvpX0=" + }, + "hamcrest-2.2.jar": { + "sha1": "1820c0968dba3a11a1b30669bb1f01978a91dedc", + "sha256": "sha256-XmKEaonwXNeM2cGlU/NA0AJFg4DDIEVd0fj8VJeoocE=" + } + } + }, + + { + "path": "org/hamcrest/hamcrest/3.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "hamcrest-3.0.pom": { + "sha1": "3c85240a040ddb51dc33d405ef0e075ec68b6796", + "sha256": "sha256-SgSmgTO/MkYfR7ilPI7p30zi9JoNrZeUvOhxe+5brDE=" + }, + "hamcrest-3.0.jar": { + "sha1": "8fd9b78a8e6a6510a078a9e30e9e86a6035cfaf7", + "sha256": "sha256-XWa2pKaAdVy27XyxBPp4Ne9kRmdYb/Bzet65d8Oezbw=" + }, + "hamcrest-3.0.module": { + "sha1": "ac51ee99de2e24e2776e9a76e49040b570811dd9", + "sha256": "sha256-mhBVNzjTWME+a69Zeb8sGlSQ7uScLcas8xcPzKCSDd4=" + } + } + }, + + { + "path": "org/jacoco/org.jacoco.build/0.7.4.201502262128", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.jacoco.build-0.7.4.201502262128.pom": { + "sha1": "f65953b9eb96d0ba6c218b851d4da48374f9d037", + "sha256": "sha256-8FbtYaKBs+Iwpj5Kv1EMDS+b9/9RPKy2WilTi6kKsXc=" + } + } + }, + + { + "path": "org/jacoco/org.jacoco.build/0.8.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.jacoco.build-0.8.13.pom": { + "sha1": "b7b9eb1f9703f481f197bf02ec7c31ac82579603", + "sha256": "sha256-mUw64c/KZ2WWj+Pt5PN5mGrrLn61GiAUyqSgnXGo9KM=" + } + } + }, + + { + "path": "org/jacoco/org.jacoco.core/0.7.4.201502262128", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.jacoco.core-0.7.4.201502262128.pom": { + "sha1": "28288d676edea9ea4587d9e4b6f5940c5b2afdd1", + "sha256": "sha256-z342MOGSnd7/ywMV2yrLNxgskt06+XARG2EVWS9k0Hc=" + }, + "org.jacoco.core-0.7.4.201502262128.jar": { + "sha1": "31482f50411a3a5769b6c78fa9b97ba731c205e7", + "sha256": "sha256-7Ex0VUMS+sURY1AWR4b5GzXJ4IL6TqWYv6QrXbBders=" + } + } + }, + + { + "path": "org/jacoco/org.jacoco.core/0.8.13", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.jacoco.core-0.8.13.pom": { + "sha1": "8d4f49611003d3bbb9c705a658c1b86c4aee3e50", + "sha256": "sha256-73/2F3zZcB1QXv6C9N1P6l5fcssEesK088qitBbaD08=" + }, + "org.jacoco.core-0.8.13.jar": { + "sha1": "4424d689f0738cd4445e948270e3f2f2d0e5cdb8", + "sha256": "sha256-UUwj32z9AV19g8EKeS41q2int+gvPWo6RIF2LBMuM6k=" + } + } + }, + + { + "path": "org/jacoco/org.jacoco.report/0.7.4.201502262128", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.jacoco.report-0.7.4.201502262128.pom": { + "sha1": "0a99ef0376820e97a5bfe2da266a2f6a68ae5cb5", + "sha256": "sha256-5lMaJ4tPYy8Zim5FueEd4bK2eM1aTg/f9EL2BMJDmkI=" + }, + "org.jacoco.report-0.7.4.201502262128.jar": { + "sha1": "5c6bb5f95ae695dc4510b95f7d5ad0397e4881ad", + "sha256": "sha256-ejVUxgXgiOfjI7EIRlYkPwRE+jU+Ly3uHxpCBOtk/wk=" + } + } + }, + + { + "path": "org/jdom/jdom2/2.0.6", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jdom2-2.0.6.pom": { + "sha1": "11e250d112bc9f2a0e1a595a5f6ecd2802af2691", + "sha256": "sha256-R7I6ef4za3QbgkNMbgSdaBZSVuQF51wQkh/XL6imXY0=" + }, + "jdom2-2.0.6.jar": { + "sha1": "6f14738ec2e9dd0011e343717fa624a10f8aab64", + "sha256": "sha256-E0XxG6YG0VYD1nQFUajCGUfAIVZAdw7GcnH+eL6pfPU=" + } + } + }, + + { + "path": "org/jetbrains/annotations/13.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-13.0.pom": { + "sha1": "fa7d3d07cc80547e2d15bf4839d3267c637c642f", + "sha256": "sha256-llrrK+3/NpgZvd4b96CzuJuCR91pyIuGN112Fju4w5c=" + }, + "annotations-13.0.jar": { + "sha1": "919f0dfe192fb4e063e7dacadee7f8bb9a2672a9", + "sha256": "sha256-rOKhDcji1f00kl7KwD5JiLLA+FFlDJS4zvSbob0RFHg=" + } + } + }, + + { + "path": "org/jetbrains/annotations/23.0.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-23.0.0.pom": { + "sha1": "deb853be3c9e05ed99fbbf83e49d349d08a6d10e", + "sha256": "sha256-yUkPZVEyMo3yz7z990P1P8ORbWwdEENxdabKbjpndxw=" + }, + "annotations-23.0.0.jar": { + "sha1": "8cc20c07506ec18e0834947b84a864bfc094484e", + "sha256": "sha256-ew8ZckCCy/y8ZuWr6iubySzwih6hHhkZM+1DgB6zzQU=" + } + } + }, + + { + "path": "org/jetbrains/dokka/dokka-core/1.4.32", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "dokka-core-1.4.32.pom": { + "sha1": "277b47a99ed1f1b3262e689549cec76906afa641", + "sha256": "sha256-hpAJXh2+OvzYqU8P6mPyIRJ5rO/gu9/Y8/qIteqG3QY=" + }, + "dokka-core-1.4.32.jar": { + "sha1": "3011d0a885e87643e41d41a902e5905934251499", + "sha256": "sha256-tRT0/RHEENG/RBuBXaAXYH0ypQDh0gDEIxga7D7Wb7E=" + }, + "dokka-core-1.4.32.module": { + "sha1": "acf43f0907576a62e22fb9c676f38b6e809971d1", + "sha256": "sha256-ywOOMYKuKKjlcyXp4PbR8y07GfR0P4MXocxaAtRefJU=" + } + } + }, + + { + "path": "org/jetbrains/intellij/deps/trove4j/1.0.20181211", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "trove4j-1.0.20181211.pom": { + "sha1": "ad9cb821b804f05ed013ce89e3ec4d32d101bd79", + "sha256": "sha256-MQpqotkFNMMrj0bx/JjNDtrpXc38oj4oR+Xvqa4MAZo=" + }, + "trove4j-1.0.20181211.jar": { + "sha1": "216c2e14b070f334479d800987affe4054cd563f", + "sha256": "sha256-r/t8haPIe9z2n/HbuE3hH2PckxKTk0vAjNerGN4INgE=" + } + } + }, + + { + "path": "org/jetbrains/intellij/deps/trove4j/1.0.20200330", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "trove4j-1.0.20200330.pom": { + "sha1": "c1f38f72d4dfc6efd773b0448a7aae08f8d5025f", + "sha256": "sha256-h3IcuqZaPJfYsbqdIHhA8WTJ/jh1n8nqEP/iZWX40+k=" + }, + "trove4j-1.0.20200330.jar": { + "sha1": "3afb14d5f9ceb459d724e907a21145e8ff394f02", + "sha256": "sha256-xf1yW/+rUYRr88d9sTg8YKquv+G3/i8A0j/ht98KQ50=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/atomicfu-jvm/0.16.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "atomicfu-jvm-0.16.3.pom": { + "sha1": "d2998b7424ad282ed29f71cec8ee99ed8159069e", + "sha256": "sha256-AXDVtkrdbDgBWymIP0N1HoXzrPoJ+GDx4NxGCWfZ3GA=" + }, + "atomicfu-jvm-0.16.3.jar": { + "sha1": "94e988a2e1365361e619ad57639a493cdcaefdf6", + "sha256": "sha256-b9jw/kKN0q+OQldC5FX9XtODYBEcl8wfwW8QYQk+5c8=" + }, + "atomicfu-jvm-0.16.3.module": { + "sha1": "67f0b1990f4773d3c91bf680c1f4074007f962f5", + "sha256": "sha256-JziDI2X7rpxGND9eEP4dVrzfT5Ww8noWdzxL5Fs78g8=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/atomicfu-jvm/0.17.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "atomicfu-jvm-0.17.3.pom": { + "sha1": "0013f60197fbb974d857920601d9f9e469c5278f", + "sha256": "sha256-Lt015lGsAczGai/vKC4yqmmIAJ8w1s/BijJb5cKnbmc=" + }, + "atomicfu-jvm-0.17.3.jar": { + "sha1": "beb13bc80c2fbb4797fda67838437fc847c1fbf6", + "sha256": "sha256-2saxZShWiWjqsI3u8Da4WUo/3g3JSmeAk4pObyWSk+k=" + }, + "atomicfu-jvm-0.17.3.module": { + "sha1": "44dfad74338c47442c0528e2d31b5fded79d12c1", + "sha256": "sha256-Un/IlodfVvClx0vs1gVtbTSpMrE8VnSZw2R+x//Culk=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/atomicfu/0.16.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "atomicfu-0.16.3.pom": { + "sha1": "6e713226565126814aa9364f7ca3d23cc8e82bb2", + "sha256": "sha256-Am7E/9ubEBOrpU1/yW//hKPUH2OAxF+g7ag+whV3e2A=" + }, + "atomicfu-0.16.3-all.jar": { + "sha1": "b3d730ea4d198024b4f540a1716c1e57be939f12", + "sha256": "sha256-w8BUbdM+DvdXgnNNlwPrNYOihBSx1EZPy95W1wp9Wm0=" + }, + "atomicfu-0.16.3.jar": { + "sha1": "3cde34906d393aa43c5536ea4d7516e73103a4c4", + "sha256": "sha256-XNn8ufayvmmLnjHMBWM0hsgYPBnwwRz0lJ1EX9WH2hk=" + }, + "atomicfu-0.16.3.module": { + "sha1": "cd883c81e058397e02232164eecf55b6aca64f58", + "sha256": "sha256-pYdlUI1atA936wbMb1Bv5b7pXi4gALN1L9gyk5Rohw0=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/atomicfu/0.17.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "atomicfu-0.17.3.pom": { + "sha1": "c74792f0f339d2d746805ab17213f38401220772", + "sha256": "sha256-tYeGz3ULcsvA7OKIjgjTKsxQvNQo5pCiBaSQMIHtna4=" + }, + "atomicfu-0.17.3-all.jar": { + "sha1": "badc7fb54d8595ed814c866f67a695d69a672ba4", + "sha256": "sha256-UvpUqOTe6AjI/s9Rs5JHiOi1V2yq8n1tYK+mWA+tSvA=" + }, + "atomicfu-0.17.3.jar": { + "sha1": "a3146ed7217e7a94bb44354efb45931d624da065", + "sha256": "sha256-eAj89x/rrIX/bmXvFF0zi8kETMeEGyIhajnDzqiBCYw=" + }, + "atomicfu-0.17.3.module": { + "sha1": "f85fcb4c1495d1a13dd6bc1621fc1a09b18e88fd", + "sha256": "sha256-hUp1qevzDLWI6M7afaG3CJ1CcqEjJNPP/K9bYpAnOL0=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-android/1.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-android-1.5.2.pom": { + "sha1": "f2aa94138aa8b932c65b8f0ea18a1da1b6616bbb", + "sha256": "sha256-vzdYkTCjnPMK9b114jEBdgVg1xc2UCxmOE3dPrKZ4OY=" + }, + "kotlinx-coroutines-android-1.5.2.jar": { + "sha1": "d246a704a55b7bddb79407cce4348890eaa341d9", + "sha256": "sha256-hs+YkrC9Uwao9NetioI1b2FNx9UZ6zBjsIh9fCtAWSg=" + }, + "kotlinx-coroutines-android-1.5.2.module": { + "sha1": "7979e0cc260c885dd9a7c893b5c08cfeb5f4236d", + "sha256": "sha256-W3bMvSdEMoh+HnU2MTqAjU6wVJcdXgZIBk0T0nJkyBE=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-android/1.6.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-android-1.6.1.pom": { + "sha1": "f931f768186549cef2017b979844db15d2959783", + "sha256": "sha256-Cf/ln3zZ3LXsRAgD343MhYHC3hfS66AIi6PN2MVzEKI=" + }, + "kotlinx-coroutines-android-1.6.1.jar": { + "sha1": "4e61fcdcc508cbaa37c4a284a50205d7c7767e37", + "sha256": "sha256-lh696BOHeaKZQwyjJahuKMSQUnqHulGDYvpFxEx+fpU=" + }, + "kotlinx-coroutines-android-1.6.1.module": { + "sha1": "3e96729b38bd4a020f86fece3d1c071bc6160117", + "sha256": "sha256-VYo+blzFAfE6rAQ2vQi7NC0Q3p+Lz9O6bVgvQz6Sw7E=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-android/1.6.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-android-1.6.4.pom": { + "sha1": "ee0113f64713ff95a3487c4fcffa063cffc0af5f", + "sha256": "sha256-SffjJHTWQ5bs4sAU29wiBNZlz6j9c+aaahs/qxOBmLc=" + }, + "kotlinx-coroutines-android-1.6.4.jar": { + "sha1": "f955fc8b2ad196e2f4429598440e15f7492eeb2b", + "sha256": "sha256-P9wO7VvEuD7pYid0UgotslRwNw6s0VgcrB43cE8JWwA=" + }, + "kotlinx-coroutines-android-1.6.4.module": { + "sha1": "2d4f1251441c613fe829c2d8f360c3023ce2513b", + "sha256": "sha256-aFFlWeb4SmIbl4PNiSpkYwzNeHWENYjds/BQFCXjPxU=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-android/1.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-android-1.7.3.pom": { + "sha1": "24c62e9584cd03c6cdabbad9dd5b5e4a32e1887e", + "sha256": "sha256-AgUVJG0Z4XbVYm6xGPgPl/eHbRrM2M7BWxpBWSV9UFQ=" + }, + "kotlinx-coroutines-android-1.7.3.jar": { + "sha1": "38d9cad3a0b03a10453b56577984bdeb48edeed5", + "sha256": "sha256-Wf/7Jr7hLDLa3PpdQgwqfbhdMlNRgSixcO/acmYTJW0=" + }, + "kotlinx-coroutines-android-1.7.3.module": { + "sha1": "cb6318617558a0ad8457289ccc55fdc59fec8d1b", + "sha256": "sha256-SN/YE57e5UgbzIsl4k11hqymFfDR7SvrJC3HR47UzuA=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.6.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-bom-1.6.1.pom": { + "sha1": "fdf8cfa27badfb963b5577d6263100742c6ca543", + "sha256": "sha256-Q+eA7D2mlZx9LJf9a0RiypmX6cA81sQ+6S9mBAItS64=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.6.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-bom-1.6.3.pom": { + "sha1": "8cade619240db870890b137e8d823e24c6528370", + "sha256": "sha256-cMN0jcFiaLZok73ZliGhu3JHLXDwYSqDH34zyZHCLwo=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.6.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-bom-1.6.4.pom": { + "sha1": "88f782b09538e0bda7fa5ee6926f102564009bb6", + "sha256": "sha256-qyYUhV+6ZqqKQlFNvj1aiEMV/+HtY/WTLnEKgAYkXOE=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-bom-1.7.3.pom": { + "sha1": "6563a4ccb53e4678a32841c5dbb0dae1c026aa0f", + "sha256": "sha256-Tl0ZAOY3nvP1lw0EqPMFKa3IL4WejMEHwhzoFJ72ZsQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-jvm-1.4.1.pom": { + "sha1": "c3b94b2a93623a25ca099abaed0361497ce33d3a", + "sha256": "sha256-ExUgW4eFNL+c4TF75Qf3rxDL+42TuZjNj3GWISsKgKk=" + }, + "kotlinx-coroutines-core-jvm-1.4.1.jar": { + "sha1": "936067d1e0195f638396f60e71da745bb14a35ca", + "sha256": "sha256-bS+HdktmOPJ6/xLtOA20tjydRrpV3DJoOmUFmPpaPiI=" + }, + "kotlinx-coroutines-core-jvm-1.4.1.module": { + "sha1": "0efb6189b1f57e6cc483291f0485eb4e94c0260b", + "sha256": "sha256-5qb4/7GkC8dqk1zdXs/PVIW1zQ7CiDBEpHYMv+MxVwc=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-jvm-1.5.0.pom": { + "sha1": "8b1169a630a788212949fe4c74043cc18be763fe", + "sha256": "sha256-U2IuA3eN+EQPwBIgGjW7S9/kAWTv7GErvvze7LL/wqs=" + }, + "kotlinx-coroutines-core-jvm-1.5.0.jar": { + "sha1": "d8cebccdcddd029022aa8646a5a953ff88b13ac8", + "sha256": "sha256-eNbMcTX4TWkv83Uvz9H6G74JQNffcGUuTx6u7Ax4r7s=" + }, + "kotlinx-coroutines-core-jvm-1.5.0.module": { + "sha1": "49d762d8bf90e3eb7d5593eb554aa7e582a5af3f", + "sha256": "sha256-yIXdAoEHbFhDgm3jF+PLzcPYhZ2+71OuHPrNG5xg+W4=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-jvm-1.5.2.pom": { + "sha1": "8affee621ce2df26c98025cbba6fcbf8bc634002", + "sha256": "sha256-oaUrwxiqy+0a9MJLW5ylXjSOgKAvR6agXuLfnSRrq/U=" + }, + "kotlinx-coroutines-core-jvm-1.5.2.jar": { + "sha1": "f4cc07a50437659e0043e7da762809a46932b6a0", + "sha256": "sha256-IxTEiqGjnAXBHzs4XN88VrF0XwZDFOT+q+zrwVcrOsE=" + }, + "kotlinx-coroutines-core-jvm-1.5.2.module": { + "sha1": "e9f9c265e85ea44111d4e240209dcb3f3ac8196c", + "sha256": "sha256-4in4HmvAmAZ5NqS4Jk6kj75yj/xQ+vSnjgACeTps2P8=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-jvm-1.6.1.pom": { + "sha1": "f44ad26bda41abb1c5cabf8f788de17e5871f315", + "sha256": "sha256-luYKsbavDOQFbshov2pkpVjulwldpH90qmkVMxTTVw4=" + }, + "kotlinx-coroutines-core-jvm-1.6.1.jar": { + "sha1": "97fd74ccf54a863d221956ffcd21835e168e2aaa", + "sha256": "sha256-OpP/0FKERkPA/vlQrlV420fMy+nnF21oEzMYLiMrsPE=" + }, + "kotlinx-coroutines-core-jvm-1.6.1.module": { + "sha1": "091d3217f26f78ef3b56e06b34b1233dab25c713", + "sha256": "sha256-8xtnKhH+sO5J1dSRQwZ+k8M/WGZ2iuQrmjJLU82tNuE=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-jvm-1.6.3.pom": { + "sha1": "03ba3b6bb5940b0cd737c381609ecac7463ca573", + "sha256": "sha256-37yekvIz2aR6v8+rQmC0RGAu7fV2i74eD09CqAd7P2c=" + }, + "kotlinx-coroutines-core-jvm-1.6.3.jar": { + "sha1": "b2c2f0c8e99eac632a8db723b365bd59067cf7bd", + "sha256": "sha256-WKSXq1ldg7u/KIkqizSrV9lDCah0LuDrpDy4ZAjSNb8=" + }, + "kotlinx-coroutines-core-jvm-1.6.3.module": { + "sha1": "a6b3ddcb2355a478c2796ca976a8d1c586ef46d9", + "sha256": "sha256-fRI6TcNVJjJMV7K9q/1zhw/KYj5fm+StaSb0A7KEqXU=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-jvm-1.6.4.pom": { + "sha1": "b7c09a7e50e8d075a6f93bd0ef876cee77f5b158", + "sha256": "sha256-Cdlg+FkikDwuUuEmsX6fpQILQlxGnsYZRLPAGDVUciQ=" + }, + "kotlinx-coroutines-core-jvm-1.6.4.jar": { + "sha1": "2c997cd1c0ef33f3e751d3831929aeff1390cb30", + "sha256": "sha256-wkyLsnuzIMSpOHFQGn5eDGFgdjiQexl672dVE9TIIL4=" + }, + "kotlinx-coroutines-core-jvm-1.6.4.module": { + "sha1": "ba90650d460262c3a1cf001e55b10317a394b174", + "sha256": "sha256-DZTIpBSD58Jwfr1pPhsTV6hBUpmM6FVQ67xUykMho6c=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-jvm-1.7.3.pom": { + "sha1": "62079ae5c1ab9b0fa65dac10ef343200a5f2137a", + "sha256": "sha256-dThYdT3su7I5c0PiuHHwYvaXgS6UIuQcnuRqZrk+7jA=" + }, + "kotlinx-coroutines-core-jvm-1.7.3.jar": { + "sha1": "2b09627576f0989a436a00a4a54b55fa5026fb86", + "sha256": "sha256-GrOsw48+c1XE+dHsYhB6RvpzyJnzBw0FXl1Dc9/mfhI=" + }, + "kotlinx-coroutines-core-jvm-1.7.3.module": { + "sha1": "623a6d49effa5660cd2810a99b1ff880a0a6b727", + "sha256": "sha256-NNbumbdqwGK1FVW0pwvhg0n+VWbaeaGQYU8XHIC2U44=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-1.4.1.pom": { + "sha1": "3be5622d79563553600afd0f24121bea4a7e42e2", + "sha256": "sha256-g1Kc+qY6g8rSLZi1ST/7WXYnYSRqjEXCaK+PWPwQLzw=" + }, + "kotlinx-coroutines-core-1.4.1.jar": { + "sha1": "936067d1e0195f638396f60e71da745bb14a35ca", + "sha256": "sha256-bS+HdktmOPJ6/xLtOA20tjydRrpV3DJoOmUFmPpaPiI=" + }, + "kotlinx-coroutines-core-1.4.1.module": { + "sha1": "88b6912f6512412c8ffc12413d32ab0adbbebe2f", + "sha256": "sha256-PADkSUHxNLGMrbxfGKt7fyPT7x94r5XjRMucYF2yGkQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.5.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-1.5.2.pom": { + "sha1": "a7c7e93534ced240c9b3df6530ef5e40339c8a23", + "sha256": "sha256-9wPjHqf4URl1mWZF94WbNR6gNokDCMRWL0dI6Y8Mx+I=" + }, + "kotlinx-coroutines-core-1.5.2-all.jar": { + "sha1": "5389a2d00b711ff7e664b14c1c51ecf8a9a70387", + "sha256": "sha256-TRmhwcgr2XPQNGRPT/o9U1XLYb00V1r/hsxgng5B1uE=" + }, + "kotlinx-coroutines-core-1.5.2.jar": { + "sha1": "b69812855f235ec832341b48e721a5e62e7a888c", + "sha256": "sha256-GWLrxs/7dkXG9fF1JGkNinKAhjRPEhnmPJygxNjzJ9c=" + }, + "kotlinx-coroutines-core-1.5.2.module": { + "sha1": "5401f9d20e13467d3e9344e9d22e7f936816accd", + "sha256": "sha256-e59pYMZonRp52CyjsAuDR71X0V/kBwo9006CbnYTY5I=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-1.6.1.pom": { + "sha1": "636f282a26b8f525f6ce0adf28113ca4016a49b5", + "sha256": "sha256-kDcmCXOlH/OVPrbY2QeBEdzwechDpTlnOlh2n6WolMg=" + }, + "kotlinx-coroutines-core-1.6.1-all.jar": { + "sha1": "5a6f3063ef40feca3ee998b96b98accdef25ea22", + "sha256": "sha256-Z8gHwjbhnDICD8W6grJz/aHnbK1QaT8JF+/mBx8Vnbk=" + }, + "kotlinx-coroutines-core-1.6.1.jar": { + "sha1": "f04befbe83100278a57f652f78e1af000cbf069f", + "sha256": "sha256-Z1SJmExmI2L0j0h4miy5wqcR1RXzk12PN7YSeQrR5Bc=" + }, + "kotlinx-coroutines-core-1.6.1.module": { + "sha1": "1cff9681cd7b0e3ac165bb19a47afd2745241b69", + "sha256": "sha256-ef5laan/+0CeUcZNX4xU0nxKo3NYhhVYmdKO0snfvEg=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-1.6.3.pom": { + "sha1": "11b3eb58dd940f573a72cc9854d778853351d7ef", + "sha256": "sha256-DLt5f7KU6HJCU49UF6fN9Y01/jiLFYYAuGILOF/kWh4=" + }, + "kotlinx-coroutines-core-1.6.3-all.jar": { + "sha1": "c1e9f41068d1112d760cf8f5092f0ed0111e0275", + "sha256": "sha256-Ob7rrEJsuQNNb0A1XPAnzVI8wDkpN1+QcWzaRC144H4=" + }, + "kotlinx-coroutines-core-1.6.3.jar": { + "sha1": "c17987358d0207357367e0eaa6e38f1e0af3cc6b", + "sha256": "sha256-8cOBxcWDPIeB+SE76pvu3eva0AIO4ru58p0fygDVG7A=" + }, + "kotlinx-coroutines-core-1.6.3.module": { + "sha1": "14344811912362af1505605bbdffade6dbfacfc4", + "sha256": "sha256-FAOXGfIQDZHgqyIPg09+XzV4qBAmE35OwWpcg+zRdAs=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-1.6.4.pom": { + "sha1": "6aa0f9ae49097dbd15e07cf73994e722528d2ce4", + "sha256": "sha256-E2tO24ekfhUlHwhC8xifoxl560q1Gs+0I5tL+NFtRYg=" + }, + "kotlinx-coroutines-core-1.6.4-all.jar": { + "sha1": "8533f177bf514eb1fa2190075fa7903b93ba9f8f", + "sha256": "sha256-riTYT9M8WquhVksWgULQ8hCw4lfgoAd+MmFqzFnmf7c=" + }, + "kotlinx-coroutines-core-1.6.4.jar": { + "sha1": "3107ff8b8130b4c17c0745c82b0ca1c4ff39aed2", + "sha256": "sha256-d4hR5zhRtQLoNmQ0vJ7Fg3FDGJD7ErieftvxcyliwDA=" + }, + "kotlinx-coroutines-core-1.6.4.module": { + "sha1": "b5cc1f448b9f9b916aaaad400c06dc349b76ed1b", + "sha256": "sha256-pu7UoYNViOfIT817BHX86aezREyHDrx5e4i6ZMz0V2s=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-core-1.7.3.pom": { + "sha1": "8404b56934d4a074e025ef4c4a4655b04a19c21a", + "sha256": "sha256-7W6wOYcXA14p8cHWCk4927iYWPPbnge1etdZ03Ta6Ck=" + }, + "kotlinx-coroutines-core-1.7.3.jar": { + "sha1": "e4e63122407750bcfae0f646bef4cd5df85e0a72", + "sha256": "sha256-+VIgla7cwqarMsdIQGHqaYNSxxvhOQrbQDtZqkijj9w=" + }, + "kotlinx-coroutines-core-1.7.3.module": { + "sha1": "55df6acabb05b9be7259feb4f89a5118cf4975e9", + "sha256": "sha256-f7FiOWWU7CjhtqRBG0V5SadnD14SAZF2d04f1rlHG78=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-play-services/1.6.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-play-services-1.6.4.pom": { + "sha1": "01b4a3b6ce28da4c498ea9ecb2080d7c3bc9b3e8", + "sha256": "sha256-UYqlb7d7yusyXnt8GMueBIp5MNvq/9WcbJGHPjmjhzw=" + }, + "kotlinx-coroutines-play-services-1.6.4.jar": { + "sha1": "e8e5e0dbe20eadc35a0eb112bcc1b2089768c906", + "sha256": "sha256-TuN4TDRlyquGrcxPk5VtVupmKqyC5cLIPKPB8EFf45c=" + }, + "kotlinx-coroutines-play-services-1.6.4.module": { + "sha1": "04cc57f65701d0842274e262825273f2b0b2d16d", + "sha256": "sha256-rt4rLLtps/lAx/Ox9IwGow8vsAmEsDxaLbDCwlMQtv0=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-coroutines-play-services/1.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-coroutines-play-services-1.7.3.pom": { + "sha1": "46f9f2624d11b89d6c060a7b77550619e90f6b9e", + "sha256": "sha256-l5OGtRKESYP9VUGIwr4OyzYXpTICKlQWUQHN460keHc=" + }, + "kotlinx-coroutines-play-services-1.7.3.jar": { + "sha1": "7087d47913cfb0062c9909dacbfc78fe44c5ecff", + "sha256": "sha256-ZNbqGk0VosCVDammH0nqfFEmjudMOx3dxGPmlVREGyQ=" + }, + "kotlinx-coroutines-play-services-1.7.3.module": { + "sha1": "0962d10f0c890a9e2cff7390a20aaeb01f3ef952", + "sha256": "sha256-k2+Y6/UlIZjX2ycnfixpFG7rt6LkZFlpizVKKUKNq4A=" + } + } + }, + + { + "path": "org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlinx-metadata-jvm-0.1.0.pom": { + "sha1": "b6b8510bd1307e5c859457e7775a864afcc81f80", + "sha256": "sha256-HcwBw7FrQuaeEYUSdoX72Gg3q3g0Z0GYUh6fiqh4z8k=" + }, + "kotlinx-metadata-jvm-0.1.0.jar": { + "sha1": "505481587ce23e1d8207734e496632df5c4e6f58", + "sha256": "sha256-l1O7Oe/vNZV8XBXfmjy3aaq/LN+nS0evy3dg5RRr47U=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/jvm/org.jetbrains.kotlin.jvm.gradle.plugin/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "org.jetbrains.kotlin.jvm.gradle.plugin-1.8.0.pom": { + "sha1": "a459d8b64406d886e8f0efc572ff170f43e7a0ec", + "sha256": "sha256-d6RTrkkBX0A9JeSlIZraLGMOk8k26swEvECPgH+sVis=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-android-extensions/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-android-extensions-1.6.20.pom": { + "sha1": "6a98431632fe7473b89efc3d6bd8796b0f68849f", + "sha256": "sha256-Jqf1qCk55OT0SEw4P43RtLnXSkGs6XFDKJYP3Nw3q6Y=" + }, + "kotlin-android-extensions-1.6.20.jar": { + "sha1": "95eaaca93565ed8e262f6b9e402d0382f28d0cf6", + "sha256": "sha256-0Wr8dviaiRc6UuqP3P1utmharFMx239d782N2+KDTro=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-android-extensions/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-android-extensions-1.8.0.pom": { + "sha1": "9d6ef1c94b631c683448e3b04b0c5e5a16cec166", + "sha256": "sha256-6VUIbaKam9xhI8NKfOSr/rKZ8qf7vE6Q+v0MePzY/ZQ=" + }, + "kotlin-android-extensions-1.8.0.jar": { + "sha1": "c53f898318b2228e3bb1e6d3af53fc8e0c22e318", + "sha256": "sha256-ggjE7eQs1MsYSEBBhZt2s2brWjJu6nsFqRROZEIXNc4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-android-extensions/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-android-extensions-1.9.0.pom": { + "sha1": "c08cc427bc37faf86bd41de0b64e8ac67899a06b", + "sha256": "sha256-WOHIg3JukUaVIDV95u0Xw/mSnI4IsRdKJ6OtbsDrzVI=" + }, + "kotlin-android-extensions-1.9.0.jar": { + "sha1": "ca23de2a64833d204f9af248e265274f3b8c0333", + "sha256": "sha256-IGdbuyp7aYKnFEUYpRi5qJZfkK6JuMmHiJgBIkSh6jM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-annotations-jvm/1.3.72", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-annotations-jvm-1.3.72.pom": { + "sha1": "c066f07dc017db3bf579a07dfff3dfe4222a68c8", + "sha256": "sha256-WNQu8MSoshb9TJcW0YNoAA32TQlPdO5kiBZa8LuBdFc=" + }, + "kotlin-annotations-jvm-1.3.72.jar": { + "sha1": "7dba6c57de526588d8080317bda0c14cd88c8055", + "sha256": "sha256-PuOlbdRR4z+DUuDSgDpVyhuRVtf4qOB1m/jhh/+Nsko=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-annotation-processing-gradle-1.6.20.pom": { + "sha1": "abcdf80a1e9feda72cb56f2ce6f4cad30eaf9d39", + "sha256": "sha256-YsBKPDQIz1AljRbgT2Kps6sGM5NJmBtbZkDmIPf14D8=" + }, + "kotlin-annotation-processing-gradle-1.6.20.jar": { + "sha1": "73fcf577f3a5416bd2feec9145ff51e8f2735ebf", + "sha256": "sha256-PoAQJjN5Ddo8X6p+ZjTZava7pNBYloGBhRBMlPDOX7s=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-annotation-processing-gradle-1.8.0.pom": { + "sha1": "a5d0ee4a6a768a376aaf7938e1534d119ffb6d14", + "sha256": "sha256-Qh1NN4HgrlaDtUwQQqjkVmLMdHzHwZGxucH/Qo4TCNE=" + }, + "kotlin-annotation-processing-gradle-1.8.0.jar": { + "sha1": "fa6b4c851584366968803fe879d995bcfe7aa8aa", + "sha256": "sha256-rsvL3qieFh86mEjh2DUc+Lyn+tZGba7PrwDyts/V2Bo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-build-common/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-build-common-1.6.20.pom": { + "sha1": "1153c5d6b88f567da7a344b62f8fd97397885363", + "sha256": "sha256-Rfeu8j9d9hbZPOwYEYOtdK5OCv0U+K36k32MfO4Qm2Q=" + }, + "kotlin-build-common-1.6.20.jar": { + "sha1": "7fec24d204906d7c9cbc695e94dd6cd11b26aec0", + "sha256": "sha256-WMkYl6x7anEK2h+ZtoJi2fKc6Yux+I/+7grz8k9qW3M=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-build-common/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-build-common-1.8.0.pom": { + "sha1": "06bec098b7853cef55dd67ba467c44adbafb23b0", + "sha256": "sha256-q7Crg9/VnXGm+QcE4KCKUK44wqfcq6cl62yFgRu+6UI=" + }, + "kotlin-build-common-1.8.0.jar": { + "sha1": "90035b6ebbdbbd3c09300d9ba084ce8f5f03b81a", + "sha256": "sha256-qDcm2J0o/M1ukjedDxDaCkEMmW41eAUT/Sl8M/zVQ/U=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-build-common/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-build-common-1.9.0.pom": { + "sha1": "ab861d4e7cbca289fa3d215daad0cbfb2a2cfb82", + "sha256": "sha256-6itlfmsETYgfDT/f/BPOKIwlQYxipbBL/64XeJDs6EU=" + }, + "kotlin-build-common-1.9.0.jar": { + "sha1": "68e075bff7dca0d2d444c19279afe6882f311a19", + "sha256": "sha256-txTWfNj1bTe4Pqj2XdysZAXFdix9n2EV81J0o5GxW2w=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-build-tools-api/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-build-tools-api-1.9.0.pom": { + "sha1": "ab0f33a948936b4ef2b51a8b82720fe5982184e5", + "sha256": "sha256-iIFxmCZbm8BrPzSTWyv2qpj0gA7OvBzOQREN70m3pCE=" + }, + "kotlin-build-tools-api-1.9.0.jar": { + "sha1": "5d33265af7283ca4d7b1f3115b83b54545f4cfab", + "sha256": "sha256-0ZyavegKnPQtJezJLLEUjtOhXu2yZy2qHkZubaWQ7sY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-build-tools-impl/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-build-tools-impl-1.9.0.pom": { + "sha1": "3d7403fd4d2f8914b46b250f6746da17c5a1ba47", + "sha256": "sha256-3Qrb3SO5XBP8N2JAqCoL/F1k37eBON7GJASX6QVtQIw=" + }, + "kotlin-build-tools-impl-1.9.0.jar": { + "sha1": "1ce2bdaf6dd80d4e7748058231059766e298a6de", + "sha256": "sha256-1yw3nxx1eZrAxm5sMVj2iGW9/tGlw1xepBwnihuAUBo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-compiler-embeddable/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-compiler-embeddable-1.6.20.pom": { + "sha1": "d9c175e55597c1a80d48ffa80b640ec2d9af5104", + "sha256": "sha256-35LK0uT7xFQ0pQudOfVKG0KKrdUogtDooxNt+s/qgkY=" + }, + "kotlin-compiler-embeddable-1.6.20.jar": { + "sha1": "18f66cca4301c12893ebe4ef47d65c3d9d2bff9b", + "sha256": "sha256-vmNPqq+1aBa2721YPlerM+TW5RgM3i9QXM99Rdxzjvg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-compiler-embeddable/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-compiler-embeddable-1.8.0.pom": { + "sha1": "fe5793922c0f307bc61b570481d0bd67fd15a45b", + "sha256": "sha256-MwLTtORBFJoLDahNYX99v/3FhkNgR8ZG6qtqcBIcJgo=" + }, + "kotlin-compiler-embeddable-1.8.0.jar": { + "sha1": "eb9118d4bcceaa2a94b2ae2a33a4ddba7c9a947f", + "sha256": "sha256-6bOlbbv98eAyjUcxt9fKeJzg8fJjNyrYjdjey9FgKFg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-compiler-embeddable/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-compiler-embeddable-1.9.0.pom": { + "sha1": "079ce81c461cfa0c3ad6b05b4a40b23bf821dbd2", + "sha256": "sha256-7jyHHO39bM9A0ESUQJbihlXeL5NF2ET+7GPMqcXkt8w=" + }, + "kotlin-compiler-embeddable-1.9.0.jar": { + "sha1": "2c8c3a7402becca891f12739b3e9fd2dc2adbd7a", + "sha256": "sha256-lTtQaR103SGf8/ZkRnDD5UnYfN/AphVPFgKmoKXf43M=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-compiler-runner/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-compiler-runner-1.6.20.pom": { + "sha1": "0f7b632751615a525b76cc0138b091b3c91fcb7e", + "sha256": "sha256-v/rhyoJhfFkphmLmP17I+BtZUpP4KIVpfkMURyRgWgE=" + }, + "kotlin-compiler-runner-1.6.20.jar": { + "sha1": "7b1a669cfd68156cf200e8279df97e39a0082237", + "sha256": "sha256-w2GQZYqjpYsnojZeczYyJRLYpP+BlibMXj7DWXrtBvI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-compiler-runner/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-compiler-runner-1.8.0.pom": { + "sha1": "13c07e3814551c362e0b50d898694f367aa78ff0", + "sha256": "sha256-ZYGJWB9VXKhlw/riJ4dumgAwuYGswJaNdBsZw5PuYSg=" + }, + "kotlin-compiler-runner-1.8.0.jar": { + "sha1": "8b3e2ba6225ff140efc1f9fa60b2c5ef095f37b0", + "sha256": "sha256-SBsovzpViFr+JZQ7zP/guD1FTrrWqGCfzkGPHpEj2oM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-compiler-runner/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-compiler-runner-1.9.0.pom": { + "sha1": "f2f333b81d78150bca741b75646a042f8a69be2c", + "sha256": "sha256-GIN9BlVntn5bzy4wTYWm4rbSRDz4En9AdnNKTo3yo2E=" + }, + "kotlin-compiler-runner-1.9.0.jar": { + "sha1": "4cf7c2cb6bfd82876e592697d3ad9bc835b21010", + "sha256": "sha256-BbWlXUlJFhpxT838ZKXwTYejZMm73IZBs7t6VVfkPqo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-daemon-client/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-daemon-client-1.6.20.pom": { + "sha1": "38862f833ac4738747a98dce9e092fbed45cc0db", + "sha256": "sha256-W7NKdK8/9UnmXFFbtgaKROxoEiKngHSsT4ueNxaIUH0=" + }, + "kotlin-daemon-client-1.6.20.jar": { + "sha1": "d3a6f83d7a6557f2142eee48e0270e2681d86b37", + "sha256": "sha256-munU03MkY0jwwmTVa32ep2EgJmtelHvdxRRdwUAz7Qs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-daemon-client/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-daemon-client-1.8.0.pom": { + "sha1": "076fa355d446bcc85572ca9f591a282cac7e692a", + "sha256": "sha256-/8ocSHLAGw3yOhdOXXe3F7MDh666v12PIlYBjRKWz9I=" + }, + "kotlin-daemon-client-1.8.0.jar": { + "sha1": "67f8cfc94b53e11080d36e44805348ae19d33cd3", + "sha256": "sha256-psWRIUYxAMiWSW8iKV94vdHAO3GdjOAdnPGcfRTEpcg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-daemon-client/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-daemon-client-1.9.0.pom": { + "sha1": "af448a4a8fdd5c0bff3d93fa78a2e495779e18a5", + "sha256": "sha256-sv50dBgjzM6JMXCWkg3LnOXBa460Z5jTn9jmdwPFidc=" + }, + "kotlin-daemon-client-1.9.0.jar": { + "sha1": "a7aff9bf77b5c882789cc495262def8948d93c5d", + "sha256": "sha256-WoyXfn3H8N4tka5Yva3CBBXe9M9dST5aGD+WxZeZAyA=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-daemon-embeddable/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-daemon-embeddable-1.6.20.pom": { + "sha1": "26dca256aa6455ed7eb82fb24afa39de1001ae5a", + "sha256": "sha256-it45q5KjWCUp1pHIWp/fzZZkbHZ0TrJgeZpzWqdZ/hQ=" + }, + "kotlin-daemon-embeddable-1.6.20.jar": { + "sha1": "d4cac343029f48f106c57f457384771165d7deb5", + "sha256": "sha256-/1Y6Sys8tsIVYp1tXGUq2Sw5OTE9BQc9P2/tCq5ng08=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-daemon-embeddable/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-daemon-embeddable-1.8.0.pom": { + "sha1": "8a66dfa9a230347fd6a078a53a4c146a72a02233", + "sha256": "sha256-CdRZ+f1UtD3GMUspcpUPZNiOBKb/uzfRb+jzsRxSssg=" + }, + "kotlin-daemon-embeddable-1.8.0.jar": { + "sha1": "d255216c551685dc3a7e852b53617f5f01aaaa5c", + "sha256": "sha256-UvMglz+jOaHCuPcaOK8UAukFJ7E8fVZOp5Fw/IHUv2Y=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-daemon-embeddable/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-daemon-embeddable-1.9.0.pom": { + "sha1": "216bc4d3f72fb679b1f79e73a730cac6e5cd3fa5", + "sha256": "sha256-OOMsu5GFenIGy/3cON1O4L5mX0CgEV/21NF348f4kq8=" + }, + "kotlin-daemon-embeddable-1.9.0.jar": { + "sha1": "186543f5e28cf8d9a0290fecc2cf34301c40a65c", + "sha256": "sha256-lDGCoTfs1A5ikFjaUtszbO6bZm+AmWtlWELyCmNz35o=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugins-bom/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugins-bom-1.9.0.pom": { + "sha1": "511e9094170ffec75cbb76b58cfe7f1a058dc046", + "sha256": "sha256-XTmtkDDf0zsLrCn3UAAJzmKHYulP5h8BF3rhNL24D6E=" + }, + "kotlin-gradle-plugins-bom-1.9.0.module": { + "sha1": "b6bc353c189d28e9e0419f0635df8121ae8c8cbf", + "sha256": "sha256-/SufabguQ8tQnwhBsYvzxd99q18eC+oDT8CypZnW4pY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-annotations/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-annotations-1.9.0.pom": { + "sha1": "b1b0737be6ae375ea12ea9e7cbc6c6f1a4832ec1", + "sha256": "sha256-qjXuWp0dpZMV92nH/tsshwBWD1PFaAupKwsniBpKQnE=" + }, + "kotlin-gradle-plugin-annotations-1.9.0.jar": { + "sha1": "42d1ea51a701505256d5bd54d9152819937835dc", + "sha256": "sha256-KiZbNbx8VYIUJukbajzU9LkWOKC8fIffz+TgX36IRzg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-api/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-api-1.6.20.pom": { + "sha1": "fd36d14e7eb3b3dd2cfeed283a8d998873077879", + "sha256": "sha256-HC/nnmJPwlvPNtJ2q4CpIjpIsq4UMNQYfGSULfX2zho=" + }, + "kotlin-gradle-plugin-api-1.6.20.jar": { + "sha1": "bc3f9737d53e0f3a38162801e1c1e344a52dbc20", + "sha256": "sha256-3UxQvCuSIP/1i+J8p4pIvx4SC+XEviFxCnFc7+mp31M=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-api/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-api-1.8.0.pom": { + "sha1": "444ec4947234fcfaea4b473c4a54e06a41eba28e", + "sha256": "sha256-V2pSpFvkKmKkYQkFLB2mbmsLfmnr1/+UMQxbctQ6PvI=" + }, + "kotlin-gradle-plugin-api-1.8.0-gradle70.jar": { + "sha1": "29f79a3bb109e05b07f1d2893074ab176a4276e0", + "sha256": "sha256-POxL8mo9ILplU39lotyHAMmP/H4DitnSBHtrTiIQOo8=" + }, + "kotlin-gradle-plugin-api-1.8.0-gradle71.jar": { + "sha1": "29f79a3bb109e05b07f1d2893074ab176a4276e0", + "sha256": "sha256-POxL8mo9ILplU39lotyHAMmP/H4DitnSBHtrTiIQOo8=" + }, + "kotlin-gradle-plugin-api-1.8.0-gradle75.jar": { + "sha1": "29f79a3bb109e05b07f1d2893074ab176a4276e0", + "sha256": "sha256-POxL8mo9ILplU39lotyHAMmP/H4DitnSBHtrTiIQOo8=" + }, + "kotlin-gradle-plugin-api-1.8.0-gradle76.jar": { + "sha1": "29f79a3bb109e05b07f1d2893074ab176a4276e0", + "sha256": "sha256-POxL8mo9ILplU39lotyHAMmP/H4DitnSBHtrTiIQOo8=" + }, + "kotlin-gradle-plugin-api-1.8.0.jar": { + "sha1": "29f79a3bb109e05b07f1d2893074ab176a4276e0", + "sha256": "sha256-POxL8mo9ILplU39lotyHAMmP/H4DitnSBHtrTiIQOo8=" + }, + "kotlin-gradle-plugin-api-1.8.0.module": { + "sha1": "ea8e46d9e43c46f49506812802144c9f9f2082a5", + "sha256": "sha256-p13UqvK20yCfVm7LLDsX+o920XOQqBq5IE3dichzxck=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-api/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-api-1.9.0.pom": { + "sha1": "7c80dda386f89cf10d0c3ba399176f5ad26cf057", + "sha256": "sha256-VPHdp1Zn7esbrIpY9KNsh4wPBJGGA2vJOnAyjNnNfaw=" + }, + "kotlin-gradle-plugin-api-1.9.0-gradle70.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0-gradle71.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0-gradle74.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0-gradle75.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0-gradle76.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0-gradle80.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0-gradle81.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0.jar": { + "sha1": "82a9928a60c442822fccaf08aa212528a85fbf25", + "sha256": "sha256-AhbssMeA+V47EBoOlc/DZ/LvVrBvLeqqqZJaDxF8INk=" + }, + "kotlin-gradle-plugin-api-1.9.0.module": { + "sha1": "cede46e2a2c9f2abeaa726fef80f85e451a6b5b3", + "sha256": "sha256-Gu1WWijZE0KsWkuhRsKFal67xjkoXFKVUhefr8dhTds=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-idea-proto-1.8.0.pom": { + "sha1": "53e2589493520fc663278f5e0c8cd81426f4b9c0", + "sha256": "sha256-jkcdHMLzltpjw7LcPT/Hlg4/MO5bnRwh5mk44Y7dFuQ=" + }, + "kotlin-gradle-plugin-idea-proto-1.8.0.jar": { + "sha1": "2f0dadae067048a92f7153db7af6613c3a3fb052", + "sha256": "sha256-ue5h+Lc9tM722wc2247VrwH4ZU7ElSF7/aHaxmZbito=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-idea-proto-1.9.0.pom": { + "sha1": "19b5a92d55669744290e83b5a7d643d1fbd83f65", + "sha256": "sha256-QeElaFOKOjQHBMlww6T2D/WAH/3OQlymjKLAtHIhclA=" + }, + "kotlin-gradle-plugin-idea-proto-1.9.0.jar": { + "sha1": "d7e3ab4f0b7f7966e446f516fddf331475319f53", + "sha256": "sha256-54mY6iK81fVtE5gszBJMnHM3vRUHVMvcivKhSNdslwA=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-idea/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-idea-1.8.0.pom": { + "sha1": "f83b043bc38ba8320ad0fcf3089ccbd683bd2792", + "sha256": "sha256-F6/rlAYYt9/YVuK+GGrdZKh/8CggMYhVethwoZnGApQ=" + }, + "kotlin-gradle-plugin-idea-1.8.0-test-fixtures.jar": { + "sha1": "0b1fb0b057a5439f97bbaf280fa1b3c1ede7d08b", + "sha256": "sha256-9EoF3oltyXib1bi0g1wjVNVGVEPn6/TTX1XUiDTulFc=" + }, + "kotlin-gradle-plugin-idea-1.8.0.jar": { + "sha1": "6998138dffe44597dc30d038d26dbb1fabde2dd2", + "sha256": "sha256-nNbqXkmX3ThT2UB73A0zKdnpnOfqm/kdIE4esNJDlQc=" + }, + "kotlin-gradle-plugin-idea-1.8.0.module": { + "sha1": "632cbb1aa519fbe1b7802d0aa315575bf7d18ada", + "sha256": "sha256-OBuuirNUtc+xt7mU8nHLF4sVVN1AXRDApH4v2KC+odI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-idea/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-idea-1.9.0.pom": { + "sha1": "e3c307cf7ee2c63e9535edb403466dc49aa5d5d9", + "sha256": "sha256-W/gGnkUbY0wdxLRpLpKO9pEanrLtk3TeIPRC3AloBo0=" + }, + "kotlin-gradle-plugin-idea-1.9.0-test-fixtures.jar": { + "sha1": "af6020bc0aa902bb438e69aa0eca58438111136b", + "sha256": "sha256-MAj3geetkPywHc2d6pbg9ky48bMYT5T4p9jNHeGlsfI=" + }, + "kotlin-gradle-plugin-idea-1.9.0.jar": { + "sha1": "5538265e4961666c769825f4543ab445cbc45bdc", + "sha256": "sha256-C7JszuNyr7m1hPKFfofMLz9TK2Kk9/pFdVjzr8Or6ZU=" + }, + "kotlin-gradle-plugin-idea-1.9.0.module": { + "sha1": "186d2bb5113962059551805961d9c80a5b06a80c", + "sha256": "sha256-pVg82bC0AS6VWtlhn7kBe1saosJRsFltr1M0kExcpls=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-model/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-model-1.6.20.pom": { + "sha1": "f39007734d937e7e15b3a0ec7a822af522a96236", + "sha256": "sha256-XL2OU7wYMu/GelPIXv9gcsZEO2It5tcV1t1b5BF9fjo=" + }, + "kotlin-gradle-plugin-model-1.6.20.jar": { + "sha1": "7d0f04964df9a8af811b3095492907a7c432037d", + "sha256": "sha256-rdINCAUc4sGnywyLBRP4tFgOJEN6yjnMYUSyPg3FRwk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-model/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-model-1.8.0.pom": { + "sha1": "1abb7959b03996bc7e5225c665d1a0557f814ac7", + "sha256": "sha256-hsjoGrXDGbCzDQYTtBwCSCRahjqgIJqAvUmHCWW7fp8=" + }, + "kotlin-gradle-plugin-model-1.8.0-gradle70.jar": { + "sha1": "9da2fce7af0a94348001ee8dd20a82167f5de284", + "sha256": "sha256-z9oJW2++VYBT8GuDrsc/CGV7butUBdFtSSquHo7FAZc=" + }, + "kotlin-gradle-plugin-model-1.8.0-gradle71.jar": { + "sha1": "9da2fce7af0a94348001ee8dd20a82167f5de284", + "sha256": "sha256-z9oJW2++VYBT8GuDrsc/CGV7butUBdFtSSquHo7FAZc=" + }, + "kotlin-gradle-plugin-model-1.8.0-gradle75.jar": { + "sha1": "9da2fce7af0a94348001ee8dd20a82167f5de284", + "sha256": "sha256-z9oJW2++VYBT8GuDrsc/CGV7butUBdFtSSquHo7FAZc=" + }, + "kotlin-gradle-plugin-model-1.8.0-gradle76.jar": { + "sha1": "9da2fce7af0a94348001ee8dd20a82167f5de284", + "sha256": "sha256-z9oJW2++VYBT8GuDrsc/CGV7butUBdFtSSquHo7FAZc=" + }, + "kotlin-gradle-plugin-model-1.8.0.jar": { + "sha1": "9da2fce7af0a94348001ee8dd20a82167f5de284", + "sha256": "sha256-z9oJW2++VYBT8GuDrsc/CGV7butUBdFtSSquHo7FAZc=" + }, + "kotlin-gradle-plugin-model-1.8.0.module": { + "sha1": "3fd6d027ad16742422ad540184e09d10a6875058", + "sha256": "sha256-7k4H4jw9EI6ysqD1t8pRweW6kSAojRmA9JSV1k1sGVw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin-model/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-model-1.9.0.pom": { + "sha1": "b9b395f72a747e294548584cb93b9e48d2d8dad7", + "sha256": "sha256-QcXjVzTqEZA+IQr5i3eHB08mQab0C0X5vpwRAAga5Y0=" + }, + "kotlin-gradle-plugin-model-1.9.0-gradle70.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0-gradle71.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0-gradle74.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0-gradle75.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0-gradle76.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0-gradle80.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0-gradle81.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0.jar": { + "sha1": "d64789139fb89d78f2128d6b5be8a164d8669a72", + "sha256": "sha256-/wWqtoizSnmSp+NfncTewlhD8Tqelwrv5W3T4SKghBU=" + }, + "kotlin-gradle-plugin-model-1.9.0.module": { + "sha1": "cc984614ad443557e13d3a07c6dbcd8ce214eec4", + "sha256": "sha256-ZvS5FEcM80bgXQP4otWenHRvpu3PZMcS8XagiBqsPkk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-1.6.20.pom": { + "sha1": "17c65455c1585e51a00fbe01e0725b9d1d817732", + "sha256": "sha256-56p+ttsbyV9x44hCFEYzgY2e5RtSnR4FZ/tux6G/VG0=" + }, + "kotlin-gradle-plugin-1.6.20.jar": { + "sha1": "f05dfcf414a9eea339cf52751abf8b9b1b6bc691", + "sha256": "sha256-U/t3cK2YgOKJtwHZ+3KcgkxZ9E8rA0nuaqyUOAxN4Sk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-1.8.0.pom": { + "sha1": "de3161a42545561fe9891d1632ce25936b0a0fd1", + "sha256": "sha256-AxreBO7rFO8LQFcZ3gz2n3fSTHbXmmDK07lm/wIXbV8=" + }, + "kotlin-gradle-plugin-1.8.0-gradle70.jar": { + "sha1": "1d9c0e5097029ad29ac54fa2b98182ffd306194a", + "sha256": "sha256-RfrgRuokQa2Gt+NdPoyxR7R8VrCvkZ/BWKeaU6qxuko=" + }, + "kotlin-gradle-plugin-1.8.0-gradle71.jar": { + "sha1": "63458c476699a23cb29a286c2d9daf8da6a25f79", + "sha256": "sha256-Bd8cRxVppaFJ52RD89cJwa4e319N8eS9Btb2oNaLcW8=" + }, + "kotlin-gradle-plugin-1.8.0-gradle75.jar": { + "sha1": "bb5544ff410fbbf37e6704cc10178ff41e2e9704", + "sha256": "sha256-jwDEfD9iAWGs9Y1xEtFW/vvXPGYt07tXPqW8gdFRFQQ=" + }, + "kotlin-gradle-plugin-1.8.0-gradle76.jar": { + "sha1": "d79a079634e8d3bd92c594b224f4de2086bb4a10", + "sha256": "sha256-ZSYMGBA9x9sudv+rvqd7aniM7Dcpu7TeJmSJbUaw65M=" + }, + "kotlin-gradle-plugin-1.8.0.jar": { + "sha1": "2b9b0db62b83259d6c04ef6d333e9d24def760e2", + "sha256": "sha256-qcib2XS9lGQrGUzpDRKtI2LnDL3M+237jxd2Eyrcias=" + }, + "kotlin-gradle-plugin-1.8.0.module": { + "sha1": "3320f6fa2cd36c6a74ee3800943ee6c352701aa0", + "sha256": "sha256-Kv6LT4KQ6ofshcK83GvpFn2m1wWQo4Vmf21lMPZb4ZQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-gradle-plugin/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-gradle-plugin-1.9.0.pom": { + "sha1": "75f5520fa218e0833a1d4332bc10cf325925f63f", + "sha256": "sha256-RSdrCf+7uANPvQRwj5D34xtw1orgI7LPs8EdiG532DY=" + }, + "kotlin-gradle-plugin-1.9.0-gradle70.jar": { + "sha1": "70f96081d51820be6e998ae76591ca2485298880", + "sha256": "sha256-tpVPc4pbBeMHcvq9tFnIHVjfGz2oBqng7qcQ9Oj1Afw=" + }, + "kotlin-gradle-plugin-1.9.0-gradle71.jar": { + "sha1": "a237533df09406429a90e4cf6eddbd665fb879ef", + "sha256": "sha256-eb2kXiIyG1eXss3mhJ9KwFUgfVcAUxsEI+aMURdlers=" + }, + "kotlin-gradle-plugin-1.9.0-gradle74.jar": { + "sha1": "8ca3abcf0134fac694a3d78e78cc879ac37efc40", + "sha256": "sha256-p0Pj9keSxLrCRuE8GYSIIGskZnHvzXB7NQw+IsEWW50=" + }, + "kotlin-gradle-plugin-1.9.0-gradle75.jar": { + "sha1": "fa282ffe215f034c063a361d338f58b14479b336", + "sha256": "sha256-y9fMKOqRe48m9sVeYWVSYOGtQ3ZNM1edrj91pJMa/vI=" + }, + "kotlin-gradle-plugin-1.9.0-gradle76.jar": { + "sha1": "5e15ae8365f8750303ad66e169cb8fb25e20f408", + "sha256": "sha256-k6DXXAMnUB4h2z8pprUoju8uIjFyan/esXYMwNmi6/I=" + }, + "kotlin-gradle-plugin-1.9.0-gradle80.jar": { + "sha1": "3e9dd06e2aabd397923535a42118bc861bcff9eb", + "sha256": "sha256-JnLbEUAqZqYyLpzT9nV2rbtyq+jkdNkStqrzP5QGuPY=" + }, + "kotlin-gradle-plugin-1.9.0-gradle81.jar": { + "sha1": "8cfc2ba810d67ffb9a931d3785163acb5ff859fd", + "sha256": "sha256-2UOg4ntHRuAfg0BaSfUmoi9HGx2x1GOdb84m1xHv+/4=" + }, + "kotlin-gradle-plugin-1.9.0.jar": { + "sha1": "a031b8e97a70eb71b3d8a9298a82d153fc40c938", + "sha256": "sha256-yxGBud8e8LclfOPYc6mWy79G3K6qnD+OxIBWE3V5BxU=" + }, + "kotlin-gradle-plugin-1.9.0.module": { + "sha1": "f15d8f1e4dff619035904e448de88cbbde259d28", + "sha256": "sha256-/mOWRe2PvcW8Et588lRopNsx52O6ea26ogahTeYP0xQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-klib-commonizer-api/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-klib-commonizer-api-1.6.20.pom": { + "sha1": "3fa75589b49fdfd99c44abeb7dadff43d70f587d", + "sha256": "sha256-cHiODZT9y9mUe4akSLOA7sVANH8aVaMiBom2hCzYwNY=" + }, + "kotlin-klib-commonizer-api-1.6.20.jar": { + "sha1": "62300f688ef89a5c1a06c4099f3becdb225d3c4c", + "sha256": "sha256-YKvEkZ60BJwDjMJ0DonKh+aSJwiorXEd9ePf9zZSlBQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-klib-commonizer-api/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-klib-commonizer-api-1.8.0.pom": { + "sha1": "e2addc62aa93f00ea092341d75c0d4f9c6ffc704", + "sha256": "sha256-VFBRyTzyhhjIKzbweySv4DZO4BK09qO2E/5zg453uO0=" + }, + "kotlin-klib-commonizer-api-1.8.0.jar": { + "sha1": "e5974a8e0109dd31d7c16c8901608281a52b6216", + "sha256": "sha256-soQq+7LpB77fYb4RqrWoLFadJzwkjvSFeBCneh7ta7o=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-klib-commonizer-api/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-klib-commonizer-api-1.9.0.pom": { + "sha1": "dc9b1e38fb03d23112825bc6ab3ab8144ef8d42a", + "sha256": "sha256-hE+SCI/0j4v64lYh4yStw1d5CM9lD0/bJWlhC4+vaz4=" + }, + "kotlin-klib-commonizer-api-1.9.0.jar": { + "sha1": "35782a8198131f9ee6eec26d7bf60b1e0a508bc5", + "sha256": "sha256-hd4ulysLtvRSjB90j0PEhmMUX8hTnsqWwT0bwNo/k10=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-klib-commonizer-embeddable/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-klib-commonizer-embeddable-1.8.0.pom": { + "sha1": "c371eec82a4902a0e331367f4a676321c2fc7681", + "sha256": "sha256-pQW8nnDaF1kArHBBV+PZddfc4DN+FHT96lOuLTCPS6g=" + }, + "kotlin-klib-commonizer-embeddable-1.8.0.jar": { + "sha1": "402a7c2a150104128a610ae07a565ec74a01d967", + "sha256": "sha256-jefizisbVlo4BPczwPJYmy7Wl8iTMch7Bfd73qBoGJ8=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-klib-commonizer-embeddable/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-klib-commonizer-embeddable-1.9.0.pom": { + "sha1": "210d03d9e5afa2938c9a6d20ce164c59277dc437", + "sha256": "sha256-L9STJpNwf9ujj80diJL7TdPPAIC5LxJSX8cF42G2eTs=" + }, + "kotlin-klib-commonizer-embeddable-1.9.0.jar": { + "sha1": "bae135d2d5ed2ae9c2c9b7f8300dbcb345ef8bc7", + "sha256": "sha256-zdYmTXJPcRKakwi+JEAsqMccKrICywO2hEmdqXrdS4Q=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-native-utils/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-native-utils-1.6.20.pom": { + "sha1": "bd2a6eaf894cf4f1404b83f20ed34ebc1dda8f1f", + "sha256": "sha256-vV2FDImKU4apF7dlxNXsKP1ckoevLyZC+HBiYYCYQCw=" + }, + "kotlin-native-utils-1.6.20.jar": { + "sha1": "5328907f59bbbd192589a3952fb897405527fcc3", + "sha256": "sha256-tYtvATOp6x9dIVQYkI2UUiXbJ/59nNWZe7b71hyZjR4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-native-utils/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-native-utils-1.8.0.pom": { + "sha1": "e02735a5b0c3cb11b0ad223b023eb4440623801d", + "sha256": "sha256-Wd+yxV5iVaOcQeCPzJZE2c+QkuocHaWhhP2+ukxZj48=" + }, + "kotlin-native-utils-1.8.0.jar": { + "sha1": "73361435f42e1fecbd81f686ccaa8aa1c498b941", + "sha256": "sha256-WP6m6mmUKe5CUyiGc8xAOBqkmywvMVO/OTPgp2lTDyM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-native-utils/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-native-utils-1.9.0.pom": { + "sha1": "80b8f19f18a627c23ed18cf2a44ef7eabc84da96", + "sha256": "sha256-Kazy3vkmve3Z6yBOx3Tnhq3Ek0EGqSlJAK81FVbn52o=" + }, + "kotlin-native-utils-1.9.0.jar": { + "sha1": "13264bd5949f9e68c5d53a6d394b800b725dc577", + "sha256": "sha256-D0gF93S/tcG20UXf5g09ILGu4ACJbOjs8+7/hM5EAjs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-project-model/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-project-model-1.6.20.pom": { + "sha1": "6a7d9c187d562f209b15037026e78dfcc6683b08", + "sha256": "sha256-whwzTZ6W8dh80dEyk31WO5Xqn8lt6IN+yc2i8NBfHRk=" + }, + "kotlin-project-model-1.6.20.jar": { + "sha1": "86e65910d00934b33e845f03540d65b9f34fc650", + "sha256": "sha256-XLq665gfD7YnHOVTuHmNV1N2P2k/V8rYjYMluanTBFk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-project-model/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-project-model-1.8.0.pom": { + "sha1": "9b731271ca0f9538258e16e6b4574ea96f505061", + "sha256": "sha256-i7tqBG5H3Teza6f4Ve87FVXCkbhKk/986xcIkBG9FKs=" + }, + "kotlin-project-model-1.8.0.jar": { + "sha1": "2b8d20018168339835a79512f46f3251ff09caf5", + "sha256": "sha256-7bIk03BDZgSqdhIkfT+SBOLxhl4VYYpoP9JqYe8vcgo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-project-model/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-project-model-1.9.0.pom": { + "sha1": "fe67d9afc6940fe0019e18fca37a37c58976536b", + "sha256": "sha256-6sHGTGAMYAJ7jmjfrTogPEFbn6IkWB1oz6Xnc+hWa3g=" + }, + "kotlin-project-model-1.9.0.jar": { + "sha1": "7c6006a523220e0a82f4954652c3140f7353e85a", + "sha256": "sha256-rgk26bTGkh8D73eTYcrLu3XtKyIQR5+S9l8cushuNAw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.1.3-2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.1.3-2.pom": { + "sha1": "e9bd6a29f4ec38157e3c2d11db6426668c21fc9e", + "sha256": "sha256-pHyVDHVXR8owk6BenshdDi8zy9IkkJ9XH+dXeWOk15k=" + }, + "kotlin-reflect-1.1.3-2.jar": { + "sha1": "2104f139db2a2d230c529b004c34a0993c4c2f19", + "sha256": "sha256-DuX2kcu+22mWm0g3TSdBL4IZWa9dLf+6vEJLYHzbvfY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.2.0.pom": { + "sha1": "2db5145a230bda2fc15d38707f202315efdf6a2f", + "sha256": "sha256-81IV2hvtaePWBzXXYiHXASzRpbwKMPb3QhcZraRjtN0=" + }, + "kotlin-reflect-1.2.0.jar": { + "sha1": "4bbda3b5425aa38a9f6960468a29c5ef3e8a28c9", + "sha256": "sha256-T0iocrrW5NnAU/StYQ0R5AEq1+WNwZoD3V64EfNgad0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.3.50", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.3.50.pom": { + "sha1": "45a559389f73d04351da2ae2986d40078e4a7ab3", + "sha256": "sha256-h0UYHlo+C6/v1GMJxrgQ33JT83n+uYUTHq+NTZwwJjU=" + }, + "kotlin-reflect-1.3.50.jar": { + "sha1": "b499f22fd7c3e9c2e5b6c4005221fa47fc7f9a7a", + "sha256": "sha256-ZFgxmepaVK79G9FZUoiSX3hCJu5WLR3SeQEcYHWz16Q=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.3.72", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.3.72.pom": { + "sha1": "9b5a835fdc0002f6d825600c771bbbea2cc1a31f", + "sha256": "sha256-YWU8z66MqkID4me0eYIb2Q+qs/73RKerD92fYRUPlww=" + }, + "kotlin-reflect-1.3.72.jar": { + "sha1": "86613e1a669a701b0c660bfd2af4f82a7ae11fca", + "sha256": "sha256-oYjZNn3hxO6UedtjCYXAWXsgcJyDFhsUMNJO2yfjjEA=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.4.32", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.4.32.pom": { + "sha1": "cd7bbe9e8aea8d8f0471373205de8a0871e949db", + "sha256": "sha256-kJ0LijJlaMTbNB8htfDiIcdcACiWpOo7FwqloVaaDlQ=" + }, + "kotlin-reflect-1.4.32.jar": { + "sha1": "ce852b166d97f0f1991b5130c2bb02e2ef6c554e", + "sha256": "sha256-2/GenNqpw8Fw8/b2zjki8438HX+hyrW3wjoZ2ote7Fs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.5.31", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.5.31.pom": { + "sha1": "de2931195ce691e75ddbf4ec2ddff41a0afcbd8a", + "sha256": "sha256-VitLqu6sfHJYlKEF/FHNPWfgolnnEUDVVUJsMycLMBQ=" + }, + "kotlin-reflect-1.5.31.jar": { + "sha1": "1523fcd842a47da0820cea772b19c51056fec8a9", + "sha256": "sha256-bg9UkOa5ZJ3dJnBTTk06A70oPDNYuO710TBP1filpPs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.6.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.6.10.pom": { + "sha1": "535560dfd5b3dba98f57b12f3e921c7a83d0aafe", + "sha256": "sha256-V5BVJCdKAK4CiqzMJyg/a8WSWpNKBGwcxdBsjuTW1ak=" + }, + "kotlin-reflect-1.6.10.jar": { + "sha1": "1cbe9c92c12a94eea200d23c2bbaedaf3daf5132", + "sha256": "sha256-MnesECrheq0QpVq+x1/1aWyNEJeQOWQ0tJbnUIeFQgM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.6.20.pom": { + "sha1": "e39dbba975c2de34882ddbd4c3ac721837cdeba3", + "sha256": "sha256-f6vj+RTSkmpKAB89N4SeoT6uyh2aPjUooJi3HcWunZI=" + }, + "kotlin-reflect-1.6.20.jar": { + "sha1": "669965154723f1a712e8677af0cc5470dfdab81c", + "sha256": "sha256-I0tgvSxJs5GsVQr+5MqakuSF2MWuX6qYza9/66F5QEI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.7.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.7.10.pom": { + "sha1": "856849d5fd882b3b8ff0fd2cf6378394eeef6b4a", + "sha256": "sha256-bD+HpozQIkgbUBWlwhtu0BSmc0jTDhA73pcNhP4yjso=" + }, + "kotlin-reflect-1.7.10.jar": { + "sha1": "165e600dfea6185cf5efa700756294cc4904dbeb", + "sha256": "sha256-GHxeWliKbtGMOkG1TfE4pZRBIb2zlr4cP6Sr7mc5eVU=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.8.20-RC2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.8.20-RC2.pom": { + "sha1": "ffc69cce4adb46e7cc1ec08bdc221431e502ae5b", + "sha256": "sha256-ffGl2JDIGca2g+gzCq7YZWFxxKCbjjYa8YiLie/IfRw=" + }, + "kotlin-reflect-1.8.20-RC2.jar": { + "sha1": "23853310e93f1c418a827be6fabe6f903addd231", + "sha256": "sha256-aI3jTuMKC8/9n8ze+g9MKK8Vi89jnSIVAsPqfDjmoKA=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-1.9.0.pom": { + "sha1": "eb198069ed7e9e8458d900c47325475ec581507a", + "sha256": "sha256-hXFWVnbRiXIEcTBs7ppV2RbAgDgvNabaaBk6x7EvTNs=" + }, + "kotlin-reflect-1.9.0.jar": { + "sha1": "2891f552979d4bf4d4ec516acb9df769fb62dbe9", + "sha256": "sha256-IHAVm+UU6o6jziyLHueZPm/2CpNWWimfIAYpqn339YE=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-reflect/2.1.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-reflect-2.1.20.pom": { + "sha1": "4eab48232a386afc0330799a6a6bdf2741feddc9", + "sha256": "sha256-XgIIh67A2JqcFo79ka7WKGVVOHf5MD8UXby50jumDcc=" + }, + "kotlin-reflect-2.1.20.jar": { + "sha1": "3c1003045c4f2a72f987a147abac8e7058be1183", + "sha256": "sha256-QZd48reoJrrW7iyh5L3r5jmrMG0pBPo+8qMI7PqrOBE=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-common/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-common-1.6.20.pom": { + "sha1": "107c46d47de88ed3837cc8d93ed8ddb1695cb289", + "sha256": "sha256-eUBj43Jwgdp91ScaH4BTrJmv80l23FKT0c8eDVkmqyY=" + }, + "kotlin-scripting-common-1.6.20.jar": { + "sha1": "0c715651864e5e186405f60e034af3b65d0eebef", + "sha256": "sha256-D/SmaEr86fMJhTyKDjs9LfzeoEKoPB/JMw6SDC8UL1o=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-common/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-common-1.8.0.pom": { + "sha1": "92823b47bdc926dbcd19407bea8125b98699fd20", + "sha256": "sha256-JUWa2UE1J0pu6cs7noNAL2eG20s4ns4Fj4ooNo1/j4g=" + }, + "kotlin-scripting-common-1.8.0.jar": { + "sha1": "ffeb4c0c2b3978876b993d6b55646e6b567077a8", + "sha256": "sha256-fO76QG5tCR1niTC4Sxa6BfjMXkqDkc+m+T0klef/SPk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-common/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-common-1.9.0.pom": { + "sha1": "326fa3ca7bb1705794879cbae1a9467b7d7aa09e", + "sha256": "sha256-C/MG5PRe6xrDAk4pnUybcL4J7OmsAOcLL/mLPtQA8wc=" + }, + "kotlin-scripting-common-1.9.0.jar": { + "sha1": "ea019fa9e3885795a81df270775e5609f7115307", + "sha256": "sha256-KFxBwO7tWaDn3LGIuJ+BVBR/vLvVNUcc9piWzNonP4Q=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-compiler-embeddable/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-compiler-embeddable-1.6.20.pom": { + "sha1": "ae1c1150ae4d4c21c419deb300637e698c68a678", + "sha256": "sha256-T9qQ9mpBliNKpHJeCLETpSis4AZTd0YtJDaETvtr+LY=" + }, + "kotlin-scripting-compiler-embeddable-1.6.20.jar": { + "sha1": "b6b54ee925970258cbb708d6dc98db58db728dce", + "sha256": "sha256-of0a25gMJpZMg4XT70pdX1+K76k/D0BvTBLE0incMoU=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-compiler-embeddable/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-compiler-embeddable-1.8.0.pom": { + "sha1": "04fcc1a6e8a1bd13723c5b44ea2666b9372c24ca", + "sha256": "sha256-yqhB2ZA5MdYMtGwNObaBAAvWo+pAT+Fum5opo/HW6l0=" + }, + "kotlin-scripting-compiler-embeddable-1.8.0.jar": { + "sha1": "e6b1ab0def6f13dc49d53594f31ed6452b653282", + "sha256": "sha256-AfYwXEm4VFS6XiBAYGJhepfPbh8v9JdtbwhlamaCknk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-compiler-embeddable/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-compiler-embeddable-1.9.0.pom": { + "sha1": "cba5705bf5fb615d2b72195faa25159e83e3a264", + "sha256": "sha256-UJ5ikUwwvT43GcDgLk5Df8vxkRa4ouZYSPbUbFTsXJ4=" + }, + "kotlin-scripting-compiler-embeddable-1.9.0.jar": { + "sha1": "1708703c0683b8d2a0a76048544480b94d3eb458", + "sha256": "sha256-Rp3RIGiVipevzhGIoBRFVM3tZ95aGqC/XKUwOfKdizY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-compiler-impl-embeddable/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-compiler-impl-embeddable-1.6.20.pom": { + "sha1": "eb4f515547404d2f0259b2e00562f0856887724f", + "sha256": "sha256-yflaMywDbGme/eVtphzL6UF3I1mAWTZkM8zea9iTWJg=" + }, + "kotlin-scripting-compiler-impl-embeddable-1.6.20.jar": { + "sha1": "6725f3ede2224227b1ce9e7b7086e264271f90c9", + "sha256": "sha256-HO5An2I06buxuU7ArWfYmAGdL7LM9TrxT9Xdy36de6U=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-compiler-impl-embeddable/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-compiler-impl-embeddable-1.8.0.pom": { + "sha1": "2d93a5fb55e0efc6c6380bab659d8ce6ffd1c9a3", + "sha256": "sha256-/xy614/wsgcbSSeObV/9bGaMZtRnsmS6IscMeJGM+Y0=" + }, + "kotlin-scripting-compiler-impl-embeddable-1.8.0.jar": { + "sha1": "3f07d79727996675a2243a4c11045a6615ec228c", + "sha256": "sha256-SSwAiFnOtyF4tfhbV0QKYBhsHP49j3ENHnoso/fcjkg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-compiler-impl-embeddable/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-compiler-impl-embeddable-1.9.0.pom": { + "sha1": "5459191b6fda205abb23fb4a767bb0f86e8cc12e", + "sha256": "sha256-zaYp8KgbqcAKM95QJ/D4/7+fC45E5BlwNHdmu4WLTmM=" + }, + "kotlin-scripting-compiler-impl-embeddable-1.9.0.jar": { + "sha1": "c825693be78dfcbc351a53856d75f4d11bb0e274", + "sha256": "sha256-ZcUlbiyPvdpCbAMobmomgmfXEu+V7C05yVKSlXN4sE0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-jvm/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-jvm-1.6.20.pom": { + "sha1": "0c7d2364320f419826795c2c74f8dfb1b47fe20b", + "sha256": "sha256-DcCkaT0jF9QjrGuXgUZaNEvYIy2/Xk2azMptsx6lo08=" + }, + "kotlin-scripting-jvm-1.6.20.jar": { + "sha1": "3d6e1b15fb9db1455136c932ade7d9df298c9274", + "sha256": "sha256-tTogzJUmRZqPsNnCPobeMWb41lwcZmkfrVIXSnX9XQk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-jvm/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-jvm-1.8.0.pom": { + "sha1": "92a155e3a09bb59538fd9d55a0ed7f718a643c2d", + "sha256": "sha256-PR1UA3TIcXpEwm2HBIQu43r+lyP8v1mpTJ3ki7OxlAQ=" + }, + "kotlin-scripting-jvm-1.8.0.jar": { + "sha1": "1f7345b3fc46786c1f36a4ba0728a95990d05929", + "sha256": "sha256-0tkW1wYwJDjSm4B9OR5lx6Sy6i+ndPLQYk8ReUYTJew=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-scripting-jvm/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-scripting-jvm-1.9.0.pom": { + "sha1": "178d3c678207d53a68cab7ce61dd89867bacdc6f", + "sha256": "sha256-Ymj4BVjTX9qSRruneTKWkrlNWJWWVQfL6KhIkSxQ5yo=" + }, + "kotlin-scripting-jvm-1.9.0.jar": { + "sha1": "e2e6a0bd37a72cded2fec7882199e1bdc4e8bcb4", + "sha256": "sha256-Zp3SvGBBjgdRU/2K2NllvlaetlwvMIOIyxEDud8TUVk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-script-runtime/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-script-runtime-1.6.20.pom": { + "sha1": "6a19659ddba700e7fbef80a6d27a82d5c2bcfca2", + "sha256": "sha256-KPrrMAuMHysTB+q0e0Y/gsG1N6Ookx+y2W/uT4J/sF0=" + }, + "kotlin-script-runtime-1.6.20.jar": { + "sha1": "a115c367e0fd9c8909293b208689afca0c88a6f1", + "sha256": "sha256-P+5c6lREnhjOWncMPEhJ8PhV2LBh99C9KgMhEJIsAgY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-script-runtime/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-script-runtime-1.8.0.pom": { + "sha1": "b70e5e777149cf0589692d22338d63e1c942be73", + "sha256": "sha256-xj8HolO42tYg8bCCL1/BUtApmyc2ApGAfsntn2b5i80=" + }, + "kotlin-script-runtime-1.8.0.jar": { + "sha1": "0b2f7e760d283cb339974851c222a4c1d48f2d3d", + "sha256": "sha256-4NjdbOvQkfmf/qzqoeVMr2yl6U8XU73BO4JRKJRKyew=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-script-runtime/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-script-runtime-1.9.0.pom": { + "sha1": "a52322c274112401ded9622eb8d7eb2570fae853", + "sha256": "sha256-FNwHhdGUFP7sEgI91z64DIPQeIC3PlS48IqHj0I9seY=" + }, + "kotlin-script-runtime-1.9.0.jar": { + "sha1": "4b3102cbbb7e1b58d9d1adf89563f00069ffa7d1", + "sha256": "sha256-2E0MykEFD6wqlS0RBEc8Mpr/BMULNxMuV1SiCYCa3Z8=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.2.71", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.2.71.pom": { + "sha1": "bf37a8a32e78a78734cc86154f4e1de85b122b93", + "sha256": "sha256-qMPtWJIDK56TlVJKTaKVOXXJlii5Mt8zNrJdVG58ZpM=" + }, + "kotlin-stdlib-common-1.2.71.jar": { + "sha1": "ba18ca1aa0e40eb6f1865b324af2f4cbb691c1ec", + "sha256": "sha256-Y5mWh/8vzopZLdGA/7v48dIcJrQETFXNx0/zzzs88yg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.3.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.3.0.pom": { + "sha1": "8aa25749d6a085239627a0a18f0502c19a5560d0", + "sha256": "sha256-fG6HCpjsigAclx/hpf5K5znNvakTqomrxqt01g45rps=" + }, + "kotlin-stdlib-common-1.3.0.jar": { + "sha1": "84a2e0288dc17cd64d692eb1e5e0de8cd5ff0846", + "sha256": "sha256-SxYe9hnu4NGkmxxPDEqORvTjQlc+/Y4BBqdl9HR1/jk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.3.31", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.3.31.pom": { + "sha1": "5cd6de0110a706c04138af5daff820031a3861f8", + "sha256": "sha256-Q/EYujqnKlMdZtZM3UIj+mErf6xXAwzpFI/7SlXMOvw=" + }, + "kotlin-stdlib-common-1.3.31.jar": { + "sha1": "20c34a04ea25cb1ef0139598bd67c764562cb170", + "sha256": "sha256-1unFTB5sTfIb6Tld5VhmVUTGvcj4B26nUY8In4LNNPw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.3.41", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.3.41.pom": { + "sha1": "3ebd033e4443e512b2d6ab29ba85dc03b68b4d3b", + "sha256": "sha256-0uU3zR93BSdiQtjMkDoChrbchrQbbnKOln73GQIvLhU=" + }, + "kotlin-stdlib-common-1.3.41.jar": { + "sha1": "2ecf4aa059427d7186312fd1736afedf7972e7f7", + "sha256": "sha256-bJHeoX19zl8LVQw94zBXZ+X7RiR7bR637KDKH+GEWN4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.3.50", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.3.50.pom": { + "sha1": "cde9d3a23570b97ee3727865a0dec0ac52a61cf7", + "sha256": "sha256-tjlv6ALXvHajgUheJmy5dfOy8tPdm/chOqtsonpWH8E=" + }, + "kotlin-stdlib-common-1.3.50.jar": { + "sha1": "3d9cd3e1bc7b92e95f43d45be3bfbcf38e36ab87", + "sha256": "sha256-jOZ46I5LoBi2bazs+VJHHk19/uFWqKgZdgpaX/KdMjw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.3.60", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.3.60.pom": { + "sha1": "9b0f75dca0716e00e80f77eca0d912434b818aa9", + "sha256": "sha256-JTOQWhDGev0NsdHcCDejxvHg1ky3bgh/fcy6ra4zFNo=" + }, + "kotlin-stdlib-common-1.3.60.jar": { + "sha1": "538bd29b2d5a7d278a7188f89c3b84183fa37f75", + "sha256": "sha256-HxWGtK1WFY+Fo3bMv/Nr9M8TG52aOTDrRGgCPPCCp38=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.3.71", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.3.71.pom": { + "sha1": "dc5b708b195cb44fcb098c96c5ed4774a53d5c93", + "sha256": "sha256-k/XLUBSN7n2nRu1IfmJ47o2U2pqZtnD59SSEVsL2Y0E=" + }, + "kotlin-stdlib-common-1.3.71.jar": { + "sha1": "e71c3fef58e26affeb03d675e91fd8abdd44aa7b", + "sha256": "sha256-l0+Km3v849cwqG7+DqshmnJiHoUw+R4wyJ9AC6mAkuw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.3.72", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.3.72.pom": { + "sha1": "c4e521d2c0af9e771afbfc11fff4b2f5111b2678", + "sha256": "sha256-JHge31sJNIBJMNyvyJEbnnENFGI5qCaZiJEy57N76dw=" + }, + "kotlin-stdlib-common-1.3.72.jar": { + "sha1": "6ca8bee3d88957eaaaef077c41c908c9940492d8", + "sha256": "sha256-Xn0VUoY+SAwWKLHMOc4jDvgp9bcjAQYhWgWs2lFyIDo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.4.0.pom": { + "sha1": "03055a223c66d74a15ae3f53870e910e769483ff", + "sha256": "sha256-mOWaRFoySPWIEc5ngkhd5T+nf8b1rbPrdmp4sSMavQo=" + }, + "kotlin-stdlib-common-1.4.0.jar": { + "sha1": "1c752cce0ead8d504ccc88a4fed6471fd83ab0dd", + "sha256": "sha256-hgHgBNdNS+XocfbEOnMLSCzk3/RQgrCJJNXWnTPJ99E=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.4.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.4.10.pom": { + "sha1": "a7a2da276e4c795619d6dd862cf8db356906ca3c", + "sha256": "sha256-Y0gOcqfU3nQrSab/kORfVEyjfz/hGvi0g5+c10dRXQA=" + }, + "kotlin-stdlib-common-1.4.10.jar": { + "sha1": "6229be3465805c99db1142ad75e6c6ddeac0b04c", + "sha256": "sha256-RoHy1DamjHUjWV2E7VdY4TgvnaD2fJHmqEhpDXESdP4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.4.32", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.4.32.pom": { + "sha1": "dedd0518589a9a4745559ec1b67ada106915329d", + "sha256": "sha256-LvaDA4OCUy/BTaN8UOHEYJu3an5v8uMw8GLBZCF/RIM=" + }, + "kotlin-stdlib-common-1.4.32.jar": { + "sha1": "ef50bfa2c0491a11dcc35d9822edbfd6170e1ea2", + "sha256": "sha256-4f9vVe6edZHcxjP3dXusJaftscx/c4s37GUvEPZqQUU=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.5.0.pom": { + "sha1": "8aa4431a9a68238f89ed04fc3f4a9c7a7536792f", + "sha256": "sha256-04UhhLLDvSbYaB74/Dl3wqMBFT0nhx8R344sEV3n8QQ=" + }, + "kotlin-stdlib-common-1.5.0.jar": { + "sha1": "4080d69efca5e39e9b4972f125e40f1607bd6460", + "sha256": "sha256-wUzmcgpIe14jjxsMMKw61z5FuQpAcxygsc/d7Bo3aC8=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.5.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.5.10.pom": { + "sha1": "0b78bbcc36abf0770a9e84c9ede6a1be18717352", + "sha256": "sha256-O9VaG5QWi8987j5QbVKmiKquNr4NAXeXP2smbtjovhw=" + }, + "kotlin-stdlib-common-1.5.10.jar": { + "sha1": "06b84d926e28493be69daf673e40076f89492ef7", + "sha256": "sha256-2VjOlL7ahfhlgp+5UBKASGbbfVJGYV/XGi9aq8o+eZQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.5.30", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.5.30.pom": { + "sha1": "850c3ca345e0afacbfaf203f5259e3e1be59df9e", + "sha256": "sha256-zXARUA4BVnJbDfEbaSDDPMPBaj3ExSa8ltQuGqaR5Lw=" + }, + "kotlin-stdlib-common-1.5.30.jar": { + "sha1": "649ffab7767038323fec0cc41e2d7b0a8f65a378", + "sha256": "sha256-jrOsUwqXhCLg+NC7p4uixii+yZfcLxqk74xbhU43ZLg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.5.31", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.5.31.pom": { + "sha1": "6f778763702ed6371c2cab78b3424ff2188a7283", + "sha256": "sha256-thXpRrjD0r6pllLs2pfVfs+Dv180xl0oZ5CvI+USg8I=" + }, + "kotlin-stdlib-common-1.5.31.jar": { + "sha1": "043331609c7de811fed085e0dfd150874b157c32", + "sha256": "sha256-36KhjiawKDiO4ZaNGZv28Wb3N6twScJaXi2mFEBOIq0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.6.0.pom": { + "sha1": "029fe9e59bc54bc9cd37d701771db87fe9883689", + "sha256": "sha256-iZ6XwgKLX3or2vZ1H6KgG2h+PuKngXZ8iiJ8v/tsAYQ=" + }, + "kotlin-stdlib-common-1.6.0.jar": { + "sha1": "7857e365f925cfa060f941c1357cda1f8790502c", + "sha256": "sha256-ZEpyV8I7UaH9UGiWDkCSLj5SwhnxHs4+BAo6vHSCPyI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.6.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.6.10.pom": { + "sha1": "d3974425e52440bfe098563c1f10f5712471453d", + "sha256": "sha256-91ryF83Y3Z4HseDgGAhKMgkqCRo5C3P+qmV+xE5c8JQ=" + }, + "kotlin-stdlib-common-1.6.10.jar": { + "sha1": "0c118700e3a33c8a0d9adc920e9dec0831171925", + "sha256": "sha256-KA3dCZTkVgyaSe6BwbBH3cK48hdhWUdQZgJbR0s37/0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.6.20.pom": { + "sha1": "2f237c62ee4a898a4c3a5904988a90c4408e06e4", + "sha256": "sha256-PgTMk1HVzsQqRcBg+HM/bpTrx+NZExClGOBuiFB4mcg=" + }, + "kotlin-stdlib-common-1.6.20.jar": { + "sha1": "27b4562b6713d70f458c6d7ea39aadacb8e6a92b", + "sha256": "sha256-jaQKJSDTDcsQEhdv6T0k6C0Io+NGw34DQ7D7b2T2vgE=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.6.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.6.21.pom": { + "sha1": "747a8ea8a8a4328946cf0252c5ceb2aa0ceb054f", + "sha256": "sha256-W8FW7nP9PC2sil7FSNWBtjMzNUfC/r7Zz2VH//FSa6I=" + }, + "kotlin-stdlib-common-1.6.21.jar": { + "sha1": "5e5b55c26dbc80372a920aef60eb774b714559b8", + "sha256": "sha256-GDvsWc2fOhSVexkOjIec8RlL0fEGsKe24cu4eQ0kI2M=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.7.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.7.10.pom": { + "sha1": "38af1ac562fefcc5e920fc7c1290b480251c5163", + "sha256": "sha256-EBHGO4julM3/XVlpNzB1WbxVA3tzPMAM5jzaPPrgqOs=" + }, + "kotlin-stdlib-common-1.7.10.jar": { + "sha1": "bac80c520d0a9e3f3673bc2658c6ed02ef45a76a", + "sha256": "sha256-GfEC7+lin46rxjhTrRXFM+R8R/kfygkoXFvehuWfkdQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.7.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.7.22.pom": { + "sha1": "13f58480f3ff6c0965d845733f7858e323916fcb", + "sha256": "sha256-+Jgw54L2Hy9HFWiibGdexK3cz/D9cWQPlV9wWikZOtU=" + }, + "kotlin-stdlib-common-1.7.22.jar": { + "sha1": "5dd2c9dcf64c94b46cf4bdb6fa4035b9bb726e2e", + "sha256": "sha256-eUiM3yhMS1s5mNMC6um/CYD6RB45BsalPRF0+Xij5IQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.8.0.pom": { + "sha1": "943e647c5b6a8edafb7fa14d01d414165eba8c76", + "sha256": "sha256-oHXsRsyN/ShG5FR9QaeyLRZJeoLieDA0GTEvuJd1e4U=" + }, + "kotlin-stdlib-common-1.8.0.jar": { + "sha1": "f7197e7cc76453ac59f8b0f8d5137cc600becd36", + "sha256": "sha256-eO+TtZ5gPMD+Ud75vUwDewfLrOOzt4BtGkkKQrwfTLI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.8.20-RC2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.8.20-RC2.pom": { + "sha1": "35c03c50837833d16677f0e74fba7e5da4d0a49e", + "sha256": "sha256-hEyKL3R7tdf01bsm5Rx6+qkIKKMqegc2Qzm80/ymoq8=" + }, + "kotlin-stdlib-common-1.8.20-RC2.jar": { + "sha1": "948fe79545343155ae7e6669c40ac9e086170e82", + "sha256": "sha256-sI6kfeMK7cKIBLj7TDkePDiGfoLEsfXLg7s8qveg5MM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.8.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.8.20.pom": { + "sha1": "c8bc98db3b7220e49408922137dbd44fc458708c", + "sha256": "sha256-YFWRuJs3ISfmspxpMl+i9qjEb0aMRdCUEOeOtZ/IChc=" + }, + "kotlin-stdlib-common-1.8.20.jar": { + "sha1": "5eddaaf234c8c49d03eebeb6a14feb7f90faca71", + "sha256": "sha256-+iAYirqo7PHQA16TqWmwcfEORaHIN4wxRSHq3nP3X9U=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.8.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.8.21.pom": { + "sha1": "8f1a58d8fd44b0f34359325b9b47c95df2c7ea9e", + "sha256": "sha256-4ZpVd8vOqJcolw21MzyCZMjGmuci7recv0HV8LDJrmU=" + }, + "kotlin-stdlib-common-1.8.21.jar": { + "sha1": "d749cd5ae25da36d06e5028785038e24f9d37976", + "sha256": "sha256-akTJ7MnXdU2elD+x41iMdNSj8Xhb5RB09J1sVyNoKnM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.8.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.8.22.pom": { + "sha1": "60260374e015ab548cfbd0dbfb90a0bff6316982", + "sha256": "sha256-pysR3wi1Mi16Xo5iB4nuPkz+846GxDDn0RO/qeVMWB4=" + }, + "kotlin-stdlib-common-1.8.22.jar": { + "sha1": "1a8e3601703ae14bb58757ea6b2d8e8e5935a586", + "sha256": "sha256-0MI2XiQ373DzRYbVDwVXQ/eXFrz+ZeS8cjnN0mae98U=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.9.0.pom": { + "sha1": "0a865a0c5a64618fd1dcca13d05dea4cba44c832", + "sha256": "sha256-NmDTanD+s6vknxG5BjPkHTYnNXbwcbDhCdqbOg3wgqU=" + }, + "kotlin-stdlib-common-1.9.0.jar": { + "sha1": "cd65c21cfd1eec4d44ef09f9f52b6d9f8a720636", + "sha256": "sha256-KDJ0IEvXwCB4nsRvj45yr0JE1/VQszkqV+XKAGrXqiw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-common/1.9.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-common-1.9.22.pom": { + "sha1": "6e65c712501870f7b7d122ee0563bf3e8728d0bf", + "sha256": "sha256-10k21oh1ZK63EOhCmLVCB/U+m88jpSrSv6IsIIZ3V2c=" + }, + "kotlin-stdlib-common-1.9.22.jar": { + "sha1": "1a7ef41fd8ea66da8184088b39e5fbf641ecda12", + "sha256": "sha256-YLU6P8DtGf9VaK1UNy8QL1EQm3SAQX6TyPNBiuT3MYg=" + }, + "kotlin-stdlib-common-1.9.22.module": { + "sha1": "ca923df03d37dbf956768397e8440bfd11431355", + "sha256": "sha256-+Tyemr+NUtjo/Y6FGqgC7OxVEyFhxK7ufTzZJL95QkY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.2.71", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.2.71.pom": { + "sha1": "682677def146529534fe87ca026d317804acd436", + "sha256": "sha256-lBQm2VUmyCjnlHkaHaiJti8fKeAiDPLy6NwJthlwgLM=" + }, + "kotlin-stdlib-jdk7-1.2.71.jar": { + "sha1": "4ce93f539e2133f172f1167291a911f83400a5d0", + "sha256": "sha256-sTa9YbJA4H1Nks4A070dv1hEAKe/XyIMLzzSJEaFgII=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.3.50", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.3.50.pom": { + "sha1": "2fb57f1beb5676a730f533556c0a5fa28177f4a4", + "sha256": "sha256-o670Y4julul1EQ21HfOreUyH2JK9vG1aSb+Sbkd5Q4s=" + }, + "kotlin-stdlib-jdk7-1.3.50.jar": { + "sha1": "50ad05ea1c2595fb31b800e76db464d08d599af3", + "sha256": "sha256-mgJmOediEvjVe4bVWwdTlMLgCfGXkRB1HTTAXF911Xs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.3.72", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.3.72.pom": { + "sha1": "765a55cb8f50072490c6029ac9208a62f6f45eaf", + "sha256": "sha256-nVoT2avDNEXhNm0livCnfkLwGUWs73wJF7nVOYVOL84=" + }, + "kotlin-stdlib-jdk7-1.3.72.jar": { + "sha1": "3adfc2f4ea4243e01204be8081fe63bde6b12815", + "sha256": "sha256-QFZsDAjUFLlBO6VW/3+KCwS5i58PQk0SLdIIhRDvzMQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.4.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.4.10.pom": { + "sha1": "1a36485397cc60f72adb00ef0586b766574de29f", + "sha256": "sha256-ePL+HUjpnGSViTDM3EP85ZOUYOzfxl8wKCOgIdY8Ug0=" + }, + "kotlin-stdlib-jdk7-1.4.10.jar": { + "sha1": "30e46450b0bb3dbf43898d2f461be4a942784780", + "sha256": "sha256-+VZjgMCHIseAzjPO7iPpjd92XKmPq9Pi+rrnl1yNIys=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.4.32", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.4.32.pom": { + "sha1": "23da429ef5dd4678d03377405822680e2da84246", + "sha256": "sha256-Bkw3mti354euiGPEFL/HMoFAcMaEG1Jbo2J8HDM86rs=" + }, + "kotlin-stdlib-jdk7-1.4.32.jar": { + "sha1": "3546900a3ebff0c43f31190baf87a9220e37b7ea", + "sha256": "sha256-X4Aedcon2HkcFLB5Q8YI2idiDZEKgJMCKvV/VD1dmLY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.5.0.pom": { + "sha1": "93c9fdc6f6ba8ac6f3f93bdf29ea24525411ba05", + "sha256": "sha256-7T5s00w9Onc9oOAHuxLhZFSHJUZOWtthZEqY+yklStE=" + }, + "kotlin-stdlib-jdk7-1.5.0.jar": { + "sha1": "f61904618ea7be07a66e0545ffe8dc2c70a19b77", + "sha256": "sha256-rBLwkvErV1wfngq1AlseYQsP6VZj4mNxwWwyiJVxG64=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.30", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.5.30.pom": { + "sha1": "efbcacb7449201f5df0375ceb38d2a603bd5af6b", + "sha256": "sha256-rS7PIPk35RI3+xLJZhDXAUnW3+zjMQQmGopE5Cw7rHc=" + }, + "kotlin-stdlib-jdk7-1.5.30.jar": { + "sha1": "525f5a7fa6d7790a571c07dd24214ed2dda352fe", + "sha256": "sha256-fOBAZG5rmvZiyWu7mIoa3NTJlINNQk4Wlg+nn/+Tgl0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.31", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.5.31.pom": { + "sha1": "b3f53ded7aa6f0f85abfeca4b1a721849587de2d", + "sha256": "sha256-IxOEie4pOmgZcGiHd0X3AL+hGvmJGHvtPDB0zYwHl3g=" + }, + "kotlin-stdlib-jdk7-1.5.31.jar": { + "sha1": "77e0f2568912e45d26c31fd417a332458508acdf", + "sha256": "sha256-olv0c1POiZ2EPL3e5RbWIac0c+f7qX+NAwHntK7XwV8=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.6.0.pom": { + "sha1": "b759cd22a86e3db7de285a70d61db41df9451522", + "sha256": "sha256-7VYHnyk5bX8mkR+1PKKLiOmKXEKxm757sjqS5zHPZcs=" + }, + "kotlin-stdlib-jdk7-1.6.0.jar": { + "sha1": "da6bdc87391322974a43ccc00a25536ae74dad51", + "sha256": "sha256-hw01/SZrLa9kwQgP5Rgk08No95lThKjXxfwv3EDrezo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.6.10.pom": { + "sha1": "5da7f12df7646d63f35e8a894157143c93efceb4", + "sha256": "sha256-YSIR/5MPW1LHJP92NBfVqigd1+AyXDs1yNGBIKao300=" + }, + "kotlin-stdlib-jdk7-1.6.10.jar": { + "sha1": "e1c380673654a089c4f0c9f83d0ddfdc1efdb498", + "sha256": "sha256-Ku3NxrabM731zCNbzqiOfPZgEUa7a83/2zErus174mE=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.6.20.pom": { + "sha1": "4476b8c551258e66dc12bd68eb03159b80c81f63", + "sha256": "sha256-iBveiiNwhuKOA0KLTvMmj0SspfoajHb4lUdIRVyuvSE=" + }, + "kotlin-stdlib-jdk7-1.6.20.jar": { + "sha1": "f8629f336bad4001c89e9cffa5ef3d4b5d0f5e22", + "sha256": "sha256-qi+i6BNVxNmN2X2iFpv0AfhCJhN49bHL6hqhGFXWdiA=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.6.21.pom": { + "sha1": "a5e777a4b99c72a66b43159cfa525666e228de07", + "sha256": "sha256-ARzSjruf3oFrA1nVrhCjZ07A/yxTEMBBLCDv6Oo9oG4=" + }, + "kotlin-stdlib-jdk7-1.6.21.jar": { + "sha1": "568c1b78a8e17a4f35b31f0a74e2916095ed74c2", + "sha256": "sha256-8bBjTbuUFyA4RjAguy3UXKJoSfjOKdYlrLDxVp0R2+4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.7.10.pom": { + "sha1": "0b836259c72e682066a0653b940d0b56e9d9fed4", + "sha256": "sha256-Ehg3/gDQA5RMn6qhwl+TzszLA+ud2PENnorfS3p9kNw=" + }, + "kotlin-stdlib-jdk7-1.7.10.jar": { + "sha1": "1ef73fee66f45d52c67e2aca12fd945dbe0659bf", + "sha256": "sha256-VPYTUbGTatiPTlMFn+eB5yPq5R147Z50Iti0A1dOxoI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.7.22.pom": { + "sha1": "064c2e0efd5b8904e7e462b783924065e1c67089", + "sha256": "sha256-tnFoC9t9WrraeUAPO4dvOLISH/gZMjr9MizyGUG5W2U=" + }, + "kotlin-stdlib-jdk7-1.7.22.jar": { + "sha1": "6aa0a0e051d6b828dc25047025ec245758fab6d9", + "sha256": "sha256-red2jZiUCjA4VlKa83P3X/1eJpW03BmOmxg5avpT+eA=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.8.0.pom": { + "sha1": "b8a4b9c31ec89be4e5806db4aa92192a9d592bf0", + "sha256": "sha256-36lkSmrluJjuR1ux9X6DC6H3cK7mycFfgRKqOBGAGEo=" + }, + "kotlin-stdlib-jdk7-1.8.0.jar": { + "sha1": "3c91271347f678c239607abb676d4032a7898427", + "sha256": "sha256-TIidHZgD9fLrbBWSprfmI2msdmDJ7uFauhb+wFkWNmY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.20-RC2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.8.20-RC2.pom": { + "sha1": "f9b658c543af78cfc111d40035706d06def4267d", + "sha256": "sha256-AcsoRLbWv07ypM1EgtF/Szr2WUJIMfBqmJSM9v+4/xs=" + }, + "kotlin-stdlib-jdk7-1.8.20-RC2.jar": { + "sha1": "d100f7cb452c22f937a7bcca2efcc45cc77c7b15", + "sha256": "sha256-Z0IlNH/jgrqdUs5W7mclW4E6PvKNFlU+qzI/kTEAK9A=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.8.20.pom": { + "sha1": "e613a7635e90125d25b100b636a85639b4487ac4", + "sha256": "sha256-NiLRBleM3cwKnsIPjOgV9/Sf9UL2QCKNIUH8r4BhawY=" + }, + "kotlin-stdlib-jdk7-1.8.20.jar": { + "sha1": "3aa51faf20aae8b31e1a4bc54f8370673d7b7df4", + "sha256": "sha256-rx7EDDuVGv3MDCoBc8e4F2PFKBwtW6+/CoVEokxdzAw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.8.21.pom": { + "sha1": "d23a31049e8289c21e78078e888f3968c82ad0ac", + "sha256": "sha256-m7EH1dXjkwvFl38AekPNILfSTZGxweUo6m7g8kjxTTY=" + }, + "kotlin-stdlib-jdk7-1.8.21.jar": { + "sha1": "7473b8cd3c0ef9932345baf569bc398e8a717046", + "sha256": "sha256-M9FI2w4R3r0NkGd9KCQrztkH+cd3MAAP1ZeGcIkDnYY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.8.22.pom": { + "sha1": "4c65e36b2298dd73462a329993522d1cb1ba7c3f", + "sha256": "sha256-T5WKqZPVmE+PXr7UFGVipfOp9pW2BJyfKHOBN5ytqzM=" + }, + "kotlin-stdlib-jdk7-1.8.22.jar": { + "sha1": "04dabb8248310d833bb6a8b516024a91fd3d275c", + "sha256": "sha256-BV9cskKH+hBhAJlae0erkhJrgegy6HX1+izwvVVpPQs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk7-1.9.0.pom": { + "sha1": "5ce6c5278ef6e342507a96372ee534ed0b830e54", + "sha256": "sha256-wRB08MiYqYuGPGFEcdQ409+Soewzgqbjf5NdfXGVS1o=" + }, + "kotlin-stdlib-jdk7-1.9.0.jar": { + "sha1": "f320478990d05e0cfaadd74f9619fd6027adbf37", + "sha256": "sha256-t5eaeqyUBV8Nnx/TtHzl/+HLYDKoQrqfvnGG8IUokXg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.2.71", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.2.71.pom": { + "sha1": "234a35fd330223e98870d7687bf431f3425ba4a6", + "sha256": "sha256-hFwz8OntX2a8otftOYc/PnWSDF4z3phMJcSHYPeVlLM=" + }, + "kotlin-stdlib-jdk8-1.2.71.jar": { + "sha1": "5470d1f752cd342edb77e1062bac07e838d2cea4", + "sha256": "sha256-rDyKv0d5C2S09+JQmlPwwUXgYawWEqWXUgU10ZmUbqk=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.3.50", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.3.50.pom": { + "sha1": "c7e40dc0558eac1f46f1760bd77a7fca0ae1008a", + "sha256": "sha256-hnSwAvZqKUiYHE9stZh+xi/z1UtqJ5mm0s0jr7g/KtM=" + }, + "kotlin-stdlib-jdk8-1.3.50.jar": { + "sha1": "bf65725d4ae2cf00010d84e945fcbc201f590e11", + "sha256": "sha256-GzUftuCcFLVVJcdMH0z0iULq5Dw0i3vHZKXm5CPU2gw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.3.72", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.3.72.pom": { + "sha1": "9c1a61f9bf0a8155519010856d13e91c8d24fb92", + "sha256": "sha256-ptUPAyG9tSg4yZE2kwyNzHjDB0pZLVJoYuwBvpH6Yis=" + }, + "kotlin-stdlib-jdk8-1.3.72.jar": { + "sha1": "916d54b9eb6442b615e6f1488978f551c0674720", + "sha256": "sha256-Ez2nDPwHtWCUKC6sXFm8zVnxZ+4urSLlKCh22LwQv5U=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.4.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.4.10.pom": { + "sha1": "affaa3a6775aacbc9064cf820e5ced9e57422b2d", + "sha256": "sha256-wvTXAILbv86mj9+nQonO8y355LXd81nwBgNt/xxbBtY=" + }, + "kotlin-stdlib-jdk8-1.4.10.jar": { + "sha1": "998caa30623f73223194a8b657abd2baec4880ea", + "sha256": "sha256-ObepRC16OGXg9KcyxWwdXaDhH/s7uCpGHTLesMDKdnM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.4.32", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.4.32.pom": { + "sha1": "5a3eef93e614c6f3f13b323563593b5bb577071d", + "sha256": "sha256-5VLiPtwOe8KfNBZF+5MnyCUn433+S94Tuko6823iP9s=" + }, + "kotlin-stdlib-jdk8-1.4.32.jar": { + "sha1": "3302f9ec8a5c1ed220781dbd37770072549bd333", + "sha256": "sha256-rcQ+VHV7EG4M17O3qiV9/0cbYe/avgZ/wCsvV+I5YmI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.5.0.pom": { + "sha1": "74dbeeae4eb7c0d61325c1179de731d990a7a138", + "sha256": "sha256-0Ls+nTxj4e2bB3lPhWtyIEJwVxffMFEOTM4RPuLKWU0=" + }, + "kotlin-stdlib-jdk8-1.5.0.jar": { + "sha1": "65fbc439df2e4aad1f3769762d54534f1b564090", + "sha256": "sha256-FebIG56EXu/ljVGgRnC7kEGARvRYJk7A5h7pvbwb+uc=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.30", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.5.30.pom": { + "sha1": "a95e23c8f40c3220953b05956eb96e959a85ca6a", + "sha256": "sha256-QSTlsk1JrJE/hNsd8pZfKi0rhzTf/KnN1y/bplYm0W0=" + }, + "kotlin-stdlib-jdk8-1.5.30.jar": { + "sha1": "5fd47535cc85f9e24996f939c2de6583991481b0", + "sha256": "sha256-s6ygU5xYMmIjsH3HwKx5tah1uoDLke4WCeyJuze4mQ4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.31", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.5.31.pom": { + "sha1": "6b1353e3e341eeddeea8e67db3d89bed51ceb920", + "sha256": "sha256-RREKqwB0eSuBWAewKy2vGNKzfodHrAaSqteg0C2ok98=" + }, + "kotlin-stdlib-jdk8-1.5.31.jar": { + "sha1": "ff5d99aecd328872494e8921b72bf6e3af97af3e", + "sha256": "sha256-tUj3dnqs8CnSQX5HRAdCvW0+vt4ZtgOG4jVUzlxMX9w=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.6.0.pom": { + "sha1": "377c3bad2db13b1135b7c1a7b8ccd89a4d050efa", + "sha256": "sha256-257OSvTucAihimZYA5giQgCvf23+q0Z2HsOedWy5uec=" + }, + "kotlin-stdlib-jdk8-1.6.0.jar": { + "sha1": "baf82c475e9372c25407f3d132439e4aa803b8b8", + "sha256": "sha256-qwVHxJWVMhSl8rKBUAFPTgITNnjVK3fXY3XqI15EPb0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.6.10.pom": { + "sha1": "9ad76ce2a9e0473c5d5d4ec01add1b061beb521d", + "sha256": "sha256-Q6ZJ+nN7+zX6SvTm3jPi8IpdGRBNdYLqQNvNK2N5Csw=" + }, + "kotlin-stdlib-jdk8-1.6.10.jar": { + "sha1": "e80fe6ac3c3573a80305f5ec43f86b829e8ab53d", + "sha256": "sha256-FFbYLQOeow2EhbAykB9Su/B+fNvoux+HCK0yqFdMQc4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.6.20.pom": { + "sha1": "6207ba2d7a89316eafea7f135ef1fc17e41c424b", + "sha256": "sha256-GEap+GBLC+HHGiEovb2diQJyAnlCf2ItK5pECsmjwwk=" + }, + "kotlin-stdlib-jdk8-1.6.20.jar": { + "sha1": "dab8089bca6ac0e394c37281ea8cff2f99acd421", + "sha256": "sha256-/asb8SDiteerbXiI6evAJOxrjKcpNhKWOV2rY0shNpU=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.6.21.pom": { + "sha1": "c3552554cf54a7297a27582d74f0aca09d12728c", + "sha256": "sha256-g2oReaCNJJFGl9JhLgO4SKCHyAy0sMoj+c+rJH86dcQ=" + }, + "kotlin-stdlib-jdk8-1.6.21.jar": { + "sha1": "eeb4d60d75e9ea9c11200d52974e522793b14fba", + "sha256": "sha256-2rRUibR3NtWfzkS4BnbxlHqba8qxD9YOh4qDvYKmlUw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.7.10.pom": { + "sha1": "1905bbcb22b41bd917604589c74bd8092ce55516", + "sha256": "sha256-WStSUvAY49vv7KGlNf0jFd2ny5573loG1rQZzzZD1H0=" + }, + "kotlin-stdlib-jdk8-1.7.10.jar": { + "sha1": "d70d7d2c56371f7aa18f32e984e3e2e998fe9081", + "sha256": "sha256-iq/dYMlPRUyS5QZtJmpe1T7MY8ePYjs/2dtW/qQDKHM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.7.22.pom": { + "sha1": "2d96d970ad78076e0b903f9e05b9fc8dbddbbc22", + "sha256": "sha256-6vPC0+N6UU9C60yVVhpGVpFaJldJeWr53K4ox8pgQ1g=" + }, + "kotlin-stdlib-jdk8-1.7.22.jar": { + "sha1": "b75c999616784d452613d670fec56a527f2bd3d1", + "sha256": "sha256-MVEC/xzC5wYoayJxR7jbI3g9uRGokpiXapCiLgcc0Rc=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.8.0.pom": { + "sha1": "fb9dbe3343144298825b8d9f23a51f4af4be6cb0", + "sha256": "sha256-K7bHVRuXx7oCn5hmWC56oZ1jq/1M1T2j/AxGLzq1/CY=" + }, + "kotlin-stdlib-jdk8-1.8.0.jar": { + "sha1": "ed04f49e186a116753ad70d34f0ac2925d1d8020", + "sha256": "sha256-BbYoBEQbDJoZILa31c9zKaTiS2JYR44ysfBGygGQCUY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.20-RC2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.8.20-RC2.pom": { + "sha1": "e5ad77e0171571b00ed937285e9fe7bc350c9bbc", + "sha256": "sha256-3ljmBrfKv1vFYloT+WCgWkHU1Ya2vm/4vb8tiGGy5x4=" + }, + "kotlin-stdlib-jdk8-1.8.20-RC2.jar": { + "sha1": "3f3b892b2b7261138cc4ad48117553b1eb7e8fc7", + "sha256": "sha256-g9t0ahRRMQ8Ao3uRXXEGRR/QmienYh2ZQRt7/dBV0r0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.8.20.pom": { + "sha1": "f9dac9f21106bd224ea78c78b56f4e401d7df56f", + "sha256": "sha256-OkYiFKM26ZVod2lTGx43sMgdjhDJlJzV6nrh14A6AjI=" + }, + "kotlin-stdlib-jdk8-1.8.20.jar": { + "sha1": "73576ddf378c5b4f1f6b449fe6b119b8183fc078", + "sha256": "sha256-45i2eXdiJxi/GP+ZtznH2doGDzP7RYouJSAyIcFq8BA=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.8.21.pom": { + "sha1": "3e5d957c0d7c874093cdf22483d4fc024476479a", + "sha256": "sha256-ODnXKNfDCaXDaLAnC0S08ceHj/XKXTKpogT6o0kUWdg=" + }, + "kotlin-stdlib-jdk8-1.8.21.jar": { + "sha1": "67f57e154437cd9e6e9cf368394b95814836ff88", + "sha256": "sha256-PbdSowB08G7mxXmEqm8n2kT00rvH9UQmUfaYjxyyt9c=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.8.22.pom": { + "sha1": "3431bd564edf876c06707fda77705c0d70c2eab5", + "sha256": "sha256-ko8hhyF0djE8uBbUgHC8dlSqO5pa6B0/xfjCecyPjZ4=" + }, + "kotlin-stdlib-jdk8-1.8.22.jar": { + "sha1": "b25c86d47d6b962b9cf0f8c3f320c8a10eea3dd1", + "sha256": "sha256-QZiw6vCQpPJbb35aWVgfQxS6jJ9s0dE+6dNI5l7Y9wc=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-jdk8-1.9.0.pom": { + "sha1": "4f04e57e4a12d4a59f620c63520681c4ff1d03e6", + "sha256": "sha256-ZNWY3YjiUEZnMeIDBKtvBsu7urfuMitHA7a1n4gcT5I=" + }, + "kotlin-stdlib-jdk8-1.9.0.jar": { + "sha1": "e000bd084353d84c9e888f6fb341dc1f5b79d948", + "sha256": "sha256-pZ+iT98f+1lLrs2/D9EAEPl3zqECNtSH/jRkl3pzd/o=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.1.3-2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.1.3-2.pom": { + "sha1": "652f49d3edbdc251e298b3d1ec74d6488d1ce8da", + "sha256": "sha256-+P2HTDfInV3+XsgZDZRiZRK9NqssKsnUtnUVxb7nRcs=" + }, + "kotlin-stdlib-1.1.3-2.jar": { + "sha1": "9b44c139a4ec57031e0c84ba0e49ba16df6d801c", + "sha256": "sha256-8ohSiIOfgvgkoMBCkirn8k7Q1VzianS2p8VX4LfjDCQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.2.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.2.0.pom": { + "sha1": "3135147249f1ab93c33c5bce2bf952b4e8ebbba6", + "sha256": "sha256-ypd42kr7zUIbCThLJXyd5H9hCuxckWXgIJIo0lozeiA=" + }, + "kotlin-stdlib-1.2.0.jar": { + "sha1": "25eb440d6eeb9fc60299121020fe726eb2100d03", + "sha256": "sha256-Bc/Z9awLQZEHA6iSX3IRpJWQmyei/90cUQbxaJrq/NQ=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.2.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.2.20.pom": { + "sha1": "5bc837f53128e24cfa9a7142db579e4018220a44", + "sha256": "sha256-mwSXPjgP1mwZkOfvQP0kq8gIn5Ut6kUaB9lbTdnondM=" + }, + "kotlin-stdlib-1.2.20.jar": { + "sha1": "1ce9e25c74aade0aa039cce459f2906a8c8ffc8e", + "sha256": "sha256-ebI9Wb+dRXSDqxNo87AtdCOm54QVM8lavFqrIKMGm+Q=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.2.71", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.2.71.pom": { + "sha1": "e34c8d5c8e7077a037dadcc70b114e130eb9824b", + "sha256": "sha256-cT23fJoMR6Zw+VwjsAUbm9lCs6DqH2zJu12eq6H6da4=" + }, + "kotlin-stdlib-1.2.71.jar": { + "sha1": "d9717625bb3c731561251f8dd2c67a1011d6764c", + "sha256": "sha256-TIlcJwuH9f7ConluHYnBVAfugh3pYVJ8KFiLtGr7xos=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.3.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.3.0.pom": { + "sha1": "174b5899a832edc66e21e5954339c5098b735567", + "sha256": "sha256-QBiyXs+R6d7E4kDYlrldEgCWQOOxzpuO/tIyPnrGWwc=" + }, + "kotlin-stdlib-1.3.0.jar": { + "sha1": "a134b0cfe9bb44f98b0b3e889cda07923eea9428", + "sha256": "sha256-T/D8uX9Jg7SquhJmjCStIbCEYJFdsbAh2PHYvuaH8hw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.3.31", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.3.31.pom": { + "sha1": "e4e40ca37fa7cc6dffeef283f7872cafc345b000", + "sha256": "sha256-k4GCVV+Hqr2qv6fqIvKSSagB0Iglh/2zbv1raattlIM=" + }, + "kotlin-stdlib-1.3.31.jar": { + "sha1": "11289d20fd95ae219333f3456072be9f081c30cc", + "sha256": "sha256-84yEMmVD5m7UiVsg+z6g/KUn/VoEDh9J0JRuzz0rOyM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.3.41", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.3.41.pom": { + "sha1": "3da9b524f1d0df9d4a52b12331534f2a832d8e4b", + "sha256": "sha256-eFtbZvEUYxfV7VsYyODBD4PNXJCUNMeZpNqYI/AozWM=" + }, + "kotlin-stdlib-1.3.41.jar": { + "sha1": "e24bd38de28a326cce8b1f0d61e809e9a92dad6a", + "sha256": "sha256-bqPQkhsmkZsobwXL25BiZmZqNvmnwJYZcRT3SVcI/7w=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.3.50", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.3.50.pom": { + "sha1": "a75b191fa2963dcd96413bab2d0fb385b136a4e6", + "sha256": "sha256-cE/XiWCuBGQo9pvNW5UcEi5MGAyUACOKhm4SyxhJSmE=" + }, + "kotlin-stdlib-1.3.50.jar": { + "sha1": "b529d1738c7e98bbfa36a4134039528f2ce78ebf", + "sha256": "sha256-5vBXRu4DZtC1KCWgkPrEdNz0QILJCDu7IFvRaXZIjWw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.3.60", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.3.60.pom": { + "sha1": "73924ccc41144c8ddd80b19e4acbc850f1afd7a5", + "sha256": "sha256-5zf6GmpR98/36vf2lSXUvsnyLnydgGzUbccgUWAEQ94=" + }, + "kotlin-stdlib-1.3.60.jar": { + "sha1": "984644b61450add3bcef8cf20f948fec458b420d", + "sha256": "sha256-6Yg0AAtdsHN21XgnYa1Y8/I8zQW28fqQ3ArUKL5YJQY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.3.71", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.3.71.pom": { + "sha1": "47da9a84ba07a4de17cddff5e139f8d120627c62", + "sha256": "sha256-/wPpmJ+qLUD6SpCYqq1x4kfyfqTSSwdU2mQusJJL2MU=" + }, + "kotlin-stdlib-1.3.71.jar": { + "sha1": "898273189ad22779da6bed88ded39b14cb5fd432", + "sha256": "sha256-Ws4isQKpZCXkrETgVYuSfzhXtWozy8KJzxtwruZF5qc=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.3.72", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.3.72.pom": { + "sha1": "aecbe532bd46a37d2217a63cc0fed4eca0bd71cc", + "sha256": "sha256-Tw1XSpaeqTxFYooWPh7T/6u0WEJ41yxkfsEk+MgQlIE=" + }, + "kotlin-stdlib-1.3.72.jar": { + "sha1": "8032138f12c0180bc4e51fe139d4c52b46db6109", + "sha256": "sha256-OFanNJ66zW0b5oArL+2cTcLFpWTqkra5RayYgkPUsWs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.4.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.4.0.pom": { + "sha1": "4758d9ec733fd494e6e67fe493d6ac60cd30659d", + "sha256": "sha256-PbZ9r8QiuQFeM8/bOk356KaRLeY8S3Mxl8jBic28bUo=" + }, + "kotlin-stdlib-1.4.0.jar": { + "sha1": "63e75298e93d4ae0b299bb869cf0c627196f8843", + "sha256": "sha256-YWoT5ubpBY8Xmhbx4oh8fVDYO3tNVDoN5YpJzoHtfrE=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.4.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.4.10.pom": { + "sha1": "1ae8d0e617a4e272ce07664ad928c57b171e31f9", + "sha256": "sha256-pLnPu0u9pSOvHiTVLHdfucD3y1aOSLBl1FrrVuY8J2I=" + }, + "kotlin-stdlib-1.4.10.jar": { + "sha1": "ea29e063d2bbe695be13e9d044dcfb0c7add398e", + "sha256": "sha256-Aeywl4LAQrkxwYOazyGhiDQLKV0FQAr9bjQV1Edbjao=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.4.32", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.4.32.pom": { + "sha1": "5dda24ec00298aed19523dead8e06bef2f0fd003", + "sha256": "sha256-ybxbx2FbJeaqs+TbsJxYPxifsS+7Ohl9ViXsBvfmGSc=" + }, + "kotlin-stdlib-1.4.32.jar": { + "sha1": "461367948840adbb0839c51d91ed74ef4a9ccb52", + "sha256": "sha256-E+n9PmnccjDOD8hzqSpOXVIdF5vPG+91pnBbqsO/7Lo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.5.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.5.0.pom": { + "sha1": "0c87eda316aa17b65a4cb7a2943ee6470e528e0b", + "sha256": "sha256-lcku2Fbb33aDJabyjWcKLacKSbDaVqB1HaLR01p7mbU=" + }, + "kotlin-stdlib-1.5.0.jar": { + "sha1": "29dae2501ca094416d15af0e21470cb634780444", + "sha256": "sha256-Uig5lv5AZ81zMCiLlq5n7NRjYU3HQRcsVNnTSatqnNc=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.5.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.5.10.pom": { + "sha1": "ecd518d6ac5e2d91266f71017cdf759a62d61328", + "sha256": "sha256-MFrTJGHhkpocwyuomGHZxBk2oRGJ5vSGAW6vizHFu8s=" + }, + "kotlin-stdlib-1.5.10.jar": { + "sha1": "da6a904b132f0402fa4d79169a3c1770598d4702", + "sha256": "sha256-yofEVM0/LmCTHxgDxZaZ1RDTtLlZzXEZKW+5R1gdci0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.5.30", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.5.30.pom": { + "sha1": "d4e1f037717ed2c78f8e5982c5dbc33daab87521", + "sha256": "sha256-NntiHbI69d0Ctt62vom3A0ue4/F/eJAsF7uX7hOJTRs=" + }, + "kotlin-stdlib-1.5.30.jar": { + "sha1": "d68efdea04955974ac1020f8f66ef8176bfbce1f", + "sha256": "sha256-xVYI6ett9zJ+dLIeJx0yTcUjzvMVh7jW0jk9sI1uAAw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.5.31", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.5.31.pom": { + "sha1": "f0765608cf114ef9fd254be913fcc39797b40cc4", + "sha256": "sha256-j3Z2AQSsTocwCcnwM90brXW507j5Uh1j2xJk2ESFLmw=" + }, + "kotlin-stdlib-1.5.31.jar": { + "sha1": "6628d61d0f5603568e72d2d5915d2c034b4f1c55", + "sha256": "sha256-SADOrLLsC7mVmghxVLjjUxjq0epOujLUuxuXNCIqfmg=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.6.0.pom": { + "sha1": "9c44afb8337b7d4c1352711a8a7b4b75db042174", + "sha256": "sha256-cSpt3XF8zVsNUOdMM16TqbGRBSzXu4KWBSAJu4XJRBI=" + }, + "kotlin-stdlib-1.6.0.jar": { + "sha1": "a40b8b22529b733892edf4b73468ce598bb17f04", + "sha256": "sha256-EV2uowsNSEr88jYCN7nZU39IpKLwPzzCoWV338bpA0I=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.6.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.6.10.pom": { + "sha1": "fe039b18132328e8e48284f6655c3edcdefab007", + "sha256": "sha256-OwhwnqS642admim54AeoVQsQwVFsEKj22+n5SrgdxG0=" + }, + "kotlin-stdlib-1.6.10.jar": { + "sha1": "b8af3fe6f1ca88526914929add63cf5e7c5049af", + "sha256": "sha256-UwX3pN7npst5opwlisqT3ke0lYim38baAb2Hcliepmw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.6.20.pom": { + "sha1": "a7c1d39b812c8867eca579245d797bbfa16a27c9", + "sha256": "sha256-oI6D3LDymFCYd94i1SZEZHbdsx6hx3Uw8sgfJNsWb5k=" + }, + "kotlin-stdlib-1.6.20.jar": { + "sha1": "6cedc143badbb4f1c6b7f5a340b04edff1743208", + "sha256": "sha256-7rUcK2eyYjP9gdC8T4BE7ISXGIkJBXY87/2Eox4st5k=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.6.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.6.21.pom": { + "sha1": "f44be76009ce4253eaa59b914f4dccc384442016", + "sha256": "sha256-zkJyW6Ab2DbNqmZ9l032hL9vjxXng5JjMgraf/quHzQ=" + }, + "kotlin-stdlib-1.6.21.jar": { + "sha1": "11ef67f1900634fd951bad28c53ec957fabbe5b8", + "sha256": "sha256-c5xSZnK7M3Vzso9jr6gwbrCIsMOgln9W1sifSjASpJI=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.7.10", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.7.10.pom": { + "sha1": "257059438321a19e567a4593e9fed8a76814c6da", + "sha256": "sha256-bMDPWivALe4GDruQw1Nfw929ejurIQrOPhQqr4F2TYE=" + }, + "kotlin-stdlib-1.7.10.jar": { + "sha1": "d2abf9e77736acc4450dc4a3f707fa2c10f5099d", + "sha256": "sha256-53H+dCUKlD6PY0ZxMgH/HYy5XDpdGpGiK2Wp4E9qiQE=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.7.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.7.22.pom": { + "sha1": "36b88d11912b5e2092cf6009d45aa625845c958f", + "sha256": "sha256-lQO7+Hvs63eosbSwWM/V5+PUvDfevEcERGs0Py1VIe4=" + }, + "kotlin-stdlib-1.7.22.jar": { + "sha1": "9fb43bccb6cf8cfaaf3776c75f5df4f052f09b72", + "sha256": "sha256-+YUF9QpqHB3AAtYQLktBmvk9AR1aEGludaOJNaluqS8=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.8.0.pom": { + "sha1": "bdf2626ad84d8666184ee777e9b1a997c18fd029", + "sha256": "sha256-8ayQZVWo5TbRVjD9O/mA5RMHRiWyBbySmt/Vc00e+HI=" + }, + "kotlin-stdlib-1.8.0.jar": { + "sha1": "1796921c7a3e2e2665a83e6c8d33399336cd39bc", + "sha256": "sha256-x3vvh3RkC5+51uIXRZ/yINrlmHi+t9LktDBQb+/8ZU4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.8.20-RC2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.8.20-RC2.pom": { + "sha1": "fa59a324ad6b8a366a8f51e5d55aa4721d9b9d51", + "sha256": "sha256-gv9JDOwCaZnqDjpjtMvgcidDcybr2t2MxKnlAP4rFlE=" + }, + "kotlin-stdlib-1.8.20-RC2.jar": { + "sha1": "436a3cf44ea8dc1c65d7a59abda28e232b05d84a", + "sha256": "sha256-s4rZISmfk2XnejI+xC68LhBIxlia+YL7vsDlIPYXgDY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.8.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.8.20.pom": { + "sha1": "d96c138bc67fc0c278623b3350b7f926a1db9f2f", + "sha256": "sha256-ycHvxLpLRsYy4qIeaTJYo+ztB+vqoAr008NG05kLIB4=" + }, + "kotlin-stdlib-1.8.20.jar": { + "sha1": "e72fc5e03ec6c064c678a6bd0d955c88d55b0c4a", + "sha256": "sha256-Q5Vkexlh2ftzCjTo2+VsKTFXvAdZAEzKY9m17mZT5cc=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.8.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.8.21.pom": { + "sha1": "69d1a37560299321348842a9e921d26ff53c80d7", + "sha256": "sha256-/gzZ4yGT5FMzP9Kx9XfmYvtavGkHECu5Z4F7wTEoD9c=" + }, + "kotlin-stdlib-1.8.21.jar": { + "sha1": "43d50ab85bc7587adfe3dda3dbe579e5f8d51265", + "sha256": "sha256-BCoc0ayXbNz+XrY/HY4LC4kskkjhWmnIz7pJXVRupSo=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.8.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.8.22.pom": { + "sha1": "1c9f1e169c4cfd1fb570709867e0a5a4e87a3cca", + "sha256": "sha256-WW9KYvMIatmPDniz4le6WyR80xO/+pgY5QlpYhDoOjc=" + }, + "kotlin-stdlib-1.8.22.jar": { + "sha1": "636bf8b320e7627482771bbac9ed7246773c02bd", + "sha256": "sha256-A6XDllzDcFESjmTkZ0jjlLa9TJf6gcbeb8cr/UTjQhs=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.9.0.pom": { + "sha1": "4aea0493dfd3ee18df73bcd97db791c2ea5bb383", + "sha256": "sha256-N3UiY/Ysw+MlCFbiiO5Kc9QQLXJqd2JwNPlIBsjBCso=" + }, + "kotlin-stdlib-1.9.0.jar": { + "sha1": "8ee15ef0c67dc83d874f412d84378d7f0eb50b63", + "sha256": "sha256-Na7/vi21qkRgcs7lD87ki3+p4vxRyjfAzH19C8OdlS4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/1.9.22", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-1.9.22.pom": { + "sha1": "08b57899b44d8973fe6b5bac305a1cbe947b9027", + "sha256": "sha256-zOLxUoXsgHijd0a1cwigVAQt1cwlQgxD9zt4V8JGjwM=" + }, + "kotlin-stdlib-1.9.22-all.jar": { + "sha1": "6c69e7b26179394dafd3c90e630ef763b8c1a267", + "sha256": "sha256-zsOLwzAucqiq+c3kNrWpBx7gMx4q0F6E2LuJczTX6dQ=" + }, + "kotlin-stdlib-1.9.22-common.jar": { + "sha1": "4c7276a1529397ee6389483a408c57a9c6d4ca7d", + "sha256": "sha256-zBF0wWk6JC3Wy8gWpSfEU+BBhJTjbFDNxleL9AFxrNs=" + }, + "kotlin-stdlib-1.9.22.jar": { + "sha1": "d6c44cd08d8f3f9bece8101216dbe6553365c6e3", + "sha256": "sha256-ar4UbCeGQTi4dMzM/l9TTj65I8maG3tdRUlO5WlPPgo=" + }, + "kotlin-stdlib-1.9.22.module": { + "sha1": "f2f3c4f5a132ab35b6c45bb7ba3ba1556fe226e6", + "sha256": "sha256-9IIxS1B5wUVfb7DUJXp0XRAcYSTOlhUiuob53JCQHkc=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-stdlib/2.1.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-stdlib-2.1.20.pom": { + "sha1": "98de1539dcc7b65c4c50b821b67fa16cca18285e", + "sha256": "sha256-Z1DheZ7lAgd9rlw9WZeW9mdgb2DTXpXLeQRI3HkStAs=" + }, + "kotlin-stdlib-2.1.20-all.jar": { + "sha1": "919751264e79f13e6ec79f4eef1d5222e6520d31", + "sha256": "sha256-geB9406Esp4U8/3vkC9LxM8dXalZuGffaD++HMVM4eE=" + }, + "kotlin-stdlib-2.1.20.jar": { + "sha1": "aa8ca79cd50578314f6d1180c47cbe14c0fee567", + "sha256": "sha256-G8x06M6E4sJeqv3hDxJINJzOMGK242l4y+7GENsekwo=" + }, + "kotlin-stdlib-2.1.20.module": { + "sha1": "9cac80175523f91b769c08efcbb7c7ee488a0ca0", + "sha256": "sha256-VdKW5FRF9siGmbCJZwbqlVCvh62Uhz3BO2W+u9VmCm8=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-tooling-core/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-tooling-core-1.8.0.pom": { + "sha1": "13729d5dade099481a474cdf1fe3f5b7a2349a30", + "sha256": "sha256-OXFVCwbX4JP72YWMcimx+Dw+spFHMUYWPe1u7lNOvpI=" + }, + "kotlin-tooling-core-1.8.0.jar": { + "sha1": "0da3a4252e4b48833eb566227b2baecbaede8993", + "sha256": "sha256-yJWNlOHBIQk2wTHTiMMjfJzPLmGJ0jbJ4HppRuUPZrM=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-tooling-core/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-tooling-core-1.9.0.pom": { + "sha1": "0c1335f31eaaf304a866a1ca780bb75c05df79d1", + "sha256": "sha256-KSbD/CFJWXAcjxYNjdyGn3s/pFKv3o3RdcjnwrvHBSI=" + }, + "kotlin-tooling-core-1.9.0.jar": { + "sha1": "7b3e809d24d27d00072fd69eb130f8a6cbd365e1", + "sha256": "sha256-5TmiJCi5ysZ/dbLWHytuKnGDUNe7MYI8fcqJB/kJma4=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-tooling-metadata/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-tooling-metadata-1.6.20.pom": { + "sha1": "33155092a6256dae10dbe1c195f8af639920a409", + "sha256": "sha256-IqLaC3uENRsV0yW8AdbawRZ8lPzwRtE3/PGJD+4+NAk=" + }, + "kotlin-tooling-metadata-1.6.20.jar": { + "sha1": "b82095b34c1dd4b9c37747b5ccf135c9a0cff26b", + "sha256": "sha256-Tsk9BRYGawtki6mHxtPWX2+wZ9wLu6lcHs5h4EKEiOU=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-util-io/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-util-io-1.6.20.pom": { + "sha1": "8908b36ef994a4ad09c78374f46857a366ceb0cb", + "sha256": "sha256-NeweRLj6bGUfli0vljIh2ll1pZeSC8OuJR1cfu79OSk=" + }, + "kotlin-util-io-1.6.20.jar": { + "sha1": "e8bae8d1093cf3445c97b337cbd3841e69b73c4f", + "sha256": "sha256-265G9TdqDNAjnXvnbSCXhAIVsV2uKC4awfhYnDa7mlY=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-util-io/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-util-io-1.8.0.pom": { + "sha1": "e538af647902383ce97ca226ac8024c34342fed3", + "sha256": "sha256-xNYWCnugZBxAdVttUc0EE0E4DhzNukdSvHgFHqH36oo=" + }, + "kotlin-util-io-1.8.0.jar": { + "sha1": "1d7ca3cc3c24111ca8a12e29b10d6df0f778afd8", + "sha256": "sha256-pkmEZBeBktEVQrP+AFjpHPetq4iozZVoBokUAB4Pplw=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-util-io/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-util-io-1.9.0.pom": { + "sha1": "9481aa1ef0a147a6ca09cbf4a5f72915303185e5", + "sha256": "sha256-BUBa+xn3tgbFKr6haJqi1ew9qQL+4teoV2lDOeSgQJQ=" + }, + "kotlin-util-io-1.9.0.jar": { + "sha1": "e9b6d257f0c94bdf8a0f4ab67d6ffc199a0deba7", + "sha256": "sha256-gdNtxt4/C+rwTDl6BrVoITujc6vO9Tlyt7gvYX1a9ws=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-util-klib/1.6.20", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-util-klib-1.6.20.pom": { + "sha1": "a4aa3c02c33150f9095b621401d158f8156b364f", + "sha256": "sha256-RXmS4jfYeXJkBZQtGcOoZXqY5JBqBfBXObUCFS07s1U=" + }, + "kotlin-util-klib-1.6.20.jar": { + "sha1": "71f5149e5d19e7b3886b508508111b62d5f4b892", + "sha256": "sha256-5njoBMxViR5sBXbhE/cl44i7WTQekG3Q+AEE3v6aScE=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-util-klib/1.8.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-util-klib-1.8.0.pom": { + "sha1": "369e4fff7cbe34477c4bf0162d9ccf62e724bd4a", + "sha256": "sha256-rjMQ8QWmSF3ZpX8cUZkefiK0gtY/X/fGYaAUG3FWq4c=" + }, + "kotlin-util-klib-1.8.0.jar": { + "sha1": "256da9f306860ba60f5d74576a36e962a1697a61", + "sha256": "sha256-QIsOSGg87aZeW1vH55pT9KGs8mEKQoDRjybpZZ7MzL0=" + } + } + }, + + { + "path": "org/jetbrains/kotlin/kotlin-util-klib/1.9.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "kotlin-util-klib-1.9.0.pom": { + "sha1": "45a02a0e039fcf3ef011e38917da856894dad326", + "sha256": "sha256-acZHGaDgRjb2whMZ/lGKC1gieEu66P7YXmUfv6b4pFs=" + }, + "kotlin-util-klib-1.9.0.jar": { + "sha1": "41df7b8939660817a283d9255d5f2e4b555eebc6", + "sha256": "sha256-wtxr820zabdzdVq4k3uX5Tmk1F04Fb7xm+36FUsS0xM=" + } + } + }, + + { + "path": "org/jetbrains/markdown-jvm/0.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "markdown-jvm-0.2.1.pom": { + "sha1": "58dbe64040315d8fb1ad177f91991bb1302c7794", + "sha256": "sha256-9y2H0gc6+UPr2zvgbCi0DXpFYmm7mvUdO3DWgBKfXFk=" + }, + "markdown-jvm-0.2.1.jar": { + "sha1": "7e8952fa13f7a45739c9a5f576609a87bdcac588", + "sha256": "sha256-ohYgBBVDT1Pv8PsbOQb0yGX2ofbZzc6hWOMLgmTr1+g=" + }, + "markdown-jvm-0.2.1.module": { + "sha1": "dee5c380d27f6c66f063c96bb2e3bde516ac7335", + "sha256": "sha256-rxbUX4qnW1anM/NX4P6CaYEmeCUdXQpWG5aKGGfSH30=" + } + } + }, + + { + "path": "org/jetbrains/markdown/0.2.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "markdown-0.2.1.pom": { + "sha1": "9167aa605529a4349edf61c4bd71989f69903567", + "sha256": "sha256-BsndppePRPwUbXrdmbiZGtoF/H523PwN7xOfXfXFVnU=" + }, + "markdown-0.2.1.module": { + "sha1": "a6b370bc494ab929867e505f73cec84104ab9093", + "sha256": "sha256-33PKL0+H4PKIkhVcatSrdXJG3WOIC5oY6g5S+ZsjgT8=" + } + } + }, + + { + "path": "org/jetbrains/trove4j/trove4j/20160824", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "trove4j-20160824.pom": { + "sha1": "5be49445de412640606e639322600e584162f8ff", + "sha256": "sha256-yUsLc3kMcMcJvNRFoBJHkIiIIR44zFnpRv9v6QB4Wuk=" + }, + "trove4j-20160824.jar": { + "sha1": "33c3e174a9c8368d93761d3d12712db18e903959", + "sha256": "sha256-GReHHI3rRoMHpYRoDIekRXL1qLC5jG05f8D1+GWW2+c=" + } + } + }, + + { + "path": "org/json/json/20180813", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "json-20180813.pom": { + "sha1": "218ad36b491e585397587a361b0a6d5c973eba53", + "sha256": "sha256-x+ZgxQv07NoPyBq2K0iduRceIH+nhN3LSMbIW7QLSfU=" + }, + "json-20180813.jar": { + "sha1": "8566b2b0391d9d4479ea225645c6ed47ef17fe41", + "sha256": "sha256-UYCABJuoMYGRRBnRGiXZvJgzotcptqbnRp+lKFE1bag=" + } + } + }, + + { + "path": "org/json/json/20250107", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "json-20250107.pom": { + "sha1": "91b62e77c1b5a92b6dc79f8c0c154311e1477c85", + "sha256": "sha256-ph+87jbeXMAb7ZMEYseLwEvBZBCwHZtfEABq8UoRvFY=" + }, + "json-20250107.jar": { + "sha1": "a1b5f0b5a1ce018b93a691c57ffc7d141b3a10f1", + "sha256": "sha256-hdTBqxktMRf9Asf/8ewP5jreRc9W3vf+lQ7wYM8G6Z8=" + } + } + }, + + { + "path": "org/jsoup/jsoup/1.13.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jsoup-1.13.1.pom": { + "sha1": "ecd02e1b652931792e1b44bca21cf50828c80476", + "sha256": "sha256-s1olAHIHNjnvUdmIlWtGeXQc17RT84Rmsd+tw/1JmOs=" + }, + "jsoup-1.13.1.jar": { + "sha1": "f9577f3732bb7caa4fee8aba5053158f4010c118", + "sha256": "sha256-4rmcDS+jn2nyfvscABY5BxP+svLgLY6n8cNreAJxWYo=" + } + } + }, + + { + "path": "org/jspecify/jspecify/1.0.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jspecify-1.0.0.pom": { + "sha1": "fae4a252aeee8a649653b2f239067bcead8f80cb", + "sha256": "sha256-zauSmjuVIR9D0gkMXi0N/oRllg43i8MrNYQdqzJEM6Y=" + }, + "jspecify-1.0.0.jar": { + "sha1": "7425a601c1c7ec76645a78d22b8c6a627edee507", + "sha256": "sha256-H61ua+dVd4Hk0zcp1Jrhzcj92m/kd7sMxozjUer9+6s=" + }, + "jspecify-1.0.0.module": { + "sha1": "9c18ac54a30620fdc216c01faf87c0f7effbeec7", + "sha256": "sha256-0wfKd6VOGKwe8artTlu+AUvS9J8p4dL4E+R8J4KDGVs=" + } + } + }, + + { + "path": "org/junit/junit-bom/5.13.0-M2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "junit-bom-5.13.0-M2.pom": { + "sha1": "40468bb34f205623b20a344b18aa7dac00aef592", + "sha256": "sha256-nyDgJlKIVEWkG94IZyunz6EOnoDbzCX73sMN4N0UVaw=" + }, + "junit-bom-5.13.0-M2.module": { + "sha1": "e8cf156e03d6a91383f9373f354b0116aa1fe846", + "sha256": "sha256-wtYfC8meJi13RjJjFLys/IfQletrDCnc9Lb0N8K/nHo=" + } + } + }, + + { + "path": "org/jvnet/staxex/stax-ex/1.7.7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stax-ex-1.7.7.pom": { + "sha1": "797e24598297af973b622e9a41662d0bbc497658", + "sha256": "sha256-J8tFC2w2egC9g2JRmQf4Te+sexIIbGTU1ul+M6uHPq0=" + }, + "stax-ex-1.7.7.jar": { + "sha1": "18bed5a0da27a6b43efe01282f2dc911b1cb3a72", + "sha256": "sha256-ox/313FjwN6wnn/uWa01rkTCzuLMhVKhFszRWD2BP7Q=" + } + } + }, + + { + "path": "org/jvnet/staxex/stax-ex/1.8.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stax-ex-1.8.1.pom": { + "sha1": "93132b1ebaf1c419ad3d2216924af39c68f43b51", + "sha256": "sha256-j8hPNs5tps6MiTtlOBmaf2mmmgcG2bF6PuajoJRS7tY=" + }, + "stax-ex-1.8.1.jar": { + "sha1": "78011e483a21102fb4858f3e8f269a677e50aa23", + "sha256": "sha256-IFIlSQVunlCqNe8LRFouR6U9Br4LCpRn1wTiSD/7BJo=" + } + } + }, + + { + "path": "org/jvnet/staxex/stax-ex/1.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stax-ex-1.8.pom": { + "sha1": "cc7022b896125220e51f46fa50f4b68e564ffec1", + "sha256": "sha256-CoTCDPcfaj0h/iJrDViDMvx64+kMtYPGCkgzF+ufNkQ=" + }, + "stax-ex-1.8.jar": { + "sha1": "8cc35f73da321c29973191f2cf143d29d26a1df7", + "sha256": "sha256-lbBdlZCvQVTGUTucXcH7LlW1OZcroKnvKOmgwB2DrXc=" + } + } + }, + + { + "path": "org/jvnet/staxex/stax-ex/2.1.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "stax-ex-2.1.0.pom": { + "sha1": "4a982ee38f283642f4ad5b884472f140c66b1fa0", + "sha256": "sha256-UUtTM5uRtQb2fyzg6El6tF2jThyeS/S96iFNJlykAyQ=" + }, + "stax-ex-2.1.0.jar": { + "sha1": "33160568d70c01da407f8ba982bacf283d00ad4a", + "sha256": "sha256-n3hqtSOSEGpTSRvR3di9kCjJW7KA4wOHtw1JioZHzzU=" + } + } + }, + + { + "path": "org/mockito/mockito-core/3.12.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mockito-core-3.12.4.pom": { + "sha1": "e159a2a2e92b2410cd76a4cde41a0d63abe82883", + "sha256": "sha256-8AK44cJMjXozH5kVpTumW1lFT8Lqqf15b9m/z8HLCv4=" + }, + "mockito-core-3.12.4.jar": { + "sha1": "f9cdc14ea4a3573c0c0366d47d5ca960be24ddb6", + "sha256": "sha256-ddSxS6eu+DbpK6ey1Tyn1rIV3X21Ylr7w5JS8TWINf4=" + } + } + }, + + { + "path": "org/mockito/mockito-core/4.0.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mockito-core-4.0.0.pom": { + "sha1": "f938feea908f731accfbfa4c1a73952413e5c3e6", + "sha256": "sha256-kUbxR0g66d/bIm65KOgZJhIvrcFmVkgmLm4JXRV2Lr8=" + }, + "mockito-core-4.0.0.jar": { + "sha1": "f5195e0c4a45716bbd2d1d29173adbd148acce3a", + "sha256": "sha256-kEY0zLMsHZEuUo6WSlBm3VmSFjsGSw5yB7w103NNEI0=" + } + } + }, + + { + "path": "org/mockito/mockito-core/5.17.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mockito-core-5.17.0.pom": { + "sha1": "021386b7e176aa5b77d44021929e714842749898", + "sha256": "sha256-0BzBTnZhxjBHlApC9Qc9Sg7L4qDqXS6jQgS0zAgeFqU=" + }, + "mockito-core-5.17.0.jar": { + "sha1": "f5fe5a2f94eb65f4f83ca0607bfe13ec0d9c6f3b", + "sha256": "sha256-3/Wa2MYbAm74bMET+U8jAesGyq+B3sMMeKlqGmVZXF4=" + } + } + }, + + { + "path": "org/mockito/mockito-inline/4.0.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "mockito-inline-4.0.0.pom": { + "sha1": "92c74395f5a4bd045cadac4ac02cdbb3c1abb601", + "sha256": "sha256-e6bgcsdtJNO+jpyZKcERWmn6nHHVPZCGXLNMymzO+wU=" + }, + "mockito-inline-4.0.0.jar": { + "sha1": "3d1dffee9a8a1998ec782383ca2f818848f2d5f1", + "sha256": "sha256-7lLhwpmmMhhPuidKk3CZPgkUBCn15RbmxVcP1ldLKX8=" + } + } + }, + + { + "path": "org/objenesis/objenesis-parent/3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "objenesis-parent-3.2.pom": { + "sha1": "833ced74e7fd53408a3915080c5e50f2dc90dc35", + "sha256": "sha256-CFm6DVz+77E5ZOWGLANvqkigvM/0kyY4+xP+NEXfM/c=" + } + } + }, + + { + "path": "org/objenesis/objenesis-parent/3.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "objenesis-parent-3.3.pom": { + "sha1": "60b46fea1dc4cb9c3123f8c366f8b33d0c774fa3", + "sha256": "sha256-MFw4SqLx4cf+U6ltpBw+w1JDuX1CjSSo93mBjMEL5P8=" + } + } + }, + + { + "path": "org/objenesis/objenesis-parent/3.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "objenesis-parent-3.4.pom": { + "sha1": "840b7af780a7f577e2132a7b60f38c691cb7beff", + "sha256": "sha256-yx9D0pjMhUu1W5k9MwAX20qUxmCZgtqBDwOTc/xiVKY=" + } + } + }, + + { + "path": "org/objenesis/objenesis/3.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "objenesis-3.2.pom": { + "sha1": "d6bda29a6a26aa5207ab880a4f09870aff20e3c6", + "sha256": "sha256-ThCt8Ud7jHEbRbihXrlg9rRIcfXh3vOPFmgTSKoWaww=" + }, + "objenesis-3.2.jar": { + "sha1": "7fadf57620c8b8abdf7519533e5527367cb51f09", + "sha256": "sha256-A9lgvVrvA8ZT6wAEE62hXrd83SuOREiIbt9WkoBeNfM=" + } + } + }, + + { + "path": "org/objenesis/objenesis/3.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "objenesis-3.3.pom": { + "sha1": "8da1208285232e541d0bbb869bbe66af6ec6abb4", + "sha256": "sha256-ugxA2iZpoEi24k73BmpHHw+8v8xQnmo+hWyk3fphStM=" + }, + "objenesis-3.3.jar": { + "sha1": "1049c09f1de4331e8193e579448d0916d75b7631", + "sha256": "sha256-At/QsEOaVZHjW3CO0vVHTrCUj1Or90Y36Vm45O9pv+s=" + } + } + }, + + { + "path": "org/objenesis/objenesis/3.4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "objenesis-3.4.pom": { + "sha1": "14fd7183376cab8cec3d9980101d37269f864663", + "sha256": "sha256-YBfsDB6B9z2aEuv8CcMoI80HXocJWTcU60AOLwE9ilg=" + }, + "objenesis-3.4.jar": { + "sha1": "675cbe121a68019235d27f6c34b4f0ac30e07418", + "sha256": "sha256-lUiBAv6vLihYrfaymTU2d9rGwVKUAG+O0cVVb4480lE=" + } + } + }, + + { + "path": "org/ow2/asm/asm-analysis/5.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-analysis-5.0.3.pom": { + "sha1": "2270e3099f178e9562a92c6eb27a7ad12e3fa850", + "sha256": "sha256-n/wCobw9hGKTrZ+Inbn0LQkg6VOsxMSCVyb/AxQFp9A=" + }, + "asm-analysis-5.0.3.jar": { + "sha1": "c7126aded0e8e13fed5f913559a0dd7b770a10f3", + "sha256": "sha256-6PoqY0YsllV9zTbCVSXhJkt3Nm/4Uc8LlOt1krKQhJ0=" + } + } + }, + + { + "path": "org/ow2/asm/asm-analysis/5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-analysis-5.1.pom": { + "sha1": "529d264690bf03a432e29ae546fdc960a176839c", + "sha256": "sha256-gRINtNVJDE8Ol4nhF40rrCMfz/Jhi9gPjvb/A8VQ1Zs=" + }, + "asm-analysis-5.1.jar": { + "sha1": "6d1bf8989fc7901f868bee3863c44f21aa63d110", + "sha256": "sha256-o0ZY9cXeS1c+7yETHMMswl97ZkB5RPMSso7C5Wq7H6k=" + } + } + }, + + { + "path": "org/ow2/asm/asm-analysis/6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-analysis-6.0.pom": { + "sha1": "f6128ea377c7d740fdc8db304689a37cd5374019", + "sha256": "sha256-06r0LkDj4kqr2lO13k3l4Xy3jgZUm/NzEtymx34lEFQ=" + }, + "asm-analysis-6.0.jar": { + "sha1": "dd1cc1381a970800268160203aae2d3784da779b", + "sha256": "sha256-LxpjhyGcOmzEhWSB8iGwO9nyQIoybUFq8Jr11vYIwfQ=" + } + } + }, + + { + "path": "org/ow2/asm/asm-analysis/7.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-analysis-7.0.pom": { + "sha1": "20186ccd5304839aea81893ebc65addc6ad4ca5e", + "sha256": "sha256-xrVEd+nVuuHnrd/y4ky/kqr/L/CP1rwFlsOTPD+twss=" + }, + "asm-analysis-7.0.jar": { + "sha1": "4b310d20d6f1c6b7197a75f1b5d69f169bc8ac1f", + "sha256": "sha256-6YH49lDE2QC7AzZQsY4SL6axYerdX4iXjQh1H3LuhHQ=" + } + } + }, + + { + "path": "org/ow2/asm/asm-analysis/8.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-analysis-8.0.1.pom": { + "sha1": "3b385ab5eb545638275f63e2deafe882648c74cb", + "sha256": "sha256-/MNrlkrvVZM6NjI13v74VjP8FEkYHTqQ4pYdGW8aicg=" + }, + "asm-analysis-8.0.1.jar": { + "sha1": "6c2c7b3006b3a54dd63467a020263c0756fd5e80", + "sha256": "sha256-/GjSfQOuJhgFI84g44fbAL0P3uxU/KJNKZBZLAr60RI=" + } + } + }, + + { + "path": "org/ow2/asm/asm-analysis/9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-analysis-9.1.pom": { + "sha1": "e30004dc53274dc96d0b9c6a3ac06bdcf6d23d86", + "sha256": "sha256-rFRUwRsDQxypUd9x+06GyMTIDfaXn5W3V8rtOrD0cVY=" + }, + "asm-analysis-9.1.jar": { + "sha1": "4f61b83b81d8b659958f4bcc48907e93ecea55a0", + "sha256": "sha256-gaiAQbG4vtpaiplkYJgEbEhwlTgnDEne9oq/8lrDvjQ=" + } + } + }, + + { + "path": "org/ow2/asm/asm-analysis/9.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-analysis-9.2.pom": { + "sha1": "467a33724d063fedc0a041e68af8fb17c06ebd94", + "sha256": "sha256-dzzBor/BTGxKl5xRoHXAI0oL9pT8Or5PrPRU83oUXxs=" + }, + "asm-analysis-9.2.jar": { + "sha1": "7487dd756daf96cab9986e44b9d7bcb796a61c10", + "sha256": "sha256-h4++UhcxwHLRTS1luYOxvq5q0G/aAAe2qLroH3P0M8Q=" + } + } + }, + + { + "path": "org/ow2/asm/asm-bom/9.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-bom-9.8.pom": { + "sha1": "71634eea1f95b1a4f3dd504d74d819ae3cf284c0", + "sha256": "sha256-DaHcsibmzf2ttNrFkZFruRe1c3RnTZG9LxMAMTe0FSA=" + } + } + }, + + { + "path": "org/ow2/asm/asm-commons/5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-commons-5.1.pom": { + "sha1": "99b522fe5920cdfddee2ce51053ba9b4ba2748ac", + "sha256": "sha256-zTazHxSV9P5VqTbdAGB6vdlF4fgHRUkiWgTEAtHTV1A=" + }, + "asm-commons-5.1.jar": { + "sha1": "25d8a575034dd9cfcb375a39b5334f0ba9c8474e", + "sha256": "sha256-l7N4bh9V50vd+K0QK/UOM7vLwfa3/Xs28Lu7Jc1Jgb4=" + } + } + }, + + { + "path": "org/ow2/asm/asm-commons/6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-commons-6.0.pom": { + "sha1": "f434c622e92b7ad7048706b78c5ce014e74a7e8e", + "sha256": "sha256-kPTylHOvuVcikBbrV0GVSXbMG2e8BJs5gcDR5lOtXN0=" + }, + "asm-commons-6.0.jar": { + "sha1": "f256fd215d8dd5a4fa2ab3201bf653de266ed4ec", + "sha256": "sha256-8bzlxkipagF73NAf5dWa+YRSl/17ebgcAVpvu9lxmr8=" + } + } + }, + + { + "path": "org/ow2/asm/asm-commons/7.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-commons-7.0.pom": { + "sha1": "7b5da826fcab00989f748b7c4f72e75e668da210", + "sha256": "sha256-9MaXiGzbSlskcgVKC140Nx6bSOYgvkDD7Ujh9LbVHrQ=" + }, + "asm-commons-7.0.jar": { + "sha1": "478006d07b7c561ae3a92ddc1829bca81ae0cdd1", + "sha256": "sha256-/tNI7wWVjj6EajrAdKEq9feTbvPSHORKYsT6CKdxkn0=" + } + } + }, + + { + "path": "org/ow2/asm/asm-commons/8.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-commons-8.0.1.pom": { + "sha1": "420ad7e8da15a1986cd594c86146d017c37b516b", + "sha256": "sha256-u07PqJWm7Pb/QnzOQNzBAHaDUb4HkBxBc73OgUOz6lE=" + }, + "asm-commons-8.0.1.jar": { + "sha1": "019c7ba355f0737815205518e332a8dc08b417c6", + "sha256": "sha256-y5Qi3x+YqIa2CLJzGNmQPWx/4oE1IdVFLIZTHsPAK58=" + } + } + }, + + { + "path": "org/ow2/asm/asm-commons/9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-commons-9.1.pom": { + "sha1": "5914a98fed0e76a0ce92a75704cacaf2454ae90a", + "sha256": "sha256-oPZRsnuK/pwOYS16Ambqy197HHh7xLWsgkXz16EYG38=" + }, + "asm-commons-9.1.jar": { + "sha1": "8b971b182eb5cf100b9e8d4119152d83e00e0fdd", + "sha256": "sha256-r8sm3B/BLAxKma2mcJCN2C4Y38SIyvXuklRplrRwwAw=" + } + } + }, + + { + "path": "org/ow2/asm/asm-commons/9.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-commons-9.2.pom": { + "sha1": "8ee5279931bc860b7dd3a764c67ac1a7cb2bc07c", + "sha256": "sha256-AoJOg58qLw5ylZ/dMLSJckDwWvxD3kLXugsYQ3YBwHA=" + }, + "asm-commons-9.2.jar": { + "sha1": "f4d7f0fc9054386f2893b602454d48e07d4fbead", + "sha256": "sha256-vkzlMTiiOLtSLNeBz5Hzulzi9sqT7GLUahYqEnIl4KY=" + } + } + }, + + { + "path": "org/ow2/asm/asm-commons/9.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-commons-9.8.pom": { + "sha1": "8f0aaa4f4079d6100a983c2ac50dfa8cdccda867", + "sha256": "sha256-95PnjwH3A3F9CUcuVs3yEv4piXDIguIRbo5Un7bRQMI=" + }, + "asm-commons-9.8.jar": { + "sha1": "36e4d212970388e5bd2c5180292012502df461bb", + "sha256": "sha256-MwGhwctMWfzFKSZI2sHXxa7UwPBn376IhzuM3+d0BPQ=" + } + } + }, + + { + "path": "org/ow2/asm/asm-debug-all/5.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-debug-all-5.0.1.pom": { + "sha1": "e536746490b031d435e1bfaa17a8d85f77973412", + "sha256": "sha256-H59eVjfKgblNuIbSn1ZMj0CBVHddD86+WEPM6hpaVX8=" + }, + "asm-debug-all-5.0.1.jar": { + "sha1": "f69b5f7d96cec0d448acf1c1a266584170c9643b", + "sha256": "sha256-RzTeW1FaRUsAlttpcfsGjl9w5vELvuKzvS/f5dl47Vc=" + } + } + }, + + { + "path": "org/ow2/asm/asm-debug-all/6.0_BETA", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-debug-all-6.0_BETA.pom": { + "sha1": "355a030ac2f0b90c7d6d047ac4743f2c24a91166", + "sha256": "sha256-jrXpdPiai94AZbtuxG6GR9TTl7NOEl8UrOj7XHi29gQ=" + }, + "asm-debug-all-6.0_BETA.jar": { + "sha1": "5d1f7d145b0094c3955dff56d3119d7bd9807aff", + "sha256": "sha256-U2qNnHxOonrkuS0okmVGwwXRuBn51SzJo3KMgosDLv0=" + } + } + }, + + { + "path": "org/ow2/asm/asm-parent/5.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-parent-5.0.1.pom": { + "sha1": "fbd79905ab811db2c82640f58ddbd22807cfb29d", + "sha256": "sha256-mITALUckQ2Th+ydnW17zsgai0tMUGsd/V/13GhdcKi8=" + } + } + }, + + { + "path": "org/ow2/asm/asm-parent/5.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-parent-5.0.3.pom": { + "sha1": "f2b915adcf47fab0e17bccf47390aa206eba7937", + "sha256": "sha256-wu2r9BKKU030uLSwubVi6U8kK6lawk3GFIVDK4oYjjI=" + } + } + }, + + { + "path": "org/ow2/asm/asm-parent/5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-parent-5.1.pom": { + "sha1": "2768685ec9f3a387a328e4851c36716de2b34720", + "sha256": "sha256-heyCWPr8KD9hZH5qEbQEiQghwR22/PVQd57PehQVEAc=" + } + } + }, + + { + "path": "org/ow2/asm/asm-parent/6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-parent-6.0.pom": { + "sha1": "6a030ede2a62bdde4a6b23ef342994703a11ea24", + "sha256": "sha256-eR0GS7nsyaRtQ7ye/s10yRRk38RR8yG4AtImHizNfRQ=" + } + } + }, + + { + "path": "org/ow2/asm/asm-parent/6.0_BETA", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-parent-6.0_BETA.pom": { + "sha1": "17f1e886ce6311f3116d0acd2f1854fd79bad267", + "sha256": "sha256-aHet/hYUNwgyK5jk0jq5wql2G+wC1HakYA85vod9bgk=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/5.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-5.0.3.pom": { + "sha1": "67e01f0d7339cdeaf709be24a7c333f396005e8e", + "sha256": "sha256-ZYGcp8WiGP9J+OvnLzs5IZyB8TfQF6zBrsFbEFTPUjY=" + }, + "asm-tree-5.0.3.jar": { + "sha1": "287749b48ba7162fb67c93a026d690b29f410bed", + "sha256": "sha256-NHp6lAD5lk6HyR05gOSO69yNAkvDs29/IhicZihTpRw=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-5.1.pom": { + "sha1": "09e0a8798bb85ba11a36f21f2a0888bdd87d4ba4", + "sha256": "sha256-+1Np/H6ksuPPjW8gJD8dl/gTQb+Bixec2NvR2np4GiE=" + }, + "asm-tree-5.1.jar": { + "sha1": "87b38c12a0ea645791ead9d3e74ae5268d1d6c34", + "sha256": "sha256-wN4rvEy4KXQZZZgT7NTtHQd+0d1cH1VEzFFD5JPoTBA=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-6.0.pom": { + "sha1": "b318fdac4c1b914798862a4ab478eae81cc308d2", + "sha256": "sha256-Noh+DX/bGFqSN4o46kXllql/EPOFxdBQ62JSRU7nAbQ=" + }, + "asm-tree-6.0.jar": { + "sha1": "a624f1a6e4e428dcd680a01bab2d4c56b35b18f0", + "sha256": "sha256-iHmY+2lyfIdZ5NJT+FaCKAHjP5/UyqVms6xY7pIQYhU=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/7.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-7.0.pom": { + "sha1": "9a26ff62a5b01e4fa15ce96fcdb78738817d3066", + "sha256": "sha256-05590S9P9TWgg50ZScOcdkQ1WkRwIgyUt2pcFoxXoGg=" + }, + "asm-tree-7.0.jar": { + "sha1": "29bc62dcb85573af6e62e5b2d735ef65966c4180", + "sha256": "sha256-z9egh0+d42qZnBJ/7q37/m4E1Kce6VTXrz2FPwvkimw=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/8.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-8.0.1.pom": { + "sha1": "068072947820842b1a530ba2abbe9650debfc732", + "sha256": "sha256-RA7VtH4VNv0RMPwLgFfHq2tAZiYOZHCftcOThgypKVM=" + }, + "asm-tree-8.0.1.jar": { + "sha1": "dfcad5abbcff36f8bdad5647fe6f4972e958ad59", + "sha256": "sha256-8yGytrpgkgfceuSQe9mo5jn5dc5i2QI5rNk1OZTRyBE=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-9.1.pom": { + "sha1": "aeef4f505fe557517394dc29e404fba6ba98c428", + "sha256": "sha256-tqANkgfANUYPgcfXDtQSU/DSFmUr7UX6GjBS/81QuUw=" + }, + "asm-tree-9.1.jar": { + "sha1": "c333f2a855069cb8eb17a40a3eb8b1b67755d0eb", + "sha256": "sha256-/QCvpJ6VlddkYgWwnOy0p3ao/wugby1ZuPe/nHBLSnM=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/9.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-9.2.pom": { + "sha1": "2e168ad097e0b56f1aa6695a80982b8f455b1ba8", + "sha256": "sha256-9h8+vqVSDd8Z9FKwPEJscjG92KgdesKHZctScSJaw3g=" + }, + "asm-tree-9.2.jar": { + "sha1": "d96c99a30f5e1a19b0e609dbb19a44d8518ac01e", + "sha256": "sha256-qr+b0jCRpOv8EJwfPufPPkuJ9rotP1HFJD8Ws8/64BE=" + } + } + }, + + { + "path": "org/ow2/asm/asm-tree/9.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-tree-9.8.pom": { + "sha1": "11540e16bb184feb827b4f746b4c662d0c93bbda", + "sha256": "sha256-cUnn+qDhkSlvh5ru2SCciULTmPBpjSzKGpxijy4qj3c=" + }, + "asm-tree-9.8.jar": { + "sha1": "018419ca5b77a2f81097c741e7872e6ab8d2f40d", + "sha256": "sha256-FLeIDLfIXu0QHicQQy/D/7gydVMqaolNxMQJXUmtWfE=" + } + } + }, + + { + "path": "org/ow2/asm/asm-util/5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-util-5.1.pom": { + "sha1": "f5b08655a7056cdf8573524347706bae44f6707f", + "sha256": "sha256-DA6dGFfCUNHXG1xr57jMKa/lXnQZqQ0Tu+lorlvXJA4=" + }, + "asm-util-5.1.jar": { + "sha1": "b60e33a6bd0d71831e0c249816d01e6c1dd90a47", + "sha256": "sha256-7gMsOa5ePNCZFI+7qaIST57WE+XLk+A+4PqICM42QEA=" + } + } + }, + + { + "path": "org/ow2/asm/asm-util/6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-util-6.0.pom": { + "sha1": "00a631614520a8655772955f43dcc80db071230e", + "sha256": "sha256-PN/GH5iNFnuXUDQZ2oKlgIsg9CBa60bDlObn/9MhHxI=" + }, + "asm-util-6.0.jar": { + "sha1": "430b2fc839b5de1f3643b528853d5cf26096c1de", + "sha256": "sha256-NWr+vbD4cBdSYuUYj4cJo7F6oqWmpLA0CwTUtEm8pfY=" + } + } + }, + + { + "path": "org/ow2/asm/asm-util/7.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-util-7.0.pom": { + "sha1": "0944ed3d072e935fe950cb08e6e5fabbec76d81b", + "sha256": "sha256-4HvOS7VdWgb0wQ2RL8ne6Km5wE7FSbu4208g2zRwb3U=" + }, + "asm-util-7.0.jar": { + "sha1": "18d4d07010c24405129a6dbb0e92057f8779fb9d", + "sha256": "sha256-dfu8pEDvRj9BwrCrGoCr5n6RCsSG2mCnhjy8tbrn4UU=" + } + } + }, + + { + "path": "org/ow2/asm/asm-util/8.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-util-8.0.1.pom": { + "sha1": "59de583ec1a4b626a8cec2347c9f0421d1de3b7f", + "sha256": "sha256-woLo11yKuJLnBrlOs78kLtAYhzl3YAFSjygBqklRbrE=" + }, + "asm-util-8.0.1.jar": { + "sha1": "a8f978fbad00c565210bebedb28e5d6f73327134", + "sha256": "sha256-N6U25KnA5sJ0mLRO7fSws/wzGLAyulihyDDGMrLCJZc=" + } + } + }, + + { + "path": "org/ow2/asm/asm-util/9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-util-9.1.pom": { + "sha1": "b16ab6c7727a796b232f09124a895567dbf7d134", + "sha256": "sha256-jd108aHiuTxwnZdtAgXnT7850AVwPJYmpe1cxXTK+88=" + }, + "asm-util-9.1.jar": { + "sha1": "36464a45d871779f3383a8a9aba2b26562a86729", + "sha256": "sha256-OA4uzRb3zA8adrqboEkXm1dgpXsoKoekxlPK7/LNW9Y=" + } + } + }, + + { + "path": "org/ow2/asm/asm-util/9.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-util-9.2.pom": { + "sha1": "f0b78aadcb036018a9885fdf461161d09717f2f9", + "sha256": "sha256-3dBpE/FH1wrmjnpuQ1alWzPxTd5hYtv/K9DiiVgfGtI=" + }, + "asm-util-9.2.jar": { + "sha1": "fbc178fc5ba3dab50fd7e8a5317b8b647c8e8946", + "sha256": "sha256-/1s80zGuipqAR2goDamPUPQk/vI908eIuzIOCMlO5Zg=" + } + } + }, + + { + "path": "org/ow2/asm/asm/5.0.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-5.0.3.pom": { + "sha1": "7d9570aceff0131a35a87d37b53452be33cf3cd9", + "sha256": "sha256-fTRlP/5ivncU0vGWlmChffnsGxnYJQxPA9sRI8grpqA=" + }, + "asm-5.0.3.jar": { + "sha1": "dcc2193db20e19e1feca8b1240dbbc4e190824fa", + "sha256": "sha256-ccT3jkN7j9zZzA39Kr6owInrZ3AFpqXP8yAgbMUrRsw=" + } + } + }, + + { + "path": "org/ow2/asm/asm/5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-5.1.pom": { + "sha1": "87afb3c6d9329d889ef8dc7bded4a5482cb15e99", + "sha256": "sha256-bQebWr858yO6rMxoRjvu9c8ulXTzM910mulnU/AOsgU=" + }, + "asm-5.1.jar": { + "sha1": "5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45", + "sha256": "sha256-0to5mplnxp8KIXOSVvp50oQiLCIwgsrK3Bc3IkR2S1Q=" + } + } + }, + + { + "path": "org/ow2/asm/asm/6.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-6.0.pom": { + "sha1": "8e8ac765fb0b5099bde117857df7f7cc7aa08165", + "sha256": "sha256-wrxJfhlxEuaZCFQmu7RbGrC+gALaaCZ6Ox9FCgMSwy4=" + }, + "asm-6.0.jar": { + "sha1": "bc6fa6b19424bb9592fe43bbc20178f92d403105", + "sha256": "sha256-3Ylxx0pOaXiZqOlcquTqh2DqbEhtxrl7F5XnV2BCBGE=" + } + } + }, + + { + "path": "org/ow2/asm/asm/7.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-7.0.pom": { + "sha1": "c6b547ce67397df979bde680a1d0cdf8ceb63f6a", + "sha256": "sha256-g/ZbEIPVzk+Lp/lUXP6f8XgkWJyafMgsOkaVgB5PX2g=" + }, + "asm-7.0.jar": { + "sha1": "d74d4ba0dee443f68fb2dcb7fcdb945a2cd89912", + "sha256": "sha256-uI72ZGizyXitDJf9bpCXnlYVW0rGkIm6ekTpqn/+ms8=" + } + } + }, + + { + "path": "org/ow2/asm/asm/8.0.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-8.0.1.pom": { + "sha1": "c89003a7d34cfde2f6a11509df0c7d1cd90ac7cd", + "sha256": "sha256-rbpBTPqwkbu6XTxWvmGW7U9iTK2TLYcEPQK6VquuNKw=" + }, + "asm-8.0.1.jar": { + "sha1": "3f5199523fb95304b44563f5d56d9f5a07270669", + "sha256": "sha256-yluNEVaeU5IbDjSGRp58Z0Nhx5hF2tPVFPOKtuDIwQo=" + } + } + }, + + { + "path": "org/ow2/asm/asm/9.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-9.1.pom": { + "sha1": "927153d04a920d5f06a9a4aefd5f6be60726117d", + "sha256": "sha256-xoOpDdaPKxeIy9/EZH6pQF71kls3HBmfj9OdRNPO3o0=" + }, + "asm-9.1.jar": { + "sha1": "a99500cf6eea30535eeac6be73899d048f8d12a8", + "sha256": "sha256-zaTeRV+rSP8Ly3xItGOUR9TehZp6/DCglKmG8JNr66I=" + } + } + }, + + { + "path": "org/ow2/asm/asm/9.2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-9.2.pom": { + "sha1": "b24408a1c2214bc57380faf45b37bd67be7732b5", + "sha256": "sha256-37EqGyJL8Bvh/WBAIEZviUJBvLZF3M45Xt2M1vilDfQ=" + }, + "asm-9.2.jar": { + "sha1": "81a03f76019c67362299c40e0ba13405f5467bff", + "sha256": "sha256-udT+TXGTjfOIOfDspCqqpkz4sxPWeNoDbwyzyhmbR/U=" + } + } + }, + + { + "path": "org/ow2/asm/asm/9.7.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-9.7.1.pom": { + "sha1": "381a4b6892969305d88ee13c53e88eabff82d382", + "sha256": "sha256-cimwOzCnPukQCActnkVppR2FR/roxQ9SeEGu9MGwuqg=" + }, + "asm-9.7.1.jar": { + "sha1": "f0ed132a49244b042cd0e15702ab9f2ce3cc8436", + "sha256": "sha256-jK3UOsXrbQneBfrsyji5F6BAu5E5x+3rTMgcdAtxMoE=" + } + } + }, + + { + "path": "org/ow2/asm/asm/9.8", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "asm-9.8.pom": { + "sha1": "420ea5b64e01f336ce294fac8d17cc50e2c8c08a", + "sha256": "sha256-wTZ8O7OD12Gef3l+ON91E4hfLu8ErntZCPaCImV7W6o=" + }, + "asm-9.8.jar": { + "sha1": "dc19ecb3f7889b7860697215cae99c0f9b6f6b4b", + "sha256": "sha256-h26raoPa7K1cpn65/KuwY8l7WuuM8fynqYns3hdSIFE=" + } + } + }, + + { + "path": "org/ow2/ow2/1.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ow2-1.3.pom": { + "sha1": "f679d6639bfb209b0836a5e7cf09bfbcc1a41f06", + "sha256": "sha256-USFcZ9LAaNi30vb4D1E3KgmAdd7MxEjUvde5h7qDKPs=" + } + } + }, + + { + "path": "org/ow2/ow2/1.5.1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ow2-1.5.1.pom": { + "sha1": "bda66fa5f1b68fa7d2de3d569bdc8508b2af82d4", + "sha256": "sha256-Mh3bt+5v5PU96mtM1tt0FU1r+kI5HB92OzYbn0hazwU=" + }, + "ow2-1.5.1-source-release.tar.gz": { + "sha1": "b6b20bbaf8a8d6359da514f547dd0c902ea40b27", + "sha256": "sha256-82ryCYIQKGkvlMEzrmiDS9ZpiqDGaYSwOiQdRAp7K0s=" + }, + "ow2-1.5.1-source-release.tar.gz.asc": { + "sha1": "3aa93d36160a12ca826be1910ab551d9079b6f8f", + "sha256": "sha256-Cg5aqe5c4aKCWXAK+vd4/GRYRUT8D0oXZnJG1T7PNcI=" + } + } + }, + + { + "path": "org/ow2/ow2/1.5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "ow2-1.5.pom": { + "sha1": "d8edc69335f4d9f95f511716fb689c86fb0ebaae", + "sha256": "sha256-D4obEW52C4/mOJxRuE5LB6cPwRCC1Pk25FO1g91QtDs=" + } + } + }, + + { + "path": "org/robolectric/annotations/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "annotations-4.7.3.pom": { + "sha1": "973a881c265453a00ac1b4ec2b8547260af64b0b", + "sha256": "sha256-7edUGoZXT9rjvfVFguRBoGPPs00lMIgFh5bQTi/A9VE=" + }, + "annotations-4.7.3.jar": { + "sha1": "ce248bbc24447bc2acfbababc51a100570c0e8cf", + "sha256": "sha256-5GNOM8VyQM9XxoxMx94sqo0lGVpRFFhr9+NX7lyiQDk=" + }, + "annotations-4.7.3.module": { + "sha1": "5919bd7a30d03635c59f81755bb7ab1ca159c26d", + "sha256": "sha256-f7J/6gRZTZaajKs3o0QfvIXQCCIsJ8zrCFbsa0mmbRQ=" + } + } + }, + + { + "path": "org/robolectric/junit/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "junit-4.7.3.pom": { + "sha1": "c73d764cee7cd09ce575f1bdda6d04eeef52b462", + "sha256": "sha256-NzViBhayE9lEKxHSW0C2l31kzIDBeYX+htchksrSudk=" + }, + "junit-4.7.3.jar": { + "sha1": "687caec5d95c48f59c5b6168f8cb1472b1757827", + "sha256": "sha256-u14owZ6jS1kLl0U2d1YWhBCXHgWxkDtdzOBZ0ZYp8HQ=" + }, + "junit-4.7.3.module": { + "sha1": "211fbb0f6640ef2d8982f0c36c30ccd9faebeda9", + "sha256": "sha256-Ov8T6C+YzzXf9fKhCOuYUaORELhFZAUs/iZ4MPorlcQ=" + } + } + }, + + { + "path": "org/robolectric/nativeruntime/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "nativeruntime-4.7.3.pom": { + "sha1": "ed211ce28e6e4d8a9f3cf65e3e359a00b0d60568", + "sha256": "sha256-6ohfCuFZzo0ktkpgeLLdJh2HUO/rlU1ZL6VAFbt/FnI=" + }, + "nativeruntime-4.7.3.jar": { + "sha1": "4f5ba41c2a5c064fb82d0f30fcc03f1300d913b5", + "sha256": "sha256-mdnKL4qAHXb4OhYY7irsToPKyfVH+WmYJ5QnCLhOajk=" + }, + "nativeruntime-4.7.3.module": { + "sha1": "17fc374472984dcf4462cd6d289e941aa06509fb", + "sha256": "sha256-pvOwJN4ZyQJ70hprjn3M/PhAxymPdUoBkc3m/NnreOA=" + } + } + }, + + { + "path": "org/robolectric/pluginapi/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "pluginapi-4.7.3.pom": { + "sha1": "6497a4d0ed24c55734a0805c101a979a23417e40", + "sha256": "sha256-PazyvaDX8HENzJGGtocJy38TmUnpyzuLVmYcGCfv25k=" + }, + "pluginapi-4.7.3.jar": { + "sha1": "56e53ba922352fac89d1851206ba95fa7ea13452", + "sha256": "sha256-V2ZAe6qsDrpamGRH06B7cKogUlj8RhUrg+dDrJ5aq0s=" + }, + "pluginapi-4.7.3.module": { + "sha1": "7942e73f4de06ea74337f01b59d12e1d0d410a54", + "sha256": "sha256-uhCmI+3g0aJdPuLMMSGy9CnKHr9/MTJbojo89+UZx8Q=" + } + } + }, + + { + "path": "org/robolectric/plugins-maven-dependency-resolver/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "plugins-maven-dependency-resolver-4.7.3.pom": { + "sha1": "8abca54cb14fb3d5a2992c700010e3ef9bc11a9c", + "sha256": "sha256-R5Tukhvi22hkDgliG/pI8Y85v54bVhVSI8cWt3Wb/+0=" + }, + "plugins-maven-dependency-resolver-4.7.3.jar": { + "sha1": "e9ad43068bd4bc6e967879dbacd2a450e2a0b994", + "sha256": "sha256-mJll3F+olo/9zS7uC1tJnkIz+V0PfuX6Pdxvedy/tAw=" + }, + "plugins-maven-dependency-resolver-4.7.3.module": { + "sha1": "ca6d8dbf4a2acca6944a867e83970fb2e45671e4", + "sha256": "sha256-nS+z8s2opHW/JFJlCv9DLhY0VowH5lYAgN4qHZoS7nY=" + } + } + }, + + { + "path": "org/robolectric/resources/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "resources-4.7.3.pom": { + "sha1": "4072adc3727cde694b7b4acf9e71331b9b88e2de", + "sha256": "sha256-IYRlaGIZnmKpKE3Jsb58PTnC6HhNrtoSEr1GZB5ATrE=" + }, + "resources-4.7.3.jar": { + "sha1": "0fadfbb93414a754f3ca2dd0e71c2ae00730b1cc", + "sha256": "sha256-tx+Ws/G+u6LpvCf+iqaGqI//rv1BTHlhUzN04dwVkt8=" + }, + "resources-4.7.3.module": { + "sha1": "38ade56f0e10d5b1d7ce2ffa491d6732350e3ba2", + "sha256": "sha256-PpUx7VmdJl75exyPqQlki/JJNTUXFsNxIXff3lsbMX8=" + } + } + }, + + { + "path": "org/robolectric/robolectric/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "robolectric-4.7.3.pom": { + "sha1": "97ce73194feeeee3686d0ead41d61d7a8ab5b67e", + "sha256": "sha256-rUBH7SqUebo2LNYerb4V1VDAb6UZmBIPZK1bhkf++3o=" + }, + "robolectric-4.7.3.jar": { + "sha1": "7365a38bf8b5cb86a42b48510f4d760f243ba3ca", + "sha256": "sha256-jZYZ8YyM9FdaL5IHPkuDB9qk8Xsa6bDo3K55N8T+3bk=" + }, + "robolectric-4.7.3.module": { + "sha1": "01f6df77f08e77968a4a3beddd7b81e122708a4f", + "sha256": "sha256-gRic5ugDVzXiGpZ3NEGvCAJC1zbDc8ym2n7y+R3vJjE=" + } + } + }, + + { + "path": "org/robolectric/sandbox/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "sandbox-4.7.3.pom": { + "sha1": "4505c148582943fbbadd09d70e7bd43bc9297d5d", + "sha256": "sha256-khTcEgOUjSzavviO6MzG8mCfKPRVfrf1izFL0Q6VPIA=" + }, + "sandbox-4.7.3.jar": { + "sha1": "449b30d91562089e74a854f9e6d47cbf1568bdd1", + "sha256": "sha256-nmmYP3oI9OngaCnDd05ALPgclrFpVhnPGsjHa52UOxM=" + }, + "sandbox-4.7.3.module": { + "sha1": "f92bdd14f06028ca63e1c5d45c279008895440c6", + "sha256": "sha256-ReCiECVC+C3UR7Ic4MtFszQBNLSNzj169MwoJt+L/NQ=" + } + } + }, + + { + "path": "org/robolectric/shadowapi/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "shadowapi-4.7.3.pom": { + "sha1": "7635051efd0cda2d30e84dbcdef8be4fcf0d2b6c", + "sha256": "sha256-p2V6LIUpfYgULe4zI5knXsDbbSIt4uopaRsLOQ0J26U=" + }, + "shadowapi-4.7.3.jar": { + "sha1": "0f58b925a11605a82f32327db4c271f3f835008f", + "sha256": "sha256-+Mz8GtT6aIIWQ4TDtm+Hr12V6gldY2rqDbRtfLMwXDo=" + }, + "shadowapi-4.7.3.module": { + "sha1": "0601d0c29862672f9033cf075314aa4149dcc91a", + "sha256": "sha256-JNvt6Rj/sPcKRDa8rt9HzgEijeg2P/vaMFdbhRh4vgQ=" + } + } + }, + + { + "path": "org/robolectric/shadows-framework/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "shadows-framework-4.7.3.pom": { + "sha1": "8c15aa28d445b87efe591d4f2944eb8daffbb220", + "sha256": "sha256-7R2gX6YhK7P2tzChs/3scVEYwsCZpTpRjBUr0tRH/Pk=" + }, + "shadows-framework-4.7.3.jar": { + "sha1": "40428b2b6fd3f04f69063c16ee81e35640efeef3", + "sha256": "sha256-p6ReHRMPa11mihLD4kFAeUT+ITVm89fHSawg9z7MRjw=" + }, + "shadows-framework-4.7.3.module": { + "sha1": "b980d5b4605d9c6009add81062ad31029886b8be", + "sha256": "sha256-zreRiEBGzaDwQXxTmh5yn36FmzLuUPsDD3XMC9Wu0gg=" + } + } + }, + + { + "path": "org/robolectric/utils-reflector/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "utils-reflector-4.7.3.pom": { + "sha1": "f3b6a499b92009240d094e0dffbb8b1c466d5a98", + "sha256": "sha256-XC2+TaJLo6JTNbrOPEDZ36C0qV66rjwzIkBRjDf6jaE=" + }, + "utils-reflector-4.7.3.jar": { + "sha1": "e27d1c1f867ff08b5aa28cae6a5b1f3d067002e4", + "sha256": "sha256-dv2UIXMzDbVNqDVtLX1HUaZ7LCy8YO3NYvYNgbZTcjE=" + }, + "utils-reflector-4.7.3.module": { + "sha1": "2e64816ea80b22fc1c1cba2529eac86645a815b8", + "sha256": "sha256-Z7z+LGaoWHpycZgaVFHs+nLO8GM1yV6VtjjzQniwlDI=" + } + } + }, + + { + "path": "org/robolectric/utils/4.7.3", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "utils-4.7.3.pom": { + "sha1": "7bc7afad9092bc2afef1cd981a5f0bd8cfb27706", + "sha256": "sha256-5tpgD8LGT/YL5gbYVWGHldd5VlBlZ5fbL0+UW9ytZ1k=" + }, + "utils-4.7.3.jar": { + "sha1": "580c8ed0e92508df69c8dc97e8cbc12b1052ef10", + "sha256": "sha256-RUJYAQ/gdyNjAlXDDp1Yiow9j76WGErmWYaPA2aaunU=" + }, + "utils-4.7.3.module": { + "sha1": "45f27ccff946dc656042f1bdb8e92d9e83c65cb0", + "sha256": "sha256-r2jXQYsIos5JutYUaoWxlmkLZHpmWELkVNY8Dciotaw=" + } + } + }, + + { + "path": "org/slf4j/slf4j-api/1.7.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "slf4j-api-1.7.21.pom": { + "sha1": "da1347f4af6398d8c9cece95ca215baf8570449b", + "sha256": "sha256-XyvfJv+i2uhK8snfhDhTKmxwOl6ff5mOmxzWAP3VT9w=" + }, + "slf4j-api-1.7.21.jar": { + "sha1": "139535a69a4239db087de9bab0bee568bf8e0b70", + "sha256": "sha256-HVrra9mLD90VEmnq6UHAX2Rop5HqDx5o2Of+UYrz598=" + } + } + }, + + { + "path": "org/slf4j/slf4j-api/1.7.30", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "slf4j-api-1.7.30.pom": { + "sha1": "02013960e5ee7f712d8fa6f2e618a6ff2e8d98a9", + "sha256": "sha256-fgdHdR6bZ+Gdy1IG8E6iLMA9JQxCJCZALq3QNRPywxQ=" + }, + "slf4j-api-1.7.30.jar": { + "sha1": "b5a4b6d16ab13e34a88fae84c35cd5d68cac922c", + "sha256": "sha256-zboHlk0btAoHYUhcax6ML4/Z6x0ZxTkorA1/lRAQXFc=" + } + } + }, + + { + "path": "org/slf4j/slf4j-api/2.1.0-alpha1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "slf4j-api-2.1.0-alpha1.pom": { + "sha1": "c5362a45b3be4d9d110f5450822ea81b27adb1a0", + "sha256": "sha256-QirqW+u6gwWzxhT6Zo7SKePJYQkw7PQvhzOO4F4minU=" + }, + "slf4j-api-2.1.0-alpha1.jar": { + "sha1": "4e2525fc4327cc553c52e3937427c7cf2114735f", + "sha256": "sha256-mrf/pkYgK0mdBZlaPsgvMbzLelA0XBUU2MtC7IzOo1M=" + } + } + }, + + { + "path": "org/slf4j/slf4j-bom/2.1.0-alpha1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "slf4j-bom-2.1.0-alpha1.pom": { + "sha1": "f8784ee61914057bec3db57b5efea4e6b0333c6f", + "sha256": "sha256-qOgTiZePRAcJJBuYPTHvp4cRO+EbgYwsa82e0wlv1IU=" + } + } + }, + + { + "path": "org/slf4j/slf4j-parent/1.7.21", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "slf4j-parent-1.7.21.pom": { + "sha1": "974e5bae7c87ae28134204511dcba6f327de9d5c", + "sha256": "sha256-eqolf13tUWyTwHWGwe0TntUO5aT/YZ5vWLuhf0nRO64=" + } + } + }, + + { + "path": "org/slf4j/slf4j-parent/1.7.30", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "slf4j-parent-1.7.30.pom": { + "sha1": "d3cf0e0cdb58d3c6f8a08c21e55894422fd725b9", + "sha256": "sha256-EWR5VuSKDFv7OsM/bafoPzQQAraFfv0zWlBbaHvjS3U=" + } + } + }, + + { + "path": "org/slf4j/slf4j-parent/2.1.0-alpha1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "slf4j-parent-2.1.0-alpha1.pom": { + "sha1": "ef0d60c3407004bb7e71643a13644f4bc2f3ec00", + "sha256": "sha256-zkc1sfnIId4Lkrjb5AsHHG6jIHMuWTVZxupt+WX4c48=" + } + } + }, + + { + "path": "org/sonatype/oss/oss-parent/4", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "oss-parent-4.pom": { + "sha1": "281323f0e2b10184d6c6b230909401166b437e01", + "sha256": "sha256-xROZXPAZ2SE9T9pmZYmTeyvxvqXEzdM35hcOgLGEBu4=" + } + } + }, + + { + "path": "org/sonatype/oss/oss-parent/5", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "oss-parent-5.pom": { + "sha1": "3ae20880ad3d5da6b1caec19e3de7e70dd2dd762", + "sha256": "sha256-FnjUEgpYXYpjATGu7ExSTZKDmFg7fqthbufVqH9SDT0=" + } + } + }, + + { + "path": "org/sonatype/oss/oss-parent/7", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "oss-parent-7.pom": { + "sha1": "46b8a785b60a2767095b8611613b58577e96d4c9", + "sha256": "sha256-tR+IZ8kranIkmVV/w6H96ne9+e9XRyL+kM5DailVlFQ=" + } + } + }, + + { + "path": "org/sonatype/oss/oss-parent/9", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "oss-parent-9.pom": { + "sha1": "e5cdc4d23b86d79c436f16fed20853284e868f65", + "sha256": "sha256-+0AmX5glSCEv+C42LllzKyGH7G8NgBgohcFO8fmCgno=" + } + } + }, + + { + "path": "org/tensorflow/tensorflow-lite-metadata/0.1.0-rc1", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "tensorflow-lite-metadata-0.1.0-rc1.pom": { + "sha1": "49601bf0916d0b2cfee191163a154376fb37e98b", + "sha256": "sha256-23Va4Zvg99RKsJ9uxYED+q7kJzOox10vtOPDK6tfScI=" + }, + "tensorflow-lite-metadata-0.1.0-rc1.jar": { + "sha1": "f6e561e9053f2e9a2779d2ce1dffd45ce4dc3c74", + "sha256": "sha256-SB2/W7OPAu71LX/5ZvVfmamAp9WHcdSFJ4X1G7gs5aA=" + } + } + }, + + { + "path": "org/tensorflow/tensorflow-lite-metadata/0.1.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "tensorflow-lite-metadata-0.1.0-rc2.pom": { + "sha1": "43e457c5e30bb8f08f5e3823fdd172d238b7cfab", + "sha256": "sha256-mk9eVnQ2bBVskDkWYvA+18WXHWqmODLfdKJx2m/4LpY=" + }, + "tensorflow-lite-metadata-0.1.0-rc2.jar": { + "sha1": "7aa9b3e16d85c2aae5eff741265315a37c9f731a", + "sha256": "sha256-LComT4QkmMNtNNKnuRNCSQ2alihiyFuqwazVTsL8ptk=" + } + } + }, + + { + "path": "tools/jackson/jackson-base/3.0.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-base-3.0.0-rc2.pom": { + "sha1": "1d25441d4344cea474bd7cc89b19ee7bf2f14807", + "sha256": "sha256-DL5RFTIMcBMydqedUuQJxRtCi7RD3kbHBsaGj43wKQA=" + } + } + }, + + { + "path": "tools/jackson/jackson-bom/3.0.0-rc2", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "jackson-bom-3.0.0-rc2.pom": { + "sha1": "e5140c20d79a87fc0f19010c5f62b3af7e0a4723", + "sha256": "sha256-Q1gSQQmlq/pJmzNVYxD2CoC+Jp3Bqz37FogHsiuiFbw=" + } + } + }, + + { + "path": "xerces/xercesImpl/2.12.0", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "xercesImpl-2.12.0.pom": { + "sha1": "673c91f8212d2f2add1a7557174af19069e87070", + "sha256": "sha256-E467M8znCA/VzbT7kKDuVQFz1+jUECTajA14ixRgofM=" + }, + "xercesImpl-2.12.0.jar": { + "sha1": "f02c844149fd306601f20e0b34853a670bef7fa2", + "sha256": "sha256-tQ06TKUC+qTRyDisuKqUgERpU0IfcyfjOMXdo9pedtA=" + } + } + }, + + { + "path": "xml-apis/xml-apis/1.4.01", + "repo": "https://repo.maven.apache.org/maven2", + "files": { + "xml-apis-1.4.01.pom": { + "sha1": "1c657bc14f11cbfcebb2ebf63eebd5cd85b08872", + "sha256": "sha256-Cagv8VCshr+jEUXgpq/YmgLkUEeF9doRLk+uFCUCDpI=" + }, + "xml-apis-1.4.01.jar": { + "sha1": "3789d9fada2d3d458c4ba2de349d48780f381ee3", + "sha256": "sha256-qECWgXZkVoS7Aa7TduBnqzlhSIX57uRKvjWl8g6+f60=" + } + } + } +] diff --git a/nix/deps/gradle/deps.list b/nix/deps/gradle/deps.list new file mode 100644 index 00000000000..ac4bf35c578 --- /dev/null +++ b/nix/deps/gradle/deps.list @@ -0,0 +1,1116 @@ +androidx.activity:activity:1.0.0 +androidx.activity:activity:1.0.0-rc01 +androidx.activity:activity:1.2.4 +androidx.activity:activity:1.6.0 +androidx.annotation:annotation-experimental:1.1.0 +androidx.annotation:annotation-experimental:1.3.0 +androidx.annotation:annotation-jvm:1.6.0 +androidx.annotation:annotation:1.0.0 +androidx.annotation:annotation:1.1.0 +androidx.annotation:annotation:1.2.0 +androidx.annotation:annotation:1.3.0 +androidx.annotation:annotation:1.5.0 +androidx.annotation:annotation:1.6.0 +androidx.appcompat:appcompat-resources:1.1.0-rc01 +androidx.appcompat:appcompat-resources:1.2.0 +androidx.appcompat:appcompat-resources:1.3.1 +androidx.appcompat:appcompat-resources:1.6.1 +androidx.appcompat:appcompat:1.0.0 +androidx.appcompat:appcompat:1.1.0-rc01 +androidx.appcompat:appcompat:1.2.0 +androidx.appcompat:appcompat:1.3.1 +androidx.appcompat:appcompat:1.6.1 +androidx.arch.core:core-common:2.0.0 +androidx.arch.core:core-common:2.1.0 +androidx.arch.core:core-common:2.1.0-rc01 +androidx.arch.core:core-runtime:2.0.0 +androidx.arch.core:core-runtime:2.1.0 +androidx.asynclayoutinflater:asynclayoutinflater:1.0.0 +androidx.autofill:autofill:1.1.0 +androidx.biometric:biometric:1.1.0 +androidx.camera:camera-camera2:1.1.0 +androidx.camera:camera-core:1.1.0 +androidx.camera:camera-lifecycle:1.1.0 +androidx.camera:camera-view:1.1.0 +androidx.cardview:cardview:1.0.0 +androidx.collection:collection:1.0.0 +androidx.collection:collection:1.1.0 +androidx.concurrent:concurrent-futures:1.0.0 +androidx.concurrent:concurrent-futures:1.1.0 +androidx.constraintlayout:constraintlayout-solver:2.0.4 +androidx.constraintlayout:constraintlayout:2.0.4 +androidx.coordinatorlayout:coordinatorlayout:1.0.0 +androidx.coordinatorlayout:coordinatorlayout:1.1.0 +androidx.core:core-ktx:1.2.0 +androidx.core:core-ktx:1.6.0 +androidx.core:core-ktx:1.9.0 +androidx.core:core-ktx:1.12.0 +androidx.core:core-splashscreen:1.0.0 +androidx.core:core:1.0.0 +androidx.core:core:1.1.0 +androidx.core:core:1.2.0 +androidx.core:core:1.6.0 +androidx.core:core:1.9.0 +androidx.core:core:1.12.0 +androidx.cursoradapter:cursoradapter:1.0.0 +androidx.customview:customview:1.0.0 +androidx.databinding:databinding-common:3.2.1 +androidx.databinding:databinding-common:3.5.3 +androidx.databinding:databinding-common:4.1.0 +androidx.databinding:databinding-common:7.0.4 +androidx.databinding:databinding-common:7.2.1 +androidx.databinding:databinding-common:7.3.1 +androidx.databinding:databinding-common:7.4.2 +androidx.databinding:databinding-common:8.1.1 +androidx.databinding:databinding-common:8.2.2 +androidx.databinding:databinding-compiler-common:3.2.1 +androidx.databinding:databinding-compiler-common:3.5.3 +androidx.databinding:databinding-compiler-common:4.1.0 +androidx.databinding:databinding-compiler-common:7.0.4 +androidx.databinding:databinding-compiler-common:7.2.1 +androidx.databinding:databinding-compiler-common:7.3.1 +androidx.databinding:databinding-compiler-common:7.4.2 +androidx.databinding:databinding-compiler-common:8.1.1 +androidx.databinding:databinding-compiler-common:8.2.2 +androidx.documentfile:documentfile:1.0.0 +androidx.drawerlayout:drawerlayout:1.0.0 +androidx.emoji2:emoji2-views-helper:1.2.0 +androidx.emoji2:emoji2:1.2.0 +androidx.exifinterface:exifinterface:1.1.0-beta01 +androidx.exifinterface:exifinterface:1.1.0-rc01 +androidx.exifinterface:exifinterface:1.2.0 +androidx.exifinterface:exifinterface:1.3.2 +androidx.fragment:fragment:1.0.0 +androidx.fragment:fragment:1.1.0 +androidx.fragment:fragment:1.1.0-rc01 +androidx.fragment:fragment:1.3.6 +androidx.fragment:fragment:1.5.7 +androidx.interpolator:interpolator:1.0.0 +androidx.legacy:legacy-support-core-ui:1.0.0 +androidx.legacy:legacy-support-core-utils:1.0.0 +androidx.legacy:legacy-support-v4:1.0.0 +androidx.lifecycle:lifecycle-common:2.0.0 +androidx.lifecycle:lifecycle-common:2.1.0 +androidx.lifecycle:lifecycle-common:2.1.0-rc01 +androidx.lifecycle:lifecycle-common:2.3.1 +androidx.lifecycle:lifecycle-common:2.5.1 +androidx.lifecycle:lifecycle-livedata-core:2.0.0 +androidx.lifecycle:lifecycle-livedata-core:2.1.0 +androidx.lifecycle:lifecycle-livedata-core:2.3.1 +androidx.lifecycle:lifecycle-livedata-core:2.5.1 +androidx.lifecycle:lifecycle-livedata:2.0.0 +androidx.lifecycle:lifecycle-livedata:2.1.0 +androidx.lifecycle:lifecycle-process:2.4.1 +androidx.lifecycle:lifecycle-runtime:2.0.0 +androidx.lifecycle:lifecycle-runtime:2.1.0 +androidx.lifecycle:lifecycle-runtime:2.1.0-rc01 +androidx.lifecycle:lifecycle-runtime:2.3.1 +androidx.lifecycle:lifecycle-runtime:2.5.1 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 +androidx.lifecycle:lifecycle-viewmodel:2.0.0 +androidx.lifecycle:lifecycle-viewmodel:2.1.0 +androidx.lifecycle:lifecycle-viewmodel:2.1.0-rc01 +androidx.lifecycle:lifecycle-viewmodel:2.3.1 +androidx.lifecycle:lifecycle-viewmodel:2.5.1 +androidx.loader:loader:1.0.0 +androidx.loader:loader:1.1.0 +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 +androidx.media:media:1.0.0 +androidx.multidex:multidex:2.0.1 +androidx.print:print:1.0.0 +androidx.recyclerview:recyclerview:1.0.0 +androidx.recyclerview:recyclerview:1.1.0 +androidx.resourceinspection:resourceinspection-annotation:1.0.1 +androidx.savedstate:savedstate:1.0.0 +androidx.savedstate:savedstate:1.0.0-rc01 +androidx.savedstate:savedstate:1.1.0 +androidx.savedstate:savedstate:1.2.0 +androidx.slidingpanelayout:slidingpanelayout:1.0.0 +androidx.startup:startup-runtime:1.1.1 +androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 +androidx.swiperefreshlayout:swiperefreshlayout:1.1.0 +androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02 +androidx.test.ext:junit:1.1.4-alpha05 +androidx.test.services:storage:1.4.2-alpha02 +androidx.test:annotation:1.0.0 +androidx.test:core:1.4.1-alpha05 +androidx.test:monitor:1.6.0-alpha02 +androidx.test:runner:1.5.0-alpha02 +androidx.tracing:tracing:1.1.0 +androidx.transition:transition:1.1.0 +androidx.transition:transition:1.2.0 +androidx.transition:transition:1.2.0-rc01 +androidx.vectordrawable:vectordrawable-animated:1.0.0 +androidx.vectordrawable:vectordrawable-animated:1.1.0 +androidx.vectordrawable:vectordrawable-animated:1.1.0-rc01 +androidx.vectordrawable:vectordrawable:1.0.0 +androidx.vectordrawable:vectordrawable:1.1.0 +androidx.vectordrawable:vectordrawable:1.1.0-rc01 +androidx.versionedparcelable:versionedparcelable:1.0.0 +androidx.versionedparcelable:versionedparcelable:1.1.0 +androidx.versionedparcelable:versionedparcelable:1.1.1 +androidx.viewpager2:viewpager2:1.0.0 +androidx.viewpager:viewpager:1.0.0 +androidx.webkit:webkit:1.4.0 +commons-codec:commons-codec:1.4 +commons-codec:commons-codec:1.6 +commons-codec:commons-codec:1.9 +commons-codec:commons-codec:1.10 +commons-codec:commons-codec:1.11 +commons-io:commons-io:2.4 +commons-logging:commons-logging:1.1.1 +commons-logging:commons-logging:1.2 +com.adarshr:gradle-test-logger-plugin:2.0.0 +com.adobe.xmp:xmpcore:6.1.11 +com.afollestad.material-dialogs:commons:0.9.6.0 +com.afollestad.material-dialogs:core:0.9.6.0 +com.airbnb.android:lottie:3.4.4 +com.almworks.sqlite4java:sqlite4java:1.0.392 +com.android.databinding:baseLibrary:1.0-rc5 +com.android.databinding:baseLibrary:3.0.1 +com.android.databinding:baseLibrary:3.2.1 +com.android.databinding:baseLibrary:3.5.3 +com.android.databinding:baseLibrary:4.1.0 +com.android.databinding:baseLibrary:7.0.4 +com.android.databinding:baseLibrary:7.2.1 +com.android.databinding:baseLibrary:7.3.1 +com.android.databinding:baseLibrary:7.4.2 +com.android.databinding:baseLibrary:8.1.1 +com.android.databinding:baseLibrary:8.2.2 +com.android.databinding:compilerCommon:1.0-rc5 +com.android.databinding:compilerCommon:3.0.1 +com.android.tools.analytics-library:crash:26.2.1 +com.android.tools.analytics-library:crash:26.5.3 +com.android.tools.analytics-library:crash:27.1.0 +com.android.tools.analytics-library:crash:30.0.4 +com.android.tools.analytics-library:crash:30.2.1 +com.android.tools.analytics-library:crash:30.3.1 +com.android.tools.analytics-library:crash:30.4.2 +com.android.tools.analytics-library:crash:31.1.1 +com.android.tools.analytics-library:crash:31.2.2 +com.android.tools.analytics-library:protos:26.0.1 +com.android.tools.analytics-library:protos:26.2.1 +com.android.tools.analytics-library:protos:26.5.3 +com.android.tools.analytics-library:protos:27.1.0 +com.android.tools.analytics-library:protos:30.0.4 +com.android.tools.analytics-library:protos:30.2.1 +com.android.tools.analytics-library:protos:30.3.1 +com.android.tools.analytics-library:protos:30.4.2 +com.android.tools.analytics-library:protos:31.1.1 +com.android.tools.analytics-library:protos:31.2.2 +com.android.tools.analytics-library:shared:26.0.1 +com.android.tools.analytics-library:shared:26.2.1 +com.android.tools.analytics-library:shared:26.5.3 +com.android.tools.analytics-library:shared:27.1.0 +com.android.tools.analytics-library:shared:30.0.4 +com.android.tools.analytics-library:shared:30.2.1 +com.android.tools.analytics-library:shared:30.3.1 +com.android.tools.analytics-library:shared:30.4.2 +com.android.tools.analytics-library:shared:31.1.1 +com.android.tools.analytics-library:shared:31.2.2 +com.android.tools.analytics-library:tracker:26.0.1 +com.android.tools.analytics-library:tracker:26.2.1 +com.android.tools.analytics-library:tracker:26.5.3 +com.android.tools.analytics-library:tracker:27.1.0 +com.android.tools.analytics-library:tracker:30.0.4 +com.android.tools.analytics-library:tracker:30.2.1 +com.android.tools.analytics-library:tracker:30.3.1 +com.android.tools.analytics-library:tracker:30.4.2 +com.android.tools.analytics-library:tracker:31.1.1 +com.android.tools.analytics-library:tracker:31.2.2 +com.android.tools.build.jetifier:jetifier-core:1.0.0-alpha10 +com.android.tools.build.jetifier:jetifier-core:1.0.0-beta04 +com.android.tools.build.jetifier:jetifier-core:1.0.0-beta09 +com.android.tools.build.jetifier:jetifier-core:1.0.0-beta10 +com.android.tools.build.jetifier:jetifier-processor:1.0.0-alpha10 +com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta04 +com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta09 +com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta10 +com.android.tools.build:aapt2-proto:0.3.1 +com.android.tools.build:aapt2-proto:0.4.0 +com.android.tools.build:aapt2-proto:4.1.0-6503028 +com.android.tools.build:aapt2-proto:7.0.4-7396180 +com.android.tools.build:aapt2-proto:7.2.1-7984345 +com.android.tools.build:aapt2-proto:7.3.1-8691043 +com.android.tools.build:aapt2-proto:7.4.2-8841542 +com.android.tools.build:aapt2-proto:8.1.1-10154469 +com.android.tools.build:aapt2-proto:8.2.2-10154469 +com.android.tools.build:aapt2:8.1.1-10154469 +com.android.tools.build:aaptcompiler:4.1.0 +com.android.tools.build:aaptcompiler:7.0.4 +com.android.tools.build:aaptcompiler:7.2.1 +com.android.tools.build:aaptcompiler:7.3.1 +com.android.tools.build:aaptcompiler:7.4.2 +com.android.tools.build:aaptcompiler:8.1.1 +com.android.tools.build:aaptcompiler:8.2.2 +com.android.tools.build:apksig:3.0.1 +com.android.tools.build:apksig:3.2.1 +com.android.tools.build:apksig:3.5.3 +com.android.tools.build:apksig:4.1.0 +com.android.tools.build:apksig:7.0.4 +com.android.tools.build:apksig:7.2.1 +com.android.tools.build:apksig:7.3.1 +com.android.tools.build:apksig:7.4.2 +com.android.tools.build:apksig:8.1.1 +com.android.tools.build:apksig:8.2.2 +com.android.tools.build:apkzlib:3.2.1 +com.android.tools.build:apkzlib:3.5.3 +com.android.tools.build:apkzlib:4.1.0 +com.android.tools.build:apkzlib:7.0.4 +com.android.tools.build:apkzlib:7.2.1 +com.android.tools.build:apkzlib:7.3.1 +com.android.tools.build:apkzlib:7.4.2 +com.android.tools.build:apkzlib:8.1.1 +com.android.tools.build:apkzlib:8.2.2 +com.android.tools.build:builder-model:1.3.1 +com.android.tools.build:builder-model:1.5.0 +com.android.tools.build:builder-model:3.0.1 +com.android.tools.build:builder-model:3.2.1 +com.android.tools.build:builder-model:3.5.3 +com.android.tools.build:builder-model:4.1.0 +com.android.tools.build:builder-model:7.0.4 +com.android.tools.build:builder-model:7.2.1 +com.android.tools.build:builder-model:7.3.1 +com.android.tools.build:builder-model:7.4.2 +com.android.tools.build:builder-model:8.1.1 +com.android.tools.build:builder-model:8.2.2 +com.android.tools.build:builder-test-api:1.3.1 +com.android.tools.build:builder-test-api:1.5.0 +com.android.tools.build:builder-test-api:3.0.1 +com.android.tools.build:builder-test-api:3.2.1 +com.android.tools.build:builder-test-api:3.5.3 +com.android.tools.build:builder-test-api:4.1.0 +com.android.tools.build:builder-test-api:7.0.4 +com.android.tools.build:builder-test-api:7.2.1 +com.android.tools.build:builder-test-api:7.3.1 +com.android.tools.build:builder-test-api:7.4.2 +com.android.tools.build:builder-test-api:8.1.1 +com.android.tools.build:builder-test-api:8.2.2 +com.android.tools.build:builder:1.3.1 +com.android.tools.build:builder:1.5.0 +com.android.tools.build:builder:3.0.1 +com.android.tools.build:builder:3.2.1 +com.android.tools.build:builder:3.5.3 +com.android.tools.build:builder:4.1.0 +com.android.tools.build:builder:7.0.4 +com.android.tools.build:builder:7.2.1 +com.android.tools.build:builder:7.3.1 +com.android.tools.build:builder:7.4.2 +com.android.tools.build:builder:8.1.1 +com.android.tools.build:builder:8.2.2 +com.android.tools.build:bundletool:0.5.0 +com.android.tools.build:bundletool:0.9.0 +com.android.tools.build:bundletool:0.14.0 +com.android.tools.build:bundletool:1.6.0 +com.android.tools.build:bundletool:1.8.2 +com.android.tools.build:bundletool:1.9.0 +com.android.tools.build:bundletool:1.11.4 +com.android.tools.build:bundletool:1.14.0 +com.android.tools.build:bundletool:1.15.2 +com.android.tools.build:gradle-api:3.0.1 +com.android.tools.build:gradle-api:3.2.1 +com.android.tools.build:gradle-api:3.5.3 +com.android.tools.build:gradle-api:4.1.0 +com.android.tools.build:gradle-api:7.0.4 +com.android.tools.build:gradle-api:7.2.1 +com.android.tools.build:gradle-api:7.3.1 +com.android.tools.build:gradle-api:7.4.2 +com.android.tools.build:gradle-api:8.1.1 +com.android.tools.build:gradle-api:8.2.2 +com.android.tools.build:gradle-core:1.3.1 +com.android.tools.build:gradle-core:1.5.0 +com.android.tools.build:gradle-core:3.0.1 +com.android.tools.build:gradle-settings-api:7.4.2 +com.android.tools.build:gradle-settings-api:8.1.1 +com.android.tools.build:gradle-settings-api:8.2.2 +com.android.tools.build:gradle:1.3.1 +com.android.tools.build:gradle:1.5.0 +com.android.tools.build:gradle:3.0.1 +com.android.tools.build:gradle:3.2.1 +com.android.tools.build:gradle:3.5.3 +com.android.tools.build:gradle:4.1.0 +com.android.tools.build:gradle:7.0.4 +com.android.tools.build:gradle:7.2.1 +com.android.tools.build:gradle:7.3.1 +com.android.tools.build:gradle:7.4.2 +com.android.tools.build:gradle:8.1.1 +com.android.tools.build:gradle:8.2.2 +com.android.tools.build:manifest-merger:24.3.1 +com.android.tools.build:manifest-merger:24.5.0 +com.android.tools.build:manifest-merger:26.0.1 +com.android.tools.build:manifest-merger:26.2.1 +com.android.tools.build:manifest-merger:26.5.3 +com.android.tools.build:manifest-merger:27.1.0 +com.android.tools.build:manifest-merger:30.0.4 +com.android.tools.build:manifest-merger:30.2.1 +com.android.tools.build:manifest-merger:30.3.1 +com.android.tools.build:manifest-merger:30.4.2 +com.android.tools.build:manifest-merger:31.1.1 +com.android.tools.build:manifest-merger:31.2.2 +com.android.tools.build:transform-api:1.5.0 +com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api +com.android.tools.ddms:ddmlib:24.3.1 +com.android.tools.ddms:ddmlib:24.5.0 +com.android.tools.ddms:ddmlib:26.0.1 +com.android.tools.ddms:ddmlib:26.2.1 +com.android.tools.ddms:ddmlib:26.5.3 +com.android.tools.ddms:ddmlib:27.1.0 +com.android.tools.ddms:ddmlib:30.0.4 +com.android.tools.ddms:ddmlib:30.2.1 +com.android.tools.ddms:ddmlib:30.3.1 +com.android.tools.ddms:ddmlib:30.4.2 +com.android.tools.ddms:ddmlib:31.1.1 +com.android.tools.ddms:ddmlib:31.2.2 +com.android.tools.emulator:proto:31.1.1 +com.android.tools.external.com-intellij:intellij-core:26.0.1 +com.android.tools.external.lombok:lombok-ast:0.2.3 +com.android.tools.external.org-jetbrains:uast:26.0.1 +com.android.tools.jack:jack-api:0.9.0 +com.android.tools.jill:jill-api:0.9.0 +com.android.tools.layoutlib:layoutlib-api:24.3.1 +com.android.tools.layoutlib:layoutlib-api:24.5.0 +com.android.tools.layoutlib:layoutlib-api:26.0.1 +com.android.tools.layoutlib:layoutlib-api:26.2.1 +com.android.tools.layoutlib:layoutlib-api:26.5.3 +com.android.tools.layoutlib:layoutlib-api:27.1.0 +com.android.tools.layoutlib:layoutlib-api:30.0.4 +com.android.tools.layoutlib:layoutlib-api:30.2.1 +com.android.tools.layoutlib:layoutlib-api:30.3.1 +com.android.tools.layoutlib:layoutlib-api:30.4.2 +com.android.tools.layoutlib:layoutlib-api:31.1.1 +com.android.tools.layoutlib:layoutlib-api:31.2.2 +com.android.tools.lint:lint-api:24.3.1 +com.android.tools.lint:lint-api:24.5.0 +com.android.tools.lint:lint-api:26.0.1 +com.android.tools.lint:lint-checks:24.3.1 +com.android.tools.lint:lint-checks:24.5.0 +com.android.tools.lint:lint-checks:26.0.1 +com.android.tools.lint:lint-gradle-api:26.2.1 +com.android.tools.lint:lint-gradle-api:26.5.3 +com.android.tools.lint:lint-gradle-api:27.1.0 +com.android.tools.lint:lint-model:27.1.0 +com.android.tools.lint:lint-model:30.0.4 +com.android.tools.lint:lint-model:30.2.1 +com.android.tools.lint:lint-model:30.3.1 +com.android.tools.lint:lint-model:30.4.2 +com.android.tools.lint:lint-model:31.1.1 +com.android.tools.lint:lint-model:31.2.2 +com.android.tools.lint:lint-typedef-remover:30.2.1 +com.android.tools.lint:lint-typedef-remover:30.3.1 +com.android.tools.lint:lint-typedef-remover:30.4.2 +com.android.tools.lint:lint-typedef-remover:31.1.1 +com.android.tools.lint:lint-typedef-remover:31.2.2 +com.android.tools.lint:lint:24.3.1 +com.android.tools.lint:lint:24.5.0 +com.android.tools.lint:lint:26.0.1 +com.android.tools.utp:android-device-provider-ddmlib-proto:30.2.1 +com.android.tools.utp:android-device-provider-ddmlib-proto:30.3.1 +com.android.tools.utp:android-device-provider-ddmlib-proto:30.4.2 +com.android.tools.utp:android-device-provider-ddmlib-proto:31.1.1 +com.android.tools.utp:android-device-provider-ddmlib-proto:31.2.2 +com.android.tools.utp:android-device-provider-ddmlib:31.1.1 +com.android.tools.utp:android-device-provider-gradle-proto:30.0.4 +com.android.tools.utp:android-device-provider-gradle-proto:30.2.1 +com.android.tools.utp:android-device-provider-gradle-proto:30.3.1 +com.android.tools.utp:android-device-provider-gradle-proto:30.4.2 +com.android.tools.utp:android-device-provider-gradle-proto:31.1.1 +com.android.tools.utp:android-device-provider-gradle-proto:31.2.2 +com.android.tools.utp:android-device-provider-gradle:31.1.1 +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.2.1 +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.3.1 +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.4.2 +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:31.1.1 +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:31.2.2 +com.android.tools.utp:android-test-plugin-host-additional-test-output:31.1.1 +com.android.tools.utp:android-test-plugin-host-apk-installer-proto:31.1.1 +com.android.tools.utp:android-test-plugin-host-apk-installer-proto:31.2.2 +com.android.tools.utp:android-test-plugin-host-apk-installer:31.1.1 +com.android.tools.utp:android-test-plugin-host-coverage-proto:30.2.1 +com.android.tools.utp:android-test-plugin-host-coverage-proto:30.3.1 +com.android.tools.utp:android-test-plugin-host-coverage-proto:30.4.2 +com.android.tools.utp:android-test-plugin-host-coverage-proto:31.1.1 +com.android.tools.utp:android-test-plugin-host-coverage-proto:31.2.2 +com.android.tools.utp:android-test-plugin-host-coverage:31.1.1 +com.android.tools.utp:android-test-plugin-host-device-info-proto:31.1.1 +com.android.tools.utp:android-test-plugin-host-device-info:31.1.1 +com.android.tools.utp:android-test-plugin-host-emulator-control-proto:31.1.1 +com.android.tools.utp:android-test-plugin-host-emulator-control-proto:31.2.2 +com.android.tools.utp:android-test-plugin-host-emulator-control:31.1.1 +com.android.tools.utp:android-test-plugin-host-logcat-proto:31.1.1 +com.android.tools.utp:android-test-plugin-host-logcat-proto:31.2.2 +com.android.tools.utp:android-test-plugin-host-logcat:31.1.1 +com.android.tools.utp:android-test-plugin-host-retention-proto:30.0.4 +com.android.tools.utp:android-test-plugin-host-retention-proto:30.2.1 +com.android.tools.utp:android-test-plugin-host-retention-proto:30.3.1 +com.android.tools.utp:android-test-plugin-host-retention-proto:30.4.2 +com.android.tools.utp:android-test-plugin-host-retention-proto:31.1.1 +com.android.tools.utp:android-test-plugin-host-retention-proto:31.2.2 +com.android.tools.utp:android-test-plugin-host-retention:31.1.1 +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.0.4 +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.2.1 +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.3.1 +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.4.2 +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:31.1.1 +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:31.2.2 +com.android.tools.utp:android-test-plugin-result-listener-gradle:31.1.1 +com.android.tools:annotations:24.3.1 +com.android.tools:annotations:24.5.0 +com.android.tools:annotations:26.0.1 +com.android.tools:annotations:26.2.1 +com.android.tools:annotations:26.5.3 +com.android.tools:annotations:27.1.0 +com.android.tools:annotations:30.0.4 +com.android.tools:annotations:30.2.1 +com.android.tools:annotations:30.3.1 +com.android.tools:annotations:30.4.2 +com.android.tools:annotations:31.1.1 +com.android.tools:annotations:31.2.2 +com.android.tools:common:24.3.1 +com.android.tools:common:24.5.0 +com.android.tools:common:26.0.1 +com.android.tools:common:26.2.1 +com.android.tools:common:26.5.3 +com.android.tools:common:27.1.0 +com.android.tools:common:30.0.4 +com.android.tools:common:30.2.1 +com.android.tools:common:30.3.1 +com.android.tools:common:30.4.2 +com.android.tools:common:31.1.1 +com.android.tools:common:31.2.2 +com.android.tools:dvlib:24.3.1 +com.android.tools:dvlib:24.5.0 +com.android.tools:dvlib:26.0.1 +com.android.tools:dvlib:26.2.1 +com.android.tools:dvlib:26.5.3 +com.android.tools:dvlib:27.1.0 +com.android.tools:dvlib:30.0.4 +com.android.tools:dvlib:30.2.1 +com.android.tools:dvlib:30.3.1 +com.android.tools:dvlib:30.4.2 +com.android.tools:dvlib:31.1.1 +com.android.tools:dvlib:31.2.2 +com.android.tools:repository:26.0.1 +com.android.tools:repository:26.2.1 +com.android.tools:repository:26.5.3 +com.android.tools:repository:27.1.0 +com.android.tools:repository:30.0.4 +com.android.tools:repository:30.2.1 +com.android.tools:repository:30.3.1 +com.android.tools:repository:30.4.2 +com.android.tools:repository:31.1.1 +com.android.tools:repository:31.2.2 +com.android.tools:sdklib:24.3.1 +com.android.tools:sdklib:24.5.0 +com.android.tools:sdklib:26.0.1 +com.android.tools:sdklib:26.2.1 +com.android.tools:sdklib:26.5.3 +com.android.tools:sdklib:27.1.0 +com.android.tools:sdklib:30.0.4 +com.android.tools:sdklib:30.2.1 +com.android.tools:sdklib:30.3.1 +com.android.tools:sdklib:30.4.2 +com.android.tools:sdklib:31.1.1 +com.android.tools:sdklib:31.2.2 +com.android.tools:sdk-common:24.3.1 +com.android.tools:sdk-common:24.5.0 +com.android.tools:sdk-common:26.0.1 +com.android.tools:sdk-common:26.2.1 +com.android.tools:sdk-common:26.5.3 +com.android.tools:sdk-common:27.1.0 +com.android.tools:sdk-common:30.0.4 +com.android.tools:sdk-common:30.2.1 +com.android.tools:sdk-common:30.3.1 +com.android.tools:sdk-common:30.4.2 +com.android.tools:sdk-common:31.1.1 +com.android.tools:sdk-common:31.2.2 +com.android:signflinger:4.1.0 +com.android:signflinger:7.0.4 +com.android:signflinger:7.2.1 +com.android:signflinger:7.3.1 +com.android:signflinger:7.4.2 +com.android:signflinger:8.1.1 +com.android:signflinger:8.2.2 +com.android:zipflinger:4.1.0 +com.android:zipflinger:7.0.4 +com.android:zipflinger:7.2.1 +com.android:zipflinger:7.3.1 +com.android:zipflinger:7.4.2 +com.android:zipflinger:8.1.1 +com.android:zipflinger:8.2.2 +com.diffplug.durian:durian-collect:1.2.0 +com.diffplug.durian:durian-core:1.2.0 +com.diffplug.durian:durian-io:1.2.0 +com.diffplug.spotless:spotless-lib-extra:2.26.2 +com.diffplug.spotless:spotless-lib-extra:2.30.0 +com.diffplug.spotless:spotless-lib:2.26.2 +com.diffplug.spotless:spotless-lib:2.30.0 +com.diffplug.spotless:spotless-plugin-gradle:6.7.2 +com.diffplug.spotless:spotless-plugin-gradle:6.11.0 +com.drewnoakes:metadata-extractor:2.18.0 +com.facebook.conceal:conceal:1.1.3 +com.facebook.fbjni:fbjni:0.5.1 +com.facebook.fresco:animated-base:2.5.0 +com.facebook.fresco:animated-drawable:2.5.0 +com.facebook.fresco:animated-gif:2.5.0 +com.facebook.fresco:drawee:2.5.0 +com.facebook.fresco:drawee:3.1.3 +com.facebook.fresco:fbcore:2.5.0 +com.facebook.fresco:fbcore:3.1.3 +com.facebook.fresco:fresco:2.5.0 +com.facebook.fresco:fresco:3.1.3 +com.facebook.fresco:imagepipeline-base:2.5.0 +com.facebook.fresco:imagepipeline-base:3.1.3 +com.facebook.fresco:imagepipeline-native:2.5.0 +com.facebook.fresco:imagepipeline-native:3.1.3 +com.facebook.fresco:imagepipeline-okhttp3:3.1.3 +com.facebook.fresco:imagepipeline:2.5.0 +com.facebook.fresco:imagepipeline:3.1.3 +com.facebook.fresco:memory-type-ashmem:2.5.0 +com.facebook.fresco:memory-type-ashmem:3.1.3 +com.facebook.fresco:memory-type-java:2.5.0 +com.facebook.fresco:memory-type-java:3.1.3 +com.facebook.fresco:memory-type-native:2.5.0 +com.facebook.fresco:memory-type-native:3.1.3 +com.facebook.fresco:middleware:3.1.3 +com.facebook.fresco:nativeimagefilters:2.5.0 +com.facebook.fresco:nativeimagefilters:3.1.3 +com.facebook.fresco:nativeimagetranscoder:2.5.0 +com.facebook.fresco:nativeimagetranscoder:3.1.3 +com.facebook.fresco:soloader:3.1.3 +com.facebook.fresco:ui-common:3.1.3 +com.facebook.infer.annotation:infer-annotation:0.18.0 +com.facebook.react:react-android:0.73.5 +com.facebook.soloader:annotation:0.10.5 +com.facebook.soloader:nativeloader:0.10.5 +com.facebook.soloader:soloader:0.10.5 +com.facebook.yoga:proguard-annotations:1.19.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.1 +com.fasterxml.jackson.core:jackson-core:2.11.1 +com.fasterxml.jackson.core:jackson-databind:2.11.1 +com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.1 +com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.11.1 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1 +com.fasterxml.woodstox:woodstox-core:6.2.1 +com.github.Dimezis:BlurView:version-2.0.4 +com.github.bumptech.glide:annotations:4.12.0 +com.github.bumptech.glide:compiler:4.12.0 +com.github.bumptech.glide:disklrucache:4.12.0 +com.github.bumptech.glide:gifdecoder:4.12.0 +com.github.bumptech.glide:glide:4.12.0 +com.github.bumptech.glide:okhttp3-integration:4.12.0 +com.github.clans:fab:1.6.4 +com.github.gundy:semver4j:0.16.4 +com.github.status-im.status-keycard-java:android:3.1.2 +com.github.status-im.status-keycard-java:lib:3.1.2 +com.github.status-im:function:0.0.1 +com.github.wix-playground:ahbottomnavigation:3.3.0 +com.github.wix-playground:reflow-animator:1.0.6 +com.github.yalantis:ucrop:2.2.6-native +com.github.zacharee:AndroidPdfViewer:4.0.1 +com.googlecode.concurrent-trees:concurrent-trees:2.6.1 +com.googlecode.javaewah:JavaEWAH:1.1.12 +com.googlecode.javaewah:JavaEWAH:1.1.13 +com.googlecode.json-simple:json-simple:1.1 +com.googlecode.juniversalchardet:juniversalchardet:1.0.3 +com.google.android.datatransport:transport-api:3.0.0 +com.google.android.datatransport:transport-api:3.1.0 +com.google.android.datatransport:transport-backend-cct:3.1.9 +com.google.android.datatransport:transport-runtime:3.1.9 +com.google.android.gms:play-services-auth-api-phone:18.0.2 +com.google.android.gms:play-services-auth-base:18.0.10 +com.google.android.gms:play-services-auth:21.3.0 +com.google.android.gms:play-services-basement:18.0.0 +com.google.android.gms:play-services-basement:18.4.0 +com.google.android.gms:play-services-base:18.5.0 +com.google.android.gms:play-services-cloud-messaging:17.2.0 +com.google.android.gms:play-services-fido:20.0.1 +com.google.android.gms:play-services-stats:17.0.2 +com.google.android.gms:play-services-tasks:18.0.1 +com.google.android.gms:play-services-tasks:18.2.0 +com.google.android.gms:strict-version-matcher-plugin:1.2.4 +com.google.android.material:material:1.2.0-alpha03 +com.google.android:annotations:4.1.1.4 +com.google.api.grpc:proto-google-common-protos:1.12.0 +com.google.api.grpc:proto-google-common-protos:2.0.1 +com.google.auto.value:auto-value-annotations:1.6.2 +com.google.auto.value:auto-value-annotations:1.6.3 +com.google.auto.value:auto-value-annotations:1.7.4 +com.google.auto.value:auto-value:1.5.2 +com.google.code.findbugs:jsr305:1.3.9 +com.google.code.findbugs:jsr305:3.0.2 +com.google.code.gson:gson:2.2.4 +com.google.code.gson:gson:2.3 +com.google.code.gson:gson:2.8.0 +com.google.code.gson:gson:2.8.5 +com.google.code.gson:gson:2.8.6 +com.google.code.gson:gson:2.8.9 +com.google.code.gson:gson:2.9.1 +com.google.code.gson:gson:2.10 +com.google.crypto.tink:tink:1.3.0-rc2 +com.google.crypto.tink:tink:1.7.0 +com.google.dagger:dagger:2.28.3 +com.google.errorprone:error_prone_annotations:2.0.18 +com.google.errorprone:error_prone_annotations:2.2.0 +com.google.errorprone:error_prone_annotations:2.3.1 +com.google.errorprone:error_prone_annotations:2.3.2 +com.google.errorprone:error_prone_annotations:2.3.4 +com.google.errorprone:error_prone_annotations:2.4.0 +com.google.errorprone:error_prone_annotations:2.7.1 +com.google.errorprone:error_prone_annotations:2.9.0 +com.google.errorprone:error_prone_annotations:2.11.0 +com.google.errorprone:error_prone_annotations:2.26.0 +com.google.firebase:firebase-annotations:16.2.0 +com.google.firebase:firebase-bom:33.8.0 +com.google.firebase:firebase-common-ktx:21.0.0 +com.google.firebase:firebase-common:21.0.0 +com.google.firebase:firebase-components:18.0.0 +com.google.firebase:firebase-datatransport:18.2.0 +com.google.firebase:firebase-encoders-json:18.0.0 +com.google.firebase:firebase-encoders-proto:16.0.0 +com.google.firebase:firebase-encoders:17.0.0 +com.google.firebase:firebase-iid-interop:17.1.0 +com.google.firebase:firebase-installations-interop:17.1.1 +com.google.firebase:firebase-installations:17.2.0 +com.google.firebase:firebase-installations:18.0.0 +com.google.firebase:firebase-measurement-connector:19.0.0 +com.google.firebase:firebase-messaging:24.1.0 +com.google.flatbuffers:flatbuffers-java:1.12.0 +com.google.gms:google-services:4.4.2 +com.google.guava:failureaccess:1.0.1 +com.google.guava:guava:17.0 +com.google.guava:guava:22.0 +com.google.guava:guava:23.0 +com.google.guava:guava:27.0.1-jre +com.google.guava:guava:28.1-jre +com.google.guava:guava:29.0-jre +com.google.guava:guava:30.1-jre +com.google.guava:guava:31.0.1-jre +com.google.guava:guava:31.1-jre +com.google.guava:listenablefuture:1.0 +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava +com.google.j2objc:j2objc-annotations:1.1 +com.google.j2objc:j2objc-annotations:1.3 +com.google.jimfs:jimfs:1.1 +com.google.prefab:cli:2.0.0 +com.google.protobuf:protobuf-java-util:3.4.0 +com.google.protobuf:protobuf-java-util:3.10.0 +com.google.protobuf:protobuf-java-util:3.17.2 +com.google.protobuf:protobuf-java-util:3.19.3 +com.google.protobuf:protobuf-java:3.0.0 +com.google.protobuf:protobuf-java:3.4.0 +com.google.protobuf:protobuf-java:3.10.0 +com.google.protobuf:protobuf-java:3.17.2 +com.google.protobuf:protobuf-java:3.19.3 +com.google.testing.platform:android-device-provider-local:0.0.8-alpha08 +com.google.testing.platform:android-driver-instrumentation:0.0.8-alpha08 +com.google.testing.platform:android-test-plugin:0.0.8-alpha08 +com.google.testing.platform:core-proto:0.0.8-alpha04 +com.google.testing.platform:core-proto:0.0.8-alpha07 +com.google.testing.platform:core-proto:0.0.8-alpha08 +com.google.testing.platform:core:0.0.8-alpha08 +com.google.testing.platform:launcher:0.0.8-alpha08 +com.google.test.platform:core-proto:0.0.2-dev +com.google.zxing:core:3.4.1 +com.ibm.icu:icu4j:53.1 +com.intellij:annotations:12.0 +com.parse.bolts:bolts-tasks:1.4.0 +com.squareup.okhttp3:okhttp-tls:4.11.0 +com.squareup.okhttp3:okhttp-urlconnection:4.9.2 +com.squareup.okhttp3:okhttp:4.9.2 +com.squareup.okhttp3:okhttp:4.11.0 +com.squareup.okio:okio-jvm:3.2.0 +com.squareup.okio:okio:2.9.0 +com.squareup.okio:okio:3.2.0 +com.squareup:javapoet:1.8.0 +com.squareup:javapoet:1.10.0 +com.squareup:javapoet:1.13.0 +com.squareup:javawriter:2.5.0 +com.sun.activation:javax.activation:1.2.0 +com.sun.istack:istack-commons-runtime:2.21 +com.sun.istack:istack-commons-runtime:3.0.7 +com.sun.istack:istack-commons-runtime:3.0.8 +com.sun.xml.fastinfoset:FastInfoset:1.2.13 +com.sun.xml.fastinfoset:FastInfoset:1.2.15 +com.sun.xml.fastinfoset:FastInfoset:1.2.16 +com.tunnelvisionlabs:antlr4-annotations:4.5 +com.tunnelvisionlabs:antlr4-runtime:4.5 +com.tunnelvisionlabs:antlr4:4.5 +de.undercouch:gradle-download-task:4.1.1 +de.undercouch:gradle-download-task:5.0.1 +gradle.plugin.io.invertase.gradle:react-native:1.5 +io.grpc:grpc-api:1.21.1 +io.grpc:grpc-api:1.39.0 +io.grpc:grpc-api:1.45.1 +io.grpc:grpc-context:1.21.1 +io.grpc:grpc-context:1.39.0 +io.grpc:grpc-context:1.45.1 +io.grpc:grpc-core:1.21.1 +io.grpc:grpc-core:1.39.0 +io.grpc:grpc-core:1.45.1 +io.grpc:grpc-netty:1.21.1 +io.grpc:grpc-netty:1.39.0 +io.grpc:grpc-netty:1.45.1 +io.grpc:grpc-protobuf-lite:1.21.1 +io.grpc:grpc-protobuf-lite:1.39.0 +io.grpc:grpc-protobuf-lite:1.45.1 +io.grpc:grpc-protobuf:1.21.1 +io.grpc:grpc-protobuf:1.39.0 +io.grpc:grpc-protobuf:1.45.1 +io.grpc:grpc-stub:1.21.1 +io.grpc:grpc-stub:1.39.0 +io.grpc:grpc-stub:1.45.1 +io.legere:pdfiumandroid:1.0.19 +io.netty:netty-buffer:4.1.34.Final +io.netty:netty-buffer:4.1.52.Final +io.netty:netty-buffer:4.1.72.Final +io.netty:netty-codec-http2:4.1.34.Final +io.netty:netty-codec-http2:4.1.52.Final +io.netty:netty-codec-http2:4.1.72.Final +io.netty:netty-codec-http:4.1.34.Final +io.netty:netty-codec-http:4.1.52.Final +io.netty:netty-codec-http:4.1.72.Final +io.netty:netty-codec-socks:4.1.34.Final +io.netty:netty-codec-socks:4.1.52.Final +io.netty:netty-codec-socks:4.1.72.Final +io.netty:netty-codec:4.1.34.Final +io.netty:netty-codec:4.1.52.Final +io.netty:netty-codec:4.1.72.Final +io.netty:netty-common:4.1.34.Final +io.netty:netty-common:4.1.52.Final +io.netty:netty-common:4.1.72.Final +io.netty:netty-handler-proxy:4.1.34.Final +io.netty:netty-handler-proxy:4.1.52.Final +io.netty:netty-handler-proxy:4.1.72.Final +io.netty:netty-handler:4.1.34.Final +io.netty:netty-handler:4.1.52.Final +io.netty:netty-handler:4.1.72.Final +io.netty:netty-resolver:4.1.34.Final +io.netty:netty-resolver:4.1.52.Final +io.netty:netty-resolver:4.1.72.Final +io.netty:netty-tcnative-classes:2.0.46.Final +io.netty:netty-transport:4.1.34.Final +io.netty:netty-transport:4.1.52.Final +io.netty:netty-transport:4.1.72.Final +io.opencensus:opencensus-api:0.21.0 +io.opencensus:opencensus-contrib-grpc-metrics:0.21.0 +io.perfmark:perfmark-api:0.23.0 +it.unimi.dsi:fastutil:7.2.0 +it.unimi.dsi:fastutil:8.4.0 +jakarta.activation:jakarta.activation-api:1.2.1 +jakarta.xml.bind:jakarta.xml.bind-api:2.3.2 +javax.activation:javax.activation-api:1.2.0 +javax.annotation:javax.annotation-api:1.3.2 +javax.inject:javax.inject:1 +javax.xml.bind:jaxb-api:2.2.12-b140109.1041 +javax.xml.bind:jaxb-api:2.3.1 +junit:junit:4.12 +junit:junit:4.13.2 +me.zhanghai.android.materialprogressbar:library:1.4.2 +net.bytebuddy:byte-buddy-agent:1.11.13 +net.bytebuddy:byte-buddy-agent:1.11.19 +net.bytebuddy:byte-buddy:1.11.13 +net.bytebuddy:byte-buddy:1.11.19 +net.java.dev.jna:jna-platform:5.6.0 +net.java.dev.jna:jna:5.6.0 +net.sf.jopt-simple:jopt-simple:4.9 +net.sf.kxml:kxml2:2.3.0 +net.sf.proguard:proguard-base:5.2.1 +net.sf.proguard:proguard-base:5.3.3 +net.sf.proguard:proguard-base:6.0.3 +net.sf.proguard:proguard-gradle:5.2.1 +net.sf.proguard:proguard-gradle:5.3.3 +net.sf.proguard:proguard-gradle:6.0.3 +org.abego.treelayout:org.abego.treelayout.core:1.0.1 +org.antlr:ST4:4.0.8 +org.antlr:antlr4:4.5.3 +org.antlr:antlr-runtime:3.5.2 +org.antlr:antlr:3.5.2 +org.apache.commons:commons-compress:1.8.1 +org.apache.commons:commons-compress:1.12 +org.apache.commons:commons-compress:1.20 +org.apache.commons:commons-compress:1.21 +org.apache.commons:commons-lang3:3.3.2 +org.apache.commons:commons-lang3:3.9 +org.apache.httpcomponents:httpclient:4.1.1 +org.apache.httpcomponents:httpclient:4.2.6 +org.apache.httpcomponents:httpclient:4.5.2 +org.apache.httpcomponents:httpclient:4.5.6 +org.apache.httpcomponents:httpclient:4.5.9 +org.apache.httpcomponents:httpclient:4.5.13 +org.apache.httpcomponents:httpclient:4.5.14 +org.apache.httpcomponents:httpcore:4.1 +org.apache.httpcomponents:httpcore:4.2.5 +org.apache.httpcomponents:httpcore:4.4.5 +org.apache.httpcomponents:httpcore:4.4.10 +org.apache.httpcomponents:httpcore:4.4.11 +org.apache.httpcomponents:httpcore:4.4.13 +org.apache.httpcomponents:httpcore:4.4.15 +org.apache.httpcomponents:httpcore:4.4.16 +org.apache.httpcomponents:httpmime:4.1 +org.apache.httpcomponents:httpmime:4.5.2 +org.apache.httpcomponents:httpmime:4.5.6 +org.bitbucket.b_c:jose4j:0.7.0 +org.bouncycastle:bcpkix-jdk15on:1.48 +org.bouncycastle:bcpkix-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.67 +org.bouncycastle:bcprov-jdk15on:1.48 +org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcprov-jdk15on:1.60 +org.bouncycastle:bcprov-jdk15on:1.67 +org.bouncycastle:bcprov-jdk15on:1.68 +org.checkerframework:checker-qual:2.5.2 +org.checkerframework:checker-qual:2.8.1 +org.checkerframework:checker-qual:2.11.1 +org.checkerframework:checker-qual:3.5.0 +org.checkerframework:checker-qual:3.12.0 +org.codehaus.groovy:groovy-xml:3.0.9 +org.codehaus.groovy:groovy-xml:3.0.10 +org.codehaus.groovy:groovy:3.0.9 +org.codehaus.groovy:groovy:3.0.10 +org.codehaus.mojo:animal-sniffer-annotations:1.14 +org.codehaus.mojo:animal-sniffer-annotations:1.17 +org.codehaus.mojo:animal-sniffer-annotations:1.18 +org.codehaus.mojo:animal-sniffer-annotations:1.19 +org.codehaus.woodstox:stax2-api:4.2.1 +org.eclipse.jdt.core.compiler:ecj:4.4.2 +org.eclipse.jdt.core.compiler:ecj:4.6.1 +org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r +org.eclipse.jgit:org.eclipse.jgit:5.13.1.202206130422-r +org.fusesource.jansi:jansi:1.18 +org.glassfish.jaxb:jaxb-core:2.2.11 +org.glassfish.jaxb:jaxb-runtime:2.2.11 +org.glassfish.jaxb:jaxb-runtime:2.3.1 +org.glassfish.jaxb:jaxb-runtime:2.3.2 +org.glassfish.jaxb:txw2:2.2.11 +org.glassfish.jaxb:txw2:2.3.1 +org.glassfish.jaxb:txw2:2.3.2 +org.gradle.toolchains:foojay-resolver:0.5.0 +org.gradle:github-dependency-graph-gradle-plugin:1.3.2 +org.hamcrest:hamcrest-core:1.3 +org.hamcrest:hamcrest:2.2 +org.jacoco:org.jacoco.core:0.7.4.201502262128 +org.jacoco:org.jacoco.report:0.7.4.201502262128 +org.jdom:jdom2:2.0.6 +org.jetbrains.dokka:dokka-core:1.4.32 +org.jetbrains.intellij.deps:trove4j:1.0.20181211 +org.jetbrains.intellij.deps:trove4j:1.0.20200330 +org.jetbrains.kotlinx:atomicfu:0.16.3 +org.jetbrains.kotlinx:atomicfu:0.17.3 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.1 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4 +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3 +org.jetbrains.kotlin:kotlin-android-extensions:1.6.20 +org.jetbrains.kotlin:kotlin-android-extensions:1.8.0 +org.jetbrains.kotlin:kotlin-android-extensions:1.9.0 +org.jetbrains.kotlin:kotlin-annotations-jvm:1.3.72 +org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.6.20 +org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.8.0 +org.jetbrains.kotlin:kotlin-build-common:1.6.20 +org.jetbrains.kotlin:kotlin-build-common:1.8.0 +org.jetbrains.kotlin:kotlin-build-tools-api:1.9.0 +org.jetbrains.kotlin:kotlin-build-tools-impl:1.9.0 +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.20 +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.0 +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.0 +org.jetbrains.kotlin:kotlin-compiler-runner:1.6.20 +org.jetbrains.kotlin:kotlin-compiler-runner:1.8.0 +org.jetbrains.kotlin:kotlin-compiler-runner:1.9.0 +org.jetbrains.kotlin:kotlin-daemon-client:1.6.20 +org.jetbrains.kotlin:kotlin-daemon-client:1.8.0 +org.jetbrains.kotlin:kotlin-daemon-client:1.9.0 +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.6.20 +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.8.0 +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.0 +org.jetbrains.kotlin:kotlin-gradle-plugins-bom:1.9.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:1.9.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.20 +org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.8.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.8.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.9.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.8.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.9.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.6.20 +org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.8.0 +org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.9.0 +org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20 +org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0 +org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0 +org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.6.20 +org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.8.0 +org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.9.0 +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.8.0 +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.9.0 +org.jetbrains.kotlin:kotlin-native-utils:1.6.20 +org.jetbrains.kotlin:kotlin-native-utils:1.8.0 +org.jetbrains.kotlin:kotlin-native-utils:1.9.0 +org.jetbrains.kotlin:kotlin-project-model:1.6.20 +org.jetbrains.kotlin:kotlin-project-model:1.8.0 +org.jetbrains.kotlin:kotlin-project-model:1.9.0 +org.jetbrains.kotlin:kotlin-reflect:1.1.3-2 +org.jetbrains.kotlin:kotlin-reflect:1.2.0 +org.jetbrains.kotlin:kotlin-reflect:1.3.50 +org.jetbrains.kotlin:kotlin-reflect:1.3.72 +org.jetbrains.kotlin:kotlin-reflect:1.4.32 +org.jetbrains.kotlin:kotlin-reflect:1.5.31 +org.jetbrains.kotlin:kotlin-reflect:1.6.10 +org.jetbrains.kotlin:kotlin-reflect:1.7.10 +org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC2 +org.jetbrains.kotlin:kotlin-reflect:1.9.0 +org.jetbrains.kotlin:kotlin-scripting-common:1.6.20 +org.jetbrains.kotlin:kotlin-scripting-common:1.8.0 +org.jetbrains.kotlin:kotlin-scripting-common:1.9.0 +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.6.20 +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.8.0 +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.0 +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.6.20 +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.8.0 +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.0 +org.jetbrains.kotlin:kotlin-scripting-jvm:1.6.20 +org.jetbrains.kotlin:kotlin-scripting-jvm:1.8.0 +org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.0 +org.jetbrains.kotlin:kotlin-script-runtime:1.8.0 +org.jetbrains.kotlin:kotlin-script-runtime:1.9.0 +org.jetbrains.kotlin:kotlin-stdlib-common:1.2.71 +org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50 +org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32 +org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 +org.jetbrains.kotlin:kotlin-stdlib-common:1.6.20 +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0 +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20-RC2 +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.71 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.32 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.20 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20-RC2 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.71 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.50 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20-RC2 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 +org.jetbrains.kotlin:kotlin-stdlib:1.1.3-2 +org.jetbrains.kotlin:kotlin-stdlib:1.2.71 +org.jetbrains.kotlin:kotlin-stdlib:1.3.50 +org.jetbrains.kotlin:kotlin-stdlib:1.3.72 +org.jetbrains.kotlin:kotlin-stdlib:1.4.32 +org.jetbrains.kotlin:kotlin-stdlib:1.5.31 +org.jetbrains.kotlin:kotlin-stdlib:1.6.20 +org.jetbrains.kotlin:kotlin-stdlib:1.7.10 +org.jetbrains.kotlin:kotlin-stdlib:1.8.0 +org.jetbrains.kotlin:kotlin-stdlib:1.8.20-RC2 +org.jetbrains.kotlin:kotlin-stdlib:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib:1.8.22 +org.jetbrains.kotlin:kotlin-stdlib:1.9.0 +org.jetbrains.kotlin:kotlin-stdlib:1.9.22 +org.jetbrains.kotlin:kotlin-tooling-core:1.8.0 +org.jetbrains.kotlin:kotlin-tooling-core:1.9.0 +org.jetbrains.kotlin:kotlin-tooling-metadata:1.6.20 +org.jetbrains.kotlin:kotlin-util-io:1.6.20 +org.jetbrains.kotlin:kotlin-util-io:1.8.0 +org.jetbrains.kotlin:kotlin-util-io:1.9.0 +org.jetbrains.kotlin:kotlin-util-klib:1.6.20 +org.jetbrains.kotlin:kotlin-util-klib:1.8.0 +org.jetbrains.kotlin:kotlin-util-klib:1.9.0 +org.jetbrains.trove4j:trove4j:20160824 +org.jetbrains:annotations:13.0 +org.jetbrains:annotations:23.0.0 +org.jetbrains:markdown-jvm:0.2.1 +org.jetbrains:markdown:0.2.1 +org.json:json:20180813 +org.jsoup:jsoup:1.13.1 +org.jvnet.staxex:stax-ex:1.7.7 +org.jvnet.staxex:stax-ex:1.8 +org.jvnet.staxex:stax-ex:1.8.1 +org.mockito:mockito-core:3.12.4 +org.mockito:mockito-core:4.0.0 +org.mockito:mockito-inline:4.0.0 +org.objenesis:objenesis:3.2 +org.ow2.asm:asm-analysis:5.0.3 +org.ow2.asm:asm-analysis:5.1 +org.ow2.asm:asm-analysis:6.0 +org.ow2.asm:asm-analysis:7.0 +org.ow2.asm:asm-analysis:9.1 +org.ow2.asm:asm-analysis:9.2 +org.ow2.asm:asm-commons:5.1 +org.ow2.asm:asm-commons:6.0 +org.ow2.asm:asm-commons:7.0 +org.ow2.asm:asm-commons:9.1 +org.ow2.asm:asm-commons:9.2 +org.ow2.asm:asm-debug-all:5.0.1 +org.ow2.asm:asm-tree:5.0.3 +org.ow2.asm:asm-tree:5.1 +org.ow2.asm:asm-tree:6.0 +org.ow2.asm:asm-tree:7.0 +org.ow2.asm:asm-tree:9.1 +org.ow2.asm:asm-tree:9.2 +org.ow2.asm:asm-util:5.1 +org.ow2.asm:asm-util:6.0 +org.ow2.asm:asm-util:7.0 +org.ow2.asm:asm-util:9.1 +org.ow2.asm:asm-util:9.2 +org.ow2.asm:asm:5.0.3 +org.ow2.asm:asm:5.1 +org.ow2.asm:asm:6.0 +org.ow2.asm:asm:7.0 +org.ow2.asm:asm:9.1 +org.ow2.asm:asm:9.2 +org.robolectric:annotations:4.7.3 +org.robolectric:junit:4.7.3 +org.robolectric:nativeruntime:4.7.3 +org.robolectric:pluginapi:4.7.3 +org.robolectric:plugins-maven-dependency-resolver:4.7.3 +org.robolectric:resources:4.7.3 +org.robolectric:robolectric:4.7.3 +org.robolectric:sandbox:4.7.3 +org.robolectric:shadowapi:4.7.3 +org.robolectric:shadows-framework:4.7.3 +org.robolectric:utils-reflector:4.7.3 +org.robolectric:utils:4.7.3 +org.slf4j:slf4j-api:1.7.30 +org.tensorflow:tensorflow-lite-metadata:0.1.0-rc1 +org.tensorflow:tensorflow-lite-metadata:0.1.0-rc2 +xerces:xercesImpl:2.12.0 +xml-apis:xml-apis:1.4.01 +org.gradle.toolchains.foojay-resolver-convention:org.gradle.toolchains.foojay-resolver-convention.gradle.plugin:0.5.0 +org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:1.8.0 +com.android.tools.build:gradle:3.5.4 +com.facebook.react:hermes-android:0.73.5 +com.android.tools.lint:lint-gradle:31.1.1 diff --git a/nix/deps/gradle/deps.urls b/nix/deps/gradle/deps.urls new file mode 100644 index 00000000000..9ee533e079b --- /dev/null +++ b/nix/deps/gradle/deps.urls @@ -0,0 +1,1631 @@ +https://dl.google.com/dl/android/maven2/androidx/activity/activity/1.0.0-rc01/activity-1.0.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/activity/activity/1.0.0/activity-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/activity/activity/1.1.0/activity-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/activity/activity/1.2.4/activity-1.2.4.pom +https://dl.google.com/dl/android/maven2/androidx/activity/activity/1.5.1/activity-1.5.1.pom +https://dl.google.com/dl/android/maven2/androidx/activity/activity/1.6.0/activity-1.6.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation-experimental/1.0.0/annotation-experimental-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation-experimental/1.1.0/annotation-experimental-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation-experimental/1.3.0/annotation-experimental-1.3.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation-jvm/1.6.0/annotation-jvm-1.6.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.0.0/annotation-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.0.1/annotation-1.0.1.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.0.2/annotation-1.0.2.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.1.0-rc01/annotation-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.2.0/annotation-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.3.0/annotation-1.3.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.5.0/annotation-1.5.0.pom +https://dl.google.com/dl/android/maven2/androidx/annotation/annotation/1.6.0/annotation-1.6.0.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat-resources/1.1.0-rc01/appcompat-resources-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat-resources/1.1.0/appcompat-resources-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat-resources/1.2.0/appcompat-resources-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat-resources/1.3.1/appcompat-resources-1.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat-resources/1.6.1/appcompat-resources-1.6.1.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat/1.0.0/appcompat-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat/1.1.0-rc01/appcompat-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat/1.1.0/appcompat-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat/1.2.0/appcompat-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat/1.3.1/appcompat-1.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/appcompat/appcompat/1.6.1/appcompat-1.6.1.pom +https://dl.google.com/dl/android/maven2/androidx/arch/core/core-common/2.0.0/core-common-2.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/arch/core/core-common/2.0.1/core-common-2.0.1.pom +https://dl.google.com/dl/android/maven2/androidx/arch/core/core-common/2.1.0-rc01/core-common-2.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/arch/core/core-common/2.1.0/core-common-2.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/arch/core/core-runtime/2.0.0/core-runtime-2.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/arch/core/core-runtime/2.1.0/core-runtime-2.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/asynclayoutinflater/asynclayoutinflater/1.0.0/asynclayoutinflater-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/autofill/autofill/1.1.0/autofill-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/biometric/biometric/1.1.0/biometric-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/camera/camera-camera2/1.1.0/camera-camera2-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/camera/camera-core/1.1.0/camera-core-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/camera/camera-lifecycle/1.1.0/camera-lifecycle-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/camera/camera-view/1.1.0/camera-view-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/cardview/cardview/1.0.0/cardview-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/collection/collection/1.0.0/collection-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/collection/collection/1.1.0/collection-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/concurrent/concurrent-futures/1.0.0/concurrent-futures-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/concurrent/concurrent-futures/1.1.0/concurrent-futures-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/constraintlayout/constraintlayout-solver/2.0.4/constraintlayout-solver-2.0.4.pom +https://dl.google.com/dl/android/maven2/androidx/constraintlayout/constraintlayout/2.0.4/constraintlayout-2.0.4.pom +https://dl.google.com/dl/android/maven2/androidx/coordinatorlayout/coordinatorlayout/1.0.0/coordinatorlayout-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/coordinatorlayout/coordinatorlayout/1.1.0/coordinatorlayout-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core-ktx/1.2.0/core-ktx-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core-ktx/1.6.0/core-ktx-1.6.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core-ktx/1.8.0/core-ktx-1.8.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core-ktx/1.9.0/core-ktx-1.9.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core-ktx/1.12.0/core-ktx-1.12.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core-splashscreen/1.0.0/core-splashscreen-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.0.0/core-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.0.1/core-1.0.1.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.1.0-rc01/core-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.1.0/core-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.2.0/core-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.3.0/core-1.3.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.3.1/core-1.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.3.2/core-1.3.2.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.5.0/core-1.5.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.6.0/core-1.6.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.8.0/core-1.8.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.9.0/core-1.9.0.pom +https://dl.google.com/dl/android/maven2/androidx/core/core/1.12.0/core-1.12.0.pom +https://dl.google.com/dl/android/maven2/androidx/cursoradapter/cursoradapter/1.0.0/cursoradapter-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/customview/customview/1.0.0/customview-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/3.2.1/databinding-common-3.2.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/3.5.3/databinding-common-3.5.3.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/3.5.4/databinding-common-3.5.4.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/4.1.0/databinding-common-4.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/7.0.4/databinding-common-7.0.4.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/7.2.1/databinding-common-7.2.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/7.3.1/databinding-common-7.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/7.4.2/databinding-common-7.4.2.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/8.1.1/databinding-common-8.1.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/8.2.2/databinding-common-8.2.2.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/3.2.1/databinding-compiler-common-3.2.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/3.5.3/databinding-compiler-common-3.5.3.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/3.5.4/databinding-compiler-common-3.5.4.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/4.1.0/databinding-compiler-common-4.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/7.0.4/databinding-compiler-common-7.0.4.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/7.2.1/databinding-compiler-common-7.2.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/7.3.1/databinding-compiler-common-7.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/7.4.2/databinding-compiler-common-7.4.2.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/8.1.1/databinding-compiler-common-8.1.1.pom +https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/8.2.2/databinding-compiler-common-8.2.2.pom +https://dl.google.com/dl/android/maven2/androidx/documentfile/documentfile/1.0.0/documentfile-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/drawerlayout/drawerlayout/1.0.0/drawerlayout-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/emoji2/emoji2-views-helper/1.2.0/emoji2-views-helper-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/emoji2/emoji2/1.2.0/emoji2-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/exifinterface/exifinterface/1.1.0-beta01/exifinterface-1.1.0-beta01.pom +https://dl.google.com/dl/android/maven2/androidx/exifinterface/exifinterface/1.1.0-rc01/exifinterface-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/exifinterface/exifinterface/1.2.0/exifinterface-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/exifinterface/exifinterface/1.3.2/exifinterface-1.3.2.pom +https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.0.0/fragment-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.1.0-rc01/fragment-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.1.0/fragment-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.2.5/fragment-1.2.5.pom +https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.3.6/fragment-1.3.6.pom +https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.5.7/fragment-1.5.7.pom +https://dl.google.com/dl/android/maven2/androidx/interpolator/interpolator/1.0.0/interpolator-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/legacy/legacy-support-core-ui/1.0.0/legacy-support-core-ui-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/legacy/legacy-support-core-utils/1.0.0/legacy-support-core-utils-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/legacy/legacy-support-v4/1.0.0/legacy-support-v4-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.0.0/lifecycle-common-2.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.1.0-rc01/lifecycle-common-2.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.1.0/lifecycle-common-2.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.2.0/lifecycle-common-2.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.3.1/lifecycle-common-2.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.4.0/lifecycle-common-2.4.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.4.1/lifecycle-common-2.4.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-common/2.5.1/lifecycle-common-2.5.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-livedata-core/2.0.0/lifecycle-livedata-core-2.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-livedata-core/2.1.0/lifecycle-livedata-core-2.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-livedata-core/2.2.0/lifecycle-livedata-core-2.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-livedata-core/2.3.1/lifecycle-livedata-core-2.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-livedata-core/2.5.1/lifecycle-livedata-core-2.5.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-livedata/2.0.0/lifecycle-livedata-2.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-livedata/2.1.0/lifecycle-livedata-2.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-process/2.4.1/lifecycle-process-2.4.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime/2.0.0/lifecycle-runtime-2.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime/2.1.0-rc01/lifecycle-runtime-2.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime/2.1.0/lifecycle-runtime-2.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime/2.2.0/lifecycle-runtime-2.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime/2.3.1/lifecycle-runtime-2.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime/2.4.1/lifecycle-runtime-2.4.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime/2.5.1/lifecycle-runtime-2.5.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel-savedstate/1.0.0/lifecycle-viewmodel-savedstate-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel-savedstate/2.2.0/lifecycle-viewmodel-savedstate-2.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel-savedstate/2.3.1/lifecycle-viewmodel-savedstate-2.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel-savedstate/2.5.1/lifecycle-viewmodel-savedstate-2.5.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel/2.0.0/lifecycle-viewmodel-2.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel/2.1.0-rc01/lifecycle-viewmodel-2.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel/2.1.0/lifecycle-viewmodel-2.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel/2.2.0/lifecycle-viewmodel-2.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel/2.3.1/lifecycle-viewmodel-2.3.1.pom +https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-viewmodel/2.5.1/lifecycle-viewmodel-2.5.1.pom +https://dl.google.com/dl/android/maven2/androidx/loader/loader/1.0.0/loader-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/loader/loader/1.1.0/loader-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/localbroadcastmanager/localbroadcastmanager/1.0.0/localbroadcastmanager-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/media/media/1.0.0/media-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/multidex/multidex/2.0.1/multidex-2.0.1.pom +https://dl.google.com/dl/android/maven2/androidx/print/print/1.0.0/print-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/recyclerview/recyclerview/1.0.0/recyclerview-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/recyclerview/recyclerview/1.1.0/recyclerview-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/resourceinspection/resourceinspection-annotation/1.0.1/resourceinspection-annotation-1.0.1.pom +https://dl.google.com/dl/android/maven2/androidx/savedstate/savedstate/1.0.0-rc01/savedstate-1.0.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/savedstate/savedstate/1.0.0/savedstate-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/savedstate/savedstate/1.1.0/savedstate-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/savedstate/savedstate/1.2.0/savedstate-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/slidingpanelayout/slidingpanelayout/1.0.0/slidingpanelayout-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/startup/startup-runtime/1.0.0/startup-runtime-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/startup/startup-runtime/1.1.1/startup-runtime-1.1.1.pom +https://dl.google.com/dl/android/maven2/androidx/swiperefreshlayout/swiperefreshlayout/1.0.0/swiperefreshlayout-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/swiperefreshlayout/swiperefreshlayout/1.1.0-alpha02/swiperefreshlayout-1.1.0-alpha02.pom +https://dl.google.com/dl/android/maven2/androidx/swiperefreshlayout/swiperefreshlayout/1.1.0/swiperefreshlayout-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/test/annotation/1.0.0/annotation-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/test/core/1.4.1-alpha05/core-1.4.1-alpha05.pom +https://dl.google.com/dl/android/maven2/androidx/test/ext/junit/1.1.4-alpha05/junit-1.1.4-alpha05.pom +https://dl.google.com/dl/android/maven2/androidx/test/monitor/1.4.0/monitor-1.4.0.pom +https://dl.google.com/dl/android/maven2/androidx/test/monitor/1.6.0-alpha02/monitor-1.6.0-alpha02.pom +https://dl.google.com/dl/android/maven2/androidx/test/runner/1.5.0-alpha02/runner-1.5.0-alpha02.pom +https://dl.google.com/dl/android/maven2/androidx/test/services/storage/1.4.2-alpha02/storage-1.4.2-alpha02.pom +https://dl.google.com/dl/android/maven2/androidx/tracing/tracing/1.0.0/tracing-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/tracing/tracing/1.1.0/tracing-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/transition/transition/1.0.0/transition-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/transition/transition/1.1.0/transition-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/transition/transition/1.2.0-rc01/transition-1.2.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/transition/transition/1.2.0/transition-1.2.0.pom +https://dl.google.com/dl/android/maven2/androidx/vectordrawable/vectordrawable-animated/1.0.0/vectordrawable-animated-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/vectordrawable/vectordrawable-animated/1.1.0-rc01/vectordrawable-animated-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/vectordrawable/vectordrawable-animated/1.1.0/vectordrawable-animated-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/vectordrawable/vectordrawable/1.0.0/vectordrawable-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/vectordrawable/vectordrawable/1.1.0-rc01/vectordrawable-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/vectordrawable/vectordrawable/1.1.0/vectordrawable-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/versionedparcelable/versionedparcelable/1.0.0/versionedparcelable-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/versionedparcelable/versionedparcelable/1.1.0-rc01/versionedparcelable-1.1.0-rc01.pom +https://dl.google.com/dl/android/maven2/androidx/versionedparcelable/versionedparcelable/1.1.0/versionedparcelable-1.1.0.pom +https://dl.google.com/dl/android/maven2/androidx/versionedparcelable/versionedparcelable/1.1.1/versionedparcelable-1.1.1.pom +https://dl.google.com/dl/android/maven2/androidx/viewpager2/viewpager2/1.0.0/viewpager2-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/viewpager/viewpager/1.0.0/viewpager-1.0.0.pom +https://dl.google.com/dl/android/maven2/androidx/webkit/webkit/1.4.0/webkit-1.4.0.pom +https://dl.google.com/dl/android/maven2/android/arch/core/common/1.0.0/common-1.0.0.pom +https://dl.google.com/dl/android/maven2/android/arch/lifecycle/common/1.0.0/common-1.0.0.pom +https://dl.google.com/dl/android/maven2/android/arch/lifecycle/runtime/1.0.0/runtime-1.0.0.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.0.1/baseLibrary-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.2.1/baseLibrary-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.5.3/baseLibrary-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.5.4/baseLibrary-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/4.1.0/baseLibrary-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/7.0.4/baseLibrary-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/7.2.1/baseLibrary-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/7.3.1/baseLibrary-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/7.4.2/baseLibrary-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/8.1.1/baseLibrary-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/8.2.2/baseLibrary-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/databinding/compilerCommon/3.0.1/compilerCommon-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/signflinger/4.1.0/signflinger-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/signflinger/7.0.4/signflinger-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/signflinger/7.2.1/signflinger-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/signflinger/7.3.1/signflinger-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/signflinger/7.4.2/signflinger-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/signflinger/8.1.1/signflinger-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/signflinger/8.2.2/signflinger-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/animated-vector-drawable/26.0.2/animated-vector-drawable-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/animated-vector-drawable/27.0.1/animated-vector-drawable-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/appcompat-v7/26.0.2/appcompat-v7-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/appcompat-v7/27.0.1/appcompat-v7-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/recyclerview-v7/27.0.1/recyclerview-v7-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-annotations/26.0.2/support-annotations-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-annotations/26.1.0/support-annotations-26.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-annotations/27.0.1/support-annotations-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-compat/26.0.2/support-compat-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-compat/26.1.0/support-compat-26.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-compat/27.0.1/support-compat-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-core-ui/26.0.2/support-core-ui-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-core-ui/26.1.0/support-core-ui-26.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-core-ui/27.0.1/support-core-ui-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-core-utils/26.0.2/support-core-utils-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-core-utils/26.1.0/support-core-utils-26.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-core-utils/27.0.1/support-core-utils-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-fragment/26.0.2/support-fragment-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-fragment/26.1.0/support-fragment-26.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-fragment/27.0.1/support-fragment-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-media-compat/26.0.2/support-media-compat-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-media-compat/26.1.0/support-media-compat-26.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-v4/26.0.2/support-v4-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-v4/26.1.0/support-v4-26.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-vector-drawable/26.0.2/support-vector-drawable-26.0.2.pom +https://dl.google.com/dl/android/maven2/com/android/support/support-vector-drawable/27.0.1/support-vector-drawable-27.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.2.1/crash-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.5.3/crash-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.5.4/crash-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/27.1.0/crash-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/30.0.4/crash-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/30.2.1/crash-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/30.3.1/crash-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/30.4.2/crash-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/31.1.1/crash-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/31.2.2/crash-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/26.0.1/protos-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/26.2.1/protos-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/26.5.3/protos-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/26.5.4/protos-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/27.1.0/protos-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/30.0.4/protos-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/30.2.1/protos-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/30.3.1/protos-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/30.4.2/protos-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/31.1.1/protos-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/31.2.2/protos-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/26.0.1/shared-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/26.2.1/shared-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/26.5.3/shared-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/26.5.4/shared-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/27.1.0/shared-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/30.0.4/shared-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/30.2.1/shared-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/30.3.1/shared-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/30.4.2/shared-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/31.1.1/shared-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/31.2.2/shared-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/26.0.1/tracker-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/26.2.1/tracker-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/26.5.3/tracker-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/26.5.4/tracker-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/27.1.0/tracker-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/30.0.4/tracker-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/30.2.1/tracker-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/30.3.1/tracker-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/30.4.2/tracker-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/31.1.1/tracker-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/31.2.2/tracker-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/26.0.1/annotations-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/26.2.1/annotations-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/26.5.3/annotations-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/26.5.4/annotations-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/27.1.0/annotations-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/30.0.4/annotations-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/30.2.1/annotations-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/30.3.1/annotations-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/30.4.2/annotations-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/31.1.1/annotations-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/annotations/31.2.2/annotations-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/0.3.1/aapt2-proto-0.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/0.4.0/aapt2-proto-0.4.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/4.1.0-6503028/aapt2-proto-4.1.0-6503028.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/4.1.0-alpha01-6193524/aapt2-proto-4.1.0-alpha01-6193524.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/7.0.0-beta04-7396180/aapt2-proto-7.0.0-beta04-7396180.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/7.0.4-7396180/aapt2-proto-7.0.4-7396180.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/7.2.1-7984345/aapt2-proto-7.2.1-7984345.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/7.3.0-alpha07-8248216/aapt2-proto-7.3.0-alpha07-8248216.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/7.3.1-8691043/aapt2-proto-7.3.1-8691043.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/7.4.2-8841542/aapt2-proto-7.4.2-8841542.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/8.1.1-10154469/aapt2-proto-8.1.1-10154469.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/8.2.2-10154469/aapt2-proto-8.2.2-10154469.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2/8.1.1-10154469/aapt2-8.1.1-10154469.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/4.1.0/aaptcompiler-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/7.0.4/aaptcompiler-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/7.2.1/aaptcompiler-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/7.3.1/aaptcompiler-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/7.4.2/aaptcompiler-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/8.1.1/aaptcompiler-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/8.2.2/aaptcompiler-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/3.0.1/apksig-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/3.2.1/apksig-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/3.5.3/apksig-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/3.5.4/apksig-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/4.1.0/apksig-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/7.0.4/apksig-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/7.2.1/apksig-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/7.3.1/apksig-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/7.4.2/apksig-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/8.1.1/apksig-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/8.2.2/apksig-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/3.2.1/apkzlib-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/3.5.3/apkzlib-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/3.5.4/apkzlib-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/4.1.0/apkzlib-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/7.0.4/apkzlib-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/7.2.1/apkzlib-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/7.3.1/apkzlib-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/7.4.2/apkzlib-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/8.1.1/apkzlib-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/8.2.2/apkzlib-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/3.0.1/builder-model-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/3.2.1/builder-model-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/3.5.3/builder-model-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/3.5.4/builder-model-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/4.1.0/builder-model-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/7.0.4/builder-model-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/7.2.1/builder-model-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/7.3.1/builder-model-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/7.4.2/builder-model-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/8.1.1/builder-model-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/8.2.2/builder-model-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/3.0.1/builder-test-api-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/3.2.1/builder-test-api-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/3.5.3/builder-test-api-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/3.5.4/builder-test-api-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/4.1.0/builder-test-api-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/7.0.4/builder-test-api-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/7.2.1/builder-test-api-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/7.3.1/builder-test-api-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/7.4.2/builder-test-api-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/8.1.1/builder-test-api-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/8.2.2/builder-test-api-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/3.0.1/builder-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/3.2.1/builder-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/3.5.3/builder-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/3.5.4/builder-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/4.1.0/builder-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/7.0.4/builder-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/7.2.1/builder-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/7.3.1/builder-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/7.4.2/builder-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/8.1.1/builder-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/8.2.2/builder-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/0.5.0/bundletool-0.5.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/0.9.0/bundletool-0.9.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/0.14.0/bundletool-0.14.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.6.0/bundletool-1.6.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.8.2/bundletool-1.8.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.9.0/bundletool-1.9.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.11.4/bundletool-1.11.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.14.0/bundletool-1.14.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.15.2/bundletool-1.15.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/3.0.1/gradle-api-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/3.2.1/gradle-api-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/3.5.3/gradle-api-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/3.5.4/gradle-api-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/4.1.0/gradle-api-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/7.0.4/gradle-api-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/7.2.1/gradle-api-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/7.3.1/gradle-api-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/7.4.2/gradle-api-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/8.1.1/gradle-api-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/8.2.2/gradle-api-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-core/3.0.1/gradle-core-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-settings-api/7.4.2/gradle-settings-api-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-settings-api/8.1.1/gradle-settings-api-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-settings-api/8.2.2/gradle-settings-api-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/3.0.1/gradle-3.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/3.2.1/gradle-3.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/3.5.3/gradle-3.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/3.5.4/gradle-3.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/4.1.0/gradle-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/7.0.4/gradle-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/7.2.1/gradle-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/7.3.1/gradle-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/7.4.2/gradle-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/8.1.1/gradle-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/8.2.2/gradle-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-core/1.0.0-alpha10/jetifier-core-1.0.0-alpha10.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta04/jetifier-core-1.0.0-beta04.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta09/jetifier-core-1.0.0-beta09.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10/jetifier-core-1.0.0-beta10.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-processor/1.0.0-alpha10/jetifier-processor-1.0.0-alpha10.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta04/jetifier-processor-1.0.0-beta04.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta09/jetifier-processor-1.0.0-beta09.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10/jetifier-processor-1.0.0-beta10.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/26.0.1/manifest-merger-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/26.2.1/manifest-merger-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/26.5.3/manifest-merger-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/26.5.4/manifest-merger-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/27.1.0/manifest-merger-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/30.0.4/manifest-merger-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/30.2.1/manifest-merger-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/30.3.1/manifest-merger-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/30.4.2/manifest-merger-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/31.1.1/manifest-merger-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/31.2.2/manifest-merger-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/26.0.1/common-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/26.2.1/common-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/26.5.3/common-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/26.5.4/common-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/27.1.0/common-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/30.0.4/common-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/30.2.1/common-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/30.3.1/common-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/30.4.2/common-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/31.1.1/common-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/common/31.2.2/common-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/26.0.1/ddmlib-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/26.2.1/ddmlib-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/26.5.3/ddmlib-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/26.5.4/ddmlib-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/27.1.0/ddmlib-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/30.0.4/ddmlib-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/30.2.1/ddmlib-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/30.3.1/ddmlib-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/30.4.2/ddmlib-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/31.1.1/ddmlib-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/31.2.2/ddmlib-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/26.0.1/dvlib-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/26.2.1/dvlib-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/26.5.3/dvlib-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/26.5.4/dvlib-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/27.1.0/dvlib-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/30.0.4/dvlib-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/30.2.1/dvlib-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/30.3.1/dvlib-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/30.4.2/dvlib-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/31.1.1/dvlib-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/31.2.2/dvlib-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/emulator/proto/31.1.1/proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/external/com-intellij/intellij-core/26.0.1/intellij-core-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/external/com-intellij/intellij-core/31.1.1/intellij-core-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/external/com-intellij/kotlin-compiler/31.1.1/kotlin-compiler-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/external/org-jetbrains/uast/26.0.1/uast-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/external/org-jetbrains/uast/31.1.1/uast-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/26.0.1/layoutlib-api-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/26.2.1/layoutlib-api-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/26.5.3/layoutlib-api-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/26.5.4/layoutlib-api-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/27.1.0/layoutlib-api-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/30.0.4/layoutlib-api-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/30.2.1/layoutlib-api-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/30.3.1/layoutlib-api-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/30.4.2/layoutlib-api-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/31.1.1/layoutlib-api-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/31.2.2/layoutlib-api-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-api/26.0.1/lint-api-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-api/31.1.1/lint-api-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-checks/26.0.1/lint-checks-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-checks/31.1.1/lint-checks-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-gradle-api/26.2.1/lint-gradle-api-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-gradle-api/26.5.3/lint-gradle-api-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-gradle-api/26.5.4/lint-gradle-api-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-gradle-api/27.1.0/lint-gradle-api-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-gradle/31.1.1/lint-gradle-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/27.1.0/lint-model-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/30.0.4/lint-model-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/30.2.1/lint-model-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/30.3.1/lint-model-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/30.4.2/lint-model-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/31.1.1/lint-model-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/31.2.2/lint-model-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-typedef-remover/30.2.1/lint-typedef-remover-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-typedef-remover/30.3.1/lint-typedef-remover-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-typedef-remover/30.4.2/lint-typedef-remover-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-typedef-remover/31.1.1/lint-typedef-remover-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-typedef-remover/31.2.2/lint-typedef-remover-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint/26.0.1/lint-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint/31.1.1/lint-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/play-sdk-proto/31.1.1/play-sdk-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/26.0.1/repository-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/26.2.1/repository-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/26.5.3/repository-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/26.5.4/repository-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/27.1.0/repository-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/30.0.4/repository-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/30.2.1/repository-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/30.3.1/repository-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/30.4.2/repository-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/31.1.1/repository-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/repository/31.2.2/repository-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/26.0.1/sdklib-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/26.2.1/sdklib-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/26.5.3/sdklib-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/26.5.4/sdklib-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/27.1.0/sdklib-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/30.0.4/sdklib-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/30.2.1/sdklib-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/30.3.1/sdklib-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/30.4.2/sdklib-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/31.1.1/sdklib-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/31.2.2/sdklib-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/26.0.1/sdk-common-26.0.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/26.2.1/sdk-common-26.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/26.5.3/sdk-common-26.5.3.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/26.5.4/sdk-common-26.5.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/27.1.0/sdk-common-27.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/30.0.4/sdk-common-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/30.2.1/sdk-common-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/30.3.1/sdk-common-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/30.4.2/sdk-common-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/31.1.1/sdk-common-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/31.2.2/sdk-common-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib-proto/30.2.1/android-device-provider-ddmlib-proto-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib-proto/30.3.1/android-device-provider-ddmlib-proto-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib-proto/30.4.2/android-device-provider-ddmlib-proto-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib-proto/31.1.1/android-device-provider-ddmlib-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib-proto/31.2.2/android-device-provider-ddmlib-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib/31.1.1/android-device-provider-ddmlib-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-gradle-proto/30.0.4/android-device-provider-gradle-proto-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-gradle-proto/30.2.1/android-device-provider-gradle-proto-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-gradle-proto/30.3.1/android-device-provider-gradle-proto-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-gradle-proto/30.4.2/android-device-provider-gradle-proto-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-gradle-proto/31.1.1/android-device-provider-gradle-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-gradle-proto/31.2.2/android-device-provider-gradle-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-gradle/31.1.1/android-device-provider-gradle-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/30.2.1/android-test-plugin-host-additional-test-output-proto-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/30.3.1/android-test-plugin-host-additional-test-output-proto-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/30.4.2/android-test-plugin-host-additional-test-output-proto-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.1.1/android-test-plugin-host-additional-test-output-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.2.2/android-test-plugin-host-additional-test-output-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output/31.1.1/android-test-plugin-host-additional-test-output-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.1.1/android-test-plugin-host-apk-installer-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.2.2/android-test-plugin-host-apk-installer-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-apk-installer/31.1.1/android-test-plugin-host-apk-installer-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage-proto/30.2.1/android-test-plugin-host-coverage-proto-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage-proto/30.3.1/android-test-plugin-host-coverage-proto-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage-proto/30.4.2/android-test-plugin-host-coverage-proto-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage-proto/31.1.1/android-test-plugin-host-coverage-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage-proto/31.2.2/android-test-plugin-host-coverage-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage/31.1.1/android-test-plugin-host-coverage-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-device-info-proto/31.1.1/android-test-plugin-host-device-info-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-device-info/31.1.1/android-test-plugin-host-device-info-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.1.1/android-test-plugin-host-emulator-control-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.2.2/android-test-plugin-host-emulator-control-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-emulator-control/31.1.1/android-test-plugin-host-emulator-control-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-logcat-proto/31.1.1/android-test-plugin-host-logcat-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-logcat-proto/31.2.2/android-test-plugin-host-logcat-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-logcat/31.1.1/android-test-plugin-host-logcat-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-retention-proto/30.0.4/android-test-plugin-host-retention-proto-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-retention-proto/30.2.1/android-test-plugin-host-retention-proto-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-retention-proto/30.3.1/android-test-plugin-host-retention-proto-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-retention-proto/30.4.2/android-test-plugin-host-retention-proto-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-retention-proto/31.1.1/android-test-plugin-host-retention-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-retention-proto/31.2.2/android-test-plugin-host-retention-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-retention/31.1.1/android-test-plugin-host-retention-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.0.4/android-test-plugin-result-listener-gradle-proto-30.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.2.1/android-test-plugin-result-listener-gradle-proto-30.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.3.1/android-test-plugin-result-listener-gradle-proto-30.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/30.4.2/android-test-plugin-result-listener-gradle-proto-30.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.1.1/android-test-plugin-result-listener-gradle-proto-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.2.2/android-test-plugin-result-listener-gradle-proto-31.2.2.pom +https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle/31.1.1/android-test-plugin-result-listener-gradle-31.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/zipflinger/4.1.0/zipflinger-4.1.0.pom +https://dl.google.com/dl/android/maven2/com/android/zipflinger/7.0.4/zipflinger-7.0.4.pom +https://dl.google.com/dl/android/maven2/com/android/zipflinger/7.2.1/zipflinger-7.2.1.pom +https://dl.google.com/dl/android/maven2/com/android/zipflinger/7.3.1/zipflinger-7.3.1.pom +https://dl.google.com/dl/android/maven2/com/android/zipflinger/7.4.2/zipflinger-7.4.2.pom +https://dl.google.com/dl/android/maven2/com/android/zipflinger/8.1.1/zipflinger-8.1.1.pom +https://dl.google.com/dl/android/maven2/com/android/zipflinger/8.2.2/zipflinger-8.2.2.pom +https://dl.google.com/dl/android/maven2/com/google/android/datatransport/transport-api/3.0.0/transport-api-3.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/datatransport/transport-api/3.1.0/transport-api-3.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/datatransport/transport-backend-cct/3.1.8/transport-backend-cct-3.1.8.pom +https://dl.google.com/dl/android/maven2/com/google/android/datatransport/transport-backend-cct/3.1.9/transport-backend-cct-3.1.9.pom +https://dl.google.com/dl/android/maven2/com/google/android/datatransport/transport-runtime/3.1.8/transport-runtime-3.1.8.pom +https://dl.google.com/dl/android/maven2/com/google/android/datatransport/transport-runtime/3.1.9/transport-runtime-3.1.9.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-auth-api-phone/18.0.2/play-services-auth-api-phone-18.0.2.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-auth-base/18.0.10/play-services-auth-base-18.0.10.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-auth/21.3.0/play-services-auth-21.3.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/16.0.1/play-services-basement-16.0.1.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/17.0.0/play-services-basement-17.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/18.0.0/play-services-basement-18.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/18.0.2/play-services-basement-18.0.2.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/18.1.0/play-services-basement-18.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/18.2.0/play-services-basement-18.2.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/18.3.0/play-services-basement-18.3.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-basement/18.4.0/play-services-basement-18.4.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-base/18.0.1/play-services-base-18.0.1.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-base/18.5.0/play-services-base-18.5.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-cloud-messaging/17.2.0/play-services-cloud-messaging-17.2.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-fido/20.0.1/play-services-fido-20.0.1.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-stats/17.0.2/play-services-stats-17.0.2.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-tasks/16.0.1/play-services-tasks-16.0.1.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-tasks/17.0.0/play-services-tasks-17.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-tasks/18.0.1/play-services-tasks-18.0.1.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-tasks/18.0.2/play-services-tasks-18.0.2.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-tasks/18.1.0/play-services-tasks-18.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-tasks/18.2.0/play-services-tasks-18.2.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/gms/strict-version-matcher-plugin/1.2.4/strict-version-matcher-plugin-1.2.4.pom +https://dl.google.com/dl/android/maven2/com/google/android/material/material/1.0.0/material-1.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/android/material/material/1.2.0-alpha03/material-1.2.0-alpha03.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-annotations/16.0.0/firebase-annotations-16.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-annotations/16.1.0/firebase-annotations-16.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-annotations/16.2.0/firebase-annotations-16.2.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-bom/33.8.0/firebase-bom-33.8.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-common-ktx/20.4.2/firebase-common-ktx-20.4.2.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-common-ktx/21.0.0/firebase-common-ktx-21.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-common/20.3.3/firebase-common-20.3.3.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-common/20.4.2/firebase-common-20.4.2.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-common/21.0.0/firebase-common-21.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-components/17.1.0/firebase-components-17.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-components/17.1.3/firebase-components-17.1.3.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-components/17.1.5/firebase-components-17.1.5.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-components/18.0.0/firebase-components-18.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-datatransport/18.2.0/firebase-datatransport-18.2.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-encoders-json/18.0.0/firebase-encoders-json-18.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-encoders-proto/16.0.0/firebase-encoders-proto-16.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-encoders/17.0.0/firebase-encoders-17.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-iid-interop/17.1.0/firebase-iid-interop-17.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-installations-interop/17.1.0/firebase-installations-interop-17.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-installations-interop/17.1.1/firebase-installations-interop-17.1.1.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-installations/17.2.0/firebase-installations-17.2.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-installations/18.0.0/firebase-installations-18.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-measurement-connector/19.0.0/firebase-measurement-connector-19.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-messaging/24.1.0/firebase-messaging-24.1.0.pom +https://dl.google.com/dl/android/maven2/com/google/gms/google-services/4.4.2/google-services-4.4.2.pom +https://dl.google.com/dl/android/maven2/com/google/prefab/cli/2.0.0/cli-2.0.0.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/android-device-provider-local/0.0.8-alpha08/android-device-provider-local-0.0.8-alpha08.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/android-driver-instrumentation/0.0.8-alpha08/android-driver-instrumentation-0.0.8-alpha08.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/android-test-plugin/0.0.8-alpha08/android-test-plugin-0.0.8-alpha08.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/core-proto/0.0.8-alpha04/core-proto-0.0.8-alpha04.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/core-proto/0.0.8-alpha07/core-proto-0.0.8-alpha07.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/core-proto/0.0.8-alpha08/core-proto-0.0.8-alpha08.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/core/0.0.8-alpha08/core-0.0.8-alpha08.pom +https://dl.google.com/dl/android/maven2/com/google/testing/platform/launcher/0.0.8-alpha08/launcher-0.0.8-alpha08.pom +https://dl.google.com/dl/android/maven2/com/google/test/platform/core-proto/0.0.2-dev/core-proto-0.0.2-dev.pom +https://jitpack.io/com/github/Dimezis/BlurView/version-2.0.4/BlurView-version-2.0.4.pom +https://jitpack.io/com/github/status-im/function/0.0.1/function-0.0.1.pom +https://jitpack.io/com/github/status-im/status-keycard-java/android/3.1.2/android-3.1.2.pom +https://jitpack.io/com/github/status-im/status-keycard-java/lib/3.1.2/lib-3.1.2.pom +https://jitpack.io/com/github/wix-playground/ahbottomnavigation/3.3.0/ahbottomnavigation-3.3.0.pom +https://jitpack.io/com/github/wix-playground/reflow-animator/1.0.6/reflow-animator-1.0.6.pom +https://jitpack.io/com/github/yalantis/ucrop/2.2.6-native/ucrop-2.2.6-native.pom +https://jitpack.io/com/github/zacharee/AndroidPdfViewer/4.0.1/AndroidPdfViewer-4.0.1.pom +https://plugins.gradle.org/m2/com/adarshr/gradle-test-logger-plugin/2.0.0/gradle-test-logger-plugin-2.0.0.pom +https://plugins.gradle.org/m2/gradle/plugin/io/invertase/gradle/react-native/1.5/react-native-1.5.pom +https://plugins.gradle.org/m2/io/invertase/gradle/build/io.invertase.gradle.build.gradle.plugin/1.5/io.invertase.gradle.build.gradle.plugin-1.5.pom +https://plugins.gradle.org/m2/org/gradle/github-dependency-graph-gradle-plugin/1.3.2/github-dependency-graph-gradle-plugin-1.3.2.pom +https://plugins.gradle.org/m2/org/gradle/toolchains/foojay-resolver-convention/org.gradle.toolchains.foojay-resolver-convention.gradle.plugin/0.5.0/org.gradle.toolchains.foojay-resolver-convention.gradle.plugin-0.5.0.pom +https://plugins.gradle.org/m2/org/gradle/toolchains/foojay-resolver/0.5.0/foojay-resolver-0.5.0.pom +https://repo.maven.apache.org/maven2/antlr/antlr/2.7.1/antlr-2.7.1.pom +https://repo.maven.apache.org/maven2/antlr/antlr/2.7.7/antlr-2.7.7.pom +https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.4/commons-codec-1.4.pom +https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.6/commons-codec-1.6.pom +https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.9/commons-codec-1.9.pom +https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.10/commons-codec-1.10.pom +https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.11/commons-codec-1.11.pom +https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.18.0/commons-codec-1.18.0.pom +https://repo.maven.apache.org/maven2/commons-io/commons-io/2.4/commons-io-2.4.pom +https://repo.maven.apache.org/maven2/commons-io/commons-io/2.18.0/commons-io-2.18.0.pom +https://repo.maven.apache.org/maven2/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1.pom +https://repo.maven.apache.org/maven2/commons-logging/commons-logging/1.2/commons-logging-1.2.pom +https://repo.maven.apache.org/maven2/commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.pom +https://repo.maven.apache.org/maven2/com/adobe/xmp/xmpcore/6.1.11/xmpcore-6.1.11.pom +https://repo.maven.apache.org/maven2/com/afollestad/material-dialogs/commons/0.9.6.0/commons-0.9.6.0.pom +https://repo.maven.apache.org/maven2/com/afollestad/material-dialogs/core/0.9.6.0/core-0.9.6.0.pom +https://repo.maven.apache.org/maven2/com/airbnb/android/lottie/3.4.4/lottie-3.4.4.pom +https://repo.maven.apache.org/maven2/com/almworks/sqlite4java/sqlite4java/1.0.392/sqlite4java-1.0.392.pom +https://repo.maven.apache.org/maven2/com/android/databinding/baseLibrary/1.0-rc5/baseLibrary-1.0-rc5.pom +https://repo.maven.apache.org/maven2/com/android/databinding/compilerCommon/1.0-rc5/compilerCommon-1.0-rc5.pom +https://repo.maven.apache.org/maven2/com/android/tools/annotations/24.3.1/annotations-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/annotations/24.5.0/annotations-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/builder-model/1.3.1/builder-model-1.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/builder-model/1.5.0/builder-model-1.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/builder-test-api/1.3.1/builder-test-api-1.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/builder-test-api/1.5.0/builder-test-api-1.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/builder/1.3.1/builder-1.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/builder/1.5.0/builder-1.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/gradle-core/1.3.1/gradle-core-1.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/gradle-core/1.5.0/gradle-core-1.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/gradle/1.3.1/gradle-1.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/gradle/1.5.0/gradle-1.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/manifest-merger/24.3.1/manifest-merger-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/manifest-merger/24.5.0/manifest-merger-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/transform-api/1.5.0/transform-api-1.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/build/transform-api/2.0.0-deprecated-use-gradle-api/transform-api-2.0.0-deprecated-use-gradle-api.pom +https://repo.maven.apache.org/maven2/com/android/tools/common/24.3.1/common-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/common/24.5.0/common-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/ddms/ddmlib/24.3.1/ddmlib-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/ddms/ddmlib/24.5.0/ddmlib-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/dvlib/24.3.1/dvlib-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/dvlib/24.5.0/dvlib-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/external/lombok/lombok-ast/0.2.3/lombok-ast-0.2.3.pom +https://repo.maven.apache.org/maven2/com/android/tools/jack/jack-api/0.9.0/jack-api-0.9.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/jill/jill-api/0.9.0/jill-api-0.9.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/layoutlib/layoutlib-api/24.3.1/layoutlib-api-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/layoutlib/layoutlib-api/24.5.0/layoutlib-api-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/lint/lint-api/24.3.1/lint-api-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/lint/lint-api/24.5.0/lint-api-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/lint/lint-checks/24.3.1/lint-checks-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/lint/lint-checks/24.5.0/lint-checks-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/lint/lint/24.3.1/lint-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/lint/lint/24.5.0/lint-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/sdklib/24.3.1/sdklib-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/sdklib/24.5.0/sdklib-24.5.0.pom +https://repo.maven.apache.org/maven2/com/android/tools/sdk-common/24.3.1/sdk-common-24.3.1.pom +https://repo.maven.apache.org/maven2/com/android/tools/sdk-common/24.5.0/sdk-common-24.5.0.pom +https://repo.maven.apache.org/maven2/com/diffplug/durian/durian-collect/1.2.0/durian-collect-1.2.0.pom +https://repo.maven.apache.org/maven2/com/diffplug/durian/durian-core/1.2.0/durian-core-1.2.0.pom +https://repo.maven.apache.org/maven2/com/diffplug/durian/durian-io/1.2.0/durian-io-1.2.0.pom +https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-lib-extra/2.26.2/spotless-lib-extra-2.26.2.pom +https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-lib-extra/2.30.0/spotless-lib-extra-2.30.0.pom +https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-lib/2.26.2/spotless-lib-2.26.2.pom +https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-lib/2.30.0/spotless-lib-2.30.0.pom +https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-plugin-gradle/6.7.2/spotless-plugin-gradle-6.7.2.pom +https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-plugin-gradle/6.11.0/spotless-plugin-gradle-6.11.0.pom +https://repo.maven.apache.org/maven2/com/drewnoakes/metadata-extractor/2.18.0/metadata-extractor-2.18.0.pom +https://repo.maven.apache.org/maven2/com/facebook/conceal/conceal/1.1.3/conceal-1.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fbjni/fbjni/0.5.1/fbjni-0.5.1.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/animated-base/2.5.0/animated-base-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/animated-drawable/2.5.0/animated-drawable-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/animated-gif/2.5.0/animated-gif-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/drawee/2.5.0/drawee-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/drawee/3.1.3/drawee-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/fbcore/2.5.0/fbcore-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/fbcore/3.1.3/fbcore-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/fresco/2.5.0/fresco-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/fresco/3.1.3/fresco-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/imagepipeline-base/2.5.0/imagepipeline-base-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/imagepipeline-base/3.1.3/imagepipeline-base-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/imagepipeline-native/2.5.0/imagepipeline-native-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/imagepipeline-native/3.1.3/imagepipeline-native-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/imagepipeline-okhttp3/3.1.3/imagepipeline-okhttp3-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/imagepipeline/2.5.0/imagepipeline-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/imagepipeline/3.1.3/imagepipeline-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/memory-type-ashmem/2.5.0/memory-type-ashmem-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/memory-type-ashmem/3.1.3/memory-type-ashmem-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/memory-type-java/2.5.0/memory-type-java-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/memory-type-java/3.1.3/memory-type-java-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/memory-type-native/2.5.0/memory-type-native-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/memory-type-native/3.1.3/memory-type-native-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/middleware/2.5.0/middleware-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/middleware/3.1.3/middleware-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/nativeimagefilters/2.5.0/nativeimagefilters-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/nativeimagefilters/3.1.3/nativeimagefilters-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/nativeimagetranscoder/2.5.0/nativeimagetranscoder-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/nativeimagetranscoder/3.1.3/nativeimagetranscoder-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/soloader/2.5.0/soloader-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/soloader/3.1.3/soloader-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/ui-common/2.5.0/ui-common-2.5.0.pom +https://repo.maven.apache.org/maven2/com/facebook/fresco/ui-common/3.1.3/ui-common-3.1.3.pom +https://repo.maven.apache.org/maven2/com/facebook/infer/annotation/infer-annotation/0.18.0/infer-annotation-0.18.0.pom +https://repo.maven.apache.org/maven2/com/facebook/react/hermes-android/0.73.5/hermes-android-0.73.5.pom +https://repo.maven.apache.org/maven2/com/facebook/react/react-android/0.73.5/react-android-0.73.5.pom +https://repo.maven.apache.org/maven2/com/facebook/soloader/annotation/0.10.1/annotation-0.10.1.pom +https://repo.maven.apache.org/maven2/com/facebook/soloader/annotation/0.10.5/annotation-0.10.5.pom +https://repo.maven.apache.org/maven2/com/facebook/soloader/nativeloader/0.10.1/nativeloader-0.10.1.pom +https://repo.maven.apache.org/maven2/com/facebook/soloader/nativeloader/0.10.5/nativeloader-0.10.5.pom +https://repo.maven.apache.org/maven2/com/facebook/soloader/soloader/0.10.1/soloader-0.10.1.pom +https://repo.maven.apache.org/maven2/com/facebook/soloader/soloader/0.10.5/soloader-0.10.5.pom +https://repo.maven.apache.org/maven2/com/facebook/yoga/proguard-annotations/1.19.0/proguard-annotations-1.19.0.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.11.1/jackson-annotations-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-annotations/3.0-rc2/jackson-annotations-3.0-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-core/2.11.1/jackson-core-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-core/2.19.0-rc2/jackson-core-2.19.0-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.11.1/jackson-databind-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.19.0-rc2/jackson-databind-2.19.0-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-xml/2.11.1/jackson-dataformat-xml-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/jackson-base/2.11.1/jackson-base-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/jackson-base/2.19.0-rc2/jackson-base-2.19.0-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/jackson-bom/2.11.1/jackson-bom-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/jackson-bom/2.19.0-rc2/jackson-bom-2.19.0-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/jackson-parent/2.11/jackson-parent-2.11.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/jackson-parent/2.19-rc2/jackson-parent-2.19-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/module/jackson-modules-base/2.11.1/jackson-modules-base-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/module/jackson-modules-base/2.19.0-rc2/jackson-modules-base-2.19.0-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/module/jackson-module-jaxb-annotations/2.11.1/jackson-module-jaxb-annotations-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/module/jackson-module-jaxb-annotations/2.19.0-rc2/jackson-module-jaxb-annotations-2.19.0-rc2.pom +https://repo.maven.apache.org/maven2/com/fasterxml/jackson/module/jackson-module-kotlin/2.11.1/jackson-module-kotlin-2.11.1.pom +https://repo.maven.apache.org/maven2/com/fasterxml/oss-parent/38/oss-parent-38.pom +https://repo.maven.apache.org/maven2/com/fasterxml/oss-parent/64/oss-parent-64.pom +https://repo.maven.apache.org/maven2/com/fasterxml/woodstox/woodstox-core/6.2.1/woodstox-core-6.2.1.pom +https://repo.maven.apache.org/maven2/com/github/bumptech/glide/annotations/4.12.0/annotations-4.12.0.pom +https://repo.maven.apache.org/maven2/com/github/bumptech/glide/compiler/4.12.0/compiler-4.12.0.pom +https://repo.maven.apache.org/maven2/com/github/bumptech/glide/disklrucache/4.12.0/disklrucache-4.12.0.pom +https://repo.maven.apache.org/maven2/com/github/bumptech/glide/gifdecoder/4.12.0/gifdecoder-4.12.0.pom +https://repo.maven.apache.org/maven2/com/github/bumptech/glide/glide/4.12.0/glide-4.12.0.pom +https://repo.maven.apache.org/maven2/com/github/bumptech/glide/okhttp3-integration/4.12.0/okhttp3-integration-4.12.0.pom +https://repo.maven.apache.org/maven2/com/github/clans/fab/1.6.4/fab-1.6.4.pom +https://repo.maven.apache.org/maven2/com/github/gundy/semver4j/0.16.4/semver4j-0.16.4.pom +https://repo.maven.apache.org/maven2/com/googlecode/concurrent-trees/concurrent-trees/2.6.1/concurrent-trees-2.6.1.pom +https://repo.maven.apache.org/maven2/com/googlecode/javaewah/JavaEWAH/1.1.12/JavaEWAH-1.1.12.pom +https://repo.maven.apache.org/maven2/com/googlecode/javaewah/JavaEWAH/1.1.13/JavaEWAH-1.1.13.pom +https://repo.maven.apache.org/maven2/com/googlecode/javaewah/JavaEWAH/1.2.3/JavaEWAH-1.2.3.pom +https://repo.maven.apache.org/maven2/com/googlecode/json-simple/json-simple/1.1/json-simple-1.1.pom +https://repo.maven.apache.org/maven2/com/googlecode/juniversalchardet/juniversalchardet/1.0.3/juniversalchardet-1.0.3.pom +https://repo.maven.apache.org/maven2/com/google/android/annotations/4.1.1.4/annotations-4.1.1.4.pom +https://repo.maven.apache.org/maven2/com/google/api/grpc/proto-google-common-protos/1.12.0/proto-google-common-protos-1.12.0.pom +https://repo.maven.apache.org/maven2/com/google/api/grpc/proto-google-common-protos/2.0.1/proto-google-common-protos-2.0.1.pom +https://repo.maven.apache.org/maven2/com/google/auto/auto-parent/3/auto-parent-3.pom +https://repo.maven.apache.org/maven2/com/google/auto/auto-parent/6/auto-parent-6.pom +https://repo.maven.apache.org/maven2/com/google/auto/auto-parent/7/auto-parent-7.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-annotations/1.6.2/auto-value-annotations-1.6.2.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-annotations/1.6.3/auto-value-annotations-1.6.3.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-annotations/1.7.4/auto-value-annotations-1.7.4.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-annotations/1.11.0/auto-value-annotations-1.11.0.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-parent/1.6.2/auto-value-parent-1.6.2.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-parent/1.6.3/auto-value-parent-1.6.3.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-parent/1.7.4/auto-value-parent-1.7.4.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-parent/1.11.0/auto-value-parent-1.11.0.pom +https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.pom +https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.pom +https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/2.0.1/jsr305-2.0.1.pom +https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.pom +https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.7/gson-parent-2.7.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.8.0/gson-parent-2.8.0.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.8.5/gson-parent-2.8.5.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.8.6/gson-parent-2.8.6.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.8.9/gson-parent-2.8.9.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.9.1/gson-parent-2.9.1.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.10/gson-parent-2.10.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.12.1/gson-parent-2.12.1.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.2.4/gson-2.2.4.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.3/gson-2.3.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.7/gson-2.7.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.8.0/gson-2.8.0.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.8.5/gson-2.8.5.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.8.6/gson-2.8.6.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.8.9/gson-2.8.9.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.9.1/gson-2.9.1.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.10/gson-2.10.pom +https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.12.1/gson-2.12.1.pom +https://repo.maven.apache.org/maven2/com/google/crypto/tink/tink/1.3.0-rc2/tink-1.3.0-rc2.pom +https://repo.maven.apache.org/maven2/com/google/crypto/tink/tink/1.7.0/tink-1.7.0.pom +https://repo.maven.apache.org/maven2/com/google/dagger/dagger/2.28.3/dagger-2.28.3.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.0.18/error_prone_annotations-2.0.18.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.3.1/error_prone_annotations-2.3.1.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.3.2/error_prone_annotations-2.3.2.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.3.4/error_prone_annotations-2.3.4.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.4.0/error_prone_annotations-2.4.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.5.1/error_prone_annotations-2.5.1.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.7.1/error_prone_annotations-2.7.1.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.9.0/error_prone_annotations-2.9.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.10.0/error_prone_annotations-2.10.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.11.0/error_prone_annotations-2.11.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.26.0/error_prone_annotations-2.26.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.36.0/error_prone_annotations-2.36.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.37.0/error_prone_annotations-2.37.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.0.18/error_prone_parent-2.0.18.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.2.0/error_prone_parent-2.2.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.3.1/error_prone_parent-2.3.1.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.3.2/error_prone_parent-2.3.2.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.3.4/error_prone_parent-2.3.4.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.4.0/error_prone_parent-2.4.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.5.1/error_prone_parent-2.5.1.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.7.1/error_prone_parent-2.7.1.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.9.0/error_prone_parent-2.9.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.10.0/error_prone_parent-2.10.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.11.0/error_prone_parent-2.11.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.26.0/error_prone_parent-2.26.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.36.0/error_prone_parent-2.36.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.37.0/error_prone_parent-2.37.0.pom +https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_type_annotations/2.37.0/error_prone_type_annotations-2.37.0.pom +https://repo.maven.apache.org/maven2/com/google/flatbuffers/flatbuffers-java/1.12.0/flatbuffers-java-1.12.0.pom +https://repo.maven.apache.org/maven2/com/google/google/1/google-1.pom +https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.pom +https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.3/failureaccess-1.0.3.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/17.0/guava-parent-17.0.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/22.0/guava-parent-22.0.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/23.0/guava-parent-23.0.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/26.0-android/guava-parent-26.0-android.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/27.0.1-jre/guava-parent-27.0.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/28.1-jre/guava-parent-28.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/29.0-jre/guava-parent-29.0-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/30.1-android/guava-parent-30.1-android.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/30.1-jre/guava-parent-30.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/31.0.1-android/guava-parent-31.0.1-android.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/31.0.1-jre/guava-parent-31.0.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/31.1-jre/guava-parent-31.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/33.4.0-android/guava-parent-33.4.0-android.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/33.4.7-jre/guava-parent-33.4.7-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava-testlib/33.4.7-jre/guava-testlib-33.4.7-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/17.0/guava-17.0.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/22.0/guava-22.0.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/23.0/guava-23.0.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/26.0-android/guava-26.0-android.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/27.0.1-jre/guava-27.0.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/28.1-jre/guava-28.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/29.0-jre/guava-29.0-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/30.1-android/guava-30.1-android.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/30.1-jre/guava-30.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/31.0.1-android/guava-31.0.1-android.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/guava/33.4.7-jre/guava-33.4.7-jre.pom +https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/1.0/listenablefuture-1.0.pom +https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.pom +https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.pom +https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.pom +https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/3.0.0/j2objc-annotations-3.0.0.pom +https://repo.maven.apache.org/maven2/com/google/jimfs/jimfs-parent/1.1/jimfs-parent-1.1.pom +https://repo.maven.apache.org/maven2/com/google/jimfs/jimfs/1.1/jimfs-1.1.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/3.7.1/protobuf-bom-3.7.1.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/3.10.0/protobuf-bom-3.10.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/3.13.0/protobuf-bom-3.13.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/3.17.2/protobuf-bom-3.17.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/3.19.2/protobuf-bom-3.19.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/3.19.3/protobuf-bom-3.19.3.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/4.30.2/protobuf-bom-4.30.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-javalite/3.17.2/protobuf-javalite-3.17.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-javalite/3.19.2/protobuf-javalite-3.19.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.4.0/protobuf-java-util-3.4.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.10.0/protobuf-java-util-3.10.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.17.2/protobuf-java-util-3.17.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.19.2/protobuf-java-util-3.19.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.19.3/protobuf-java-util-3.19.3.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.0.0/protobuf-java-3.0.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.4.0/protobuf-java-3.4.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.5.1/protobuf-java-3.5.1.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.7.1/protobuf-java-3.7.1.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.10.0/protobuf-java-3.10.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.13.0/protobuf-java-3.13.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.17.2/protobuf-java-3.17.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.19.2/protobuf-java-3.19.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.19.3/protobuf-java-3.19.3.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/4.30.2/protobuf-java-4.30.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-lite/3.0.1/protobuf-lite-3.0.1.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.0.0/protobuf-parent-3.0.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.4.0/protobuf-parent-3.4.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.5.1/protobuf-parent-3.5.1.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.7.1/protobuf-parent-3.7.1.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.10.0/protobuf-parent-3.10.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.13.0/protobuf-parent-3.13.0.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.17.2/protobuf-parent-3.17.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.19.2/protobuf-parent-3.19.2.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.19.3/protobuf-parent-3.19.3.pom +https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/4.30.2/protobuf-parent-4.30.2.pom +https://repo.maven.apache.org/maven2/com/google/truth/truth-parent/1.4.4/truth-parent-1.4.4.pom +https://repo.maven.apache.org/maven2/com/google/truth/truth/1.4.4/truth-1.4.4.pom +https://repo.maven.apache.org/maven2/com/google/zxing/core/3.4.1/core-3.4.1.pom +https://repo.maven.apache.org/maven2/com/google/zxing/zxing-parent/3.4.1/zxing-parent-3.4.1.pom +https://repo.maven.apache.org/maven2/com/ibm/icu/icu4j/53.1/icu4j-53.1.pom +https://repo.maven.apache.org/maven2/com/intellij/annotations/12.0/annotations-12.0.pom +https://repo.maven.apache.org/maven2/com/parse/bolts/bolts-tasks/1.4.0/bolts-tasks-1.4.0.pom +https://repo.maven.apache.org/maven2/com/squareup/javapoet/1.8.0/javapoet-1.8.0.pom +https://repo.maven.apache.org/maven2/com/squareup/javapoet/1.10.0/javapoet-1.10.0.pom +https://repo.maven.apache.org/maven2/com/squareup/javapoet/1.13.0/javapoet-1.13.0.pom +https://repo.maven.apache.org/maven2/com/squareup/javawriter/2.5.0/javawriter-2.5.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp-tls/4.11.0/okhttp-tls-4.11.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp-urlconnection/4.9.2/okhttp-urlconnection-4.9.2.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp/3.9.1/okhttp-3.9.1.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp/3.12.1/okhttp-3.12.1.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp/4.9.2/okhttp-4.9.2.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp/4.11.0/okhttp-4.11.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/parent/3.9.1/parent-3.9.1.pom +https://repo.maven.apache.org/maven2/com/squareup/okhttp3/parent/3.12.1/parent-3.12.1.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio-jvm/3.2.0/okio-jvm-3.2.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio-jvm/3.11.0/okio-jvm-3.11.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio-parent/1.17.4/okio-parent-1.17.4.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio/1.17.4/okio-1.17.4.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio/2.8.0/okio-2.8.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio/2.9.0/okio-2.9.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio/3.2.0/okio-3.2.0.pom +https://repo.maven.apache.org/maven2/com/squareup/okio/okio/3.11.0/okio-3.11.0.pom +https://repo.maven.apache.org/maven2/com/sun/activation/all/1.2.0/all-1.2.0.pom +https://repo.maven.apache.org/maven2/com/sun/activation/all/1.2.1/all-1.2.1.pom +https://repo.maven.apache.org/maven2/com/sun/activation/all/1.2.2/all-1.2.2.pom +https://repo.maven.apache.org/maven2/com/sun/activation/javax.activation/1.2.0/javax.activation-1.2.0.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons-runtime/2.21/istack-commons-runtime-2.21.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons-runtime/3.0.7/istack-commons-runtime-3.0.7.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons-runtime/3.0.8/istack-commons-runtime-3.0.8.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons-runtime/4.2.0/istack-commons-runtime-4.2.0.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons/2.21/istack-commons-2.21.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons/3.0.7/istack-commons-3.0.7.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons/3.0.8/istack-commons-3.0.8.pom +https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons/4.2.0/istack-commons-4.2.0.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/jaxb-bom-ext/2.2.11/jaxb-bom-ext-2.2.11.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/jaxb-bom-ext/2.3.1/jaxb-bom-ext-2.3.1.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/jaxb-bom-ext/2.3.2/jaxb-bom-ext-2.3.2.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/jaxb-bom-ext/4.0.5/jaxb-bom-ext-4.0.5.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-parent/2.2.11/jaxb-parent-2.2.11.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-parent/2.3.1/jaxb-parent-2.3.1.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-parent/2.3.2/jaxb-parent-2.3.2.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-parent/4.0.5/jaxb-parent-4.0.5.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-runtime-parent/2.2.11/jaxb-runtime-parent-2.2.11.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-runtime-parent/2.3.1/jaxb-runtime-parent-2.3.1.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-runtime-parent/2.3.2/jaxb-runtime-parent-2.3.2.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-txw-parent/2.2.11/jaxb-txw-parent-2.2.11.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-txw-parent/2.3.1/jaxb-txw-parent-2.3.1.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-txw-parent/2.3.2/jaxb-txw-parent-2.3.2.pom +https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-txw-parent/4.0.5/jaxb-txw-parent-4.0.5.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/FastInfoset/1.2.13/FastInfoset-1.2.13.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/FastInfoset/1.2.15/FastInfoset-1.2.15.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/FastInfoset/1.2.16/FastInfoset-1.2.16.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/FastInfoset/2.1.1/FastInfoset-2.1.1.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/fastinfoset-project/1.2.13/fastinfoset-project-1.2.13.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/fastinfoset-project/1.2.15/fastinfoset-project-1.2.15.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/fastinfoset-project/1.2.16/fastinfoset-project-1.2.16.pom +https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/fastinfoset-project/2.1.1/fastinfoset-project-2.1.1.pom +https://repo.maven.apache.org/maven2/com/tunnelvisionlabs/antlr4-annotations/4.5/antlr4-annotations-4.5.pom +https://repo.maven.apache.org/maven2/com/tunnelvisionlabs/antlr4-annotations/4.9.0/antlr4-annotations-4.9.0.pom +https://repo.maven.apache.org/maven2/com/tunnelvisionlabs/antlr4-master/4.5/antlr4-master-4.5.pom +https://repo.maven.apache.org/maven2/com/tunnelvisionlabs/antlr4-master/4.9.0/antlr4-master-4.9.0.pom +https://repo.maven.apache.org/maven2/com/tunnelvisionlabs/antlr4-runtime/4.5/antlr4-runtime-4.5.pom +https://repo.maven.apache.org/maven2/com/tunnelvisionlabs/antlr4-runtime/4.9.0/antlr4-runtime-4.9.0.pom +https://repo.maven.apache.org/maven2/com/tunnelvisionlabs/antlr4/4.5/antlr4-4.5.pom +https://repo.maven.apache.org/maven2/de/undercouch/gradle-download-task/4.1.1/gradle-download-task-4.1.1.pom +https://repo.maven.apache.org/maven2/de/undercouch/gradle-download-task/5.0.1/gradle-download-task-5.0.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-api/1.21.1/grpc-api-1.21.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-api/1.39.0/grpc-api-1.39.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-api/1.45.1/grpc-api-1.45.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-context/1.19.0/grpc-context-1.19.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-context/1.21.1/grpc-context-1.21.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-context/1.39.0/grpc-context-1.39.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-context/1.45.1/grpc-context-1.45.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-core/1.21.1/grpc-core-1.21.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-core/1.39.0/grpc-core-1.39.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-core/1.45.1/grpc-core-1.45.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-netty/1.21.1/grpc-netty-1.21.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-netty/1.39.0/grpc-netty-1.39.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-netty/1.45.1/grpc-netty-1.45.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf-lite/1.21.1/grpc-protobuf-lite-1.21.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf-lite/1.39.0/grpc-protobuf-lite-1.39.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf-lite/1.45.1/grpc-protobuf-lite-1.45.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf/1.21.1/grpc-protobuf-1.21.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf/1.39.0/grpc-protobuf-1.39.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf/1.45.1/grpc-protobuf-1.45.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-stub/1.21.1/grpc-stub-1.21.1.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-stub/1.39.0/grpc-stub-1.39.0.pom +https://repo.maven.apache.org/maven2/io/grpc/grpc-stub/1.45.1/grpc-stub-1.45.1.pom +https://repo.maven.apache.org/maven2/io/legere/pdfiumandroid/1.0.19/pdfiumandroid-1.0.19.pom +https://repo.maven.apache.org/maven2/io/netty/netty-buffer/4.1.34.Final/netty-buffer-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-buffer/4.1.52.Final/netty-buffer-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-buffer/4.1.72.Final/netty-buffer-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-buffer/5.0.0.Alpha2/netty-buffer-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-http2/4.1.34.Final/netty-codec-http2-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-http2/4.1.52.Final/netty-codec-http2-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-http2/4.1.72.Final/netty-codec-http2-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-http/4.1.34.Final/netty-codec-http-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-http/4.1.52.Final/netty-codec-http-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-http/4.1.72.Final/netty-codec-http-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-http/5.0.0.Alpha2/netty-codec-http-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-socks/4.1.34.Final/netty-codec-socks-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-socks/4.1.52.Final/netty-codec-socks-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-socks/4.1.72.Final/netty-codec-socks-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec-socks/5.0.0.Alpha2/netty-codec-socks-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec/4.1.34.Final/netty-codec-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec/4.1.52.Final/netty-codec-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec/4.1.72.Final/netty-codec-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-codec/5.0.0.Alpha2/netty-codec-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-common/4.1.34.Final/netty-common-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-common/4.1.52.Final/netty-common-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-common/4.1.72.Final/netty-common-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-common/5.0.0.Alpha2/netty-common-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-handler-proxy/4.1.34.Final/netty-handler-proxy-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-handler-proxy/4.1.52.Final/netty-handler-proxy-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-handler-proxy/4.1.72.Final/netty-handler-proxy-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-handler/4.1.34.Final/netty-handler-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-handler/4.1.52.Final/netty-handler-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-handler/4.1.72.Final/netty-handler-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-handler/5.0.0.Alpha2/netty-handler-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-parent/4.1.34.Final/netty-parent-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-parent/4.1.52.Final/netty-parent-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-parent/4.1.72.Final/netty-parent-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-parent/5.0.0.Alpha2/netty-parent-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-resolver/4.1.34.Final/netty-resolver-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-resolver/4.1.52.Final/netty-resolver-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-resolver/4.1.72.Final/netty-resolver-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-resolver/5.0.0.Alpha2/netty-resolver-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/netty/netty-tcnative-classes/2.0.46.Final/netty-tcnative-classes-2.0.46.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-tcnative-classes/2.0.70.Final/netty-tcnative-classes-2.0.70.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-tcnative-parent/2.0.46.Final/netty-tcnative-parent-2.0.46.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-tcnative-parent/2.0.70.Final/netty-tcnative-parent-2.0.70.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-transport/4.1.34.Final/netty-transport-4.1.34.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-transport/4.1.52.Final/netty-transport-4.1.52.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-transport/4.1.72.Final/netty-transport-4.1.72.Final.pom +https://repo.maven.apache.org/maven2/io/netty/netty-transport/5.0.0.Alpha2/netty-transport-5.0.0.Alpha2.pom +https://repo.maven.apache.org/maven2/io/opencensus/opencensus-api/0.21.0/opencensus-api-0.21.0.pom +https://repo.maven.apache.org/maven2/io/opencensus/opencensus-contrib-grpc-metrics/0.21.0/opencensus-contrib-grpc-metrics-0.21.0.pom +https://repo.maven.apache.org/maven2/io/perfmark/perfmark-api/0.23.0/perfmark-api-0.23.0.pom +https://repo.maven.apache.org/maven2/it/unimi/dsi/fastutil/7.2.0/fastutil-7.2.0.pom +https://repo.maven.apache.org/maven2/it/unimi/dsi/fastutil/8.4.0/fastutil-8.4.0.pom +https://repo.maven.apache.org/maven2/jakarta/activation/jakarta.activation-api/1.2.1/jakarta.activation-api-1.2.1.pom +https://repo.maven.apache.org/maven2/jakarta/activation/jakarta.activation-api/1.2.2/jakarta.activation-api-1.2.2.pom +https://repo.maven.apache.org/maven2/jakarta/activation/jakarta.activation-api/2.1.3/jakarta.activation-api-2.1.3.pom +https://repo.maven.apache.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api-parent/2.3.2/jakarta.xml.bind-api-parent-2.3.2.pom +https://repo.maven.apache.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api-parent/4.0.2/jakarta.xml.bind-api-parent-4.0.2.pom +https://repo.maven.apache.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api/2.3.2/jakarta.xml.bind-api-2.3.2.pom +https://repo.maven.apache.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api/4.0.2/jakarta.xml.bind-api-4.0.2.pom +https://repo.maven.apache.org/maven2/javax/activation/activation/1.1/activation-1.1.pom +https://repo.maven.apache.org/maven2/javax/activation/javax.activation-api/1.2.0/javax.activation-api-1.2.0.pom +https://repo.maven.apache.org/maven2/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.pom +https://repo.maven.apache.org/maven2/javax/inject/javax.inject/1/javax.inject-1.pom +https://repo.maven.apache.org/maven2/javax/xml/bind/jaxb-api-parent/2.3.1/jaxb-api-parent-2.3.1.pom +https://repo.maven.apache.org/maven2/javax/xml/bind/jaxb-api-parent/2.4.0-b180830.0359/jaxb-api-parent-2.4.0-b180830.0359.pom +https://repo.maven.apache.org/maven2/javax/xml/bind/jaxb-api/2.2.12-b140109.1041/jaxb-api-2.2.12-b140109.1041.pom +https://repo.maven.apache.org/maven2/javax/xml/bind/jaxb-api/2.2.12/jaxb-api-2.2.12.pom +https://repo.maven.apache.org/maven2/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.pom +https://repo.maven.apache.org/maven2/javax/xml/bind/jaxb-api/2.4.0-b180830.0359/jaxb-api-2.4.0-b180830.0359.pom +https://repo.maven.apache.org/maven2/javax/xml/stream/stax-api/1.0-2/stax-api-1.0-2.pom +https://repo.maven.apache.org/maven2/junit/junit/4.12/junit-4.12.pom +https://repo.maven.apache.org/maven2/junit/junit/4.13.2/junit-4.13.2.pom +https://repo.maven.apache.org/maven2/me/zhanghai/android/materialprogressbar/library/1.4.2/library-1.4.2.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy-agent/1.11.13/byte-buddy-agent-1.11.13.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy-agent/1.11.19/byte-buddy-agent-1.11.19.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy-agent/1.15.11/byte-buddy-agent-1.15.11.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy-parent/1.11.13/byte-buddy-parent-1.11.13.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy-parent/1.11.19/byte-buddy-parent-1.11.19.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy-parent/1.15.10/byte-buddy-parent-1.15.10.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy-parent/1.15.11/byte-buddy-parent-1.15.11.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy/1.11.13/byte-buddy-1.11.13.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy/1.11.19/byte-buddy-1.11.19.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy/1.15.10/byte-buddy-1.15.10.pom +https://repo.maven.apache.org/maven2/net/bytebuddy/byte-buddy/1.15.11/byte-buddy-1.15.11.pom +https://repo.maven.apache.org/maven2/net/java/dev/jna/jna-platform/5.6.0/jna-platform-5.6.0.pom +https://repo.maven.apache.org/maven2/net/java/dev/jna/jna/5.6.0/jna-5.6.0.pom +https://repo.maven.apache.org/maven2/net/java/jvnet-parent/1/jvnet-parent-1.pom +https://repo.maven.apache.org/maven2/net/java/jvnet-parent/3/jvnet-parent-3.pom +https://repo.maven.apache.org/maven2/net/java/jvnet-parent/4/jvnet-parent-4.pom +https://repo.maven.apache.org/maven2/net/java/jvnet-parent/5/jvnet-parent-5.pom +https://repo.maven.apache.org/maven2/net/sf/jopt-simple/jopt-simple/4.9/jopt-simple-4.9.pom +https://repo.maven.apache.org/maven2/net/sf/kxml/kxml2/2.3.0/kxml2-2.3.0.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-base/5.2.1/proguard-base-5.2.1.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-base/5.3.3/proguard-base-5.3.3.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-base/6.0.3/proguard-base-6.0.3.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-base/6.3.0beta1/proguard-base-6.3.0beta1.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-gradle/5.2.1/proguard-gradle-5.2.1.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-gradle/5.3.3/proguard-gradle-5.3.3.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-gradle/6.0.3/proguard-gradle-6.0.3.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-parent/5.2.1/proguard-parent-5.2.1.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-parent/5.3.3/proguard-parent-5.3.3.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-parent/6.0.3/proguard-parent-6.0.3.pom +https://repo.maven.apache.org/maven2/net/sf/proguard/proguard-parent/6.3.0beta1/proguard-parent-6.3.0beta1.pom +https://repo.maven.apache.org/maven2/org/abego/treelayout/org.abego.treelayout.core/1.0.1/org.abego.treelayout.core-1.0.1.pom +https://repo.maven.apache.org/maven2/org/antlr/ST4/4.0.8/ST4-4.0.8.pom +https://repo.maven.apache.org/maven2/org/antlr/ST4/4.3.4/ST4-4.3.4.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr4-master/4.5.3/antlr4-master-4.5.3.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr4-master/4.13.2/antlr4-master-4.13.2.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr4-runtime/4.13.2/antlr4-runtime-4.13.2.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr4/4.5.3/antlr4-4.5.3.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr-master/3.3/antlr-master-3.3.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr-master/3.5.2/antlr-master-3.5.2.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr-master/3.5.3/antlr-master-3.5.3.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr-runtime/3.3/antlr-runtime-3.3.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr-runtime/3.5.2/antlr-runtime-3.5.2.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr-runtime/3.5.3/antlr-runtime-3.5.3.pom +https://repo.maven.apache.org/maven2/org/antlr/antlr/3.5.2/antlr-3.5.2.pom +https://repo.maven.apache.org/maven2/org/antlr/stringtemplate/3.2.1/stringtemplate-3.2.1.pom +https://repo.maven.apache.org/maven2/org/antlr/stringtemplate/4.0.2/stringtemplate-4.0.2.pom +https://repo.maven.apache.org/maven2/org/apache/apache/4/apache-4.pom +https://repo.maven.apache.org/maven2/org/apache/apache/9/apache-9.pom +https://repo.maven.apache.org/maven2/org/apache/apache/13/apache-13.pom +https://repo.maven.apache.org/maven2/org/apache/apache/15/apache-15.pom +https://repo.maven.apache.org/maven2/org/apache/apache/16/apache-16.pom +https://repo.maven.apache.org/maven2/org/apache/apache/18/apache-18.pom +https://repo.maven.apache.org/maven2/org/apache/apache/21/apache-21.pom +https://repo.maven.apache.org/maven2/org/apache/apache/23/apache-23.pom +https://repo.maven.apache.org/maven2/org/apache/apache/33/apache-33.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.12/commons-compress-1.12.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.20/commons-compress-1.20.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.21/commons-compress-1.21.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.9/commons-lang3-3.9.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/5/commons-parent-5.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/11/commons-parent-11.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/22/commons-parent-22.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/25/commons-parent-25.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/32/commons-parent-32.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/33/commons-parent-33.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/34/commons-parent-34.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/35/commons-parent-35.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/39/commons-parent-39.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/42/commons-parent-42.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/48/commons-parent-48.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/52/commons-parent-52.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/78/commons-parent-78.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/79/commons-parent-79.pom +https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/81/commons-parent-81.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.1.1/httpclient-4.1.1.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.2.6/httpclient-4.2.6.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.2/httpclient-4.5.2.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.6/httpclient-4.5.6.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.9/httpclient-4.5.9.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.1.1/httpcomponents-client-4.1.1.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.1/httpcomponents-client-4.1.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.2.6/httpcomponents-client-4.2.6.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.5.2/httpcomponents-client-4.5.2.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.5.6/httpcomponents-client-4.5.6.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.5.9/httpcomponents-client-4.5.9.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.5.13/httpcomponents-client-4.5.13.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.5.14/httpcomponents-client-4.5.14.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.1/httpcomponents-core-4.1.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.2.5/httpcomponents-core-4.2.5.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.4.5/httpcomponents-core-4.4.5.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.4.10/httpcomponents-core-4.4.10.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.4.11/httpcomponents-core-4.4.11.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.4.13/httpcomponents-core-4.4.13.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.4.15/httpcomponents-core-4.4.15.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.4.16/httpcomponents-core-4.4.16.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-parent/10/httpcomponents-parent-10.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-parent/11/httpcomponents-parent-11.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.1/httpcore-4.1.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.2.5/httpcore-4.2.5.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.5/httpcore-4.4.5.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.10/httpcore-4.4.10.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.11/httpcore-4.4.11.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.13/httpcore-4.4.13.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.15/httpcore-4.4.15.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpmime/4.1/httpmime-4.1.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpmime/4.5.2/httpmime-4.5.2.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpmime/4.5.6/httpmime-4.5.6.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/project/4.1.1/project-4.1.1.pom +https://repo.maven.apache.org/maven2/org/apache/httpcomponents/project/7/project-7.pom +https://repo.maven.apache.org/maven2/org/bitbucket/b_c/jose4j/0.7.0/jose4j-0.7.0.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.48/bcpkix-jdk15on-1.48.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.56/bcpkix-jdk15on-1.56.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.67/bcpkix-jdk15on-1.67.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk15on/1.48/bcprov-jdk15on-1.48.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk15on/1.56/bcprov-jdk15on-1.56.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk15on/1.60/bcprov-jdk15on-1.60.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk15on/1.67/bcprov-jdk15on-1.67.pom +https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk15on/1.68/bcprov-jdk15on-1.68.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-compat-qual/2.5.6/checker-compat-qual-2.5.6.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/2.5.8/checker-qual-2.5.8.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/2.8.1/checker-qual-2.8.1.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/2.11.1/checker-qual-2.11.1.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/3.5.0/checker-qual-3.5.0.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/3.12.0/checker-qual-3.12.0.pom +https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/3.49.2/checker-qual-3.49.2.pom +https://repo.maven.apache.org/maven2/org/codehaus/codehaus-parent/4/codehaus-parent-4.pom +https://repo.maven.apache.org/maven2/org/codehaus/groovy/groovy-xml/3.0.9/groovy-xml-3.0.9.pom +https://repo.maven.apache.org/maven2/org/codehaus/groovy/groovy-xml/3.0.10/groovy-xml-3.0.10.pom +https://repo.maven.apache.org/maven2/org/codehaus/groovy/groovy/3.0.9/groovy-3.0.9.pom +https://repo.maven.apache.org/maven2/org/codehaus/groovy/groovy/3.0.10/groovy-3.0.10.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-annotations/1.18/animal-sniffer-annotations-1.18.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-annotations/1.19/animal-sniffer-annotations-1.19.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-annotations/1.24/animal-sniffer-annotations-1.24.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-parent/1.14/animal-sniffer-parent-1.14.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-parent/1.17/animal-sniffer-parent-1.17.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-parent/1.18/animal-sniffer-parent-1.18.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-parent/1.19/animal-sniffer-parent-1.19.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-parent/1.24/animal-sniffer-parent-1.24.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/mojo-parent/34/mojo-parent-34.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/mojo-parent/40/mojo-parent-40.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/mojo-parent/50/mojo-parent-50.pom +https://repo.maven.apache.org/maven2/org/codehaus/mojo/mojo-parent/84/mojo-parent-84.pom +https://repo.maven.apache.org/maven2/org/codehaus/woodstox/stax2-api/4.2.1/stax2-api-4.2.1.pom +https://repo.maven.apache.org/maven2/org/easymock/easymockclassextension/3.2/easymockclassextension-3.2.pom +https://repo.maven.apache.org/maven2/org/easymock/easymock-parent/3.2/easymock-parent-3.2.pom +https://repo.maven.apache.org/maven2/org/easymock/easymock-parent/5.5.0/easymock-parent-5.5.0.pom +https://repo.maven.apache.org/maven2/org/easymock/easymock/5.5.0/easymock-5.5.0.pom +https://repo.maven.apache.org/maven2/org/eclipse/angus/angus-activation-project/2.0.2/angus-activation-project-2.0.2.pom +https://repo.maven.apache.org/maven2/org/eclipse/angus/angus-activation/2.0.2/angus-activation-2.0.2.pom +https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.2/project-1.0.2.pom +https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.5/project-1.0.5.pom +https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.6/project-1.0.6.pom +https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.7/project-1.0.7.pom +https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.9/project-1.0.9.pom +https://repo.maven.apache.org/maven2/org/eclipse/jdt/core/compiler/ecj/4.4.2/ecj-4.4.2.pom +https://repo.maven.apache.org/maven2/org/eclipse/jdt/core/compiler/ecj/4.6.1/ecj-4.6.1.pom +https://repo.maven.apache.org/maven2/org/eclipse/jgit/org.eclipse.jgit-parent/5.13.0.202109080827-r/org.eclipse.jgit-parent-5.13.0.202109080827-r.pom +https://repo.maven.apache.org/maven2/org/eclipse/jgit/org.eclipse.jgit-parent/5.13.1.202206130422-r/org.eclipse.jgit-parent-5.13.1.202206130422-r.pom +https://repo.maven.apache.org/maven2/org/eclipse/jgit/org.eclipse.jgit/5.13.0.202109080827-r/org.eclipse.jgit-5.13.0.202109080827-r.pom +https://repo.maven.apache.org/maven2/org/eclipse/jgit/org.eclipse.jgit/5.13.1.202206130422-r/org.eclipse.jgit-5.13.1.202206130422-r.pom +https://repo.maven.apache.org/maven2/org/fusesource/fusesource-pom/1.11/fusesource-pom-1.11.pom +https://repo.maven.apache.org/maven2/org/fusesource/jansi/jansi-project/1.18/jansi-project-1.18.pom +https://repo.maven.apache.org/maven2/org/fusesource/jansi/jansi/1.18/jansi-1.18.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-bom/2.2.11/jaxb-bom-2.2.11.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-bom/2.3.1/jaxb-bom-2.3.1.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-bom/2.3.2/jaxb-bom-2.3.2.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-bom/4.0.5/jaxb-bom-4.0.5.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-core/2.2.11/jaxb-core-2.2.11.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-core/4.0.5/jaxb-core-4.0.5.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-runtime/2.2.11/jaxb-runtime-2.2.11.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-runtime/2.3.1/jaxb-runtime-2.3.1.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-runtime/2.3.2/jaxb-runtime-2.3.2.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/txw2/2.2.11/txw2-2.2.11.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/txw2/2.3.1/txw2-2.3.1.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/txw2/2.3.2/txw2-2.3.2.pom +https://repo.maven.apache.org/maven2/org/glassfish/jaxb/txw2/4.0.5/txw2-4.0.5.pom +https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.pom +https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest-core/3.0/hamcrest-core-3.0.pom +https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest-parent/1.3/hamcrest-parent-1.3.pom +https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest/2.2/hamcrest-2.2.pom +https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest/3.0/hamcrest-3.0.pom +https://repo.maven.apache.org/maven2/org/jacoco/org.jacoco.build/0.7.4.201502262128/org.jacoco.build-0.7.4.201502262128.pom +https://repo.maven.apache.org/maven2/org/jacoco/org.jacoco.build/0.8.13/org.jacoco.build-0.8.13.pom +https://repo.maven.apache.org/maven2/org/jacoco/org.jacoco.core/0.7.4.201502262128/org.jacoco.core-0.7.4.201502262128.pom +https://repo.maven.apache.org/maven2/org/jacoco/org.jacoco.core/0.8.13/org.jacoco.core-0.8.13.pom +https://repo.maven.apache.org/maven2/org/jacoco/org.jacoco.report/0.7.4.201502262128/org.jacoco.report-0.7.4.201502262128.pom +https://repo.maven.apache.org/maven2/org/jdom/jdom2/2.0.6/jdom2-2.0.6.pom +https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/annotations/23.0.0/annotations-23.0.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/dokka/dokka-core/1.4.32/dokka-core-1.4.32.pom +https://repo.maven.apache.org/maven2/org/jetbrains/intellij/deps/trove4j/1.0.20181211/trove4j-1.0.20181211.pom +https://repo.maven.apache.org/maven2/org/jetbrains/intellij/deps/trove4j/1.0.20200330/trove4j-1.0.20200330.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/atomicfu-jvm/0.16.3/atomicfu-jvm-0.16.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/atomicfu-jvm/0.17.3/atomicfu-jvm-0.17.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/atomicfu/0.16.3/atomicfu-0.16.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/atomicfu/0.17.3/atomicfu-0.17.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.5.2/kotlinx-coroutines-android-1.5.2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.6.1/kotlinx-coroutines-android-1.6.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.6.4/kotlinx-coroutines-android-1.6.4.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.7.3/kotlinx-coroutines-android-1.7.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.6.1/kotlinx-coroutines-bom-1.6.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.6.3/kotlinx-coroutines-bom-1.6.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.6.4/kotlinx-coroutines-bom-1.6.4.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.7.3/kotlinx-coroutines-bom-1.7.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.1/kotlinx-coroutines-core-jvm-1.4.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.5.0/kotlinx-coroutines-core-jvm-1.5.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.5.2/kotlinx-coroutines-core-jvm-1.5.2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.1/kotlinx-coroutines-core-jvm-1.6.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.3/kotlinx-coroutines-core-jvm-1.6.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.7.3/kotlinx-coroutines-core-jvm-1.7.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.5.2/kotlinx-coroutines-core-1.5.2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.1/kotlinx-coroutines-core-1.6.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.3/kotlinx-coroutines-core-1.6.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/kotlinx-coroutines-core-1.6.4.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.7.3/kotlinx-coroutines-core-1.7.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-play-services/1.6.4/kotlinx-coroutines-play-services-1.6.4.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-play-services/1.7.3/kotlinx-coroutines-play-services-1.7.3.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/jvm/org.jetbrains.kotlin.jvm.gradle.plugin/1.8.0/org.jetbrains.kotlin.jvm.gradle.plugin-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-android-extensions/1.6.20/kotlin-android-extensions-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-android-extensions/1.8.0/kotlin-android-extensions-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-android-extensions/1.9.0/kotlin-android-extensions-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-annotations-jvm/1.3.72/kotlin-annotations-jvm-1.3.72.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.6.20/kotlin-annotation-processing-gradle-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.8.0/kotlin-annotation-processing-gradle-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-common/1.6.20/kotlin-build-common-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-common/1.8.0/kotlin-build-common-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-common/1.9.0/kotlin-build-common-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-tools-api/1.9.0/kotlin-build-tools-api-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-tools-impl/1.9.0/kotlin-build-tools-impl-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-embeddable/1.6.20/kotlin-compiler-embeddable-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-embeddable/1.8.0/kotlin-compiler-embeddable-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-embeddable/1.9.0/kotlin-compiler-embeddable-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-runner/1.6.20/kotlin-compiler-runner-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-runner/1.8.0/kotlin-compiler-runner-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-runner/1.9.0/kotlin-compiler-runner-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-client/1.6.20/kotlin-daemon-client-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-client/1.8.0/kotlin-daemon-client-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-client/1.9.0/kotlin-daemon-client-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-embeddable/1.6.20/kotlin-daemon-embeddable-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-embeddable/1.8.0/kotlin-daemon-embeddable-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-embeddable/1.9.0/kotlin-daemon-embeddable-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugins-bom/1.9.0/kotlin-gradle-plugins-bom-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-annotations/1.9.0/kotlin-gradle-plugin-annotations-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-api/1.6.20/kotlin-gradle-plugin-api-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-api/1.8.0/kotlin-gradle-plugin-api-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-api/1.9.0/kotlin-gradle-plugin-api-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/1.8.0/kotlin-gradle-plugin-idea-proto-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/1.9.0/kotlin-gradle-plugin-idea-proto-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/1.8.0/kotlin-gradle-plugin-idea-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/1.9.0/kotlin-gradle-plugin-idea-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-model/1.6.20/kotlin-gradle-plugin-model-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-model/1.8.0/kotlin-gradle-plugin-model-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-model/1.9.0/kotlin-gradle-plugin-model-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin/1.6.20/kotlin-gradle-plugin-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin/1.8.0/kotlin-gradle-plugin-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin/1.9.0/kotlin-gradle-plugin-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-klib-commonizer-api/1.6.20/kotlin-klib-commonizer-api-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-klib-commonizer-api/1.8.0/kotlin-klib-commonizer-api-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-klib-commonizer-api/1.9.0/kotlin-klib-commonizer-api-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-klib-commonizer-embeddable/1.8.0/kotlin-klib-commonizer-embeddable-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-klib-commonizer-embeddable/1.9.0/kotlin-klib-commonizer-embeddable-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-native-utils/1.6.20/kotlin-native-utils-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-native-utils/1.8.0/kotlin-native-utils-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-native-utils/1.9.0/kotlin-native-utils-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-project-model/1.6.20/kotlin-project-model-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-project-model/1.8.0/kotlin-project-model-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-project-model/1.9.0/kotlin-project-model-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.1.3-2/kotlin-reflect-1.1.3-2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.2.0/kotlin-reflect-1.2.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.3.50/kotlin-reflect-1.3.50.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.3.72/kotlin-reflect-1.3.72.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.4.32/kotlin-reflect-1.4.32.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.5.31/kotlin-reflect-1.5.31.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.6.10/kotlin-reflect-1.6.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.6.20/kotlin-reflect-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.7.10/kotlin-reflect-1.7.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.8.20-RC2/kotlin-reflect-1.8.20-RC2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/1.9.0/kotlin-reflect-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/2.1.20/kotlin-reflect-2.1.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-common/1.6.20/kotlin-scripting-common-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-common/1.8.0/kotlin-scripting-common-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-common/1.9.0/kotlin-scripting-common-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-compiler-embeddable/1.6.20/kotlin-scripting-compiler-embeddable-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-compiler-embeddable/1.8.0/kotlin-scripting-compiler-embeddable-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-compiler-embeddable/1.9.0/kotlin-scripting-compiler-embeddable-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-compiler-impl-embeddable/1.6.20/kotlin-scripting-compiler-impl-embeddable-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-compiler-impl-embeddable/1.8.0/kotlin-scripting-compiler-impl-embeddable-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-compiler-impl-embeddable/1.9.0/kotlin-scripting-compiler-impl-embeddable-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-jvm/1.6.20/kotlin-scripting-jvm-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-jvm/1.8.0/kotlin-scripting-jvm-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-scripting-jvm/1.9.0/kotlin-scripting-jvm-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-script-runtime/1.6.20/kotlin-script-runtime-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-script-runtime/1.8.0/kotlin-script-runtime-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-script-runtime/1.9.0/kotlin-script-runtime-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.2.71/kotlin-stdlib-common-1.2.71.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.0/kotlin-stdlib-common-1.3.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.31/kotlin-stdlib-common-1.3.31.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.41/kotlin-stdlib-common-1.3.41.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.50/kotlin-stdlib-common-1.3.50.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.60/kotlin-stdlib-common-1.3.60.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.71/kotlin-stdlib-common-1.3.71.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.72/kotlin-stdlib-common-1.3.72.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.4.0/kotlin-stdlib-common-1.4.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.4.10/kotlin-stdlib-common-1.4.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.4.32/kotlin-stdlib-common-1.4.32.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.5.0/kotlin-stdlib-common-1.5.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.5.10/kotlin-stdlib-common-1.5.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.5.30/kotlin-stdlib-common-1.5.30.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.5.31/kotlin-stdlib-common-1.5.31.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.6.0/kotlin-stdlib-common-1.6.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.6.10/kotlin-stdlib-common-1.6.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.6.20/kotlin-stdlib-common-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.6.21/kotlin-stdlib-common-1.6.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.7.10/kotlin-stdlib-common-1.7.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.7.22/kotlin-stdlib-common-1.7.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.20-RC2/kotlin-stdlib-common-1.8.20-RC2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.20/kotlin-stdlib-common-1.8.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.21/kotlin-stdlib-common-1.8.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.22/kotlin-stdlib-common-1.8.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.9.0/kotlin-stdlib-common-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/1.9.22/kotlin-stdlib-common-1.9.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.2.71/kotlin-stdlib-jdk7-1.2.71.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.3.50/kotlin-stdlib-jdk7-1.3.50.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.3.72/kotlin-stdlib-jdk7-1.3.72.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.4.10/kotlin-stdlib-jdk7-1.4.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.4.32/kotlin-stdlib-jdk7-1.4.32.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.0/kotlin-stdlib-jdk7-1.5.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.30/kotlin-stdlib-jdk7-1.5.30.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.31/kotlin-stdlib-jdk7-1.5.31.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.0/kotlin-stdlib-jdk7-1.6.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.10/kotlin-stdlib-jdk7-1.6.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.20/kotlin-stdlib-jdk7-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.21/kotlin-stdlib-jdk7-1.6.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.10/kotlin-stdlib-jdk7-1.7.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.22/kotlin-stdlib-jdk7-1.7.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.20-RC2/kotlin-stdlib-jdk7-1.8.20-RC2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.20/kotlin-stdlib-jdk7-1.8.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.21/kotlin-stdlib-jdk7-1.8.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.22/kotlin-stdlib-jdk7-1.8.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.9.0/kotlin-stdlib-jdk7-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.2.71/kotlin-stdlib-jdk8-1.2.71.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.3.50/kotlin-stdlib-jdk8-1.3.50.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.3.72/kotlin-stdlib-jdk8-1.3.72.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.4.10/kotlin-stdlib-jdk8-1.4.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.4.32/kotlin-stdlib-jdk8-1.4.32.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.0/kotlin-stdlib-jdk8-1.5.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.30/kotlin-stdlib-jdk8-1.5.30.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.31/kotlin-stdlib-jdk8-1.5.31.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.0/kotlin-stdlib-jdk8-1.6.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.10/kotlin-stdlib-jdk8-1.6.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.20/kotlin-stdlib-jdk8-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.21/kotlin-stdlib-jdk8-1.6.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.10/kotlin-stdlib-jdk8-1.7.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.22/kotlin-stdlib-jdk8-1.7.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.20-RC2/kotlin-stdlib-jdk8-1.8.20-RC2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.20/kotlin-stdlib-jdk8-1.8.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.21/kotlin-stdlib-jdk8-1.8.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.22/kotlin-stdlib-jdk8-1.8.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.9.0/kotlin-stdlib-jdk8-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.1.3-2/kotlin-stdlib-1.1.3-2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.2.0/kotlin-stdlib-1.2.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.2.20/kotlin-stdlib-1.2.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.2.71/kotlin-stdlib-1.2.71.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.3.0/kotlin-stdlib-1.3.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.3.31/kotlin-stdlib-1.3.31.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.3.41/kotlin-stdlib-1.3.41.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.3.50/kotlin-stdlib-1.3.50.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.3.60/kotlin-stdlib-1.3.60.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.3.71/kotlin-stdlib-1.3.71.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.3.72/kotlin-stdlib-1.3.72.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.4.0/kotlin-stdlib-1.4.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.4.10/kotlin-stdlib-1.4.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.4.32/kotlin-stdlib-1.4.32.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.5.0/kotlin-stdlib-1.5.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.5.10/kotlin-stdlib-1.5.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.5.30/kotlin-stdlib-1.5.30.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.5.31/kotlin-stdlib-1.5.31.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.6.0/kotlin-stdlib-1.6.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.6.10/kotlin-stdlib-1.6.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.6.20/kotlin-stdlib-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.6.21/kotlin-stdlib-1.6.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.7.10/kotlin-stdlib-1.7.10.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.7.22/kotlin-stdlib-1.7.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.8.20-RC2/kotlin-stdlib-1.8.20-RC2.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.8.20/kotlin-stdlib-1.8.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.8.21/kotlin-stdlib-1.8.21.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.8.22/kotlin-stdlib-1.8.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.9.0/kotlin-stdlib-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.9.22/kotlin-stdlib-1.9.22.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.1.20/kotlin-stdlib-2.1.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-tooling-core/1.8.0/kotlin-tooling-core-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-tooling-core/1.9.0/kotlin-tooling-core-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-tooling-metadata/1.6.20/kotlin-tooling-metadata-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-io/1.6.20/kotlin-util-io-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-io/1.8.0/kotlin-util-io-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-io/1.9.0/kotlin-util-io-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-klib/1.6.20/kotlin-util-klib-1.6.20.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-klib/1.8.0/kotlin-util-klib-1.8.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-klib/1.9.0/kotlin-util-klib-1.9.0.pom +https://repo.maven.apache.org/maven2/org/jetbrains/markdown-jvm/0.2.1/markdown-jvm-0.2.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/markdown/0.2.1/markdown-0.2.1.pom +https://repo.maven.apache.org/maven2/org/jetbrains/trove4j/trove4j/20160824/trove4j-20160824.pom +https://repo.maven.apache.org/maven2/org/json/json/20180813/json-20180813.pom +https://repo.maven.apache.org/maven2/org/json/json/20250107/json-20250107.pom +https://repo.maven.apache.org/maven2/org/jsoup/jsoup/1.13.1/jsoup-1.13.1.pom +https://repo.maven.apache.org/maven2/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.pom +https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.13.0-M2/junit-bom-5.13.0-M2.pom +https://repo.maven.apache.org/maven2/org/jvnet/staxex/stax-ex/1.7.7/stax-ex-1.7.7.pom +https://repo.maven.apache.org/maven2/org/jvnet/staxex/stax-ex/1.8.1/stax-ex-1.8.1.pom +https://repo.maven.apache.org/maven2/org/jvnet/staxex/stax-ex/1.8/stax-ex-1.8.pom +https://repo.maven.apache.org/maven2/org/jvnet/staxex/stax-ex/2.1.0/stax-ex-2.1.0.pom +https://repo.maven.apache.org/maven2/org/mockito/mockito-core/3.12.4/mockito-core-3.12.4.pom +https://repo.maven.apache.org/maven2/org/mockito/mockito-core/4.0.0/mockito-core-4.0.0.pom +https://repo.maven.apache.org/maven2/org/mockito/mockito-core/5.17.0/mockito-core-5.17.0.pom +https://repo.maven.apache.org/maven2/org/mockito/mockito-inline/4.0.0/mockito-inline-4.0.0.pom +https://repo.maven.apache.org/maven2/org/objenesis/objenesis-parent/3.2/objenesis-parent-3.2.pom +https://repo.maven.apache.org/maven2/org/objenesis/objenesis-parent/3.3/objenesis-parent-3.3.pom +https://repo.maven.apache.org/maven2/org/objenesis/objenesis-parent/3.4/objenesis-parent-3.4.pom +https://repo.maven.apache.org/maven2/org/objenesis/objenesis/3.2/objenesis-3.2.pom +https://repo.maven.apache.org/maven2/org/objenesis/objenesis/3.3/objenesis-3.3.pom +https://repo.maven.apache.org/maven2/org/objenesis/objenesis/3.4/objenesis-3.4.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/5.0.3/asm-analysis-5.0.3.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/5.1/asm-analysis-5.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/6.0/asm-analysis-6.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/7.0/asm-analysis-7.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/8.0.1/asm-analysis-8.0.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/9.1/asm-analysis-9.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/9.2/asm-analysis-9.2.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-bom/9.8/asm-bom-9.8.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/5.1/asm-commons-5.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/6.0/asm-commons-6.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/7.0/asm-commons-7.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/8.0.1/asm-commons-8.0.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/9.1/asm-commons-9.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/9.2/asm-commons-9.2.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/9.8/asm-commons-9.8.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-debug-all/5.0.1/asm-debug-all-5.0.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-debug-all/6.0_BETA/asm-debug-all-6.0_BETA.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-parent/5.0.1/asm-parent-5.0.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-parent/5.0.3/asm-parent-5.0.3.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-parent/5.1/asm-parent-5.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-parent/6.0/asm-parent-6.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-parent/6.0_BETA/asm-parent-6.0_BETA.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/5.0.3/asm-tree-5.0.3.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/5.1/asm-tree-5.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/6.0/asm-tree-6.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/7.0/asm-tree-7.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/8.0.1/asm-tree-8.0.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/9.1/asm-tree-9.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/9.2/asm-tree-9.2.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/9.8/asm-tree-9.8.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/5.1/asm-util-5.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/6.0/asm-util-6.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/7.0/asm-util-7.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/8.0.1/asm-util-8.0.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/9.1/asm-util-9.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/9.2/asm-util-9.2.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/5.0.3/asm-5.0.3.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/5.1/asm-5.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/6.0/asm-6.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/7.0/asm-7.0.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/8.0.1/asm-8.0.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/9.1/asm-9.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/9.2/asm-9.2.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/9.7.1/asm-9.7.1.pom +https://repo.maven.apache.org/maven2/org/ow2/asm/asm/9.8/asm-9.8.pom +https://repo.maven.apache.org/maven2/org/ow2/ow2/1.3/ow2-1.3.pom +https://repo.maven.apache.org/maven2/org/ow2/ow2/1.5.1/ow2-1.5.1.pom +https://repo.maven.apache.org/maven2/org/ow2/ow2/1.5/ow2-1.5.pom +https://repo.maven.apache.org/maven2/org/robolectric/annotations/4.7.3/annotations-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/junit/4.7.3/junit-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/nativeruntime/4.7.3/nativeruntime-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/pluginapi/4.7.3/pluginapi-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/plugins-maven-dependency-resolver/4.7.3/plugins-maven-dependency-resolver-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/resources/4.7.3/resources-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/robolectric/4.7.3/robolectric-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/sandbox/4.7.3/sandbox-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/shadowapi/4.7.3/shadowapi-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/shadows-framework/4.7.3/shadows-framework-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/utils-reflector/4.7.3/utils-reflector-4.7.3.pom +https://repo.maven.apache.org/maven2/org/robolectric/utils/4.7.3/utils-4.7.3.pom +https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/1.7.21/slf4j-api-1.7.21.pom +https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.pom +https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.1.0-alpha1/slf4j-api-2.1.0-alpha1.pom +https://repo.maven.apache.org/maven2/org/slf4j/slf4j-bom/2.1.0-alpha1/slf4j-bom-2.1.0-alpha1.pom +https://repo.maven.apache.org/maven2/org/slf4j/slf4j-parent/1.7.21/slf4j-parent-1.7.21.pom +https://repo.maven.apache.org/maven2/org/slf4j/slf4j-parent/1.7.30/slf4j-parent-1.7.30.pom +https://repo.maven.apache.org/maven2/org/slf4j/slf4j-parent/2.1.0-alpha1/slf4j-parent-2.1.0-alpha1.pom +https://repo.maven.apache.org/maven2/org/sonatype/oss/oss-parent/4/oss-parent-4.pom +https://repo.maven.apache.org/maven2/org/sonatype/oss/oss-parent/5/oss-parent-5.pom +https://repo.maven.apache.org/maven2/org/sonatype/oss/oss-parent/7/oss-parent-7.pom +https://repo.maven.apache.org/maven2/org/sonatype/oss/oss-parent/9/oss-parent-9.pom +https://repo.maven.apache.org/maven2/org/tensorflow/tensorflow-lite-metadata/0.1.0-rc1/tensorflow-lite-metadata-0.1.0-rc1.pom +https://repo.maven.apache.org/maven2/org/tensorflow/tensorflow-lite-metadata/0.1.0-rc2/tensorflow-lite-metadata-0.1.0-rc2.pom +https://repo.maven.apache.org/maven2/tools/jackson/jackson-base/3.0.0-rc2/jackson-base-3.0.0-rc2.pom +https://repo.maven.apache.org/maven2/tools/jackson/jackson-bom/3.0.0-rc2/jackson-bom-3.0.0-rc2.pom +https://repo.maven.apache.org/maven2/xerces/xercesImpl/2.12.0/xercesImpl-2.12.0.pom +https://repo.maven.apache.org/maven2/xml-apis/xml-apis/1.4.01/xml-apis-1.4.01.pom diff --git a/nix/deps/gradle/deps_hack.sh b/nix/deps/gradle/deps_hack.sh new file mode 100755 index 00000000000..0584329ee29 --- /dev/null +++ b/nix/deps/gradle/deps_hack.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fallback versions for each dependency +readonly FOOJAY_FALLBACK="0.5.0" +readonly KOTLIN_FALLBACK="1.8.0" +readonly TOOLS_FALLBACK="3.5.4" +readonly HERMES_FALLBACK="0.73.5" +readonly GRADLE_LINT_FALLBACK="31.1.1" + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +cd "${GIT_ROOT}" + +get_version() { + local file="$1" + local pattern="$2" + local fallback="$3" + local version + + if [[ ! -f "$file" ]]; then + echo "$fallback" + return 0 + fi + + version=$(grep "$pattern" "$file" 2>/dev/null | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.*[0-9]*" | tail -n1 || echo "$fallback") + echo "$version" +} + +# https://github.com/googlesamples/android-custom-lint-rules/blob/be672cd747ecf13844918e1afccadb935f856a72/docs/api-guide/example.md.html#L112-L129 +get_gradle_lint_version() { + gradlePluginVersion=$(get_version "./nix/pkgs/aapt2/default.nix" "version =" $GRADLE_LINT_FALLBACK) + major=$(echo "$gradlePluginVersion" | grep -o "^[0-9]*") + # lintVersion = gradlePluginVersion + 23.0.0 + major=$((major + 23)) + minor=$(echo "$gradlePluginVersion" | grep -o "\.[0-9]*\." | grep -o "[0-9]*") + patch=$(echo "$gradlePluginVersion" | grep -o "[0-9]*$") + + echo "$major.$minor.$patch" +} + +foojay_version=$(get_version "./node_modules/@react-native/gradle-plugin/settings.gradle.kts" "foojay.*version" "$FOOJAY_FALLBACK") +kotlin_version=$(get_version "./node_modules/@react-native/gradle-plugin/gradle/libs.versions.toml" "kotlin =" "$KOTLIN_FALLBACK") +tools_version=$(get_version "./patches/BlurView-build.gradle.patch" "gradle:" "$TOOLS_FALLBACK") +hermes_version=$(get_version "./node_modules/react-native/ReactAndroid/gradle.properties" "VERSION_NAME" "$HERMES_FALLBACK") +lint_version=$(get_gradle_lint_version) + +cat << EOF +org.gradle.toolchains.foojay-resolver-convention:org.gradle.toolchains.foojay-resolver-convention.gradle.plugin:$foojay_version +org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:$kotlin_version +com.android.tools.build:gradle:$tools_version +com.facebook.react:hermes-android:$hermes_version +com.android.tools.lint:lint-gradle:$lint_version +EOF diff --git a/nix/deps/gradle/generate.sh b/nix/deps/gradle/generate.sh new file mode 100755 index 00000000000..267cb74e8b0 --- /dev/null +++ b/nix/deps/gradle/generate.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +# This script takes care of generating/updating the maven-sources.nix file +# representing the offline Maven repo containing the dependencies +# required to build the project + +set -Eeo pipefail + +if [[ -z "${IN_NIX_SHELL}" ]]; then + echo "Remember to call 'make shell'!" + exit 1 +fi + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" + +CUR_DIR=$(cd "${BASH_SOURCE%/*}" && pwd) +PROJ_LIST="${CUR_DIR}/proj.list" +DEPS_LIST="${CUR_DIR}/deps.list" +DEPS_URLS="${CUR_DIR}/deps.urls" +DEPS_JSON="${CUR_DIR}/deps.json" + +# Raise limit of file descriptors +ulimit -n 16384 + +# Generate list of Gradle sub-projects. +function gen_proj_list() { + "${CUR_DIR}/get_projects.sh" | sort -u -o "${PROJ_LIST}" + echo -e "Found ${GRN}$(wc -l < "${PROJ_LIST}")${RST} sub-projects..." +} + +# Check each sub-project in parallel, the "" is for local deps. +function gen_deps_list() { + PROJECTS=$(cat "${PROJ_LIST}") + # WARNING: The ${PROJECTS[@]} needs to remain unquoted to expand correctly. + # shellcheck disable=SC2068 + "${CUR_DIR}/get_deps.sh" "" ${PROJECTS[@]} | sort -uV -o "${DEPS_LIST}" + echo -e "${CLR}Found ${GRN}$(wc -l < "${DEPS_LIST}")${RST} direct dependencies..." +} + +# FIXME: Fix for missing packages. +# https://github.com/status-im/status-mobile/issues/15447 +function add_deps_hack() { + "${CUR_DIR}/deps_hack.sh" >> "${DEPS_LIST}" +} + +# Find download URLs for each dependency. +function gen_deps_urls() { + go-maven-resolver < "${DEPS_LIST}" | sort -uV -o "${DEPS_URLS}" + echo -e "${CLR}Found ${GRN}$(wc -l < "${DEPS_URLS}")${RST} dependency URLs..." +} + +# Generate the JSON that Nix will consume. +function gen_deps_json() { + # Open the Nix attribute set. + echo -n "[" > "${DEPS_JSON}" + + # Format URLs into a Nix consumable file. + URLS=$(cat "${DEPS_URLS}") + # Avoid rate limiting by using 4 of the available threads. + echo "${URLS}" | parallel --will-cite --keep-order --jobs 4 \ + "${CUR_DIR}/url2json.sh" \ + >> "${DEPS_JSON}" + + # Drop tailing comma on last object, stupid JSON + sed -i '$ s/},/}/' "${DEPS_JSON}" + + # Close the Nix attribute set + echo "]" >> "${DEPS_JSON}" +} + +# ------------------------------------------------------------------------------ +echo "Regenerating Nix files..." + +# Gradle needs to be run in 'android' subfolder +cd "${GIT_ROOT}/android" + +# Stop gradle daemons to avoid locking +./gradlew --stop >/dev/null + +# A way to run a specific stage of generation +if [[ -n "${1}" ]] && type "${1}" > /dev/null; then + ${1}; exit 0 +elif [[ -n "${1}" ]]; then + echo "No such function: ${1}"; exit 1 +fi + +# Run each stage in order +gen_proj_list +gen_deps_list +add_deps_hack +gen_deps_urls +gen_deps_json + +REL_DEPS_JSON=$(realpath --relative-to="${PWD}" "${DEPS_JSON}") +echo -e "${CLR}Generated Nix deps file: ${REL_DEPS_JSON#../}" +echo -e "${GRN}Done${RST}" diff --git a/nix/deps/gradle/get_deps.sh b/nix/deps/gradle/get_deps.sh new file mode 100755 index 00000000000..ef7d1f55b1f --- /dev/null +++ b/nix/deps/gradle/get_deps.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# This script generates a list of dependencies for the main project and its +# sub-projects defined using Gradle config files. It uses github-dependency-graph-gradle-plugin: +# https://github.com/gradle/github-dependency-graph-gradle-plugin?tab=readme-ov-file#using-the-plugin-to-generate-dependency-reports + +set -Eeuo pipefail + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +GRADLE_REPORTS_DIR="${GIT_ROOT}/android/build/reports/dependency-graph-snapshots" +# Gradle needs to be run in 'android' subfolder. +cd "${GIT_ROOT}/android" + +# Show Gradle log in case of failure. +GRADLE_LOG_FILE='/tmp/gradle.log' +function show_gradle_log() { cat "${GRADLE_LOG_FILE}" >&2; } +trap show_gradle_log ERR + +./gradlew -I init.gradle \ + --dependency-verification=off \ + --no-configuration-cache --no-configure-on-demand \ + :ForceDependencyResolutionPlugin_resolveAllDependencies > "${GRADLE_LOG_FILE}" 2>&1 + +# skip org.webkit:android-jsc, its provided by react-native +# remove when new architecture is enabled +jq -r '.[].dependency | select(startswith("org.webkit:android-jsc") | not)' "${GRADLE_REPORTS_DIR}/dependency-resolution.json" diff --git a/nix/deps/gradle/get_projects.sh b/nix/deps/gradle/get_projects.sh new file mode 100755 index 00000000000..8a97870a23e --- /dev/null +++ b/nix/deps/gradle/get_projects.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# This script generates a list of Gradle sub-projects by parsing the output +# of Gradle 'projects' task using grep and sed. It is necessary in order to +# collect list of dependencies for main project and its sub-projects. + +set -Eeuo pipefail + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +# Gradle needs to be run in 'android' subfolder. +cd "${GIT_ROOT}/android" + +# Show Gradle log in case of failure. +GRADLE_LOG_FILE='/tmp/gradle.log' +function show_gradle_log() { cat "${GRADLE_LOG_FILE}" >&2; } +trap show_gradle_log ERR + +# Print all our sub-projects +./gradlew projects --no-daemon --console plain 2>&1 \ + | tee "${GRADLE_LOG_FILE}" \ + | grep "Project ':" \ + | sed -E "s;^.--- Project '\:([@_a-zA-Z0-9\-]+)'.*;\1;" diff --git a/nix/deps/gradle/proj.list b/nix/deps/gradle/proj.list new file mode 100644 index 00000000000..39e5af6b067 --- /dev/null +++ b/nix/deps/gradle/proj.list @@ -0,0 +1,42 @@ +app +react-native-async-storage_async-storage +react-native-background-timer +react-native-biometrics +react-native-blob-util +react-native-blur +react-native-camera-kit +react-native-camera-roll_camera-roll +react-native-clipboard_clipboard +react-native-community_audio-toolkit +react-native-community_masked-view +react-native-community_netinfo +react-native-community_slider +react-native-config +react-native-dialogs +react-native-fast-image +react-native-firebase_app +react-native-firebase_messaging +react-native-fs +react-native-gesture-handler +react-native-get-random-values +react-native-hole-view +react-native-image-crop-picker +react-native-image-resizer +react-native-keychain +react-native-linear-gradient +react-native-lottie-splash-screen +react-native-mmkv +react-native-navigation +react-native-nfc-manager +react-native-orientation-locker +react-native-pdf +react-native-permissions +react-native-reanimated +react-native-safe-area-context +react-native-shake +react-native-share +react-native-status +react-native-status-keycard +react-native-svg +react-native-webview +walletconnect_react-native-compat diff --git a/nix/deps/gradle/url2json.sh b/nix/deps/gradle/url2json.sh new file mode 100755 index 00000000000..8a84a592a94 --- /dev/null +++ b/nix/deps/gradle/url2json.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +# +# This script takes a deps.list file and builds a Nix expression +# that can be used by maven-repo-builder.nix to produce a path to +# a local Maven repository. +# + +# This defines URLs of Maven repos we know about and use. +declare -a REPOS=( + "https://repo.maven.apache.org/maven2" + "https://dl.google.com/dl/android/maven2" + "https://plugins.gradle.org/m2" + "https://jitpack.io" +) + +# These files are not necessary for the build process. +FILENAMES_BLACKLIST='-(javadoc|runtime|gwt|headers|sources|src|tests|adapters|modular|site|bin)\.' +FILETYPES_BLACKLIST='(pom|json|zip|xml|md5|sha1|sha256|sha512)$' +SIGNATURE_BLACKLIST='(pom|jar|json|zip|module|xml|md5|asc).asc$' + +function nix_prefetch() { + nix store prefetch-file --json "${1}" 2>/dev/null +} + +function get_nix_path() { echo "${1}" | jq -r .storePath; } +function get_nix_sha() { echo "${1}" | jq -r .hash; } +function get_sha1() { sha1sum "${1}" | cut -d' ' -f1; } + +# Assumes REPOS from repos.sh is available +function match_repo_url() { + for REPO_URL in "${REPOS[@]}"; do + if [[ "$1" = ${REPO_URL}* ]]; then + echo "${REPO_URL}" + return + fi + done + echo " ! Failed to match a repo for: ${1}" >&2 + exit 1 +} + +function pom_has_nodeps_jar() { + grep 'nodeps' "${1}" \ + >/dev/null 2>&1 +} + +function guess_pkg_files() { + # Some deps are just POMs, in which case there is no JAR to fetch. + [[ "${OBJ_TYPE}" == "" ]] && echo "${PKG_NAME}.jar" + [[ "${OBJ_TYPE}" == "jar" ]] && echo "${PKG_NAME}.jar" + [[ "${OBJ_TYPE}" == "bundle" ]] && echo "${PKG_NAME}.jar" + [[ "${OBJ_TYPE}" =~ aar* ]] && echo "${PKG_NAME}.aar" + [[ "${OBJ_TYPE}" == "aar.asc" ]] && echo "${PKG_NAME}.${OBJ_TYPE}" + pom_has_nodeps_jar "${POM_PATH}" && echo "${PKG_NAME}-nodeps.jar" +} + +function get_pkg_files() { + REPO_URL="${1}" + PKG_PATH="${2}" + PKG_NAME="${3}" + # Google Maven repo doesn't have normal HTML directory listing. + if [[ "${REPO_URL}" == "https://dl.google.com/dl/android/maven2" ]]; then + FOUND=$(curl --fail -s "${REPO_URL}/${PKG_PATH}/artifact-metadata.json") + # Some older packages do not have artifacts-metadata.json. + if [[ "$?" -eq 0 ]]; then + FOUND=$(echo "${FOUND}" | jq -r '.artifacts[].name') + else + FOUND='' + fi + else + FOUND=$( + curl -s "${REPO_URL}/${PKG_PATH}/" \ + | htmlq a -a href \ + | grep -e "^${PKG_NAME}" + ) + fi + if [[ "${FOUND}" == '' ]]; then + guess_pkg_files + else + # Filter out files we don't actually need for builds. + echo "${FOUND}" \ + | grep -v -E \ + -e "${FILENAMES_BLACKLIST}" \ + -e "${FILETYPES_BLACKLIST}" \ + -e "${SIGNATURE_BLACKLIST}" + fi +} + +function fetch_and_template_file() { + local FILENAME="${1}" + local OBJ_URL OBJ_NIX_FETCH_OUT OBJ_NAME OBJ_PATH + + OBJ_URL="${REPO_URL}/${PKG_PATH}/${FILENAME}" + if ! OBJ_NIX_FETCH_OUT=$(nix_prefetch "${OBJ_URL}"); then + echo " ! Failed to fetch: ${OBJ_URL}" >&2 + return 1 + fi + + OBJ_NAME="${FILENAME}" + OBJ_PATH=$(get_nix_path "${OBJ_NIX_FETCH_OUT}") + echo -n ", + \"${OBJ_NAME}\": { + \"sha1\": \"$(get_sha1 "${OBJ_PATH}")\", + \"sha256\": \"$(get_nix_sha "${OBJ_NIX_FETCH_OUT}")\" + }" +} + +if [[ -z "${1}" ]]; then + echo "Required POM URL argument not given!" >&2 + exit 1 +fi +POM_URL=${1} +# Drop the POM extension. +PKG_URL_NO_EXT="${POM_URL%.pom}" +# Name of package without extension. +PKG_NAME="$(basename "${PKG_URL_NO_EXT}")" + +# Append spaces to avoid overlapping package names. +printf -v spaces '%*s' 60 '' +echo -en "${CLR} - Nix entry for: ${1##*/}$spaces\r" >&2 + +REPO_URL=$(match_repo_url "${PKG_URL_NO_EXT}") + +if [[ -z "${REPO_URL}" ]]; then + echo " ! Repo URL not found for: ${POM_URL}" >&2 + exit 1 +fi +# Get the relative path without full URL +PKG_PATH="${PKG_URL_NO_EXT#"${REPO_URL}/"}" +PKG_PATH="$(dirname "${PKG_PATH}")" + +# Both JARs and AARs have a POM +POM_NIX_FETCH_OUT=$(nix_prefetch "${PKG_URL_NO_EXT}.pom") +POM_PATH=$(get_nix_path "${POM_NIX_FETCH_OUT}") +POM_NAME=$(basename "${PKG_URL_NO_EXT}.pom") +if [[ -z "${POM_PATH}" ]]; then + echo " ! Failed to fetch: ${PKG_URL_NO_EXT}.pom" >&2 + exit 1 +fi +POM_SHA256=$(get_nix_sha "${POM_NIX_FETCH_OUT}") +POM_SHA1=$(get_sha1 "${POM_PATH}") + +# Identify packaging type, JAR, AAR, bundle, or just POM. +OBJ_TYPE=$(grep -oP '\K[^<]+' "${POM_PATH}") + +# Some deps are Eclipse plugins, and we don't need those. +if [[ "${OBJ_TYPE}" == "eclipse-plugin" ]]; then + exit 0 +fi + +# Format into a Nix attrset entry +echo -ne " + { + \"path\": \"${PKG_PATH}\", + \"repo\": \"${REPO_URL}\", + \"files\": { + \"${POM_NAME}\": { + \"sha1\": \"${POM_SHA1}\", + \"sha256\": \"${POM_SHA256}\" + }" + +for FILE in $(get_pkg_files "${REPO_URL}" "${PKG_PATH}" "${PKG_NAME}"); do + fetch_and_template_file "${FILE}" +done + +echo -e '\n }\n },' diff --git a/nix/deps/nodejs-patched/README.md b/nix/deps/nodejs-patched/README.md new file mode 100644 index 00000000000..0b0c28caddf --- /dev/null +++ b/nix/deps/nodejs-patched/README.md @@ -0,0 +1,18 @@ +# Description + +This Nix derivation takes the result of the [`nix/deps/nodejs/default.nix`](../nodejs/default.nix) derivation and adjusts it for use with Gradle. + +# Details + +Modules provided by `yarn2nix` are normally fine, but we use `react-native-*` packages which have their own `gradle.build` files that reference external Maven repositories: +```js +repositories { + google() + maven { url "https://www.jitpack.io" } +} +``` +And these need to be patched and replaced with `mavenLocal()` to make sure Gradle doesn't try to fetch dependencies from remote repos. + +This derivation symlinks most of the modules found in the `yarn2nix` result and copies the ones that require patching of `gradle.build` files in `patchGradlePhase`. + +It also applies other fixes like making `BuildId` static in `patchBuildIdPhase` and fixing a Hermes bug in `patchHermesPhase`. diff --git a/nix/deps/nodejs-patched/default.nix b/nix/deps/nodejs-patched/default.nix new file mode 100644 index 00000000000..6588b61df56 --- /dev/null +++ b/nix/deps/nodejs-patched/default.nix @@ -0,0 +1,72 @@ +# This derivation patches Node.js dependencies by +# taking the result of yarn2nix and symlinking what is fine, +# and copying and modifying what needs to be adjusted. + +{ stdenv, deps, nodejs, patchMavenSources }: + +let + patchesDir = ./../../../patches; + patches = builtins.attrNames (builtins.readDir patchesDir); +in +stdenv.mkDerivation { + name = "${deps.nodejs.name}-patched"; + phases = [ + "unpackPhase" + "patchGradlePhase" + "patchNodeLibsPhase" + "installPhase" + ]; + + # First symlink all modules as is + # WARNING: Metro has issues when dealing with symlinks! + unpackPhase = '' + mkdir -p ./node_modules/ + for module in $(ls ${deps.nodejs}/node_modules); do + ln -s ${deps.nodejs}/node_modules/$module ./node_modules/ + done + cp -r ${deps.nodejs}/node_modules/.bin ./node_modules/ + ''; + + # Patch build.gradle files in 'react-native-*' dependencies to replace + # maven and google central repositories with our own local directories. + # This prevents the builder from downloading Maven artifacts + patchGradlePhase = '' + gradleConfigs=$( + find -L ./node_modules \ + -name build.gradle -or \ + -name build.gradle.kts -or \ + -name settings.gradle -or \ + -name settings.gradle.kts + ) + for modBuildGradle in $gradleConfigs; do + relativeToNode=''${modBuildGradle#*node_modules/} + moduleName=''${relativeToNode%%/*} + if [[ -L ./node_modules/$moduleName ]]; then + unlink ./node_modules/$moduleName + cp -r ${deps.nodejs}/node_modules/$moduleName ./node_modules/ + chmod u+w -R ./node_modules/$moduleName + fi + ${patchMavenSources} $modBuildGradle + done + ''; + + installPhase = '' + mkdir -p $out + cp -R node_modules $out/ + ''; + + patchNodeLibsPhase = '' + for patch in ${toString patches}; do + patch -p1 < ${patchesDir}/$patch + done + ''; + + # The ELF types are incompatible with the host platform, so let's not even try + # TODO: Use Android NDK to strip binaries manually + dontPatchELF = true; + dontStripHost = true; + + # Take whole sources into consideration when calculating sha + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; +} diff --git a/nix/deps/nodejs/README.md b/nix/deps/nodejs/README.md new file mode 100644 index 00000000000..6bb663dd591 --- /dev/null +++ b/nix/deps/nodejs/README.md @@ -0,0 +1,57 @@ +# Description + +This derivation uses a [yarn2nix](https://github.com/nix-community/yarn2nix) to generate a Nix store package containing all NodeJS dependencies. + +# Details + +If you look at [`nix/deps/nodejs/default.nix`](./default.nix) you'll see that it it loads `package.json` and `yarn.lock` files and uses them to generate the derivation: +```sh + > make nix-repl +Welcome to Nix version 2.3.9. Type :? for help. + +nix-repl> pkgs.deps.nodejs +«derivation /nix/store/23mpmpjjnq7miclv6bc6ilgypy8wz69p-status-mobile-node-deps-1.8.0.drv» +``` +You can build the derivation: +``` +nix-repl> :b pkgs.deps.nodejs + +this derivation produced the following outputs: + out -> /nix/store/dqb2cjyz1g6n7jic07058y26lnmgaaz9-status-mobile-node-deps-1.8.0 +``` +And look inside: +``` + > cd /nix/store/dqb2cjyz1g6n7jic07058y26lnmgaaz9-status-mobile-node-deps-1.8.0 + > ls +deps node_modules + > ls node_modules | grep react-native-image +react-native-image-crop-picker +react-native-image-resizer +``` + +# Known Issues + +It's important that dependencies from GitHub are added in a correct format: +``` +git+https://github.com/status-im/bignumber.js.git#v4.0.2-status +``` +Notice three things: + +* `git+` prefix for the `https://` URL +* Repository name ends with `.git` +* Tag or branch is added after `#` character + +__WARNING__: Using branches can cause dependencies to change their hash and cause builds to fail. Using them during development is fine, but tags should be used in the final version. + +If this format is not used correctly you can see issues like: +``` +fatal: unable to access 'https://github.com/status-im/bignumber.js.git/': Could not resolve host: github.com +``` +or +``` +error Couldn't find any versions for "bignumber.js" that matches "github.com/status-im/bignumber.js.git#v4.0.2-status" in our cache (possible versions are ""). This is usually caused by a missing entry in the lockfile, running Yarn without the --offline flag may help fix this issue. +``` +or +``` +error Can't make a request in offline mode ("https://codeload.github.com/status-im/bignumber.js/tar.gz/f322b670969512a35c84441036a0ba4836a96428") +``` diff --git a/nix/deps/nodejs/default.nix b/nix/deps/nodejs/default.nix new file mode 100644 index 00000000000..1a9061dfd1b --- /dev/null +++ b/nix/deps/nodejs/default.nix @@ -0,0 +1,10 @@ +{ lib, yarn2nix-moretea }: + +# Create a yarn package for our project that contains all the dependecies. +yarn2nix-moretea.mkYarnModules rec { + pname = "status-mobile"; + name = "${pname}-node-deps-${version}"; + version = lib.fileContents ../../../VERSION; + yarnLock = ../../../yarn.lock; + packageJSON = ../../../package.json; +} diff --git a/nix/deps/react-native/default.nix b/nix/deps/react-native/default.nix new file mode 100644 index 00000000000..044864d4544 --- /dev/null +++ b/nix/deps/react-native/default.nix @@ -0,0 +1,29 @@ +{ stdenvNoCC, fetchurl }: + +let + inherit (builtins) map baseNameOf; + + deps = import ./deps.nix; + fetchTarball = dep: with dep; + fetchurl { + inherit url sha256; + name = "${name}-${baseNameOf url}"; + }; + +in stdenvNoCC.mkDerivation { + name = "reactnative-android-deps"; + phases = [ "unpackPhase" ]; + srcs = map fetchTarball deps; + unpackPhase = '' + # Unpack all source archives. + mkdir -p $out/deps + cd $out/deps + for tarball in $srcs; do + unpackFile "$tarball" 2>/dev/null + done + cd .. + + export sourceRoot=$out/deps + echo $sourceRoot + ''; +} diff --git a/nix/deps/react-native/deps.nix b/nix/deps/react-native/deps.nix new file mode 100644 index 00000000000..03850ccb2ed --- /dev/null +++ b/nix/deps/react-native/deps.nix @@ -0,0 +1,29 @@ +# These versions should match values in: +# node_modules/react-native/ReactAndroid/gradle.properties +# Currently this is done manually. +[ + rec { + name = "boost"; + version = "1.63.0"; + url = "https://github.com/react-native-community/boost-for-react-native/releases/download/v${version}-0/${name}_${builtins.replaceStrings ["."] ["_"] version}.tar.gz"; + sha256 = "1hq5h6wzcr3sk1kccfk769d59swwxy1kvfhqd6p7786a1zsavdr6"; + } + rec { + name = "double-conversion"; + version = "1.1.6"; + url = "https://github.com/google/${name}/archive/v${version}.tar.gz"; + sha256 = "0ynnckpyyhpwisb976knk9gr8jklfwr9flic8xj5flc8iv6hm1bb"; + } + rec { + name = "folly"; + version = "2018.10.22.00"; + url = "https://github.com/facebook/${name}/archive/v${version}.tar.gz"; + sha256 = "08cxc8hw34vnyyq45d04spy4dilrgqrv374lwxhg3sx60ww640y4"; + } + rec { + name = "glog"; + version = "0.3.5"; + url = "https://github.com/google/${name}/archive/v${version}.tar.gz"; + sha256 = "1q6ihk2asbx95a56kmyqwysq1x3grrw9jwqllafaidf0l84f903m"; + } +] diff --git a/nix/lib/checkEnvVarSet.nix b/nix/lib/checkEnvVarSet.nix new file mode 100644 index 00000000000..0ab5916bcaf --- /dev/null +++ b/nix/lib/checkEnvVarSet.nix @@ -0,0 +1,6 @@ +# Helper for verifying an environment variable is set +name: '' + if [[ -z ''$${name} ]]; then + echo 'WARNING! Env var not set: ${name}' >&2 + fi +'' diff --git a/nix/lib/default.nix b/nix/lib/default.nix new file mode 100644 index 00000000000..91ec86c680a --- /dev/null +++ b/nix/lib/default.nix @@ -0,0 +1,9 @@ +{ lib }: + +{ + getEnvWithDefault = import ./getEnvWithDefault.nix; + mkFilter = import ./mkFilter.nix { inherit lib; }; + mergeSh = import ./mergeSh.nix { inherit lib; }; + checkEnvVarSet = import ./checkEnvVarSet.nix; + sanitizeVersion = import ./sanitizeVersion.nix { inherit lib; }; +} diff --git a/nix/lib/getEnvWithDefault.nix b/nix/lib/getEnvWithDefault.nix new file mode 100644 index 00000000000..45cdc02f885 --- /dev/null +++ b/nix/lib/getEnvWithDefault.nix @@ -0,0 +1,8 @@ +name: default: let + envOverride = builtins.getEnv name; + logEnvOverride = value: + builtins.trace "getEnvWithDefault ${name} (default: ${toString default}): ${value}" value; +in + if envOverride != "" && envOverride != default + then logEnvOverride envOverride + else default diff --git a/nix/lib/mergeSh.nix b/nix/lib/mergeSh.nix new file mode 100644 index 00000000000..ae70aa1a911 --- /dev/null +++ b/nix/lib/mergeSh.nix @@ -0,0 +1,20 @@ +# this is an utility for mergeing shells created with mkShell +# TODO: make this an attribute of mkShell result set. Or just use inputsFrom. +{ lib }: + +super: shells: +super.overrideAttrs (super: + let + inherit (lib) attrByPath unique catAttrs concatStrings; + + mergeAttrsFor = attrName: + (attrByPath [attrName] [] super) ++ unique (catAttrs attrName shells); + in { + inputsFrom = mergeAttrsFor "inputsFrom"; + buildInputs = mergeAttrsFor "buildInputs"; + nativeBuildInputs = mergeAttrsFor "nativeBuildInputs"; + propagatedBuildInputs = mergeAttrsFor "propagatedBuildInputs"; + propagatedNativeBuildInputs = mergeAttrsFor "propagatedNativeBuildInputs"; + # shellHook is a string, not a list + shellHook = (super.shellHook or "") + concatStrings (catAttrs "shellHook" shells); + }) diff --git a/nix/lib/mkFilter.nix b/nix/lib/mkFilter.nix new file mode 100644 index 00000000000..0097e4ddaf7 --- /dev/null +++ b/nix/lib/mkFilter.nix @@ -0,0 +1,62 @@ +# This Nix expression allows filtering a local directory by +# specifying dirRootsToInclude, dirsToExclude and filesToInclude. +# It also filters out symlinks to result folders created by nix-build, +# as well as backup/swap/generated files. + +{ lib }: + +let + inherit (lib) + any range flatten length sublist cleanSourceFilter + splitString hasPrefix removePrefix concatStringsSep; + inherit (builtins) map match; + + mkFilter = { + # primary path under which all files are included, unless excluded + root, + # list of regex expressions to match files to include/exclude + include ? [ ], + exclude ? [ ], # has precedence over include + # by default we ignore Version Control System files + ignoreVCS ? true, + }: + let + # removes superfluous slashes from the path + cleanRoot = "${toString (/. + root)}/"; + in path: type: + let + # unpack path: "x/y/.*" => ["x" "x/y" "x/y/.*"] + unpackPath = path: + let + tokens = splitString "/" path; + perms = range 1 (length tokens); + subPaths = builtins.map (x: sublist 0 x tokens) perms; + in builtins.map (x: concatStringsSep "/" x) subPaths; + # accept subdirs from regexes + includeSubdirs = regexes: flatten (map unpackPath regexes); + # checks all regexes in a list against str + matchesRegexes = str: regexes: (map (r: (match r str)) regexes); + # match returns empty list on match + isMatch = x: x == [ ]; + # path relative to search root + relPath = removePrefix cleanRoot path; + # check if any of the regexes match the relative path + checkRegexes = regexes: any isMatch (matchesRegexes relPath regexes); + + # main check methods + isRootSubdir = hasPrefix cleanRoot path; + isIncluded = checkRegexes (includeSubdirs include); + isExcluded = checkRegexes exclude; + isVCS = ignoreVCS && !cleanSourceFilter path type; + + in + if !isRootSubdir then + # everything outside of root is excluded + false + else if isExcluded || isVCS then + # isExcluded has precedence over isIncluded + false + else + isIncluded; + +in mkFilter diff --git a/nix/lib/mkFilter_test.nix b/nix/lib/mkFilter_test.nix new file mode 100644 index 00000000000..9651c48d442 --- /dev/null +++ b/nix/lib/mkFilter_test.nix @@ -0,0 +1,61 @@ +# This file is an example of the syntax required to call a Nix function +# and serves to test mkFilter.nix. +# +# nix-instantiate --strict --json --eval ./mkFilter_test.nix | jq +# [ +# { +# "pass": true, +# "path": "/ABS/PATH/android/subdir", +# }, +# { +# "pass": true, +# "path": "/ABS/PATH/ios", +# } +# ... +# ] + +let + pkgs = import { }; + mkFilter = pkgs.callPackage ./mkFilter.nix { inherit (pkgs) lib; }; + absPath = "/ABS/PATH"; + filter = mkFilter { + root = absPath; + include = [ "android" ".*included.*" "sub/folder/.*" ]; + exclude = [ ".*excluded.*" ]; + }; + tests = [ + { path = "/WRONG/ABS/PATH"; type = "directory"; + expected = false; } + { path = "${absPath}/.git"; type = "directory"; + expected = false; } + { path = "${absPath}/included"; type = "file"; + expected = true; } + { path = "${absPath}/android/included"; type = "directory"; + expected = true; } + { path = "${absPath}/sub"; type = "file"; + expected = true; } + { path = "${absPath}/sub/folder"; type = "file"; + expected = true; } + { path = "${absPath}/sub/folder/file"; type = "file"; + expected = true; } + { path = "${absPath}/sub/folder/xyz/file"; type = "file"; + expected = true; } + { path = "${absPath}/android/sub/included"; type = "directory"; + expected = true; } + { path = "${absPath}/android/included/excluded"; type = "directory"; + expected = false; } + { path = "${absPath}/android/excluded"; type = "directory"; + expected = false; } + { path = "${absPath}/ios/included"; type = "directory"; + expected = true; } + { path = "${absPath}/ios/subfile"; type = "file"; + expected = false; } + ]; +in builtins.map ( + t: let + rval = (filter t.path t.type); + in { + path = t.path; + pass = t.expected == rval; + } + ) tests diff --git a/nix/lib/sanitizeVersion.nix b/nix/lib/sanitizeVersion.nix new file mode 100644 index 00000000000..ab6435e2c2d --- /dev/null +++ b/nix/lib/sanitizeVersion.nix @@ -0,0 +1,14 @@ +{ lib }: + +version: +let + # paths don't like slashes in them + dropSlashes = builtins.replaceStrings [ "/" ] [ "_" ]; + # if version doesn't match this it's probably a commit, it's lax semver + versionRegex = "^v?[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+[[:alnum:]+_.-]*$"; +in + if (builtins.match versionRegex version) != null + # Geth forces a 'v' prefix for all versions + then lib.removePrefix "v" (dropSlashes version) + # reduce metrics cardinality in Prometheus + else lib.traceValFn (_: "WARNING: Marking build version as 'develop'!") "develop" diff --git a/nix/mobile/android/build.nix b/nix/mobile/android/build.nix new file mode 100644 index 00000000000..d2058d4772a --- /dev/null +++ b/nix/mobile/android/build.nix @@ -0,0 +1,194 @@ +{ stdenv, pkgs, deps, lib +, androidPkgs, patchMavenSources, jsbundle, status-go }: + +{ + # Value for BUILD_ENV checked by Clojure code at compile time + buildEnv ? "prod", + # Path to the file containing secret environment variables + secretsFile ? "", + # Build type (influences which .env file gets used for feature flags) + buildType ? lib.getEnvWithDefault "BUILD_TYPE" "release", + # Used for versionCode + versionCode ? lib.getEnvWithDefault "ORG_GRADLE_PROJECT_versionCode" 9999, + # Included in APK Manifest for easier identification. + commitHash ? lib.getEnvWithDefault "ORG_GRADLE_PROJECT_commitHash" "unknown", + # Disabled for debug builds to avoid 'maximum call stack exceeded' errors. + # https://github.com/status-im/status-mobile/issues/18493 + hermesEnabled ? lib.getEnvWithDefault "ORG_GRADLE_PROJECT_hermesEnabled" "true", + buildUrl ? lib.getEnvWithDefault "ORG_GRADLE_PROJECT_buildUrl" null, + statusGoSrcOverride ? lib.getEnvWithDefault "STATUS_GO_SRC_OVERRIDE" null, + reactMetroPort ? lib.getEnvWithDefault "RCT_METRO_PORT" 8081, + # If APKs should be split based on architectures + androidAbiSplit ? lib.getEnvWithDefault "ANDROID_ABI_SPLIT" "true", + # Android architectures to build for + # Used to detect end-to-end builds + androidAbiInclude ? lib.getEnvWithDefault "ANDROID_ABI_INCLUDE" "arm64-v8a", + universalApk ? lib.getEnvWithDefault "ORG_GRADLE_PROJECT_universalApk" "false" +}: + +let + inherit (lib) toLower optionalString stringLength makeLibraryPath elem; + notDebug = (buildType != "debug"); + + # Pass secretsFile for POKT_TOKEN to jsbundle build + builtJsBundle = lib.optionals notDebug jsbundle { inherit secretsFile; }; + + # Map ANDROID_ABI_INCLUDE to status-go targets + androidAbiIncludeSplit = lib.splitString ";" androidAbiInclude; + + + envFileName = + if (elem androidAbiInclude ["x86" "x86_64" "x86;x86_64"]) then ".env.e2e" + else if (elem buildType ["release" "nightly"]) then ".env.${buildType}" + else if (elem buildType ["pr" "manual"]) then ".env.jenkins" + else ".env"; + + gradleBuildType = + if buildType == "pr" then "Pr" + else if buildType == "debug" then "Debug" + else "Release"; + + apksPath = "./android/app/build/outputs/apk/${toLower gradleBuildType}"; + + baseName = "${buildType}-android"; +in stdenv.mkDerivation rec { + name = "status-mobile-build-${baseName}"; + + src = let path = ./../../..; + # We use builtins.path so that we can name the resulting derivation + in builtins.path { + inherit path; + name = "status-mobile-source-${baseName}"; + # Keep this filter as restrictive as possible in order to avoid unnecessary rebuilds and limit closure size + filter = lib.mkFilter { + root = path; + include = [ + "package.json" "yarn.lock" "metro.config.js" "babel.config.js" + "resources/.*" "translations/.*" "src/js/.*" "index.js" + "modules/react-native-status/android.*" "android/.*" + envFileName "VERSION" "status-go-version.json" "react-native.config.js" + ]; + }; + }; + + buildInputs = with pkgs; [ nodejs openjdk_headless ]; + nativeBuildInputs = with pkgs; [ bash gradle unzip ] + ++ lib.optionals stdenv.isDarwin [ file gnumake ]; + + # Disable metro watching for file changes. (#13783) + CI = true; + + # Used by Clojure at compile time to include JS modules + BUILD_ENV = buildEnv; + + STATUS_GO_SRC_OVERRIDE = statusGoSrcOverride; + ANDROID_ABI_SPLIT = androidAbiSplit; + ANDROID_ABI_INCLUDE = androidAbiInclude; + # Disabled for debug builds to avoid 'maximum call stack exceeded' errors. + # https://github.com/status-im/status-mobile/issues/18493 + ORG_GRADLE_PROJECT_versionCode = versionCode; + ORG_GRADLE_PROJECT_commitHash = commitHash; + ORG_GRADLE_PROJECT_buildUrl = buildUrl; + ORG_GRADLE_PROJECT_hermesEnabled = hermesEnabled; + ORG_GRADLE_PROJECT_universalApk = universalApk; + + # Fix for ERR_OSSL_EVP_UNSUPPORTED error. + NODE_OPTIONS = "--openssl-legacy-provider"; + + phases = [ + "shellHook" "unpackPhase" "secretsPhase" "buildPhase" "checkPhase" "installPhase" + ]; + + # We use shellHook as a single place to setup env vars for both build derivation and shell + shellHook = '' + # Used by the Android Gradle build script in android/build.gradle + export STATUS_GO_ANDROID_LIBDIR=${ status-go { abis = androidAbiIncludeSplit; } } + + # Android SDK/NDK for use by Gradle + export ANDROID_SDK_ROOT="${androidPkgs.sdk}" + export ANDROID_NDK_ROOT="${androidPkgs.ndk}" + + export STATUS_NIX_MAVEN_REPO="${deps.gradle}" + ''; + + unpackPhase = '' + cp -ar $src/. ./ + chmod u+w -R ./ + runHook postUnpack + ''; + postUnpack = '' + # Ensure we have the right .env file + cp -bf ./${envFileName} ./.env + + # Export all vars from .env file + export $(cut -d= -f1 .env) + + ${lib.optionalString notDebug '' + # Symlink React Native entrypoint. + cp -Lr ${builtJsBundle} ./result + ''} + + # Copy android/ directory + mkdir -p ./android/build + chmod -R +w ./android + + # Copy node_modules/ directory. The -L is CRUCIAL! + # Otherwise Metro failes to find modules due to symlinks. + cp -aL ${deps.nodejs-patched}/node_modules/ ./ + chmod +w -R ./node_modules + + # Patch build.gradle to use local repo + ${patchMavenSources} ./android/build.gradle + ''; + + # Secrets file is passed to sandbox using extra-sandbox-paths. + secretsPhase = if (secretsFile != "") then '' + source "${secretsFile}" + '' else '' + echo 'WARNING: No secrets provided!' >&2 + ''; + + buildPhase = let + adhocEnvVars = optionalString stdenv.isLinux + "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${makeLibraryPath [ pkgs.zlib ]}"; + gradleCommand = '' + ${pkgs.gradle}/bin/gradle \ + --console=plain \ + --offline \ + --no-daemon \ + --no-scan \ + --no-watch-fs \ + --no-build-cache \ + --parallel \ + -Dmaven.repo.local='${deps.gradle}' \ + -PreactNativeDevServerPort=${toString reactMetroPort} \ + assemble${gradleBuildType} + ''; + in + assert ANDROID_ABI_SPLIT != null && ANDROID_ABI_SPLIT != ""; + assert stringLength ANDROID_ABI_INCLUDE > 0; + '' + # Fixes issue with failing to load libnative-platform.so + export GRADLE_USER_HOME=$(mktemp -d) + export ANDROID_SDK_HOME=$(mktemp -d) + + echo "Adhoc ENV: ${adhocEnvVars}" + echo "Running: ${gradleCommand}" + + pushd ./android + ${adhocEnvVars} ${gradleCommand} + popd > /dev/null + ''; + + doCheck = buildType != "debug"; + checkPhase = '' + ls ${apksPath}/*.apk \ + | xargs -n1 ${pkgs.unzip}/bin/unzip -qql \ + | grep 'index.android.bundle' + ''; + + installPhase = '' + mkdir -p $out + cp ${apksPath}/*.apk $out/ + ''; +} diff --git a/nix/mobile/android/default.nix b/nix/mobile/android/default.nix new file mode 100644 index 00000000000..8ddcb79477b --- /dev/null +++ b/nix/mobile/android/default.nix @@ -0,0 +1,27 @@ +{ pkgs, deps, callPackage, mkShell +, jsbundle, status-go, androidPkgs, androidShell }: + +rec { + build = callPackage ./build.nix { + inherit jsbundle status-go; + }; + + shell = mkShell { + buildInputs = with pkgs; [ + openjdk_headless + gradle + lsof # used in start-react-native.sh + flock # used in nix/scripts/node_modules.sh + ]; + + inputsFrom = [ + (build {}) + androidShell + ]; + + shellHook = '' + # check if node modules changed and if so install them + $STATUS_MOBILE_HOME/nix/scripts/node_modules.sh ${deps.nodejs-patched} + ''; + }; +} diff --git a/nix/mobile/default.nix b/nix/mobile/default.nix new file mode 100644 index 00000000000..7c7784132e6 --- /dev/null +++ b/nix/mobile/default.nix @@ -0,0 +1,33 @@ +{ lib, callPackage, mkShell, status-go }: + +let + fastlane = callPackage ./fastlane { }; + + jsbundle = callPackage ./jsbundle { }; + + android = callPackage ./android { + inherit jsbundle; + status-go = status-go.mobile.android; + }; + + ios = callPackage ./ios { + inherit fastlane; + status-go = status-go.mobile.ios; + }; + + selectedSources = [ + status-go.mobile.android + status-go.mobile.ios + fastlane + android + ios + ]; + +in { + shell = mkShell { + inputsFrom = lib.catAttrs "shell" selectedSources; + }; + + # TARGETS + inherit android ios fastlane jsbundle; +} diff --git a/nix/mobile/fastlane/default.nix b/nix/mobile/fastlane/default.nix new file mode 100644 index 00000000000..2bb8adc318d --- /dev/null +++ b/nix/mobile/fastlane/default.nix @@ -0,0 +1,28 @@ +{ lib, stdenv, callPackage, mkShell, + bundlerEnv, cocoapods, bundler, ruby, curl, clang }: + +let + inherit (lib) optionals optionalString unique; + + fastlane = callPackage ../../../fastlane { + bundlerEnv = _: + bundlerEnv { + name = "fastlane-gems"; + gemdir = ../../../fastlane; + }; + }; + + inherit (fastlane) shellHook; + + buildInputs = [ ruby bundler fastlane curl clang ] + ++ optionals stdenv.isDarwin [ cocoapods ]; +in { + # HELPERS + inherit shellHook buildInputs; + + # TARGETS + drv = fastlane; + shell = mkShell { + inherit shellHook buildInputs; + }; +} diff --git a/nix/mobile/ios/default.nix b/nix/mobile/ios/default.nix new file mode 100644 index 00000000000..ca423f50f17 --- /dev/null +++ b/nix/mobile/ios/default.nix @@ -0,0 +1,37 @@ +{ callPackage, lib, mkShell, pkgs, stdenv +, status-go, fastlane }: + +assert lib.assertMsg stdenv.isDarwin + "iOS development shell supported only on OSX."; + +let + inherit (lib) catAttrs unique; + + # Sub-shells preparing various dependencies. + nodejs-sh = callPackage ./shells/nodejs.nix { }; + bundler-sh = callPackage ./shells/bundler.nix { }; + cocoapods-sh = callPackage ./shells/cocoapods.nix { }; + status-go-sh = callPackage ./shells/status-go.nix { inherit status-go; }; + +in { + inherit nodejs-sh cocoapods-sh status-go-sh; + + shell = mkShell { + buildInputs = with pkgs; [ + xcodeWrapper watchman procps + flock # used in nix/scripts/node_modules.sh + xcbeautify # used in 'make run-ios' + libimobiledevice # used in `make run-ios-device` + ]; + + # WARNING: Executes shellHook in reverse order. + # WARNING: Only some variables are merged. + inputsFrom = [ + fastlane.shell + cocoapods-sh + nodejs-sh # before 'pod install' + bundler-sh # before 'pod install' + status-go-sh # before 'pod install' + ]; + }; +} diff --git a/nix/mobile/ios/shells/bundler.nix b/nix/mobile/ios/shells/bundler.nix new file mode 100644 index 00000000000..506054ef09b --- /dev/null +++ b/nix/mobile/ios/shells/bundler.nix @@ -0,0 +1,8 @@ +{ mkShell, bundler }: + +mkShell { + buildInputs = [ bundler ]; + shellHook = '' + bundle install --quiet --gemfile=fastlane/Gemfile + ''; +} diff --git a/nix/mobile/ios/shells/cocoapods.nix b/nix/mobile/ios/shells/cocoapods.nix new file mode 100644 index 00000000000..32d447a2a25 --- /dev/null +++ b/nix/mobile/ios/shells/cocoapods.nix @@ -0,0 +1,26 @@ +{ mkShell, cocoapods }: + +let + # source of what pod should install + podfileLock = "ios/Podfile.lock"; + # current state of pods installed by pod + manifestLock = "ios/Pods/Manifest.lock"; +in mkShell { + buildInputs = [ cocoapods ]; + # https://github.com/reactwg/react-native-new-architecture/blob/main/docs/enable-apps.md#enable-new-architecture-for-ios + RCT_NEW_ARCH_ENABLED = 1; + shellHook = '' + pushd "$STATUS_MOBILE_HOME" > /dev/null + { + echo "Checking for modifications in ios/Pods..." + if diff -q ${podfileLock} ${manifestLock}; then + echo "No modifications detected." + else + # CocoaPods are trash and can't handle other pod instances running + ./scripts/wait-for.sh 240 'pod install' + (cd ios && pod install) + fi + } + popd > /dev/null + ''; +} diff --git a/nix/mobile/ios/shells/nodejs.nix b/nix/mobile/ios/shells/nodejs.nix new file mode 100644 index 00000000000..9526d63c0ae --- /dev/null +++ b/nix/mobile/ios/shells/nodejs.nix @@ -0,0 +1,15 @@ +{ mkShell, nodejs, deps }: + +mkShell { + shellHook = '' + # Fix for ERR_OSSL_EVP_UNSUPPORTED error. + export NODE_OPTIONS="--openssl-legacy-provider"; + # Same fix but for Xcode React Native script. + export NODE_ARGS="--openssl-legacy-provider --max-old-space-size=16384"; + # Fix Xcode using system Node.js version. + export NODE_BINARY="${nodejs}/bin/node"; + + # Check if node modules changed and if so install them. + "$STATUS_MOBILE_HOME/nix/scripts/node_modules.sh" ${deps.nodejs-patched} + ''; +} diff --git a/nix/mobile/ios/shells/status-go.nix b/nix/mobile/ios/shells/status-go.nix new file mode 100644 index 00000000000..1fd1257b32c --- /dev/null +++ b/nix/mobile/ios/shells/status-go.nix @@ -0,0 +1,35 @@ +# +# This prepares status-go build file called Statusgo.xcframework for XCode. +# It copies the status-go build result to 'modules/react-native-status/ios/RCTStatus'. +# + +{ lib, mkShell, status-go }: + +let + targets = let + targetsString = lib.getEnvWithDefault "IOS_STATUS_GO_TARGETS" "ios/arm64;iossimulator/amd64"; + in lib.splitString ";" targetsString; +in + mkShell { + shellHook = '' + export STATUS_GO_IOS_LIBDIR=${status-go { inherit targets; } }/Statusgo.xcframework + + RCTSTATUS_DIR="$STATUS_MOBILE_HOME/modules/react-native-status/ios/RCTStatus" + targetBasename='Statusgo.xcframework' + + # Compare target folder with source to see if copying is required + if [ -d "$RCTSTATUS_DIR/$targetBasename" ] && [ -d $STATUS_MOBILE_HOME/ios/Pods/ ] && \ + diff -qr --no-dereference $RCTSTATUS_DIR/$targetBasename/ $STATUS_GO_IOS_LIBDIR/ > /dev/null; then + echo "$RCTSTATUS_DIR/$targetBasename already in place" + else + sourceBasename="$(basename $STATUS_GO_IOS_LIBDIR)" + echo "Copying $sourceBasename from Nix store to $RCTSTATUS_DIR" + rm -rf "$RCTSTATUS_DIR/$targetBasename/" + cp -a $STATUS_GO_IOS_LIBDIR $RCTSTATUS_DIR + chmod -R 755 "$RCTSTATUS_DIR/$targetBasename" + if [ "$sourceBasename" != "$targetBasename" ]; then + mv "$RCTSTATUS_DIR/$sourceBasename" "$RCTSTATUS_DIR/$targetBasename" + fi + fi + ''; +} diff --git a/nix/mobile/jsbundle/default.nix b/nix/mobile/jsbundle/default.nix new file mode 100644 index 00000000000..ee361976710 --- /dev/null +++ b/nix/mobile/jsbundle/default.nix @@ -0,0 +1,85 @@ +# +# This Nix expression builds the js files for the current repository given a node modules Nix expression +# + +{ stdenv, lib, deps, pkgs }: + +# Path to the file containing secret environment variables +{ secretsFile ? "" }: + +stdenv.mkDerivation { + name = "status-mobile-jsbundle"; + src = + let path = ./../../..; + # We use builtins.path so that we can name the resulting derivation, + # otherwise the name would be taken from the checkout directory, + # which is outside of our control inherit path; + in builtins.path { + inherit path; + name = "status-mobile-source-jsbundle"; + filter = + # Keep this filter as restrictive as possible in order to avoid + # unnecessary rebuilds and limit closure size + lib.mkFilter { + root = path; + ignoreVCS = false; + include = [ + "VERSION" "BUILD_NUMBER" "scripts/version/.*" + "src/.*" "shadow-cljs.edn" + # I want to avoid including the whole .git directory + ".git/HEAD" ".git/objects" ".git/refs/heads/.*" + # shadow-cljs expects these for deps resolution + "package.json" "yarn.lock" + # build stat's images to check if they exist + "resources/.*" "translations/.*" + ]; + exclude = [ + "resources/fonts/.*" + ]; + }; + }; + buildInputs = with pkgs; [ clojure nodejs bash git openjdk_headless ]; + phases = [ + "unpackPhase" "secretsPhase" "patchPhase" + "configurePhase" "buildPhase" "installPhase" + ]; + + # For optional POKT_TOKEN variable + secretsPhase = if (secretsFile != "") then '' + source "${secretsFile}" + ${lib.checkEnvVarSet "POKT_TOKEN"} + '' else '' + echo "No secrets provided!" + ''; + + # Patching shadow-cljs.edn so it uses the local maven repo of dependencies provided by Nix + patchPhase = + let anchor = ''{:source-paths ["src" "test/cljs"]''; + in '' + substituteInPlace shadow-cljs.edn \ + --replace-quiet '${anchor}' \ + '${anchor} + :maven {:local-repo "${deps.clojure}"}' + ''; + configurePhase = '' + # Symlink Node.js modules to build directory + ln -s ${deps.nodejs}/node_modules + ''; + buildPhase = '' + # Assemble CLASSPATH from available clojure dependencies. + # We prepend 'src' so it can find the local sources and prioritize + # our own namespaces over dependencies, given that indirect dependencies + # can also cause naming conflicts (e.g. prismatic/schema already uses + # namespace schema.core). + export CLASS_PATH="src:$(find ${deps.clojure} \ + -iname '*.jar' | tr '\n' ':')" + + # target must be one of the builds defined in shadow-cljs.edn + java -cp "$CLASS_PATH" clojure.main \ + -m shadow.cljs.devtools.cli release mobile + ''; + installPhase = '' + mkdir -p $out + cp -r result/* $out/ + ''; +} diff --git a/nix/nix.conf b/nix/nix.conf new file mode 100644 index 00000000000..706ed419085 --- /dev/null +++ b/nix/nix.conf @@ -0,0 +1,17 @@ +# NOTE: If you are in Asia you might want to add https://nix-cache-cn.status.im/ to substituters. +substituters = https://nix-cache.status.im/ https://cache.nixos.org/ +trusted-substituters = https://nix-cache.status.im/ https://cache.nixos.org/ +trusted-public-keys = nix-cache.status.im-1:x/93lOfLU+duPplwMSBR+OlY4+mo+dCN7n0mr4oPwgY= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= +# Some downloads are multiple GB, default is 5 minutes +stalled-download-timeout = 3600 +connect-timeout = 10 +max-jobs = auto +# Build packages if fetching from cache fails. +fallback = true +# Helps avoid removing currently used dependencies via garbage collection +keep-derivations = true +keep-outputs = true +# Some builds on MacOS have issue with sandbox so they are disabled with __noChroot. +sandbox = relaxed +# Enable Nix v2 interface. +experimental-features = nix-command diff --git a/nix/overlay.nix b/nix/overlay.nix new file mode 100644 index 00000000000..9c3a8c4d501 --- /dev/null +++ b/nix/overlay.nix @@ -0,0 +1,75 @@ +# Override some packages and utilities in 'pkgs' +# and make them available globally via callPackage. +# +# For more details see: +# - https://nixos.wiki/wiki/Overlays +# - https://nixos.org/nixos/nix-pills/callpackage-design-pattern.html + +self: super: + +let + inherit (super) stdenv stdenvNoCC callPackage; +in { + # Fix for MacOS + mkShell = super.mkShell.override { stdenv = stdenvNoCC; }; + + lib = (super.lib or { }) // (import ./lib { + inherit (super) lib; + }); + + # Project dependencies + deps = { + clojure = callPackage ./deps/clojure { }; + gradle = callPackage ./deps/gradle { }; + nodejs = callPackage ./deps/nodejs { }; + nodejs-patched = callPackage ./deps/nodejs-patched { }; + react-native = callPackage ./deps/react-native { }; + }; + + # Clojure's linter receives frequent upgrades, and we want to take advantage + # of the latest available rules. + clj-kondo = super.clj-kondo.override rec { + buildGraalvmNativeImage = args: super.buildGraalvmNativeImage (args // rec { + inherit (args) pname; + version = "2023.09.07"; + src = super.fetchurl { + url = "https://github.com/clj-kondo/${pname}/releases/download/v${version}/${pname}-${version}-standalone.jar"; + sha256 = "sha256-F7ePdITYKkGB6nsR3EFJ7zLDCUoT0g3i+AAjXzBd624="; + }; + }); + }; + + # Checks fail on darwin. + git-lfs = super.git-lfs.overrideAttrs (old: { + doCheck = false; + }); + + + # Package version adjustments + nodejs = super.nodejs_20; + ruby = super.ruby_3_1; + yarn = super.yarn.override { nodejs = super.nodejs_20; }; + openjdk_headless = super.openjdk17_headless; + xcodeWrapper = callPackage ./pkgs/xcodeenv/compose-xcodewrapper.nix { } { + versions = ["16.0" "16.1" "16.2"]; + }; + go = super.go_1_22; + clang = super.clang_15; + buildGoPackage = super.buildGo122Package; + buildGoModule = super.buildGo122Module; + gomobile = callPackage ./pkgs/gomobile { + #FIXME: No Android SDK packages for aarch64-darwin. + androidPkgs = self.androidEnvCustom.compose; + withAndroidPkgs = stdenv.system != "aarch64-darwin"; + }; + + # Android environment + androidEnvCustom = callPackage ./pkgs/android-sdk { }; + androidPkgs = self.androidEnvCustom.pkgs; + androidShell = self.androidEnvCustom.shell; + + # Custom packages + aapt2 = callPackage ./pkgs/aapt2 { }; + patchMavenSources = callPackage ./pkgs/patch-maven-srcs { }; + goMavenResolver = callPackage ./pkgs/go-maven-resolver { }; +} diff --git a/nix/pkgs.nix b/nix/pkgs.nix new file mode 100644 index 00000000000..5a29d42e06d --- /dev/null +++ b/nix/pkgs.nix @@ -0,0 +1,55 @@ +# This file controls the pinned version of nixpkgs we use for our Nix environment +# as well as which versions of package we use, including their overrides. +{ config ? { } }: + +let + # For testing local version of nixpkgs + #nixpkgsSrc = (import { }).lib.cleanSource "/home/jakubgs/work/nixpkgs"; + + # We follow release 24-05 of nixpkgs + # https://github.com/NixOS/nixpkgs/releases/tag/24.05 + nixpkgsSrc = builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/df27247e6f3e636c119e2610bf12d38b5e98cc79.tar.gz"; + sha256 = "sha256:0bbvimk7xb7akrx106mmsiwf9nzxnssisqmqffla03zz51d0kz2n"; + }; + + # FIXME: remove this additional source when nixpkgs includes gradle 8.8 in stable channel + # We use a commit from the unstable channel of nixpkgs for gradle 8.8 + gradleNixpkgsSrc = builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/48d567fc7b299de90f70a48e4263e31f690ba03e.tar.gz"; + sha256 = "sha256:0zynbk52khdfhg4qfv26h3r5156xff5p0cga2cin7b07i7lqminh"; + }; + + # Status specific configuration defaults + defaultConfig = { + android_sdk.accept_license = true; + allowUnfree = true; + }; + + # Override some packages and utilities + pkgsOverlay = import ./overlay.nix; + + # FIXME: remove this additional source when nixpkgs includes gradle 8.8 in stable channel + gradleOverlay = final: prev: { + gradle = (import gradleNixpkgsSrc { + inherit (prev) system; + config = defaultConfig // config; + }).gradle_8.override { java = prev.openjdk17_headless; }; + }; + + # Fix for lack of Android SDK for M1 Macs. + systemOverride = let + inherit (builtins) currentSystem getEnv; + envSystemOverride = getEnv "NIXPKGS_SYSTEM_OVERRIDE"; + in + if envSystemOverride != "" then + envSystemOverride + else + currentSystem; +in + # import nixpkgs with a config override + (import nixpkgsSrc) { + config = defaultConfig // config; + system = systemOverride; + overlays = [ pkgsOverlay gradleOverlay ]; + } diff --git a/nix/pkgs/aapt2/default.nix b/nix/pkgs/aapt2/default.nix new file mode 100644 index 00000000000..37fc985feb0 --- /dev/null +++ b/nix/pkgs/aapt2/default.nix @@ -0,0 +1,83 @@ +# This is the Android Asset Packaging Tool(AAPT2). +# It is used by Gradle to package Android app resources. +# See: https://developer.android.com/studio/command-line/aapt2 + +{ lib, stdenv, pkgs, fetchurl }: + +let + inherit (lib) getAttr; + inherit (stdenv) isLinux isDarwin; + + pname = "aapt2"; + # Warning: This must be the same as gradlePluginVersion android/gradle.properties + # also referenced in nix/deps/gradle/deps_hack.sh + version = "8.1.1-10154469"; + + pkgPath = "com/android/tools/build/aapt2"; + repoUrl = "https://dl.google.com/dl/android/maven2"; + + platform = + if isLinux then "linux" else + if isDarwin then "osx" else + throw "Unknown platform!"; + + filenames = { + jar = "${pname}-${version}-${platform}.jar"; + pom = "${pname}-${version}.pom"; + }; + + urls = { + jar = fetchurl { + url = "${repoUrl}/${pkgPath}/${version}/${filenames.jar}"; + sha256 = getAttr platform { + linux = "sha256-p54GGvEfAo0yk8euVO7QTu/c3zuityZhyGdhFSV6w+E="; + osx = "sha256-bO4ljdUEfbuns7EyT1FKGLqNGz+0bms5XsplXvzD2T0="; + }; + }; + sha = fetchurl { + url = "${repoUrl}/${pkgPath}/${version}/${filenames.jar}.sha1"; + sha256 = getAttr platform { + linux = "sha256-oUJOQ9j/sJ7Yt0V9WOrRmuqYI+2UWZxeefJ/WyNoWgw="; + osx = "sha256-w+q2j4MKtIOihPMaJxuIP9IwXvRIyBxgRIJLox4E7ac="; + }; + }; + pom = fetchurl { + url = "${repoUrl}/${pkgPath}/${version}/${filenames.pom}"; + sha256 = "sha256-UdUGCoMmEfJxBS2demYKGdG4lzd2DqQbDUCHt/h7Ohg="; + }; + }; + +in stdenv.mkDerivation { + inherit pname version; + + srcs = with urls; [ jar sha pom ]; + # patchelf is Linux specific and won't work on Darwin Platform + phases = if isDarwin then ["unpackPhase"] else ["unpackPhase" "patchPhase" "installPhase"]; + buildInputs = with pkgs; [ zip unzip patchelf ]; + + unpackPhase = '' + mkdir -p $out + for src in $srcs; do + filename=$(stripHash $src) + cp $src $out/$filename + done + tmpDir=$(mktemp -d) + unzip $out/${filenames.jar} -d $tmpDir + ''; + + # On Linux, we need to patch the interpreter in Java packages + # that contain native executables to use Nix's interpreter instead. + patchPhase = '' + # Patch executables from maven dependency to use Nix's interpreter + patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $tmpDir/aapt2 + ''; + + # Rebuild the .jar file with patched binaries + installPhase = '' + pushd $tmpDir > /dev/null + chmod u+w $out/${filenames.jar} + zip -fr $out/${filenames.jar} + chmod $out/${filenames.jar} --reference=$out/${filenames.jar}.sha1 + popd > /dev/null + ''; +} diff --git a/nix/pkgs/android-sdk/compose.nix b/nix/pkgs/android-sdk/compose.nix new file mode 100644 index 00000000000..8233a3f079c --- /dev/null +++ b/nix/pkgs/android-sdk/compose.nix @@ -0,0 +1,27 @@ +# +# This Nix expression centralizes the configuration +# for the Android development environment. +# + +{ androidenv, lib, stdenv }: + +assert lib.assertMsg (stdenv.system != "aarch64-darwin") + "aarch64-darwin not supported for Android SDK. Use: NIXPKGS_SYSTEM_OVERRIDE=x86_64-darwin"; + +# The "android-sdk-license" license is accepted +# by setting android_sdk.accept_license = true. +androidenv.composeAndroidPackages { + cmdLineToolsVersion = "9.0"; + toolsVersion = "26.1.1"; + platformToolsVersion = "33.0.3"; + buildToolsVersions = [ "34.0.0" ]; + platformVersions = [ "34" ]; + # This version must match cmake version inside android/app/build.gradle + cmakeVersions = [ "3.22.1" ]; + ndkVersion = "25.2.9519653"; + includeNDK = true; + includeExtras = [ + "extras;android;m2repository" + "extras;google;m2repository" + ]; +} diff --git a/nix/pkgs/android-sdk/default.nix b/nix/pkgs/android-sdk/default.nix new file mode 100644 index 00000000000..f3f795251df --- /dev/null +++ b/nix/pkgs/android-sdk/default.nix @@ -0,0 +1,14 @@ +# +# This Nix expression centralizes the configuration +# for the Android development environment. +# + +{ callPackage }: + +let + compose = callPackage ./compose.nix { }; + pkgs = callPackage ./pkgs.nix { inherit compose; }; + shell = callPackage ./shell.nix { androidPkgs = pkgs; }; +in { + inherit compose pkgs shell; +} diff --git a/nix/pkgs/android-sdk/pkgs.nix b/nix/pkgs/android-sdk/pkgs.nix new file mode 100644 index 00000000000..645987b3a83 --- /dev/null +++ b/nix/pkgs/android-sdk/pkgs.nix @@ -0,0 +1,17 @@ +{ stdenv, compose }: + +# +# This derivation simply symlinks some stuff to get +# shorter paths as libexec/android-sdk is quite the mouthful. +# With this you can just do `androidPkgs.sdk` and `androidPkgs.ndk`. +# +stdenv.mkDerivation { + name = "${compose.androidsdk.name}-mod"; + phases = [ "symlinkPhase" ]; + outputs = [ "out" "sdk" "ndk" ]; + symlinkPhase = '' + ln -s ${compose.androidsdk} $out + ln -s ${compose.androidsdk}/libexec/android-sdk $sdk + ln -s ${compose.androidsdk}/libexec/android-sdk/ndk-bundle $ndk + ''; +} diff --git a/nix/pkgs/android-sdk/shell.nix b/nix/pkgs/android-sdk/shell.nix new file mode 100644 index 00000000000..3f1a15ab835 --- /dev/null +++ b/nix/pkgs/android-sdk/shell.nix @@ -0,0 +1,18 @@ +{ mkShell, openjdk_headless, androidPkgs }: + +mkShell { + name = "android-sdk-shell"; + buildInputs = [ openjdk_headless ]; + + shellHook = '' + export ANDROID_HOME="${androidPkgs.sdk}" + export ANDROID_NDK_ROOT="${androidPkgs.ndk}" + export ANDROID_SDK_ROOT="$ANDROID_HOME" + + export PATH="$ANDROID_NDK_ROOT:$PATH" + export PATH="$ANDROID_SDK_ROOT/tools:$PATH" + export PATH="$ANDROID_SDK_ROOT/tools/bin:$PATH" + export PATH="$(echo $ANDROID_SDK_ROOT/cmdline-tools/*/bin):$PATH" + export PATH="$ANDROID_SDK_ROOT/platform-tools:$PATH" + ''; +} diff --git a/nix/pkgs/go-maven-resolver/default.nix b/nix/pkgs/go-maven-resolver/default.nix new file mode 100644 index 00000000000..eed76185763 --- /dev/null +++ b/nix/pkgs/go-maven-resolver/default.nix @@ -0,0 +1,16 @@ +{ buildGoModule, fetchFromGitHub }: + +buildGoModule rec { + pname = "go-maven-resolver"; + version = "v1.1.2"; + + vendorHash = "sha256-dlqI+onfeo4tTwmHeq8heVKRzLU1gFEQ+4iv+8egN90="; + + src = fetchFromGitHub rec { + name = "${repo}-${version}-source"; + owner = "status-im"; + repo = pname; + rev = version; + sha256 = "0p3qz5w8spzdxs70m1sdfwi0ajv4ciw3f7fxligf45vj2fwp5dab"; + }; +} diff --git a/nix/pkgs/gomobile/default.nix b/nix/pkgs/gomobile/default.nix new file mode 100644 index 00000000000..21c6f5fd081 --- /dev/null +++ b/nix/pkgs/gomobile/default.nix @@ -0,0 +1,85 @@ +{ + stdenv, + lib, + fetchgit, + fetchurl, + buildGo122Module, + zlib, + makeWrapper, + xcodeenv, + androidenv, + xcodeWrapperArgs ? { }, + xcodeWrapper ? xcodeenv.composeXcodeWrapper xcodeWrapperArgs, + withAndroidPkgs ? stdenv.system != "aarch64-darwin", + androidPkgs ? ( + androidenv.composeAndroidPackages { + includeNDK = true; + } + ), +}: +buildGo122Module { + pname = "gomobile"; + version = "0-unstable-2024-12-13"; + + src = fetchgit { + name = "gomobile"; + url = "https://go.googlesource.com/mobile"; + rev = "a87c1cf6cf463f0d4476cfe0fcf67c2953d76e7c"; + hash = "sha256-7j4rdmCZMC8tn4vAsC9x/mMNkom/+Tl7uAY+5gkSvfY="; + }; + + vendorHash = "sha256-6ycxEDEE0/i6Lxo0gb8wq3U2U7Q49AJj+PdzSl57wwI="; + + CGO_ENABLED = "1"; + + subPackages = [ + "bind" + "cmd/gobind" + "cmd/gomobile" + ]; + + # Fails with: go: cannot find GOROOT directory + doCheck = false; + + nativeBuildInputs = [ makeWrapper ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ xcodeWrapper ]; + + # Prevent a non-deterministic temporary directory from polluting the resulting object files + postPatch = '' + substituteInPlace cmd/gomobile/env.go --replace-quiet \ + 'tmpdir, err = ioutil.TempDir("", "gomobile-work-")' \ + 'tmpdir = filepath.Join(os.Getenv("NIX_BUILD_TOP"), "gomobile-work")' + substituteInPlace cmd/gomobile/init.go --replace-quiet \ + 'tmpdir, err = ioutil.TempDir(gomobilepath, "work-")' \ + 'tmpdir = filepath.Join(os.Getenv("NIX_BUILD_TOP"), "work")' + + # To fix unable to import bind: no Go package in golang.org/x/mobile/bind + substituteInPlace cmd/gomobile/init.go --replace \ + 'golang.org/x/mobile/cmd/gobind@latest' \ + 'golang.org/x/mobile/cmd/gobind' + ''; + + # Necessary for GOPATH when using gomobile. + postInstall = '' + mkdir -p $out/src/golang.org/x + ln -s $src $out/src/golang.org/x/mobile + ''; + + postFixup = '' + for prog in gomobile gobind; do + wrapProgram $out/bin/$prog \ + --suffix GOPATH : $out \ + --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ zlib ]}" \ + ${lib.optionalString withAndroidPkgs '' + --prefix PATH : "${androidPkgs.androidsdk}/bin" \ + --set-default ANDROID_HOME "${androidPkgs.androidsdk}/libexec/android-sdk" + ''} + done + ''; + + meta = { + description = "Tool for building and running mobile apps written in Go"; + homepage = "https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile"; + license = with lib.licenses; [ bsd3 ]; + maintainers = with lib.maintainers; [ jakubgs ]; + }; +} diff --git a/nix/pkgs/patch-maven-srcs/default.nix b/nix/pkgs/patch-maven-srcs/default.nix new file mode 100644 index 00000000000..06292dbb79f --- /dev/null +++ b/nix/pkgs/patch-maven-srcs/default.nix @@ -0,0 +1,23 @@ +# This script patches build.gradle files in node_modules to use +# our local version of Maven dependencies instead of fetching them. + +{ stdenv, writeScript, runtimeShell }: + +writeScript "patch-maven-srcs" ('' + #!${runtimeShell} + # Source setup.sh for substituteInPlace + source ${stdenv}/setup + + function patchMavenSource() { + grep "$source" $1 > /dev/null && \ + substituteInPlace $1 --replace-quiet "$2" "$3" 2>/dev/null + } + + gradleFile="$1" + + # Some of those find something, some don't, that's fine. + patchMavenSource "$gradleFile" 'mavenCentral()' 'mavenLocal()' + patchMavenSource "$gradleFile" 'google()' 'mavenLocal()' + patchMavenSource "$gradleFile" 'jcenter()' 'mavenLocal()' + patchMavenSource "$gradleFile" 'gradlePluginPortal()' 'mavenLocal()' +'') diff --git a/nix/pkgs/patched-go/default.nix b/nix/pkgs/patched-go/default.nix new file mode 100644 index 00000000000..9818edd0f3f --- /dev/null +++ b/nix/pkgs/patched-go/default.nix @@ -0,0 +1,18 @@ +# Patch the Go compiler so that we can have a say (using a NIX_GOWORKDIR env variable) +# as to the temporary directory it uses for linking, since that directory path ends up +# in the string table and .gnu.version_d ELF header. +# + +{ go }: + +go.overrideDerivation (oldAttrs: { + postPatch = (oldAttrs.postPatch or "") + '' + substituteInPlace "src/cmd/go/internal/work/action.go" --replace-quiet \ + 'tmp, err := ioutil.TempDir(os.Getenv("GOTMPDIR"), "go-build")' \ + 'var err error + tmp := os.Getenv("NIX_GOWORKDIR") + if tmp == "" { + tmp, err = ioutil.TempDir(os.Getenv("GOTMPDIR"), "go-build") + }' + ''; +}) diff --git a/nix/pkgs/xcodeenv/compose-xcodewrapper.nix b/nix/pkgs/xcodeenv/compose-xcodewrapper.nix new file mode 100644 index 00000000000..1c89e79f581 --- /dev/null +++ b/nix/pkgs/xcodeenv/compose-xcodewrapper.nix @@ -0,0 +1,50 @@ +{ stdenv, lib, writeShellScriptBin }: +{ versions ? [ "15.1" ] +, xcodeBaseDir ? "/Applications/Xcode.app" }: + +assert stdenv.isDarwin; + +let + xcodebuildPath = "${xcodeBaseDir}/Contents/Developer/usr/bin/xcodebuild"; + + xcodebuildWrapper = writeShellScriptBin "xcodebuild" '' + currentVer="$(${xcodebuildPath} -version | awk 'NR==1{print $2}')" + wrapperVers=(${lib.concatStringsSep " " versions}) + + for ver in "''${wrapperVers[@]}"; do + if [[ "$currentVer" == "$ver" ]]; then + # here exec replaces the shell without creating a new process + # https://www.gnu.org/software/bash/manual/bash.html#index-exec + exec "${xcodebuildPath}" "$@" + fi + done + + echo "The installed Xcode version ($currentVer) does not match any of the allowed versions: ${lib.concatStringsSep ", " versions}" + echo "Please update your local Xcode installation to match one of the allowed versions" + exit 1 + ''; +in +stdenv.mkDerivation { + pname = "xcode-wrapper-plus"; + version = lib.concatStringsSep "," versions; + # Fails in sandbox. Use `--option sandbox relaxed` or `--option sandbox false`. + __noChroot = true; + buildCommand = '' + mkdir -p $out/bin + cd $out/bin + ln -s "${xcodebuildWrapper}/bin/xcode-select" + ln -s /usr/bin/security + ln -s /usr/bin/codesign + ln -s /usr/bin/xcrun + ln -s /usr/bin/plutil + ln -s /usr/bin/clang + ln -s /usr/bin/lipo + ln -s /usr/bin/file + ln -s /usr/bin/rev + ln -s "${xcodebuildWrapper}/bin/xcodebuild" + ln -s "${xcodeBaseDir}/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator" + + cd .. + ln -s "${xcodeBaseDir}/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs" + ''; +} diff --git a/nix/scripts/build.sh b/nix/scripts/build.sh new file mode 100755 index 00000000000..d7ad408abbe --- /dev/null +++ b/nix/scripts/build.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# This script is a wrapper around nix-build with some niceties. +set -e + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +resultPath="${GIT_ROOT}/result/" +source "${GIT_ROOT}/scripts/colors.sh" +source "${GIT_ROOT}/nix/scripts/source.sh" + +# cleanup for artifacts created during builds +cleanup() { + # clear trapped signals + trap - EXIT ERR INT QUIT + # do the actual cleanup, ignore failure + if ${GIT_ROOT}/nix/scripts/clean.sh "${nixResultPath}"; then + echo -e "${GRN}Successful cleanup!${RST}" + elif [[ -n "${JENKINS_URL}" ]]; then + # in CI removing some paths can fail due to parallel builds + echo -e "${YLW}Ignoring cleanup failure in CI.${RST}" + else + echo -e "${RED}Failed cleanup!${RST}" + exit 1 + fi +} + +# If you want to clean after every build set _NIX_CLEAN=true +if [[ -n "${_NIX_CLEAN}" ]]; then + trap cleanup EXIT ERR INT QUIT +fi + +# build output will end up under /nix, we have to extract it +extractResults() { + local nixResultPath="$1" + mkdir -p "${resultPath}" + cp -vfr ${nixResultPath}/* "${resultPath}" | sed 's#'${PWD}'#.#' + chmod -R u+w "${resultPath}" +} + +TARGET="${1}" +shift + +if [[ -z "${TARGET}" ]]; then + echo -e "${RED}First argument is mandatory and has to specify the Nix attribute!${RST}" + exit 1 +fi + +# Hack fix for missing Android SDK for aarch64 on Darwin. See systemOverride in `nix/pkgs.nix`. +if [[ "${TARGET}" =~ ^(targets.status-go.mobile.android|targets.mobile.android.build)$ ]]; then + os=$(uname -s | tr '[:upper:]' '[:lower:]') + export NIXPKGS_SYSTEM_OVERRIDE="x86_64-${os}" +fi + +# Some defaults flags, --pure could be optional in the future. +# NOTE: The --keep-failed flag can be used for debugging issues. +nixOpts=( + "--pure" + "--fallback" + "--no-out-link" + "--show-trace" + "--attr" "${TARGET}" +) + +# Save derivation from being garbage collected +"${GIT_ROOT}/nix/scripts/gcroots.sh" "${TARGET}" "${@}" + +# Run the actual build +echo -e "${GRN}Running:${RST} ${BLD}nix-build "${nixOpts[@]}" ${@}${RST}" +nixResultPath=$(nix-build "${nixOpts[@]}" "${@}" default.nix) + +echo -e "\n${YLW}Extracting result${RST}: ${BLD}${nixResultPath}${RST}" + +extractResults "${nixResultPath}" + +echo -e "\n${GRN}SUCCESS${RST}" diff --git a/nix/scripts/clean.sh b/nix/scripts/clean.sh new file mode 100755 index 00000000000..d46c2981738 --- /dev/null +++ b/nix/scripts/clean.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -e + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/nix/scripts/source.sh" + +log() { echo "$@" 1>&2; } + +# helpers for getting related paths in Nix store +getSources() { nix-store --query --binding src "${1}"; } +getOutputs() { nix-store --query --outputs "${1}"; } +getDrvFiles() { nix-store --query --deriver "${1}"; } +getReferrers() { nix-store --query --referrers "${1}"; } +getRoots() { nix-store --query --roots "${1}"; } + +findRelated() { + path="${1}" + found+=("${path}") + if [[ "${path}" =~ .*.chroot ]]; then + log " ! Chroot: ${path}" + return + elif [[ "${path}" =~ .*.lock ]]; then + log " ! Lock: ${path}" + return + elif [[ "${path}" =~ .*status-mobile-shell.drv ]]; then + echo -n "${path}" + return + fi + log " ? Checking: ${path}" + drv=$(getDrvFiles "${path}") + # if drv is unknown-deriver then path is a source + if [[ "${drv}" == "unknown-deriver" ]]; then + drv=$(getReferrers "${path}" | head -n1) + src="${path}" + elif [[ -f "${drv}" ]]; then + src=$(getSources "${drv}") + fi + # empty paths means this is a source + if [[ -z "${drv}" ]]; then + echo -n "${src}" + return + fi + if [[ $(getRoots "${drv}" | wc -l) -eq 0 ]]; then + log " - Derivation: ${drv}" + log " - Source: ${src}" + found+=("${drv}" "${src}") + fi + + printf '%s\n' "${found[@]}" +} + +# used to find things to delete based on a regex +findByRegex() { + regex="${1}" + + log "Searching by regex: '${regex}'" + # search for matching entries in the store + drvPaths=$( + nix-store --gc --print-dead 2> /dev/null | grep -E "${regex}" + ) + + # list of store entries to delete + declare -a found + + # for each entry find the source and derivation + for mainPath in ${drvPaths}; do + findRelated "${mainPath}" + done +} + +# used to find things to delete based on a given path +findByResult() { + mainPath="${1}" + log "Searching by result: '${mainPath}'" + + # list of store entries to delete + declare -a found + + findRelated "${mainPath}" +} + +log "Cleanup of /nix/store..." + +# This is an optional CLI argument +nixResultPath="${1}" +if [[ -n "${nixResultPath}" ]]; then + # if provided we can narrow down what to clean based on result path + toDelete=$(findByResult "${nixResultPath}") +else + # use regular expression that should match all status-mobile build artifacts + toDelete=$(findByRegex '.*-status-(mobile|go)-(shell|source|build|patched-npm-gradle-modules).*') +fi + +# remove duplicates and return +toDelete=$(printf '%s\n' "${toDelete[@]}" | sort | uniq) + +log "Deleting..." +nix-store --delete ${toDelete[@]} diff --git a/nix/scripts/gcroots.sh b/nix/scripts/gcroots.sh new file mode 100755 index 00000000000..78727262d72 --- /dev/null +++ b/nix/scripts/gcroots.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -Ee + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/nix/scripts/source.sh" +source "${GIT_ROOT}/scripts/colors.sh" + +_NIX_GCROOTS="${_NIX_GCROOTS:-${GIT_ROOT}/.nix-gcroots}" + +TARGET="${1}" +shift +if [[ -z "${TARGET}" ]]; then + echo -e "${RED}No target specified for gcroots.sh!${RST}" >&2 + exit 1 +fi + +# Creates a symlink to derivation in _NIX_GCROOTS directory. +# This prevents it from being removed by 'gc-collect-garbage'. +nix-instantiate --attr "${TARGET}" \ + --add-root "${_NIX_GCROOTS}/${TARGET}" \ + "${@}" "${GIT_ROOT}/default.nix" >/dev/null diff --git a/nix/scripts/kill_proc_prompt.sh b/nix/scripts/kill_proc_prompt.sh new file mode 100644 index 00000000000..ae1a8d19a36 --- /dev/null +++ b/nix/scripts/kill_proc_prompt.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +# script to prompt user for terminating a process + +pid="${1}" + +read -p "Do you want to terminate this process? (y/n): " choice +if [[ "${choice}" == "y" ]]; then + sudo kill "${pid}" + echo "Process ${pid} terminated." +else + echo "Process not terminated. Please close it manually and retry." + exit 1 +fi diff --git a/nix/scripts/lib.sh b/nix/scripts/lib.sh new file mode 100755 index 00000000000..89993ade0ab --- /dev/null +++ b/nix/scripts/lib.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -eo pipefail +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) + +# Checking group ownership to identify installation type. +file_group() { + UNAME=$(uname -s) + if [[ "${UNAME}" == "Linux" ]]; then + stat -Lc "%G" "${1}" 2>/dev/null + elif [[ "${UNAME}" == "Darwin" ]]; then + # Avoid using Nix GNU stat when in Nix shell. + /usr/bin/stat -Lf "%Sg" "${1}" 2>/dev/null + fi +} + +os_name() { + source /etc/os-release 2>/dev/null + echo "${NAME}" +} + +is_arch_linux() { + [[ -f /etc/arch-release ]] +} + +nix_install_type() { + NIX_STORE_DIR_GROUP=$(file_group /nix/store) + if [[ "$(os_name)" =~ NixOS ]]; then + echo "nixos" + else + USER=$(id -un) # Missing in Docker. + case "${NIX_STORE_DIR_GROUP}" in + "nixbld") echo "multi";; + "30000") echo "multi";; + "(30000)") echo "multi";; + "wheel") echo "single";; + "users") echo "single";; + "${USER}") echo "single";; + "${UID}") echo "single";; + "(${UID})") echo "single";; + "") echo "none"; + echo "No Nix installation detected!" >&2;; + *) echo "Unknown Nix installation type!" >&2; exit 1;; + esac + fi +} + +nix_root() { + NIX_ROOT="/nix" + if [[ $(uname -s) == "Darwin" ]]; then + # Special case due to read-only root on MacOS Catalina + NIX_ROOT="/opt/nix" + fi + echo "${NIX_ROOT}" +} + +nix_current_version() { + nix-env --version | awk '{print $3}' +} + +nix_get_local_setting() { + local NIX_LOCAL_CONFIG="${GIT_ROOT}/nix/nix.conf" + local KEY="${1}" + awk -F' = ' "/^${KEY} *=/{print \$2}" nix/nix.conf +} + +nix_set_global_setting() { + local NIX_GLOBAL_CONFIG="/etc/nix/nix.conf" + local KEY="${1}" + local VAL="${2}" + if grep "${KEY}" "${NIX_GLOBAL_CONFIG}" 2>/dev/null; then + sed -i "s/${KEY} = \(.*\)$/${KEY} = ${VAL}/" "${NIX_GLOBAL_CONFIG}" + else + echo "${KEY} = ${VAL}" | sudo tee -a "${NIX_GLOBAL_CONFIG}" >/dev/null + fi +} + +nix_daemon_restart() { + # Restarting Nix Daemon makes sense only on a multi-user install. + [[ $(nix_install_type) != "multi" ]] && return + if [[ "$(uname -s)" == "Darwin" ]]; then + echo "Restarting Nix daemon Launchd service..." >&2 + sudo launchctl unload /Library/LaunchDaemons/org.nixos.nix-daemon.plist + sudo launchctl load /Library/LaunchDaemons/org.nixos.nix-daemon.plist + elif [[ "$(uname -s)" == "Linux" ]] && [[ "$(nix_install_type)" == "multi" ]]; then + echo "Restarting Nix daemon Systemd service..." >&2 + sudo systemctl daemon-reload + sudo systemctl restart nix-daemon + else + echo "Unknown platform! Unable to restart daemon!" >&2 + exit 1 + fi +} diff --git a/nix/scripts/node_modules.sh b/nix/scripts/node_modules.sh new file mode 100755 index 00000000000..48eaf5b24c7 --- /dev/null +++ b/nix/scripts/node_modules.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash + +# +# Check if we need to copy node_modules. +# (e.g. in case it has been modified after last copy) +# +# The reasoning for a (mostly) read-only node_modules folder: +# ideally we’d symlink the folder directly to the Nix store +# so that we’re guaranteed to have a reproducible source. +# Unfortunately react-native wants to build some stuff after the fact +# and this is incompatible with the concept of a pure Nix package. +# Therefore we copy the whole source to the repo directory, +# allow writing only on the folders where it is absolutely required, +# and therefore we still keep some peace of mind that the rest +# of node_modules is unchanged the rest of the time. +# + +set -Eeuo pipefail + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" + +# More concise output from 'time' +export TIMEFORMAT="Done in: %Es" + +removeDir() { + [[ ! -d "${tmp}" ]] && return + chmod -R u+w "${tmp}" + rm -rf "${tmp}" +} + +copyNodeModules() { + local src="${1}" + local dst="${2}" + # WARNING: The ../ is there to avoid a Nix builtins.path bug: + # https://github.com/NixOS/nix/issues/3593 + local tmp=$(mktemp -d -p "$(dirname "${dst}")/../") + + # We use a temporary directory to use mv as "atomic" change + trap "removeDir ${tmp}" ERR INT HUP + + # WARNING: The -L here is crucial to let Metro find modules. + cp -LRf ${src}/node_modules/. "${tmp}" + chmod -R +w "${tmp}" + + # WARNING: We can't de-reference .bin symlinks + cp -Rf ${src}/node_modules/.bin/. "${tmp}/.bin/" + # Cmake called by react-native-reanimated 3.3.0 during build creates read-only temporary files. + chmod -R u+w node_modules/react-native-reanimated/android/ -f || true + rm -r "${dst}" + mv -f "${tmp}" "${dst}" +} + +# Find files that were modified and should cause a re-copying of node modules. +# Some files are generated/modified by build processes and should be ignored. +findFilesNewerThan() { + local sentinel="${1}" + local dir="${2}" + find "${dir}" -type f -writable \ + -newer "${sentinel}" \ + -not -ipath "*/*android/build/*" -prune \ + -not -ipath "*/xcuserdata/*" -prune \ + -not -ipath "*/scripts/.packager.env" \ + \ # generated at runtime by react-native \ + \ # related code https://github.com/facebook/react-native/blob/v0.72.5/packages/react-native/ReactCommon/React-rncore.podspec#L19 \ + -not -path "*/node_modules/react-native/ReactCommon/react/renderer/components/rncore/*" \ + \ # generated at runtime by react-native-config \ + \ # related code https://github.com/lugg/react-native-config/blob/v1.5.0/react-native-config.podspec#L52 \ + -not -path "*/node_modules/react-native-config/ios/ReactNativeConfig/GeneratedDotEnv.m" \ + \ # generated at runtime by react-native from v0.73.x onwards \ + -not -path "*/node_modules/react-native/React/Fabric/RCTThirdPartyFabricComponentsProvider.mm" \ + -print +} + +nodeModulesUnchanged() { + local src="$1" + local dst="$2" + local sentinelFile="${dst}/.copied~" + + # Check if node_modules exists and is valid + if [[ ! -f "${sentinelFile}" ]]; then + # node_modules have not been created by this script + echo -e "${YLW}Node modules not created by Nix${RST}" >&2 + return 1 + fi + + # Sentinel file holds location of the node_modules source in Nix store + currentNixSrc="$(cat "${sentinelFile}")" + + if [ "${currentNixSrc}" != "${src}" ]; then + echo -e "${YLW}Yarn modules changed, copying new version over${RST}" >&2 + return 1 + fi + + # Some build processes modify files in node_modules + modifiedFiles=($(findFilesNewerThan "${sentinelFile}" "${dst}")) + if [ ${#modifiedFiles[@]} -ne 0 ]; then + echo -e "${YLW}Changes detected in node_modules:${RST} ${#modifiedFiles[@]}" >&2 + # Print files that have changes + for file in ${modifiedFiles[@]}; do + echo "- $(realpath --relative-to="${dst}" "${file}")" >&2 + done + return 1 + fi + + echo -e "${GRN}No changes detected.${RST}" >&2 + return 0 +} + +replaceNodeModules() { + local src="$1" + local dst="$2" + local sentinelFile="${dst}/.copied~" + + if nodeModulesUnchanged "${src}" "${dst}"; then + return + fi + + # Replace node_modules if necessary + echo "Copying node_modules from Nix store:" >&2 + echo " - ${src}" >&2 + copyNodeModules "${src}" "${dst}" + echo -n "${src}" > "${sentinelFile}" +} + +# Destination folder, Nix sets STATUS_MOBILE_HOME +dst="$STATUS_MOBILE_HOME/node_modules" +# Source of Node modules from /nix/store +src="$1" + +if [[ ! -d ${src} ]]; then + echo -e "${RED}No such folder:${RST} ${src}" >&2 + exit 1 +fi + +# Make those available in shell spawned by flock +export -f replaceNodeModules nodeModulesUnchanged copyNodeModules findFilesNewerThan removeDir + +mkdir -p "${dst}" +# Leverage file lock to create an exclusive lock. +# Otherwise multiple calls to this script would clash. +flock "${dst}/" sh -c "time replaceNodeModules '${src}' '${dst}'" diff --git a/nix/scripts/purge.sh b/nix/scripts/purge.sh new file mode 100755 index 00000000000..efd81f3da45 --- /dev/null +++ b/nix/scripts/purge.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# This script removes all Nix files. + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/nix/scripts/lib.sh" +source "${GIT_ROOT}/scripts/colors.sh" + +nix_purge_linux_multi_user_service() { + NIX_SERVICES=(nix-daemon.service nix-daemon.socket) + for NIX_SERVICE in "${NIX_SERVICES[@]}"; do + sudo systemctl stop "${NIX_SERVICE}" + sudo systemctl disable "${NIX_SERVICE}" + done + sudo systemctl daemon-reload +} + +nix_purge_linux_multi_user_users() { + for NIX_USER in $(awk -F: '/nixbld/{print $1}' /etc/passwd); do + sudo userdel "${NIX_USER}" + done + sudo groupdel nixbld +} + +nix_purge_darwin_multi_user_service() { + sudo launchctl unload /Library/LaunchDaemons/org.nixos.nix-daemon.plist + sudo rm /Library/LaunchDaemons/org.nixos.nix-daemon.plist + sudo launchctl unload /Library/LaunchDaemons/org.nixos.darwin-store.plist + sudo rm /Library/LaunchDaemons/org.nixos.darwin-store.plist +} + +nix_purge_darwin_multi_user_users() { + for NIX_USER in $(dscl . list /Users | grep nixbld); do + sudo dscl . -delete "/Users/${NIX_USER}" + done + sudo dscl . -delete /Groups/nixbld +} + +# This still leaves an empty /nix, which will disappear after reboot. +nix_purge_darwin_multi_user_volumes() { + sudo sed -i.bkp '/nix/d' /etc/synthetic.conf + sudo sed -i.bkp '/nix/d' /etc/fstab + + # Attempt to delete the volume + if ! sudo diskutil apfs deleteVolume /nix; then + echo "Failed to unmount /nix because it is in use." + + # Identify the process using the volume + local pid=$(lsof +D /nix | awk 'NR==2{print $2}') + if [[ -n "$pid" ]]; then + echo "The volume /nix is in use by process ID $pid." + "${GIT_ROOT}/nix/scripts/kill_proc_prompt.sh" "${pid}" || return 1 + + else + echo "No process found using /nix. Manual intervention required." + return 1 + fi + fi + + echo -e "${YLW}You will need to reboot your system!${RST}" >&2 +} + +nix_purge_multi_user() { + if [[ $(uname -s) == "Darwin" ]]; then + nix_purge_darwin_multi_user_service + nix_purge_darwin_multi_user_users + nix_purge_darwin_multi_user_volumes + else + nix_purge_linux_multi_user_service + nix_purge_linux_multi_user_users + fi + + sudo rm -fr /etc/nix + sudo rm -f /etc/profile.d/nix.sh* + + # Restore old shell profiles + NIX_PROFILE_FILES=( + /etc/bash.bashrc /etc/bashrc /etc/bash/bashrc + /etc/zsh.zshhrc /etc/zshrc /etc/zsh/zshrc + ) + for NIX_FILE in "${NIX_PROFILE_FILES[@]}"; do + if [[ -f "${NIX_FILE}.backup-before-nix" ]]; then + sudo mv -f "${NIX_FILE}.backup-before-nix" "${NIX_FILE}" + fi + done +} + +nix_purge_user_profile() { + sudo rm -rf \ + ~/.nix-* \ + ~/.cache/nix \ + ~/.config/nixpkgs \ + "${GIT_ROOT}/.nix-gcroots" +} + +nix_purge_root() { + NIX_ROOT=$(nix_root) + if [[ -z "${NIX_ROOT}" ]]; then + echo -e "${RED}Unable to identify Nix root!${RST}" >&2 + exit 1 + fi + sudo rm -fr "${NIX_ROOT}" +} + +# Don't run anything if script is just sourced. +if (return 0 2>/dev/null); then + echo -e "${YLW}Script sourced, not running purge.${RST}" + return +fi + +# Confirm user decision, unless --force is used. +if [[ "${1}" != "--force" ]]; then + echo -e "${YLW}Are you sure you want to purge Nix?${RST}" >&2 + read -p "[y/n]: " -n 1 -r + echo + if [[ $REPLY =~ ^[^Yy]$ ]]; then + echo -e "${GRN}Aborting Nix purge!${RST}" >&2 + exit 0 + fi +fi + +NIX_INSTALL_TYPE=$(nix_install_type) +# Purging /nix on NixOS would be disastrous. +if [[ "${NIX_INSTALL_TYPE}" == "nixos" ]]; then + echo -e "${RED}You should not purge Nix files on NixOS!${RST}" >&2 + exit +elif [[ "${NIX_INSTALL_TYPE}" == "none" ]] && [[ "${1}" != "--force" ]]; then + echo -e "${YLW}Nothing to remove, Nix not installed.${RST}" >&2 + exit +elif [[ "${NIX_INSTALL_TYPE}" == "multi" ]] || [[ "${1}" == "--force" ]]; then + echo -e "${YLW}Detected multi-user Nix installation.${RST}" >&2 + nix_purge_multi_user +elif [[ "${NIX_INSTALL_TYPE}" == "single" ]] || [[ "${1}" == "--force" ]]; then + echo -e "${YLW}Detected single-user Nix installation.${RST}" >&2 + nix_purge_user_profile +fi +nix_purge_root + +echo -e "${GRN}Purged all Nix files from your system.${RST}" >&2 diff --git a/nix/scripts/setup.sh b/nix/scripts/setup.sh new file mode 100755 index 00000000000..00843d6b060 --- /dev/null +++ b/nix/scripts/setup.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# This script installs a specific version of Nix. +set -eo pipefail + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" +source "${GIT_ROOT}/nix/scripts/lib.sh" +source "${GIT_ROOT}/nix/scripts/version.sh" + +nix_install() { + # Download installer and verify SHA256> + curl -sSf "${NIX_INSTALL_URL}" -o "${NIX_INSTALL_PATH}" + echo "${NIX_INSTALL_SHA256} ${NIX_INSTALL_PATH}" | sha256sum -c + chmod +x "${NIX_INSTALL_PATH}" + + # Identify installation type. + if [[ -z "${NIX_INSTALL_OPTS}" ]]; then + if [[ "$(uname -r)" =~ microsoft ]]; then + # Systemd is not started by default on WSL. + NIX_INSTALL_OPTS="--no-daemon" + elif [[ "$(uname -s)" == "Darwin" ]]; then + # Single-user not supported on Darwin. + NIX_INSTALL_OPTS="--daemon" + elif [[ "$(uname -s)" == "Linux" ]]; then + # Open file limit issues on Linux. + # https://github.com/NixOS/nix/issues/6007 + # Alson known issues with nix-daemon.socket on Arch. + NIX_INSTALL_OPTS="--no-daemon" + fi + fi + + # Run the installer + "${NIX_INSTALL_PATH}" "${NIX_INSTALL_OPTS}" + if [[ $? -eq 0 ]]; then + echo -e "${GRN}The Nix package manager was successfully installed.${RST}" + else + echo -e "${RED}Failed to install Nix package manager!${RST}" >&2 + echo "Please see: https://nixos.org/nix/manual/#chap-installation" >&2 + exit 1 + fi + + # Additional fixes + nix_add_extra_cache + nix_daemon_restart +} + +# Adding directly to global config to avoid warnings like this: +# "ignoring untrusted substituter 'https://nix-cache.status.im/', you are not a trusted user." +nix_add_extra_cache() { + # Single-user installations do not have this issue. + [[ ! -f /etc/nix/nix.conf ]] && return + echo -e 'Adding our cache to Nix daemon config...' >&2 + local NIX_SETTINGS=('substituters' 'trusted-substituters' 'trusted-public-keys') + for NIX_SETTING in "${NIX_SETTINGS[@]}"; do + nix_set_global_setting "${NIX_SETTING}" "$(nix_get_local_setting "${NIX_SETTING}")" + done +} + +if [[ ! -x "$(command -v sha256sum)" ]]; then + echo -e "${RED}The 'sha256sum' utility is required for Nix installation.${RST}" >&2 + echo -e "${YLW}Install 'coreutils' package on your system.${RST}" >&2 + exit 1 +fi + +if [[ ! -x "$(command -v curl)" ]]; then + echo -e "${RED}The 'curl' utility is required for Nix installation.${RST}" >&2 + exit 1 +fi + +if [[ "$(source /etc/os-release 2>/dev/null && echo "${NAME}")" == *NixOS* ]]; then + echo -e "${GRN}Already running NixOS.${RST}" + exit +fi + +if [[ -x "$(command -v nix)" ]]; then + echo -e "${GRN}Nix package manager already installed.${RST}" + exit +fi + +if [[ "${IN_NIX_SHELL}" == 'pure' ]]; then + echo -e "${GRN}Already in a pure Nix shell.${RST}" + exit +fi + +# If none of the checks before succeeded we need to install Nix +echo -e "${GRN}Setting up Nix package manager...${RST}" +nix_install +echo -e "${YLW}See STARTING_GUIDE.md if you're new here.${RST}" diff --git a/nix/scripts/shell.sh b/nix/scripts/shell.sh new file mode 100755 index 00000000000..32e3384ca27 --- /dev/null +++ b/nix/scripts/shell.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +# This script is used by the Makefile to have an implicit nix-shell. +# The following environment variables modify the script behavior: +# - TARGET: This attribute is passed via --attr to Nix, defining the scope. +# - _NIX_PURE: This variable allows for making the shell pure with the use of --pure. +# Take note that this makes Nix tools like `nix-build` unavailable in the shell. +# - _NIX_KEEP: This variable allows specifying which env vars to keep for Nix pure shell. + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" +source "${GIT_ROOT}/nix/scripts/source.sh" + +export TERM=xterm # fix for colors +shift # we remove the first -c from arguments + +if [[ -z "${TARGET}" ]]; then + export TARGET="default" + echo -e "${YLW}Missing TARGET, assuming default target.${RST} See nix/README.md for more details." 1>&2 +fi + +# Minimal shell with just Nix sourced, useful for `make nix-gc`. +if [[ "${TARGET}" == "nix" ]]; then + eval $@ + exit 0 +fi +if [[ -n "${IN_NIX_SHELL}" ]] && [[ -n "${NIX_SHELL_TARGET}" ]]; then + if [[ "${NIX_SHELL_TARGET}" == "${TARGET}" ]]; then + echo -e "${YLW}Nix shell for TARGET=${TARGET} is already active.${RST}" >&2 + exit 0 + else + # Nesting nix shells does not work due to how we detect already present shell. + echo -e "${RED}Cannot nest Nix shells with different targets!${RST}" >&2 + exit 1 + fi +fi + +entryPoint="default.nix" +nixArgs=( + "--show-trace" + "--attr shells.${TARGET}" +) + +# This variable allows specifying which env vars to keep for Nix pure shell +# The separator is a colon +if [[ -n "${_NIX_KEEP}" ]]; then + nixArgs+=("--keep ${_NIX_KEEP//,/ --keep }") +fi + +# Not all builds are ready to be run in a pure environment +if [[ -n "${_NIX_PURE}" ]]; then + nixArgs+=("--pure") + pureDesc='pure ' +fi + +# Hack fix for missing Android SDK for aarch64 on Darwin. See systemOverride in `nix/pkgs.nix`. +if [[ "${TARGET}" =~ ^(android-sdk|android|gradle|keytool|status-go)$ ]]; then + os=$(uname -s | tr '[:upper:]' '[:lower:]') + export NIXPKGS_SYSTEM_OVERRIDE="x86_64-${os}" +fi + +echo -e "${GRN}Configuring ${pureDesc}Nix shell for target '${TARGET}'...${RST}" 1>&2 + +# Save derivation from being garbage collected +"${GIT_ROOT}/nix/scripts/gcroots.sh" "shells.${TARGET}" + +# ENTER_NIX_SHELL is the fake command used when `make shell` is run. +# It is just a special string, not a variable, and a marker to not use `--run`. +if [[ "${@}" == "ENTER_NIX_SHELL" ]]; then + export NIX_SHELL_TARGET="${TARGET}" + exec nix-shell ${nixArgs[@]} --keep NIX_SHELL_TARGET ${entryPoint} +else + exec nix-shell ${nixArgs[@]} --run "$@" ${entryPoint} +fi diff --git a/nix/scripts/source.sh b/nix/scripts/source.sh new file mode 100755 index 00000000000..5272bebd97e --- /dev/null +++ b/nix/scripts/source.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# This script makes sure we have Nix tools available +set -e + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/nix/scripts/lib.sh" +source "${GIT_ROOT}/scripts/colors.sh" + +source_nix_profile() { + NIX_INSTALL_TYPE=$(nix_install_type) + if [[ "${NIX_INSTALL_TYPE}" == "multi" ]]; then + source "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" + elif [[ "${NIX_INSTALL_TYPE}" == "single" ]]; then + source "${HOME}/.nix-profile/etc/profile.d/nix.sh" + elif [[ "${NIX_INSTALL_TYPE}" == "nixos" ]]; then + echo "Sourcing profile not necessary on NixOS!" >&2 + fi +} + +main() { + # Just stop if Nix is already available + if [[ -x $(command -v nix) ]]; then + return + fi + + # Setup Nix if not available + if [[ ! -d /nix ]]; then + "${GIT_ROOT}/nix/scripts/setup.sh" + fi + + # Load Nix profile + source_nix_profile + + # Verify Nix is available + if [[ ! -x $(command -v nix) ]]; then + echo -e "${RED}Nix not available, sourcing profile failed!${RST}" >&2 + exit 1 + fi +} + +main diff --git a/nix/scripts/upgrade.sh b/nix/scripts/upgrade.sh new file mode 100755 index 00000000000..4597e81f593 --- /dev/null +++ b/nix/scripts/upgrade.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# This script upgrades Nix to specific version. +# https://nixos.org/manual/nix/stable/installation/upgrading.html +set -eo pipefail + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" +source "${GIT_ROOT}/nix/scripts/lib.sh" +source "${GIT_ROOT}/nix/scripts/source.sh" +source "${GIT_ROOT}/nix/scripts/version.sh" + +nix_upgrade() { + echo -e "Upgrading Nix interpreter to: ${GRN}${NIX_VERSION}${RST}" >&2 + nix-channel --update + nix-env --install --attr "nixpkgs.${NIX_PACKAGE}" "nixpkgs.cacert" + nix_daemon_restart +} + +# Allow for sourcing the script +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + return +fi + +if [[ "$(nix_current_version)" == "${NIX_VERSION}" ]]; then + echo -e "Nix interpreter already on version: ${GRN}${NIX_VERSION}${RST}" + exit 0 +fi + +NIX_INSTALL_TYPE=$(nix_install_type) +if [[ "${NIX_INSTALL_TYPE}" == "nixos" ]]; then + echo -e "${YLW}WARNING:${RST} Upgrade Nix in your NixOS configuration!" >&2 + exit 0 +elif [[ "${NIX_INSTALL_TYPE}" == "single" ]]; then + nix_upgrade +elif [[ "${NIX_INSTALL_TYPE}" == "multi" ]]; then + sudo -i bash -c "source ${PWD}/${0}; nix_upgrade" +fi diff --git a/nix/scripts/version.sh b/nix/scripts/version.sh new file mode 100755 index 00000000000..7929607221f --- /dev/null +++ b/nix/scripts/version.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +export NIX_VERSION="2.24.11" +export NIX_PACKAGE="nixVersions.nix_2_24" +export NIX_INSTALL_URL="https://nixos.org/releases/nix/nix-${NIX_VERSION}/install" +export NIX_INSTALL_SHA256="5c610a0e97c8a2dd48c1a630608502e43fdecd7d2572463dd4166126106d9188" +export NIX_INSTALL_PATH="/tmp/nix-install-${NIX_VERSION}" diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 index 00000000000..e2a06e385ca --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,53 @@ +# +# Defines the default shell that is used when target is not specified. +# It is also merged with all the other shells for a complete set of tools. +# +{ config ? {} +, pkgs ? import ./pkgs.nix { inherit config; } }: + +let + inherit (pkgs) mkShell; +in mkShell { + name = "status-mobile-shell"; # for identifying all shells + + buildInputs = let + appleSDKFrameworks = (with pkgs.darwin.apple_sdk.frameworks; [ + IOKit CoreServices + ]); + in + with pkgs; lib.unique ([ + # core utilities that should always be present in a shell + bash curl wget file unzip flock procps + git gnumake jq ncurses gnugrep parallel + lsof # used in start-react-native.sh + # build specific utilities + clojure maven watchman + # other nice to have stuff + yarn nodejs python310 maestro + # Required for /scripts/compress_image.sh script + imagemagick + ] # and some special cases + ++ lib.optionals stdenv.isDarwin ([ cocoapods clang tcl idb-companion ] ++ appleSDKFrameworks) + ++ lib.optionals (!stdenv.isDarwin) [ gcc8 ] + ); + + # avoid terminal issues + TERM="xterm"; + + # default locale + LANG="en_US.UTF-8"; + LANGUAGE="en_US.UTF-8"; + + # Prevent Yarn from checking packageManager field in parent directories + # https://github.com/yarnpkg/yarn/blob/740c38c3a962c30ddb344a919bbfb7065620714b/src/cli/index.js#L292C21-L292C33 + SKIP_YARN_COREPACK_CHECK="1"; + + # Important to load our own Nix binary cache. + NIX_CONFIG = builtins.readFile ./nix.conf; + + # just a nicety for easy access to node scripts + shellHook = '' + export STATUS_MOBILE_HOME=$(git rev-parse --show-toplevel) + export PATH="$STATUS_MOBILE_HOME/node_modules/.bin:$PATH" + ''; +} diff --git a/nix/shells.nix b/nix/shells.nix new file mode 100644 index 00000000000..202748276f0 --- /dev/null +++ b/nix/shells.nix @@ -0,0 +1,81 @@ +# This file defines custom shells as well as shortcuts +# for accessing more nested shells. +{ config ? {} +, pkgs ? import ./pkgs.nix { inherit config; } }: + +let + inherit (pkgs) lib mkShell callPackage; + + # everything else we define in nix/ dir + targets = callPackage ./targets.nix { inherit config; }; + + # the default shell with most commonly used tools + default = callPackage ./shell.nix { }; + + # An attrset for easier merging with default shell + shells = { + inherit default; + + # for calling clojure targets in CI or Makefile + clojure = mkShell { + buildInputs = with pkgs; [ + clojure flock maven openjdk_headless + # lint specific utilities + babashka clj-kondo clojure-lsp ripgrep zprint + ]; + # CLASSPATH from clojure deps with 'src' appended to find local sources. + shellHook = with pkgs; '' + export CLASS_PATH="$(find ${deps.clojure} -iname '*.jar' | tr '\n' ':')src" + + # Check if node modules changed and if so install them. + "$STATUS_MOBILE_HOME/nix/scripts/node_modules.sh" ${pkgs.deps.nodejs-patched} + ''; + }; + + # for 'make watchman-clean' + watchman = mkShell { + buildInputs = with pkgs; [ watchman ]; + }; + + # for running fastlane commands alone + fastlane = targets.mobile.fastlane.shell; + + # for running gradle by hand + gradle = mkShell { + buildInputs = with pkgs; [ gradle maven goMavenResolver htmlq ]; + shellHook = '' + export STATUS_GO_ANDROID_LIBDIR="DUMMY" + export STATUS_NIX_MAVEN_REPO="${pkgs.deps.gradle}" + export ANDROID_SDK_ROOT="${pkgs.androidPkgs.sdk}" + export ANDROID_NDK_ROOT="${pkgs.androidPkgs.ndk}" + + export STATUS_MOBILE_HOME=$(git rev-parse --show-toplevel) + # WARNING: Unpatched Node.js deps allow Gradle to use remote repos. + "$STATUS_MOBILE_HOME/nix/scripts/node_modules.sh" ${pkgs.deps.nodejs} + function restore_patched_modules() { + "$STATUS_MOBILE_HOME/nix/scripts/node_modules.sh" ${pkgs.deps.nodejs-patched} + } + trap restore_patched_modules EXIT + ''; + }; + + # for 'scripts/generate-keystore.sh' + keytool = mkShell { + buildInputs = with pkgs; [ openjdk_headless apksigner ]; + }; + + # for targets needing 'adb', 'apkanalyzer' and other SDK/NDK tools + android-sdk = pkgs.androidShell; + + # helpers for use with target argument + ios = targets.mobile.ios.shell; + android = targets.mobile.android.shell; + status-go = targets.status-go.mobile.android {}; + }; + + # for merging the default shell with others + mergeDefaultShell = (_: val: lib.mergeSh default [ val ]); + +# values here can be selected using `nix-shell --attr shells.$TARGET default.nix` +# the nix/scripts/shell.sh wrapper does this for us and expects TARGET to be set +in lib.mapAttrs mergeDefaultShell shells diff --git a/nix/status-go/default.nix b/nix/status-go/default.nix new file mode 100644 index 00000000000..d4c2f864dbc --- /dev/null +++ b/nix/status-go/default.nix @@ -0,0 +1,45 @@ +{ lib, callPackage, mkShell }: + +let + inherit (lib) getEnvWithDefault attrValues mapAttrs; + + # Metadata common to all builds of status-go + meta = { + description = "The Status Go module that consumes go-ethereum."; + license = lib.licenses.mpl20; + platforms = with lib.platforms; linux ++ darwin; + }; + + # Source can be changed with a local override from config + source = callPackage ./source.nix { }; + + # Params to be set at build time, important for About section and metrics + goBuildParams = { + GitCommit = source.rev; + Version = source.cleanVersion; + # FIXME: This should be moved to status-go config. + IpfsGatewayURL = getEnvWithDefault "STATUS_GO_IPFS_GATEWAY_URL" "https://ipfs.status.im/"; + }; + + # These are necessary for status-go to show correct version + paramsLdFlags = attrValues (mapAttrs (name: value: + "-X github.com/status-im/status-go/params.${name}=${value}" + ) goBuildParams); + + goBuildLdFlags = paramsLdFlags ++ [ + "-s" # -s disabled symbol table + "-w" # -w disables DWARF debugging information + ]; +in rec { + mobile = callPackage ./mobile { + inherit meta source goBuildLdFlags; + }; + + library = callPackage ./library { + inherit meta source; + }; + + shell = mkShell { + inputsFrom = [ mobile.android mobile.ios ]; + }; +} diff --git a/nix/status-go/library/default.nix b/nix/status-go/library/default.nix new file mode 100644 index 00000000000..a6bd6d22043 --- /dev/null +++ b/nix/status-go/library/default.nix @@ -0,0 +1,47 @@ +{ lib, stdenv, meta, source, buildGoPackage, + go-bindata, mockgen, protoc-gen-go, protobuf3_20 }: + +buildGoPackage { + pname = source.repo; + version = "${source.cleanVersion}-${source.shortRev}"; + + inherit meta; + inherit (source) src goPackagePath; + + nativeBuildInputs = [ + go-bindata mockgen protoc-gen-go protobuf3_20 + ]; + + phases = ["unpackPhase" "configurePhase" "buildPhase"]; + + # https://pkg.go.dev/net#hdr-Name_Resolution + # https://github.com/status-im/status-mobile/issues/19736 + # https://github.com/status-im/status-mobile/issues/19581 + # TODO: try removing when go is upgraded to 1.22 + GODEBUG = "netdns=cgo+2"; + + # Since go 1.21 status-go compiled library includes references to cgo runtime. + # FIXME: Remove this when go 1.23 or later versions fix this madness. + allowGoReference = true; + + preBuild = '' + pushd go/src/$goPackagePath + go run cmd/library/*.go > $NIX_BUILD_TOP/main.go + make generate SHELL=$SHELL GO111MODULE=on GO_GENERATE_CMD='go generate' + popd + ''; + + # Build the Go library + # ld flags and netgo tag are necessary for integration tests to work on MacOS + # https://github.com/status-im/status-mobile/issues/20135 + buildPhase = '' + runHook preBuild + go build \ + -buildmode='c-archive' \ + ${lib.optionalString stdenv.isDarwin "-ldflags=-extldflags=-lresolv"} \ + -tags='gowaku_skip_migrations gowaku_no_rln ${lib.optionalString stdenv.isDarwin "netgo"}' \ + -o "$out/libstatus.a" \ + $NIX_BUILD_TOP/main.go + runHook postBuild + ''; +} diff --git a/nix/status-go/mobile/build.nix b/nix/status-go/mobile/build.nix new file mode 100644 index 00000000000..6174483a56e --- /dev/null +++ b/nix/status-go/mobile/build.nix @@ -0,0 +1,90 @@ +{ callPackage, lib, buildGoPackage, pkgs +, androidPkgs, openjdk_headless, gomobile, xcodeWrapper, removeReferencesTo +, go-bindata, mockgen, protobuf3_20, protoc-gen-go +, meta +, source +, platform ? "android" +, platformVersion ? "23" +, targets ? [ "android/arm64" "android/arm" ] +, goBuildFlags ? [ ] # Use -v or -x for debugging. +, goBuildLdFlags ? [ ] +, outputFileName ? "status-go-${source.shortRev}-${platform}.aar" }: + +let + inherit (lib) concatStringsSep optionalString optional splitString; + isIOS = platform == "ios"; + isAndroid = platform == "android"; + enforceXCodeAvailable = callPackage ./enforceXCodeAvailable.nix { }; + # Fixes fatal: not a git repository (or any of the parent directories): .git + fakeGit = pkgs.writeScriptBin "git" "echo ${source.version}"; + +in buildGoPackage rec { + pname = source.repo; + version = "${source.cleanVersion}-${source.shortRev}-${platform}"; + + inherit meta; + inherit (source) src goPackagePath; + + # Sandbox causes Xcode issues on MacOS. Requires sandbox=relaxed. + # https://github.com/status-im/status-mobile/pull/13912 + __noChroot = isIOS; + + extraSrcPaths = [ gomobile ]; + nativeBuildInputs = [ + gomobile removeReferencesTo go-bindata mockgen protoc-gen-go protobuf3_20 fakeGit + ] ++ optional isAndroid openjdk_headless + ++ optional isIOS xcodeWrapper; + + ldflags = goBuildLdFlags; + + ANDROID_HOME = optionalString isAndroid androidPkgs.sdk; + + # Ensure XCode is present for iOS, instead of failing at the end of the build. + preConfigure = optionalString isIOS enforceXCodeAvailable; + + # https://pkg.go.dev/net#hdr-Name_Resolution + # https://github.com/status-im/status-mobile/issues/19736 + # https://github.com/status-im/status-mobile/issues/19581 + # TODO: try removing when go is upgraded to 1.22 + GODEBUG = "netdns=cgo+2"; + + # Sentry for status-go + SENTRY_CONTEXT_NAME = "status-mobile"; + SENTRY_CONTEXT_VERSION = version; + + preBuild = '' + echo 'Generate static files' + pushd go/src/$goPackagePath + make generate SHELL=$SHELL GO111MODULE=on GO_GENERATE_CMD='go generate' + popd + ''; + + buildPhase = '' + runHook preBuild + echo -e "\nBuilding $pname for: ${concatStringsSep "," targets}" + gomobile bind \ + ${concatStringsSep " " goBuildFlags} \ + -ldflags="$ldflags" \ + -target=${concatStringsSep "," targets} \ + ${optionalString isAndroid "-androidapi=${platformVersion}" } \ + ${optionalString isIOS "-iosversion=${platformVersion}" } \ + -tags='${optionalString isIOS "nowatchdog"} gowaku_skip_migrations gowaku_no_rln' \ + -o ${outputFileName} \ + ${source.goPackagePath}/mobile + + runHook postBuild + ''; + + installPhase = '' + mkdir -p $out + cp -r ${outputFileName} $out/ + ''; + + # Drop govers from disallowedReferences. + dontRenameImports = true; + # Replace hardcoded paths to go package in /nix/store. + preFixup = optionalString isIOS '' + find $out -type f -exec \ + remove-references-to -t $disallowedReferences '{}' + || true + ''; +} diff --git a/nix/status-go/mobile/default.nix b/nix/status-go/mobile/default.nix new file mode 100644 index 00000000000..b3efe613674 --- /dev/null +++ b/nix/status-go/mobile/default.nix @@ -0,0 +1,26 @@ +{ callPackage, meta, source, goBuildLdFlags }: + +{ + android = {abis ? [ "armeabi-v7a" "arm64-v8a" "x86" ]}: callPackage ./build.nix { + platform = "android"; + platformVersion = "23"; + # Hide different arch naming in gomobile from Android builds. + targets = let + abiMap = { + "armeabi-v7a" = "android/arm"; + "arm64-v8a" = "android/arm64"; + "x86" = "android/386"; + "x86_64" = "android/amd64"; + }; + in map (arch: abiMap."${arch}") abis; + outputFileName = "status-go-${source.shortRev}.aar"; + inherit meta source goBuildLdFlags; + }; + + ios = {targets ? [ "ios/arm64" "iossimulator/amd64"]}: callPackage ./build.nix { + platform = "ios"; + platformVersion = "11.0"; + outputFileName = "Statusgo.xcframework"; + inherit meta source goBuildLdFlags targets; + }; +} diff --git a/nix/status-go/mobile/enforceXCodeAvailable.nix b/nix/status-go/mobile/enforceXCodeAvailable.nix new file mode 100644 index 00000000000..df7ce05b834 --- /dev/null +++ b/nix/status-go/mobile/enforceXCodeAvailable.nix @@ -0,0 +1,30 @@ +{ xcodeWrapper }: + +let + RED = "\\033[0;31m"; + GREEN = "\\033[0;32m"; + NC = "\\033[0m"; + + _xcodeToolsTest = '' + xcode=0 + iPhoneSDK=0 + export PATH=${xcodeWrapper}/bin:$PATH + xcrun xcodebuild -version && xcode=1 + [ $xcode -eq 1 ] && xcrun --sdk iphoneos --show-sdk-version > /dev/null && iPhoneSDK=1 + ''; + _xcodeToolReportScript = tool-name: ''[ $SELECTED -eq 0 ] && echo -e "${NC}- ${RED}[ ] ${tool-name}" || echo -e "${NC}- ${GREEN}[√] ${tool-name}${RED}"''; + _xcodeToolsReportScript = '' + echo -e "${RED}There are some required tools missing in the system:" + export SELECTED=$xcode; ${_xcodeToolReportScript "Xcode ${_xcodeVersion}"} + export SELECTED=$iPhoneSDK; ${_xcodeToolReportScript "iPhone SDK"} + ''; + _xcodeVersion = builtins.replaceStrings ["xcode-wrapper-"] [""] xcodeWrapper.name; +in + '' + ${_xcodeToolsTest} + if [ $xcode -eq 0 ]; then + ${_xcodeToolsReportScript} + echo -e "Please install Xcode ${_xcodeVersion} from the App Store.${NC}" + exit 1 + fi + '' diff --git a/nix/status-go/source.nix b/nix/status-go/source.nix new file mode 100644 index 00000000000..535f3f2b560 --- /dev/null +++ b/nix/status-go/source.nix @@ -0,0 +1,60 @@ +{ lib, fetchFromGitHub }: + +let + inherit (lib) strings traceValFn importJSON getEnvWithDefault; + + # TODO: move to default? + srcOverride = getEnvWithDefault "STATUS_GO_SRC_OVERRIDE" null; + # Warning message about using local sources + localSrcWarn = (path: "Using local status-go sources from ${path}"); + + localSrc = rec { + owner = "status-im"; + repo = "status-go"; + rev = "unknown"; + shortRev = rev; + version = "unknown"; + cleanVersion = version; + goPackagePath = "github.com/${owner}/${repo}"; + # We use builtins.path so that we can name the resulting derivation, + # Normally the name would not be deterministic, taken from the checkout directory. + src = builtins.path rec { + path = traceValFn localSrcWarn srcOverride; + name = "${repo}-source-${shortRev}"; + # Keep this filter as restrictive as possible in order + # to avoid unnecessary rebuilds and limit closure size + filter = lib.mkFilter { + root = path; + include = [ ".*" ]; + exclude = [ + ".*/[.]git.*" ".*[.]md" ".*[.]yml" ".*/.*_test.go$" + "VERSION" "_assets/.*" "build/.*" + ".*/.*LICENSE.*" ".*/CONTRIB.*" ".*/AUTHOR.*" + ]; + }; + }; + }; + + githubSrc = let + # TODO: Simplify this path search with lib.locateDominatingFile + versionJSON = importJSON ../../status-go-version.json; + sha256 = versionJSON.src-sha256; + in rec { + inherit (versionJSON) owner repo version; + rev = versionJSON.commit-sha1; + shortRev = strings.substring 0 7 rev; + cleanVersion = lib.sanitizeVersion versionJSON.version; + # Need to pretend this is from status-im to let Go build it. + goPackagePath = "github.com/status-im/${repo}"; + src = fetchFromGitHub { + inherit rev owner repo sha256; + name = "${repo}-${shortRev}-source"; + }; + }; +in + if srcOverride != null + # If config.status-im.status-go.src-override is defined, + # instruct Nix to use that path to build status-go + then localSrc + # Otherwise grab it from the location defined by status-go-version.json + else githubSrc diff --git a/nix/targets.nix b/nix/targets.nix new file mode 100644 index 00000000000..50d82ba0c2c --- /dev/null +++ b/nix/targets.nix @@ -0,0 +1,13 @@ +{ + config ? {}, + pkgs ? import ./pkgs.nix { inherit config; } +}: + +let + inherit (pkgs) stdenv callPackage; + + status-go = callPackage ./status-go { }; + mobile = callPackage ./mobile { inherit status-go; }; +in { + inherit mobile status-go; +} diff --git a/package.json b/package.json new file mode 100644 index 00000000000..ac06b3ea422 --- /dev/null +++ b/package.json @@ -0,0 +1,120 @@ +{ + "name": "StatusIm-Mobile", + "version": "0.0.1", + "main": "modules/react-native-status/nodejs/bindings.js", + "private": true, + "scripts": { + "_comment": "This also prevents 'yarn install' from running outside of nix.", + "preinstall": "scripts/check-nix-shell.sh && make status-go-library" + }, + "dependencies": { + "@ethersproject/shims": "^5.7.0", + "@json-rpc-tools/utils": "^1.7.6", + "@react-native-async-storage/async-storage": "1.19.3", + "@react-native-camera-roll/camera-roll": "7.5.2", + "@react-native-clipboard/clipboard": "1.13.2", + "@react-native-community/audio-toolkit": "git+https://github.com/tbenr/react-native-audio-toolkit.git#refs/tags/v2.0.3-status-v6", + "@react-native-community/blur": "4.4.1", + "@react-native-community/hooks": "^3.0.0", + "@react-native-community/masked-view": "^0.1.6", + "@react-native-community/netinfo": "^4.4.0", + "@react-native-community/push-notification-ios": "^1.4.1", + "@react-native-community/slider": "^3.0.0", + "@react-native-firebase/app": "^21.7.2", + "@react-native-firebase/messaging": "^21.7.2", + "@reown/walletkit": "^1.0.0", + "@walletconnect/react-native-compat": "2.17.3", + "base-64": "^1.0.0", + "big-integer": "1.6.51", + "bignumber.js": "git+https://github.com/status-im/bignumber.js.git#refs/tags/v4.0.2-status", + "chance": "^1.1.0", + "emojilib": "^2.4.0", + "eth-phishing-detect": "^1.2.0", + "ethers": "5.7.2", + "fast-text-encoding": "^1.0.6", + "form-data": "^4.0.0", + "functional-red-black-tree": "^1.0.1", + "i18n-js": "^3.3.0", + "jsc-android": "^250231.0.0", + "node-libs-react-native": "^1.2.1", + "react": "18.2.0", + "react-dom": "18.0.0", + "react-native": "0.73.5", + "react-native-background-timer": "^2.1.1", + "react-native-biometrics": "^3.0.1", + "react-native-blob-util": "^0.13.18", + "react-native-camera-kit": "git+https://github.com/status-im/react-native-camera-kit.git#refs/tags/v14.0.0-beta15-status-v1", + "react-native-config": "^1.5.0", + "react-native-dialogs": "1.1.2", + "react-native-draggable-flatlist": "4.0.1", + "react-native-fast-image": "^8.5.11", + "react-native-fs": "^2.14.1", + "react-native-gesture-handler": "2.14.1", + "react-native-get-random-values": "^1.11.0", + "react-native-gifted-charts": "^1.3.2", + "react-native-hole-view": "^3.0.0-alpha4", + "react-native-image-crop-picker": "0.41.2", + "react-native-image-resizer": "^1.2.3", + "react-native-keychain": "8.1.2", + "react-native-linear-gradient": "3.0.0-alpha.1", + "react-native-lottie-splash-screen": "^1.0.1", + "react-native-mmkv": "2.12.2", + "react-native-navigation": "7.43.0", + "react-native-orientation-locker": "^1.5.0", + "react-native-pdf": "^6.7.5", + "react-native-permissions": "4.1.5", + "react-native-reanimated": "3.6.1", + "react-native-redash": "18.1.0", + "react-native-safe-area-context": "4.14.1", + "react-native-shadow-2": "^7.0.8", + "react-native-shake": "^3.3.1", + "react-native-share": "10.0.2", + "react-native-status-keycard": "git+https://github.com/status-im/react-native-status-keycard.git#refs/tags/v2.6.6", + "react-native-svg": "13.10.0", + "react-native-webview": "13.6.3", + "react-syntax-highlighter": "^15.5.0" + }, + "devDependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.0", + "@babel/helper-builder-react-jsx": "^7.20.0", + "@babel/plugin-transform-block-scoping": "^7.20.0", + "@babel/preset-env": "^7.20.0", + "@babel/preset-react": "^7.18.6", + "@babel/register": "7.0.0", + "@jest/globals": "^25.1.0", + "@mapbox/node-pre-gyp": "^1.0.9", + "@react-native/babel-preset": "0.73.21", + "@react-native/codegen": "0.73.2", + "@react-native/gradle-plugin": "0.73.4", + "@react-native/metro-config": "0.73.5", + "@testing-library/jest-native": "^5.4.3", + "@testing-library/react-native": "^12.4.2", + "@tsconfig/react-native": "^3.0.0", + "@types/jest": "^28.1.6", + "@types/react": "^18.2.6", + "@types/react-test-renderer": "^18.0.0", + "babel-jest": "^29.6.3", + "concurrently": "^7.6.0", + "jest": "^29.7.0", + "jest-image-snapshot": "^5.1.0", + "jest-silent-reporter": "^0.5.0", + "nodemon": "^2.0.16", + "nyc": "^14.1.1", + "prettier": "^3.3.3", + "prettier-plugin-sort-json": "^4.0.0", + "process": "0.11.10", + "react-test-renderer": "18.1.0", + "shadow-cljs": "2.26.2", + "websocket": "^1.0.35" + }, + "binary": { + "module_name": "status_nodejs_addon", + "module_path": "./lib/binding/", + "host": "https://github.com/status-im/status-mobile/releases/download/", + "remote_path": "{version}" + }, + "engines": { + "node": ">=18" + } +} diff --git a/patches/BlurView-build.gradle.patch b/patches/BlurView-build.gradle.patch new file mode 100644 index 00000000000..cd93e309cae --- /dev/null +++ b/patches/BlurView-build.gradle.patch @@ -0,0 +1,11 @@ +--- /tmp/tmp-status-mobile-851dbcb56/tmp.gcyqIAUEIA/build.gradle 2024-04-16 18:11:12.481819000 +0200 ++++ ./node_modules/@react-native-community/blur/android/build.gradle 2024-04-16 18:11:18.889991702 +0200 +@@ -5,7 +5,7 @@ + } + + dependencies { +- classpath 'com.android.tools.build:gradle:3.5.3' ++ classpath 'com.android.tools.build:gradle:3.5.4' + } + } + diff --git a/patches/BlurView.ios.tsx.patch b/patches/BlurView.ios.tsx.patch new file mode 100644 index 00000000000..85b09c59e58 --- /dev/null +++ b/patches/BlurView.ios.tsx.patch @@ -0,0 +1,10 @@ +--- /tmp/tmp-status-mobile-61974dae4/tmp.6k5ZPUa0Hp/BlurView.ios.tsx 2024-04-16 17:07:33.229847000 +0200 ++++ ./node_modules/@react-native-community/blur/src/components/BlurView.ios.tsx 2024-04-16 17:07:58.540617624 +0200 +@@ -6,6 +6,7 @@ + | 'dark' + | 'light' + | 'xlight' ++ | 'transparent' + | 'prominent' + | 'regular' + | 'extraDark' diff --git a/patches/BlurView.mm.patch b/patches/BlurView.mm.patch new file mode 100644 index 00000000000..13bf7d36d37 --- /dev/null +++ b/patches/BlurView.mm.patch @@ -0,0 +1,57 @@ +--- /tmp/tmp-status-mobile-851dbcb56/tmp.beqWjtcNwi/BlurView.mm 2024-04-16 17:55:00.688450000 +0200 ++++ ./node_modules/@react-native-community/blur/ios/BlurView.mm 2024-04-16 17:56:17.772952042 +0200 +@@ -73,7 +73,7 @@ + { + const auto &oldViewProps = *std::static_pointer_cast(_props); + const auto &newViewProps = *std::static_pointer_cast(props); +- ++ + if (oldViewProps.blurAmount != newViewProps.blurAmount) { + NSNumber *blurAmount = [NSNumber numberWithInt:newViewProps.blurAmount]; + [self setBlurAmount:blurAmount]; +@@ -83,12 +83,12 @@ + NSString *blurType = [NSString stringWithUTF8String:toString(newViewProps.blurType).c_str()]; + [self setBlurType:blurType]; + } +- ++ + if (oldViewProps.reducedTransparencyFallbackColor != newViewProps.reducedTransparencyFallbackColor) { + UIColor *color = RCTUIColorFromSharedColor(newViewProps.reducedTransparencyFallbackColor); + [self setReducedTransparencyFallbackColor:color]; + } +- ++ + [super updateProps:props oldProps:oldProps]; + } + #endif // RCT_NEW_ARCH_ENABLED +@@ -131,6 +131,7 @@ + + - (UIBlurEffectStyle)blurEffectStyle + { ++ if ([self.blurType isEqual: @"transparent"]) return UIBlurEffectStyleDark; + if ([self.blurType isEqual: @"xlight"]) return UIBlurEffectStyleExtraLight; + if ([self.blurType isEqual: @"light"]) return UIBlurEffectStyleLight; + if ([self.blurType isEqual: @"dark"]) return UIBlurEffectStyleDark; +@@ -160,7 +161,7 @@ + if ([self.blurType isEqual: @"thinMaterialLight"]) return UIBlurEffectStyleSystemThinMaterialLight; + if ([self.blurType isEqual: @"ultraThinMaterialLight"]) return UIBlurEffectStyleSystemUltraThinMaterialLight; + #endif +- ++ + #if TARGET_OS_TV + if ([self.blurType isEqual: @"regular"]) return UIBlurEffectStyleRegular; + if ([self.blurType isEqual: @"prominent"]) return UIBlurEffectStyleProminent; +@@ -183,6 +184,13 @@ + UIBlurEffectStyle style = [self blurEffectStyle]; + self.blurEffect = [BlurEffectWithAmount effectWithStyle:style andBlurAmount:self.blurAmount]; + self.blurEffectView.effect = self.blurEffect; ++ ++ if ([self.blurType isEqual: @"transparent"]) { ++ for (UIView *subview in self.blurEffectView.subviews) { ++ subview.backgroundColor = [UIColor clearColor]; ++ } ++ } ++ + } + + - (void)updateFallbackView diff --git a/patches/BlurViewNativeComponent.ts.patch b/patches/BlurViewNativeComponent.ts.patch new file mode 100644 index 00000000000..e08b1d94d88 --- /dev/null +++ b/patches/BlurViewNativeComponent.ts.patch @@ -0,0 +1,10 @@ +--- /tmp/tmp-status-mobile-61974dae4/tmp.NHLSFZG6LG/BlurViewNativeComponent.ts 2024-04-16 17:08:42.755929000 +0200 ++++ ./node_modules/@react-native-community/blur/src/fabric/BlurViewNativeComponent.ts 2024-04-16 17:08:57.634037113 +0200 +@@ -10,6 +10,7 @@ + | 'dark' + | 'light' + | 'xlight' ++ | 'transparent' + | 'prominent' + | 'regular' + | 'extraDark' diff --git a/patches/CameraRoll.ts.patch b/patches/CameraRoll.ts.patch new file mode 100644 index 00000000000..f2af11c1743 --- /dev/null +++ b/patches/CameraRoll.ts.patch @@ -0,0 +1,22 @@ +--- /tmp/tmp-status-mobile-3907e6b2e/tmp.re8kHerusA/CameraRoll.ts 2024-04-16 15:17:12.942432000 +0200 ++++ ./node_modules/@react-native-camera-roll/camera-roll/src/CameraRoll.ts 2024-04-16 15:17:42.455250986 +0200 +@@ -239,6 +239,19 @@ + } + + /** ++ * Returns total iOS image count ++ */ ++ static getPhotosCountiOS(): Promise { ++ return RNCCameraRoll.getPhotosCountiOS(''); ++ } ++ /** ++ * Returns favorites and their count iOS ++ */ ++ static getFavoritesiOS(): Promise { ++ return RNCCameraRoll.getFavoritesiOS(''); ++ } ++ ++ /** + * Saves the photo or video to the camera roll or photo library, and returns the URI of the newly created asset. + * + * @deprecated `save(...)` is deprecated - use `saveAsset(...)` instead. diff --git a/patches/CameraRollModule.java.patch b/patches/CameraRollModule.java.patch new file mode 100644 index 00000000000..970fd90c436 --- /dev/null +++ b/patches/CameraRollModule.java.patch @@ -0,0 +1,37 @@ +--- /tmp/tmp-status-mobile-ca53bf738/tmp.QcO69SAqs7/CameraRollModule.java 2024-04-19 14:02:42.746932000 +0200 ++++ ./node_modules/@react-native-camera-roll/camera-roll/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java 2024-04-19 14:03:51.776866113 +0200 +@@ -677,7 +677,7 @@ + + WritableArray subTypes = Arguments.createArray(); + node.putArray("subTypes", subTypes); +- ++ + if (includeSourceType) { + node.putString("sourceType", "UserLibrary"); + } else { +@@ -685,7 +685,7 @@ + } + + WritableArray group_name = Arguments.createArray(); +- ++ + if (includeAlbums) { + group_name.pushString(media.getString(groupNameIndex)); + } +@@ -1089,6 +1089,16 @@ + promise.reject("CameraRoll:getPhotoThumbnail", "getPhotoThumbnail is not supported on Android"); + } + ++ @ReactMethod ++ public void getPhotosCountiOS(String arg, Promise promise) { ++ promise.reject("CameraRoll:getPhotosCountiOS", "getPhotosCountiOS is not supported on Android"); ++ } ++ ++ @ReactMethod ++ public void getFavoritesiOS(String arg, Promise promise) { ++ promise.reject("CameraRoll:getFavoritesiOS", "getFavoritesiOS is not supported on Android"); ++ } ++ + public void addListener(String eventName) {} + public void removeListeners(double count) {} + } diff --git a/patches/NativeCameraRollModule.ts.patch b/patches/NativeCameraRollModule.ts.patch new file mode 100644 index 00000000000..90c1126e8aa --- /dev/null +++ b/patches/NativeCameraRollModule.ts.patch @@ -0,0 +1,11 @@ +--- /tmp/tmp-status-mobile-3907e6b2e/tmp.juxTO1BeCM/NativeCameraRollModule.ts 2024-04-16 15:21:28.379979000 +0200 ++++ ./node_modules/@react-native-camera-roll/camera-roll/src/NativeCameraRollModule.ts 2024-04-16 15:21:40.490391291 +0200 +@@ -81,6 +81,8 @@ + getPhotos(params: Object): Promise; + getAlbums(params: Object): Promise; + deletePhotos(photoUris: Array): Promise; ++ getPhotosCountiOS(arg: string): Promise; ++ getFavoritesiOS(arg: string): Promise; + getPhotoByInternalID( + internalID: string, + options: Object, diff --git a/patches/NativeCameraRollModuleSpec.java.patch b/patches/NativeCameraRollModuleSpec.java.patch new file mode 100644 index 00000000000..45ebce7a033 --- /dev/null +++ b/patches/NativeCameraRollModuleSpec.java.patch @@ -0,0 +1,16 @@ +--- /tmp/tmp-status-mobile-ca53bf738/tmp.Ay8Xen8Mmn/NativeCameraRollModuleSpec.java 2024-04-19 12:10:05.789750000 +0200 ++++ ./node_modules/@react-native-camera-roll/camera-roll/android/src/paper/java/com/reactnativecommunity/cameraroll/NativeCameraRollModuleSpec.java 2024-04-19 12:12:27.231493426 +0200 +@@ -50,4 +50,13 @@ + @ReactMethod + @DoNotStrip + public abstract void getPhotoThumbnail(String internalID, ReadableMap options, Promise promise); ++ ++ @ReactMethod ++ @DoNotStrip ++ public abstract void getPhotosCountiOS(String arg, Promise promise); ++ ++ @ReactMethod ++ @DoNotStrip ++ public abstract void getFavoritesiOS(String arg, Promise promise); ++ + } diff --git a/patches/RNCCameraRoll.mm.patch b/patches/RNCCameraRoll.mm.patch new file mode 100644 index 00000000000..2951cbe6442 --- /dev/null +++ b/patches/RNCCameraRoll.mm.patch @@ -0,0 +1,56 @@ +--- /tmp/tmp-status-mobile-3907e6b2e/tmp.O0mkyjqnsy/RNCCameraRoll.mm 2024-04-16 15:26:23.070258000 +0200 ++++ ./node_modules/@react-native-camera-roll/camera-roll/ios/RNCCameraRoll.mm 2024-04-16 15:26:32.664996066 +0200 +@@ -955,6 +955,53 @@ + return [albumTitles copy]; + } + ++RCT_EXPORT_METHOD(getPhotosCountiOS:(NSString *)blank ++ resolve:(RCTPromiseResolveBlock)resolve ++ reject:(RCTPromiseRejectBlock)reject) ++{ ++ __block NSInteger intTotalCount=0; ++ PHFetchOptions *allPhotosOptions = [PHFetchOptions new]; ++ allPhotosOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d ",PHAssetMediaTypeImage]; ++ PHFetchResult *allPhotosResult = [PHAsset fetchAssetsWithOptions:allPhotosOptions]; ++ intTotalCount+=allPhotosResult.count; ++ ++ resolve(@(intTotalCount)); ++} ++ ++RCT_EXPORT_METHOD(getFavoritesiOS:(NSString *)blank ++ resolve:(RCTPromiseResolveBlock)resolve ++ reject:(RCTPromiseRejectBlock)reject) ++{ ++ __block NSInteger intTotalCount=0; ++ PHFetchOptions *fetchOptions = [PHFetchOptions new]; ++ NSString *format = @"(favorite == true)"; ++ fetchOptions.predicate = [NSPredicate predicateWithFormat:format]; ++ PHFetchResult *const assetsFetchResult = [PHAsset fetchAssetsWithOptions:fetchOptions]; ++ PHAsset *imageAsset = [assetsFetchResult firstObject]; ++ NSMutableArray * result = [NSMutableArray new]; ++ ++ for (PHAsset* asset in assetsFetchResult) { ++ NSArray *resources = [PHAssetResource assetResourcesForAsset:asset ]; ++ if ([resources count] < 1) continue; ++ NSString *orgFilename = ((PHAssetResource*)resources[0]).originalFilename; ++ NSString *uit = ((PHAssetResource*)resources[0]).uniformTypeIdentifier; ++ NSString *mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(uit), kUTTagClassMIMEType)); ++ CFStringRef extension = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(uit), kUTTagClassFilenameExtension); ++ [result addObject:@{ ++ @"width": @([asset pixelWidth]), ++ @"height": @([asset pixelHeight]), ++ @"filename": orgFilename ?: @"", ++ @"mimeType": mimeType ?: @"", ++ @"id": [asset localIdentifier], ++ @"creationDate": [asset creationDate], ++ @"uri": [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]], ++ @"duration": @([asset duration]) ++ }]; ++ } ++ [result addObject:@{@"count": @(assetsFetchResult.count)}]; ++ resolve(result); ++} ++ + static void checkPhotoLibraryConfig() + { + #if RCT_DEV diff --git a/patches/VibrancyViewNativeComponent.ts.patch b/patches/VibrancyViewNativeComponent.ts.patch new file mode 100644 index 00000000000..14742361e51 --- /dev/null +++ b/patches/VibrancyViewNativeComponent.ts.patch @@ -0,0 +1,10 @@ +--- /tmp/tmp-status-mobile-61974dae4/tmp.zy6uXqO4gW/VibrancyViewNativeComponent.ts 2024-04-16 17:09:23.772623000 +0200 ++++ ./node_modules/@react-native-community/blur/src/fabric/VibrancyViewNativeComponent.ts 2024-04-16 17:09:39.033737349 +0200 +@@ -10,6 +10,7 @@ + | 'dark' + | 'light' + | 'xlight' ++ | 'transparent' + | 'prominent' + | 'regular' + | 'extraDark' diff --git a/patches/metro+0.30.2.patch b/patches/metro+0.30.2.patch deleted file mode 100644 index 65f17839d4a..00000000000 --- a/patches/metro+0.30.2.patch +++ /dev/null @@ -1,23 +0,0 @@ -patch-package ---- a/node_modules/metro/src/JSTransformer/index.js -+++ b/node_modules/metro/src/JSTransformer/index.js -@@ -151,6 +151,8 @@ module.exports = class Transformer { - /^--heap[_-]growing[_-]percent=[0-9]+$/.test(arg) || - /^--max[_-]old[_-]space[_-]size=[0-9]+$/.test(arg)); - -+ execArgv.push("--max-old-space-size=8192"); -+ - const env = _extends({}, - process.env, { - // Force color to print syntax highlighted code frames. ---- a/node_modules/metro/src/defaults.js -+++ b/node_modules/metro/src/defaults.js -@@ -45,7 +45,7 @@ exports.sourceExts = ['js', 'json']; - - exports.moduleSystem = require.resolve('./lib/polyfills/require.js'); - --exports.platforms = ['ios', 'android', 'windows', 'web']; -+exports.platforms = ['ios', 'android', 'windows', 'web', 'desktop']; - - exports.providesModuleNodeModules = ['react-native', 'react-native-windows']; - diff --git a/patches/react-native-android.patch b/patches/react-native-android.patch new file mode 100644 index 00000000000..2d632bce043 --- /dev/null +++ b/patches/react-native-android.patch @@ -0,0 +1,12 @@ +# Do not add a BuildId to the generated libraries, for reproducibility +--- ./node_modules/react-native/ReactAndroid/src/main/jni/CMakeLists.txt ++++ ./node_modules/react-native/ReactAndroid/src/main/jni/CMakeLists-patched.txt +@@ -21,7 +21,7 @@ + endif(CCACHE_FOUND) + + # Make sure every shared lib includes a .note.gnu.build-id header +-add_link_options(-Wl,--build-id) ++add_link_options(-Wl,--build-id=none) + add_compile_options(-Wall -Werror -std=c++20) + + function(add_react_android_subdir relative_path) diff --git a/patches/react-native-keychain.patch b/patches/react-native-keychain.patch new file mode 100644 index 00000000000..9e277287246 --- /dev/null +++ b/patches/react-native-keychain.patch @@ -0,0 +1,20 @@ +# Remove gradle-test-logger-plugin: +# https://github.com/oblador/react-native-keychain/issues/595 +# TODO: remove this patch when we this library fixes above issue + +--- ./node_modules/react-native-keychain/android/build.gradle ++++ ./node_modules/react-native-keychain/android/build-patched.gradle +@@ -4,13 +4,9 @@ + url 'https://plugins.gradle.org/m2/' + } + } +- dependencies { +- classpath 'com.adarshr:gradle-test-logger-plugin:2.0.0' +- } + } + + apply plugin: 'com.android.library' +-apply plugin: "com.adarshr.test-logger" + + def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback diff --git a/patches/react-native-script.patch b/patches/react-native-script.patch new file mode 100644 index 00000000000..4083731aa92 --- /dev/null +++ b/patches/react-native-script.patch @@ -0,0 +1,13 @@ +# to fix https://github.com/status-im/status-mobile/issues/18548 + +--- ./node_modules/react-native/scripts/react_native_pods_utils/script_phases.sh ++++ ./node_modules/react-native/scripts/react_native_pods_utils/script_phases-patched.sh +@@ -104,7 +104,7 @@ + mkdir -p "$RCT_SCRIPT_OUTPUT_DIR" + + # Copy all output to output_dir +- cp -R -X "$TEMP_OUTPUT_DIR/." "$RCT_SCRIPT_OUTPUT_DIR" || exit 1 ++ cp -R "$TEMP_OUTPUT_DIR/." "$RCT_SCRIPT_OUTPUT_DIR" || exit 1 + echo "$LIBRARY_NAME output has been written to $RCT_SCRIPT_OUTPUT_DIR:" >> "${SCRIPT_OUTPUT_FILE_0}" 2>&1 + ls -1 "$RCT_SCRIPT_OUTPUT_DIR" >> "${SCRIPT_OUTPUT_FILE_0}" 2>&1 + } diff --git a/patches/react-native.patch b/patches/react-native.patch new file mode 100755 index 00000000000..402fedefe29 --- /dev/null +++ b/patches/react-native.patch @@ -0,0 +1,15 @@ +# Fix pod issue after upgrading to MacOS Sonoma and Xcode 15 +# https://github.com/status-im/status-mobile/issues/17682 + +--- ./node_modules/react-native/scripts/ios-configure-glog.sh ++++ ./node_modules/react-native/scripts/ios-configure-glog-patched.sh +@@ -42,9 +42,6 @@ + patch -p1 config.sub fix_glog_0.3.5_apple_silicon.patch + fi + +-export CC="$(xcrun -find -sdk $PLATFORM_NAME cc) -arch $CURRENT_ARCH -isysroot $(xcrun -sdk $PLATFORM_NAME --show-sdk-path)" +-export CXX="$CC" +- + # Remove automake symlink if it exists + if [ -h "test-driver" ]; then + rm test-driver diff --git a/project.clj b/project.clj deleted file mode 100644 index 85c6c758528..00000000000 --- a/project.clj +++ /dev/null @@ -1,135 +0,0 @@ -(defproject status-im "0.1.0-SNAPSHOT" - :url "https://github.com/status-im/status-react/" - :license {:name "Eclipse Public License" - :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.9.0"] - [org.clojure/clojurescript "1.10.238"] - [org.clojure/core.async "0.4.474"] - [reagent "0.7.0" :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server cljsjs/create-react-class]] - [status-im/re-frame "0.10.5"] - [com.andrewmcveigh/cljs-time "0.5.2"] - [com.taoensso/timbre "4.10.0"] - [hickory "0.7.1"] - [com.cognitect/transit-cljs "0.8.248"]] - :plugins [[lein-cljsbuild "1.1.7"] - [lein-re-frisk "0.5.8"] - [lein-cljfmt "0.5.7"] - [rasom/lein-githooks "0.1.5"]] - :githooks {:auto-install true - :pre-commit ["lein cljfmt check src/status_im/core.cljs $(git diff --diff-filter=d --cached --name-only src test/cljs)"]} - :cljfmt {:indents {letsubs [[:inner 0]]}} - :clean-targets ["target/" "index.ios.js" "index.android.js"] - :aliases {"prod-build" ^{:doc "Recompile code with prod profile."} - ["do" "clean" - ["with-profile" "prod" "cljsbuild" "once" "ios"] - ["with-profile" "prod" "cljsbuild" "once" "android"] - ["with-profile" "prod" "cljsbuild" "once" "desktop"]] - "prod-build-android" ^{:doc "Recompile code for Android with prod profile."} - ["do" "clean" - ["with-profile" "prod" "cljsbuild" "once" "android"]] - "prod-build-ios" ^{:doc "Recompile code for iOS with prod profile."} - ["do" "clean" - ["with-profile" "prod" "cljsbuild" "once" "ios"]] - "prod-build-desktop" ^{:doc "Recompile code for desktop with prod profile."} - ["do" "clean" - ["with-profile" "prod" "cljsbuild" "once" "desktop"]] - "figwheel-repl" ["with-profile" "+figwheel" "run" "-m" "clojure.main" "env/dev/run.clj"] - "test-cljs" ["with-profile" "test" "doo" "node" "test" "once"] - "test-protocol" ["with-profile" "test" "doo" "node" "protocol" "once"] - "test-env-dev-utils" ["with-profile" "test" "doo" "node" "env-dev-utils" "once"]} - :profiles {:dev {:dependencies [[com.cemerick/piggieback "0.2.2"]] - :cljsbuild {:builds - {:ios - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src"] - :compiler {:output-to "target/ios/app.js" - :main "env.ios.main" - :output-dir "target/ios" - :optimizations :none}} - :android - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src"] - :compiler {:output-to "target/android/app.js" - :main "env.android.main" - :output-dir "target/android" - :optimizations :none} - :warning-handlers [status-im.utils.build/warning-handler]} - :desktop - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src"] - :compiler {:output-to "target/desktop/app.js" - :main "env.desktop.main" - :output-dir "target/desktop" - :optimizations :none}}}} - :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl] - :timeout 240000}} - :figwheel [:dev - {:dependencies [[figwheel-sidecar "0.5.16-SNAPSHOT"] - [re-frisk-remote "0.5.5"] - [re-frisk-sidecar "0.5.7"] - [day8.re-frame/tracing "0.5.0"] - [hawk "0.2.11"]] - :source-paths ["src" "env/dev" "react-native/src/cljsjs" "components/src"]}] - :test {:dependencies [[day8.re-frame/test "0.1.5"]] - :plugins [[lein-doo "0.1.9"]] - :cljsbuild {:builds - [{:id "test" - :source-paths ["components/src" "src" "test/cljs"] - :compiler {:main status-im.test.runner - :output-to "target/test/test.js" - :output-dir "target/test" - :optimizations :none - :preamble ["js/hook-require.js"] - :target :nodejs}} - {:id "protocol" - :source-paths ["components/src" "src" "test/cljs"] - :compiler {:main status-im.test.protocol.runner - :output-to "target/test/test.js" - :output-dir "target/test" - :optimizations :none - :preamble ["js/hook-require.js"] - :target :nodejs}} - {:id "env-dev-utils" - :source-paths ["env/dev/env/utils.cljs" "test/env/dev"] - :compiler {:main env.test.runner - :output-to "target/test/test.js" - :output-dir "target/test" - :optimizations :none - :target :nodejs}}]}} - :prod {:cljsbuild {:builds - {:ios - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod"] - :compiler {:output-to "index.ios.js" - :main "env.ios.main" - :output-dir "target/ios-prod" - :static-fns true - :optimize-constants true - :optimizations :simple - :closure-defines {"goog.DEBUG" false} - :parallel-build false - :elide-asserts true - :language-in :ecmascript5} - :warning-handlers [status-im.utils.build/warning-handler]} - :android - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod"] - :compiler {:output-to "index.android.js" - :main "env.android.main" - :output-dir "target/android-prod" - :static-fns true - :optimize-constants true - :optimizations :simple - :closure-defines {"goog.DEBUG" false} - :parallel-build false - :elide-asserts true - :language-in :ecmascript5} - :warning-handlers [status-im.utils.build/warning-handler]} - :desktop - {:source-paths ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src" "env/prod"] - :compiler {:output-to "index.desktop.js" - :main "env.desktop.main" - :output-dir "target/desktop-prod" - :static-fns true - :optimize-constants true - :optimizations :simple - :closure-defines {"goog.DEBUG" false} - :parallel-build false - :elide-asserts true - :language-in :ecmascript5} - :warning-handlers [status-im.utils.build/warning-handler]}}}}}) diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 00000000000..0b03f578984 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,25 @@ +module.exports = { + dependencies: { + 'react-native-config': { + platforms: { + ios: null, + }, + }, + 'react-native-image-resizer': { + platforms: { + ios: null, + }, + }, + 'react-native-status-keycard': { + platforms: { + android: null, + ios: null, + }, + }, + '@react-native-community/blur': { + platforms: { + android: null, + }, + }, + }, +}; diff --git a/react-native/src/cljsjs/cljsjs/create_react_class.cljs b/react-native/src/cljsjs/cljsjs/create_react_class.cljs deleted file mode 100644 index 2b565554c1f..00000000000 --- a/react-native/src/cljsjs/cljsjs/create_react_class.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns cljsjs.create-react-class) diff --git a/react-native/src/cljsjs/cljsjs/react.cljs b/react-native/src/cljsjs/cljsjs/react.cljs deleted file mode 100644 index 81d07942a17..00000000000 --- a/react-native/src/cljsjs/cljsjs/react.cljs +++ /dev/null @@ -1,5 +0,0 @@ -(ns cljsjs.react - (:require [status-im.react-native.js-dependencies :as rn-dependencies])) - -(when (exists? js/window) - (set! js/ReactNative rn-dependencies/react-native)) diff --git a/react-native/src/cljsjs/cljsjs/react/dom.cljs b/react-native/src/cljsjs/cljsjs/react/dom.cljs deleted file mode 100644 index 3c69f0090fa..00000000000 --- a/react-native/src/cljsjs/cljsjs/react/dom.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns cljsjs.react.dom) \ No newline at end of file diff --git a/react-native/src/cljsjs/cljsjs/react/dom/server.cljs b/react-native/src/cljsjs/cljsjs/react/dom/server.cljs deleted file mode 100644 index c7ffcf638fe..00000000000 --- a/react-native/src/cljsjs/cljsjs/react/dom/server.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns cljsjs.react.dom.server) \ No newline at end of file diff --git a/react-native/src/desktop/status_im/react_native/js_dependencies.cljs b/react-native/src/desktop/status_im/react_native/js_dependencies.cljs deleted file mode 100644 index 47ec56f74b1..00000000000 --- a/react-native/src/desktop/status_im/react_native/js_dependencies.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.react-native.js-dependencies) - -(def config (js/require "react-native-config")) -(def fs (js/require "react-native-fs")) -(def http-bridge (js/require "react-native-http-bridge")) -(def keychain (js/require "react-native-keychain")) -(def qr-code (js/require "react-native-qrcode")) -(def react-native (js/require "react-native")) -(def realm (js/require "realm")) -(def webview-bridge (js/require "react-native-webview-bridge")) -(def secure-random (.-generateSecureRandom (js/require "react-native-securerandom"))) -(def EventEmmiter (js/require "react-native/Libraries/vendor/emitter/EventEmitter")) -(def fetch (.-default (js/require "react-native-fetch-polyfill"))) -(def i18n (js/require "react-native-i18n")) -(def camera #js {:constants {:Aspect "Portrait"}}) -(def dialogs #js {}) -(def dismiss-keyboard #js {}) -(def image-crop-picker #js {}) -(def image-resizer #js {}) -(def instabug #js {:IBGLog ( fn [])}) -(def nfc #js {}) -(def svg #js {}) -(def react-native-fcm #js {:default #js {:getFCMToken (fn []) - :requestPermissions (fn [])}}) -(def snoopy #js {}) -(def snoopy-filter #js {}) -(def snoopy-bars #js {}) -(def snoopy-buffer #js {}) -(def background-timer #js {:setTimeout (fn [])}) -(def testfairy #js {}) - diff --git a/react-native/src/mobile/status_im/react_native/js_dependencies.cljs b/react-native/src/mobile/status_im/react_native/js_dependencies.cljs deleted file mode 100644 index c37d1977d93..00000000000 --- a/react-native/src/mobile/status_im/react_native/js_dependencies.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.react-native.js-dependencies) - -(def config (js/require "react-native-config")) -(def fs (js/require "react-native-fs")) -(def http-bridge (js/require "react-native-http-bridge")) -(def keychain (js/require "react-native-keychain")) -(def qr-code (js/require "react-native-qrcode")) -(def react-native (js/require "react-native")) -(def realm (js/require "realm")) -(def webview-bridge (js/require "react-native-webview-bridge")) -(def secure-random (.-generateSecureRandom (js/require "react-native-securerandom"))) -(def EventEmmiter (js/require "react-native/Libraries/vendor/emitter/EventEmitter")) -(def fetch (.-default (js/require "react-native-fetch-polyfill"))) -(def i18n (.-default (js/require "react-native-i18n"))) -(def camera (js/require "react-native-camera")) -(def dialogs (js/require "react-native-dialogs")) -(def dismiss-keyboard (js/require "dismissKeyboard")) -(def image-crop-picker (js/require "react-native-image-crop-picker")) -(def image-resizer (js/require "react-native-image-resizer")) -(def instabug (js/require "instabug-reactnative")) -(def svg (js/require "react-native-svg")) -(def react-native-fcm (js/require "react-native-fcm")) -(def snoopy (js/require "rn-snoopy")) -(def snoopy-filter (js/require "rn-snoopy/stream/filter")) -(def snoopy-bars (js/require "rn-snoopy/stream/bars")) -(def snoopy-buffer (js/require "rn-snoopy/stream/buffer")) -(def background-timer (.-default (js/require "react-native-background-timer"))) -(def testfairy (js/require "react-native-testfairy")) diff --git a/resources/config/fleets.json b/resources/config/fleets.json new file mode 100644 index 00000000000..21a1a906d44 --- /dev/null +++ b/resources/config/fleets.json @@ -0,0 +1,230 @@ +{ + "meta": { + "hostname": "node-01.do-ams3.sites.misc", + "timestamp": "2024-05-21T00:00:40.992919", + "warning": "DO NOT EDIT! Auto generated from Consul. Should only be used at build time." + }, + "fleets": { + "eth.prod": { + "boot": { + "boot-01.do-ams3.eth.prod": "enode://436cc6f674928fdc9a9f7990f2944002b685d1c37f025c1be425185b5b1f0900feaf1ccc2a6130268f9901be4a7d252f37302c8335a2c1a62736e9232691cc3a@178.128.138.128:443", + "boot-02.do-ams3.eth.prod": "enode://5395aab7833f1ecb671b59bf0521cf20224fe8162fc3d2675de4ee4d5636a75ec32d13268fc184df8d1ddfa803943906882da62a4df42d4fccf6d17808156a87@178.128.140.188:443", + "boot-01.gc-us-central1-a.eth.prod": "enode://32ff6d88760b0947a3dee54ceff4d8d7f0b4c023c6dad34568615fcae89e26cc2753f28f12485a4116c977be937a72665116596265aa0736b53d46b27446296a@34.70.75.208:443", + "boot-02.gc-us-central1-a.eth.prod": "enode://5405c509df683c962e7c9470b251bb679dd6978f82d5b469f1f6c64d11d50fbd5dd9f7801c6ad51f3b20a5f6c7ffe248cc9ab223f8bcbaeaf14bb1c0ef295fd0@35.223.215.156:443", + "boot-01.ac-cn-hongkong-c.eth.prod": "enode://6e6554fb3034b211398fcd0f0082cbb6bd13619e1a7e76ba66e1809aaa0c5f1ac53c9ae79cf2fd4a7bacb10d12010899b370c75fed19b991d9c0cdd02891abad@47.75.99.169:443", + "boot-02.ac-cn-hongkong-c.eth.prod": "enode://23d0740b11919358625d79d4cac7d50a34d79e9c69e16831c5c70573757a1f5d7d884510bc595d7ee4da3c1508adf87bbc9e9260d804ef03f8c1e37f2fb2fc69@47.52.106.107:443" + }, + "mail": { + "mail-01.do-ams3.eth.prod": "enode://c42f368a23fa98ee546fd247220759062323249ef657d26d357a777443aec04db1b29a3a22ef3e7c548e18493ddaf51a31b0aed6079bd6ebe5ae838fcfaf3a49@178.128.142.54:443", + "mail-02.do-ams3.eth.prod": "enode://7aa648d6e855950b2e3d3bf220c496e0cae4adfddef3e1e6062e6b177aec93bc6cdcf1282cb40d1656932ebfdd565729da440368d7c4da7dbd4d004b1ac02bf8@178.128.142.26:443", + "mail-03.do-ams3.eth.prod": "enode://8a64b3c349a2e0ef4a32ea49609ed6eb3364be1110253c20adc17a3cebbc39a219e5d3e13b151c0eee5d8e0f9a8ba2cd026014e67b41a4ab7d1d5dd67ca27427@178.128.142.94:443", + "mail-01.gc-us-central1-a.eth.prod": "enode://ee2b53b0ace9692167a410514bca3024695dbf0e1a68e1dff9716da620efb195f04a4b9e873fb9b74ac84de801106c465b8e2b6c4f0d93b8749d1578bfcaf03e@104.197.238.144:443", + "mail-02.gc-us-central1-a.eth.prod": "enode://30211cbd81c25f07b03a0196d56e6ce4604bb13db773ff1c0ea2253547fafd6c06eae6ad3533e2ba39d59564cfbdbb5e2ce7c137a5ebb85e99dcfc7a75f99f55@23.236.58.92:443", + "mail-03.gc-us-central1-a.eth.prod": "enode://44160e22e8b42bd32a06c1532165fa9e096eebedd7fa6d6e5f8bbef0440bc4a4591fe3651be68193a7ec029021cdb496cfe1d7f9f1dc69eb99226e6f39a7a5d4@35.225.221.245:443", + "mail-01.ac-cn-hongkong-c.eth.prod": "enode://606ae04a71e5db868a722c77a21c8244ae38f1bd6e81687cc6cfe88a3063fa1c245692232f64f45bd5408fed5133eab8ed78049332b04f9c110eac7f71c1b429@47.75.247.214:443", + "mail-02.ac-cn-hongkong-c.eth.prod": "enode://2c8de3cbb27a3d30cbb5b3e003bc722b126f5aef82e2052aaef032ca94e0c7ad219e533ba88c70585ebd802de206693255335b100307645ab5170e88620d2a81@47.244.221.14:443", + "mail-03.ac-cn-hongkong-c.eth.prod": "enode://e85f1d4209f2f99da801af18db8716e584a28ad0bdc47fbdcd8f26af74dbd97fc279144680553ec7cd9092afe683ddea1e0f9fc571ebcb4b1d857c03a088853d@47.244.129.82:443" + }, + "whisper": { + "node-01.do-ams3.eth.prod": "enode://66ba15600cda86009689354c3a77bdf1a97f4f4fb3ab50ffe34dbc904fac561040496828397be18d9744c75881ffc6ac53729ddbd2cdbdadc5f45c400e2622f7@178.128.141.87:443", + "node-02.do-ams3.eth.prod": "enode://4ea35352702027984a13274f241a56a47854a7fd4b3ba674a596cff917d3c825506431cf149f9f2312a293bb7c2b1cca55db742027090916d01529fe0729643b@134.209.136.79:443", + "node-03.do-ams3.eth.prod": "enode://ce559a37a9c344d7109bd4907802dd690008381d51f658c43056ec36ac043338bd92f1ac6043e645b64953b06f27202d679756a9c7cf62fdefa01b2e6ac5098e@134.209.136.123:443", + "node-04.do-ams3.eth.prod": "enode://4e0a8db9b73403c9339a2077e911851750fc955db1fc1e09f81a4a56725946884dd5e4d11258eac961f9078a393c45bcab78dd0e3bc74e37ce773b3471d2e29c@134.209.136.101:443", + "node-05.do-ams3.eth.prod": "enode://a8bddfa24e1e92a82609b390766faa56cf7a5eef85b22a2b51e79b333c8aaeec84f7b4267e432edd1cf45b63a3ad0fc7d6c3a16f046aa6bc07ebe50e80b63b8c@178.128.141.249:443", + "node-06.do-ams3.eth.prod": "enode://887cbd92d95afc2c5f1e227356314a53d3d18855880ac0509e0c0870362aee03939d4074e6ad31365915af41d34320b5094bfcc12a67c381788cd7298d06c875@178.128.141.0:443", + "node-07.do-ams3.eth.prod": "enode://2b01955d7e11e29dce07343b456e4e96c081760022d1652b1c4b641eaf320e3747871870fa682e9e9cfb85b819ce94ed2fee1ac458904d54fd0b97d33ba2c4a4@134.209.136.112:443", + "node-08.do-ams3.eth.prod": "enode://0d9d65fcd5592df33ed4507ce862b9c748b6dbd1ea3a1deb94e3750052760b4850aa527265bbaf357021d64d5cc53c02b410458e732fafc5b53f257944247760@178.128.141.42:443", + "node-01.gc-us-central1-a.eth.prod": "enode://182ed5d658d1a1a4382c9e9f7c9e5d8d9fec9db4c71ae346b9e23e1a589116aeffb3342299bdd00e0ab98dbf804f7b2d8ae564ed18da9f45650b444aed79d509@34.68.132.118:443", + "node-02.gc-us-central1-a.eth.prod": "enode://fbeddac99d396b91d59f2c63a3cb5fc7e0f8a9f7ce6fe5f2eed5e787a0154161b7173a6a73124a4275ef338b8966dc70a611e9ae2192f0f2340395661fad81c0@34.67.230.193:443", + "node-03.gc-us-central1-a.eth.prod": "enode://c07aa0deea3b7056c5d45a85bca42f0d8d3b1404eeb9577610f386e0a4744a0e7b2845ae328efc4aa4b28075af838b59b5b3985bffddeec0090b3b7669abc1f3@35.226.92.155:443", + "node-04.gc-us-central1-a.eth.prod": "enode://0624b4a90063923c5cc27d12624b6a49a86dfb3623fcb106801217fdbab95f7617b83fa2468b9ae3de593ff6c1cf556ccf9bc705bfae9cb4625999765127b423@35.222.158.246:443", + "node-05.gc-us-central1-a.eth.prod": "enode://a5fe9c82ad1ffb16ae60cb5d4ffe746b9de4c5fbf20911992b7dd651b1c08ba17dd2c0b27ee6b03162c52d92f219961cc3eb14286aca8a90b75cf425826c3bd8@104.154.230.58:443", + "node-06.gc-us-central1-a.eth.prod": "enode://282e009967f9f132a5c2dd366a76319f0d22d60d0c51f7e99795a1e40f213c2705a2c10e4cc6f3890319f59da1a535b8835ed9b9c4b57c3aad342bf312fd7379@35.223.240.17:443", + "node-07.gc-us-central1-a.eth.prod": "enode://b706a60572634760f18a27dd407b2b3582f7e065110dae10e3998498f1ae3f29ba04db198460d83ed6d2bfb254bb06b29aab3c91415d75d3b869cd0037f3853c@35.239.5.162:443", + "node-08.gc-us-central1-a.eth.prod": "enode://e87f1d8093d304c3a9d6f1165b85d6b374f1c0cc907d39c0879eb67f0a39d779be7a85cbd52920b6f53a94da43099c58837034afa6a7be4b099bfcd79ad13999@35.238.106.101:443", + "node-01.ac-cn-hongkong-c.eth.prod": "enode://b957e51f41e4abab8382e1ea7229e88c6e18f34672694c6eae389eac22dab8655622bbd4a08192c321416b9becffaab11c8e2b7a5d0813b922aa128b82990dab@47.75.222.178:443", + "node-02.ac-cn-hongkong-c.eth.prod": "enode://8bebe73ddf7cf09e77602c7d04c93a73f455b51f24ae0d572917a4792f1dec0bb4c562759b8830cc3615a658d38c1a4a38597a1d7ae3ba35111479fc42d65dec@47.75.85.212:443", + "node-03.ac-cn-hongkong-c.eth.prod": "enode://ac3948b2c0786ada7d17b80cf869cf59b1909ea3accd45944aae35bf864cc069126da8b82dfef4ddf23f1d6d6b44b1565c4cf81c8b98022253c6aea1a89d3ce2@47.75.88.12:443", + "node-04.ac-cn-hongkong-c.eth.prod": "enode://385579fc5b14e04d5b04af7eee835d426d3d40ccf11f99dbd95340405f37cf3bbbf830b3eb8f70924be0c2909790120682c9c3e791646e2d5413e7801545d353@47.244.221.249:443", + "node-05.ac-cn-hongkong-c.eth.prod": "enode://b77bffc29e2592f30180311dd81204ab845e5f78953b5ba0587c6631be9c0862963dea5eb64c90617cf0efd75308e22a42e30bc4eb3cd1bbddbd1da38ff6483e@47.75.10.177:443", + "node-06.ac-cn-hongkong-c.eth.prod": "enode://cf5f7a7e64e3b306d1bc16073fba45be3344cb6695b0b616ccc2da66ea35b9f35b3b231c6cf335fdfaba523519659a440752fc2e061d1e5bc4ef33864aac2f19@47.75.221.196:443", + "node-07.ac-cn-hongkong-c.eth.prod": "enode://13d63a1f85ccdcbd2fb6861b9bd9d03f94bdba973608951f7c36e5df5114c91de2b8194d71288f24bfd17908c48468e89dd8f0fb8ccc2b2dedae84acdf65f62a@47.244.210.80:443", + "node-08.ac-cn-hongkong-c.eth.prod": "enode://32915c8841faaef21a6b75ab6ed7c2b6f0790eb177ad0f4ea6d731bacc19b938624d220d937ebd95e0f6596b7232bbb672905ee12601747a12ee71a15bfdf31c@47.75.59.11:443" + }, + "rendezvous": { + "boot-01.do-ams3.eth.prod": "/ip4/178.128.138.128/tcp/30703/ethv4/16Uiu2HAmRHPzF3rQg55PgYPcQkyvPVH9n2hWsYPhUJBZ6kVjJgdV", + "boot-02.do-ams3.eth.prod": "/ip4/178.128.140.188/tcp/30703/ethv4/16Uiu2HAmLqTXuY4Sb6G28HNooaFUXUKzpzKXCcgyJxgaEE2i5vnf", + "boot-01.gc-us-central1-a.eth.prod": "/ip4/34.70.75.208/tcp/30703/ethv4/16Uiu2HAm6ZsERLx2BwVD2UM9SVPnnMU6NBycG8XPtu8qKys5awsU", + "boot-02.gc-us-central1-a.eth.prod": "/ip4/35.223.215.156/tcp/30703/ethv4/16Uiu2HAmQEUFE2YaJohavWtHxPTEFv3sEGJtDqvtGEv78DFoEWQF", + "boot-01.ac-cn-hongkong-c.eth.prod": "/ip4/47.75.99.169/tcp/30703/ethv4/16Uiu2HAmV8Hq9e3zm9TMVP4zrVHo3BjqW5D6bDVV6VQntQd687e4", + "boot-02.ac-cn-hongkong-c.eth.prod": "/ip4/47.52.106.107/tcp/30703/ethv4/16Uiu2HAmEHiptiDDd9gqNY8oQqo8hHUWMHJzfwt5aLRdD6W2zcXR" + } + }, + "waku.test": { + "tcp/p2p/waku": { + "node-01.do-ams3.waku.test": "/dns4/node-01.do-ams3.waku.test.status.im/tcp/30303/p2p/16Uiu2HAkykgaECHswi3YKJ5dMLbq2kPVCo89fcyTd38UcQD6ej5W", + "node-01.gc-us-central1-a.waku.test": "/dns4/node-01.gc-us-central1-a.waku.test.status.im/tcp/30303/p2p/16Uiu2HAmDCp8XJ9z1ev18zuv8NHekAsjNyezAvmMfFEJkiharitG", + "node-01.ac-cn-hongkong-c.waku.test": "/dns4/node-01.ac-cn-hongkong-c.waku.test.status.im/tcp/30303/p2p/16Uiu2HAkzHaTP5JsUwfR9NR8Rj9HC24puS6ocaU8wze4QrXr9iXp" + }, + "enr/p2p/waku": { + "node-01.do-ams3.waku.test": "enr:-QEMuEDbayK340kH24XzK5FPIYNzWNYuH01NASNIb1skZfe_6l4_JSsG-vZ0LgN4Cgzf455BaP5zrxMQADHL5OQpbW6OAYJpZIJ2NIJpcISygI2rim11bHRpYWRkcnO4VgAoNiNub2RlLTAxLmRvLWFtczMud2FrdS50ZXN0LnN0YXR1cy5pbQZ2XwAqNiNub2RlLTAxLmRvLWFtczMud2FrdS50ZXN0LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQJATXRSRSUyTw_QLB6H_U3oziVQgNRgrXpK7wp2AMyNxYN0Y3CCdl-DdWRwgiMohXdha3UyDw", + "node-01.gc-us-central1-a.waku.test": "enr:-QEeuEBO08GSjWDOV13HTf6L7iFoPQhv4S0-_Bd7Of3lFCBNBmpB9j6pGLedkX88KAXm6BFCS4ViQ_rLeDQuzj9Q6fs9AYJpZIJ2NIJpcIQiEAFDim11bHRpYWRkcnO4aAAxNixub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS50ZXN0LnN0YXR1cy5pbQZ2XwAzNixub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS50ZXN0LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQMIJwesBVgUiBCi8yiXGx7RWylBQkYm1U9dvEy-neLG2YN0Y3CCdl-DdWRwgiMohXdha3UyDw", + "node-01.ac-cn-hongkong-c.waku.test": "enr:-QEeuECvvBe6kIzHgMv_mD1YWQ3yfOfid2MO9a_A6ZZmS7E0FmAfntz2ZixAnPXvLWDJ81ARp4oV9UM4WXyc5D5USdEPAYJpZIJ2NIJpcIQI2ttrim11bHRpYWRkcnO4aAAxNixub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS50ZXN0LnN0YXR1cy5pbQZ2XwAzNixub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS50ZXN0LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQJIN4qwz3v4r2Q8Bv8zZD0eqBcKw6bdLvdkV7-JLjqIj4N0Y3CCdl-DdWRwgiMohXdha3UyDw" + }, + "wss/p2p/waku": { + "node-01.do-ams3.waku.test": "/dns4/node-01.do-ams3.waku.test.status.im/tcp/8000/wss/p2p/16Uiu2HAkykgaECHswi3YKJ5dMLbq2kPVCo89fcyTd38UcQD6ej5W", + "node-01.gc-us-central1-a.waku.test": "/dns4/node-01.gc-us-central1-a.waku.test.status.im/tcp/8000/wss/p2p/16Uiu2HAmDCp8XJ9z1ev18zuv8NHekAsjNyezAvmMfFEJkiharitG", + "node-01.ac-cn-hongkong-c.waku.test": "/dns4/node-01.ac-cn-hongkong-c.waku.test.status.im/tcp/8000/wss/p2p/16Uiu2HAkzHaTP5JsUwfR9NR8Rj9HC24puS6ocaU8wze4QrXr9iXp" + } + }, + "waku.sandbox": { + "tcp/p2p/waku": { + "node-01.do-ams3.waku.sandbox": "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmQSMNExfUYUqfuXWkD5DaNZnMYnigRxFKbk3tcEFQeQeE", + "node-01.gc-us-central1-a.waku.sandbox": "/dns4/node-01.gc-us-central1-a.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAm6fyqE1jB5MonzvoMdU8v76bWV8ZeNpncDamY1MQXfjdB", + "node-01.ac-cn-hongkong-c.waku.sandbox": "/dns4/node-01.ac-cn-hongkong-c.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmSJvSJphxRdbnigUV5bjRRZFBhTtWFTSyiKaQByCjwmpV" + }, + "enr/p2p/waku": { + "node-01.do-ams3.waku.sandbox": "enr:-QESuEB4Dchgjn7gfAvwB00CxTA-nGiyk-aALI-H4dYSZD3rUk7bZHmP8d2U6xDiQ2vZffpo45Jp7zKNdnwDUx6g4o6XAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOvD3S3jUNICsrOILlmhENiWAMmMVlAl6-Q8wRB7hidY4N0Y3CCdl-DdWRwgiMohXdha3UyDw", + "node-01.gc-us-central1-a.waku.sandbox": "enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw", + "node-01.ac-cn-hongkong-c.waku.sandbox": "enr:-QEkuEB3WHNS-xA3RDpfu9A2Qycr3bN3u7VoArMEiDIFZJ66F1EB3d4wxZN1hcdcOX-RfuXB-MQauhJGQbpz3qUofOtLAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPK35Nnz0cWUtSAhBp7zvHEhyU_AqeQUlqzLiLxfP2L4oN0Y3CCdl-DdWRwgiMohXdha3UyDw" + }, + "wss/p2p/waku": { + "node-01.do-ams3.waku.sandbox": "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/8000/wss/p2p/16Uiu2HAmQSMNExfUYUqfuXWkD5DaNZnMYnigRxFKbk3tcEFQeQeE", + "node-01.gc-us-central1-a.waku.sandbox": "/dns4/node-01.gc-us-central1-a.waku.sandbox.status.im/tcp/8000/wss/p2p/16Uiu2HAm6fyqE1jB5MonzvoMdU8v76bWV8ZeNpncDamY1MQXfjdB", + "node-01.ac-cn-hongkong-c.waku.sandbox": "/dns4/node-01.ac-cn-hongkong-c.waku.sandbox.status.im/tcp/8000/wss/p2p/16Uiu2HAmSJvSJphxRdbnigUV5bjRRZFBhTtWFTSyiKaQByCjwmpV" + } + }, + "status.test": { + "tcp/p2p/waku": { + "node-01.do-ams3.status.test": "/dns4/node-01.do-ams3.status.test.status.im/tcp/30303/p2p/16Uiu2HAkukebeXjTQ9QDBeNDWuGfbaSg79wkkhK4vPocLgR6QFDf", + "node-01.gc-us-central1-a.status.test": "/dns4/node-01.gc-us-central1-a.status.test.status.im/tcp/30303/p2p/16Uiu2HAmGDX3iAFox93PupVYaHa88kULGqMpJ7AEHGwj3jbMtt76", + "node-01.ac-cn-hongkong-c.status.test": "/dns4/node-01.ac-cn-hongkong-c.status.test.status.im/tcp/30303/p2p/16Uiu2HAm2BjXxCp1sYFJQKpLLbPbwd5juxbsYofu3TsS3auvT9Yi" + }, + "enr/p2p/waku": { + "node-01.do-ams3.status.test": "enr:-Pm4QE-rdAE-X76ruaDgYa5te0eHZ1tNtT7s2hsk0OE-06pCS5Rc4cYF_izHLHyEUUTzYmInLzB7xxhgrsYa9OUzDhQBgmlkgnY0gmlwhEDhUe2KbXVsdGlhZGRyc7haACo2JW5vZGUtMDEuZG8tYW1zMy5zdGF0dXMudGVzdC5zdGF0dXMuaW0Gdl8ALDYlbm9kZS0wMS5kby1hbXMzLnN0YXR1cy50ZXN0LnN0YXR1cy5pbQYBu94DiXNlY3AyNTZrMaECBNx5Erw5Jdsw-uI9pBjB_V2R6brvuVWE-MJwYCQKVv6DdGNwgnZfg3VkcIIjKIV3YWt1Mg8", + "node-01.gc-us-central1-a.status.test": "enr:-QELuEA5PjqjeMw4Uv7hti0d3hw4jc2vANqlFq9ko6mbUo6bHC_ugxLdNMVhjOKIMtHvQjcwc6vy0ZWHnbDvVZIQrFD4AYJpZIJ2NIJpcIQievx2im11bHRpYWRkcnO4bAAzNi5ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEuc3RhdHVzLnRlc3Quc3RhdHVzLmltBnZfADU2Lm5vZGUtMDEuZ2MtdXMtY2VudHJhbDEtYS5zdGF0dXMudGVzdC5zdGF0dXMuaW0GAbveA4lzZWNwMjU2azGhAzToWxPoQnnubNH_CIu7YHPV0RHadQVIKlpfMJxxC2eFg3RjcIJ2X4N1ZHCCIyiFd2FrdTIP", + "node-01.ac-cn-hongkong-c.status.test": "enr:-QELuEAbXz6W8UgujhK2lktjDCg9kjgB0wmPuRDlQz4-PQubnRCJlhDIrwhOuJ6Ecz4m_Rb5qkvHOT4Dg-V33nr1vlIYAYJpZIJ2NIJpcIQv8ukkim11bHRpYWRkcnO4bAAzNi5ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMuc3RhdHVzLnRlc3Quc3RhdHVzLmltBnZfADU2Lm5vZGUtMDEuYWMtY24taG9uZ2tvbmctYy5zdGF0dXMudGVzdC5zdGF0dXMuaW0GAbveA4lzZWNwMjU2azGhAmRvsjqPlfX7OOJ4x4F3GAOpBa4y4NQSoNHjX2XIo1lvg3RjcIJ2X4N1ZHCCIyiFd2FrdTIP" + }, + "wss/p2p/waku": { + "node-01.do-ams3.status.test": "/dns4/node-01.do-ams3.status.test.status.im/tcp/443/wss/p2p/16Uiu2HAkukebeXjTQ9QDBeNDWuGfbaSg79wkkhK4vPocLgR6QFDf", + "node-01.gc-us-central1-a.status.test": "/dns4/node-01.gc-us-central1-a.status.test.status.im/tcp/443/wss/p2p/16Uiu2HAmGDX3iAFox93PupVYaHa88kULGqMpJ7AEHGwj3jbMtt76", + "node-01.ac-cn-hongkong-c.status.test": "/dns4/node-01.ac-cn-hongkong-c.status.test.status.im/tcp/443/wss/p2p/16Uiu2HAm2BjXxCp1sYFJQKpLLbPbwd5juxbsYofu3TsS3auvT9Yi" + } + }, + "status.prod": { + "tcp/p2p/waku": { + "node-01.do-ams3.status.prod": "/dns4/node-01.do-ams3.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAm6HZZr7aToTvEBPpiys4UxajCTU97zj5v7RNR2gbniy1D", + "node-02.do-ams3.status.prod": "/dns4/node-02.do-ams3.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmSve7tR5YZugpskMv2dmJAsMUKmfWYEKRXNUxRaTCnsXV", + "node-01.gc-us-central1-a.status.prod": "/dns4/node-01.gc-us-central1-a.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAkwBp8T6G77kQXSNMnxgaMky1JeyML5yqoTHRM8dbeCBNb", + "node-02.gc-us-central1-a.status.prod": "/dns4/node-02.gc-us-central1-a.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmDQugwDHM3YeUp86iGjrUvbdw3JPRgikC7YoGBsT2ymMg", + "node-01.ac-cn-hongkong-c.status.prod": "/dns4/node-01.ac-cn-hongkong-c.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAkvEZgh3KLwhLwXg95e5ojM8XykJ4Kxi2T7hk22rnA7pJC", + "node-02.ac-cn-hongkong-c.status.prod": "/dns4/node-02.ac-cn-hongkong-c.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmFy8BrJhCEmCYrUfBdSNkrPw6VHExtv4rRp1DSBnCPgx8" + }, + "enr/p2p/waku": { + "node-01.do-ams3.status.prod": "enr:-P-4QGVNANzbhCI49du6Moyw98AjuMhKoOpE_Jges9JlCq-ICAVadktjfcNpuhQgT0g1cu86_S3nbM7eYkCsqDAQG7UBgmlkgnY0gmlwhI_G-a6KbXVsdGlhZGRyc7hgAC02KG5vZGUtMDEuZG8tYW1zMy5zdGF0dXMucHJvZC5zdGF0dXNpbS5uZXQGdl8ALzYobm9kZS0wMS5kby1hbXMzLnN0YXR1cy5wcm9kLnN0YXR1c2ltLm5ldAYBu94DiXNlY3AyNTZrMaECoVyonsTGEQvVioM562Q1fjzTb_vKD152PPIdsV7sM6SDdGNwgnZfg3VkcIIjKIV3YWt1Mg8", + "node-02.do-ams3.status.prod": "enr:-P-4QMXNung2KVE9xLMzctM754vz4kbavb4Stai3RBPfN-5XMfuf2i0x8J5l8fkxkiZ3Vpd0IkN9Gg0PbbDh2BUyIF8BgmlkgnY0gmlwhKEj9HmKbXVsdGlhZGRyc7hgAC02KG5vZGUtMDIuZG8tYW1zMy5zdGF0dXMucHJvZC5zdGF0dXNpbS5uZXQGdl8ALzYobm9kZS0wMi5kby1hbXMzLnN0YXR1cy5wcm9kLnN0YXR1c2ltLm5ldAYBu94DiXNlY3AyNTZrMaED1AYI2Ox27DnSqf2qoih5M2fNpHFq-OzJ3thREEApdiiDdGNwgnZfg3VkcIIjKIV3YWt1Mg8", + "node-01.gc-us-central1-a.status.prod": "enr:-QERuEAFZ-rlajEo617uz0NDycgW4cPqqBumBvTOWywyFzIg4UOmbctTjYi6sfMArMgJEIDpphaXpLkCt5r12_gmtDQlAYJpZIJ2NIJpcIQjyjeZim11bHRpYWRkcnO4cgA2NjFub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEuc3RhdHVzLnByb2Quc3RhdHVzaW0ubmV0BnZfADg2MW5vZGUtMDEuZ2MtdXMtY2VudHJhbDEtYS5zdGF0dXMucHJvZC5zdGF0dXNpbS5uZXQGAbveA4lzZWNwMjU2azGhAhoqdPkAZNvNRuzEecDGwnt5UbF98qwM16PZG3JmW6kUg3RjcIJ2X4N1ZHCCIyiFd2FrdTIP", + "node-02.gc-us-central1-a.status.prod": "enr:-QERuEA_42Fh7Kq3drLxfIuRkqMfoJpfKpjwRcUXLv1PUQdUt1AAhIOUpun_kHKAMBmrREU9y2VoTKykl0dPBw02Fbh-AYJpZIJ2NIJpcIQihNXpim11bHRpYWRkcnO4cgA2NjFub2RlLTAyLmdjLXVzLWNlbnRyYWwxLWEuc3RhdHVzLnByb2Quc3RhdHVzaW0ubmV0BnZfADg2MW5vZGUtMDIuZ2MtdXMtY2VudHJhbDEtYS5zdGF0dXMucHJvZC5zdGF0dXNpbS5uZXQGAbveA4lzZWNwMjU2azGhAwtASPpiz5Gq3zuFuWF4MTviQVipi3HaQGytF-4wfbdvg3RjcIJ2X4N1ZHCCIyiFd2FrdTIP", + "node-01.ac-cn-hongkong-c.status.prod": "enr:-QERuEAVmLHSKOsMq2Atya-0mhdvu718BAcynh40JAPBprvhpgcsTqkXV5kRrxaZbtzKa5-b0I4aO9U_O7-zHe24UDMAAYJpZIJ2NIJpcIQv8so7im11bHRpYWRkcnO4cgA2NjFub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMuc3RhdHVzLnByb2Quc3RhdHVzaW0ubmV0BnZfADg2MW5vZGUtMDEuYWMtY24taG9uZ2tvbmctYy5zdGF0dXMucHJvZC5zdGF0dXNpbS5uZXQGAbveA4lzZWNwMjU2azGhAgwDLi50TMXqxWTCnR_rjQP3Eeznjs645ZNBoRh5B3jhg3RjcIJ2X4N1ZHCCIyiFd2FrdTIL", + "node-02.ac-cn-hongkong-c.status.prod": "enr:-QERuECscpffU15jE9GD77vwwKCeAWn8jBl7IXS9fMj8FcY6YzBPNNr17pXuOHPqhLbieZxM3zc0AMWDa5tWqpfbtzMKAYJpZIJ2NIJpcIQv84CGim11bHRpYWRkcnO4cgA2NjFub2RlLTAyLmFjLWNuLWhvbmdrb25nLWMuc3RhdHVzLnByb2Quc3RhdHVzaW0ubmV0BnZfADg2MW5vZGUtMDIuYWMtY24taG9uZ2tvbmctYy5zdGF0dXMucHJvZC5zdGF0dXNpbS5uZXQGAbveA4lzZWNwMjU2azGhAzE4YIBrPK5oT_9MvYEeggmaDd205iU4Nn1MAhU5xqY5g3RjcIJ2X4N1ZHCCIyiFd2FrdTIP" + }, + "wss/p2p/waku": { + "node-01.do-ams3.status.prod": "/dns4/node-01.do-ams3.status.prod.statusim.net/tcp/443/wss/p2p/16Uiu2HAm6HZZr7aToTvEBPpiys4UxajCTU97zj5v7RNR2gbniy1D", + "node-02.do-ams3.status.prod": "/dns4/node-02.do-ams3.status.prod.statusim.net/tcp/443/wss/p2p/16Uiu2HAmSve7tR5YZugpskMv2dmJAsMUKmfWYEKRXNUxRaTCnsXV", + "node-01.gc-us-central1-a.status.prod": "/dns4/node-01.gc-us-central1-a.status.prod.statusim.net/tcp/443/wss/p2p/16Uiu2HAkwBp8T6G77kQXSNMnxgaMky1JeyML5yqoTHRM8dbeCBNb", + "node-02.gc-us-central1-a.status.prod": "/dns4/node-02.gc-us-central1-a.status.prod.statusim.net/tcp/443/wss/p2p/16Uiu2HAmDQugwDHM3YeUp86iGjrUvbdw3JPRgikC7YoGBsT2ymMg", + "node-01.ac-cn-hongkong-c.status.prod": "/dns4/node-01.ac-cn-hongkong-c.status.prod.statusim.net/tcp/443/wss/p2p/16Uiu2HAkvEZgh3KLwhLwXg95e5ojM8XykJ4Kxi2T7hk22rnA7pJC", + "node-02.ac-cn-hongkong-c.status.prod": "/dns4/node-02.ac-cn-hongkong-c.status.prod.statusim.net/tcp/443/wss/p2p/16Uiu2HAmFy8BrJhCEmCYrUfBdSNkrPw6VHExtv4rRp1DSBnCPgx8" + } + }, + "shards.test": { + "tcp/p2p/waku/boot": { + "boot-01.do-ams3.shards.test": "/dns4/boot-01.do-ams3.shards.test.status.im/tcp/30303/p2p/16Uiu2HAmAR24Mbb6VuzoyUiGx42UenDkshENVDj4qnmmbabLvo31", + "boot-01.gc-us-central1-a.shards.test": "/dns4/boot-01.gc-us-central1-a.shards.test.status.im/tcp/30303/p2p/16Uiu2HAm8mUZ18tBWPXDQsaF7PbCKYA35z7WB2xNZH2EVq1qS8LJ", + "boot-01.ac-cn-hongkong-c.shards.test": "/dns4/boot-01.ac-cn-hongkong-c.shards.test.status.im/tcp/30303/p2p/16Uiu2HAmGwcE8v7gmJNEWFtZtojYpPMTHy2jBLL6xRk33qgDxFWX" + }, + "enr/p2p/waku/boot": { + "boot-01.do-ams3.shards.test": "enr:-QEKuECA0zhRJej2eaOoOPddNcYr7-5NdRwuoLCe2EE4wfEYkAZhFotg6Kkr8K15pMAGyUyt0smHkZCjLeld0BUzogNtAYJpZIJ2NIJpcISnYxMvim11bHRpYWRkcnO4WgAqNiVib290LTAxLmRvLWFtczMuc2hhcmRzLnRlc3Quc3RhdHVzLmltBnZfACw2JWJvb3QtMDEuZG8tYW1zMy5zaGFyZHMudGVzdC5zdGF0dXMuaW0GAbveA4Jyc40AEAUAAQAgAEAAgAEAiXNlY3AyNTZrMaEC3rRtFQSgc24uWewzXaxTY8hDAHB8sgnxr9k8Rjb5GeSDdGNwgnZfg3VkcIIjKIV3YWt1Mg0", + "boot-01.gc-us-central1-a.shards.test": "enr:-QEcuEAgXDqrYd_TrpUWtn3zmxZ9XPm7O3GS6lV7aMJJOTsbOAAeQwSd_eoHcCXqVzTUtwTyB4855qtbd8DARnExyqHPAYJpZIJ2NIJpcIQihw1Xim11bHRpYWRkcnO4bAAzNi5ib290LTAxLmdjLXVzLWNlbnRyYWwxLWEuc2hhcmRzLnRlc3Quc3RhdHVzLmltBnZfADU2LmJvb3QtMDEuZ2MtdXMtY2VudHJhbDEtYS5zaGFyZHMudGVzdC5zdGF0dXMuaW0GAbveA4Jyc40AEAUAAQAgAEAAgAEAiXNlY3AyNTZrMaECxjqgDQ0WyRSOilYU32DA5k_XNlDis3m1VdXkK9xM6kODdGNwgnZfg3VkcIIjKIV3YWt1Mg0", + "boot-01.ac-cn-hongkong-c.shards.test": "enr:-QEcuEAX6Qk-vVAoJLxR4A_4UVogGhvQrqKW4DFKlf8MA1PmCjgowL-LBtSC9BLjXbb8gf42FdDHGtSjEvvWKD10erxqAYJpZIJ2NIJpcIQI2hdMim11bHRpYWRkcnO4bAAzNi5ib290LTAxLmFjLWNuLWhvbmdrb25nLWMuc2hhcmRzLnRlc3Quc3RhdHVzLmltBnZfADU2LmJvb3QtMDEuYWMtY24taG9uZ2tvbmctYy5zaGFyZHMudGVzdC5zdGF0dXMuaW0GAbveA4Jyc40AEAUAAQAgAEAAgAEAiXNlY3AyNTZrMaEDP7CbRk-YKJwOFFM4Z9ney0GPc7WPJaCwGkpNRyla7mCDdGNwgnZfg3VkcIIjKIV3YWt1Mg0" + }, + "wss/p2p/waku/boot": { + "boot-01.do-ams3.shards.test": "/dns4/boot-01.do-ams3.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmAR24Mbb6VuzoyUiGx42UenDkshENVDj4qnmmbabLvo31", + "boot-01.gc-us-central1-a.shards.test": "/dns4/boot-01.gc-us-central1-a.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAm8mUZ18tBWPXDQsaF7PbCKYA35z7WB2xNZH2EVq1qS8LJ", + "boot-01.ac-cn-hongkong-c.shards.test": "/dns4/boot-01.ac-cn-hongkong-c.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmGwcE8v7gmJNEWFtZtojYpPMTHy2jBLL6xRk33qgDxFWX" + }, + "tcp/p2p/waku/store": { + "store-01.do-ams3.shards.test": "/dns4/store-01.do-ams3.shards.test.status.im/tcp/30303/p2p/16Uiu2HAmAUdrQ3uwzuE4Gy4D56hX6uLKEeerJAnhKEHZ3DxF1EfT", + "store-02.do-ams3.shards.test": "/dns4/store-02.do-ams3.shards.test.status.im/tcp/30303/p2p/16Uiu2HAm9aDJPkhGxc2SFcEACTFdZ91Q5TJjp76qZEhq9iF59x7R", + "store-01.gc-us-central1-a.shards.test": "/dns4/store-01.gc-us-central1-a.shards.test.status.im/tcp/30303/p2p/16Uiu2HAmMELCo218hncCtTvC2Dwbej3rbyHQcR8erXNnKGei7WPZ", + "store-02.gc-us-central1-a.shards.test": "/dns4/store-02.gc-us-central1-a.shards.test.status.im/tcp/30303/p2p/16Uiu2HAmJnVR7ZzFaYvciPVafUXuYGLHPzSUigqAmeNw9nJUVGeM", + "store-01.ac-cn-hongkong-c.shards.test": "/dns4/store-01.ac-cn-hongkong-c.shards.test.status.im/tcp/30303/p2p/16Uiu2HAm2M7xs7cLPc3jamawkEqbr7cUJX11uvY7LxQ6WFUdUKUT", + "store-02.ac-cn-hongkong-c.shards.test": "/dns4/store-02.ac-cn-hongkong-c.shards.test.status.im/tcp/30303/p2p/16Uiu2HAm9CQhsuwPR54q27kNj9iaQVfyRzTGKrhFmr94oD8ujU6P" + }, + "enr/p2p/waku/store": { + "store-01.do-ams3.shards.test": "enr:-QEMuECnqjg1zLOojy1kFMed-XwW9EWdJC4xMpUX7eu88iua0RCoNW3P9NpIpEvIAM2NhByh2WKJhhly45bgnMnRlRMYAYJpZIJ2NIJpcISf3_Jeim11bHRpYWRkcnO4XAArNiZzdG9yZS0wMS5kby1hbXMzLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQZ2XwAtNiZzdG9yZS0wMS5kby1hbXMzLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQLfoaQH3oSYW59yxEBfeAZbltmUnC4BzYkHqer2VQMTyoN0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-02.do-ams3.shards.test": "enr:-QEMuECpoJZkEMRV_n6v3vX7vRYr5v6zFaTiqXCD5vwM9pZDaAVvwF9Ya8snhePUvaAqxENTAcJzB3JrWT5Y7A8Xt12hAYJpZIJ2NIJpcISf3_Kaim11bHRpYWRkcnO4XAArNiZzdG9yZS0wMi5kby1hbXMzLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQZ2XwAtNiZzdG9yZS0wMi5kby1hbXMzLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQLSM62HmqGpZ382YM4CyI-MCIlkxMP7ZbOwqwRPvk9wsIN0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-01.gc-us-central1-a.shards.test": "enr:-QEeuEDv5g_0Hszm2xv8VYxcgRpMXIOyELY5kZXwwA-OBOhxcWE7bza7D-9zW5zgCD09LW3wBqKymelRAuyYHfJIsH7MAYJpZIJ2NIJpcIQiqsAnim11bHRpYWRkcnO4bgA0Ni9zdG9yZS0wMS5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQZ2XwA2Ni9zdG9yZS0wMS5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQN_aBxNsOBrceDLyC75vBFRuzv_tWfaHG50Jc9DQztwkIN0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-02.gc-us-central1-a.shards.test": "enr:-QEeuECPnGIyDXTzgrsZhVYeoer9WrxIw9C2FpN1wdCLuavmtWyiCKAobJD7ksIjFg5A1K-xeti5lmQbisc351iXvpQwAYJpZIJ2NIJpcIQiqpoCim11bHRpYWRkcnO4bgA0Ni9zdG9yZS0wMi5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQZ2XwA2Ni9zdG9yZS0wMi5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQNbEg1bkMJCBiD5Tje3Z_11R-kd9munZF0v4iiYZa1jgoN0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-01.ac-cn-hongkong-c.shards.test": "enr:-QEeuED6AQJ3dfY3sK5fiX3XwLn7FwfucG8Se5aMgHfdaUWiz2SvpAlLbxJFOQOlCYB5YpHO-0RZOY-zhSEiQ6r86zNgAYJpZIJ2NIJpcIQI2kpJim11bHRpYWRkcnO4bgA0Ni9zdG9yZS0wMS5hYy1jbi1ob25na29uZy1jLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQZ2XwA2Ni9zdG9yZS0wMS5hYy1jbi1ob25na29uZy1jLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQJm10jdarzx9hcdhRKGfsAyS0Hc5pWj3yhyTvT5FIwKGIN0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-02.ac-cn-hongkong-c.shards.test": "enr:-QEeuEAYtigUojOmT1QO3RvINpthiFMsv147q9Uz2bk0sGlvERqb8i4OaNujlA8ywHa_cfnEygq2D5DNoo6g-o3KQ_srAYJpZIJ2NIJpcIQI2nnoim11bHRpYWRkcnO4bgA0Ni9zdG9yZS0wMi5hYy1jbi1ob25na29uZy1jLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQZ2XwA2Ni9zdG9yZS0wMi5hYy1jbi1ob25na29uZy1jLnNoYXJkcy50ZXN0LnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQLMncuu6pJ3DQRzYUqkB1PbaRxZXIGJi8waKbbBFbOSNIN0Y3CCdl-DdWRwgiMohXdha3UyAw" + }, + "wss/p2p/waku/store": { + "store-01.do-ams3.shards.test": "/dns4/store-01.do-ams3.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmAUdrQ3uwzuE4Gy4D56hX6uLKEeerJAnhKEHZ3DxF1EfT", + "store-02.do-ams3.shards.test": "/dns4/store-02.do-ams3.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAm9aDJPkhGxc2SFcEACTFdZ91Q5TJjp76qZEhq9iF59x7R", + "store-01.gc-us-central1-a.shards.test": "/dns4/store-01.gc-us-central1-a.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmMELCo218hncCtTvC2Dwbej3rbyHQcR8erXNnKGei7WPZ", + "store-02.gc-us-central1-a.shards.test": "/dns4/store-02.gc-us-central1-a.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmJnVR7ZzFaYvciPVafUXuYGLHPzSUigqAmeNw9nJUVGeM", + "store-01.ac-cn-hongkong-c.shards.test": "/dns4/store-01.ac-cn-hongkong-c.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAm2M7xs7cLPc3jamawkEqbr7cUJX11uvY7LxQ6WFUdUKUT", + "store-02.ac-cn-hongkong-c.shards.test": "/dns4/store-02.ac-cn-hongkong-c.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAm9CQhsuwPR54q27kNj9iaQVfyRzTGKrhFmr94oD8ujU6P" + } + }, + "shards.staging": { + "tcp/p2p/waku/boot": { + "boot-01.do-ams3.shards.staging": "/dns4/boot-01.do-ams3.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmQE7FXQc6iZHdBzYfw3qCSDa9dLc1wsBJKoP4aZvztq2d", + "boot-01.gc-us-central1-a.shards.staging": "/dns4/boot-01.gc-us-central1-a.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmGAA54bBTE78MYidSy3P7Q9yAWFNTAEReJYD69VRvtL5r", + "boot-01.ac-cn-hongkong-c.shards.staging": "/dns4/boot-01.ac-cn-hongkong-c.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmNTpGnyZ8W1BK2sXEmgSCNWiyDKgRU3NBR2DXST2HzxRU" + }, + "enr/p2p/waku/boot": { + "boot-01.do-ams3.shards.staging": "enr:-QEQuEBuiQgFlJNcv255042zwyl4pOBOivakX8N30Dr9vaaEU2q8-7N4GVY4Hk87iEKELjlIXTpE9Wj6EQq1lrBuc7ayAYJpZIJ2NIJpcISPxvrpim11bHRpYWRkcnO4YAAtNihib290LTAxLmRvLWFtczMuc3RhdHVzLnN0YWdpbmcuc3RhdHVzLmltBnZfAC82KGJvb3QtMDEuZG8tYW1zMy5zdGF0dXMuc3RhZ2luZy5zdGF0dXMuaW0GAbveA4Jyc40AEAUAAQAgAEAAgAEAiXNlY3AyNTZrMaEDq-yGgpuoUG6NKkbIDRmrMiT-bEVzFlpWLEK_rF3yKUaDdGNwgnZfg3VkcIIjKIV3YWt1Mg0", + "boot-01.gc-us-central1-a.shards.staging": "enr:-QEiuECWCuk1CoefbPYUV_cff27t-471k_QhJ_MvgOUQmRE4vgI6h9fD4LgXy7TZNSFXeBBP9haWJB7lx8Wujx-LPmhrAYJpZIJ2NIJpcIRoxQVgim11bHRpYWRkcnO4cgA2NjFib290LTAxLmdjLXVzLWNlbnRyYWwxLWEuc2hhcmRzLnN0YWdpbmcuc3RhdHVzLmltBnZfADg2MWJvb3QtMDEuZ2MtdXMtY2VudHJhbDEtYS5zaGFyZHMuc3RhZ2luZy5zdGF0dXMuaW0GAbveA4Jyc40AEAUAAQAgAEAAgAEAiXNlY3AyNTZrMaEDNAvlGjekD1YV4WpmjwArGAH2g9kHFJnMRfgUhcIkoA2DdGNwgnZfg3VkcIIjKIV3YWt1Mg0", + "boot-01.ac-cn-hongkong-c.shards.staging": "enr:-QEiuEAcHWW7aNBtJhigTCAnKu9-H1b98SWpBJkGm3PAxth5QjJA0kdiabOfou4HgCD6RuyZEnFE5L8ymahouw3kWRiWAYJpZIJ2NIJpcIQvTKi6im11bHRpYWRkcnO4cgA2NjFib290LTAxLmFjLWNuLWhvbmdrb25nLWMuc2hhcmRzLnN0YWdpbmcuc3RhdHVzLmltBnZfADg2MWJvb3QtMDEuYWMtY24taG9uZ2tvbmctYy5zaGFyZHMuc3RhZ2luZy5zdGF0dXMuaW0GAbveA4Jyc40AEAUAAQAgAEAAgAEAiXNlY3AyNTZrMaEDkbgV7oqPNmFtX5FzSPi9WH8kkmrPB1R3n9xRXge91M-DdGNwgnZfg3VkcIIjKIV3YWt1Mg0" + }, + "wss/p2p/waku/boot": { + "boot-01.do-ams3.shards.staging": "/dns4/boot-01.do-ams3.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmQE7FXQc6iZHdBzYfw3qCSDa9dLc1wsBJKoP4aZvztq2d", + "boot-01.gc-us-central1-a.shards.staging": "/dns4/boot-01.gc-us-central1-a.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmGAA54bBTE78MYidSy3P7Q9yAWFNTAEReJYD69VRvtL5r", + "boot-01.ac-cn-hongkong-c.shards.staging": "/dns4/boot-01.ac-cn-hongkong-c.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmNTpGnyZ8W1BK2sXEmgSCNWiyDKgRU3NBR2DXST2HzxRU" + }, + "tcp/p2p/waku/store": { + "store-01.do-ams3.shards.staging": "/dns4/store-01.do-ams3.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAm3xVDaz6SRJ6kErwC21zBJEZjavVXg7VSkoWzaV1aMA3F", + "store-02.do-ams3.shards.staging": "/dns4/store-02.do-ams3.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmCDSnT8oNpMR9HH6uipD71KstYuDCAQGpek9XDAVmqdEr", + "store-01.gc-us-central1-a.shards.staging": "/dns4/store-01.gc-us-central1-a.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmB7Ur9HQqo3cWDPovRQjo57fxWWDaQx27WxSzDGhN4JKg", + "store-02.gc-us-central1-a.shards.staging": "/dns4/store-02.gc-us-central1-a.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmKBd6crqQNZ6nKCSCpHCAwUPn3DUDmkcPSWUTyVXpxKsW", + "store-01.ac-cn-hongkong-c.shards.staging": "/dns4/store-01.ac-cn-hongkong-c.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmMU7Y29oL6DmoJfBFv8J4JhYzYgazPL7nGKJFBV3qcj2E", + "store-02.ac-cn-hongkong-c.shards.staging": "/dns4/store-02.ac-cn-hongkong-c.shards.staging.status.im/tcp/30303/p2p/16Uiu2HAmU7xtcwytXpGpeDrfyhJkiFvTkQbLB9upL5MXPLGceG9K" + }, + "enr/p2p/waku/store": { + "store-01.do-ams3.shards.staging": "enr:-QESuEBbUJK4M7wLejjM7J7db98Cmj1Kpw34laIqAjzrUe7-vUP2ZcODAjcygfL68UchouYzTY8pQu_4lPelvCLULb0XAYJpZIJ2NIJpcIQYkE53im11bHRpYWRkcnO4YgAuNilzdG9yZS0wMS5kby1hbXMzLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQZ2XwAwNilzdG9yZS0wMS5kby1hbXMzLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQJ-wlTnBcknPNUG72hag4NXSa6SeozscHKtYg1Ss3pldoN0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-02.do-ams3.shards.staging": "enr:-QESuEB02PjklRMAnwJn5ZBNZF1cXTMgABVlSQ0lvDsBQ-4VzQ7VLaGNeR2oNmjTbexZUp_tyEOyeYJZYgK0gs8xAXqoAYJpZIJ2NIJpcIQYkE54im11bHRpYWRkcnO4YgAuNilzdG9yZS0wMi5kby1hbXMzLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQZ2XwAwNilzdG9yZS0wMi5kby1hbXMzLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQL5dMmr5GzH0Fton8NGBlUW_rZG8-f3Ph0XhvMUMeVIM4N0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-01.gc-us-central1-a.shards.staging": "enr:-QEkuEDmoxYCHlMz5ruZVRTeLHvmxEJzkSi9YGGEsLRxMjTEF2zh-yuH_Uh_bSVs628asyyUq6nurBLPYd_JMjv4i_-eAYJpZIJ2NIJpcIQj4OfRim11bHRpYWRkcnO4dAA3NjJzdG9yZS0wMS5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQZ2XwA5NjJzdG9yZS0wMS5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQLpEfMK4rQu4Vj5p2mH3YpiNCaiB8Q9JWuCa5sHA1BoJ4N0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-02.gc-us-central1-a.shards.staging": "enr:-QEkuEBLGAmhGv8Oa3hIPgtv-vVEBD_pLS2pP4GxyKfFS60XMwB5GklIr7WgFJV82STuI7v4TCIbkvsZQsxn8e1gPK42AYJpZIJ2NIJpcIQiSIy3im11bHRpYWRkcnO4dAA3NjJzdG9yZS0wMi5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQZ2XwA5NjJzdG9yZS0wMi5nYy11cy1jZW50cmFsMS1hLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQNg_xiKKSUfqa798Ay2GZzh1iRx58F7v5TQBfzFb9T0QYN0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-01.ac-cn-hongkong-c.shards.staging": "enr:-QEkuEDE7UJakM6jmzIPK0-dFwZ6It160yuEPPaJApuVHn7bogXvOYlklh1XdEz9K6QeSiZwk2GAJHqnX023Xi8GA7B2AYJpZIJ2NIJpcIQI2s6Gim11bHRpYWRkcnO4dAA3NjJzdG9yZS0wMS5hYy1jbi1ob25na29uZy1jLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQZ2XwA5NjJzdG9yZS0wMS5hYy1jbi1ob25na29uZy1jLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQOC7-rlGZ1POquzYNLxqu1_RddP7HXIGafRaEKM934p54N0Y3CCdl-DdWRwgiMohXdha3UyAw", + "store-02.ac-cn-hongkong-c.shards.staging": "enr:-QEkuEATTXRamOFShcCt-ViPcEAo_fkqbVjFiuHGgXEk602hURuXG4Qoei9z6Zt4X_HJ2vYoeygBla10RZJxU0Kw7P7dAYJpZIJ2NIJpcIQvTLKkim11bHRpYWRkcnO4dAA3NjJzdG9yZS0wMi5hYy1jbi1ob25na29uZy1jLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQZ2XwA5NjJzdG9yZS0wMi5hYy1jbi1ob25na29uZy1jLnNoYXJkcy5zdGFnaW5nLnN0YXR1cy5pbQYBu94DgnJzjQAQBQABACAAQACAAQCJc2VjcDI1NmsxoQPlyFXKktjIFNaZtTIFI_4ZfNyt3RKWxSPEyH_nb7-YFoN0Y3CCdl-DdWRwgiMohXdha3UyAw" + }, + "wss/p2p/waku/store": { + "store-01.do-ams3.shards.staging": "/dns4/store-01.do-ams3.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAm3xVDaz6SRJ6kErwC21zBJEZjavVXg7VSkoWzaV1aMA3F", + "store-02.do-ams3.shards.staging": "/dns4/store-02.do-ams3.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmCDSnT8oNpMR9HH6uipD71KstYuDCAQGpek9XDAVmqdEr", + "store-01.gc-us-central1-a.shards.staging": "/dns4/store-01.gc-us-central1-a.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmB7Ur9HQqo3cWDPovRQjo57fxWWDaQx27WxSzDGhN4JKg", + "store-02.gc-us-central1-a.shards.staging": "/dns4/store-02.gc-us-central1-a.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmKBd6crqQNZ6nKCSCpHCAwUPn3DUDmkcPSWUTyVXpxKsW", + "store-01.ac-cn-hongkong-c.shards.staging": "/dns4/store-01.ac-cn-hongkong-c.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmMU7Y29oL6DmoJfBFv8J4JhYzYgazPL7nGKJFBV3qcj2E", + "store-02.ac-cn-hongkong-c.shards.staging": "/dns4/store-02.ac-cn-hongkong-c.shards.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmU7xtcwytXpGpeDrfyhJkiFvTkQbLB9upL5MXPLGceG9K" + } + }, + "notify.prod": { + "tcp/p2p/waku": { + "node-01.do-ams3.notify.prod": "enode://401ba5eda402678dc78a0a40fd0795f4ea8b1e34972c4d15cf33ac01292341c89f0cbc637fa9f7a3ffe0b9dfe90e9cdae7a14925500ab01b6a91c67bae42a97a@178.128.139.80:1443", + "node-01.gc-us-central1-a.notify.prod": "enode://181141b1d111908aaf05f4788e6778ec07073a1d4e1ce43c73815c40ee4e7345a1cbf5a90a45f601bf3763f12be63b01624ba1f36eeb9572455e7034b8f9f2c4@34.71.141.140:1443", + "node-01.ac-cn-hongkong-c.notify.prod": "enode://5ffc34d5ffda180d94cd3974d9ed2bb082ede68f342babdbe801ceffb7da902087d43f9aa961c7b85029358874c08ef04ecad9f1d95a1f0e448cbdd5d04350c7@47.242.88.117:1443" + } + } + } +} diff --git a/resources/data/emojis/en.json b/resources/data/emojis/en.json new file mode 100644 index 00000000000..ca122baf6eb --- /dev/null +++ b/resources/data/emojis/en.json @@ -0,0 +1 @@ +[{"group":0,"hexcode":"1f600","label":"grinning face","tags":["face","grin"],"unicode":"😀"},{"group":0,"hexcode":"1f603","label":"grinning face with big eyes","tags":["face","mouth","open","smile"],"unicode":"😃"},{"group":0,"hexcode":"1f604","label":"grinning face with smiling eyes","tags":["eye","face","mouth","open","smile"],"unicode":"😄","emoticon":":D"},{"group":0,"hexcode":"1f601","label":"beaming face with smiling eyes","tags":["eye","face","grin","smile"],"unicode":"😁"},{"group":0,"hexcode":"1f606","label":"grinning squinting face","tags":["face","laugh","mouth","satisfied","smile"],"unicode":"😆","emoticon":["xD","XD"]},{"group":0,"hexcode":"1f605","label":"grinning face with sweat","tags":["cold","face","open","smile","sweat"],"unicode":"😅"},{"group":0,"hexcode":"1f923","label":"rolling on the floor laughing","tags":["face","floor","laugh","rofl","rolling","rotfl"],"unicode":"🤣","emoticon":":'D"},{"group":0,"hexcode":"1f602","label":"face with tears of joy","tags":["face","joy","laugh","tear"],"unicode":"😂","emoticon":":')"},{"group":0,"hexcode":"1f642","label":"slightly smiling face","tags":["face","smile"],"unicode":"🙂","emoticon":":)"},{"group":0,"hexcode":"1f643","label":"upside-down face","tags":["face","upside-down"],"unicode":"🙃"},{"group":0,"hexcode":"1fae0","label":"melting face","tags":["disappear","dissolve","liquid","melt"],"unicode":"🫠"},{"group":0,"hexcode":"1f609","label":"winking face","tags":["face","wink"],"unicode":"😉","emoticon":";)"},{"group":0,"hexcode":"1f60a","label":"smiling face with smiling eyes","tags":["blush","eye","face","smile"],"unicode":"😊","emoticon":":>"},{"group":0,"hexcode":"1f607","label":"smiling face with halo","tags":["angel","face","fantasy","halo","innocent"],"unicode":"😇","emoticon":["o:)","O:)"]},{"group":0,"hexcode":"1f970","label":"smiling face with hearts","tags":["adore","crush","hearts","in love"],"unicode":"🥰"},{"group":0,"hexcode":"1f60d","label":"smiling face with heart-eyes","tags":["eye","face","love","smile"],"unicode":"😍"},{"group":0,"hexcode":"1f929","label":"star-struck","tags":["eyes","face","grinning","star"],"unicode":"🤩"},{"group":0,"hexcode":"1f618","label":"face blowing a kiss","tags":["face","kiss"],"unicode":"😘","emoticon":[":x",":X"]},{"group":0,"hexcode":"1f617","label":"kissing face","tags":["face","kiss"],"unicode":"😗"},{"group":0,"hexcode":"263a","label":"smiling face","tags":["face","outlined","relaxed","smile"],"unicode":"☺️"},{"group":0,"hexcode":"1f61a","label":"kissing face with closed eyes","tags":["closed","eye","face","kiss"],"unicode":"😚","emoticon":":*"},{"group":0,"hexcode":"1f619","label":"kissing face with smiling eyes","tags":["eye","face","kiss","smile"],"unicode":"😙"},{"group":0,"hexcode":"1f972","label":"smiling face with tear","tags":["grateful","proud","relieved","smiling","tear","touched"],"unicode":"🥲"},{"group":0,"hexcode":"1f60b","label":"face savoring food","tags":["delicious","face","savouring","smile","yum"],"unicode":"😋"},{"group":0,"hexcode":"1f61b","label":"face with tongue","tags":["face","tongue"],"unicode":"😛","emoticon":[":p",":P"]},{"group":0,"hexcode":"1f61c","label":"winking face with tongue","tags":["eye","face","joke","tongue","wink"],"unicode":"😜","emoticon":[";p",";P"]},{"group":0,"hexcode":"1f92a","label":"zany face","tags":["eye","goofy","large","small"],"unicode":"🤪"},{"group":0,"hexcode":"1f61d","label":"squinting face with tongue","tags":["eye","face","horrible","taste","tongue"],"unicode":"😝","emoticon":["xp","xP","XP"]},{"group":0,"hexcode":"1f911","label":"money-mouth face","tags":["face","money","mouth"],"unicode":"🤑"},{"group":0,"hexcode":"1f917","label":"smiling face with open hands","tags":["face","hug","hugging","open hands","smiling face"],"unicode":"🤗"},{"group":0,"hexcode":"1f92d","label":"face with hand over mouth","tags":["whoops"],"unicode":"🤭"},{"group":0,"hexcode":"1fae2","label":"face with open eyes and hand over mouth","tags":["amazement","awe","disbelief","embarrass","scared","surprise"],"unicode":"🫢"},{"group":0,"hexcode":"1fae3","label":"face with peeking eye","tags":["captivated","peep","stare"],"unicode":"🫣"},{"group":0,"hexcode":"1f92b","label":"shushing face","tags":["quiet","shush"],"unicode":"🤫"},{"group":0,"hexcode":"1f914","label":"thinking face","tags":["face","thinking"],"unicode":"🤔","emoticon":[":l",":L"]},{"group":0,"hexcode":"1fae1","label":"saluting face","tags":["ok","salute","sunny","troops","yes"],"unicode":"🫡"},{"group":0,"hexcode":"1f910","label":"zipper-mouth face","tags":["face","mouth","zipper"],"unicode":"🤐","emoticon":[":z",":Z"]},{"group":0,"hexcode":"1f928","label":"face with raised eyebrow","tags":["distrust","skeptic"],"unicode":"🤨"},{"group":0,"hexcode":"1f610","label":"neutral face","tags":["deadpan","face","meh","neutral"],"unicode":"😐️","emoticon":":|"},{"group":0,"hexcode":"1f611","label":"expressionless face","tags":["expressionless","face","inexpressive","meh","unexpressive"],"unicode":"😑"},{"group":0,"hexcode":"1f636","label":"face without mouth","tags":["face","mouth","quiet","silent"],"unicode":"😶","emoticon":":#"},{"group":0,"hexcode":"1fae5","label":"dotted line face","tags":["depressed","disappear","hide","introvert","invisible"],"unicode":"🫥"},{"group":0,"hexcode":"1f636-200d-1f32b-fe0f","label":"face in clouds","tags":["absentminded","face in the fog","head in clouds"],"unicode":"😶‍🌫️"},{"group":0,"hexcode":"1f60f","label":"smirking face","tags":["face","smirk"],"unicode":"😏","emoticon":":j"},{"group":0,"hexcode":"1f612","label":"unamused face","tags":["face","unamused","unhappy"],"unicode":"😒","emoticon":":?"},{"group":0,"hexcode":"1f644","label":"face with rolling eyes","tags":["eyeroll","eyes","face","rolling"],"unicode":"🙄"},{"group":0,"hexcode":"1f62c","label":"grimacing face","tags":["face","grimace"],"unicode":"😬","emoticon":"8D"},{"group":0,"hexcode":"1f62e-200d-1f4a8","label":"face exhaling","tags":["exhale","gasp","groan","relief","whisper","whistle"],"unicode":"😮‍💨"},{"group":0,"hexcode":"1f925","label":"lying face","tags":["face","lie","pinocchio"],"unicode":"🤥"},{"group":0,"hexcode":"1fae8","label":"shaking face","tags":["earthquake","face","shaking","shock","vibrate"],"unicode":"🫨"},{"group":0,"hexcode":"1f60c","label":"relieved face","tags":["face","relieved"],"unicode":"😌"},{"group":0,"hexcode":"1f614","label":"pensive face","tags":["dejected","face","pensive"],"unicode":"😔"},{"group":0,"hexcode":"1f62a","label":"sleepy face","tags":["face","good night","sleep"],"unicode":"😪"},{"group":0,"hexcode":"1f924","label":"drooling face","tags":["drooling","face"],"unicode":"🤤"},{"group":0,"hexcode":"1f634","label":"sleeping face","tags":["face","good night","sleep","zzz"],"unicode":"😴"},{"group":0,"hexcode":"1f637","label":"face with medical mask","tags":["cold","doctor","face","mask","sick"],"unicode":"😷"},{"group":0,"hexcode":"1f912","label":"face with thermometer","tags":["face","ill","sick","thermometer"],"unicode":"🤒"},{"group":0,"hexcode":"1f915","label":"face with head-bandage","tags":["bandage","face","hurt","injury"],"unicode":"🤕"},{"group":0,"hexcode":"1f922","label":"nauseated face","tags":["face","nauseated","vomit"],"unicode":"🤢","emoticon":"%("},{"group":0,"hexcode":"1f92e","label":"face vomiting","tags":["puke","sick","vomit"],"unicode":"🤮"},{"group":0,"hexcode":"1f927","label":"sneezing face","tags":["face","gesundheit","sneeze"],"unicode":"🤧"},{"group":0,"hexcode":"1f975","label":"hot face","tags":["feverish","heat stroke","hot","red-faced","sweating"],"unicode":"🥵"},{"group":0,"hexcode":"1f976","label":"cold face","tags":["blue-faced","cold","freezing","frostbite","icicles"],"unicode":"🥶"},{"group":0,"hexcode":"1f974","label":"woozy face","tags":["dizzy","intoxicated","tipsy","uneven eyes","wavy mouth"],"unicode":"🥴","emoticon":":&"},{"group":0,"hexcode":"1f635","label":"face with crossed-out eyes","tags":["crossed-out eyes","dead","face","knocked out"],"unicode":"😵","emoticon":["xo","XO"]},{"group":0,"hexcode":"1f635-200d-1f4ab","label":"face with spiral eyes","tags":["dizzy","hypnotized","spiral","trouble","whoa"],"unicode":"😵‍💫"},{"group":0,"hexcode":"1f92f","label":"exploding head","tags":["mind blown","shocked"],"unicode":"🤯"},{"group":0,"hexcode":"1f920","label":"cowboy hat face","tags":["cowboy","cowgirl","face","hat"],"unicode":"🤠"},{"group":0,"hexcode":"1f973","label":"partying face","tags":["celebration","hat","horn","party"],"unicode":"🥳"},{"group":0,"hexcode":"1f978","label":"disguised face","tags":["disguise","face","glasses","incognito","nose"],"unicode":"🥸"},{"group":0,"hexcode":"1f60e","label":"smiling face with sunglasses","tags":["bright","cool","face","sun","sunglasses"],"unicode":"😎","emoticon":"8)"},{"group":0,"hexcode":"1f913","label":"nerd face","tags":["face","geek","nerd"],"unicode":"🤓","emoticon":":B"},{"group":0,"hexcode":"1f9d0","label":"face with monocle","tags":["face","monocle","stuffy"],"unicode":"🧐"},{"group":0,"hexcode":"1f615","label":"confused face","tags":["confused","face","meh"],"unicode":"😕","emoticon":":/"},{"group":0,"hexcode":"1fae4","label":"face with diagonal mouth","tags":["disappointed","meh","skeptical","unsure"],"unicode":"🫤"},{"group":0,"hexcode":"1f61f","label":"worried face","tags":["face","worried"],"unicode":"😟"},{"group":0,"hexcode":"1f641","label":"slightly frowning face","tags":["face","frown"],"unicode":"🙁"},{"group":0,"hexcode":"2639","label":"frowning face","tags":["face","frown"],"unicode":"☹️","emoticon":":("},{"group":0,"hexcode":"1f62e","label":"face with open mouth","tags":["face","mouth","open","sympathy"],"unicode":"😮"},{"group":0,"hexcode":"1f62f","label":"hushed face","tags":["face","hushed","stunned","surprised"],"unicode":"😯"},{"group":0,"hexcode":"1f632","label":"astonished face","tags":["astonished","face","shocked","totally"],"unicode":"😲","emoticon":[":o",":O"]},{"group":0,"hexcode":"1f633","label":"flushed face","tags":["dazed","face","flushed"],"unicode":"😳","emoticon":":$"},{"group":0,"hexcode":"1f97a","label":"pleading face","tags":["begging","mercy","puppy eyes"],"unicode":"🥺"},{"group":0,"hexcode":"1f979","label":"face holding back tears","tags":["angry","cry","proud","resist","sad"],"unicode":"🥹"},{"group":0,"hexcode":"1f626","label":"frowning face with open mouth","tags":["face","frown","mouth","open"],"unicode":"😦"},{"group":0,"hexcode":"1f627","label":"anguished face","tags":["anguished","face"],"unicode":"😧","emoticon":[":s",":S"]},{"group":0,"hexcode":"1f628","label":"fearful face","tags":["face","fear","fearful","scared"],"unicode":"😨"},{"group":0,"hexcode":"1f630","label":"anxious face with sweat","tags":["blue","cold","face","rushed","sweat"],"unicode":"😰"},{"group":0,"hexcode":"1f625","label":"sad but relieved face","tags":["disappointed","face","relieved","whew"],"unicode":"😥"},{"group":0,"hexcode":"1f622","label":"crying face","tags":["cry","face","sad","tear"],"unicode":"😢","emoticon":":'("},{"group":0,"hexcode":"1f62d","label":"loudly crying face","tags":["cry","face","sad","sob","tear"],"unicode":"😭","emoticon":":'o"},{"group":0,"hexcode":"1f631","label":"face screaming in fear","tags":["face","fear","munch","scared","scream"],"unicode":"😱","emoticon":"Dx"},{"group":0,"hexcode":"1f616","label":"confounded face","tags":["confounded","face"],"unicode":"😖","emoticon":["x(","X("]},{"group":0,"hexcode":"1f623","label":"persevering face","tags":["face","persevere"],"unicode":"😣"},{"group":0,"hexcode":"1f61e","label":"disappointed face","tags":["disappointed","face"],"unicode":"😞"},{"group":0,"hexcode":"1f613","label":"downcast face with sweat","tags":["cold","face","sweat"],"unicode":"😓","emoticon":":<"},{"group":0,"hexcode":"1f629","label":"weary face","tags":["face","tired","weary"],"unicode":"😩","emoticon":"D:"},{"group":0,"hexcode":"1f62b","label":"tired face","tags":["face","tired"],"unicode":"😫","emoticon":[":c",":C"]},{"group":0,"hexcode":"1f971","label":"yawning face","tags":["bored","tired","yawn"],"unicode":"🥱"},{"group":0,"hexcode":"1f624","label":"face with steam from nose","tags":["face","triumph","won"],"unicode":"😤"},{"group":0,"hexcode":"1f621","label":"enraged face","tags":["angry","enraged","face","mad","pouting","rage","red"],"unicode":"😡","emoticon":">:/"},{"group":0,"hexcode":"1f620","label":"angry face","tags":["anger","angry","face","mad"],"unicode":"😠"},{"group":0,"hexcode":"1f92c","label":"face with symbols on mouth","tags":["swearing"],"unicode":"🤬","emoticon":":@"},{"group":0,"hexcode":"1f608","label":"smiling face with horns","tags":["face","fairy tale","fantasy","horns","smile"],"unicode":"😈","emoticon":">:)"},{"group":0,"hexcode":"1f47f","label":"angry face with horns","tags":["demon","devil","face","fantasy","imp"],"unicode":"👿","emoticon":">:("},{"group":0,"hexcode":"1f480","label":"skull","tags":["death","face","fairy tale","monster"],"unicode":"💀"},{"group":0,"hexcode":"2620","label":"skull and crossbones","tags":["crossbones","death","face","monster","skull"],"unicode":"☠️"},{"group":0,"hexcode":"1f4a9","label":"pile of poo","tags":["dung","face","monster","poo","poop"],"unicode":"💩"},{"group":0,"hexcode":"1f921","label":"clown face","tags":["clown","face"],"unicode":"🤡"},{"group":0,"hexcode":"1f479","label":"ogre","tags":["creature","face","fairy tale","fantasy","monster"],"unicode":"👹","emoticon":">0)"},{"group":0,"hexcode":"1f47a","label":"goblin","tags":["creature","face","fairy tale","fantasy","monster"],"unicode":"👺"},{"group":0,"hexcode":"1f47b","label":"ghost","tags":["creature","face","fairy tale","fantasy","monster"],"unicode":"👻"},{"group":0,"hexcode":"1f47d","label":"alien","tags":["creature","extraterrestrial","face","fantasy","ufo"],"unicode":"👽️"},{"group":0,"hexcode":"1f47e","label":"alien monster","tags":["alien","creature","extraterrestrial","face","monster","ufo"],"unicode":"👾"},{"group":0,"hexcode":"1f916","label":"robot","tags":["face","monster"],"unicode":"🤖"},{"group":0,"hexcode":"1f63a","label":"grinning cat","tags":["cat","face","grinning","mouth","open","smile"],"unicode":"😺"},{"group":0,"hexcode":"1f638","label":"grinning cat with smiling eyes","tags":["cat","eye","face","grin","smile"],"unicode":"😸"},{"group":0,"hexcode":"1f639","label":"cat with tears of joy","tags":["cat","face","joy","tear"],"unicode":"😹"},{"group":0,"hexcode":"1f63b","label":"smiling cat with heart-eyes","tags":["cat","eye","face","heart","love","smile"],"unicode":"😻"},{"group":0,"hexcode":"1f63c","label":"cat with wry smile","tags":["cat","face","ironic","smile","wry"],"unicode":"😼"},{"group":0,"hexcode":"1f63d","label":"kissing cat","tags":["cat","eye","face","kiss"],"unicode":"😽","emoticon":":3"},{"group":0,"hexcode":"1f640","label":"weary cat","tags":["cat","face","oh","surprised","weary"],"unicode":"🙀"},{"group":0,"hexcode":"1f63f","label":"crying cat","tags":["cat","cry","face","sad","tear"],"unicode":"😿"},{"group":0,"hexcode":"1f63e","label":"pouting cat","tags":["cat","face","pouting"],"unicode":"😾"},{"group":0,"hexcode":"1f648","label":"see-no-evil monkey","tags":["evil","face","forbidden","monkey","see"],"unicode":"🙈"},{"group":0,"hexcode":"1f649","label":"hear-no-evil monkey","tags":["evil","face","forbidden","hear","monkey"],"unicode":"🙉"},{"group":0,"hexcode":"1f64a","label":"speak-no-evil monkey","tags":["evil","face","forbidden","monkey","speak"],"unicode":"🙊"},{"group":0,"hexcode":"1f48c","label":"love letter","tags":["heart","letter","love","mail"],"unicode":"💌"},{"group":0,"hexcode":"1f498","label":"heart with arrow","tags":["arrow","cupid"],"unicode":"💘"},{"group":0,"hexcode":"1f49d","label":"heart with ribbon","tags":["ribbon","valentine"],"unicode":"💝"},{"group":0,"hexcode":"1f496","label":"sparkling heart","tags":["excited","sparkle"],"unicode":"💖"},{"group":0,"hexcode":"1f497","label":"growing heart","tags":["excited","growing","nervous","pulse"],"unicode":"💗"},{"group":0,"hexcode":"1f493","label":"beating heart","tags":["beating","heartbeat","pulsating"],"unicode":"💓"},{"group":0,"hexcode":"1f49e","label":"revolving hearts","tags":["revolving"],"unicode":"💞"},{"group":0,"hexcode":"1f495","label":"two hearts","tags":["love"],"unicode":"💕"},{"group":0,"hexcode":"1f49f","label":"heart decoration","tags":["heart"],"unicode":"💟"},{"group":0,"hexcode":"2763","label":"heart exclamation","tags":["exclamation","mark","punctuation"],"unicode":"❣️"},{"group":0,"hexcode":"1f494","label":"broken heart","tags":["break","broken"],"unicode":"💔","emoticon":""},{"group":1,"hexcode":"1f9d9-200d-2640-fe0f","label":"woman mage","tags":["sorceress","witch"],"unicode":"🧙‍♀️"},{"group":1,"hexcode":"1f9da","label":"fairy","tags":["oberon","puck","titania"],"unicode":"🧚"},{"group":1,"hexcode":"1f9da-200d-2642-fe0f","label":"man fairy","tags":["oberon","puck"],"unicode":"🧚‍♂️"},{"group":1,"hexcode":"1f9da-200d-2640-fe0f","label":"woman fairy","tags":["titania"],"unicode":"🧚‍♀️"},{"group":1,"hexcode":"1f9db","label":"vampire","tags":["dracula","undead"],"unicode":"🧛","emoticon":":E"},{"group":1,"hexcode":"1f9db-200d-2642-fe0f","label":"man vampire","tags":["dracula","undead"],"unicode":"🧛‍♂️"},{"group":1,"hexcode":"1f9db-200d-2640-fe0f","label":"woman vampire","tags":["undead"],"unicode":"🧛‍♀️"},{"group":1,"hexcode":"1f9dc","label":"merperson","tags":["mermaid","merman","merwoman"],"unicode":"🧜"},{"group":1,"hexcode":"1f9dc-200d-2642-fe0f","label":"merman","tags":["triton"],"unicode":"🧜‍♂️"},{"group":1,"hexcode":"1f9dc-200d-2640-fe0f","label":"mermaid","tags":["merwoman"],"unicode":"🧜‍♀️"},{"group":1,"hexcode":"1f9dd","label":"elf","tags":["magical"],"unicode":"🧝"},{"group":1,"hexcode":"1f9dd-200d-2642-fe0f","label":"man elf","tags":["magical"],"unicode":"🧝‍♂️"},{"group":1,"hexcode":"1f9dd-200d-2640-fe0f","label":"woman elf","tags":["magical"],"unicode":"🧝‍♀️"},{"group":1,"hexcode":"1f9de","label":"genie","tags":["djinn"],"unicode":"🧞"},{"group":1,"hexcode":"1f9de-200d-2642-fe0f","label":"man genie","tags":["djinn"],"unicode":"🧞‍♂️"},{"group":1,"hexcode":"1f9de-200d-2640-fe0f","label":"woman genie","tags":["djinn"],"unicode":"🧞‍♀️"},{"group":1,"hexcode":"1f9df","label":"zombie","tags":["undead","walking dead"],"unicode":"🧟","emoticon":"8#"},{"group":1,"hexcode":"1f9df-200d-2642-fe0f","label":"man zombie","tags":["undead","walking dead"],"unicode":"🧟‍♂️"},{"group":1,"hexcode":"1f9df-200d-2640-fe0f","label":"woman zombie","tags":["undead","walking dead"],"unicode":"🧟‍♀️"},{"group":1,"hexcode":"1f9cc","label":"troll","tags":["fairy tale","fantasy","monster"],"unicode":"🧌"},{"group":1,"hexcode":"1f486","label":"person getting massage","tags":["face","massage","salon"],"unicode":"💆"},{"group":1,"hexcode":"1f486-200d-2642-fe0f","label":"man getting massage","tags":["face","man","massage"],"unicode":"💆‍♂️"},{"group":1,"hexcode":"1f486-200d-2640-fe0f","label":"woman getting massage","tags":["face","massage","woman"],"unicode":"💆‍♀️"},{"group":1,"hexcode":"1f487","label":"person getting haircut","tags":["barber","beauty","haircut","parlor"],"unicode":"💇"},{"group":1,"hexcode":"1f487-200d-2642-fe0f","label":"man getting haircut","tags":["haircut","man"],"unicode":"💇‍♂️"},{"group":1,"hexcode":"1f487-200d-2640-fe0f","label":"woman getting haircut","tags":["haircut","woman"],"unicode":"💇‍♀️"},{"group":1,"hexcode":"1f6b6","label":"person walking","tags":["hike","walk","walking"],"unicode":"🚶"},{"group":1,"hexcode":"1f6b6-200d-2642-fe0f","label":"man walking","tags":["hike","man","walk"],"unicode":"🚶‍♂️"},{"group":1,"hexcode":"1f6b6-200d-2640-fe0f","label":"woman walking","tags":["hike","walk","woman"],"unicode":"🚶‍♀️"},{"group":1,"hexcode":"1f9cd","label":"person standing","tags":["stand","standing"],"unicode":"🧍"},{"group":1,"hexcode":"1f9cd-200d-2642-fe0f","label":"man standing","tags":["man","standing"],"unicode":"🧍‍♂️"},{"group":1,"hexcode":"1f9cd-200d-2640-fe0f","label":"woman standing","tags":["standing","woman"],"unicode":"🧍‍♀️"},{"group":1,"hexcode":"1f9ce","label":"person kneeling","tags":["kneel","kneeling"],"unicode":"🧎"},{"group":1,"hexcode":"1f9ce-200d-2642-fe0f","label":"man kneeling","tags":["kneeling","man"],"unicode":"🧎‍♂️"},{"group":1,"hexcode":"1f9ce-200d-2640-fe0f","label":"woman kneeling","tags":["kneeling","woman"],"unicode":"🧎‍♀️"},{"group":1,"hexcode":"1f9d1-200d-1f9af","label":"person with white cane","tags":["accessibility","blind"],"unicode":"🧑‍🦯"},{"group":1,"hexcode":"1f468-200d-1f9af","label":"man with white cane","tags":["accessibility","blind","man"],"unicode":"👨‍🦯"},{"group":1,"hexcode":"1f469-200d-1f9af","label":"woman with white cane","tags":["accessibility","blind","woman"],"unicode":"👩‍🦯"},{"group":1,"hexcode":"1f9d1-200d-1f9bc","label":"person in motorized wheelchair","tags":["accessibility","wheelchair"],"unicode":"🧑‍🦼"},{"group":1,"hexcode":"1f468-200d-1f9bc","label":"man in motorized wheelchair","tags":["accessibility","man","wheelchair"],"unicode":"👨‍🦼"},{"group":1,"hexcode":"1f469-200d-1f9bc","label":"woman in motorized wheelchair","tags":["accessibility","wheelchair","woman"],"unicode":"👩‍🦼"},{"group":1,"hexcode":"1f9d1-200d-1f9bd","label":"person in manual wheelchair","tags":["accessibility","wheelchair"],"unicode":"🧑‍🦽"},{"group":1,"hexcode":"1f468-200d-1f9bd","label":"man in manual wheelchair","tags":["accessibility","man","wheelchair"],"unicode":"👨‍🦽"},{"group":1,"hexcode":"1f469-200d-1f9bd","label":"woman in manual wheelchair","tags":["accessibility","wheelchair","woman"],"unicode":"👩‍🦽"},{"group":1,"hexcode":"1f3c3","label":"person running","tags":["marathon","running"],"unicode":"🏃"},{"group":1,"hexcode":"1f3c3-200d-2642-fe0f","label":"man running","tags":["man","marathon","racing","running"],"unicode":"🏃‍♂️"},{"group":1,"hexcode":"1f3c3-200d-2640-fe0f","label":"woman running","tags":["marathon","racing","running","woman"],"unicode":"🏃‍♀️"},{"group":1,"hexcode":"1f483","label":"woman dancing","tags":["dance","dancing","woman"],"unicode":"💃"},{"group":1,"hexcode":"1f57a","label":"man dancing","tags":["dance","dancing","man"],"unicode":"🕺"},{"group":1,"hexcode":"1f574","label":"person in suit levitating","tags":["business","person","suit"],"unicode":"🕴️"},{"group":1,"hexcode":"1f46f","label":"people with bunny ears","tags":["bunny ear","dancer","partying"],"unicode":"👯"},{"group":1,"hexcode":"1f46f-200d-2642-fe0f","label":"men with bunny ears","tags":["bunny ear","dancer","men","partying"],"unicode":"👯‍♂️"},{"group":1,"hexcode":"1f46f-200d-2640-fe0f","label":"women with bunny ears","tags":["bunny ear","dancer","partying","women"],"unicode":"👯‍♀️"},{"group":1,"hexcode":"1f9d6","label":"person in steamy room","tags":["sauna","steam room"],"unicode":"🧖"},{"group":1,"hexcode":"1f9d6-200d-2642-fe0f","label":"man in steamy room","tags":["sauna","steam room"],"unicode":"🧖‍♂️"},{"group":1,"hexcode":"1f9d6-200d-2640-fe0f","label":"woman in steamy room","tags":["sauna","steam room"],"unicode":"🧖‍♀️"},{"group":1,"hexcode":"1f9d7","label":"person climbing","tags":["climber"],"unicode":"🧗"},{"group":1,"hexcode":"1f9d7-200d-2642-fe0f","label":"man climbing","tags":["climber"],"unicode":"🧗‍♂️"},{"group":1,"hexcode":"1f9d7-200d-2640-fe0f","label":"woman climbing","tags":["climber"],"unicode":"🧗‍♀️"},{"group":1,"hexcode":"1f93a","label":"person fencing","tags":["fencer","fencing","sword"],"unicode":"🤺"},{"group":1,"hexcode":"1f3c7","label":"horse racing","tags":["horse","jockey","racehorse","racing"],"unicode":"🏇"},{"group":1,"hexcode":"26f7","label":"skier","tags":["ski","snow"],"unicode":"⛷️"},{"group":1,"hexcode":"1f3c2","label":"snowboarder","tags":["ski","snow","snowboard"],"unicode":"🏂️"},{"group":1,"hexcode":"1f3cc","label":"person golfing","tags":["ball","golf"],"unicode":"🏌️"},{"group":1,"hexcode":"1f3cc-fe0f-200d-2642-fe0f","label":"man golfing","tags":["golf","man"],"unicode":"🏌️‍♂️"},{"group":1,"hexcode":"1f3cc-fe0f-200d-2640-fe0f","label":"woman golfing","tags":["golf","woman"],"unicode":"🏌️‍♀️"},{"group":1,"hexcode":"1f3c4","label":"person surfing","tags":["surfing"],"unicode":"🏄️"},{"group":1,"hexcode":"1f3c4-200d-2642-fe0f","label":"man surfing","tags":["man","surfing"],"unicode":"🏄‍♂️"},{"group":1,"hexcode":"1f3c4-200d-2640-fe0f","label":"woman surfing","tags":["surfing","woman"],"unicode":"🏄‍♀️"},{"group":1,"hexcode":"1f6a3","label":"person rowing boat","tags":["boat","rowboat"],"unicode":"🚣"},{"group":1,"hexcode":"1f6a3-200d-2642-fe0f","label":"man rowing boat","tags":["boat","man","rowboat"],"unicode":"🚣‍♂️"},{"group":1,"hexcode":"1f6a3-200d-2640-fe0f","label":"woman rowing boat","tags":["boat","rowboat","woman"],"unicode":"🚣‍♀️"},{"group":1,"hexcode":"1f3ca","label":"person swimming","tags":["swim"],"unicode":"🏊️"},{"group":1,"hexcode":"1f3ca-200d-2642-fe0f","label":"man swimming","tags":["man","swim"],"unicode":"🏊‍♂️"},{"group":1,"hexcode":"1f3ca-200d-2640-fe0f","label":"woman swimming","tags":["swim","woman"],"unicode":"🏊‍♀️"},{"group":1,"hexcode":"26f9","label":"person bouncing ball","tags":["ball"],"unicode":"⛹️"},{"group":1,"hexcode":"26f9-fe0f-200d-2642-fe0f","label":"man bouncing ball","tags":["ball","man"],"unicode":"⛹️‍♂️"},{"group":1,"hexcode":"26f9-fe0f-200d-2640-fe0f","label":"woman bouncing ball","tags":["ball","woman"],"unicode":"⛹️‍♀️"},{"group":1,"hexcode":"1f3cb","label":"person lifting weights","tags":["lifter","weight"],"unicode":"🏋️"},{"group":1,"hexcode":"1f3cb-fe0f-200d-2642-fe0f","label":"man lifting weights","tags":["man","weight lifter"],"unicode":"🏋️‍♂️"},{"group":1,"hexcode":"1f3cb-fe0f-200d-2640-fe0f","label":"woman lifting weights","tags":["weight lifter","woman"],"unicode":"🏋️‍♀️"},{"group":1,"hexcode":"1f6b4","label":"person biking","tags":["bicycle","biking","cyclist"],"unicode":"🚴"},{"group":1,"hexcode":"1f6b4-200d-2642-fe0f","label":"man biking","tags":["bicycle","biking","cyclist","man"],"unicode":"🚴‍♂️"},{"group":1,"hexcode":"1f6b4-200d-2640-fe0f","label":"woman biking","tags":["bicycle","biking","cyclist","woman"],"unicode":"🚴‍♀️"},{"group":1,"hexcode":"1f6b5","label":"person mountain biking","tags":["bicycle","bicyclist","bike","cyclist","mountain"],"unicode":"🚵"},{"group":1,"hexcode":"1f6b5-200d-2642-fe0f","label":"man mountain biking","tags":["bicycle","bike","cyclist","man","mountain"],"unicode":"🚵‍♂️"},{"group":1,"hexcode":"1f6b5-200d-2640-fe0f","label":"woman mountain biking","tags":["bicycle","bike","biking","cyclist","mountain","woman"],"unicode":"🚵‍♀️"},{"group":1,"hexcode":"1f938","label":"person cartwheeling","tags":["cartwheel","gymnastics"],"unicode":"🤸"},{"group":1,"hexcode":"1f938-200d-2642-fe0f","label":"man cartwheeling","tags":["cartwheel","gymnastics","man"],"unicode":"🤸‍♂️"},{"group":1,"hexcode":"1f938-200d-2640-fe0f","label":"woman cartwheeling","tags":["cartwheel","gymnastics","woman"],"unicode":"🤸‍♀️"},{"group":1,"hexcode":"1f93c","label":"people wrestling","tags":["wrestle","wrestler"],"unicode":"🤼"},{"group":1,"hexcode":"1f93c-200d-2642-fe0f","label":"men wrestling","tags":["men","wrestle"],"unicode":"🤼‍♂️"},{"group":1,"hexcode":"1f93c-200d-2640-fe0f","label":"women wrestling","tags":["women","wrestle"],"unicode":"🤼‍♀️"},{"group":1,"hexcode":"1f93d","label":"person playing water polo","tags":["polo","water"],"unicode":"🤽"},{"group":1,"hexcode":"1f93d-200d-2642-fe0f","label":"man playing water polo","tags":["man","water polo"],"unicode":"🤽‍♂️"},{"group":1,"hexcode":"1f93d-200d-2640-fe0f","label":"woman playing water polo","tags":["water polo","woman"],"unicode":"🤽‍♀️"},{"group":1,"hexcode":"1f93e","label":"person playing handball","tags":["ball","handball"],"unicode":"🤾"},{"group":1,"hexcode":"1f93e-200d-2642-fe0f","label":"man playing handball","tags":["handball","man"],"unicode":"🤾‍♂️"},{"group":1,"hexcode":"1f93e-200d-2640-fe0f","label":"woman playing handball","tags":["handball","woman"],"unicode":"🤾‍♀️"},{"group":1,"hexcode":"1f939","label":"person juggling","tags":["balance","juggle","multitask","skill"],"unicode":"🤹"},{"group":1,"hexcode":"1f939-200d-2642-fe0f","label":"man juggling","tags":["juggling","man","multitask"],"unicode":"🤹‍♂️"},{"group":1,"hexcode":"1f939-200d-2640-fe0f","label":"woman juggling","tags":["juggling","multitask","woman"],"unicode":"🤹‍♀️"},{"group":1,"hexcode":"1f9d8","label":"person in lotus position","tags":["meditation","yoga"],"unicode":"🧘"},{"group":1,"hexcode":"1f9d8-200d-2642-fe0f","label":"man in lotus position","tags":["meditation","yoga"],"unicode":"🧘‍♂️"},{"group":1,"hexcode":"1f9d8-200d-2640-fe0f","label":"woman in lotus position","tags":["meditation","yoga"],"unicode":"🧘‍♀️"},{"group":1,"hexcode":"1f6c0","label":"person taking bath","tags":["bath","bathtub"],"unicode":"🛀"},{"group":1,"hexcode":"1f6cc","label":"person in bed","tags":["good night","hotel","sleep"],"unicode":"🛌"},{"group":1,"hexcode":"1f9d1-200d-1f91d-200d-1f9d1","label":"people holding hands","tags":["couple","hand","hold","holding hands","person"],"unicode":"🧑‍🤝‍🧑"},{"group":1,"hexcode":"1f46d","label":"women holding hands","tags":["couple","hand","holding hands","women"],"unicode":"👭"},{"group":1,"hexcode":"1f46b","label":"woman and man holding hands","tags":["couple","hand","hold","holding hands","man","woman"],"unicode":"👫"},{"group":1,"hexcode":"1f46c","label":"men holding hands","tags":["couple","gemini","holding hands","man","men","twins","zodiac"],"unicode":"👬"},{"group":1,"hexcode":"1f48f","label":"kiss","tags":["couple"],"unicode":"💏"},{"group":1,"hexcode":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468","label":"kiss: woman, man","tags":["couple","kiss","man","woman"],"unicode":"👩‍❤️‍💋‍👨"},{"group":1,"hexcode":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","label":"kiss: man, man","tags":["couple","kiss","man"],"unicode":"👨‍❤️‍💋‍👨"},{"group":1,"hexcode":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","label":"kiss: woman, woman","tags":["couple","kiss","woman"],"unicode":"👩‍❤️‍💋‍👩"},{"group":1,"hexcode":"1f491","label":"couple with heart","tags":["couple","love"],"unicode":"💑"},{"group":1,"hexcode":"1f469-200d-2764-fe0f-200d-1f468","label":"couple with heart: woman, man","tags":["couple","couple with heart","love","man","woman"],"unicode":"👩‍❤️‍👨"},{"group":1,"hexcode":"1f468-200d-2764-fe0f-200d-1f468","label":"couple with heart: man, man","tags":["couple","couple with heart","love","man"],"unicode":"👨‍❤️‍👨"},{"group":1,"hexcode":"1f469-200d-2764-fe0f-200d-1f469","label":"couple with heart: woman, woman","tags":["couple","couple with heart","love","woman"],"unicode":"👩‍❤️‍👩"},{"group":1,"hexcode":"1f46a","label":"family","tags":["family"],"unicode":"👪️"},{"group":1,"hexcode":"1f468-200d-1f469-200d-1f466","label":"family: man, woman, boy","tags":["boy","family","man","woman"],"unicode":"👨‍👩‍👦"},{"group":1,"hexcode":"1f468-200d-1f469-200d-1f467","label":"family: man, woman, girl","tags":["family","girl","man","woman"],"unicode":"👨‍👩‍👧"},{"group":1,"hexcode":"1f468-200d-1f469-200d-1f467-200d-1f466","label":"family: man, woman, girl, boy","tags":["boy","family","girl","man","woman"],"unicode":"👨‍👩‍👧‍👦"},{"group":1,"hexcode":"1f468-200d-1f469-200d-1f466-200d-1f466","label":"family: man, woman, boy, boy","tags":["boy","family","man","woman"],"unicode":"👨‍👩‍👦‍👦"},{"group":1,"hexcode":"1f468-200d-1f469-200d-1f467-200d-1f467","label":"family: man, woman, girl, girl","tags":["family","girl","man","woman"],"unicode":"👨‍👩‍👧‍👧"},{"group":1,"hexcode":"1f468-200d-1f468-200d-1f466","label":"family: man, man, boy","tags":["boy","family","man"],"unicode":"👨‍👨‍👦"},{"group":1,"hexcode":"1f468-200d-1f468-200d-1f467","label":"family: man, man, girl","tags":["family","girl","man"],"unicode":"👨‍👨‍👧"},{"group":1,"hexcode":"1f468-200d-1f468-200d-1f467-200d-1f466","label":"family: man, man, girl, boy","tags":["boy","family","girl","man"],"unicode":"👨‍👨‍👧‍👦"},{"group":1,"hexcode":"1f468-200d-1f468-200d-1f466-200d-1f466","label":"family: man, man, boy, boy","tags":["boy","family","man"],"unicode":"👨‍👨‍👦‍👦"},{"group":1,"hexcode":"1f468-200d-1f468-200d-1f467-200d-1f467","label":"family: man, man, girl, girl","tags":["family","girl","man"],"unicode":"👨‍👨‍👧‍👧"},{"group":1,"hexcode":"1f469-200d-1f469-200d-1f466","label":"family: woman, woman, boy","tags":["boy","family","woman"],"unicode":"👩‍👩‍👦"},{"group":1,"hexcode":"1f469-200d-1f469-200d-1f467","label":"family: woman, woman, girl","tags":["family","girl","woman"],"unicode":"👩‍👩‍👧"},{"group":1,"hexcode":"1f469-200d-1f469-200d-1f467-200d-1f466","label":"family: woman, woman, girl, boy","tags":["boy","family","girl","woman"],"unicode":"👩‍👩‍👧‍👦"},{"group":1,"hexcode":"1f469-200d-1f469-200d-1f466-200d-1f466","label":"family: woman, woman, boy, boy","tags":["boy","family","woman"],"unicode":"👩‍👩‍👦‍👦"},{"group":1,"hexcode":"1f469-200d-1f469-200d-1f467-200d-1f467","label":"family: woman, woman, girl, girl","tags":["family","girl","woman"],"unicode":"👩‍👩‍👧‍👧"},{"group":1,"hexcode":"1f468-200d-1f466","label":"family: man, boy","tags":["boy","family","man"],"unicode":"👨‍👦"},{"group":1,"hexcode":"1f468-200d-1f466-200d-1f466","label":"family: man, boy, boy","tags":["boy","family","man"],"unicode":"👨‍👦‍👦"},{"group":1,"hexcode":"1f468-200d-1f467","label":"family: man, girl","tags":["family","girl","man"],"unicode":"👨‍👧"},{"group":1,"hexcode":"1f468-200d-1f467-200d-1f466","label":"family: man, girl, boy","tags":["boy","family","girl","man"],"unicode":"👨‍👧‍👦"},{"group":1,"hexcode":"1f468-200d-1f467-200d-1f467","label":"family: man, girl, girl","tags":["family","girl","man"],"unicode":"👨‍👧‍👧"},{"group":1,"hexcode":"1f469-200d-1f466","label":"family: woman, boy","tags":["boy","family","woman"],"unicode":"👩‍👦"},{"group":1,"hexcode":"1f469-200d-1f466-200d-1f466","label":"family: woman, boy, boy","tags":["boy","family","woman"],"unicode":"👩‍👦‍👦"},{"group":1,"hexcode":"1f469-200d-1f467","label":"family: woman, girl","tags":["family","girl","woman"],"unicode":"👩‍👧"},{"group":1,"hexcode":"1f469-200d-1f467-200d-1f466","label":"family: woman, girl, boy","tags":["boy","family","girl","woman"],"unicode":"👩‍👧‍👦"},{"group":1,"hexcode":"1f469-200d-1f467-200d-1f467","label":"family: woman, girl, girl","tags":["family","girl","woman"],"unicode":"👩‍👧‍👧"},{"group":1,"hexcode":"1f5e3","label":"speaking head","tags":["face","head","silhouette","speak","speaking"],"unicode":"🗣️"},{"group":1,"hexcode":"1f464","label":"bust in silhouette","tags":["bust","silhouette"],"unicode":"👤"},{"group":1,"hexcode":"1f465","label":"busts in silhouette","tags":["bust","silhouette"],"unicode":"👥"},{"group":1,"hexcode":"1fac2","label":"people hugging","tags":["goodbye","hello","hug","thanks"],"unicode":"🫂"},{"group":1,"hexcode":"1f463","label":"footprints","tags":["clothing","footprint","print"],"unicode":"👣"},{"group":3,"hexcode":"1f435","label":"monkey face","tags":["face","monkey"],"unicode":"🐵"},{"group":3,"hexcode":"1f412","label":"monkey","tags":["monkey"],"unicode":"🐒"},{"group":3,"hexcode":"1f98d","label":"gorilla","tags":["gorilla"],"unicode":"🦍"},{"group":3,"hexcode":"1f9a7","label":"orangutan","tags":["ape"],"unicode":"🦧"},{"group":3,"hexcode":"1f436","label":"dog face","tags":["dog","face","pet"],"unicode":"🐶"},{"group":3,"hexcode":"1f415","label":"dog","tags":["pet"],"unicode":"🐕️"},{"group":3,"hexcode":"1f9ae","label":"guide dog","tags":["accessibility","blind","guide"],"unicode":"🦮"},{"group":3,"hexcode":"1f415-200d-1f9ba","label":"service dog","tags":["accessibility","assistance","dog","service"],"unicode":"🐕‍🦺"},{"group":3,"hexcode":"1f429","label":"poodle","tags":["dog"],"unicode":"🐩"},{"group":3,"hexcode":"1f43a","label":"wolf","tags":["face"],"unicode":"🐺"},{"group":3,"hexcode":"1f98a","label":"fox","tags":["face"],"unicode":"🦊"},{"group":3,"hexcode":"1f99d","label":"raccoon","tags":["curious","sly"],"unicode":"🦝"},{"group":3,"hexcode":"1f431","label":"cat face","tags":["cat","face","pet"],"unicode":"🐱"},{"group":3,"hexcode":"1f408","label":"cat","tags":["pet"],"unicode":"🐈️"},{"group":3,"hexcode":"1f408-200d-2b1b","label":"black cat","tags":["black","cat","unlucky"],"unicode":"🐈‍⬛"},{"group":3,"hexcode":"1f981","label":"lion","tags":["face","leo","zodiac"],"unicode":"🦁"},{"group":3,"hexcode":"1f42f","label":"tiger face","tags":["face","tiger"],"unicode":"🐯"},{"group":3,"hexcode":"1f405","label":"tiger","tags":["tiger"],"unicode":"🐅"},{"group":3,"hexcode":"1f406","label":"leopard","tags":["leopard"],"unicode":"🐆"},{"group":3,"hexcode":"1f434","label":"horse face","tags":["face","horse"],"unicode":"🐴"},{"group":3,"hexcode":"1face","label":"moose","tags":["animal","antlers","elk","mammal"],"unicode":"🫎"},{"group":3,"hexcode":"1facf","label":"donkey","tags":["animal","ass","burro","mammal","mule","stubborn"],"unicode":"🫏"},{"group":3,"hexcode":"1f40e","label":"horse","tags":["equestrian","racehorse","racing"],"unicode":"🐎"},{"group":3,"hexcode":"1f984","label":"unicorn","tags":["face"],"unicode":"🦄"},{"group":3,"hexcode":"1f993","label":"zebra","tags":["stripe"],"unicode":"🦓"},{"group":3,"hexcode":"1f98c","label":"deer","tags":["deer"],"unicode":"🦌"},{"group":3,"hexcode":"1f9ac","label":"bison","tags":["buffalo","herd","wisent"],"unicode":"🦬"},{"group":3,"hexcode":"1f42e","label":"cow face","tags":["cow","face"],"unicode":"🐮"},{"group":3,"hexcode":"1f402","label":"ox","tags":["bull","taurus","zodiac"],"unicode":"🐂"},{"group":3,"hexcode":"1f403","label":"water buffalo","tags":["buffalo","water"],"unicode":"🐃"},{"group":3,"hexcode":"1f404","label":"cow","tags":["cow"],"unicode":"🐄"},{"group":3,"hexcode":"1f437","label":"pig face","tags":["face","pig"],"unicode":"🐷"},{"group":3,"hexcode":"1f416","label":"pig","tags":["sow"],"unicode":"🐖"},{"group":3,"hexcode":"1f417","label":"boar","tags":["pig"],"unicode":"🐗"},{"group":3,"hexcode":"1f43d","label":"pig nose","tags":["face","nose","pig"],"unicode":"🐽"},{"group":3,"hexcode":"1f40f","label":"ram","tags":["aries","male","sheep","zodiac"],"unicode":"🐏"},{"group":3,"hexcode":"1f411","label":"ewe","tags":["female","sheep"],"unicode":"🐑"},{"group":3,"hexcode":"1f410","label":"goat","tags":["capricorn","zodiac"],"unicode":"🐐"},{"group":3,"hexcode":"1f42a","label":"camel","tags":["dromedary","hump"],"unicode":"🐪"},{"group":3,"hexcode":"1f42b","label":"two-hump camel","tags":["bactrian","camel","hump"],"unicode":"🐫"},{"group":3,"hexcode":"1f999","label":"llama","tags":["alpaca","guanaco","vicuña","wool"],"unicode":"🦙"},{"group":3,"hexcode":"1f992","label":"giraffe","tags":["spots"],"unicode":"🦒"},{"group":3,"hexcode":"1f418","label":"elephant","tags":["elephant"],"unicode":"🐘"},{"group":3,"hexcode":"1f9a3","label":"mammoth","tags":["extinction","large","tusk","woolly"],"unicode":"🦣"},{"group":3,"hexcode":"1f98f","label":"rhinoceros","tags":["rhinoceros"],"unicode":"🦏"},{"group":3,"hexcode":"1f99b","label":"hippopotamus","tags":["hippo"],"unicode":"🦛"},{"group":3,"hexcode":"1f42d","label":"mouse face","tags":["face","mouse"],"unicode":"🐭"},{"group":3,"hexcode":"1f401","label":"mouse","tags":["mouse"],"unicode":"🐁"},{"group":3,"hexcode":"1f400","label":"rat","tags":["rat"],"unicode":"🐀"},{"group":3,"hexcode":"1f439","label":"hamster","tags":["face","pet"],"unicode":"🐹"},{"group":3,"hexcode":"1f430","label":"rabbit face","tags":["bunny","face","pet","rabbit"],"unicode":"🐰"},{"group":3,"hexcode":"1f407","label":"rabbit","tags":["bunny","pet"],"unicode":"🐇"},{"group":3,"hexcode":"1f43f","label":"chipmunk","tags":["squirrel"],"unicode":"🐿️"},{"group":3,"hexcode":"1f9ab","label":"beaver","tags":["dam"],"unicode":"🦫"},{"group":3,"hexcode":"1f994","label":"hedgehog","tags":["spiny"],"unicode":"🦔"},{"group":3,"hexcode":"1f987","label":"bat","tags":["vampire"],"unicode":"🦇"},{"group":3,"hexcode":"1f43b","label":"bear","tags":["face"],"unicode":"🐻"},{"group":3,"hexcode":"1f43b-200d-2744-fe0f","label":"polar bear","tags":["arctic","bear","white"],"unicode":"🐻‍❄️"},{"group":3,"hexcode":"1f428","label":"koala","tags":["face","marsupial"],"unicode":"🐨"},{"group":3,"hexcode":"1f43c","label":"panda","tags":["face"],"unicode":"🐼"},{"group":3,"hexcode":"1f9a5","label":"sloth","tags":["lazy","slow"],"unicode":"🦥"},{"group":3,"hexcode":"1f9a6","label":"otter","tags":["fishing","playful"],"unicode":"🦦"},{"group":3,"hexcode":"1f9a8","label":"skunk","tags":["stink"],"unicode":"🦨"},{"group":3,"hexcode":"1f998","label":"kangaroo","tags":["joey","jump","marsupial"],"unicode":"🦘"},{"group":3,"hexcode":"1f9a1","label":"badger","tags":["honey badger","pester"],"unicode":"🦡"},{"group":3,"hexcode":"1f43e","label":"paw prints","tags":["feet","paw","print"],"unicode":"🐾"},{"group":3,"hexcode":"1f983","label":"turkey","tags":["bird"],"unicode":"🦃"},{"group":3,"hexcode":"1f414","label":"chicken","tags":["bird"],"unicode":"🐔"},{"group":3,"hexcode":"1f413","label":"rooster","tags":["bird"],"unicode":"🐓"},{"group":3,"hexcode":"1f423","label":"hatching chick","tags":["baby","bird","chick","hatching"],"unicode":"🐣"},{"group":3,"hexcode":"1f424","label":"baby chick","tags":["baby","bird","chick"],"unicode":"🐤"},{"group":3,"hexcode":"1f425","label":"front-facing baby chick","tags":["baby","bird","chick"],"unicode":"🐥"},{"group":3,"hexcode":"1f426","label":"bird","tags":["bird"],"unicode":"🐦️"},{"group":3,"hexcode":"1f427","label":"penguin","tags":["bird"],"unicode":"🐧"},{"group":3,"hexcode":"1f54a","label":"dove","tags":["bird","fly","peace"],"unicode":"🕊️"},{"group":3,"hexcode":"1f985","label":"eagle","tags":["bird"],"unicode":"🦅"},{"group":3,"hexcode":"1f986","label":"duck","tags":["bird"],"unicode":"🦆"},{"group":3,"hexcode":"1f9a2","label":"swan","tags":["bird","cygnet","ugly duckling"],"unicode":"🦢"},{"group":3,"hexcode":"1f989","label":"owl","tags":["bird","wise"],"unicode":"🦉"},{"group":3,"hexcode":"1f9a4","label":"dodo","tags":["extinction","large","mauritius"],"unicode":"🦤"},{"group":3,"hexcode":"1fab6","label":"feather","tags":["bird","flight","light","plumage"],"unicode":"🪶"},{"group":3,"hexcode":"1f9a9","label":"flamingo","tags":["flamboyant","tropical"],"unicode":"🦩"},{"group":3,"hexcode":"1f99a","label":"peacock","tags":["bird","ostentatious","peahen","proud"],"unicode":"🦚"},{"group":3,"hexcode":"1f99c","label":"parrot","tags":["bird","pirate","talk"],"unicode":"🦜"},{"group":3,"hexcode":"1fabd","label":"wing","tags":["angelic","aviation","bird","flying","mythology"],"unicode":"🪽"},{"group":3,"hexcode":"1f426-200d-2b1b","label":"black bird","tags":["bird","black","crow","raven","rook"],"unicode":"🐦‍⬛"},{"group":3,"hexcode":"1fabf","label":"goose","tags":["bird","fowl","honk","silly"],"unicode":"🪿"},{"group":3,"hexcode":"1f438","label":"frog","tags":["face"],"unicode":"🐸"},{"group":3,"hexcode":"1f40a","label":"crocodile","tags":["crocodile"],"unicode":"🐊"},{"group":3,"hexcode":"1f422","label":"turtle","tags":["terrapin","tortoise"],"unicode":"🐢"},{"group":3,"hexcode":"1f98e","label":"lizard","tags":["reptile"],"unicode":"🦎"},{"group":3,"hexcode":"1f40d","label":"snake","tags":["bearer","ophiuchus","serpent","zodiac"],"unicode":"🐍"},{"group":3,"hexcode":"1f432","label":"dragon face","tags":["dragon","face","fairy tale"],"unicode":"🐲"},{"group":3,"hexcode":"1f409","label":"dragon","tags":["fairy tale"],"unicode":"🐉"},{"group":3,"hexcode":"1f995","label":"sauropod","tags":["brachiosaurus","brontosaurus","diplodocus"],"unicode":"🦕"},{"group":3,"hexcode":"1f996","label":"t-rex","tags":["t-rex","tyrannosaurus rex"],"unicode":"🦖"},{"group":3,"hexcode":"1f433","label":"spouting whale","tags":["face","spouting","whale"],"unicode":"🐳"},{"group":3,"hexcode":"1f40b","label":"whale","tags":["whale"],"unicode":"🐋"},{"group":3,"hexcode":"1f42c","label":"dolphin","tags":["flipper"],"unicode":"🐬"},{"group":3,"hexcode":"1f9ad","label":"seal","tags":["sea lion"],"unicode":"🦭"},{"group":3,"hexcode":"1f41f","label":"fish","tags":["pisces","zodiac"],"unicode":"🐟️"},{"group":3,"hexcode":"1f420","label":"tropical fish","tags":["fish","tropical"],"unicode":"🐠"},{"group":3,"hexcode":"1f421","label":"blowfish","tags":["fish"],"unicode":"🐡"},{"group":3,"hexcode":"1f988","label":"shark","tags":["fish"],"unicode":"🦈"},{"group":3,"hexcode":"1f419","label":"octopus","tags":["octopus"],"unicode":"🐙"},{"group":3,"hexcode":"1f41a","label":"spiral shell","tags":["shell","spiral"],"unicode":"🐚"},{"group":3,"hexcode":"1fab8","label":"coral","tags":["ocean","reef"],"unicode":"🪸"},{"group":3,"hexcode":"1fabc","label":"jellyfish","tags":["burn","invertebrate","jelly","marine","ouch","stinger"],"unicode":"🪼"},{"group":3,"hexcode":"1f40c","label":"snail","tags":["snail"],"unicode":"🐌"},{"group":3,"hexcode":"1f98b","label":"butterfly","tags":["insect","pretty"],"unicode":"🦋"},{"group":3,"hexcode":"1f41b","label":"bug","tags":["insect"],"unicode":"🐛"},{"group":3,"hexcode":"1f41c","label":"ant","tags":["insect"],"unicode":"🐜"},{"group":3,"hexcode":"1f41d","label":"honeybee","tags":["bee","insect"],"unicode":"🐝"},{"group":3,"hexcode":"1fab2","label":"beetle","tags":["bug","insect"],"unicode":"🪲"},{"group":3,"hexcode":"1f41e","label":"lady beetle","tags":["beetle","insect","ladybird","ladybug"],"unicode":"🐞"},{"group":3,"hexcode":"1f997","label":"cricket","tags":["grasshopper"],"unicode":"🦗"},{"group":3,"hexcode":"1fab3","label":"cockroach","tags":["insect","pest","roach"],"unicode":"🪳"},{"group":3,"hexcode":"1f577","label":"spider","tags":["insect"],"unicode":"🕷️"},{"group":3,"hexcode":"1f578","label":"spider web","tags":["spider","web"],"unicode":"🕸️"},{"group":3,"hexcode":"1f982","label":"scorpion","tags":["scorpio","zodiac"],"unicode":"🦂"},{"group":3,"hexcode":"1f99f","label":"mosquito","tags":["disease","fever","malaria","pest","virus"],"unicode":"🦟"},{"group":3,"hexcode":"1fab0","label":"fly","tags":["disease","maggot","pest","rotting"],"unicode":"🪰"},{"group":3,"hexcode":"1fab1","label":"worm","tags":["annelid","earthworm","parasite"],"unicode":"🪱"},{"group":3,"hexcode":"1f9a0","label":"microbe","tags":["amoeba","bacteria","virus"],"unicode":"🦠"},{"group":3,"hexcode":"1f490","label":"bouquet","tags":["flower"],"unicode":"💐"},{"group":3,"hexcode":"1f338","label":"cherry blossom","tags":["blossom","cherry","flower"],"unicode":"🌸"},{"group":3,"hexcode":"1f4ae","label":"white flower","tags":["flower"],"unicode":"💮"},{"group":3,"hexcode":"1fab7","label":"lotus","tags":["buddhism","flower","hinduism","purity"],"unicode":"🪷"},{"group":3,"hexcode":"1f3f5","label":"rosette","tags":["plant"],"unicode":"🏵️"},{"group":3,"hexcode":"1f339","label":"rose","tags":["flower"],"unicode":"🌹"},{"group":3,"hexcode":"1f940","label":"wilted flower","tags":["flower","wilted"],"unicode":"🥀"},{"group":3,"hexcode":"1f33a","label":"hibiscus","tags":["flower"],"unicode":"🌺"},{"group":3,"hexcode":"1f33b","label":"sunflower","tags":["flower","sun"],"unicode":"🌻"},{"group":3,"hexcode":"1f33c","label":"blossom","tags":["flower"],"unicode":"🌼"},{"group":3,"hexcode":"1f337","label":"tulip","tags":["flower"],"unicode":"🌷"},{"group":3,"hexcode":"1fabb","label":"hyacinth","tags":["bluebonnet","flower","lavender","lupine","snapdragon"],"unicode":"🪻"},{"group":3,"hexcode":"1f331","label":"seedling","tags":["young"],"unicode":"🌱"},{"group":3,"hexcode":"1fab4","label":"potted plant","tags":["boring","grow","house","nurturing","plant","useless"],"unicode":"🪴"},{"group":3,"hexcode":"1f332","label":"evergreen tree","tags":["tree"],"unicode":"🌲"},{"group":3,"hexcode":"1f333","label":"deciduous tree","tags":["deciduous","shedding","tree"],"unicode":"🌳"},{"group":3,"hexcode":"1f334","label":"palm tree","tags":["palm","tree"],"unicode":"🌴"},{"group":3,"hexcode":"1f335","label":"cactus","tags":["plant"],"unicode":"🌵"},{"group":3,"hexcode":"1f33e","label":"sheaf of rice","tags":["ear","grain","rice"],"unicode":"🌾"},{"group":3,"hexcode":"1f33f","label":"herb","tags":["leaf"],"unicode":"🌿"},{"group":3,"hexcode":"2618","label":"shamrock","tags":["plant"],"unicode":"☘️"},{"group":3,"hexcode":"1f340","label":"four leaf clover","tags":["4","clover","four","four-leaf clover","leaf"],"unicode":"🍀"},{"group":3,"hexcode":"1f341","label":"maple leaf","tags":["falling","leaf","maple"],"unicode":"🍁"},{"group":3,"hexcode":"1f342","label":"fallen leaf","tags":["falling","leaf"],"unicode":"🍂"},{"group":3,"hexcode":"1f343","label":"leaf fluttering in wind","tags":["blow","flutter","leaf","wind"],"unicode":"🍃"},{"group":3,"hexcode":"1fab9","label":"empty nest","tags":["nesting"],"unicode":"🪹"},{"group":3,"hexcode":"1faba","label":"nest with eggs","tags":["nesting"],"unicode":"🪺"},{"group":3,"hexcode":"1f344","label":"mushroom","tags":["toadstool"],"unicode":"🍄"},{"group":4,"hexcode":"1f347","label":"grapes","tags":["fruit","grape"],"unicode":"🍇"},{"group":4,"hexcode":"1f348","label":"melon","tags":["fruit"],"unicode":"🍈"},{"group":4,"hexcode":"1f349","label":"watermelon","tags":["fruit"],"unicode":"🍉"},{"group":4,"hexcode":"1f34a","label":"tangerine","tags":["fruit","orange"],"unicode":"🍊"},{"group":4,"hexcode":"1f34b","label":"lemon","tags":["citrus","fruit"],"unicode":"🍋"},{"group":4,"hexcode":"1f34c","label":"banana","tags":["fruit"],"unicode":"🍌"},{"group":4,"hexcode":"1f34d","label":"pineapple","tags":["fruit"],"unicode":"🍍"},{"group":4,"hexcode":"1f96d","label":"mango","tags":["fruit","tropical"],"unicode":"🥭"},{"group":4,"hexcode":"1f34e","label":"red apple","tags":["apple","fruit","red"],"unicode":"🍎"},{"group":4,"hexcode":"1f34f","label":"green apple","tags":["apple","fruit","green"],"unicode":"🍏"},{"group":4,"hexcode":"1f350","label":"pear","tags":["fruit"],"unicode":"🍐"},{"group":4,"hexcode":"1f351","label":"peach","tags":["fruit"],"unicode":"🍑"},{"group":4,"hexcode":"1f352","label":"cherries","tags":["berries","cherry","fruit","red"],"unicode":"🍒"},{"group":4,"hexcode":"1f353","label":"strawberry","tags":["berry","fruit"],"unicode":"🍓"},{"group":4,"hexcode":"1fad0","label":"blueberries","tags":["berry","bilberry","blue","blueberry"],"unicode":"🫐"},{"group":4,"hexcode":"1f95d","label":"kiwi fruit","tags":["food","fruit","kiwi"],"unicode":"🥝"},{"group":4,"hexcode":"1f345","label":"tomato","tags":["fruit","vegetable"],"unicode":"🍅"},{"group":4,"hexcode":"1fad2","label":"olive","tags":["food"],"unicode":"🫒"},{"group":4,"hexcode":"1f965","label":"coconut","tags":["palm","piña colada"],"unicode":"🥥"},{"group":4,"hexcode":"1f951","label":"avocado","tags":["food","fruit"],"unicode":"🥑"},{"group":4,"hexcode":"1f346","label":"eggplant","tags":["aubergine","vegetable"],"unicode":"🍆"},{"group":4,"hexcode":"1f954","label":"potato","tags":["food","vegetable"],"unicode":"🥔"},{"group":4,"hexcode":"1f955","label":"carrot","tags":["food","vegetable"],"unicode":"🥕"},{"group":4,"hexcode":"1f33d","label":"ear of corn","tags":["corn","ear","maize","maze"],"unicode":"🌽"},{"group":4,"hexcode":"1f336","label":"hot pepper","tags":["hot","pepper"],"unicode":"🌶️"},{"group":4,"hexcode":"1fad1","label":"bell pepper","tags":["capsicum","pepper","vegetable"],"unicode":"🫑"},{"group":4,"hexcode":"1f952","label":"cucumber","tags":["food","pickle","vegetable"],"unicode":"🥒"},{"group":4,"hexcode":"1f96c","label":"leafy green","tags":["bok choy","cabbage","kale","lettuce"],"unicode":"🥬"},{"group":4,"hexcode":"1f966","label":"broccoli","tags":["wild cabbage"],"unicode":"🥦"},{"group":4,"hexcode":"1f9c4","label":"garlic","tags":["flavoring"],"unicode":"🧄"},{"group":4,"hexcode":"1f9c5","label":"onion","tags":["flavoring"],"unicode":"🧅"},{"group":4,"hexcode":"1f95c","label":"peanuts","tags":["food","nut","peanut","vegetable"],"unicode":"🥜"},{"group":4,"hexcode":"1fad8","label":"beans","tags":["food","kidney","legume"],"unicode":"🫘"},{"group":4,"hexcode":"1f330","label":"chestnut","tags":["plant"],"unicode":"🌰"},{"group":4,"hexcode":"1fada","label":"ginger root","tags":["beer","root","spice"],"unicode":"🫚"},{"group":4,"hexcode":"1fadb","label":"pea pod","tags":["beans","edamame","legume","pea","pod","vegetable"],"unicode":"🫛"},{"group":4,"hexcode":"1f35e","label":"bread","tags":["loaf"],"unicode":"🍞"},{"group":4,"hexcode":"1f950","label":"croissant","tags":["bread","breakfast","food","french","roll"],"unicode":"🥐"},{"group":4,"hexcode":"1f956","label":"baguette bread","tags":["baguette","bread","food","french"],"unicode":"🥖"},{"group":4,"hexcode":"1fad3","label":"flatbread","tags":["arepa","lavash","naan","pita"],"unicode":"🫓"},{"group":4,"hexcode":"1f968","label":"pretzel","tags":["twisted"],"unicode":"🥨"},{"group":4,"hexcode":"1f96f","label":"bagel","tags":["bakery","breakfast","schmear"],"unicode":"🥯"},{"group":4,"hexcode":"1f95e","label":"pancakes","tags":["breakfast","crêpe","food","hotcake","pancake"],"unicode":"🥞"},{"group":4,"hexcode":"1f9c7","label":"waffle","tags":["breakfast","indecisive","iron"],"unicode":"🧇"},{"group":4,"hexcode":"1f9c0","label":"cheese wedge","tags":["cheese"],"unicode":"🧀"},{"group":4,"hexcode":"1f356","label":"meat on bone","tags":["bone","meat"],"unicode":"🍖"},{"group":4,"hexcode":"1f357","label":"poultry leg","tags":["bone","chicken","drumstick","leg","poultry"],"unicode":"🍗"},{"group":4,"hexcode":"1f969","label":"cut of meat","tags":["chop","lambchop","porkchop","steak"],"unicode":"🥩"},{"group":4,"hexcode":"1f953","label":"bacon","tags":["breakfast","food","meat"],"unicode":"🥓"},{"group":4,"hexcode":"1f354","label":"hamburger","tags":["burger"],"unicode":"🍔"},{"group":4,"hexcode":"1f35f","label":"french fries","tags":["french","fries"],"unicode":"🍟"},{"group":4,"hexcode":"1f355","label":"pizza","tags":["cheese","slice"],"unicode":"🍕"},{"group":4,"hexcode":"1f32d","label":"hot dog","tags":["frankfurter","hotdog","sausage"],"unicode":"🌭"},{"group":4,"hexcode":"1f96a","label":"sandwich","tags":["bread"],"unicode":"🥪"},{"group":4,"hexcode":"1f32e","label":"taco","tags":["mexican"],"unicode":"🌮"},{"group":4,"hexcode":"1f32f","label":"burrito","tags":["mexican","wrap"],"unicode":"🌯"},{"group":4,"hexcode":"1fad4","label":"tamale","tags":["mexican","wrapped"],"unicode":"🫔"},{"group":4,"hexcode":"1f959","label":"stuffed flatbread","tags":["falafel","flatbread","food","gyro","kebab","stuffed"],"unicode":"🥙"},{"group":4,"hexcode":"1f9c6","label":"falafel","tags":["chickpea","meatball"],"unicode":"🧆"},{"group":4,"hexcode":"1f95a","label":"egg","tags":["breakfast","food"],"unicode":"🥚"},{"group":4,"hexcode":"1f373","label":"cooking","tags":["breakfast","egg","frying","pan"],"unicode":"🍳"},{"group":4,"hexcode":"1f958","label":"shallow pan of food","tags":["casserole","food","paella","pan","shallow"],"unicode":"🥘"},{"group":4,"hexcode":"1f372","label":"pot of food","tags":["pot","stew"],"unicode":"🍲"},{"group":4,"hexcode":"1fad5","label":"fondue","tags":["cheese","chocolate","melted","pot","swiss"],"unicode":"🫕"},{"group":4,"hexcode":"1f963","label":"bowl with spoon","tags":["breakfast","cereal","congee"],"unicode":"🥣"},{"group":4,"hexcode":"1f957","label":"green salad","tags":["food","green","salad"],"unicode":"🥗"},{"group":4,"hexcode":"1f37f","label":"popcorn","tags":["popcorn"],"unicode":"🍿"},{"group":4,"hexcode":"1f9c8","label":"butter","tags":["dairy"],"unicode":"🧈"},{"group":4,"hexcode":"1f9c2","label":"salt","tags":["condiment","shaker"],"unicode":"🧂"},{"group":4,"hexcode":"1f96b","label":"canned food","tags":["can"],"unicode":"🥫"},{"group":4,"hexcode":"1f371","label":"bento box","tags":["bento","box"],"unicode":"🍱"},{"group":4,"hexcode":"1f358","label":"rice cracker","tags":["cracker","rice"],"unicode":"🍘"},{"group":4,"hexcode":"1f359","label":"rice ball","tags":["ball","japanese","rice"],"unicode":"🍙"},{"group":4,"hexcode":"1f35a","label":"cooked rice","tags":["cooked","rice"],"unicode":"🍚"},{"group":4,"hexcode":"1f35b","label":"curry rice","tags":["curry","rice"],"unicode":"🍛"},{"group":4,"hexcode":"1f35c","label":"steaming bowl","tags":["bowl","noodle","ramen","steaming"],"unicode":"🍜"},{"group":4,"hexcode":"1f35d","label":"spaghetti","tags":["pasta"],"unicode":"🍝"},{"group":4,"hexcode":"1f360","label":"roasted sweet potato","tags":["potato","roasted","sweet"],"unicode":"🍠"},{"group":4,"hexcode":"1f362","label":"oden","tags":["kebab","seafood","skewer","stick"],"unicode":"🍢"},{"group":4,"hexcode":"1f363","label":"sushi","tags":["sushi"],"unicode":"🍣"},{"group":4,"hexcode":"1f364","label":"fried shrimp","tags":["fried","prawn","shrimp","tempura"],"unicode":"🍤"},{"group":4,"hexcode":"1f365","label":"fish cake with swirl","tags":["cake","fish","pastry","swirl"],"unicode":"🍥"},{"group":4,"hexcode":"1f96e","label":"moon cake","tags":["autumn","festival","yuèbǐng"],"unicode":"🥮"},{"group":4,"hexcode":"1f361","label":"dango","tags":["dessert","japanese","skewer","stick","sweet"],"unicode":"🍡"},{"group":4,"hexcode":"1f95f","label":"dumpling","tags":["empanada","gyōza","jiaozi","pierogi","potsticker"],"unicode":"🥟"},{"group":4,"hexcode":"1f960","label":"fortune cookie","tags":["prophecy"],"unicode":"🥠"},{"group":4,"hexcode":"1f961","label":"takeout box","tags":["oyster pail"],"unicode":"🥡"},{"group":4,"hexcode":"1f980","label":"crab","tags":["cancer","zodiac"],"unicode":"🦀"},{"group":4,"hexcode":"1f99e","label":"lobster","tags":["bisque","claws","seafood"],"unicode":"🦞"},{"group":4,"hexcode":"1f990","label":"shrimp","tags":["food","shellfish","small"],"unicode":"🦐"},{"group":4,"hexcode":"1f991","label":"squid","tags":["food","molusc"],"unicode":"🦑"},{"group":4,"hexcode":"1f9aa","label":"oyster","tags":["diving","pearl"],"unicode":"🦪"},{"group":4,"hexcode":"1f366","label":"soft ice cream","tags":["cream","dessert","ice","icecream","soft","sweet"],"unicode":"🍦"},{"group":4,"hexcode":"1f367","label":"shaved ice","tags":["dessert","ice","shaved","sweet"],"unicode":"🍧"},{"group":4,"hexcode":"1f368","label":"ice cream","tags":["cream","dessert","ice","sweet"],"unicode":"🍨"},{"group":4,"hexcode":"1f369","label":"doughnut","tags":["breakfast","dessert","donut","sweet"],"unicode":"🍩"},{"group":4,"hexcode":"1f36a","label":"cookie","tags":["dessert","sweet"],"unicode":"🍪"},{"group":4,"hexcode":"1f382","label":"birthday cake","tags":["birthday","cake","celebration","dessert","pastry","sweet"],"unicode":"🎂"},{"group":4,"hexcode":"1f370","label":"shortcake","tags":["cake","dessert","pastry","slice","sweet"],"unicode":"🍰"},{"group":4,"hexcode":"1f9c1","label":"cupcake","tags":["bakery","sweet"],"unicode":"🧁"},{"group":4,"hexcode":"1f967","label":"pie","tags":["filling","pastry"],"unicode":"🥧"},{"group":4,"hexcode":"1f36b","label":"chocolate bar","tags":["bar","chocolate","dessert","sweet"],"unicode":"🍫"},{"group":4,"hexcode":"1f36c","label":"candy","tags":["dessert","sweet"],"unicode":"🍬"},{"group":4,"hexcode":"1f36d","label":"lollipop","tags":["candy","dessert","sweet"],"unicode":"🍭"},{"group":4,"hexcode":"1f36e","label":"custard","tags":["dessert","pudding","sweet"],"unicode":"🍮"},{"group":4,"hexcode":"1f36f","label":"honey pot","tags":["honey","honeypot","pot","sweet"],"unicode":"🍯"},{"group":4,"hexcode":"1f37c","label":"baby bottle","tags":["baby","bottle","drink","milk"],"unicode":"🍼"},{"group":4,"hexcode":"1f95b","label":"glass of milk","tags":["drink","glass","milk"],"unicode":"🥛"},{"group":4,"hexcode":"2615","label":"hot beverage","tags":["beverage","coffee","drink","hot","steaming","tea"],"unicode":"☕️"},{"group":4,"hexcode":"1fad6","label":"teapot","tags":["drink","pot","tea"],"unicode":"🫖"},{"group":4,"hexcode":"1f375","label":"teacup without handle","tags":["beverage","cup","drink","tea","teacup"],"unicode":"🍵"},{"group":4,"hexcode":"1f376","label":"sake","tags":["bar","beverage","bottle","cup","drink"],"unicode":"🍶"},{"group":4,"hexcode":"1f37e","label":"bottle with popping cork","tags":["bar","bottle","cork","drink","popping"],"unicode":"🍾"},{"group":4,"hexcode":"1f377","label":"wine glass","tags":["bar","beverage","drink","glass","wine"],"unicode":"🍷"},{"group":4,"hexcode":"1f378","label":"cocktail glass","tags":["bar","cocktail","drink","glass"],"unicode":"🍸️"},{"group":4,"hexcode":"1f379","label":"tropical drink","tags":["bar","drink","tropical"],"unicode":"🍹"},{"group":4,"hexcode":"1f37a","label":"beer mug","tags":["bar","beer","drink","mug"],"unicode":"🍺"},{"group":4,"hexcode":"1f37b","label":"clinking beer mugs","tags":["bar","beer","clink","drink","mug"],"unicode":"🍻"},{"group":4,"hexcode":"1f942","label":"clinking glasses","tags":["celebrate","clink","drink","glass"],"unicode":"🥂"},{"group":4,"hexcode":"1f943","label":"tumbler glass","tags":["glass","liquor","shot","tumbler","whisky"],"unicode":"🥃"},{"group":4,"hexcode":"1fad7","label":"pouring liquid","tags":["drink","empty","glass","spill"],"unicode":"🫗"},{"group":4,"hexcode":"1f964","label":"cup with straw","tags":["juice","soda"],"unicode":"🥤"},{"group":4,"hexcode":"1f9cb","label":"bubble tea","tags":["bubble","milk","pearl","tea"],"unicode":"🧋"},{"group":4,"hexcode":"1f9c3","label":"beverage box","tags":["beverage","box","juice","straw","sweet"],"unicode":"🧃"},{"group":4,"hexcode":"1f9c9","label":"mate","tags":["drink"],"unicode":"🧉"},{"group":4,"hexcode":"1f9ca","label":"ice","tags":["cold","ice cube","iceberg"],"unicode":"🧊"},{"group":4,"hexcode":"1f962","label":"chopsticks","tags":["hashi"],"unicode":"🥢"},{"group":4,"hexcode":"1f37d","label":"fork and knife with plate","tags":["cooking","fork","knife","plate"],"unicode":"🍽️"},{"group":4,"hexcode":"1f374","label":"fork and knife","tags":["cooking","cutlery","fork","knife"],"unicode":"🍴"},{"group":4,"hexcode":"1f944","label":"spoon","tags":["tableware"],"unicode":"🥄"},{"group":4,"hexcode":"1f52a","label":"kitchen knife","tags":["cooking","hocho","knife","tool","weapon"],"unicode":"🔪"},{"group":4,"hexcode":"1fad9","label":"jar","tags":["condiment","container","empty","sauce","store"],"unicode":"🫙"},{"group":4,"hexcode":"1f3fa","label":"amphora","tags":["aquarius","cooking","drink","jug","zodiac"],"unicode":"🏺"},{"group":5,"hexcode":"1f30d","label":"globe showing europe-africa","tags":["africa","earth","europe","globe","globe showing europe-africa","world"],"unicode":"🌍️"},{"group":5,"hexcode":"1f30e","label":"globe showing americas","tags":["americas","earth","globe","globe showing americas","world"],"unicode":"🌎️"},{"group":5,"hexcode":"1f30f","label":"globe showing asia-australia","tags":["asia","australia","earth","globe","globe showing asia-australia","world"],"unicode":"🌏️"},{"group":5,"hexcode":"1f310","label":"globe with meridians","tags":["earth","globe","meridians","world"],"unicode":"🌐"},{"group":5,"hexcode":"1f5fa","label":"world map","tags":["map","world"],"unicode":"🗺️"},{"group":5,"hexcode":"1f5fe","label":"map of japan","tags":["japan","map","map of japan"],"unicode":"🗾"},{"group":5,"hexcode":"1f9ed","label":"compass","tags":["magnetic","navigation","orienteering"],"unicode":"🧭"},{"group":5,"hexcode":"1f3d4","label":"snow-capped mountain","tags":["cold","mountain","snow"],"unicode":"🏔️"},{"group":5,"hexcode":"26f0","label":"mountain","tags":["mountain"],"unicode":"⛰️"},{"group":5,"hexcode":"1f30b","label":"volcano","tags":["eruption","mountain"],"unicode":"🌋"},{"group":5,"hexcode":"1f5fb","label":"mount fuji","tags":["fuji","mountain"],"unicode":"🗻"},{"group":5,"hexcode":"1f3d5","label":"camping","tags":["camping"],"unicode":"🏕️"},{"group":5,"hexcode":"1f3d6","label":"beach with umbrella","tags":["beach","umbrella"],"unicode":"🏖️"},{"group":5,"hexcode":"1f3dc","label":"desert","tags":["desert"],"unicode":"🏜️"},{"group":5,"hexcode":"1f3dd","label":"desert island","tags":["desert","island"],"unicode":"🏝️"},{"group":5,"hexcode":"1f3de","label":"national park","tags":["park"],"unicode":"🏞️"},{"group":5,"hexcode":"1f3df","label":"stadium","tags":["stadium"],"unicode":"🏟️"},{"group":5,"hexcode":"1f3db","label":"classical building","tags":["classical"],"unicode":"🏛️"},{"group":5,"hexcode":"1f3d7","label":"building construction","tags":["construction"],"unicode":"🏗️"},{"group":5,"hexcode":"1f9f1","label":"brick","tags":["bricks","clay","mortar","wall"],"unicode":"🧱"},{"group":5,"hexcode":"1faa8","label":"rock","tags":["boulder","heavy","solid","stone"],"unicode":"🪨"},{"group":5,"hexcode":"1fab5","label":"wood","tags":["log","lumber","timber"],"unicode":"🪵"},{"group":5,"hexcode":"1f6d6","label":"hut","tags":["house","roundhouse","yurt"],"unicode":"🛖"},{"group":5,"hexcode":"1f3d8","label":"houses","tags":["houses"],"unicode":"🏘️"},{"group":5,"hexcode":"1f3da","label":"derelict house","tags":["derelict","house"],"unicode":"🏚️"},{"group":5,"hexcode":"1f3e0","label":"house","tags":["home"],"unicode":"🏠️"},{"group":5,"hexcode":"1f3e1","label":"house with garden","tags":["garden","home","house"],"unicode":"🏡"},{"group":5,"hexcode":"1f3e2","label":"office building","tags":["building"],"unicode":"🏢"},{"group":5,"hexcode":"1f3e3","label":"japanese post office","tags":["japanese","japanese post office","post"],"unicode":"🏣"},{"group":5,"hexcode":"1f3e4","label":"post office","tags":["european","post"],"unicode":"🏤"},{"group":5,"hexcode":"1f3e5","label":"hospital","tags":["doctor","medicine"],"unicode":"🏥"},{"group":5,"hexcode":"1f3e6","label":"bank","tags":["building"],"unicode":"🏦"},{"group":5,"hexcode":"1f3e8","label":"hotel","tags":["building"],"unicode":"🏨"},{"group":5,"hexcode":"1f3e9","label":"love hotel","tags":["hotel","love"],"unicode":"🏩"},{"group":5,"hexcode":"1f3ea","label":"convenience store","tags":["convenience","store"],"unicode":"🏪"},{"group":5,"hexcode":"1f3eb","label":"school","tags":["building"],"unicode":"🏫"},{"group":5,"hexcode":"1f3ec","label":"department store","tags":["department","store"],"unicode":"🏬"},{"group":5,"hexcode":"1f3ed","label":"factory","tags":["building"],"unicode":"🏭️"},{"group":5,"hexcode":"1f3ef","label":"japanese castle","tags":["castle","japanese"],"unicode":"🏯"},{"group":5,"hexcode":"1f3f0","label":"castle","tags":["european"],"unicode":"🏰"},{"group":5,"hexcode":"1f492","label":"wedding","tags":["chapel","romance"],"unicode":"💒"},{"group":5,"hexcode":"1f5fc","label":"tokyo tower","tags":["tokyo","tower"],"unicode":"🗼"},{"group":5,"hexcode":"1f5fd","label":"statue of liberty","tags":["liberty","statue","statue of liberty"],"unicode":"🗽"},{"group":5,"hexcode":"26ea","label":"church","tags":["christian","cross","religion"],"unicode":"⛪️"},{"group":5,"hexcode":"1f54c","label":"mosque","tags":["islam","muslim","religion"],"unicode":"🕌"},{"group":5,"hexcode":"1f6d5","label":"hindu temple","tags":["hindu","temple"],"unicode":"🛕"},{"group":5,"hexcode":"1f54d","label":"synagogue","tags":["jew","jewish","religion","temple"],"unicode":"🕍"},{"group":5,"hexcode":"26e9","label":"shinto shrine","tags":["religion","shinto","shrine"],"unicode":"⛩️"},{"group":5,"hexcode":"1f54b","label":"kaaba","tags":["islam","muslim","religion"],"unicode":"🕋"},{"group":5,"hexcode":"26f2","label":"fountain","tags":["fountain"],"unicode":"⛲️"},{"group":5,"hexcode":"26fa","label":"tent","tags":["camping"],"unicode":"⛺️"},{"group":5,"hexcode":"1f301","label":"foggy","tags":["fog"],"unicode":"🌁"},{"group":5,"hexcode":"1f303","label":"night with stars","tags":["night","star"],"unicode":"🌃"},{"group":5,"hexcode":"1f3d9","label":"cityscape","tags":["city"],"unicode":"🏙️"},{"group":5,"hexcode":"1f304","label":"sunrise over mountains","tags":["morning","mountain","sun","sunrise"],"unicode":"🌄"},{"group":5,"hexcode":"1f305","label":"sunrise","tags":["morning","sun"],"unicode":"🌅"},{"group":5,"hexcode":"1f306","label":"cityscape at dusk","tags":["city","dusk","evening","landscape","sunset"],"unicode":"🌆"},{"group":5,"hexcode":"1f307","label":"sunset","tags":["dusk","sun"],"unicode":"🌇"},{"group":5,"hexcode":"1f309","label":"bridge at night","tags":["bridge","night"],"unicode":"🌉"},{"group":5,"hexcode":"2668","label":"hot springs","tags":["hot","hotsprings","springs","steaming"],"unicode":"♨️"},{"group":5,"hexcode":"1f3a0","label":"carousel horse","tags":["carousel","horse"],"unicode":"🎠"},{"group":5,"hexcode":"1f6dd","label":"playground slide","tags":["amusement park","play"],"unicode":"🛝"},{"group":5,"hexcode":"1f3a1","label":"ferris wheel","tags":["amusement park","ferris","wheel"],"unicode":"🎡"},{"group":5,"hexcode":"1f3a2","label":"roller coaster","tags":["amusement park","coaster","roller"],"unicode":"🎢"},{"group":5,"hexcode":"1f488","label":"barber pole","tags":["barber","haircut","pole"],"unicode":"💈"},{"group":5,"hexcode":"1f3aa","label":"circus tent","tags":["circus","tent"],"unicode":"🎪"},{"group":5,"hexcode":"1f682","label":"locomotive","tags":["engine","railway","steam","train"],"unicode":"🚂"},{"group":5,"hexcode":"1f683","label":"railway car","tags":["car","electric","railway","train","tram","trolleybus"],"unicode":"🚃"},{"group":5,"hexcode":"1f684","label":"high-speed train","tags":["railway","shinkansen","speed","train"],"unicode":"🚄"},{"group":5,"hexcode":"1f685","label":"bullet train","tags":["bullet","railway","shinkansen","speed","train"],"unicode":"🚅"},{"group":5,"hexcode":"1f686","label":"train","tags":["railway"],"unicode":"🚆"},{"group":5,"hexcode":"1f687","label":"metro","tags":["subway"],"unicode":"🚇️"},{"group":5,"hexcode":"1f688","label":"light rail","tags":["railway"],"unicode":"🚈"},{"group":5,"hexcode":"1f689","label":"station","tags":["railway","train"],"unicode":"🚉"},{"group":5,"hexcode":"1f68a","label":"tram","tags":["trolleybus"],"unicode":"🚊"},{"group":5,"hexcode":"1f69d","label":"monorail","tags":["vehicle"],"unicode":"🚝"},{"group":5,"hexcode":"1f69e","label":"mountain railway","tags":["car","mountain","railway"],"unicode":"🚞"},{"group":5,"hexcode":"1f68b","label":"tram car","tags":["car","tram","trolleybus"],"unicode":"🚋"},{"group":5,"hexcode":"1f68c","label":"bus","tags":["vehicle"],"unicode":"🚌"},{"group":5,"hexcode":"1f68d","label":"oncoming bus","tags":["bus","oncoming"],"unicode":"🚍️"},{"group":5,"hexcode":"1f68e","label":"trolleybus","tags":["bus","tram","trolley"],"unicode":"🚎"},{"group":5,"hexcode":"1f690","label":"minibus","tags":["bus"],"unicode":"🚐"},{"group":5,"hexcode":"1f691","label":"ambulance","tags":["vehicle"],"unicode":"🚑️"},{"group":5,"hexcode":"1f692","label":"fire engine","tags":["engine","fire","truck"],"unicode":"🚒"},{"group":5,"hexcode":"1f693","label":"police car","tags":["car","patrol","police"],"unicode":"🚓"},{"group":5,"hexcode":"1f694","label":"oncoming police car","tags":["car","oncoming","police"],"unicode":"🚔️"},{"group":5,"hexcode":"1f695","label":"taxi","tags":["vehicle"],"unicode":"🚕"},{"group":5,"hexcode":"1f696","label":"oncoming taxi","tags":["oncoming","taxi"],"unicode":"🚖"},{"group":5,"hexcode":"1f697","label":"automobile","tags":["car"],"unicode":"🚗"},{"group":5,"hexcode":"1f698","label":"oncoming automobile","tags":["automobile","car","oncoming"],"unicode":"🚘️"},{"group":5,"hexcode":"1f699","label":"sport utility vehicle","tags":["recreational","sport utility"],"unicode":"🚙"},{"group":5,"hexcode":"1f6fb","label":"pickup truck","tags":["pick-up","pickup","truck"],"unicode":"🛻"},{"group":5,"hexcode":"1f69a","label":"delivery truck","tags":["delivery","truck"],"unicode":"🚚"},{"group":5,"hexcode":"1f69b","label":"articulated lorry","tags":["lorry","semi","truck"],"unicode":"🚛"},{"group":5,"hexcode":"1f69c","label":"tractor","tags":["vehicle"],"unicode":"🚜"},{"group":5,"hexcode":"1f3ce","label":"racing car","tags":["car","racing"],"unicode":"🏎️"},{"group":5,"hexcode":"1f3cd","label":"motorcycle","tags":["racing"],"unicode":"🏍️"},{"group":5,"hexcode":"1f6f5","label":"motor scooter","tags":["motor","scooter"],"unicode":"🛵"},{"group":5,"hexcode":"1f9bd","label":"manual wheelchair","tags":["accessibility"],"unicode":"🦽"},{"group":5,"hexcode":"1f9bc","label":"motorized wheelchair","tags":["accessibility"],"unicode":"🦼"},{"group":5,"hexcode":"1f6fa","label":"auto rickshaw","tags":["tuk tuk"],"unicode":"🛺"},{"group":5,"hexcode":"1f6b2","label":"bicycle","tags":["bike"],"unicode":"🚲️"},{"group":5,"hexcode":"1f6f4","label":"kick scooter","tags":["kick","scooter"],"unicode":"🛴"},{"group":5,"hexcode":"1f6f9","label":"skateboard","tags":["board"],"unicode":"🛹"},{"group":5,"hexcode":"1f6fc","label":"roller skate","tags":["roller","skate"],"unicode":"🛼"},{"group":5,"hexcode":"1f68f","label":"bus stop","tags":["bus","stop"],"unicode":"🚏"},{"group":5,"hexcode":"1f6e3","label":"motorway","tags":["highway","road"],"unicode":"🛣️"},{"group":5,"hexcode":"1f6e4","label":"railway track","tags":["railway","train"],"unicode":"🛤️"},{"group":5,"hexcode":"1f6e2","label":"oil drum","tags":["drum","oil"],"unicode":"🛢️"},{"group":5,"hexcode":"26fd","label":"fuel pump","tags":["diesel","fuel","fuelpump","gas","pump","station"],"unicode":"⛽️"},{"group":5,"hexcode":"1f6de","label":"wheel","tags":["circle","tire","turn"],"unicode":"🛞"},{"group":5,"hexcode":"1f6a8","label":"police car light","tags":["beacon","car","light","police","revolving"],"unicode":"🚨"},{"group":5,"hexcode":"1f6a5","label":"horizontal traffic light","tags":["light","signal","traffic"],"unicode":"🚥"},{"group":5,"hexcode":"1f6a6","label":"vertical traffic light","tags":["light","signal","traffic"],"unicode":"🚦"},{"group":5,"hexcode":"1f6d1","label":"stop sign","tags":["octagonal","sign","stop"],"unicode":"🛑"},{"group":5,"hexcode":"1f6a7","label":"construction","tags":["barrier"],"unicode":"🚧"},{"group":5,"hexcode":"2693","label":"anchor","tags":["ship","tool"],"unicode":"⚓️"},{"group":5,"hexcode":"1f6df","label":"ring buoy","tags":["float","life preserver","life saver","rescue","safety"],"unicode":"🛟"},{"group":5,"hexcode":"26f5","label":"sailboat","tags":["boat","resort","sea","yacht"],"unicode":"⛵️"},{"group":5,"hexcode":"1f6f6","label":"canoe","tags":["boat"],"unicode":"🛶"},{"group":5,"hexcode":"1f6a4","label":"speedboat","tags":["boat"],"unicode":"🚤"},{"group":5,"hexcode":"1f6f3","label":"passenger ship","tags":["passenger","ship"],"unicode":"🛳️"},{"group":5,"hexcode":"26f4","label":"ferry","tags":["boat","passenger"],"unicode":"⛴️"},{"group":5,"hexcode":"1f6e5","label":"motor boat","tags":["boat","motorboat"],"unicode":"🛥️"},{"group":5,"hexcode":"1f6a2","label":"ship","tags":["boat","passenger"],"unicode":"🚢"},{"group":5,"hexcode":"2708","label":"airplane","tags":["aeroplane"],"unicode":"✈️"},{"group":5,"hexcode":"1f6e9","label":"small airplane","tags":["aeroplane","airplane"],"unicode":"🛩️"},{"group":5,"hexcode":"1f6eb","label":"airplane departure","tags":["aeroplane","airplane","check-in","departure","departures"],"unicode":"🛫"},{"group":5,"hexcode":"1f6ec","label":"airplane arrival","tags":["aeroplane","airplane","arrivals","arriving","landing"],"unicode":"🛬"},{"group":5,"hexcode":"1fa82","label":"parachute","tags":["hang-glide","parasail","skydive"],"unicode":"🪂"},{"group":5,"hexcode":"1f4ba","label":"seat","tags":["chair"],"unicode":"💺"},{"group":5,"hexcode":"1f681","label":"helicopter","tags":["vehicle"],"unicode":"🚁"},{"group":5,"hexcode":"1f69f","label":"suspension railway","tags":["railway","suspension"],"unicode":"🚟"},{"group":5,"hexcode":"1f6a0","label":"mountain cableway","tags":["cable","gondola","mountain"],"unicode":"🚠"},{"group":5,"hexcode":"1f6a1","label":"aerial tramway","tags":["aerial","cable","car","gondola","tramway"],"unicode":"🚡"},{"group":5,"hexcode":"1f6f0","label":"satellite","tags":["space"],"unicode":"🛰️"},{"group":5,"hexcode":"1f680","label":"rocket","tags":["space"],"unicode":"🚀"},{"group":5,"hexcode":"1f6f8","label":"flying saucer","tags":["ufo"],"unicode":"🛸"},{"group":5,"hexcode":"1f6ce","label":"bellhop bell","tags":["bell","bellhop","hotel"],"unicode":"🛎️"},{"group":5,"hexcode":"1f9f3","label":"luggage","tags":["packing","travel"],"unicode":"🧳"},{"group":5,"hexcode":"231b","label":"hourglass done","tags":["sand","timer"],"unicode":"⌛️"},{"group":5,"hexcode":"23f3","label":"hourglass not done","tags":["hourglass","sand","timer"],"unicode":"⏳️"},{"group":5,"hexcode":"231a","label":"watch","tags":["clock"],"unicode":"⌚️"},{"group":5,"hexcode":"23f0","label":"alarm clock","tags":["alarm","clock"],"unicode":"⏰"},{"group":5,"hexcode":"23f1","label":"stopwatch","tags":["clock"],"unicode":"⏱️"},{"group":5,"hexcode":"23f2","label":"timer clock","tags":["clock","timer"],"unicode":"⏲️"},{"group":5,"hexcode":"1f570","label":"mantelpiece clock","tags":["clock"],"unicode":"🕰️"},{"group":5,"hexcode":"1f55b","label":"twelve o’clock","tags":["00","12","12:00","clock","o’clock","twelve"],"unicode":"🕛️"},{"group":5,"hexcode":"1f567","label":"twelve-thirty","tags":["12","12:30","clock","thirty","twelve"],"unicode":"🕧️"},{"group":5,"hexcode":"1f550","label":"one o’clock","tags":["00","1","1:00","clock","one","o’clock"],"unicode":"🕐️"},{"group":5,"hexcode":"1f55c","label":"one-thirty","tags":["1","1:30","clock","one","thirty"],"unicode":"🕜️"},{"group":5,"hexcode":"1f551","label":"two o’clock","tags":["00","2","2:00","clock","o’clock","two"],"unicode":"🕑️"},{"group":5,"hexcode":"1f55d","label":"two-thirty","tags":["2","2:30","clock","thirty","two"],"unicode":"🕝️"},{"group":5,"hexcode":"1f552","label":"three o’clock","tags":["00","3","3:00","clock","o’clock","three"],"unicode":"🕒️"},{"group":5,"hexcode":"1f55e","label":"three-thirty","tags":["3","3:30","clock","thirty","three"],"unicode":"🕞️"},{"group":5,"hexcode":"1f553","label":"four o’clock","tags":["00","4","4:00","clock","four","o’clock"],"unicode":"🕓️"},{"group":5,"hexcode":"1f55f","label":"four-thirty","tags":["4","4:30","clock","four","thirty"],"unicode":"🕟️"},{"group":5,"hexcode":"1f554","label":"five o’clock","tags":["00","5","5:00","clock","five","o’clock"],"unicode":"🕔️"},{"group":5,"hexcode":"1f560","label":"five-thirty","tags":["5","5:30","clock","five","thirty"],"unicode":"🕠️"},{"group":5,"hexcode":"1f555","label":"six o’clock","tags":["00","6","6:00","clock","o’clock","six"],"unicode":"🕕️"},{"group":5,"hexcode":"1f561","label":"six-thirty","tags":["6","6:30","clock","six","thirty"],"unicode":"🕡️"},{"group":5,"hexcode":"1f556","label":"seven o’clock","tags":["00","7","7:00","clock","o’clock","seven"],"unicode":"🕖️"},{"group":5,"hexcode":"1f562","label":"seven-thirty","tags":["7","7:30","clock","seven","thirty"],"unicode":"🕢️"},{"group":5,"hexcode":"1f557","label":"eight o’clock","tags":["00","8","8:00","clock","eight","o’clock"],"unicode":"🕗️"},{"group":5,"hexcode":"1f563","label":"eight-thirty","tags":["8","8:30","clock","eight","thirty"],"unicode":"🕣️"},{"group":5,"hexcode":"1f558","label":"nine o’clock","tags":["00","9","9:00","clock","nine","o’clock"],"unicode":"🕘️"},{"group":5,"hexcode":"1f564","label":"nine-thirty","tags":["9","9:30","clock","nine","thirty"],"unicode":"🕤️"},{"group":5,"hexcode":"1f559","label":"ten o’clock","tags":["00","10","10:00","clock","o’clock","ten"],"unicode":"🕙️"},{"group":5,"hexcode":"1f565","label":"ten-thirty","tags":["10","10:30","clock","ten","thirty"],"unicode":"🕥️"},{"group":5,"hexcode":"1f55a","label":"eleven o’clock","tags":["00","11","11:00","clock","eleven","o’clock"],"unicode":"🕚️"},{"group":5,"hexcode":"1f566","label":"eleven-thirty","tags":["11","11:30","clock","eleven","thirty"],"unicode":"🕦️"},{"group":5,"hexcode":"1f311","label":"new moon","tags":["dark","moon"],"unicode":"🌑"},{"group":5,"hexcode":"1f312","label":"waxing crescent moon","tags":["crescent","moon","waxing"],"unicode":"🌒"},{"group":5,"hexcode":"1f313","label":"first quarter moon","tags":["moon","quarter"],"unicode":"🌓"},{"group":5,"hexcode":"1f314","label":"waxing gibbous moon","tags":["gibbous","moon","waxing"],"unicode":"🌔"},{"group":5,"hexcode":"1f315","label":"full moon","tags":["full","moon"],"unicode":"🌕️"},{"group":5,"hexcode":"1f316","label":"waning gibbous moon","tags":["gibbous","moon","waning"],"unicode":"🌖"},{"group":5,"hexcode":"1f317","label":"last quarter moon","tags":["moon","quarter"],"unicode":"🌗"},{"group":5,"hexcode":"1f318","label":"waning crescent moon","tags":["crescent","moon","waning"],"unicode":"🌘"},{"group":5,"hexcode":"1f319","label":"crescent moon","tags":["crescent","moon"],"unicode":"🌙"},{"group":5,"hexcode":"1f31a","label":"new moon face","tags":["face","moon"],"unicode":"🌚"},{"group":5,"hexcode":"1f31b","label":"first quarter moon face","tags":["face","moon","quarter"],"unicode":"🌛"},{"group":5,"hexcode":"1f31c","label":"last quarter moon face","tags":["face","moon","quarter"],"unicode":"🌜️"},{"group":5,"hexcode":"1f321","label":"thermometer","tags":["weather"],"unicode":"🌡️"},{"group":5,"hexcode":"2600","label":"sun","tags":["bright","rays","sunny"],"unicode":"☀️"},{"group":5,"hexcode":"1f31d","label":"full moon face","tags":["bright","face","full","moon"],"unicode":"🌝"},{"group":5,"hexcode":"1f31e","label":"sun with face","tags":["bright","face","sun"],"unicode":"🌞"},{"group":5,"hexcode":"1fa90","label":"ringed planet","tags":["saturn","saturnine"],"unicode":"🪐"},{"group":5,"hexcode":"2b50","label":"star","tags":["star"],"unicode":"⭐️"},{"group":5,"hexcode":"1f31f","label":"glowing star","tags":["glittery","glow","shining","sparkle","star"],"unicode":"🌟"},{"group":5,"hexcode":"1f320","label":"shooting star","tags":["falling","shooting","star"],"unicode":"🌠"},{"group":5,"hexcode":"1f30c","label":"milky way","tags":["space"],"unicode":"🌌"},{"group":5,"hexcode":"2601","label":"cloud","tags":["weather"],"unicode":"☁️"},{"group":5,"hexcode":"26c5","label":"sun behind cloud","tags":["cloud","sun"],"unicode":"⛅️"},{"group":5,"hexcode":"26c8","label":"cloud with lightning and rain","tags":["cloud","rain","thunder"],"unicode":"⛈️"},{"group":5,"hexcode":"1f324","label":"sun behind small cloud","tags":["cloud","sun"],"unicode":"🌤️"},{"group":5,"hexcode":"1f325","label":"sun behind large cloud","tags":["cloud","sun"],"unicode":"🌥️"},{"group":5,"hexcode":"1f326","label":"sun behind rain cloud","tags":["cloud","rain","sun"],"unicode":"🌦️"},{"group":5,"hexcode":"1f327","label":"cloud with rain","tags":["cloud","rain"],"unicode":"🌧️"},{"group":5,"hexcode":"1f328","label":"cloud with snow","tags":["cloud","cold","snow"],"unicode":"🌨️"},{"group":5,"hexcode":"1f329","label":"cloud with lightning","tags":["cloud","lightning"],"unicode":"🌩️"},{"group":5,"hexcode":"1f32a","label":"tornado","tags":["cloud","whirlwind"],"unicode":"🌪️"},{"group":5,"hexcode":"1f32b","label":"fog","tags":["cloud"],"unicode":"🌫️"},{"group":5,"hexcode":"1f32c","label":"wind face","tags":["blow","cloud","face","wind"],"unicode":"🌬️"},{"group":5,"hexcode":"1f300","label":"cyclone","tags":["dizzy","hurricane","twister","typhoon"],"unicode":"🌀"},{"group":5,"hexcode":"1f308","label":"rainbow","tags":["rain"],"unicode":"🌈"},{"group":5,"hexcode":"1f302","label":"closed umbrella","tags":["clothing","rain","umbrella"],"unicode":"🌂"},{"group":5,"hexcode":"2602","label":"umbrella","tags":["clothing","rain"],"unicode":"☂️"},{"group":5,"hexcode":"2614","label":"umbrella with rain drops","tags":["clothing","drop","rain","umbrella"],"unicode":"☔️"},{"group":5,"hexcode":"26f1","label":"umbrella on ground","tags":["rain","sun","umbrella"],"unicode":"⛱️"},{"group":5,"hexcode":"26a1","label":"high voltage","tags":["danger","electric","lightning","voltage","zap"],"unicode":"⚡️"},{"group":5,"hexcode":"2744","label":"snowflake","tags":["cold","snow"],"unicode":"❄️"},{"group":5,"hexcode":"2603","label":"snowman","tags":["cold","snow"],"unicode":"☃️"},{"group":5,"hexcode":"26c4","label":"snowman without snow","tags":["cold","snow","snowman"],"unicode":"⛄️"},{"group":5,"hexcode":"2604","label":"comet","tags":["space"],"unicode":"☄️"},{"group":5,"hexcode":"1f525","label":"fire","tags":["flame","tool"],"unicode":"🔥"},{"group":5,"hexcode":"1f4a7","label":"droplet","tags":["cold","comic","drop","sweat"],"unicode":"💧"},{"group":5,"hexcode":"1f30a","label":"water wave","tags":["ocean","water","wave"],"unicode":"🌊"},{"group":6,"hexcode":"1f383","label":"jack-o-lantern","tags":["celebration","halloween","jack","lantern"],"unicode":"🎃"},{"group":6,"hexcode":"1f384","label":"christmas tree","tags":["celebration","christmas","tree"],"unicode":"🎄"},{"group":6,"hexcode":"1f386","label":"fireworks","tags":["celebration"],"unicode":"🎆"},{"group":6,"hexcode":"1f387","label":"sparkler","tags":["celebration","fireworks","sparkle"],"unicode":"🎇"},{"group":6,"hexcode":"1f9e8","label":"firecracker","tags":["dynamite","explosive","fireworks"],"unicode":"🧨"},{"group":6,"hexcode":"2728","label":"sparkles","tags":["*","sparkle","star"],"unicode":"✨"},{"group":6,"hexcode":"1f388","label":"balloon","tags":["celebration"],"unicode":"🎈"},{"group":6,"hexcode":"1f389","label":"party popper","tags":["celebration","party","popper","tada"],"unicode":"🎉"},{"group":6,"hexcode":"1f38a","label":"confetti ball","tags":["ball","celebration","confetti"],"unicode":"🎊"},{"group":6,"hexcode":"1f38b","label":"tanabata tree","tags":["banner","celebration","japanese","tree"],"unicode":"🎋"},{"group":6,"hexcode":"1f38d","label":"pine decoration","tags":["bamboo","celebration","japanese","pine"],"unicode":"🎍"},{"group":6,"hexcode":"1f38e","label":"japanese dolls","tags":["celebration","doll","festival","japanese","japanese dolls"],"unicode":"🎎"},{"group":6,"hexcode":"1f38f","label":"carp streamer","tags":["carp","celebration","streamer"],"unicode":"🎏"},{"group":6,"hexcode":"1f390","label":"wind chime","tags":["bell","celebration","chime","wind"],"unicode":"🎐"},{"group":6,"hexcode":"1f391","label":"moon viewing ceremony","tags":["celebration","ceremony","moon"],"unicode":"🎑"},{"group":6,"hexcode":"1f9e7","label":"red envelope","tags":["gift","good luck","hóngbāo","lai see","money"],"unicode":"🧧"},{"group":6,"hexcode":"1f380","label":"ribbon","tags":["celebration"],"unicode":"🎀"},{"group":6,"hexcode":"1f381","label":"wrapped gift","tags":["box","celebration","gift","present","wrapped"],"unicode":"🎁"},{"group":6,"hexcode":"1f397","label":"reminder ribbon","tags":["celebration","reminder","ribbon"],"unicode":"🎗️"},{"group":6,"hexcode":"1f39f","label":"admission tickets","tags":["admission","ticket"],"unicode":"🎟️"},{"group":6,"hexcode":"1f3ab","label":"ticket","tags":["admission"],"unicode":"🎫"},{"group":6,"hexcode":"1f396","label":"military medal","tags":["celebration","medal","military"],"unicode":"🎖️"},{"group":6,"hexcode":"1f3c6","label":"trophy","tags":["prize"],"unicode":"🏆️"},{"group":6,"hexcode":"1f3c5","label":"sports medal","tags":["medal"],"unicode":"🏅"},{"group":6,"hexcode":"1f947","label":"1st place medal","tags":["first","gold","medal"],"unicode":"🥇"},{"group":6,"hexcode":"1f948","label":"2nd place medal","tags":["medal","second","silver"],"unicode":"🥈"},{"group":6,"hexcode":"1f949","label":"3rd place medal","tags":["bronze","medal","third"],"unicode":"🥉"},{"group":6,"hexcode":"26bd","label":"soccer ball","tags":["ball","football","soccer"],"unicode":"⚽️"},{"group":6,"hexcode":"26be","label":"baseball","tags":["ball"],"unicode":"⚾️"},{"group":6,"hexcode":"1f94e","label":"softball","tags":["ball","glove","underarm"],"unicode":"🥎"},{"group":6,"hexcode":"1f3c0","label":"basketball","tags":["ball","hoop"],"unicode":"🏀"},{"group":6,"hexcode":"1f3d0","label":"volleyball","tags":["ball","game"],"unicode":"🏐"},{"group":6,"hexcode":"1f3c8","label":"american football","tags":["american","ball","football"],"unicode":"🏈"},{"group":6,"hexcode":"1f3c9","label":"rugby football","tags":["ball","football","rugby"],"unicode":"🏉"},{"group":6,"hexcode":"1f3be","label":"tennis","tags":["ball","racquet"],"unicode":"🎾"},{"group":6,"hexcode":"1f94f","label":"flying disc","tags":["ultimate"],"unicode":"🥏"},{"group":6,"hexcode":"1f3b3","label":"bowling","tags":["ball","game"],"unicode":"🎳"},{"group":6,"hexcode":"1f3cf","label":"cricket game","tags":["ball","bat","game"],"unicode":"🏏"},{"group":6,"hexcode":"1f3d1","label":"field hockey","tags":["ball","field","game","hockey","stick"],"unicode":"🏑"},{"group":6,"hexcode":"1f3d2","label":"ice hockey","tags":["game","hockey","ice","puck","stick"],"unicode":"🏒"},{"group":6,"hexcode":"1f94d","label":"lacrosse","tags":["ball","goal","stick"],"unicode":"🥍"},{"group":6,"hexcode":"1f3d3","label":"ping pong","tags":["ball","bat","game","paddle","table tennis"],"unicode":"🏓"},{"group":6,"hexcode":"1f3f8","label":"badminton","tags":["birdie","game","racquet","shuttlecock"],"unicode":"🏸"},{"group":6,"hexcode":"1f94a","label":"boxing glove","tags":["boxing","glove"],"unicode":"🥊"},{"group":6,"hexcode":"1f94b","label":"martial arts uniform","tags":["judo","karate","martial arts","taekwondo","uniform"],"unicode":"🥋"},{"group":6,"hexcode":"1f945","label":"goal net","tags":["goal","net"],"unicode":"🥅"},{"group":6,"hexcode":"26f3","label":"flag in hole","tags":["golf","hole"],"unicode":"⛳️"},{"group":6,"hexcode":"26f8","label":"ice skate","tags":["ice","skate"],"unicode":"⛸️"},{"group":6,"hexcode":"1f3a3","label":"fishing pole","tags":["fish","pole"],"unicode":"🎣"},{"group":6,"hexcode":"1f93f","label":"diving mask","tags":["diving","scuba","snorkeling"],"unicode":"🤿"},{"group":6,"hexcode":"1f3bd","label":"running shirt","tags":["athletics","running","sash","shirt"],"unicode":"🎽"},{"group":6,"hexcode":"1f3bf","label":"skis","tags":["ski","snow"],"unicode":"🎿"},{"group":6,"hexcode":"1f6f7","label":"sled","tags":["sledge","sleigh"],"unicode":"🛷"},{"group":6,"hexcode":"1f94c","label":"curling stone","tags":["game","rock"],"unicode":"🥌"},{"group":6,"hexcode":"1f3af","label":"bullseye","tags":["dart","direct hit","game","hit","target"],"unicode":"🎯"},{"group":6,"hexcode":"1fa80","label":"yo-yo","tags":["fluctuate","toy"],"unicode":"🪀"},{"group":6,"hexcode":"1fa81","label":"kite","tags":["fly","soar"],"unicode":"🪁"},{"group":6,"hexcode":"1f52b","label":"water pistol","tags":["gun","handgun","pistol","revolver","tool","water","weapon"],"unicode":"🔫"},{"group":6,"hexcode":"1f3b1","label":"pool 8 ball","tags":["8","ball","billiard","eight","game"],"unicode":"🎱"},{"group":6,"hexcode":"1f52e","label":"crystal ball","tags":["ball","crystal","fairy tale","fantasy","fortune","tool"],"unicode":"🔮"},{"group":6,"hexcode":"1fa84","label":"magic wand","tags":["magic","witch","wizard"],"unicode":"🪄"},{"group":6,"hexcode":"1f3ae","label":"video game","tags":["controller","game"],"unicode":"🎮️"},{"group":6,"hexcode":"1f579","label":"joystick","tags":["game","video game"],"unicode":"🕹️"},{"group":6,"hexcode":"1f3b0","label":"slot machine","tags":["game","slot"],"unicode":"🎰"},{"group":6,"hexcode":"1f3b2","label":"game die","tags":["dice","die","game"],"unicode":"🎲"},{"group":6,"hexcode":"1f9e9","label":"puzzle piece","tags":["clue","interlocking","jigsaw","piece","puzzle"],"unicode":"🧩"},{"group":6,"hexcode":"1f9f8","label":"teddy bear","tags":["plaything","plush","stuffed","toy"],"unicode":"🧸"},{"group":6,"hexcode":"1fa85","label":"piñata","tags":["celebration","party"],"unicode":"🪅"},{"group":6,"hexcode":"1faa9","label":"mirror ball","tags":["dance","disco","glitter","party"],"unicode":"🪩"},{"group":6,"hexcode":"1fa86","label":"nesting dolls","tags":["doll","nesting","russia"],"unicode":"🪆"},{"group":6,"hexcode":"2660","label":"spade suit","tags":["card","game"],"unicode":"♠️"},{"group":6,"hexcode":"2665","label":"heart suit","tags":["card","game"],"unicode":"♥️"},{"group":6,"hexcode":"2666","label":"diamond suit","tags":["card","game"],"unicode":"♦️"},{"group":6,"hexcode":"2663","label":"club suit","tags":["card","game"],"unicode":"♣️"},{"group":6,"hexcode":"265f","label":"chess pawn","tags":["chess","dupe","expendable"],"unicode":"♟️"},{"group":6,"hexcode":"1f0cf","label":"joker","tags":["card","game","wildcard"],"unicode":"🃏"},{"group":6,"hexcode":"1f004","label":"mahjong red dragon","tags":["game","mahjong","red"],"unicode":"🀄️"},{"group":6,"hexcode":"1f3b4","label":"flower playing cards","tags":["card","flower","game","japanese","playing"],"unicode":"🎴"},{"group":6,"hexcode":"1f3ad","label":"performing arts","tags":["art","mask","performing","theater","theatre"],"unicode":"🎭️"},{"group":6,"hexcode":"1f5bc","label":"framed picture","tags":["art","frame","museum","painting","picture"],"unicode":"🖼️"},{"group":6,"hexcode":"1f3a8","label":"artist palette","tags":["art","museum","painting","palette"],"unicode":"🎨"},{"group":6,"hexcode":"1f9f5","label":"thread","tags":["needle","sewing","spool","string"],"unicode":"🧵"},{"group":6,"hexcode":"1faa1","label":"sewing needle","tags":["embroidery","needle","sewing","stitches","sutures","tailoring"],"unicode":"🪡"},{"group":6,"hexcode":"1f9f6","label":"yarn","tags":["ball","crochet","knit"],"unicode":"🧶"},{"group":6,"hexcode":"1faa2","label":"knot","tags":["rope","tangled","tie","twine","twist"],"unicode":"🪢"},{"group":7,"hexcode":"1f453","label":"glasses","tags":["clothing","eye","eyeglasses","eyewear"],"unicode":"👓️"},{"group":7,"hexcode":"1f576","label":"sunglasses","tags":["dark","eye","eyewear","glasses"],"unicode":"🕶️"},{"group":7,"hexcode":"1f97d","label":"goggles","tags":["eye protection","swimming","welding"],"unicode":"🥽"},{"group":7,"hexcode":"1f97c","label":"lab coat","tags":["doctor","experiment","scientist"],"unicode":"🥼"},{"group":7,"hexcode":"1f9ba","label":"safety vest","tags":["emergency","safety","vest"],"unicode":"🦺"},{"group":7,"hexcode":"1f454","label":"necktie","tags":["clothing","tie"],"unicode":"👔"},{"group":7,"hexcode":"1f455","label":"t-shirt","tags":["clothing","shirt","tshirt"],"unicode":"👕"},{"group":7,"hexcode":"1f456","label":"jeans","tags":["clothing","pants","trousers"],"unicode":"👖"},{"group":7,"hexcode":"1f9e3","label":"scarf","tags":["neck"],"unicode":"🧣"},{"group":7,"hexcode":"1f9e4","label":"gloves","tags":["hand"],"unicode":"🧤"},{"group":7,"hexcode":"1f9e5","label":"coat","tags":["jacket"],"unicode":"🧥"},{"group":7,"hexcode":"1f9e6","label":"socks","tags":["stocking"],"unicode":"🧦"},{"group":7,"hexcode":"1f457","label":"dress","tags":["clothing"],"unicode":"👗"},{"group":7,"hexcode":"1f458","label":"kimono","tags":["clothing"],"unicode":"👘"},{"group":7,"hexcode":"1f97b","label":"sari","tags":["clothing","dress"],"unicode":"🥻"},{"group":7,"hexcode":"1fa71","label":"one-piece swimsuit","tags":["bathing suit"],"unicode":"🩱"},{"group":7,"hexcode":"1fa72","label":"briefs","tags":["bathing suit","one-piece","swimsuit","underwear"],"unicode":"🩲"},{"group":7,"hexcode":"1fa73","label":"shorts","tags":["bathing suit","pants","underwear"],"unicode":"🩳"},{"group":7,"hexcode":"1f459","label":"bikini","tags":["clothing","swim"],"unicode":"👙"},{"group":7,"hexcode":"1f45a","label":"woman’s clothes","tags":["clothing","woman"],"unicode":"👚"},{"group":7,"hexcode":"1faad","label":"folding hand fan","tags":["cooling","dance","fan","flutter","hot","shy"],"unicode":"🪭"},{"group":7,"hexcode":"1f45b","label":"purse","tags":["clothing","coin"],"unicode":"👛"},{"group":7,"hexcode":"1f45c","label":"handbag","tags":["bag","clothing","purse"],"unicode":"👜"},{"group":7,"hexcode":"1f45d","label":"clutch bag","tags":["bag","clothing","pouch"],"unicode":"👝"},{"group":7,"hexcode":"1f6cd","label":"shopping bags","tags":["bag","hotel","shopping"],"unicode":"🛍️"},{"group":7,"hexcode":"1f392","label":"backpack","tags":["bag","rucksack","satchel","school"],"unicode":"🎒"},{"group":7,"hexcode":"1fa74","label":"thong sandal","tags":["beach sandals","sandals","thong sandals","thongs","zōri"],"unicode":"🩴"},{"group":7,"hexcode":"1f45e","label":"man’s shoe","tags":["clothing","man","shoe"],"unicode":"👞"},{"group":7,"hexcode":"1f45f","label":"running shoe","tags":["athletic","clothing","shoe","sneaker"],"unicode":"👟"},{"group":7,"hexcode":"1f97e","label":"hiking boot","tags":["backpacking","boot","camping","hiking"],"unicode":"🥾"},{"group":7,"hexcode":"1f97f","label":"flat shoe","tags":["ballet flat","slip-on","slipper"],"unicode":"🥿"},{"group":7,"hexcode":"1f460","label":"high-heeled shoe","tags":["clothing","heel","shoe","woman"],"unicode":"👠"},{"group":7,"hexcode":"1f461","label":"woman’s sandal","tags":["clothing","sandal","shoe","woman"],"unicode":"👡"},{"group":7,"hexcode":"1fa70","label":"ballet shoes","tags":["ballet","dance"],"unicode":"🩰"},{"group":7,"hexcode":"1f462","label":"woman’s boot","tags":["boot","clothing","shoe","woman"],"unicode":"👢"},{"group":7,"hexcode":"1faae","label":"hair pick","tags":["afro","comb","hair","pick"],"unicode":"🪮"},{"group":7,"hexcode":"1f451","label":"crown","tags":["clothing","king","queen"],"unicode":"👑"},{"group":7,"hexcode":"1f452","label":"woman’s hat","tags":["clothing","hat","woman"],"unicode":"👒"},{"group":7,"hexcode":"1f3a9","label":"top hat","tags":["clothing","hat","top","tophat"],"unicode":"🎩"},{"group":7,"hexcode":"1f393","label":"graduation cap","tags":["cap","celebration","clothing","graduation","hat"],"unicode":"🎓️"},{"group":7,"hexcode":"1f9e2","label":"billed cap","tags":["baseball cap"],"unicode":"🧢"},{"group":7,"hexcode":"1fa96","label":"military helmet","tags":["army","helmet","military","soldier","warrior"],"unicode":"🪖"},{"group":7,"hexcode":"26d1","label":"rescue worker’s helmet","tags":["aid","cross","face","hat","helmet"],"unicode":"⛑️"},{"group":7,"hexcode":"1f4ff","label":"prayer beads","tags":["beads","clothing","necklace","prayer","religion"],"unicode":"📿"},{"group":7,"hexcode":"1f484","label":"lipstick","tags":["cosmetics","makeup"],"unicode":"💄"},{"group":7,"hexcode":"1f48d","label":"ring","tags":["diamond"],"unicode":"💍"},{"group":7,"hexcode":"1f48e","label":"gem stone","tags":["diamond","gem","jewel"],"unicode":"💎"},{"group":7,"hexcode":"1f507","label":"muted speaker","tags":["mute","quiet","silent","speaker"],"unicode":"🔇"},{"group":7,"hexcode":"1f508","label":"speaker low volume","tags":["soft"],"unicode":"🔈️"},{"group":7,"hexcode":"1f509","label":"speaker medium volume","tags":["medium"],"unicode":"🔉"},{"group":7,"hexcode":"1f50a","label":"speaker high volume","tags":["loud"],"unicode":"🔊"},{"group":7,"hexcode":"1f4e2","label":"loudspeaker","tags":["loud","public address"],"unicode":"📢"},{"group":7,"hexcode":"1f4e3","label":"megaphone","tags":["cheering"],"unicode":"📣"},{"group":7,"hexcode":"1f4ef","label":"postal horn","tags":["horn","post","postal"],"unicode":"📯"},{"group":7,"hexcode":"1f514","label":"bell","tags":["bell"],"unicode":"🔔"},{"group":7,"hexcode":"1f515","label":"bell with slash","tags":["bell","forbidden","mute","quiet","silent"],"unicode":"🔕"},{"group":7,"hexcode":"1f3bc","label":"musical score","tags":["music","score"],"unicode":"🎼"},{"group":7,"hexcode":"1f3b5","label":"musical note","tags":["music","note"],"unicode":"🎵"},{"group":7,"hexcode":"1f3b6","label":"musical notes","tags":["music","note","notes"],"unicode":"🎶"},{"group":7,"hexcode":"1f399","label":"studio microphone","tags":["mic","microphone","music","studio"],"unicode":"🎙️"},{"group":7,"hexcode":"1f39a","label":"level slider","tags":["level","music","slider"],"unicode":"🎚️"},{"group":7,"hexcode":"1f39b","label":"control knobs","tags":["control","knobs","music"],"unicode":"🎛️"},{"group":7,"hexcode":"1f3a4","label":"microphone","tags":["karaoke","mic"],"unicode":"🎤"},{"group":7,"hexcode":"1f3a7","label":"headphone","tags":["earbud"],"unicode":"🎧️"},{"group":7,"hexcode":"1f4fb","label":"radio","tags":["video"],"unicode":"📻️"},{"group":7,"hexcode":"1f3b7","label":"saxophone","tags":["instrument","music","sax"],"unicode":"🎷"},{"group":7,"hexcode":"1fa97","label":"accordion","tags":["concertina","squeeze box"],"unicode":"🪗"},{"group":7,"hexcode":"1f3b8","label":"guitar","tags":["instrument","music"],"unicode":"🎸"},{"group":7,"hexcode":"1f3b9","label":"musical keyboard","tags":["instrument","keyboard","music","piano"],"unicode":"🎹"},{"group":7,"hexcode":"1f3ba","label":"trumpet","tags":["instrument","music"],"unicode":"🎺"},{"group":7,"hexcode":"1f3bb","label":"violin","tags":["instrument","music"],"unicode":"🎻"},{"group":7,"hexcode":"1fa95","label":"banjo","tags":["music","stringed"],"unicode":"🪕"},{"group":7,"hexcode":"1f941","label":"drum","tags":["drumsticks","music"],"unicode":"🥁"},{"group":7,"hexcode":"1fa98","label":"long drum","tags":["beat","conga","drum","rhythm"],"unicode":"🪘"},{"group":7,"hexcode":"1fa87","label":"maracas","tags":["instrument","music","percussion","rattle","shake"],"unicode":"🪇"},{"group":7,"hexcode":"1fa88","label":"flute","tags":["fife","music","pipe","recorder","woodwind"],"unicode":"🪈"},{"group":7,"hexcode":"1f4f1","label":"mobile phone","tags":["cell","mobile","phone","telephone"],"unicode":"📱"},{"group":7,"hexcode":"1f4f2","label":"mobile phone with arrow","tags":["arrow","cell","mobile","phone","receive"],"unicode":"📲"},{"group":7,"hexcode":"260e","label":"telephone","tags":["phone"],"unicode":"☎️"},{"group":7,"hexcode":"1f4de","label":"telephone receiver","tags":["phone","receiver","telephone"],"unicode":"📞"},{"group":7,"hexcode":"1f4df","label":"pager","tags":["pager"],"unicode":"📟️"},{"group":7,"hexcode":"1f4e0","label":"fax machine","tags":["fax"],"unicode":"📠"},{"group":7,"hexcode":"1f50b","label":"battery","tags":["battery"],"unicode":"🔋"},{"group":7,"hexcode":"1faab","label":"low battery","tags":["electronic","low energy"],"unicode":"🪫"},{"group":7,"hexcode":"1f50c","label":"electric plug","tags":["electric","electricity","plug"],"unicode":"🔌"},{"group":7,"hexcode":"1f4bb","label":"laptop","tags":["computer","pc","personal"],"unicode":"💻️"},{"group":7,"hexcode":"1f5a5","label":"desktop computer","tags":["computer","desktop"],"unicode":"🖥️"},{"group":7,"hexcode":"1f5a8","label":"printer","tags":["computer"],"unicode":"🖨️"},{"group":7,"hexcode":"2328","label":"keyboard","tags":["computer"],"unicode":"⌨️"},{"group":7,"hexcode":"1f5b1","label":"computer mouse","tags":["computer"],"unicode":"🖱️"},{"group":7,"hexcode":"1f5b2","label":"trackball","tags":["computer"],"unicode":"🖲️"},{"group":7,"hexcode":"1f4bd","label":"computer disk","tags":["computer","disk","minidisk","optical"],"unicode":"💽"},{"group":7,"hexcode":"1f4be","label":"floppy disk","tags":["computer","disk","floppy"],"unicode":"💾"},{"group":7,"hexcode":"1f4bf","label":"optical disk","tags":["cd","computer","disk","optical"],"unicode":"💿️"},{"group":7,"hexcode":"1f4c0","label":"dvd","tags":["blu-ray","computer","disk","optical"],"unicode":"📀"},{"group":7,"hexcode":"1f9ee","label":"abacus","tags":["calculation"],"unicode":"🧮"},{"group":7,"hexcode":"1f3a5","label":"movie camera","tags":["camera","cinema","movie"],"unicode":"🎥"},{"group":7,"hexcode":"1f39e","label":"film frames","tags":["cinema","film","frames","movie"],"unicode":"🎞️"},{"group":7,"hexcode":"1f4fd","label":"film projector","tags":["cinema","film","movie","projector","video"],"unicode":"📽️"},{"group":7,"hexcode":"1f3ac","label":"clapper board","tags":["clapper","movie"],"unicode":"🎬️"},{"group":7,"hexcode":"1f4fa","label":"television","tags":["tv","video"],"unicode":"📺️"},{"group":7,"hexcode":"1f4f7","label":"camera","tags":["video"],"unicode":"📷️"},{"group":7,"hexcode":"1f4f8","label":"camera with flash","tags":["camera","flash","video"],"unicode":"📸"},{"group":7,"hexcode":"1f4f9","label":"video camera","tags":["camera","video"],"unicode":"📹️"},{"group":7,"hexcode":"1f4fc","label":"videocassette","tags":["tape","vhs","video"],"unicode":"📼"},{"group":7,"hexcode":"1f50d","label":"magnifying glass tilted left","tags":["glass","magnifying","search","tool"],"unicode":"🔍️"},{"group":7,"hexcode":"1f50e","label":"magnifying glass tilted right","tags":["glass","magnifying","search","tool"],"unicode":"🔎"},{"group":7,"hexcode":"1f56f","label":"candle","tags":["light"],"unicode":"🕯️"},{"group":7,"hexcode":"1f4a1","label":"light bulb","tags":["bulb","comic","electric","idea","light"],"unicode":"💡"},{"group":7,"hexcode":"1f526","label":"flashlight","tags":["electric","light","tool","torch"],"unicode":"🔦"},{"group":7,"hexcode":"1f3ee","label":"red paper lantern","tags":["bar","lantern","light","red"],"unicode":"🏮"},{"group":7,"hexcode":"1fa94","label":"diya lamp","tags":["diya","lamp","oil"],"unicode":"🪔"},{"group":7,"hexcode":"1f4d4","label":"notebook with decorative cover","tags":["book","cover","decorated","notebook"],"unicode":"📔"},{"group":7,"hexcode":"1f4d5","label":"closed book","tags":["book","closed"],"unicode":"📕"},{"group":7,"hexcode":"1f4d6","label":"open book","tags":["book","open"],"unicode":"📖"},{"group":7,"hexcode":"1f4d7","label":"green book","tags":["book","green"],"unicode":"📗"},{"group":7,"hexcode":"1f4d8","label":"blue book","tags":["blue","book"],"unicode":"📘"},{"group":7,"hexcode":"1f4d9","label":"orange book","tags":["book","orange"],"unicode":"📙"},{"group":7,"hexcode":"1f4da","label":"books","tags":["book"],"unicode":"📚️"},{"group":7,"hexcode":"1f4d3","label":"notebook","tags":["notebook"],"unicode":"📓"},{"group":7,"hexcode":"1f4d2","label":"ledger","tags":["notebook"],"unicode":"📒"},{"group":7,"hexcode":"1f4c3","label":"page with curl","tags":["curl","document","page"],"unicode":"📃"},{"group":7,"hexcode":"1f4dc","label":"scroll","tags":["paper"],"unicode":"📜"},{"group":7,"hexcode":"1f4c4","label":"page facing up","tags":["document","page"],"unicode":"📄"},{"group":7,"hexcode":"1f4f0","label":"newspaper","tags":["news","paper"],"unicode":"📰"},{"group":7,"hexcode":"1f5de","label":"rolled-up newspaper","tags":["news","newspaper","paper","rolled"],"unicode":"🗞️"},{"group":7,"hexcode":"1f4d1","label":"bookmark tabs","tags":["bookmark","mark","marker","tabs"],"unicode":"📑"},{"group":7,"hexcode":"1f516","label":"bookmark","tags":["mark"],"unicode":"🔖"},{"group":7,"hexcode":"1f3f7","label":"label","tags":["label"],"unicode":"🏷️"},{"group":7,"hexcode":"1f4b0","label":"money bag","tags":["bag","dollar","money","moneybag"],"unicode":"💰️"},{"group":7,"hexcode":"1fa99","label":"coin","tags":["gold","metal","money","silver","treasure"],"unicode":"🪙"},{"group":7,"hexcode":"1f4b4","label":"yen banknote","tags":["banknote","bill","currency","money","note","yen"],"unicode":"💴"},{"group":7,"hexcode":"1f4b5","label":"dollar banknote","tags":["banknote","bill","currency","dollar","money","note"],"unicode":"💵"},{"group":7,"hexcode":"1f4b6","label":"euro banknote","tags":["banknote","bill","currency","euro","money","note"],"unicode":"💶"},{"group":7,"hexcode":"1f4b7","label":"pound banknote","tags":["banknote","bill","currency","money","note","pound"],"unicode":"💷"},{"group":7,"hexcode":"1f4b8","label":"money with wings","tags":["banknote","bill","fly","money","wings"],"unicode":"💸"},{"group":7,"hexcode":"1f4b3","label":"credit card","tags":["card","credit","money"],"unicode":"💳️"},{"group":7,"hexcode":"1f9fe","label":"receipt","tags":["accounting","bookkeeping","evidence","proof"],"unicode":"🧾"},{"group":7,"hexcode":"1f4b9","label":"chart increasing with yen","tags":["chart","graph","growth","money","yen"],"unicode":"💹"},{"group":7,"hexcode":"2709","label":"envelope","tags":["email","letter"],"unicode":"✉️"},{"group":7,"hexcode":"1f4e7","label":"e-mail","tags":["email","letter","mail"],"unicode":"📧"},{"group":7,"hexcode":"1f4e8","label":"incoming envelope","tags":["e-mail","email","envelope","incoming","letter","receive"],"unicode":"📨"},{"group":7,"hexcode":"1f4e9","label":"envelope with arrow","tags":["arrow","e-mail","email","envelope","outgoing"],"unicode":"📩"},{"group":7,"hexcode":"1f4e4","label":"outbox tray","tags":["box","letter","mail","outbox","sent","tray"],"unicode":"📤️"},{"group":7,"hexcode":"1f4e5","label":"inbox tray","tags":["box","inbox","letter","mail","receive","tray"],"unicode":"📥️"},{"group":7,"hexcode":"1f4e6","label":"package","tags":["box","parcel"],"unicode":"📦️"},{"group":7,"hexcode":"1f4eb","label":"closed mailbox with raised flag","tags":["closed","mail","mailbox","postbox"],"unicode":"📫️"},{"group":7,"hexcode":"1f4ea","label":"closed mailbox with lowered flag","tags":["closed","lowered","mail","mailbox","postbox"],"unicode":"📪️"},{"group":7,"hexcode":"1f4ec","label":"open mailbox with raised flag","tags":["mail","mailbox","open","postbox"],"unicode":"📬️"},{"group":7,"hexcode":"1f4ed","label":"open mailbox with lowered flag","tags":["lowered","mail","mailbox","open","postbox"],"unicode":"📭️"},{"group":7,"hexcode":"1f4ee","label":"postbox","tags":["mail","mailbox"],"unicode":"📮"},{"group":7,"hexcode":"1f5f3","label":"ballot box with ballot","tags":["ballot","box"],"unicode":"🗳️"},{"group":7,"hexcode":"270f","label":"pencil","tags":["pencil"],"unicode":"✏️"},{"group":7,"hexcode":"2712","label":"black nib","tags":["nib","pen"],"unicode":"✒️"},{"group":7,"hexcode":"1f58b","label":"fountain pen","tags":["fountain","pen"],"unicode":"🖋️"},{"group":7,"hexcode":"1f58a","label":"pen","tags":["ballpoint"],"unicode":"🖊️"},{"group":7,"hexcode":"1f58c","label":"paintbrush","tags":["painting"],"unicode":"🖌️"},{"group":7,"hexcode":"1f58d","label":"crayon","tags":["crayon"],"unicode":"🖍️"},{"group":7,"hexcode":"1f4dd","label":"memo","tags":["pencil"],"unicode":"📝"},{"group":7,"hexcode":"1f4bc","label":"briefcase","tags":["briefcase"],"unicode":"💼"},{"group":7,"hexcode":"1f4c1","label":"file folder","tags":["file","folder"],"unicode":"📁"},{"group":7,"hexcode":"1f4c2","label":"open file folder","tags":["file","folder","open"],"unicode":"📂"},{"group":7,"hexcode":"1f5c2","label":"card index dividers","tags":["card","dividers","index"],"unicode":"🗂️"},{"group":7,"hexcode":"1f4c5","label":"calendar","tags":["date"],"unicode":"📅"},{"group":7,"hexcode":"1f4c6","label":"tear-off calendar","tags":["calendar"],"unicode":"📆"},{"group":7,"hexcode":"1f5d2","label":"spiral notepad","tags":["note","pad","spiral"],"unicode":"🗒️"},{"group":7,"hexcode":"1f5d3","label":"spiral calendar","tags":["calendar","pad","spiral"],"unicode":"🗓️"},{"group":7,"hexcode":"1f4c7","label":"card index","tags":["card","index","rolodex"],"unicode":"📇"},{"group":7,"hexcode":"1f4c8","label":"chart increasing","tags":["chart","graph","growth","trend","upward"],"unicode":"📈"},{"group":7,"hexcode":"1f4c9","label":"chart decreasing","tags":["chart","down","graph","trend"],"unicode":"📉"},{"group":7,"hexcode":"1f4ca","label":"bar chart","tags":["bar","chart","graph"],"unicode":"📊"},{"group":7,"hexcode":"1f4cb","label":"clipboard","tags":["clipboard"],"unicode":"📋️"},{"group":7,"hexcode":"1f4cc","label":"pushpin","tags":["pin"],"unicode":"📌"},{"group":7,"hexcode":"1f4cd","label":"round pushpin","tags":["pin","pushpin"],"unicode":"📍"},{"group":7,"hexcode":"1f4ce","label":"paperclip","tags":["paperclip"],"unicode":"📎"},{"group":7,"hexcode":"1f587","label":"linked paperclips","tags":["link","paperclip"],"unicode":"🖇️"},{"group":7,"hexcode":"1f4cf","label":"straight ruler","tags":["ruler","straight edge"],"unicode":"📏"},{"group":7,"hexcode":"1f4d0","label":"triangular ruler","tags":["ruler","set","triangle"],"unicode":"📐"},{"group":7,"hexcode":"2702","label":"scissors","tags":["cutting","tool"],"unicode":"✂️"},{"group":7,"hexcode":"1f5c3","label":"card file box","tags":["box","card","file"],"unicode":"🗃️"},{"group":7,"hexcode":"1f5c4","label":"file cabinet","tags":["cabinet","file","filing"],"unicode":"🗄️"},{"group":7,"hexcode":"1f5d1","label":"wastebasket","tags":["wastebasket"],"unicode":"🗑️"},{"group":7,"hexcode":"1f512","label":"locked","tags":["closed"],"unicode":"🔒️"},{"group":7,"hexcode":"1f513","label":"unlocked","tags":["lock","open","unlock"],"unicode":"🔓️"},{"group":7,"hexcode":"1f50f","label":"locked with pen","tags":["ink","lock","nib","pen","privacy"],"unicode":"🔏"},{"group":7,"hexcode":"1f510","label":"locked with key","tags":["closed","key","lock","secure"],"unicode":"🔐"},{"group":7,"hexcode":"1f511","label":"key","tags":["lock","password"],"unicode":"🔑"},{"group":7,"hexcode":"1f5dd","label":"old key","tags":["clue","key","lock","old"],"unicode":"🗝️"},{"group":7,"hexcode":"1f528","label":"hammer","tags":["tool"],"unicode":"🔨"},{"group":7,"hexcode":"1fa93","label":"axe","tags":["chop","hatchet","split","wood"],"unicode":"🪓"},{"group":7,"hexcode":"26cf","label":"pick","tags":["mining","tool"],"unicode":"⛏️"},{"group":7,"hexcode":"2692","label":"hammer and pick","tags":["hammer","pick","tool"],"unicode":"⚒️"},{"group":7,"hexcode":"1f6e0","label":"hammer and wrench","tags":["hammer","spanner","tool","wrench"],"unicode":"🛠️"},{"group":7,"hexcode":"1f5e1","label":"dagger","tags":["knife","weapon"],"unicode":"🗡️"},{"group":7,"hexcode":"2694","label":"crossed swords","tags":["crossed","swords","weapon"],"unicode":"⚔️"},{"group":7,"hexcode":"1f4a3","label":"bomb","tags":["comic"],"unicode":"💣️"},{"group":7,"hexcode":"1fa83","label":"boomerang","tags":["rebound","repercussion"],"unicode":"🪃"},{"group":7,"hexcode":"1f3f9","label":"bow and arrow","tags":["archer","arrow","bow","sagittarius","zodiac"],"unicode":"🏹"},{"group":7,"hexcode":"1f6e1","label":"shield","tags":["weapon"],"unicode":"🛡️"},{"group":7,"hexcode":"1fa9a","label":"carpentry saw","tags":["carpenter","lumber","saw","tool"],"unicode":"🪚"},{"group":7,"hexcode":"1f527","label":"wrench","tags":["spanner","tool"],"unicode":"🔧"},{"group":7,"hexcode":"1fa9b","label":"screwdriver","tags":["screw","tool"],"unicode":"🪛"},{"group":7,"hexcode":"1f529","label":"nut and bolt","tags":["bolt","nut","tool"],"unicode":"🔩"},{"group":7,"hexcode":"2699","label":"gear","tags":["cog","cogwheel","tool"],"unicode":"⚙️"},{"group":7,"hexcode":"1f5dc","label":"clamp","tags":["compress","tool","vice"],"unicode":"🗜️"},{"group":7,"hexcode":"2696","label":"balance scale","tags":["balance","justice","libra","scale","zodiac"],"unicode":"⚖️"},{"group":7,"hexcode":"1f9af","label":"white cane","tags":["accessibility","blind"],"unicode":"🦯"},{"group":7,"hexcode":"1f517","label":"link","tags":["link"],"unicode":"🔗"},{"group":7,"hexcode":"26d3","label":"chains","tags":["chain"],"unicode":"⛓️"},{"group":7,"hexcode":"1fa9d","label":"hook","tags":["catch","crook","curve","ensnare","selling point"],"unicode":"🪝"},{"group":7,"hexcode":"1f9f0","label":"toolbox","tags":["chest","mechanic","tool"],"unicode":"🧰"},{"group":7,"hexcode":"1f9f2","label":"magnet","tags":["attraction","horseshoe","magnetic"],"unicode":"🧲"},{"group":7,"hexcode":"1fa9c","label":"ladder","tags":["climb","rung","step"],"unicode":"🪜"},{"group":7,"hexcode":"2697","label":"alembic","tags":["chemistry","tool"],"unicode":"⚗️"},{"group":7,"hexcode":"1f9ea","label":"test tube","tags":["chemist","chemistry","experiment","lab","science"],"unicode":"🧪"},{"group":7,"hexcode":"1f9eb","label":"petri dish","tags":["bacteria","biologist","biology","culture","lab"],"unicode":"🧫"},{"group":7,"hexcode":"1f9ec","label":"dna","tags":["biologist","evolution","gene","genetics","life"],"unicode":"🧬"},{"group":7,"hexcode":"1f52c","label":"microscope","tags":["science","tool"],"unicode":"🔬"},{"group":7,"hexcode":"1f52d","label":"telescope","tags":["science","tool"],"unicode":"🔭"},{"group":7,"hexcode":"1f4e1","label":"satellite antenna","tags":["antenna","dish","satellite"],"unicode":"📡"},{"group":7,"hexcode":"1f489","label":"syringe","tags":["medicine","needle","shot","sick"],"unicode":"💉"},{"group":7,"hexcode":"1fa78","label":"drop of blood","tags":["bleed","blood donation","injury","medicine","menstruation"],"unicode":"🩸"},{"group":7,"hexcode":"1f48a","label":"pill","tags":["doctor","medicine","sick"],"unicode":"💊"},{"group":7,"hexcode":"1fa79","label":"adhesive bandage","tags":["bandage"],"unicode":"🩹"},{"group":7,"hexcode":"1fa7c","label":"crutch","tags":["cane","disability","hurt","mobility aid","stick"],"unicode":"🩼"},{"group":7,"hexcode":"1fa7a","label":"stethoscope","tags":["doctor","heart","medicine"],"unicode":"🩺"},{"group":7,"hexcode":"1fa7b","label":"x-ray","tags":["bones","doctor","medical","skeleton"],"unicode":"🩻"},{"group":7,"hexcode":"1f6aa","label":"door","tags":["door"],"unicode":"🚪"},{"group":7,"hexcode":"1f6d7","label":"elevator","tags":["accessibility","hoist","lift"],"unicode":"🛗"},{"group":7,"hexcode":"1fa9e","label":"mirror","tags":["reflection","reflector","speculum"],"unicode":"🪞"},{"group":7,"hexcode":"1fa9f","label":"window","tags":["frame","fresh air","opening","transparent","view"],"unicode":"🪟"},{"group":7,"hexcode":"1f6cf","label":"bed","tags":["hotel","sleep"],"unicode":"🛏️"},{"group":7,"hexcode":"1f6cb","label":"couch and lamp","tags":["couch","hotel","lamp"],"unicode":"🛋️"},{"group":7,"hexcode":"1fa91","label":"chair","tags":["seat","sit"],"unicode":"🪑"},{"group":7,"hexcode":"1f6bd","label":"toilet","tags":["toilet"],"unicode":"🚽"},{"group":7,"hexcode":"1faa0","label":"plunger","tags":["force cup","plumber","suction","toilet"],"unicode":"🪠"},{"group":7,"hexcode":"1f6bf","label":"shower","tags":["water"],"unicode":"🚿"},{"group":7,"hexcode":"1f6c1","label":"bathtub","tags":["bath"],"unicode":"🛁"},{"group":7,"hexcode":"1faa4","label":"mouse trap","tags":["bait","mousetrap","snare","trap"],"unicode":"🪤"},{"group":7,"hexcode":"1fa92","label":"razor","tags":["sharp","shave"],"unicode":"🪒"},{"group":7,"hexcode":"1f9f4","label":"lotion bottle","tags":["lotion","moisturizer","shampoo","sunscreen"],"unicode":"🧴"},{"group":7,"hexcode":"1f9f7","label":"safety pin","tags":["diaper","punk rock"],"unicode":"🧷"},{"group":7,"hexcode":"1f9f9","label":"broom","tags":["cleaning","sweeping","witch"],"unicode":"🧹"},{"group":7,"hexcode":"1f9fa","label":"basket","tags":["farming","laundry","picnic"],"unicode":"🧺"},{"group":7,"hexcode":"1f9fb","label":"roll of paper","tags":["paper towels","toilet paper"],"unicode":"🧻"},{"group":7,"hexcode":"1faa3","label":"bucket","tags":["cask","pail","vat"],"unicode":"🪣"},{"group":7,"hexcode":"1f9fc","label":"soap","tags":["bar","bathing","cleaning","lather","soapdish"],"unicode":"🧼"},{"group":7,"hexcode":"1fae7","label":"bubbles","tags":["burp","clean","soap","underwater"],"unicode":"🫧"},{"group":7,"hexcode":"1faa5","label":"toothbrush","tags":["bathroom","brush","clean","dental","hygiene","teeth"],"unicode":"🪥"},{"group":7,"hexcode":"1f9fd","label":"sponge","tags":["absorbing","cleaning","porous"],"unicode":"🧽"},{"group":7,"hexcode":"1f9ef","label":"fire extinguisher","tags":["extinguish","fire","quench"],"unicode":"🧯"},{"group":7,"hexcode":"1f6d2","label":"shopping cart","tags":["cart","shopping","trolley"],"unicode":"🛒"},{"group":7,"hexcode":"1f6ac","label":"cigarette","tags":["smoking"],"unicode":"🚬"},{"group":7,"hexcode":"26b0","label":"coffin","tags":["death"],"unicode":"⚰️"},{"group":7,"hexcode":"1faa6","label":"headstone","tags":["cemetery","grave","graveyard","tombstone"],"unicode":"🪦"},{"group":7,"hexcode":"26b1","label":"funeral urn","tags":["ashes","death","funeral","urn"],"unicode":"⚱️"},{"group":7,"hexcode":"1f9ff","label":"nazar amulet","tags":["bead","charm","evil-eye","nazar","talisman"],"unicode":"🧿"},{"group":7,"hexcode":"1faac","label":"hamsa","tags":["amulet","fatima","hand","mary","miriam","protection"],"unicode":"🪬"},{"group":7,"hexcode":"1f5ff","label":"moai","tags":["face","moyai","statue"],"unicode":"🗿"},{"group":7,"hexcode":"1faa7","label":"placard","tags":["demonstration","picket","protest","sign"],"unicode":"🪧"},{"group":7,"hexcode":"1faaa","label":"identification card","tags":["credentials","id","license","security"],"unicode":"🪪"},{"group":8,"hexcode":"1f3e7","label":"atm sign","tags":["atm","atm sign","automated","bank","teller"],"unicode":"🏧"},{"group":8,"hexcode":"1f6ae","label":"litter in bin sign","tags":["litter","litter bin"],"unicode":"🚮"},{"group":8,"hexcode":"1f6b0","label":"potable water","tags":["drinking","potable","water"],"unicode":"🚰"},{"group":8,"hexcode":"267f","label":"wheelchair symbol","tags":["access"],"unicode":"♿️"},{"group":8,"hexcode":"1f6b9","label":"men’s room","tags":["bathroom","lavatory","man","restroom","toilet","wc"],"unicode":"🚹️"},{"group":8,"hexcode":"1f6ba","label":"women’s room","tags":["bathroom","lavatory","restroom","toilet","wc","woman"],"unicode":"🚺️"},{"group":8,"hexcode":"1f6bb","label":"restroom","tags":["bathroom","lavatory","toilet","wc"],"unicode":"🚻"},{"group":8,"hexcode":"1f6bc","label":"baby symbol","tags":["baby","changing"],"unicode":"🚼️"},{"group":8,"hexcode":"1f6be","label":"water closet","tags":["bathroom","closet","lavatory","restroom","toilet","water","wc"],"unicode":"🚾"},{"group":8,"hexcode":"1f6c2","label":"passport control","tags":["control","passport"],"unicode":"🛂"},{"group":8,"hexcode":"1f6c3","label":"customs","tags":["customs"],"unicode":"🛃"},{"group":8,"hexcode":"1f6c4","label":"baggage claim","tags":["baggage","claim"],"unicode":"🛄"},{"group":8,"hexcode":"1f6c5","label":"left luggage","tags":["baggage","locker","luggage"],"unicode":"🛅"},{"group":8,"hexcode":"26a0","label":"warning","tags":["warning"],"unicode":"⚠️"},{"group":8,"hexcode":"1f6b8","label":"children crossing","tags":["child","crossing","pedestrian","traffic"],"unicode":"🚸"},{"group":8,"hexcode":"26d4","label":"no entry","tags":["entry","forbidden","no","not","prohibited","traffic"],"unicode":"⛔️"},{"group":8,"hexcode":"1f6ab","label":"prohibited","tags":["entry","forbidden","no","not"],"unicode":"🚫"},{"group":8,"hexcode":"1f6b3","label":"no bicycles","tags":["bicycle","bike","forbidden","no","prohibited"],"unicode":"🚳"},{"group":8,"hexcode":"1f6ad","label":"no smoking","tags":["forbidden","no","not","prohibited","smoking"],"unicode":"🚭️"},{"group":8,"hexcode":"1f6af","label":"no littering","tags":["forbidden","litter","no","not","prohibited"],"unicode":"🚯"},{"group":8,"hexcode":"1f6b1","label":"non-potable water","tags":["non-drinking","non-potable","water"],"unicode":"🚱"},{"group":8,"hexcode":"1f6b7","label":"no pedestrians","tags":["forbidden","no","not","pedestrian","prohibited"],"unicode":"🚷"},{"group":8,"hexcode":"1f4f5","label":"no mobile phones","tags":["cell","forbidden","mobile","no","phone"],"unicode":"📵"},{"group":8,"hexcode":"1f51e","label":"no one under eighteen","tags":["18","age restriction","eighteen","prohibited","underage"],"unicode":"🔞"},{"group":8,"hexcode":"2622","label":"radioactive","tags":["sign"],"unicode":"☢️"},{"group":8,"hexcode":"2623","label":"biohazard","tags":["sign"],"unicode":"☣️"},{"group":8,"hexcode":"2b06","label":"up arrow","tags":["arrow","cardinal","direction","north"],"unicode":"⬆️"},{"group":8,"hexcode":"2197","label":"up-right arrow","tags":["arrow","direction","intercardinal","northeast"],"unicode":"↗️"},{"group":8,"hexcode":"27a1","label":"right arrow","tags":["arrow","cardinal","direction","east"],"unicode":"➡️"},{"group":8,"hexcode":"2198","label":"down-right arrow","tags":["arrow","direction","intercardinal","southeast"],"unicode":"↘️"},{"group":8,"hexcode":"2b07","label":"down arrow","tags":["arrow","cardinal","direction","down","south"],"unicode":"⬇️"},{"group":8,"hexcode":"2199","label":"down-left arrow","tags":["arrow","direction","intercardinal","southwest"],"unicode":"↙️"},{"group":8,"hexcode":"2b05","label":"left arrow","tags":["arrow","cardinal","direction","west"],"unicode":"⬅️"},{"group":8,"hexcode":"2196","label":"up-left arrow","tags":["arrow","direction","intercardinal","northwest"],"unicode":"↖️"},{"group":8,"hexcode":"2195","label":"up-down arrow","tags":["arrow"],"unicode":"↕️"},{"group":8,"hexcode":"2194","label":"left-right arrow","tags":["arrow"],"unicode":"↔️"},{"group":8,"hexcode":"21a9","label":"right arrow curving left","tags":["arrow"],"unicode":"↩️"},{"group":8,"hexcode":"21aa","label":"left arrow curving right","tags":["arrow"],"unicode":"↪️"},{"group":8,"hexcode":"2934","label":"right arrow curving up","tags":["arrow"],"unicode":"⤴️"},{"group":8,"hexcode":"2935","label":"right arrow curving down","tags":["arrow","down"],"unicode":"⤵️"},{"group":8,"hexcode":"1f503","label":"clockwise vertical arrows","tags":["arrow","clockwise","reload"],"unicode":"🔃"},{"group":8,"hexcode":"1f504","label":"counterclockwise arrows button","tags":["anticlockwise","arrow","counterclockwise","withershins"],"unicode":"🔄"},{"group":8,"hexcode":"1f519","label":"back arrow","tags":["arrow","back"],"unicode":"🔙"},{"group":8,"hexcode":"1f51a","label":"end arrow","tags":["arrow","end"],"unicode":"🔚"},{"group":8,"hexcode":"1f51b","label":"on! arrow","tags":["arrow","mark","on","on!"],"unicode":"🔛"},{"group":8,"hexcode":"1f51c","label":"soon arrow","tags":["arrow","soon"],"unicode":"🔜"},{"group":8,"hexcode":"1f51d","label":"top arrow","tags":["arrow","top","up"],"unicode":"🔝"},{"group":8,"hexcode":"1f6d0","label":"place of worship","tags":["religion","worship"],"unicode":"🛐"},{"group":8,"hexcode":"269b","label":"atom symbol","tags":["atheist","atom"],"unicode":"⚛️"},{"group":8,"hexcode":"1f549","label":"om","tags":["hindu","religion"],"unicode":"🕉️"},{"group":8,"hexcode":"2721","label":"star of david","tags":["david","jew","jewish","religion","star","star of david"],"unicode":"✡️"},{"group":8,"hexcode":"2638","label":"wheel of dharma","tags":["buddhist","dharma","religion","wheel"],"unicode":"☸️"},{"group":8,"hexcode":"262f","label":"yin yang","tags":["religion","tao","taoist","yang","yin"],"unicode":"☯️"},{"group":8,"hexcode":"271d","label":"latin cross","tags":["christian","cross","religion"],"unicode":"✝️"},{"group":8,"hexcode":"2626","label":"orthodox cross","tags":["christian","cross","religion"],"unicode":"☦️"},{"group":8,"hexcode":"262a","label":"star and crescent","tags":["islam","muslim","religion"],"unicode":"☪️"},{"group":8,"hexcode":"262e","label":"peace symbol","tags":["peace"],"unicode":"☮️"},{"group":8,"hexcode":"1f54e","label":"menorah","tags":["candelabrum","candlestick","religion"],"unicode":"🕎"},{"group":8,"hexcode":"1f52f","label":"dotted six-pointed star","tags":["fortune","star"],"unicode":"🔯"},{"group":8,"hexcode":"1faaf","label":"khanda","tags":["religion","sikh"],"unicode":"🪯"},{"group":8,"hexcode":"2648","label":"aries","tags":["aries","ram","zodiac"],"unicode":"♈️"},{"group":8,"hexcode":"2649","label":"taurus","tags":["bull","ox","taurus","zodiac"],"unicode":"♉️"},{"group":8,"hexcode":"264a","label":"gemini","tags":["gemini","twins","zodiac"],"unicode":"♊️"},{"group":8,"hexcode":"264b","label":"cancer","tags":["cancer","crab","zodiac"],"unicode":"♋️"},{"group":8,"hexcode":"264c","label":"leo","tags":["leo","lion","zodiac"],"unicode":"♌️"},{"group":8,"hexcode":"264d","label":"virgo","tags":["virgo","zodiac"],"unicode":"♍️"},{"group":8,"hexcode":"264e","label":"libra","tags":["balance","justice","libra","scales","zodiac"],"unicode":"♎️"},{"group":8,"hexcode":"264f","label":"scorpio","tags":["scorpio","scorpion","scorpius","zodiac"],"unicode":"♏️"},{"group":8,"hexcode":"2650","label":"sagittarius","tags":["archer","sagittarius","zodiac"],"unicode":"♐️"},{"group":8,"hexcode":"2651","label":"capricorn","tags":["capricorn","goat","zodiac"],"unicode":"♑️"},{"group":8,"hexcode":"2652","label":"aquarius","tags":["aquarius","bearer","water","zodiac"],"unicode":"♒️"},{"group":8,"hexcode":"2653","label":"pisces","tags":["fish","pisces","zodiac"],"unicode":"♓️"},{"group":8,"hexcode":"26ce","label":"ophiuchus","tags":["bearer","ophiuchus","serpent","snake","zodiac"],"unicode":"⛎"},{"group":8,"hexcode":"1f500","label":"shuffle tracks button","tags":["arrow","crossed"],"unicode":"🔀"},{"group":8,"hexcode":"1f501","label":"repeat button","tags":["arrow","clockwise","repeat"],"unicode":"🔁"},{"group":8,"hexcode":"1f502","label":"repeat single button","tags":["arrow","clockwise","once"],"unicode":"🔂"},{"group":8,"hexcode":"25b6","label":"play button","tags":["arrow","play","right","triangle"],"unicode":"▶️"},{"group":8,"hexcode":"23e9","label":"fast-forward button","tags":["arrow","double","fast","forward"],"unicode":"⏩️"},{"group":8,"hexcode":"23ed","label":"next track button","tags":["arrow","next scene","next track","triangle"],"unicode":"⏭️"},{"group":8,"hexcode":"23ef","label":"play or pause button","tags":["arrow","pause","play","right","triangle"],"unicode":"⏯️"},{"group":8,"hexcode":"25c0","label":"reverse button","tags":["arrow","left","reverse","triangle"],"unicode":"◀️"},{"group":8,"hexcode":"23ea","label":"fast reverse button","tags":["arrow","double","rewind"],"unicode":"⏪️"},{"group":8,"hexcode":"23ee","label":"last track button","tags":["arrow","previous scene","previous track","triangle"],"unicode":"⏮️"},{"group":8,"hexcode":"1f53c","label":"upwards button","tags":["arrow","button"],"unicode":"🔼"},{"group":8,"hexcode":"23eb","label":"fast up button","tags":["arrow","double"],"unicode":"⏫"},{"group":8,"hexcode":"1f53d","label":"downwards button","tags":["arrow","button","down"],"unicode":"🔽"},{"group":8,"hexcode":"23ec","label":"fast down button","tags":["arrow","double","down"],"unicode":"⏬"},{"group":8,"hexcode":"23f8","label":"pause button","tags":["bar","double","pause","vertical"],"unicode":"⏸️"},{"group":8,"hexcode":"23f9","label":"stop button","tags":["square","stop"],"unicode":"⏹️"},{"group":8,"hexcode":"23fa","label":"record button","tags":["circle","record"],"unicode":"⏺️"},{"group":8,"hexcode":"23cf","label":"eject button","tags":["eject"],"unicode":"⏏️"},{"group":8,"hexcode":"1f3a6","label":"cinema","tags":["camera","film","movie"],"unicode":"🎦"},{"group":8,"hexcode":"1f505","label":"dim button","tags":["brightness","dim","low"],"unicode":"🔅"},{"group":8,"hexcode":"1f506","label":"bright button","tags":["bright","brightness"],"unicode":"🔆"},{"group":8,"hexcode":"1f4f6","label":"antenna bars","tags":["antenna","bar","cell","mobile","phone"],"unicode":"📶"},{"group":8,"hexcode":"1f6dc","label":"wireless","tags":["computer","internet","network"],"unicode":"🛜"},{"group":8,"hexcode":"1f4f3","label":"vibration mode","tags":["cell","mobile","mode","phone","telephone","vibration"],"unicode":"📳"},{"group":8,"hexcode":"1f4f4","label":"mobile phone off","tags":["cell","mobile","off","phone","telephone"],"unicode":"📴"},{"group":8,"hexcode":"2640","label":"female sign","tags":["woman"],"unicode":"♀️"},{"group":8,"hexcode":"2642","label":"male sign","tags":["man"],"unicode":"♂️"},{"group":8,"hexcode":"26a7","label":"transgender symbol","tags":["transgender"],"unicode":"⚧️"},{"group":8,"hexcode":"2716","label":"multiply","tags":["cancel","multiplication","sign","x","×"],"unicode":"✖️"},{"group":8,"hexcode":"2795","label":"plus","tags":["+","math","sign"],"unicode":"➕"},{"group":8,"hexcode":"2796","label":"minus","tags":["-","math","sign","−"],"unicode":"➖"},{"group":8,"hexcode":"2797","label":"divide","tags":["division","math","sign","÷"],"unicode":"➗"},{"group":8,"hexcode":"1f7f0","label":"heavy equals sign","tags":["equality","math"],"unicode":"🟰"},{"group":8,"hexcode":"267e","label":"infinity","tags":["forever","unbounded","universal"],"unicode":"♾️"},{"group":8,"hexcode":"203c","label":"double exclamation mark","tags":["!","!!","bangbang","exclamation","mark"],"unicode":"‼️"},{"group":8,"hexcode":"2049","label":"exclamation question mark","tags":["!","!?","?","exclamation","interrobang","mark","punctuation","question"],"unicode":"⁉️"},{"group":8,"hexcode":"2753","label":"red question mark","tags":["?","mark","punctuation","question"],"unicode":"❓️"},{"group":8,"hexcode":"2754","label":"white question mark","tags":["?","mark","outlined","punctuation","question"],"unicode":"❔"},{"group":8,"hexcode":"2755","label":"white exclamation mark","tags":["!","exclamation","mark","outlined","punctuation"],"unicode":"❕"},{"group":8,"hexcode":"2757","label":"red exclamation mark","tags":["!","exclamation","mark","punctuation"],"unicode":"❗️"},{"group":8,"hexcode":"3030","label":"wavy dash","tags":["dash","punctuation","wavy"],"unicode":"〰️"},{"group":8,"hexcode":"1f4b1","label":"currency exchange","tags":["bank","currency","exchange","money"],"unicode":"💱"},{"group":8,"hexcode":"1f4b2","label":"heavy dollar sign","tags":["currency","dollar","money"],"unicode":"💲"},{"group":8,"hexcode":"2695","label":"medical symbol","tags":["aesculapius","medicine","staff"],"unicode":"⚕️"},{"group":8,"hexcode":"267b","label":"recycling symbol","tags":["recycle"],"unicode":"♻️"},{"group":8,"hexcode":"269c","label":"fleur-de-lis","tags":["fleur-de-lis"],"unicode":"⚜️"},{"group":8,"hexcode":"1f531","label":"trident emblem","tags":["anchor","emblem","ship","tool","trident"],"unicode":"🔱"},{"group":8,"hexcode":"1f4db","label":"name badge","tags":["badge","name"],"unicode":"📛"},{"group":8,"hexcode":"1f530","label":"japanese symbol for beginner","tags":["beginner","chevron","japanese","japanese symbol for beginner","leaf"],"unicode":"🔰"},{"group":8,"hexcode":"2b55","label":"hollow red circle","tags":["circle","large","o","red"],"unicode":"⭕️"},{"group":8,"hexcode":"2705","label":"check mark button","tags":["button","check","mark","✓"],"unicode":"✅"},{"group":8,"hexcode":"2611","label":"check box with check","tags":["box","check","✓"],"unicode":"☑️"},{"group":8,"hexcode":"2714","label":"check mark","tags":["check","mark","✓"],"unicode":"✔️"},{"group":8,"hexcode":"274c","label":"cross mark","tags":["cancel","cross","mark","multiplication","multiply","x","×"],"unicode":"❌"},{"group":8,"hexcode":"274e","label":"cross mark button","tags":["mark","square","x","×"],"unicode":"❎"},{"group":8,"hexcode":"27b0","label":"curly loop","tags":["curl","loop"],"unicode":"➰"},{"group":8,"hexcode":"27bf","label":"double curly loop","tags":["curl","double","loop"],"unicode":"➿"},{"group":8,"hexcode":"303d","label":"part alternation mark","tags":["mark","part"],"unicode":"〽️"},{"group":8,"hexcode":"2733","label":"eight-spoked asterisk","tags":["*","asterisk"],"unicode":"✳️"},{"group":8,"hexcode":"2734","label":"eight-pointed star","tags":["*","star"],"unicode":"✴️"},{"group":8,"hexcode":"2747","label":"sparkle","tags":["*"],"unicode":"❇️"},{"group":8,"hexcode":"00a9","label":"copyright","tags":["c"],"unicode":"©️"},{"group":8,"hexcode":"00ae","label":"registered","tags":["r"],"unicode":"®️"},{"group":8,"hexcode":"2122","label":"trade mark","tags":["mark","tm","trademark"],"unicode":"™️"},{"group":8,"hexcode":"0023-fe0f-20e3","label":"keycap: #","tags":["keycap"],"unicode":"#️⃣"},{"group":8,"hexcode":"002a-fe0f-20e3","label":"keycap: *","tags":["keycap"],"unicode":"*️⃣"},{"group":8,"hexcode":"0030-fe0f-20e3","label":"keycap: 0","tags":["keycap"],"unicode":"0️⃣"},{"group":8,"hexcode":"0031-fe0f-20e3","label":"keycap: 1","tags":["keycap"],"unicode":"1️⃣"},{"group":8,"hexcode":"0032-fe0f-20e3","label":"keycap: 2","tags":["keycap"],"unicode":"2️⃣"},{"group":8,"hexcode":"0033-fe0f-20e3","label":"keycap: 3","tags":["keycap"],"unicode":"3️⃣"},{"group":8,"hexcode":"0034-fe0f-20e3","label":"keycap: 4","tags":["keycap"],"unicode":"4️⃣"},{"group":8,"hexcode":"0035-fe0f-20e3","label":"keycap: 5","tags":["keycap"],"unicode":"5️⃣"},{"group":8,"hexcode":"0036-fe0f-20e3","label":"keycap: 6","tags":["keycap"],"unicode":"6️⃣"},{"group":8,"hexcode":"0037-fe0f-20e3","label":"keycap: 7","tags":["keycap"],"unicode":"7️⃣"},{"group":8,"hexcode":"0038-fe0f-20e3","label":"keycap: 8","tags":["keycap"],"unicode":"8️⃣"},{"group":8,"hexcode":"0039-fe0f-20e3","label":"keycap: 9","tags":["keycap"],"unicode":"9️⃣"},{"group":8,"hexcode":"1f51f","label":"keycap: 10","tags":["keycap"],"unicode":"🔟"},{"group":8,"hexcode":"1f520","label":"input latin uppercase","tags":["abcd","input","latin","letters","uppercase"],"unicode":"🔠"},{"group":8,"hexcode":"1f521","label":"input latin lowercase","tags":["abcd","input","latin","letters","lowercase"],"unicode":"🔡"},{"group":8,"hexcode":"1f522","label":"input numbers","tags":["1234","input","numbers"],"unicode":"🔢"},{"group":8,"hexcode":"1f523","label":"input symbols","tags":["input","〒♪&%"],"unicode":"🔣"},{"group":8,"hexcode":"1f524","label":"input latin letters","tags":["abc","alphabet","input","latin","letters"],"unicode":"🔤"},{"group":8,"hexcode":"1f170","label":"a button (blood type)","tags":["a","a button (blood type)","blood type"],"unicode":"🅰️"},{"group":8,"hexcode":"1f18e","label":"ab button (blood type)","tags":["ab","ab button (blood type)","blood type"],"unicode":"🆎"},{"group":8,"hexcode":"1f171","label":"b button (blood type)","tags":["b","b button (blood type)","blood type"],"unicode":"🅱️"},{"group":8,"hexcode":"1f191","label":"cl button","tags":["cl","cl button"],"unicode":"🆑"},{"group":8,"hexcode":"1f192","label":"cool button","tags":["cool","cool button"],"unicode":"🆒"},{"group":8,"hexcode":"1f193","label":"free button","tags":["free","free button"],"unicode":"🆓"},{"group":8,"hexcode":"2139","label":"information","tags":["i"],"unicode":"ℹ️"},{"group":8,"hexcode":"1f194","label":"id button","tags":["id","id button","identity"],"unicode":"🆔"},{"group":8,"hexcode":"24c2","label":"circled m","tags":["circle","circled m","m"],"unicode":"Ⓜ️"},{"group":8,"hexcode":"1f195","label":"new button","tags":["new","new button"],"unicode":"🆕"},{"group":8,"hexcode":"1f196","label":"ng button","tags":["ng","ng button"],"unicode":"🆖"},{"group":8,"hexcode":"1f17e","label":"o button (blood type)","tags":["blood type","o","o button (blood type)"],"unicode":"🅾️"},{"group":8,"hexcode":"1f197","label":"ok button","tags":["ok","ok button"],"unicode":"🆗"},{"group":8,"hexcode":"1f17f","label":"p button","tags":["p","p button","parking"],"unicode":"🅿️"},{"group":8,"hexcode":"1f198","label":"sos button","tags":["help","sos","sos button"],"unicode":"🆘"},{"group":8,"hexcode":"1f199","label":"up! button","tags":["mark","up","up!","up! button"],"unicode":"🆙"},{"group":8,"hexcode":"1f19a","label":"vs button","tags":["versus","vs","vs button"],"unicode":"🆚"},{"group":8,"hexcode":"1f201","label":"japanese “here” button","tags":["japanese","japanese “here” button","katakana","“here”","ココ"],"unicode":"🈁"},{"group":8,"hexcode":"1f202","label":"japanese “service charge” button","tags":["japanese","japanese “service charge” button","katakana","“service charge”","サ"],"unicode":"🈂️"},{"group":8,"hexcode":"1f237","label":"japanese “monthly amount” button","tags":["ideograph","japanese","japanese “monthly amount” button","“monthly amount”","月"],"unicode":"🈷️"},{"group":8,"hexcode":"1f236","label":"japanese “not free of charge” button","tags":["ideograph","japanese","japanese “not free of charge” button","“not free of charge”","有"],"unicode":"🈶"},{"group":8,"hexcode":"1f22f","label":"japanese “reserved” button","tags":["ideograph","japanese","japanese “reserved” button","“reserved”","指"],"unicode":"🈯️"},{"group":8,"hexcode":"1f250","label":"japanese “bargain” button","tags":["ideograph","japanese","japanese “bargain” button","“bargain”","得"],"unicode":"🉐"},{"group":8,"hexcode":"1f239","label":"japanese “discount” button","tags":["ideograph","japanese","japanese “discount” button","“discount”","割"],"unicode":"🈹"},{"group":8,"hexcode":"1f21a","label":"japanese “free of charge” button","tags":["ideograph","japanese","japanese “free of charge” button","“free of charge”","無"],"unicode":"🈚️"},{"group":8,"hexcode":"1f232","label":"japanese “prohibited” button","tags":["ideograph","japanese","japanese “prohibited” button","“prohibited”","禁"],"unicode":"🈲"},{"group":8,"hexcode":"1f251","label":"japanese “acceptable” button","tags":["ideograph","japanese","japanese “acceptable” button","“acceptable”","可"],"unicode":"🉑"},{"group":8,"hexcode":"1f238","label":"japanese “application” button","tags":["ideograph","japanese","japanese “application” button","“application”","申"],"unicode":"🈸"},{"group":8,"hexcode":"1f234","label":"japanese “passing grade” button","tags":["ideograph","japanese","japanese “passing grade” button","“passing grade”","合"],"unicode":"🈴"},{"group":8,"hexcode":"1f233","label":"japanese “vacancy” button","tags":["ideograph","japanese","japanese “vacancy” button","“vacancy”","空"],"unicode":"🈳"},{"group":8,"hexcode":"3297","label":"japanese “congratulations” button","tags":["ideograph","japanese","japanese “congratulations” button","“congratulations”","祝"],"unicode":"㊗️"},{"group":8,"hexcode":"3299","label":"japanese “secret” button","tags":["ideograph","japanese","japanese “secret” button","“secret”","秘"],"unicode":"㊙️"},{"group":8,"hexcode":"1f23a","label":"japanese “open for business” button","tags":["ideograph","japanese","japanese “open for business” button","“open for business”","営"],"unicode":"🈺"},{"group":8,"hexcode":"1f235","label":"japanese “no vacancy” button","tags":["ideograph","japanese","japanese “no vacancy” button","“no vacancy”","満"],"unicode":"🈵"},{"group":8,"hexcode":"1f534","label":"red circle","tags":["circle","geometric","red"],"unicode":"🔴"},{"group":8,"hexcode":"1f7e0","label":"orange circle","tags":["circle","orange"],"unicode":"🟠"},{"group":8,"hexcode":"1f7e1","label":"yellow circle","tags":["circle","yellow"],"unicode":"🟡"},{"group":8,"hexcode":"1f7e2","label":"green circle","tags":["circle","green"],"unicode":"🟢"},{"group":8,"hexcode":"1f535","label":"blue circle","tags":["blue","circle","geometric"],"unicode":"🔵"},{"group":8,"hexcode":"1f7e3","label":"purple circle","tags":["circle","purple"],"unicode":"🟣"},{"group":8,"hexcode":"1f7e4","label":"brown circle","tags":["brown","circle"],"unicode":"🟤"},{"group":8,"hexcode":"26ab","label":"black circle","tags":["circle","geometric"],"unicode":"⚫️"},{"group":8,"hexcode":"26aa","label":"white circle","tags":["circle","geometric"],"unicode":"⚪️"},{"group":8,"hexcode":"1f7e5","label":"red square","tags":["red","square"],"unicode":"🟥"},{"group":8,"hexcode":"1f7e7","label":"orange square","tags":["orange","square"],"unicode":"🟧"},{"group":8,"hexcode":"1f7e8","label":"yellow square","tags":["square","yellow"],"unicode":"🟨"},{"group":8,"hexcode":"1f7e9","label":"green square","tags":["green","square"],"unicode":"🟩"},{"group":8,"hexcode":"1f7e6","label":"blue square","tags":["blue","square"],"unicode":"🟦"},{"group":8,"hexcode":"1f7ea","label":"purple square","tags":["purple","square"],"unicode":"🟪"},{"group":8,"hexcode":"1f7eb","label":"brown square","tags":["brown","square"],"unicode":"🟫"},{"group":8,"hexcode":"2b1b","label":"black large square","tags":["geometric","square"],"unicode":"⬛️"},{"group":8,"hexcode":"2b1c","label":"white large square","tags":["geometric","square"],"unicode":"⬜️"},{"group":8,"hexcode":"25fc","label":"black medium square","tags":["geometric","square"],"unicode":"◼️"},{"group":8,"hexcode":"25fb","label":"white medium square","tags":["geometric","square"],"unicode":"◻️"},{"group":8,"hexcode":"25fe","label":"black medium-small square","tags":["geometric","square"],"unicode":"◾️"},{"group":8,"hexcode":"25fd","label":"white medium-small square","tags":["geometric","square"],"unicode":"◽️"},{"group":8,"hexcode":"25aa","label":"black small square","tags":["geometric","square"],"unicode":"▪️"},{"group":8,"hexcode":"25ab","label":"white small square","tags":["geometric","square"],"unicode":"▫️"},{"group":8,"hexcode":"1f536","label":"large orange diamond","tags":["diamond","geometric","orange"],"unicode":"🔶"},{"group":8,"hexcode":"1f537","label":"large blue diamond","tags":["blue","diamond","geometric"],"unicode":"🔷"},{"group":8,"hexcode":"1f538","label":"small orange diamond","tags":["diamond","geometric","orange"],"unicode":"🔸"},{"group":8,"hexcode":"1f539","label":"small blue diamond","tags":["blue","diamond","geometric"],"unicode":"🔹"},{"group":8,"hexcode":"1f53a","label":"red triangle pointed up","tags":["geometric","red"],"unicode":"🔺"},{"group":8,"hexcode":"1f53b","label":"red triangle pointed down","tags":["down","geometric","red"],"unicode":"🔻"},{"group":8,"hexcode":"1f4a0","label":"diamond with a dot","tags":["comic","diamond","geometric","inside"],"unicode":"💠"},{"group":8,"hexcode":"1f518","label":"radio button","tags":["button","geometric","radio"],"unicode":"🔘"},{"group":8,"hexcode":"1f533","label":"white square button","tags":["button","geometric","outlined","square"],"unicode":"🔳"},{"group":8,"hexcode":"1f532","label":"black square button","tags":["button","geometric","square"],"unicode":"🔲"},{"group":9,"hexcode":"1f3c1","label":"chequered flag","tags":["checkered","chequered","racing"],"unicode":"🏁"},{"group":9,"hexcode":"1f6a9","label":"triangular flag","tags":["post"],"unicode":"🚩"},{"group":9,"hexcode":"1f38c","label":"crossed flags","tags":["celebration","cross","crossed","japanese"],"unicode":"🎌"},{"group":9,"hexcode":"1f3f4","label":"black flag","tags":["waving"],"unicode":"🏴"},{"group":9,"hexcode":"1f3f3","label":"white flag","tags":["waving"],"unicode":"🏳️"},{"group":9,"hexcode":"1f3f3-fe0f-200d-1f308","label":"rainbow flag","tags":["pride","rainbow"],"unicode":"🏳️‍🌈"},{"group":9,"hexcode":"1f3f3-fe0f-200d-26a7-fe0f","label":"transgender flag","tags":["flag","light blue","pink","transgender","white"],"unicode":"🏳️‍⚧️"},{"group":9,"hexcode":"1f3f4-200d-2620-fe0f","label":"pirate flag","tags":["jolly roger","pirate","plunder","treasure"],"unicode":"🏴‍☠️"},{"group":9,"hexcode":"1f1e6-1f1e8","label":"flag: ascension island","tags":["ac","flag"],"unicode":"🇦🇨"},{"group":9,"hexcode":"1f1e6-1f1e9","label":"flag: andorra","tags":["ad","flag"],"unicode":"🇦🇩"},{"group":9,"hexcode":"1f1e6-1f1ea","label":"flag: united arab emirates","tags":["ae","flag"],"unicode":"🇦🇪"},{"group":9,"hexcode":"1f1e6-1f1eb","label":"flag: afghanistan","tags":["af","flag"],"unicode":"🇦🇫"},{"group":9,"hexcode":"1f1e6-1f1ec","label":"flag: antigua & barbuda","tags":["ag","flag"],"unicode":"🇦🇬"},{"group":9,"hexcode":"1f1e6-1f1ee","label":"flag: anguilla","tags":["ai","flag"],"unicode":"🇦🇮"},{"group":9,"hexcode":"1f1e6-1f1f1","label":"flag: albania","tags":["al","flag"],"unicode":"🇦🇱"},{"group":9,"hexcode":"1f1e6-1f1f2","label":"flag: armenia","tags":["am","flag"],"unicode":"🇦🇲"},{"group":9,"hexcode":"1f1e6-1f1f4","label":"flag: angola","tags":["ao","flag"],"unicode":"🇦🇴"},{"group":9,"hexcode":"1f1e6-1f1f6","label":"flag: antarctica","tags":["aq","flag"],"unicode":"🇦🇶"},{"group":9,"hexcode":"1f1e6-1f1f7","label":"flag: argentina","tags":["ar","flag"],"unicode":"🇦🇷"},{"group":9,"hexcode":"1f1e6-1f1f8","label":"flag: american samoa","tags":["as","flag"],"unicode":"🇦🇸"},{"group":9,"hexcode":"1f1e6-1f1f9","label":"flag: austria","tags":["at","flag"],"unicode":"🇦🇹"},{"group":9,"hexcode":"1f1e6-1f1fa","label":"flag: australia","tags":["au","flag"],"unicode":"🇦🇺"},{"group":9,"hexcode":"1f1e6-1f1fc","label":"flag: aruba","tags":["aw","flag"],"unicode":"🇦🇼"},{"group":9,"hexcode":"1f1e6-1f1fd","label":"flag: åland islands","tags":["ax","flag"],"unicode":"🇦🇽"},{"group":9,"hexcode":"1f1e6-1f1ff","label":"flag: azerbaijan","tags":["az","flag"],"unicode":"🇦🇿"},{"group":9,"hexcode":"1f1e7-1f1e6","label":"flag: bosnia & herzegovina","tags":["ba","flag"],"unicode":"🇧🇦"},{"group":9,"hexcode":"1f1e7-1f1e7","label":"flag: barbados","tags":["bb","flag"],"unicode":"🇧🇧"},{"group":9,"hexcode":"1f1e7-1f1e9","label":"flag: bangladesh","tags":["bd","flag"],"unicode":"🇧🇩"},{"group":9,"hexcode":"1f1e7-1f1ea","label":"flag: belgium","tags":["be","flag"],"unicode":"🇧🇪"},{"group":9,"hexcode":"1f1e7-1f1eb","label":"flag: burkina faso","tags":["bf","flag"],"unicode":"🇧🇫"},{"group":9,"hexcode":"1f1e7-1f1ec","label":"flag: bulgaria","tags":["bg","flag"],"unicode":"🇧🇬"},{"group":9,"hexcode":"1f1e7-1f1ed","label":"flag: bahrain","tags":["bh","flag"],"unicode":"🇧🇭"},{"group":9,"hexcode":"1f1e7-1f1ee","label":"flag: burundi","tags":["bi","flag"],"unicode":"🇧🇮"},{"group":9,"hexcode":"1f1e7-1f1ef","label":"flag: benin","tags":["bj","flag"],"unicode":"🇧🇯"},{"group":9,"hexcode":"1f1e7-1f1f1","label":"flag: st. barthélemy","tags":["bl","flag"],"unicode":"🇧🇱"},{"group":9,"hexcode":"1f1e7-1f1f2","label":"flag: bermuda","tags":["bm","flag"],"unicode":"🇧🇲"},{"group":9,"hexcode":"1f1e7-1f1f3","label":"flag: brunei","tags":["bn","flag"],"unicode":"🇧🇳"},{"group":9,"hexcode":"1f1e7-1f1f4","label":"flag: bolivia","tags":["bo","flag"],"unicode":"🇧🇴"},{"group":9,"hexcode":"1f1e7-1f1f6","label":"flag: caribbean netherlands","tags":["bq","flag"],"unicode":"🇧🇶"},{"group":9,"hexcode":"1f1e7-1f1f7","label":"flag: brazil","tags":["br","flag"],"unicode":"🇧🇷"},{"group":9,"hexcode":"1f1e7-1f1f8","label":"flag: bahamas","tags":["bs","flag"],"unicode":"🇧🇸"},{"group":9,"hexcode":"1f1e7-1f1f9","label":"flag: bhutan","tags":["bt","flag"],"unicode":"🇧🇹"},{"group":9,"hexcode":"1f1e7-1f1fb","label":"flag: bouvet island","tags":["bv","flag"],"unicode":"🇧🇻"},{"group":9,"hexcode":"1f1e7-1f1fc","label":"flag: botswana","tags":["bw","flag"],"unicode":"🇧🇼"},{"group":9,"hexcode":"1f1e7-1f1fe","label":"flag: belarus","tags":["by","flag"],"unicode":"🇧🇾"},{"group":9,"hexcode":"1f1e7-1f1ff","label":"flag: belize","tags":["bz","flag"],"unicode":"🇧🇿"},{"group":9,"hexcode":"1f1e8-1f1e6","label":"flag: canada","tags":["ca","flag"],"unicode":"🇨🇦"},{"group":9,"hexcode":"1f1e8-1f1e8","label":"flag: cocos (keeling) islands","tags":["cc","flag"],"unicode":"🇨🇨"},{"group":9,"hexcode":"1f1e8-1f1e9","label":"flag: congo - kinshasa","tags":["cd","flag"],"unicode":"🇨🇩"},{"group":9,"hexcode":"1f1e8-1f1eb","label":"flag: central african republic","tags":["cf","flag"],"unicode":"🇨🇫"},{"group":9,"hexcode":"1f1e8-1f1ec","label":"flag: congo - brazzaville","tags":["cg","flag"],"unicode":"🇨🇬"},{"group":9,"hexcode":"1f1e8-1f1ed","label":"flag: switzerland","tags":["ch","flag"],"unicode":"🇨🇭"},{"group":9,"hexcode":"1f1e8-1f1ee","label":"flag: côte d’ivoire","tags":["ci","flag"],"unicode":"🇨🇮"},{"group":9,"hexcode":"1f1e8-1f1f0","label":"flag: cook islands","tags":["ck","flag"],"unicode":"🇨🇰"},{"group":9,"hexcode":"1f1e8-1f1f1","label":"flag: chile","tags":["cl","flag"],"unicode":"🇨🇱"},{"group":9,"hexcode":"1f1e8-1f1f2","label":"flag: cameroon","tags":["cm","flag"],"unicode":"🇨🇲"},{"group":9,"hexcode":"1f1e8-1f1f3","label":"flag: china","tags":["cn","flag"],"unicode":"🇨🇳"},{"group":9,"hexcode":"1f1e8-1f1f4","label":"flag: colombia","tags":["co","flag"],"unicode":"🇨🇴"},{"group":9,"hexcode":"1f1e8-1f1f5","label":"flag: clipperton island","tags":["cp","flag"],"unicode":"🇨🇵"},{"group":9,"hexcode":"1f1e8-1f1f7","label":"flag: costa rica","tags":["cr","flag"],"unicode":"🇨🇷"},{"group":9,"hexcode":"1f1e8-1f1fa","label":"flag: cuba","tags":["cu","flag"],"unicode":"🇨🇺"},{"group":9,"hexcode":"1f1e8-1f1fb","label":"flag: cape verde","tags":["cv","flag"],"unicode":"🇨🇻"},{"group":9,"hexcode":"1f1e8-1f1fc","label":"flag: curaçao","tags":["cw","flag"],"unicode":"🇨🇼"},{"group":9,"hexcode":"1f1e8-1f1fd","label":"flag: christmas island","tags":["cx","flag"],"unicode":"🇨🇽"},{"group":9,"hexcode":"1f1e8-1f1fe","label":"flag: cyprus","tags":["cy","flag"],"unicode":"🇨🇾"},{"group":9,"hexcode":"1f1e8-1f1ff","label":"flag: czechia","tags":["cz","flag"],"unicode":"🇨🇿"},{"group":9,"hexcode":"1f1e9-1f1ea","label":"flag: germany","tags":["de","flag"],"unicode":"🇩🇪"},{"group":9,"hexcode":"1f1e9-1f1ec","label":"flag: diego garcia","tags":["dg","flag"],"unicode":"🇩🇬"},{"group":9,"hexcode":"1f1e9-1f1ef","label":"flag: djibouti","tags":["dj","flag"],"unicode":"🇩🇯"},{"group":9,"hexcode":"1f1e9-1f1f0","label":"flag: denmark","tags":["dk","flag"],"unicode":"🇩🇰"},{"group":9,"hexcode":"1f1e9-1f1f2","label":"flag: dominica","tags":["dm","flag"],"unicode":"🇩🇲"},{"group":9,"hexcode":"1f1e9-1f1f4","label":"flag: dominican republic","tags":["do","flag"],"unicode":"🇩🇴"},{"group":9,"hexcode":"1f1e9-1f1ff","label":"flag: algeria","tags":["dz","flag"],"unicode":"🇩🇿"},{"group":9,"hexcode":"1f1ea-1f1e6","label":"flag: ceuta & melilla","tags":["ea","flag"],"unicode":"🇪🇦"},{"group":9,"hexcode":"1f1ea-1f1e8","label":"flag: ecuador","tags":["ec","flag"],"unicode":"🇪🇨"},{"group":9,"hexcode":"1f1ea-1f1ea","label":"flag: estonia","tags":["ee","flag"],"unicode":"🇪🇪"},{"group":9,"hexcode":"1f1ea-1f1ec","label":"flag: egypt","tags":["eg","flag"],"unicode":"🇪🇬"},{"group":9,"hexcode":"1f1ea-1f1ed","label":"flag: western sahara","tags":["eh","flag"],"unicode":"🇪🇭"},{"group":9,"hexcode":"1f1ea-1f1f7","label":"flag: eritrea","tags":["er","flag"],"unicode":"🇪🇷"},{"group":9,"hexcode":"1f1ea-1f1f8","label":"flag: spain","tags":["es","flag"],"unicode":"🇪🇸"},{"group":9,"hexcode":"1f1ea-1f1f9","label":"flag: ethiopia","tags":["et","flag"],"unicode":"🇪🇹"},{"group":9,"hexcode":"1f1ea-1f1fa","label":"flag: european union","tags":["eu","flag"],"unicode":"🇪🇺"},{"group":9,"hexcode":"1f1eb-1f1ee","label":"flag: finland","tags":["fi","flag"],"unicode":"🇫🇮"},{"group":9,"hexcode":"1f1eb-1f1ef","label":"flag: fiji","tags":["fj","flag"],"unicode":"🇫🇯"},{"group":9,"hexcode":"1f1eb-1f1f0","label":"flag: falkland islands","tags":["fk","flag"],"unicode":"🇫🇰"},{"group":9,"hexcode":"1f1eb-1f1f2","label":"flag: micronesia","tags":["fm","flag"],"unicode":"🇫🇲"},{"group":9,"hexcode":"1f1eb-1f1f4","label":"flag: faroe islands","tags":["fo","flag"],"unicode":"🇫🇴"},{"group":9,"hexcode":"1f1eb-1f1f7","label":"flag: france","tags":["fr","flag"],"unicode":"🇫🇷"},{"group":9,"hexcode":"1f1ec-1f1e6","label":"flag: gabon","tags":["ga","flag"],"unicode":"🇬🇦"},{"group":9,"hexcode":"1f1ec-1f1e7","label":"flag: united kingdom","tags":["gb","flag"],"unicode":"🇬🇧"},{"group":9,"hexcode":"1f1ec-1f1e9","label":"flag: grenada","tags":["gd","flag"],"unicode":"🇬🇩"},{"group":9,"hexcode":"1f1ec-1f1ea","label":"flag: georgia","tags":["ge","flag"],"unicode":"🇬🇪"},{"group":9,"hexcode":"1f1ec-1f1eb","label":"flag: french guiana","tags":["gf","flag"],"unicode":"🇬🇫"},{"group":9,"hexcode":"1f1ec-1f1ec","label":"flag: guernsey","tags":["gg","flag"],"unicode":"🇬🇬"},{"group":9,"hexcode":"1f1ec-1f1ed","label":"flag: ghana","tags":["gh","flag"],"unicode":"🇬🇭"},{"group":9,"hexcode":"1f1ec-1f1ee","label":"flag: gibraltar","tags":["gi","flag"],"unicode":"🇬🇮"},{"group":9,"hexcode":"1f1ec-1f1f1","label":"flag: greenland","tags":["gl","flag"],"unicode":"🇬🇱"},{"group":9,"hexcode":"1f1ec-1f1f2","label":"flag: gambia","tags":["gm","flag"],"unicode":"🇬🇲"},{"group":9,"hexcode":"1f1ec-1f1f3","label":"flag: guinea","tags":["gn","flag"],"unicode":"🇬🇳"},{"group":9,"hexcode":"1f1ec-1f1f5","label":"flag: guadeloupe","tags":["gp","flag"],"unicode":"🇬🇵"},{"group":9,"hexcode":"1f1ec-1f1f6","label":"flag: equatorial guinea","tags":["gq","flag"],"unicode":"🇬🇶"},{"group":9,"hexcode":"1f1ec-1f1f7","label":"flag: greece","tags":["gr","flag"],"unicode":"🇬🇷"},{"group":9,"hexcode":"1f1ec-1f1f8","label":"flag: south georgia & south sandwich islands","tags":["gs","flag"],"unicode":"🇬🇸"},{"group":9,"hexcode":"1f1ec-1f1f9","label":"flag: guatemala","tags":["gt","flag"],"unicode":"🇬🇹"},{"group":9,"hexcode":"1f1ec-1f1fa","label":"flag: guam","tags":["gu","flag"],"unicode":"🇬🇺"},{"group":9,"hexcode":"1f1ec-1f1fc","label":"flag: guinea-bissau","tags":["gw","flag"],"unicode":"🇬🇼"},{"group":9,"hexcode":"1f1ec-1f1fe","label":"flag: guyana","tags":["gy","flag"],"unicode":"🇬🇾"},{"group":9,"hexcode":"1f1ed-1f1f0","label":"flag: hong kong sar china","tags":["hk","flag"],"unicode":"🇭🇰"},{"group":9,"hexcode":"1f1ed-1f1f2","label":"flag: heard & mcdonald islands","tags":["hm","flag"],"unicode":"🇭🇲"},{"group":9,"hexcode":"1f1ed-1f1f3","label":"flag: honduras","tags":["hn","flag"],"unicode":"🇭🇳"},{"group":9,"hexcode":"1f1ed-1f1f7","label":"flag: croatia","tags":["hr","flag"],"unicode":"🇭🇷"},{"group":9,"hexcode":"1f1ed-1f1f9","label":"flag: haiti","tags":["ht","flag"],"unicode":"🇭🇹"},{"group":9,"hexcode":"1f1ed-1f1fa","label":"flag: hungary","tags":["hu","flag"],"unicode":"🇭🇺"},{"group":9,"hexcode":"1f1ee-1f1e8","label":"flag: canary islands","tags":["ic","flag"],"unicode":"🇮🇨"},{"group":9,"hexcode":"1f1ee-1f1e9","label":"flag: indonesia","tags":["id","flag"],"unicode":"🇮🇩"},{"group":9,"hexcode":"1f1ee-1f1ea","label":"flag: ireland","tags":["ie","flag"],"unicode":"🇮🇪"},{"group":9,"hexcode":"1f1ee-1f1f1","label":"flag: israel","tags":["il","flag"],"unicode":"🇮🇱"},{"group":9,"hexcode":"1f1ee-1f1f2","label":"flag: isle of man","tags":["im","flag"],"unicode":"🇮🇲"},{"group":9,"hexcode":"1f1ee-1f1f3","label":"flag: india","tags":["in","flag"],"unicode":"🇮🇳"},{"group":9,"hexcode":"1f1ee-1f1f4","label":"flag: british indian ocean territory","tags":["io","flag"],"unicode":"🇮🇴"},{"group":9,"hexcode":"1f1ee-1f1f6","label":"flag: iraq","tags":["iq","flag"],"unicode":"🇮🇶"},{"group":9,"hexcode":"1f1ee-1f1f7","label":"flag: iran","tags":["ir","flag"],"unicode":"🇮🇷"},{"group":9,"hexcode":"1f1ee-1f1f8","label":"flag: iceland","tags":["is","flag"],"unicode":"🇮🇸"},{"group":9,"hexcode":"1f1ee-1f1f9","label":"flag: italy","tags":["it","flag"],"unicode":"🇮🇹"},{"group":9,"hexcode":"1f1ef-1f1ea","label":"flag: jersey","tags":["je","flag"],"unicode":"🇯🇪"},{"group":9,"hexcode":"1f1ef-1f1f2","label":"flag: jamaica","tags":["jm","flag"],"unicode":"🇯🇲"},{"group":9,"hexcode":"1f1ef-1f1f4","label":"flag: jordan","tags":["jo","flag"],"unicode":"🇯🇴"},{"group":9,"hexcode":"1f1ef-1f1f5","label":"flag: japan","tags":["jp","flag"],"unicode":"🇯🇵"},{"group":9,"hexcode":"1f1f0-1f1ea","label":"flag: kenya","tags":["ke","flag"],"unicode":"🇰🇪"},{"group":9,"hexcode":"1f1f0-1f1ec","label":"flag: kyrgyzstan","tags":["kg","flag"],"unicode":"🇰🇬"},{"group":9,"hexcode":"1f1f0-1f1ed","label":"flag: cambodia","tags":["kh","flag"],"unicode":"🇰🇭"},{"group":9,"hexcode":"1f1f0-1f1ee","label":"flag: kiribati","tags":["ki","flag"],"unicode":"🇰🇮"},{"group":9,"hexcode":"1f1f0-1f1f2","label":"flag: comoros","tags":["km","flag"],"unicode":"🇰🇲"},{"group":9,"hexcode":"1f1f0-1f1f3","label":"flag: st. kitts & nevis","tags":["kn","flag"],"unicode":"🇰🇳"},{"group":9,"hexcode":"1f1f0-1f1f5","label":"flag: north korea","tags":["kp","flag"],"unicode":"🇰🇵"},{"group":9,"hexcode":"1f1f0-1f1f7","label":"flag: south korea","tags":["kr","flag"],"unicode":"🇰🇷"},{"group":9,"hexcode":"1f1f0-1f1fc","label":"flag: kuwait","tags":["kw","flag"],"unicode":"🇰🇼"},{"group":9,"hexcode":"1f1f0-1f1fe","label":"flag: cayman islands","tags":["ky","flag"],"unicode":"🇰🇾"},{"group":9,"hexcode":"1f1f0-1f1ff","label":"flag: kazakhstan","tags":["kz","flag"],"unicode":"🇰🇿"},{"group":9,"hexcode":"1f1f1-1f1e6","label":"flag: laos","tags":["la","flag"],"unicode":"🇱🇦"},{"group":9,"hexcode":"1f1f1-1f1e7","label":"flag: lebanon","tags":["lb","flag"],"unicode":"🇱🇧"},{"group":9,"hexcode":"1f1f1-1f1e8","label":"flag: st. lucia","tags":["lc","flag"],"unicode":"🇱🇨"},{"group":9,"hexcode":"1f1f1-1f1ee","label":"flag: liechtenstein","tags":["li","flag"],"unicode":"🇱🇮"},{"group":9,"hexcode":"1f1f1-1f1f0","label":"flag: sri lanka","tags":["lk","flag"],"unicode":"🇱🇰"},{"group":9,"hexcode":"1f1f1-1f1f7","label":"flag: liberia","tags":["lr","flag"],"unicode":"🇱🇷"},{"group":9,"hexcode":"1f1f1-1f1f8","label":"flag: lesotho","tags":["ls","flag"],"unicode":"🇱🇸"},{"group":9,"hexcode":"1f1f1-1f1f9","label":"flag: lithuania","tags":["lt","flag"],"unicode":"🇱🇹"},{"group":9,"hexcode":"1f1f1-1f1fa","label":"flag: luxembourg","tags":["lu","flag"],"unicode":"🇱🇺"},{"group":9,"hexcode":"1f1f1-1f1fb","label":"flag: latvia","tags":["lv","flag"],"unicode":"🇱🇻"},{"group":9,"hexcode":"1f1f1-1f1fe","label":"flag: libya","tags":["ly","flag"],"unicode":"🇱🇾"},{"group":9,"hexcode":"1f1f2-1f1e6","label":"flag: morocco","tags":["ma","flag"],"unicode":"🇲🇦"},{"group":9,"hexcode":"1f1f2-1f1e8","label":"flag: monaco","tags":["mc","flag"],"unicode":"🇲🇨"},{"group":9,"hexcode":"1f1f2-1f1e9","label":"flag: moldova","tags":["md","flag"],"unicode":"🇲🇩"},{"group":9,"hexcode":"1f1f2-1f1ea","label":"flag: montenegro","tags":["me","flag"],"unicode":"🇲🇪"},{"group":9,"hexcode":"1f1f2-1f1eb","label":"flag: st. martin","tags":["mf","flag"],"unicode":"🇲🇫"},{"group":9,"hexcode":"1f1f2-1f1ec","label":"flag: madagascar","tags":["mg","flag"],"unicode":"🇲🇬"},{"group":9,"hexcode":"1f1f2-1f1ed","label":"flag: marshall islands","tags":["mh","flag"],"unicode":"🇲🇭"},{"group":9,"hexcode":"1f1f2-1f1f0","label":"flag: north macedonia","tags":["mk","flag"],"unicode":"🇲🇰"},{"group":9,"hexcode":"1f1f2-1f1f1","label":"flag: mali","tags":["ml","flag"],"unicode":"🇲🇱"},{"group":9,"hexcode":"1f1f2-1f1f2","label":"flag: myanmar (burma)","tags":["mm","flag"],"unicode":"🇲🇲"},{"group":9,"hexcode":"1f1f2-1f1f3","label":"flag: mongolia","tags":["mn","flag"],"unicode":"🇲🇳"},{"group":9,"hexcode":"1f1f2-1f1f4","label":"flag: macao sar china","tags":["mo","flag"],"unicode":"🇲🇴"},{"group":9,"hexcode":"1f1f2-1f1f5","label":"flag: northern mariana islands","tags":["mp","flag"],"unicode":"🇲🇵"},{"group":9,"hexcode":"1f1f2-1f1f6","label":"flag: martinique","tags":["mq","flag"],"unicode":"🇲🇶"},{"group":9,"hexcode":"1f1f2-1f1f7","label":"flag: mauritania","tags":["mr","flag"],"unicode":"🇲🇷"},{"group":9,"hexcode":"1f1f2-1f1f8","label":"flag: montserrat","tags":["ms","flag"],"unicode":"🇲🇸"},{"group":9,"hexcode":"1f1f2-1f1f9","label":"flag: malta","tags":["mt","flag"],"unicode":"🇲🇹"},{"group":9,"hexcode":"1f1f2-1f1fa","label":"flag: mauritius","tags":["mu","flag"],"unicode":"🇲🇺"},{"group":9,"hexcode":"1f1f2-1f1fb","label":"flag: maldives","tags":["mv","flag"],"unicode":"🇲🇻"},{"group":9,"hexcode":"1f1f2-1f1fc","label":"flag: malawi","tags":["mw","flag"],"unicode":"🇲🇼"},{"group":9,"hexcode":"1f1f2-1f1fd","label":"flag: mexico","tags":["mx","flag"],"unicode":"🇲🇽"},{"group":9,"hexcode":"1f1f2-1f1fe","label":"flag: malaysia","tags":["my","flag"],"unicode":"🇲🇾"},{"group":9,"hexcode":"1f1f2-1f1ff","label":"flag: mozambique","tags":["mz","flag"],"unicode":"🇲🇿"},{"group":9,"hexcode":"1f1f3-1f1e6","label":"flag: namibia","tags":["na","flag"],"unicode":"🇳🇦"},{"group":9,"hexcode":"1f1f3-1f1e8","label":"flag: new caledonia","tags":["nc","flag"],"unicode":"🇳🇨"},{"group":9,"hexcode":"1f1f3-1f1ea","label":"flag: niger","tags":["ne","flag"],"unicode":"🇳🇪"},{"group":9,"hexcode":"1f1f3-1f1eb","label":"flag: norfolk island","tags":["nf","flag"],"unicode":"🇳🇫"},{"group":9,"hexcode":"1f1f3-1f1ec","label":"flag: nigeria","tags":["ng","flag"],"unicode":"🇳🇬"},{"group":9,"hexcode":"1f1f3-1f1ee","label":"flag: nicaragua","tags":["ni","flag"],"unicode":"🇳🇮"},{"group":9,"hexcode":"1f1f3-1f1f1","label":"flag: netherlands","tags":["nl","flag"],"unicode":"🇳🇱"},{"group":9,"hexcode":"1f1f3-1f1f4","label":"flag: norway","tags":["no","flag"],"unicode":"🇳🇴"},{"group":9,"hexcode":"1f1f3-1f1f5","label":"flag: nepal","tags":["np","flag"],"unicode":"🇳🇵"},{"group":9,"hexcode":"1f1f3-1f1f7","label":"flag: nauru","tags":["nr","flag"],"unicode":"🇳🇷"},{"group":9,"hexcode":"1f1f3-1f1fa","label":"flag: niue","tags":["nu","flag"],"unicode":"🇳🇺"},{"group":9,"hexcode":"1f1f3-1f1ff","label":"flag: new zealand","tags":["nz","flag"],"unicode":"🇳🇿"},{"group":9,"hexcode":"1f1f4-1f1f2","label":"flag: oman","tags":["om","flag"],"unicode":"🇴🇲"},{"group":9,"hexcode":"1f1f5-1f1e6","label":"flag: panama","tags":["pa","flag"],"unicode":"🇵🇦"},{"group":9,"hexcode":"1f1f5-1f1ea","label":"flag: peru","tags":["pe","flag"],"unicode":"🇵🇪"},{"group":9,"hexcode":"1f1f5-1f1eb","label":"flag: french polynesia","tags":["pf","flag"],"unicode":"🇵🇫"},{"group":9,"hexcode":"1f1f5-1f1ec","label":"flag: papua new guinea","tags":["pg","flag"],"unicode":"🇵🇬"},{"group":9,"hexcode":"1f1f5-1f1ed","label":"flag: philippines","tags":["ph","flag"],"unicode":"🇵🇭"},{"group":9,"hexcode":"1f1f5-1f1f0","label":"flag: pakistan","tags":["pk","flag"],"unicode":"🇵🇰"},{"group":9,"hexcode":"1f1f5-1f1f1","label":"flag: poland","tags":["pl","flag"],"unicode":"🇵🇱"},{"group":9,"hexcode":"1f1f5-1f1f2","label":"flag: st. pierre & miquelon","tags":["pm","flag"],"unicode":"🇵🇲"},{"group":9,"hexcode":"1f1f5-1f1f3","label":"flag: pitcairn islands","tags":["pn","flag"],"unicode":"🇵🇳"},{"group":9,"hexcode":"1f1f5-1f1f7","label":"flag: puerto rico","tags":["pr","flag"],"unicode":"🇵🇷"},{"group":9,"hexcode":"1f1f5-1f1f8","label":"flag: palestinian territories","tags":["ps","flag"],"unicode":"🇵🇸"},{"group":9,"hexcode":"1f1f5-1f1f9","label":"flag: portugal","tags":["pt","flag"],"unicode":"🇵🇹"},{"group":9,"hexcode":"1f1f5-1f1fc","label":"flag: palau","tags":["pw","flag"],"unicode":"🇵🇼"},{"group":9,"hexcode":"1f1f5-1f1fe","label":"flag: paraguay","tags":["py","flag"],"unicode":"🇵🇾"},{"group":9,"hexcode":"1f1f6-1f1e6","label":"flag: qatar","tags":["qa","flag"],"unicode":"🇶🇦"},{"group":9,"hexcode":"1f1f7-1f1ea","label":"flag: réunion","tags":["re","flag"],"unicode":"🇷🇪"},{"group":9,"hexcode":"1f1f7-1f1f4","label":"flag: romania","tags":["ro","flag"],"unicode":"🇷🇴"},{"group":9,"hexcode":"1f1f7-1f1f8","label":"flag: serbia","tags":["rs","flag"],"unicode":"🇷🇸"},{"group":9,"hexcode":"1f1f7-1f1fa","label":"flag: russia","tags":["ru","flag"],"unicode":"🇷🇺"},{"group":9,"hexcode":"1f1f7-1f1fc","label":"flag: rwanda","tags":["rw","flag"],"unicode":"🇷🇼"},{"group":9,"hexcode":"1f1f8-1f1e6","label":"flag: saudi arabia","tags":["sa","flag"],"unicode":"🇸🇦"},{"group":9,"hexcode":"1f1f8-1f1e7","label":"flag: solomon islands","tags":["sb","flag"],"unicode":"🇸🇧"},{"group":9,"hexcode":"1f1f8-1f1e8","label":"flag: seychelles","tags":["sc","flag"],"unicode":"🇸🇨"},{"group":9,"hexcode":"1f1f8-1f1e9","label":"flag: sudan","tags":["sd","flag"],"unicode":"🇸🇩"},{"group":9,"hexcode":"1f1f8-1f1ea","label":"flag: sweden","tags":["se","flag"],"unicode":"🇸🇪"},{"group":9,"hexcode":"1f1f8-1f1ec","label":"flag: singapore","tags":["sg","flag"],"unicode":"🇸🇬"},{"group":9,"hexcode":"1f1f8-1f1ed","label":"flag: st. helena","tags":["sh","flag"],"unicode":"🇸🇭"},{"group":9,"hexcode":"1f1f8-1f1ee","label":"flag: slovenia","tags":["si","flag"],"unicode":"🇸🇮"},{"group":9,"hexcode":"1f1f8-1f1ef","label":"flag: svalbard & jan mayen","tags":["sj","flag"],"unicode":"🇸🇯"},{"group":9,"hexcode":"1f1f8-1f1f0","label":"flag: slovakia","tags":["sk","flag"],"unicode":"🇸🇰"},{"group":9,"hexcode":"1f1f8-1f1f1","label":"flag: sierra leone","tags":["sl","flag"],"unicode":"🇸🇱"},{"group":9,"hexcode":"1f1f8-1f1f2","label":"flag: san marino","tags":["sm","flag"],"unicode":"🇸🇲"},{"group":9,"hexcode":"1f1f8-1f1f3","label":"flag: senegal","tags":["sn","flag"],"unicode":"🇸🇳"},{"group":9,"hexcode":"1f1f8-1f1f4","label":"flag: somalia","tags":["so","flag"],"unicode":"🇸🇴"},{"group":9,"hexcode":"1f1f8-1f1f7","label":"flag: suriname","tags":["sr","flag"],"unicode":"🇸🇷"},{"group":9,"hexcode":"1f1f8-1f1f8","label":"flag: south sudan","tags":["ss","flag"],"unicode":"🇸🇸"},{"group":9,"hexcode":"1f1f8-1f1f9","label":"flag: são tomé & príncipe","tags":["st","flag"],"unicode":"🇸🇹"},{"group":9,"hexcode":"1f1f8-1f1fb","label":"flag: el salvador","tags":["sv","flag"],"unicode":"🇸🇻"},{"group":9,"hexcode":"1f1f8-1f1fd","label":"flag: sint maarten","tags":["sx","flag"],"unicode":"🇸🇽"},{"group":9,"hexcode":"1f1f8-1f1fe","label":"flag: syria","tags":["sy","flag"],"unicode":"🇸🇾"},{"group":9,"hexcode":"1f1f8-1f1ff","label":"flag: eswatini","tags":["sz","flag"],"unicode":"🇸🇿"},{"group":9,"hexcode":"1f1f9-1f1e6","label":"flag: tristan da cunha","tags":["ta","flag"],"unicode":"🇹🇦"},{"group":9,"hexcode":"1f1f9-1f1e8","label":"flag: turks & caicos islands","tags":["tc","flag"],"unicode":"🇹🇨"},{"group":9,"hexcode":"1f1f9-1f1e9","label":"flag: chad","tags":["td","flag"],"unicode":"🇹🇩"},{"group":9,"hexcode":"1f1f9-1f1eb","label":"flag: french southern territories","tags":["tf","flag"],"unicode":"🇹🇫"},{"group":9,"hexcode":"1f1f9-1f1ec","label":"flag: togo","tags":["tg","flag"],"unicode":"🇹🇬"},{"group":9,"hexcode":"1f1f9-1f1ed","label":"flag: thailand","tags":["th","flag"],"unicode":"🇹🇭"},{"group":9,"hexcode":"1f1f9-1f1ef","label":"flag: tajikistan","tags":["tj","flag"],"unicode":"🇹🇯"},{"group":9,"hexcode":"1f1f9-1f1f0","label":"flag: tokelau","tags":["tk","flag"],"unicode":"🇹🇰"},{"group":9,"hexcode":"1f1f9-1f1f1","label":"flag: timor-leste","tags":["tl","flag"],"unicode":"🇹🇱"},{"group":9,"hexcode":"1f1f9-1f1f2","label":"flag: turkmenistan","tags":["tm","flag"],"unicode":"🇹🇲"},{"group":9,"hexcode":"1f1f9-1f1f3","label":"flag: tunisia","tags":["tn","flag"],"unicode":"🇹🇳"},{"group":9,"hexcode":"1f1f9-1f1f4","label":"flag: tonga","tags":["to","flag"],"unicode":"🇹🇴"},{"group":9,"hexcode":"1f1f9-1f1f7","label":"flag: turkey","tags":["tr","flag"],"unicode":"🇹🇷"},{"group":9,"hexcode":"1f1f9-1f1f9","label":"flag: trinidad & tobago","tags":["tt","flag"],"unicode":"🇹🇹"},{"group":9,"hexcode":"1f1f9-1f1fb","label":"flag: tuvalu","tags":["tv","flag"],"unicode":"🇹🇻"},{"group":9,"hexcode":"1f1f9-1f1fc","label":"flag: taiwan","tags":["tw","flag"],"unicode":"🇹🇼"},{"group":9,"hexcode":"1f1f9-1f1ff","label":"flag: tanzania","tags":["tz","flag"],"unicode":"🇹🇿"},{"group":9,"hexcode":"1f1fa-1f1e6","label":"flag: ukraine","tags":["ua","flag"],"unicode":"🇺🇦"},{"group":9,"hexcode":"1f1fa-1f1ec","label":"flag: uganda","tags":["ug","flag"],"unicode":"🇺🇬"},{"group":9,"hexcode":"1f1fa-1f1f2","label":"flag: u.s. outlying islands","tags":["um","flag"],"unicode":"🇺🇲"},{"group":9,"hexcode":"1f1fa-1f1f3","label":"flag: united nations","tags":["un","flag"],"unicode":"🇺🇳"},{"group":9,"hexcode":"1f1fa-1f1f8","label":"flag: united states","tags":["us","flag"],"unicode":"🇺🇸"},{"group":9,"hexcode":"1f1fa-1f1fe","label":"flag: uruguay","tags":["uy","flag"],"unicode":"🇺🇾"},{"group":9,"hexcode":"1f1fa-1f1ff","label":"flag: uzbekistan","tags":["uz","flag"],"unicode":"🇺🇿"},{"group":9,"hexcode":"1f1fb-1f1e6","label":"flag: vatican city","tags":["va","flag"],"unicode":"🇻🇦"},{"group":9,"hexcode":"1f1fb-1f1e8","label":"flag: st. vincent & grenadines","tags":["vc","flag"],"unicode":"🇻🇨"},{"group":9,"hexcode":"1f1fb-1f1ea","label":"flag: venezuela","tags":["ve","flag"],"unicode":"🇻🇪"},{"group":9,"hexcode":"1f1fb-1f1ec","label":"flag: british virgin islands","tags":["vg","flag"],"unicode":"🇻🇬"},{"group":9,"hexcode":"1f1fb-1f1ee","label":"flag: u.s. virgin islands","tags":["vi","flag"],"unicode":"🇻🇮"},{"group":9,"hexcode":"1f1fb-1f1f3","label":"flag: vietnam","tags":["vn","flag"],"unicode":"🇻🇳"},{"group":9,"hexcode":"1f1fb-1f1fa","label":"flag: vanuatu","tags":["vu","flag"],"unicode":"🇻🇺"},{"group":9,"hexcode":"1f1fc-1f1eb","label":"flag: wallis & futuna","tags":["wf","flag"],"unicode":"🇼🇫"},{"group":9,"hexcode":"1f1fc-1f1f8","label":"flag: samoa","tags":["ws","flag"],"unicode":"🇼🇸"},{"group":9,"hexcode":"1f1fd-1f1f0","label":"flag: kosovo","tags":["xk","flag"],"unicode":"🇽🇰"},{"group":9,"hexcode":"1f1fe-1f1ea","label":"flag: yemen","tags":["ye","flag"],"unicode":"🇾🇪"},{"group":9,"hexcode":"1f1fe-1f1f9","label":"flag: mayotte","tags":["yt","flag"],"unicode":"🇾🇹"},{"group":9,"hexcode":"1f1ff-1f1e6","label":"flag: south africa","tags":["za","flag"],"unicode":"🇿🇦"},{"group":9,"hexcode":"1f1ff-1f1f2","label":"flag: zambia","tags":["zm","flag"],"unicode":"🇿🇲"},{"group":9,"hexcode":"1f1ff-1f1fc","label":"flag: zimbabwe","tags":["zw","flag"],"unicode":"🇿🇼"},{"group":9,"hexcode":"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f","label":"flag: england","tags":["flag","gbeng"],"unicode":"🏴󠁧󠁢󠁥󠁮󠁧󠁿"},{"group":9,"hexcode":"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f","label":"flag: scotland","tags":["flag","gbsct"],"unicode":"🏴󠁧󠁢󠁳󠁣󠁴󠁿"},{"group":9,"hexcode":"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f","label":"flag: wales","tags":["flag","gbwls"],"unicode":"🏴󠁧󠁢󠁷󠁬󠁳󠁿"}] diff --git a/resources/default_contacts.json b/resources/default_contacts.json deleted file mode 100644 index 4393a28868c..00000000000 --- a/resources/default_contacts.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "transactor": - { - "name": - { - "en": "Transactor" - }, - "dapp?": true, - "hide-contact?": true, - "bot-url": "local://transactor-bot" - }, - "demo-bot": - { - "name": - { - "en": "Demo bot" - }, - "dapp?": true, - "bot-url": "local://demo-bot" - } -} diff --git a/resources/fonts/Inter-Bold.otf b/resources/fonts/Inter-Bold.otf new file mode 100644 index 00000000000..0dbf67b7c46 Binary files /dev/null and b/resources/fonts/Inter-Bold.otf differ diff --git a/resources/fonts/Inter-BoldItalic.otf b/resources/fonts/Inter-BoldItalic.otf new file mode 100644 index 00000000000..d9d46b67e84 Binary files /dev/null and b/resources/fonts/Inter-BoldItalic.otf differ diff --git a/resources/fonts/Inter-Italic.otf b/resources/fonts/Inter-Italic.otf new file mode 100644 index 00000000000..d041c41f042 Binary files /dev/null and b/resources/fonts/Inter-Italic.otf differ diff --git a/resources/fonts/Inter-Medium.otf b/resources/fonts/Inter-Medium.otf new file mode 100644 index 00000000000..1bcb0a93435 Binary files /dev/null and b/resources/fonts/Inter-Medium.otf differ diff --git a/resources/fonts/Inter-MediumItalic.otf b/resources/fonts/Inter-MediumItalic.otf new file mode 100644 index 00000000000..0274a218f44 Binary files /dev/null and b/resources/fonts/Inter-MediumItalic.otf differ diff --git a/resources/fonts/Inter-Regular.otf b/resources/fonts/Inter-Regular.otf new file mode 100644 index 00000000000..6724353f432 Binary files /dev/null and b/resources/fonts/Inter-Regular.otf differ diff --git a/resources/fonts/Inter-SemiBold.otf b/resources/fonts/Inter-SemiBold.otf new file mode 100644 index 00000000000..a6f9b2dc5a0 Binary files /dev/null and b/resources/fonts/Inter-SemiBold.otf differ diff --git a/resources/fonts/Inter-SemiBoldItalic.otf b/resources/fonts/Inter-SemiBoldItalic.otf new file mode 100644 index 00000000000..ac35ffc52a8 Binary files /dev/null and b/resources/fonts/Inter-SemiBoldItalic.otf differ diff --git a/resources/fonts/InterStatus-Regular.otf b/resources/fonts/InterStatus-Regular.otf new file mode 100644 index 00000000000..92c5dcb6e68 Binary files /dev/null and b/resources/fonts/InterStatus-Regular.otf differ diff --git a/resources/fonts/UbuntuMono-Regular.ttf b/resources/fonts/UbuntuMono-Regular.ttf new file mode 100644 index 00000000000..fdd309d7166 Binary files /dev/null and b/resources/fonts/UbuntuMono-Regular.ttf differ diff --git a/resources/icons/QR.svg b/resources/icons/QR.svg deleted file mode 100644 index 90999f3bb3a..00000000000 --- a/resources/icons/QR.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/add.svg b/resources/icons/add.svg deleted file mode 100644 index b575ddf7618..00000000000 --- a/resources/icons/add.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/add_contact.svg b/resources/icons/add_contact.svg deleted file mode 100644 index e334a58286f..00000000000 --- a/resources/icons/add_contact.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/icons/add_wallet.svg b/resources/icons/add_wallet.svg deleted file mode 100644 index e169f6933e1..00000000000 --- a/resources/icons/add_wallet.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/address.svg b/resources/icons/address.svg deleted file mode 100644 index 7c5d9a95be9..00000000000 --- a/resources/icons/address.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/arrow_left.svg b/resources/icons/arrow_left.svg deleted file mode 100644 index 809d3229499..00000000000 --- a/resources/icons/arrow_left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/arrow_right.svg b/resources/icons/arrow_right.svg deleted file mode 100644 index 04c94c00832..00000000000 --- a/resources/icons/arrow_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/attach.svg b/resources/icons/attach.svg deleted file mode 100644 index c9a9d87d839..00000000000 --- a/resources/icons/attach.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/back.svg b/resources/icons/back.svg deleted file mode 100644 index 163aa80a042..00000000000 --- a/resources/icons/back.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/bottom/chats_active.svg b/resources/icons/bottom/chats_active.svg deleted file mode 100644 index c9c992778e8..00000000000 --- a/resources/icons/bottom/chats_active.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/bottom/chats_gray.svg b/resources/icons/bottom/chats_gray.svg deleted file mode 100644 index 5df8c98c391..00000000000 --- a/resources/icons/bottom/chats_gray.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/bottom/contacts_active.svg b/resources/icons/bottom/contacts_active.svg deleted file mode 100644 index d3021a98f8c..00000000000 --- a/resources/icons/bottom/contacts_active.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/bottom/contacts_gray.svg b/resources/icons/bottom/contacts_gray.svg deleted file mode 100644 index d9a15a611dc..00000000000 --- a/resources/icons/bottom/contacts_gray.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/bottom/discover_active.svg b/resources/icons/bottom/discover_active.svg deleted file mode 100644 index b1a00592956..00000000000 --- a/resources/icons/bottom/discover_active.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/bottom/discover_gray.svg b/resources/icons/bottom/discover_gray.svg deleted file mode 100644 index 2c97c318678..00000000000 --- a/resources/icons/bottom/discover_gray.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/bottom/home_blue.svg b/resources/icons/bottom/home_blue.svg deleted file mode 100644 index c3dc3985ad4..00000000000 --- a/resources/icons/bottom/home_blue.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/resources/icons/bottom/home_gray.svg b/resources/icons/bottom/home_gray.svg deleted file mode 100644 index 49da5ae7f04..00000000000 --- a/resources/icons/bottom/home_gray.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/resources/icons/bottom/profile_blue.svg b/resources/icons/bottom/profile_blue.svg deleted file mode 100644 index c8d58c86af3..00000000000 --- a/resources/icons/bottom/profile_blue.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/bottom/profile_gray.svg b/resources/icons/bottom/profile_gray.svg deleted file mode 100644 index ed5bc01e96f..00000000000 --- a/resources/icons/bottom/profile_gray.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/bottom/wallet_active.svg b/resources/icons/bottom/wallet_active.svg deleted file mode 100644 index 65797b73da6..00000000000 --- a/resources/icons/bottom/wallet_active.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/bottom/wallet_gray.svg b/resources/icons/bottom/wallet_gray.svg deleted file mode 100644 index 06e9df51267..00000000000 --- a/resources/icons/bottom/wallet_gray.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/browse.svg b/resources/icons/browse.svg deleted file mode 100644 index ddc0c9793ad..00000000000 --- a/resources/icons/browse.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/camera.svg b/resources/icons/camera.svg deleted file mode 100644 index 8b13a024221..00000000000 --- a/resources/icons/camera.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/chats.svg b/resources/icons/chats.svg deleted file mode 100644 index 131296ad7e7..00000000000 --- a/resources/icons/chats.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/check.svg b/resources/icons/check.svg deleted file mode 100644 index 95a7a925ba4..00000000000 --- a/resources/icons/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/close.svg b/resources/icons/close.svg deleted file mode 100644 index f3a8a38beac..00000000000 --- a/resources/icons/close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/copy_from.svg b/resources/icons/copy_from.svg deleted file mode 100644 index 7f8e9f618e8..00000000000 --- a/resources/icons/copy_from.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/resources/icons/delete.svg b/resources/icons/delete.svg deleted file mode 100644 index bb1e5c5d142..00000000000 --- a/resources/icons/delete.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/dots.svg b/resources/icons/dots.svg deleted file mode 100644 index 5b245e6ad2b..00000000000 --- a/resources/icons/dots.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/resources/icons/dots_horizontal.svg b/resources/icons/dots_horizontal.svg deleted file mode 100644 index d7f90d5e9c5..00000000000 --- a/resources/icons/dots_horizontal.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/dots_vertical.svg b/resources/icons/dots_vertical.svg deleted file mode 100644 index eff62106e08..00000000000 --- a/resources/icons/dots_vertical.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/down.svg b/resources/icons/down.svg deleted file mode 100644 index 9ad7430a6de..00000000000 --- a/resources/icons/down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/icons/dropdown_up.svg b/resources/icons/dropdown_up.svg deleted file mode 100644 index 44432b5d9dc..00000000000 --- a/resources/icons/dropdown_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/exclamation_mark.svg b/resources/icons/exclamation_mark.svg deleted file mode 100644 index 47919eff224..00000000000 --- a/resources/icons/exclamation_mark.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/resources/icons/filter.svg b/resources/icons/filter.svg deleted file mode 100644 index e9ea2c45d7e..00000000000 --- a/resources/icons/filter.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/resources/icons/flash_active.svg b/resources/icons/flash_active.svg deleted file mode 100644 index f1efce88b05..00000000000 --- a/resources/icons/flash_active.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/icons/flash_inactive.svg b/resources/icons/flash_inactive.svg deleted file mode 100644 index 8bf77893e06..00000000000 --- a/resources/icons/flash_inactive.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/forward.svg b/resources/icons/forward.svg deleted file mode 100644 index 0ad3144ae49..00000000000 --- a/resources/icons/forward.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/fullscreen.svg b/resources/icons/fullscreen.svg deleted file mode 100644 index 6726e8607a9..00000000000 --- a/resources/icons/fullscreen.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/grab.svg b/resources/icons/grab.svg deleted file mode 100644 index 014533ba299..00000000000 --- a/resources/icons/grab.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/group_big.svg b/resources/icons/group_big.svg deleted file mode 100644 index e8c3ca6cb41..00000000000 --- a/resources/icons/group_big.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/group_chat.svg b/resources/icons/group_chat.svg deleted file mode 100644 index b794cd71aa9..00000000000 --- a/resources/icons/group_chat.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/hamburger.svg b/resources/icons/hamburger.svg deleted file mode 100644 index 2c8715e5176..00000000000 --- a/resources/icons/hamburger.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/hidden.svg b/resources/icons/hidden.svg deleted file mode 100644 index e666c9f0a5e..00000000000 --- a/resources/icons/hidden.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/resources/icons/in_contacts.svg b/resources/icons/in_contacts.svg deleted file mode 100644 index e4a1db25595..00000000000 --- a/resources/icons/in_contacts.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/icons/input_commands.svg b/resources/icons/input_commands.svg deleted file mode 100644 index 7188dbdfe8d..00000000000 --- a/resources/icons/input_commands.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/input_send.svg b/resources/icons/input_send.svg deleted file mode 100644 index 309b13351fe..00000000000 --- a/resources/icons/input_send.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/lock.svg b/resources/icons/lock.svg deleted file mode 100644 index 291cf61c6ed..00000000000 --- a/resources/icons/lock.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/lock_opened.svg b/resources/icons/lock_opened.svg deleted file mode 100644 index 43eb95435be..00000000000 --- a/resources/icons/lock_opened.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/icons/logo.svg b/resources/icons/logo.svg deleted file mode 100644 index aa3fcd19864..00000000000 --- a/resources/icons/logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/resources/icons/mic.svg b/resources/icons/mic.svg deleted file mode 100644 index a190cce21b6..00000000000 --- a/resources/icons/mic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/network.svg b/resources/icons/network.svg deleted file mode 100644 index 78a2d008322..00000000000 --- a/resources/icons/network.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/newchat.svg b/resources/icons/newchat.svg deleted file mode 100644 index 296b02065ff..00000000000 --- a/resources/icons/newchat.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/icons/ok.svg b/resources/icons/ok.svg deleted file mode 100644 index 4bde42159e5..00000000000 --- a/resources/icons/ok.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/open.svg b/resources/icons/open.svg deleted file mode 100644 index 1c8534f51df..00000000000 --- a/resources/icons/open.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/resources/icons/public.svg b/resources/icons/public.svg deleted file mode 100644 index 83c65dc28a4..00000000000 --- a/resources/icons/public.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/public_chat.svg b/resources/icons/public_chat.svg deleted file mode 100644 index 21ea6184440..00000000000 --- a/resources/icons/public_chat.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/refresh.svg b/resources/icons/refresh.svg deleted file mode 100644 index 7539be9a0a0..00000000000 --- a/resources/icons/refresh.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/share.svg b/resources/icons/share.svg deleted file mode 100644 index cc7819f6808..00000000000 --- a/resources/icons/share.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/resources/icons/speaker.svg b/resources/icons/speaker.svg deleted file mode 100644 index 54072264fa7..00000000000 --- a/resources/icons/speaker.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/speaker_off.svg b/resources/icons/speaker_off.svg deleted file mode 100644 index 16dd2e405fc..00000000000 --- a/resources/icons/speaker_off.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/tooltip-triangle.svg b/resources/icons/tooltip-triangle.svg deleted file mode 100644 index f19a119bec1..00000000000 --- a/resources/icons/tooltip-triangle.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/icons/transaction_history.svg b/resources/icons/transaction_history.svg deleted file mode 100644 index 8c703ec72b4..00000000000 --- a/resources/icons/transaction_history.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/resources/icons/up.svg b/resources/icons/up.svg deleted file mode 100644 index 90b2386d7cc..00000000000 --- a/resources/icons/up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/icons/warning.svg b/resources/icons/warning.svg deleted file mode 100644 index 027f1708344..00000000000 --- a/resources/icons/warning.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/wnode.svg b/resources/icons/wnode.svg deleted file mode 100644 index 77315a07ee4..00000000000 --- a/resources/icons/wnode.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/images/assets/ethereum.png b/resources/images/assets/ethereum.png deleted file mode 100644 index 1a6146c2ec6..00000000000 Binary files a/resources/images/assets/ethereum.png and /dev/null differ diff --git a/resources/images/contacts/Dentacoin.png b/resources/images/contacts/Dentacoin.png deleted file mode 100644 index cbd3f981e6d..00000000000 Binary files a/resources/images/contacts/Dentacoin.png and /dev/null differ diff --git a/resources/images/contacts/airswap.png b/resources/images/contacts/airswap.png deleted file mode 100644 index 9f9bb508bb6..00000000000 Binary files a/resources/images/contacts/airswap.png and /dev/null differ diff --git a/resources/images/contacts/augur.png b/resources/images/contacts/augur.png deleted file mode 100644 index 48ebc348145..00000000000 Binary files a/resources/images/contacts/augur.png and /dev/null differ diff --git a/resources/images/contacts/bancor.png b/resources/images/contacts/bancor.png deleted file mode 100644 index ffdb1ae9351..00000000000 Binary files a/resources/images/contacts/bancor.png and /dev/null differ diff --git a/resources/images/contacts/bchat.png b/resources/images/contacts/bchat.png deleted file mode 100644 index 80984deca58..00000000000 Binary files a/resources/images/contacts/bchat.png and /dev/null differ diff --git a/resources/images/contacts/bounties-network.png b/resources/images/contacts/bounties-network.png deleted file mode 100755 index c202f826f7e..00000000000 Binary files a/resources/images/contacts/bounties-network.png and /dev/null differ diff --git a/resources/images/contacts/cent.png b/resources/images/contacts/cent.png deleted file mode 100755 index 66e5f26e2f2..00000000000 Binary files a/resources/images/contacts/cent.png and /dev/null differ diff --git a/resources/images/contacts/civitas.png b/resources/images/contacts/civitas.png deleted file mode 100644 index 21f78045515..00000000000 Binary files a/resources/images/contacts/civitas.png and /dev/null differ diff --git a/resources/images/contacts/commiteth.png b/resources/images/contacts/commiteth.png deleted file mode 100644 index 7049c0a0453..00000000000 Binary files a/resources/images/contacts/commiteth.png and /dev/null differ diff --git a/resources/images/contacts/console.png b/resources/images/contacts/console.png deleted file mode 100644 index cadb7ea2cb3..00000000000 Binary files a/resources/images/contacts/console.png and /dev/null differ diff --git a/resources/images/contacts/cryptocribs.png b/resources/images/contacts/cryptocribs.png deleted file mode 100644 index 179fff8eb43..00000000000 Binary files a/resources/images/contacts/cryptocribs.png and /dev/null differ diff --git a/resources/images/contacts/cryptofighters.png b/resources/images/contacts/cryptofighters.png deleted file mode 100755 index 994b42a376a..00000000000 Binary files a/resources/images/contacts/cryptofighters.png and /dev/null differ diff --git a/resources/images/contacts/cryptokitties.png b/resources/images/contacts/cryptokitties.png deleted file mode 100644 index 9d8d8064b0f..00000000000 Binary files a/resources/images/contacts/cryptokitties.png and /dev/null differ diff --git a/resources/images/contacts/cryptopunks.png b/resources/images/contacts/cryptopunks.png deleted file mode 100644 index c14200b79e3..00000000000 Binary files a/resources/images/contacts/cryptopunks.png and /dev/null differ diff --git a/resources/images/contacts/cryptopurr.png b/resources/images/contacts/cryptopurr.png deleted file mode 100755 index b0390bb5408..00000000000 Binary files a/resources/images/contacts/cryptopurr.png and /dev/null differ diff --git a/resources/images/contacts/cryptostrikers.png b/resources/images/contacts/cryptostrikers.png deleted file mode 100644 index 8a1bae4507a..00000000000 Binary files a/resources/images/contacts/cryptostrikers.png and /dev/null differ diff --git a/resources/images/contacts/dai.png b/resources/images/contacts/dai.png deleted file mode 100755 index 50554ff309d..00000000000 Binary files a/resources/images/contacts/dai.png and /dev/null differ diff --git a/resources/images/contacts/ddex.png b/resources/images/contacts/ddex.png deleted file mode 100644 index 648b32b4870..00000000000 Binary files a/resources/images/contacts/ddex.png and /dev/null differ diff --git a/resources/images/contacts/easytrade.png b/resources/images/contacts/easytrade.png deleted file mode 100644 index ecf3e967d59..00000000000 Binary files a/resources/images/contacts/easytrade.png and /dev/null differ diff --git a/resources/images/contacts/emoon.png b/resources/images/contacts/emoon.png deleted file mode 100644 index f7a32a1093b..00000000000 Binary files a/resources/images/contacts/emoon.png and /dev/null differ diff --git a/resources/images/contacts/erc-dex.png b/resources/images/contacts/erc-dex.png deleted file mode 100755 index 46cc40d2590..00000000000 Binary files a/resources/images/contacts/erc-dex.png and /dev/null differ diff --git a/resources/images/contacts/eth2phone.png b/resources/images/contacts/eth2phone.png deleted file mode 100755 index 84342ff29e4..00000000000 Binary files a/resources/images/contacts/eth2phone.png and /dev/null differ diff --git a/resources/images/contacts/ethcro.png b/resources/images/contacts/ethcro.png deleted file mode 100644 index 8e71e742251..00000000000 Binary files a/resources/images/contacts/ethcro.png and /dev/null differ diff --git a/resources/images/contacts/etherbots.png b/resources/images/contacts/etherbots.png deleted file mode 100755 index abea443682d..00000000000 Binary files a/resources/images/contacts/etherbots.png and /dev/null differ diff --git a/resources/images/contacts/etheremon.png b/resources/images/contacts/etheremon.png deleted file mode 100644 index edfd718aa78..00000000000 Binary files a/resources/images/contacts/etheremon.png and /dev/null differ diff --git a/resources/images/contacts/etherplay.png b/resources/images/contacts/etherplay.png deleted file mode 100644 index aecdbc8b0b9..00000000000 Binary files a/resources/images/contacts/etherplay.png and /dev/null differ diff --git a/resources/images/contacts/ethlance.png b/resources/images/contacts/ethlance.png deleted file mode 100755 index 0cbca22eec7..00000000000 Binary files a/resources/images/contacts/ethlance.png and /dev/null differ diff --git a/resources/images/contacts/ethlend.png b/resources/images/contacts/ethlend.png deleted file mode 100755 index ff18af592f8..00000000000 Binary files a/resources/images/contacts/ethlend.png and /dev/null differ diff --git a/resources/images/contacts/gnosis.png b/resources/images/contacts/gnosis.png deleted file mode 100644 index 163196f5df3..00000000000 Binary files a/resources/images/contacts/gnosis.png and /dev/null differ diff --git a/resources/images/contacts/hexel.png b/resources/images/contacts/hexel.png deleted file mode 100755 index 0b745de7d32..00000000000 Binary files a/resources/images/contacts/hexel.png and /dev/null differ diff --git a/resources/images/contacts/kyber.png b/resources/images/contacts/kyber.png deleted file mode 100755 index 4e26cc86bb0..00000000000 Binary files a/resources/images/contacts/kyber.png and /dev/null differ diff --git a/resources/images/contacts/livepeer.png b/resources/images/contacts/livepeer.png deleted file mode 100644 index 7cf045495f7..00000000000 Binary files a/resources/images/contacts/livepeer.png and /dev/null differ diff --git a/resources/images/contacts/local-ethereum.png b/resources/images/contacts/local-ethereum.png deleted file mode 100755 index 25b24da574f..00000000000 Binary files a/resources/images/contacts/local-ethereum.png and /dev/null differ diff --git a/resources/images/contacts/melonport.png b/resources/images/contacts/melonport.png deleted file mode 100644 index 705e67ac943..00000000000 Binary files a/resources/images/contacts/melonport.png and /dev/null differ diff --git a/resources/images/contacts/mkr-market.png b/resources/images/contacts/mkr-market.png deleted file mode 100644 index 16e4f780711..00000000000 Binary files a/resources/images/contacts/mkr-market.png and /dev/null differ diff --git a/resources/images/contacts/name-bazaar.png b/resources/images/contacts/name-bazaar.png deleted file mode 100755 index b9f2451c878..00000000000 Binary files a/resources/images/contacts/name-bazaar.png and /dev/null differ diff --git a/resources/images/contacts/oaken-water-meter.png b/resources/images/contacts/oaken-water-meter.png deleted file mode 100644 index 2377175391f..00000000000 Binary files a/resources/images/contacts/oaken-water-meter.png and /dev/null differ diff --git a/resources/images/contacts/oasis-direct.png b/resources/images/contacts/oasis-direct.png deleted file mode 100755 index 707664cf5c9..00000000000 Binary files a/resources/images/contacts/oasis-direct.png and /dev/null differ diff --git a/resources/images/contacts/opensea.png b/resources/images/contacts/opensea.png deleted file mode 100755 index 754da2e8266..00000000000 Binary files a/resources/images/contacts/opensea.png and /dev/null differ diff --git a/resources/images/contacts/peepeth.png b/resources/images/contacts/peepeth.png deleted file mode 100755 index 51031d78998..00000000000 Binary files a/resources/images/contacts/peepeth.png and /dev/null differ diff --git a/resources/images/contacts/smartz.png b/resources/images/contacts/smartz.png deleted file mode 100755 index de677b67691..00000000000 Binary files a/resources/images/contacts/smartz.png and /dev/null differ diff --git a/resources/images/dapps/1inch@2x.png b/resources/images/dapps/1inch@2x.png new file mode 100644 index 00000000000..efbc4373e1a Binary files /dev/null and b/resources/images/dapps/1inch@2x.png differ diff --git a/resources/images/dapps/1inch@3x.png b/resources/images/dapps/1inch@3x.png new file mode 100644 index 00000000000..87fcc9104b2 Binary files /dev/null and b/resources/images/dapps/1inch@3x.png differ diff --git a/resources/images/dapps/Aave@2x.png b/resources/images/dapps/Aave@2x.png new file mode 100644 index 00000000000..c3f4319691b Binary files /dev/null and b/resources/images/dapps/Aave@2x.png differ diff --git a/resources/images/dapps/Aave@3x.png b/resources/images/dapps/Aave@3x.png new file mode 100644 index 00000000000..3f7bf192d7f Binary files /dev/null and b/resources/images/dapps/Aave@3x.png differ diff --git a/resources/images/dapps/CoinGecko@2x.png b/resources/images/dapps/CoinGecko@2x.png new file mode 100644 index 00000000000..3641cabaffc Binary files /dev/null and b/resources/images/dapps/CoinGecko@2x.png differ diff --git a/resources/images/dapps/CoinGecko@3x.png b/resources/images/dapps/CoinGecko@3x.png new file mode 100644 index 00000000000..10ece11a165 Binary files /dev/null and b/resources/images/dapps/CoinGecko@3x.png differ diff --git a/resources/images/dapps/Uniswap@2x.png b/resources/images/dapps/Uniswap@2x.png new file mode 100644 index 00000000000..8b7ee85a026 Binary files /dev/null and b/resources/images/dapps/Uniswap@2x.png differ diff --git a/resources/images/dapps/Uniswap@3x.png b/resources/images/dapps/Uniswap@3x.png new file mode 100644 index 00000000000..079e34d9392 Binary files /dev/null and b/resources/images/dapps/Uniswap@3x.png differ diff --git a/resources/images/dapps/WalletConnect@2x.png b/resources/images/dapps/WalletConnect@2x.png new file mode 100644 index 00000000000..6ae713eb62d Binary files /dev/null and b/resources/images/dapps/WalletConnect@2x.png differ diff --git a/resources/images/dapps/WalletConnect@3x.png b/resources/images/dapps/WalletConnect@3x.png new file mode 100644 index 00000000000..c69ecf6f2db Binary files /dev/null and b/resources/images/dapps/WalletConnect@3x.png differ diff --git a/resources/images/dapps/Zapper@2x.png b/resources/images/dapps/Zapper@2x.png new file mode 100644 index 00000000000..1d99e58da4a Binary files /dev/null and b/resources/images/dapps/Zapper@2x.png differ diff --git a/resources/images/dapps/Zapper@3x.png b/resources/images/dapps/Zapper@3x.png new file mode 100644 index 00000000000..bf7208d32ab Binary files /dev/null and b/resources/images/dapps/Zapper@3x.png differ diff --git a/resources/images/dapps/Zerion@2x.png b/resources/images/dapps/Zerion@2x.png new file mode 100644 index 00000000000..1a1080d482a Binary files /dev/null and b/resources/images/dapps/Zerion@2x.png differ diff --git a/resources/images/dapps/Zerion@3x.png b/resources/images/dapps/Zerion@3x.png new file mode 100644 index 00000000000..3fc4aa5d1d0 Binary files /dev/null and b/resources/images/dapps/Zerion@3x.png differ diff --git a/resources/images/icons/account@2x.png b/resources/images/icons/account@2x.png new file mode 100644 index 00000000000..094fd24490d Binary files /dev/null and b/resources/images/icons/account@2x.png differ diff --git a/resources/images/icons/account@3x.png b/resources/images/icons/account@3x.png new file mode 100644 index 00000000000..4bb3c37d22f Binary files /dev/null and b/resources/images/icons/account@3x.png differ diff --git a/resources/images/icons/activity@2x.png b/resources/images/icons/activity@2x.png new file mode 100644 index 00000000000..3e972cb5800 Binary files /dev/null and b/resources/images/icons/activity@2x.png differ diff --git a/resources/images/icons/activity@3x.png b/resources/images/icons/activity@3x.png new file mode 100644 index 00000000000..628aac87fd7 Binary files /dev/null and b/resources/images/icons/activity@3x.png differ diff --git a/resources/images/icons/add@2x.png b/resources/images/icons/add@2x.png new file mode 100755 index 00000000000..6132d9a03d0 Binary files /dev/null and b/resources/images/icons/add@2x.png differ diff --git a/resources/images/icons/add@3x.png b/resources/images/icons/add@3x.png new file mode 100755 index 00000000000..9bd9913f905 Binary files /dev/null and b/resources/images/icons/add@3x.png differ diff --git a/resources/images/icons/add_circle@2x.png b/resources/images/icons/add_circle@2x.png new file mode 100755 index 00000000000..797c0132030 Binary files /dev/null and b/resources/images/icons/add_circle@2x.png differ diff --git a/resources/images/icons/add_circle@3x.png b/resources/images/icons/add_circle@3x.png new file mode 100755 index 00000000000..b775d95e78c Binary files /dev/null and b/resources/images/icons/add_circle@3x.png differ diff --git a/resources/images/icons/add_contact@2x.png b/resources/images/icons/add_contact@2x.png new file mode 100755 index 00000000000..cdbbeab5830 Binary files /dev/null and b/resources/images/icons/add_contact@2x.png differ diff --git a/resources/images/icons/add_contact@3x.png b/resources/images/icons/add_contact@3x.png new file mode 100755 index 00000000000..878b6d19238 Binary files /dev/null and b/resources/images/icons/add_contact@3x.png differ diff --git a/resources/images/icons/address@2x.png b/resources/images/icons/address@2x.png new file mode 100755 index 00000000000..11f3ef943f8 Binary files /dev/null and b/resources/images/icons/address@2x.png differ diff --git a/resources/images/icons/address@3x.png b/resources/images/icons/address@3x.png new file mode 100755 index 00000000000..9000eae9dfb Binary files /dev/null and b/resources/images/icons/address@3x.png differ diff --git a/resources/images/icons/animals-nature@2x.png b/resources/images/icons/animals-nature@2x.png new file mode 100644 index 00000000000..162182d359f Binary files /dev/null and b/resources/images/icons/animals-nature@2x.png differ diff --git a/resources/images/icons/animals-nature@3x.png b/resources/images/icons/animals-nature@3x.png new file mode 100644 index 00000000000..760fbb6d543 Binary files /dev/null and b/resources/images/icons/animals-nature@3x.png differ diff --git a/resources/images/icons/appearance@2x.png b/resources/images/icons/appearance@2x.png new file mode 100755 index 00000000000..071d38497cd Binary files /dev/null and b/resources/images/icons/appearance@2x.png differ diff --git a/resources/images/icons/appearance@3x.png b/resources/images/icons/appearance@3x.png new file mode 100755 index 00000000000..7c1cb325b61 Binary files /dev/null and b/resources/images/icons/appearance@3x.png differ diff --git a/resources/images/icons/arrow_down@2x.png b/resources/images/icons/arrow_down@2x.png new file mode 100755 index 00000000000..45f2d33d56b Binary files /dev/null and b/resources/images/icons/arrow_down@2x.png differ diff --git a/resources/images/icons/arrow_down@3x.png b/resources/images/icons/arrow_down@3x.png new file mode 100755 index 00000000000..28e7f10b881 Binary files /dev/null and b/resources/images/icons/arrow_down@3x.png differ diff --git a/resources/images/icons/arrow_left@2x.png b/resources/images/icons/arrow_left@2x.png new file mode 100755 index 00000000000..d4e57d91603 Binary files /dev/null and b/resources/images/icons/arrow_left@2x.png differ diff --git a/resources/images/icons/arrow_left@3x.png b/resources/images/icons/arrow_left@3x.png new file mode 100755 index 00000000000..78f54edc5bd Binary files /dev/null and b/resources/images/icons/arrow_left@3x.png differ diff --git a/resources/images/icons/arrow_right@2x.png b/resources/images/icons/arrow_right@2x.png new file mode 100755 index 00000000000..84408bd96d0 Binary files /dev/null and b/resources/images/icons/arrow_right@2x.png differ diff --git a/resources/images/icons/arrow_right@3x.png b/resources/images/icons/arrow_right@3x.png new file mode 100755 index 00000000000..29674185156 Binary files /dev/null and b/resources/images/icons/arrow_right@3x.png differ diff --git a/resources/images/icons/arrow_up@2x.png b/resources/images/icons/arrow_up@2x.png new file mode 100755 index 00000000000..9708380cdce Binary files /dev/null and b/resources/images/icons/arrow_up@2x.png differ diff --git a/resources/images/icons/arrow_up@3x.png b/resources/images/icons/arrow_up@3x.png new file mode 100755 index 00000000000..3a087475964 Binary files /dev/null and b/resources/images/icons/arrow_up@3x.png differ diff --git a/resources/images/icons/back@2x.png b/resources/images/icons/back@2x.png new file mode 100755 index 00000000000..ff9917a6b37 Binary files /dev/null and b/resources/images/icons/back@2x.png differ diff --git a/resources/images/icons/back@3x.png b/resources/images/icons/back@3x.png new file mode 100755 index 00000000000..2c8617aa4d5 Binary files /dev/null and b/resources/images/icons/back@3x.png differ diff --git a/resources/images/icons/backspace@2x.png b/resources/images/icons/backspace@2x.png new file mode 100755 index 00000000000..023be94cfc1 Binary files /dev/null and b/resources/images/icons/backspace@2x.png differ diff --git a/resources/images/icons/backspace@3x.png b/resources/images/icons/backspace@3x.png new file mode 100755 index 00000000000..7cfd525c21b Binary files /dev/null and b/resources/images/icons/backspace@3x.png differ diff --git a/resources/images/icons/billfold@2x.png b/resources/images/icons/billfold@2x.png new file mode 100644 index 00000000000..96c63ac7150 Binary files /dev/null and b/resources/images/icons/billfold@2x.png differ diff --git a/resources/images/icons/billfold@3x.png b/resources/images/icons/billfold@3x.png new file mode 100644 index 00000000000..5d3bf8198e5 Binary files /dev/null and b/resources/images/icons/billfold@3x.png differ diff --git a/resources/images/icons/browser@2x.png b/resources/images/icons/browser@2x.png new file mode 100755 index 00000000000..8d8df2d08fb Binary files /dev/null and b/resources/images/icons/browser@2x.png differ diff --git a/resources/images/icons/browser@3x.png b/resources/images/icons/browser@3x.png new file mode 100755 index 00000000000..b47e7b6f0f1 Binary files /dev/null and b/resources/images/icons/browser@3x.png differ diff --git a/resources/images/icons/browser_context20@2x.png b/resources/images/icons/browser_context20@2x.png new file mode 100644 index 00000000000..e49ae0decce Binary files /dev/null and b/resources/images/icons/browser_context20@2x.png differ diff --git a/resources/images/icons/browser_context20@3x.png b/resources/images/icons/browser_context20@3x.png new file mode 100644 index 00000000000..4e1685848f0 Binary files /dev/null and b/resources/images/icons/browser_context20@3x.png differ diff --git a/resources/images/icons/buy_crypto@2x.png b/resources/images/icons/buy_crypto@2x.png new file mode 100644 index 00000000000..440b24bd605 Binary files /dev/null and b/resources/images/icons/buy_crypto@2x.png differ diff --git a/resources/images/icons/buy_crypto@3x.png b/resources/images/icons/buy_crypto@3x.png new file mode 100644 index 00000000000..9e5748bf95a Binary files /dev/null and b/resources/images/icons/buy_crypto@3x.png differ diff --git a/resources/images/icons/camera@2x.png b/resources/images/icons/camera@2x.png new file mode 100755 index 00000000000..0dec1be5180 Binary files /dev/null and b/resources/images/icons/camera@2x.png differ diff --git a/resources/images/icons/camera@3x.png b/resources/images/icons/camera@3x.png new file mode 100755 index 00000000000..d9856a49e2f Binary files /dev/null and b/resources/images/icons/camera@3x.png differ diff --git a/resources/images/icons/cancel@2x.png b/resources/images/icons/cancel@2x.png new file mode 100755 index 00000000000..dc71dd004ec Binary files /dev/null and b/resources/images/icons/cancel@2x.png differ diff --git a/resources/images/icons/cancel@3x.png b/resources/images/icons/cancel@3x.png new file mode 100755 index 00000000000..097c1b0579f Binary files /dev/null and b/resources/images/icons/cancel@3x.png differ diff --git a/resources/images/icons/change@2x.png b/resources/images/icons/change@2x.png new file mode 100755 index 00000000000..7f22effe0b5 Binary files /dev/null and b/resources/images/icons/change@2x.png differ diff --git a/resources/images/icons/change@3x.png b/resources/images/icons/change@3x.png new file mode 100755 index 00000000000..4383b2aab2c Binary files /dev/null and b/resources/images/icons/change@3x.png differ diff --git a/resources/images/icons/channel@2x.png b/resources/images/icons/channel@2x.png new file mode 100644 index 00000000000..4dd50c8b849 Binary files /dev/null and b/resources/images/icons/channel@2x.png differ diff --git a/resources/images/icons/channel@3x.png b/resources/images/icons/channel@3x.png new file mode 100644 index 00000000000..f14a31cddae Binary files /dev/null and b/resources/images/icons/channel@3x.png differ diff --git a/resources/images/icons/channel_category@2x.png b/resources/images/icons/channel_category@2x.png new file mode 100644 index 00000000000..cfbf85c2061 Binary files /dev/null and b/resources/images/icons/channel_category@2x.png differ diff --git a/resources/images/icons/channel_category@3x.png b/resources/images/icons/channel_category@3x.png new file mode 100644 index 00000000000..bf743066882 Binary files /dev/null and b/resources/images/icons/channel_category@3x.png differ diff --git a/resources/images/icons/check@2x.png b/resources/images/icons/check@2x.png new file mode 100755 index 00000000000..32035bb325f Binary files /dev/null and b/resources/images/icons/check@2x.png differ diff --git a/resources/images/icons/check@3x.png b/resources/images/icons/check@3x.png new file mode 100755 index 00000000000..98e28546a7d Binary files /dev/null and b/resources/images/icons/check@3x.png differ diff --git a/resources/images/icons/checkmark@2x.png b/resources/images/icons/checkmark@2x.png new file mode 100644 index 00000000000..e5fffc5a0a5 Binary files /dev/null and b/resources/images/icons/checkmark@2x.png differ diff --git a/resources/images/icons/checkmark@3x.png b/resources/images/icons/checkmark@3x.png new file mode 100644 index 00000000000..43c9cdb35e7 Binary files /dev/null and b/resources/images/icons/checkmark@3x.png differ diff --git a/resources/images/icons/checkmark_circle@2x.png b/resources/images/icons/checkmark_circle@2x.png new file mode 100755 index 00000000000..935b95d8009 Binary files /dev/null and b/resources/images/icons/checkmark_circle@2x.png differ diff --git a/resources/images/icons/checkmark_circle@3x.png b/resources/images/icons/checkmark_circle@3x.png new file mode 100755 index 00000000000..53b709363d2 Binary files /dev/null and b/resources/images/icons/checkmark_circle@3x.png differ diff --git a/resources/images/icons/chevron_down@2x.png b/resources/images/icons/chevron_down@2x.png new file mode 100644 index 00000000000..2eb293bf3e9 Binary files /dev/null and b/resources/images/icons/chevron_down@2x.png differ diff --git a/resources/images/icons/chevron_down@3x.png b/resources/images/icons/chevron_down@3x.png new file mode 100644 index 00000000000..1c3680098fe Binary files /dev/null and b/resources/images/icons/chevron_down@3x.png differ diff --git a/resources/images/icons/chevron_right@2x.png b/resources/images/icons/chevron_right@2x.png new file mode 100644 index 00000000000..0fc2c2eddc6 Binary files /dev/null and b/resources/images/icons/chevron_right@2x.png differ diff --git a/resources/images/icons/chevron_right@3x.png b/resources/images/icons/chevron_right@3x.png new file mode 100644 index 00000000000..edc9820def3 Binary files /dev/null and b/resources/images/icons/chevron_right@3x.png differ diff --git a/resources/images/icons/close@2x.png b/resources/images/icons/close@2x.png new file mode 100755 index 00000000000..021be71d03f Binary files /dev/null and b/resources/images/icons/close@2x.png differ diff --git a/resources/images/icons/close@3x.png b/resources/images/icons/close@3x.png new file mode 100755 index 00000000000..37f8ce87538 Binary files /dev/null and b/resources/images/icons/close@3x.png differ diff --git a/resources/images/icons/close_circle@2x.png b/resources/images/icons/close_circle@2x.png new file mode 100755 index 00000000000..32f2ef17e85 Binary files /dev/null and b/resources/images/icons/close_circle@2x.png differ diff --git a/resources/images/icons/close_circle@3x.png b/resources/images/icons/close_circle@3x.png new file mode 100755 index 00000000000..bd250260e94 Binary files /dev/null and b/resources/images/icons/close_circle@3x.png differ diff --git a/resources/images/icons/commands@2x.png b/resources/images/icons/commands@2x.png new file mode 100755 index 00000000000..819a2c10456 Binary files /dev/null and b/resources/images/icons/commands@2x.png differ diff --git a/resources/images/icons/commands@3x.png b/resources/images/icons/commands@3x.png new file mode 100755 index 00000000000..a67a046324c Binary files /dev/null and b/resources/images/icons/commands@3x.png differ diff --git a/resources/images/icons/communities@2x.png b/resources/images/icons/communities@2x.png new file mode 100644 index 00000000000..d4d9582d620 Binary files /dev/null and b/resources/images/icons/communities@2x.png differ diff --git a/resources/images/icons/communities@3x.png b/resources/images/icons/communities@3x.png new file mode 100644 index 00000000000..7187477a167 Binary files /dev/null and b/resources/images/icons/communities@3x.png differ diff --git a/resources/images/icons/connector@2x.png b/resources/images/icons/connector@2x.png new file mode 100644 index 00000000000..4690c4949eb Binary files /dev/null and b/resources/images/icons/connector@2x.png differ diff --git a/resources/images/icons/connector@3x.png b/resources/images/icons/connector@3x.png new file mode 100644 index 00000000000..82bffcb93e9 Binary files /dev/null and b/resources/images/icons/connector@3x.png differ diff --git a/resources/images/icons/copy@2x.png b/resources/images/icons/copy@2x.png new file mode 100755 index 00000000000..187e2d759e4 Binary files /dev/null and b/resources/images/icons/copy@2x.png differ diff --git a/resources/images/icons/copy@3x.png b/resources/images/icons/copy@3x.png new file mode 100755 index 00000000000..c94257970ff Binary files /dev/null and b/resources/images/icons/copy@3x.png differ diff --git a/resources/images/icons/dapp@2x.png b/resources/images/icons/dapp@2x.png new file mode 100755 index 00000000000..ba6360de4d9 Binary files /dev/null and b/resources/images/icons/dapp@2x.png differ diff --git a/resources/images/icons/dapp@3x.png b/resources/images/icons/dapp@3x.png new file mode 100755 index 00000000000..7856efbf734 Binary files /dev/null and b/resources/images/icons/dapp@3x.png differ diff --git a/resources/images/icons/delete16@2x.png b/resources/images/icons/delete16@2x.png new file mode 100644 index 00000000000..45b9ad9b0bb Binary files /dev/null and b/resources/images/icons/delete16@2x.png differ diff --git a/resources/images/icons/delete16@3x.png b/resources/images/icons/delete16@3x.png new file mode 100644 index 00000000000..059a32bf6d6 Binary files /dev/null and b/resources/images/icons/delete16@3x.png differ diff --git a/resources/images/icons/delete@2x.png b/resources/images/icons/delete@2x.png new file mode 100755 index 00000000000..644b37eae6d Binary files /dev/null and b/resources/images/icons/delete@2x.png differ diff --git a/resources/images/icons/delete@3x.png b/resources/images/icons/delete@3x.png new file mode 100755 index 00000000000..40857ec4732 Binary files /dev/null and b/resources/images/icons/delete@3x.png differ diff --git a/resources/images/icons/desktop@2x.png b/resources/images/icons/desktop@2x.png new file mode 100755 index 00000000000..0af6b323c02 Binary files /dev/null and b/resources/images/icons/desktop@2x.png differ diff --git a/resources/images/icons/desktop@3x.png b/resources/images/icons/desktop@3x.png new file mode 100755 index 00000000000..3864f57a518 Binary files /dev/null and b/resources/images/icons/desktop@3x.png differ diff --git a/resources/images/icons/download@2x.png b/resources/images/icons/download@2x.png new file mode 100755 index 00000000000..7bf81cc2b98 Binary files /dev/null and b/resources/images/icons/download@2x.png differ diff --git a/resources/images/icons/download@3x.png b/resources/images/icons/download@3x.png new file mode 100755 index 00000000000..90450e0d549 Binary files /dev/null and b/resources/images/icons/download@3x.png differ diff --git a/resources/images/icons/dropdown@2x.png b/resources/images/icons/dropdown@2x.png new file mode 100755 index 00000000000..b82ca668246 Binary files /dev/null and b/resources/images/icons/dropdown@2x.png differ diff --git a/resources/images/icons/dropdown@3x.png b/resources/images/icons/dropdown@3x.png new file mode 100755 index 00000000000..889cf393bbe Binary files /dev/null and b/resources/images/icons/dropdown@3x.png differ diff --git a/resources/images/icons/dropdown_up@2x.png b/resources/images/icons/dropdown_up@2x.png new file mode 100755 index 00000000000..389516c802a Binary files /dev/null and b/resources/images/icons/dropdown_up@2x.png differ diff --git a/resources/images/icons/dropdown_up@3x.png b/resources/images/icons/dropdown_up@3x.png new file mode 100755 index 00000000000..20245c0577b Binary files /dev/null and b/resources/images/icons/dropdown_up@3x.png differ diff --git a/resources/images/icons/edit@2x.png b/resources/images/icons/edit@2x.png new file mode 100755 index 00000000000..2d479cd7c19 Binary files /dev/null and b/resources/images/icons/edit@2x.png differ diff --git a/resources/images/icons/edit@3x.png b/resources/images/icons/edit@3x.png new file mode 100755 index 00000000000..09ddc321ab8 Binary files /dev/null and b/resources/images/icons/edit@3x.png differ diff --git a/resources/images/icons/edit_connector@2x.png b/resources/images/icons/edit_connector@2x.png new file mode 100644 index 00000000000..58d7e3b1f41 Binary files /dev/null and b/resources/images/icons/edit_connector@2x.png differ diff --git a/resources/images/icons/edit_connector@3x.png b/resources/images/icons/edit_connector@3x.png new file mode 100644 index 00000000000..bb1ab6c3655 Binary files /dev/null and b/resources/images/icons/edit_connector@3x.png differ diff --git a/resources/images/icons/edit_context20@2x.png b/resources/images/icons/edit_context20@2x.png new file mode 100644 index 00000000000..ee213e6b69f Binary files /dev/null and b/resources/images/icons/edit_context20@2x.png differ diff --git a/resources/images/icons/edit_context20@3x.png b/resources/images/icons/edit_context20@3x.png new file mode 100644 index 00000000000..934790cb2a3 Binary files /dev/null and b/resources/images/icons/edit_context20@3x.png differ diff --git a/resources/images/icons/emojis@2x.png b/resources/images/icons/emojis@2x.png new file mode 100644 index 00000000000..021fb6218a2 Binary files /dev/null and b/resources/images/icons/emojis@2x.png differ diff --git a/resources/images/icons/emojis@3x.png b/resources/images/icons/emojis@3x.png new file mode 100644 index 00000000000..0847aaf3150 Binary files /dev/null and b/resources/images/icons/emojis@3x.png differ diff --git a/resources/images/icons/faceid@2x.png b/resources/images/icons/faceid@2x.png new file mode 100755 index 00000000000..26549e0ba65 Binary files /dev/null and b/resources/images/icons/faceid@2x.png differ diff --git a/resources/images/icons/faceid@3x.png b/resources/images/icons/faceid@3x.png new file mode 100755 index 00000000000..64c2240d5a8 Binary files /dev/null and b/resources/images/icons/faceid@3x.png differ diff --git a/resources/images/icons/favourite@2x.png b/resources/images/icons/favourite@2x.png new file mode 100644 index 00000000000..612a597b4ba Binary files /dev/null and b/resources/images/icons/favourite@2x.png differ diff --git a/resources/images/icons/favourite@3x.png b/resources/images/icons/favourite@3x.png new file mode 100644 index 00000000000..5f22aa0fb1e Binary files /dev/null and b/resources/images/icons/favourite@3x.png differ diff --git a/resources/images/icons/filter@2x.png b/resources/images/icons/filter@2x.png new file mode 100755 index 00000000000..a3441eecbdf Binary files /dev/null and b/resources/images/icons/filter@2x.png differ diff --git a/resources/images/icons/filter@3x.png b/resources/images/icons/filter@3x.png new file mode 100755 index 00000000000..e9e1ec7ab87 Binary files /dev/null and b/resources/images/icons/filter@3x.png differ diff --git a/resources/images/icons/flags@2x.png b/resources/images/icons/flags@2x.png new file mode 100644 index 00000000000..41426612bdd Binary files /dev/null and b/resources/images/icons/flags@2x.png differ diff --git a/resources/images/icons/flags@3x.png b/resources/images/icons/flags@3x.png new file mode 100644 index 00000000000..5ac420ecfbb Binary files /dev/null and b/resources/images/icons/flags@3x.png differ diff --git a/resources/images/icons/flash@2x.png b/resources/images/icons/flash@2x.png new file mode 100755 index 00000000000..c63372dcfba Binary files /dev/null and b/resources/images/icons/flash@2x.png differ diff --git a/resources/images/icons/flash@3x.png b/resources/images/icons/flash@3x.png new file mode 100755 index 00000000000..f3e813cfee6 Binary files /dev/null and b/resources/images/icons/flash@3x.png differ diff --git a/resources/images/icons/flash_active@2x.png b/resources/images/icons/flash_active@2x.png new file mode 100755 index 00000000000..e33307f6d65 Binary files /dev/null and b/resources/images/icons/flash_active@2x.png differ diff --git a/resources/images/icons/flash_active@3x.png b/resources/images/icons/flash_active@3x.png new file mode 100755 index 00000000000..db7e367db2e Binary files /dev/null and b/resources/images/icons/flash_active@3x.png differ diff --git a/resources/images/icons/food@2x.png b/resources/images/icons/food@2x.png new file mode 100644 index 00000000000..fb14a08150e Binary files /dev/null and b/resources/images/icons/food@2x.png differ diff --git a/resources/images/icons/food@3x.png b/resources/images/icons/food@3x.png new file mode 100644 index 00000000000..31dfb3e1fcf Binary files /dev/null and b/resources/images/icons/food@3x.png differ diff --git a/resources/images/icons/forward@2x.png b/resources/images/icons/forward@2x.png new file mode 100644 index 00000000000..960d92f918f Binary files /dev/null and b/resources/images/icons/forward@2x.png differ diff --git a/resources/images/icons/forward@3x.png b/resources/images/icons/forward@3x.png new file mode 100644 index 00000000000..b9740b4e8da Binary files /dev/null and b/resources/images/icons/forward@3x.png differ diff --git a/resources/images/icons/gallery@2x.png b/resources/images/icons/gallery@2x.png new file mode 100755 index 00000000000..44a4dfbed63 Binary files /dev/null and b/resources/images/icons/gallery@2x.png differ diff --git a/resources/images/icons/gallery@3x.png b/resources/images/icons/gallery@3x.png new file mode 100755 index 00000000000..22e8e528622 Binary files /dev/null and b/resources/images/icons/gallery@3x.png differ diff --git a/resources/images/icons/gif@2x.png b/resources/images/icons/gif@2x.png new file mode 100644 index 00000000000..daeb482ff0e Binary files /dev/null and b/resources/images/icons/gif@2x.png differ diff --git a/resources/images/icons/gif@3x.png b/resources/images/icons/gif@3x.png new file mode 100644 index 00000000000..59f560b5dd4 Binary files /dev/null and b/resources/images/icons/gif@3x.png differ diff --git a/resources/images/icons/group_chat@2x.png b/resources/images/icons/group_chat@2x.png new file mode 100755 index 00000000000..449234a8431 Binary files /dev/null and b/resources/images/icons/group_chat@2x.png differ diff --git a/resources/images/icons/group_chat@3x.png b/resources/images/icons/group_chat@3x.png new file mode 100755 index 00000000000..c045bc6384a Binary files /dev/null and b/resources/images/icons/group_chat@3x.png differ diff --git a/resources/images/icons/help@2x.png b/resources/images/icons/help@2x.png new file mode 100755 index 00000000000..2715e4d5dff Binary files /dev/null and b/resources/images/icons/help@2x.png differ diff --git a/resources/images/icons/help@3x.png b/resources/images/icons/help@3x.png new file mode 100755 index 00000000000..5532f68e472 Binary files /dev/null and b/resources/images/icons/help@3x.png differ diff --git a/resources/images/icons/hide@2x.png b/resources/images/icons/hide@2x.png new file mode 100755 index 00000000000..77a0e536af6 Binary files /dev/null and b/resources/images/icons/hide@2x.png differ diff --git a/resources/images/icons/hide@3x.png b/resources/images/icons/hide@3x.png new file mode 100755 index 00000000000..fad5a5591cc Binary files /dev/null and b/resources/images/icons/hide@3x.png differ diff --git a/resources/images/icons/history@2x.png b/resources/images/icons/history@2x.png new file mode 100755 index 00000000000..07953fc0b4f Binary files /dev/null and b/resources/images/icons/history@2x.png differ diff --git a/resources/images/icons/history@3x.png b/resources/images/icons/history@3x.png new file mode 100755 index 00000000000..6ed7eaa65c2 Binary files /dev/null and b/resources/images/icons/history@3x.png differ diff --git a/resources/images/icons/in_contacts@2x.png b/resources/images/icons/in_contacts@2x.png new file mode 100755 index 00000000000..2361afcbc09 Binary files /dev/null and b/resources/images/icons/in_contacts@2x.png differ diff --git a/resources/images/icons/in_contacts@3x.png b/resources/images/icons/in_contacts@3x.png new file mode 100755 index 00000000000..ed834aa1474 Binary files /dev/null and b/resources/images/icons/in_contacts@3x.png differ diff --git a/resources/images/icons/info@2x.png b/resources/images/icons/info@2x.png new file mode 100755 index 00000000000..f27fa86821e Binary files /dev/null and b/resources/images/icons/info@2x.png differ diff --git a/resources/images/icons/info@3x.png b/resources/images/icons/info@3x.png new file mode 100755 index 00000000000..5e5aa59d467 Binary files /dev/null and b/resources/images/icons/info@3x.png differ diff --git a/resources/images/icons/key@2x.png b/resources/images/icons/key@2x.png new file mode 100644 index 00000000000..411c4ab353e Binary files /dev/null and b/resources/images/icons/key@2x.png differ diff --git a/resources/images/icons/key@3x.png b/resources/images/icons/key@3x.png new file mode 100644 index 00000000000..21cb98d3fcc Binary files /dev/null and b/resources/images/icons/key@3x.png differ diff --git a/resources/images/icons/keyboard@2x.png b/resources/images/icons/keyboard@2x.png new file mode 100644 index 00000000000..c61c1dfdd64 Binary files /dev/null and b/resources/images/icons/keyboard@2x.png differ diff --git a/resources/images/icons/keyboard@3x.png b/resources/images/icons/keyboard@3x.png new file mode 100644 index 00000000000..d3cc2cbe540 Binary files /dev/null and b/resources/images/icons/keyboard@3x.png differ diff --git a/resources/images/icons/keycard@2x.png b/resources/images/icons/keycard@2x.png new file mode 100755 index 00000000000..163c56b3a3f Binary files /dev/null and b/resources/images/icons/keycard@2x.png differ diff --git a/resources/images/icons/keycard@3x.png b/resources/images/icons/keycard@3x.png new file mode 100755 index 00000000000..2a2f53fdf55 Binary files /dev/null and b/resources/images/icons/keycard@3x.png differ diff --git a/resources/images/icons/keycard_account@2x.png b/resources/images/icons/keycard_account@2x.png new file mode 100755 index 00000000000..a2cb2e2aea3 Binary files /dev/null and b/resources/images/icons/keycard_account@2x.png differ diff --git a/resources/images/icons/keycard_account@3x.png b/resources/images/icons/keycard_account@3x.png new file mode 100755 index 00000000000..d991afaa3e9 Binary files /dev/null and b/resources/images/icons/keycard_account@3x.png differ diff --git a/resources/images/icons/keycard_logo_big@2x.png b/resources/images/icons/keycard_logo_big@2x.png new file mode 100644 index 00000000000..53eb0d68541 Binary files /dev/null and b/resources/images/icons/keycard_logo_big@2x.png differ diff --git a/resources/images/icons/keycard_logo_big@3x.png b/resources/images/icons/keycard_logo_big@3x.png new file mode 100644 index 00000000000..332afc2f0cb Binary files /dev/null and b/resources/images/icons/keycard_logo_big@3x.png differ diff --git a/resources/images/icons/language@2x.png b/resources/images/icons/language@2x.png new file mode 100755 index 00000000000..996ca796c25 Binary files /dev/null and b/resources/images/icons/language@2x.png differ diff --git a/resources/images/icons/language@3x.png b/resources/images/icons/language@3x.png new file mode 100755 index 00000000000..0e998becbd1 Binary files /dev/null and b/resources/images/icons/language@3x.png differ diff --git a/resources/images/icons/link@2x.png b/resources/images/icons/link@2x.png new file mode 100755 index 00000000000..d1a0620b5fb Binary files /dev/null and b/resources/images/icons/link@2x.png differ diff --git a/resources/images/icons/link@3x.png b/resources/images/icons/link@3x.png new file mode 100755 index 00000000000..f1b02519da7 Binary files /dev/null and b/resources/images/icons/link@3x.png differ diff --git a/resources/images/icons/log_out@2x.png b/resources/images/icons/log_out@2x.png new file mode 100755 index 00000000000..a7b97680e1e Binary files /dev/null and b/resources/images/icons/log_out@2x.png differ diff --git a/resources/images/icons/log_out@3x.png b/resources/images/icons/log_out@3x.png new file mode 100755 index 00000000000..f09ee825e82 Binary files /dev/null and b/resources/images/icons/log_out@3x.png differ diff --git a/resources/images/icons/mailserver@2x.png b/resources/images/icons/mailserver@2x.png new file mode 100755 index 00000000000..f22171aa91f Binary files /dev/null and b/resources/images/icons/mailserver@2x.png differ diff --git a/resources/images/icons/mailserver@3x.png b/resources/images/icons/mailserver@3x.png new file mode 100755 index 00000000000..b0a80033c90 Binary files /dev/null and b/resources/images/icons/mailserver@3x.png differ diff --git a/resources/images/icons/make_admin@2x.png b/resources/images/icons/make_admin@2x.png new file mode 100755 index 00000000000..5f4400d4096 Binary files /dev/null and b/resources/images/icons/make_admin@2x.png differ diff --git a/resources/images/icons/make_admin@3x.png b/resources/images/icons/make_admin@3x.png new file mode 100755 index 00000000000..850abdb66c3 Binary files /dev/null and b/resources/images/icons/make_admin@3x.png differ diff --git a/resources/images/icons/manage_connections@2x.png b/resources/images/icons/manage_connections@2x.png new file mode 100644 index 00000000000..c5db35bf2b1 Binary files /dev/null and b/resources/images/icons/manage_connections@2x.png differ diff --git a/resources/images/icons/manage_connections@3x.png b/resources/images/icons/manage_connections@3x.png new file mode 100644 index 00000000000..45332060887 Binary files /dev/null and b/resources/images/icons/manage_connections@3x.png differ diff --git a/resources/images/icons/max@2x.png b/resources/images/icons/max@2x.png new file mode 100755 index 00000000000..76522345810 Binary files /dev/null and b/resources/images/icons/max@2x.png differ diff --git a/resources/images/icons/max@3x.png b/resources/images/icons/max@3x.png new file mode 100755 index 00000000000..d410f234f21 Binary files /dev/null and b/resources/images/icons/max@3x.png differ diff --git a/resources/images/icons/mention@2x.png b/resources/images/icons/mention@2x.png new file mode 100644 index 00000000000..1469fdd8d60 Binary files /dev/null and b/resources/images/icons/mention@2x.png differ diff --git a/resources/images/icons/mention@3x.png b/resources/images/icons/mention@3x.png new file mode 100644 index 00000000000..fb251c0e19a Binary files /dev/null and b/resources/images/icons/mention@3x.png differ diff --git a/resources/images/icons/message@2x.png b/resources/images/icons/message@2x.png new file mode 100755 index 00000000000..9db11896b95 Binary files /dev/null and b/resources/images/icons/message@2x.png differ diff --git a/resources/images/icons/message@3x.png b/resources/images/icons/message@3x.png new file mode 100755 index 00000000000..54ab2641c1e Binary files /dev/null and b/resources/images/icons/message@3x.png differ diff --git a/resources/images/icons/mobile@2x.png b/resources/images/icons/mobile@2x.png new file mode 100755 index 00000000000..28233679a1a Binary files /dev/null and b/resources/images/icons/mobile@2x.png differ diff --git a/resources/images/icons/mobile@3x.png b/resources/images/icons/mobile@3x.png new file mode 100755 index 00000000000..2cb91f9b4b1 Binary files /dev/null and b/resources/images/icons/mobile@3x.png differ diff --git a/resources/images/icons/mobile_sync@2x.png b/resources/images/icons/mobile_sync@2x.png new file mode 100644 index 00000000000..341c4d69950 Binary files /dev/null and b/resources/images/icons/mobile_sync@2x.png differ diff --git a/resources/images/icons/mobile_sync@3x.png b/resources/images/icons/mobile_sync@3x.png new file mode 100644 index 00000000000..3e5dfea8bda Binary files /dev/null and b/resources/images/icons/mobile_sync@3x.png differ diff --git a/resources/images/icons/mobile_sync_off@2x.png b/resources/images/icons/mobile_sync_off@2x.png new file mode 100644 index 00000000000..82e6ccbcb28 Binary files /dev/null and b/resources/images/icons/mobile_sync_off@2x.png differ diff --git a/resources/images/icons/mobile_sync_off@3x.png b/resources/images/icons/mobile_sync_off@3x.png new file mode 100644 index 00000000000..f3d46eaa83e Binary files /dev/null and b/resources/images/icons/mobile_sync_off@3x.png differ diff --git a/resources/images/icons/more@2x.png b/resources/images/icons/more@2x.png new file mode 100755 index 00000000000..00eac9cb2e1 Binary files /dev/null and b/resources/images/icons/more@2x.png differ diff --git a/resources/images/icons/more@3x.png b/resources/images/icons/more@3x.png new file mode 100755 index 00000000000..0209da70e4e Binary files /dev/null and b/resources/images/icons/more@3x.png differ diff --git a/resources/images/icons/network@2x.png b/resources/images/icons/network@2x.png new file mode 100755 index 00000000000..9933c8faf56 Binary files /dev/null and b/resources/images/icons/network@2x.png differ diff --git a/resources/images/icons/network@3x.png b/resources/images/icons/network@3x.png new file mode 100755 index 00000000000..93b86430ee0 Binary files /dev/null and b/resources/images/icons/network@3x.png differ diff --git a/resources/images/icons/next@2x.png b/resources/images/icons/next@2x.png new file mode 100755 index 00000000000..bb05b6d803f Binary files /dev/null and b/resources/images/icons/next@2x.png differ diff --git a/resources/images/icons/next@3x.png b/resources/images/icons/next@3x.png new file mode 100755 index 00000000000..d9092a4206e Binary files /dev/null and b/resources/images/icons/next@3x.png differ diff --git a/resources/images/icons/node_offline@2x.png b/resources/images/icons/node_offline@2x.png new file mode 100644 index 00000000000..722c92efd4d Binary files /dev/null and b/resources/images/icons/node_offline@2x.png differ diff --git a/resources/images/icons/node_offline@3x.png b/resources/images/icons/node_offline@3x.png new file mode 100644 index 00000000000..4d44df801a7 Binary files /dev/null and b/resources/images/icons/node_offline@3x.png differ diff --git a/resources/images/icons/notification2@2x.png b/resources/images/icons/notification2@2x.png new file mode 100644 index 00000000000..618571a5268 Binary files /dev/null and b/resources/images/icons/notification2@2x.png differ diff --git a/resources/images/icons/notification2@3x.png b/resources/images/icons/notification2@3x.png new file mode 100644 index 00000000000..6720c67c53b Binary files /dev/null and b/resources/images/icons/notification2@3x.png differ diff --git a/resources/images/icons/notification@2x.png b/resources/images/icons/notification@2x.png new file mode 100755 index 00000000000..3f2eeb1b694 Binary files /dev/null and b/resources/images/icons/notification@2x.png differ diff --git a/resources/images/icons/notification@3x.png b/resources/images/icons/notification@3x.png new file mode 100755 index 00000000000..1b456e0bd16 Binary files /dev/null and b/resources/images/icons/notification@3x.png differ diff --git a/resources/images/icons/objects@2x.png b/resources/images/icons/objects@2x.png new file mode 100644 index 00000000000..411c4ab353e Binary files /dev/null and b/resources/images/icons/objects@2x.png differ diff --git a/resources/images/icons/objects@3x.png b/resources/images/icons/objects@3x.png new file mode 100644 index 00000000000..21cb98d3fcc Binary files /dev/null and b/resources/images/icons/objects@3x.png differ diff --git a/resources/images/icons/offline@2x.png b/resources/images/icons/offline@2x.png new file mode 100644 index 00000000000..4a5ddc2229c Binary files /dev/null and b/resources/images/icons/offline@2x.png differ diff --git a/resources/images/icons/offline@3x.png b/resources/images/icons/offline@3x.png new file mode 100644 index 00000000000..9e4bac37bf5 Binary files /dev/null and b/resources/images/icons/offline@3x.png differ diff --git a/resources/images/icons/one_on_one_chat@2x.png b/resources/images/icons/one_on_one_chat@2x.png new file mode 100755 index 00000000000..aafc462455a Binary files /dev/null and b/resources/images/icons/one_on_one_chat@2x.png differ diff --git a/resources/images/icons/one_on_one_chat@3x.png b/resources/images/icons/one_on_one_chat@3x.png new file mode 100755 index 00000000000..fd424ef5fcd Binary files /dev/null and b/resources/images/icons/one_on_one_chat@3x.png differ diff --git a/resources/images/icons/password@2x.png b/resources/images/icons/password@2x.png new file mode 100755 index 00000000000..25a7c0aec27 Binary files /dev/null and b/resources/images/icons/password@2x.png differ diff --git a/resources/images/icons/password@3x.png b/resources/images/icons/password@3x.png new file mode 100755 index 00000000000..fadbb414af1 Binary files /dev/null and b/resources/images/icons/password@3x.png differ diff --git a/resources/images/icons/paste@2x.png b/resources/images/icons/paste@2x.png new file mode 100755 index 00000000000..67051621f7d Binary files /dev/null and b/resources/images/icons/paste@2x.png differ diff --git a/resources/images/icons/paste@3x.png b/resources/images/icons/paste@3x.png new file mode 100755 index 00000000000..982f2419dcd Binary files /dev/null and b/resources/images/icons/paste@3x.png differ diff --git a/resources/images/icons/pause@2x.png b/resources/images/icons/pause@2x.png new file mode 100755 index 00000000000..83ceee86e32 Binary files /dev/null and b/resources/images/icons/pause@2x.png differ diff --git a/resources/images/icons/pause@3x.png b/resources/images/icons/pause@3x.png new file mode 100755 index 00000000000..8691dd1676d Binary files /dev/null and b/resources/images/icons/pause@3x.png differ diff --git a/resources/images/icons/photo@2x.png b/resources/images/icons/photo@2x.png new file mode 100755 index 00000000000..e9cf66783fc Binary files /dev/null and b/resources/images/icons/photo@2x.png differ diff --git a/resources/images/icons/photo@3x.png b/resources/images/icons/photo@3x.png new file mode 100755 index 00000000000..880059c3055 Binary files /dev/null and b/resources/images/icons/photo@3x.png differ diff --git a/resources/images/icons/pin@2x.png b/resources/images/icons/pin@2x.png new file mode 100644 index 00000000000..60e49683998 Binary files /dev/null and b/resources/images/icons/pin@2x.png differ diff --git a/resources/images/icons/pin@3x.png b/resources/images/icons/pin@3x.png new file mode 100644 index 00000000000..082c94092d3 Binary files /dev/null and b/resources/images/icons/pin@3x.png differ diff --git a/resources/images/icons/play@2x.png b/resources/images/icons/play@2x.png new file mode 100755 index 00000000000..58785d143bd Binary files /dev/null and b/resources/images/icons/play@2x.png differ diff --git a/resources/images/icons/play@3x.png b/resources/images/icons/play@3x.png new file mode 100755 index 00000000000..6d05bb410ac Binary files /dev/null and b/resources/images/icons/play@3x.png differ diff --git a/resources/images/icons/print@2x.png b/resources/images/icons/print@2x.png new file mode 100755 index 00000000000..23fa3e52233 Binary files /dev/null and b/resources/images/icons/print@2x.png differ diff --git a/resources/images/icons/print@3x.png b/resources/images/icons/print@3x.png new file mode 100755 index 00000000000..2e6e3268a8a Binary files /dev/null and b/resources/images/icons/print@3x.png differ diff --git a/resources/images/icons/private_chat@2x.png b/resources/images/icons/private_chat@2x.png new file mode 100644 index 00000000000..b9124a5a478 Binary files /dev/null and b/resources/images/icons/private_chat@2x.png differ diff --git a/resources/images/icons/private_chat@3x.png b/resources/images/icons/private_chat@3x.png new file mode 100644 index 00000000000..12279c2f6d0 Binary files /dev/null and b/resources/images/icons/private_chat@3x.png differ diff --git a/resources/images/icons/profile@2x.png b/resources/images/icons/profile@2x.png new file mode 100755 index 00000000000..942cbb22203 Binary files /dev/null and b/resources/images/icons/profile@2x.png differ diff --git a/resources/images/icons/profile@3x.png b/resources/images/icons/profile@3x.png new file mode 100755 index 00000000000..7a291540602 Binary files /dev/null and b/resources/images/icons/profile@3x.png differ diff --git a/resources/images/icons/public_chat@2x.png b/resources/images/icons/public_chat@2x.png new file mode 100755 index 00000000000..f7d7aa2fc08 Binary files /dev/null and b/resources/images/icons/public_chat@2x.png differ diff --git a/resources/images/icons/public_chat@3x.png b/resources/images/icons/public_chat@3x.png new file mode 100755 index 00000000000..e2c69b1de83 Binary files /dev/null and b/resources/images/icons/public_chat@3x.png differ diff --git a/resources/images/icons/qr2@2x.png b/resources/images/icons/qr2@2x.png new file mode 100644 index 00000000000..6b9a5c46141 Binary files /dev/null and b/resources/images/icons/qr2@2x.png differ diff --git a/resources/images/icons/qr2@3x.png b/resources/images/icons/qr2@3x.png new file mode 100644 index 00000000000..c90bf31918e Binary files /dev/null and b/resources/images/icons/qr2@3x.png differ diff --git a/resources/images/icons/qr@2x.png b/resources/images/icons/qr@2x.png new file mode 100755 index 00000000000..6e8317195a8 Binary files /dev/null and b/resources/images/icons/qr@2x.png differ diff --git a/resources/images/icons/qr@3x.png b/resources/images/icons/qr@3x.png new file mode 100755 index 00000000000..f4b4768f745 Binary files /dev/null and b/resources/images/icons/qr@3x.png differ diff --git a/resources/images/icons/receive@2x.png b/resources/images/icons/receive@2x.png new file mode 100755 index 00000000000..fc1cf4b3a36 Binary files /dev/null and b/resources/images/icons/receive@2x.png differ diff --git a/resources/images/icons/receive@3x.png b/resources/images/icons/receive@3x.png new file mode 100755 index 00000000000..fd395c9f0c4 Binary files /dev/null and b/resources/images/icons/receive@3x.png differ diff --git a/resources/images/icons/recent@2x.png b/resources/images/icons/recent@2x.png new file mode 100755 index 00000000000..28e8a31f27d Binary files /dev/null and b/resources/images/icons/recent@2x.png differ diff --git a/resources/images/icons/recent@3x.png b/resources/images/icons/recent@3x.png new file mode 100755 index 00000000000..d7e2ff4d736 Binary files /dev/null and b/resources/images/icons/recent@3x.png differ diff --git a/resources/images/icons/refresh@2x.png b/resources/images/icons/refresh@2x.png new file mode 100755 index 00000000000..7215201e3fb Binary files /dev/null and b/resources/images/icons/refresh@2x.png differ diff --git a/resources/images/icons/refresh@3x.png b/resources/images/icons/refresh@3x.png new file mode 100755 index 00000000000..e21f576ea1d Binary files /dev/null and b/resources/images/icons/refresh@3x.png differ diff --git a/resources/images/icons/remove_circle@2x.png b/resources/images/icons/remove_circle@2x.png new file mode 100755 index 00000000000..1216b2337ee Binary files /dev/null and b/resources/images/icons/remove_circle@2x.png differ diff --git a/resources/images/icons/remove_circle@3x.png b/resources/images/icons/remove_circle@3x.png new file mode 100755 index 00000000000..8ed65c16814 Binary files /dev/null and b/resources/images/icons/remove_circle@3x.png differ diff --git a/resources/images/icons/remove_contact@2x.png b/resources/images/icons/remove_contact@2x.png new file mode 100755 index 00000000000..4dc3ffadebd Binary files /dev/null and b/resources/images/icons/remove_contact@2x.png differ diff --git a/resources/images/icons/remove_contact@3x.png b/resources/images/icons/remove_contact@3x.png new file mode 100755 index 00000000000..f7c3bf33822 Binary files /dev/null and b/resources/images/icons/remove_contact@3x.png differ diff --git a/resources/images/icons/reorder_handle@2x.png b/resources/images/icons/reorder_handle@2x.png new file mode 100644 index 00000000000..6ea42f4129d Binary files /dev/null and b/resources/images/icons/reorder_handle@2x.png differ diff --git a/resources/images/icons/reorder_handle@3x.png b/resources/images/icons/reorder_handle@3x.png new file mode 100644 index 00000000000..9dd3e129138 Binary files /dev/null and b/resources/images/icons/reorder_handle@3x.png differ diff --git a/resources/images/icons/reply@2x.png b/resources/images/icons/reply@2x.png new file mode 100755 index 00000000000..fce2c0dd9c0 Binary files /dev/null and b/resources/images/icons/reply@2x.png differ diff --git a/resources/images/icons/reply@3x.png b/resources/images/icons/reply@3x.png new file mode 100755 index 00000000000..1e03b39e87f Binary files /dev/null and b/resources/images/icons/reply@3x.png differ diff --git a/resources/images/icons/rotate_camera@2x.png b/resources/images/icons/rotate_camera@2x.png new file mode 100755 index 00000000000..d3cb68b346e Binary files /dev/null and b/resources/images/icons/rotate_camera@2x.png differ diff --git a/resources/images/icons/rotate_camera@3x.png b/resources/images/icons/rotate_camera@3x.png new file mode 100755 index 00000000000..2bd7ab9cd51 Binary files /dev/null and b/resources/images/icons/rotate_camera@3x.png differ diff --git a/resources/images/icons/save@2x.png b/resources/images/icons/save@2x.png new file mode 100644 index 00000000000..b5b53617e42 Binary files /dev/null and b/resources/images/icons/save@2x.png differ diff --git a/resources/images/icons/save@3x.png b/resources/images/icons/save@3x.png new file mode 100644 index 00000000000..96cb7a01ca1 Binary files /dev/null and b/resources/images/icons/save@3x.png differ diff --git a/resources/images/icons/scan2@2x.png b/resources/images/icons/scan2@2x.png new file mode 100644 index 00000000000..a6e9105e043 Binary files /dev/null and b/resources/images/icons/scan2@2x.png differ diff --git a/resources/images/icons/scan2@3x.png b/resources/images/icons/scan2@3x.png new file mode 100644 index 00000000000..7d666a00104 Binary files /dev/null and b/resources/images/icons/scan2@3x.png differ diff --git a/resources/images/icons/scan@2x.png b/resources/images/icons/scan@2x.png new file mode 100644 index 00000000000..f15a22cd902 Binary files /dev/null and b/resources/images/icons/scan@2x.png differ diff --git a/resources/images/icons/scan@3x.png b/resources/images/icons/scan@3x.png new file mode 100644 index 00000000000..97e4df92be7 Binary files /dev/null and b/resources/images/icons/scan@3x.png differ diff --git a/resources/images/icons/search2@2x.png b/resources/images/icons/search2@2x.png new file mode 100644 index 00000000000..5bf3dbfd946 Binary files /dev/null and b/resources/images/icons/search2@2x.png differ diff --git a/resources/images/icons/search2@3x.png b/resources/images/icons/search2@3x.png new file mode 100644 index 00000000000..2080d4c1cdc Binary files /dev/null and b/resources/images/icons/search2@3x.png differ diff --git a/resources/images/icons/search@2x.png b/resources/images/icons/search@2x.png new file mode 100755 index 00000000000..08742ab6daf Binary files /dev/null and b/resources/images/icons/search@2x.png differ diff --git a/resources/images/icons/search@3x.png b/resources/images/icons/search@3x.png new file mode 100755 index 00000000000..1811c0e9b0a Binary files /dev/null and b/resources/images/icons/search@3x.png differ diff --git a/resources/images/icons/security24@2x.png b/resources/images/icons/security24@2x.png new file mode 100755 index 00000000000..c50375e8038 Binary files /dev/null and b/resources/images/icons/security24@2x.png differ diff --git a/resources/images/icons/security@2x.png b/resources/images/icons/security@2x.png new file mode 100755 index 00000000000..c50375e8038 Binary files /dev/null and b/resources/images/icons/security@2x.png differ diff --git a/resources/images/icons/security@3x.png b/resources/images/icons/security@3x.png new file mode 100755 index 00000000000..4784c695045 Binary files /dev/null and b/resources/images/icons/security@3x.png differ diff --git a/resources/images/icons/send@2x.png b/resources/images/icons/send@2x.png new file mode 100755 index 00000000000..9084f394068 Binary files /dev/null and b/resources/images/icons/send@2x.png differ diff --git a/resources/images/icons/send@3x.png b/resources/images/icons/send@3x.png new file mode 100755 index 00000000000..636b4699769 Binary files /dev/null and b/resources/images/icons/send@3x.png differ diff --git a/resources/images/icons/settings@2x.png b/resources/images/icons/settings@2x.png new file mode 100755 index 00000000000..d9f40481ac6 Binary files /dev/null and b/resources/images/icons/settings@2x.png differ diff --git a/resources/images/icons/settings@3x.png b/resources/images/icons/settings@3x.png new file mode 100755 index 00000000000..1b185ffe58f Binary files /dev/null and b/resources/images/icons/settings@3x.png differ diff --git a/resources/images/icons/settings_advanced@2x.png b/resources/images/icons/settings_advanced@2x.png new file mode 100755 index 00000000000..6e4f1d3bc56 Binary files /dev/null and b/resources/images/icons/settings_advanced@2x.png differ diff --git a/resources/images/icons/settings_advanced@3x.png b/resources/images/icons/settings_advanced@3x.png new file mode 100755 index 00000000000..cf53357da76 Binary files /dev/null and b/resources/images/icons/settings_advanced@3x.png differ diff --git a/resources/images/icons/share@2x.android.png b/resources/images/icons/share@2x.android.png new file mode 100755 index 00000000000..5f0d4e2d630 Binary files /dev/null and b/resources/images/icons/share@2x.android.png differ diff --git a/resources/images/icons/share@2x.ios.png b/resources/images/icons/share@2x.ios.png new file mode 100755 index 00000000000..aeb279a80fb Binary files /dev/null and b/resources/images/icons/share@2x.ios.png differ diff --git a/resources/images/icons/share@2x.png b/resources/images/icons/share@2x.png new file mode 100644 index 00000000000..d01e97520d8 Binary files /dev/null and b/resources/images/icons/share@2x.png differ diff --git a/resources/images/icons/share@3x.android.png b/resources/images/icons/share@3x.android.png new file mode 100755 index 00000000000..09eeff835aa Binary files /dev/null and b/resources/images/icons/share@3x.android.png differ diff --git a/resources/images/icons/share@3x.ios.png b/resources/images/icons/share@3x.ios.png new file mode 100755 index 00000000000..122680892d3 Binary files /dev/null and b/resources/images/icons/share@3x.ios.png differ diff --git a/resources/images/icons/share@3x.png b/resources/images/icons/share@3x.png new file mode 100644 index 00000000000..75e69f7811f Binary files /dev/null and b/resources/images/icons/share@3x.png differ diff --git a/resources/images/icons/share_default@2x.png b/resources/images/icons/share_default@2x.png new file mode 100755 index 00000000000..aeb279a80fb Binary files /dev/null and b/resources/images/icons/share_default@2x.png differ diff --git a/resources/images/icons/share_default@3x.png b/resources/images/icons/share_default@3x.png new file mode 100755 index 00000000000..122680892d3 Binary files /dev/null and b/resources/images/icons/share_default@3x.png differ diff --git a/resources/images/icons/show@2x.png b/resources/images/icons/show@2x.png new file mode 100755 index 00000000000..6b9f0568e0f Binary files /dev/null and b/resources/images/icons/show@2x.png differ diff --git a/resources/images/icons/show@3x.png b/resources/images/icons/show@3x.png new file mode 100755 index 00000000000..5582bda6fdd Binary files /dev/null and b/resources/images/icons/show@3x.png differ diff --git a/resources/images/icons/smileys@2x.png b/resources/images/icons/smileys@2x.png new file mode 100644 index 00000000000..f77d693a2b1 Binary files /dev/null and b/resources/images/icons/smileys@2x.png differ diff --git a/resources/images/icons/smileys@3x.png b/resources/images/icons/smileys@3x.png new file mode 100644 index 00000000000..1d11ca2a85c Binary files /dev/null and b/resources/images/icons/smileys@3x.png differ diff --git a/resources/images/icons/speech@2x.png b/resources/images/icons/speech@2x.png new file mode 100644 index 00000000000..45f36003b3b Binary files /dev/null and b/resources/images/icons/speech@2x.png differ diff --git a/resources/images/icons/speech@3x.png b/resources/images/icons/speech@3x.png new file mode 100644 index 00000000000..93c7c09ed01 Binary files /dev/null and b/resources/images/icons/speech@3x.png differ diff --git a/resources/images/icons/status@2x.png b/resources/images/icons/status@2x.png new file mode 100644 index 00000000000..36cbd21b70e Binary files /dev/null and b/resources/images/icons/status@2x.png differ diff --git a/resources/images/icons/status@3x.png b/resources/images/icons/status@3x.png new file mode 100644 index 00000000000..2245af31d26 Binary files /dev/null and b/resources/images/icons/status@3x.png differ diff --git a/resources/images/icons/sticker_history@2x.png b/resources/images/icons/sticker_history@2x.png new file mode 100644 index 00000000000..63ffd3fe3cf Binary files /dev/null and b/resources/images/icons/sticker_history@2x.png differ diff --git a/resources/images/icons/sticker_history@3x.png b/resources/images/icons/sticker_history@3x.png new file mode 100644 index 00000000000..de8e2cd42df Binary files /dev/null and b/resources/images/icons/sticker_history@3x.png differ diff --git a/resources/images/icons/stickers@2x.png b/resources/images/icons/stickers@2x.png new file mode 100755 index 00000000000..9be3fb8544a Binary files /dev/null and b/resources/images/icons/stickers@2x.png differ diff --git a/resources/images/icons/stickers@3x.png b/resources/images/icons/stickers@3x.png new file mode 100755 index 00000000000..25c93c89995 Binary files /dev/null and b/resources/images/icons/stickers@3x.png differ diff --git a/resources/images/icons/stickers_big@2x.png b/resources/images/icons/stickers_big@2x.png new file mode 100644 index 00000000000..e1026f2c1e4 Binary files /dev/null and b/resources/images/icons/stickers_big@2x.png differ diff --git a/resources/images/icons/stickers_big@3x.png b/resources/images/icons/stickers_big@3x.png new file mode 100644 index 00000000000..7f648a195c4 Binary files /dev/null and b/resources/images/icons/stickers_big@3x.png differ diff --git a/resources/images/icons/symbols@2x.png b/resources/images/icons/symbols@2x.png new file mode 100644 index 00000000000..00a39f56677 Binary files /dev/null and b/resources/images/icons/symbols@2x.png differ diff --git a/resources/images/icons/symbols@3x.png b/resources/images/icons/symbols@3x.png new file mode 100644 index 00000000000..d6e0a032506 Binary files /dev/null and b/resources/images/icons/symbols@3x.png differ diff --git a/resources/images/icons/tabs@2x.png b/resources/images/icons/tabs@2x.png new file mode 100644 index 00000000000..5a76be6f79a Binary files /dev/null and b/resources/images/icons/tabs@2x.png differ diff --git a/resources/images/icons/tabs@3x.png b/resources/images/icons/tabs@3x.png new file mode 100644 index 00000000000..76e04ec4938 Binary files /dev/null and b/resources/images/icons/tabs@3x.png differ diff --git a/resources/images/icons/text@2x.png b/resources/images/icons/text@2x.png new file mode 100755 index 00000000000..e918677ad24 Binary files /dev/null and b/resources/images/icons/text@2x.png differ diff --git a/resources/images/icons/text@3x.png b/resources/images/icons/text@3x.png new file mode 100755 index 00000000000..85260cc7c73 Binary files /dev/null and b/resources/images/icons/text@3x.png differ diff --git a/resources/images/icons/tiny_arrow_down@2x.png b/resources/images/icons/tiny_arrow_down@2x.png new file mode 100644 index 00000000000..447dc244d72 Binary files /dev/null and b/resources/images/icons/tiny_arrow_down@2x.png differ diff --git a/resources/images/icons/tiny_arrow_down@3x.png b/resources/images/icons/tiny_arrow_down@3x.png new file mode 100644 index 00000000000..a73cde7a1b7 Binary files /dev/null and b/resources/images/icons/tiny_arrow_down@3x.png differ diff --git a/resources/images/icons/tiny_check@2x.png b/resources/images/icons/tiny_check@2x.png new file mode 100644 index 00000000000..9369f0bd73a Binary files /dev/null and b/resources/images/icons/tiny_check@2x.png differ diff --git a/resources/images/icons/tiny_check@3x.png b/resources/images/icons/tiny_check@3x.png new file mode 100644 index 00000000000..f24424fb866 Binary files /dev/null and b/resources/images/icons/tiny_check@3x.png differ diff --git a/resources/images/icons/tiny_community@2x.png b/resources/images/icons/tiny_community@2x.png new file mode 100644 index 00000000000..ff26bbb8464 Binary files /dev/null and b/resources/images/icons/tiny_community@2x.png differ diff --git a/resources/images/icons/tiny_community@3x.png b/resources/images/icons/tiny_community@3x.png new file mode 100644 index 00000000000..97f43b69fde Binary files /dev/null and b/resources/images/icons/tiny_community@3x.png differ diff --git a/resources/images/icons/tiny_delivered@2x.png b/resources/images/icons/tiny_delivered@2x.png new file mode 100644 index 00000000000..8b89b8dd34c Binary files /dev/null and b/resources/images/icons/tiny_delivered@2x.png differ diff --git a/resources/images/icons/tiny_delivered@3x.png b/resources/images/icons/tiny_delivered@3x.png new file mode 100644 index 00000000000..cb212df6066 Binary files /dev/null and b/resources/images/icons/tiny_delivered@3x.png differ diff --git a/resources/images/icons/tiny_edit@2x.png b/resources/images/icons/tiny_edit@2x.png new file mode 100644 index 00000000000..8b30392bf97 Binary files /dev/null and b/resources/images/icons/tiny_edit@2x.png differ diff --git a/resources/images/icons/tiny_edit@3x.png b/resources/images/icons/tiny_edit@3x.png new file mode 100644 index 00000000000..ffb8c6d2fcc Binary files /dev/null and b/resources/images/icons/tiny_edit@3x.png differ diff --git a/resources/images/icons/tiny_external@2x.png b/resources/images/icons/tiny_external@2x.png new file mode 100644 index 00000000000..a2272398dba Binary files /dev/null and b/resources/images/icons/tiny_external@2x.png differ diff --git a/resources/images/icons/tiny_external@3x.png b/resources/images/icons/tiny_external@3x.png new file mode 100644 index 00000000000..a5880d7c2eb Binary files /dev/null and b/resources/images/icons/tiny_external@3x.png differ diff --git a/resources/images/icons/tiny_group2@2x.png b/resources/images/icons/tiny_group2@2x.png new file mode 100644 index 00000000000..b00fcb767e0 Binary files /dev/null and b/resources/images/icons/tiny_group2@2x.png differ diff --git a/resources/images/icons/tiny_group2@3x.png b/resources/images/icons/tiny_group2@3x.png new file mode 100644 index 00000000000..883657d87e1 Binary files /dev/null and b/resources/images/icons/tiny_group2@3x.png differ diff --git a/resources/images/icons/tiny_group@2x.png b/resources/images/icons/tiny_group@2x.png new file mode 100755 index 00000000000..96068768b20 Binary files /dev/null and b/resources/images/icons/tiny_group@2x.png differ diff --git a/resources/images/icons/tiny_group@3x.png b/resources/images/icons/tiny_group@3x.png new file mode 100755 index 00000000000..a9dfcb08830 Binary files /dev/null and b/resources/images/icons/tiny_group@3x.png differ diff --git a/resources/images/icons/tiny_lock@2x.png b/resources/images/icons/tiny_lock@2x.png new file mode 100644 index 00000000000..ca7d80c69ea Binary files /dev/null and b/resources/images/icons/tiny_lock@2x.png differ diff --git a/resources/images/icons/tiny_lock@3x.png b/resources/images/icons/tiny_lock@3x.png new file mode 100644 index 00000000000..b2effadae7a Binary files /dev/null and b/resources/images/icons/tiny_lock@3x.png differ diff --git a/resources/images/icons/tiny_lock_broken@2x.png b/resources/images/icons/tiny_lock_broken@2x.png new file mode 100644 index 00000000000..005a5e2a0a4 Binary files /dev/null and b/resources/images/icons/tiny_lock_broken@2x.png differ diff --git a/resources/images/icons/tiny_lock_broken@3x.png b/resources/images/icons/tiny_lock_broken@3x.png new file mode 100644 index 00000000000..235f9d8c0bd Binary files /dev/null and b/resources/images/icons/tiny_lock_broken@3x.png differ diff --git a/resources/images/icons/tiny_muted@2x.png b/resources/images/icons/tiny_muted@2x.png new file mode 100644 index 00000000000..fe82c38a86f Binary files /dev/null and b/resources/images/icons/tiny_muted@2x.png differ diff --git a/resources/images/icons/tiny_muted@3x.png b/resources/images/icons/tiny_muted@3x.png new file mode 100644 index 00000000000..7ca77d51f3a Binary files /dev/null and b/resources/images/icons/tiny_muted@3x.png differ diff --git a/resources/images/icons/tiny_new_contact@2x.png b/resources/images/icons/tiny_new_contact@2x.png new file mode 100755 index 00000000000..aea62fa8b9d Binary files /dev/null and b/resources/images/icons/tiny_new_contact@2x.png differ diff --git a/resources/images/icons/tiny_new_contact@3x.png b/resources/images/icons/tiny_new_contact@3x.png new file mode 100755 index 00000000000..48f40d3d128 Binary files /dev/null and b/resources/images/icons/tiny_new_contact@3x.png differ diff --git a/resources/images/icons/tiny_pending@2x.png b/resources/images/icons/tiny_pending@2x.png new file mode 100644 index 00000000000..14752545391 Binary files /dev/null and b/resources/images/icons/tiny_pending@2x.png differ diff --git a/resources/images/icons/tiny_pending@3x.png b/resources/images/icons/tiny_pending@3x.png new file mode 100644 index 00000000000..3ce31c088ae Binary files /dev/null and b/resources/images/icons/tiny_pending@3x.png differ diff --git a/resources/images/icons/tiny_public@2x.png b/resources/images/icons/tiny_public@2x.png new file mode 100755 index 00000000000..e1281bc1f50 Binary files /dev/null and b/resources/images/icons/tiny_public@2x.png differ diff --git a/resources/images/icons/tiny_public@3x.png b/resources/images/icons/tiny_public@3x.png new file mode 100755 index 00000000000..6156320bb6a Binary files /dev/null and b/resources/images/icons/tiny_public@3x.png differ diff --git a/resources/images/icons/tiny_reply@2x.png b/resources/images/icons/tiny_reply@2x.png new file mode 100644 index 00000000000..584b0bbccc2 Binary files /dev/null and b/resources/images/icons/tiny_reply@2x.png differ diff --git a/resources/images/icons/tiny_reply@3x.png b/resources/images/icons/tiny_reply@3x.png new file mode 100644 index 00000000000..2211f84461c Binary files /dev/null and b/resources/images/icons/tiny_reply@3x.png differ diff --git a/resources/images/icons/tiny_sent@2x.png b/resources/images/icons/tiny_sent@2x.png new file mode 100644 index 00000000000..446e9545195 Binary files /dev/null and b/resources/images/icons/tiny_sent@2x.png differ diff --git a/resources/images/icons/tiny_sent@3x.png b/resources/images/icons/tiny_sent@3x.png new file mode 100644 index 00000000000..4c8bd23abe2 Binary files /dev/null and b/resources/images/icons/tiny_sent@3x.png differ diff --git a/resources/images/icons/tiny_snt@2x.png b/resources/images/icons/tiny_snt@2x.png new file mode 100644 index 00000000000..83bc68ce28f Binary files /dev/null and b/resources/images/icons/tiny_snt@2x.png differ diff --git a/resources/images/icons/tiny_snt@3x.png b/resources/images/icons/tiny_snt@3x.png new file mode 100644 index 00000000000..7b61eeb42e7 Binary files /dev/null and b/resources/images/icons/tiny_snt@3x.png differ diff --git a/resources/images/icons/tiny_warning@2x.png b/resources/images/icons/tiny_warning@2x.png new file mode 100644 index 00000000000..f5b7436073a Binary files /dev/null and b/resources/images/icons/tiny_warning@2x.png differ diff --git a/resources/images/icons/tiny_warning@3x.png b/resources/images/icons/tiny_warning@3x.png new file mode 100644 index 00000000000..c4817a30bb8 Binary files /dev/null and b/resources/images/icons/tiny_warning@3x.png differ diff --git a/resources/images/icons/tiny_warning_background@2x.png b/resources/images/icons/tiny_warning_background@2x.png new file mode 100644 index 00000000000..007010fd258 Binary files /dev/null and b/resources/images/icons/tiny_warning_background@2x.png differ diff --git a/resources/images/icons/tiny_warning_background@3x.png b/resources/images/icons/tiny_warning_background@3x.png new file mode 100644 index 00000000000..6d1005d8cee Binary files /dev/null and b/resources/images/icons/tiny_warning_background@3x.png differ diff --git a/resources/images/icons/token@2x.png b/resources/images/icons/token@2x.png new file mode 100755 index 00000000000..eb966f63d20 Binary files /dev/null and b/resources/images/icons/token@2x.png differ diff --git a/resources/images/icons/token@3x.png b/resources/images/icons/token@3x.png new file mode 100755 index 00000000000..60e066e82ec Binary files /dev/null and b/resources/images/icons/token@3x.png differ diff --git a/resources/images/icons/tooltip_tip@2x.png b/resources/images/icons/tooltip_tip@2x.png new file mode 100644 index 00000000000..ebaaa0272d3 Binary files /dev/null and b/resources/images/icons/tooltip_tip@2x.png differ diff --git a/resources/images/icons/tooltip_tip@3x.png b/resources/images/icons/tooltip_tip@3x.png new file mode 100644 index 00000000000..eb70c36a015 Binary files /dev/null and b/resources/images/icons/tooltip_tip@3x.png differ diff --git a/resources/images/icons/total_members12@2x.png b/resources/images/icons/total_members12@2x.png new file mode 100644 index 00000000000..a400e2ed01c Binary files /dev/null and b/resources/images/icons/total_members12@2x.png differ diff --git a/resources/images/icons/total_members12@3x.png b/resources/images/icons/total_members12@3x.png new file mode 100644 index 00000000000..4b76d4176a8 Binary files /dev/null and b/resources/images/icons/total_members12@3x.png differ diff --git a/resources/images/icons/total_members16@2x.png b/resources/images/icons/total_members16@2x.png new file mode 100644 index 00000000000..f6c4b348e15 Binary files /dev/null and b/resources/images/icons/total_members16@2x.png differ diff --git a/resources/images/icons/total_members16@3x.png b/resources/images/icons/total_members16@3x.png new file mode 100644 index 00000000000..45a0b234b14 Binary files /dev/null and b/resources/images/icons/total_members16@3x.png differ diff --git a/resources/images/icons/total_members20@2x.png b/resources/images/icons/total_members20@2x.png new file mode 100644 index 00000000000..06029b3622a Binary files /dev/null and b/resources/images/icons/total_members20@2x.png differ diff --git a/resources/images/icons/total_members20@3x.png b/resources/images/icons/total_members20@3x.png new file mode 100644 index 00000000000..2dece6c26c2 Binary files /dev/null and b/resources/images/icons/total_members20@3x.png differ diff --git a/resources/images/icons/travel@2x.png b/resources/images/icons/travel@2x.png new file mode 100644 index 00000000000..9ab01686543 Binary files /dev/null and b/resources/images/icons/travel@2x.png differ diff --git a/resources/images/icons/travel@3x.png b/resources/images/icons/travel@3x.png new file mode 100644 index 00000000000..a15b97cba55 Binary files /dev/null and b/resources/images/icons/travel@3x.png differ diff --git a/resources/images/icons/tribute_to_talk@2x.png b/resources/images/icons/tribute_to_talk@2x.png new file mode 100644 index 00000000000..3226a74b533 Binary files /dev/null and b/resources/images/icons/tribute_to_talk@2x.png differ diff --git a/resources/images/icons/tribute_to_talk@3x.png b/resources/images/icons/tribute_to_talk@3x.png new file mode 100644 index 00000000000..a6ce5e2fa58 Binary files /dev/null and b/resources/images/icons/tribute_to_talk@3x.png differ diff --git a/resources/images/icons/two_arrows@2x.png b/resources/images/icons/two_arrows@2x.png new file mode 100755 index 00000000000..d242fd8dc09 Binary files /dev/null and b/resources/images/icons/two_arrows@2x.png differ diff --git a/resources/images/icons/two_arrows@3x.png b/resources/images/icons/two_arrows@3x.png new file mode 100755 index 00000000000..09e8bc8cff7 Binary files /dev/null and b/resources/images/icons/two_arrows@3x.png differ diff --git a/resources/images/icons/union_nfc@2x.png b/resources/images/icons/union_nfc@2x.png new file mode 100644 index 00000000000..4870dd05f73 Binary files /dev/null and b/resources/images/icons/union_nfc@2x.png differ diff --git a/resources/images/icons/union_nfc@3x.png b/resources/images/icons/union_nfc@3x.png new file mode 100644 index 00000000000..01ddc55359e Binary files /dev/null and b/resources/images/icons/union_nfc@3x.png differ diff --git a/resources/images/icons/user_profile@2x.png b/resources/images/icons/user_profile@2x.png new file mode 100755 index 00000000000..3b94d6563e5 Binary files /dev/null and b/resources/images/icons/user_profile@2x.png differ diff --git a/resources/images/icons/user_profile@3x.png b/resources/images/icons/user_profile@3x.png new file mode 100755 index 00000000000..b7b9b941c5b Binary files /dev/null and b/resources/images/icons/user_profile@3x.png differ diff --git a/resources/images/icons/username@2x.png b/resources/images/icons/username@2x.png new file mode 100755 index 00000000000..a555c5be59b Binary files /dev/null and b/resources/images/icons/username@2x.png differ diff --git a/resources/images/icons/username@3x.png b/resources/images/icons/username@3x.png new file mode 100755 index 00000000000..bab1ea14a23 Binary files /dev/null and b/resources/images/icons/username@3x.png differ diff --git a/resources/images/icons/wallet@2x.png b/resources/images/icons/wallet@2x.png new file mode 100755 index 00000000000..d6effc1c520 Binary files /dev/null and b/resources/images/icons/wallet@2x.png differ diff --git a/resources/images/icons/wallet@3x.png b/resources/images/icons/wallet@3x.png new file mode 100755 index 00000000000..c07657ea734 Binary files /dev/null and b/resources/images/icons/wallet@3x.png differ diff --git a/resources/images/icons/warning@2x.png b/resources/images/icons/warning@2x.png new file mode 100755 index 00000000000..ba17da1b896 Binary files /dev/null and b/resources/images/icons/warning@2x.png differ diff --git a/resources/images/icons/warning@3x.png b/resources/images/icons/warning@3x.png new file mode 100755 index 00000000000..91aefc0f08e Binary files /dev/null and b/resources/images/icons/warning@3x.png differ diff --git a/resources/images/icons2/12x12/Arrow-Up@2x.png b/resources/images/icons2/12x12/Arrow-Up@2x.png new file mode 100644 index 00000000000..fdf670bdbed Binary files /dev/null and b/resources/images/icons2/12x12/Arrow-Up@2x.png differ diff --git a/resources/images/icons2/12x12/Arrow-Up@3x.png b/resources/images/icons2/12x12/Arrow-Up@3x.png new file mode 100644 index 00000000000..1d65f09f51b Binary files /dev/null and b/resources/images/icons2/12x12/Arrow-Up@3x.png differ diff --git a/resources/images/icons2/12x12/Jump-to@2x.png b/resources/images/icons2/12x12/Jump-to@2x.png new file mode 100644 index 00000000000..2c20905862b Binary files /dev/null and b/resources/images/icons2/12x12/Jump-to@2x.png differ diff --git a/resources/images/icons2/12x12/Jump-to@3x.png b/resources/images/icons2/12x12/Jump-to@3x.png new file mode 100644 index 00000000000..a72a6858756 Binary files /dev/null and b/resources/images/icons2/12x12/Jump-to@3x.png differ diff --git a/resources/images/icons2/12x12/Lightning@2x.png b/resources/images/icons2/12x12/Lightning@2x.png new file mode 100644 index 00000000000..45a9b7035ee Binary files /dev/null and b/resources/images/icons2/12x12/Lightning@2x.png differ diff --git a/resources/images/icons2/12x12/Lightning@3x.png b/resources/images/icons2/12x12/Lightning@3x.png new file mode 100644 index 00000000000..07f1dd13488 Binary files /dev/null and b/resources/images/icons2/12x12/Lightning@3x.png differ diff --git a/resources/images/icons2/12x12/More@2x.png b/resources/images/icons2/12x12/More@2x.png new file mode 100644 index 00000000000..7a5a4f25fbe Binary files /dev/null and b/resources/images/icons2/12x12/More@2x.png differ diff --git a/resources/images/icons2/12x12/More@3x.png b/resources/images/icons2/12x12/More@3x.png new file mode 100644 index 00000000000..51caa7e5668 Binary files /dev/null and b/resources/images/icons2/12x12/More@3x.png differ diff --git a/resources/images/icons2/12x12/Total-Members@2x.png b/resources/images/icons2/12x12/Total-Members@2x.png new file mode 100644 index 00000000000..834569a502c Binary files /dev/null and b/resources/images/icons2/12x12/Total-Members@2x.png differ diff --git a/resources/images/icons2/12x12/Total-Members@3x.png b/resources/images/icons2/12x12/Total-Members@3x.png new file mode 100644 index 00000000000..9ac4b56689a Binary files /dev/null and b/resources/images/icons2/12x12/Total-Members@3x.png differ diff --git a/resources/images/icons2/12x12/Trash@2x.png b/resources/images/icons2/12x12/Trash@2x.png new file mode 100644 index 00000000000..72fae02f4b9 Binary files /dev/null and b/resources/images/icons2/12x12/Trash@2x.png differ diff --git a/resources/images/icons2/12x12/Trash@3x.png b/resources/images/icons2/12x12/Trash@3x.png new file mode 100644 index 00000000000..30be513a409 Binary files /dev/null and b/resources/images/icons2/12x12/Trash@3x.png differ diff --git a/resources/images/icons2/12x12/active-members@2x.png b/resources/images/icons2/12x12/active-members@2x.png new file mode 100644 index 00000000000..e12590df90c Binary files /dev/null and b/resources/images/icons2/12x12/active-members@2x.png differ diff --git a/resources/images/icons2/12x12/active-members@3x.png b/resources/images/icons2/12x12/active-members@3x.png new file mode 100644 index 00000000000..232dca9ea7a Binary files /dev/null and b/resources/images/icons2/12x12/active-members@3x.png differ diff --git a/resources/images/icons2/12x12/active@2x.png b/resources/images/icons2/12x12/active@2x.png new file mode 100644 index 00000000000..22302bb4352 Binary files /dev/null and b/resources/images/icons2/12x12/active@2x.png differ diff --git a/resources/images/icons2/12x12/active@3x.png b/resources/images/icons2/12x12/active@3x.png new file mode 100644 index 00000000000..30c6f939f9e Binary files /dev/null and b/resources/images/icons2/12x12/active@3x.png differ diff --git a/resources/images/icons2/12x12/add-circle@2x.png b/resources/images/icons2/12x12/add-circle@2x.png new file mode 100644 index 00000000000..bedbf75f068 Binary files /dev/null and b/resources/images/icons2/12x12/add-circle@2x.png differ diff --git a/resources/images/icons2/12x12/add-circle@3x.png b/resources/images/icons2/12x12/add-circle@3x.png new file mode 100644 index 00000000000..652a8060cd3 Binary files /dev/null and b/resources/images/icons2/12x12/add-circle@3x.png differ diff --git a/resources/images/icons2/12x12/add-reaction@2x.png b/resources/images/icons2/12x12/add-reaction@2x.png new file mode 100644 index 00000000000..89e58d68daa Binary files /dev/null and b/resources/images/icons2/12x12/add-reaction@2x.png differ diff --git a/resources/images/icons2/12x12/add-reaction@3x.png b/resources/images/icons2/12x12/add-reaction@3x.png new file mode 100644 index 00000000000..8771df74266 Binary files /dev/null and b/resources/images/icons2/12x12/add-reaction@3x.png differ diff --git a/resources/images/icons2/12x12/add-user@2x.png b/resources/images/icons2/12x12/add-user@2x.png new file mode 100644 index 00000000000..47385a38c56 Binary files /dev/null and b/resources/images/icons2/12x12/add-user@2x.png differ diff --git a/resources/images/icons2/12x12/add-user@3x.png b/resources/images/icons2/12x12/add-user@3x.png new file mode 100644 index 00000000000..8d679452807 Binary files /dev/null and b/resources/images/icons2/12x12/add-user@3x.png differ diff --git a/resources/images/icons2/12x12/add@2x.png b/resources/images/icons2/12x12/add@2x.png new file mode 100644 index 00000000000..590496d4be3 Binary files /dev/null and b/resources/images/icons2/12x12/add@2x.png differ diff --git a/resources/images/icons2/12x12/add@3x.png b/resources/images/icons2/12x12/add@3x.png new file mode 100644 index 00000000000..f1cbe34af61 Binary files /dev/null and b/resources/images/icons2/12x12/add@3x.png differ diff --git a/resources/images/icons2/12x12/alert@2x.png b/resources/images/icons2/12x12/alert@2x.png new file mode 100644 index 00000000000..0d491576a80 Binary files /dev/null and b/resources/images/icons2/12x12/alert@2x.png differ diff --git a/resources/images/icons2/12x12/alert@3x.png b/resources/images/icons2/12x12/alert@3x.png new file mode 100644 index 00000000000..caade5ba90d Binary files /dev/null and b/resources/images/icons2/12x12/alert@3x.png differ diff --git a/resources/images/icons2/12x12/arrow-down@2x.png b/resources/images/icons2/12x12/arrow-down@2x.png new file mode 100644 index 00000000000..4c9feac752a Binary files /dev/null and b/resources/images/icons2/12x12/arrow-down@2x.png differ diff --git a/resources/images/icons2/12x12/arrow-down@3x.png b/resources/images/icons2/12x12/arrow-down@3x.png new file mode 100644 index 00000000000..1a269a6b945 Binary files /dev/null and b/resources/images/icons2/12x12/arrow-down@3x.png differ diff --git a/resources/images/icons2/12x12/arrow-left-right@2x.png b/resources/images/icons2/12x12/arrow-left-right@2x.png new file mode 100644 index 00000000000..bc552e1e0f6 Binary files /dev/null and b/resources/images/icons2/12x12/arrow-left-right@2x.png differ diff --git a/resources/images/icons2/12x12/arrow-left-right@3x.png b/resources/images/icons2/12x12/arrow-left-right@3x.png new file mode 100644 index 00000000000..aaaeffe2bd7 Binary files /dev/null and b/resources/images/icons2/12x12/arrow-left-right@3x.png differ diff --git a/resources/images/icons2/12x12/arrow-left@2x.png b/resources/images/icons2/12x12/arrow-left@2x.png new file mode 100644 index 00000000000..3760753c606 Binary files /dev/null and b/resources/images/icons2/12x12/arrow-left@2x.png differ diff --git a/resources/images/icons2/12x12/arrow-left@3x.png b/resources/images/icons2/12x12/arrow-left@3x.png new file mode 100644 index 00000000000..e36ffe628c8 Binary files /dev/null and b/resources/images/icons2/12x12/arrow-left@3x.png differ diff --git a/resources/images/icons2/12x12/arrow-right@2x.png b/resources/images/icons2/12x12/arrow-right@2x.png new file mode 100644 index 00000000000..ec86143182f Binary files /dev/null and b/resources/images/icons2/12x12/arrow-right@2x.png differ diff --git a/resources/images/icons2/12x12/arrow-right@3x.png b/resources/images/icons2/12x12/arrow-right@3x.png new file mode 100644 index 00000000000..995159bee49 Binary files /dev/null and b/resources/images/icons2/12x12/arrow-right@3x.png differ diff --git a/resources/images/icons2/12x12/arrow-top@2x.png b/resources/images/icons2/12x12/arrow-top@2x.png new file mode 100644 index 00000000000..778f9becf12 Binary files /dev/null and b/resources/images/icons2/12x12/arrow-top@2x.png differ diff --git a/resources/images/icons2/12x12/arrow-top@3x.png b/resources/images/icons2/12x12/arrow-top@3x.png new file mode 100644 index 00000000000..afa2df695df Binary files /dev/null and b/resources/images/icons2/12x12/arrow-top@3x.png differ diff --git a/resources/images/icons2/12x12/block@2x.png b/resources/images/icons2/12x12/block@2x.png new file mode 100644 index 00000000000..e6b870c407f Binary files /dev/null and b/resources/images/icons2/12x12/block@2x.png differ diff --git a/resources/images/icons2/12x12/block@3x.png b/resources/images/icons2/12x12/block@3x.png new file mode 100644 index 00000000000..2ac64330831 Binary files /dev/null and b/resources/images/icons2/12x12/block@3x.png differ diff --git a/resources/images/icons2/12x12/browser@2x.png b/resources/images/icons2/12x12/browser@2x.png new file mode 100644 index 00000000000..946eb935a58 Binary files /dev/null and b/resources/images/icons2/12x12/browser@2x.png differ diff --git a/resources/images/icons2/12x12/browser@3x.png b/resources/images/icons2/12x12/browser@3x.png new file mode 100644 index 00000000000..c2a78fbe2a4 Binary files /dev/null and b/resources/images/icons2/12x12/browser@3x.png differ diff --git a/resources/images/icons2/12x12/camera@2x.png b/resources/images/icons2/12x12/camera@2x.png new file mode 100644 index 00000000000..80fdef1947e Binary files /dev/null and b/resources/images/icons2/12x12/camera@2x.png differ diff --git a/resources/images/icons2/12x12/camera@3x.png b/resources/images/icons2/12x12/camera@3x.png new file mode 100644 index 00000000000..1c5b200043d Binary files /dev/null and b/resources/images/icons2/12x12/camera@3x.png differ diff --git a/resources/images/icons2/12x12/cards@2x.png b/resources/images/icons2/12x12/cards@2x.png new file mode 100644 index 00000000000..3ccba676234 Binary files /dev/null and b/resources/images/icons2/12x12/cards@2x.png differ diff --git a/resources/images/icons2/12x12/cards@3x.png b/resources/images/icons2/12x12/cards@3x.png new file mode 100644 index 00000000000..f2eb2f23c65 Binary files /dev/null and b/resources/images/icons2/12x12/cards@3x.png differ diff --git a/resources/images/icons2/12x12/check-circle@2x.png b/resources/images/icons2/12x12/check-circle@2x.png new file mode 100644 index 00000000000..07ce5b3e475 Binary files /dev/null and b/resources/images/icons2/12x12/check-circle@2x.png differ diff --git a/resources/images/icons2/12x12/check-circle@3x.png b/resources/images/icons2/12x12/check-circle@3x.png new file mode 100644 index 00000000000..0a9f5febdc3 Binary files /dev/null and b/resources/images/icons2/12x12/check-circle@3x.png differ diff --git a/resources/images/icons2/12x12/check-large@2x.png b/resources/images/icons2/12x12/check-large@2x.png new file mode 100644 index 00000000000..6cd5fda4678 Binary files /dev/null and b/resources/images/icons2/12x12/check-large@2x.png differ diff --git a/resources/images/icons2/12x12/check-large@3x.png b/resources/images/icons2/12x12/check-large@3x.png new file mode 100644 index 00000000000..695121ada01 Binary files /dev/null and b/resources/images/icons2/12x12/check-large@3x.png differ diff --git a/resources/images/icons2/12x12/check@2x.png b/resources/images/icons2/12x12/check@2x.png new file mode 100644 index 00000000000..fcb9b8e984b Binary files /dev/null and b/resources/images/icons2/12x12/check@2x.png differ diff --git a/resources/images/icons2/12x12/check@3x.png b/resources/images/icons2/12x12/check@3x.png new file mode 100644 index 00000000000..0e2f134e592 Binary files /dev/null and b/resources/images/icons2/12x12/check@3x.png differ diff --git a/resources/images/icons2/12x12/chevron-down@2x.png b/resources/images/icons2/12x12/chevron-down@2x.png new file mode 100644 index 00000000000..f1eb4af1715 Binary files /dev/null and b/resources/images/icons2/12x12/chevron-down@2x.png differ diff --git a/resources/images/icons2/12x12/chevron-down@3x.png b/resources/images/icons2/12x12/chevron-down@3x.png new file mode 100644 index 00000000000..372be2b623a Binary files /dev/null and b/resources/images/icons2/12x12/chevron-down@3x.png differ diff --git a/resources/images/icons2/12x12/chevron-left@2x.png b/resources/images/icons2/12x12/chevron-left@2x.png new file mode 100644 index 00000000000..b868cd22926 Binary files /dev/null and b/resources/images/icons2/12x12/chevron-left@2x.png differ diff --git a/resources/images/icons2/12x12/chevron-left@3x.png b/resources/images/icons2/12x12/chevron-left@3x.png new file mode 100644 index 00000000000..30336affc51 Binary files /dev/null and b/resources/images/icons2/12x12/chevron-left@3x.png differ diff --git a/resources/images/icons2/12x12/chevron-right@2x.png b/resources/images/icons2/12x12/chevron-right@2x.png new file mode 100644 index 00000000000..015ce168f57 Binary files /dev/null and b/resources/images/icons2/12x12/chevron-right@2x.png differ diff --git a/resources/images/icons2/12x12/chevron-right@3x.png b/resources/images/icons2/12x12/chevron-right@3x.png new file mode 100644 index 00000000000..56eaff7df93 Binary files /dev/null and b/resources/images/icons2/12x12/chevron-right@3x.png differ diff --git a/resources/images/icons2/12x12/chevron-top@2x.png b/resources/images/icons2/12x12/chevron-top@2x.png new file mode 100644 index 00000000000..235c6376df1 Binary files /dev/null and b/resources/images/icons2/12x12/chevron-top@2x.png differ diff --git a/resources/images/icons2/12x12/chevron-top@3x.png b/resources/images/icons2/12x12/chevron-top@3x.png new file mode 100644 index 00000000000..863cbff6bf3 Binary files /dev/null and b/resources/images/icons2/12x12/chevron-top@3x.png differ diff --git a/resources/images/icons2/12x12/close@2x.png b/resources/images/icons2/12x12/close@2x.png new file mode 100644 index 00000000000..76dcf42edef Binary files /dev/null and b/resources/images/icons2/12x12/close@2x.png differ diff --git a/resources/images/icons2/12x12/close@3x.png b/resources/images/icons2/12x12/close@3x.png new file mode 100644 index 00000000000..1aa8d7f9a32 Binary files /dev/null and b/resources/images/icons2/12x12/close@3x.png differ diff --git a/resources/images/icons2/12x12/color@2x-1.png b/resources/images/icons2/12x12/color@2x-1.png new file mode 100644 index 00000000000..c4b76359905 Binary files /dev/null and b/resources/images/icons2/12x12/color@2x-1.png differ diff --git a/resources/images/icons2/12x12/color@2x.png b/resources/images/icons2/12x12/color@2x.png new file mode 100644 index 00000000000..5df5918ca4c Binary files /dev/null and b/resources/images/icons2/12x12/color@2x.png differ diff --git a/resources/images/icons2/12x12/color@3x-1.png b/resources/images/icons2/12x12/color@3x-1.png new file mode 100644 index 00000000000..ba344d3de0f Binary files /dev/null and b/resources/images/icons2/12x12/color@3x-1.png differ diff --git a/resources/images/icons2/12x12/color@3x.png b/resources/images/icons2/12x12/color@3x.png new file mode 100644 index 00000000000..b56bb1d42b1 Binary files /dev/null and b/resources/images/icons2/12x12/color@3x.png differ diff --git a/resources/images/icons2/12x12/command@2x.png b/resources/images/icons2/12x12/command@2x.png new file mode 100644 index 00000000000..77745036b03 Binary files /dev/null and b/resources/images/icons2/12x12/command@2x.png differ diff --git a/resources/images/icons2/12x12/command@3x.png b/resources/images/icons2/12x12/command@3x.png new file mode 100644 index 00000000000..3f9873752f7 Binary files /dev/null and b/resources/images/icons2/12x12/command@3x.png differ diff --git a/resources/images/icons2/12x12/communities@2x.png b/resources/images/icons2/12x12/communities@2x.png new file mode 100644 index 00000000000..059ce1b8538 Binary files /dev/null and b/resources/images/icons2/12x12/communities@2x.png differ diff --git a/resources/images/icons2/12x12/communities@3x.png b/resources/images/icons2/12x12/communities@3x.png new file mode 100644 index 00000000000..23efbcd6705 Binary files /dev/null and b/resources/images/icons2/12x12/communities@3x.png differ diff --git a/resources/images/icons2/12x12/contact@2x.png b/resources/images/icons2/12x12/contact@2x.png new file mode 100644 index 00000000000..900bc5889b1 Binary files /dev/null and b/resources/images/icons2/12x12/contact@2x.png differ diff --git a/resources/images/icons2/12x12/contact@3x.png b/resources/images/icons2/12x12/contact@3x.png new file mode 100644 index 00000000000..dc466fd8d3a Binary files /dev/null and b/resources/images/icons2/12x12/contact@3x.png differ diff --git a/resources/images/icons2/12x12/copy@2x.png b/resources/images/icons2/12x12/copy@2x.png new file mode 100644 index 00000000000..ca1166a2b13 Binary files /dev/null and b/resources/images/icons2/12x12/copy@2x.png differ diff --git a/resources/images/icons2/12x12/copy@3x.png b/resources/images/icons2/12x12/copy@3x.png new file mode 100644 index 00000000000..e1ee1fe1a97 Binary files /dev/null and b/resources/images/icons2/12x12/copy@3x.png differ diff --git a/resources/images/icons2/12x12/crown@2x.png b/resources/images/icons2/12x12/crown@2x.png new file mode 100644 index 00000000000..4bab704f259 Binary files /dev/null and b/resources/images/icons2/12x12/crown@2x.png differ diff --git a/resources/images/icons2/12x12/crown@3x.png b/resources/images/icons2/12x12/crown@3x.png new file mode 100644 index 00000000000..235e367c032 Binary files /dev/null and b/resources/images/icons2/12x12/crown@3x.png differ diff --git a/resources/images/icons2/12x12/customize@2x.png b/resources/images/icons2/12x12/customize@2x.png new file mode 100644 index 00000000000..2f0113d85f5 Binary files /dev/null and b/resources/images/icons2/12x12/customize@2x.png differ diff --git a/resources/images/icons2/12x12/customize@3x.png b/resources/images/icons2/12x12/customize@3x.png new file mode 100644 index 00000000000..7245775f21f Binary files /dev/null and b/resources/images/icons2/12x12/customize@3x.png differ diff --git a/resources/images/icons2/12x12/decline@2x.png b/resources/images/icons2/12x12/decline@2x.png new file mode 100644 index 00000000000..f9ae4b8bdff Binary files /dev/null and b/resources/images/icons2/12x12/decline@2x.png differ diff --git a/resources/images/icons2/12x12/decline@3x.png b/resources/images/icons2/12x12/decline@3x.png new file mode 100644 index 00000000000..d94f4e02226 Binary files /dev/null and b/resources/images/icons2/12x12/decline@3x.png differ diff --git a/resources/images/icons2/12x12/delete@2x.png b/resources/images/icons2/12x12/delete@2x.png new file mode 100644 index 00000000000..7d34a78810a Binary files /dev/null and b/resources/images/icons2/12x12/delete@2x.png differ diff --git a/resources/images/icons2/12x12/delete@3x.png b/resources/images/icons2/12x12/delete@3x.png new file mode 100644 index 00000000000..f9c759fd104 Binary files /dev/null and b/resources/images/icons2/12x12/delete@3x.png differ diff --git a/resources/images/icons2/12x12/delivered@2x.png b/resources/images/icons2/12x12/delivered@2x.png new file mode 100644 index 00000000000..996afeb2127 Binary files /dev/null and b/resources/images/icons2/12x12/delivered@2x.png differ diff --git a/resources/images/icons2/12x12/delivered@3x.png b/resources/images/icons2/12x12/delivered@3x.png new file mode 100644 index 00000000000..6a03a713f2a Binary files /dev/null and b/resources/images/icons2/12x12/delivered@3x.png differ diff --git a/resources/images/icons2/12x12/desktop@2x.png b/resources/images/icons2/12x12/desktop@2x.png new file mode 100644 index 00000000000..f4f167dc769 Binary files /dev/null and b/resources/images/icons2/12x12/desktop@2x.png differ diff --git a/resources/images/icons2/12x12/desktop@3x.png b/resources/images/icons2/12x12/desktop@3x.png new file mode 100644 index 00000000000..a2e7fbdc583 Binary files /dev/null and b/resources/images/icons2/12x12/desktop@3x.png differ diff --git a/resources/images/icons2/12x12/diamond@2x.png b/resources/images/icons2/12x12/diamond@2x.png new file mode 100644 index 00000000000..a78fbe23fc9 Binary files /dev/null and b/resources/images/icons2/12x12/diamond@2x.png differ diff --git a/resources/images/icons2/12x12/diamond@3x.png b/resources/images/icons2/12x12/diamond@3x.png new file mode 100644 index 00000000000..84761f65265 Binary files /dev/null and b/resources/images/icons2/12x12/diamond@3x.png differ diff --git a/resources/images/icons2/12x12/done@2x.png b/resources/images/icons2/12x12/done@2x.png new file mode 100644 index 00000000000..f8fc2982990 Binary files /dev/null and b/resources/images/icons2/12x12/done@2x.png differ diff --git a/resources/images/icons2/12x12/done@3x.png b/resources/images/icons2/12x12/done@3x.png new file mode 100644 index 00000000000..480d5e2ffa5 Binary files /dev/null and b/resources/images/icons2/12x12/done@3x.png differ diff --git a/resources/images/icons2/12x12/download@2x.png b/resources/images/icons2/12x12/download@2x.png new file mode 100644 index 00000000000..4ac0be242fb Binary files /dev/null and b/resources/images/icons2/12x12/download@2x.png differ diff --git a/resources/images/icons2/12x12/download@3x.png b/resources/images/icons2/12x12/download@3x.png new file mode 100644 index 00000000000..f94a111e29b Binary files /dev/null and b/resources/images/icons2/12x12/download@3x.png differ diff --git a/resources/images/icons2/12x12/dropdown@2x.png b/resources/images/icons2/12x12/dropdown@2x.png new file mode 100644 index 00000000000..73806c66120 Binary files /dev/null and b/resources/images/icons2/12x12/dropdown@2x.png differ diff --git a/resources/images/icons2/12x12/dropdown@3x.png b/resources/images/icons2/12x12/dropdown@3x.png new file mode 100644 index 00000000000..6a6f0ecbf68 Binary files /dev/null and b/resources/images/icons2/12x12/dropdown@3x.png differ diff --git a/resources/images/icons2/12x12/edit@2x.png b/resources/images/icons2/12x12/edit@2x.png new file mode 100644 index 00000000000..6b8c374d7b5 Binary files /dev/null and b/resources/images/icons2/12x12/edit@2x.png differ diff --git a/resources/images/icons2/12x12/edit@3x.png b/resources/images/icons2/12x12/edit@3x.png new file mode 100644 index 00000000000..b59121cae2b Binary files /dev/null and b/resources/images/icons2/12x12/edit@3x.png differ diff --git a/resources/images/icons2/12x12/external@2x.png b/resources/images/icons2/12x12/external@2x.png new file mode 100644 index 00000000000..71ad5aa28e4 Binary files /dev/null and b/resources/images/icons2/12x12/external@2x.png differ diff --git a/resources/images/icons2/12x12/external@3x.png b/resources/images/icons2/12x12/external@3x.png new file mode 100644 index 00000000000..02299a9c643 Binary files /dev/null and b/resources/images/icons2/12x12/external@3x.png differ diff --git a/resources/images/icons2/12x12/face-id@2x.png b/resources/images/icons2/12x12/face-id@2x.png new file mode 100644 index 00000000000..f9b5d8023b4 Binary files /dev/null and b/resources/images/icons2/12x12/face-id@2x.png differ diff --git a/resources/images/icons2/12x12/face-id@3x.png b/resources/images/icons2/12x12/face-id@3x.png new file mode 100644 index 00000000000..99f905cf106 Binary files /dev/null and b/resources/images/icons2/12x12/face-id@3x.png differ diff --git a/resources/images/icons2/12x12/fees@2x.png b/resources/images/icons2/12x12/fees@2x.png new file mode 100644 index 00000000000..0fbc6a0693f Binary files /dev/null and b/resources/images/icons2/12x12/fees@2x.png differ diff --git a/resources/images/icons2/12x12/fees@3x.png b/resources/images/icons2/12x12/fees@3x.png new file mode 100644 index 00000000000..b617a6357e5 Binary files /dev/null and b/resources/images/icons2/12x12/fees@3x.png differ diff --git a/resources/images/icons2/12x12/flag@2x.png b/resources/images/icons2/12x12/flag@2x.png new file mode 100644 index 00000000000..63c12b2c1e4 Binary files /dev/null and b/resources/images/icons2/12x12/flag@2x.png differ diff --git a/resources/images/icons2/12x12/flag@3x.png b/resources/images/icons2/12x12/flag@3x.png new file mode 100644 index 00000000000..d2aec6d50a6 Binary files /dev/null and b/resources/images/icons2/12x12/flag@3x.png differ diff --git a/resources/images/icons2/12x12/friend@2x.png b/resources/images/icons2/12x12/friend@2x.png new file mode 100644 index 00000000000..5afdc708ab9 Binary files /dev/null and b/resources/images/icons2/12x12/friend@2x.png differ diff --git a/resources/images/icons2/12x12/friend@3x.png b/resources/images/icons2/12x12/friend@3x.png new file mode 100644 index 00000000000..4138da5cf03 Binary files /dev/null and b/resources/images/icons2/12x12/friend@3x.png differ diff --git a/resources/images/icons2/12x12/gas@2x.png b/resources/images/icons2/12x12/gas@2x.png new file mode 100644 index 00000000000..26aef864500 Binary files /dev/null and b/resources/images/icons2/12x12/gas@2x.png differ diff --git a/resources/images/icons2/12x12/gas@3x.png b/resources/images/icons2/12x12/gas@3x.png new file mode 100644 index 00000000000..3020ce68db6 Binary files /dev/null and b/resources/images/icons2/12x12/gas@3x.png differ diff --git a/resources/images/icons2/12x12/gavel@2x.png b/resources/images/icons2/12x12/gavel@2x.png new file mode 100644 index 00000000000..647ef1440b7 Binary files /dev/null and b/resources/images/icons2/12x12/gavel@2x.png differ diff --git a/resources/images/icons2/12x12/gavel@3x.png b/resources/images/icons2/12x12/gavel@3x.png new file mode 100644 index 00000000000..7fbd5d6fe84 Binary files /dev/null and b/resources/images/icons2/12x12/gavel@3x.png differ diff --git a/resources/images/icons2/12x12/git@2x.png b/resources/images/icons2/12x12/git@2x.png new file mode 100644 index 00000000000..9b95a424709 Binary files /dev/null and b/resources/images/icons2/12x12/git@2x.png differ diff --git a/resources/images/icons2/12x12/git@3x.png b/resources/images/icons2/12x12/git@3x.png new file mode 100644 index 00000000000..a776af8ba30 Binary files /dev/null and b/resources/images/icons2/12x12/git@3x.png differ diff --git a/resources/images/icons2/12x12/group@2x.png b/resources/images/icons2/12x12/group@2x.png new file mode 100644 index 00000000000..834569a502c Binary files /dev/null and b/resources/images/icons2/12x12/group@2x.png differ diff --git a/resources/images/icons2/12x12/group@3x.png b/resources/images/icons2/12x12/group@3x.png new file mode 100644 index 00000000000..9ac4b56689a Binary files /dev/null and b/resources/images/icons2/12x12/group@3x.png differ diff --git a/resources/images/icons2/12x12/hide@2x.png b/resources/images/icons2/12x12/hide@2x.png new file mode 100644 index 00000000000..bf86017e82b Binary files /dev/null and b/resources/images/icons2/12x12/hide@2x.png differ diff --git a/resources/images/icons2/12x12/hide@3x.png b/resources/images/icons2/12x12/hide@3x.png new file mode 100644 index 00000000000..f4162589b9c Binary files /dev/null and b/resources/images/icons2/12x12/hide@3x.png differ diff --git a/resources/images/icons2/12x12/hold@2x.png b/resources/images/icons2/12x12/hold@2x.png new file mode 100644 index 00000000000..4cfbbfa55de Binary files /dev/null and b/resources/images/icons2/12x12/hold@2x.png differ diff --git a/resources/images/icons2/12x12/hold@3x.png b/resources/images/icons2/12x12/hold@3x.png new file mode 100644 index 00000000000..b6b1d21e490 Binary files /dev/null and b/resources/images/icons2/12x12/hold@3x.png differ diff --git a/resources/images/icons2/12x12/inactive@2x.png b/resources/images/icons2/12x12/inactive@2x.png new file mode 100644 index 00000000000..ff17c8ad0b2 Binary files /dev/null and b/resources/images/icons2/12x12/inactive@2x.png differ diff --git a/resources/images/icons2/12x12/inactive@3x.png b/resources/images/icons2/12x12/inactive@3x.png new file mode 100644 index 00000000000..ab310eb3fc7 Binary files /dev/null and b/resources/images/icons2/12x12/inactive@3x.png differ diff --git a/resources/images/icons2/12x12/info@2x-1.png b/resources/images/icons2/12x12/info@2x-1.png new file mode 100644 index 00000000000..e5db750f029 Binary files /dev/null and b/resources/images/icons2/12x12/info@2x-1.png differ diff --git a/resources/images/icons2/12x12/info@2x.png b/resources/images/icons2/12x12/info@2x.png new file mode 100644 index 00000000000..f6209c52a7e Binary files /dev/null and b/resources/images/icons2/12x12/info@2x.png differ diff --git a/resources/images/icons2/12x12/info@3x-1.png b/resources/images/icons2/12x12/info@3x-1.png new file mode 100644 index 00000000000..5528c9d8d9b Binary files /dev/null and b/resources/images/icons2/12x12/info@3x-1.png differ diff --git a/resources/images/icons2/12x12/info@3x.png b/resources/images/icons2/12x12/info@3x.png new file mode 100644 index 00000000000..69181ff836b Binary files /dev/null and b/resources/images/icons2/12x12/info@3x.png differ diff --git a/resources/images/icons2/12x12/link@2x.png b/resources/images/icons2/12x12/link@2x.png new file mode 100644 index 00000000000..fd7fa77dce0 Binary files /dev/null and b/resources/images/icons2/12x12/link@2x.png differ diff --git a/resources/images/icons2/12x12/link@3x.png b/resources/images/icons2/12x12/link@3x.png new file mode 100644 index 00000000000..081cee7a2da Binary files /dev/null and b/resources/images/icons2/12x12/link@3x.png differ diff --git a/resources/images/icons2/12x12/list@2x.png b/resources/images/icons2/12x12/list@2x.png new file mode 100644 index 00000000000..4a9e6927e68 Binary files /dev/null and b/resources/images/icons2/12x12/list@2x.png differ diff --git a/resources/images/icons2/12x12/list@3x.png b/resources/images/icons2/12x12/list@3x.png new file mode 100644 index 00000000000..3a2eff64316 Binary files /dev/null and b/resources/images/icons2/12x12/list@3x.png differ diff --git a/resources/images/icons2/12x12/loading@2x.png b/resources/images/icons2/12x12/loading@2x.png new file mode 100644 index 00000000000..b2e7c78e77a Binary files /dev/null and b/resources/images/icons2/12x12/loading@2x.png differ diff --git a/resources/images/icons2/12x12/loading@3x.png b/resources/images/icons2/12x12/loading@3x.png new file mode 100644 index 00000000000..2532811698a Binary files /dev/null and b/resources/images/icons2/12x12/loading@3x.png differ diff --git a/resources/images/icons2/12x12/locked@2x.png b/resources/images/icons2/12x12/locked@2x.png new file mode 100644 index 00000000000..795d53cfeff Binary files /dev/null and b/resources/images/icons2/12x12/locked@2x.png differ diff --git a/resources/images/icons2/12x12/locked@3x.png b/resources/images/icons2/12x12/locked@3x.png new file mode 100644 index 00000000000..6d530e1650b Binary files /dev/null and b/resources/images/icons2/12x12/locked@3x.png differ diff --git a/resources/images/icons2/12x12/members@2x.png b/resources/images/icons2/12x12/members@2x.png new file mode 100644 index 00000000000..940f327d0d5 Binary files /dev/null and b/resources/images/icons2/12x12/members@2x.png differ diff --git a/resources/images/icons2/12x12/members@3x.png b/resources/images/icons2/12x12/members@3x.png new file mode 100644 index 00000000000..7ee75a92cbb Binary files /dev/null and b/resources/images/icons2/12x12/members@3x.png differ diff --git a/resources/images/icons2/12x12/mention@2x.png b/resources/images/icons2/12x12/mention@2x.png new file mode 100644 index 00000000000..c2f1709d94a Binary files /dev/null and b/resources/images/icons2/12x12/mention@2x.png differ diff --git a/resources/images/icons2/12x12/mention@3x.png b/resources/images/icons2/12x12/mention@3x.png new file mode 100644 index 00000000000..28c8ec2a4aa Binary files /dev/null and b/resources/images/icons2/12x12/mention@3x.png differ diff --git a/resources/images/icons2/12x12/mobile@2x.png b/resources/images/icons2/12x12/mobile@2x.png new file mode 100644 index 00000000000..c5315b138b2 Binary files /dev/null and b/resources/images/icons2/12x12/mobile@2x.png differ diff --git a/resources/images/icons2/12x12/mobile@3x.png b/resources/images/icons2/12x12/mobile@3x.png new file mode 100644 index 00000000000..9c4eec4ae43 Binary files /dev/null and b/resources/images/icons2/12x12/mobile@3x.png differ diff --git a/resources/images/icons2/12x12/mutual-contact@2x.png b/resources/images/icons2/12x12/mutual-contact@2x.png new file mode 100644 index 00000000000..226c96293b9 Binary files /dev/null and b/resources/images/icons2/12x12/mutual-contact@2x.png differ diff --git a/resources/images/icons2/12x12/mutual-contact@3x.png b/resources/images/icons2/12x12/mutual-contact@3x.png new file mode 100644 index 00000000000..c050c9cb049 Binary files /dev/null and b/resources/images/icons2/12x12/mutual-contact@3x.png differ diff --git a/resources/images/icons2/12x12/negative-state@2x.png b/resources/images/icons2/12x12/negative-state@2x.png new file mode 100644 index 00000000000..a1fb88e6f0f Binary files /dev/null and b/resources/images/icons2/12x12/negative-state@2x.png differ diff --git a/resources/images/icons2/12x12/negative-state@3x.png b/resources/images/icons2/12x12/negative-state@3x.png new file mode 100644 index 00000000000..347592a41f6 Binary files /dev/null and b/resources/images/icons2/12x12/negative-state@3x.png differ diff --git a/resources/images/icons2/12x12/negative@2x.png b/resources/images/icons2/12x12/negative@2x.png new file mode 100644 index 00000000000..3a744c31eff Binary files /dev/null and b/resources/images/icons2/12x12/negative@2x.png differ diff --git a/resources/images/icons2/12x12/negative@3x.png b/resources/images/icons2/12x12/negative@3x.png new file mode 100644 index 00000000000..27949e49f46 Binary files /dev/null and b/resources/images/icons2/12x12/negative@3x.png differ diff --git a/resources/images/icons2/12x12/notification@2x.png b/resources/images/icons2/12x12/notification@2x.png new file mode 100644 index 00000000000..c4b76359905 Binary files /dev/null and b/resources/images/icons2/12x12/notification@2x.png differ diff --git a/resources/images/icons2/12x12/notification@3x.png b/resources/images/icons2/12x12/notification@3x.png new file mode 100644 index 00000000000..ba344d3de0f Binary files /dev/null and b/resources/images/icons2/12x12/notification@3x.png differ diff --git a/resources/images/icons2/12x12/open-state@2x.png b/resources/images/icons2/12x12/open-state@2x.png new file mode 100644 index 00000000000..1c7b0f8a5fc Binary files /dev/null and b/resources/images/icons2/12x12/open-state@2x.png differ diff --git a/resources/images/icons2/12x12/open-state@3x.png b/resources/images/icons2/12x12/open-state@3x.png new file mode 100644 index 00000000000..33f01ff068c Binary files /dev/null and b/resources/images/icons2/12x12/open-state@3x.png differ diff --git a/resources/images/icons2/12x12/options@2x.png b/resources/images/icons2/12x12/options@2x.png new file mode 100644 index 00000000000..f8ca64a47e0 Binary files /dev/null and b/resources/images/icons2/12x12/options@2x.png differ diff --git a/resources/images/icons2/12x12/options@3x.png b/resources/images/icons2/12x12/options@3x.png new file mode 100644 index 00000000000..1a87c6595b0 Binary files /dev/null and b/resources/images/icons2/12x12/options@3x.png differ diff --git a/resources/images/icons2/12x12/password@2x.png b/resources/images/icons2/12x12/password@2x.png new file mode 100644 index 00000000000..ded220f72fd Binary files /dev/null and b/resources/images/icons2/12x12/password@2x.png differ diff --git a/resources/images/icons2/12x12/password@3x.png b/resources/images/icons2/12x12/password@3x.png new file mode 100644 index 00000000000..d1a554005bd Binary files /dev/null and b/resources/images/icons2/12x12/password@3x.png differ diff --git a/resources/images/icons2/12x12/pause@2x.png b/resources/images/icons2/12x12/pause@2x.png new file mode 100644 index 00000000000..86a70b00917 Binary files /dev/null and b/resources/images/icons2/12x12/pause@2x.png differ diff --git a/resources/images/icons2/12x12/pause@3x.png b/resources/images/icons2/12x12/pause@3x.png new file mode 100644 index 00000000000..5c772f37b2b Binary files /dev/null and b/resources/images/icons2/12x12/pause@3x.png differ diff --git a/resources/images/icons2/12x12/pending-dark-blur@2x.png b/resources/images/icons2/12x12/pending-dark-blur@2x.png new file mode 100644 index 00000000000..cb39e1a630f Binary files /dev/null and b/resources/images/icons2/12x12/pending-dark-blur@2x.png differ diff --git a/resources/images/icons2/12x12/pending-dark-blur@3x.png b/resources/images/icons2/12x12/pending-dark-blur@3x.png new file mode 100644 index 00000000000..6a7b83a7b4e Binary files /dev/null and b/resources/images/icons2/12x12/pending-dark-blur@3x.png differ diff --git a/resources/images/icons2/12x12/pending-dark@2x.png b/resources/images/icons2/12x12/pending-dark@2x.png new file mode 100644 index 00000000000..5d752740e25 Binary files /dev/null and b/resources/images/icons2/12x12/pending-dark@2x.png differ diff --git a/resources/images/icons2/12x12/pending-dark@3x.png b/resources/images/icons2/12x12/pending-dark@3x.png new file mode 100644 index 00000000000..2856d93998f Binary files /dev/null and b/resources/images/icons2/12x12/pending-dark@3x.png differ diff --git a/resources/images/icons2/12x12/pending-light@2x.png b/resources/images/icons2/12x12/pending-light@2x.png new file mode 100644 index 00000000000..5a7706fc6a1 Binary files /dev/null and b/resources/images/icons2/12x12/pending-light@2x.png differ diff --git a/resources/images/icons2/12x12/pending-light@3x.png b/resources/images/icons2/12x12/pending-light@3x.png new file mode 100644 index 00000000000..12778ca12ac Binary files /dev/null and b/resources/images/icons2/12x12/pending-light@3x.png differ diff --git a/resources/images/icons2/12x12/pending-state@2x.png b/resources/images/icons2/12x12/pending-state@2x.png new file mode 100644 index 00000000000..5a7706fc6a1 Binary files /dev/null and b/resources/images/icons2/12x12/pending-state@2x.png differ diff --git a/resources/images/icons2/12x12/pending-state@3x.png b/resources/images/icons2/12x12/pending-state@3x.png new file mode 100644 index 00000000000..12778ca12ac Binary files /dev/null and b/resources/images/icons2/12x12/pending-state@3x.png differ diff --git a/resources/images/icons2/12x12/pending@2x.png b/resources/images/icons2/12x12/pending@2x.png new file mode 100644 index 00000000000..4e4445c101e Binary files /dev/null and b/resources/images/icons2/12x12/pending@2x.png differ diff --git a/resources/images/icons2/12x12/pending@3x.png b/resources/images/icons2/12x12/pending@3x.png new file mode 100644 index 00000000000..dbb5bb8e52a Binary files /dev/null and b/resources/images/icons2/12x12/pending@3x.png differ diff --git a/resources/images/icons2/12x12/pick@2x.png b/resources/images/icons2/12x12/pick@2x.png new file mode 100644 index 00000000000..5f906e07762 Binary files /dev/null and b/resources/images/icons2/12x12/pick@2x.png differ diff --git a/resources/images/icons2/12x12/pick@3x.png b/resources/images/icons2/12x12/pick@3x.png new file mode 100644 index 00000000000..8872f2916d4 Binary files /dev/null and b/resources/images/icons2/12x12/pick@3x.png differ diff --git a/resources/images/icons2/12x12/pin@2x.png b/resources/images/icons2/12x12/pin@2x.png new file mode 100644 index 00000000000..2de9b42765a Binary files /dev/null and b/resources/images/icons2/12x12/pin@2x.png differ diff --git a/resources/images/icons2/12x12/pin@3x.png b/resources/images/icons2/12x12/pin@3x.png new file mode 100644 index 00000000000..83cb954b993 Binary files /dev/null and b/resources/images/icons2/12x12/pin@3x.png differ diff --git a/resources/images/icons2/12x12/placeholder@2x.png b/resources/images/icons2/12x12/placeholder@2x.png new file mode 100644 index 00000000000..1e8dd2fdaef Binary files /dev/null and b/resources/images/icons2/12x12/placeholder@2x.png differ diff --git a/resources/images/icons2/12x12/placeholder@3x.png b/resources/images/icons2/12x12/placeholder@3x.png new file mode 100644 index 00000000000..9d9a22b4302 Binary files /dev/null and b/resources/images/icons2/12x12/placeholder@3x.png differ diff --git a/resources/images/icons2/12x12/play@2x.png b/resources/images/icons2/12x12/play@2x.png new file mode 100644 index 00000000000..dca786a751e Binary files /dev/null and b/resources/images/icons2/12x12/play@2x.png differ diff --git a/resources/images/icons2/12x12/play@3x.png b/resources/images/icons2/12x12/play@3x.png new file mode 100644 index 00000000000..2d606c50795 Binary files /dev/null and b/resources/images/icons2/12x12/play@3x.png differ diff --git a/resources/images/icons2/12x12/positive-state@2x.png b/resources/images/icons2/12x12/positive-state@2x.png new file mode 100644 index 00000000000..b40a50e26c7 Binary files /dev/null and b/resources/images/icons2/12x12/positive-state@2x.png differ diff --git a/resources/images/icons2/12x12/positive-state@3x.png b/resources/images/icons2/12x12/positive-state@3x.png new file mode 100644 index 00000000000..171f1e4e0bd Binary files /dev/null and b/resources/images/icons2/12x12/positive-state@3x.png differ diff --git a/resources/images/icons2/12x12/positive@2x.png b/resources/images/icons2/12x12/positive@2x.png new file mode 100644 index 00000000000..334eb72a837 Binary files /dev/null and b/resources/images/icons2/12x12/positive@2x.png differ diff --git a/resources/images/icons2/12x12/positive@3x.png b/resources/images/icons2/12x12/positive@3x.png new file mode 100644 index 00000000000..248d903c05f Binary files /dev/null and b/resources/images/icons2/12x12/positive@3x.png differ diff --git a/resources/images/icons2/12x12/progress-100@2x.png b/resources/images/icons2/12x12/progress-100@2x.png new file mode 100644 index 00000000000..2a714d67dc4 Binary files /dev/null and b/resources/images/icons2/12x12/progress-100@2x.png differ diff --git a/resources/images/icons2/12x12/progress-100@3x.png b/resources/images/icons2/12x12/progress-100@3x.png new file mode 100644 index 00000000000..c8a4a0a5089 Binary files /dev/null and b/resources/images/icons2/12x12/progress-100@3x.png differ diff --git a/resources/images/icons2/12x12/progress-25@2x.png b/resources/images/icons2/12x12/progress-25@2x.png new file mode 100644 index 00000000000..6984f5b9cf2 Binary files /dev/null and b/resources/images/icons2/12x12/progress-25@2x.png differ diff --git a/resources/images/icons2/12x12/progress-25@3x.png b/resources/images/icons2/12x12/progress-25@3x.png new file mode 100644 index 00000000000..e43e1533401 Binary files /dev/null and b/resources/images/icons2/12x12/progress-25@3x.png differ diff --git a/resources/images/icons2/12x12/progress-50@2x.png b/resources/images/icons2/12x12/progress-50@2x.png new file mode 100644 index 00000000000..b1a8d5fb3a6 Binary files /dev/null and b/resources/images/icons2/12x12/progress-50@2x.png differ diff --git a/resources/images/icons2/12x12/progress-50@3x.png b/resources/images/icons2/12x12/progress-50@3x.png new file mode 100644 index 00000000000..1257f2eb3d9 Binary files /dev/null and b/resources/images/icons2/12x12/progress-50@3x.png differ diff --git a/resources/images/icons2/12x12/progress-75@2x.png b/resources/images/icons2/12x12/progress-75@2x.png new file mode 100644 index 00000000000..a7e48c79d33 Binary files /dev/null and b/resources/images/icons2/12x12/progress-75@2x.png differ diff --git a/resources/images/icons2/12x12/progress-75@3x.png b/resources/images/icons2/12x12/progress-75@3x.png new file mode 100644 index 00000000000..4ba783d60d6 Binary files /dev/null and b/resources/images/icons2/12x12/progress-75@3x.png differ diff --git a/resources/images/icons2/12x12/progress@2x.png b/resources/images/icons2/12x12/progress@2x.png new file mode 100644 index 00000000000..a755783c283 Binary files /dev/null and b/resources/images/icons2/12x12/progress@2x.png differ diff --git a/resources/images/icons2/12x12/progress@3x.png b/resources/images/icons2/12x12/progress@3x.png new file mode 100644 index 00000000000..4a808d76a32 Binary files /dev/null and b/resources/images/icons2/12x12/progress@3x.png differ diff --git a/resources/images/icons2/12x12/pullup@2x.png b/resources/images/icons2/12x12/pullup@2x.png new file mode 100644 index 00000000000..f9f568abd6b Binary files /dev/null and b/resources/images/icons2/12x12/pullup@2x.png differ diff --git a/resources/images/icons2/12x12/pullup@3x.png b/resources/images/icons2/12x12/pullup@3x.png new file mode 100644 index 00000000000..01725d53b90 Binary files /dev/null and b/resources/images/icons2/12x12/pullup@3x.png differ diff --git a/resources/images/icons2/12x12/refresh@2x.png b/resources/images/icons2/12x12/refresh@2x.png new file mode 100644 index 00000000000..b064f020f48 Binary files /dev/null and b/resources/images/icons2/12x12/refresh@2x.png differ diff --git a/resources/images/icons2/12x12/refresh@3x.png b/resources/images/icons2/12x12/refresh@3x.png new file mode 100644 index 00000000000..f8f8a52dc25 Binary files /dev/null and b/resources/images/icons2/12x12/refresh@3x.png differ diff --git a/resources/images/icons2/12x12/remove-user@2x.png b/resources/images/icons2/12x12/remove-user@2x.png new file mode 100644 index 00000000000..eefb0c6952e Binary files /dev/null and b/resources/images/icons2/12x12/remove-user@2x.png differ diff --git a/resources/images/icons2/12x12/remove-user@3x.png b/resources/images/icons2/12x12/remove-user@3x.png new file mode 100644 index 00000000000..473ac4ff28f Binary files /dev/null and b/resources/images/icons2/12x12/remove-user@3x.png differ diff --git a/resources/images/icons2/12x12/remove@2x.png b/resources/images/icons2/12x12/remove@2x.png new file mode 100644 index 00000000000..9e6cb7dcad1 Binary files /dev/null and b/resources/images/icons2/12x12/remove@2x.png differ diff --git a/resources/images/icons2/12x12/remove@3x.png b/resources/images/icons2/12x12/remove@3x.png new file mode 100644 index 00000000000..40496bd3d8f Binary files /dev/null and b/resources/images/icons2/12x12/remove@3x.png differ diff --git a/resources/images/icons2/12x12/reveal@2x.png b/resources/images/icons2/12x12/reveal@2x.png new file mode 100644 index 00000000000..6535996f304 Binary files /dev/null and b/resources/images/icons2/12x12/reveal@2x.png differ diff --git a/resources/images/icons2/12x12/reveal@3x.png b/resources/images/icons2/12x12/reveal@3x.png new file mode 100644 index 00000000000..54637926dd0 Binary files /dev/null and b/resources/images/icons2/12x12/reveal@3x.png differ diff --git a/resources/images/icons2/12x12/scan@2x.png b/resources/images/icons2/12x12/scan@2x.png new file mode 100644 index 00000000000..c11b83e4595 Binary files /dev/null and b/resources/images/icons2/12x12/scan@2x.png differ diff --git a/resources/images/icons2/12x12/scan@3x.png b/resources/images/icons2/12x12/scan@3x.png new file mode 100644 index 00000000000..b933bd16740 Binary files /dev/null and b/resources/images/icons2/12x12/scan@3x.png differ diff --git a/resources/images/icons2/12x12/search@2x.png b/resources/images/icons2/12x12/search@2x.png new file mode 100644 index 00000000000..0ee831bbf35 Binary files /dev/null and b/resources/images/icons2/12x12/search@2x.png differ diff --git a/resources/images/icons2/12x12/search@3x.png b/resources/images/icons2/12x12/search@3x.png new file mode 100644 index 00000000000..a98fe71e365 Binary files /dev/null and b/resources/images/icons2/12x12/search@3x.png differ diff --git a/resources/images/icons2/12x12/seed-phrase@2x.png b/resources/images/icons2/12x12/seed-phrase@2x.png new file mode 100644 index 00000000000..250bc7ae6e9 Binary files /dev/null and b/resources/images/icons2/12x12/seed-phrase@2x.png differ diff --git a/resources/images/icons2/12x12/seed-phrase@3x.png b/resources/images/icons2/12x12/seed-phrase@3x.png new file mode 100644 index 00000000000..4fe09f12990 Binary files /dev/null and b/resources/images/icons2/12x12/seed-phrase@3x.png differ diff --git a/resources/images/icons2/12x12/sent@2x.png b/resources/images/icons2/12x12/sent@2x.png new file mode 100644 index 00000000000..d03e59f7451 Binary files /dev/null and b/resources/images/icons2/12x12/sent@2x.png differ diff --git a/resources/images/icons2/12x12/sent@3x.png b/resources/images/icons2/12x12/sent@3x.png new file mode 100644 index 00000000000..6cb34df5dde Binary files /dev/null and b/resources/images/icons2/12x12/sent@3x.png differ diff --git a/resources/images/icons2/12x12/star@2x.png b/resources/images/icons2/12x12/star@2x.png new file mode 100644 index 00000000000..d7418fbc15c Binary files /dev/null and b/resources/images/icons2/12x12/star@2x.png differ diff --git a/resources/images/icons2/12x12/star@3x.png b/resources/images/icons2/12x12/star@3x.png new file mode 100644 index 00000000000..bacb62e5a4d Binary files /dev/null and b/resources/images/icons2/12x12/star@3x.png differ diff --git a/resources/images/icons2/12x12/stop@2x.png b/resources/images/icons2/12x12/stop@2x.png new file mode 100644 index 00000000000..4dfc8f5ed6d Binary files /dev/null and b/resources/images/icons2/12x12/stop@2x.png differ diff --git a/resources/images/icons2/12x12/stop@3x.png b/resources/images/icons2/12x12/stop@3x.png new file mode 100644 index 00000000000..c1de1813514 Binary files /dev/null and b/resources/images/icons2/12x12/stop@3x.png differ diff --git a/resources/images/icons2/12x12/thumb-dark@2x.android.png b/resources/images/icons2/12x12/thumb-dark@2x.android.png new file mode 100644 index 00000000000..6e1698af92c Binary files /dev/null and b/resources/images/icons2/12x12/thumb-dark@2x.android.png differ diff --git a/resources/images/icons2/12x12/thumb-dark@2x.ios.png b/resources/images/icons2/12x12/thumb-dark@2x.ios.png new file mode 100644 index 00000000000..0e834c2c7fe Binary files /dev/null and b/resources/images/icons2/12x12/thumb-dark@2x.ios.png differ diff --git a/resources/images/icons2/12x12/thumb-dark@2x.png b/resources/images/icons2/12x12/thumb-dark@2x.png new file mode 100644 index 00000000000..0e834c2c7fe Binary files /dev/null and b/resources/images/icons2/12x12/thumb-dark@2x.png differ diff --git a/resources/images/icons2/12x12/thumb-dark@3x.android.png b/resources/images/icons2/12x12/thumb-dark@3x.android.png new file mode 100644 index 00000000000..eaf3b5a7900 Binary files /dev/null and b/resources/images/icons2/12x12/thumb-dark@3x.android.png differ diff --git a/resources/images/icons2/12x12/thumb-dark@3x.ios.png b/resources/images/icons2/12x12/thumb-dark@3x.ios.png new file mode 100644 index 00000000000..03267ffa172 Binary files /dev/null and b/resources/images/icons2/12x12/thumb-dark@3x.ios.png differ diff --git a/resources/images/icons2/12x12/thumb-dark@3x.png b/resources/images/icons2/12x12/thumb-dark@3x.png new file mode 100644 index 00000000000..03267ffa172 Binary files /dev/null and b/resources/images/icons2/12x12/thumb-dark@3x.png differ diff --git a/resources/images/icons2/12x12/thumb-light@2x.android.png b/resources/images/icons2/12x12/thumb-light@2x.android.png new file mode 100644 index 00000000000..7bd4c3f2a8f Binary files /dev/null and b/resources/images/icons2/12x12/thumb-light@2x.android.png differ diff --git a/resources/images/icons2/12x12/thumb-light@2x.ios.png b/resources/images/icons2/12x12/thumb-light@2x.ios.png new file mode 100644 index 00000000000..5cc203db10a Binary files /dev/null and b/resources/images/icons2/12x12/thumb-light@2x.ios.png differ diff --git a/resources/images/icons2/12x12/thumb-light@2x.png b/resources/images/icons2/12x12/thumb-light@2x.png new file mode 100644 index 00000000000..5cc203db10a Binary files /dev/null and b/resources/images/icons2/12x12/thumb-light@2x.png differ diff --git a/resources/images/icons2/12x12/thumb-light@3x.android.png b/resources/images/icons2/12x12/thumb-light@3x.android.png new file mode 100644 index 00000000000..9043cb4153e Binary files /dev/null and b/resources/images/icons2/12x12/thumb-light@3x.android.png differ diff --git a/resources/images/icons2/12x12/thumb-light@3x.ios.png b/resources/images/icons2/12x12/thumb-light@3x.ios.png new file mode 100644 index 00000000000..ba1526eeaba Binary files /dev/null and b/resources/images/icons2/12x12/thumb-light@3x.ios.png differ diff --git a/resources/images/icons2/12x12/thumb-light@3x.png b/resources/images/icons2/12x12/thumb-light@3x.png new file mode 100644 index 00000000000..ba1526eeaba Binary files /dev/null and b/resources/images/icons2/12x12/thumb-light@3x.png differ diff --git a/resources/images/icons2/12x12/timeout@2x.png b/resources/images/icons2/12x12/timeout@2x.png new file mode 100644 index 00000000000..2a6f459c68b Binary files /dev/null and b/resources/images/icons2/12x12/timeout@2x.png differ diff --git a/resources/images/icons2/12x12/timeout@3x.png b/resources/images/icons2/12x12/timeout@3x.png new file mode 100644 index 00000000000..b88b9d074fc Binary files /dev/null and b/resources/images/icons2/12x12/timeout@3x.png differ diff --git a/resources/images/icons2/12x12/token-master@2x.png b/resources/images/icons2/12x12/token-master@2x.png new file mode 100644 index 00000000000..176f2384c32 Binary files /dev/null and b/resources/images/icons2/12x12/token-master@2x.png differ diff --git a/resources/images/icons2/12x12/token-master@3x.png b/resources/images/icons2/12x12/token-master@3x.png new file mode 100644 index 00000000000..9d1871e77f0 Binary files /dev/null and b/resources/images/icons2/12x12/token-master@3x.png differ diff --git a/resources/images/icons2/12x12/unlocked@2x.png b/resources/images/icons2/12x12/unlocked@2x.png new file mode 100644 index 00000000000..eb4c9f78c21 Binary files /dev/null and b/resources/images/icons2/12x12/unlocked@2x.png differ diff --git a/resources/images/icons2/12x12/unlocked@3x.png b/resources/images/icons2/12x12/unlocked@3x.png new file mode 100644 index 00000000000..6edaa634806 Binary files /dev/null and b/resources/images/icons2/12x12/unlocked@3x.png differ diff --git a/resources/images/icons2/12x12/unpin@2x.png b/resources/images/icons2/12x12/unpin@2x.png new file mode 100644 index 00000000000..f39eb42247e Binary files /dev/null and b/resources/images/icons2/12x12/unpin@2x.png differ diff --git a/resources/images/icons2/12x12/unpin@3x.png b/resources/images/icons2/12x12/unpin@3x.png new file mode 100644 index 00000000000..af35487820c Binary files /dev/null and b/resources/images/icons2/12x12/unpin@3x.png differ diff --git a/resources/images/icons2/12x12/untrustworthy@2x.png b/resources/images/icons2/12x12/untrustworthy@2x.png new file mode 100644 index 00000000000..422e5e9dd46 Binary files /dev/null and b/resources/images/icons2/12x12/untrustworthy@2x.png differ diff --git a/resources/images/icons2/12x12/untrustworthy@3x.png b/resources/images/icons2/12x12/untrustworthy@3x.png new file mode 100644 index 00000000000..8c70e70eff9 Binary files /dev/null and b/resources/images/icons2/12x12/untrustworthy@3x.png differ diff --git a/resources/images/icons2/12x12/verified@2x.png b/resources/images/icons2/12x12/verified@2x.png new file mode 100644 index 00000000000..9c4cd122191 Binary files /dev/null and b/resources/images/icons2/12x12/verified@2x.png differ diff --git a/resources/images/icons2/12x12/verified@3x.png b/resources/images/icons2/12x12/verified@3x.png new file mode 100644 index 00000000000..87b24b6258f Binary files /dev/null and b/resources/images/icons2/12x12/verified@3x.png differ diff --git a/resources/images/icons2/12x12/volume-max@2x.png b/resources/images/icons2/12x12/volume-max@2x.png new file mode 100644 index 00000000000..abf32483157 Binary files /dev/null and b/resources/images/icons2/12x12/volume-max@2x.png differ diff --git a/resources/images/icons2/12x12/volume-max@3x.png b/resources/images/icons2/12x12/volume-max@3x.png new file mode 100644 index 00000000000..8e41e0323ed Binary files /dev/null and b/resources/images/icons2/12x12/volume-max@3x.png differ diff --git a/resources/images/icons2/12x12/volume-min@2x.png b/resources/images/icons2/12x12/volume-min@2x.png new file mode 100644 index 00000000000..43ecf71b9b9 Binary files /dev/null and b/resources/images/icons2/12x12/volume-min@2x.png differ diff --git a/resources/images/icons2/12x12/volume-min@3x.png b/resources/images/icons2/12x12/volume-min@3x.png new file mode 100644 index 00000000000..5e7bd85d18b Binary files /dev/null and b/resources/images/icons2/12x12/volume-min@3x.png differ diff --git a/resources/images/icons2/12x12/volume@2x.png b/resources/images/icons2/12x12/volume@2x.png new file mode 100644 index 00000000000..b6de8960490 Binary files /dev/null and b/resources/images/icons2/12x12/volume@2x.png differ diff --git a/resources/images/icons2/12x12/volume@3x.png b/resources/images/icons2/12x12/volume@3x.png new file mode 100644 index 00000000000..3b8328fb611 Binary files /dev/null and b/resources/images/icons2/12x12/volume@3x.png differ diff --git a/resources/images/icons2/12x12/wallet@2x.png b/resources/images/icons2/12x12/wallet@2x.png new file mode 100644 index 00000000000..c117dcabc24 Binary files /dev/null and b/resources/images/icons2/12x12/wallet@2x.png differ diff --git a/resources/images/icons2/12x12/wallet@3x.png b/resources/images/icons2/12x12/wallet@3x.png new file mode 100644 index 00000000000..f1918e44a87 Binary files /dev/null and b/resources/images/icons2/12x12/wallet@3x.png differ diff --git a/resources/images/icons2/12x12/warning@2x.png b/resources/images/icons2/12x12/warning@2x.png new file mode 100644 index 00000000000..52c7735b1a5 Binary files /dev/null and b/resources/images/icons2/12x12/warning@2x.png differ diff --git a/resources/images/icons2/12x12/warning@3x.png b/resources/images/icons2/12x12/warning@3x.png new file mode 100644 index 00000000000..9670fbf4c6d Binary files /dev/null and b/resources/images/icons2/12x12/warning@3x.png differ diff --git a/resources/images/icons2/12x12/weak@2x.png b/resources/images/icons2/12x12/weak@2x.png new file mode 100644 index 00000000000..8f21b182193 Binary files /dev/null and b/resources/images/icons2/12x12/weak@2x.png differ diff --git a/resources/images/icons2/12x12/weak@3x.png b/resources/images/icons2/12x12/weak@3x.png new file mode 100644 index 00000000000..de33a441f73 Binary files /dev/null and b/resources/images/icons2/12x12/weak@3x.png differ diff --git a/resources/images/icons2/16x16/active-members@2x.png b/resources/images/icons2/16x16/active-members@2x.png new file mode 100644 index 00000000000..e94bc47079c Binary files /dev/null and b/resources/images/icons2/16x16/active-members@2x.png differ diff --git a/resources/images/icons2/16x16/active-members@3x.png b/resources/images/icons2/16x16/active-members@3x.png new file mode 100644 index 00000000000..a538b931e4b Binary files /dev/null and b/resources/images/icons2/16x16/active-members@3x.png differ diff --git a/resources/images/icons2/16x16/active@2x.png b/resources/images/icons2/16x16/active@2x.png new file mode 100644 index 00000000000..bd5f905c4dc Binary files /dev/null and b/resources/images/icons2/16x16/active@2x.png differ diff --git a/resources/images/icons2/16x16/active@3x.png b/resources/images/icons2/16x16/active@3x.png new file mode 100644 index 00000000000..8593010f33d Binary files /dev/null and b/resources/images/icons2/16x16/active@3x.png differ diff --git a/resources/images/icons2/16x16/add-reaction@2x.png b/resources/images/icons2/16x16/add-reaction@2x.png new file mode 100644 index 00000000000..f85199a974f Binary files /dev/null and b/resources/images/icons2/16x16/add-reaction@2x.png differ diff --git a/resources/images/icons2/16x16/add-reaction@3x.png b/resources/images/icons2/16x16/add-reaction@3x.png new file mode 100644 index 00000000000..543fbb2566d Binary files /dev/null and b/resources/images/icons2/16x16/add-reaction@3x.png differ diff --git a/resources/images/icons2/16x16/add-user@2x.png b/resources/images/icons2/16x16/add-user@2x.png new file mode 100644 index 00000000000..ba185f3369a Binary files /dev/null and b/resources/images/icons2/16x16/add-user@2x.png differ diff --git a/resources/images/icons2/16x16/add-user@3x.png b/resources/images/icons2/16x16/add-user@3x.png new file mode 100644 index 00000000000..127c1162c0d Binary files /dev/null and b/resources/images/icons2/16x16/add-user@3x.png differ diff --git a/resources/images/icons2/16x16/add@2x.png b/resources/images/icons2/16x16/add@2x.png new file mode 100644 index 00000000000..791a3faa0ab Binary files /dev/null and b/resources/images/icons2/16x16/add@2x.png differ diff --git a/resources/images/icons2/16x16/add@3x.png b/resources/images/icons2/16x16/add@3x.png new file mode 100644 index 00000000000..bb9e921064b Binary files /dev/null and b/resources/images/icons2/16x16/add@3x.png differ diff --git a/resources/images/icons2/16x16/alert@2x.png b/resources/images/icons2/16x16/alert@2x.png new file mode 100644 index 00000000000..2f66b8f3b85 Binary files /dev/null and b/resources/images/icons2/16x16/alert@2x.png differ diff --git a/resources/images/icons2/16x16/alert@3x.png b/resources/images/icons2/16x16/alert@3x.png new file mode 100644 index 00000000000..4ba7c83b3ee Binary files /dev/null and b/resources/images/icons2/16x16/alert@3x.png differ diff --git a/resources/images/icons2/16x16/arrow-down@2x.png b/resources/images/icons2/16x16/arrow-down@2x.png new file mode 100644 index 00000000000..35910dfdd54 Binary files /dev/null and b/resources/images/icons2/16x16/arrow-down@2x.png differ diff --git a/resources/images/icons2/16x16/arrow-down@3x.png b/resources/images/icons2/16x16/arrow-down@3x.png new file mode 100644 index 00000000000..36a8ecfd738 Binary files /dev/null and b/resources/images/icons2/16x16/arrow-down@3x.png differ diff --git a/resources/images/icons2/16x16/arrow-left@2x.png b/resources/images/icons2/16x16/arrow-left@2x.png new file mode 100644 index 00000000000..bde44c6f895 Binary files /dev/null and b/resources/images/icons2/16x16/arrow-left@2x.png differ diff --git a/resources/images/icons2/16x16/arrow-left@3x.png b/resources/images/icons2/16x16/arrow-left@3x.png new file mode 100644 index 00000000000..eb4aa4561fd Binary files /dev/null and b/resources/images/icons2/16x16/arrow-left@3x.png differ diff --git a/resources/images/icons2/16x16/arrow-right@2x.png b/resources/images/icons2/16x16/arrow-right@2x.png new file mode 100644 index 00000000000..1d6d125ae0b Binary files /dev/null and b/resources/images/icons2/16x16/arrow-right@2x.png differ diff --git a/resources/images/icons2/16x16/arrow-right@3x.png b/resources/images/icons2/16x16/arrow-right@3x.png new file mode 100644 index 00000000000..d15bb33c0cf Binary files /dev/null and b/resources/images/icons2/16x16/arrow-right@3x.png differ diff --git a/resources/images/icons2/16x16/arrow-top@2x.png b/resources/images/icons2/16x16/arrow-top@2x.png new file mode 100644 index 00000000000..5d1070965d5 Binary files /dev/null and b/resources/images/icons2/16x16/arrow-top@2x.png differ diff --git a/resources/images/icons2/16x16/arrow-top@3x.png b/resources/images/icons2/16x16/arrow-top@3x.png new file mode 100644 index 00000000000..f3d17a7006e Binary files /dev/null and b/resources/images/icons2/16x16/arrow-top@3x.png differ diff --git a/resources/images/icons2/16x16/block@2x.png b/resources/images/icons2/16x16/block@2x.png new file mode 100644 index 00000000000..dd7ce70bc20 Binary files /dev/null and b/resources/images/icons2/16x16/block@2x.png differ diff --git a/resources/images/icons2/16x16/block@3x.png b/resources/images/icons2/16x16/block@3x.png new file mode 100644 index 00000000000..e23684d1780 Binary files /dev/null and b/resources/images/icons2/16x16/block@3x.png differ diff --git a/resources/images/icons2/16x16/calendar@2x.png b/resources/images/icons2/16x16/calendar@2x.png new file mode 100644 index 00000000000..f53e8beb904 Binary files /dev/null and b/resources/images/icons2/16x16/calendar@2x.png differ diff --git a/resources/images/icons2/16x16/calendar@3x.png b/resources/images/icons2/16x16/calendar@3x.png new file mode 100644 index 00000000000..87a812b5354 Binary files /dev/null and b/resources/images/icons2/16x16/calendar@3x.png differ diff --git a/resources/images/icons2/16x16/chart-line@2x.png b/resources/images/icons2/16x16/chart-line@2x.png new file mode 100644 index 00000000000..0a8cae38219 Binary files /dev/null and b/resources/images/icons2/16x16/chart-line@2x.png differ diff --git a/resources/images/icons2/16x16/chart-line@3x.png b/resources/images/icons2/16x16/chart-line@3x.png new file mode 100644 index 00000000000..4efc9def25b Binary files /dev/null and b/resources/images/icons2/16x16/chart-line@3x.png differ diff --git a/resources/images/icons2/16x16/check-circle@2x-1.png b/resources/images/icons2/16x16/check-circle@2x-1.png new file mode 100644 index 00000000000..d9e84bef7d1 Binary files /dev/null and b/resources/images/icons2/16x16/check-circle@2x-1.png differ diff --git a/resources/images/icons2/16x16/check-circle@2x.png b/resources/images/icons2/16x16/check-circle@2x.png new file mode 100644 index 00000000000..6d3b0fed38d Binary files /dev/null and b/resources/images/icons2/16x16/check-circle@2x.png differ diff --git a/resources/images/icons2/16x16/check-circle@3x-1.png b/resources/images/icons2/16x16/check-circle@3x-1.png new file mode 100644 index 00000000000..837c8b9719c Binary files /dev/null and b/resources/images/icons2/16x16/check-circle@3x-1.png differ diff --git a/resources/images/icons2/16x16/check-circle@3x.png b/resources/images/icons2/16x16/check-circle@3x.png new file mode 100644 index 00000000000..f86e70765f4 Binary files /dev/null and b/resources/images/icons2/16x16/check-circle@3x.png differ diff --git a/resources/images/icons2/16x16/check@2x.png b/resources/images/icons2/16x16/check@2x.png new file mode 100644 index 00000000000..a1cebfc609b Binary files /dev/null and b/resources/images/icons2/16x16/check@2x.png differ diff --git a/resources/images/icons2/16x16/check@3x.png b/resources/images/icons2/16x16/check@3x.png new file mode 100644 index 00000000000..dc2a947c607 Binary files /dev/null and b/resources/images/icons2/16x16/check@3x.png differ diff --git a/resources/images/icons2/16x16/chevron-down@2x.png b/resources/images/icons2/16x16/chevron-down@2x.png new file mode 100644 index 00000000000..b5817bfecca Binary files /dev/null and b/resources/images/icons2/16x16/chevron-down@2x.png differ diff --git a/resources/images/icons2/16x16/chevron-down@3x.png b/resources/images/icons2/16x16/chevron-down@3x.png new file mode 100644 index 00000000000..956802d9b26 Binary files /dev/null and b/resources/images/icons2/16x16/chevron-down@3x.png differ diff --git a/resources/images/icons2/16x16/chevron-left@2x.png b/resources/images/icons2/16x16/chevron-left@2x.png new file mode 100644 index 00000000000..8c732304a6c Binary files /dev/null and b/resources/images/icons2/16x16/chevron-left@2x.png differ diff --git a/resources/images/icons2/16x16/chevron-left@3x.png b/resources/images/icons2/16x16/chevron-left@3x.png new file mode 100644 index 00000000000..a1a4fc62874 Binary files /dev/null and b/resources/images/icons2/16x16/chevron-left@3x.png differ diff --git a/resources/images/icons2/16x16/chevron-right@2x.png b/resources/images/icons2/16x16/chevron-right@2x.png new file mode 100644 index 00000000000..f207720f0f2 Binary files /dev/null and b/resources/images/icons2/16x16/chevron-right@2x.png differ diff --git a/resources/images/icons2/16x16/chevron-right@3x.png b/resources/images/icons2/16x16/chevron-right@3x.png new file mode 100644 index 00000000000..9460a3aef2a Binary files /dev/null and b/resources/images/icons2/16x16/chevron-right@3x.png differ diff --git a/resources/images/icons2/16x16/chevron-top@2x.png b/resources/images/icons2/16x16/chevron-top@2x.png new file mode 100644 index 00000000000..60469d3a393 Binary files /dev/null and b/resources/images/icons2/16x16/chevron-top@2x.png differ diff --git a/resources/images/icons2/16x16/chevron-top@3x.png b/resources/images/icons2/16x16/chevron-top@3x.png new file mode 100644 index 00000000000..e5f29b594ee Binary files /dev/null and b/resources/images/icons2/16x16/chevron-top@3x.png differ diff --git a/resources/images/icons2/16x16/chevrons-left@2x.png b/resources/images/icons2/16x16/chevrons-left@2x.png new file mode 100644 index 00000000000..2f4f50bb50b Binary files /dev/null and b/resources/images/icons2/16x16/chevrons-left@2x.png differ diff --git a/resources/images/icons2/16x16/chevrons-left@3x.png b/resources/images/icons2/16x16/chevrons-left@3x.png new file mode 100644 index 00000000000..6908dc6e312 Binary files /dev/null and b/resources/images/icons2/16x16/chevrons-left@3x.png differ diff --git a/resources/images/icons2/16x16/chevrons-right@2x.png b/resources/images/icons2/16x16/chevrons-right@2x.png new file mode 100644 index 00000000000..de20810379e Binary files /dev/null and b/resources/images/icons2/16x16/chevrons-right@2x.png differ diff --git a/resources/images/icons2/16x16/chevrons-right@3x.png b/resources/images/icons2/16x16/chevrons-right@3x.png new file mode 100644 index 00000000000..879019bcc20 Binary files /dev/null and b/resources/images/icons2/16x16/chevrons-right@3x.png differ diff --git a/resources/images/icons2/16x16/close@2x-1.png b/resources/images/icons2/16x16/close@2x-1.png new file mode 100644 index 00000000000..887428845a6 Binary files /dev/null and b/resources/images/icons2/16x16/close@2x-1.png differ diff --git a/resources/images/icons2/16x16/close@2x.png b/resources/images/icons2/16x16/close@2x.png new file mode 100644 index 00000000000..349f88262ad Binary files /dev/null and b/resources/images/icons2/16x16/close@2x.png differ diff --git a/resources/images/icons2/16x16/close@3x-1.png b/resources/images/icons2/16x16/close@3x-1.png new file mode 100644 index 00000000000..83722657329 Binary files /dev/null and b/resources/images/icons2/16x16/close@3x-1.png differ diff --git a/resources/images/icons2/16x16/close@3x.png b/resources/images/icons2/16x16/close@3x.png new file mode 100644 index 00000000000..043bfa72e3d Binary files /dev/null and b/resources/images/icons2/16x16/close@3x.png differ diff --git a/resources/images/icons2/16x16/collapse-left-sidebar@2x.png b/resources/images/icons2/16x16/collapse-left-sidebar@2x.png new file mode 100644 index 00000000000..5945c2bfae4 Binary files /dev/null and b/resources/images/icons2/16x16/collapse-left-sidebar@2x.png differ diff --git a/resources/images/icons2/16x16/collapse-left-sidebar@3x.png b/resources/images/icons2/16x16/collapse-left-sidebar@3x.png new file mode 100644 index 00000000000..d33bd4f41e3 Binary files /dev/null and b/resources/images/icons2/16x16/collapse-left-sidebar@3x.png differ diff --git a/resources/images/icons2/16x16/collapse-right-sidebar@2x.png b/resources/images/icons2/16x16/collapse-right-sidebar@2x.png new file mode 100644 index 00000000000..ae912b3dbed Binary files /dev/null and b/resources/images/icons2/16x16/collapse-right-sidebar@2x.png differ diff --git a/resources/images/icons2/16x16/collapse-right-sidebar@3x.png b/resources/images/icons2/16x16/collapse-right-sidebar@3x.png new file mode 100644 index 00000000000..8797006ea5f Binary files /dev/null and b/resources/images/icons2/16x16/collapse-right-sidebar@3x.png differ diff --git a/resources/images/icons2/16x16/collapse@2x.png b/resources/images/icons2/16x16/collapse@2x.png new file mode 100644 index 00000000000..f21a322cca8 Binary files /dev/null and b/resources/images/icons2/16x16/collapse@2x.png differ diff --git a/resources/images/icons2/16x16/collapse@3x.png b/resources/images/icons2/16x16/collapse@3x.png new file mode 100644 index 00000000000..8ea5a4f8eec Binary files /dev/null and b/resources/images/icons2/16x16/collapse@3x.png differ diff --git a/resources/images/icons2/16x16/connection@2x.png b/resources/images/icons2/16x16/connection@2x.png new file mode 100644 index 00000000000..d05e9f4849c Binary files /dev/null and b/resources/images/icons2/16x16/connection@2x.png differ diff --git a/resources/images/icons2/16x16/connection@3x.png b/resources/images/icons2/16x16/connection@3x.png new file mode 100644 index 00000000000..c4fec0a8d79 Binary files /dev/null and b/resources/images/icons2/16x16/connection@3x.png differ diff --git a/resources/images/icons2/16x16/connector-dotted@2x.png b/resources/images/icons2/16x16/connector-dotted@2x.png new file mode 100644 index 00000000000..f669467fbf0 Binary files /dev/null and b/resources/images/icons2/16x16/connector-dotted@2x.png differ diff --git a/resources/images/icons2/16x16/connector-dotted@3x.png b/resources/images/icons2/16x16/connector-dotted@3x.png new file mode 100644 index 00000000000..3f6efe38aab Binary files /dev/null and b/resources/images/icons2/16x16/connector-dotted@3x.png differ diff --git a/resources/images/icons2/16x16/connector@2x.png b/resources/images/icons2/16x16/connector@2x.png new file mode 100644 index 00000000000..f69e4b2bb4d Binary files /dev/null and b/resources/images/icons2/16x16/connector@2x.png differ diff --git a/resources/images/icons2/16x16/connector@3x.png b/resources/images/icons2/16x16/connector@3x.png new file mode 100644 index 00000000000..dc1c6b459e4 Binary files /dev/null and b/resources/images/icons2/16x16/connector@3x.png differ diff --git a/resources/images/icons2/16x16/contact-book@2x.png b/resources/images/icons2/16x16/contact-book@2x.png new file mode 100644 index 00000000000..16a5d2fb5ec Binary files /dev/null and b/resources/images/icons2/16x16/contact-book@2x.png differ diff --git a/resources/images/icons2/16x16/contact-book@3x.png b/resources/images/icons2/16x16/contact-book@3x.png new file mode 100644 index 00000000000..9c2c4ffb553 Binary files /dev/null and b/resources/images/icons2/16x16/contact-book@3x.png differ diff --git a/resources/images/icons2/16x16/cover@2x.png b/resources/images/icons2/16x16/cover@2x.png new file mode 100644 index 00000000000..e49e0be7418 Binary files /dev/null and b/resources/images/icons2/16x16/cover@2x.png differ diff --git a/resources/images/icons2/16x16/cover@3x.png b/resources/images/icons2/16x16/cover@3x.png new file mode 100644 index 00000000000..6a2e5a08174 Binary files /dev/null and b/resources/images/icons2/16x16/cover@3x.png differ diff --git a/resources/images/icons2/16x16/customize@2x.png b/resources/images/icons2/16x16/customize@2x.png new file mode 100644 index 00000000000..f69e645df6e Binary files /dev/null and b/resources/images/icons2/16x16/customize@2x.png differ diff --git a/resources/images/icons2/16x16/customize@3x.png b/resources/images/icons2/16x16/customize@3x.png new file mode 100644 index 00000000000..b54c041a9f8 Binary files /dev/null and b/resources/images/icons2/16x16/customize@3x.png differ diff --git a/resources/images/icons2/16x16/dark@2x.png b/resources/images/icons2/16x16/dark@2x.png new file mode 100644 index 00000000000..20d13603286 Binary files /dev/null and b/resources/images/icons2/16x16/dark@2x.png differ diff --git a/resources/images/icons2/16x16/dark@3x.png b/resources/images/icons2/16x16/dark@3x.png new file mode 100644 index 00000000000..43ba2b14d88 Binary files /dev/null and b/resources/images/icons2/16x16/dark@3x.png differ diff --git a/resources/images/icons2/16x16/decline@2x.png b/resources/images/icons2/16x16/decline@2x.png new file mode 100644 index 00000000000..e1036a0f614 Binary files /dev/null and b/resources/images/icons2/16x16/decline@2x.png differ diff --git a/resources/images/icons2/16x16/decline@3x.png b/resources/images/icons2/16x16/decline@3x.png new file mode 100644 index 00000000000..eea25c1c819 Binary files /dev/null and b/resources/images/icons2/16x16/decline@3x.png differ diff --git a/resources/images/icons2/16x16/delete@2x.png b/resources/images/icons2/16x16/delete@2x.png new file mode 100644 index 00000000000..a3eedef72cf Binary files /dev/null and b/resources/images/icons2/16x16/delete@2x.png differ diff --git a/resources/images/icons2/16x16/delete@3x.png b/resources/images/icons2/16x16/delete@3x.png new file mode 100644 index 00000000000..70a1e94c437 Binary files /dev/null and b/resources/images/icons2/16x16/delete@3x.png differ diff --git a/resources/images/icons2/16x16/desktop@2x.png b/resources/images/icons2/16x16/desktop@2x.png new file mode 100644 index 00000000000..5dea692fc69 Binary files /dev/null and b/resources/images/icons2/16x16/desktop@2x.png differ diff --git a/resources/images/icons2/16x16/desktop@3x.png b/resources/images/icons2/16x16/desktop@3x.png new file mode 100644 index 00000000000..b5c8fa9ca8b Binary files /dev/null and b/resources/images/icons2/16x16/desktop@3x.png differ diff --git a/resources/images/icons2/16x16/diamond@2x.png b/resources/images/icons2/16x16/diamond@2x.png new file mode 100644 index 00000000000..d1968f43ead Binary files /dev/null and b/resources/images/icons2/16x16/diamond@2x.png differ diff --git a/resources/images/icons2/16x16/diamond@3x.png b/resources/images/icons2/16x16/diamond@3x.png new file mode 100644 index 00000000000..3080b1831f4 Binary files /dev/null and b/resources/images/icons2/16x16/diamond@3x.png differ diff --git a/resources/images/icons2/16x16/done@2x.png b/resources/images/icons2/16x16/done@2x.png new file mode 100644 index 00000000000..c0372752ca9 Binary files /dev/null and b/resources/images/icons2/16x16/done@2x.png differ diff --git a/resources/images/icons2/16x16/done@3x.png b/resources/images/icons2/16x16/done@3x.png new file mode 100644 index 00000000000..a7461a20a01 Binary files /dev/null and b/resources/images/icons2/16x16/done@3x.png differ diff --git a/resources/images/icons2/16x16/dropdown@2x.png b/resources/images/icons2/16x16/dropdown@2x.png new file mode 100644 index 00000000000..0da0f2fc7ce Binary files /dev/null and b/resources/images/icons2/16x16/dropdown@2x.png differ diff --git a/resources/images/icons2/16x16/dropdown@3x.png b/resources/images/icons2/16x16/dropdown@3x.png new file mode 100644 index 00000000000..1aa4f00d8db Binary files /dev/null and b/resources/images/icons2/16x16/dropdown@3x.png differ diff --git a/resources/images/icons2/16x16/email@2x.png b/resources/images/icons2/16x16/email@2x.png new file mode 100644 index 00000000000..1e3301810c1 Binary files /dev/null and b/resources/images/icons2/16x16/email@2x.png differ diff --git a/resources/images/icons2/16x16/email@3x.png b/resources/images/icons2/16x16/email@3x.png new file mode 100644 index 00000000000..2a919b3fc43 Binary files /dev/null and b/resources/images/icons2/16x16/email@3x.png differ diff --git a/resources/images/icons2/16x16/expand-left-sidebar@2x.png b/resources/images/icons2/16x16/expand-left-sidebar@2x.png new file mode 100644 index 00000000000..5945c2bfae4 Binary files /dev/null and b/resources/images/icons2/16x16/expand-left-sidebar@2x.png differ diff --git a/resources/images/icons2/16x16/expand-left-sidebar@3x.png b/resources/images/icons2/16x16/expand-left-sidebar@3x.png new file mode 100644 index 00000000000..d33bd4f41e3 Binary files /dev/null and b/resources/images/icons2/16x16/expand-left-sidebar@3x.png differ diff --git a/resources/images/icons2/16x16/expand-right-sidebar@2x.png b/resources/images/icons2/16x16/expand-right-sidebar@2x.png new file mode 100644 index 00000000000..651845a0357 Binary files /dev/null and b/resources/images/icons2/16x16/expand-right-sidebar@2x.png differ diff --git a/resources/images/icons2/16x16/expand-right-sidebar@3x.png b/resources/images/icons2/16x16/expand-right-sidebar@3x.png new file mode 100644 index 00000000000..5e7f166c295 Binary files /dev/null and b/resources/images/icons2/16x16/expand-right-sidebar@3x.png differ diff --git a/resources/images/icons2/16x16/external@2x.png b/resources/images/icons2/16x16/external@2x.png new file mode 100644 index 00000000000..1e0a73b122e Binary files /dev/null and b/resources/images/icons2/16x16/external@2x.png differ diff --git a/resources/images/icons2/16x16/external@3x.png b/resources/images/icons2/16x16/external@3x.png new file mode 100644 index 00000000000..25fe19c42f8 Binary files /dev/null and b/resources/images/icons2/16x16/external@3x.png differ diff --git a/resources/images/icons2/16x16/focal-point@2x.png b/resources/images/icons2/16x16/focal-point@2x.png new file mode 100644 index 00000000000..34b0a9a59a0 Binary files /dev/null and b/resources/images/icons2/16x16/focal-point@2x.png differ diff --git a/resources/images/icons2/16x16/focal-point@3x.png b/resources/images/icons2/16x16/focal-point@3x.png new file mode 100644 index 00000000000..bb23eef5249 Binary files /dev/null and b/resources/images/icons2/16x16/focal-point@3x.png differ diff --git a/resources/images/icons2/16x16/folder@2x.png b/resources/images/icons2/16x16/folder@2x.png new file mode 100644 index 00000000000..c07d4a27096 Binary files /dev/null and b/resources/images/icons2/16x16/folder@2x.png differ diff --git a/resources/images/icons2/16x16/folder@3x.png b/resources/images/icons2/16x16/folder@3x.png new file mode 100644 index 00000000000..af65585ca55 Binary files /dev/null and b/resources/images/icons2/16x16/folder@3x.png differ diff --git a/resources/images/icons2/16x16/forward@2x.png b/resources/images/icons2/16x16/forward@2x.png new file mode 100644 index 00000000000..1d7ea292abe Binary files /dev/null and b/resources/images/icons2/16x16/forward@2x.png differ diff --git a/resources/images/icons2/16x16/forward@3x.png b/resources/images/icons2/16x16/forward@3x.png new file mode 100644 index 00000000000..27c06d36377 Binary files /dev/null and b/resources/images/icons2/16x16/forward@3x.png differ diff --git a/resources/images/icons2/16x16/friend@2x.png b/resources/images/icons2/16x16/friend@2x.png new file mode 100644 index 00000000000..b931e74a81f Binary files /dev/null and b/resources/images/icons2/16x16/friend@2x.png differ diff --git a/resources/images/icons2/16x16/friend@3x.png b/resources/images/icons2/16x16/friend@3x.png new file mode 100644 index 00000000000..052ab2ce9c9 Binary files /dev/null and b/resources/images/icons2/16x16/friend@3x.png differ diff --git a/resources/images/icons2/16x16/fullscreen@2x.png b/resources/images/icons2/16x16/fullscreen@2x.png new file mode 100644 index 00000000000..f87a5654791 Binary files /dev/null and b/resources/images/icons2/16x16/fullscreen@2x.png differ diff --git a/resources/images/icons2/16x16/fullscreen@3x.png b/resources/images/icons2/16x16/fullscreen@3x.png new file mode 100644 index 00000000000..2f88ab904a3 Binary files /dev/null and b/resources/images/icons2/16x16/fullscreen@3x.png differ diff --git a/resources/images/icons2/16x16/gif@2x.png b/resources/images/icons2/16x16/gif@2x.png new file mode 100644 index 00000000000..81a5c9e61c3 Binary files /dev/null and b/resources/images/icons2/16x16/gif@2x.png differ diff --git a/resources/images/icons2/16x16/gif@3x.png b/resources/images/icons2/16x16/gif@3x.png new file mode 100644 index 00000000000..a4c1d63008b Binary files /dev/null and b/resources/images/icons2/16x16/gif@3x.png differ diff --git a/resources/images/icons2/16x16/group@2x.png b/resources/images/icons2/16x16/group@2x.png new file mode 100644 index 00000000000..9d05b055205 Binary files /dev/null and b/resources/images/icons2/16x16/group@2x.png differ diff --git a/resources/images/icons2/16x16/group@3x.png b/resources/images/icons2/16x16/group@3x.png new file mode 100644 index 00000000000..8a8cc39bd6d Binary files /dev/null and b/resources/images/icons2/16x16/group@3x.png differ diff --git a/resources/images/icons2/16x16/hashtag@2x.png b/resources/images/icons2/16x16/hashtag@2x.png new file mode 100644 index 00000000000..230913c4600 Binary files /dev/null and b/resources/images/icons2/16x16/hashtag@2x.png differ diff --git a/resources/images/icons2/16x16/hashtag@3x.png b/resources/images/icons2/16x16/hashtag@3x.png new file mode 100644 index 00000000000..084c1b9a20e Binary files /dev/null and b/resources/images/icons2/16x16/hashtag@3x.png differ diff --git a/resources/images/icons2/16x16/history@2x.png b/resources/images/icons2/16x16/history@2x.png new file mode 100644 index 00000000000..e4b8c7866b8 Binary files /dev/null and b/resources/images/icons2/16x16/history@2x.png differ diff --git a/resources/images/icons2/16x16/history@3x.png b/resources/images/icons2/16x16/history@3x.png new file mode 100644 index 00000000000..44fc9f4e65c Binary files /dev/null and b/resources/images/icons2/16x16/history@3x.png differ diff --git a/resources/images/icons2/16x16/inactive-left@2x.png b/resources/images/icons2/16x16/inactive-left@2x.png new file mode 100644 index 00000000000..ecc53820261 Binary files /dev/null and b/resources/images/icons2/16x16/inactive-left@2x.png differ diff --git a/resources/images/icons2/16x16/inactive-left@3x.png b/resources/images/icons2/16x16/inactive-left@3x.png new file mode 100644 index 00000000000..9167fdf884f Binary files /dev/null and b/resources/images/icons2/16x16/inactive-left@3x.png differ diff --git a/resources/images/icons2/16x16/inactive@2x-1.png b/resources/images/icons2/16x16/inactive@2x-1.png new file mode 100644 index 00000000000..caa1263e0e0 Binary files /dev/null and b/resources/images/icons2/16x16/inactive@2x-1.png differ diff --git a/resources/images/icons2/16x16/inactive@2x.png b/resources/images/icons2/16x16/inactive@2x.png new file mode 100644 index 00000000000..51bba5983a6 Binary files /dev/null and b/resources/images/icons2/16x16/inactive@2x.png differ diff --git a/resources/images/icons2/16x16/inactive@3x-1.png b/resources/images/icons2/16x16/inactive@3x-1.png new file mode 100644 index 00000000000..c2debfe5754 Binary files /dev/null and b/resources/images/icons2/16x16/inactive@3x-1.png differ diff --git a/resources/images/icons2/16x16/inactive@3x.png b/resources/images/icons2/16x16/inactive@3x.png new file mode 100644 index 00000000000..11807d137fd Binary files /dev/null and b/resources/images/icons2/16x16/inactive@3x.png differ diff --git a/resources/images/icons2/16x16/info@2x.png b/resources/images/icons2/16x16/info@2x.png new file mode 100644 index 00000000000..02b2b6cf83f Binary files /dev/null and b/resources/images/icons2/16x16/info@2x.png differ diff --git a/resources/images/icons2/16x16/info@3x.png b/resources/images/icons2/16x16/info@3x.png new file mode 100644 index 00000000000..7fe8fcf3ac4 Binary files /dev/null and b/resources/images/icons2/16x16/info@3x.png differ diff --git a/resources/images/icons2/16x16/keycard-card@2x.png b/resources/images/icons2/16x16/keycard-card@2x.png new file mode 100644 index 00000000000..6f2baf3ab02 Binary files /dev/null and b/resources/images/icons2/16x16/keycard-card@2x.png differ diff --git a/resources/images/icons2/16x16/keycard-card@3x.png b/resources/images/icons2/16x16/keycard-card@3x.png new file mode 100644 index 00000000000..8f5881ff0c1 Binary files /dev/null and b/resources/images/icons2/16x16/keycard-card@3x.png differ diff --git a/resources/images/icons2/16x16/light@2x.png b/resources/images/icons2/16x16/light@2x.png new file mode 100644 index 00000000000..9bd440dcbc9 Binary files /dev/null and b/resources/images/icons2/16x16/light@2x.png differ diff --git a/resources/images/icons2/16x16/light@3x.png b/resources/images/icons2/16x16/light@3x.png new file mode 100644 index 00000000000..776e726e734 Binary files /dev/null and b/resources/images/icons2/16x16/light@3x.png differ diff --git a/resources/images/icons2/16x16/lightning@2x.png b/resources/images/icons2/16x16/lightning@2x.png new file mode 100644 index 00000000000..7a275542847 Binary files /dev/null and b/resources/images/icons2/16x16/lightning@2x.png differ diff --git a/resources/images/icons2/16x16/lightning@3x.png b/resources/images/icons2/16x16/lightning@3x.png new file mode 100644 index 00000000000..0063e78f4b0 Binary files /dev/null and b/resources/images/icons2/16x16/lightning@3x.png differ diff --git a/resources/images/icons2/16x16/link@2x.png b/resources/images/icons2/16x16/link@2x.png new file mode 100644 index 00000000000..2749ed91e6c Binary files /dev/null and b/resources/images/icons2/16x16/link@2x.png differ diff --git a/resources/images/icons2/16x16/link@3x.png b/resources/images/icons2/16x16/link@3x.png new file mode 100644 index 00000000000..64862c33483 Binary files /dev/null and b/resources/images/icons2/16x16/link@3x.png differ diff --git a/resources/images/icons2/16x16/locked@2x.png b/resources/images/icons2/16x16/locked@2x.png new file mode 100644 index 00000000000..3c3b4285958 Binary files /dev/null and b/resources/images/icons2/16x16/locked@2x.png differ diff --git a/resources/images/icons2/16x16/locked@3x.png b/resources/images/icons2/16x16/locked@3x.png new file mode 100644 index 00000000000..e81cdb527bf Binary files /dev/null and b/resources/images/icons2/16x16/locked@3x.png differ diff --git a/resources/images/icons2/16x16/logout@2x.png b/resources/images/icons2/16x16/logout@2x.png new file mode 100644 index 00000000000..a7fe8dfef7b Binary files /dev/null and b/resources/images/icons2/16x16/logout@2x.png differ diff --git a/resources/images/icons2/16x16/logout@3x.png b/resources/images/icons2/16x16/logout@3x.png new file mode 100644 index 00000000000..c424f39ba5d Binary files /dev/null and b/resources/images/icons2/16x16/logout@3x.png differ diff --git a/resources/images/icons2/16x16/members@2x.png b/resources/images/icons2/16x16/members@2x.png new file mode 100644 index 00000000000..ec120fe5c21 Binary files /dev/null and b/resources/images/icons2/16x16/members@2x.png differ diff --git a/resources/images/icons2/16x16/members@3x.png b/resources/images/icons2/16x16/members@3x.png new file mode 100644 index 00000000000..639c5e32c8a Binary files /dev/null and b/resources/images/icons2/16x16/members@3x.png differ diff --git a/resources/images/icons2/16x16/mention@2x.png b/resources/images/icons2/16x16/mention@2x.png new file mode 100644 index 00000000000..5eed1046be0 Binary files /dev/null and b/resources/images/icons2/16x16/mention@2x.png differ diff --git a/resources/images/icons2/16x16/mention@3x.png b/resources/images/icons2/16x16/mention@3x.png new file mode 100644 index 00000000000..f68db7bb3a5 Binary files /dev/null and b/resources/images/icons2/16x16/mention@3x.png differ diff --git a/resources/images/icons2/16x16/messages@2x.png b/resources/images/icons2/16x16/messages@2x.png new file mode 100644 index 00000000000..00856a50133 Binary files /dev/null and b/resources/images/icons2/16x16/messages@2x.png differ diff --git a/resources/images/icons2/16x16/messages@3x.png b/resources/images/icons2/16x16/messages@3x.png new file mode 100644 index 00000000000..3e992aa2d2c Binary files /dev/null and b/resources/images/icons2/16x16/messages@3x.png differ diff --git a/resources/images/icons2/16x16/mobile@2x.png b/resources/images/icons2/16x16/mobile@2x.png new file mode 100644 index 00000000000..cde3fdea7bb Binary files /dev/null and b/resources/images/icons2/16x16/mobile@2x.png differ diff --git a/resources/images/icons2/16x16/mobile@3x.png b/resources/images/icons2/16x16/mobile@3x.png new file mode 100644 index 00000000000..0247f1aaa4d Binary files /dev/null and b/resources/images/icons2/16x16/mobile@3x.png differ diff --git a/resources/images/icons2/16x16/more@2x.png b/resources/images/icons2/16x16/more@2x.png new file mode 100644 index 00000000000..8aa9f7bbb10 Binary files /dev/null and b/resources/images/icons2/16x16/more@2x.png differ diff --git a/resources/images/icons2/16x16/more@3x.png b/resources/images/icons2/16x16/more@3x.png new file mode 100644 index 00000000000..23fca5f6ca3 Binary files /dev/null and b/resources/images/icons2/16x16/more@3x.png differ diff --git a/resources/images/icons2/16x16/mute@2x.png b/resources/images/icons2/16x16/mute@2x.png new file mode 100644 index 00000000000..b050227ed5c Binary files /dev/null and b/resources/images/icons2/16x16/mute@2x.png differ diff --git a/resources/images/icons2/16x16/mute@3x.png b/resources/images/icons2/16x16/mute@3x.png new file mode 100644 index 00000000000..39db7bc8d3c Binary files /dev/null and b/resources/images/icons2/16x16/mute@3x.png differ diff --git a/resources/images/icons2/16x16/negative-state@2x.png b/resources/images/icons2/16x16/negative-state@2x.png new file mode 100644 index 00000000000..66486423902 Binary files /dev/null and b/resources/images/icons2/16x16/negative-state@2x.png differ diff --git a/resources/images/icons2/16x16/negative-state@3x.png b/resources/images/icons2/16x16/negative-state@3x.png new file mode 100644 index 00000000000..238ef682935 Binary files /dev/null and b/resources/images/icons2/16x16/negative-state@3x.png differ diff --git a/resources/images/icons2/16x16/negative@2x.png b/resources/images/icons2/16x16/negative@2x.png new file mode 100644 index 00000000000..b14704bec0f Binary files /dev/null and b/resources/images/icons2/16x16/negative@2x.png differ diff --git a/resources/images/icons2/16x16/negative@3x.png b/resources/images/icons2/16x16/negative@3x.png new file mode 100644 index 00000000000..25fca1d9f4f Binary files /dev/null and b/resources/images/icons2/16x16/negative@3x.png differ diff --git a/resources/images/icons2/16x16/nft@2x.png b/resources/images/icons2/16x16/nft@2x.png new file mode 100644 index 00000000000..1d58d6b0352 Binary files /dev/null and b/resources/images/icons2/16x16/nft@2x.png differ diff --git a/resources/images/icons2/16x16/nft@3x.png b/resources/images/icons2/16x16/nft@3x.png new file mode 100644 index 00000000000..bd575c4def2 Binary files /dev/null and b/resources/images/icons2/16x16/nft@3x.png differ diff --git a/resources/images/icons2/16x16/not-started@2x.png b/resources/images/icons2/16x16/not-started@2x.png new file mode 100644 index 00000000000..c4c65ef461d Binary files /dev/null and b/resources/images/icons2/16x16/not-started@2x.png differ diff --git a/resources/images/icons2/16x16/not-started@3x.png b/resources/images/icons2/16x16/not-started@3x.png new file mode 100644 index 00000000000..a6e302e5a39 Binary files /dev/null and b/resources/images/icons2/16x16/not-started@3x.png differ diff --git a/resources/images/icons2/16x16/notification@2x.png b/resources/images/icons2/16x16/notification@2x.png new file mode 100644 index 00000000000..5f84a4e1597 Binary files /dev/null and b/resources/images/icons2/16x16/notification@2x.png differ diff --git a/resources/images/icons2/16x16/notification@3x.png b/resources/images/icons2/16x16/notification@3x.png new file mode 100644 index 00000000000..da11ac4121f Binary files /dev/null and b/resources/images/icons2/16x16/notification@3x.png differ diff --git a/resources/images/icons2/16x16/notifications-unread@2x.png b/resources/images/icons2/16x16/notifications-unread@2x.png new file mode 100644 index 00000000000..cfceb5ab32d Binary files /dev/null and b/resources/images/icons2/16x16/notifications-unread@2x.png differ diff --git a/resources/images/icons2/16x16/notifications-unread@3x.png b/resources/images/icons2/16x16/notifications-unread@3x.png new file mode 100644 index 00000000000..1cbc23c2de5 Binary files /dev/null and b/resources/images/icons2/16x16/notifications-unread@3x.png differ diff --git a/resources/images/icons2/16x16/online-left@2x.png b/resources/images/icons2/16x16/online-left@2x.png new file mode 100644 index 00000000000..edd2f7a6edb Binary files /dev/null and b/resources/images/icons2/16x16/online-left@2x.png differ diff --git a/resources/images/icons2/16x16/online-left@3x.png b/resources/images/icons2/16x16/online-left@3x.png new file mode 100644 index 00000000000..408423bb74d Binary files /dev/null and b/resources/images/icons2/16x16/online-left@3x.png differ diff --git a/resources/images/icons2/16x16/online@2x.png b/resources/images/icons2/16x16/online@2x.png new file mode 100644 index 00000000000..275346dc13f Binary files /dev/null and b/resources/images/icons2/16x16/online@2x.png differ diff --git a/resources/images/icons2/16x16/online@3x.png b/resources/images/icons2/16x16/online@3x.png new file mode 100644 index 00000000000..7d72958ca07 Binary files /dev/null and b/resources/images/icons2/16x16/online@3x.png differ diff --git a/resources/images/icons2/16x16/open@2x.png b/resources/images/icons2/16x16/open@2x.png new file mode 100644 index 00000000000..cad92c86bad Binary files /dev/null and b/resources/images/icons2/16x16/open@2x.png differ diff --git a/resources/images/icons2/16x16/open@3x.png b/resources/images/icons2/16x16/open@3x.png new file mode 100644 index 00000000000..57313f95a49 Binary files /dev/null and b/resources/images/icons2/16x16/open@3x.png differ diff --git a/resources/images/icons2/16x16/options@2x.png b/resources/images/icons2/16x16/options@2x.png new file mode 100644 index 00000000000..6a557ef0aa3 Binary files /dev/null and b/resources/images/icons2/16x16/options@2x.png differ diff --git a/resources/images/icons2/16x16/options@3x.png b/resources/images/icons2/16x16/options@3x.png new file mode 100644 index 00000000000..b1df450f8c7 Binary files /dev/null and b/resources/images/icons2/16x16/options@3x.png differ diff --git a/resources/images/icons2/16x16/pending-state@2x.png b/resources/images/icons2/16x16/pending-state@2x.png new file mode 100644 index 00000000000..e1b63e4b3b5 Binary files /dev/null and b/resources/images/icons2/16x16/pending-state@2x.png differ diff --git a/resources/images/icons2/16x16/pending-state@3x.png b/resources/images/icons2/16x16/pending-state@3x.png new file mode 100644 index 00000000000..0589a96cc20 Binary files /dev/null and b/resources/images/icons2/16x16/pending-state@3x.png differ diff --git a/resources/images/icons2/16x16/pending-user@2x.png b/resources/images/icons2/16x16/pending-user@2x.png new file mode 100644 index 00000000000..7fa05fcdad9 Binary files /dev/null and b/resources/images/icons2/16x16/pending-user@2x.png differ diff --git a/resources/images/icons2/16x16/pending-user@3x.png b/resources/images/icons2/16x16/pending-user@3x.png new file mode 100644 index 00000000000..e27f6e7e2e5 Binary files /dev/null and b/resources/images/icons2/16x16/pending-user@3x.png differ diff --git a/resources/images/icons2/16x16/pin@2x.png b/resources/images/icons2/16x16/pin@2x.png new file mode 100644 index 00000000000..c2f01541dbd Binary files /dev/null and b/resources/images/icons2/16x16/pin@2x.png differ diff --git a/resources/images/icons2/16x16/pin@3x.png b/resources/images/icons2/16x16/pin@3x.png new file mode 100644 index 00000000000..9aed73b08c3 Binary files /dev/null and b/resources/images/icons2/16x16/pin@3x.png differ diff --git a/resources/images/icons2/16x16/placeholder@2x.png b/resources/images/icons2/16x16/placeholder@2x.png new file mode 100644 index 00000000000..55b75df64b5 Binary files /dev/null and b/resources/images/icons2/16x16/placeholder@2x.png differ diff --git a/resources/images/icons2/16x16/placeholder@3x.png b/resources/images/icons2/16x16/placeholder@3x.png new file mode 100644 index 00000000000..606edf3db07 Binary files /dev/null and b/resources/images/icons2/16x16/placeholder@3x.png differ diff --git a/resources/images/icons2/16x16/positive-right@2x.png b/resources/images/icons2/16x16/positive-right@2x.png new file mode 100644 index 00000000000..6649d1576fc Binary files /dev/null and b/resources/images/icons2/16x16/positive-right@2x.png differ diff --git a/resources/images/icons2/16x16/positive-right@3x.png b/resources/images/icons2/16x16/positive-right@3x.png new file mode 100644 index 00000000000..1278cca64aa Binary files /dev/null and b/resources/images/icons2/16x16/positive-right@3x.png differ diff --git a/resources/images/icons2/16x16/positive-state@2x.png b/resources/images/icons2/16x16/positive-state@2x.png new file mode 100644 index 00000000000..9c17ee79929 Binary files /dev/null and b/resources/images/icons2/16x16/positive-state@2x.png differ diff --git a/resources/images/icons2/16x16/positive-state@3x.png b/resources/images/icons2/16x16/positive-state@3x.png new file mode 100644 index 00000000000..a2b50ca7bf1 Binary files /dev/null and b/resources/images/icons2/16x16/positive-state@3x.png differ diff --git a/resources/images/icons2/16x16/positive@2x.png b/resources/images/icons2/16x16/positive@2x.png new file mode 100644 index 00000000000..678668487f4 Binary files /dev/null and b/resources/images/icons2/16x16/positive@2x.png differ diff --git a/resources/images/icons2/16x16/positive@3x.png b/resources/images/icons2/16x16/positive@3x.png new file mode 100644 index 00000000000..6e2a78c5e16 Binary files /dev/null and b/resources/images/icons2/16x16/positive@3x.png differ diff --git a/resources/images/icons2/16x16/privacy@2x.png b/resources/images/icons2/16x16/privacy@2x.png new file mode 100644 index 00000000000..def4e996652 Binary files /dev/null and b/resources/images/icons2/16x16/privacy@2x.png differ diff --git a/resources/images/icons2/16x16/privacy@3x.png b/resources/images/icons2/16x16/privacy@3x.png new file mode 100644 index 00000000000..d3cf76e20eb Binary files /dev/null and b/resources/images/icons2/16x16/privacy@3x.png differ diff --git a/resources/images/icons2/16x16/profile@2x.png b/resources/images/icons2/16x16/profile@2x.png new file mode 100644 index 00000000000..78d3d0bef4b Binary files /dev/null and b/resources/images/icons2/16x16/profile@2x.png differ diff --git a/resources/images/icons2/16x16/profile@3x.png b/resources/images/icons2/16x16/profile@3x.png new file mode 100644 index 00000000000..94a988eb781 Binary files /dev/null and b/resources/images/icons2/16x16/profile@3x.png differ diff --git a/resources/images/icons2/16x16/progress@2x.png b/resources/images/icons2/16x16/progress@2x.png new file mode 100644 index 00000000000..5cba1d9e5d2 Binary files /dev/null and b/resources/images/icons2/16x16/progress@2x.png differ diff --git a/resources/images/icons2/16x16/progress@3x.png b/resources/images/icons2/16x16/progress@3x.png new file mode 100644 index 00000000000..bdc915a1f04 Binary files /dev/null and b/resources/images/icons2/16x16/progress@3x.png differ diff --git a/resources/images/icons2/16x16/pullup@2x.png b/resources/images/icons2/16x16/pullup@2x.png new file mode 100644 index 00000000000..5947a723d4a Binary files /dev/null and b/resources/images/icons2/16x16/pullup@2x.png differ diff --git a/resources/images/icons2/16x16/pullup@3x.png b/resources/images/icons2/16x16/pullup@3x.png new file mode 100644 index 00000000000..3f683c82831 Binary files /dev/null and b/resources/images/icons2/16x16/pullup@3x.png differ diff --git a/resources/images/icons2/16x16/reaction@2x.png b/resources/images/icons2/16x16/reaction@2x.png new file mode 100644 index 00000000000..80723db1735 Binary files /dev/null and b/resources/images/icons2/16x16/reaction@2x.png differ diff --git a/resources/images/icons2/16x16/reaction@3x.png b/resources/images/icons2/16x16/reaction@3x.png new file mode 100644 index 00000000000..c5ae33e5194 Binary files /dev/null and b/resources/images/icons2/16x16/reaction@3x.png differ diff --git a/resources/images/icons2/16x16/receive@2x.png b/resources/images/icons2/16x16/receive@2x.png new file mode 100644 index 00000000000..4c7bf138456 Binary files /dev/null and b/resources/images/icons2/16x16/receive@2x.png differ diff --git a/resources/images/icons2/16x16/receive@3x.png b/resources/images/icons2/16x16/receive@3x.png new file mode 100644 index 00000000000..6355fc09ecb Binary files /dev/null and b/resources/images/icons2/16x16/receive@3x.png differ diff --git a/resources/images/icons2/16x16/refresh@2x.png b/resources/images/icons2/16x16/refresh@2x.png new file mode 100644 index 00000000000..900a1e04728 Binary files /dev/null and b/resources/images/icons2/16x16/refresh@2x.png differ diff --git a/resources/images/icons2/16x16/refresh@3x.png b/resources/images/icons2/16x16/refresh@3x.png new file mode 100644 index 00000000000..c3eded8ef4f Binary files /dev/null and b/resources/images/icons2/16x16/refresh@3x.png differ diff --git a/resources/images/icons2/16x16/remove-user@2x.png b/resources/images/icons2/16x16/remove-user@2x.png new file mode 100644 index 00000000000..1bf109e4a64 Binary files /dev/null and b/resources/images/icons2/16x16/remove-user@2x.png differ diff --git a/resources/images/icons2/16x16/remove-user@3x.png b/resources/images/icons2/16x16/remove-user@3x.png new file mode 100644 index 00000000000..5a7f8647bf0 Binary files /dev/null and b/resources/images/icons2/16x16/remove-user@3x.png differ diff --git a/resources/images/icons2/16x16/remove@2x.png b/resources/images/icons2/16x16/remove@2x.png new file mode 100644 index 00000000000..b21daa58462 Binary files /dev/null and b/resources/images/icons2/16x16/remove@2x.png differ diff --git a/resources/images/icons2/16x16/remove@3x.png b/resources/images/icons2/16x16/remove@3x.png new file mode 100644 index 00000000000..e5c482ae83b Binary files /dev/null and b/resources/images/icons2/16x16/remove@3x.png differ diff --git a/resources/images/icons2/16x16/reply@2x.png b/resources/images/icons2/16x16/reply@2x.png new file mode 100644 index 00000000000..82342d1c128 Binary files /dev/null and b/resources/images/icons2/16x16/reply@2x.png differ diff --git a/resources/images/icons2/16x16/reply@3x.png b/resources/images/icons2/16x16/reply@3x.png new file mode 100644 index 00000000000..0634666b7d8 Binary files /dev/null and b/resources/images/icons2/16x16/reply@3x.png differ diff --git a/resources/images/icons2/16x16/sad-face@2x.png b/resources/images/icons2/16x16/sad-face@2x.png new file mode 100644 index 00000000000..7d1c01eedfd Binary files /dev/null and b/resources/images/icons2/16x16/sad-face@2x.png differ diff --git a/resources/images/icons2/16x16/sad-face@3x.png b/resources/images/icons2/16x16/sad-face@3x.png new file mode 100644 index 00000000000..4d9d75d2b7f Binary files /dev/null and b/resources/images/icons2/16x16/sad-face@3x.png differ diff --git a/resources/images/icons2/16x16/sad@2x.png b/resources/images/icons2/16x16/sad@2x.png new file mode 100644 index 00000000000..8347e0d2299 Binary files /dev/null and b/resources/images/icons2/16x16/sad@2x.png differ diff --git a/resources/images/icons2/16x16/sad@3x.png b/resources/images/icons2/16x16/sad@3x.png new file mode 100644 index 00000000000..23d54802493 Binary files /dev/null and b/resources/images/icons2/16x16/sad@3x.png differ diff --git a/resources/images/icons2/16x16/save@2x.png b/resources/images/icons2/16x16/save@2x.png new file mode 100644 index 00000000000..c37fc4921e5 Binary files /dev/null and b/resources/images/icons2/16x16/save@2x.png differ diff --git a/resources/images/icons2/16x16/save@3x.png b/resources/images/icons2/16x16/save@3x.png new file mode 100644 index 00000000000..dffbe9b7dd4 Binary files /dev/null and b/resources/images/icons2/16x16/save@3x.png differ diff --git a/resources/images/icons2/16x16/scanning@2x.png b/resources/images/icons2/16x16/scanning@2x.png new file mode 100644 index 00000000000..509b02716a8 Binary files /dev/null and b/resources/images/icons2/16x16/scanning@2x.png differ diff --git a/resources/images/icons2/16x16/scanning@3x.png b/resources/images/icons2/16x16/scanning@3x.png new file mode 100644 index 00000000000..b8ea24dead0 Binary files /dev/null and b/resources/images/icons2/16x16/scanning@3x.png differ diff --git a/resources/images/icons2/16x16/search@2x.png b/resources/images/icons2/16x16/search@2x.png new file mode 100644 index 00000000000..affdf2fd6b1 Binary files /dev/null and b/resources/images/icons2/16x16/search@2x.png differ diff --git a/resources/images/icons2/16x16/search@3x.png b/resources/images/icons2/16x16/search@3x.png new file mode 100644 index 00000000000..a0b3efcdda5 Binary files /dev/null and b/resources/images/icons2/16x16/search@3x.png differ diff --git a/resources/images/icons2/16x16/send@2x.png b/resources/images/icons2/16x16/send@2x.png new file mode 100644 index 00000000000..5585d8c5b28 Binary files /dev/null and b/resources/images/icons2/16x16/send@2x.png differ diff --git a/resources/images/icons2/16x16/send@3x.png b/resources/images/icons2/16x16/send@3x.png new file mode 100644 index 00000000000..1ee6562ac29 Binary files /dev/null and b/resources/images/icons2/16x16/send@3x.png differ diff --git a/resources/images/icons2/16x16/share@2x.png b/resources/images/icons2/16x16/share@2x.png new file mode 100644 index 00000000000..c8221bdb2d7 Binary files /dev/null and b/resources/images/icons2/16x16/share@2x.png differ diff --git a/resources/images/icons2/16x16/share@3x.png b/resources/images/icons2/16x16/share@3x.png new file mode 100644 index 00000000000..95bcbc3f851 Binary files /dev/null and b/resources/images/icons2/16x16/share@3x.png differ diff --git a/resources/images/icons2/16x16/sort@2x.png b/resources/images/icons2/16x16/sort@2x.png new file mode 100644 index 00000000000..5d07a21df21 Binary files /dev/null and b/resources/images/icons2/16x16/sort@2x.png differ diff --git a/resources/images/icons2/16x16/sort@3x.png b/resources/images/icons2/16x16/sort@3x.png new file mode 100644 index 00000000000..00dbf7103cf Binary files /dev/null and b/resources/images/icons2/16x16/sort@3x.png differ diff --git a/resources/images/icons2/16x16/stickers@2x.png b/resources/images/icons2/16x16/stickers@2x.png new file mode 100644 index 00000000000..e2da7806dcb Binary files /dev/null and b/resources/images/icons2/16x16/stickers@2x.png differ diff --git a/resources/images/icons2/16x16/stickers@3x.png b/resources/images/icons2/16x16/stickers@3x.png new file mode 100644 index 00000000000..719248b10ad Binary files /dev/null and b/resources/images/icons2/16x16/stickers@3x.png differ diff --git a/resources/images/icons2/16x16/swap@2x.png b/resources/images/icons2/16x16/swap@2x.png new file mode 100644 index 00000000000..def8665e0c7 Binary files /dev/null and b/resources/images/icons2/16x16/swap@2x.png differ diff --git a/resources/images/icons2/16x16/swap@3x.png b/resources/images/icons2/16x16/swap@3x.png new file mode 100644 index 00000000000..b302d5df4c9 Binary files /dev/null and b/resources/images/icons2/16x16/swap@3x.png differ diff --git a/resources/images/icons2/16x16/token@2x.png b/resources/images/icons2/16x16/token@2x.png new file mode 100644 index 00000000000..decd86a288d Binary files /dev/null and b/resources/images/icons2/16x16/token@2x.png differ diff --git a/resources/images/icons2/16x16/token@3x.png b/resources/images/icons2/16x16/token@3x.png new file mode 100644 index 00000000000..25b311b0101 Binary files /dev/null and b/resources/images/icons2/16x16/token@3x.png differ diff --git a/resources/images/icons2/16x16/unblock@2x.png b/resources/images/icons2/16x16/unblock@2x.png new file mode 100644 index 00000000000..42b268f1711 Binary files /dev/null and b/resources/images/icons2/16x16/unblock@2x.png differ diff --git a/resources/images/icons2/16x16/unblock@3x.png b/resources/images/icons2/16x16/unblock@3x.png new file mode 100644 index 00000000000..d835edd9e1d Binary files /dev/null and b/resources/images/icons2/16x16/unblock@3x.png differ diff --git a/resources/images/icons2/16x16/unlocked@2x.png b/resources/images/icons2/16x16/unlocked@2x.png new file mode 100644 index 00000000000..89c6725543f Binary files /dev/null and b/resources/images/icons2/16x16/unlocked@2x.png differ diff --git a/resources/images/icons2/16x16/unlocked@3x.png b/resources/images/icons2/16x16/unlocked@3x.png new file mode 100644 index 00000000000..d329e965683 Binary files /dev/null and b/resources/images/icons2/16x16/unlocked@3x.png differ diff --git a/resources/images/icons2/16x16/unmute@2x.png b/resources/images/icons2/16x16/unmute@2x.png new file mode 100644 index 00000000000..5763cd7691e Binary files /dev/null and b/resources/images/icons2/16x16/unmute@2x.png differ diff --git a/resources/images/icons2/16x16/unmute@3x.png b/resources/images/icons2/16x16/unmute@3x.png new file mode 100644 index 00000000000..d77a7570458 Binary files /dev/null and b/resources/images/icons2/16x16/unmute@3x.png differ diff --git a/resources/images/icons2/16x16/unpin@2x.png b/resources/images/icons2/16x16/unpin@2x.png new file mode 100644 index 00000000000..93f731704b6 Binary files /dev/null and b/resources/images/icons2/16x16/unpin@2x.png differ diff --git a/resources/images/icons2/16x16/unpin@3x.png b/resources/images/icons2/16x16/unpin@3x.png new file mode 100644 index 00000000000..0fd6e36b216 Binary files /dev/null and b/resources/images/icons2/16x16/unpin@3x.png differ diff --git a/resources/images/icons2/16x16/unread@2x.png b/resources/images/icons2/16x16/unread@2x.png new file mode 100644 index 00000000000..a3d420aad84 Binary files /dev/null and b/resources/images/icons2/16x16/unread@2x.png differ diff --git a/resources/images/icons2/16x16/unread@3x.png b/resources/images/icons2/16x16/unread@3x.png new file mode 100644 index 00000000000..fab8141ca1e Binary files /dev/null and b/resources/images/icons2/16x16/unread@3x.png differ diff --git a/resources/images/icons2/16x16/volume-max@2x.png b/resources/images/icons2/16x16/volume-max@2x.png new file mode 100644 index 00000000000..ca461ca7203 Binary files /dev/null and b/resources/images/icons2/16x16/volume-max@2x.png differ diff --git a/resources/images/icons2/16x16/volume-max@3x.png b/resources/images/icons2/16x16/volume-max@3x.png new file mode 100644 index 00000000000..47017cfbfca Binary files /dev/null and b/resources/images/icons2/16x16/volume-max@3x.png differ diff --git a/resources/images/icons2/16x16/volume-min@2x.png b/resources/images/icons2/16x16/volume-min@2x.png new file mode 100644 index 00000000000..4dac2c3296d Binary files /dev/null and b/resources/images/icons2/16x16/volume-min@2x.png differ diff --git a/resources/images/icons2/16x16/volume-min@3x.png b/resources/images/icons2/16x16/volume-min@3x.png new file mode 100644 index 00000000000..7c39375e1bb Binary files /dev/null and b/resources/images/icons2/16x16/volume-min@3x.png differ diff --git a/resources/images/icons2/16x16/volume@2x.png b/resources/images/icons2/16x16/volume@2x.png new file mode 100644 index 00000000000..1a41e91b0b0 Binary files /dev/null and b/resources/images/icons2/16x16/volume@2x.png differ diff --git a/resources/images/icons2/16x16/volume@3x.png b/resources/images/icons2/16x16/volume@3x.png new file mode 100644 index 00000000000..bd1f01eb74d Binary files /dev/null and b/resources/images/icons2/16x16/volume@3x.png differ diff --git a/resources/images/icons2/16x16/warning@2x-1.png b/resources/images/icons2/16x16/warning@2x-1.png new file mode 100644 index 00000000000..6b12355407c Binary files /dev/null and b/resources/images/icons2/16x16/warning@2x-1.png differ diff --git a/resources/images/icons2/16x16/warning@2x.png b/resources/images/icons2/16x16/warning@2x.png new file mode 100644 index 00000000000..c151ac3251a Binary files /dev/null and b/resources/images/icons2/16x16/warning@2x.png differ diff --git a/resources/images/icons2/16x16/warning@3x-1.png b/resources/images/icons2/16x16/warning@3x-1.png new file mode 100644 index 00000000000..1010a893194 Binary files /dev/null and b/resources/images/icons2/16x16/warning@3x-1.png differ diff --git a/resources/images/icons2/16x16/warning@3x.png b/resources/images/icons2/16x16/warning@3x.png new file mode 100644 index 00000000000..5642b3c288e Binary files /dev/null and b/resources/images/icons2/16x16/warning@3x.png differ diff --git a/resources/images/icons2/16x16/web@2x.png b/resources/images/icons2/16x16/web@2x.png new file mode 100644 index 00000000000..cd984bd7824 Binary files /dev/null and b/resources/images/icons2/16x16/web@2x.png differ diff --git a/resources/images/icons2/16x16/web@3x.png b/resources/images/icons2/16x16/web@3x.png new file mode 100644 index 00000000000..7c11eabf299 Binary files /dev/null and b/resources/images/icons2/16x16/web@3x.png differ diff --git a/resources/images/icons2/16x16/world@2x.png b/resources/images/icons2/16x16/world@2x.png new file mode 100644 index 00000000000..8ddb6a3657c Binary files /dev/null and b/resources/images/icons2/16x16/world@2x.png differ diff --git a/resources/images/icons2/16x16/world@3x.png b/resources/images/icons2/16x16/world@3x.png new file mode 100644 index 00000000000..71656520973 Binary files /dev/null and b/resources/images/icons2/16x16/world@3x.png differ diff --git a/resources/images/icons2/20x20/account-number@2x.png b/resources/images/icons2/20x20/account-number@2x.png new file mode 100644 index 00000000000..21216951eb2 Binary files /dev/null and b/resources/images/icons2/20x20/account-number@2x.png differ diff --git a/resources/images/icons2/20x20/account-number@3x.png b/resources/images/icons2/20x20/account-number@3x.png new file mode 100644 index 00000000000..ab1a569b8df Binary files /dev/null and b/resources/images/icons2/20x20/account-number@3x.png differ diff --git a/resources/images/icons2/20x20/active-member@2x.png b/resources/images/icons2/20x20/active-member@2x.png new file mode 100644 index 00000000000..6160085e484 Binary files /dev/null and b/resources/images/icons2/20x20/active-member@2x.png differ diff --git a/resources/images/icons2/20x20/active-member@3x.png b/resources/images/icons2/20x20/active-member@3x.png new file mode 100644 index 00000000000..6bc73944c75 Binary files /dev/null and b/resources/images/icons2/20x20/active-member@3x.png differ diff --git a/resources/images/icons2/20x20/active-members@2x.png b/resources/images/icons2/20x20/active-members@2x.png new file mode 100644 index 00000000000..b0fdb085813 Binary files /dev/null and b/resources/images/icons2/20x20/active-members@2x.png differ diff --git a/resources/images/icons2/20x20/active-members@3x.png b/resources/images/icons2/20x20/active-members@3x.png new file mode 100644 index 00000000000..ee750acf0c4 Binary files /dev/null and b/resources/images/icons2/20x20/active-members@3x.png differ diff --git a/resources/images/icons2/20x20/active@2x.png b/resources/images/icons2/20x20/active@2x.png new file mode 100644 index 00000000000..261de160596 Binary files /dev/null and b/resources/images/icons2/20x20/active@2x.png differ diff --git a/resources/images/icons2/20x20/active@3x.png b/resources/images/icons2/20x20/active@3x.png new file mode 100644 index 00000000000..6e820976582 Binary files /dev/null and b/resources/images/icons2/20x20/active@3x.png differ diff --git a/resources/images/icons2/20x20/activity-center@2x.png b/resources/images/icons2/20x20/activity-center@2x.png new file mode 100644 index 00000000000..a36505e90fd Binary files /dev/null and b/resources/images/icons2/20x20/activity-center@2x.png differ diff --git a/resources/images/icons2/20x20/activity-center@3x.png b/resources/images/icons2/20x20/activity-center@3x.png new file mode 100644 index 00000000000..cffc27a84bb Binary files /dev/null and b/resources/images/icons2/20x20/activity-center@3x.png differ diff --git a/resources/images/icons2/20x20/activity@2x.png b/resources/images/icons2/20x20/activity@2x.png new file mode 100644 index 00000000000..058706e4e30 Binary files /dev/null and b/resources/images/icons2/20x20/activity@2x.png differ diff --git a/resources/images/icons2/20x20/activity@3x.png b/resources/images/icons2/20x20/activity@3x.png new file mode 100644 index 00000000000..342a174af3f Binary files /dev/null and b/resources/images/icons2/20x20/activity@3x.png differ diff --git a/resources/images/icons2/20x20/add-reaction@2x.png b/resources/images/icons2/20x20/add-reaction@2x.png new file mode 100644 index 00000000000..207c3b4e251 Binary files /dev/null and b/resources/images/icons2/20x20/add-reaction@2x.png differ diff --git a/resources/images/icons2/20x20/add-reaction@3x.png b/resources/images/icons2/20x20/add-reaction@3x.png new file mode 100644 index 00000000000..4ed6e3db359 Binary files /dev/null and b/resources/images/icons2/20x20/add-reaction@3x.png differ diff --git a/resources/images/icons2/20x20/add-small@2x.png b/resources/images/icons2/20x20/add-small@2x.png new file mode 100644 index 00000000000..ec1a8f4ee0c Binary files /dev/null and b/resources/images/icons2/20x20/add-small@2x.png differ diff --git a/resources/images/icons2/20x20/add-small@3x.png b/resources/images/icons2/20x20/add-small@3x.png new file mode 100644 index 00000000000..cb876b6219e Binary files /dev/null and b/resources/images/icons2/20x20/add-small@3x.png differ diff --git a/resources/images/icons2/20x20/add-token@2x.png b/resources/images/icons2/20x20/add-token@2x.png new file mode 100644 index 00000000000..9293f6d9e42 Binary files /dev/null and b/resources/images/icons2/20x20/add-token@2x.png differ diff --git a/resources/images/icons2/20x20/add-token@3x.png b/resources/images/icons2/20x20/add-token@3x.png new file mode 100644 index 00000000000..8548c643d66 Binary files /dev/null and b/resources/images/icons2/20x20/add-token@3x.png differ diff --git a/resources/images/icons2/20x20/add-user@2x.png b/resources/images/icons2/20x20/add-user@2x.png new file mode 100644 index 00000000000..95ae3e8fe9c Binary files /dev/null and b/resources/images/icons2/20x20/add-user@2x.png differ diff --git a/resources/images/icons2/20x20/add-user@3x.png b/resources/images/icons2/20x20/add-user@3x.png new file mode 100644 index 00000000000..271e560cda2 Binary files /dev/null and b/resources/images/icons2/20x20/add-user@3x.png differ diff --git a/resources/images/icons2/20x20/add@2x.png b/resources/images/icons2/20x20/add@2x.png new file mode 100644 index 00000000000..cf0361ee101 Binary files /dev/null and b/resources/images/icons2/20x20/add@2x.png differ diff --git a/resources/images/icons2/20x20/add@3x.png b/resources/images/icons2/20x20/add@3x.png new file mode 100644 index 00000000000..1b8328c3c34 Binary files /dev/null and b/resources/images/icons2/20x20/add@3x.png differ diff --git a/resources/images/icons2/20x20/advanced@2x.png b/resources/images/icons2/20x20/advanced@2x.png new file mode 100644 index 00000000000..c5237b5ba18 Binary files /dev/null and b/resources/images/icons2/20x20/advanced@2x.png differ diff --git a/resources/images/icons2/20x20/advanced@3x.png b/resources/images/icons2/20x20/advanced@3x.png new file mode 100644 index 00000000000..1bea150af36 Binary files /dev/null and b/resources/images/icons2/20x20/advanced@3x.png differ diff --git a/resources/images/icons2/20x20/airdrop@2x.png b/resources/images/icons2/20x20/airdrop@2x.png new file mode 100644 index 00000000000..caf8fe440ac Binary files /dev/null and b/resources/images/icons2/20x20/airdrop@2x.png differ diff --git a/resources/images/icons2/20x20/airdrop@3x.png b/resources/images/icons2/20x20/airdrop@3x.png new file mode 100644 index 00000000000..4c2ec63bc5a Binary files /dev/null and b/resources/images/icons2/20x20/airdrop@3x.png differ diff --git a/resources/images/icons2/20x20/alert@2x.png b/resources/images/icons2/20x20/alert@2x.png new file mode 100644 index 00000000000..89c91821dc9 Binary files /dev/null and b/resources/images/icons2/20x20/alert@2x.png differ diff --git a/resources/images/icons2/20x20/alert@3x.png b/resources/images/icons2/20x20/alert@3x.png new file mode 100644 index 00000000000..a249cf273a3 Binary files /dev/null and b/resources/images/icons2/20x20/alert@3x.png differ diff --git a/resources/images/icons2/20x20/alphabetically@2x.png b/resources/images/icons2/20x20/alphabetically@2x.png new file mode 100644 index 00000000000..af47ee7fc2e Binary files /dev/null and b/resources/images/icons2/20x20/alphabetically@2x.png differ diff --git a/resources/images/icons2/20x20/alphabetically@3x.png b/resources/images/icons2/20x20/alphabetically@3x.png new file mode 100644 index 00000000000..acf9ef54999 Binary files /dev/null and b/resources/images/icons2/20x20/alphabetically@3x.png differ diff --git a/resources/images/icons2/20x20/android@2x.png b/resources/images/icons2/20x20/android@2x.png new file mode 100644 index 00000000000..7b17bb2ae4f Binary files /dev/null and b/resources/images/icons2/20x20/android@2x.png differ diff --git a/resources/images/icons2/20x20/android@3x.png b/resources/images/icons2/20x20/android@3x.png new file mode 100644 index 00000000000..ad765a922dc Binary files /dev/null and b/resources/images/icons2/20x20/android@3x.png differ diff --git a/resources/images/icons2/20x20/anonymous@2x.png b/resources/images/icons2/20x20/anonymous@2x.png new file mode 100644 index 00000000000..b6ce409cfe6 Binary files /dev/null and b/resources/images/icons2/20x20/anonymous@2x.png differ diff --git a/resources/images/icons2/20x20/anonymous@3x.png b/resources/images/icons2/20x20/anonymous@3x.png new file mode 100644 index 00000000000..5f1239f32af Binary files /dev/null and b/resources/images/icons2/20x20/anonymous@3x.png differ diff --git a/resources/images/icons2/20x20/arrow-down@2x.png b/resources/images/icons2/20x20/arrow-down@2x.png new file mode 100644 index 00000000000..7ef9d07f0a4 Binary files /dev/null and b/resources/images/icons2/20x20/arrow-down@2x.png differ diff --git a/resources/images/icons2/20x20/arrow-down@3x.png b/resources/images/icons2/20x20/arrow-down@3x.png new file mode 100644 index 00000000000..1ee6be5f90e Binary files /dev/null and b/resources/images/icons2/20x20/arrow-down@3x.png differ diff --git a/resources/images/icons2/20x20/arrow-left@2x.png b/resources/images/icons2/20x20/arrow-left@2x.png new file mode 100644 index 00000000000..cc09453bde6 Binary files /dev/null and b/resources/images/icons2/20x20/arrow-left@2x.png differ diff --git a/resources/images/icons2/20x20/arrow-left@3x.png b/resources/images/icons2/20x20/arrow-left@3x.png new file mode 100644 index 00000000000..e0531084617 Binary files /dev/null and b/resources/images/icons2/20x20/arrow-left@3x.png differ diff --git a/resources/images/icons2/20x20/arrow-right@2x.png b/resources/images/icons2/20x20/arrow-right@2x.png new file mode 100644 index 00000000000..20f725fdea0 Binary files /dev/null and b/resources/images/icons2/20x20/arrow-right@2x.png differ diff --git a/resources/images/icons2/20x20/arrow-right@3x.png b/resources/images/icons2/20x20/arrow-right@3x.png new file mode 100644 index 00000000000..b27d5ea2ffe Binary files /dev/null and b/resources/images/icons2/20x20/arrow-right@3x.png differ diff --git a/resources/images/icons2/20x20/arrow-top@2x.png b/resources/images/icons2/20x20/arrow-top@2x.png new file mode 100644 index 00000000000..5ee7a006518 Binary files /dev/null and b/resources/images/icons2/20x20/arrow-top@2x.png differ diff --git a/resources/images/icons2/20x20/arrow-top@3x.png b/resources/images/icons2/20x20/arrow-top@3x.png new file mode 100644 index 00000000000..f2937fa1d8a Binary files /dev/null and b/resources/images/icons2/20x20/arrow-top@3x.png differ diff --git a/resources/images/icons2/20x20/arrow-up@2x.png b/resources/images/icons2/20x20/arrow-up@2x.png new file mode 100644 index 00000000000..2fbb6e4d6d8 Binary files /dev/null and b/resources/images/icons2/20x20/arrow-up@2x.png differ diff --git a/resources/images/icons2/20x20/arrow-up@3x.png b/resources/images/icons2/20x20/arrow-up@3x.png new file mode 100644 index 00000000000..e8a9fee0bdb Binary files /dev/null and b/resources/images/icons2/20x20/arrow-up@3x.png differ diff --git a/resources/images/icons2/20x20/attach@2x.png b/resources/images/icons2/20x20/attach@2x.png new file mode 100644 index 00000000000..b6d8d5f704d Binary files /dev/null and b/resources/images/icons2/20x20/attach@2x.png differ diff --git a/resources/images/icons2/20x20/attach@3x.png b/resources/images/icons2/20x20/attach@3x.png new file mode 100644 index 00000000000..cd231b042f5 Binary files /dev/null and b/resources/images/icons2/20x20/attach@3x.png differ diff --git a/resources/images/icons2/20x20/audio@2x.png b/resources/images/icons2/20x20/audio@2x.png new file mode 100644 index 00000000000..e8af8c6c0d1 Binary files /dev/null and b/resources/images/icons2/20x20/audio@2x.png differ diff --git a/resources/images/icons2/20x20/audio@3x.png b/resources/images/icons2/20x20/audio@3x.png new file mode 100644 index 00000000000..a9339e3f425 Binary files /dev/null and b/resources/images/icons2/20x20/audio@3x.png differ diff --git a/resources/images/icons2/20x20/automatic@2x.png b/resources/images/icons2/20x20/automatic@2x.png new file mode 100644 index 00000000000..a2723d294ae Binary files /dev/null and b/resources/images/icons2/20x20/automatic@2x.png differ diff --git a/resources/images/icons2/20x20/automatic@3x.png b/resources/images/icons2/20x20/automatic@3x.png new file mode 100644 index 00000000000..3a96a8a6510 Binary files /dev/null and b/resources/images/icons2/20x20/automatic@3x.png differ diff --git a/resources/images/icons2/20x20/backspace@2x.png b/resources/images/icons2/20x20/backspace@2x.png new file mode 100644 index 00000000000..1a31ec9b2d2 Binary files /dev/null and b/resources/images/icons2/20x20/backspace@2x.png differ diff --git a/resources/images/icons2/20x20/backspace@3x.png b/resources/images/icons2/20x20/backspace@3x.png new file mode 100644 index 00000000000..101e52c6153 Binary files /dev/null and b/resources/images/icons2/20x20/backspace@3x.png differ diff --git a/resources/images/icons2/20x20/ban-user@2x.png b/resources/images/icons2/20x20/ban-user@2x.png new file mode 100644 index 00000000000..c5bec662bdd Binary files /dev/null and b/resources/images/icons2/20x20/ban-user@2x.png differ diff --git a/resources/images/icons2/20x20/ban-user@3x.png b/resources/images/icons2/20x20/ban-user@3x.png new file mode 100644 index 00000000000..d27b1fa571a Binary files /dev/null and b/resources/images/icons2/20x20/ban-user@3x.png differ diff --git a/resources/images/icons2/20x20/block@2x.png b/resources/images/icons2/20x20/block@2x.png new file mode 100644 index 00000000000..d550558bb7e Binary files /dev/null and b/resources/images/icons2/20x20/block@2x.png differ diff --git a/resources/images/icons2/20x20/block@3x.png b/resources/images/icons2/20x20/block@3x.png new file mode 100644 index 00000000000..305c0f8149d Binary files /dev/null and b/resources/images/icons2/20x20/block@3x.png differ diff --git a/resources/images/icons2/20x20/bold@2x.png b/resources/images/icons2/20x20/bold@2x.png new file mode 100644 index 00000000000..35df78dad9f Binary files /dev/null and b/resources/images/icons2/20x20/bold@2x.png differ diff --git a/resources/images/icons2/20x20/bold@3x.png b/resources/images/icons2/20x20/bold@3x.png new file mode 100644 index 00000000000..c3289aa342b Binary files /dev/null and b/resources/images/icons2/20x20/bold@3x.png differ diff --git a/resources/images/icons2/20x20/bridge@2x.png b/resources/images/icons2/20x20/bridge@2x.png new file mode 100644 index 00000000000..05b5cdd45bb Binary files /dev/null and b/resources/images/icons2/20x20/bridge@2x.png differ diff --git a/resources/images/icons2/20x20/bridge@3x.png b/resources/images/icons2/20x20/bridge@3x.png new file mode 100644 index 00000000000..e55f8a977d0 Binary files /dev/null and b/resources/images/icons2/20x20/bridge@3x.png differ diff --git a/resources/images/icons2/20x20/browser-context@2x.png b/resources/images/icons2/20x20/browser-context@2x.png new file mode 100644 index 00000000000..f2c45bb303a Binary files /dev/null and b/resources/images/icons2/20x20/browser-context@2x.png differ diff --git a/resources/images/icons2/20x20/browser-context@3x.png b/resources/images/icons2/20x20/browser-context@3x.png new file mode 100644 index 00000000000..78a1bdc4f27 Binary files /dev/null and b/resources/images/icons2/20x20/browser-context@3x.png differ diff --git a/resources/images/icons2/20x20/browser@2x.png b/resources/images/icons2/20x20/browser@2x.png new file mode 100644 index 00000000000..c298086ca3c Binary files /dev/null and b/resources/images/icons2/20x20/browser@2x.png differ diff --git a/resources/images/icons2/20x20/browser@3x.png b/resources/images/icons2/20x20/browser@3x.png new file mode 100644 index 00000000000..477de76f949 Binary files /dev/null and b/resources/images/icons2/20x20/browser@3x.png differ diff --git a/resources/images/icons2/20x20/bullet-list@2x.png b/resources/images/icons2/20x20/bullet-list@2x.png new file mode 100644 index 00000000000..bb173ea78de Binary files /dev/null and b/resources/images/icons2/20x20/bullet-list@2x.png differ diff --git a/resources/images/icons2/20x20/bullet-list@3x.png b/resources/images/icons2/20x20/bullet-list@3x.png new file mode 100644 index 00000000000..cf2a0030573 Binary files /dev/null and b/resources/images/icons2/20x20/bullet-list@3x.png differ diff --git a/resources/images/icons2/20x20/bullet@2x.png b/resources/images/icons2/20x20/bullet@2x.png new file mode 100644 index 00000000000..535de03690e Binary files /dev/null and b/resources/images/icons2/20x20/bullet@2x.png differ diff --git a/resources/images/icons2/20x20/bullet@3x.png b/resources/images/icons2/20x20/bullet@3x.png new file mode 100644 index 00000000000..7a0eb208d29 Binary files /dev/null and b/resources/images/icons2/20x20/bullet@3x.png differ diff --git a/resources/images/icons2/20x20/buy@2x.png b/resources/images/icons2/20x20/buy@2x.png new file mode 100644 index 00000000000..54ed321b357 Binary files /dev/null and b/resources/images/icons2/20x20/buy@2x.png differ diff --git a/resources/images/icons2/20x20/buy@3x.png b/resources/images/icons2/20x20/buy@3x.png new file mode 100644 index 00000000000..d356dd72736 Binary files /dev/null and b/resources/images/icons2/20x20/buy@3x.png differ diff --git a/resources/images/icons2/20x20/calendar@2x.png b/resources/images/icons2/20x20/calendar@2x.png new file mode 100644 index 00000000000..877cd4ea9aa Binary files /dev/null and b/resources/images/icons2/20x20/calendar@2x.png differ diff --git a/resources/images/icons2/20x20/calendar@3x.png b/resources/images/icons2/20x20/calendar@3x.png new file mode 100644 index 00000000000..5fc73e64ac2 Binary files /dev/null and b/resources/images/icons2/20x20/calendar@3x.png differ diff --git a/resources/images/icons2/20x20/camera@2x.png b/resources/images/icons2/20x20/camera@2x.png new file mode 100644 index 00000000000..3a832533a42 Binary files /dev/null and b/resources/images/icons2/20x20/camera@2x.png differ diff --git a/resources/images/icons2/20x20/camera@3x.png b/resources/images/icons2/20x20/camera@3x.png new file mode 100644 index 00000000000..0b162547afc Binary files /dev/null and b/resources/images/icons2/20x20/camera@3x.png differ diff --git a/resources/images/icons2/20x20/card-view@2x.png b/resources/images/icons2/20x20/card-view@2x.png new file mode 100644 index 00000000000..bf1d1eccf8e Binary files /dev/null and b/resources/images/icons2/20x20/card-view@2x.png differ diff --git a/resources/images/icons2/20x20/card-view@3x.png b/resources/images/icons2/20x20/card-view@3x.png new file mode 100644 index 00000000000..deb393fee9d Binary files /dev/null and b/resources/images/icons2/20x20/card-view@3x.png differ diff --git a/resources/images/icons2/20x20/centre-align@2x.png b/resources/images/icons2/20x20/centre-align@2x.png new file mode 100644 index 00000000000..bd6f1d16436 Binary files /dev/null and b/resources/images/icons2/20x20/centre-align@2x.png differ diff --git a/resources/images/icons2/20x20/centre-align@3x.png b/resources/images/icons2/20x20/centre-align@3x.png new file mode 100644 index 00000000000..a05ab5d695c Binary files /dev/null and b/resources/images/icons2/20x20/centre-align@3x.png differ diff --git a/resources/images/icons2/20x20/chart@2x.png b/resources/images/icons2/20x20/chart@2x.png new file mode 100644 index 00000000000..7ad94081862 Binary files /dev/null and b/resources/images/icons2/20x20/chart@2x.png differ diff --git a/resources/images/icons2/20x20/chart@3x.png b/resources/images/icons2/20x20/chart@3x.png new file mode 100644 index 00000000000..2c251494f17 Binary files /dev/null and b/resources/images/icons2/20x20/chart@3x.png differ diff --git a/resources/images/icons2/20x20/chatkey@2x.png b/resources/images/icons2/20x20/chatkey@2x.png new file mode 100644 index 00000000000..429448b5e52 Binary files /dev/null and b/resources/images/icons2/20x20/chatkey@2x.png differ diff --git a/resources/images/icons2/20x20/chatkey@3x.png b/resources/images/icons2/20x20/chatkey@3x.png new file mode 100644 index 00000000000..200fb779fac Binary files /dev/null and b/resources/images/icons2/20x20/chatkey@3x.png differ diff --git a/resources/images/icons2/20x20/check-circle@2x.png b/resources/images/icons2/20x20/check-circle@2x.png new file mode 100644 index 00000000000..f0ae3eb903a Binary files /dev/null and b/resources/images/icons2/20x20/check-circle@2x.png differ diff --git a/resources/images/icons2/20x20/check-circle@3x.png b/resources/images/icons2/20x20/check-circle@3x.png new file mode 100644 index 00000000000..bcb845afdcd Binary files /dev/null and b/resources/images/icons2/20x20/check-circle@3x.png differ diff --git a/resources/images/icons2/20x20/check-small@2x.png b/resources/images/icons2/20x20/check-small@2x.png new file mode 100644 index 00000000000..3f097233a0f Binary files /dev/null and b/resources/images/icons2/20x20/check-small@2x.png differ diff --git a/resources/images/icons2/20x20/check-small@3x.png b/resources/images/icons2/20x20/check-small@3x.png new file mode 100644 index 00000000000..e51e59afbab Binary files /dev/null and b/resources/images/icons2/20x20/check-small@3x.png differ diff --git a/resources/images/icons2/20x20/check@2x.png b/resources/images/icons2/20x20/check@2x.png new file mode 100644 index 00000000000..8103df13a5d Binary files /dev/null and b/resources/images/icons2/20x20/check@2x.png differ diff --git a/resources/images/icons2/20x20/check@3x.png b/resources/images/icons2/20x20/check@3x.png new file mode 100644 index 00000000000..398f1dcf665 Binary files /dev/null and b/resources/images/icons2/20x20/check@3x.png differ diff --git a/resources/images/icons2/20x20/checktoken@2x.png b/resources/images/icons2/20x20/checktoken@2x.png new file mode 100644 index 00000000000..54eb0f61d2b Binary files /dev/null and b/resources/images/icons2/20x20/checktoken@2x.png differ diff --git a/resources/images/icons2/20x20/checktoken@3x.png b/resources/images/icons2/20x20/checktoken@3x.png new file mode 100644 index 00000000000..a5f688be952 Binary files /dev/null and b/resources/images/icons2/20x20/checktoken@3x.png differ diff --git a/resources/images/icons2/20x20/chevron-down@2x.png b/resources/images/icons2/20x20/chevron-down@2x.png new file mode 100644 index 00000000000..cc67b1f7d68 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-down@2x.png differ diff --git a/resources/images/icons2/20x20/chevron-down@3x.png b/resources/images/icons2/20x20/chevron-down@3x.png new file mode 100644 index 00000000000..783e6e2d2a8 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-down@3x.png differ diff --git a/resources/images/icons2/20x20/chevron-left@2x.png b/resources/images/icons2/20x20/chevron-left@2x.png new file mode 100644 index 00000000000..1a0cad19a00 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-left@2x.png differ diff --git a/resources/images/icons2/20x20/chevron-left@3x.png b/resources/images/icons2/20x20/chevron-left@3x.png new file mode 100644 index 00000000000..7aabc6a41a7 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-left@3x.png differ diff --git a/resources/images/icons2/20x20/chevron-right@2x.png b/resources/images/icons2/20x20/chevron-right@2x.png new file mode 100644 index 00000000000..a86b9cfba9a Binary files /dev/null and b/resources/images/icons2/20x20/chevron-right@2x.png differ diff --git a/resources/images/icons2/20x20/chevron-right@3x.png b/resources/images/icons2/20x20/chevron-right@3x.png new file mode 100644 index 00000000000..2dacf5f6292 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-right@3x.png differ diff --git a/resources/images/icons2/20x20/chevron-top@2x.png b/resources/images/icons2/20x20/chevron-top@2x.png new file mode 100644 index 00000000000..263919ff229 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-top@2x.png differ diff --git a/resources/images/icons2/20x20/chevron-top@3x.png b/resources/images/icons2/20x20/chevron-top@3x.png new file mode 100644 index 00000000000..3d4f6d54cd6 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-top@3x.png differ diff --git a/resources/images/icons2/20x20/chevron-up@2x.png b/resources/images/icons2/20x20/chevron-up@2x.png new file mode 100644 index 00000000000..ccf22ee9123 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-up@2x.png differ diff --git a/resources/images/icons2/20x20/chevron-up@3x.png b/resources/images/icons2/20x20/chevron-up@3x.png new file mode 100644 index 00000000000..c5f1c3327b9 Binary files /dev/null and b/resources/images/icons2/20x20/chevron-up@3x.png differ diff --git a/resources/images/icons2/20x20/chevrons-left@2x.png b/resources/images/icons2/20x20/chevrons-left@2x.png new file mode 100644 index 00000000000..4847f3244d0 Binary files /dev/null and b/resources/images/icons2/20x20/chevrons-left@2x.png differ diff --git a/resources/images/icons2/20x20/chevrons-left@3x.png b/resources/images/icons2/20x20/chevrons-left@3x.png new file mode 100644 index 00000000000..872d7dbdf61 Binary files /dev/null and b/resources/images/icons2/20x20/chevrons-left@3x.png differ diff --git a/resources/images/icons2/20x20/chevrons-right@2x.png b/resources/images/icons2/20x20/chevrons-right@2x.png new file mode 100644 index 00000000000..c863dd34573 Binary files /dev/null and b/resources/images/icons2/20x20/chevrons-right@2x.png differ diff --git a/resources/images/icons2/20x20/chevrons-right@3x.png b/resources/images/icons2/20x20/chevrons-right@3x.png new file mode 100644 index 00000000000..a4368390b46 Binary files /dev/null and b/resources/images/icons2/20x20/chevrons-right@3x.png differ diff --git a/resources/images/icons2/20x20/circle@2x.png b/resources/images/icons2/20x20/circle@2x.png new file mode 100644 index 00000000000..34fa68f16cb Binary files /dev/null and b/resources/images/icons2/20x20/circle@2x.png differ diff --git a/resources/images/icons2/20x20/circle@3x.png b/resources/images/icons2/20x20/circle@3x.png new file mode 100644 index 00000000000..bde6cd08db8 Binary files /dev/null and b/resources/images/icons2/20x20/circle@3x.png differ diff --git a/resources/images/icons2/20x20/clear@2x.png b/resources/images/icons2/20x20/clear@2x.png new file mode 100644 index 00000000000..4156d53209c Binary files /dev/null and b/resources/images/icons2/20x20/clear@2x.png differ diff --git a/resources/images/icons2/20x20/clear@3x.png b/resources/images/icons2/20x20/clear@3x.png new file mode 100644 index 00000000000..a6e4c00a704 Binary files /dev/null and b/resources/images/icons2/20x20/clear@3x.png differ diff --git a/resources/images/icons2/20x20/close-circle@2x.png b/resources/images/icons2/20x20/close-circle@2x.png new file mode 100644 index 00000000000..4253dc53e13 Binary files /dev/null and b/resources/images/icons2/20x20/close-circle@2x.png differ diff --git a/resources/images/icons2/20x20/close-circle@3x.png b/resources/images/icons2/20x20/close-circle@3x.png new file mode 100644 index 00000000000..ed2265f0989 Binary files /dev/null and b/resources/images/icons2/20x20/close-circle@3x.png differ diff --git a/resources/images/icons2/20x20/close@2x.png b/resources/images/icons2/20x20/close@2x.png new file mode 100644 index 00000000000..4b68ba60646 Binary files /dev/null and b/resources/images/icons2/20x20/close@2x.png differ diff --git a/resources/images/icons2/20x20/close@3x.png b/resources/images/icons2/20x20/close@3x.png new file mode 100644 index 00000000000..306933bc04e Binary files /dev/null and b/resources/images/icons2/20x20/close@3x.png differ diff --git a/resources/images/icons2/20x20/code-block@2x.png b/resources/images/icons2/20x20/code-block@2x.png new file mode 100644 index 00000000000..4b082de3515 Binary files /dev/null and b/resources/images/icons2/20x20/code-block@2x.png differ diff --git a/resources/images/icons2/20x20/code-block@3x.png b/resources/images/icons2/20x20/code-block@3x.png new file mode 100644 index 00000000000..54ddd82e14d Binary files /dev/null and b/resources/images/icons2/20x20/code-block@3x.png differ diff --git a/resources/images/icons2/20x20/code@2x.png b/resources/images/icons2/20x20/code@2x.png new file mode 100644 index 00000000000..720f35730bf Binary files /dev/null and b/resources/images/icons2/20x20/code@2x.png differ diff --git a/resources/images/icons2/20x20/code@3x.png b/resources/images/icons2/20x20/code@3x.png new file mode 100644 index 00000000000..b15a712fa8d Binary files /dev/null and b/resources/images/icons2/20x20/code@3x.png differ diff --git a/resources/images/icons2/20x20/collapse-ls@2x.png b/resources/images/icons2/20x20/collapse-ls@2x.png new file mode 100644 index 00000000000..9fb210ca5dd Binary files /dev/null and b/resources/images/icons2/20x20/collapse-ls@2x.png differ diff --git a/resources/images/icons2/20x20/collapse-ls@3x.png b/resources/images/icons2/20x20/collapse-ls@3x.png new file mode 100644 index 00000000000..9ab62f7b47a Binary files /dev/null and b/resources/images/icons2/20x20/collapse-ls@3x.png differ diff --git a/resources/images/icons2/20x20/collapse-rs@2x.png b/resources/images/icons2/20x20/collapse-rs@2x.png new file mode 100644 index 00000000000..acf948bc9c5 Binary files /dev/null and b/resources/images/icons2/20x20/collapse-rs@2x.png differ diff --git a/resources/images/icons2/20x20/collapse-rs@3x.png b/resources/images/icons2/20x20/collapse-rs@3x.png new file mode 100644 index 00000000000..17bfa118979 Binary files /dev/null and b/resources/images/icons2/20x20/collapse-rs@3x.png differ diff --git a/resources/images/icons2/20x20/collapse-topbar@2x.png b/resources/images/icons2/20x20/collapse-topbar@2x.png new file mode 100644 index 00000000000..08e3a42ae32 Binary files /dev/null and b/resources/images/icons2/20x20/collapse-topbar@2x.png differ diff --git a/resources/images/icons2/20x20/collapse-topbar@3x.png b/resources/images/icons2/20x20/collapse-topbar@3x.png new file mode 100644 index 00000000000..55e42a68213 Binary files /dev/null and b/resources/images/icons2/20x20/collapse-topbar@3x.png differ diff --git a/resources/images/icons2/20x20/collapse@2x.png b/resources/images/icons2/20x20/collapse@2x.png new file mode 100644 index 00000000000..eb756ae848e Binary files /dev/null and b/resources/images/icons2/20x20/collapse@2x.png differ diff --git a/resources/images/icons2/20x20/collapse@3x.png b/resources/images/icons2/20x20/collapse@3x.png new file mode 100644 index 00000000000..f3bd3a727a4 Binary files /dev/null and b/resources/images/icons2/20x20/collapse@3x.png differ diff --git a/resources/images/icons2/20x20/colour-pick@2x.png b/resources/images/icons2/20x20/colour-pick@2x.png new file mode 100644 index 00000000000..5257d6df6b5 Binary files /dev/null and b/resources/images/icons2/20x20/colour-pick@2x.png differ diff --git a/resources/images/icons2/20x20/colour-pick@3x.png b/resources/images/icons2/20x20/colour-pick@3x.png new file mode 100644 index 00000000000..6529d8730a3 Binary files /dev/null and b/resources/images/icons2/20x20/colour-pick@3x.png differ diff --git a/resources/images/icons2/20x20/command@2x.png b/resources/images/icons2/20x20/command@2x.png new file mode 100644 index 00000000000..2b91f64d783 Binary files /dev/null and b/resources/images/icons2/20x20/command@2x.png differ diff --git a/resources/images/icons2/20x20/command@3x.png b/resources/images/icons2/20x20/command@3x.png new file mode 100644 index 00000000000..8e7397129dc Binary files /dev/null and b/resources/images/icons2/20x20/command@3x.png differ diff --git a/resources/images/icons2/20x20/communities@2x.png b/resources/images/icons2/20x20/communities@2x.png new file mode 100644 index 00000000000..a353a11d7b1 Binary files /dev/null and b/resources/images/icons2/20x20/communities@2x.png differ diff --git a/resources/images/icons2/20x20/communities@3x.png b/resources/images/icons2/20x20/communities@3x.png new file mode 100644 index 00000000000..573ee04c5a0 Binary files /dev/null and b/resources/images/icons2/20x20/communities@3x.png differ diff --git a/resources/images/icons2/20x20/compact@2x.png b/resources/images/icons2/20x20/compact@2x.png new file mode 100644 index 00000000000..6fbeac1671f Binary files /dev/null and b/resources/images/icons2/20x20/compact@2x.png differ diff --git a/resources/images/icons2/20x20/compact@3x.png b/resources/images/icons2/20x20/compact@3x.png new file mode 100644 index 00000000000..163fe408a62 Binary files /dev/null and b/resources/images/icons2/20x20/compact@3x.png differ diff --git a/resources/images/icons2/20x20/complete-id@2x.png b/resources/images/icons2/20x20/complete-id@2x.png new file mode 100644 index 00000000000..b9df5746167 Binary files /dev/null and b/resources/images/icons2/20x20/complete-id@2x.png differ diff --git a/resources/images/icons2/20x20/complete-id@3x.png b/resources/images/icons2/20x20/complete-id@3x.png new file mode 100644 index 00000000000..2622110e34e Binary files /dev/null and b/resources/images/icons2/20x20/complete-id@3x.png differ diff --git a/resources/images/icons2/20x20/confetti@2x.png b/resources/images/icons2/20x20/confetti@2x.png new file mode 100644 index 00000000000..152199161d2 Binary files /dev/null and b/resources/images/icons2/20x20/confetti@2x.png differ diff --git a/resources/images/icons2/20x20/confetti@3x.png b/resources/images/icons2/20x20/confetti@3x.png new file mode 100644 index 00000000000..52095884d5e Binary files /dev/null and b/resources/images/icons2/20x20/confetti@3x.png differ diff --git a/resources/images/icons2/20x20/connection@2x.png b/resources/images/icons2/20x20/connection@2x.png new file mode 100644 index 00000000000..8f358842ae0 Binary files /dev/null and b/resources/images/icons2/20x20/connection@2x.png differ diff --git a/resources/images/icons2/20x20/connection@3x.png b/resources/images/icons2/20x20/connection@3x.png new file mode 100644 index 00000000000..a05ca71456d Binary files /dev/null and b/resources/images/icons2/20x20/connection@3x.png differ diff --git a/resources/images/icons2/20x20/contact-book@2x.png b/resources/images/icons2/20x20/contact-book@2x.png new file mode 100644 index 00000000000..92815baaf18 Binary files /dev/null and b/resources/images/icons2/20x20/contact-book@2x.png differ diff --git a/resources/images/icons2/20x20/contact-book@3x.png b/resources/images/icons2/20x20/contact-book@3x.png new file mode 100644 index 00000000000..d1a00102661 Binary files /dev/null and b/resources/images/icons2/20x20/contact-book@3x.png differ diff --git a/resources/images/icons2/20x20/contact@2x.png b/resources/images/icons2/20x20/contact@2x.png new file mode 100644 index 00000000000..686a68ec480 Binary files /dev/null and b/resources/images/icons2/20x20/contact@2x.png differ diff --git a/resources/images/icons2/20x20/contact@3x.png b/resources/images/icons2/20x20/contact@3x.png new file mode 100644 index 00000000000..14bdd5e9e24 Binary files /dev/null and b/resources/images/icons2/20x20/contact@3x.png differ diff --git a/resources/images/icons2/20x20/copy@2x.png b/resources/images/icons2/20x20/copy@2x.png new file mode 100644 index 00000000000..c431eaea3c9 Binary files /dev/null and b/resources/images/icons2/20x20/copy@2x.png differ diff --git a/resources/images/icons2/20x20/copy@3x.png b/resources/images/icons2/20x20/copy@3x.png new file mode 100644 index 00000000000..48a998ecaff Binary files /dev/null and b/resources/images/icons2/20x20/copy@3x.png differ diff --git a/resources/images/icons2/20x20/correct-dark@2x.png b/resources/images/icons2/20x20/correct-dark@2x.png new file mode 100644 index 00000000000..4ca3dc50977 Binary files /dev/null and b/resources/images/icons2/20x20/correct-dark@2x.png differ diff --git a/resources/images/icons2/20x20/correct-dark@3x.png b/resources/images/icons2/20x20/correct-dark@3x.png new file mode 100644 index 00000000000..c8b12c7da95 Binary files /dev/null and b/resources/images/icons2/20x20/correct-dark@3x.png differ diff --git a/resources/images/icons2/20x20/correct@2x.png b/resources/images/icons2/20x20/correct@2x.png new file mode 100644 index 00000000000..591397667ea Binary files /dev/null and b/resources/images/icons2/20x20/correct@2x.png differ diff --git a/resources/images/icons2/20x20/correct@3x.png b/resources/images/icons2/20x20/correct@3x.png new file mode 100644 index 00000000000..5bc188ae5fa Binary files /dev/null and b/resources/images/icons2/20x20/correct@3x.png differ diff --git a/resources/images/icons2/20x20/cross-circle@2x.png b/resources/images/icons2/20x20/cross-circle@2x.png new file mode 100644 index 00000000000..5cb36d4036d Binary files /dev/null and b/resources/images/icons2/20x20/cross-circle@2x.png differ diff --git a/resources/images/icons2/20x20/cross-circle@3x.png b/resources/images/icons2/20x20/cross-circle@3x.png new file mode 100644 index 00000000000..8371ad349bd Binary files /dev/null and b/resources/images/icons2/20x20/cross-circle@3x.png differ diff --git a/resources/images/icons2/20x20/crown@2x.png b/resources/images/icons2/20x20/crown@2x.png new file mode 100644 index 00000000000..1abf84cc0d9 Binary files /dev/null and b/resources/images/icons2/20x20/crown@2x.png differ diff --git a/resources/images/icons2/20x20/crown@3x.png b/resources/images/icons2/20x20/crown@3x.png new file mode 100644 index 00000000000..936fb699afe Binary files /dev/null and b/resources/images/icons2/20x20/crown@3x.png differ diff --git a/resources/images/icons2/20x20/customize@2x.png b/resources/images/icons2/20x20/customize@2x.png new file mode 100644 index 00000000000..987ecc08bdd Binary files /dev/null and b/resources/images/icons2/20x20/customize@2x.png differ diff --git a/resources/images/icons2/20x20/customize@3x.png b/resources/images/icons2/20x20/customize@3x.png new file mode 100644 index 00000000000..5a6d40e2f66 Binary files /dev/null and b/resources/images/icons2/20x20/customize@3x.png differ diff --git a/resources/images/icons2/20x20/danger@2x.png b/resources/images/icons2/20x20/danger@2x.png new file mode 100644 index 00000000000..834aea6a961 Binary files /dev/null and b/resources/images/icons2/20x20/danger@2x.png differ diff --git a/resources/images/icons2/20x20/danger@3x.png b/resources/images/icons2/20x20/danger@3x.png new file mode 100644 index 00000000000..efa4bd5ec2b Binary files /dev/null and b/resources/images/icons2/20x20/danger@3x.png differ diff --git a/resources/images/icons2/20x20/dapps@2x.png b/resources/images/icons2/20x20/dapps@2x.png new file mode 100644 index 00000000000..35abe8064ef Binary files /dev/null and b/resources/images/icons2/20x20/dapps@2x.png differ diff --git a/resources/images/icons2/20x20/dapps@3x.png b/resources/images/icons2/20x20/dapps@3x.png new file mode 100644 index 00000000000..b905d606f07 Binary files /dev/null and b/resources/images/icons2/20x20/dapps@3x.png differ diff --git a/resources/images/icons2/20x20/dark@2x.png b/resources/images/icons2/20x20/dark@2x.png new file mode 100644 index 00000000000..2ea1a5e18a6 Binary files /dev/null and b/resources/images/icons2/20x20/dark@2x.png differ diff --git a/resources/images/icons2/20x20/dark@3x.png b/resources/images/icons2/20x20/dark@3x.png new file mode 100644 index 00000000000..084f2ae83fe Binary files /dev/null and b/resources/images/icons2/20x20/dark@3x.png differ diff --git a/resources/images/icons2/20x20/dashboard@2x.png b/resources/images/icons2/20x20/dashboard@2x.png new file mode 100644 index 00000000000..4473791160f Binary files /dev/null and b/resources/images/icons2/20x20/dashboard@2x.png differ diff --git a/resources/images/icons2/20x20/dashboard@3x.png b/resources/images/icons2/20x20/dashboard@3x.png new file mode 100644 index 00000000000..5451884311a Binary files /dev/null and b/resources/images/icons2/20x20/dashboard@3x.png differ diff --git a/resources/images/icons2/20x20/data-usage@2x.png b/resources/images/icons2/20x20/data-usage@2x.png new file mode 100644 index 00000000000..c217d2b5a5b Binary files /dev/null and b/resources/images/icons2/20x20/data-usage@2x.png differ diff --git a/resources/images/icons2/20x20/data-usage@3x.png b/resources/images/icons2/20x20/data-usage@3x.png new file mode 100644 index 00000000000..f60b740711b Binary files /dev/null and b/resources/images/icons2/20x20/data-usage@3x.png differ diff --git a/resources/images/icons2/20x20/decline@2x.png b/resources/images/icons2/20x20/decline@2x.png new file mode 100644 index 00000000000..414e629513e Binary files /dev/null and b/resources/images/icons2/20x20/decline@2x.png differ diff --git a/resources/images/icons2/20x20/decline@3x.png b/resources/images/icons2/20x20/decline@3x.png new file mode 100644 index 00000000000..6931a39d711 Binary files /dev/null and b/resources/images/icons2/20x20/decline@3x.png differ diff --git a/resources/images/icons2/20x20/delete@2x.png b/resources/images/icons2/20x20/delete@2x.png new file mode 100644 index 00000000000..25618b6b47a Binary files /dev/null and b/resources/images/icons2/20x20/delete@2x.png differ diff --git a/resources/images/icons2/20x20/delete@3x.png b/resources/images/icons2/20x20/delete@3x.png new file mode 100644 index 00000000000..f9ab8b7cabe Binary files /dev/null and b/resources/images/icons2/20x20/delete@3x.png differ diff --git a/resources/images/icons2/20x20/derivated-path@2x.png b/resources/images/icons2/20x20/derivated-path@2x.png new file mode 100644 index 00000000000..2f6ba45fd18 Binary files /dev/null and b/resources/images/icons2/20x20/derivated-path@2x.png differ diff --git a/resources/images/icons2/20x20/derivated-path@3x.png b/resources/images/icons2/20x20/derivated-path@3x.png new file mode 100644 index 00000000000..e98ff3ee235 Binary files /dev/null and b/resources/images/icons2/20x20/derivated-path@3x.png differ diff --git a/resources/images/icons2/20x20/desktop@2x.png b/resources/images/icons2/20x20/desktop@2x.png new file mode 100644 index 00000000000..c51fae43ec9 Binary files /dev/null and b/resources/images/icons2/20x20/desktop@2x.png differ diff --git a/resources/images/icons2/20x20/desktop@3x.png b/resources/images/icons2/20x20/desktop@3x.png new file mode 100644 index 00000000000..0326454d1de Binary files /dev/null and b/resources/images/icons2/20x20/desktop@3x.png differ diff --git a/resources/images/icons2/20x20/destroy@2x.png b/resources/images/icons2/20x20/destroy@2x.png new file mode 100644 index 00000000000..7a321e1f22d Binary files /dev/null and b/resources/images/icons2/20x20/destroy@2x.png differ diff --git a/resources/images/icons2/20x20/destroy@3x.png b/resources/images/icons2/20x20/destroy@3x.png new file mode 100644 index 00000000000..98ae8a06d9a Binary files /dev/null and b/resources/images/icons2/20x20/destroy@3x.png differ diff --git a/resources/images/icons2/20x20/disconnect@2x.png b/resources/images/icons2/20x20/disconnect@2x.png new file mode 100644 index 00000000000..a4b7bdb508f Binary files /dev/null and b/resources/images/icons2/20x20/disconnect@2x.png differ diff --git a/resources/images/icons2/20x20/disconnect@3x.png b/resources/images/icons2/20x20/disconnect@3x.png new file mode 100644 index 00000000000..9a7a785b5ef Binary files /dev/null and b/resources/images/icons2/20x20/disconnect@3x.png differ diff --git a/resources/images/icons2/20x20/dock@2x.png b/resources/images/icons2/20x20/dock@2x.png new file mode 100644 index 00000000000..f86b04c17b6 Binary files /dev/null and b/resources/images/icons2/20x20/dock@2x.png differ diff --git a/resources/images/icons2/20x20/dock@3x.png b/resources/images/icons2/20x20/dock@3x.png new file mode 100644 index 00000000000..34f17aec824 Binary files /dev/null and b/resources/images/icons2/20x20/dock@3x.png differ diff --git a/resources/images/icons2/20x20/done@2x.png b/resources/images/icons2/20x20/done@2x.png new file mode 100644 index 00000000000..780649a5ec5 Binary files /dev/null and b/resources/images/icons2/20x20/done@2x.png differ diff --git a/resources/images/icons2/20x20/done@3x.png b/resources/images/icons2/20x20/done@3x.png new file mode 100644 index 00000000000..059a6c2550d Binary files /dev/null and b/resources/images/icons2/20x20/done@3x.png differ diff --git a/resources/images/icons2/20x20/download@2x.png b/resources/images/icons2/20x20/download@2x.png new file mode 100644 index 00000000000..7426a9f36f0 Binary files /dev/null and b/resources/images/icons2/20x20/download@2x.png differ diff --git a/resources/images/icons2/20x20/download@3x.png b/resources/images/icons2/20x20/download@3x.png new file mode 100644 index 00000000000..f91fa4a5548 Binary files /dev/null and b/resources/images/icons2/20x20/download@3x.png differ diff --git a/resources/images/icons2/20x20/drag@2x.png b/resources/images/icons2/20x20/drag@2x.png new file mode 100644 index 00000000000..b9482fbf93f Binary files /dev/null and b/resources/images/icons2/20x20/drag@2x.png differ diff --git a/resources/images/icons2/20x20/drag@3x.png b/resources/images/icons2/20x20/drag@3x.png new file mode 100644 index 00000000000..6fbb572bcc1 Binary files /dev/null and b/resources/images/icons2/20x20/drag@3x.png differ diff --git a/resources/images/icons2/20x20/dropdown@2x.png b/resources/images/icons2/20x20/dropdown@2x.png new file mode 100644 index 00000000000..42896753d17 Binary files /dev/null and b/resources/images/icons2/20x20/dropdown@2x.png differ diff --git a/resources/images/icons2/20x20/dropdown@3x.png b/resources/images/icons2/20x20/dropdown@3x.png new file mode 100644 index 00000000000..80145aec0a4 Binary files /dev/null and b/resources/images/icons2/20x20/dropdown@3x.png differ diff --git a/resources/images/icons2/20x20/duration@2x.png b/resources/images/icons2/20x20/duration@2x.png new file mode 100644 index 00000000000..6fd0f27dcbb Binary files /dev/null and b/resources/images/icons2/20x20/duration@2x.png differ diff --git a/resources/images/icons2/20x20/duration@3x.png b/resources/images/icons2/20x20/duration@3x.png new file mode 100644 index 00000000000..c473cb1037a Binary files /dev/null and b/resources/images/icons2/20x20/duration@3x.png differ diff --git a/resources/images/icons2/20x20/edit@2x.png b/resources/images/icons2/20x20/edit@2x.png new file mode 100644 index 00000000000..681f2bd9749 Binary files /dev/null and b/resources/images/icons2/20x20/edit@2x.png differ diff --git a/resources/images/icons2/20x20/edit@3x.png b/resources/images/icons2/20x20/edit@3x.png new file mode 100644 index 00000000000..194d5158658 Binary files /dev/null and b/resources/images/icons2/20x20/edit@3x.png differ diff --git a/resources/images/icons2/20x20/email@2x.png b/resources/images/icons2/20x20/email@2x.png new file mode 100644 index 00000000000..254cf88c595 Binary files /dev/null and b/resources/images/icons2/20x20/email@2x.png differ diff --git a/resources/images/icons2/20x20/email@3x.png b/resources/images/icons2/20x20/email@3x.png new file mode 100644 index 00000000000..342ad55c3de Binary files /dev/null and b/resources/images/icons2/20x20/email@3x.png differ diff --git a/resources/images/icons2/20x20/epic@2x.png b/resources/images/icons2/20x20/epic@2x.png new file mode 100644 index 00000000000..1aa6ef101d1 Binary files /dev/null and b/resources/images/icons2/20x20/epic@2x.png differ diff --git a/resources/images/icons2/20x20/epic@3x.png b/resources/images/icons2/20x20/epic@3x.png new file mode 100644 index 00000000000..eb71ceba8c3 Binary files /dev/null and b/resources/images/icons2/20x20/epic@3x.png differ diff --git a/resources/images/icons2/20x20/expand-ls@2x.png b/resources/images/icons2/20x20/expand-ls@2x.png new file mode 100644 index 00000000000..749e65ad1f2 Binary files /dev/null and b/resources/images/icons2/20x20/expand-ls@2x.png differ diff --git a/resources/images/icons2/20x20/expand-ls@3x.png b/resources/images/icons2/20x20/expand-ls@3x.png new file mode 100644 index 00000000000..e42c91de904 Binary files /dev/null and b/resources/images/icons2/20x20/expand-ls@3x.png differ diff --git a/resources/images/icons2/20x20/expand-rs@2x.png b/resources/images/icons2/20x20/expand-rs@2x.png new file mode 100644 index 00000000000..87dd5e18d4a Binary files /dev/null and b/resources/images/icons2/20x20/expand-rs@2x.png differ diff --git a/resources/images/icons2/20x20/expand-rs@3x.png b/resources/images/icons2/20x20/expand-rs@3x.png new file mode 100644 index 00000000000..16bed5a50c3 Binary files /dev/null and b/resources/images/icons2/20x20/expand-rs@3x.png differ diff --git a/resources/images/icons2/20x20/expand-topbar@2x.png b/resources/images/icons2/20x20/expand-topbar@2x.png new file mode 100644 index 00000000000..e076169094c Binary files /dev/null and b/resources/images/icons2/20x20/expand-topbar@2x.png differ diff --git a/resources/images/icons2/20x20/expand-topbar@3x.png b/resources/images/icons2/20x20/expand-topbar@3x.png new file mode 100644 index 00000000000..ea1a0473c3a Binary files /dev/null and b/resources/images/icons2/20x20/expand-topbar@3x.png differ diff --git a/resources/images/icons2/20x20/expand@2x.png b/resources/images/icons2/20x20/expand@2x.png new file mode 100644 index 00000000000..9a23a65c9f8 Binary files /dev/null and b/resources/images/icons2/20x20/expand@2x.png differ diff --git a/resources/images/icons2/20x20/expand@3x.png b/resources/images/icons2/20x20/expand@3x.png new file mode 100644 index 00000000000..7e6a82ce92f Binary files /dev/null and b/resources/images/icons2/20x20/expand@3x.png differ diff --git a/resources/images/icons2/20x20/external@2x.png b/resources/images/icons2/20x20/external@2x.png new file mode 100644 index 00000000000..aa4742bfcb2 Binary files /dev/null and b/resources/images/icons2/20x20/external@2x.png differ diff --git a/resources/images/icons2/20x20/external@3x.png b/resources/images/icons2/20x20/external@3x.png new file mode 100644 index 00000000000..eec47f0326a Binary files /dev/null and b/resources/images/icons2/20x20/external@3x.png differ diff --git a/resources/images/icons2/20x20/face-id@2x.png b/resources/images/icons2/20x20/face-id@2x.png new file mode 100644 index 00000000000..5f7298b4947 Binary files /dev/null and b/resources/images/icons2/20x20/face-id@2x.png differ diff --git a/resources/images/icons2/20x20/face-id@3x.png b/resources/images/icons2/20x20/face-id@3x.png new file mode 100644 index 00000000000..a31c7573df8 Binary files /dev/null and b/resources/images/icons2/20x20/face-id@3x.png differ diff --git a/resources/images/icons2/20x20/faceid-key@2x.png b/resources/images/icons2/20x20/faceid-key@2x.png new file mode 100644 index 00000000000..6a21eaad3b9 Binary files /dev/null and b/resources/images/icons2/20x20/faceid-key@2x.png differ diff --git a/resources/images/icons2/20x20/faceid-key@3x.png b/resources/images/icons2/20x20/faceid-key@3x.png new file mode 100644 index 00000000000..e9e152fd358 Binary files /dev/null and b/resources/images/icons2/20x20/faceid-key@3x.png differ diff --git a/resources/images/icons2/20x20/faces@2x.png b/resources/images/icons2/20x20/faces@2x.png new file mode 100644 index 00000000000..5c7f4742514 Binary files /dev/null and b/resources/images/icons2/20x20/faces@2x.png differ diff --git a/resources/images/icons2/20x20/faces@3x.png b/resources/images/icons2/20x20/faces@3x.png new file mode 100644 index 00000000000..94f4584f15e Binary files /dev/null and b/resources/images/icons2/20x20/faces@3x.png differ diff --git a/resources/images/icons2/20x20/favourite@2x.png b/resources/images/icons2/20x20/favourite@2x.png new file mode 100644 index 00000000000..79b50377e60 Binary files /dev/null and b/resources/images/icons2/20x20/favourite@2x.png differ diff --git a/resources/images/icons2/20x20/favourite@3x.png b/resources/images/icons2/20x20/favourite@3x.png new file mode 100644 index 00000000000..6f94c7713f9 Binary files /dev/null and b/resources/images/icons2/20x20/favourite@3x.png differ diff --git a/resources/images/icons2/20x20/feed@2x.png b/resources/images/icons2/20x20/feed@2x.png new file mode 100644 index 00000000000..8488f459986 Binary files /dev/null and b/resources/images/icons2/20x20/feed@2x.png differ diff --git a/resources/images/icons2/20x20/feed@3x.png b/resources/images/icons2/20x20/feed@3x.png new file mode 100644 index 00000000000..484fcbf950f Binary files /dev/null and b/resources/images/icons2/20x20/feed@3x.png differ diff --git a/resources/images/icons2/20x20/file@2x.png b/resources/images/icons2/20x20/file@2x.png new file mode 100644 index 00000000000..858df7ec7c1 Binary files /dev/null and b/resources/images/icons2/20x20/file@2x.png differ diff --git a/resources/images/icons2/20x20/file@3x.png b/resources/images/icons2/20x20/file@3x.png new file mode 100644 index 00000000000..240e1d40d38 Binary files /dev/null and b/resources/images/icons2/20x20/file@3x.png differ diff --git a/resources/images/icons2/20x20/flag@2x.png b/resources/images/icons2/20x20/flag@2x.png new file mode 100644 index 00000000000..d10f2fd1a07 Binary files /dev/null and b/resources/images/icons2/20x20/flag@2x.png differ diff --git a/resources/images/icons2/20x20/flag@3x.png b/resources/images/icons2/20x20/flag@3x.png new file mode 100644 index 00000000000..2e55606e008 Binary files /dev/null and b/resources/images/icons2/20x20/flag@3x.png differ diff --git a/resources/images/icons2/20x20/flags@2x.png b/resources/images/icons2/20x20/flags@2x.png new file mode 100644 index 00000000000..d10f2fd1a07 Binary files /dev/null and b/resources/images/icons2/20x20/flags@2x.png differ diff --git a/resources/images/icons2/20x20/flags@3x.png b/resources/images/icons2/20x20/flags@3x.png new file mode 100644 index 00000000000..2e55606e008 Binary files /dev/null and b/resources/images/icons2/20x20/flags@3x.png differ diff --git a/resources/images/icons2/20x20/flash-off@2x.png b/resources/images/icons2/20x20/flash-off@2x.png new file mode 100644 index 00000000000..e797c0477cf Binary files /dev/null and b/resources/images/icons2/20x20/flash-off@2x.png differ diff --git a/resources/images/icons2/20x20/flash-off@3x.png b/resources/images/icons2/20x20/flash-off@3x.png new file mode 100644 index 00000000000..b1cfef1f740 Binary files /dev/null and b/resources/images/icons2/20x20/flash-off@3x.png differ diff --git a/resources/images/icons2/20x20/flash@2x.png b/resources/images/icons2/20x20/flash@2x.png new file mode 100644 index 00000000000..531b456fb05 Binary files /dev/null and b/resources/images/icons2/20x20/flash@2x.png differ diff --git a/resources/images/icons2/20x20/flash@3x.png b/resources/images/icons2/20x20/flash@3x.png new file mode 100644 index 00000000000..69c4342668f Binary files /dev/null and b/resources/images/icons2/20x20/flash@3x.png differ diff --git a/resources/images/icons2/20x20/flashlight-off@2x.png b/resources/images/icons2/20x20/flashlight-off@2x.png new file mode 100644 index 00000000000..531b456fb05 Binary files /dev/null and b/resources/images/icons2/20x20/flashlight-off@2x.png differ diff --git a/resources/images/icons2/20x20/flashlight-off@3x.png b/resources/images/icons2/20x20/flashlight-off@3x.png new file mode 100644 index 00000000000..69c4342668f Binary files /dev/null and b/resources/images/icons2/20x20/flashlight-off@3x.png differ diff --git a/resources/images/icons2/20x20/flashlight-on@2x.png b/resources/images/icons2/20x20/flashlight-on@2x.png new file mode 100644 index 00000000000..e797c0477cf Binary files /dev/null and b/resources/images/icons2/20x20/flashlight-on@2x.png differ diff --git a/resources/images/icons2/20x20/flashlight-on@3x.png b/resources/images/icons2/20x20/flashlight-on@3x.png new file mode 100644 index 00000000000..b1cfef1f740 Binary files /dev/null and b/resources/images/icons2/20x20/flashlight-on@3x.png differ diff --git a/resources/images/icons2/20x20/flip@2x.png b/resources/images/icons2/20x20/flip@2x.png new file mode 100644 index 00000000000..01a1448f08f Binary files /dev/null and b/resources/images/icons2/20x20/flip@2x.png differ diff --git a/resources/images/icons2/20x20/flip@3x.png b/resources/images/icons2/20x20/flip@3x.png new file mode 100644 index 00000000000..4432bcb73e2 Binary files /dev/null and b/resources/images/icons2/20x20/flip@3x.png differ diff --git a/resources/images/icons2/20x20/focus@2x.png b/resources/images/icons2/20x20/focus@2x.png new file mode 100644 index 00000000000..c0c8f9d1120 Binary files /dev/null and b/resources/images/icons2/20x20/focus@2x.png differ diff --git a/resources/images/icons2/20x20/focus@3x.png b/resources/images/icons2/20x20/focus@3x.png new file mode 100644 index 00000000000..72e6255b744 Binary files /dev/null and b/resources/images/icons2/20x20/focus@3x.png differ diff --git a/resources/images/icons2/20x20/folder@2x.png b/resources/images/icons2/20x20/folder@2x.png new file mode 100644 index 00000000000..6a8c1c3044f Binary files /dev/null and b/resources/images/icons2/20x20/folder@2x.png differ diff --git a/resources/images/icons2/20x20/folder@3x.png b/resources/images/icons2/20x20/folder@3x.png new file mode 100644 index 00000000000..5e5f72c83db Binary files /dev/null and b/resources/images/icons2/20x20/folder@3x.png differ diff --git a/resources/images/icons2/20x20/food@2x.png b/resources/images/icons2/20x20/food@2x.png new file mode 100644 index 00000000000..968585cdbb3 Binary files /dev/null and b/resources/images/icons2/20x20/food@2x.png differ diff --git a/resources/images/icons2/20x20/food@3x.png b/resources/images/icons2/20x20/food@3x.png new file mode 100644 index 00000000000..c47578120ef Binary files /dev/null and b/resources/images/icons2/20x20/food@3x.png differ diff --git a/resources/images/icons2/20x20/format@2x.png b/resources/images/icons2/20x20/format@2x.png new file mode 100644 index 00000000000..c4770ac6753 Binary files /dev/null and b/resources/images/icons2/20x20/format@2x.png differ diff --git a/resources/images/icons2/20x20/format@3x.png b/resources/images/icons2/20x20/format@3x.png new file mode 100644 index 00000000000..7d24482fbef Binary files /dev/null and b/resources/images/icons2/20x20/format@3x.png differ diff --git a/resources/images/icons2/20x20/forward@2x.png b/resources/images/icons2/20x20/forward@2x.png new file mode 100644 index 00000000000..a7a5d90480c Binary files /dev/null and b/resources/images/icons2/20x20/forward@2x.png differ diff --git a/resources/images/icons2/20x20/forward@3x.png b/resources/images/icons2/20x20/forward@3x.png new file mode 100644 index 00000000000..e88abb0ae57 Binary files /dev/null and b/resources/images/icons2/20x20/forward@3x.png differ diff --git a/resources/images/icons2/20x20/friend@2x.png b/resources/images/icons2/20x20/friend@2x.png new file mode 100644 index 00000000000..7bad7ae97c9 Binary files /dev/null and b/resources/images/icons2/20x20/friend@2x.png differ diff --git a/resources/images/icons2/20x20/friend@3x.png b/resources/images/icons2/20x20/friend@3x.png new file mode 100644 index 00000000000..aa6a2cd61b2 Binary files /dev/null and b/resources/images/icons2/20x20/friend@3x.png differ diff --git a/resources/images/icons2/20x20/gas@2x.png b/resources/images/icons2/20x20/gas@2x.png new file mode 100644 index 00000000000..3f7ae336d71 Binary files /dev/null and b/resources/images/icons2/20x20/gas@2x.png differ diff --git a/resources/images/icons2/20x20/gas@3x.png b/resources/images/icons2/20x20/gas@3x.png new file mode 100644 index 00000000000..e69614cd95a Binary files /dev/null and b/resources/images/icons2/20x20/gas@3x.png differ diff --git a/resources/images/icons2/20x20/gavel@2x.png b/resources/images/icons2/20x20/gavel@2x.png new file mode 100644 index 00000000000..04a2580ce3c Binary files /dev/null and b/resources/images/icons2/20x20/gavel@2x.png differ diff --git a/resources/images/icons2/20x20/gavel@3x.png b/resources/images/icons2/20x20/gavel@3x.png new file mode 100644 index 00000000000..839e3a4f61c Binary files /dev/null and b/resources/images/icons2/20x20/gavel@3x.png differ diff --git a/resources/images/icons2/20x20/gif@2x.png b/resources/images/icons2/20x20/gif@2x.png new file mode 100644 index 00000000000..322cdd68e57 Binary files /dev/null and b/resources/images/icons2/20x20/gif@2x.png differ diff --git a/resources/images/icons2/20x20/gif@3x.png b/resources/images/icons2/20x20/gif@3x.png new file mode 100644 index 00000000000..14a7e77c41d Binary files /dev/null and b/resources/images/icons2/20x20/gif@3x.png differ diff --git a/resources/images/icons2/20x20/gift@2x.png b/resources/images/icons2/20x20/gift@2x.png new file mode 100644 index 00000000000..ee53c2da142 Binary files /dev/null and b/resources/images/icons2/20x20/gift@2x.png differ diff --git a/resources/images/icons2/20x20/gift@3x.png b/resources/images/icons2/20x20/gift@3x.png new file mode 100644 index 00000000000..81fb31b5b02 Binary files /dev/null and b/resources/images/icons2/20x20/gift@3x.png differ diff --git a/resources/images/icons2/20x20/globe@2x.png b/resources/images/icons2/20x20/globe@2x.png new file mode 100644 index 00000000000..a6946c697d7 Binary files /dev/null and b/resources/images/icons2/20x20/globe@2x.png differ diff --git a/resources/images/icons2/20x20/globe@3x.png b/resources/images/icons2/20x20/globe@3x.png new file mode 100644 index 00000000000..4a76bdd0247 Binary files /dev/null and b/resources/images/icons2/20x20/globe@3x.png differ diff --git a/resources/images/icons2/20x20/group@2x.png b/resources/images/icons2/20x20/group@2x.png new file mode 100644 index 00000000000..a74433a6405 Binary files /dev/null and b/resources/images/icons2/20x20/group@2x.png differ diff --git a/resources/images/icons2/20x20/group@3x.png b/resources/images/icons2/20x20/group@3x.png new file mode 100644 index 00000000000..f798aaf6089 Binary files /dev/null and b/resources/images/icons2/20x20/group@3x.png differ diff --git a/resources/images/icons2/20x20/hashtag@2x-1.png b/resources/images/icons2/20x20/hashtag@2x-1.png new file mode 100644 index 00000000000..2ccd034114b Binary files /dev/null and b/resources/images/icons2/20x20/hashtag@2x-1.png differ diff --git a/resources/images/icons2/20x20/hashtag@2x.png b/resources/images/icons2/20x20/hashtag@2x.png new file mode 100644 index 00000000000..7b2ae3fa3cc Binary files /dev/null and b/resources/images/icons2/20x20/hashtag@2x.png differ diff --git a/resources/images/icons2/20x20/hashtag@3x-1.png b/resources/images/icons2/20x20/hashtag@3x-1.png new file mode 100644 index 00000000000..d5c1004ad92 Binary files /dev/null and b/resources/images/icons2/20x20/hashtag@3x-1.png differ diff --git a/resources/images/icons2/20x20/hashtag@3x.png b/resources/images/icons2/20x20/hashtag@3x.png new file mode 100644 index 00000000000..5ead3250cbb Binary files /dev/null and b/resources/images/icons2/20x20/hashtag@3x.png differ diff --git a/resources/images/icons2/20x20/heart-break@2x.png b/resources/images/icons2/20x20/heart-break@2x.png new file mode 100644 index 00000000000..9211bb77acf Binary files /dev/null and b/resources/images/icons2/20x20/heart-break@2x.png differ diff --git a/resources/images/icons2/20x20/heart-break@3x.png b/resources/images/icons2/20x20/heart-break@3x.png new file mode 100644 index 00000000000..377fd3c2229 Binary files /dev/null and b/resources/images/icons2/20x20/heart-break@3x.png differ diff --git a/resources/images/icons2/20x20/heart@2x.png b/resources/images/icons2/20x20/heart@2x.png new file mode 100644 index 00000000000..82a8ca94b3e Binary files /dev/null and b/resources/images/icons2/20x20/heart@2x.png differ diff --git a/resources/images/icons2/20x20/heart@3x.png b/resources/images/icons2/20x20/heart@3x.png new file mode 100644 index 00000000000..84b8c9ff4fb Binary files /dev/null and b/resources/images/icons2/20x20/heart@3x.png differ diff --git a/resources/images/icons2/20x20/help@2x.png b/resources/images/icons2/20x20/help@2x.png new file mode 100644 index 00000000000..fc2d0cb0ab3 Binary files /dev/null and b/resources/images/icons2/20x20/help@2x.png differ diff --git a/resources/images/icons2/20x20/help@3x.png b/resources/images/icons2/20x20/help@3x.png new file mode 100644 index 00000000000..53fae388317 Binary files /dev/null and b/resources/images/icons2/20x20/help@3x.png differ diff --git a/resources/images/icons2/20x20/hide-password@2x.png b/resources/images/icons2/20x20/hide-password@2x.png new file mode 100644 index 00000000000..7994cdfce40 Binary files /dev/null and b/resources/images/icons2/20x20/hide-password@2x.png differ diff --git a/resources/images/icons2/20x20/hide-password@3x.png b/resources/images/icons2/20x20/hide-password@3x.png new file mode 100644 index 00000000000..f6c8e097833 Binary files /dev/null and b/resources/images/icons2/20x20/hide-password@3x.png differ diff --git a/resources/images/icons2/20x20/hide@2x.png b/resources/images/icons2/20x20/hide@2x.png new file mode 100644 index 00000000000..6836490c95a Binary files /dev/null and b/resources/images/icons2/20x20/hide@2x.png differ diff --git a/resources/images/icons2/20x20/hide@3x.png b/resources/images/icons2/20x20/hide@3x.png new file mode 100644 index 00000000000..bf395a82103 Binary files /dev/null and b/resources/images/icons2/20x20/hide@3x.png differ diff --git a/resources/images/icons2/20x20/history@2x.png b/resources/images/icons2/20x20/history@2x.png new file mode 100644 index 00000000000..f3d0867cac3 Binary files /dev/null and b/resources/images/icons2/20x20/history@2x.png differ diff --git a/resources/images/icons2/20x20/history@3x.png b/resources/images/icons2/20x20/history@3x.png new file mode 100644 index 00000000000..2f35b0029f8 Binary files /dev/null and b/resources/images/icons2/20x20/history@3x.png differ diff --git a/resources/images/icons2/20x20/hold@2x.png b/resources/images/icons2/20x20/hold@2x.png new file mode 100644 index 00000000000..8fcc9036615 Binary files /dev/null and b/resources/images/icons2/20x20/hold@2x.png differ diff --git a/resources/images/icons2/20x20/hold@3x.png b/resources/images/icons2/20x20/hold@3x.png new file mode 100644 index 00000000000..42beec7d755 Binary files /dev/null and b/resources/images/icons2/20x20/hold@3x.png differ diff --git a/resources/images/icons2/20x20/hourglass@2x.png b/resources/images/icons2/20x20/hourglass@2x.png new file mode 100644 index 00000000000..2bbfa43df35 Binary files /dev/null and b/resources/images/icons2/20x20/hourglass@2x.png differ diff --git a/resources/images/icons2/20x20/hourglass@3x.png b/resources/images/icons2/20x20/hourglass@3x.png new file mode 100644 index 00000000000..d3f95020833 Binary files /dev/null and b/resources/images/icons2/20x20/hourglass@3x.png differ diff --git a/resources/images/icons2/20x20/image@2x.png b/resources/images/icons2/20x20/image@2x.png new file mode 100644 index 00000000000..1b37669fcf6 Binary files /dev/null and b/resources/images/icons2/20x20/image@2x.png differ diff --git a/resources/images/icons2/20x20/image@3x.png b/resources/images/icons2/20x20/image@3x.png new file mode 100644 index 00000000000..fa6b3e2f3a1 Binary files /dev/null and b/resources/images/icons2/20x20/image@3x.png differ diff --git a/resources/images/icons2/20x20/import@2x.png b/resources/images/icons2/20x20/import@2x.png new file mode 100644 index 00000000000..6c52768eee9 Binary files /dev/null and b/resources/images/icons2/20x20/import@2x.png differ diff --git a/resources/images/icons2/20x20/import@3x.png b/resources/images/icons2/20x20/import@3x.png new file mode 100644 index 00000000000..965d9267efd Binary files /dev/null and b/resources/images/icons2/20x20/import@3x.png differ diff --git a/resources/images/icons2/20x20/inactive-left@2x.png b/resources/images/icons2/20x20/inactive-left@2x.png new file mode 100644 index 00000000000..124572f5680 Binary files /dev/null and b/resources/images/icons2/20x20/inactive-left@2x.png differ diff --git a/resources/images/icons2/20x20/inactive-left@3x.png b/resources/images/icons2/20x20/inactive-left@3x.png new file mode 100644 index 00000000000..13c01475f27 Binary files /dev/null and b/resources/images/icons2/20x20/inactive-left@3x.png differ diff --git a/resources/images/icons2/20x20/inactive@2x-1.png b/resources/images/icons2/20x20/inactive@2x-1.png new file mode 100644 index 00000000000..31b454844da Binary files /dev/null and b/resources/images/icons2/20x20/inactive@2x-1.png differ diff --git a/resources/images/icons2/20x20/inactive@2x.png b/resources/images/icons2/20x20/inactive@2x.png new file mode 100644 index 00000000000..0f944923008 Binary files /dev/null and b/resources/images/icons2/20x20/inactive@2x.png differ diff --git a/resources/images/icons2/20x20/inactive@3x-1.png b/resources/images/icons2/20x20/inactive@3x-1.png new file mode 100644 index 00000000000..37dca045334 Binary files /dev/null and b/resources/images/icons2/20x20/inactive@3x-1.png differ diff --git a/resources/images/icons2/20x20/inactive@3x.png b/resources/images/icons2/20x20/inactive@3x.png new file mode 100644 index 00000000000..d8cb7791c56 Binary files /dev/null and b/resources/images/icons2/20x20/inactive@3x.png differ diff --git a/resources/images/icons2/20x20/incorrect-dark@2x.png b/resources/images/icons2/20x20/incorrect-dark@2x.png new file mode 100644 index 00000000000..83b3e60d08d Binary files /dev/null and b/resources/images/icons2/20x20/incorrect-dark@2x.png differ diff --git a/resources/images/icons2/20x20/incorrect-dark@3x.png b/resources/images/icons2/20x20/incorrect-dark@3x.png new file mode 100644 index 00000000000..5807b5e66b7 Binary files /dev/null and b/resources/images/icons2/20x20/incorrect-dark@3x.png differ diff --git a/resources/images/icons2/20x20/incorrect@2x.png b/resources/images/icons2/20x20/incorrect@2x.png new file mode 100644 index 00000000000..22e9ebd018e Binary files /dev/null and b/resources/images/icons2/20x20/incorrect@2x.png differ diff --git a/resources/images/icons2/20x20/incorrect@3x.png b/resources/images/icons2/20x20/incorrect@3x.png new file mode 100644 index 00000000000..712c1ed7847 Binary files /dev/null and b/resources/images/icons2/20x20/incorrect@3x.png differ diff --git a/resources/images/icons2/20x20/info-badge@2x.png b/resources/images/icons2/20x20/info-badge@2x.png new file mode 100644 index 00000000000..5f32d5505ad Binary files /dev/null and b/resources/images/icons2/20x20/info-badge@2x.png differ diff --git a/resources/images/icons2/20x20/info-badge@3x.png b/resources/images/icons2/20x20/info-badge@3x.png new file mode 100644 index 00000000000..5f89bb0041e Binary files /dev/null and b/resources/images/icons2/20x20/info-badge@3x.png differ diff --git a/resources/images/icons2/20x20/info@2x.png b/resources/images/icons2/20x20/info@2x.png new file mode 100644 index 00000000000..f3dec358d04 Binary files /dev/null and b/resources/images/icons2/20x20/info@2x.png differ diff --git a/resources/images/icons2/20x20/info@3x.png b/resources/images/icons2/20x20/info@3x.png new file mode 100644 index 00000000000..0de4970059d Binary files /dev/null and b/resources/images/icons2/20x20/info@3x.png differ diff --git a/resources/images/icons2/20x20/italic@2x.png b/resources/images/icons2/20x20/italic@2x.png new file mode 100644 index 00000000000..e6760ed443a Binary files /dev/null and b/resources/images/icons2/20x20/italic@2x.png differ diff --git a/resources/images/icons2/20x20/italic@3x.png b/resources/images/icons2/20x20/italic@3x.png new file mode 100644 index 00000000000..23c50d4e753 Binary files /dev/null and b/resources/images/icons2/20x20/italic@3x.png differ diff --git a/resources/images/icons2/20x20/justify@2x.png b/resources/images/icons2/20x20/justify@2x.png new file mode 100644 index 00000000000..dce224ddcb3 Binary files /dev/null and b/resources/images/icons2/20x20/justify@2x.png differ diff --git a/resources/images/icons2/20x20/justify@3x.png b/resources/images/icons2/20x20/justify@3x.png new file mode 100644 index 00000000000..d697591740f Binary files /dev/null and b/resources/images/icons2/20x20/justify@3x.png differ diff --git a/resources/images/icons2/20x20/key@2x.png b/resources/images/icons2/20x20/key@2x.png new file mode 100644 index 00000000000..f2f9fb9e228 Binary files /dev/null and b/resources/images/icons2/20x20/key@2x.png differ diff --git a/resources/images/icons2/20x20/key@3x.png b/resources/images/icons2/20x20/key@3x.png new file mode 100644 index 00000000000..459392fe63b Binary files /dev/null and b/resources/images/icons2/20x20/key@3x.png differ diff --git a/resources/images/icons2/20x20/keyboard@2x.png b/resources/images/icons2/20x20/keyboard@2x.png new file mode 100644 index 00000000000..a6d9810514c Binary files /dev/null and b/resources/images/icons2/20x20/keyboard@2x.png differ diff --git a/resources/images/icons2/20x20/keyboard@3x.png b/resources/images/icons2/20x20/keyboard@3x.png new file mode 100644 index 00000000000..ca8212ffbbd Binary files /dev/null and b/resources/images/icons2/20x20/keyboard@3x.png differ diff --git a/resources/images/icons2/20x20/keycard-card@2x.png b/resources/images/icons2/20x20/keycard-card@2x.png new file mode 100644 index 00000000000..c6cf24cad62 Binary files /dev/null and b/resources/images/icons2/20x20/keycard-card@2x.png differ diff --git a/resources/images/icons2/20x20/keycard-card@3x.png b/resources/images/icons2/20x20/keycard-card@3x.png new file mode 100644 index 00000000000..7ee696bf8e3 Binary files /dev/null and b/resources/images/icons2/20x20/keycard-card@3x.png differ diff --git a/resources/images/icons2/20x20/keycard-logo@2x.png b/resources/images/icons2/20x20/keycard-logo@2x.png new file mode 100644 index 00000000000..29638fb929f Binary files /dev/null and b/resources/images/icons2/20x20/keycard-logo@2x.png differ diff --git a/resources/images/icons2/20x20/keycard-logo@3x.png b/resources/images/icons2/20x20/keycard-logo@3x.png new file mode 100644 index 00000000000..063cf4ee2b5 Binary files /dev/null and b/resources/images/icons2/20x20/keycard-logo@3x.png differ diff --git a/resources/images/icons2/20x20/keycard@2x.png b/resources/images/icons2/20x20/keycard@2x.png new file mode 100644 index 00000000000..ea300653632 Binary files /dev/null and b/resources/images/icons2/20x20/keycard@2x.png differ diff --git a/resources/images/icons2/20x20/keycard@3x.png b/resources/images/icons2/20x20/keycard@3x.png new file mode 100644 index 00000000000..28b33a993e5 Binary files /dev/null and b/resources/images/icons2/20x20/keycard@3x.png differ diff --git a/resources/images/icons2/20x20/kick-user@2x.png b/resources/images/icons2/20x20/kick-user@2x.png new file mode 100644 index 00000000000..5f197e54a70 Binary files /dev/null and b/resources/images/icons2/20x20/kick-user@2x.png differ diff --git a/resources/images/icons2/20x20/kick-user@3x.png b/resources/images/icons2/20x20/kick-user@3x.png new file mode 100644 index 00000000000..c226458c891 Binary files /dev/null and b/resources/images/icons2/20x20/kick-user@3x.png differ diff --git a/resources/images/icons2/20x20/language@2x.png b/resources/images/icons2/20x20/language@2x.png new file mode 100644 index 00000000000..0bd27fef965 Binary files /dev/null and b/resources/images/icons2/20x20/language@2x.png differ diff --git a/resources/images/icons2/20x20/language@3x.png b/resources/images/icons2/20x20/language@3x.png new file mode 100644 index 00000000000..eb26fd45b77 Binary files /dev/null and b/resources/images/icons2/20x20/language@3x.png differ diff --git a/resources/images/icons2/20x20/laptop@2x.png b/resources/images/icons2/20x20/laptop@2x.png new file mode 100644 index 00000000000..98ce77bd24d Binary files /dev/null and b/resources/images/icons2/20x20/laptop@2x.png differ diff --git a/resources/images/icons2/20x20/laptop@3x.png b/resources/images/icons2/20x20/laptop@3x.png new file mode 100644 index 00000000000..3738f4f4075 Binary files /dev/null and b/resources/images/icons2/20x20/laptop@3x.png differ diff --git a/resources/images/icons2/20x20/left-align@2x.png b/resources/images/icons2/20x20/left-align@2x.png new file mode 100644 index 00000000000..38ed106bd1d Binary files /dev/null and b/resources/images/icons2/20x20/left-align@2x.png differ diff --git a/resources/images/icons2/20x20/left-align@3x.png b/resources/images/icons2/20x20/left-align@3x.png new file mode 100644 index 00000000000..338960a5415 Binary files /dev/null and b/resources/images/icons2/20x20/left-align@3x.png differ diff --git a/resources/images/icons2/20x20/light@2x.png b/resources/images/icons2/20x20/light@2x.png new file mode 100644 index 00000000000..c078f725350 Binary files /dev/null and b/resources/images/icons2/20x20/light@2x.png differ diff --git a/resources/images/icons2/20x20/light@3x.png b/resources/images/icons2/20x20/light@3x.png new file mode 100644 index 00000000000..500483b3e0a Binary files /dev/null and b/resources/images/icons2/20x20/light@3x.png differ diff --git a/resources/images/icons2/20x20/link@2x.png b/resources/images/icons2/20x20/link@2x.png new file mode 100644 index 00000000000..a6bb0e6d04c Binary files /dev/null and b/resources/images/icons2/20x20/link@2x.png differ diff --git a/resources/images/icons2/20x20/link@3x.png b/resources/images/icons2/20x20/link@3x.png new file mode 100644 index 00000000000..2c174816a10 Binary files /dev/null and b/resources/images/icons2/20x20/link@3x.png differ diff --git a/resources/images/icons2/20x20/list-view-collapsed@2x.png b/resources/images/icons2/20x20/list-view-collapsed@2x.png new file mode 100644 index 00000000000..975f07075dc Binary files /dev/null and b/resources/images/icons2/20x20/list-view-collapsed@2x.png differ diff --git a/resources/images/icons2/20x20/list-view-collapsed@3x.png b/resources/images/icons2/20x20/list-view-collapsed@3x.png new file mode 100644 index 00000000000..40fbde75b22 Binary files /dev/null and b/resources/images/icons2/20x20/list-view-collapsed@3x.png differ diff --git a/resources/images/icons2/20x20/list-view@2x.png b/resources/images/icons2/20x20/list-view@2x.png new file mode 100644 index 00000000000..94b651a6bda Binary files /dev/null and b/resources/images/icons2/20x20/list-view@2x.png differ diff --git a/resources/images/icons2/20x20/list-view@3x.png b/resources/images/icons2/20x20/list-view@3x.png new file mode 100644 index 00000000000..9edd8349ca5 Binary files /dev/null and b/resources/images/icons2/20x20/list-view@3x.png differ diff --git a/resources/images/icons2/20x20/loading@2x.png b/resources/images/icons2/20x20/loading@2x.png new file mode 100644 index 00000000000..7b20ce03279 Binary files /dev/null and b/resources/images/icons2/20x20/loading@2x.png differ diff --git a/resources/images/icons2/20x20/loading@3x.png b/resources/images/icons2/20x20/loading@3x.png new file mode 100644 index 00000000000..75ac2b71cd2 Binary files /dev/null and b/resources/images/icons2/20x20/loading@3x.png differ diff --git a/resources/images/icons2/20x20/locked@2x.png b/resources/images/icons2/20x20/locked@2x.png new file mode 100644 index 00000000000..91f64b82823 Binary files /dev/null and b/resources/images/icons2/20x20/locked@2x.png differ diff --git a/resources/images/icons2/20x20/locked@3x.png b/resources/images/icons2/20x20/locked@3x.png new file mode 100644 index 00000000000..189c97d5e88 Binary files /dev/null and b/resources/images/icons2/20x20/locked@3x.png differ diff --git a/resources/images/icons2/20x20/log-out@2x.png b/resources/images/icons2/20x20/log-out@2x.png new file mode 100644 index 00000000000..b1fe189bcf2 Binary files /dev/null and b/resources/images/icons2/20x20/log-out@2x.png differ diff --git a/resources/images/icons2/20x20/log-out@3x.png b/resources/images/icons2/20x20/log-out@3x.png new file mode 100644 index 00000000000..48b50f26be4 Binary files /dev/null and b/resources/images/icons2/20x20/log-out@3x.png differ diff --git a/resources/images/icons2/20x20/mark-as-read@2x.png b/resources/images/icons2/20x20/mark-as-read@2x.png new file mode 100644 index 00000000000..e1a9a929163 Binary files /dev/null and b/resources/images/icons2/20x20/mark-as-read@2x.png differ diff --git a/resources/images/icons2/20x20/mark-as-read@3x.png b/resources/images/icons2/20x20/mark-as-read@3x.png new file mode 100644 index 00000000000..b35b954655f Binary files /dev/null and b/resources/images/icons2/20x20/mark-as-read@3x.png differ diff --git a/resources/images/icons2/20x20/media@2x.png b/resources/images/icons2/20x20/media@2x.png new file mode 100644 index 00000000000..2e516e4467b Binary files /dev/null and b/resources/images/icons2/20x20/media@2x.png differ diff --git a/resources/images/icons2/20x20/media@3x.png b/resources/images/icons2/20x20/media@3x.png new file mode 100644 index 00000000000..ff2f5315e2a Binary files /dev/null and b/resources/images/icons2/20x20/media@3x.png differ diff --git a/resources/images/icons2/20x20/members@2x.png b/resources/images/icons2/20x20/members@2x.png new file mode 100644 index 00000000000..30e8a4c6b83 Binary files /dev/null and b/resources/images/icons2/20x20/members@2x.png differ diff --git a/resources/images/icons2/20x20/members@3x.png b/resources/images/icons2/20x20/members@3x.png new file mode 100644 index 00000000000..8f3a86d1fbe Binary files /dev/null and b/resources/images/icons2/20x20/members@3x.png differ diff --git a/resources/images/icons2/20x20/mention@2x.png b/resources/images/icons2/20x20/mention@2x.png new file mode 100644 index 00000000000..9b572530505 Binary files /dev/null and b/resources/images/icons2/20x20/mention@2x.png differ diff --git a/resources/images/icons2/20x20/mention@3x.png b/resources/images/icons2/20x20/mention@3x.png new file mode 100644 index 00000000000..4e94a263f56 Binary files /dev/null and b/resources/images/icons2/20x20/mention@3x.png differ diff --git a/resources/images/icons2/20x20/menu@2x.png b/resources/images/icons2/20x20/menu@2x.png new file mode 100644 index 00000000000..2964c36ac45 Binary files /dev/null and b/resources/images/icons2/20x20/menu@2x.png differ diff --git a/resources/images/icons2/20x20/menu@3x.png b/resources/images/icons2/20x20/menu@3x.png new file mode 100644 index 00000000000..95e01bd76c0 Binary files /dev/null and b/resources/images/icons2/20x20/menu@3x.png differ diff --git a/resources/images/icons2/20x20/messages@2x-1.png b/resources/images/icons2/20x20/messages@2x-1.png new file mode 100644 index 00000000000..0c711b2e6c0 Binary files /dev/null and b/resources/images/icons2/20x20/messages@2x-1.png differ diff --git a/resources/images/icons2/20x20/messages@2x.png b/resources/images/icons2/20x20/messages@2x.png new file mode 100644 index 00000000000..8131dfc0d83 Binary files /dev/null and b/resources/images/icons2/20x20/messages@2x.png differ diff --git a/resources/images/icons2/20x20/messages@3x-1.png b/resources/images/icons2/20x20/messages@3x-1.png new file mode 100644 index 00000000000..089e60cbbfb Binary files /dev/null and b/resources/images/icons2/20x20/messages@3x-1.png differ diff --git a/resources/images/icons2/20x20/messages@3x.png b/resources/images/icons2/20x20/messages@3x.png new file mode 100644 index 00000000000..27f3fb40540 Binary files /dev/null and b/resources/images/icons2/20x20/messages@3x.png differ diff --git a/resources/images/icons2/20x20/mint@2x.png b/resources/images/icons2/20x20/mint@2x.png new file mode 100644 index 00000000000..829ecb3ca6f Binary files /dev/null and b/resources/images/icons2/20x20/mint@2x.png differ diff --git a/resources/images/icons2/20x20/mint@3x.png b/resources/images/icons2/20x20/mint@3x.png new file mode 100644 index 00000000000..879f6eceb18 Binary files /dev/null and b/resources/images/icons2/20x20/mint@3x.png differ diff --git a/resources/images/icons2/20x20/mobile@2x.png b/resources/images/icons2/20x20/mobile@2x.png new file mode 100644 index 00000000000..cef188ec8d3 Binary files /dev/null and b/resources/images/icons2/20x20/mobile@2x.png differ diff --git a/resources/images/icons2/20x20/mobile@3x.png b/resources/images/icons2/20x20/mobile@3x.png new file mode 100644 index 00000000000..7feacf869b8 Binary files /dev/null and b/resources/images/icons2/20x20/mobile@3x.png differ diff --git a/resources/images/icons2/20x20/move-up@2x.png b/resources/images/icons2/20x20/move-up@2x.png new file mode 100644 index 00000000000..5e97fe46184 Binary files /dev/null and b/resources/images/icons2/20x20/move-up@2x.png differ diff --git a/resources/images/icons2/20x20/move-up@3x.png b/resources/images/icons2/20x20/move-up@3x.png new file mode 100644 index 00000000000..5337a889874 Binary files /dev/null and b/resources/images/icons2/20x20/move-up@3x.png differ diff --git a/resources/images/icons2/20x20/multi-profile@2x.png b/resources/images/icons2/20x20/multi-profile@2x.png new file mode 100644 index 00000000000..6a40f6ba3cd Binary files /dev/null and b/resources/images/icons2/20x20/multi-profile@2x.png differ diff --git a/resources/images/icons2/20x20/multi-profile@3x.png b/resources/images/icons2/20x20/multi-profile@3x.png new file mode 100644 index 00000000000..2be5c424691 Binary files /dev/null and b/resources/images/icons2/20x20/multi-profile@3x.png differ diff --git a/resources/images/icons2/20x20/muted@2x.png b/resources/images/icons2/20x20/muted@2x.png new file mode 100644 index 00000000000..4de6f9b8841 Binary files /dev/null and b/resources/images/icons2/20x20/muted@2x.png differ diff --git a/resources/images/icons2/20x20/muted@3x.png b/resources/images/icons2/20x20/muted@3x.png new file mode 100644 index 00000000000..7fcf85d6e0b Binary files /dev/null and b/resources/images/icons2/20x20/muted@3x.png differ diff --git a/resources/images/icons2/20x20/mutual-contact@2x.png b/resources/images/icons2/20x20/mutual-contact@2x.png new file mode 100644 index 00000000000..be7b892e92e Binary files /dev/null and b/resources/images/icons2/20x20/mutual-contact@2x.png differ diff --git a/resources/images/icons2/20x20/mutual-contact@3x.png b/resources/images/icons2/20x20/mutual-contact@3x.png new file mode 100644 index 00000000000..5ed7e795ed5 Binary files /dev/null and b/resources/images/icons2/20x20/mutual-contact@3x.png differ diff --git a/resources/images/icons2/20x20/nature@2x.png b/resources/images/icons2/20x20/nature@2x.png new file mode 100644 index 00000000000..829ecb3ca6f Binary files /dev/null and b/resources/images/icons2/20x20/nature@2x.png differ diff --git a/resources/images/icons2/20x20/nature@3x.png b/resources/images/icons2/20x20/nature@3x.png new file mode 100644 index 00000000000..879f6eceb18 Binary files /dev/null and b/resources/images/icons2/20x20/nature@3x.png differ diff --git a/resources/images/icons2/20x20/negative-state@2x.png b/resources/images/icons2/20x20/negative-state@2x.png new file mode 100644 index 00000000000..2f694036e17 Binary files /dev/null and b/resources/images/icons2/20x20/negative-state@2x.png differ diff --git a/resources/images/icons2/20x20/negative-state@3x.png b/resources/images/icons2/20x20/negative-state@3x.png new file mode 100644 index 00000000000..33ee14f0c32 Binary files /dev/null and b/resources/images/icons2/20x20/negative-state@3x.png differ diff --git a/resources/images/icons2/20x20/networks@2x.png b/resources/images/icons2/20x20/networks@2x.png new file mode 100644 index 00000000000..404341c746b Binary files /dev/null and b/resources/images/icons2/20x20/networks@2x.png differ diff --git a/resources/images/icons2/20x20/networks@3x.png b/resources/images/icons2/20x20/networks@3x.png new file mode 100644 index 00000000000..22eb6f8e7a7 Binary files /dev/null and b/resources/images/icons2/20x20/networks@3x.png differ diff --git a/resources/images/icons2/20x20/new-message@2x.png b/resources/images/icons2/20x20/new-message@2x.png new file mode 100644 index 00000000000..0f50678e68f Binary files /dev/null and b/resources/images/icons2/20x20/new-message@2x.png differ diff --git a/resources/images/icons2/20x20/new-message@3x.png b/resources/images/icons2/20x20/new-message@3x.png new file mode 100644 index 00000000000..d1bfdbc902e Binary files /dev/null and b/resources/images/icons2/20x20/new-message@3x.png differ diff --git a/resources/images/icons2/20x20/new-window@2x.png b/resources/images/icons2/20x20/new-window@2x.png new file mode 100644 index 00000000000..63dcbfb3093 Binary files /dev/null and b/resources/images/icons2/20x20/new-window@2x.png differ diff --git a/resources/images/icons2/20x20/new-window@3x.png b/resources/images/icons2/20x20/new-window@3x.png new file mode 100644 index 00000000000..7eeb025ce5b Binary files /dev/null and b/resources/images/icons2/20x20/new-window@3x.png differ diff --git a/resources/images/icons2/20x20/nft@2x.png b/resources/images/icons2/20x20/nft@2x.png new file mode 100644 index 00000000000..98330d7ceaa Binary files /dev/null and b/resources/images/icons2/20x20/nft@2x.png differ diff --git a/resources/images/icons2/20x20/nft@3x.png b/resources/images/icons2/20x20/nft@3x.png new file mode 100644 index 00000000000..88a935cf5d7 Binary files /dev/null and b/resources/images/icons2/20x20/nft@3x.png differ diff --git a/resources/images/icons2/20x20/node@2x.png b/resources/images/icons2/20x20/node@2x.png new file mode 100644 index 00000000000..9c15af01940 Binary files /dev/null and b/resources/images/icons2/20x20/node@2x.png differ diff --git a/resources/images/icons2/20x20/node@3x.png b/resources/images/icons2/20x20/node@3x.png new file mode 100644 index 00000000000..c2a17c4b80e Binary files /dev/null and b/resources/images/icons2/20x20/node@3x.png differ diff --git a/resources/images/icons2/20x20/not-started@2x.png b/resources/images/icons2/20x20/not-started@2x.png new file mode 100644 index 00000000000..ae6f6469d6a Binary files /dev/null and b/resources/images/icons2/20x20/not-started@2x.png differ diff --git a/resources/images/icons2/20x20/not-started@3x.png b/resources/images/icons2/20x20/not-started@3x.png new file mode 100644 index 00000000000..96815e44dfa Binary files /dev/null and b/resources/images/icons2/20x20/not-started@3x.png differ diff --git a/resources/images/icons2/20x20/notification@2x.png b/resources/images/icons2/20x20/notification@2x.png new file mode 100644 index 00000000000..0d33832ae40 Binary files /dev/null and b/resources/images/icons2/20x20/notification@2x.png differ diff --git a/resources/images/icons2/20x20/notification@3x.png b/resources/images/icons2/20x20/notification@3x.png new file mode 100644 index 00000000000..618debed1db Binary files /dev/null and b/resources/images/icons2/20x20/notification@3x.png differ diff --git a/resources/images/icons2/20x20/notifications-unread@2x.png b/resources/images/icons2/20x20/notifications-unread@2x.png new file mode 100644 index 00000000000..cb0d8fdfeca Binary files /dev/null and b/resources/images/icons2/20x20/notifications-unread@2x.png differ diff --git a/resources/images/icons2/20x20/notifications-unread@3x.png b/resources/images/icons2/20x20/notifications-unread@3x.png new file mode 100644 index 00000000000..b8ed51d8dc0 Binary files /dev/null and b/resources/images/icons2/20x20/notifications-unread@3x.png differ diff --git a/resources/images/icons2/20x20/notifications@2x.png b/resources/images/icons2/20x20/notifications@2x.png new file mode 100644 index 00000000000..5060a8ade50 Binary files /dev/null and b/resources/images/icons2/20x20/notifications@2x.png differ diff --git a/resources/images/icons2/20x20/notifications@3x.png b/resources/images/icons2/20x20/notifications@3x.png new file mode 100644 index 00000000000..1ddd699a643 Binary files /dev/null and b/resources/images/icons2/20x20/notifications@3x.png differ diff --git a/resources/images/icons2/20x20/numbered-list@2x.png b/resources/images/icons2/20x20/numbered-list@2x.png new file mode 100644 index 00000000000..7c4e21618b3 Binary files /dev/null and b/resources/images/icons2/20x20/numbered-list@2x.png differ diff --git a/resources/images/icons2/20x20/numbered-list@3x.png b/resources/images/icons2/20x20/numbered-list@3x.png new file mode 100644 index 00000000000..699b690a055 Binary files /dev/null and b/resources/images/icons2/20x20/numbered-list@3x.png differ diff --git a/resources/images/icons2/20x20/objects@2x.png b/resources/images/icons2/20x20/objects@2x.png new file mode 100644 index 00000000000..cf90eed769e Binary files /dev/null and b/resources/images/icons2/20x20/objects@2x.png differ diff --git a/resources/images/icons2/20x20/objects@3x.png b/resources/images/icons2/20x20/objects@3x.png new file mode 100644 index 00000000000..78e33286979 Binary files /dev/null and b/resources/images/icons2/20x20/objects@3x.png differ diff --git a/resources/images/icons2/20x20/offline@2x.png b/resources/images/icons2/20x20/offline@2x.png new file mode 100644 index 00000000000..0f944923008 Binary files /dev/null and b/resources/images/icons2/20x20/offline@2x.png differ diff --git a/resources/images/icons2/20x20/offline@3x.png b/resources/images/icons2/20x20/offline@3x.png new file mode 100644 index 00000000000..d8cb7791c56 Binary files /dev/null and b/resources/images/icons2/20x20/offline@3x.png differ diff --git a/resources/images/icons2/20x20/online-left@2x.png b/resources/images/icons2/20x20/online-left@2x.png new file mode 100644 index 00000000000..ec1dfa0ad41 Binary files /dev/null and b/resources/images/icons2/20x20/online-left@2x.png differ diff --git a/resources/images/icons2/20x20/online-left@3x.png b/resources/images/icons2/20x20/online-left@3x.png new file mode 100644 index 00000000000..2bec28b1fb5 Binary files /dev/null and b/resources/images/icons2/20x20/online-left@3x.png differ diff --git a/resources/images/icons2/20x20/online@2x.png b/resources/images/icons2/20x20/online@2x.png new file mode 100644 index 00000000000..5b80a667936 Binary files /dev/null and b/resources/images/icons2/20x20/online@2x.png differ diff --git a/resources/images/icons2/20x20/online@3x.png b/resources/images/icons2/20x20/online@3x.png new file mode 100644 index 00000000000..9e5d6901ea1 Binary files /dev/null and b/resources/images/icons2/20x20/online@3x.png differ diff --git a/resources/images/icons2/20x20/open@2x.png b/resources/images/icons2/20x20/open@2x.png new file mode 100644 index 00000000000..7c35dbada20 Binary files /dev/null and b/resources/images/icons2/20x20/open@2x.png differ diff --git a/resources/images/icons2/20x20/open@3x.png b/resources/images/icons2/20x20/open@3x.png new file mode 100644 index 00000000000..23a149bef35 Binary files /dev/null and b/resources/images/icons2/20x20/open@3x.png differ diff --git a/resources/images/icons2/20x20/opensea@2x.png b/resources/images/icons2/20x20/opensea@2x.png new file mode 100644 index 00000000000..bb921a392a6 Binary files /dev/null and b/resources/images/icons2/20x20/opensea@2x.png differ diff --git a/resources/images/icons2/20x20/opensea@3x.png b/resources/images/icons2/20x20/opensea@3x.png new file mode 100644 index 00000000000..3d17150a80b Binary files /dev/null and b/resources/images/icons2/20x20/opensea@3x.png differ diff --git a/resources/images/icons2/20x20/options-circle@2x.png b/resources/images/icons2/20x20/options-circle@2x.png new file mode 100644 index 00000000000..de0ea53a50e Binary files /dev/null and b/resources/images/icons2/20x20/options-circle@2x.png differ diff --git a/resources/images/icons2/20x20/options-circle@3x.png b/resources/images/icons2/20x20/options-circle@3x.png new file mode 100644 index 00000000000..ab72928b12d Binary files /dev/null and b/resources/images/icons2/20x20/options-circle@3x.png differ diff --git a/resources/images/icons2/20x20/options@2x.png b/resources/images/icons2/20x20/options@2x.png new file mode 100644 index 00000000000..b628f2e502b Binary files /dev/null and b/resources/images/icons2/20x20/options@2x.png differ diff --git a/resources/images/icons2/20x20/options@3x.png b/resources/images/icons2/20x20/options@3x.png new file mode 100644 index 00000000000..41df9f3550f Binary files /dev/null and b/resources/images/icons2/20x20/options@3x.png differ diff --git a/resources/images/icons2/20x20/pallete@2x.png b/resources/images/icons2/20x20/pallete@2x.png new file mode 100644 index 00000000000..f0ae5cd7ec1 Binary files /dev/null and b/resources/images/icons2/20x20/pallete@2x.png differ diff --git a/resources/images/icons2/20x20/pallete@3x.png b/resources/images/icons2/20x20/pallete@3x.png new file mode 100644 index 00000000000..f3ae29fb256 Binary files /dev/null and b/resources/images/icons2/20x20/pallete@3x.png differ diff --git a/resources/images/icons2/20x20/password@2x.png b/resources/images/icons2/20x20/password@2x.png new file mode 100644 index 00000000000..4924f1bf8a4 Binary files /dev/null and b/resources/images/icons2/20x20/password@2x.png differ diff --git a/resources/images/icons2/20x20/password@3x.png b/resources/images/icons2/20x20/password@3x.png new file mode 100644 index 00000000000..802af935c16 Binary files /dev/null and b/resources/images/icons2/20x20/password@3x.png differ diff --git a/resources/images/icons2/20x20/pause-audio@2x.png b/resources/images/icons2/20x20/pause-audio@2x.png new file mode 100644 index 00000000000..b73e28e5a80 Binary files /dev/null and b/resources/images/icons2/20x20/pause-audio@2x.png differ diff --git a/resources/images/icons2/20x20/pause-audio@3x.png b/resources/images/icons2/20x20/pause-audio@3x.png new file mode 100644 index 00000000000..b0a26a06823 Binary files /dev/null and b/resources/images/icons2/20x20/pause-audio@3x.png differ diff --git a/resources/images/icons2/20x20/pause@2x.png b/resources/images/icons2/20x20/pause@2x.png new file mode 100644 index 00000000000..ce45ac22a32 Binary files /dev/null and b/resources/images/icons2/20x20/pause@2x.png differ diff --git a/resources/images/icons2/20x20/pause@3x.png b/resources/images/icons2/20x20/pause@3x.png new file mode 100644 index 00000000000..c161d42ac1d Binary files /dev/null and b/resources/images/icons2/20x20/pause@3x.png differ diff --git a/resources/images/icons2/20x20/pending-dark-blur@2x.png b/resources/images/icons2/20x20/pending-dark-blur@2x.png new file mode 100644 index 00000000000..f87620a4d7d Binary files /dev/null and b/resources/images/icons2/20x20/pending-dark-blur@2x.png differ diff --git a/resources/images/icons2/20x20/pending-dark-blur@3x.png b/resources/images/icons2/20x20/pending-dark-blur@3x.png new file mode 100644 index 00000000000..40f1495d71c Binary files /dev/null and b/resources/images/icons2/20x20/pending-dark-blur@3x.png differ diff --git a/resources/images/icons2/20x20/pending-dark@2x.png b/resources/images/icons2/20x20/pending-dark@2x.png new file mode 100644 index 00000000000..d96d724e180 Binary files /dev/null and b/resources/images/icons2/20x20/pending-dark@2x.png differ diff --git a/resources/images/icons2/20x20/pending-dark@3x.png b/resources/images/icons2/20x20/pending-dark@3x.png new file mode 100644 index 00000000000..2d98e3d97b4 Binary files /dev/null and b/resources/images/icons2/20x20/pending-dark@3x.png differ diff --git a/resources/images/icons2/20x20/pending-light@2x.png b/resources/images/icons2/20x20/pending-light@2x.png new file mode 100644 index 00000000000..e9ded33b354 Binary files /dev/null and b/resources/images/icons2/20x20/pending-light@2x.png differ diff --git a/resources/images/icons2/20x20/pending-light@3x.png b/resources/images/icons2/20x20/pending-light@3x.png new file mode 100644 index 00000000000..960f93e19ae Binary files /dev/null and b/resources/images/icons2/20x20/pending-light@3x.png differ diff --git a/resources/images/icons2/20x20/pending-state@2x.png b/resources/images/icons2/20x20/pending-state@2x.png new file mode 100644 index 00000000000..e9ded33b354 Binary files /dev/null and b/resources/images/icons2/20x20/pending-state@2x.png differ diff --git a/resources/images/icons2/20x20/pending-state@3x.png b/resources/images/icons2/20x20/pending-state@3x.png new file mode 100644 index 00000000000..960f93e19ae Binary files /dev/null and b/resources/images/icons2/20x20/pending-state@3x.png differ diff --git a/resources/images/icons2/20x20/pending-user@2x.png b/resources/images/icons2/20x20/pending-user@2x.png new file mode 100644 index 00000000000..409d0c4dcdb Binary files /dev/null and b/resources/images/icons2/20x20/pending-user@2x.png differ diff --git a/resources/images/icons2/20x20/pending-user@3x.png b/resources/images/icons2/20x20/pending-user@3x.png new file mode 100644 index 00000000000..53f45c8cc46 Binary files /dev/null and b/resources/images/icons2/20x20/pending-user@3x.png differ diff --git a/resources/images/icons2/20x20/pending@2x.png b/resources/images/icons2/20x20/pending@2x.png new file mode 100644 index 00000000000..e9eb902adb0 Binary files /dev/null and b/resources/images/icons2/20x20/pending@2x.png differ diff --git a/resources/images/icons2/20x20/pending@3x.png b/resources/images/icons2/20x20/pending@3x.png new file mode 100644 index 00000000000..77c738455c5 Binary files /dev/null and b/resources/images/icons2/20x20/pending@3x.png differ diff --git a/resources/images/icons2/20x20/percentage@2x.png b/resources/images/icons2/20x20/percentage@2x.png new file mode 100644 index 00000000000..020f6d65661 Binary files /dev/null and b/resources/images/icons2/20x20/percentage@2x.png differ diff --git a/resources/images/icons2/20x20/percentage@3x.png b/resources/images/icons2/20x20/percentage@3x.png new file mode 100644 index 00000000000..9511f0c810b Binary files /dev/null and b/resources/images/icons2/20x20/percentage@3x.png differ diff --git a/resources/images/icons2/20x20/pin@2x.png b/resources/images/icons2/20x20/pin@2x.png new file mode 100644 index 00000000000..4d036932e83 Binary files /dev/null and b/resources/images/icons2/20x20/pin@2x.png differ diff --git a/resources/images/icons2/20x20/pin@3x.png b/resources/images/icons2/20x20/pin@3x.png new file mode 100644 index 00000000000..c6202128565 Binary files /dev/null and b/resources/images/icons2/20x20/pin@3x.png differ diff --git a/resources/images/icons2/20x20/pincode@2x.png b/resources/images/icons2/20x20/pincode@2x.png new file mode 100644 index 00000000000..0672941b88e Binary files /dev/null and b/resources/images/icons2/20x20/pincode@2x.png differ diff --git a/resources/images/icons2/20x20/pincode@3x.png b/resources/images/icons2/20x20/pincode@3x.png new file mode 100644 index 00000000000..3164aee0c60 Binary files /dev/null and b/resources/images/icons2/20x20/pincode@3x.png differ diff --git a/resources/images/icons2/20x20/placeholder@2x.png b/resources/images/icons2/20x20/placeholder@2x.png new file mode 100644 index 00000000000..8f9c1a08cb2 Binary files /dev/null and b/resources/images/icons2/20x20/placeholder@2x.png differ diff --git a/resources/images/icons2/20x20/placeholder@3x.png b/resources/images/icons2/20x20/placeholder@3x.png new file mode 100644 index 00000000000..b0f177fabaa Binary files /dev/null and b/resources/images/icons2/20x20/placeholder@3x.png differ diff --git a/resources/images/icons2/20x20/play-audio@2x.png b/resources/images/icons2/20x20/play-audio@2x.png new file mode 100644 index 00000000000..38f9803d4a5 Binary files /dev/null and b/resources/images/icons2/20x20/play-audio@2x.png differ diff --git a/resources/images/icons2/20x20/play-audio@3x.png b/resources/images/icons2/20x20/play-audio@3x.png new file mode 100644 index 00000000000..417b576aad3 Binary files /dev/null and b/resources/images/icons2/20x20/play-audio@3x.png differ diff --git a/resources/images/icons2/20x20/play@2x.png b/resources/images/icons2/20x20/play@2x.png new file mode 100644 index 00000000000..6cf0ebf7215 Binary files /dev/null and b/resources/images/icons2/20x20/play@2x.png differ diff --git a/resources/images/icons2/20x20/play@3x.png b/resources/images/icons2/20x20/play@3x.png new file mode 100644 index 00000000000..7739746a9a7 Binary files /dev/null and b/resources/images/icons2/20x20/play@3x.png differ diff --git a/resources/images/icons2/20x20/positive-state@2x.png b/resources/images/icons2/20x20/positive-state@2x.png new file mode 100644 index 00000000000..58cb3be431b Binary files /dev/null and b/resources/images/icons2/20x20/positive-state@2x.png differ diff --git a/resources/images/icons2/20x20/positive-state@3x.png b/resources/images/icons2/20x20/positive-state@3x.png new file mode 100644 index 00000000000..212de06bbd7 Binary files /dev/null and b/resources/images/icons2/20x20/positive-state@3x.png differ diff --git a/resources/images/icons2/20x20/privacy@2x.png b/resources/images/icons2/20x20/privacy@2x.png new file mode 100644 index 00000000000..31a41b1e7c1 Binary files /dev/null and b/resources/images/icons2/20x20/privacy@2x.png differ diff --git a/resources/images/icons2/20x20/privacy@3x.png b/resources/images/icons2/20x20/privacy@3x.png new file mode 100644 index 00000000000..ad57c161794 Binary files /dev/null and b/resources/images/icons2/20x20/privacy@3x.png differ diff --git a/resources/images/icons2/20x20/profile@2x.png b/resources/images/icons2/20x20/profile@2x.png new file mode 100644 index 00000000000..03970f6478d Binary files /dev/null and b/resources/images/icons2/20x20/profile@2x.png differ diff --git a/resources/images/icons2/20x20/profile@3x.png b/resources/images/icons2/20x20/profile@3x.png new file mode 100644 index 00000000000..c5198acba8f Binary files /dev/null and b/resources/images/icons2/20x20/profile@3x.png differ diff --git a/resources/images/icons2/20x20/pullup@2x.png b/resources/images/icons2/20x20/pullup@2x.png new file mode 100644 index 00000000000..1c8fbe68b9f Binary files /dev/null and b/resources/images/icons2/20x20/pullup@2x.png differ diff --git a/resources/images/icons2/20x20/pullup@3x.png b/resources/images/icons2/20x20/pullup@3x.png new file mode 100644 index 00000000000..148032fa1c4 Binary files /dev/null and b/resources/images/icons2/20x20/pullup@3x.png differ diff --git a/resources/images/icons2/20x20/qr-code@2x.png b/resources/images/icons2/20x20/qr-code@2x.png new file mode 100644 index 00000000000..e78be518da0 Binary files /dev/null and b/resources/images/icons2/20x20/qr-code@2x.png differ diff --git a/resources/images/icons2/20x20/qr-code@3x.png b/resources/images/icons2/20x20/qr-code@3x.png new file mode 100644 index 00000000000..038732d25dd Binary files /dev/null and b/resources/images/icons2/20x20/qr-code@3x.png differ diff --git a/resources/images/icons2/20x20/reaction@2x.png b/resources/images/icons2/20x20/reaction@2x.png new file mode 100644 index 00000000000..5c7f4742514 Binary files /dev/null and b/resources/images/icons2/20x20/reaction@2x.png differ diff --git a/resources/images/icons2/20x20/reaction@3x.png b/resources/images/icons2/20x20/reaction@3x.png new file mode 100644 index 00000000000..94f4584f15e Binary files /dev/null and b/resources/images/icons2/20x20/reaction@3x.png differ diff --git a/resources/images/icons2/20x20/receive-message@2x.png b/resources/images/icons2/20x20/receive-message@2x.png new file mode 100644 index 00000000000..53676d18cd6 Binary files /dev/null and b/resources/images/icons2/20x20/receive-message@2x.png differ diff --git a/resources/images/icons2/20x20/receive-message@3x.png b/resources/images/icons2/20x20/receive-message@3x.png new file mode 100644 index 00000000000..42affa6d0f9 Binary files /dev/null and b/resources/images/icons2/20x20/receive-message@3x.png differ diff --git a/resources/images/icons2/20x20/receive@2x.png b/resources/images/icons2/20x20/receive@2x.png new file mode 100644 index 00000000000..344798313ef Binary files /dev/null and b/resources/images/icons2/20x20/receive@2x.png differ diff --git a/resources/images/icons2/20x20/receive@3x.png b/resources/images/icons2/20x20/receive@3x.png new file mode 100644 index 00000000000..761b2bc147b Binary files /dev/null and b/resources/images/icons2/20x20/receive@3x.png differ diff --git a/resources/images/icons2/20x20/recent@2x.png b/resources/images/icons2/20x20/recent@2x.png new file mode 100644 index 00000000000..038ca23afe9 Binary files /dev/null and b/resources/images/icons2/20x20/recent@2x.png differ diff --git a/resources/images/icons2/20x20/recent@3x.png b/resources/images/icons2/20x20/recent@3x.png new file mode 100644 index 00000000000..2224b0e6990 Binary files /dev/null and b/resources/images/icons2/20x20/recent@3x.png differ diff --git a/resources/images/icons2/20x20/refresh@2x.png b/resources/images/icons2/20x20/refresh@2x.png new file mode 100644 index 00000000000..f2f72a0ac22 Binary files /dev/null and b/resources/images/icons2/20x20/refresh@2x.png differ diff --git a/resources/images/icons2/20x20/refresh@3x.png b/resources/images/icons2/20x20/refresh@3x.png new file mode 100644 index 00000000000..aed1deb3427 Binary files /dev/null and b/resources/images/icons2/20x20/refresh@3x.png differ diff --git a/resources/images/icons2/20x20/relaxed@2x.png b/resources/images/icons2/20x20/relaxed@2x.png new file mode 100644 index 00000000000..16f9c58d3d3 Binary files /dev/null and b/resources/images/icons2/20x20/relaxed@2x.png differ diff --git a/resources/images/icons2/20x20/relaxed@3x.png b/resources/images/icons2/20x20/relaxed@3x.png new file mode 100644 index 00000000000..ace3aad9a33 Binary files /dev/null and b/resources/images/icons2/20x20/relaxed@3x.png differ diff --git a/resources/images/icons2/20x20/remove-user@2x.png b/resources/images/icons2/20x20/remove-user@2x.png new file mode 100644 index 00000000000..3a0f9589486 Binary files /dev/null and b/resources/images/icons2/20x20/remove-user@2x.png differ diff --git a/resources/images/icons2/20x20/remove-user@3x.png b/resources/images/icons2/20x20/remove-user@3x.png new file mode 100644 index 00000000000..d5074f0a47b Binary files /dev/null and b/resources/images/icons2/20x20/remove-user@3x.png differ diff --git a/resources/images/icons2/20x20/remove@2x.png b/resources/images/icons2/20x20/remove@2x.png new file mode 100644 index 00000000000..d869c520ec6 Binary files /dev/null and b/resources/images/icons2/20x20/remove@2x.png differ diff --git a/resources/images/icons2/20x20/remove@3x.png b/resources/images/icons2/20x20/remove@3x.png new file mode 100644 index 00000000000..ad4da8002a8 Binary files /dev/null and b/resources/images/icons2/20x20/remove@3x.png differ diff --git a/resources/images/icons2/20x20/reorder@2x.png b/resources/images/icons2/20x20/reorder@2x.png new file mode 100644 index 00000000000..2933a6e8073 Binary files /dev/null and b/resources/images/icons2/20x20/reorder@2x.png differ diff --git a/resources/images/icons2/20x20/reorder@3x.png b/resources/images/icons2/20x20/reorder@3x.png new file mode 100644 index 00000000000..ce6e7aaf863 Binary files /dev/null and b/resources/images/icons2/20x20/reorder@3x.png differ diff --git a/resources/images/icons2/20x20/reply@2x.png b/resources/images/icons2/20x20/reply@2x.png new file mode 100644 index 00000000000..aaab824b839 Binary files /dev/null and b/resources/images/icons2/20x20/reply@2x.png differ diff --git a/resources/images/icons2/20x20/reply@3x.png b/resources/images/icons2/20x20/reply@3x.png new file mode 100644 index 00000000000..50eb73b9a3a Binary files /dev/null and b/resources/images/icons2/20x20/reply@3x.png differ diff --git a/resources/images/icons2/20x20/reveal-whitelist@2x.png b/resources/images/icons2/20x20/reveal-whitelist@2x.png new file mode 100644 index 00000000000..425d510564e Binary files /dev/null and b/resources/images/icons2/20x20/reveal-whitelist@2x.png differ diff --git a/resources/images/icons2/20x20/reveal-whitelist@3x.png b/resources/images/icons2/20x20/reveal-whitelist@3x.png new file mode 100644 index 00000000000..777fb4a6210 Binary files /dev/null and b/resources/images/icons2/20x20/reveal-whitelist@3x.png differ diff --git a/resources/images/icons2/20x20/reveal@2x.png b/resources/images/icons2/20x20/reveal@2x.png new file mode 100644 index 00000000000..d8579705bb1 Binary files /dev/null and b/resources/images/icons2/20x20/reveal@2x.png differ diff --git a/resources/images/icons2/20x20/reveal@3x.png b/resources/images/icons2/20x20/reveal@3x.png new file mode 100644 index 00000000000..74a98e66051 Binary files /dev/null and b/resources/images/icons2/20x20/reveal@3x.png differ diff --git a/resources/images/icons2/20x20/revere@2x.png b/resources/images/icons2/20x20/revere@2x.png new file mode 100644 index 00000000000..3c7222a5094 Binary files /dev/null and b/resources/images/icons2/20x20/revere@2x.png differ diff --git a/resources/images/icons2/20x20/revere@3x.png b/resources/images/icons2/20x20/revere@3x.png new file mode 100644 index 00000000000..c9ebf606d1e Binary files /dev/null and b/resources/images/icons2/20x20/revere@3x.png differ diff --git a/resources/images/icons2/20x20/revert@2x.png b/resources/images/icons2/20x20/revert@2x.png new file mode 100644 index 00000000000..43d35ed8cd8 Binary files /dev/null and b/resources/images/icons2/20x20/revert@2x.png differ diff --git a/resources/images/icons2/20x20/revert@3x.png b/resources/images/icons2/20x20/revert@3x.png new file mode 100644 index 00000000000..2a068139439 Binary files /dev/null and b/resources/images/icons2/20x20/revert@3x.png differ diff --git a/resources/images/icons2/20x20/review-id@2x.png b/resources/images/icons2/20x20/review-id@2x.png new file mode 100644 index 00000000000..eb2ee1432b4 Binary files /dev/null and b/resources/images/icons2/20x20/review-id@2x.png differ diff --git a/resources/images/icons2/20x20/review-id@3x.png b/resources/images/icons2/20x20/review-id@3x.png new file mode 100644 index 00000000000..fc04a1edb41 Binary files /dev/null and b/resources/images/icons2/20x20/review-id@3x.png differ diff --git a/resources/images/icons2/20x20/review-request@2x.png b/resources/images/icons2/20x20/review-request@2x.png new file mode 100644 index 00000000000..a23948b16be Binary files /dev/null and b/resources/images/icons2/20x20/review-request@2x.png differ diff --git a/resources/images/icons2/20x20/review-request@3x.png b/resources/images/icons2/20x20/review-request@3x.png new file mode 100644 index 00000000000..b78bffc7286 Binary files /dev/null and b/resources/images/icons2/20x20/review-request@3x.png differ diff --git a/resources/images/icons2/20x20/right-align@2x.png b/resources/images/icons2/20x20/right-align@2x.png new file mode 100644 index 00000000000..f6152c80297 Binary files /dev/null and b/resources/images/icons2/20x20/right-align@2x.png differ diff --git a/resources/images/icons2/20x20/right-align@3x.png b/resources/images/icons2/20x20/right-align@3x.png new file mode 100644 index 00000000000..963e5031bff Binary files /dev/null and b/resources/images/icons2/20x20/right-align@3x.png differ diff --git a/resources/images/icons2/20x20/rotate@2x.png b/resources/images/icons2/20x20/rotate@2x.png new file mode 100644 index 00000000000..64e52564511 Binary files /dev/null and b/resources/images/icons2/20x20/rotate@2x.png differ diff --git a/resources/images/icons2/20x20/rotate@3x.png b/resources/images/icons2/20x20/rotate@3x.png new file mode 100644 index 00000000000..9b31e76ff28 Binary files /dev/null and b/resources/images/icons2/20x20/rotate@3x.png differ diff --git a/resources/images/icons2/20x20/sad@2x.png b/resources/images/icons2/20x20/sad@2x.png new file mode 100644 index 00000000000..3e2e95f5bf7 Binary files /dev/null and b/resources/images/icons2/20x20/sad@2x.png differ diff --git a/resources/images/icons2/20x20/sad@3x.png b/resources/images/icons2/20x20/sad@3x.png new file mode 100644 index 00000000000..0c9ac8a588e Binary files /dev/null and b/resources/images/icons2/20x20/sad@3x.png differ diff --git a/resources/images/icons2/20x20/save@2x.png b/resources/images/icons2/20x20/save@2x.png new file mode 100644 index 00000000000..3e2d06a5b2a Binary files /dev/null and b/resources/images/icons2/20x20/save@2x.png differ diff --git a/resources/images/icons2/20x20/save@3x.png b/resources/images/icons2/20x20/save@3x.png new file mode 100644 index 00000000000..f37fc09d33d Binary files /dev/null and b/resources/images/icons2/20x20/save@3x.png differ diff --git a/resources/images/icons2/20x20/scan-big@2x.png b/resources/images/icons2/20x20/scan-big@2x.png new file mode 100644 index 00000000000..8e0e79cbafc Binary files /dev/null and b/resources/images/icons2/20x20/scan-big@2x.png differ diff --git a/resources/images/icons2/20x20/scan-big@3x.png b/resources/images/icons2/20x20/scan-big@3x.png new file mode 100644 index 00000000000..fe85f7acb66 Binary files /dev/null and b/resources/images/icons2/20x20/scan-big@3x.png differ diff --git a/resources/images/icons2/20x20/scan@2x.png b/resources/images/icons2/20x20/scan@2x.png new file mode 100644 index 00000000000..3ded1cb67dd Binary files /dev/null and b/resources/images/icons2/20x20/scan@2x.png differ diff --git a/resources/images/icons2/20x20/scan@3x.png b/resources/images/icons2/20x20/scan@3x.png new file mode 100644 index 00000000000..adda01c6612 Binary files /dev/null and b/resources/images/icons2/20x20/scan@3x.png differ diff --git a/resources/images/icons2/20x20/search@2x.png b/resources/images/icons2/20x20/search@2x.png new file mode 100644 index 00000000000..5a43eca30d7 Binary files /dev/null and b/resources/images/icons2/20x20/search@2x.png differ diff --git a/resources/images/icons2/20x20/search@3x.png b/resources/images/icons2/20x20/search@3x.png new file mode 100644 index 00000000000..ae2cc46a2ce Binary files /dev/null and b/resources/images/icons2/20x20/search@3x.png differ diff --git a/resources/images/icons2/20x20/seed@2x.png b/resources/images/icons2/20x20/seed@2x.png new file mode 100644 index 00000000000..56bf3cca0ef Binary files /dev/null and b/resources/images/icons2/20x20/seed@2x.png differ diff --git a/resources/images/icons2/20x20/seed@3x.png b/resources/images/icons2/20x20/seed@3x.png new file mode 100644 index 00000000000..049373cc58a Binary files /dev/null and b/resources/images/icons2/20x20/seed@3x.png differ diff --git a/resources/images/icons2/20x20/send-message@2x.png b/resources/images/icons2/20x20/send-message@2x.png new file mode 100644 index 00000000000..60d41ded699 Binary files /dev/null and b/resources/images/icons2/20x20/send-message@2x.png differ diff --git a/resources/images/icons2/20x20/send-message@3x.png b/resources/images/icons2/20x20/send-message@3x.png new file mode 100644 index 00000000000..42f68d8c91d Binary files /dev/null and b/resources/images/icons2/20x20/send-message@3x.png differ diff --git a/resources/images/icons2/20x20/send@2x.png b/resources/images/icons2/20x20/send@2x.png new file mode 100644 index 00000000000..a917e731ec5 Binary files /dev/null and b/resources/images/icons2/20x20/send@2x.png differ diff --git a/resources/images/icons2/20x20/send@3x.png b/resources/images/icons2/20x20/send@3x.png new file mode 100644 index 00000000000..67edbc2834e Binary files /dev/null and b/resources/images/icons2/20x20/send@3x.png differ diff --git a/resources/images/icons2/20x20/settings@2x.png b/resources/images/icons2/20x20/settings@2x.png new file mode 100644 index 00000000000..e0c0631d60d Binary files /dev/null and b/resources/images/icons2/20x20/settings@2x.png differ diff --git a/resources/images/icons2/20x20/settings@3x.png b/resources/images/icons2/20x20/settings@3x.png new file mode 100644 index 00000000000..b4da2199bf3 Binary files /dev/null and b/resources/images/icons2/20x20/settings@3x.png differ diff --git a/resources/images/icons2/20x20/share@2x.png b/resources/images/icons2/20x20/share@2x.png new file mode 100644 index 00000000000..11a2886b3b3 Binary files /dev/null and b/resources/images/icons2/20x20/share@2x.png differ diff --git a/resources/images/icons2/20x20/share@3x.png b/resources/images/icons2/20x20/share@3x.png new file mode 100644 index 00000000000..a6718f0f378 Binary files /dev/null and b/resources/images/icons2/20x20/share@3x.png differ diff --git a/resources/images/icons2/20x20/signature@2x.png b/resources/images/icons2/20x20/signature@2x.png new file mode 100644 index 00000000000..fec0e2ee244 Binary files /dev/null and b/resources/images/icons2/20x20/signature@2x.png differ diff --git a/resources/images/icons2/20x20/signature@3x.png b/resources/images/icons2/20x20/signature@3x.png new file mode 100644 index 00000000000..ca5f1e21cdf Binary files /dev/null and b/resources/images/icons2/20x20/signature@3x.png differ diff --git a/resources/images/icons2/20x20/sort@2x.png b/resources/images/icons2/20x20/sort@2x.png new file mode 100644 index 00000000000..5b9bd088baf Binary files /dev/null and b/resources/images/icons2/20x20/sort@2x.png differ diff --git a/resources/images/icons2/20x20/sort@3x.png b/resources/images/icons2/20x20/sort@3x.png new file mode 100644 index 00000000000..ea73ed40da9 Binary files /dev/null and b/resources/images/icons2/20x20/sort@3x.png differ diff --git a/resources/images/icons2/20x20/speed@2x.png b/resources/images/icons2/20x20/speed@2x.png new file mode 100644 index 00000000000..ba77dccb053 Binary files /dev/null and b/resources/images/icons2/20x20/speed@2x.png differ diff --git a/resources/images/icons2/20x20/speed@3x.png b/resources/images/icons2/20x20/speed@3x.png new file mode 100644 index 00000000000..c0fba541d24 Binary files /dev/null and b/resources/images/icons2/20x20/speed@3x.png differ diff --git a/resources/images/icons2/20x20/status-logo-bw@2x.png b/resources/images/icons2/20x20/status-logo-bw@2x.png new file mode 100644 index 00000000000..3953f770463 Binary files /dev/null and b/resources/images/icons2/20x20/status-logo-bw@2x.png differ diff --git a/resources/images/icons2/20x20/status-logo-bw@3x.png b/resources/images/icons2/20x20/status-logo-bw@3x.png new file mode 100644 index 00000000000..e34286a11a0 Binary files /dev/null and b/resources/images/icons2/20x20/status-logo-bw@3x.png differ diff --git a/resources/images/icons2/20x20/status-logo@2x.png b/resources/images/icons2/20x20/status-logo@2x.png new file mode 100644 index 00000000000..a0abf331315 Binary files /dev/null and b/resources/images/icons2/20x20/status-logo@2x.png differ diff --git a/resources/images/icons2/20x20/status-logo@3x.png b/resources/images/icons2/20x20/status-logo@3x.png new file mode 100644 index 00000000000..0c28d1e718a Binary files /dev/null and b/resources/images/icons2/20x20/status-logo@3x.png differ diff --git a/resources/images/icons2/20x20/status@2x.png b/resources/images/icons2/20x20/status@2x.png new file mode 100644 index 00000000000..614825c3cad Binary files /dev/null and b/resources/images/icons2/20x20/status@2x.png differ diff --git a/resources/images/icons2/20x20/status@3x.png b/resources/images/icons2/20x20/status@3x.png new file mode 100644 index 00000000000..f0ebc075a6c Binary files /dev/null and b/resources/images/icons2/20x20/status@3x.png differ diff --git a/resources/images/icons2/20x20/stickers@2x.png b/resources/images/icons2/20x20/stickers@2x.png new file mode 100644 index 00000000000..bebfe36a152 Binary files /dev/null and b/resources/images/icons2/20x20/stickers@2x.png differ diff --git a/resources/images/icons2/20x20/stickers@3x.png b/resources/images/icons2/20x20/stickers@3x.png new file mode 100644 index 00000000000..72202c4cf8d Binary files /dev/null and b/resources/images/icons2/20x20/stickers@3x.png differ diff --git a/resources/images/icons2/20x20/stop@2x.png b/resources/images/icons2/20x20/stop@2x.png new file mode 100644 index 00000000000..10927d0662b Binary files /dev/null and b/resources/images/icons2/20x20/stop@2x.png differ diff --git a/resources/images/icons2/20x20/stop@3x.png b/resources/images/icons2/20x20/stop@3x.png new file mode 100644 index 00000000000..c728fbf9194 Binary files /dev/null and b/resources/images/icons2/20x20/stop@3x.png differ diff --git a/resources/images/icons2/20x20/strikethrough@2x.png b/resources/images/icons2/20x20/strikethrough@2x.png new file mode 100644 index 00000000000..2d92a2aa3c0 Binary files /dev/null and b/resources/images/icons2/20x20/strikethrough@2x.png differ diff --git a/resources/images/icons2/20x20/strikethrough@3x.png b/resources/images/icons2/20x20/strikethrough@3x.png new file mode 100644 index 00000000000..4035fd998c3 Binary files /dev/null and b/resources/images/icons2/20x20/strikethrough@3x.png differ diff --git a/resources/images/icons2/20x20/subscript@2x.png b/resources/images/icons2/20x20/subscript@2x.png new file mode 100644 index 00000000000..66f2d481230 Binary files /dev/null and b/resources/images/icons2/20x20/subscript@2x.png differ diff --git a/resources/images/icons2/20x20/subscript@3x.png b/resources/images/icons2/20x20/subscript@3x.png new file mode 100644 index 00000000000..8f26e7a1075 Binary files /dev/null and b/resources/images/icons2/20x20/subscript@3x.png differ diff --git a/resources/images/icons2/20x20/superscript@2x.png b/resources/images/icons2/20x20/superscript@2x.png new file mode 100644 index 00000000000..2e7463418df Binary files /dev/null and b/resources/images/icons2/20x20/superscript@2x.png differ diff --git a/resources/images/icons2/20x20/superscript@3x.png b/resources/images/icons2/20x20/superscript@3x.png new file mode 100644 index 00000000000..bc4ca303e15 Binary files /dev/null and b/resources/images/icons2/20x20/superscript@3x.png differ diff --git a/resources/images/icons2/20x20/swap@2x.png b/resources/images/icons2/20x20/swap@2x.png new file mode 100644 index 00000000000..7c19e306819 Binary files /dev/null and b/resources/images/icons2/20x20/swap@2x.png differ diff --git a/resources/images/icons2/20x20/swap@3x.png b/resources/images/icons2/20x20/swap@3x.png new file mode 100644 index 00000000000..6fa567a0556 Binary files /dev/null and b/resources/images/icons2/20x20/swap@3x.png differ diff --git a/resources/images/icons2/20x20/symbols@2x.png b/resources/images/icons2/20x20/symbols@2x.png new file mode 100644 index 00000000000..7b2ae3fa3cc Binary files /dev/null and b/resources/images/icons2/20x20/symbols@2x.png differ diff --git a/resources/images/icons2/20x20/symbols@3x.png b/resources/images/icons2/20x20/symbols@3x.png new file mode 100644 index 00000000000..5ead3250cbb Binary files /dev/null and b/resources/images/icons2/20x20/symbols@3x.png differ diff --git a/resources/images/icons2/20x20/syncing@2x.png b/resources/images/icons2/20x20/syncing@2x.png new file mode 100644 index 00000000000..b039f5656cb Binary files /dev/null and b/resources/images/icons2/20x20/syncing@2x.png differ diff --git a/resources/images/icons2/20x20/syncing@3x.png b/resources/images/icons2/20x20/syncing@3x.png new file mode 100644 index 00000000000..5a40ab119c1 Binary files /dev/null and b/resources/images/icons2/20x20/syncing@3x.png differ diff --git a/resources/images/icons2/20x20/table@2x.png b/resources/images/icons2/20x20/table@2x.png new file mode 100644 index 00000000000..fbd8a0e2228 Binary files /dev/null and b/resources/images/icons2/20x20/table@2x.png differ diff --git a/resources/images/icons2/20x20/table@3x.png b/resources/images/icons2/20x20/table@3x.png new file mode 100644 index 00000000000..d3261ef9c06 Binary files /dev/null and b/resources/images/icons2/20x20/table@3x.png differ diff --git a/resources/images/icons2/20x20/tablet@2x.png b/resources/images/icons2/20x20/tablet@2x.png new file mode 100644 index 00000000000..62b5eb7e701 Binary files /dev/null and b/resources/images/icons2/20x20/tablet@2x.png differ diff --git a/resources/images/icons2/20x20/tablet@3x.png b/resources/images/icons2/20x20/tablet@3x.png new file mode 100644 index 00000000000..754e56a382a Binary files /dev/null and b/resources/images/icons2/20x20/tablet@3x.png differ diff --git a/resources/images/icons2/20x20/tabs@2x.png b/resources/images/icons2/20x20/tabs@2x.png new file mode 100644 index 00000000000..1ded8bce7d6 Binary files /dev/null and b/resources/images/icons2/20x20/tabs@2x.png differ diff --git a/resources/images/icons2/20x20/tabs@3x.png b/resources/images/icons2/20x20/tabs@3x.png new file mode 100644 index 00000000000..bccdbf75d47 Binary files /dev/null and b/resources/images/icons2/20x20/tabs@3x.png differ diff --git a/resources/images/icons2/20x20/toggle@2x.png b/resources/images/icons2/20x20/toggle@2x.png new file mode 100644 index 00000000000..c7a79057e16 Binary files /dev/null and b/resources/images/icons2/20x20/toggle@2x.png differ diff --git a/resources/images/icons2/20x20/toggle@3x.png b/resources/images/icons2/20x20/toggle@3x.png new file mode 100644 index 00000000000..3cbcf6f0225 Binary files /dev/null and b/resources/images/icons2/20x20/toggle@3x.png differ diff --git a/resources/images/icons2/20x20/token-sales@2x.png b/resources/images/icons2/20x20/token-sales@2x.png new file mode 100644 index 00000000000..2ec8af7ebf8 Binary files /dev/null and b/resources/images/icons2/20x20/token-sales@2x.png differ diff --git a/resources/images/icons2/20x20/token-sales@3x.png b/resources/images/icons2/20x20/token-sales@3x.png new file mode 100644 index 00000000000..c4c34677088 Binary files /dev/null and b/resources/images/icons2/20x20/token-sales@3x.png differ diff --git a/resources/images/icons2/20x20/token@2x.png b/resources/images/icons2/20x20/token@2x.png new file mode 100644 index 00000000000..4e092d0b39d Binary files /dev/null and b/resources/images/icons2/20x20/token@2x.png differ diff --git a/resources/images/icons2/20x20/token@3x.png b/resources/images/icons2/20x20/token@3x.png new file mode 100644 index 00000000000..a809a41bdcd Binary files /dev/null and b/resources/images/icons2/20x20/token@3x.png differ diff --git a/resources/images/icons2/20x20/topbar-dock@2x.png b/resources/images/icons2/20x20/topbar-dock@2x.png new file mode 100644 index 00000000000..3a6f128f351 Binary files /dev/null and b/resources/images/icons2/20x20/topbar-dock@2x.png differ diff --git a/resources/images/icons2/20x20/topbar-dock@3x.png b/resources/images/icons2/20x20/topbar-dock@3x.png new file mode 100644 index 00000000000..ca32bca26de Binary files /dev/null and b/resources/images/icons2/20x20/topbar-dock@3x.png differ diff --git a/resources/images/icons2/20x20/topbar@2x.png b/resources/images/icons2/20x20/topbar@2x.png new file mode 100644 index 00000000000..7f3aff5904e Binary files /dev/null and b/resources/images/icons2/20x20/topbar@2x.png differ diff --git a/resources/images/icons2/20x20/topbar@3x.png b/resources/images/icons2/20x20/topbar@3x.png new file mode 100644 index 00000000000..1f24455b15b Binary files /dev/null and b/resources/images/icons2/20x20/topbar@3x.png differ diff --git a/resources/images/icons2/20x20/touch-id@2x.png b/resources/images/icons2/20x20/touch-id@2x.png new file mode 100644 index 00000000000..1202fd4ab4a Binary files /dev/null and b/resources/images/icons2/20x20/touch-id@2x.png differ diff --git a/resources/images/icons2/20x20/touch-id@3x.png b/resources/images/icons2/20x20/touch-id@3x.png new file mode 100644 index 00000000000..a72665f5662 Binary files /dev/null and b/resources/images/icons2/20x20/touch-id@3x.png differ diff --git a/resources/images/icons2/20x20/transaction@2x.png b/resources/images/icons2/20x20/transaction@2x.png new file mode 100644 index 00000000000..4efbe4b6f76 Binary files /dev/null and b/resources/images/icons2/20x20/transaction@2x.png differ diff --git a/resources/images/icons2/20x20/transaction@3x.png b/resources/images/icons2/20x20/transaction@3x.png new file mode 100644 index 00000000000..1226a961b02 Binary files /dev/null and b/resources/images/icons2/20x20/transaction@3x.png differ diff --git a/resources/images/icons2/20x20/travel@2x.png b/resources/images/icons2/20x20/travel@2x.png new file mode 100644 index 00000000000..380af7184e0 Binary files /dev/null and b/resources/images/icons2/20x20/travel@2x.png differ diff --git a/resources/images/icons2/20x20/travel@3x.png b/resources/images/icons2/20x20/travel@3x.png new file mode 100644 index 00000000000..99393142abd Binary files /dev/null and b/resources/images/icons2/20x20/travel@3x.png differ diff --git a/resources/images/icons2/20x20/ublock-user@2x.png b/resources/images/icons2/20x20/ublock-user@2x.png new file mode 100644 index 00000000000..ab48ade3dd8 Binary files /dev/null and b/resources/images/icons2/20x20/ublock-user@2x.png differ diff --git a/resources/images/icons2/20x20/ublock-user@3x.png b/resources/images/icons2/20x20/ublock-user@3x.png new file mode 100644 index 00000000000..625d19f3a51 Binary files /dev/null and b/resources/images/icons2/20x20/ublock-user@3x.png differ diff --git a/resources/images/icons2/20x20/unblock@2x.png b/resources/images/icons2/20x20/unblock@2x.png new file mode 100644 index 00000000000..305fc6f7b66 Binary files /dev/null and b/resources/images/icons2/20x20/unblock@2x.png differ diff --git a/resources/images/icons2/20x20/unblock@3x.png b/resources/images/icons2/20x20/unblock@3x.png new file mode 100644 index 00000000000..7202a88d0d7 Binary files /dev/null and b/resources/images/icons2/20x20/unblock@3x.png differ diff --git a/resources/images/icons2/20x20/underline@2x.png b/resources/images/icons2/20x20/underline@2x.png new file mode 100644 index 00000000000..7de87e47d43 Binary files /dev/null and b/resources/images/icons2/20x20/underline@2x.png differ diff --git a/resources/images/icons2/20x20/underline@3x.png b/resources/images/icons2/20x20/underline@3x.png new file mode 100644 index 00000000000..c3d1116cfa0 Binary files /dev/null and b/resources/images/icons2/20x20/underline@3x.png differ diff --git a/resources/images/icons2/20x20/unlocked@2x.png b/resources/images/icons2/20x20/unlocked@2x.png new file mode 100644 index 00000000000..995acb354fa Binary files /dev/null and b/resources/images/icons2/20x20/unlocked@2x.png differ diff --git a/resources/images/icons2/20x20/unlocked@3x.png b/resources/images/icons2/20x20/unlocked@3x.png new file mode 100644 index 00000000000..70d025f3849 Binary files /dev/null and b/resources/images/icons2/20x20/unlocked@3x.png differ diff --git a/resources/images/icons2/20x20/unpin@2x.png b/resources/images/icons2/20x20/unpin@2x.png new file mode 100644 index 00000000000..4a1066ba2e2 Binary files /dev/null and b/resources/images/icons2/20x20/unpin@2x.png differ diff --git a/resources/images/icons2/20x20/unpin@3x.png b/resources/images/icons2/20x20/unpin@3x.png new file mode 100644 index 00000000000..ca5071cee33 Binary files /dev/null and b/resources/images/icons2/20x20/unpin@3x.png differ diff --git a/resources/images/icons2/20x20/unread@2x.png b/resources/images/icons2/20x20/unread@2x.png new file mode 100644 index 00000000000..5d548982186 Binary files /dev/null and b/resources/images/icons2/20x20/unread@2x.png differ diff --git a/resources/images/icons2/20x20/unread@3x.png b/resources/images/icons2/20x20/unread@3x.png new file mode 100644 index 00000000000..38452ac269e Binary files /dev/null and b/resources/images/icons2/20x20/unread@3x.png differ diff --git a/resources/images/icons2/20x20/untrustworthy@2x.png b/resources/images/icons2/20x20/untrustworthy@2x.png new file mode 100644 index 00000000000..5ba0cdb6bbe Binary files /dev/null and b/resources/images/icons2/20x20/untrustworthy@2x.png differ diff --git a/resources/images/icons2/20x20/untrustworthy@3x.png b/resources/images/icons2/20x20/untrustworthy@3x.png new file mode 100644 index 00000000000..27e1e45d426 Binary files /dev/null and b/resources/images/icons2/20x20/untrustworthy@3x.png differ diff --git a/resources/images/icons2/20x20/up-to-date@2x.png b/resources/images/icons2/20x20/up-to-date@2x.png new file mode 100644 index 00000000000..466706a4c12 Binary files /dev/null and b/resources/images/icons2/20x20/up-to-date@2x.png differ diff --git a/resources/images/icons2/20x20/up-to-date@3x.png b/resources/images/icons2/20x20/up-to-date@3x.png new file mode 100644 index 00000000000..2e3df8d24aa Binary files /dev/null and b/resources/images/icons2/20x20/up-to-date@3x.png differ diff --git a/resources/images/icons2/20x20/verified@2x.png b/resources/images/icons2/20x20/verified@2x.png new file mode 100644 index 00000000000..81641e55094 Binary files /dev/null and b/resources/images/icons2/20x20/verified@2x.png differ diff --git a/resources/images/icons2/20x20/verified@3x.png b/resources/images/icons2/20x20/verified@3x.png new file mode 100644 index 00000000000..69269b54f36 Binary files /dev/null and b/resources/images/icons2/20x20/verified@3x.png differ diff --git a/resources/images/icons2/20x20/video@2x.png b/resources/images/icons2/20x20/video@2x.png new file mode 100644 index 00000000000..326ff042923 Binary files /dev/null and b/resources/images/icons2/20x20/video@2x.png differ diff --git a/resources/images/icons2/20x20/video@3x.png b/resources/images/icons2/20x20/video@3x.png new file mode 100644 index 00000000000..1b09f472d1e Binary files /dev/null and b/resources/images/icons2/20x20/video@3x.png differ diff --git a/resources/images/icons2/20x20/waffle@2x.png b/resources/images/icons2/20x20/waffle@2x.png new file mode 100644 index 00000000000..eedccdeb500 Binary files /dev/null and b/resources/images/icons2/20x20/waffle@2x.png differ diff --git a/resources/images/icons2/20x20/waffle@3x.png b/resources/images/icons2/20x20/waffle@3x.png new file mode 100644 index 00000000000..62c73a2076a Binary files /dev/null and b/resources/images/icons2/20x20/waffle@3x.png differ diff --git a/resources/images/icons2/20x20/wallet@2x.png b/resources/images/icons2/20x20/wallet@2x.png new file mode 100644 index 00000000000..dd61236c291 Binary files /dev/null and b/resources/images/icons2/20x20/wallet@2x.png differ diff --git a/resources/images/icons2/20x20/wallet@3x.png b/resources/images/icons2/20x20/wallet@3x.png new file mode 100644 index 00000000000..044c44aa40b Binary files /dev/null and b/resources/images/icons2/20x20/wallet@3x.png differ diff --git a/resources/images/icons2/20x20/warning@2x.png b/resources/images/icons2/20x20/warning@2x.png new file mode 100644 index 00000000000..7d85fc429a6 Binary files /dev/null and b/resources/images/icons2/20x20/warning@2x.png differ diff --git a/resources/images/icons2/20x20/warning@3x.png b/resources/images/icons2/20x20/warning@3x.png new file mode 100644 index 00000000000..afcfd1b49b9 Binary files /dev/null and b/resources/images/icons2/20x20/warning@3x.png differ diff --git a/resources/images/icons2/20x20/world@2x.png b/resources/images/icons2/20x20/world@2x.png new file mode 100644 index 00000000000..f47077c3b80 Binary files /dev/null and b/resources/images/icons2/20x20/world@2x.png differ diff --git a/resources/images/icons2/20x20/world@3x.png b/resources/images/icons2/20x20/world@3x.png new file mode 100644 index 00000000000..db828615f06 Binary files /dev/null and b/resources/images/icons2/20x20/world@3x.png differ diff --git a/resources/images/icons2/20x20/zoom-in@2x.png b/resources/images/icons2/20x20/zoom-in@2x.png new file mode 100644 index 00000000000..04056771dd0 Binary files /dev/null and b/resources/images/icons2/20x20/zoom-in@2x.png differ diff --git a/resources/images/icons2/20x20/zoom-in@3x.png b/resources/images/icons2/20x20/zoom-in@3x.png new file mode 100644 index 00000000000..01fc4539ef1 Binary files /dev/null and b/resources/images/icons2/20x20/zoom-in@3x.png differ diff --git a/resources/images/icons2/20x20/zoom-out@2x.png b/resources/images/icons2/20x20/zoom-out@2x.png new file mode 100644 index 00000000000..bc5b1d8b24c Binary files /dev/null and b/resources/images/icons2/20x20/zoom-out@2x.png differ diff --git a/resources/images/icons2/20x20/zoom-out@3x.png b/resources/images/icons2/20x20/zoom-out@3x.png new file mode 100644 index 00000000000..56f9325a424 Binary files /dev/null and b/resources/images/icons2/20x20/zoom-out@3x.png differ diff --git a/resources/images/icons2/24x24/browser@2x.png b/resources/images/icons2/24x24/browser@2x.png new file mode 100644 index 00000000000..a4dc31f5978 Binary files /dev/null and b/resources/images/icons2/24x24/browser@2x.png differ diff --git a/resources/images/icons2/24x24/browser@3x.png b/resources/images/icons2/24x24/browser@3x.png new file mode 100644 index 00000000000..03a7c8560c4 Binary files /dev/null and b/resources/images/icons2/24x24/browser@3x.png differ diff --git a/resources/images/icons2/24x24/communities@2x.png b/resources/images/icons2/24x24/communities@2x.png new file mode 100644 index 00000000000..db49e2b5c82 Binary files /dev/null and b/resources/images/icons2/24x24/communities@2x.png differ diff --git a/resources/images/icons2/24x24/communities@3x.png b/resources/images/icons2/24x24/communities@3x.png new file mode 100644 index 00000000000..0d67a476e44 Binary files /dev/null and b/resources/images/icons2/24x24/communities@3x.png differ diff --git a/resources/images/icons2/24x24/discover@2x.png b/resources/images/icons2/24x24/discover@2x.png new file mode 100644 index 00000000000..8ab2b26b2cc Binary files /dev/null and b/resources/images/icons2/24x24/discover@2x.png differ diff --git a/resources/images/icons2/24x24/discover@3x.png b/resources/images/icons2/24x24/discover@3x.png new file mode 100644 index 00000000000..5158cffba49 Binary files /dev/null and b/resources/images/icons2/24x24/discover@3x.png differ diff --git a/resources/images/icons2/24x24/feed@2x.png b/resources/images/icons2/24x24/feed@2x.png new file mode 100644 index 00000000000..ed5a95a389c Binary files /dev/null and b/resources/images/icons2/24x24/feed@2x.png differ diff --git a/resources/images/icons2/24x24/feed@3x.png b/resources/images/icons2/24x24/feed@3x.png new file mode 100644 index 00000000000..f6b9abaa6f4 Binary files /dev/null and b/resources/images/icons2/24x24/feed@3x.png differ diff --git a/resources/images/icons2/24x24/flash-camera@2x.png b/resources/images/icons2/24x24/flash-camera@2x.png new file mode 100644 index 00000000000..c5c139b46ad Binary files /dev/null and b/resources/images/icons2/24x24/flash-camera@2x.png differ diff --git a/resources/images/icons2/24x24/flash-camera@3x.png b/resources/images/icons2/24x24/flash-camera@3x.png new file mode 100644 index 00000000000..6c6ddd88ea8 Binary files /dev/null and b/resources/images/icons2/24x24/flash-camera@3x.png differ diff --git a/resources/images/icons2/24x24/messages@2x.png b/resources/images/icons2/24x24/messages@2x.png new file mode 100644 index 00000000000..d1e6cf2e06a Binary files /dev/null and b/resources/images/icons2/24x24/messages@2x.png differ diff --git a/resources/images/icons2/24x24/messages@3x.png b/resources/images/icons2/24x24/messages@3x.png new file mode 100644 index 00000000000..9354ff885fb Binary files /dev/null and b/resources/images/icons2/24x24/messages@3x.png differ diff --git a/resources/images/icons2/24x24/nodes@2x.png b/resources/images/icons2/24x24/nodes@2x.png new file mode 100644 index 00000000000..c07d867adc1 Binary files /dev/null and b/resources/images/icons2/24x24/nodes@2x.png differ diff --git a/resources/images/icons2/24x24/nodes@3x.png b/resources/images/icons2/24x24/nodes@3x.png new file mode 100644 index 00000000000..ef152cf4d57 Binary files /dev/null and b/resources/images/icons2/24x24/nodes@3x.png differ diff --git a/resources/images/icons2/24x24/placeholder@2x.png b/resources/images/icons2/24x24/placeholder@2x.png new file mode 100644 index 00000000000..e174b102428 Binary files /dev/null and b/resources/images/icons2/24x24/placeholder@2x.png differ diff --git a/resources/images/icons2/24x24/placeholder@3x.png b/resources/images/icons2/24x24/placeholder@3x.png new file mode 100644 index 00000000000..36d25f4326e Binary files /dev/null and b/resources/images/icons2/24x24/placeholder@3x.png differ diff --git a/resources/images/icons2/24x24/settings@2x.png b/resources/images/icons2/24x24/settings@2x.png new file mode 100644 index 00000000000..95c021b679b Binary files /dev/null and b/resources/images/icons2/24x24/settings@2x.png differ diff --git a/resources/images/icons2/24x24/settings@3x.png b/resources/images/icons2/24x24/settings@3x.png new file mode 100644 index 00000000000..c61bcaf972c Binary files /dev/null and b/resources/images/icons2/24x24/settings@3x.png differ diff --git a/resources/images/icons2/24x24/swap@2x.png b/resources/images/icons2/24x24/swap@2x.png new file mode 100644 index 00000000000..89048b93501 Binary files /dev/null and b/resources/images/icons2/24x24/swap@2x.png differ diff --git a/resources/images/icons2/24x24/swap@3x.png b/resources/images/icons2/24x24/swap@3x.png new file mode 100644 index 00000000000..8d0d8907bd0 Binary files /dev/null and b/resources/images/icons2/24x24/swap@3x.png differ diff --git a/resources/images/icons2/24x24/wallet@2x.png b/resources/images/icons2/24x24/wallet@2x.png new file mode 100644 index 00000000000..9bed857bda8 Binary files /dev/null and b/resources/images/icons2/24x24/wallet@2x.png differ diff --git a/resources/images/icons2/24x24/wallet@3x.png b/resources/images/icons2/24x24/wallet@3x.png new file mode 100644 index 00000000000..329acf43365 Binary files /dev/null and b/resources/images/icons2/24x24/wallet@3x.png differ diff --git a/resources/images/icons2/32x32/derivation-path@2x.png b/resources/images/icons2/32x32/derivation-path@2x.png new file mode 100644 index 00000000000..e859586968c Binary files /dev/null and b/resources/images/icons2/32x32/derivation-path@2x.png differ diff --git a/resources/images/icons2/32x32/derivation-path@3x.png b/resources/images/icons2/32x32/derivation-path@3x.png new file mode 100644 index 00000000000..81b1f0fd6d7 Binary files /dev/null and b/resources/images/icons2/32x32/derivation-path@3x.png differ diff --git a/resources/images/icons2/32x32/identicon-ring@2x.png b/resources/images/icons2/32x32/identicon-ring@2x.png new file mode 100644 index 00000000000..5142b86d37f Binary files /dev/null and b/resources/images/icons2/32x32/identicon-ring@2x.png differ diff --git a/resources/images/icons2/32x32/identicon-ring@3x.png b/resources/images/icons2/32x32/identicon-ring@3x.png new file mode 100644 index 00000000000..3da71b20f33 Binary files /dev/null and b/resources/images/icons2/32x32/identicon-ring@3x.png differ diff --git a/resources/images/icons2/32x32/members@2x.png b/resources/images/icons2/32x32/members@2x.png new file mode 100644 index 00000000000..3d045706926 Binary files /dev/null and b/resources/images/icons2/32x32/members@2x.png differ diff --git a/resources/images/icons2/32x32/members@3x.png b/resources/images/icons2/32x32/members@3x.png new file mode 100644 index 00000000000..6acf361937e Binary files /dev/null and b/resources/images/icons2/32x32/members@3x.png differ diff --git a/resources/images/icons2/48x48/rotate-camera@2x.png b/resources/images/icons2/48x48/rotate-camera@2x.png new file mode 100644 index 00000000000..6c2b4ee0f4e Binary files /dev/null and b/resources/images/icons2/48x48/rotate-camera@2x.png differ diff --git a/resources/images/icons2/48x48/rotate-camera@3x.png b/resources/images/icons2/48x48/rotate-camera@3x.png new file mode 100644 index 00000000000..0916067d477 Binary files /dev/null and b/resources/images/icons2/48x48/rotate-camera@3x.png differ diff --git a/resources/images/mock2/bored-ape.png b/resources/images/mock2/bored-ape.png new file mode 100644 index 00000000000..a87d1d8ec31 Binary files /dev/null and b/resources/images/mock2/bored-ape.png differ diff --git a/resources/images/mock2/coinbase@2x.png b/resources/images/mock2/coinbase@2x.png new file mode 100644 index 00000000000..b3bc187f15a Binary files /dev/null and b/resources/images/mock2/coinbase@2x.png differ diff --git a/resources/images/mock2/collectible-monkey.png b/resources/images/mock2/collectible-monkey.png new file mode 100644 index 00000000000..d268d50a468 Binary files /dev/null and b/resources/images/mock2/collectible-monkey.png differ diff --git a/resources/images/mock2/collectible.png b/resources/images/mock2/collectible.png new file mode 100644 index 00000000000..c22b21c6b25 Binary files /dev/null and b/resources/images/mock2/collectible.png differ diff --git a/resources/images/mock2/collectible1.png b/resources/images/mock2/collectible1.png new file mode 100644 index 00000000000..e93214e3aca Binary files /dev/null and b/resources/images/mock2/collectible1.png differ diff --git a/resources/images/mock2/collectible2.png b/resources/images/mock2/collectible2.png new file mode 100644 index 00000000000..b7b78109e56 Binary files /dev/null and b/resources/images/mock2/collectible2.png differ diff --git a/resources/images/mock2/collectible3.png b/resources/images/mock2/collectible3.png new file mode 100644 index 00000000000..f2afd8b2626 Binary files /dev/null and b/resources/images/mock2/collectible3.png differ diff --git a/resources/images/mock2/collectible4.png b/resources/images/mock2/collectible4.png new file mode 100644 index 00000000000..5c7f274c135 Binary files /dev/null and b/resources/images/mock2/collectible4.png differ diff --git a/resources/images/mock2/collectible5.png b/resources/images/mock2/collectible5.png new file mode 100644 index 00000000000..129ac2b1256 Binary files /dev/null and b/resources/images/mock2/collectible5.png differ diff --git a/resources/images/mock2/collectible6.png b/resources/images/mock2/collectible6.png new file mode 100644 index 00000000000..1aa7041031a Binary files /dev/null and b/resources/images/mock2/collectible6.png differ diff --git a/resources/images/mock2/community-banner@2x.png b/resources/images/mock2/community-banner@2x.png new file mode 100644 index 00000000000..f7a88273a24 Binary files /dev/null and b/resources/images/mock2/community-banner@2x.png differ diff --git a/resources/images/mock2/community-cover.png b/resources/images/mock2/community-cover.png new file mode 100644 index 00000000000..6149370a29c Binary files /dev/null and b/resources/images/mock2/community-cover.png differ diff --git a/resources/images/mock2/community-logo@2x.png b/resources/images/mock2/community-logo@2x.png new file mode 100644 index 00000000000..a2390c76753 Binary files /dev/null and b/resources/images/mock2/community-logo@2x.png differ diff --git a/resources/images/mock2/contact.png b/resources/images/mock2/contact.png new file mode 100644 index 00000000000..48a3d672c58 Binary files /dev/null and b/resources/images/mock2/contact.png differ diff --git a/resources/images/mock2/dark-blur-background@2x.png b/resources/images/mock2/dark-blur-background@2x.png new file mode 100644 index 00000000000..95facd8be5f Binary files /dev/null and b/resources/images/mock2/dark-blur-background@2x.png differ diff --git a/resources/images/mock2/dark-blur-background@3x.png b/resources/images/mock2/dark-blur-background@3x.png new file mode 100644 index 00000000000..48d3dab4dc0 Binary files /dev/null and b/resources/images/mock2/dark-blur-background@3x.png differ diff --git a/resources/images/mock2/dark_blur_bg.png b/resources/images/mock2/dark_blur_bg.png new file mode 100644 index 00000000000..49aae684874 Binary files /dev/null and b/resources/images/mock2/dark_blur_bg.png differ diff --git a/resources/images/mock2/decentraland.png b/resources/images/mock2/decentraland.png new file mode 100644 index 00000000000..95eca4f04d5 Binary files /dev/null and b/resources/images/mock2/decentraland.png differ diff --git a/resources/images/mock2/decentraland@2x.png b/resources/images/mock2/decentraland@2x.png new file mode 100644 index 00000000000..781260a22c7 Binary files /dev/null and b/resources/images/mock2/decentraland@2x.png differ diff --git a/resources/images/mock2/decentraland@3x.png b/resources/images/mock2/decentraland@3x.png new file mode 100644 index 00000000000..da6be7ad9c0 Binary files /dev/null and b/resources/images/mock2/decentraland@3x.png differ diff --git a/resources/images/mock2/diamond.png b/resources/images/mock2/diamond.png new file mode 100644 index 00000000000..769597bdf0a Binary files /dev/null and b/resources/images/mock2/diamond.png differ diff --git a/resources/images/mock2/gif@2x.png b/resources/images/mock2/gif@2x.png new file mode 100644 index 00000000000..cab7068bda9 Binary files /dev/null and b/resources/images/mock2/gif@2x.png differ diff --git a/resources/images/mock2/light-blur-background@2x.png b/resources/images/mock2/light-blur-background@2x.png new file mode 100644 index 00000000000..902c8517c25 Binary files /dev/null and b/resources/images/mock2/light-blur-background@2x.png differ diff --git a/resources/images/mock2/light-blur-background@3x.png b/resources/images/mock2/light-blur-background@3x.png new file mode 100644 index 00000000000..a70f61485d2 Binary files /dev/null and b/resources/images/mock2/light-blur-background@3x.png differ diff --git a/resources/images/mock2/monkey.png b/resources/images/mock2/monkey.png new file mode 100644 index 00000000000..d289db96e7e Binary files /dev/null and b/resources/images/mock2/monkey.png differ diff --git a/resources/images/mock2/photo1@2x.png b/resources/images/mock2/photo1@2x.png new file mode 100644 index 00000000000..1f1cb155ecd Binary files /dev/null and b/resources/images/mock2/photo1@2x.png differ diff --git a/resources/images/mock2/photo2@2x.png b/resources/images/mock2/photo2@2x.png new file mode 100644 index 00000000000..af0125043ba Binary files /dev/null and b/resources/images/mock2/photo2@2x.png differ diff --git a/resources/images/mock2/photo3@2x.png b/resources/images/mock2/photo3@2x.png new file mode 100644 index 00000000000..e56a0d6695e Binary files /dev/null and b/resources/images/mock2/photo3@2x.png differ diff --git a/resources/images/mock2/pinterest.png b/resources/images/mock2/pinterest.png new file mode 100644 index 00000000000..71de92646b7 Binary files /dev/null and b/resources/images/mock2/pinterest.png differ diff --git a/resources/images/mock2/qr-code@2x.png b/resources/images/mock2/qr-code@2x.png new file mode 100644 index 00000000000..1beadac3bb7 Binary files /dev/null and b/resources/images/mock2/qr-code@2x.png differ diff --git a/resources/images/mock2/qr-code@3x.png b/resources/images/mock2/qr-code@3x.png new file mode 100644 index 00000000000..17a9f29a5e3 Binary files /dev/null and b/resources/images/mock2/qr-code@3x.png differ diff --git a/resources/images/mock2/rarible.png b/resources/images/mock2/rarible.png new file mode 100644 index 00000000000..9033a711adb Binary files /dev/null and b/resources/images/mock2/rarible.png differ diff --git a/resources/images/mock2/rarible@2x.png b/resources/images/mock2/rarible@2x.png new file mode 100644 index 00000000000..2eb2899ca12 Binary files /dev/null and b/resources/images/mock2/rarible@2x.png differ diff --git a/resources/images/mock2/rarible@3x.png b/resources/images/mock2/rarible@3x.png new file mode 100644 index 00000000000..b25d31b04da Binary files /dev/null and b/resources/images/mock2/rarible@3x.png differ diff --git a/resources/images/mock2/ring.png b/resources/images/mock2/ring.png new file mode 100644 index 00000000000..792a2f87826 Binary files /dev/null and b/resources/images/mock2/ring.png differ diff --git a/resources/images/mock2/small_opt_card_icon.png b/resources/images/mock2/small_opt_card_icon.png new file mode 100644 index 00000000000..49c5fd2f455 Binary files /dev/null and b/resources/images/mock2/small_opt_card_icon.png differ diff --git a/resources/images/mock2/small_opt_card_main.png b/resources/images/mock2/small_opt_card_main.png new file mode 100644 index 00000000000..803ec43f7f3 Binary files /dev/null and b/resources/images/mock2/small_opt_card_main.png differ diff --git a/resources/images/mock2/status-logo.png b/resources/images/mock2/status-logo.png new file mode 100644 index 00000000000..e3cf13ecafe Binary files /dev/null and b/resources/images/mock2/status-logo.png differ diff --git a/resources/images/mock2/sticker@2x.png b/resources/images/mock2/sticker@2x.png new file mode 100644 index 00000000000..5c2abf47387 Binary files /dev/null and b/resources/images/mock2/sticker@2x.png differ diff --git a/resources/images/mock2/user_picture_female2@2x.png b/resources/images/mock2/user_picture_female2@2x.png new file mode 100644 index 00000000000..8c92662bc68 Binary files /dev/null and b/resources/images/mock2/user_picture_female2@2x.png differ diff --git a/resources/images/mock2/user_picture_male4@2x.png b/resources/images/mock2/user_picture_male4@2x.png new file mode 100644 index 00000000000..6dd80d64f42 Binary files /dev/null and b/resources/images/mock2/user_picture_male4@2x.png differ diff --git a/resources/images/mock2/user_picture_male5@2x.png b/resources/images/mock2/user_picture_male5@2x.png new file mode 100644 index 00000000000..2eff8701404 Binary files /dev/null and b/resources/images/mock2/user_picture_male5@2x.png differ diff --git a/resources/images/mock2/verified.png b/resources/images/mock2/verified.png new file mode 100644 index 00000000000..8eb9f0ac72e Binary files /dev/null and b/resources/images/mock2/verified.png differ diff --git a/resources/images/networks/Arbitrum@2x.png b/resources/images/networks/Arbitrum@2x.png new file mode 100644 index 00000000000..e6bbaadfd86 Binary files /dev/null and b/resources/images/networks/Arbitrum@2x.png differ diff --git a/resources/images/networks/Arbitrum@3x.png b/resources/images/networks/Arbitrum@3x.png new file mode 100644 index 00000000000..a255d7bb1f4 Binary files /dev/null and b/resources/images/networks/Arbitrum@3x.png differ diff --git a/resources/images/networks/Base@2x.png b/resources/images/networks/Base@2x.png new file mode 100644 index 00000000000..99188686218 Binary files /dev/null and b/resources/images/networks/Base@2x.png differ diff --git a/resources/images/networks/Base@3x.png b/resources/images/networks/Base@3x.png new file mode 100644 index 00000000000..cb064a92a88 Binary files /dev/null and b/resources/images/networks/Base@3x.png differ diff --git a/resources/images/networks/Binance@2x.png b/resources/images/networks/Binance@2x.png new file mode 100644 index 00000000000..c2b8e70e187 Binary files /dev/null and b/resources/images/networks/Binance@2x.png differ diff --git a/resources/images/networks/Binance@3x.png b/resources/images/networks/Binance@3x.png new file mode 100644 index 00000000000..d6279d44204 Binary files /dev/null and b/resources/images/networks/Binance@3x.png differ diff --git a/resources/images/networks/Ethereum@2x.png b/resources/images/networks/Ethereum@2x.png new file mode 100644 index 00000000000..dee99ad3b7e Binary files /dev/null and b/resources/images/networks/Ethereum@2x.png differ diff --git a/resources/images/networks/Ethereum@3x.png b/resources/images/networks/Ethereum@3x.png new file mode 100644 index 00000000000..8c5703dd3dd Binary files /dev/null and b/resources/images/networks/Ethereum@3x.png differ diff --git a/resources/images/networks/Gnosis@2x.png b/resources/images/networks/Gnosis@2x.png new file mode 100644 index 00000000000..31d206ccfd9 Binary files /dev/null and b/resources/images/networks/Gnosis@2x.png differ diff --git a/resources/images/networks/Gnosis@3x.png b/resources/images/networks/Gnosis@3x.png new file mode 100644 index 00000000000..a775fdbf332 Binary files /dev/null and b/resources/images/networks/Gnosis@3x.png differ diff --git a/resources/images/networks/Hermez@2x.png b/resources/images/networks/Hermez@2x.png new file mode 100644 index 00000000000..ed578c30c3e Binary files /dev/null and b/resources/images/networks/Hermez@2x.png differ diff --git a/resources/images/networks/Hermez@3x.png b/resources/images/networks/Hermez@3x.png new file mode 100644 index 00000000000..89da4b3449d Binary files /dev/null and b/resources/images/networks/Hermez@3x.png differ diff --git a/resources/images/networks/Optimism@2x.png b/resources/images/networks/Optimism@2x.png new file mode 100644 index 00000000000..f21a9685a43 Binary files /dev/null and b/resources/images/networks/Optimism@2x.png differ diff --git a/resources/images/networks/Optimism@3x.png b/resources/images/networks/Optimism@3x.png new file mode 100644 index 00000000000..ff2ae2d8202 Binary files /dev/null and b/resources/images/networks/Optimism@3x.png differ diff --git a/resources/images/networks/Paraswap@2x.png b/resources/images/networks/Paraswap@2x.png new file mode 100644 index 00000000000..d37384748a9 Binary files /dev/null and b/resources/images/networks/Paraswap@2x.png differ diff --git a/resources/images/networks/Paraswap@3x.png b/resources/images/networks/Paraswap@3x.png new file mode 100644 index 00000000000..1d54cd7430d Binary files /dev/null and b/resources/images/networks/Paraswap@3x.png differ diff --git a/resources/images/networks/Polygon@2x.png b/resources/images/networks/Polygon@2x.png new file mode 100644 index 00000000000..6f281ecd793 Binary files /dev/null and b/resources/images/networks/Polygon@2x.png differ diff --git a/resources/images/networks/Polygon@3x.png b/resources/images/networks/Polygon@3x.png new file mode 100644 index 00000000000..d204be764d5 Binary files /dev/null and b/resources/images/networks/Polygon@3x.png differ diff --git a/resources/images/networks/Scroll@2x.png b/resources/images/networks/Scroll@2x.png new file mode 100644 index 00000000000..8a94d502793 Binary files /dev/null and b/resources/images/networks/Scroll@2x.png differ diff --git a/resources/images/networks/Scroll@3x.png b/resources/images/networks/Scroll@3x.png new file mode 100644 index 00000000000..7650a1ba18d Binary files /dev/null and b/resources/images/networks/Scroll@3x.png differ diff --git a/resources/images/networks/Status@2x.png b/resources/images/networks/Status@2x.png new file mode 100644 index 00000000000..fe8affb8ee4 Binary files /dev/null and b/resources/images/networks/Status@2x.png differ diff --git a/resources/images/networks/Status@3x.png b/resources/images/networks/Status@3x.png new file mode 100644 index 00000000000..83d540e0a82 Binary files /dev/null and b/resources/images/networks/Status@3x.png differ diff --git a/resources/images/networks/Taiko@2x.png b/resources/images/networks/Taiko@2x.png new file mode 100644 index 00000000000..c2d69ece9bf Binary files /dev/null and b/resources/images/networks/Taiko@2x.png differ diff --git a/resources/images/networks/Taiko@3x.png b/resources/images/networks/Taiko@3x.png new file mode 100644 index 00000000000..e84f5e7879c Binary files /dev/null and b/resources/images/networks/Taiko@3x.png differ diff --git a/resources/images/networks/Unknown@2x.png b/resources/images/networks/Unknown@2x.png new file mode 100644 index 00000000000..9363a7a4880 Binary files /dev/null and b/resources/images/networks/Unknown@2x.png differ diff --git a/resources/images/networks/Unknown@3x.png b/resources/images/networks/Unknown@3x.png new file mode 100644 index 00000000000..ab7e92b1f29 Binary files /dev/null and b/resources/images/networks/Unknown@3x.png differ diff --git a/resources/images/networks/xDAI@2x.png b/resources/images/networks/xDAI@2x.png new file mode 100644 index 00000000000..f897c80bf84 Binary files /dev/null and b/resources/images/networks/xDAI@2x.png differ diff --git a/resources/images/networks/xDAI@3x.png b/resources/images/networks/xDAI@3x.png new file mode 100644 index 00000000000..22fd3f372ab Binary files /dev/null and b/resources/images/networks/xDAI@3x.png differ diff --git a/resources/images/networks/zkSync@2x.png b/resources/images/networks/zkSync@2x.png new file mode 100644 index 00000000000..314c6e78378 Binary files /dev/null and b/resources/images/networks/zkSync@2x.png differ diff --git a/resources/images/networks/zkSync@3x.png b/resources/images/networks/zkSync@3x.png new file mode 100644 index 00000000000..5a0f1ef4b10 Binary files /dev/null and b/resources/images/networks/zkSync@3x.png differ diff --git a/resources/images/reactions/angry.png b/resources/images/reactions/angry.png new file mode 100644 index 00000000000..6759feca83d Binary files /dev/null and b/resources/images/reactions/angry.png differ diff --git a/resources/images/reactions/angry@2x.png b/resources/images/reactions/angry@2x.png new file mode 100644 index 00000000000..5c408b17236 Binary files /dev/null and b/resources/images/reactions/angry@2x.png differ diff --git a/resources/images/reactions/angry@3x.png b/resources/images/reactions/angry@3x.png new file mode 100644 index 00000000000..487e5961d38 Binary files /dev/null and b/resources/images/reactions/angry@3x.png differ diff --git a/resources/images/reactions/laugh.png b/resources/images/reactions/laugh.png new file mode 100644 index 00000000000..4b5e9a66019 Binary files /dev/null and b/resources/images/reactions/laugh.png differ diff --git a/resources/images/reactions/laugh@2x.png b/resources/images/reactions/laugh@2x.png new file mode 100644 index 00000000000..21387d6fa6d Binary files /dev/null and b/resources/images/reactions/laugh@2x.png differ diff --git a/resources/images/reactions/laugh@3x.png b/resources/images/reactions/laugh@3x.png new file mode 100644 index 00000000000..01a4ea199c8 Binary files /dev/null and b/resources/images/reactions/laugh@3x.png differ diff --git a/resources/images/reactions/love.png b/resources/images/reactions/love.png new file mode 100644 index 00000000000..6dc171e58b8 Binary files /dev/null and b/resources/images/reactions/love.png differ diff --git a/resources/images/reactions/love@2x.png b/resources/images/reactions/love@2x.png new file mode 100644 index 00000000000..afaae3c84b8 Binary files /dev/null and b/resources/images/reactions/love@2x.png differ diff --git a/resources/images/reactions/love@3x.png b/resources/images/reactions/love@3x.png new file mode 100644 index 00000000000..22d033b076b Binary files /dev/null and b/resources/images/reactions/love@3x.png differ diff --git a/resources/images/reactions/sad.png b/resources/images/reactions/sad.png new file mode 100644 index 00000000000..00fc5648852 Binary files /dev/null and b/resources/images/reactions/sad.png differ diff --git a/resources/images/reactions/sad@2x.png b/resources/images/reactions/sad@2x.png new file mode 100644 index 00000000000..db786e5142d Binary files /dev/null and b/resources/images/reactions/sad@2x.png differ diff --git a/resources/images/reactions/sad@3x.png b/resources/images/reactions/sad@3x.png new file mode 100644 index 00000000000..ae9caebd22f Binary files /dev/null and b/resources/images/reactions/sad@3x.png differ diff --git a/resources/images/reactions/thumbs-down.png b/resources/images/reactions/thumbs-down.png new file mode 100644 index 00000000000..15a96e735c8 Binary files /dev/null and b/resources/images/reactions/thumbs-down.png differ diff --git a/resources/images/reactions/thumbs-down@2x.png b/resources/images/reactions/thumbs-down@2x.png new file mode 100644 index 00000000000..38ee1d258f1 Binary files /dev/null and b/resources/images/reactions/thumbs-down@2x.png differ diff --git a/resources/images/reactions/thumbs-down@3x.png b/resources/images/reactions/thumbs-down@3x.png new file mode 100644 index 00000000000..54cacd339af Binary files /dev/null and b/resources/images/reactions/thumbs-down@3x.png differ diff --git a/resources/images/reactions/thumbs-up.png b/resources/images/reactions/thumbs-up.png new file mode 100644 index 00000000000..58523f40f22 Binary files /dev/null and b/resources/images/reactions/thumbs-up.png differ diff --git a/resources/images/reactions/thumbs-up@2x.png b/resources/images/reactions/thumbs-up@2x.png new file mode 100644 index 00000000000..f2b26b7e719 Binary files /dev/null and b/resources/images/reactions/thumbs-up@2x.png differ diff --git a/resources/images/reactions/thumbs-up@3x.png b/resources/images/reactions/thumbs-up@3x.png new file mode 100644 index 00000000000..e39f21d923f Binary files /dev/null and b/resources/images/reactions/thumbs-up@3x.png differ diff --git a/resources/images/socials/bigger/default/facebook@2x.png b/resources/images/socials/bigger/default/facebook@2x.png new file mode 100644 index 00000000000..63c08203e17 Binary files /dev/null and b/resources/images/socials/bigger/default/facebook@2x.png differ diff --git a/resources/images/socials/bigger/default/facebook@3x.png b/resources/images/socials/bigger/default/facebook@3x.png new file mode 100644 index 00000000000..3c0ed7ba630 Binary files /dev/null and b/resources/images/socials/bigger/default/facebook@3x.png differ diff --git a/resources/images/socials/bigger/default/flickr@2x.png b/resources/images/socials/bigger/default/flickr@2x.png new file mode 100644 index 00000000000..7d25e1cac6a Binary files /dev/null and b/resources/images/socials/bigger/default/flickr@2x.png differ diff --git a/resources/images/socials/bigger/default/flickr@3x.png b/resources/images/socials/bigger/default/flickr@3x.png new file mode 100644 index 00000000000..afa585c1901 Binary files /dev/null and b/resources/images/socials/bigger/default/flickr@3x.png differ diff --git a/resources/images/socials/bigger/default/github@2x.png b/resources/images/socials/bigger/default/github@2x.png new file mode 100644 index 00000000000..092d2dded8a Binary files /dev/null and b/resources/images/socials/bigger/default/github@2x.png differ diff --git a/resources/images/socials/bigger/default/github@3x.png b/resources/images/socials/bigger/default/github@3x.png new file mode 100644 index 00000000000..a0d5bb4e7dc Binary files /dev/null and b/resources/images/socials/bigger/default/github@3x.png differ diff --git a/resources/images/socials/bigger/default/instagram@2x.png b/resources/images/socials/bigger/default/instagram@2x.png new file mode 100644 index 00000000000..5ea58310c3f Binary files /dev/null and b/resources/images/socials/bigger/default/instagram@2x.png differ diff --git a/resources/images/socials/bigger/default/instagram@3x.png b/resources/images/socials/bigger/default/instagram@3x.png new file mode 100644 index 00000000000..ef7a1b88c80 Binary files /dev/null and b/resources/images/socials/bigger/default/instagram@3x.png differ diff --git a/resources/images/socials/bigger/default/lens@2x.png b/resources/images/socials/bigger/default/lens@2x.png new file mode 100644 index 00000000000..032680e771a Binary files /dev/null and b/resources/images/socials/bigger/default/lens@2x.png differ diff --git a/resources/images/socials/bigger/default/lens@3x.png b/resources/images/socials/bigger/default/lens@3x.png new file mode 100644 index 00000000000..e1bbb47098d Binary files /dev/null and b/resources/images/socials/bigger/default/lens@3x.png differ diff --git a/resources/images/socials/bigger/default/link@2x.png b/resources/images/socials/bigger/default/link@2x.png new file mode 100644 index 00000000000..35044765d30 Binary files /dev/null and b/resources/images/socials/bigger/default/link@2x.png differ diff --git a/resources/images/socials/bigger/default/link@3x.png b/resources/images/socials/bigger/default/link@3x.png new file mode 100644 index 00000000000..de7fa9ca83a Binary files /dev/null and b/resources/images/socials/bigger/default/link@3x.png differ diff --git a/resources/images/socials/bigger/default/linkedin@2x.png b/resources/images/socials/bigger/default/linkedin@2x.png new file mode 100644 index 00000000000..42de7dfbc2c Binary files /dev/null and b/resources/images/socials/bigger/default/linkedin@2x.png differ diff --git a/resources/images/socials/bigger/default/linkedin@3x.png b/resources/images/socials/bigger/default/linkedin@3x.png new file mode 100644 index 00000000000..7e018693e92 Binary files /dev/null and b/resources/images/socials/bigger/default/linkedin@3x.png differ diff --git a/resources/images/socials/bigger/default/mirror@2x.png b/resources/images/socials/bigger/default/mirror@2x.png new file mode 100644 index 00000000000..aa13d697c86 Binary files /dev/null and b/resources/images/socials/bigger/default/mirror@2x.png differ diff --git a/resources/images/socials/bigger/default/mirror@3x.png b/resources/images/socials/bigger/default/mirror@3x.png new file mode 100644 index 00000000000..009afdb03b4 Binary files /dev/null and b/resources/images/socials/bigger/default/mirror@3x.png differ diff --git a/resources/images/socials/bigger/default/opensea@2x.png b/resources/images/socials/bigger/default/opensea@2x.png new file mode 100644 index 00000000000..36826f3b134 Binary files /dev/null and b/resources/images/socials/bigger/default/opensea@2x.png differ diff --git a/resources/images/socials/bigger/default/opensea@3x.png b/resources/images/socials/bigger/default/opensea@3x.png new file mode 100644 index 00000000000..771a71fa291 Binary files /dev/null and b/resources/images/socials/bigger/default/opensea@3x.png differ diff --git a/resources/images/socials/bigger/default/pinterest@2x.png b/resources/images/socials/bigger/default/pinterest@2x.png new file mode 100644 index 00000000000..fa7edc2884c Binary files /dev/null and b/resources/images/socials/bigger/default/pinterest@2x.png differ diff --git a/resources/images/socials/bigger/default/pinterest@3x.png b/resources/images/socials/bigger/default/pinterest@3x.png new file mode 100644 index 00000000000..6a94dcfc634 Binary files /dev/null and b/resources/images/socials/bigger/default/pinterest@3x.png differ diff --git a/resources/images/socials/bigger/default/rarible@2x.png b/resources/images/socials/bigger/default/rarible@2x.png new file mode 100644 index 00000000000..3bc0c792e96 Binary files /dev/null and b/resources/images/socials/bigger/default/rarible@2x.png differ diff --git a/resources/images/socials/bigger/default/rarible@3x.png b/resources/images/socials/bigger/default/rarible@3x.png new file mode 100644 index 00000000000..c3ba980d62e Binary files /dev/null and b/resources/images/socials/bigger/default/rarible@3x.png differ diff --git a/resources/images/socials/bigger/default/reddit@2x.png b/resources/images/socials/bigger/default/reddit@2x.png new file mode 100644 index 00000000000..56e41c47a85 Binary files /dev/null and b/resources/images/socials/bigger/default/reddit@2x.png differ diff --git a/resources/images/socials/bigger/default/reddit@3x.png b/resources/images/socials/bigger/default/reddit@3x.png new file mode 100644 index 00000000000..ec546fec441 Binary files /dev/null and b/resources/images/socials/bigger/default/reddit@3x.png differ diff --git a/resources/images/socials/bigger/default/snapchat@2x.png b/resources/images/socials/bigger/default/snapchat@2x.png new file mode 100644 index 00000000000..b5b68504187 Binary files /dev/null and b/resources/images/socials/bigger/default/snapchat@2x.png differ diff --git a/resources/images/socials/bigger/default/snapchat@3x.png b/resources/images/socials/bigger/default/snapchat@3x.png new file mode 100644 index 00000000000..22d1c850ea7 Binary files /dev/null and b/resources/images/socials/bigger/default/snapchat@3x.png differ diff --git a/resources/images/socials/bigger/default/spotify@2x.png b/resources/images/socials/bigger/default/spotify@2x.png new file mode 100644 index 00000000000..2ff12581a56 Binary files /dev/null and b/resources/images/socials/bigger/default/spotify@2x.png differ diff --git a/resources/images/socials/bigger/default/spotify@3x.png b/resources/images/socials/bigger/default/spotify@3x.png new file mode 100644 index 00000000000..3ddf82b51fa Binary files /dev/null and b/resources/images/socials/bigger/default/spotify@3x.png differ diff --git a/resources/images/socials/bigger/default/status@2x.png b/resources/images/socials/bigger/default/status@2x.png new file mode 100644 index 00000000000..79209f850a5 Binary files /dev/null and b/resources/images/socials/bigger/default/status@2x.png differ diff --git a/resources/images/socials/bigger/default/status@3x.png b/resources/images/socials/bigger/default/status@3x.png new file mode 100644 index 00000000000..48af0506ed2 Binary files /dev/null and b/resources/images/socials/bigger/default/status@3x.png differ diff --git a/resources/images/socials/bigger/default/superrare@2x.png b/resources/images/socials/bigger/default/superrare@2x.png new file mode 100644 index 00000000000..e213f84b021 Binary files /dev/null and b/resources/images/socials/bigger/default/superrare@2x.png differ diff --git a/resources/images/socials/bigger/default/superrare@3x.png b/resources/images/socials/bigger/default/superrare@3x.png new file mode 100644 index 00000000000..2b5060da4c9 Binary files /dev/null and b/resources/images/socials/bigger/default/superrare@3x.png differ diff --git a/resources/images/socials/bigger/default/tumblr@2x.png b/resources/images/socials/bigger/default/tumblr@2x.png new file mode 100644 index 00000000000..4036a4fb096 Binary files /dev/null and b/resources/images/socials/bigger/default/tumblr@2x.png differ diff --git a/resources/images/socials/bigger/default/tumblr@3x.png b/resources/images/socials/bigger/default/tumblr@3x.png new file mode 100644 index 00000000000..cde2fd0d272 Binary files /dev/null and b/resources/images/socials/bigger/default/tumblr@3x.png differ diff --git a/resources/images/socials/bigger/default/twitch@2x.png b/resources/images/socials/bigger/default/twitch@2x.png new file mode 100644 index 00000000000..8a95f707ebf Binary files /dev/null and b/resources/images/socials/bigger/default/twitch@2x.png differ diff --git a/resources/images/socials/bigger/default/twitch@3x.png b/resources/images/socials/bigger/default/twitch@3x.png new file mode 100644 index 00000000000..9859f1b5c85 Binary files /dev/null and b/resources/images/socials/bigger/default/twitch@3x.png differ diff --git a/resources/images/socials/bigger/default/twitter@2x.png b/resources/images/socials/bigger/default/twitter@2x.png new file mode 100644 index 00000000000..c7430b3f054 Binary files /dev/null and b/resources/images/socials/bigger/default/twitter@2x.png differ diff --git a/resources/images/socials/bigger/default/twitter@3x.png b/resources/images/socials/bigger/default/twitter@3x.png new file mode 100644 index 00000000000..40a7d02bde5 Binary files /dev/null and b/resources/images/socials/bigger/default/twitter@3x.png differ diff --git a/resources/images/socials/bigger/default/youtube@2x.png b/resources/images/socials/bigger/default/youtube@2x.png new file mode 100644 index 00000000000..c67715028f9 Binary files /dev/null and b/resources/images/socials/bigger/default/youtube@2x.png differ diff --git a/resources/images/socials/bigger/default/youtube@3x.png b/resources/images/socials/bigger/default/youtube@3x.png new file mode 100644 index 00000000000..1ee65bdecae Binary files /dev/null and b/resources/images/socials/bigger/default/youtube@3x.png differ diff --git a/resources/images/socials/bigger/solid/facebook@2x.png b/resources/images/socials/bigger/solid/facebook@2x.png new file mode 100644 index 00000000000..7e47049538d Binary files /dev/null and b/resources/images/socials/bigger/solid/facebook@2x.png differ diff --git a/resources/images/socials/bigger/solid/facebook@3x.png b/resources/images/socials/bigger/solid/facebook@3x.png new file mode 100644 index 00000000000..ba088e1d930 Binary files /dev/null and b/resources/images/socials/bigger/solid/facebook@3x.png differ diff --git a/resources/images/socials/bigger/solid/flickr@2x.png b/resources/images/socials/bigger/solid/flickr@2x.png new file mode 100644 index 00000000000..dc8c533b338 Binary files /dev/null and b/resources/images/socials/bigger/solid/flickr@2x.png differ diff --git a/resources/images/socials/bigger/solid/flickr@3x.png b/resources/images/socials/bigger/solid/flickr@3x.png new file mode 100644 index 00000000000..2eaf40002c5 Binary files /dev/null and b/resources/images/socials/bigger/solid/flickr@3x.png differ diff --git a/resources/images/socials/bigger/solid/github@2x.png b/resources/images/socials/bigger/solid/github@2x.png new file mode 100644 index 00000000000..092d2dded8a Binary files /dev/null and b/resources/images/socials/bigger/solid/github@2x.png differ diff --git a/resources/images/socials/bigger/solid/github@3x.png b/resources/images/socials/bigger/solid/github@3x.png new file mode 100644 index 00000000000..a0d5bb4e7dc Binary files /dev/null and b/resources/images/socials/bigger/solid/github@3x.png differ diff --git a/resources/images/socials/bigger/solid/instagram@2x.png b/resources/images/socials/bigger/solid/instagram@2x.png new file mode 100644 index 00000000000..4ace4bac770 Binary files /dev/null and b/resources/images/socials/bigger/solid/instagram@2x.png differ diff --git a/resources/images/socials/bigger/solid/instagram@3x.png b/resources/images/socials/bigger/solid/instagram@3x.png new file mode 100644 index 00000000000..e896e1d2ae9 Binary files /dev/null and b/resources/images/socials/bigger/solid/instagram@3x.png differ diff --git a/resources/images/socials/bigger/solid/lens@2x.png b/resources/images/socials/bigger/solid/lens@2x.png new file mode 100644 index 00000000000..977f119b9ed Binary files /dev/null and b/resources/images/socials/bigger/solid/lens@2x.png differ diff --git a/resources/images/socials/bigger/solid/lens@3x.png b/resources/images/socials/bigger/solid/lens@3x.png new file mode 100644 index 00000000000..bbc9b840828 Binary files /dev/null and b/resources/images/socials/bigger/solid/lens@3x.png differ diff --git a/resources/images/socials/bigger/solid/link@2x.png b/resources/images/socials/bigger/solid/link@2x.png new file mode 100644 index 00000000000..35044765d30 Binary files /dev/null and b/resources/images/socials/bigger/solid/link@2x.png differ diff --git a/resources/images/socials/bigger/solid/link@3x.png b/resources/images/socials/bigger/solid/link@3x.png new file mode 100644 index 00000000000..de7fa9ca83a Binary files /dev/null and b/resources/images/socials/bigger/solid/link@3x.png differ diff --git a/resources/images/socials/bigger/solid/linkedin@2x.png b/resources/images/socials/bigger/solid/linkedin@2x.png new file mode 100644 index 00000000000..3a5ff804ebf Binary files /dev/null and b/resources/images/socials/bigger/solid/linkedin@2x.png differ diff --git a/resources/images/socials/bigger/solid/linkedin@3x.png b/resources/images/socials/bigger/solid/linkedin@3x.png new file mode 100644 index 00000000000..50c7ca1f5e0 Binary files /dev/null and b/resources/images/socials/bigger/solid/linkedin@3x.png differ diff --git a/resources/images/socials/bigger/solid/mirror@2x.png b/resources/images/socials/bigger/solid/mirror@2x.png new file mode 100644 index 00000000000..c5e13d0b830 Binary files /dev/null and b/resources/images/socials/bigger/solid/mirror@2x.png differ diff --git a/resources/images/socials/bigger/solid/mirror@3x.png b/resources/images/socials/bigger/solid/mirror@3x.png new file mode 100644 index 00000000000..d22b6656005 Binary files /dev/null and b/resources/images/socials/bigger/solid/mirror@3x.png differ diff --git a/resources/images/socials/bigger/solid/opensea@2x.png b/resources/images/socials/bigger/solid/opensea@2x.png new file mode 100644 index 00000000000..13d4c92daf9 Binary files /dev/null and b/resources/images/socials/bigger/solid/opensea@2x.png differ diff --git a/resources/images/socials/bigger/solid/opensea@3x.png b/resources/images/socials/bigger/solid/opensea@3x.png new file mode 100644 index 00000000000..1d578ecf62b Binary files /dev/null and b/resources/images/socials/bigger/solid/opensea@3x.png differ diff --git a/resources/images/socials/bigger/solid/pinterest@2x.png b/resources/images/socials/bigger/solid/pinterest@2x.png new file mode 100644 index 00000000000..0675da7e102 Binary files /dev/null and b/resources/images/socials/bigger/solid/pinterest@2x.png differ diff --git a/resources/images/socials/bigger/solid/pinterest@3x.png b/resources/images/socials/bigger/solid/pinterest@3x.png new file mode 100644 index 00000000000..cb9816b2b7a Binary files /dev/null and b/resources/images/socials/bigger/solid/pinterest@3x.png differ diff --git a/resources/images/socials/bigger/solid/rarible@2x.png b/resources/images/socials/bigger/solid/rarible@2x.png new file mode 100644 index 00000000000..8a4a781adb9 Binary files /dev/null and b/resources/images/socials/bigger/solid/rarible@2x.png differ diff --git a/resources/images/socials/bigger/solid/rarible@3x.png b/resources/images/socials/bigger/solid/rarible@3x.png new file mode 100644 index 00000000000..d4672421a18 Binary files /dev/null and b/resources/images/socials/bigger/solid/rarible@3x.png differ diff --git a/resources/images/socials/bigger/solid/reddit@2x.png b/resources/images/socials/bigger/solid/reddit@2x.png new file mode 100644 index 00000000000..331bc84c12b Binary files /dev/null and b/resources/images/socials/bigger/solid/reddit@2x.png differ diff --git a/resources/images/socials/bigger/solid/reddit@3x.png b/resources/images/socials/bigger/solid/reddit@3x.png new file mode 100644 index 00000000000..610f3fa0788 Binary files /dev/null and b/resources/images/socials/bigger/solid/reddit@3x.png differ diff --git a/resources/images/socials/bigger/solid/snapchat@2x.png b/resources/images/socials/bigger/solid/snapchat@2x.png new file mode 100644 index 00000000000..b3b46096976 Binary files /dev/null and b/resources/images/socials/bigger/solid/snapchat@2x.png differ diff --git a/resources/images/socials/bigger/solid/snapchat@3x.png b/resources/images/socials/bigger/solid/snapchat@3x.png new file mode 100644 index 00000000000..3f836fe292b Binary files /dev/null and b/resources/images/socials/bigger/solid/snapchat@3x.png differ diff --git a/resources/images/socials/bigger/solid/spotify@2x.png b/resources/images/socials/bigger/solid/spotify@2x.png new file mode 100644 index 00000000000..eedb87972e4 Binary files /dev/null and b/resources/images/socials/bigger/solid/spotify@2x.png differ diff --git a/resources/images/socials/bigger/solid/spotify@3x.png b/resources/images/socials/bigger/solid/spotify@3x.png new file mode 100644 index 00000000000..23086b946ca Binary files /dev/null and b/resources/images/socials/bigger/solid/spotify@3x.png differ diff --git a/resources/images/socials/bigger/solid/status@2x.png b/resources/images/socials/bigger/solid/status@2x.png new file mode 100644 index 00000000000..e4e99b474f7 Binary files /dev/null and b/resources/images/socials/bigger/solid/status@2x.png differ diff --git a/resources/images/socials/bigger/solid/status@3x.png b/resources/images/socials/bigger/solid/status@3x.png new file mode 100644 index 00000000000..fad9f459532 Binary files /dev/null and b/resources/images/socials/bigger/solid/status@3x.png differ diff --git a/resources/images/socials/bigger/solid/superrare@2x.png b/resources/images/socials/bigger/solid/superrare@2x.png new file mode 100644 index 00000000000..e213f84b021 Binary files /dev/null and b/resources/images/socials/bigger/solid/superrare@2x.png differ diff --git a/resources/images/socials/bigger/solid/superrare@3x.png b/resources/images/socials/bigger/solid/superrare@3x.png new file mode 100644 index 00000000000..2b5060da4c9 Binary files /dev/null and b/resources/images/socials/bigger/solid/superrare@3x.png differ diff --git a/resources/images/socials/bigger/solid/tumblr@2x.png b/resources/images/socials/bigger/solid/tumblr@2x.png new file mode 100644 index 00000000000..dd39da3889b Binary files /dev/null and b/resources/images/socials/bigger/solid/tumblr@2x.png differ diff --git a/resources/images/socials/bigger/solid/tumblr@3x.png b/resources/images/socials/bigger/solid/tumblr@3x.png new file mode 100644 index 00000000000..a8ad1e9777e Binary files /dev/null and b/resources/images/socials/bigger/solid/tumblr@3x.png differ diff --git a/resources/images/socials/bigger/solid/twitch@2x.png b/resources/images/socials/bigger/solid/twitch@2x.png new file mode 100644 index 00000000000..e1b8ca16149 Binary files /dev/null and b/resources/images/socials/bigger/solid/twitch@2x.png differ diff --git a/resources/images/socials/bigger/solid/twitch@3x.png b/resources/images/socials/bigger/solid/twitch@3x.png new file mode 100644 index 00000000000..ba2eada02dc Binary files /dev/null and b/resources/images/socials/bigger/solid/twitch@3x.png differ diff --git a/resources/images/socials/bigger/solid/twitter@2x.png b/resources/images/socials/bigger/solid/twitter@2x.png new file mode 100644 index 00000000000..fbd82d2c197 Binary files /dev/null and b/resources/images/socials/bigger/solid/twitter@2x.png differ diff --git a/resources/images/socials/bigger/solid/twitter@3x.png b/resources/images/socials/bigger/solid/twitter@3x.png new file mode 100644 index 00000000000..0ab7f64b7d3 Binary files /dev/null and b/resources/images/socials/bigger/solid/twitter@3x.png differ diff --git a/resources/images/socials/bigger/solid/youtube@2x.png b/resources/images/socials/bigger/solid/youtube@2x.png new file mode 100644 index 00000000000..11a9dc53016 Binary files /dev/null and b/resources/images/socials/bigger/solid/youtube@2x.png differ diff --git a/resources/images/socials/bigger/solid/youtube@3x.png b/resources/images/socials/bigger/solid/youtube@3x.png new file mode 100644 index 00000000000..4d1ec1cc6ca Binary files /dev/null and b/resources/images/socials/bigger/solid/youtube@3x.png differ diff --git a/resources/images/socials/default/default/facebook@2x.png b/resources/images/socials/default/default/facebook@2x.png new file mode 100644 index 00000000000..9a16ed498b0 Binary files /dev/null and b/resources/images/socials/default/default/facebook@2x.png differ diff --git a/resources/images/socials/default/default/facebook@3x.png b/resources/images/socials/default/default/facebook@3x.png new file mode 100644 index 00000000000..e27abb602ef Binary files /dev/null and b/resources/images/socials/default/default/facebook@3x.png differ diff --git a/resources/images/socials/default/default/flickr@2x.png b/resources/images/socials/default/default/flickr@2x.png new file mode 100644 index 00000000000..7d25e1cac6a Binary files /dev/null and b/resources/images/socials/default/default/flickr@2x.png differ diff --git a/resources/images/socials/default/default/flickr@3x.png b/resources/images/socials/default/default/flickr@3x.png new file mode 100644 index 00000000000..afa585c1901 Binary files /dev/null and b/resources/images/socials/default/default/flickr@3x.png differ diff --git a/resources/images/socials/default/default/github@2x.png b/resources/images/socials/default/default/github@2x.png new file mode 100644 index 00000000000..9e981059c92 Binary files /dev/null and b/resources/images/socials/default/default/github@2x.png differ diff --git a/resources/images/socials/default/default/github@3x.png b/resources/images/socials/default/default/github@3x.png new file mode 100644 index 00000000000..fb74fa607f1 Binary files /dev/null and b/resources/images/socials/default/default/github@3x.png differ diff --git a/resources/images/socials/default/default/instagram@2x.png b/resources/images/socials/default/default/instagram@2x.png new file mode 100644 index 00000000000..60877d2c72f Binary files /dev/null and b/resources/images/socials/default/default/instagram@2x.png differ diff --git a/resources/images/socials/default/default/instagram@3x.png b/resources/images/socials/default/default/instagram@3x.png new file mode 100644 index 00000000000..0f638a43ed9 Binary files /dev/null and b/resources/images/socials/default/default/instagram@3x.png differ diff --git a/resources/images/socials/default/default/lens@2x.png b/resources/images/socials/default/default/lens@2x.png new file mode 100644 index 00000000000..88b366b95be Binary files /dev/null and b/resources/images/socials/default/default/lens@2x.png differ diff --git a/resources/images/socials/default/default/lens@3x.png b/resources/images/socials/default/default/lens@3x.png new file mode 100644 index 00000000000..ff21d93135b Binary files /dev/null and b/resources/images/socials/default/default/lens@3x.png differ diff --git a/resources/images/socials/default/default/link@2x.png b/resources/images/socials/default/default/link@2x.png new file mode 100644 index 00000000000..35044765d30 Binary files /dev/null and b/resources/images/socials/default/default/link@2x.png differ diff --git a/resources/images/socials/default/default/link@3x.png b/resources/images/socials/default/default/link@3x.png new file mode 100644 index 00000000000..de7fa9ca83a Binary files /dev/null and b/resources/images/socials/default/default/link@3x.png differ diff --git a/resources/images/socials/default/default/linkedin@2x.png b/resources/images/socials/default/default/linkedin@2x.png new file mode 100644 index 00000000000..8b74fe4d5ca Binary files /dev/null and b/resources/images/socials/default/default/linkedin@2x.png differ diff --git a/resources/images/socials/default/default/linkedin@3x.png b/resources/images/socials/default/default/linkedin@3x.png new file mode 100644 index 00000000000..f9666bc72e8 Binary files /dev/null and b/resources/images/socials/default/default/linkedin@3x.png differ diff --git a/resources/images/socials/default/default/mirror@2x.png b/resources/images/socials/default/default/mirror@2x.png new file mode 100644 index 00000000000..0b3e771a5b5 Binary files /dev/null and b/resources/images/socials/default/default/mirror@2x.png differ diff --git a/resources/images/socials/default/default/mirror@3x.png b/resources/images/socials/default/default/mirror@3x.png new file mode 100644 index 00000000000..12ca004299b Binary files /dev/null and b/resources/images/socials/default/default/mirror@3x.png differ diff --git a/resources/images/socials/default/default/opensea@2x.png b/resources/images/socials/default/default/opensea@2x.png new file mode 100644 index 00000000000..3e8cb8a4710 Binary files /dev/null and b/resources/images/socials/default/default/opensea@2x.png differ diff --git a/resources/images/socials/default/default/opensea@3x.png b/resources/images/socials/default/default/opensea@3x.png new file mode 100644 index 00000000000..5be1effa1e9 Binary files /dev/null and b/resources/images/socials/default/default/opensea@3x.png differ diff --git a/resources/images/socials/default/default/pinterest@2x.png b/resources/images/socials/default/default/pinterest@2x.png new file mode 100644 index 00000000000..2ff837a3dca Binary files /dev/null and b/resources/images/socials/default/default/pinterest@2x.png differ diff --git a/resources/images/socials/default/default/pinterest@3x.png b/resources/images/socials/default/default/pinterest@3x.png new file mode 100644 index 00000000000..71de92646b7 Binary files /dev/null and b/resources/images/socials/default/default/pinterest@3x.png differ diff --git a/resources/images/socials/default/default/rarible@2x.png b/resources/images/socials/default/default/rarible@2x.png new file mode 100644 index 00000000000..91cf74e1226 Binary files /dev/null and b/resources/images/socials/default/default/rarible@2x.png differ diff --git a/resources/images/socials/default/default/rarible@3x.png b/resources/images/socials/default/default/rarible@3x.png new file mode 100644 index 00000000000..fe78918a9b1 Binary files /dev/null and b/resources/images/socials/default/default/rarible@3x.png differ diff --git a/resources/images/socials/default/default/reddit@2x.png b/resources/images/socials/default/default/reddit@2x.png new file mode 100644 index 00000000000..08b7140f0dc Binary files /dev/null and b/resources/images/socials/default/default/reddit@2x.png differ diff --git a/resources/images/socials/default/default/reddit@3x.png b/resources/images/socials/default/default/reddit@3x.png new file mode 100644 index 00000000000..6b443ed76da Binary files /dev/null and b/resources/images/socials/default/default/reddit@3x.png differ diff --git a/resources/images/socials/default/default/snapchat@2x.png b/resources/images/socials/default/default/snapchat@2x.png new file mode 100644 index 00000000000..59fbfc06413 Binary files /dev/null and b/resources/images/socials/default/default/snapchat@2x.png differ diff --git a/resources/images/socials/default/default/snapchat@3x.png b/resources/images/socials/default/default/snapchat@3x.png new file mode 100644 index 00000000000..85e8aa0c2de Binary files /dev/null and b/resources/images/socials/default/default/snapchat@3x.png differ diff --git a/resources/images/socials/default/default/spotify@2x.png b/resources/images/socials/default/default/spotify@2x.png new file mode 100644 index 00000000000..f6e1d7004b0 Binary files /dev/null and b/resources/images/socials/default/default/spotify@2x.png differ diff --git a/resources/images/socials/default/default/spotify@3x.png b/resources/images/socials/default/default/spotify@3x.png new file mode 100644 index 00000000000..05231d8088d Binary files /dev/null and b/resources/images/socials/default/default/spotify@3x.png differ diff --git a/resources/images/socials/default/default/status@2x.png b/resources/images/socials/default/default/status@2x.png new file mode 100644 index 00000000000..2ceb831efa3 Binary files /dev/null and b/resources/images/socials/default/default/status@2x.png differ diff --git a/resources/images/socials/default/default/status@3x.png b/resources/images/socials/default/default/status@3x.png new file mode 100644 index 00000000000..db80ebd8a7c Binary files /dev/null and b/resources/images/socials/default/default/status@3x.png differ diff --git a/resources/images/socials/default/default/superrare@2x.png b/resources/images/socials/default/default/superrare@2x.png new file mode 100644 index 00000000000..2f20568ba13 Binary files /dev/null and b/resources/images/socials/default/default/superrare@2x.png differ diff --git a/resources/images/socials/default/default/superrare@3x.png b/resources/images/socials/default/default/superrare@3x.png new file mode 100644 index 00000000000..47cd340809c Binary files /dev/null and b/resources/images/socials/default/default/superrare@3x.png differ diff --git a/resources/images/socials/default/default/tumblr@2x.png b/resources/images/socials/default/default/tumblr@2x.png new file mode 100644 index 00000000000..858dc2def89 Binary files /dev/null and b/resources/images/socials/default/default/tumblr@2x.png differ diff --git a/resources/images/socials/default/default/tumblr@3x.png b/resources/images/socials/default/default/tumblr@3x.png new file mode 100644 index 00000000000..e73ff79d2c2 Binary files /dev/null and b/resources/images/socials/default/default/tumblr@3x.png differ diff --git a/resources/images/socials/default/default/twitch@2x.png b/resources/images/socials/default/default/twitch@2x.png new file mode 100644 index 00000000000..c1b7ec46656 Binary files /dev/null and b/resources/images/socials/default/default/twitch@2x.png differ diff --git a/resources/images/socials/default/default/twitch@3x.png b/resources/images/socials/default/default/twitch@3x.png new file mode 100644 index 00000000000..8409936bf3a Binary files /dev/null and b/resources/images/socials/default/default/twitch@3x.png differ diff --git a/resources/images/socials/default/default/twitter@2x.png b/resources/images/socials/default/default/twitter@2x.png new file mode 100644 index 00000000000..7e7a6101b1b Binary files /dev/null and b/resources/images/socials/default/default/twitter@2x.png differ diff --git a/resources/images/socials/default/default/twitter@3x.png b/resources/images/socials/default/default/twitter@3x.png new file mode 100644 index 00000000000..600b922424d Binary files /dev/null and b/resources/images/socials/default/default/twitter@3x.png differ diff --git a/resources/images/socials/default/default/youtube@2x.png b/resources/images/socials/default/default/youtube@2x.png new file mode 100644 index 00000000000..79f6959d4c7 Binary files /dev/null and b/resources/images/socials/default/default/youtube@2x.png differ diff --git a/resources/images/socials/default/default/youtube@3x.png b/resources/images/socials/default/default/youtube@3x.png new file mode 100644 index 00000000000..5129c425465 Binary files /dev/null and b/resources/images/socials/default/default/youtube@3x.png differ diff --git a/resources/images/socials/default/solid/facebook@2x.png b/resources/images/socials/default/solid/facebook@2x.png new file mode 100644 index 00000000000..982e9f597e9 Binary files /dev/null and b/resources/images/socials/default/solid/facebook@2x.png differ diff --git a/resources/images/socials/default/solid/facebook@3x.png b/resources/images/socials/default/solid/facebook@3x.png new file mode 100644 index 00000000000..89c62aebedc Binary files /dev/null and b/resources/images/socials/default/solid/facebook@3x.png differ diff --git a/resources/images/socials/default/solid/flickr@2x.png b/resources/images/socials/default/solid/flickr@2x.png new file mode 100644 index 00000000000..dc8c533b338 Binary files /dev/null and b/resources/images/socials/default/solid/flickr@2x.png differ diff --git a/resources/images/socials/default/solid/flickr@3x.png b/resources/images/socials/default/solid/flickr@3x.png new file mode 100644 index 00000000000..2eaf40002c5 Binary files /dev/null and b/resources/images/socials/default/solid/flickr@3x.png differ diff --git a/resources/images/socials/default/solid/github@2x.png b/resources/images/socials/default/solid/github@2x.png new file mode 100644 index 00000000000..9e981059c92 Binary files /dev/null and b/resources/images/socials/default/solid/github@2x.png differ diff --git a/resources/images/socials/default/solid/github@3x.png b/resources/images/socials/default/solid/github@3x.png new file mode 100644 index 00000000000..2de837785c0 Binary files /dev/null and b/resources/images/socials/default/solid/github@3x.png differ diff --git a/resources/images/socials/default/solid/instagram@2x.png b/resources/images/socials/default/solid/instagram@2x.png new file mode 100644 index 00000000000..258fd2ab7b4 Binary files /dev/null and b/resources/images/socials/default/solid/instagram@2x.png differ diff --git a/resources/images/socials/default/solid/instagram@3x.png b/resources/images/socials/default/solid/instagram@3x.png new file mode 100644 index 00000000000..f5a55ba5dc1 Binary files /dev/null and b/resources/images/socials/default/solid/instagram@3x.png differ diff --git a/resources/images/socials/default/solid/lens@2x.png b/resources/images/socials/default/solid/lens@2x.png new file mode 100644 index 00000000000..3f8ec6956cb Binary files /dev/null and b/resources/images/socials/default/solid/lens@2x.png differ diff --git a/resources/images/socials/default/solid/lens@3x.png b/resources/images/socials/default/solid/lens@3x.png new file mode 100644 index 00000000000..bf098a7cd30 Binary files /dev/null and b/resources/images/socials/default/solid/lens@3x.png differ diff --git a/resources/images/socials/default/solid/link@2x.png b/resources/images/socials/default/solid/link@2x.png new file mode 100644 index 00000000000..35044765d30 Binary files /dev/null and b/resources/images/socials/default/solid/link@2x.png differ diff --git a/resources/images/socials/default/solid/link@3x.png b/resources/images/socials/default/solid/link@3x.png new file mode 100644 index 00000000000..de7fa9ca83a Binary files /dev/null and b/resources/images/socials/default/solid/link@3x.png differ diff --git a/resources/images/socials/default/solid/linkedin@2x.png b/resources/images/socials/default/solid/linkedin@2x.png new file mode 100644 index 00000000000..8e8d6a06ac3 Binary files /dev/null and b/resources/images/socials/default/solid/linkedin@2x.png differ diff --git a/resources/images/socials/default/solid/linkedin@3x.png b/resources/images/socials/default/solid/linkedin@3x.png new file mode 100644 index 00000000000..9d23c515e3c Binary files /dev/null and b/resources/images/socials/default/solid/linkedin@3x.png differ diff --git a/resources/images/socials/default/solid/mirror@2x.png b/resources/images/socials/default/solid/mirror@2x.png new file mode 100644 index 00000000000..473a75a140f Binary files /dev/null and b/resources/images/socials/default/solid/mirror@2x.png differ diff --git a/resources/images/socials/default/solid/mirror@3x.png b/resources/images/socials/default/solid/mirror@3x.png new file mode 100644 index 00000000000..7466927c9c6 Binary files /dev/null and b/resources/images/socials/default/solid/mirror@3x.png differ diff --git a/resources/images/socials/default/solid/opensea@2x.png b/resources/images/socials/default/solid/opensea@2x.png new file mode 100644 index 00000000000..a15da8ee021 Binary files /dev/null and b/resources/images/socials/default/solid/opensea@2x.png differ diff --git a/resources/images/socials/default/solid/opensea@3x.png b/resources/images/socials/default/solid/opensea@3x.png new file mode 100644 index 00000000000..7d898d204d8 Binary files /dev/null and b/resources/images/socials/default/solid/opensea@3x.png differ diff --git a/resources/images/socials/default/solid/pinterest@2x.png b/resources/images/socials/default/solid/pinterest@2x.png new file mode 100644 index 00000000000..bb37a160322 Binary files /dev/null and b/resources/images/socials/default/solid/pinterest@2x.png differ diff --git a/resources/images/socials/default/solid/pinterest@3x.png b/resources/images/socials/default/solid/pinterest@3x.png new file mode 100644 index 00000000000..703d4b02ef1 Binary files /dev/null and b/resources/images/socials/default/solid/pinterest@3x.png differ diff --git a/resources/images/socials/default/solid/rarible@2x.png b/resources/images/socials/default/solid/rarible@2x.png new file mode 100644 index 00000000000..3bea0d384ba Binary files /dev/null and b/resources/images/socials/default/solid/rarible@2x.png differ diff --git a/resources/images/socials/default/solid/rarible@3x.png b/resources/images/socials/default/solid/rarible@3x.png new file mode 100644 index 00000000000..0a454c15ea5 Binary files /dev/null and b/resources/images/socials/default/solid/rarible@3x.png differ diff --git a/resources/images/socials/default/solid/reddit@2x.png b/resources/images/socials/default/solid/reddit@2x.png new file mode 100644 index 00000000000..203cdcb03c1 Binary files /dev/null and b/resources/images/socials/default/solid/reddit@2x.png differ diff --git a/resources/images/socials/default/solid/reddit@3x.png b/resources/images/socials/default/solid/reddit@3x.png new file mode 100644 index 00000000000..1dfe6bee22d Binary files /dev/null and b/resources/images/socials/default/solid/reddit@3x.png differ diff --git a/resources/images/socials/default/solid/snapchat@2x.png b/resources/images/socials/default/solid/snapchat@2x.png new file mode 100644 index 00000000000..de9c83c272b Binary files /dev/null and b/resources/images/socials/default/solid/snapchat@2x.png differ diff --git a/resources/images/socials/default/solid/snapchat@3x.png b/resources/images/socials/default/solid/snapchat@3x.png new file mode 100644 index 00000000000..916fb83a4d3 Binary files /dev/null and b/resources/images/socials/default/solid/snapchat@3x.png differ diff --git a/resources/images/socials/default/solid/spotify@2x.png b/resources/images/socials/default/solid/spotify@2x.png new file mode 100644 index 00000000000..508914e3996 Binary files /dev/null and b/resources/images/socials/default/solid/spotify@2x.png differ diff --git a/resources/images/socials/default/solid/spotify@3x.png b/resources/images/socials/default/solid/spotify@3x.png new file mode 100644 index 00000000000..6a5ee78ee00 Binary files /dev/null and b/resources/images/socials/default/solid/spotify@3x.png differ diff --git a/resources/images/socials/default/solid/status@2x.png b/resources/images/socials/default/solid/status@2x.png new file mode 100644 index 00000000000..c5a6d3d9e69 Binary files /dev/null and b/resources/images/socials/default/solid/status@2x.png differ diff --git a/resources/images/socials/default/solid/status@3x.png b/resources/images/socials/default/solid/status@3x.png new file mode 100644 index 00000000000..7ee03b02bbd Binary files /dev/null and b/resources/images/socials/default/solid/status@3x.png differ diff --git a/resources/images/socials/default/solid/superrare@2x.png b/resources/images/socials/default/solid/superrare@2x.png new file mode 100644 index 00000000000..2f20568ba13 Binary files /dev/null and b/resources/images/socials/default/solid/superrare@2x.png differ diff --git a/resources/images/socials/default/solid/superrare@3x.png b/resources/images/socials/default/solid/superrare@3x.png new file mode 100644 index 00000000000..47cd340809c Binary files /dev/null and b/resources/images/socials/default/solid/superrare@3x.png differ diff --git a/resources/images/socials/default/solid/tumblr@2x.png b/resources/images/socials/default/solid/tumblr@2x.png new file mode 100644 index 00000000000..998ba66e30b Binary files /dev/null and b/resources/images/socials/default/solid/tumblr@2x.png differ diff --git a/resources/images/socials/default/solid/tumblr@3x.png b/resources/images/socials/default/solid/tumblr@3x.png new file mode 100644 index 00000000000..bc6f0a4b46c Binary files /dev/null and b/resources/images/socials/default/solid/tumblr@3x.png differ diff --git a/resources/images/socials/default/solid/twitch@2x.png b/resources/images/socials/default/solid/twitch@2x.png new file mode 100644 index 00000000000..d137c48d514 Binary files /dev/null and b/resources/images/socials/default/solid/twitch@2x.png differ diff --git a/resources/images/socials/default/solid/twitch@3x.png b/resources/images/socials/default/solid/twitch@3x.png new file mode 100644 index 00000000000..6bd35d4caeb Binary files /dev/null and b/resources/images/socials/default/solid/twitch@3x.png differ diff --git a/resources/images/socials/default/solid/twitter@2x.png b/resources/images/socials/default/solid/twitter@2x.png new file mode 100644 index 00000000000..df9c7d89c8c Binary files /dev/null and b/resources/images/socials/default/solid/twitter@2x.png differ diff --git a/resources/images/socials/default/solid/twitter@3x.png b/resources/images/socials/default/solid/twitter@3x.png new file mode 100644 index 00000000000..b0a8cd40b51 Binary files /dev/null and b/resources/images/socials/default/solid/twitter@3x.png differ diff --git a/resources/images/socials/default/solid/youtube@2x.png b/resources/images/socials/default/solid/youtube@2x.png new file mode 100644 index 00000000000..8d3f5af35f0 Binary files /dev/null and b/resources/images/socials/default/solid/youtube@2x.png differ diff --git a/resources/images/socials/default/solid/youtube@3x.png b/resources/images/socials/default/solid/youtube@3x.png new file mode 100644 index 00000000000..893c859b74f Binary files /dev/null and b/resources/images/socials/default/solid/youtube@3x.png differ diff --git a/resources/images/tokens/bsc/0-native.png b/resources/images/tokens/bsc/0-native.png new file mode 100644 index 00000000000..e715b87e05e Binary files /dev/null and b/resources/images/tokens/bsc/0-native.png differ diff --git a/resources/images/tokens/default-native.png b/resources/images/tokens/default-native.png new file mode 100644 index 00000000000..54c8551090c Binary files /dev/null and b/resources/images/tokens/default-native.png differ diff --git a/resources/images/tokens/default-native@2x.png b/resources/images/tokens/default-native@2x.png new file mode 100644 index 00000000000..d391ba331ff Binary files /dev/null and b/resources/images/tokens/default-native@2x.png differ diff --git a/resources/images/tokens/default-native@3x.png b/resources/images/tokens/default-native@3x.png new file mode 100644 index 00000000000..63e38d31bc4 Binary files /dev/null and b/resources/images/tokens/default-native@3x.png differ diff --git a/resources/images/tokens/default-token.png b/resources/images/tokens/default-token.png new file mode 100644 index 00000000000..8a49b66c046 Binary files /dev/null and b/resources/images/tokens/default-token.png differ diff --git a/resources/images/tokens/default-token@2x.png b/resources/images/tokens/default-token@2x.png new file mode 100644 index 00000000000..a2474c21252 Binary files /dev/null and b/resources/images/tokens/default-token@2x.png differ diff --git a/resources/images/tokens/default-token@3x.png b/resources/images/tokens/default-token@3x.png new file mode 100644 index 00000000000..41bbae54aa6 Binary files /dev/null and b/resources/images/tokens/default-token@3x.png differ diff --git a/resources/images/tokens/default.png b/resources/images/tokens/default.png deleted file mode 100644 index 0b45475f8aa..00000000000 Binary files a/resources/images/tokens/default.png and /dev/null differ diff --git a/resources/images/tokens/default@2x.png b/resources/images/tokens/default@2x.png deleted file mode 100644 index 54b8e4119d8..00000000000 Binary files a/resources/images/tokens/default@2x.png and /dev/null differ diff --git a/resources/images/tokens/default@3x.png b/resources/images/tokens/default@3x.png deleted file mode 100644 index 8ae03b4cc8a..00000000000 Binary files a/resources/images/tokens/default@3x.png and /dev/null differ diff --git a/resources/images/tokens/mainnet/0-native.png b/resources/images/tokens/mainnet/0-native.png new file mode 100644 index 00000000000..54c8551090c Binary files /dev/null and b/resources/images/tokens/mainnet/0-native.png differ diff --git a/resources/images/tokens/mainnet/0-native@2x.png b/resources/images/tokens/mainnet/0-native@2x.png new file mode 100644 index 00000000000..d391ba331ff Binary files /dev/null and b/resources/images/tokens/mainnet/0-native@2x.png differ diff --git a/resources/images/tokens/mainnet/0-native@3x.png b/resources/images/tokens/mainnet/0-native@3x.png new file mode 100644 index 00000000000..63e38d31bc4 Binary files /dev/null and b/resources/images/tokens/mainnet/0-native@3x.png differ diff --git a/resources/images/tokens/mainnet/0XBTC.png b/resources/images/tokens/mainnet/0XBTC.png new file mode 100644 index 00000000000..372f202ca69 Binary files /dev/null and b/resources/images/tokens/mainnet/0XBTC.png differ diff --git a/resources/images/tokens/mainnet/0XBTC@2x.png b/resources/images/tokens/mainnet/0XBTC@2x.png new file mode 100644 index 00000000000..2e982dbbff4 Binary files /dev/null and b/resources/images/tokens/mainnet/0XBTC@2x.png differ diff --git a/resources/images/tokens/mainnet/0XBTC@3x.png b/resources/images/tokens/mainnet/0XBTC@3x.png new file mode 100644 index 00000000000..7465dba9938 Binary files /dev/null and b/resources/images/tokens/mainnet/0XBTC@3x.png differ diff --git a/resources/images/tokens/mainnet/1ST.png b/resources/images/tokens/mainnet/1ST.png index e8c8f99b17e..6f5471ce281 100644 Binary files a/resources/images/tokens/mainnet/1ST.png and b/resources/images/tokens/mainnet/1ST.png differ diff --git a/resources/images/tokens/mainnet/1ST@2x.png b/resources/images/tokens/mainnet/1ST@2x.png index e3c16076e5f..adc5be5bccc 100644 Binary files a/resources/images/tokens/mainnet/1ST@2x.png and b/resources/images/tokens/mainnet/1ST@2x.png differ diff --git a/resources/images/tokens/mainnet/1ST@3x.png b/resources/images/tokens/mainnet/1ST@3x.png index 9c8d270c3a5..160e9f3c706 100644 Binary files a/resources/images/tokens/mainnet/1ST@3x.png and b/resources/images/tokens/mainnet/1ST@3x.png differ diff --git a/resources/images/tokens/mainnet/ABT.png b/resources/images/tokens/mainnet/ABT.png new file mode 100644 index 00000000000..2b4abc79bf7 Binary files /dev/null and b/resources/images/tokens/mainnet/ABT.png differ diff --git a/resources/images/tokens/mainnet/ABT@2x.png b/resources/images/tokens/mainnet/ABT@2x.png new file mode 100644 index 00000000000..4b672597753 Binary files /dev/null and b/resources/images/tokens/mainnet/ABT@2x.png differ diff --git a/resources/images/tokens/mainnet/ABT@3x.png b/resources/images/tokens/mainnet/ABT@3x.png new file mode 100644 index 00000000000..99738b308ce Binary files /dev/null and b/resources/images/tokens/mainnet/ABT@3x.png differ diff --git a/resources/images/tokens/mainnet/ABYSS.png b/resources/images/tokens/mainnet/ABYSS.png new file mode 100644 index 00000000000..7e3f15d4746 Binary files /dev/null and b/resources/images/tokens/mainnet/ABYSS.png differ diff --git a/resources/images/tokens/mainnet/ABYSS@2x.png b/resources/images/tokens/mainnet/ABYSS@2x.png new file mode 100644 index 00000000000..b080d006640 Binary files /dev/null and b/resources/images/tokens/mainnet/ABYSS@2x.png differ diff --git a/resources/images/tokens/mainnet/ABYSS@3x.png b/resources/images/tokens/mainnet/ABYSS@3x.png new file mode 100644 index 00000000000..c708218256f Binary files /dev/null and b/resources/images/tokens/mainnet/ABYSS@3x.png differ diff --git a/resources/images/tokens/mainnet/AE.png b/resources/images/tokens/mainnet/AE.png index 5096fd0f04e..60c834a756f 100644 Binary files a/resources/images/tokens/mainnet/AE.png and b/resources/images/tokens/mainnet/AE.png differ diff --git a/resources/images/tokens/mainnet/AE@2x.png b/resources/images/tokens/mainnet/AE@2x.png index 3ba8f5b0429..f6cef421665 100644 Binary files a/resources/images/tokens/mainnet/AE@2x.png and b/resources/images/tokens/mainnet/AE@2x.png differ diff --git a/resources/images/tokens/mainnet/AE@3x.png b/resources/images/tokens/mainnet/AE@3x.png index 711c1914226..c12d389ab9e 100644 Binary files a/resources/images/tokens/mainnet/AE@3x.png and b/resources/images/tokens/mainnet/AE@3x.png differ diff --git a/resources/images/tokens/mainnet/AGLD@2x.png b/resources/images/tokens/mainnet/AGLD@2x.png new file mode 100644 index 00000000000..2c24edcdba4 Binary files /dev/null and b/resources/images/tokens/mainnet/AGLD@2x.png differ diff --git a/resources/images/tokens/mainnet/AGLD@3x.png b/resources/images/tokens/mainnet/AGLD@3x.png new file mode 100644 index 00000000000..f25ed10db1f Binary files /dev/null and b/resources/images/tokens/mainnet/AGLD@3x.png differ diff --git a/resources/images/tokens/mainnet/AKRO.png b/resources/images/tokens/mainnet/AKRO.png new file mode 100644 index 00000000000..b9bfa4a1f92 Binary files /dev/null and b/resources/images/tokens/mainnet/AKRO.png differ diff --git a/resources/images/tokens/mainnet/AKRO@2x.png b/resources/images/tokens/mainnet/AKRO@2x.png new file mode 100644 index 00000000000..869d9a17036 Binary files /dev/null and b/resources/images/tokens/mainnet/AKRO@2x.png differ diff --git a/resources/images/tokens/mainnet/AKRO@3x.png b/resources/images/tokens/mainnet/AKRO@3x.png new file mode 100644 index 00000000000..09df3409344 Binary files /dev/null and b/resources/images/tokens/mainnet/AKRO@3x.png differ diff --git a/resources/images/tokens/mainnet/AMB.png b/resources/images/tokens/mainnet/AMB.png index 0587220e58a..c98f629cfa8 100644 Binary files a/resources/images/tokens/mainnet/AMB.png and b/resources/images/tokens/mainnet/AMB.png differ diff --git a/resources/images/tokens/mainnet/AMB@2x.png b/resources/images/tokens/mainnet/AMB@2x.png index d17ec8f6a07..011c2da1659 100644 Binary files a/resources/images/tokens/mainnet/AMB@2x.png and b/resources/images/tokens/mainnet/AMB@2x.png differ diff --git a/resources/images/tokens/mainnet/AMB@3x.png b/resources/images/tokens/mainnet/AMB@3x.png index 08cb29a6df9..3049c5386a3 100644 Binary files a/resources/images/tokens/mainnet/AMB@3x.png and b/resources/images/tokens/mainnet/AMB@3x.png differ diff --git a/resources/images/tokens/mainnet/AMPL.png b/resources/images/tokens/mainnet/AMPL.png new file mode 100644 index 00000000000..17410b577d3 Binary files /dev/null and b/resources/images/tokens/mainnet/AMPL.png differ diff --git a/resources/images/tokens/mainnet/AMPL@2x.png b/resources/images/tokens/mainnet/AMPL@2x.png new file mode 100644 index 00000000000..9e7cff2cdac Binary files /dev/null and b/resources/images/tokens/mainnet/AMPL@2x.png differ diff --git a/resources/images/tokens/mainnet/AMPL@3x.png b/resources/images/tokens/mainnet/AMPL@3x.png new file mode 100644 index 00000000000..1f6ff3974ec Binary files /dev/null and b/resources/images/tokens/mainnet/AMPL@3x.png differ diff --git a/resources/images/tokens/mainnet/ANT.png b/resources/images/tokens/mainnet/ANT.png index 74ed7fb00d7..9101308051d 100644 Binary files a/resources/images/tokens/mainnet/ANT.png and b/resources/images/tokens/mainnet/ANT.png differ diff --git a/resources/images/tokens/mainnet/ANT@2x.png b/resources/images/tokens/mainnet/ANT@2x.png index 10b2f6f32ac..0ac0cc8306c 100644 Binary files a/resources/images/tokens/mainnet/ANT@2x.png and b/resources/images/tokens/mainnet/ANT@2x.png differ diff --git a/resources/images/tokens/mainnet/ANT@3x.png b/resources/images/tokens/mainnet/ANT@3x.png index 9f528823968..37136102814 100644 Binary files a/resources/images/tokens/mainnet/ANT@3x.png and b/resources/images/tokens/mainnet/ANT@3x.png differ diff --git a/resources/images/tokens/mainnet/APPC.png b/resources/images/tokens/mainnet/APPC.png new file mode 100644 index 00000000000..3561e3bb432 Binary files /dev/null and b/resources/images/tokens/mainnet/APPC.png differ diff --git a/resources/images/tokens/mainnet/APPC@2x.png b/resources/images/tokens/mainnet/APPC@2x.png new file mode 100644 index 00000000000..8924963d1b9 Binary files /dev/null and b/resources/images/tokens/mainnet/APPC@2x.png differ diff --git a/resources/images/tokens/mainnet/APPC@3x.png b/resources/images/tokens/mainnet/APPC@3x.png new file mode 100644 index 00000000000..fd2154d27ea Binary files /dev/null and b/resources/images/tokens/mainnet/APPC@3x.png differ diff --git a/resources/images/tokens/mainnet/AST.png b/resources/images/tokens/mainnet/AST.png index 33bcb9dda22..324f576d553 100644 Binary files a/resources/images/tokens/mainnet/AST.png and b/resources/images/tokens/mainnet/AST.png differ diff --git a/resources/images/tokens/mainnet/AST@2x.png b/resources/images/tokens/mainnet/AST@2x.png index 607622d95ff..0b13ddd3d8f 100644 Binary files a/resources/images/tokens/mainnet/AST@2x.png and b/resources/images/tokens/mainnet/AST@2x.png differ diff --git a/resources/images/tokens/mainnet/AST@3x.png b/resources/images/tokens/mainnet/AST@3x.png index ea19fb075b0..e2d98a00d2e 100644 Binary files a/resources/images/tokens/mainnet/AST@3x.png and b/resources/images/tokens/mainnet/AST@3x.png differ diff --git a/resources/images/tokens/mainnet/ATMChain.png b/resources/images/tokens/mainnet/ATMChain.png index 9f5bbd3e916..d5041aea1c0 100644 Binary files a/resources/images/tokens/mainnet/ATMChain.png and b/resources/images/tokens/mainnet/ATMChain.png differ diff --git a/resources/images/tokens/mainnet/ATMChain@2x.png b/resources/images/tokens/mainnet/ATMChain@2x.png index d35fb188e24..9a976441d89 100644 Binary files a/resources/images/tokens/mainnet/ATMChain@2x.png and b/resources/images/tokens/mainnet/ATMChain@2x.png differ diff --git a/resources/images/tokens/mainnet/ATMChain@3x.png b/resources/images/tokens/mainnet/ATMChain@3x.png index 802116c29a7..5002833291f 100644 Binary files a/resources/images/tokens/mainnet/ATMChain@3x.png and b/resources/images/tokens/mainnet/ATMChain@3x.png differ diff --git a/resources/images/tokens/mainnet/BAL.png b/resources/images/tokens/mainnet/BAL.png new file mode 100644 index 00000000000..294404f5571 Binary files /dev/null and b/resources/images/tokens/mainnet/BAL.png differ diff --git a/resources/images/tokens/mainnet/BAL@2x.png b/resources/images/tokens/mainnet/BAL@2x.png new file mode 100644 index 00000000000..a23d7806772 Binary files /dev/null and b/resources/images/tokens/mainnet/BAL@2x.png differ diff --git a/resources/images/tokens/mainnet/BAL@3x.png b/resources/images/tokens/mainnet/BAL@3x.png new file mode 100644 index 00000000000..bd596df5ee7 Binary files /dev/null and b/resources/images/tokens/mainnet/BAL@3x.png differ diff --git a/resources/images/tokens/mainnet/BAM.png b/resources/images/tokens/mainnet/BAM.png new file mode 100644 index 00000000000..48a85dcf25b Binary files /dev/null and b/resources/images/tokens/mainnet/BAM.png differ diff --git a/resources/images/tokens/mainnet/BAM@2x.png b/resources/images/tokens/mainnet/BAM@2x.png new file mode 100644 index 00000000000..423aa137039 Binary files /dev/null and b/resources/images/tokens/mainnet/BAM@2x.png differ diff --git a/resources/images/tokens/mainnet/BAM@3x.png b/resources/images/tokens/mainnet/BAM@3x.png new file mode 100644 index 00000000000..17a5bf4880f Binary files /dev/null and b/resources/images/tokens/mainnet/BAM@3x.png differ diff --git a/resources/images/tokens/mainnet/BAND.png b/resources/images/tokens/mainnet/BAND.png new file mode 100644 index 00000000000..6a3e42986d2 Binary files /dev/null and b/resources/images/tokens/mainnet/BAND.png differ diff --git a/resources/images/tokens/mainnet/BAND@2x.png b/resources/images/tokens/mainnet/BAND@2x.png new file mode 100644 index 00000000000..a198be59258 Binary files /dev/null and b/resources/images/tokens/mainnet/BAND@2x.png differ diff --git a/resources/images/tokens/mainnet/BAND@3x.png b/resources/images/tokens/mainnet/BAND@3x.png new file mode 100644 index 00000000000..a9540b922bf Binary files /dev/null and b/resources/images/tokens/mainnet/BAND@3x.png differ diff --git a/resources/images/tokens/mainnet/BAT.png b/resources/images/tokens/mainnet/BAT.png index cd26c65578f..69a7f747b38 100644 Binary files a/resources/images/tokens/mainnet/BAT.png and b/resources/images/tokens/mainnet/BAT.png differ diff --git a/resources/images/tokens/mainnet/BAT@2x.png b/resources/images/tokens/mainnet/BAT@2x.png index 374bc3bcacb..ed1883f7b7d 100644 Binary files a/resources/images/tokens/mainnet/BAT@2x.png and b/resources/images/tokens/mainnet/BAT@2x.png differ diff --git a/resources/images/tokens/mainnet/BAT@3x.png b/resources/images/tokens/mainnet/BAT@3x.png index 93c40c744c3..c68c722ec92 100644 Binary files a/resources/images/tokens/mainnet/BAT@3x.png and b/resources/images/tokens/mainnet/BAT@3x.png differ diff --git a/resources/images/tokens/mainnet/BLT.png b/resources/images/tokens/mainnet/BLT.png new file mode 100644 index 00000000000..63ea2e804a4 Binary files /dev/null and b/resources/images/tokens/mainnet/BLT.png differ diff --git a/resources/images/tokens/mainnet/BLT@2x.png b/resources/images/tokens/mainnet/BLT@2x.png new file mode 100644 index 00000000000..a3c141f9d8e Binary files /dev/null and b/resources/images/tokens/mainnet/BLT@2x.png differ diff --git a/resources/images/tokens/mainnet/BLT@3x.png b/resources/images/tokens/mainnet/BLT@3x.png new file mode 100644 index 00000000000..0fccb7bfea4 Binary files /dev/null and b/resources/images/tokens/mainnet/BLT@3x.png differ diff --git a/resources/images/tokens/mainnet/BLZ.png b/resources/images/tokens/mainnet/BLZ.png new file mode 100644 index 00000000000..d681ae4ccfd Binary files /dev/null and b/resources/images/tokens/mainnet/BLZ.png differ diff --git a/resources/images/tokens/mainnet/BLZ@2x.png b/resources/images/tokens/mainnet/BLZ@2x.png new file mode 100644 index 00000000000..a22197b41a8 Binary files /dev/null and b/resources/images/tokens/mainnet/BLZ@2x.png differ diff --git a/resources/images/tokens/mainnet/BLZ@3x.png b/resources/images/tokens/mainnet/BLZ@3x.png new file mode 100644 index 00000000000..d47778f17d7 Binary files /dev/null and b/resources/images/tokens/mainnet/BLZ@3x.png differ diff --git a/resources/images/tokens/mainnet/BNB.png b/resources/images/tokens/mainnet/BNB.png index 3a503dd6063..d0fb2ef40fb 100644 Binary files a/resources/images/tokens/mainnet/BNB.png and b/resources/images/tokens/mainnet/BNB.png differ diff --git a/resources/images/tokens/mainnet/BNB@2x.png b/resources/images/tokens/mainnet/BNB@2x.png index 79d7eaacbb7..9462eaa94b1 100644 Binary files a/resources/images/tokens/mainnet/BNB@2x.png and b/resources/images/tokens/mainnet/BNB@2x.png differ diff --git a/resources/images/tokens/mainnet/BNB@3x.png b/resources/images/tokens/mainnet/BNB@3x.png index bf83cecf107..2488676bf44 100644 Binary files a/resources/images/tokens/mainnet/BNB@3x.png and b/resources/images/tokens/mainnet/BNB@3x.png differ diff --git a/resources/images/tokens/mainnet/BNT.png b/resources/images/tokens/mainnet/BNT.png index 5b015fc87b6..688d71673e4 100644 Binary files a/resources/images/tokens/mainnet/BNT.png and b/resources/images/tokens/mainnet/BNT.png differ diff --git a/resources/images/tokens/mainnet/BNT@2x.png b/resources/images/tokens/mainnet/BNT@2x.png index a8d83f1ef0f..e243592283a 100644 Binary files a/resources/images/tokens/mainnet/BNT@2x.png and b/resources/images/tokens/mainnet/BNT@2x.png differ diff --git a/resources/images/tokens/mainnet/BNT@3x.png b/resources/images/tokens/mainnet/BNT@3x.png index eeac8ea3d6a..851ef42f9b2 100644 Binary files a/resources/images/tokens/mainnet/BNT@3x.png and b/resources/images/tokens/mainnet/BNT@3x.png differ diff --git a/resources/images/tokens/mainnet/BRLN.png b/resources/images/tokens/mainnet/BRLN.png new file mode 100644 index 00000000000..654440d66e9 Binary files /dev/null and b/resources/images/tokens/mainnet/BRLN.png differ diff --git a/resources/images/tokens/mainnet/BRLN@2x.png b/resources/images/tokens/mainnet/BRLN@2x.png new file mode 100644 index 00000000000..966a1a028c2 Binary files /dev/null and b/resources/images/tokens/mainnet/BRLN@2x.png differ diff --git a/resources/images/tokens/mainnet/BRLN@3x.png b/resources/images/tokens/mainnet/BRLN@3x.png new file mode 100644 index 00000000000..3f1da72de5f Binary files /dev/null and b/resources/images/tokens/mainnet/BRLN@3x.png differ diff --git a/resources/images/tokens/mainnet/BTC.png b/resources/images/tokens/mainnet/BTC.png new file mode 100644 index 00000000000..ee426a391bd Binary files /dev/null and b/resources/images/tokens/mainnet/BTC.png differ diff --git a/resources/images/tokens/mainnet/BTC@2x.png b/resources/images/tokens/mainnet/BTC@2x.png new file mode 100644 index 00000000000..8b78b5ebd7a Binary files /dev/null and b/resources/images/tokens/mainnet/BTC@2x.png differ diff --git a/resources/images/tokens/mainnet/BTC@3x.png b/resources/images/tokens/mainnet/BTC@3x.png new file mode 100644 index 00000000000..f099862d0a1 Binary files /dev/null and b/resources/images/tokens/mainnet/BTC@3x.png differ diff --git a/resources/images/tokens/mainnet/BTM.png b/resources/images/tokens/mainnet/BTM.png index fb0ef722381..85a8edb35b3 100644 Binary files a/resources/images/tokens/mainnet/BTM.png and b/resources/images/tokens/mainnet/BTM.png differ diff --git a/resources/images/tokens/mainnet/BTM@2x.png b/resources/images/tokens/mainnet/BTM@2x.png index 5828437205a..053ca0711fa 100644 Binary files a/resources/images/tokens/mainnet/BTM@2x.png and b/resources/images/tokens/mainnet/BTM@2x.png differ diff --git a/resources/images/tokens/mainnet/BTM@3x.png b/resources/images/tokens/mainnet/BTM@3x.png index e9f508fee84..58c9e87f48d 100644 Binary files a/resources/images/tokens/mainnet/BTM@3x.png and b/resources/images/tokens/mainnet/BTM@3x.png differ diff --git a/resources/images/tokens/mainnet/BTU.png b/resources/images/tokens/mainnet/BTU.png new file mode 100644 index 00000000000..4463e546b9f Binary files /dev/null and b/resources/images/tokens/mainnet/BTU.png differ diff --git a/resources/images/tokens/mainnet/BTU@2x.png b/resources/images/tokens/mainnet/BTU@2x.png new file mode 100644 index 00000000000..7ef40ed0041 Binary files /dev/null and b/resources/images/tokens/mainnet/BTU@2x.png differ diff --git a/resources/images/tokens/mainnet/BTU@3x.png b/resources/images/tokens/mainnet/BTU@3x.png new file mode 100644 index 00000000000..6eb6c802731 Binary files /dev/null and b/resources/images/tokens/mainnet/BTU@3x.png differ diff --git a/resources/images/tokens/mainnet/CDT.png b/resources/images/tokens/mainnet/CDT.png index e6eb88f458e..514eab013ee 100644 Binary files a/resources/images/tokens/mainnet/CDT.png and b/resources/images/tokens/mainnet/CDT.png differ diff --git a/resources/images/tokens/mainnet/CDT@2x.png b/resources/images/tokens/mainnet/CDT@2x.png index 8d590c1768e..b95aaccb16f 100644 Binary files a/resources/images/tokens/mainnet/CDT@2x.png and b/resources/images/tokens/mainnet/CDT@2x.png differ diff --git a/resources/images/tokens/mainnet/CDT@3x.png b/resources/images/tokens/mainnet/CDT@3x.png index 1de5c38e7ec..aa4b7959876 100644 Binary files a/resources/images/tokens/mainnet/CDT@3x.png and b/resources/images/tokens/mainnet/CDT@3x.png differ diff --git a/resources/images/tokens/mainnet/CFI.png b/resources/images/tokens/mainnet/CFI.png index baa442c632f..8536b188452 100644 Binary files a/resources/images/tokens/mainnet/CFI.png and b/resources/images/tokens/mainnet/CFI.png differ diff --git a/resources/images/tokens/mainnet/CFI@2x.png b/resources/images/tokens/mainnet/CFI@2x.png index 105ac20db1e..65c1de7912d 100644 Binary files a/resources/images/tokens/mainnet/CFI@2x.png and b/resources/images/tokens/mainnet/CFI@2x.png differ diff --git a/resources/images/tokens/mainnet/CFI@3x.png b/resources/images/tokens/mainnet/CFI@3x.png index c23f73d1875..113ab0b4472 100644 Binary files a/resources/images/tokens/mainnet/CFI@3x.png and b/resources/images/tokens/mainnet/CFI@3x.png differ diff --git a/resources/images/tokens/mainnet/CK.png b/resources/images/tokens/mainnet/CK.png old mode 100755 new mode 100644 index 5375407a3c0..73aef9eb2d9 Binary files a/resources/images/tokens/mainnet/CK.png and b/resources/images/tokens/mainnet/CK.png differ diff --git a/resources/images/tokens/mainnet/CK@2x.png b/resources/images/tokens/mainnet/CK@2x.png old mode 100755 new mode 100644 index c8eb0fac4d0..e74fffb4d55 Binary files a/resources/images/tokens/mainnet/CK@2x.png and b/resources/images/tokens/mainnet/CK@2x.png differ diff --git a/resources/images/tokens/mainnet/CK@3x.png b/resources/images/tokens/mainnet/CK@3x.png old mode 100755 new mode 100644 index 9557561bb3e..7508bf71f85 Binary files a/resources/images/tokens/mainnet/CK@3x.png and b/resources/images/tokens/mainnet/CK@3x.png differ diff --git a/resources/images/tokens/mainnet/CND.png b/resources/images/tokens/mainnet/CND.png index 6a6f7788613..8c4b255bec4 100644 Binary files a/resources/images/tokens/mainnet/CND.png and b/resources/images/tokens/mainnet/CND.png differ diff --git a/resources/images/tokens/mainnet/CND@2x.png b/resources/images/tokens/mainnet/CND@2x.png index 79911d2a2ae..f126dcb307f 100644 Binary files a/resources/images/tokens/mainnet/CND@2x.png and b/resources/images/tokens/mainnet/CND@2x.png differ diff --git a/resources/images/tokens/mainnet/CND@3x.png b/resources/images/tokens/mainnet/CND@3x.png index bc5ac8bda34..7c7c690bb9b 100644 Binary files a/resources/images/tokens/mainnet/CND@3x.png and b/resources/images/tokens/mainnet/CND@3x.png differ diff --git a/resources/images/tokens/mainnet/COB.png b/resources/images/tokens/mainnet/COB.png index 350a20f1cba..c2da194d4ed 100644 Binary files a/resources/images/tokens/mainnet/COB.png and b/resources/images/tokens/mainnet/COB.png differ diff --git a/resources/images/tokens/mainnet/COB@2x.png b/resources/images/tokens/mainnet/COB@2x.png index de3858e4a3e..93e7d47ba18 100644 Binary files a/resources/images/tokens/mainnet/COB@2x.png and b/resources/images/tokens/mainnet/COB@2x.png differ diff --git a/resources/images/tokens/mainnet/COB@3x.png b/resources/images/tokens/mainnet/COB@3x.png index 8562564d77a..8aadcccf7ad 100644 Binary files a/resources/images/tokens/mainnet/COB@3x.png and b/resources/images/tokens/mainnet/COB@3x.png differ diff --git a/resources/images/tokens/mainnet/COMP.png b/resources/images/tokens/mainnet/COMP.png new file mode 100644 index 00000000000..e2ca2671fe1 Binary files /dev/null and b/resources/images/tokens/mainnet/COMP.png differ diff --git a/resources/images/tokens/mainnet/COMP@2x.png b/resources/images/tokens/mainnet/COMP@2x.png new file mode 100644 index 00000000000..0820063a4de Binary files /dev/null and b/resources/images/tokens/mainnet/COMP@2x.png differ diff --git a/resources/images/tokens/mainnet/COMP@3x.png b/resources/images/tokens/mainnet/COMP@3x.png new file mode 100644 index 00000000000..049311dd80f Binary files /dev/null and b/resources/images/tokens/mainnet/COMP@3x.png differ diff --git a/resources/images/tokens/mainnet/CRV@2x.png b/resources/images/tokens/mainnet/CRV@2x.png new file mode 100644 index 00000000000..4afeaad3744 Binary files /dev/null and b/resources/images/tokens/mainnet/CRV@2x.png differ diff --git a/resources/images/tokens/mainnet/CRV@3x.png b/resources/images/tokens/mainnet/CRV@3x.png new file mode 100644 index 00000000000..a1a3f17fdb5 Binary files /dev/null and b/resources/images/tokens/mainnet/CRV@3x.png differ diff --git a/resources/images/tokens/mainnet/CVC.png b/resources/images/tokens/mainnet/CVC.png index 1e1b37771fb..d356826429d 100644 Binary files a/resources/images/tokens/mainnet/CVC.png and b/resources/images/tokens/mainnet/CVC.png differ diff --git a/resources/images/tokens/mainnet/CVC@2x.png b/resources/images/tokens/mainnet/CVC@2x.png index 92964cde840..4557aa67ab1 100644 Binary files a/resources/images/tokens/mainnet/CVC@2x.png and b/resources/images/tokens/mainnet/CVC@2x.png differ diff --git a/resources/images/tokens/mainnet/CVC@3x.png b/resources/images/tokens/mainnet/CVC@3x.png index 8c7548a35ab..ca1c3e161f9 100644 Binary files a/resources/images/tokens/mainnet/CVC@3x.png and b/resources/images/tokens/mainnet/CVC@3x.png differ diff --git a/resources/images/tokens/mainnet/Centra.png b/resources/images/tokens/mainnet/Centra.png index 687c32c0e70..34d21ee364c 100644 Binary files a/resources/images/tokens/mainnet/Centra.png and b/resources/images/tokens/mainnet/Centra.png differ diff --git a/resources/images/tokens/mainnet/Centra@2x.png b/resources/images/tokens/mainnet/Centra@2x.png index 27d6c2b6bae..3b3146c9474 100644 Binary files a/resources/images/tokens/mainnet/Centra@2x.png and b/resources/images/tokens/mainnet/Centra@2x.png differ diff --git a/resources/images/tokens/mainnet/Centra@3x.png b/resources/images/tokens/mainnet/Centra@3x.png index 40a57118e76..c7c9379375c 100644 Binary files a/resources/images/tokens/mainnet/Centra@3x.png and b/resources/images/tokens/mainnet/Centra@3x.png differ diff --git a/resources/images/tokens/mainnet/DAI.png b/resources/images/tokens/mainnet/DAI.png deleted file mode 100644 index d7985b9157a..00000000000 Binary files a/resources/images/tokens/mainnet/DAI.png and /dev/null differ diff --git a/resources/images/tokens/mainnet/DAI@2x.png b/resources/images/tokens/mainnet/DAI@2x.png index fd9136fd0cf..ff5a8b76b6f 100644 Binary files a/resources/images/tokens/mainnet/DAI@2x.png and b/resources/images/tokens/mainnet/DAI@2x.png differ diff --git a/resources/images/tokens/mainnet/DAI@3x.png b/resources/images/tokens/mainnet/DAI@3x.png index 02f1c014c02..87a2aff9549 100644 Binary files a/resources/images/tokens/mainnet/DAI@3x.png and b/resources/images/tokens/mainnet/DAI@3x.png differ diff --git a/resources/images/tokens/mainnet/DAT.png b/resources/images/tokens/mainnet/DAT.png new file mode 100644 index 00000000000..df07014ce2f Binary files /dev/null and b/resources/images/tokens/mainnet/DAT.png differ diff --git a/resources/images/tokens/mainnet/DAT@2x.png b/resources/images/tokens/mainnet/DAT@2x.png new file mode 100644 index 00000000000..335202de708 Binary files /dev/null and b/resources/images/tokens/mainnet/DAT@2x.png differ diff --git a/resources/images/tokens/mainnet/DAT@3x.png b/resources/images/tokens/mainnet/DAT@3x.png new file mode 100644 index 00000000000..511e12f5487 Binary files /dev/null and b/resources/images/tokens/mainnet/DAT@3x.png differ diff --git a/resources/images/tokens/mainnet/DATA.png b/resources/images/tokens/mainnet/DATA.png index 37627979288..8f7ad1c8e68 100644 Binary files a/resources/images/tokens/mainnet/DATA.png and b/resources/images/tokens/mainnet/DATA.png differ diff --git a/resources/images/tokens/mainnet/DATA@2x.png b/resources/images/tokens/mainnet/DATA@2x.png index d843eae637a..a37eb81b57d 100644 Binary files a/resources/images/tokens/mainnet/DATA@2x.png and b/resources/images/tokens/mainnet/DATA@2x.png differ diff --git a/resources/images/tokens/mainnet/DATA@3x.png b/resources/images/tokens/mainnet/DATA@3x.png index 5ebe7913674..b7de819d6fb 100644 Binary files a/resources/images/tokens/mainnet/DATA@3x.png and b/resources/images/tokens/mainnet/DATA@3x.png differ diff --git a/resources/images/tokens/mainnet/DCN.png b/resources/images/tokens/mainnet/DCN.png index 881587757c8..eeb8879b2e0 100644 Binary files a/resources/images/tokens/mainnet/DCN.png and b/resources/images/tokens/mainnet/DCN.png differ diff --git a/resources/images/tokens/mainnet/DCN@2x.png b/resources/images/tokens/mainnet/DCN@2x.png index 91a6d2afa21..4379aef92c9 100644 Binary files a/resources/images/tokens/mainnet/DCN@2x.png and b/resources/images/tokens/mainnet/DCN@2x.png differ diff --git a/resources/images/tokens/mainnet/DCN@3x.png b/resources/images/tokens/mainnet/DCN@3x.png index e32e9c5e7c9..d03c9313a7c 100644 Binary files a/resources/images/tokens/mainnet/DCN@3x.png and b/resources/images/tokens/mainnet/DCN@3x.png differ diff --git a/resources/images/tokens/mainnet/DGD.png b/resources/images/tokens/mainnet/DGD.png index 70b956e0d86..04aca123214 100644 Binary files a/resources/images/tokens/mainnet/DGD.png and b/resources/images/tokens/mainnet/DGD.png differ diff --git a/resources/images/tokens/mainnet/DGD@2x.png b/resources/images/tokens/mainnet/DGD@2x.png index d7b8feb6b1f..2b347143c76 100644 Binary files a/resources/images/tokens/mainnet/DGD@2x.png and b/resources/images/tokens/mainnet/DGD@2x.png differ diff --git a/resources/images/tokens/mainnet/DGD@3x.png b/resources/images/tokens/mainnet/DGD@3x.png index 767dbb3c4b4..bbc19815fda 100644 Binary files a/resources/images/tokens/mainnet/DGD@3x.png and b/resources/images/tokens/mainnet/DGD@3x.png differ diff --git a/resources/images/tokens/mainnet/DGX.png b/resources/images/tokens/mainnet/DGX.png new file mode 100644 index 00000000000..6f45ee702ba Binary files /dev/null and b/resources/images/tokens/mainnet/DGX.png differ diff --git a/resources/images/tokens/mainnet/DGX@2x.png b/resources/images/tokens/mainnet/DGX@2x.png new file mode 100644 index 00000000000..24d3150d714 Binary files /dev/null and b/resources/images/tokens/mainnet/DGX@2x.png differ diff --git a/resources/images/tokens/mainnet/DGX@3x.png b/resources/images/tokens/mainnet/DGX@3x.png new file mode 100644 index 00000000000..252463f5011 Binary files /dev/null and b/resources/images/tokens/mainnet/DGX@3x.png differ diff --git a/resources/images/tokens/mainnet/DLT.png b/resources/images/tokens/mainnet/DLT.png index 059e0a20eae..fcb92d64b73 100644 Binary files a/resources/images/tokens/mainnet/DLT.png and b/resources/images/tokens/mainnet/DLT.png differ diff --git a/resources/images/tokens/mainnet/DLT@2x.png b/resources/images/tokens/mainnet/DLT@2x.png index eb7892a7869..abf05a60f4e 100644 Binary files a/resources/images/tokens/mainnet/DLT@2x.png and b/resources/images/tokens/mainnet/DLT@2x.png differ diff --git a/resources/images/tokens/mainnet/DLT@3x.png b/resources/images/tokens/mainnet/DLT@3x.png index 821fa8787da..56f99e1f7cf 100644 Binary files a/resources/images/tokens/mainnet/DLT@3x.png and b/resources/images/tokens/mainnet/DLT@3x.png differ diff --git a/resources/images/tokens/mainnet/DNT.png b/resources/images/tokens/mainnet/DNT.png index bc953314d03..5ce90850779 100644 Binary files a/resources/images/tokens/mainnet/DNT.png and b/resources/images/tokens/mainnet/DNT.png differ diff --git a/resources/images/tokens/mainnet/DNT@2x.png b/resources/images/tokens/mainnet/DNT@2x.png index ce59a606b70..fe7d042adde 100644 Binary files a/resources/images/tokens/mainnet/DNT@2x.png and b/resources/images/tokens/mainnet/DNT@2x.png differ diff --git a/resources/images/tokens/mainnet/DNT@3x.png b/resources/images/tokens/mainnet/DNT@3x.png index 5488be2f603..67f078fa80b 100644 Binary files a/resources/images/tokens/mainnet/DNT@3x.png and b/resources/images/tokens/mainnet/DNT@3x.png differ diff --git a/resources/images/tokens/mainnet/DPY.png b/resources/images/tokens/mainnet/DPY.png index 1ffe8bf2dbd..a977cf0c765 100644 Binary files a/resources/images/tokens/mainnet/DPY.png and b/resources/images/tokens/mainnet/DPY.png differ diff --git a/resources/images/tokens/mainnet/DPY@2x.png b/resources/images/tokens/mainnet/DPY@2x.png index 458cd34fbbe..4f9544e317d 100644 Binary files a/resources/images/tokens/mainnet/DPY@2x.png and b/resources/images/tokens/mainnet/DPY@2x.png differ diff --git a/resources/images/tokens/mainnet/DPY@3x.png b/resources/images/tokens/mainnet/DPY@3x.png index 9951b996fdb..23c9aee6f7d 100644 Binary files a/resources/images/tokens/mainnet/DPY@3x.png and b/resources/images/tokens/mainnet/DPY@3x.png differ diff --git a/resources/images/tokens/mainnet/DRT.png b/resources/images/tokens/mainnet/DRT.png index 5a30b3b7626..a4e8e29b04e 100644 Binary files a/resources/images/tokens/mainnet/DRT.png and b/resources/images/tokens/mainnet/DRT.png differ diff --git a/resources/images/tokens/mainnet/DRT@2x.png b/resources/images/tokens/mainnet/DRT@2x.png index e8c0f0bfc59..02340a3848e 100644 Binary files a/resources/images/tokens/mainnet/DRT@2x.png and b/resources/images/tokens/mainnet/DRT@2x.png differ diff --git a/resources/images/tokens/mainnet/DRT@3x.png b/resources/images/tokens/mainnet/DRT@3x.png index 52c9f01bbed..1ef8284633b 100644 Binary files a/resources/images/tokens/mainnet/DRT@3x.png and b/resources/images/tokens/mainnet/DRT@3x.png differ diff --git a/resources/images/tokens/mainnet/DTA.png b/resources/images/tokens/mainnet/DTA.png new file mode 100644 index 00000000000..f1f6f75de6b Binary files /dev/null and b/resources/images/tokens/mainnet/DTA.png differ diff --git a/resources/images/tokens/mainnet/DTA@2x.png b/resources/images/tokens/mainnet/DTA@2x.png new file mode 100644 index 00000000000..b5b7d504a23 Binary files /dev/null and b/resources/images/tokens/mainnet/DTA@2x.png differ diff --git a/resources/images/tokens/mainnet/DTA@3x.png b/resources/images/tokens/mainnet/DTA@3x.png new file mode 100644 index 00000000000..e422ac24287 Binary files /dev/null and b/resources/images/tokens/mainnet/DTA@3x.png differ diff --git a/resources/images/tokens/mainnet/EDG.png b/resources/images/tokens/mainnet/EDG.png index 6563f8222f4..881d716cb53 100644 Binary files a/resources/images/tokens/mainnet/EDG.png and b/resources/images/tokens/mainnet/EDG.png differ diff --git a/resources/images/tokens/mainnet/EDG@2x.png b/resources/images/tokens/mainnet/EDG@2x.png index cbf0387851b..5ffd704fda9 100644 Binary files a/resources/images/tokens/mainnet/EDG@2x.png and b/resources/images/tokens/mainnet/EDG@2x.png differ diff --git a/resources/images/tokens/mainnet/EDG@3x.png b/resources/images/tokens/mainnet/EDG@3x.png index 839939b5b9d..33cbf9d7ae1 100644 Binary files a/resources/images/tokens/mainnet/EDG@3x.png and b/resources/images/tokens/mainnet/EDG@3x.png differ diff --git a/resources/images/tokens/mainnet/EDO.png b/resources/images/tokens/mainnet/EDO.png index ee94e8ab0e2..548422d8157 100644 Binary files a/resources/images/tokens/mainnet/EDO.png and b/resources/images/tokens/mainnet/EDO.png differ diff --git a/resources/images/tokens/mainnet/EDO@2x.png b/resources/images/tokens/mainnet/EDO@2x.png index 80f922a8918..4735a790dfe 100644 Binary files a/resources/images/tokens/mainnet/EDO@2x.png and b/resources/images/tokens/mainnet/EDO@2x.png differ diff --git a/resources/images/tokens/mainnet/EDO@3x.png b/resources/images/tokens/mainnet/EDO@3x.png index 68a34fdf755..075bdd65e0c 100644 Binary files a/resources/images/tokens/mainnet/EDO@3x.png and b/resources/images/tokens/mainnet/EDO@3x.png differ diff --git a/resources/images/tokens/mainnet/EKG.png b/resources/images/tokens/mainnet/EKG.png new file mode 100644 index 00000000000..66588b67d59 Binary files /dev/null and b/resources/images/tokens/mainnet/EKG.png differ diff --git a/resources/images/tokens/mainnet/EKG@2x.png b/resources/images/tokens/mainnet/EKG@2x.png new file mode 100644 index 00000000000..7c773c2f7b2 Binary files /dev/null and b/resources/images/tokens/mainnet/EKG@2x.png differ diff --git a/resources/images/tokens/mainnet/EKG@3x.png b/resources/images/tokens/mainnet/EKG@3x.png new file mode 100644 index 00000000000..407ae48b935 Binary files /dev/null and b/resources/images/tokens/mainnet/EKG@3x.png differ diff --git a/resources/images/tokens/mainnet/EKO.png b/resources/images/tokens/mainnet/EKO.png new file mode 100644 index 00000000000..7b8a33d16c1 Binary files /dev/null and b/resources/images/tokens/mainnet/EKO.png differ diff --git a/resources/images/tokens/mainnet/EKO@2x.png b/resources/images/tokens/mainnet/EKO@2x.png new file mode 100644 index 00000000000..d1c90c4a1f2 Binary files /dev/null and b/resources/images/tokens/mainnet/EKO@2x.png differ diff --git a/resources/images/tokens/mainnet/EKO@3x.png b/resources/images/tokens/mainnet/EKO@3x.png new file mode 100644 index 00000000000..754880a2d0e Binary files /dev/null and b/resources/images/tokens/mainnet/EKO@3x.png differ diff --git a/resources/images/tokens/mainnet/ELF.png b/resources/images/tokens/mainnet/ELF.png new file mode 100644 index 00000000000..c4f832cc6e0 Binary files /dev/null and b/resources/images/tokens/mainnet/ELF.png differ diff --git a/resources/images/tokens/mainnet/ELF@2x.png b/resources/images/tokens/mainnet/ELF@2x.png new file mode 100644 index 00000000000..aba52959bb7 Binary files /dev/null and b/resources/images/tokens/mainnet/ELF@2x.png differ diff --git a/resources/images/tokens/mainnet/ELF@3x.png b/resources/images/tokens/mainnet/ELF@3x.png new file mode 100644 index 00000000000..ac2ce095830 Binary files /dev/null and b/resources/images/tokens/mainnet/ELF@3x.png differ diff --git a/resources/images/tokens/mainnet/EMONA.png b/resources/images/tokens/mainnet/EMONA.png old mode 100755 new mode 100644 index 20b6229589c..2b2c51d6950 Binary files a/resources/images/tokens/mainnet/EMONA.png and b/resources/images/tokens/mainnet/EMONA.png differ diff --git a/resources/images/tokens/mainnet/EMONA@2x.png b/resources/images/tokens/mainnet/EMONA@2x.png old mode 100755 new mode 100644 index f3dc12c4a10..290d6b16fe9 Binary files a/resources/images/tokens/mainnet/EMONA@2x.png and b/resources/images/tokens/mainnet/EMONA@2x.png differ diff --git a/resources/images/tokens/mainnet/EMONA@3x.png b/resources/images/tokens/mainnet/EMONA@3x.png old mode 100755 new mode 100644 index 4c4a794338e..5d57889716e Binary files a/resources/images/tokens/mainnet/EMONA@3x.png and b/resources/images/tokens/mainnet/EMONA@3x.png differ diff --git a/resources/images/tokens/mainnet/ENG.png b/resources/images/tokens/mainnet/ENG.png index 3cb39c93bbf..f4a71c5ed19 100644 Binary files a/resources/images/tokens/mainnet/ENG.png and b/resources/images/tokens/mainnet/ENG.png differ diff --git a/resources/images/tokens/mainnet/ENG@2x.png b/resources/images/tokens/mainnet/ENG@2x.png index f94566b42df..5fb8d4e3d3c 100644 Binary files a/resources/images/tokens/mainnet/ENG@2x.png and b/resources/images/tokens/mainnet/ENG@2x.png differ diff --git a/resources/images/tokens/mainnet/ENG@3x.png b/resources/images/tokens/mainnet/ENG@3x.png index b8b5de0a86c..3e5d679b9ba 100644 Binary files a/resources/images/tokens/mainnet/ENG@3x.png and b/resources/images/tokens/mainnet/ENG@3x.png differ diff --git a/resources/images/tokens/mainnet/ENJ.png b/resources/images/tokens/mainnet/ENJ.png new file mode 100644 index 00000000000..f02841ef060 Binary files /dev/null and b/resources/images/tokens/mainnet/ENJ.png differ diff --git a/resources/images/tokens/mainnet/ENJ@2x.png b/resources/images/tokens/mainnet/ENJ@2x.png new file mode 100644 index 00000000000..f07cc9e887c Binary files /dev/null and b/resources/images/tokens/mainnet/ENJ@2x.png differ diff --git a/resources/images/tokens/mainnet/ENJ@3x.png b/resources/images/tokens/mainnet/ENJ@3x.png new file mode 100644 index 00000000000..01fbd812157 Binary files /dev/null and b/resources/images/tokens/mainnet/ENJ@3x.png differ diff --git a/resources/images/tokens/mainnet/ENS.png b/resources/images/tokens/mainnet/ENS.png new file mode 100644 index 00000000000..18480fcb0d6 Binary files /dev/null and b/resources/images/tokens/mainnet/ENS.png differ diff --git a/resources/images/tokens/mainnet/EOS.png b/resources/images/tokens/mainnet/EOS.png index 4b5f3ae4d95..80ce8bc263d 100644 Binary files a/resources/images/tokens/mainnet/EOS.png and b/resources/images/tokens/mainnet/EOS.png differ diff --git a/resources/images/tokens/mainnet/EOS@2x.png b/resources/images/tokens/mainnet/EOS@2x.png index 2ee5b5865ef..10a5e64dcb6 100644 Binary files a/resources/images/tokens/mainnet/EOS@2x.png and b/resources/images/tokens/mainnet/EOS@2x.png differ diff --git a/resources/images/tokens/mainnet/EOS@3x.png b/resources/images/tokens/mainnet/EOS@3x.png index c17d7374e5c..b431ecaa4dc 100644 Binary files a/resources/images/tokens/mainnet/EOS@3x.png and b/resources/images/tokens/mainnet/EOS@3x.png differ diff --git a/resources/images/tokens/mainnet/EQUAD.png b/resources/images/tokens/mainnet/EQUAD.png new file mode 100644 index 00000000000..9e2aacfb7af Binary files /dev/null and b/resources/images/tokens/mainnet/EQUAD.png differ diff --git a/resources/images/tokens/mainnet/EQUAD@2x.png b/resources/images/tokens/mainnet/EQUAD@2x.png new file mode 100644 index 00000000000..69bd933d55e Binary files /dev/null and b/resources/images/tokens/mainnet/EQUAD@2x.png differ diff --git a/resources/images/tokens/mainnet/EQUAD@3x.png b/resources/images/tokens/mainnet/EQUAD@3x.png new file mode 100644 index 00000000000..57191048712 Binary files /dev/null and b/resources/images/tokens/mainnet/EQUAD@3x.png differ diff --git a/resources/images/tokens/mainnet/ETH2x-FLI.png b/resources/images/tokens/mainnet/ETH2x-FLI.png new file mode 100644 index 00000000000..a03328b868a Binary files /dev/null and b/resources/images/tokens/mainnet/ETH2x-FLI.png differ diff --git a/resources/images/tokens/mainnet/ETH2x-FLI@2x.png b/resources/images/tokens/mainnet/ETH2x-FLI@2x.png new file mode 100644 index 00000000000..073c74a4fd6 Binary files /dev/null and b/resources/images/tokens/mainnet/ETH2x-FLI@2x.png differ diff --git a/resources/images/tokens/mainnet/ETH2x-FLI@3x.png b/resources/images/tokens/mainnet/ETH2x-FLI@3x.png new file mode 100644 index 00000000000..714f8df1f58 Binary files /dev/null and b/resources/images/tokens/mainnet/ETH2x-FLI@3x.png differ diff --git a/resources/images/tokens/mainnet/ETH@2x.png b/resources/images/tokens/mainnet/ETH@2x.png new file mode 100644 index 00000000000..2fbda6722fe Binary files /dev/null and b/resources/images/tokens/mainnet/ETH@2x.png differ diff --git a/resources/images/tokens/mainnet/ETH@3x.png b/resources/images/tokens/mainnet/ETH@3x.png new file mode 100644 index 00000000000..30279c4dd20 Binary files /dev/null and b/resources/images/tokens/mainnet/ETH@3x.png differ diff --git a/resources/images/tokens/mainnet/ETHOS.png b/resources/images/tokens/mainnet/ETHOS.png index 5116a3929c5..c55d79ac0c0 100644 Binary files a/resources/images/tokens/mainnet/ETHOS.png and b/resources/images/tokens/mainnet/ETHOS.png differ diff --git a/resources/images/tokens/mainnet/ETHOS@2x.png b/resources/images/tokens/mainnet/ETHOS@2x.png index 33ce81506db..a00efd586c0 100644 Binary files a/resources/images/tokens/mainnet/ETHOS@2x.png and b/resources/images/tokens/mainnet/ETHOS@2x.png differ diff --git a/resources/images/tokens/mainnet/ETHOS@3x.png b/resources/images/tokens/mainnet/ETHOS@3x.png index 31a9722ba2e..d675ca8b637 100644 Binary files a/resources/images/tokens/mainnet/ETHOS@3x.png and b/resources/images/tokens/mainnet/ETHOS@3x.png differ diff --git a/resources/images/tokens/mainnet/EVX.png b/resources/images/tokens/mainnet/EVX.png index 12252094ea0..55b7ba8ef45 100644 Binary files a/resources/images/tokens/mainnet/EVX.png and b/resources/images/tokens/mainnet/EVX.png differ diff --git a/resources/images/tokens/mainnet/EVX@2x.png b/resources/images/tokens/mainnet/EVX@2x.png index 3d9d0cb3c04..f43e590f651 100644 Binary files a/resources/images/tokens/mainnet/EVX@2x.png and b/resources/images/tokens/mainnet/EVX@2x.png differ diff --git a/resources/images/tokens/mainnet/EVX@3x.png b/resources/images/tokens/mainnet/EVX@3x.png index 998178508df..7e403ccec3a 100644 Binary files a/resources/images/tokens/mainnet/EVX@3x.png and b/resources/images/tokens/mainnet/EVX@3x.png differ diff --git a/resources/images/tokens/mainnet/FUEL.png b/resources/images/tokens/mainnet/FUEL.png index f11895a26d8..06973200812 100644 Binary files a/resources/images/tokens/mainnet/FUEL.png and b/resources/images/tokens/mainnet/FUEL.png differ diff --git a/resources/images/tokens/mainnet/FUEL@2x.png b/resources/images/tokens/mainnet/FUEL@2x.png index 25a84fc3215..bbfb0ece59a 100644 Binary files a/resources/images/tokens/mainnet/FUEL@2x.png and b/resources/images/tokens/mainnet/FUEL@2x.png differ diff --git a/resources/images/tokens/mainnet/FUEL@3x.png b/resources/images/tokens/mainnet/FUEL@3x.png index 5b746194bc8..9c1de4fa327 100644 Binary files a/resources/images/tokens/mainnet/FUEL@3x.png and b/resources/images/tokens/mainnet/FUEL@3x.png differ diff --git a/resources/images/tokens/mainnet/FUN.png b/resources/images/tokens/mainnet/FUN.png index 5b6f2361637..e225aa35a1b 100644 Binary files a/resources/images/tokens/mainnet/FUN.png and b/resources/images/tokens/mainnet/FUN.png differ diff --git a/resources/images/tokens/mainnet/FUN@2x.png b/resources/images/tokens/mainnet/FUN@2x.png index e62f23ed039..09aa6e16056 100644 Binary files a/resources/images/tokens/mainnet/FUN@2x.png and b/resources/images/tokens/mainnet/FUN@2x.png differ diff --git a/resources/images/tokens/mainnet/FUN@3x.png b/resources/images/tokens/mainnet/FUN@3x.png index ee6935738c6..943b00642c9 100644 Binary files a/resources/images/tokens/mainnet/FUN@3x.png and b/resources/images/tokens/mainnet/FUN@3x.png differ diff --git a/resources/images/tokens/mainnet/FXC.png b/resources/images/tokens/mainnet/FXC.png new file mode 100644 index 00000000000..9fb73a64916 Binary files /dev/null and b/resources/images/tokens/mainnet/FXC.png differ diff --git a/resources/images/tokens/mainnet/FXC@2x.png b/resources/images/tokens/mainnet/FXC@2x.png new file mode 100644 index 00000000000..d9437ecc505 Binary files /dev/null and b/resources/images/tokens/mainnet/FXC@2x.png differ diff --git a/resources/images/tokens/mainnet/FXC@3x.png b/resources/images/tokens/mainnet/FXC@3x.png new file mode 100644 index 00000000000..95974e326a1 Binary files /dev/null and b/resources/images/tokens/mainnet/FXC@3x.png differ diff --git a/resources/images/tokens/mainnet/GDC.png b/resources/images/tokens/mainnet/GDC.png new file mode 100644 index 00000000000..ed1091d97db Binary files /dev/null and b/resources/images/tokens/mainnet/GDC.png differ diff --git a/resources/images/tokens/mainnet/GDC@2x.png b/resources/images/tokens/mainnet/GDC@2x.png new file mode 100644 index 00000000000..719cb63d26d Binary files /dev/null and b/resources/images/tokens/mainnet/GDC@2x.png differ diff --git a/resources/images/tokens/mainnet/GDC@3x.png b/resources/images/tokens/mainnet/GDC@3x.png new file mode 100644 index 00000000000..464c42858b1 Binary files /dev/null and b/resources/images/tokens/mainnet/GDC@3x.png differ diff --git a/resources/images/tokens/mainnet/GEN.png b/resources/images/tokens/mainnet/GEN.png new file mode 100644 index 00000000000..dfa69de17e8 Binary files /dev/null and b/resources/images/tokens/mainnet/GEN.png differ diff --git a/resources/images/tokens/mainnet/GEN@2x.png b/resources/images/tokens/mainnet/GEN@2x.png new file mode 100644 index 00000000000..af421063f2c Binary files /dev/null and b/resources/images/tokens/mainnet/GEN@2x.png differ diff --git a/resources/images/tokens/mainnet/GEN@3x.png b/resources/images/tokens/mainnet/GEN@3x.png new file mode 100644 index 00000000000..37f37b2fc2d Binary files /dev/null and b/resources/images/tokens/mainnet/GEN@3x.png differ diff --git a/resources/images/tokens/mainnet/GLM.png b/resources/images/tokens/mainnet/GLM.png new file mode 100644 index 00000000000..e62917a4829 Binary files /dev/null and b/resources/images/tokens/mainnet/GLM.png differ diff --git a/resources/images/tokens/mainnet/GLM@2x.png b/resources/images/tokens/mainnet/GLM@2x.png new file mode 100644 index 00000000000..7083cfef9dc Binary files /dev/null and b/resources/images/tokens/mainnet/GLM@2x.png differ diff --git a/resources/images/tokens/mainnet/GLM@3x.png b/resources/images/tokens/mainnet/GLM@3x.png new file mode 100644 index 00000000000..08897a3a569 Binary files /dev/null and b/resources/images/tokens/mainnet/GLM@3x.png differ diff --git a/resources/images/tokens/mainnet/GNO.png b/resources/images/tokens/mainnet/GNO.png index 3f35f00d429..49d73889957 100644 Binary files a/resources/images/tokens/mainnet/GNO.png and b/resources/images/tokens/mainnet/GNO.png differ diff --git a/resources/images/tokens/mainnet/GNO@2x.png b/resources/images/tokens/mainnet/GNO@2x.png index 15226acf0fa..47e72ce4f81 100644 Binary files a/resources/images/tokens/mainnet/GNO@2x.png and b/resources/images/tokens/mainnet/GNO@2x.png differ diff --git a/resources/images/tokens/mainnet/GNO@3x.png b/resources/images/tokens/mainnet/GNO@3x.png index 6f5262b987c..6dbc6138a74 100644 Binary files a/resources/images/tokens/mainnet/GNO@3x.png and b/resources/images/tokens/mainnet/GNO@3x.png differ diff --git a/resources/images/tokens/mainnet/GNT.png b/resources/images/tokens/mainnet/GNT.png index 50fa92a3d7e..e62917a4829 100644 Binary files a/resources/images/tokens/mainnet/GNT.png and b/resources/images/tokens/mainnet/GNT.png differ diff --git a/resources/images/tokens/mainnet/GNT@2x.png b/resources/images/tokens/mainnet/GNT@2x.png index 737d3dce3f9..7083cfef9dc 100644 Binary files a/resources/images/tokens/mainnet/GNT@2x.png and b/resources/images/tokens/mainnet/GNT@2x.png differ diff --git a/resources/images/tokens/mainnet/GNT@3x.png b/resources/images/tokens/mainnet/GNT@3x.png index 44d0c41aa6e..08897a3a569 100644 Binary files a/resources/images/tokens/mainnet/GNT@3x.png and b/resources/images/tokens/mainnet/GNT@3x.png differ diff --git a/resources/images/tokens/mainnet/GRID.png b/resources/images/tokens/mainnet/GRID.png index bb4019969d7..0800d72e858 100644 Binary files a/resources/images/tokens/mainnet/GRID.png and b/resources/images/tokens/mainnet/GRID.png differ diff --git a/resources/images/tokens/mainnet/GRID@2x.png b/resources/images/tokens/mainnet/GRID@2x.png index 613f799ee4e..6ddda0b9d94 100644 Binary files a/resources/images/tokens/mainnet/GRID@2x.png and b/resources/images/tokens/mainnet/GRID@2x.png differ diff --git a/resources/images/tokens/mainnet/GRID@3x.png b/resources/images/tokens/mainnet/GRID@3x.png index 1271ce57e29..93ed82bfbe4 100644 Binary files a/resources/images/tokens/mainnet/GRID@3x.png and b/resources/images/tokens/mainnet/GRID@3x.png differ diff --git a/resources/images/tokens/mainnet/GRT.png b/resources/images/tokens/mainnet/GRT.png new file mode 100644 index 00000000000..bcefbc76658 Binary files /dev/null and b/resources/images/tokens/mainnet/GRT.png differ diff --git a/resources/images/tokens/mainnet/GRT@2x.png b/resources/images/tokens/mainnet/GRT@2x.png new file mode 100644 index 00000000000..9cbb487ce12 Binary files /dev/null and b/resources/images/tokens/mainnet/GRT@2x.png differ diff --git a/resources/images/tokens/mainnet/HEZ.png b/resources/images/tokens/mainnet/HEZ.png new file mode 100644 index 00000000000..a2338e51da0 Binary files /dev/null and b/resources/images/tokens/mainnet/HEZ.png differ diff --git a/resources/images/tokens/mainnet/HST.png b/resources/images/tokens/mainnet/HST.png index eade6afd251..432de8bc10b 100644 Binary files a/resources/images/tokens/mainnet/HST.png and b/resources/images/tokens/mainnet/HST.png differ diff --git a/resources/images/tokens/mainnet/HST@2x.png b/resources/images/tokens/mainnet/HST@2x.png index 9a97cba044a..5ea8055e7ed 100644 Binary files a/resources/images/tokens/mainnet/HST@2x.png and b/resources/images/tokens/mainnet/HST@2x.png differ diff --git a/resources/images/tokens/mainnet/HST@3x.png b/resources/images/tokens/mainnet/HST@3x.png index 10a8018c59c..cf397544e59 100644 Binary files a/resources/images/tokens/mainnet/HST@3x.png and b/resources/images/tokens/mainnet/HST@3x.png differ diff --git a/resources/images/tokens/mainnet/HT.png b/resources/images/tokens/mainnet/HT.png new file mode 100644 index 00000000000..a4c087df8d4 Binary files /dev/null and b/resources/images/tokens/mainnet/HT.png differ diff --git a/resources/images/tokens/mainnet/HT@2x.png b/resources/images/tokens/mainnet/HT@2x.png new file mode 100644 index 00000000000..8502b1f863a Binary files /dev/null and b/resources/images/tokens/mainnet/HT@2x.png differ diff --git a/resources/images/tokens/mainnet/HT@3x.png b/resources/images/tokens/mainnet/HT@3x.png new file mode 100644 index 00000000000..eb96404c24a Binary files /dev/null and b/resources/images/tokens/mainnet/HT@3x.png differ diff --git a/resources/images/tokens/mainnet/ICN.png b/resources/images/tokens/mainnet/ICN.png index 585ec9e7ced..d50534a6a30 100644 Binary files a/resources/images/tokens/mainnet/ICN.png and b/resources/images/tokens/mainnet/ICN.png differ diff --git a/resources/images/tokens/mainnet/ICN@2x.png b/resources/images/tokens/mainnet/ICN@2x.png index 7285df901e8..2f5320c5814 100644 Binary files a/resources/images/tokens/mainnet/ICN@2x.png and b/resources/images/tokens/mainnet/ICN@2x.png differ diff --git a/resources/images/tokens/mainnet/ICN@3x.png b/resources/images/tokens/mainnet/ICN@3x.png index baf30781193..75db8190284 100644 Binary files a/resources/images/tokens/mainnet/ICN@3x.png and b/resources/images/tokens/mainnet/ICN@3x.png differ diff --git a/resources/images/tokens/mainnet/ICOS.png b/resources/images/tokens/mainnet/ICOS.png index a77120d930b..b822a5bbd82 100644 Binary files a/resources/images/tokens/mainnet/ICOS.png and b/resources/images/tokens/mainnet/ICOS.png differ diff --git a/resources/images/tokens/mainnet/ICOS@2x.png b/resources/images/tokens/mainnet/ICOS@2x.png index 1f069c3e5fd..16bba6b6f61 100644 Binary files a/resources/images/tokens/mainnet/ICOS@2x.png and b/resources/images/tokens/mainnet/ICOS@2x.png differ diff --git a/resources/images/tokens/mainnet/ICOS@3x.png b/resources/images/tokens/mainnet/ICOS@3x.png index 2f9f8adc846..672fb9c8d56 100644 Binary files a/resources/images/tokens/mainnet/ICOS@3x.png and b/resources/images/tokens/mainnet/ICOS@3x.png differ diff --git a/resources/images/tokens/mainnet/IOST.png b/resources/images/tokens/mainnet/IOST.png new file mode 100644 index 00000000000..a26304765ac Binary files /dev/null and b/resources/images/tokens/mainnet/IOST.png differ diff --git a/resources/images/tokens/mainnet/IOST@2x.png b/resources/images/tokens/mainnet/IOST@2x.png new file mode 100644 index 00000000000..3267fb6cbb5 Binary files /dev/null and b/resources/images/tokens/mainnet/IOST@2x.png differ diff --git a/resources/images/tokens/mainnet/IOST@3x.png b/resources/images/tokens/mainnet/IOST@3x.png new file mode 100644 index 00000000000..db48463af00 Binary files /dev/null and b/resources/images/tokens/mainnet/IOST@3x.png differ diff --git a/resources/images/tokens/mainnet/KDO.png b/resources/images/tokens/mainnet/KDO.png new file mode 100644 index 00000000000..482fdb25012 Binary files /dev/null and b/resources/images/tokens/mainnet/KDO.png differ diff --git a/resources/images/tokens/mainnet/KDO@2x.png b/resources/images/tokens/mainnet/KDO@2x.png new file mode 100644 index 00000000000..b8d9c5a7472 Binary files /dev/null and b/resources/images/tokens/mainnet/KDO@2x.png differ diff --git a/resources/images/tokens/mainnet/KDO@3x.png b/resources/images/tokens/mainnet/KDO@3x.png new file mode 100644 index 00000000000..bde4fa25a44 Binary files /dev/null and b/resources/images/tokens/mainnet/KDO@3x.png differ diff --git a/resources/images/tokens/mainnet/KIN.png b/resources/images/tokens/mainnet/KIN.png index d3b57e83efe..4d256c371a2 100644 Binary files a/resources/images/tokens/mainnet/KIN.png and b/resources/images/tokens/mainnet/KIN.png differ diff --git a/resources/images/tokens/mainnet/KIN@2x.png b/resources/images/tokens/mainnet/KIN@2x.png index 77b30dca87f..5515bbf8120 100644 Binary files a/resources/images/tokens/mainnet/KIN@2x.png and b/resources/images/tokens/mainnet/KIN@2x.png differ diff --git a/resources/images/tokens/mainnet/KIN@3x.png b/resources/images/tokens/mainnet/KIN@3x.png index 763ace92547..707c64f26d7 100644 Binary files a/resources/images/tokens/mainnet/KIN@3x.png and b/resources/images/tokens/mainnet/KIN@3x.png differ diff --git a/resources/images/tokens/mainnet/KNC.png b/resources/images/tokens/mainnet/KNC.png index 7c0f2945e26..6e55f558c25 100644 Binary files a/resources/images/tokens/mainnet/KNC.png and b/resources/images/tokens/mainnet/KNC.png differ diff --git a/resources/images/tokens/mainnet/KNC@2x.png b/resources/images/tokens/mainnet/KNC@2x.png index 9dbf23479ae..9af92b54dc6 100644 Binary files a/resources/images/tokens/mainnet/KNC@2x.png and b/resources/images/tokens/mainnet/KNC@2x.png differ diff --git a/resources/images/tokens/mainnet/KNC@3x.png b/resources/images/tokens/mainnet/KNC@3x.png index 4d7dd17d8f5..3f2764aa17f 100644 Binary files a/resources/images/tokens/mainnet/KNC@3x.png and b/resources/images/tokens/mainnet/KNC@3x.png differ diff --git a/resources/images/tokens/mainnet/Kudos.png b/resources/images/tokens/mainnet/Kudos.png new file mode 100644 index 00000000000..962b4296881 Binary files /dev/null and b/resources/images/tokens/mainnet/Kudos.png differ diff --git a/resources/images/tokens/mainnet/Kudos@2x.png b/resources/images/tokens/mainnet/Kudos@2x.png new file mode 100644 index 00000000000..9d3faf6dc87 Binary files /dev/null and b/resources/images/tokens/mainnet/Kudos@2x.png differ diff --git a/resources/images/tokens/mainnet/Kudos@3x.png b/resources/images/tokens/mainnet/Kudos@3x.png new file mode 100644 index 00000000000..0c4b38fc6e2 Binary files /dev/null and b/resources/images/tokens/mainnet/Kudos@3x.png differ diff --git a/resources/images/tokens/mainnet/LEND.png b/resources/images/tokens/mainnet/LEND.png new file mode 100644 index 00000000000..510f5a3afe4 Binary files /dev/null and b/resources/images/tokens/mainnet/LEND.png differ diff --git a/resources/images/tokens/mainnet/LEND@2x.png b/resources/images/tokens/mainnet/LEND@2x.png new file mode 100644 index 00000000000..948c2f38257 Binary files /dev/null and b/resources/images/tokens/mainnet/LEND@2x.png differ diff --git a/resources/images/tokens/mainnet/LEND@3x.png b/resources/images/tokens/mainnet/LEND@3x.png new file mode 100644 index 00000000000..35d7ae6f71f Binary files /dev/null and b/resources/images/tokens/mainnet/LEND@3x.png differ diff --git a/resources/images/tokens/mainnet/LINK.png b/resources/images/tokens/mainnet/LINK.png index 37b4a9d88c6..fb166ead45e 100644 Binary files a/resources/images/tokens/mainnet/LINK.png and b/resources/images/tokens/mainnet/LINK.png differ diff --git a/resources/images/tokens/mainnet/LINK@2x.png b/resources/images/tokens/mainnet/LINK@2x.png index b25c0813c6d..a847aa22f2a 100644 Binary files a/resources/images/tokens/mainnet/LINK@2x.png and b/resources/images/tokens/mainnet/LINK@2x.png differ diff --git a/resources/images/tokens/mainnet/LINK@3x.png b/resources/images/tokens/mainnet/LINK@3x.png index 70800be6961..3adeef15605 100644 Binary files a/resources/images/tokens/mainnet/LINK@3x.png and b/resources/images/tokens/mainnet/LINK@3x.png differ diff --git a/resources/images/tokens/mainnet/LISK@2x.png b/resources/images/tokens/mainnet/LISK@2x.png index 01d884a7e08..d71838c10e0 100644 Binary files a/resources/images/tokens/mainnet/LISK@2x.png and b/resources/images/tokens/mainnet/LISK@2x.png differ diff --git a/resources/images/tokens/mainnet/LISK@3x.png b/resources/images/tokens/mainnet/LISK@3x.png index 79b91f43400..e8c9e70824e 100644 Binary files a/resources/images/tokens/mainnet/LISK@3x.png and b/resources/images/tokens/mainnet/LISK@3x.png differ diff --git a/resources/images/tokens/mainnet/LOOM.png b/resources/images/tokens/mainnet/LOOM.png new file mode 100644 index 00000000000..a2ac98237ff Binary files /dev/null and b/resources/images/tokens/mainnet/LOOM.png differ diff --git a/resources/images/tokens/mainnet/LOOM@2x.png b/resources/images/tokens/mainnet/LOOM@2x.png new file mode 100644 index 00000000000..898f4bb95a1 Binary files /dev/null and b/resources/images/tokens/mainnet/LOOM@2x.png differ diff --git a/resources/images/tokens/mainnet/LOOM@3x.png b/resources/images/tokens/mainnet/LOOM@3x.png new file mode 100644 index 00000000000..20dcbb59070 Binary files /dev/null and b/resources/images/tokens/mainnet/LOOM@3x.png differ diff --git a/resources/images/tokens/mainnet/LPT.png b/resources/images/tokens/mainnet/LPT.png new file mode 100644 index 00000000000..049df447e38 Binary files /dev/null and b/resources/images/tokens/mainnet/LPT.png differ diff --git a/resources/images/tokens/mainnet/LPT@2x.png b/resources/images/tokens/mainnet/LPT@2x.png new file mode 100644 index 00000000000..6746fdaf8cb Binary files /dev/null and b/resources/images/tokens/mainnet/LPT@2x.png differ diff --git a/resources/images/tokens/mainnet/LPT@3x.png b/resources/images/tokens/mainnet/LPT@3x.png new file mode 100644 index 00000000000..d7601bfbb07 Binary files /dev/null and b/resources/images/tokens/mainnet/LPT@3x.png differ diff --git a/resources/images/tokens/mainnet/LRC.png b/resources/images/tokens/mainnet/LRC.png index c981e6fb1c0..351222ddf00 100644 Binary files a/resources/images/tokens/mainnet/LRC.png and b/resources/images/tokens/mainnet/LRC.png differ diff --git a/resources/images/tokens/mainnet/LRC@2x.png b/resources/images/tokens/mainnet/LRC@2x.png index e3498f288c6..11356787218 100644 Binary files a/resources/images/tokens/mainnet/LRC@2x.png and b/resources/images/tokens/mainnet/LRC@2x.png differ diff --git a/resources/images/tokens/mainnet/LRC@3x.png b/resources/images/tokens/mainnet/LRC@3x.png index 566cfcbf10f..87a0869ecf0 100644 Binary files a/resources/images/tokens/mainnet/LRC@3x.png and b/resources/images/tokens/mainnet/LRC@3x.png differ diff --git a/resources/images/tokens/mainnet/MANA.png b/resources/images/tokens/mainnet/MANA.png index 12c7821bca5..499288dcc06 100644 Binary files a/resources/images/tokens/mainnet/MANA.png and b/resources/images/tokens/mainnet/MANA.png differ diff --git a/resources/images/tokens/mainnet/MANA@2x.png b/resources/images/tokens/mainnet/MANA@2x.png index 1f68b51017c..3b8017fc3a0 100644 Binary files a/resources/images/tokens/mainnet/MANA@2x.png and b/resources/images/tokens/mainnet/MANA@2x.png differ diff --git a/resources/images/tokens/mainnet/MANA@3x.png b/resources/images/tokens/mainnet/MANA@3x.png index 601b3aeafb8..26bba8ce040 100644 Binary files a/resources/images/tokens/mainnet/MANA@3x.png and b/resources/images/tokens/mainnet/MANA@3x.png differ diff --git a/resources/images/tokens/mainnet/MCO.png b/resources/images/tokens/mainnet/MCO.png index 2beb7ffbb8a..d0ec8be780f 100644 Binary files a/resources/images/tokens/mainnet/MCO.png and b/resources/images/tokens/mainnet/MCO.png differ diff --git a/resources/images/tokens/mainnet/MCO@2x.png b/resources/images/tokens/mainnet/MCO@2x.png index 8c17faad2ea..d046a255e09 100644 Binary files a/resources/images/tokens/mainnet/MCO@2x.png and b/resources/images/tokens/mainnet/MCO@2x.png differ diff --git a/resources/images/tokens/mainnet/MCO@3x.png b/resources/images/tokens/mainnet/MCO@3x.png index 38926d7a335..174e2a676c3 100644 Binary files a/resources/images/tokens/mainnet/MCO@3x.png and b/resources/images/tokens/mainnet/MCO@3x.png differ diff --git a/resources/images/tokens/mainnet/MDA.png b/resources/images/tokens/mainnet/MDA.png index 032c9686fd8..96f7951ea2f 100644 Binary files a/resources/images/tokens/mainnet/MDA.png and b/resources/images/tokens/mainnet/MDA.png differ diff --git a/resources/images/tokens/mainnet/MDA@2x.png b/resources/images/tokens/mainnet/MDA@2x.png index 026fadda6f3..61554a4a034 100644 Binary files a/resources/images/tokens/mainnet/MDA@2x.png and b/resources/images/tokens/mainnet/MDA@2x.png differ diff --git a/resources/images/tokens/mainnet/MDA@3x.png b/resources/images/tokens/mainnet/MDA@3x.png index 5aea76dbcf7..d5d9e883dc7 100644 Binary files a/resources/images/tokens/mainnet/MDA@3x.png and b/resources/images/tokens/mainnet/MDA@3x.png differ diff --git a/resources/images/tokens/mainnet/MET.png b/resources/images/tokens/mainnet/MET.png new file mode 100644 index 00000000000..e45be942cd4 Binary files /dev/null and b/resources/images/tokens/mainnet/MET.png differ diff --git a/resources/images/tokens/mainnet/MET@2x.png b/resources/images/tokens/mainnet/MET@2x.png new file mode 100644 index 00000000000..b6cfff83d47 Binary files /dev/null and b/resources/images/tokens/mainnet/MET@2x.png differ diff --git a/resources/images/tokens/mainnet/MET@3x.png b/resources/images/tokens/mainnet/MET@3x.png new file mode 100644 index 00000000000..7eaddad9550 Binary files /dev/null and b/resources/images/tokens/mainnet/MET@3x.png differ diff --git a/resources/images/tokens/mainnet/MFG.png b/resources/images/tokens/mainnet/MFG.png new file mode 100644 index 00000000000..955f71b2282 Binary files /dev/null and b/resources/images/tokens/mainnet/MFG.png differ diff --git a/resources/images/tokens/mainnet/MFG@2x.png b/resources/images/tokens/mainnet/MFG@2x.png new file mode 100644 index 00000000000..2aa06cebb28 Binary files /dev/null and b/resources/images/tokens/mainnet/MFG@2x.png differ diff --git a/resources/images/tokens/mainnet/MFG@3x.png b/resources/images/tokens/mainnet/MFG@3x.png new file mode 100644 index 00000000000..a0bd2a89224 Binary files /dev/null and b/resources/images/tokens/mainnet/MFG@3x.png differ diff --git a/resources/images/tokens/mainnet/MGO.png b/resources/images/tokens/mainnet/MGO.png index e1129be21b7..4b8495d2367 100644 Binary files a/resources/images/tokens/mainnet/MGO.png and b/resources/images/tokens/mainnet/MGO.png differ diff --git a/resources/images/tokens/mainnet/MGO@2x.png b/resources/images/tokens/mainnet/MGO@2x.png index becaaea91ca..0e7b14ae5f9 100644 Binary files a/resources/images/tokens/mainnet/MGO@2x.png and b/resources/images/tokens/mainnet/MGO@2x.png differ diff --git a/resources/images/tokens/mainnet/MGO@3x.png b/resources/images/tokens/mainnet/MGO@3x.png index 4f346b32044..92270bcd06d 100644 Binary files a/resources/images/tokens/mainnet/MGO@3x.png and b/resources/images/tokens/mainnet/MGO@3x.png differ diff --git a/resources/images/tokens/mainnet/MKR.png b/resources/images/tokens/mainnet/MKR.png new file mode 100644 index 00000000000..6be35c24870 Binary files /dev/null and b/resources/images/tokens/mainnet/MKR.png differ diff --git a/resources/images/tokens/mainnet/MKR@2x.png b/resources/images/tokens/mainnet/MKR@2x.png new file mode 100644 index 00000000000..0a3833e316f Binary files /dev/null and b/resources/images/tokens/mainnet/MKR@2x.png differ diff --git a/resources/images/tokens/mainnet/MKR@3x.png b/resources/images/tokens/mainnet/MKR@3x.png new file mode 100644 index 00000000000..ca1445cf685 Binary files /dev/null and b/resources/images/tokens/mainnet/MKR@3x.png differ diff --git a/resources/images/tokens/mainnet/MLN.png b/resources/images/tokens/mainnet/MLN.png index ff373e6fe96..b93d4662913 100644 Binary files a/resources/images/tokens/mainnet/MLN.png and b/resources/images/tokens/mainnet/MLN.png differ diff --git a/resources/images/tokens/mainnet/MLN@2x.png b/resources/images/tokens/mainnet/MLN@2x.png index e03b628cca6..95c37351fb8 100644 Binary files a/resources/images/tokens/mainnet/MLN@2x.png and b/resources/images/tokens/mainnet/MLN@2x.png differ diff --git a/resources/images/tokens/mainnet/MLN@3x.png b/resources/images/tokens/mainnet/MLN@3x.png index cc48a8ea4f9..34afa62ca0b 100644 Binary files a/resources/images/tokens/mainnet/MLN@3x.png and b/resources/images/tokens/mainnet/MLN@3x.png differ diff --git a/resources/images/tokens/mainnet/MOC.png b/resources/images/tokens/mainnet/MOC.png new file mode 100644 index 00000000000..66c547a24a6 Binary files /dev/null and b/resources/images/tokens/mainnet/MOC.png differ diff --git a/resources/images/tokens/mainnet/MOC@2x.png b/resources/images/tokens/mainnet/MOC@2x.png new file mode 100644 index 00000000000..c0c6508c462 Binary files /dev/null and b/resources/images/tokens/mainnet/MOC@2x.png differ diff --git a/resources/images/tokens/mainnet/MOC@3x.png b/resources/images/tokens/mainnet/MOC@3x.png new file mode 100644 index 00000000000..bc82159e31f Binary files /dev/null and b/resources/images/tokens/mainnet/MOC@3x.png differ diff --git a/resources/images/tokens/mainnet/MOD.png b/resources/images/tokens/mainnet/MOD.png index 318310cc164..90de67b96ba 100644 Binary files a/resources/images/tokens/mainnet/MOD.png and b/resources/images/tokens/mainnet/MOD.png differ diff --git a/resources/images/tokens/mainnet/MOD@2x.png b/resources/images/tokens/mainnet/MOD@2x.png index e2fa11c829f..226b912bab3 100644 Binary files a/resources/images/tokens/mainnet/MOD@2x.png and b/resources/images/tokens/mainnet/MOD@2x.png differ diff --git a/resources/images/tokens/mainnet/MOD@3x.png b/resources/images/tokens/mainnet/MOD@3x.png index 4a6f89df4b3..aca9baa6d96 100644 Binary files a/resources/images/tokens/mainnet/MOD@3x.png and b/resources/images/tokens/mainnet/MOD@3x.png differ diff --git a/resources/images/tokens/mainnet/MTH.png b/resources/images/tokens/mainnet/MTH.png index b84be27925d..03bd5752be0 100644 Binary files a/resources/images/tokens/mainnet/MTH.png and b/resources/images/tokens/mainnet/MTH.png differ diff --git a/resources/images/tokens/mainnet/MTH@2x.png b/resources/images/tokens/mainnet/MTH@2x.png index 7e2f397eb41..f6be85ca6cd 100644 Binary files a/resources/images/tokens/mainnet/MTH@2x.png and b/resources/images/tokens/mainnet/MTH@2x.png differ diff --git a/resources/images/tokens/mainnet/MTH@3x.png b/resources/images/tokens/mainnet/MTH@3x.png index 7db2630b689..dde9b3fcc40 100644 Binary files a/resources/images/tokens/mainnet/MTH@3x.png and b/resources/images/tokens/mainnet/MTH@3x.png differ diff --git a/resources/images/tokens/mainnet/MTL.png b/resources/images/tokens/mainnet/MTL.png new file mode 100644 index 00000000000..714213bf2dc Binary files /dev/null and b/resources/images/tokens/mainnet/MTL.png differ diff --git a/resources/images/tokens/mainnet/MTL@2x.png b/resources/images/tokens/mainnet/MTL@2x.png new file mode 100644 index 00000000000..725ff44417a Binary files /dev/null and b/resources/images/tokens/mainnet/MTL@2x.png differ diff --git a/resources/images/tokens/mainnet/MTL@3x.png b/resources/images/tokens/mainnet/MTL@3x.png new file mode 100644 index 00000000000..2424df9af5f Binary files /dev/null and b/resources/images/tokens/mainnet/MTL@3x.png differ diff --git a/resources/images/tokens/mainnet/MYB.png b/resources/images/tokens/mainnet/MYB.png new file mode 100644 index 00000000000..0bb1b8467d8 Binary files /dev/null and b/resources/images/tokens/mainnet/MYB.png differ diff --git a/resources/images/tokens/mainnet/MYB@2x.png b/resources/images/tokens/mainnet/MYB@2x.png new file mode 100644 index 00000000000..62a0a0787bc Binary files /dev/null and b/resources/images/tokens/mainnet/MYB@2x.png differ diff --git a/resources/images/tokens/mainnet/MYB@3x.png b/resources/images/tokens/mainnet/MYB@3x.png new file mode 100644 index 00000000000..8ada92212a3 Binary files /dev/null and b/resources/images/tokens/mainnet/MYB@3x.png differ diff --git a/resources/images/tokens/mainnet/NEXO.png b/resources/images/tokens/mainnet/NEXO.png new file mode 100644 index 00000000000..f627cd1149e Binary files /dev/null and b/resources/images/tokens/mainnet/NEXO.png differ diff --git a/resources/images/tokens/mainnet/NEXO@2x.png b/resources/images/tokens/mainnet/NEXO@2x.png new file mode 100644 index 00000000000..157663f0953 Binary files /dev/null and b/resources/images/tokens/mainnet/NEXO@2x.png differ diff --git a/resources/images/tokens/mainnet/NEXO@3x.png b/resources/images/tokens/mainnet/NEXO@3x.png new file mode 100644 index 00000000000..f32c21329d5 Binary files /dev/null and b/resources/images/tokens/mainnet/NEXO@3x.png differ diff --git a/resources/images/tokens/mainnet/NEXXO.png b/resources/images/tokens/mainnet/NEXXO.png new file mode 100644 index 00000000000..17298c83776 Binary files /dev/null and b/resources/images/tokens/mainnet/NEXXO.png differ diff --git a/resources/images/tokens/mainnet/NEXXO@2x.png b/resources/images/tokens/mainnet/NEXXO@2x.png new file mode 100644 index 00000000000..b3904a640ae Binary files /dev/null and b/resources/images/tokens/mainnet/NEXXO@2x.png differ diff --git a/resources/images/tokens/mainnet/NEXXO@3x.png b/resources/images/tokens/mainnet/NEXXO@3x.png new file mode 100644 index 00000000000..d098b7a62c3 Binary files /dev/null and b/resources/images/tokens/mainnet/NEXXO@3x.png differ diff --git a/resources/images/tokens/mainnet/NMR.png b/resources/images/tokens/mainnet/NMR.png index da7b92cd898..ffdcafd30ff 100644 Binary files a/resources/images/tokens/mainnet/NMR.png and b/resources/images/tokens/mainnet/NMR.png differ diff --git a/resources/images/tokens/mainnet/NMR@2x.png b/resources/images/tokens/mainnet/NMR@2x.png index 7b5674946c0..dc568ea653b 100644 Binary files a/resources/images/tokens/mainnet/NMR@2x.png and b/resources/images/tokens/mainnet/NMR@2x.png differ diff --git a/resources/images/tokens/mainnet/NMR@3x.png b/resources/images/tokens/mainnet/NMR@3x.png index fadb300cc41..3c31ed7d211 100644 Binary files a/resources/images/tokens/mainnet/NMR@3x.png and b/resources/images/tokens/mainnet/NMR@3x.png differ diff --git a/resources/images/tokens/mainnet/NPXS.png b/resources/images/tokens/mainnet/NPXS.png new file mode 100644 index 00000000000..d74e593a1b4 Binary files /dev/null and b/resources/images/tokens/mainnet/NPXS.png differ diff --git a/resources/images/tokens/mainnet/NPXS@2x.png b/resources/images/tokens/mainnet/NPXS@2x.png new file mode 100644 index 00000000000..94fdd23bdd9 Binary files /dev/null and b/resources/images/tokens/mainnet/NPXS@2x.png differ diff --git a/resources/images/tokens/mainnet/NPXS@3x.png b/resources/images/tokens/mainnet/NPXS@3x.png new file mode 100644 index 00000000000..71c6ba5b67a Binary files /dev/null and b/resources/images/tokens/mainnet/NPXS@3x.png differ diff --git a/resources/images/tokens/mainnet/OGN.png b/resources/images/tokens/mainnet/OGN.png new file mode 100644 index 00000000000..5b286f03122 Binary files /dev/null and b/resources/images/tokens/mainnet/OGN.png differ diff --git a/resources/images/tokens/mainnet/OGN@2x.png b/resources/images/tokens/mainnet/OGN@2x.png new file mode 100644 index 00000000000..41055a37496 Binary files /dev/null and b/resources/images/tokens/mainnet/OGN@2x.png differ diff --git a/resources/images/tokens/mainnet/OGN@3x.png b/resources/images/tokens/mainnet/OGN@3x.png new file mode 100644 index 00000000000..6091dfc9e96 Binary files /dev/null and b/resources/images/tokens/mainnet/OGN@3x.png differ diff --git a/resources/images/tokens/mainnet/OMG.png b/resources/images/tokens/mainnet/OMG.png index 21c36a31b38..9158b102657 100644 Binary files a/resources/images/tokens/mainnet/OMG.png and b/resources/images/tokens/mainnet/OMG.png differ diff --git a/resources/images/tokens/mainnet/OMG@2x.png b/resources/images/tokens/mainnet/OMG@2x.png index 915c667ecf7..3d5fee5e638 100644 Binary files a/resources/images/tokens/mainnet/OMG@2x.png and b/resources/images/tokens/mainnet/OMG@2x.png differ diff --git a/resources/images/tokens/mainnet/OMG@3x.png b/resources/images/tokens/mainnet/OMG@3x.png index 9fed41ecb84..ae65d202683 100644 Binary files a/resources/images/tokens/mainnet/OMG@3x.png and b/resources/images/tokens/mainnet/OMG@3x.png differ diff --git a/resources/images/tokens/mainnet/OTN.png b/resources/images/tokens/mainnet/OTN.png index 11a52e319ad..6c9e747a963 100644 Binary files a/resources/images/tokens/mainnet/OTN.png and b/resources/images/tokens/mainnet/OTN.png differ diff --git a/resources/images/tokens/mainnet/OTN@2x.png b/resources/images/tokens/mainnet/OTN@2x.png index 96b865f34a2..64d139751a4 100644 Binary files a/resources/images/tokens/mainnet/OTN@2x.png and b/resources/images/tokens/mainnet/OTN@2x.png differ diff --git a/resources/images/tokens/mainnet/OTN@3x.png b/resources/images/tokens/mainnet/OTN@3x.png index 97a718ddebb..127bfef57ab 100644 Binary files a/resources/images/tokens/mainnet/OTN@3x.png and b/resources/images/tokens/mainnet/OTN@3x.png differ diff --git a/resources/images/tokens/mainnet/OXT.png b/resources/images/tokens/mainnet/OXT.png new file mode 100644 index 00000000000..f2c1ef3bbf9 Binary files /dev/null and b/resources/images/tokens/mainnet/OXT.png differ diff --git a/resources/images/tokens/mainnet/OXT@2x.png b/resources/images/tokens/mainnet/OXT@2x.png new file mode 100644 index 00000000000..331b8af00fe Binary files /dev/null and b/resources/images/tokens/mainnet/OXT@2x.png differ diff --git a/resources/images/tokens/mainnet/OXT@3x.png b/resources/images/tokens/mainnet/OXT@3x.png new file mode 100644 index 00000000000..ff33028f2c2 Binary files /dev/null and b/resources/images/tokens/mainnet/OXT@3x.png differ diff --git a/resources/images/tokens/mainnet/PAX.png b/resources/images/tokens/mainnet/PAX.png new file mode 100644 index 00000000000..3f6626e735b Binary files /dev/null and b/resources/images/tokens/mainnet/PAX.png differ diff --git a/resources/images/tokens/mainnet/PAX@2x.png b/resources/images/tokens/mainnet/PAX@2x.png new file mode 100644 index 00000000000..cc6bd46f6cd Binary files /dev/null and b/resources/images/tokens/mainnet/PAX@2x.png differ diff --git a/resources/images/tokens/mainnet/PAX@3x.png b/resources/images/tokens/mainnet/PAX@3x.png new file mode 100644 index 00000000000..ec4e1839299 Binary files /dev/null and b/resources/images/tokens/mainnet/PAX@3x.png differ diff --git a/resources/images/tokens/mainnet/PAXG.png b/resources/images/tokens/mainnet/PAXG.png new file mode 100644 index 00000000000..431b7c47e9f Binary files /dev/null and b/resources/images/tokens/mainnet/PAXG.png differ diff --git a/resources/images/tokens/mainnet/PAXG@2x.png b/resources/images/tokens/mainnet/PAXG@2x.png new file mode 100644 index 00000000000..504c86819dd Binary files /dev/null and b/resources/images/tokens/mainnet/PAXG@2x.png differ diff --git a/resources/images/tokens/mainnet/PAXG@3x.png b/resources/images/tokens/mainnet/PAXG@3x.png new file mode 100644 index 00000000000..3a45f96a257 Binary files /dev/null and b/resources/images/tokens/mainnet/PAXG@3x.png differ diff --git a/resources/images/tokens/mainnet/PAY.png b/resources/images/tokens/mainnet/PAY.png index 1f96648db8c..005f26e80b8 100644 Binary files a/resources/images/tokens/mainnet/PAY.png and b/resources/images/tokens/mainnet/PAY.png differ diff --git a/resources/images/tokens/mainnet/PAY@2x.png b/resources/images/tokens/mainnet/PAY@2x.png index 81711b075af..dbc4c6d47f3 100644 Binary files a/resources/images/tokens/mainnet/PAY@2x.png and b/resources/images/tokens/mainnet/PAY@2x.png differ diff --git a/resources/images/tokens/mainnet/PAY@3x.png b/resources/images/tokens/mainnet/PAY@3x.png index 7b314ddcbd6..ecc3a8e2aa0 100644 Binary files a/resources/images/tokens/mainnet/PAY@3x.png and b/resources/images/tokens/mainnet/PAY@3x.png differ diff --git a/resources/images/tokens/mainnet/PBTC.png b/resources/images/tokens/mainnet/PBTC.png new file mode 100644 index 00000000000..495bc98a605 Binary files /dev/null and b/resources/images/tokens/mainnet/PBTC.png differ diff --git a/resources/images/tokens/mainnet/PBTC@2x.png b/resources/images/tokens/mainnet/PBTC@2x.png new file mode 100644 index 00000000000..51531f6e2be Binary files /dev/null and b/resources/images/tokens/mainnet/PBTC@2x.png differ diff --git a/resources/images/tokens/mainnet/PBTC@3x.png b/resources/images/tokens/mainnet/PBTC@3x.png new file mode 100644 index 00000000000..72b16f2a49e Binary files /dev/null and b/resources/images/tokens/mainnet/PBTC@3x.png differ diff --git a/resources/images/tokens/mainnet/PLR.png b/resources/images/tokens/mainnet/PLR.png index e027af2c025..7288464dc39 100644 Binary files a/resources/images/tokens/mainnet/PLR.png and b/resources/images/tokens/mainnet/PLR.png differ diff --git a/resources/images/tokens/mainnet/PLR@2x.png b/resources/images/tokens/mainnet/PLR@2x.png index 81362ef81b4..66bfee6ffe5 100644 Binary files a/resources/images/tokens/mainnet/PLR@2x.png and b/resources/images/tokens/mainnet/PLR@2x.png differ diff --git a/resources/images/tokens/mainnet/PLR@3x.png b/resources/images/tokens/mainnet/PLR@3x.png index 63bc118fce5..dacf2094e51 100644 Binary files a/resources/images/tokens/mainnet/PLR@3x.png and b/resources/images/tokens/mainnet/PLR@3x.png differ diff --git a/resources/images/tokens/mainnet/POE.png b/resources/images/tokens/mainnet/POE.png new file mode 100644 index 00000000000..a5c3bafcc7c Binary files /dev/null and b/resources/images/tokens/mainnet/POE.png differ diff --git a/resources/images/tokens/mainnet/POE@2x.png b/resources/images/tokens/mainnet/POE@2x.png new file mode 100644 index 00000000000..fc41a0e8d53 Binary files /dev/null and b/resources/images/tokens/mainnet/POE@2x.png differ diff --git a/resources/images/tokens/mainnet/POE@3x.png b/resources/images/tokens/mainnet/POE@3x.png new file mode 100644 index 00000000000..6433e145a08 Binary files /dev/null and b/resources/images/tokens/mainnet/POE@3x.png differ diff --git a/resources/images/tokens/mainnet/POLY.png b/resources/images/tokens/mainnet/POLY.png new file mode 100644 index 00000000000..f85800b6ca1 Binary files /dev/null and b/resources/images/tokens/mainnet/POLY.png differ diff --git a/resources/images/tokens/mainnet/POLY@2x.png b/resources/images/tokens/mainnet/POLY@2x.png new file mode 100644 index 00000000000..f8c8e1e294a Binary files /dev/null and b/resources/images/tokens/mainnet/POLY@2x.png differ diff --git a/resources/images/tokens/mainnet/POLY@3x.png b/resources/images/tokens/mainnet/POLY@3x.png new file mode 100644 index 00000000000..edf2f9674de Binary files /dev/null and b/resources/images/tokens/mainnet/POLY@3x.png differ diff --git a/resources/images/tokens/mainnet/POWR.png b/resources/images/tokens/mainnet/POWR.png index 92cf571f6c8..7f146a52894 100644 Binary files a/resources/images/tokens/mainnet/POWR.png and b/resources/images/tokens/mainnet/POWR.png differ diff --git a/resources/images/tokens/mainnet/POWR@2x.png b/resources/images/tokens/mainnet/POWR@2x.png index 847e6fa0ed5..cc0bf3eaef6 100644 Binary files a/resources/images/tokens/mainnet/POWR@2x.png and b/resources/images/tokens/mainnet/POWR@2x.png differ diff --git a/resources/images/tokens/mainnet/POWR@3x.png b/resources/images/tokens/mainnet/POWR@3x.png index a696f834879..7ce613e8c34 100644 Binary files a/resources/images/tokens/mainnet/POWR@3x.png and b/resources/images/tokens/mainnet/POWR@3x.png differ diff --git a/resources/images/tokens/mainnet/PPP.png b/resources/images/tokens/mainnet/PPP.png index 51b1ccbbc8e..268ae763025 100644 Binary files a/resources/images/tokens/mainnet/PPP.png and b/resources/images/tokens/mainnet/PPP.png differ diff --git a/resources/images/tokens/mainnet/PPP@2x.png b/resources/images/tokens/mainnet/PPP@2x.png index ede7da0cc3e..2995eb4352b 100644 Binary files a/resources/images/tokens/mainnet/PPP@2x.png and b/resources/images/tokens/mainnet/PPP@2x.png differ diff --git a/resources/images/tokens/mainnet/PPP@3x.png b/resources/images/tokens/mainnet/PPP@3x.png index 77423baf500..caa1cd96a37 100644 Binary files a/resources/images/tokens/mainnet/PPP@3x.png and b/resources/images/tokens/mainnet/PPP@3x.png differ diff --git a/resources/images/tokens/mainnet/PPT.png b/resources/images/tokens/mainnet/PPT.png index 1568b121a4f..38a5c05e9ec 100644 Binary files a/resources/images/tokens/mainnet/PPT.png and b/resources/images/tokens/mainnet/PPT.png differ diff --git a/resources/images/tokens/mainnet/PPT@2x.png b/resources/images/tokens/mainnet/PPT@2x.png index 140705a1834..2abc1273671 100644 Binary files a/resources/images/tokens/mainnet/PPT@2x.png and b/resources/images/tokens/mainnet/PPT@2x.png differ diff --git a/resources/images/tokens/mainnet/PPT@3x.png b/resources/images/tokens/mainnet/PPT@3x.png index 5ba6605d956..aae1fb31c30 100644 Binary files a/resources/images/tokens/mainnet/PPT@3x.png and b/resources/images/tokens/mainnet/PPT@3x.png differ diff --git a/resources/images/tokens/mainnet/PT.png b/resources/images/tokens/mainnet/PT.png new file mode 100644 index 00000000000..79d155177ea Binary files /dev/null and b/resources/images/tokens/mainnet/PT.png differ diff --git a/resources/images/tokens/mainnet/PT@2x.png b/resources/images/tokens/mainnet/PT@2x.png new file mode 100644 index 00000000000..57cb9a4a4ff Binary files /dev/null and b/resources/images/tokens/mainnet/PT@2x.png differ diff --git a/resources/images/tokens/mainnet/PT@3x.png b/resources/images/tokens/mainnet/PT@3x.png new file mode 100644 index 00000000000..b7bb0b61882 Binary files /dev/null and b/resources/images/tokens/mainnet/PT@3x.png differ diff --git a/resources/images/tokens/mainnet/QKC.png b/resources/images/tokens/mainnet/QKC.png new file mode 100644 index 00000000000..c060e0ed47f Binary files /dev/null and b/resources/images/tokens/mainnet/QKC.png differ diff --git a/resources/images/tokens/mainnet/QKC@2x.png b/resources/images/tokens/mainnet/QKC@2x.png new file mode 100644 index 00000000000..2dba7f0c44b Binary files /dev/null and b/resources/images/tokens/mainnet/QKC@2x.png differ diff --git a/resources/images/tokens/mainnet/QKC@3x.png b/resources/images/tokens/mainnet/QKC@3x.png new file mode 100644 index 00000000000..51cf0770494 Binary files /dev/null and b/resources/images/tokens/mainnet/QKC@3x.png differ diff --git a/resources/images/tokens/mainnet/QRL.png b/resources/images/tokens/mainnet/QRL.png index f3b329b9632..e879d0c929e 100644 Binary files a/resources/images/tokens/mainnet/QRL.png and b/resources/images/tokens/mainnet/QRL.png differ diff --git a/resources/images/tokens/mainnet/QRL@2x.png b/resources/images/tokens/mainnet/QRL@2x.png index 1a62d9f84d7..5a7251df718 100644 Binary files a/resources/images/tokens/mainnet/QRL@2x.png and b/resources/images/tokens/mainnet/QRL@2x.png differ diff --git a/resources/images/tokens/mainnet/QRL@3x.png b/resources/images/tokens/mainnet/QRL@3x.png index 3df501b8c9a..eb435a7a089 100644 Binary files a/resources/images/tokens/mainnet/QRL@3x.png and b/resources/images/tokens/mainnet/QRL@3x.png differ diff --git a/resources/images/tokens/mainnet/QSP.png b/resources/images/tokens/mainnet/QSP.png index bcd21d73b87..db2f3275465 100644 Binary files a/resources/images/tokens/mainnet/QSP.png and b/resources/images/tokens/mainnet/QSP.png differ diff --git a/resources/images/tokens/mainnet/QSP@2x.png b/resources/images/tokens/mainnet/QSP@2x.png index bb5b0a1a036..2cc7503fb18 100644 Binary files a/resources/images/tokens/mainnet/QSP@2x.png and b/resources/images/tokens/mainnet/QSP@2x.png differ diff --git a/resources/images/tokens/mainnet/QSP@3x.png b/resources/images/tokens/mainnet/QSP@3x.png index 677e2baa65a..0e0c482a5ab 100644 Binary files a/resources/images/tokens/mainnet/QSP@3x.png and b/resources/images/tokens/mainnet/QSP@3x.png differ diff --git a/resources/images/tokens/mainnet/R.png b/resources/images/tokens/mainnet/R.png index 87b74fc82a6..f67a2ced39c 100644 Binary files a/resources/images/tokens/mainnet/R.png and b/resources/images/tokens/mainnet/R.png differ diff --git a/resources/images/tokens/mainnet/R@2x.png b/resources/images/tokens/mainnet/R@2x.png index c22fd0f21bb..9bb0708e04c 100644 Binary files a/resources/images/tokens/mainnet/R@2x.png and b/resources/images/tokens/mainnet/R@2x.png differ diff --git a/resources/images/tokens/mainnet/R@3x.png b/resources/images/tokens/mainnet/R@3x.png index 95ccda049c6..8ae06bf6f69 100644 Binary files a/resources/images/tokens/mainnet/R@3x.png and b/resources/images/tokens/mainnet/R@3x.png differ diff --git a/resources/images/tokens/mainnet/RAE.png b/resources/images/tokens/mainnet/RAE.png new file mode 100644 index 00000000000..daa936f248d Binary files /dev/null and b/resources/images/tokens/mainnet/RAE.png differ diff --git a/resources/images/tokens/mainnet/RAE@2x.png b/resources/images/tokens/mainnet/RAE@2x.png new file mode 100644 index 00000000000..8bf44edaf05 Binary files /dev/null and b/resources/images/tokens/mainnet/RAE@2x.png differ diff --git a/resources/images/tokens/mainnet/RAE@3x.png b/resources/images/tokens/mainnet/RAE@3x.png new file mode 100644 index 00000000000..5f34789b30e Binary files /dev/null and b/resources/images/tokens/mainnet/RAE@3x.png differ diff --git a/resources/images/tokens/mainnet/RARE.png b/resources/images/tokens/mainnet/RARE.png new file mode 100644 index 00000000000..7d8faa483d0 Binary files /dev/null and b/resources/images/tokens/mainnet/RARE.png differ diff --git a/resources/images/tokens/mainnet/RCN.png b/resources/images/tokens/mainnet/RCN.png index 45ef2ed1e60..e8853c21a9f 100644 Binary files a/resources/images/tokens/mainnet/RCN.png and b/resources/images/tokens/mainnet/RCN.png differ diff --git a/resources/images/tokens/mainnet/RCN@2x.png b/resources/images/tokens/mainnet/RCN@2x.png index a8118daff5f..76d3502fd95 100644 Binary files a/resources/images/tokens/mainnet/RCN@2x.png and b/resources/images/tokens/mainnet/RCN@2x.png differ diff --git a/resources/images/tokens/mainnet/RCN@3x.png b/resources/images/tokens/mainnet/RCN@3x.png index 1bfdc8f90cf..569af97d03c 100644 Binary files a/resources/images/tokens/mainnet/RCN@3x.png and b/resources/images/tokens/mainnet/RCN@3x.png differ diff --git a/resources/images/tokens/mainnet/RDN.png b/resources/images/tokens/mainnet/RDN.png index 368e1cbf38a..4f8e24a43ba 100644 Binary files a/resources/images/tokens/mainnet/RDN.png and b/resources/images/tokens/mainnet/RDN.png differ diff --git a/resources/images/tokens/mainnet/RDN@2x.png b/resources/images/tokens/mainnet/RDN@2x.png index bc550c1f4cf..4086f893c3c 100644 Binary files a/resources/images/tokens/mainnet/RDN@2x.png and b/resources/images/tokens/mainnet/RDN@2x.png differ diff --git a/resources/images/tokens/mainnet/RDN@3x.png b/resources/images/tokens/mainnet/RDN@3x.png index 3e4a303c712..dcb766e1211 100644 Binary files a/resources/images/tokens/mainnet/RDN@3x.png and b/resources/images/tokens/mainnet/RDN@3x.png differ diff --git a/resources/images/tokens/mainnet/REN.png b/resources/images/tokens/mainnet/REN.png new file mode 100644 index 00000000000..bcf7dd9f033 Binary files /dev/null and b/resources/images/tokens/mainnet/REN.png differ diff --git a/resources/images/tokens/mainnet/REN@2x.png b/resources/images/tokens/mainnet/REN@2x.png new file mode 100644 index 00000000000..aed2651e8c2 Binary files /dev/null and b/resources/images/tokens/mainnet/REN@2x.png differ diff --git a/resources/images/tokens/mainnet/REN@3x.png b/resources/images/tokens/mainnet/REN@3x.png new file mode 100644 index 00000000000..149583f5616 Binary files /dev/null and b/resources/images/tokens/mainnet/REN@3x.png differ diff --git a/resources/images/tokens/mainnet/REP.png b/resources/images/tokens/mainnet/REP.png new file mode 100644 index 00000000000..c0de72b2c41 Binary files /dev/null and b/resources/images/tokens/mainnet/REP.png differ diff --git a/resources/images/tokens/mainnet/REP@2x.png b/resources/images/tokens/mainnet/REP@2x.png new file mode 100644 index 00000000000..ddbb97218be Binary files /dev/null and b/resources/images/tokens/mainnet/REP@2x.png differ diff --git a/resources/images/tokens/mainnet/REP@3x.png b/resources/images/tokens/mainnet/REP@3x.png new file mode 100644 index 00000000000..00b779db960 Binary files /dev/null and b/resources/images/tokens/mainnet/REP@3x.png differ diff --git a/resources/images/tokens/mainnet/REQ.png b/resources/images/tokens/mainnet/REQ.png index 1bb71f5939d..e17e66d29dd 100644 Binary files a/resources/images/tokens/mainnet/REQ.png and b/resources/images/tokens/mainnet/REQ.png differ diff --git a/resources/images/tokens/mainnet/REQ@2x.png b/resources/images/tokens/mainnet/REQ@2x.png index 6c6e1994090..2857dd27c80 100644 Binary files a/resources/images/tokens/mainnet/REQ@2x.png and b/resources/images/tokens/mainnet/REQ@2x.png differ diff --git a/resources/images/tokens/mainnet/REQ@3x.png b/resources/images/tokens/mainnet/REQ@3x.png index 794bfcf1173..b38e4689e37 100644 Binary files a/resources/images/tokens/mainnet/REQ@3x.png and b/resources/images/tokens/mainnet/REQ@3x.png differ diff --git a/resources/images/tokens/mainnet/RHOC.png b/resources/images/tokens/mainnet/RHOC.png index 15d9ea6d53c..269b9776c66 100644 Binary files a/resources/images/tokens/mainnet/RHOC.png and b/resources/images/tokens/mainnet/RHOC.png differ diff --git a/resources/images/tokens/mainnet/RHOC@2x.png b/resources/images/tokens/mainnet/RHOC@2x.png index 8b5a5f6dc14..e756bd154f6 100644 Binary files a/resources/images/tokens/mainnet/RHOC@2x.png and b/resources/images/tokens/mainnet/RHOC@2x.png differ diff --git a/resources/images/tokens/mainnet/RHOC@3x.png b/resources/images/tokens/mainnet/RHOC@3x.png index 610453210cd..30e47e848fe 100644 Binary files a/resources/images/tokens/mainnet/RHOC@3x.png and b/resources/images/tokens/mainnet/RHOC@3x.png differ diff --git a/resources/images/tokens/mainnet/RLC.png b/resources/images/tokens/mainnet/RLC.png index 1d80f20e0b2..cae90649a81 100644 Binary files a/resources/images/tokens/mainnet/RLC.png and b/resources/images/tokens/mainnet/RLC.png differ diff --git a/resources/images/tokens/mainnet/RLC@2x.png b/resources/images/tokens/mainnet/RLC@2x.png index e3832d7a828..fc8c6767095 100644 Binary files a/resources/images/tokens/mainnet/RLC@2x.png and b/resources/images/tokens/mainnet/RLC@2x.png differ diff --git a/resources/images/tokens/mainnet/RLC@3x.png b/resources/images/tokens/mainnet/RLC@3x.png index 40d08f515ba..a40fcadfad0 100644 Binary files a/resources/images/tokens/mainnet/RLC@3x.png and b/resources/images/tokens/mainnet/RLC@3x.png differ diff --git a/resources/images/tokens/mainnet/ROL.png b/resources/images/tokens/mainnet/ROL.png index 032b27e0fa7..5690648a4ad 100644 Binary files a/resources/images/tokens/mainnet/ROL.png and b/resources/images/tokens/mainnet/ROL.png differ diff --git a/resources/images/tokens/mainnet/ROL@2x.png b/resources/images/tokens/mainnet/ROL@2x.png index c935fb2372d..1acdc0eb3fa 100644 Binary files a/resources/images/tokens/mainnet/ROL@2x.png and b/resources/images/tokens/mainnet/ROL@2x.png differ diff --git a/resources/images/tokens/mainnet/ROL@3x.png b/resources/images/tokens/mainnet/ROL@3x.png index 0c57da0a2ea..bc829be0373 100644 Binary files a/resources/images/tokens/mainnet/ROL@3x.png and b/resources/images/tokens/mainnet/ROL@3x.png differ diff --git a/resources/images/tokens/mainnet/SAI.png b/resources/images/tokens/mainnet/SAI.png new file mode 100644 index 00000000000..23e2d36b69e Binary files /dev/null and b/resources/images/tokens/mainnet/SAI.png differ diff --git a/resources/images/tokens/mainnet/SAI@2x.png b/resources/images/tokens/mainnet/SAI@2x.png new file mode 100644 index 00000000000..40c2ee31bc5 Binary files /dev/null and b/resources/images/tokens/mainnet/SAI@2x.png differ diff --git a/resources/images/tokens/mainnet/SAI@3x.png b/resources/images/tokens/mainnet/SAI@3x.png new file mode 100644 index 00000000000..8fa2409ab9e Binary files /dev/null and b/resources/images/tokens/mainnet/SAI@3x.png differ diff --git a/resources/images/tokens/mainnet/SALT.png b/resources/images/tokens/mainnet/SALT.png index a511472cd96..c95031ca6e1 100644 Binary files a/resources/images/tokens/mainnet/SALT.png and b/resources/images/tokens/mainnet/SALT.png differ diff --git a/resources/images/tokens/mainnet/SALT@2x.png b/resources/images/tokens/mainnet/SALT@2x.png index 89d61e0e7e2..468369bee87 100644 Binary files a/resources/images/tokens/mainnet/SALT@2x.png and b/resources/images/tokens/mainnet/SALT@2x.png differ diff --git a/resources/images/tokens/mainnet/SALT@3x.png b/resources/images/tokens/mainnet/SALT@3x.png index 4da0f28a032..6745c22c9b5 100644 Binary files a/resources/images/tokens/mainnet/SALT@3x.png and b/resources/images/tokens/mainnet/SALT@3x.png differ diff --git a/resources/images/tokens/mainnet/SAN.png b/resources/images/tokens/mainnet/SAN.png index d4d49bea399..7f6245185f0 100644 Binary files a/resources/images/tokens/mainnet/SAN.png and b/resources/images/tokens/mainnet/SAN.png differ diff --git a/resources/images/tokens/mainnet/SAN@2x.png b/resources/images/tokens/mainnet/SAN@2x.png index 42f8b8fb5c6..2a36e67f1f2 100644 Binary files a/resources/images/tokens/mainnet/SAN@2x.png and b/resources/images/tokens/mainnet/SAN@2x.png differ diff --git a/resources/images/tokens/mainnet/SAN@3x.png b/resources/images/tokens/mainnet/SAN@3x.png index 5be302445a8..f21b6269fdb 100644 Binary files a/resources/images/tokens/mainnet/SAN@3x.png and b/resources/images/tokens/mainnet/SAN@3x.png differ diff --git a/resources/images/tokens/mainnet/SNGLS.png b/resources/images/tokens/mainnet/SNGLS.png index ede7af951da..9b2fb3088c5 100644 Binary files a/resources/images/tokens/mainnet/SNGLS.png and b/resources/images/tokens/mainnet/SNGLS.png differ diff --git a/resources/images/tokens/mainnet/SNGLS@2x.png b/resources/images/tokens/mainnet/SNGLS@2x.png index c1a2053df7a..74148d2fc97 100644 Binary files a/resources/images/tokens/mainnet/SNGLS@2x.png and b/resources/images/tokens/mainnet/SNGLS@2x.png differ diff --git a/resources/images/tokens/mainnet/SNGLS@3x.png b/resources/images/tokens/mainnet/SNGLS@3x.png index 609b2342519..0af8adc83d3 100644 Binary files a/resources/images/tokens/mainnet/SNGLS@3x.png and b/resources/images/tokens/mainnet/SNGLS@3x.png differ diff --git a/resources/images/tokens/mainnet/SNM.png b/resources/images/tokens/mainnet/SNM.png index 1934ecdf9d1..cc47f3de847 100644 Binary files a/resources/images/tokens/mainnet/SNM.png and b/resources/images/tokens/mainnet/SNM.png differ diff --git a/resources/images/tokens/mainnet/SNM@2x.png b/resources/images/tokens/mainnet/SNM@2x.png index 5d5c29f3d94..8bbd87cc6f5 100644 Binary files a/resources/images/tokens/mainnet/SNM@2x.png and b/resources/images/tokens/mainnet/SNM@2x.png differ diff --git a/resources/images/tokens/mainnet/SNM@3x.png b/resources/images/tokens/mainnet/SNM@3x.png index 8913f8fa494..fd5796aabc1 100644 Binary files a/resources/images/tokens/mainnet/SNM@3x.png and b/resources/images/tokens/mainnet/SNM@3x.png differ diff --git a/resources/images/tokens/mainnet/SNT.png b/resources/images/tokens/mainnet/SNT.png index 323c5d2026c..6a5b152b2e4 100644 Binary files a/resources/images/tokens/mainnet/SNT.png and b/resources/images/tokens/mainnet/SNT.png differ diff --git a/resources/images/tokens/mainnet/SNT@2x.png b/resources/images/tokens/mainnet/SNT@2x.png index c87a6f2e74b..ffa49bb1dd4 100644 Binary files a/resources/images/tokens/mainnet/SNT@2x.png and b/resources/images/tokens/mainnet/SNT@2x.png differ diff --git a/resources/images/tokens/mainnet/SNT@3x.png b/resources/images/tokens/mainnet/SNT@3x.png index 09ef45c59d6..7037af0441e 100644 Binary files a/resources/images/tokens/mainnet/SNT@3x.png and b/resources/images/tokens/mainnet/SNT@3x.png differ diff --git a/resources/images/tokens/mainnet/SNX.png b/resources/images/tokens/mainnet/SNX.png new file mode 100644 index 00000000000..f50ed2121f0 Binary files /dev/null and b/resources/images/tokens/mainnet/SNX.png differ diff --git a/resources/images/tokens/mainnet/SNX@2x.png b/resources/images/tokens/mainnet/SNX@2x.png new file mode 100644 index 00000000000..99342662fc9 Binary files /dev/null and b/resources/images/tokens/mainnet/SNX@2x.png differ diff --git a/resources/images/tokens/mainnet/SNX@3x.png b/resources/images/tokens/mainnet/SNX@3x.png new file mode 100644 index 00000000000..0915fa0a494 Binary files /dev/null and b/resources/images/tokens/mainnet/SNX@3x.png differ diff --git a/resources/images/tokens/mainnet/SOCKS.png b/resources/images/tokens/mainnet/SOCKS.png new file mode 100644 index 00000000000..e76615b9e91 Binary files /dev/null and b/resources/images/tokens/mainnet/SOCKS.png differ diff --git a/resources/images/tokens/mainnet/SOCKS@2x.png b/resources/images/tokens/mainnet/SOCKS@2x.png new file mode 100644 index 00000000000..b6319b20434 Binary files /dev/null and b/resources/images/tokens/mainnet/SOCKS@2x.png differ diff --git a/resources/images/tokens/mainnet/SOCKS@3x.png b/resources/images/tokens/mainnet/SOCKS@3x.png new file mode 100644 index 00000000000..66c757abea7 Binary files /dev/null and b/resources/images/tokens/mainnet/SOCKS@3x.png differ diff --git a/resources/images/tokens/mainnet/SPANK.png b/resources/images/tokens/mainnet/SPANK.png index c47804d33db..4a89ff09c10 100644 Binary files a/resources/images/tokens/mainnet/SPANK.png and b/resources/images/tokens/mainnet/SPANK.png differ diff --git a/resources/images/tokens/mainnet/SPANK@2x.png b/resources/images/tokens/mainnet/SPANK@2x.png index 7b1e7933d3f..1ce947b2386 100644 Binary files a/resources/images/tokens/mainnet/SPANK@2x.png and b/resources/images/tokens/mainnet/SPANK@2x.png differ diff --git a/resources/images/tokens/mainnet/SPANK@3x.png b/resources/images/tokens/mainnet/SPANK@3x.png index fa99774b9d7..020dbdaa58c 100644 Binary files a/resources/images/tokens/mainnet/SPANK@3x.png and b/resources/images/tokens/mainnet/SPANK@3x.png differ diff --git a/resources/images/tokens/mainnet/SPIKE.png b/resources/images/tokens/mainnet/SPIKE.png new file mode 100644 index 00000000000..c03de8ce46e Binary files /dev/null and b/resources/images/tokens/mainnet/SPIKE.png differ diff --git a/resources/images/tokens/mainnet/SPIKE@2x.png b/resources/images/tokens/mainnet/SPIKE@2x.png new file mode 100644 index 00000000000..5464ca8a40d Binary files /dev/null and b/resources/images/tokens/mainnet/SPIKE@2x.png differ diff --git a/resources/images/tokens/mainnet/SPIKE@3x.png b/resources/images/tokens/mainnet/SPIKE@3x.png new file mode 100644 index 00000000000..4342c80e24e Binary files /dev/null and b/resources/images/tokens/mainnet/SPIKE@3x.png differ diff --git a/resources/images/tokens/mainnet/SPN.png b/resources/images/tokens/mainnet/SPN.png new file mode 100644 index 00000000000..69a0b984fc9 Binary files /dev/null and b/resources/images/tokens/mainnet/SPN.png differ diff --git a/resources/images/tokens/mainnet/SPN@2x.png b/resources/images/tokens/mainnet/SPN@2x.png new file mode 100644 index 00000000000..af71d9c6d83 Binary files /dev/null and b/resources/images/tokens/mainnet/SPN@2x.png differ diff --git a/resources/images/tokens/mainnet/SPN@3x.png b/resources/images/tokens/mainnet/SPN@3x.png new file mode 100644 index 00000000000..fedf2e3041d Binary files /dev/null and b/resources/images/tokens/mainnet/SPN@3x.png differ diff --git a/resources/images/tokens/mainnet/ST.png b/resources/images/tokens/mainnet/ST.png new file mode 100644 index 00000000000..0e64b0f1bd3 Binary files /dev/null and b/resources/images/tokens/mainnet/ST.png differ diff --git a/resources/images/tokens/mainnet/ST@2x.png b/resources/images/tokens/mainnet/ST@2x.png new file mode 100644 index 00000000000..682eb17f394 Binary files /dev/null and b/resources/images/tokens/mainnet/ST@2x.png differ diff --git a/resources/images/tokens/mainnet/ST@3x.png b/resources/images/tokens/mainnet/ST@3x.png new file mode 100644 index 00000000000..c5610e7ab5a Binary files /dev/null and b/resources/images/tokens/mainnet/ST@3x.png differ diff --git a/resources/images/tokens/mainnet/STORJ.png b/resources/images/tokens/mainnet/STORJ.png index f22d9340fc7..84b0164e270 100644 Binary files a/resources/images/tokens/mainnet/STORJ.png and b/resources/images/tokens/mainnet/STORJ.png differ diff --git a/resources/images/tokens/mainnet/STORJ@2x.png b/resources/images/tokens/mainnet/STORJ@2x.png index f64d3ad2256..5d7f269d0fc 100644 Binary files a/resources/images/tokens/mainnet/STORJ@2x.png and b/resources/images/tokens/mainnet/STORJ@2x.png differ diff --git a/resources/images/tokens/mainnet/STORJ@3x.png b/resources/images/tokens/mainnet/STORJ@3x.png index a88d504597c..6b2e9c4d89c 100644 Binary files a/resources/images/tokens/mainnet/STORJ@3x.png and b/resources/images/tokens/mainnet/STORJ@3x.png differ diff --git a/resources/images/tokens/mainnet/STORM.png b/resources/images/tokens/mainnet/STORM.png new file mode 100644 index 00000000000..a883453e0b2 Binary files /dev/null and b/resources/images/tokens/mainnet/STORM.png differ diff --git a/resources/images/tokens/mainnet/STORM@2x.png b/resources/images/tokens/mainnet/STORM@2x.png new file mode 100644 index 00000000000..250a29a2d89 Binary files /dev/null and b/resources/images/tokens/mainnet/STORM@2x.png differ diff --git a/resources/images/tokens/mainnet/STORM@3x.png b/resources/images/tokens/mainnet/STORM@3x.png new file mode 100644 index 00000000000..5ce7f5c104c Binary files /dev/null and b/resources/images/tokens/mainnet/STORM@3x.png differ diff --git a/resources/images/tokens/mainnet/STRK.png b/resources/images/tokens/mainnet/STRK.png old mode 100755 new mode 100644 index 03a38b9acab..dfef3393778 Binary files a/resources/images/tokens/mainnet/STRK.png and b/resources/images/tokens/mainnet/STRK.png differ diff --git a/resources/images/tokens/mainnet/STRK@2x.png b/resources/images/tokens/mainnet/STRK@2x.png old mode 100755 new mode 100644 index 065cb5b997b..df750209af3 Binary files a/resources/images/tokens/mainnet/STRK@2x.png and b/resources/images/tokens/mainnet/STRK@2x.png differ diff --git a/resources/images/tokens/mainnet/STRK@3x.png b/resources/images/tokens/mainnet/STRK@3x.png old mode 100755 new mode 100644 index 439864f1082..f35a0676df2 Binary files a/resources/images/tokens/mainnet/STRK@3x.png and b/resources/images/tokens/mainnet/STRK@3x.png differ diff --git a/resources/images/tokens/mainnet/STT.png b/resources/images/tokens/mainnet/STT.png new file mode 100644 index 00000000000..6a5b152b2e4 Binary files /dev/null and b/resources/images/tokens/mainnet/STT.png differ diff --git a/resources/images/tokens/mainnet/STT@2x.png b/resources/images/tokens/mainnet/STT@2x.png new file mode 100644 index 00000000000..ffa49bb1dd4 Binary files /dev/null and b/resources/images/tokens/mainnet/STT@2x.png differ diff --git a/resources/images/tokens/mainnet/STT@3x.png b/resources/images/tokens/mainnet/STT@3x.png new file mode 100644 index 00000000000..7037af0441e Binary files /dev/null and b/resources/images/tokens/mainnet/STT@3x.png differ diff --git a/resources/images/tokens/mainnet/STX.png b/resources/images/tokens/mainnet/STX.png index f9fd776c8a4..fc1ceedc61f 100644 Binary files a/resources/images/tokens/mainnet/STX.png and b/resources/images/tokens/mainnet/STX.png differ diff --git a/resources/images/tokens/mainnet/STX@2x.png b/resources/images/tokens/mainnet/STX@2x.png index b868655ef19..1807fbcca63 100644 Binary files a/resources/images/tokens/mainnet/STX@2x.png and b/resources/images/tokens/mainnet/STX@2x.png differ diff --git a/resources/images/tokens/mainnet/STX@3x.png b/resources/images/tokens/mainnet/STX@3x.png index e39f5276f91..e3241829d01 100644 Binary files a/resources/images/tokens/mainnet/STX@3x.png and b/resources/images/tokens/mainnet/STX@3x.png differ diff --git a/resources/images/tokens/mainnet/SUB.png b/resources/images/tokens/mainnet/SUB.png index e5122f8ee3a..db54d3928b7 100644 Binary files a/resources/images/tokens/mainnet/SUB.png and b/resources/images/tokens/mainnet/SUB.png differ diff --git a/resources/images/tokens/mainnet/SUB@2x.png b/resources/images/tokens/mainnet/SUB@2x.png index c94d7a3e731..83998d15e8c 100644 Binary files a/resources/images/tokens/mainnet/SUB@2x.png and b/resources/images/tokens/mainnet/SUB@2x.png differ diff --git a/resources/images/tokens/mainnet/SUB@3x.png b/resources/images/tokens/mainnet/SUB@3x.png index aa3db9f2e46..2e5b07f7169 100644 Binary files a/resources/images/tokens/mainnet/SUB@3x.png and b/resources/images/tokens/mainnet/SUB@3x.png differ diff --git a/resources/images/tokens/mainnet/SUPR.png b/resources/images/tokens/mainnet/SUPR.png new file mode 100644 index 00000000000..7d8faa483d0 Binary files /dev/null and b/resources/images/tokens/mainnet/SUPR.png differ diff --git a/resources/images/tokens/mainnet/SUPRR.png b/resources/images/tokens/mainnet/SUPRR.png new file mode 100644 index 00000000000..7d8faa483d0 Binary files /dev/null and b/resources/images/tokens/mainnet/SUPRR.png differ diff --git a/resources/images/tokens/mainnet/TAAS.png b/resources/images/tokens/mainnet/TAAS.png index 0d905d345b2..c5f205127ea 100644 Binary files a/resources/images/tokens/mainnet/TAAS.png and b/resources/images/tokens/mainnet/TAAS.png differ diff --git a/resources/images/tokens/mainnet/TAAS@2x.png b/resources/images/tokens/mainnet/TAAS@2x.png index 1e148e61826..1575955a63f 100644 Binary files a/resources/images/tokens/mainnet/TAAS@2x.png and b/resources/images/tokens/mainnet/TAAS@2x.png differ diff --git a/resources/images/tokens/mainnet/TAAS@3x.png b/resources/images/tokens/mainnet/TAAS@3x.png index 42f58fafbeb..810fc57a10e 100644 Binary files a/resources/images/tokens/mainnet/TAAS@3x.png and b/resources/images/tokens/mainnet/TAAS@3x.png differ diff --git a/resources/images/tokens/mainnet/TAUD.png b/resources/images/tokens/mainnet/TAUD.png new file mode 100644 index 00000000000..1bd3fb0f8ec Binary files /dev/null and b/resources/images/tokens/mainnet/TAUD.png differ diff --git a/resources/images/tokens/mainnet/TAUD@2x.png b/resources/images/tokens/mainnet/TAUD@2x.png new file mode 100644 index 00000000000..3e5bd058251 Binary files /dev/null and b/resources/images/tokens/mainnet/TAUD@2x.png differ diff --git a/resources/images/tokens/mainnet/TAUD@3x.png b/resources/images/tokens/mainnet/TAUD@3x.png new file mode 100644 index 00000000000..6be9a0abeaf Binary files /dev/null and b/resources/images/tokens/mainnet/TAUD@3x.png differ diff --git a/resources/images/tokens/mainnet/TCAD.png b/resources/images/tokens/mainnet/TCAD.png new file mode 100644 index 00000000000..823f7d4dba7 Binary files /dev/null and b/resources/images/tokens/mainnet/TCAD.png differ diff --git a/resources/images/tokens/mainnet/TCAD@2x.png b/resources/images/tokens/mainnet/TCAD@2x.png new file mode 100644 index 00000000000..57ca52e3dad Binary files /dev/null and b/resources/images/tokens/mainnet/TCAD@2x.png differ diff --git a/resources/images/tokens/mainnet/TCAD@3x.png b/resources/images/tokens/mainnet/TCAD@3x.png new file mode 100644 index 00000000000..715b45d7cf0 Binary files /dev/null and b/resources/images/tokens/mainnet/TCAD@3x.png differ diff --git a/resources/images/tokens/mainnet/TGBP.png b/resources/images/tokens/mainnet/TGBP.png new file mode 100644 index 00000000000..39c8765cf95 Binary files /dev/null and b/resources/images/tokens/mainnet/TGBP.png differ diff --git a/resources/images/tokens/mainnet/TGBP@2x.png b/resources/images/tokens/mainnet/TGBP@2x.png new file mode 100644 index 00000000000..7caa9df318a Binary files /dev/null and b/resources/images/tokens/mainnet/TGBP@2x.png differ diff --git a/resources/images/tokens/mainnet/TGBP@3x.png b/resources/images/tokens/mainnet/TGBP@3x.png new file mode 100644 index 00000000000..b55c201c45f Binary files /dev/null and b/resources/images/tokens/mainnet/TGBP@3x.png differ diff --git a/resources/images/tokens/mainnet/TKN.png b/resources/images/tokens/mainnet/TKN.png index f27ec04a8bf..c713eaf3319 100644 Binary files a/resources/images/tokens/mainnet/TKN.png and b/resources/images/tokens/mainnet/TKN.png differ diff --git a/resources/images/tokens/mainnet/TKN@2x.png b/resources/images/tokens/mainnet/TKN@2x.png index 2fbc33c8168..187ac51c066 100644 Binary files a/resources/images/tokens/mainnet/TKN@2x.png and b/resources/images/tokens/mainnet/TKN@2x.png differ diff --git a/resources/images/tokens/mainnet/TKN@3x.png b/resources/images/tokens/mainnet/TKN@3x.png index 8c7b8e8e918..1aefb2f14bd 100644 Binary files a/resources/images/tokens/mainnet/TKN@3x.png and b/resources/images/tokens/mainnet/TKN@3x.png differ diff --git a/resources/images/tokens/mainnet/TKX.png b/resources/images/tokens/mainnet/TKX.png new file mode 100644 index 00000000000..126fb23f44e Binary files /dev/null and b/resources/images/tokens/mainnet/TKX.png differ diff --git a/resources/images/tokens/mainnet/TKX@2x.png b/resources/images/tokens/mainnet/TKX@2x.png new file mode 100644 index 00000000000..53c6a9d6c30 Binary files /dev/null and b/resources/images/tokens/mainnet/TKX@2x.png differ diff --git a/resources/images/tokens/mainnet/TKX@3x.png b/resources/images/tokens/mainnet/TKX@3x.png new file mode 100644 index 00000000000..c8108ece862 Binary files /dev/null and b/resources/images/tokens/mainnet/TKX@3x.png differ diff --git a/resources/images/tokens/mainnet/TNT.png b/resources/images/tokens/mainnet/TNT.png index 39b7b639a80..b00f9da2a9d 100644 Binary files a/resources/images/tokens/mainnet/TNT.png and b/resources/images/tokens/mainnet/TNT.png differ diff --git a/resources/images/tokens/mainnet/TNT@2x.png b/resources/images/tokens/mainnet/TNT@2x.png index 1fb677341d4..e989f06eb90 100644 Binary files a/resources/images/tokens/mainnet/TNT@2x.png and b/resources/images/tokens/mainnet/TNT@2x.png differ diff --git a/resources/images/tokens/mainnet/TNT@3x.png b/resources/images/tokens/mainnet/TNT@3x.png index 650365fdad2..618ca8f2d8d 100644 Binary files a/resources/images/tokens/mainnet/TNT@3x.png and b/resources/images/tokens/mainnet/TNT@3x.png differ diff --git a/resources/images/tokens/mainnet/TRST.png b/resources/images/tokens/mainnet/TRST.png index 4dfee0ba3c9..dde4b8283d9 100644 Binary files a/resources/images/tokens/mainnet/TRST.png and b/resources/images/tokens/mainnet/TRST.png differ diff --git a/resources/images/tokens/mainnet/TRST@2x.png b/resources/images/tokens/mainnet/TRST@2x.png index de35e624bf5..55716f6a612 100644 Binary files a/resources/images/tokens/mainnet/TRST@2x.png and b/resources/images/tokens/mainnet/TRST@2x.png differ diff --git a/resources/images/tokens/mainnet/TRST@3x.png b/resources/images/tokens/mainnet/TRST@3x.png index c6da69e1275..1ad908aa8e7 100644 Binary files a/resources/images/tokens/mainnet/TRST@3x.png and b/resources/images/tokens/mainnet/TRST@3x.png differ diff --git a/resources/images/tokens/mainnet/TRX.png b/resources/images/tokens/mainnet/TRX.png index f11c3d5b3ac..15b2c08271e 100644 Binary files a/resources/images/tokens/mainnet/TRX.png and b/resources/images/tokens/mainnet/TRX.png differ diff --git a/resources/images/tokens/mainnet/TRX@2x.png b/resources/images/tokens/mainnet/TRX@2x.png index 0348cd37840..f48265e514b 100644 Binary files a/resources/images/tokens/mainnet/TRX@2x.png and b/resources/images/tokens/mainnet/TRX@2x.png differ diff --git a/resources/images/tokens/mainnet/TRX@3x.png b/resources/images/tokens/mainnet/TRX@3x.png index bbdb6c205ed..66f19ded4ad 100644 Binary files a/resources/images/tokens/mainnet/TRX@3x.png and b/resources/images/tokens/mainnet/TRX@3x.png differ diff --git a/resources/images/tokens/mainnet/TUSD.png b/resources/images/tokens/mainnet/TUSD.png new file mode 100644 index 00000000000..02965edc59a Binary files /dev/null and b/resources/images/tokens/mainnet/TUSD.png differ diff --git a/resources/images/tokens/mainnet/TUSD@2x.png b/resources/images/tokens/mainnet/TUSD@2x.png new file mode 100644 index 00000000000..3145e2e21b2 Binary files /dev/null and b/resources/images/tokens/mainnet/TUSD@2x.png differ diff --git a/resources/images/tokens/mainnet/TUSD@3x.png b/resources/images/tokens/mainnet/TUSD@3x.png new file mode 100644 index 00000000000..423845322c0 Binary files /dev/null and b/resources/images/tokens/mainnet/TUSD@3x.png differ diff --git a/resources/images/tokens/mainnet/UBI.png b/resources/images/tokens/mainnet/UBI.png new file mode 100644 index 00000000000..31a9a0739bf Binary files /dev/null and b/resources/images/tokens/mainnet/UBI.png differ diff --git a/resources/images/tokens/mainnet/UBI@2x.png b/resources/images/tokens/mainnet/UBI@2x.png new file mode 100644 index 00000000000..ea998d9aab7 Binary files /dev/null and b/resources/images/tokens/mainnet/UBI@2x.png differ diff --git a/resources/images/tokens/mainnet/UBI@3x.png b/resources/images/tokens/mainnet/UBI@3x.png new file mode 100644 index 00000000000..c729dba5b6d Binary files /dev/null and b/resources/images/tokens/mainnet/UBI@3x.png differ diff --git a/resources/images/tokens/mainnet/UBT.png b/resources/images/tokens/mainnet/UBT.png new file mode 100644 index 00000000000..125dc93df89 Binary files /dev/null and b/resources/images/tokens/mainnet/UBT.png differ diff --git a/resources/images/tokens/mainnet/UBT@2x.png b/resources/images/tokens/mainnet/UBT@2x.png new file mode 100644 index 00000000000..e8682e3022f Binary files /dev/null and b/resources/images/tokens/mainnet/UBT@2x.png differ diff --git a/resources/images/tokens/mainnet/UBT@3x.png b/resources/images/tokens/mainnet/UBT@3x.png new file mode 100644 index 00000000000..6d660fb8f33 Binary files /dev/null and b/resources/images/tokens/mainnet/UBT@3x.png differ diff --git a/resources/images/tokens/mainnet/UKG.png b/resources/images/tokens/mainnet/UKG.png index f185a94881a..6866bfe5a3a 100644 Binary files a/resources/images/tokens/mainnet/UKG.png and b/resources/images/tokens/mainnet/UKG.png differ diff --git a/resources/images/tokens/mainnet/UKG@2x.png b/resources/images/tokens/mainnet/UKG@2x.png index eb1dbdb734d..9d8eeb3cb5b 100644 Binary files a/resources/images/tokens/mainnet/UKG@2x.png and b/resources/images/tokens/mainnet/UKG@2x.png differ diff --git a/resources/images/tokens/mainnet/UKG@3x.png b/resources/images/tokens/mainnet/UKG@3x.png index f3e04721bfa..231a09e1774 100644 Binary files a/resources/images/tokens/mainnet/UKG@3x.png and b/resources/images/tokens/mainnet/UKG@3x.png differ diff --git a/resources/images/tokens/mainnet/UNI.png b/resources/images/tokens/mainnet/UNI.png new file mode 100644 index 00000000000..6d267c04df7 Binary files /dev/null and b/resources/images/tokens/mainnet/UNI.png differ diff --git a/resources/images/tokens/mainnet/UNI@2x.png b/resources/images/tokens/mainnet/UNI@2x.png new file mode 100644 index 00000000000..faaa1953d7e Binary files /dev/null and b/resources/images/tokens/mainnet/UNI@2x.png differ diff --git a/resources/images/tokens/mainnet/UNI@3x.png b/resources/images/tokens/mainnet/UNI@3x.png new file mode 100644 index 00000000000..60ea845fb3b Binary files /dev/null and b/resources/images/tokens/mainnet/UNI@3x.png differ diff --git a/resources/images/tokens/mainnet/UPP.png b/resources/images/tokens/mainnet/UPP.png new file mode 100644 index 00000000000..d86eab63614 Binary files /dev/null and b/resources/images/tokens/mainnet/UPP.png differ diff --git a/resources/images/tokens/mainnet/UPP@2x.png b/resources/images/tokens/mainnet/UPP@2x.png new file mode 100644 index 00000000000..58d5abe0e27 Binary files /dev/null and b/resources/images/tokens/mainnet/UPP@2x.png differ diff --git a/resources/images/tokens/mainnet/UPP@3x.png b/resources/images/tokens/mainnet/UPP@3x.png new file mode 100644 index 00000000000..5d83d27d846 Binary files /dev/null and b/resources/images/tokens/mainnet/UPP@3x.png differ diff --git a/resources/images/tokens/mainnet/USDC.png b/resources/images/tokens/mainnet/USDC.png new file mode 100644 index 00000000000..1eae924a4d3 Binary files /dev/null and b/resources/images/tokens/mainnet/USDC.png differ diff --git a/resources/images/tokens/mainnet/USDC@2x.png b/resources/images/tokens/mainnet/USDC@2x.png new file mode 100644 index 00000000000..c791e33fdd5 Binary files /dev/null and b/resources/images/tokens/mainnet/USDC@2x.png differ diff --git a/resources/images/tokens/mainnet/USDC@3x.png b/resources/images/tokens/mainnet/USDC@3x.png new file mode 100644 index 00000000000..fa1e07b65bc Binary files /dev/null and b/resources/images/tokens/mainnet/USDC@3x.png differ diff --git a/resources/images/tokens/mainnet/USDS.png b/resources/images/tokens/mainnet/USDS.png new file mode 100644 index 00000000000..c5627a0c0bc Binary files /dev/null and b/resources/images/tokens/mainnet/USDS.png differ diff --git a/resources/images/tokens/mainnet/USDS@2x.png b/resources/images/tokens/mainnet/USDS@2x.png new file mode 100644 index 00000000000..f80b7f7b7ff Binary files /dev/null and b/resources/images/tokens/mainnet/USDS@2x.png differ diff --git a/resources/images/tokens/mainnet/USDS@3x.png b/resources/images/tokens/mainnet/USDS@3x.png new file mode 100644 index 00000000000..73828464037 Binary files /dev/null and b/resources/images/tokens/mainnet/USDS@3x.png differ diff --git a/resources/images/tokens/mainnet/USDT.png b/resources/images/tokens/mainnet/USDT.png new file mode 100644 index 00000000000..f4867a97d4f Binary files /dev/null and b/resources/images/tokens/mainnet/USDT.png differ diff --git a/resources/images/tokens/mainnet/USDT@2x.png b/resources/images/tokens/mainnet/USDT@2x.png new file mode 100644 index 00000000000..12677b6d635 Binary files /dev/null and b/resources/images/tokens/mainnet/USDT@2x.png differ diff --git a/resources/images/tokens/mainnet/USDT@3x.png b/resources/images/tokens/mainnet/USDT@3x.png new file mode 100644 index 00000000000..9a9b5ac8157 Binary files /dev/null and b/resources/images/tokens/mainnet/USDT@3x.png differ diff --git a/resources/images/tokens/mainnet/VERI.png b/resources/images/tokens/mainnet/VERI.png index 51ecff0e248..c7ef9556825 100644 Binary files a/resources/images/tokens/mainnet/VERI.png and b/resources/images/tokens/mainnet/VERI.png differ diff --git a/resources/images/tokens/mainnet/VERI@2x.png b/resources/images/tokens/mainnet/VERI@2x.png index 8732360c97c..85323971d54 100644 Binary files a/resources/images/tokens/mainnet/VERI@2x.png and b/resources/images/tokens/mainnet/VERI@2x.png differ diff --git a/resources/images/tokens/mainnet/VERI@3x.png b/resources/images/tokens/mainnet/VERI@3x.png index ad606bcf97e..72408c659aa 100644 Binary files a/resources/images/tokens/mainnet/VERI@3x.png and b/resources/images/tokens/mainnet/VERI@3x.png differ diff --git a/resources/images/tokens/mainnet/VGX.png b/resources/images/tokens/mainnet/VGX.png new file mode 100644 index 00000000000..241bf4bb488 Binary files /dev/null and b/resources/images/tokens/mainnet/VGX.png differ diff --git a/resources/images/tokens/mainnet/VGX@2x.png b/resources/images/tokens/mainnet/VGX@2x.png new file mode 100644 index 00000000000..3f835d9383a Binary files /dev/null and b/resources/images/tokens/mainnet/VGX@2x.png differ diff --git a/resources/images/tokens/mainnet/VGX@3x.png b/resources/images/tokens/mainnet/VGX@3x.png new file mode 100644 index 00000000000..fbef5d06f5e Binary files /dev/null and b/resources/images/tokens/mainnet/VGX@3x.png differ diff --git a/resources/images/tokens/mainnet/VIB.png b/resources/images/tokens/mainnet/VIB.png index d5848560c88..73de4dcbe78 100644 Binary files a/resources/images/tokens/mainnet/VIB.png and b/resources/images/tokens/mainnet/VIB.png differ diff --git a/resources/images/tokens/mainnet/VIB@2x.png b/resources/images/tokens/mainnet/VIB@2x.png index 66a499352d7..0caebcefd3b 100644 Binary files a/resources/images/tokens/mainnet/VIB@2x.png and b/resources/images/tokens/mainnet/VIB@2x.png differ diff --git a/resources/images/tokens/mainnet/VIB@3x.png b/resources/images/tokens/mainnet/VIB@3x.png index 5ad778adfdd..0a74a946368 100644 Binary files a/resources/images/tokens/mainnet/VIB@3x.png and b/resources/images/tokens/mainnet/VIB@3x.png differ diff --git a/resources/images/tokens/mainnet/WBTC.png b/resources/images/tokens/mainnet/WBTC.png new file mode 100644 index 00000000000..782a12f0243 Binary files /dev/null and b/resources/images/tokens/mainnet/WBTC.png differ diff --git a/resources/images/tokens/mainnet/WBTC@2x.png b/resources/images/tokens/mainnet/WBTC@2x.png new file mode 100644 index 00000000000..096e86ac4f8 Binary files /dev/null and b/resources/images/tokens/mainnet/WBTC@2x.png differ diff --git a/resources/images/tokens/mainnet/WBTC@3x.png b/resources/images/tokens/mainnet/WBTC@3x.png new file mode 100644 index 00000000000..8f6a28053ee Binary files /dev/null and b/resources/images/tokens/mainnet/WBTC@3x.png differ diff --git a/resources/images/tokens/mainnet/WETH.png b/resources/images/tokens/mainnet/WETH.png new file mode 100644 index 00000000000..270129529db Binary files /dev/null and b/resources/images/tokens/mainnet/WETH.png differ diff --git a/resources/images/tokens/mainnet/WETH@2x.png b/resources/images/tokens/mainnet/WETH@2x.png new file mode 100644 index 00000000000..fa4cfdd17ad Binary files /dev/null and b/resources/images/tokens/mainnet/WETH@2x.png differ diff --git a/resources/images/tokens/mainnet/WETH@3x.png b/resources/images/tokens/mainnet/WETH@3x.png new file mode 100644 index 00000000000..6cadd3e9dd2 Binary files /dev/null and b/resources/images/tokens/mainnet/WETH@3x.png differ diff --git a/resources/images/tokens/mainnet/WINGS.png b/resources/images/tokens/mainnet/WINGS.png index ed07b1949bd..2e412a9ec21 100644 Binary files a/resources/images/tokens/mainnet/WINGS.png and b/resources/images/tokens/mainnet/WINGS.png differ diff --git a/resources/images/tokens/mainnet/WINGS@2x.png b/resources/images/tokens/mainnet/WINGS@2x.png index ffa5b2584f6..6d41d9afae5 100644 Binary files a/resources/images/tokens/mainnet/WINGS@2x.png and b/resources/images/tokens/mainnet/WINGS@2x.png differ diff --git a/resources/images/tokens/mainnet/WINGS@3x.png b/resources/images/tokens/mainnet/WINGS@3x.png index 3df3dddd98d..7fc1b448d6a 100644 Binary files a/resources/images/tokens/mainnet/WINGS@3x.png and b/resources/images/tokens/mainnet/WINGS@3x.png differ diff --git a/resources/images/tokens/mainnet/WTC.png b/resources/images/tokens/mainnet/WTC.png index 65398986d06..52d66559f3e 100644 Binary files a/resources/images/tokens/mainnet/WTC.png and b/resources/images/tokens/mainnet/WTC.png differ diff --git a/resources/images/tokens/mainnet/WTC@2x.png b/resources/images/tokens/mainnet/WTC@2x.png index f92185c0c55..59196ffc79b 100644 Binary files a/resources/images/tokens/mainnet/WTC@2x.png and b/resources/images/tokens/mainnet/WTC@2x.png differ diff --git a/resources/images/tokens/mainnet/WTC@3x.png b/resources/images/tokens/mainnet/WTC@3x.png index 0b1976d8288..eef48274d1a 100644 Binary files a/resources/images/tokens/mainnet/WTC@3x.png and b/resources/images/tokens/mainnet/WTC@3x.png differ diff --git a/resources/images/tokens/mainnet/WaBi.png b/resources/images/tokens/mainnet/WaBi.png new file mode 100644 index 00000000000..24d01b15e73 Binary files /dev/null and b/resources/images/tokens/mainnet/WaBi.png differ diff --git a/resources/images/tokens/mainnet/WaBi@2x.png b/resources/images/tokens/mainnet/WaBi@2x.png new file mode 100644 index 00000000000..28c6a40fdcd Binary files /dev/null and b/resources/images/tokens/mainnet/WaBi@2x.png differ diff --git a/resources/images/tokens/mainnet/WaBi@3x.png b/resources/images/tokens/mainnet/WaBi@3x.png new file mode 100644 index 00000000000..8cd6b007562 Binary files /dev/null and b/resources/images/tokens/mainnet/WaBi@3x.png differ diff --git a/resources/images/tokens/mainnet/XAUR.png b/resources/images/tokens/mainnet/XAUR.png index bd1a89efd30..fcef37aaa2f 100644 Binary files a/resources/images/tokens/mainnet/XAUR.png and b/resources/images/tokens/mainnet/XAUR.png differ diff --git a/resources/images/tokens/mainnet/XAUR@2x.png b/resources/images/tokens/mainnet/XAUR@2x.png index 122d9684e77..24caf996830 100644 Binary files a/resources/images/tokens/mainnet/XAUR@2x.png and b/resources/images/tokens/mainnet/XAUR@2x.png differ diff --git a/resources/images/tokens/mainnet/XAUR@3x.png b/resources/images/tokens/mainnet/XAUR@3x.png index 5537b8d9322..a708452c641 100644 Binary files a/resources/images/tokens/mainnet/XAUR@3x.png and b/resources/images/tokens/mainnet/XAUR@3x.png differ diff --git a/resources/images/tokens/mainnet/XPA.png b/resources/images/tokens/mainnet/XPA.png index 58b3ec5e69d..b293ac55dd4 100644 Binary files a/resources/images/tokens/mainnet/XPA.png and b/resources/images/tokens/mainnet/XPA.png differ diff --git a/resources/images/tokens/mainnet/XPA@2x.png b/resources/images/tokens/mainnet/XPA@2x.png index 459bf310b2d..fda6713b7ce 100644 Binary files a/resources/images/tokens/mainnet/XPA@2x.png and b/resources/images/tokens/mainnet/XPA@2x.png differ diff --git a/resources/images/tokens/mainnet/XPA@3x.png b/resources/images/tokens/mainnet/XPA@3x.png index fe8bbc0c446..1e940bdc8c3 100644 Binary files a/resources/images/tokens/mainnet/XPA@3x.png and b/resources/images/tokens/mainnet/XPA@3x.png differ diff --git a/resources/images/tokens/mainnet/XRL.png b/resources/images/tokens/mainnet/XRL.png index bbd747ed111..24c5a44b3f9 100644 Binary files a/resources/images/tokens/mainnet/XRL.png and b/resources/images/tokens/mainnet/XRL.png differ diff --git a/resources/images/tokens/mainnet/XRL@2x.png b/resources/images/tokens/mainnet/XRL@2x.png index c69df2550b3..820ae27bfcb 100644 Binary files a/resources/images/tokens/mainnet/XRL@2x.png and b/resources/images/tokens/mainnet/XRL@2x.png differ diff --git a/resources/images/tokens/mainnet/XRL@3x.png b/resources/images/tokens/mainnet/XRL@3x.png index f1fe62b55c0..430770dab9a 100644 Binary files a/resources/images/tokens/mainnet/XRL@3x.png and b/resources/images/tokens/mainnet/XRL@3x.png differ diff --git a/resources/images/tokens/mainnet/XUC.png b/resources/images/tokens/mainnet/XUC.png index 2a41837037d..b00f2e0b8c1 100644 Binary files a/resources/images/tokens/mainnet/XUC.png and b/resources/images/tokens/mainnet/XUC.png differ diff --git a/resources/images/tokens/mainnet/XUC@2x.png b/resources/images/tokens/mainnet/XUC@2x.png index 383d3cfda42..1d5bad323be 100644 Binary files a/resources/images/tokens/mainnet/XUC@2x.png and b/resources/images/tokens/mainnet/XUC@2x.png differ diff --git a/resources/images/tokens/mainnet/XUC@3x.png b/resources/images/tokens/mainnet/XUC@3x.png index 9ad1d155262..c767ae24654 100644 Binary files a/resources/images/tokens/mainnet/XUC@3x.png and b/resources/images/tokens/mainnet/XUC@3x.png differ diff --git a/resources/images/tokens/mainnet/ZRX.png b/resources/images/tokens/mainnet/ZRX.png index 5a0ccfcb0cf..2ba4354b60c 100644 Binary files a/resources/images/tokens/mainnet/ZRX.png and b/resources/images/tokens/mainnet/ZRX.png differ diff --git a/resources/images/tokens/mainnet/ZRX@2x.png b/resources/images/tokens/mainnet/ZRX@2x.png index 328abf6788e..fc6163bd7f3 100644 Binary files a/resources/images/tokens/mainnet/ZRX@2x.png and b/resources/images/tokens/mainnet/ZRX@2x.png differ diff --git a/resources/images/tokens/mainnet/ZRX@3x.png b/resources/images/tokens/mainnet/ZRX@3x.png index c4ce570484d..3340b73c14d 100644 Binary files a/resources/images/tokens/mainnet/ZRX@3x.png and b/resources/images/tokens/mainnet/ZRX@3x.png differ diff --git a/resources/images/tokens/mainnet/ZSC.png b/resources/images/tokens/mainnet/ZSC.png index f5a60f3cd2f..74deb12aae2 100644 Binary files a/resources/images/tokens/mainnet/ZSC.png and b/resources/images/tokens/mainnet/ZSC.png differ diff --git a/resources/images/tokens/mainnet/ZSC@2x.png b/resources/images/tokens/mainnet/ZSC@2x.png index 07954de193e..85567b67714 100644 Binary files a/resources/images/tokens/mainnet/ZSC@2x.png and b/resources/images/tokens/mainnet/ZSC@2x.png differ diff --git a/resources/images/tokens/mainnet/ZSC@3x.png b/resources/images/tokens/mainnet/ZSC@3x.png index 3fbc99628cf..0661275afa9 100644 Binary files a/resources/images/tokens/mainnet/ZSC@3x.png and b/resources/images/tokens/mainnet/ZSC@3x.png differ diff --git a/resources/images/tokens/mainnet/aUSDC.png b/resources/images/tokens/mainnet/aUSDC.png new file mode 100644 index 00000000000..f58150bd155 Binary files /dev/null and b/resources/images/tokens/mainnet/aUSDC.png differ diff --git a/resources/images/tokens/mainnet/aUSDC@2x.png b/resources/images/tokens/mainnet/aUSDC@2x.png new file mode 100644 index 00000000000..aab8e488ce3 Binary files /dev/null and b/resources/images/tokens/mainnet/aUSDC@2x.png differ diff --git a/resources/images/tokens/mainnet/aUSDC@3x.png b/resources/images/tokens/mainnet/aUSDC@3x.png new file mode 100644 index 00000000000..36412c1ff14 Binary files /dev/null and b/resources/images/tokens/mainnet/aUSDC@3x.png differ diff --git a/resources/images/tokens/mainnet/cDAI.png b/resources/images/tokens/mainnet/cDAI.png new file mode 100644 index 00000000000..fcf52ea020f Binary files /dev/null and b/resources/images/tokens/mainnet/cDAI.png differ diff --git a/resources/images/tokens/mainnet/cDAI@2x.png b/resources/images/tokens/mainnet/cDAI@2x.png new file mode 100644 index 00000000000..e9b598f827f Binary files /dev/null and b/resources/images/tokens/mainnet/cDAI@2x.png differ diff --git a/resources/images/tokens/mainnet/cDAI@3x.png b/resources/images/tokens/mainnet/cDAI@3x.png new file mode 100644 index 00000000000..7181be5f6d1 Binary files /dev/null and b/resources/images/tokens/mainnet/cDAI@3x.png differ diff --git a/resources/images/tokens/mainnet/sUSD.png b/resources/images/tokens/mainnet/sUSD.png new file mode 100644 index 00000000000..ffb6e54d1e2 Binary files /dev/null and b/resources/images/tokens/mainnet/sUSD.png differ diff --git a/resources/images/tokens/mainnet/sUSD@2x.png b/resources/images/tokens/mainnet/sUSD@2x.png new file mode 100644 index 00000000000..53037ead634 Binary files /dev/null and b/resources/images/tokens/mainnet/sUSD@2x.png differ diff --git a/resources/images/tokens/mainnet/sUSD@3x.png b/resources/images/tokens/mainnet/sUSD@3x.png new file mode 100644 index 00000000000..f044267c522 Binary files /dev/null and b/resources/images/tokens/mainnet/sUSD@3x.png differ diff --git a/resources/images/tokens/poa/0-native.png b/resources/images/tokens/poa/0-native.png new file mode 100644 index 00000000000..e1161443dea Binary files /dev/null and b/resources/images/tokens/poa/0-native.png differ diff --git a/resources/images/tokens/poa/0-native@2x.png b/resources/images/tokens/poa/0-native@2x.png new file mode 100644 index 00000000000..b1f55de4315 Binary files /dev/null and b/resources/images/tokens/poa/0-native@2x.png differ diff --git a/resources/images/tokens/poa/0-native@3x.png b/resources/images/tokens/poa/0-native@3x.png new file mode 100644 index 00000000000..e9b4e356346 Binary files /dev/null and b/resources/images/tokens/poa/0-native@3x.png differ diff --git a/resources/images/tokens/rinkeby/KDO.png b/resources/images/tokens/rinkeby/KDO.png new file mode 100644 index 00000000000..482fdb25012 Binary files /dev/null and b/resources/images/tokens/rinkeby/KDO.png differ diff --git a/resources/images/tokens/rinkeby/KDO@2x.png b/resources/images/tokens/rinkeby/KDO@2x.png new file mode 100644 index 00000000000..b8d9c5a7472 Binary files /dev/null and b/resources/images/tokens/rinkeby/KDO@2x.png differ diff --git a/resources/images/tokens/rinkeby/KDO@3x.png b/resources/images/tokens/rinkeby/KDO@3x.png new file mode 100644 index 00000000000..bde4fa25a44 Binary files /dev/null and b/resources/images/tokens/rinkeby/KDO@3x.png differ diff --git a/resources/images/tokens/rinkeby/MOKSHA.png b/resources/images/tokens/rinkeby/MOKSHA.png index ce5da72ccd2..9779617797d 100644 Binary files a/resources/images/tokens/rinkeby/MOKSHA.png and b/resources/images/tokens/rinkeby/MOKSHA.png differ diff --git a/resources/images/tokens/rinkeby/MOKSHA@2x.png b/resources/images/tokens/rinkeby/MOKSHA@2x.png index 5ee0bb0c04b..dfbae4d22fb 100644 Binary files a/resources/images/tokens/rinkeby/MOKSHA@2x.png and b/resources/images/tokens/rinkeby/MOKSHA@2x.png differ diff --git a/resources/images/tokens/rinkeby/MOKSHA@3x.png b/resources/images/tokens/rinkeby/MOKSHA@3x.png index eedd282aaa2..dc717cda37f 100644 Binary files a/resources/images/tokens/rinkeby/MOKSHA@3x.png and b/resources/images/tokens/rinkeby/MOKSHA@3x.png differ diff --git a/resources/images/tokens/testnet/0-native.png b/resources/images/tokens/testnet/0-native.png new file mode 100644 index 00000000000..13efc324e85 Binary files /dev/null and b/resources/images/tokens/testnet/0-native.png differ diff --git a/resources/images/tokens/testnet/0-native@2x.png b/resources/images/tokens/testnet/0-native@2x.png new file mode 100644 index 00000000000..27788bbae33 Binary files /dev/null and b/resources/images/tokens/testnet/0-native@2x.png differ diff --git a/resources/images/tokens/testnet/0-native@3x.png b/resources/images/tokens/testnet/0-native@3x.png new file mode 100644 index 00000000000..88dc57b08d1 Binary files /dev/null and b/resources/images/tokens/testnet/0-native@3x.png differ diff --git a/resources/images/tokens/testnet/ADI.png b/resources/images/tokens/testnet/ADI.png index e5dcaf80b09..2140fb52114 100644 Binary files a/resources/images/tokens/testnet/ADI.png and b/resources/images/tokens/testnet/ADI.png differ diff --git a/resources/images/tokens/testnet/ADI@2x.png b/resources/images/tokens/testnet/ADI@2x.png index a7e5315e14d..e72293fc706 100644 Binary files a/resources/images/tokens/testnet/ADI@2x.png and b/resources/images/tokens/testnet/ADI@2x.png differ diff --git a/resources/images/tokens/testnet/ADI@3x.png b/resources/images/tokens/testnet/ADI@3x.png index 990c6e4aadb..8a3638f7c67 100644 Binary files a/resources/images/tokens/testnet/ADI@3x.png and b/resources/images/tokens/testnet/ADI@3x.png differ diff --git a/resources/images/tokens/testnet/HND.png b/resources/images/tokens/testnet/HND.png index c0af8bf3829..d5136990c5c 100644 Binary files a/resources/images/tokens/testnet/HND.png and b/resources/images/tokens/testnet/HND.png differ diff --git a/resources/images/tokens/testnet/HND@2x.png b/resources/images/tokens/testnet/HND@2x.png index bb0e876d351..459f6c6b0c1 100644 Binary files a/resources/images/tokens/testnet/HND@2x.png and b/resources/images/tokens/testnet/HND@2x.png differ diff --git a/resources/images/tokens/testnet/HND@3x.png b/resources/images/tokens/testnet/HND@3x.png index ebee8108b8e..5d54d547664 100644 Binary files a/resources/images/tokens/testnet/HND@3x.png and b/resources/images/tokens/testnet/HND@3x.png differ diff --git a/resources/images/tokens/testnet/LXS.png b/resources/images/tokens/testnet/LXS.png index d0760d9bad6..3c1b036108c 100644 Binary files a/resources/images/tokens/testnet/LXS.png and b/resources/images/tokens/testnet/LXS.png differ diff --git a/resources/images/tokens/testnet/LXS@2x.png b/resources/images/tokens/testnet/LXS@2x.png index 700c252f796..7f2d2949002 100644 Binary files a/resources/images/tokens/testnet/LXS@2x.png and b/resources/images/tokens/testnet/LXS@2x.png differ diff --git a/resources/images/tokens/testnet/LXS@3x.png b/resources/images/tokens/testnet/LXS@3x.png index a1c86fea873..7278f6672bf 100644 Binary files a/resources/images/tokens/testnet/LXS@3x.png and b/resources/images/tokens/testnet/LXS@3x.png differ diff --git a/resources/images/tokens/testnet/MDS@3x.png b/resources/images/tokens/testnet/MDS@3x.png index 945071c958a..3fd54c19297 100644 Binary files a/resources/images/tokens/testnet/MDS@3x.png and b/resources/images/tokens/testnet/MDS@3x.png differ diff --git a/resources/images/tokens/testnet/SUPR.png b/resources/images/tokens/testnet/SUPR.png new file mode 100644 index 00000000000..7d8faa483d0 Binary files /dev/null and b/resources/images/tokens/testnet/SUPR.png differ diff --git a/resources/images/tokens/testnet/WGN.png b/resources/images/tokens/testnet/WGN.png index c2a9e79120c..bb8bb6f0623 100644 Binary files a/resources/images/tokens/testnet/WGN.png and b/resources/images/tokens/testnet/WGN.png differ diff --git a/resources/images/tokens/testnet/WGN@2x.png b/resources/images/tokens/testnet/WGN@2x.png index b07e74a41ee..3a98765dadc 100644 Binary files a/resources/images/tokens/testnet/WGN@2x.png and b/resources/images/tokens/testnet/WGN@2x.png differ diff --git a/resources/images/tokens/testnet/WGN@3x.png b/resources/images/tokens/testnet/WGN@3x.png index a269bcc367f..fe29c8b52a2 100644 Binary files a/resources/images/tokens/testnet/WGN@3x.png and b/resources/images/tokens/testnet/WGN@3x.png differ diff --git a/resources/images/tokens/testnet/ant.png b/resources/images/tokens/testnet/ant.png new file mode 100644 index 00000000000..02d68f5639b Binary files /dev/null and b/resources/images/tokens/testnet/ant.png differ diff --git a/resources/images/tokens/testnet/ant@2x.png b/resources/images/tokens/testnet/ant@2x.png new file mode 100644 index 00000000000..0ac0cc8306c Binary files /dev/null and b/resources/images/tokens/testnet/ant@2x.png differ diff --git a/resources/images/tokens/testnet/ant@3x.png b/resources/images/tokens/testnet/ant@3x.png new file mode 100644 index 00000000000..1487b7d61a7 Binary files /dev/null and b/resources/images/tokens/testnet/ant@3x.png differ diff --git a/resources/images/tokens/testnet/aragon.png b/resources/images/tokens/testnet/aragon.png index 2a4396adf8a..02d68f5639b 100644 Binary files a/resources/images/tokens/testnet/aragon.png and b/resources/images/tokens/testnet/aragon.png differ diff --git a/resources/images/tokens/testnet/aragon@2x.png b/resources/images/tokens/testnet/aragon@2x.png index 348fff2f7ea..0ac0cc8306c 100644 Binary files a/resources/images/tokens/testnet/aragon@2x.png and b/resources/images/tokens/testnet/aragon@2x.png differ diff --git a/resources/images/tokens/testnet/aragon@3x.png b/resources/images/tokens/testnet/aragon@3x.png index 43c36b21a95..1487b7d61a7 100644 Binary files a/resources/images/tokens/testnet/aragon@3x.png and b/resources/images/tokens/testnet/aragon@3x.png differ diff --git a/resources/images/tokens/xdai/0-native.png b/resources/images/tokens/xdai/0-native.png new file mode 100644 index 00000000000..129b9c64ae1 Binary files /dev/null and b/resources/images/tokens/xdai/0-native.png differ diff --git a/resources/images/tokens/xdai/0-native@2x.png b/resources/images/tokens/xdai/0-native@2x.png new file mode 100644 index 00000000000..5d85a961c42 Binary files /dev/null and b/resources/images/tokens/xdai/0-native@2x.png differ diff --git a/resources/images/tokens/xdai/0-native@3x.png b/resources/images/tokens/xdai/0-native@3x.png new file mode 100644 index 00000000000..9241c96a819 Binary files /dev/null and b/resources/images/tokens/xdai/0-native@3x.png differ diff --git a/resources/images/tokens/xdai/BUFF.png b/resources/images/tokens/xdai/BUFF.png new file mode 100644 index 00000000000..b8adf6b3ade Binary files /dev/null and b/resources/images/tokens/xdai/BUFF.png differ diff --git a/resources/images/tokens/xdai/BUFF@2x.png b/resources/images/tokens/xdai/BUFF@2x.png new file mode 100644 index 00000000000..c037eef9291 Binary files /dev/null and b/resources/images/tokens/xdai/BUFF@2x.png differ diff --git a/resources/images/tokens/xdai/BUFF@3x.png b/resources/images/tokens/xdai/BUFF@3x.png new file mode 100644 index 00000000000..a8f5ff6d792 Binary files /dev/null and b/resources/images/tokens/xdai/BUFF@3x.png differ diff --git a/resources/images/ui/analytics-image.png b/resources/images/ui/analytics-image.png deleted file mode 100644 index 7e8590c8988..00000000000 Binary files a/resources/images/ui/analytics-image.png and /dev/null differ diff --git a/resources/images/ui/browser-dark@2x.jpg b/resources/images/ui/browser-dark@2x.jpg new file mode 100755 index 00000000000..56ffbf1e994 Binary files /dev/null and b/resources/images/ui/browser-dark@2x.jpg differ diff --git a/resources/images/ui/browser-dark@3x.jpg b/resources/images/ui/browser-dark@3x.jpg new file mode 100755 index 00000000000..d2c4961d4a6 Binary files /dev/null and b/resources/images/ui/browser-dark@3x.jpg differ diff --git a/resources/images/ui/browser@2x.jpg b/resources/images/ui/browser@2x.jpg new file mode 100755 index 00000000000..e49bba07169 Binary files /dev/null and b/resources/images/ui/browser@2x.jpg differ diff --git a/resources/images/ui/browser@3x.jpg b/resources/images/ui/browser@3x.jpg new file mode 100755 index 00000000000..03db2a3a114 Binary files /dev/null and b/resources/images/ui/browser@3x.jpg differ diff --git a/resources/images/ui/chat-dark@2x.jpg b/resources/images/ui/chat-dark@2x.jpg new file mode 100755 index 00000000000..7332a894956 Binary files /dev/null and b/resources/images/ui/chat-dark@2x.jpg differ diff --git a/resources/images/ui/chat-dark@3x.jpg b/resources/images/ui/chat-dark@3x.jpg new file mode 100755 index 00000000000..eeb9eb53735 Binary files /dev/null and b/resources/images/ui/chat-dark@3x.jpg differ diff --git a/resources/images/ui/chat@2x.jpg b/resources/images/ui/chat@2x.jpg new file mode 100755 index 00000000000..7e671f02df0 Binary files /dev/null and b/resources/images/ui/chat@2x.jpg differ diff --git a/resources/images/ui/chat@3x.jpg b/resources/images/ui/chat@3x.jpg new file mode 100755 index 00000000000..9cb00ced8d0 Binary files /dev/null and b/resources/images/ui/chat@3x.jpg differ diff --git a/resources/images/ui/coinbase-community-background@2x.png b/resources/images/ui/coinbase-community-background@2x.png new file mode 100644 index 00000000000..ac46114bc14 Binary files /dev/null and b/resources/images/ui/coinbase-community-background@2x.png differ diff --git a/resources/images/ui/coinbase_community-background@3x.png b/resources/images/ui/coinbase_community-background@3x.png new file mode 100644 index 00000000000..cfbb95e23a8 Binary files /dev/null and b/resources/images/ui/coinbase_community-background@3x.png differ diff --git a/resources/images/ui/collectible-dark@2x.png b/resources/images/ui/collectible-dark@2x.png new file mode 100644 index 00000000000..fc0c066e5bf Binary files /dev/null and b/resources/images/ui/collectible-dark@2x.png differ diff --git a/resources/images/ui/collectible-dark@3x.png b/resources/images/ui/collectible-dark@3x.png new file mode 100644 index 00000000000..4bc40bf7fad Binary files /dev/null and b/resources/images/ui/collectible-dark@3x.png differ diff --git a/resources/images/ui/collectible@2x.png b/resources/images/ui/collectible@2x.png new file mode 100644 index 00000000000..07141bb38bf Binary files /dev/null and b/resources/images/ui/collectible@2x.png differ diff --git a/resources/images/ui/collectible@3x.png b/resources/images/ui/collectible@3x.png new file mode 100644 index 00000000000..daf70ecfcee Binary files /dev/null and b/resources/images/ui/collectible@3x.png differ diff --git a/resources/images/ui/dapp-store.png b/resources/images/ui/dapp-store.png new file mode 100644 index 00000000000..e7ef38f4aec Binary files /dev/null and b/resources/images/ui/dapp-store.png differ diff --git a/resources/images/ui/discover.png b/resources/images/ui/discover.png new file mode 100644 index 00000000000..ed782a40772 Binary files /dev/null and b/resources/images/ui/discover.png differ diff --git a/resources/images/ui/empty-chats-header@2x.png b/resources/images/ui/empty-chats-header@2x.png new file mode 100644 index 00000000000..4d743867564 Binary files /dev/null and b/resources/images/ui/empty-chats-header@2x.png differ diff --git a/resources/images/ui/empty-chats-header@3x.png b/resources/images/ui/empty-chats-header@3x.png new file mode 100644 index 00000000000..9ed0e8488cf Binary files /dev/null and b/resources/images/ui/empty-chats-header@3x.png differ diff --git a/resources/images/ui/empty-hashtags.png b/resources/images/ui/empty-hashtags.png deleted file mode 100644 index b90ccda27d0..00000000000 Binary files a/resources/images/ui/empty-hashtags.png and /dev/null differ diff --git a/resources/images/ui/empty-recent.png b/resources/images/ui/empty-recent.png deleted file mode 100644 index b9173ebdc87..00000000000 Binary files a/resources/images/ui/empty-recent.png and /dev/null differ diff --git a/resources/images/ui/empty-wallet@2x.png b/resources/images/ui/empty-wallet@2x.png new file mode 100644 index 00000000000..0e605bb5337 Binary files /dev/null and b/resources/images/ui/empty-wallet@2x.png differ diff --git a/resources/images/ui/empty-wallet@3x.png b/resources/images/ui/empty-wallet@3x.png new file mode 100644 index 00000000000..6e6e8c5139f Binary files /dev/null and b/resources/images/ui/empty-wallet@3x.png differ diff --git a/resources/images/ui/ens-header-dark@2x.png b/resources/images/ui/ens-header-dark@2x.png new file mode 100644 index 00000000000..758d43f0dd9 Binary files /dev/null and b/resources/images/ui/ens-header-dark@2x.png differ diff --git a/resources/images/ui/ens-header-dark@3x.png b/resources/images/ui/ens-header-dark@3x.png new file mode 100644 index 00000000000..7fdda6cbab1 Binary files /dev/null and b/resources/images/ui/ens-header-dark@3x.png differ diff --git a/resources/images/ui/ens-header@2x.png b/resources/images/ui/ens-header@2x.png new file mode 100644 index 00000000000..ab3a0ed13c9 Binary files /dev/null and b/resources/images/ui/ens-header@2x.png differ diff --git a/resources/images/ui/ens-header@3x.png b/resources/images/ui/ens-header@3x.png new file mode 100644 index 00000000000..162d7e8a4f8 Binary files /dev/null and b/resources/images/ui/ens-header@3x.png differ diff --git a/resources/images/ui/graph@2x.png b/resources/images/ui/graph@2x.png new file mode 100644 index 00000000000..d1da4b4aebf Binary files /dev/null and b/resources/images/ui/graph@2x.png differ diff --git a/resources/images/ui/graph@3x.png b/resources/images/ui/graph@3x.png new file mode 100644 index 00000000000..f80b6ceeed6 Binary files /dev/null and b/resources/images/ui/graph@3x.png differ diff --git a/resources/images/ui/hand-wave.png b/resources/images/ui/hand-wave.png new file mode 100644 index 00000000000..d761bd07d61 Binary files /dev/null and b/resources/images/ui/hand-wave.png differ diff --git a/resources/images/ui/hand-wave@2x.png b/resources/images/ui/hand-wave@2x.png new file mode 100644 index 00000000000..80fd494cd9b Binary files /dev/null and b/resources/images/ui/hand-wave@2x.png differ diff --git a/resources/images/ui/hand-wave@3x.png b/resources/images/ui/hand-wave@3x.png new file mode 100644 index 00000000000..966468d61e5 Binary files /dev/null and b/resources/images/ui/hand-wave@3x.png differ diff --git a/resources/images/ui/hardwallet-card.png b/resources/images/ui/hardwallet-card.png new file mode 100644 index 00000000000..687d1bbe4a9 Binary files /dev/null and b/resources/images/ui/hardwallet-card.png differ diff --git a/resources/images/ui/keycard-connection.png b/resources/images/ui/keycard-connection.png new file mode 100644 index 00000000000..916ef4b226a Binary files /dev/null and b/resources/images/ui/keycard-connection.png differ diff --git a/resources/images/ui/keycard-empty.png b/resources/images/ui/keycard-empty.png new file mode 100644 index 00000000000..4e955eaab1c Binary files /dev/null and b/resources/images/ui/keycard-empty.png differ diff --git a/resources/images/ui/keycard-key.png b/resources/images/ui/keycard-key.png new file mode 100644 index 00000000000..b08864e5ace Binary files /dev/null and b/resources/images/ui/keycard-key.png differ diff --git a/resources/images/ui/keycard-lock.png b/resources/images/ui/keycard-lock.png new file mode 100644 index 00000000000..f5446c08911 Binary files /dev/null and b/resources/images/ui/keycard-lock.png differ diff --git a/resources/images/ui/keycard-logo-blue.png b/resources/images/ui/keycard-logo-blue.png new file mode 100644 index 00000000000..ad505daee45 Binary files /dev/null and b/resources/images/ui/keycard-logo-blue.png differ diff --git a/resources/images/ui/keycard-logo-gray.png b/resources/images/ui/keycard-logo-gray.png new file mode 100644 index 00000000000..f6491618dfa Binary files /dev/null and b/resources/images/ui/keycard-logo-gray.png differ diff --git a/resources/images/ui/keycard-logo.png b/resources/images/ui/keycard-logo.png new file mode 100644 index 00000000000..8cd8acd049f Binary files /dev/null and b/resources/images/ui/keycard-logo.png differ diff --git a/resources/images/ui/keycard-phone.png b/resources/images/ui/keycard-phone.png new file mode 100644 index 00000000000..597eccf8a45 Binary files /dev/null and b/resources/images/ui/keycard-phone.png differ diff --git a/resources/images/ui/keycard-wrong.png b/resources/images/ui/keycard-wrong.png new file mode 100644 index 00000000000..286c3ad44ac Binary files /dev/null and b/resources/images/ui/keycard-wrong.png differ diff --git a/resources/images/ui/keycard.png b/resources/images/ui/keycard.png new file mode 100644 index 00000000000..de20a8cd80e Binary files /dev/null and b/resources/images/ui/keycard.png differ diff --git a/resources/images/ui/keys-dark@2x.jpg b/resources/images/ui/keys-dark@2x.jpg new file mode 100755 index 00000000000..e3289ea47fa Binary files /dev/null and b/resources/images/ui/keys-dark@2x.jpg differ diff --git a/resources/images/ui/keys-dark@3x.jpg b/resources/images/ui/keys-dark@3x.jpg new file mode 100755 index 00000000000..1976d393977 Binary files /dev/null and b/resources/images/ui/keys-dark@3x.jpg differ diff --git a/resources/images/ui/keys@2x.jpg b/resources/images/ui/keys@2x.jpg new file mode 100755 index 00000000000..daf9a3447ab Binary files /dev/null and b/resources/images/ui/keys@2x.jpg differ diff --git a/resources/images/ui/keys@3x.jpg b/resources/images/ui/keys@3x.jpg new file mode 100755 index 00000000000..8d45da19590 Binary files /dev/null and b/resources/images/ui/keys@3x.jpg differ diff --git a/resources/images/ui/lock.png b/resources/images/ui/lock.png index 5972b6f3593..e6f6331905f 100644 Binary files a/resources/images/ui/lock.png and b/resources/images/ui/lock.png differ diff --git a/resources/images/ui/message-gap-circle-bg-dark@2x.png b/resources/images/ui/message-gap-circle-bg-dark@2x.png new file mode 100644 index 00000000000..5912ef9bfca Binary files /dev/null and b/resources/images/ui/message-gap-circle-bg-dark@2x.png differ diff --git a/resources/images/ui/message-gap-circle-bg-dark@3x.png b/resources/images/ui/message-gap-circle-bg-dark@3x.png new file mode 100644 index 00000000000..fef2d8f0f02 Binary files /dev/null and b/resources/images/ui/message-gap-circle-bg-dark@3x.png differ diff --git a/resources/images/ui/message-gap-circle-bg-light@2x.png b/resources/images/ui/message-gap-circle-bg-light@2x.png new file mode 100644 index 00000000000..1d0e919437f Binary files /dev/null and b/resources/images/ui/message-gap-circle-bg-light@2x.png differ diff --git a/resources/images/ui/message-gap-circle-bg-light@3x.png b/resources/images/ui/message-gap-circle-bg-light@3x.png new file mode 100644 index 00000000000..4920191385e Binary files /dev/null and b/resources/images/ui/message-gap-circle-bg-light@3x.png differ diff --git a/resources/images/ui/message-gap-hborder-dark@2x.png b/resources/images/ui/message-gap-hborder-dark@2x.png new file mode 100644 index 00000000000..987875372f4 Binary files /dev/null and b/resources/images/ui/message-gap-hborder-dark@2x.png differ diff --git a/resources/images/ui/message-gap-hborder-dark@3x.png b/resources/images/ui/message-gap-hborder-dark@3x.png new file mode 100644 index 00000000000..f2d4ce33187 Binary files /dev/null and b/resources/images/ui/message-gap-hborder-dark@3x.png differ diff --git a/resources/images/ui/message-gap-hborder-light@2x.png b/resources/images/ui/message-gap-hborder-light@2x.png new file mode 100644 index 00000000000..9793be98718 Binary files /dev/null and b/resources/images/ui/message-gap-hborder-light@2x.png differ diff --git a/resources/images/ui/message-gap-hborder-light@3x.png b/resources/images/ui/message-gap-hborder-light@3x.png new file mode 100644 index 00000000000..4f02fa4092f Binary files /dev/null and b/resources/images/ui/message-gap-hborder-light@3x.png differ diff --git a/resources/images/ui/message-gap-vborder-light@2x.png b/resources/images/ui/message-gap-vborder-light@2x.png new file mode 100644 index 00000000000..c564acbbbc4 Binary files /dev/null and b/resources/images/ui/message-gap-vborder-light@2x.png differ diff --git a/resources/images/ui/message-gap-vborder-light@3x.png b/resources/images/ui/message-gap-vborder-light@3x.png new file mode 100644 index 00000000000..bb5cdef1dbf Binary files /dev/null and b/resources/images/ui/message-gap-vborder-light@3x.png differ diff --git a/resources/images/ui/new-chat-header@2x.png b/resources/images/ui/new-chat-header@2x.png new file mode 100755 index 00000000000..da6169b6051 Binary files /dev/null and b/resources/images/ui/new-chat-header@2x.png differ diff --git a/resources/images/ui/new-chat-header@3x.png b/resources/images/ui/new-chat-header@3x.png new file mode 100755 index 00000000000..f112640152a Binary files /dev/null and b/resources/images/ui/new-chat-header@3x.png differ diff --git a/resources/images/ui/no-contacts-dark@2x.png b/resources/images/ui/no-contacts-dark@2x.png new file mode 100644 index 00000000000..44ce769be51 Binary files /dev/null and b/resources/images/ui/no-contacts-dark@2x.png differ diff --git a/resources/images/ui/no-contacts-dark@3x.png b/resources/images/ui/no-contacts-dark@3x.png new file mode 100644 index 00000000000..eefc229afe2 Binary files /dev/null and b/resources/images/ui/no-contacts-dark@3x.png differ diff --git a/resources/images/ui/no-contacts@2x.png b/resources/images/ui/no-contacts@2x.png new file mode 100644 index 00000000000..42b0454dab2 Binary files /dev/null and b/resources/images/ui/no-contacts@2x.png differ diff --git a/resources/images/ui/no-contacts@3x.png b/resources/images/ui/no-contacts@3x.png new file mode 100644 index 00000000000..8e5c8f881da Binary files /dev/null and b/resources/images/ui/no-contacts@3x.png differ diff --git a/resources/images/ui/not-keycard.png b/resources/images/ui/not-keycard.png new file mode 100644 index 00000000000..b4c5798927d Binary files /dev/null and b/resources/images/ui/not-keycard.png differ diff --git a/resources/images/ui/notifications@2x.png b/resources/images/ui/notifications@2x.png new file mode 100644 index 00000000000..fc798257cb6 Binary files /dev/null and b/resources/images/ui/notifications@2x.png differ diff --git a/resources/images/ui/notifications@3x.png b/resources/images/ui/notifications@3x.png new file mode 100644 index 00000000000..61343590e04 Binary files /dev/null and b/resources/images/ui/notifications@3x.png differ diff --git a/resources/images/ui/onboarding-phone.png b/resources/images/ui/onboarding-phone.png new file mode 100644 index 00000000000..79272178136 Binary files /dev/null and b/resources/images/ui/onboarding-phone.png differ diff --git a/resources/images/ui/phone-nfc-off.png b/resources/images/ui/phone-nfc-off.png new file mode 100644 index 00000000000..b9a044db63e Binary files /dev/null and b/resources/images/ui/phone-nfc-off.png differ diff --git a/resources/images/ui/phone-nfc-on.png b/resources/images/ui/phone-nfc-on.png new file mode 100644 index 00000000000..6f5d00a2d33 Binary files /dev/null and b/resources/images/ui/phone-nfc-on.png differ diff --git a/resources/images/ui/status-logo.png b/resources/images/ui/status-logo.png new file mode 100644 index 00000000000..e3cf13ecafe Binary files /dev/null and b/resources/images/ui/status-logo.png differ diff --git a/resources/images/ui/theme-dark@2x.png b/resources/images/ui/theme-dark@2x.png new file mode 100644 index 00000000000..72a38e3905d Binary files /dev/null and b/resources/images/ui/theme-dark@2x.png differ diff --git a/resources/images/ui/theme-dark@3x.png b/resources/images/ui/theme-dark@3x.png new file mode 100644 index 00000000000..9532c23a2c4 Binary files /dev/null and b/resources/images/ui/theme-dark@3x.png differ diff --git a/resources/images/ui/theme-light@2x.png b/resources/images/ui/theme-light@2x.png new file mode 100644 index 00000000000..397fa406d6b Binary files /dev/null and b/resources/images/ui/theme-light@2x.png differ diff --git a/resources/images/ui/theme-light@3x.png b/resources/images/ui/theme-light@3x.png new file mode 100644 index 00000000000..a964bbdab31 Binary files /dev/null and b/resources/images/ui/theme-light@3x.png differ diff --git a/resources/images/ui/theme-system@2x.png b/resources/images/ui/theme-system@2x.png new file mode 100644 index 00000000000..f7ca8904001 Binary files /dev/null and b/resources/images/ui/theme-system@2x.png differ diff --git a/resources/images/ui/theme-system@3x.png b/resources/images/ui/theme-system@3x.png new file mode 100644 index 00000000000..a81631498fa Binary files /dev/null and b/resources/images/ui/theme-system@3x.png differ diff --git a/resources/images/ui/tribute-to-talk.png b/resources/images/ui/tribute-to-talk.png new file mode 100644 index 00000000000..9a6bea6b1f8 Binary files /dev/null and b/resources/images/ui/tribute-to-talk.png differ diff --git a/resources/images/ui/unfurl-dark@2x.png b/resources/images/ui/unfurl-dark@2x.png new file mode 100644 index 00000000000..580b7268a2a Binary files /dev/null and b/resources/images/ui/unfurl-dark@2x.png differ diff --git a/resources/images/ui/unfurl-dark@3x.png b/resources/images/ui/unfurl-dark@3x.png new file mode 100644 index 00000000000..e8cbce6b549 Binary files /dev/null and b/resources/images/ui/unfurl-dark@3x.png differ diff --git a/resources/images/ui/unfurl@2x.png b/resources/images/ui/unfurl@2x.png new file mode 100644 index 00000000000..11eafa17e44 Binary files /dev/null and b/resources/images/ui/unfurl@2x.png differ diff --git a/resources/images/ui/unfurl@3x.png b/resources/images/ui/unfurl@3x.png new file mode 100644 index 00000000000..5d0b87deada Binary files /dev/null and b/resources/images/ui/unfurl@3x.png differ diff --git a/resources/images/ui/wallet-dark@2x.jpg b/resources/images/ui/wallet-dark@2x.jpg new file mode 100755 index 00000000000..fe1b27f1e2b Binary files /dev/null and b/resources/images/ui/wallet-dark@2x.jpg differ diff --git a/resources/images/ui/wallet-dark@3x.jpg b/resources/images/ui/wallet-dark@3x.jpg new file mode 100755 index 00000000000..a6ec1ba4a04 Binary files /dev/null and b/resources/images/ui/wallet-dark@3x.jpg differ diff --git a/resources/images/ui/wallet-setup.png b/resources/images/ui/wallet-setup.png deleted file mode 100644 index 7e4857cf741..00000000000 Binary files a/resources/images/ui/wallet-setup.png and /dev/null differ diff --git a/resources/images/ui/wallet-welcome.png b/resources/images/ui/wallet-welcome.png deleted file mode 100644 index 0c9acca8583..00000000000 Binary files a/resources/images/ui/wallet-welcome.png and /dev/null differ diff --git a/resources/images/ui/wallet@2x.jpg b/resources/images/ui/wallet@2x.jpg new file mode 100755 index 00000000000..7891717dfc7 Binary files /dev/null and b/resources/images/ui/wallet@2x.jpg differ diff --git a/resources/images/ui/wallet@3x.jpg b/resources/images/ui/wallet@3x.jpg new file mode 100755 index 00000000000..7f49721541a Binary files /dev/null and b/resources/images/ui/wallet@3x.jpg differ diff --git a/resources/images/ui/warning-sign.png b/resources/images/ui/warning-sign.png new file mode 100644 index 00000000000..8d06ee0efa2 Binary files /dev/null and b/resources/images/ui/warning-sign.png differ diff --git a/resources/images/ui/welcome-dark@2x.jpg b/resources/images/ui/welcome-dark@2x.jpg new file mode 100755 index 00000000000..8ca95ec827a Binary files /dev/null and b/resources/images/ui/welcome-dark@2x.jpg differ diff --git a/resources/images/ui/welcome-dark@3x.jpg b/resources/images/ui/welcome-dark@3x.jpg new file mode 100755 index 00000000000..2ccb55c22cb Binary files /dev/null and b/resources/images/ui/welcome-dark@3x.jpg differ diff --git a/resources/images/ui/welcome-image.png b/resources/images/ui/welcome-image.png deleted file mode 100644 index 22d881bf489..00000000000 Binary files a/resources/images/ui/welcome-image.png and /dev/null differ diff --git a/resources/images/ui/welcome@2x.jpg b/resources/images/ui/welcome@2x.jpg new file mode 100644 index 00000000000..b24090ba383 Binary files /dev/null and b/resources/images/ui/welcome@2x.jpg differ diff --git a/resources/images/ui/welcome@3x.jpg b/resources/images/ui/welcome@3x.jpg new file mode 100644 index 00000000000..282a302b1fd Binary files /dev/null and b/resources/images/ui/welcome@3x.jpg differ diff --git a/resources/images/ui2/add-contact@2x.png b/resources/images/ui2/add-contact@2x.png new file mode 100644 index 00000000000..e920a9deb08 Binary files /dev/null and b/resources/images/ui2/add-contact@2x.png differ diff --git a/resources/images/ui2/add-contact@3x.png b/resources/images/ui2/add-contact@3x.png new file mode 100644 index 00000000000..3063b197ca3 Binary files /dev/null and b/resources/images/ui2/add-contact@3x.png differ diff --git a/resources/images/ui2/add-key-to-keycard@2x.png b/resources/images/ui2/add-key-to-keycard@2x.png new file mode 100644 index 00000000000..ea208eb0457 Binary files /dev/null and b/resources/images/ui2/add-key-to-keycard@2x.png differ diff --git a/resources/images/ui2/add-key-to-keycard@3x.png b/resources/images/ui2/add-key-to-keycard@3x.png new file mode 100644 index 00000000000..efeacfa3b12 Binary files /dev/null and b/resources/images/ui2/add-key-to-keycard@3x.png differ diff --git a/resources/images/ui2/angry-man-dark@2x.png b/resources/images/ui2/angry-man-dark@2x.png new file mode 100644 index 00000000000..58f730d09f5 Binary files /dev/null and b/resources/images/ui2/angry-man-dark@2x.png differ diff --git a/resources/images/ui2/angry-man-dark@3x.png b/resources/images/ui2/angry-man-dark@3x.png new file mode 100644 index 00000000000..fe37500b5c2 Binary files /dev/null and b/resources/images/ui2/angry-man-dark@3x.png differ diff --git a/resources/images/ui2/angry-man-light@2x.png b/resources/images/ui2/angry-man-light@2x.png new file mode 100644 index 00000000000..cb52b301cdb Binary files /dev/null and b/resources/images/ui2/angry-man-light@2x.png differ diff --git a/resources/images/ui2/angry-man-light@3x.png b/resources/images/ui2/angry-man-light@3x.png new file mode 100644 index 00000000000..06cbaeb4e12 Binary files /dev/null and b/resources/images/ui2/angry-man-light@3x.png differ diff --git a/resources/images/ui2/biometrics@2x.png b/resources/images/ui2/biometrics@2x.png new file mode 100644 index 00000000000..5296c525eaa Binary files /dev/null and b/resources/images/ui2/biometrics@2x.png differ diff --git a/resources/images/ui2/biometrics@3x.png b/resources/images/ui2/biometrics@3x.png new file mode 100644 index 00000000000..1ee8d159f24 Binary files /dev/null and b/resources/images/ui2/biometrics@3x.png differ diff --git a/resources/images/ui2/buy@2x.png b/resources/images/ui2/buy@2x.png new file mode 100644 index 00000000000..26de202921b Binary files /dev/null and b/resources/images/ui2/buy@2x.png differ diff --git a/resources/images/ui2/buy@3x.png b/resources/images/ui2/buy@3x.png new file mode 100644 index 00000000000..91b84d4886a Binary files /dev/null and b/resources/images/ui2/buy@3x.png differ diff --git a/resources/images/ui2/cat-in-box-dark@2x.png b/resources/images/ui2/cat-in-box-dark@2x.png new file mode 100644 index 00000000000..bf76883d584 Binary files /dev/null and b/resources/images/ui2/cat-in-box-dark@2x.png differ diff --git a/resources/images/ui2/cat-in-box-dark@3x.png b/resources/images/ui2/cat-in-box-dark@3x.png new file mode 100644 index 00000000000..4db1fb8e73a Binary files /dev/null and b/resources/images/ui2/cat-in-box-dark@3x.png differ diff --git a/resources/images/ui2/cat-in-box-light@2x.png b/resources/images/ui2/cat-in-box-light@2x.png new file mode 100644 index 00000000000..20aaed4e694 Binary files /dev/null and b/resources/images/ui2/cat-in-box-light@2x.png differ diff --git a/resources/images/ui2/cat-in-box-light@3x.png b/resources/images/ui2/cat-in-box-light@3x.png new file mode 100644 index 00000000000..ece5cbf60d2 Binary files /dev/null and b/resources/images/ui2/cat-in-box-light@3x.png differ diff --git a/resources/images/ui2/chat-privately-with-friends@2x.png b/resources/images/ui2/chat-privately-with-friends@2x.png new file mode 100644 index 00000000000..f6f816e080f Binary files /dev/null and b/resources/images/ui2/chat-privately-with-friends@2x.png differ diff --git a/resources/images/ui2/chat-privately-with-friends@3x.png b/resources/images/ui2/chat-privately-with-friends@3x.png new file mode 100644 index 00000000000..118c7d0859e Binary files /dev/null and b/resources/images/ui2/chat-privately-with-friends@3x.png differ diff --git a/resources/images/ui2/desktop-how-to-pair-logged-in@2x.png b/resources/images/ui2/desktop-how-to-pair-logged-in@2x.png new file mode 100644 index 00000000000..4b9b9753705 Binary files /dev/null and b/resources/images/ui2/desktop-how-to-pair-logged-in@2x.png differ diff --git a/resources/images/ui2/desktop-how-to-pair-logged-in@3x.png b/resources/images/ui2/desktop-how-to-pair-logged-in@3x.png new file mode 100644 index 00000000000..3004aecc21d Binary files /dev/null and b/resources/images/ui2/desktop-how-to-pair-logged-in@3x.png differ diff --git a/resources/images/ui2/desktop-how-to-pair-sign-in@2x.png b/resources/images/ui2/desktop-how-to-pair-sign-in@2x.png new file mode 100644 index 00000000000..3ba0f12af5f Binary files /dev/null and b/resources/images/ui2/desktop-how-to-pair-sign-in@2x.png differ diff --git a/resources/images/ui2/desktop-how-to-pair-sign-in@3x.png b/resources/images/ui2/desktop-how-to-pair-sign-in@3x.png new file mode 100644 index 00000000000..cfd6702257e Binary files /dev/null and b/resources/images/ui2/desktop-how-to-pair-sign-in@3x.png differ diff --git a/resources/images/ui2/discover@2x.png b/resources/images/ui2/discover@2x.png new file mode 100644 index 00000000000..a77e9285ba2 Binary files /dev/null and b/resources/images/ui2/discover@2x.png differ diff --git a/resources/images/ui2/discover@3x.png b/resources/images/ui2/discover@3x.png new file mode 100644 index 00000000000..bae167e6d96 Binary files /dev/null and b/resources/images/ui2/discover@3x.png differ diff --git a/resources/images/ui2/div@2x.png b/resources/images/ui2/div@2x.png new file mode 100644 index 00000000000..c9e482c988d Binary files /dev/null and b/resources/images/ui2/div@2x.png differ diff --git a/resources/images/ui2/div@3x.png b/resources/images/ui2/div@3x.png new file mode 100644 index 00000000000..c997499dc3e Binary files /dev/null and b/resources/images/ui2/div@3x.png differ diff --git a/resources/images/ui2/ethereum-address@2x.png b/resources/images/ui2/ethereum-address@2x.png new file mode 100644 index 00000000000..3672fbe46d4 Binary files /dev/null and b/resources/images/ui2/ethereum-address@2x.png differ diff --git a/resources/images/ui2/ethereum-address@3x.png b/resources/images/ui2/ethereum-address@3x.png new file mode 100644 index 00000000000..6535af62b5a Binary files /dev/null and b/resources/images/ui2/ethereum-address@3x.png differ diff --git a/resources/images/ui2/find-sync-code-desktop@2x.png b/resources/images/ui2/find-sync-code-desktop@2x.png new file mode 100644 index 00000000000..c95201a96ac Binary files /dev/null and b/resources/images/ui2/find-sync-code-desktop@2x.png differ diff --git a/resources/images/ui2/find-sync-code-desktop@3x.png b/resources/images/ui2/find-sync-code-desktop@3x.png new file mode 100644 index 00000000000..2ed5c0c395d Binary files /dev/null and b/resources/images/ui2/find-sync-code-desktop@3x.png differ diff --git a/resources/images/ui2/find-sync-code-mobile@2x.png b/resources/images/ui2/find-sync-code-mobile@2x.png new file mode 100644 index 00000000000..c62f20d6fa2 Binary files /dev/null and b/resources/images/ui2/find-sync-code-mobile@2x.png differ diff --git a/resources/images/ui2/find-sync-code-mobile@3x.png b/resources/images/ui2/find-sync-code-mobile@3x.png new file mode 100644 index 00000000000..11b75b41a5c Binary files /dev/null and b/resources/images/ui2/find-sync-code-mobile@3x.png differ diff --git a/resources/images/ui2/generate-keys@2x.png b/resources/images/ui2/generate-keys@2x.png new file mode 100644 index 00000000000..a305fde6437 Binary files /dev/null and b/resources/images/ui2/generate-keys@2x.png differ diff --git a/resources/images/ui2/generate-keys@3x.png b/resources/images/ui2/generate-keys@3x.png new file mode 100644 index 00000000000..693c1c84e9c Binary files /dev/null and b/resources/images/ui2/generate-keys@3x.png differ diff --git a/resources/images/ui2/import-key-to-keycard@2x.png b/resources/images/ui2/import-key-to-keycard@2x.png new file mode 100644 index 00000000000..df5e83874ef Binary files /dev/null and b/resources/images/ui2/import-key-to-keycard@2x.png differ diff --git a/resources/images/ui2/import-key-to-keycard@3x.png b/resources/images/ui2/import-key-to-keycard@3x.png new file mode 100644 index 00000000000..59c92807ef0 Binary files /dev/null and b/resources/images/ui2/import-key-to-keycard@3x.png differ diff --git a/resources/images/ui2/invite-friends@2x.png b/resources/images/ui2/invite-friends@2x.png new file mode 100644 index 00000000000..9b25f680d63 Binary files /dev/null and b/resources/images/ui2/invite-friends@2x.png differ diff --git a/resources/images/ui2/invite-friends@3x.png b/resources/images/ui2/invite-friends@3x.png new file mode 100644 index 00000000000..1e6e8245077 Binary files /dev/null and b/resources/images/ui2/invite-friends@3x.png differ diff --git a/resources/images/ui2/keycard-backup-positive@2x.png b/resources/images/ui2/keycard-backup-positive@2x.png new file mode 100644 index 00000000000..bba9946b4a7 Binary files /dev/null and b/resources/images/ui2/keycard-backup-positive@2x.png differ diff --git a/resources/images/ui2/keycard-backup-positive@3x.png b/resources/images/ui2/keycard-backup-positive@3x.png new file mode 100644 index 00000000000..eef56bd23c2 Binary files /dev/null and b/resources/images/ui2/keycard-backup-positive@3x.png differ diff --git a/resources/images/ui2/keycard-buy.png b/resources/images/ui2/keycard-buy.png new file mode 100644 index 00000000000..447aba87587 Binary files /dev/null and b/resources/images/ui2/keycard-buy.png differ diff --git a/resources/images/ui2/keycard-change-pin-negative@2x.png b/resources/images/ui2/keycard-change-pin-negative@2x.png new file mode 100644 index 00000000000..a731152516d Binary files /dev/null and b/resources/images/ui2/keycard-change-pin-negative@2x.png differ diff --git a/resources/images/ui2/keycard-change-pin-negative@3x.png b/resources/images/ui2/keycard-change-pin-negative@3x.png new file mode 100644 index 00000000000..af1eccdfd4f Binary files /dev/null and b/resources/images/ui2/keycard-change-pin-negative@3x.png differ diff --git a/resources/images/ui2/keycard-change-pin-positive@2x.png b/resources/images/ui2/keycard-change-pin-positive@2x.png new file mode 100644 index 00000000000..046ea688dcf Binary files /dev/null and b/resources/images/ui2/keycard-change-pin-positive@2x.png differ diff --git a/resources/images/ui2/keycard-change-pin-positive@3x.png b/resources/images/ui2/keycard-change-pin-positive@3x.png new file mode 100644 index 00000000000..f6b6335f3e0 Binary files /dev/null and b/resources/images/ui2/keycard-change-pin-positive@3x.png differ diff --git a/resources/images/ui2/keycard-change-pin@2x.png b/resources/images/ui2/keycard-change-pin@2x.png new file mode 100644 index 00000000000..e9c5298430e Binary files /dev/null and b/resources/images/ui2/keycard-change-pin@2x.png differ diff --git a/resources/images/ui2/keycard-change-pin@3x.png b/resources/images/ui2/keycard-change-pin@3x.png new file mode 100644 index 00000000000..afb351b5b2b Binary files /dev/null and b/resources/images/ui2/keycard-change-pin@3x.png differ diff --git a/resources/images/ui2/keycard-check@2x.png b/resources/images/ui2/keycard-check@2x.png new file mode 100644 index 00000000000..cffd60151f6 Binary files /dev/null and b/resources/images/ui2/keycard-check@2x.png differ diff --git a/resources/images/ui2/keycard-check@3x.png b/resources/images/ui2/keycard-check@3x.png new file mode 100644 index 00000000000..44237e2f7e0 Binary files /dev/null and b/resources/images/ui2/keycard-check@3x.png differ diff --git a/resources/images/ui2/keycard-chip-dark@2x.png b/resources/images/ui2/keycard-chip-dark@2x.png new file mode 100644 index 00000000000..1f73f68d6bc Binary files /dev/null and b/resources/images/ui2/keycard-chip-dark@2x.png differ diff --git a/resources/images/ui2/keycard-chip-dark@3x.png b/resources/images/ui2/keycard-chip-dark@3x.png new file mode 100644 index 00000000000..181238b9b7e Binary files /dev/null and b/resources/images/ui2/keycard-chip-dark@3x.png differ diff --git a/resources/images/ui2/keycard-chip-light@2x.png b/resources/images/ui2/keycard-chip-light@2x.png new file mode 100644 index 00000000000..e5c1f74f96e Binary files /dev/null and b/resources/images/ui2/keycard-chip-light@2x.png differ diff --git a/resources/images/ui2/keycard-chip-light@3x.png b/resources/images/ui2/keycard-chip-light@3x.png new file mode 100644 index 00000000000..7f8a6f36177 Binary files /dev/null and b/resources/images/ui2/keycard-chip-light@3x.png differ diff --git a/resources/images/ui2/keycard-logo@2x.png b/resources/images/ui2/keycard-logo@2x.png new file mode 100644 index 00000000000..bc49dc31fae Binary files /dev/null and b/resources/images/ui2/keycard-logo@2x.png differ diff --git a/resources/images/ui2/keycard-logo@3x.png b/resources/images/ui2/keycard-logo@3x.png new file mode 100644 index 00000000000..9d25e847491 Binary files /dev/null and b/resources/images/ui2/keycard-logo@3x.png differ diff --git a/resources/images/ui2/keycard-migration-failed@2x.png b/resources/images/ui2/keycard-migration-failed@2x.png new file mode 100644 index 00000000000..f1822ec73fc Binary files /dev/null and b/resources/images/ui2/keycard-migration-failed@2x.png differ diff --git a/resources/images/ui2/keycard-migration-failed@3x.png b/resources/images/ui2/keycard-migration-failed@3x.png new file mode 100644 index 00000000000..90625af2d39 Binary files /dev/null and b/resources/images/ui2/keycard-migration-failed@3x.png differ diff --git a/resources/images/ui2/keycard-migration-succeeded@2x.png b/resources/images/ui2/keycard-migration-succeeded@2x.png new file mode 100644 index 00000000000..5ff9de6129d Binary files /dev/null and b/resources/images/ui2/keycard-migration-succeeded@2x.png differ diff --git a/resources/images/ui2/keycard-migration-succeeded@3x.png b/resources/images/ui2/keycard-migration-succeeded@3x.png new file mode 100644 index 00000000000..0fcd763c856 Binary files /dev/null and b/resources/images/ui2/keycard-migration-succeeded@3x.png differ diff --git a/resources/images/ui2/keycard-migration@2x.png b/resources/images/ui2/keycard-migration@2x.png new file mode 100644 index 00000000000..2352766f565 Binary files /dev/null and b/resources/images/ui2/keycard-migration@2x.png differ diff --git a/resources/images/ui2/keycard-migration@3x.png b/resources/images/ui2/keycard-migration@3x.png new file mode 100644 index 00000000000..28e2e08968f Binary files /dev/null and b/resources/images/ui2/keycard-migration@3x.png differ diff --git a/resources/images/ui2/keycard-not-empty@2x.png b/resources/images/ui2/keycard-not-empty@2x.png new file mode 100644 index 00000000000..c121a034c96 Binary files /dev/null and b/resources/images/ui2/keycard-not-empty@2x.png differ diff --git a/resources/images/ui2/keycard-not-empty@3x.png b/resources/images/ui2/keycard-not-empty@3x.png new file mode 100644 index 00000000000..e4f84f68ee0 Binary files /dev/null and b/resources/images/ui2/keycard-not-empty@3x.png differ diff --git a/resources/images/ui2/keycard-not-same@2x.png b/resources/images/ui2/keycard-not-same@2x.png new file mode 100644 index 00000000000..cf6900778f7 Binary files /dev/null and b/resources/images/ui2/keycard-not-same@2x.png differ diff --git a/resources/images/ui2/keycard-not-same@3x.png b/resources/images/ui2/keycard-not-same@3x.png new file mode 100644 index 00000000000..5cbd259169a Binary files /dev/null and b/resources/images/ui2/keycard-not-same@3x.png differ diff --git a/resources/images/ui2/keycard-orange@2x.png b/resources/images/ui2/keycard-orange@2x.png new file mode 100644 index 00000000000..5f7453c54b2 Binary files /dev/null and b/resources/images/ui2/keycard-orange@2x.png differ diff --git a/resources/images/ui2/keycard-orange@3x.png b/resources/images/ui2/keycard-orange@3x.png new file mode 100644 index 00000000000..9101e392e94 Binary files /dev/null and b/resources/images/ui2/keycard-orange@3x.png differ diff --git a/resources/images/ui2/keycard-profile-already-added@2x.png b/resources/images/ui2/keycard-profile-already-added@2x.png new file mode 100644 index 00000000000..47399aa56d4 Binary files /dev/null and b/resources/images/ui2/keycard-profile-already-added@2x.png differ diff --git a/resources/images/ui2/keycard-profile-already-added@3x.png b/resources/images/ui2/keycard-profile-already-added@3x.png new file mode 100644 index 00000000000..b79300d7def Binary files /dev/null and b/resources/images/ui2/keycard-profile-already-added@3x.png differ diff --git a/resources/images/ui2/keycard-reset-negative@2x.png b/resources/images/ui2/keycard-reset-negative@2x.png new file mode 100644 index 00000000000..94b918d2304 Binary files /dev/null and b/resources/images/ui2/keycard-reset-negative@2x.png differ diff --git a/resources/images/ui2/keycard-reset-negative@3x.png b/resources/images/ui2/keycard-reset-negative@3x.png new file mode 100644 index 00000000000..0452f083412 Binary files /dev/null and b/resources/images/ui2/keycard-reset-negative@3x.png differ diff --git a/resources/images/ui2/keycard-reset-positive@2x.png b/resources/images/ui2/keycard-reset-positive@2x.png new file mode 100644 index 00000000000..47858c89a73 Binary files /dev/null and b/resources/images/ui2/keycard-reset-positive@2x.png differ diff --git a/resources/images/ui2/keycard-reset-positive@3x.png b/resources/images/ui2/keycard-reset-positive@3x.png new file mode 100644 index 00000000000..dae9c62718c Binary files /dev/null and b/resources/images/ui2/keycard-reset-positive@3x.png differ diff --git a/resources/images/ui2/keycard-unblock-positive@2x.png b/resources/images/ui2/keycard-unblock-positive@2x.png new file mode 100644 index 00000000000..d2dca950586 Binary files /dev/null and b/resources/images/ui2/keycard-unblock-positive@2x.png differ diff --git a/resources/images/ui2/keycard-unblock-positive@3x.png b/resources/images/ui2/keycard-unblock-positive@3x.png new file mode 100644 index 00000000000..b49492ceec1 Binary files /dev/null and b/resources/images/ui2/keycard-unblock-positive@3x.png differ diff --git a/resources/images/ui2/keycard-unblock@2x.png b/resources/images/ui2/keycard-unblock@2x.png new file mode 100644 index 00000000000..4cf3706cc33 Binary files /dev/null and b/resources/images/ui2/keycard-unblock@2x.png differ diff --git a/resources/images/ui2/keycard-unblock@3x.png b/resources/images/ui2/keycard-unblock@3x.png new file mode 100644 index 00000000000..477872877f0 Binary files /dev/null and b/resources/images/ui2/keycard-unblock@3x.png differ diff --git a/resources/images/ui2/keycard@2x.png b/resources/images/ui2/keycard@2x.png new file mode 100644 index 00000000000..a625083d668 Binary files /dev/null and b/resources/images/ui2/keycard@2x.png differ diff --git a/resources/images/ui2/keycard@3x.png b/resources/images/ui2/keycard@3x.png new file mode 100644 index 00000000000..7c77418ae85 Binary files /dev/null and b/resources/images/ui2/keycard@3x.png differ diff --git a/resources/images/ui2/lifestyle@2x.png b/resources/images/ui2/lifestyle@2x.png new file mode 100644 index 00000000000..40c45dd66cb Binary files /dev/null and b/resources/images/ui2/lifestyle@2x.png differ diff --git a/resources/images/ui2/lifestyle@3x.png b/resources/images/ui2/lifestyle@3x.png new file mode 100644 index 00000000000..e5dc465277f Binary files /dev/null and b/resources/images/ui2/lifestyle@3x.png differ diff --git a/resources/images/ui2/login-syncing@2x.png b/resources/images/ui2/login-syncing@2x.png new file mode 100644 index 00000000000..5e292e5561c Binary files /dev/null and b/resources/images/ui2/login-syncing@2x.png differ diff --git a/resources/images/ui2/login-syncing@3x.png b/resources/images/ui2/login-syncing@3x.png new file mode 100644 index 00000000000..c6ad028a0d3 Binary files /dev/null and b/resources/images/ui2/login-syncing@3x.png differ diff --git a/resources/images/ui2/mobile-how-to-pair-logged-in@2x.png b/resources/images/ui2/mobile-how-to-pair-logged-in@2x.png new file mode 100644 index 00000000000..9f0f8186eac Binary files /dev/null and b/resources/images/ui2/mobile-how-to-pair-logged-in@2x.png differ diff --git a/resources/images/ui2/mobile-how-to-pair-logged-in@3x.png b/resources/images/ui2/mobile-how-to-pair-logged-in@3x.png new file mode 100644 index 00000000000..5d61aac7fad Binary files /dev/null and b/resources/images/ui2/mobile-how-to-pair-logged-in@3x.png differ diff --git a/resources/images/ui2/mobile-how-to-pair-sign-in@2x.png b/resources/images/ui2/mobile-how-to-pair-sign-in@2x.png new file mode 100644 index 00000000000..1c1e692535f Binary files /dev/null and b/resources/images/ui2/mobile-how-to-pair-sign-in@2x.png differ diff --git a/resources/images/ui2/mobile-how-to-pair-sign-in@3x.png b/resources/images/ui2/mobile-how-to-pair-sign-in@3x.png new file mode 100644 index 00000000000..a604d791d34 Binary files /dev/null and b/resources/images/ui2/mobile-how-to-pair-sign-in@3x.png differ diff --git a/resources/images/ui2/music@2x.png b/resources/images/ui2/music@2x.png new file mode 100644 index 00000000000..6e487445e7e Binary files /dev/null and b/resources/images/ui2/music@2x.png differ diff --git a/resources/images/ui2/music@3x.png b/resources/images/ui2/music@3x.png new file mode 100644 index 00000000000..5731ef43c20 Binary files /dev/null and b/resources/images/ui2/music@3x.png differ diff --git a/resources/images/ui2/nfc-fail@2x.png b/resources/images/ui2/nfc-fail@2x.png new file mode 100644 index 00000000000..328e35a9f75 Binary files /dev/null and b/resources/images/ui2/nfc-fail@2x.png differ diff --git a/resources/images/ui2/nfc-fail@3x.png b/resources/images/ui2/nfc-fail@3x.png new file mode 100644 index 00000000000..336b4d3266d Binary files /dev/null and b/resources/images/ui2/nfc-fail@3x.png differ diff --git a/resources/images/ui2/nfc-prompt-android@2x.png b/resources/images/ui2/nfc-prompt-android@2x.png new file mode 100644 index 00000000000..cbcce6b8fff Binary files /dev/null and b/resources/images/ui2/nfc-prompt-android@2x.png differ diff --git a/resources/images/ui2/nfc-prompt-android@3x.png b/resources/images/ui2/nfc-prompt-android@3x.png new file mode 100644 index 00000000000..fbe3161b32f Binary files /dev/null and b/resources/images/ui2/nfc-prompt-android@3x.png differ diff --git a/resources/images/ui2/nfc-prompt@2x.png b/resources/images/ui2/nfc-prompt@2x.png new file mode 100644 index 00000000000..c2b224cc7fb Binary files /dev/null and b/resources/images/ui2/nfc-prompt@2x.png differ diff --git a/resources/images/ui2/nfc-prompt@3x.png b/resources/images/ui2/nfc-prompt@3x.png new file mode 100644 index 00000000000..3dcf9358252 Binary files /dev/null and b/resources/images/ui2/nfc-prompt@3x.png differ diff --git a/resources/images/ui2/nfc-success@2x.png b/resources/images/ui2/nfc-success@2x.png new file mode 100644 index 00000000000..8a24eae441e Binary files /dev/null and b/resources/images/ui2/nfc-success@2x.png differ diff --git a/resources/images/ui2/nfc-success@3x.png b/resources/images/ui2/nfc-success@3x.png new file mode 100644 index 00000000000..242157f61b9 Binary files /dev/null and b/resources/images/ui2/nfc-success@3x.png differ diff --git a/resources/images/ui2/no-activity-dark@2x.png b/resources/images/ui2/no-activity-dark@2x.png new file mode 100644 index 00000000000..23f63bd868c Binary files /dev/null and b/resources/images/ui2/no-activity-dark@2x.png differ diff --git a/resources/images/ui2/no-activity-dark@3x.png b/resources/images/ui2/no-activity-dark@3x.png new file mode 100644 index 00000000000..9d8df1accf2 Binary files /dev/null and b/resources/images/ui2/no-activity-dark@3x.png differ diff --git a/resources/images/ui2/no-activity-light@2x.png b/resources/images/ui2/no-activity-light@2x.png new file mode 100644 index 00000000000..41d7cd36232 Binary files /dev/null and b/resources/images/ui2/no-activity-light@2x.png differ diff --git a/resources/images/ui2/no-activity-light@3x.png b/resources/images/ui2/no-activity-light@3x.png new file mode 100644 index 00000000000..77204ca32c3 Binary files /dev/null and b/resources/images/ui2/no-activity-light@3x.png differ diff --git a/resources/images/ui2/no-assets-dark@2x.png b/resources/images/ui2/no-assets-dark@2x.png new file mode 100644 index 00000000000..5a3ee6eb653 Binary files /dev/null and b/resources/images/ui2/no-assets-dark@2x.png differ diff --git a/resources/images/ui2/no-assets-dark@3x.png b/resources/images/ui2/no-assets-dark@3x.png new file mode 100644 index 00000000000..d32e1f07d7a Binary files /dev/null and b/resources/images/ui2/no-assets-dark@3x.png differ diff --git a/resources/images/ui2/no-assets-light@2x.png b/resources/images/ui2/no-assets-light@2x.png new file mode 100644 index 00000000000..d0c18b4965b Binary files /dev/null and b/resources/images/ui2/no-assets-light@2x.png differ diff --git a/resources/images/ui2/no-assets-light@3x.png b/resources/images/ui2/no-assets-light@3x.png new file mode 100644 index 00000000000..310ce94a170 Binary files /dev/null and b/resources/images/ui2/no-assets-light@3x.png differ diff --git a/resources/images/ui2/no-collectibles-dark@2x.png b/resources/images/ui2/no-collectibles-dark@2x.png new file mode 100644 index 00000000000..85d08a25dc9 Binary files /dev/null and b/resources/images/ui2/no-collectibles-dark@2x.png differ diff --git a/resources/images/ui2/no-collectibles-dark@3x.png b/resources/images/ui2/no-collectibles-dark@3x.png new file mode 100644 index 00000000000..fcd24ebb570 Binary files /dev/null and b/resources/images/ui2/no-collectibles-dark@3x.png differ diff --git a/resources/images/ui2/no-collectibles-light@2x.png b/resources/images/ui2/no-collectibles-light@2x.png new file mode 100644 index 00000000000..ce293b7c4e4 Binary files /dev/null and b/resources/images/ui2/no-collectibles-light@2x.png differ diff --git a/resources/images/ui2/no-collectibles-light@3x.png b/resources/images/ui2/no-collectibles-light@3x.png new file mode 100644 index 00000000000..7d1f2c8c5a3 Binary files /dev/null and b/resources/images/ui2/no-collectibles-light@3x.png differ diff --git a/resources/images/ui2/no-communities-dark@2x.png b/resources/images/ui2/no-communities-dark@2x.png new file mode 100644 index 00000000000..58e0965f584 Binary files /dev/null and b/resources/images/ui2/no-communities-dark@2x.png differ diff --git a/resources/images/ui2/no-communities-dark@3x.png b/resources/images/ui2/no-communities-dark@3x.png new file mode 100644 index 00000000000..1fb8ca44c7e Binary files /dev/null and b/resources/images/ui2/no-communities-dark@3x.png differ diff --git a/resources/images/ui2/no-communities-light@2x.png b/resources/images/ui2/no-communities-light@2x.png new file mode 100644 index 00000000000..03618a717d4 Binary files /dev/null and b/resources/images/ui2/no-communities-light@2x.png differ diff --git a/resources/images/ui2/no-communities-light@3x.png b/resources/images/ui2/no-communities-light@3x.png new file mode 100644 index 00000000000..0e678f3cf53 Binary files /dev/null and b/resources/images/ui2/no-communities-light@3x.png differ diff --git a/resources/images/ui2/no-contacts-dark@2x.png b/resources/images/ui2/no-contacts-dark@2x.png new file mode 100644 index 00000000000..1e307387644 Binary files /dev/null and b/resources/images/ui2/no-contacts-dark@2x.png differ diff --git a/resources/images/ui2/no-contacts-dark@3x.png b/resources/images/ui2/no-contacts-dark@3x.png new file mode 100644 index 00000000000..1f1d0019520 Binary files /dev/null and b/resources/images/ui2/no-contacts-dark@3x.png differ diff --git a/resources/images/ui2/no-contacts-light@2x.png b/resources/images/ui2/no-contacts-light@2x.png new file mode 100644 index 00000000000..0753f3bf901 Binary files /dev/null and b/resources/images/ui2/no-contacts-light@2x.png differ diff --git a/resources/images/ui2/no-contacts-light@3x.png b/resources/images/ui2/no-contacts-light@3x.png new file mode 100644 index 00000000000..e3ecbec4149 Binary files /dev/null and b/resources/images/ui2/no-contacts-light@3x.png differ diff --git a/resources/images/ui2/no-contacts-to-chat-dark@2x.png b/resources/images/ui2/no-contacts-to-chat-dark@2x.png new file mode 100644 index 00000000000..e882079541e Binary files /dev/null and b/resources/images/ui2/no-contacts-to-chat-dark@2x.png differ diff --git a/resources/images/ui2/no-contacts-to-chat-dark@3x.png b/resources/images/ui2/no-contacts-to-chat-dark@3x.png new file mode 100644 index 00000000000..d15b6f7fd7c Binary files /dev/null and b/resources/images/ui2/no-contacts-to-chat-dark@3x.png differ diff --git a/resources/images/ui2/no-contacts-to-chat-light@2x.png b/resources/images/ui2/no-contacts-to-chat-light@2x.png new file mode 100644 index 00000000000..b0acbe66845 Binary files /dev/null and b/resources/images/ui2/no-contacts-to-chat-light@2x.png differ diff --git a/resources/images/ui2/no-contacts-to-chat-light@3x.png b/resources/images/ui2/no-contacts-to-chat-light@3x.png new file mode 100644 index 00000000000..e5d619564d3 Binary files /dev/null and b/resources/images/ui2/no-contacts-to-chat-light@3x.png differ diff --git a/resources/images/ui2/no-contacts-to-invite-dark@2x.png b/resources/images/ui2/no-contacts-to-invite-dark@2x.png new file mode 100644 index 00000000000..e882079541e Binary files /dev/null and b/resources/images/ui2/no-contacts-to-invite-dark@2x.png differ diff --git a/resources/images/ui2/no-contacts-to-invite-dark@3x.png b/resources/images/ui2/no-contacts-to-invite-dark@3x.png new file mode 100644 index 00000000000..d15b6f7fd7c Binary files /dev/null and b/resources/images/ui2/no-contacts-to-invite-dark@3x.png differ diff --git a/resources/images/ui2/no-contacts-to-invite-light@2x.png b/resources/images/ui2/no-contacts-to-invite-light@2x.png new file mode 100644 index 00000000000..b0acbe66845 Binary files /dev/null and b/resources/images/ui2/no-contacts-to-invite-light@2x.png differ diff --git a/resources/images/ui2/no-contacts-to-invite-light@3x.png b/resources/images/ui2/no-contacts-to-invite-light@3x.png new file mode 100644 index 00000000000..e5d619564d3 Binary files /dev/null and b/resources/images/ui2/no-contacts-to-invite-light@3x.png differ diff --git a/resources/images/ui2/no-dapps-dark@2x.png b/resources/images/ui2/no-dapps-dark@2x.png new file mode 100644 index 00000000000..a58dcca3671 Binary files /dev/null and b/resources/images/ui2/no-dapps-dark@2x.png differ diff --git a/resources/images/ui2/no-dapps-dark@3x.png b/resources/images/ui2/no-dapps-dark@3x.png new file mode 100644 index 00000000000..b64a5e0d160 Binary files /dev/null and b/resources/images/ui2/no-dapps-dark@3x.png differ diff --git a/resources/images/ui2/no-dapps-light@2x.png b/resources/images/ui2/no-dapps-light@2x.png new file mode 100644 index 00000000000..f6a80c268cc Binary files /dev/null and b/resources/images/ui2/no-dapps-light@2x.png differ diff --git a/resources/images/ui2/no-dapps-light@3x.png b/resources/images/ui2/no-dapps-light@3x.png new file mode 100644 index 00000000000..fcc2f8abe7b Binary files /dev/null and b/resources/images/ui2/no-dapps-light@3x.png differ diff --git a/resources/images/ui2/no-funds-dark@2x.png b/resources/images/ui2/no-funds-dark@2x.png new file mode 100644 index 00000000000..21228b186ed Binary files /dev/null and b/resources/images/ui2/no-funds-dark@2x.png differ diff --git a/resources/images/ui2/no-funds-dark@3x.png b/resources/images/ui2/no-funds-dark@3x.png new file mode 100644 index 00000000000..d07c1cbb563 Binary files /dev/null and b/resources/images/ui2/no-funds-dark@3x.png differ diff --git a/resources/images/ui2/no-funds-light@2x.png b/resources/images/ui2/no-funds-light@2x.png new file mode 100644 index 00000000000..b391438af94 Binary files /dev/null and b/resources/images/ui2/no-funds-light@2x.png differ diff --git a/resources/images/ui2/no-funds-light@3x.png b/resources/images/ui2/no-funds-light@3x.png new file mode 100644 index 00000000000..2cd8883f17a Binary files /dev/null and b/resources/images/ui2/no-funds-light@3x.png differ diff --git a/resources/images/ui2/no-group-chats-dark@2x.png b/resources/images/ui2/no-group-chats-dark@2x.png new file mode 100644 index 00000000000..f3e46041592 Binary files /dev/null and b/resources/images/ui2/no-group-chats-dark@2x.png differ diff --git a/resources/images/ui2/no-group-chats-dark@3x.png b/resources/images/ui2/no-group-chats-dark@3x.png new file mode 100644 index 00000000000..43ea3d90701 Binary files /dev/null and b/resources/images/ui2/no-group-chats-dark@3x.png differ diff --git a/resources/images/ui2/no-group-chats-light@2x.png b/resources/images/ui2/no-group-chats-light@2x.png new file mode 100644 index 00000000000..24c7451bc97 Binary files /dev/null and b/resources/images/ui2/no-group-chats-light@2x.png differ diff --git a/resources/images/ui2/no-group-chats-light@3x.png b/resources/images/ui2/no-group-chats-light@3x.png new file mode 100644 index 00000000000..f03c6a58053 Binary files /dev/null and b/resources/images/ui2/no-group-chats-light@3x.png differ diff --git a/resources/images/ui2/no-notifications-dark@2x.png b/resources/images/ui2/no-notifications-dark@2x.png new file mode 100644 index 00000000000..edae8e85cba Binary files /dev/null and b/resources/images/ui2/no-notifications-dark@2x.png differ diff --git a/resources/images/ui2/no-notifications-dark@3x.png b/resources/images/ui2/no-notifications-dark@3x.png new file mode 100644 index 00000000000..74627a569cd Binary files /dev/null and b/resources/images/ui2/no-notifications-dark@3x.png differ diff --git a/resources/images/ui2/no-notifications-light@2x.png b/resources/images/ui2/no-notifications-light@2x.png new file mode 100644 index 00000000000..8aba185a7a8 Binary files /dev/null and b/resources/images/ui2/no-notifications-light@2x.png differ diff --git a/resources/images/ui2/no-notifications-light@3x.png b/resources/images/ui2/no-notifications-light@3x.png new file mode 100644 index 00000000000..46a84eae5ea Binary files /dev/null and b/resources/images/ui2/no-notifications-light@3x.png differ diff --git a/resources/images/ui2/no-opened-communities-dark@2x.png b/resources/images/ui2/no-opened-communities-dark@2x.png new file mode 100644 index 00000000000..46d1b011cbc Binary files /dev/null and b/resources/images/ui2/no-opened-communities-dark@2x.png differ diff --git a/resources/images/ui2/no-opened-communities-dark@3x.png b/resources/images/ui2/no-opened-communities-dark@3x.png new file mode 100644 index 00000000000..90bf960ab19 Binary files /dev/null and b/resources/images/ui2/no-opened-communities-dark@3x.png differ diff --git a/resources/images/ui2/no-opened-communities-light@2x.png b/resources/images/ui2/no-opened-communities-light@2x.png new file mode 100644 index 00000000000..edc2ec79b1c Binary files /dev/null and b/resources/images/ui2/no-opened-communities-light@2x.png differ diff --git a/resources/images/ui2/no-opened-communities-light@3x.png b/resources/images/ui2/no-opened-communities-light@3x.png new file mode 100644 index 00000000000..364f3421b83 Binary files /dev/null and b/resources/images/ui2/no-opened-communities-light@3x.png differ diff --git a/resources/images/ui2/no-pending-communities-dark@2x.png b/resources/images/ui2/no-pending-communities-dark@2x.png new file mode 100644 index 00000000000..d73cd3b58a0 Binary files /dev/null and b/resources/images/ui2/no-pending-communities-dark@2x.png differ diff --git a/resources/images/ui2/no-pending-communities-dark@3x.png b/resources/images/ui2/no-pending-communities-dark@3x.png new file mode 100644 index 00000000000..ba429218b33 Binary files /dev/null and b/resources/images/ui2/no-pending-communities-dark@3x.png differ diff --git a/resources/images/ui2/no-pending-communities-light@2x.png b/resources/images/ui2/no-pending-communities-light@2x.png new file mode 100644 index 00000000000..9b15b9daba8 Binary files /dev/null and b/resources/images/ui2/no-pending-communities-light@2x.png differ diff --git a/resources/images/ui2/no-pending-communities-light@3x.png b/resources/images/ui2/no-pending-communities-light@3x.png new file mode 100644 index 00000000000..0481887dbd3 Binary files /dev/null and b/resources/images/ui2/no-pending-communities-light@3x.png differ diff --git a/resources/images/ui2/no-permissions-dark@2x.png b/resources/images/ui2/no-permissions-dark@2x.png new file mode 100644 index 00000000000..b718ec74533 Binary files /dev/null and b/resources/images/ui2/no-permissions-dark@2x.png differ diff --git a/resources/images/ui2/no-permissions-dark@3x.png b/resources/images/ui2/no-permissions-dark@3x.png new file mode 100644 index 00000000000..d076152fb33 Binary files /dev/null and b/resources/images/ui2/no-permissions-dark@3x.png differ diff --git a/resources/images/ui2/no-permissions-light@2x.png b/resources/images/ui2/no-permissions-light@2x.png new file mode 100644 index 00000000000..38942ca7ddd Binary files /dev/null and b/resources/images/ui2/no-permissions-light@2x.png differ diff --git a/resources/images/ui2/no-permissions-light@3x.png b/resources/images/ui2/no-permissions-light@3x.png new file mode 100644 index 00000000000..5ede5a60389 Binary files /dev/null and b/resources/images/ui2/no-permissions-light@3x.png differ diff --git a/resources/images/ui2/no-pinned-messages-dark@2x.png b/resources/images/ui2/no-pinned-messages-dark@2x.png new file mode 100644 index 00000000000..204650486d2 Binary files /dev/null and b/resources/images/ui2/no-pinned-messages-dark@2x.png differ diff --git a/resources/images/ui2/no-pinned-messages-dark@3x.png b/resources/images/ui2/no-pinned-messages-dark@3x.png new file mode 100644 index 00000000000..799f5c3bf74 Binary files /dev/null and b/resources/images/ui2/no-pinned-messages-dark@3x.png differ diff --git a/resources/images/ui2/no-pinned-messages-light@2x.png b/resources/images/ui2/no-pinned-messages-light@2x.png new file mode 100644 index 00000000000..28db7d9fdb4 Binary files /dev/null and b/resources/images/ui2/no-pinned-messages-light@2x.png differ diff --git a/resources/images/ui2/no-pinned-messages-light@3x.png b/resources/images/ui2/no-pinned-messages-light@3x.png new file mode 100644 index 00000000000..495d3ec8026 Binary files /dev/null and b/resources/images/ui2/no-pinned-messages-light@3x.png differ diff --git a/resources/images/ui2/no-received-requests-dark@2x.png b/resources/images/ui2/no-received-requests-dark@2x.png new file mode 100644 index 00000000000..24b9103bb63 Binary files /dev/null and b/resources/images/ui2/no-received-requests-dark@2x.png differ diff --git a/resources/images/ui2/no-received-requests-dark@3x.png b/resources/images/ui2/no-received-requests-dark@3x.png new file mode 100644 index 00000000000..5395e0f98d7 Binary files /dev/null and b/resources/images/ui2/no-received-requests-dark@3x.png differ diff --git a/resources/images/ui2/no-received-requests-light@2x.png b/resources/images/ui2/no-received-requests-light@2x.png new file mode 100644 index 00000000000..4e2892ac5b7 Binary files /dev/null and b/resources/images/ui2/no-received-requests-light@2x.png differ diff --git a/resources/images/ui2/no-received-requests-light@3x.png b/resources/images/ui2/no-received-requests-light@3x.png new file mode 100644 index 00000000000..933d1069670 Binary files /dev/null and b/resources/images/ui2/no-received-requests-light@3x.png differ diff --git a/resources/images/ui2/no-sent-requests-dark@2x.png b/resources/images/ui2/no-sent-requests-dark@2x.png new file mode 100644 index 00000000000..108ea913e2e Binary files /dev/null and b/resources/images/ui2/no-sent-requests-dark@2x.png differ diff --git a/resources/images/ui2/no-sent-requests-dark@3x.png b/resources/images/ui2/no-sent-requests-dark@3x.png new file mode 100644 index 00000000000..a327275b153 Binary files /dev/null and b/resources/images/ui2/no-sent-requests-dark@3x.png differ diff --git a/resources/images/ui2/no-sent-requests-light@2x.png b/resources/images/ui2/no-sent-requests-light@2x.png new file mode 100644 index 00000000000..e8a86c990cb Binary files /dev/null and b/resources/images/ui2/no-sent-requests-light@2x.png differ diff --git a/resources/images/ui2/no-sent-requests-light@3x.png b/resources/images/ui2/no-sent-requests-light@3x.png new file mode 100644 index 00000000000..292cebf2e03 Binary files /dev/null and b/resources/images/ui2/no-sent-requests-light@3x.png differ diff --git a/resources/images/ui2/not-keycard@2x.png b/resources/images/ui2/not-keycard@2x.png new file mode 100644 index 00000000000..34fa97a71a8 Binary files /dev/null and b/resources/images/ui2/not-keycard@2x.png differ diff --git a/resources/images/ui2/not-keycard@3x.png b/resources/images/ui2/not-keycard@3x.png new file mode 100644 index 00000000000..29aa10c3236 Binary files /dev/null and b/resources/images/ui2/not-keycard@3x.png differ diff --git a/resources/images/ui2/notifications@2x.png b/resources/images/ui2/notifications@2x.png new file mode 100644 index 00000000000..5597036c330 Binary files /dev/null and b/resources/images/ui2/notifications@2x.png differ diff --git a/resources/images/ui2/notifications@3x.png b/resources/images/ui2/notifications@3x.png new file mode 100644 index 00000000000..54952e204af Binary files /dev/null and b/resources/images/ui2/notifications@3x.png differ diff --git a/resources/images/ui2/own-buy-and-swap-your-crypto@2x.png b/resources/images/ui2/own-buy-and-swap-your-crypto@2x.png new file mode 100644 index 00000000000..e6b288e86e7 Binary files /dev/null and b/resources/images/ui2/own-buy-and-swap-your-crypto@2x.png differ diff --git a/resources/images/ui2/own-buy-and-swap-your-crypto@3x.png b/resources/images/ui2/own-buy-and-swap-your-crypto@3x.png new file mode 100644 index 00000000000..63dbc96ad66 Binary files /dev/null and b/resources/images/ui2/own-buy-and-swap-your-crypto@3x.png differ diff --git a/resources/images/ui2/podcasts@2x.png b/resources/images/ui2/podcasts@2x.png new file mode 100644 index 00000000000..72ba0b53bc1 Binary files /dev/null and b/resources/images/ui2/podcasts@2x.png differ diff --git a/resources/images/ui2/podcasts@3x.png b/resources/images/ui2/podcasts@3x.png new file mode 100644 index 00000000000..0348134ce7b Binary files /dev/null and b/resources/images/ui2/podcasts@3x.png differ diff --git a/resources/images/ui2/preparing-status@2x.png b/resources/images/ui2/preparing-status@2x.png new file mode 100644 index 00000000000..a7f14e87a83 Binary files /dev/null and b/resources/images/ui2/preparing-status@2x.png differ diff --git a/resources/images/ui2/preparing-status@3x.png b/resources/images/ui2/preparing-status@3x.png new file mode 100644 index 00000000000..e6dc7a52b1f Binary files /dev/null and b/resources/images/ui2/preparing-status@3x.png differ diff --git a/resources/images/ui2/qr-code@2x.png b/resources/images/ui2/qr-code@2x.png new file mode 100644 index 00000000000..40ed3edbb7c Binary files /dev/null and b/resources/images/ui2/qr-code@2x.png differ diff --git a/resources/images/ui2/qr-code@3x.png b/resources/images/ui2/qr-code@3x.png new file mode 100644 index 00000000000..028600b2951 Binary files /dev/null and b/resources/images/ui2/qr-code@3x.png differ diff --git a/resources/images/ui2/receive@2x.png b/resources/images/ui2/receive@2x.png new file mode 100644 index 00000000000..7eef3d8bb1f Binary files /dev/null and b/resources/images/ui2/receive@2x.png differ diff --git a/resources/images/ui2/receive@3x.png b/resources/images/ui2/receive@3x.png new file mode 100644 index 00000000000..8d1d8614e9d Binary files /dev/null and b/resources/images/ui2/receive@3x.png differ diff --git a/resources/images/ui2/recovery-phrase@2x.png b/resources/images/ui2/recovery-phrase@2x.png new file mode 100644 index 00000000000..5e09d4553d4 Binary files /dev/null and b/resources/images/ui2/recovery-phrase@2x.png differ diff --git a/resources/images/ui2/recovery-phrase@3x.png b/resources/images/ui2/recovery-phrase@3x.png new file mode 100644 index 00000000000..0d4e8b7bcf0 Binary files /dev/null and b/resources/images/ui2/recovery-phrase@3x.png differ diff --git a/resources/images/ui2/store-your-assets-on-keycard@2x.png b/resources/images/ui2/store-your-assets-on-keycard@2x.png new file mode 100644 index 00000000000..53ddaa17b18 Binary files /dev/null and b/resources/images/ui2/store-your-assets-on-keycard@2x.png differ diff --git a/resources/images/ui2/store-your-assets-on-keycard@3x.png b/resources/images/ui2/store-your-assets-on-keycard@3x.png new file mode 100644 index 00000000000..cc8b71d4816 Binary files /dev/null and b/resources/images/ui2/store-your-assets-on-keycard@3x.png differ diff --git a/resources/images/ui2/sweating-man-dark@2x.png b/resources/images/ui2/sweating-man-dark@2x.png new file mode 100644 index 00000000000..dfabff1b73d Binary files /dev/null and b/resources/images/ui2/sweating-man-dark@2x.png differ diff --git a/resources/images/ui2/sweating-man-dark@3x.png b/resources/images/ui2/sweating-man-dark@3x.png new file mode 100644 index 00000000000..b2ff46d007a Binary files /dev/null and b/resources/images/ui2/sweating-man-dark@3x.png differ diff --git a/resources/images/ui2/sweating-man-light@2x.png b/resources/images/ui2/sweating-man-light@2x.png new file mode 100644 index 00000000000..be01dea6002 Binary files /dev/null and b/resources/images/ui2/sweating-man-light@2x.png differ diff --git a/resources/images/ui2/sweating-man-light@3x.png b/resources/images/ui2/sweating-man-light@3x.png new file mode 100644 index 00000000000..9298a43dd63 Binary files /dev/null and b/resources/images/ui2/sweating-man-light@3x.png differ diff --git a/resources/images/ui2/syncing_devices@2x.png b/resources/images/ui2/syncing_devices@2x.png new file mode 100644 index 00000000000..3f30a2ca531 Binary files /dev/null and b/resources/images/ui2/syncing_devices@2x.png differ diff --git a/resources/images/ui2/syncing_devices@3x.png b/resources/images/ui2/syncing_devices@3x.png new file mode 100644 index 00000000000..71fb84657ba Binary files /dev/null and b/resources/images/ui2/syncing_devices@3x.png differ diff --git a/resources/images/ui2/syncing_wrong@2x.png b/resources/images/ui2/syncing_wrong@2x.png new file mode 100644 index 00000000000..001e715e482 Binary files /dev/null and b/resources/images/ui2/syncing_wrong@2x.png differ diff --git a/resources/images/ui2/syncing_wrong@3x.png b/resources/images/ui2/syncing_wrong@3x.png new file mode 100644 index 00000000000..8e9d849cf4c Binary files /dev/null and b/resources/images/ui2/syncing_wrong@3x.png differ diff --git a/resources/images/ui2/transaction-progress@2x.png b/resources/images/ui2/transaction-progress@2x.png new file mode 100644 index 00000000000..0bd2b5607bb Binary files /dev/null and b/resources/images/ui2/transaction-progress@2x.png differ diff --git a/resources/images/ui2/transaction-progress@3x.png b/resources/images/ui2/transaction-progress@3x.png new file mode 100644 index 00000000000..99d7c583f8a Binary files /dev/null and b/resources/images/ui2/transaction-progress@3x.png differ diff --git a/resources/images/ui2/usage-data@2x.png b/resources/images/ui2/usage-data@2x.png new file mode 100644 index 00000000000..a808bca3642 Binary files /dev/null and b/resources/images/ui2/usage-data@2x.png differ diff --git a/resources/images/ui2/usage-data@3x.png b/resources/images/ui2/usage-data@3x.png new file mode 100644 index 00000000000..54c10eaaaed Binary files /dev/null and b/resources/images/ui2/usage-data@3x.png differ diff --git a/resources/images/ui2/welcome_illustration@2x.png b/resources/images/ui2/welcome_illustration@2x.png new file mode 100644 index 00000000000..54e0d1b45d9 Binary files /dev/null and b/resources/images/ui2/welcome_illustration@2x.png differ diff --git a/resources/images/ui2/welcome_illustration@3x.png b/resources/images/ui2/welcome_illustration@3x.png new file mode 100644 index 00000000000..6b52e1cb844 Binary files /dev/null and b/resources/images/ui2/welcome_illustration@3x.png differ diff --git a/resources/js/hook-require.js b/resources/js/hook-require.js deleted file mode 100644 index 162d1340daa..00000000000 --- a/resources/js/hook-require.js +++ /dev/null @@ -1,14 +0,0 @@ -const m = require('module'); -const originalLoader = m._load; - -/* - Hook `require` so that RN abuse of require does not break when running tests in nodejs. -*/ - -m._load = function hookedLoader(request, parent, isMain) { - if (request.match(/.jpeg|.jpg|.png$/)) { - return { uri: request }; - } - - return originalLoader(request, parent, isMain); -}; diff --git a/resources/js/i18n.js b/resources/js/i18n.js deleted file mode 100644 index 842710a9b6a..00000000000 --- a/resources/js/i18n.js +++ /dev/null @@ -1,4 +0,0 @@ -// i18n.js 3.0.0.rc14 -!function(a){if("undefined"!=typeof module&&module.exports)module.exports=a(this);else if("function"==typeof define&&define.amd){var b=this;define("i18n",function(){return a(b)})}else this.I18n=a(this)}(function(a){"use strict";var b=a&&a.I18n||{},c=Array.prototype.slice,d=function(a){return("0"+a.toString()).substr(-2)},e=function(a,b){return h("round",a,-b).toFixed(b)},f=function(a){var b=typeof a;return"function"===b||"object"===b&&!!a},g=function(a){return Array.isArray?Array.isArray(a):"[object Array]"===Object.prototype.toString.call(a)},h=function(a,b,c){return"undefined"==typeof c||0===+c?Math[a](b):(b=+b,c=+c,isNaN(b)||"number"!=typeof c||c%1!==0?NaN:(b=b.toString().split("e"),b=Math[a](+(b[0]+"e"+(b[1]?+b[1]-c:-c))),b=b.toString().split("e"),+(b[0]+"e"+(b[1]?+b[1]+c:c))))},i=function(a,b){var c,d;for(c in b)b.hasOwnProperty(c)&&(d=b[c],"[object String]"===Object.prototype.toString.call(d)?a[c]=d:(null==a[c]&&(a[c]={}),i(a[c],d)));return a},j={day_names:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbr_day_names:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],month_names:[null,"January","February","March","April","May","June","July","August","September","October","November","December"],abbr_month_names:[null,"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],meridian:["AM","PM"]},k={precision:3,separator:".",delimiter:",",strip_insignificant_zeros:!1},l={unit:"$",precision:2,format:"%u%n",sign_first:!0,delimiter:",",separator:"."},m={unit:"%",precision:3,format:"%n%u",separator:".",delimiter:""},n=[null,"kb","mb","gb","tb"],o={defaultLocale:"en",locale:"en",defaultSeparator:".",placeholder:/(?:\{\{|%\{)(.*?)(?:\}\}?)/gm,fallbacks:!1,translations:{},missingBehaviour:"message",missingTranslationPrefix:""};return b.reset=function(){this.defaultLocale=o.defaultLocale,this.locale=o.locale,this.defaultSeparator=o.defaultSeparator,this.placeholder=o.placeholder,this.fallbacks=o.fallbacks,this.translations=o.translations,this.missingBehaviour=o.missingBehaviour,this.missingTranslationPrefix=o.missingTranslationPrefix},b.initializeOptions=function(){"undefined"==typeof this.defaultLocale&&null!==this.defaultLocale&&(this.defaultLocale=o.defaultLocale),"undefined"==typeof this.locale&&null!==this.locale&&(this.locale=o.locale),"undefined"==typeof this.defaultSeparator&&null!==this.defaultSeparator&&(this.defaultSeparator=o.defaultSeparator),"undefined"==typeof this.placeholder&&null!==this.placeholder&&(this.placeholder=o.placeholder),"undefined"==typeof this.fallbacks&&null!==this.fallbacks&&(this.fallbacks=o.fallbacks),"undefined"==typeof this.translations&&null!==this.translations&&(this.translations=o.translations),"undefined"==typeof this.missingBehaviour&&null!==this.missingBehaviour&&(this.missingBehaviour=o.missingBehaviour),"undefined"==typeof this.missingTranslationPrefix&&null!==this.missingTranslationPrefix&&(this.missingTranslationPrefix=o.missingTranslationPrefix)},b.initializeOptions(),b.locales={},b.locales.get=function(a){var c=this[a]||this[b.locale]||this.default;return"function"==typeof c&&(c=c(a)),g(c)===!1&&(c=[c]),c},b.locales.default=function(a){var e,c=[],d=[];return a&&c.push(a),!a&&b.locale&&c.push(b.locale),b.fallbacks&&b.defaultLocale&&c.push(b.defaultLocale),c.forEach(function(a){e=a.split("-")[0],~d.indexOf(a)||d.push(a),b.fallbacks&&e&&e!==a&&!~d.indexOf(e)&&d.push(e)}),c.length||c.push("en"),d},b.pluralization={},b.pluralization.get=function(a){return this[a]||this[b.locale]||this.default},b.pluralization.default=function(a){switch(a){case 0:return["zero","other"];case 1:return["one"];default:return["other"]}},b.currentLocale=function(){return this.locale||this.defaultLocale},b.isSet=function(a){return void 0!==a&&null!==a},b.lookup=function(a,b){b=this.prepareOptions(b);var e,f,g,c=this.locales.get(b.locale).slice();c[0];for(a=this.getFullScope(a,b);c.length;)if(e=c.shift(),f=a.split(this.defaultSeparator),g=this.translations[e]){for(;f.length&&(g=g[f.shift()],void 0!==g&&null!==g););if(void 0!==g&&null!==g)return g}if(this.isSet(b.defaultValue))return b.defaultValue},b.meridian=function(){var a=this.lookup("time"),b=this.lookup("date");return a&&a.am&&a.pm?[a.am,a.pm]:b&&b.meridian?b.meridian:j.meridian},b.prepareOptions=function(){for(var d,a=c.call(arguments),b={};a.length;)if(d=a.shift(),"object"==typeof d)for(var e in d)d.hasOwnProperty(e)&&(this.isSet(b[e])||(b[e]=d[e]));return b},b.createTranslationOptions=function(a,b){var c=[{scope:a}];return this.isSet(b.defaults)&&(c=c.concat(b.defaults)),this.isSet(b.defaultValue)&&(c.push({message:b.defaultValue}),delete b.defaultValue),c},b.translate=function(a,b){b=this.prepareOptions(b);var d,c=this.createTranslationOptions(a,b),e=c.some(function(a){if(this.isSet(a.scope)?d=this.lookup(a.scope,b):this.isSet(a.message)&&(d=a.message),void 0!==d&&null!==d)return!0},this);return e?("string"==typeof d?d=this.interpolate(d,b):f(d)&&this.isSet(b.count)&&(d=this.pluralize(b.count,d,b)),d):this.missingTranslation(a,b)},b.interpolate=function(a,b){b=this.prepareOptions(b);var d,e,f,g,c=a.match(this.placeholder);if(!c)return a;for(var e;c.length;)d=c.shift(),f=d.replace(this.placeholder,"$1"),e=this.isSet(b[f])?b[f].toString().replace(/\$/gm,"_#$#_"):f in b?this.nullPlaceholder(d,a,b):this.missingPlaceholder(d,a,b),g=new RegExp(d.replace(/\{/gm,"\\{").replace(/\}/gm,"\\}")),a=a.replace(g,e);return a.replace(/_#\$#_/g,"$")},b.pluralize=function(a,b,c){c=this.prepareOptions(c);var d,e,g,h,i;if(d=f(b)?b:this.lookup(b,c),!d)return this.missingTranslation(b,c);for(e=this.pluralization.get(c.locale),g=e(a);g.length;)if(h=g.shift(),this.isSet(d[h])){i=d[h];break}return c.count=String(a),this.interpolate(i,c)},b.missingTranslation=function(a,b){if("guess"==this.missingBehaviour){var c=a.split(".").slice(-1)[0];return(this.missingTranslationPrefix.length>0?this.missingTranslationPrefix:"")+c.replace("_"," ").replace(/([a-z])([A-Z])/g,function(a,b,c){return b+" "+c.toLowerCase()})}var d=null!=b&&null!=b.locale?b.locale:this.currentLocale(),e=this.getFullScope(a,b),f=[d,e].join(this.defaultSeparator);return'[missing "'+f+'" translation]'},b.missingPlaceholder=function(a,b,c){return"[missing "+a+" value]"},b.nullPlaceholder=function(){return b.missingPlaceholder.apply(b,arguments)},b.toNumber=function(a,b){b=this.prepareOptions(b,this.lookup("number.format"),k);var g,i,c=a<0,d=e(Math.abs(a),b.precision).toString(),f=d.split("."),h=[],j=b.format||"%n",l=c?"-":"";for(a=f[0],g=f[1];a.length>0;)h.unshift(a.substr(Math.max(0,a.length-3),3)),a=a.substr(0,a.length-3);return i=h.join(b.delimiter),b.strip_insignificant_zeros&&g&&(g=g.replace(/0+$/,"")),b.precision>0&&g&&(i+=b.separator+g),j=b.sign_first?"%s"+j:j.replace("%n","%s%n"),i=j.replace("%u",b.unit).replace("%n",i).replace("%s",l)},b.toCurrency=function(a,b){return b=this.prepareOptions(b,this.lookup("number.currency.format"),this.lookup("number.format"),l),this.toNumber(a,b)},b.localize=function(a,b,c){switch(c||(c={}),a){case"currency":return this.toCurrency(b);case"number":return a=this.lookup("number.format"),this.toNumber(b,a);case"percentage":return this.toPercentage(b);default:var d;return d=a.match(/^(date|time)/)?this.toTime(a,b):b.toString(),this.interpolate(d,c)}},b.parseDate=function(a){var b,c,d;if("object"==typeof a)return a;if(b=a.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})([\.,]\d{1,3})?)?(Z|\+00:?00)?/)){for(var e=1;e<=6;e++)b[e]=parseInt(b[e],10)||0;b[2]-=1,d=b[7]?1e3*("0"+b[7]):null,c=b[8]?new Date(Date.UTC(b[1],b[2],b[3],b[4],b[5],b[6],d)):new Date(b[1],b[2],b[3],b[4],b[5],b[6],d)}else"number"==typeof a?(c=new Date,c.setTime(a)):a.match(/([A-Z][a-z]{2}) ([A-Z][a-z]{2}) (\d+) (\d+:\d+:\d+) ([+-]\d+) (\d+)/)?(c=new Date,c.setTime(Date.parse([RegExp.$1,RegExp.$2,RegExp.$3,RegExp.$6,RegExp.$4,RegExp.$5].join(" ")))):a.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)?(c=new Date,c.setTime(Date.parse(a))):(c=new Date,c.setTime(Date.parse(a)));return c},b.strftime=function(a,c){var e=this.lookup("date"),f=b.meridian();if(e||(e={}),e=this.prepareOptions(e,j),isNaN(a.getTime()))throw new Error("I18n.strftime() requires a valid date object, but received an invalid date.");var g=a.getDay(),h=a.getDate(),i=a.getFullYear(),k=a.getMonth()+1,l=a.getHours(),m=l,n=l>11?1:0,o=a.getSeconds(),p=a.getMinutes(),q=a.getTimezoneOffset(),r=Math.floor(Math.abs(q/60)),s=Math.abs(q)-60*r,t=(q>0?"-":"+")+(r.toString().length<2?"0"+r:r)+(s.toString().length<2?"0"+s:s);return m>12?m-=12:0===m&&(m=12),c=c.replace("%a",e.abbr_day_names[g]),c=c.replace("%A",e.day_names[g]),c=c.replace("%b",e.abbr_month_names[k]),c=c.replace("%B",e.month_names[k]),c=c.replace("%d",d(h)),c=c.replace("%e",h),c=c.replace("%-d",h),c=c.replace("%H",d(l)),c=c.replace("%-H",l),c=c.replace("%I",d(m)),c=c.replace("%-I",m),c=c.replace("%m",d(k)),c=c.replace("%-m",k),c=c.replace("%M",d(p)),c=c.replace("%-M",p),c=c.replace("%p",f[n]),c=c.replace("%S",d(o)),c=c.replace("%-S",o),c=c.replace("%w",g),c=c.replace("%y",d(i)),c=c.replace("%-y",d(i).replace(/^0+/,"")),c=c.replace("%Y",i),c=c.replace("%z",t)},b.toTime=function(a,b){var c=this.parseDate(b),d=this.lookup(a);return c.toString().match(/invalid/i)?c.toString():d?this.strftime(c,d):c.toString()},b.toPercentage=function(a,b){return b=this.prepareOptions(b,this.lookup("number.percentage.format"),this.lookup("number.format"),m),this.toNumber(a,b)},b.toHumanSize=function(a,b){for(var f,g,c=1024,d=a,e=0;d>=c&&e<4;)d/=c,e+=1;return 0===e?(f=this.t("number.human.storage_units.units.byte",{count:d}),g=0):(f=this.t("number.human.storage_units.units."+n[e]),g=d-Math.floor(d)===0?0:1),b=this.prepareOptions(b,{unit:f,precision:g,format:"%n%u",delimiter:""}),this.toNumber(d,b)},b.getFullScope=function(a,b){return b=this.prepareOptions(b),a.constructor===Array&&(a=a.join(this.defaultSeparator)),b.scope&&(a=[b.scope,a].join(this.defaultSeparator)),a},b.extend=function(a,b){return"undefined"==typeof a&&"undefined"==typeof b?{}:i(a,b)},b.t=b.translate,b.l=b.localize,b.p=b.pluralize,b}); -I18n.defaultLocale = "en"; -I18n.fallbacks = true; diff --git a/resources/js/provider.js b/resources/js/provider.js new file mode 100644 index 00000000000..05dee916379 --- /dev/null +++ b/resources/js/provider.js @@ -0,0 +1,372 @@ +(function () { + if (typeof EthereumProvider === 'undefined') { + var callbackId = 0; + var callbacks = {}; + + var bridgeSend = function (data) { + ReactNativeWebView.postMessage(JSON.stringify(data)); + }; + + var history = window.history; + var pushState = history.pushState; + history.pushState = function (state) { + setTimeout(function () { + bridgeSend({ + type: 'history-state-changed', + navState: { url: location.href, title: document.title }, + }); + }, 100); + return pushState.apply(history, arguments); + }; + + function sendAPIrequest(permission, params) { + var messageId = callbackId++; + var params = params || {}; + + bridgeSend({ + type: 'api-request', + permission: permission, + messageId: messageId, + params: params, + }); + + return new Promise(function (resolve, reject) { + params['resolve'] = resolve; + params['reject'] = reject; + callbacks[messageId] = params; + }); + } + + function qrCodeResponse(data, callback) { + var result = data.data; + var regex = new RegExp(callback.regex); + if (!result) { + if (callback.reject) { + callback.reject(new Error('Cancelled')); + } + } else if (regex.test(result)) { + if (callback.resolve) { + callback.resolve(result); + } + } else { + if (callback.reject) { + callback.reject(new Error("Doesn't match")); + } + } + } + + function Unauthorized() { + this.name = 'Unauthorized'; + this.id = 4100; + this.code = 4100; + this.message = 'The requested method and/or account has not been authorized by the user.'; + } + + Unauthorized.prototype = Object.create(Error.prototype); + + function UserRejectedRequest() { + this.name = 'UserRejectedRequest'; + this.id = 4001; + this.code = 4001; + this.message = 'The user rejected the request.'; + } + + UserRejectedRequest.prototype = Object.create(Error.prototype); + ReactNativeWebView.onMessage = function (message) { + data = JSON.parse(message); + var id = data.messageId; + var callback = callbacks[id]; + + if (callback) { + if (data.type === 'api-response') { + if (data.permission == 'qr-code') { + qrCodeResponse(data, callback); + } else if (data.isAllowed) { + if (data.permission == 'web3') { + var selectedAddress = data.data[0]; + window.statusAppcurrentAccountAddress = selectedAddress; + // Set deprecated metamask fields + window.ethereum.selectedAddress = selectedAddress; + window.ethereum.emit('accountsChanged', data.data); + } + callback.resolve(data.data); + } else { + callback.reject(new UserRejectedRequest()); + } + } else if (data.type === 'web3-send-async-callback') { + if (callback.beta) { + if (data.error) { + if (data.error.code == 4100) callback.reject(new Unauthorized()); + else callback.reject(data.error); + } else { + if (window.statusAppDebug) { + console.log('resolve ' + callback.method + ' :' + JSON.stringify(data.result.result)); + } + callback.resolve(data.result.result); + } + } else if (callback.results) { + callback.results.push(data.error || data.result); + if (callback.results.length == callback.num) callback.callback(undefined, callback.results); + } else { + callback.callback(data.error, data.result); + } + } + } + }; + + function web3SuccessResponse(payload, result) { + return { + id: payload.id, + jsonrpc: '2.0', + method: payload.method, + result: result, + }; + } + + function web3ErrorResponse(payload, error) { + return { + id: payload.id, + jsonrpc: '2.0', + method: payload.method, + error: error, + }; + } + + function getSyncResponse(payload) { + if (payload.method == 'eth_accounts' && typeof window.statusAppcurrentAccountAddress !== 'undefined') { + return web3SuccessResponse(payload, [window.statusAppcurrentAccountAddress]); + } else if (payload.method == 'eth_coinbase' && typeof window.statusAppcurrentAccountAddress !== 'undefined') { + return web3SuccessResponse(payload, window.statusAppcurrentAccountAddress); + } else if (payload.method == 'net_version') { + return web3SuccessResponse(payload, window.statusAppNetworkId); + } else if (payload.method == 'eth_chainId') { + return web3SuccessResponse(payload, '0x' + Number(window.statusAppNetworkId).toString(16)); + } else if (payload.method == 'eth_uninstallFilter') { + return web3SuccessResponse(payload, true); + } else { + return null; + } + } + + var StatusAPI = function () {}; + + StatusAPI.prototype.getContactCode = function () { + return sendAPIrequest('contact-code'); + }; + + var EthereumProvider = function () {}; + + EthereumProvider.prototype.isStatus = true; + EthereumProvider.prototype.status = new StatusAPI(); + EthereumProvider.prototype.isConnected = function () { + return true; + }; + // Set legacy metamask fields https://docs.metamask.io/guide/ethereum-provider.html#legacy-api + EthereumProvider.prototype.networkVersion = window.statusAppNetworkId; + EthereumProvider.prototype.chainId = '0x' + Number(window.statusAppNetworkId).toString(16); + + EthereumProvider.prototype._events = {}; + + EthereumProvider.prototype.on = function (name, listener) { + if (!this._events[name]) { + this._events[name] = []; + } + this._events[name].push(listener); + }; + + EthereumProvider.prototype.removeListener = function (name, listenerToRemove) { + if (!this._events[name]) { + return; + } + + const filterListeners = (listener) => listener !== listenerToRemove; + this._events[name] = this._events[name].filter(filterListeners); + }; + + EthereumProvider.prototype.removeAllListeners = function () { + this._events = {}; + }; + + EthereumProvider.prototype.emit = function (name, data) { + if (!this._events[name]) { + return; + } + this._events[name].forEach((cb) => { + // Fixes: https://github.com/status-im/status-mobile/issues/13642 + // Metamask also errors on the same issue, but it's using https://github.com/MetaMask/safe-event-emitter and therefore the dapp still works + try { + cb(data); + } catch (e) { + setTimeout(() => { + throw e; + }); + } + }); + }; + EthereumProvider.prototype.enable = function () { + if (window.statusAppDebug) { + console.log('enable'); + } + return sendAPIrequest('web3'); + }; + + EthereumProvider.prototype.scanQRCode = function (regex) { + return sendAPIrequest('qr-code', { regex: regex }); + }; + + EthereumProvider.prototype.request = function (requestArguments) { + if (window.statusAppDebug) { + console.log('request: ' + JSON.stringify(requestArguments)); + } + if (!requestArguments) { + return Promise.reject(new Error('Request is not valid.')); + } + var method = requestArguments.method; + if (!method) { + return Promise.reject(new Error('Request is not valid.')); + } + + if (method === 'eth_requestAccounts') { + return sendAPIrequest('web3'); + } + + var syncResponse = getSyncResponse({ method: method }); + if (syncResponse) { + return new Promise(function (resolve, reject) { + if (window.statusAppDebug) { + console.log('resolved sync method: ' + method + ', result: ' + JSON.stringify(syncResponse.result)); + } + resolve(syncResponse.result); + }); + } + + var messageId = callbackId++; + var payload = { + id: messageId, + jsonrpc: '2.0', + method: method, + params: requestArguments.params, + }; + + bridgeSend({ + type: 'web3-send-async-read-only', + messageId: messageId, + payload: payload, + }); + + return new Promise(function (resolve, reject) { + callbacks[messageId] = { + beta: true, + method: method, + resolve: resolve, + reject: reject, + }; + }); + }; + + // (DEPRECATED) Support for legacy send method + EthereumProvider.prototype.send = function (param1, param2) { + // reference: https://docs.metamask.io/guide/ethereum-provider.html#legacy-methods + if (typeof param1 == 'object') { + //maybe ways of: + //1.ethereum.send(payload: JsonRpcRequest, callback: JsonRpcCallback): void; + //2.ethereum.send(payload: JsonRpcRequest): unknown; + if (window.statusAppDebug) { + console.log('send (legacy), payload: ' + JSON.stringify(param1) + ', callback: ' + param2); + } + + var syncResponse = getSyncResponse(param1); + if (syncResponse) { + if (param2) { + param2(null, syncResponse); + return; + } else return syncResponse; + } + + this.request(param1).then( + (result) => { + if (param2) { + param2(null, web3SuccessResponse(param1, result)); + } + }, + (reason) => { + if (window.statusAppDebug) { + console.log('send (legacy) failed. payload: ' + JSON.stringify(param1) + ', reason: ' + reason); + } + if (param2) { + param2(reason, web3ErrorResponse(param1, reason)); + } + }, + ); + } else if (typeof param1 == 'string') { + var method = param1; + var params = param2; + if (window.statusAppDebug) { + console.log('send (legacy), method: ' + method + ', params: ' + JSON.stringify(params)); + } + return this.request({ method: method, params: params }); + } else { + throw new Error('unsupported call to send, param1: ' + param1 + ', param2: ' + param2); + } + }; + + // (DEPRECATED) Support for legacy sendSync method + EthereumProvider.prototype.sendSync = function (payload) { + if (window.statusAppDebug) { + console.log('sendSync (legacy)' + JSON.stringify(payload)); + } + if (payload.method == 'eth_uninstallFilter') { + this.sendAsync(payload, function (res, err) {}); + } + var syncResponse = getSyncResponse(payload); + if (syncResponse) { + return syncResponse; + } else { + return web3SuccessResponse(payload, null); + } + }; + + // (DEPRECATED) Support for legacy sendAsync method + EthereumProvider.prototype.sendAsync = function (payload, callback) { + if (window.statusAppDebug) { + console.log('sendAsync (legacy)' + JSON.stringify(payload)); + } + if (!payload) { + return new Error('Request is not valid.'); + } + if (payload.method == 'eth_requestAccounts') { + return sendAPIrequest('web3'); + } + var syncResponse = getSyncResponse(payload); + if (syncResponse && callback) { + callback(null, syncResponse); + } else { + var messageId = callbackId++; + + if (Array.isArray(payload)) { + callbacks[messageId] = { + num: payload.length, + results: [], + callback: callback, + }; + for (var i in payload) { + bridgeSend({ + type: 'web3-send-async-read-only', + messageId: messageId, + payload: payload[i], + }); + } + } else { + callbacks[messageId] = { callback: callback }; + bridgeSend({ + type: 'web3-send-async-read-only', + messageId: messageId, + payload: payload, + }); + } + } + }; + } + + window.ethereum = new EthereumProvider(); +})(); diff --git a/resources/js/web3_init.js b/resources/js/web3_init.js deleted file mode 100644 index 6d1e298a750..00000000000 --- a/resources/js/web3_init.js +++ /dev/null @@ -1,122 +0,0 @@ -if(typeof StatusHttpProvider === "undefined"){ -var callbackId = 0; -var callbacks = {}; - -function httpCallback(id, data) { - var result = data; - var error = null; - - try { - result = JSON.parse(data); - } catch (e) { - error = {message: "InvalidResponse"}; - } - - if (callbacks[id]) { - callbacks[id](error, result); - } -} - -var StatusHttpProvider = function (host, timeout) { - this.host = host || 'http://localhost:8545'; - this.timeout = timeout || 0; -}; - -StatusHttpProvider.prototype.isStatus = true; - -function web3Response (payload, result){ - return {id: payload.id, - jsonrpc: "2.0", - result: result}; -} - -function getSyncResponse (payload) { - if (payload.method == "eth_accounts"){ - return web3Response(payload, [currentAccountAddress]) - } else if (payload.method == "eth_coinbase"){ - return web3Response(payload, currentAccountAddress) - } else if (payload.method == "net_version"){ - return web3Response(payload, networkId) - } else if (payload.method == "eth_uninstallFilter"){ - return web3Response(payload, true); - } else { - return null; - } -} - -StatusHttpProvider.prototype.send = function (payload) { - //TODO to be compatible with MM https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client - if (payload.method == "eth_uninstallFilter"){ - this.sendAsync(payload, function (res, err) {}) - } - var syncResponse = getSyncResponse(payload); - if (syncResponse){ - return syncResponse; - } else { - alert('You tried to send "' + payload.method + '" synchronously. Synchronous requests are not supported, sorry.'); - return null; - } -}; - -StatusHttpProvider.prototype.sendAsync = function (payload, callback) { - var syncResponse = getSyncResponse(payload); - if (syncResponse && callback){ - callback(null, syncResponse); - } - else { - var messageId = callbackId++; - callbacks[messageId] = callback; - if (typeof StatusBridge == "undefined") { - var data = { - payload: JSON.stringify(payload), - callbackId: JSON.stringify(messageId), - host: this.host - }; - - webkit.messageHandlers.sendRequest.postMessage(JSON.stringify(data)); - } else { - StatusBridge.sendRequest(this.host, JSON.stringify(messageId), JSON.stringify(payload)); - } - } -}; - -StatusHttpProvider.prototype.prepareRequest = function () { - var request = new XMLHttpRequest(); - - request.open('POST', this.host, false); - request.setRequestHeader('Content-Type', 'application/json'); - return request; -}; - -/** - * Synchronously tries to make Http request - * - * @method isConnected - * @return {Boolean} returns true if request haven't failed. Otherwise false - */ -StatusHttpProvider.prototype.isConnected = function () { - try { - this.sendAsync({ - id: 9999999999, - jsonrpc: '2.0', - method: 'net_listening', - params: [] - }, function () { - }); - return true; - } catch (e) { - return false; - } -}; -} - -var protocol = window.location.protocol -var address = providerAddress || "http://localhost:8545"; -console.log(protocol); -if (typeof web3 === "undefined") { - if (protocol == "https:" || protocol == "http:") { - console.log("StatusHttpProvider"); - web3 = new Web3(new StatusHttpProvider(address)); - web3.eth.defaultAccount = currentAccountAddress; - } -} diff --git a/resources/js/webview.js b/resources/js/webview.js deleted file mode 100644 index 1dfdeb464c8..00000000000 --- a/resources/js/webview.js +++ /dev/null @@ -1,40 +0,0 @@ -(function () { - function bridgeSend(data){ - WebViewBridge.send(JSON.stringify(data)); - } - - var history = window.history; - var pushState = history.pushState; - history.pushState = function(state) { - setTimeout(function () { - bridgeSend({ - type: 'history-state-changed', - navState: { url: location.href, title: document.title } - }); - }, 100); - return pushState.apply(history, arguments); - }; - - window.addEventListener('message', function (event) { - if (!event.data || !event.data.type) { return; } - if (event.data.type === 'STATUS_API_REQUEST') { - bridgeSend({ - type: 'status-api-request', - permissions: event.data.permissions, - host: window.location.hostname - }); - } - }); - - WebViewBridge.onMessage = function (message) { - - data = JSON.parse(message); - - if (data.type === "navigate-to-blank") - window.location.href = "about:blank"; - - else if (data.type === "status-api-success") - window.STATUS_API = data.data; - window.postMessage({ type: 'STATUS_API_SUCCESS', permissions: data.keys }, "*"); - }; -}()); diff --git a/resources/privacy.mdwn b/resources/privacy.mdwn new file mode 100644 index 00000000000..a453c839886 --- /dev/null +++ b/resources/privacy.mdwn @@ -0,0 +1,78 @@ +Status Software - Privacy Policy + +Last update: 16 August 2024 + +This Status Software - Privacy Policy (“Privacy Policy”) is intended to inform users of Status’ approach to privacy in respect of Status Software. In this regard, if you are using Status Software, this Privacy Policy applies to you. +Under the relevant data protection legislation, we are under certain obligations if we process any personal data when you use Status Software. Personal data means all information by which a person can be directly or indirectly identified, in line with the definitions of the General Data Protection Regulation (GDPR), the Swiss Federal Act on Data Protection of June 19, 1992 (DPA) (as amended from time to time) and its ordinances, and other relevant legislation on the protection of personal data. When we refer to privacy legislation in this Privacy Policy we mean GDPR and all such relevant legislation. +This Privacy Policy describes how we process the (limited) amount of personal data that we collect when you use Status Software. We also inform you about your rights and choices that you have in respect of any personal data we process. +If you do not agree with this Privacy Policy or any part of it, please do not use Status Software. If you are already using Status Software, we ask that you immediately stop using Status Software. +Who we are +Status is developing a set of open source projects that use peer-to-peer technologies to help people transact securely, communicate freely, and organise with confidence. Anyone participating in these projects helps to build technology and tools that empowers people to advance their own sovereign communities. + +Whenever "Status" or "we" is used in this Privacy Policy, we're referring to Status Research & Development GmbH, a Swiss company with its registered offices at Baarerstrasse 10, 6302 Zug Switzerland. Our contact information can be found on our website and at the end of this Privacy Policy. + +Status does not provide any services, such as financial services, to users of Status Software or any third party. Status is not an intermediary, agent, advisor, or custodian, and does not owe you any fiduciary duty. +Status Software +At Status, we strive to develop open source software that can serve as a secure communication tool that helps to uphold human rights. Status Software is specifically designed to facilitate the free flow of information, protect the right to private, secure communications and promote the sovereignty of individuals. + +Status Software is composed of a secure messaging tool, a crypto wallet, and a Web 3 browser that are integrated together. Any software developed by Status in this regard is simply called “Status Software” and you can find more details about its development here. +Status limits its processing of personal data from your use of Status Software +Status uses an open source, peer-to-peer protocol with end-to-end encryption and metadata suppression, which by design, means that Status (and any third party) is unable to and does not collect, store, own, control or have any visibility or means of access to your identity, Wallet, browsing information, any user private keys, Digital Assets, messages, content, history of interactions, user accounts or any other user information. + +Notwithstanding the above, Status processes personal data on a limited basis from users of Status Software and the specific types of information we process will depend on how you use Status Software. As such, we only collect and process personal data in the following instances: + +Usage Data: Where you opt-in to share usage data, we collect and/or briefly process certain data about your interactions with Status Software. Adhering to the principle of privacy by design, this option is off by default. The situations where we collect usage data and the type of personal data we process are as follows: +Network Behaviour: Status Software utilises a private, censorship resistant, peer-to-peer messaging protocol called Waku (https://waku.org/). To allow us to understand the performance, usage patterns, and reliability of the Waku protocol, we collect non-personally identifiable information such as the number of messages sent to you, connected and discovered peers, the rate of successfully sent messages, type of connection to peers, and details about your OS, Status Software application version and bandwidth usage. Such usage data is linked to a randomly generated peer ID associated with your instance of Status Software, which is a unique identifier used for the duration of your interaction with Status Software and generated with each restart of the Status Software. Such usage data will be kept for only as long as necessary to fulfil the aforementioned purposes and in any event, no longer than thirty (30) days and it will be deleted thereafter. +Analytics: Status also uses privacy-focused analytics to collect trends and insights about Status Software users. The usage data consists of personal data which we briefly process, that includes your IP address, universally unique identifiers of your device (UUID), and logs of actions, including button presses and screen visits, during your interactions with Status Software. Such usage data will be kept for only as long as necessary to fulfil the aforementioned purposes and it will be deleted thereafter. + +We process any personal data collected in the context of usage data based on your consent when you choose to opt-in. If you no longer wish to provide us any further usage data, you can opt-out at any time by disabling these functions. + +Proxy Server: When you use Status Software, it will automatically interact with a server (“Proxy Server”) that we control and has been implemented to improve the performance of Status Software. The Proxy Server is hosted on a number of reputable third party cloud providers (see section 4 of this Privacy Policy). As part of the implementation of the Proxy Server, certain information, including personal data will be shared from the user’s instance of Status Software and incidentally processed by us by way of the Proxy Server, which include the following: +the user’s IP address: your IP address will be processed when third party requests are run through the Proxy Server. This however removes your individual device details and IP address through the external requests made from the Proxy Server to the third party service provider (e.g. Infura); and +the user’s public address (also known as a wallet address): your public address is temporarily processed in the Proxy Server by us when making API calls to third party service providers in order to facilitate certain user activities on Status Software (for instance, transactions conducted using the Wallet). + +Apart from your IP address, we do not retain any of the above personal data (or any other information) in line with the principle of data minimisation. The IP address will be kept for only as long as necessary to fulfil the aforementioned purposes and in any event, no longer than fourteen (14) days and it will be deleted thereafter. +We process the above personal data for the purposes of facilitating the technical operation of Status Software and optimising the functionality and user’s experience of Status Software. We have a legitimate interest in processing this personal data for these purposes. + +Personal data sharing with third party service providers +We share personal data with third party service providers in the context of fulfilling the above purposes in which we collect and process personal data. We have contracted such third party service providers to provide their services and act as data processors on our behalf and are only permitted to process personal data in accordance with our instructions. +Third party service providers we engage and services we utilise from them include: +DigitalOcean: Providing hosting services in relation to the Proxy Server; +Alibaba Cloud: Providing hosting services in relation to the Proxy Server; +Google Cloud: Providing hosting services in relation to the Proxy Server;and +MixPanel: Providing analytics services in relation to the Usage Data. + + +Third party collection and processing of personal data +In addition to our limited collection of personal data, third parties might collect or process personal data as a result of Status Software making use of certain features or to provide certain content. To the extent you interact with such third party content or features, their respective privacy policies will apply. + +We do note however the following: +Cookies: We do not set any cookies for the use of Status Software. However, the Web 3 browser embedded within Status Software technically supports the use of cookies set by third party websites. Status is not responsible for nor is it able to influence whether such cookies are set by such third party websites and you should consult the relevant privacy policies of such third party websites. +Phone number and social media handles: When generating an account on Status Software, you will have the option to utilise your phone number and certain social media handles such as your X (formerly known as Twitter) account, to generate an account. While this information is not shared with Status and Status has no access to it, third party service providers might receive it to authenticate your ownership of such accounts. Using your phone number or social media handle, such as on X, will allow you to find your contacts who are also using Status Software and it will help your contacts to find you. This will allow them to discover and potentially interact with you, if you accept their request to connect, so that you can start building up your network of contacts on Status Software. Please note that this is not required for you to access or utilise Status Software, and you can always choose to generate an account anonymously without utilising your phone number or social media handle. +Web 3: An inherent feature of Web 3 is its transparency, particularly in the context of blockchain networks. This means that your public key and wallet address will be visible to others when you engage in transactions on such networks and that third parties may be able to (and for the avoidance of doubt, not through the use of Status Software) connect your public key and wallet address to your identity and determine the Digital Assets you own in your Wallet. You should also be aware that entries on blockchain networks are practically immutable, which means that they generally cannot be deleted or modified by anyone, including Status, even if the transaction turns out to have been made in error or otherwise. + + +Security measures we take in respect of Status Software +As a general approach, Status takes data security seriously and the measures we take as an organisation. In respect of Status Software, we implement a variety of security measures that are reasonably designed to maintain the safety of your personal data when it is shared with us. +Exporting personal data outside the European Union and Switzerland +While it is not intended that Status will export your personal data outside the European Union or Switzerland, Status is obliged to protect the privacy of such personal data if it is exported outside these areas and it will only be processed in countries or by parties that provide an adequate level of protection as deemed by Switzerland or the European Commission. Otherwise, Status will use specific forms of contractual clauses to ensure such personal data is provided the same protection as required in Switzerland or Europe. The transmission of personal data outside the European Union and Switzerland will always occur in conformity with applicable privacy legislation. +Your choices and rights +As explained in this Privacy Policy, Status limits its collection and processing of your personal data. Nonetheless, you still have certain choices and rights in respect of the personal data which we do collect and process. As laid out in relevant privacy legislation, you have the right to: +Ask us to correct or update your personal data (where possible); +Ask us to remove your personal data from our systems; +Ask us for a copy of your personal data, which may also be transferred to another data controller at your request; +Withdraw your consent to process your personal data (only if consent was asked for a processing activity), which only affects processing activities that are based on your consent and doesn't affect the validity of such processing activities before you have withdrawn your consent; +Object to the processing of your personal data; and +File a complaint with the Federal Data Protection and Information Commissioner (FDPIC), if you believe that your personal data has been processed unlawfully. + +Once we receive your request, we might require you to verify your identity such that we can appropriately respond to your request and we will do so in line with any applicable mandatory deadlines. Please contact us with the relevant request at: legal@status.im. +Third party websites +Within Status Software, you might come across links to third party websites. These third party sites will often have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Please refer to these privacy policies of these third party websites when you use such websites. +The Privacy Policy might change +We may modify or replace any part of this Privacy Policy. Please check on Status Software periodically for any changes. The new Privacy Policy will be effective immediately upon it being placed in Status Software . + + Contact Information +To the extent that you have any questions about the Privacy Policy, please email us at legal@status.im. + + +This document is licensed under CC-BY-SA. diff --git a/resources/terms-of-use.mdwn b/resources/terms-of-use.mdwn new file mode 100644 index 00000000000..f97792e496c --- /dev/null +++ b/resources/terms-of-use.mdwn @@ -0,0 +1,353 @@ +Status Terms of Use + +Last updated: 10 December 2024 + +1. Introduction + + +1.1 Who we are + +Status is developing a set of open source projects that use peer-to-peer technologies to help people transact securely, communicate freely, and organise with confidence. Anyone participating in these projects helps to build technology and tools that empowers people to advance their own sovereign communities. + +Whenever “Status” or “we” or “us” or any similar variation, are used in these terms of use (referred to as, just “terms”), we are referring to Status Research & Development GmbH, a Swiss company with its registered office at Baarerstrasse 10, Zug, Switzerland, and includes its “representatives”, which means Status’ affiliates, directors, officers, employees, agents and any other representatives of Status. For the purposes of these terms, “representatives” also includes Status core contributors without prejudice to the other legal categories mentioned. + +Status does not provide any services, such as financial services, to users of Status Software or any third party. Status is not an intermediary, agent, advisor, or custodian, and does not owe you any fiduciary duty. + +1.2 About these terms + +These terms are a contract between you and Status and apply to your use of Status Software. The term “Status Software” as used herein means a software developed by Status and is composed of a secure messaging tool and a crypto wallet integrated together. + +Please read these terms carefully before using Status Software. By installing or using Status Software on any device, you agree to these terms. If you do not agree, you should not install or use Status Software. + +We would like to draw your attention to certain parts of these terms which are summarised here for your convenience and we ask that you refer to the corresponding provisions below: + +- You are solely responsible for the use of Status Software +You are ultimately solely responsible for the use of Status Software. Throughout these terms, we have indicated where you are solely responsible in relation to your use of Status Software, for example, your use of the Wallet, securing your seed phrase and private keys (including backups) and the content you may create and submit through Status Software. + +- Third party services and Web 3 +When using Status Software, you may access or use Third Party Services or interact with Web 3. We note that Status does not control or operate any of these Third Party Services or Web 3 and that there may be applicable terms and conditions which you should refer to if you choose to interact with them. + +- Acceptable use of Status Software +Status adheres to a number of Principles which are reflected in the design and development of Status Software. We ask that you use Status Software in a way that promotes and aligns with the Status Principles, particularly not acting or using Status Software in manner as set out in the relevant provision below. + +- Risks +The use of Status Software is not without risk. You should understand the risks involved and how they may affect you. We have listed a number of risks in these terms, such as from your use of the Wallet, the novel and experimental nature of Web 3, the unpredictable and volatile nature of Digital Assets and potential legal, and regulatory and tax implications. The risks listed in these terms are not exhaustive and ultimately, you are solely responsible for evaluating the risks and deciding whether or not to use Status Software. + +- Indemnity +You provide Status a broad indemnity from and against any and all claims, damages and expenses, including attorneys’ fees, arising from or related to your use of Status Software. + +- Release and limitation of liability +You not only release Status’ liability on a number of items, but also agree that Status’ aggregate liability is limited to EUR 100 (one hundred Euros) or to the maximum extent permitted by law. + +- Arbitration +If a dispute arises between you and Status, we will seek to first solve it amicably with you before proceeding to the use of an arbitration administered by the Swiss Chambers’ Arbitration Institution. + + +2. About Status Software + +2.1 What is Status Software + +At Status, we strive to develop open source software that can serve as a secure communication tool that helps to uphold human rights. Status Software is specifically designed to facilitate the free flow of information, protect the right to private, secure communications and promote the sovereignty of individuals. + +Status Software is composed of a secure messaging tool and a crypto wallet integrated together. The software developed by Status in this regard is simply called “Status Software” and you can find more details about its development on Status’ GitHub page. + +Status Software is open source client software that is designed to be user-friendly and can be installed or run in a web browser on your local desktop or mobile device. You can use Status Software to interact with and participate in Web 3 protocols, applications and peer to peer networks (referred to in these terms collectively, as “Web 3”). Please note that Status Software is not a blockchain protocol, or a blockchain network, a platform or a web-based platform. + +Status develops Status Software and makes it available for you to run in a web browser or for your local use on your desktop or mobile device. This means that you are solely responsible for your activity and any potential risk or loss you may incur through using the Status Software as further detailed below in these terms. + +As a result of the privacy-first design of Status Software: + +1. Status does not store nor have access to user data, messages, or content; +2. Status cannot associate content or actions with individual users. However, users may choose to disclose certain limited information to Status voluntarily, such as when opting in to share usage analytics. This data, shared only through explicit consent of the user, is processed in accordance with our Privacy Policy; +3. Status cannot block or ban users from using Status Software features; +4. Status has no means of monitoring, moderating, removing, modifying or regulating users’ content or activity, except in Community Spaces created and directly managed by Status. + +2.1.1 Wallet + +Status Software includes a crypto wallet. We will refer to such wallet functionality in these terms as “Wallet”. + +The Wallet is your own personal crypto wallet. This type of wallet is sometimes also called “non-custodial”, “self-custodial” or “self-hosted”, which means you are in complete control of the crypto tokens (the “Digital Assets”) in the Wallet. Status will never have access to or control over your seed phrase, private keys or Digital Assets in your Wallet given its design. + +Using the Wallet, you are able to send, receive and store Digital Assets and connect to and interact with third-party services that enable the exchange of Digital Assets for fiat currency and Web 3, including certain blockchain bridges and decentralised exchanges. + +The Wallet contains the latest security standards, anti-phishing mechanisms and locally generated and stored public and private keys to ensure that your Digital Assets are secured to the highest standards. + +When generating an account, a randomisation process is started on your own device that generates a key pair. Status does not have visibility of or access to your private keys. You are solely responsible for storing and securing your seed phrase and private keys, and creating, storing and securing their backups. If you fail to maintain the appropriate security measures of your private keys, it may result in you losing access to your Wallet and control of any Digital Assets in your Wallet. If you lose or forget your seed phrase or private keys, Status will not be able to help you recover or replace them and further, Status will not be able to assist you with the recovery of your Digital Assets. + +In order to further enhance the security of storing your private keys, you may also choose to store your keys on a keycard, such as the Status Keycard. + +2.1.2 Messaging + +Status Software includes messaging functionality that enables secure communication between users. We will refer to the messaging functionality of Status Software and the Community Spaces collectively in these terms, as “Messaging”. + +Messaging uses an open source, peer-to-peer protocol with end-to-end encryption and metadata suppression to protect your messages from third parties (including Status). End-to-end encryption means that only the sender and recipient of a message can read its contents, and no one else. Peer-to-peer communication is accomplished using the Waku peer-to-peer messaging protocol, which relies on a distributed network of nodes instead of a centralised server. This means that there is no central party or server from which messages can be intercepted, modified or blocked. Learn more about Waku and how it works on Waku’s relevant website. + +Within Messaging, you will also be able to participate in Community Spaces. This feature allows users to create and join communities with varying access and discoverability options, which may include token-gated communities, private communities and open and public communities, where you can create group chats and engage with your communities. We note that content in all Community Spaces will be accessible to the members of those spaces. For open and public communities, content will be accessible to all other users of Status Software. Nonetheless, all content, regardless of the type of community, will still be protected by the same end-to-end encryption utilised in Status Software. + + + +3. Privacy + +Status processes a limited amount of user data to ensure the technical operation of the Status Software, enhance its functionality, and improve the user experience. For more details on this limited process of user data, please refer to our Privacy Policy. + +If you use third party services offered through Status Software, your privacy may not be protected and you will be subject to the privacy terms and conditions of such third parties. Please refer to our full terms regarding third party services below. + + + +4. Using third party services and interacting with Web 3 + +When using Status Software, you may access or use third party products, services or tools (“Third Party Services”) and interact with Web 3. Status Software is only one means of interacting with these Third Party Services and Web 3 and you can independently utilise other means and tools to do so as well. + +Your interactions with Web 3 could include many aspects, such as interacting with smart contracts that reside on blockchain networks, for example to swap Digital Assets or use (crypto) bridges, or deploying your own smart contracts through Community Spaces. You may also interact with and participate in peer-to-peer networks which could vary in their level of decentralisation, ranging from more centralised networks controlled by a single party to networks governed by decentralised communities. + +When you use Third Party Services or interact with Web 3, you acknowledge that Status does not control or operate any Third Party Service or Web 3. These products, services, or tools are either controlled, operated, owned, or licensed by the third party providing them, or they may operate autonomously, independently of Status or any other party. + +Please note that using Third Party Services or interacting with Web 3 might be subject to specific terms and conditions, including privacy policies, set by the third-party providers. These providers might exclude and disclaim all liability regarding their Third Party Services and are not responsible or liable for your use of Status Software. It is crucial that you read and understand the applicable terms and conditions, including privacy policies, of these Third Party Services to be aware of the implications of their use. In case of interactions with Web 3, such as decentralised exchanges, there might be in some cases no applicable terms and conditions, and the relevant software code will govern your interactions. + +Please also note that bridges are used to move tokens from one blockchain network to another. When you use a bridge, for example in the course of sending or bridging tokens, Status utilises an algorithm that aims to determine the lowest service fee which the user may incur at the time of a bridge’s use. This algorithm relies on information extracted directly from the relevant blockchain networks. Status is not responsible for the accuracy and correctness of any information that these algorithms may extract from such networks + +You are solely responsible for your use of Third Party Services and your interactions with Web 3. If you have any technical issues or other issues with any of these Third Party Services or Web 3, you should contact the relevant third party or appropriate point of contact directly (if any). + +In principle, Status does not provide any advice, recommendation, support or endorse Third Party Services you may use or any aspect of Web 3 that you may interact with through Status Software. Their integration or availability through Status Software does not imply any support or endorsement by Status. Status may also receive a fee from certain third parties who provide the Third Party Services. + +Please also note that Status participates in the development of other open source projects that utilise peer-to-peer technologies, which you can find more details about on Status’ GitHub page. + + +5. Nodes + +The use of Status Software on your device will turn it into a node that supports the Waku network. + +In general, by participating in or interacting with Web 3 networks, you may choose or be required to operate a node or perform other functions in support of such a network. Status is not responsible for the operation or functioning of such nodes. + + +6. Security + +Although security is a top priority for us and many functions of Status Software are privacy-mode by default, we cannot guarantee the full security of Status Software and we are not responsible for alerting you to potential security risks. We intend to continuously update the security measures of Status Software and we will from time to time post updates on the security of Status Software through our communication channels. + + +7. Content + +At Status, we believe in the sovereignty of individuals and we stand for the cause of personal liberty. We aim to promote social, political, and economic freedoms through Status Software, which is built on the principles of the free flow of information and censorship resistance. Through Status Software, particularly when using Messenger and Community Spaces, you will be able to create and interact with various content. We consider content to include anything you (or any other user) create, post, distribute or exchange through Status Software (as well as any future functions that may be added to Status Software), whether its text, links, GIFs, emoji, photos or any other media formats. + +You are solely responsible for any content that you create, post, distribute or submit, messages you exchange or any other activity you may engage in related to your content on or through Status Software and any harm or liability that may result from such content. + +You represent and warrant in respect of such content that: +1. your creation, posting, distribution or submission of content does not infringe on any intellectual property or other proprietary rights of any third party; and +2. you are the creator and owner or that you have the necessary permissions to create, post, distribute or submit such content through Status Software. + +You acknowledge that Status has no means of monitoring, moderating, removing, modifying or regulating users’ content or activity. Status is further unable to and is not responsible for verifying the accuracy or reliability of such content or activity. While you use Status Software, you may view content that: +1. could be offensive, indecent, or otherwise objectionable; +2. contains technical inaccuracies, typographical mistakes, and other errors; or +3. could violate others’ rights, including but not limited to privacy, publicity, intellectual property, or other proprietary rights. + + +8. Acceptable use of Status software + +8.1 Adherence to Status Principles + +One of Status’ goals is the widespread adoption of the decentralised web while also staying true to our Principles. These terms are designed to reflect our Principles, while providing important legal protections for Status and setting out guidelines and terms for the Status Software community of users. We ask that you use Status Software in a way that promotes and aligns with the Status Principles, and avoid actions that may harm or disrupt the community of users. + +We strongly encourage users to act responsibly and ethically in their interactions and refrain from (i) inciting harm, violence or illegal activity, (ii) engaging in harassment, abuse or discrimination, and (iii) spreading false or misleading information. + +If you are a Community Space creator or admin, we encourage you to establish your own guidelines to foster healthy and respectful community environments for your members. + +8.2 Community Governance + +Community Spaces are self-sovereign environments managed independently by their creators and appointed admins. These users are solely responsible for: + +1. establishing and enforcing their own community guidelines; +2. configuring access controls and permissions; +3. managing community membership, including accepting or rejecting requests; +4. moderating content and interactions within their community; and +5. taking actions against community members who violate their community guidelines or behave disruptively, including the ability to exclude or ban members. + +By participating in Community Spaces, you acknowledge that community governance lies solely with community creators and their appointed admins. Status cannot monitor, moderate or intervene in community interactions or governance decisions within any Community Space, except in cases where a Community Space is created and directly managed by Status. + +8.3 Limitation on certain uses of and acts in Status Software + +You agree not to use Status Software, or attempt or assist others to use it, in a manner that: +1. uses unauthorised methods to create or gain access to an account on Status Software; +2. interferes with or disrupts the integrity, safety, security, availability or performance of Status Software; +3. installs or transmits on or through Status Software, any viruses, worms, malware, Trojan horses, or other harmful or destructive code or content; +4. use Status Software, if you are subject to sanctions, or otherwise designated on a list of prohibited or restricted parties as maintained by, the United Nations, the European Union or any of its member states or the Singaporean, Swiss or US government, or intend to interact or transact with such parties; +5. violate, misappropriate or infringe the rights of Status, other users of Status Software, or any other third parties in any way, including but not limited to privacy, publicity, intellectual property, confidentiality, property or other rights; +6. engage in any activity through Status Software that may be in violation of applicable law, rules or regulations in your jurisdiction; +7. interfere, disrupt, negatively affect, or inhibit other users from their use of Status Software; or +8. engage in any attack, hack, denial-of-service attack, interference, or exploit of any smart contract in connection with use of Status Software. + + +9. Fees + +When you use Status Software to interact with Third Party Services or Web 3, you may incur costs and may have to pay various fees, such as service fees, transfer fees, transaction fees, swap fees, bridge fees or network fees. You will be solely liable for any such fees, costs and expenses. Status has no control over and is unable to influence these fees in any manner. + + +10. Intellectual property and branding + +10.1 Status develops open source software + +We want everyone to benefit from Status Software, so we made it available under free and open source licences. This means that anyone can use, share, and modify the source code, as long as they follow the terms of the applicable licence. We believe in permission-less participation, which means that everyone has the right to participate in the development of our software. The applicable licences can be found on Status’ GitHub page. + +10.2 Respect the Status brand + +Status (and its affiliates as the case may be) owns all the rights to Status brand, including copyrights, domains, trade dress (look and feel), design rights and themes, trademarks, logos, graphics, and other intellectual property. These terms do not grant you any rights to use the Status brand or its intellectual property. + +We love to see creative work from the Status community, but please let us know in advance and in writing if you want to use trademarks, logos, or graphics or modify them from our standard colours and designs. If you write articles, blogs, create websites, or talk about Status, please make sure it is clear that you are not speaking for, on behalf of, or under endorsement by Status. + +To request permission to use our copyrighted material or trademarks or any other intellectual property, such as logos, please contact: marketing@status.im. If we grant you permission, you must follow our guidelines and instructions for using our intellectual property. + + +11. Risks + +Before you use Status Software, you should understand the risks involved and how they may affect you. The risks listed below are not intended to be exhaustive and may not reflect all the potential risks you could be exposed to from using Status Software. + +You are solely responsible for evaluating the risks and deciding whether or not to use Status Software. By using Status Software, you agree to accept all of the risks identified below, as well as any other risks that a reasonable person should have been aware about. Status is not responsible for notifying you on an ongoing basis about any changes in the risks associated with using Status Software. + +11.1 Wallet + +By using the Wallet, you represent that you have a sufficient understanding about crypto wallets and Digital Assets. + +You acknowledge that Digital Assets stored in the Wallet are at risk of loss due to various factors, including but not limited to: theft, hacking (or other cyber-attacks), malware, technical or human error. You further acknowledge that transactions made through the Wallet are final and cannot be reversed by anyone, including Status. You are solely responsible for the transactions you undertake using Status Software, including verifying the accuracy and legitimacy of your transactions before confirming and proceeding with them. + +Status is not a service provider or an intermediary or custodian. We do not store, control, custody, transfer, or handle any users’ funds or Digital Assets. Transactions are processed by validators, nodes, miners, and other participants on the Web 3 network (and not by Status). We do not have visibility of and cannot interfere with, modify, stop, block, change, reverse, or refund any transactions. + +11.2 Novel and experimental nature of Web 3 + +You acknowledge that there are inherent risks related to Web 3, arising from its novel and experimental nature. These risks include but are not limited to security risks (such as hacking and malware or other cyber-attacks, unauthorised access to your Wallet, attacks on blockchain networks, among others), technical risks (such as software errors, vulnerabilities or bugs) or operational risks related to Digital Assets, smart contracts, use of certain services, such as (crypto) bridges and swaps or other aspects and functionalities of Web 3. + +The technology underlying Web 3 is also novel and experimental, as for the most part, it is based on still unproven, emerging and complex technologies, such as blockchain networks, that are developed or operated by third parties and distributed across a series of unaffiliated nodes across the world. As such, these changes and development in this technology may adversely affect your use of Web 3 and even Status Software. It is your sole responsibility to monitor such changes and the potential impact on your use of Web 3 and Status Software. + +Given the novel and experimental nature of Web 3, the laws, rules and regulations, or their interpretation, may change and develop in a manner which could adversely affect your ability to interact with any aspect of Web 3, including deploying smart contracts, as well as creating, using, distributing or purchasing Digital Assets. These rules and regulations vary significantly in maturity across jurisdictions, but are for the most part, still in the early stages of development and may rapidly evolve and as such, their potential impact is subject to significant uncertainty. This could result in adverse consequences to you that may include criminal or civil penalties, including fines. It is your sole responsibility to ensure that you comply with all applicable laws, rules and regulations, including but not limited to, cryptocurrencies, financial markets, securities and anti-money laundering. + +Web 3 is also decentralised to varying degrees and this may lead to a lack of predictability, accountability, or control over Web 3. There is no guarantee that the protocols and networks will function as expected. Changes may occur suddenly, like forks, which may affect Status Software and your experience of using Status Software. Status may decide not to support a forked network in its sole discretion. + +11.3 Unpredictable and volatile nature of Digital Assets + +With Status Software, you can interact with and transact in Digital Assets and their markets. You acknowledge that these markets and Digital Assets are often highly unpredictable and volatile due to multiple factors, such as supply and demand, changes in technology, the regulatory environment, political, natural and economic events in a jurisdiction, and price speculation. In addition, the underlying factors that determine the price of a Digital Asset are often opaque and could be subject to speculation and market manipulation. This means that the value of Digital Assets can be lost in part or in full and you should not interact with Digital Assets unless you have the necessary advice, knowledge, expertise or risk appetite. + +11.4 Potential legal, regulatory and tax implications + +You acknowledge that there may be legal and regulatory implications applicable to you while using Status Software and interacting with Web 3 and that may also adversely impact your use of Status Software. + +You are solely responsible for evaluating the legal and regulatory implications and taking appropriate actions, which could include obtaining any necessary licences, permits or approvals. Additionally, you are solely responsible for any tax implications, such as the payment of any taxes, resulting from your activities conducted through Status Software, including any transactions in Digital Assets. + + +12. Disclaimers + +Status Software is provided on an “as is” basis. Status does not make any express or implied representations or warranties in relation to Status Software. This includes warranties of merchantability, fitness for use or for a particular purpose, and non-infringement of any copyright or intellectual property rights. In particular, Status makes no representations or warranties: +1. about the completeness, accuracy, utility, reliability, suitability or availability of any information provided by Status; +2. that Status Software will be operational, secure, safe or free from failures, disruptions, unauthorised access, bugs, errors, or delays, viruses, malicious code or any other technical or security issues; +3. regarding the completeness, accuracy, legality, utility, suitability, availability, functionality, timeliness or security of Web 3 or any Third Party Services; +4. regarding the value, utility or legality of any Digital Asset; +5. regarding the completeness, accuracy, legality, utility, suitability, functionality, security or availability of any smart contract that you may interact with or deploy; or +6. regarding the completeness, accuracy, legality, utility, reliability, suitability or availability of any user content. + + +13. Releases you should read closely + +You release Status from all liability related to any claim, cause of action, controversy, dispute, loss, or damages, known or unknown, related to, arising from, or in any way connected with your use of Status Software. This includes, but is not limited to: +1. user error, such as forgotten passwords, incorrectly constructed transactions, or mistyped wallet addresses; +2. your inability to access your Wallet or the Digital Assets held in within it, for any reason, such as the loss of your seed phrase or private keys; +3. your transactions or transfers of Digital Assets from and to your Wallet; +4. any loss of value (in part or full) of your Digital Assets held in your Wallet; +5. the failure of your hardware, software, or Internet connection; +6. unauthorised access to your account, or your data on or through the Status Software, including any loss of data (such as your wallet address, private key and seed phrase); +7. technical or operational errors in or related to Status Software, including underlying code or its functionalities, as well as the code of smart contracts you may deploy; +8. any modification, suspension or discontinuance of Status Software or any feature or functionality of Status Software; +9. your use of any Third Party Service or aspect of Web 3, including smart contracts and Digital Assets, as well as your interaction with such Third Party Service or Web 3; +10. your content or other users’ content; +11. any unauthorised third party activities, including without limitation the use of viruses, phishing, brute forcing, or other means of attack against Status Software; and +12. any risks identified in these terms or any that a reasonable person should have been aware of in relation to using Status Software. + + +14. Limitation of liability + +Status will not be held liable to you under any contract, negligence, strict liability, or other legal or equitable theory for any lost profits, cost of procurement for substitute services, or any special, incidental, or consequential damages related to, arising from, or in any way connected with these terms, Status, your use of Status Software, particularly where Status has indicated that it is not responsible. This includes without limitation, your use of SNT or other Digital Assets (creating, distributing, transacting in or otherwise) even if Status has been advised of the possibility of such damages. + +In any event, Status’ aggregate liability for any claims is limited to EUR 100 (one hundred Euros). This limitation of liability will apply to the extent permitted by applicable law. + + +15. Indemnity + +You shall indemnify and hold harmless Status from and against any and all claims, damages and expenses, including attorneys’ fees, arising from or related to your use of Status Software. This includes, but is not limited to: +1. your breach or alleged breach of these terms; +2. any content you create, post, distribute or submit, messages you exchange or any other activity you may engage in related to your content on or through Status Software; +3. your use, including any of your activities on or through, Status Software; +4. your violation of any laws, rules or regulations; +5. your harm to or violation of the rights of any third party, including any privacy, intellectual property, publicity, confidentiality, property or any other rights; +6. your use of Third Party Services or any aspect of Web 3; or +7. any misrepresentation or fraudulent statements made by you. + + +16. Restricted jurisdictions + +Status is a Swiss company which makes Status Software available through third party platforms, such as third party app stores, which enables global distribution to potential users. + +You acknowledge that Status Software is not intended for use or distribution in any jurisdiction where such use or distribution of software that is similar or identical to Status Software is prohibited or restricted in any way. Your use of Status Software does not and will not subject Status to such jurisdiction under any circumstance, particularly if there are any prohibitions or restrictions on the use of such software. Status reserves the right to limit the availability of Status Software in any jurisdiction. + +Status does not conduct any business activities (i.e. activities undertaken for the purpose of earning a profit) within the United States of America, District of Columbia, Puerto Rico, the U.S. Virgin Islands, and all other U.S. territories and possessions, as well as all Swiss embargoed countries. + + +17. Materials provided are solely for informational purposes + +Status provides materials and other information through Status Software for informational purposes only and they may not always be entirely accurate, complete, or current and may also include inaccuracies or typographical errors. You are solely responsible for verifying their adequacy, completeness and accuracy and any reliance you place on such information is at your own risk, including any decisions or actions you may take in this respect. Status is not liable for any loss resulting from your action (or inaction) and decisions based on these materials or any other information. + +You should always conduct your own research and seek independent professional advice if necessary. The materials and other information that Status may provide through Status Software does not constitute financial, legal, tax, or other advice and should not be treated as such. + +Additionally, Status is not responsible for any information, content, or services contained on any third party websites accessible or linked through the Status Software. By linking such third party websites, Status does not represent or imply that it endorses or supports such third party websites or content therein, or that it believes such third party websites and content therein to be accurate, useful or non-harmful. Status has no control over such third party websites and will not be liable for your use of or activities on any third party websites accessed through Status Software. The terms and conditions, including privacy policies, of such third party websites govern your use of their websites.If you access such third party websites through Status Software, it is at your own risk and you are solely responsible for your activities on such third party websites. + + +18. Governing law and dispute resolution + +18.1 Governing law + +Swiss law governs these terms and any disputes between you and Status, whether in court or arbitration, without regard to conflict of laws provisions. + +18.2 Dispute resolution + +In these terms, “dispute” has the broadest meaning enforceable by law and includes any claim you make against Status or controversy you may have in relation to these terms, with Status or your use of Status Software. + +We prefer arbitration over litigation as we believe it meets our Principle of resolving disputes in the most efficient and cost effective manner. You are bound by the following arbitration clause, which waives your right to litigation and to be heard by a judge. Please note that a court review of an arbitration award is limited. You also waive all your rights to a jury trial (if any) in any and all jurisdictions. + +If a (potential) dispute arises, you must first use your reasonable efforts to resolve it amicably with Status by reaching out to Status’ legal team at legal@status.im. If these efforts do not result in a resolution of such dispute, you shall then send to legal@status.im a written notice of dispute setting out (i) the nature of the dispute, and the claim you are making; and (ii) the remedy you are seeking. + +If you and Status are unable to further resolve this dispute within sixty (60) calendar days of Status receiving this notice of dispute, then any such dispute will be referred to and finally resolved by you and Status through an arbitration administered by the Swiss Chambers’ Arbitration Institution in accordance with the Swiss Rules of International Arbitration for the time being in force, which rules are deemed to be incorporated herein by reference. The arbitral decision may be enforced in any court. The arbitration will be held in Zug, Switzerland, and may be conducted via video conference virtual/online methods if possible. The tribunal will consist of one arbitrator, and all proceedings as well as communications between the parties will be kept confidential. The language of the arbitration will be in English. Payment of all relevant fees in respect of the arbitration, including filing, administration and arbitrator fees will be in accordance with the Swiss Rules of International Arbitration. + +Regardless of any applicable statute of limitations, you must bring any claims within one year after the claim arose or the time when you should have reasonably known about the claim. You also waive the right to participate in a class action lawsuit or a classwide arbitration against Status. + + +19. What happens if you stop using Status Software + +If, at any time, you no longer agree to these terms, you must immediately stop using Status Software and uninstall it from all your devices. You acknowledge that even after uninstalling Status Software from all your devices, messages (including content) you have sent may still remain on the recipient’s device, blockchain transactions you have may still be recorded on the blockchain, and any smart contracts you have deployed to a blockchain may still remain deployed. + +Even after you have stopped using Status Software, all provisions of these terms which by their nature should survive termination shall survive termination. These provisions include, but are not limited to, warranty disclaimers, indemnity, limitations of liability, dispute resolution, and governing law. + + +20. General provisions + +20.1 Entire agreement + +These terms, including the underlying open source licences governing the Status Software, cover the entire agreement between you and Status regarding the Status Software. If these terms are considered an offer by Status, acceptance is expressly limited to these terms. Any failure by Status to exercise or enforce any right or provision of these terms will not constitute our waiver of such right or provision. + +20.2 Severability + +If any section of these terms is determined to be unlawful, void, or unenforceable, the unenforceable portion will be deemed to be severed from these terms. Such determination shall not affect the validity and enforceability of any other remaining provisions. + +20.3 Captions and headings + +The captions and headings identifying sections and subsections of these terms are for reference only and do not define, modify, expand, limit, or affect the interpretation of any provisions of these terms. Any illustrative graphics and links to websites are provided for your information and education, and, unless otherwise specifically indicated, they are not included in or part of these terms. + +20.4 Changes to Status Software and terms + +20.4.1 Changes to Status Software + +Status Software is an open source, community-driven project. Users can both request and contribute to feature or functionality development by contacting us directly. Status may change or discontinue some or all features and functionality on Status Software at any time in our sole discretion, including features or support for certain devices. We will not be liable to you or any third party for any modification, suspension, or discontinuation of any features or functionality of Status Software. Any and all new functions added to Status Software are also subject to these terms. + +20.4.2 Notice of changes to the terms + +We reserve the right to modify or replace any part of these terms at any time and in our sole discretion. You are solely responsible for checking Status Software, or the relevant location where these terms are published, periodically for any changes. Your continued use of Status Software following changes to these terms means that you have accepted those changes. If you do not accept such changes, then you must immediately stop using Status Software and uninstall it from your devices. + +20.5 Contact us + +If you have any questions about these terms, please contact us at legal@status.im or through the appropriate means of communication indicated on Status’ website. + +This document is CC-BY-SA. diff --git a/scripts/adb_devices_abis.sh b/scripts/adb_devices_abis.sh new file mode 100755 index 00000000000..2c3163812df --- /dev/null +++ b/scripts/adb_devices_abis.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e +typeset -a abis +SCRIPT_NAME=$(basename $0) + +function log() { + echo "${SCRIPT_NAME} ${1}: ${2}" >&2 +} + +# make sure server is running, otherwise sdb will show devices offline +adb start-server + +while IFS= read line; do + read -a arr <<< "${line}" + + device_name="${arr[0]}" + device_status=${arr[1]} + + log "DEBUG" "device_name=${device_name}" + log "DEBUG" "device_status=${device_status}" + + if [[ "${device_status}" != "device" ]]; then + continue + fi + + device_product_abi=$(adb -s ${device_name} shell -n getprop ro.product.cpu.abi) + log "DEBUG" "device_product_abi=${device_product_abi}" + + if [[ ! ${abis[*]} =~ ${device_product_abi} ]]; then + abis+=("${device_product_abi}") + fi +done <<< "$(adb devices | tail -n+2)" + +if [[ ! "${abis[*]}" ]]; then + log "ERROR" "no devices found. Check 'adb devices -l' output and share with Infra team if needed." + exit 3 +fi + +log "DEBUG" "resulting abis:" +IFS=\;; echo "${abis[*]}" diff --git a/scripts/build-android.sh b/scripts/build-android.sh index 06a8cd51fdd..0ef6090f97f 100755 --- a/scripts/build-android.sh +++ b/scripts/build-android.sh @@ -1,36 +1,89 @@ #!/usr/bin/env bash -set -euf - -TARGET=${1:-debug} - -CURRENT_DIR="$( cd "$( dirname "$0" )" && pwd )" -. "$CURRENT_DIR/lib/setup/path-support.sh" -source_lib "output.sh" -source_lib "properties.sh" - -cecho "Building @b@green[[${TARGET}]] environment" -echo - -GRADLE_PROPERTIES="--daemon --parallel -q -b android/build.gradle" - -npm install - -case $TARGET in - debug) - lein do clean, cljsbuild once android && ./android/gradlew ${GRADLE_PROPERTIES} assembleDebug - echo "Generated android/app/build/outputs/apk/debug/app-debug.apk" - exit 1 - ;; - release) - STORE_FILE=$(property_gradle 'STATUS_RELEASE_STORE_FILE') - [[ ! -e "${STORE_FILE/#\~/$HOME}" ]] && echo "Please generate keystore first using ./generate-keystore.sh" && exit 0 - lein do clean, with-profile prod cljsbuild once android && ./android/gradlew ${GRADLE_PROPERTIES} assembleRelease - cecho "Generated @b@blueandroid/app/build/outputs/apk/release/app-release.apk" - echo - exit - ;; - *) - echo "Only debug and release targets are supported" - exit 1 -esac +# Needed to fail on must_get_env() +set -e + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" + +function must_get_env() { + declare -n VAR_VALUE="$1" + if [[ -n "${VAR_VALUE}" ]]; then + echo "${VAR_VALUE}" + return + fi + echo -e "${RED}No required env variable:${RST} ${BLD}${!VAR_VALUE}${RST}" 1>&2 + exit 1 +} + +function append_env_export() { + VAR_NAME=${1} + VAR_VALUE=$(must_get_env "${VAR_NAME}") + echo "export ${VAR_NAME}=\"${VAR_VALUE}\";" >> "${SECRETS_FILE_PATH}" +} +nixOpts=() + +# We create if now so the trap knows its location +export SECRETS_FILE_PATH=$(mktemp) +chmod 644 ${SECRETS_FILE_PATH} +# If secrets file was created we want to remove it. +trap "rm -vf ${SECRETS_FILE_PATH}" EXIT ERR INT QUIT + +# Names of variables containing secrets to save in env file. +SECRETS_ENV_VARS=( + 'ALCHEMY_ETHEREUM_MAINNET_TOKEN' + 'ALCHEMY_ETHEREUM_SEPOLIA_TOKEN' + 'ALCHEMY_ARBITRUM_MAINNET_TOKEN' + 'ALCHEMY_ARBITRUM_SEPOLIA_TOKEN' + 'ALCHEMY_OPTIMISM_MAINNET_TOKEN' + 'ALCHEMY_OPTIMISM_SEPOLIA_TOKEN' + 'ALCHEMY_BASE_MAINNET_TOKEN' + 'ALCHEMY_BASE_SEPOLIA_TOKEN' + 'INFURA_TOKEN' + 'INFURA_TOKEN_SECRET' + 'MIXPANEL_APP_ID' + 'MIXPANEL_APP_TOKEN' + 'OPENSEA_API_KEY' + 'POKT_TOKEN' + 'RARIBLE_MAINNET_API_KEY' + 'RARIBLE_TESTNET_API_KEY' + 'SENTRY_DSN_STATUS_GO' + 'STATUS_BUILD_ETH_RPC_PROXY_PASSWORD' + 'STATUS_BUILD_ETH_RPC_PROXY_URL' + 'STATUS_BUILD_ETH_RPC_PROXY_USER' + 'STATUS_BUILD_PROXY_PASSWORD' + 'STATUS_BUILD_PROXY_USER' + 'STATUS_MARKET_DATA_PROXY_URL' + 'STATUS_MARKET_DATA_PROXY_USER' + 'STATUS_MARKET_DATA_PROXY_PASSWORD' +) + +# Secrets like this can't be passed via args or they end up in derivation. +for SECRET_VAR_NAME in "${SECRETS_ENV_VARS[@]}"; do + [[ -n "${!SECRET_VAR_NAME}" ]] && append_env_export "${SECRET_VAR_NAME}" +done + +# Used by Clojure at compile time for remove import of firebase for fdroid release +if [[ -n "${ANDROID_STORE}" ]]; then + append_env_export 'ANDROID_STORE' +fi + +# If no secrets were passed there's no need to pass the 'secretsFile'. +if [[ -s "${SECRETS_FILE_PATH}" ]]; then + nixOpts+=("--option" "extra-sandbox-paths" "${SECRETS_FILE_PATH}") + nixOpts+=("--argstr" "secretsFile" "${SECRETS_FILE_PATH}") +fi + +# Used by Clojure at compile time to include JS modules +nixOpts+=("--argstr" "buildEnv" "$(must_get_env BUILD_ENV)") + +# On Darwin we hit a sandbox serialization limit of 65535. +# https://github.com/NixOS/nix/issues/4119 +if [[ "$(uname -s)" =~ Darwin ]]; then + nixOpts+=("--option" "build-use-sandbox" "false") +else + nixOpts+=("--option" "build-use-sandbox" "true") +fi + + +"${GIT_ROOT}/nix/scripts/build.sh" targets.mobile.android.build "${nixOpts[@]}" diff --git a/scripts/build_no.sh b/scripts/build_no.sh deleted file mode 100755 index f5e7cbcb8aa..00000000000 --- a/scripts/build_no.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env sh - -set -e - -# -# This script manages app build numbers. -# It returns the next build number to be used. -# If ran with --tag it will mark current HEAD with new build number. -# -# These numbers are used to mark app artifacts for: -# * Play Store - versionCode attribute (gradle) -# * Apple Store - CFBundleVersion attribute (plutil) -# -# The numbers need to be incremeneted and are maintained via -# git tags matching the '^build-[0-9]+$' regex. -# Builds from an already tagged commit should use the same number. -# -# For more details see: -# * https://developer.android.com/studio/publish/versioning -# * https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# - -getNumber () { - echo "$BUILD" | sed 's/[^0-9]*//g' -} - -REGEX='^build-[0-9]\+$' - -# make sure we have all the tags -git fetch --tags --quiet >/dev/null || >&2 echo "Could not fetch tags from remote" - -# even if the current commit has a tag already, it is normal that the same commit -# is built multiple times (with different build configurations, for instance), -# so we increment the build number every time. - -# find the last used build number -BUILD=$(git tag -l --sort=-v:refname | grep -e "$REGEX" | head -n 1) -# extract the number -BUILD_NO=$(getNumber "$BUILD") - -if [ "$1" = "--increment" ]; then - # These need to be provided by Jenkins - if [ -z "${GIT_USER}" ] || [ -z "${GIT_PASS}" ]; then - echo "Git credentials not specified! (GIT_USER, GIT_PASS)" >&2 - exit 1 - fi - # increment - BUILD_NO="$((BUILD_NO+1))" - - echo "Tagging HEAD: build-$BUILD_NO" >&2 - git tag "build-$BUILD_NO" HEAD - git push --tags https://${GIT_USER}:${GIT_PASS}@github.com/status-im/status-react -fi - -# finally print build number -echo "$BUILD_NO" diff --git a/scripts/bundle-status-go.sh b/scripts/bundle-status-go.sh deleted file mode 100755 index b37e1921157..00000000000 --- a/scripts/bundle-status-go.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env sh - -if [ -z $STATUS_GO_HOME ] ; then - echo "Please define STATUS_GO_HOME" - exit 1 -fi -if [ -z $STATUS_REACT_HOME ] ; then - echo "Please define STATUS_REACT_HOME" - exit 1 -fi -if [ $# -eq 0 ]; then - echo "Please specify platforms to bundle as discrete arguments (ios, android)" - exit 1 -fi - -set -euf - -for platform in "$@"; do - case $platform in - ios | android) - echo "Bundling $platform platform" - - cd $STATUS_GO_HOME - ;; - *) - echo "Undefined platform $platform" - exit 1 - esac - - case $platform in - ios) - # Build status-go artifact for iOS: - make statusgo-ios-simulator - - # You should see iOS framework cross compilation done. This builds the following artifact: - # - # > (cd status-go && find . -iname "Statusgo.framework") - # ./build/bin/statusgo-ios-9.3-framework/Statusgo.framework - # - # Normally this is installed by Maven via Artifactory in this step - # mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack - # - # Locally you can see it here: - # > (cd status-react && find . -iname "Statusgo.framework") - # ./modules/react-native-status/ios/RCTStatus/Statusgo.framework - # ./modules/react-native-status/ios/RCTStatus/Statusgo.framework/Statusgo.framework - # - # Instead we are going to manually overwrite it. - - # For Xcode to pick up the new version, remove the whole framework first: - rm -r $STATUS_REACT_HOME/modules/react-native-status/ios/RCTStatus/Statusgo.framework/ || true - - # Then copy over framework: - cp -R $STATUS_GO_HOME/build/bin/statusgo-ios-9.3-framework/Statusgo.framework $STATUS_REACT_HOME/modules/react-native-status/ios/RCTStatus/Statusgo.framework - - # In Xcode, clean and build. If you have any scripts to do this, make sure that - # you don't accidentally run the mvn step to undo your manual install. - # - - # It might also be a good idea to print something custom so you can easily tell - # the difference between an old and new version of status-go. - - cd - - - echo "[Done]" - echo "[You can now build in Xcode]" - ;; - android) - # Build status-go artifact for Android: - make statusgo-android - - target=$STATUS_REACT_HOME/modules/react-native-status/android/libs/status-im/status-go/local - [ -d $target ] || mkdir -p $target - # Copy over framework: - cp -R $STATUS_GO_HOME/build/bin/statusgo-android-16.aar $target/status-go-local.aar - - # It might also be a good idea to print something custom so you can easily tell - # the difference between an old and new version of status-go. - - cd - - - echo "[Done]" - ;; - esac -done \ No newline at end of file diff --git a/scripts/check-metro-shadow-process.sh b/scripts/check-metro-shadow-process.sh new file mode 100755 index 00000000000..32111d0e021 --- /dev/null +++ b/scripts/check-metro-shadow-process.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +if pgrep -f 'shadow-cljs watch mobile' > /dev/null; then + echo "Error: make run-clojure is already running in another terminal" >&2 + echo "Please close that terminal before running this command." >&2 + exit 1 +fi + +if pgrep -f "react-native start --port=${RCT_METRO_PORT:-8081}" > /dev/null; then + echo "Error: make run-metro is already running in another terminal" >&2 + echo "Please close that terminal before running this command." >&2 + exit 1 +fi + +exit 0 diff --git a/scripts/check-nix-shell.sh b/scripts/check-nix-shell.sh new file mode 100755 index 00000000000..3da325c2e73 --- /dev/null +++ b/scripts/check-nix-shell.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Check if inside Nix shell +if [[ -z "${IN_NIX_SHELL}" ]]; then + echo "ERROR: This command must be run inside a Nix shell. Please ensure you're inside a nix shell and try again." + exit 1 +fi diff --git a/scripts/colors.sh b/scripts/colors.sh new file mode 100644 index 00000000000..6b19a153f67 --- /dev/null +++ b/scripts/colors.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Colors +export YLW='\033[1;33m' +export RED='\033[0;31m' +export GRN='\033[0;32m' +export BLU='\033[0;34m' +export BLD='\033[1m' +export RST='\033[0m' + +# Clear line +export CLR='\033[2K' diff --git a/scripts/compress_image.sh b/scripts/compress_image.sh new file mode 100755 index 00000000000..c79e1226b1c --- /dev/null +++ b/scripts/compress_image.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +input_file="$1" +backup_file="${input_file%.png}.bak.png" +output_file="$input_file" + +cp "$input_file" "$backup_file" + +magick "$input_file" -quality 100 -define png:compression-level=9 -strip -colors 256 "$output_file" + +if [ $? -eq 0 ]; then + echo "Image compression completed: $input_file" + echo "Backup created: $backup_file" +else + mv "$backup_file" "$input_file" + echo "Error during image compression" + exit 1 +fi diff --git a/scripts/create-desktop-mac-bundle.sh b/scripts/create-desktop-mac-bundle.sh deleted file mode 100755 index 32504c86273..00000000000 --- a/scripts/create-desktop-mac-bundle.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' -SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" -STATUSREACTPATH="$SCRIPTPATH/.." -WORKFOLDER="$SCRIPTPATH/../mac_bundle" -MACDEPLOYQT="" - -#if no arguments passed, inform user about possible ones (one for making script interactive, one for path to macdeployqt) - -if [ $# -eq 0 ] - then - echo -e "${RED}You need to specify path to macdeployqt binary as an argument${NC}" - echo "Example: scripts/create-desktop-mac-bundle.sh /usr/bin/macdeployqt" - exit 1 - else - MACDEPLOYQT=$1 -fi - -# check if gdrive installed -command -v gdrive >/dev/null 2>&1 || { echo -e "${RED}gdrive tool need to be installed. (brew install gdrive). Aborting.${NC}" >&2; exit 1; } - - -# inform user that define should be changed in "desktop/main.cpp" -echo "" -echo -e "${YELLOW}In desktop/main.cpp file please uncomment #define BULID_FOR_BUNDLE line.${NC}" -read -p "When ready, plese press enter to continue" -echo "" - - -# create directory for all work related to bundling -mkdir -p $WORKFOLDER -echo -e "${GREEN}Work folder created: $WORKFOLDER${NC}" -echo "" - -# from index.desktop.js create javascript bundle and resources folder -echo "Generating StatusIm.bundle and assets folder..." -react-native bundle --entry-file index.desktop.js --bundle-output $WORKFOLDER/StatusIm.jsbundle --dev false --platform desktop --assets-dest $WORKFOLDER/assets -echo -e "${GREEN}Generating done.${NC}" -echo "" - -# show path to javascript bundle and line that should be added to package.json -echo -e "${YELLOW}Please add the following line to package.json:${NC}" -echo "\"desktopJSBundlePath\": \"$WORKFOLDER/StatusIm.jsbundle\"" -echo "" -read -p "When ready, plese press enter to continue" -echo "" - - -# build desktop app -echo "Building StatusIm desktop..." -react-native build-desktop -echo -e "${GREEN}Building done.${NC}" -echo "" - - -# download prepared package with mac bundle files (it contains qt libraries, icon) -echo "Downloading skeleton of mac bundle..." -echo -e "${YELLOW}First time gdrive can ask you for permissions to google drive${NC}" -gdrive download --path $WORKFOLDER 1fJbW9FzGGPvYkuJcSH5mCAcGdnyUeDSY -echo -e "${GREEN}Downloading done.${NC}" -echo "" - - -# Unpacking downloaded archive -echo "Unpacking bundle skeleton" -unzip $WORKFOLDER/StatusIm.app.zip -d $WORKFOLDER -chmod +x $WORKFOLDER/StatusIm.app/Contents/MacOs/ubuntu-server -echo -e "${GREEN}Unzipping done.${NC}" -echo "" - - -# copy binary and resources to mac bundle -echo "Copying resources and binary..." -cp -r $WORKFOLDER/assets/share/assets $WORKFOLDER/StatusIm.app/Contents/MacOs -cp $STATUSREACTPATH/desktop/bin/StatusIm $WORKFOLDER/StatusIm.app/Contents/MacOs -echo -e "${GREEN}Copying done.${NC}" -echo "" - - -# invoke macdeployqt to create StatusIm.dmg -echo "Creating bundle dmg..." -$MACDEPLOYQT $WORKFOLDER/StatusIm.app -verbose=3 -qmldir="$STATUSREACTPATH/node_modules/react-native/ReactQt/application/src/" -qmldir="$STATUSREACTPATH/node_modules/react-native/ReactQt/runtime/src/qml/" -dmg -echo -e "${GREEN}Bundle ready!${NC}" -echo "" diff --git a/scripts/cut-release.sh b/scripts/cut-release.sh new file mode 100755 index 00000000000..28ffd9f3f30 --- /dev/null +++ b/scripts/cut-release.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +# Increment a version string using Semantic Versioning (SemVer) terminology. + +# Parse command line options. + +set -e + +if ! git diff-index --quiet HEAD; then + echo "Your Git working directory is not clean!" >&2 + exit 1 +fi + +while getopts ":Mmp" Option +do + case $Option in + M ) major=true;; + m ) minor=true;; + esac +done + +shift $(($OPTIND - 1)) + +echo "Checking out develop..." +git checkout develop + +echo "Pulling latest develop..." +git pull + +version=$(cat VERSION) + +if [[ -z $major ]] && [[ -z $minor ]]; then + minor=true +fi + +# Build array from version string. + +a=( ${version//./ } ) + +# If version string is missing or has the wrong number of members, show usage message. + +if [ ${#a[@]} -ne 3 ] +then + echo "usage: $(basename $0) [-Mmp]" + exit 1 +fi + +# Increment version numbers as requested. + +if [ ! -z $major ] +then + ((a[0]++)) + a[1]=0 + a[2]=0 +fi + +if [ ! -z $minor ] +then + ((a[1]++)) + a[2]=0 +fi + +newversion=${a[0]}.${a[1]}.${a[2]} +releasebranch="${a[0]}.${a[1]}.x" +echo "Cutting release branch $newversion" + +prbranch="chore/update-release-to-$newversion" + + +git checkout -b $prbranch +echo $newversion > VERSION +git add VERSION +git commit -m "Bump release to $newversion" +git push --set-upstream origin $prbranch +echo "Creating PR..." +if ! command -v gh &> /dev/null +then + echo "Github command line not present, don't forget to create one" +else + gh pr create --title "Bump release to $newversion" --body "status:ready" +fi + +echo "Creating release branch..." +git checkout -b release/$releasebranch +echo "Release branch created, push to origin to start building" diff --git a/scripts/diawi-upload.mjs b/scripts/diawi-upload.mjs new file mode 100755 index 00000000000..9c5ba1e1619 --- /dev/null +++ b/scripts/diawi-upload.mjs @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +import https from 'node:https' +import { basename } from 'node:path' +import { promisify } from 'node:util' +import { createReadStream } from 'node:fs' + +import log from 'npmlog' +import FormData from 'form-data' + +const UPLOAD_URL = 'https://upload.diawi.com/' +const STATUS_URL = 'https://upload.diawi.com/status' +const DIAWI_TOKEN = process.env.DIAWI_TOKEN +const LOG_LEVEL = process.env.VERBOSE ? 'verbose' : 'info' +const POLL_MAX_COUNT = process.env.POLL_MAX_COUNT ? process.env.POLL_MAX_COUNT : 120 +const POLL_INTERVAL_MS = process.env.POLL_INTERVAL_MS ? process.env.POLL_INTERVAL_MS : 500 + +const sleep = (ms) => { + return new Promise((resolve, reject) => { + setTimeout(resolve, (ms)) + }) +} + +const getRequest = async (url) => { + return new Promise((resolve, reject) => { + let data = [] + https.get(url, res => { + res.on('error', err => reject(err)) + res.on('data', chunk => { data.push(chunk) }) + res.on('end', () => { + let payload = Buffer.concat(data).toString() + resolve({ + code: res.statusCode, + message: res.statusMessage, + payload: payload, + }) + }) + }) + }) +} + +const uploadIpa = async (ipaPath, comment, token) => { + let form = new FormData() + form.append('token', token) + form.append('file', createReadStream(ipaPath)) + form.append('comment', comment || basename(ipaPath)) + + const formSubmitPromise = promisify(form.submit.bind(form)) + + const res = await formSubmitPromise(UPLOAD_URL) + if (res.statusCode != 200) { + log.error('uploadIpa', 'Upload failed: %d %s', res.statusCode, res.statusMessage) + process.exit(1) + } + + return new Promise((resolve) => { + const jobId = res.on('data', async (data) => { + resolve(JSON.parse(data)['job']) + }) + }) +} + +const checkStatus = async (jobId, token) => { + let params = new URLSearchParams({ + token: token, job: jobId, + }) + let rval = await getRequest(`${STATUS_URL}?${params.toString()}`) + if (rval.code != 200) { + log.error('checkStatus', 'Check query failed: %d %s', rval.code, rval.message) + process.exit(1) + } + return JSON.parse(rval.payload) +} + +const pollStatus = async (jobId, token) => { + let interval = POLL_INTERVAL_MS + for (let i = 0; i <= POLL_MAX_COUNT; i++) { + let json = await checkStatus(jobId, token) + switch (json.status) { + case 2000: + return json + case 2001: + log.verbose('pollStatus', 'Waiting: %s', json.message) + break /* Nothing, just poll again. */ + case 4000000: + log.warning('pollStatus', 'Doubling polling interval: %s', json.message) + interval *= 2 + break + default: + log.error('pollStatus', `Error in status response: ${json.message}`) + process.exit(1) + } + await sleep(interval) + } + log.error('pollStatus', 'Failed to poll status after %d retries.', POLL_MAX_COUNT) + process.exit(1) +} + +const main = async () => { + const targetFile = process.argv[2] + const comment = process.argv[3] + log.level = LOG_LEVEL + + if (DIAWI_TOKEN === undefined) { + log.error('main', 'No DIAWI_TOKEN env var provided!') + process.exit(1) + } + if (targetFile === undefined) { + log.error('main', 'No file path provided!') + process.exit(1) + } + + log.info('main', 'Uploading: %s', targetFile) + let jobId = await uploadIpa(targetFile, comment, DIAWI_TOKEN) + + log.info('main', 'Polling upload job status: %s', jobId) + let uploadMeta = await pollStatus(jobId, DIAWI_TOKEN) + + console.log(uploadMeta) +} + +main() diff --git a/scripts/download-package-files.sh b/scripts/download-package-files.sh deleted file mode 100755 index 4b44a1c5630..00000000000 --- a/scripts/download-package-files.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -filename=$1 -#fileid="1yPTGcPe5DZd3ubzAgUBp3aAQRAOK9eKQ" -fileid=$2 -rm -rf ./cookie -curl -c ./cookie -s -L "https://drive.google.com/uc?export=download&id=${fileid}" > /dev/null -curl -Lb ./cookie "https://drive.google.com/uc?export=download&confirm=`awk '/download/ {print $NF}' ./cookie`&id=${fileid}" -o ${filename} \ No newline at end of file diff --git a/scripts/encrypt-keychain.sh b/scripts/encrypt-keychain.sh new file mode 100755 index 00000000000..dbd9eebf1b4 --- /dev/null +++ b/scripts/encrypt-keychain.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +KEYCHAIN="$1" + +if [ $# -ne 1 ]; then + echo -e "encrypt-keychain.sh " + exit 10 +elif [ ! -f "$KEYCHAIN" ]; then + echo -e "Keychain file does not exist." + exit 20 +fi + +INNER_CIPHER="CAMELLIA256" +OUTER_CIPHER="AES256" + +echo -e "\n### Double-encrypting ${KEYCHAIN}. Enter first inner, then outer passphrase..." +gpg --symmetric --cipher-algo "$INNER_CIPHER" --s2k-digest-algo SHA256 --s2k-count 65011712 "${KEYCHAIN}" +gpg --symmetric --cipher-algo "$OUTER_CIPHER" --s2k-digest-algo SHA256 --s2k-count 65011712 "${KEYCHAIN}.gpg" +mv -f "${KEYCHAIN}.gpg.gpg" "${KEYCHAIN}.gpg" + diff --git a/scripts/fdroid-pr.sh b/scripts/fdroid-pr.sh new file mode 100755 index 00000000000..b0985d3a732 --- /dev/null +++ b/scripts/fdroid-pr.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +FDROIDDATA_REPO_URL="https://gitlab.com/fdroid/fdroiddata.git" +WORKING_DIR="${HOME}/fdroid-release" + +source "${GIT_ROOT}/scripts/colors.sh" + +function log_info() { echo -e " ${GRN}>>>${RST} $@"; } +function log_data() { echo -e " ${BLU}===${RST} $@"; } +function log_notice() { echo -e " ${YLW}<<<${RST} $@"; } +function log_warning() { echo -e " ${RED}!!!${RST} $@"; } + +if [[ $# -ne 1 ]]; then + echo "No release URL provided!" >&2 + echo "Usage: fdroid-pr.sh https://.../release.apk" >&2 + exit 1 +fi +# URL of APK to identify release version code +RELEASE_APK="${1}" + +log_info "Creating working dir..." +mkdir -p "${WORKING_DIR}" + +APK_FILE="${WORKING_DIR}/status.apk" +if [[ -f "${RELEASE_APK}" ]]; then + log_info "Using: ${RELEASE_APK}" + APK_FILE="${RELEASE_APK}" +else + log_info "Fetching release..." + curl -s "${RELEASE_APK}" -o "${APK_FILE}" +fi + +log_info "Parsing APK..." + +VERSION_CODE=$(apkanalyzer manifest version-code "${APK_FILE}") +if [[ -n "${VERSION_CODE}" ]]; then + log_data "Version Code: ${VERSION_CODE}" +else + log_warning "Failed to find version code." >&2; exit 1 +fi + +VERSION_NAME=$(apkanalyzer manifest print "${APK_FILE}" | awk -F'"' '/android:versionName/{print $2}') +if [[ -n "${VERSION_NAME}" ]]; then + log_data "Version Name: ${VERSION_NAME}" +else + log_warning "Failed to find version name." >&2; exit 1 +fi + +COMMIT_HASH=$(apkanalyzer manifest print "${APK_FILE}" | awk -F'"' '/commitHash/{getline; print $2}') +if [[ -n "${COMMIT_HASH}" ]]; then + log_data "Commit Hash: ${COMMIT_HASH}" +else + log_warning "Failed to find commit hash." >&2; exit 1 +fi + +CLONE_DIR="${WORKING_DIR}/fdroiddata" +METADATA_FILE="${CLONE_DIR}/metadata/im.status.ethereum.yml" + +PREVIOUS_BRANCH="" +if [[ -d "${CLONE_DIR}" ]]; then + log_info "Fetching: ${FDROIDDATA_REPO_URL}" + cd "${CLONE_DIR}" + PREVIOUS_BRANCH=$(git rev-parse --abbrev-ref HEAD) + git checkout master + git pull --force +else + log_info "Cloning: ${FDROIDDATA_REPO_URL}" + git clone -q --depth=1 "${FDROIDDATA_REPO_URL}" "${CLONE_DIR}" + cd "${CLONE_DIR}" +fi + +BRANCH_NAME="status-im/v${VERSION_NAME}" +log_info "Checkout out branch: ${BRANCH_NAME}" +if [[ "${PREVIOUS_BRANCH}" == "${BRANCH_NAME}" ]]; then + log_warning "Removing previous branch: ${PREVIOUS_BRANCH}" + git branch -D "${BRANCH_NAME}" +fi +git switch -C "${BRANCH_NAME}" + +log_info "Updating metadata file..." + +# find line number of last "versionName" line +START_LINE=$(awk '/ versionCode:/{n=NR} END{print n}' "${METADATA_FILE}") +# find line number of last "build" line +END_LINE=$(awk '/ build:/{n=NR} END{print n}' "${METADATA_FILE}") +# Get the latest entry, excluding version info +BUILD_PARAMS=$(awk "NR >= $((START_LINE+2)) && NR <= ${END_LINE}" "${METADATA_FILE}") + +# Build new entry +NEW_ENTRY=" + - versionName: ${VERSION_NAME} + versionCode: ${VERSION_CODE} + commit: ${COMMIT_HASH} +${BUILD_PARAMS}" + +# Insert new release build entry +sed -i "$((END_LINE+1))i\\${NEW_ENTRY//$'\n'/\\n}" "${METADATA_FILE}" +# Update current version values +sed -i "s/CurrentVersion: .*/CurrentVersion: ${VERSION_NAME}/" "${METADATA_FILE}" +sed -i "s/CurrentVersionCode: .*/CurrentVersionCode: ${VERSION_CODE}/" "${METADATA_FILE}" + +log_info "Committing changes..." +# Add, commit and push +COMMIT_MESSAGE="Update Status to ${VERSION_NAME} (${VERSION_CODE})" +git add ${METADATA_FILE} +git commit -m "${COMMIT_MESSAGE}" + +log_info "SUCCESS" +log_notice "Now add your fork of fdroiddata as a remote to the repository and push." +log_notice "Then create a Merge Request from the branch in your fork." +log_notice "Repo path: ${BLD}${CLONE_DIR}${RST}" diff --git a/scripts/gen-random-passphrase.sh b/scripts/gen-random-passphrase.sh new file mode 100755 index 00000000000..28448d5b11e --- /dev/null +++ b/scripts/gen-random-passphrase.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +if [ `uname` == 'Darwin' ]; then + echo " +Uh oh. We seem to be on macOS where I don't know how to bypass the +PRNG and can't guarantee the entropy of the data. Run this on Linux or +BSD, or use 'pwgen --secure 86' instead if using a PRNG is acceptable. +" + exit 1 +fi + +echo " +Generating random passphrases with 512 bits of entropy and a length +of 86 characters, intended for publicly hosted private keys. This is +total overkill even for AES-256, but we want some headroom if the RNG +is less than perfect and they aren't meant for human memorisation. + +If this hangs, move the mouse around, type into a different window and do +some disk and network I/O to generate additional noise for the entropy pool. + +When done, intersperse some random characters of your choice throughout the +passphrases. +" +for PASS in INNER OUTER KEYCHAIN; do + echo "$PASS" + head -c 64 /dev/random | base64 | head -c 86 + echo -e "\n" +done diff --git a/scripts/generate-keystore.sh b/scripts/generate-keystore.sh index 7ef3c2e62c6..5f4c473c717 100755 --- a/scripts/generate-keystore.sh +++ b/scripts/generate-keystore.sh @@ -1,21 +1,53 @@ #!/usr/bin/env bash -set -euf pipefail +set -ef pipefail -TARGET=${1:-debug} +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" -CURRENT_DIR="$( cd "$( dirname "$0" )" && pwd )" -. "$CURRENT_DIR/lib/setup/path-support.sh" -source_lib "properties.sh" +function property() { + grep "${2}" "${1}" | cut -d'=' -f2 +} -STORE_FILE=$(property_gradle 'STATUS_RELEASE_STORE_FILE') -STORE_FILE="${STORE_FILE/#\~/$HOME}" -STATUS_RELEASE_STORE_PASSWORD=$(property_gradle 'STATUS_RELEASE_STORE_PASSWORD') -STATUS_RELEASE_KEY_ALIAS=$(property_gradle 'STATUS_RELEASE_KEY_ALIAS') -STATUS_RELEASE_KEY_PASSWORD=$(property_gradle 'STATUS_RELEASE_KEY_PASSWORD') +function gradle_property() { + property "${GIT_ROOT}/android/gradle.properties" ${1} +} -[[ -e "${STORE_FILE/#\~/$HOME}" ]] && echo "Keystore $STORE_FILE already exists, please manually remove it if you want to regenerate." && exit 0 +function env_var_or_gradle_prop() { + VAR_NAME="${1}" + if [[ -n "${!VAR_NAME}" ]]; then + echo "${!VAR_NAME}" + else + gradle_property "${VAR_NAME}" + fi +} -echo "Generating keystore $STORE_FILE" -keytool -genkey -v -keystore ${STORE_FILE} -keyalg RSA -keysize 2048 -validity 10000 -alias ${STATUS_RELEASE_KEY_ALIAS} \ - -storepass ${STATUS_RELEASE_STORE_PASSWORD} -keypass ${STATUS_RELEASE_KEY_PASSWORD} -dname "CN=, OU=, O=, L=, S=, C=" \ No newline at end of file +KEYSTORE_PASSWORD=$(env_var_or_gradle_prop 'KEYSTORE_PASSWORD') +KEYSTORE_ALIAS=$(env_var_or_gradle_prop 'KEYSTORE_ALIAS') +KEYSTORE_KEY_PASSWORD=$(env_var_or_gradle_prop 'KEYSTORE_KEY_PASSWORD') +KEYSTORE_PATH=$(env_var_or_gradle_prop 'KEYSTORE_PATH') + +# Use absolute path for Keystore +KEYSTORE_PATH=${KEYSTORE_PATH/#\~/$HOME} + +if [[ -e "${KEYSTORE_PATH}" ]]; then + echo -e "${YLW}Keystore file already exists:${RST} ${KEYSTORE_PATH}" >&2 + exit 0 +fi + +KEYSTORE_DIR=$(dirname "${KEYSTORE_PATH}") +[[ -d $KEYSTORE_DIR ]] || mkdir -p $KEYSTORE_DIR + +echo -e "${GRN}Generating keystore...${RST}" >&2 + +exec keytool -genkey -v \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -deststoretype pkcs12 \ + -dname "CN=, OU=, O=, L=, S=, C=" \ + -keystore "${KEYSTORE_PATH}" \ + -alias "${KEYSTORE_ALIAS}" \ + -storepass "${KEYSTORE_PASSWORD}" \ + -keypass "${KEYSTORE_KEY_PASSWORD}" \ + >&2 diff --git a/scripts/generate-pull-request-report.sh b/scripts/generate-pull-request-report.sh deleted file mode 100755 index 7c3f31e6b7f..00000000000 --- a/scripts/generate-pull-request-report.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash - -# Github Client Credentials -# Required to increase rate limits to a useable level. - -GITHUB_CLIENT_ID=cae8611e191547e163bb -GITHUB_CLIENT_SECRET=ebcfe4e78a6bb96933a3e2476a6d1e8efa8d8b07 - -# Generates a markdown report for the pull requests given in STDIN -# The input format should match the output of list-pull-requests.sh: -# [pr #] [tab] [github pr api url] [tab] [comma separated list of commits] - -usage() { - echo "DESCRIPTION" - echo "" - # Generates a markdown report for the pull requests given in STDIN - # The input format should match the output of list-pull-requests.sh: - # [pr #] [tab] [github pr api url] [tab] [comma separated list of commits] - echo "" -} - -if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then - usage - exit -fi - -fatal() { - echo "FATAL: $@" >&2 - exit 1 -} - -echo_json() { - local json=$1 - local path=$2 - echo -n "$json" | jq -r "$path" -} - -check_pr_prereq() { - if ! command -v jq >/dev/null; then - fatal "jq(1) is not found, PR cannot be queried." - fi - if ! command -v curl >/dev/null; then - fatal "curl(1) is not found, PR cannot be queried." - fi -} - -fetch() { - local url="$1?client_id=$GITHUB_CLIENT_ID&client_secret=$GITHUB_CLIENT_SECRET" - fetch_result=$(curl --fail -fsS "$url") - if [ $? -ne 0 ]; then fatal "Unable to get fetch from $1"; fi -} - -process_stdin() { - while IFS= read -r line; do - local pr_url=`echo "$line" | cut -f 2` - echo_pull_request "$pr_url" - done -} - -echo_pull_request() { - local pr_url=$1 - fetch "$pr_url" - local pr=$fetch_result - - echo "## $(echo_json "$pr" .title) (#$(echo_json "$pr" .number)) - -URL: $(echo_json "$pr" .html_url) -Creator: $(echo_json "$pr" .user.login) -Labels: $(echo_json "$pr" ' [.labels[].name] | join(", ") ' ) - -$(echo_json "$pr" .body) - -" -} - -check_pr_prereq - -echo "# Pull Request Report - -$( process_stdin ) - -" diff --git a/scripts/google-free.sh b/scripts/google-free.sh new file mode 100755 index 00000000000..0c21577943e --- /dev/null +++ b/scripts/google-free.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +# Used by Clojure to condition code and Gradle to make dependencies optional +sed -i -e '$aGOOGLE_FREE=1' .env.release + +# remove firebase (uses google dependencies) and google-services.json for the fdroid-build +yarn remove @react-native-firebase/app +yarn remove @react-native-firebase/messaging +rm android/app/google-services.json +rm android/app/googleServices.gradle diff --git a/scripts/hooks/prepare-commit-msg b/scripts/hooks/prepare-commit-msg new file mode 100755 index 00000000000..9a3dc5f00cb --- /dev/null +++ b/scripts/hooks/prepare-commit-msg @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -eo pipefail + +if [[ -z $(git diff --cached --name-only -r | grep status-go-version.json) ]]; then + exit 0 # No status-go-version.json changes found. +fi + +# MacOS sed is garbage and interprets newlines differently. +[[ "$(uname)" -eq "Darwin" ]] && NL=$'\\\n' || NL="\n" + +MERGE_BASE=$(git merge-base -a develop "$(git rev-parse --abbrev-ref HEAD)" develop) +GO_COMMIT_MERGE_BASE=$(git show "${MERGE_BASE}":status-go-version.json | awk -F'"' '/commit-sha1/{print $4}') +GO_COMMIT_CURRENT=$(awk -F'"' '/commit-sha1/{print $4}' status-go-version.json) +GITHUB_LINK_PREFIX="https://github.com/status-im/status-go/compare/" +# Link to the current StatusGo changelog being updated. +GITHUB_LINK="${GITHUB_LINK_PREFIX}${GO_COMMIT_MERGE_BASE::8}...${GO_COMMIT_CURRENT::8}" + +COMMIT_MSG_FILE=$1 +# Check if the commit message already contains the link (rebase) and update otherwise insert into line2 +if ! grep -qF "${GITHUB_LINK_PREFIX}" "${COMMIT_MSG_FILE}" >/dev/null; then + sed -in'' -e "2i\\ +${NL}${GITHUB_LINK}" "${COMMIT_MSG_FILE}" +else + sed -in'' -e "s;^${GITHUB_LINK_PREFIX}.*$;${GITHUB_LINK};" "${COMMIT_MSG_FILE}" +fi diff --git a/scripts/inotify_fix.sh b/scripts/inotify_fix.sh new file mode 100755 index 00000000000..f326f2717fc --- /dev/null +++ b/scripts/inotify_fix.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# If this is not run on some machines react-native builds fail. + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +source "${GIT_ROOT}/scripts/colors.sh" + +if [[ "$(uname -s)" != Linux ]]; then + echo "inotify fix not applicable on non-linux OS" + exit +fi + +watches=$(cat /proc/sys/fs/inotify/max_user_watches) +required_watches=524288 + +if [ $watches -lt $required_watches ]; then + echo -e "${YLW}fs.inotify.max_user_watches limit is too low ($watches). Increasing it.${RST}" + echo "fs.inotify.max_user_watches = $required_watches" | sudo tee -a /etc/sysctl.conf + sudo sysctl -p +else + echo -e "${GRN}fs.inotify.max_user_watches limit is high enough.${RST}" +fi diff --git a/scripts/lib/setup/installers.sh b/scripts/lib/setup/installers.sh deleted file mode 100755 index a497a6f932f..00000000000 --- a/scripts/lib/setup/installers.sh +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env bash - -_localPropertiesPath=./android/local.properties - -function install_node() { - if nvm_installed; then - install_node_via_nvm - else - install_node_via_package_manager - fi -} - -function install_and_setup_package_manager() { - if is_macos; then - install_homebrew_if_needed - brew_tap "caskroom/cask" - elif is_linux; then - # Linux - buildtools=( - autoconf - automake - build-essential - cmake - curl - g++ - libssl-dev - libtool - make - python-dev - wget - unzip - ) - - for package in "${buildtools[@]}"; do - linux_install "$package" - done - fi -} - -function install_wget() { - if is_macos; then - brew_install wget - fi - // it's installed on ubuntu/debian by default -} - -function needs_java8_linux() { - ! program_exists "java" || !(java -version 2>&1 | grep -q "1.8.0") -} - -function install_java8() { - if is_macos; then - brew_cask_install "caskroom/versions/java8" - elif is_linux; then - if needs_java8_linux; then - sudo su << EOF - add-apt-repository ppa:webupd8team/java -y - apt update - echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections - apt install -y oracle-java8-installer -EOF - else - already_installed "java8" - fi - fi -} - -function install_leiningen() { - if is_macos; then - brew_install leiningen - elif is_linux; then - install_leiningen_linux - fi -} - -function install_leiningen_linux() { - local destination=/usr/bin/lein - - if ! program_exists "lein"; then - cecho "@b@blue[[+ Installing lein...]]" - - sudo su << EOF - curl --silent \ - https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein \ - -o $destination - - chmod 755 $destination - - cd $HOME && lein upgrade -EOF - else - already_installed "lein" - fi -} - -function install_clojure_cli() { - if is_macos; then - brew_install clojure - elif is_linux; then - install_clojure_cli_linux - fi -} - -function install_clojure_cli_linux() { - if ! program_exists "lein"; then - cecho "@b@blue[[+ Installing Clojure CLI...]]" - - local current_dir=$(pwd) - sudo su << EOF - curl --silent \ - https://download.clojure.org/install/linux-install-1.9.0.381.sh \ - -o /tmp/clojure - - chmod +x /tmp/clojure - cd /tmp - ./clojure -EOF - cd "$current_dir" - else - already_installed "Clojure CLI" - fi -} - -function install_watchman() { - if is_macos; then - brew_install watchman - elif is_linux; then - if ! program_exists "watchman"; then - local current_dir=$(pwd) - local clone_path="/tmp/watchman" - - git clone https://github.com/facebook/watchman.git $clone_path - cd $clone_path - git checkout v4.9.0 - ./autogen.sh && \ - ./configure && \ - make && \ - sudo make install - - cd "$current_dir" - else - already_installed "watchman" - fi - fi -} - -function install_homebrew_if_needed() { - ! is_macos && return 1 - - if test ! $(which brew); then - cecho "@b@blue[[+ Installing homebrew]]" - - ruby -e "$(curl -fsSL \ - https://raw.githubusercontent.com/Homebrew/install/master/install)" - - brew update - else - already_installed "Homebrew" - fi -} - -function install_android_sdk() { - if is_macos; then - brew_cask_install android-sdk - elif is_linux; then - install_android_sdk_linux - fi - - use_android_sdk -} - -function install_android_sdk_linux() { - cecho "@b@yellow[[+ Skipping Android SDK setup, not implemented on Linux]]" - - return 1 -} - -function install_maven() { - brew_install maven - linux_install maven -} - -function install_react_native_cli() { - cd "$(repo_path)" - - local npm_command="npm" - - if is_linux && ! nvm_installed; then - # aptitude version of node requires sudo for global install - npm_command="sudo npm" - fi - - if npm list -g | grep -q react-native-cli; then - already_installed "react-native-cli" - else - $npm_command install -g react-native-cli - fi -} - -function install_node_via_nvm() { - local nvmrc="$(repo_path)/.nvmrc" - - cd "$(repo_path)" - - if [ ! -e "$nvmrc" ]; then - cecho "@b@blue[[+ Installing Node 9 (Node 10 is not supported by Realm)]]" - - nvm install 9 - nvm alias status-im 9 - echo status-im > "$nvmrc" - - nvm use status-im - else - local version_alias=$(cat "$nvmrc") - nvm use $version_alias - - local version=$(node -v) - cecho "+ Node already installed ($version_alias $version via NVM)... skipping." - fi -} - -function install_node_via_package_manager() { - if ! program_exists "node"; then - if is_macos; then - brew_install node - elif is_linux; then - curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash - - linux_update - - linux_install nodejs - fi - else - cecho \ - "+ Node already installed ($(node -v) via package manager)... skipping." - fi -} - -function required_pod_version() { - cat "$(repo_path)/ios/Podfile.lock" | grep "COCOAPODS: " | awk '{ print $2 }' -} - -function correct_pod_version_is_installed() { - ! program_exists "pod" && return 1 - - [[ "$(required_pod_version)" == "$(pod --version)" ]] -} - -function using_rvm() { - program_exists "rvm" -} - -function initialize_rvm() { - cd "$(repo_path)" - - if [ ! -e "$(repo_path)/.ruby-version" ]; then - rvm use --default > /dev/null - echo "$(rvm current)" > .ruby-version - fi - - rvm use . >/dev/null -} - -function using_cocoapods() { - is_macos -} - -function install_cocoapods() { - ! using_cocoapods && return 1 - - local gem_command="sudo gem" - local destination="system Ruby" - local version=$(required_pod_version) - - if using_rvm; then - initialize_rvm - - gem_command="gem" - destination="RVM ($(rvm current))" - fi - - if ! program_exists "pod"; then - $gem_command install cocoapods -v "$version" - elif ! correct_pod_version_is_installed; then - cecho "@b@blue[[+ Updating to cocoapods $version]]" - - $gem_command uninstall cocoapods --ignore-dependencies --silent - $gem_command install cocoapods -v "$version" - else - cecho "+ cocoapods already installed to $destination... skipping." - fi -} - -function dependency_setup() { - cecho "@b@blue[[\$ $@]]" - echo - - cd "$(repo_path)" - eval "$@" || (cecho "@b@red[[Error running dependency install]]" && exit 1) - - echo - echo " + done" - echo -} - -function use_android_sdk() { - if [ -n "$ANDROID_SDK_ROOT" ]; then - if ! grep -Fq "sdk.dir" $_localPropertiesPath; then - echo "sdk.dir=$ANDROID_SDK_ROOT" | tee -a $_localPropertiesPath - fi - else - local _docUrl="https://docs.status.im/docs/build_status.html" - cecho "@yellow[[ANDROID_SDK_ROOT environment variable not defined, please install the Android SDK.]]" - cecho "@yellow[[(see $_docUrl).]]" - - echo - - exit 1 - fi -} - -function install_android_ndk() { - if grep -Fq "ndk.dir" $_localPropertiesPath; then - dependency_setup \ - "pushd android && ./gradlew react-native-android:installArchives && popd" - else - local _ndkParentDir=~/Android/Sdk - mkdir -p $_ndkParentDir - cecho "@cyan[[Downloading Android NDK.]]" - wget --output-document=android-ndk.zip https://dl.google.com/android/repository/android-ndk-r10e-linux-x86_64.zip && \ - cecho "@cyan[[Extracting Android NDK to $_ndkParentDir.]]" && \ - unzip -q -o android-ndk.zip -d "$_ndkParentDir" && \ - rm -f android-ndk.zip && \ - _ndkTargetDir="$_ndkParentDir/$(ls $_ndkParentDir | head -n 1)" && \ - echo "ndk.dir=$_ndkTargetDir" | tee -a $_localPropertiesPath && \ - cecho "@blue[[Android NDK installation completed in $_ndkTargetDir.]]" - fi -} \ No newline at end of file diff --git a/scripts/lib/setup/output.sh b/scripts/lib/setup/output.sh deleted file mode 100755 index 96e48d3f286..00000000000 --- a/scripts/lib/setup/output.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash - -function load_color_support() { - # Check if we're in a terminal - if test -t 1; then - local color_count=$(tput colors) - - # Only load colors if our terminal supports them - if test -n "$color_count" && test $color_count -ge 8; then - _color_bold="$(tput bold)" - _color_underline="$(tput sgr 0 1)" - _color_red="$(tput setaf 1)" - _color_green="$(tput setaf 2)" - _color_yellow="$(tput setaf 3)" - _color_blue="$(tput setaf 4)" - _color_magenta="$(tput setaf 5)" - _color_cyan="$(tput setaf 6)" - _color_white="$(tput setaf 7)" - _color_reset="$(tput sgr0)" - fi - fi -} - -load_color_support - -function cecho() { - local colorized=$( - echo "$@" | sed -E \ - -e 's/((@(red|green|yellow|blue|magenta|cyan|white|reset|b|u))+)[[]{2}(.*)[]]{2}/\1\4@reset/g' \ - -e "s/@red/${_color_red}/g" \ - -e "s/@green/${_color_green}/g" \ - -e "s/@yellow/${_color_yellow}/g" \ - -e "s/@blue/${_color_blue}/g" \ - -e "s/@magenta/${_color_magenta}/g" \ - -e "s/@cyan/${_color_cyan}/g" \ - -e "s/@white/${_color_white}/g" \ - -e "s/@reset/${_color_reset}/g" \ - -e "s/@b/${_color_bold}/g" \ - -e "s/@u/${_color_underline}/g" - ) - - echo "$colorized" -} - -function setup_header() { - local header=$1 - - cecho "@b@green[[$header]]" - echo -} - -function already_installed() { - local package=$1 - - cecho "+ $package already installed... skipping" -} - -function setup_complete() { - cecho "@b@blue[[Setup complete!]] -=============== - -There are a few @b[[manual steps]] you might want to do: - -1. Optionally set up Genymotion if you don't want to use Android Virtual Device: - - @blue[[https://www.genymotion.com]] - -2. Setup Android Development Environment + Simulator: - - @blue[[https://facebook.github.io/react-native/docs/getting-started.html]] - -3. Add your SSH public key to Github if it isn't already in there. -" - - echo -} diff --git a/scripts/lib/setup/packages.sh b/scripts/lib/setup/packages.sh deleted file mode 100755 index 896068773cd..00000000000 --- a/scripts/lib/setup/packages.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env bash - -######## -# Install checks -######## - -function program_exists() { - local program=$1 - command -v "$program" >/dev/null 2>&1 -} - - - -######## -# Homebrew -######## - -function brew_install() { - local package=$1 - - if ! is_macos; then - return 1 - fi - - if brew list "$package" > /dev/null 2>&1; then - already_installed "$package" - else - brew install $@ - fi -} - -function brew_cask_install() { - local package=$1 - - if ! is_macos; then - return 1 - fi - - if brew cask list | grep -q "$package"; then - already_installed "$package" - else - brew cask install $@ - fi -} - -function brew_tap() { - local cask=$1 - - if ! is_macos; then - return 1 - fi - - if ! brew tap | grep -q "$cask"; then - brew tap "$cask" - fi -} - -############### -# Linux -############### - -function linux_update() { - ! is_linux && return 1 - - if program_exists "apt"; then - apt_update - elif program_exists "pacman"; then - pacman_update - else - echo "Unsupported Linux distro." - exit 1; - fi -} - -function linux_is_installed() { - ! is_linux && return 1 - - if program_exists "apt"; then - apt_is_installed "$@" - elif program_exists "pacman"; then - pacman_is_installed "$@" - else - echo "Unsupported Linux distro." - exit 1; - fi -} - -# FIXME This command assumes that package names in different package managers (apt, pacman) are same. -# At this moment, it works as expected because we only call it for installing maven and nodejs. -# If this list grows, please consider adding some sort of mapping mechanism. -function linux_install() { - ! is_linux && return 1 - - if program_exists "apt"; then - apt_install "$@" - elif program_exists "pacman"; then - pacman_install "$@" - else - echo "Unsupported Linux distro." - exit 1; - fi -} - - -############### -# Aptitude -############### - -function apt_update() { - sudo apt update -} - -function apt_is_installed() { - local package=$1 - - dpkg -s "$package" >/dev/null 2>&1 -} - -function apt_install() { - local package=$1 - - if apt_is_installed "$package"; then - cecho "+ $package already installed... skipping." - else - sudo apt install -y "$package" - fi -} - -############### -# Pacman -############### - -function pacman_update() { - sudo pacman -Syu -} - -function pacman_is_installed() { - local package=$1 - pacman -Qs $package >/dev/null 2>&1 -} - -function pacman_install() { - local package=$1 - - if pacman_is_installed "$package"; then - cecho "+ $package already installed... skipping." - else - sudo pacman -S --noconfirm "$package" - fi -} - -############### -# RVM -############### - -function load_rvm_if_available() { - [ -f ~/.rvm/scripts/rvm ] && source ~/.rvm/scripts/rvm -} - -############### -# NVM -############### - -function load_nvm_if_available() { - [ -f ~/.nvm/nvm.sh ] && source ~/.nvm/nvm.sh -} - -function nvm_installed() { - program_exists "nvm" -} diff --git a/scripts/lib/setup/path-support.sh b/scripts/lib/setup/path-support.sh deleted file mode 100644 index 32a1c930ec5..00000000000 --- a/scripts/lib/setup/path-support.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -_current_dir=$(cd "${BASH_SOURCE%/*}" && pwd) -_scripts_dir=$(cd "$_current_dir/../.." && pwd) -_repo_dir=$(cd "$_scripts_dir/.." && pwd) - -function scripts_path() { - echo $_scripts_dir -} - -function repo_path() { - echo $_repo_dir -} - -function source_lib() { - local library_path=$1 - - source "$_current_dir/$library_path" -} diff --git a/scripts/lib/setup/platform.sh b/scripts/lib/setup/platform.sh deleted file mode 100755 index 33235d6a15a..00000000000 --- a/scripts/lib/setup/platform.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -OS=$(uname -s) - -function is_macos() { - [[ "$OS" =~ Darwin ]] -} - -function is_linux() { - [[ "$OS" =~ Linux ]] -} - -function exit_unless_os_supported() { - if ! is_macos && ! is_linux; then - cecho "@red[[This install script currently supports Mac OS X and Linux \ -via apt. To manually install, please visit the wiki for more information:]] - - @blue[[https://wiki.status.im/Building_Status]]" - - echo - - exit 1 - fi -} diff --git a/scripts/lib/setup/properties.sh b/scripts/lib/setup/properties.sh deleted file mode 100755 index c9970ee50ef..00000000000 --- a/scripts/lib/setup/properties.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -function property() { - grep "${2}" ${1}|cut -d'=' -f2 -} - -function property_gradle() { - property $(repo_path)/android/gradle.properties ${1} -} \ No newline at end of file diff --git a/scripts/lint/direct-require-component-outside-quo.sh b/scripts/lint/direct-require-component-outside-quo.sh new file mode 100755 index 00000000000..19c52bee712 --- /dev/null +++ b/scripts/lint/direct-require-component-outside-quo.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +# quo components namespace ends with style|utils|types|reaction-resource +# are not the component view, they are usually utils fns or styles + +if rg --pcre2 --glob '!src/quo/**/*' 'quo\.components(\.[\w-]+)*\.[\w-]*(?evt|status-im\.)' --include '*.cljs' --include '*.clj' './src/quo') + +if test -n "$INVALID_CHANGES"; then + echo "WARNING: re-frame, status-im, reagent atoms are not allowed in quo components" + echo '' + echo "$INVALID_CHANGES" + exit 1 +fi + +# Add exception for status-im.config in the utils package check +INVALID_CHANGES2=$(grep -E -r '(status-im\.)' --include '*.cljs' --include '*.clj' './src/utils' | grep -v 'status-im.config') + +if test -n "$INVALID_CHANGES2"; then + echo "WARNING: status-im are not allowed in utils package" + echo '' + echo "$INVALID_CHANGES2" + exit 1 +fi + +INVALID_CHANGES3=$(grep -E -r '(status-im\.)' --include '*.cljs' --include '*.clj' './src/react_native') + +if test -n "$INVALID_CHANGES3"; then + echo "WARNING: status-im are not allowed in react-native package" + echo '' + echo "$INVALID_CHANGES3" + exit 1 +fi diff --git a/scripts/lint/require-i18n-resource-first.sh b/scripts/lint/require-i18n-resource-first.sh new file mode 100755 index 00000000000..af688aad133 --- /dev/null +++ b/scripts/lint/require-i18n-resource-first.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +if rg --quiet --multiline '^\(ns.*\n^\s*\(:require\n(^\s*(;|#_).*\n)*(^\s*\[status-im\.setup\.i18n-resources\W)' src/status_im/core.cljs src/test_helpers/component_tests_preload.cljs; then + exit 0 +elif [ $? -eq 1 ]; then + echo "status-im.setup.i18n-resources must be loaded first (be the first one in ns :require form) in status-im.core" + echo "For more info, check the comment here https://github.com/status-im/status-mobile/pull/17618#discussion_r1361275489" +else + exit $? +fi diff --git a/scripts/lint/trailing-newline.sh b/scripts/lint/trailing-newline.sh new file mode 100755 index 00000000000..97872a643ae --- /dev/null +++ b/scripts/lint/trailing-newline.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -eof pipefail + +FILES=$(comm -23 <(sort <(git ls-files --cached --others --exclude-standard)) <(sort <(git ls-files --deleted)) | grep --ignore-case -E '\.(java|cpp|nix|json|sh|md|js|clj|cljs|cljc|edn|kt|m)$') +N_FILES=$(echo "$FILES" | wc -l) +LINT_SHOULD_FIX=0 + +if [[ -n $1 && $1 != '--fix' ]]; then + echo "Unknown option '$1'" >&2 + exit 1 +elif [[ $1 == '--fix' ]]; then + LINT_SHOULD_FIX=1 +fi + +echo "Checking ${N_FILES} files for missing trailing newlines." + +# Do not process the whole file and only check the last character. Ignore empty +# files. Taken from https://stackoverflow.com/a/10082466. +for file in $FILES; do + if [ -s "$file" ] && [ "$(tail -c1 "$file"; echo x)" != $'\nx' ]; then + if [[ $LINT_SHOULD_FIX -eq 1 ]]; then + echo "" >>"$file" + else + LINT_ERROR=1 + echo "No trailing newline: $file" >&2 + fi + fi +done + +if [[ $LINT_ERROR -eq 1 ]]; then + exit 1 +fi diff --git a/scripts/lint/translations.clj b/scripts/lint/translations.clj new file mode 100755 index 00000000000..3e1866365d8 --- /dev/null +++ b/scripts/lint/translations.clj @@ -0,0 +1,98 @@ +#!/usr/bin/env bb + +(ns lint-translations + (:require [babashka.pods :as pods])) + +(pods/load-pod 'clj-kondo/clj-kondo "2023.09.07") +(require '[pod.borkdude.clj-kondo :as kondo]) +(require '[cheshire.core :as json]) +(require '[clojure.set :as set]) + +(def src-paths ["src"]) + +(def translation-file "translations/en.json") + +;; set the following to true when solving https://github.com/status-im/status-mobile/issues/17811 +(def flag-show-non-namespaced-translation-keys false) +(def flag-show-non-namespaced-translation-keys-occurrences false) ;; this makes the output super verbose! + +;; set the following to true when solving https://github.com/status-im/status-mobile/issues/17813 +;; and keep it permanently on after #17811 and #17813 have both been solved +(def flag-show-unused-translation-keys false) + +(def flag-show-missing-translation-keys true) + +(defn- safe-name + [x] + (when x (name x))) + +(defn- ->keyword + [analysis-keyword] + (keyword (safe-name (:ns analysis-keyword)) (:name analysis-keyword))) + +(defn- report-issues + [incorrect-usages] + (doseq [incorrect-usage incorrect-usages] + (->> incorrect-usage + ((juxt :filename :row :reason ->keyword)) + (apply format "%s:%s %s %s") + println))) + +(defn- extract-translation-keys + [file] + (-> file slurp json/parse-string keys)) + +(def ^:private probably-unused-warning + (format "Probably unused translation key in %s:" translation-file)) + +(defn -main + [& _args] + (println "Linting translations...") + (let [result (kondo/run! + {:lint src-paths + :config {:output {:analysis {:keywords true}}}}) + all-keywords (get-in result [:analysis :keywords]) + used-translations (filter (comp (partial = 't) :ns) all-keywords) + file-translation-keys (apply sorted-set (extract-translation-keys translation-file)) + missing-translations (remove (comp file-translation-keys :name) used-translations) + used-translation-keys (set (map :name used-translations)) + possibly-unused-translation-keys (set/difference file-translation-keys used-translation-keys) + non-namespaced-translations (filter + (fn [kw] + (and (not (:ns kw)) + (possibly-unused-translation-keys (:name kw)))) + all-keywords) + unused-translation-keys (set/difference possibly-unused-translation-keys + (set (map :name non-namespaced-translations)))] + + ;; TODO (2023-11-06 akatov): delete the following once #17811 and #17813 have both been solved + (doseq [k (apply sorted-set (map :name non-namespaced-translations))] + (when flag-show-non-namespaced-translation-keys + (println "Probably non-namespaced key" k)) + (when flag-show-non-namespaced-translation-keys-occurrences + (->> non-namespaced-translations + (filter #(= k (:name %))) + (map #(assoc % :reason "Possibly non-namespaced translation key")) + report-issues))) + + (when flag-show-unused-translation-keys + (run! #(println probably-unused-warning %) unused-translation-keys)) + + (when flag-show-missing-translation-keys + (report-issues (map #(assoc % :reason "Undefined Translation Key") missing-translations))) + + (if (and + (or (not flag-show-missing-translation-keys) + (empty? missing-translations)) + (or (not flag-show-unused-translation-keys) + (empty? possibly-unused-translation-keys)) + (or (not flag-show-non-namespaced-translation-keys) + (not flag-show-non-namespaced-translation-keys-occurrences) + (empty? unused-translation-keys))) + 0 + 1))) + +(when (= *file* (System/getProperty "babashka.file")) + (->> *command-line-args* + (apply -main) + System/exit)) diff --git a/scripts/list-pull-requests.sh b/scripts/list-pull-requests.sh index dfcfa2d2abc..9d8eab524d3 100755 --- a/scripts/list-pull-requests.sh +++ b/scripts/list-pull-requests.sh @@ -3,7 +3,7 @@ # Fetches pull request refs and prints commits # for the given range grouped by pull request -REPOSITORY="status-im/status-react" +REPOSITORY="status-im/status-mobile" REPOSITORY_URL="https://github.com/$REPOSITORY.git" PULL_REQUEST_REMOTE="pullRequestRemote" PULL_REQUEST_URL="https://api.github.com/repos/$REPOSITORY/pulls/" diff --git a/scripts/merge-pr.sh b/scripts/merge-pr.sh index b80795a9c3e..a1a9d27637e 100755 --- a/scripts/merge-pr.sh +++ b/scripts/merge-pr.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eof pipefail @@ -23,7 +23,7 @@ confirm() { load_config() { [[ -f merge-pr.conf ]] && . merge-pr.conf : ${OWNER:=status-im} - : ${REPO:=status-react} + : ${REPO:=status-mobile} : ${REMOTE:=origin} : ${BRANCH:=develop} } @@ -46,6 +46,7 @@ check_sync() { read response; if [ "$response" = "reset" ]; then git fetch $REMOTE + git checkout $BRANCH git reset --hard $REMOTE/$BRANCH else fatal "the local branch $BRANCH is behind $REMOTE/$BRANCH." @@ -103,6 +104,13 @@ check_is_pr_single_commit() { fi } +request_to_squash() { + if [[ $(git rev-list $BRANCH..$PR_LOCAL_BRANCH | wc -l) -gt 1 ]] ;then + confirm "PR has multiple commits, do interactive rebase?" + git rebase -i $BRANCH + fi +} + confirm_pr() { git log -p $BRANCH..$PR_LOCAL_BRANCH confirm "Do you like this PR?" @@ -155,6 +163,7 @@ EOF fetch_pr refresh_base_branch rebase_pr + request_to_squash check_is_pr_single_commit confirm_pr sign_pr diff --git a/scripts/patch-npm-lib.sh b/scripts/patch-npm-lib.sh new file mode 100755 index 00000000000..cc9024703c1 --- /dev/null +++ b/scripts/patch-npm-lib.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) + +read -p "Enter the path of the file to patch: " FILE_PATH + +if [ ! -f "${FILE_PATH}" ]; then + echo "File not found: ${FILE_PATH}" >&2 + exit 1 +fi + +if [[ "${FILE_PATH}" == *"node_modules"* ]]; then + if [[ "${FILE_PATH}" != *"./node_modules"* ]]; then + echo "Please prefix the file path like this './node_modules/'. The path you provided was: ${FILE_PATH}" >&2 + exit 1 + fi +fi + +FILE_NAME=$(basename "${FILE_PATH}") +echo "File to patch: ${FILE_PATH}" + +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "${TEMP_DIR}"' EXIT +echo "Temporary directory created: ${TEMP_DIR}" + +cp "${FILE_PATH}" "${TEMP_DIR}" +echo "Original file copied to temporary directory." + +echo "Please make the necessary changes to the file: ${FILE_PATH}" +echo "Press any key when you are done with the changes..." + +ORIGINAL_MTIME=$(stat -c %Y "${FILE_PATH}") +read -n 1 -s +CURRENT_MTIME=$(stat -c %Y "${FILE_PATH}") + +if [[ "${ORIGINAL_MTIME}" -eq "${CURRENT_MTIME}" ]]; then + echo "Warning: No changes were made to the file. Patch file will not be generated." + exit 0 +fi + +echo "Generating patch file..." +diff -Naur "${TEMP_DIR}/${FILE_NAME}" "${FILE_PATH}" > "${GIT_ROOT}/patches/${FILE_NAME}.patch" + +echo "Patch file created at ${GIT_ROOT}/patches/${FILE_NAME}.patch" +echo "Info: Please execute 'make run-clojure' to test if the patch file works as expected." diff --git a/scripts/pr-statistics.sh b/scripts/pr-statistics.sh index e9465bc49df..3c598b3bbde 100755 --- a/scripts/pr-statistics.sh +++ b/scripts/pr-statistics.sh @@ -6,7 +6,7 @@ # PARAMETERS ---------------------------- # Github repo to scan -GITHUB_REPO="status-im/status-react" +GITHUB_REPO="status-im/status-mobile" # Computational mode # available values : diff --git a/scripts/prepare-for-platform.sh b/scripts/prepare-for-platform.sh deleted file mode 100755 index 3435d52fb05..00000000000 --- a/scripts/prepare-for-platform.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' -PLATFORM="" -PLATFORM_FOLDER="" - -#if no arguments passed, inform user about possible ones - -if [ $# -eq 0 ] - then - echo -e "${GREEN}This script should be invoked with platform argument: 'mobile' or 'desktop'${NC}" - echo "When called it links" - # echo "If invoked with 'mobile' argument it will make a copying: " - # echo "package.json.mobile -> package.json" - # echo "etc.." - exit 1 - else - PLATFORM=$1 - PLATFORM_FOLDER="${PLATFORM}_files" -fi - - -echo "Creating link: package.json -> ${PLATFORM_FOLDER}/package.json " -ln -sf ${PLATFORM_FOLDER}/package.json package.json - -echo "Creating link: package-lock.json -> ${PLATFORM_FOLDER}/package-lock.json" -ln -sf ${PLATFORM_FOLDER}/package-lock.json package-lock.json - -echo "Creating link: VERSION -> ${PLATFORM_FOLDER}/VERSION" -ln -sf ${PLATFORM_FOLDER}/VERSION VERSION - -if [ "${PLATFORM}" == "mobile" ] - then - echo -e "Removing .re-natal symlink from root" - rm -rf .re-natal - else - echo "Creating link: .re-natal -> ${PLATFORM_FOLDER}/.re-natal" - ln -sf ${PLATFORM_FOLDER}/.re-natal .re-natal -fi - - -echo -e "${GREEN}Finished!${NC}" diff --git a/scripts/run-android.sh b/scripts/run-android.sh new file mode 100755 index 00000000000..071ea90c406 --- /dev/null +++ b/scripts/run-android.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +ADB_INSTALL_LOG_FILE="${GIT_ROOT}/logs/adb_install.log" +ADB_SHELL_MONKEY_LOG_FILE="${GIT_ROOT}/logs/adb_shell_monkey.log" + +# Generate android debug build. +export ANDROID_ABI_INCLUDE=$("${GIT_ROOT}/scripts/adb_devices_abis.sh") +export BUILD_ENV=debug +export BUILD_TYPE=debug +"${GIT_ROOT}/scripts/build-android.sh" + +# Install the APK on running emulator or android device. +installAndLaunchApp() { + adb install -r "./result/app-${ANDROID_ABI_INCLUDE}-debug.apk" > "${ADB_INSTALL_LOG_FILE}" 2>&1 + "${GIT_ROOT}/scripts/wait-for-metro-port.sh" 2>&1 + # connected android devices need this port to be exposed for metro + adb reverse "tcp:${RCT_METRO_PORT}" "tcp:${RCT_METRO_PORT}" + adb shell monkey -p im.status.ethereum.debug 1 > "${ADB_SHELL_MONKEY_LOG_FILE}" 2>&1 +} + +showAdbLogs() { + cat "${ADB_INSTALL_LOG_FILE}" >&2; + cat "${ADB_SHELL_MONKEY_LOG_FILE}" >&2; +} + +trap showAdbLogs EXIT ERR INT QUIT +installAndLaunchApp & +exec "${GIT_ROOT}/scripts/run-metro.sh" 2>&1 diff --git a/scripts/run-ios-device.sh b/scripts/run-ios-device.sh new file mode 100755 index 00000000000..11cbd5f2dd2 --- /dev/null +++ b/scripts/run-ios-device.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail +set -m # needed to access jobs + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +XCRUN_DEVICE_INSTALL_LOG_DIR="${GIT_ROOT}/logs/xcrun_device_install.log" +XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR="${GIT_ROOT}/logs/xcrun_device_process_launch.log" +XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR="${GIT_ROOT}/logs/xcrun_device_process_resume.log" + +# Install on the connected device +installAndLaunchApp() { + xcrun devicectl device install app --device "${DEVICE_UUID}" "${APP_PATH}" --json-output "${XCRUN_DEVICE_INSTALL_LOG_DIR}" 2>&1 + + # Extract installationURL + INSTALLATION_URL=$(jq -r '.result.installedApplications[0].installationURL' "${XCRUN_DEVICE_INSTALL_LOG_DIR}") + + # launch the app and put it in background + xcrun devicectl device process launch --no-activate --verbose --device "${DEVICE_UUID}" "${INSTALLATION_URL}" --json-output "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}" + + # Extract background PID of status app + STATUS_PID=$(jq -r '.result.process.processIdentifier' "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}") + "${GIT_ROOT}/scripts/wait-for-metro-port.sh" 2>&1 + + # now that metro is ready, resume the app from background + xcrun devicectl device process resume --device "${DEVICE_UUID}" --pid "${STATUS_PID}" > "${XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR}" 2>&1 +} + +showXcrunLogs() { + cat "${XCRUN_DEVICE_INSTALL_LOG_DIR}" >&2; + cat "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}" >&2; + cat "${XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR}" >&2; +} + +# find the first connected iPhone's UUID +DEVICE_UUID=$(idevice_id -l) + +# Check if any device is connected +if [ -z "${DEVICE_UUID}" ]; then + echo "No connected iPhone device detected." + exit 1 +else + echo "Connected iPhone UDID: ${DEVICE_UUID}" +fi + +BUILD_DIR="${GIT_ROOT}/build" + +#iOS build of debug scheme +xcodebuild -workspace "ios/StatusIm.xcworkspace" -configuration Debug -scheme StatusIm -destination id="${DEVICE_UUID}" -derivedDataPath "${BUILD_DIR}" -verbose | xcbeautify + +APP_PATH="${BUILD_DIR}/Build/Products/Debug-iphoneos/StatusIm.app" + +trap showXcrunLogs EXIT ERR INT QUIT +installAndLaunchApp & +exec "${GIT_ROOT}/scripts/run-metro.sh" 2>&1 + diff --git a/scripts/run-ios.sh b/scripts/run-ios.sh new file mode 100755 index 00000000000..cb38d2d1453 --- /dev/null +++ b/scripts/run-ios.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail +set -m # needed to access jobs + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +XCRUN_INSTALL_LOG_FILE="${GIT_ROOT}/logs/xcrun_install.log" +XCRUN_LAUNCH_LOG_FILE="${GIT_ROOT}/logs/xcrun_launch.log" +XCRUN_SIMULATOR_JSON_FILE="${GIT_ROOT}/logs/ios_simulators_list.log" +XCODEBUILD_LOG_FILE="${GIT_ROOT}/logs/react-native-xcode.log" + +# Install on the simulator +installAndLaunchApp() { + xcrun simctl install "$UDID" "$APP_PATH" > "${XCRUN_INSTALL_LOG_FILE}" 2>&1 + "${GIT_ROOT}/scripts/wait-for-metro-port.sh" 2>&1 + xcrun simctl launch "$UDID" im.status.ethereum.debug > "${XCRUN_LAUNCH_LOG_FILE}" 2>&1 + +} + +showXcrunLogs() { + cat "${XCRUN_INSTALL_LOG_FILE}" >&2; + cat "${XCRUN_LAUNCH_LOG_FILE}" >&2; +} + +# Check if the first argument is provided +if [ -z "${1-}" ]; then + echo "Error: No simulator name provided." >&2 + exit 1 +fi + +# fetch available iOS Simulators +xcrun simctl list devices -j > "${XCRUN_SIMULATOR_JSON_FILE}" + +SIMULATOR=${1} + +# get the first available UDID for Simulators that match the name +read -r UDID SIMULATOR_STATE IS_AVAILABLE < <(jq --raw-output --arg simulator "${SIMULATOR}" ' + [ .devices[] | .[] | select(.name == $simulator) ] | + map(select(.isAvailable)) + map(select(.isAvailable | not)) | + first | + "\(.udid) \(.state) \(.isAvailable)" +' "${XCRUN_SIMULATOR_JSON_FILE}") + +if [ "${IS_AVAILABLE}" == false ] || [ "${UDID}" == null ]; then + echo "Error: Simulator ${SIMULATOR} is not available, Please find and install them." + echo "For help please refer" + echo "https://developer.apple.com/documentation/safari-developer-tools/adding-additional-simulators#Add-and-remove-Simulators " >&2 + exit 1 +fi + +# sometimes a simulator is already running, shut it down to avoid errors +if [ "${SIMULATOR_STATE}" != "Shutdown" ]; then + xcrun simctl shutdown "${UDID}" +fi + +# boot up iOS for simulator +xcrun simctl boot "${UDID}" + +# start the simulator +open -a Simulator --args -CurrentDeviceUDID "${UDID}" + +BUILD_DIR="${GIT_ROOT}/build" + +#iOS build of debug scheme +xcodebuild -workspace "ios/StatusIm.xcworkspace" -configuration Debug -scheme StatusIm -destination id="${UDID}" -derivedDataPath "${BUILD_DIR}" -verbose | xcbeautify | tee -a "${XCODEBUILD_LOG_FILE}" 2>&1 + +APP_PATH="${BUILD_DIR}/Build/Products/Debug-iphonesimulator/StatusIm.app" + +trap showXcrunLogs EXIT ERR INT QUIT +installAndLaunchApp & +exec "${GIT_ROOT}/scripts/run-metro.sh" 2>&1 diff --git a/scripts/run-metro.sh b/scripts/run-metro.sh new file mode 100755 index 00000000000..c5dca5c7855 --- /dev/null +++ b/scripts/run-metro.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if pgrep -f "react-native start --port=${RCT_METRO_PORT:-8081}" > /dev/null; then + echo "Info: metro is already running in another terminal" +else + echo "Info: starting a new metro terminal" + react-native start --port=${RCT_METRO_PORT:-8081} --reset-cache +fi diff --git a/scripts/run-osx b/scripts/run-osx deleted file mode 100755 index 9991858ef67..00000000000 --- a/scripts/run-osx +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/sh - -[ `uname -s` != "Darwin" ] && return - -function tab () { - local cmd="" - local cdto="$PWD" - local args="$@" - - if [ -d "$1" ]; then - cdto=`cd "$1"; pwd` - args="${@:2}" - fi - - if [ -n "$args" ]; then - cmd="; $args" - fi - - iterm_exists=`osascript -e "id of application \"iterm2\""` - if [ ! -z iterm_exists ]; then - osascript &>/dev/null </dev/null </dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@" +identify_git_sha() { + # Do not fail on grep for refs + set +o pipefail + + # ls-remote finds only tags, branches, and pull requests, but can't find commits + STATUS_GO_MATCHING_REFS=$(git ls-remote ${REPO_URL} ${1}) + + # It's possible that there's both a branch and a tag matching the given version + STATUS_GO_TAG_SHA1=$(echo "${STATUS_GO_MATCHING_REFS}" | grep "refs/tags/${1}" | cut -f1) + STATUS_GO_BRANCH_SHA1=$(echo "${STATUS_GO_MATCHING_REFS}" | grep 'refs/heads' | cut -f1) + + # Prefer tag over branch if both are found + if [[ -n "${STATUS_GO_TAG_SHA1}" ]]; then + echo -n "${STATUS_GO_TAG_SHA1}" + elif [[ -n "${STATUS_GO_BRANCH_SHA1}" ]]; then + echo -n "${STATUS_GO_BRANCH_SHA1}" + elif [[ "${#STATUS_GO_VERSION}" -gt 4 ]]; then + echo -n "${1}" + else + echo -e "${RED}${BLD}ERROR:${RST} Input not a tag or branch, but too short to be a SHA1!" >&2 + exit 1 + fi } +HELP_MESSAGE=$(cat <<-END +This is a tool for upgrading status-go to a given version in: +${VERSION_FILE} +Which is then used by Nix derivations to build status-go for the app. +If the given name matches both a branch and a tag the tag is used. + +Usage: + ${SCRIPT_FILE} {version} + +Examples: + + # Using branch name + ${SCRIPT_FILE} feature-abc-xyz + + # Using tag name + ${SCRIPT_FILE} v2.1.1 + + # Using commit SHA1 + ${SCRIPT_FILE} 1a2b3c4d + + # Using PR number + ${SCRIPT_FILE} PR-2134 +END +) + +if [[ ! -x "$(command -v nix-prefetch-url)" ]]; then + echo "No 'nix-prefetch-url' utility found!" >&2 + exit 1 +fi + if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then - usage + echo "${HELP_MESSAGE}" exit 1 fi if [ $# -eq 0 ]; then - echo "Need to provide a status-go version" + echo "Need to provide a status-go version!" + echo + echo "${HELP_MESSAGE}" exit 1 fi -STATUSGO_VERSION=$1 +VERSION_FILE="${GIT_ROOT}/status-go-version.json" +SCRIPT_FILE="$(basename "$0")" + +STATUS_GO_REPO="${STATUS_GO_REPO:=status-go}" +STATUS_GO_OWNER="${STATUS_GO_OWNER:=status-im}" +REPO_URL="https://github.com/${STATUS_GO_OWNER}/${STATUS_GO_REPO}" +STATUS_GO_VERSION=$1 + +# If prefixed with # we assume argument is a PR number +if [[ "${STATUS_GO_VERSION}" = PR-* ]]; then + STATUS_GO_VERSION="refs/pull/${STATUS_GO_VERSION#"PR-"}/head" +fi + +STATUS_GO_COMMIT_SHA1="$(identify_git_sha "${STATUS_GO_VERSION}")" +STATUS_GO_COMMIT_URL="https://github.com/${STATUS_GO_OWNER}/${STATUS_GO_REPO}/commit/${STATUS_GO_COMMIT_SHA1}" +STATUS_GO_ZIP_URL="${REPO_URL}/archive/$(urlencode ${STATUS_GO_VERSION}).zip" +STATUS_GO_SHA256=$(nix-prefetch-url --unpack ${STATUS_GO_ZIP_URL} --name status-go-archive.zip) + +cat << EOF > ${VERSION_FILE} +{ + "_comment": "THIS SHOULD NOT BE EDITED BY HAND.", + "_comment": "Instead use: scripts/update-status-go.sh ", + "owner": "${STATUS_GO_OWNER}", + "repo": "${STATUS_GO_REPO}", + "version": "${STATUS_GO_VERSION}", + "commit-sha1": "${STATUS_GO_COMMIT_SHA1}", + "src-sha256": "${STATUS_GO_SHA256}" +} +EOF -sedi "s/\([[:blank:]]\{28,\}\).*\(<\/version>\)/\1$STATUSGO_VERSION\2/" modules/react-native-status/ios/RCTStatus/pom.xml -sedi "s/\(statusGoVersion = '\).*\('\)/\1$STATUSGO_VERSION\2/" modules/react-native-status/android/build.gradle +echo -e "${GRN}URL${RST}: ${BLD}${STATUS_GO_COMMIT_URL}${RST}" +echo -e "${GRN}SHA-1${RST}: ${BLD}${STATUS_GO_COMMIT_SHA1}${RST}" +echo -e "${GRN}SHA-256${RST}: ${BLD}${STATUS_GO_SHA256}${RST}" diff --git a/scripts/version/build_no.sh b/scripts/version/build_no.sh new file mode 100755 index 00000000000..65a3c49ec0a --- /dev/null +++ b/scripts/version/build_no.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +##################################################################### +# +# This script manages app build numbers. +# It returns the next build number to be used. +# The limit of size of the number is signed int, which is 2147483647. +# +# These numbers are used to mark app artifacts for: +# * Play Store - versionCode attribute (gradle) +# * Apple Store - CFBundleVersion attribute (plutil) +# +# For more details see: +# * https://developer.android.com/studio/publish/versioning +# * https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# +# History: +# +# This script used to tag builds with `build-[0-9]+` tags. +# Since only release builds actually get uploaded to Play or Apple +# stores only then is the uniqueness of numbers checked. +# Because of that we are fine using just hour granurality. +# +##################################################################### + +# Fail on first error +set -e + +DEV_BUILD_NUMBER=9999 + +SCRIPTPATH="$(cd "$(dirname "$0")" ; pwd -P)" +ROOT_PATH="${SCRIPTPATH}/../../" +BUILD_NUMBER_FILE="${ROOT_PATH}/BUILD_NUMBER" + +# If running under Jenkins use a timestamp-based build number. +# That build number is generated by the scripts/version/gen_build_no.sh script. +if [[ -f "${BUILD_NUMBER_FILE}" ]]; then + echo "Build number file found, using: $(cat ${BUILD_NUMBER_FILE})" 1>&2 + cat "${BUILD_NUMBER_FILE}" +else + echo "Build number file missing, using: ${DEV_BUILD_NUMBER}" 1>&2 + echo "${DEV_BUILD_NUMBER}" +fi diff --git a/scripts/version/gen_build_no.sh b/scripts/version/gen_build_no.sh new file mode 100755 index 00000000000..bd32e30d0d0 --- /dev/null +++ b/scripts/version/gen_build_no.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +##################################################################### +# +# Save the timestamp-based build number for builds +# that get uploaded to places like: +# Apple Store, Play Store, or TestFlight +# +# The result of this script is used by scripts/version/build_no.sh +# when being run in Jenkins build context. +# +##################################################################### + +# Fail on first error +set -e + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) +BUILD_NUMBER_FILE="${GIT_ROOT}/BUILD_NUMBER" + +if [[ -f "${BUILD_NUMBER_FILE}" ]]; then + cat "${BUILD_NUMBER_FILE}" +else + # Format: Year(4 digit) + Month + Day + Hour + # Example: 2018120118 + # We limited precision to hours to avoid of mismatched numbers. + date '+%Y%m%d%H%M%S' | tee "${BUILD_NUMBER_FILE}" +fi diff --git a/scripts/wait-for-metro-port.sh b/scripts/wait-for-metro-port.sh new file mode 100755 index 00000000000..ed55f5bf2a9 --- /dev/null +++ b/scripts/wait-for-metro-port.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +TIMEOUT=10 # Metro should not take this long to start. + +while [ "${TIMEOUT}" -gt 0 ]; do + if ! lsof -i:"${RCT_METRO_PORT:-8081}" &> /dev/null; then + echo "." + sleep 1 + ((TIMEOUT--)) + else + break + fi +done diff --git a/scripts/wait-for.sh b/scripts/wait-for.sh new file mode 100755 index 00000000000..6c83f062756 --- /dev/null +++ b/scripts/wait-for.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -e + +TIMEOUT=${1} +shift +PGREP_FILTER=${@} +SLEEP_SEC=5 +STEPS=$((TIMEOUT / SLEEP_SEC)) +# use UID to restrict search +PGREP_OPTS="-U ${UID} -f" + +if [[ -z ${PGREP_OPTS} ]]; then + echo "No pgrep options name specified!" >&2 + exit 1 +fi + +echo "Checking for process: '${PGREP_FILTER}'" +for ((i = 0; i < ${STEPS}; i += 1)); do + if pgrep ${PGREP_OPTS} "${PGREP_FILTER}" > /dev/null; then + echo "Process found. Sleeping ${SLEEP_SEC}..." >&2 + sleep ${SLEEP_SEC} + else + echo "Process '${PGREP_FILTER}' gone." >&2 + exit 0 + fi +done + +echo "Timeout reached! (${TIMEOUT}s) Process still up: ${PGREP_OPTS}" >&2 +echo "Following processes matched:" +# show what is still up +ps u $(pgrep -l ${PGREP_OPTS} "${PGREP_FILTER}" | cut -d' ' -f1 | xargs -I{} echo -n "-p {} ") +exit 1 diff --git a/shadow-cljs.edn b/shadow-cljs.edn new file mode 100644 index 00000000000..1ce7fca9323 --- /dev/null +++ b/shadow-cljs.edn @@ -0,0 +1,267 @@ +;; shadow-cljs configuration +{:source-paths ["src" "test/cljs"] + + :dependencies [[reagent "1.2.0"] + [re-frame "1.4.3"] + [binaryage/oops "0.7.2"] + [com.andrewmcveigh/cljs-time "0.5.2"] + [com.taoensso/timbre "6.3.1"] + [cljs-bean "1.9.0"] + [com.cognitect/transit-cljs "0.8.280"] + [camel-snake-kebab "0.4.3"] + [metosin/malli "0.13.0"] + [funcool/promesa "11.0.678"] + + ;; Dev dependencies + [com.github.jpmonettas/flow-storm-inst "3.7.5"] + [refactor-nrepl "3.9.1"] + [cider/cider-nrepl "0.31.0"] + [cider/piggieback "0.4.1"] + [org.slf4j/slf4j-nop "2.0.9"] + [re-frisk-remote "1.6.0"] + [nubank/matcher-combinators "3.9.1"] + + ;; Use the same version specified in the Nix dependency. + [clj-kondo/clj-kondo "2024.03.13"] + + ;; Routing + [bidi "2.1.6"] + ;; Test dependencies + [day8.re-frame/test "0.1.5"] + [com.taoensso/tufte "2.6.3"]] + + ;; port and middleware for repl in development + :nrepl {:port 7888 + :middleware [cider.piggieback/wrap-cljs-repl + refactor-nrepl.middleware/wrap-refactor]} + + ;; shadow-cljs web interface + :http {:port 3449 + :host "0.0.0.0"} + + :cache-blockers #{legacy.status-im.utils.js-resources legacy.status-im.ui.components.icons.icons} + + :builds + {:mobile + {:target :react-native + ;; To match the folder created by Nix build of JSBundle. + :output-dir "result" + :init-fn status-im.core/init + ;; When false, the Shadow-CLJS watcher won't automatically refresh + ;; the target files (a.k.a hot reload). When false, you can manually + ;; reload by calling `shadow.cljs.devtools.api/watch-compile-all!`. + :devtools {:autobuild #shadow/env ["SHADOW_AUTOBUILD_ENABLED" :default true :as :bool]} + :dev + {:devtools {:before-load-async status-im.setup.hot-reload/before-reload + :after-load-async status-im.setup.hot-reload/reload + :build-notify status-im.setup.hot-reload/build-notify + :preloads [;; The official recommendation is to + ;; load the debugger preload first. + flow-storm.api + dev.re-frisk-preload + status-im.setup.schema-preload + ;; In order to use component test helpers in the REPL we need to + ;; preload namespaces that are not normally required by + ;; production code, such as @testing-library/react-native. + test-helpers.component]} + :closure-defines + {status-im.config/POKT_TOKEN #shadow/env "POKT_TOKEN" + status-im.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN" + status-im.config/STATUS_BUILD_PROXY_USER #shadow/env "STATUS_BUILD_PROXY_USER" + status-im.config/STATUS_BUILD_PROXY_PASSWORD #shadow/env "STATUS_BUILD_PROXY_PASSWORD" + status-im.config/STATUS_MARKET_DATA_PROXY_URL #shadow/env "STATUS_MARKET_DATA_PROXY_URL" + status-im.config/STATUS_MARKET_DATA_PROXY_USER #shadow/env "STATUS_MARKET_DATA_PROXY_USER" + status-im.config/STATUS_MARKET_DATA_PROXY_PASSWORD #shadow/env "STATUS_MARKET_DATA_PROXY_PASSWORD" + status-im.config/STATUS_BACKEND_SERVER_ENABLED #shadow/env "STATUS_BACKEND_SERVER_ENABLED" + status-im.config/STATUS_BACKEND_SERVER_HOST #shadow/env "STATUS_BACKEND_SERVER_HOST" + status-im.config/STATUS_BACKEND_SERVER_MEDIA_SERVER_ENABLE_TLS + #shadow/env "STATUS_BACKEND_SERVER_MEDIA_SERVER_ENABLE_TLS" + status-im.config/STATUS_BACKEND_SERVER_IMAGE_SERVER_URI_PREFIX + #shadow/env "STATUS_BACKEND_SERVER_IMAGE_SERVER_URI_PREFIX" + status-im.config/STATUS_BACKEND_SERVER_ROOT_DATA_DIR #shadow/env + "STATUS_BACKEND_SERVER_ROOT_DATA_DIR" + + ;; Uncomment this line during development if you want to test the integration with Sentry. + ;; + ;; status-im.config/SENTRY_DSN_STATUS_GO #shadow/env "SENTRY_DSN_STATUS_GO" + + status-im.config/MIXPANEL_APP_ID #shadow/env "MIXPANEL_APP_ID" + status-im.config/MIXPANEL_APP_TOKEN #shadow/env "MIXPANEL_APP_TOKEN" + status-im.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY" + status-im.config/RARIBLE_MAINNET_API_KEY #shadow/env "RARIBLE_MAINNET_API_KEY" + status-im.config/RARIBLE_TESTNET_API_KEY #shadow/env "RARIBLE_TESTNET_API_KEY" + status-im.config/ALCHEMY_ETHEREUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ETHEREUM_MAINNET_TOKEN" + status-im.config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ETHEREUM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN" + status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN" + status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_BASE_MAINNET_TOKEN #shadow/env "ALCHEMY_BASE_MAINNET_TOKEN" + status-im.config/ALCHEMY_BASE_SEPOLIA_TOKEN #shadow/env "ALCHEMY_BASE_SEPOLIA_TOKEN" + status-im.config/STATUS_BUILD_ETH_RPC_PROXY_USER #shadow/env "STATUS_BUILD_ETH_RPC_PROXY_USER" + status-im.config/STATUS_BUILD_ETH_RPC_PROXY_PASSWORD #shadow/env + "STATUS_BUILD_ETH_RPC_PROXY_PASSWORD" + status-im.config/STATUS_BUILD_ETH_RPC_PROXY_URL #shadow/env "STATUS_BUILD_ETH_RPC_PROXY_URL"} + :compiler-options {:output-feature-set :es5 + ;; We disable `:fn-deprecated` warnings because we + ;; are managing deprecation via clj-kondo and we + ;; don't want the terminal output to be littered + ;; with warnings on every code reload. + :warnings {:fn-deprecated false} + :closure-defines {re-frame.trace/trace-enabled? true} + :source-map false + ;; This seems to be necessary while using the REPL, + ;; otherwise sometimes you'll get weird errors when + ;; instrumenting functions. + :static-fns false + :infer-externs true + :reader-features #{:mobile + ;; NOTE(@seanstrom): Here we are allowing for an + ;; environment variable to configure the intended store + ;; where a user could download the app. For example, we + ;; support the FDroid and Google Play stores, so we can + ;; configure the app to build for FDroid by setting the + ;; `ANDROID_STORE` environment variable to `fdroid`. + ;; + ;; This value is used for configuring Shadow-CLJS to + ;; only require namespaces that will be compatible with + ;; some restrictions imposed by the Android stores. For + ;; example, FDroid does not allow for Google Play + ;; services like Firebase, so we use the + ;; reader-conditional of `:fdroid` or `:google` to + ;; clearly avoid importing Firebase dependencies in the + ;; FDroid builds. + #shadow/env ["ANDROID_STORE" :as :keyword :default + :google]}} + ;; if you want to use a real device, set your local ip + ;; in the SHADOW_HOST env variable to make sure that + ;; it will use the right interface + :local-ip #shadow/env "SHADOW_HOST"} + :chunks {:fleets legacy.status-im.fleet.default-fleet/default-fleets} + :release + {:closure-defines + {status-im.config/POKT_TOKEN #shadow/env "POKT_TOKEN" + status-im.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN" + status-im.config/STATUS_BUILD_PROXY_USER #shadow/env "STATUS_BUILD_PROXY_USER" + status-im.config/STATUS_BUILD_PROXY_PASSWORD #shadow/env "STATUS_BUILD_PROXY_PASSWORD" + ;; market proxy config values (https://github.com/status-im/market-proxy): + status-im.config/STATUS_MARKET_DATA_PROXY_URL #shadow/env "STATUS_MARKET_DATA_PROXY_URL" + status-im.config/STATUS_MARKET_DATA_PROXY_USER #shadow/env "STATUS_MARKET_DATA_PROXY_USER" + status-im.config/STATUS_MARKET_DATA_PROXY_PASSWORD #shadow/env "STATUS_MARKET_DATA_PROXY_PASSWORD" + status-im.config/SENTRY_DSN_STATUS_GO #shadow/env "SENTRY_DSN_STATUS_GO" + status-im.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY" + status-im.config/MIXPANEL_APP_ID #shadow/env "MIXPANEL_APP_ID" + status-im.config/MIXPANEL_APP_TOKEN #shadow/env "MIXPANEL_APP_TOKEN" + status-im.config/RARIBLE_MAINNET_API_KEY #shadow/env "RARIBLE_MAINNET_API_KEY" + status-im.config/RARIBLE_TESTNET_API_KEY #shadow/env "RARIBLE_TESTNET_API_KEY" + status-im.config/ALCHEMY_ETHEREUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ETHEREUM_MAINNET_TOKEN" + status-im.config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ETHEREUM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN" + status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN" + status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_BASE_MAINNET_TOKEN #shadow/env "ALCHEMY_BASE_MAINNET_TOKEN" + status-im.config/ALCHEMY_BASE_SEPOLIA_TOKEN #shadow/env "ALCHEMY_BASE_SEPOLIA_TOKEN" + status-im.config/STATUS_BUILD_ETH_RPC_PROXY_USER #shadow/env "STATUS_BUILD_ETH_RPC_PROXY_USER" + status-im.config/STATUS_BUILD_ETH_RPC_PROXY_PASSWORD #shadow/env + "STATUS_BUILD_ETH_RPC_PROXY_PASSWORD" + status-im.config/STATUS_BUILD_ETH_RPC_PROXY_URL #shadow/env "STATUS_BUILD_ETH_RPC_PROXY_URL"} + :compiler-options {:output-feature-set :es6 + ;;disable for android build as there + ;;is an intermittent warning with deftype + :warnings-as-errors false + :infer-externs :auto + :static-fns true + :fn-invoke-direct true + :optimizations :advanced + :js-options {:js-provider :closure} + :reader-features #{:mobile + ;; NOTE(@seanstrom): Here we are allowing for an + ;; environment variable to configure the intended store + ;; where a user could download the app. For example, we + ;; support the FDroid and Google Play stores, so we can + ;; configure the app to build for FDroid by setting the + ;; `ANDROID_STORE` environment variable to `fdroid`. + ;; + ;; This value is used for configuring Shadow-CLJS to + ;; only require namespaces that will be compatible with + ;; some restrictions imposed by the Android stores. For + ;; example, FDroid does not allow for Google Play + ;; services like Firebase, so we use the + ;; reader-conditional of `:fdroid` or `:google` to + ;; clearly avoid importing Firebase dependencies in the + ;; FDroid builds. + #shadow/env ["ANDROID_STORE" :as :keyword :default + :google]}}}} + ;; the tests are ran with node, react-native dependencies are mocked + ;; by using node --require override.js, which uses the node-library + ;; produced by the target :mocks below and redefines node require + ;; function to use the mocks instead of the rn libraries + :test + {:output-to #shadow/env "SHADOW_OUTPUT_TO" + :output-dir "target/test" + :optimizations :simple + :target :node-test + :dev {:devtools {:preloads [status-im.setup.schema-preload + status-im.setup.test-preload]}} + ;; Uncomment line below to `make test-watch` a specific file + :ns-regexp #shadow/env "SHADOW_NS_REGEXP" + :main legacy.status-im.test-runner/main + ;; set :ui-driven to true to let shadow-cljs inject node-repl + :ui-driven true + :closure-defines + {schema.core/throw-on-error? true + status-im.config/POKT_TOKEN #shadow/env "POKT_TOKEN" + status-im.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN" + status-im.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY" + status-im.config/MIXPANEL_APP_ID #shadow/env "MIXPANEL_APP_ID" + status-im.config/MIXPANEL_APP_TOKEN #shadow/env "MIXPANEL_APP_TOKEN" + status-im.config/RARIBLE_MAINNET_API_KEY #shadow/env "RARIBLE_MAINNET_API_KEY" + status-im.config/RARIBLE_TESTNET_API_KEY #shadow/env "RARIBLE_TESTNET_API_KEY" + status-im.config/ALCHEMY_ETHEREUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ETHEREUM_MAINNET_TOKEN" + status-im.config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ETHEREUM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN" + status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN" + status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN" + status-im.config/ALCHEMY_BASE_MAINNET_TOKEN #shadow/env "ALCHEMY_BASE_MAINNET_TOKEN" + status-im.config/ALCHEMY_BASE_SEPOLIA_TOKEN #shadow/env "ALCHEMY_BASE_SEPOLIA_TOKEN" + status-im.config/WALLET_CONNECT_PROJECT_ID #shadow/env "WALLET_CONNECT_PROJECT_ID"} + :compiler-options + {;; needed because we override require and it + ;; messes with source-map which reports callstack + ;; exceeded exceptions instead of real issues + :source-map false + ;; needed because we use deref in tests + :static-fns false + :optimizations :simple + :warnings {:fn-deprecated false} + :infer-externs true + :reader-features #{:mobile}}} + + ;; mock.js-dependencies is mocking the react-native libraries + ;; we build it as a node library so that it can be required by + ;; override.js + :mocks + {:target :node-library + :exports {:mocks mocks.js-dependencies/mock} + :output-to "target/mocks/mocks.js" + :output-dir "target/mocks" + :compiler-options {:optimizations :simple + :source-map false}} + :component-test {:target :npm-module + :entries [;; We need to tell shadow-cljs to compile + ;; the preloads namespaces because they + ;; will be used directly by Jest in the + ;; option setupFilesAfterEnv. + test-helpers.component-tests-preload + status-im.setup.schema-preload + quo.core-spec + status-im.core-spec] + :ns-regexp "component-spec$" + :output-dir "component-spec" + :closure-defines {schema.core/throw-on-error? true} + :compiler-options {:warnings-as-errors false + :warnings {:fn-deprecated false} + :static-fns false + :infer-externs true}}}} diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000000..fb0162b1308 --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +# for passing build options, see nix/README.md +{ config ? { } }: + +let + main = import ./nix { inherit config; }; +in + # use the default shell when calling nix-shell without arguments + main.shells.default diff --git a/src/dev/README.md b/src/dev/README.md new file mode 100644 index 00000000000..884aa18614d --- /dev/null +++ b/src/dev/README.md @@ -0,0 +1,23 @@ +# The `dev/` directory + +The `src/dev/` directory is a place where developers can commit non-production +and non-test code which can facilitate development in general. The code should, +ideally, be useful to more than one team member. + +**Note**: As of today (2024-05-07), we don't have further guidelines or examples +about how mobile teams should use and maintain these dev-only namespaces. Before +opening a PR introducing new dev-only namespaces, consider sharing and +discussing with as many team members as you can. + +### The `user` namespace + +You may optionally create a `src/dev/user.cljs` and/or `src/user.cljs` file. +These files are git-ignored and are meant to be your own playground. Clojure +developers usually use such namespaces to create temporary utilities and +experiments that aren't necessarily useful to others. They are particularly +valuable for developers adept at the REPL-Driven Development workflow. + +Be careful with `make clean` because it will delete all non-tracked git files. +If you rely on this file, you may prefer to create a symbolic link and store the +original `user.cljs` file outside the `status-mobile` repository. Then, after +`make clean`, you will need to recreate the symlink. diff --git a/src/dev/re_frisk_preload.cljs b/src/dev/re_frisk_preload.cljs new file mode 100644 index 00000000000..f4f98eab82a --- /dev/null +++ b/src/dev/re_frisk_preload.cljs @@ -0,0 +1,5 @@ +(ns dev.re-frisk-preload + (:require [re-frisk-remote.core :as re-frisk-remote] + [status-im.config])) + +(re-frisk-remote/enable {:host (str status-im.config/re-frisk-host ":" status-im.config/re-frisk-port)}) diff --git a/src/js/worklets/bottom_sheet.js b/src/js/worklets/bottom_sheet.js new file mode 100644 index 00000000000..074b6a91cad --- /dev/null +++ b/src/js/worklets/bottom_sheet.js @@ -0,0 +1,15 @@ +import { useDerivedValue, runOnJS as reaRunOnJS, runOnJS } from 'react-native-reanimated'; + +export function useTranslateY(initialTranslationY, bottomSheetDy, panY) { + return useDerivedValue(() => { + return initialTranslationY - (bottomSheetDy.value - panY.value); + }); +} + +export function useBackgroundOpacity(translateY, backgroundHeight, windowHeight, opacity) { + return useDerivedValue(() => { + const calculatedOpacity = ((translateY.value - windowHeight) / -backgroundHeight) * opacity; + + return Math.max(Math.min(calculatedOpacity, opacity), 0); + }); +} diff --git a/src/js/worklets/chat/messenger/composer.js b/src/js/worklets/chat/messenger/composer.js new file mode 100644 index 00000000000..6ced4607121 --- /dev/null +++ b/src/js/worklets/chat/messenger/composer.js @@ -0,0 +1,12 @@ +import { useDerivedValue, withTiming, withDelay } from 'react-native-reanimated'; + +export function scrollDownButtonOpacity(chatListScrollY, isComposerFocused, windowHeight) { + return useDerivedValue(function () { + 'worklet'; + if (isComposerFocused.value) { + return 0; + } else { + return chatListScrollY.value > windowHeight * 0.75 ? 1 : 0; + } + }); +} diff --git a/src/js/worklets/chat/messenger/lightbox.js b/src/js/worklets/chat/messenger/lightbox.js new file mode 100644 index 00000000000..101eaa62b47 --- /dev/null +++ b/src/js/worklets/chat/messenger/lightbox.js @@ -0,0 +1,8 @@ +import { useDerivedValue } from 'react-native-reanimated'; + +export function infoLayout(input, isTop) { + return useDerivedValue(function () { + 'worklet'; + return isTop ? input.value : -input.value; + }); +} diff --git a/src/js/worklets/chat/messenger/messages.js b/src/js/worklets/chat/messenger/messages.js new file mode 100644 index 00000000000..e6c6215ab5f --- /dev/null +++ b/src/js/worklets/chat/messenger/messages.js @@ -0,0 +1,33 @@ +import { useAnimatedReaction, withTiming, runOnJS } from 'react-native-reanimated'; +import { useState } from 'react'; + +export function messagesListOnScroll(distanceFromListTop, chatListScrollY, callback) { + return function (event) { + 'worklet'; + const currentY = event.contentOffset.y; + const layoutHeight = event.layoutMeasurement.height; + const contentSizeY = event.contentSize.height - layoutHeight; + const newDistance = contentSizeY - currentY; + distanceFromListTop.value = newDistance; + chatListScrollY.value = currentY; + runOnJS(callback)(layoutHeight, newDistance); + }; +} + +export function useMessagesScrolledToThreshold(distanceFromListTop, threshold) { + const [scrolledToThreshold, setScrolledToThreshold] = useState(false); + + useAnimatedReaction( + function () { + return distanceFromListTop.value <= threshold; + }, + function (current) { + if (current !== scrolledToThreshold) { + runOnJS(setScrolledToThreshold)(current); + } + }, + [scrolledToThreshold], + ); + + return scrolledToThreshold; +} diff --git a/src/js/worklets/communities.js b/src/js/worklets/communities.js new file mode 100644 index 00000000000..d158c753d32 --- /dev/null +++ b/src/js/worklets/communities.js @@ -0,0 +1,295 @@ +import { + useDerivedValue, + useAnimatedStyle, + interpolate, + scrollTo, + withTiming, + cancelAnimation, + withDecay, +} from 'react-native-reanimated'; + +import { Platform } from 'react-native'; + +export function useLogoStyles({ + scrollAmount, + expandHeaderThreshold, + sheetDisplacementThreshold, + textMovementThreshold, +}) { + return useAnimatedStyle(() => { + const firstDisplacement = scrollAmount.value < expandHeaderThreshold; + if (firstDisplacement) { + return { + transform: [ + { translateX: 20 }, + { translateY: interpolate(scrollAmount.value, [0, expandHeaderThreshold], [0, -42.5], 'clamp') }, + { scale: interpolate(scrollAmount.value, [0, textMovementThreshold], [1, 0.4], 'clamp') }, + ], + }; + } else { + return { + transform: [ + { translateX: 20 }, + { + translateY: interpolate( + scrollAmount.value, + [expandHeaderThreshold, sheetDisplacementThreshold], + [-42.5, -50.5], + 'clamp', + ), + }, + { scale: 0.4 }, + ], + }; + } + }); +} + +export function useSheetStyles({ scrollAmount, expandHeaderThreshold, sheetDisplacementThreshold }) { + return useAnimatedStyle(() => { + const firstDisplacement = scrollAmount.value < expandHeaderThreshold; + if (firstDisplacement) { + return { + transform: [{ translateY: interpolate(scrollAmount.value, [0, expandHeaderThreshold], [40, 0], 'clamp') }], + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + }; + } else { + const radius = interpolate( + scrollAmount.value, + [expandHeaderThreshold, sheetDisplacementThreshold], + [20, 0], + 'clamp', + ); + return { + transform: [ + { + translateY: interpolate( + scrollAmount.value, + [expandHeaderThreshold, sheetDisplacementThreshold], + [0, -8], + 'clamp', + ), + }, + ], + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + }; + } + }); +} + +export function useNameStyles({ scrollAmount, expandHeaderThreshold, textMovementThreshold }) { + return useAnimatedStyle(() => { + const animationProgress = interpolate( + scrollAmount.value, + [textMovementThreshold, expandHeaderThreshold], + [0, 40], + 'clamp', + ); + return { + marginRight: animationProgress, + transform: [ + { translateX: animationProgress }, + { + translateY: interpolate( + scrollAmount.value, + [textMovementThreshold, expandHeaderThreshold], + [0, -44.5], + 'clamp', + ), + }, + ], + }; + }); +} + +export function useInfoStyles({ scrollAmount, infoOpacityThreshold }) { + return useAnimatedStyle(() => { + return { + opacity: interpolate(scrollAmount.value, [0, infoOpacityThreshold], [1, 0.2], 'extend'), + }; + }); +} + +export function useChannelsStyles({ + scrollAmount, + headerHeight, + expandHeaderThreshold, + sheetDisplacementThreshold, + expandHeaderLimit, +}) { + return useAnimatedStyle(() => { + const headerDisplacement = (headerHeight.value - 55.5) * -1; + const firstDisplacement = scrollAmount.value < expandHeaderThreshold; + const secondDisplacement = scrollAmount.value > sheetDisplacementThreshold; + if (firstDisplacement) { + return { + transform: [ + { + translateY: interpolate(scrollAmount.value, [0, expandHeaderThreshold], [39, headerDisplacement], 'clamp'), + }, + ], + }; + } else if (secondDisplacement) { + return { + transform: [ + { + translateY: interpolate( + scrollAmount.value, + [sheetDisplacementThreshold, expandHeaderLimit], + [headerDisplacement - 8, headerDisplacement - 64], + 'clamp', + ), + }, + ], + }; + } else { + return { + transform: [ + { + translateY: interpolate( + scrollAmount.value, + [expandHeaderThreshold, sheetDisplacementThreshold], + [headerDisplacement, headerDisplacement - 8], + 'clamp', + ), + }, + ], + }; + } + }, [headerHeight.value]); +} + +export function useScrollTo({ animatedRef, scrollAmount, expandHeaderLimit }) { + const isAndroid = Platform.OS === 'android'; + return useDerivedValue(() => { + scrollTo(animatedRef, 0, scrollAmount.value - expandHeaderLimit, isAndroid); + }); +} + +export function useHeaderOpacity({ scrollAmount, expandHeaderThreshold, sheetDisplacementThreshold }) { + return useDerivedValue(() => { + return interpolate(scrollAmount.value, [expandHeaderThreshold, sheetDisplacementThreshold], [0, 1], 'clamp'); + }); +} + +export function useOppositeHeaderOpacity(headerOpacity) { + return useDerivedValue(() => { + return 1 - headerOpacity.value; + }); +} + +export function useNavContentOpacity({ scrollAmount, sheetDisplacementThreshold, expandHeaderLimit }) { + return useDerivedValue(() => { + return interpolate(scrollAmount.value, [sheetDisplacementThreshold, expandHeaderLimit], [0, 1], 'clamp'); + }); +} + +export function onScrollAnimationEnd( + scrollAmount, + scrollStart, + expandHeaderThreshold, + snapHeaderThreshold, + expandHeaderLimit, + animationDuration, +) { + 'worklet'; + return function () { + 'worklet'; + const duration = { duration: animationDuration }; + if (scrollAmount.value > snapHeaderThreshold && scrollAmount.value <= expandHeaderThreshold) { + scrollStart.value = withTiming(-expandHeaderThreshold, duration); + scrollAmount.value = withTiming(expandHeaderThreshold, duration); + } + + if (scrollAmount.value > expandHeaderThreshold) { + scrollStart.value = -scrollAmount.value; + } + + if (scrollAmount.value <= snapHeaderThreshold) { + scrollStart.value = withTiming(0, duration); + scrollAmount.value = withTiming(0, duration); + } + + if (scrollAmount.value > expandHeaderThreshold && scrollAmount.value < expandHeaderLimit) { + if (scrollAmount.value >= (expandHeaderLimit - expandHeaderThreshold) * 0.65 + expandHeaderThreshold) { + scrollAmount.value = withTiming(expandHeaderLimit, duration); + scrollStart.value = withTiming(-expandHeaderLimit, duration); + } else { + scrollAmount.value = withTiming(expandHeaderThreshold, duration); + scrollStart.value = withTiming(-expandHeaderThreshold, duration); + } + } + }; +} + +export function onPanStart(scrollStart, scrollAmount) { + return function () { + 'worklet'; + cancelAnimation(scrollStart); + cancelAnimation(scrollAmount); + scrollStart.value = -1 * scrollAmount.value; + }; +} + +export function onPanUpdate({ scrollStart, scrollAmount, maxScroll, expandHeaderLimit }) { + return function (event) { + 'worklet'; + const newScrollAmount = -1 * (event.translationY + scrollStart.value); + if (newScrollAmount <= 0) { + scrollAmount.value = 0; + } else { + const limit = expandHeaderLimit + maxScroll.value; + scrollAmount.value = newScrollAmount <= limit ? newScrollAmount : limit; + } + }; +} + +export function onPanEnd({ + scrollStart, + scrollAmount, + maxScroll, + expandHeaderLimit, + expandHeaderThreshold, + snapHeaderThreshold, + animationDuration, +}) { + const isIOS = Platform.OS === 'ios'; + return function (event) { + 'worklet'; + scrollStart.value = -scrollAmount.value; + const endAnimation = onScrollAnimationEnd( + scrollAmount, + scrollStart, + expandHeaderThreshold, + snapHeaderThreshold, + expandHeaderLimit, + animationDuration, + ); + if (scrollAmount.value < expandHeaderLimit) { + endAnimation(); + } else { + const maxValue = maxScroll.value + expandHeaderLimit; + const decelerationRate = isIOS ? { deceleration: 0.998 } : { deceleration: 0.996 }; + + scrollStart.value = withDecay({ + ...{ + velocity: event.velocityY, + clamp: [-1 * maxValue, 0], + }, + ...decelerationRate, + }); + scrollAmount.value = withDecay( + { + ...{ + velocity: -1 * event.velocityY, + clamp: [0, maxValue], + }, + ...decelerationRate, + }, + endAnimation, + ); + } + }; +} diff --git a/src/js/worklets/core.js b/src/js/worklets/core.js new file mode 100644 index 00000000000..b0083781c92 --- /dev/null +++ b/src/js/worklets/core.js @@ -0,0 +1,70 @@ +import { useDerivedValue, interpolate } from 'react-native-reanimated'; + +// Generic Worklets + +// 1. kebab-case styles are not working for worklets +// so we have to convert kebab case styles into camel case styles +// 2. remove keys with nil value, else useAnimatedStyle will throw an error +// https://github.com/status-im/status-mobile/issues/14756 +export function applyAnimationsToStyle(animations, style) { + return function () { + 'worklet'; + + var animatedStyle = {}; + + // Normal Style + for (var key in style) { + if (key == 'transform') { + var transforms = style[key]; + var filteredTransforms = []; + + for (var transform of transforms) { + var transformKey = Object.keys(transform)[0]; + var transformValue = transform[transformKey]; + if (transformValue !== null) { + filteredTransforms.push({ [transformKey.replace(/-./g, (x) => x[1].toUpperCase())]: transformValue }); + } + } + + animatedStyle[key] = filteredTransforms; + } else { + var value = style[key]; + if (value !== null) { + animatedStyle[key.replace(/-./g, (x) => x[1].toUpperCase())] = value; + } + } + } + + // Animations + for (var key in animations) { + if (key == 'transform') { + var transforms = animations[key]; + var animatedTransforms = []; + + for (var transform of transforms) { + var transformKey = Object.keys(transform)[0]; + var transformValue = transform[transformKey].value; + if (transformValue !== null) { + animatedTransforms.push({ [transformKey.replace(/-./g, (x) => x[1].toUpperCase())]: transformValue }); + } + } + + animatedStyle[key] = animatedTransforms; + } else { + var animatedValue = animations[key].value; + if (animatedValue !== null) { + animatedStyle[key.replace(/-./g, (x) => x[1].toUpperCase())] = animatedValue; + } + } + } + + return animatedStyle; + }; +} + +export function interpolateValue(sharedValue, inputRange, outputRange, extrapolation) { + return useDerivedValue(function () { + 'worklet'; + return interpolate(sharedValue.value, inputRange, outputRange, extrapolation); + }); +} diff --git a/src/js/worklets/header_animations.js b/src/js/worklets/header_animations.js new file mode 100644 index 00000000000..83a0e14d0d4 --- /dev/null +++ b/src/js/worklets/header_animations.js @@ -0,0 +1,25 @@ +import { + useDerivedValue, + interpolate, + interpolateColor, + Extrapolation, + useAnimatedStyle, +} from 'react-native-reanimated'; + +const CLAMP_MIN = 0; +const CLAMP_MAX = 60; +const BLUR_MIN = 1; +const BLUR_MAX = 15; + +export const useBlurAmount = (sharedValue) => + useDerivedValue(() => + parseInt(interpolate(sharedValue.value, [CLAMP_MIN, CLAMP_MAX], [BLUR_MIN, BLUR_MAX], Extrapolation.CLAMP)), + ); + +export function useLayerOpacity(sharedValue, from, to) { + return useAnimatedStyle(() => ({ + flex: 1, + justifyContent: 'flex-end', + backgroundColor: interpolateColor(sharedValue.value, [0, 60], [from, to], 'RGB'), + })); +} diff --git a/src/js/worklets/identifiers_highlighting.js b/src/js/worklets/identifiers_highlighting.js new file mode 100644 index 00000000000..2098d2b5ddf --- /dev/null +++ b/src/js/worklets/identifiers_highlighting.js @@ -0,0 +1,92 @@ +import { useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; + +export function background(color, progress) { + return useDerivedValue(function () { + 'worklet'; + const r = parseInt(color.substring(1, 3), 16); + const g = parseInt(color.substring(3, 5), 16); + const b = parseInt(color.substring(5, 7), 16); + + return progress.value <= 25 ? `rgba(${r}, ${g}, ${b}, .4)` : `rgba(${r}, ${g}, ${b}, .2)`; + }); +} + +export function opacity(progress) { + return useDerivedValue(function () { + 'worklet'; + return progress.value <= 25 ? 1 : 0.3; + }); +} + +export function avatarOpacity(progress) { + return useDerivedValue(function () { + 'worklet'; + const progressValue = progress.value; + + switch (true) { + case progressValue <= 25: + return 1; + case progressValue <= 50: + return 0.1; + default: + return 0.3; + } + }); +} + +export function ringOpacity(progress) { + return useDerivedValue(function () { + 'worklet'; + return progress.value <= 50 ? 1 : 0.3; + }); +} + +export function userHashOpacity(progress) { + return useDerivedValue(function () { + 'worklet'; + const progressValue = progress.value; + switch (true) { + case progressValue <= 25: + return 1; + case progressValue <= 50: + return 0.3; + case progressValue <= 75: + return 1; + default: + return 0.3; + } + }); +} + +export function userHashColor(progress) { + return useDerivedValue(function () { + 'worklet'; + const progressValue = progress.value; + switch (true) { + case progressValue <= 25: + return 'rgba(255, 255, 255, .6)'; + case progressValue <= 50: + return 'rgba(255, 255, 255, .6)'; + case progressValue <= 75: + return 'rgb(255, 255, 255)'; + default: + return 'rgba(255, 255, 255, .6)'; + } + }); +} + +export function emojiHashStyle(progress) { + return useAnimatedStyle(function () { + 'worklet'; + const progressValue = progress.value; + + switch (true) { + case progressValue <= 25: + return { opacity: 1 }; + case progressValue <= 75: + return { opacity: 0.3 }; + default: + return { opacity: 1 }; + } + }); +} diff --git a/src/js/worklets/onboarding_carousel.js b/src/js/worklets/onboarding_carousel.js new file mode 100644 index 00000000000..84647d0a5d1 --- /dev/null +++ b/src/js/worklets/onboarding_carousel.js @@ -0,0 +1,31 @@ +import { useDerivedValue, withTiming, Easing } from 'react-native-reanimated'; + +const slideAnimationDuration = 300; +const totalPages = 3; +const pageSize = 25; + +const easeOut = { + duration: slideAnimationDuration, + easing: Easing.bezier(0, 0, 0.58, 1), +}; + +// Derived Values +export function dynamicProgressBarWidth(staticProgressBarWidth, progress) { + return useDerivedValue(function () { + 'worklet'; + return (staticProgressBarWidth * (progress.value || 0)) / 75; + }); +} + +export function carouselLeftPosition(windowWidth, progress, isDragging, dragAmount) { + return useDerivedValue(function () { + 'worklet'; + const progressValue = progress.value; + const dragAmountValue = dragAmount.value; + + const baseOffset = (Math.floor(progressValue / pageSize) % totalPages) * windowWidth; + const adjustedOffset = baseOffset === 0 && dragAmountValue > 0 ? baseOffset : -baseOffset + dragAmountValue; + + return isDragging.value ? adjustedOffset : withTiming(-baseOffset, easeOut); + }); +} diff --git a/src/js/worklets/profile_header.js b/src/js/worklets/profile_header.js new file mode 100644 index 00000000000..f26ee276906 --- /dev/null +++ b/src/js/worklets/profile_header.js @@ -0,0 +1,14 @@ +import { useAnimatedStyle, withTiming } from 'react-native-reanimated'; + +export function profileHeaderAnimation(scrollY, threshold, topBarHeight) { + return useAnimatedStyle(() => { + 'worklet'; + const opacity = scrollY.value < threshold ? withTiming(0) : withTiming(1); + const translateY = scrollY.value < threshold ? withTiming(topBarHeight) : withTiming(0); + + return { + opacity: opacity, + transform: [{ translateY: translateY }], + }; + }); +} diff --git a/src/js/worklets/record_audio.js b/src/js/worklets/record_audio.js new file mode 100644 index 00000000000..9527ddccdc8 --- /dev/null +++ b/src/js/worklets/record_audio.js @@ -0,0 +1,16 @@ +import { useDerivedValue } from 'react-native-reanimated'; + +const MAX_SCALE = 1.8; + +export function ringScale(scale, subtract) { + return useDerivedValue(function () { + 'worklet'; + const value = scale.value; + const maxDelta = MAX_SCALE - 1; + const maxDeltaDiff = 1 - maxDelta; + const maxVirtualScale = MAX_SCALE + maxDelta; + const decimals = value - Math.floor(value); + const normalizedValue = value >= maxVirtualScale ? decimals + (parseInt(value) - 1) * maxDeltaDiff + 1 : value; + return (normalizedValue - subtract > MAX_SCALE ? normalizedValue - maxDelta : normalizedValue) - subtract; + }); +} diff --git a/src/js/worklets/scroll_view.js b/src/js/worklets/scroll_view.js new file mode 100644 index 00000000000..91fdccd6f5e --- /dev/null +++ b/src/js/worklets/scroll_view.js @@ -0,0 +1,7 @@ +import { useAnimatedScrollHandler } from 'react-native-reanimated'; + +export function useAnimatedScrollHandlerWorklet(scrollY) { + return useAnimatedScrollHandler((event) => { + scrollY.value = event.contentOffset.y; + }); +} diff --git a/src/js/worklets/shell/bottom_tabs.js b/src/js/worklets/shell/bottom_tabs.js new file mode 100644 index 00000000000..83db1c64df2 --- /dev/null +++ b/src/js/worklets/shell/bottom_tabs.js @@ -0,0 +1,12 @@ +import { useDerivedValue, withTiming } from 'react-native-reanimated'; + +export function bottomTabIconColor(stackId, selectedStackId, selectedTabColor, defaultColor) { + return useDerivedValue(function () { + 'worklet'; + if (selectedStackId.value == stackId) { + return selectedTabColor; + } else { + return defaultColor; + } + }); +} diff --git a/src/js/worklets/shell/home_stack.js b/src/js/worklets/shell/home_stack.js new file mode 100644 index 00000000000..97874cee6f7 --- /dev/null +++ b/src/js/worklets/shell/home_stack.js @@ -0,0 +1,16 @@ +import { useDerivedValue, withTiming, withSequence, withDelay } from 'react-native-reanimated'; + +// Derived values for each stack (communities, chat, wallet, browser) +export function stackOpacity(stackId, selectedStackId) { + return useDerivedValue(function () { + 'worklet'; + return selectedStackId.value == stackId ? 1 : 0; + }); +} + +export function stackZIndex(stackId, selectedStackId) { + return useDerivedValue(function () { + 'worklet'; + return selectedStackId.value == stackId ? 10 : 9; + }); +} diff --git a/src/keycard/keycard.cljs b/src/keycard/keycard.cljs new file mode 100644 index 00000000000..490dd16b2cb --- /dev/null +++ b/src/keycard/keycard.cljs @@ -0,0 +1,290 @@ +(ns keycard.keycard + (:require + ["react-native" :as rn] + ["react-native-status-keycard" :default status-keycard] + [oops.core :as oops] + [react-native.platform :as platform] + [schema.core :as schema] + [taoensso.timbre :as log])) + +(defonce event-emitter + (if platform/ios? + (new (.-NativeEventEmitter rn) status-keycard) + (.-DeviceEventEmitter rn))) + +(defn start-nfc + [{:keys [on-success on-failure prompt-message]}] + (log/debug "start-nfc") + (.. status-keycard + (startNFC (str prompt-message)) + (then on-success) + (catch on-failure))) + +(defn stop-nfc + [{:keys [on-success on-failure error-message]}] + (log/debug "stop-nfc") + (.. status-keycard + (stopNFC (str error-message)) + (then on-success) + (catch on-failure))) + +(defn set-nfc-message + [{:keys [on-success on-failure status-message]}] + (log/debug "set-nfc-message") + (.. status-keycard + (setNFCMessage (str status-message)) + (then on-success) + (catch on-failure))) + +(defn check-nfc-support + [{:keys [on-success]}] + (.. status-keycard + nfcIsSupported + (then on-success))) + +(defn check-nfc-enabled + [{:keys [on-success]}] + (.. status-keycard + nfcIsEnabled + (then on-success))) + +(defn open-nfc-settings + [] + (.openNfcSettings status-keycard)) + +(defn remove-event-listeners + [] + (doseq [event ["keyCardOnConnected" "keyCardOnDisconnected" "keyCardOnNFCUserCancelled" + "keyCardOnNFCTimeout"]] + (.removeAllListeners ^js event-emitter event))) + +(defn remove-event-listener + [^js event] + (when event + (.remove event))) + +(defn on-card-connected + [callback] + (.addListener ^js event-emitter "keyCardOnConnected" callback)) + +(defn on-card-disconnected + [callback] + (.addListener ^js event-emitter "keyCardOnDisconnected" callback)) + +(defn on-card-new-pairing + [callback] + (.addListener ^js event-emitter "keyCardNewPairing" callback)) + +(defn on-nfc-user-cancelled + [callback] + (.addListener ^js event-emitter "keyCardOnNFCUserCancelled" callback)) + +(defn on-nfc-timeout + [callback] + (.addListener ^js event-emitter "keyCardOnNFCTimeout" callback)) + +(defn on-nfc-enabled + [callback] + (.addListener ^js event-emitter "keyCardOnNFCEnabled" callback)) + +(defn on-nfc-disabled + [callback] + (.addListener ^js event-emitter "keyCardOnNFCDisabled" callback)) + +(defn set-pairings + [pairings] + (.. status-keycard (setPairings (clj->js (or pairings {}))))) + +(defn get-application-info + [{:keys [on-success on-failure]}] + (.. status-keycard + (getApplicationInfo) + (then on-success) + (catch on-failure))) + +(defn factory-reset + [{:keys [on-success on-failure]}] + (.. status-keycard + (factoryReset) + (then on-success) + (catch on-failure))) + +(defn install-applet + [{:keys [on-success on-failure]}] + (.. status-keycard + installApplet + (then on-success) + (catch on-failure))) + +(defn install-cash-applet + [{:keys [on-success on-failure]}] + (.. status-keycard + installCashApplet + (then on-success) + (catch on-failure))) + +(defn init-card + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (init pin) + (then on-success) + (catch on-failure))) + +(defn install-applet-and-init-card + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (installAppletAndInitCard pin) + (then on-success) + (catch on-failure))) + +(defn pair + [{:keys [password on-success on-failure]}] + (when password + (.. status-keycard + (pair password) + (then on-success) + (catch on-failure)))) + +(defn generate-mnemonic + [{:keys [words on-success on-failure]}] + (.. status-keycard + (generateMnemonic words) + (then on-success) + (catch on-failure))) + +(defn generate-and-load-key + [{:keys [mnemonic pin on-success on-failure]}] + (.. status-keycard + (generateAndLoadKey mnemonic pin) + (then on-success) + (catch on-failure))) + +(defn save-mnemonic + [{:keys [mnemonic pin on-success on-failure]}] + (.. status-keycard + (saveMnemonic mnemonic pin) + (then on-success) + (catch on-failure))) + +(defn unblock-pin + [{:keys [puk new-pin on-success on-failure]}] + (when (and new-pin puk) + (.. status-keycard + (unblockPin puk new-pin) + (then on-success) + (catch on-failure)))) + +(defn verify-pin + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (verifyPin pin) + (then on-success) + (catch on-failure)))) + +(defn change-pin + [{:keys [current-pin new-pin on-success on-failure]}] + (when (and current-pin new-pin) + (.. status-keycard + (changePin current-pin new-pin) + (then on-success) + (catch on-failure)))) + +(defn change-puk + [{:keys [pin puk on-success on-failure]}] + (when (and pin puk) + (.. status-keycard + (changePUK pin puk) + (then on-success) + (catch on-failure)))) + +(defn change-pairing + [{:keys [pin pairing on-success on-failure]}] + (when (and pin pairing) + (.. status-keycard + (changePairingPassword pin pairing) + (then on-success) + (catch on-failure)))) + +(defn unpair + [{:keys [pin on-success on-failure]}] + (when pin + (.. status-keycard + (unpair pin) + (then on-success) + (catch on-failure)))) + +(defn delete + [{:keys [on-success on-failure]}] + (.. status-keycard + (delete) + (then on-success) + (catch on-failure))) + +(defn remove-key + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (removeKey pin) + (then on-success) + (catch on-failure))) + +(defn remove-key-with-unpair + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (removeKeyWithUnpair pin) + (then on-success) + (catch on-failure))) + +(defn export-key + [{:keys [pin path on-success on-failure]}] + (.. status-keycard + (exportKeyWithPath pin path) + (then on-success) + (catch on-failure))) + +(defn unpair-and-delete + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (unpairAndDelete pin) + (then on-success) + (catch on-failure)))) + +(defn import-keys + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (importKeys pin) + (then on-success) + (catch on-failure)))) + +(defn get-keys + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (getKeys pin) + (then on-success) + (catch on-failure)))) + +(defn sign + [{:keys [pin path hash-data]}] + (if path + (oops/ocall status-keycard "signWithPath" pin path hash-data) + (oops/ocall status-keycard "sign" pin hash-data))) + +(schema/=> sign + [:=> + [:cat + [:map + [:pin :string] + [:hash-data :string] + [:path {:optional true} [:maybe :string]]]] + :any]) + +(defn sign-typed-data + [{card-hash :hash on-success :on-success on-failure :on-failure}] + (when card-hash + (.. status-keycard + (signPinless card-hash) + (then on-success) + (catch on-failure)))) diff --git a/src/legacy/status_im/browser/core.cljs b/src/legacy/status_im/browser/core.cljs new file mode 100644 index 00000000000..0e8f5ec4676 --- /dev/null +++ b/src/legacy/status_im/browser/core.cljs @@ -0,0 +1,559 @@ +(ns legacy.status-im.browser.core + (:require + ["eth-phishing-detect" :as eth-phishing-detect] + [clojure.string :as string] + legacy.status-im.browser.effects + [legacy.status-im.browser.permissions :as browser.permissions] + [legacy.status-im.browser.webview-ref :as webview-ref] + [legacy.status-im.ethereum.ens :as ens] + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [legacy.status-im.ui.components.list-selection :as list-selection] + [legacy.status-im.utils.deprecated-types :as types] + [legacy.status-im.utils.random :as random] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [react-native.platform :as platform] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.common.universal-links :as links] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.chat.events :as chat.events] + [status-im.navigation.events :as navigation] + [utils.debounce :as debounce] + [utils.ens.core :as utils.ens] + [utils.ethereum.chain :as chain] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.url :as url])) + +(rf/defn update-browser-option + [{:keys [db]} option-key option-value] + {:db (assoc-in db [:browser/options option-key] option-value)}) + +(rf/defn update-browser-options + [{:keys [db]} options] + {:db (update db :browser/options merge options)}) + +(defn get-current-browser + [db] + (get-in db [:browser/browsers (get-in db [:browser/options :browser-id])])) + +(defn get-current-url + [{:keys [history history-index] + :or {history-index 0}}] + (when history + (nth history history-index))) + +(defn secure? + [{:keys [error? dapp?]} {:keys [url]}] + (or dapp? + (and (not error?) + (when url + (string/starts-with? url "https://"))))) + +(rf/defn remove-browser + {:events [:browser.ui/remove-browser-pressed]} + [{:keys [db]} browser-id] + {:db (update-in db [:browser/browsers] dissoc browser-id) + :json-rpc/call [{:method "wakuext_deleteBrowser" + :params [browser-id] + :on-success #()}]}) + +(rf/defn clear-all-browsers + {:events [:browser.ui/clear-all-browsers-pressed]} + [{:keys [db]}] + {:db (dissoc db :browser/browsers) + :json-rpc/call (for [browser-id (keys (get db :browser/browsers))] + {:method "wakuext_deleteBrowser" + :params [browser-id] + :on-success #()})}) + +(defn update-dapp-name + [{:keys [name] :as browser}] + (assoc browser :dapp? false :name (or name (i18n/label :t/browser)))) + +(defn check-if-phishing-url + [{:keys [history history-index] :as browser}] + (let [history-host (url/url-host (try (nth history history-index) (catch js/Error _)))] + (cond-> browser history-host (assoc :unsafe? (eth-phishing-detect history-host))))) + +(defn resolve-ens-contenthash-callback + [url] + (if (not (string/blank? url)) + (re-frame/dispatch [:browser.callback/resolve-ens-multihash-success url]) + (re-frame/dispatch [:browser.callback/resolve-ens-multihash-error]))) + +(rf/defn resolve-url + [{:keys [db]} {:keys [error? resolved-url]}] + (when (not error?) + (let [current-url (get-current-url (get-current-browser db)) + host (url/url-host current-url)] + (if (and (not resolved-url) (utils.ens/is-valid-eth-name? host)) + {:db (update db :browser/options assoc :resolving? true) + :browser/resolve-ens-contenthash {:chain-id (chain/chain-id db) + :ens-name host + :cb resolve-ens-contenthash-callback}} + {:db (update db :browser/options assoc :url (or resolved-url current-url) :resolving? false)})))) + +(rf/defn update-browser + [{:keys [db]} + {:keys [browser-id] :as browser}] + (let [updated-browser (-> browser + (update-dapp-name) + (check-if-phishing-url))] + {:db (update-in db + [:browser/browsers browser-id] + merge + updated-browser) + :json-rpc/call [{:method "wakuext_addBrowser" + :params [(select-keys updated-browser + [:browser-id :timestamp :name :dapp? :history + :history-index])] + :on-success #()}]})) + +(rf/defn store-bookmark + {:events [:browser/store-bookmark]} + [{:keys [db]} + {:keys [url] :as bookmark}] + {:db (assoc-in db [:bookmarks/bookmarks url] bookmark) + :json-rpc/call [{:method "wakuext_addBookmark" + :params [bookmark] + :on-success #()}]}) + +(rf/defn update-bookmark + {:events [:browser/update-bookmark]} + [{:keys [db] :as cofx} + {:keys [url] :as bookmark}] + (let [old-bookmark (get-in db [:bookmarks/bookmarks url]) + new-bookmark (merge old-bookmark bookmark)] + (rf/merge cofx + {:db (assoc-in db [:bookmarks/bookmarks url] new-bookmark) + :json-rpc/call [{:method "wakuext_updateBookmark" + :params [url bookmark] + :on-success #()}]}))) + +(rf/defn delete-bookmark + {:events [:browser/delete-bookmark]} + [{:keys [db] :as cofx} + url] + (rf/merge cofx + {:db (update db :bookmarks/bookmarks dissoc url) + :json-rpc/call [{:method "wakuext_removeBookmark" + :params [url] + :on-success #()}]})) + +(defn can-go-back? + [{:keys [history-index]}] + (pos? history-index)) + +(rf/defn navigate-to-previous-page + {:events [:browser.ui/previous-page-button-pressed]} + [cofx] + (let [{:keys [history-index] :as browser} (get-current-browser (:db cofx))] + (when (can-go-back? browser) + (rf/merge cofx + (update-browser (assoc browser :history-index (dec history-index))) + (resolve-url nil))))) + +(rf/defn ignore-unsafe + {:events [:browser/ignore-unsafe]} + [cofx] + (let [browser (get-current-browser (:db cofx)) + host (url/url-host (get-current-url browser))] + (update-browser cofx (assoc browser :ignore-unsafe host)))) + +(defn can-go-forward? + [{:keys [history-index history]}] + (< history-index (dec (count history)))) + +(rf/defn navigate-to-next-page + {:events [:browser.ui/next-page-button-pressed]} + [cofx] + (let [{:keys [history-index] :as browser} (get-current-browser (:db cofx))] + (when (can-go-forward? browser) + (rf/merge cofx + (update-browser (assoc browser :history-index (inc history-index))) + (resolve-url nil))))) + +(rf/defn update-browser-history + [cofx browser url] + (let [history-index (:history-index browser) + history (:history browser)] + (when history + (let [new-history (conj (subvec history 0 (inc history-index)) url) + new-index (dec (count new-history))] + (update-browser cofx + (assoc browser + :history new-history + :history-index new-index)))))) + +(rf/defn resolve-ens-multihash-success + {:events [:browser.callback/resolve-ens-multihash-success]} + [{:keys [db] :as cofx} url] + (let [current-url (get-current-url (get-current-browser db)) + host (url/url-host current-url) + path (subs current-url (+ (.indexOf ^js current-url host) (count host))) + gateway url] + (rf/merge cofx + {:db (-> (update db + :browser/options + assoc + :url (str gateway path) + :resolving? false) + (assoc-in [:browser/options :resolved-ens host] gateway))}))) + +(rf/defn resolve-ens-multihash-error + {:events [:browser.callback/resolve-ens-multihash-error]} + [{:keys [db] :as cofx}] + (update-browser-options cofx + {:url (get-current-url (get-current-browser db)) + :resolving? false + :error? true})) + +(rf/defn handle-browser-error + {:events [:browser/error-occured]} + [cofx] + (rf/merge cofx + (update-browser-option :error? true) + (update-browser-option :loading? false))) + +(rf/defn handle-pdf + [_ url] + (when (and platform/android? (string/ends-with? url ".pdf")) + {:browser/show-web-browser-selection url})) + +(rf/defn handle-message-link + {:events [:browser.ui/message-link-pressed]} + [_ link] + (when (security/safe-link? link) + (if (links/universal-link? link) + {:dispatch [:universal-links/handle-url link]} + {:browser/show-browser-selection link}))) + +(rf/defn update-browser-on-nav-change + [cofx url error?] + (let [browser (get-current-browser (:db cofx)) + options (get-in cofx [:db :browser/options]) + current-url (:url options)] + (when (and browser + (not (string/blank? url)) + (not= "about:blank" url) + (not= current-url url) + (not= (str current-url "/") url)) + (let [resolved-ens (first (filter (fn [v] + (not= (.indexOf ^js url (second v)) -1)) + (:resolved-ens options))) + resolved-url (if resolved-ens + (url/normalize-url (string/replace url + (second resolved-ens) + (first resolved-ens))) + url)] + (rf/merge cofx + (update-browser-history browser resolved-url) + (handle-pdf url) + (resolve-url {:error? error? :resolved-url (when resolved-ens url)})))))) + +(rf/defn update-browser-name + [cofx title] + (let [browser (get-current-browser (:db cofx))] + (when (and browser (not (:dapp? browser)) title (not (string/blank? title))) + (update-browser cofx (assoc browser :name title))))) + +(rf/defn navigation-state-changed + {:events [:browser/navigation-state-changed]} + [cofx event error?] + (let [{:strs [url loading title]} (js->clj event) + deep-link? (links/deep-link? url)] + (if (links/universal-link? url) + (when-not (and deep-link? platform/ios?) ;; ios webview handles this + {:dispatch [:universal-links/handle-url url]}) + (rf/merge cofx + (update-browser-option :loading? loading) + (update-browser-name title) + (update-browser-on-nav-change url error?))))) + +(rf/defn open-url-in-current-browser + "Opens a url in the current browser, which mean no new entry is added to the home page + and history of the current browser is updated so that the user can navigate back to the + origin url" + {:events [:browser.ui/url-submitted]} + [cofx url] + (let [browser (get-current-browser (:db cofx)) + normalized-url (url/normalize-and-decode-url url)] + (if (links/universal-link? normalized-url) + {:dispatch [:universal-links/handle-url normalized-url]} + (rf/merge cofx + (update-browser-option :url-editing? false) + (update-browser-history browser normalized-url) + (resolve-url nil))))) + +(rf/defn open-url + "Opens a url in the browser. If a host can be extracted from the url and + there is already a browser for this host, this browser is reused + If the browser is reused, the history is flushed" + {:events [:browser.ui/open-url]} + [{:keys [db] :as cofx} url] + (let [normalized-url (url/normalize-and-decode-url url) + browser {:browser-id (random/id) + :history-index 0 + :history [normalized-url]}] + (if (links/universal-link? normalized-url) + {:dispatch [:universal-links/handle-url normalized-url]} + (if config/show-not-implemented-features? + (rf/merge cofx + {:db (assoc db + :browser/options + {:browser-id (:browser-id browser)} + :browser/screen-id :browser) + :dispatch-n [[:shell/change-tab :screen/browser-stack]]} + (navigation/pop-to-root :screen/shell-stack) + (chat.events/close-chat (:current-chat-id db)) + (update-browser browser) + (resolve-url nil)) + {:linking/open-url url})))) + +(rf/defn open-existing-browser + "Opens an existing browser with it's history" + {:events [:browser.ui/browser-item-selected]} + [{:keys [db] :as cofx} browser-id] + (let [browser (get-in db [:browser/browsers browser-id])] + (rf/merge cofx + {:db (assoc db + :browser/options + {:browser-id browser-id} + :browser/screen-id :browser) + :dispatch-n [[:shell/change-tab :screen/browser-stack]]} + (update-browser browser) + (resolve-url nil)))) + +(rf/defn open-browser-tabs + {:events [:browser.ui/open-browser-tabs]} + [{:keys [db] :as cofx}] + (rf/merge cofx + {:db (assoc db :browser/screen-id :browser-tabs) + :dispatch-n [[:shell/change-tab :screen/browser-stack]]})) + +(rf/defn web3-error-callback + {:events [:browser.dapp/transaction-on-error]} + [_ message-id message] + {:browser/send-to-bridge + {:type constants/web3-send-async-callback + :messageId message-id + :error message}}) + +(rf/defn dapp-complete-transaction + {:events [:browser.dapp/transaction-on-result]} + [_ message-id id result] + ;;TODO check and test id + {:browser/send-to-bridge + {:type constants/web3-send-async-callback + :messageId message-id + :result {:jsonrpc "2.0" + :id (int id) + :result result}}}) + +(rf/defn send-to-bridge + {:events [:browser.callback/call-rpc]} + [_ message] + {:browser/send-to-bridge message}) + +(rf/defn handle-no-permissions + [cofx {:keys [method id]} message-id] + (if (= method "eth_accounts") + ;; eth_accounts returns empty array for compatibility with meta-mask + (send-to-bridge cofx + {:type constants/web3-send-async-callback + :messageId message-id + :result {:jsonrpc "2.0" + :id (int id) + :result []}}) + (send-to-bridge cofx + {:type constants/web3-send-async-callback + :messageId message-id + :error {:code 4100}}))) + +(def permissioned-method + #{"eth_accounts" "eth_coinbase" "eth_sendTransaction" "eth_sign" + "keycard_signTypedData" + "eth_signTypedData" "personal_sign" "personal_ecRecover"}) + +(defn has-permissions? + [{:dapps/keys [permissions]} dapp-name method] + (boolean + (and (permissioned-method method) + (not (some #{constants/dapp-permission-web3} (get-in permissions [dapp-name :permissions])))))) + +(rf/defn web3-send-async-read-only + [{:keys [db] :as cofx} dapp-name {:keys [method] :as payload} message-id] + (when (has-permissions? db dapp-name method) + (handle-no-permissions cofx payload message-id))) + +(rf/defn handle-scanned-qr-code + {:events [:browser.bridge.callback/qr-code-scanned]} + [cofx data {{:keys [dapp-name permission message-id]} :data}] + (rf/merge (assoc-in cofx [:db :browser/options :yielding-control?] false) + (browser.permissions/send-response-to-bridge permission message-id true data) + (browser.permissions/process-next-permission dapp-name) + (navigation/navigate-back nil))) + +(rf/defn handle-canceled-qr-code + {:events [:browser.bridge.callback/qr-code-canceled]} + [cofx {{:keys [dapp-name permission message-id]} :data}] + (rf/merge (assoc-in cofx [:db :browser/options :yielding-control?] false) + (browser.permissions/send-response-to-bridge permission message-id true nil) + (browser.permissions/process-next-permission dapp-name))) + +(rf/defn process-bridge-message + {:events [:browser/bridge-message-received]} + [{:keys [db] :as cofx} message] + (let [browser (get-current-browser + db) + url-original (get-current-url + browser) + data (types/json->clj + message) + {{:keys [url]} :navState :keys [type permission payload messageId params]} data + {:keys [dapp? name]} browser + dapp-name (if dapp? + name + (url/url-host + url-original))] + (cond + (and (= type constants/history-state-changed) + (not= "about:blank" url)) + (update-browser-on-nav-change cofx url nil) + + (= type constants/web3-send-async-read-only) + (web3-send-async-read-only cofx dapp-name payload messageId) + + (= type constants/api-request) + (browser.permissions/process-permission cofx dapp-name permission messageId params)))) + +(re-frame/reg-fx + :browser/resolve-ens-contenthash + (fn [{:keys [chain-id ens-name cb]}] + (ens/resource-url chain-id ens-name cb))) + +(re-frame/reg-fx + :browser/send-to-bridge + (fn [message] + (let + [^js webview @webview-ref/webview-ref + msg + (str + "(function() { var __send = function() { if (ReactNativeWebView.onMessage) { ReactNativeWebView.onMessage('" + (types/clj->json message) + "');} else {setTimeout(__send, 0)}}; __send();})();")] + (when (and message webview) + (.injectJavaScript webview msg))))) + +(re-frame/reg-fx + :browser/show-browser-selection + (fn [link] + (js/setTimeout #(list-selection/browse link) 500))) + +(re-frame/reg-fx + :browser/show-web-browser-selection + (fn [link] + (list-selection/browse-in-web-browser link))) + +(re-frame/reg-fx + :browser/clear-web-data + (fn [] + (native-module/clear-web-data))) + +(defn share-link + [url] + (let [link (links/generate-link :browse :external url) + message (i18n/label :t/share-dapp-text {:link link})] + (list-selection/open-share {:message message}))) + +(rf/defn dapps-account-selected + {:events [:dapps-account-selected]} + [{:keys [db] :as cofx} address] + (rf/merge cofx + {:browser/clear-web-data nil} + (navigation/hide-bottom-sheet) + (browser.permissions/clear-dapps-permissions) + (multiaccounts.update/multiaccount-update :dapps-address address {}) + #(when (= (:view-id db) :browser) + (merge (navigation/navigate-back %) + {:dispatch [:browser.ui/browser-item-selected + (get-in db [:browser/options :browser-id])]})))) + +(rf/defn open-empty-tab + {:events [:browser.ui/open-empty-tab]} + [{:keys [db]}] + (debounce/clear :browser/navigation-state-changed) + {:db (assoc db :browser/screen-id :empty-tab)}) + +(rf/defn url-input-pressed + {:events [:browser.ui/url-input-pressed]} + [cofx _] + (update-browser-option cofx :url-editing? true)) + +(rf/defn url-input-blured + {:events [:browser.ui/url-input-blured]} + [cofx] + (update-browser-option cofx :url-editing? false)) + +(rf/defn lock-pressed + {:events [:browser.ui/lock-pressed]} + [cofx is-secure?] + (update-browser-option cofx :show-tooltip (if is-secure? :secure :not-secure))) + +(rf/defn close-tooltip-pressed + {:events [:browser.ui/close-tooltip-pressed]} + [cofx] + (update-browser-option cofx :show-tooltip nil)) + +(rf/defn loading-started + {:events [:browser/loading-started]} + [cofx] + (update-browser-options cofx {:error? false :loading? true})) + +(rf/defn handle-bookmarks + [{:keys [db]} bookmarks] + (let [changed-bookmarks (reduce (fn [acc {:keys [url] :as bookmark}] + (assoc acc url bookmark)) + {} + bookmarks) + stored-bookmarks (get-in db [:bookmarks/bookmarks])] + {:db (assoc-in db [:bookmarks/bookmarks] (merge stored-bookmarks changed-bookmarks))})) + +(rf/defn initialize-dapp-permissions + {:events [::initialize-dapp-permissions]} + [{:keys [db]} all-dapp-permissions] + (let [dapp-permissions (reduce (fn [acc {:keys [dapp] :as dapp-permissions}] + (assoc acc dapp dapp-permissions)) + {} + all-dapp-permissions)] + {:db (assoc db :dapps/permissions dapp-permissions)})) + +(rf/defn initialize-browsers + {:events [::initialize-browsers]} + [{:keys [db]} all-stored-browsers] + (let [browsers (reduce (fn [acc {:keys [browser-id] :as browser}] + (assoc acc browser-id browser)) + {} + all-stored-browsers)] + {:db (assoc db :browser/browsers browsers)})) + +(rf/defn initialize-bookmarks + {:events [::initialize-bookmarks]} + [{:keys [db]} stored-bookmarks] + (let [bookmarks (reduce (fn [acc {:keys [url] :as bookmark}] + (assoc acc url bookmark)) + {} + stored-bookmarks)] + {:db (assoc db :bookmarks/bookmarks bookmarks)})) + +(re-frame/reg-fx :browser/initialize-browser + (fn [] + (json-rpc/call {:method "wakuext_getBrowsers" + :on-success [::initialize-browsers]}) + (json-rpc/call {:method "browsers_getBookmarks" + :on-success [::initialize-bookmarks]}) + (json-rpc/call {:method "permissions_getDappPermissions" + :on-success [::initialize-dapp-permissions]}))) diff --git a/src/legacy/status_im/browser/effects.cljs b/src/legacy/status_im/browser/effects.cljs new file mode 100644 index 00000000000..51355be52fd --- /dev/null +++ b/src/legacy/status_im/browser/effects.cljs @@ -0,0 +1,11 @@ +(ns legacy.status-im.browser.effects + (:require [clojure.string :as string] + [react-native.core :as react] + [utils.re-frame :as rf] + [utils.url :as url])) + +(rf/reg-fx + :linking/open-url + (fn [url] + (when (not (string/blank? url)) + (.openURL ^js react/linking (url/normalize-url url))))) diff --git a/src/legacy/status_im/browser/permissions.cljs b/src/legacy/status_im/browser/permissions.cljs new file mode 100644 index 00000000000..4036a446472 --- /dev/null +++ b/src/legacy/status_im/browser/permissions.cljs @@ -0,0 +1,176 @@ +(ns legacy.status-im.browser.permissions + (:require + [legacy.status-im.qr-scanner.core :as qr-scanner] + [status-im.constants :as constants] + [status-im.navigation.events :as navigation] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(declare process-next-permission) +(declare send-response-to-bridge) + +(def supported-permissions + {constants/dapp-permission-qr-code {:yield-control? true + :allowed? true} + constants/dapp-permission-contact-code {:type :profile + :title (i18n/label :t/wants-to-access-profile) + :description (i18n/label :t/your-contact-code) + :icon :main-icons/profile} + constants/dapp-permission-web3 {:type :wallet + :title (i18n/label :t/dapp-would-like-to-connect-wallet) + :description (i18n/label :t/allowing-authorizes-this-dapp) + :icon :main-icons/wallet}}) + +(rf/defn permission-yield-control + [cofx dapp-name permission message-id] + (cond + (= permission constants/dapp-permission-qr-code) + (rf/merge (assoc-in cofx [:db :browser/options :yielding-control?] true) + (qr-scanner/scan-qr-code {:handler :browser.bridge.callback/qr-code-scanned + :cancel-handler :browser.bridge.callback/qr-code-canceled + :data {:dapp-name dapp-name + :permission permission + :message-id message-id}})))) + +(rf/defn permission-show-permission + [{:keys [db]} dapp-name permission message-id yield-control?] + {:db (assoc-in db + [:browser/options :show-permission] + {:requested-permission permission + :message-id message-id + :dapp-name dapp-name + :yield-control? yield-control?})}) + +(defn get-permission-data + [cofx allowed-permission] + (let [multiaccount (get-in cofx [:db :profile/profile])] + (get {constants/dapp-permission-contact-code (:public-key multiaccount) + constants/dapp-permission-web3 [(:dapps-address multiaccount)]} + allowed-permission))) + +(rf/defn send-response-to-bridge + "Send response to the bridge. If the permission is allowed, send data associated + with the permission" + [_cofx permission message-id allowed? data] + {:browser/send-to-bridge (cond-> {:type constants/api-response + :isAllowed allowed? + :permission permission + :messageId message-id} + allowed? + (assoc :data data))}) + +(rf/defn update-dapp-permissions + [{:keys [db]} dapp-name permission allowed?] + (let [dapp-permissions-set (set (get-in db [:dapps/permissions dapp-name :permissions])) + allowed-permissions-set (if allowed? + (conj dapp-permissions-set permission) + (disj dapp-permissions-set permission)) + allowed-permissions {:dapp dapp-name + :permissions (vec allowed-permissions-set)}] + {:db (assoc-in db [:dapps/permissions dapp-name] allowed-permissions) + :json-rpc/call [{:method "permissions_addDappPermissions" + :params [allowed-permissions] + :on-success #()}]})) + +(rf/defn revoke-permissions + {:events [:browser/revoke-dapp-permissions]} + [{:keys [db] :as cofx} dapp] + (rf/merge cofx + {:db (update-in db [:dapps/permissions] dissoc dapp) + :json-rpc/call [{:method "permissions_deleteDappPermissions" + :params [dapp] + :on-success #()}]})) + +(rf/defn revoke-dapp-permissions + {:events [:dapps/revoke-access]} + [cofx dapp] + (rf/merge cofx + (revoke-permissions dapp) + (navigation/navigate-back nil))) + +(rf/defn clear-dapps-permissions + [{:keys [db]}] + (let [dapp-permissions (keys (:dapps/permissions db))] + {:db (dissoc db :dapps/permissions) + :json-rpc/call (for [dapp dapp-permissions] + {:method "permissions_deleteDappPermissions" + :params [dapp] + :on-success #()})})) + +(rf/defn process-next-permission + "Process next permission by removing it from pending permissions and prompting user + if there is no pending permissions left, save all granted permissions + and return the result to the bridge" + [{:keys [db] :as cofx} dapp-name] + (let [{:keys [show-permission yielding-control?]} (get db :browser/options)] + (if (or show-permission yielding-control?) + {:db db} + (let [pending-permissions (get-in db [:browser/options :pending-permissions]) + next-permission (last pending-permissions) + new-cofx (update-in cofx [:db :browser/options :pending-permissions] butlast)] + (when-let [{:keys [yield-control? permission message-id allowed?]} next-permission] + (if (and yield-control? allowed?) + (permission-yield-control new-cofx dapp-name permission message-id) + (permission-show-permission new-cofx dapp-name permission message-id yield-control?))))))) + +(rf/defn send-response-and-process-next-permission + [cofx dapp-name requested-permission message-id] + (rf/merge cofx + (send-response-to-bridge requested-permission + message-id + true + (get-permission-data cofx requested-permission)) + (process-next-permission dapp-name))) + +(rf/defn allow-permission + "Add permission to set of allowed permission and process next permission" + {:events [:browser.permissions.ui/dapp-permission-allowed]} + [{:keys [db] :as cofx}] + (let [{:keys [requested-permission message-id dapp-name yield-control?]} + (get-in db [:browser/options :show-permission])] + (rf/merge (assoc-in cofx [:db :browser/options :show-permission] nil) + (update-dapp-permissions dapp-name requested-permission true) + (if yield-control? + (permission-yield-control dapp-name requested-permission message-id) + (send-response-and-process-next-permission dapp-name requested-permission message-id))))) + +(rf/defn deny-permission + "Add permission to set of allowed permission and process next permission" + {:events [:browser.permissions.ui/dapp-permission-denied]} + [{:keys [db] :as cofx}] + (let [{:keys [requested-permission message-id dapp-name]} (get-in db + [:browser/options :show-permission])] + (rf/merge (assoc-in cofx [:db :browser/options :show-permission] nil) + (send-response-to-bridge requested-permission + message-id + false + (get-permission-data cofx requested-permission)) + (process-next-permission dapp-name)))) + +(rf/defn process-permission + "Process the permission requested by a dapp + If supported permission is already granted, return the result immediatly to the bridge + Otherwise process the first permission which will prompt user" + [cofx dapp-name permission message-id params] + (let [allowed-permissions (set (get-in cofx [:db :dapps/permissions dapp-name :permissions])) + permission-allowed? (boolean (allowed-permissions permission)) + supported-permission (get supported-permissions permission)] + (cond + (not supported-permission) + (send-response-to-bridge cofx permission message-id false nil) + + (and (or permission-allowed? (:allowed? supported-permission)) + (not (:yield-control? supported-permission))) + (send-response-to-bridge cofx permission message-id true (get-permission-data cofx permission)) + + :else + (process-next-permission (update-in cofx + [:db :browser/options :pending-permissions] + conj + {:permission permission + :allowed? (or permission-allowed? + (:allowed? supported-permission)) + :yield-control? (:yield-control? supported-permission) + :params params + :message-id message-id}) + dapp-name)))) diff --git a/src/legacy/status_im/browser/webview_ref.cljs b/src/legacy/status_im/browser/webview_ref.cljs new file mode 100644 index 00000000000..d9a024e2b99 --- /dev/null +++ b/src/legacy/status_im/browser/webview_ref.cljs @@ -0,0 +1,3 @@ +(ns legacy.status-im.browser.webview-ref) + +(def webview-ref (atom nil)) diff --git a/src/legacy/status_im/chat/models/loading.cljs b/src/legacy/status_im/chat/models/loading.cljs new file mode 100644 index 00000000000..a0e10de5f71 --- /dev/null +++ b/src/legacy/status_im/chat/models/loading.cljs @@ -0,0 +1,201 @@ +(ns legacy.status-im.chat.models.loading + (:require + [legacy.status-im.data-store.chats :as data-store.chats] + [legacy.status-im.data-store.messages :as data-store.messages] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.list.events :as message-list] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn cursor->clock-value + [^js cursor] + (js/parseInt (.substring cursor 51 64))) + +(defn clock-value->cursor + [clock-value] + (str "000000000000000000000000000000000000000000000000000" + clock-value + "0x0000000000000000000000000000000000000000000000000000000000000000")) + +(rf/reg-event-fx :chats-list/load-success + (fn [{:keys [db]} [^js new-chats-js]] + (let [{:keys [all-chats chats-home-list]} + (reduce (fn [acc ^js chat-js] + (let [{:keys [chat-id profile-public-key timeline? community-id active] :as chat} + (data-store.chats/<-rpc-js chat-js)] + (cond-> acc + (and (not profile-public-key) (not timeline?) (not community-id) active) + (update :chats-home-list conj chat-id) + :always + (assoc-in [:all-chats chat-id] chat)))) + {:all-chats {} + :chats-home-list #{}} + new-chats-js)] + {:db (-> db + (update :chats merge all-chats) + (update :chats-home-list into chats-home-list))}))) + +(rf/defn load-chat-success + {:events [:chats-list/load-chat-success]} + [{:keys [db]} ^js chat] + (let [{:keys [chat-id] :as chat} (data-store.chats/<-rpc chat)] + {:db (update-in db [:chats chat-id] merge chat)})) + +(rf/defn load-chat + {:events [:chats-list/load-chat]} + [_ chat-id] + {:json-rpc/call [{:method "wakuext_chat" + :params [chat-id] + :on-success #(re-frame/dispatch [:chats-list/load-chat-success %]) + :on-error #(log/error "failed to fetch chats" 0 -1 %)}]}) + +(rf/defn handle-failed-loading-messages + {:events [::failed-loading-messages]} + [{:keys [db]} current-chat-id _ err] + (log/error "failed loading messages" current-chat-id err) + (when current-chat-id + {:db (assoc-in db [:pagination-info current-chat-id :loading-messages?] false)})) + +(defn mark-chat-all-read + [db chat-id] + (when (get-in db [:chats chat-id]) + (update-in + db + [:chats chat-id] + assoc + :unviewed-messages-count 0 + :unviewed-mentions-count 0 + :highlight false))) + +(rf/defn handle-mark-all-read-successful + {:events [::mark-all-read-successful]} + [_] + {:dispatch [:activity-center.notifications/fetch-unread-count]}) + +(rf/defn handle-mark-all-read-in-community-successful + {:events [::mark-all-read-in-community-successful]} + [{:keys [db] :as cofx} {:keys [chats] :as _messenger-response}] + (rf/merge cofx + {:db (reduce mark-chat-all-read db (map :id chats)) + :dispatch [:activity-center.notifications/fetch-unread-count]})) + +(rf/defn handle-mark-all-read + {:events [:chat.ui/mark-all-read-pressed :chat/mark-all-as-read]} + [{db :db} chat-id] + {:db (mark-chat-all-read db chat-id) + :effects/push-notifications-clear-message-notifications [chat-id] + :json-rpc/call [{:method "wakuext_markAllRead" + :params [chat-id] + :on-success + #(re-frame/dispatch + [::mark-all-read-successful])}]}) + +(rf/defn handle-mark-mark-all-read-in-community + {:events [:chat.ui/mark-all-read-in-community-pressed]} + [{db :db} community-id] + (let [community-chat-ids (map #(str community-id %) + (keys (get-in db [:communities community-id :chats])))] + {:effects/push-notifications-clear-message-notifications community-chat-ids + :json-rpc/call [{:method "wakuext_markAllReadInCommunity" + :params [community-id] + :on-success + #(re-frame/dispatch + [::mark-all-read-in-community-successful + %])}]})) + +(rf/defn messages-loaded + "Loads more messages for current chat" + {:events [::messages-loaded]} + [{db :db} chat-id session-id {:keys [cursor messages]} on-loaded] + (when-not (and (get-in db [:pagination-info chat-id :messages-initialized?]) + (not= session-id + (get-in db [:pagination-info chat-id :messages-initialized?]))) + (let [already-loaded-messages (get-in db [:messages chat-id]) + ;; We remove those messages that are already loaded, as we might get some duplicates + {:keys [all-messages new-messages contacts]} + (reduce (fn [{:keys [all-messages] :as acc} + {:keys [message-id from] + :as message}] + (let [message + ;; For example, when a user receives a list of 4 image messages while + ;; inside the chat screen we shouldn't group the images into albums. When + ;; the user exists the chat screen then enters the chat screen again, we + ;; now need to group the images into albums (like WhatsApp). The albumize? + ;; boolean is used to know whether we need to group these images into + ;; albums now or not. The album-id can't be used for this because it will + ;; always be there. + (if (and (:album-id message) (nil? (get all-messages message-id))) + (assoc message :albumize? true) + message)] + (cond-> acc + (not (get-in db [:chats chat-id :users from])) + (update :senders assoc from message) + + (nil? (get all-messages message-id)) + (update :new-messages conj message) + + :always + (update :all-messages assoc message-id message)))) + {:all-messages already-loaded-messages + :senders {} + :contacts {} + :new-messages []} + messages) + current-clock-value (get-in db + [:pagination-info chat-id + :cursor-clock-value]) + clock-value (when cursor (cursor->clock-value cursor))] + (when on-loaded + (on-loaded (count new-messages))) + {:db (-> db + (update-in [:pagination-info chat-id :cursor-clock-value] + #(if (and (seq cursor) (or (not %) (< clock-value %))) + clock-value + %)) + (update-in [:pagination-info chat-id :cursor] + #(if (or (empty? cursor) + (not current-clock-value) + (< clock-value current-clock-value)) + cursor + %)) + (assoc-in [:pagination-info chat-id :loading-messages?] false) + (assoc-in [:messages chat-id] all-messages) + (update-in [:message-lists chat-id] message-list/add-many new-messages) + (assoc-in [:pagination-info chat-id :all-loaded?] + (empty? cursor)) + (update :contacts/contacts merge contacts))}))) + +(rf/defn load-more-messages + {:events [:chat.ui/load-more-messages]} + [{:keys [db]} chat-id first-request on-loaded] + (when-let [session-id (get-in db [:pagination-info chat-id :messages-initialized?])] + (when (and + (not (get-in db [:pagination-info chat-id :all-loaded?])) + (not (get-in db [:pagination-info chat-id :loading-messages?]))) + (let [cursor (get-in db [:pagination-info chat-id :cursor])] + (when (or first-request cursor) + (merge + {:db (assoc-in db [:pagination-info chat-id :loading-messages?] true)} + {:utils/dispatch-later [{:ms 100 :dispatch [:reactions/load-more cursor chat-id]}]} + (data-store.messages/messages-by-chat-id-rpc + chat-id + cursor + constants/default-number-of-messages + #(re-frame/dispatch [::messages-loaded chat-id session-id % on-loaded]) + #(re-frame/dispatch [::failed-loading-messages chat-id session-id %])))))))) + +(rf/defn load-more-messages-for-current-chat + {:events [:chat.ui/load-more-messages-for-current-chat]} + [{:keys [db] :as cofx} on-loaded] + (load-more-messages cofx (:current-chat-id db) false on-loaded)) + +(rf/defn load-messages + [{:keys [db now] :as cofx} chat-id] + (when-not (get-in db [:pagination-info chat-id :messages-initialized?]) + (rf/merge cofx + {:db (assoc-in db [:pagination-info chat-id :messages-initialized?] now) + :utils/dispatch-later [{:ms 50 :dispatch [:chat.ui/mark-all-read-pressed chat-id]} + (when-not (get-in cofx [:db :chats chat-id :public?]) + {:ms 100 :dispatch [:pin-message/load-pin-messages chat-id]})]} + (load-more-messages chat-id true nil)))) diff --git a/src/legacy/status_im/chat/models/mentions.cljs b/src/legacy/status_im/chat/models/mentions.cljs new file mode 100644 index 00000000000..12e1152aeb4 --- /dev/null +++ b/src/legacy/status_im/chat/models/mentions.cljs @@ -0,0 +1,170 @@ +(ns legacy.status-im.chat.models.mentions + (:require [clojure.set :as set] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn- transfer-input-segments + [segments] + (map (fn [segment] + (let [{:keys [type value]} segment + type (case type + 0 :text + 1 :mention + (log/warn "unknown segment type" {:type type}))] + [type value])) + segments)) + +(defn- rename-at-idxs + [at-idxs] + (map #(set/rename-keys % + {:From :from + :To :to + :Checked :checked? + :Mentioned :mention? + :Mention :mention + :NextAtIdx :next-at-idx}) + at-idxs)) + +(defn- rename-state + [state] + (-> state + (set/rename-keys {:AtSignIdx :at-sign-idx + :AtIdxs :at-idxs + :MentionEnd :mention-end + :PreviousText :previous-text + :NewText :new-text + :Start :start + :End :end}) + (update :at-idxs rename-at-idxs))) + +; referenced function: contact-list-item +(defn- rename-mentionable-users + [mentionable-users] + (reduce (fn [acc [id v]] + (assoc acc + id + (set/rename-keys v + {:id :public-key + :primaryName :primary-name + :secondaryName :secondary-name + :compressedKey :compressed-key + :ensVerified :ens-verified + :added :added? + :displayName :display-name + :searchedText :searched-text}))) + + {} + mentionable-users)) + +(defn- transfer-mention-result + [result] + (let [{:keys [input-segments mentionable-users state chat-id new-text call-id]} + (set/rename-keys result + {:InputSegments :input-segments + :MentionSuggestions :mentionable-users + :MentionState :state + :ChatID :chat-id + :NewText :new-text + :CallID :call-id})] + {:chat-id chat-id + :input-segments (transfer-input-segments input-segments) + :mentionable-users (rename-mentionable-users mentionable-users) + :state (rename-state state) + :new-text new-text + :call-id call-id})) + +(rf/defn on-error + {:events [:mention/on-error]} + [_ context error] + (log/error "[mentions] on-error" + {:context context + :error error})) + +(rf/defn to-input-field + {:events [:mention/to-input-field]} + [_ text chat-id] + (let [params [chat-id text] + method "wakuext_chatMentionToInputField"] + (log/debug "[mentions] to-input-field" {:params params}) + {:json-rpc/call [{:method method + :params params + :on-success #(rf/dispatch [:mention/on-to-input-field-success %]) + :on-error #(rf/dispatch [:mention/on-error + {:method method + :params params} %])}]})) + +(rf/defn on-to-input-field-success + {:events [:mention/on-to-input-field-success]} + [{:keys [db]} result] + (log/debug "[mentions] on-to-input-field-success" {:result result}) + (let [{:keys [chat-id new-text]} (transfer-mention-result result)] + {:effects/set-input-text-value [(get-in db [:chat/inputs chat-id :input-ref]) new-text] + :dispatch [:chat.ui/set-chat-input-text new-text chat-id]})) + +(rf/defn on-change-text + {:events [:mention/on-change-text]} + [{:keys [db]} text] + (let [chat-id (:current-chat-id db) + call-id (inc (get-in db [:chat/inputs chat-id :current-mentions-call-id] 0)) + params [chat-id text call-id] + method "wakuext_chatMentionOnChangeText"] + (log/debug "[mentions] on-change-text" {:params params}) + {:db (assoc-in db [:chat/inputs chat-id :current-mentions-call-id] call-id) + :json-rpc/call [{:method method + :params params + :on-success #(rf/dispatch [:mention/on-change-text-success %]) + :on-error #(rf/dispatch [:mention/on-error + {:method method + :params params} %])}]})) + +(rf/defn on-change-text-success + {:events [:mention/on-change-text-success]} + [{:keys [db]} result] + (log/debug "[mentions] on-change-text-success" {:result result}) + (let [{:keys [state chat-id mentionable-users + input-segments call-id]} (transfer-mention-result result) + current-mentions-call-id (get-in db [:chat/inputs chat-id :current-mentions-call-id])] + (when (= call-id current-mentions-call-id) + {:db (-> db + (assoc-in [:chat/inputs-with-mentions chat-id] input-segments) + (assoc-in [:chats/mention-suggestions chat-id] mentionable-users) + (assoc-in [:chats/mentions chat-id :mentions] state))}))) + +(rf/defn on-select-mention-success + {:events [:mention/on-select-mention-success]} + [{:keys [db]} result primary-name match searched-text public-key] + (log/debug "[mentions] on-select-mention-success" + {:result result + :primary-name primary-name + :match match + :searched-text searched-text + :public-key public-key}) + (let [{:keys [new-text chat-id]} (transfer-mention-result result)] + {:db (assoc-in db [:chats/mention-suggestions chat-id] nil) + :effects/set-input-text-value [(get-in db [:chat/inputs (:current-chat-id db) :input-ref]) new-text] + :dispatch [:chat.ui/set-chat-input-text new-text chat-id]})) + +(rf/defn clear-mentions + [{:keys [db]}] + (let [chat-id (:current-chat-id db)] + {:db (update db :chats/mention-suggestions dissoc chat-id) + :json-rpc/call [{:method "wakuext_chatMentionClearMentions" + :params [chat-id] + :on-success #() + :on-error #(log/error "Error while calling wakuext_chatMentionClearMentions" + {:error %})}]})) + +(rf/defn select-mention + {:events [:chat.ui/select-mention]} + [{:keys [db]} {:keys [primary-name searched-text match public-key] :as _user}] + (let [chat-id (:current-chat-id db) + text (get-in db [:chat/inputs chat-id :input-text]) + method "wakuext_chatMentionSelectMention" + params [chat-id text primary-name public-key]] + {:json-rpc/call [{:method method + :params params + :on-success #(rf/dispatch [:mention/on-select-mention-success % + primary-name match searched-text public-key]) + :on-error #(rf/dispatch [:mention/on-error + {:method method + :params params} %])}]})) diff --git a/src/legacy/status_im/chat/models/message.cljs b/src/legacy/status_im/chat/models/message.cljs new file mode 100644 index 00000000000..9438b5ade71 --- /dev/null +++ b/src/legacy/status_im/chat/models/message.cljs @@ -0,0 +1,181 @@ +(ns legacy.status-im.chat.models.message + (:require + [clojure.string :as string] + [legacy.status-im.chat.models.loading :as chat.loading] + [legacy.status-im.data-store.messages :as data-store.messages] + [legacy.status-im.utils.deprecated-types :as types] + [react-native.platform :as platform] + [status-im.contexts.chat.messenger.messages.list.events :as message-list] + [status-im.contexts.chat.messenger.messages.list.state :as view.state] + [utils.re-frame :as rf])) + +(defn- message-loaded? + [db chat-id message-id] + (get-in db [:messages chat-id message-id])) + +(defn- earlier-than-deleted-at? + [db chat-id clock-value] + (>= (get-in db [:chats chat-id :deleted-at-clock-value]) clock-value)) + +(defn hide-message + "Hide chat message, rebuild message-list" + [{:keys [db]} chat-id message-id] + ;; TODO this is too expensive, probably we could mark message somehow and just hide it in the UI + (message-list/rebuild-message-list {:db (update-in db [:messages chat-id] dissoc message-id)} chat-id)) + +(defn add-pinned-message + [acc chat-id message-id message] + (let [{:keys [pinned-by pinned-at] :as pinned-message} + (get-in acc [:db :pin-messages chat-id message-id])] + (if pinned-message + (assoc-in acc + [:db :pin-messages chat-id message-id] + (assoc + message + :pinned-by pinned-by + :pinned-at pinned-at)) + acc))) + +(defn add-message + [{:keys [db] :as acc} message-js chat-id message-id cursor-clock-value] + (let [{:keys [from clock-value] :as message} + (data-store.messages/<-rpc (types/js->clj message-js)) + acc-with-pinned-message (add-pinned-message acc chat-id message-id message)] + (if (message-loaded? db chat-id message-id) + ;; If the message is already loaded, it means it's an update, that + ;; happens when a message that was missing a reply had the reply + ;; coming through, in which case we just insert the new message + (assoc-in acc-with-pinned-message [:db :messages chat-id message-id] message) + (cond-> acc-with-pinned-message + ;;add new message to db + :always + (update-in [:db :messages chat-id] assoc message-id message) + :always + (update-in [:db :message-lists chat-id] message-list/add message) + + (or (not cursor-clock-value) (< clock-value cursor-clock-value)) + (update-in [:db :pagination-info chat-id] + assoc + :cursor (chat.loading/clock-value->cursor clock-value) + :cursor-clock-value clock-value) + + ;;conj sender for add-sender-to-chat-users + (not (get-in db [:chats chat-id :users from])) + (update :senders assoc from message) + + (not (string/blank? (:replace message))) + ;;TODO this is expensive + (hide-message chat-id (:replace message)))))) + +(defn reduce-js-messages + [{:keys [db] :as acc} ^js message-js] + (let [chat-id (.-localChatId message-js) + clock-value (.-clock message-js) + message-id (.-id message-js) + current-chat-id (:current-chat-id db) + cursor-clock-value (get-in db [:pagination-info current-chat-id :cursor-clock-value])] + ;;ignore not opened chats and earlier clock + (if (and (get-in db [:pagination-info chat-id :messages-initialized?]) + ;;TODO why do we need this ? + (not (earlier-than-deleted-at? db chat-id clock-value))) + (if (or (not @view.state/first-not-visible-item) + (<= (:clock-value @view.state/first-not-visible-item) + clock-value)) + (add-message acc message-js chat-id message-id cursor-clock-value) + ;; Not in the current view, set all-loaded to false + ;; and offload to db and update cursor if necessary + ;;TODO if we'll offload messages , it will conflict with end reached, so probably if we + ;;reached the end of visible area, + ;; we need to drop other messages with (< clock-value cursor-clock-value) from response-js + ;; so we don't update :cursor-clock-value because it will be changed when we loadMore + ;; message + {:db (cond-> (assoc-in db [:pagination-info chat-id :all-loaded?] false) + (> clock-value cursor-clock-value) + ;;TODO cut older messages from messages-list + (update-in [:pagination-info chat-id] + assoc + :cursor (chat.loading/clock-value->cursor clock-value) + :cursor-clock-value clock-value))}) + acc))) + +(defn receive-many + [{:keys [db]} ^js response-js] + (let [messages-js ^js (.splice (.-messages response-js) 0 (if platform/low-device? 3 10)) + {:keys [db]} + (reduce reduce-js-messages + {:db db :chats #{} :senders {} :transactions #{}} + messages-js)] + ;;we want to render new messages as soon as possible so we dispatch later all other events which + ;;can be handled async + {:db db + :utils/dispatch-later + (concat [{:ms 20 :dispatch [:process-response response-js]}] + (when (and (:current-chat-id db) (= "active" (:app-state db))) + [{:ms 100 :dispatch [:chat/mark-all-as-read (:current-chat-id db)]}]))})) + +(rf/defn update-message-status + [{:keys [db]} chat-id message-id status] + (when (get-in db [:messages chat-id message-id]) + {:db (assoc-in db [:messages chat-id message-id :outgoing-status] status)})) + +(defn- update-db-delete-locally-without-time-limit + "Delete message in re-frame db, used to handle received removed-messages" + [db chat-id message-id deleted-by] + (when (get-in db [:messages chat-id message-id]) + (update-in db [:messages chat-id message-id] assoc :deleted? true :deleted-by deleted-by))) + +(rf/defn delete-messages-locally + "Mark messages :deleted? localy in client" + {:events [:chat.ui/delete-messages-localy]} + [{:keys [db]} messages chat-id] + (let [new-db (->> messages + (filter #(get-in db [:messages chat-id (:message-id %)])) + (reduce #(update-db-delete-locally-without-time-limit %1 + chat-id + (:message-id %2) + (:deleted-by %2)) + db))] + (when new-db + (message-list/rebuild-message-list {:db new-db} chat-id)))) + +(rf/defn handle-removed-messages + [cofx removed-messages] + (let [mark-as-deleted-fx (->> removed-messages + (map #(assoc % + :message-id (:messageId %) + :deleted-by (:deletedBy %))) + (group-by :chatId) + (mapv (fn [[chat-id messages]] + (delete-messages-locally messages chat-id)))) + mark-as-seen-fx (mapv + (fn [removed-message] + (let [chat-id (:chatId removed-message) + message-id (:messageId removed-message)] + (data-store.messages/mark-messages-seen chat-id + [message-id] + nil))) + removed-messages) + remove-messages-fx (fn [_cofx] + {:dispatch [:activity-center.notifications/fetch-unread-count]})] + (apply rf/merge + cofx + (-> mark-as-deleted-fx + (concat mark-as-seen-fx) + (conj remove-messages-fx))))) + +(defn remove-cleared-message + [messages cleared-at] + (into {} + (remove #(let [message-clock (:clock-value (second %))] + (<= message-clock cleared-at)) + messages))) + +(rf/defn handle-cleared-histories-messages + [{:keys [db]} cleared-histories] + {:db (reduce (fn [acc current] + (update-in acc + [:messages (:chatId current)] + remove-cleared-message + (:clearedAt current))) + db + cleared-histories)}) diff --git a/src/legacy/status_im/chat/models/message_test.cljs b/src/legacy/status_im/chat/models/message_test.cljs new file mode 100644 index 00000000000..e7ffed79591 --- /dev/null +++ b/src/legacy/status_im/chat/models/message_test.cljs @@ -0,0 +1,136 @@ +(ns legacy.status-im.chat.models.message-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [legacy.status-im.chat.models.loading :as loading] + [legacy.status-im.chat.models.message :as message] + [status-im.contexts.chat.messenger.messages.list.state :as list.state])) + +(deftest add-received-message-test + (with-redefs [message/add-message #(identity %1)] + (let [chat-id "chat-id" + clock-value 10 + cursor-clock-value (dec clock-value) + cursor (loading/clock-value->cursor cursor-clock-value) + cofx {:now 0 + :db {:current-chat-id chat-id + :pagination-info {chat-id {:messages-initialized? true + :cursor cursor + :cursor-clock-value cursor-clock-value}}}} + message #js + {:localChatId chat-id + :clock clock-value + :alias "alias" + :name "name" + :from "from"}] + ;; <- cursor <- message <- top of the chat + (testing "there's no hidden item" + (with-redefs [list.state/first-not-visible-item (atom nil)] + (is + (= + {:db + {:current-chat-id "chat-id" + :pagination-info + {"chat-id" + {:messages-initialized? true + :cursor + "00000000000000000000000000000000000000000000000000090x0000000000000000000000000000000000000000000000000000000000000000" + :cursor-clock-value 9}}}} + (dissoc (message/receive-many + cofx + #js {:messages (to-array [message])}) + :utils/dispatch-later))))) + ;; <- cursor <- first-hidden-item <- message <- top of the chat + (testing "the hidden item has a clock value less than the current" + (with-redefs [list.state/first-not-visible-item (atom {:clock-value (dec clock-value)})] + (is + (= + {:db + {:current-chat-id "chat-id" + :pagination-info + {"chat-id" + {:messages-initialized? true + :cursor + "00000000000000000000000000000000000000000000000000090x0000000000000000000000000000000000000000000000000000000000000000" + :cursor-clock-value 9}}}} + (dissoc (message/receive-many + cofx + #js {:messages (to-array [message])}) + :utils/dispatch-later))))) + ;; <- cursor <- message <- first-hidden-item <- top of the chat + (testing "the message falls between the first-hidden-item and cursor" + (with-redefs [list.state/first-not-visible-item (atom {:clock-value (inc clock-value)})] + (let [result (dissoc (message/receive-many + cofx + #js {:messages (to-array [message])}) + :utils/dispatch-later)] + (testing "it sets all-loaded? to false" + (is (not (get-in result [:db :pagination-info chat-id :all-loaded?])))) + (testing "it updates cursor-clock-value & cursor" + (is (= clock-value (get-in result [:db :pagination-info chat-id :cursor-clock-value]))) + (is (= (loading/clock-value->cursor clock-value) + (get-in result [:db :pagination-info chat-id :cursor]))))))) + ;; <- message <- first-hidden-item <- top of the chat + (testing "the message falls between the first-hidden-item and cursor is nil" + (with-redefs [list.state/first-not-visible-item (atom {:clock-value (inc clock-value)})] + (let [result (dissoc (message/receive-many + (update-in cofx + [:db :pagination-info chat-id] + dissoc + :cursor + :cursor-clock-value) + #js {:messages (to-array [message])}) + :utils/dispatch-later)] + (testing "it sets all-loaded? to false" + (is (not (get-in result [:db :pagination-info chat-id :all-loaded?])))) + (testing "it updates cursor-clock-value & cursor" + (is (= clock-value (get-in result [:db :pagination-info chat-id :cursor-clock-value]))) + (is (= (loading/clock-value->cursor clock-value) + (get-in result [:db :pagination-info chat-id :cursor]))))))) + ;; <- message <- cursor <- first-hidden-item <- top of the chat + (testing "the message falls before both the first-hidden-item and cursor" + (with-redefs [list.state/first-not-visible-item (atom {:clock-value (inc clock-value)})] + (let [message #js + {:localChatId chat-id + :clock (- clock-value 2) + :alias "alias" + :name "name" + :from "from"} + result (dissoc (message/receive-many + cofx + #js {:messages (to-array [message])}) + :utils/dispatch-later)] + (testing "it sets all-loaded? to false" + (is (not (get-in result [:db :pagination-info chat-id :all-loaded?])))) + (testing "it does not update cursor-clock-value & cursor" + (is (= cursor-clock-value + (get-in result [:db :pagination-info chat-id :cursor-clock-value]))) + (is (= cursor (get-in result [:db :pagination-info chat-id :cursor])))))))))) + +(deftest message-loaded?-test + (testing "it returns false when it's not in loaded message" + (is + (not + (#'legacy.status-im.chat.models.message/message-loaded? {:messages {"a" {}}} "a" "message-id")))) + (testing "it returns true when it's already in the loaded message" + (is (#'legacy.status-im.chat.models.message/message-loaded? + {:messages {"a" {"message-id" {}}}} + "a" + "message-id")))) + +(deftest earlier-than-deleted-at?-test + (testing "it returns true when the clock-value is the same as the deleted-clock-value in chat" + (is (#'legacy.status-im.chat.models.message/earlier-than-deleted-at? + {:chats {"a" {:deleted-at-clock-value 1}}} + "a" + 1))) + (testing "it returns false when the clock-value is greater than the deleted-clock-value in chat" + (is (not (#'legacy.status-im.chat.models.message/earlier-than-deleted-at? + {:chats {"a" {:deleted-at-clock-value 1}}} + "a" + 2)))) + + (testing "it returns true when the clock-value is less than the deleted-clock-value in chat" + (is (#'legacy.status-im.chat.models.message/earlier-than-deleted-at? + {:chats {"a" {:deleted-at-clock-value 1}}} + "a" + 0)))) diff --git a/src/legacy/status_im/communities/core.cljs b/src/legacy/status_im/communities/core.cljs new file mode 100644 index 00000000000..1d91883969a --- /dev/null +++ b/src/legacy/status_im/communities/core.cljs @@ -0,0 +1,110 @@ +(ns legacy.status-im.communities.core + (:require + [clojure.set :as set] + legacy.status-im.communities.e2e + [re-frame.core :as re-frame] + [status-im.contexts.shell.activity-center.events :as activity-center] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn <-request-to-join-community-rpc + [r] + (set/rename-keys r + {:communityId :community-id + :publicKey :public-key + :chatId :chat-id})) + +(defn <-requests-to-join-community-rpc + [requests key-fn] + (reduce (fn [acc r] + (assoc acc (key-fn r) (<-request-to-join-community-rpc r))) + {} + requests)) + +(rf/defn handle-response + [_ response-js] + {:dispatch [:sanitize-messages-and-process-response response-js]}) + +(rf/defn people-invited + {:events [::people-invited]} + [cofx response-js] + (rf/merge cofx + (navigation/navigate-back nil) + (handle-response response-js))) + +(rf/defn member-banned + {:events [::member-banned]} + [cofx response-js] + (rf/merge cofx + (navigation/hide-bottom-sheet) + (handle-response response-js) + (activity-center/notifications-fetch-unread-count))) + +(rf/defn member-ban + {:events [::member-ban]} + [_cofx community-id public-key] + {:json-rpc/call [{:method "wakuext_banUserFromCommunity" + :params [{:communityId community-id + :user public-key}] + :js-response true + :on-success #(re-frame/dispatch [::member-banned %]) + :on-error #(log/error "failed to ban user from community" + community-id + public-key + %)}]}) + +(rf/defn member-kicked + {:events [::member-kicked]} + [cofx response-js] + (rf/merge cofx + (navigation/hide-bottom-sheet) + (handle-response response-js))) + +(rf/defn member-kick + {:events [::member-kick]} + [_cofx community-id public-key] + {:json-rpc/call [{:method "wakuext_removeUserFromCommunity" + :params [community-id public-key] + :js-response true + :on-success #(re-frame/dispatch [::member-kicked %]) + :on-error #(log/error "failed to remove user from community" + community-id + public-key + %)}]}) + +(rf/defn requests-to-join-fetched + {:events [::requests-to-join-fetched]} + [{:keys [db]} community-id requests] + {:db (assoc-in db + [:communities/requests-to-join community-id] + (<-requests-to-join-community-rpc requests :id))}) + +(rf/defn fetch-requests-to-join + {:events [:community/fetch-requests-to-join]} + [_ community-id] + {:json-rpc/call [{:method "wakuext_pendingRequestsToJoinForCommunity" + :params [community-id] + :on-success #(re-frame/dispatch [::requests-to-join-fetched community-id %]) + :on-error #(log/error "failed to fetch requests-to-join" community-id %)}]}) + +(rf/defn member-role-updated + {:events [:community.member/role-updated]} + [cofx response-js] + (rf/merge cofx + (navigation/hide-bottom-sheet) + (handle-response response-js))) + +(rf/defn add-role-to-member + {:events [:community.member/add-role]} + [_cofx community-id public-key role-id] + {:json-rpc/call [{:method "wakuext_addRoleToMember" + :params [{:communityId community-id + :user public-key + :role role-id}] + :on-success #(re-frame/dispatch [:community.member/role-updated %]) + :on-error #(log/error "failed to add role to member" + {:error % + :community-id community-id + :public-key public-key + :role-id role-id})}]}) diff --git a/src/legacy/status_im/communities/e2e.cljs b/src/legacy/status_im/communities/e2e.cljs new file mode 100644 index 00000000000..ca6d1987353 --- /dev/null +++ b/src/legacy/status_im/communities/e2e.cljs @@ -0,0 +1,35 @@ +(ns legacy.status-im.communities.e2e + (:require [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +;;NOTE: ONLY FOR QA + +(rf/defn create-closed-community + {:events [:fast-create-community/create-closed-community]} + [_] + {:json-rpc/call [{:method "wakuext_createClosedCommunity" + :params [] + :js-response true + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(log/error "failed to create closed community." {:error %})}] + :dispatch [:hide-bottom-sheet]}) + +(rf/defn create-open-community + {:events [:fast-create-community/create-open-community]} + [_] + {:json-rpc/call [{:method "wakuext_createOpenCommunity" + :params [] + :js-response true + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(log/error "failed to create open community." {:error %})}] + :dispatch [:hide-bottom-sheet]}) + +(rf/defn create-token-gated-community + {:events [:fast-create-community/create-token-gated-community]} + [_] + {:json-rpc/call [{:method "wakuext_createTokenGatedCommunity" + :params [] + :js-response true + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(log/error "failed to create token gated community." {:error %})}] + :dispatch [:hide-bottom-sheet]}) diff --git a/src/legacy/status_im/data_store/activities.cljs b/src/legacy/status_im/data_store/activities.cljs new file mode 100644 index 00000000000..c0ee319c28a --- /dev/null +++ b/src/legacy/status_im/data_store/activities.cljs @@ -0,0 +1,102 @@ +(ns legacy.status-im.data-store.activities + (:require + [clojure.set :as set] + [clojure.string :as string] + [legacy.status-im.data-store.messages :as messages] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.notification-types :as notification-types])) + +(defn mark-notifications-as-read + [notifications] + (map #(assoc % :read true) notifications)) + +(defn pending-contact-request? + [contact-id {:keys [type author]}] + (and (= type notification-types/contact-request) + (= contact-id author))) + +(defn parse-notification-counts-response + [response] + (reduce-kv (fn [acc k count-number] + (let [maybe-type (js/parseInt (name k) 10)] + (if (notification-types/all-supported maybe-type) + (assoc acc maybe-type count-number) + acc))) + {} + response)) + +(defn- rpc->type + [{:keys [type name] :as chat}] + (condp = type + notification-types/reply + (assoc chat + :chat-name name + :chat-type constants/private-group-chat-type) + + notification-types/mention + (assoc chat + :chat-type constants/private-group-chat-type + :chat-name name) + + notification-types/private-group-chat + (assoc chat + :chat-type constants/private-group-chat-type + :chat-name name + :public? false + :group-chat true) + + notification-types/one-to-one-chat + (assoc chat + :chat-type constants/one-to-one-chat-type + :chat-name name + :public? false + :group-chat false) + + chat)) + +(defn strip-html + [html] + (-> html + ;; Replace block-level tags with newlines + (clojure.string/replace #"]*>" "\n") + ;; Remove all remaining tags + (clojure.string/replace #"<[^>]+>" "") + ;; Decode common HTML entities + (clojure.string/replace #" " " ") + (clojure.string/replace #"&" "&") + ;; Collapse multiple newlines into one + (clojure.string/replace #"\n+" "\n") + ;; Trim spaces at start/end of lines + (clojure.string/replace #"[ \t]+\n" "\n") + (clojure.string/replace #"\n[ \t]+" "\n") + clojure.string/trim)) + +(defn <-rpc + [item] + (-> item + rpc->type + (set/rename-keys {:lastMessage :last-message + :replyMessage :reply-message + :chatId :chat-id + :contactVerificationStatus :contact-verification-status + :communityId :community-id + :installationId :installation-id + :membershipStatus :membership-status + :albumMessages :album-messages + :newsImageUrl :news-image-url + :newsTitle :news-title + :newsDescription :news-description + :newsContent :news-content + :newsLink :news-link + :newsLinkLabel :news-link-label}) + (update :last-message #(when % (messages/<-rpc %))) + (update :message #(when % (messages/<-rpc %))) + (update :reply-message #(when % (messages/<-rpc %))) + (update :news-description #(when % (strip-html %))) + (update :news-title #(when % (strip-html %))) + (update :news-content #(when % (strip-html %))) + (dissoc :chatId))) + +(defn <-rpc-seen-state + [item] + (:hasSeen item)) diff --git a/src/legacy/status_im/data_store/activities_test.cljs b/src/legacy/status_im/data_store/activities_test.cljs new file mode 100644 index 00000000000..b1f6a0408d1 --- /dev/null +++ b/src/legacy/status_im/data_store/activities_test.cljs @@ -0,0 +1,142 @@ +(ns legacy.status-im.data-store.activities-test + (:require + [cljs.test :refer [deftest is testing]] + [legacy.status-im.data-store.activities :as store] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.notification-types :as notification-types])) + +(def chat-id + "0x04c66155") + +(def chat-name + "0x04c661") + +(def raw-notification + {:chatId chat-id + :contactVerificationStatus constants/contact-verification-status-pending + :lastMessage {} + :name chat-name + :replyMessage {}}) + +(deftest <-rpc-test + (testing "renames keys" + (is + (= {:name chat-name + :chat-id chat-id + :contact-verification-status constants/contact-verification-status-pending} + (-> + raw-notification + store/<-rpc + (dissoc :last-message :message :reply-message :news-title :news-content :news-description))))) + + (testing "transforms messages from RPC response" + (is + (= {:last-message {:quoted-message nil + :outgoing-status nil + :command-parameters nil + :link-previews [] + :status-link-previews [] + :content {:sticker nil + :rtl? nil + :ens-name nil + :parsed-text nil + :response-to nil + :chat-id nil + :image nil + :line-count nil + :links nil + :text nil} + :bridge-message nil + :outgoing false} + :message nil + :reply-message {:quoted-message nil + :outgoing-status nil + :command-parameters nil + :link-previews [] + :status-link-previews [] + :content {:sticker nil + :rtl? nil + :ens-name nil + :parsed-text nil + :response-to nil + :chat-id nil + :image nil + :line-count nil + :links nil + :text nil} + :bridge-message nil + :outgoing false}} + (-> raw-notification + store/<-rpc + (select-keys [:last-message :message :reply-message]))))) + + (testing "augments notification based on its type" + (is (= {:chat-name chat-name + :chat-type constants/private-group-chat-type + :name chat-name} + (-> raw-notification + (assoc :type notification-types/reply) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))) + + (is (= {:chat-name chat-name + :chat-type constants/private-group-chat-type + :name chat-name} + (-> raw-notification + (assoc :type notification-types/mention) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))) + + (is (= {:chat-name chat-name + :chat-type constants/private-group-chat-type + :group-chat true + :name chat-name + :public? false} + (-> raw-notification + (assoc :type notification-types/private-group-chat) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))) + + (is (= {:chat-name chat-name + :chat-type constants/one-to-one-chat-type + :group-chat false + :name chat-name + :public? false} + (-> raw-notification + (assoc :type notification-types/one-to-one-chat) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))))) + +(deftest remove-pending-contact-request-test + (is (true? (store/pending-contact-request? + "contact-id" + {:type notification-types/contact-request + :author "contact-id"}))) + (is (false? (store/pending-contact-request? + "contact-id" + {:type notification-types/contact-request + :author "contactzzzz"})))) + +(deftest parse-notification-counts-response-test + (is + (= {notification-types/one-to-one-chat 15 + notification-types/private-group-chat 16 + notification-types/mention 17 + notification-types/reply 18 + notification-types/contact-request 19 + notification-types/admin 20 + notification-types/contact-verification 21 + notification-types/new-installation-received 22 + notification-types/new-installation-created 23} + (store/parse-notification-counts-response + {(keyword (str notification-types/one-to-one-chat)) 15 + (keyword (str notification-types/private-group-chat)) 16 + (keyword (str notification-types/mention)) 17 + (keyword (str notification-types/reply)) 18 + (keyword (str notification-types/contact-request)) 19 + (keyword (str notification-types/admin)) 20 + (keyword (str notification-types/contact-verification)) 21 + (keyword (str notification-types/new-installation-received)) 22 + (keyword (str notification-types/new-installation-created)) 23 + ;; Unsupported type in the response is ignored + :999 100})))) diff --git a/src/legacy/status_im/data_store/chats.cljs b/src/legacy/status_im/data_store/chats.cljs new file mode 100644 index 00000000000..392699d0591 --- /dev/null +++ b/src/legacy/status_im/data_store/chats.cljs @@ -0,0 +1,142 @@ +(ns legacy.status-im.data-store.chats + (:require + [clojure.set :as set] + [clojure.string :as string] + [legacy.status-im.data-store.messages :as messages] + [legacy.status-im.utils.deprecated-types :as types] + [re-frame.core :as re-frame] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.constants :as constants] + [taoensso.timbre :as log])) + +(defn rpc->type + [{:keys [chat-type name] :as chat}] + (cond + (or (= constants/profile-chat-type chat-type) + (= constants/timeline-chat-type chat-type)) + (assoc chat + :chat-name (str "#" name) + :public? true + :group-chat true + :timeline? (= constants/timeline-chat-type chat-type)) + (= constants/community-chat-type chat-type) (assoc chat + :chat-name name + :group-chat true) + (= constants/private-group-chat-type chat-type) (assoc chat + :chat-name name + :public? false + :group-chat true) + :else (assoc chat :public? false :group-chat false))) + +(defn members-reducer + [acc member] + (cond-> acc + (:admin member) + (update :admins conj (:id member)) + (:joined member) + (update :members-joined conj (:id member)) + :always + (update :contacts conj (:id member)))) + +(defn community-chat-id->channel-id [chat-id] (subs chat-id constants/community-id-length)) + +(defn decode-chat-id + [chat-id] + (let [community-id (subs chat-id 0 constants/community-id-length) + channel-id (community-chat-id->channel-id chat-id)] + {:community-id community-id + :channel-id channel-id})) + +(defn- unmarshal-members + [{:keys [members chat-type] :as chat}] + (cond + (= constants/private-group-chat-type chat-type) (merge chat + (reduce members-reducer + {:admins #{} + :members-joined #{} + :contacts #{}} + members)) + :else + (assoc chat + :contacts #{(:id chat)} + :admins #{} + :members-joined #{}))) + +(defn <-color + [value] + (if (and (some? value) + (string/starts-with? value "#")) + value + (keyword value))) + +(defn <-rpc + [chat] + (-> chat + (set/rename-keys {:id :chat-id + :communityId :community-id + :syncedFrom :synced-from + :syncedTo :synced-to + :membershipUpdateEvents :membership-update-events + :deletedAtClockValue :deleted-at-clock-value + :chatType :chat-type + :unviewedMessagesCount :unviewed-messages-count + :unviewedMentionsCount :unviewed-mentions-count + :lastMessage :last-message + :lastClockValue :last-clock-value + :invitationAdmin :invitation-admin + :profile :profile-public-key + :muteTill :muted-till + :hideIfPermissionsNotMet :hide-if-permissions-not-met?}) + rpc->type + unmarshal-members + (update :last-message #(when % (messages/<-rpc %))) + (update :color <-color) + (dissoc :members))) + +(defn <-rpc-js + [^js chat] + (-> {:name (.-name chat) + :description (.-description chat) + :color (<-color (.-color chat)) + :emoji (.-emoji chat) + :timestamp (.-timestamp chat) + :alias (.-alias chat) + :muted (.-muted chat) + :joined (.-joined chat) + :muted-till (.-muteTill chat) + :chat-id (.-id chat) + :community-id (.-communityId chat) + :synced-from (.-syncedFrom chat) + :synced-to (.-syncedTo chat) + :deleted-at-clock-value (.-deletedAtClockValue chat) + :chat-type (.-chatType chat) + :unviewed-messages-count (.-unviewedMessagesCount chat) + :unviewed-mentions-count (.-unviewedMentionsCount chat) + :last-message {:content {:text (.-text chat) + :parsed-text (types/js->clj (.-parsedText + chat)) + :response-to (.-responseTo chat)} + :content-type (.-contentType chat) + :community-id (.-contentCommunityId chat) + :outgoing (boolean (.-outgoingStatus chat)) + :album-images-count (.-albumImagesCount chat) + :from (.-from chat) + :deleted? (.-deleted chat) + :deleted-for-me? (.-deletedForMe chat)} + :last-clock-value (.-lastClockValue chat) + :profile-public-key (.-profile chat) + :highlight (.-highlight chat) + :active (.-active chat) + :image (.-image chat) + :members (types/js->clj (.-members chat)) + :hide-if-permissions-not-met (.-hideIfPermissionsNotMet chat)} + rpc->type + unmarshal-members)) + +(re-frame/reg-fx :fetch-chats-preview + (fn [{:keys [on-success chat-preview-type]}] + (json-rpc/call {:method "wakuext_chatsPreview" + :params [chat-preview-type] + :js-response true + :on-success #(on-success ^js %) + :on-error #(log/error "failed to fetch chats" 0 -1 %)}))) diff --git a/src/legacy/status_im/data_store/chats_test.cljs b/src/legacy/status_im/data_store/chats_test.cljs new file mode 100644 index 00000000000..5d8a9c22463 --- /dev/null +++ b/src/legacy/status_im/data_store/chats_test.cljs @@ -0,0 +1,51 @@ +(ns legacy.status-im.data-store.chats-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [legacy.status-im.data-store.chats :as chats])) + +(deftest normalize-chat-test + (let [chat {:id "chat-id" + :color "yellow" + :name "name" + :chatType 3 + :members [{:id "a" + :admin true + :joined true} + {:id "b" + :admin true + :joined false} + {:id "c" + :admin false + :joined true} + {:id "d" + :admin false + :joined false}] + :lastClockValue 10 + :membershipUpdateEvents :events + :unviewedMessagesCount 2 + :timestamp 2} + expected-chat {:public? false + :group-chat true + :color :yellow + :chat-name "name" + :contacts #{"a" "b" "c" "d"} + :chat-type 3 + :last-clock-value 10 + :last-message nil + :admins #{"a" "b"} + :members-joined #{"a" "c"} + :name "name" + :membership-update-events :events + :unviewed-messages-count 2 + :chat-id "chat-id" + :timestamp 2}] + (testing "from-rpc" + (is (= expected-chat (chats/<-rpc chat)))))) + +(deftest decode-chat-id-test + (let [community-id "0x0322b9b84acb1631d6a31d41d9cf8e1938d352a624bc130de92869e025c7ca79c4" + channel-id "02081bc7-20e4-4362-99e2-37669c28cc08" + chat-id (str community-id channel-id)] + (is (= {:community-id community-id + :channel-id channel-id} + (chats/decode-chat-id chat-id))))) diff --git a/src/legacy/status_im/data_store/communities.cljs b/src/legacy/status_im/data_store/communities.cljs new file mode 100644 index 00000000000..1a38fb6bcc3 --- /dev/null +++ b/src/legacy/status_im/data_store/communities.cljs @@ -0,0 +1,107 @@ +(ns legacy.status-im.data-store.communities + (:require + [clojure.set :as set] + [status-im.constants :as constants] + [utils.transforms :as transforms])) + +(defn <-request-to-join-community-rpc + [r] + (set/rename-keys r + {:communityId :community-id + :publicKey :public-key + :chatId :chat-id})) + +(defn <-requests-to-join-community-rpc + [requests key-fn] + (reduce #(assoc %1 (key-fn %2) (<-request-to-join-community-rpc %2)) {} requests)) + +(defn <-chats-rpc + "This transformation from RPC is optimized differently because there can be + thousands of members in all chats and we don't want to transform them from JS + to CLJS because they will only be used to list community members or community + chat members." + [chats-js] + (let [chat-key-fn (fn [k] + (case k + "tokenGated" :token-gated? + "canPost" :can-post? + "canView" :can-view? + "hideIfPermissionsNotMet" :hide-if-permissions-not-met? + (keyword k))) + chat-val-fn (fn [k v] + (if (= "members" k) + v + (transforms/js->clj v)))] + (transforms/<-js-map + chats-js + {:val-fn (fn [_ v] + (transforms/<-js-map v {:key-fn chat-key-fn :val-fn chat-val-fn}))}))) + +(defn <-categories-rpc + [categ] + (reduce-kv #(assoc %1 (name %2) %3) {} categ)) + +(defn role-permission? + [token-permission] + (contains? constants/community-role-permissions (:type token-permission))) + +(defn membership-permission? + [token-permission] + (= (:type token-permission) constants/community-token-permission-become-member)) + +(defn- rename-community-key + [k] + (case k + "canRequestAccess" :can-request-access? + "canManageUsers" :can-manage-users? + "canDeleteMessageForEveryone" :can-delete-message-for-everyone? + ;; This flag is misleading based on its name alone + ;; because it should not be used to decide if the user + ;; is *allowed* to join. Allowance is based on token + ;; permissions. Still, the flag can be used to know + ;; whether or not the user will have to wait until an + ;; admin approves a join request. + "canJoin" :can-join? + "requestedToJoinAt" :requested-to-join-at + "isMember" :is-member? + "outroMessage" :outro-message + "adminSettings" :admin-settings + "tokenPermissions" :token-permissions + "communityTokensMetadata" :tokens-metadata + "introMessage" :intro-message + "muteTill" :muted-till + "lastOpenedAt" :last-opened-at + "joinedAt" :joined-at + (keyword k))) + +(defn <-rpc + [community-js] + (let [community (transforms/<-js-map + community-js + {:key-fn rename-community-key + :val-fn (fn [k v] + (case k + "members" v + "chats" (<-chats-rpc v) + (transforms/js->clj v)))})] + (-> community + (update :admin-settings + set/rename-keys + {:pinMessageAllMembersEnabled :pin-message-all-members-enabled?}) + (update :token-permissions seq) + (update :categories <-categories-rpc) + (assoc :role-permissions? + (->> community + :token-permissions + vals + (some role-permission?))) + (assoc :membership-permissions? + (->> community + :token-permissions + vals + (some membership-permission?))) + (assoc :token-images + (reduce (fn [acc {sym :symbol image :image}] + (assoc acc sym image)) + {} + (:tokens-metadata community)))))) diff --git a/src/legacy/status_im/data_store/invitations.cljs b/src/legacy/status_im/data_store/invitations.cljs new file mode 100644 index 00000000000..464492987fa --- /dev/null +++ b/src/legacy/status_im/data_store/invitations.cljs @@ -0,0 +1,10 @@ +(ns legacy.status-im.data-store.invitations + (:require + clojure.set)) + +(defn <-rpc + [message] + (-> message + (clojure.set/rename-keys {:chatId :chat-id + :introductionMessage :introduction-message + :messageType :message-type}))) diff --git a/src/legacy/status_im/data_store/messages.cljs b/src/legacy/status_im/data_store/messages.cljs new file mode 100644 index 00000000000..7a20d0b63fe --- /dev/null +++ b/src/legacy/status_im/data_store/messages.cljs @@ -0,0 +1,176 @@ +(ns legacy.status-im.data-store.messages + (:require + [clojure.set :as set] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn ->rpc + [{:keys [content] :as message}] + (cond-> message + content + (assoc :text (:text content) + :sticker (:sticker content)) + :always + (set/rename-keys {:chat-id :chat_id + :whisper-timestamp :whisperTimestamp + :community-id :communityId + :clock-value :clock}))) + +(defn <-status-link-previews-rpc + [preview] + (-> preview + (update :community + set/rename-keys + {:communityId :community-id + :displayName :display-name + :membersCount :members-count + :activeMembersCount :active-members-count}) + (update-in [:community :banner] set/rename-keys {:dataUri :data-uri}) + (update-in [:community :icon] set/rename-keys {:dataUri :data-uri}))) + +(defn ->status-link-previews-rpc + [preview] + (-> preview + (update :community + set/rename-keys + {:community-id :communityId + :display-name :displayName + :members-count :membersCount + :active-members-count :activeMembersCount}) + (update-in [:community :banner] set/rename-keys {:data-uri :dataUri}) + (update-in [:community :icon] set/rename-keys {:data-uri :dataUri}))) + +(defn <-link-preview-rpc + [preview] + (-> preview + (update :thumbnail set/rename-keys {:dataUri :data-uri}) + (update :favicon set/rename-keys {:dataUri :data-uri}))) + +(defn ->link-preview-rpc + [preview] + (-> preview + (update :thumbnail set/rename-keys {:data-uri :dataUri}) + (update :favicon set/rename-keys {:data-uri :dataUri}))) + +(defn <-rpc + [message] + (-> message + (set/rename-keys + {:id :message-id + :whisperTimestamp :whisper-timestamp + :compressedKey :compressed-key + :editedAt :edited-at + :contactVerificationState :contact-verification-state + :contactRequestState :contact-request-state + :commandParameters :command-parameters + :gapParameters :gap-parameters + :messageType :message-type + :localChatId :chat-id + :communityId :community-id + :contentType :content-type + :clock :clock-value + :quotedMessage :quoted-message + :outgoingStatus :outgoing-status + :audioDurationMs :audio-duration-ms + :deleted :deleted? + :deletedForMe :deleted-for-me? + :deletedBy :deleted-by + :albumId :album-id + :imageWidth :image-width + :imageHeight :image-height + :new :new? + :albumImagesCount :album-images-count + :displayName :display-name + :linkPreviews :link-previews + :statusLinkPreviews :status-link-previews + :bridgeMessage :bridge-message}) + (update :bridge-message + set/rename-keys + {:bridgeName :bridge-name + :userName :user-name + :userAvatar :user-avatar}) + (update :link-previews #(map <-link-preview-rpc %)) + (update :status-link-previews #(map <-status-link-previews-rpc %)) + (update :quoted-message + set/rename-keys + {:parsedText :parsed-text + :deleted :deleted? + :deletedForMe :deleted-for-me? + :communityId :community-id + :albumImagesCount :album-images-count + :bridgeMessage :bridge-message}) + (update :outgoing-status keyword) + (update :command-parameters + set/rename-keys + {:transactionHash :transaction-hash + :commandState :command-state}) + (assoc :content {:chat-id (:chatId message) + :text (:text message) + :image (:image message) + :sticker (:sticker message) + :ens-name (:ensName message) + :line-count (:lineCount message) + :parsed-text (:parsedText message) + :links (:links message) + :rtl? (:rtl message) + :response-to (:responseTo message)} + :outgoing (boolean (:outgoingStatus message))) + (dissoc :ensName :chatId :text :rtl :responseTo :image :sticker :lineCount :parsedText :links))) + +(defn messages-by-chat-id-rpc + [chat-id + cursor + limit + on-success + on-error] + {:json-rpc/call [{:method "wakuext_chatMessages" + :params [chat-id cursor limit] + :on-success (fn [result] + (on-success (update result :messages #(map <-rpc %)))) + :on-error on-error}]}) + +(defn mark-seen-rpc + [chat-id ids on-success] + {:json-rpc/call [{:method "wakuext_markMessagesSeen" + :params [chat-id ids] + :on-success #(do + (log/debug "successfully marked as seen" %) + (when on-success (on-success chat-id ids %))) + :on-error #(log/error "failed to get messages" %)}]}) + +(defn delete-message-rpc + [id] + {:json-rpc/call [{:method "wakuext_deleteMessage" + :params [id] + :on-success #(log/debug "successfully deleted message" id) + :on-error #(log/error "failed to delete message" % id)}]}) + +(defn delete-messages-from-rpc + [author] + {:json-rpc/call [{:method "wakuext_deleteMessagesFrom" + :params [author] + :on-success #(log/debug "successfully deleted messages from" author) + :on-error #(log/error "failed to delete messages from" % author)}]}) + +(defn delete-messages-by-chat-id-rpc + [chat-id] + {:json-rpc/call [{:method "wakuext_deleteMessagesByChatID" + :params [chat-id] + :on-success #(log/debug "successfully deleted messages by chat-id" chat-id) + :on-error #(log/error "failed to delete messages by chat-id" % chat-id)}]}) + +(rf/defn delete-message + [_cofx id] + (delete-message-rpc id)) + +(rf/defn delete-messages-from + [_cofx author] + (delete-messages-from-rpc author)) + +(rf/defn mark-messages-seen + [_cofx chat-id ids on-success] + (mark-seen-rpc chat-id ids on-success)) + +(rf/defn delete-messages-by-chat-id + [_cofx chat-id] + (delete-messages-by-chat-id-rpc chat-id)) diff --git a/src/legacy/status_im/data_store/messages_test.cljs b/src/legacy/status_im/data_store/messages_test.cljs new file mode 100644 index 00000000000..51ef3fc7f31 --- /dev/null +++ b/src/legacy/status_im/data_store/messages_test.cljs @@ -0,0 +1,90 @@ +(ns legacy.status-im.data-store.messages-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [legacy.status-im.data-store.messages :as m])) + +(def message-id "0xfe96d03da2159e632a6653d04028b0de8b55f78f03521b26ce10dc5f48a16aee") +(def chat-id "chat-id") +(def from + "0x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1") + +(deftest message<-rpc-test + (testing "message from RPC" + (let [expected {:message-id message-id + :content {:chat-id chat-id + :sticker {:hash "hash" :pack 1} + :text "hta" + :line-count 1 + :ens-name "ens-name" + :parsed-text "parsed-text" + :rtl? false + :image nil + :response-to "a" + :links nil} + :bridge-message nil + :whisper-timestamp 1 + :contact-verification-state 1 + :contact-request-state 2 + :outgoing-status :sending + :command-parameters nil + :outgoing true + :message-type 0 + :clock-value 2 + :from from + :chat-id chat-id + :quoted-message {:from "from" + :text "reply"} + :content-type 1 + :compressed-key "c" + :timestamp 3 + :link-previews [{:thumbnail {:url "http://localhost" + :data-uri "data:image/png"} + :favicon nil}] + :status-link-previews [{:url "http://localhost" + :community {:community-id "0x01" + :display-name "Test Comm" + :members-count 12 + :active-members-count 12 + :banner + {:data-uri + "data:image/png"} + :icon + {:data-uri + "data:image/png"}}}]} + message {:id message-id + :whisperTimestamp 1 + :parsedText "parsed-text" + :ensName "ens-name" + :contactVerificationState 1 + :contactRequestState 2 + :localChatId chat-id + :from from + :text "hta" + :rtl false + :chatId chat-id + :lineCount 1 + :sticker {:hash "hash" :pack 1} + :contentType 1 + :messageType 0 + :clock 2 + :responseTo "a" + :compressedKey "c" + :quotedMessage {:from "from" + :text "reply"} + :timestamp 3 + :outgoingStatus "sending" + :linkPreviews [{:thumbnail {:url "http://localhost" + :dataUri "data:image/png"} + :favicon nil}] + :statusLinkPreviews [{:url "http://localhost" + :community {:communityId "0x01" + :displayName "Test Comm" + :membersCount 12 + :activeMembersCount 12 + :banner + {:dataUri + "data:image/png"} + :icon + {:dataUri + "data:image/png"}}}]}] + (is (= expected (m/<-rpc message)))))) diff --git a/src/legacy/status_im/data_store/pin_messages.cljs b/src/legacy/status_im/data_store/pin_messages.cljs new file mode 100644 index 00000000000..d90103bf491 --- /dev/null +++ b/src/legacy/status_im/data_store/pin_messages.cljs @@ -0,0 +1,37 @@ +(ns legacy.status-im.data-store.pin-messages + (:require + [clojure.set :as set] + [legacy.status-im.data-store.messages :as messages] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn <-rpc + [message] + (-> message + (merge (messages/<-rpc (message :message))) + (set/rename-keys {:pinnedAt :pinned-at + :pinnedBy :pinned-by}) + (dissoc :message))) + +(defn pinned-message-by-chat-id-rpc + [chat-id + cursor + limit + on-success + on-error] + {:json-rpc/call [{:method "wakuext_chatPinnedMessages" + :params [chat-id cursor limit] + :on-success (fn [result] + (let [result (set/rename-keys result + {:pinnedMessages + :pinned-messages})] + (on-success (update result :pinned-messages #(map <-rpc %))))) + :on-error on-error}]}) + +(rf/defn send-pin-message + [_cofx pin-message] + {:json-rpc/call [{:method "wakuext_sendPinMessage" + :params [(messages/->rpc pin-message)] + :js-response true + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(log/error "failed to pin message" %)}]}) diff --git a/src/legacy/status_im/data_store/settings.cljs b/src/legacy/status_im/data_store/settings.cljs new file mode 100644 index 00000000000..ac148ba7b60 --- /dev/null +++ b/src/legacy/status_im/data_store/settings.cljs @@ -0,0 +1,48 @@ +(ns legacy.status-im.data-store.settings + (:require + [clojure.set :as set] + [clojure.string :as string] + [legacy.status-im.data-store.visibility-status-updates :as visibility-status-updates] + [status-im.contexts.wallet.common.validation :as validation] + [utils.ethereum.eip.eip55 :as eip55])) + +(defn rpc->visible-tokens + [visible-tokens] + (reduce-kv (fn [acc chain visible-tokens] + (assoc acc chain (into #{} (map keyword visible-tokens)))) + {} + visible-tokens)) + +(defn rpc->pinned-mailservers + [pinned-mailservers] + (reduce-kv (fn [acc chain pinned-mailserver] + (assoc acc chain (keyword pinned-mailserver))) + {} + pinned-mailservers)) + +(defn rpc->currency + [currency] + (-> currency + (name) + (string/lower-case) + (keyword))) + +(defn rpc->settings + [settings] + (-> settings + (update :dapps-address eip55/address->checksum) + (update :address eip55/address->checksum) + (update :wallet-legacy/visible-tokens rpc->visible-tokens) + (update :pinned-mailservers rpc->pinned-mailservers) + (update :link-previews-enabled-sites set) + (update :currency rpc->currency) + (assoc :ens-name? (validation/ens-name? (:preferred-name settings))) + (visibility-status-updates/<-rpc-settings) + (set/rename-keys {:compressedKey :compressed-key + :emojiHash :emoji-hash}))) + +(defn rpc->setting-value + [{:keys [name] :as setting}] + (condp = name + :currency (update setting :value rpc->currency) + setting)) diff --git a/src/legacy/status_im/data_store/visibility_status_updates.cljs b/src/legacy/status_im/data_store/visibility_status_updates.cljs new file mode 100644 index 00000000000..e537664c977 --- /dev/null +++ b/src/legacy/status_im/data_store/visibility_status_updates.cljs @@ -0,0 +1,30 @@ +(ns legacy.status-im.data-store.visibility-status-updates + (:require + [clojure.set :as set] + [re-frame.core :as re-frame] + [status-im.common.json-rpc.events :as json-rpc] + [taoensso.timbre :as log])) + +(defn <-rpc + [visibility-status-update] + (set/rename-keys visibility-status-update + {:publicKey :public-key + :statusType :status-type})) + +(defn <-rpc-settings + [settings] + (-> settings + (set/rename-keys + {:current-user-status :current-user-visibility-status}) + (update :current-user-visibility-status <-rpc))) + +(re-frame/reg-fx :visibility-status-updates/fetch + (fn [] + (json-rpc/call {:method "wakuext_statusUpdates" + :params [] + :on-success #(re-frame/dispatch + [:visibility-status-updates/visibility-status-updates-loaded + (:statusUpdates ^js %)]) + :on-error #(log/error + "failed to fetch visibility-status-updates" + %)}))) diff --git a/src/legacy/status_im/ens/core.cljs b/src/legacy/status_im/ens/core.cljs new file mode 100644 index 00000000000..9660311f3b9 --- /dev/null +++ b/src/legacy/status_im/ens/core.cljs @@ -0,0 +1,366 @@ +(ns legacy.status-im.ens.core + (:refer-clojure :exclude [name]) + (:require + [clojure.set :as set] + [clojure.string :as string] + [legacy.status-im.ethereum.ens :as ens] + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [legacy.status-im.utils.random :as random] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.datetime :as datetime] + [utils.ens.core :as utils.ens] + [utils.ens.stateofus :as stateofus] + [utils.ethereum.chain :as chain] + [utils.ethereum.eip.eip55 :as eip55] + [utils.re-frame :as rf])) + +(defn fullname + [custom-domain? username] + (if custom-domain? + username + (stateofus/subdomain username))) + +(re-frame/reg-fx + ::resolve-address + (fn [[chain-id name cb]] + (ens/address chain-id name cb))) + +(re-frame/reg-fx + ::resolve-owner + (fn [[chain-id name cb]] + (ens/owner chain-id name cb))) + +(re-frame/reg-fx + ::resolve-pubkey + (fn [[chain-id name cb]] + (ens/pubkey chain-id name cb))) + +(re-frame/reg-fx + ::get-expiration-time + (fn [[chain-id name cb]] + (ens/expire-at chain-id name cb))) + +(rf/defn update-ens-tx-state + {:events [:ens/update-ens-tx-state]} + [{:keys [db]} new-state username custom-domain? tx-hash] + {:db (assoc-in db + [:ens/registrations tx-hash] + {:state new-state + :username username + :custom-domain? custom-domain?})}) + +(rf/defn redirect-to-ens-summary + {:events [::redirect-to-ens-summary]} + [cofx] + ;; we reset navigation so that navigate back doesn't return into the registration flow + (rf/merge cofx + (navigation/navigate-back-to :my-profile) + (navigation/navigate-to :ens-confirmation {}))) + +(rf/defn update-ens-tx-state-and-redirect + {:events [:update-ens-tx-state-and-redirect]} + [cofx new-state username custom-domain? tx-hash] + (rf/merge cofx + (update-ens-tx-state new-state username custom-domain? tx-hash) + (redirect-to-ens-summary))) + +(rf/defn clear-ens-registration + {:events [:ens/clear-registration]} + [{:keys [db]} tx-hash] + {:db (update db :ens/registrations dissoc tx-hash)}) + +(rf/defn set-state + {:events [::name-resolved]} + [{:keys [db]} username state address] + (when (= username + (get-in db [:ens/registration :username])) + {:db (-> db + (assoc-in [:ens/registration :state] state) + (assoc-in [:ens/registration :address] address))})) + +(rf/defn update-usernames + {:events [:ens/update-usernames]} + [{:keys [db]} name-details] + (let [name-details (map #(set/rename-keys % + {:chainId :chain-id + :removed :removed?}) + name-details)] + {:db (reduce (fn [db {:keys [username removed? chain-id] :as name-detail}] + (if removed? + (update-in db [:ens/names chain-id] dissoc username) + (let [old (get-in db [:ens/names chain-id username])] + (assoc-in db [:ens/names chain-id username] (merge old name-detail))))) + db + name-details)})) + +(rf/defn save-username + {:events [:ens/save-username]} + [{:keys [db] :as cofx} custom-domain? username redirect-to-summary? connected?] + (let [name (fullname custom-domain? username) + chain-id (chain/chain-id db) + names (get-in db [:ens/names chain-id] [])] + (rf/merge cofx + (cond-> {:dispatch-n [[:ens/update-usernames [{:username name :chain-id chain-id}]]]} + connected? (assoc :json-rpc/call + [{:method "ens_add" + :params [chain-id name] + :on-success #() + :on-error #(log/error + "Failed to add ens name" + {:chain-id chain-id :name name :error %})}]) + redirect-to-summary? (update-in [:dispatch-n] #(conj % [::redirect-to-ens-summary]))) + #(when (empty? names) + (multiaccounts.update/multiaccount-update + cofx + :preferred-name + name + {}))))) + +(rf/defn set-pub-key + {:events [::set-pub-key]} + [{:keys [db]}] + (let [{:keys [username address custom-domain?]} (:ens/registration db) + address address + {:keys [public-key]} (:profile/profile db) + chain-id (chain/chain-id db) + username (fullname custom-domain? username)] + {:db (assoc-in db [:ens/registration :action] constants/ens-action-type-set-pub-key) + :json-rpc/call [{:method "ens_setPubKeyPrepareTx" + :params [chain-id {:from address} username public-key] + :on-success #(re-frame/dispatch [:signing.ui/sign + {:tx-obj % + :on-result [:ens/save-username custom-domain? + username true] + :on-error [::on-registration-failure]}])}]})) + +(rf/defn on-input-submitted + {:events [::input-submitted]} + [{:keys [db] :as cofx}] + (let [{:keys [state username custom-domain?]} (:ens/registration db)] + (case state + (:available :owned) + (navigation/navigate-to cofx :ens-checkout {}) + :connected-with-different-key + (re-frame/dispatch [::set-pub-key]) + :connected + (save-username cofx custom-domain? username true true) + ;; for other states, we do nothing + nil))) + +(defn- on-resolve-owner + [chain-id custom-domain? username addresses public-key response resolve-last-id* resolve-last-id] + (when (= @resolve-last-id* resolve-last-id) + (cond + + ;; No address for a stateofus subdomain: it can be registered + (and (= response utils.ens/default-address) (not custom-domain?)) + (re-frame/dispatch [::name-resolved username :available]) + + ;; if we get an address back, we try to get the public key associated with the username as + ;; well + (get addresses (eip55/address->checksum response)) + (ens/pubkey + chain-id + (fullname custom-domain? username) + #(re-frame/dispatch + [::name-resolved username + (cond + (not public-key) :owned + (and (string/ends-with? % "0000000000000000000000000000000000000000000000000000000000000000") + (not custom-domain?)) + :invalid-ens + (= % public-key) :connected + :else :connected-with-different-key) + (eip55/address->checksum response)])) + :else + (re-frame/dispatch [::name-resolved username :taken])))) + +(rf/defn register-name + {:events [::register-name-pressed]} + [{:keys [db]} address] + (let [{:keys [username]} + (:ens/registration db) + {:keys [public-key]} (:profile/profile db) + chain-id (chain/chain-id db)] + {:db (assoc-in db [:ens/registration :action] constants/ens-action-type-register) + :json-rpc/call [{:method "ens_registerPrepareTx" + :params [chain-id {:from address} username public-key] + :on-success #(re-frame/dispatch [:signing.ui/sign + {:tx-obj % + :on-result [:update-ens-tx-state-and-redirect + :submitted username false] + :on-error [::on-registration-failure]}])}]})) + +(defn- valid-custom-domain? + [username] + (and (utils.ens/is-valid-eth-name? username) + (stateofus/lower-case? username))) + +(defn- valid-username? + [custom-domain? username] + (if custom-domain? + (valid-custom-domain? username) + (stateofus/valid-username? username))) + +(defn- state + [custom-domain? username usernames] + (cond + (or (string/blank? username) + (> 4 (count username))) + :too-short + (valid-username? custom-domain? username) + (if (usernames (fullname custom-domain? username)) + :already-added + :searching) + :else :invalid)) + +(defn addresses-without-watch + [db] + (into #{} + (remove #(= (:type %) :watch) + (map #(eip55/address->checksum (:address %)) (vals (get-in db [:wallet :accounts])))))) + +;;NOTE we want to handle only last resolve +(def resolve-last-id (atom nil)) + +(rf/defn set-username-candidate + {:events [::set-username-candidate]} + [{:keys [db]} username] + (let [{:keys [custom-domain?]} (:ens/registration db) + chain-id (chain/chain-id db) + usernames (into #{} (keys (get-in db [:ens/names chain-id]))) + next-state (state custom-domain? username usernames)] + (reset! resolve-last-id (random/id)) + (merge + {:db (update db + :ens/registration assoc + :username username + :state next-state)} + (when (= next-state :searching) + (let [{:profile/keys [profile]} db + {:keys [public-key]} profile + addresses (addresses-without-watch db) + chain-id (chain/chain-id db)] + {::resolve-owner [chain-id + (fullname custom-domain? username) + #(on-resolve-owner + chain-id + custom-domain? + username + addresses + public-key + % + resolve-last-id + @resolve-last-id)]}))))) + +(rf/defn return-to-ens-main-screen + {:events [::got-it-pressed ::cancel-pressed]} + [{:keys [db] :as cofx} _] + (rf/merge cofx + ;; clear registration data + {:db (dissoc db :ens/registration)} + ;; we reset navigation so that navigate back doesn't return into the registration flow + (navigation/navigate-back-to :my-profile) + (navigation/navigate-to :ens-main {}))) + + +(rf/defn switch-domain-type + {:events [::switch-domain-type]} + [{:keys [db] :as cofx} _] + (rf/merge cofx + {:db (-> db + (update :ens/registration dissoc :username :state) + (update-in [:ens/registration :custom-domain?] not))})) + +(rf/defn change-address + {:events [::change-address]} + [{:keys [db] :as cofx} _ {:keys [address]}] + (rf/merge cofx + {:db (assoc-in db [:ens/registration :address] address)} + (navigation/hide-bottom-sheet))) + +(rf/defn save-preferred-name + {:events [::save-preferred-name]} + [cofx name] + (multiaccounts.update/multiaccount-update cofx + :preferred-name + name + {})) + +(rf/defn on-registration-failure + "TODO not sure there is actually anything to do here + it should only be called if the user cancels the signing + Actual registration failure has not been implemented properly" + {:events [::on-registration-failure]} + [_ _]) + +(rf/defn store-name-address + {:events [::address-resolved]} + [{:keys [db]} username address] + (let [chain-id (chain/chain-id db)] + {:db (assoc-in db [:ens/names chain-id username :address] address)})) + +(rf/defn store-name-public-key + {:events [::public-key-resolved]} + [{:keys [db]} username public-key] + (let [chain-id (chain/chain-id db)] + {:db (assoc-in db [:ens/names chain-id username :public-key] public-key)})) + +(rf/defn store-expiration-date + {:events [::get-expiration-time-success]} + [{:keys [now db]} username timestamp] + (let [chain-id (chain/chain-id db)] + {:db (-> db + (assoc-in [:ens/names chain-id username :expiration-date] + (datetime/timestamp->year-month-day-date timestamp)) + (assoc-in [:ens/names chain-id username :releasable?] (<= timestamp now)))})) + +(rf/defn navigate-to-name + {:events [::navigate-to-name]} + [{:keys [db] :as cofx} username] + (let [chain-id (chain/chain-id db)] + (rf/merge cofx + {::get-expiration-time + [chain-id + (stateofus/username username) + #(re-frame/dispatch [::get-expiration-time-success username %])] + ::resolve-address + [chain-id username + #(re-frame/dispatch [::address-resolved username %])] + ::resolve-pubkey + [chain-id username + #(re-frame/dispatch [::public-key-resolved username %])]} + (navigation/navigate-to :ens-name-details username)))) + +(rf/defn start-registration + {:events [::add-username-pressed ::get-started-pressed]} + [{:keys [db] :as cofx}] + (rf/merge cofx + (set-username-candidate (get-in db [:ens/registration :username] "")) + (navigation/navigate-to :ens-search {}))) + +(rf/defn remove-username + {:events [::remove-username]} + [{:keys [db] :as cofx} name] + (let [chain-id (chain/chain-id db) + names (get-in db [:ens/names chain-id] []) + preferred-name (get-in db [:profile/profile :preferred-name]) + new-names (remove #(= name %) (keys names)) + {:keys [chain-id username]} (get-in names [name])] + (rf/merge cofx + {:json-rpc/call [{:method "ens_remove" + :params [chain-id username] + :on-success #() + :on-error #(log/error "Failed to remove ENS name" + {:name name :error %})}] + :dispatch [:ens/update-usernames + [{:username username :chain-id chain-id :removed? true}]]} + (when (= name preferred-name) + (multiaccounts.update/multiaccount-update + :preferred-name + (first new-names) + {})) + (navigation/navigate-back nil)))) diff --git a/src/legacy/status_im/ethereum/decode.cljs b/src/legacy/status_im/ethereum/decode.cljs new file mode 100644 index 00000000000..a3a5b40cb9f --- /dev/null +++ b/src/legacy/status_im/ethereum/decode.cljs @@ -0,0 +1,9 @@ +(ns legacy.status-im.ethereum.decode + (:require + [utils.money :as money])) + +(defn uint + [hex] + (let [n (money/bignumber hex)] + (when n + (.toString n 10)))) diff --git a/src/legacy/status_im/ethereum/encode.cljs b/src/legacy/status_im/ethereum/encode.cljs new file mode 100644 index 00000000000..5881ff08d05 --- /dev/null +++ b/src/legacy/status_im/ethereum/encode.cljs @@ -0,0 +1,7 @@ +(ns legacy.status-im.ethereum.encode + (:require + [native-module.core :as native-module])) + +(defn uint + [x] + (str "0x" (native-module/number-to-hex x))) diff --git a/src/legacy/status_im/ethereum/ens.cljs b/src/legacy/status_im/ethereum/ens.cljs new file mode 100644 index 00000000000..582608d2182 --- /dev/null +++ b/src/legacy/status_im/ethereum/ens.cljs @@ -0,0 +1,47 @@ +(ns legacy.status-im.ethereum.ens + (:require + [native-module.core :as native-module] + [status-im.common.json-rpc.events :as json-rpc] + [utils.ens.core :as utils.ens])) + +(defn address + [chain-id ens-name cb] + {:pre [(utils.ens/is-valid-eth-name? ens-name)]} + (json-rpc/call {:method "ens_addressOf" + :params [chain-id ens-name] + :on-success cb + :on-error #(cb "0x")})) + +(defn pubkey + ([chain-id ens-name on-success on-error] + {:pre [(utils.ens/is-valid-eth-name? ens-name)]} + (json-rpc/call {:method "ens_publicKeyOf" + :params [chain-id ens-name] + :on-success on-success + :on-error on-error})) + ;; At some point, infura started to return "execution reverted" error + ;; instead of "0x" result. Our code expects "0x" result. + ([chain-id ens-name cb] + (pubkey chain-id ens-name cb #(cb "0x")))) + +(defn owner + [chain-id ens-name cb] + (json-rpc/call {:method "ens_ownerOf" + :params [chain-id ens-name] + :on-success cb + :on-error #(cb "0x")})) + +(defn resource-url + [chain-id ens-name cb] + (json-rpc/call {:method "ens_resourceURL" + :params [chain-id ens-name] + :on-success #(cb (str "https://" (:Host %))) + :on-error #(cb "0x")})) + +(defn expire-at + [chain-id ens-name cb] + (json-rpc/call {:method "ens_expireAt" + :params [chain-id ens-name] + :on-success + ;;NOTE: returns a timestamp in s and we want ms + #(cb (* (js/Number (native-module/hex-to-number %)) 1000))})) diff --git a/src/legacy/status_im/ethereum/macros.clj b/src/legacy/status_im/ethereum/macros.clj new file mode 100644 index 00000000000..447c3842669 --- /dev/null +++ b/src/legacy/status_im/ethereum/macros.clj @@ -0,0 +1,23 @@ +(ns legacy.status-im.ethereum.macros + (:require + [clojure.java.io :as io])) + +(defn network->icon + [network] + (let [s (str "./resources/images/tokens/" (name network) "/0-native.png") + s-js (str "." s)] + (if (.exists (io/file s)) + `(js/require ~s-js) + `(js/require "../resources/images/tokens/default-token.png")))) + +(defmacro resolve-native-currency-icons + "In react-native arguments to require must be static strings. + Resolve all icons at compilation time so no variable is used." + [all-native-currencies] + (into {} + (map (fn [[network native-currency]] + [network + (assoc-in native-currency + [:icon :source] + (network->icon network))]) + all-native-currencies))) diff --git a/src/legacy/status_im/ethereum/mnemonic.cljs b/src/legacy/status_im/ethereum/mnemonic.cljs new file mode 100644 index 00000000000..d1173ea592f --- /dev/null +++ b/src/legacy/status_im/ethereum/mnemonic.cljs @@ -0,0 +1,199 @@ +(ns legacy.status-im.ethereum.mnemonic + (:require + [clojure.string :as string])) + +(def dictionary + ["abandon" "ability" "able" "about" "above" "absent" "absorb" "abstract" "absurd" "abuse" "access" + "accident" "account" "accuse" "achieve" "acid" "acoustic" "acquire" "across" "act" "action" "actor" + "actress" "actual" "adapt" "add" "addict" "address" "adjust" "admit" "adult" "advance" "advice" + "aerobic" "affair" "afford" "afraid" "again" "age" "agent" "agree" "ahead" "aim" "air" "airport" + "aisle" "alarm" "album" "alcohol" "alert" "alien" "all" "alley" "allow" "almost" "alone" "alpha" + "already" "also" "alter" "always" "amateur" "amazing" "among" "amount" "amused" "analyst" "anchor" + "ancient" "anger" "angle" "angry" "animal" "ankle" "announce" "annual" "another" "answer" "antenna" + "antique" "anxiety" "any" "apart" "apology" "appear" "apple" "approve" "april" "arch" "arctic" "area" + "arena" "argue" "arm" "armed" "armor" "army" "around" "arrange" "arrest" "arrive" "arrow" "art" + "artefact" "artist" "artwork" "ask" "aspect" "assault" "asset" "assist" "assume" "asthma" "athlete" + "atom" "attack" "attend" "attitude" "attract" "auction" "audit" "august" "aunt" "author" "auto" + "autumn" "average" "avocado" "avoid" "awake" "aware" "away" "awesome" "awful" "awkward" "axis" "baby" + "bachelor" "bacon" "badge" "bag" "balance" "balcony" "ball" "bamboo" "banana" "banner" "bar" "barely" + "bargain" "barrel" "base" "basic" "basket" "battle" "beach" "bean" "beauty" "because" "become" "beef" + "before" "begin" "behave" "behind" "believe" "below" "belt" "bench" "benefit" "best" "betray" + "better" "between" "beyond" "bicycle" "bid" "bike" "bind" "biology" "bird" "birth" "bitter" "black" + "blade" "blame" "blanket" "blast" "bleak" "bless" "blind" "blood" "blossom" "blouse" "blue" "blur" + "blush" "board" "boat" "body" "boil" "bomb" "bone" "bonus" "book" "boost" "border" "boring" "borrow" + "boss" "bottom" "bounce" "box" "boy" "bracket" "brain" "brand" "brass" "brave" "bread" "breeze" + "brick" "bridge" "brief" "bright" "bring" "brisk" "broccoli" "broken" "bronze" "broom" "brother" + "brown" "brush" "bubble" "buddy" "budget" "buffalo" "build" "bulb" "bulk" "bullet" "bundle" "bunker" + "burden" "burger" "burst" "bus" "business" "busy" "butter" "buyer" "buzz" "cabbage" "cabin" "cable" + "cactus" "cage" "cake" "call" "calm" "camera" "camp" "can" "canal" "cancel" "candy" "cannon" "canoe" + "canvas" "canyon" "capable" "capital" "captain" "car" "carbon" "card" "cargo" "carpet" "carry" "cart" + "case" "cash" "casino" "castle" "casual" "cat" "catalog" "catch" "category" "cattle" "caught" "cause" + "caution" "cave" "ceiling" "celery" "cement" "census" "century" "cereal" "certain" "chair" "chalk" + "champion" "change" "chaos" "chapter" "charge" "chase" "chat" "cheap" "check" "cheese" "chef" + "cherry" "chest" "chicken" "chief" "child" "chimney" "choice" "choose" "chronic" "chuckle" "chunk" + "churn" "cigar" "cinnamon" "circle" "citizen" "city" "civil" "claim" "clap" "clarify" "claw" "clay" + "clean" "clerk" "clever" "click" "client" "cliff" "climb" "clinic" "clip" "clock" "clog" "close" + "cloth" "cloud" "clown" "club" "clump" "cluster" "clutch" "coach" "coast" "coconut" "code" "coffee" + "coil" "coin" "collect" "color" "column" "combine" "come" "comfort" "comic" "common" "company" + "concert" "conduct" "confirm" "congress" "connect" "consider" "control" "convince" "cook" "cool" + "copper" "copy" "coral" "core" "corn" "correct" "cost" "cotton" "couch" "country" "couple" "course" + "cousin" "cover" "coyote" "crack" "cradle" "craft" "cram" "crane" "crash" "crater" "crawl" "crazy" + "cream" "credit" "creek" "crew" "cricket" "crime" "crisp" "critic" "crop" "cross" "crouch" "crowd" + "crucial" "cruel" "cruise" "crumble" "crunch" "crush" "cry" "crystal" "cube" "culture" "cup" + "cupboard" "curious" "current" "curtain" "curve" "cushion" "custom" "cute" "cycle" "dad" "damage" + "damp" "dance" "danger" "daring" "dash" "daughter" "dawn" "day" "deal" "debate" "debris" "decade" + "december" "decide" "decline" "decorate" "decrease" "deer" "defense" "define" "defy" "degree" "delay" + "deliver" "demand" "demise" "denial" "dentist" "deny" "depart" "depend" "deposit" "depth" "deputy" + "derive" "describe" "desert" "design" "desk" "despair" "destroy" "detail" "detect" "develop" "device" + "devote" "diagram" "dial" "diamond" "diary" "dice" "diesel" "diet" "differ" "digital" "dignity" + "dilemma" "dinner" "dinosaur" "direct" "dirt" "disagree" "discover" "disease" "dish" "dismiss" + "disorder" "display" "distance" "divert" "divide" "divorce" "dizzy" "doctor" "document" "dog" "doll" + "dolphin" "domain" "donate" "donkey" "donor" "door" "dose" "double" "dove" "draft" "dragon" "drama" + "drastic" "draw" "dream" "dress" "drift" "drill" "drink" "drip" "drive" "drop" "drum" "dry" "duck" + "dumb" "dune" "during" "dust" "dutch" "duty" "dwarf" "dynamic" "eager" "eagle" "early" "earn" "earth" + "easily" "east" "easy" "echo" "ecology" "economy" "edge" "edit" "educate" "effort" "egg" "eight" + "either" "elbow" "elder" "electric" "elegant" "element" "elephant" "elevator" "elite" "else" "embark" + "embody" "embrace" "emerge" "emotion" "employ" "empower" "empty" "enable" "enact" "end" "endless" + "endorse" "enemy" "energy" "enforce" "engage" "engine" "enhance" "enjoy" "enlist" "enough" "enrich" + "enroll" "ensure" "enter" "entire" "entry" "envelope" "episode" "equal" "equip" "era" "erase" "erode" + "erosion" "error" "erupt" "escape" "essay" "essence" "estate" "eternal" "ethics" "evidence" "evil" + "evoke" "evolve" "exact" "example" "excess" "exchange" "excite" "exclude" "excuse" "execute" + "exercise" "exhaust" "exhibit" "exile" "exist" "exit" "exotic" "expand" "expect" "expire" "explain" + "expose" "express" "extend" "extra" "eye" "eyebrow" "fabric" "face" "faculty" "fade" "faint" "faith" + "fall" "false" "fame" "family" "famous" "fan" "fancy" "fantasy" "farm" "fashion" "fat" "fatal" + "father" "fatigue" "fault" "favorite" "feature" "february" "federal" "fee" "feed" "feel" "female" + "fence" "festival" "fetch" "fever" "few" "fiber" "fiction" "field" "figure" "file" "film" "filter" + "final" "find" "fine" "finger" "finish" "fire" "firm" "first" "fiscal" "fish" "fit" "fitness" "fix" + "flag" "flame" "flash" "flat" "flavor" "flee" "flight" "flip" "float" "flock" "floor" "flower" + "fluid" "flush" "fly" "foam" "focus" "fog" "foil" "fold" "follow" "food" "foot" "force" "forest" + "forget" "fork" "fortune" "forum" "forward" "fossil" "foster" "found" "fox" "fragile" "frame" + "frequent" "fresh" "friend" "fringe" "frog" "front" "frost" "frown" "frozen" "fruit" "fuel" "fun" + "funny" "furnace" "fury" "future" "gadget" "gain" "galaxy" "gallery" "game" "gap" "garage" "garbage" + "garden" "garlic" "garment" "gas" "gasp" "gate" "gather" "gauge" "gaze" "general" "genius" "genre" + "gentle" "genuine" "gesture" "ghost" "giant" "gift" "giggle" "ginger" "giraffe" "girl" "give" "glad" + "glance" "glare" "glass" "glide" "glimpse" "globe" "gloom" "glory" "glove" "glow" "glue" "goat" + "goddess" "gold" "good" "goose" "gorilla" "gospel" "gossip" "govern" "gown" "grab" "grace" "grain" + "grant" "grape" "grass" "gravity" "great" "green" "grid" "grief" "grit" "grocery" "group" "grow" + "grunt" "guard" "guess" "guide" "guilt" "guitar" "gun" "gym" "habit" "hair" "half" "hammer" "hamster" + "hand" "happy" "harbor" "hard" "harsh" "harvest" "hat" "have" "hawk" "hazard" "head" "health" "heart" + "heavy" "hedgehog" "height" "hello" "helmet" "help" "hen" "hero" "hidden" "high" "hill" "hint" "hip" + "hire" "history" "hobby" "hockey" "hold" "hole" "holiday" "hollow" "home" "honey" "hood" "hope" + "horn" "horror" "horse" "hospital" "host" "hotel" "hour" "hover" "hub" "huge" "human" "humble" + "humor" "hundred" "hungry" "hunt" "hurdle" "hurry" "hurt" "husband" "hybrid" "ice" "icon" "idea" + "identify" "idle" "ignore" "ill" "illegal" "illness" "image" "imitate" "immense" "immune" "impact" + "impose" "improve" "impulse" "inch" "include" "income" "increase" "index" "indicate" "indoor" + "industry" "infant" "inflict" "inform" "inhale" "inherit" "initial" "inject" "injury" "inmate" + "inner" "innocent" "input" "inquiry" "insane" "insect" "inside" "inspire" "install" "intact" + "interest" "into" "invest" "invite" "involve" "iron" "island" "isolate" "issue" "item" "ivory" + "jacket" "jaguar" "jar" "jazz" "jealous" "jeans" "jelly" "jewel" "job" "join" "joke" "journey" "joy" + "judge" "juice" "jump" "jungle" "junior" "junk" "just" "kangaroo" "keen" "keep" "ketchup" "key" + "kick" "kid" "kidney" "kind" "kingdom" "kiss" "kit" "kitchen" "kite" "kitten" "kiwi" "knee" "knife" + "knock" "know" "lab" "label" "labor" "ladder" "lady" "lake" "lamp" "language" "laptop" "large" + "later" "latin" "laugh" "laundry" "lava" "law" "lawn" "lawsuit" "layer" "lazy" "leader" "leaf" + "learn" "leave" "lecture" "left" "leg" "legal" "legend" "leisure" "lemon" "lend" "length" "lens" + "leopard" "lesson" "letter" "level" "liar" "liberty" "library" "license" "life" "lift" "light" "like" + "limb" "limit" "link" "lion" "liquid" "list" "little" "live" "lizard" "load" "loan" "lobster" "local" + "lock" "logic" "lonely" "long" "loop" "lottery" "loud" "lounge" "love" "loyal" "lucky" "luggage" + "lumber" "lunar" "lunch" "luxury" "lyrics" "machine" "mad" "magic" "magnet" "maid" "mail" "main" + "major" "make" "mammal" "man" "manage" "mandate" "mango" "mansion" "manual" "maple" "marble" "march" + "margin" "marine" "market" "marriage" "mask" "mass" "master" "match" "material" "math" "matrix" + "matter" "maximum" "maze" "meadow" "mean" "measure" "meat" "mechanic" "medal" "media" "melody" "melt" + "member" "memory" "mention" "menu" "mercy" "merge" "merit" "merry" "mesh" "message" "metal" "method" + "middle" "midnight" "milk" "million" "mimic" "mind" "minimum" "minor" "minute" "miracle" "mirror" + "misery" "miss" "mistake" "mix" "mixed" "mixture" "mobile" "model" "modify" "mom" "moment" "monitor" + "monkey" "monster" "month" "moon" "moral" "more" "morning" "mosquito" "mother" "motion" "motor" + "mountain" "mouse" "move" "movie" "much" "muffin" "mule" "multiply" "muscle" "museum" "mushroom" + "music" "must" "mutual" "myself" "mystery" "myth" "naive" "name" "napkin" "narrow" "nasty" "nation" + "nature" "near" "neck" "need" "negative" "neglect" "neither" "nephew" "nerve" "nest" "net" "network" + "neutral" "never" "news" "next" "nice" "night" "noble" "noise" "nominee" "noodle" "normal" "north" + "nose" "notable" "note" "nothing" "notice" "novel" "now" "nuclear" "number" "nurse" "nut" "oak" + "obey" "object" "oblige" "obscure" "observe" "obtain" "obvious" "occur" "ocean" "october" "odor" + "off" "offer" "office" "often" "oil" "okay" "old" "olive" "olympic" "omit" "once" "one" "onion" + "online" "only" "open" "opera" "opinion" "oppose" "option" "orange" "orbit" "orchard" "order" + "ordinary" "organ" "orient" "original" "orphan" "ostrich" "other" "outdoor" "outer" "output" + "outside" "oval" "oven" "over" "own" "owner" "oxygen" "oyster" "ozone" "pact" "paddle" "page" "pair" + "palace" "palm" "panda" "panel" "panic" "panther" "paper" "parade" "parent" "park" "parrot" "party" + "pass" "patch" "path" "patient" "patrol" "pattern" "pause" "pave" "payment" "peace" "peanut" "pear" + "peasant" "pelican" "pen" "penalty" "pencil" "people" "pepper" "perfect" "permit" "person" "pet" + "phone" "photo" "phrase" "physical" "piano" "picnic" "picture" "piece" "pig" "pigeon" "pill" "pilot" + "pink" "pioneer" "pipe" "pistol" "pitch" "pizza" "place" "planet" "plastic" "plate" "play" "please" + "pledge" "pluck" "plug" "plunge" "poem" "poet" "point" "polar" "pole" "police" "pond" "pony" "pool" + "popular" "portion" "position" "possible" "post" "potato" "pottery" "poverty" "powder" "power" + "practice" "praise" "predict" "prefer" "prepare" "present" "pretty" "prevent" "price" "pride" + "primary" "print" "priority" "prison" "private" "prize" "problem" "process" "produce" "profit" + "program" "project" "promote" "proof" "property" "prosper" "protect" "proud" "provide" "public" + "pudding" "pull" "pulp" "pulse" "pumpkin" "punch" "pupil" "puppy" "purchase" "purity" "purpose" + "purse" "push" "put" "puzzle" "pyramid" "quality" "quantum" "quarter" "question" "quick" "quit" + "quiz" "quote" "rabbit" "raccoon" "race" "rack" "radar" "radio" "rail" "rain" "raise" "rally" "ramp" + "ranch" "random" "range" "rapid" "rare" "rate" "rather" "raven" "raw" "razor" "ready" "real" "reason" + "rebel" "rebuild" "recall" "receive" "recipe" "record" "recycle" "reduce" "reflect" "reform" "refuse" + "region" "regret" "regular" "reject" "relax" "release" "relief" "rely" "remain" "remember" "remind" + "remove" "render" "renew" "rent" "reopen" "repair" "repeat" "replace" "report" "require" "rescue" + "resemble" "resist" "resource" "response" "result" "retire" "retreat" "return" "reunion" "reveal" + "review" "reward" "rhythm" "rib" "ribbon" "rice" "rich" "ride" "ridge" "rifle" "right" "rigid" "ring" + "riot" "ripple" "risk" "ritual" "rival" "river" "road" "roast" "robot" "robust" "rocket" "romance" + "roof" "rookie" "room" "rose" "rotate" "rough" "round" "route" "royal" "rubber" "rude" "rug" "rule" + "run" "runway" "rural" "sad" "saddle" "sadness" "safe" "sail" "salad" "salmon" "salon" "salt" + "salute" "same" "sample" "sand" "satisfy" "satoshi" "sauce" "sausage" "save" "say" "scale" "scan" + "scare" "scatter" "scene" "scheme" "school" "science" "scissors" "scorpion" "scout" "scrap" "screen" + "script" "scrub" "sea" "search" "season" "seat" "second" "secret" "section" "security" "seed" "seek" + "segment" "select" "sell" "seminar" "senior" "sense" "sentence" "series" "service" "session" "settle" + "setup" "seven" "shadow" "shaft" "shallow" "share" "shed" "shell" "sheriff" "shield" "shift" "shine" + "ship" "shiver" "shock" "shoe" "shoot" "shop" "short" "shoulder" "shove" "shrimp" "shrug" "shuffle" + "shy" "sibling" "sick" "side" "siege" "sight" "sign" "silent" "silk" "silly" "silver" "similar" + "simple" "since" "sing" "siren" "sister" "situate" "six" "size" "skate" "sketch" "ski" "skill" "skin" + "skirt" "skull" "slab" "slam" "sleep" "slender" "slice" "slide" "slight" "slim" "slogan" "slot" + "slow" "slush" "small" "smart" "smile" "smoke" "smooth" "snack" "snake" "snap" "sniff" "snow" "soap" + "soccer" "social" "sock" "soda" "soft" "solar" "soldier" "solid" "solution" "solve" "someone" "song" + "soon" "sorry" "sort" "soul" "sound" "soup" "source" "south" "space" "spare" "spatial" "spawn" + "speak" "special" "speed" "spell" "spend" "sphere" "spice" "spider" "spike" "spin" "spirit" "split" + "spoil" "sponsor" "spoon" "sport" "spot" "spray" "spread" "spring" "spy" "square" "squeeze" + "squirrel" "stable" "stadium" "staff" "stage" "stairs" "stamp" "stand" "start" "state" "stay" "steak" + "steel" "stem" "step" "stereo" "stick" "still" "sting" "stock" "stomach" "stone" "stool" "story" + "stove" "strategy" "street" "strike" "strong" "struggle" "student" "stuff" "stumble" "style" + "subject" "submit" "subway" "success" "such" "sudden" "suffer" "sugar" "suggest" "suit" "summer" + "sun" "sunny" "sunset" "super" "supply" "supreme" "sure" "surface" "surge" "surprise" "surround" + "survey" "suspect" "sustain" "swallow" "swamp" "swap" "swarm" "swear" "sweet" "swift" "swim" "swing" + "switch" "sword" "symbol" "symptom" "syrup" "system" "table" "tackle" "tag" "tail" "talent" "talk" + "tank" "tape" "target" "task" "taste" "tattoo" "taxi" "teach" "team" "tell" "ten" "tenant" "tennis" + "tent" "term" "test" "text" "thank" "that" "theme" "then" "theory" "there" "they" "thing" "this" + "thought" "three" "thrive" "throw" "thumb" "thunder" "ticket" "tide" "tiger" "tilt" "timber" "time" + "tiny" "tip" "tired" "tissue" "title" "toast" "tobacco" "today" "toddler" "toe" "together" "toilet" + "token" "tomato" "tomorrow" "tone" "tongue" "tonight" "tool" "tooth" "top" "topic" "topple" "torch" + "tornado" "tortoise" "toss" "total" "tourist" "toward" "tower" "town" "toy" "track" "trade" "traffic" + "tragic" "train" "transfer" "trap" "trash" "travel" "tray" "treat" "tree" "trend" "trial" "tribe" + "trick" "trigger" "trim" "trip" "trophy" "trouble" "truck" "true" "truly" "trumpet" "trust" "truth" + "try" "tube" "tuition" "tumble" "tuna" "tunnel" "turkey" "turn" "turtle" "twelve" "twenty" "twice" + "twin" "twist" "two" "type" "typical" "ugly" "umbrella" "unable" "unaware" "uncle" "uncover" "under" + "undo" "unfair" "unfold" "unhappy" "uniform" "unique" "unit" "universe" "unknown" "unlock" "until" + "unusual" "unveil" "update" "upgrade" "uphold" "upon" "upper" "upset" "urban" "urge" "usage" "use" + "used" "useful" "useless" "usual" "utility" "vacant" "vacuum" "vague" "valid" "valley" "valve" "van" + "vanish" "vapor" "various" "vast" "vault" "vehicle" "velvet" "vendor" "venture" "venue" "verb" + "verify" "version" "very" "vessel" "veteran" "viable" "vibrant" "vicious" "victory" "video" "view" + "village" "vintage" "violin" "virtual" "virus" "visa" "visit" "visual" "vital" "vivid" "vocal" + "voice" "void" "volcano" "volume" "vote" "voyage" "wage" "wagon" "wait" "walk" "wall" "walnut" "want" + "warfare" "warm" "warrior" "wash" "wasp" "waste" "water" "wave" "way" "wealth" "weapon" "wear" + "weasel" "weather" "web" "wedding" "weekend" "weird" "welcome" "west" "wet" "whale" "what" "wheat" + "wheel" "when" "where" "whip" "whisper" "wide" "width" "wife" "wild" "will" "win" "window" "wine" + "wing" "wink" "winner" "winter" "wire" "wisdom" "wise" "wish" "witness" "wolf" "woman" "wonder" + "wood" "wool" "word" "work" "world" "worry" "worth" "wrap" "wreck" "wrestle" "wrist" "write" "wrong" + "yard" "year" "yellow" "you" "young" "youth" "zebra" "zero" "zone" "zoo"]) + +(defn sanitize-passphrase + [s] + (-> (string/trim s) + (string/replace #"[\s\n]+" " "))) + +(defn passphrase->words + [s] + (when s + (-> (sanitize-passphrase s) + (string/split #" ")))) + +(defn words-count + [s] + (if (empty? s) + 0 + (-> s + passphrase->words + count))) diff --git a/src/legacy/status_im/ethereum/mnemonic_test.cljs b/src/legacy/status_im/ethereum/mnemonic_test.cljs new file mode 100644 index 00000000000..d8cc249a32d --- /dev/null +++ b/src/legacy/status_im/ethereum/mnemonic_test.cljs @@ -0,0 +1,11 @@ +(ns legacy.status-im.ethereum.mnemonic-test + (:require + [cljs.test :refer-macros [deftest is]] + [legacy.status-im.ethereum.mnemonic :as mnemonic])) + +(deftest passphrase->words?-test + (is (= ["one" "two" "three" "for" "five" "six" "seven" "height" "nine" "ten" "eleven" "twelve"] + (mnemonic/passphrase->words "one two three for five six seven height nine ten eleven twelve")) + (= ["one" "two" "three" "for" "five" "six" "seven" "height" "nine" "ten" "eleven" "twelve"] + (mnemonic/passphrase->words + " one two three for five six seven height nine ten eleven twelve ")))) diff --git a/src/legacy/status_im/ethereum/tokens.cljs b/src/legacy/status_im/ethereum/tokens.cljs new file mode 100644 index 00000000000..c9e8c7c867d --- /dev/null +++ b/src/legacy/status_im/ethereum/tokens.cljs @@ -0,0 +1,42 @@ +(ns legacy.status-im.ethereum.tokens + (:require + [utils.ethereum.chain :as chain]) + (:require-macros [legacy.status-im.ethereum.macros :as ethereum.macros])) + +(def default-native-currency + (memoize + (fn [sym] + {:name "Native" + :symbol :ETH + :symbol-display sym + :decimals 18 + :icon {:source (js/require "../resources/images/tokens/default-token.png")}}))) + +(def all-native-currencies + (ethereum.macros/resolve-native-currency-icons + {:mainnet {:name "Ether" + :symbol :ETH + :decimals 18} + :sepolia {:name "Sepolia Ether" + :symbol :ETH + :symbol-display :ETH + :decimals 18} + :xdai {:name "xDAI" + :symbol :ETH + :symbol-display :xDAI + :symbol-exchange :DAI + :decimals 18} + :bsc {:name "BSC" + :symbol :ETH + :symbol-display :BNB + :decimals 18} + :bsc-testnet {:name "BSC test" + :symbol :ETH + :symbol-display :BNBtest + :decimals 18}})) + +(defn native-currency + [{sym :symbol :as current-network}] + (let [chain (chain/network->chain-keyword current-network)] + (get all-native-currencies chain (default-native-currency sym)))) + diff --git a/src/legacy/status_im/ethereum/transactions/core.cljs b/src/legacy/status_im/ethereum/transactions/core.cljs new file mode 100644 index 00000000000..a51a2cd94cb --- /dev/null +++ b/src/legacy/status_im/ethereum/transactions/core.cljs @@ -0,0 +1,308 @@ +(ns legacy.status-im.ethereum.transactions.core + (:require + [cljs.spec.alpha :as spec] + [legacy.status-im.ethereum.decode :as decode] + [legacy.status-im.ethereum.encode :as encode] + [legacy.status-im.utils.mobile-sync :as utils.mobile-sync] + [re-frame.core :as re-frame] + [status-im.common.json-rpc.events :as json-rpc] + [taoensso.timbre :as log] + [utils.ethereum.eip.eip55 :as eip55] + [utils.re-frame :as rf])) + +(def default-erc20-token + {:symbol :ERC20 + :decimals 18 + :name "ERC20"}) + +(defn direction + [address to] + (if (= address to) + :inbound + :outbound)) + +(defn- parse-token-transfer + [chain-tokens contract] + (let [token (get chain-tokens contract default-erc20-token)] + {:symbol (:symbol token) + :token token + ;; NOTE(goranjovic) - just a flag we need when we merge this entry + ;; with the existing entry in the app, e.g. transaction info with + ;; gas details, or a previous transfer entry with old confirmations + ;; count. + :transfer true})) + +(defn enrich-transfer + [chain-tokens + {:keys [address blockNumber timestamp from txStatus txHash gasPrice + gasUsed contract value gasLimit input nonce to type id + maxFeePerGas maxPriorityFeePerGas effectiveGasPrice]}] + (let [erc20? (= type "erc20") + failed? (= txStatus "0x0")] + (merge {:address (eip55/address->checksum address) + :id id + :block (str (decode/uint blockNumber)) + :timestamp (* (decode/uint timestamp) 1000) + :gas-used (str (decode/uint gasUsed)) + :gas-price (str (if effectiveGasPrice + (decode/uint effectiveGasPrice) + (decode/uint gasPrice))) + :fee-cap (str (decode/uint maxFeePerGas)) + :tip-cap (str (decode/uint maxPriorityFeePerGas)) + :gas-limit (str (decode/uint gasLimit)) + :nonce (str (decode/uint nonce)) + :hash txHash + :data input + :from from + :to to + :type (if failed? + :failed + (direction address to)) + :value (str (decode/uint value))} + (if erc20? + (parse-token-transfer chain-tokens contract) + ;; this is not a ERC20 token transaction + {:symbol :ETH})))) + +(defn enrich-transfers + [chain-tokens transfers] + (mapv (fn [transfer] + (enrich-transfer chain-tokens transfer)) + transfers)) + +;; ----------------------------------------------- +;; transactions api +;; ----------------------------------------------- + +(def default-transfers-limit 20) + +(rf/defn watch-transaction + "Set a watch for the given transaction + `watch-params` needs to contain a `trigger-fn` and `on-trigger` functions + `trigger-fn` is a function that returns true if the watch has been triggered + `on-trigger` is a function that returns the effects to apply when the + transaction has been triggered" + {:events [:transactions/watch-transaction]} + [{:keys [db]} transaction-id {:keys [trigger-fn on-trigger] :as watch-params}] + (when (and (fn? trigger-fn) + (fn? on-trigger)) + {:db (assoc-in db + [:ethereum/watched-transactions transaction-id] + watch-params)})) + +(rf/defn check-transaction + "Check if the transaction has been triggered and applies the effects returned + by `on-trigger` if that is the case" + [{:keys [db] :as cofx} {tx-hash :hash :as transaction}] + (when-let [watch-params + (get-in db [:ethereum/watched-transactions tx-hash])] + (let [{:keys [trigger-fn on-trigger]} watch-params] + (when (trigger-fn db transaction) + (rf/merge cofx + {:db (update db + :ethereum/watched-transactions + dissoc + tx-hash)} + (on-trigger transaction)))))) + +(rf/defn check-watched-transactions + [{:keys [db] :as cofx}] + (let [watched-transactions + (reduce-kv (fn [acc _ {:keys [transactions]}] + (merge acc + (select-keys transactions + (keys (get db :ethereum/watched-transactions))))) + {} + (get-in db [:wallet-legacy :accounts]))] + (apply rf/merge + cofx + (map (fn [[_ transaction]] + (check-transaction transaction)) + watched-transactions)))) + +(rf/defn add-transfer + "We determine a unique id for the transfer before adding it because some + transaction can contain multiple transfers and they would overwrite each other + in the transfer map if identified by hash" + [{:keys [db] :as cofx} {tx-hash :hash :keys [id address] :as transfer}] + (let [transfer-by-hash (get-in db [:wallet-legacy :accounts address :transactions tx-hash])] + (when-let [unique-id (when-not (= transfer transfer-by-hash) + (if (and transfer-by-hash + (not (= :pending + (:type transfer-by-hash)))) + id + tx-hash))] + (rf/merge cofx + {:db (assoc-in db + [:wallet-legacy :accounts address :transactions unique-id] + (assoc transfer :hash unique-id))} + (check-transaction transfer))))) + +(defn get-min-known-block + [db address] + (get-in db [:wallet-legacy :accounts (eip55/address->checksum address) :min-block])) + +(defn count-min-block-transfers + [db address] + (get-in db + [:wallet-legacy :accounts + (eip55/address->checksum address) + :min-block-transfers-count])) + +(rf/defn set-lowest-fetched-block + [{:keys [db]} address transfers] + (let [checksum (eip55/address->checksum address) + {:keys [min-block min-block-transfers-count]} + (reduce + (fn [{:keys [min-block] :as acc} + {tx-hash :hash block :block}] + (cond + (or (nil? min-block) (> min-block (js/parseInt block))) + {:min-block (js/parseInt block) + :min-block-transfers-count 1} + + (and (= min-block block) + (nil? (get-in db [:wallet-legacy :accounts checksum :transactions tx-hash]))) + (update acc :min-block-transfers-count inc) + + :else acc)) + {:min-block + (when-let [min-block-string (get-min-known-block db address)] + (js/parseInt min-block-string)) + + :min-block-transfers-count + (count-min-block-transfers db address)} + transfers)] + (log/debug "[transactions] set-lowest-fetched-block" + "address" address + "min-block" min-block + "min-block-transfers-count" min-block-transfers-count) + {:db (update-in db + [:wallet-legacy :accounts checksum] + assoc + :min-block min-block + :min-block-transfers-count min-block-transfers-count)})) + +(defn update-fetching-status + [db addresses fetching-type state] + (update-in + db + [:wallet-legacy :fetching] + (fn [accounts] + (reduce + (fn [accounts address] + (assoc-in accounts + [(eip55/address->checksum address) fetching-type] + state)) + accounts + addresses)))) + +(rf/defn tx-fetching-in-progress + [{:keys [db]} addresses] + {:db (update-fetching-status db addresses :history? true)}) + +(rf/defn tx-fetching-ended + [{:keys [db]} addresses] + {:db (update-fetching-status db addresses :history? false)}) + +(rf/defn tx-history-end-reached + [{:keys [db]} address] + (let [syncing-allowed? (utils.mobile-sync/syncing-allowed? db)] + {:db (assoc-in db + [:wallet-legacy :fetching address :all-fetched?] + (if syncing-allowed? + :all + :all-preloaded))})) + +(rf/defn handle-new-transfer + [cofx transfers {:keys [address limit]}] + (log/debug "[transfers] new-transfers" + "address" address + "count" (count transfers) + "limit" limit) + (let [checksum (eip55/address->checksum address) + effects (cond-> [(when (seq transfers) + (set-lowest-fetched-block checksum transfers))] + + (seq transfers) + (concat + [] + (mapv add-transfer transfers)) + + (< (count transfers) limit) + (conj (tx-history-end-reached checksum)))] + (apply rf/merge cofx (tx-fetching-ended [checksum]) effects))) + +(rf/defn new-transfers + {:events [::new-transfers]} + [cofx transfers params] + (rf/merge cofx + (handle-new-transfer transfers params))) + +(rf/defn tx-fetching-failed + {:events [::tx-fetching-failed]} + [cofx error address] + (log/debug "[transactions] tx-fetching-failed" + "address" address + "error" error) + (tx-fetching-ended cofx [address])) + +(re-frame/reg-fx + :transactions/get-transfers + (fn + [{:keys [chain-tokens addresses before-block limit + limit-per-address fetch-more?] + :as params + :or {limit default-transfers-limit + fetch-more? true}}] + {:pre [(spec/valid? + (spec/coll-of string?) + addresses)]} + (log/debug "[transactions] get-transfers" + "addresses" addresses + "block" before-block + "limit" limit + "limit-per-address" limit-per-address + "fetch-more?" fetch-more?) + (doseq [address addresses] + (let [limit (or (get limit-per-address address) + limit)] + (json-rpc/call + {:method "wallet_getTransfersByAddress" + :params [address (encode/uint before-block) (encode/uint limit) fetch-more?] + :on-success #(re-frame/dispatch + [::new-transfers + (enrich-transfers chain-tokens %) + (assoc params + :address address + :limit limit)]) + :on-error #(re-frame/dispatch [::tx-fetching-failed address])}))))) + +(rf/defn fetch-more-tx + {:events [:transactions/fetch-more]} + [{:keys [db] :as cofx} address] + (let [min-known-block (or (get-min-known-block db address) + (:ethereum/current-block db)) + min-count (or (count-min-block-transfers db address) 0)] + (rf/merge + cofx + {:transactions/get-transfers + {:chain-tokens (:wallet-legacy/all-tokens db) + :addresses [address] + :before-block min-known-block + :fetch-more? (utils.mobile-sync/syncing-allowed? db) + ;; Transfers are requested before and including `min-known-block` because there is no + ;; guarantee that all transfers from that block are shown already. To make sure that we fetch + ;; the whole `default-transfers-limit` of transfers the number of transfers already received + ;; for `min-known-block` is added to the page size. + :limit-per-address {address (+ default-transfers-limit + min-count)}}} + (tx-fetching-in-progress [address])))) + +(rf/defn get-fetched-transfers + {:events [:transaction/get-fetched-transfers]} + [{:keys [db]}] + {:transactions/get-transfers + {:chain-tokens (:wallet-legacy/all-tokens db) + :addresses (map :address (get db :profile/wallet-accounts)) + :fetch-more? false}}) diff --git a/src/legacy/status_im/events.cljs b/src/legacy/status_im/events.cljs new file mode 100644 index 00000000000..3bb65ab17cf --- /dev/null +++ b/src/legacy/status_im/events.cljs @@ -0,0 +1,221 @@ +(ns legacy.status-im.events + (:require + clojure.set + legacy.status-im.browser.core + legacy.status-im.browser.permissions + legacy.status-im.chat.models.loading + legacy.status-im.data-store.chats + legacy.status-im.data-store.visibility-status-updates + legacy.status-im.ens.core + legacy.status-im.fleet.core + legacy.status-im.group-chats.core + legacy.status-im.log-level.core + legacy.status-im.multiaccounts.login.core + [legacy.status-im.multiaccounts.model :as multiaccounts.model] + legacy.status-im.multiaccounts.update.core + legacy.status-im.pairing.core + legacy.status-im.profile.core + legacy.status-im.search.core + legacy.status-im.stickers.core + legacy.status-im.ui.components.invite.events + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.utils.dimensions :as dimensions] + legacy.status-im.utils.logging.core + [legacy.status-im.utils.utils :as utils] + legacy.status-im.visibility-status-popover.core + legacy.status-im.visibility-status-updates.core + legacy.status-im.waku.core + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [react-native.core :as rn] + [react-native.permissions :as permissions] + [react-native.platform :as platform] + status-im.common.serialization + status-im.common.standard-authentication.events + [status-im.common.theme.core :as theme] + [status-im.common.universal-links :as universal-links] + [status-im.constants :as constants] + status-im.contexts.chat.home.events + status-im.contexts.communities.home.events + status-im.contexts.onboarding.events + status-im.contexts.shell.activity-center.events + status-im.contexts.shell.activity-center.notification.contact-requests.events + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(re-frame/reg-fx + :dismiss-keyboard + (fn [] + (react/dismiss-keyboard!))) + +(re-frame/reg-fx + :request-permissions-fx + (fn [options] + (permissions/request-permissions options))) + +(re-frame/reg-fx + :request-notifications-fx + (fn [options] + (permissions/request-notifications options))) + +(re-frame/reg-fx + :ui/show-confirmation + (fn [options] + (utils/show-confirmation options))) + +(re-frame/reg-fx + ::app-state-change-fx + (fn [state] + (when (and platform/ios? (= state "active")) + ;; Change the app theme if the ios device theme was updated when the app was in the background + ;; https://github.com/status-im/status-mobile/issues/15708 + (theme/change-device-theme (rn/get-color-scheme))) + (native-module/app-state-change state))) + +(rf/defn dismiss-keyboard + {:events [:dismiss-keyboard]} + [_] + {:dismiss-keyboard nil}) + +(rf/defn gfycat-generated + {:events [:gfycat-generated]} + [{:keys [db]} path gfycat] + {:db (assoc-in db path gfycat)}) + +(rf/reg-event-fx + :system-theme-mode-changed + (fn [{:keys [db]}] + (let [appearance-type (-> db :profile/profile :appearance)] + (when (and (multiaccounts.model/logged-in? db) + (= appearance-type status-im.constants/appearance-type-system)) + {:fx [[:dispatch [:theme/switch]]]})))) + +(defn- on-biometric-auth-fail + [{:keys [code]}] + (if (= code "USER_FALLBACK") + (re-frame/dispatch [:profile/logout]) + (utils/show-confirmation + {:title (i18n/label :t/biometric-auth-confirm-title) + :content (i18n/label :t/biometric-auth-confirm-message) + :confirm-button-text (i18n/label :t/biometric-auth-confirm-try-again) + :cancel-button-text (i18n/label :t/biometric-auth-confirm-logout) + :on-accept #(rf/dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}]) + :on-cancel #(re-frame/dispatch [:profile/logout])}))) + +(rf/defn on-return-from-background + [{:keys [db now] :as cofx}] + (let [new-account? (get db :onboarding/new-account?) + app-in-background-since (get db :app-in-background-since) + signed-up? (get-in db [:profile/profile :signed-up?]) + notifications-settings? (= (:view-id db) :screen/settings.notifications) + requires-bio-auth (and + signed-up? + (= (:auth-method db) "biometric") + (some? app-in-background-since) + (>= (- now app-in-background-since) + constants/ms-in-bg-for-require-bioauth))] + (rf/merge cofx + {:db (dissoc db :app-in-background-since) + :effects.biometric/get-supported-type nil} + (when-not new-account? + (universal-links/process-stored-event)) + #(when-let [chat-id (:current-chat-id db)] + {:dispatch [:chat/mark-all-as-read chat-id]}) + #(when requires-bio-auth + {:dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}]}) + #(when notifications-settings? + {:dispatch [:notifications/check-notifications-blocked]})))) + +(rf/defn on-going-in-background + [{:keys [db now]}] + {:db (-> db + (assoc :app-in-background-since now) + (dissoc :universal-links/handling))}) + ;; event not implemented + ;; :dispatch-n [[:audio-recorder/on-background] [:audio-message/on-background]] + + +(rf/defn app-state-change + {:events [:app-state-change]} + [{:keys [db] :as cofx} state] + (let [app-coming-from-background? (= state "active") + app-going-in-background? (= state "background")] + (rf/merge cofx + {::app-state-change-fx state + :db (assoc db :app-state state)} + #(when app-coming-from-background? + (on-return-from-background %)) + #(when app-going-in-background? + (on-going-in-background %))))) + +(rf/defn request-permissions + {:events [:request-permissions]} + [_ options] + {:request-permissions-fx options}) + +(rf/reg-event-fx + :request-notifications + (fn [_ [options]] + (prn options) + {:request-notifications-fx options})) + +(rf/defn update-window-dimensions + {:events [:update-window-dimensions]} + [{:keys [db]} dimensions] + {:db (assoc db :dimensions/window (dimensions/window dimensions))}) + +(rf/defn on-will-focus + {:events [:screens/on-will-focus]} + [{:keys [db] :as cofx} view-id] + (rf/merge cofx + (cond + (= :screen/chat view-id) + {:effects.async-storage/set {:chat-id (get-in cofx [:db :current-chat-id]) + :key-uid (get-in cofx [:db :profile/profile :key-uid])} + :db (assoc db :screens/was-focused-once? true) + :dispatch [:alert-banners/unhide]} + + (not (get db :screens/was-focused-once?)) + {:db (assoc db :screens/was-focused-once? true)}))) + + +;;TODO :replace by named events +(rf/defn set-event + {:events [:set]} + [{:keys [db]} k v] + {:db (assoc db k v)}) + +;;TODO :replace by named events +(rf/defn set-once-event + {:events [:set-once]} + [{:keys [db]} k v] + (when-not (get db k) + {:db (assoc db k v)})) + +;;TODO :replace by named events +(rf/defn set-in-event + {:events [:set-in]} + [{:keys [db]} path v] + {:db (assoc-in db path v)}) + +(defn on-ramp<-rpc + [on-ramp] + (clojure.set/rename-keys on-ramp + {:logoUrl :logo-url + :siteUrl :site-url})) + +(rf/defn crypto-loaded-event + {:events [::crypto-loaded]} + [{:keys [db]} on-ramps] + {:db (assoc + db + :buy-crypto/on-ramps + (map on-ramp<-rpc on-ramps))}) + +(rf/defn buy-crypto-ui-loaded + {:events [:buy-crypto.ui/loaded]} + [_] + {:json-rpc/call [{:method "wallet_getCryptoOnRamps" + :params [] + :on-success (fn [on-ramps] + (re-frame/dispatch [::crypto-loaded on-ramps]))}]}) diff --git a/src/legacy/status_im/fleet/core.cljs b/src/legacy/status_im/fleet/core.cljs new file mode 100644 index 00000000000..ebcb835956f --- /dev/null +++ b/src/legacy/status_im/fleet/core.cljs @@ -0,0 +1,84 @@ +(ns legacy.status-im.fleet.core + (:require + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [legacy.status-im.node.core :as node] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(def ^:private default-fleets (transforms/json->clj (native-module/fleets))) +(def ^:private default-fleet (:defaultFleet default-fleets)) +(def fleets + (->> default-fleets + :fleets + keys + (map name))) + +(defn current-fleet-sub + [multiaccount] + (keyword (or (get multiaccount :fleet) + default-fleet))) + +(defn format-mailserver + [mailserver address] + {:id mailserver + :name (name mailserver) + :password constants/mailserver-password + :address address}) + +(defn format-mailservers + [mailservers] + (reduce (fn [acc [mailserver address]] + (assoc acc mailserver (format-mailserver mailserver address))) + {} + mailservers)) + +(defn default-mailservers + [db] + (reduce (fn [acc [fleet node-by-type]] + (assoc acc fleet (format-mailservers (:mail node-by-type)))) + {} + (node/fleets db))) + +(defn nodes->fleet + [nodes] + (letfn [(format-nodes [nodes] + (reduce (fn [acc n] + (assoc acc + (keyword n) + n)) + {} + nodes))] + {:boot (format-nodes nodes) + :mail (format-nodes nodes) + :whisper (format-nodes nodes)})) + +(rf/defn set-nodes + [{:keys [db]} fleet nodes] + {:db (-> db + (assoc-in [:custom-fleets fleet] (nodes->fleet nodes)) + (assoc-in [:mailserver/mailservers fleet] + (format-mailservers + (reduce + (fn [acc e] + (assoc acc + (keyword e) + e)) + {} + nodes))))}) + +(rf/defn save + {:events [:fleet.ui/save-fleet-confirmed]} + [{:keys [db] :as cofx} fleet] + (let [old-fleet (get-in db [:profile/profile :fleet])] + (when (not= fleet old-fleet) + (multiaccounts.update/multiaccount-update + cofx + :fleet + fleet + {:on-success (fn [] + (log/info "Fleet changed successfully to" fleet) + (re-frame/dispatch [:profile/logout]))})))) diff --git a/src/legacy/status_im/fleet/core_test.cljs b/src/legacy/status_im/fleet/core_test.cljs new file mode 100644 index 00000000000..dfc3fa73a75 --- /dev/null +++ b/src/legacy/status_im/fleet/core_test.cljs @@ -0,0 +1,47 @@ +(ns legacy.status-im.fleet.core-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [legacy.status-im.fleet.core :as fleet] + [status-im.constants :as constants])) + +#_(deftest fleets-test + (testing "not passing any extra fleet" + (testing "it returns the default fleets" + (is (= + #{:eth.prod} + (into #{} + (keys (node/fleets {}))))))) + (testing "passing a custom fleet" + (testing "it sets the custom fleet" + (is (= {:mail {"a" "a"} + :whisper {"w" "w"} + :boot {"b" "b"}} + (:custom-fleet + (node/fleets {:custom-fleets {:custom-fleet + {:mail {"a" "a"} + :whisper {"w" "w"} + :boot {"b" "b"}}}}))))))) + +(deftest set-nodes-test + (testing "set-nodes" + (let [actual (fleet/set-nodes {:db {}} :test-fleet ["a" "b" "c"]) + actual-custom-fleet (get-in actual [:db :custom-fleets]) + actual-mailservers (get-in actual [:db :mailserver/mailservers :test-fleet])] + (testing "it sets the custom fleet in the db" + (is actual-custom-fleet)) + (testing "it sets the custom mailservers in the db" + (is actual-mailservers)) + (testing "it correctly formats mailservers" + (is (= {:a {:id :a + :name "a" + :password constants/mailserver-password + :address "a"} + :b {:id :b + :name "b" + :password constants/mailserver-password + :address "b"} + :c {:id :c + :name "c" + :password constants/mailserver-password + :address "c"}} + actual-mailservers)))))) diff --git a/src/legacy/status_im/fleet/default_fleet.cljs b/src/legacy/status_im/fleet/default_fleet.cljs new file mode 100644 index 00000000000..1b07501a332 --- /dev/null +++ b/src/legacy/status_im/fleet/default_fleet.cljs @@ -0,0 +1,4 @@ +(ns legacy.status-im.fleet.default-fleet (:require-macros [legacy.status-im.utils.slurp :refer [slurp]])) + +(def default-fleets + (slurp "resources/config/fleets.json")) diff --git a/src/legacy/status_im/group_chats/core.cljs b/src/legacy/status_im/group_chats/core.cljs new file mode 100644 index 00000000000..2187ddb93f5 --- /dev/null +++ b/src/legacy/status_im/group_chats/core.cljs @@ -0,0 +1,249 @@ +(ns legacy.status-im.group-chats.core + (:refer-clojure :exclude [remove]) + (:require + [clojure.spec.alpha :as spec] + [clojure.string :as string] + [legacy.status-im.data-store.invitations :as data-store.invitations] + [oops.core :as oops] + [re-frame.core :as re-frame] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.constants :as constants] + [status-im.contexts.chat.events :as chat.events] + [status-im.navigation.events :as navigation] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/defn navigate-chat-updated + {:events [:navigate-chat-updated]} + [cofx chat-id] + (when (get-in cofx [:db :chats chat-id]) + (chat.events/pop-to-root-and-navigate-to-chat cofx chat-id nil))) + +(re-frame/reg-event-fx :chat-removed + (fn [{:keys [db]} [response]] + {:db (dissoc db :current-chat-id) + :fx [[:dispatch [:sanitize-messages-and-process-response response]] + [:dispatch [:pop-to-root :screen/shell-stack]] + [:activity-center.notifications/fetch-unread-count]]})) + +(rf/defn handle-chat-update + {:events [:chat-updated]} + [_ response do-not-navigate?] + {:dispatch-n [[:sanitize-messages-and-process-response response] + (when-not do-not-navigate? + [:navigate-chat-updated + (-> response + (oops/oget :chats) + first + (oops/oget :id))])]}) + +(rf/defn remove-member + "Format group update message and sign membership" + {:events [:group-chats.ui/remove-member-pressed]} + [_ chat-id member _do-not-navigate?] + {:json-rpc/call [{:method "wakuext_removeMemberFromGroupChat" + :params [nil chat-id member] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated % true])}]}) + +(rf/defn remove-members + {:events [:group-chats.ui/remove-members-pressed]} + [{{:group-chat/keys [deselected-members]} :db :as _cofx} chat-id] + {:json-rpc/call [{:method "wakuext_removeMembersFromGroupChat" + :params [nil chat-id deselected-members] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated % true]) + :on-error #()}]}) + +(rf/defn join-chat + {:events [:group-chats.ui/join-pressed]} + [_ chat-id] + {:json-rpc/call [{:method "wakuext_confirmJoiningGroup" + :params [chat-id] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated %])}]}) + +(rf/defn create-from-link + {:events [:group-chats/create-from-link]} + [cofx {:keys [chat-id invitation-admin chat-name]}] + (if (get-in cofx [:db :chats chat-id]) + {:dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]} + {:json-rpc/call [{:method "wakuext_createGroupChatFromInvitation" + :params [chat-name chat-id invitation-admin] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated %])}]})) + +(rf/defn make-admin + {:events [:group-chats.ui/make-admin-pressed]} + [_ chat-id member] + {:json-rpc/call [{:method "wakuext_addAdminsToGroupChat" + :params [nil chat-id [member]] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated %])}]}) + +(rf/defn add-members + "Add members to a group chat" + {:events [:group-chats.ui/add-members-pressed]} + [{{:group-chat/keys [selected-participants]} :db} chat-id] + (when (seq selected-participants) + {:json-rpc/call [{:method "wakuext_addMembersToGroupChat" + :params [nil chat-id selected-participants] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated % true])}]})) + +(rf/defn add-members-from-invitation + "Add members to a group chat" + {:events [:group-chats.ui/add-members-from-invitation]} + [{:keys [db]} id participant chat-id] + {:db (assoc-in db [:group-chat/invitations id :state] constants/invitation-state-approved) + :json-rpc/call [{:method "wakuext_addMembersToGroupChat" + :params [nil chat-id [participant]] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated %])}]}) + +(rf/defn leave + "Leave chat" + {:events [:group-chats.ui/leave-chat-confirmed]} + [_cofx chat-id] + {:json-rpc/call [{:method "wakuext_leaveGroupChat" + :params [nil chat-id true] + :js-response true + :on-success #(re-frame/dispatch [:chat-removed % chat-id])}]}) + +(rf/defn remove + "Remove chat" + {:events [:group-chats.ui/remove-chat-confirmed]} + [cofx chat-id] + (rf/merge cofx + (chat.events/deactivate-chat chat-id) + (navigation/pop-to-root :screen/shell-stack))) + +(def not-blank? + (complement string/blank?)) + +(defn- valid-name? + [name] + (spec/valid? not-blank? name)) + +(rf/defn name-changed + "Save chat from edited profile" + {:events [:group-chats.ui/name-changed]} + [{:keys [db] :as _cofx} chat-id new-name] + (when (valid-name? new-name) + {:db (assoc-in db [:chats chat-id :name] new-name) + :json-rpc/call [{:method "wakuext_changeGroupChatName" + :params [nil chat-id new-name] + :js-response true + :on-success #(re-frame/dispatch [:chat-updated %])}]})) + +(rf/defn membership-retry + {:events [:group-chats.ui/membership-retry]} + [{:keys [db]} chat-id] + {:db (assoc-in db [:chat/memberships chat-id :retry?] true)}) + +(rf/defn membership-message + {:events [:group-chats.ui/update-membership-message]} + [{:keys [db]} message chat-id] + {:db (assoc-in db [:chat/memberships chat-id :message] message)}) + +(rf/defn send-group-chat-membership-request + "Send group chat membership request" + {:events [:send-group-chat-membership-request]} + [{{:keys [chats] :as db} :db :as _cofx} chat-id] + (let [{:keys [invitation-admin]} (get chats chat-id) + message (get-in db [:chat/memberships chat-id :message])] + {:db (assoc-in db [:chat/memberships chat-id] nil) + :json-rpc/call [{:method "wakuext_sendGroupChatInvitationRequest" + :params [nil chat-id invitation-admin message] + :js-response true + :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]})) + +(rf/defn send-group-chat-membership-rejection + "Send group chat membership rejection" + {:events [:send-group-chat-membership-rejection]} + [_cofx invitation-id] + {:json-rpc/call [{:method "wakuext_sendGroupChatInvitationRejection" + :params [nil invitation-id] + :js-response true + :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]}) + +(rf/defn handle-invitations + [{db :db} invitations] + {:db (update db + :group-chat/invitations + #(reduce (fn [acc {:keys [id] :as inv}] + (assoc acc id inv)) + % + invitations))}) + +(rf/defn deselect-member + {:events [:deselect-member]} + [{:keys [db]} id] + {:db (update db :group-chat/deselected-members conj id)}) + +(rf/defn undo-deselect-member + {:events [:undo-deselect-member]} + [{:keys [db]} id] + {:db (update db :group-chat/deselected-members disj id)}) + +(rf/defn deselect-contact + {:events [:deselect-contact]} + [{:keys [db]} id] + {:db (update db :group/selected-contacts disj id)}) + +(rf/defn select-contact + {:events [:select-contact]} + [{:keys [db]} id] + {:db (update db :group/selected-contacts conj id)}) + +(rf/defn clear-contacts + {:events [:group-chat/clear-contacts]} + [{:keys [db]}] + {:db (assoc db :group/selected-contacts #{})}) + +(rf/defn deselect-participant + {:events [:deselect-participant]} + [{:keys [db]} id] + {:db (update db :group-chat/selected-participants disj id)}) + +(rf/defn select-participant + {:events [:select-participant]} + [{:keys [db]} id] + {:db (update db :group-chat/selected-participants conj id)}) + +(rf/defn clear-added-participants + {:events [:group/clear-added-participants]} + [{db :db}] + {:db (assoc db :group-chat/selected-participants #{})}) + +(rf/defn clear-removed-members + {:events [:group/clear-removed-members]} + [{db :db}] + {:db (assoc db :group-chat/deselected-members #{})}) + +(rf/defn ui-leave-chat-pressed + {:events [:group-chats.ui/leave-chat-pressed]} + [{:keys [db]} chat-id] + (let [chat-name (get-in db [:chats chat-id :name])] + {:ui/show-confirmation + {:title (i18n/label :t/leave-confirmation {:chat-name chat-name}) + :content (i18n/label :t/leave-chat-confirmation) + :confirm-button-text (i18n/label :t/leave) + :on-accept #(do + (re-frame/dispatch [:bottom-sheet/hide-old]) + (re-frame/dispatch [:group-chats.ui/leave-chat-confirmed chat-id]))}})) + +(rf/defn initialize-invitations + {:events [::initialize-invitations]} + [{:keys [db]} invitations] + {:db (assoc db + :group-chat/invitations + (reduce (fn [acc {:keys [id] :as inv}] + (assoc acc id (data-store.invitations/<-rpc inv))) + {} + invitations))}) + +(re-frame/reg-fx :group-chats/get-group-chat-invitations + (fn [] + (json-rpc/call {:method "wakuext_getGroupChatInvitations" + :on-success [::initialize-invitations]}))) diff --git a/src/legacy/status_im/log_level/core.cljs b/src/legacy/status_im/log_level/core.cljs new file mode 100644 index 00000000000..0ec1cca789f --- /dev/null +++ b/src/legacy/status_im/log_level/core.cljs @@ -0,0 +1,29 @@ +(ns legacy.status-im.log-level.core + (:require + [native-module.core :as native-module] + [utils.re-frame :as rf])) + +(rf/reg-fx + :log-level/set-profile-log-level + (fn [log-level] + (when (seq log-level) + (native-module/set-profile-log-level log-level)))) + +(rf/reg-fx + :log-level/set-profile-log-enabled + (fn [enabled?] + (native-module/set-profile-log-enabled enabled?))) + +(rf/defn save-log-level + {:events [:log-level.ui/change-log-level-confirmed]} + [{:keys [db]} log-level] + (let [old-log-level (get-in db [:profile/profile :log-level])] + (when (not= old-log-level log-level) + (let [need-set-profile-log-enabled? (or (empty? old-log-level) (empty? log-level)) + profile-log-enabled? (boolean (seq log-level))] + {:fx [[:log-level/set-profile-log-level log-level] + (when need-set-profile-log-enabled? + [:log-level/set-profile-log-enabled profile-log-enabled?]) + ;; update log level in taoensso.timbre + [:logs/set-level log-level] + [:dispatch [:multiaccounts.ui/update :log-level log-level]]]})))) diff --git a/src/legacy/status_im/mailserver/core.cljs b/src/legacy/status_im/mailserver/core.cljs new file mode 100644 index 00000000000..969772fbbcf --- /dev/null +++ b/src/legacy/status_im/mailserver/core.cljs @@ -0,0 +1,43 @@ +(ns ^{:doc "Mailserver events and API"} legacy.status-im.mailserver.core + (:require + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/defn handle-mailserver-changed + [{:keys [db]} ms] + (when (seq ms) + {:db (assoc db :mailserver/current-id (keyword ms))})) + +(rf/defn handle-mailserver-available + [{:keys [db]} ms] + {:db (assoc db :mailserver/current-id (keyword ms))}) + +(rf/defn toggle-use-mailservers + [_ value] + {:json-rpc/call + [{:method "wakuext_toggleUseMailservers" + :params [value] + :on-success #(log/info "successfully toggled use-mailservers" value) + :on-error #(log/error "failed to toggle use-mailserver" value %)}]}) + +(rf/defn update-use-mailservers + {:events [:mailserver.ui/use-history-switch-pressed]} + [cofx use-mailservers?] + (rf/merge cofx + (multiaccounts.update/optimistic :use-mailservers? use-mailservers?) + (toggle-use-mailservers use-mailservers?))) + +(defn add-mailservers + [db mailservers] + (reduce (fn [db {:keys [fleet id name] :as mailserver}] + (let [updated-mailserver + (-> mailserver + (update :id keyword) + (assoc :name (if (seq name) name id)) + (dissoc :fleet))] + (assoc-in db + [:mailserver/mailservers (keyword fleet) (keyword id)] + updated-mailserver))) + db + mailservers)) diff --git a/src/legacy/status_im/multiaccounts/create/core.cljs b/src/legacy/status_im/multiaccounts/create/core.cljs new file mode 100644 index 00000000000..270b418fa33 --- /dev/null +++ b/src/legacy/status_im/multiaccounts/create/core.cljs @@ -0,0 +1,71 @@ +(ns legacy.status-im.multiaccounts.create.core + (:require + [legacy.status-im.utils.deprecated-types :as types] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [utils.re-frame :as rf])) + +(defn normalize-derived-data-keys + [derived-data] + (->> derived-data + (map (fn [[path {:keys [publicKey compressedKey] :as data}]] + [path + (cond-> (-> data + (dissoc :publicKey :compressedKey) + (assoc + :public-key publicKey + :compressed-key compressedKey)))])) + (into {}))) + +(defn normalize-multiaccount-data-keys + [{:keys [publicKey keyUid derived compressedKey] :as data}] + (cond-> (-> data + (dissoc :keyUid :publicKey) + (assoc :key-uid keyUid + :compressed-key compressedKey + :public-key publicKey)) + derived + (update :derived normalize-derived-data-keys))) + +(re-frame/reg-fx + :multiaccount-generate-and-derive-addresses + (fn [] + (native-module/multiaccount-generate-and-derive-addresses + 5 + 12 + [constants/path-whisper + constants/path-wallet-root + constants/path-default-wallet] + #(re-frame/dispatch [:multiaccount-generate-and-derive-addresses-success + (mapv normalize-multiaccount-data-keys + (types/json->clj %))])))) + +(rf/defn multiaccount-generate-and-derive-addresses-success + {:events [:multiaccount-generate-and-derive-addresses-success]} + [{:keys [db]} result] + {:db (update db + :intro-wizard + (fn [data] + (-> data + (dissoc :processing?) + (assoc :multiaccounts result + :selected-storage-type :default + :selected-id (-> result first :id) + :step :choose-key)))) + :navigate-to [:choose-name (:theme db)]}) + +(rf/defn generate-and-derive-addresses + {:events [:generate-and-derive-addresses]} + [{:keys [db]}] + {:db (-> db + (update :intro-wizard + #(-> % + (assoc :processing? true) + (dissoc :recovering?))) + (dissoc :recovered-account?)) + :multiaccount-generate-and-derive-addresses nil}) + +(rf/defn save-multiaccount-and-login-with-keycard + [_ args] + {:keycard/save-multiaccount-and-login args}) diff --git a/src/legacy/status_im/multiaccounts/db.cljs b/src/legacy/status_im/multiaccounts/db.cljs new file mode 100644 index 00000000000..2c1d83e045c --- /dev/null +++ b/src/legacy/status_im/multiaccounts/db.cljs @@ -0,0 +1,10 @@ +(ns legacy.status-im.multiaccounts.db + (:require + [cljs.spec.alpha :as spec] + [status-im.constants :as const])) + +(defn valid-length? + [password] + (>= (count password) const/min-password-length)) + +(spec/def ::password (spec/and string? not-empty valid-length?)) diff --git a/src/legacy/status_im/multiaccounts/login/core.cljs b/src/legacy/status_im/multiaccounts/login/core.cljs new file mode 100644 index 00000000000..5c5b7c28d1c --- /dev/null +++ b/src/legacy/status_im/multiaccounts/login/core.cljs @@ -0,0 +1,43 @@ +(ns legacy.status-im.multiaccounts.login.core + (:require + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.utils.deprecated-types :as types] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [react-native.platform :as platform] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(re-frame/reg-fx + ::export-db + (fn [[key-uid account-data hashed-password callback]] + (native-module/export-db key-uid account-data hashed-password callback))) + +(re-frame/reg-fx + ::import-db + (fn [[key-uid account-data hashed-password]] + (native-module/import-db key-uid account-data hashed-password))) + +(rf/defn export-db-submitted + {:events [:multiaccounts.login.ui/export-db-submitted]} + [{:keys [db]}] + (let [{:keys [key-uid password name]} (:profile/login db)] + {::export-db [key-uid + (types/clj->json {:name name + :key-uid key-uid}) + (native-module/sha3 (security/safe-unmask-data password)) + (fn [path] + (when platform/ios? + (let [uri (str "file://" path)] + (.share ^js react/sharing + (clj->js {:title "Unencrypted database" + :url uri})))))]})) + +(rf/defn import-db-submitted + {:events [:multiaccounts.login.ui/import-db-submitted]} + [{:keys [db]}] + (let [{:keys [key-uid password name]} (:profile/login db)] + {::import-db [key-uid + (types/clj->json {:name name + :key-uid key-uid}) + (native-module/sha3 (security/safe-unmask-data password))]})) diff --git a/src/legacy/status_im/multiaccounts/model.cljs b/src/legacy/status_im/multiaccounts/model.cljs new file mode 100644 index 00000000000..167a9fb43e3 --- /dev/null +++ b/src/legacy/status_im/multiaccounts/model.cljs @@ -0,0 +1,5 @@ +(ns legacy.status-im.multiaccounts.model) + +(defn logged-in? + [{:keys [profile/profile]}] + (boolean profile)) diff --git a/src/legacy/status_im/multiaccounts/model_test.cljs b/src/legacy/status_im/multiaccounts/model_test.cljs new file mode 100644 index 00000000000..b92ab31e2ca --- /dev/null +++ b/src/legacy/status_im/multiaccounts/model_test.cljs @@ -0,0 +1,10 @@ +(ns legacy.status-im.multiaccounts.model-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [legacy.status-im.multiaccounts.model :as multiaccounts.model])) + +(deftest logged-in-test + (testing "multiaccount is defined" + (is (multiaccounts.model/logged-in? {:profile/profile {}}))) + (testing "multiaccount is not there" + (is (not (multiaccounts.model/logged-in? {}))))) diff --git a/src/legacy/status_im/multiaccounts/update/core.cljs b/src/legacy/status_im/multiaccounts/update/core.cljs new file mode 100644 index 00000000000..4ce5476dfb9 --- /dev/null +++ b/src/legacy/status_im/multiaccounts/update/core.cljs @@ -0,0 +1,123 @@ +(ns legacy.status-im.multiaccounts.update.core + (:require + [legacy.status-im.data-store.settings :as settings] + [legacy.status-im.utils.deprecated-types :as types] + [status-im.constants :as constants] + [taoensso.timbre :as log] + [utils.ens.core :as utils.ens] + [utils.re-frame :as rf])) + +(rf/defn send-contact-update + [{:keys [db]}] + (let [{:keys [name preferred-name display-name]} (:profile/profile db)] + {:json-rpc/call [{:method "wakuext_sendContactUpdates" + :params [(or preferred-name display-name name) "" ""] + :on-success #(log/debug "sent contact update")}]})) + +(rf/defn update-multiaccount-account-name + "This updates the profile name in the profile list before login" + {:events [:multiaccounts.ui/update-name]} + [{:keys [db] :as cofx} raw-multiaccounts-from-status-go] + (let [{:keys [key-uid name preferred-name + display-name]} (:profile/profile db) + account (some #(and (= (:key-uid %) key-uid) %) raw-multiaccounts-from-status-go)] + (when-let [new-name (and account (or preferred-name display-name name))] + (rf/merge + cofx + {:db (assoc-in db + [:profile/profile :ens-name?] + (utils.ens/is-valid-eth-name? new-name)) + :json-rpc/call [{:method "multiaccounts_updateAccount" + :params [(assoc account :name new-name)] + :on-success #(log/debug "sent multiaccount update")}]})))) + +(rf/defn multiaccount-update + "Takes effects (containing :db) + new multiaccount fields, adds all effects necessary for multiaccount update. + Optionally, one can specify a success-event to be dispatched after fields are persisted." + {:events [:multiaccounts.ui/update]} + [{:keys [db] :as cofx} + setting setting-value + {:keys [dont-sync? on-success] :or {on-success #()}}] + (let [current-multiaccount (:profile/profile db)] + (if (empty? current-multiaccount) + ;; NOTE: this should never happen, but if it does this is a critical error + ;; and it is better to crash than risk having an unstable state + (throw + (js/Error. + "Please shake the phone to report this error and restart the app. multiaccount is currently empty, which means something went wrong when trying to update it with")) + (rf/merge + cofx + {:db (if setting-value + (assoc-in db [:profile/profile setting] setting-value) + (update db :profile/profile dissoc setting)) + :json-rpc/call + [{:method "settings_saveSetting" + :params [setting setting-value] + :on-success on-success}]} + + (when (#{:name :preferred-name} setting) + (constantly {:profile/get-profiles-overview #(rf/dispatch [:multiaccounts.ui/update-name %])})) + + (when (and (not dont-sync?) (#{:name :preferred-name} setting)) + (send-contact-update)))))) + +(rf/defn clean-seed-phrase + "A helper function that removes seed phrase from storage." + [cofx on-success] + (multiaccount-update cofx :mnemonic nil on-success)) + +(defn augment-synchronized-recent-stickers + "Add 'url' parameter to stickers that are synchronized from other devices. + It is not sent from another devices but we have it in our db." + [synced-stickers stickers-from-db] + (mapv #(assoc % + :url + (->> (get stickers-from-db (or (:packID %) (:pack %))) + (:stickers) + (filter (fn [sticker-db] (= (:hash sticker-db) (:hash %)))) + (first) + (:url))) + synced-stickers)) + +(rf/defn optimistic + [{:keys [db]} setting setting-value] + (let [setting-value (if (= :currency setting) + (settings/rpc->currency setting-value) + setting-value) + db (case setting + :stickers/packs-pending + (let [packs-pending (keys (js->clj setting-value))] + (update db :stickers/packs-pending conj packs-pending)) + :stickers/packs-installed + (let [packs-installed-keys (keys (js->clj setting-value))] + (reduce #(assoc-in %1 + [:stickers/packs %2 :status] + constants/sticker-pack-status-installed) + db + packs-installed-keys)) + :stickers/recent-stickers + (let [recent-stickers-from-remote (augment-synchronized-recent-stickers + (types/js->clj setting-value) + (:stickers/packs db))] + (assoc db :stickers/recent-stickers recent-stickers-from-remote)) + db)] + {:db (if setting-value + (assoc-in db [:profile/profile setting] setting-value) + (update db :profile/profile dissoc setting)) + :fx [(when (= setting :currency) + [:dispatch [:wallet/get-wallet-token-for-all-accounts]])]})) + +(rf/defn set-many-js + [cofx settings-js] + (apply rf/merge + cofx + (map + #(optimistic + (keyword (.-name %)) + (.-value %)) + settings-js))) + +(rf/defn toggle-backup-enabled + {:events [:multiaccounts.ui/switch-backup-enabled]} + [cofx enabled?] + (multiaccount-update cofx :backup-enabled? enabled? {})) diff --git a/src/legacy/status_im/multiaccounts/update/core_test.cljs b/src/legacy/status_im/multiaccounts/update/core_test.cljs new file mode 100644 index 00000000000..4712ed98587 --- /dev/null +++ b/src/legacy/status_im/multiaccounts/update/core_test.cljs @@ -0,0 +1,57 @@ +(ns legacy.status-im.multiaccounts.update.core-test + (:require + [clojure.test :refer-macros [deftest is testing]] + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update])) + +(deftest multiaccount-update-test + ;;TODO this test case actually shows that we are doing a needless rpc call when + ;;there is no changes, but it is an edge case that shouldn't really happen + (let [efx (multiaccounts.update/multiaccount-update + {:db {:profile/profile {:not-empty "would throw an error if was empty"}}} + nil + nil + {}) + json-rpc (into #{} (map :method (:json-rpc/call efx)))] + (is (json-rpc "settings_saveSetting")) + (is (= (get-in efx [:db :profile/profile]) {:not-empty "would throw an error if was empty"})))) + +(deftest clean-seed-phrase-test + (let [efx (multiaccounts.update/clean-seed-phrase + {:db {:profile/profile {:mnemonic "lalalala"}}} + {}) + json-rpc (into #{} (map :method (:json-rpc/call efx)))] + (is (json-rpc "settings_saveSetting")) + (is (nil? (get-in efx [:db :profile/profile :mnemonic]))))) + +(deftest update-multiaccount-account-name-test + (let [cofx {:db {:profile/profile {:key-uid 1 + :name "name" + :preferred-name "preferred-name" + :display-name "display-name"}}} + raw-multiaccounts-from-status-go [{:key-uid 1 :name "old-name"}]] + (testing "wrong account" + (is (nil? (multiaccounts.update/update-multiaccount-account-name cofx [])))) + (testing "name priority preferred-name > display-name > name" + (let [new-account-name= (fn [efx new-name] + (-> efx + :json-rpc/call + first + :params + first + :name + (= new-name)))] + (is (new-account-name= + (multiaccounts.update/update-multiaccount-account-name + cofx + raw-multiaccounts-from-status-go) + "preferred-name")) + (is (new-account-name= + (multiaccounts.update/update-multiaccount-account-name + (update-in cofx [:db :profile/profile] dissoc :preferred-name) + raw-multiaccounts-from-status-go) + "display-name")) + (is (new-account-name= + (multiaccounts.update/update-multiaccount-account-name + (update-in cofx [:db :profile/profile] dissoc :preferred-name :display-name) + raw-multiaccounts-from-status-go) + "name")))))) diff --git a/src/legacy/status_im/node/core.cljs b/src/legacy/status_im/node/core.cljs new file mode 100644 index 00000000000..cb3e60665e2 --- /dev/null +++ b/src/legacy/status_im/node/core.cljs @@ -0,0 +1,10 @@ +(ns legacy.status-im.node.core + (:require + [legacy.status-im.utils.deprecated-types :as types])) + +(defn fleets + [{:keys [custom-fleets]}] + (as-> [(js/require "./fleets.js")] $ + (mapv #(:fleets (types/json->clj %)) $) + (conj $ custom-fleets) + (reduce merge $))) diff --git a/src/legacy/status_im/notifications/wallet.cljs b/src/legacy/status_im/notifications/wallet.cljs new file mode 100644 index 00000000000..df82d95a7fe --- /dev/null +++ b/src/legacy/status_im/notifications/wallet.cljs @@ -0,0 +1,84 @@ +(ns legacy.status-im.notifications.wallet + (:require + [clojure.string :as string] + [legacy.status-im.ethereum.decode :as decode] + [legacy.status-im.ethereum.tokens :as tokens] + [legacy.status-im.utils.utils :as utils] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) + +(def default-erc20-token + {:symbol :ERC20 + :decimals 18 + :name "ERC20"}) + +(defn preference= + [x y] + (and (= (:service x) (:service y)) + (= (:event x) (:event y)) + (= (:identifier x) (:identifier y)))) + +(defn- update-preference + [all new-preference] + (conj (filter (comp not (partial preference= new-preference)) + all) + new-preference)) + +(rf/defn switch-transaction-notifications + {:events [:push-notifications.wallet/switch-transactions]} + [{:keys [db]} enabled?] + {:db (update db + :push-notifications/preferences + update-preference + {:enabled? enabled? + :service "wallet" + :event "transaction" + :identifier "all"}) + :json-rpc/call [{:method "localnotifications_switchWalletNotifications" + :params [enabled?] + :on-success #(log/info "[push-notifications] switch-transaction successful" %) + :on-error #(log/error "[push-notifications] switch-transaction error" %)}]}) + +(defn create-transfer-notification + [{db :db} + {{:keys [state from to fromAccount toAccount value erc20 contract network]} + :body + :as notification}] + (let [token (if erc20 + (get-in db + [:wallet-legacy/all-tokens (string/lower-case contract)] + default-erc20-token) + (tokens/native-currency network)) + amount (money/wei->ether (decode/uint value)) + to (or (:name toAccount) (utils/get-shortened-address to)) + from (or (:name fromAccount) (utils/get-shortened-address from)) + title (case state + "inbound" (i18n/label :t/push-inbound-transaction + {:value amount + :currency (:symbol token)}) + "outbound" (i18n/label :t/push-outbound-transaction + {:value amount + :currency (:symbol token)}) + "failed" (i18n/label :t/push-failed-transaction + {:value amount + :currency (:symbol token)}) + nil) + description (case state + "inbound" (i18n/label :t/push-inbound-transaction-body + {:from from + :to to}) + "outbound" (i18n/label :t/push-outbound-transaction-body + {:from from + :to to}) + "failed" (i18n/label :t/push-failed-transaction-body + {:value amount + :currency (:symbol token) + :to to}) + nil)] + {:title title + :icon (get-in token [:icon :source]) + :deepLink (:deepLink notification) + :user-info notification + :message description})) diff --git a/src/legacy/status_im/pairing/core.cljs b/src/legacy/status_im/pairing/core.cljs new file mode 100644 index 00000000000..57b0eddd083 --- /dev/null +++ b/src/legacy/status_im/pairing/core.cljs @@ -0,0 +1,276 @@ +(ns legacy.status-im.pairing.core + (:require + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [re-frame.core :as re-frame] + [react-native.platform :as utils.platform] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.config :as config] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn enable-installation-rpc + [installation-id on-success on-error] + (json-rpc/call {:method "wakuext_enableInstallation" + :params [installation-id] + :on-success on-success + :on-error on-error})) + +(defn disable-installation-rpc + [installation-id on-success on-error] + (json-rpc/call {:method "wakuext_disableInstallation" + :params [installation-id] + :on-success on-success + :on-error on-error})) + +(defn set-installation-metadata-rpc + [installation-id metadata on-success on-error] + (json-rpc/call {:method "wakuext_setInstallationMetadata" + :params [installation-id metadata] + :on-success on-success + :on-error on-error})) + +(defn get-our-installations-rpc + [on-success on-error] + (json-rpc/call {:method "wakuext_getOurInstallations" + :params [] + :on-success on-success + :on-error on-error})) + +(defn compare-installation + "Sort installations, first by our installation-id, then on whether is + enabled, and last on timestamp value" + [our-installation-id a b] + (cond + (= our-installation-id (:installation-id a)) + -1 + (= our-installation-id (:installation-id b)) + 1 + :else + (let [enabled-compare (compare (:enabled? b) (:enabled? a))] + (if (not= 0 enabled-compare) + enabled-compare + (compare (:timestamp a) (:timestamp b)))))) + +(defn sort-installations + [our-installation-id installations] + (sort (partial compare-installation our-installation-id) installations)) + +(rf/defn send-pair-installation + {:events [:pairing.ui/pair-devices-pressed]} + [_] + {:json-rpc/call [{:method "wakuext_sendPairInstallation" + :params [] + :on-success #(log/info "sent pair installation message")}]}) + +(rf/defn prompt-dismissed + {:events [:pairing.ui/prompt-dismissed]} + [{:keys [db]}] + {:db (assoc-in db [:pairing/prompt-user-pop-up] false)}) + +(rf/defn prompt-accepted + {:events [:pairing.ui/prompt-accepted]} + [{:keys [db] :as cofx}] + (rf/merge cofx + {:db (assoc-in db [:pairing/prompt-user-pop-up] false)} + (navigation/navigate-to :screen/installations nil))) + +(rf/defn prompt-user-on-new-installation + [{:keys [db]}] + (when-not config/pairing-popup-disabled? + {:db (assoc-in db [:pairing/prompt-user-pop-up] true) + :ui/show-confirmation {:title (i18n/label :t/pairing-new-installation-detected-title) + :content (i18n/label + :t/pairing-new-installation-detected-content) + :confirm-button-text (i18n/label :t/pairing-go-to-installation) + :cancel-button-text (i18n/label :t/cancel) + :on-cancel #(re-frame/dispatch [:pairing.ui/prompt-dismissed]) + :on-accept #(re-frame/dispatch [:pairing.ui/prompt-accepted])}})) + +(rf/defn set-name + "Set the name of the device" + {:events [:pairing.ui/set-name-pressed]} + [{:keys [db]} installation-name] + (let [our-installation-id (get-in db [:profile/profile :installation-id])] + {:pairing/set-installation-metadata [our-installation-id + {:name installation-name + :deviceType utils.platform/os}]})) + +(rf/defn init + [_cofx] + {:pairing/get-our-installations nil}) + +(rf/defn enable + [{:keys [db]} installation-id] + {:db (assoc-in db + [:pairing/installations installation-id :enabled?] + true)}) + +(rf/defn disable + [{:keys [db]} installation-id] + {:db (assoc-in db + [:pairing/installations installation-id :enabled?] + false)}) + +(defn handle-enable-installation-response-success + "Callback to dispatch on enable signature response" + [installation-id] + (re-frame/dispatch [:pairing.callback/enable-installation-success installation-id])) + +(defn handle-disable-installation-response-success + "Callback to dispatch on disable signature response" + [installation-id] + (re-frame/dispatch [:pairing.callback/disable-installation-success installation-id])) + +(defn handle-set-installation-metadata-response-success + "Callback to dispatch on set-installation-metadata response" + [installation-id metadata] + (re-frame/dispatch [:pairing.callback/set-installation-metadata-success installation-id metadata])) + +(defn handle-get-our-installations-response-success + "Callback to dispatch on get-our-installation response" + [result] + (re-frame/dispatch [:pairing.callback/get-our-installations-success result])) + +(defn enable-installation! + [installation-id] + (enable-installation-rpc + installation-id + (partial handle-enable-installation-response-success installation-id) + nil)) + +(defn disable-installation! + [installation-id] + (disable-installation-rpc + installation-id + (partial handle-disable-installation-response-success installation-id) + nil)) + +(defn set-installation-metadata! + [installation-id metadata] + (set-installation-metadata-rpc + installation-id + metadata + (partial handle-set-installation-metadata-response-success installation-id metadata) + nil)) + +(defn get-our-installations + [] + (get-our-installations-rpc handle-get-our-installations-response-success nil)) + +(rf/defn enable-fx + {:events [:pairing.ui/enable-installation-pressed]} + [cofx installation-id] + (if (< (count (filter :enabled? (vals (get-in cofx [:db :pairing/installations])))) + (inc config/max-installations)) + {:pairing/enable-installation [installation-id]} + {:effects.utils/show-popup {:title (i18n/label :t/pairing-maximum-number-reached-title) + + :content (i18n/label :t/pairing-maximum-number-reached-content)}})) + +(rf/defn disable-fx + {:events [:pairing.ui/disable-installation-pressed]} + [_ installation-id] + {:pairing/disable-installation [installation-id]}) + +(re-frame/reg-fx + :pairing/enable-installation + (fn [[installation-id]] + (enable-installation! installation-id))) + +(re-frame/reg-fx + :pairing/disable-installation + (fn [[installation-id]] + (disable-installation! installation-id))) + +(re-frame/reg-fx + :pairing/set-installation-metadata + (fn [[installation-id metadata]] + (set-installation-metadata! installation-id metadata))) + +(re-frame/reg-fx + :pairing/get-our-installations + get-our-installations) + +(rf/defn send-installation-messages + {:events [:pairing.ui/synchronize-installation-pressed]} + [{:keys [db]}] + (let [multiaccount (:profile/profile db) + {:keys [name preferred-name]} multiaccount] + {:json-rpc/call [{:method "wakuext_syncDevices" + :params [(or preferred-name name)] + :on-success #(log/debug "successfully synced devices")}]})) + +(defn installation<-rpc + [{:keys [metadata id enabled timestamp]}] + {:installation-id id + :name (:name metadata) + :timestamp timestamp + :device-type (:deviceType metadata) + :enabled? enabled}) + +(rf/defn update-installation + {:events [:pairing.callback/set-installation-metadata-success]} + [{:keys [db]} installation-id metadata] + {:db (update-in db + [:pairing/installations installation-id] + assoc + :installation-id installation-id + :name (:name metadata) + :device-type (:deviceType metadata))}) + +(rf/defn handle-installations + [{:keys [db]} installations] + {:db (update db + :pairing/installations + #(reduce + (fn [acc {:keys [id] :as i}] + (update acc id merge (installation<-rpc i))) + % + installations))}) + +(rf/defn load-installations + {:events [:pairing.callback/get-our-installations-success]} + [{:keys [db]} installations] + {:db (assoc db + :pairing/installations + (reduce + (fn [acc {:keys [id] :as i}] + (assoc acc + id + (installation<-rpc i))) + {} + installations))}) + +(rf/defn finish-seed-phrase-fallback-syncing + {:events [:pairing/finish-seed-phrase-fallback-syncing]} + [{:keys [db]}] + {:fx [[:json-rpc/call + [{:method "wakuext_enableInstallationAndPair" + :params [{:installationId (:syncing/installation-id db)}] + :js-response true + :on-success [:sanitize-messages-and-process-response]}]]]}) + +(rf/defn pair-and-sync + {:events [:pairing/pair-and-sync]} + [_cofx installation-id] + {:fx [[:json-rpc/call + [{:method "wakuext_enableInstallationAndSync" + :params [{:installationId installation-id}] + :js-response true + :on-success [:sanitize-messages-and-process-response]}]]]}) + +(rf/defn enable-installation-success + {:events [:pairing.callback/enable-installation-success]} + [cofx installation-id] + (rf/merge cofx + (enable installation-id) + (multiaccounts.update/send-contact-update))) + +(rf/defn disable-installation-success + {:events [:pairing.callback/disable-installation-success]} + [cofx installation-id] + (rf/merge cofx + (disable installation-id) + (multiaccounts.update/send-contact-update))) diff --git a/src/legacy/status_im/popover/core.cljs b/src/legacy/status_im/popover/core.cljs new file mode 100644 index 00000000000..c9fc60c70a0 --- /dev/null +++ b/src/legacy/status_im/popover/core.cljs @@ -0,0 +1,26 @@ +(ns legacy.status-im.popover.core + (:require + [utils.re-frame :as rf])) + +(rf/defn show-popover + {:events [:show-popover]} + [_ value] + (let [delay-ms (or (:delay-ms value) 250) + value (dissoc value :delay-ms)] + {:show-popover nil + ;; We should probably refactor to start the animation on mount, so that the delay can be + ;; removed. See comment for more details: + ;; https://github.com/status-im/status-mobile/pull/15222#issuecomment-1450162137 + :dispatch-later [{:ms delay-ms :dispatch [:show-popover-db value]}] + :dismiss-keyboard nil})) + +(rf/defn show-popover-db + {:events [:show-popover-db]} + [{:keys [db]} value] + {:db (assoc db :popover/popover value)}) + +(rf/defn hide-popover + {:events [:hide-popover]} + [{:keys [db]}] + {:db (dissoc db :popover/popover) + :hide-popover nil}) diff --git a/src/legacy/status_im/profile/core.cljs b/src/legacy/status_im/profile/core.cljs new file mode 100644 index 00000000000..94d228bd83b --- /dev/null +++ b/src/legacy/status_im/profile/core.cljs @@ -0,0 +1,74 @@ +(ns legacy.status-im.profile.core + (:require + [clojure.string :as string] + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [legacy.status-im.ui.components.react :as react] + [re-frame.core :as re-frame] + [utils.re-frame :as rf])) + +(re-frame/reg-fx + :copy-to-clipboard + (fn [value] + (react/copy-to-clipboard value))) + +(re-frame/reg-fx + :show-tooltip + (fn [] + (let [tooltips (atom {})] + (fn [tooltip-id] + (when-let [{:keys [interval-id]} (@tooltips tooltip-id)] + (js/clearInterval interval-id)) + (let [interval-id (js/setInterval + #(let [{:keys [opacity interval-id cnt]} (@tooltips tooltip-id)] + (when opacity + (swap! tooltips assoc-in [tooltip-id :cnt] (inc cnt)) + (if (and opacity (>= 0.0 opacity)) + (do + (js/clearInterval interval-id) + (re-frame/dispatch [:set-in [:tooltips tooltip-id] nil]) + (swap! tooltips dissoc interval-id)) + (do (re-frame/dispatch [:set-in [:tooltips tooltip-id] opacity]) + (when (< 10 cnt) + (swap! tooltips assoc-in + [tooltip-id :opacity] + (- opacity 0.05))))))) + 100)] + (swap! tooltips assoc tooltip-id {:opacity 1.0 :interval-id interval-id :cnt 0})))))) + +(rf/defn finish-success + {:events [:my-profile/finish-success]} + [{:keys [db]}] + {:db (update db :my-profile/seed assoc :step :finish :error nil :word nil)}) + +(rf/defn finish + {:events [:my-profile/finish]} + [cofx] + (multiaccounts.update/clean-seed-phrase + cofx + {:on-success #(re-frame/dispatch [:my-profile/finish-success])})) + +(rf/defn enter-two-random-words + {:events [:my-profile/enter-two-random-words]} + [{:keys [db]}] + (let [{:keys [mnemonic]} (:profile/profile db) + shuffled-mnemonic (shuffle (map-indexed vector (string/split mnemonic #" ")))] + {:db (assoc db + :my-profile/seed + {:step :first-word + :first-word (first shuffled-mnemonic) + :second-word (second shuffled-mnemonic)})})) + +(rf/defn set-step + {:events [:my-profile/set-step]} + [{:keys [db]} step] + {:db (update db :my-profile/seed assoc :step step :error nil :word nil)}) + +(rf/defn copy-to-clipboard + {:events [:copy-to-clipboard]} + [_ value] + {:copy-to-clipboard value}) + +(rf/defn show-tooltip + {:events [:show-tooltip]} + [_ tooltip-id] + {:show-tooltip tooltip-id}) diff --git a/src/legacy/status_im/qr_scanner/core.cljs b/src/legacy/status_im/qr_scanner/core.cljs new file mode 100644 index 00000000000..3d8bf1ffca1 --- /dev/null +++ b/src/legacy/status_im/qr_scanner/core.cljs @@ -0,0 +1,116 @@ +(ns legacy.status-im.qr-scanner.core + (:require + [legacy.status-im.group-chats.core :as group-chats] + [legacy.status-im.utils.utils :as utils] + [re-frame.core :as re-frame] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.ethereum.chain :as chain] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/defn scan-qr-code + {:events [::scan-code]} + [_ opts] + {:request-permissions-fx + {:permissions [:camera] + :on-allowed #(re-frame/dispatch [:open-modal :screen/qr-scanner opts]) + :on-denied (fn [] + (utils/set-timeout + #(utils/show-popup (i18n/label :t/error) + (i18n/label :t/camera-access-error)) + 50))}}) + +(rf/defn set-qr-code + {:events [:qr-scanner.callback/scan-qr-code-success]} + [_ opts data] + (when-let [handler (:handler opts)] + {:dispatch [handler data opts]})) + +(rf/defn set-qr-code-cancel + {:events [:qr-scanner.callback/scan-qr-code-cancel]} + [cofx opts] + (rf/merge cofx + (navigation/navigate-back nil) + (when-let [handler (:cancel-handler opts)] + (fn [] {:dispatch [handler opts]})))) + +(rf/defn handle-browse + [cofx {:keys [url]}] + (rf/merge cofx + {:browser/show-browser-selection url} + (navigation/navigate-back nil))) + +(defn own-public-key? + [{:keys [profile/profile]} public-key] + (= (:public-key profile) public-key)) + +(rf/defn handle-private-chat + [{:keys [db]} {:keys [chat-id]}] + (if-not (own-public-key? db chat-id) + {:dispatch [:chat.ui/start-chat chat-id]} + {:effects.utils/show-popup {:title (i18n/label :t/unable-to-read-this-code) + :content (i18n/label :t/can-not-add-yourself)}})) + +(rf/defn handle-group-chat + [cofx params] + (group-chats/create-from-link cofx params)) + +(rf/defn handle-view-profile + [{:keys [db] :as cofx} {:keys [public-key ens-name]}] + (let [own (own-public-key? db public-key)] + (cond + (and public-key own) + (rf/merge cofx + (navigation/pop-to-root :screen/shell-stack) + (navigation/navigate-to :screen/my-profile nil)) + + (and public-key (not own)) + (rf/merge cofx + {:dispatch [:chat.ui/show-profile public-key ens-name]} + (navigation/navigate-back nil)) + + :else + {:effects.utils/show-popup {:title (i18n/label :t/unable-to-read-this-code) + :content (i18n/label :t/ens-name-not-found) + :on-dismiss #(re-frame/dispatch [:pop-to-root + :screen/shell-stack])}}))) + +(rf/defn handle-eip681 + [cofx data] + (rf/merge cofx + {:dispatch-n [[:shell/change-tab :screen/wallet-stack] + [:wallet-legacy/parse-eip681-uri-and-resolve-ens data]]} + (navigation/pop-to-root :screen/shell-stack))) + +(rf/defn handle-local-pairing + {:events [::handle-local-pairing-uri]} + [_ data] + {:dispatch [:syncing/input-connection-string-for-bootstrapping data]}) + +(rf/defn match-scan + {:events [::match-scanned-value]} + [cofx {:keys [type] :as data}] + (case type + :group-chat (handle-group-chat cofx data) + :private-chat (handle-private-chat cofx data) + :contact (handle-view-profile cofx data) + :browser (handle-browse cofx data) + :eip681 (handle-eip681 cofx data) + :localpairing (handle-local-pairing cofx data) + (do + (log/info "Unable to find matcher for scanned value" + {:type type + :event ::match-scanned-value}) + {:dispatch [:navigate-back] + :effects.utils/show-popup {:title (i18n/label :t/unable-to-read-this-code) + :on-dismiss #(re-frame/dispatch [:pop-to-root + :screen/shell-stack])}}))) + +(rf/defn on-scan + {:events [::on-scan-success]} + [{:keys [db]} uri] + {:router/handle-uri {:chain (chain/chain-keyword db) + :chats (get db :chats) + :uri uri + :cb #(re-frame/dispatch [::match-scanned-value %])}}) diff --git a/src/legacy/status_im/react_native/resources.cljs b/src/legacy/status_im/react_native/resources.cljs new file mode 100644 index 00000000000..bd4d7c20abb --- /dev/null +++ b/src/legacy/status_im/react_native/resources.cljs @@ -0,0 +1,62 @@ +(ns legacy.status-im.react-native.resources) + +(def ui + {:empty-chats-header (js/require "../resources/images/ui/empty-chats-header.png") + :welcome (js/require "../resources/images/ui/welcome.jpg") + :welcome-dark (js/require "../resources/images/ui/welcome-dark.jpg") + :chat (js/require "../resources/images/ui/chat.jpg") + :chat-dark (js/require "../resources/images/ui/chat-dark.jpg") + :wallet (js/require "../resources/images/ui/wallet.jpg") + :browser (js/require "../resources/images/ui/browser.jpg") + :wallet-dark (js/require "../resources/images/ui/wallet-dark.jpg") + :browser-dark (js/require "../resources/images/ui/browser-dark.jpg") + :keys (js/require "../resources/images/ui/keys.jpg") + :keys-dark (js/require "../resources/images/ui/keys-dark.jpg") + :lock (js/require "../resources/images/ui/lock.png") + :empty-wallet (js/require "../resources/images/ui/empty-wallet.png") + :tribute-to-talk (js/require "../resources/images/ui/tribute-to-talk.png") + :keycard-card (js/require "../resources/images/ui/hardwallet-card.png") + :keycard-lock (js/require "../resources/images/ui/keycard-lock.png") + :keycard (js/require "../resources/images/ui/keycard.png") + :keycard-logo (js/require "../resources/images/ui/keycard-logo.png") + :keycard-logo-blue (js/require "../resources/images/ui/keycard-logo-blue.png") + :keycard-logo-gray (js/require "../resources/images/ui/keycard-logo-gray.png") + :keycard-key (js/require "../resources/images/ui/keycard-key.png") + :keycard-empty (js/require "../resources/images/ui/keycard-empty.png") + :keycard-phone (js/require "../resources/images/ui/keycard-phone.png") + :keycard-connection (js/require "../resources/images/ui/keycard-connection.png") + :keycard-wrong (js/require "../resources/images/ui/keycard-wrong.png") + :not-keycard (js/require "../resources/images/ui/not-keycard.png") + :status-logo (js/require "../resources/images/ui/status-logo.png") + :warning-sign (js/require "../resources/images/ui/warning-sign.png") + :phone-nfc-on (js/require "../resources/images/ui/phone-nfc-on.png") + :phone-nfc-off (js/require "../resources/images/ui/phone-nfc-off.png") + :dapp-store (js/require "../resources/images/ui/dapp-store.png") + :ens-header (js/require "../resources/images/ui/ens-header.png") + :ens-header-dark (js/require "../resources/images/ui/ens-header-dark.png") + :unfurl (js/require "../resources/images/ui/unfurl.png") + :unfurl-dark (js/require "../resources/images/ui/unfurl-dark.png") + :new-chat-header (js/require "../resources/images/ui/new-chat-header.png") + :onboarding-phone (js/require "../resources/images/ui/onboarding-phone.png") + :theme-dark (js/require "../resources/images/ui/theme-dark.png") + :theme-light (js/require "../resources/images/ui/theme-light.png") + :theme-system (js/require "../resources/images/ui/theme-system.png") + :notifications (js/require "../resources/images/ui/notifications.png") + :collectible (js/require "../resources/images/ui/collectible.png") + :collectible-dark (js/require "../resources/images/ui/collectible-dark.png") + :hand-wave (js/require "../resources/images/ui/hand-wave.png") + :graph (js/require "../resources/images/ui/graph.png") + :discover (js/require "../resources/images/ui/discover.png") + :no-contacts (js/require "../resources/images/ui/no-contacts.png") + :no-contacts-dark (js/require "../resources/images/ui/no-contacts-dark.png")}) + +(def loaded-images (atom {})) + +(defn get-image + [k] + (if (contains? @loaded-images k) + (get @loaded-images k) + (get (swap! loaded-images assoc + k + (get ui k)) + k))) diff --git a/src/legacy/status_im/search/core.cljs b/src/legacy/status_im/search/core.cljs new file mode 100644 index 00000000000..330039558cd --- /dev/null +++ b/src/legacy/status_im/search/core.cljs @@ -0,0 +1,18 @@ +(ns legacy.status-im.search.core + (:require + [utils.re-frame :as rf])) + +(rf/defn currency-filter-changed + {:events [:search/currency-filter-changed]} + [cofx search-filter] + {:db (assoc-in (:db cofx) [:ui/search :currency-filter] search-filter)}) + +(rf/defn token-filter-changed + {:events [:wallet-legacy/search-token-filter-changed]} + [cofx search-filter] + {:db (assoc-in (:db cofx) [:ui/search :token-filter] search-filter)}) + +(rf/defn recipient-filter-changed + {:events [:wallet-legacy/search-recipient-filter-changed]} + [cofx search-filter] + {:db (assoc-in (:db cofx) [:ui/search :recipient-filter] search-filter)}) diff --git a/src/legacy/status_im/stickers/core.cljs b/src/legacy/status_im/stickers/core.cljs new file mode 100644 index 00000000000..9224369b634 --- /dev/null +++ b/src/legacy/status_im/stickers/core.cljs @@ -0,0 +1,93 @@ +(ns legacy.status-im.stickers.core + (:require + [legacy.status-im.utils.utils :as utils] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [utils.ethereum.chain :as chain] + [utils.re-frame :as rf])) + +(re-frame/reg-fx + :stickers/set-pending-timeout-fx + (fn [] + (utils/set-timeout #(re-frame/dispatch [:stickers/pending-timeout]) + 10000))) + +(rf/defn install-stickers-pack + {:events [:stickers/install-pack]} + [{db :db :as cofx} id] + (rf/merge + cofx + {:db (assoc-in db [:stickers/packs id :status] constants/sticker-pack-status-installed) + :json-rpc/call [{:method "stickers_install" + :params [(chain/chain-id db) id] + :on-success #()}]})) + +(rf/defn pending-pack + {:events [:stickers/pending-pack]} + [{db :db} id] + {:db (-> db + (assoc-in [:stickers/packs id :status] + constants/sticker-pack-status-pending) + (update :stickers/packs-pending conj id)) + :stickers/set-pending-timeout-fx nil + :json-rpc/call [{:method "stickers_addPending" + :params [(chain/chain-id db) (int id)] + :on-success #()}]}) + +(rf/defn pending-timeout + {:events [:stickers/pending-timeout]} + [{{:stickers/keys [packs-pending] :as db} :db}] + (when (seq packs-pending) + {:json-rpc/call [{:method "stickers_processPending" + :params [(chain/chain-id db)] + :on-success #(re-frame/dispatch [:stickers/stickers-process-pending-success + %])}]})) + +(rf/defn stickers-process-pending-success + {:events [:stickers/stickers-process-pending-success]} + [{{:stickers/keys [packs-pending packs] :as db} :db} purchased] + (let [purchased-ids (map :id (vals purchased)) + packs-pending (apply disj packs-pending purchased-ids) + packs (reduce (fn [packs id] + (assoc-in packs [id :status] constants/sticker-pack-status-owned)) + packs + purchased-ids)] + (merge + {:db (-> db + (assoc :stickers/packs packs) + (assoc :stickers/packs-pending packs-pending))} + (when (seq packs-pending) + {:stickers/set-pending-timeout-fx nil})))) + +(rf/defn stickers-market-success + {:events [:stickers/stickers-market-success]} + [{:keys [db]} packs] + (let [packs (reduce (fn [acc pack] (assoc acc (:id pack) pack)) {} packs)] + {:db (update db :stickers/packs merge packs)})) + +(rf/defn stickers-installed-success + {:events [:stickers/stickers-installed-success]} + [{:keys [db]} packs] + (let [packs (reduce (fn [acc [_ pack]] (assoc acc (:id pack) pack)) {} packs)] + {:db (update db :stickers/packs merge packs)})) + +(rf/defn stickers-pending-success + {:events [:stickers/stickers-pending-success]} + [{:keys [db]} packs] + (let [packs (reduce (fn [acc [_ pack]] (assoc acc (:id pack) pack)) {} packs)] + (merge + {:db (-> db + (assoc :stickers/packs-pending (into #{} (keys packs))) + (update :stickers/packs merge packs))} + (when (seq packs) + {:stickers/set-pending-timeout-fx nil})))) + +(rf/defn stickers-recent-success + {:events [:stickers/stickers-recent-success]} + [{:keys [db]} packs] + {:db (assoc db :stickers/recent-stickers packs)}) + +(rf/defn select-pack + {:events [:stickers/select-pack]} + [{:keys [db]} id] + {:db (assoc db :stickers/selected-pack id)}) diff --git a/src/legacy/status_im/subs/browser.cljs b/src/legacy/status_im/subs/browser.cljs new file mode 100644 index 00000000000..1f01982cc94 --- /dev/null +++ b/src/legacy/status_im/subs/browser.cljs @@ -0,0 +1,33 @@ +(ns legacy.status-im.subs.browser + (:require + [legacy.status-im.browser.core :as browser] + [re-frame.core :as re-frame])) + +(re-frame/reg-sub + :browser/browsers + :<- [:browsers] + (fn [browsers] + (reduce (fn [acc [k browser]] + (update acc k assoc :url (browser/get-current-url browser))) + browsers + browsers))) + +(re-frame/reg-sub + :browser/browsers-vals + :<- [:browser/browsers] + (fn [browsers] + (reverse (vals browsers)))) + +(re-frame/reg-sub + :get-current-browser + :<- [:browser/options] + :<- [:browser/browsers] + (fn [[options browsers]] + (let [browser (get browsers (:browser-id options))] + (assoc browser :secure? (browser/secure? browser options))))) + +(re-frame/reg-sub + :bookmarks/active + :<- [:bookmarks] + (fn [bookmarks] + (into {} (remove #(:removed (second %)) bookmarks)))) diff --git a/src/legacy/status_im/subs/mailservers.cljs b/src/legacy/status_im/subs/mailservers.cljs new file mode 100644 index 00000000000..d9949f0a46e --- /dev/null +++ b/src/legacy/status_im/subs/mailservers.cljs @@ -0,0 +1,11 @@ +(ns legacy.status-im.subs.mailservers + (:require + [re-frame.core :as re-frame])) + +(re-frame/reg-sub + :mailserver/current-name + :<- [:mailserver/current-id] + :<- [:fleets/current-fleet] + :<- [:mailserver/mailservers] + (fn [[current-mailserver-id current-fleet mailservers]] + (get-in mailservers [current-fleet current-mailserver-id :name]))) diff --git a/src/legacy/status_im/subs/root.cljs b/src/legacy/status_im/subs/root.cljs new file mode 100644 index 00000000000..953b6ef31f4 --- /dev/null +++ b/src/legacy/status_im/subs/root.cljs @@ -0,0 +1,110 @@ +(ns legacy.status-im.subs.root + (:require + legacy.status-im.subs.browser + legacy.status-im.subs.mailservers + [re-frame.core :as re-frame])) + +(defn reg-root-key-sub + [sub-name db-key] + (re-frame/reg-sub sub-name (fn [db] (get db db-key)))) + +;;general +(reg-root-key-sub :visibility-status-popover/popover :visibility-status-popover/popover) +(reg-root-key-sub :visibility-status-updates :visibility-status-updates) +(reg-root-key-sub :fleets/custom-fleets :custom-fleets) +(reg-root-key-sub :ui/search :ui/search) +(reg-root-key-sub :peer-stats/count :peer-stats/count) +(reg-root-key-sub :peers-summary :peers-summary) +(reg-root-key-sub :web3-node-version :web3-node-version) + +;;bottom sheet old +(reg-root-key-sub :bottom-sheet/show? :bottom-sheet/show?) +(reg-root-key-sub :bottom-sheet/view :bottom-sheet/view) +(reg-root-key-sub :bottom-sheet/options :bottom-sheet/options) + +;;browser +(reg-root-key-sub :browsers :browser/browsers) +(reg-root-key-sub :browser/options :browser/options) +(reg-root-key-sub :dapps/permissions :dapps/permissions) +(reg-root-key-sub :bookmarks :bookmarks/bookmarks) +(reg-root-key-sub :browser/screen-id :browser/screen-id) + +;;stickers +(reg-root-key-sub :stickers/selected-pack :stickers/selected-pack) +(reg-root-key-sub :stickers/packs :stickers/packs) +(reg-root-key-sub :stickers/recent-stickers :stickers/recent-stickers) + +;;mailserver +(reg-root-key-sub :mailserver/current-id :mailserver/current-id) +(reg-root-key-sub :mailserver/mailservers :mailserver/mailservers) + +;;contacts +(reg-root-key-sub :contacts/contacts-raw :contacts/contacts) +(reg-root-key-sub :contacts/current-contact-identity :contacts/identity) +(reg-root-key-sub :contacts/current-contact-ens-name :contacts/ens-name) +(reg-root-key-sub :contacts/new-identity :contacts/new-identity) +(reg-root-key-sub :group/selected-contacts :group/selected-contacts) +(reg-root-key-sub :contacts/search-query :contacts/search-query) + +;;wallet +(reg-root-key-sub :prices :prices) +(reg-root-key-sub :prices-loading? :prices-loading?) +(reg-root-key-sub :add-account :add-account) +(reg-root-key-sub :buy-crypto/on-ramps :buy-crypto/on-ramps) + +(reg-root-key-sub :wallet-legacy :wallet-legacy) +(reg-root-key-sub :wallet-legacy.transactions :wallet-legacy.transactions) +(reg-root-key-sub :wallet-legacy/custom-token-screen :wallet-legacy/custom-token-screen) +(reg-root-key-sub :wallet-legacy/prepare-transaction :wallet-legacy/prepare-transaction) +(reg-root-key-sub :wallet-legacy/recipient :wallet-legacy/recipient) +(reg-root-key-sub :wallet-legacy/favourites :wallet-legacy/favourites) +(reg-root-key-sub :wallet-legacy/refreshing-history? :wallet-legacy/refreshing-history?) +(reg-root-key-sub :wallet-legacy/fetching-error :wallet-legacy/fetching-error) +(reg-root-key-sub :wallet-legacy/non-archival-node :wallet-legacy/non-archival-node) +(reg-root-key-sub :wallet-legacy/current-base-fee :wallet-legacy/current-base-fee) +(reg-root-key-sub :wallet-legacy/slow-base-fee :wallet-legacy/slow-base-fee) +(reg-root-key-sub :wallet-legacy/normal-base-fee :wallet-legacy/normal-base-fee) +(reg-root-key-sub :wallet-legacy/fast-base-fee :wallet-legacy/fast-base-fee) +(reg-root-key-sub :wallet-legacy/current-priority-fee :wallet-legacy/current-priority-fee) +(reg-root-key-sub :wallet-legacy/transactions-management-enabled? + :wallet-legacy/transactions-management-enabled?) +(reg-root-key-sub :wallet-legacy/all-tokens :wallet-legacy/all-tokens) +(reg-root-key-sub :wallet-legacy/collectible-collections :wallet-legacy/collectible-collections) +(reg-root-key-sub :wallet-legacy/fetching-collection-assets :wallet-legacy/fetching-collection-assets) +(reg-root-key-sub :wallet-legacy/collectible-assets :wallet-legacy/collectible-assets) +(reg-root-key-sub :wallet-legacy/selected-collectible :wallet-legacy/selected-collectible) +(reg-root-key-sub :wallet-legacy/modal-selecting-source-token? + :wallet-legacy/modal-selecting-source-token?) +(reg-root-key-sub :wallet-legacy/swap-from-token :wallet-legacy/swap-from-token) +(reg-root-key-sub :wallet-legacy/swap-to-token :wallet-legacy/swap-to-token) +(reg-root-key-sub :wallet-legacy/swap-from-token-amount :wallet-legacy/swap-from-token-amount) +(reg-root-key-sub :wallet-legacy/swap-to-token-amount :wallet-legacy/swap-to-token-amount) +(reg-root-key-sub :wallet-legacy/swap-advanced-mode? :wallet-legacy/swap-advanced-mode?) + +;;; Link previews +(reg-root-key-sub :link-previews-whitelist :link-previews-whitelist) +(reg-root-key-sub :chat/link-previews :chat/link-previews) +(reg-root-key-sub :chat/status-link-previews :chat/status-link-previews) + +;;commands +(reg-root-key-sub :commands/select-account :commands/select-account) + +;;ethereum +(reg-root-key-sub :ethereum/current-block :ethereum/current-block) + +;;ens +(reg-root-key-sub :ens/registration :ens/registration) +(reg-root-key-sub :ens/registrations :ens/registrations) +(reg-root-key-sub :ens/names :ens/names) + +;;signing +(reg-root-key-sub :signing/sign :signing/sign) +(reg-root-key-sub :signing/tx :signing/tx) +(reg-root-key-sub :signing/edit-fee :signing/edit-fee) + +(reg-root-key-sub :contact-requests/pending :contact-requests/pending) + +(reg-root-key-sub :bug-report/description-error :bug-report/description-error) +(reg-root-key-sub :bug-report/details :bug-report/details) + +(reg-root-key-sub :backup/performing-backup :backup/performing-backup) diff --git a/src/legacy/status_im/test_runner.cljs b/src/legacy/status_im/test_runner.cljs new file mode 100644 index 00000000000..ae16b0a8141 --- /dev/null +++ b/src/legacy/status_im/test_runner.cljs @@ -0,0 +1,136 @@ +(ns legacy.status-im.test-runner + {:dev/always true} + (:require + [cljs.test :as ct] + [clojure.string :as string] + legacy.status-im.subs.root + [shadow.test :as st] + [shadow.test.env :as env] + status-im.setup.i18n-resources + [status-im.setup.interceptors :as interceptors] + [status-im.setup.schema :as schema] + status-im.subs.root + [utils.re-frame :as rf])) + +(defonce repl? (atom false)) + +(defmethod ct/report [::ct/default :end-run-tests] + [m] + (when-not @repl? + (if (ct/successful? m) + (js/process.exit 0) + (js/process.exit 1)))) + +;; get-test-data is a macro so this namespace REQUIRES :dev/always hint ns so that it is always +;; recompiled +(defn ^:dev/after-load reset-test-data! + [] + (-> (env/get-test-data) + (env/reset-test-data!))) + +(defn parse-args + [args] + (reduce + (fn [opts arg] + (cond + (= "--help" arg) + (assoc opts :help true) + + (= "--list" arg) + (assoc opts :list true) + + (= "--repl" arg) + (assoc opts :repl true) + + (string/starts-with? arg "--test=") + (let [test-arg (subs arg 7) + test-syms + (->> (string/split test-arg ",") + (map symbol))] + (update opts :test-syms into test-syms)) + + :else + (do (println (str "Unknown arg: " arg)) + opts))) + {:test-syms []} + args)) + +(defn find-matching-test-vars + [test-syms] + ;; FIXME: should have some kind of wildcard support + (let [test-namespaces + (->> test-syms + (filter simple-symbol?) + (set)) + test-var-syms + (->> test-syms + (filter qualified-symbol?) + (set))] + + (->> (env/get-test-vars) + (filter (fn [the-var] + (let [{name :name the-ns :ns} (meta the-var)] + (or (contains? test-namespaces the-ns) + (contains? test-var-syms (symbol the-ns name))))))))) + +(defn execute-cli + [{:keys [test-syms help repl] :as opts}] + (let [test-env + (-> (ct/empty-env) + ;; can't think of a proper way to let CLI specify custom reporter? :report-fn is mostly + ;; for UI purposes, CLI should be fine with default report + #_(assoc :report-fn + (fn [m] + (tap> [:test m (ct/get-current-env)]) + (prn m))))] + + (cond + help + (do + (println "Usage:") + (println " --list (list known test names)") + (println + " --test=, (run test for namespace or single var, separated by comma)") + (println " --repl (start node without automatically running tests)")) + + (:list opts) + (doseq [[the-ns ns-info] + (->> (env/get-tests) + (sort-by first))] + (println "Namespace:" the-ns) + (doseq [the-var (:vars ns-info) + :let [m (meta the-var)]] + (println (str " " (:ns m) "/" (:name m)))) + (println "---------------------------------")) + + repl + (do + (reset! repl? true) + (js/process.on "SIGINT" #(js/process.exit 0))) + + (seq test-syms) + (let [test-vars (find-matching-test-vars test-syms)] + (st/run-test-vars test-env test-vars)) + + :else + (st/run-all-tests test-env nil)))) + +(defn ^:export main + [& args] + (reset-test-data!) + (interceptors/register-global-interceptors) + (schema/setup!) + (rf/set-mergeable-keys #{:filters/load-filters + :pairing/set-installation-metadata + :dispatch-n + :legacy.status-im.ens.core/verify-names + :shh/send-direct-message + :shh/remove-filter + :transport/confirm-messages-processed + :group-chats/extract-membership-signature + :utils/dispatch-later + :json-rpc/call + :fx}) + + (let [opts (parse-args args)] + (execute-cli opts))) diff --git a/src/legacy/status_im/ui/components/action_sheet.cljs b/src/legacy/status_im/ui/components/action_sheet.cljs new file mode 100644 index 00000000000..9e792750cd4 --- /dev/null +++ b/src/legacy/status_im/ui/components/action_sheet.cljs @@ -0,0 +1,31 @@ +(ns legacy.status-im.ui.components.action-sheet + (:require + ["react-native" :refer (ActionSheetIOS)] + [utils.collection :as utils] + [utils.i18n :as i18n])) + +(defn- callback + [options on-cancel] + (fn [index] + (if (< index (count options)) + (when-let [handler (:action (nth options index))] + (handler)) + (when on-cancel + (on-cancel))))) + +(defn- prepare-options + [title message options] + (let [destructive-opt-index (utils/first-index :destructive? options)] ;; TODO Can only be a + ;; single destructive? + (clj->js (merge {:options (conj (mapv :label options) (i18n/label :t/cancel)) + :cancelButtonIndex (count options)} + (when destructive-opt-index + {:destructiveButtonIndex destructive-opt-index}) + (when title {:title title}) + (when message {:message message}))))) + +(defn show + [{:keys [title message options on-cancel]}] + (.showActionSheetWithOptions ActionSheetIOS + (prepare-options title message options) + (callback options on-cancel))) diff --git a/src/legacy/status_im/ui/components/animation.cljs b/src/legacy/status_im/ui/components/animation.cljs new file mode 100644 index 00000000000..264de9bc071 --- /dev/null +++ b/src/legacy/status_im/ui/components/animation.cljs @@ -0,0 +1,55 @@ +(ns legacy.status-im.ui.components.animation + (:require + ["react-native" :as rn] + [legacy.status-im.ui.components.react :as react])) + +(defn start + ([^js anim] (.start anim)) + ([^js anim callback] (.start anim callback))) + +(defn anim-loop + [animation] + (.loop ^js react/animated animation)) + +(defn interpolate + [^js anim-value config] + (.interpolate anim-value (clj->js config))) + +(defn add-native-driver + [{:keys [useNativeDriver] :as config}] + (assoc config + :useNativeDriver + (if (nil? useNativeDriver) + true + useNativeDriver))) + +(defn timing + [anim-value config] + (.timing ^js react/animated + anim-value + (clj->js (add-native-driver config)))) + +(defn spring + [anim-value config] + (.spring ^js react/animated + anim-value + (clj->js (add-native-driver config)))) + +(defn anim-sequence + [animations] + (.sequence ^js react/animated (clj->js animations))) + +(defn parallel + [animations] + (.parallel ^js react/animated (clj->js animations))) + +(def animated-value (-> ^js rn .-Animated .-Value)) +(def easing (-> ^js rn .-Easing)) + +(defn create-value + [value] + (new animated-value value)) + +(defn easing-in [] (.-in ^js easing)) +(defn easing-out [] (.-out ^js easing)) + diff --git a/src/legacy/status_im/ui/components/badge.cljs b/src/legacy/status_im/ui/components/badge.cljs new file mode 100644 index 00000000000..4ed3a3b306b --- /dev/null +++ b/src/legacy/status_im/ui/components/badge.cljs @@ -0,0 +1,29 @@ +(ns legacy.status-im.ui.components.badge + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react] + [utils.i18n :as i18n])) + +(defn badge + [label & [small?]] + [react/view + (merge + (if small? + {:height 18 :border-radius 9 :min-width 18 :padding-horizontal 6} + {:height 22 :border-radius 11 :min-width 22 :padding-horizontal 8}) + {:background-color colors/blue + :justify-content :center + :align-items :center}) + [react/text + {:style {:typography :caption + :font-weight "500" + :color colors/white-persist}} + label]]) + +(defn message-counter + [value & [small?]] + [badge + (if (> value 99) + (i18n/label :t/counter-99-plus) + value) + small?]) diff --git a/src/legacy/status_im/ui/components/button/view.cljs b/src/legacy/status_im/ui/components/button/view.cljs new file mode 100644 index 00000000000..477277bd265 --- /dev/null +++ b/src/legacy/status_im/ui/components/button/view.cljs @@ -0,0 +1,109 @@ +(ns legacy.status-im.ui.components.button.view + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.spacing :as spacing] + [legacy.status-im.ui.components.text :as text] + [react-native.core :as rn])) + +(defn style-container + [type] + (merge {:height 44 + :align-items :center + :justify-content :center + :flex-direction :row} + (case type + :primary (:base spacing/padding-horizontal) + :secondary (:x-tiny spacing/padding-horizontal) + :icon {} + nil))) + +(defn content-style + [type] + (case type + :primary (:base spacing/padding-horizontal) + :secondary (:x-tiny spacing/padding-horizontal) + :icon (:tiny spacing/padding-horizontal) + nil)) + +(defn themes + [theme] + (case theme + :main {:icon-color (:icon-04 @colors/theme) + :background-color (:interactive-02 @colors/theme) + :text-color (:text-04 @colors/theme)} + :icon {:icon-color (:icon-01 @colors/theme) + :background-color (:interactive-02 @colors/theme) + :text-color (:text-01 @colors/theme)} + :negative {:icon-color (:negative-01 @colors/theme) + :background-color (:negative-02 @colors/theme) + :text-color (:negative-01 @colors/theme)} + :positive {:icon-color (:positive-01 @colors/theme) + :background-color (:positive-02 @colors/theme) + :text-color (:positive-01 @colors/theme)} + :accent {:icon-color (:icon-05 @colors/theme) + :background-color (:interactive-01 @colors/theme) + :text-color (:text-05 @colors/theme)} + :secondary {:icon-color (:icon-02 @colors/theme) + :background-color (:interactive-02 @colors/theme) + :text-color (:text-02 @colors/theme)} + :disabled {:icon-color (:icon-02 @colors/theme) + :background-color (:ui-01 @colors/theme) + :text-color (:text-02 @colors/theme)} + :monocromatic {:icon-color (:icon-01 @colors/theme) + :background-color (:ui-background @colors/theme) + :text-color (:text-01 @colors/theme) + :border-color (:ui-01 @colors/theme)})) + +(defn button + [{:keys [on-press disabled type theme before after + on-long-press accessibility-label loading border-radius style test-ID] + :or {theme :main + type :primary + border-radius 8}} + children] + (let [theme' (cond + disabled :disabled + :else theme) + {:keys [icon-color background-color text-color border-color]} + (themes theme')] + [rn/pressable + (merge {:disabled disabled + :accessibility-label accessibility-label} + (when on-press + {:on-press on-press}) + (when on-long-press + {:on-long-press on-long-press})) + [rn/view + (merge {:background-color background-color + :border-radius border-radius} + (when border-color + {:border-color border-color + :border-width 1})) + [rn/view {:test-ID test-ID :style (merge (style-container type) style)} + (when before + [rn/view + [icons/icon before {:color icon-color}]]) + (when loading + [rn/view {:style {:position :absolute}} + [rn/activity-indicator]]) + [rn/view + {:style (merge (content-style type) + (when loading + {:opacity 0}))} + (cond + (= type :icon) + [icons/icon children {:color icon-color}] + + (string? children) + [text/text + {:weight :medium + :number-of-lines 1 + :style {:color text-color}} + children] + + (vector? children) + children)] + (when after + [rn/view + [icons/icon after {:color icon-color}]])]]])) diff --git a/src/legacy/status_im/ui/components/chat_icon/screen.cljs b/src/legacy/status_im/ui/components/chat_icon/screen.cljs new file mode 100644 index 00000000000..f471670a024 --- /dev/null +++ b/src/legacy/status_im/ui/components/chat_icon/screen.cljs @@ -0,0 +1,100 @@ +(ns legacy.status-im.ui.components.chat-icon.screen + (:require + [clojure.string :as string] + [legacy.status-im.ui.components.chat-icon.styles :as styles] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.screens.chat.photos :as photos] + [legacy.status-im.ui.screens.profile.visibility-status.utils :as visibility-status-utils] + [quo.components.avatars.user-avatar.style :as user-avatar.style] + [quo.context] + [quo.core :as quo] + [re-frame.core :as re-frame.core] + [react-native.core :as rn] + [status-im.contexts.profile.utils :as profile.utils])) + +;;TODO REWORK THIS NAMESPACE + +(def get-name-first-char + (memoize + (fn [name] + ;; TODO: for now we check if the first letter is a # + ;; which means it is most likely a public chat and + ;; use the second letter if that is the case + ;; a broader refactoring should clean up upstream params + ;; for default-chat-icon + (string/capitalize (if (and (= "#" (first name)) + (< 1 (count name))) + (second name) + (first name)))))) + +(defn default-chat-icon + [name styles] + (when-not (string/blank? name) + [rn/view (:default-chat-icon styles) + [rn/text {:style (:default-chat-icon-text styles)} + (get-name-first-char name)]])) + +(defn profile-photo-plus-dot-view + [{:keys [public-key full-name customization-color photo-container photo-path community?]}] + (let [theme @(re-frame.core/subscribe [:theme]) + photo-container (if (nil? photo-container) + styles/container-chat-list + photo-container) + size (:width photo-container) + dot-styles (visibility-status-utils/icon-visibility-status-dot + public-key + size) + dot-accessibility-label (:accessibility-label dot-styles) + text-style (styles/default-chat-icon-text size)] + [rn/view + {:style photo-container + :accessibility-label :profile-photo} + (if (:fn photo-path) + ;; temp support new media server avatar for old component + [photos/photo + {:uri ((:fn photo-path) + {:size size + :full-name full-name + :font-size (get text-style :font-size) + :background-color (user-avatar.style/customization-color customization-color theme) + :indicator-size 0 + :indicator-border 0 + :indicator-color "#000000" + :color (get text-style :color) + :length 2})} + {:size size}] + [photos/photo photo-path {:size size}]) + (when-not community? + [rn/view + {:style dot-styles + :accessibility-label dot-accessibility-label}])])) + +(defn custom-icon-view-list + [name color & [size]] + [rn/view (styles/container-list-size (or size 40)) + [default-chat-icon name + {:default-chat-icon (styles/default-chat-icon-profile color (or size 40)) + :default-chat-icon-text (styles/default-chat-icon-text (or size 40))}]]) + +(defn contact-icon-view + [contact {:keys [container] :as styles}] + [rn/view container + [photos/photo (profile.utils/photo contact) styles]]) + +(defn contact-icon-contacts-tab + [profile] + [rn/view styles/container-chat-list + [quo/user-avatar + {:full-name (profile.utils/displayed-name profile) + :profile-picture (profile.utils/photo profile) + :size :small + :status-indicator? false}]]) + +(defn dapp-icon-permission + [contact size] + [contact-icon-view contact + {:container {:width size :height size} + :size size + :chat-icon (styles/custom-size-icon size) + :default-chat-icon (styles/default-chat-icon-profile colors/default-chat-color size) + :default-chat-icon-text (styles/default-chat-icon-text size)}]) diff --git a/src/legacy/status_im/ui/components/chat_icon/styles.cljs b/src/legacy/status_im/ui/components/chat_icon/styles.cljs new file mode 100644 index 00000000000..7e93cba42e6 --- /dev/null +++ b/src/legacy/status_im/ui/components/chat_icon/styles.cljs @@ -0,0 +1,49 @@ +(ns legacy.status-im.ui.components.chat-icon.styles + (:require + [legacy.status-im.ui.components.colors :as colors])) + +(defn default-chat-icon + [color] + {:margin 0 + :width 40 + :height 40 + :align-items :center + :justify-content :center + :border-radius 20 + :background-color color}) + +(defn default-chat-icon-profile + [color size] + (merge (default-chat-icon color) + {:width size + :height size + :border-radius (/ size 2)})) + +(defn default-chat-icon-text + [size] + {:color colors/white-transparent-70-persist + :font-weight "700" + :font-size (/ size 2) + :line-height size}) + +(def chat-icon + {:margin 4 + :border-radius 20 + :width 40 + :height 40}) + +(defn custom-size-icon + [size] + (merge chat-icon + {:width size + :height size + :margin 0})) + +(def container-chat-list + {:width 40 + :height 40}) + +(defn container-list-size + [size] + {:width size + :height size}) diff --git a/src/legacy/status_im/ui/components/colors.cljs b/src/legacy/status_im/ui/components/colors.cljs new file mode 100644 index 00000000000..374fd638aa6 --- /dev/null +++ b/src/legacy/status_im/ui/components/colors.cljs @@ -0,0 +1,218 @@ +(ns legacy.status-im.ui.components.colors + (:require + [clojure.string :as string] + [reagent.core :as reagent])) + +;; Colors mapping from figma to code, note that theme is more extended and +;; one can follow the comments from the light theme to choose what to use in a component. +(comment + {"Accent blue, #4360DF" [:interactive-01 :text-04 :icon-04] + "Accent blue as background, #ECEFFC" [:interactive-02] + "Dark grey, #939BA1" [:text-02 :icon-02] + "Black" [:text-01 :icon-01] + "Main Green/Success, #4EBC60" [:positive-01] + "Shades 10% green, #EDFBEF" [:positive-02] + "Main Red/Error, #FF2D55" [:negative-01] + "Shades 10% Red, #FFEAEE" [:negative-02] + "Light grey, #EEF2F5" [:ui-01] + "White, #FFFFFF" [:ui-background] + "Devider, 0.1 of black" [:ui-02]}) + +(def light-theme + {:positive-01 "rgba(68,208,88,1)" ; Primary Positive, text, icons color + :positive-02 "rgba(78,188,96,0.1)" ; Secondary Positive, Supporting color for success + ; illustrations + :positive-03 "rgba(78,188,96,1)" ; Lighter Positive, Supporting color for success + ; illustrations + :negative-01 "rgba(255,45,85,1)" ; Primary Negative, text, icons color + :negative-02 "rgba(255,45,85,0.1))" ; Secondary Negative, Supporting color for errors + ; illustrations + :warning-01 "rgba(255, 202, 15, 1)" + :warning-02 "rgba(255, 202, 15, 0.1)" + :interactive-01 "rgba(67,96,223,1)" ; Accent color, buttons, own message, actions,active + ; state + :interactive-02 "rgba(236,239,252,1)" ; Light Accent, buttons background, actions background, + ; messages + :interactive-03 "rgba(255,255,255,0.1)" ; Background for interactive above accent + :interactive-04 "rgba(147,155,161,1)" ; Disabled state + :ui-background "rgba(255,255,255,1)" ; Default view background + :ui-01 "rgba(238,242,245,1)" ; Secondary background + :ui-02 "rgba(0,0,0,0.1)" ; Deviders + :ui-03 "rgba(0,0,0,0.86)" ; Dark background + :text-01 "rgba(0,0,0,1)" ; Main text color + :text-02 "rgba(147,155,161,1)" ; Secondary text + :text-03 "rgba(255,255,255,0.7)" ; Secondary on accent + :text-04 "rgba(67,96,223,1)" ; Links text color + :text-05 "rgba(255,255,255,1)" ; Text inverse on accent + :icon-01 "rgba(0,0,0,1)" ; Primary icons + :icon-02 "rgba(147,155,161,1)" ; Secondary icons + :icon-03 "rgba(255,255,255,0.4)" ; Secondary icons on accent bg + :icon-04 "rgba(67,96,223,1)" ; Interactive icon + :icon-05 "rgba(255,255,255,1)" ; Icons inverse on accent background + :shadow-01 "rgba(0,9,26,0.12)" ; Main shadow color + :backdrop "rgba(0,0,0,0.4)" ; Backdrop for modals and bottom sheet + :border-01 "rgba(238,242,245,1)" + :border-02 "rgba(67, 96, 223, 0.1)" + :highlight "rgba(67,96,223,0.4)" + :blurred-bg "rgba(255,255,255,0.3)"}) + +(def dark-theme + {:positive-01 "rgba(68,208,88,1)" + :positive-02 "rgba(78,188,96,0.1)" + :positive-03 "rgba(78,188,96,1)" + :negative-01 "rgba(252,95,95,1)" + :negative-02 "rgba(252,95,95,0.1)" + :warning-01 "rgba(255, 202, 15, 1)" + :warning-02 "rgba(255, 202, 15, 0.1)" + :interactive-01 "rgba(97,119,229,1)" + :interactive-02 "rgba(35,37,47,1)" + :interactive-03 "rgba(255,255,255,0.1)" + :interactive-04 "rgba(131,140,145,1)" + :ui-background "rgba(20,20,20,1)" + :ui-01 "rgba(37,37,40,1)" + :ui-02 "rgba(0,0,0,0.1)" + :ui-03 "rgba(0,0,0,0.86)" + :text-01 "rgba(255,255,255,1)" + :text-02 "rgba(131,140,145,1)" + :text-03 "rgba(255,255,255,0.7)" + :text-04 "rgba(97,119,229,1)" + :text-05 "rgba(20,20,20,1)" + :icon-01 "rgba(255,255,255,1)" + :icon-02 "rgba(131,140,145,1)" + :icon-03 "rgba(255,255,255,0.4)" + :icon-04 "rgba(97,119,229,1)" + :icon-05 "rgba(20,20,20,1)" + :shadow-01 "rgba(0,0,0,0.75)" + :backdrop "rgba(0,0,0,0.4)" + :border-01 "rgba(37,37,40,1)" + :border-02 "rgba(97,119,229,0.1)" + :highlight "rgba(67,96,223,0.4)" + :blurred-bg "rgba(0,0,0,0.3)"}) + +(def theme (reagent/atom light-theme)) + +(defn get-color + [color] + (get @theme color)) + +;; LEGACY COLORS + +(defn alpha + [value opacity] + (if (string/starts-with? value "#") + (let [hex (string/replace value #"#" "") + r (js/parseInt (subs hex 0 2) 16) + g (js/parseInt (subs hex 2 4) 16) + b (js/parseInt (subs hex 4 6) 16)] + (str "rgba(" r "," g "," b "," opacity ")")) + (let [rgb (string/split value #",")] + (str (string/join "," (butlast rgb)) "," opacity ")")))) + +(def old-colors-mapping-light + {:mentioned-background "#def6fc" + :mentioned-border "#b8ecf9" + :pin-background "#FFEECC"}) + +(def old-colors-mapping-dark + {:mentioned-background "#2a4046" + :mentioned-border "#2a4046" + :pin-background "#34232B"}) + +(def old-colors-mapping-themes {:dark old-colors-mapping-dark :light old-colors-mapping-light}) + +;; WHITE +(def white (:ui-background light-theme)) +(def white-persist (:ui-background light-theme)) ;; this doesn't with theme +(def white-transparent-10 (:interactive-03 light-theme)) ;; Used as icon background color for a dark foreground +(def white-transparent (:icon-03 light-theme)) ;; Used as icon color on dark background and input placeholder color +(def white-transparent-70 (:text-03 light-theme)) +(def white-transparent-70-persist (:text-03 light-theme)) + +(def mentioned-background (:mentioned-background old-colors-mapping-light)) +(def mentioned-border (:mentioned-border old-colors-mapping-light)) + +;; BLACK +(def black (:text-01 light-theme)) ;; Used as the default text color +(def black-persist (:ui-background dark-theme)) ;; this doesn't with theme +(def black-transparent (:ui-02 light-theme)) ;; Used as background color for rounded button on dark background and as + ;; background +;; color for containers like "Backup recovery phrase" +(def black-transparent-20 (:backdrop light-theme)) ; accounts divider +(def black-transparent-40 (:backdrop light-theme)) +(def black-transparent-50 (:backdrop light-theme)) + +;; DARK GREY +(def gray (:text-02 light-theme)) ;; Dark grey, used as a background for a light foreground and + ;; as +;; section header and secondary text color +(def gray-transparent-10 (alpha gray 0.1)) +(def gray-transparent-40 (alpha gray 0.4)) +;; LIGHT GREY +(def gray-lighter (:ui-01 light-theme)) ;; Light Grey, used as a background or shadow + +;; ACCENT BLUE +(def blue (:interactive-01 light-theme)) ;; Accent blue, used as main wallet color, and ios home add + ;; button + +;; LIGHT BLUE +(def blue-light (:interactive-02 light-theme)) ;; Light Blue +(def blue-transparent-10 (alpha blue 0.1)) ;; unknown + +;; RED +(def red (:negative-01 light-theme)) ;; Used to highlight errors or "dangerous" actions +(def red-transparent-10 (alpha red 0.1)) ;;action-row ;; ttt finish + + +;; GREEN +(def green "#44d058") ;; icon for successful inboud transaction +(def green-transparent-10 (alpha green 0.1)) ;; icon for successful inboud transaction + +;; YELLOW +(def pin-background (:pin-background old-colors-mapping-light)) ;; Light yellow, used as background for pinned messages + + +(def text black) +(def text-gray gray) +(def default-chat-color "#a187d5") ;; legacy + +;; THEME + +(def theme-type (reagent/atom :light)) + +(defn dark? + [] + (= :dark @theme-type)) + +(defn set-legacy-theme-type + [type] + (when-not (= type @theme-type) + (let [old-colors-mapping-colors (get old-colors-mapping-themes type)] + (set! white (:ui-background @theme)) + (set! black (:text-01 @theme)) + (set! gray-lighter (:ui-01 @theme)) + (set! blue (:interactive-01 @theme)) + (set! gray (:text-02 @theme)) + (set! blue-light (:interactive-02 @theme)) + (set! red (:negative-01 @theme)) + (set! text black) + (set! mentioned-background (:mentioned-background old-colors-mapping-colors)) + (set! mentioned-border (:mentioned-border old-colors-mapping-colors)) + (set! white-transparent-10 (alpha white 0.1)) + (set! white-transparent (alpha white 0.4)) + (set! white-transparent-70 (alpha white 0.7)) + (set! black-transparent (alpha black 0.1)) + (set! black-transparent-20 (alpha black 0.2)) + (set! black-transparent-40 (alpha black 0.4)) + (set! black-transparent-50 (alpha black 0.5)) + (set! gray-transparent-10 (alpha gray 0.1)) + (set! gray-transparent-40 (alpha gray 0.4)) + (set! green-transparent-10 (alpha green 0.1)) + (set! red-transparent-10 (alpha red 0.1)) + (set! blue-transparent-10 (alpha blue 0.1)) + (set! pin-background (:pin-background old-colors-mapping-colors))) + (reset! theme-type type))) + +;; Colors related to Visibility Status +(def color-online "#7CDA00") +(def color-dnd "#FA6565") +(def color-inactive "#939BA1") diff --git a/src/legacy/status_im/ui/components/common/common.cljs b/src/legacy/status_im/ui/components/common/common.cljs new file mode 100644 index 00000000000..be1be60925d --- /dev/null +++ b/src/legacy/status_im/ui/components/common/common.cljs @@ -0,0 +1,34 @@ +(ns legacy.status-im.ui.components.common.common + (:require + [legacy.status-im.ui.components.common.styles :as styles] + [legacy.status-im.ui.components.react :as react] + [reagent.core :as reagent]) + (:require-macros [legacy.status-im.utils.views :refer [defview letsubs]])) + +(def small-screen-image-k 0.8) +(def small-screen-height 600) + +(defview image-contain + [{:keys [container-style style]} {:keys [image width height]}] + (letsubs [content-width (reagent/atom 0) + {window-width :width window-height :height} [:dimensions/window]] + [react/view + {:style (merge styles/image-contain container-style) + :on-layout #(reset! content-width (-> ^js % .-nativeEvent .-layout .-width))} + [react/image + {:source image + :resize-mode :contain + :style (merge style + (if (> window-height window-width) + {:width (* @content-width + (if (< window-height small-screen-height) + small-screen-image-k + 1)) + :height (/ (* @content-width + height + (if (< window-height small-screen-height) + small-screen-image-k + 1)) + width)} + {:width @content-width + :height (* window-height small-screen-image-k)}))}]])) diff --git a/src/legacy/status_im/ui/components/common/styles.cljs b/src/legacy/status_im/ui/components/common/styles.cljs new file mode 100644 index 00000000000..d6052db7df1 --- /dev/null +++ b/src/legacy/status_im/ui/components/common/styles.cljs @@ -0,0 +1,17 @@ +(ns legacy.status-im.ui.components.common.styles + (:require + [legacy.status-im.ui.components.colors :as colors])) + +(defn logo-container + [size] + {:width size + :height size + :border-radius size + :background-color colors/blue + :align-items :center + :justify-content :center}) + +(def image-contain + {:align-self :stretch + :align-items :center + :justify-content :center}) diff --git a/src/legacy/status_im/ui/components/connectivity/view.cljs b/src/legacy/status_im/ui/components/connectivity/view.cljs new file mode 100644 index 00000000000..2caf1aa30ff --- /dev/null +++ b/src/legacy/status_im/ui/components/connectivity/view.cljs @@ -0,0 +1,69 @@ +(ns legacy.status-im.ui.components.connectivity.view + (:require + [legacy.status-im.ui.components.animation :as animation] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react]) + (:require-macros [legacy.status-im.utils.views :as views])) + +(defn easing + [direction n] + {:toValue n + :easing ((if (= :in direction) + (animation/easing-in) + (animation/easing-out)) + (.-quad ^js animation/easing)) + :duration 400 + :useNativeDriver true}) + +(defn animated-bar-style + [margin-value width color] + {:position :absolute + :width width + :transform [{:translateX + (animation/interpolate + margin-value + {:inputRange [0 1] + :outputRange [0 width]})}] + :height 3 + :background-color color}) + +(views/defview loading-indicator-anim + [parent-width] + (views/letsubs [blue-bar-left-margin (animation/create-value 0) + white-bar-left-margin (animation/create-value 0)] + {:component-did-mount + (fn [_] + (animation/start + (animation/anim-loop + (animation/anim-sequence + [(animation/parallel + [(animation/timing blue-bar-left-margin (easing :in 0.19)) + (animation/timing white-bar-left-margin (easing :in 0.65))]) + (animation/parallel + [(animation/timing blue-bar-left-margin (easing :out 0.85)) + (animation/timing white-bar-left-margin (easing :out 0.85))]) + (animation/parallel + [(animation/timing blue-bar-left-margin (easing :in 0.19)) + (animation/timing white-bar-left-margin (easing :in 0.65))]) + (animation/parallel + [(animation/timing blue-bar-left-margin (easing :out 0)) + (animation/timing white-bar-left-margin (easing :out 0))])]))))} + [react/view + [react/view + {:style {:width parent-width + :position :absolute + :top -3 + :z-index 3 + :height 3 + :background-color colors/white} + :accessibility-label :loading-indicator} + [react/animated-view + {:style (animated-bar-style blue-bar-left-margin + parent-width + colors/blue)}] + [react/animated-view + {:style (assoc (animated-bar-style white-bar-left-margin + parent-width + colors/white) + :left + (* 0.15 parent-width))}]]])) diff --git a/src/legacy/status_im/ui/components/controls/styles.cljs b/src/legacy/status_im/ui/components/controls/styles.cljs new file mode 100644 index 00000000000..22fe1c25687 --- /dev/null +++ b/src/legacy/status_im/ui/components/controls/styles.cljs @@ -0,0 +1,72 @@ +(ns legacy.status-im.ui.components.controls.styles + (:require + [legacy.status-im.ui.components.colors :as colors])) + +(defn switch-style + [value disabled] + {:width 52 + :height 28 + :border-radius 14 + :padding 4 + :background-color (if value + (if disabled + (:interactive-04 @colors/theme) + (:interactive-01 @colors/theme)) + (:ui-01 @colors/theme))}) + +(defn switch-bullet-style + [value hold] + {:width 20 + :height 20 + :border-radius 10 + :opacity (if hold 1 0.6) + :transform [{:translateX (if value 24 0)}] + :background-color colors/white-persist + :elevation 4 + :shadow-opacity 1 + :shadow-radius 16 + :shadow-color (:shadow-01 @colors/theme) + :shadow-offset {:width 0 :height 4}}) + +(defn radio-style + [value disabled] + {:width 20 + :height 20 + :border-radius 10 + :padding 4 + :background-color (if value + (if disabled + (:interactive-04 @colors/theme) + (:interactive-01 @colors/theme)) + (:ui-01 @colors/theme))}) + +(defn radio-bullet-style + [value hold] + {:width 12 + :height 12 + :border-radius 6 + :opacity (if hold 1 0.6) + :transform [{:scale (if value 1 0.0001)}] + :background-color colors/white-persist + :elevation 4 + :shadow-opacity 1 + :shadow-radius 16 + :shadow-color (:shadow-01 @colors/theme) + :shadow-offset {:width 0 :height 4}}) + +(defn checkbox-style + [value disabled] + {:width 18 + :height 18 + :border-radius 4 + :justify-content :center + :align-items :center + :background-color (if value + (if disabled + (:interactive-04 @colors/theme) + (:interactive-01 @colors/theme)) + (:ui-01 @colors/theme))}) + +(defn check-icon-style + [value] + {:opacity (if value 1 0)}) diff --git a/src/legacy/status_im/ui/components/controls/view.cljs b/src/legacy/status_im/ui/components/controls/view.cljs new file mode 100644 index 00000000000..58e1c847e81 --- /dev/null +++ b/src/legacy/status_im/ui/components/controls/view.cljs @@ -0,0 +1,31 @@ +(ns legacy.status-im.ui.components.controls.view + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.controls.styles :as styles] + [legacy.status-im.ui.components.icons.icons :as icons] + [react-native.core :as rn])) + +(defn switch + [{:keys [disabled value]}] + [rn/view + {:style (styles/switch-style value disabled) + :accessibility-label (str "switch-" (if value "on" "off")) + :accessibility-role :switch} + [rn/view {:style (styles/switch-bullet-style value false)}]]) + +(defn radio + [{:keys [disabled value]}] + [rn/view + {:style (styles/radio-style value disabled) + :accessibility-label (str "radio-" (if value "on" "off")) + :accessibility-role :radio} + [rn/view {:style (styles/radio-bullet-style value false)}]]) + +(defn checkbox + [{:keys [value disabled]}] + [rn/view + {:style (styles/checkbox-style value disabled) + :accessibility-label (str "checkbox-" (if value "on" "off")) + :accessibility-role :checkbox} + [rn/view {:style (styles/check-icon-style value)} + [icons/tiny-icon :tiny-icons/tiny-check {:color colors/white}]]]) diff --git a/src/legacy/status_im/ui/components/core.cljs b/src/legacy/status_im/ui/components/core.cljs new file mode 100644 index 00000000000..a8fa3a41c58 --- /dev/null +++ b/src/legacy/status_im/ui/components/core.cljs @@ -0,0 +1,17 @@ +(ns legacy.status-im.ui.components.core + (:require + [legacy.status-im.ui.components.button.view :as button] + [legacy.status-im.ui.components.controls.view :as controls] + [legacy.status-im.ui.components.header :as header] + [legacy.status-im.ui.components.list.header :as list-header] + [legacy.status-im.ui.components.separator :as separator] + [legacy.status-im.ui.components.text :as text] + [legacy.status-im.ui.components.text-input :as text-input])) + +(def text text/text) +(def header header/header) +(def text-input text-input/text-input) +(def button button/button) +(def list-header list-header/header) +(def radio controls/radio) +(def separator separator/separator) diff --git a/src/legacy/status_im/ui/components/dialog.cljs b/src/legacy/status_im/ui/components/dialog.cljs new file mode 100644 index 00000000000..146768583ff --- /dev/null +++ b/src/legacy/status_im/ui/components/dialog.cljs @@ -0,0 +1,25 @@ +(ns legacy.status-im.ui.components.dialog + (:require + ["react-native-dialogs" :default dialogs])) + +(defn show + [{:keys [title options cancel-text on-cancel]}] + (.. dialogs + (showPicker title + nil + (clj->js {:items (mapv #(select-keys % [:label]) + options) + :negativeText cancel-text + :positiveText nil})) + (then (fn [selected] + (let [result (js->clj selected :keywordize-keys true)] + (if (not= (get result :action) "actionSelect") + (when on-cancel + (on-cancel)) + (when-let [label (get-in result + [:selectedItem :label])] + (when-let [action (->> options + (filter #(= label (:label %))) + first + :action)] + (action))))))))) diff --git a/src/legacy/status_im/ui/components/fast_image.cljs b/src/legacy/status_im/ui/components/fast_image.cljs new file mode 100644 index 00000000000..1a08ab90d67 --- /dev/null +++ b/src/legacy/status_im/ui/components/fast_image.cljs @@ -0,0 +1,34 @@ +(ns legacy.status-im.ui.components.fast-image + (:require + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.react :as react] + [reagent.core :as reagent])) + +(defn placeholder + [style child] + [react/view {:style (merge style {:flex 1 :justify-content :center :align-items :center})} + child]) + +(defn fast-image + [_] + (let [loaded? (reagent/atom false) + error? (reagent/atom false)] + (fn [props] + [react/fast-image-class + (merge + props + {:on-error (fn [e] + (when-let [on-error (:on-error props)] + (on-error e)) + (reset! error? true)) + :on-load (fn [e] + (when-let [on-load (:on-load props)] + (on-load e)) + (reset! loaded? true) + (reset! error? false))}) + (when (or @error? (not @loaded?)) + [placeholder (:style props) + (if @error? + [icons/icon :main-icons/cancel] + (when-not @loaded? + [react/activity-indicator {:animating true}]))])]))) diff --git a/src/legacy/status_im/ui/components/header.cljs b/src/legacy/status_im/ui/components/header.cljs new file mode 100644 index 00000000000..35e182ebda2 --- /dev/null +++ b/src/legacy/status_im/ui/components/header.cljs @@ -0,0 +1,206 @@ +(ns legacy.status-im.ui.components.header + (:require + [legacy.status-im.ui.components.button.view :as button] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.spacing :as spacing] + [legacy.status-im.ui.components.text :as text] + [oops.core :refer [oget]] + [react-native.core :as rn] + [react-native.reanimated :as animated] + [reagent.core :as reagent])) + +(def header-height 56) + +(defn header-wrapper-style + [{:keys [height border-bottom background]}] + (merge + (:x-tiny spacing/padding-horizontal) + {:background-color (:ui-background @colors/theme) + :height height} + (when background + {:background-color background}) + (when border-bottom + {:border-bottom-width 1 + :border-bottom-color (:ui-01 @colors/theme)}))) + +(def absolute-fill + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(def content + {:flex 1 + :flex-direction :row + :align-items :center + :justify-content :center}) + +(def left-style + {:position :absolute + :left 0 + :top 0 + :bottom 0 + :justify-content :center + :align-items :flex-start}) + +(def right-style + {:position :absolute + :right 0 + :top 0 + :bottom 0 + :justify-content :center + :align-items :flex-end}) + +(defn title-style + [{:keys [left right]} title-align] + (merge + absolute-fill + (case title-align + :left {:left (:width left) + :right (:width right)} + {:align-items :center + :justify-content :center + :left (max (:width left) (:width right)) + :right (max (:width left) (:width right))}))) + +(def header-actions-style + (merge + {:flex 1 + :flex-direction :row + :align-items :center + :justify-content :center} + (:x-tiny spacing/padding-horizontal))) + +(def header-action-placeholder + {:width (:base spacing/spacing)}) + +(def element + {:align-items :center + :justify-content :center + :flex 1}) + +(defn header-action + [{:keys [icon label on-press disabled accessibility-label]}] + [button/button + (merge {:on-press on-press + :disabled disabled} + (cond + icon {:type :icon + :theme :icon} + label {:type :secondary}) + (when accessibility-label + {:accessibility-label accessibility-label})) + (cond + icon icon + label label)]) + +(defn header-actions + [{:keys [accessories component]}] + [rn/view {:style element} + (cond + (seq accessories) + (into [rn/view {:style header-actions-style}] + (map header-action accessories)) + + component component + + :else + [rn/view {:style header-action-placeholder}])]) + +(defn header-title + [{:keys [title subtitle component title-align]}] + [:<> + (cond + component component + + (and title subtitle) + [:<> + [text/text + {:weight :medium + :number-of-lines 1} + title] + [text/text + {:weight :regular + :color :secondary + :number-of-lines 1} + subtitle]] + + title [text/text + {:weight :bold + :number-of-lines 0 + :align title-align + :size :large} + title])]) + +(defn header + [{:keys [left-width right-width]}] + (let [layout (reagent/atom {:left {:width (or left-width 8) + :height header-height} + :right {:width (or right-width 8) + :height header-height} + :title {:width 0 + :height header-height}}) + handle-layout (fn [el get-layout] + (fn [evt] + (let [width (oget evt "nativeEvent" "layout" "width") + height (oget evt "nativeEvent" "layout" "height")] + (when get-layout + (get-layout el + {:width width + :height height})) + (swap! layout assoc + el + {:width width + :height height}))))] + (fn + [{:keys [left-accessories left-component border-bottom + right-accessories right-component insets get-layout + title subtitle title-component style title-align + background] + :or {title-align :center + border-bottom true}}] + (let [status-bar-height (get insets :top 0) + height (+ header-height status-bar-height)] + [animated/view + {:style (header-wrapper-style {:height height + :background background + :border-bottom border-bottom})} + [rn/view + {:pointer-events :box-none + :height status-bar-height}] + [rn/view + {:style (merge {:height header-height} + style) + :pointer-events :box-none} + [rn/view + {:style absolute-fill + :pointer-events :box-none} + [rn/view + {:style content + :pointer-events :box-none} + [rn/view + {:style left-style + :on-layout (handle-layout :left get-layout) + :pointer-events :box-none} + [header-actions + {:accessories left-accessories + :component left-component}]] + + [rn/view + {:style (title-style @layout title-align) + :on-layout (handle-layout :title get-layout) + :pointer-events :box-none} + [header-title + {:title title + :subtitle subtitle + :title-align title-align + :component title-component}]] + + [rn/view + {:style right-style + :on-layout (handle-layout :right get-layout) + :pointer-events :box-none} + [header-actions + {:accessories right-accessories + :component right-component}]]]]]])))) diff --git a/src/legacy/status_im/ui/components/icons/icons.clj b/src/legacy/status_im/ui/components/icons/icons.clj new file mode 100644 index 00000000000..92b2411c922 --- /dev/null +++ b/src/legacy/status_im/ui/components/icons/icons.clj @@ -0,0 +1,21 @@ +(ns legacy.status-im.ui.components.icons.icons + (:require + [clojure.java.io :as io] + [clojure.string :as string])) + +(def icon-path "./resources/images/icons/") + +(defn require-icon + [el] + (let [s (str "../resources/images/icons/" el ".png") + k (string/replace el "_" "-")] + [k `(js/require ~s)])) + +(defmacro resolve-icons + [] + (let [files (->> (io/file icon-path) + file-seq + (filter #(string/ends-with? % "png")) + (map #(first (string/split (.getName %) #"@"))) + distinct)] + (into {} (map require-icon files)))) diff --git a/src/legacy/status_im/ui/components/icons/icons.cljs b/src/legacy/status_im/ui/components/icons/icons.cljs new file mode 100644 index 00000000000..46f577df75a --- /dev/null +++ b/src/legacy/status_im/ui/components/icons/icons.cljs @@ -0,0 +1,59 @@ +(ns legacy.status-im.ui.components.icons.icons + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react]) + (:refer-clojure :exclude [use]) + (:require-macros [legacy.status-im.ui.components.icons.icons :as icons])) + +(def icons (icons/resolve-icons)) + +(defn icon-source + [icon] + (get icons (name icon))) + +(defn- match-color + [color] + (cond + (keyword? color) + (case color + :dark colors/black + :gray colors/gray + :blue colors/blue + :active colors/blue + :white colors/white + :red colors/red + :none nil + colors/black) + (string? color) + color + :else + colors/black)) + +(defn memo-icon-fn + ([name] (memo-icon-fn name nil)) + ([name + {:keys [color resize-mode container-style + accessibility-label width height no-color] + :or {accessibility-label :icon}}] + ^{:key name} + [react/image + {:style (merge (cond-> {:width (or width 24) + :height (or height 24)} + + resize-mode + (assoc :resize-mode resize-mode) + + (not no-color) + (assoc :tint-color (match-color color))) + container-style) + :accessibility-label accessibility-label + :source (icon-source name)}])) + +(def icon (memoize memo-icon-fn)) + +(defn tiny-icon + ([name] (tiny-icon name {})) + ([name options] + (icon name + (merge {:width 16 :height 16} + options)))) diff --git a/src/legacy/status_im/ui/components/invite/events.cljs b/src/legacy/status_im/ui/components/invite/events.cljs new file mode 100644 index 00000000000..8ed7bdadf17 --- /dev/null +++ b/src/legacy/status_im/ui/components/invite/events.cljs @@ -0,0 +1,18 @@ +(ns legacy.status-im.ui.components.invite.events + (:require + [legacy.status-im.ui.components.react :as react] + [re-frame.core :as re-frame] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(re-frame/reg-fx + ::share + (fn [content] + (.share ^js react/sharing (clj->js content)))) + +(rf/reg-event-fx + :invite.events/share-link + (fn [{:keys [db]}] + (let [{:keys [universal-profile-url]} (get db :profile/profile) + message (i18n/label :t/join-me {:url universal-profile-url})] + {::share {:message message}}))) diff --git a/src/legacy/status_im/ui/components/list/header.cljs b/src/legacy/status_im/ui/components/list/header.cljs new file mode 100644 index 00000000000..b110bc3e34b --- /dev/null +++ b/src/legacy/status_im/ui/components/list/header.cljs @@ -0,0 +1,20 @@ +(ns legacy.status-im.ui.components.list.header + (:require + [legacy.status-im.ui.components.spacing :as spacing] + [legacy.status-im.ui.components.text :as text] + [react-native.core :as rn] + [react-native.utils :as rn.utils])) + +(defn header + [& argv] + (let [[props children] (rn.utils/get-props-and-children argv) + {:keys [color] + :or {color :secondary}} + props] + [rn/view + {:style (merge (:base spacing/padding-horizontal) + (:x-tiny spacing/padding-vertical))} + (into [text/text + {:color color + :style {:margin-top 10}}] + children)])) diff --git a/src/legacy/status_im/ui/components/list/item.cljs b/src/legacy/status_im/ui/components/list/item.cljs new file mode 100644 index 00000000000..4db1ead115d --- /dev/null +++ b/src/legacy/status_im/ui/components/list/item.cljs @@ -0,0 +1,287 @@ +(ns legacy.status-im.ui.components.list.item + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.controls.view :as controls] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.spacing :as spacing] + [legacy.status-im.ui.components.text :as text] + [legacy.status-im.ui.components.tooltip :as tooltip] + [react-native.core :as rn] + [react-native.platform :as platform])) + +(defn themes + [theme] + (case theme + :main {:icon-color (:icon-04 @colors/theme) + :icon-bg-color (:interactive-02 @colors/theme) + :active-background (:interactive-02 @colors/theme) + :passive-background (:ui-background @colors/theme) + :text-color (:text-01 @colors/theme)} + :accent {:icon-color (:icon-04 @colors/theme) + :icon-bg-color (:interactive-02 @colors/theme) + :active-background (:interactive-02 @colors/theme) + :passive-background (:ui-background @colors/theme) + :text-color (:text-04 @colors/theme)} + :negative {:icon-color (:negative-01 @colors/theme) + :icon-bg-color (:negative-02 @colors/theme) + :active-background (:negative-02 @colors/theme) + :passive-background (:ui-background @colors/theme) + :text-color (:negative-01 @colors/theme)} + :positive {:icon-color (:positive-01 @colors/theme) + :icon-bg-color (:positive-02 @colors/theme) + :active-background (:positive-02 @colors/theme) + :passive-background (:ui-background @colors/theme) + :text-color (:positive-01 @colors/theme)} + :disabled {:icon-color (:icon-02 @colors/theme) + :icon-bg-color (:ui-01 @colors/theme) + :active-background (:ui-01 @colors/theme) + :passive-background (:ui-background @colors/theme) + :text-color (:text-02 @colors/theme)})) + +(defn size->icon-size + [size] + (case size + :small 36 + 40)) + +(defn size->container-size + [size] + (case size + :small 52 + 64)) + +(defn size->single-title-size + [size] + (case size + :small :base + :large)) + +(defn container + [{:keys [size container-style]} & children] + (into [rn/view + {:style (merge (:tiny spacing/padding-horizontal) + {:min-height (size->container-size size) + :padding-vertical 8 + :flex-direction :row + :align-items :center + :justify-content :space-between} + container-style)}] + children)) + +(defn icon-column + [{:keys [icon icon-bg-color icon-color size icon-container-style]}] + (when icon + (let [icon-size (size->icon-size size)] + [rn/view {:style (or icon-container-style (:tiny spacing/padding-horizontal))} + (cond + (vector? icon) + icon + (keyword? icon) + [rn/view + {:style {:width icon-size + :height icon-size + :align-items :center + :justify-content :center + :border-radius (/ icon-size 2) + :background-color icon-bg-color}} + [icons/icon icon {:color icon-color}]])]))) + +(defn title-column + [{:keys [title text-color subtitle subtitle-max-lines subtitle-secondary + title-accessibility-label size text-size title-text-weight + right-side-present?]}] + [rn/view + {:style (merge (:tiny spacing/padding-horizontal) + ;; make left-side title grow if nothing is present on right-side + (when-not right-side-present? + {:flex 1 + :justify-content :center}))} + (cond + + (and title subtitle) + [:<> + ;; FIXME(Ferossgp): ReactNative 63 will support view inside text on andrid, remove thess if + ;; when migrating + (if (string? title) + [text/text + {:weight (or title-text-weight :medium) + :style {:color text-color} + :accessibility-label title-accessibility-label + :ellipsize-mode :tail + :number-of-lines 1 + :size text-size} + title] + title) + (if (string? subtitle-secondary) + [rn/view {:flex-direction :row} + [text/text + {:style {:max-width "56.5%"} + :weight :regular + :color :secondary + :ellipsize-mode :tail + :number-of-lines subtitle-max-lines + :size text-size} + subtitle] + [text/text + {:style {:width "7%" :text-align :center} + :weight :regular + :color :secondary + :ellipsize-mode :middle + :number-of-lines subtitle-max-lines + :size text-size} + "•"] + [text/text + {:style {:max-width "36.5%"} + :weight :regular + :color :secondary + :ellipsize-mode :middle + :number-of-lines subtitle-max-lines + :size text-size} + subtitle-secondary]] + (if (string? subtitle) + [text/text + {:weight :regular + :color :secondary + :ellipsize-mode :tail + :number-of-lines subtitle-max-lines + :size text-size} + subtitle] + subtitle))] + + title + (if (string? title) + [text/text + {:weight (or title-text-weight :regular) + :number-of-lines 1 + :style {:color text-color} + :title-accessibility-label title-accessibility-label + :ellipsize-mode :tail + :size (or text-size (size->single-title-size size))} + title] + title))]) + +(defn left-side + [props] + [rn/view + {:style {:flex-direction :row + ;; Occupy only content width, never grow, but shrink if need be + :flex-grow 0 + :flex-shrink 1 + :padding-right 16 + :align-items (or (:left-side-alignment props) :center)}} + [icon-column props] + [title-column props]]) + +(defn right-side + [{:keys [chevron active disabled accessory accessory-text accessory-style]}] + (when (or chevron accessory) + [rn/view + {:style (merge {:align-items :center + :justify-content :flex-end + :flex-direction :row + ;; Grow to occupy full space, shrink when need be, but always maitaining 16px + ;; left gutter + :flex-grow 1 + :flex-shrink 0 + :margin-left 16 + ;; When the left-side leaves no room for right-side, the rendered element is + ;; pushed out. A flex-basis ensures that there is some room reserved. + ;; The number 80px was determined by trial and error. + :flex-basis 80} + accessory-style)} + [rn/view {:style (:tiny spacing/padding-horizontal)} + (case accessory + :radio [controls/radio {:value active :disabled disabled}] + :checkbox [controls/checkbox {:value active :disabled disabled}] + :switch [controls/switch {:value active :disabled disabled}] + :text [text/text + {:color :secondary + :ellipsize-mode :middle + :number-of-lines 1} + accessory-text] + accessory)] + (when (and chevron platform/ios?) + [rn/view {:style {:padding-right (:tiny spacing/spacing)}} + [icons/icon :main-icons/next + {:container-style {:opacity 0.4 + :align-items :center + :justify-content :center} + :resize-mode :center + :color (:icon-02 @colors/theme)}]])])) + +(defn list-item + [{:keys [theme accessory disabled subtitle-max-lines icon icon-container-style + left-side-alignment icon-color icon-bg-color + title subtitle subtitle-secondary active on-press on-long-press chevron size text-size + accessory-text accessibility-label title-accessibility-label accessory-style + error title-text-weight + container-style + active-background-enabled background-color] + :or {subtitle-max-lines 1 + theme :main + active-background-enabled true}}] + (let [theme (if disabled :disabled theme) + {:keys [text-color active-background passive-background]} + (themes theme) + icon-color (or icon-color + (:icon-color (themes theme))) + icon-bg-color (or icon-bg-color + (:icon-bg-color (themes theme))) + component (cond + (and (not on-press) + (not on-long-press)) + rn/view + :else rn/touchable-highlight)] + [rn/view + {:background-color (cond (not= background-color nil) + background-color + (and (= accessory :radio) active) + active-background + :else + passive-background)} + [component + (merge {:type :list-item + :disabled disabled + :bg-color (when active-background-enabled active-background) + :accessibility-label accessibility-label + :underlay-color (:interactive-02 @colors/theme)} + (when on-press + {:on-press on-press}) + (when on-long-press + {:on-long-press on-long-press})) + [container {:size size :container-style container-style} + [left-side + {:icon-color icon-color + :text-color (if on-press + text-color + (:text-color (themes :main))) + :left-side-alignment left-side-alignment + :icon-bg-color icon-bg-color + :title-accessibility-label title-accessibility-label + :icon icon + :icon-container-style icon-container-style + :title title + :title-text-weight title-text-weight + :size size + :text-size text-size + :subtitle subtitle + :subtitle-max-lines subtitle-max-lines + :subtitle-secondary subtitle-secondary + :right-side-present? (or accessory chevron)}] + [right-side + {:chevron chevron + :active active + :disabled disabled + :on-press on-press + :accessory-text accessory-text + :accessory-style accessory-style + :accessory accessory}]]] + (when error + [tooltip/tooltip + (merge {:bottom-value 0} + (when accessibility-label + {:accessibility-label (str (name accessibility-label) "-error")})) + [text/text + {:color :negative + :size :small} + error]])])) diff --git a/src/legacy/status_im/ui/components/list/styles.cljs b/src/legacy/status_im/ui/components/list/styles.cljs new file mode 100644 index 00000000000..edcccd7fe0f --- /dev/null +++ b/src/legacy/status_im/ui/components/list/styles.cljs @@ -0,0 +1,23 @@ +(ns legacy.status-im.ui.components.list.styles + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.utils.styles :as styles])) + +(def base-separator + {:height 1 + :background-color colors/black-transparent}) + +(def separator + (merge + base-separator + {:margin-left 64})) + +(styles/def section-header + {:font-size 14 + :color colors/gray + :margin-left 16 + :margin-top 16 + :android {:margin-bottom 3} + :ios {:margin-bottom 10}}) + +(def section-header-container {}) diff --git a/src/legacy/status_im/ui/components/list/views.cljs b/src/legacy/status_im/ui/components/list/views.cljs new file mode 100644 index 00000000000..15e26db77d6 --- /dev/null +++ b/src/legacy/status_im/ui/components/list/views.cljs @@ -0,0 +1,103 @@ +(ns legacy.status-im.ui.components.list.views + (:require + ["react-native" :as react-native] + [legacy.status-im.ui.components.list.styles :as styles] + [legacy.status-im.ui.components.react :as react] + [react-native.platform :as platform] + [reagent.core :as reagent])) + +(def flat-list-class (reagent/adapt-react-class (.-FlatList react-native))) +(def section-list-class (reagent/adapt-react-class (.-SectionList react-native))) + +(def memo-wrap-render-fn + (memoize + (fn [f render-data] + (fn [^js data] + (reagent/as-element [f (.-item data) (.-index data) (.-separators data) render-data]))))) + +(def memo-separator-fn + (memoize + (fn [separator default-separator?] + (when-let [separator (or separator + (when (and platform/ios? default-separator?) + [react/view styles/separator]))] + (fn [] + (reagent/as-element separator)))))) + +(def memo-as-element + (memoize + (fn [element] + (reagent/as-element element)))) + +(def memo-wrap-key-fn + (memoize + (fn [f] + (fn [data index] + {:post [(some? %)]} + (f data index))))) + +(defn- base-list-props + [{:keys [key-fn render-fn empty-component header footer separator default-separator? render-data]}] + (merge (when key-fn {:keyExtractor (memo-wrap-key-fn key-fn)}) + (when render-fn {:renderItem (memo-wrap-render-fn render-fn render-data)}) + (when separator {:ItemSeparatorComponent (memo-separator-fn separator default-separator?)}) + (when empty-component {:ListEmptyComponent (memo-as-element empty-component)}) + (when header {:ListHeaderComponent (memo-as-element header)}) + (when footer {:ListFooterComponent (memo-as-element footer)}))) + +(defn flat-list + "A wrapper for FlatList. + See https://facebook.github.io/react-native/docs/flatlist.html" + ([props] (flat-list props nil)) + ([{:keys [data] :as props} {:keys [animated?]}] + (let [class (if animated? react/animated-flat-list-class flat-list-class)] + {:pre [(or (nil? data) + (sequential? data))]} + [class + (merge (base-list-props props) + props + {:data (to-array data)})]))) + +(defn- wrap-render-section-header-fn + [f] + (fn [^js data] + (let [^js section (.-section data)] + (reagent/as-element [f + {:title (.-title section) + :data (.-data section)}])))) + +(defn- default-render-section-header + [{:keys [title data]}] + (when (seq data) + [react/view styles/section-header-container + [react/text {:style styles/section-header} + title]])) + +(defn- wrap-per-section-render-fn + [props] + (update + (if-let [f (:render-fn props)] + (assoc (dissoc props :render-fn :render-data) + :renderItem + (memo-wrap-render-fn f (:render-data props))) + props) + :data + to-array)) + +(defn section-list + "A wrapper for SectionList. + To render something on empty sections, use renderSectionFooter and conditionaly + render on empty data + See https://facebook.github.io/react-native/docs/sectionlist.html" + [{:keys [sections render-section-header-fn render-section-footer-fn style] + :as props + :or {render-section-header-fn default-render-section-header + style {}}}] + [section-list-class + (merge (base-list-props props) + props + (when render-section-footer-fn + {:renderSectionFooter (wrap-render-section-header-fn render-section-footer-fn)}) + {:sections (clj->js (map wrap-per-section-render-fn sections)) + :renderSectionHeader (wrap-render-section-header-fn render-section-header-fn) + :style style})]) diff --git a/src/legacy/status_im/ui/components/list_selection.cljs b/src/legacy/status_im/ui/components/list_selection.cljs new file mode 100644 index 00000000000..e0279307fc7 --- /dev/null +++ b/src/legacy/status_im/ui/components/list_selection.cljs @@ -0,0 +1,45 @@ +(ns legacy.status-im.ui.components.list-selection + (:require + [legacy.status-im.ui.components.action-sheet :as action-sheet] + [legacy.status-im.ui.components.dialog :as dialog] + [legacy.status-im.ui.components.react :as react] + [re-frame.core :as re-frame] + [react-native.platform :as platform] + [status-im.config :as config] + [utils.i18n :as i18n] + [utils.url :as url])) + +(defn open-share + [content] + (when (or (:message content) + (:url content)) + (.share ^js react/sharing (clj->js content)))) + +(defn show + [options] + (cond + platform/ios? (action-sheet/show options) + platform/android? (dialog/show options))) + +(defn- platform-web-browser + [] + (if platform/ios? :t/browsing-open-in-ios-web-browser :t/browsing-open-in-android-web-browser)) + +(defn browse + [link] + (show {:title (i18n/label :t/browsing-title) + :options (if config/show-not-implemented-features? + [{:label (i18n/label :t/browsing-open-in-status) + :action #(re-frame/dispatch [:browser.ui/open-url link])} + {:label (i18n/label (platform-web-browser)) + :action #(.openURL ^js react/linking (url/normalize-url link))}] + [{:label (i18n/label (platform-web-browser)) + :action #(.openURL ^js react/linking (url/normalize-url link))}]) + :cancel-text (i18n/label :t/browsing-cancel)})) + +(defn browse-in-web-browser + [link] + (show {:title (i18n/label :t/browsing-title) + :options [{:label (i18n/label (platform-web-browser)) + :action #(.openURL ^js react/linking (url/normalize-url link))}] + :cancel-text (i18n/label :t/browsing-cancel)})) diff --git a/src/legacy/status_im/ui/components/plus_button.cljs b/src/legacy/status_im/ui/components/plus_button.cljs new file mode 100644 index 00000000000..2f82625b52f --- /dev/null +++ b/src/legacy/status_im/ui/components/plus_button.cljs @@ -0,0 +1,46 @@ +(ns legacy.status-im.ui.components.plus-button + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.react :as react])) + +(def action-button-container-old + {:position :absolute + :z-index 2 + :align-items :center + :justify-content :center + :left 0 + :right 0 + :bottom 16 + :height 40}) + +(defn action-button-old + [] + {:width 40 + :height 40 + :background-color colors/blue + :border-radius 20 + :align-items :center + :justify-content :center + :shadow-offset {:width 0 :height 1} + :shadow-radius 6 + :shadow-opacity 1 + :shadow-color (if (colors/dark?) + "rgba(0, 0, 0, 0.75)" + "rgba(0, 12, 63, 0.2)") + :elevation 2}) + +(defn plus-button-old + [{:keys [on-press loading accessibility-label]}] + [react/view action-button-container-old + [quo/button + {:type :scale + :accessibility-label (or accessibility-label :plus-button) + :on-press on-press} + [react/view (action-button-old) + (if loading + [react/activity-indicator + {:color colors/white-persist + :animating true}] + [icons/icon :main-icons/add {:color colors/white-persist}])]]]) diff --git a/deployment/linux/usr/bin/.keep b/src/legacy/status_im/ui/components/radio.cljs similarity index 100% rename from deployment/linux/usr/bin/.keep rename to src/legacy/status_im/ui/components/radio.cljs diff --git a/src/legacy/status_im/ui/components/react.cljs b/src/legacy/status_im/ui/components/react.cljs new file mode 100644 index 00000000000..6027922d672 --- /dev/null +++ b/src/legacy/status_im/ui/components/react.cljs @@ -0,0 +1,200 @@ +(ns legacy.status-im.ui.components.react + (:require + ["@react-native-clipboard/clipboard" :default Clipboard] + ["@react-native-community/blur" :as blur] + ["react" :as reactjs] + ["react-native" :as react-native :refer (Keyboard)] + ["react-native-fast-image" :as FastImage] + ["react-native-navigation" :refer (Navigation)] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.text-style :as typography] + [react-native.platform :as platform] + [reagent.core :as reagent] + [utils.i18n :as i18n])) + + +;; React Components + +(def view (reagent/adapt-react-class (.-View react-native))) + +(def scroll-view-class (reagent/adapt-react-class (.-ScrollView react-native))) +(def keyboard-avoiding-view-class (reagent/adapt-react-class (.-KeyboardAvoidingView react-native))) + +(def text-class (reagent/adapt-react-class (.-Text react-native))) +(def text-input-class (reagent/adapt-react-class (.-TextInput react-native))) + +(def image-class (reagent/adapt-react-class (reactjs/memo (.-Image react-native)))) + +(def fast-image-class (reagent/adapt-react-class FastImage)) + +(def blur-view (reagent/adapt-react-class (.-BlurView blur))) + +(defn valid-source? + [source] + (or (not (map? source)) + (not (contains? source :uri)) + (and (contains? source :uri) + (:uri source)))) + +(defn image + [{:keys [source] :as props}] + (when (valid-source? source) + [image-class props])) + +(def switch-class (reagent/adapt-react-class (.-Switch react-native))) + +(defn switch + [props] + [switch-class props]) + +(def touchable-highlight-class (reagent/adapt-react-class (.-TouchableHighlight react-native))) +(def activity-indicator-class (reagent/adapt-react-class (.-ActivityIndicator react-native))) + +(defn activity-indicator + [props] + [activity-indicator-class (update props :color #(or % colors/gray))]) + +(def animated (.-Animated react-native)) + +(def animated-view-class + (reagent/adapt-react-class (.-View ^js animated))) + +(def animated-flat-list-class + (reagent/adapt-react-class (.-FlatList ^js animated))) + +(defn animated-view + [props & content] + (vec (conj content props animated-view-class))) + +(def dimensions (.-Dimensions react-native)) +(def dismiss-keyboard! #(.dismiss ^js Keyboard)) +(def linking (.-Linking react-native)) + +(def max-font-size-multiplier 1.25) + +(defn prepare-text-props + [props] + (-> props + (update :style typography/get-style) + (assoc :max-font-size-multiplier max-font-size-multiplier))) + +(defn prepare-nested-text-props + [props] + (-> props + (update :style typography/get-nested-style) + (assoc :parseBasicMarkdown true) + (assoc :nested? true))) + +;; Accessor methods for React Components +(defn text + "For nested text elements, use nested-text instead" + ([text-element] + (text {} text-element)) + ([options text-element] + [text-class (prepare-text-props options) text-element])) + +(defn nested-text + "Returns nested text elements with proper styling and typography + Do not use the nested? option, it is for internal usage of the function only" + [options & nested-text-elements] + (let [options-with-style (if (:nested? options) + (prepare-nested-text-props options) + (prepare-text-props options))] + (reduce (fn [acc text-element] + (conj acc + (if (string? text-element) + text-element + (let [[options & nested-text-elements] text-element] + (apply nested-text + (prepare-nested-text-props options) + nested-text-elements))))) + [text-class (dissoc options-with-style :nested?)] + nested-text-elements))) + +;; We track all currently mounted text input refs +;; in a ref-to-defaultValue map +;; so that we can clear them (restore their default values) +;; when global react-navigation's onWillBlur event is invoked +(def text-input-refs (atom {})) + +(defn text-input + [options _] + (let [render-fn (fn [options value] + [text-input-class + (merge + {:underline-color-android :transparent + :max-font-size-multiplier max-font-size-multiplier + :placeholder-text-color colors/text-gray + :placeholder (i18n/label :t/type-a-message) + :value value} + (-> options + (dissoc :preserve-input?) + (update :style typography/get-style) + (update :style dissoc :line-height)))])] + (if (:preserve-input? options) + render-fn + (let [input-ref (atom nil)] + (reagent/create-class + {:component-will-unmount #(when @input-ref + (swap! text-input-refs dissoc @input-ref)) + :reagent-render + (fn [options value] + (render-fn (assoc options + :ref + (fn [r] + ;; Store input and its defaultValue + ;; one we receive a non-nil ref + (when (and r (nil? @input-ref)) + (swap! text-input-refs assoc r (:default-value options))) + (reset! input-ref r) + (when (:ref options) + ((:ref options) r)))) + value))}))))) + +(defn i18n-text + [{style :style k :key}] + [text {:style style} (i18n/label k)]) + +(defn touchable-highlight + [props content] + [touchable-highlight-class + (merge {:underlay-color :transparent} props) + content]) + +(defn get-dimensions + [name] + (js->clj (.get ^js dimensions name) :keywordize-keys true)) + + +;; Clipboard + +(def sharing + (.-Share react-native)) + +(defn copy-to-clipboard + [s] + (.setString ^js Clipboard s)) + + + +;; KeyboardAvoidingView +(def navigation-const (atom nil)) + +(.then (.constants Navigation) + (fn [^js consts] + (reset! navigation-const {:top-bar-height (.-topBarHeight consts) + :bottom-tabs-height (.-bottomTabsHeight consts) + :status-bar-height (.-statusBarHeight consts)}))) + +(defn keyboard-avoiding-view + [props & children] + (into [keyboard-avoiding-view-class + (merge (when platform/ios? {:behavior :padding}) + (if (:ignore-offset props) + props + (update props :keyboard-vertical-offset + 44 (:status-bar-height @navigation-const))))] + children)) + +(defn scroll-view + [props & children] + (vec (conj children props scroll-view-class))) diff --git a/src/legacy/status_im/ui/components/separator.cljs b/src/legacy/status_im/ui/components/separator.cljs new file mode 100644 index 00000000000..21b0fad0b22 --- /dev/null +++ b/src/legacy/status_im/ui/components/separator.cljs @@ -0,0 +1,14 @@ +(ns legacy.status-im.ui.components.separator + (:require + [legacy.status-im.ui.components.colors :as colors] + [react-native.core :as react])) + +(defn separator + [{:keys [color style]}] + [react/view + {:style + (merge + style + {:background-color (colors/get-color (or color :ui-01)) + :align-self :stretch + :height 1})}]) diff --git a/src/legacy/status_im/ui/components/spacing.cljs b/src/legacy/status_im/ui/components/spacing.cljs new file mode 100644 index 00000000000..0276b01838c --- /dev/null +++ b/src/legacy/status_im/ui/components/spacing.cljs @@ -0,0 +1,22 @@ +(ns legacy.status-im.ui.components.spacing) + +(def spacing + {:x-tiny 4 + :tiny 8 + :small 12 + :base 16 + :large 24 + :x-large 32 + :xx-large 48}) + +(def padding-horizontal + (reduce-kv (fn [m k v] + (assoc m k {:padding-horizontal v})) + {} + spacing)) + +(def padding-vertical + (reduce-kv (fn [m k v] + (assoc m k {:padding-vertical v})) + {} + spacing)) diff --git a/deployment/linux/usr/lib/.keep b/src/legacy/status_im/ui/components/status_bar/view.cljs similarity index 100% rename from deployment/linux/usr/lib/.keep rename to src/legacy/status_im/ui/components/status_bar/view.cljs diff --git a/src/legacy/status_im/ui/components/text.cljs b/src/legacy/status_im/ui/components/text.cljs new file mode 100644 index 00000000000..9f215ed5d6a --- /dev/null +++ b/src/legacy/status_im/ui/components/text.cljs @@ -0,0 +1,54 @@ +(ns legacy.status-im.ui.components.text + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.typography :as typography] + [react-native.core :as rn] + [react-native.utils :as rn.utils])) + +(defn text-style + [{:keys [size align weight monospace color style]}] + ;; NOTE(Ferossgo): or in destructoring will keep nil as a value + (merge (if monospace + ;; TODO(Ferossgp): Add all weights for monospace + typography/monospace + (case (or weight :regular) + :regular typography/font-regular + :medium typography/font-medium + :semi-bold typography/font-semi-bold + :bold typography/font-bold + :monospace typography/monospace ; DEPRECATED + :inherit nil)) + (case (or color :main) + :main {:color (:text-01 @colors/theme)} + :secondary {:color (:text-02 @colors/theme)} + :secondary-inverse {:color (:text-03 @colors/theme)} + :link {:color (:text-04 @colors/theme)} + :inverse {:color (:text-05 @colors/theme)} + :positive {:color (:positive-01 @colors/theme)} + :negative {:color (:negative-01 @colors/theme)} + :inherit nil) + (case (or size :base) + :tiny typography/tiny + :x-small typography/x-small + :small typography/small + :base typography/base + :large typography/large + :x-large typography/x-large + :xx-large typography/xx-large + :inherit nil) + {:text-align (or align :auto)} + style)) + +(defn text + [& argv] + (let [[props children] (rn.utils/get-props-and-children argv)] + (into [rn/text + (merge {:style (text-style props)} + (dissoc props + :style + :size + :weight + :color + :align + :animated?))] + children))) diff --git a/src/legacy/status_im/ui/components/text_input.cljs b/src/legacy/status_im/ui/components/text_input.cljs new file mode 100644 index 00000000000..ff67c2d6421 --- /dev/null +++ b/src/legacy/status_im/ui/components/text_input.cljs @@ -0,0 +1,310 @@ +(ns legacy.status-im.ui.components.text-input + (:require + [clojure.spec.alpha :as s] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.spacing :as spacing] + [legacy.status-im.ui.components.text :as text] + [legacy.status-im.ui.components.tooltip :as tooltip] + [legacy.status-im.ui.components.typography :as typography] + [oops.core :refer [ocall]] + [react-native.core :as rn] ;; TODO(Ferossgp): Move icon component to lib + [react-native.platform :as platform] + [reagent.core :as reagent])) + +;; NOTE(Ferossgp): Refactor with hooks when available +;; We track all currently mounted text input refs +;; in a ref-to-defaultValue map +;; so that we can clear them (restore their default values) +;; when global react-navigation's onWillBlur event is invoked +(defonce text-input-refs (atom {})) + +(s/def ::multiline boolean?) +(s/def ::secure-text-entry boolean?) +(s/def ::show-cancel boolean?) +(s/def ::label + (s/nilable (s/or :string string? + :component vector?))) +(s/def ::cancel-label (s/nilable string?)) +(s/def ::keyboard-type + (s/nilable (s/or :string string? + :keyword keyword?))) ; TODO: make set +(s/def ::accessibility-label + (s/nilable (s/or :string string? + :keyword keyword?))) +(s/def ::on-focus fn?) +(s/def ::on-blur fn?) +(s/def ::on-press fn?) + +(s/def ::accessory + (s/keys :opt-un [::on-press + ::icon + ::component])) +(s/def ::after (s/nilable ::accessory)) +(s/def ::before (s/nilable ::accessory)) + +(s/def ::style (s/nilable map?)) +(s/def ::input-style ::style) +(s/def ::container-style ::style) + +(s/def ::text-input + (s/keys :opt-un + [::label + ::multiline + ::error + ::style + ::input-style + ::keyboard-type + ::before + ::after + ::cancel-label + ::on-focus + ::on-blur + ::container-style + ::show-cancel + ::accessibility-label + ::bottom-value + ::secure-text-entry])) + +(defn check-spec + [spec prop] + (if (s/valid? spec prop) + true + (do + (s/explain spec prop) + false))) + +(def height 44) ; 22 line-height + 11*2 vertical padding +(def multiline-height 88) ; 3 * 22 three line-height + 11* vertical padding + +(defn label-style + [] + {:margin-bottom (:tiny spacing/spacing)}) + +(defn text-input-row-style + [] + {:flex-direction :row + :align-items :center}) + +(defn text-input-view-style + [style] + (merge {:border-radius 8 + :flex-direction :row + :flex 1 + :align-items :center + :background-color (:ui-01 @colors/theme)} + style)) + +(defn text-input-style + [multiline input-style monospace before after] + (merge (if monospace + typography/monospace + typography/font-regular) + {:padding-top 11 + :padding-bottom 11 + :font-size 15 + :margin 0 + :text-align-vertical :center + :flex 1 + :color (:text-01 @colors/theme) + :height height} + (when-not before + {:padding-left (:base spacing/spacing)}) + (when-not after + {:padding-right (:base spacing/spacing)}) + (when multiline + {:text-align-vertical :top + :line-height 22 + :height multiline-height}) + input-style)) + +(defn cancel-style + [] + {:margin-left (:tiny spacing/spacing) + :padding-left (:tiny spacing/spacing) + :justify-content :center + :align-self :stretch}) + +(defn accessory-style + [] + (merge (:base spacing/padding-horizontal) + {:flex 1 + :justify-content :center})) + +(defn accessory-element + [{:keys [icon component icon-opts style accessibility-label on-press]}] + (let [el (if on-press + rn/touchable-opacity + rn/view)] + [el + (merge {:style {:align-self :stretch}} + (when on-press + {:on-press on-press})) + [rn/view + (merge {:style (merge (accessory-style) + style)} + (when accessibility-label + {:accessibility-label accessibility-label})) + (cond + icon + [icons/icon icon + (merge {:color (:icon-01 @colors/theme)} + icon-opts)] + component + component + + :else + nil)]])) + +(defn text-input-raw + [] + (let [focused (reagent/atom nil) + visible (reagent/atom false) + ref (atom nil) + blur (fn [] + (some-> @ref + (ocall "blur")))] + (fn + [{:keys [label multiline error style input-style keyboard-type before after + cancel-label on-focus on-blur show-cancel accessibility-label + bottom-value secure-text-entry container-style get-ref on-cancel + monospace auto-complete-type auto-correct] + :or {cancel-label "Cancel"} + :as props}] + {:pre [(check-spec ::text-input props)]} + (let [show-cancel (if (nil? show-cancel) + ;; Enabled by default on iOs and disabled on Android + platform/ios? + show-cancel) + after (cond + (and secure-text-entry @visible) + {:icon :main-icons/hide + :on-press #(reset! visible false)} + + (and secure-text-entry (not @visible)) + {:icon :main-icons/show + :on-press #(reset! visible true)} + + :else after) + secure (boolean (and secure-text-entry (not @visible))) ; must be a boolean to + ; work on iOS + auto-complete (cond + (= keyboard-type :visible-password) + :off + + secure-text-entry + :password + + :else + auto-complete-type) + auto-correct (and (not= keyboard-type :visible-password) + (not secure-text-entry) + auto-correct) + on-cancel (fn [] + (when on-cancel + (on-cancel)) + (blur)) + keyboard-type (cond + (and platform/ios? (= keyboard-type :visible-password)) + :default + + ; the correct approach on Android would be keep secure-text-entry on set + ; keyboard type to visible-password. But until + ; https://github.com/facebook/react-native/issues/27946 + ; is solved that's the second best way. + (and platform/android? secure-text-entry) + :default + + :else + keyboard-type)] + [rn/view {:style container-style} + (when label + [text/text {:style (label-style)} + label]) + [rn/view {:style (text-input-row-style)} + [rn/view + {:style (text-input-view-style style) + :important-for-accessibility (if secure-text-entry + :no-hide-descendants + :auto)} + (when before + [accessory-element before]) + [rn/text-input + (merge + {:style (text-input-style multiline input-style monospace before after) + :ref (fn [r] + (reset! ref r) + (when get-ref (get-ref r))) + :placeholder-text-color (:text-02 @colors/theme) + :underline-color-android :transparent + :auto-capitalize :none + :secure-text-entry secure + :auto-correct auto-correct + :auto-complete-type auto-complete + :on-focus (fn [evt] + (when on-focus (on-focus evt)) + (when show-cancel + (rn/configure-next (:ease-in-ease-out + rn/layout-animation-presets))) + (reset! focused true)) + :on-blur (fn [evt] + (when on-blur (on-blur evt)) + (when show-cancel + (rn/configure-next (:ease-in-ease-out + rn/layout-animation-presets))) + (reset! focused false)) + :keyboard-type keyboard-type} + (when (and platform/ios? (not after)) + {:clear-button-mode :while-editing}) + (dissoc props + :style + :keyboard-type + :on-focus + :on-blur + :secure-text-entry + :ref + :get-ref + :auto-correct + :auto-complete-type))] + (when after + [accessory-element after])] + (when (and show-cancel + (not multiline) + @focused) + [rn/touchable-opacity + {:style (cancel-style) + :on-press on-cancel} + [text/text {:color :link} cancel-label]]) + (when error + [tooltip/tooltip + (merge {:bottom-value (if bottom-value bottom-value 0)} + (when accessibility-label + {:accessibility-label (str (name accessibility-label) "-error")})) + [text/text + {:color :negative + :align :center + :size :small} + error]])]])))) + +;; TODO(Ferossgp): Refactor me when hooks available +(defn text-input + [{:keys [preserve-input?] + :as props}] + (if preserve-input? + [text-input-raw props] + (let [id (random-uuid)] + (reagent/create-class + {:component-will-unmount + (fn [] + (swap! text-input-refs dissoc id)) + :reagent-render + (fn [{:keys [get-ref default-value] + :as props}] + [text-input-raw + (merge props + {:get-ref (fn [r] + ;; Store input and its defaultValue + ;; one we receive a non-nil ref + (when r + (swap! text-input-refs assoc id {:ref r :value default-value})) + (when get-ref (get-ref r)))})])})))) diff --git a/src/legacy/status_im/ui/components/text_style.cljs b/src/legacy/status_im/ui/components/text_style.cljs new file mode 100644 index 00000000000..e97f97c36c4 --- /dev/null +++ b/src/legacy/status_im/ui/components/text_style.cljs @@ -0,0 +1,72 @@ +(ns legacy.status-im.ui.components.text-style + (:require + [legacy.status-im.ui.components.colors :as colors])) + +(def default-font-family "Inter") +(defn default-style + [] + {:color colors/black + :font-weight "400" + :font-size 15}) + +(def typography-styles + {:header {:font-weight "700" + :font-size 22} + + :title-bold {:font-weight "700" + :font-size 17} + + :title {:font-size 17} + + :main-semibold {:font-weight "600"} + + :main-medium {:font-weight "500"} + + :caption {:font-size 12} + + :timestamp {:font-size 10 + :text-transform :uppercase}}) + +(defn get-style + [{:keys [typography] :as style}] + {:pre [(or (nil? typography) (contains? typography-styles typography))]} + (let [{:keys [font-weight font-style] + :as style} + (merge (default-style) + (get typography-styles + typography) + (dissoc style :typography :nested?))] + (-> style + (assoc :font-family + (str default-font-family + "-" + (case font-weight + "400" (when-not (= font-style :italic) + "Regular") + "500" "Medium" + "600" "SemiBold" + "700" "Bold") + (when (= font-style :italic) + "Italic"))) + (dissoc :font-weight :font-style)))) + +(defn get-nested-style + [{:keys [typography] :as style}] + {:pre [(or (nil? typography) (contains? typography-styles typography))]} + (let [{:keys [font-weight font-style] :as style} + (merge (get typography-styles + typography) + (dissoc style :typography))] + (cond-> (dissoc style :font-weight :font-style) + (or font-weight font-style) + (assoc :font-family + (str default-font-family + "-" + (case font-weight + "500" "Medium" + "600" "SemiBold" + "700" "Bold" + (when-not (= font-style :italic) + "Regular")) + (when (= font-style :italic) + "Italic")))))) diff --git a/src/legacy/status_im/ui/components/toolbar.cljs b/src/legacy/status_im/ui/components/toolbar.cljs new file mode 100644 index 00000000000..fd54c5b2a83 --- /dev/null +++ b/src/legacy/status_im/ui/components/toolbar.cljs @@ -0,0 +1,39 @@ +(ns legacy.status-im.ui.components.toolbar + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react])) + +(defn toolbar-container + [{:keys [show-border? size center?] + :or {size :default}}] + (merge {:align-items :center + :padding-horizontal 8 + :width "100%" + :flex-direction :row + :justify-content :space-between} + (when center? + {:justify-content :center}) + (when show-border? + {:border-top-width 1 + :border-top-color colors/gray-lighter}) + (case size + :large {:height 60} + {:height 52}))) + +;; TODO(Ferossgp): Allow components when moving to Quo +(defn toolbar + [{:keys [center left right show-border? size]}] + (if center + [react/view + {:style (toolbar-container {:show-border? show-border? + :center? true + :size size})} + center] + [react/view + {:style (toolbar-container {:show-border? show-border? + :center? false + :size size})} + [react/view {:flex-shrink 1} + (when left left)] + [react/view + (when right right)]])) diff --git a/src/legacy/status_im/ui/components/tooltip.cljs b/src/legacy/status_im/ui/components/tooltip.cljs new file mode 100644 index 00000000000..3e14098766c --- /dev/null +++ b/src/legacy/status_im/ui/components/tooltip.cljs @@ -0,0 +1,72 @@ +(ns legacy.status-im.ui.components.tooltip + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.spacing :as spacing] + [oops.core :refer [oget]] + [react-native.core :as rn] + [react-native.platform :as platform] + [reagent.core :as reagent])) + +(def ^:private initial-height 22) + +(defn tooltip-style + [{:keys [bottom-value]}] + (merge + (:base spacing/padding-horizontal) + {:position :absolute + :align-items :center + :left 0 + :right 0 + :top (- bottom-value) + :opacity 1 + :transform [{:translateY 10}]})) + +(defn container-style + [] + {:z-index 2 + :align-items :center + :shadow-radius 16 + :shadow-opacity 1 + :shadow-color (:shadow-01 @colors/theme) + :shadow-offset {:width 0 :height 4}}) + +(defn content-style + [] + (merge (:base spacing/padding-horizontal) + {:padding-vertical 6 + :elevation 2 + :background-color (:ui-background @colors/theme) + :border-radius 8})) + +(defn tooltip + [] + (let [layout (reagent/atom {:height initial-height}) + on-layout (fn [evt] + (let [width (oget evt "nativeEvent" "layout" "width") + height (oget evt "nativeEvent" "layout" "height")] + (reset! layout {:width width + :height height})))] + (fn [{:keys [bottom-value accessibility-label]} & children] + [:<> + [rn/view + {:style (tooltip-style {:bottom-value (- (get @layout :height) + bottom-value)}) + :pointer-events :box-none} + [rn/view + {:style (container-style) + :pointer-events :box-none} + (into [rn/view + {:style (content-style) + :pointer-events :box-none + :accessibility-label accessibility-label + :on-layout on-layout}] + children) + + (when platform/ios? + ;; NOTE(Ferossgp): Android does not show elevation for tooltip making it lost on white bg + [icons/icon :icons/tooltip-tip + {:width 18 + :height 8 + :container-style {:elevation 3} + :color (:ui-background @colors/theme)}])]]]))) diff --git a/src/legacy/status_im/ui/components/tooltip/animations.cljs b/src/legacy/status_im/ui/components/tooltip/animations.cljs new file mode 100644 index 00000000000..5dfc6448d9a --- /dev/null +++ b/src/legacy/status_im/ui/components/tooltip/animations.cljs @@ -0,0 +1,18 @@ +(ns legacy.status-im.ui.components.tooltip.animations + (:require + [legacy.status-im.ui.components.animation :as animation])) + +(defn animate-tooltip + [bottom-value bottom-anim-value opacity-value delta] + (fn [] + (animation/start + (animation/parallel + [(animation/timing opacity-value + {:toValue 1 + :duration 500 + :useNativeDriver true}) + (animation/timing bottom-anim-value + {:toValue (- bottom-value delta) + :easing (.bezier ^js animation/easing 0.685 0.000 0.025 1.185) + :duration 500 + :useNativeDriver true})])))) diff --git a/src/legacy/status_im/ui/components/tooltip/styles.cljs b/src/legacy/status_im/ui/components/tooltip/styles.cljs new file mode 100644 index 00000000000..cf5c783c70f --- /dev/null +++ b/src/legacy/status_im/ui/components/tooltip/styles.cljs @@ -0,0 +1,48 @@ +(ns legacy.status-im.ui.components.tooltip.styles + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.utils.styles :as styles] + [status-im.config :as config])) + +(styles/def bottom-tooltip-container + {:position :absolute + :align-items :center + :left 12 + :right 12 + :ios {:top 0} + :android {:top 30}}) + +(defn tooltip-animated + [bottom-value opacity-value] + (cond-> {:position :absolute + :align-items :center + :left 0 + :right 0 + :bottom 0 + :transform [{:translateY bottom-value}] + :opacity opacity-value} + + ;;we need this for e2e tests + config/tooltip-events? + (assoc :margin-top -20 + :position :relative))) + +(def bottom-tooltip-text-container + {:flex-direction :row + :align-items :center + :margin-horizontal 12 + :padding-horizontal 16 + :padding-vertical 9 + :background-color colors/gray + :border-radius 8}) + +(def bottom-tooltip-text + {:color colors/white}) + +(def tooltip-triangle + {:width 16 + :height 8}) + +(def close-icon + {:margin-right 4 + :margin-left 10}) diff --git a/src/legacy/status_im/ui/components/tooltip/views.cljs b/src/legacy/status_im/ui/components/tooltip/views.cljs new file mode 100644 index 00000000000..9f3dea8f299 --- /dev/null +++ b/src/legacy/status_im/ui/components/tooltip/views.cljs @@ -0,0 +1,28 @@ +(ns legacy.status-im.ui.components.tooltip.views + (:require + [legacy.status-im.ui.components.animation :as animation] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.components.tooltip.animations :as animations] + [legacy.status-im.ui.components.tooltip.styles :as styles]) + (:require-macros [legacy.status-im.utils.views :as views])) + +(views/defview bottom-tooltip-info + [label on-close] + (views/letsubs [bottom-anim-value (animation/create-value 75) + opacity-value (animation/create-value 0)] + {:component-did-mount (animations/animate-tooltip 75 bottom-anim-value opacity-value 10)} + [react/view styles/bottom-tooltip-container + [react/animated-view {:style (styles/tooltip-animated bottom-anim-value opacity-value)} + [icons/icon :icons/tooltip-tip + (assoc + styles/tooltip-triangle + :color colors/gray + :container-style {:transform [{:rotate "180deg"}]})] + [react/view styles/bottom-tooltip-text-container + [react/text {:style styles/bottom-tooltip-text} label] + [react/touchable-highlight + {:on-press on-close + :style styles/close-icon} + [icons/icon :main-icons/close {:color colors/white}]]]]])) diff --git a/src/legacy/status_im/ui/components/topbar.cljs b/src/legacy/status_im/ui/components/topbar.cljs new file mode 100644 index 00000000000..b5f34e130d2 --- /dev/null +++ b/src/legacy/status_im/ui/components/topbar.cljs @@ -0,0 +1,53 @@ +(ns legacy.status-im.ui.components.topbar + (:require + [legacy.status-im.ui.components.core :as quo] + [quo.context] + [quo.foundations.colors :as quo.colors] + [re-frame.core :as re-frame] + [react-native.safe-area :as safe-area])) + +(def default-button-width 48) + +(defn default-navigation + [modal? {:keys [on-press label icon]}] + (cond-> {:icon (if modal? :i/close :i/arrow-left) + :accessibility-label :back-button + :on-press #(re-frame/dispatch [:navigate-back :bottom-sheet/hide-old])} + on-press + (assoc :on-press on-press) + + icon + (assoc :icon icon) + + label + (dissoc :icon) + + label + (assoc :label label))) + +(defn topbar + [{:keys [navigation use-insets right-accessories modal? content border-bottom? new-ui?] ;; remove + ;; new-ui? + ;; key, temp + ;; fix + :or {border-bottom? true + new-ui? false} + :as props}] + (let [theme (quo.context/use-theme) + navigation (if (= navigation :none) + nil + [(default-navigation modal? navigation)])] + [quo/header + (merge {:left-accessories navigation + :title-component content + :insets (when use-insets safe-area/insets) + :left-width (when navigation + default-button-width) + :border-bottom border-bottom?} + props + (when (seq right-accessories) + {:right-accessories right-accessories}) + (when new-ui? + {:background (quo.colors/theme-colors quo.colors/neutral-5 + quo.colors/neutral-95 + theme)}))])) diff --git a/src/legacy/status_im/ui/components/typography.cljs b/src/legacy/status_im/ui/components/typography.cljs new file mode 100644 index 00000000000..0de0a8a5b29 --- /dev/null +++ b/src/legacy/status_im/ui/components/typography.cljs @@ -0,0 +1,39 @@ +(ns legacy.status-im.ui.components.typography) + +(def tiny + {:font-size 10 + :line-height 14}) + +(def x-small + {:font-size 12 + :line-height 16}) + +(def small + {:font-size 13 + :line-height 18}) + +(def base + {:font-size 15 + :line-height 22}) + +(def large + {:font-size 17 + :line-height 24}) + +(def x-large + {:font-size 22 + :line-height 30}) + +(def xx-large + {:font-size 28 + :line-height 38}) + +(def font-regular {:font-family "Inter-Regular"}) ; 400 + +(def font-medium {:font-family "Inter-Medium"}) ; 500 ff + +(def font-semi-bold {:font-family "Inter-SemiBold"}) ; 600 + +(def font-bold {:font-family "Inter-Bold"}) ; 700 + +(def monospace {:font-family "InterStatus-Regular"}) diff --git a/src/legacy/status_im/ui/components/unviewed_indicator.cljs b/src/legacy/status_im/ui/components/unviewed_indicator.cljs new file mode 100644 index 00000000000..664c0e801e5 --- /dev/null +++ b/src/legacy/status_im/ui/components/unviewed_indicator.cljs @@ -0,0 +1,13 @@ +(ns legacy.status-im.ui.components.unviewed-indicator + (:require + [legacy.status-im.ui.components.badge :as badge] + [legacy.status-im.ui.components.react :as react])) + +(defn unviewed-indicator + [c] + (when (pos? c) + [react/view + {:padding-left 16 + :justify-content :flex-end + :align-items :flex-end} + [badge/message-counter c]])) diff --git a/src/legacy/status_im/ui/components/webview.cljs b/src/legacy/status_im/ui/components/webview.cljs new file mode 100644 index 00000000000..c482b70f3af --- /dev/null +++ b/src/legacy/status_im/ui/components/webview.cljs @@ -0,0 +1,28 @@ +(ns legacy.status-im.ui.components.webview + (:require + ["react-native-webview" :default rn-webview] + [react-native.platform :as platform] + [reagent.core :as reagent] + [status-im.config :as config])) + +(def webview-class + (reagent/adapt-react-class rn-webview)) + +(defn webview + [{:keys [dapp?] :as opts}] + (if (and config/cached-webviews-enabled? platform/android? dapp?) + (reagent/create-class + (let [dapp-name-sent? (reagent/atom false)] + {:component-did-mount + (fn [] + ;; unfortunately it's impossible to pass some initial params + ;; to view, that's why we have to pass dapp-name to the module + ;; before showing webview + #_(.setCurrentDapp (module) + dapp-name + (fn [] (reset! dapp-name-sent? true)))) + :reagent-render + (fn [opts] + (when @dapp-name-sent? + [webview-class opts]))})) + [webview-class opts])) diff --git a/src/legacy/status_im/ui/screens/appearance/views.cljs b/src/legacy/status_im/ui/screens/appearance/views.cljs new file mode 100644 index 00000000000..02173cf7e3d --- /dev/null +++ b/src/legacy/status_im/ui/screens/appearance/views.cljs @@ -0,0 +1,43 @@ +(ns legacy.status-im.ui.screens.appearance.views + (:require-macros [legacy.status-im.utils.views :as views]) + (:require + [legacy.status-im.react-native.resources :as resources] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.core :as components] + [legacy.status-im.ui.components.react :as react] + [quo.core :as quo] + [re-frame.core :as re-frame] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn button + [label icon theme selected?] + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:profile.settings/change-appearance theme])} + [react/view + (merge {:align-items :center :padding 8 :border-radius 20} + (when selected? + {:background-color colors/blue-light})) + [react/image {:source (get resources/ui icon)}] + [react/text {:style {:margin-top 8}} + (i18n/label label)]]]) + +(views/defview appearance-view + [] + (views/letsubs [{:keys [appearance]} [:profile/profile]] + [:<> + [quo/page-nav + {:type :title + :title (i18n/label :t/appearance) + :background :blur + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + [components/list-header (i18n/label :t/preference)] + [react/view + {:flex-direction :row + :padding-horizontal 8 + :justify-content :space-between + :margin-vertical 16} + [button :t/light :theme-light 1 (= 1 appearance)] + [button :t/dark :theme-dark 2 (= 2 appearance)] + [button :t/system :theme-system 0 (= 0 appearance)]]])) diff --git a/src/legacy/status_im/ui/screens/browser/accounts.cljs b/src/legacy/status_im/ui/screens/browser/accounts.cljs new file mode 100644 index 00000000000..483f53bd790 --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/accounts.cljs @@ -0,0 +1,32 @@ +(ns legacy.status-im.ui.screens.browser.accounts + (:require + [legacy.status-im.ui.components.chat-icon.screen :as chat-icon] + [legacy.status-im.ui.components.list.item :as list.item] + [legacy.status-im.ui.components.list.views :as list] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.utils.utils :as utils] + [re-frame.core :as re-frame] + [utils.i18n :as i18n])) + +(defn render-account + [account _ _ dapps-account] + [list.item/list-item + (merge {:accessory :radio + :active (= (:address dapps-account) (:address account)) + :icon [chat-icon/custom-icon-view-list (:name account) (:color account)] + :title (:name account) + :subtitle (utils/get-shortened-checksum-address (:address account))} + (when (not= (:address dapps-account) (:address account)) + {:on-press #(re-frame/dispatch [:dapps-account-selected (:address account)])}))]) + +(defn accounts-list + [accounts dapps-account] + (fn [] + [react/view {:flex 1} + [react/text {:style {:margin 16 :text-align :center}} + (i18n/label :t/select-account-dapp)] + [list/flat-list + {:data accounts + :key-fn :address + :render-data dapps-account + :render-fn render-account}]])) diff --git a/src/legacy/status_im/ui/screens/browser/empty_tab/styles.cljs b/src/legacy/status_im/ui/screens/browser/empty_tab/styles.cljs new file mode 100644 index 00000000000..b5399cfbcba --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/empty_tab/styles.cljs @@ -0,0 +1,28 @@ +(ns legacy.status-im.ui.screens.browser.empty-tab.styles + (:require + [legacy.status-im.ui.components.colors :as colors])) + +(def input + {:height 36 + :padding 0}) + +(def input-container-style + {:margin-horizontal 16 + :margin-vertical 10}) + +(defn dapp-store-container + [] + {:margin 16 + :border-color colors/gray-lighter + :margin-top 18 + :border-width 1 + :border-radius 12 + :padding-vertical 16 + :padding-horizontal 44 + :align-items :center}) + +(def open-dapp-store + {:margin-top 12 + :font-size 15 + :font-weight "500" + :line-height 22}) diff --git a/src/legacy/status_im/ui/screens/browser/empty_tab/views.cljs b/src/legacy/status_im/ui/screens/browser/empty_tab/views.cljs new file mode 100644 index 00000000000..059fd9ed68f --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/empty_tab/views.cljs @@ -0,0 +1,128 @@ +(ns legacy.status-im.ui.screens.browser.empty-tab.views + (:require + [legacy.status-im.react-native.resources :as resources] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.common.common :as components.common] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.list.item :as list.item] + [legacy.status-im.ui.components.list.views :as list] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.screens.browser.empty-tab.styles :as styles] + [legacy.status-im.ui.screens.browser.views :as browser] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [utils.i18n :as i18n] + [utils.url :as url]) + (:require-macros [legacy.status-im.utils.views :as views])) + +(defn hide-sheet-and-dispatch + [event] + (re-frame/dispatch [:bottom-sheet/hide-old]) + (js/setTimeout #(re-frame/dispatch event) 200)) + +(defn list-item + [_] + (let [loaded (reagent/atom nil)] + (fn [{:keys [name url] :as bookmark}] + [list.item/list-item + {:accessibility-label (keyword (str "fav-item" name)) + :on-press #(re-frame/dispatch [:browser.ui/open-url url]) + :on-long-press (fn [] + (re-frame/dispatch + [:show-bottom-sheet + {:content (fn [] + [react/view {:flex 1} + [list.item/list-item + {:theme :accent + :title (i18n/label :t/open-in-new-tab) + :accessibility-label :open-in-new-tab + :icon :main-icons/tabs + :on-press #(hide-sheet-and-dispatch + [:browser.ui/open-url url])}] + [list.item/list-item + {:theme :accent + :title (i18n/label :t/edit) + :accessibility-label :edit-bookmark + :icon :main-icons/edit + :on-press #(hide-sheet-and-dispatch + [:open-modal :new-bookmark + bookmark])}] + [list.item/list-item + {:theme :negative + :title (i18n/label :t/delete) + :accessibility-label :delete-bookmark + :icon :main-icons/delete + :on-press #(hide-sheet-and-dispatch + [:browser/delete-bookmark + url])}]])}])) + :title name + :subtitle (or url (i18n/label :t/dapp)) + :icon [react/view + {:width 40 + :height 40 + :align-items :center + :justify-content :center} + (when (or (nil? @loaded) @loaded) + [react/image + {:onLoad #(reset! loaded true) + :style {:width 32 :height 32 :position :absolute :top 4 :left 4} + :source {:uri (str "https://" (url/url-host url) "/favicon.ico")}}]) + (when-not @loaded + [react/view + {:width 40 + :height 40 + :border-radius 20 + :background-color colors/gray-lighter + :align-items :center + :justify-content :center} + [icons/icon :main-icons/browser {:color colors/gray}]])]}]))) + +(def dapp-image-data {:image (resources/get-image :dapp-store) :width 768 :height 333}) +(defn dapp-image [] [components.common/image-contain nil dapp-image-data]) + +(defn list-header + [empty-bookmarks?] + [react/view + [react/touchable-highlight {:on-press #(re-frame/dispatch [:browser.ui/open-url "https://dap.ps"])} + [react/view (styles/dapp-store-container) + [dapp-image nil dapp-image-data] + [react/text {:style styles/open-dapp-store} (i18n/label :t/open-dapp-store)] + [react/text {:style {:color colors/blue :font-size 13 :line-height 22}} "https://dap.ps ->"]]] + (when-not empty-bookmarks? + [react/view {:margin-top 14 :margin-left 16 :margin-bottom 4} + [react/text {:style {:line-height 22 :font-size 15 :color colors/gray}} + (i18n/label :t/favourites)]])]) + +(views/defview empty-tab + [] + (views/letsubs [bookmarks [:bookmarks/active] + dapps-account [:dapps-account] + url-text (atom nil)] + (let [bookmarks (vals bookmarks)] + [react/keyboard-avoiding-view + {:style {:flex 1} + :ignore-offset true} + [quo/text-input + {:on-change-text #(reset! url-text %) + :on-submit-editing #(re-frame/dispatch [:browser.ui/open-url @url-text]) + :placeholder (i18n/label :t/enter-url) + :auto-capitalize :none + :auto-correct false + :style styles/input + :container-style styles/input-container-style + :accessibility-label :dapp-url-input + :return-key-type :go}] + [react/view {:style {:height 1 :background-color (colors/alpha colors/black 0.1)}}] + [list/flat-list + {:header [list-header (empty? bookmarks)] + :data bookmarks + :key-fn :browser-id + :empty-component [react/view {:align-items :center :margin-top 20} + [icons/icon :main-icons/favourite {:color colors/gray}] + [react/text {:style {:color colors/gray :margin-top 4}} + (i18n/label :t/favourite-description)]] + :render-fn list-item}] + [browser/navigation + {:dapps-account dapps-account + :empty-tab true}]]))) diff --git a/src/legacy/status_im/ui/screens/browser/options/views.cljs b/src/legacy/status_im/ui/screens/browser/options/views.cljs new file mode 100644 index 00000000000..ef100da677e --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/options/views.cljs @@ -0,0 +1,61 @@ +(ns legacy.status-im.ui.screens.browser.options.views + (:require + [legacy.status-im.browser.core :as browser] + [legacy.status-im.qr-scanner.core :as qr-scanner] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.list.item :as list.item] + [legacy.status-im.ui.components.react :as react] + [re-frame.core :as re-frame] + [utils.i18n :as i18n])) + +(defn hide-sheet-and-dispatch + [event] + (re-frame/dispatch [:bottom-sheet/hide-old]) + (re-frame/dispatch event)) + +(defn browser-options + [url _account empty-tab name] + (fn [] + (let [bookmarks @(re-frame/subscribe [:bookmarks/active]) + fav? (get bookmarks url)] + [react/view {:flex 1} + [quo/button + {:style {:align-self :flex-end + :margin-right 15} + :type :icon + :theme :icon + :accessibility-label :universal-qr-scanner + :on-press #(hide-sheet-and-dispatch + [::qr-scanner/scan-code + {:handler ::qr-scanner/on-scan-success}])} + :main-icons/qr] + (when-not empty-tab + [:<> + [list.item/list-item + {:theme :accent + :title (i18n/label :t/new-tab) + :accessibility-label :new-tab + :icon :main-icons/add + :on-press #(hide-sheet-and-dispatch [:browser.ui/open-empty-tab])}] + [list.item/list-item + {:theme :accent + :title (if fav? (i18n/label :t/remove-favourite) (i18n/label :t/add-favourite)) + :accessibility-label :add-remove-fav + :icon (if fav? :main-icons/delete :main-icons/favourite) + :on-press #(hide-sheet-and-dispatch + (if fav? + [:browser/delete-bookmark url] + [:open-modal :new-bookmark {:url url :name name :new true}]))}] + [list.item/list-item + {:theme :accent + :title (i18n/label :t/share) + :accessibility-label :share + :icon :main-icons/share + :on-press (fn [] + (re-frame/dispatch-sync [:bottom-sheet/hide-old]) + (js/setTimeout + #(browser/share-link url) + 200))}] + [react/view {:style {:height 1 :background-color (colors/alpha colors/black 0.1)}}]]) + ]))) diff --git a/src/legacy/status_im/ui/screens/browser/permissions/views.cljs b/src/legacy/status_im/ui/screens/browser/permissions/views.cljs new file mode 100644 index 00000000000..8bb7b86c065 --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/permissions/views.cljs @@ -0,0 +1,149 @@ +(ns legacy.status-im.ui.screens.browser.permissions.views + (:require + [legacy.status-im.browser.permissions :as browser.permissions] + [legacy.status-im.ui.components.animation :as anim] + [legacy.status-im.ui.components.chat-icon.screen :as chat-icon.screen] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.screens.browser.styles :as styles] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [utils.i18n :as i18n]) + (:require-macros [legacy.status-im.utils.views :as views])) + +(defn hide-panel-anim + [bottom-anim-value alpha-value] + (anim/start + (anim/parallel + [(anim/spring bottom-anim-value + {:toValue styles/panel-height + :useNativeDriver true}) + (anim/timing alpha-value + {:toValue 0 + :duration 500 + :useNativeDriver true})]))) + +(defn show-panel-anim + [bottom-anim-value alpha-value] + (anim/start + (anim/parallel + [(anim/spring bottom-anim-value + {:toValue 20 + :useNativeDriver true}) + (anim/timing alpha-value + {:toValue 0.6 + :duration 500 + :useNativeDriver true})]))) + +(defn permission-details + [requested-permission] + (get browser.permissions/supported-permissions requested-permission)) + +(views/defview permissions-panel + [[dapp? dapp dapps-account] {:keys [dapp-name]}] + (views/letsubs [bottom-anim-value (anim/create-value styles/panel-height) + alpha-value (anim/create-value 0) + current-permission (reagent/atom nil) + update? (reagent/atom nil)] + {:UNSAFE_componentWillUpdate (fn [_ [_ _ {:keys [requested-permission]}]] + (cond + @update? + ;; the component has been updated with a new permission, we + ;; show the panel + (do (reset! update? false) + (show-panel-anim bottom-anim-value alpha-value)) + + (and @current-permission requested-permission) + ;; a permission has been accepted/denied by the user, and there + ;; is another permission that needs to be processed by the user + ;; we hide the processed permission with an animation and + ;; update `current-permission` with a delay so that the + ;; information is still available during the animation + (do (reset! update? true) + (js/setTimeout #(reset! current-permission + (permission-details requested-permission)) + 600) + (hide-panel-anim bottom-anim-value alpha-value)) + + requested-permission + ;; the dapp is asking for a permission, we put it in + ;; current-permission and start the show-animation + (do (reset! current-permission + (get browser.permissions/supported-permissions + requested-permission)) + (show-panel-anim bottom-anim-value alpha-value)) + + :else + ;; a permission has been accepted/denied by the user, and there + ;; is no other permission that needs to be processed by the + ;; user we hide the processed permission with an animation and + ;; update `current-permission` with a delay so that the + ;; information is still available during the animation + (do (js/setTimeout #(reset! current-permission nil) 500) + (hide-panel-anim bottom-anim-value alpha-value))))} + (when @current-permission + (let [{:keys [title description type icon]} @current-permission] + [react/view styles/permissions-panel-container + [react/animated-view {:style (styles/permissions-panel-background alpha-value)}] + [react/animated-view {:style (styles/permissions-panel bottom-anim-value)} + [react/view styles/permissions-panel-icons-container + (if dapp? + [chat-icon.screen/dapp-icon-permission dapp 40] + [react/view styles/permissions-panel-dapp-icon-container + [icons/icon :main-icons/dapp {:color colors/gray}]]) + [react/view {:margin-left 8 :margin-right 4} + [react/view styles/dot]] + [react/view {:margin-right 4} + [react/view styles/dot]] + [react/view {:margin-right 8} + [react/view styles/dot]] + [react/view styles/permissions-panel-ok-icon-container + [icons/icon :tiny-icons/tiny-check styles/permissions-panel-ok-ico]] + [react/view {:margin-left 8 :margin-right 4} + [react/view styles/dot]] + [react/view {:margin-right 4} + [react/view styles/dot]] + [react/view {:margin-right 8} + [react/view styles/dot]] + (if (= :wallet type) + [chat-icon.screen/custom-icon-view-list (:name dapps-account) (:color dapps-account)] + [react/view styles/permissions-panel-wallet-icon-container + (when icon + [icons/icon icon {:color colors/white}])])] + [react/text {:style styles/permissions-panel-title-label :number-of-lines 2} + (str "\"" dapp-name "\" " title)] + (when (= :wallet type) + [react/view styles/permissions-account + [icons/icon :main-icons/account {:color (:color dapps-account)}] + [react/view {:flex-shrink 1} + [react/text + {:numberOfLines 1 + :style {:margin-horizontal 6 + :color (:color dapps-account) + :typography :main-medium + :font-size 13}} + (:name dapps-account)]]]) + [react/text {:style styles/permissions-panel-description-label :number-of-lines 2} + description] + [react/view + {:style {:flex-direction :row + :justify-content :center + :margin-horizontal 8 + :margin-top 24}} + [react/view + {:flex 1 + :margin-horizontal 8} + [quo/button + {:theme :negative + :on-press #(re-frame/dispatch [:browser.permissions.ui/dapp-permission-denied])} + (i18n/label :t/deny)]] + [react/view + {:flex 1 + :margin-horizontal 8} + [quo/button + {:theme :positive + :style {:margin-horizontal 8} + :on-press #(re-frame/dispatch [:browser.permissions.ui/dapp-permission-allowed])} + (i18n/label :t/allow)]]]]])))) diff --git a/src/legacy/status_im/ui/screens/browser/site_blocked/styles.cljs b/src/legacy/status_im/ui/screens/browser/site_blocked/styles.cljs new file mode 100644 index 00000000000..d63ed64f56b --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/site_blocked/styles.cljs @@ -0,0 +1,29 @@ +(ns legacy.status-im.ui.screens.browser.site-blocked.styles + (:require + [legacy.status-im.ui.components.colors :as colors])) + +(def container + {:justify-content :center + :flex 1}) + +(def container-root-view + {:flex 1 + :margin 24 + :justify-content :center + :align-items :center}) + +(def title-text + {:typography :title-bold + :margin-top 26 + :margin-bottom 10 + :text-align :center}) + +(def description-text + {:color colors/gray + :text-align :center}) + +(def chat-link-text + {:color colors/blue}) + +(def buttons-container + {:margin 24}) diff --git a/src/legacy/status_im/ui/screens/browser/site_blocked/views.cljs b/src/legacy/status_im/ui/screens/browser/site_blocked/views.cljs new file mode 100644 index 00000000000..fe823e52a1d --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/site_blocked/views.cljs @@ -0,0 +1,40 @@ +(ns legacy.status-im.ui.screens.browser.site-blocked.views + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.screens.browser.site-blocked.styles :as styles] + [re-frame.core :as re-frame] + [utils.i18n :as i18n]) + (:require-macros [legacy.status-im.utils.views :as views])) + +(views/defview view + [{:keys [can-go-back?]}] + [react/scroll-view + {:keyboard-should-persist-taps :always + :bounces false + :content-container-style styles/container} + [react/view styles/container-root-view + [icons/icon :main-icons/info {:color colors/red}] + [react/text {:style styles/title-text} + (i18n/label :t/browsing-site-blocked-title)] + [react/nested-text {:style styles/description-text} + (i18n/label :t/browsing-site-blocked-description1) + ;; NOTE: this link is broken + [{:on-press #(.openURL ^js react/linking "status-app://chat/public/status") + :style styles/chat-link-text} + "#status"] + (i18n/label :t/browsing-site-blocked-description2)] + [react/view styles/buttons-container + [quo/button + {:on-press (fn [] + (let [handler (if can-go-back? + :browser.ui/previous-page-button-pressed + :navigate-back)] + (re-frame/dispatch [handler])))} + (i18n/label :t/browsing-site-blocked-go-back)]] + [quo/button + {:theme :negative + :on-press #(re-frame/dispatch [:browser/ignore-unsafe])} + (i18n/label :t/continue-anyway)]]]) diff --git a/src/legacy/status_im/ui/screens/browser/stack.cljs b/src/legacy/status_im/ui/screens/browser/stack.cljs new file mode 100644 index 00000000000..153b21f609c --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/stack.cljs @@ -0,0 +1,18 @@ +(ns legacy.status-im.ui.screens.browser.stack + (:require + [legacy.status-im.ui.screens.browser.empty-tab.views :as empty-tab] + [legacy.status-im.ui.screens.browser.tabs.views :as tabs] + [legacy.status-im.ui.screens.browser.views :as browser] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [utils.re-frame :as rf])) + +(defn browser-stack + [] + (let [screen-id (rf/sub [:browser/screen-id])] + [rn/view {:padding-top safe-area/top :flex 1} + (case screen-id + :empty-tab [empty-tab/empty-tab] + :browser [browser/browser] + :browser-tabs [tabs/tabs] + [empty-tab/empty-tab])])) diff --git a/src/legacy/status_im/ui/screens/browser/styles.cljs b/src/legacy/status_im/ui/screens/browser/styles.cljs new file mode 100644 index 00000000000..5cddf1e5d3d --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/styles.cljs @@ -0,0 +1,188 @@ +(ns legacy.status-im.ui.screens.browser.styles + (:require + [legacy.status-im.ui.components.colors :as colors])) + +(def browser {:flex 1}) + +(defn navbar + [] + {:background-color colors/white + :height 100 + :flex-direction :row + :align-items :center + :justify-content :space-between + :border-top-color colors/gray-lighter + :border-top-width 1 + :padding-horizontal 24 + :padding-bottom 50}) + +(def disabled-button + {:opacity 0.4}) + +(def web-view-error + {:justify-content :center + :align-items :center + :position :absolute + :padding 20 + :top 0 + :left 0 + :bottom 0 + :right 0 + :background-color colors/white}) + +(def web-view-error-text + {:color colors/gray + :line-height 22 + :text-align :center}) + +(defn toolbar-content + [] + {:flex-direction :row + :flex 1 + :border-radius 8 + :max-height 36 + :background-color colors/gray-lighter + :padding-horizontal 10 + :align-items :center + :align-self :center + :margin-horizontal 16 + :margin-vertical 10}) + +(def url-input + {:flex 1 + :text-align-vertical :center + :margin 0 + :padding 0 + :margin-left 6}) + +(def url-text-container + {:justify-content :center + :flex 1 + :margin-horizontal 5}) + +(def dot + {:height 4 + :width 4 + :background-color colors/blue + :border-radius 2}) + +(def permissions-panel-container + {:position :absolute + :top 0 + :bottom 0 + :right 0 + :left 0}) + +(defn permissions-panel-background + [alpha-value] + {:flex 1 + :background-color colors/black + :opacity alpha-value}) + +(def panel-height 354) + +(defn permissions-panel + [bottom-anim-value] + {:height panel-height + :position :absolute + :transform [{:translateY bottom-anim-value}] + :right 0 + :left 0 + :bottom 0 + :align-items :center + :background-color colors/white + :border-top-left-radius 8 + :border-top-right-radius 8}) + +(def permissions-panel-icons-container + {:margin-top 26 + :align-items :center + :justify-content :center + :flex-direction :row}) + +(def permissions-panel-dapp-icon-container + {:height 40 + :width 40 + :background-color colors/gray-lighter + :border-radius 20 + :align-items :center + :justify-content :center}) + +(def permissions-panel-ok-icon-container + {:height 24 + :width 24 + :background-color colors/blue-light + :border-radius 12 + :align-items :center + :justify-content :center}) + +(def permissions-panel-ok-ico + {:color colors/blue + :width 16 + :height 16}) + +(def blocked-access-container + {:align-items :center + :margin 16}) + +(def blocked-access-icon-container + {:height 40 + :width 40 + :background-color colors/blue-light + :border-radius 20 + :align-items :center + :justify-content :center}) + +(def blocked-access-camera-icon + {:color colors/blue + :width 20 + :height 20}) + +(def blocked-access-text-container + {:margin-top 16}) + +(def blocked-access-text + {:text-align :center}) + +(def blocked-access-buttons-container + {:flex-direction :row + :justify-content :center + :margin-top 16}) + +(def blocked-access-button-wrapper + {:flex 1 + :margin-horizontal 8}) + +(def blocked-access-button + {:margin-horizontal 8}) + +(def permissions-panel-wallet-icon-container + {:height 40 + :width 40 + :background-color colors/blue + :border-radius 20 + :align-items :center + :justify-content :center}) + +(def permissions-panel-title-label + {:typography :title-bold + :margin-horizontal 20 + :text-align :center + :margin-top 16}) + +(def permissions-panel-description-label + {:margin-horizontal 20 + :color colors/gray + :text-align :center + :margin-top 16}) + +(def permissions-account + {:flex-direction :row + :border-radius 36 + :border-width 1 + :border-color colors/gray-lighter + :padding-horizontal 8 + :padding-vertical 6 + :margin-top 16 + :align-items :center + :justify-content :center}) diff --git a/src/legacy/status_im/ui/screens/browser/tabs/views.cljs b/src/legacy/status_im/ui/screens/browser/tabs/views.cljs new file mode 100644 index 00000000000..45ad9f0bda1 --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/tabs/views.cljs @@ -0,0 +1,82 @@ +(ns legacy.status-im.ui.screens.browser.tabs.views + (:require-macros [legacy.status-im.utils.views :as views]) + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.list.item :as list.item] + [legacy.status-im.ui.components.list.views :as list] + [legacy.status-im.ui.components.plus-button :as components.plus-button] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.components.topbar :as topbar] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [utils.i18n :as i18n] + [utils.url :as url])) + +(defn list-item + [_] + (let [loaded (reagent/atom nil)] + (fn [{:keys [browser-id name url empty-tab]}] + [react/view {:flex-direction :row :flex 1} + [react/view {:flex 1} + [list.item/list-item + {:on-press #(if empty-tab + (re-frame/dispatch [:browser.ui/open-empty-tab]) + (re-frame/dispatch [:browser.ui/browser-item-selected browser-id])) + :title name + :subtitle (when-not empty-tab (or url (i18n/label :t/dapp))) + :accessibility-label (keyword (str "tab-item" name)) + :icon [react/view + {:width 40 + :height 40 + :align-items :center + :justify-content :center} + (when (or (nil? @loaded) @loaded) + [react/image + {:onLoad #(reset! loaded true) + :style {:width 32 :height 32 :position :absolute :top 4 :left 4} + :source {:uri (str "https://" (url/url-host url) "/favicon.ico")}}]) + (when-not @loaded + [react/view + {:width 40 + :height 40 + :border-radius 20 + :background-color colors/gray-lighter + :align-items :center + :justify-content :center} + [icons/icon :main-icons/browser {:color colors/gray}]])]}]] + (when-not empty-tab + [react/touchable-highlight + {:style {:width 60 :justify-content :center :align-items :center} + :accessibility-label :empty-tab + :on-press #(re-frame/dispatch [:browser.ui/remove-browser-pressed browser-id])} + [icons/icon :main-icons/close-circle {:color colors/gray}]])]))) + +(views/defview tabs + [] + (views/letsubs [browsers [:browser/browsers-vals]] + [react/view {:flex 1 :margin-bottom 50} + [topbar/topbar + {:modal? true + :border-bottom false + :navigation :none + :right-accessories + [{:label (i18n/label :t/close-all) + :accessibility-label :close-all + :on-press #(do (re-frame/dispatch [:browser.ui/clear-all-browsers-pressed]) + (re-frame/dispatch [:browser.ui/open-empty-tab]))}] + :title (i18n/label :t/tabs)}] + [react/view {:style {:height 1 :background-color (colors/alpha colors/black 0.1)}}] + [list/flat-list + {:data (conj browsers + {:empty-tab true + :name (i18n/label :t/empty-tab) + :url ""}) + :footer [react/view + {:style {:height 64 + :align-self :stretch}}] + :key-fn :browser-id + :render-fn list-item}] + + [components.plus-button/plus-button-old + {:on-press #(re-frame/dispatch [:browser.ui/open-empty-tab])}]])) diff --git a/src/legacy/status_im/ui/screens/browser/views.cljs b/src/legacy/status_im/ui/screens/browser/views.cljs new file mode 100644 index 00000000000..c2a29313d71 --- /dev/null +++ b/src/legacy/status_im/ui/screens/browser/views.cljs @@ -0,0 +1,278 @@ +(ns legacy.status-im.ui.screens.browser.views + (:require + [legacy.status-im.browser.core :as browser] + [legacy.status-im.browser.webview-ref :as webview-ref] + [legacy.status-im.ui.components.chat-icon.screen :as chat-icon] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.connectivity.view :as connectivity] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.components.tooltip.views :as tooltip] + [legacy.status-im.ui.components.webview :as components.webview] + [legacy.status-im.ui.screens.browser.accounts :as accounts] + [legacy.status-im.ui.screens.browser.options.views :as options] + [legacy.status-im.ui.screens.browser.permissions.views :as permissions.views] + [legacy.status-im.ui.screens.browser.site-blocked.views :as site-blocked.views] + [legacy.status-im.ui.screens.browser.styles :as styles] + [legacy.status-im.utils.js-resources :as js-res] + [re-frame.core :as re-frame] + [react-native.permissions :as components.permissions] + [reagent.core :as reagent] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.url :as url]) + (:require-macros [legacy.status-im.utils.views :as views])) + +(defn toolbar-content + [url url-original secure? url-editing? unsafe?] + (let [url-text (atom url)] + [react/view (styles/toolbar-content) + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:browser.ui/lock-pressed secure?]) + :accessibility-label :security-icon} + (if secure? + [icons/tiny-icon :tiny-icons/tiny-lock {:color colors/green}] + [icons/tiny-icon :tiny-icons/tiny-lock-broken {:color colors/black}])] + (if url-editing? + [react/text-input + {:on-change-text #(reset! url-text %) + :on-blur #(re-frame/dispatch [:browser.ui/url-input-blured]) + :on-submit-editing #(re-frame/dispatch [:browser.ui/url-submitted @url-text]) + :placeholder (i18n/label :t/enter-url) + :auto-capitalize :none + :auto-correct false + :auto-focus true + :default-value url + :ellipsize :end + :style styles/url-input + :accessibility-label :browser-input}] + [react/touchable-highlight + {:style styles/url-text-container + :on-press #(re-frame/dispatch [:browser.ui/url-input-pressed])} + [react/text {:number-of-lines 1} (url/url-host url-original)]]) + (when-not unsafe? + [react/touchable-highlight + {:on-press #(.reload ^js @webview-ref/webview-ref) + :accessibility-label :refresh-page-button} + [icons/icon :main-icons/refresh]])])) + +(defn- web-view-error + [_ _ desc] + (reagent/as-element + [react/view styles/web-view-error + [react/image + {:style {:width 140 :height 140 :margin-bottom 16} + :source {:uri + "https://bafybeiabxprplp52amc2hpocar753rd64wk2len55zq54yca77bekzre4e.ipfs.cf-ipfs.com"}}] + [react/i18n-text {:style styles/web-view-error-text :key :web-view-error}] + [react/text {:style styles/web-view-error-text} + (str desc)]])) + +(views/defview navigation + [{:keys [url can-go-back? can-go-forward? dapps-account empty-tab browser-id name]}] + (views/letsubs [accounts [:visible-accounts-without-watch-only]] + [react/view (styles/navbar) + [react/touchable-highlight + {:on-press #(if can-go-back? + (re-frame/dispatch [:browser.ui/previous-page-button-pressed]) + (do + (re-frame/dispatch [:browser.ui/remove-browser-pressed browser-id]) + (re-frame/dispatch [:browser.ui/open-empty-tab]))) + :disabled empty-tab + :style (when empty-tab styles/disabled-button) + :accessibility-label :previous-page-button} + [icons/icon :main-icons/arrow-left {:color colors/black}]] + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:browser.ui/next-page-button-pressed]) + :disabled (not can-go-forward?) + :style (when-not can-go-forward? styles/disabled-button) + :accessibility-label :next-page-button} + [icons/icon :main-icons/arrow-right {:color colors/black}]] + [react/touchable-highlight + {:accessibility-label :select-account + :on-press #(re-frame/dispatch [:show-bottom-sheet + {:content (accounts/accounts-list accounts + dapps-account)}])} + [chat-icon/custom-icon-view-list (:name dapps-account) (:color dapps-account) 32]] + + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:browser.ui/open-browser-tabs]) + :accessibility-label :browser-open-tabs} + [icons/icon :main-icons/tabs {:color colors/black}]] + + (if empty-tab + [react/touchable-highlight + {:accessibility-label :universal-qr-scanner + :on-press #(re-frame/dispatch [:open-modal :screen/shell-qr-reader])} + [icons/icon :main-icons/qr {:color colors/black}]] + [react/touchable-highlight + {:on-press #(re-frame/dispatch + [:show-bottom-sheet + {:content (options/browser-options + url + dapps-account + empty-tab + name)}]) + :accessibility-label :browser-options} + [icons/icon :main-icons/more {:color colors/black}]])])) + +(def resources-to-permissions-map + {"android.webkit.resource.VIDEO_CAPTURE" :camera + "android.webkit.resource.AUDIO_CAPTURE" :record-audio}) + +(views/defview request-resources-panel + [resources url webview-ref] + [react/view styles/blocked-access-container + [react/view styles/blocked-access-icon-container + [icons/icon :main-icons/camera styles/blocked-access-camera-icon]] + [react/view styles/blocked-access-text-container + [react/text {:style styles/blocked-access-text} + (str url " " (i18n/label :t/page-would-like-to-use-camera))]] + [react/view styles/blocked-access-buttons-container + [react/view styles/blocked-access-button-wrapper + [quo/button + {:theme :positive + :style styles/blocked-access-button + :on-press (fn [] + (components.permissions/request-permissions + {:permissions (map resources-to-permissions-map resources) + :on-allowed #(.answerPermissionRequest ^js webview-ref true resources) + :on-denied #(.answerPermissionRequest ^js webview-ref false)}) + (re-frame/dispatch [:bottom-sheet/hide-old]))} + (i18n/label :t/allow)]] + [react/view styles/blocked-access-button-wrapper + [quo/button + {:theme :negative + :style styles/blocked-access-button + :on-press (fn [] + (.answerPermissionRequest ^js webview-ref false) + (re-frame/dispatch [:bottom-sheet/hide-old]))} + (i18n/label :t/deny)]]]]) + +(views/defview block-resources-panel + [url] + [react/view styles/blocked-access-container + [react/view styles/blocked-access-icon-container + [icons/icon :main-icons/camera styles/blocked-access-camera-icon]] + [react/view styles/blocked-access-text-container + [react/text {:style styles/blocked-access-text} + (str url " " (i18n/label :t/page-camera-request-blocked))]]]) + +(defn request-resources-access-for-page + [resources url webview-ref] + (re-frame/dispatch + [:show-bottom-sheet + {:content (fn [] [request-resources-panel resources url webview-ref]) + :show-handle? false + :backdrop-dismiss? false + :disable-drag? true + :back-button-cancel false}])) + +(defn block-resources-access-and-notify-user + [url] + (.answerPermissionRequest ^js @webview-ref/webview-ref false) + (re-frame/dispatch [:show-bottom-sheet + {:content (fn [] [block-resources-panel url])}])) + +;; should-component-update is called only when component's props are changed, +;; that's why it can't be used in `browser`, because `url` comes from subs +(views/defview browser-component + [{:keys [error? url browser-id unsafe? can-go-back? ignore-unsafe + can-go-forward? resolving? network-id url-original dapp? dapp + show-permission show-tooltip name dapps-account resources-permission?]}] + {:should-component-update (fn [_ _ args] + (let [[_ props] args] + (not (nil? (:url props)))))} + [react/view + {:flex 1 + :elevation -10} + [react/view {:flex 1} + (if (and unsafe? (not= (url/url-host url) ignore-unsafe)) + [site-blocked.views/view + {:can-go-back? can-go-back? + :site browser-id}] + [components.webview/webview + {:dapp? dapp? + :dapp-name name + :ref #(reset! webview-ref/webview-ref %) + :source (when (and url (not resolving?)) {:uri url}) + :java-script-enabled true + :bounces false + :local-storage-enabled true + :set-support-multiple-windows false + :render-error web-view-error + :on-navigation-state-change #(do + (re-frame/dispatch [:set-in + [:ens/registration :state] + :searching]) + (debounce/debounce-and-dispatch + [:browser/navigation-state-changed % error?] + 500)) + + :on-permission-request #(if resources-permission? + (request-resources-access-for-page + (-> ^js % .-nativeEvent .-resources) + url + @webview-ref/webview-ref) + (block-resources-access-and-notify-user url)) + ;; Extract event data here due to https://reactjs.org/docs/events.html#event-pooling + :on-message #(re-frame/dispatch [:browser/bridge-message-received + (.. ^js % -nativeEvent -data)]) + :on-load #(re-frame/dispatch [:browser/loading-started]) + :on-error #(re-frame/dispatch [:browser/error-occured]) + :injected-java-script-before-content-loaded (js-res/ethereum-provider (str network-id)) + ;; https://github.com/status-im/status-mobile/issues/17854 + :allows-inline-media-playback true}])] + [navigation + {:url url-original + :name name + :can-go-back? can-go-back? + :can-go-forward? can-go-forward? + :dapps-account dapps-account + :browser-id browser-id}] + [permissions.views/permissions-panel [dapp? dapp dapps-account] show-permission] + (when show-tooltip + [tooltip/bottom-tooltip-info + (if (= show-tooltip :secure) + (i18n/label :t/browser-secure) + (i18n/label :t/browser-not-secure)) + #(re-frame/dispatch [:browser.ui/close-tooltip-pressed])])]) + +(views/defview browser + [] + (views/letsubs [window-width [:dimensions/window-width] + {:keys [browser-id dapp? dapp name unsafe? ignore-unsafe secure?] :as current-browser} + [:get-current-browser] + {:keys [url error? loading? url-editing? show-tooltip show-permission resolving?]} + [:browser/options] + dapps-account [:dapps-account] + network-id [:chain-id] + {:keys [webview-allow-permission-requests?]} [:profile/profile]] + (let [can-go-back? (browser/can-go-back? current-browser) + can-go-forward? (browser/can-go-forward? current-browser) + url-original (browser/get-current-url current-browser)] + [react/view {:style styles/browser} + [toolbar-content url url-original secure? url-editing? unsafe?] + [react/view {:style {:height 1 :background-color (colors/alpha colors/black 0.1)}}] + [react/view + (when loading? + [connectivity/loading-indicator-anim window-width])] + [browser-component + {:dapp? dapp? + :dapp dapp + :error? error? + :url url + :url-original url-original + :browser-id browser-id + :unsafe? unsafe? + :ignore-unsafe ignore-unsafe + :can-go-back? can-go-back? + :can-go-forward? can-go-forward? + :resolving? resolving? + :network-id network-id + :show-permission show-permission + :show-tooltip show-tooltip + :name name + :dapps-account dapps-account + :resources-permission? webview-allow-permission-requests?}]]))) diff --git a/src/legacy/status_im/ui/screens/bug_report.cljs b/src/legacy/status_im/ui/screens/bug_report.cljs new file mode 100644 index 00000000000..84c7f9d8af3 --- /dev/null +++ b/src/legacy/status_im/ui/screens/bug_report.cljs @@ -0,0 +1,56 @@ +(ns legacy.status-im.ui.screens.bug-report + (:require + [legacy.status-im.ui.components.core :as component] + [quo.core :as quo] + [re-frame.core :as re-frame] + [react-native.core :as react-native] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn bug-report + [] + (let [{:keys [description steps]} @(re-frame/subscribe [:bug-report/details])] + [react-native/view {:style {:flex 1}} + [quo/page-nav + {:type :title + :title (i18n/label :t/bug-report) + :background :blur + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + [react-native/view + {:style {:flex 1 + :padding-top 8 + :padding-horizontal 16}} + [component/text-input + {:label (i18n/label :t/bug-report-description) + :default-value description + :placeholder (i18n/label :t/bug-report-description-placeholder) + :style {:margin-bottom 8} + :multiline true + :error @(re-frame/subscribe [:bug-report/description-error]) + :accessibility-label :bug-report-description + :on-change-text #(re-frame/dispatch [:logging/report-details :description %])}] + [component/text-input + {:label (i18n/label :t/bug-report-steps) + :default-value steps + :placeholder (i18n/label :t/bug-report-steps-placeholder) + :style {:margin-bottom 16} + :multiline true + :accessibility-label :bug-report-steps + :on-change-text #(re-frame/dispatch [:logging/report-details :steps %])}] + [component/button + {:type :primary + :accessibility-label :bug-report-submit + :theme :accent + :on-press #(re-frame/dispatch [:logging/submit-report])} + (i18n/label :t/bug-report-submit-email)] + [react-native/view + {:style {:margin-vertical 16 + :align-items :center}} + [component/text (i18n/label :t/or)]] + [component/button + {:type :primary + :accessibility-label :bug-report-submit-gh-issue + :theme :accent + :on-press #(re-frame/dispatch [:logging/submit-gh-issue])} + (i18n/label :t/bug-report-submit-gh-issue)]]])) diff --git a/src/legacy/status_im/ui/screens/chat/group.cljs b/src/legacy/status_im/ui/screens/chat/group.cljs new file mode 100644 index 00000000000..c21ebde1d73 --- /dev/null +++ b/src/legacy/status_im/ui/screens/chat/group.cljs @@ -0,0 +1,66 @@ +(ns legacy.status-im.ui.screens.chat.group + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.react :as react] + [re-frame.core :as re-frame] + [react-native.platform :as platform] + [status-im.constants :as constants] + [utils.i18n :as i18n]) + (:require-macros [legacy.status-im.utils.views :refer [defview letsubs]])) + +(def message-max-length 100) + +(defn request-membership + [{:keys [state introduction-message] :as invitation} chat-id] + (let [{:keys [message retry?]} @(re-frame/subscribe [:chats/current-chat-membership]) + message-length (count message)] + [react/view {:margin-horizontal 16 :margin-top 10} + (cond + (and invitation (= constants/invitation-state-requested state) (not retry?)) + [react/view + [react/text (i18n/label :t/introduce-yourself)] + [react/text + {:style {:margin-top 10 + :margin-bottom 16 + :min-height 66 + :padding-horizontal 16 + :padding-vertical 11 + :border-color colors/gray-lighter + :border-width 1 + :border-radius 8 + :color colors/gray}} + introduction-message] + [react/text + {:style {:align-self :flex-end + :margin-bottom 30 + :color colors/gray}} + (str (count introduction-message) "/100")]] + + (and invitation (= constants/invitation-state-rejected state) (not retry?)) + [react/view + [react/text {:style {:align-self :center :margin-bottom 30}} + (i18n/label :t/membership-declined)]] + + :else + [react/view + [react/text (i18n/label :t/introduce-yourself)] + [quo/text-input + {:placeholder (i18n/label :t/message) + :on-change-text #(re-frame/dispatch [:group-chats.ui/update-membership-message % chat-id]) + :max-length (if platform/android? + message-max-length + (when (>= message-length message-max-length) + message-length)) + :multiline true + :default-value message + :accessibility-label :introduce-yourself-input + :container-style {:margin-top 10 :margin-bottom 16}}] + [react/text {:style {:align-self :flex-end :margin-bottom 30}} + (str (count message) "/100")]])])) + +(defview group-chat-footer + [chat-id invitation-admin] + (letsubs [invitations [:group-chat/invitations-by-chat-id chat-id]] + (when invitation-admin + [request-membership (first invitations) chat-id]))) diff --git a/src/legacy/status_im/ui/screens/chat/message/legacy_style.cljs b/src/legacy/status_im/ui/screens/chat/message/legacy_style.cljs new file mode 100644 index 00000000000..7d878879609 --- /dev/null +++ b/src/legacy/status_im/ui/screens/chat/message/legacy_style.cljs @@ -0,0 +1,180 @@ +(ns legacy.status-im.ui.screens.chat.message.legacy-style + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react] + [quo.foundations.colors :as quo.colors] + [quo.foundations.typography :as typography] + [status-im.constants :as constants])) + +(defn message-wrapper + [{:keys [in-popover?]}] + (if (not in-popover?) + {:margin-left 10 + :padding-right 5} + {:margin-right 10})) + +(def status-container + {:padding-horizontal 5}) + +(defn status-text + [] + {:margin-top 9 + :font-size 14 + :color colors/gray}) + +(defn message-default-style + [theme] + {:font-family "Inter-Regular" + :color (quo.colors/theme-colors quo.colors/neutral-100 quo.colors/white theme) + :font-size 15 + :line-height 21.75 + :letter-spacing -0.135}) + +;; Markdown styles +(defn default-text-style + [theme] + {:max-font-size-multiplier react/max-font-size-multiplier + :style (message-default-style theme)}) + +(defn system-text-style + [theme] + (update (default-text-style theme) + :style assoc + :color colors/gray + :line-height 20 + :font-size 14 + :text-align :center + :font-weight "400")) + +(defn text-style + [content-type in-popover? theme] + (merge + (when in-popover? {:number-of-lines 2}) + (cond + (= content-type constants/content-type-system-text) (system-text-style theme) + (= content-type constants/content-type-system-pinned-message) (system-text-style theme) + :else (default-text-style theme)))) + +(defn emph-text-style + [theme] + (update (default-text-style theme) + :style + assoc + :font-style :italic)) + +(defn emph-style + [theme] + (emph-text-style theme)) + +(defn strong-text-style + [theme] + (update (default-text-style theme) + :style + assoc + :font-weight "700")) + +(defn outgoing-strong-text-style + [theme] + (update (strong-text-style theme) + :style + assoc + :color colors/white-persist)) + +(defn strong-style + [theme] + (outgoing-strong-text-style theme) + (strong-text-style theme)) + +(defn strong-emph-style + [theme] + (update (strong-style theme) + :style + assoc + :font-style :italic)) + +(defn strikethrough-style + [theme] + (cond-> (update (default-text-style theme) + :style + assoc + :text-decoration-line :line-through))) + +(defn edited-style + [theme] + (cond-> + (update (default-text-style theme) + :style + assoc + :color (quo.colors/theme-colors quo.colors/neutral-40 quo.colors/neutral-50 theme) + :font-size 13 + :line-height 18.2 + :letter-spacing (typography/tracking 13)))) + +(def codeblock-style + {:padding 10 + :background-color "#2E386B" + :border-radius 4}) + +(defn default-blockquote-style + [] + {:style {:border-left-width 2 + :padding-left 3 + :border-left-color colors/gray-transparent-40}}) + +(defn blockquote-style + [] + (default-blockquote-style)) + +(defn default-blockquote-text-style + [theme] + (update (default-text-style theme) + :style + assoc + :line-height 19 + :font-size 14 + :color colors/black-transparent-50)) + +(defn outgoing-blockquote-text-style + [theme] + (update (default-blockquote-text-style theme) + :style + assoc + :color colors/white-transparent-70-persist)) + +(defn blockquote-text-style + [theme] + (outgoing-blockquote-text-style theme) + (default-blockquote-text-style theme)) + +(defn community-verified + [] + {:border-right-width 1 + :border-left-width 1 + :border-top-width 1 + :border-left-color colors/gray-lighter + :border-right-color colors/gray-lighter + :border-top-left-radius 10 + :border-top-right-radius 10 + :padding-vertical 8 + :padding-horizontal 15 + :border-top-color colors/gray-lighter}) + +(defn community-message + [verified] + {:flex-direction :row + :padding-vertical 12 + :border-top-left-radius (when-not verified 10) + :border-top-right-radius (when-not verified 10) + :border-right-width 1 + :border-left-width 1 + :border-top-width 1 + :border-color colors/gray-lighter}) + +(defn community-view-button + [] + {:border-width 1 + :padding-vertical 8 + :border-bottom-left-radius 10 + :border-bottom-right-radius 10 + :border-color colors/gray-lighter}) + diff --git a/src/legacy/status_im/ui/screens/chat/message/legacy_view.cljs b/src/legacy/status_im/ui/screens/chat/message/legacy_view.cljs new file mode 100644 index 00000000000..cfd2e04c5da --- /dev/null +++ b/src/legacy/status_im/ui/screens/chat/message/legacy_view.cljs @@ -0,0 +1,193 @@ +(ns legacy.status-im.ui.screens.chat.message.legacy-view + (:require + [legacy.status-im.ui.components.colors :as quo.colors] + [legacy.status-im.ui.screens.chat.message.legacy-style :as style] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [quo.foundations.typography :as typography] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.delete-message-for-me.events] + [status-im.contexts.chat.messenger.messages.delete-message.events] + [utils.i18n :as i18n] + [utils.re-frame :as rf]) + (:require-macros [legacy.status-im.utils.views :refer [defview letsubs]])) + +(defn system-text? + [content-type] + (or + (= content-type constants/content-type-system-text) + (= content-type constants/content-type-system-pinned-message))) + +(defn mention-element + [from] + (rf/sub [:messages/resolve-mention from])) + +(defn render-inline + [_message-text content-type acc {:keys [type literal destination]} + community-id theme] + (case type + "" + (conj acc literal) + + "code" + (conj acc [rn/text literal]) + + "emph" + (conj acc [rn/text (style/emph-style theme) literal]) + + "strong" + (conj acc [rn/text (style/strong-style theme) literal]) + + "strong-emph" + (conj acc [quo/text (style/strong-emph-style theme) literal]) + + "del" + (conj acc [rn/text (style/strikethrough-style theme) literal]) + + "link" + (conj + acc + [rn/text + {:style {:color (colors/theme-colors colors/primary-50 colors/primary-60 theme) + :text-decoration-line :underline} + :on-press #(rf/dispatch [:browser.ui/message-link-pressed destination])} + destination]) + + "mention" + (conj + acc + [rn/view + {:style {:background-color colors/primary-50-opa-10 + :border-radius 6 + :padding-horizontal 3 + :margin-top -3}} + [rn/text + {:style (merge {:color (if (system-text? content-type) quo.colors/black colors/primary-50)} + (if (system-text? content-type) typography/font-regular typography/font-medium)) + :on-press (when-not (system-text? content-type) + #(rf/dispatch [:chat.ui/show-profile literal]))} + [mention-element literal]]]) + "status-tag" + (conj + acc + [rn/text + (when community-id + {:style {:color (colors/theme-colors colors/primary-50 colors/primary-60 theme) + :text-decoration-line :underline} + :on-press #(rf/dispatch [:communities/status-tag-pressed community-id literal])}) + "#" + literal]) + + "edited" + (conj acc [rn/text (style/edited-style theme) (str " (" (i18n/label :t/edited) ")")]) + + (conj acc literal))) + +;; TEXT +(defn render-block + [{:keys [content content-type edited-at in-popover?]} acc + {:keys [type ^js literal children]} + community-id theme] + + (case type + + "paragraph" + (conj acc + (reduce + (fn [acc e] + (render-inline (:text content) + content-type + acc + e + community-id + theme)) + [rn/text (style/text-style content-type in-popover? theme)] + (conj + children + (when edited-at + {:type "edited"})))) + + "blockquote" + (conj acc + [rn/view (style/blockquote-style) + [rn/text (style/blockquote-text-style theme) + (.substring literal 0 (.-length literal))]]) + + "codeblock" + (conj acc + [rn/view {:style style/codeblock-style} + [rn/text (.substring literal 0 (dec (.-length literal)))]]) + + acc)) + +(defn render-parsed-text + [{:keys [content chat-id] + :as message-data}] + (let [community-id (rf/sub [:community-id-by-chat-id chat-id]) + theme (quo.context/use-theme)] + (reduce (fn [acc e] + (render-block message-data + acc + e + community-id + theme)) + [:<>] + (:parsed-text content)))) + +(defmulti ->message :content-type) + +(defmethod ->message constants/content-type-gap + [_] + [rn/view]) + +;; STATUS ? whats that ? +(defmethod ->message constants/content-type-status + [{:keys [content content-type]}] + (let [theme (quo.context/use-theme)] + [rn/view style/status-container + [rn/text {:style (style/status-text)} + (reduce + (fn [acc e] (render-inline (:text content) content-type acc e nil theme)) + [rn/text {:style (style/status-text)}] + (-> content :parsed-text peek :children))]])) + +;;;; SYSTEM + +(defview community-content + [{:keys [community-id] :as message}] + (letsubs [{:keys [name description verified] :as community} [:communities/community community-id]] + (when community + [rn/view + {:style (assoc (style/message-wrapper message) + :margin-vertical 10 + :margin-left 8 + :width 271)} + (when verified + [rn/view (style/community-verified) + [rn/text + {:style {:font-size 13 + :color quo.colors/blue}} (i18n/label :t/communities-verified)]]) + [rn/view (style/community-message verified) + [rn/view + {:width 62 + :padding-left 14}] + [rn/view {:padding-right 14 :flex 1} + [rn/text {:style {:font-weight "700" :font-size 17 :color quo.colors/black}} + name] + [rn/text {:style {:color quo.colors/black}} description]]] + [rn/view (style/community-view-button) + [rn/touchable-opacity + {:on-press #(do + (rf/dispatch [:pop-to-root :screen/shell-stack]) + (rf/dispatch [:communities/navigate-to-community-overview (:id community)]) + (rf/dispatch [:chat/close]))} + [rn/text + {:style {:text-align :center + :color quo.colors/blue}} (i18n/label :t/view)]]]]))) + +;; COMMUNITY (like system ? ) no wrapper +(defn community + [message] + [community-content message]) diff --git a/src/legacy/status_im/ui/screens/chat/photos.cljs b/src/legacy/status_im/ui/screens/chat/photos.cljs new file mode 100644 index 00000000000..e78e5b79aa3 --- /dev/null +++ b/src/legacy/status_im/ui/screens/chat/photos.cljs @@ -0,0 +1,46 @@ +(ns legacy.status-im.ui.screens.chat.photos + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.fast-image :as fast-image] + [legacy.status-im.ui.components.react :as react])) + +(defn radius [size] (/ size 2)) + +(defn photo-container + [size] + {:position :relative + :border-radius (radius size)}) + +(defn photo-border + ([size] (photo-border size :absolute)) + ([size position] + {:position position + :width size + :height size + :border-color colors/black-transparent + :border-width 1 + :border-radius (radius size)})) + +(defn photo-style + [size] + {:border-radius (radius size) + :width size + :height size + :background-color colors/white}) + +(def memo-photo-rend + (memoize + (fn [photo-path size accessibility-label _] + [react/view {:style (photo-container size)} + [fast-image/fast-image + {:source photo-path + :style (photo-style size) + :accessibility-label (or accessibility-label :chat-icon)}] + [react/view {:style (photo-border size)}]]))) + +;; "(colors/dark?)" is passed to memoized function to avoid previous themes cache +;; TODO: it's only used for old code, `quo/user-avatar` should be used instead for all the new one +(defn photo + ^:deprecated + [photo-path {:keys [size accessibility-label]}] + [memo-photo-rend photo-path size accessibility-label (colors/dark?)]) diff --git a/src/legacy/status_im/ui/screens/communities/members.cljs b/src/legacy/status_im/ui/screens/communities/members.cljs new file mode 100644 index 00000000000..203f46e06c2 --- /dev/null +++ b/src/legacy/status_im/ui/screens/communities/members.cljs @@ -0,0 +1,141 @@ +(ns legacy.status-im.ui.screens.communities.members + (:require + [legacy.status-im.communities.core :as communities] + [legacy.status-im.ui.components.chat-icon.screen :as chat-icon] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.list.item :as list.item] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.components.topbar :as topbar] + [legacy.status-im.ui.components.unviewed-indicator :as unviewed-indicator] + [quo.context] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hide-sheet-and-dispatch + [event] + (rf/dispatch [:bottom-sheet/hide-old]) + (rf/dispatch event)) + +(defn member-sheet + [primary-name {:keys [public-key] :as member} community-id can-kick-users? can-manage-users? admin?] + [:<> + [list.item/list-item + {:theme :accent + :icon [chat-icon/contact-icon-contacts-tab member] + :title primary-name + :subtitle (i18n/label :t/view-profile) + :accessibility-label :view-chat-details-button + :chevron true + :on-press #(hide-sheet-and-dispatch [:chat.ui/show-profile public-key])}] + (when can-kick-users? + [:<> + [quo/separator {:style {:margin-vertical 8}}] + [list.item/list-item + {:theme :negative + :icon :main-icons/arrow-left + :title (i18n/label :t/member-kick) + :on-press #(rf/dispatch [::communities/member-kick community-id public-key])}]]) + (when can-manage-users? + [:<> + [list.item/list-item + {:theme :negative + :icon :main-icons/cancel + :title (i18n/label :t/member-ban) + :on-press #(rf/dispatch [::communities/member-ban community-id public-key])}]]) + (when admin? + [:<> + [list.item/list-item + {:theme :accent + :icon :main-icons/make-admin + :title (i18n/label :t/make-moderator) + :on-press #(rf/dispatch [:community.member/add-role community-id public-key + constants/community-member-role-moderator])}]])]) + +(defn render-member + [public-key _ _ + {:keys [community-id + my-public-key + can-manage-users? + can-kick-users? + admin?]}] + (let [member (rf/sub [:contacts/contact-by-identity public-key]) + [primary-name secondary-name] (rf/sub [:contacts/contact-two-names-by-identity public-key])] + [list.item/list-item + {:title primary-name + :subtitle secondary-name + :accessibility-label :member-item + :icon [chat-icon/profile-photo-plus-dot-view + {:public-key public-key + :photo-path (profile.utils/photo member)}] + :accessory (when (not= public-key my-public-key) + [quo/button + {:on-press + #(rf/dispatch [:show-bottom-sheet + {:content (fn [] + [member-sheet primary-name member community-id + can-kick-users? can-manage-users? admin?])}]) + :type :icon + :theme :icon + :accessibility-label :menu-option} + :main-icons/more])}])) + +(defn header + [community-id] + [:<> + [list.item/list-item + {:icon :main-icons/share + :title (i18n/label :t/invite-people) + :accessibility-label :community-invite-people + :theme :accent + :on-press #(rf/dispatch [:communities/invite-people-pressed community-id])}] + [quo/separator {:style {:margin-vertical 8}}]]) + +(defn requests-to-join + [community-id] + (let [requests (rf/sub [:communities/requests-to-join-for-community community-id]) + requests-count (count requests)] + [:<> + [list.item/list-item + {:chevron true + :accessory + [react/view {:flex-direction :row} + (when (pos? requests-count) + [unviewed-indicator/unviewed-indicator requests-count])] + :on-press #(rf/dispatch [:navigate-to :screen/community-requests-to-join + {:community-id community-id}]) + :title (i18n/label :t/membership-requests)}] + [quo/separator {:style {:margin-vertical 8}}]])) + +(defn view + [] + (let [{:keys [community-id]} (quo.context/use-screen-params) + my-public-key (rf/sub [:multiaccount/public-key]) + {:keys [permissions + can-manage-users? + admin]} + (rf/sub [:communities/community community-id]) + sorted-members (rf/sub [:communities/sorted-community-members + community-id])] + (rn/use-mount + #(rf/dispatch [:community/fetch-requests-to-join community-id])) + [:<> + [topbar/topbar + {:title (i18n/label :t/community-members-title) + :subtitle (str (count sorted-members))}] + [header community-id] + (when (and can-manage-users? (= constants/community-on-request-access (:access permissions))) + [requests-to-join community-id]) + [rn/flat-list + {:data sorted-members + :render-data {:community-id community-id + :my-public-key my-public-key + :can-kick-users? (and can-manage-users? + (not= (:access permissions) + constants/community-no-membership-access)) + :can-manage-users? can-manage-users? + :admin? admin} + :key-fn identity + :render-fn render-member}]])) diff --git a/src/legacy/status_im/ui/screens/help_center/styles.cljs b/src/legacy/status_im/ui/screens/help_center/styles.cljs new file mode 100644 index 00000000000..1d7e5f21b33 --- /dev/null +++ b/src/legacy/status_im/ui/screens/help_center/styles.cljs @@ -0,0 +1 @@ +(ns legacy.status-im.ui.screens.help-center.styles) diff --git a/src/legacy/status_im/ui/screens/help_center/views.cljs b/src/legacy/status_im/ui/screens/help_center/views.cljs new file mode 100644 index 00000000000..4789dd3ebfd --- /dev/null +++ b/src/legacy/status_im/ui/screens/help_center/views.cljs @@ -0,0 +1,35 @@ +(ns legacy.status-im.ui.screens.help-center.views + (:require + [legacy.status-im.ui.components.list.item :as list.item] + [legacy.status-im.ui.components.list.views :as list] + [quo.core :as quo] + [re-frame.core :as re-frame] + [status-im.constants :as const] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def data + [{:size :small + :title (i18n/label :t/submit-bug) + :accessibility-label :submit-bug-button + :on-press #(re-frame/dispatch [:open-modal :screen/bug-report]) + :chevron true} + {:size :small + :title (i18n/label :t/status-help) + :accessibility-label :status-help-button + :on-press #(rf/dispatch [:open-url const/status-help-link]) + :chevron true}]) + +(defn help-center + [] + [:<> + [quo/page-nav + {:type :title + :title (i18n/label :t/need-help) + :background :blur + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + [list/flat-list + {:data data + :key-fn (fn [_ i] (str i)) + :render-fn list.item/list-item}]]) diff --git a/src/legacy/status_im/ui/screens/log_level_settings/styles.cljs b/src/legacy/status_im/ui/screens/log_level_settings/styles.cljs new file mode 100644 index 00000000000..b774307e13d --- /dev/null +++ b/src/legacy/status_im/ui/screens/log_level_settings/styles.cljs @@ -0,0 +1,33 @@ +(ns legacy.status-im.ui.screens.log-level-settings.styles + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.utils.styles :as styles])) + +(def log-level-item-inner + {:padding-horizontal 16}) + +(styles/def log-level-item + {:flex-direction :row + :align-items :center + :padding-horizontal 16 + :ios {:height 64} + :android {:height 56}}) + +(def log-level-item-name-text + {:typography :title}) + +(defn log-level-icon-container + [current?] + {:width 40 + :height 40 + :border-radius 20 + :background-color (if current? + colors/blue + colors/black-transparent) + :align-items :center + :justify-content :center}) + +(defn log-level-icon + [current?] + (hash-map :color + (if current? colors/white-persist colors/gray))) diff --git a/src/legacy/status_im/ui/screens/pairing/styles.cljs b/src/legacy/status_im/ui/screens/pairing/styles.cljs new file mode 100644 index 00000000000..3126e8dc6c8 --- /dev/null +++ b/src/legacy/status_im/ui/screens/pairing/styles.cljs @@ -0,0 +1,76 @@ +(ns legacy.status-im.ui.screens.pairing.styles + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.utils.styles :as styles])) + +(def wrapper + {:flex 1}) + +(def installation-list + {:padding-horizontal 16 + :flex 1}) + +(def edit-installation + {:padding-top 10 + :padding-horizontal 16}) + +(def footer-content + {:justify-content :center + :flex 1 + :align-items :center}) + +(def footer-text + {:color colors/blue + :text-align :center}) + +(def pair-this-device + {:height 80 + :padding-horizontal 16 + :padding-top 12}) + +(def info-section + {:padding-horizontal 16 + :padding-top 12}) + +(def info-section-text + {:color colors/blue}) + +(def pair-this-device-actions + {:flex 1 + :flex-direction :row}) + +(defn pairing-button + [enabled?] + {:width 40 + :height 40 + :background-color (if enabled? + colors/blue-light + colors/gray-lighter) + :border-radius 28 + :align-items :center + :justify-content :center}) + +(def pairing-actions-text + {:flex 1 + :margin-left 16}) + +(def pair-this-device-title + {:color colors/blue + :margin-bottom 6}) + +(styles/defn pairing-button-icon + [enabled?] + (let [color (if enabled? + colors/blue + colors/gray)] + {:ios {:color color} + :android {:color color}})) + +(def paired-devices-title + {:color colors/gray + :margin-vertical 10}) + +(def bottom-container + {:flex-direction :row + :margin-horizontal 12 + :margin-vertical 15}) diff --git a/src/legacy/status_im/ui/screens/pairing/views.cljs b/src/legacy/status_im/ui/screens/pairing/views.cljs new file mode 100644 index 00000000000..5614f6c94b6 --- /dev/null +++ b/src/legacy/status_im/ui/screens/pairing/views.cljs @@ -0,0 +1,165 @@ +(ns legacy.status-im.ui.screens.pairing.views + (:require-macros [legacy.status-im.utils.views :as views]) + (:require + [clojure.string :as string] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.list.item :as list.item] + [legacy.status-im.ui.components.list.views :as list] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.screens.pairing.styles :as styles] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [utils.i18n :as i18n])) + +(def syncing (reagent/atom false)) +(def installation-name (reagent/atom "")) + +(defn icon-style + [style] + style) + +(defn synchronize-installations! + [] + (reset! syncing true) + ;; Currently we don't know how long it takes, so we just disable for 10s, to avoid spamming + (js/setTimeout #(reset! syncing false) 10000) + (re-frame/dispatch [:pairing.ui/synchronize-installation-pressed])) + +(defn pair! + [] + (re-frame/dispatch [:pairing.ui/pair-devices-pressed])) + +(defn enable-installation! + [installation-id] + (re-frame/dispatch [:pairing.ui/enable-installation-pressed installation-id])) + +(defn disable-installation! + [installation-id] + (re-frame/dispatch [:pairing.ui/disable-installation-pressed installation-id])) + +(defn toggle-enabled! + [installation-id enabled? _] + (if enabled? + (disable-installation! installation-id) + (enable-installation! installation-id))) + +(defn footer + [] + [react/touchable-highlight + {:on-press (when-not @syncing + synchronize-installations!) + :style {:height 52}} + [react/view + {:style styles/footer-content} + [react/text + {:style styles/footer-text} + (if @syncing + (i18n/label :t/syncing-devices) + (i18n/label :t/sync-all-devices))]]]) + +(defn pair-this-device + [] + [react/touchable-highlight + {:on-press pair! + :style styles/pair-this-device} + [react/view {:style styles/pair-this-device-actions} + [react/view + [react/view (styles/pairing-button true) + [icons/icon :main-icons/add (icon-style (styles/pairing-button-icon true))]]] + [react/view {:style styles/pairing-actions-text} + [react/view + [react/text + {:style styles/pair-this-device-title + :accessibility-label :advertise-device} (i18n/label :t/pair-this-device)]] + [react/view + [react/text (i18n/label :t/pair-this-device-description)]]]]]) + +(defn your-device + [{:keys [installation-id name device-type]}] + [list.item/list-item + {:icon (if (= "desktop" + device-type) + :main-icons/desktop + :main-icons/mobile) + :title (str name " (" (i18n/label :t/you) ", " (subs installation-id 0 5) ")")}]) + +(defn render-row + [{:keys [name + enabled? + device-type + installation-id]}] + [list.item/list-item + {:icon (if (= "desktop" device-type) + :main-icons/desktop + :main-icons/mobile) + :title (str (if (string/blank? name) + (i18n/label :t/pairing-no-info) + name) + " (" + (subs installation-id 0 5) + ")") + :accessory :checkbox + :active enabled? + :on-press (partial toggle-enabled! installation-id enabled?)}]) + +(defn render-rows + [installations] + [react/scroll-view {:style styles/wrapper} + [your-device (first installations)] + (when (seq (rest installations)) + [list/flat-list + {:data (rest installations) + :default-separator? false + :key-fn :installation-id + :render-fn render-row}])]) + +(views/defview edit-installation-name + [] + [react/keyboard-avoiding-view styles/edit-installation + [react/scroll-view {:keyboard-should-persist-taps :handled} + [react/view + [quo/text-input + {:placeholder (i18n/label :t/specify-name) + :label (i18n/label :t/pairing-please-set-a-name) + :accessibility-label :device-name + :default-value @installation-name + :on-change-text #(reset! installation-name %) + :auto-focus true}]]] + [react/view styles/bottom-container + [react/view {:flex 1}] + [quo/button + {:type :secondary + :after :main-icon/next + :disabled (string/blank? @installation-name) + :on-press #(do + (re-frame/dispatch [:pairing.ui/set-name-pressed @installation-name]) + (reset! installation-name ""))} + (i18n/label :t/continue)]]]) + +(defn info-section + [] + [react/view {:style styles/info-section} + [react/touchable-highlight + {:on-press #(.openURL ^js react/linking "https://status.im/user_guides/pairing_devices.html")} + [react/text {:style styles/info-section-text} (i18n/label :t/learn-more)]]]) + +(defn installations-list + [installations] + [react/view {:style styles/installation-list} + [react/view {:style styles/paired-devices-title} + [react/text (i18n/label :t/paired-devices)]] + (render-rows installations)]) + +(views/defview installations + [] + (views/letsubs [installs [:pairing/installations]] + [:<> + [react/scroll-view + (if (string/blank? (-> installs first :name)) + [edit-installation-name] + [react/view + [pair-this-device] + [info-section] + [installations-list installs]])] + (when (seq installs) [footer])])) diff --git a/src/legacy/status_im/ui/screens/popover/views.cljs b/src/legacy/status_im/ui/screens/popover/views.cljs new file mode 100644 index 00000000000..b553d03f1ae --- /dev/null +++ b/src/legacy/status_im/ui/screens/popover/views.cljs @@ -0,0 +1,139 @@ +(ns legacy.status-im.ui.screens.popover.views + (:require-macros [legacy.status-im.utils.views :as views]) + (:require + ["react-native" :refer (BackHandler)] + [legacy.status-im.ui.components.animation :as anim] + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.screens.reset-password.views :as reset-password.views] + [re-frame.core :as re-frame] + [react-native.platform :as platform] + [reagent.core :as reagent])) + +(defn hide-panel-anim + [bottom-anim-value alpha-value window-height] + (anim/start + (anim/parallel + [(anim/timing bottom-anim-value + {:toValue (- window-height) + :duration 300 + :useNativeDriver true}) + (anim/timing alpha-value + {:toValue 0 + :duration 300 + :useNativeDriver true})]))) + +(defn show-panel-anim + [bottom-anim-value alpha-value] + (anim/start + (anim/parallel + [(anim/timing bottom-anim-value + {:toValue 0 + :duration 300 + :useNativeDriver true}) + (anim/timing alpha-value + {:toValue 0.4 + :duration 300 + :useNativeDriver true})]))) + +(defn popover-view + [_ window-height] + (let [bottom-anim-value (anim/create-value window-height) + alpha-value (anim/create-value 0) + clear-timeout (atom nil) + current-popover (reagent/atom nil) + update? (reagent/atom nil) + request-close (fn [] + (when-not (:prevent-closing? @current-popover) + (reset! clear-timeout + (js/setTimeout + #(do (reset! current-popover nil) + (re-frame/dispatch [:hide-popover])) + 300)) + (hide-panel-anim + bottom-anim-value + alpha-value + (- window-height))) + true) + on-show (fn [] + (show-panel-anim bottom-anim-value alpha-value) + (when platform/android? + (.removeEventListener BackHandler + "hardwareBackPress" + request-close) + (.addEventListener BackHandler + "hardwareBackPress" + request-close))) + on-hide (fn [] + (when platform/android? + (.removeEventListener BackHandler + "hardwareBackPress" + request-close)))] + (reagent/create-class + {:UNSAFE_componentWillUpdate + (fn [_ [_ popover _]] + (when @clear-timeout (js/clearTimeout @clear-timeout)) + (cond + @update? + (do (reset! update? false) + (on-show)) + + (and @current-popover popover) + (do (reset! update? true) + (js/setTimeout #(reset! current-popover popover) 600) + (hide-panel-anim bottom-anim-value alpha-value (- window-height))) + + popover + (do (reset! current-popover popover) + (on-show)) + + :else + (do (reset! current-popover nil) + (on-hide)))) + :component-will-unmount on-hide + :reagent-render + (fn [] + (when @current-popover + (let [{:keys [view style disable-touchable-overlay? blur-view? blur-view-props]} + @current-popover + component (if blur-view? react/blur-view react/view) + overlay-component (if disable-touchable-overlay? react/view react/touchable-highlight)] + [component + (merge {:style {:position :absolute :top 0 :bottom 0 :left 0 :right 0}} blur-view-props) + (when platform/ios? + [react/animated-view + {:style {:flex 1 :background-color colors/black-persist :opacity alpha-value}}]) + [react/animated-view + {:style + {:position :absolute + :height window-height + :left 0 + :right 0 + :transform [{:translateY bottom-anim-value}]}} + [overlay-component + {:style {:flex 1 :align-items :center :justify-content :center} + :on-press request-close} + [react/view + (merge {:background-color (if blur-view? :transparent colors/white) + :border-radius 16 + :margin 32 + :shadow-offset {:width 0 :height 2} + :shadow-radius 8 + :shadow-opacity 1 + :shadow-color "rgba(0, 9, 26, 0.12)"} + style) + (cond + (vector? view) + view + + (= :password-reset-popover view) + [reset-password.views/reset-password-popover] + + :else + [view])]]]])))}))) + +(views/defview popover + [] + (views/letsubs [current-popover [:popover/popover] + {window-height :height} [:dimensions/window]] + [popover-view current-popover window-height])) diff --git a/src/legacy/status_im/ui/screens/profile/components/styles.cljs b/src/legacy/status_im/ui/screens/profile/components/styles.cljs new file mode 100644 index 00000000000..83856f85c13 --- /dev/null +++ b/src/legacy/status_im/ui/screens/profile/components/styles.cljs @@ -0,0 +1,21 @@ +(ns legacy.status-im.ui.screens.profile.components.styles) + +;; profile header elements + +;; settings items elements + +(def settings-item + {:padding-left 16 + :padding-right 8 + :flex 1 + :flex-direction :row + :align-items :center + :height 52}) + +(def settings-item-text-wrapper + {:flex 1 + :flex-direction :row + :justify-content :space-between}) + +(def settings-item-text + {:flex-wrap :nowrap}) diff --git a/src/legacy/status_im/ui/screens/profile/components/views.cljs b/src/legacy/status_im/ui/screens/profile/components/views.cljs new file mode 100644 index 00000000000..adec170fd45 --- /dev/null +++ b/src/legacy/status_im/ui/screens/profile/components/views.cljs @@ -0,0 +1,19 @@ +(ns legacy.status-im.ui.screens.profile.components.views + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.screens.profile.components.styles :as styles])) + +;; settings items elements + +(defn settings-switch-item + [{:keys [label-kw value action-fn active?] + :or {active? true}}] + [react/view styles/settings-item + [react/view styles/settings-item-text-wrapper + [react/i18n-text {:style styles/settings-item-text :key label-kw}]] + [react/switch + {:track-color #js {:true colors/blue :false colors/gray-lighter} + :value (boolean value) + :on-value-change action-fn + :disabled (not active?)}]]) diff --git a/src/legacy/status_im/ui/screens/profile/contact/views.cljs b/src/legacy/status_im/ui/screens/profile/contact/views.cljs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/legacy/status_im/ui/screens/profile/group_chat/views.cljs b/src/legacy/status_im/ui/screens/profile/group_chat/views.cljs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/legacy/status_im/ui/screens/profile/user/views.cljs b/src/legacy/status_im/ui/screens/profile/user/views.cljs new file mode 100644 index 00000000000..0a4171b18b0 --- /dev/null +++ b/src/legacy/status_im/ui/screens/profile/user/views.cljs @@ -0,0 +1,31 @@ +(ns legacy.status-im.ui.screens.profile.user.views + (:require + [legacy.status-im.ui.components.core :as components] + [legacy.status-im.ui.components.list.item :as list.item] + [quo.context] + [quo.core :as quo] + [re-frame.core :as re-frame] + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn content + [] + [rn/scroll-view {:flex 1} + [components/list-header "Legacy settings"] + [list.item/list-item + {:icon :main-icons/mobile + :title (i18n/label :t/sync-settings) + :accessibility-label :sync-settings-button + :chevron true + :on-press #(re-frame/dispatch [:open-modal :screen/legacy-sync-settings])}]]) + +(defn legacy-settings + [] + [:<> + [quo/page-nav + {:type :no-title + :background :blur + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + [content]]) diff --git a/src/legacy/status_im/ui/screens/profile/visibility_status/styles.cljs b/src/legacy/status_im/ui/screens/profile/visibility_status/styles.cljs new file mode 100644 index 00000000000..7be5809c335 --- /dev/null +++ b/src/legacy/status_im/ui/screens/profile/visibility_status/styles.cljs @@ -0,0 +1,20 @@ +(ns legacy.status-im.ui.screens.profile.visibility-status.styles + (:require + [legacy.status-im.ui.components.colors :as colors] + [quo.foundations.colors :as quo.colors])) + +(defn visibility-status-dot + [{:keys [color size new-ui?]} theme] + (if new-ui? + {:background-color color + :width size + :height size + :border-radius (/ size 2) + :border-width 3.5 + :border-color (quo.colors/theme-colors quo.colors/white quo.colors/neutral-90 theme)} + {:background-color color + :width size + :height size + :border-radius (/ size 2) + :border-width 1 + :border-color colors/white})) diff --git a/src/legacy/status_im/ui/screens/profile/visibility_status/utils.cljs b/src/legacy/status_im/ui/screens/profile/visibility_status/utils.cljs new file mode 100644 index 00000000000..4143d1a3b86 --- /dev/null +++ b/src/legacy/status_im/ui/screens/profile/visibility_status/utils.cljs @@ -0,0 +1,92 @@ +(ns legacy.status-im.ui.screens.profile.visibility-status.utils + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.screens.profile.visibility-status.styles :as styles] + [quo.context] + [quo.foundations.colors :as quo.colors] + [status-im.constants :as constants] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +;; Specs: +;; :visibility-status-automatic +;; To Send - "visibility-status-automatic" status ping every 5 minutes +;; Display - Online for up to 5 minutes from the last clock, after that Offline +;; :visibility-status-always-online +;; To Send - "visibility-status-always-online" status ping every 5 minutes +;; Display - Online for up to 2 weeks from the last clock, after that Offline +;; :visibility-status-inactive +;; To Send - A single "visibility-status-inactive" status ping +;; Display - Offline forever +;; Note: Only send pings if the user interacted with the app in the last x minutes. +(def visibility-status-type-data + {constants/visibility-status-unknown + {:color colors/red + :title (i18n/label :t/error)} + constants/visibility-status-automatic + {:color quo.colors/success-50 + :title (i18n/label :t/status-automatic) + :subtitle (i18n/label :t/status-automatic-subtitle)} + constants/visibility-status-dnd + {:color colors/color-dnd + :title (i18n/label :t/status-dnd) + :subtitle (i18n/label :t/status-dnd-subtitle)} + constants/visibility-status-always-online + {:color quo.colors/success-50 + :title (i18n/label :t/status-always-online)} + constants/visibility-status-inactive + {:color colors/color-inactive + :title (i18n/label :t/status-inactive) + :subtitle (i18n/label :t/status-inactive-subtitle)}}) + +(defn calculate-real-status-type + [{:keys [status-type clock]}] + (let [status-lifespan (if (= status-type constants/visibility-status-automatic) + (datetime/minutes 5) + (datetime/weeks 2)) + status-expire-time (+ (datetime/to-ms clock) status-lifespan) + time-left (- status-expire-time (datetime/timestamp))] + (if (or (nil? status-type) + (and + (not= status-type constants/visibility-status-inactive) + (neg? time-left))) + constants/visibility-status-inactive + status-type))) + +(defn icon-dot-color + [{:keys [status-type] :or {status-type constants/visibility-status-inactive}}] + (:color (get visibility-status-type-data status-type))) + +(defn icon-dot-accessibility-label + [dot-color] + (if (= dot-color quo.colors/success-50) + :online-profile-photo-dot + :offline-profile-photo-dot)) + +(defn icon-dot-size + [container-size] + (/ container-size 2.4)) + +(defn icon-visibility-status-dot + [public-key container-size] + (let [status (rf/sub [:visibility-status-updates/visibility-status-update public-key]) + theme (quo.context/use-theme) + size (icon-dot-size container-size) + margin -2 + dot-color (icon-dot-color status) + new-ui? true] + (merge (styles/visibility-status-dot {:color dot-color + :size size + :new-ui? new-ui?} + theme) + {:bottom margin + :right margin + :position :absolute + :accessibility-label (icon-dot-accessibility-label dot-color)}))) + +(defn visibility-status-order + [public-key] + (let [status (rf/sub [:visibility-status-updates/visibility-status-update public-key]) + dot-color (icon-dot-color status)] + (if (= dot-color colors/color-online) 0 1))) diff --git a/src/legacy/status_im/ui/screens/progress/views.cljs b/src/legacy/status_im/ui/screens/progress/views.cljs new file mode 100644 index 00000000000..837c3eec13b --- /dev/null +++ b/src/legacy/status_im/ui/screens/progress/views.cljs @@ -0,0 +1,15 @@ +(ns legacy.status-im.ui.screens.progress.views + (:require-macros [legacy.status-im.utils.views :refer [defview]]) + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.react :as react])) + +;; a simple view with animated progress indicator in its center +(defview progress + [_] + [react/view + {:flex 1 + :align-items :center + :justify-content :center + :background-color colors/white} + [react/activity-indicator {:animating true}]]) diff --git a/src/legacy/status_im/ui/screens/reset_password/views.cljs b/src/legacy/status_im/ui/screens/reset_password/views.cljs new file mode 100644 index 00000000000..f74a8e96e31 --- /dev/null +++ b/src/legacy/status_im/ui/screens/reset_password/views.cljs @@ -0,0 +1,52 @@ +(ns legacy.status-im.ui.screens.reset-password.views + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.components.core :as quo] + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.react :as react] + [re-frame.core :as re-frame] + [utils.i18n :as i18n]) + (:require-macros [legacy.status-im.utils.views :refer [defview letsubs]])) + +(defview reset-password-popover + [] + (letsubs [{:keys [resetting?]} [:multiaccount/reset-password-form-vals-and-errors]] + [react/view + {:padding-vertical 24 + :padding-horizontal 48 + :align-items :center} + [react/view + {:width 32 + :height 32 + :background-color (if resetting? + colors/gray-lighter + colors/green-transparent-10) + :border-radius 32 + :align-items :center + :justify-content :center} + (if resetting? + [react/activity-indicator + {:size :small + :animating true}] + [icons/icon :main-icons/check {:color colors/green}])] + [quo/text + {:size :x-large + :weight :bold + :align :center + :style {:typography :title-bold + :margin-top 16 + :margin-bottom 24}} + (i18n/label (if resetting? + :t/password-reset-in-progress + :t/password-reset-success))] + (when-not resetting? + [quo/text + {:align :center + :color :secondary + :style {:margin-bottom 24}} + (i18n/label :t/password-reset-success-message)]) + [react/view {:align-items :center} + [quo/button + {:on-press #(re-frame/dispatch [:profile/logout]) + :disabled resetting?} + (i18n/label :t/okay)]]])) diff --git a/src/legacy/status_im/ui/screens/rpc_usage_info.cljs b/src/legacy/status_im/ui/screens/rpc_usage_info.cljs new file mode 100644 index 00000000000..17f17e85533 --- /dev/null +++ b/src/legacy/status_im/ui/screens/rpc_usage_info.cljs @@ -0,0 +1,165 @@ +(ns legacy.status-im.ui.screens.rpc-usage-info + (:require + [clojure.string :as string] + [legacy.status-im.ui.components.core :as legacy.status-im.ui.components.core] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.components.typography :as typography] + [legacy.status-im.utils.utils :as utils] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [status-im.common.json-rpc.events :as json-rpc] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(re-frame/reg-sub :rpc-usage/raw-data (fn [db] (get db :rpc-usage/data))) +(re-frame/reg-sub :rpc-usage/filter (fn [db] (get db :rpc-usage/filter))) + +(re-frame/reg-sub + :rpc-usage/data + :<- [:rpc-usage/raw-data] + :<- [:rpc-usage/filter] + (fn [[{total :total rpc-methods :methods} method-filter]] + (let [data + (->> rpc-methods + (map (fn [[k v]] + [(name k) v])) + (filter (fn [[k]] + (string/includes? k method-filter))) + (sort-by second >)) + filtered-total (reduce + (map second data))] + {:stats data + :filtered-total filtered-total + :total (or total 0)}))) + +(re-frame/reg-fx + ::get-stats + (fn [] + (json-rpc/call + {:method "rpcstats_getStats" + :params [] + :on-success #(re-frame/dispatch [::handle-stats %])}))) + +(re-frame/reg-fx + ::reset + (fn [] + (json-rpc/call + {:method "rpcstats_reset" + :params [] + :on-success #(log/debug "rpcstats_reset success")}))) + +;; RPC refresh interval ID +(defonce rpc-refresh-interval (atom nil)) + +;; RPC usage refresh interval (ms) +(def rpc-usage-refresh-interval-ms 2000) + +(rf/defn handle-stats + {:events [::handle-stats]} + [{:keys [db]} data] + {:db (assoc db :rpc-usage/data data)}) + +(rf/defn get-stats + {:events [::get-stats]} + [{:keys [db]}] + (let [method-filter (get db :rpc-usage/filter "eth_")] + {:db (assoc db :rpc-usage/filter method-filter) + ::get-stats nil})) + +(rf/defn reset + {:events [::reset]} + [{:keys [db]}] + {:db (dissoc db :rpc-usage/data) + ::reset nil}) + +(rf/defn copy + {:events [::copy]} + [{:keys [db]}] + {:db (dissoc db :rpc-usage/data) + ::reset nil}) + +(rf/defn set-filter + {:events [::set-filter]} + [{:keys [db]} method-filter] + {:db (assoc db :rpc-usage/filter method-filter)}) + +(defn stats-table + [{:keys [total filtered-total stats]}] + [react/scroll-view + {:style {:padding-horizontal 8}} + [react/view + [react/view + {:style {:flex-direction :row + :justify-content :space-between + :margin-bottom 2}} + [legacy.status-im.ui.components.core/text {:style typography/font-semi-bold} + (i18n/label :t/rpc-usage-total)] + [legacy.status-im.ui.components.core/text {:style typography/font-semi-bold} + (i18n/label :t/rpc-usage-filtered-total {:filtered-total filtered-total :total total})]] + [react/view + {:style {:flex-direction :column + :flex 1}} + (when (seq stats) + (for [[k v] stats] + ^{:key (str k v)} + [react/view + {:style {:flex-direction :row + :align-items :stretch}} + [legacy.status-im.ui.components.core/text {:style {:flex 7}} k] + [legacy.status-im.ui.components.core/text {:style {:flex 1}} v]]))]]]) + +(defn prepare-stats + [{:keys [stats]}] + (string/join + "\n" + (map (fn [[k v]] + (str k " " v)) + stats))) + +(defn usage-info-render + [] + (let [stats @(re-frame/subscribe [:rpc-usage/data]) + methods-filter @(re-frame/subscribe [:rpc-usage/filter])] + [react/view + {:flex 1 + :margin-horizontal 8} + [react/view + {:style {:flex-direction :column + :justify-content :space-between}} + [react/view + {:style {:flex-direction :row + :justify-content :space-between}} + [legacy.status-im.ui.components.core/button + {:on-press #(re-frame/dispatch [::reset]) + :accessibility-label :rpc-usage-reset} + (i18n/label :t/rpc-usage-reset)] + [legacy.status-im.ui.components.core/button + {:on-press + #(react/copy-to-clipboard (prepare-stats stats)) + :accessibility-label :rpc-usage-copy} + (i18n/label :t/rpc-usage-copy)]] + [legacy.status-im.ui.components.core/text-input + {:on-change-text #(re-frame/dispatch [::set-filter %]) + :label (i18n/label :t/rpc-usage-filter-methods) + :placeholder (i18n/label :t/rpc-usage-filter) + :container-style {:margin-vertical 18} + :before {:icon :main-icons/search + :style {:padding-horizontal 8}} + :default-value methods-filter + :auto-capitalize :none + :show-cancel false + :auto-focus false}]] + [stats-table stats]])) + +(defn usage-info + [] + (reagent/create-class + {:component-did-mount + (fn [] + (reset! rpc-refresh-interval + (utils/set-interval #(re-frame/dispatch [::get-stats]) rpc-usage-refresh-interval-ms))) + + :component-will-unmount (fn [] + (utils/clear-interval @rpc-refresh-interval) + (reset! rpc-refresh-interval nil)) + :reagent-render usage-info-render})) diff --git a/src/legacy/status_im/ui/screens/screens.cljs b/src/legacy/status_im/ui/screens/screens.cljs new file mode 100644 index 00000000000..585ce0171f8 --- /dev/null +++ b/src/legacy/status_im/ui/screens/screens.cljs @@ -0,0 +1,73 @@ +(ns legacy.status-im.ui.screens.screens + (:require + [legacy.status-im.ui.components.colors :as colors] + [legacy.status-im.ui.screens.appearance.views :as appearance] + [legacy.status-im.ui.screens.bug-report :as bug-report] + [legacy.status-im.ui.screens.communities.members :as members] + [legacy.status-im.ui.screens.help-center.views :as help-center] + [legacy.status-im.ui.screens.pairing.views :as pairing] + [legacy.status-im.ui.screens.profile.user.views :as profile.user] + [legacy.status-im.ui.screens.progress.views :as progress] + [legacy.status-im.ui.screens.rpc-usage-info :as rpc-usage-info] + [react-native.platform :as platform] + [utils.i18n :as i18n])) + +(defn topbar-options + [title] + {:elevation 0 + :title {:color (if (colors/dark?) colors/white colors/black) + :text (i18n/label title)} + :rightButtonColor (if (colors/dark?) colors/white colors/black) + :background {:color (if (colors/dark?) colors/black colors/white)} + :backButton {:color (if (colors/dark?) colors/white colors/black) + :id :legacy-back-button + :testID :back-button + :visible true + :popStackOnPress false}}) + +(defn screens + [] + [;;PROGRESS + {:name :screen/progress + :options {:insets {:top? true} + :theme :dark} + :component progress/progress} + + ;;COMMUNITY + {:name :screen/legacy-community-members + :options {:insets {:top? true}} + :component members/view} + + + ;;SETTINGS + {:name :screen/legacy-appearance + :options {:topBar {:visible false} + :insets {:top? platform/android?}} + :component appearance/appearance-view} + + ;; LEGACY SETTINGS + {:name :screen/legacy-settings + :options {:topBar {:visible false} + :insets {:top? platform/android?}} + :component profile.user/legacy-settings} + + ;; STATUS HELP + {:name :screen/help-center + :options {:topBar {:visible false} + :insets {:top? platform/android?}} + :component help-center/help-center} + + {:name :screen/bug-report + :options {:topBar {:visible false} + :insets {:top? platform/android?}} + :component bug-report/bug-report} + + ;; OTHER + {:name :screen/installations + :options {:topBar (topbar-options (i18n/label :t/devices)) + :insets {:top? true}} + :component pairing/installations} + {:name :screen/rpc-usage-info + :options {:topBar (topbar-options :t/rpc-usage-info) + :insets {:top? true}} + :component rpc-usage-info/usage-info}]) diff --git a/src/legacy/status_im/utils/async.cljs b/src/legacy/status_im/utils/async.cljs new file mode 100644 index 00000000000..a4ac0473a1e --- /dev/null +++ b/src/legacy/status_im/utils/async.cljs @@ -0,0 +1,90 @@ +(ns legacy.status-im.utils.async + "Utility namespace containing `core.async` helper constructs" + (:require + [cljs.core.async :as async] + [legacy.status-im.utils.utils :as utils] + [taoensso.timbre :as log])) + +(defn timeout + [ms] + (let [c (async/chan)] + (utils/set-timeout (fn [] (async/close! c)) ms) + c)) + +;; This wrapping is required as core.async macro replaces tries and catch with +;; https://github.com/clojure/core.async/blob/18d2f903b169c681ed008dd9545dc33458604b89/src/main/clojure/cljs/core/async/impl/ioc_helpers.cljs#L74 +;; and this does not seem to play nice with desktop, and the error is bubble up killing the go-loop +(defn chunked-pipe! + "Connects input channel to the output channel with time-based chunking. + `flush-time` parameter decides for how long we are waiting to accumulate + value from input channel in a vector before it's put on the output channel. + When `flush-time` interval elapses and there are no values accumulated, nothing + is put on the output channel till the next input arrives, which is then put on + the output channel immediately (wrapped in a vector). + When input channel is closed, output channel is closed as well and go-loop exits." + [input-ch output-ch flush-time] + (async/go-loop [acc [] + flush? false] + (if flush? + (do (async/put! output-ch acc) + (recur [] false)) + (let [[v ch] (async/alts! [input-ch (timeout flush-time)])] + (if (= ch input-ch) + (if v + (recur (conj acc v) (and (seq acc) flush?)) + (async/close! output-ch)) + (recur acc (seq acc))))))) + +;; --------------------------------------------------------------------------- +;; Periodic background job +;; --------------------------------------------------------------------------- + +(defn async-periodic-stop! + [async-periodic-chan] + (async/close! async-periodic-chan)) + +(defn async-periodic-exec + "Periodically execute an function. + + Takes a work-fn of one argument `finished-fn -> any` this function + is passed a finished-fn that must be called to signal that the work + being performed in the work-fn is finished. + + Returns a go channel that represents a way to control the looping process. + + Stop the polling loop with `async-periodic-stop!` + + The work-fn can be forced to run immediately with `async-periodic-run!` + + Or you can queue up another fn `finished-fn -> any` to execute on + the queue with `async-periodic-run!`." + [work-fn interval-ms timeout-ms] + {:pre [(fn? work-fn) (integer? interval-ms) (integer? timeout-ms)]} + (let [do-now-chan (async/chan (async/sliding-buffer 1)) + try-it (fn [exec-fn catch-fn] (try (exec-fn) (catch :default e (catch-fn e))))] + (async/go-loop [] + (let [timeout-chan (timeout interval-ms) + finished-chan (async/promise-chan) + [v ch] (async/alts! [do-now-chan timeout-chan]) + worker (if (and (= ch do-now-chan) (fn? v)) + v + work-fn)] + (when-not (and (= ch do-now-chan) (nil? v)) + ;; don't let try catch be parsed by go-block + (try-it #(worker (fn [] (async/put! finished-chan true))) + (fn [e] + (log/error "failed to run job" e) + ;; if an error occurs in work-fn log it and consider it done + (async/put! finished-chan true))) + ;; sanity timeout for work-fn + (async/alts! [finished-chan (timeout timeout-ms)]) + (recur)))) + do-now-chan)) + +(comment + (def c (atom nil)) + + (let [periodic-task-chan (async-periodic-exec #(prn :task) 5000 1000)] + (reset! c periodic-task-chan)) + + (async-periodic-stop! @c)) diff --git a/src/legacy/status_im/utils/async_test.cljs b/src/legacy/status_im/utils/async_test.cljs new file mode 100644 index 00000000000..cf2f4bb3539 --- /dev/null +++ b/src/legacy/status_im/utils/async_test.cljs @@ -0,0 +1,36 @@ +(ns legacy.status-im.utils.async-test + (:require + [cljs.core.async :as async] + [cljs.test :refer-macros [deftest is testing async]] + [legacy.status-im.utils.async :as async-util])) + +(deftest chunking-test + (testing "Accumulating result works as expected for `chunked-pipe!`" + (let [input (async/chan) + output (async/chan)] + (async-util/chunked-pipe! input output 100) + (async done + (async/go + (async/put! input 1) + (async/put! input 2) + (async/put! input 3) + (async/clj]) + (:require + [cljs-bean.core :as clj-bean])) + +;; NOTE(19/12/22 yqrashawn) this namespace has been moved to the utils.transforms namespace, +;; we keep this only for old (status 1.0) code, can be removed with old code later + +(defn js->clj + [data] + (cljs.core/js->clj data :keywordize-keys true)) + +(defn clj->pretty-json + [data spaces] + (.stringify js/JSON (clj-bean/->js data) nil spaces)) + +(defn clj->json + [data] + (clj->pretty-json data 0)) + +(defn json->clj + [json] + (when-not (= json "undefined") + (try + (js->clj (.parse js/JSON json)) + (catch js/Error _ + (when (string? json) json))))) + diff --git a/src/legacy/status_im/utils/dimensions.cljs b/src/legacy/status_im/utils/dimensions.cljs new file mode 100644 index 00000000000..d49a834716c --- /dev/null +++ b/src/legacy/status_im/utils/dimensions.cljs @@ -0,0 +1,11 @@ +(ns legacy.status-im.utils.dimensions + (:require + [legacy.status-im.ui.components.react :as react])) + +(defn window + ([] + (react/get-dimensions "window")) + ([m] + (-> m + (js->clj :keywordize-keys true) + :window))) diff --git a/src/legacy/status_im/utils/js_resources.cljs b/src/legacy/status_im/utils/js_resources.cljs new file mode 100644 index 00000000000..e6cad669a96 --- /dev/null +++ b/src/legacy/status_im/utils/js_resources.cljs @@ -0,0 +1,13 @@ +(ns legacy.status-im.utils.js-resources + (:require-macros [legacy.status-im.utils.slurp :refer [slurp]]) + (:require + [status-im.config :as config])) + +(def provider-file (slurp "resources/js/provider.js")) +(defn ethereum-provider + [network-id] + (str "window.statusAppNetworkId = \"" + network-id + "\";" + (when config/debug-webview? "window.statusAppDebug = true;") + provider-file)) diff --git a/src/legacy/status_im/utils/logging/core.cljs b/src/legacy/status_im/utils/logging/core.cljs new file mode 100644 index 00000000000..cc46fd080a8 --- /dev/null +++ b/src/legacy/status_im/utils/logging/core.cljs @@ -0,0 +1,285 @@ +(ns legacy.status-im.utils.logging.core + (:require + [clojure.string :as string] + [goog.string :as gstring] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.utils.deprecated-types :as types] + [legacy.status-im.utils.logging.view :as view] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [react-native.mmkv :as mmkv] + [react-native.platform :as platform] + [status-im.app-build.core :as build] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.common.log :as common-log] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(re-frame/reg-fx + :logs/archive-logs + (fn [[db-json callback-handler]] + (native-module/send-logs + db-json + (string/join "\n" (common-log/get-logs-queue)) + config/use-public-log-dir? + #(re-frame/dispatch [callback-handler %])))) + +(rf/defn store-web3-client-version + {:events [:logging/store-web3-client-version]} + [{:keys [db]} node-version] + {:db (assoc db :web3-node-version node-version)}) + +(re-frame/reg-fx :logging/initialize-web3-client-version + (fn [] + (json-rpc/call {:method "web3_clientVersion" + :on-success [:logging/store-web3-client-version]}))) + +(defn extract-url-components + [address] + (when address + (rest (re-matches #"enode://(.*?)@(.*):(.*)" address)))) + +(defn email-body + "logs attached" + [{:keys [:web3-node-version :mailserver/current-id + :node-info :peers-summary :bug-report/details]}] + (let [build-number build/build-no + build-version (str build/version " (" build-number ")") + separator (string/join (take 40 (repeat "-"))) + [enode-id ip-address port] + (extract-url-components (:enode node-info))] + (string/join + "\n" + (concat [(i18n/label :t/report-bug-email-template + {:description (:description details) + :steps (:steps details)})] + [separator + (str "App version: " build-version) + (str "OS: " platform/os) + (str "Node version: " web3-node-version) + (when current-id + (str "Mailserver: " (name current-id))) + separator + "Node Info" + (str "id: " enode-id) + (str "ip: " ip-address) + (str "port: " port) + separator + "Peers"] + (mapcat + (fn [{:keys [enode]}] + (let [[enode-id ip-address port] + (extract-url-components enode)] + [(str "id: " enode-id) + (str "ip: " ip-address) + (str "port: " port) + "\n"])) + peers-summary) + [separator + (datetime/timestamp->long-date + (datetime/now))])))) + +(rf/defn dialog-closed + {:events [:logging/dialog-left]} + [{:keys [db]}] + {:db (dissoc db :logging/dialog-shown?)}) + +(rf/defn send-email + [_ opts callback] + {:email/send [opts callback]}) + +(rf/defn send-email-event + {:events [::send-email]} + [{:keys [db] :as cofx} archive-uri] + (rf/merge + cofx + {:db (dissoc db :bug-report/details)} + (send-email + (cond-> {:subject "Error report" + :recipients [constants/report-email] + :body (email-body db)} + + (not (nil? archive-uri)) + (assoc :attachments + [{:uri archive-uri + :type "zip" + :name "status_logs.zip"}])) + (fn [event] + (when (= event "not_available") + (re-frame/dispatch [:show-client-error])))))) + +(defn logs-enabled? + [{:profile/keys [profile]}] + (let [log-level (if profile ;; already login + (get profile :log-level) + (config/log-level))] + (not (string/blank? log-level)))) + +(rf/defn trigger-archive-logs + {:events [:logging/trigger-archive-logs]} + [_ db-json callback-handler] + {:logs/archive-logs [db-json callback-handler]}) + +(rf/defn send-logs + {:events [:logging.ui/send-logs-pressed]} + [{:keys [db]} transport hide-bottom-sheet?] + (let [log-enabled? (logs-enabled? db) + db-json (when log-enabled? + (types/clj->json + (select-keys db + [:app-state + :current-chat-id + :network + :network/status + :peers-summary + :sync-state + :view-id + :chat/cooldown-enabled? + :chat/cooldowns + :chat/last-outgoing-message-sent-at + :chat/spam-messages-frequency + :dimensions/window])))] + {:fx [(when hide-bottom-sheet? [:dispatch [:hide-bottom-sheet]]) + (if log-enabled? + [:dispatch-later + {:ms 1000 ;; wait for hide-bottom-sheet to be processed, otherwise we won't see + ;; the share dialog on iOS + :dispatch [:logging/trigger-archive-logs + db-json + (if (= transport :email) ::send-email ::share-logs-file)]}] + [:dispatch [::send-email nil]])]})) + +(rf/defn send-logs-on-error + {:events [:logging/send-logs-on-error]} + [{:keys [db]} error-message] + (rf/merge + {:db (assoc-in db [:bug-report/details :description] error-message)} + (send-logs :email false))) + +(rf/defn show-client-error + {:events [:show-client-error]} + [_] + {:effects.utils/show-popup {:title (i18n/label :t/cant-report-bug) + :content (i18n/label :t/mail-should-be-configured)}}) + +(rf/defn show-logs-dialog + {:events [:shake-event]} + [{:keys [db]}] + (when-not (:logging/dialog-shown? db) + {:db (assoc db :logging/dialog-shown? true) + :fx [[:dispatch [:show-bottom-sheet {:content view/logs-management-drawer}]] + [:dispatch-later + {:ms 2000 ;; process :shake-event after 2 seconds, use :logging/dialog-shown? to + ;; avoid handling :shake-event multiple times in a short time + :dispatch [:logging/dialog-left]}]]})) + +(re-frame/reg-fx + :email/send + (fn [[opts callback]] + (native-module/mail (clj->js opts) callback))) + +(re-frame/reg-fx + ::share-archive + (fn [{:keys [url] :as opts}] + (if platform/android? + (native-module/share-logs url (fn [error] (log/error "Error sharing logs" error))) + (.share ^js react/sharing (clj->js opts))))) + +(rf/defn share-archive + [_ opts] + {::share-archive opts}) + +(rf/defn share-logs-file + {:events [::share-logs-file]} + [cofx archive-uri] + (rf/merge + cofx + (share-archive + {:title "Archived logs" + :url archive-uri}))) + +(rf/defn details + {:events [:logging/report-details]} + [{:keys [db]} log-key value] + {:db (-> db + (assoc-in [:bug-report/details log-key] value) + (dissoc :bug-report/description-error))}) + +(def min-description-lenght 6) + +(defn validate-description + [db] + (let [description (get-in db [:bug-report/details :description] "")] + (when (> min-description-lenght + (count (string/trim description))) + (i18n/label :t/bug-report-too-short-description)))) + +(rf/defn submit-report + {:events [:logging/submit-report]} + [{:keys [db] :as cofx}] + (if-let [error (validate-description db)] + {:db (assoc db :bug-report/description-error error)} + (rf/merge + cofx + (navigation/hide-bottom-sheet) + (send-logs :email false)))) + +(re-frame/reg-fx + ::open-url + (fn [url] + (.openURL ^js react/linking url))) + +(def gh-issue-url "https://github.com/status-im/status-mobile/issues/new?labels=bug&title=%s&body=%s") + +(rf/defn submit-issue + [{:keys [db]}] + (let [{:keys [steps description]} + (get db :bug-report/details) + + title (or description (i18n/label :t/bug-report-description-placeholder)) + body (str title + "\n\n" + (or steps (i18n/label :t/bug-report-steps-placeholder))) + url (gstring/format gh-issue-url (js/escape title) (js/escape body))] + {::open-url url})) + +(rf/defn submit-gh-issue + {:events [:logging/submit-gh-issue]} + [{:keys [db] :as cofx}] + (rf/merge + cofx + {:db (dissoc db :bug-report/details)} + (navigation/hide-bottom-sheet) + (submit-issue))) + +(rf/defn change-pre-login-log-level + {:events [:log-level.ui/change-pre-login-log-level]} + [{:keys [db]} log-level] + (let [old-log-level (get-in db [:log-level/pre-login-log-level])] + (when (not= old-log-level log-level) + (let [need-set-pre-login-log-enabled? (or (empty? old-log-level) (empty? log-level)) + pre-login-log-enabled? (boolean (seq log-level))] + {:fx [[:log-level/set-pre-login-log-level log-level] + (when need-set-pre-login-log-enabled? + [:log-level/set-pre-login-log-enabled pre-login-log-enabled?]) + ;; update log level in taoensso.timbre + [:logs/set-level log-level] + [:dispatch [:hide-bottom-sheet]]] + :db (assoc db :log-level/pre-login-log-level log-level)})))) + +(rf/reg-fx + :log-level/set-pre-login-log-level + (fn [log-level] + (mmkv/set constants/pre-login-log-level-key log-level) + (when (seq log-level) + (native-module/set-pre-login-log-level log-level)))) + +(rf/reg-fx + :log-level/set-pre-login-log-enabled + (fn [enabled?] + (native-module/set-pre-login-log-enabled enabled?))) diff --git a/src/legacy/status_im/utils/logging/view.cljs b/src/legacy/status_im/utils/logging/view.cljs new file mode 100644 index 00000000000..5e50a79a478 --- /dev/null +++ b/src/legacy/status_im/utils/logging/view.cljs @@ -0,0 +1,79 @@ +(ns legacy.status-im.utils.logging.view + (:require + [legacy.status-im.ui.components.icons.icons :as icons] + [legacy.status-im.ui.components.list.views :as list] + [legacy.status-im.ui.components.react :as react] + [legacy.status-im.ui.screens.log-level-settings.styles :as styles] + [quo.context] + [quo.core :as quo] + [re-frame.core :as re-frame] + [status-im.config :as config] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf]) + (:require-macros [legacy.status-im.utils.views :as views])) + +(defn- log-level-icon + [current?] + [react/view (styles/log-level-icon-container current?) + [icons/icon :main-icons/mailserver + (styles/log-level-icon current?)]]) + +(defn change-log-level + [{:keys [value]}] + (re-frame/dispatch [:log-level.ui/change-pre-login-log-level value])) + +(defn render-row + [{:keys [name value] :as log-level} _ _ current-log-level] + (let [current? (= value current-log-level)] + [react/touchable-highlight + {:on-press #(change-log-level log-level) + :accessibility-label :log-level-item} + [react/view styles/log-level-item + [log-level-icon current?] + [react/view styles/log-level-item-inner + [react/text {:style styles/log-level-item-name-text} + name]]]])) + +(def log-levels + "The status-go log library (zap) doesn't support the trace level, so we remove + the trace option from the UI. When trace is enabled an error will happen while + trying to login (user will see the error 'wrong password')." + [{:name "DISABLED" + :value ""} + {:name "ERROR" + :value "ERROR"} + {:name "WARN" + :value "WARN"} + {:name "INFO" + :value "INFO"} + {:name "DEBUG" + :value "DEBUG"}]) + +(views/defview pre-login-log-level-settings + [] + (views/letsubs [current-log-level [:log-level/pre-login-log-level]] + [:<> + [list/flat-list + {:data log-levels + :default-separator? false + :key-fn :name + :render-data current-log-level + :render-fn render-row}]])) + +(views/defview logs-management-drawer + [] + (views/letsubs [logged-in? [:multiaccount/logged-in?]] + [quo/action-drawer + [[{:label (i18n/label :t/send-logs) + :sub-label (i18n/label :t/send-logs-to + {:email constants/report-email}) + :on-press #(rf/dispatch [:open-modal :bug-report])} + {:label (i18n/label :t/share-logs) + :on-press #(rf/dispatch [:logging.ui/send-logs-pressed :sharing true])} + (when-not logged-in? + {:label (i18n/label :t/set-pre-login-log-level) + :on-press #(rf/dispatch [:show-bottom-sheet {:content pre-login-log-level-settings}])}) + (when config/debug-or-pr-build? + {:label (i18n/label :t/feature-flags) + :on-press #(rf/dispatch [:open-modal :screen/feature-flags])})]]])) diff --git a/src/legacy/status_im/utils/mobile_sync.cljs b/src/legacy/status_im/utils/mobile_sync.cljs new file mode 100644 index 00000000000..66a7da1662b --- /dev/null +++ b/src/legacy/status_im/utils/mobile_sync.cljs @@ -0,0 +1,9 @@ +(ns legacy.status-im.utils.mobile-sync) + +(defn syncing-allowed? + [db] + (let [network (:network/type db) + {:keys [syncing-on-mobile-network?]} (:profile/profile db)] + (or (= network "wifi") + (and syncing-on-mobile-network? + (= network "cellular"))))) diff --git a/src/legacy/status_im/utils/random.cljs b/src/legacy/status_im/utils/random.cljs new file mode 100644 index 00000000000..35f7e71e06a --- /dev/null +++ b/src/legacy/status_im/utils/random.cljs @@ -0,0 +1,37 @@ +(ns legacy.status-im.utils.random + (:require + ["chance" :as Chance] + [re-frame.core :as re-frame] + [utils.datetime :as datetime])) + +(def chance (Chance.)) + +(defn guid + [] + (.guid ^js chance)) + +(defn id + [] + (str (datetime/timestamp) "-" (.guid ^js chance))) + +(defn rand-gen + [seed] + (Chance. seed)) + +(defn seeded-rand-int + [^js gen n] + (.integer ^js gen (clj->js {:min 0 :max (dec n)}))) + +(defn seeded-rand-nth + [gen coll] + (nth coll (seeded-rand-int gen (count coll)))) + +(re-frame/reg-cofx + :random-guid-generator + (fn [coeffects _] + (assoc coeffects :random-guid-generator guid))) + +(re-frame/reg-cofx + :random-id-generator + (fn [coeffects _] + (assoc coeffects :random-id-generator id))) diff --git a/src/legacy/status_im/utils/random_test.cljs b/src/legacy/status_im/utils/random_test.cljs new file mode 100644 index 00000000000..4f761f143de --- /dev/null +++ b/src/legacy/status_im/utils/random_test.cljs @@ -0,0 +1,91 @@ +(ns legacy.status-im.utils.random-test + (:require + [cljs.test :refer-macros [deftest is]] + [legacy.status-im.utils.random :as random])) + +(deftest seeded-rand-int-test + ;try with one seed + (let [seed 0 + gen (random/rand-gen seed)] + (is (= (random/seeded-rand-int gen 100) 54)) + (is (= (random/seeded-rand-int gen 100) 59)) + (is (= (random/seeded-rand-int gen 10) 7)) + (is (= (random/seeded-rand-int gen 10) 8))) + ;repeat with the same seed, but generator re-set + (let [seed 0 + gen (random/rand-gen seed)] + (is (= (random/seeded-rand-int gen 100) 54)) + (is (= (random/seeded-rand-int gen 100) 59)) + (is (= (random/seeded-rand-int gen 10) 7)) + (is (= (random/seeded-rand-int gen 10) 8))) + ;a string seed + (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" + gen (random/rand-gen seed)] + (is (= (random/seeded-rand-int gen 100) 7)) + (is (= (random/seeded-rand-int gen 100) 4)) + (is (= (random/seeded-rand-int gen 10) 5)) + (is (= (random/seeded-rand-int gen 10) 7))) + ;a string seed again + (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" + gen (random/rand-gen seed)] + (is (= (random/seeded-rand-int gen 100) 7)) + (is (= (random/seeded-rand-int gen 100) 4)) + (is (= (random/seeded-rand-int gen 10) 5)) + (is (= (random/seeded-rand-int gen 10) 7))) + ;nil seed is the same as 0 + (let [seed 0 + gen (random/rand-gen seed)] + (is (= (random/seeded-rand-int gen 100) 54)) + (is (= (random/seeded-rand-int gen 100) 59)) + (is (= (random/seeded-rand-int gen 10) 7)) + (is (= (random/seeded-rand-int gen 10) 8)))) + +(deftest seeded-rand-int-boundaries-test + (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" + n 10 + gen (random/rand-gen seed) + sample (into #{} (repeatedly 1000 #(random/seeded-rand-int gen n)))] + ;no result should be negative + (is (empty? (filter #(< % 0) sample))) + ;no result should be larger than n + (is (empty? (filter #(>= % n) sample))) + ;and while there is a very small probability it is very unlikely and probably wrong if all 1000 + ;calls got us the same number. + (is (> (count sample) 1)))) + +(deftest seeded-rand-nth-test + (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" + gen (random/rand-gen seed) + coll [:a :b :c :d :e :f :g]] + (is (= (random/seeded-rand-nth gen coll) :a)) + (is (= (random/seeded-rand-nth gen coll) :a)) + (is (= (random/seeded-rand-nth gen coll) :e)) + (is (= (random/seeded-rand-nth gen coll) :f)) + (is (= (random/seeded-rand-nth gen coll) :c))) + ;try again with the same seed but gen reset + (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" + gen (random/rand-gen seed) + coll [:a :b :c :d :e :f :g]] + (is (= (random/seeded-rand-nth gen coll) :a)) + (is (= (random/seeded-rand-nth gen coll) :a)) + (is (= (random/seeded-rand-nth gen coll) :e)) + (is (= (random/seeded-rand-nth gen coll) :f)) + (is (= (random/seeded-rand-nth gen coll) :c))) + ;try again with a different seed + (let [seed "57348975ff9199ca636207a396b915d6b6a675b4" + gen (random/rand-gen seed) + coll [:a :b :c :d :e :f :g]] + (is (= (random/seeded-rand-nth gen coll) :f)) + (is (= (random/seeded-rand-nth gen coll) :c)) + (is (= (random/seeded-rand-nth gen coll) :e)) + (is (= (random/seeded-rand-nth gen coll) :e)) + (is (= (random/seeded-rand-nth gen coll) :a))) + ;and re-set + (let [seed "57348975ff9199ca636207a396b915d6b6a675b4" + gen (random/rand-gen seed) + coll [:a :b :c :d :e :f :g]] + (is (= (random/seeded-rand-nth gen coll) :f)) + (is (= (random/seeded-rand-nth gen coll) :c)) + (is (= (random/seeded-rand-nth gen coll) :e)) + (is (= (random/seeded-rand-nth gen coll) :e)) + (is (= (random/seeded-rand-nth gen coll) :a)))) diff --git a/src/legacy/status_im/utils/signing_phrase/core.cljs b/src/legacy/status_im/utils/signing_phrase/core.cljs new file mode 100644 index 00000000000..b4b7d28f46b --- /dev/null +++ b/src/legacy/status_im/utils/signing_phrase/core.cljs @@ -0,0 +1,30 @@ +(ns legacy.status-im.utils.signing-phrase.core + (:require + [clojure.string :as string] + [legacy.status-im.utils.signing-phrase.dictionaries.en :as en])) + +; In order to reduce phishing threat for Status.im users we want to have them +; recognize 3 predefined words when they sign transactions or make other sensitive operations. +; +; Onboarding flow needs to generate the 3 words for the user and store it in the profile. +; In order to reduce phishing threat for users we want to have them recognize 3 +; predefined words when they sign transactions or make other sensitive operations. +; Onboarding flow needs to generate the 3 words for the user and store it in the profile. +; +; As a user, I want to accept 3 generated words when creating my account so that I can recognize these +; words when signing transactions +; and thus make it hard to create a phishing page and protect myself from phishing attack. +; +; See more info at: https://github.com/status-im/status-mobile/issues/1585 +; +; Currently only English is supported (as the default language), +; for more details see #https://github.com/status-im/status-mobile/issues/1679 + +(defn pick-words + [dictionary] + (repeatedly 3 #(rand-nth dictionary))) + +(defn generate + [] + (string/join " " (pick-words en/dictionary))) + diff --git a/src/legacy/status_im/utils/signing_phrase/core_test.cljs b/src/legacy/status_im/utils/signing_phrase/core_test.cljs new file mode 100644 index 00000000000..99631b9b644 --- /dev/null +++ b/src/legacy/status_im/utils/signing_phrase/core_test.cljs @@ -0,0 +1,12 @@ +(ns legacy.status-im.utils.signing-phrase.core-test + (:require + [cljs.test :refer-macros [deftest is]] + [clojure.string :as string] + [legacy.status-im.utils.signing-phrase.core :refer [generate]])) + +(deftest generate-test + (doseq [_ (range 30)] + (let [result (generate)] + (is (not (string/starts-with? result " "))) + (is (not (string/ends-with? result " "))) + (is (= (get (frequencies result) " ") 2))))) diff --git a/src/legacy/status_im/utils/signing_phrase/dictionaries/en.cljs b/src/legacy/status_im/utils/signing_phrase/dictionaries/en.cljs new file mode 100644 index 00000000000..803e6aac022 --- /dev/null +++ b/src/legacy/status_im/utils/signing_phrase/dictionaries/en.cljs @@ -0,0 +1,625 @@ +(ns legacy.status-im.utils.signing-phrase.dictionaries.en) + +(def dictionary + ["acid" + "alto" + "apse" + "arch" + "area" + "army" + "atom" + "aunt" + "babe" + "baby" + "back" + "bail" + "bait" + "bake" + "ball" + "band" + "bank" + "barn" + "base" + "bass" + "bath" + "bead" + "beak" + "beam" + "bean" + "bear" + "beat" + "beef" + "beer" + "beet" + "bell" + "belt" + "bend" + "bike" + "bill" + "bird" + "bite" + "blow" + "blue" + "boar" + "boat" + "body" + "bolt" + "bomb" + "bone" + "book" + "boot" + "bore" + "boss" + "bowl" + "brow" + "bulb" + "bull" + "burn" + "bush" + "bust" + "cafe" + "cake" + "calf" + "call" + "calm" + "camp" + "cane" + "cape" + "card" + "care" + "carp" + "cart" + "case" + "cash" + "cast" + "cave" + "cell" + "cent" + "chap" + "chef" + "chin" + "chip" + "chop" + "chub" + "chug" + "city" + "clam" + "clef" + "clip" + "club" + "clue" + "coal" + "coat" + "code" + "coil" + "coin" + "coke" + "cold" + "colt" + "comb" + "cone" + "cook" + "cope" + "copy" + "cord" + "cork" + "corn" + "cost" + "crab" + "craw" + "crew" + "crib" + "crop" + "crow" + "curl" + "cyst" + "dame" + "dare" + "dark" + "dart" + "dash" + "data" + "date" + "dead" + "deal" + "dear" + "debt" + "deck" + "deep" + "deer" + "desk" + "dhow" + "diet" + "dill" + "dime" + "dirt" + "dish" + "disk" + "dock" + "doll" + "door" + "dory" + "drag" + "draw" + "drop" + "drug" + "drum" + "duck" + "dump" + "dust" + "duty" + "ease" + "east" + "eave" + "eddy" + "edge" + "envy" + "epee" + "exam" + "exit" + "face" + "fact" + "fail" + "fall" + "fame" + "fang" + "farm" + "fawn" + "fear" + "feed" + "feel" + "feet" + "file" + "fill" + "film" + "find" + "fine" + "fire" + "fish" + "flag" + "flat" + "flax" + "flow" + "foam" + "fold" + "font" + "food" + "foot" + "fork" + "form" + "fort" + "fowl" + "frog" + "fuel" + "full" + "gain" + "gale" + "galn" + "game" + "garb" + "gate" + "gear" + "gene" + "gift" + "girl" + "give" + "glad" + "glen" + "glue" + "glut" + "goal" + "goat" + "gold" + "golf" + "gong" + "good" + "gown" + "grab" + "gram" + "gray" + "grey" + "grip" + "grit" + "gyro" + "hail" + "hair" + "half" + "hall" + "hand" + "hang" + "harm" + "harp" + "hate" + "hawk" + "head" + "heat" + "heel" + "hell" + "helo" + "help" + "hemp" + "herb" + "hide" + "high" + "hill" + "hire" + "hive" + "hold" + "hole" + "home" + "hood" + "hoof" + "hook" + "hope" + "hops" + "horn" + "hose" + "host" + "hour" + "hunt" + "hurt" + "icon" + "idea" + "inch" + "iris" + "iron" + "item" + "jail" + "jeep" + "jeff" + "joey" + "join" + "joke" + "judo" + "jump" + "junk" + "jury" + "jute" + "kale" + "keep" + "kick" + "kill" + "kilt" + "kind" + "king" + "kiss" + "kite" + "knee" + "knot" + "lace" + "lack" + "lady" + "lake" + "lamb" + "lamp" + "land" + "lark" + "lava" + "lawn" + "lead" + "leaf" + "leek" + "lier" + "life" + "lift" + "lily" + "limo" + "line" + "link" + "lion" + "lisa" + "list" + "load" + "loaf" + "loan" + "lock" + "loft" + "long" + "look" + "loss" + "lout" + "love" + "luck" + "lung" + "lute" + "lynx" + "lyre" + "maid" + "mail" + "main" + "make" + "male" + "mall" + "manx" + "many" + "mare" + "mark" + "mask" + "mass" + "mate" + "math" + "meal" + "meat" + "meet" + "menu" + "mess" + "mice" + "midi" + "mile" + "milk" + "mime" + "mind" + "mine" + "mini" + "mint" + "miss" + "mist" + "moat" + "mode" + "mole" + "mood" + "moon" + "most" + "moth" + "move" + "mule" + "mutt" + "nail" + "name" + "neat" + "neck" + "need" + "neon" + "nest" + "news" + "node" + "nose" + "note" + "oboe" + "okra" + "open" + "oval" + "oven" + "oxen" + "pace" + "pack" + "page" + "pail" + "pain" + "pair" + "palm" + "pard" + "park" + "part" + "pass" + "past" + "path" + "peak" + "pear" + "peen" + "peer" + "pelt" + "perp" + "pest" + "pick" + "pier" + "pike" + "pile" + "pimp" + "pine" + "ping" + "pink" + "pint" + "pipe" + "piss" + "pith" + "plan" + "play" + "plot" + "plow" + "poem" + "poet" + "pole" + "polo" + "pond" + "pony" + "poof" + "pool" + "port" + "post" + "prow" + "pull" + "puma" + "pump" + "pupa" + "push" + "quit" + "race" + "rack" + "raft" + "rage" + "rail" + "rain" + "rake" + "rank" + "rate" + "read" + "rear" + "reef" + "rent" + "rest" + "rice" + "rich" + "ride" + "ring" + "rise" + "risk" + "road" + "robe" + "rock" + "role" + "roll" + "roof" + "room" + "root" + "rope" + "rose" + "ruin" + "rule" + "rush" + "ruth" + "sack" + "safe" + "sage" + "sail" + "sale" + "salt" + "sand" + "sari" + "sash" + "save" + "scow" + "seal" + "seat" + "seed" + "self" + "sell" + "shed" + "shin" + "ship" + "shoe" + "shop" + "shot" + "show" + "sick" + "side" + "sign" + "silk" + "sill" + "silo" + "sing" + "sink" + "site" + "size" + "skin" + "sled" + "slip" + "smog" + "snob" + "snow" + "soap" + "sock" + "soda" + "sofa" + "soft" + "soil" + "song" + "soot" + "sort" + "soup" + "spot" + "spur" + "stag" + "star" + "stay" + "stem" + "step" + "stew" + "stop" + "stud" + "suck" + "suit" + "swan" + "swim" + "tail" + "tale" + "talk" + "tank" + "tard" + "task" + "taxi" + "team" + "tear" + "teen" + "tell" + "temp" + "tent" + "term" + "test" + "text" + "thaw" + "tile" + "till" + "time" + "tire" + "toad" + "toga" + "togs" + "tone" + "tool" + "toot" + "tote" + "tour" + "town" + "tram" + "tray" + "tree" + "trim" + "trip" + "tuba" + "tube" + "tuna" + "tune" + "turn" + "tutu" + "twig" + "type" + "unit" + "user" + "vane" + "vase" + "vast" + "veal" + "veil" + "vein" + "vest" + "vibe" + "view" + "vise" + "wait" + "wake" + "walk" + "wall" + "wash" + "wasp" + "wave" + "wear" + "weed" + "week" + "well" + "west" + "whip" + "wife" + "will" + "wind" + "wine" + "wing" + "wire" + "wish" + "wolf" + "wood" + "wool" + "word" + "work" + "worm" + "wrap" + "wren" + "yard" + "yarn" + "yawl" + "year" + "yoga" + "yoke" + "yurt" + "zinc" + "zone"]) diff --git a/src/legacy/status_im/utils/slurp.clj b/src/legacy/status_im/utils/slurp.clj new file mode 100644 index 00000000000..e33402c6893 --- /dev/null +++ b/src/legacy/status_im/utils/slurp.clj @@ -0,0 +1,5 @@ +(ns legacy.status-im.utils.slurp (:refer-clojure :exclude [slurp])) + +(defmacro slurp + [file] + (clojure.core/slurp file)) diff --git a/src/legacy/status_im/utils/styles.clj b/src/legacy/status_im/utils/styles.clj new file mode 100644 index 00000000000..7d522955055 --- /dev/null +++ b/src/legacy/status_im/utils/styles.clj @@ -0,0 +1,52 @@ +(ns legacy.status-im.utils.styles (:refer-clojure :exclude [defn def])) + +(defn- body + [style] + `(let [style# ~style + common# (dissoc style# :android :ios) + platform# (keyword react-native.platform/os) + platform-specific# (get style# platform#)] + (if platform-specific# + (merge common# platform-specific#) + common#))) + +(defmacro def + "Defines style symbol. + Style parameter may contain platform specific style: + {:width 100 + :height 125 + :ios {:height 20} + :android {:margin-top 3}} + + Resulting style for Android: + {:width 100 + :height 125 + :margin-top 3} + + Resulting style for iOS: + {:width 100 + :height 20}" + [style-name style] + `(def ~style-name + ~(body style))) + +(defmacro defn + "Defines style function. + Style parameter may contain platform specific style: + {:width 100 + :height (* a 2) + :ios {:height (/ a 2)} + :android {:margin-top 3}} + + Resulting style for Android (with (= a 10)): + {:width 100 + :height 20 + :margin-top 3} + + Resulting style for iOS (with (= a 10)): + {:width 100 + :height 5}" + [style-name params style] + `(clojure.core/defn ~style-name + [~@params] + ~(body style))) diff --git a/src/legacy/status_im/utils/styles.cljs b/src/legacy/status_im/utils/styles.cljs new file mode 100644 index 00000000000..62f44c34fa5 --- /dev/null +++ b/src/legacy/status_im/utils/styles.cljs @@ -0,0 +1,4 @@ +(ns legacy.status-im.utils.styles + (:require-macros legacy.status-im.utils.styles) + (:require + react-native.platform)) diff --git a/src/legacy/status_im/utils/transducers.cljs b/src/legacy/status_im/utils/transducers.cljs new file mode 100644 index 00000000000..926f0718d25 --- /dev/null +++ b/src/legacy/status_im/utils/transducers.cljs @@ -0,0 +1,26 @@ +(ns legacy.status-im.utils.transducers "Utility namespace containing various usefull transducers") + +(defn last-distinct-by + "Just like regular `distinct`, but you provide function + computing the distinctness of input elements and when + duplicate elements are removed, the last, not the first + one is removed." + [compare-fn] + (fn [rf] + (let [accumulated-input (volatile! {:seen {} + :input []})] + (fn + ([] (rf)) + ([result] + (reduce rf result (:input @accumulated-input))) + ([result input] + (let [compare-value (compare-fn input)] + (if-let [previous-duplicate-index (get-in @accumulated-input [:seen compare-value])] + (do (vswap! accumulated-input assoc-in [:input previous-duplicate-index] input) + result) + (do (vswap! accumulated-input + (fn [{previous-input :input :as accumulated-input}] + (-> accumulated-input + (update :seen assoc compare-value (count previous-input)) + (update :input conj input)))) + result)))))))) diff --git a/src/legacy/status_im/utils/transducers_test.cljs b/src/legacy/status_im/utils/transducers_test.cljs new file mode 100644 index 00000000000..150a487587f --- /dev/null +++ b/src/legacy/status_im/utils/transducers_test.cljs @@ -0,0 +1,49 @@ +(ns legacy.status-im.utils.transducers-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [legacy.status-im.utils.transducers :as transducers])) + +(def jail-calls + '({:jail-id 1 + :path [:suggestions] + :params {:arg 0}} + {:jail-id 1 + :path [:function] + :params {:sub :a}} + {:jail-id 1 + :path [:function] + :params {:sub :b}} + {:jail-id 1 + :path [:suggestions] + :params {:arg 1}} + {:jail-id 1 + :path [:suggestions] + :params {:arg 2}} + preview-call-1 + preview-call-2)) + +(deftest last-distinct-by-test + (testing + "Elements are removed from input according to provided `compare-fn`, + when duplicate elements are removed, the last one stays" + (is (= (sequence (transducers/last-distinct-by (fn [{:keys [jail-id path] :as call}] + (if (= :suggestions (last path)) + [jail-id path] + call))) + jail-calls) + '({:jail-id 1 + :path [:suggestions] + :params {:arg 2}} + {:jail-id 1 + :path [:function] + :params {:sub :a}} + {:jail-id 1 + :path [:function] + :params {:sub :b}} + preview-call-1 + preview-call-2)))) + (testing "Edge cases with input size `N=0` and `N=1` work as well" + (is (= (sequence (transducers/last-distinct-by identity) '()) + '())) + (is (= (sequence (transducers/last-distinct-by identity) '(1)) + '(1))))) diff --git a/src/legacy/status_im/utils/utils.cljs b/src/legacy/status_im/utils/utils.cljs new file mode 100644 index 00000000000..57b72d230c9 --- /dev/null +++ b/src/legacy/status_im/utils/utils.cljs @@ -0,0 +1,97 @@ +(ns legacy.status-im.utils.utils + (:require + ["react-native" :as react-native] + ["react-native-background-timer" :default background-timer] + [re-frame.core :as re-frame] + [utils.address :as address] + [utils.ethereum.eip.eip55 :as eip55] + [utils.i18n :as i18n])) + +;;TODO (14/11/22 flexsurfer) .-Alert usage code has been moved to the status-im2 namespace, we keep this +;;only for old (status 1.0) code, +;; can be removed with old code later +(defn show-popup + ([title content] + (show-popup title content nil)) + ([title content on-dismiss] + (.alert (.-Alert react-native) + title + content + (clj->js + (vector (merge {:text "OK" + :style "cancel" + :accessibility-label :cancel-button} + (when on-dismiss {:onPress on-dismiss})))) + (when on-dismiss + (clj->js {:cancelable false}))))) + +(defn show-confirmation + [{:keys [title content confirm-button-text on-accept on-cancel cancel-button-text + extra-options]}] + (.alert (.-Alert react-native) + title + content + ;; Styles are only relevant on iOS. On Android first button is 'neutral' and second is + ;; 'positive' + (clj->js + (concat + (vector (merge {:text (or cancel-button-text (i18n/label :t/cancel)) + :style "cancel" + :accessibility-label :cancel-button} + (when on-cancel {:onPress on-cancel})) + {:text (or confirm-button-text (i18n/label :t/ok)) + :onPress on-accept + :style "default" + :accessibility-label :confirm-button}) + (or extra-options nil))) + #js {:cancelable false})) + +(defn show-question + ([title content on-accept] + (show-question title content on-accept nil)) + ([title content on-accept on-cancel] + (.alert (.-Alert react-native) + title + content + (clj->js + (vector (merge {:text (i18n/label :t/no) + :accessibility-label :no-button} + (when on-cancel {:onPress on-cancel})) + {:text (i18n/label :t/yes) + :onPress on-accept + :accessibility-label :yes-button}))))) + +;;TODO (14/11/22 flexsurfer) background-timer usage code has been moved to the status-im2 namespace, we +;;keep this only for old (status 1.0) code, +;; can be removed with old code later + +(defn set-timeout + [cb ms] + (.setTimeout background-timer cb ms)) + +(defn set-interval + [cb ms] + (.setInterval background-timer cb ms)) + +(defn clear-interval + [id] + (.clearInterval background-timer id)) + +(re-frame/reg-fx + :utils/dispatch-later + (fn [params] + (doseq [{:keys [ms dispatch]} params] + (when (and ms dispatch) + (set-timeout #(re-frame/dispatch dispatch) ms))))) + +(defn get-shortened-address + "Takes first and last 4 digits from address including leading 0x + and adds unicode ellipsis in between" + [address] + (when address + (str (subs address 0 6) "\u2026" (subs address (- (count address) 3) (count address))))) + +(defn get-shortened-checksum-address + [address] + (when address + (get-shortened-address (eip55/address->checksum (address/normalized-hex address))))) diff --git a/src/legacy/status_im/utils/views.clj b/src/legacy/status_im/utils/views.clj new file mode 100644 index 00000000000..732c84ed916 --- /dev/null +++ b/src/legacy/status_im/utils/views.clj @@ -0,0 +1,78 @@ +(ns legacy.status-im.utils.views + (:require + [clojure.walk :as walk])) + +(defn atom? + [sub] + (and (seq sub) + (#{`reagent.core/atom} (first sub)))) + +(defn walk-sub + [sub form->sym] + (if (coll? sub) + (walk/postwalk (fn [f] + (or (form->sym f) f)) + sub) + (or (form->sym sub) sub))) + +(defn prepare-subs + [subscriptions] + (let [pairs (map (fn [[form sub]] + {:form form + :sub sub + :sym (if (atom? sub) + (gensym (str (if (map? form) "keys" form))) + form)}) + (partition 2 subscriptions)) + form->sym (->> pairs + (map (fn [{:keys [form sym]}] + [form sym])) + (into {}))] + [(mapcat (fn [{:keys [form sub]}] + (when-not (vector? sub) + [form (walk-sub sub form->sym)])) + pairs) + (apply concat + (keep (fn [{:keys [sym form sub]}] + (cond + (vector? sub) + [form `(deref (re-frame.core/subscribe ~(walk-sub sub form->sym)))] + + (atom? sub) + [form `(deref ~sym)])) + pairs))])) + +(defmacro letsubs [_ & _]) + +(defmacro defview + [n params & rest-body] + (let [first-symbol (ffirst rest-body) + rest-body' (if (and (symbol? first-symbol) + (= (name first-symbol) "letsubs")) + (rest (first rest-body)) + rest-body) + [subscriptions component-map body] (case (count rest-body') + 1 [nil {} (first rest-body')] + 2 (let [first-element (first rest-body')] + (if (map? first-element) + [nil first-element (second rest-body')] + [(first rest-body') {} (second rest-body')])) + 3 rest-body') + [subs-bindings vars-bindings] (prepare-subs subscriptions)] + `(defn ~n + ~params + (let [~@subs-bindings] + (reagent.core/create-class + (merge ~(->> component-map + (map (fn [[k f]] + (let [args (gensym "args")] + [k + `(fn [& ~args] + (let [~@vars-bindings] + (apply ~f ~args)))]))) + (into {})) + {:display-name (name '~n) + :reagent-render + (fn ~params + (let [~@vars-bindings] + ~body))})))))) diff --git a/src/legacy/status_im/visibility_status_popover/core.cljs b/src/legacy/status_im/visibility_status_popover/core.cljs new file mode 100644 index 00000000000..3819c10ca04 --- /dev/null +++ b/src/legacy/status_im/visibility_status_popover/core.cljs @@ -0,0 +1,22 @@ +(ns legacy.status-im.visibility-status-popover.core + (:require + [utils.re-frame :as rf])) + +(rf/defn show-visibility-status-popover + {:events [:show-visibility-status-popover]} + [_ value] + {:show-visibility-status-popover nil + :dispatch-later [{:ms 250 + :dispatch [:show-visibility-status-popover-db value]}] + :dismiss-keyboard nil}) + +(rf/defn show-visibility-status-popover-db + {:events [:show-visibility-status-popover-db]} + [{:keys [db]} value] + {:db (assoc db :visibility-status-popover/popover value)}) + +(rf/defn hide-visibility-status-popover + {:events [:hide-visibility-status-popover]} + [{:keys [db]}] + {:db (dissoc db :visibility-status-popover/popover) + :hide-visibility-status-popover nil}) diff --git a/src/legacy/status_im/visibility_status_updates/core.cljs b/src/legacy/status_im/visibility_status_updates/core.cljs new file mode 100644 index 00000000000..c6188b78138 --- /dev/null +++ b/src/legacy/status_im/visibility_status_updates/core.cljs @@ -0,0 +1,183 @@ +(ns legacy.status-im.visibility-status-updates.core + (:require + [legacy.status-im.data-store.visibility-status-updates :as visibility-status-updates-store] + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [legacy.status-im.ui.screens.profile.visibility-status.utils :as utils] + [status-im.constants :as constants] + [utils.datetime :as datetime] + [utils.re-frame :as rf])) + +(defn valid-status-type? + [status-type] + (some #(= status-type %) + (list constants/visibility-status-always-online + constants/visibility-status-inactive + constants/visibility-status-automatic))) + +(defn process-visibility-status-update + [acc visibility-status-update] + (let [real-status-type (utils/calculate-real-status-type visibility-status-update)] + (assoc-in + acc + [:visibility-status-updates (:public-key visibility-status-update)] + (assoc visibility-status-update :status-type real-status-type)))) + +(rf/defn load-visibility-status-updates + {:events [:visibility-status-updates/visibility-status-updates-loaded]} + [{:keys [db]} visibility-status-updates-loaded] + (let [{:keys [visibility-status-updates]} + (reduce (fn [acc visibility-status-update-loaded] + (let [visibility-status-update (visibility-status-updates-store/<-rpc + visibility-status-update-loaded)] + (process-visibility-status-update acc visibility-status-update))) + {} + visibility-status-updates-loaded)] + {:db (assoc db :visibility-status-updates visibility-status-updates)})) + +(defn handle-my-visibility-status-updates + [acc my-current-status clock visibility-status-update] + (let [status-type (:status-type visibility-status-update)] + (if (and (valid-status-type? status-type) + (or + (nil? my-current-status) + (> clock (:clock my-current-status)))) + (-> acc + (update :current-user-visibility-status + merge + {:clock clock :status-type status-type}) + (assoc :dispatch + [:visibility-status-updates/send-visibility-status-updates? + (not= status-type constants/visibility-status-inactive)])) + acc))) + +(defn handle-other-visibility-status-updates + [acc public-key clock visibility-status-update] + (let [status-type (:status-type visibility-status-update) + visibility-status-update-old + (get-in acc [:visibility-status-updates public-key])] + (if (and (valid-status-type? status-type) + (or + (nil? visibility-status-update-old) + (> clock (:clock visibility-status-update-old)))) + (process-visibility-status-update acc visibility-status-update) + acc))) + +(rf/defn handle-visibility-status-updates + [{:keys [db]} visibility-status-updates-received] + (let [visibility-status-updates-old (get db :visibility-status-updates {}) + my-public-key (get-in + db + [:profile/profile :public-key]) + my-current-status (get-in + db + [:profile/profile :current-user-visibility-status]) + {:keys [visibility-status-updates current-user-visibility-status dispatch]} + (reduce (fn [acc visibility-status-update-received] + (let [{:keys [public-key clock] :as visibility-status-update} + (visibility-status-updates-store/<-rpc + visibility-status-update-received)] + (if (= public-key my-public-key) + (handle-my-visibility-status-updates + acc + my-current-status + clock + visibility-status-update) + (handle-other-visibility-status-updates + acc + public-key + clock + visibility-status-update)))) + {:visibility-status-updates visibility-status-updates-old + :current-user-visibility-status my-current-status} + visibility-status-updates-received)] + (merge {:db (-> db + (update-in [:visibility-status-updates] + merge + visibility-status-updates) + (update-in [:profile/profile :current-user-visibility-status] + merge + current-user-visibility-status))} + (when dispatch {:dispatch dispatch})))) + +(rf/defn update-visibility-status + {:events [:visibility-status-updates/update-visibility-status]} + [{:keys [db]} status-type] + {:db (update-in db + [:profile/profile :current-user-visibility-status] + merge + {:status-type status-type + :clock (datetime/timestamp-sec)}) + :json-rpc/call [{:method "wakuext_setUserStatus" + :params [status-type ""] + :on-success #()}]}) + +(rf/defn send-visibility-status-updates? + {:events [:visibility-status-updates/send-visibility-status-updates?]} + [cofx value] + (multiaccounts.update/multiaccount-update cofx + :send-status-updates? + value + {})) + +(rf/defn visibility-status-option-pressed + {:events [:visibility-status-updates/visibility-status-option-pressed]} + [{:keys [db] :as cofx} status-type] + (let [events-to-dispatch-later + (cond-> + [{:ms 10 + :dispatch + [:visibility-status-updates/update-visibility-status + status-type]}] + (and + (= status-type constants/visibility-status-inactive) + (pos? (:peer-stats/count db))) + ;; Disable broadcasting further updates + (conj {:ms 1000 + :dispatch + [:visibility-status-updates/send-visibility-status-updates? false]}))] + (rf/merge cofx + {:dispatch-later events-to-dispatch-later} + ;; Enable broadcasting for current broadcast + (send-visibility-status-updates? true)))) + +(rf/defn delayed-visibility-status-update + {:events [:visibility-status-updates/delayed-visibility-status-update]} + [_ status-type] + {:dispatch-later + [{:ms 200 + :dispatch + [:visibility-status-updates/visibility-status-option-pressed status-type]}]}) + +(rf/defn peers-summary-change + [{:keys [db] :as cofx} peers-count] + (let [send-updates? + (get-in db [:profile/profile :send-status-updates?]) + status-type + (get-in db [:profile/profile :current-user-visibility-status :status-type])] + (when (and + (> peers-count 0) + send-updates? + (= status-type constants/visibility-status-inactive)) + (rf/merge cofx + {:dispatch-later [{:ms 1000 + :dispatch + [:visibility-status-updates/send-visibility-status-updates? false]}] + :db (assoc-in db [:profile/profile :send-status-updates?] false)} + (update-visibility-status status-type))))) + +(rf/defn sync-visibility-status-update + [{:keys [db] :as cofx} visibility-status-update-received] + (let [my-current-status (get-in db [:profile/profile :current-user-visibility-status]) + {:keys [status-type clock]} (visibility-status-updates-store/<-rpc + visibility-status-update-received)] + (when (and (valid-status-type? status-type) + (or + (nil? my-current-status) + (> clock (:clock my-current-status)))) + (rf/merge cofx + {:db (update-in db + [:profile/profile :current-user-visibility-status] + merge + {:clock clock :status-type status-type})} + (send-visibility-status-updates? + (not= status-type constants/visibility-status-inactive)))))) diff --git a/src/legacy/status_im/waku/core.cljs b/src/legacy/status_im/waku/core.cljs new file mode 100644 index 00000000000..dc4f9f02723 --- /dev/null +++ b/src/legacy/status_im/waku/core.cljs @@ -0,0 +1,155 @@ +(ns legacy.status-im.waku.core + (:require + [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + + +(def address-regex #"/ip4/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/tcp/\d{1,5}/p2p/[a-zA-Z0-9]+") + +(defn valid-address? + [address] + (re-matches address-regex address)) + +(rf/defn set-input + {:events [:wakuv2.ui/input-changed]} + [{:keys [db]} input-key value] + {:db (assoc-in db + [:wakuv2-nodes/manage input-key] + {:value value + :error (case input-key + :name (string/blank? value) + :address (when value (not (valid-address? value))))})}) + +(rf/defn enter-settings + {:events [:wakuv2.ui/enter-settings-pressed] + :interceptors [(re-frame/inject-cofx :random-guid-generator)]} + [{:keys [db random-guid-generator] :as cofx}] + (let [custom-nodes (into {} + (map #(vector (random-guid-generator) + {:name (name (first %1)) :address (second %1)}) + (get-in db [:profile/profile :wakuv2-config :CustomNodes])))] + (rf/merge cofx + {:db (assoc db :wakuv2-nodes/list custom-nodes) + :dispatch [:navigate-to :screen/wakuv2-settings]}))) + +(rf/defn edit + {:events [:wakuv2.ui/add-node-pressed] + :interceptors [(re-frame/inject-cofx :random-guid-generator)]} + [{:keys [db random-guid-generator] :as cofx} id] + (let [{:keys [name address]} (get-in db [:wakuv2-nodes/list id]) + id (or id (random-guid-generator)) + fxs (rf/merge cofx + {:db (update db + :wakuv2-nodes/manage + assoc + :new? (nil? name) + :id id)} + (set-input :name name) + (set-input :address address))] + (assoc fxs :dispatch [:navigate-to :screen/edit-wakuv2-node]))) + +(rf/defn delete + [{:keys [db] :as cofx} id] + (rf/merge cofx + {:db (-> db + (update :wakuv2-nodes/list dissoc id) + (dissoc :wakuv2-nodes/manage))})) + +(rf/defn save-node + {:events [:wakuv2.ui/save-node-pressed]} + [{:keys [db] :as cofx}] + (let [manage (:wakuv2-nodes/manage db) + id (:id manage)] + (rf/merge cofx + {:db (-> db + (assoc-in [:wakuv2-nodes/list id] + {:name (get-in manage [:name :value]) + :address (get-in manage [:address :value])}) + (dissoc :wakuv2-nodes/manage)) + :dispatch [:navigate-back]}))) + +(rf/defn discard-all + {:events [:wakuv2.ui/discard-all-pressed]} + [{:keys [db] :as cofx}] + (rf/merge cofx + {:db (dissoc db :wakuv2-nodes/manage :wakuv2-nodes/list) + :dispatch [:navigate-back]})) + +(rf/defn save-all-pressed + {:events [:wakuv2.ui/save-all-pressed]} + [_cofx] + {:ui/show-confirmation + {:title (i18n/label :t/close-app-title) + :content (i18n/label :t/wakuv2-change-nodes) + :confirm-button-text (i18n/label :t/close-app-button) + :on-accept + #(re-frame/dispatch [:wakuv2.ui/save-all-confirmed]) + :on-cancel nil}}) + +(rf/defn save-all + {:events [:wakuv2.ui/save-all-confirmed]} + [{:keys [db]}] + (let [new-nodes (->> (:wakuv2-nodes/list db) + vals + (map #(vector (:name %1) (:address %1))) + (into {}))] + {:db (-> db + (assoc-in [:profile/profile :wakuv2-config :CustomNodes] new-nodes) + (dissoc :wakuv2-nodes/manage :wakuv2-nodes/list)) + :json-rpc/call [{:method "wakuext_setCustomNodes" + :params [{:customNodes new-nodes}] + :on-success #(log/info "updated custom nodes") + :on-error #(log/error "failed to set custom nodes" + {:error % + :custom-nodes new-nodes})}] + :dispatch [:navigate-back]})) + +(rf/defn show-delete-node-confirmation + {:events [:wakuv2.ui/delete-pressed]} + [_ node-name] + {:ui/show-confirmation {:title (i18n/label :t/delete-node-title) + :content (i18n/label :t/delete-node-are-you-sure) + :confirm-button-text (i18n/label :t/delete-node) + :on-accept #(re-frame/dispatch [:wakuv2.ui/delete-confirmed + node-name])}}) + +(rf/defn delete-node + {:events [:wakuv2.ui/delete-confirmed]} + [cofx id] + (rf/merge cofx + (delete id) + (navigation/navigate-back nil))) + +(rf/defn toggle-light-client + {:events [:wakuv2.ui/toggle-light-client]} + [{:keys [db]} enabled?] + {:db (assoc-in db [:profile/profile :wakuv2-config :LightClient] enabled?) + + :json-rpc/call [{:method "wakuext_setLightClient" + :params [{:enabled enabled?}] + :on-success (fn [] + (log/info "light client set successfully" enabled?) + (re-frame/dispatch [:profile/logout])) + :on-error #(log/error "failed to set light client" + {:error % + :enabled? enabled?})}]}) + +(rf/defn toggle-store-confirmations + {:events [:wakuv2.ui/toggle-store-confirmations]} + [{:keys [db]} enabled?] + {:db (assoc-in db + [:profile/profile :wakuv2-config :EnableStoreConfirmationForMessagesSent] + enabled?) + + :json-rpc/call [{:method "wakuext_setStoreConfirmationForMessagesSent" + :params [{:enabled enabled?}] + :on-success (fn [] + (log/info "store confirmation set successfully" enabled?) + (re-frame/dispatch [:profile/logout])) + :on-error #(log/error "failed to set store confirmation" + {:error % + :enabled? enabled?})}]}) diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs new file mode 100644 index 00000000000..d2931eb6bc9 --- /dev/null +++ b/src/mocks/js_dependencies.cljs @@ -0,0 +1,502 @@ +(ns mocks.js-dependencies + (:require-macros [legacy.status-im.utils.slurp :refer [slurp]]) + (:require + [legacy.status-im.fleet.default-fleet :refer (default-fleets)]) + (:require [tests.test-utils :as utils.test])) + +;; to generate a js Proxy at js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ that accept any (.xxx) call and +;; return itself +;; For the convenience to mock eg. +;; (-> reanimated/slide-out-up-animation .springify (.damping 20) (.stiffness 300)) +;; (-> reanimated/slide-out-up-animation (.damping 20) .springify (.stiffness 300)) +(js/eval + " + var globalThis + if (typeof window === \"undefined\") { + globalThis = global + } else { + globalThis = window + } + globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return () => globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__}}) + ") + +(def action-button #js {:default #js {:Item #js {}}}) +(def config #js {:default #js {}}) +(def camera #js {:RNCamera #js {:Constants #js {}}}) +(def dialogs #js {}) +(def dismiss-keyboard #js {}) +(def emoji-picker #js {:default #js {}}) +(def fs #js {}) +(def i18n #js {:locale "en"}) +(def image-crop-picker #js {}) +(def image-resizer #js {}) +(def qr-code #js {}) +(def svg #js {}) + +(def react-native + (clj->js + {:NativeModules {:RNGestureHandlerModule {:Direction (fn [])} + :PushNotifications {} + :Status utils.test/status + :EncryptionUtils utils.test/encryption-utils + :AccountManager utils.test/account-manager + :Utils utils.test/utils + :LogManager utils.test/log-manager + :NetworkManager utils.test/network + :UIHelper utils.test/ui-helper + :ReanimatedModule {:configureProps (fn [])}} + + :View {} + :RefreshControl {} + :AppState {} + :Alert {:alert (fn [])} + :FlatList {} + :SectionList {} + :Text {} + :StatusBar {:setBarStyle identity} + :ScrollView {} + :SafeAreaView {} + :KeyboardAvoidingView {} + :TextInput {} + :Image {} + :Picker {:Item {}} + :Switch {} + :Modal {} + :Keyboard {:dismiss (fn [])} + :Linking {} + :TouchableWithoutFeedback {} + :TouchableHighlight {} + :Pressable {} + :TouchableOpacity {} + :ActivityIndicator {} + :StyleSheet {:create (fn [])} + :Animated {:createAnimatedComponent identity + :Value (fn []) + :ValueXY (fn []) + :View {} + :FlatList {} + :ScrollView {} + :Text {}} + :Easing {:bezier (fn []) + :poly (fn []) + :out (fn []) + :in (fn []) + :inOut (fn [])} + :DeviceEventEmitter {:addListener (fn [])} + :Dimensions {:get (fn []) :addEventListener identity} + :useWindowDimensions {} + :Platform {:select (fn [])} + :I18nManager {:isRTL ""} + :NativeEventEmitter (fn []) + :LayoutAnimation {:Presets #js + {:easeInEaseOut nil + :linear nil + :spring nil} + :Types #js {} + :Properties #{} + :create (fn []) + :configureNext (fn [])} + :requireNativeComponent (fn [] {:propTypes ""}) + :Appearance {:getColorScheme (fn []) + :addChangeListener (fn [])} + :PixelRatio {:get (fn [])}})) + +(set! js/ReactNative react-native) + +(def reanimated-bottom-sheet #js {:default #js {}}) + +(def icons #js {:default #js {}}) +(def webview #js {:WebView #js {}}) +(def status-keycard + #js + {:default #js + {:nfcIsSupported (fn [] #js {:then identity}) + :nfcIsEnabled (fn [] #js {:then identity}) + :getApplicationInfo (fn [] #js {:then identity}) + :getKeys (fn [] #js {:then identity}) + :setPairings (fn [] #js {:then identity})}}) + +(def snoopy #js {:default #js {}}) +(def snoopy-filter #js {:default #js {}}) +(def snoopy-bars #js {:default #js {}}) +(def snoopy-buffer #js {:default #js {}}) +(def fetch #js {}) + +(def async-storage-atom (atom {})) +(def async-storage + #js + {:default #js + {:setItem #(js/Promise.resolve) + :multiGet #(js/Promise.resolve) + :getItem #(js/Promise.resolve)}}) + +(def background-timer + (clj->js {:default {:setTimeout js/setTimeout + :setInterval js/setInterval + :clearTimeout js/clearTimeout + :clearInterval js/clearInterval}})) + +(def keychain + #js + {:setGenericPassword (constantly (.resolve js/Promise true)) + :setInternetCredentials #(js/Promise.resolve) + :resetInternetCredentials #(js/Promise.resolve) + "ACCESSIBLE" {} + "ACCESS_CONTROL" {}}) + +(def react-native-mail #js {:mail #js {}}) +(def react-native-screens #js {}) +(def react-native-shake #js {}) +(def react-native-share #js {:default {}}) +(def react-native-svg + #js + {:ClipPath #js {:render identity} + :Circle #js {:render identity} + :Defs #js {:render identity} + :G #js {:render identity} + :Path #js {:render identity} + :Rect #js {:render identity} + :SvgUri #js {:render identity} + :SvgXml #js {:render identity} + :LinearGradient #js {:render identity} + :Stop #js {:render identity} + :default #js {:render identity}}) +(def react-native-webview #js {:default {}}) +(def react-native-audio-toolkit + #js + {:MediaStates {} + :PlaybackCategories {}}) + +(def react-native-clipboard + #js + {:getEnforcing {}}) + +(def net-info #js {:addEventListener identity}) +(def react-native-biometrics #js {:default {}}) +(def react-native-safe-area-context + #js {:initialWindowMetrics #js {} + :SafeAreaProvider #js {}}) + +(def back-handler + #js + {:addEventListener identity + :removeEventListener identity}) +(def react + #js + {:useCallback nil + :useEffect nil + :useRef nil + :createRef nil + :Fragment identity}) +(def react-navigation-native + #js + {:NavigationContainer #js {} + :useFocusEffect identity + :CommonActions #js {} + :StackActions #js {}}) + +(def react-native-navigation + #js + {:Navigation #js + {:constants (fn [] #js {:then identity}) + :setDefaultOptions identity + :setRoot identity + :dismissOverlay #(js/Promise.resolve) + :showOverlay identity + :setLazyComponentRegistrator identity + :pop identity + :push identity + :mergeOptions identity + :popToRoot identity + :showModal identity + :dismissModal identity + :registerComponent identity + :events + (fn [] + #js + {:registerModalDismissedListener identity + :registerAppLaunchedListener identity + :registerBottomTabSelectedListener identity + :registerComponentDidDisappearListener identity + :registerComponentDidAppearListener identity + :registerNavigationButtonPressedListener identity})}}) + +(def react-navigation-stack + #js + {:createStackNavigator identity + :TransitionPresets #js {:ModalPresentationIOS #js {}}}) +(def react-navigation-bottom-tabs #js {:createBottomTabNavigator identity}) + +(def react-native-reanimated + #js + {:default #js + {:createAnimatedComponent identity + :eq nil + :greaterOrEq nil + :greaterThan nil + :lessThan nil + :lessOrEq nil + :add nil + :diff nil + :divide nil + :sub nil + :multiply nil + :abs nil + :min nil + :max nil + :neq nil + :and nil + :or nil + :not nil + :set nil + :startClock nil + :stopClock nil + :Value nil + :Clock nil + :debug nil + :log nil + :event nil + :cond nil + :block nil + :interpolateNode nil + :call nil + :timing nil + :onChange nil + :View #js {} + :Image #js {} + :FlatList #js {} + :ScrollView #js {} + :Text #js {} + :Extrapolate #js {:CLAMP nil} + :Code #js {}} + :EasingNode #js + {:bezier identity + :linear identity} + :clockRunning nil + :useSharedValue (fn []) + :useAnimatedStyle (fn []) + :withTiming (fn []) + :withDelay (fn []) + :Easing #js {:bezier identity} + :Keyframe (fn []) + :enableLayoutAnimations (fn []) + :SlideOutUp js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ + :SlideInUp js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ + :LinearTransition js/__STATUS_MOBILE_JS_IDENTITY_PROXY__}) + +(def react-native-gesture-handler + #js + {:FlatList #js {} + :Gesture #js {:Pan nil} + :GestureDetector #js {} + :LongPressGestureHandler #js {} + :NativeViewGestureHandler #js {} + :PanGestureHandler #js {} + :PureNativeButton #js {} + :RectButton #js {} + :ScrollView #js {} + :State #js + {:BEGAN nil + :ACTIVE nil + :CANCELLED nil + :END nil + :FAILED nil + :UNDETERMINED nil} + :Swipeable #js {} + :TapGestureHandler #js {} + :TouchableHighlight #js {} + :TouchableOpacity #js {} + :TouchableWithoutFeedback #js {} + :createNativeWrapper identity + :default #js {}}) + +(def react-native-redash + #js + {:clamp nil + :withPause (fn [])}) + + +(def react-native-device-info + #js {:getInstallReferrer identity}) + +(def react-native-camera-kit + #js + {:Camera #js {} + :CameraType #js {}}) + +(def react-native-push-notification + #js + {:localNotification identity + :requestPermission identity}) + +(def react-native-gradien #js {:default #js {}}) + +(def masked-view #js {:default #js {}}) + +(def react-native-permissions #js {:default #js {}}) + +(def push-notification-ios + #js + {:default #js + {:abandonPermissions identity + :removeAllDeliveredNotifications identity}}) + +(def react-native-hole-view + #js {:RNHoleView #js {}}) + +(def react-native-draggable-flatlist + #js {:default #js {}}) + +(def react-native-blob-util + #js {:default #js {}}) + +(def react-native-blur + (clj->js {:BlurView {}})) + +(def react-native-shadow-2 + (clj->js {:Shadow {}})) + +(def react-native-camera-roll + (clj->js {:default #js {} + :CameraRoll #js {}})) + +(def react-native-orientation-locker + (clj->js {:default #js {} + :useDeviceOrientationChange #js {}})) + +(def react-native-transparent-video #js {:default #js {}}) + +(def react-native-gifted-charts + #js + {:BarChart #js {} + :PieChart #js {} + :LineChart #js {} + :LineChartBicolor #js {}}) + +(def react-native-pdf #js {:default {}}) + +(def worklet-factory + #js {:applyAnimationsToStyle (fn [])}) + +(def mmkv-storage-atom (atom {})) + +(defn create-mmkv-mock + [] + (let [getter (fn [k] (get @mmkv-storage-atom k))] + (clj->js + {:set (fn [k v] (swap! mmkv-storage-atom assoc k v) true) + :getString getter + :getBoolean getter + :getNumber getter + :contains (fn [k] (contains? @mmkv-storage-atom k)) + :delete (fn [k] (swap! mmkv-storage-atom dissoc k) true) + :clearAll (fn [] (reset! mmkv-storage-atom {}) true) + :getAllKeys (fn [] (clj->js (keys @mmkv-storage-atom)))}))) + +(def react-native-mmkv + (clj->js + {:MMKV (fn [_] (create-mmkv-mock)) + :createMMKV (fn [_] (create-mmkv-mock))})) + +(defn mock + [module] + (case module + "react-native-background-timer" background-timer + "react-native-image-crop-picker" image-crop-picker + "react-native-gesture-handler" react-native-gesture-handler + "react-native-safe-area-context" react-native-safe-area-context + "react-native-config" config + "react-native-mmkv" react-native-mmkv + "react-native-iphone-x-helper" (clj->js {:getStatusBarHeight (fn []) + :getBottomSpace (fn [])}) + "react-native-screens" (clj->js {}) + "react-native-reanimated" react-native-reanimated + "react-native-redash" react-native-redash + "react-native-fetch-polyfill" fetch + "react-native-status-keycard" status-keycard + "react-native-keychain" keychain + "react-native-biometrics" react-native-biometrics + "@react-native-community/netinfo" net-info + "react-native-dialogs" dialogs + "react-native" react-native + "react-native-fs" fs + "react-native-mail" react-native-mail + "react-native-image-resizer" image-resizer + "react-native-device-info" react-native-device-info + "react-native-push-notification" react-native-push-notification + "react-native-linear-gradient" react-native-gradien + "@react-native-community/masked-view" masked-view + "react-native-blob-util" react-native-blob-util + "react-native-navigation" react-native-navigation + "@react-native-community/push-notification-ios" push-notification-ios + "@react-native-community/blur" react-native-blur + "@react-native-camera-roll/camera-roll" react-native-camera-roll + "react-native-camera-kit" react-native-camera-kit + "react-native-permissions" react-native-permissions + "react-native-hole-view" react-native-hole-view + "react-native-draggable-flatlist" react-native-draggable-flatlist + "react-native-webview" react-native-webview + "@react-native-community/audio-toolkit" react-native-audio-toolkit + "@react-native-clipboard/clipboard" react-native-clipboard + "react-native-share" react-native-share + "react-native-shadow-2" react-native-shadow-2 + "@react-native-async-storage/async-storage" async-storage + "react-native-svg" react-native-svg + "react-native-transparent-video" react-native-transparent-video + "react-native-orientation-locker" react-native-orientation-locker + "react-native-gifted-charts" react-native-gifted-charts + "react-native-pdf" react-native-pdf + "@walletconnect/react-native-compat" #js {} + "../resources/data/emojis/en.json" (js/JSON.parse + (slurp + "./resources/data/emojis/en.json")) + "../src/js/worklets/core.js" worklet-factory + "../src/js/worklets/communities.js" #js {"useLogoStyles" #js {} + "useSheetStyles" #js {} + "useNameStyles" #js {} + "useInfoStyles" #js {} + "useChannelsStyles" #js {} + "useScrollTo" #js {} + "useHeaderOpacity" #js {} + "useOppositeHeaderOpacity" #js {} + "useNavContentOpacity" #js {} + "onPanStart" #js {} + "onPanUpdate" #js {} + "onPanEnd" #js {}} + "../src/js/worklets/shell/bottom_tabs.js" #js {} + "../src/js/worklets/shell/home_stack.js" #js {} + "../src/js/worklets/bottom_sheet.js" #js {} + "../src/js/worklets/record_audio.js" #js {} + "../src/js/worklets/scroll_view.js" #js {} + "../src/js/worklets/onboarding_carousel.js" #js {} + "../src/js/worklets/chat/messenger/lightbox.js" #js {} + "../src/js/worklets/chat/messenger/messages.js" #js {} + "../src/js/worklets/chat/messenger/navigation.js" #js {} + "../src/js/worklets/chat/messenger/composer.js" #js {} + "../src/js/worklets/chat/messenger/placeholder.js" #js {} + "../src/js/worklets/parallax.js" #js {} + "../src/js/worklets/profile_header.js" #js {} + "../src/js/worklets/identifiers_highlighting.js" #js {} + "../src/js/worklets/header_animations.js" #js {} + "./fleets.js" default-fleets + "../translations/ar.json" (js/JSON.parse (slurp "./translations/ar.json")) + "../translations/de.json" (js/JSON.parse (slurp "./translations/de.json")) + "../translations/en.json" (js/JSON.parse (slurp "./translations/en.json")) + "../translations/es.json" (js/JSON.parse (slurp "./translations/es.json")) + "../translations/es_419.json" (js/JSON.parse (slurp + "./translations/es_419.json")) + "../translations/fil.json" (js/JSON.parse (slurp "./translations/fil.json")) + "../translations/fr.json" (js/JSON.parse (slurp "./translations/fr.json")) + "../translations/id.json" (js/JSON.parse (slurp "./translations/id.json")) + "../translations/it.json" (js/JSON.parse (slurp "./translations/it.json")) + "../translations/ko.json" (js/JSON.parse (slurp "./translations/ko.json")) + "../translations/pt_BR.json" (js/JSON.parse (slurp + "./translations/pt_BR.json")) + "../translations/ru.json" (js/JSON.parse (slurp "./translations/ru.json")) + "../translations/tr.json" (js/JSON.parse (slurp "./translations/tr.json")) + "../translations/zh.json" (js/JSON.parse (slurp "./translations/zh.json")) + "../translations/zh_hant.json" (js/JSON.parse (slurp + "./translations/zh_hant.json")) + "../translations/zh_TW.json" (js/JSON.parse (slurp + "./translations/zh_TW.json")) + nil)) diff --git a/src/native_module/core.cljs b/src/native_module/core.cljs new file mode 100644 index 00000000000..1882c71c998 --- /dev/null +++ b/src/native_module/core.cljs @@ -0,0 +1,598 @@ +(ns native-module.core + (:require + ["react-native" :as react-native] + [clojure.string :as string] + [native-module.utils :as native-utils] + [react-native.platform :as platform] + [taoensso.timbre :as log] + [utils.transforms :as types])) + +(defn status + [] + (when (exists? (.-NativeModules react-native)) + (.-Status ^js (.-NativeModules react-native)))) + +(defn account-manager + [] + (when (exists? (.-NativeModules react-native)) + (.-AccountManager ^js (.-NativeModules react-native)))) + +(defn encryption + [] + (when (exists? (.-NativeModules react-native)) + (.-EncryptionUtils ^js (.-NativeModules react-native)))) + +(defn database + [] + (when (exists? (.-NativeModules react-native)) + (.-DatabaseManager ^js (.-NativeModules react-native)))) + +(defn ui-helper + [] + (when (exists? (.-NativeModules react-native)) + (.-UIHelper ^js (.-NativeModules react-native)))) + +(defn log-manager + [] + (when (exists? (.-NativeModules react-native)) + (.-LogManager ^js (.-NativeModules react-native)))) + +(defn utils + [] + (when (exists? (.-NativeModules react-native)) + (.-Utils ^js (.-NativeModules react-native)))) + +(defn network + [] + (when (exists? (.-NativeModules react-native)) + (.-NetworkManager ^js (.-NativeModules react-native)))) + +(defn mail-manager + [] + (when (exists? (.-NativeModules react-native)) + (.-MailManager ^js (.-NativeModules react-native)))) + +(defn mail + [opts callback] + (.mail ^js (mail-manager) (clj->js opts) callback)) + +(defn init + [handler] + (.addListener ^js (.-DeviceEventEmitter ^js react-native) "gethEvent" #(handler (.-jsonEvent ^js %)))) + +(defn clear-web-data + [] + (log/debug "[native-module] clear-web-data") + (when (ui-helper) + (.clearCookies ^js (ui-helper)) + (.clearStorageAPIs ^js (ui-helper)))) + +(defn init-keystore + [key-uid callback] + (log/debug "[native-module] init-keystore" key-uid) + (.initKeystore ^js (encryption) key-uid callback)) + +(defn initialize-application + [request callback] + (log/debug "[native-module] initialize-application") + (.initializeApplication ^js (account-manager) + (types/clj->json request) + #(callback (types/json->clj %)))) + +(defn accept-terms + ([] + (native-utils/promisify-native-module-call accept-terms)) + ([callback] + (.acceptTerms ^js (account-manager) callback))) + +(defn login-account + "NOTE: beware, the password has to be sha3 hashed" + [{:keys [keyUid] :as request}] + (log/debug "[native-module] loginAccount") + (clear-web-data) + (init-keystore + keyUid + #(.loginAccount ^js (account-manager) (types/clj->json request)))) + +(defn create-account-and-login + [request] + (.createAccountAndLogin ^js (account-manager) (types/clj->json request))) + +(defn restore-account-and-login + [request] + (.restoreAccountAndLogin ^js (account-manager) (types/clj->json request))) + +(defn export-db + "NOTE: beware, the password has to be sha3 hashed" + [key-uid account-data hashed-password callback] + (log/debug "[native-module] export-db") + (clear-web-data) + (init-keystore + key-uid + #(.exportUnencryptedDatabase ^js (database) account-data hashed-password callback))) + +(defn import-db + "NOTE: beware, the password has to be sha3 hashed" + [key-uid account-data hashed-password] + (log/debug "[native-module] import-db") + (clear-web-data) + (init-keystore + key-uid + #(.importUnencryptedDatabase ^js (database) account-data hashed-password))) + +(defn logout + [] + (log/debug "[native-module] logout") + (clear-web-data) + (.logout ^js (account-manager))) + +(defn multiaccount-derive-addresses + "NOTE: this should be named derive-accounts + this only derive addresses, they still need to be stored + with `multiaccount-store-derived` if you want to be able to + reuse the derived addresses later" + [account-id paths callback] + (log/debug "[native-module] multiaccount-derive-addresses") + (when (status) + (.multiAccountDeriveAddresses ^js (account-manager) + (types/clj->json {:accountID account-id + :paths paths}) + callback))) + +(defn multiaccount-generate-and-derive-addresses + "used to generate multiple multiaccounts for onboarding + NOTE: nothing is saved so you will need to use + `multiaccount-store-account` on the selected multiaccount + to store the key" + [n mnemonic-length paths callback] + (log/debug "[native-module] multiaccount-generate-and-derive-addresses") + (.multiAccountGenerateAndDeriveAddresses ^js (account-manager) + (types/clj->json {:n n + :mnemonicPhraseLength mnemonic-length + :bip39Passphrase "" + :paths paths}) + callback)) + +(defn multiaccount-import-mnemonic + [mnemonic password callback] + (log/debug "[native-module] multiaccount-import-mnemonic") + (.multiAccountImportMnemonic ^js (account-manager) + (types/clj->json {:mnemonicPhrase mnemonic + ;;NOTE this is not the multiaccount password + :Bip39Passphrase password}) + callback)) + +(defn set-soft-input-mode + [mode] + (log/debug "[native-module] set-soft-input-mode") + (.setSoftInputMode ^js (ui-helper) mode)) + +(defn call-rpc + [payload callback] + (log/debug "[native-module] call-rpc") + (.callRPC ^js (network) payload callback)) + +(defn call-private-rpc + [payload callback] + (.callPrivateRPC ^js (network) payload callback)) + +(defn hash-transaction + "used for keycard" + [rpcParams callback] + (log/debug "[native-module] hash-transaction") + (.hashTransaction ^js (encryption) rpcParams callback)) + +(defn hash-message + "used for keycard" + [message callback] + (log/debug "[native-module] hash-message") + (.hashMessage ^js (encryption) message callback)) + +(defn start-searching-for-local-pairing-peers + "starts a UDP multicast beacon that both listens for and broadcasts to LAN peers" + [callback] + (log/info "[native-module] Start Searching for Local Pairing Peers" + {:fn :start-searching-for-local-pairing-peers}) + (.startSearchForLocalPairingPeers ^js (network) callback)) + +(defn local-pairing-preflight-outbound-check + "Checks whether the device has allows connecting to the local server" + [callback] + (log/info "[native-module] Performing local pairing preflight check") + (when platform/ios? + (.localPairingPreflightOutboundCheck ^js (encryption) callback))) + +(defn get-connection-string-for-bootstrapping-another-device + "Generates connection string form status-go for the purpose of local pairing on the sender end" + [config-json callback] + (log/info "[native-module] Fetching Connection String" + {:fn :get-connection-string-for-bootstrapping-another-device + :config-json config-json}) + (.getConnectionStringForBootstrappingAnotherDevice ^js (network) config-json callback)) + +(defn input-connection-string-for-bootstrapping + "Provides connection string to status-go for the purpose of local pairing on the receiver end" + [connection-string config-json callback] + (log/info "[native-module] Sending Connection String" + {:fn :input-connection-string-for-bootstrapping + :config-json config-json + :connection-string connection-string}) + (.inputConnectionStringForBootstrapping ^js (network) connection-string config-json callback)) + +(defn deserialize-and-compress-key + "Provides a community id (public key) to status-go which is first deserialized + and then compressed. Example input/output : + input key = zQ3shTAten2v9CwyQD1Kc7VXAqNPDcHZAMsfbLHCZEx6nFqk9 and + output key = 0x025596a7ff87da36860a84b0908191ce60a504afc94aac93c1abd774f182967ce6" + [input-key callback] + (log/info "[native-module] Deserializing and then compressing public key" + {:fn :deserialize-and-compress-key + :key input-key}) + (.deserializeAndCompressKey ^js (encryption) input-key callback)) + +(defn serialize-legacy-key + "Compresses an old format public key (0x04...) to the new one zQ..." + [public-key] + (.serializeLegacyKey ^js (encryption) public-key)) + +(defn compressed-key->public-key + "Provides compressed key to status-go and gets back the uncompressed public key via deserialization" + [public-key deserialization-key callback] + (log/info "[native-module] Deserializing compressed key" + {:fn :compressed-key->public-key + :public-key public-key}) + (.multiformatDeserializePublicKey ^js (encryption) public-key deserialization-key callback)) + +(defn hash-typed-data + "used for keycard" + ([data] + (native-utils/promisify-native-module-call hash-typed-data data)) + ([data callback] + (log/debug "[native-module] hash-typed-data") + (.hashTypedData ^js (encryption) data callback))) + +(defn hash-typed-data-v4 + "used for keycard" + ([data] + (native-utils/promisify-native-module-call hash-typed-data-v4 data)) + ([data callback] + (log/debug "[native-module] hash-typed-data-v4") + (.hashTypedDataV4 ^js (encryption) data callback))) + +(defn send-transaction-with-signature + "used for keycard" + [rpcParams sig callback] + (log/debug "[native-module] send-transaction-with-signature") + (.sendTransactionWithSignature ^js (network) rpcParams sig callback)) + +(defn sign-message + "NOTE: beware, the password in rpcParams has to be sha3 hashed" + ([rpcParams] + (native-utils/promisify-native-module-call sign-message rpcParams)) + ([rpcParams callback] + (log/debug "[native-module] sign-message") + (.signMessage ^js (encryption) rpcParams callback))) + +(defn recover-message + [rpcParams callback] + (log/debug "[native-module] recover") + (.recover ^js (network) rpcParams callback)) + +(defn send-transaction + "NOTE: beware, the password has to be sha3 hashed" + [rpcParams hashed-password callback] + (log/debug "[native-module] send-transaction") + (.sendTransaction ^js (network) rpcParams hashed-password callback)) + +(defn sign-typed-data + "NOTE: beware, the password has to be sha3 hashed" + ([data account hashed-password] + (native-utils/promisify-native-module-call sign-typed-data data account hashed-password)) + ([data account hashed-password callback] + (log/debug "[native-module] sign-typed-data") + (.signTypedData ^js (encryption) data account hashed-password callback))) + +(defn sign-typed-data-v4 + "NOTE: beware, the password has to be sha3 hashed" + ([data account hashed-password] + (native-utils/promisify-native-module-call sign-typed-data-v4 data account hashed-password)) + ([data account hashed-password callback] + (log/debug "[native-module] sign-typed-data-v4") + (.signTypedDataV4 ^js (encryption) data account hashed-password callback))) + +(defn send-logs + [dbJson js-logs use-public-log-dir? callback] + (log/debug "[native-module] send-logs") + (.sendLogs ^js (log-manager) dbJson js-logs use-public-log-dir? callback)) + +;; workaround for android since react-native-share is not working for zip files +(defn share-logs + [fileUri callback] + (log/debug "[native-module] share-logs") + (.shareLogs ^js (log-manager) fileUri callback)) + +(defn close-application + [] + (log/debug "[native-module] close-application") + (.closeApplication ^js (status))) + +(defn connection-change + [type expensive?] + (log/debug "[native-module] connection-change") + (.connectionChange ^js (status) type (boolean expensive?))) + +(defn app-state-change + [state] + (log/debug "[native-module] app-state-change") + (.appStateChange ^js (status) state)) + +(defn start-local-notifications + [] + (log/debug "[native-module] start-local-notifications") + (.startLocalNotifications ^js (status))) + +(defn set-blank-preview-flag + [flag] + (log/debug "[native-module] set-blank-preview-flag") + ;; Sometimes the app crashes during logout because `flag` is nil. + (when-not (nil? flag) + (.setBlankPreviewFlag ^js (encryption) flag))) + +(defn get-device-model-info + [] + (log/debug "[native-module] get-device-model-info") + ;;NOTE: we have to check for status module because of tests + (when-let [^js status-module (status)] + {:model (.-model status-module) + :brand (.-brand status-module) + :build-id (.-buildId status-module) + :device-id (.-deviceId status-module)})) + +(defn get-installation-name + [] + ;; NOTE(rasom): Only needed for android devices currently + (when platform/android? + (string/join " " + ((juxt :model :device-id) + (get-device-model-info))))) + +(defn get-node-config + [callback] + (log/debug "[native-module] get-node-config") + (.getNodeConfig ^js (status) callback)) + +(defn toggle-webview-debug + [on] + (log/debug "[native-module] toggle-webview-debug" on) + (.toggleWebviewDebug ^js (ui-helper) on)) + +(defn rooted-device? + [callback] + (log/debug "[native-module] rooted-device?") + (cond + ;; we assume that iOS is safe by default + platform/ios? + (callback false) + + ;; we check root on android + platform/android? + (if (status) + (.isDeviceRooted ^js (status) callback) + ;; if module isn't initialized we return true to avoid degrading security + (callback true)) + + ;; in unknown scenarios we also consider the device rooted to avoid degrading security + :else (callback true))) + +(defn decode-parameters + [bytes-string types] + (log/debug "[native-module] decode-parameters") + (let [json-str (.decodeParameters ^js (encryption) + (types/clj->json {:bytesString bytes-string :types types}))] + (types/json->clj json-str))) + +(defn hex-to-number + [hex] + (log/debug "[native-module] hex-to-number") + (let [json-str (.hexToNumber ^js (encryption) hex)] + (types/json->clj json-str))) + +(defn number-to-hex + [num] + (log/debug "[native-module] number-to-hex") + (.numberToHex ^js (encryption) (str num))) + +(defn sha3 + [s] + (log/debug "[native-module] sha3") + (when s + (.sha3 ^js (encryption) (str s)))) + +(defn utf8-to-hex + [s] + (log/debug "[native-module] utf8-to-hex") + (.utf8ToHex ^js (encryption) s)) + +(defn hex-to-utf8 + [s] + (log/debug "[native-module] hex-to-utf8") + (.hexToUtf8 ^js (encryption) s)) + +(defn check-address-checksum + [address] + (log/debug "[native-module] check-address-checksum") + (let [result (.checkAddressChecksum ^js (utils) address)] + (types/json->clj result))) + +(defn toggle-centralized-metrics + [enabled callback] + (.toggleCentralizedMetrics ^js (status) (types/clj->json {:enabled enabled}) callback)) + +(defn add-centralized-metric + [metric] + (.addCentralizedMetric ^js (status) (types/clj->json metric) #(log/debug "pushed metric" % metric))) + +(defn address? + [address] + (log/debug "[native-module] address?") + (when address + (let [result (.isAddress ^js (utils) address)] + (types/json->clj result)))) + +(defn to-checksum-address + [address] + (log/debug "[native-module] to-checksum-address") + (.toChecksumAddress ^js (utils) address)) + +(defn validate-mnemonic + "Validate that a mnemonic conforms to BIP39 dictionary/checksum standards" + ([mnemonic] + (native-utils/promisify-native-module-call validate-mnemonic mnemonic)) + ([mnemonic callback] + (log/debug "[native-module] validate-mnemonic") + (.validateMnemonic ^js (utils) mnemonic callback))) + +(defn validate-connection-string + [connection-string] + (log/debug "[native-module] validate-connection-string") + (->> connection-string + (.validateConnectionString ^js (utils)) + types/json->clj)) + +(defn delete-multiaccount + "Delete multiaccount from database, deletes multiaccount's database and + key files." + [key-uid callback] + (log/debug "[native-module] delete-multiaccount") + (.deleteMultiaccount ^js (account-manager) key-uid callback)) + +(defn delete-imported-key + "Delete imported key file." + [key-uid address hashed-password callback] + (log/debug "[native-module] delete-imported-key") + (.deleteImportedKey ^js (status) key-uid address hashed-password callback)) + +(defn reset-keyboard-input + [input selection] + (log/debug "[native-module] resetKeyboardInput") + (when platform/android? + (.resetKeyboardInputCursor ^js (ui-helper) input selection))) + +(defn reset-password + ([key-uid current-password-hashed new-password-hashed] + (native-utils/promisify-native-module-call reset-password + key-uid + current-password-hashed + new-password-hashed)) + ([key-uid current-password-hashed new-password-hashed callback] + (log/debug "[native-module] change-database-password") + (init-keystore + key-uid + #(.reEncryptDbAndKeystore ^js (encryption) + key-uid + current-password-hashed + new-password-hashed + callback)))) + +(defn convert-to-keycard-profile + [{:keys [key-uid] :as profile} settings password new-password callback] + (.convertToKeycardAccount ^js (encryption) + key-uid + (types/clj->json profile) + (types/clj->json settings) + (:keycard-instance-uid settings) + password + new-password + #(when callback (callback (types/json->clj %))))) + +(defn backup-disabled-data-dir + [] + (.backupDisabledDataDir ^js (utils))) + +(defn fleets + [] + (.fleets ^js (status))) + +(defn keystore-dir + [] + (.keystoreDir ^js (utils))) + +(defn log-file-directory + [use-public-log-dir?] + (.logFileDirectory ^js (log-manager) use-public-log-dir?)) + +(defn get-random-mnemonic + [callback] + (.getRandomMnemonic ^js (account-manager) #(callback (types/json->clj %)))) + +(defn create-account-from-mnemonic + [mnemonic callback] + (.createAccountFromMnemonicAndDeriveAccountsForPaths ^js (account-manager) + (types/clj->json mnemonic) + #(callback (types/json->clj %)))) + +(defn get-connection-string-for-exporting-keypairs-keystores + "Generates connection string form status-go for the purpose of exporting keypairs and keystores on sender side" + ([config-json] + (native-utils/promisify-native-module-call get-connection-string-for-exporting-keypairs-keystores + config-json)) + ([config-json callback] + (log/info "[native-module] Fetching Export Keypairs Connection String" + {:fn :get-connection-string-for-exporting-keypairs-keystores + :config-json config-json}) + (.getConnectionStringForExportingKeypairsKeystores ^js (network) config-json callback))) + +(defn input-connection-string-for-importing-keypairs-keystores + "Provides connection string to status-go for the purpose of importing keypairs and keystores on the receiver side" + ([connection-string config-json] + (native-utils/promisify-native-module-call input-connection-string-for-importing-keypairs-keystores + connection-string + config-json)) + ([connection-string config-json callback] + (log/info "[native-module] Sending Import Keypairs Connection String" + {:fn :input-connection-string-for-importing-keypairs-keystores + :config-json config-json + :connection-string connection-string}) + (.inputConnectionStringForImportingKeypairsKeystores ^js (network) + connection-string + config-json + callback))) + +(defn create-account-from-private-key + "Validate that a mnemonic conforms to BIP39 dictionary/checksum standards" + ([private-key] + (native-utils/promisify-native-module-call create-account-from-private-key private-key)) + ([private-key callback] + (log/debug "[native-module] create-account-from-private-key") + (.createAccountFromPrivateKey ^js (account-manager) + (types/clj->json {:privateKey private-key}) + callback))) + +(defn intended-panic + [message] + (.intendedPanic ^js (status) message)) + +(defn set-profile-log-level + [level] + (log/debug "[native-module] set-profile-log-level") + (.setProfileLogLevel ^js (log-manager) (types/clj->json {:logLevel level}))) + +(defn set-profile-log-enabled + [enabled] + (log/debug "[native-module] set-profile-log-enabled") + (.setProfileLogEnabled ^js (log-manager) (types/clj->json {:enabled enabled}))) + +(defn set-pre-login-log-level + [level] + (log/debug "[native-module] set-pre-login-log-level") + (.setPreLoginLogLevel ^js (log-manager) (types/clj->json {:logLevel level}))) + +(defn set-pre-login-log-enabled + [enabled] + (log/debug "[native-module] set-pre-login-log-enabled") + (.setPreLoginLogEnabled ^js (log-manager) (types/clj->json {:enabled enabled}))) + +(defn get-mmkv-storage-path + [] + (log/debug "[native-module] get-mmkv-storage-path") + (.getMMKVStoragePath ^js (utils))) diff --git a/src/native_module/push_notifications.cljs b/src/native_module/push_notifications.cljs new file mode 100644 index 00000000000..408cf0d97ea --- /dev/null +++ b/src/native_module/push_notifications.cljs @@ -0,0 +1,43 @@ +(ns native-module.push-notifications + (:require + ["react-native" :as react-native] + [taoensso.timbre :as log])) + +(defn push-notification + [] + (when (exists? (.-NativeModules react-native)) + (.-PushNotification ^js (.-NativeModules react-native)))) + +(defn present-local-notification + [opts] + (.presentLocalNotification ^js (push-notification) (clj->js opts))) + +(defn clear-message-notifications + [chat-id] + (.clearMessageNotifications ^js (push-notification) chat-id)) + +(defn clear-all-message-notifications + [] + (.clearAllMessageNotifications ^js (push-notification))) + +(defn create-channel + [{:keys [channel-id channel-name]}] + (.createChannel ^js (push-notification) + #js {:channelId channel-id :channelName channel-name} + #(log/info "Notifications create channel:" %))) + +(defn enable-notifications + [] + (.enableNotifications ^js (push-notification))) + +(defn disable-notifications + [] + (.disableNotifications ^js (push-notification))) + +(defn add-listener + [event callback] + (.addListener ^js (.-DeviceEventEmitter ^js react-native) + event + (fn [^js data] + (when (and data (.-dataJSON data) callback) + (callback (.-dataJSON data)))))) diff --git a/src/native_module/utils.cljs b/src/native_module/utils.cljs new file mode 100644 index 00000000000..836184b7bc0 --- /dev/null +++ b/src/native_module/utils.cljs @@ -0,0 +1,23 @@ +(ns native-module.utils + (:require + [clojure.string :as string] + [promesa.core :as promesa] + [utils.transforms :as types])) + +(defn- promisify-callback + [res rej] + (fn [result] + (let [native-error (let [{:keys [error]} (types/json->clj result)] + (when-not (string/blank? error) + error))] + (if native-error + (rej (ex-info "Native module call error" {:error native-error})) + (res result))))) + +(defn promisify-native-module-call + [f & args] + (promesa/create + (fn [res rej] + (->> [(promisify-callback res rej)] + (concat args) + (apply f))))) diff --git a/src/quo/README.md b/src/quo/README.md new file mode 100644 index 00000000000..d12e15140c1 --- /dev/null +++ b/src/quo/README.md @@ -0,0 +1,252 @@ +# Quo Library + +Quo is the name of our mobile *component library* that implements the [Status +Design System for Mobile](https://www.figma.com/file/WQZcp6S0EnzxdTL4taoKDv/Design-System-for-Mobile). + +The overarching goals of having this component library are: + +- Achieve the highest possible fidelity between code and the design system in + Figma. +- Decouple components from ever-changing business requirements. + +> [!NOTE] +> This document captures our current practices and guidelines for implementing +> the design system. For guidelines that apply across the entire project, take +> a look at [new-guidelines](/doc/new-guidelines.md). + +## Directory structure and file names + +We follow one basic rule: mirror Figma *pages* and their *component names* in +the directory structure. + +For example, in the screenshot below we see the Figma page is `Banners` +and the component name is `Banner`. + + + +Therefore, the structure should look like: + +``` +quo/ +└── components/ + └── banners/ + └── banner/ + ├── component_spec.cljs + ├── style.cljs + └── view.cljs +``` + +Files `view.cljs`, `style.cljs`, and `component_spec.cljs` should always have +the same name, regardless of component. + +## Component API + +Adhere to the **same component properties and values** used in a Figma component +when translating it to Clojure. This means using the same names for props and +the same values. If the Figma property is a boolean, use a question mark suffix +to make the name more idiomatic in Clojure. + +We have found over time that the less we drift from the design system the +better. Some key benefits: + +- It helps developers quickly check for issues when comparing the code with the + source of truth in Figma. +- It is easier for pull-request reviewers to double-check components for + correctness. +- It helps developers create preview screens that are identical or very similar + to Figma, which aids in spotting bugs more easily. +- It helps designers review all component variations in preview screens. + + + +In the image above we can see the properties are `Type`, `State`, `Size`, +`Icon`, `Theme`, and `Background`. Translated to Clojure: + +```clojure +;; ns quo.components.buttons.button.view +(def view + [{:keys [type state size icon theme background]}] + ...) +``` + +### Handling Sizes +In the designs, sizes are referred to as integers. To avoid having the codebase littered with magic numbers we instead have a keyword convention to use in components to map these keywords with their sizes. + +The convention is `:size-`, e.g size `20` is `:size-20` + +```clojure +;; bad +(defn button + [{:keys [size]}] + [rn/view + {:style {:height (case size + 20 20 + 40 40 + 0)}}] + ...) +``` + +```clojure +;; good +(defn button + [{:keys [size]}] + [rn/view + {:style {:height (case size + :size-20 20 + :size-40 40 + 0)}}] + ...) +``` + +## Clojure var conventions + +- Due to the fact that every `view` namespace should export only one component + and to avoid the redundancy of `[some-component/some-component ...]`, name the + public var `view` as well. +- Try to make all other vars private because they should almost never be used + directly. + +## Default value when destructuring + +Too often callers pass nil values because values can be wrapped in a `when` for example. +In this case, the default value is not applied, because :or macro will use default only when the value is absent. +Instead, use `(or (:value props) some-default-value)` in a `let` expression or as a parameter value. + +```clojure +;; bad (unreliable) +(defn- view-internal + [{:keys [auto-focus? + init-value + return-key-type] + :or {auto-focus? false + init-value 0 + return-key-type :done}}] + ...) + +;; good +(defn- view-internal + [{:keys [theme size something] :as props}] + (let [auto-focus? (or (:auto-focus? props) false) + init-value (or (:init-value props) 0) + return-key-type (or (:return-key-type props) :done)] + ...)) +``` + +## Component tests + +We don't attempt to write component tests verifying how components look on the +screen. Instead, we have found a middle ground, where the focus is on verifying +if events are triggered as intended and that all component variations are +rendered. We use [React Native Testing Library](https://callstack.github.io/react-native-testing-library/). + +There are dozens of examples in the repository, so use them as a reference. A +good and complete example is [quo.components.avatars.user-avatar.component-spec](/src/quo/components/avatars/user_avatar/component_spec.cljs) + +### No-props test + +When writing tests for the component that has props, please add one test that covers situation when props aren't passed. Because even if component not showing anything meaningful without props, it shouldn't crash. + +```clojure +(h/describe "Transaction Progress" + (h/test "component renders without props" + (h/render [quo/transaction-progress {}]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) +``` + +## Do not couple the library with re-frame + +Don't use re-frame inside this library (e.g. dispatch & subscribe). If a +component needs to be stateful, the state should be local to its rendering +lifecycle (using `reagent.core/atom`). Additionally, if the component requires +any other data, it should be passed as arguments. + +```clojure +;; bad +(defn view [] + (let [window-width (rf/sub [:dimensions/window-width])] + [rn/pressable {:on-press #(rf/dispatch [:do-xyz])} + (do-something window-width)])) + +;; good +(defn view [{:keys [window-width on-press]}] + [rn/pressable {:on-press on-press} + (do-something window-width)]) +``` + +## Themes + +Our goal is to make all design system components *themeable*, which means they +should not use, nor fallback to the OS theme, because themes are *contextual* +and can be overridden in specific parts of the app. + +To achieve this, use the higher-order function `quo.context/with-theme` to +automatically inject the current theme context (based on the [React Context +API](https://react.dev/learn/passing-data-deeply-with-context)). + +Use the following pattern: + +```clojure +(ns quo.components...view + (:require [quo.context :as quo.context])) + +(defn- view-internal [{:keys [theme]}] + ...) + +(def view (quo.context/with-theme view-internal)) +``` + +Then pass the `theme` value down to all functions that may rely on the OS theme, +like `quo.foundations.colors/theme-colors` or `quo.foundations.shadows/get`. + +## Avoid using quo's version number in namespace aliases + +When requiring quo namespaces, don't use the version number in the +[alias](https://clojure.org/guides/learn/namespaces#_require), unless for a +special reason you need to require both the old and new namespaces in the same +file. + +> [!NOTE] +> Keep in mind that, at the moment, we need to keep both `src/quo/` and +> `src/quo/` directories in the repository, but eventually the old one will go +> away and the version number will lose its meaning. + +```clojure +;; bad +(ns ... + (require [quo.context :as quo.context] + [quo.core :as quo])) + +;; good +(ns ... + (require [quo.context :as quo.context] + [quo.core :as quo])) +``` + +## Preview screens + +Every component should be accompanied by a preview screen in +`src/status_im/contexts/quo_preview/`. Ideally, **all possible variations in +Figma should be achievable in the preview screen** by changing the input values +without resorting to code changes. Designers will also use this capability to +review components in PR builds. + +## Allow outermost containers to have their styles overridden + +If a component needs to be wrapped in a `rn/view` instance to force it to be +styled differently, consider changing the component to accept a +`container-style` argument. This will help reduce the number of nodes to be +rendered. + +```clojure +;; bad +[rn/view {:style {:margin-right 12}} + [quo/button + {:size 32} + :i/info]] + +;; good +[quo/button + {:size 32 + :container-style {:margin-right 12}} + :i/info] +``` diff --git a/src/quo/components/animated_header_flatlist/style.cljs b/src/quo/components/animated_header_flatlist/style.cljs new file mode 100644 index 00000000000..bfc5f346ab4 --- /dev/null +++ b/src/quo/components/animated_header_flatlist/style.cljs @@ -0,0 +1,79 @@ +(ns quo.components.animated-header-flatlist.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated])) + +(defn container-view + [view-height] + {:position :absolute + :top 0 + :left 0 + :right 0 + ;; height must be set, otherwise list will not scroll + :height view-height}) + +(defn button-container + [{:keys [left right theme]}] + (merge + {:width 32 + :height 32 + :border-radius 10 + :justify-content :center + :align-items :center + :background-color (colors/theme-colors colors/white-opa-40 colors/neutral-80-opa-40 theme) + :position :absolute + :top 56 + :z-index 3 + :left left + :right right})) + +(defn blur-view + [animation theme] + (reanimated/apply-animations-to-style + {:opacity animation} + {:position :absolute + :top 0 + :left 0 + :right 0 + :height 100 + :z-index 2 + :overflow :hidden + :background-color (when platform/android? + (colors/theme-colors colors/white colors/neutral-80 theme))})) + +(defn entity-picture + [animation theme] + (reanimated/apply-animations-to-style + {:width animation + :height animation} + {:transform [{:scale 1}] + :border-radius 40 + :position :absolute + :bottom 42 + :left 20 + :justify-content :center + :align-items :center + :background-color (colors/theme-colors colors/white colors/neutral-100 theme) + :overflow :hidden})) + +(defn header-bottom-part + [animation theme] + (reanimated/apply-animations-to-style + {:border-top-right-radius animation + :border-top-left-radius animation} + {:position :absolute + :bottom 0 + :height 86 + :left 0 + :right 0 + :background-color (colors/theme-colors colors/white colors/neutral-100 theme)})) + +(defn header-comp + [y-animation opacity-animation] + (reanimated/apply-animations-to-style + ;; here using `left` won't work on Android, so we are using `translateX` + {:transform [{:translateX (reanimated/use-shared-value 64)} {:translateY y-animation}] + :opacity opacity-animation} + {:position :absolute + :z-index 3})) diff --git a/src/quo/components/animated_header_flatlist/view.cljs b/src/quo/components/animated_header_flatlist/view.cljs new file mode 100644 index 00000000000..0b3281d93b8 --- /dev/null +++ b/src/quo/components/animated_header_flatlist/view.cljs @@ -0,0 +1,100 @@ +(ns quo.components.animated-header-flatlist.view + (:require + [oops.core :as oops] + [quo.components.animated-header-flatlist.style :as style] + [quo.components.icon :as icon] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent])) + +(def header-height 234) +(def cover-height 192) +(def blur-view-height 100) +(def threshold (- header-height blur-view-height)) + +(defn interpolate + [value input-range output-range] + (reanimated/interpolate value + input-range + output-range + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"})) + +(defn scroll-handler + [event initial-y scroll-y] + (let [current-y (- (oops/oget event "nativeEvent.contentOffset.y") initial-y)] + (reanimated/set-shared-value scroll-y current-y))) + +(defn header + [{:keys [theme-color f-display-picture-comp cover-uri title-comp theme]} top-inset scroll-y] + (let [input-range [0 (* threshold 0.33)] + picture-scale-down 0.4 + size-animation (interpolate scroll-y input-range [80 (* 80 picture-scale-down)]) + image-animation (interpolate scroll-y input-range [72 (* 72 picture-scale-down)]) + border-animation (interpolate scroll-y input-range [12 0])] + [rn/view + {:style {:height header-height + :background-color (or theme-color + (colors/theme-colors colors/white colors/neutral-95 theme)) + :margin-top (when platform/ios? (- top-inset))}} + (when cover-uri + [fast-image/fast-image + {:style {:width "100%" + :height cover-height} + :source {:uri cover-uri}}]) + [reanimated/view {:style (style/header-bottom-part border-animation theme)} + [title-comp]] + [reanimated/view {:style (style/entity-picture size-animation theme)} + [:f> f-display-picture-comp image-animation]]])) + +(defn- f-animated-header-list + [{:keys [header-comp main-comp back-button-on-press] :as params}] + (let [theme (quo.context/use-theme) + window-height (:height (rn/get-window)) + {:keys [top bottom]} safe-area/insets + ;; view height calculation is different because window height is different on iOS and + ;; Android: + view-height (if platform/ios? + (- window-height bottom) + (+ window-height top)) + initial-y (if platform/ios? (- top) 0) + scroll-y (reanimated/use-shared-value initial-y) + opacity-animation (interpolate scroll-y + [(* threshold 0.33) (* threshold 0.66)] + [0 1]) + translate-animation (interpolate scroll-y [(* threshold 0.66) threshold] [100 56]) + title-opacity-animation (interpolate scroll-y [(* threshold 0.66) threshold] [0 1])] + [rn/view {:style (style/container-view view-height)} + [rn/touchable-opacity + {:active-opacity 1 + :on-press back-button-on-press + :style (style/button-container {:left 20 :theme theme})} + [icon/icon :i/arrow-left {:size 20 :color (colors/theme-colors colors/black colors/white theme)}]] + [rn/touchable-opacity + {:active-opacity 1 + :style (style/button-container {:right 20 :theme theme})} + [icon/icon :i/options {:size 20 :color (colors/theme-colors colors/black colors/white theme)}]] + [reanimated/blur-view + {:blurAmount 32 + :blurType :light + :overlayColor (if platform/ios? colors/white-opa-70 :transparent) + :style (style/blur-view opacity-animation theme)} + [reanimated/view {:style (style/header-comp translate-animation title-opacity-animation)} + [header-comp]]] + [reanimated/flat-list + {:data [nil] + :render-fn main-comp + :key-fn str + :header (reagent/as-element (header (assoc params :theme theme) top scroll-y)) + ;; TODO: https://github.com/status-im/status-mobile/issues/14924 + :scroll-event-throttle 8 + :on-scroll (fn [event] (scroll-handler event initial-y scroll-y))}]])) + +(defn animated-header-list + [params] + [:f> f-animated-header-list params]) diff --git a/src/quo/components/avatars/account_avatar/component_spec.cljs b/src/quo/components/avatars/account_avatar/component_spec.cljs new file mode 100644 index 00000000000..f63de93c59e --- /dev/null +++ b/src/quo/components/avatars/account_avatar/component_spec.cljs @@ -0,0 +1,91 @@ +(ns quo.components.avatars.account-avatar.component-spec + (:require + [quo.components.avatars.account-avatar.style :as style] + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(h/describe "Account Avatar" + (h/test "default render" + (h/render [account-avatar/view]) + (h/is-truthy (h/query-by-label-text :account-avatar)) + (h/is-truthy (h/query-by-label-text :account-emoji))) + + (h/test "with emoji" + (let [emoji "💸"] + (h/render [account-avatar/view {:emoji emoji :size 80}]) + (h/is-truthy (h/query-by-label-text :account-avatar)) + (h/is-truthy (h/query-by-label-text :account-emoji)) + (h/is-truthy (h/query-by-text emoji)))) + + (h/test "size 80 with emoji, with type - default" + (let [opts {:emoji "🏝️" + :size 80 + :type :default + :customization-color :blue}] + (h/render [account-avatar/view opts]) + (h/is-truthy (h/query-by-label-text :account-avatar)) + (h/has-style (h/get-by-label-text :account-avatar) + {:height (:size opts) + :width (:size opts) + :borderRadius (style/get-border-radius (:size opts)) + :backgroundColor (colors/resolve-color (:customization-color opts) :light)}) + (h/is-truthy (h/query-by-label-text :account-emoji)) + (h/has-style (h/query-by-label-text :account-emoji) + {:fontSize (style/get-emoji-size (:size opts))}) + (h/is-truthy (h/query-by-text (:emoji opts))))) + + (h/test "size 48 with emoji, with type - watch only" + (let [opts {:emoji "💵" + :size 48 + :type :watch-only + :customization-color :purple}] + (h/render [account-avatar/view opts]) + (h/is-truthy (h/query-by-label-text :account-avatar)) + (h/has-style + (h/get-by-label-text :account-avatar) + {:height (:size opts) + :width (:size opts) + :borderRadius (style/get-border-radius (:size opts)) + :borderWidth 1 + :backgroundColor (colors/resolve-color (:customization-color opts) :light 10)}) + (h/is-truthy (h/query-by-label-text :account-emoji)) + (h/has-style (h/query-by-label-text :account-emoji) + {:fontSize (style/get-emoji-size (:size opts))}) + (h/is-truthy (h/query-by-text (:emoji opts))))) + + (h/test "size 28 with emoji, with type - default" + (let [opts {:emoji "🏝️" + :size 28 + :type :default + :customization-color :turquoise}] + (h/render [account-avatar/view opts]) + (h/is-truthy (h/query-by-label-text :account-avatar)) + (h/has-style (h/get-by-label-text :account-avatar) + {:height (:size opts) + :width (:size opts) + :borderRadius (style/get-border-radius (:size opts)) + :backgroundColor (colors/resolve-color (:customization-color opts) :light)}) + (h/is-truthy (h/query-by-label-text :account-emoji)) + (h/has-style (h/query-by-label-text :account-emoji) + {:fontSize (style/get-emoji-size (:size opts))}) + (h/is-truthy (h/query-by-text (:emoji opts))))) + + (h/test "size 16 with emoji, with type - watch only" + (let [opts {:emoji "🎉" + :size 16 + :type :watch-only + :customization-color :copper}] + (h/render [account-avatar/view opts]) + (h/is-truthy (h/query-by-label-text :account-avatar)) + (h/has-style + (h/get-by-label-text :account-avatar) + {:height (:size opts) + :width (:size opts) + :borderRadius (style/get-border-radius (:size opts)) + :borderWidth 0.8 + :backgroundColor (colors/resolve-color (:customization-color opts) :light 10)}) + (h/is-truthy (h/query-by-label-text :account-emoji)) + (h/has-style (h/query-by-label-text :account-emoji) + {:fontSize (style/get-emoji-size (:size opts))}) + (h/is-truthy (h/query-by-text (:emoji opts)))))) diff --git a/src/quo/components/avatars/account_avatar/style.cljs b/src/quo/components/avatars/account_avatar/style.cljs new file mode 100644 index 00000000000..25d87527cb7 --- /dev/null +++ b/src/quo/components/avatars/account_avatar/style.cljs @@ -0,0 +1,78 @@ +(ns quo.components.avatars.account-avatar.style + (:require + [quo.foundations.colors :as colors])) + +(def default-size 80) +(def default-border-radius 16) +(def default-padding 16) +(def default-emoji-size 36) + +(def ^:private container-size + {:size-64 64}) + +(defn get-border-radius + [size] + (case size + 80 16 + :size-64 16 + 48 12 + 32 10 + 28 8 + 24 8 + 20 6 + 16 4 + default-border-radius)) + +(defn get-padding + [size] + (case size + 80 16 + 48 8 + 32 6 + 28 6 + 24 6 + 20 4 + 16 2 + default-padding)) + +(defn get-emoji-size + [size] + (case size + 80 36 + :size-64 30 + 48 24 + 32 15 + 28 12 + 24 11 + 20 11 + 16 11 + default-emoji-size)) + +(defn get-border-width + [size] + (if (= size 16) + 0.8 ;; 0.8 px is for only size 16 + 1)) ;; Rest of the size will have 1 px + + +(defn root-container + [{:keys [type size customization-color] + :or {size default-size + customization-color :blue}} + theme] + (let [watch-only? (= type :watch-only) + width (cond-> size + (keyword? size) (container-size size))] + (cond-> {:width width + :height width + :background-color (colors/resolve-color customization-color theme) + :border-radius (get-border-radius size) + :border-color (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + :padding (get-padding size) + :align-items :center + :justify-content :center} + + watch-only? + (assoc :border-width (get-border-width size) + :background-color (colors/resolve-color customization-color theme 10))))) + diff --git a/src/quo/components/avatars/account_avatar/view.cljs b/src/quo/components/avatars/account_avatar/view.cljs new file mode 100644 index 00000000000..4073da98756 --- /dev/null +++ b/src/quo/components/avatars/account_avatar/view.cljs @@ -0,0 +1,34 @@ +(ns quo.components.avatars.account-avatar.view + (:require + [clojure.string :as string] + [quo.components.avatars.account-avatar.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + "Opts: + + :type - keyword -> :default/:watch-only + + :emoji - string -> 🍑 [default] + + :size - number -> 80 [default] /48/32/28/24/20/16 + + :customization-color - keyword or hexstring -> :blue/:army/... or #ABCEDF + + :theme - keyword -> :light/:dark" + [{:keys [size emoji] + :or {size style/default-size + emoji "🍑"} + :as opts}] + (let [theme (quo.context/use-theme) + emoji-size (style/get-emoji-size size)] + [rn/view + {:accessible true + :accessibility-label :account-avatar + :style (style/root-container opts theme)} + [rn/text + {:accessibility-label :account-emoji + :adjusts-font-size-to-fit true + :style {:font-size emoji-size}} + (when emoji (string/trim emoji))]])) diff --git a/src/quo/components/avatars/channel_avatar/component_spec.cljs b/src/quo/components/avatars/channel_avatar/component_spec.cljs new file mode 100644 index 00000000000..50beb5ac7c6 --- /dev/null +++ b/src/quo/components/avatars/channel_avatar/component_spec.cljs @@ -0,0 +1,36 @@ +(ns quo.components.avatars.channel-avatar.component-spec + (:require + [quo.components.avatars.channel-avatar.view :as component] + [test-helpers.component :as h])) + +(h/describe "Channel Avatar" + (h/test "default render" + (h/render [component/view]) + (h/is-truthy (h/query-by-label-text :initials)) + (h/is-null (h/query-by-label-text :emoji)) + (h/is-null (h/query-by-label-text :lock))) + + (h/test "with emoji, no lock set, large size" + (let [emoji "🍓"] + (h/render [component/view {:emoji emoji :size :size-32}]) + (h/is-null (h/query-by-label-text :initials)) + (h/is-truthy (h/query-by-text emoji)) + (h/is-null (h/query-by-label-text :lock)))) + + (h/test "locked" + (h/render [component/view {:locked? true}]) + (h/is-truthy (h/query-by-label-text :lock))) + + (h/test "unlocked" + (h/render [component/view {:locked? false}]) + (h/is-truthy (h/query-by-label-text :lock))) + + (h/test "no emoji, smaller size" + (h/render [component/view {:full-name "Status Mobile"}]) + (h/is-truthy (h/query-by-text "S"))) + + (h/test "no emoji, large size" + (h/render [component/view + {:full-name "Status Mobile" + :size :size-32}]) + (h/is-truthy (h/query-by-text "SM")))) diff --git a/src/quo/components/avatars/channel_avatar/style.cljs b/src/quo/components/avatars/channel_avatar/style.cljs new file mode 100644 index 00000000000..5c4e0d354db --- /dev/null +++ b/src/quo/components/avatars/channel_avatar/style.cljs @@ -0,0 +1,42 @@ +(ns quo.components.avatars.channel-avatar.style + (:require + [quo.foundations.colors :as colors])) + +(def lock-icon-size 12) + +(defn outer-container + [{:keys [size customization-color theme]}] + (let [container-size (case size + :size-80 80 + :size-64 64 + :size-32 32 + 24)] + {:width container-size + :height container-size + :border-radius container-size + :justify-content :center + :align-items :center + :background-color (colors/resolve-color customization-color theme 10)})) + +(defn emoji-size + [size] + {:text-align :center + :font-size (case size + :size-80 36 + :size-64 24 + :size-32 15 + 11)}) + +(defn lock-container + [size theme] + (let [distance (if (= size :size-32) 20 12)] + {:position :absolute + :left distance + :top distance + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :border-radius (* 2 lock-icon-size) + :padding 2})) + +(def lock-icon + {:width lock-icon-size + :height lock-icon-size}) diff --git a/src/quo/components/avatars/channel_avatar/view.cljs b/src/quo/components/avatars/channel_avatar/view.cljs new file mode 100644 index 00000000000..49489e30eb6 --- /dev/null +++ b/src/quo/components/avatars/channel_avatar/view.cljs @@ -0,0 +1,72 @@ +(ns quo.components.avatars.channel-avatar.view + (:require + [clojure.string :as string] + [quo.components.avatars.channel-avatar.style :as style] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.string])) + +(defn- initials + [{:keys [full-name size customization-color theme]}] + (let [amount-initials (if (#{:size-32 :size-64 :size-80} size) 2 1) + channel-name (utils.string/safe-replace full-name "#" "")] + [text/text + (cond-> {:accessibility-label :initials + :style {:color (colors/resolve-color customization-color theme)} + :size :paragraph-2 + :weight :semi-bold} + (#{:size-64 :size-80} size) (assoc :size :heading-1 + :weight :medium)) + (utils.string/get-initials channel-name amount-initials)])) + +(defn- lock + [locked? size theme] + ;; When `locked?` is nil, we must not display the unlocked icon. + (when (boolean? locked?) + [rn/view + {:accessibility-label :lock + :style (style/lock-container size theme)} + [icons/icon (if locked? :i/locked :i/unlocked) + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :container-style style/lock-icon + :size 12}]])) + +(defn view + "Options: + + :size - keyword (default nil) - Container size, for the moment, + only :size/l (meaning large) is supported. + + :emoji - string (default nil) + + :customization-color - color (default nil) If the component is used for a + community channel, then the default color should be the community custom + color. + + :locked? - nil/bool (default nil) - When true/false display a locked/unlocked + icon respectively. When nil does not show icon. + + :full-name - string (default nil) - When :emoji is blank, this value will be + used to extract the initials. + " + [{:keys [size emoji customization-color locked? full-name]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :channel-avatar + :style (style/outer-container {:theme theme + :size size + :customization-color customization-color})} + (if (string/blank? emoji) + [initials + {:full-name full-name + :size size + :customization-color customization-color + :theme theme}] + [rn/text + {:style (style/emoji-size size) + :accessibility-label :emoji} + (when emoji (string/trim emoji))]) + [lock locked? size theme]])) diff --git a/src/quo/components/avatars/collection_avatar/component_spec.cljs b/src/quo/components/avatars/collection_avatar/component_spec.cljs new file mode 100644 index 00000000000..bbcf7a46131 --- /dev/null +++ b/src/quo/components/avatars/collection_avatar/component_spec.cljs @@ -0,0 +1,17 @@ +(ns quo.components.avatars.collection-avatar.component-spec + (:require + [quo.components.avatars.collection-avatar.view :as collection-avatar] + [quo.foundations.resources :as resources] + [test-helpers.component :as h])) + +(h/describe "collection avatar" + (h/describe "Profile picture" + (h/test "Doesn't crash without image" + (h/render + [collection-avatar/view]) + (h/is-truthy (h/get-by-label-text :collection-avatar))) + + (h/test "Renders with image" + (h/render + [collection-avatar/view {:image (resources/get-image :bored-ape)}]) + (h/is-truthy (h/get-by-label-text :collection-avatar))))) diff --git a/src/quo/components/avatars/collection_avatar/style.cljs b/src/quo/components/avatars/collection_avatar/style.cljs new file mode 100644 index 00000000000..51edd5c185c --- /dev/null +++ b/src/quo/components/avatars/collection_avatar/style.cljs @@ -0,0 +1,26 @@ +(ns quo.components.avatars.collection-avatar.style + (:require + [quo.foundations.colors :as colors])) + +(defn- get-dimensions + [size] + (case size + :size-32 32 + :size-24 24 + :size-20 20 + nil)) + +(defn collection-avatar-container + [theme size] + {:width (get-dimensions size) + :height (get-dimensions size) + :border-width 1 + :border-color (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :border-radius 6 + :justify-content :center + :align-items :center}) + +(defn collection-avatar + [size] + {:width (get-dimensions size) + :height (get-dimensions size)}) diff --git a/src/quo/components/avatars/collection_avatar/view.cljs b/src/quo/components/avatars/collection_avatar/view.cljs new file mode 100644 index 00000000000..531566700b6 --- /dev/null +++ b/src/quo/components/avatars/collection_avatar/view.cljs @@ -0,0 +1,22 @@ +(ns quo.components.avatars.collection-avatar.view + (:require + [quo.components.avatars.collection-avatar.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn view + "Opts: + + :image - collection image + :theme - keyword -> :light/:dark" + [{:keys [image size on-load-start on-load-end on-error] :or {size :size-24}}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/collection-avatar-container theme size)} + [fast-image/fast-image + {:accessibility-label :collection-avatar + :source image + :on-load-start on-load-start + :on-load-end on-load-end + :on-error on-error + :style (style/collection-avatar size)}]])) diff --git a/src/quo/components/avatars/community_avatar/component_spec.cljs b/src/quo/components/avatars/community_avatar/component_spec.cljs new file mode 100644 index 00000000000..485045f5dd4 --- /dev/null +++ b/src/quo/components/avatars/community_avatar/component_spec.cljs @@ -0,0 +1,9 @@ +(ns quo.components.avatars.community-avatar.component-spec + (:require [quo.components.avatars.community-avatar.view :as community-avatar] + [test-helpers.component :as h])) + +(h/describe "Avatars: Community Avatar" + (h/test "should render correctly" + (h/render-with-theme-provider + [community-avatar/view {:image "mock-image"}]) + (h/is-truthy (h/get-by-label-text :community-avatar)))) diff --git a/src/quo/components/avatars/community_avatar/style.cljs b/src/quo/components/avatars/community_avatar/style.cljs new file mode 100644 index 00000000000..5641dbe0f02 --- /dev/null +++ b/src/quo/components/avatars/community_avatar/style.cljs @@ -0,0 +1,12 @@ +(ns quo.components.avatars.community-avatar.style) + +(def ^:private sizes + {:size-32 32 + :size-24 24}) + +(defn image + [size] + (let [size-val (sizes size)] + {:border-radius (/ size-val 2) + :width size-val + :height size-val})) diff --git a/src/quo/components/avatars/community_avatar/view.cljs b/src/quo/components/avatars/community_avatar/view.cljs new file mode 100644 index 00000000000..1518efe0f2b --- /dev/null +++ b/src/quo/components/avatars/community_avatar/view.cljs @@ -0,0 +1,23 @@ +(ns quo.components.avatars.community-avatar.view + (:require [quo.components.avatars.community-avatar.style :as style] + [react-native.core :as rn] + [schema.core :as schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:size {:optional true} [:maybe [:enum :size-32 :size-24]]] + [:image :schema.common/image-source] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) + +(defn- view-internal + [{:keys [size image container-style]}] + [rn/image + {:source image + :accessibility-label :community-avatar + :style (merge (style/image size) container-style)}]) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/avatars/dapp_avatar/component_spec.cljs b/src/quo/components/avatars/dapp_avatar/component_spec.cljs new file mode 100644 index 00000000000..31459153e02 --- /dev/null +++ b/src/quo/components/avatars/dapp_avatar/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo.components.avatars.dapp-avatar.component-spec + (:require [quo.components.avatars.dapp-avatar.view :as dapp-avatar] + [test-helpers.component :as h])) + +(h/describe "Avatars: dApp Avatar" + (h/test "should render correctly without context" + (h/render-with-theme-provider + [dapp-avatar/view {:image "mock-image"}]) + (h/is-truthy (h/get-by-label-text :dapp-avatar))) + + (h/test "should render correctly with context" + (h/render-with-theme-provider + [dapp-avatar/view {:image "mock-image" :network-image "mock-image" :context? true}]) + (h/is-truthy (h/get-by-label-text :dapp-avatar)))) diff --git a/src/quo/components/avatars/dapp_avatar/style.cljs b/src/quo/components/avatars/dapp_avatar/style.cljs new file mode 100644 index 00000000000..01878b61acb --- /dev/null +++ b/src/quo/components/avatars/dapp_avatar/style.cljs @@ -0,0 +1,59 @@ +(ns quo.components.avatars.dapp-avatar.style) + +(defn- size->int + [size] + (case size + :size-32 32 + :size-64 64 + 32)) + +(defn container + [size] + (let [container-size (size->int size)] + {:height container-size + :width container-size})) + +(defn hole-view + [size] + (let [container-size (size->int size)] + {:width container-size + :height container-size})) + +(defn context + [size] + (let [context-size (case size + :size-32 16 + :size-64 20 + 20) + offset (case size + :size-32 -4 + :size-64 0 + -4)] + {:width context-size + :height context-size + :border-radius (/ context-size 2) + :position :absolute + :right offset + :bottom offset})) + +(defn context-hole + [size] + (let [context-hole-size (case size + :size-32 18 + :size-64 24 + 18) + offset (case size + :size-32 19 + :size-64 42 + 19)] + {:x offset + :y offset + :width context-hole-size + :height context-hole-size + :borderRadius (/ context-hole-size 2)})) + +(defn image + [size] + (let [container-size (size->int size)] + {:width container-size + :height container-size})) diff --git a/src/quo/components/avatars/dapp_avatar/view.cljs b/src/quo/components/avatars/dapp_avatar/view.cljs new file mode 100644 index 00000000000..5a4bcdeda51 --- /dev/null +++ b/src/quo/components/avatars/dapp_avatar/view.cljs @@ -0,0 +1,40 @@ +(ns quo.components.avatars.dapp-avatar.view + (:require [quo.components.avatars.dapp-avatar.style :as style] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [react-native.platform :as platform] + [schema.core :as schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:context? {:optional true} [:maybe :boolean]] + [:size {:optional true} [:maybe [:enum :size-32 :size-64]]] + [:image :schema.common/image-source] + [:network-image {:optional true} [:maybe :schema.common/image-source]] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) + +(defn- view-internal + [{:keys [context? size image network-image container-style]}] + [rn/view + {:style (-> (style/container size) + (merge container-style)) + :accessibility-label :dapp-avatar} + [hole-view/hole-view + (cond-> {:holes (if context? + [(style/context-hole size)] + []) + :style (style/hole-view size)} + platform/android? (assoc :key context?)) + [rn/image + {:source image + :style (style/image size)}]] + (when context? + [rn/image + {:source network-image + :style (style/context size)}])]) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/avatars/group_avatar.cljs b/src/quo/components/avatars/group_avatar.cljs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/quo/components/avatars/group_avatar/style.cljs b/src/quo/components/avatars/group_avatar/style.cljs new file mode 100644 index 00000000000..357bf27fbc9 --- /dev/null +++ b/src/quo/components/avatars/group_avatar/style.cljs @@ -0,0 +1,21 @@ +(ns quo.components.avatars.group-avatar.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [{:keys [container-size customization-color theme]}] + {:width container-size + :height container-size + :align-items :center + :justify-content :center + :border-radius (/ container-size 2) + :overflow :hidden + :background-color (colors/resolve-color customization-color theme)}) + +(defn avatar-identifier + [theme] + {:text-align :center + :font-size 36 + :color (colors/theme-colors colors/black + colors/white + theme)}) diff --git a/src/quo/components/avatars/group_avatar/view.cljs b/src/quo/components/avatars/group_avatar/view.cljs new file mode 100644 index 00000000000..654c526366c --- /dev/null +++ b/src/quo/components/avatars/group_avatar/view.cljs @@ -0,0 +1,55 @@ +(ns quo.components.avatars.group-avatar.view + (:require + [quo.components.avatars.group-avatar.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(def sizes + {:size-20 {:icon 12 + :container 20} + :size-28 {:icon 16 + :container 28} + :size-32 {:icon 16 + :container 32} + :size-48 {:icon 20 + :container 48} + :size-80 {:icon 32 + :container 80}}) + +(defn view + [{:keys [size customization-color picture icon-name emoji] + :or {size :size-20 + customization-color :blue + picture nil + icon-name :i/members}}] + (let [theme (quo.context/use-theme) + container-size (get-in sizes [size :container]) + icon-size (get-in sizes [size :icon])] + [rn/view + {:accessibility-label :group-avatar + :style (style/container {:container-size container-size + :customization-color customization-color + :theme theme})} + (if picture + [fast-image/fast-image + {:source picture + :style {:width container-size + :height container-size}}] + (cond + emoji + (if (= size :size-80) + [rn/text + {:style (style/avatar-identifier theme)} + emoji] + [text/text + {:size :paragraph-1 + :style (dissoc (style/avatar-identifier theme) :font-size)} + emoji]) + :else + [icon/icon icon-name + {:size icon-size + :color colors/white-opa-70}]))])) diff --git a/src/quo/components/avatars/icon_avatar.cljs b/src/quo/components/avatars/icon_avatar.cljs new file mode 100644 index 00000000000..361e6d33781 --- /dev/null +++ b/src/quo/components/avatars/icon_avatar.cljs @@ -0,0 +1,43 @@ +(ns quo.components.avatars.icon-avatar + (:require + [quo.components.icon :as icons] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(def ^:private sizes + {:size-48 {:component 48 + :icon 20} + :size-32 {:component 32 + :icon 20} + :size-24 {:component 24 + :icon 16} + :size-20 {:component 20 + :icon 12}}) + +(defn icon-avatar + [{:keys [size icon color opacity border? blur?] + :or {opacity 20 + size :size-32}}] + (let [theme (quo.context/use-theme) + {component-size :component icon-size :icon} (get sizes size) + circle-color (colors/resolve-color color theme opacity) + icon-color (colors/resolve-color color theme)] + (if (keyword? icon) + [rn/view + {:style {:width component-size + :height component-size + :border-radius component-size + :border-width (when border? 1) + :border-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)) + :background-color circle-color + :justify-content :center + :align-items :center}} + [icons/icon icon + {:size icon-size + :color icon-color}]] + [rn/image + {:source icon + :style {:width component-size :height component-size}}]))) diff --git a/src/quo/components/avatars/token_avatar/component_spec.cljs b/src/quo/components/avatars/token_avatar/component_spec.cljs new file mode 100644 index 00000000000..cd59cda7af7 --- /dev/null +++ b/src/quo/components/avatars/token_avatar/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo.components.avatars.token-avatar.component-spec + (:require [quo.components.avatars.token-avatar.view :as token-avatar] + [test-helpers.component :as h])) + +(h/describe "Avatars: Token Avatar" + (h/test "should render correctly without context" + (h/render-with-theme-provider + [token-avatar/view {:image "mock-image"}]) + (h/is-truthy (h/get-by-label-text :token-avatar))) + + (h/test "should render correctly with context" + (h/render-with-theme-provider + [token-avatar/view {:image "mock-image" :network-image "mock-image" :context? true}]) + (h/is-truthy (h/get-by-label-text :token-avatar)))) diff --git a/src/quo/components/avatars/token_avatar/style.cljs b/src/quo/components/avatars/token_avatar/style.cljs new file mode 100644 index 00000000000..c6b7d0801eb --- /dev/null +++ b/src/quo/components/avatars/token_avatar/style.cljs @@ -0,0 +1,25 @@ +(ns quo.components.avatars.token-avatar.style) + +(def ^:const size 32) + +(def container + {:height size + :width size}) + +(def hole-view + {:width size + :height size}) + +(def context + {:width 16 + :height 16 + :border-radius 8 + :position :absolute + :right -4 + :bottom -4}) + +(defn image + [type] + {:width size + :border-radius (if (= type :collectible) 8 0) + :height size}) diff --git a/src/quo/components/avatars/token_avatar/view.cljs b/src/quo/components/avatars/token_avatar/view.cljs new file mode 100644 index 00000000000..6f97695fb58 --- /dev/null +++ b/src/quo/components/avatars/token_avatar/view.cljs @@ -0,0 +1,47 @@ +(ns quo.components.avatars.token-avatar.view + (:require [quo.components.avatars.token-avatar.style :as style] + [quo.components.utilities.token.view :as token] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [react-native.platform :as platform] + [schema.core :as schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:type {:optional true} [:enum :asset :collectible]] + [:context? {:optional true} [:maybe :boolean]] + [:image {:optional true} [:maybe :schema.common/image-source]] + [:token {:optional true} [:maybe [:or :keyword :string]]] + [:network-image {:optional true} [:maybe :schema.common/image-source]] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) + +(defn- view-internal + [{:keys [type context? image token network-image container-style]}] + [rn/view + {:style (merge style/container container-style) + :accessibility-label :token-avatar} + [hole-view/hole-view + (cond-> {:holes (if context? + [{:x 19 + :y 19 + :width 18 + :height 18 + :borderRadius 9}] + []) + :style style/hole-view} + platform/android? (assoc :key context?)) + [token/view + {:size :size-32 + :token token + :style (style/image type) + :image-source image}]] + (when context? + [rn/image + {:source network-image + :style style/context}])]) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/avatars/user_avatar/component_spec.cljs b/src/quo/components/avatars/user_avatar/component_spec.cljs new file mode 100644 index 00000000000..5458be45c27 --- /dev/null +++ b/src/quo/components/avatars/user_avatar/component_spec.cljs @@ -0,0 +1,18 @@ +(ns quo.components.avatars.user-avatar.component-spec + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [test-helpers.component :as h])) + +(defonce mock-picture 1) + +(h/describe "user avatar" + (h/describe "Profile picture" + (h/test "Renders" + (h/render-with-theme-provider + [user-avatar/user-avatar {:profile-picture mock-picture}]) + (h/is-truthy (h/get-by-label-text :profile-picture))) + + (h/test "Renders even if `:full-name` is passed" + (h/render-with-theme-provider + [user-avatar/user-avatar {:profile-picture mock-picture}]) + (h/is-truthy (h/get-by-label-text :profile-picture))))) diff --git a/src/quo/components/avatars/user_avatar/schema.cljs b/src/quo/components/avatars/user_avatar/schema.cljs new file mode 100644 index 00000000000..9381ac01a91 --- /dev/null +++ b/src/quo/components/avatars/user_avatar/schema.cljs @@ -0,0 +1,19 @@ +(ns quo.components.avatars.user-avatar.schema + (:require + [quo.components.avatars.user-avatar.style :as style])) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:full-name {:optional true} [:maybe string?]] + [:size {:optional true} [:maybe (into [:enum] (keys style/sizes))]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:static? {:optional true} [:maybe boolean?]] + [:status-indicator? {:optional true} [:maybe boolean?]] + [:online? {:optional true} [:maybe boolean?]] + [:profile-picture + {:optional true} + [:maybe :schema.quo/profile-picture-source]]]]] + :any]) diff --git a/src/quo/components/avatars/user_avatar/style.cljs b/src/quo/components/avatars/user_avatar/style.cljs new file mode 100644 index 00000000000..9f049088d11 --- /dev/null +++ b/src/quo/components/avatars/user_avatar/style.cljs @@ -0,0 +1,80 @@ +(ns quo.components.avatars.user-avatar.style + (:require + [quo.foundations.colors :as colors])) + +(def sizes + {:big {:dimensions 80 + :status-indicator 12 + :status-indicator-border 4 + :status-indicator-center-to-edge 12 + :font-size :heading-1} + :size-64 {:dimensions 64 + :status-indicator 8 + :status-indicator-border 2 + :status-indicator-center-to-edge 6 + :font-size :heading-1} + :medium {:dimensions 48 + :status-indicator 8 + :status-indicator-border 2 + :status-indicator-center-to-edge 6 + :font-size :heading-2} + :small {:dimensions 32 + :status-indicator 8 + :status-indicator-border 2 + :status-indicator-center-to-edge 4 + :font-size :paragraph-2} + 28 {:dimensions 28 + :status-indicator 0 + :status-indicator-border 0 + :status-indicator-center-to-edge 0 + :font-size :paragraph-2} + :xs {:dimensions 24 + :status-indicator 0 + :status-indicator-border 0 + :status-indicator-center-to-edge 0 + :font-size :paragraph-2} + :xxs {:dimensions 20 + :status-indicator 0 + :status-indicator-border 0 + :status-indicator-center-to-edge 0 + :font-size :label} + :xxxs {:dimensions 16 + :status-indicator 0 + :status-indicator-border 0 + :status-indicator-center-to-edge 0 + :font-size :label}}) + +(defn initials-avatar + [size customization-color theme] + (let [dimensions (get-in sizes [size :dimensions])] + {:position :absolute + :top 0 + :left 0 + :width dimensions + :height dimensions + :border-radius dimensions + :justify-content :center + :align-items :center + :background-color (colors/resolve-color customization-color theme)})) + +(defn indicator-color + [theme] + {:online (colors/theme-colors colors/success-50 colors/success-60 theme) + :offline colors/neutral-40}) + +(defn outer + [size static-profile-picture?] + (let [dimensions (get-in sizes [size :dimensions]) + outer-style {:width dimensions + :height dimensions} + outer-style-with-border-radius (assoc outer-style :border-radius dimensions)] + (if static-profile-picture? + outer-style-with-border-radius + outer-style))) + +(defn customization-color + [color-id theme] + (colors/resolve-color color-id theme)) + +(def initials-avatar-text + {:color colors/white-opa-70}) diff --git a/src/quo/components/avatars/user_avatar/view.cljs b/src/quo/components/avatars/user_avatar/view.cljs new file mode 100644 index 00000000000..46e80a1f998 --- /dev/null +++ b/src/quo/components/avatars/user_avatar/view.cljs @@ -0,0 +1,86 @@ +(ns quo.components.avatars.user-avatar.view + (:require + [quo.components.avatars.user-avatar.schema :as component-schema] + [quo.components.avatars.user-avatar.style :as style] + [quo.components.common.no-flicker-image :as no-flicker-image] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [schema.core :as schema] + [utils.image-server :as image-server] + utils.string)) + +(defn initials-avatar + [{:keys [full-name size customization-color] + :or {customization-color :blue}}] + (let [theme (quo.context/use-theme) + font-size (get-in style/sizes [size :font-size]) + amount-initials (if (#{:xs :xxs :xxxs} size) 1 2)] + [rn/view + {:accessibility-label :initials-avatar + :style (style/initials-avatar size customization-color theme)} + [text/text + {:style style/initials-avatar-text + :size font-size + :weight :semi-bold} + (utils.string/get-initials full-name amount-initials)]])) + +(defn user-avatar-internal + "Render user avatar with `profile-picture`." + [{:keys [full-name size profile-picture static? status-indicator? online?] + :or {size :big + status-indicator? true + online? true} + :as props}] + (let [theme (quo.context/use-theme) + picture-config (:config profile-picture) + full-name (or full-name "Your Name") + ;; image generated with `profile-picture-fn` is round cropped + ;; no need to add border-radius for them + outer-styles (style/outer size (not picture-config)) + ;; Once image is loaded, fast image re-renders view with the help of reagent atom, + ;; But dynamic updates don't work when user-avatar is used inside hole-view + ;; https://github.com/status-im/status-mobile/issues/15553 + image-view (if static? no-flicker-image/image fast-image/fast-image) + font-size (get-in style/sizes [size :font-size]) + amount-initials (if (#{:xs :xxs :xxxs} size) 1 2) + sizes (get style/sizes size) + indicator-color (get (style/indicator-color theme) (if online? :online :offline))] + + [rn/view {:style outer-styles :accessibility-label :user-avatar} + (if (and full-name (not (or picture-config profile-picture))) + ;; this is for things that's not user-avatar but are currently using user-avatar to render + ;; the initials e.g. community avatar + [initials-avatar props] + [image-view + {:accessibility-label :profile-picture + :style outer-styles + :source + (cond picture-config + {:uri (image-server/get-image-uri + picture-config + {:length amount-initials + :full-name full-name + :font-size (:font-size (text/text-style {:size + font-size} + nil)) + :indicator-size (when status-indicator? + (:status-indicator sizes)) + :indicator-border (when status-indicator? + (:status-indicator-border sizes)) + :indicator-center-to-edge (when status-indicator? + (:status-indicator-center-to-edge sizes)) + :indicator-color indicator-color + :theme theme + :color (:color style/initials-avatar-text) + :size (:width outer-styles)})} + (:uri profile-picture) + profile-picture + + (number? profile-picture) + profile-picture + + :else {:uri profile-picture})}])])) + +(def user-avatar (schema/instrument #'user-avatar-internal component-schema/?schema)) diff --git a/src/quo/components/avatars/wallet_user_avatar/component_spec.cljs b/src/quo/components/avatars/wallet_user_avatar/component_spec.cljs new file mode 100644 index 00000000000..af14a7ea115 --- /dev/null +++ b/src/quo/components/avatars/wallet_user_avatar/component_spec.cljs @@ -0,0 +1,16 @@ +(ns quo.components.avatars.wallet-user-avatar.component-spec + (:require [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [test-helpers.component :as h])) + +(h/describe "wallet user avatar" + (h/describe "View internal" + (h/test "Renders by default even with no input parameters" + (h/render + [wallet-user-avatar/wallet-user-avatar {}]) + (h/is-truthy (h/query-by-label-text :wallet-user-avatar))) + + (h/test "Renders user’s initials when full name is provided" + (h/render + [wallet-user-avatar/wallet-user-avatar {:full-name "Jane Smith"}]) + (h/is-truthy (h/get-by-text "JS"))))) + diff --git a/src/quo/components/avatars/wallet_user_avatar/style.cljs b/src/quo/components/avatars/wallet_user_avatar/style.cljs new file mode 100644 index 00000000000..1d05d0fbf86 --- /dev/null +++ b/src/quo/components/avatars/wallet_user_avatar/style.cljs @@ -0,0 +1,28 @@ +(ns quo.components.avatars.wallet-user-avatar.style + (:require [quo.foundations.colors :as colors])) + +(defn- circle-color + [customization-color neutral? theme] + (if neutral? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/resolve-color customization-color theme 20))) + +(defn- text-color + [customization-color neutral? theme] + (if neutral? + (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + (colors/resolve-color customization-color theme))) + +(defn container + [circle-size customization-color neutral? theme] + {:width circle-size + :height circle-size + :border-radius circle-size + :text-align :center + :justify-content :center + :align-items :center + :background-color (circle-color customization-color neutral? theme)}) + +(defn text + [customization-color neutral? theme] + {:color (text-color customization-color neutral? theme)}) diff --git a/src/quo/components/avatars/wallet_user_avatar/view.cljs b/src/quo/components/avatars/wallet_user_avatar/view.cljs new file mode 100644 index 00000000000..226b7934b82 --- /dev/null +++ b/src/quo/components/avatars/wallet_user_avatar/view.cljs @@ -0,0 +1,60 @@ +(ns quo.components.avatars.wallet-user-avatar.view + (:require [clojure.string :as string] + [quo.components.avatars.wallet-user-avatar.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + utils.string)) + +(def properties + {:size-20 {:size 20 + :font-size :label + :font-weight :medium} + :size-24 {:size 24 + :font-size :label + :font-weight :semi-bold} + :size-32 {:size 32 + :font-size :paragraph-2 + :font-weight :semi-bold} + :size-48 {:size 48 + :font-size :paragraph-1 + :font-weight :semi-bold} + :size-64 {:size 64 + :font-size :paragraph-1 + :font-weight :medium} + :size-80 {:size 80 + :font-size :heading-1 + :font-weight :medium}}) + +(def smallest-possible (first (keys properties))) +(def second-smallest-possible (second (keys properties))) +(defn check-if-size-small + [size] + (or (= size smallest-possible) + (= size second-smallest-possible))) +(def biggest-possible (last (keys properties))) + +(defn wallet-user-avatar + "Options: + + :full-name - string (default: nil) - used to generate initials + :customization-color - keyword (default: nil) - color of the avatar + :size - keyword (default: last element of properties object) - size of the + avatar + :monospace? - boolean (default: false) - use monospace font + :lowercase? - boolean (default: false) - lowercase text + :neutral? - boolean (default: false) - use neutral colors variant" + [{:keys [full-name customization-color size monospace? lowercase? neutral? container-style] + :or {size biggest-possible}}] + (let [theme (quo.context/use-theme) + circle-size (:size (size properties)) + small? (check-if-size-small size) + initials (utils.string/get-initials full-name (if small? 1 2))] + [rn/view + {:style (merge (style/container circle-size customization-color neutral? theme) container-style)} + [text/text + {:accessibility-label :wallet-user-avatar + :size (:font-size (size properties)) + :weight (if monospace? :monospace (:font-weight (size properties))) + :style (style/text customization-color neutral? theme)} + (if (and initials lowercase?) (string/lower-case initials) initials)]])) diff --git a/src/quo/components/banners/alert_banner/component_spec.cljs b/src/quo/components/banners/alert_banner/component_spec.cljs new file mode 100644 index 00000000000..7163f5e5f49 --- /dev/null +++ b/src/quo/components/banners/alert_banner/component_spec.cljs @@ -0,0 +1,35 @@ +(ns quo.components.banners.alert-banner.component-spec + (:require + [quo.components.banners.alert-banner.view :as alert-banner] + [test-helpers.component :as h])) + +(h/describe "Alert Banner" + (h/test "Render without props is not throwing any error" + (h/render [alert-banner/view {}]) + (h/is-truthy (h/query-by-label-text :alert-banner))) + + (h/test "Button is not displayed when :action? prop is false" + (h/render [alert-banner/view {}]) + (h/is-falsy (h/query-by-label-text :button))) + + (h/test "Button is displayed when :action? prop is true" + (h/render [alert-banner/view {:action? true}]) + (h/is-truthy (h/query-by-label-text :button))) + + (h/test "Button text is displayed when :action? prop is true and :button-text prop is set" + (h/render [alert-banner/view + {:action? true + :button-text "button"}]) + (h/is-truthy (h/get-by-text "button"))) + + (h/test "Button is called when it's pressed and :action? prop is true" + (let [event (h/mock-fn)] + (h/render [alert-banner/view + {:action? true + :on-button-press event}]) + (h/fire-event :press (h/query-by-label-text :button)) + (h/was-called event))) + + (h/test "Text message is displayed :text prop is passed" + (h/render [alert-banner/view {:text "message"}]) + (h/is-truthy (h/get-by-text "message")))) diff --git a/src/quo/components/banners/alert_banner/schema.cljs b/src/quo/components/banners/alert_banner/schema.cljs new file mode 100644 index 00000000000..4e9e3414f44 --- /dev/null +++ b/src/quo/components/banners/alert_banner/schema.cljs @@ -0,0 +1,14 @@ +(ns quo.components.banners.alert-banner.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:action? {:optional true} [:maybe boolean?]] + [:text {:optional true} [:maybe string?]] + [:container-style {:optional true} [:maybe :map]] + [:button-text {:optional true} [:maybe string?]] + [:text-number-of-lines {:optional true} [:maybe number?]] + [:on-button-press {:optional true} [:maybe fn?]]]]] + :any]) diff --git a/src/quo/components/banners/alert_banner/style.cljs b/src/quo/components/banners/alert_banner/style.cljs new file mode 100644 index 00000000000..765cc672bd7 --- /dev/null +++ b/src/quo/components/banners/alert_banner/style.cljs @@ -0,0 +1,32 @@ +(ns quo.components.banners.alert-banner.style + (:require [quo.foundations.colors :as colors])) + +(defn alert-banner-container + [theme] + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(defn container + [container-style] + (merge + {:flex-direction :row + :align-items :center + :padding-horizontal 20 + :padding-vertical 16} + container-style)) + +(def content-container + {:flex 1 + :flex-direction :row}) + +(defn label + [theme] + {:color (colors/resolve-color :danger theme) + :margin-horizontal 4 + :flex 1 + :flex-wrap :wrap}) + +(def button-text + {:color colors/white}) + +(def icon + {:margin-top 2}) diff --git a/src/quo/components/banners/alert_banner/view.cljs b/src/quo/components/banners/alert_banner/view.cljs new file mode 100644 index 00000000000..0f7e6e6bf89 --- /dev/null +++ b/src/quo/components/banners/alert_banner/view.cljs @@ -0,0 +1,51 @@ +(ns quo.components.banners.alert-banner.view + (:require [quo.components.banners.alert-banner.schema :as component-schema] + [quo.components.banners.alert-banner.style :as style] + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [schema.core :as schema])) + +(defn- view-internal + [{:keys [action? text button-text text-number-of-lines container-style on-button-press]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :alert-banner + :style (style/alert-banner-container theme)} + [linear-gradient/linear-gradient + {:style (style/container container-style) + :start {:x 0 :y 0} + :end {:x 0 :y 1} + :colors [(colors/theme-colors + colors/danger-50-opa-5 + colors/danger-50-opa-10 + theme) + colors/danger-50-opa-0]} + [rn/view {:style style/content-container} + [icon/icon + :i/alert + {:color (colors/resolve-color :danger theme) + :size 16 + :container-style style/icon}] + [text/text + {:style (style/label theme) + :size :paragraph-2 + :number-of-lines (or text-number-of-lines 1)} + text]] + (when action? + [button/button + {:accessibility-label :button + :type :danger + :size 24 + :on-press on-button-press} + [text/text + {:style style/button-text + :size :paragraph-2 + :weight :medium} + button-text]])]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/banners/banner/component_spec.cljs b/src/quo/components/banners/banner/component_spec.cljs new file mode 100644 index 00000000000..4ffe30e7dc4 --- /dev/null +++ b/src/quo/components/banners/banner/component_spec.cljs @@ -0,0 +1,28 @@ +(ns quo.components.banners.banner.component-spec + (:require + ["@testing-library/react-native" :as rtl] + [quo.components.banners.banner.view :as banner] + [reagent.core :as reagent])) + +(defn render-banner + [opts] + (rtl/render (reagent/as-element [banner/view opts]))) + +(js/global.test "basic render of banner component" + (fn [] + (render-banner {:pins-count "5" + :latest-pin-text "this message"}) + (-> (js/expect (rtl/screen.getByText "this message")) + (.toBeTruthy)) + (-> (js/expect (rtl/screen.getByText "5")) + (.toBeTruthy)))) + +(js/global.test "banner component fires an event when pressed" + (let [mock-fn (js/jest.fn)] + (fn [] + (render-banner {:on-press mock-fn + :pins-count "5" + :latest-pin-text "this message"}) + (rtl/fireEvent.press (rtl/screen.getByText "this message")) + (-> (js/expect mock-fn) + (.toHaveBeenCalledTimes 1))))) diff --git a/src/quo/components/banners/banner/style.cljs b/src/quo/components/banners/banner/style.cljs new file mode 100644 index 00000000000..1d8f71e5405 --- /dev/null +++ b/src/quo/components/banners/banner/style.cljs @@ -0,0 +1,27 @@ +(ns quo.components.banners.banner.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:height 40 + :background-color (colors/resolve-color :blue theme 20) + :flex-direction :row + :align-items :center + :padding-right 22 + :padding-left 20 + :padding-vertical 10}) + +(def counter + {:flex 1 + :justify-content :center + :align-items :center}) + +(def icon + {:flex 1 + :margin-right 10}) + +(defn text + [hide-pin?] + {:flex (if hide-pin? 16 15) + :margin-right 10}) diff --git a/src/quo/components/banners/banner/view.cljs b/src/quo/components/banners/banner/view.cljs new file mode 100644 index 00000000000..3ee7ba4672d --- /dev/null +++ b/src/quo/components/banners/banner/view.cljs @@ -0,0 +1,33 @@ +(ns quo.components.banners.banner.view + (:require + [quo.components.banners.banner.style :as style] + [quo.components.counter.counter.view :as counter] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn view + [{:keys [hide-pin? latest-pin-text pins-count on-press]}] + (let [theme (quo.context/use-theme)] + (when (pos? pins-count) + [rn/touchable-opacity + {:accessibility-label :pinned-banner + :style (style/container theme) + :active-opacity 1 + :on-press on-press} + (when-not hide-pin? + [rn/view {:style style/icon} + [icons/icon :i/pin + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :size 20}]]) + [rn/view {:style (style/text hide-pin?)} + [text/text + {:number-of-lines 1 + :size :paragraph-2} + latest-pin-text]] + [rn/view + {:accessibility-label :pins-count + :style style/counter} + [counter/view {:type :secondary} pins-count]]]))) diff --git a/src/quo/components/blur/view.cljs b/src/quo/components/blur/view.cljs new file mode 100644 index 00000000000..6c70256cc9c --- /dev/null +++ b/src/quo/components/blur/view.cljs @@ -0,0 +1,19 @@ +(ns quo.components.blur.view + (:require [quo.context] + [quo.foundations.colors :as colors] + [react-native.blur :as blur] + [react-native.core :as rn] + [react-native.platform :as platform])) + +(defn- view-android + ([props] (view-android props nil)) + ([{:keys [style]} child] + (let [theme (quo.context/use-theme)] + [rn/view + {:style [style + {:pointer-events :box-none + :background-color (or (:background-color style) + (colors/theme-colors colors/white colors/neutral-80 theme))}]} + child]))) + +(def view (if platform/ios? blur/view view-android)) diff --git a/src/quo/components/browser/browser_input/component_spec.cljs b/src/quo/components/browser/browser_input/component_spec.cljs new file mode 100644 index 00000000000..16ed4d75866 --- /dev/null +++ b/src/quo/components/browser/browser_input/component_spec.cljs @@ -0,0 +1,59 @@ +(ns quo.components.browser.browser-input.component-spec + (:require + [quo.components.browser.browser-input.view :as browser-input] + [test-helpers.component :as h])) + +(h/describe "Browser input" + (h/test "Renders empty in default state" + (h/render [browser-input/view + {:on-change-text (h/mock-fn) + :value ""}]) + (h/is-truthy (h/get-by-label-text :browser-input))) + + (h/test "On change text is fired" + (let [on-change-text (h/mock-fn)] + (h/render [browser-input/view + {:on-change-text on-change-text + :value "mock text"}]) + (h/fire-event :change-text (h/get-by-label-text :browser-input) "mock-text-new") + (h/was-called on-change-text))) + + (h/describe "Input Label" + (h/test "Doesn't render label text when input is focused" + (h/render [browser-input/view + {:auto-focus true}]) + (h/is-null (h/query-by-label-text :browser-input-label))) + + (h/test "Renders label text when input has a value and input is not focused" + (h/render [browser-input/view + {:default-value "mock default"}]) + (h/is-truthy (h/query-by-label-text :browser-input-label))) + + (h/test "Renders site favicon when specified" + (h/render [browser-input/view + {:default-value "mock default" :favicon :i/verified}]) + (h/is-truthy (h/query-by-label-text :browser-input-favicon))) + + (h/test "Renders lock icon when lock is enabled" + (h/render [browser-input/view + {:default-value "mock default" :locked? true}]) + (h/is-truthy (h/query-by-label-text :browser-input-locked-icon)))) + + (h/describe "Clear button" + (h/test "Doesn't render in default state" + (h/render [browser-input/view]) + (h/is-null (h/query-by-label-text :browser-input-clear-button))) + + (h/test "Renders when there is a value" + (h/render [browser-input/view + {:default-value "mock text"}]) + (h/is-truthy (h/query-by-label-text :browser-input-clear-button))) + + (h/test "Is pressable" + (let [on-clear (h/mock-fn)] + (h/render [browser-input/view + {:default-value "mock text" + :on-clear on-clear}]) + (h/is-truthy (h/query-by-label-text :browser-input-clear-button)) + (h/fire-event :press (h/query-by-label-text :browser-input-clear-button)) + (h/was-called on-clear))))) diff --git a/src/quo/components/browser/browser_input/style.cljs b/src/quo/components/browser/browser_input/style.cljs new file mode 100644 index 00000000000..4eb8b82a620 --- /dev/null +++ b/src/quo/components/browser/browser_input/style.cljs @@ -0,0 +1,59 @@ +(ns quo.components.browser.browser-input.style + (:require + [quo.components.markdown.text :as text] + [quo.foundations.colors :as colors])) + +(def clear-icon-container + {:align-items :center + :height 20 + :justify-content :center + :margin-left 8 + :top 4 + :width 20}) + +(def favicon-icon-container + {:margin-right 2}) + +(defn input + [disabled?] + (assoc (text/text-style {:size :paragraph-1 + :weight :regular} + nil) + :flex 1 + :min-height 36 + :min-width 120 + :opacity (if disabled? 0.3 1))) + +(def lock-icon-container + {:margin-left 2}) + +(defn active-container + [display?] + {:align-items :center + :flex-direction :row + :justify-content :center + :opacity (if display? 1 0)}) + +(def default-container + {:align-items :center + :bottom 0 + :flex-direction :row + :left 0 + :padding-horizontal 20 + :position :absolute + :right 0 + :top 0 + :z-index 10}) + +(defn text + [theme] + (assoc (text/text-style {:size :paragraph-1 + :weight :medium} + nil) + :color + (colors/theme-colors colors/neutral-100 colors/white theme))) + +(def root-container + {:height 60 + :padding-horizontal 20 + :padding-vertical 8}) diff --git a/src/quo/components/browser/browser_input/view.cljs b/src/quo/components/browser/browser_input/view.cljs new file mode 100644 index 00000000000..9b0b7ecdf62 --- /dev/null +++ b/src/quo/components/browser/browser_input/view.cljs @@ -0,0 +1,150 @@ +(ns quo.components.browser.browser-input.view + (:require + [clojure.string :as string] + [quo.components.browser.browser-input.style :as style] + [quo.components.icon :as icon] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform])) + +(defn remove-http-https-www + [value] + (string/replace value #"(https?://(www\.)?|http://)" "")) + +(defn clear-icon-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme))) + +(defn lock-icon-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn- clear-button + [{:keys [on-press blur? theme]}] + [rn/touchable-opacity + {:accessibility-label :browser-input-clear-button + :on-press on-press + :style style/clear-icon-container} + [icon/icon :i/clear + {:color (clear-icon-color blur? theme)}]]) + +(defn lock-icon + [{:keys [blur? theme]}] + [rn/view + [icon/icon :i/locked + {:accessibility-label :browser-input-locked-icon + :color (lock-icon-color blur? theme) + :container-style style/lock-icon-container + :size 16}]]) + +(defn cursor-color + [customization-color theme] + (colors/resolve-color customization-color theme)) + +(defn placeholder-color + [state blur? theme] + (cond + (and blur? (= state :active)) + (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + + blur? + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-30 theme) + + (= state :active) + (colors/theme-colors colors/neutral-30 colors/neutral-60 theme) + + :else + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))) + +(def ^:private props-to-remove + [:cursor-color :placeholder-text-color :editable :on-change-text :on-focus + :on-blur :on-clear :value :disabled? :blur? :customization-color :theme]) + +(defn view + [{:keys [disabled? blur? on-change-text customization-color + on-clear on-focus on-blur get-ref locked? + favicon favicon-color favicon-size default-value] + :or {default-value ""} + :as props}] + (let [ref (rn/use-ref-atom nil) + on-ref (rn/use-callback + (fn [r] + (reset! ref r) + (when get-ref (get-ref r))) + [get-ref]) + theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + [value set-value] (rn/use-state default-value) + on-clear (rn/use-callback + (fn [] + (.clear ^js @ref) + (set-value "") + (when on-clear (on-clear))) + [on-clear]) + focus-input (rn/use-callback + (fn [] + (set-state :active) + (.focus ^js @ref))) + on-blur (rn/use-callback + (fn [] + (set-state :default) + (when on-blur (on-blur))) + [on-blur]) + on-change-text (rn/use-callback + (fn [new-text] + (set-value new-text) + (when on-change-text (on-change-text new-text))) + [on-change-text]) + on-focus (rn/use-callback + (fn [] + (set-state :active) + (when on-focus (on-focus))) + [on-focus]) + clean-props (apply dissoc props props-to-remove)] + [rn/view {:style style/root-container} + (when (and (seq value) (= state :default)) + [rn/touchable-opacity + {:style style/default-container + :on-press focus-input} + (when favicon + [icon/icon favicon + {:accessibility-label :browser-input-favicon + :color favicon-color + :container-style style/favicon-icon-container + :size favicon-size}]) + [rn/text + {:accessibility-label :browser-input-label + :style (style/text theme)} + (remove-http-https-www value)] + (when locked? + [lock-icon {:blur? blur? :theme theme}])]) + [rn/view {:style (style/active-container (or (empty? value) (= state :active)))} + [rn/text-input + (merge + clean-props + {:accessibility-label :browser-input + :auto-capitalize :none + :auto-correct false + :cursor-color (cursor-color customization-color theme) + :editable (not disabled?) + :keyboard-appearance (colors/theme-colors :light :dark theme) + :keyboard-type :web-search + :on-blur on-blur + :on-change-text on-change-text + :on-focus on-focus + :placeholder-text-color (placeholder-color state blur? theme) + :ref on-ref + :selection-color (when platform/ios? + (cursor-color customization-color theme)) + :select-text-on-focus true + :style (style/input disabled?)})] + (when (seq value) + [clear-button + {:blur? blur? + :on-press on-clear + :theme theme}])]])) diff --git a/src/quo/components/buttons/button/component_spec.cljs b/src/quo/components/buttons/button/component_spec.cljs new file mode 100644 index 00000000000..a28d720d45f --- /dev/null +++ b/src/quo/components/buttons/button/component_spec.cljs @@ -0,0 +1,19 @@ +(ns quo.components.buttons.button.component-spec + (:require + [quo.components.buttons.button.view :as button] + [test-helpers.component :as h])) + +(h/describe "button tests" + (h/test "default render of button component" + (h/render [button/button {:accessibility-label "test-button"} ""]) + (h/is-truthy (h/get-by-label-text "test-button"))) + + (h/test "button renders with a label" + (h/render [button/button {} "test-label"]) + (h/is-truthy (h/get-by-text "test-label"))) + + (h/test "button on-press works" + (let [event (h/mock-fn)] + (h/render [button/button {:on-press event} "test-label"]) + (h/fire-event :press (h/get-by-text "test-label")) + (h/was-called event)))) diff --git a/src/quo/components/buttons/button/properties.cljs b/src/quo/components/buttons/button/properties.cljs new file mode 100644 index 00000000000..2ebc286f6be --- /dev/null +++ b/src/quo/components/buttons/button/properties.cljs @@ -0,0 +1,102 @@ +(ns quo.components.buttons.button.properties + (:require + [quo.foundations.colors :as colors])) + +(def backgrounds #{:photo :blur}) + +;; Note: We hardcode the :light theme for the background-color and apply an overlay when necessary, such +;; as for dark themes and pressed states. This approach is taken because, for communities, we only have +;; one color available, which is the light theme version. +;; For more information, see the related issue: https://github.com/status-im/status-mobile/issues/16396 +(defn- custom-color-type + [customization-color icon-only?] + {:icon-color colors/white-opa-70 + :label-color colors/white + :background-color (colors/resolve-color customization-color :light) + :border-radius (when icon-only? 24) + :overlay-customization-color customization-color}) + +(defn- grey-photo + [theme pressed?] + {:icon-color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-70 theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :blur-overlay-color (if pressed? + (colors/theme-colors colors/white-opa-50 + colors/neutral-80-opa-50 + theme) + (colors/theme-colors colors/white-opa-40 + colors/neutral-80-opa-40 + theme)) + :blur-type (if (= theme :light) :light :dark)}) + +(defn- grey-blur + [theme pressed?] + {:icon-color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-70 theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :background-color (if pressed? + (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme))}) + +(defn- outline-blur + [theme pressed?] + {:icon-color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-color (if pressed? + (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme))}) + +(defn- grey + [theme pressed?] + {:icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :background-color (if pressed? + (colors/theme-colors colors/neutral-20 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-10 + colors/neutral-90 + theme))}) + +(defn- dark-grey + [theme pressed?] + {:icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :background-color (if pressed? + (colors/theme-colors colors/neutral-30 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-20 colors/neutral-90 theme))}) + +(defn- outline + [theme pressed?] + {:icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-color (if pressed? + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-30 colors/neutral-70 theme))}) + +(defn- ghost + [theme pressed?] + {:icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :background-color (when pressed? + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) + +(defn- black + [pressed?] + {:label-color colors/white + :background-color (if pressed? colors/neutral-80 colors/neutral-95)}) + +(defn get-values + [{:keys [customization-color theme type background pressed? icon-only?]}] + (let [customization-color (get {:primary customization-color + :positive :success + :danger :danger} + type)] + (cond + (contains? #{:primary :positive :danger} type) (custom-color-type customization-color + icon-only?) + (and (= :photo background) (= type :grey)) (grey-photo theme pressed?) + (and (= :blur background) (= type :grey)) (grey-blur theme pressed?) + (and (= :blur background) (= type :outline)) (outline-blur theme pressed?) + (= type :grey) (grey theme pressed?) + (= type :dark-grey) (dark-grey theme pressed?) + (= type :outline) (outline theme pressed?) + (= type :ghost) (ghost theme pressed?) + (= type :black) (black pressed?)))) diff --git a/src/quo/components/buttons/button/style.cljs b/src/quo/components/buttons/button/style.cljs new file mode 100644 index 00000000000..cfbbe2e079d --- /dev/null +++ b/src/quo/components/buttons/button/style.cljs @@ -0,0 +1,118 @@ +(ns quo.components.buttons.button.style + (:require [quo.foundations.colors :as colors])) + +(def blur-view + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0}) + +(defn icon-style + [{:keys [icon-container-size icon-container-rounded? + icon-size margin-left margin-right disabled?]}] + (cond-> (merge {:margin-left margin-left + :margin-right margin-right + :align-items :center + :justify-content :center + :opacity (when disabled? 0.3)}) + icon-container-size + (assoc :width icon-container-size :height icon-container-size) + icon-container-rounded? + (assoc :border-radius (/ (or icon-container-size icon-size) 2)))) + +(defn icon-left-icon-style + [{:keys [override-margins size icon-container-size icon-container-rounded? + icon-size disabled?]}] + (icon-style + {:margin-left (or (get override-margins :left) + (if (= size 40) 12 8)) + :margin-right (or (get override-margins :right) 4) + :icon-container-size icon-container-size + :icon-container-rounded? icon-container-rounded? + :icon-size icon-size + :disabled? disabled?})) + +(defn icon-right-icon-style + [{:keys [override-margins size icon-container-size icon-container-rounded? + icon-size disabled?]}] + (icon-style {:margin-left (or (get override-margins :left) 4) + :margin-right (or (get override-margins :right) + (if (= size 40) 12 8)) + :icon-container-size icon-container-size + :icon-container-rounded? icon-container-rounded? + :icon-size icon-size + :disabled? disabled?})) + +(defn shape-style-container + [size border-radius] + {:height size + :border-radius (if border-radius + border-radius + (case size + 56 12 + 40 12 + 32 10 + 24 8 + 12))}) + +(defn container-styles + [{:keys [size disabled? border-radius background-color border-color icon-only? icon-top + icon-left icon-right inner-style]}] + [{:height size + :align-items :center + :justify-content :center + :flex-direction (if icon-top :column :row) + :padding-horizontal (when-not (or icon-only? icon-left icon-right) + (case size + 56 (if border-color 10 11) + 40 16 + 32 12 + 24 7 + 16)) + :padding-left (when-not (or icon-only? icon-left) + (case size + 56 nil + 40 16 + 32 12 + 24 8 + 16)) + :padding-right (when-not (or icon-only? icon-right) + (case size + 56 nil + 40 16 + 32 12 + 24 8 + 16)) + :padding-top (when-not (or icon-only? icon-left icon-right) + (case size + 56 0 + 40 (if border-color 8 9) + 32 (if border-color 4 5) + 24 0 + (if border-color 8 9))) + :padding-bottom (when-not (or icon-only? icon-left icon-right) + (case size + 56 0 + 40 9 + 32 5 + 24 0 + 9)) + :overflow :hidden + :background-color (if disabled? (colors/alpha background-color 0.3) background-color) + :border-radius (if border-radius + border-radius + (case size + 56 12 + 40 12 + 32 10 + 24 8 + 12)) + :border-color border-color + :border-width (when border-color 1)} + (when icon-only? + {:width size}) + (when border-color + {:border-color border-color + :border-width 1}) + inner-style]) diff --git a/src/quo/components/buttons/button/view.cljs b/src/quo/components/buttons/button/view.cljs new file mode 100644 index 00000000000..fecf2e9229a --- /dev/null +++ b/src/quo/components/buttons/button/view.cljs @@ -0,0 +1,130 @@ +(ns quo.components.buttons.button.view + (:require + [quo.components.blur.view :as blur] + [quo.components.buttons.button.properties :as button-properties] + [quo.components.buttons.button.style :as style] + [quo.components.icon :as quo.icons] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.customization-colors :as customization-colors] + [react-native.core :as rn])) + +(defn button + "with label + [button opts \"label\"] + opts + {on-press callback + on-long-press callback + disabled? boolean + :type :primary/:positive/:grey/:dark-grey/:outline/:ghost/ + :danger/:black + background :photo/:blur or nil + :size 40 [default] /32/24/56 + :icon-only? true/false + :icon-top :icon-keyword + :icon-left :icon-keyword + :icon-right :icon-keyword} + :customization-color keyword or hexstring + :theme :light/:dark + only icon + [button {:icon-only? true} :i/close-circle]" + [{:keys [on-press on-long-press disabled? type background size icon-left icon-left-color icon-right + icon-right-color icon-top icon-top-color customization-color accessibility-label icon-only? + container-style inner-style pressed? on-press-in on-press-out allow-multiple-presses?] + :or {type :primary + size 40 + customization-color (if (= type :primary) :blue nil)}} + children] + (let [[pressed-state? set-pressed-state] (rn/use-state false) + theme (quo.context/use-theme) + {:keys [icon-color background-color label-color border-color blur-type + blur-overlay-color border-radius overlay-customization-color]} + (button-properties/get-values {:customization-color customization-color + :background background + :type type + :theme theme + :pressed? (if pressed? pressed? pressed-state?) + :icon-only? icon-only?}) + icon-size (when (= 24 size) 12) + on-press-in-cb (rn/use-callback + (fn [] + (set-pressed-state true) + (when on-press-in (on-press-in)))) + on-press-out-cb (rn/use-callback + (fn [] + (set-pressed-state nil) + (when on-press-out (on-press-out))))] + [rn/pressable + {:disabled (boolean disabled?) + :accessibility-label accessibility-label + :on-press-in on-press-in-cb + :on-press-out on-press-out-cb + :on-press on-press + :allow-multiple-presses? allow-multiple-presses? + :on-long-press on-long-press + :style [(style/shape-style-container size border-radius) container-style]} + [rn/view + {:style (style/container-styles {:size size + :disabled? disabled? + :border-radius border-radius + :background-color background-color + :border-color border-color + :icon-only? icon-only? + :icon-top icon-top + :icon-left icon-left + :icon-right icon-right + :inner-style inner-style})} + (when overlay-customization-color + [customization-colors/overlay + {:customization-color overlay-customization-color + :theme theme + :pressed? (if pressed? pressed? pressed-state?)}]) + (when (= background :photo) + [blur/view + {:blur-radius 20 + :blur-type blur-type + :overlay-color blur-overlay-color + :style style/blur-view}]) + (when icon-top + [rn/view + [quo.icons/icon icon-top + {:container-style {:margin-bottom 2 + :opacity (when disabled? 0.3)} + :color (or icon-top-color icon-color) + :size icon-size}]]) + (when icon-left + [rn/view + {:style (style/icon-left-icon-style + {:size size + :icon-size icon-size + :disabled? disabled?})} + [quo.icons/icon icon-left + {:color (or icon-left-color icon-color) + :size icon-size}]]) + [rn/view + (cond + icon-only? + [quo.icons/icon children + {:color label-color + :size icon-size}] + + (string? children) + [text/text + {:size (when (#{56 24} size) :paragraph-2) + :weight :medium + :number-of-lines 1 + :style {:color label-color + :opacity (when (and disabled? (= theme :dark)) 0.3)}} + children] + + (vector? children) + children)] + (when icon-right + [rn/view + {:style (style/icon-right-icon-style + {:size size + :icon-size icon-size + :disabled? disabled?})} + [quo.icons/icon icon-right + {:color (or icon-right-color icon-color) + :size icon-size}]])]])) diff --git a/src/quo/components/buttons/composer_button/component_spec.cljs b/src/quo/components/buttons/composer_button/component_spec.cljs new file mode 100644 index 00000000000..f1d8106db57 --- /dev/null +++ b/src/quo/components/buttons/composer_button/component_spec.cljs @@ -0,0 +1,39 @@ +(ns quo.components.buttons.composer-button.component-spec + (:require + [quo.components.buttons.composer-button.view :as composer-button] + [test-helpers.component :as h])) + +(h/describe "button tests" + (h/test "default render of composer button component" + (h/render [composer-button/view + {:icon :i/placeholder + :accessibility-label "test-button"}]) + (h/is-truthy (h/get-by-label-text "test-button"))) + + (h/test "button on-press works" + (let [event (h/mock-fn)] + (h/render [composer-button/view + {:icon :i/placeholder + :on-press event + :accessibility-label "test-button"}]) + (h/fire-event :press (h/get-by-label-text "test-button")) + (h/was-called event))) + + (h/test "button on-press disabled when disabled" + (let [event (h/mock-fn)] + (h/render [composer-button/view + {:icon :i/placeholder + :on-press event + :accessibility-label "test-button" + :disabled? true}]) + (h/fire-event :press (h/get-by-label-text "test-button")) + (h/was-not-called event))) + + (h/test "button on-long-press works" + (let [event (h/mock-fn)] + (h/render [composer-button/view + {:icon :i/placeholder + :on-long-press event + :accessibility-label "test-button"}]) + (h/fire-event :long-press (h/get-by-label-text "test-button")) + (h/was-called event)))) diff --git a/src/quo/components/buttons/composer_button/style.cljs b/src/quo/components/buttons/composer_button/style.cljs new file mode 100644 index 00000000000..0d7ebcd006b --- /dev/null +++ b/src/quo/components/buttons/composer_button/style.cljs @@ -0,0 +1,30 @@ +(ns quo.components.buttons.composer-button.style + (:require + [quo.foundations.colors :as colors])) + +(defn get-border-color + [{:keys [pressed? blur? theme]}] + (cond + (and pressed? blur?) (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + pressed? (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + blur? (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :else (colors/theme-colors colors/neutral-30 colors/neutral-70 theme))) + +(defn get-label-color + [{:keys [blur? theme]}] + (cond + blur? (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + :else (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn main + [{:keys [pressed? blur? theme disabled?]}] + {:border-color (get-border-color {:pressed? pressed? + :blur? blur? + :theme theme}) + :border-width 1 + :border-radius 10 + :width 32 + :height 32 + :justify-content :center + :align-items :center + :opacity (when disabled? 0.3)}) diff --git a/src/quo/components/buttons/composer_button/view.cljs b/src/quo/components/buttons/composer_button/view.cljs new file mode 100644 index 00000000000..2091597b630 --- /dev/null +++ b/src/quo/components/buttons/composer_button/view.cljs @@ -0,0 +1,26 @@ +(ns quo.components.buttons.composer-button.view + (:require + [quo.components.buttons.composer-button.style :as style] + [quo.components.icon :as quo.icons] + [quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [on-press on-long-press disabled? blur? icon accessibility-label container-style]}] + (let [[pressed? set-pressed] (rn/use-state false) + theme (quo.context/use-theme) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed nil))] + [rn/pressable + {:accessibility-label (or accessibility-label :composer-button) + :on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out + :on-long-press on-long-press + :disabled disabled? + :style (merge (style/main {:pressed? pressed? + :blur? blur? + :theme theme + :disabled? disabled?}) + container-style)} + [quo.icons/icon icon {:color (style/get-label-color {:blur? blur? :theme theme})}]])) diff --git a/src/quo/components/buttons/dynamic_button/style.cljs b/src/quo/components/buttons/dynamic_button/style.cljs new file mode 100644 index 00000000000..31f53b5ac30 --- /dev/null +++ b/src/quo/components/buttons/dynamic_button/style.cljs @@ -0,0 +1,39 @@ +(ns quo.components.buttons.dynamic-button.style) + +(defn container + [type] + {:margin-top 6 + :margin-bottom 6 + :margin-left (case type + :mention 8 + :notification-down 2 + :notification-up 2 + :search-with-label 8 + :search 6 + :scroll-to-bottom 6 + nil) + :margin-right (case type + :mention 2 + :notification-down 8 + :notification-up 8 + :search-with-label 4 + :search 6 + :scroll-to-bottom 6 + nil)}) + +(defn text + [type] + {:margin-top 2.5 + :margin-bottom 3.5 + :margin-left (case type + :mention 0 + :notification-down 8 + :notification-up 8 + :search-with-label 0 + nil) + :margin-right (case type + :mention 8 + :notification-down 0 + :notification-up 0 + :search-with-label 8 + nil)}) diff --git a/src/quo/components/buttons/dynamic_button/view.cljs b/src/quo/components/buttons/dynamic_button/view.cljs new file mode 100644 index 00000000000..a7970867213 --- /dev/null +++ b/src/quo/components/buttons/dynamic_button/view.cljs @@ -0,0 +1,83 @@ +(ns quo.components.buttons.dynamic-button.view + (:require + [quo.components.buttons.dynamic-button.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- get-button-color + [{:keys [type pressed? customization-color theme]}] + (if (= type :mention) + (if pressed? + (colors/resolve-color customization-color (colors/invert-theme theme)) + (colors/resolve-color customization-color theme)) + (if pressed? + (colors/theme-colors colors/neutral-80-opa-80 colors/white-opa-80 theme) + (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme)))) + +(defn- get-icon-and-text-color + [type theme] + (if (= type :mention) + colors/white + (colors/theme-colors colors/white colors/neutral-100 theme))) + +(defn- icon-view + [type theme] + [icon/icon + (case type + :mention :i/mention + :notification-down :i/arrow-down + :notification-up :i/arrow-up + :search-with-label :i/search + :search :i/search + :scroll-to-bottom :i/arrow-down) + {:size 12 + :color (get-icon-and-text-color type theme) + :container-style (style/container type)}]) + +(defn view + "[dynamic-button opts] + opts + {:type :mention/:notification-down/:notification-up/:search/:search-with-label/:scroll-to-bottom + :on-press fn + :count mentions or notifications count + :customization-color customize and mention button color}" + [{:keys [type label on-press customization-color style] :as args}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + button-color (get-button-color {:type type + :pressed? pressed? + :customization-color (or customization-color :primary) + :theme theme}) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/touchable-opacity + {:on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :active-opacity 1 + :hit-slop {:top 5 :bottom 5 :left 5 :right 5} + :pointer-events :auto + :style {:height 24} + :accessibility-label type} + [rn/view + {:style (merge + {:flex-direction :row + :height 24 + :border-radius 12 + :background-color button-color} + style)} + (when (#{:mention :search :search-with-label :scroll-to-bottom} type) + [icon-view type theme]) + (when (#{:mention :notification-down :notification-up :search-with-label} type) + [text/text + {:weight :medium + :size :paragraph-2 + :style (assoc (style/text type) :color (get-icon-and-text-color type theme))} + (case type + :search-with-label label + (:mention :notification-down :notification-up) (str (:count args)))]) + (when (#{:notification-down :notification-up} type) + [icon-view type theme])]])) diff --git a/src/quo/components/buttons/logout_button/component_spec.cljs b/src/quo/components/buttons/logout_button/component_spec.cljs new file mode 100644 index 00000000000..3945252f1ab --- /dev/null +++ b/src/quo/components/buttons/logout_button/component_spec.cljs @@ -0,0 +1,31 @@ +(ns quo.components.buttons.logout-button.component-spec + (:require + [quo.components.buttons.logout-button.view :as logout-button] + [test-helpers.component :as h])) + +(h/describe "button tests" + (h/test "default render of logout button component" + (h/render [logout-button/view]) + (h/is-truthy (h/get-by-label-text :log-out-button))) + + (h/test "button on-press works" + (let [event (h/mock-fn)] + (h/render [logout-button/view + {:on-press event}]) + (h/fire-event :press (h/get-by-label-text :log-out-button)) + (h/was-called event))) + + (h/test "button on-press disabled when disabled" + (let [event (h/mock-fn)] + (h/render [logout-button/view + {:on-press event + :disabled? true}]) + (h/fire-event :press (h/get-by-label-text :log-out-button)) + (h/was-not-called event))) + + (h/test "button on-long-press works" + (let [event (h/mock-fn)] + (h/render [logout-button/view + {:on-long-press event}]) + (h/fire-event :long-press (h/get-by-label-text :log-out-button)) + (h/was-called event)))) diff --git a/src/quo/components/buttons/logout_button/style.cljs b/src/quo/components/buttons/logout_button/style.cljs new file mode 100644 index 00000000000..c2aa9c19eeb --- /dev/null +++ b/src/quo/components/buttons/logout_button/style.cljs @@ -0,0 +1,15 @@ +(ns quo.components.buttons.logout-button.style + (:require + [quo.foundations.colors :as colors])) + +(defn main + [{:keys [pressed? disabled?]}] + {:background-color (if pressed? colors/danger-50-opa-40 colors/danger-50-opa-30) + :border-radius 12 + :height 40 + :gap 4 + :padding-right 3 + :flex-direction :row + :justify-content :center + :align-items :center + :opacity (when disabled? 0.2)}) diff --git a/src/quo/components/buttons/logout_button/view.cljs b/src/quo/components/buttons/logout_button/view.cljs new file mode 100644 index 00000000000..da316e44d46 --- /dev/null +++ b/src/quo/components/buttons/logout_button/view.cljs @@ -0,0 +1,30 @@ +(ns quo.components.buttons.logout-button.view + (:require + [quo.components.buttons.logout-button.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn view + [{:keys [on-press on-long-press disabled? container-style]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed nil))] + [rn/pressable + {:accessibility-label :log-out-button + :on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out + :on-long-press on-long-press + :disabled disabled? + :style (merge (style/main {:pressed? pressed? + :theme theme + :disabled? disabled?}) + container-style)} + [icon/icon :i/log-out {:color (if pressed? colors/white-opa-40 colors/white-opa-70)}] + [text/text {:weight :medium :size :paragraph-1} + (i18n/label :t/logout)]])) diff --git a/src/quo/components/buttons/predictive_keyboard/component_spec.cljs b/src/quo/components/buttons/predictive_keyboard/component_spec.cljs new file mode 100644 index 00000000000..b96360ac9fd --- /dev/null +++ b/src/quo/components/buttons/predictive_keyboard/component_spec.cljs @@ -0,0 +1,21 @@ +(ns quo.components.buttons.predictive-keyboard.component-spec + (:require + [quo.components.buttons.predictive-keyboard.view :as predictive-keyboard] + [test-helpers.component :as h])) + +(h/describe "predictive-keyboard" + (h/test "basic render" + (h/render [predictive-keyboard/view {:type :error :text "Error message"}]) + (h/is-truthy (h/get-by-label-text :predictive-keyboard))) + (h/test "rendered with correct text" + (h/render [predictive-keyboard/view {:type :info :text "Error message"}]) + (h/is-truthy (h/get-by-text "Error message"))) + (h/test "rendered with correct words" + (h/render [predictive-keyboard/view {:type :words :words ["lab" "label"]}]) + (h/is-truthy (h/get-by-text "lab")) + (h/is-truthy (h/get-by-text "label"))) + (h/test "word press event" + (let [event (h/mock-fn)] + (h/render [predictive-keyboard/view {:type :words :words ["lab" "label"] :on-press #(event %)}]) + (h/fire-event :press (h/get-by-text "lab")) + (h/was-called event)))) diff --git a/src/quo/components/buttons/predictive_keyboard/style.cljs b/src/quo/components/buttons/predictive_keyboard/style.cljs new file mode 100644 index 00000000000..34db1748fcc --- /dev/null +++ b/src/quo/components/buttons/predictive_keyboard/style.cljs @@ -0,0 +1,11 @@ +(ns quo.components.buttons.predictive-keyboard.style) + +(defn wrapper + [type] + {:flex 1 + :min-height 48 + :padding-vertical 8 + :justify-content :center + :padding-horizontal (if (= type :words) 0 20)}) + +(def word-list {:align-items :center :padding-horizontal 20}) diff --git a/src/quo/components/buttons/predictive_keyboard/view.cljs b/src/quo/components/buttons/predictive_keyboard/view.cljs new file mode 100644 index 00000000000..d8d93a0eaac --- /dev/null +++ b/src/quo/components/buttons/predictive_keyboard/view.cljs @@ -0,0 +1,75 @@ +(ns quo.components.buttons.predictive-keyboard.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.buttons.predictive-keyboard.style :as style] + [quo.components.info.info-message.view :as info-message] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient])) + +(def ^:private gradients + {:light [(colors/alpha colors/neutral-40 0.05) (colors/alpha colors/neutral-40 0)] + :dark [(colors/alpha colors/neutral-80 0.7) (colors/alpha colors/neutral-80 0)] + :blur [colors/white-opa-5 colors/white-opa-0]}) + +(defn- word-component + [word _ _ {:keys [on-press]}] + [button/button + {:type :grey + :background :blur + :size 32 + :on-press #(on-press word)} + word]) + +(defn- separator + [] + [rn/view {:style {:width 8}}]) + +(defn view + "Options + - `type` `:words`/`:error`/`:info`/`:empty`. + - `blur?` Boolean to enable blur background support. + - `text` error/info text. + - `words` List of words to display in the keyboard. + - `on-press` Callback called when a word is pressed `(fn [word])` + - `theme` :light or :dark, received from with-theme HOC." + [{:keys [type blur? text words on-press container-style] :or {container-style {}}}] + (let [theme (quo.context/use-theme)] + [linear-gradient/linear-gradient + {:style (assoc container-style :flex-direction :row) + :accessibility-label :predictive-keyboard + :colors (if blur? + (gradients :blur) + (colors/theme-colors (gradients :light) (gradients :dark) theme))} + [rn/view {:style (style/wrapper type)} + (case type + :words + [rn/flat-list + {:keyboard-should-persist-taps :always + :data words + :content-container-style style/word-list + :render-fn word-component + :render-data {:on-press on-press} + :shows-horizontal-scroll-indicator false + :separator [separator] + :horizontal true + :key-fn str}] + + :error + [info-message/view + {:icon :i/alert + :size :default + :status :error} + text] + + :info + [info-message/view + (merge {:icon :i/info + :size :default + :status (if (= type :error) :error :default)} + (when blur? + {:text-color colors/white-opa-70 + :icon-color colors/white-opa-40})) + text] + nil)]])) diff --git a/src/quo/components/buttons/slide_button/animations.cljs b/src/quo/components/buttons/slide_button/animations.cljs new file mode 100644 index 00000000000..5aa8e3f4035 --- /dev/null +++ b/src/quo/components/buttons/slide_button/animations.cljs @@ -0,0 +1,67 @@ +(ns quo.components.buttons.slide-button.animations + (:require [react-native.reanimated :as reanimated])) + +(def ^:private extrapolation + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"}) + +(defn- track-interpolation-inputs + [in-vectors track-width] + (map (partial * track-width) in-vectors)) + +;; Interpolations +(defn- track-clamp-interpolation + [track-width] + {:in [-1 0 1] + :out [track-width 0 track-width]}) + +(defn- track-cover-interpolation + [track-width thumb-size] + {:in [0 1] + :out [thumb-size (+ track-width thumb-size)]}) + +(defn- arrow-icon-position-interpolation + [thumb-size] + {:in [0.9 1] + :out [0 (- thumb-size)]}) + +(defn- action-icon-position-interpolation + [thumb-size] + {:in [0.9 1] + :out [thumb-size 0]}) + +(defn interpolate-track + "Interpolate the position in the track + `x-pos` Track animated value + `track-width` Usable width of the track + `thumb-size` Size of the thumb + `interpolation` ` :thumb-border-radius`/`:thumb-drop-position`/`:thumb-drop-scale`/`:thumb-drop-z-index`/..." + ([x-pos track-width thumb-size interpolation] + (let [interpolations {:track-cover (track-cover-interpolation track-width thumb-size) + :track-clamp (track-clamp-interpolation track-width) + :action-icon-position (action-icon-position-interpolation thumb-size) + :arrow-icon-position (arrow-icon-position-interpolation thumb-size)} + + interpolation-values (interpolation interpolations) + output (:out interpolation-values) + input (-> (:in interpolation-values) + (track-interpolation-inputs track-width))] + (if interpolation-values + (reanimated/interpolate x-pos + input + output + extrapolation) + x-pos)))) + +;; Animations +(defn- animate-spring + [value to-value] + (reanimated/animate-shared-value-with-spring value + to-value + {:mass 1 + :damping 30 + :stiffness 400})) + +(defn reset-track-position + [x-pos] + (animate-spring x-pos 0)) diff --git a/src/quo/components/buttons/slide_button/component_spec.cljs b/src/quo/components/buttons/slide_button/component_spec.cljs new file mode 100644 index 00000000000..7a1c66fe3db --- /dev/null +++ b/src/quo/components/buttons/slide_button/component_spec.cljs @@ -0,0 +1,106 @@ +(ns quo.components.buttons.slide-button.component-spec + (:require + ["@testing-library/react-native" :as rtl] + ["react-native-gesture-handler/jest-utils" :as gestures-jest] + [quo.components.buttons.slide-button.constants :as constants] + [quo.components.buttons.slide-button.view :as slide-button] + [reagent.core :as r] + [test-helpers.component :as h])) + +;; NOTE stolen from +;; (https://github.com/reagent-project/reagent/blob/a14faba55e373000f8f93edfcfce0d1222f7e71a/test/reagenttest/utils.cljs#LL104C7-L104C10), +;; +;; There's also a comment over there about it being +;; not "usable with production React", but no explanation why. +;; If we decide to keep it, can be moved to `test-helpers.component`. +(defn act + "Run f to trigger Reagent updates, + will return Promise which will resolve after + Reagent and React render." + [f] + (js/Promise. + (fn [resolve-fn reject] + (try + (.then (rtl/act + #(let [p (js/Promise. (fn [resolve-fn2 _reject] + (r/after-render (fn reagent-act-after-reagent-flush [] + (resolve-fn2)))))] + (f) + p)) + resolve-fn + reject) + (catch :default e + (reject e)))))) + +(def ^:private gesture-state + {:untedermined 0 + :failed 1 + :began 2 + :cancelled 3 + :active 4 + :end 5}) + +(defn gesture-x-event + [event position] + (clj->js {:state (event gesture-state) + :translationX position})) + +(defn slide-events + [dest] + [(gesture-x-event :began 0) + (gesture-x-event :active 0) + (gesture-x-event :active dest) + (gesture-x-event :end dest)]) + +(defn get-by-gesture-test-id + [test-id] + (gestures-jest/getByGestureTestId + (str test-id))) + +(def ^:private default-props + {:on-complete identity + :track-text :test-track-text + :track-icon :face-id}) + +(h/describe "slide-button" + (h/setup-fake-timers) + + (h/test "render the correct text" + (h/render [slide-button/view default-props]) + (h/is-truthy (h/get-by-text :test-track-text))) + + (h/test "render the disabled button" + (h/render [slide-button/view (assoc default-props :disabled? true)]) + (let [track-mock (h/get-by-test-id :slide-button-track)] + (h/has-style track-mock {:opacity constants/disable-opacity}))) + + (h/test "render the small button" + (h/render [slide-button/view (assoc default-props :size :size-40)]) + (let [mock (h/get-by-test-id :slide-button-track) + small-height (:track-height constants/small-dimensions)] + (h/has-style mock {:height small-height}))) + + (h/test + "calls on-complete when dragged" + (let [props (merge default-props {:on-complete (h/mock-fn)}) + slide-dest constants/default-width + gesture-events (slide-events slide-dest)] + (h/render [slide-button/view props]) + (let [promise + (-> (act #(gestures-jest/fireGestureHandler (get-by-gesture-test-id :slide-button-gestures) + gesture-events)))] + (h/advance-timers-by-time 250) + (-> promise + (.then #(h/was-called (:on-complete props))))))) + + (h/test + "doesn't call on-complete if the slide was incomplete" + (let [props (merge default-props {:on-complete (h/mock-fn)}) + slide-dest (- constants/default-width 100) + gesture-events (slide-events slide-dest)] + (h/render [slide-button/view props]) + (let [promise (-> (act #(gestures-jest/fireGestureHandler (get-by-gesture-test-id + :slide-button-gestures) + gesture-events)))] + (h/advance-timers-by-time 250) + (-> promise (.then #(h/was-not-called (:on-complete props)))))))) diff --git a/src/quo/components/buttons/slide_button/constants.cljs b/src/quo/components/buttons/slide_button/constants.cljs new file mode 100644 index 00000000000..e919ad53912 --- /dev/null +++ b/src/quo/components/buttons/slide_button/constants.cljs @@ -0,0 +1,15 @@ +(ns quo.components.buttons.slide-button.constants) + +(def ^:constant small-dimensions + {:track-height 40 + :thumb 32}) + +(def ^:constant large-dimensions + {:track-height 48 + :thumb 40}) + +(def ^:constant track-padding 4) +(def ^:constant disable-opacity 0.3) +(def ^:constant default-width 300) + +(def ^:constant reset-timeout-ms 500) diff --git a/src/quo/components/buttons/slide_button/style.cljs b/src/quo/components/buttons/slide_button/style.cljs new file mode 100644 index 00000000000..5e3e1eed7be --- /dev/null +++ b/src/quo/components/buttons/slide_button/style.cljs @@ -0,0 +1,73 @@ +(ns quo.components.buttons.slide-button.style + (:require + [quo.components.buttons.slide-button.constants :as constants] + [quo.components.buttons.slide-button.utils :as utils])) + +(def absolute-fill + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(defn thumb-container + [{:keys [interpolate-track thumb-size customization-color theme]}] + [{:transform [{:translate-x (interpolate-track :track-clamp)}]} + {:background-color (utils/main-color customization-color theme) + :border-radius 12 + :height thumb-size + :width thumb-size + :align-items :center + :overflow :hidden + :justify-content :center}]) + +(defn arrow-icon-container + [interpolate-track] + {:transform [{:translate-x (interpolate-track :arrow-icon-position)}] + :flex 1 + :align-items :center + :justify-content :center}) + +(defn action-icon + [interpolate-track size] + [{:transform [{:translate-x (interpolate-track :action-icon-position)}]} + {:height size + :width size + :position :absolute + :align-items :center + :left 0 + :top 0 + :transform [{:translate-x 0}] + :flex-direction :row + :justify-content :space-around}]) + +(defn track + [{:keys [disabled? customization-color height blur? theme]}] + {:align-items :flex-start + :justify-content :center + :border-radius 14 + :height height + :align-self :stretch + :padding constants/track-padding + :opacity (if disabled? 0.3 1) + :background-color (utils/track-color customization-color theme blur?)}) + +(defn track-cover + [interpolate-track] + [{:left (interpolate-track :track-cover)} + (assoc absolute-fill :overflow :hidden)]) + +(defn track-cover-text-container + [track-width] + {:position :absolute + :right 0 + :top 0 + :bottom 0 + :align-items :center + :justify-content :center + :flex-direction :row + :width track-width}) + +(defn track-text + [customization-color theme blur?] + {:color (utils/text-color customization-color theme blur?)}) diff --git a/src/quo/components/buttons/slide_button/utils.cljs b/src/quo/components/buttons/slide_button/utils.cljs new file mode 100644 index 00000000000..bbdaff92e0d --- /dev/null +++ b/src/quo/components/buttons/slide_button/utils.cljs @@ -0,0 +1,49 @@ +(ns quo.components.buttons.slide-button.utils + (:require + [quo.components.buttons.slide-button.constants :as constants] + [quo.foundations.colors :as colors])) + +(defn main-color + "`customization-color` Customization color" + [customization-color theme] + (colors/resolve-color customization-color theme)) + +(defn track-color + "`customization-color` Customization color" + ([customization-color theme blur?] + (if blur? + colors/white-opa-5 + (colors/resolve-color customization-color theme 10)))) + +(defn text-color + "`customization-color` Customization color" + [customization-color theme blur?] + (if blur? + colors/white-opa-40 + (colors/resolve-color customization-color theme))) + +(defn clamp-value + [value min-value max-value] + (cond + (< value min-value) min-value + (> value max-value) max-value + :else value)) + +(defn calc-usable-track + "Calculate the track section in which the + thumb can move in. Mostly used for interpolations." + [track-width thumb-size] + (let [double-padding (* constants/track-padding 2)] + (- track-width double-padding thumb-size))) + +(defn get-dimensions + [track-width size dimension-key] + (let [default-dimensions (case size + :size-40 constants/small-dimensions + :size-48 constants/large-dimensions + constants/large-dimensions)] + (-> default-dimensions + (merge {:usable-track (calc-usable-track + track-width + (:thumb default-dimensions))}) + (get dimension-key)))) diff --git a/src/quo/components/buttons/slide_button/view.cljs b/src/quo/components/buttons/slide_button/view.cljs new file mode 100644 index 00000000000..0be03b1be47 --- /dev/null +++ b/src/quo/components/buttons/slide_button/view.cljs @@ -0,0 +1,120 @@ +(ns quo.components.buttons.slide-button.view + (:require + [oops.core :as oops] + [quo.components.buttons.slide-button.animations :as animations] + [quo.components.buttons.slide-button.constants :as constants] + [quo.components.buttons.slide-button.style :as style] + [quo.components.buttons.slide-button.utils :as utils] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated])) + +(defn drag-gesture + [x-pos disabled? track-width sliding-complete? set-sliding-complete on-complete reset-fn] + (-> (gesture/gesture-pan) + (gesture/with-test-ID :slide-button-gestures) + (gesture/enabled (not disabled?)) + (gesture/min-distance 0) + (gesture/on-update (fn [event] + (let [x-translation (oops/oget event "translationX") + clamped-x (utils/clamp-value x-translation 0 track-width) + reached-end? (>= clamped-x track-width)] + (reanimated/set-shared-value x-pos clamped-x) + (when (and reached-end? (not sliding-complete?)) + (set-sliding-complete true) + (when on-complete + (js/setTimeout reset-fn constants/reset-timeout-ms) + (on-complete)))))) + (gesture/on-end (fn [event] + (let [x-translation (oops/oget event "translationX") + reached-end? (>= x-translation track-width)] + (when (not reached-end?) + (animations/reset-track-position x-pos))))))) + +(defn view + "Options + - `on-complete` Callback called when the sliding is complete, returns reset fn as a parameter + - `disabled?` Boolean that disables the button + (_and gestures_) + - `size` :size/s-40`/`:size/s-48` + - `track-text` Text that is shown on the track + - `track-icon` Key of the icon shown on the track + (e.g. `:face-id`) + - `customization-color` Customization color + " + [{:keys [on-complete track-text track-icon disabled? customization-color size + container-style type blur?]}] + (let [theme (quo.context/use-theme) + x-pos (reanimated/use-shared-value 0) + [track-width set-track-width] (rn/use-state nil) + [sliding-complete? + set-sliding-complete] (rn/use-state false) + on-track-layout (rn/use-callback + #(set-track-width (oops/oget % "nativeEvent.layout.width"))) + reset-fn (rn/use-callback + (fn [] + (set-sliding-complete false) + (animations/reset-track-position x-pos))) + dimensions (rn/use-callback + (partial utils/get-dimensions + (or track-width constants/default-width) + size) + [track-width]) + interpolate-track (rn/use-callback + (partial animations/interpolate-track + x-pos + (dimensions :usable-track) + (dimensions :thumb)) + [dimensions]) + custom-color (if (= type :danger) :danger customization-color) + gesture (rn/use-memo + (fn [] + (drag-gesture x-pos + disabled? + (dimensions :usable-track) + sliding-complete? + set-sliding-complete + on-complete + reset-fn)) + [sliding-complete? disabled? on-complete])] + [gesture/gesture-detector + {:gesture gesture} + [reanimated/view + {:test-ID :slide-button-track + :style (merge (style/track {:disabled? disabled? + :customization-color custom-color + :height (dimensions :track-height) + :blur? blur? + :theme theme}) + container-style) + :on-layout on-track-layout} + [reanimated/view {:style (style/track-cover interpolate-track)} + [rn/view {:style (style/track-cover-text-container track-width)} + [icon/icon track-icon + {:color (utils/text-color custom-color theme blur?) + :size 20}] + [rn/view {:width 4}] + [text/text + {:weight :medium + :size :paragraph-1 + :style (style/track-text custom-color theme blur?)} + track-text]]] + [reanimated/view + {:style (style/thumb-container {:interpolate-track interpolate-track + :thumb-size (dimensions :thumb) + :customization-color custom-color + :theme theme + :blur? blur?})} + [reanimated/view {:style (style/arrow-icon-container interpolate-track)} + [icon/icon :arrow-right + {:color colors/white + :size 20}]] + [reanimated/view + {:style (style/action-icon interpolate-track (dimensions :thumb))} + [icon/icon track-icon + {:color colors/white + :size 20}]]]]])) diff --git a/src/quo/components/buttons/swap_order_button/component_spec.cljs b/src/quo/components/buttons/swap_order_button/component_spec.cljs new file mode 100644 index 00000000000..1e83c5d5e91 --- /dev/null +++ b/src/quo/components/buttons/swap_order_button/component_spec.cljs @@ -0,0 +1,23 @@ +(ns quo.components.buttons.swap-order-button.component-spec + (:require [quo.components.buttons.swap-order-button.view :as swap-order-button] + [test-helpers.component :as h])) + +(h/describe "Buttons: Swap Order Button" + (h/test "should render correctly" + (h/render-with-theme-provider + [swap-order-button/view {:on-press identity}]) + (h/is-truthy (h/get-by-label-text :swap-order-button))) + + (h/test "should call on-press handler when pressed" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider + [swap-order-button/view {:on-press on-press}]) + (h/fire-event :press (h/get-by-label-text :swap-order-button)) + (h/was-called on-press))) + + (h/test "should not call on-press handler when button is disabled" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider + [swap-order-button/view {:on-press on-press :disabled? true}]) + (h/fire-event :press (h/get-by-label-text :swap-order-button)) + (h/was-not-called on-press)))) diff --git a/src/quo/components/buttons/swap_order_button/schema.cljs b/src/quo/components/buttons/swap_order_button/schema.cljs new file mode 100644 index 00000000000..3fa9a772014 --- /dev/null +++ b/src/quo/components/buttons/swap_order_button/schema.cljs @@ -0,0 +1,11 @@ +(ns quo.components.buttons.swap-order-button.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:disabled? {:optional true} [:maybe :boolean]] + [:on-press fn?] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) diff --git a/src/quo/components/buttons/swap_order_button/style.cljs b/src/quo/components/buttons/swap_order_button/style.cljs new file mode 100644 index 00000000000..5a10b6a3a79 --- /dev/null +++ b/src/quo/components/buttons/swap_order_button/style.cljs @@ -0,0 +1,18 @@ +(ns quo.components.buttons.swap-order-button.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [pressed? disabled? theme] + {:width 32 + :height 32 + :border-radius 10 + :border-width 1 + :opacity (if disabled? 0.3 1) + :align-items :center + :justify-content :center + :border-color (if pressed? + (colors/theme-colors colors/neutral-20 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)) + :background-color (if pressed? + (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme))}) diff --git a/src/quo/components/buttons/swap_order_button/view.cljs b/src/quo/components/buttons/swap_order_button/view.cljs new file mode 100644 index 00000000000..e21c2351781 --- /dev/null +++ b/src/quo/components/buttons/swap_order_button/view.cljs @@ -0,0 +1,28 @@ +(ns quo.components.buttons.swap-order-button.view + (:require [quo.components.buttons.swap-order-button.schema :as swap-order-button.schema] + [quo.components.buttons.swap-order-button.style :as style] + [quo.components.icon :as icon] + quo.context + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- view-internal + [{:keys [disabled? on-press container-style]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true) []) + on-press-out (rn/use-callback #(set-pressed false) [])] + [rn/pressable + {:style (merge (style/container pressed? disabled? theme) + container-style) + :accessibility-label :swap-order-button + :disabled disabled? + :on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out} + [icon/icon :i/arrow-down + {:size 20 + :color (colors/theme-colors colors/neutral-100 colors/white theme)}]])) + +(def view (schema/instrument #'view-internal swap-order-button.schema/?schema)) diff --git a/src/quo/components/buttons/wallet_button/component_spec.cljs b/src/quo/components/buttons/wallet_button/component_spec.cljs new file mode 100644 index 00000000000..514f532096e --- /dev/null +++ b/src/quo/components/buttons/wallet_button/component_spec.cljs @@ -0,0 +1,40 @@ +(ns quo.components.buttons.wallet-button.component-spec + (:require + [quo.components.buttons.wallet-button.view :as wallet-button] + [test-helpers.component :as h])) + +(h/describe "button tests" + (h/test "default render of wallet button component" + (h/render [wallet-button/view + {:icon :i/placeholder + :accessibility-label "test-button"}]) + (h/is-truthy (h/get-by-label-text "test-button"))) + + (h/test "button on-press works" + (let [event (h/mock-fn)] + (h/render [wallet-button/view + {:icon :i/placeholder + :on-press event + :accessibility-label "test-button"}]) + (h/fire-event :press (h/get-by-label-text "test-button")) + (h/was-called event))) + + (h/test "button on-press disabled when disabled" + (let [event (h/mock-fn)] + (h/render [wallet-button/view + {:icon :i/placeholder + :on-press event + :accessibility-label "test-button" + :disabled? true}]) + (h/fire-event :press (h/get-by-label-text "test-button")) + (h/was-not-called event))) + + (h/test "button on-long-press works" + (let [event (h/mock-fn)] + (h/render [wallet-button/view + {:icon :i/placeholder + :on-long-press event + :accessibility-label "test-button"}]) + (h/fire-event :long-press (h/get-by-label-text "test-button")) + (h/was-called event)))) + diff --git a/src/quo/components/buttons/wallet_button/style.cljs b/src/quo/components/buttons/wallet_button/style.cljs new file mode 100644 index 00000000000..bef3ca4875d --- /dev/null +++ b/src/quo/components/buttons/wallet_button/style.cljs @@ -0,0 +1,21 @@ +(ns quo.components.buttons.wallet-button.style + (:require + [quo.foundations.colors :as colors])) + +(defn get-border-color + [{:keys [pressed? theme]}] + (if pressed? + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-30 colors/neutral-70 theme))) + +(defn main + [{:keys [pressed? theme disabled?]}] + {:border-color (get-border-color {:pressed? pressed? + :theme theme}) + :border-width 1 + :border-radius 10 + :width 32 + :height 32 + :justify-content :center + :align-items :center + :opacity (when disabled? 0.3)}) diff --git a/src/quo/components/buttons/wallet_button/view.cljs b/src/quo/components/buttons/wallet_button/view.cljs new file mode 100644 index 00000000000..ed83d8a09cb --- /dev/null +++ b/src/quo/components/buttons/wallet_button/view.cljs @@ -0,0 +1,27 @@ +(ns quo.components.buttons.wallet-button.view + (:require + [quo.components.buttons.wallet-button.style :as style] + [quo.components.icon :as quo.icons] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn view + [{:keys [on-press on-long-press disabled? icon accessibility-label container-style]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed nil))] + [rn/pressable + {:accessibility-label (or accessibility-label :wallet-button) + :on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out + :on-long-press on-long-press + :disabled disabled? + :style (merge (style/main {:pressed? pressed? + :theme theme + :disabled? disabled?}) + container-style)} + [quo.icons/icon icon + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}]])) diff --git a/src/quo/components/buttons/wallet_ctas/component_spec.cljs b/src/quo/components/buttons/wallet_ctas/component_spec.cljs new file mode 100644 index 00000000000..5870c82fac6 --- /dev/null +++ b/src/quo/components/buttons/wallet_ctas/component_spec.cljs @@ -0,0 +1,12 @@ +(ns quo.components.buttons.wallet-ctas.component-spec + (:require + [quo.components.buttons.wallet-ctas.view :as wallet-ctas] + [test-helpers.component :as h])) + +(h/describe "Wallet CTAs test" + (h/test "Buttons render" + (h/render [wallet-ctas/view]) + (h/is-truthy (h/get-by-label-text :buy)) + (h/is-truthy (h/get-by-label-text :send)) + (h/is-truthy (h/get-by-label-text :receive)) + (h/is-truthy (h/get-by-label-text :bridge)))) diff --git a/src/quo/components/buttons/wallet_ctas/style.cljs b/src/quo/components/buttons/wallet_ctas/style.cljs new file mode 100644 index 00000000000..9e948ee1b8d --- /dev/null +++ b/src/quo/components/buttons/wallet_ctas/style.cljs @@ -0,0 +1,19 @@ +(ns quo.components.buttons.wallet-ctas.style + (:require [quo.foundations.colors :as colors])) + +(def inner-container + {:flex-direction :row + :justify-content :space-between + :flex 1}) + +(def button-container + {:padding-vertical 8 + :flex 1 + :justify-content :center + :align-items :center}) + +(defn action-button-text + [theme disabled?] + (cond-> {:margin-top 4 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)} + disabled? (assoc :opacity 0.5))) diff --git a/src/quo/components/buttons/wallet_ctas/view.cljs b/src/quo/components/buttons/wallet_ctas/view.cljs new file mode 100644 index 00000000000..b648ce75885 --- /dev/null +++ b/src/quo/components/buttons/wallet_ctas/view.cljs @@ -0,0 +1,63 @@ +(ns quo.components.buttons.wallet-ctas.view + (:require + [quo.components.buttons.wallet-button.view :as wallet-button] + [quo.components.buttons.wallet-ctas.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [utils.i18n :as i18n])) + + +(defn action-button + [{:keys [icon text on-press theme accessibility-label disabled?]}] + [rn/view + {:style style/button-container + :accessibility-label accessibility-label} + [wallet-button/view + {:icon icon + :disabled? disabled? + :on-press on-press}] + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/action-button-text theme disabled?)} text]]) + +(defn view + [{:keys [buy-action send-action receive-action bridge-action swap-action bridge-disabled? + swap-disabled? container-style]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style container-style} + [rn/view {:style style/inner-container} + [action-button + {:icon :i/add + :text (i18n/label :t/buy) + :on-press buy-action + :theme theme + :accessibility-label :buy}] + [action-button + {:icon :i/send + :text (i18n/label :t/send) + :on-press send-action + :theme theme + :accessibility-label :send}] + [action-button + {:icon :i/receive + :text (i18n/label :t/receive) + :on-press receive-action + :theme theme + :accessibility-label :receive}] + (when swap-action + [action-button + {:icon :i/transaction + :text (i18n/label :t/swap) + :on-press swap-action + :theme theme + :disabled? swap-disabled? + :accessibility-label :swap}]) + [action-button + {:icon :i/bridge + :text (i18n/label :t/bridge) + :on-press bridge-action + :theme theme + :disabled? bridge-disabled? + :accessibility-label :bridge}]]])) diff --git a/src/quo/components/calendar/calendar/component_spec.cljs b/src/quo/components/calendar/calendar/component_spec.cljs new file mode 100644 index 00000000000..f8ef5bddb80 --- /dev/null +++ b/src/quo/components/calendar/calendar/component_spec.cljs @@ -0,0 +1,65 @@ +(ns quo.components.calendar.calendar.component-spec + (:require + [cljs-time.core :as time] + [quo.components.calendar.calendar.view :as calendar] + [test-helpers.component :as h])) + +(def start-date (time/date-time (time/year (time/now)) (time/month (time/now)) 8)) +(def end-date (time/date-time (time/plus start-date (time/days 2)))) + +(h/describe "calendar component" + (h/test "default render of calendar component" + (h/render + [calendar/view + {:start-date start-date + :end-date end-date}]) + (-> (h/expect (h/query-by-translation-text "Mo")) + (h/is-truthy))) + + (h/test "should call on-change with selected date on first click" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date nil + :end-date nil + :on-change on-change}]) + (h/fire-event :press (h/query-first-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date start-date :end-date nil}))) + + (h/test "should call on-change with start and end date on second click" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date start-date :end-date nil :on-change on-change}]) + (h/fire-event :press (h/query-first-by-text (str (time/day end-date)))) + (h/was-called-with on-change {:start-date start-date :end-date end-date}))) + + (h/test "should reset the dates on third click" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date start-date + :end-date end-date + :on-change on-change}]) + (h/fire-event :press (h/query-first-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date start-date :end-date nil}))) + + (h/test "should reset dates when start date is clicked again" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date start-date + :end-date nil + :on-change on-change}]) + (h/fire-event :press (h/query-first-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date nil :end-date nil}))) + + (h/test "should assign start and end date correctly when upper range selected first" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date end-date + :end-date nil + :on-change on-change}]) + (h/fire-event :press (h/query-first-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date start-date :end-date end-date})))) diff --git a/src/quo/components/calendar/calendar/days_grid/style.cljs b/src/quo/components/calendar/calendar/days_grid/style.cljs new file mode 100644 index 00000000000..cc7242b6380 --- /dev/null +++ b/src/quo/components/calendar/calendar/days_grid/style.cljs @@ -0,0 +1,7 @@ +(ns quo.components.calendar.calendar.days-grid.style) + +(def container-days + {:flex-grow 1 + :margin-top 4 + :margin-horizontal 8 + :overflow :hidden}) diff --git a/src/quo/components/calendar/calendar/days_grid/utils.cljs b/src/quo/components/calendar/calendar/days_grid/utils.cljs new file mode 100644 index 00000000000..137f286ac8d --- /dev/null +++ b/src/quo/components/calendar/calendar/days_grid/utils.cljs @@ -0,0 +1,65 @@ +(ns quo.components.calendar.calendar.days-grid.utils + (:require + [cljs-time.core :as time] + [utils.number :as utils.number])) + +(defn- day-of-week + [date] + (let [day (time/day-of-week date)] + (mod day 7))) + +(defn- add-days + [date days] + (time/plus date (time/days days))) + +(defn day-grid + [year month] + (let [year (utils.number/parse-int year) + month (utils.number/parse-int month) + first-day (time/date-time year month 1) + start-day (add-days first-day (- 0 (day-of-week first-day))) + end-day (add-days start-day 34)] + (loop [days [] + current-day start-day] + (if (time/after? current-day end-day) + days + (recur (conj days current-day) (add-days current-day 1)))))) + +(defn get-day-state + [day today year month start-date end-date] + (cond + (and start-date (time/equal? day start-date)) :selected + (and end-date (time/equal? day end-date)) :selected + (and (= (time/year day) (time/year today)) + (= (time/month day) (time/month today)) + (= (time/day day) (time/day today))) :today + (and (= (time/year day) year) (= (time/month day) month)) :default + :else :disabled)) + +(defn update-range + [day start-date end-date] + (let [new-state (cond + (and start-date end-date) {:start-date day :end-date nil} + (and start-date (time/equal? day start-date)) {:start-date nil :end-date nil} + (and end-date (time/equal? day end-date)) {:start-date nil :end-date nil} + (nil? start-date) {:start-date day :end-date nil} + (nil? end-date) {:start-date start-date :end-date day} + :else {:start-date start-date + :end-date end-date})] + (if (and (:start-date new-state) + (:end-date new-state) + (time/after? (:start-date new-state) (:end-date new-state))) + {:start-date (:end-date new-state) :end-date (:start-date new-state)} + new-state))) + +(defn in-range? + [day start-date end-date] + (and start-date end-date (time/after? day start-date) (time/before? day end-date))) + +(defn get-in-range-pos + [day start-date end-date] + (cond + (or (nil? start-date) (nil? end-date)) nil + (and start-date (time/equal? day start-date)) :start + (and end-date (time/equal? day end-date)) :end + (in-range? day start-date end-date) :middle)) diff --git a/src/quo/components/calendar/calendar/days_grid/utils_test.cljs b/src/quo/components/calendar/calendar/days_grid/utils_test.cljs new file mode 100644 index 00000000000..ce98e1137f9 --- /dev/null +++ b/src/quo/components/calendar/calendar/days_grid/utils_test.cljs @@ -0,0 +1,52 @@ +(ns quo.components.calendar.calendar.days-grid.utils-test + (:require + [cljs-time.core :as time] + [cljs.test :refer-macros [deftest is testing]] + [quo.components.calendar.calendar.days-grid.utils :as utils])) + +(deftest day-grid-test + (let [day-grid-result (utils/day-grid "2023" "7")] + (testing "it returns correct days grid" + (is (= 35 (count day-grid-result))) + (is (time/equal? (time/date-time 2023 6 25) (first day-grid-result))) + (is (time/equal? (time/date-time 2023 7 29) (last day-grid-result)))))) + +(deftest get-day-state-test + (let [today (time/date-time 2023 7 27) + year 2023 + month 7 + start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30)] + (testing "it returns :today when day equals today" + (is (= :today (utils/get-day-state today today year month start-date end-date)))) + (testing "it returns :selected when day equals start-date and not today" + (is + (= :selected (utils/get-day-state start-date today year month start-date end-date)))) + (testing "it returns :selected when day equals end-date and not today" + (is + (= :selected (utils/get-day-state end-date today year month start-date end-date)))))) + +(deftest update-range-test + (let [start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30) + day (time/date-time 2023 7 27)] + (testing "it returns updated range" + (is + (= {:start-date day :end-date nil} (utils/update-range day start-date end-date)))))) + +(deftest in-range-test + (let [start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30) + day (time/date-time 2023 7 27)] + (testing "it returns true when day is within range" + (is (utils/in-range? day start-date end-date)) + (is (not (utils/in-range? (time/date-time 2023 7 19) start-date end-date)))))) + +(deftest get-in-range-pos-test + (let [start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30) + day (time/date-time 2023 7 27)] + (testing "it returns correct position within range" + (is (= :start (utils/get-in-range-pos start-date start-date end-date))) + (is (= :end (utils/get-in-range-pos end-date start-date end-date))) + (is (= :middle (utils/get-in-range-pos day start-date end-date)))))) diff --git a/src/quo/components/calendar/calendar/days_grid/view.cljs b/src/quo/components/calendar/calendar/days_grid/view.cljs new file mode 100644 index 00000000000..719f9df4c26 --- /dev/null +++ b/src/quo/components/calendar/calendar/days_grid/view.cljs @@ -0,0 +1,42 @@ +(ns quo.components.calendar.calendar.days-grid.view + (:require + [cljs-time.core :as time] + [quo.components.calendar.calendar-day.view :as calendar-day] + [quo.components.calendar.calendar.days-grid.style :as style] + [quo.components.calendar.calendar.days-grid.utils :as utils] + [react-native.core :as rn])) + +(defn- day-view + [day _ _ {:keys [year month selection-range on-press customization-color]}] + (let [today (time/now) + start-date (:start-date selection-range) + end-date (:end-date selection-range) + state (utils/get-day-state day today year month start-date end-date) + in-range (utils/get-in-range-pos day start-date end-date) + on-press #(on-press (time/date-time day))] + [calendar-day/view + {:customization-color customization-color + :state state + :in-range in-range + :on-press on-press} + (str (time/day day))])) + +(defn view + [{:keys [year month on-change start-date end-date customization-color]}] + (let [on-day-press (fn [day] + (let [new-selection (utils/update-range day start-date end-date)] + (on-change new-selection)))] + [rn/view + {:style style/container-days} + [rn/flat-list + {:data (utils/day-grid year month) + :key-fn str + :num-columns 7 + :content-container-style {:margin-horizontal -2} + :render-fn day-view + :render-data {:customization-color customization-color + :year year + :month month + :on-press on-day-press + :selection-range {:start-date start-date + :end-date end-date}}}]])) diff --git a/src/quo/components/calendar/calendar/month_picker/component_spec.cljs b/src/quo/components/calendar/calendar/month_picker/component_spec.cljs new file mode 100644 index 00000000000..e56c70caa53 --- /dev/null +++ b/src/quo/components/calendar/calendar/month_picker/component_spec.cljs @@ -0,0 +1,26 @@ +(ns quo.components.calendar.calendar.month-picker.component-spec + (:require + [quo.components.calendar.calendar.month-picker.view :as month-picker] + [test-helpers.component :as h])) + +(h/describe "month-picker component" + (h/test "default render of month-picker component" + (h/render + [month-picker/view + {:year "2023" :month "7"}]) + (-> (h/expect (h/query-by-translation-text "July 2023")) + (h/is-truthy))) + + (h/test "should call on-change with next month when right button pressed" + (let [on-change (h/mock-fn)] + (h/render + [month-picker/view {:year "2023" :month "7" :on-change on-change}]) + (h/fire-event :press (h/query-by-label-text :next-month-button)) + (h/was-called on-change))) + + (h/test "should call on-change with previous month when left button pressed" + (let [on-change (h/mock-fn)] + (h/render + [month-picker/view {:year "2023" :month "1" :on-change on-change}]) + (h/fire-event :press (h/query-by-label-text :previous-month-button)) + (h/was-called on-change)))) diff --git a/src/quo/components/calendar/calendar/month_picker/style.cljs b/src/quo/components/calendar/calendar/month_picker/style.cljs new file mode 100644 index 00000000000..307068ecfce --- /dev/null +++ b/src/quo/components/calendar/calendar/month_picker/style.cljs @@ -0,0 +1,15 @@ +(ns quo.components.calendar.calendar.month-picker.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:align-items :center + :flex-direction :row + :flex-grow 1 + :padding-horizontal 12 + :padding-vertical 9 + :justify-content :space-between}) + +(defn text + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) diff --git a/src/quo/components/calendar/calendar/month_picker/utils.cljs b/src/quo/components/calendar/calendar/month_picker/utils.cljs new file mode 100644 index 00000000000..626ed4249e9 --- /dev/null +++ b/src/quo/components/calendar/calendar/month_picker/utils.cljs @@ -0,0 +1,23 @@ +(ns quo.components.calendar.calendar.month-picker.utils + (:require + [utils.datetime :as datetime])) + +(defn format-month-year + [year month] + (let [month (cond + (or (nil? month) (zero? month)) 1 + (> month 12) 12 + :else month)] + (str (datetime/format-long-month month) " " year))) + +(defn next-month + [year month] + (let [new-month (if (= month 12) 1 (inc month)) + new-year (if (= month 12) (inc year) year)] + {:year (str new-year) :month (str new-month)})) + +(defn previous-month + [year month] + (let [new-month (if (= month 1) 12 (dec month)) + new-year (if (= month 1) (dec year) year)] + {:year (str new-year) :month (str new-month)})) diff --git a/src/quo/components/calendar/calendar/month_picker/utils_test.cljs b/src/quo/components/calendar/calendar/month_picker/utils_test.cljs new file mode 100644 index 00000000000..1051fc7fc92 --- /dev/null +++ b/src/quo/components/calendar/calendar/month_picker/utils_test.cljs @@ -0,0 +1,21 @@ +(ns quo.components.calendar.calendar.month-picker.utils-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [quo.components.calendar.calendar.month-picker.utils :as utils])) + +(deftest format-month-year-test + (testing "returns correct format for given year and month" + (is (= (utils/format-month-year 2023 1) "January 2023")) + (is (= (utils/format-month-year 2023 12) "December 2023")) + (is (= (utils/format-month-year 2023 0) "January 2023")) + (is (= (utils/format-month-year 2023 13) "December 2023")))) + +(deftest next-month-test + (testing "returns the next month and year" + (is (= (utils/next-month 2023 1) {:year "2023" :month "2"})) + (is (= (utils/next-month 2023 12) {:year "2024" :month "1"})))) + +(deftest previous-month-test + (testing "returns the previous month and year" + (is (= (utils/previous-month 2023 1) {:year "2022" :month "12"})) + (is (= (utils/previous-month 2023 12) {:year "2023" :month "11"})))) diff --git a/src/quo/components/calendar/calendar/month_picker/view.cljs b/src/quo/components/calendar/calendar/month_picker/view.cljs new file mode 100644 index 00000000000..0f1a6086759 --- /dev/null +++ b/src/quo/components/calendar/calendar/month_picker/view.cljs @@ -0,0 +1,38 @@ +(ns quo.components.calendar.calendar.month-picker.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.calendar.calendar.month-picker.style :as style] + [quo.components.calendar.calendar.month-picker.utils :as utils] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn] + [utils.number :as utils.number])) + +(defn view + [{:keys [year month on-change]}] + (let [theme (quo.context/use-theme) + year (utils.number/parse-int year) + month (utils.number/parse-int month)] + [rn/view + {:style style/container} + [button/button + {:icon true + :type :outline + :accessibility-label :previous-month-button + :allow-multiple-presses? true + :size 24 + :on-press #(on-change (utils/previous-month year month))} + :i/chevron-left] + [text/text + {:weight :semi-bold + :size :paragraph-1 + :style (style/text theme)} + (utils/format-month-year year month)] + [button/button + {:icon true + :accessibility-label :next-month-button + :allow-multiple-presses? true + :size 24 + :type :outline + :on-press #(on-change (utils/next-month year month))} + :i/chevron-right]])) diff --git a/src/quo/components/calendar/calendar/style.cljs b/src/quo/components/calendar/calendar/style.cljs new file mode 100644 index 00000000000..35e111449ca --- /dev/null +++ b/src/quo/components/calendar/calendar/style.cljs @@ -0,0 +1,15 @@ +(ns quo.components.calendar.calendar.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:flex-direction :row + :height 270 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :border-radius 12 + :border-width 1 + :background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme)}) + +(def container-main + {:flex-grow 1}) diff --git a/src/quo/components/calendar/calendar/utils.cljs b/src/quo/components/calendar/calendar/utils.cljs new file mode 100644 index 00000000000..1742532cfc6 --- /dev/null +++ b/src/quo/components/calendar/calendar/utils.cljs @@ -0,0 +1,26 @@ +(ns quo.components.calendar.calendar.utils + (:require + [clojure.string :as string] + [utils.datetime :as datetime] + [utils.number :as utils.number])) + +(defn generate-years + [current-year] + (let [current-year current-year] + (reverse (vec (range (- current-year 100) (inc current-year)))))) + +(defn current-year + [] + (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + first + utils.number/parse-int)) + +(defn current-month + [] + (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + second + utils.number/parse-int)) diff --git a/src/quo/components/calendar/calendar/utils_test.cljs b/src/quo/components/calendar/calendar/utils_test.cljs new file mode 100644 index 00000000000..a2da1d7ac1f --- /dev/null +++ b/src/quo/components/calendar/calendar/utils_test.cljs @@ -0,0 +1,31 @@ +(ns quo.components.calendar.calendar.utils-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [clojure.string :as string] + [quo.components.calendar.calendar.utils :as utils] + [utils.datetime :as datetime] + [utils.number :as utils.number])) + +(deftest generate-years-test + (testing "returns correct years range" + (let [current-year (utils/current-year)] + (is (= (last (utils/generate-years current-year)) (- current-year 100))) + (is (= (first (utils/generate-years current-year)) current-year))))) + +(deftest current-year-test + (testing "returns the current year" + (let [current-year (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + first + utils.number/parse-int)] + (is (= (utils/current-year) current-year))))) + +(deftest current-month-test + (testing "returns the current month" + (let [current-month (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + second + utils.number/parse-int)] + (is (= (utils/current-month) current-month))))) diff --git a/src/quo/components/calendar/calendar/view.cljs b/src/quo/components/calendar/calendar/view.cljs new file mode 100644 index 00000000000..b1c94455a27 --- /dev/null +++ b/src/quo/components/calendar/calendar/view.cljs @@ -0,0 +1,43 @@ +(ns quo.components.calendar.calendar.view + (:require + [quo.components.calendar.calendar.days-grid.view :as days-grid] + [quo.components.calendar.calendar.month-picker.view :as month-picker] + [quo.components.calendar.calendar.style :as style] + [quo.components.calendar.calendar.utils :as utils] + [quo.components.calendar.calendar.weekdays-header.view :as weekdays-header] + [quo.components.calendar.calendar.years-list.view :as years-list] + [quo.context] + [react-native.core :as rn] + [utils.number :as utils.number])) + +(defn view + [{:keys [on-change start-date end-date]}] + (let [theme (quo.context/use-theme) + [selected-year set-selected-year] (rn/use-state (utils/current-year)) + [selected-month set-selected-month] (rn/use-state (utils/current-month)) + on-change-year (rn/use-callback #(set-selected-year %)) + on-change-month (rn/use-callback + (fn [new-date] + (set-selected-year + (utils.number/parse-int (:year new-date))) + (set-selected-month + (utils.number/parse-int (:month new-date)))))] + [rn/view + {:style (style/container theme)} + [years-list/view + {:on-change-year on-change-year + :year selected-year}] + [rn/view + {:style style/container-main} + [month-picker/view + {:year selected-year + :month selected-month + :on-change on-change-month}] + [weekdays-header/view] + [days-grid/view + {:year selected-year + :month selected-month + :start-date start-date + :end-date end-date + :on-change on-change + :customization-color :blue}]]])) diff --git a/src/quo/components/calendar/calendar/weekdays_header/style.cljs b/src/quo/components/calendar/calendar/weekdays_header/style.cljs new file mode 100644 index 00000000000..a4d056309ff --- /dev/null +++ b/src/quo/components/calendar/calendar/weekdays_header/style.cljs @@ -0,0 +1,19 @@ +(ns quo.components.calendar.calendar.weekdays-header.style + (:require + [quo.foundations.colors :as colors])) + +(def container-weekday-row + {:flex-direction :row + :justify-content :space-between + :padding-horizontal 8}) + +(def container-weekday + {:width 32 + :height 30 + :padding-top 2 + :justify-content :center + :align-items :center}) + +(defn text-weekdays + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo/components/calendar/calendar/weekdays_header/view.cljs b/src/quo/components/calendar/calendar/weekdays_header/view.cljs new file mode 100644 index 00000000000..55ffec574ff --- /dev/null +++ b/src/quo/components/calendar/calendar/weekdays_header/view.cljs @@ -0,0 +1,23 @@ +(ns quo.components.calendar.calendar.weekdays-header.view + (:require + [quo.components.calendar.calendar.weekdays-header.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn] + [utils.datetime :as datetime] + [utils.i18n :as i18n])) + +(defn view + [] + (let [theme (quo.context/use-theme)] + [rn/view + {:style style/container-weekday-row} + (for [weekday datetime/weekday-names] + [rn/view + {:style style/container-weekday + :key weekday} + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/text-weekdays theme)} + (str (i18n/label weekday))]])])) diff --git a/src/quo/components/calendar/calendar/years_list/style.cljs b/src/quo/components/calendar/calendar/years_list/style.cljs new file mode 100644 index 00000000000..9a3c14208f9 --- /dev/null +++ b/src/quo/components/calendar/calendar/years_list/style.cljs @@ -0,0 +1,34 @@ +(ns quo.components.calendar.calendar.years-list.style + (:require + [quo.foundations.colors :as colors])) + +(defn gradient-start-color + [theme] + (colors/theme-colors colors/white colors/neutral-90 theme)) + +(defn gradient-end-color + [theme] + (colors/theme-colors colors/white-opa-0 colors/neutral-100-opa-0 theme)) + +(def gradient-view + {:position :absolute + :height 50 + :border-top-left-radius 12 + :top 0 + :left 0 + :right 0}) + +(defn container-years + [theme] + {:border-width 1 + :overflow :hidden + :padding-left 8 + :padding-right 7 + :padding-vertical 8 + :margin-left -1 + :margin-top -1 + :margin-bottom -1 + :border-style :dashed + :border-top-left-radius 12 + :border-bottom-left-radius 12 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)}) diff --git a/src/quo/components/calendar/calendar/years_list/view.cljs b/src/quo/components/calendar/calendar/years_list/view.cljs new file mode 100644 index 00000000000..7c417f1594a --- /dev/null +++ b/src/quo/components/calendar/calendar/years_list/view.cljs @@ -0,0 +1,49 @@ +(ns quo.components.calendar.calendar.years-list.view + (:require + [quo.components.calendar.calendar-year.view :as calendar-year] + [quo.components.calendar.calendar.utils :as utils] + [quo.components.calendar.calendar.years-list.style :as style] + [quo.context] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient])) + +(defn- year-view + [year _ _ {:keys [selected-year on-press]}] + [calendar-year/view + {:selected? (= year selected-year) + :on-press #(on-press year)} + (str year)]) + +(defn- separator + [] + [rn/view {:style {:height 4}}]) + +(defn- footer + [] + [rn/view {:style {:height 32}}]) + +(defn- gradiant-overview + [theme] + [linear-gradient/linear-gradient + {:colors [(style/gradient-start-color theme) (style/gradient-end-color theme)] + :style style/gradient-view + :start {:x 0 :y 0} + :end {:x 0 :y 1}}]) + +(defn view + [{:keys [on-change-year year]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (style/container-years theme)} + [rn/flat-list + {:data (utils/generate-years (utils/current-year)) + :key-fn str + :list-key :years-list + :inverted true + :shows-vertical-scroll-indicator false + :footer [footer] + :separator [separator] + :render-fn year-view + :render-data {:selected-year year + :on-press #(on-change-year %)}}] + [gradiant-overview theme]])) diff --git a/src/quo/components/calendar/calendar_day/component_spec.cljs b/src/quo/components/calendar/calendar_day/component_spec.cljs new file mode 100644 index 00000000000..bba079901f7 --- /dev/null +++ b/src/quo/components/calendar/calendar_day/component_spec.cljs @@ -0,0 +1,15 @@ +(ns quo.components.calendar.calendar-day.component-spec + (:require + [quo.components.calendar.calendar-day.view :as calendar-day] + [test-helpers.component :as h])) + +(h/describe "calendar-day component" + (h/test "default render of calendar-day component" + (h/render [calendar-day/view {} "25"]) + (h/is-truthy (h/query-by-text "25"))) + + (h/test "should not call on-press when state is :disabled" + (let [on-press (h/mock-fn)] + (h/render [calendar-day/view {:on-press on-press :state :disabled} "25"]) + (h/fire-event :press (h/query-by-text "25")) + (h/was-not-called on-press)))) diff --git a/src/quo/components/calendar/calendar_day/style.cljs b/src/quo/components/calendar/calendar_day/style.cljs new file mode 100644 index 00000000000..2bc34223ece --- /dev/null +++ b/src/quo/components/calendar/calendar_day/style.cljs @@ -0,0 +1,70 @@ +(ns quo.components.calendar.calendar-day.style + (:require + [quo.foundations.colors :as colors])) + +(def wrapper + {:flex 1 + :margin-vertical 2 + :justify-content :center + :align-items :center}) + +(def container-base + {:align-items :center + :justify-content :center + :border-radius 10 + :height 32 + :width 32}) + +(defn text-base + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :text-align :center}) + +(defn in-range-background + [{:keys [in-range theme]}] + (cond-> {:position :absolute + :top 0 + :right 0 + :left 0 + :bottom 0} + (= in-range :start) + (assoc :background-color + (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :left 20) + + (= in-range :middle) + (assoc :background-color + (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)) + + (= in-range :end) + (assoc :background-color + (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :right 20))) + +(defn container + [{:keys [state theme customization-color]}] + (cond-> container-base + (= state :default) + (assoc :background-color colors/neutral-100-opa-0) + + (= state :disabled) + (assoc :opacity 0.3) + + (= state :selected) + (assoc :background-color (colors/resolve-color customization-color theme)))) + +(defn text + [{:keys [state theme]}] + (cond-> (text-base theme) + (= state :selected) (assoc :color colors/white))) + +(defn indicator + [{:keys [state theme customization-color]}] + {:width 4 + :position :absolute + :bottom 3 + :height 2 + :border-radius 8 + :background-color (if (= state :today) + (colors/resolve-color customization-color theme) + colors/neutral-100-opa-0)}) diff --git a/src/quo/components/calendar/calendar_day/view.cljs b/src/quo/components/calendar/calendar_day/view.cljs new file mode 100644 index 00000000000..60434a66d94 --- /dev/null +++ b/src/quo/components/calendar/calendar_day/view.cljs @@ -0,0 +1,31 @@ +(ns quo.components.calendar.calendar-day.view + (:require + [quo.components.calendar.calendar-day.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [state in-range on-press customization-color] + :or {state :default}} + day] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/wrapper} + [rn/view {:style (style/in-range-background {:in-range in-range :theme theme})}] + [rn/touchable-opacity + {:on-press on-press + :style (style/container + {:state state + :theme theme + :customization-color customization-color}) + :disabled (= state :disabled)} + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/text {:state state :theme theme})} + day] + [rn/view + {:style (style/indicator + {:state state + :theme theme + :customization-color customization-color})}]]])) diff --git a/src/quo/components/calendar/calendar_year/component_spec.cljs b/src/quo/components/calendar/calendar_year/component_spec.cljs new file mode 100644 index 00000000000..2efd2601e33 --- /dev/null +++ b/src/quo/components/calendar/calendar_year/component_spec.cljs @@ -0,0 +1,15 @@ +(ns quo.components.calendar.calendar-year.component-spec + (:require + [quo.components.calendar.calendar-year.view :as calendar-year] + [test-helpers.component :as h])) + +(h/describe "calendar-year component" + (h/test "default render of calendar-year component" + (h/render [calendar-year/view {} "2023"]) + (h/is-truthy (h/query-by-text "2023"))) + + (h/test "should not call on-press when disabled" + (let [on-press (h/mock-fn)] + (h/render [calendar-year/view {:on-press on-press :disabled? true} "2023"]) + (h/fire-event :press (h/query-by-text "2023")) + (h/was-not-called on-press)))) diff --git a/src/quo/components/calendar/calendar_year/style.cljs b/src/quo/components/calendar/calendar_year/style.cljs new file mode 100644 index 00000000000..795ebda7e1c --- /dev/null +++ b/src/quo/components/calendar/calendar_year/style.cljs @@ -0,0 +1,26 @@ +(ns quo.components.calendar.calendar-year.style + (:require + [quo.foundations.colors :as colors])) + +(def container-base + {:align-items :center + :justify-content :center + :border-radius 10 + :height 32 + :width 48}) + +(defn text-base + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn container + [{:keys [selected? disabled? theme]}] + (cond-> container-base + disabled? (assoc :opacity 0.3) + selected? (assoc :background-color + (colors/theme-colors colors/neutral-10 colors/neutral-70 theme)))) + +(defn text + [{:keys [selected? theme]}] + (cond-> (text-base theme) + selected? (assoc :color (colors/theme-colors colors/neutral-100 colors/white theme)))) diff --git a/src/quo/components/calendar/calendar_year/view.cljs b/src/quo/components/calendar/calendar_year/view.cljs new file mode 100644 index 00000000000..b32409ed4bc --- /dev/null +++ b/src/quo/components/calendar/calendar_year/view.cljs @@ -0,0 +1,24 @@ +(ns quo.components.calendar.calendar-year.view + (:require + [quo.components.calendar.calendar-year.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [selected? disabled? on-press]} year] + (let [theme (quo.context/use-theme)] + [rn/touchable-opacity + {:on-press on-press + :style (style/container + {:selected? selected? + :disabled? disabled? + :theme theme}) + :disabled disabled?} + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/text + {:selected? selected? + :theme theme})} + year]])) diff --git a/src/quo/components/cards/wallet_card/component_spec.cljs b/src/quo/components/cards/wallet_card/component_spec.cljs new file mode 100644 index 00000000000..3141214440c --- /dev/null +++ b/src/quo/components/cards/wallet_card/component_spec.cljs @@ -0,0 +1,34 @@ +(ns quo.components.cards.wallet-card.component-spec + (:require + [quo.components.cards.wallet-card.view :as wallet-card] + [quo.foundations.resources :as resources] + [test-helpers.component :as h])) + +(def ^:private base-props + {:image (resources/get-image :keycard-logo) + :title "Buy" + :subtitle "Start investing now"}) + +(h/describe "cards: wallet card" + (h/test "Test default render" + (let [event (h/mock-fn)] + (h/render-with-theme-provider [wallet-card/view + (assoc base-props + :on-press + event)]) + (h/is-truthy (h/get-by-label-text :wallet-card)) + (h/is-truthy (h/get-by-text "Buy")) + (h/is-truthy (h/get-by-text "Start investing now")) + (h/is-truthy (h/get-by-label-text :image)) + (h/fire-event :press (h/get-by-label-text :wallet-card)) + (h/was-called event))) + + (h/test "Test render with dismissible prop" + (let [event (h/mock-fn)] + (h/render-with-theme-provider [wallet-card/view + (assoc base-props + :dismissible? true + :on-press-close event)]) + (h/is-truthy (h/get-by-label-text :close-icon)) + (h/fire-event :press (h/get-by-label-text :icon-container)) + (h/was-called event)))) diff --git a/src/quo/components/cards/wallet_card/schema.cljs b/src/quo/components/cards/wallet_card/schema.cljs new file mode 100644 index 00000000000..a4088d15169 --- /dev/null +++ b/src/quo/components/cards/wallet_card/schema.cljs @@ -0,0 +1,15 @@ +(ns quo.components.cards.wallet-card.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:image :schema.common/image-source] + [:title :string] + [:subtitle :string] + [:dismissible? {:optional true} :boolean] + [:on-press {:optional true} fn?] + [:on-press-close {:optional true} fn?] + [:container-style {:optional true} :map]]]] + :any]) diff --git a/src/quo/components/cards/wallet_card/style.cljs b/src/quo/components/cards/wallet_card/style.cljs new file mode 100644 index 00000000000..faaa1b43e86 --- /dev/null +++ b/src/quo/components/cards/wallet_card/style.cljs @@ -0,0 +1,31 @@ +(ns quo.components.cards.wallet-card.style + (:require [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows])) + +(defn root-container + [theme container-style] + (merge (shadows/get 2 theme) + {:border-radius 16 + :padding-vertical 10 + :padding-horizontal 12 + :width 161 + :background-color (colors/theme-colors colors/white colors/neutral-90 theme)} + container-style)) + +(def top-container + {:flex-direction :row + :height 32 + :justify-content :space-between + :margin-bottom 8}) + +(def image + {:height 32 + :width 32}) + +(defn title + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn subtitle + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo/components/cards/wallet_card/view.cljs b/src/quo/components/cards/wallet_card/view.cljs new file mode 100644 index 00000000000..748405bd124 --- /dev/null +++ b/src/quo/components/cards/wallet_card/view.cljs @@ -0,0 +1,44 @@ +(ns quo.components.cards.wallet-card.view + (:require [quo.components.cards.wallet-card.schema :as component-schema] + [quo.components.cards.wallet-card.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [schema.core :as schema])) + +(defn- view-internal + [{:keys [image title subtitle dismissible? on-press on-press-close container-style]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/root-container theme container-style)} + [rn/pressable + {:on-press on-press + :accessibility-label :wallet-card} + [rn/view {:style style/top-container} + [fast-image/fast-image + {:style style/image + :source image + :accessibility-label :image}] + (when dismissible? + [rn/pressable + {:on-press on-press-close + :accessibility-label :icon-container + :hit-slop {:top 5 :bottom 5 :left 5 :right 5}} + [icon/icon :i/close + {:size 12 + :accessibility-label :close-icon}]])] + [text/text + {:style (style/title theme) + :size :paragraph-1 + :weight :semi-bold + :number-of-lines 1} + title] + [text/text + {:style (style/subtitle theme) + :size :paragraph-2 + :weight :regular + :number-of-lines 1} + subtitle]]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/code/common/style.cljs b/src/quo/components/code/common/style.cljs new file mode 100644 index 00000000000..bc3b698604a --- /dev/null +++ b/src/quo/components/code/common/style.cljs @@ -0,0 +1,104 @@ +(ns quo.components.code.common.style + (:require + [quo.foundations.colors :as colors])) + +;; Example themes: +;; https://github.com/react-syntax-highlighter/react-syntax-highlighter/tree/master/src/styles/hljs +(defn highlight-theme + [class-name theme] + (case class-name + :hljs-comment (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + :hljs-title (colors/resolve-color :sky theme) + :hljs-keyword (colors/resolve-color :green theme) + :hljs-string (colors/resolve-color :turquoise theme) + :hljs-literal (colors/resolve-color :turquoise theme) + :hljs-number (colors/resolve-color :turquoise theme) + :hljs-symbol (colors/resolve-color :orange theme) + :hljs-builtin-name (colors/resolve-color :pink theme) + :line-number colors/neutral-40 + nil)) + +(defn text-style + [class-names preview? theme] + (let [text-color (->> class-names + (map keyword) + (some (fn [class-name] + (when-let [text-color (highlight-theme class-name theme)] + text-color))))] + (cond-> {:flex-shrink 1 + :line-height 18} + preview? (assoc :color colors/white) + text-color (assoc :color text-color)))) + +(defn border-color + [theme] + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)) + +(defn container + [preview? theme] + (if preview? + {:overflow :hidden + :width 108 + :background-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :border-radius 8} + {:overflow :hidden + :padding 8 + :background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme) + :border-color (border-color theme) + :border-width 1 + :border-radius 16})) + +(def gradient-container + {:position :absolute + :bottom 0 + :left 0 + :right 0 + :z-index 1}) + +(def gradient {:height 48}) + +(defn line-number-container + [line-number-width preview? theme] + (cond-> {:position :absolute + :bottom 0 + :top 0 + :left 0 + :width (+ line-number-width 8 7) + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)} + preview? (assoc :width 20 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-70 theme)))) + +(defn divider + [line-number-width theme] + {:position :absolute + :bottom 0 + :top 0 + :left (+ line-number-width 7 7) + :width 1 + :z-index 2 + :background-color (border-color theme)}) + +(def line {:flex-direction :row}) + +(defn line-number + [width preview?] + (if preview? + {:width width + :padding-vertical 3 + :padding-left 8 + :padding-right 4} + {:margin-right 20 ; 8+12 margin + :width width})) + +(def line-content + {:flex 1 + :padding-horizontal 8 + :padding-vertical 3}) + +(def copy-button + {:position :absolute + :bottom 8 + :right 8 + :z-index 1}) + +(defn gradient-color [theme] (colors/theme-colors colors/white colors/neutral-80 theme)) diff --git a/src/quo/components/code/common/view.cljs b/src/quo/components/code/common/view.cljs new file mode 100644 index 00000000000..a51fc79cfea --- /dev/null +++ b/src/quo/components/code/common/view.cljs @@ -0,0 +1,130 @@ +(ns quo.components.code.common.view + (:require + [cljs-bean.core :as bean] + [clojure.string :as string] + [quo.components.buttons.button.view :as button] + [quo.components.code.common.style :as style] + [quo.components.markdown.text :as text] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [react-native.syntax-highlighter :as highlighter] + [reagent.core :as reagent])) + +(defn- render-nodes + [nodes theme preview?] + (map (fn [{:keys [children value last-line?] :as node}] + (let [classname (get-in node [:properties :className])] + (if children + (into [text/text + (cond-> {:weight :code + :size :paragraph-2 + :style (style/text-style classname preview? theme)} + last-line? (assoc :number-of-lines 1))] + (render-nodes children theme preview?)) + ;; Remove newlines as we already render each line separately. + (string/trim-newline value)))) + nodes)) + +(defn- line + [{:keys [line-number line-number-width preview? theme]} children] + [rn/view {:style style/line} + [rn/view {:style (style/line-number line-number-width preview?)} + [text/text + {:style (style/text-style ["line-number"] preview? theme) + :weight :code + :size :paragraph-2} + line-number]] + (cond->> children + preview? (conj [rn/view {:style style/line-content}]))]) + +(defn- code-block + [{:keys [rows line-number-width preview? theme]}] + [rn/view + (->> preview? + (render-nodes rows theme) + (map-indexed (fn [idx row-content] + [line + {:line-number (inc idx) + :line-number-width line-number-width + :preview? preview? + :theme theme} + row-content])) + (into [:<>]))]) + +(defn- mask-view + [{:keys [apply-mask? theme]} child] + (if apply-mask? + [:<> + [rn/view {:style style/gradient-container} + [linear-gradient/linear-gradient + {:style style/gradient + :colors [:transparent (style/gradient-color theme)]} + [rn/view {:style style/gradient}]]] + child] + child)) + +(defn- calc-line-number-width + [font-scale rows-to-show] + (let [max-line-digits (-> rows-to-show str count)] + (if (= 1 max-line-digits) + 18 ;; ~ 9 is char width, 18 is width used in Figma. + (* 9 max-line-digits font-scale)))) + +(defn- f-native-renderer + [{:keys [rows max-lines on-copy-press preview? theme] + :or {max-lines ##Inf}}] + (let [font-scale (:font-scale (rn/get-window)) + total-rows (count rows) + number-rows-to-show (if preview? + 0 + (min (count rows) max-lines)) + line-number-width (if preview? 20 (calc-line-number-width font-scale number-rows-to-show)) + truncated? (< number-rows-to-show total-rows) + rows-to-show-coll (if truncated? + (as-> rows $ + (update $ number-rows-to-show assoc :last-line? true) + (take (inc number-rows-to-show) $)) + rows)] + [rn/view {:style (style/container preview? theme)} + [rn/view {:style (style/line-number-container line-number-width preview? theme)}] + (if preview? + [code-block + {:rows rows-to-show-coll + :line-number-width line-number-width + :preview? preview? + :theme theme}] + [:<> + [rn/view {:style (style/divider line-number-width theme)}] + [mask-view + {:apply-mask? truncated? + :theme theme} + [code-block + {:rows rows-to-show-coll + :line-number-width line-number-width + :theme theme}]] + [rn/view {:style style/copy-button} + [button/button + {:icon-only? true + :type :grey + :background :blur + :size 24 + :on-press on-copy-press} + :main-icons/copy]]])])) + +(defn view + [{:keys [language max-lines on-copy-press preview? theme]} children] + [highlighter/highlighter + {:language language + :renderer (fn [^js/Object props] + (reagent/as-element + [:f> f-native-renderer + {:rows (-> props .-rows bean/->clj) + :on-copy-press #(when on-copy-press (on-copy-press children)) + :max-lines max-lines + :preview? preview? + :theme theme}])) + :show-line-numbers false + :style {} + :custom-style {:background-color nil}} + children]) + diff --git a/src/quo/components/code/snippet/view.cljs b/src/quo/components/code/snippet/view.cljs new file mode 100644 index 00000000000..2999ca8b9f2 --- /dev/null +++ b/src/quo/components/code/snippet/view.cljs @@ -0,0 +1,12 @@ +(ns quo.components.code.snippet.view + (:require + [quo.components.code.common.view :as code-common])) + +(defn view + [{:keys [language max-lines on-copy-press]} children] + [code-common/view + {:language language + :max-lines max-lines + :on-copy-press on-copy-press + :preview? false} + children]) diff --git a/src/quo/components/code/snippet_preview/view.cljs b/src/quo/components/code/snippet_preview/view.cljs new file mode 100644 index 00000000000..bea8eea44bf --- /dev/null +++ b/src/quo/components/code/snippet_preview/view.cljs @@ -0,0 +1,12 @@ +(ns quo.components.code.snippet-preview.view + (:require + [quo.components.code.common.view :as code-common])) + +(defn view + [{:keys [language]} children] + [code-common/view + {:preview? true + :language language + :max-lines 0 + :theme :dark} + children]) diff --git a/src/quo/components/colors/color/constants.cljs b/src/quo/components/colors/color/constants.cljs new file mode 100644 index 00000000000..aec56c88924 --- /dev/null +++ b/src/quo/components/colors/color/constants.cljs @@ -0,0 +1,3 @@ +(ns quo.components.colors.color.constants) + +(def ^:const color-size 48) diff --git a/src/quo/components/colors/color/style.cljs b/src/quo/components/colors/color/style.cljs new file mode 100644 index 00000000000..1099ec3d795 --- /dev/null +++ b/src/quo/components/colors/color/style.cljs @@ -0,0 +1,69 @@ +(ns quo.components.colors.color.style + (:require + [quo.components.colors.color.constants :as constants] + [quo.foundations.colors :as colors] + [utils.responsiveness :as responsiveness])) + +(defn color-button-common + [window-width] + {:width constants/color-size + :height constants/color-size + :border-width 4 + :border-radius 24 + :margin-right (-> window-width + (- responsiveness/IPHONE_11_PRO_VIEWPORT_WIDTH) ;We want the design to be 100% + ;identical to Figma on iPhone 11 + ;Pro, So we're using it's VW here. + (/ 6) ;Dividing by an appropriate factor to get a reasonable value for each VW + ;on other devices (This will yield 0 on iPhone 11 Pro, Which is what we + ;want) + (+ 8)) ;Add same margin that's on Figma for 11 Pro, This value will increase + ;based on the device VW because of the division above. + :transform [{:rotate "45deg"}] + :border-color :transparent}) + +(defn color-button + ([color selected?] + (color-button color selected? nil nil)) + ([color selected? idx window-width] + (cond-> (color-button-common window-width) + selected? (assoc :border-top-color (colors/alpha color 0.4) + :border-end-color (colors/alpha color 0.4) + :border-bottom-color (colors/alpha color 0.2) + :border-start-color (colors/alpha color 0.2) + :border-color nil) + (zero? idx) (assoc :margin-left -4)))) + +(defn color-circle + [color border?] + {:width 40 + :height 40 + :transform [{:rotate "-45deg"}] + :background-color color + :justify-content :center + :align-items :center + :border-color color + :border-width (if border? 2 0) + :overflow :hidden + :border-radius 20}) + +(defn feng-shui + [theme] + {:width 40 + :height 40 + :transform [{:rotate "45deg"}] + :overflow :hidden + :border-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-width 2 + :background-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-radius 20}) + +(defn left-half + [theme] + {:flex 1 + :background-color (colors/theme-colors colors/white colors/neutral-100 theme)}) + +(defn right-half + [theme] + {:flex 1 + :background-color (colors/theme-colors colors/neutral-100 colors/white theme)}) diff --git a/src/quo/components/colors/color/view.cljs b/src/quo/components/colors/color/view.cljs new file mode 100644 index 00000000000..cc703e82b19 --- /dev/null +++ b/src/quo/components/colors/color/view.cljs @@ -0,0 +1,46 @@ +(ns quo.components.colors.color.view + (:require + [quo.components.colors.color.style :as style] + [quo.components.icon :as icon] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- feng-shui + [{:keys [color theme]}] + [rn/view + {:accessibility-label color + :style (style/feng-shui theme)} + [rn/view {:style (style/left-half theme)}] + [rn/view {:style (style/right-half theme)}]]) + +(defn view + [{:keys [color selected? on-press blur? idx window-width] + :as props}] + (let [theme (quo.context/use-theme) + border? (and (not blur?) (not selected?)) + + hex-color (if (= :feng-shui color) + (colors/theme-colors colors/neutral-100 colors/white theme) + (colors/resolve-color color theme))] + + [rn/pressable + {:style (style/color-button hex-color selected? idx window-width) + :accessibility-label :color-picker-item + :allow-multiple-presses? true + :on-press #(on-press color)} + (if (and (= :feng-shui color) (not selected?)) + [feng-shui + (assoc props + :theme theme + :hex-color hex-color + :border? border?)] + [rn/view + {:accessibility-label color + :style (style/color-circle hex-color border?)} + (when selected? + [icon/icon :i/check + {:size 20 + :color (if (= :feng-shui color) + (colors/theme-colors colors/white colors/neutral-100 theme) + colors/white)}])])])) diff --git a/src/quo/components/colors/color_picker/component_spec.cljs b/src/quo/components/colors/color_picker/component_spec.cljs new file mode 100644 index 00000000000..527f1235679 --- /dev/null +++ b/src/quo/components/colors/color_picker/component_spec.cljs @@ -0,0 +1,18 @@ +(ns quo.components.colors.color-picker.component-spec + (:require + [quo.components.colors.color-picker.view :as color-picker] + [test-helpers.component :as h])) + +(h/describe "color-picker" + (h/test "clicks on a color item" + (let [event (h/mock-fn)] + (h/render [color-picker/view {:on-change #(event)}]) + (h/fire-event :press (get (h/get-all-by-label-text :color-picker-item) 0)) + (-> (h/expect event) + (.toHaveBeenCalled)))) + (h/test "color picker color changed" + (let [selected (atom nil)] + (h/render [color-picker/view {:on-change #(reset! selected %)}]) + (h/fire-event :press (get (h/get-all-by-label-text :color-picker-item) 0)) + (-> (h/expect @selected) + (.toStrictEqual :blue))))) diff --git a/src/quo/components/colors/color_picker/view.cljs b/src/quo/components/colors/color_picker/view.cljs new file mode 100644 index 00000000000..5db7bae5b38 --- /dev/null +++ b/src/quo/components/colors/color_picker/view.cljs @@ -0,0 +1,67 @@ +(ns quo.components.colors.color-picker.view + (:require + [quo.components.colors.color.constants :as constants] + [quo.components.colors.color.view :as color] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture])) + +(defn get-item-layout + [_ index] + #js + {:length constants/color-size + :offset (* (+ constants/color-size 8) index) + :index index}) + +(defn view + "Options + - `default-selected` Default selected color name. + - `on-change` Callback called when a color is selected `(fn [color-name])`. + - `blur?` Boolean to enable blur background support.}" + [{:keys [default-selected blur? on-change feng-shui? container-style]}] + (let [[selected set-selected] (rn/use-state default-selected) + {window-width :width} (rn/get-window) + ref (rn/use-ref-atom nil) + on-ref (rn/use-callback #(reset! ref %)) + render-fn (rn/use-callback + (fn [color idx] + [color/view + {:selected? (= color selected) + :on-press (fn [color-name] + (.scrollToIndex ^js @ref + #js + {:animated true + :index idx + :viewPosition 0.5}) + (set-selected color-name) + (when on-change (on-change color-name))) + :blur? blur? + :key color + :color color + :idx idx + :window-width window-width}]) + [selected blur? on-change @ref])] + (rn/use-mount + (fn [] + (js/setTimeout + (fn [] + (let [index (.indexOf colors/account-colors default-selected)] + (when (and @ref (>= index 0)) + (some-> ^js @ref + (.scrollToIndex #js + {:animated false + :index index + :viewPosition 0.5}))))) + 50))) + [gesture/flat-list + {:ref on-ref + ;; TODO: using :feng-shui? temporarily while b & w is being developed. + ;; https://github.com/status-im/status-mobile/discussions/16676 + :data (if feng-shui? + (conj colors/account-colors :feng-shui) + colors/account-colors) + :render-fn render-fn + :get-item-layout get-item-layout + :horizontal true + :shows-horizontal-scroll-indicator false + :content-container-style container-style}])) diff --git a/src/quo/components/common/dot_separator.cljs b/src/quo/components/common/dot_separator.cljs new file mode 100644 index 00000000000..62dc8cad45c --- /dev/null +++ b/src/quo/components/common/dot_separator.cljs @@ -0,0 +1,20 @@ +(ns quo.components.common.dot-separator + (:require [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn view + [{:keys [color size container-style] + :or {size 2}}] + (let [theme (quo.context/use-theme) + default-color (colors/theme-colors colors/neutral-80-opa-60 colors/neutral-40 theme)] + [rn/view + {:style (merge {:padding-horizontal 4 + :align-items :center + :justify-content :center} + container-style)} + [rn/view + {:style {:width size + :height size + :border-radius (/ size 2) + :background-color (or color default-color)}}]])) diff --git a/src/quo/components/common/list/style.cljs b/src/quo/components/common/list/style.cljs new file mode 100644 index 00000000000..e32622eb239 --- /dev/null +++ b/src/quo/components/common/list/style.cljs @@ -0,0 +1,20 @@ +(ns quo.components.common.list.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [blur? theme] + {:border-radius 16 + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/white colors/neutral-95 theme)) + :border-width (if blur? 0 1) + :border-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) + +(defn separator + [blur? theme] + {:height 1 + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) diff --git a/src/quo/components/common/list/view.cljs b/src/quo/components/common/list/view.cljs new file mode 100644 index 00000000000..2196b17f3b3 --- /dev/null +++ b/src/quo/components/common/list/view.cljs @@ -0,0 +1,21 @@ +(ns quo.components.common.list.view + (:require + [quo.components.common.list.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [data render-fn blur? container-style]}] + (let [theme (quo.context/use-theme) + list-items (remove nil? data) + last-index (dec (count list-items))] + [rn/view {:style (merge (style/container blur? theme) container-style)} + (doall + (map-indexed + (fn [index item] + ^{:key index} + [:<> + (render-fn item) + (when-not (= last-index index) + [rn/view {:style (style/separator blur? theme)}])]) + list-items))])) diff --git a/src/quo/components/common/new_feature_dot.cljs b/src/quo/components/common/new_feature_dot.cljs new file mode 100644 index 00000000000..00c8614fa14 --- /dev/null +++ b/src/quo/components/common/new_feature_dot.cljs @@ -0,0 +1,13 @@ +(ns quo.components.common.new-feature-dot + (:require [quo.components.common.new-feature-gradient :as new-feature-gradient])) + +(def ^:const size 8) + +(defn view + [{:keys [style accessibility-label]}] + [new-feature-gradient/view + {:style (merge {:width size + :height size + :border-radius (/ size 2)} + style) + :accessibility-label accessibility-label}]) diff --git a/src/quo/components/common/new_feature_gradient.cljs b/src/quo/components/common/new_feature_gradient.cljs new file mode 100644 index 00000000000..0bb3fd87509 --- /dev/null +++ b/src/quo/components/common/new_feature_gradient.cljs @@ -0,0 +1,17 @@ +(ns quo.components.common.new-feature-gradient + (:require [quo.context] + [quo.foundations.colors :as colors] + [react-native.linear-gradient :as linear-gradient])) + +(defn view + [{:keys [style accessibility-label]} & children] + (let [theme (quo.context/use-theme)] + (into [linear-gradient/linear-gradient + {:style style + :accessibility-label accessibility-label + :colors [(colors/resolve-color :yellow theme) + colors/new-feature-gradient-pink] + :use-angle true + :angle 78 + :angle-center {:x 0.5 :y 0.5}}] + children))) diff --git a/src/quo/components/common/no_flicker_image.cljs b/src/quo/components/common/no_flicker_image.cljs new file mode 100644 index 00000000000..66126d3dc58 --- /dev/null +++ b/src/quo/components/common/no_flicker_image.cljs @@ -0,0 +1,46 @@ +(ns quo.components.common.no-flicker-image + (:require + [oops.core :as oops] + [react-native.core :as rn] + [react-native.platform :as platform] + [reagent.core :as reagent])) + +(def cached-sources (js/Set. (js/Array.))) + +(defn- caching-image + [{:keys [source]} on-source-loaded] + (reagent/create-class + {:component-did-update + (fn [] + (when (oops/ocall cached-sources "has" source) + (on-source-loaded source))) + :reagent-render + (fn [{:keys [source] :as props}] + [rn/image + (assoc props + ;; hide the cache image under the real one + ;; have to render it for the on-load event + :style {:width 1 + :height 1 + :left "50%" + :top "50%" + :position :absolute} + :on-load + (fn [_] + (when-not (oops/ocall cached-sources "has" source) + (oops/ocall cached-sources "add" source) + (on-source-loaded source))) + :on-error js/console.error)])})) + +(defn image + "Same as rn/image but cache the image source in a js/Set, so the image won't + flicker when re-render on android" + [props] + (let [[loaded-source set-loaded-source] (rn/use-state nil)] + (if platform/ios? + [rn/image props] + [:<> + [rn/image + (cond-> props + loaded-source (assoc :source loaded-source))] + [caching-image props set-loaded-source]]))) diff --git a/src/quo/components/common/notification_dot/style.cljs b/src/quo/components/common/notification_dot/style.cljs new file mode 100644 index 00000000000..9aa92939a27 --- /dev/null +++ b/src/quo/components/common/notification_dot/style.cljs @@ -0,0 +1,21 @@ +(ns quo.components.common.notification-dot.style + (:require + [quo.foundations.colors :as colors])) + +(def ^:const size 8) + +(defn dot-background-color + [customization-color theme blur?] + (cond + customization-color (colors/resolve-color customization-color theme) + blur? (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :else (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))) + +(defn notification-dot + [customization-color theme blur?] + {:background-color (dot-background-color customization-color theme blur?) + :width size + :height size + :border-radius (/ size 2) + :position :absolute + :z-index 1}) diff --git a/src/quo/components/common/notification_dot/view.cljs b/src/quo/components/common/notification_dot/view.cljs new file mode 100644 index 00000000000..6d24bde1c15 --- /dev/null +++ b/src/quo/components/common/notification_dot/view.cljs @@ -0,0 +1,14 @@ +(ns quo.components.common.notification-dot.view + (:require + [quo.components.common.notification-dot.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [customization-color style blur?]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :notification-dot + :style (merge + (style/notification-dot customization-color theme blur?) + style)}])) diff --git a/src/quo/components/common/separator/view.cljs b/src/quo/components/common/separator/view.cljs new file mode 100644 index 00000000000..aeb81e47388 --- /dev/null +++ b/src/quo/components/common/separator/view.cljs @@ -0,0 +1,19 @@ +(ns quo.components.common.separator.view + (:require + [quo.context] + [quo.foundations.colors :as quo.colors] + [react-native.core :as rn])) + +(defn separator + [{:keys [style]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style + (merge + style + {:background-color (quo.colors/theme-colors + quo.colors/neutral-10 + quo.colors/neutral-80 + theme) + :align-self :stretch + :height 1})}])) diff --git a/src/quo/components/common/unread_grey_dot/style.cljs b/src/quo/components/common/unread_grey_dot/style.cljs new file mode 100644 index 00000000000..2249d414dcd --- /dev/null +++ b/src/quo/components/common/unread_grey_dot/style.cljs @@ -0,0 +1,13 @@ +(ns quo.components.common.unread-grey-dot.style + (:require + [quo.foundations.colors :as colors])) + +(defn unread-grey-dot + [theme] + {:width 8 + :height 8 + :border-radius 4 + :background-color (colors/theme-colors + colors/neutral-40 + colors/neutral-60 + theme)}) diff --git a/src/quo/components/common/unread_grey_dot/view.cljs b/src/quo/components/common/unread_grey_dot/view.cljs new file mode 100644 index 00000000000..ac1d1439c6d --- /dev/null +++ b/src/quo/components/common/unread_grey_dot/view.cljs @@ -0,0 +1,12 @@ +(ns quo.components.common.unread-grey-dot.view + (:require + [quo.components.common.unread-grey-dot.style :as style] + [quo.context] + [react-native.core :as rn])) + +(defn unread-grey-dot + [accessibility-label] + (let [theme (quo.context/use-theme)] + [rn/view + (cond-> {:style (style/unread-grey-dot theme)} + accessibility-label (assoc :accessibility-label accessibility-label :accessible true))])) diff --git a/src/quo/components/community/banner/style.cljs b/src/quo/components/community/banner/style.cljs new file mode 100644 index 00000000000..51f31511e7c --- /dev/null +++ b/src/quo/components/community/banner/style.cljs @@ -0,0 +1,33 @@ +(ns quo.components.community.banner.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows])) + +(defn community-card + [theme] + (assoc + (shadows/get 2 theme) + :border-radius 16 + :justify-content :space-between + :background-color (colors/theme-colors colors/white colors/neutral-90 theme) + :flex-direction :row + :margin-horizontal 20 + :margin-top 8 + :margin-bottom 8 + :height 56 + :padding-right 12)) + +(def banner-content + {:flex 1 + :padding-top 8 + :padding-bottom 8 + :border-radius 12}) + +(def banner-title + {:flex 1 + :padding-horizontal 12}) + +(def discover-illustration + {:position :absolute + :top -4 + :right 0}) diff --git a/src/quo/components/community/banner/view.cljs b/src/quo/components/community/banner/view.cljs new file mode 100644 index 00000000000..612fd54cdce --- /dev/null +++ b/src/quo/components/community/banner/view.cljs @@ -0,0 +1,40 @@ +(ns quo.components.community.banner.view + (:require + [quo.components.community.banner.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- card-title-and-description + [title description theme] + [rn/view {:style style/banner-content} + [rn/view {:style style/banner-title} + [text/text + {:accessibility-label :community-name-text + :ellipsize-mode :tail + :number-of-lines 1 + :weight :semi-bold + :size :paragraph-1} + title] + [text/text + {:accessibility-label :community-name-text + :ellipsize-mode :tail + :number-of-lines 1 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :weight :regular + :size :paragraph-2} + description]]]) + +(defn view + [{:keys [title description on-press accessibility-label banner style]}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:on-press on-press + :accessibility-label accessibility-label + :style (merge (style/community-card theme) style)} + [card-title-and-description title description theme] + [rn/image + {:style style/discover-illustration + :source banner + :accessibility-label :discover-communities-illustration}]])) diff --git a/src/quo/components/community/channel_action/style.cljs b/src/quo/components/community/channel_action/style.cljs new file mode 100644 index 00000000000..985643ed9eb --- /dev/null +++ b/src/quo/components/community/channel_action/style.cljs @@ -0,0 +1,25 @@ +(ns quo.components.community.channel-action.style + (:require + [quo.foundations.colors :as colors])) + +(defn channel-action-container + [{:keys [big? disabled?]}] + (cond-> {} + disabled? (assoc :opacity 0.3) + big? (assoc :flex 1) + (not big?) (assoc :width 104))) + +(defn channel-action + [{:keys [color pressed? theme]}] + {:height 102 + :padding 12 + :padding-bottom 10 + :border-radius 16 + :background-color (colors/resolve-color color theme (if pressed? 20 10)) + :justify-content :space-between}) + +(def channel-action-row + {:flex-direction :row + :justify-content :space-between + :align-items :center + :padding-right 2}) diff --git a/src/quo/components/community/channel_action/view.cljs b/src/quo/components/community/channel_action/view.cljs new file mode 100644 index 00000000000..9d7d1b69877 --- /dev/null +++ b/src/quo/components/community/channel_action/view.cljs @@ -0,0 +1,40 @@ +(ns quo.components.community.channel-action.view + (:require + [quo.components.community.channel-action.style :as style] + [quo.components.counter.counter.view :as counter] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [big? customization-color label counter-value icon on-press accessibility-label disabled?]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/view + {:accessibility-label :channel-action + :style (style/channel-action-container + {:big? big? + :disabled? disabled?})} + [rn/pressable + (cond-> {:style (style/channel-action + {:big? big? + :color customization-color + :pressed? pressed? + :theme theme + :disabled? disabled?}) + :accessibility-label accessibility-label} + (not disabled?) (assoc :on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out)) + [rn/view {:style style/channel-action-row} + [icons/icon icon] + (when counter-value + [counter/view {:type :secondary} counter-value])] + [text/text + {:size :paragraph-1 + :weight :medium + :number-of-lines 2} + label]]])) diff --git a/src/quo/components/community/channel_actions/view.cljs b/src/quo/components/community/channel_actions/view.cljs new file mode 100644 index 00000000000..ff4cd380c00 --- /dev/null +++ b/src/quo/components/community/channel_actions/view.cljs @@ -0,0 +1,17 @@ +(ns quo.components.community.channel-actions.view + (:require + [quo.components.community.channel-action.view :as channel-action] + [react-native.core :as rn])) + +(defn view + [{:keys [container-style actions]}] + [rn/view {:style (assoc container-style :flex-direction :row)} + (map-indexed + (fn [index action] + (when action + ^{:key index} + [:<> + [channel-action/view action] + (when (not= action (last actions)) + [rn/view {:style {:width 16}}])])) + actions)]) diff --git a/src/quo/components/community/community_card_view.cljs b/src/quo/components/community/community_card_view.cljs new file mode 100644 index 00000000000..374cdab9094 --- /dev/null +++ b/src/quo/components/community/community_card_view.cljs @@ -0,0 +1,78 @@ +(ns quo.components.community.community-card-view + (:require + [quo.components.community.community-view :as community-view] + [quo.components.community.icon :as icon] + [quo.components.community.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn- loading-card-view + [{:keys [width]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/loading-card width theme)} + [rn/view (style/loading-cover-container theme)] + [rn/view (style/loading-content-container theme) + [rn/view (style/loading-avatar theme)] + [rn/view (style/loading-lock theme)] + [rn/view + {:style style/loading-card-content-container} + [rn/view {:style (style/loading-content-line {:theme theme :width 84 :margin-top 0})}] + [rn/view {:style (style/loading-content-line {:theme theme :width 311 :margin-top 8})}] + [rn/view {:style (style/loading-content-line {:theme theme :width 271 :margin-top 8})}]] + [rn/view + {:style style/loading-stats-container} + [rn/view {:style (style/loading-stat-circle theme 0)}] + [rn/view {:style (style/loading-stat-line theme 4)}] + [rn/view {:style (style/loading-stat-circle theme 12)}] + [rn/view {:style (style/loading-stat-line theme 4)}]] + [rn/view + {:style style/loading-tags-container} + [rn/view {:style (style/loading-tag theme 0)}] + [rn/view {:style (style/loading-tag theme 8)}] + [rn/view {:style (style/loading-tag theme 8)}]]]])) + +(defn- community-card-view + [{:keys [community on-press width]}] + (let [theme (quo.context/use-theme) + {:keys [name description locked? images cover status tokens tags]} community] + [rn/pressable + {:accessibility-label :community-card-item + :on-press on-press + :style (style/community-card 20 theme)} + [rn/view + {:style {:width width + :height 230 + :border-radius 20} + :on-press on-press} + [rn/view {:style style/detail-container} + [rn/view (style/community-cover-container 60) + [rn/image + {:source cover + :style {:flex 1 + :border-top-right-radius 20 + :border-top-left-radius 20}}]] + [rn/view (style/card-view-content-container 12 theme) + [rn/view (style/card-view-chat-icon 48 theme) + [icon/community-icon {:images images} 48]] + (when (= status :gated) + [rn/view style/permission-tag-styles + [community-view/permission-tag-container + {:locked? locked? + :status status + :tokens tokens}]]) + [community-view/community-title + {:title name + :description description}] + [rn/view {:style style/card-stats-position} + [community-view/community-stats-column + {:type :card-view + :members-count (:members-count community) + :active-members-count (:active-members-count community)}]] + [rn/view {:style style/community-tags-position} + [community-view/community-tags {:tags tags}]]]]]])) + +(defn view + [{:keys [loading?] :as props}] + (if-not loading? + [community-card-view props] + [loading-card-view props])) diff --git a/src/quo/components/community/community_detail_token_gating/component_spec.cljs b/src/quo/components/community/community_detail_token_gating/component_spec.cljs new file mode 100644 index 00000000000..5ce40919856 --- /dev/null +++ b/src/quo/components/community/community_detail_token_gating/component_spec.cljs @@ -0,0 +1,71 @@ +(ns quo.components.community.community-detail-token-gating.component-spec + (:require + [quo.core :as quo] + [test-helpers.component :as h])) + +(h/describe "Community Detail Token Gating Component" + (h/test "render with member permissions" + (h/render + [quo/community-detail-token-gating + {:permissions [{:role 2 + :role-text "Member" + :satisfied? true + :tokens [[{:symbol "ETH" + :sufficient? true + :collectible? false + :amount 0.8 + :img-src nil}] + [{:symbol "ETH" + :sufficient? false + :collectible? false + :amount 1 + :img-src nil} + {:symbol "STT" + :sufficient? false + :collectible? false + :amount 10 + :img-src nil}]]}]}]) + (h/is-truthy (h/get-by-translation-text :t/you-eligible-to-join-as {:role "Member"})) + (h/is-truthy (h/get-by-text "0.8 ETH")) + (h/is-truthy (h/get-by-text "1 ETH")) + (h/is-truthy (h/get-by-text "10 STT"))) + + (h/test "render with admin permissions" + (h/render + [quo/community-detail-token-gating + {:permissions [{:role 1 + :role-text "Admin" + :satisfied? true + :tokens [[{:symbol "ETH" + :sufficient? true + :collectible? false + :amount 2 + :img-src nil}]]}]}]) + (h/is-truthy (h/get-by-translation-text :t/you-eligible-to-join-as {:role "Admin"})) + (h/is-truthy (h/get-by-text "2 ETH"))) + + (h/test "render with token master permissions" + (h/render-with-theme-provider + [quo/community-detail-token-gating + {:permissions [{:role 5 + :role-text "Token Master" + :satisfied? true + :tokens [[{:symbol "TMANI " + :sufficient? true + :collectible? true + :img-src {:uri "mock-image"}}]]}]}]) + (h/is-truthy (h/get-by-translation-text :t/you-eligible-to-join-as {:role "Token Master"})) + (h/is-truthy (h/get-by-text "TMANI"))) + + (h/test "render with token owner permissions" + (h/render-with-theme-provider + [quo/community-detail-token-gating + {:permissions [{:role 6 + :role-text "Token Owner" + :satisfied? true + :tokens [[{:symbol "TOANI" + :sufficient? true + :collectible? true + :img-src {:uri "mock-image"}}]]}]}]) + (h/is-truthy (h/get-by-translation-text :t/you-eligible-to-join-as {:role "Token Owner"})) + (h/is-truthy (h/get-by-text "TOANI")))) diff --git a/src/quo/components/community/community_detail_token_gating/style.cljs b/src/quo/components/community/community_detail_token_gating/style.cljs new file mode 100644 index 00000000000..c80812d6a32 --- /dev/null +++ b/src/quo/components/community/community_detail_token_gating/style.cljs @@ -0,0 +1,20 @@ +(ns quo.components.community.community-detail-token-gating.style) + +(def container + {:padding-horizontal 20 + :margin-vertical -4}) + +(defn token-row + [first?] + {:flex-direction :row + :margin-top (if first? 8 4) + :row-gap 10 + :column-gap 8 + :flex-wrap :wrap + :margin-bottom 16}) + +(def divider + {:padding-left 0 + :height 28 + :padding-top 0 + :align-items :flex-start}) diff --git a/src/quo/components/community/community_detail_token_gating/view.cljs b/src/quo/components/community/community_detail_token_gating/view.cljs new file mode 100644 index 00000000000..71a0f5b63d3 --- /dev/null +++ b/src/quo/components/community/community_detail_token_gating/view.cljs @@ -0,0 +1,76 @@ +(ns quo.components.community.community-detail-token-gating.view + (:require [clojure.string :as string] + [quo.components.community.community-detail-token-gating.style :as style] + [quo.components.dividers.divider-label.view :as divider-label] + [quo.components.markdown.text :as text] + [quo.components.tags.collectible-tag.view :as collectible-tag] + [quo.components.tags.token-tag.view :as token-tag] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn- token-view + [{:keys [collectible? img-src amount sufficient?] :as token}] + (let [token-symbol (:symbol token)] + (if collectible? + [collectible-tag/view + {:collectible-name token-symbol + :size :size-24 + :collectible-img-src img-src + :options (when sufficient? + :hold)}] + [token-tag/view + {:token-symbol token-symbol + :size :size-24 + :token-value amount + :token-img-src img-src + :options (when sufficient? :hold)}]))) + +(defn- tokens-row + [{:keys [tokens divider? first?]}] + (let [theme (quo.context/use-theme)] + [:<> + [rn/view + {:style (style/token-row first?)} + (map-indexed (fn [token-index token] + ^{:key (str "token-" token-index)} + [token-view token]) + tokens)] + (when-not divider? + [divider-label/view + {:container-style style/divider} + [text/text + {:size :label + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + (string/lower-case (i18n/label :t/or))]])])) + +(defn- role-view + [{:keys [role tokens satisfied? role-text]}] + (when (seq tokens) + ^{:key (str "role-" role)} + [:<> + [text/text + {:size :paragraph-1 + :weight :medium + :style {:margin-top 4}} + (if satisfied? + (i18n/label :t/you-eligible-to-join-as {:role role-text}) + (i18n/label :t/you-not-eligible-to-join-as {:role role-text}))] + [text/text + {:size :paragraph-2 + :style {:margin-bottom 8}} + (i18n/label (if satisfied? :t/you-hodl :t/you-must-hold))] + + (map-indexed (fn [index tokens-item] + ^{:key (str role "-tokens-" index)} + [tokens-row + {:tokens tokens-item + :first? (zero? index) + :divider? (= index (dec (count tokens)))}]) + tokens)])) + +(defn view + [{:keys [permissions]}] + [rn/view {:style style/container} + (map role-view permissions)]) diff --git a/src/quo/components/community/community_list_view.cljs b/src/quo/components/community/community_list_view.cljs new file mode 100644 index 00000000000..53d1c5557a1 --- /dev/null +++ b/src/quo/components/community/community_list_view.cljs @@ -0,0 +1,92 @@ +(ns quo.components.community.community-list-view + (:require + [quo.components.common.unread-grey-dot.view :refer [unread-grey-dot]] + [quo.components.community.community-view :as community-view] + [quo.components.community.icon :as community-icon] + [quo.components.community.style :as style] + [quo.components.counter.counter.view :as counter] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn notification-view + [{:keys [theme + muted? + unread-messages? + unread-mentions-count + customization-color]}] + [rn/view {:style style/notification-container} + (cond + muted? + [icons/icon :i/muted + {:container-style {:align-items :center + :justify-content :center} + :resize-mode :center + :size 20 + :color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}] + (pos? unread-mentions-count) + [counter/view + {:customization-color customization-color + :type :default} + unread-mentions-count] + + unread-messages? + [unread-grey-dot :unviewed-messages-public])]) + +(defn- title-color + [unread-messages? muted theme] + (cond + muted (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + unread-messages? (colors/theme-colors colors/neutral-100 colors/white theme) + :else (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn communities-membership-list-item + [{:keys [customization-color] :as props} + bottom-sheet? + {:keys [name muted unviewed-messages-count unviewed-mentions-count status + images tokens locked? style]}] + (let [theme (quo.context/use-theme) + unread-messages? (pos? unviewed-messages-count)] + [rn/touchable-highlight + (merge {:underlay-color (colors/theme-colors + colors/neutral-5 + colors/neutral-95 + theme) + :style {:border-radius 12 + :margin-left 12}} + props) + [rn/view (merge style/membership-info-container style) + [community-icon/community-icon + {:images images} 32] + [rn/view + {:flex 1 + :margin-left 12 + :justify-content :center} + [text/text + {:accessibility-label :chat-name-text + :number-of-lines 1 + :ellipsize-mode :tail + :weight :semi-bold + :size :paragraph-1 + :style {:color (title-color unread-messages? muted theme)}} + name]] + + [rn/view + {:justify-content :center + :margin-right (when bottom-sheet? + 16)} + (if (= status :gated) + [community-view/permission-tag-container + {:locked? locked? + :tokens tokens}] + [notification-view + {:theme theme + :customization-color customization-color + :muted? muted + :unread-mentions-count unviewed-mentions-count + :unread-messages? unread-messages?}])]]])) diff --git a/src/quo/components/community/community_stat/component_spec.cljs b/src/quo/components/community/community_stat/component_spec.cljs new file mode 100644 index 00000000000..0307099cb74 --- /dev/null +++ b/src/quo/components/community/community_stat/component_spec.cljs @@ -0,0 +1,18 @@ +(ns quo.components.community.community-stat.component-spec + (:require + [quo.core :as quo] + [test-helpers.component :as h] + utils.money)) + +(h/describe "Community Stat Component" + (h/test "default render" + (h/render [quo/community-stat + {:value "5000" + :icon :i/active-members + :accessibility-label :community-active-members}]) + (h/is-truthy (h/query-by-label-text :community-active-members))) + (h/test "renders community stat with a value" + (h/render [quo/community-stat + {:value "5000" + :icon :i/members}]) + (h/is-truthy (h/query-by-text (utils.money/format-amount "5000"))))) diff --git a/src/quo/components/community/community_stat/style.cljs b/src/quo/components/community/community_stat/style.cljs new file mode 100644 index 00000000000..c5479d1b4b1 --- /dev/null +++ b/src/quo/components/community/community_stat/style.cljs @@ -0,0 +1,12 @@ +(ns quo.components.community.community-stat.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:flex-direction :row + :align-items :center + :height 22}) + +(defn text + [theme] + {:color (colors/theme-colors colors/black colors/white theme) + :margin-left 2}) diff --git a/src/quo/components/community/community_stat/view.cljs b/src/quo/components/community/community_stat/view.cljs new file mode 100644 index 00000000000..90a7ec77832 --- /dev/null +++ b/src/quo/components/community/community_stat/view.cljs @@ -0,0 +1,25 @@ +(ns quo.components.community.community-stat.view + (:require [quo.components.community.community-stat.style :as style] + [quo.components.icon :as quo.icons] + [quo.components.markdown.text :as quo.text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + utils.money)) + +(defn view + [{:keys [value icon style accessibility-label text-size]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge style/container style) + :accessibility-label accessibility-label} + [quo.icons/icon icon + {:size 16 + :container-style {:align-items :center + :justify-content :center} + :resize-mode :center + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}] + [quo.text/text + {:size (or text-size :paragraph-1) + :weight :regular + :style (style/text theme)} (utils.money/format-amount value)]])) diff --git a/src/quo/components/community/community_token_gating/component_spec.cljs b/src/quo/components/community/community_token_gating/component_spec.cljs new file mode 100644 index 00000000000..309fa84d3d5 --- /dev/null +++ b/src/quo/components/community/community_token_gating/component_spec.cljs @@ -0,0 +1,61 @@ +(ns quo.components.community.community-token-gating.component-spec + (:require + [quo.core :as quo] + [test-helpers.component :as h])) + +(h/describe "Community Detail Token Gating Component" + (h/test "render with satisfied permissions" + (let [mock-on-press-fn (h/mock-fn) + mock-on-press-info-fn (h/mock-fn)] + (h/render + [quo/community-token-gating + {:tokens [[{:symbol "ETH" + :sufficient? true + :collectible? false + :amount "0.8" + :img-src nil}] + [{:symbol "ETH" + :sufficient? false + :collectible? false + :amount "1" + :img-src nil} + {:symbol "STT" + :sufficient? false + :collectible? false + :amount "10" + :img-src nil}]] + :community-color "#FF0000" + :role "Member" + :satisfied? true + :on-press mock-on-press-fn + :on-press-info mock-on-press-info-fn}]) + (h/is-truthy (h/get-by-translation-text :t/you-eligible-to-join-as {:role "Member"})) + (h/is-truthy (h/get-by-text "0.8 ETH")) + (h/is-truthy (h/get-by-text "1 ETH")) + (h/is-truthy (h/get-by-text "10 STT")) + (h/fire-event :press (h/get-by-translation-text :t/request-to-join)) + (h/was-called mock-on-press-fn) + (h/fire-event :press (h/get-by-label-text :community-token-gating-info)) + (h/was-called mock-on-press-info-fn))) + + (h/test "render with unsatisfied permissions" + (let [mock-on-press-fn (h/mock-fn) + mock-on-press-info-fn (h/mock-fn)] + (h/render + [quo/community-token-gating + {:tokens [[{:symbol "ETH" + :sufficient? false + :collectible? false + :amount "0.8" + :img-src nil}]] + :community-color "#FF0000" + :role "Member" + :satisfied? false + :on-press mock-on-press-fn + :on-press-info mock-on-press-info-fn}]) + (h/is-truthy (h/get-by-translation-text :t/you-not-eligible-to-join)) + (h/is-truthy (h/get-by-text "0.8 ETH")) + (h/fire-event :press (h/get-by-translation-text :t/request-to-join)) + (h/was-not-called mock-on-press-fn) + (h/fire-event :press (h/get-by-label-text :community-token-gating-info)) + (h/was-called mock-on-press-info-fn)))) diff --git a/src/quo/components/community/community_token_gating/schema.cljs b/src/quo/components/community/community_token_gating/schema.cljs new file mode 100644 index 00000000000..9d0aa3ff847 --- /dev/null +++ b/src/quo/components/community/community_token_gating/schema.cljs @@ -0,0 +1,22 @@ +(ns quo.components.community.community-token-gating.schema) + +(def ^:private ?token-schema + [:map + [:symbol :string] + [:sufficient? :boolean] + [:collectible? :boolean] + [:amount {:optional true} [:maybe :string]] + [:img-src {:optional true} [:maybe :schema.common/image-source]]]) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:tokens [:sequential [:sequential ?token-schema]]] + [:community-color [:or :string :schema.common/customization-color]] + [:role {:optional true} [:maybe :string]] + [:satisfied? :boolean] + [:on-press fn?] + [:on-press-info fn?]]]] + :any]) diff --git a/src/quo/components/community/community_token_gating/style.cljs b/src/quo/components/community/community_token_gating/style.cljs new file mode 100644 index 00000000000..696a5fa0054 --- /dev/null +++ b/src/quo/components/community/community_token_gating/style.cljs @@ -0,0 +1,47 @@ +(ns quo.components.community.community-token-gating.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:padding-bottom 12 + :padding-top 10 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :border-radius 16 + :margin-vertical -4}) + +(def eligibility-row + {:flex-direction :row + :padding-horizontal 12 + :align-items :center}) + +(def divider + {:height 27 + :padding-left 12 + :padding-top 0 + :align-items :flex-start}) + +(def eligibility-label + {:flex 1}) + +(def you-hodl + {:padding-horizontal 12 + :margin-bottom 15}) + +(def join-button + {:padding-horizontal 12 + :margin-top 8}) + +(def token-row + {:flex-direction :row + :padding-horizontal 12 + :row-gap 10 + :column-gap 8 + :flex-wrap :wrap + :margin-bottom 11}) + +;; This wrapper prevents layout shifts caused by border effects on +;; the view's height when the token has a border. +(def token-wrapper + {:height 26 + :justify-content :center}) diff --git a/src/quo/components/community/community_token_gating/view.cljs b/src/quo/components/community/community_token_gating/view.cljs new file mode 100644 index 00000000000..ba81550d6c1 --- /dev/null +++ b/src/quo/components/community/community_token_gating/view.cljs @@ -0,0 +1,89 @@ +(ns quo.components.community.community-token-gating.view + (:require [clojure.string :as string] + [quo.components.buttons.button.view :as button] + [quo.components.community.community-token-gating.schema :as component-schema] + [quo.components.community.community-token-gating.style :as style] + [quo.components.dividers.divider-label.view :as divider-label] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.tags.collectible-tag.view :as collectible-tag] + [quo.components.tags.token-tag.view :as token-tag] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(defn- token-view + [{:keys [collectible? img-src amount sufficient?] :as token}] + (let [token-symbol (:symbol token)] + [rn/view {:style style/token-wrapper} + (if collectible? + [collectible-tag/view + {:collectible-name token-symbol + :size :size-24 + :collectible-img-src img-src + :options (when sufficient? + :hold)}] + [token-tag/view + {:token-symbol token-symbol + :size :size-24 + :token-value amount + :token-img-src img-src + :options (when sufficient? :hold)}])])) + +(defn- tokens-row + [{:keys [theme tokens divider?]}] + [:<> + [rn/view + {:style style/token-row} + (map-indexed (fn [token-index token] + ^{:key (str "token-" token-index)} + [token-view token]) + tokens)] + (when-not divider? + [divider-label/view + {:container-style style/divider} + [text/text + {:size :label + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + (string/lower-case (i18n/label :t/or))]])]) + +(defn- view-internal + [{:keys [tokens community-color role satisfied? on-press on-press-info]}] + (let [theme (quo.context/use-theme) + last-token-index (dec (count tokens))] + [rn/view {:style (style/container theme)} + [rn/view {:style style/eligibility-row} + [text/text + {:size :paragraph-1 + :weight :medium + :style style/eligibility-label} + (if satisfied? + (i18n/label :t/you-eligible-to-join-as {:role role}) + (i18n/label :t/you-not-eligible-to-join))] + [rn/pressable {:on-press on-press-info} + [icon/icon :i/info + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :accessibility-label :community-token-gating-info}]]] + [text/text + {:size :paragraph-2 + :style style/you-hodl} + (i18n/label (if satisfied? :t/you-hodl :t/you-must-hold))] + (map-indexed (fn [index tokens-item] + ^{:key (str role "-tokens-" index)} + [tokens-row + {:tokens tokens-item + :theme theme + :divider? (= index last-token-index)}]) + tokens) + [button/button + {:on-press on-press + :container-style style/join-button + :accessibility-label :join-community-button + :customization-color community-color + :disabled? (not satisfied?) + :icon-left (if satisfied? :i/unlocked :i/locked)} + (i18n/label :t/request-to-join)]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/community/community_view.cljs b/src/quo/components/community/community_view.cljs new file mode 100644 index 00000000000..54ddda83247 --- /dev/null +++ b/src/quo/components/community/community_view.cljs @@ -0,0 +1,81 @@ +(ns quo.components.community.community-view + (:require + [quo.components.community.community-stat.view :as community-stat] + [quo.components.community.style :as style] + [quo.components.markdown.text :as text] + [quo.components.tags.permission-tag :as permission] + [quo.components.tags.tag :as tag] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture])) + +(defn community-stats-column + [{:keys [type members-count active-members-count]}] + [rn/view (if (= type :card-view) style/card-stats-container style/list-stats-container) + [community-stat/view + {:accessibility-label :stats-members-count + :icon :i/group + :value members-count + :style {:margin-right 12}}] + [community-stat/view + {:accessibility-label :stats-active-count + :icon :i/active-members + :value active-members-count}]]) + +(defn community-tags + [{:keys [tags container-style last-item-style]}] + [gesture/scroll-view + {:shows-horizontal-scroll-indicator false + :horizontal true + :style container-style} + (let [last-index (max 0 (dec (count tags)))] + (map-indexed + (fn [index {tag-name :name emoji :emoji}] + (let [last? (= index last-index)] + [rn/view + {:key tag-name + :style (if last? + last-item-style + {:margin-right 8})} + [tag/tag + {:size 24 + :label tag-name + :type :emoji + :labelled? true + :resource emoji}]])) + tags))]) + +(defn community-title + [{:keys [title description size] :or {size :small}}] + [rn/view (style/community-title-description-container (if (= size :large) 56 32)) + (when title + [text/text + {:accessibility-label :chat-name-text + :number-of-lines 1 + :ellipsize-mode :tail + :weight :semi-bold + :size (if (= size :large) :heading-1 :heading-2)} + title]) + (when description + [text/text + {:accessibility-label :community-description-text + :number-of-lines 2 + :ellipsize-mode :tail + :weight :regular + :size :paragraph-1 + :style {:margin-top (if (= size :large) 8 2)}} + description])]) + +(defn permission-tag-container + [{:keys [locked? blur? tokens on-press]}] + (let [theme (quo.context/use-theme)] + [permission/tag + {:accessibility-label :permission-tag + :background-color (if (and (= :dark theme) blur?) + colors/white-opa-10 + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)) + :locked? locked? + :tokens tokens + :size 24 + :on-press on-press}])) diff --git a/src/quo/components/community/icon.cljs b/src/quo/components/community/icon.cljs new file mode 100644 index 00000000000..631e0635e75 --- /dev/null +++ b/src/quo/components/community/icon.cljs @@ -0,0 +1,14 @@ +(ns quo.components.community.icon + (:require + [react-native.core :as rn])) + +(defn community-icon + [{:keys [images]} size] + (let [thumbnail-image (get-in images [:thumbnail :uri])] + [rn/image + {:source {:uri thumbnail-image} + :style {:border-radius 50 + :border-width 0 + :border-color :transparent + :width size + :height size}}])) diff --git a/src/quo/components/community/style.cljs b/src/quo/components/community/style.cljs new file mode 100644 index 00000000000..ec94ff9d9fa --- /dev/null +++ b/src/quo/components/community/style.cljs @@ -0,0 +1,189 @@ +(ns quo.components.community.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows])) + +(defn community-card + [radius theme] + (assoc + (shadows/get 2 theme) + :border-radius radius + :justify-content :space-between + :background-color (colors/theme-colors + colors/white + colors/neutral-90 + theme))) + +(def detail-container + {:flex 1}) + +(def card-stats-container + {:flex-direction :row}) + +(def list-stats-container + {:flex-direction :row + :align-items :center}) + +(def card-stats-position + {:position :absolute + :top 116 + :right 12 + :left 12}) + +(def community-tags-position + {:position :absolute + :top 154 + :right 12 + :left 12}) + +(defn card-view-content-container + [padding-horizontal theme] + {:position :absolute + :top 40 + :bottom 0 + :left 0 + :right 0 + :border-radius 20 + :padding-horizontal padding-horizontal + :border-top-right-radius 16 + :border-top-left-radius 16 + :background-color (colors/theme-colors + colors/white + colors/neutral-90 + theme)}) + +(defn card-view-chat-icon + [icon-size theme] + {:border-radius 48 + :position :absolute + :top (- (/ icon-size 2)) + :left (/ icon-size 4) + :padding 2 + :background-color (colors/theme-colors + colors/white + colors/neutral-90 + theme)}) + +(def membership-info-container + {:flex-direction :row + :border-radius 16 + :align-items :center + :height 48}) + +(defn community-title-description-container + [margin-top] + {:margin-top margin-top}) + +(defn community-cover-container + [height] + {:flex-direction :row + :height height + :border-top-right-radius 20 + :border-top-left-radius 20 + :background-color colors/primary-50-opa-20}) + +(def permission-tag-styles + {:position :absolute + :top 8 + :right 8}) + +(defn loading-card + [width theme] + (merge + {:width width + :height 230 + :border-radius 16 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)} + (shadows/get 2 theme))) + +(defn loading-avatar + [theme] + {:width 48 + :height 48 + :border-radius 24 + :position :absolute + :top -24 + :left 12 + :background-color (colors/theme-colors colors/neutral-20 colors/neutral-70 theme) + :border-color (colors/theme-colors colors/white colors/neutral-90 theme) + :border-width 2}) + +(defn loading-lock + [theme] + {:width 48 + :height 24 + :border-radius 20 + :position :absolute + :top 8 + :right 8 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + + +(defn loading-cover-container + [theme] + {:flex-direction :row + :height 64 + :border-top-right-radius 16 + :border-top-left-radius 16 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +(defn loading-content-container + [theme] + {:position :absolute + :heigth 190 + :top 40 + :bottom 0 + :left 0 + :right 0 + :padding-horizontal 12 + :border-radius 16 + :background-color (colors/theme-colors colors/white colors/neutral-90 theme)}) + +(def loading-tags-container + {:margin-top 20 + :flex-direction :row + :align-items :center}) + +(def loading-stats-container + {:margin-top 20 + :flex-direction :row + :align-items :center}) + +(defn loading-stat-circle + [theme margin-left] + {:height 14 + :width 14 + :border-radius 7 + :background-color (colors/theme-colors colors/neutral-20 colors/neutral-70 theme) + :margin-left margin-left}) + +(defn loading-stat-line + [theme margin-left] + {:height 12 + :width 50 + :border-radius 5 + :background-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :margin-left margin-left}) + +(defn loading-tag + [theme margin-left] + {:height 24 + :width 76 + :border-radius 20 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :margin-left margin-left}) + +(defn loading-content-line + [{:keys [theme width margin-top]}] + {:width width + :height 16 + :background-color (colors/theme-colors colors/neutral-20 colors/neutral-70 theme) + :border-radius 5 + :margin-top margin-top}) + +(def loading-card-content-container {:margin-top 36}) + +(def notification-container + {:width 20 + :display :flex + :align-items :center}) diff --git a/src/quo/components/counter/collectible_counter/component_spec.cljs b/src/quo/components/counter/collectible_counter/component_spec.cljs new file mode 100644 index 00000000000..4add5fa6f2e --- /dev/null +++ b/src/quo/components/counter/collectible_counter/component_spec.cljs @@ -0,0 +1,34 @@ +(ns quo.components.counter.collectible-counter.component-spec + (:require + [quo.components.counter.collectible-counter.view :as collectible-counter] + [test-helpers.component :as h])) + +(defn render + [component] + (h/render-with-theme-provider component :dark)) + +(h/describe "collectible counter component" + (h/test "default render of component" + (render [collectible-counter/view {}]) + (-> (h/expect (h/get-by-label-text :collectible-counter)) + (h/is-truthy))) + + (h/test "render with a string value" + (render [collectible-counter/view {:value "x500"}]) + (-> (h/expect (h/get-by-text "x500")) + (h/is-truthy))) + + (h/test "render with an integer value and default status" + (render [collectible-counter/view + {:value 100 + :status :default}]) + (-> (h/expect (h/get-by-text "100")) + (h/is-truthy))) + + (h/test "render with all availiable props" + (render [collectible-counter/view + {:value "x100" + :status :error + :size :size-24}]) + (-> (h/expect (h/get-by-text "x100")) + (h/is-truthy)))) diff --git a/src/quo/components/counter/collectible_counter/style.cljs b/src/quo/components/counter/collectible_counter/style.cljs new file mode 100644 index 00000000000..efcbb53b032 --- /dev/null +++ b/src/quo/components/counter/collectible_counter/style.cljs @@ -0,0 +1,57 @@ +(ns quo.components.counter.collectible-counter.style + (:require + [quo.foundations.colors :as colors])) + +(defn- get-background-color + [{:keys [status]} theme] + (case status + :default (colors/theme-colors colors/white-opa-70 colors/neutral-95-opa-70 theme) + :error (colors/resolve-color :danger theme 10) + (colors/theme-colors colors/white-opa-70 colors/neutral-95-opa-70 theme))) + +(defn- get-container-border-styles + [{:keys [status]} theme] + (when (= status :error) + {:border-color (colors/resolve-color :danger theme 20) + :border-width 1})) + +(defn- get-container-styles-by-size + [{:keys [size]}] + (let [style-size-24 {:height 24 + :padding-vertical 3 + :padding-horizontal 8} + style-size-32 {:height 32 + :padding-vertical 5 + :padding-horizontal 12}] + (case size + :size-32 style-size-32 + :size-24 style-size-24 + style-size-32))) + +(defn container + [props theme] + (merge {:align-self :flex-start + :flex-direcrion :row + :justify-content :center + :border-radius 999 + :background-color (get-background-color props theme)} + (get-container-border-styles props theme) + (get-container-styles-by-size props))) + +(defn- get-text-color + [{:keys [status]} theme] + (case status + :default (colors/theme-colors colors/neutral-100 colors/white theme) + :error (colors/resolve-color :danger theme) + (colors/theme-colors colors/neutral-100 colors/white theme))) + +(defn get-text-size + [{:keys [size]}] + (case size + :size-32 :paragraph-1 + :size-24 :paragraph-2 + :paragraph-1)) + +(defn text + [props theme] + {:color (get-text-color props theme)}) diff --git a/src/quo/components/counter/collectible_counter/view.cljs b/src/quo/components/counter/collectible_counter/view.cljs new file mode 100644 index 00000000000..52094534030 --- /dev/null +++ b/src/quo/components/counter/collectible_counter/view.cljs @@ -0,0 +1,38 @@ +(ns quo.components.counter.collectible-counter.view + (:require + [quo.components.counter.collectible-counter.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:container-style {:optional true} [:maybe :map]] + [:value {:optional true} [:maybe [:or :string :int]]] + [:status {:optional true} [:maybe :keyword]] + [:size {:optional true} [:maybe [:enum :size-32 :size-24]]] + [:accessibility-label {:optional true} [:maybe :keyword]]]]] + :any]) + +(defn- view-internal + [{:keys [value accessibility-label container-style] + :as props}] + (let [theme (quo.context/use-theme) + default-props {:status :default + :size :size-32} + props (merge default-props props)] + [rn/view + {:accessible true + :accessibility-label (or accessibility-label :collectible-counter) + :style (merge (style/container props theme) container-style)} + [text/text + {:weight :medium + :size (style/get-text-size props) + :style (style/text props theme)} + value]])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/counter/counter/component_spec.cljs b/src/quo/components/counter/counter/component_spec.cljs new file mode 100644 index 00000000000..562f332f66c --- /dev/null +++ b/src/quo/components/counter/counter/component_spec.cljs @@ -0,0 +1,30 @@ +(ns quo.components.counter.counter.component-spec + (:require + [quo.components.counter.counter.view :as counter] + [test-helpers.component :as h])) + +(h/describe "counter component" + (h/test "default render of counter component" + (h/render [counter/view {} nil]) + (-> (h/expect (h/get-by-test-id :counter-component)) + (h/is-truthy))) + + (h/test "renders counter with a string value" + (h/render [counter/view {} "1"]) + (-> (h/expect (h/get-by-text "1")) + (h/is-truthy))) + + (h/test "renders counter with an integer value" + (h/render [counter/view {} 1]) + (-> (h/expect (h/get-by-text "1")) + (h/is-truthy))) + + (h/test "renders counter with max value 99+ by default" + (h/render [counter/view {} 100]) + (-> (h/expect (h/get-by-text "99+")) + (h/is-truthy))) + + (h/test "renders counter with custom max value when set to 150" + (h/render [counter/view {:max-value 150} 151]) + (-> (h/expect (h/get-by-text "150+")) + (h/is-truthy)))) diff --git a/src/quo/components/counter/counter/style.cljs b/src/quo/components/counter/counter/style.cljs new file mode 100644 index 00000000000..42f895941bd --- /dev/null +++ b/src/quo/components/counter/counter/style.cljs @@ -0,0 +1,30 @@ +(ns quo.components.counter.counter.style + (:require + [quo.foundations.colors :as colors])) + +(defn get-color + [type customization-color theme blur?] + (case type + :default (colors/resolve-color customization-color theme) + :secondary (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + :grey (if blur? + (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)) + :outline (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + nil)) + +(defn container + [{:keys [type container-style customization-color theme blur?]}] + (cond-> [{:align-items :center + :justify-content :center + :border-radius 6 + :height 16 + :padding-horizontal 5 + :margin 2} + container-style] + (= type :outline) + (conj {:border-width 1 + :border-color (get-color type customization-color theme blur?)}) + + (not= type :outline) + (conj {:background-color (get-color type customization-color theme blur?)}))) diff --git a/src/quo/components/counter/counter/view.cljs b/src/quo/components/counter/counter/view.cljs new file mode 100644 index 00000000000..be3ce9161fa --- /dev/null +++ b/src/quo/components/counter/counter/view.cljs @@ -0,0 +1,34 @@ +(ns quo.components.counter.counter.view + (:require + [quo.components.counter.counter.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.number])) + +(defn view + [{:keys [type customization-color container-style accessibility-label max-value blur?] + :or {max-value 99 customization-color :blue}} + value] + (let [theme (quo.context/use-theme) + type (or type :default) + value (utils.number/parse-int value) + label (if (> value max-value) + (str max-value "+") + (str value))] + [rn/view + {:test-ID :counter-component + :accessible true + :accessibility-label accessibility-label + :style (style/container + {:type type + :blur? blur? + :customization-color customization-color + :theme theme + :container-style container-style})} + [text/text + {:weight :medium + :size :label + :style (when (= type :default) {:color colors/white})} + label]])) diff --git a/src/quo/components/counter/fraction_counter/style.cljs b/src/quo/components/counter/fraction_counter/style.cljs new file mode 100644 index 00000000000..faa0cc5e010 --- /dev/null +++ b/src/quo/components/counter/fraction_counter/style.cljs @@ -0,0 +1,13 @@ +(ns quo.components.counter.fraction-counter.style + (:require [quo.foundations.colors :as colors])) + +(def counter + {:padding-top 12 + :padding-bottom 2}) + +(defn counter-text + [error? blur? theme] + {:color (cond + error? (colors/theme-colors colors/danger-50 colors/danger-60 theme) + blur? (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :else (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))}) diff --git a/src/quo/components/counter/fraction_counter/view.cljs b/src/quo/components/counter/fraction_counter/view.cljs new file mode 100644 index 00000000000..ede55212cde --- /dev/null +++ b/src/quo/components/counter/fraction_counter/view.cljs @@ -0,0 +1,48 @@ +(ns quo.components.counter.fraction-counter.view + (:require [quo.components.counter.fraction-counter.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema] + [utils.number :as number])) + +(defn- parse-value + [value] + (if (int? value) + value + (number/parse-int value 0))) + +(defn- error-state-counter? + [numerator denominator] + (let [numerator (parse-value numerator) + denominator (parse-value denominator)] + (> numerator denominator))) + +(defn- view-internal + [{:keys [blur? left-value right-value suffix show-counter-warning?]}] + (let [theme (quo.context/use-theme) + counter-warning? (when show-counter-warning? + (error-state-counter? left-value right-value))] + [rn/view {:style style/counter} + [text/text + {:size :paragraph-2 + :weight :regular + :style (style/counter-text counter-warning? blur? theme)} + (str left-value + "/" + right-value + (when suffix + (str " " suffix)))]])) + +(def ?schema + [:=> + [:cat + [:map {:closed true} + [:left-value [:or int? string?]] + [:right-value [:or int? string?]] + [:suffix {:optional true} [:maybe string?]] + [:blur? {:optional true} [:maybe boolean?]] + [:show-counter-warning? {:optional true} [:maybe boolean?]]]] + :any]) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/counter/step/component_spec.cljs b/src/quo/components/counter/step/component_spec.cljs new file mode 100644 index 00000000000..32be19bd841 --- /dev/null +++ b/src/quo/components/counter/step/component_spec.cljs @@ -0,0 +1,24 @@ +(ns quo.components.counter.step.component-spec + (:require + [quo.components.counter.step.view :as step] + [test-helpers.component :as h])) + +(defn render + [component] + (h/render-with-theme-provider component :dark)) + +(h/describe "step component" + (h/test "default render of step component" + (render [step/view {} nil]) + (-> (h/expect (h/query-by-label-text :step-counter)) + (h/is-truthy))) + + (h/test "renders step with a string value" + (render [step/view {} "1"]) + (-> (h/expect (h/get-by-text "1")) + (h/is-truthy))) + + (h/test "renders step with an integer value" + (render [step/view {} 1]) + (-> (h/expect (h/get-by-text "1")) + (h/is-truthy)))) diff --git a/src/quo/components/counter/step/style.cljs b/src/quo/components/counter/step/style.cljs new file mode 100644 index 00000000000..e7c2ad7f279 --- /dev/null +++ b/src/quo/components/counter/step/style.cljs @@ -0,0 +1,46 @@ +(ns quo.components.counter.step.style + (:require + [quo.foundations.colors :as colors])) + +(def container-base + {:align-items :center + :justify-content :center + :border-radius 6 + :height 20}) + +(defn neutral-border-color + [in-blur-view? theme] + (if in-blur-view? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme))) + +(defn active-background-color + [customization-color theme] + (colors/resolve-color customization-color theme 10)) + +(defn complete-background-color + [customization-color] + (colors/resolve-color customization-color :light)) + +(defn container + [{:keys [size type in-blur-view? theme customization-color]}] + (cond-> container-base + (#{1 2} size) (assoc :width 20) + (= size 3) (assoc :width 28) + + (= type :neutral) + (assoc :border-width 1 + :border-color (neutral-border-color in-blur-view? theme)) + + (= type :active) + (assoc :background-color (active-background-color customization-color theme)) + + (= type :complete) + (assoc :background-color (complete-background-color customization-color)))) + +(defn text-color + ([type] (text-color type nil)) + ([type theme] + (case type + (:neutral :active) (colors/theme-colors colors/neutral-100-opa-100 colors/white theme) + :complete colors/white))) diff --git a/src/quo/components/counter/step/view.cljs b/src/quo/components/counter/step/view.cljs new file mode 100644 index 00000000000..9b5bcba3f79 --- /dev/null +++ b/src/quo/components/counter/step/view.cljs @@ -0,0 +1,43 @@ +(ns quo.components.counter.step.view + (:require + [quo.components.counter.step.style :as style] + [quo.components.markdown.text :as text] + quo.context + [react-native.core :as rn] + [schema.core :as schema] + [utils.number])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:accessibility-label {:optional true} [:maybe :keyword]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:in-blur-view? {:optional true} [:maybe :boolean]] + [:type {:optional true} [:enum :active :complete :neutral]]]] + [:value [:maybe [:or :string :int]]]] + :any]) + +(defn- view-internal + [{:keys [type accessibility-label in-blur-view? customization-color]} value] + (let [theme (quo.context/use-theme) + type (or type :neutral) + value (utils.number/parse-int value) + label (str value) + size (count label)] + [rn/view + {:accessible true + :accessibility-label (or accessibility-label :step-counter) + :style (style/container {:size size + :type type + :in-blur-view? in-blur-view? + :theme theme + :customization-color customization-color})} + [text/text + {:weight :medium + :size :label + :style {:color (style/text-color type theme)}} + label]])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/dividers/date.cljs b/src/quo/components/dividers/date.cljs new file mode 100644 index 00000000000..804cd614a41 --- /dev/null +++ b/src/quo/components/dividers/date.cljs @@ -0,0 +1,27 @@ +(ns quo.components.dividers.date + (:require + [quo.components.common.separator.view :as separator] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn date + [value] + (let [theme (quo.context/use-theme)] + [rn/view + {:padding-top 8 + :padding-bottom 12 + :padding-right 20 + :padding-left 60} + [text/text + {:weight :medium + :accessibility-label :divider-date-text + :size :label + :style {:color (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme) + :text-transform :capitalize + :margin-bottom 4}} + value] + [separator/separator]])) diff --git a/src/quo/components/dividers/divider_label/component_spec.cljs b/src/quo/components/dividers/divider_label/component_spec.cljs new file mode 100644 index 00000000000..d396e6e64aa --- /dev/null +++ b/src/quo/components/dividers/divider_label/component_spec.cljs @@ -0,0 +1,21 @@ +(ns quo.components.dividers.divider-label.component-spec + (:require + [quo.components.dividers.divider-label.view :as divider-label] + [test-helpers.component :as h])) + +(h/describe "Divider Label" + (h/test "default render" + (h/render [divider-label/view "Welcome"]) + (h/is-truthy (h/query-by-label-text :divider-label))) + (h/test "with label" + (h/render [divider-label/view {:tight? true} "Welcome"]) + (h/is-truthy (h/query-by-text "Welcome"))) + (h/test "with label & counter value" + (h/render [divider-label/view {:tight? true :counter? true :counter-value 5} "Public"]) + (h/is-truthy (h/query-by-text "Public")) + (h/is-truthy (h/query-by-text "5"))) + (h/test "on-press event" + (let [on-press (h/mock-fn)] + (h/render [divider-label/view {:tight? false :on-press on-press} "General"]) + (h/fire-event :press (h/query-by-label-text :divider-label)) + (h/was-called on-press)))) diff --git a/src/quo/components/dividers/divider_label/style.cljs b/src/quo/components/dividers/divider_label/style.cljs new file mode 100644 index 00000000000..6d7ce9cab2b --- /dev/null +++ b/src/quo/components/dividers/divider_label/style.cljs @@ -0,0 +1,45 @@ +(ns quo.components.dividers.divider-label.style + (:require + [quo.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) + (if blur? colors/white-opa-5 colors/neutral-90) + theme)) + +(defn get-content-color + [blur? theme] + (colors/theme-colors (if blur? colors/neutral-80-opa-70 colors/neutral-50) + (if blur? colors/white-opa-70 colors/neutral-40) + theme)) + +(defn container + [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) + :padding-right 20 + :align-items :center + :flex-direction :row}) + +(defn content + [chevron] + {:flex-direction (if (= chevron :right) + :row-reverse + :row) + :align-items :center + :height 20 + :flex 1}) + +(defn text + [blur? theme] + {:color (get-content-color blur? theme) + :flex 1}) diff --git a/src/quo/components/dividers/divider_label/view.cljs b/src/quo/components/dividers/divider_label/view.cljs new file mode 100644 index 00000000000..1a63fbac462 --- /dev/null +++ b/src/quo/components/dividers/divider_label/view.cljs @@ -0,0 +1,54 @@ +(ns quo.components.dividers.divider-label.view + (:require + [quo.components.counter.counter.view :as counter] + [quo.components.dividers.divider-label.style :as style] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn- view-internal + "Options: + - `counter?` (default: nil) - Display a counter on the right side of the divider. + - `counter-value` (default: nil) - Number to display if counter? is true. + - `chevron` nil/:right/:left (default: nil) - Display a chevron on the left or right side of the divider. + - `chevron-icon` (default: :i/chevron-right) - Icon to display when chevron is :left/:right. + - `tight?` (default: true) - Use a tighter padding. + - `blur?` (default: nil) - Use a blur background. + - `theme` - Theme value from with-theme HOC. + - `on-press` - Function called when the divider is pressed. + - `container-style` - Style applied to the container view. + + label - String or markdown text component to display in the divider label." + [{:keys [counter? counter-value chevron chevron-icon tight? blur? on-press container-style] + :or {tight? true}} + label] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:on-press on-press + :accessibility-label :divider-label + :style (merge (style/container blur? tight? chevron theme) + container-style)} + [rn/view + {:style (style/content chevron)} + (when chevron + [icons/icon (or chevron-icon :i/chevron-right) + {:color (style/get-content-color blur? theme) + :container-style {(if (= chevron :right) + :margin-left + :margin-right) + 2}}]) + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/text blur? theme)} + label]] + (when counter? + [counter/view + {:type (if blur? :secondary :grey) + :container-style {:margin-left 2}} + counter-value])])) + +(defn view + ([props label] [view-internal props label]) + ([label] [view {} label])) diff --git a/src/quo/components/dividers/divider_line/style.cljs b/src/quo/components/dividers/divider_line/style.cljs new file mode 100644 index 00000000000..bfa71b85d19 --- /dev/null +++ b/src/quo/components/dividers/divider_line/style.cljs @@ -0,0 +1,12 @@ +(ns quo.components.dividers.divider-line.style + (:require + [quo.foundations.colors :as colors])) + +(defn divider-line + [{:keys [blur? container-style]} theme] + (merge + {:border-color (if blur? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)) + :border-bottom-width 1} + container-style)) diff --git a/src/quo/components/dividers/divider_line/view.cljs b/src/quo/components/dividers/divider_line/view.cljs new file mode 100644 index 00000000000..5843f9779e8 --- /dev/null +++ b/src/quo/components/dividers/divider_line/view.cljs @@ -0,0 +1,10 @@ +(ns quo.components.dividers.divider-line.view + (:require + [quo.components.dividers.divider-line.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [props] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/divider-line props theme)}])) diff --git a/src/quo/components/dividers/new_messages.cljs b/src/quo/components/dividers/new_messages.cljs new file mode 100644 index 00000000000..c55ac4673b0 --- /dev/null +++ b/src/quo/components/dividers/new_messages.cljs @@ -0,0 +1,27 @@ +(ns quo.components.dividers.new-messages + (:require + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient])) + +(defn view + "new-messages params - label, customization-color, theme" + [{:keys [label customization-color] :or {customization-color :blue}}] + (let [theme (quo.context/use-theme) + bg-color (colors/resolve-color customization-color theme 5) + text-color (colors/resolve-color customization-color theme)] + [linear-gradient/linear-gradient + {:colors [bg-color "rgba(0,0,0,0)"] + :start {:x 0 :y 0} + :end {:x 0 :y 1}} + [rn/view + {:style {:padding-left 60 + :padding-vertical 12 + :padding-right 24}} + [text/text + {:size :paragraph-2 + :weight :medium + :style {:color text-color}} + label]]])) diff --git a/src/quo/components/dividers/strength_divider/component_spec.cljs b/src/quo/components/dividers/strength_divider/component_spec.cljs new file mode 100644 index 00000000000..b5d3d831836 --- /dev/null +++ b/src/quo/components/dividers/strength_divider/component_spec.cljs @@ -0,0 +1,30 @@ +(ns quo.components.dividers.strength-divider.component-spec + (:require + [quo.components.dividers.strength-divider.view :as strength-divider] + [test-helpers.component :as h])) + +(h/describe "select-profile component" + (h/test "render component" + (h/render [strength-divider/view {:type :okay}]) + (h/is-truthy (h/get-by-label-text :strength-divider))) + (h/test "render component with :type :very-weak" + (h/render [strength-divider/view {:type :very-weak}]) + (h/is-truthy (h/get-by-translation-text :t/strength-divider-very-weak-label))) + (h/test "render component with :type :weak" + (h/render [strength-divider/view {:type :weak}]) + (h/is-truthy (h/get-by-translation-text :t/strength-divider-weak-label))) + (h/test "render component with :type :okay" + (h/render [strength-divider/view {:type :okay}]) + (h/is-truthy (h/get-by-translation-text :t/strength-divider-okay-label))) + (h/test "render component with :type :strong" + (h/render [strength-divider/view {:type :strong}]) + (h/is-truthy (h/get-by-translation-text :t/strength-divider-strong-label))) + (h/test "render component with :type :very-strong" + (h/render [strength-divider/view {:type :very-strong}]) + (h/is-truthy (h/get-by-translation-text :t/strength-divider-very-strong-label))) + (h/test "render component with :type :alert" + (h/render [strength-divider/view {:type :alert} "Text message"]) + (h/is-truthy (h/get-by-text "Text message"))) + (h/test "render component with :type :info" + (h/render [strength-divider/view {:type :info} "Text Info"]) + (h/is-truthy (h/get-by-text "Text Info")))) diff --git a/src/quo/components/dividers/strength_divider/style.cljs b/src/quo/components/dividers/strength_divider/style.cljs new file mode 100644 index 00000000000..c328006c8ec --- /dev/null +++ b/src/quo/components/dividers/strength_divider/style.cljs @@ -0,0 +1,13 @@ +(ns quo.components.dividers.strength-divider.style) + +(def container + {:height 40 + :flex-direction :row + :padding-horizontal 16 + :align-items :center}) + +(defn text + [color] + {:color color + :margin-left 4}) + diff --git a/src/quo/components/dividers/strength_divider/view.cljs b/src/quo/components/dividers/strength_divider/view.cljs new file mode 100644 index 00000000000..36389f2586b --- /dev/null +++ b/src/quo/components/dividers/strength_divider/view.cljs @@ -0,0 +1,89 @@ +(ns quo.components.dividers.strength-divider.view + (:require + [quo.components.dividers.strength-divider.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [react-native.svg :as svg] + [utils.i18n :as i18n])) + +(def strength-divider-types + {:very-weak {:default-text (i18n/label :t/strength-divider-very-weak-label) + :color colors/danger-60 + :percentage 20} + :weak {:default-text (i18n/label :t/strength-divider-weak-label) + :color (colors/resolve-color :orange nil) + :percentage 40} + :okay {:default-text (i18n/label :t/strength-divider-okay-label) + :color (colors/resolve-color :yellow nil) + :percentage 60} + :strong {:default-text (i18n/label :t/strength-divider-strong-label) + :color colors/success-60 + :percentage 80} + :very-strong {:default-text (i18n/label :t/strength-divider-very-strong-label) + :color colors/success-60 + :percentage 100} + :info {:color colors/white-opa-40} + :alert {:color colors/danger-60}}) + +(defn circular-progress + [{:keys [color percentage]}] + (let [strength-indicator-radius 7.5 + strength-indicator-circumference (* 2 Math/PI strength-indicator-radius)] + [svg/svg + {:view-box "0 0 16 16" + :width 14.2 + :transform [{:rotate "270deg"}] + :height 14.2} + [svg/circle + {:cx 8 + :cy 8 + :r strength-indicator-radius + :fill :transparent + :stroke-width 1.2 + :stroke (colors/alpha color 0.2)}] + [svg/circle + {:cx 8 + :cy 8 + :r strength-indicator-radius + :fill :transparent + :stroke-width 1.2 + :stroke-dasharray strength-indicator-circumference + :stroke-dashoffset (* (- 100 percentage) 0.01 strength-indicator-circumference) + :stroke color}] + [svg/circle]])) + +(defn strength-indicator + [type] + (let [{:keys [color percentage]} (strength-divider-types type)] + (case type + :info nil + :alert [icon/icon :i/alert + {:color color + :size 16}] + [rn/view + {:padding-left 4 + :padding-right 2} + [circular-progress {:color color :percentage percentage}]]))) + +(defn view + "Options + - `:type` `:very-weak`/`:weak`/`:okay`/`:strong`/`:very-strong`/`:info`/`:alert`) + + `text` message string(only works when type is `info`/`alert`)" + [{:keys [type] :or {type :very-weak}} text] + (let [{:keys [color default-text]} (strength-divider-types type)] + [linear-gradient/linear-gradient + {:colors [(colors/alpha color 0.05) (colors/alpha color 0)]} + [rn/view + {:style style/container + :accessibility-label :strength-divider} + [strength-indicator type] + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/text color)} + (or default-text text)]]])) + diff --git a/src/quo/components/drawers/action_drawers/component_spec.cljs b/src/quo/components/drawers/action_drawers/component_spec.cljs new file mode 100644 index 00000000000..b5ae5b557f0 --- /dev/null +++ b/src/quo/components/drawers/action_drawers/component_spec.cljs @@ -0,0 +1,78 @@ +(ns quo.components.drawers.action-drawers.component-spec + (:require + [quo.components.drawers.action-drawers.view :as action-drawer] + [test-helpers.component :as h])) + +(h/describe "action drawer" + (h/test "default render" + (h/render [action-drawer/action-drawer + [[{:icon :i/friend + :label "a sample label"}]]]) + (h/is-truthy (h/get-by-text "a sample label"))) + + (h/test "renders with elements sub-label displaying" + (h/render [action-drawer/action-drawer + [[{:icon :i/friend + :label "a sample label" + :sub-label "a sample sub label"}]]]) + (h/is-truthy (h/get-by-text "a sample sub label"))) + + (h/test "on click action works on element" + (let [on-press (js/jest.fn)] + (h/render [action-drawer/action-drawer + [[{:icon :i/friend + :label "a sample label" + :on-press on-press}]]]) + (h/fire-event :press (h/get-by-text "a sample label")) + (h/was-called on-press))) + + (h/test "disabled state" + (let [on-press-muted (js/jest.fn) + on-press-friend (js/jest.fn) + label-mute "Mute" + label-friend "View member details"] + (h/render [action-drawer/action-drawer + [[{:icon :i/muted + :label label-mute + :on-press on-press-muted} + {:icon :i/friend + :label label-friend + :disabled? true + :on-press on-press-friend}]]]) + (h/fire-event :press (h/get-by-text label-mute)) + (h/was-called on-press-muted) + + (h/fire-event :press (h/get-by-text label-friend)) + (h/was-not-called on-press-friend))) + + (h/test "renders two icons when set" + (h/render [action-drawer/action-drawer + [[{:icon :i/friend + :label "a sample label" + :right-icon :i/friend + :accessibility-label :first-element}]]]) + (h/is-truthy (h/query-by-label-text :right-icon-for-action)) + (h/is-truthy (h/query-by-label-text :left-icon-for-action))) + + (h/test "renders right text besides icon when non-nil" + (h/render [action-drawer/action-drawer + [[{:icon :i/friend + :label "a sample label" + :right-text "20+" + :right-icon :i/friend}]]]) + (h/is-truthy (h/query-by-label-text :right-text-for-action))) + + (h/test "does not render right text when not present" + (h/render [action-drawer/action-drawer + [[{:icon :i/friend + :label "a sample label" + :right-icon :i/friend}]]]) + (h/is-null (h/query-by-label-text :right-text-for-action))) + + (h/test "renders a divider when the add-divider? prop is true" + (h/render [action-drawer/action-drawer + [[{:icon :i/friend + :label "a sample label" + :add-divider? true + :accessibility-label :first-element}]]]) + (h/is-truthy (h/get-all-by-label-text "divider")))) diff --git a/src/quo/components/drawers/action_drawers/style.cljs b/src/quo/components/drawers/action_drawers/style.cljs new file mode 100644 index 00000000000..12df92530fe --- /dev/null +++ b/src/quo/components/drawers/action_drawers/style.cljs @@ -0,0 +1,67 @@ +(ns quo.components.drawers.action-drawers.style + (:require + [quo.foundations.colors :as colors])) + +(defn divider + [theme blur?] + {:border-top-width 1 + :border-top-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)) + :margin-top 8 + :margin-bottom 7 + :align-items :center + :flex-direction :row}) + +(defn container + [{:keys [sub-label disabled? state customization-color blur? theme]}] + (cond-> {:border-radius 12 + :margin-horizontal 8} + + sub-label + (assoc :height 56) + + (not sub-label) + (assoc :height 48) + + disabled? + (assoc :opacity 0.3) + + (= state :selected) + (assoc :background-color + (if blur? + colors/white-opa-5 + (colors/resolve-color customization-color theme 5))))) + +(defn row-container + [sub-label] + {:height (if sub-label 56 48) + :margin-horizontal 12 + :flex-direction :row}) + +(defn left-icon + [sub-label?] + {:height 20 + :margin-top (if sub-label? 8 :auto) + :margin-bottom (when-not sub-label? :auto) + :margin-right 12 + :width 20}) + +(def text-container + {:flex 1 + :justify-content :center}) + +(def right-side-container + {:flex-direction :row + :align-items :center}) + +(def right-icon + {:height 20 + :margin-top :auto + :margin-bottom :auto + :width 20}) + +(defn right-text + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :margin-right 12}) diff --git a/src/quo/components/drawers/action_drawers/view.cljs b/src/quo/components/drawers/action_drawers/view.cljs new file mode 100644 index 00000000000..04f583b0b1c --- /dev/null +++ b/src/quo/components/drawers/action_drawers/view.cljs @@ -0,0 +1,105 @@ +(ns quo.components.drawers.action-drawers.view + (:require + [quo.components.drawers.action-drawers.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- get-icon-color + [blur? danger? theme] + (cond + danger? (colors/theme-colors colors/danger-50 colors/danger-60 theme) + blur? colors/white-opa-70 + :else (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn- divider + [theme blur?] + [rn/view + {:style (style/divider theme blur?) + :accessible true + :accessibility-label :divider}]) + +(defn- maybe-pressable + [disabled? props child] + (if disabled? + [rn/view (dissoc props :on-press) child] + [rn/touchable-highlight props child])) + +(defn action + [{:keys [icon label sub-label right-icon right-text danger? disabled? on-press add-divider? + accessibility-label icon-color no-icon-color? state customization-color blur?]}] + (let [theme (quo.context/use-theme)] + [:<> + (when add-divider? + [divider theme blur?]) + [maybe-pressable disabled? + {:accessibility-label accessibility-label + :style (style/container {:sub-label sub-label + :disabled? disabled? + :state state + :customization-color customization-color + :blur? blur? + :theme theme}) + :underlay-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme) + :on-press on-press} + [rn/view + {:style (style/row-container sub-label)} + (when icon + [rn/view + {:accessibility-label :left-icon-for-action + :accessible true + :style (style/left-icon sub-label)} + [icon/icon icon + {:color (or icon-color (get-icon-color blur? danger? theme)) + :no-color no-icon-color? + :size 20}]]) + [rn/view + {:style style/text-container} + [text/text + {:size :paragraph-1 + :weight :medium + :style {:color + (cond + danger? (colors/theme-colors colors/danger-50 colors/danger-60 theme) + :else (colors/theme-colors colors/neutral-100 colors/white theme))}} + label] + (when sub-label + [text/text + {:size :paragraph-2 + :style {:color + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + sub-label])] + (when (or right-text right-icon (= state :selected)) + [rn/view {:style style/right-side-container} + (when right-text + [text/text + {:accessibility-label :right-text-for-action + :size :paragraph-1 + :style (style/right-text theme)} + right-text]) + (when right-icon + [rn/view + {:style style/right-icon + :accessible true + :accessibility-label :right-icon-for-action} + [icon/icon right-icon + {:color (get-icon-color blur? danger? theme) + :size 20}]]) + (when (= state :selected) + [rn/view {:style style/right-icon} + [icon/icon :i/check + {:color (if blur? + colors/white + (colors/resolve-color customization-color theme)) + :size 20}]])])]]])) + +(defn action-drawer + [sections] + [:<> + (doall + (for [actions sections] + (let [filtered-actions (filter some? actions)] + (doall + (map #(with-meta [action %] {:key (:label %)}) filtered-actions)))))]) diff --git a/src/quo/components/drawers/bottom_actions/component_spec.cljs b/src/quo/components/drawers/bottom_actions/component_spec.cljs new file mode 100644 index 00000000000..2ccf7ff2780 --- /dev/null +++ b/src/quo/components/drawers/bottom_actions/component_spec.cljs @@ -0,0 +1,73 @@ +(ns quo.components.drawers.bottom-actions.component-spec + (:require + [quo.components.drawers.bottom-actions.view :as bottom-actions] + [test-helpers.component :as h])) + +(defn- render + [component] + (h/render-with-theme-provider component :light)) + +(h/describe "bottom actions tests" + (h/test "default render with no description and single action button" + (render [bottom-actions/view {:actions :one-action :button-one-label "Request to join"}]) + (h/is-truthy (h/get-by-text "Request to join"))) + + (h/test "render with description & 1 action" + (render [bottom-actions/view + {:description :bottom + :description-text "Sample description" + :actions :one-action + :button-one-label "Request to join"}]) + (h/is-truthy (h/get-by-text "Sample description")) + (h/is-truthy (h/get-by-text "Request to join"))) + + (h/test "render with 2 actions" + (let [button-one "Button One" + button-two "Button Two"] + (render [bottom-actions/view + {:actions :two-actions + :button-one-label button-one + :button-one-props {:icon-left :i/arrow-left} + :button-two-label button-two}]) + (h/is-truthy (h/get-by-text button-one)) + (h/is-truthy (h/get-by-label-text :icon)) + (h/is-truthy (h/get-by-text button-two)))) + + (h/test "render disabled button" + (render [bottom-actions/view + {:description :bottom + :actions :one-action + :description-text "Sample description" + :button-one-props {:disabled? true} + :button-one-label "button"}]) + (h/is-disabled (h/get-by-label-text :button-one))) + + (h/test "render with error & 1 action" + (render [bottom-actions/view + {:description :top-error + :error-message "Sample error" + :actions :one-action + :button-one-label "Request to join"}]) + (h/is-truthy (h/get-by-text "Sample error")) + (h/is-truthy (h/get-by-text "Request to join"))) + + (h/test "render with top description & 1 action" + (render [bottom-actions/view + {:description :top + :role :admin + :actions :one-action + :button-one-label "Request to join"}]) + (h/is-truthy (h/get-by-text "Eligible to join as")) + (h/is-truthy (h/get-by-text "Admin")) + (h/is-truthy (h/get-by-text "Request to join"))) + + (h/test "render with top description with custom text & 1 action" + (render [bottom-actions/view + {:description :top + :role :admin + :actions :one-action + :description-top-text "You'll be an" + :button-one-label "Request to join"}]) + (h/is-truthy (h/get-by-text "You'll be an")) + (h/is-truthy (h/get-by-text "Admin")) + (h/is-truthy (h/get-by-text "Request to join")))) diff --git a/src/quo/components/drawers/bottom_actions/style.cljs b/src/quo/components/drawers/bottom_actions/style.cljs new file mode 100644 index 00000000000..2747053b7a8 --- /dev/null +++ b/src/quo/components/drawers/bottom_actions/style.cljs @@ -0,0 +1,62 @@ +(ns quo.components.drawers.bottom-actions.style + (:require + [quo.foundations.colors :as colors])) + +(defn- text-color + [scroll? blur? theme] + (cond + scroll? (if blur? + colors/white-70-blur + (colors/theme-colors colors/neutral-80-opa-70 + colors/white-opa-70 + theme)) + blur? colors/white-opa-40 + :else (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme))) + +(defn container + [scroll? blur? theme] + {:background-color (when-not (or scroll? blur?) + (colors/theme-colors colors/white colors/neutral-95 theme))}) + +(defn buttons-container + [actions container-style] + (merge + {:flex-direction (if (= actions :two-vertical-actions) :column :row) + :justify-content :space-around + :padding-vertical 12 + :gap 12 + :padding-horizontal 20} + container-style)) + +(def button-container + {:flex-grow 1}) + +(def description-top + {:flex-direction :row + :align-items :center + :gap 5 + :padding-top 12 + :padding-bottom 4 + :justify-content :center}) + +(defn description-top-text + [scroll? blur? theme] + {:color (text-color scroll? blur? theme)}) + +(defn description-bottom + [scroll? blur? theme] + {:color (text-color scroll? blur? theme) + :text-align :center + :padding-bottom 12 + :padding-horizontal 40}) + +(def error-message + {:flex-direction :row + :gap 4 + :justify-content :center + :align-items :center + :padding-top 15 + :padding-horizontal 20 + :padding-bottom 7}) diff --git a/src/quo/components/drawers/bottom_actions/view.cljs b/src/quo/components/drawers/bottom_actions/view.cljs new file mode 100644 index 00000000000..2196ca1be58 --- /dev/null +++ b/src/quo/components/drawers/bottom_actions/view.cljs @@ -0,0 +1,107 @@ +(ns quo.components.drawers.bottom-actions.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.drawers.bottom-actions.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.schema :as context-tag.schema] + [quo.components.tags.context-tag.view :as context-tag] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:actions [:maybe [:enum :one-action :two-actions :two-vertical-actions]]] + [:description {:optional true} [:maybe [:enum :top :bottom :top-error]]] + [:description-text {:optional true} [:maybe [:or :string :schema.common/hiccup]]] + [:description-top-text {:optional true} [:maybe [:or :string :schema.common/hiccup]]] + [:error-message {:optional true} [:maybe :string]] + [:role {:optional true} [:maybe [:enum :admin :member :token-master :owner]]] + [:context-tag-props {:optional true} [:maybe context-tag.schema/?schema]] + [:button-one-label {:optional true} [:maybe :string]] + [:button-two-label {:optional true} [:maybe :string]] + [:button-one-props {:optional true} [:maybe :map]] + [:button-two-props {:optional true} [:maybe :map]] + [:scroll? {:optional true} [:maybe :boolean]] + [:blur? {:optional true} [:maybe :boolean]] + [:container-style {:optional true} [:maybe :map]] + [:buttons-container-style {:optional true} [:maybe :map]] + [:buttons-style {:optional true} [:maybe :map]]]]] + :any]) + +(def ^:private role-icon + {:admin :i/gavel + :member :i/members + :token-master :i/token-master + :owner :i/crown}) + +(defn- view-internal + [{:keys [actions description description-text description-top-text error-message role button-one-label + button-two-label blur? button-one-props button-two-props scroll? container-style + buttons-container-style buttons-style context-tag-props]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (merge (style/container scroll? blur? theme) container-style)} + (when (= description :top-error) + [rn/view {:style style/error-message} + [icon/icon + :i/alert + {:color (colors/theme-colors colors/danger-50 colors/danger-60 theme) + :size 16}] + [text/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/danger-50 colors/danger-60 theme)}} + error-message]]) + + (when (= description :top) + (if (vector? description-top-text) + description-top-text + (when (or role context-tag-props) + [rn/view {:style style/description-top} + [text/text + {:size :paragraph-2 + :style (style/description-top-text scroll? blur? theme)} + (or description-top-text (i18n/label :t/eligible-to-join-as))] + [context-tag/view + (if role + {:type :icon + :size 24 + :icon (role role-icon) + :blur? blur? + :context (i18n/label (keyword "t" role))} + context-tag-props)]]))) + + [rn/view {:style (style/buttons-container actions buttons-container-style)} + (when (or (= actions :two-actions) + (= actions :two-vertical-actions)) + [button/button + (merge + {:size 40 + :container-style (merge style/button-container buttons-style) + :background (when (or blur? scroll?) :blur) + :theme theme + :accessibility-label :button-two} + button-two-props) + button-two-label]) + [button/button + (merge + {:size 40 + :container-style (merge style/button-container buttons-style) + :background (when (or blur? scroll?) :blur) + :theme theme + :accessibility-label :button-one} + button-one-props) + button-one-label]] + (when (= description :bottom) + (if (string? description-text) + [text/text + {:size :paragraph-2 + :style (style/description-bottom scroll? blur? theme)} description-text] + description-text))])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/drawers/documentation_drawers/component_spec.cljs b/src/quo/components/drawers/documentation_drawers/component_spec.cljs new file mode 100644 index 00000000000..2265332dec7 --- /dev/null +++ b/src/quo/components/drawers/documentation_drawers/component_spec.cljs @@ -0,0 +1,31 @@ +(ns quo.components.drawers.documentation-drawers.component-spec + (:require + [quo.components.drawers.documentation-drawers.view :as documentation-drawers] + [test-helpers.component :as h])) + +(h/describe "Documentation drawers component" + (h/test "render component without button" + (h/render [documentation-drawers/view {:title "Documentation"} "Content"]) + (-> (h/expect (h/get-by-label-text :documentation-drawer-title)) + (.toBeTruthy)) + (-> (h/expect (h/get-by-label-text :documentation-drawer-content)) + (.toBeTruthy))) + (h/test "render component with button" + (h/render [documentation-drawers/view + {:title "Documentation" :show-button? true :button-label "Read more"} "Content"]) + (-> (h/expect (h/get-by-label-text :documentation-drawer-title)) + (.toBeTruthy)) + (-> (h/expect (h/get-by-label-text :documentation-drawer-content)) + (.toBeTruthy)) + (-> (h/expect (h/get-by-label-text :documentation-drawer-button)) + (.toBeTruthy))) + (h/test "button is pressed" + (let [event (h/mock-fn)] + (h/render + [documentation-drawers/view + {:title "Documentation" :show-button? true :button-label "Read more" :on-press-button event} + "Content"]) + (h/fire-event :press (h/get-by-label-text :documentation-drawer-button)) + (-> (h/expect event) + (.toHaveBeenCalled))))) + diff --git a/src/quo/components/drawers/documentation_drawers/style.cljs b/src/quo/components/drawers/documentation_drawers/style.cljs new file mode 100644 index 00000000000..c079aced6b1 --- /dev/null +++ b/src/quo/components/drawers/documentation_drawers/style.cljs @@ -0,0 +1,23 @@ +(ns quo.components.drawers.documentation-drawers.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(def outer-container + ;; Margin negative to fill the bottom-sheet container where this component is used + {:margin-bottom (- (+ safe-area/bottom 8))}) + +(def container + {:align-items :flex-start + :padding-horizontal 20}) + +(def content + {:align-items :flex-start + :margin-bottom (+ safe-area/bottom 12)}) + +(defn title + [theme] + {:color (colors/theme-colors colors/neutral-100 + colors/white + theme) + :margin-bottom 8}) diff --git a/src/quo/components/drawers/documentation_drawers/view.cljs b/src/quo/components/drawers/documentation_drawers/view.cljs new file mode 100644 index 00000000000..24051ceb809 --- /dev/null +++ b/src/quo/components/drawers/documentation_drawers/view.cljs @@ -0,0 +1,45 @@ +(ns quo.components.drawers.documentation-drawers.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.drawers.documentation-drawers.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.gesture :as gesture])) + +(defn view + "Options + - `title` Title text + - `show-button?` Show button + - `button-label` button label + - `button-icon` button icon + - `on-press-button` On press handler for the button + - `shell?` use shell theme + `content` Content of the drawer + " + [{:keys [title show-button? on-press-button button-label button-icon shell?]} content] + (let [theme (quo.context/use-theme)] + [gesture/scroll-view + {:style style/outer-container + :always-bounce-vertical false + :content-inset-adjustment-behavior :never} + [rn/view {:style style/container} + (when title + [text/text + {:size :heading-2 + :accessibility-label :documentation-drawer-title + :style (style/title theme) + :weight :semi-bold} + title]) + [rn/view {:style style/content :accessibility-label :documentation-drawer-content} + content + (when show-button? + [button/button + {:size 24 + :type :outline + :container-style {:margin-top 16} + :background (when shell? :blur) + :on-press on-press-button + :accessibility-label :documentation-drawer-button + :icon-right button-icon} + button-label])]]])) diff --git a/src/quo/components/drawers/drawer_action/component_spec.cljs b/src/quo/components/drawers/drawer_action/component_spec.cljs new file mode 100644 index 00000000000..4153c842f6e --- /dev/null +++ b/src/quo/components/drawers/drawer_action/component_spec.cljs @@ -0,0 +1,66 @@ +(ns quo.components.drawers.drawer-action.component-spec + (:require + [quo.components.drawers.drawer-action.view :as drawer-action] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(h/describe "Drawers: drawer-action" + (h/test "default render" + (h/render-with-theme-provider [drawer-action/view {:accessibility-label :container}]) + (h/is-truthy (h/query-by-label-text :container))) + + (h/test "on-press-in changes internal state to :pressed" + (h/render-with-theme-provider [drawer-action/view {:accessibility-label :container}]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 5)}))) + + (h/test "render default action with state :selected" + (h/render-with-theme-provider [drawer-action/view + {:state :selected :accessibility-label :container}]) + (h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 5)}) + (h/is-truthy (h/query-by-label-text :check-icon))) + + (h/test "call on-press" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [drawer-action/view + {:on-press on-press :accessibility-label :container}]) + (h/fire-event :on-press (h/get-by-label-text :container)) + (h/was-called on-press))) + + + (h/test "render :arrow action" + (h/render-with-theme-provider [drawer-action/view {:action :arrow}]) + (h/is-truthy (h/query-by-label-text :arrow-icon))) + + (h/test "render :toggle action" + (h/render-with-theme-provider [drawer-action/view {:action :toggle}]) + (h/is-truthy (h/query-by-label-text "toggle-off"))) + + (h/test "render :toggle action with state :selected" + (h/render-with-theme-provider [drawer-action/view + {:accessibility-label :container + :action :toggle + :state :selected}]) + (h/is-truthy (h/query-by-label-text "toggle-on")) + (h/has-style (h/query-by-label-text :container) + {:backgroundColor :transparent})) + + (h/test "render :input action" + (h/render-with-theme-provider + [drawer-action/view + {:action :input + :input-props {:placeholder "Type something"} + :state :selected}]) + (h/is-truthy (h/query-by-label-text "input"))) + + (h/test "render default action with icon, title, description" + (h/render-with-theme-provider [drawer-action/view + {:icon :i/contact + :title "Check contact" + :description "Just a small desc"}]) + (h/is-truthy (h/query-by-label-text :left-icon)) + (h/is-truthy (h/query-by-text "Check contact")) + (h/has-style (h/query-by-text "Check contact") {:color colors/neutral-100}) + (h/is-truthy (h/query-by-text "Just a small desc")))) diff --git a/src/quo/components/drawers/drawer_action/schema.cljs b/src/quo/components/drawers/drawer_action/schema.cljs new file mode 100644 index 00000000000..db5f9b7dd39 --- /dev/null +++ b/src/quo/components/drawers/drawer_action/schema.cljs @@ -0,0 +1,19 @@ +(ns quo.components.drawers.drawer-action.schema) + +(def ?schema + [:=> + [:cat + [:map {:closed true} + [:accessibility-label {:optional true} [:maybe :keyword]] + [:type {:optional true} [:maybe [:enum :main :danger]]] + [:action {:optional true} [:maybe [:enum :arrow :toggle :input]]] + [:icon {:optional true} [:maybe :keyword]] + [:description {:optional true} [:maybe :string]] + [:state {:optional true} [:maybe [:enum :selected]]] + [:title {:optional true} :string] + [:on-press {:optional true} [:maybe fn?]] + [:input-props {:optional true} [:maybe :map]] + [:customization-color {:optional true} + [:maybe :schema.common/customization-color]] + [:blur? {:optional true} [:maybe :boolean]]]] + :any]) diff --git a/src/quo/components/drawers/drawer_action/style.cljs b/src/quo/components/drawers/drawer_action/style.cljs new file mode 100644 index 00000000000..b02d9ebbea6 --- /dev/null +++ b/src/quo/components/drawers/drawer_action/style.cljs @@ -0,0 +1,79 @@ +(ns quo.components.drawers.drawer-action.style + (:require + [quo.foundations.colors :as colors])) + +(defn- background-color + [{:keys [state action customization-color theme pressed? blur?]}] + (let [checked? (and (= :selected state) (or (nil? action) (= action :input)))] + (cond + (and (or checked? pressed?) blur?) + colors/white-opa-5 + + (or pressed? checked?) + (colors/resolve-color customization-color theme 5) + + :else :transparent))) + +(defn container + [{:keys [description? action state] :as props}] + {:padding-top (if description? 8 13) + :padding-bottom (if (and (= action :input) + (= state :selected)) + 12 + (if description? 8 13)) + :padding-horizontal 13 + :border-radius 12 + :gap 8 + :background-color (background-color props)}) + +(def text-container + {:flex 1 + :margin-right 12}) + +(defn text + [{:keys [theme blur? type]}] + (let [base {:weight :medium} + theme-with-blur (if blur? :blur theme) + matcher [theme-with-blur type] + color + (case matcher + ([:dark :main] [:light :main]) (colors/theme-colors colors/neutral-100 + colors/white + theme) + [:blur :main] colors/white-70-blur + ([:dark :danger] [:light :danger]) (colors/theme-colors colors/danger-50 + colors/danger-60 + theme) + [:blur :danger] colors/danger-60)] + (assoc-in base [:style :color] color))) + +(defn- neutral-color + [theme blur?] + (if blur? + colors/white-70-blur + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(def left-icon + {:align-self :flex-start + :margin-right 13 + :margin-top 1}) + +(defn icon-color + [{:keys [theme blur? type]}] + (let [theme-with-blur (if blur? :blur theme) + matcher [theme-with-blur type]] + (case matcher + ([:dark :main] [:light :main]) (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + [:blur :main] colors/white-70-blur + ([:dark :danger] [:light :danger]) (colors/theme-colors colors/danger-50 colors/danger-60 theme) + [:blur :danger] colors/danger-60))) + +(defn desc + [{:keys [theme blur?]}] + {:color (neutral-color theme blur?)}) + +(defn check-color + [{:keys [theme blur? customization-color]}] + (if blur? + colors/white-70-blur + (colors/resolve-color customization-color theme))) diff --git a/src/quo/components/drawers/drawer_action/view.cljs b/src/quo/components/drawers/drawer_action/view.cljs new file mode 100644 index 00000000000..fe1fee16354 --- /dev/null +++ b/src/quo/components/drawers/drawer_action/view.cljs @@ -0,0 +1,93 @@ +(ns quo.components.drawers.drawer-action.view + (:require + [quo.components.drawers.drawer-action.schema :as component-schema] + [quo.components.drawers.drawer-action.style :as style] + [quo.components.icon :as icon] + [quo.components.inputs.input.view :as input] + [quo.components.markdown.text :as text] + [quo.components.selectors.selectors.view :as selectors] + [quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn view-internal + [{:keys [action icon description state title on-press customization-color + blur? accessibility-label input-props] + action-type :type + :or {customization-color :blue + blur? false}}] + (let [theme (quo.context/use-theme) + action-type (or action-type :main) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + + [rn/pressable + {:on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out + :style (style/container {:state state + :action action + :customization-color customization-color + :theme theme + :pressed? pressed? + :description? (not-empty description) + :blur? blur?}) + :accessibility-label accessibility-label} + [rn/view + {:style {:flex-direction :row + :align-items :center}} + (when icon + [icon/icon icon + {:accessibility-label :left-icon + :container-style style/left-icon + :color (style/icon-color {:theme theme + :type action-type + :blur? blur?})}]) + + [rn/view + {:style style/text-container} + [text/text + (style/text {:theme theme + :type action-type + :blur? blur?}) + title] + + (when (seq description) + [text/text + {:size :paragraph-2 + :style (style/desc {:theme theme + :blur? blur?})} + description])] + + (cond + (= action :toggle) + [selectors/view + {:theme theme + :label-prefix "toggle" + :customization-color customization-color + :type :toggle + :on-change on-press + :checked? (= state :selected)}] + + (= action :arrow) + [icon/icon :i/chevron-right + {:accessibility-label :arrow-icon + :color (style/icon-color {:theme theme + :type action-type + :blur? blur?})}] + + (= state :selected) + [icon/icon :i/check + {:accessibility-label :check-icon + :color (style/check-color {:theme theme + :blur? blur? + :customization-color customization-color})}])] + + (when (and (= action :input) (= state :selected)) + [input/input + (assoc input-props + :blur? blur? + :accessibility-label :input)])])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/drawers/drawer_buttons/component_spec.cljs b/src/quo/components/drawers/drawer_buttons/component_spec.cljs new file mode 100644 index 00000000000..eb3e1f15028 --- /dev/null +++ b/src/quo/components/drawers/drawer_buttons/component_spec.cljs @@ -0,0 +1,74 @@ +(ns quo.components.drawers.drawer-buttons.component-spec + (:require + [quo.components.drawers.drawer-buttons.view :as drawer-buttons] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [test-helpers.component :as h])) + +(h/describe "drawer-buttons" + (h/setup-fake-timers) + + (h/test "the top heading and subheading render" + (h/render [drawer-buttons/view + {:top-card {:heading :top-heading} + :bottom-card {:heading :bottom-heading}} + :top-sub-heading + :bottom-sub-heading]) + (-> (js/expect (h/get-by-text "top-heading")) + (.toBeTruthy)) + (-> (js/expect (h/get-by-text "top-sub-heading")) + (.toBeTruthy))) + + (h/test "the bottom heading and subheading render" + (h/render [drawer-buttons/view + {:top-card {:heading :top-heading} + :bottom-card {:heading :bottom-heading}} + :top-sub-heading + :bottom-sub-heading]) + (-> (js/expect (h/get-by-text "bottom-heading")) + (.toBeTruthy)) + (-> (js/expect (h/get-by-text "bottom-sub-heading")) + (.toBeTruthy))) + + (h/test "it clicks the top card" + (let [event (h/mock-fn)] + (with-redefs [safe-area/top 10] + (h/render [drawer-buttons/view + {:top-card {:on-press event + :heading :top-heading} + :bottom-card {:heading :bottom-heading}} + :top-sub-heading + :bottom-sub-heading]) + (h/fire-event :press (h/get-by-text "top-heading")) + (-> (js/expect event) + (.toHaveBeenCalled))))) + + (h/test "it clicks the bottom card" + (let [event (h/mock-fn)] + (h/render [drawer-buttons/view + {:top-card {:heading :top-heading} + :bottom-card {:on-press event + :heading :bottom-heading}} + :top-sub-heading + :bottom-sub-heading]) + (h/fire-event :press (h/get-by-text "bottom-heading")) + (-> (js/expect event) + (.toHaveBeenCalled)))) + + (h/test "the top card child renders with a render prop" + (h/render [drawer-buttons/view + {:top-card {:heading :top-heading} + :bottom-card {:heading :bottom-heading}} + [rn/text :top-render-fn] + :bottom-sub-heading]) + (-> (js/expect (h/get-by-text "top-render-fn")) + (.toBeTruthy))) + + (h/test "the bottom card child renders with a render prop" + (h/render [drawer-buttons/view + {:top-card {:heading :top-heading} + :bottom-card {:heading :bottom-heading}} + :top-sub-heading + [rn/text :bottom-render-fn]]) + (-> (js/expect (h/get-by-text "bottom-render-fn")) + (.toBeTruthy)))) diff --git a/src/quo/components/drawers/drawer_buttons/style.cljs b/src/quo/components/drawers/drawer_buttons/style.cljs new file mode 100644 index 00000000000..7229d8c9928 --- /dev/null +++ b/src/quo/components/drawers/drawer_buttons/style.cljs @@ -0,0 +1,77 @@ +(ns quo.components.drawers.drawer-buttons.style + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(defn outer-container + [height border-radius container-style] + (reanimated/apply-animations-to-style + {:height height + :border-top-left-radius border-radius + :border-top-right-radius border-radius} + (assoc + container-style + :position :absolute + :left 0 + :right 0 + :bottom 0 + :overflow :hidden))) + +(def top-card + {:flex 1 + :padding-vertical 12 + :padding-horizontal 20 + :border-top-left-radius 20 + :border-top-right-radius 20 + :background-color colors/neutral-80-opa-80-blur}) + +(def bottom-card + {:position :absolute + :left 0 + :right 0 + :bottom 0 + :padding-vertical 12 + :padding-horizontal 20 + :border-top-left-radius 20 + :border-top-right-radius 20 + :background-color colors/white-opa-5}) + +(def bottom-container + {:flex-direction :row + :justify-content :space-between}) + +(def bottom-icon + {:border-radius 32 + :border-width 1 + :margin-left 24 + :height 32 + :width 32 + :justify-content :center + :align-items :center + :border-color colors/white-opa-5}) + +(def blur-view-style + {:flex 1 + :border-top-left-radius 20 + :border-top-right-radius 20}) + +(def blur-content-style + {:flex 1 + :background-color :transparent + :position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0}) + +(def bottom-text + {:flex 1 + :color colors/white-70-blur}) + +(def top-text + {:color colors/white-70-blur}) + +(defn heading-text + [gap] + {:color colors/white + :margin-bottom gap}) diff --git a/src/quo/components/drawers/drawer_buttons/view.cljs b/src/quo/components/drawers/drawer_buttons/view.cljs new file mode 100644 index 00000000000..06619419884 --- /dev/null +++ b/src/quo/components/drawers/drawer_buttons/view.cljs @@ -0,0 +1,215 @@ +(ns quo.components.drawers.drawer-buttons.view + (:require + [quo.components.blur.view :as blur] + [quo.components.drawers.drawer-buttons.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area])) + +(def default-height 216) + +(defn render-bottom + [children] + [rn/view {:style style/bottom-container} + children + [rn/view + {:style style/bottom-icon} + [icon/icon :arrow-right + {:color colors/white + :size 20}]]]) + +(defn label? [el] (or (string? el) (keyword? el))) + +(defn render-children-bottom + [children] + (if (label? children) + [render-bottom + [text/text + {:size :paragraph-2 + :style style/bottom-text + :weight :semi-bold} + children]] + [render-bottom children])) + +(defn- f-render-children-top + [children top-children-opacity] + (let [scale (reanimated/interpolate top-children-opacity [1 0] [1 1.1]) + padding-left (reanimated/interpolate scale [1 1.1] [0 14])] + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity top-children-opacity + :transform [{:scale scale} + {:translate-x padding-left}]} + {})} + (if (label? children) + [text/text + {:size :paragraph-2 + :style style/top-text + :weight :semi-bold} + children] + children)])) + +(defn- f-card + [{:keys [on-press default-on-press style heading gap accessibility-label top? top-title-opacity + top-children-opacity + animated-heading] + :or {top-title-opacity (reanimated/use-shared-value 1)}} + children] + [rn/touchable-highlight + {:accessibility-label accessibility-label + :on-press (fn [] + (when on-press + (on-press)) + (when default-on-press + (default-on-press))) + :border-radius 20 + :style style + :underlay-color (:background-color style)} + [rn/view + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity top-title-opacity} + {})} + [text/text + {:size :heading-1 + :style (style/heading-text gap) + :weight :semi-bold} + heading]] + (when animated-heading + (let [animated-heading-opacity (reanimated/interpolate top-children-opacity [1 0] [0 1])] + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity top-title-opacity} + {:position :absolute})} + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity animated-heading-opacity} + {})} + [text/text + {:size :heading-1 + :style (style/heading-text gap) + :weight :semi-bold} + animated-heading]]])) + (if top? + [:f> f-render-children-top children top-children-opacity] + [render-children-bottom children])]]) + +(defn card + [props children] + [:f> f-card props children]) + +(defn f-view + "[view opts] + opts + {:container-style style-object + :top-card { :on-press event + :heading string + :accessibility-label keyword} + :bottom-card { :on-press event + :heading string + :accessibility-label keyword}} + child-1 string, keyword or hiccup + child-2 string, keyword or hiccup + " + [{:keys [container-style top-card bottom-card on-init animations-duration animations-delay]} + child-1 + child-2] + (let [max-height (+ (:height (rn/get-window)) (if platform/android? safe-area/top 0)) + height (reanimated/use-shared-value default-height) + top-padding (reanimated/use-shared-value 12) + border-radius (reanimated/use-shared-value 20) + bottom-view-top (reanimated/use-shared-value 80) + top-title-opacity (reanimated/use-shared-value 1) + top-children-opacity (reanimated/use-shared-value 1) + animations-delay (/ animations-delay 1.4) + start-top-animation (fn [] + (reanimated/animate-shared-value-with-delay bottom-view-top + (:height (rn/get-screen)) + animations-duration + :easing4 + animations-delay) + (reanimated/animate-shared-value-with-delay + height + max-height + animations-duration + :easing4 + animations-delay) + (reanimated/animate-shared-value-with-delay + top-padding + (+ 68 safe-area/top) + animations-duration + :easing4 + animations-delay) + (reanimated/animate-shared-value-with-delay + top-children-opacity + 0 + animations-duration + :easing4 animations-delay) + (reanimated/animate-shared-value-with-delay + top-title-opacity + 0 + 0 + :linear + (+ animations-delay animations-duration 500))) + reset-top-animation (fn [] + (reanimated/set-shared-value top-title-opacity 1) + (reanimated/animate-shared-value-with-delay bottom-view-top + 80 + animations-duration + :easing4 + 50) + (reanimated/animate-shared-value-with-timing + height + default-height + animations-duration + :easing4) + (reanimated/animate-shared-value-with-timing + top-padding + 12 + animations-duration + :easing4) + (reanimated/animate-shared-value-with-timing + top-children-opacity + 1 + animations-duration + :easing4))] + (rn/use-mount (fn [] + (when on-init + (on-init reset-top-animation)))) + [reanimated/view {:style (style/outer-container height border-radius container-style)} + [blur/view + {:blur-type :dark + :blur-amount 10 + :style style/blur-view-style} + [rn/view + {:style style/blur-content-style} + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:padding-top top-padding} + style/top-card)} + [card + (merge {:gap 4 + :top? true + :style {:flex 1} + :top-children-opacity top-children-opacity + :top-title-opacity top-title-opacity + :default-on-press start-top-animation} + top-card) + child-1]] + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:top bottom-view-top} + style/bottom-card)} + [card + (merge {:style {:flex 1} + :gap 10} + bottom-card) + child-2]]]]])) + +(defn view + [props child-1 child-2] + [:f> f-view props child-1 child-2]) diff --git a/src/quo/components/drawers/drawer_top/component_spec.cljs b/src/quo/components/drawers/drawer_top/component_spec.cljs new file mode 100644 index 00000000000..3a2793a5a35 --- /dev/null +++ b/src/quo/components/drawers/drawer_top/component_spec.cljs @@ -0,0 +1,141 @@ +(ns quo.components.drawers.drawer-top.component-spec + (:require + [quo.core :as quo] + [test-helpers.component :as h])) + +(def ^:private theme :light) + +(h/describe "drawer top tests" + (h/test "component renders in default type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :type :default}] + theme) + (h/is-truthy (h/get-by-text "Title"))) + + (h/test "component renders in default + description type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :type :default + :description "Description"}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-text "Description"))) + + (h/test "component renders in address type" + (h/render-with-theme-provider [quo/drawer-top + {:title "0x1" + :type :address}] + theme) + (h/is-truthy (h/get-by-text "0x1"))) + + (h/test "component renders in info type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :type :info}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :info-icon))) + + (h/test "component renders in info + description type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :description "Description" + :type :info}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-text "Description")) + (h/is-truthy (h/get-by-label-text :info-icon))) + + (h/test "component renders in context-tag type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :type :context-tag + :community-name "Coinbase"}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :context-tag-wrapper))) + + (h/test "component renders in context-tag + button type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :type :context-tag + :button-icon :i/placeholder + :community-name "Coinbase"}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :button-icon)) + (h/is-truthy (h/get-by-label-text :context-tag-wrapper))) + + (h/test "component renders in account type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :type :account + :account-avatar-emoji "🍿" + :networks [{:network-name :ethereum :short-name "eth"}] + :description "0x62b...0a5" + :customization-color :purple}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-text "0x62b...0a5")) + (h/is-truthy (h/get-by-label-text :account-avatar))) + + (h/test "component renders keypair type with default label" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :stored nil + :icon-avatar :i/placeholder + :type :keypair}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (-> (h/expect (h/get-by-translation-text :t/on-device)) + (.toBeTruthy))) + + (h/test "component renders keypair type when stored on device" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :stored :on-device + :icon-avatar :i/placeholder + :type :keypair}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (-> (h/expect (h/get-by-translation-text :t/on-device)) + (.toBeTruthy))) + + (h/test "component renders keypair type when stored on keycard" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :stored :on-keycard + :icon-avatar :i/placeholder + :type :keypair}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (-> (h/expect (h/get-by-translation-text :t/on-keycard)) + (.toBeTruthy))) + + (h/test "component renders keypair type when considered missing" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :stored :missing + :icon-avatar :i/placeholder + :type :keypair}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (-> (h/expect (h/get-by-translation-text :t/import-to-use-derived-accounts)) + (.toBeTruthy))) + + (h/test "component renders in default-keypair type" + (h/render-with-theme-provider [quo/drawer-top + {:title "Title" + :description "0x62b...0a5" + :type :default-keypair}] + theme) + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :default-keypair-text))) + + (h/test "component renders in label type" + (h/render-with-theme-provider [quo/drawer-top + {:label "label" + :type :label}] + theme) + (h/is-truthy (h/get-by-text "label")))) diff --git a/src/quo/components/drawers/drawer_top/style.cljs b/src/quo/components/drawers/drawer_top/style.cljs new file mode 100644 index 00000000000..d225532c599 --- /dev/null +++ b/src/quo/components/drawers/drawer_top/style.cljs @@ -0,0 +1,36 @@ +(ns quo.components.drawers.drawer-top.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:padding-horizontal 20 + :padding-bottom 12 + :flex-direction :row}) + +(def body-container + {:flex 1}) + +(defn description + [theme blur?] + {:color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)) + :margin-top 2}) + +(def left-container + {:margin-right 8 + :justify-content :center}) + +(def row + {:flex-direction :row + :align-items :center}) + +(def keycard-icon + {:margin-left 4}) + +(def title-container + {:flex-direction :row + :align-items :center}) + +(def title-icon + {:margin-left 4}) diff --git a/src/quo/components/drawers/drawer_top/view.cljs b/src/quo/components/drawers/drawer_top/view.cljs new file mode 100644 index 00000000000..3e419822e98 --- /dev/null +++ b/src/quo/components/drawers/drawer_top/view.cljs @@ -0,0 +1,246 @@ +(ns quo.components.drawers.drawer-top.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.icon-avatar :as icon-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.buttons.button.view :as button] + [quo.components.drawers.drawer-top.style :as style] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.view :as context-tag] + [quo.components.wallet.address-text.view :as address-text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(def ^:private left-image-supported-types #{:account :keypair :default-keypair}) + +(defn- left-image + [{:keys [type customization-color account-avatar-emoji account-avatar-type icon-avatar + profile-picture blur?]}] + (case type + :account [account-avatar/view + {:customization-color customization-color + :size 32 + :emoji account-avatar-emoji + :type (or account-avatar-type :default)}] + :keypair [icon-avatar/icon-avatar + {:icon icon-avatar + :border? true + :blur? blur? + :color :neutral}] + + :default-keypair [user-avatar/user-avatar + {:size :small + :status-indicator? false + :profile-picture profile-picture}] + nil)) + +(defn- keypair-subtitle + [{:keys [theme blur? stored]}] + [rn/view {:style style/row} + [text/text + {:size :paragraph-2 + :weight :regular + :style (style/description theme blur?)} + (case stored + :on-device (i18n/label :t/on-device) + :on-keycard (i18n/label :t/on-keycard) + :missing (i18n/label :t/import-to-use-derived-accounts) + (i18n/label :t/on-device))] + (when (= stored :on-keycard) + [icons/icon + :i/keycard-card + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :size 16 + :container-style style/keycard-icon}])]) + +(defn- account-subtitle + [{:keys [description]}] + [address-text/view + {:address description + :format :short}]) + +(defn- default-keypair-subtitle + [{:keys [description theme blur?]}] + [text/text + {:accessibility-label :default-keypair-text + :size :paragraph-2 + :weight :regular + :style (style/description theme blur?)} + (str description " · " (i18n/label :t/on-device))]) + +(defn- context-tag-subtitle + [{:keys [context-tag-type context icon community-logo community-name account-name emoji + customization-color full-name profile-picture blur?]}] + (let [tag-type (or context-tag-type :account)] + [rn/view + {:accessibility-label :context-tag-wrapper + :style {:flex-wrap :wrap + :margin-top 4}} + [context-tag/view + {:type tag-type + :account-name account-name + :emoji emoji + :community-name community-name + :community-logo community-logo + :size 24 + :customization-color customization-color + :profile-picture profile-picture + :full-name full-name + :context context + :icon icon + :blur? blur?}]])) + +(defn- description-subtitle + [{:keys [theme blur? description]}] + [text/text + {:size :paragraph-1 + :weight :regular + :style (style/description theme blur?)} + description]) + +(defn- subtitle + [{:keys [type theme blur? stored description community-name community-logo + context-tag-type account-name emoji customization-color full-name profile-picture context + icon]}] + (cond + (= :keypair type) + [keypair-subtitle + {:theme theme + :blur? blur? + :stored stored}] + + (= :account type) + [account-subtitle + {:theme theme + :blur? blur? + :description description}] + + (= :default-keypair type) + [default-keypair-subtitle + {:description description + :theme theme + :blur? blur?}] + + (= :context-tag type) + [context-tag-subtitle + {:context-tag-type context-tag-type + :community-logo community-logo + :community-name community-name + :account-name account-name + :emoji emoji + :customization-color customization-color + :profile-picture profile-picture + :full-name full-name + :context context + :icon icon + :blur? blur?}] + + (and (not= :label type) description) + [description-subtitle + {:theme theme + :blur? blur? + :description description}])) + +(defn- right-icon + [{:keys [theme type on-button-press on-button-long-press button-disabled? button-icon button-type] + :or {button-type :primary}}] + (cond + (= :info type) + [icons/icon + :i/info + {:accessibility-label :info-icon + :size 20 + :color (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme)}] + (and (= :context-tag type) button-icon) + [button/button + {:accessibility-label :button-icon + :on-press on-button-press + :on-long-press on-button-long-press + :disabled? button-disabled? + :type button-type + :size 24 + :icon-only? true} + button-icon])) + +(defn- left-title + [{:keys [type label title title-icon theme blur?]}] + (case type + :label [text/text + {:weight :medium + :size :paragraph-2 + :style (style/description theme blur?)} + label] + :address [address-text/view + {:address title + :full-address? true + :weight :semi-bold + :size :heading-2}] + [rn/view {:style style/title-container} + [text/text + {:size :heading-2 + :weight :semi-bold + :style {:color (when blur? colors/white)}} + title] + (when title-icon + [icons/icon title-icon + {:container-style style/title-icon + :size 20 + :color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}])])) + +(defn view + [{:keys [title title-icon type description blur? community-name community-logo button-icon + account-name emoji context-tag-type button-type container-style + on-button-press on-button-long-press profile-picture stored label full-name + button-disabled? account-avatar-emoji account-avatar-type customization-color icon-avatar + context icon]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style [style/container container-style]} + (when (left-image-supported-types type) + [rn/view {:style style/left-container} + [left-image + {:type type + :blur? blur? + :customization-color customization-color + :account-avatar-emoji account-avatar-emoji + :account-avatar-type account-avatar-type + :icon-avatar icon-avatar + :profile-picture profile-picture}]]) + [rn/view {:style style/body-container} + [left-title + {:type type + :label label + :title title + :title-icon title-icon + :theme theme + :blur? blur?}] + [subtitle + {:type type + :theme theme + :blur? blur? + :stored stored + :description description + :community-name community-name + :community-logo community-logo + :context-tag-type context-tag-type + :customization-color customization-color + :account-name account-name + :emoji emoji + :full-name full-name + :profile-picture profile-picture + :context context + :icon icon}]] + [right-icon + {:theme theme + :type type + :button-type button-type + :on-button-press on-button-press + :on-button-long-press on-button-long-press + :button-disabled? button-disabled? + :button-icon button-icon}]])) diff --git a/src/quo/components/drawers/permission_context/component_spec.cljs b/src/quo/components/drawers/permission_context/component_spec.cljs new file mode 100644 index 00000000000..443993e40ea --- /dev/null +++ b/src/quo/components/drawers/permission_context/component_spec.cljs @@ -0,0 +1,68 @@ +(ns quo.components.drawers.permission-context.component-spec + (:require + [quo.components.drawers.permission-context.view :as permission-context] + [test-helpers.component :as h])) + +(h/describe "permission context" + (js/beforeEach (fn [] + (-> (js/jest.spyOn js/console "error") + (.mockImplementation js/jest.fn)))) + + (h/describe "action type" + (h/test "shows the label" + (h/render [permission-context/view + {:type :action + :action-label "test" + :action-icon :i/communities}]) + (-> (js/expect (h/get-by-text "test")) + (.toBeTruthy))) + + (h/test "fails due to missing props" + (-> (js/expect + (fn [] + (h/render [permission-context/view + {:type :action}]))) + (.toThrow)))) + + (h/describe "single-token-gating type" + (h/test "shows the token-tag container" + (h/render [permission-context/view + {:type :single-token-gating + :token-value "23" + :token-symbol :eth}]) + (-> (js/expect (h/get-by-label-text :permission-context-single-token)) + (.toBeTruthy))) + + (h/test "fails due to wrong props" + (-> (js/expect + (fn [] + (h/render [permission-context/view + {:type :single-token-gating + :action-label "test" + :action-icon :i/communities}]))) + (.toThrow)))) + + (h/describe "multiple-token-gating" + (h/test "shows the token-tag container" + (h/render [permission-context/view + {:type :multiple-token-gating + :token-groups [[:eth :snt] [:snt :eth :snt]]}]) + (-> (js/expect (h/get-by-label-text :permission-context-multiple-token)) + (.toBeTruthy))) + + (h/test "fails if token-groups is empty" + (-> (js/expect + (fn [] + (h/render [permission-context/view + {:type :multiple-token-gating + :token-groups []}]))) + (.toThrow))) + + (h/test "fails due to wrong props" + (-> (js/expect + (fn [] + (h/render [permission-context/view + {:type :multiple-token-gating + :token-value "23" + :token-symbol :eth}]))) + (.toThrow))))) diff --git a/src/quo/components/drawers/permission_context/schema.cljs b/src/quo/components/drawers/permission_context/schema.cljs new file mode 100644 index 00000000000..bc3330b0613 --- /dev/null +++ b/src/quo/components/drawers/permission_context/schema.cljs @@ -0,0 +1,34 @@ +(ns quo.components.drawers.permission-context.schema) + +(def ^:private ?base + [:map {:closed true} + [:type [:enum :action :single-token-gating :multiple-token-gating]] + [:blur? {:optional true} [:maybe :boolean]] + [:container-style {:optional true} [:maybe :map]] + [:on-press {:optional true} [:maybe fn?]]]) + +(def ^:private ?action + [:map {:closed true} + [:action-label :string] + [:action-icon [:qualified-keyword {:namespace :i}]]]) + +(def ^:private ?token-symbol [:or :keyword :string]) + +(def ^:private ?single-token-gating + [:map {:closed true} + [:token-value :string] + [:token-symbol ?token-symbol]]) + +(def ^:private ?multiple-token-gating + [:map {:closed true} + [:token-groups + [:sequential {:min 1} [:sequential {:min 1} ?token-symbol]]]]) + +(def ?schema + [:=> + [:cat + [:multi {:dispatch :type} + [:action [:merge ?base ?action]] + [:single-token-gating [:merge ?base ?single-token-gating]] + [:multiple-token-gating [:merge ?base ?multiple-token-gating]]]] + :any]) diff --git a/src/quo/components/drawers/permission_context/style.cljs b/src/quo/components/drawers/permission_context/style.cljs new file mode 100644 index 00000000000..e2e31f7b76c --- /dev/null +++ b/src/quo/components/drawers/permission_context/style.cljs @@ -0,0 +1,34 @@ +(ns quo.components.drawers.permission-context.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area])) + +(def ^:private radius 20) + +(def blur-container + {:background-color (when platform/ios? :transparent) + :position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0}) + +(defn container + [blur? theme] + {:flex-direction :row + :overflow :hidden + :background-color (when-not blur? + (colors/theme-colors colors/white + colors/neutral-95 + theme)) + :padding-top 12 + :padding-bottom (+ 12 safe-area/bottom) + :justify-content :center + :padding-horizontal 20 + :border-top-left-radius radius + :border-top-right-radius radius}) + +(def token-group + {:flex-direction :row + :margin-left 3}) diff --git a/src/quo/components/drawers/permission_context/view.cljs b/src/quo/components/drawers/permission_context/view.cljs new file mode 100644 index 00000000000..27be8b823f8 --- /dev/null +++ b/src/quo/components/drawers/permission_context/view.cljs @@ -0,0 +1,102 @@ +(ns quo.components.drawers.permission-context.view + (:require + [clojure.string :as string] + [quo.components.blur.view :as blur] + [quo.components.buttons.button.view :as button] + [quo.components.drawers.permission-context.schema :as component-schema] + [quo.components.drawers.permission-context.style :as style] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.components.tags.number-tag.view :as number-tag] + [quo.components.tags.token-tag.view :as token-tag] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.shadow :as shadow] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(defn- single-token-gating + [{:keys [blur? token-value token-symbol]}] + [rn/view + {:style {:flex-direction :row} + :accessibility-label :permission-context-single-token} + [text/text {:style {:margin-right 3}} + (i18n/label :t/hold-to-post-1)] + [token-tag/view + {:size :size-24 + :option false + :blur? blur? + :token-value token-value + :token-symbol token-symbol}] + [text/text {:style {:margin-left 3}} + (i18n/label :t/hold-to-post-2)]]) + +(defn- token-group + [blur? idx group] + ^{:key idx} + [rn/view {:style style/token-group} + (when-not (zero? idx) + [text/text {:style {:margin-right 3}} + (string/lower-case (i18n/label :t/or))]) + [preview-list/view + {:type :tokens + :blur? blur? + :size :size-24} group]]) + +(defn- multiple-token-gating + [{:keys [token-groups blur?]}] + (let [visible-token-groups (take 2 token-groups) + extra-groups? (-> token-groups count (> 2))] + [rn/view + {:style {:flex-direction :row} + :accessibility-label :permission-context-multiple-token} + [text/text (i18n/label :t/hold-to-post-1)] + [rn/view {:style {:flex-direction :row}} + (map-indexed (partial token-group blur?) visible-token-groups)] + (when extra-groups? + [:<> + [text/text {:style {:margin-horizontal 3}} + (string/lower-case (i18n/label :t/or))] + [number-tag/view + {:size :size-24 + :number "9999" ;; show the options tag + :blur? blur? + :type :rounded}]]) + [text/text {:style {:margin-left 3}} + (i18n/label :t/hold-to-post-2)]])) + +(defn- view-internal + [{:keys [on-press blur? container-style] :as props}] + (let [theme (quo.context/use-theme) + context-type (:type props)] + [shadow/view + {:offset [0 4] + :paint-inside false + :start-color (colors/theme-colors colors/neutral-100-opa-8 colors/neutral-100-opa-60 theme) + :distance 25 + :style {:align-self :stretch}} + [rn/view {:style (merge (style/container blur? theme) container-style)} + (when blur? + [blur/view + {:style style/blur-container + :blur-amount 20 + :blur-radius (if platform/ios? 20 10) + :overlay-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur theme) + :blur-type :transparent}]) + [button/button + {:type :ghost + :size 24 + :on-press on-press + :icon-left (when (= context-type :action) + (:action-icon props))} + (condp = context-type + :action (:action-label props) + :single-token-gating [single-token-gating + (select-keys props [:token-value :token-symbol :blur?])] + :multiple-token-gating [multiple-token-gating + (select-keys props [:token-groups :blur?])])]]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) + diff --git a/src/quo/components/dropdowns/dropdown/component_spec.cljs b/src/quo/components/dropdowns/dropdown/component_spec.cljs new file mode 100644 index 00000000000..b3c06e3a0c2 --- /dev/null +++ b/src/quo/components/dropdowns/dropdown/component_spec.cljs @@ -0,0 +1,36 @@ +(ns quo.components.dropdowns.dropdown.component-spec + (:require + [quo.components.dropdowns.dropdown.view :as dropdown] + [test-helpers.component :as h])) + +(h/describe "Dropdown" + (h/test "default render" + (let [label "Recent"] + (h/render [dropdown/view {} label]) + (h/is-truthy (h/get-by-label-text :dropdown)) + (h/is-null (h/query-by-label-text :left-icon)) + (h/is-truthy (h/get-by-text label)) + (h/is-truthy (h/get-by-label-text :right-icon)))) + + (h/test "render with icon" + (let [label "Recent"] + (h/render [dropdown/view {:icon? true} label]) + (h/is-truthy (h/get-by-label-text :dropdown)) + (h/is-truthy (h/get-by-label-text :left-icon)) + (h/is-truthy (h/get-by-text label)) + (h/is-truthy (h/get-by-label-text :right-icon)))) + + (h/test "render only emoji" + (let [label "🍑"] + (h/render [dropdown/view {:emoji? true} label]) + (h/is-truthy (h/get-by-label-text :dropdown)) + (h/is-null (h/query-by-label-text :left-icon)) + (h/is-truthy (h/get-by-text label)) + (h/is-null (h/query-by-label-text :right-icon)))) + + (h/test "on-press" + (let [event (h/mock-fn) + label "Recent"] + (h/render [dropdown/view {:on-press event} label]) + (h/fire-event :press (h/get-by-text label)) + (h/was-called event)))) diff --git a/src/quo/components/dropdowns/dropdown/properties.cljs b/src/quo/components/dropdowns/dropdown/properties.cljs new file mode 100644 index 00000000000..abe08edd6c1 --- /dev/null +++ b/src/quo/components/dropdowns/dropdown/properties.cljs @@ -0,0 +1,121 @@ +(ns quo.components.dropdowns.dropdown.properties + (:require + [quo.foundations.colors :as colors])) + +(def backgrounds #{:photo :blur}) + +(def sizes + {:size-40 {:size 40 + :icon-size 20 + :emoji-size 20 + :emoji-padding 10 + :padding-vertical 9 + :padding-left {:default 16 :icon 12} + :padding-right 12 + :border-radius 12 + :text-size :paragraph-1} + :size-32 {:size 32 + :icon-size 20 + :emoji-size 20 + :emoji-padding 6 + :padding-vertical 5 + :padding-left {:default 12 :icon 8} + :padding-right 8 + :border-radius 10 + :text-size :paragraph-1} + :size-24 {:size 24 + :icon-size 12 + :emoji-size 12 + :emoji-padding 6 + :padding-vertical 3 + :padding-left {:default 8 :icon 8} + :padding-right 8 + :border-radius 8 + :text-size :paragraph-2}}) + +(defn- custom-color-type + [customization-color] + {:left-icon-color colors/white-opa-70 + :right-icon-color colors/white-opa-20 + :right-icon-color-2 colors/white + :label-color colors/white + :background-color (colors/resolve-color customization-color :light)}) + +(def sizes-to-exclude-blur-in-photo-bg #{:size-40}) + +(defn- grey-photo + [theme active? size] + {:left-icon-color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :right-icon-color (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :blur-overlay-color (when (and (= theme :dark) (nil? (sizes-to-exclude-blur-in-photo-bg size))) + (if active? colors/neutral-80-opa-50 colors/neutral-80-opa-40)) + :blur-type (if (= theme :light) :light :dark) + :background-color (cond + (= theme :light) + (if active? colors/white-opa-50 colors/white-opa-40) + + (and (= theme :dark) (sizes-to-exclude-blur-in-photo-bg size)) + (if active? colors/neutral-80-opa-50 colors/neutral-80-opa-40))}) + +(defn- grey-blur + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-70 theme) + :right-icon-color (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :background-color (if active? + (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme))}) + +(defn- outline-blur + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :right-icon-color (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-color (if active? + (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme))}) + +(defn- outline + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :right-icon-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-color (if active? + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-30 colors/neutral-70 theme))}) + +(defn- grey + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :right-icon-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :background-color (if active? + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + (colors/theme-colors colors/neutral-5 colors/neutral-90 theme))}) + +(defn- ghost + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :right-icon-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :background-color (when active? + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) + +(defn get-colors + [{:keys [customization-color type state background size]} theme] + (let [active? (= state :active)] + (cond + (and (= background :photo) (= type :grey)) (grey-photo theme active? size) + (and (= background :blur) (= type :grey)) (grey-blur theme active?) + (and (= background :blur) (= type :outline)) (outline-blur theme active?) + (= type :outline) (outline theme active?) + (= type :grey) (grey theme active?) + (= type :ghost) (ghost theme active?) + (= type :customization) (custom-color-type customization-color)))) diff --git a/src/quo/components/dropdowns/dropdown/style.cljs b/src/quo/components/dropdowns/dropdown/style.cljs new file mode 100644 index 00000000000..c59a43f66fb --- /dev/null +++ b/src/quo/components/dropdowns/dropdown/style.cljs @@ -0,0 +1,53 @@ +(ns quo.components.dropdowns.dropdown.style) + +(def gap 4) + +(def blur-view + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0}) + +(def left-icon + {:margin-right gap}) + +(defn right-icon + [label-color] + {:color label-color + :margin-right gap}) + +(defn- add-styles-for-emoji + [style size emoji-padding] + (-> style + (dissoc :padding-vertical :padding-left :padding-right) + (assoc :width size + :padding emoji-padding))) + +(defn root-container + [{:keys [size border-radius padding-vertical padding-right padding-left emoji-padding]} + {:keys [background-color border-color]} + {:keys [icon? emoji? state]}] + (cond-> {:height size + :align-items :center + :justify-content :center + :flex-direction :row + :padding-vertical padding-vertical + :padding-left (:default padding-left) + :padding-right padding-right + :overflow :hidden + :background-color background-color + :border-radius border-radius} + + icon? + (assoc :padding-left (:icon padding-left)) + + emoji? + (add-styles-for-emoji size emoji-padding) + + border-color + (assoc :border-color border-color + :border-width 1) + + (= state :disabled) + (assoc :opacity 0.3))) diff --git a/src/quo/components/dropdowns/dropdown/view.cljs b/src/quo/components/dropdowns/dropdown/view.cljs new file mode 100644 index 00000000000..d78aa16471b --- /dev/null +++ b/src/quo/components/dropdowns/dropdown/view.cljs @@ -0,0 +1,88 @@ +(ns quo.components.dropdowns.dropdown.view + (:require + [quo.components.blur.view :as blur] + [quo.components.dropdowns.dropdown.properties :as properties] + [quo.components.dropdowns.dropdown.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.customization-colors :as customization-colors] + [react-native.core :as rn])) + +(defn view + "Props: + - type: :outline |:grey (default) | :ghost | :customization + - size: :size-40 (default) | :size-32 | :size-24 + - state: :default (default) | :active | :disabled + - emoji?: boolean + - icon?: boolean + - no-icon-color?: boolean + - background: :blur | :photo (optional) + - icon-name: keyword + - on-press: function + + Child: string - used as label or emoji (for emoji only)" + [{:keys [type size state background customization-color on-press icon-name icon? emoji? + accessibility-label no-icon-color?] + :or {type :grey + size :size-40 + state :default + icon? false + emoji? false + icon-name :i/placeholder} + :as props} + text] + (let [theme (quo.context/use-theme) + {:keys [icon-size text-size emoji-size border-radius] + :as size-properties} (properties/sizes size) + {:keys [left-icon-color right-icon-color right-icon-color-2 label-color blur-type + blur-overlay-color] + :as colors} (properties/get-colors props theme) + right-icon (if (= state :active) :i/pullup :i/dropdown) + customization-type? (= type :customization) + show-blur-background? (and (= background :photo) + (= type :grey) + (nil? (properties/sizes-to-exclude-blur-in-photo-bg size)))] + [rn/pressable + {:accessibility-label (or accessibility-label :dropdown) + :disabled (= state :disabled) + :on-press on-press + :style (style/root-container size-properties colors props)} + (when show-blur-background? + [blur/view + {:blur-radius 20 + :blur-type blur-type + :overlay-color blur-overlay-color + :style style/blur-view}]) + (when customization-type? + [customization-colors/overlay + {:customization-color customization-color + :theme theme + :pressed? (= state :active) + :border-radius border-radius}]) + (if emoji? + [rn/text + {:adjusts-font-size-to-fit true + :style {:font-size emoji-size}} + text] + [:<> + (when icon? + [icon/icon + icon-name + {:accessibility-label :left-icon + :color left-icon-color + :no-color no-icon-color? + :size icon-size + :container-style style/left-icon}]) + [text/text + {:size text-size + :weight :medium + :number-of-lines 1 + :style (style/right-icon label-color)} + text] + [icon/icon + right-icon + {:size icon-size + :accessibility-label :right-icon + :color right-icon-color + :color-2 right-icon-color-2}]])])) diff --git a/src/quo/components/dropdowns/dropdown_input/component_spec.cljs b/src/quo/components/dropdowns/dropdown_input/component_spec.cljs new file mode 100644 index 00000000000..d0d96ceb7c3 --- /dev/null +++ b/src/quo/components/dropdowns/dropdown_input/component_spec.cljs @@ -0,0 +1,56 @@ +(ns quo.components.dropdowns.dropdown-input.component-spec + (:require + [quo.components.dropdowns.dropdown-input.view :as dropdown-input] + [test-helpers.component :as h])) + +(h/describe "Dropdown Input" + + (h/test "default render" + (let [header-label "Label" + label "Dropdown"] + (h/render [dropdown-input/view + {:label? true + :header-label + header-label} label]) + (h/is-truthy (h/get-by-label-text :dropdown)) + (h/is-truthy (h/get-by-text header-label)) + (h/is-truthy (h/get-by-text label)))) + + (h/test "render with icon" + (let [label "Dropdown"] + (h/render [dropdown-input/view + {:icon? true + :icon-name + :i/wallet + :label? true} + label]) + (h/is-truthy (h/get-by-label-text :dropdown)) + (h/is-truthy (h/get-by-label-text :left-icon)) + (h/is-truthy (h/get-by-text label)))) + + (h/test "render in active state" + (let [label "Dropdown"] + (h/render [dropdown-input/view + {:state :active + :label? + true} label]) + (h/is-truthy (h/get-by-label-text :dropdown)) + (h/is-truthy (h/get-by-label-text :right-icon "Expecting active state icon")))) + + (h/test "render in disabled state" + (let [label "Dropdown"] + (h/render [dropdown-input/view + {:state :disabled + :label? true} label]) + (h/is-truthy (h/get-by-label-text :dropdown)) + (h/is-disabled (h/get-by-text label "Dropdown should be disabled in disabled state")))) + + (h/test "on-press" + (let [event (h/mock-fn) + label "Dropdown"] + (h/render [dropdown-input/view + {:on-press event + :label? + true} label]) + (h/fire-event :press (h/get-by-text label)) + (h/was-called event)))) diff --git a/src/quo/components/dropdowns/dropdown_input/properties.cljs b/src/quo/components/dropdowns/dropdown_input/properties.cljs new file mode 100644 index 00000000000..33153bdb497 --- /dev/null +++ b/src/quo/components/dropdowns/dropdown_input/properties.cljs @@ -0,0 +1,49 @@ +(ns quo.components.dropdowns.dropdown-input.properties + (:require + [quo.foundations.colors :as colors])) + +(def icon-size 20) + +(defn- grey-blur + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-70 theme) + :right-icon-color (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-color (if active? + (colors/theme-colors colors/neutral-80-opa-20 + colors/white-opa-40 + theme) + (colors/theme-colors colors/neutral-80-opa-10 + colors/white-opa-10 + theme))}) + +(defn- outline + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :right-icon-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-color (if active? + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-30 colors/neutral-70 theme))}) + +(defn- grey + [theme active?] + {:left-icon-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :right-icon-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :right-icon-color-2 (colors/theme-colors colors/neutral-100 colors/white theme) + :label-color (colors/theme-colors colors/neutral-100 colors/white theme) + :border-color (if active? + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + (colors/theme-colors colors/neutral-20 + colors/neutral-60 + theme))}) + +(defn get-colors + [{:keys [theme state blur?]}] + (let [active? (= state :active)] + (cond + blur? (grey-blur theme active?) + (= theme :dark) (outline theme active?) + (= theme :light) (grey theme active?)))) diff --git a/src/quo/components/dropdowns/dropdown_input/style.cljs b/src/quo/components/dropdowns/dropdown_input/style.cljs new file mode 100644 index 00000000000..1a1dc661bcf --- /dev/null +++ b/src/quo/components/dropdowns/dropdown_input/style.cljs @@ -0,0 +1,55 @@ +(ns quo.components.dropdowns.dropdown-input.style + (:require + [quo.foundations.colors :as colors])) + +(def gap 8) + +(def left-icon + {:margin-right gap}) + +(defn right-icon + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :margin-right gap}) + +(defn header-label + [theme blur?] + {:color (if blur? + (colors/theme-colors colors/neutral-80-opa-40 + colors/white-opa-70 + theme) + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme)) + :margin-bottom gap}) + +(def root-container + {:width "100%" + :height 40}) + +(defn container + [{:keys [background-color border-color]} + {:keys [icon? state]}] + (cond-> {:align-items :center + :justify-content :space-between + :flex-direction :row + :padding-vertical 9 + :padding-left 16 + :padding-right 12 + :overflow :hidden + :background-color background-color + :border-radius 12} + + icon? + (assoc :padding-left 12) + + border-color + (assoc :border-color border-color + :border-width 1) + + (= state :disabled) + (assoc :opacity 0.3))) + +(def right-half-container + {:flex-direction :row + :align-items :center}) diff --git a/src/quo/components/dropdowns/dropdown_input/view.cljs b/src/quo/components/dropdowns/dropdown_input/view.cljs new file mode 100644 index 00000000000..b8f39093cec --- /dev/null +++ b/src/quo/components/dropdowns/dropdown_input/view.cljs @@ -0,0 +1,64 @@ +(ns quo.components.dropdowns.dropdown-input.view + (:require + [quo.components.dropdowns.dropdown-input.properties :as properties] + [quo.components.dropdowns.dropdown-input.style :as style] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn])) + +(defn view + "Props: + - state: :default (default) | :active | :disabled + - label: string + - header-label: string + - icon?: boolean + - label?: boolean + - blur?: boolean + - on-press: function" + [{:keys [state on-press icon? label? blur? accessibility-label header-label] + :or {state :default + icon? true + label? true + blur? false + header-label "Label"} + :as props} + label] + (let [theme (quo.context/use-theme) + {:keys [left-icon-color right-icon-color right-icon-color-2] + :as colors} (properties/get-colors props) + right-icon (if (= state :active) :i/pullup :i/dropdown)] + [rn/view + {:style style/root-container} + (when label? + [text/text + {:size :paragraph-2 + :weight :medium + :align :left + :style (style/header-label theme blur?)} header-label]) + [rn/pressable + {:accessibility-label (or accessibility-label :dropdown) + :disabled (= state :disabled) + :on-press on-press + :style (style/container colors props)} + [rn/view + {:style style/right-half-container} + (when icon? + [icon/icon + :i/placeholder + {:accessibility-label :left-icon + :color left-icon-color + :size properties/icon-size + :container-style style/left-icon}]) + [text/text + {:size :paragraph-1 + :weight :regular + :number-of-lines 1 + :style (style/right-icon theme)} + label]] + [icon/icon + right-icon + {:size properties/icon-size + :accessibility-label :right-icon + :color right-icon-color + :color-2 right-icon-color-2}]]])) diff --git a/src/quo/components/dropdowns/network_dropdown/component_spec.cljs b/src/quo/components/dropdowns/network_dropdown/component_spec.cljs new file mode 100644 index 00000000000..94747afcc9b --- /dev/null +++ b/src/quo/components/dropdowns/network_dropdown/component_spec.cljs @@ -0,0 +1,58 @@ +(ns quo.components.dropdowns.network-dropdown.component-spec + (:require + [quo.components.dropdowns.network-dropdown.view :as network-dropdown] + [quo.foundations.resources :as quo.resources] + [test-helpers.component :as h])) + +(def ^:private networks-list + [{:source (quo.resources/get-network :ethereum)} + {:source (quo.resources/get-network :optimism)} + {:source (quo.resources/get-network :arbitrum)}]) + +(h/describe "Network dropdown" + (h/test "default render" + (h/render [network-dropdown/view {:state :default} + networks-list]) + (h/is-truthy (h/query-by-label-text :network-dropdown))) + + (h/test "Should call :on-press in enabled state" + (let [on-press (h/mock-fn)] + (h/render [network-dropdown/view + {:state :default + :on-press on-press} + networks-list]) + (h/is-truthy (h/query-by-label-text :network-dropdown)) + (h/fire-event :press (h/query-by-label-text :network-dropdown)) + (h/was-called on-press))) + + (h/test "Should not call :on-press in disabled state" + (let [on-press (h/mock-fn)] + (h/render [network-dropdown/view + {:state :disabled + :on-press on-press} + networks-list]) + (h/is-truthy (h/query-by-label-text :network-dropdown)) + (h/fire-event :press (h/query-by-label-text :network-dropdown)) + (h/was-not-called on-press))) + + (h/test "Should display new chain indicator" + (h/render [network-dropdown/view + {:state :default + :show-new-chain-indicator? true} + networks-list]) + (h/is-truthy (h/query-by-label-text :network-dropdown)) + (h/is-truthy (h/query-by-label-text :new-chain-indicator))) + + (h/test "Should show label" + (h/render [network-dropdown/view + {:state :default + :label "Mainnet"} + networks-list]) + (h/is-truthy (h/get-by-text "Mainnet"))) + + (h/test "Should show dropdown icon" + (h/render [network-dropdown/view + {:state :default + :dropdown-icon? true} + networks-list]) + (h/is-truthy (h/query-by-label-text :dropdown-icon)))) diff --git a/src/quo/components/dropdowns/network_dropdown/style.cljs b/src/quo/components/dropdowns/network_dropdown/style.cljs new file mode 100644 index 00000000000..f9087a2ff9b --- /dev/null +++ b/src/quo/components/dropdowns/network_dropdown/style.cljs @@ -0,0 +1,59 @@ +(ns quo.components.dropdowns.network-dropdown.style + (:require + [quo.foundations.colors :as colors])) + +(defn container-border-color + [{:keys [state blur? theme pressed?]}] + (let [default-color (if blur? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/theme-colors colors/neutral-30 colors/neutral-70 theme)) + active-color (if blur? + (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme))] + (condp = state + :disabled default-color + :default (if pressed? active-color default-color)))) + +(defn dropdown-container + [{:keys [state] :as props}] + {:border-width 1 + :border-radius 10 + :padding-horizontal 8 + :padding-vertical 6 + :opacity (if (= state :disabled) 0.3 1) + :border-color (container-border-color props) + :align-items :center}) + +(defn dropdown-text + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :padding-left 8}) + +(def dropdown-icon-container + {:padding-left 8}) + +(defn dropdown-icon-colors + [theme] + {:background (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :foreground (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(def new-chain-indicator + {:position :absolute + :right 0}) + +(def single-network-container + {:flex-direction :row + :align-items :center + :justify-content :center + :margin-right 4}) + +(def filtered-container + {:flex-direction :row + :align-items :center + :justify-content :center}) + +(def single-network-image + {:width 20 + :height 20 + :border-radius 10 + :margin-right 4}) diff --git a/src/quo/components/dropdowns/network_dropdown/view.cljs b/src/quo/components/dropdowns/network_dropdown/view.cljs new file mode 100644 index 00000000000..50bfb4fed2c --- /dev/null +++ b/src/quo/components/dropdowns/network_dropdown/view.cljs @@ -0,0 +1,77 @@ +(ns quo.components.dropdowns.network-dropdown.view + (:require + [quo.components.common.new-feature-dot :as new-feature-dot] + [quo.components.dropdowns.network-dropdown.style :as style] + [quo.components.icon :as icon] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn single-network-preview + [item] + [rn/view {:style style/single-network-container} + [fast-image/fast-image + {:source (or (:source item) item) + :style style/single-network-image}] + [text/text + {:weight :medium + :size :paragraph-2} (:full-name item)]]) + +(defn dropdown-icon + [theme] + (let [{:keys [background foreground]} (style/dropdown-icon-colors theme)] + [rn/view {:style style/dropdown-icon-container} + [icon/icon + :i/dropdown + {:size 20 + :accessibility-label :dropdown-icon + :color background + :color-2 foreground}]])) + +(defn view + [{:keys [on-press networks-filtered? state show-new-chain-indicator? label dropdown-icon?] :as props} + networks] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/pressable + {:style (style/dropdown-container (merge props {:pressed? pressed? :theme theme})) + :accessibility-label :network-dropdown + :disabled (= state :disabled) + :on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out} + (when show-new-chain-indicator? + [new-feature-dot/view + {:style style/new-chain-indicator + :accessibility-label :new-chain-indicator}]) + (if networks-filtered? + [rn/view {:style style/filtered-container} + (if (= 1 (count networks)) + [single-network-preview (first networks)] + [preview-list/view + {:type :network + :list-size (count networks) + :size :size-20} + networks]) + [icon/icon :i/close + {:size 16 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}]] + [preview-list/view + {:type :network + :list-size (count networks) + :size :size-20} + networks]) + (when label + [text/text + {:size :paragraph-1 + :weight :medium + :style (style/dropdown-text theme) + :number-of-lines 1} + label]) + (when dropdown-icon? + [dropdown-icon theme])])) diff --git a/src/quo/components/empty_state/empty_state/styles.cljs b/src/quo/components/empty_state/empty_state/styles.cljs new file mode 100644 index 00000000000..694047d5c2d --- /dev/null +++ b/src/quo/components/empty_state/empty_state/styles.cljs @@ -0,0 +1,38 @@ +(ns quo.components.empty-state.empty-state.styles + (:require + [quo.foundations.colors :as colors])) + +(def container + {:padding 12 + :align-items :center}) + +(def image + {:width 72 + :height 72}) + +(def image-container + {:width 80 + :height 80 + :align-items :center + :justify-content :center}) + +(def text-container + {:margin-top 12 + :align-items :center}) + +(defn title + [blur?] + (cond-> {:margin-bottom 2} + blur? (assoc :color colors/white))) + +(defn description + [blur?] + (cond-> {:text-align :center} + blur? (assoc :color colors/white))) + +(def button-container {:margin-top 20}) + +(def image-placeholder + {:width 80 + :height 80 + :background-color colors/danger-50}) diff --git a/src/quo/components/empty_state/empty_state/view.cljs b/src/quo/components/empty_state/empty_state/view.cljs new file mode 100644 index 00000000000..996dac91383 --- /dev/null +++ b/src/quo/components/empty_state/empty_state/view.cljs @@ -0,0 +1,51 @@ +(ns quo.components.empty-state.empty-state.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.empty-state.empty-state.styles :as styles] + [quo.components.markdown.text :as text] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn empty-state + [{:keys [customization-color image title description blur? placeholder? container-style] + upper-button :upper-button + lower-button :lower-button + :or {customization-color :blue}}] + [rn/view {:style (merge styles/container container-style)} + (if placeholder? + [rn/view {:style styles/image-placeholder}] + [rn/view {:style styles/image-container} + [fast-image/fast-image + {:style styles/image + :source image}]]) + [rn/view {:style styles/text-container} + [text/text + {:style (styles/title blur?) + :number-of-lines 1 + :weight :semi-bold + :size :paragraph-1} + title] + [text/text + {:style (styles/description blur?) + :number-of-lines 2 + :weight :regular + :size :paragraph-2} + description]] + (when-let [{upper-button-text :text + upper-button-on-press :on-press} upper-button] + [rn/view {:style styles/button-container} + [button/button + {:type :primary + :size 32 + :customization-color customization-color + :on-press upper-button-on-press} + upper-button-text] + (when-let [{lower-button-text :text + lower-button-on-press :on-press} lower-button] + [button/button + {:container-style {:margin-top 12} + :size 32 + :type :grey + :background :blur + :on-press lower-button-on-press} + lower-button-text])])]) diff --git a/src/quo/components/gradient/gradient_cover/component_spec.cljs b/src/quo/components/gradient/gradient_cover/component_spec.cljs new file mode 100644 index 00000000000..b18d10b03cc --- /dev/null +++ b/src/quo/components/gradient/gradient_cover/component_spec.cljs @@ -0,0 +1,9 @@ +(ns quo.components.gradient.gradient-cover.component-spec + (:require + [quo.components.gradient.gradient-cover.view :as gradient-cover] + [test-helpers.component :as h])) + +(h/describe "gradient cover" + (h/test "default render" + (h/render [gradient-cover/view]) + (h/is-truthy (h/get-by-label-text :gradient-cover)))) diff --git a/src/quo/components/gradient/gradient_cover/style.cljs b/src/quo/components/gradient/gradient_cover/style.cljs new file mode 100644 index 00000000000..4e22eb48d10 --- /dev/null +++ b/src/quo/components/gradient/gradient_cover/style.cljs @@ -0,0 +1,11 @@ +(ns quo.components.gradient.gradient-cover.style) + +(defn root-container + [opacity height] + {:height (or height 252) + :opacity opacity + :position :absolute + :top 0 + :left 0 + :right 0 + :z-index -1}) diff --git a/src/quo/components/gradient/gradient_cover/view.cljs b/src/quo/components/gradient/gradient_cover/view.cljs new file mode 100644 index 00000000000..ef271710bc9 --- /dev/null +++ b/src/quo/components/gradient/gradient_cover/view.cljs @@ -0,0 +1,22 @@ +(ns quo.components.gradient.gradient-cover.view + (:require + [quo.components.gradient.gradient-cover.style :as style] + [quo.foundations.colors :as colors] + [react-native.linear-gradient :as linear-gradient])) + +(defn view + [{:keys [customization-color opacity container-style height bottom-color-override] + :or {customization-color :blue}}] + ;; `when` added for safety, `linear-gradient` will break if `nil` is passed, the `:or` + ;; destructuring won't work because it's only applied when the `:customization-color` key is + ;; non-existent. While deleting an account the key exists and has a `nil` value. + (let [color-top (colors/resolve-color customization-color 50 20) + color-bottom (or bottom-color-override (colors/resolve-color customization-color 50 0))] + (when (and color-top color-bottom) + [linear-gradient/linear-gradient + {:accessibility-label :gradient-cover + :colors [color-top color-bottom] + :start {:x 0 :y 0} + :end {:x 0 :y 1} + :style [(style/root-container opacity height) + container-style]}]))) diff --git a/src/quo/components/graph/interactive_graph/component_spec.cljs b/src/quo/components/graph/interactive_graph/component_spec.cljs new file mode 100644 index 00000000000..8c52766e5d5 --- /dev/null +++ b/src/quo/components/graph/interactive_graph/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo.components.graph.interactive-graph.component-spec + (:require + [quo.components.graph.interactive-graph.view :as interactive-graph] + [test-helpers.component :as h])) + +(defn data + [num-elements] + (vec (take num-elements (repeat {:value 10})))) + +(h/describe "interactive-graph" + (h/test "render interactive graph" + (h/render [interactive-graph/view + {:data data}]) + (h/is-truthy (h/get-by-label-text :interactive-graph)))) diff --git a/src/quo/components/graph/interactive_graph/style.cljs b/src/quo/components/graph/interactive_graph/style.cljs new file mode 100644 index 00000000000..9cd48a164ec --- /dev/null +++ b/src/quo/components/graph/interactive_graph/style.cljs @@ -0,0 +1,50 @@ +(ns quo.components.graph.interactive-graph.style + (:require + [quo.foundations.typography :as typography])) + +(defn x-axis-label-text + [width y-axis-label-text-color] + (merge + typography/label + {:color y-axis-label-text-color + :height 16 + :text-align :center + :width width})) + +(defn y-axis-text + [y-axis-label-text-color y-axis-label-background-color] + (merge + typography/label + {:color y-axis-label-text-color + :padding-horizontal 3 + :margin-left 23 + :height 16 + :border-radius 6 + :overflow :hidden + :background-color y-axis-label-background-color})) + +(defn pointer-component + [customization-color] + {:width 8 + :height 8 + :border-radius 4 + :margin-left 1 + :background-color customization-color}) + +(defn reference-line-label + [border-color background-color text-color] + (merge + typography/label + {:align-self :flex-end + :right 10 + :margin-top -9 + :padding-horizontal 5 + :padding-top -10 + :height 19 + :line-height 14.62 + :border-radius 6 + :overflow :hidden + :border-color border-color + :border-width 2 + :background-color background-color + :color text-color})) diff --git a/src/quo/components/graph/interactive_graph/view.cljs b/src/quo/components/graph/interactive_graph/view.cljs new file mode 100644 index 00000000000..ea45a1f7b43 --- /dev/null +++ b/src/quo/components/graph/interactive_graph/view.cljs @@ -0,0 +1,138 @@ +(ns quo.components.graph.interactive-graph.view + (:require + [quo.components.graph.interactive-graph.style :as style] + [quo.components.graph.utils :as utils] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.charts :as charts] + [react-native.core :as rn] + [reagent.core :as reagent])) + +(def chart-height 375) +(def max-data-points 500) +(def no-of-sections 4) +(def initial-spacing 56) +(def end-spacing 22) +(def y-axis-label-width -33) + +(defn- pointer + [customization-color] + (reagent/as-element + [rn/view + {:style + (style/pointer-component + customization-color)}])) + +(defn- pointer-config + [customization-color] + {:stroke-dash-array [2 2] + :pointer-component #(pointer customization-color) + :pointer-strip-color customization-color + :pointer-color customization-color + :pointer-strip-enable-gradient true}) + +(defn- get-line-color + [state theme inspecting?] + (if inspecting? + (colors/theme-colors colors/neutral-80-opa-40 + colors/white-opa-20 + theme) + (if (= state :positive) + (colors/theme-colors colors/success-50 + colors/success-60 + theme) + (colors/theme-colors colors/danger-50 + colors/danger-60 + theme)))) + +(defn view + [{:keys [data state customization-color reference-value reference-prefix decimal-separator] + :or {reference-prefix "$" + decimal-separator :dot}}] + (let [theme (quo.context/use-theme) + [inspecting? set-inspecting] (rn/use-state false) + data (if (> (count data) max-data-points) + (utils/downsample-data data max-data-points) + data) + highest-value (utils/find-highest-value data) + lowest-value (utils/find-lowest-value data) + min-value (utils/calculate-rounded-min lowest-value) + max-value (- (utils/calculate-rounded-max highest-value) min-value) + step-value (/ max-value 4) + width (:width (rn/get-window)) + line-color (get-line-color state theme inspecting?) + get-pointer-props (rn/use-callback + (fn [pointer-props] + (let [pointer-index (.-pointerIndex ^js pointer-props)] + (set-inspecting (not= pointer-index -1))))) + rules-color (colors/theme-colors colors/neutral-80-opa-10 + colors/white-opa-5 + theme) + y-axis-label-text-color (colors/theme-colors colors/neutral-80-opa-40 + colors/white-opa-40 + theme) + price-reference-label-text-color (colors/theme-colors colors/neutral-100 colors/white theme) + reference-label-border-color (colors/theme-colors colors/white colors/neutral-95 theme) + y-axis-label-background-color (colors/theme-colors colors/white-70-blur-opaque + colors/neutral-95 + theme) + customization-color (colors/resolve-color customization-color + (colors/invert-theme theme)) + y-axis-label-texts (utils/calculate-y-axis-labels min-value step-value 4) + x-axis-label-texts (utils/calculate-x-axis-labels data 5) + reference-label-background-color (colors/theme-colors colors/neutral-80-opa-5-opaque + colors/neutral-80 + theme) + reference-value (or reference-value (/ (+ highest-value lowest-value) 2)) + formatted-reference-value (utils/format-currency-number reference-value decimal-separator) + chart-width (+ width 13)] + [rn/view {:accessibility-label :interactive-graph} + [charts/line-chart + {:height chart-height + :width chart-width + :max-value max-value + :x-axis-length chart-width + :y-axis-offset min-value + :y-axis-label-texts y-axis-label-texts + :y-axis-label-texts-ignore-offset true + :adjust-to-width true + :data data + :hide-data-points true + :no-of-sections no-of-sections + :step-value step-value + :rules-color rules-color + :dash-width 2 + :dash-gap 2 + :hide-y-axis-text false + :thickness 1 + :color line-color + :y-axis-thickness 0 + :x-axis-thickness 0 + :initial-spacing initial-spacing + :end-spacing end-spacing + :disable-scroll true + :hide-origin true + :show-reference-line-1 true + :get-pointer-props get-pointer-props + :show-strip-on-focus true + :reference-line-1-config {:color rules-color} + :reference-line-1-position 0 + :show-reference-line-2 (and (not inspecting?) + (<= reference-value highest-value) + (>= reference-value lowest-value)) + :reference-line-2-config {:color y-axis-label-text-color + :label-text-style (style/reference-line-label + reference-label-border-color + reference-label-background-color + price-reference-label-text-color) + :label-text (str reference-prefix + formatted-reference-value) + :dash-width 2} + :reference-line-2-position (- reference-value min-value) + :y-axis-text-style (style/y-axis-text y-axis-label-text-color + y-axis-label-background-color) + :y-axis-label-width y-axis-label-width + :pointer-config (pointer-config customization-color) + :x-axis-label-text-style (style/x-axis-label-text (/ width (count x-axis-label-texts)) + y-axis-label-text-color) + :x-axis-label-texts x-axis-label-texts}]])) diff --git a/src/quo/components/graph/utils.cljs b/src/quo/components/graph/utils.cljs new file mode 100644 index 00000000000..25dc9617e95 --- /dev/null +++ b/src/quo/components/graph/utils.cljs @@ -0,0 +1,96 @@ +(ns quo.components.graph.utils + (:require + [clojure.string :as string] + [goog.string :as gstring])) + +(defn find-highest-value + [coll] + (apply max (map :value coll))) + +(defn find-lowest-value + [coll] + (apply min (map :value coll))) + +(defn downsample-data + [data max-array-size] + (let [data-size (count data)] + (if (> data-size max-array-size) + (let [step-size (max (/ data-size max-array-size) 1)] + (vec (take-nth step-size data))) + data))) + +(defn format-compact-number + [number] + (let [abbreviations ["" "k" "M" "B" "T"] + log-base-1000 (js/Math.log10 1000) ; Calculate the logarithm base 1000 + magnitude (int (/ (js/Math.log10 number) log-base-1000)) + suffix (nth abbreviations magnitude) + scaled-number (/ number (js/Math.pow 1000.0 magnitude)) + formatted-scaled-number (if (zero? (rem scaled-number 1)) + (int scaled-number) + (-> (gstring/format "%.2f" scaled-number) + (string/replace #"\.?0+$" "")))] + (if (zero? magnitude) + (str number) + (if (and (>= scaled-number 1) (< scaled-number 1000)) + (str formatted-scaled-number suffix) + (str formatted-scaled-number "0" suffix))))) + +(defn calculate-x-axis-labels + [coll num-elements] + (let [array-length (count coll) + partitions (partition-all (js/Math.floor (/ array-length (min array-length num-elements))) + coll)] + (->> partitions + (map first) + (map :date)))) + +(defn calculate-y-axis-labels + [min-value step-value no-of-steps] + (let [labels-array (for [i (range (inc no-of-steps))] + (let [value (+ min-value (* step-value i)) + compact-number (format-compact-number value)] + compact-number))] + (vec labels-array))) + +(defn calculate-rounded-max + [highest-value] + (let [min-percentage-above 1.05 ; 5% above + rounded-divisor 4 ; Divisor for even division by 4 + target-max (* min-percentage-above highest-value) + rounded-up (js/Math.ceil target-max) + remainder (mod rounded-up rounded-divisor) + y-axis-max (if (zero? remainder) + rounded-up + (+ rounded-up (- rounded-divisor remainder)))] + y-axis-max)) + +(defn calculate-rounded-min + [lowest] + (let [order-of-magnitude (js/Math.pow 10 (js/Math.floor (js/Math.log10 lowest))) + rounded-min (cond + (and (< lowest 1) (>= lowest 0)) 0 + (>= lowest 1) + (* (js/Math.floor (/ lowest + order-of-magnitude)) + order-of-magnitude) + (< lowest 0) + (* (calculate-rounded-max (* -1 lowest)) + -1))] + rounded-min)) + +(defn format-currency-number + [number decimal-separator] + (let [formatted (-> (if (= decimal-separator :comma) + (js/Intl.NumberFormat + "de-DE" + {:style "currency" :currency "EUR" :minimumFractionDigits 2}) + (js/Intl.NumberFormat + "en-US" + {:style "currency" :currency "USD" :minimumFractionDigits 2})) + (.format number)) + separator-char (if (= decimal-separator :comma) "," ".") + formatted-with-decimals (if (.includes formatted separator-char) + formatted + (str formatted separator-char "00"))] + formatted-with-decimals)) diff --git a/src/quo/components/graph/utils_test.cljs b/src/quo/components/graph/utils_test.cljs new file mode 100644 index 00000000000..ea0495e09b4 --- /dev/null +++ b/src/quo/components/graph/utils_test.cljs @@ -0,0 +1,181 @@ +(ns quo.components.graph.utils-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [quo.components.graph.utils :as utils])) + +(deftest find-highest-value-test + (testing "Find highest value with a single map" + (let [data [{:value 5}]] + (is (= (utils/find-highest-value data) 5)))) + + (testing "Find highest value with multiple maps" + (let [data [{:value 5} {:value 10} {:value 3} {:value 7}]] + (is (= (utils/find-highest-value data) 10)))) + + (testing "Find highest value with negative values" + (let [data [{:value -2} {:value -10} {:value -3} {:value -7}]] + (is (= (utils/find-highest-value data) -2)))) + + (testing "Find highest value with decimal values" + (let [data [{:value 3.5} {:value 7.2} {:value 2.9}]] + (is (= (utils/find-highest-value data) 7.2)))) + + (testing "Find highest value with a large data set" + (let [data (vec (for [num (range 1000)] {:value num}))] + (is (= (utils/find-highest-value data) 999))))) + +(deftest find-lowest-value-test + (testing "Find lowest value with a single map" + (let [data [{:value 5}]] + (is (= (utils/find-lowest-value data) 5)))) + + (testing "Find lowest value with multiple maps" + (let [data [{:value 5} {:value 10} {:value 3} {:value 7}]] + (is (= (utils/find-lowest-value data) 3)))) + + (testing "Find lowest value with negative values" + (let [data [{:value -2} {:value -10} {:value -3} {:value -7}]] + (is (= (utils/find-lowest-value data) -10)))) + + (testing "Find lowest value with decimal values" + (let [data [{:value 3.5} {:value 7.2} {:value 2.9}]] + (is (= (utils/find-lowest-value data) 2.9)))) + + (testing "Find lowest value with a large data set" + (let [data (vec (for [num (range 1000)] {:value num}))] + (is (= (utils/find-lowest-value data) 0))))) + +(deftest downsample-data-test + (testing "Downsampling is applied correctly when needed" + (let [input-data [1 2 3 4 5 6 7 8 9 10] + max-array-size 5 + expected-output [1 3 5 7 9]] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling is not applied when not needed" + (let [input-data [1 2 3 4 5 6 7 8 9 10] + max-array-size 10 + expected-output [1 2 3 4 5 6 7 8 9 10]] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling works with empty input data" + (let [input-data [] + max-array-size 5 + expected-output []] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling works with max-array-size of 1 (edge case)" + (let [input-data [1 2 3 4 5] + max-array-size 1 + expected-output [1]] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling works with large input data and max-array-size (randomized test)" + (let [large-data (range 1000) + max-array-size 500 + expected-output (range 0 1000 2)] ; expected-output contains every 2nd element from 0 to + ; 1000 + (is (= (utils/downsample-data large-data max-array-size) expected-output))))) + +(deftest format-compact-number-test + (testing "Format a whole number less than 1000" + (is (= (utils/format-compact-number 567) "567"))) + + (testing "Format a whole number exactly 1000" + (is (= (utils/format-compact-number 1000) "1k"))) + + (testing "Format a whole number greater than 1000" + (is (= (utils/format-compact-number 2500) "2.5k"))) + + (testing "Format a decimal number less than 1000" + (is (= (utils/format-compact-number 123.45) "123.45"))) + + (testing "Format a decimal number exactly 1000" + (is (= (utils/format-compact-number 1000) "1k"))) + + (testing "Format a decimal number greater than 1000" + (is (= (utils/format-compact-number 7890.123) "7.89k"))) + + (testing "Format a number in millions" + (is (= (utils/format-compact-number 123456789) "123.46M"))) + + (testing "Format a number in billions" + (is (= (utils/format-compact-number 1234567890) "1.23B"))) + + (testing "Format a number in trillions" + (is (= (utils/format-compact-number 1234567890000) "1.23T")))) + +(deftest calculate-x-axis-labels-test + (testing "Calculate x-axis labels with a small array and fewer elements" + (let [data [{:date "2023-01-01"} {:date "2023-01-02"} {:date "2023-01-03"} {:date "2023-01-04"}]] + (is (= (utils/calculate-x-axis-labels data 2) '("2023-01-01" "2023-01-03"))))) + + (testing "Calculate x-axis labels with a larger array and more elements" + (let [data (vec (for [i (range 10)] {:date (str "2023-01-0" (inc i))}))] + (is (= (utils/calculate-x-axis-labels data 5) + '("2023-01-01" "2023-01-03" "2023-01-05" "2023-01-07" "2023-01-09"))))) + + (testing "Calculate x-axis labels with a very small array" + (let [data [{:date "2023-01-01"}]] + (is (= (utils/calculate-x-axis-labels data 3) '("2023-01-01"))))) + + (testing "Calculate x-axis labels with a larger array and a single element" + (let [data (vec (for [i (range 10)] {:date (str "2023-01-0" (inc i))}))] + (is (= (utils/calculate-x-axis-labels data 1) '("2023-01-01")))))) + +(deftest calculate-y-axis-labels-test + (testing "Calculate y-axis labels with positive values" + (is (= (utils/calculate-y-axis-labels 0 10 5) ["0" "10" "20" "30" "40" "50"]))) + + (testing "Calculate y-axis labels with negative values" + (is (= (utils/calculate-y-axis-labels -20 5 4) ["-20" "-15" "-10" "-5" "0"]))) + + (testing "Calculate y-axis labels with decimal step value" + (is (= (utils/calculate-y-axis-labels 2.5 0.5 4) ["2.5" "3" "3.5" "4" "4.5"]))) + + (testing "Calculate y-axis labels with a single step" + (is (= (utils/calculate-y-axis-labels 5 1 1) ["5" "6"]))) + + (testing "Calculate y-axis labels with large step value and number of steps" + (is (= (utils/calculate-y-axis-labels 100 1000 3) ["100" "1.1k" "2.1k" "3.1k"])))) + +(deftest calculate-rounded-max-test + (testing "Calculate rounded max with a whole number" + (is (= (utils/calculate-rounded-max 100) 108))) + + (testing "Calculate rounded max with a decimal number" + (is (= (utils/calculate-rounded-max 50.5) 56))) + + (testing "Calculate rounded max with a number already divisible by divisor" + (is (= (utils/calculate-rounded-max 108) 116))) + + (testing "Calculate rounded max with a number close to the next divisor" + (is (= (utils/calculate-rounded-max 250) 264))) + + (testing "Calculate rounded max with a large number" + (is (= (utils/calculate-rounded-max 1000) 1052)))) + +(deftest calculate-rounded-min-test + (testing "Calculate rounded min with a whole number" + (is (= (utils/calculate-rounded-min 157) 100))) + + (testing "Calculate rounded min with a decimal number" + (is (= (utils/calculate-rounded-min 49.8) 40))) + + (testing "Calculate rounded min with a small number" + (is (= (utils/calculate-rounded-min 0.007) 0))) + + (testing "Calculate rounded min with a number already rounded" + (is (= (utils/calculate-rounded-min 500) 500))) + + (testing "Calculate rounded min with a negative number" + (is (= (utils/calculate-rounded-min -63) -68)))) + +(deftest format-currency-number-test + (testing "Format currency number with comma decimal separator" + (is (= (utils/format-currency-number 12345 :comma) "12.345,00")) + (is (= (utils/format-currency-number 12345.67 :comma) "12.345,67"))) + + (testing "Format currency number with dot decimal separator" + (is (= (utils/format-currency-number 12345 :dot) "12,345.00")) + (is (= (utils/format-currency-number 12345.67 :dot) "12,345.67")))) diff --git a/src/quo/components/graph/wallet_graph/component_spec.cljs b/src/quo/components/graph/wallet_graph/component_spec.cljs new file mode 100644 index 00000000000..871a85d477b --- /dev/null +++ b/src/quo/components/graph/wallet_graph/component_spec.cljs @@ -0,0 +1,23 @@ +(ns quo.components.graph.wallet-graph.component-spec + (:require + [quo.components.graph.wallet-graph.view :as wallet-graph] + [test-helpers.component :as h])) + +(defn data + [num-elements] + (vec (take num-elements (repeat {:value 10})))) + +(h/describe "wallet-graph" + (h/test "render empty wallet graph" + (h/render [wallet-graph/view + {:time-frame :empty}]) + (h/is-truthy (h/get-by-label-text :illustration))) + + ;; NOTE: Throws error: + ;; _reactNative.Animated.timing(widthValue2, { + ;; Cannot read properties of undefined (reading 'timing') + (h/test-skip "render 1 week wallet graph" + (h/render [wallet-graph/view + {:time-frame :1-week + :data (data 7)}]) + (h/is-truthy (h/get-by-label-text :line-chart)))) diff --git a/src/quo/components/graph/wallet_graph/style.cljs b/src/quo/components/graph/wallet_graph/style.cljs new file mode 100644 index 00000000000..6f328a406fa --- /dev/null +++ b/src/quo/components/graph/wallet_graph/style.cljs @@ -0,0 +1,17 @@ +(ns quo.components.graph.wallet-graph.style) + +(def gradient-background + {:height 294 + :position :absolute + :left 0 + :right 0 + :bottom 0}) + +(def x-axis-label-text-style ; We need this to remove unnecessary bottom spacing from graph + {:margin-bottom -3 + :padding-top -10 + :height 0}) + +(def empty-state + {:width 375 + :height 116}) diff --git a/src/quo/components/graph/wallet_graph/view.cljs b/src/quo/components/graph/wallet_graph/view.cljs new file mode 100644 index 00000000000..7651b07380a --- /dev/null +++ b/src/quo/components/graph/wallet_graph/view.cljs @@ -0,0 +1,78 @@ +(ns quo.components.graph.wallet-graph.view + (:require + [quo.components.graph.utils :as utils] + [quo.components.graph.wallet-graph.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.resources :as resources] + [react-native.charts :as charts] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.linear-gradient :as linear-gradient])) + +(defn- time-frame->max-data-points + [time-frame] + (case time-frame + :empty 0 + :1-week 7 + :1-month 30 + :3-months 90 + :1-year 365 + 500)) + +(defn- get-line-color + [customization-color state theme] + (let [color-keyword (cond + customization-color customization-color + (= state :positive) :success + :else :danger)] + (colors/resolve-color color-keyword theme))) + +(defn view + [{:keys [data state time-frame customization-color]}] + (let [theme (quo.context/use-theme) + max-data-points (time-frame->max-data-points time-frame) + data (if (and (not= time-frame :empty) (> (count data) max-data-points)) + (utils/downsample-data data max-data-points) + data) + max-value (when-not (= time-frame :empty) (utils/find-highest-value data)) + width (:width (rn/get-window)) + line-color (get-line-color customization-color state theme) + gradient-colors [(colors/alpha line-color 0.1) (colors/alpha line-color 0)] + fill-color (colors/theme-colors colors/white colors/neutral-95 theme)] + (if (= time-frame :empty) + [fast-image/fast-image + {:style style/empty-state + :source (resources/get-themed-image :no-funds theme) + :accessibility-label :illustration}] + [rn/view + [linear-gradient/linear-gradient + {:colors gradient-colors + :start {:x 0 :y 1} + :end {:x 0 :y 0} + :style style/gradient-background}] + [rn/view {:accessibility-label :line-chart} + [charts/line-chart + {:height 96 + :width (inc width) + :max-value max-value + :min-value 0 + :adjust-to-width true + :data data + :area-chart true + :start-fill-color fill-color + :end-fill-color fill-color + :hide-data-points true + :hide-rules true + :hide-y-axis-text true + :x-axis-indices-height 100 + :thickness 2 + :color line-color + :y-axis-thickness 0 + :x-axis-thickness 0 + :initial-spacing 0 + :end-spacing 0 + :disable-scroll true + :y-axis-label-width 0.01 + :labels-extra-height -36 + :x-axis-label-text-style style/x-axis-label-text-style}]]]))) diff --git a/src/quo/components/icon.cljs b/src/quo/components/icon.cljs new file mode 100644 index 00000000000..b82c21f4a91 --- /dev/null +++ b/src/quo/components/icon.cljs @@ -0,0 +1,56 @@ +(ns quo.components.icon + (:require + [clojure.string :as string] + [quo.components.icons.icons :as icons] + [quo.components.icons.svg :as icons.svg] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- valid-color? + [color] + (and color + (or (keyword? color) + (and (string? color) + (not (string/blank? color)))))) + +(defn- image-icon-style + [{:keys [color no-color size container-style]} theme] + (cond-> {:width size + :height size} + (not no-color) + (assoc :tint-color + (if (and (string? color) (not (string/blank? color))) + color + (colors/theme-colors colors/neutral-100 colors/white theme))) + :always + (merge container-style))) + +(def memo-icon-fn + (fn [{:keys [color color-2 container-style size accessibility-label] + :or {accessibility-label :icon} + :as props} + icon-name + theme] + (let [size (or size 20)] + (with-meta + (if-let [svg-icon (icons.svg/get-icon icon-name size)] + [svg-icon + (cond-> {:size size + :accessibility-label accessibility-label + :style container-style} + (valid-color? color) (assoc :color color) + (valid-color? color-2) (assoc :color-2 color-2))] + [rn/image + {:style (image-icon-style (assoc props :size size) theme) + :accessibility-label accessibility-label + :source (icons/icon-source (str (name icon-name) size))}]) + {:key icon-name})))) + +(def ^:private memoized-icon (memoize memo-icon-fn)) + +(defn icon + ([icon-name] (icon icon-name nil)) + ([icon-name params] + (let [theme (quo.context/use-theme)] + (memoized-icon params icon-name theme)))) diff --git a/src/quo/components/icons/icons.clj b/src/quo/components/icons/icons.clj new file mode 100644 index 00000000000..65a722dcddf --- /dev/null +++ b/src/quo/components/icons/icons.clj @@ -0,0 +1,40 @@ +(ns quo.components.icons.icons + (:require + [clojure.java.io :as io] + [clojure.string :as string])) + +(def ^:private icon-path "./resources/images/icons2/") + +(defn- require-icon + [size path] + (fn [el] + (let [s (str "." path el ".png") + k (-> el + (string/replace "_" "-") + (string/replace " " "-") + (string/lower-case) + (str size))] + [k `(js/require ~s)]))) + +(defn- get-files + [path] + (->> (io/file path) + file-seq + (filter #(string/ends-with? % "png")) + (map #(first (string/split (.getName %) #"@"))) + distinct)) + +(defn- get-icons + [size] + (let [path (str icon-path size "x" size "/")] + (into {} (map (require-icon size path) (get-files path))))) + +(defmacro resolve-icons + [] + (merge + (get-icons 12) + (get-icons 16) + (get-icons 20) + (get-icons 24) + (get-icons 32) + (get-icons 48))) diff --git a/src/quo/components/icons/icons.cljs b/src/quo/components/icons/icons.cljs new file mode 100644 index 00000000000..8164ee0ecfd --- /dev/null +++ b/src/quo/components/icons/icons.cljs @@ -0,0 +1,14 @@ +(ns quo.components.icons.icons + (:require-macros [quo.components.icons.icons :as icons]) + (:require + [taoensso.timbre :as log])) + +(def ^:private icons (icons/resolve-icons)) + +(defn icon-source + [icon] + (if-let [icon (get icons (name icon))] + icon + (do + (log/error "could not find source for " icon " icon") + nil))) diff --git a/src/quo/components/icons/svg.cljs b/src/quo/components/icons/svg.cljs new file mode 100644 index 00000000000..9cdfc46f716 --- /dev/null +++ b/src/quo/components/icons/svg.cljs @@ -0,0 +1,185 @@ +(ns quo.components.icons.svg + "Declare icons in this namespace when they have two possible colors, because the + ReactNative `:tint-color` prop affects all non-transparent pixels of PNGs. If + the icon has only one color, prefer a PNG. + + Keep all SVG components private and expose them by name in the `icons` var." + (:require + [quo.foundations.colors :as colors] + [react-native.svg :as svg])) + +(defn- container + [{:keys [size accessibility-label style] + :or {size 20}} + & children] + (into [svg/svg + {:accessibility-label accessibility-label + :style style + :width size + :height size + :view-box (str "0 0 " size " " size) + :fill :none}] + children)) + +(defn- clear-20 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/path + {:d + "M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10Z" + :fill color}] + [svg/path + {:d + "M9.15142 9.99998L7.07566 12.0757L7.9242 12.9243L9.99994 10.8485L12.0757 12.9242L12.9242 12.0757L10.8485 9.99998L12.9242 7.92421L12.0757 7.07568L9.99994 9.15145L7.92421 7.07572L7.07568 7.92425L9.15142 9.99998Z" + :fill color-2}]]) + +(defn- dropdown-20 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/path + {:d + "M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10Z" + :fill color}] + [svg/path + {:d + "M7 8.5L10 11.5L13 8.5" + :stroke color-2 + :stroke-width 1.2}]]) + +(defn- pullup-20 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/path + {:d + "M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10Z" + :fill color}] + [svg/path + {:d + "M7 11.5L10 8.5L13 11.5" + :stroke color-2 + :stroke-width 1.2}]]) + +(defn- dropdown-12 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/circle + {:cx 6 + :cy 6 + :r 6 + :fill color}] + [svg/path + {:d + "M3.5 5L6 7.5L8.5 5" + :stroke color-2 + :stroke-width 1.1}]]) + +(defn- pullup-12 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/circle + {:cx 6 + :cy 6 + :r 6 + :fill color}] + [svg/path + {:d + "M3.5 7L6 4.5L8.5 7" + :stroke color-2 + :stroke-width 1.1}]]) + +(defn- contact-12 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/circle + {:cx 6 + :cy 6 + :r 6 + :fill color}] + [svg/path + {:d + "M2.79504 9.01494C2.89473 8.85584 3.00297 8.70977 3.11963 8.5761C3.90594 7.67512 4.97212 7.45024 5.99992 7.45024C7.02772 7.45024 8.0939 7.67512 8.88021 8.5761C8.99688 8.70978 9.10512 8.85585 9.2048 9.01495C8.9501 9.28561 8.66149 9.52401 8.34577 9.72335C8.25403 9.55824 8.15512 9.41818 8.05145 9.29939C7.56503 8.74204 6.88121 8.55024 5.99992 8.55024C5.11862 8.55024 4.43481 8.74204 3.94839 9.29939C3.84472 9.41818 3.74582 9.55824 3.65408 9.72334C3.33835 9.524 3.04975 9.2856 2.79504 9.01494ZM5.99992 3.5502C5.47525 3.5502 5.04992 3.97552 5.04992 4.5002C5.04992 5.02487 5.47525 5.4502 5.99992 5.4502C6.52459 5.4502 6.94992 5.02487 6.94992 4.5002C6.94992 3.97552 6.52459 3.5502 5.99992 3.5502ZM3.94992 4.5002C3.94992 3.36801 4.86773 2.4502 5.99992 2.4502C7.1321 2.4502 8.04992 3.36801 8.04992 4.5002C8.04992 5.63238 7.1321 6.5502 5.99992 6.5502C4.86773 6.5502 3.94992 5.63238 3.94992 4.5002Z" + :fill color-2}]]) + +(defn- contact-20 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/circle + {:cx 10 + :cy 10 + :r 7.5 + :fill color}] + [svg/path + {:d + "M8.65002 7.99998C8.65002 7.25439 9.25443 6.64998 10 6.64998C10.7456 6.64998 11.35 7.25439 11.35 7.99998C11.35 8.74556 10.7456 9.34998 10 9.34998C9.25443 9.34998 8.65002 8.74556 8.65002 7.99998ZM10 5.34998C8.53646 5.34998 7.35002 6.53642 7.35002 7.99998C7.35002 9.46353 8.53646 10.65 10 10.65C11.4636 10.65 12.65 9.46353 12.65 7.99998C12.65 6.53642 11.4636 5.34998 10 5.34998ZM10 13.65C8.67557 13.65 7.53064 14.4186 6.98692 15.5341C6.60094 15.3235 6.23942 15.0737 5.90771 14.79C6.69408 13.3369 8.23179 12.35 10 12.35C11.7682 12.35 13.306 13.3369 14.0923 14.79C13.7606 15.0737 13.3991 15.3235 13.0131 15.5341C12.4694 14.4186 11.3245 13.65 10 13.65Z" + :fill color-2}]]) + +(defn- verified-20 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/path + {:d + "M18 10C18 8.77003 17.1118 7.74751 15.9418 7.53891C16.6216 6.56407 16.5267 5.21294 15.6569 4.34321C14.7872 3.47344 13.436 3.37852 12.4611 4.05845C12.2526 2.88834 11.23 2 10 2C8.77 2 7.74747 2.88827 7.53889 4.05832C6.56406 3.37846 5.21292 3.47339 4.34318 4.34313C3.47343 5.21288 3.3785 6.56404 4.05839 7.53888C2.88831 7.74743 2 8.76998 2 10C2 11.23 2.88824 12.2525 4.05826 12.4611C3.3784 13.4359 3.47333 14.7871 4.34307 15.6568C5.21284 16.5266 6.56403 16.6215 7.53887 15.9416C7.74741 17.1117 8.76996 18 10 18C11.23 18 12.2525 17.1117 12.4611 15.9417C13.4359 16.6215 14.7871 16.5266 15.6568 15.6569C16.5265 14.7871 16.6215 13.436 15.9416 12.4611C17.1117 12.2526 18 11.23 18 10Z" + :fill color}] + [svg/path + {:d "M7.25 10.75L9.25 12.25L12.75 7.75" + :stroke color-2 + :stroke-width "1.2"}]]) + +(defn- untrustworthy-20 + [{:keys [color color-2] + :or {color colors/neutral-100 + color-2 colors/white} + :as props}] + [container props + [svg/path + {:d "M10 2L15.6569 4.34315L18 10L15.6569 15.6569L10 18L4.34315 15.6569L2 10L4.34315 4.34315L10 2Z" + :fill color}] + [svg/path + {:d + "M10.75 5.5L10.55 11.5H9.45L9.25 5.5H10.75ZM10 13C10.4142 13 10.75 13.3358 10.75 13.75C10.75 14.1642 10.4142 14.5 10 14.5C9.58579 14.5 9.25 14.1642 9.25 13.75C9.25 13.3358 9.58579 13 10 13Z" + :fill color-2}]]) + +(def ^:private icons + {:i/clear-20 clear-20 + :i/dropdown-20 dropdown-20 + :i/pullup-20 pullup-20 + :i/dropdown-12 dropdown-12 + :i/pullup-12 pullup-12 + :i/contact-12 contact-12 + :i/contact-20 contact-20 + :i/verified-20 verified-20 + :i/untrustworthy-20 untrustworthy-20}) + +(defn- append-to-keyword + [k & xs] + (keyword (apply str + (subs (str k) 1) + xs))) + +(defn get-icon + [icon-name size] + (get icons (append-to-keyword icon-name "-" size))) diff --git a/src/quo/components/info/info_message/component_spec.cljs b/src/quo/components/info/info_message/component_spec.cljs new file mode 100644 index 00000000000..3d2b8912e3e --- /dev/null +++ b/src/quo/components/info/info_message/component_spec.cljs @@ -0,0 +1,32 @@ +(ns quo.components.info.info-message.component-spec + (:require + [quo.components.info.info-message.view :as info-message] + [test-helpers.component :as h])) + +(h/describe "Info: Info Message" + (h/test "basic render" + (h/render-with-theme-provider + [info-message/view + {:status :default + :size :default + :accessibility-label :info-message + :icon :i/placeholder} "Message"]) + (h/is-truthy (h/get-by-label-text :info-message))) + + (h/test "render with correct message" + (h/render-with-theme-provider + [info-message/view + {:status :default + :size :default + :accessibility-label :info-message + :icon :i/placeholder} "Message"]) + (h/is-truthy (h/get-by-text "Message"))) + + (h/test "render icon" + (h/render-with-theme-provider + [info-message/view + {:status :default + :size :default + :accessibility-label :info-message + :icon :i/placeholder} "Message"]) + (h/is-truthy (h/get-by-label-text :icon)))) diff --git a/src/quo/components/info/info_message/style.cljs b/src/quo/components/info/info_message/style.cljs new file mode 100644 index 00000000000..40535061e3e --- /dev/null +++ b/src/quo/components/info/info_message/style.cljs @@ -0,0 +1,6 @@ +(ns quo.components.info.info-message.style) + +(def container + {:flex-direction :row + :gap 4 + :align-items :center}) diff --git a/src/quo/components/info/info_message/view.cljs b/src/quo/components/info/info_message/view.cljs new file mode 100644 index 00000000000..3642555de3b --- /dev/null +++ b/src/quo/components/info/info_message/view.cljs @@ -0,0 +1,53 @@ +(ns quo.components.info.info-message.view + (:require [quo.components.icon :as icons] + [quo.components.info.info-message.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:status {:optional true} [:maybe [:enum :default :success :error :warning]]] + [:size {:optional true} [:maybe [:enum :default :tiny]]] + [:blur? {:optional true} [:maybe :boolean]] + [:icon :keyword] + [:color {:optional true} [:maybe :string]] + [:text-color {:optional true} [:maybe :string]] + [:icon-color {:optional true} [:maybe :string]] + [:no-icon-color? {:optional true} [:maybe :boolean]] + [:accessibility-label {:optional true} [:maybe :keyword]] + [:container-style {:optional true} [:maybe :map]]]] + [:message :string]] + :any]) + +(defn- get-color + [status theme blur?] + (case status + :success (if blur? colors/success-60 (colors/resolve-color :success theme)) + :error (if blur? colors/danger-60 (colors/resolve-color :danger theme)) + :warning (if blur? colors/warning-60 (colors/resolve-color :warning theme)) + (if blur? colors/white-opa-40 (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)))) + +(defn view-internal + [{:keys [status size blur? icon color icon-color text-color no-icon-color? container-style + accessibility-label]} message] + (let [theme (quo.context/use-theme) + default-color (get-color status theme blur?) + text-color (or text-color color default-color) + icon-color (or icon-color color default-color)] + [rn/view {:style (merge style/container container-style)} + [icons/icon icon + {:size (if (= size :tiny) 12 16) + :color icon-color + :no-color? no-icon-color?}] + [text/text + {:size (if (= size :tiny) :label :paragraph-2) + :accessibility-label accessibility-label + :style {:color text-color}} message]])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/info/information_box/component_spec.cljs b/src/quo/components/info/information_box/component_spec.cljs new file mode 100644 index 00000000000..d87fcea2efc --- /dev/null +++ b/src/quo/components/info/information_box/component_spec.cljs @@ -0,0 +1,31 @@ +(ns quo.components.info.information-box.component-spec + (:require + [quo.components.info.information-box.view :as view] + [test-helpers.component :as h])) + +(h/describe "Info - Information Box" + (h/test "default render" + (h/render [view/view {:icon :i/placeholder} + "Lorem ipsum"]) + (h/is-null (h/query-by-label-text :information-box-action-button)) + (h/is-null (h/query-by-label-text :information-box-close-button))) + + (h/test "with close button" + (let [on-close (h/mock-fn)] + (h/render [view/view + {:icon :i/placeholder + :on-close on-close} + "Lorem ipsum"]) + (h/is-null (h/query-by-label-text :information-box-action-button)) + (h/fire-event :on-press (h/get-by-label-text :information-box-close-button)) + (h/was-called on-close))) + + (h/test "with button" + (let [on-press (h/mock-fn)] + (h/render [view/view + {:icon :i/placeholder + :button-label "Press me" + :on-button-press on-press} + "Lorem ipsum"]) + (h/fire-event :on-press (h/get-by-label-text :information-box-action-button)) + (h/was-called on-press)))) diff --git a/src/quo/components/info/information_box/style.cljs b/src/quo/components/info/information_box/style.cljs new file mode 100644 index 00000000000..327af81fd5c --- /dev/null +++ b/src/quo/components/info/information_box/style.cljs @@ -0,0 +1,80 @@ +(ns quo.components.info.information-box.style + (:require + [quo.foundations.colors :as colors])) + +(defn get-colors-map + [customization-color-hex] + {:light {:default {:bg colors/neutral-5 + :border colors/neutral-20 + :icon colors/neutral-50 + :text colors/neutral-100} + :informative {:bg (colors/alpha customization-color-hex 0.05) + :border (colors/alpha customization-color-hex 0.1) + :icon customization-color-hex + :text colors/neutral-100} + :error {:bg colors/danger-50-opa-5 + :border colors/danger-50-opa-10 + :icon colors/danger-50 + :text colors/danger-50} + :close-button colors/neutral-100} + :dark {:default {:bg colors/neutral-80-opa-40 + :border colors/neutral-80 + :icon colors/neutral-40 + :text colors/white} + :informative {:bg (colors/alpha customization-color-hex 0.05) + :border (colors/alpha customization-color-hex 0.1) + :icon customization-color-hex + :text colors/white} + :error {:bg colors/danger-60-opa-5 + :border colors/danger-60-opa-10 + :icon colors/danger-60 + :text colors/danger-60} + :close-button colors/white} + :shell {:default {:bg colors/white-opa-5 + :border colors/white-opa-10 + :icon colors/white-opa-70 + :text colors/white} + :informative {:bg (colors/alpha customization-color-hex 0.05) + :border (colors/alpha customization-color-hex 0.1) + :icon customization-color-hex + :text colors/white} + :error {:bg colors/danger-60-opa-5 + :border colors/danger-60-opa-10 + :icon colors/danger-60 + :text colors/danger-60} + :close-button colors/white}}) + +(defn get-color + [colors-map theme k] + (get-in colors-map [theme k])) + +(defn get-color-by-type + [colors-map theme type k] + (get-in colors-map [theme type k])) + +(defn container + [{:keys [theme type include-button? colors-map]}] + {:background-color (get-color-by-type colors-map theme type :bg) + :border-color (get-color-by-type colors-map theme type :border) + :border-width 1 + :border-radius 12 + :padding-top (if include-button? 10 11) + :padding-bottom (if include-button? 12 11) + :flex-direction :row + :padding-horizontal 12}) + +(def icon + {:margin-top 1 :margin-right 8}) + +(def close-button + {:margin-top 4 + :margin-left 8}) + +(defn content-text + [theme type colors-map] + {:color (get-color-by-type colors-map theme type :text) + :margin-right 8}) + +(def content-button + {:margin-top 8 + :align-self :flex-start}) diff --git a/src/quo/components/info/information_box/view.cljs b/src/quo/components/info/information_box/view.cljs new file mode 100644 index 00000000000..50344d73237 --- /dev/null +++ b/src/quo/components/info/information_box/view.cljs @@ -0,0 +1,90 @@ +(ns quo.components.info.information-box.view + (:require + [clojure.string :as string] + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icons] + [quo.components.info.information-box.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- info-type->button-type + [type] + (if (= type :error) + :danger + :primary)) + +(defn- close-button + [{:keys [theme on-close colors-map]}] + [rn/touchable-opacity + {:on-press on-close + :hit-slop {:top 3 :right 3 :bottom 3 :left 3} + :accessibility-label :information-box-close-button} + [icons/icon :i/close + {:size 12 + :color (style/get-color colors-map theme :close-button) + :container-style style/close-button}]]) + +(defn- content + [{:keys [theme type button-label on-button-press message colors-map customization-color]}] + [rn/view {:style {:flex 1}} + [text/text + {:size :paragraph-2 + :style (style/content-text colors-map theme type)} + message] + (when (not (string/blank? button-label)) + [button/button + {:type (info-type->button-type type) + :accessibility-label :information-box-action-button + :size 24 + :customization-color customization-color + :on-press on-button-press + :container-style style/content-button} + button-label])]) + +(defn view + "[view opts \"message\"] + opts + {:type :default/:informative/:error + :closed? bool (false) ;; Information box's state + :icon keyword, required (:i/info) + :icon-size int (16) + :no-icon-color? bool (false) + :style map + :button-label string + :on-button-press function + :on-close function" + [{:keys [type closed? blur? icon style button-label + on-button-press on-close no-icon-color? icon-size customization-color] + :or {customization-color :primary}} + message] + (when-not closed? + (let [quo-theme (quo.context/use-theme) + theme (if blur? :shell quo-theme) + customization-color-hex (colors/resolve-color customization-color theme) + colors-map (style/get-colors-map customization-color-hex) + include-button? (not (string/blank? button-label))] + [rn/view + {:accessibility-label :information-box + :style (merge (style/container {:theme theme + :colors-map colors-map + :customization-color customization-color + :type type + :include-button? include-button?}) + style)} + [icons/icon (or icon :i/info) + {:color (style/get-color-by-type colors-map theme type :icon) + :no-color no-icon-color? + :size (or icon-size 16) + :container-style style/icon}] + [content + {:theme theme + :type type + :button-label button-label + :on-button-press on-button-press + :colors-map colors-map + :customization-color customization-color + :message message}] + (when on-close + [close-button {:theme theme :colors-map colors-map :on-close on-close}])]))) diff --git a/src/quo/components/inputs/address_input/component_spec.cljs b/src/quo/components/inputs/address_input/component_spec.cljs new file mode 100644 index 00000000000..a303b2556d0 --- /dev/null +++ b/src/quo/components/inputs/address_input/component_spec.cljs @@ -0,0 +1,205 @@ +(ns quo.components.inputs.address-input.component-spec + (:require + [quo.components.inputs.address-input.view :as address-input] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [test-helpers.component :as h])) + +(def ens-regex #"^(?=.{5,255}$)([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$") +(def address-regex #"^0x[a-fA-F0-9]{40}$") + +(h/describe "Address input" + (h/test "default render" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input]) + (h/is-truthy (h/get-by-label-text :address-text-input)))) + + (h/test "on focus with blur? false" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input {:ens-regex ens-regex}]) + (h/fire-event :on-focus (h/get-by-label-text :address-text-input)) + (h/has-prop (h/get-by-label-text :address-text-input) + :placeholder-text-color + colors/neutral-30))) + + (h/test "on focus with blur? true" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:blur? true + :ens-regex ens-regex}]) + (h/fire-event :on-focus (h/get-by-label-text :address-text-input)) + (h/has-prop (h/get-by-label-text :address-text-input) + :placeholder-text-color + colors/neutral-80-opa-20))) + + (h/test "default value is properly set" + (let [default-value "default-value"] + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:default-value default-value + :ens-regex ens-regex}]) + (h/has-prop (h/get-by-label-text :address-text-input) + :value + default-value)))) + + (h/test "clear icon is shown when input has text" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:default-value "default value" + :ens-regex ens-regex}]) + (-> (h/wait-for #(h/get-by-label-text :clear-button-container)) + (.then #(h/is-truthy (h/get-by-label-text :clear-button)))))) + + (h/test "on blur with text and blur? false" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:default-value "default value" + :ens-regex ens-regex}]) + (-> (h/wait-for #(h/get-by-label-text :clear-button)) + (.then (fn [] + (h/fire-event :on-focus (h/get-by-label-text :address-text-input)) + (h/fire-event :on-blur (h/get-by-label-text :address-text-input)) + (h/has-prop (h/get-by-label-text :address-text-input) + :placeholder-text-color + colors/neutral-40)))))) + + (h/test "on blur with text blur? true" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:default-value "default value" + :blur? true + :ens-regex ens-regex}]) + (-> (h/wait-for #(h/get-by-label-text :clear-button)) + (.then (fn [] + (h/fire-event :on-focus (h/get-by-label-text :address-text-input)) + (h/fire-event :on-blur (h/get-by-label-text :address-text-input)) + (h/has-prop (h/get-by-label-text :address-text-input) + :placeholder-text-color + colors/neutral-80-opa-40)))))) + + (h/test "on blur with no text and blur? false" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input {:ens-regex ens-regex}]) + (h/fire-event :on-focus (h/get-by-label-text :address-text-input)) + (h/fire-event :on-blur (h/get-by-label-text :address-text-input)) + (h/has-prop (h/get-by-label-text :address-text-input) + :placeholder-text-color + colors/neutral-40))) + + (h/test "on blur with no text blur? true" + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:blur? true + :ens-regex ens-regex}]) + (h/fire-event :on-focus (h/get-by-label-text :address-text-input)) + (h/fire-event :on-blur (h/get-by-label-text :address-text-input)) + (h/has-prop (h/get-by-label-text :address-text-input) + :placeholder-text-color + colors/neutral-80-opa-40))) + + (h/test "on-clear is called" + (let [on-clear (h/mock-fn)] + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:default-value "default value" + :on-clear on-clear + :ens-regex ens-regex}]) + (-> (h/wait-for #(h/get-by-label-text :clear-button)) + (.then (fn [] + (h/fire-event :press (h/get-by-label-text :clear-button)) + (h/was-called on-clear))))))) + + (h/test "on-focus is called" + (let [on-focus (h/mock-fn)] + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input {:on-focus on-focus}]) + (h/fire-event :on-focus (h/get-by-label-text :address-text-input)) + (h/was-called on-focus)))) + + (h/test "on-blur is called" + (let [on-blur (h/mock-fn)] + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input + {:on-blur on-blur + :ens-regex ens-regex}]) + (h/fire-event :on-blur (h/get-by-label-text :address-text-input)) + (h/was-called on-blur)))) + + (h/test "on-scan is called" + (let [on-scan (h/mock-fn)] + (with-redefs [clipboard/get-string #(% "")] + (h/render [address-input/address-input {:on-scan on-scan}]) + (-> (h/wait-for #(h/get-by-label-text :scan-button)) + (.then (fn [] + (h/fire-event :press (h/get-by-label-text :scan-button)) + (h/was-called on-scan))))))) + + (h/test "paste from clipboard" + (let [clipboard "clipboard"] + (with-redefs [clipboard/get-string #(% clipboard)] + (h/render [address-input/address-input {:ens-regex ens-regex}]) + (h/is-truthy (h/query-by-label-text :paste-button)) + (h/fire-event :press (h/get-by-label-text :paste-button)) + (-> (h/wait-for #(h/get-by-label-text :clear-button)) + (.then (fn [] + (h/has-prop (h/get-by-label-text :address-text-input) + :value + clipboard))))))) + + (h/test "ENS loading state and call on-detect-ens" + (let [clipboard "test.eth" + on-detect-ens (h/mock-fn)] + (with-redefs [clipboard/get-string #(% clipboard)] + (h/render [address-input/address-input + {:on-detect-ens on-detect-ens + :ens-regex ens-regex}]) + (h/is-truthy (h/query-by-label-text :paste-button)) + (h/fire-event :press (h/get-by-label-text :paste-button)) + (-> (h/wait-for #(h/is-falsy (h/query-by-label-text :clear-button))) + (.then (fn [] + (h/wait-for #(h/get-by-label-text :loading-button-container)))) + (.then #(h/was-called on-detect-ens)))))) + + (h/test "ENS valid state and call on-detect-ens" + (let [clipboard "test.eth" + on-detect-ens (h/mock-fn)] + (with-redefs [clipboard/get-string #(% clipboard)] + (h/render [address-input/address-input + {:on-detect-ens on-detect-ens + :valid-ens-or-address? true + :ens-regex ens-regex}]) + (h/is-truthy (h/query-by-label-text :paste-button)) + (h/fire-event :press (h/get-by-label-text :paste-button)) + (-> (h/wait-for #(h/is-falsy (h/query-by-label-text :clear-button))) + (.then (fn [] + (h/wait-for #(h/get-by-label-text :positive-button-container)))) + (.then #(h/was-called on-detect-ens)))))) + + (h/test "address loading state and call on-detect-address" + (let [clipboard "0x2f88d65f3cb52605a54a833ae118fb1363acccd2" + on-detect-address (h/mock-fn)] + (with-redefs [clipboard/get-string #(% clipboard)] + (h/render [address-input/address-input + {:on-detect-address on-detect-address + :address-regex address-regex}]) + (h/is-truthy (h/query-by-label-text :paste-button)) + (h/fire-event :press (h/get-by-label-text :paste-button)) + (-> (h/wait-for #(h/is-falsy (h/query-by-label-text :clear-button))) + (.then (fn [] + (h/wait-for #(h/get-by-label-text :loading-button-container)))) + (.then #(h/was-called on-detect-address)))))) + + (h/test "address valid state and call on-detect-address" + (let [clipboard "0x2f88d65f3cb52605a54a833ae118fb1363acccd2" + on-detect-address (h/mock-fn)] + (with-redefs [clipboard/get-string #(% clipboard)] + (h/render [address-input/address-input + {:on-detect-address on-detect-address + :address-regex address-regex + :valid-ens-or-address? true}]) + (h/is-truthy (h/query-by-label-text :paste-button)) + (h/fire-event :press (h/get-by-label-text :paste-button)) + (-> (h/wait-for #(h/is-falsy (h/query-by-label-text :clear-button))) + (.then (fn [] + (h/wait-for #(h/get-by-label-text :positive-button-container)))) + (.then #(h/was-called on-detect-address))))))) diff --git a/src/quo/components/inputs/address_input/style.cljs b/src/quo/components/inputs/address_input/style.cljs new file mode 100644 index 00000000000..1f716b1073d --- /dev/null +++ b/src/quo/components/inputs/address_input/style.cljs @@ -0,0 +1,43 @@ +(ns quo.components.inputs.address-input.style + (:require + [quo.components.markdown.text :as text] + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn container + [container-style] + (merge {:padding-horizontal 20 + :padding-top 8 + :padding-bottom 16 + :flex-direction :row + :align-items :flex-start} + container-style)) + +(def buttons-container + {:flex-direction :row + :align-items :flex-start}) + +(def clear-icon-container + {:justify-content :flex-start + :align-items :center + :padding-top (when platform/ios? 4) + :height 24 + :width 20}) + +(defn input-text + [theme] + (assoc (text/text-style {:size :paragraph-1 + :weight :monospace} + nil) + :flex 1 + :color (colors/theme-colors colors/neutral-100 colors/white theme) + :margin-top -4 + :margin-right 8 + :max-height 50 + :padding 0)) + +(defn accessory-button + [blur? theme] + {:border-color (if blur? + (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-30 colors/neutral-70 theme))}) diff --git a/src/quo/components/inputs/address_input/view.cljs b/src/quo/components/inputs/address_input/view.cljs new file mode 100644 index 00000000000..740826f9566 --- /dev/null +++ b/src/quo/components/inputs/address_input/view.cljs @@ -0,0 +1,178 @@ +(ns quo.components.inputs.address-input.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.inputs.address-input.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.platform :as platform] + [utils.i18n :as i18n])) + +(defn- icon-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme))) + +(defn- clear-button + [{:keys [on-press blur? theme]}] + [rn/touchable-opacity + {:accessibility-label :clear-button + :style style/clear-icon-container + :on-press on-press} + [icon/icon :i/clear + {:color (icon-color blur? theme) + :size 20}]]) + +(defn- loading-icon + [blur? theme] + [rn/view {:style style/clear-icon-container} + [icon/icon :i/loading + {:color (icon-color blur? theme) + :size 20}]]) + +(defn- positive-state-icon + [theme] + [rn/view {:style style/clear-icon-container} + [icon/icon :i/positive-state + {:color (colors/resolve-color :success theme) + :size 20}]]) + +(defn- get-placeholder-text-color + [status theme blur?] + (cond + (and (= status :default) blur?) + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-30 theme) + (and (= status :default) (not blur?)) + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme) + (and (not= status :default) blur?) + (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + (and (not= status :default) (not blur?)) + (colors/theme-colors colors/neutral-30 colors/neutral-60 theme))) + +(defn address-input + [{:keys [default-value blur? on-change-text on-blur on-focus on-clear on-scan on-paste + on-detect-ens on-detect-address on-detect-unclassified address-regex ens-regex + valid-ens-or-address? container-style]}] + (let [theme (quo.context/use-theme) + [status set-status] (rn/use-state :default) + [value set-value] (rn/use-state nil) + [focused? set-focused] (rn/use-state false) + on-change (rn/use-callback + (fn [text] + (let [address? (when address-regex + (boolean (re-matches address-regex text))) + ens? (when ens-regex + (boolean (re-matches ens-regex text)))] + (set-value text) + (if (> (count text) 0) + (set-status :typing) + (set-status :active)) + (when on-change-text + (on-change-text text)) + (when (and on-detect-ens ens?) + (set-status :loading) + (on-detect-ens text #(set-status :typing))) + (when (and address? on-detect-address) + (set-status :loading) + (on-detect-address text)) + (when (and (not address?) + (not ens?) + on-detect-unclassified) + (on-detect-unclassified text))))) + on-paste (rn/use-callback #(if (fn? on-paste) + (on-paste on-change) + (clipboard/get-string + (fn [clipboard] + (when-not (empty? clipboard) + (on-change clipboard))))) + [on-paste]) + on-clear (rn/use-callback + (fn [] + (on-change "") + (set-status (if focused? :active :default)) + (when on-change-text + (on-change-text "")) + (when on-clear + (on-clear))) + [focused?]) + on-scan (rn/use-callback #(when on-scan (on-scan on-change)) + [on-scan]) + on-focus (rn/use-callback + (fn [] + (when (= (count value) 0) + (set-status :active)) + (set-focused true) + (when on-focus (on-focus)))) + on-blur (rn/use-callback + (fn [] + (when (= status :active) + (set-status :default)) + (set-focused false) + (when on-blur (on-blur))) + [status]) + placeholder-text-color (rn/use-memo #(get-placeholder-text-color status theme blur?) + [status theme blur?])] + (rn/use-mount #(on-change (or default-value ""))) + [rn/view {:style (style/container container-style)} + [rn/text-input + {:accessibility-label :address-text-input + :style (style/input-text theme) + :placeholder (i18n/label :t/name-ens-or-address) + :placeholder-text-color placeholder-text-color + :value value + :auto-complete (when platform/ios? :off) + :auto-capitalize :none + :auto-correct false + :spell-check false + :multiline true + :keyboard-appearance theme + :on-focus on-focus + :on-blur on-blur + :on-change-text on-change}] + (when (empty? value) + [rn/view + {:style style/buttons-container + :accessibility-label :paste-scan-buttons-container} + [button/button + {:accessibility-label :paste-button + :type :outline + :size 24 + :container-style {:margin-right 8} + :inner-style (style/accessory-button blur? theme) + :on-press on-paste} + (i18n/label :t/paste)] + (when on-scan + [button/button + {:accessibility-label :scan-button + :icon-only? true + :type :outline + :size 24 + :inner-style (style/accessory-button blur? theme) + :on-press on-scan} + :main-icons/scan])]) + (when (or (= status :typing) + (and (= status :active) + (not-empty value))) + [rn/view + {:style style/buttons-container + :accessibility-label :clear-button-container} + [clear-button + {:on-press on-clear + :blur? blur? + :theme theme}]]) + (when (and (= status :loading) (not valid-ens-or-address?)) + [rn/view + {:style style/buttons-container + :accessibility-label :loading-button-container} + [loading-icon blur? theme]]) + (when (and (or (= status :loading) + (= status :default)) + valid-ens-or-address? + (not-empty value)) + [rn/view + {:style style/buttons-container + :accessibility-label :positive-button-container} + [positive-state-icon theme]])])) diff --git a/src/quo/components/inputs/input/component_spec.cljs b/src/quo/components/inputs/input/component_spec.cljs new file mode 100644 index 00000000000..502e4d05ee3 --- /dev/null +++ b/src/quo/components/inputs/input/component_spec.cljs @@ -0,0 +1,71 @@ +(ns quo.components.inputs.input.component-spec + (:require + [quo.components.inputs.input.view :as input] + [test-helpers.component :as h])) + +(h/describe "Input" + (h/test "default render" + (h/render [input/input]) + (h/is-truthy (h/query-by-label-text :input)) + (h/is-null (h/query-by-label-text :password-input))) + + (h/test "Password type" + (h/render [input/input {:type :password}]) + (h/is-truthy (h/query-by-label-text :password-input)) + (h/is-null (h/query-by-label-text :input))) + + (h/describe "Icon" + (h/test "Doesn't exist in base input" + (h/render [input/input]) + (h/is-null (h/query-by-label-text :input-icon))) + + (h/test "Renders" + (h/render [input/input {:icon-name :i/placeholder}]) + (h/is-truthy (h/get-by-label-text :input-icon)))) + + (h/describe "Right accessory" + (h/test "Doesn't exist in base input" + (h/render [input/input]) + (h/is-null (h/query-by-label-text :input-right-icon))) + + (h/test "Clear icon" + (h/render [input/input {:clearable? true}]) + (h/is-truthy (h/query-by-label-text :input-right-icon))) + + (h/test "Password icon" + (h/render [input/input {:type :password}]) + (h/is-truthy (h/query-by-label-text :input-right-icon)))) + + (h/describe "Button" + (h/test "Doesn't exist in base input" + (h/render [input/input]) + (h/is-null (h/query-by-label-text :input-button))) + + (h/test "Renders with given text and it's pressable" + (let [button-text "This is a button" + button-callback (h/mock-fn)] + (h/render [input/input + {:button {:on-press button-callback + :text button-text}}]) + (h/is-truthy (h/query-by-label-text :input-button)) + (h/is-truthy (h/get-by-text button-text)) + (h/fire-event :press (h/query-by-label-text :input-button)) + (h/was-called button-callback)))) + + (h/describe "Label" + (h/test "Doesn't exist in base input" + (h/render [input/input]) + (h/is-null (h/query-by-label-text :input-labels))) + + (h/test "Renders with specified text" + (let [input-label "My label"] + (h/render [input/input {:label input-label}]) + (h/is-truthy (h/query-by-label-text :input-labels)) + (h/is-truthy (h/get-by-text input-label))))) + + (h/test "Char limit counter" + (let [char-limit 100 + char-limit-str (str "0/" char-limit)] + (h/render [input/input {:char-limit char-limit}]) + (h/is-truthy (h/query-by-label-text :input-labels)) + (h/is-truthy (h/get-by-text char-limit-str))))) diff --git a/src/quo/components/inputs/input/style.cljs b/src/quo/components/inputs/input/style.cljs new file mode 100644 index 00000000000..42dd2bad4b7 --- /dev/null +++ b/src/quo/components/inputs/input/style.cljs @@ -0,0 +1,161 @@ +(ns quo.components.inputs.input.style + (:require + [quo.components.markdown.text :as text] + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn variants-colors + [blur? theme] + (if blur? + {:label (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :icon (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + :button-border (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 theme) + :password-icon (colors/theme-colors colors/neutral-100 colors/white-opa-70 theme) + :clear-icon (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 theme) + :cursor (colors/theme-colors (colors/resolve-color :blue :light) + colors/white + theme)} + {:label (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :icon (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :button-border (colors/theme-colors colors/neutral-30 colors/neutral-70 theme) + :clear-icon (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + :password-icon (colors/theme-colors colors/neutral-50 colors/white theme) + :cursor (colors/resolve-color :blue theme)})) + +(defn status-colors + [status blur? theme] + (if blur? + (case status + :focus + {:border-color (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-40 theme) + :placeholder (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + :text (colors/theme-colors colors/neutral-100 colors/white theme)} + :error + {:border-color colors/danger-50-opa-40 + :placeholder (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :text (colors/theme-colors colors/neutral-100 colors/white theme)} + :disabled + {:border-color (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :placeholder (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-20 theme) + :text (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-20 theme)} + ;; :default + {:border-color (colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme) + :placeholder (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + :text (colors/theme-colors colors/neutral-100 colors/white theme)}) + (case status + :focus + {:border-color (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + :placeholder (colors/theme-colors colors/neutral-30 colors/neutral-60 theme) + :text (colors/theme-colors colors/neutral-100 colors/white theme)} + :error + {:border-color colors/danger-50-opa-40 + :placeholder (colors/theme-colors colors/neutral-40 colors/white-opa-40 theme) + :text (colors/theme-colors colors/neutral-100 colors/white theme)} + :disabled + {:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :placeholder (colors/theme-colors colors/neutral-40 colors/neutral-40 theme) + :text (colors/theme-colors colors/neutral-40 colors/neutral-40 theme)} + ;; :default + {:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :placeholder (colors/theme-colors colors/neutral-40 colors/neutral-50 theme) + :text (colors/theme-colors colors/neutral-100 colors/white theme)}))) + +(defn input-container + [colors-by-status small? disabled?] + {:flex-direction :row + :padding-horizontal 8 + :border-width 1 + :border-color (:border-color colors-by-status) + :border-radius (if small? 10 12) + :opacity (if disabled? 0.3 1)}) + + +(defn left-icon-container + [small?] + {:margin-left (if small? 0 4) + :margin-top (if small? 5 9) + :height 20 + :width 20}) + +(defn icon + [colors-by-variant] + {:color (:icon colors-by-variant) + :size 20}) + +(defn input + [colors-by-status small? multiple-lines? weight] + (let [padding (if small? 4 8) + base-props (assoc (text/text-style {:size :paragraph-1 :weight (or weight :regular)} nil) + :flex 1 + :padding-right 0 + :padding-left padding + :padding-top padding + :padding-bottom padding + :color (:text colors-by-status))] + (if multiple-lines? + (assoc base-props + :text-align-vertical :top + :line-height 22) + (cond-> base-props + :always + (assoc :height (if small? 30 38) + :line-height nil) + platform/ios? + (assoc :padding-top (+ padding 2)))))) + +(defn right-icon-touchable-area + [small?] + {:margin-left (if small? 4 8) + :padding-right (if small? 0 4) + :padding-top (if small? 5 9)}) + +(defn password-icon + [variant-colors] + {:size 20 + :color (:password-icon variant-colors)}) + +(defn clear-icon + [variant-colors] + {:size 20 + :color (:clear-icon variant-colors) + :color-2 colors/white}) + +(def texts-container + {:flex-direction :row + :height 18 + :margin-bottom 8}) + +(def label-container {:flex 1}) + +(defn label-color + [variant-colors] + {:color (:label variant-colors)}) + +(def right-label-container + {:flex 1 + :align-items :flex-end}) + +(defn counter-color + [{:keys [current-chars char-limit variant-colors theme]}] + {:color (if (> current-chars char-limit) + (colors/resolve-color :danger theme) + (:label variant-colors))}) + +(defn button + [colors-by-variant small?] + {:justify-content :center + :align-items :center + :height 24 + :border-width 1 + :border-color (:button-border colors-by-variant) + :border-radius 8 + :margin-vertical (if small? 3 7) + :margin-left 4 + :margin-right (if small? -4 0) + :padding-horizontal 7 + :padding-top 1.5 + :padding-bottom 2.5}) + +(defn button-text + [colors-by-status] + {:color (:text colors-by-status)}) diff --git a/src/quo/components/inputs/input/view.cljs b/src/quo/components/inputs/input/view.cljs new file mode 100644 index 00000000000..5471be043af --- /dev/null +++ b/src/quo/components/inputs/input/view.cljs @@ -0,0 +1,228 @@ +(ns quo.components.inputs.input.view + (:require [oops.core :as oops] + [quo.components.icon :as icon] + [quo.components.inputs.input.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.platform :as platform])) + +(defn- label-line + [{:keys [label label-right current-chars char-limit variant-colors theme]}] + [rn/view + {:accessibility-label :input-labels + :style style/texts-container} + [rn/view {:style style/label-container} + [text/text + {:style (style/label-color variant-colors) + :weight :medium + :size :paragraph-2} + label]] + (when label-right + [rn/view {:style style/right-label-container} + [text/text + {:style (style/label-color variant-colors) + :weight :regular + :size :paragraph-2} + label-right]]) + (when char-limit + (let [count-text (str current-chars "/" char-limit)] + [rn/view {:style style/right-label-container} + [text/text + {:style (style/counter-color {:current-chars current-chars + :char-limit char-limit + :variant-colors variant-colors + :theme theme}) + :weight :regular + :size :paragraph-2} + count-text]]))]) + +(defn- left-accessory + [{:keys [variant-colors small? icon-name]}] + [rn/view + {:accessibility-label :input-icon + :style (style/left-icon-container small?)} + [icon/icon icon-name (style/icon variant-colors)]]) + +(defn- right-accessory + [{:keys [variant-colors small? disabled? on-press icon-style-fn icon-name]}] + [rn/touchable-opacity + {:accessibility-label :input-right-icon + :style (style/right-icon-touchable-area small?) + :disabled disabled? + :on-press on-press} + [icon/icon icon-name (icon-style-fn variant-colors)]]) + +(defn- right-button + [{:keys [variant-colors colors-by-status small? disabled? on-press text]}] + [rn/touchable-opacity + {:accessibility-label :input-button + :style (style/button variant-colors small?) + :disabled disabled? + :on-press on-press} + [rn/text {:style (style/button-text colors-by-status)} + text]]) + +(def ^:private custom-props + "Custom properties that must be removed from properties map passed to InputText." + [:type :blur? :error? :right-icon :left-icon :disabled? :small? :button + :label :char-limit :on-char-limit-reach :icon-name :multiline? :on-focus :on-blur + :container-style :input-container-style :ref :placeholder]) + +(defn- base-input + [{:keys [blur? error? right-icon left-icon disabled? small? button + label char-limit multiline? clearable? on-focus on-blur container-style input-container-style + on-change-text on-char-limit-reach weight default-value on-clear placeholder label-right] + :as props}] + (let [theme (quo.context/use-theme) + ref (rn/use-ref-atom nil) + on-ref (rn/use-callback (fn [value] + (when (:ref props) + ((:ref props) value)) + (reset! ref value))) + [status set-status] (rn/use-state :default) + internal-on-focus (rn/use-callback #(set-status :focus)) + internal-on-blur (rn/use-callback #(set-status :default)) + [multiple-lines? + set-multiple-lines] (rn/use-state false) + on-content-size-change (rn/use-callback + (fn [event] + (let [height (oops/oget event "nativeEvent.contentSize.height") + ;; In Android height comes with padding + min-height (if platform/android? 40 22)] + (if (> height min-height) + (set-multiple-lines true) + (set-multiple-lines false))))) + [char-count + set-char-count] (rn/use-state (count default-value)) + on-change-text (rn/use-callback + (fn [new-text] + (when on-change-text (on-change-text new-text)) + (let [amount-chars (count new-text)] + (set-char-count amount-chars) + (when (and (>= amount-chars char-limit) on-char-limit-reach) + (on-char-limit-reach amount-chars))))) + status-kw (cond + disabled? :disabled + error? :error + :else status) + colors-by-status (style/status-colors status-kw blur? theme) + variant-colors (style/variants-colors blur? theme) + clear-on-press (rn/use-callback (fn [] + (.clear ^js @ref) + (when on-clear (on-clear))) + [on-clear]) + right-icon (or right-icon + (when clearable? + {:style-fn style/clear-icon + :icon-name :i/clear + :on-press clear-on-press})) + clean-props (apply dissoc props custom-props) + ;; Workaround for android cursor overlapping placeholder + ;; https://github.com/facebook/react-native/issues/27687 + modified-placeholder (if platform/android? (str "\u2009" placeholder) placeholder)] + [rn/view {:style container-style} + (when (or label char-limit label-right) + [label-line + {:variant-colors variant-colors + :label label + :label-right label-right + :current-chars char-count + :char-limit char-limit + :theme theme}]) + [rn/view + {:style [(style/input-container colors-by-status small? disabled?) input-container-style]} + (when-let [{:keys [icon-name]} left-icon] + [left-accessory + {:variant-colors variant-colors + :small? small? + :icon-name icon-name}]) + [rn/text-input + (cond-> {:ref on-ref + :style (style/input colors-by-status small? multiple-lines? weight) + :accessibility-label :input + :placeholder-text-color (:placeholder colors-by-status) + :keyboard-appearance theme + :cursor-color (:cursor variant-colors) + :editable (not disabled?) + :placeholder modified-placeholder + :on-focus (fn [] + (when on-focus (on-focus)) + (internal-on-focus)) + :on-blur (fn [] + (when on-blur (on-blur)) + (internal-on-blur))} + :always (merge clean-props) + multiline? (assoc :multiline true + :on-content-size-change on-content-size-change) + char-limit (assoc :on-change-text on-change-text))] + (when-let [{:keys [on-press icon-name style-fn]} right-icon] + [right-accessory + {:variant-colors variant-colors + :small? small? + :disabled? disabled? + :icon-style-fn style-fn + :icon-name icon-name + :on-press (fn [] + (when clearable? (set-char-count 0)) + (on-press))}]) + (when-let [{:keys [on-press text]} button] + [right-button + {:colors-by-status colors-by-status + :variant-colors variant-colors + :small? small? + :disabled? disabled? + :on-press on-press + :text text}])]])) + +(defn- password-input + [{:keys [default-shown?] + :or {default-shown? false} + :as props}] + (let [[password-shown? + set-password-shown] (rn/use-state default-shown?) + on-press (rn/use-callback #(set-password-shown (not password-shown?)) + [password-shown?])] + [base-input + (assoc props + :accessibility-label :password-input + :auto-capitalize :none + :auto-complete :password + :secure-text-entry (not password-shown?) + :right-icon {:style-fn style/password-icon + :icon-name (if password-shown? :i/hide-password :i/reveal) + :on-press on-press})])) + +(defn input + "This input supports the following properties: + - :type - Can be `:text`(default) or `:password`. + - :blur? - Boolean to set the blur color variant. + - :theme - Can be `light` or `:dark`. + - :small? - Boolean to specify if this input is rendered in its small version. + - :multiline? - Boolean to specify if this input support multiple lines. + - :icon-name - The name of an icon to display at the left of the input. + - :error? - Boolean to specify it this input marks an error. + - :disabled? - Boolean to specify if this input is disabled or not. + - :clearable? - Boolean to specify if this input has a clear button at the end. + - :on-clear - Function executed when the clear button is pressed. + - :button - Map containing `:on-press` & `:text` keys, if provided renders a button + - :label - A string to set as label for this input. + - :label-right - Additional label aligned to the right + - :char-limit - A number to set a maximum char limit for this input. + - :on-char-limit-reach - Function executed each time char limit is reached or exceeded. + - :default-shown? - boolean to show password input initially + and supports the usual React Native's TextInput properties to control its behaviour: + - :value + - :default-value + - :on-change + - :on-change-text + ... + " + [{:keys [type icon-name] + :or {type :text} + :as props}] + (let [base-props (cond-> props + icon-name (assoc-in [:left-icon :icon-name] icon-name))] + (if (= type :password) + [password-input base-props] + [base-input base-props]))) diff --git a/src/quo/components/inputs/locked_input/component_spec.cljs b/src/quo/components/inputs/locked_input/component_spec.cljs new file mode 100644 index 00000000000..4dca11b1832 --- /dev/null +++ b/src/quo/components/inputs/locked_input/component_spec.cljs @@ -0,0 +1,25 @@ +(ns quo.components.inputs.locked-input.component-spec + (:require + [quo.components.inputs.locked-input.view :as locked-input] + [test-helpers.component :as h])) + +(h/describe "Locked Input" + (h/test "renders label, value and icon" + (h/render [locked-input/locked-input + {:label "Label" + :icon :i/gas} "Value"]) + (h/is-truthy (h/query-by-text "Label")) + (h/is-truthy (h/get-by-text "Value"))) + + (h/test "no value" + (h/render [locked-input/locked-input + {:label "Label" + :icon :i/gas}]) + (h/is-null (h/query-by-text "Value")) + (h/is-truthy (h/get-by-text "Label"))) + + (h/test "no emoji" + (h/render [locked-input/locked-input + {:label "Label"} "Value"]) + (h/is-truthy (h/get-by-text "Label")) + (h/is-truthy (h/get-by-text "Value")))) diff --git a/src/quo/components/inputs/locked_input/style.cljs b/src/quo/components/inputs/locked_input/style.cljs new file mode 100644 index 00000000000..d4645a9031c --- /dev/null +++ b/src/quo/components/inputs/locked_input/style.cljs @@ -0,0 +1,21 @@ +(ns quo.components.inputs.locked-input.style + (:require + [quo.foundations.colors :as colors])) + +(defn info-box-container + [theme] + {:flex-direction :row + :border-radius 12 + :align-items :center + :background-color (colors/theme-colors colors/neutral-10 + colors/neutral-80-opa-80 + theme) + :min-height 40 + :padding-horizontal 12 + :padding-vertical 9 + :margin-top 4}) + +(defn info-box-label + [theme] + {:color (colors/theme-colors colors/black colors/white theme) + :margin-left 8}) diff --git a/src/quo/components/inputs/locked_input/view.cljs b/src/quo/components/inputs/locked_input/view.cljs new file mode 100644 index 00000000000..e1843eb554c --- /dev/null +++ b/src/quo/components/inputs/locked_input/view.cljs @@ -0,0 +1,43 @@ +(ns quo.components.inputs.locked-input.view + (:require + [quo.components.icon :as icons] + [quo.components.inputs.locked-input.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- info-box + [{:keys [icon value-text theme]}] + [rn/view + {:style (style/info-box-container theme)} + [rn/view + (when icon + [icons/icon icon + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}])] + [text/text + {:size :paragraph-1 + :style (style/info-box-label theme)} value-text]]) + +(defn locked-input + "Options: + + :label - string (default nil) - Text to display above the input + :icon - keyword (default nil) - Icon to display in the info box + :container-style - style map (default nil) - Override style for the container + :theme - :light/:dark (passed from with-theme HOC) + + :value - string (default nil) - value to display in the info box" + [{:keys [label icon container-style]} value] + (let [theme (quo.context/use-theme)] + [rn/view {:style container-style} + (when label + [text/text + {:size :paragraph-2 + :weight :medium + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + label]) + [info-box + {:theme theme + :icon icon + :value-text value}]])) diff --git a/src/quo/components/inputs/profile_input/component_spec.cljs b/src/quo/components/inputs/profile_input/component_spec.cljs new file mode 100644 index 00000000000..9bdfe570830 --- /dev/null +++ b/src/quo/components/inputs/profile_input/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo.components.inputs.profile-input.component-spec + (:require + [quo.components.inputs.profile-input.view :as profile-input] + [test-helpers.component :as h])) + +(h/describe "Profile Input" + (h/test "on press event fires" + (let [event (h/mock-fn)] + (h/render-with-theme-provider + [profile-input/profile-input + {:placeholder "Your Name" + :on-press event}]) + (h/fire-event :press (h/get-by-label-text :select-profile-picture-button)) + (h/was-called-times event 1)))) diff --git a/src/quo/components/inputs/profile_input/style.cljs b/src/quo/components/inputs/profile_input/style.cljs new file mode 100644 index 00000000000..4834da0b5ed --- /dev/null +++ b/src/quo/components/inputs/profile_input/style.cljs @@ -0,0 +1,29 @@ +(ns quo.components.inputs.profile-input.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [customization-color] + {:background-color (colors/resolve-color customization-color :light 40) + :padding-horizontal 12 + :padding-top 12 + :padding-bottom 10 + :border-radius 16 + :flex 1}) + +(def button + {:position :absolute + :top 24 + :bottom 0 + :left 33 + :right 0 + :width 24 + :height 24 + :border-radius 24}) + +(def button-inner {:border-radius 24}) + +(def input-container + {:flex-direction :row + :margin-top 4 + :align-items :center}) diff --git a/src/quo/components/inputs/profile_input/view.cljs b/src/quo/components/inputs/profile_input/view.cljs new file mode 100644 index 00000000000..c2224057c31 --- /dev/null +++ b/src/quo/components/inputs/profile_input/view.cljs @@ -0,0 +1,54 @@ +(ns quo.components.inputs.profile-input.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.buttons.button.view :as buttons] + [quo.components.inputs.profile-input.style :as style] + [quo.components.inputs.title-input.view :as title-input] + [react-native.core :as rn] + [react-native.hole-view :as hole-view])) + +(defn profile-input + [{:keys [customization-color + placeholder + on-press + title-input-props + image-picker-props]}] + (let [full-name (:full-name image-picker-props)] + [rn/view + {:style (style/container customization-color)} + [rn/view + [hole-view/hole-view + ;; Force re-render hole view when props are changed + ;; https://github.com/status-im/status-mobile/issues/15577 + {:key (hash + (if (:profile-picture image-picker-props) + (:profile-picture image-picker-props) + image-picker-props)) + :holes [{:x 33 + :y 24 + :width 24 + :height 24 + :borderRadius 12}]} + [user-avatar/user-avatar + (assoc image-picker-props + :customization-color customization-color + :static? true + :status-indicator? false + :full-name (if (seq full-name) full-name placeholder) + :size :medium)]] + [buttons/button + {:accessibility-label :select-profile-picture-button + :type :grey + :background :blur + :on-press on-press + :size 24 + :icon-only? true + :container-style style/button + :inner-style style/button-inner} :i/camera]] + [rn/view {:style style/input-container} + [title-input/view + (merge title-input-props + {:blur? true + :size :heading-2 + :placeholder placeholder + :customization-color customization-color})]]])) diff --git a/src/quo/components/inputs/recovery_phrase/component_spec.cljs b/src/quo/components/inputs/recovery_phrase/component_spec.cljs new file mode 100644 index 00000000000..e94e325c3b5 --- /dev/null +++ b/src/quo/components/inputs/recovery_phrase/component_spec.cljs @@ -0,0 +1,61 @@ +(ns quo.components.inputs.recovery-phrase.component-spec + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.components.inputs.recovery-phrase.view :as recovery-phrase] + [test-helpers.component :as h])) + +(h/describe "Recovery phrase input" + (h/test "Default render" + (h/render [recovery-phrase/recovery-phrase-input {}]) + (h/is-truthy (h/get-by-label-text :recovery-phrase-input))) + + (h/test "Renders specified text" + (let [text-expected "My custom text"] + (h/render [recovery-phrase/recovery-phrase-input {} text-expected]) + (h/is-equal (oops/oget (h/get-by-label-text :recovery-phrase-input) "props" "children") + text-expected))) + + (h/test "Text changes and dispatches on-change-text" + (let [new-text "New text" + on-change-mock (h/mock-fn) + get-new-text #(-> % (oops/oget "props" "onChangeText" "mock" "calls") (aget 0 0))] + (h/render [recovery-phrase/recovery-phrase-input {:on-change-text on-change-mock} + "Old text"]) + (h/fire-event :change-text (h/get-by-label-text :recovery-phrase-input) new-text) + (h/is-equal (get-new-text (h/get-by-label-text :recovery-phrase-input)) new-text) + (h/was-called on-change-mock))) + + (h/describe "Error text" + (h/test "Marked when words doesn't satisfy a predicate" + (h/render [recovery-phrase/recovery-phrase-input + {:mark-errors? true + :error-pred-current-word #(>= (count %) 5) + :error-pred-written-words #(>= (count %) 5)} + "Text with some error words that don't satisfy the predicate"]) + (let [children-text-nodes (-> (h/get-by-label-text :recovery-phrase-input) + (oops/oget "props" "children" "props" "children") + (js->clj :keywordize-keys true)) + {:keys [ok-words error-words]} (group-by #(if (string? %) :ok-words :error-words) + children-text-nodes)] + (h/is-equal (apply str ok-words) "Text with some that the ") + (h/is-truthy (= (map #(-> % :props :argv first) error-words) + ["error" "words" "don't" "satisfy" "predicate"])))) + + (h/test "Marked when words exceed the limit given" + (h/render [recovery-phrase/recovery-phrase-input + {:mark-errors? true + :word-limit 4} + "these are ok words, these words exceed the limit"]) + (let [children-text-nodes (-> (h/get-by-label-text :recovery-phrase-input) + (oops/oget "props" "children" "props" "children") + (js->clj :keywordize-keys true)) + {:keys [ok-words error-words]} (group-by #(if (string? %) :ok-words :error-words) + children-text-nodes)] + (h/is-equal (string/trim (apply str ok-words)) + "these are ok words,") + (h/is-equal (->> error-words + (map #(-> % :props :argv first)) + (interpose " ") + (apply str)) + "these words exceed the limit"))))) diff --git a/src/quo/components/inputs/recovery_phrase/style.cljs b/src/quo/components/inputs/recovery_phrase/style.cljs new file mode 100644 index 00000000000..9c3292ebab0 --- /dev/null +++ b/src/quo/components/inputs/recovery_phrase/style.cljs @@ -0,0 +1,46 @@ +(ns quo.components.inputs.recovery-phrase.style + (:require + [quo.components.markdown.text :as text] + [quo.foundations.colors :as colors])) + +(defn container + [container-style] + (merge {:min-height 40 + :flex 1 + :padding-vertical 4 + :padding-horizontal 20} + container-style)) + +(defn input + [theme] + (assoc (text/text-style {} theme) + :height 32 + :flex-grow 1 + :padding-vertical 5 + :text-align-vertical :top)) + +(defn placeholder-color + [input-state theme blur?] + (cond + (and (= input-state :focused) blur?) + (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + + (= input-state :focused) ; Not blur + (colors/theme-colors colors/neutral-30 colors/neutral-60 theme) + + blur? ; :default & blur + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-30 theme) + + :else ; :default & not blur + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))) + +(defn cursor-color + [customization-color theme] + (colors/resolve-color customization-color theme)) + +(defn error-word + [theme] + {:height 22 + :padding-horizontal 20 + :background-color colors/danger-50-opa-10 + :color (colors/theme-colors colors/danger-50 colors/danger-60 theme)}) diff --git a/src/quo/components/inputs/recovery_phrase/view.cljs b/src/quo/components/inputs/recovery_phrase/view.cljs new file mode 100644 index 00000000000..74ba3730121 --- /dev/null +++ b/src/quo/components/inputs/recovery_phrase/view.cljs @@ -0,0 +1,80 @@ +(ns quo.components.inputs.recovery-phrase.view + (:require + [clojure.string :as string] + [quo.components.inputs.recovery-phrase.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(def ^:private custom-props + [:customization-color :theme :blur? :cursor-color :multiline :on-focus :on-blur + :placeholder-text-color :mark-errors? :error-pred :word-limit]) + +(defn- error-word + [text theme] + [rn/text {:style (style/error-word theme)} + text]) + +(defn- mark-error-words + [{:keys [pred-last-word pred-previous-words text word-limit theme]}] + (let [last-index (dec (count (string/split text #"\s+"))) + words (map #(apply str %) + (partition-by #(= " " %) text))] + (->> words + (reduce (fn [{:keys [idx] :as acc} word] + (let [error-pred (if (= last-index idx) pred-last-word pred-previous-words) + invalid-word? (and (or (error-pred word) + (>= idx word-limit)) + (not (string/blank? word))) + not-blank-spaces? (not (string/blank? word))] + (cond-> acc + not-blank-spaces? (update :idx inc) + :always (update :result + conj + (if invalid-word? + [error-word word theme] + word))))) + {:result [:<>] + :idx 0}) + :result))) + +(defn recovery-phrase-input + [{:keys [customization-color blur? on-focus on-blur mark-errors? + error-pred-current-word error-pred-written-words word-limit + container-style placeholder-text-color on-input-ref] + :or {customization-color :blue + word-limit ##Inf + error-pred-current-word (constantly false) + error-pred-written-words (constantly false)} + :as props} + text] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + on-focus (rn/use-callback + (fn [] + (set-state :focused) + (when on-focus (on-focus)))) + on-blur (rn/use-callback + (fn [] + (set-state :default) + (when on-blur (on-blur)))) + extra-props (apply dissoc props custom-props)] + [rn/view {:style (style/container container-style)} + [rn/text-input + (merge {:ref on-input-ref + :accessibility-label :recovery-phrase-input + :style (style/input theme) + :placeholder-text-color (or placeholder-text-color + (style/placeholder-color state theme blur?)) + :cursor-color (style/cursor-color customization-color theme) + :keyboard-appearance theme + :multiline true + :on-focus on-focus + :on-blur on-blur} + extra-props) + (if mark-errors? + (mark-error-words {:pred-last-word error-pred-current-word + :pred-previous-words error-pred-written-words + :text text + :word-limit word-limit + :theme theme}) + text)]])) diff --git a/src/quo/components/inputs/search_input/style.cljs b/src/quo/components/inputs/search_input/style.cljs new file mode 100644 index 00000000000..5d5495ec921 --- /dev/null +++ b/src/quo/components/inputs/search_input/style.cljs @@ -0,0 +1,70 @@ +(ns quo.components.inputs.search-input.style + (:require + [quo.components.markdown.text :as text] + [quo.foundations.colors :as colors])) + +(defn placeholder-color + [state blur? theme] + (cond + (and blur? (= state :active)) + (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + + blur? ; state is default + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-30 theme) + + (= state :active) + (colors/theme-colors colors/neutral-30 colors/neutral-60 theme) + + :else ; Not blur and state is default + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))) + +(def clear-icon-container + {:justify-content :center + :align-items :center + :margin-left 8 + :height 20 + :width 20}) + +(defn clear-icon + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme))) + +(defn cursor + [customization-color theme] + (colors/resolve-color customization-color theme)) + +(def tag-separator {:width 8}) + +(def tag-container + {:flex-direction :row + :margin-right 8}) + +(defn container + [container-style] + (merge {:flex 1 + :flex-direction :row + :align-items :center} + container-style)) + +(def scroll-container + {:flex-direction :row + :flex 1 + :height 32}) + +(def scroll-content + {:flex-grow 1 + :align-items :center + :justify-content :flex-start}) + +(defn input-text + [disabled? theme] + (assoc (text/text-style {:size :paragraph-1 + :weight :regular} + theme) + :flex 1 + :padding-vertical 5 + :min-width 120 + :opacity (if disabled? 0.3 1) + :min-height 32)) diff --git a/src/quo/components/inputs/search_input/view.cljs b/src/quo/components/inputs/search_input/view.cljs new file mode 100644 index 00000000000..8d7b01ad933 --- /dev/null +++ b/src/quo/components/inputs/search_input/view.cljs @@ -0,0 +1,93 @@ +(ns quo.components.inputs.search-input.view + (:require + [oops.core :as oops] + [quo.components.icon :as icon] + [quo.components.inputs.search-input.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(def ^:private tag-separator [rn/view {:style style/tag-separator}]) + +(defn- inner-tags + [tags-coll] + (into [rn/view {:style style/tag-container}] + (interpose tag-separator) + tags-coll)) + +(defn- clear-button + [{:keys [on-press blur? theme]}] + [rn/touchable-opacity + {:style style/clear-icon-container + :on-press on-press} + [icon/icon :i/clear + {:color (style/clear-icon blur? theme) + :size 20}]]) + +(defn- handle-backspace + [event ^js/Object scroll-view-ref] + (when (and (= (oops/oget event "nativeEvent.key") "Backspace") + scroll-view-ref) + (.scrollToEnd scroll-view-ref #js {:animated false}))) + +(def ^:private props-to-remove + [:cursor-color :placeholder-text-color :editable :on-change-text :on-focus + :on-blur :on-clear :value :tags :disabled? :blur? :customization-color]) + +(defn- add-children + [text-input children] + (if (seq children) + (into text-input children) + text-input)) + +(defn search-input + [{:keys [value tags disabled? blur? on-change-text customization-color show-clear-button? + on-clear on-focus on-blur container-style] + :or {customization-color :blue} + :as props} + & children] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + on-focus (rn/use-callback + (fn [] + (set-state :active) + (when on-focus (on-focus)))) + on-blur (rn/use-callback + (fn [] + (set-state :default) + (when on-blur (on-blur)))) + scroll-view-ref (rn/use-ref-atom nil) + on-scroll-view-ref (rn/use-callback #(reset! scroll-view-ref %)) + on-key-press (rn/use-callback #(handle-backspace % @scroll-view-ref)) + use-value? (boolean value) + clean-props (apply dissoc props props-to-remove)] + [rn/view + {:accessibility-label :search-input + :style (style/container container-style)} + [rn/scroll-view + {:ref on-scroll-view-ref + :style style/scroll-container + :content-container-style style/scroll-content + :horizontal true + :shows-horizontal-scroll-indicator false} + (when (seq tags) + [inner-tags tags]) + (add-children + [rn/text-input + (cond-> {:style (style/input-text disabled? theme) + :cursor-color (style/cursor customization-color theme) + :placeholder-text-color (style/placeholder-color state blur? theme) + :editable (not disabled?) + :on-key-press on-key-press + :keyboard-appearance (colors/theme-colors :light :dark theme) + :on-change-text on-change-text + :on-focus on-focus + :on-blur on-blur} + use-value? (assoc :value value) + (seq clean-props) (merge clean-props))] + (when-not use-value? children))] + (when (or (seq value) (seq children) show-clear-button?) + [clear-button + {:on-press on-clear + :blur? blur? + :theme theme}])])) diff --git a/src/quo/components/inputs/title_input/component_spec.cljs b/src/quo/components/inputs/title_input/component_spec.cljs new file mode 100644 index 00000000000..52544a771ac --- /dev/null +++ b/src/quo/components/inputs/title_input/component_spec.cljs @@ -0,0 +1,34 @@ +(ns quo.components.inputs.title-input.component-spec + (:require + [quo.components.inputs.title-input.view :as title-input] + [test-helpers.component :as h])) + +(h/describe "input -> title-input component" + (h/test "renders empty" + (h/render [title-input/view {:value ""}]) + (h/is-truthy (h/query-by-label-text :profile-title-input))) + + (h/test "empty text renders with max length digits and 00" + (h/render [title-input/view + {:value "" + :max-length 24}]) + (h/fire-event :on-focus (h/query-by-label-text :profile-title-input)) + (-> (h/wait-for #(h/get-by-text "0")) + (.then #(h/is-truthy (h/query-by-text "/24"))))) + + (h/test "renders with max length digits and character count" + (h/render [title-input/view + {:default-value "abc" + :max-length 24} + "abc"]) + (h/fire-event :on-focus (h/query-by-label-text :profile-title-input)) + (-> (h/wait-for #(h/get-by-text "3")) + (.then #(h/is-truthy (h/query-by-text "/24"))))) + + (h/test "text updates on change" + (let [on-change-text (h/mock-fn)] + (h/render [title-input/view + {:value "mock text" + :on-change-text on-change-text}]) + (h/fire-event :change-text (h/get-by-label-text :profile-title-input) "mock-text-new") + (h/was-called on-change-text)))) diff --git a/src/quo/components/inputs/title_input/style.cljs b/src/quo/components/inputs/title_input/style.cljs new file mode 100644 index 00000000000..2e79ab3b993 --- /dev/null +++ b/src/quo/components/inputs/title_input/style.cljs @@ -0,0 +1,53 @@ +(ns quo.components.inputs.title-input.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn get-focused-placeholder-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme) + (colors/theme-colors colors/neutral-30 colors/neutral-60 theme))) + +(defn get-placeholder-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-30 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))) + +(defn get-char-count-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))) + +(defn get-selection-color + [customization-color blur? theme] + (colors/alpha (if blur? + (colors/theme-colors colors/neutral-100 colors/white theme) + (colors/resolve-color customization-color theme)) + (if platform/ios? 1 0.2))) + +(def text-input-container {:flex 1}) + +(defn title-text + [theme] + {:text-align-vertical :bottom + :padding 0 + :color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn char-count + [blur? theme] + {:color (get-char-count-color blur? theme)}) + +(defn container + [disabled?] + {:flex-direction :row + :opacity (if disabled? 0.3 1) + :justify-content :center + :align-items :center}) + +(defn counter-container + [focused?] + {:padding-top (if focused? 12 9) + :padding-bottom (if focused? 2 3)}) diff --git a/src/quo/components/inputs/title_input/view.cljs b/src/quo/components/inputs/title_input/view.cljs new file mode 100644 index 00000000000..26f5dfd4876 --- /dev/null +++ b/src/quo/components/inputs/title_input/view.cljs @@ -0,0 +1,80 @@ +(ns quo.components.inputs.title-input.view + (:require + [quo.components.icon :as icon] + [quo.components.inputs.title-input.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn- pad-0 + [value] + (if (<= (count value) 1) + (str 0 value) + value)) + +(defn view + [{:keys [blur? on-change-text auto-focus placeholder max-length default-value return-key-type + size on-focus on-blur container-style customization-color disabled?] + :or {max-length 0 + auto-focus false + default-value ""}}] + (let [theme (quo.context/use-theme) + [focused? set-focused] (rn/use-state auto-focus) + [value set-value] (rn/use-state default-value) + input-ref (rn/use-ref-atom nil) + on-inpur-ref (rn/use-callback #(reset! input-ref %)) + on-press (rn/use-callback + #(when-not disabled? (.focus ^js @input-ref)) + [disabled?]) + on-change (rn/use-callback + (fn [v] + (set-value v) + (when on-change-text (on-change-text v)))) + on-focus (rn/use-callback + (fn [] + (when (fn? on-focus) (on-focus)) + (set-focused true))) + on-blur (rn/use-callback + (fn [] + (when (fn? on-blur) (on-blur)) + (set-focused false)))] + [rn/view + {:style (merge (style/container disabled?) container-style)} + [rn/view {:style style/text-input-container} + [rn/text-input + {:style (text/text-style + {:size (or size :heading-1) + :weight :semi-bold + :style (style/title-text theme)} + nil) + :default-value default-value + :accessibility-label :profile-title-input + :keyboard-appearance theme + :return-key-type return-key-type + :on-focus on-focus + :on-blur on-blur + :auto-focus auto-focus + :input-mode :text + :on-change-text on-change + :editable (not disabled?) + :max-length max-length + :placeholder placeholder + :ref on-inpur-ref + :selection-color (style/get-selection-color customization-color blur? theme) + :placeholder-text-color (if focused? + (style/get-focused-placeholder-color blur? theme) + (style/get-placeholder-color blur? theme))}]] + [rn/view + {:style (style/counter-container focused?)} + (if focused? + [text/text + [text/text + {:style (style/char-count blur? theme) + :size :paragraph-2} + (str (count value))] + [text/text + {:style (style/char-count blur? theme) + :size :paragraph-2} + (str "/" (pad-0 (str max-length)))]] + [rn/pressable {:on-press on-press} + [icon/icon :i/edit {:color (style/get-char-count-color blur? theme)}]])]])) diff --git a/src/quo/components/ios/drawer_bar/style.cljs b/src/quo/components/ios/drawer_bar/style.cljs new file mode 100644 index 00000000000..112edb3f668 --- /dev/null +++ b/src/quo/components/ios/drawer_bar/style.cljs @@ -0,0 +1,17 @@ +(ns quo.components.ios.drawer-bar.style + (:require + [quo.context] + [quo.foundations.colors :as colors])) + +(def handle-container + {:padding-vertical 8 + :height 20 + :align-items :center}) + +(defn handle + [theme] + {:width 32 + :height 4 + :background-color (colors/theme-colors colors/neutral-100 colors/white theme) + :opacity (if (= theme :light) 0.05 0.1) + :border-radius 100}) diff --git a/src/quo/components/ios/drawer_bar/view.cljs b/src/quo/components/ios/drawer_bar/view.cljs new file mode 100644 index 00000000000..6a250d44603 --- /dev/null +++ b/src/quo/components/ios/drawer_bar/view.cljs @@ -0,0 +1,11 @@ +(ns quo.components.ios.drawer-bar.view + (:require + [quo.components.ios.drawer-bar.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [_] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/handle-container} + [rn/view {:style (style/handle theme)}]])) diff --git a/src/quo/components/keycard/component_spec.cljs b/src/quo/components/keycard/component_spec.cljs new file mode 100644 index 00000000000..1629fb49345 --- /dev/null +++ b/src/quo/components/keycard/component_spec.cljs @@ -0,0 +1,10 @@ +(ns quo.components.keycard.component-spec + (:require + [quo.components.keycard.view :as keycard] + [test-helpers.component :as h])) + +(h/describe "keycard component" + (let [holder-name "Alisha"] + (h/test "Render of keycard component when: holder-name prop is set" + (h/render [keycard/keycard-component {:holder-name holder-name}]) + (h/is-truthy (h/get-by-translation-text :t/user-keycard {:name holder-name}))))) diff --git a/src/quo/components/keycard/style.cljs b/src/quo/components/keycard/style.cljs new file mode 100644 index 00000000000..8e62613792e --- /dev/null +++ b/src/quo/components/keycard/style.cljs @@ -0,0 +1,35 @@ +(ns quo.components.keycard.style + (:require + [quo.foundations.colors :as colors])) + +(def keycard-height 210) +(def keycard-chip-height 28) + +(defn card-container + [locked? theme blur?] + {:overflow :hidden + :height keycard-height + :align-items :flex-start + :justify-content :space-between + :border-width 1 + :border-color (if (or locked? blur?) + colors/white-opa-5 + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)) + :border-style :solid + :border-radius 16 + :padding 16 + :background-color (if locked? + (colors/theme-colors colors/danger-50 colors/danger-50-opa-40 theme) + colors/keycard-color)}) + +(defn keycard-logo + [locked? theme] + {:tint-color (if locked? + colors/white + (colors/theme-colors colors/neutral-100 colors/white theme))}) + +(def keycard-chip + {:height keycard-chip-height + :position :absolute + :right 16 + :top (/ (- keycard-height 28) 2)}) diff --git a/src/quo/components/keycard/view.cljs b/src/quo/components/keycard/view.cljs new file mode 100644 index 00000000000..c4a5683c83e --- /dev/null +++ b/src/quo/components/keycard/view.cljs @@ -0,0 +1,52 @@ +(ns quo.components.keycard.view + (:require + [clojure.string :as string] + [quo.components.keycard.style :as style] + [quo.components.tags.tag :as tag] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn keycard + [] + (let [width (- (:width (rn/get-window)) 55)] + [rn/image + {:resize-mode :contain + :style {:margin-top 8 + :margin-bottom 20 + :width width + :max-height (/ width 1.6) + :flex 1 + :flex-direction :row + :align-self :center} + :source (resources/get-image :keycard-orange)}])) + +(defn keycard-component + "This component based on the following properties: + - :holder-name - Can be owner's name. Default is Empty + - :locked? - Boolean to specify whether the keycard is locked or not + - :theme :light/:dark + " + [{:keys [holder-name locked? blur?]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/card-container locked? theme blur?)} + [rn/image + {:source (resources/get-image :keycard-logo) + :style (style/keycard-logo locked? theme)}] + [rn/image + {:source (resources/get-image + (if (or locked? (= :dark theme)) :keycard-chip-dark :keycard-chip-light)) + :style style/keycard-chip}] + (when-not (string/blank? holder-name) + [tag/tag + {:size 32 + :type (when locked? :icon) + :label (i18n/label :t/user-keycard {:name holder-name}) + :labelled? true + :blurred? true + :resource (when locked? :i/locked) + :accessibility-label :holder-name + :icon-color colors/white-70-blur + :override-theme (when locked? :dark)}])])) diff --git a/src/quo/components/links/internal_link_card/channel/style.cljs b/src/quo/components/links/internal_link_card/channel/style.cljs new file mode 100644 index 00000000000..2f0b7d17dfc --- /dev/null +++ b/src/quo/components/links/internal_link_card/channel/style.cljs @@ -0,0 +1,72 @@ +(ns quo.components.links.internal-link-card.channel.style + (:require [quo.foundations.colors :as colors])) + +(defn loading-circle + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :margin-right 4 + :width 16 + :height 16 + :border-radius 16}) + +(defn loading-first-line-bar + [theme margin-right?] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :width 72 + :margin-right (when margin-right? 4) + :height 16 + :border-radius 6}) + +(defn loading-second-line-bar + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :width 112 + :height 8 + :border-radius 6 + :margin-bottom 17}) + +(defn loading-thumbnail-box + [theme size] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :height (if (= :message size) 139 160) + :border-radius 12}) + +(defn thumbnail + [size] + {:width "100%" + :height (if (= :message size) 139 160) + :margin-top 8 + :border-radius 12}) + +(defn container + [size theme] + {:border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme) + :border-radius 16 + :padding-horizontal 12 + :padding-top 10 + :padding-bottom 12 + :height (if (= :message size) 215 236) + :width (if (= :message size) 295 335)}) + +(def header-container + {:flex-direction :row + :align-items :center}) + +(def title + {:margin-bottom 2}) + +(def logo + {:margin-right 6 + :width 16 + :height 16 + :border-radius 8}) + +(defn channel-chevron-props + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def row-spacing + {:flex-direction :row + :margin-bottom 13}) diff --git a/src/quo/components/links/internal_link_card/channel/view.cljs b/src/quo/components/links/internal_link_card/channel/view.cljs new file mode 100644 index 00000000000..16796e0008a --- /dev/null +++ b/src/quo/components/links/internal_link_card/channel/view.cljs @@ -0,0 +1,90 @@ +(ns quo.components.links.internal-link-card.channel.view + (:require + [quo.components.icon :as icon] + [quo.components.links.internal-link-card.channel.style :as style] + [quo.components.links.internal-link-card.schema :as component-schema] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- description-comp + [description] + [rn/view {:style {:margin-bottom 4}} + [text/text + {:size :paragraph-2 + :number-of-lines 3 + :accessibility-label :description} + description]]) + +(defn- title-comp + [title channel-name theme] + [rn/view + {:style {:flex-direction :row + :align-items :center}} + [text/text + {:size :paragraph-1 + :number-of-lines 1 + :weight :semi-bold + :style style/title + :accessibility-label :title} + title] + [icon/icon :i/chevron-right (style/channel-chevron-props theme)] + [text/text + {:size :paragraph-1 + :number-of-lines 1 + :weight :semi-bold + :style style/title + :accessibility-label :title} + channel-name]]) + +(defn- banner-comp + [thumbnail size] + [rn/image + {:style (style/thumbnail size) + :source thumbnail + :accessibility-label :banner}]) + +(defn- logo-comp + [logo] + [rn/image + {:accessibility-label :logo + :source logo + :style (assoc style/logo :margin-bottom 2)}]) + +(defn- loading-view + [theme size] + [rn/view + {:accessibility-label :loading-channel-link-view + :style {:height 215}} + [rn/view {:style {:flex-direction :row}} + [rn/view {:style style/row-spacing} + [rn/view {:style (style/loading-circle theme)}] + [rn/view {:style (style/loading-first-line-bar theme true)}]] + [rn/view {:style style/row-spacing} + [rn/view {:style (style/loading-circle theme)}] + [rn/view {:style (style/loading-first-line-bar theme false)}]]] + [rn/view {:style (style/loading-second-line-bar theme)}] + [rn/view {:style (style/loading-thumbnail-box theme size)}]]) + +(defn view-internal + [{:keys [title description loading? icon banner on-press channel-name size] + :or {channel-name "empty name"}}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:style (style/container size theme) + :accessibility-label :internal-link-card + :on-press on-press} + (if loading? + [loading-view theme size] + [:<> + [rn/view {:style style/header-container} + (when icon + [logo-comp icon]) + [title-comp title channel-name theme]] + (when description + [description-comp description]) + (when banner + [banner-comp banner size])])])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/links/internal_link_card/community/style.cljs b/src/quo/components/links/internal_link_card/community/style.cljs new file mode 100644 index 00000000000..c809f40d07a --- /dev/null +++ b/src/quo/components/links/internal_link_card/community/style.cljs @@ -0,0 +1,87 @@ +(ns quo.components.links.internal-link-card.community.style + (:require [quo.foundations.colors :as colors])) + +(defn loading-circle + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :margin-right 4 + :width 16 + :height 16 + :border-radius 16}) + +(defn loading-stat + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :margin-right 4 + :width 32 + :height 8 + :border-radius 16}) + +(def loading-stat-container + {:flex-direction :row + :align-items :center + :margin-right 12 + :margin-bottom -6}) + +(defn loading-first-line-bar + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :width 145 + :height 16 + :border-radius 6}) + +(defn loading-second-line-bar + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :width 112 + :height 8 + :border-radius 6 + :margin-bottom 17}) + +(defn loading-thumbnail-box + [theme size] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :height (if (= :message size) 139 160) + :border-radius 12}) + +(defn thumbnail + [size] + {:width "100%" + :height (if (= :message size) 139 160) + :margin-top 6 + :border-radius 12}) + +(defn container + [size theme] + {:border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme) + :border-radius 16 + :padding-horizontal 12 + :padding-top 10 + :padding-bottom 12 + :width (if (= :message size) 295 335) + :flex 1}) + +(def header-container + {:flex-direction :row + :align-items :center}) + +(def title + {:margin-bottom 2}) + +(def logo + {:width 16 + :height 16 + :border-radius 8 + :margin-right 4 + :margin-bottom 2}) + +(def row-spacing + {:flex-direction :row + :margin-bottom 12 + :margin-top 4}) + +(def stat-container + {:flex-direction :row + :margin-top 12}) diff --git a/src/quo/components/links/internal_link_card/community/view.cljs b/src/quo/components/links/internal_link_card/community/view.cljs new file mode 100644 index 00000000000..fce147632bd --- /dev/null +++ b/src/quo/components/links/internal_link_card/community/view.cljs @@ -0,0 +1,95 @@ +(ns quo.components.links.internal-link-card.community.view + (:require + [quo.components.community.community-stat.view :as community-stat] + [quo.components.links.internal-link-card.community.style :as style] + [quo.components.links.internal-link-card.schema :as component-schema] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- description-comp + [description members-count active-members-count] + [rn/view + [text/text + {:size :paragraph-2 + :number-of-lines 3 + :accessibility-label :description} + description] + [rn/view {:style style/stat-container} + [community-stat/view + {:value members-count + :icon :i/members + :accessibility-label :members-count + :style {:margin-right 12} + :text-size :paragraph-2}] + (when active-members-count + [community-stat/view + {:value active-members-count + :icon :i/active-members + :accessibility-label :active-members-count + :text-size :paragraph-2}])]]) + +(defn- title-comp + [title] + [text/text + {:size :paragraph-1 + :number-of-lines 1 + :weight :semi-bold + :style style/title + :accessibility-label :title} + title]) + +(defn- thumbnail-comp + [thumbnail size] + [rn/image + {:style (style/thumbnail size) + :source thumbnail + :accessibility-label :thumbnail}]) + +(defn- logo-comp + [logo] + [rn/image + {:accessibility-label :logo + :source logo + :style style/logo}]) + +(defn- stat-loading + [theme] + [rn/view {:style style/loading-stat-container} + [rn/view {:style (style/loading-circle theme)}] + [rn/view {:style (style/loading-stat theme)}]]) + +(defn- loading-view + [theme size] + [rn/view {:accessibility-label :loading-community-link-view} + [rn/view {:style style/row-spacing} + [rn/view {:style (style/loading-circle theme)}] + [rn/view {:style (style/loading-first-line-bar theme)}]] + [rn/view {:style (style/loading-second-line-bar theme)}] + [rn/view {:style style/row-spacing} + [stat-loading theme] + [stat-loading theme]] + [rn/view {:style (style/loading-thumbnail-box theme size)}]]) + +(defn- view-internal + [{:keys [title description loading? icon banner members-count active-members-count + on-press size]}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:style (style/container size theme) + :accessibility-label :internal-link-card + :on-press on-press} + (if loading? + [loading-view theme size] + [:<> + [rn/view {:style style/header-container} + (when icon + [logo-comp icon]) + [title-comp title]] + (when description + [description-comp description members-count active-members-count]) + (when banner + [thumbnail-comp banner size])])])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/links/internal_link_card/component_spec.cljs b/src/quo/components/links/internal_link_card/component_spec.cljs new file mode 100644 index 00000000000..9279c554c78 --- /dev/null +++ b/src/quo/components/links/internal_link_card/component_spec.cljs @@ -0,0 +1,81 @@ +(ns quo.components.links.internal-link-card.component-spec + (:require + [quo.components.links.internal-link-card.view :as view] + [test-helpers.component :as h])) + +(defn- render + [component] + (h/render-with-theme-provider component :light)) + +(def user-props + {:title "Some title" + :subtitle "Some description" + :loading? false + :icon "data:image/png,logo-x" + :type :user + :customization-color "#ff0000" + :emoji-hash "🌟🚀🐠🌈🏰🔮🦉🐼🍉🎨🚲🌙🍔🌵"}) + +(h/describe "Internal link card - User" + (h/test "renders with most common props" + (render [view/view user-props]) + (h/is-truthy (h/query-by-text (:title user-props))) + (h/is-truthy (h/query-by-text (:subtitle user-props)))) + + (h/test "does not render logo if prop is not present" + (render [view/view (dissoc user-props :icon)]) + (h/is-null (h/query-by-label-text :logo)))) + +(def community-props + {:title "Some title" + :description "Some description" + :icon "data:image/png,logo-x" + :banner "data:image/png,whatever" + :members-count "20" + :loading? false + :active-members-count "15" + :type :community}) + +(h/describe "Internal link card - Community" + (h/test "renders with most common props" + (render [view/view community-props]) + (h/is-truthy (h/query-by-text (:title community-props))) + (h/is-truthy (h/query-by-text (:description community-props))) + (h/is-truthy (h/query-by-text (:members-count community-props))) + (h/is-truthy (h/query-by-text (:active-members-count community-props))) + (h/is-truthy (h/query-by-label-text :logo)) + (h/is-truthy (h/query-by-label-text :thumbnail))) + + (h/test "does not render thumbnail if prop is not present" + (render [view/view (dissoc community-props :banner)]) + (h/is-null (h/query-by-label-text :thumbnail))) + + (h/test "does not render logo if prop is not present" + (render [view/view (dissoc community-props :icon)]) + (h/is-null (h/query-by-label-text :logo)))) + +(def channel-props + {:title "Doodles" + :description "Coloring the world with joy • ᴗ •" + :icon "data:image/png,logo-x" + :banner "data:image/png,whatever" + :loading? false + :channel-name "#general" + :type :channel}) + +(h/describe "Internal link card - Channel" + (h/test "renders with most common props" + (render [view/view channel-props]) + (h/is-truthy (h/query-by-text (:title channel-props))) + (h/is-truthy (h/query-by-text (:description channel-props))) + (h/is-truthy (h/query-by-text (:channel-name channel-props))) + (h/is-truthy (h/query-by-label-text :logo)) + (h/is-truthy (h/query-by-label-text :banner))) + + (h/test "does not render banner if prop is not present" + (render [view/view (dissoc channel-props :banner)]) + (h/is-null (h/query-by-label-text :banner))) + + (h/test "does not render logo if prop is not present" + (render [view/view (dissoc channel-props :icon)]) + (h/is-null (h/query-by-label-text :logo)))) diff --git a/src/quo/components/links/internal_link_card/schema.cljs b/src/quo/components/links/internal_link_card/schema.cljs new file mode 100644 index 00000000000..a3a1a47f84b --- /dev/null +++ b/src/quo/components/links/internal_link_card/schema.cljs @@ -0,0 +1,22 @@ +(ns quo.components.links.internal-link-card.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:title {:optional true} [:maybe :string]] + [:description {:optional true} [:maybe :string]] + [:channel-name {:optional true} [:maybe :string]] + [:loading? {:optional true} [:maybe :boolean]] + [:subtitle {:optional true} [:maybe :string]] + [:icon {:optional true} [:maybe [:or :string :int]]] + [:banner {:optional true} [:maybe [:or :string :int]]] + [:type {:optional true} [:maybe :keyword]] + [:on-press {:optional true} [:maybe fn?]] + [:members-count {:optional true} [:maybe [:or :int :string]]] + [:active-members-count {:optional true} [:maybe [:or :int :string]]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:emoji-hash {:optional true} [:maybe :string]] + [:size {:optional true} [:maybe :keyword]]]]] + :any]) diff --git a/src/quo/components/links/internal_link_card/user/style.cljs b/src/quo/components/links/internal_link_card/user/style.cljs new file mode 100644 index 00000000000..b2d99beb490 --- /dev/null +++ b/src/quo/components/links/internal_link_card/user/style.cljs @@ -0,0 +1,66 @@ +(ns quo.components.links.internal-link-card.user.style + (:require [quo.foundations.colors :as colors])) + +(defn loading-circle + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :margin-right 4 + :width 16 + :height 16 + :border-radius 16}) + +(defn loading-first-line-bar + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :width 145 + :height 16 + :border-radius 6}) + +(defn loading-second-line-bar + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :width 112 + :height 8 + :border-radius 6 + :margin-bottom 17}) + +(defn last-bar-line-bar + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :width 271 + :height 16 + :border-radius 6}) + +(defn gradient-start-color + [customization-color theme] + (colors/theme-colors (colors/resolve-color customization-color theme 10) + (colors/resolve-color customization-color theme 20) + theme)) + +(defn container + [loading? theme size] + {:border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme) + :border-radius 16 + :padding-horizontal 12 + :padding-top 10 + :padding-bottom 12 + :height (if loading? 92 110) + :width (if (= :message size) 295 335)}) + +(def header-container + {:flex-direction :row + :align-items :center}) + +(def title + {:margin-bottom 2}) + +(def logo + {:margin-right 6 + :width 16 + :height 16 + :border-radius 8 + :margin-bottom 2}) + +(def row-spacing {:flex-direction :row :margin-bottom 12}) diff --git a/src/quo/components/links/internal_link_card/user/view.cljs b/src/quo/components/links/internal_link_card/user/view.cljs new file mode 100644 index 00000000000..ee3e241a7a6 --- /dev/null +++ b/src/quo/components/links/internal_link_card/user/view.cljs @@ -0,0 +1,80 @@ +(ns quo.components.links.internal-link-card.user.view + (:require + [quo.components.links.internal-link-card.schema :as component-schema] + [quo.components.links.internal-link-card.user.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [schema.core :as schema])) + +(defn- subtitle-comp + [subtitle emoji-hash] + [rn/view + [text/text + {:size :paragraph-2 + :number-of-lines 2 + :accessibility-label :subtitle + :style {:margin-bottom 12}} + subtitle] + [rn/view {:style {:flex-direction :row}} + [text/text + {:size :paragraph-2 + :number-of-lines 1 + :weight :regular + :accessibility-label :emoji-hash} + emoji-hash]]]) + +(defn- title-comp + [title] + [text/text + {:size :paragraph-1 + :number-of-lines 1 + :weight :semi-bold + :style style/title + :accessibility-label :title} + title]) + +(defn- logo-comp + [logo] + [rn/image + {:accessibility-label :logo + :source logo + :style style/logo}]) + +(defn- loading-view + [theme] + [rn/view {:accessibility-label :loading-user-link-view} + [rn/view {:style style/row-spacing} + [rn/view {:style (style/loading-circle theme)}] + [rn/view {:style (style/loading-first-line-bar theme)}]] + [rn/view {:style (style/loading-second-line-bar theme)}] + [rn/view {:style (style/last-bar-line-bar theme)}]]) + +(defn- linear-gradient-props + [theme customization-color] + [(style/gradient-start-color customization-color theme) :transparent]) + +(defn view-internal + [{:keys [title loading? icon on-press subtitle emoji-hash customization-color size]}] + (let [theme (quo.context/use-theme)] + (if loading? + [rn/pressable + {:accessibility-label :internal-link-card + :on-press on-press + :style (style/container loading? theme size)} + [loading-view theme]] + [linear-gradient/linear-gradient + {:style (style/container loading? theme size) + :colors (linear-gradient-props theme customization-color)} + [rn/pressable + {:accessibility-label :internal-link-card + :on-press on-press} + [rn/view {:style style/header-container} + (when icon + [logo-comp icon]) + [title-comp title]] + (when subtitle + [subtitle-comp subtitle emoji-hash])]]))) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/links/internal_link_card/view.cljs b/src/quo/components/links/internal_link_card/view.cljs new file mode 100644 index 00000000000..9625efccca9 --- /dev/null +++ b/src/quo/components/links/internal_link_card/view.cljs @@ -0,0 +1,11 @@ +(ns quo.components.links.internal-link-card.view + (:require [quo.components.links.internal-link-card.channel.view :as channel.view] + [quo.components.links.internal-link-card.community.view :as community.view] + [quo.components.links.internal-link-card.user.view :as user.view])) + +(defn view + [{card-type :type :as props}] + (case card-type + :community [community.view/view props] + :channel [channel.view/view props] + :user [user.view/view props])) diff --git a/src/quo/components/links/link_preview/component_spec.cljs b/src/quo/components/links/link_preview/component_spec.cljs new file mode 100644 index 00000000000..5c391147044 --- /dev/null +++ b/src/quo/components/links/link_preview/component_spec.cljs @@ -0,0 +1,44 @@ +(ns quo.components.links.link-preview.component-spec + (:require + [quo.components.links.link-preview.view :as view] + [test-helpers.component :as h])) + +(def props + {:title "Some title" + :description "Some description" + :link "status.im" + :thumbnail "data:image/png,whatever"}) + +(h/describe "Links - Link Preview" + (h/test "default render" + (h/render [view/view]) + (h/is-truthy (h/query-by-label-text :link-preview)) + (h/is-null (h/query-by-label-text :button-enable-preview))) + + (h/test "renders with most common props" + (h/render [view/view props]) + (h/is-truthy (h/query-by-text (:title props))) + (h/is-truthy (h/query-by-text (:description props))) + (h/is-truthy (h/query-by-text (:link props))) + (h/is-truthy (h/query-by-label-text :thumbnail))) + + (h/test "does not render thumbnail if prop is not present" + (h/render [view/view (dissoc props :thumbnail)]) + (h/is-null (h/query-by-label-text :thumbnail))) + + (h/test "shows button to enable preview when preview is disabled" + (h/render [view/view + (assoc props + :enabled? false + :disabled-text "I'm disabled")]) + (h/is-truthy (h/query-by-label-text :button-enable-preview)) + (h/is-truthy (h/query-by-text "I'm disabled"))) + + (h/test "on-enable event" + (let [on-enable (h/mock-fn)] + (h/render [view/view + (assoc props + :enabled? false + :on-enable on-enable)]) + (h/fire-event :press (h/get-by-label-text :button-enable-preview)) + (h/was-called on-enable)))) diff --git a/src/quo/components/links/link_preview/style.cljs b/src/quo/components/links/link_preview/style.cljs new file mode 100644 index 00000000000..2801a1b9788 --- /dev/null +++ b/src/quo/components/links/link_preview/style.cljs @@ -0,0 +1,43 @@ +(ns quo.components.links.link-preview.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [preview-enabled? theme] + (merge {:border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme) + :border-radius 16 + :padding-horizontal 12 + :padding-top 10 + :padding-bottom 12} + (when-not preview-enabled? + {:height 139 + :align-items :center + :justify-content :center}))) + +(def header-container + {:flex-direction :row + :align-items :center}) + +(def title + {:flex 1 + :margin-bottom 2}) + +(defn link + [theme] + {:margin-top 8 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn thumbnail + [size] + {:width "100%" + :height (if (= size :large) 271 139) + :margin-top 12 + :border-radius 12}) + +(def logo + {:margin-right 6 + :width 16 + :height 16 + :border-radius 8}) diff --git a/src/quo/components/links/link_preview/view.cljs b/src/quo/components/links/link_preview/view.cljs new file mode 100644 index 00000000000..e16a070ff6c --- /dev/null +++ b/src/quo/components/links/link_preview/view.cljs @@ -0,0 +1,105 @@ +(ns quo.components.links.link-preview.view + (:require + ["react-native-blob-util" :default ReactNativeBlobUtil] + [oops.core :as oops] + [quo.components.buttons.button.view :as button] + [quo.components.links.link-preview.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.svg :as svg] + [taoensso.timbre :as log])) + +(defn- button-disabled + [disabled-text on-enable] + [button/button + {:icon-left :i/reveal + :size 32 + :type :grey + :on-press on-enable + :accessibility-label :button-enable-preview} + disabled-text]) + +(defn- description-comp + [description] + [text/text + {:size :paragraph-2 + :number-of-lines 3 + :accessibility-label :description} + description]) + +(defn- link-comp + [link theme] + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/link theme) + :accessibility-label :link} + link]) + +(defn- title-comp + [title] + [text/text + {:size :paragraph-1 + :number-of-lines 1 + :weight :semi-bold + :style style/title + :accessibility-label :title} + title]) + +(defn- thumbnail-comp + [thumbnail size] + [rn/image + {:style (style/thumbnail size) + :source (if (string? thumbnail) + {:uri thumbnail} + thumbnail) + :accessibility-label :thumbnail}]) + +(defn- get-image-data + [logo set-is-svg on-success] + (-> (.config ReactNativeBlobUtil (clj->js {:trusty platform/ios?})) + (.fetch "GET" logo) + (.then (fn [imgObj] + (set-is-svg (= "image/svg" + (oops/oget imgObj + ["respInfo" "headers" "Content-Type"]))) + (on-success (oops/oget imgObj "data")))) + (.catch #(log/error "could not fetch favicon " logo)))) + +(defn- logo-comp + [logo] + (let [[image-data set-image-data] (rn/use-state nil) + [is-svg? set-is-svg] (rn/use-state nil) + on-success (fn [data-uri] + (set-image-data data-uri)) + _get-image-data (get-image-data logo set-is-svg on-success)] + (if is-svg? + [svg/svg-xml (merge style/logo {:xml image-data})] + [rn/image + {:accessibility-label :logo + :source {:uri (str "data:image/png;base64," image-data)} + :style style/logo}]))) + +(defn view + [{:keys [title logo description link thumbnail + enabled? on-enable disabled-text + container-style thumbnail-size] + :or {enabled? true}}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge (style/container enabled? theme) container-style) + :accessibility-label :link-preview} + (if enabled? + [:<> + [rn/view {:style style/header-container} + (when logo + [logo-comp logo]) + [title-comp title]] + (when description + [description-comp description]) + [link-comp link] + (when thumbnail + [thumbnail-comp thumbnail thumbnail-size])] + [button-disabled disabled-text on-enable])])) diff --git a/src/quo/components/links/url_preview/component_spec.cljs b/src/quo/components/links/url_preview/component_spec.cljs new file mode 100644 index 00000000000..6eeeb4c6f5c --- /dev/null +++ b/src/quo/components/links/url_preview/component_spec.cljs @@ -0,0 +1,33 @@ +(ns quo.components.links.url-preview.component-spec + (:require + [quo.components.links.url-preview.view :as view] + [test-helpers.component :as h])) + +(h/describe "Links - URL Preview" + (h/test "default render" + (h/render [view/view]) + (h/is-truthy (h/query-by-label-text :title)) + (h/is-truthy (h/query-by-label-text :button-clear-preview)) + (h/is-null (h/query-by-label-text :logo)) + (h/is-null (h/query-by-label-text :url-preview-loading))) + + (h/test "renders logo when prop is present" + (h/render [view/view {:logo "data:image/png,logo"}]) + (h/is-truthy (h/query-by-label-text :logo))) + + (h/test "on-clear event" + (let [on-clear (h/mock-fn)] + (h/render [view/view {:on-clear on-clear}]) + (h/fire-event :press (h/get-by-label-text :button-clear-preview)) + (h/was-called on-clear))) + + (h/describe "loading state" + (h/test "shows a loading container" + (h/render [view/view {:loading? true :loading-message "Hello"}]) + (h/is-null (h/query-by-label-text :title)) + (h/is-truthy (h/query-by-label-text :url-preview-loading))) + + (h/test "renders if `loading-message` is not passed" + (h/render [view/view {:loading? true}]) + (h/is-null (h/query-by-label-text :title)) + (h/is-truthy (h/query-by-label-text :url-preview-loading))))) diff --git a/src/quo/components/links/url_preview/style.cljs b/src/quo/components/links/url_preview/style.cljs new file mode 100644 index 00000000000..247b3dc00b6 --- /dev/null +++ b/src/quo/components/links/url_preview/style.cljs @@ -0,0 +1,56 @@ +(ns quo.components.links.url-preview.style + (:require + [quo.foundations.colors :as colors])) + +(def horizontal-padding 12) + +(defn container + [theme] + {:height 56 + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme) + :padding-vertical 10 + :padding-horizontal horizontal-padding + :border-radius 12 + :align-self :stretch + :flex-direction :row}) + +(defn loading-container + [theme] + {:height 56 + :border-width 1 + :border-radius 12 + :border-style :dashed + :align-items :center + :flex-direction :row + :justify-content :center + :align-self :stretch + :padding horizontal-padding + :border-color (colors/theme-colors colors/neutral-30 colors/neutral-80 theme)}) + +(defn loading-message + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def logo + {:width 16 + :height 16 + :top 1 + :border-radius 8}) + +(def content-container + {:margin-left 6 + :flex 1}) + +(defn title + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn body + [theme] + {:text-transform :lowercase + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def clear-button-container + {:width 20 + :height 20 + :margin-left 6}) diff --git a/src/quo/components/links/url_preview/view.cljs b/src/quo/components/links/url_preview/view.cljs new file mode 100644 index 00000000000..a0e47cc861c --- /dev/null +++ b/src/quo/components/links/url_preview/view.cljs @@ -0,0 +1,88 @@ +(ns quo.components.links.url-preview.view + (:require + [clojure.string :as string] + [quo.components.icon :as icon] + [quo.components.links.url-preview.style :as style] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.svg :as svg])) + +(def base64-svg-prefix "data:image/svg;base64,") + +(defn- logo-comp + [{:keys [logo]}] + (if (and (string? logo) + (string/starts-with? logo base64-svg-prefix)) + [svg/svg-xml + (merge style/logo + {:xml (-> logo + (string/replace base64-svg-prefix "") + js/atob)})] + [rn/image + {:accessibility-label :logo + :source (if (string? logo) + {:uri logo} + logo) + :style style/logo + :resize-mode :cover}])) + +(defn- content + [{:keys [title body]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/content-container} + [text/text + {:accessibility-label :title + :size :paragraph-2 + :weight :semi-bold + :number-of-lines 1 + :style (style/title theme)} + title] + [text/text + {:accessibility-label :body + :size :paragraph-2 + :weight :medium + :number-of-lines 1 + :style (style/body theme)} + body]])) + +(defn- clear-button + [{:keys [on-press]}] + (let [theme (quo.context/use-theme)] + [rn/touchable-opacity + {:on-press on-press + :style style/clear-button-container + :hit-slop {:top 3 :right 3 :bottom 3 :left 3} + :accessibility-label :button-clear-preview} + [icon/icon :i/clear + {:size 20 + :color (colors/theme-colors colors/neutral-50 colors/neutral-60 theme)}]])) + +(defn view + [{:keys [title body logo on-clear loading? loading-message container-style]}] + (let [theme (quo.context/use-theme)] + (if loading? + [rn/view + {:accessibility-label :url-preview-loading + :style (merge (style/loading-container theme) container-style)} + [icon/icon :i/loading + {:size 12 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :container-style {:margin-right 8}}] + [rn/text + {:size :paragraph-2 + :weight :medium + :number-of-lines 1 + :style (style/loading-message theme)} + loading-message]] + [rn/view + {:accessibility-label :url-preview + :style (merge (style/container theme) container-style)} + (when logo + [logo-comp + {:logo (if (map? logo) + (:data-uri logo) + logo)}]) + [content {:title title :body body}] + [clear-button {:on-press on-clear}]]))) diff --git a/src/quo/components/links/url_preview_list/component_spec.cljs b/src/quo/components/links/url_preview_list/component_spec.cljs new file mode 100644 index 00000000000..dc378d1086e --- /dev/null +++ b/src/quo/components/links/url_preview_list/component_spec.cljs @@ -0,0 +1,49 @@ +(ns quo.components.links.url-preview-list.component-spec + (:require + [oops.core :as oops] + [quo.components.links.url-preview-list.view :as view] + [test-helpers.component :as h])) + +(def previews + (->> (range 3) + (map inc) + (mapv (fn [index] + {:title (str "Title " index) + :body (str "status.im." index) + :loading? false + :url (str "status.im." index)})))) + +(h/describe "Links - URL Preview List" + (h/test "default render" + (h/render [view/view + {:data previews + :key-fn :url + :horizontal-spacing 10}]) + (-> (count (h/query-all-by-label-text :url-preview)) + (h/expect) + (.toEqual 3)) + + (-> (map #(oops/oget % "props.children") + (h/query-all-by-label-text :title)) + (clj->js) + (h/expect) + (.toStrictEqual #js ["Title 1" "Title 2" "Title 3"]))) + + (h/test "on-clear event is individually handled by each preview" + (let [on-clear (h/mock-fn)] + (h/render [view/view + {:data previews + :key-fn :url + :on-clear on-clear}]) + (h/fire-event :press (first (h/get-all-by-label-text :button-clear-preview))) + (h/fire-event :press (second (h/get-all-by-label-text :button-clear-preview))) + (h/was-called-times on-clear 2))) + + (h/test "previews have separate loading states" + (h/render [view/view + {:data (assoc-in previews [1 :loading?] true) + :key-fn :url}]) + (h/is-truthy (h/get-by-label-text :url-preview-loading)) + (-> (count (h/query-all-by-label-text :url-preview)) + (h/expect) + (.toEqual 2)))) diff --git a/src/quo/components/links/url_preview_list/style.cljs b/src/quo/components/links/url_preview_list/style.cljs new file mode 100644 index 00000000000..f4c00e8e9ce --- /dev/null +++ b/src/quo/components/links/url_preview_list/style.cljs @@ -0,0 +1,8 @@ +(ns quo.components.links.url-preview-list.style) + +(def url-preview-gap 12) + +(def url-preview-separator + {:width url-preview-gap + :padding-top 12 + :padding-bottom 8}) diff --git a/src/quo/components/links/url_preview_list/view.cljs b/src/quo/components/links/url_preview_list/view.cljs new file mode 100644 index 00000000000..eb668500526 --- /dev/null +++ b/src/quo/components/links/url_preview_list/view.cljs @@ -0,0 +1,80 @@ +(ns quo.components.links.url-preview-list.view + (:require + [quo.components.links.url-preview-list.style :as style] + [quo.components.links.url-preview.view :as url-preview] + [react-native.core :as rn] + [react-native.gesture :as gesture])) + +(defn- use-scroll-to-last-item + [flat-list-ref item-count item-width] + (rn/use-effect + (fn [] + (when (and (pos? item-count) (pos? item-width)) + ;; We use a delay because calling `scrollToOffset` without a delay does + ;; nothing while the flatlist is still rendering its children. + ;; `scrollToEnd` doesn't work because it positions the item off-center + ;; and there's no argument to offset it. + (let [timer-id (js/setTimeout + (fn [] + (when (and @flat-list-ref (pos? item-count)) + (.scrollToOffset ^js @flat-list-ref + #js + {:animated true + :offset (* (+ item-width style/url-preview-gap) + (max 0 (dec item-count)))}))) + 25)] + (fn [] + (js/clearTimeout timer-id))))) + [item-count item-width])) + +(defn- separator + [] + [rn/view {:style style/url-preview-separator}]) + +(defn- item-component + [{:keys [title body loading? logo url]} _ _ + {:keys [width on-clear loading-message container-style]}] + [url-preview/view + {:logo logo + :title title + :body body + :loading? loading? + :loading-message loading-message + :on-clear on-clear + :container-style (merge container-style {:width width}) + :url url}]) + +(defn- f-view + [] + (let [flat-list-ref (atom nil)] + (fn [{:keys [data key-fn horizontal-spacing on-clear loading-message + container-style container-style-item + preview-width]}] + (use-scroll-to-last-item flat-list-ref (count data) preview-width) + ;; We need to use a wrapping view expanded to 100% instead of "flex 1", + ;; otherwise `on-layout` will be triggered multiple times as the flat list + ;; renders its children. + [rn/view + {:style container-style + :accessibility-label :url-preview-list} + [gesture/flat-list + {:ref #(reset! flat-list-ref %) + :keyboard-should-persist-taps :always + :key-fn key-fn + :horizontal true + :deceleration-rate :fast + :on-scroll-to-index-failed identity + :content-container-style {:padding-horizontal horizontal-spacing} + :separator [separator] + :snap-to-interval (+ preview-width style/url-preview-gap) + :shows-horizontal-scroll-indicator false + :data data + :render-fn item-component + :render-data {:width preview-width + :on-clear on-clear + :loading-message loading-message + :container-style container-style-item}}]]))) + +(defn view + [props] + [:f> f-view props]) diff --git a/src/quo/components/list_items/account/component_spec.cljs b/src/quo/components/list_items/account/component_spec.cljs new file mode 100644 index 00000000000..50697914a1d --- /dev/null +++ b/src/quo/components/list_items/account/component_spec.cljs @@ -0,0 +1,117 @@ +(ns quo.components.list-items.account.component-spec + (:require + [quo.components.list-items.account.view :as account] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(def ^:private theme :light) + +(def ^:private default-props + {:account-props {:name "Account name" + :address "0x1234567890" + :emoji "🍑" + :customization-color "blue"}}) + +(defn- with-defaults + ([] default-props) + ([props] (merge default-props props))) + +(h/describe "List items: account" + (h/test "default render'" + (h/render-with-theme-provider [account/view (with-defaults)] theme) + (h/is-truthy (h/query-by-label-text :container))) + + (h/test "on-press-in changes state to :pressed" + (h/render-with-theme-provider [account/view (with-defaults)] theme) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 5)}))) + + (h/test "on-press-in changes state to :pressed with blur? enabled" + (h/render-with-theme-provider [account/view (with-defaults {:blur? true})] theme) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor colors/white-opa-5}))) + + (h/test "render with state :active" + (h/render-with-theme-provider [account/view (with-defaults {:state :active})] theme) + (h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 10)})) + + (h/test "render with state :active and blur? enabled" + (h/render-with-theme-provider [account/view + (with-defaults + {:blur? true + :state :active})] + theme) + (h/has-style (h/query-by-label-text :container) + {:backgroundColor colors/white-opa-10})) + + (h/test "render with state :selected" + (h/render-with-theme-provider [account/view (with-defaults {:state :selected})] theme) + (h/is-truthy (h/query-by-label-text :check-icon))) + + (h/test "calls on-press" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [account/view (with-defaults {:on-press on-press})] theme) + (h/fire-event :on-press (h/get-by-label-text :container)) + (h/was-called on-press))) + + (h/test "renders token props if type :tag" + (h/render-with-theme-provider [account/view + (with-defaults {:type :tag + :token-props {:symbol "SNT" + :value "1,000"}})] + theme) + (h/is-truthy (h/query-by-label-text :tag-container))) + + (h/test "renders keycard icon if title-icon is present" + (h/render-with-theme-provider [account/view (with-defaults {:title-icon :i/placeholder})] theme) + (h/is-truthy (h/query-by-label-text :title-icon))) + + (h/test "doesn't render keycard icon if title-icon is missing" + (h/render-with-theme-provider [account/view (with-defaults)] theme) + (h/is-falsy (h/query-by-label-text :title-icon))) + + (h/test "renders balance container but not arrow icon if type :balance-neutral" + (h/render-with-theme-provider [account/view + (with-defaults {:type :balance-neutral + :balance-props {:fiat-value "€0.00" + :percentage-change "0.0" + :fiat-change "€0.00"}})] + theme) + (h/is-truthy (h/query-by-label-text :balance-container)) + (h/is-falsy (h/query-by-label-text :arrow-icon))) + + (h/test "renders balance container and negative arrow icon if type :balance-negative" + (h/render-with-theme-provider [account/view + (with-defaults {:type :balance-negative + :balance-props {:fiat-value "€0.00" + :percentage-change "0.0" + :fiat-change "€0.00"}})] + theme) + (h/is-truthy (h/query-by-label-text :balance-container)) + (h/is-truthy (h/query-by-label-text :icon-negative)) + (h/is-falsy (h/query-by-label-text :icon-positive))) + + (h/test "renders balance container and positive arrow icon if type :balance-positive" + (h/render-with-theme-provider [account/view + (with-defaults {:type :balance-positive + :balance-props {:fiat-value "€0.00" + :percentage-change "0.0" + :fiat-change "€0.00"}})] + theme) + (h/is-truthy (h/query-by-label-text :balance-container)) + (h/is-falsy (h/query-by-label-text :icon-negative)) + (h/is-truthy (h/query-by-label-text :icon-positive))) + + (h/test "renders options button if type :action" + (let [on-options-press (h/mock-fn)] + (h/render-with-theme-provider [account/view + (with-defaults + {:type :action + :on-options-press on-options-press})] + theme) + (h/is-truthy (h/query-by-label-text :options-button)) + (h/fire-event :on-press (h/get-by-label-text :options-button)) + (h/was-called on-options-press)))) diff --git a/src/quo/components/list_items/account/schema.cljs b/src/quo/components/list_items/account/schema.cljs new file mode 100644 index 00000000000..f7890355c3b --- /dev/null +++ b/src/quo/components/list_items/account/schema.cljs @@ -0,0 +1,49 @@ +(ns quo.components.list-items.account.schema) + +(def ^:private ?balance + [:map + [:balance-props + [:map + [:fiat-value :string] + [:percentage-change :string] + [:fiat-change :string]]]]) + +(def ^:private ?tag + [:map + [:token-props + [:map + [:symbol :string] + [:value :string]]]]) + +(def ^:private ?action + [:map + [:on-options-press {:optional true} [:maybe fn?]]]) + +(def ^:private ?base + [:map + [:type {:optional true} + [:enum :default :tag :action :balance-neutral :balance-negative :balance-positive]] + [:state {:optional true} [:enum :default :selected :active :disabled]] + [:blur? {:optional true} [:maybe :boolean]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:on-press {:optional true} [:maybe fn?]] + [:title-icon {:optional true} [:maybe :keyword]] + [:account-props + [:map + [:name :string] + [:address :string] + [:emoji :string] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]]) + +(def ?schema + [:=> + [:cat + [:multi {:dispatch :type} + [:balance-neutral [:merge ?base ?balance]] + [:balance-negative [:merge ?base ?balance]] + [:balance-positive [:merge ?base ?balance]] + [:tag [:merge ?base ?tag]] + [:action [:merge ?base ?action]] + [:default ?base] + [:malli.core/default ?base]]] + :any]) diff --git a/src/quo/components/list_items/account/style.cljs b/src/quo/components/list_items/account/style.cljs new file mode 100644 index 00000000000..d04fe32ad77 --- /dev/null +++ b/src/quo/components/list_items/account/style.cljs @@ -0,0 +1,105 @@ +(ns quo.components.list-items.account.style + (:require + [quo.foundations.colors :as colors])) + +(defn- background-color + [{:keys [state pressed? blur? customization-color]}] + (cond (or pressed? (= state :selected)) + (if blur? colors/white-opa-5 (colors/resolve-color customization-color :light 5)) + (= state :active) + (if blur? colors/white-opa-10 (colors/resolve-color customization-color :light 10)) + (and pressed? blur?) colors/white-opa-10 + :else :transparent)) + +(defn container + [{:keys [state] :as props}] + {:height 56 + :border-radius 12 + :background-color (background-color props) + :flex-direction :row + :align-items :center + :opacity (if (= state :disabled) 0.3 1) + :padding-horizontal 12 + :padding-vertical 6 + :justify-content :space-between}) + +(def left-container + {:flex-direction :row + :align-items :center}) + +(defn metric-text + [type theme] + {:color (case type + :balance-positive (colors/theme-colors colors/success-50 colors/success-60 theme) + :balance-negative (colors/theme-colors colors/danger-50 colors/danger-60 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(defn dot-divider + [type theme] + {:width 2 + :height 2 + :border-radius 2 + :margin-horizontal 4 + :background-color (case type + :balance-positive (colors/theme-colors colors/success-50-opa-40 + colors/success-60-opa-40 + theme) + :balance-negative (colors/theme-colors colors/danger-50-opa-40 + colors/danger-50-opa-40 + theme) + (colors/theme-colors colors/neutral-80-opa-40 colors/neutral-50-opa-40 theme))}) + +(defn arrow-icon + [type theme] + {:size 16 + :color (if (= type :balance-positive) + (colors/theme-colors colors/success-50 colors/success-60 theme) + (colors/theme-colors colors/danger-50 colors/danger-60 theme))}) + +(def arrow-icon-container + {:margin-left 4}) + +(def account-container + {:margin-left 8}) + +(def account-title-container + {:flex-direction :row + :height 22 + :align-items :center}) + +(def title-icon-container + {:margin-left 4}) + +(def token-tag-container + {:height 40 + :padding-top 4}) + +(defn token-tag-text-container + [blur? theme] + {:flex-direction :row + :align-items :center + :height 16 + :padding-horizontal 3 + :border-width 1 + :border-radius 6 + :border-color (if blur? + colors/white-opa-10 + (colors/theme-colors colors/neutral-20 + colors/neutral-80 + theme))}) + +(defn token-tag-text + [blur? theme] + {:margin-top -1 + :color (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(def balance-container + {:align-items :flex-end + :justify-content :space-between}) + +(def metrics-container + {:flex-direction :row + :align-items :center + :margin-top 2}) diff --git a/src/quo/components/list_items/account/view.cljs b/src/quo/components/list_items/account/view.cljs new file mode 100644 index 00000000000..04c81629379 --- /dev/null +++ b/src/quo/components/list_items/account/view.cljs @@ -0,0 +1,133 @@ +(ns quo.components.list-items.account.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.icon :as icon] + [quo.components.list-items.account.schema :as component-schema] + [quo.components.list-items.account.style :as style] + [quo.components.markdown.text :as text] + [quo.components.wallet.address-text.view :as address-text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- account-view + [{:keys [account-props title-icon blur? theme]}] + [rn/view {:style style/left-container} + [account-avatar/view (assoc account-props :size 32)] + [rn/view {:style style/account-container} + [rn/view + {:style style/account-title-container} + [text/text + {:weight :semi-bold + :size :paragraph-1} + (:name account-props)] + (when title-icon + [icon/icon title-icon + {:size 20 + :color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)) + :container-style style/title-icon-container + :accessibility-label :title-icon}])] + [address-text/view (assoc account-props :format :short)]]]) + +(defn- balance-view + [{:keys [balance-props type theme]}] + [rn/view + {:style style/balance-container + :accessibility-label :balance-container} + [text/text + {:weight :medium + :size :paragraph-2} + (:fiat-value balance-props)] + [rn/view + {:style style/metrics-container} + [text/text + {:size :paragraph-2 + :style (style/metric-text type theme)} + (str (:percentage-change balance-props) "%")] + [rn/view {:style (style/dot-divider type theme)}] + [text/text + {:size :paragraph-2 + :style (style/metric-text type theme)} + (:fiat-change balance-props)] + (when (not= type :balance-neutral) + [rn/view + {:style style/arrow-icon-container + :accessibility-label :arrow-icon} + [icon/icon (if (= type :balance-positive) :i/positive :i/negative) + (assoc (style/arrow-icon type theme) + :accessibility-label + (if (= type :balance-positive) :icon-positive :icon-negative))]])]]) + +(defn- token-tag + [{:keys [token-props blur? theme]}] + ;; TODO: Use Tiny tag component when available (issue #17341) + [rn/view + {:style (style/token-tag-text-container blur? theme) + :accessibility-label :tag-container} + [text/text + {:size :label + :weight :medium + :style (style/token-tag-text blur? theme)} + (str (:value token-props) " " (:symbol token-props))]]) + +(defn- options-button + [{:keys [on-options-press blur? theme]}] + [rn/pressable + {:accessibility-label :options-button + :on-press #(when on-options-press + (on-options-press))} + [icon/icon :i/options + {:color (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}]]) + +(defn- check-icon + [{:keys [blur? customization-color theme]}] + [rn/view {:accessibility-label :check-icon} + [icon/icon :i/check + {:color (if blur? + colors/white + (colors/resolve-color customization-color theme))}]]) + +(defn- internal-view + [{:keys [type state blur? customization-color on-press] + :or {customization-color :blue + type :default + state :default + blur? false} + :as props}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false)) + props (assoc props :theme theme)] + [rn/pressable + {:style (style/container + {:state state + :blur? blur? + :customization-color customization-color + :pressed? pressed?}) + :on-press-in on-press-in + :on-press on-press + :disabled (= state :disabled) + :on-press-out on-press-out + :accessibility-label :container} + [account-view props] + [rn/view {:style (when (= type :tag) style/token-tag-container)} + (cond + (#{:balance-neutral :balance-negative :balance-positive} type) + [balance-view props] + + (= type :tag) + [token-tag props] + + (= type :action) + [options-button props] + + (and (= type :default) (= state :selected)) + [check-icon props])]])) + +(def view (schema/instrument #'internal-view component-schema/?schema)) diff --git a/src/quo/components/list_items/account_list_card/component_spec.cljs b/src/quo/components/list_items/account_list_card/component_spec.cljs new file mode 100644 index 00000000000..da3c4431fec --- /dev/null +++ b/src/quo/components/list_items/account_list_card/component_spec.cljs @@ -0,0 +1,20 @@ +(ns quo.components.list-items.account-list-card.component-spec + (:require + [quo.components.list-items.account-list-card.view :as account-list-card] + [test-helpers.component :as h])) + +(def account-props + {:customization-color :purple + :size 32 + :emoji "🍑" + :type :default + :name "Tip to Vegas" + :address "0x0ah...78b"}) + +(h/describe "List items: account list card" + (h/test "Test icon renders for ':action :icon'" + (h/render-with-theme-provider [account-list-card/view + {:account-props account-props + :action :icon + :on-options-press (fn [])}]) + (h/is-truthy (h/get-by-label-text :icon)))) diff --git a/src/quo/components/list_items/account_list_card/schema.cljs b/src/quo/components/list_items/account_list_card/schema.cljs new file mode 100644 index 00000000000..9d170e31426 --- /dev/null +++ b/src/quo/components/list_items/account_list_card/schema.cljs @@ -0,0 +1,29 @@ +(ns quo.components.list-items.account-list-card.schema) + +(def ^:private ?base + [:map + [:action {:optional true} [:enum :icon :none]] + [:blur? {:optional true} [:maybe :boolean]] + [:on-press {:optional true} [:maybe fn?]] + [:account-props + [:map {:closed true} + [:type [:enum :default :watch-only]] + [:name :string] + [:address :string] + [:emoji :string] + [:size {:optional true} [:enum 80 :size-64 48 32 28 24 20 16]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]] + [:networks {:optional true} [:* [:map [:network-name :keyword] [:short-name :string]]]]]) + +(def ^:private ?on-option-press + [:map + [:on-options-press [:maybe fn?]]]) + +(def ?schema + [:=> + [:cat + [:multi {:dispatch :action} + [:icon [:merge ?base ?on-option-press]] + [:none ?base] + [:malli.core/default ?base]]] + :any]) diff --git a/src/quo/components/list_items/account_list_card/style.cljs b/src/quo/components/list_items/account_list_card/style.cljs new file mode 100644 index 00000000000..829d4e2111b --- /dev/null +++ b/src/quo/components/list_items/account_list_card/style.cljs @@ -0,0 +1,25 @@ +(ns quo.components.list-items.account-list-card.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [{:keys [state blur? theme]}] + (let [light-bg (if (= state :default) colors/neutral-2_5 colors/neutral-5) + dark-bg (if (= state :default) colors/neutral-80-opa-40 colors/neutral-80-opa-60) + blur-bg (if (= state :default) colors/white-opa-5 colors/white-opa-10) + light-border (if (= state :default) colors/neutral-10 colors/neutral-20) + dark-border (if (= state :default) colors/neutral-80 colors/neutral-70)] + {:height 48 + :border-radius 12 + :background-color (if blur? blur-bg (colors/theme-colors light-bg dark-bg theme)) + :border-width (if blur? 0 1) + :border-color (colors/theme-colors light-border dark-border theme) + :flex-direction :row + :align-items :center + :padding-horizontal 8 + :padding-vertical 6 + :justify-content :space-between})) + +(def left-container + {:flex-direction :row + :align-items :center}) diff --git a/src/quo/components/list_items/account_list_card/view.cljs b/src/quo/components/list_items/account_list_card/view.cljs new file mode 100644 index 00000000000..6d4afa528b4 --- /dev/null +++ b/src/quo/components/list_items/account_list_card/view.cljs @@ -0,0 +1,45 @@ +(ns quo.components.list-items.account-list-card.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.icon :as icon] + [quo.components.list-items.account-list-card.schema :as component-schema] + [quo.components.list-items.account-list-card.style :as style] + [quo.components.markdown.text :as text] + [quo.components.wallet.address-text.view :as address-text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- internal-view + [{:keys [action blur? account-props on-press on-options-press]}] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + on-press-in (rn/use-callback #(set-state :pressed)) + on-press-out (rn/use-callback #(set-state :default))] + [rn/pressable + {:style (style/container {:state state :blur? blur? :theme theme}) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :accessibility-label :container} + [rn/view {:style style/left-container} + [account-avatar/view account-props] + [rn/view {:style {:margin-left 8}} + [text/text + {:weight :semi-bold + :size :paragraph-2} + (:name account-props)] + [address-text/view + {:blur? blur? + :address (:address account-props) + :format :short}]]] + (when (= action :icon) + [rn/pressable {:on-press on-options-press} + [icon/icon :i/options + {:color (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)) + :accessibility-label :icon}]])])) + +(def view (schema/instrument #'internal-view component-schema/?schema)) diff --git a/src/quo/components/list_items/address/component_spec.cljs b/src/quo/components/list_items/address/component_spec.cljs new file mode 100644 index 00000000000..df86e5505ba --- /dev/null +++ b/src/quo/components/list_items/address/component_spec.cljs @@ -0,0 +1,46 @@ +(ns quo.components.list-items.address.component-spec + (:require + [quo.components.list-items.address.view :as address] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(defn- with-defaults + ([] (with-defaults {})) + ([props] (merge {:address "0x0ah...78b"} props))) + +(h/describe "List items: address" + (h/test "default render" + (h/render-with-theme-provider [address/view (with-defaults)]) + (h/is-truthy (h/query-by-label-text :container))) + + (h/test "on-press-in changes state to :pressed" + (h/render-with-theme-provider [address/view (with-defaults)]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 5)}))) + + (h/test "on-press-in changes state to :pressed with blur? enabled" + (h/render-with-theme-provider [address/view (with-defaults {:blur? true})]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor colors/white-opa-5}))) + + (h/test "on-press-out changes state to :active" + (h/render-with-theme-provider [address/view (with-defaults {:active-state? true})]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/fire-event :on-press-out (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 10)}))) + + (h/test "on-press-out changes state to :active with blur? enabled" + (h/render-with-theme-provider [address/view (with-defaults {:active-state? true :blur? true})]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/fire-event :on-press-out (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor colors/white-opa-10}))) + + (h/test "on-press event is called" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [address/view (with-defaults {:on-press on-press})]) + (h/fire-event :on-press (h/get-by-label-text :container)) + (h/was-called on-press)))) diff --git a/src/quo/components/list_items/address/schema.cljs b/src/quo/components/list_items/address/schema.cljs new file mode 100644 index 00000000000..611addad25c --- /dev/null +++ b/src/quo/components/list_items/address/schema.cljs @@ -0,0 +1,13 @@ +(ns quo.components.list-items.address.schema) + +(def ?schema + [:=> + [:cat + [:map + [:address :string] + [:networks {:optional true} [:* [:map [:network-name :keyword] [:short-name :string]]]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:on-press {:optional true} [:maybe fn?]] + [:blur? {:optional true} [:maybe :boolean]] + [:active-state? {:optional true} [:maybe :boolean]]]] + :any]) diff --git a/src/quo/components/list_items/address/style.cljs b/src/quo/components/list_items/address/style.cljs new file mode 100644 index 00000000000..ff62ba8c4f1 --- /dev/null +++ b/src/quo/components/list_items/address/style.cljs @@ -0,0 +1,35 @@ +(ns quo.components.list-items.address.style + (:require + [quo.foundations.colors :as colors])) + +(defn- background-color + [state customization-color blur?] + (cond (and (or (= state :pressed) (= state :selected)) (not blur?)) + (colors/resolve-color customization-color :light 5) + (and (or (= state :pressed) (= state :selected)) blur?) + colors/white-opa-5 + (and (= state :active) (not blur?)) + (colors/resolve-color customization-color :light 10) + (and (= state :active) blur?) + colors/white-opa-10 + (and (= state :pressed) blur?) colors/white-opa-10 + :else :transparent)) + +(defn container + [state customization-color blur?] + {:height 56 + :border-radius 12 + :background-color (background-color state customization-color blur?) + :flex-direction :row + :align-items :center + :padding-horizontal 12 + :padding-vertical 6 + :justify-content :space-between}) + +(def left-container + {:flex-direction :row + :align-items :center}) + +(def account-container + {:margin-left 8 + :padding-right 56}) diff --git a/src/quo/components/list_items/address/view.cljs b/src/quo/components/list_items/address/view.cljs new file mode 100644 index 00000000000..b483bbb74a2 --- /dev/null +++ b/src/quo/components/list_items/address/view.cljs @@ -0,0 +1,72 @@ +(ns quo.components.list-items.address.view + (:require + [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [quo.components.list-items.address.schema :as component-schema] + [quo.components.list-items.address.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.address :as address])) + +(defn- left-container + [{:keys [theme address networks blur?]}] + [rn/view {:style style/left-container} + [wallet-user-avatar/wallet-user-avatar + {:size :size-32 + :full-name "0 x" + :monospace? true + :lowercase? true + :neutral? true}] + [rn/view {:style style/account-container} + [text/text {:size :paragraph-1} + (map (fn [network] + ^{:key (str network)} + [text/text + {:size :paragraph-1 + :weight :semi-bold + :style {:color (colors/resolve-color (:network-name network) theme)}} + (str (:short-name network) ":")]) + networks) + [text/text + {:size :paragraph-1 + :weight :monospace + :style {:color (if blur? + colors/white + (colors/theme-colors colors/neutral-100 colors/white theme))}} + (address/get-shortened-key address)]]]]) + +(defn- internal-view + [{:keys [networks address customization-color on-press active-state? blur?] + :or {customization-color :blue}}] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + active? (rn/use-ref-atom false) + timer (rn/use-ref-atom nil) + on-press-in (rn/use-callback + (fn [] + (when-not (= state :selected) + (reset! timer (js/setTimeout #(set-state :pressed) 100)))) + [state]) + on-press-out (rn/use-callback + (fn [] + (let [new-state (if (or (not active-state?) @active?) :default :active)] + (when @timer (js/clearTimeout @timer)) + (reset! timer nil) + (reset! active? (= new-state :active)) + (set-state new-state))) + [active-state?])] + [rn/pressable + {:style (style/container state customization-color blur?) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :accessibility-label :container} + [left-container + {:theme theme + :networks networks + :address address + :blur? blur?}]])) + +(def view (schema/instrument #'internal-view component-schema/?schema)) diff --git a/src/quo/components/list_items/approval_info/component_spec.cljs b/src/quo/components/list_items/approval_info/component_spec.cljs new file mode 100644 index 00000000000..0533100170c --- /dev/null +++ b/src/quo/components/list_items/approval_info/component_spec.cljs @@ -0,0 +1,58 @@ +(ns quo.components.list-items.approval-info.component-spec + (:require [quo.components.list-items.approval-info.view :as approval-info] + [test-helpers.component :as h])) + +(h/describe "List Items: Approval Info" + (h/test "should render correctly with basic props" + (h/render-with-theme-provider + [approval-info/view + {:type :spending-cap + :label "Spending Cap" + :avatar-props {:image "image"}}]) + (h/is-truthy (h/get-by-label-text :approval-info))) + + (h/test "should render correctly with label & description" + (h/render-with-theme-provider + [approval-info/view + {:type :spending-cap + :label "Spending Cap" + :description "Description" + :avatar-props {:image "image"}}]) + (h/is-truthy (h/get-by-text "Spending Cap")) + (h/is-truthy (h/get-by-text "Description"))) + + (h/test "on-button-press event is called when button is pressed" + (let [on-button-press (h/mock-fn)] + (h/render-with-theme-provider + [approval-info/view + {:type :spending-cap + :label "Spending Cap" + :button-label "Edit" + :button-icon :i/options + :on-button-press on-button-press + :avatar-props {:image "image"}}]) + (h/fire-event :press (h/get-by-text "Edit")) + (h/was-called on-button-press))) + + (h/test "on-option-press event is called when option icon is pressed" + (let [on-option-press (h/mock-fn)] + (h/render-with-theme-provider + [approval-info/view + {:type :spending-cap + :label "Spending Cap" + :option-icon :i/options + :on-option-press on-option-press + :avatar-props {:image "image"}}]) + (h/fire-event :press (h/get-by-label-text :icon)) + (h/was-called on-option-press))) + + (h/test "on-avatar-press event is called when avatar is pressed" + (let [on-avatar-press (h/mock-fn)] + (h/render-with-theme-provider + [approval-info/view + {:type :spending-cap + :label "Spending Cap" + :on-avatar-press on-avatar-press + :avatar-props {:image "image"}}]) + (h/fire-event :press (h/get-by-label-text :token-avatar)) + (h/was-called on-avatar-press)))) diff --git a/src/quo/components/list_items/approval_info/style.cljs b/src/quo/components/list_items/approval_info/style.cljs new file mode 100644 index 00000000000..38e81d43e18 --- /dev/null +++ b/src/quo/components/list_items/approval_info/style.cljs @@ -0,0 +1,36 @@ +(ns quo.components.list-items.approval-info.style + (:require [quo.foundations.colors :as colors])) + +(defn icon-description-color + [blur? theme] + (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn container + [description? blur? theme] + {:flex-direction :row + :padding-horizontal 12 + :padding-vertical (if description? 8 12) + :border-radius 16 + :gap 8 + :align-items :center + :border-width 1 + :border-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) + +(def labels + {:flex 1 + :justify-content :center + :padding-right 4}) + +(defn label + [blur? theme] + {:color (if blur? + colors/white + (colors/theme-colors colors/neutral-100 colors/white theme))}) + +(defn description + [blur? theme] + {:color (icon-description-color blur? theme)}) diff --git a/src/quo/components/list_items/approval_info/view.cljs b/src/quo/components/list_items/approval_info/view.cljs new file mode 100644 index 00000000000..b9f2f775dcf --- /dev/null +++ b/src/quo/components/list_items/approval_info/view.cljs @@ -0,0 +1,124 @@ +(ns quo.components.list-items.approval-info.view + (:require [clojure.string :as string] + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.collection-avatar.view :as collection-avatar] + [quo.components.avatars.community-avatar.view :as community-avatar] + [quo.components.avatars.dapp-avatar.view :as dapp-avatar] + [quo.components.avatars.icon-avatar :as icon-avatar] + [quo.components.avatars.token-avatar.view :as token-avatar] + [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.list-items.approval-info.style :as style] + [quo.components.markdown.text :as text] + [quo.components.tags.tiny-tag.view :as tiny-tag] + quo.context + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:type + [:enum :spending-cap :token-contract :account :spending-contract :network :date-signed + :collectible :address :community :collectible-contract]] + [:avatar-props :map] + [:label :string] + [:description {:optional true} [:maybe :string]] + [:button-label {:optional true} [:maybe :string]] + [:button-icon {:optional true} [:maybe :keyword]] + [:blur? {:optional true} [:maybe :boolean]] + [:unlimited-icon? {:optional true} [:maybe :boolean]] + [:option-icon {:optional true} [:maybe :keyword]] + [:tag-label {:optional true} [:maybe :string]] + [:on-button-press {:optional true} [:maybe fn?]] + [:on-avatar-press {:optional true} [:maybe fn?]] + [:on-option-press {:optional true} [:maybe fn?]] + [:container-style {:optional true} [:maybe :any]]]]] + :any]) + +(defn- avatar + [{:keys [type avatar-props blur? theme on-press]}] + [rn/pressable {:on-press on-press} + (case type + :account [account-avatar/view (assoc avatar-props :size 32)] + :collectible-contract [collection-avatar/view (assoc avatar-props :size :size-32)] + :spending-contract [dapp-avatar/view avatar-props] + :date-signed [icon-avatar/icon-avatar + (assoc avatar-props + :size :size-32 + :opacity 10 + :color (if blur? + colors/white + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme)))] + :address [wallet-user-avatar/wallet-user-avatar + (assoc avatar-props + :size :size-32 + :monospace? true + :lowercase? true + :neutral? true)] + :community [community-avatar/view avatar-props] + [token-avatar/view + (assoc avatar-props + :type + (if (= type :collectible) :collectible :asset))])]) + +(defn- view-internal + [{:keys [type avatar-props label description blur? unlimited-icon? container-style + on-option-press on-avatar-press on-button-press button-label button-icon tag-label + option-icon]}] + (let [theme (quo.context/use-theme) + description? (not (string/blank? description))] + [rn/view + {:style (merge (style/container description? blur? theme) container-style) + :accessibility-label :approval-info} + [avatar + {:type type + :blur? blur? + :theme theme + :on-press on-avatar-press + :avatar-props avatar-props}] + [rn/view + {:style style/labels} + [rn/view + {:style {:flex-direction :row :align-items :center}} + [text/text + {:size :paragraph-1 + :weight (if (= type :address) :monospace :semi-bold) + :style (style/label blur? theme)} + label] + (when unlimited-icon? + [icon/icon :i/alert + {:container-style {:margin-left 4} + :size 16 + :color (style/icon-description-color blur? theme)}])] + (when description? + [text/text + {:size :paragraph-2 + :weight (if (contains? #{:collectible-contract :spending-contract :account :token-contract} + type) + :monospace + :regular) + :style (style/description blur? theme)} + description])] + (when (= type :account) [tiny-tag/view {:label tag-label}]) + (when (and (= type :spending-cap) button-icon) + [button/button + {:type :outline + :size 24 + :blur? blur? + :icon-left button-icon + :on-press on-button-press} + button-label]) + (when option-icon + [rn/pressable {:on-press on-option-press} + [icon/icon option-icon + {:color (style/icon-description-color blur? theme) + :size 20}]])])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/list_items/channel/component_spec.cljs b/src/quo/components/list_items/channel/component_spec.cljs new file mode 100644 index 00000000000..4dc36210eaa --- /dev/null +++ b/src/quo/components/list_items/channel/component_spec.cljs @@ -0,0 +1,45 @@ +(ns quo.components.list-items.channel.component-spec + (:require + [quo.components.list-items.channel.view :as channel] + [test-helpers.component :as h])) + +(h/describe "list-items/channel Component" + (h/test "default render" + (h/render [channel/view {:name "general"}]) + (h/is-truthy (h/query-by-label-text :channel-list-item))) + + (h/test "with name & emoji" + (h/render [channel/view + {:name "general" + :emoji "👋"}]) + (h/is-truthy (h/query-by-text "# general")) + (h/is-truthy (h/query-by-text "👋"))) + + (h/test "notification & mentions count" + (h/render [channel/view + {:name "general" + :mentions-count 10 + :notification :mention}]) + (h/is-truthy (h/query-by-text "10"))) + + (h/test "unread indicator" + (h/render [channel/view + {:name "general" + :notification :notification}]) + (h/is-truthy (h/query-by-label-text :unviewed-messages-public))) + + (h/test "on-press event" + (let [on-press (h/mock-fn)] + (h/render [channel/view + {:name "general" + :on-press on-press}]) + (h/fire-event :press (h/query-by-label-text :channel-list-item)) + (h/was-called on-press))) + + (h/test "on-long-press event" + (let [on-long-press (h/mock-fn)] + (h/render [channel/view + {:name "general" + :on-long-press on-long-press}]) + (h/fire-event :long-press (h/query-by-label-text :channel-list-item)) + (h/was-called on-long-press)))) diff --git a/src/quo/components/list_items/channel/style.cljs b/src/quo/components/list_items/channel/style.cljs new file mode 100644 index 00000000000..ca12aa7c0fd --- /dev/null +++ b/src/quo/components/list_items/channel/style.cljs @@ -0,0 +1,48 @@ +(ns quo.components.list-items.channel.style + (:require + [quo.foundations.colors :as colors])) + +(defn- get-label-color + [notification theme] + (let [colors {:notification (colors/theme-colors colors/neutral-100 + colors/white + theme) + :mention (colors/theme-colors colors/neutral-100 + colors/white + theme) + :mute (colors/theme-colors colors/neutral-40 + colors/neutral-60 + theme) + :default (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme)}] + (colors (or notification :default)))) + +(defn mute-notification-icon-color + [theme] + (colors/theme-colors colors/neutral-40 + colors/neutral-60 + theme)) + +(defn container + [pressed? customization-color theme] + {:height 48 + :border-radius 12 + :padding-horizontal 12 + :padding-vertical 8 + :align-items :center + :overflow :hidden + :background-color (if pressed? + (colors/resolve-color customization-color theme 5) + :transparent) + :flex-direction :row}) + +(defn label + [notification theme] + {:margin-horizontal 12 + :color (get-label-color notification theme) + :flex 1}) + +(defn counter + [mentions] + {:margin-horizontal (if (= (count (str mentions)) 1) 2 0)}) diff --git a/src/quo/components/list_items/channel/view.cljs b/src/quo/components/list_items/channel/view.cljs new file mode 100644 index 00000000000..af18944f5d0 --- /dev/null +++ b/src/quo/components/list_items/channel/view.cljs @@ -0,0 +1,62 @@ +(ns quo.components.list-items.channel.view + (:require + [quo.components.avatars.channel-avatar.view :as channel-avatar] + [quo.components.counter.counter.view :as counter] + [quo.components.icon :as quo.icons] + [quo.components.list-items.channel.style :as style] + [quo.components.markdown.text :as quo.text] + [quo.context] + [react-native.core :as rn])) + +(defn view + "Options: + - notification - (nil/:notification/:mention/:mute, default: nil): + - :notification - Display a grey dot. + - :mention - Display a counter. + - :mute - Display a mute icon. + - locked? - (nil/boolean, default: nil): + - When true, display a locked icon. + - When false, display an unlocked icon. + - mentions-count - (default: nil) - Number of mentions to display in the counter with :mention notification. + - customization-color - (default: nil) - Community color. + - emoji - (string, default: nil): + - Emoji to be displayed on the channel avatar. + - If blank, initials of the channel name are displayed. + - name - (string, default: nil) - Channel name. + - on-press - (function, default: nil) - Function called when the component is pressed. + - on-long-press - (function, default: nil) - Function called when the component is long pressed. + - theme - Theme value from with-theme HOC" + [{:keys [notification locked? mentions-count customization-color emoji name on-press on-long-press]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/pressable + {:style (style/container pressed? customization-color theme) + :accessibility-label :channel-list-item + :on-press on-press + :on-long-press on-long-press + :on-press-in on-press-in + :on-press-out on-press-out} + [channel-avatar/view + {:size :size-32 + :locked? locked? + :full-name name + :customization-color customization-color + :emoji emoji}] + [quo.text/text + {:style (style/label notification theme) + :weight :medium + :size :paragraph-1} (str "# " name)] + (when-not locked? + (condp = notification + :mute [quo.icons/icon :i/muted + {:color (style/mute-notification-icon-color theme)}] + :mention [counter/view + {:customization-color customization-color + :container-style (style/counter mentions-count)} + mentions-count] + :notification [quo.icons/icon :i/notification + {:color (style/mute-notification-icon-color theme) + :accessibility-label :unviewed-messages-public}] + nil))])) diff --git a/src/quo/components/list_items/community/component_spec.cljs b/src/quo/components/list_items/community/component_spec.cljs new file mode 100644 index 00000000000..2f33596926f --- /dev/null +++ b/src/quo/components/list_items/community/component_spec.cljs @@ -0,0 +1,71 @@ +(ns quo.components.list-items.community.component-spec + (:require + [quo.components.list-items.community.view :as component] + [test-helpers.component :as h])) + +(h/describe "Community list" + (h/test "default render" + (h/render [component/view {}]) + (h/is-truthy (h/get-by-label-text :community-item-title)) + (h/is-null (h/query-by-label-text :community-item-info))) + + (h/describe "type share" + (h/test "renders" + (h/render [component/view + {:type :share + :title "Title" + :subtitle "Subtitle"}]) + (h/is-truthy (h/get-by-text "# Title")) + (h/is-truthy (h/get-by-text "Subtitle")) + (h/is-null (h/query-by-label-text :community-item-info)))) + + (h/describe "type engage" + (h/test "default, no info" + (h/render [component/view {:type :engage}]) + (h/is-null (h/query-by-label-text :community-item-info))) + + (h/test "info is :navigation" + (h/render [component/view {:type :engage :info :navigation}]) + (h/is-truthy (h/get-by-label-text :info-navigation))) + + (h/test "info is :token-gated" + (h/render [component/view {:type :engage :info :token-gated}]) + (h/is-truthy (h/get-by-label-text :info-token-gated))) + + (h/test "info is :muted" + (h/render [component/view {:type :engage :info :muted}]) + (h/is-truthy (h/get-by-label-text :info-muted))) + + (h/test "info is :notification" + (h/render [component/view {:type :engage :info :notification}]) + (h/is-truthy (h/get-by-label-text :info-notification-dot))) + + (h/test "info is :mention" + (h/render [component/view + {:type :engage + :info :mention + :unread-count 9000}]) + (h/is-truthy (h/get-by-text "99+")))) + + (h/describe "type discover" + (h/test "default, no info, no member stats" + (h/render [component/view {:type :discover}]) + (h/is-null (h/query-by-label-text :stats-members-count)) + (h/is-null (h/query-by-label-text :community-item-info))) + + (h/test "info is :token-gated, show member stats" + (let [on-press-info (h/mock-fn)] + (h/render [component/view + {:type :discover + :info :token-gated + :locked? true + :blur? false + :on-press-info on-press-info + :members {:members-count 9876 :active-count 1350} + :tokens [{:id 1 :group [{:id 1}]}]}]) + (h/is-truthy (h/get-by-label-text :community-item-info)) + (h/is-truthy (h/get-by-label-text :permission-tag)) + (h/is-truthy (h/get-by-label-text :stats-members-count)) + (h/is-truthy (h/get-by-label-text :stats-active-count)) + (h/fire-event :press (h/get-by-label-text :permission-tag)) + (h/was-called on-press-info))))) diff --git a/src/quo/components/list_items/community/style.cljs b/src/quo/components/list_items/community/style.cljs new file mode 100644 index 00000000000..255f0e773d6 --- /dev/null +++ b/src/quo/components/list_items/community/style.cljs @@ -0,0 +1,66 @@ +(ns quo.components.list-items.community.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows])) + +(def logo + {:border-radius 50 + :border-width 0 + :width 32 + :height 32 + :margin-right 10}) + +(defn notification-dot + [blur? theme] + {:width 8 + :height 8 + :border-radius 4 + :background-color (if (and (= :dark theme) blur?) + colors/white-opa-40 + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme))}) + +(defn title + [{:keys [type info blur? theme]}] + {:color (cond + (and (= type :engage) (= info :muted)) + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + + (and (= type :engage) (= info :default) (not blur?)) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + + (and (= type :engage) (= info :default) (= :dark theme) blur?) + colors/white-70-blur + + :else + (colors/theme-colors colors/neutral-100 colors/white theme))}) + +(defn subtitle + [blur? theme] + {:color (if (and (= :dark theme) blur?) + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(defn container + [{:keys [type pressed? blur? customization-color theme info]}] + (merge {:padding-vertical 8 + :padding-horizontal 12 + :border-radius 12 + :flex-direction :row + :align-items :center + :background-color (cond + (and pressed? (= type :engage) (= info :default) (= :dark theme) blur?) + colors/white-opa-5 + + (and pressed? (#{:engage :share} type)) + (colors/resolve-color customization-color theme 5) + + (and (not pressed?) (= type :discover) (not blur?)) + (colors/theme-colors colors/white colors/neutral-90 theme) + + (and (not pressed?) (= type :discover) (= :dark theme) blur?) + colors/white-opa-5 + + (and pressed? (= type :discover)) + (colors/theme-colors colors/white :transparent theme))} + (when (and (= type :discover) (not pressed?)) + (shadows/get 3 theme)))) diff --git a/src/quo/components/list_items/community/view.cljs b/src/quo/components/list_items/community/view.cljs new file mode 100644 index 00000000000..1d5ba4db1ae --- /dev/null +++ b/src/quo/components/list_items/community/view.cljs @@ -0,0 +1,162 @@ +(ns quo.components.list-items.community.view + (:require + [quo.components.community.community-view :as community-view] + [quo.components.counter.counter.view :as counter] + [quo.components.icon :as icons] + [quo.components.list-items.community.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- logo-component + [logo] + [rn/image {:source logo :style style/logo}]) + +(defn- title-component + [{:keys [title type] :as props}] + [text/text + {:weight :semi-bold + :size :paragraph-1 + :accessibility-label :community-item-title + :number-of-lines 1 + :ellipsize-mode :tail + :style (style/title props)} + (if (= type :share) + (str "# " title) + title)]) + +(defn- subtitle-component + [subtitle blur? theme] + [text/text + {:size :paragraph-2 + :number-of-lines 1 + :accessibility-label :community-item-subtitle + :style (style/subtitle blur? theme)} + subtitle]) + +(defn- notification-dot + [blur? theme] + [rn/view + {:style (style/notification-dot blur? theme) + :accessibility-label :info-notification-dot}]) + +(defn- info-component + [{:keys [customization-color info type blur? locked? on-press-info theme tokens unread-count]}] + (let [component + (cond + (and (= type :discover) (= info :token-gated) (seq tokens)) + [community-view/permission-tag-container + {:locked? locked? + :tokens tokens + :on-press on-press-info + :theme theme + :blur? blur?}] + + (and (= type :engage) (= info :mention) (pos? unread-count)) + [counter/view + {:type :default + :customization-color customization-color} + unread-count] + + (and (= type :engage) (= info :notification)) + [notification-dot blur? theme] + + (and (= type :engage) (= info :muted)) + [icons/icon :i/muted + {:accessibility-label :info-muted + :color (colors/theme-colors colors/neutral-40 colors/neutral-60 theme)}] + + (and (= type :engage) (= info :token-gated)) + [icons/icon (if locked? :i/locked :i/unlocked) + {:accessibility-label :info-token-gated + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}] + + (and (= type :engage) (= info :navigation)) + [icons/icon :i/chevron-right + {:accessibility-label :info-navigation + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}])] + (when component + [rn/view + {:accessibility-label :community-item-info + :style {:margin-left 10}} + component]))) + +(defn view + "Options: + + :type - :discover/engage/share + :info - keyword - Acceptable values vary based on the :type option. + + :title - string + :logo - image resource + :theme - :light/dark + + :container-style - style map - Override styles in top-level view component. + + :members - {:members-count number, :active-count number} - When non-nil, the + statistics component will be shown. + + :tokens - A sequence of maps, e.g. [{:id 1 :group [{:id 1 :token-icon an-icon}]}]. + + :locked? - boolean - When true, the permission-tag icon or the token gated + icon will appear as closed. + + :blur? - boolean - It will be taken into account when true and dark mode is + enabled. + + :customization-color - color - It will be passed down to components that + should vary based on a custom color. + + :on-press/:on-long-press - fn - Used by the top-level pressable component. + + :on-press-info - fn - Will be called when the info component is pressed. + + :unread-count - number - When the info is :mention, it will be used to show + the number of unread mentions. + " + [{:keys [members type info tokens locked? title subtitle logo blur? customization-color + on-press on-long-press on-press-info container-style unread-count]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/pressable + {:accessibility-label :container + + :on-press on-press + :on-long-press on-long-press + :on-press-in on-press-in + :on-press-out on-press-out + :style (merge (style/container {:blur? blur? + :customization-color customization-color + :info info + :type type + :pressed? pressed? + :theme theme}) + container-style)} + [logo-component logo] + [rn/view {:style {:flex 1}} + [title-component + {:blur? blur? + :info info + :theme theme + :title title + :type type}] + (when (and (= type :share) subtitle) + [subtitle-component subtitle blur? theme]) + (when (and members (= type :discover)) + [community-view/community-stats-column + {:type :list-view + :members-count (:members-count members) + :active-count (:active-count members)}])] + [info-component + {:blur? blur? + :customization-color customization-color + :info info + :type type + :locked? locked? + :on-press-info on-press-info + :theme theme + :tokens tokens + :unread-count unread-count}]])) diff --git a/src/quo/components/list_items/dapp/component_spec.cljs b/src/quo/components/list_items/dapp/component_spec.cljs new file mode 100644 index 00000000000..d2fb420a59e --- /dev/null +++ b/src/quo/components/list_items/dapp/component_spec.cljs @@ -0,0 +1,25 @@ +(ns quo.components.list-items.dapp.component-spec + (:require + [quo.components.list-items.dapp.view :as dapp] + [test-helpers.component :as h])) + +(def props + {:dapp {:avatar nil + :name "Coingecko" + :value "coingecko.com"} + :accessibility-label "dapp-coingecko" + :state :default + :blur? false + :customization-color :blue}) + +(h/describe + "dApp component test" + (h/test "default render" + (h/render [dapp/view props]) + (h/is-truthy (h/get-by-text (:name (:dapp props)))) + (h/is-truthy (h/get-by-text (:value (:dapp props))))) + (h/test "on-press event" + (let [mock-fn (h/mock-fn)] + (h/render [dapp/view (assoc props :on-press mock-fn)]) + (h/fire-event :press (h/get-by-label-text "dapp-coingecko")) + (h/was-called mock-fn)))) diff --git a/src/quo/components/list_items/dapp/style.cljs b/src/quo/components/list_items/dapp/style.cljs new file mode 100644 index 00000000000..0853a33b504 --- /dev/null +++ b/src/quo/components/list_items/dapp/style.cljs @@ -0,0 +1,58 @@ +(ns quo.components.list-items.dapp.style + (:require + [quo.foundations.colors :as colors])) + +(defn get-background-color + [{:keys [pressed? state blur? customization-color theme]}] + (cond + (and pressed? (= theme :dark) blur?) colors/white-opa-5 + + pressed? (colors/theme-colors + (colors/override-color customization-color 5 50) + (colors/override-color customization-color 5 60) + theme) + + (and (= state :active) (= theme :dark) blur?) colors/white-opa-10 + + (= state :active) (colors/theme-colors + (colors/override-color customization-color 10 50) + (colors/override-color customization-color 10 60) + theme) + + :else :transparent)) + +(defn container + [props] + {:flex 1 + :padding-horizontal 12 + :padding-vertical 8 + :border-radius 12 + :background-color (get-background-color props) + :flex-direction :row + :justify-content :space-between + :align-items :center}) + +(def container-info + {:flex-direction :row + :align-items :center + :flex 1}) + +(def user-info + {:margin-left 8}) + +(defn style-text-name + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn style-text-value + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/white theme)}) + +(def initials-avatar-container + {:width 32 + :height 32}) + +(def image-avatar + {:width 32 + :height 32 + :border-radius 32}) diff --git a/src/quo/components/list_items/dapp/view.cljs b/src/quo/components/list_items/dapp/view.cljs new file mode 100644 index 00000000000..d64ec667bac --- /dev/null +++ b/src/quo/components/list_items/dapp/view.cljs @@ -0,0 +1,47 @@ +(ns quo.components.list-items.dapp.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.list-items.dapp.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn view + [{:keys [dapp on-press right-component accessibility-label] :as props}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/pressable + {:style (style/container (assoc props :pressed? pressed?)) + :accessibility-label accessibility-label + :on-press (when on-press + (fn [] (on-press dapp))) + :on-press-in on-press-in + :on-press-out on-press-out} + [rn/view {:style style/container-info} + (if (:avatar dapp) + [fast-image/fast-image + {:source (:avatar dapp) + :style style/image-avatar}] + [rn/view {:style style/initials-avatar-container} + [user-avatar/initials-avatar + {:full-name (:name dapp) + :size :small + :customization-color (:customization-color dapp)}]]) + [rn/view {:style style/user-info} + [text/text + {:weight :semi-bold + :size :paragraph-1 + :number-of-lines 1 + :style (style/style-text-name theme)} + (:name dapp)] + [text/text + {:weight :regular + :size :paragraph-2 + :number-of-lines 1 + :style (style/style-text-value theme)} + (:value dapp)]]] + (when right-component + [right-component dapp])])) diff --git a/src/quo/components/list_items/market_token/component_spec.cljs b/src/quo/components/list_items/market_token/component_spec.cljs new file mode 100644 index 00000000000..6bf762a9e9d --- /dev/null +++ b/src/quo/components/list_items/market_token/component_spec.cljs @@ -0,0 +1,20 @@ +(ns quo.components.list-items.market-token.component-spec + (:require + [quo.components.list-items.market-token.view :as market-token] + [test-helpers.component :as h])) + +(h/describe "List Items: Market token" + (h/test "Render market token" + (h/render-with-theme-provider [market-token/view + {:token :snt + :token-rank 10 + :token-name "Status" + :market-cap "$332.1B" + :price "$0.8" + :percentage-change 5.2 + :customization-color :blue}]) + (h/is-truthy (h/get-by-text "Status"))) + + (h/test "Render without props" + (h/render-with-theme-provider [market-token/view]) + (h/is-truthy (h/get-by-label-text :market-token-container)))) diff --git a/src/quo/components/list_items/market_token/schema.cljs b/src/quo/components/list_items/market_token/schema.cljs new file mode 100644 index 00000000000..2c9410ac3b8 --- /dev/null +++ b/src/quo/components/list_items/market_token/schema.cljs @@ -0,0 +1,17 @@ +(ns quo.components.list-items.market-token.schema) + +(def ?schema + [:=> + [:cat + [:map + [:token-name :string] + [:token-short-name :string] + [:token-image {:optional true} :string] + [:token-rank :int] + [:market-cap :string] + [:price :string] + [:percentage-change :double] + [:on-press {:optional true} [:maybe fn?]] + [:on-long-press {:optional true} [:maybe fn?]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]] + :any]) diff --git a/src/quo/components/list_items/market_token/style.cljs b/src/quo/components/list_items/market_token/style.cljs new file mode 100644 index 00000000000..2cc2116818a --- /dev/null +++ b/src/quo/components/list_items/market_token/style.cljs @@ -0,0 +1,56 @@ +(ns quo.components.list-items.market-token.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [color bg-opacity theme] + {:height 56 + :padding-horizontal 12 + :padding-vertical 8 + :border-radius 12 + :flex-direction :row + :justify-content :space-between + :background-color (colors/resolve-color color theme bg-opacity)}) + +(defn percentage-text + [percentage-change theme] + {:color (if (pos? percentage-change) + (colors/theme-colors colors/success-50 colors/success-60 theme) + (colors/theme-colors colors/danger-50 colors/danger-60 theme))}) + +(defn arrow-icon + [percentage-change theme] + {:size 16 + :color (if (pos? percentage-change) + (colors/resolve-color :success theme) + (colors/resolve-color :danger theme))}) + +(def left-side + {:flex-direction :row + :align-items :center + :flex 1}) + +(def right-side + {:align-items :flex-end + :justify-content :space-between}) + +(def percentage-container + {:flex-direction :row + :align-items :center}) + +(def token-name + {:weight :semi-bold}) + +(defn token-short-name + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def arrow + {:margin-left 4}) + +(def left-text-block + {:margin-left 8}) + +(defn market-cap + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo/components/list_items/market_token/view.cljs b/src/quo/components/list_items/market_token/view.cljs new file mode 100644 index 00000000000..88d99368bec --- /dev/null +++ b/src/quo/components/list_items/market_token/view.cljs @@ -0,0 +1,79 @@ +(ns quo.components.list-items.market-token.view + (:require + [quo.components.counter.counter.view :as counter] + [quo.components.icon :as icon] + [quo.components.list-items.market-token.schema :as component-schema] + [quo.components.list-items.market-token.style :as style] + [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- internal-view + [{:keys [token-short-name token-image token-name token-rank market-cap price percentage-change + customization-color + on-press on-long-press]}] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + bg-opacity (case state + :active 10 + :pressed 5 + 0) + on-press-in (rn/use-callback #(set-state :pressed)) + on-press-out (rn/use-callback #(set-state :default)) + on-press (rn/use-callback + (fn [] + (set-state :active) + (js/setTimeout #(set-state :default) 300) + (when on-press + (on-press))))] + [rn/pressable + {:style (style/container customization-color bg-opacity theme) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :on-long-press on-long-press + :accessibility-label :market-token-container} + [rn/view + {:style style/left-side} + [counter/view + {:type :outline + :max-value 99999 + :container-style {:margin-right 8}} + token-rank] + [token/view {:image-source {:uri token-image} :size :size-32}] + [rn/view style/left-text-block + [rn/text + ;; If we place 2 texts of different styles within parent _view_ component they won't be + ;; aligned to the same baseline. So they should be a children of the same _text_ component + ;; to share same basline and look nice. But that means we can't use precise margins anymore + ;; and have to rely on whitespaces to split 2 texts. + [text/text style/token-name (str token-name " ")] + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/token-short-name theme)} token-short-name]] + [text/text + {:size :paragraph-2 + :style (style/market-cap theme)} + market-cap]]] + [rn/view + {:style style/right-side} + [text/text + {:weight :medium + :size :paragraph-2} price] + (when percentage-change + [rn/view + {:style style/percentage-container} + [text/text + {:size :paragraph-2 + :style (style/percentage-text percentage-change theme)} + (str percentage-change "%")] + [rn/view + {:style style/arrow + :accessibility-label :arrow-icon} + [icon/icon (if (pos? percentage-change) :i/positive :i/negative) + (style/arrow-icon percentage-change theme)]]])]])) + +(def view (schema/instrument #'internal-view component-schema/?schema)) diff --git a/src/quo/components/list_items/menu_item.cljs b/src/quo/components/list_items/menu_item.cljs new file mode 100644 index 00000000000..b687fcb920d --- /dev/null +++ b/src/quo/components/list_items/menu_item.cljs @@ -0,0 +1,63 @@ +(ns quo.components.list-items.menu-item + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors :refer [theme-colors]] + [react-native.core :as rn])) + +(defn themes + [type theme] + (case type + :main {:icon-color (theme-colors colors/neutral-50 colors/neutral-40 theme) + :background (theme-colors colors/white colors/neutral-95 theme) + :text-color (theme-colors colors/neutral-100 colors/white theme)} + :danger {:icon-color (theme-colors colors/danger-50 colors/danger-60 theme) + :background (theme-colors colors/white colors/neutral-95 theme) + :text-color (theme-colors colors/danger-50 colors/danger-60 theme)} + :transparent {:icon-color (theme-colors colors/neutral-50 colors/neutral-10 theme) + :text-color (theme-colors colors/neutral-100 colors/white theme)})) + +(defn menu-item + [{:keys [type title accessibility-label icon on-press style-props subtitle subtitle-color] + :or {type :main}}] + (let [theme (quo.context/use-theme) + {:keys [icon-color text-color background]} (themes type theme)] + [rn/touchable-opacity + (merge {:accessibility-label accessibility-label + :style (merge style-props + {:background-color background + :height 48 + :flex-direction :row + :align-items :center})} + (when on-press + {:on-press on-press})) + [rn/view + {:style (cond-> {:flex-direction :row + :flex-grow 0 + :flex-shrink 1 + :align-items :center} + icon (assoc :padding-horizontal 20))} + [rn/view + {:style (cond-> {:width 20 + :height 20 + :align-items :center + :justify-content :center} + icon (assoc :margin-right 12))} + (when icon [icons/icon icon {:color icon-color}])] + [rn/view + [text/text + {:weight :medium + :style {:color text-color} + :ellipsize-mode :tail + :number-of-lines 1 + :size :paragraph-1} + title] + (when subtitle + [text/text + {:weight :medium + :style {:color subtitle-color} + :ellipsize-mode :tail + :number-of-lines 1 + :size :paragraph-1} + subtitle])]]])) diff --git a/src/quo/components/list_items/missing_keypair/component_spec.cljs b/src/quo/components/list_items/missing_keypair/component_spec.cljs new file mode 100644 index 00000000000..f209816f3ff --- /dev/null +++ b/src/quo/components/list_items/missing_keypair/component_spec.cljs @@ -0,0 +1,30 @@ +(ns quo.components.list-items.missing-keypair.component-spec + (:require + [quo.components.list-items.missing-keypair.view :as missing-keypair] + [test-helpers.component :as h])) + +(def keypair-data + {:accounts [] + :name "Key Pair Name"}) + +(def props + {:keypair keypair-data + :blur? true + :on-options-press (fn [])}) + +(h/describe "List items: missing keypair item" + (h/test "Test item container renders" + (h/render-with-theme-provider [missing-keypair/view props]) + (h/is-truthy (h/get-by-label-text :missing-keypair-item))) + (h/test "Test keypair icon renders" + (h/render-with-theme-provider [missing-keypair/view props]) + (h/is-truthy (h/get-by-label-text :icon))) + (h/test "Test name renders" + (h/render-with-theme-provider [missing-keypair/view props]) + (h/is-truthy (h/get-by-label-text :name))) + (h/test "Test preview-list renders" + (h/render-with-theme-provider [missing-keypair/view props]) + (h/is-truthy (h/get-by-label-text :preview-list))) + (h/test "Test options button renders" + (h/render-with-theme-provider [missing-keypair/view props]) + (h/is-truthy (h/get-by-label-text :options-button)))) diff --git a/src/quo/components/list_items/missing_keypair/schema.cljs b/src/quo/components/list_items/missing_keypair/schema.cljs new file mode 100644 index 00000000000..bc86c4c0635 --- /dev/null +++ b/src/quo/components/list_items/missing_keypair/schema.cljs @@ -0,0 +1,25 @@ +(ns quo.components.list-items.missing-keypair.schema) + +(def ^:private ?base + [:map + [:blur? {:optional true} [:maybe :boolean]] + [:keypair + [:map + [:key-uid :string] + [:name :string] + [:accounts + [:sequential + [:map {:closed true} + [:type [:enum :default]] + [:emoji :string] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]]]]]) + +(def ^:private ?on-option-press + [:map + [:on-options-press {:optional true} [:maybe fn?]]]) + +(def ?schema + [:=> + [:cat + [:merge ?base ?on-option-press]] + :any]) diff --git a/src/quo/components/list_items/missing_keypair/style.cljs b/src/quo/components/list_items/missing_keypair/style.cljs new file mode 100644 index 00000000000..d4925ad7e7a --- /dev/null +++ b/src/quo/components/list_items/missing_keypair/style.cljs @@ -0,0 +1,36 @@ +(ns quo.components.list-items.missing-keypair.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [{:keys [blur? theme]}] + {:flex-direction :row + :align-items :center + :flex 1 + :padding-right 12 + :padding-left 8 + :padding-vertical 8 + :border-radius 12 + :border-width (if blur? 0 1) + :border-color (colors/theme-colors colors/neutral-10 + colors/neutral-80 + theme) + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-2_5 + colors/neutral-80-opa-40 + theme))}) + +(def name-container + {:flex 1 + :padding-right 16 + :padding-left 8}) + +(def preview-list-container + {:padding-right 16}) + +(defn options-icon-color + [{:keys [theme blur?]}] + (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) diff --git a/src/quo/components/list_items/missing_keypair/view.cljs b/src/quo/components/list_items/missing_keypair/view.cljs new file mode 100644 index 00000000000..0c926ed5013 --- /dev/null +++ b/src/quo/components/list_items/missing_keypair/view.cljs @@ -0,0 +1,55 @@ +(ns quo.components.list-items.missing-keypair.view + (:require + [quo.components.avatars.icon-avatar :as icon-avatar] + [quo.components.icon :as icon] + [quo.components.list-items.missing-keypair.schema :as component-schema] + [quo.components.list-items.missing-keypair.style :as style] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- internal-view + [{{:keys [accounts name type]} :keypair + :keys [keypair blur? on-options-press]}] + (let [theme (quo.context/use-theme) + on-keypair-options-press (rn/use-callback + (fn [event] + (on-options-press event keypair)) + [keypair on-options-press])] + [rn/view + {:style (style/container {:theme theme + :blur? blur?}) + :accessibility-label :missing-keypair-item} + [icon-avatar/icon-avatar + {:size :size-32 + :icon (case type + :seed :i/seed + :key :i/password + nil) + :blur? true + :border? true}] + [rn/view + {:style style/name-container + :accessibility-label :name} + [text/text + {:weight :semi-bold} + name]] + [rn/view + {:accessibility-label :preview-list} + [preview-list/view + {:blur? blur? + :type :accounts + :size :size-24 + :number (count accounts) + :container-style style/preview-list-container} + accounts]] + [rn/pressable {:on-press on-keypair-options-press} + [icon/icon :i/options + {:color (style/options-icon-color + {:theme theme + :blur? blur?}) + :accessibility-label :options-button}]]])) + +(def view (schema/instrument #'internal-view component-schema/?schema)) diff --git a/src/quo/components/list_items/network_list/component_spec.cljs b/src/quo/components/list_items/network_list/component_spec.cljs new file mode 100644 index 00000000000..24c2b149a05 --- /dev/null +++ b/src/quo/components/list_items/network_list/component_spec.cljs @@ -0,0 +1,39 @@ +(ns quo.components.list-items.network-list.component-spec + (:require [quo.components.list-items.network-list.view :as network-list] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(defn- render + [component] + (h/render-with-theme-provider component :light)) + +(def props + {:state :default + :label "Ethereum" + :network-image 873 + :customization-color :blue + :token-value "100.00 ETH" + :fiat-value "€100.00"}) + + +(h/describe "List items: Network List" + (h/test "default state explicit" + (render [network-list/view props]) + (h/is-truthy (h/get-by-text "Ethereum"))) + + (h/test "Pressed state" + (render [network-list/view props]) + (h/fire-event :on-press-in (h/get-by-label-text ::network-list)) + (h/wait-for #(h/has-style (h/query-by-label-text ::network-list) + {:backgroundColor (colors/resolve-color :blue :light 5)}))) + + (h/test "Active state" + (render [network-list/view (assoc props :state :active)]) + (h/has-style (h/query-by-label-text ::network-list) + {:backgroundColor (colors/resolve-color :blue :light 10)})) + + (h/test "Call on-press" + (let [on-press (h/mock-fn)] + (render [network-list/view (assoc props :on-press on-press)]) + (h/fire-event :on-press (h/get-by-label-text ::network-list)) + (h/was-called on-press)))) diff --git a/src/quo/components/list_items/network_list/style.cljs b/src/quo/components/list_items/network_list/style.cljs new file mode 100644 index 00000000000..4fe65292855 --- /dev/null +++ b/src/quo/components/list_items/network_list/style.cljs @@ -0,0 +1,42 @@ +(ns quo.components.list-items.network-list.style + (:require [quo.foundations.colors :as colors])) + +(defn- background-color + [state customization-color theme] + (case state + :pressed (colors/resolve-color customization-color theme 5) + :active (colors/resolve-color customization-color theme 10) + :transparent)) + +(defn container + [state customization-color theme] + {:flex-direction :row + :justify-content :space-between + :align-items :center + :padding-horizontal 12 + :padding-vertical 8 + :border-radius 12 + :height 56 + :opacity (if (= state :disabled) 0.3 1) + :background-color (background-color state customization-color theme)}) + +(def info + {:flex-direction :row + :align-items :center + :gap 2}) + +(def network-image + {:border-width 1 + :border-radius 16 + :border-color colors/neutral-80-opa-5 + :margin-right 8 + :background-color colors/neutral-80-opa-5 + :height 32 + :width 32}) + +(def values-container + {:align-items :flex-end}) + +(defn fiat-value + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo/components/list_items/network_list/view.cljs b/src/quo/components/list_items/network_list/view.cljs new file mode 100644 index 00000000000..41ac5f0fd62 --- /dev/null +++ b/src/quo/components/list_items/network_list/view.cljs @@ -0,0 +1,75 @@ +(ns quo.components.list-items.network-list.view + (:require + [clojure.string :as string] + [quo.components.icon :as icon] + [quo.components.list-items.network-list.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- info + [{:keys [network-image label]}] + [rn/view {:style style/info} + [rn/image + {:source network-image + :style style/network-image}] + [rn/view + [text/text + {:weight :semi-bold + :style {:text-transform :capitalize} + :number-of-lines 1} + (if (string/blank? label) "-" label)]]]) + +(defn- values + [{:keys [token-value fiat-value]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/values-container} + [text/text + {:weight :medium + :size :paragraph-2 + :number-of-lines 1} + token-value] + [text/text + {:style (style/fiat-value theme) + :size :paragraph-2 + :number-of-lines 1} + fiat-value]])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:network-image :int] + [:label :string] + [:fiat-value {:optional true} [:maybe :string]] + [:token-value {:optional true} [:maybe :string]] + [:container-style {:optional true} [:maybe :map]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:state {:optional true} [:enum :pressed :active :disabled :default]] + [:on-press {:optional true} [:maybe fn?]]]]] + :any]) + +(defn- view-internal + [{:keys [fiat-value token-value on-press container-style state customization-color] + :as props + :or {customization-color :blue}}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false)) + internal-state (if pressed? :pressed state)] + [rn/pressable + {:style (merge (style/container internal-state customization-color theme) + container-style) + :on-press-in (when-not (= state :disabled) on-press-in) + :on-press-out (when-not (= state :disabled) on-press-out) + :on-press (when-not (= state :disabled) on-press) + :accessibility-label :network-list} + [info props] + (if (or token-value fiat-value) + [values props] + [icon/icon :i/chevron-right])])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/list_items/preview_list/properties.cljs b/src/quo/components/list_items/preview_list/properties.cljs new file mode 100644 index 00000000000..1502b3936bb --- /dev/null +++ b/src/quo/components/list_items/preview_list/properties.cljs @@ -0,0 +1,49 @@ +(ns quo.components.list-items.preview-list.properties) + +(def ^:private types-for-squared-border #{:accounts :collectibles}) + +(defn border-type + [type] + (if (types-for-squared-border type) :squared :rounded)) + +(def sizes + {:size-32 {:size 32 + :user-avatar-size :small + :border-radius {:rounded 16 :squared 10} + :hole-radius {:rounded 18 :squared 12} + :margin-left -8 + :hole-size 36 + :hole-x 22 + :hole-y -2} + :size-24 {:size 24 + :user-avatar-size :xs + :border-radius {:rounded 12 :squared 8} + :hole-radius {:rounded 13 :squared 9} + :margin-left -4 + :hole-size 26 + :hole-x 19 + :hole-y -1} + :size-20 {:size 20 + :user-avatar-size :xxs + :border-radius {:rounded 10 :squared 8} + :hole-radius {:rounded 11 :squared 9} + :margin-left -4 + :hole-size 22 + :hole-x 15 + :hole-y -1} + :size-16 {:size 16 + :user-avatar-size :xxxs + :border-radius {:rounded 8 :squared 8} + :hole-radius {:rounded 9 :squared 9} + :margin-left -4 + :hole-size 18 + :hole-x 11 + :hole-y -1} + :size-14 {:size 14 + :user-avatar-size :xxxs + :border-radius {:rounded 7 :squared 7} + :hole-radius {:rounded 8 :squared 8} + :margin-left -2 + :hole-size 16 + :hole-x 11 + :hole-y -1}}) diff --git a/src/quo/components/list_items/preview_list/view.cljs b/src/quo/components/list_items/preview_list/view.cljs new file mode 100644 index 00000000000..51db5313cda --- /dev/null +++ b/src/quo/components/list_items/preview_list/view.cljs @@ -0,0 +1,97 @@ +(ns quo.components.list-items.preview-list.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.list-items.preview-list.properties :as properties] + [quo.components.tags.number-tag.view :as number-tag] + [quo.components.utilities.token.view :as token] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.hole-view :as hole-view])) + +(defn- preview-item + [{:keys [item type size-key]}] + (let [size (get-in properties/sizes [size-key :size]) + user-avatar-size (get-in properties/sizes [size-key :user-avatar-size]) + border-radius (get-in properties/sizes + [size-key :border-radius (properties/border-type type)])] + (case type + :user [user-avatar/user-avatar + (assoc item + :status-indicator? false + :size user-avatar-size)] + + :accounts [account-avatar/view + (assoc item :size size)] + + (:communities :collectibles) [fast-image/fast-image + {:source (or (:source item) item) + :style {:width size + :height size + :border-radius border-radius}}] + + :tokens [token/view + {:token item + :size size + :style {:border-radius border-radius}}] + + (:network :dapps) [fast-image/fast-image + {:source (or (:source item) item) + :style {:width size + :height size + :border-radius border-radius}}] + nil))) + +(defn- list-item + [{:keys [index type size-key item number]}] + (let [last-item? (= index (dec number)) + border-type (properties/border-type type) + {margin-left :margin-left + hole-size :hole-size + hole-x :hole-x + hole-y :hole-y + {hole-radius border-type} :hole-radius} (properties/sizes size-key)] + [hole-view/hole-view + {:style {:margin-left (if (= index 0) 0 margin-left)} + :holes (if last-item? + [] + [{:x hole-x + :y hole-y + :width hole-size + :height hole-size + :borderRadius hole-radius}])} + [preview-item + {:item item + :type type + :size-key size-key}]])) + +(defn view + "[preview-list opts items] + opts + {:type :user/:communities/:accounts/:tokens/:collectibles/:dapps/:network + :size :size-32 | :size-24 | :size-20 | :size-16 | :size-14 + :number number of items in the list (optional) + :blur? overflow-label blur?} + items preview list items (only 4 items is required for preview) + " + [{:keys [type size number blur? container-style]} items] + (let [size-key (if (contains? properties/sizes size) size :size-24) + number (or number (count items)) + border-type (properties/border-type type) + margin-left (get-in properties/sizes [size-key :margin-left])] + [rn/view {:style (assoc container-style :flex-direction :row)} + (for [index (range (if (> number 4) 3 number))] + ^{:key (str index number)} + [list-item + {:index index + :type type + :size-key size-key + :item (get (vec items) index) + :number number}]) + (when (> number 4) + [number-tag/view + {:type border-type + :number (str (- number 3)) + :size size-key + :blur? blur? + :container-style {:margin-left margin-left}}])])) diff --git a/src/quo/components/list_items/quiz_item/component_spec.cljs b/src/quo/components/list_items/quiz_item/component_spec.cljs new file mode 100644 index 00000000000..6429f24b226 --- /dev/null +++ b/src/quo/components/list_items/quiz_item/component_spec.cljs @@ -0,0 +1,24 @@ +(ns quo.components.list-items.quiz-item.component-spec + (:require + [quo.components.list-items.quiz-item.view :as quiz-item] + [test-helpers.component :as h])) + +(h/describe "List Items: Token Value" + (h/test "Number label renders" + (h/render [quiz-item/view + {:state :empty + :word "collapse" + :number 2}]) + (h/is-truthy (h/get-by-label-text :number-container))) + (h/test "Success icon renders" + (h/render [quiz-item/view + {:state :success + :word "collapse" + :number 2}]) + (h/is-truthy (h/get-by-label-text :success-icon))) + (h/test "Error icon renders" + (h/render [quiz-item/view + {:state :error + :word "collapse" + :number 2}]) + (h/is-truthy (h/get-by-label-text :error-icon)))) diff --git a/src/quo/components/list_items/quiz_item/style.cljs b/src/quo/components/list_items/quiz_item/style.cljs new file mode 100644 index 00000000000..f19504cfa9e --- /dev/null +++ b/src/quo/components/list_items/quiz_item/style.cljs @@ -0,0 +1,45 @@ +(ns quo.components.list-items.quiz-item.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [{:keys [blur? state]} theme] + {:flex 1 + :flex-direction :row + :justify-content :space-between + :align-items :center + :height 56 + :padding 12 + :border-radius 12 + :opacity (if (= state :disabled) 0.3 1) + :border-width (if (and blur? (or (= state :empty) (= state :disabled))) 0 1) + :border-color (case state + :success colors/success-50-opa-20 + :error colors/danger-50-opa-20 + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)) + :background-color (case state + :empty (if blur? + colors/white-opa-5 + (colors/theme-colors colors/white colors/neutral-80-opa-40 theme)) + :disabled (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-5 colors/neutral-80-opa-40 theme)) + :success (colors/resolve-color :success theme 10) + :error (colors/resolve-color :danger theme 10))}) + +(defn num-container + [{:keys [blur?]} theme] + {:width 32 + :height 32 + :justify-content :center + :align-items :center + :border-radius 10 + :border-width 1 + :border-color (if blur? + colors/white-opa-10 + (colors/theme-colors colors/neutral-20 colors/neutral-70 theme))}) + +(defn text + [{:keys [state]} theme] + {:color (case state + :success (colors/theme-colors colors/success-50 colors/success-60 theme) + :error (colors/theme-colors colors/danger-50 colors/danger-60 theme))}) diff --git a/src/quo/components/list_items/quiz_item/view.cljs b/src/quo/components/list_items/quiz_item/view.cljs new file mode 100644 index 00000000000..454954a1507 --- /dev/null +++ b/src/quo/components/list_items/quiz_item/view.cljs @@ -0,0 +1,31 @@ +(ns quo.components.list-items.quiz-item.view + (:require + [quo.components.icon :as icon] + [quo.components.list-items.quiz-item.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn view + [{:keys [state number word on-press] :as props}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:style (style/container props theme) + :on-press on-press} + (if (or (= state :empty) (= state :disabled)) + [rn/view + {:style (style/num-container props theme) + :accessibility-label :number-container} + [text/text {:weight :semi-bold} number]] + [text/text {:style (style/text props theme)} + (if (= state :success) word (i18n/label :t/oops-wrong-word))]) + (when (= state :success) + [icon/icon :i/check + {:color (colors/theme-colors colors/success-50 colors/success-60 theme) + :accessibility-label :success-icon}]) + (when (= state :error) + [icon/icon :i/incorrect + {:color (colors/theme-colors colors/danger-50 colors/danger-60 theme) + :accessibility-label :error-icon}])])) diff --git a/src/quo/components/list_items/saved_address/component_spec.cljs b/src/quo/components/list_items/saved_address/component_spec.cljs new file mode 100644 index 00000000000..a9ca604d785 --- /dev/null +++ b/src/quo/components/list_items/saved_address/component_spec.cljs @@ -0,0 +1,53 @@ +(ns quo.components.list-items.saved-address.component-spec + (:require + [quo.components.list-items.saved-address.view :as saved-address] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(h/describe "List items: saved address" + (h/test "default render" + (h/render [saved-address/view]) + (h/is-truthy (h/query-by-label-text :container))) + + (h/test "on-press-in changes state to :pressed" + (h/render [saved-address/view]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 5)}))) + + (h/test "on-press-in changes state to :pressed with blur? enabled" + (h/render [saved-address/view {:blur? true}]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor colors/white-opa-5}))) + + (h/test "on-press-out changes state to :active" + (h/render [saved-address/view {:active-state? true}]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/fire-event :on-press-out (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 10)}))) + + (h/test "on-press-out changes state to :active with blur? enabled" + (h/render [saved-address/view + {:blur? true + :active-state? true}]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/fire-event :on-press-out (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor colors/white-opa-10}))) + + (h/test "on-press container calls on-press" + (let [on-press (h/mock-fn)] + (h/render [saved-address/view {:on-press on-press}]) + (h/fire-event :on-press (h/get-by-label-text :container)) + (h/was-called on-press))) + + (h/test "renders options button if type :action" + (let [on-options-press (h/mock-fn)] + (h/render [saved-address/view + {:type :action + :on-options-press on-options-press}]) + (h/is-truthy (h/query-by-label-text :options-button)) + (h/fire-event :on-press (h/get-by-label-text :options-button)) + (h/was-called on-options-press)))) diff --git a/src/quo/components/list_items/saved_address/style.cljs b/src/quo/components/list_items/saved_address/style.cljs new file mode 100644 index 00000000000..7b86491aece --- /dev/null +++ b/src/quo/components/list_items/saved_address/style.cljs @@ -0,0 +1,43 @@ +(ns quo.components.list-items.saved-address.style + (:require + [quo.foundations.colors :as colors])) + +(defn- background-color + [{:keys [state blur? customization-color]}] + (cond (and (or (= state :pressed) (= state :selected)) (not blur?)) + (colors/resolve-color customization-color :light 5) + (and (or (= state :pressed) (= state :selected)) blur?) + colors/white-opa-5 + (and (= state :active) (not blur?)) + (colors/resolve-color customization-color :light 10) + (and (= state :active) blur?) + colors/white-opa-10 + (and (= state :pressed) blur?) colors/white-opa-10 + :else :transparent)) + +(defn container + [props] + {:height 56 + :border-radius 12 + :background-color (background-color props) + :flex-direction :row + :align-items :center + :padding-horizontal 12 + :padding-vertical 6 + :justify-content :space-between}) + +(def left-container + {:flex-direction :row + :align-items :center}) + +(def name-text + {:height 22}) + +(def account-container + {:margin-left 8}) + +(defn account-address + [blur? theme] + {:color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) diff --git a/src/quo/components/list_items/saved_address/view.cljs b/src/quo/components/list_items/saved_address/view.cljs new file mode 100644 index 00000000000..ab540beec74 --- /dev/null +++ b/src/quo/components/list_items/saved_address/view.cljs @@ -0,0 +1,80 @@ +(ns quo.components.list-items.saved-address.view + (:require + [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [quo.components.icon :as icon] + [quo.components.list-items.saved-address.style :as style] + [quo.components.markdown.text :as text] + [quo.components.wallet.address-text.view :as address-text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- left-container + [{:keys [blur? name ens address customization-color]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/left-container} + [wallet-user-avatar/wallet-user-avatar + {:size :size-32 + :full-name name + :customization-color customization-color}] + [rn/view {:style style/account-container} + [text/text + {:weight :semi-bold + :size :paragraph-1 + :style style/name-text} + name] + (if ens + [text/text + {:size :paragraph-2 + :weight :monospace + :style (style/account-address blur? theme)} ens] + [address-text/view + {:address address + :full-address? true + :format :short + :blur? blur?}])]])) + +(defn view + [{:keys [blur? user-props active-state? customization-color on-press on-options-press container-style] + :or {customization-color :blue + blur? false}}] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + active? (rn/use-ref-atom false) + timer (rn/use-ref-atom nil) + on-press-in (rn/use-callback + (fn [] + (when-not (= state :selected) + (reset! timer (js/setTimeout #(set-state :pressed) 100)))) + [state]) + on-press-out (rn/use-callback + (fn [] + (let [new-state (if (or (not active-state?) @active?) :default :active)] + (when @timer (js/clearTimeout @timer)) + (reset! timer nil) + (reset! active? (= new-state :active)) + (set-state new-state))) + [active-state?])] + [rn/pressable + {:style (merge (style/container + {:state state :blur? blur? :customization-color customization-color}) + container-style) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :accessibility-label :container} + [left-container + {:blur? blur? + :theme theme + :name (:name user-props) + :ens (:ens user-props) + :address (:address user-props) + :customization-color (or (:customization-color user-props) :blue)}] + (when on-options-press + [rn/pressable + {:accessibility-label :options-button + :on-press on-options-press} + [icon/icon :i/options + {:color (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}]])])) diff --git a/src/quo/components/list_items/saved_contact_address/component_spec.cljs b/src/quo/components/list_items/saved_contact_address/component_spec.cljs new file mode 100644 index 00000000000..145cfd3a7cd --- /dev/null +++ b/src/quo/components/list_items/saved_contact_address/component_spec.cljs @@ -0,0 +1,68 @@ +(ns quo.components.list-items.saved-contact-address.component-spec + (:require + [quo.components.list-items.saved-contact-address.view :as saved-contact-address] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(defn- with-defaults + ([] (with-defaults {})) + ([props] (merge {:contact-props {:full-name "Thomas Anderson"}} props))) + +(def account + {:name "New House" + :address "0x21a...49e" + :emoji "🍔" + :customization-color :purple}) + +(h/describe "List items: saved contact address" + (h/test "default render" + (h/render-with-theme-provider [saved-contact-address/view (with-defaults)]) + (h/is-truthy (h/query-by-label-text :container))) + + (h/test "renders account detail when passing one account" + (h/render-with-theme-provider [saved-contact-address/view + (with-defaults + {:accounts (repeat 1 account)})]) + (h/is-truthy (h/query-by-label-text :account-container))) + + (h/test "renders account count when passing multiple accounts" + (h/render-with-theme-provider [saved-contact-address/view + (with-defaults + {:accounts (repeat 2 account)})]) + (h/is-truthy (h/query-by-label-text :accounts-count))) + + (h/test "on-press-in changes state to :pressed" + (h/render-with-theme-provider [saved-contact-address/view + (with-defaults + {:accounts (repeat 1 account)})]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 5)}))) + + (h/test "on-press-out changes state to :active if active-state? is true (default value)" + (h/render-with-theme-provider [saved-contact-address/view + (with-defaults + {:accounts (repeat 1 account)})]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/fire-event :on-press-out (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor (colors/resolve-color :blue :light 10)}))) + + (h/test "on-press-out changes state to :default if active-state? is false" + (h/render-with-theme-provider [saved-contact-address/view + (with-defaults + {:accounts (repeat 1 account) + :active-state? false})]) + (h/fire-event :on-press-in (h/get-by-label-text :container)) + (h/fire-event :on-press-out (h/get-by-label-text :container)) + (h/wait-for #(h/has-style (h/query-by-label-text :container) + {:backgroundColor :transparent}))) + + (h/test "on-press calls on-press" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [saved-contact-address/view + (with-defaults + {:on-press on-press + :accounts (repeat 1 account)})]) + (h/fire-event :on-press (h/get-by-label-text :container)) + (h/was-called on-press)))) diff --git a/src/quo/components/list_items/saved_contact_address/schema.cljs b/src/quo/components/list_items/saved_contact_address/schema.cljs new file mode 100644 index 00000000000..532039accd8 --- /dev/null +++ b/src/quo/components/list_items/saved_contact_address/schema.cljs @@ -0,0 +1,28 @@ +(ns quo.components.list-items.saved-contact-address.schema) + +(def ^:private ?contact + [:map + [:full-name :string] + [:profile-picture {:optional true} [:maybe [:or :schema.common/image-source :string]]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]) + +(def ^:private ?account + [:map + [:name :string] + [:address :string] + [:emoji {:optional true} :string] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]) + +(def ^:private ?accounts + [:+ ?account]) + +(def ?schema + [:=> + [:cat + [:map + [:contact-props ?contact] + [:accounts {:optional true} ?accounts] + [:on-press {:optional true} [:maybe fn?]] + [:active-state? {:optional true} [:maybe :boolean]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]] + :any]) diff --git a/src/quo/components/list_items/saved_contact_address/style.cljs b/src/quo/components/list_items/saved_contact_address/style.cljs new file mode 100644 index 00000000000..387f7317b7d --- /dev/null +++ b/src/quo/components/list_items/saved_contact_address/style.cljs @@ -0,0 +1,63 @@ +(ns quo.components.list-items.saved-contact-address.style + (:require + [quo.foundations.colors :as colors])) + +(defn- background-color + [{:keys [state customization-color]}] + (cond (or (= state :pressed) (= state :selected)) + (colors/resolve-color customization-color :light 5) + (= state :active) + (colors/resolve-color customization-color :light 10) + :else :transparent)) + +(defn container + [props] + {:height 56 + :border-radius 12 + :background-color (background-color props) + :flex-direction :row + :align-items :center + :padding-horizontal 12 + :padding-vertical 6 + :justify-content :space-between}) + +(def left-container + {:flex-direction :row + :align-items :center}) + +(defn dot-divider + [theme] + {:width 2 + :height 2 + :border-radius 2 + :margin-horizontal 4 + :background-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def saved-contact-container + {:margin-left 8}) + +(def account-container + {:flex-direction :row + :align-items :center}) + +(def account-title-container + {:flex-direction :row + :height 22 + :align-items :center}) + +(defn account-name + [theme] + {:margin-left 4 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn accounts-count + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn account-address + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def contact-icon-container + {:margin-left 4 + :margin-top 1.5}) diff --git a/src/quo/components/list_items/saved_contact_address/view.cljs b/src/quo/components/list_items/saved_contact_address/view.cljs new file mode 100644 index 00000000000..c8e0342cb3a --- /dev/null +++ b/src/quo/components/list_items/saved_contact_address/view.cljs @@ -0,0 +1,97 @@ +(ns quo.components.list-items.saved-contact-address.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.icon :as icon] + [quo.components.list-items.saved-contact-address.schema :as component-schema] + [quo.components.list-items.saved-contact-address.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.address :as address] + [utils.i18n :as i18n])) + +(defn- account + [{:keys [emoji name address customization-color]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :account-container + :style style/account-container} + [account-avatar/view + {:emoji emoji + :size 16 + :customization-color customization-color}] + [text/text + {:size :paragraph-2 + :weight :monospace + :style (style/account-name theme)} + name] + [rn/view {:style (style/dot-divider theme)}] + [text/text + {:size :paragraph-2 + :weight :monospace + :style (style/account-address theme)} + (address/get-shortened-key address)]])) + +(defn- internal-view + [{:keys [contact-props accounts active-state? customization-color on-press] + :or {customization-color :blue + accounts [] + active-state? true}}] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + active? (rn/use-ref-atom false) + timer (rn/use-ref-atom nil) + on-press (rn/use-callback #(when on-press (on-press))) + on-press-in (rn/use-callback + (fn [] + (when-not (= state :selected) + (reset! timer (js/setTimeout #(set-state :pressed) 100)))) + [state]) + accounts-count (count accounts) + account-props (when (= accounts-count 1) (first accounts)) + on-press-out (rn/use-callback + (fn [] + (let [new-state (if (or (not active-state?) @active?) :default :active)] + (when @timer (js/clearTimeout @timer)) + (reset! timer nil) + (reset! active? (= new-state :active)) + (set-state new-state))) + [active-state?])] + [rn/pressable + {:style (style/container {:state state :customization-color customization-color}) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :accessibility-label :container} + [rn/view {:style style/left-container} + [user-avatar/user-avatar (assoc contact-props :size :small)] + [rn/view {:style style/saved-contact-container} + [rn/view + {:style style/account-title-container} + [text/text + {:weight :semi-bold + :size :paragraph-1} + (:full-name contact-props)] + [icon/icon :i/contact + {:container-style style/contact-icon-container + :accessibility-label :contact-icon + :size 12 + :color (colors/theme-colors colors/primary-50 colors/primary-60 theme) + :color-2 colors/white}]] + (if account-props + [account (assoc account-props :theme theme)] + [text/text + {:accessibility-label :accounts-count + :size :paragraph-2 + :style (style/accounts-count theme)} + (i18n/label :t/accounts-count {:count accounts-count})])]] + (when (> accounts-count 1) + [icon/icon :i/chevron-right + {:accessibility-label :check-icon + :size 20 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}])])) + +(def view (schema/instrument #'internal-view component-schema/?schema)) diff --git a/src/quo/components/list_items/status_list_item/component_spec.cljs b/src/quo/components/list_items/status_list_item/component_spec.cljs new file mode 100644 index 00000000000..4dec52ca25e --- /dev/null +++ b/src/quo/components/list_items/status_list_item/component_spec.cljs @@ -0,0 +1,13 @@ +(ns quo.components.list-items.status-list-item.component-spec + (:require + [quo.components.list-items.status-list-item.view :as status-list-item] + [test-helpers.component :as h])) + +(h/describe "Status List Item" + (h/test "With title and description" + (h/render [status-list-item/view + {:status :positive + :title "Account" + :description "This is a description"}]) + (h/is-truthy (h/get-by-text "Account")) + (h/is-truthy (h/get-by-text "This is a description")))) diff --git a/src/quo/components/list_items/status_list_item/schema.cljs b/src/quo/components/list_items/status_list_item/schema.cljs new file mode 100644 index 00000000000..3dfbdcd65ab --- /dev/null +++ b/src/quo/components/list_items/status_list_item/schema.cljs @@ -0,0 +1,12 @@ +(ns quo.components.list-items.status-list-item.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:status [:enum :positive :negative]] + [:title [:maybe string?]] + [:description {:optional true} [:maybe string?]] + [:blur? {:optional true} [:maybe boolean?]]]]] + :any]) diff --git a/src/quo/components/list_items/status_list_item/style.cljs b/src/quo/components/list_items/status_list_item/style.cljs new file mode 100644 index 00000000000..6d5f0f96aaf --- /dev/null +++ b/src/quo/components/list_items/status_list_item/style.cljs @@ -0,0 +1,15 @@ +(ns quo.components.list-items.status-list-item.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:padding-vertical 5 + :padding-horizontal 20 + :flex-direction :row}) + +(defn title + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn description + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/white-opa-40 theme)}) diff --git a/src/quo/components/list_items/status_list_item/view.cljs b/src/quo/components/list_items/status_list_item/view.cljs new file mode 100644 index 00000000000..bdc9c79475f --- /dev/null +++ b/src/quo/components/list_items/status_list_item/view.cljs @@ -0,0 +1,40 @@ +(ns quo.components.list-items.status-list-item.view + (:require + [clojure.string :as string] + [quo.components.icon :as quo.icons] + [quo.components.list-items.status-list-item.schema :as component-schema] + [quo.components.list-items.status-list-item.style :as style] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn view-internal + [{:keys [status title description]}] + (let [theme (quo.context/use-theme) + positive? (= status :positive) + icon-color (if positive? + (colors/theme-colors colors/success-50 colors/success-60 theme) + (colors/theme-colors colors/danger-50 colors/danger-60 theme))] + [rn/view + {:style style/container + :accessibility-label :status-list-item} + [quo.icons/icon (if positive? :i/correct :i/warning) + {:size 20 + :color icon-color + :container-style {:margin-vertical 1}}] + [rn/view + {:style {:flex-direction :column :margin-left 8}} + [text/text + {:weight :regular + :size :paragraph-1 + :style (style/title theme)} title] + (when-not (string/blank? description) + [text/text + {:weight :regular + :size :paragraph-2 + :style (style/description theme)} + description])]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/list_items/token_info/component_spec.cljs b/src/quo/components/list_items/token_info/component_spec.cljs new file mode 100644 index 00000000000..17cb1c6498c --- /dev/null +++ b/src/quo/components/list_items/token_info/component_spec.cljs @@ -0,0 +1,13 @@ +(ns quo.components.list-items.token-info.component-spec + (:require + [quo.components.list-items.token-info.view :as token-info] + [test-helpers.component :as h])) + +(h/describe "List Items: Token Info" + (h/test "Token label renders" + (h/render-with-theme-provider [token-info/view + {:token :snt + :label "Status" + :state :default + :customization-color :blue}]) + (h/is-truthy (h/get-by-text "Status")))) diff --git a/src/quo/components/list_items/token_info/schema.cljs b/src/quo/components/list_items/token_info/schema.cljs new file mode 100644 index 00000000000..6d20d0ec6ed --- /dev/null +++ b/src/quo/components/list_items/token_info/schema.cljs @@ -0,0 +1,12 @@ +(ns quo.components.list-items.token-info.schema) + +(def ?schema + [:=> + [:cat + [:map + [:token [:or :string :keyword]] + [:label :string] + [:on-press {:optional true} [:maybe fn?]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:state {:optional true} [:maybe [:enum :default :active :selected :disabled]]]]] + :any]) diff --git a/src/quo/components/list_items/token_info/style.cljs b/src/quo/components/list_items/token_info/style.cljs new file mode 100644 index 00000000000..5edda495192 --- /dev/null +++ b/src/quo/components/list_items/token_info/style.cljs @@ -0,0 +1,42 @@ +(ns quo.components.list-items.token-info.style + (:require [quo.foundations.colors :as colors])) + +(defn- background-color + [state customization-color theme] + (cond + (= state :pressed) (colors/resolve-color customization-color theme 5) + (= state :active) (colors/resolve-color customization-color theme 10) + (= state :selected) (colors/resolve-color customization-color theme 5) + :else :transparent)) + +(defn container + [state customization-color theme] + {:flex-direction :row + :justify-content :space-between + :align-items :center + :padding-horizontal 12 + :padding-vertical 8 + :border-radius 12 + :height 56 + :opacity (when (= state :disabled) 0.3) + :background-color (background-color state customization-color theme)}) + +(def info + {:flex-direction :row + :align-items :center + :width "70%"}) + +(def token-info + {:height 40 + :flex 1}) + +(defn token-description-label + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def token-image + {:border-width 1 + :border-radius 16 + :border-color colors/neutral-80-opa-5 + :margin-right 8 + :background-color colors/neutral-80-opa-5}) diff --git a/src/quo/components/list_items/token_info/view.cljs b/src/quo/components/list_items/token_info/view.cljs new file mode 100644 index 00000000000..2f6f0a19b40 --- /dev/null +++ b/src/quo/components/list_items/token_info/view.cljs @@ -0,0 +1,50 @@ +(ns quo.components.list-items.token-info.view + (:require + [quo.components.list-items.token-info.schema :as component-schema] + [quo.components.list-items.token-info.style :as style] + [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- info + [{:keys [token label]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/info} + (when token + [token/view + {:style style/token-image + :size :size-32 + :token token}]) + [rn/view {:style style/token-info} + [text/text + {:weight :semi-bold + :number-of-lines 1} + (if-not (empty? label) label "-")] + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/token-description-label theme) + :number-of-lines 1} + token]]])) + +(defn- view-internal + [{:keys [on-press state customization-color] + :as props + :or {customization-color :blue}}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false)) + internal-state (if pressed? :pressed state)] + [rn/pressable + {:style (style/container internal-state customization-color theme) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :disabled (= state :disabled) + :accessibility-label :token} + [info props]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/list_items/token_network/component_spec.cljs b/src/quo/components/list_items/token_network/component_spec.cljs new file mode 100644 index 00000000000..a35ff53882c --- /dev/null +++ b/src/quo/components/list_items/token_network/component_spec.cljs @@ -0,0 +1,43 @@ +(ns quo.components.list-items.token-network.component-spec + (:require [quo.components.list-items.token-network.view :as token-network] + [quo.foundations.colors :as colors] + [quo.foundations.resources :as resources] + [test-helpers.component :as h])) + +(def props + {:token :snt + :state :default + :label "Status" + :networks [{:source (resources/get-network :ethereum)}] + :token-value "100.00 SNT" + :fiat-value "€0.00"}) + +(h/describe "List items: Token network" + (h/test "default state" + (h/render-with-theme-provider [token-network/view (dissoc props :state)]) + (h/is-truthy (h/get-by-text "Status"))) + + (h/test "default state explicit" + (h/render-with-theme-provider [token-network/view props]) + (h/is-truthy (h/get-by-text "Status"))) + + (h/test "Pressed state" + (h/render-with-theme-provider [token-network/view props]) + (h/fire-event :on-press-in (h/get-by-label-text :token-network)) + (h/wait-for #(h/has-style (h/query-by-label-text :token-network) + {:backgroundColor (colors/resolve-color :blue :light 5)}))) + + (h/test "Active state" + (h/render-with-theme-provider [token-network/view (assoc props :state :active)]) + (h/has-style (h/query-by-label-text :token-network) + {:backgroundColor (colors/resolve-color :blue :light 10)})) + + (h/test "Selected state" + (h/render-with-theme-provider [token-network/view (assoc props :state :selected)]) + (h/is-truthy (h/query-by-label-text :check-icon))) + + (h/test "Call on-press" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [token-network/view (assoc props :on-press on-press)]) + (h/fire-event :on-press (h/get-by-label-text :token-network)) + (h/was-called on-press)))) diff --git a/src/quo/components/list_items/token_network/schema.cljs b/src/quo/components/list_items/token_network/schema.cljs new file mode 100644 index 00000000000..095a78ac106 --- /dev/null +++ b/src/quo/components/list_items/token_network/schema.cljs @@ -0,0 +1,15 @@ +(ns quo.components.list-items.token-network.schema) + +(def ?schema + [:=> + [:cat + [:map + [:token [:or :string :keyword]] + [:label :string] + [:token-value :string] + [:fiat-value :string] + [:networks [:* [:map [:source :schema.common/image-source]]]] + [:on-press {:optional true} [:maybe fn?]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:state {:optional true} [:maybe [:enum :default :active :selected :disabled]]]]] + :any]) diff --git a/src/quo/components/list_items/token_network/style.cljs b/src/quo/components/list_items/token_network/style.cljs new file mode 100644 index 00000000000..04241d614e3 --- /dev/null +++ b/src/quo/components/list_items/token_network/style.cljs @@ -0,0 +1,50 @@ +(ns quo.components.list-items.token-network.style + (:require [quo.foundations.colors :as colors])) + +(defn- background-color + [state customization-color theme] + (cond + (= state :pressed) (colors/resolve-color customization-color theme 5) + (= state :active) (colors/resolve-color customization-color theme 10) + (= state :selected) (colors/resolve-color customization-color theme 5) + :else :transparent)) + +(defn container + [state customization-color theme] + {:flex-direction :row + :justify-content :space-between + :align-items :center + :padding-horizontal 12 + :padding-vertical 8 + :border-radius 12 + :height 56 + :opacity (when (= state :disabled) 0.3) + :background-color (background-color state customization-color theme)}) + +(defn check-color + [customization-color theme] + (colors/resolve-color customization-color theme)) + +(def info + {:flex-direction :row + :align-items :center + :width "70%"}) + +(def token-info + {:height 40 + :flex 1}) + +(def token-image + {:border-width 1 + :border-radius 16 + :border-color colors/neutral-80-opa-5 + :margin-right 8 + :background-color colors/neutral-80-opa-5}) + +(def values-container + {:align-items :flex-end + :max-width "30%"}) + +(defn fiat-value + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo/components/list_items/token_network/view.cljs b/src/quo/components/list_items/token_network/view.cljs new file mode 100644 index 00000000000..f5f49ff5a82 --- /dev/null +++ b/src/quo/components/list_items/token_network/view.cljs @@ -0,0 +1,70 @@ +(ns quo.components.list-items.token-network.view + (:require + [quo.components.icon :as icon] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.list-items.token-network.schema :as component-schema] + [quo.components.list-items.token-network.style :as style] + [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- info + [{:keys [token label networks]}] + [rn/view {:style style/info} + (when token + [token/view + {:style style/token-image + :size :size-32 + :token token}]) + [rn/view {:style style/token-info} + [text/text + {:weight :semi-bold + :number-of-lines 1} + (if-not (empty? label) label "-")] + [preview-list/view + {:type :network + :size :size-14 + :number (count networks)} + networks]]]) + +(defn- values + [{:keys [state token-value fiat-value customization-color]}] + (let [theme (quo.context/use-theme)] + (if (= state :selected) + [icon/icon :i/check + {:color (style/check-color customization-color theme) + :accessibility-label :check-icon}] + [rn/view {:style style/values-container} + [text/text + {:weight :medium + :size :paragraph-2 + :number-of-lines 1} + token-value] + [text/text + {:style (style/fiat-value theme) + :size :paragraph-2 + :number-of-lines 1} + fiat-value]]))) + +(defn- view-internal + [{:keys [on-press state customization-color _token _networks _token-value _fiat-value] + :as props + :or {customization-color :blue}}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false)) + internal-state (if pressed? :pressed state)] + [rn/pressable + {:style (style/container internal-state customization-color theme) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :disabled (= state :disabled) + :accessibility-label :token-network} + [info props] + [values props]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/list_items/token_value/component_spec.cljs b/src/quo/components/list_items/token_value/component_spec.cljs new file mode 100644 index 00000000000..97dfccb985c --- /dev/null +++ b/src/quo/components/list_items/token_value/component_spec.cljs @@ -0,0 +1,33 @@ +(ns quo.components.list-items.token-value.component-spec + (:require + [quo.components.list-items.token-value.view :as token-value] + [test-helpers.component :as h])) + +(h/describe "List Items: Token Value" + (h/test "Token label renders" + (h/render-with-theme-provider [token-value/view + {:token :snt + :token-name "Status" + :state :default + :status :empty + :customization-color :blue + :metrics? true + :values {:crypto-value "0.00" + :fiat-value "€0.00" + :percentage-change "0.00" + :fiat-change "€0.00"}}]) + (h/is-truthy (h/get-by-text "Status"))) + + (h/test "Status change" + (h/render-with-theme-provider [token-value/view + {:token :snt + :token-name "Status" + :state :default + :status :positive + :customization-color :blue + :metrics? true + :values {:crypto-value "0.00" + :fiat-value "€0.00" + :percentage-change "0.00" + :fiat-change "€0.00"}}]) + (h/is-truthy (h/get-by-label-text :arrow-icon)))) diff --git a/src/quo/components/list_items/token_value/schema.cljs b/src/quo/components/list_items/token_value/schema.cljs new file mode 100644 index 00000000000..bbc55b3cf13 --- /dev/null +++ b/src/quo/components/list_items/token_value/schema.cljs @@ -0,0 +1,22 @@ +(ns quo.components.list-items.token-value.schema) + +(def ^:private ?values + [:map + [:crypto-value :string] + [:fiat-value :string] + [:percentage-change {:optional true} :string] + [:fiat-change {:optional true} :string]]) + +(def ?schema + [:=> + [:cat + [:map + [:token [:or keyword? string?]] + [:token-name :string] + [:status [:enum :empty :positive :negative]] + [:values ?values] + [:on-press {:optional true} [:maybe fn?]] + [:on-long-press {:optional true} [:maybe fn?]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:metrics? {:optional true} [:maybe :boolean]]]] + :any]) diff --git a/src/quo/components/list_items/token_value/style.cljs b/src/quo/components/list_items/token_value/style.cljs new file mode 100644 index 00000000000..2590984911c --- /dev/null +++ b/src/quo/components/list_items/token_value/style.cljs @@ -0,0 +1,38 @@ +(ns quo.components.list-items.token-value.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [color bg-opacity theme] + {:height 56 + :padding-horizontal 12 + :padding-vertical 8 + :border-radius 12 + :flex-direction :row + :justify-content :space-between + :background-color (colors/resolve-color color theme bg-opacity)}) + +(defn metric-text + [status theme] + {:color (case status + :positive (colors/theme-colors colors/success-50 colors/success-60 theme) + :negative (colors/theme-colors colors/danger-50 colors/danger-60 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(defn dot-divider + [status theme] + {:width 2 + :height 2 + :border-radius 2 + :margin-horizontal 4 + :background-color (case status + :positive (colors/resolve-color :success theme 40) + :negative (colors/resolve-color :danger theme 40) + (colors/theme-colors colors/neutral-80-opa-40 colors/neutral-50-opa-40 theme))}) + +(defn arrow-icon + [status theme] + {:size 16 + :color (if (= status :positive) + (colors/resolve-color :success theme) + (colors/resolve-color :danger theme))}) diff --git a/src/quo/components/list_items/token_value/view.cljs b/src/quo/components/list_items/token_value/view.cljs new file mode 100644 index 00000000000..5d3b1905b1c --- /dev/null +++ b/src/quo/components/list_items/token_value/view.cljs @@ -0,0 +1,76 @@ +(ns quo.components.list-items.token-value.view + (:require + [clojure.string :as string] + [quo.components.icon :as icon] + [quo.components.list-items.token-value.schema :as component-schema] + [quo.components.list-items.token-value.style :as style] + [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- internal-view + [{:keys [customization-color status token metrics? values on-press on-long-press token-name]}] + (let [theme (quo.context/use-theme) + [state set-state] (rn/use-state :default) + bg-opacity (case state + :active 10 + :pressed 5 + 0) + {:keys [crypto-value + fiat-value + percentage-change + fiat-change]} values + on-press-in (rn/use-callback #(set-state :pressed)) + on-press-out (rn/use-callback #(set-state :default)) + on-press (rn/use-callback + (fn [] + (set-state :active) + (js/setTimeout #(set-state :default) 300) + (when on-press + (on-press))))] + [rn/pressable + {:style (style/container customization-color bg-opacity theme) + :on-press-in on-press-in + :on-press-out on-press-out + :on-press on-press + :on-long-press on-long-press + :accessibility-label :container} + [rn/view + {:style {:flex-direction :row + :align-items :center + :flex 1}} + [token/view {:token token :size :size-32}] + [rn/view {:style {:margin-left 8}} + [text/text {:weight :semi-bold} token-name] + [text/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + (str crypto-value " " (if token (string/upper-case (clj->js token)) ""))]]] + [rn/view + {:style {:align-items :flex-end + :justify-content :space-between}} + [text/text + {:weight :medium + :size :paragraph-2} fiat-value] + (when metrics? + [rn/view + {:style {:flex-direction :row + :align-items :center}} + [text/text + {:size :paragraph-2 + :style (style/metric-text status theme)} (str percentage-change "%")] + [rn/view {:style (style/dot-divider status theme)}] + [text/text + {:size :paragraph-2 + :style (style/metric-text status theme)} fiat-change] + (when (not= status :empty) + [rn/view + {:style {:margin-left 4} + :accessibility-label :arrow-icon} + [icon/icon (if (= status :positive) :i/positive :i/negative) + (style/arrow-icon status theme)]])])]])) + +(def view (schema/instrument #'internal-view component-schema/?schema)) diff --git a/src/quo/components/list_items/user.cljs b/src/quo/components/list_items/user.cljs new file mode 100644 index 00000000000..fb3a7536323 --- /dev/null +++ b/src/quo/components/list_items/user.cljs @@ -0,0 +1,78 @@ +(ns quo.components.list-items.user + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.messages.author.view :as author] + [quo.components.selectors.selectors.view :as selectors] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(def container + {:margin-horizontal 8 + :padding-vertical 8 + :padding-horizontal 12 + :border-radius 12 + :flex 1 + :flex-direction :row + :align-items :center}) + +(defn accessory-item + [{:keys [type on-press on-check disabled? checked? child]} customization-color theme] + (if (= type :custom) + child + [rn/touchable-opacity {:on-press on-press} + (case type + :options + [icons/icon :i/options + {:size 20 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}] + + :checkbox + [selectors/view + {:type :checkbox + :checked? checked? + :customization-color customization-color + :accessibility-label :user-list-toggle-check + :disabled? disabled? + :on-change (when on-check on-check)}] + + :close + [text/text "not implemented"] + [rn/view])])) + +(defn user + [{:keys [short-chat-key primary-name secondary-name photo-path online? contact? verified? + untrustworthy? on-press on-long-press accessory customization-color theme + allow-multiple-presses? disabled? container-style]}] + [rn/touchable-highlight + {:style (merge container container-style) + :underlay-color (colors/resolve-color customization-color theme 5) + :allow-multiple-presses? allow-multiple-presses? + :accessibility-label :user-list + :on-press (when on-press on-press) + :on-long-press (when on-long-press on-long-press) + :disabled disabled?} + [:<> + [user-avatar/user-avatar + {:full-name primary-name + :profile-picture photo-path + :online? online? + :size :small}] + [rn/view + {:style {:margin-horizontal 8 + :flex 1}} + [author/view + {:primary-name primary-name + :secondary-name secondary-name + :contact? contact? + :verified? verified? + :untrustworthy? untrustworthy? + :size 15}] + (when short-chat-key + [text/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + short-chat-key])] + (when accessory + [accessory-item accessory customization-color disabled? theme])]]) diff --git a/src/quo/components/loaders/skeleton_list/component_spec.cljs b/src/quo/components/loaders/skeleton_list/component_spec.cljs new file mode 100644 index 00000000000..a0f559fe086 --- /dev/null +++ b/src/quo/components/loaders/skeleton_list/component_spec.cljs @@ -0,0 +1,27 @@ +(ns quo.components.loaders.skeleton-list.component-spec + (:require + [quo.components.loaders.skeleton-list.view :as skeleton-list] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(defn test-skeleton + [content animated?] + (let [rendered (h/render [skeleton-list/view + {:index 0 + :content content + :color colors/neutral-10 + :parent-height 600 + :animated? animated?}]) + accessibility-text :skeleton-list] + (h/is-truthy (h/get-by-label-text rendered accessibility-text)))) + +(h/describe "Skeleton tests" + (doseq [content [:messages :notifications :list-items :assets] + animated? [true false] + :let [content-str (name content)]] + (h/test (str "Skeleton :" + content-str + " is " + (if animated? "animated" "static") + " based on animated? " animated?) + (test-skeleton content animated?)))) diff --git a/src/quo/components/loaders/skeleton_list/constants.cljs b/src/quo/components/loaders/skeleton_list/constants.cljs new file mode 100644 index 00000000000..a5f184c6fdd --- /dev/null +++ b/src/quo/components/loaders/skeleton_list/constants.cljs @@ -0,0 +1,157 @@ +(ns quo.components.loaders.skeleton-list.constants) + +(def ^:const layout-dimensions + {:messages {:height 56 + :padding-top 12} + :list-items {:height 56 + :padding-top 12} + :notifications {:height 90 + :padding-top 8} + :assets {:height 56 + :padding-top 12}}) + +(def ^:const content-dimensions + {:messages {0 {:author {:width 112 + :height 8 + :margin-bottom 8} + :message {:width 144 + :height 16 + :margin-bottom 0}} + 1 {:author {:width 96 + :height 8 + :margin-bottom 8} + :message {:width 212 + :height 16 + :margin-bottom 0}} + 2 {:author {:width 80 + :height 8 + :margin-bottom 8} + :message {:width 249 + :height 16 + :margin-bottom 0}} + 3 {:author {:width 124 + :height 8 + :margin-bottom 8} + :message {:width 156 + :height 16 + :margin-bottom 0}}} + :list-items {0 {:author {:width 144 + :height 16 + :margin-bottom 8} + :message {:width 112 + :height 8 + :margin-bottom 0}} + 1 {:author {:width 212 + :height 16 + :margin-bottom 8} + :message {:width 96 + :height 8 + :margin-bottom 0}} + 2 {:author {:width 249 + :height 16 + :margin-bottom 8} + :message {:width 80 + :height 8 + :margin-bottom 0}} + 3 {:author {:width 156 + :height 16 + :margin-bottom 8} + :message {:width 124 + :height 8 + :margin-bottom 0}}} + :notifications {0 {:author {:width 109 + :height 8 + :margin-bottom 8} + :message {:width 167 + :height 16 + :margin-bottom 8} + :message2 {:width 242 + :height 30 + :margin-bottom 0}} + 1 {:author {:width 165 + :height 8 + :margin-bottom 8} + :message {:width 112 + :height 16 + :margin-bottom 8} + :message2 {:width 294 + :height 30 + :margin-bottom 0}} + 2 {:author {:width 136 + :height 8 + :margin-bottom 8} + :message {:width 178 + :height 16 + :margin-bottom 8} + :message2 {:width 178 + :height 30 + :margin-bottom 0}} + 3 {:author {:width 136 + :height 8 + :margin-bottom 8} + :message {:width 166 + :height 16 + :margin-bottom 8} + :message2 {:width 256 + :height 30 + :margin-bottom 0}}} + :assets {0 {:author {:width 60 + :height 14 + :margin-bottom 8} + :message {:width 92 + :height 10 + :margin-bottom 0} + :author2 {:width 80 + :height 10 + :margin-bottom 8} + :message2 {:width 52 + :height 10 + :margin-right 4} + :message3 {:width 52 + :height 10 + :margin-right 4}} + 1 {:author {:width 60 + :height 14 + :margin-bottom 8} + :message {:width 92 + :height 10 + :margin-bottom 0} + :author2 {:width 100 + :height 10 + :margin-bottom 8} + :message2 {:width 32 + :height 10 + :margin-right 4} + :message3 {:width 32 + :height 10 + :margin-right 4}} + 2 {:author {:width 80 + :height 14 + :margin-bottom 8} + :message {:width 112 + :height 10 + :margin-bottom 0} + :author2 {:width 80 + :height 10 + :margin-bottom 8} + :message2 {:width 52 + :height 10 + :margin-right 4} + :message3 {:width 52 + :height 10 + :margin-right 4}} + 3 {:author {:width 80 + :height 14 + :margin-bottom 8} + :message {:width 112 + :height 10 + :margin-bottom 0} + :author2 {:width 100 + :height 10 + :margin-bottom 8} + :message2 {:width 32 + :height 10 + :margin-right 4} + :message3 {:width 32 + :height 10 + :margin-right 4}}}}) diff --git a/src/quo/components/loaders/skeleton_list/style.cljs b/src/quo/components/loaders/skeleton_list/style.cljs new file mode 100644 index 00000000000..bf7ec841c92 --- /dev/null +++ b/src/quo/components/loaders/skeleton_list/style.cljs @@ -0,0 +1,46 @@ +(ns quo.components.loaders.skeleton-list.style + (:require + [quo.components.loaders.skeleton-list.constants :as constants])) + +(defn container + [content] + {:flex-direction :row + :align-items :center + :padding 12 + :padding-top (get-in constants/layout-dimensions [content :padding-top]) + :height (get-in constants/layout-dimensions [content :height])}) + +(defn avatar + [color] + {:height 32 + :width 32 + :border-radius 16 + :background-color color}) + +(def content-container + {:flex 1 + :padding-left 8}) + +(def right-content-container + {:align-items :flex-end}) + +(def right-bottom-content-container + {:flex-direction :row}) + +(defn author + [color] + {:height 10 + :width 10 + :border-radius 6 + :background-color color}) + +(defn content-view + [{:keys [type index content color]}] + (let [{:keys [width height margin-bottom margin-right]} + (get-in constants/content-dimensions [content index type])] + {:height height + :width width + :border-radius 6 + :background-color color + :margin-bottom margin-bottom + :margin-right margin-right})) diff --git a/src/quo/components/loaders/skeleton_list/view.cljs b/src/quo/components/loaders/skeleton_list/view.cljs new file mode 100644 index 00000000000..e064653a094 --- /dev/null +++ b/src/quo/components/loaders/skeleton_list/view.cljs @@ -0,0 +1,117 @@ +(ns quo.components.loaders.skeleton-list.view + (:require + [quo.components.loaders.skeleton-list.constants :as constants] + [quo.components.loaders.skeleton-list.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.masked-view :as masked-view] + [react-native.reanimated :as reanimated] + [reagent.core :as reagent])) + +(defn static-skeleton-view + [{:keys [index content color]}] + [rn/view + {:style (style/container content)} + [rn/view {:style (style/avatar color)}] + [rn/view {:style style/content-container} + [rn/view + {:style (style/content-view + {:type :author + :index index + :content content + :color color})}] + [rn/view + {:style (style/content-view + {:type :message + :index index + :content content + :color color})}] + (when (= content :notifications) + [rn/view + {:style (style/content-view {:type :message2 + :index index + :content content + :color color})}])] + (when (= content :assets) + [rn/view {:style style/right-content-container} + [rn/view + {:style (style/content-view + {:type :author2 + :index index + :content content + :color color})}] + [rn/view {:style style/right-bottom-content-container} + [rn/view + {:style (style/content-view + {:type :message2 + :index index + :content content + :color color})}] + [rn/view + {:style (style/content-view + {:type :message3 + :index index + :content content + :color color})}] + [rn/view {:style (style/author color)}]]])]) + +(defn- f-animated-skeleton-view + [{:keys [style color skeleton-height animated? translate-x window-width] :as data}] + (let [theme (quo.context/use-theme) + loading-color (colors/theme-colors colors/neutral-10 colors/neutral-60 theme)] + (rn/use-effect + (fn [] + (when-not animated? + (reanimated/cancel-animation translate-x)) + (reanimated/animate-shared-value-with-repeat translate-x window-width 1000 :linear -1 false) + #(when-not animated? + (reanimated/cancel-animation translate-x))) + [animated?]) + + + [masked-view/masked-view + {:style {:height skeleton-height} + :mask-element (reagent/as-element [static-skeleton-view data])} + [rn/view + {:style {:flex 1 + :background-color color}} + [reanimated/linear-gradient + {:colors [color color loading-color color color] + :start {:x 0 :y 0} + :end {:x 1 :y 0} + :style style}]]])) + +(defn- animated-skeleton-view + [props] + [:f> f-animated-skeleton-view props]) + +(defn view + [{:keys [content blur? parent-height animated?] :as props}] + (let [theme (quo.context/use-theme) + {window-width :width} (rn/get-window) + translate-x (reanimated/use-shared-value (- window-width)) + animated-gradient-style (reanimated/apply-animations-to-style + {:transform [{:translateX translate-x}]} + {:width window-width + :height "100%"}) + skeleton-height (get-in constants/layout-dimensions [content :height]) + number-of-skeletons (int (Math/ceil (/ parent-height skeleton-height))) + color (if (and blur? (= theme :dark)) + colors/white-opa-5 + (colors/theme-colors colors/neutral-5 colors/neutral-90 theme)) + component (if animated? animated-skeleton-view static-skeleton-view)] + [rn/view + {:style {:padding 8} + :accessibility-label :skeleton-list} + (for [parent-index (range number-of-skeletons)] + ^{:key parent-index} + [component + (merge props + {:index (mod parent-index 4) + :parent-index parent-index + :color color + :translate-x translate-x + :skeleton-height skeleton-height + :style animated-gradient-style})])])) + diff --git a/src/quo/components/markdown/list/component_spec.cljs b/src/quo/components/markdown/list/component_spec.cljs new file mode 100644 index 00000000000..51295005b4d --- /dev/null +++ b/src/quo/components/markdown/list/component_spec.cljs @@ -0,0 +1,40 @@ +(ns quo.components.markdown.list.component-spec + (:require + [quo.components.markdown.list.view :as list] + [test-helpers.component :as h])) + +(h/describe "tests for markdown/list component" + (h/test "renders component with title" + (h/render-with-theme-provider [list/view {:title "test title"}]) + (h/is-truthy (h/get-by-text "test title"))) + + (h/test "renders component with description" + (h/render-with-theme-provider [list/view + {:title "test title" + :description "test description"}]) + (h/is-truthy (h/get-by-text "test description"))) + + (h/test "renders component with title and description" + (h/render-with-theme-provider [list/view + {:title "test title" + :description "test description"}]) + (h/is-truthy (h/get-by-text "test title")) + (h/is-truthy (h/get-by-text "test description"))) + + (h/test "renders step component when step-number is valid and type is step" + (h/render-with-theme-provider [list/view + {:type :step + :step-number 1}] + :dark) + (h/is-truthy (h/get-by-label-text :step-counter))) + + (h/test "renders decription with a context tag component and description after the tag" + (h/render-with-theme-provider [list/view + {:step-number 1 + :description "Lorem ipsum " + :tag-name "dolor" + :description-after-tag "text after tag"}]) + (h/is-truthy (h/get-by-text "Lorem ipsum")) + (h/is-truthy (h/get-by-label-text :user-avatar)) + (h/is-truthy (h/get-by-text "dolor")) + (h/is-truthy (h/get-by-text "text after tag")))) diff --git a/src/quo/components/markdown/list/style.cljs b/src/quo/components/markdown/list/style.cljs new file mode 100644 index 00000000000..0df496c2ab9 --- /dev/null +++ b/src/quo/components/markdown/list/style.cljs @@ -0,0 +1,13 @@ +(ns quo.components.markdown.list.style) + +(defn container + [container-style] + (merge + {:padding-vertical 7 + :padding-horizontal 20 + :flex-direction :row + :align-items :flex-start} + container-style)) + +(def text-container + {:margin-left 8}) diff --git a/src/quo/components/markdown/list/view.cljs b/src/quo/components/markdown/list/view.cljs new file mode 100644 index 00000000000..ad2b1b6c659 --- /dev/null +++ b/src/quo/components/markdown/list/view.cljs @@ -0,0 +1,74 @@ +(ns quo.components.markdown.list.view + (:require + [quo.components.counter.step.view :as step] + [quo.components.icon :as icon] + [quo.components.markdown.list.style :as style] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.view :as context-tag] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn get-colors + [theme blur?] + (cond (and blur? (= theme :dark)) colors/white-opa-40 + (= theme :dark) colors/neutral-50 + :else colors/neutral-40)) + +(defn description-text + [{:keys [description tag-name tag-picture description-after-tag blur?]}] + (if-not tag-name + [text/text + {:accessibility-label :list-item-description + :size :paragraph-2} + description] + [rn/view {:style {:flex-direction :row :align-items :center}} + [text/text + {:accessibility-label :list-item-description + :size :paragraph-2} + description] + [rn/view {:style {:margin-left 4}} + [context-tag/view + {:blur? blur? + :size 24 + :profile-picture tag-picture + :full-name tag-name}]] + [text/text + {:style {:margin-left 4} + :accessibility-label :list-item-description-after-tag + :size :paragraph-2} + description-after-tag]])) + +(defn view + [{:keys [title description tag-picture tag-name description-after-tag step-number + customization-color type blur? container-style icon icon-props] + :or {type :bullet}}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/container container-style)} + [rn/view + (case type + :step + [step/view + {:in-blur-view? blur? + :customization-color customization-color + :type (if customization-color :complete :neutral)} step-number] + :lock + [icon/icon :i/locked {:color (get-colors theme blur?)}] + :custom-icon + [icon/icon icon icon-props] + [icon/icon :i/bullet {:color (get-colors theme blur?)}])] + [rn/view {:style style/text-container} + (when title + [text/text + {:accessibility-label :list-item-title + :weight :semi-bold + :size :paragraph-2} + title]) + (when description + [rn/view (when title {:style {:margin-top 0}}) + [description-text + {:description description + :tag-name tag-name + :tag-picture tag-picture + :description-after-tag description-after-tag + :blur? blur?}]])]])) diff --git a/src/quo/components/markdown/text.cljs b/src/quo/components/markdown/text.cljs new file mode 100644 index 00000000000..f2a7423c609 --- /dev/null +++ b/src/quo/components/markdown/text.cljs @@ -0,0 +1,45 @@ +(ns quo.components.markdown.text + (:require + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.typography :as typography] + [react-native.core :as rn] + [react-native.utils :as rn.utils])) + +(defn text-style + [{:keys [size align weight style]} theme] + (merge (case (or weight :regular) + :regular typography/font-regular + :medium typography/font-medium + :semi-bold typography/font-semi-bold + :bold typography/font-bold + :monospace typography/monospace + :code typography/code + :inherit nil) + (case (or size :paragraph-1) + :label typography/label + :paragraph-2 typography/paragraph-2 + :paragraph-1 typography/paragraph-1 + :heading-2 typography/heading-2 + :heading-1 typography/heading-1 + :inherit nil) + {:text-align (or align :auto)} + (if (:color style) + style + (assoc style + :color + (if (= theme :dark) colors/white colors/neutral-100))))) + +(defn- text-view + [props & children] + (let [theme (quo.context/use-theme) + style (text-style props theme)] + (into [rn/text + (merge {:style style} + (dissoc props :style :size :align :weight :color))] + children))) + +(defn text + [& argv] + (let [[props children] (rn.utils/get-props-and-children argv)] + (into [text-view props] children))) diff --git a/src/quo/components/markdown/text_component_spec.cljs b/src/quo/components/markdown/text_component_spec.cljs new file mode 100644 index 00000000000..79dfeee65d3 --- /dev/null +++ b/src/quo/components/markdown/text_component_spec.cljs @@ -0,0 +1,15 @@ +(ns quo.components.markdown.text-component-spec + (:require + ["@testing-library/react-native" :as rtl] + [quo.components.markdown.text :as text] + [reagent.core :as reagent])) + +(defn render-text + ([options value] + (rtl/render (reagent/as-element [text/text options value])))) + +(js/global.test "text renders with text" + (fn [] + (render-text {} "hello") + (-> (js/expect (rtl/screen.getByText "hello")) + (.toBeTruthy)))) diff --git a/src/quo/components/messages/author/style.cljs b/src/quo/components/messages/author/style.cljs new file mode 100644 index 00000000000..3454a630bd6 --- /dev/null +++ b/src/quo/components/messages/author/style.cljs @@ -0,0 +1,48 @@ +(ns quo.components.messages.author.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn container + [size] + (cond-> + {:flex-shrink 1 + :flex-wrap :nowrap + :flex-direction :row + :align-items :center + :height (if (= size 15) 22 18)})) + +(defn middle-dot + [theme] + {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme) + :margin-horizontal 2}) + +(defn chat-key-text + [theme] + {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}) + +(defn primary-name + [muted? theme size] + {:color (if muted? + colors/neutral-50 + (colors/theme-colors colors/neutral-100 colors/white theme)) + ;; iOS: primary-name height is 22.3 / 18.7 + ;; Android: primary-name height is 21.8 / 18.5 + :margin-vertical (if (= size 15) + (if platform/ios? -0.15 0) + (if platform/ios? -0.35 -0.25)) + :flex-shrink 1}) + +(defn secondary-name + [theme] + {:flex-shrink 999999 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn icon-container + [is-first? size] + {:margin-left (if is-first? 4 2) + :padding-top (if (= size 15) 6 4)}) + +(defn time-text + [theme] + {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}) diff --git a/src/quo/components/messages/author/view.cljs b/src/quo/components/messages/author/view.cljs new file mode 100644 index 00000000000..94980f83f1a --- /dev/null +++ b/src/quo/components/messages/author/view.cljs @@ -0,0 +1,90 @@ +(ns quo.components.messages.author.view + (:require + [clojure.string :as string] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.messages.author.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(def middle-dot "·") + +(defn view + [{:keys [primary-name secondary-name style short-chat-key time-str contact? verified? untrustworthy? + muted? size] + :or {size 13}}] + (let [theme (quo.context/use-theme) + + short-chat-key-component + (when (and (not verified?) short-chat-key) + [text/text + {:weight :monospace + :size :label + :number-of-lines 1 + :style (style/chat-key-text theme)} + short-chat-key]) + + time-str-component + (when time-str + [text/text + {:monospace true + :size :label + :accessibility-label :message-timestamp + :number-of-lines 1 + :style (style/time-text theme)} + time-str]) + + middle-dot-seperator-component + (when (and short-chat-key-component time-str-component) + [text/text + {:monospace true + :size :label + :number-of-lines 1 + :style (style/middle-dot theme)} + middle-dot])] + [rn/view + {:style (merge (style/container size) style)} + [text/text + {:weight :semi-bold + :size (if (= size 15) :paragraph-1 :paragraph-2) + :number-of-lines 1 + :accessibility-label :author-primary-name + :style (style/primary-name muted? theme size)} + primary-name] + (when-not (string/blank? secondary-name) + [:<> + [text/text + {:size :label + :number-of-lines 1 + :style (style/middle-dot theme)} + middle-dot] + [text/text + {:weight :medium + :size :label + :number-of-lines 1 + :accessibility-label :author-secondary-name + :style (style/secondary-name theme)} + secondary-name]]) + (when contact? + [icons/icon :main-icons2/contact + {:size 12 + :no-color true + :container-style (style/icon-container true size)}]) + (cond + verified? + [icons/icon :main-icons2/verified + {:size 12 + :no-color true + :container-style (style/icon-container contact? size)}] + untrustworthy? + [icons/icon :main-icons2/untrustworthy + {:size 12 + :no-color true + :container-style (style/icon-container contact? size)}]) + + (when (or short-chat-key-component time-str-component) + [rn/view {:style {:width 8}}]) + + short-chat-key-component + middle-dot-seperator-component + time-str-component])) diff --git a/src/quo/components/messages/gap.cljs b/src/quo/components/messages/gap.cljs new file mode 100644 index 00000000000..c1b6e9c35ab --- /dev/null +++ b/src/quo/components/messages/gap.cljs @@ -0,0 +1,165 @@ +(ns quo.components.messages.gap + (:require + [oops.core :refer [oget]] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +;;; helpers +(def themes + {:light {:icon colors/neutral-40 + :time colors/neutral-50 + :background colors/neutral-5} + :dark {:icon colors/neutral-60 + :time colors/neutral-40 + :background colors/neutral-95}}) + +(defn get-color + [k theme] + (get-in themes [theme k])) + +(def ui-images + {:light {:horizontal (js/require "../resources/images/ui/message-gap-hborder-light.png") + :vertical (js/require "../resources/images/ui/message-gap-vborder-light.png") + :circles (js/require "../resources/images/ui/message-gap-circle-bg-light.png")} + :dark {:horizontal (js/require "../resources/images/ui/message-gap-hborder-dark.png") + :circles (js/require "../resources/images/ui/message-gap-circle-bg-dark.png")}}) + +(defn get-image + [k theme] + (get-in ui-images [theme k])) + +;;; components +;;;; borders +(defn hborder + [{:keys [type style]}] + (let [theme (quo.context/use-theme)] + [rn/image + {:source (get-image :horizontal theme) + :resize-mode :repeat + :style (merge {:position :absolute + :left 0 + :padding-horizontal 4 + :overflow :hidden + :width "110%" + :height 8 + :margin-left -4} + (if (= type :top) + {:top 0} + {:transform [{:rotate-z "180deg"}] + :bottom 0}) + style)}])) + +(defn vborder + [type body-height] + (let [theme (quo.context/use-theme) + height @body-height + img (get-image :vertical theme)] + (when (and img height) + [rn/image + {:source img + :resize-mode :repeat + :style (merge + {:position :absolute + :top 4 + :height (- height 8) + :width 4} + (if (= type :left) + {:left 0} + {:transform [{:rotate "180deg"}] + :right 0}))}]))) + +;;;; others +(defn circle + [] + (let [theme (quo.context/use-theme)] + [rn/view + {:width 9 + :height 9 + :border-width 1 + :margin 4 + :flex 0 + :border-color (get-color :icon theme) + :border-radius 50}])) + +(defn timestamp + [s] + (let [theme (quo.context/use-theme)] + [text/text + {:size :label + :style {:text-transform :none + :color (get-color :time theme)}} + s])) + +(defn info-button + [on-press] + [rn/pressable + {:on-press on-press} + [icon/icon "message-gap-info" {:size 12 :no-color true :container-style {:padding 4}}]]) + +;;;; timeline/body +(defn timeline + [] + (let [theme (quo.context/use-theme)] + [rn/view + {:flex 0 + :margin-right 20 + :align-items :center + :width 9 + :justify-content :space-between} + [circle] + [rn/image {:style {:flex 1} :source (get-image :circles theme) :resize-mode :repeat}] + [circle]])) + +(defn body + [timestamp-far timestamp-near on-info-button-pressed on-press warning-label] + [rn/view {:flex 1} + [rn/view + {:flex-direction :row + :align-items :center + :justify-content :space-between + :margin-right 2} + [timestamp timestamp-far] + (when on-info-button-pressed [info-button on-info-button-pressed])] + [rn/pressable + {:style {:flex 1 :margin-top 16 :margin-bottom 20} + :on-press #(when on-press (on-press))} + [text/text warning-label]] + + [timestamp timestamp-near]]) + +;;; main +(defn gap + "if `gap-ids` and `chat-id` are provided, press the main text area to fetch messages + if `on-info-button-pressed` fn is provided, the info button will show up and is pressable" + [{:keys [timestamp-far + timestamp-near + on-info-button-pressed + style + on-press + warning-label]}] + (let [theme (quo.context/use-theme) + [body-height set-body-height] (rn/use-state nil) + on-layout (rn/use-callback #(set-body-height + (oget % "nativeEvent.layout.height")))] + [rn/view + {:on-layout on-layout + :overflow :hidden + :flex 1} + [hborder {:type :top}] + [hborder {:type :bottom}] + [rn/view + (merge {:width "100%" + :background-color (get-color :background theme) + :flex-direction :row + :padding 20 + :padding-left 31 + :margin-vertical 4} + style) + + [timeline] + [body timestamp-far timestamp-near on-info-button-pressed on-press warning-label]] + [vborder :left body-height] + [vborder :right body-height]])) diff --git a/src/quo/components/messages/system_message/style.cljs b/src/quo/components/messages/system_message/style.cljs new file mode 100644 index 00000000000..0e760a37ff3 --- /dev/null +++ b/src/quo/components/messages/system_message/style.cljs @@ -0,0 +1,96 @@ +(ns quo.components.messages.system-message.style + (:require + [quo.foundations.colors :as colors])) + +(defn text-color + [theme] + (colors/theme-colors colors/neutral-100 colors/white theme)) + +(defn time-color + [theme] + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)) + +(def sm-icon-wrapper {:margin-right 8}) + +(def sm-timestamp-wrapper {:margin-left 8}) + +(defn sm-timestamp-text + [theme] + {:color (time-color theme) + :text-transform :none}) + +(def sm-user-avatar-wrapper {:margin-right 4}) + +(def split-text-wrapper {:flex-direction :row :flex-shrink 0 :align-items :center}) + +(defn each-split-text + [theme indx label-vector] + {:color (text-color theme) + :margin-right (if (= indx (dec (count label-vector))) + 0 + 3)}) + +(def system-message-base-wrapper + {:flex-direction :row + :flex 1}) + +(def system-message-base-content-wrapper + {:align-self :center + :flex-direction :row + :flex 1}) + +(def system-message-deleted-wrapper + {:flex-direction :row + :align-items :center}) + +(defn system-message-deleted-text + [theme] + {:color (text-color theme)}) + +(def system-message-contact-wrapper + {:flex-direction :row + :align-items :center + :flex-shrink 1 + :flex-wrap :nowrap}) + +(def system-message-contact-account-wrapper + {:flex-direction :row + :align-items :center + :flex-shrink 1}) + +(def system-message-contact-account-name + {:flex-shrink 1}) + +(def system-message-contact-request-wrapper + {:flex-direction :row + :align-items :center + :flex-shrink 1 + :flex-wrap :nowrap}) + +(def system-message-contact-request-account-wrapper + {:flex-direction :row + :align-items :center + :margin-left 4 + :flex-shrink 1}) + +(def system-message-contact-request-account-name + {:flex-shrink 1}) + +(def system-message-pinned-wrapper + {:flex 1}) + +(def system-message-pinned-content-wrapper + {:flex-direction :row + :align-items :center + :flex-wrap :nowrap}) + +(def system-message-pinned-content-pinned-by + {:flex-shrink 1}) + +(def system-message-deleted-animation-start-bg-color colors/danger-50-opa-5) +(def system-message-deleted-animation-end-bg-color colors/danger-50-opa-0) + +(def system-message-wrapper + {:padding-horizontal 12 + :padding-vertical 8 + :flex 1}) diff --git a/src/quo/components/messages/system_message/view.cljs b/src/quo/components/messages/system_message/view.cljs new file mode 100644 index 00000000000..ab933ebc4b4 --- /dev/null +++ b/src/quo/components/messages/system_message/view.cljs @@ -0,0 +1,198 @@ +(ns quo.components.messages.system-message.view + (:require + [clojure.string :as string] + [quo.components.avatars.icon-avatar :as icon-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.markdown.text :as text] + [quo.components.messages.system-message.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [utils.i18n :as i18n])) + +(defn sm-icon + [{:keys [icon color opacity]}] + [rn/view + {:style style/sm-icon-wrapper} + [icon-avatar/icon-avatar + {:size :size-32 + :icon icon + :color color + :opacity opacity}]]) + +(defn sm-timestamp + [timestamp theme] + [rn/view style/sm-timestamp-wrapper + [text/text + {:size :label + :style (style/sm-timestamp-text theme)} + timestamp]]) + +(defn sm-user-avatar + [display-name photo-path] + [rn/view style/sm-user-avatar-wrapper + [user-avatar/user-avatar + {:size :xxxs + :full-name display-name + :profile-picture photo-path + :status-indicator? false}]]) + +(defn split-text + [label theme add-pred?] + (let [label-vector (map-indexed vector (string/split label " "))] + [rn/view {:style style/split-text-wrapper} + (when add-pred? + [text/text {} " "]) + (for [[indx item] label-vector] + ^{:key indx} + [text/text + {:size :paragraph-2 + :style (style/each-split-text theme indx label-vector)} + item])])) + +(defn system-message-base + [{:keys [icon]} child] + [rn/view {:style style/system-message-base-wrapper} + [sm-icon icon] + [rn/view {:style style/system-message-base-content-wrapper} child]]) + +(defn system-message-deleted + [{:keys [label child timestamp]}] + (let [theme (quo.context/use-theme)] + [system-message-base + {:icon {:icon :i/delete + :color :danger + :opacity 5}} + [rn/view {:style style/system-message-deleted-wrapper} + (if child + child + [text/text + {:size :paragraph-2 + :style (style/system-message-deleted-text theme)} + (or label (i18n/label :t/message-deleted))]) + [sm-timestamp timestamp theme]]])) + +(defn system-message-contact + [{:keys [display-name photo-path customization-color timestamp]} label icon] + (let [theme (quo.context/use-theme)] + [system-message-base + {:icon {:icon icon + :color (or customization-color :primary) + :opacity 5}} + [rn/view + {:style style/system-message-contact-wrapper} + [rn/view {:style style/system-message-contact-account-wrapper} + [sm-user-avatar display-name photo-path] + [text/text + {:weight :semi-bold + :number-of-lines 1 + :style style/system-message-contact-account-name + :size :paragraph-2} + display-name]] + [split-text label theme true] + [sm-timestamp timestamp theme]]])) + +(defn system-message-added + [data] + [system-message-contact data (i18n/label :t/contact-request-is-now-a-contact) :i/add-user]) + +(defn system-message-removed + [{:keys [incoming?] :as data}] + [system-message-contact + data + (if incoming? + (i18n/label :t/contact-request-removed-you-as-contact) + (i18n/label :t/contact-request-removed-as-contact)) + :i/sad]) + +(defn system-message-contact-request + [{:keys [display-name photo-path customization-color timestamp incoming?]}] + (let [theme (quo.context/use-theme)] + [system-message-base + {:icon {:icon :i/add-user + :color (or customization-color :primary) + :opacity 5}} + [rn/view + {:style style/system-message-contact-request-wrapper} + (when-not incoming? [split-text "Contact request sent to" theme false]) + [rn/view {:style style/system-message-contact-request-account-wrapper} + [sm-user-avatar display-name photo-path] + [text/text + {:weight :semi-bold + :number-of-lines 1 + :style style/system-message-contact-request-account-name + :size :paragraph-2} + display-name]] + (when incoming? [split-text "sent you a contact request" theme true]) + [sm-timestamp timestamp theme]]])) + +(defn system-message-group + [{:keys [customization-color child]}] + [system-message-base + {:icon {:icon :i/add-user + :color (or customization-color :primary) + :opacity 5}} + [rn/view child]]) + +(defn system-message-pinned + [{:keys [pinned-by child customization-color timestamp]}] + (let [theme (quo.context/use-theme)] + [system-message-base + {:icon {:icon :i/pin + :color (or customization-color :primary) + :opacity 5}} + [rn/view {:style style/system-message-pinned-wrapper} + [rn/view + {:style style/system-message-pinned-content-wrapper} + [text/text + {:weight :semi-bold + :number-of-lines 1 + :style style/system-message-pinned-content-pinned-by + :size :paragraph-2} + pinned-by] + [split-text (i18n/label :t/pinned-a-message) theme true] + [sm-timestamp timestamp theme]] + (when child child)]])) + +(defn f-system-message + [{:keys [type animate-bg-color? bg-color-animation-duration on-long-press] + :or {bg-color-animation-duration 1000} + :as data}] + (let [animated-bg-color (reanimated/use-shared-value + (if animate-bg-color? + style/system-message-deleted-animation-start-bg-color + style/system-message-deleted-animation-end-bg-color)) + wrapper (if (or on-long-press animate-bg-color?) + reanimated/touchable-opacity + rn/view) + animated-style (reanimated/apply-animations-to-style + {:background-color animated-bg-color} + (assoc style/system-message-wrapper + :background-color + animated-bg-color))] + + (when animate-bg-color? + (reanimated/animate-shared-value-with-delay + animated-bg-color + style/system-message-deleted-animation-end-bg-color + 0 + :linear + bg-color-animation-duration)) + + [wrapper + {:style (if (or on-long-press animate-bg-color?) + animated-style + style/system-message-wrapper) + :on-long-press on-long-press} + (case type + :pinned [system-message-pinned data] + :deleted [system-message-deleted data] + :contact-request [system-message-contact-request data] + :added [system-message-added data] + :removed [system-message-removed data] + :group [system-message-group data] + nil)])) + +(defn system-message + [message] + [:f> f-system-message message]) diff --git a/src/quo/components/navigation/bottom_nav_tab/styles.cljs b/src/quo/components/navigation/bottom_nav_tab/styles.cljs new file mode 100644 index 00000000000..c5f9ac53f76 --- /dev/null +++ b/src/quo/components/navigation/bottom_nav_tab/styles.cljs @@ -0,0 +1,18 @@ +(ns quo.components.navigation.bottom-nav-tab.styles + (:require + [quo.foundations.colors :as colors])) + +(defn notification-dot + [customization-color] + {:width 8 + :height 8 + :border-radius 4 + :top 6 + :left 51 + :position :absolute + :background-color (colors/resolve-color customization-color nil)}) + +(def notification-counter + {:position :absolute + :left 48 + :top 2}) diff --git a/src/quo/components/navigation/bottom_nav_tab/view.cljs b/src/quo/components/navigation/bottom_nav_tab/view.cljs new file mode 100644 index 00000000000..723eb13d233 --- /dev/null +++ b/src/quo/components/navigation/bottom_nav_tab/view.cljs @@ -0,0 +1,90 @@ +(ns quo.components.navigation.bottom-nav-tab.view + (:require + [quo.components.counter.counter.view :as counter] + [quo.components.icons.icons :as icons] + [quo.components.navigation.bottom-nav-tab.styles :as styles] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated])) + +(defn toggle-background-color + [background-color press-out? pass-through?] + (reanimated/set-shared-value + background-color + (cond + press-out? "transparent" + pass-through? colors/white-opa-5 + :else colors/neutral-70))) + +(defn- f-bottom-nav-tab + "[bottom-nav-tab opts] + opts + {:icon :i/communities + :new-notifications? true/false + :notification-indicator :unread-dot/:counter + :counter-label number + :on-press bottom-tab on-press function + :pass-through? true/false + :icon-color-anim reanimated shared value + :customization-color Customization color for the notification mark + " + [{:keys [icon new-notifications? notification-indicator counter-label + on-press pass-through? icon-color-anim accessibility-label test-ID + customization-color] + :or {customization-color :blue}}] + (let [icon-animated-style (reanimated/apply-animations-to-style + {:tint-color icon-color-anim} + {:width 24 + :height 24 + :margin-left 33 + :margin-top 8}) + background-color (reanimated/use-shared-value "transparent") + background-animated-style (reanimated/apply-animations-to-style + {:background-color background-color} + {:width 90 + :height 40 + :border-radius 10})] + [rn/pressable + {:test-ID test-ID + :allow-multiple-presses? true + :on-press on-press + :on-press-in #(toggle-background-color background-color false pass-through?) + :on-press-out #(toggle-background-color background-color true pass-through?) + :accessibility-label accessibility-label} + [reanimated/view {:style background-animated-style} + ;; In android animations are not working for the animated components which are nested by + ;; hole-view,. Interestingly this only happens when hole view and blur view are used together + ;; Similar behavior is also seen while removing holes, and to fix that we used key and + ;; force-rendered view. But we need animations faster for tab clicks, so we can't rely on + ;; reagent atoms, so for now only using hole view for the ios tab icon notification boundary + (if platform/ios? + [hole-view/hole-view + {:key new-notifications? ;; Key is required to force removal of holes + :holes (cond + (not new-notifications?) ;; No new notifications, remove holes + [] + + (= notification-indicator :unread-dot) + [{:x 50 :y 5 :width 10 :height 10 :borderRadius 5}] + + :else + [{:x 47 :y 1 :width 18 :height 18 :borderRadius 7}])} + [reanimated/image + {:style icon-animated-style + :source (icons/icon-source (keyword (str icon 24)))}]] + [reanimated/image + {:style icon-animated-style + :source (icons/icon-source (keyword (str icon 24)))}]) + (when new-notifications? + (if (= notification-indicator :counter) + [counter/view + {:customization-color customization-color + :container-style styles/notification-counter} + counter-label] + [rn/view {:style (styles/notification-dot customization-color)}]))]])) + +(defn view + [opts] + [:f> f-bottom-nav-tab opts]) diff --git a/src/quo/components/navigation/floating_shell_button/style.cljs b/src/quo/components/navigation/floating_shell_button/style.cljs new file mode 100644 index 00000000000..a940a04e10e --- /dev/null +++ b/src/quo/components/navigation/floating_shell_button/style.cljs @@ -0,0 +1,23 @@ +(ns quo.components.navigation.floating-shell-button.style + (:require + [react-native.reanimated :as reanimated])) + +(defn floating-shell-button + [style opacity-anim] + (reanimated/apply-animations-to-style + (if opacity-anim + {:opacity opacity-anim} + {}) + (merge + {:flex-direction :row + :height 44 + :align-self :center + :padding-top 8 + :padding-horizontal 12 + :pointer-events :box-none} + style))) + +(def right-section + {:position :absolute + :flex-direction :row + :right 0}) diff --git a/src/quo/components/navigation/floating_shell_button/view.cljs b/src/quo/components/navigation/floating_shell_button/view.cljs new file mode 100644 index 00000000000..f6ea1dbe834 --- /dev/null +++ b/src/quo/components/navigation/floating_shell_button/view.cljs @@ -0,0 +1,48 @@ +(ns quo.components.navigation.floating-shell-button.view + (:require + [quo.components.buttons.dynamic-button.view :as dynamic-button] + [quo.components.navigation.floating-shell-button.style :as style] + [react-native.core :as rn] + [react-native.reanimated :as reanimated])) + +(defn dynamic-button-view + [type dynamic-buttons style] + (when-let [{:keys [on-press customization-color label] :as props} (get dynamic-buttons type)] + [dynamic-button/view + {:type type + :label label + :on-press on-press + :count (:count props) + :style style + :customization-color customization-color}])) + +(defn- section + [children] + [rn/view {:style {:flex 1} :pointer-events :box-none} children]) + +(defn- f-floating-shell-button + [dynamic-buttons style opacity-anim] + [reanimated/view {:style (style/floating-shell-button style opacity-anim)} + ;; Left Section + [section + [dynamic-button-view :search dynamic-buttons + {:position :absolute + :right 8}]] + ;; Right Section + [section + [rn/view + {:style style/right-section} + [dynamic-button-view :mention dynamic-buttons {:margin-left 8}] + [dynamic-button-view :notification-down dynamic-buttons {:margin-left 8}] + [dynamic-button-view :notification-up dynamic-buttons {:margin-left 8}] + [dynamic-button-view :scroll-to-bottom dynamic-buttons {:margin-left 8}]]]]) + +(defn view + "[floating-shell-button dynamic-buttons style opacity-anim pointer-anim] + dynamic-buttons {:button-type {:on-press on-press :count count}} + style override style + opacity-anim reanimated value (optional)" + ([dynamic-buttons style] + [:f> f-floating-shell-button dynamic-buttons style nil]) + ([dynamic-buttons style opacity-anim] + [:f> f-floating-shell-button dynamic-buttons style opacity-anim])) diff --git a/src/quo/components/navigation/page_nav/style.cljs b/src/quo/components/navigation/page_nav/style.cljs new file mode 100644 index 00000000000..c085ef97b22 --- /dev/null +++ b/src/quo/components/navigation/page_nav/style.cljs @@ -0,0 +1,95 @@ +(ns quo.components.navigation.page-nav.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [margin-top] + {:margin-top margin-top + :padding-horizontal 20 + :padding-vertical 12 + :height 56 + :flex-direction :row + :justify-content :space-between + :align-items :center}) + +(def icon-container + {:flex-grow 1 + :flex-basis 1}) + +(defn center-content-container + [centered?] + {:flex 1 + :margin-horizontal 12 + :flex-direction :row + :align-items :center + :justify-content (if centered? :center :flex-start)}) + +(def right-actions-container + {:flex-direction :row + :justify-content :flex-end}) + +(def right-actions-spacing + {:width 12}) + +(defn right-content + [min-size? centered-content?] + (cond-> {} + centered-content? (assoc :flex-grow 1 + :flex-basis 1) + min-size? (assoc :min-height 32))) + +(def token-logo + {:width 16 :height 16}) + +(def token-name + {:margin-left 4 + :text-align-vertical :center}) + +(defn token-abbreviation + [theme background] + (let [color (case background + :photo nil + :blur (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-40 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))] + {:margin-left 4 + :color color + :text-align-vertical :center})) + +(def channel-emoji + {:width 20 :height 20}) + +(defn channel-icon-color + [theme background] + (case background + :photo nil + :blur (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(def channel-name + {:margin-horizontal 4 + :text-align-vertical :center}) + +(def group-avatar-picture + {:margin-right 8}) + +(def title-description-container + {:height 32 + :justify-content :center}) + +(def title-description-title + {:text-align-vertical :center}) + +(defn title-description-description + [theme background] + (let [color (case background + :photo (colors/theme-colors colors/neutral-80-opa-80-blur colors/white-opa-70 theme) + :blur (colors/theme-colors colors/neutral-80-opa-50 colors/white-opa-40 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))] + {:color color + :text-align-vertical :center})) + +(def community-network-logo + {:width 24 + :height 24 + :border-radius 12 + :margin-right 6}) diff --git a/src/quo/components/navigation/page_nav/view.cljs b/src/quo/components/navigation/page_nav/view.cljs new file mode 100644 index 00000000000..adc0bddf488 --- /dev/null +++ b/src/quo/components/navigation/page_nav/view.cljs @@ -0,0 +1,350 @@ +(ns quo.components.navigation.page-nav.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.group-avatar.view :as group-avatar] + [quo.components.buttons.button.properties :as button-properties] + [quo.components.buttons.button.view :as button] + [quo.components.dropdowns.dropdown.properties :as dropdown-properties] + [quo.components.dropdowns.dropdown.view :as dropdown] + [quo.components.dropdowns.network-dropdown.view :as network-dropdown] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.navigation.page-nav.style :as style] + [quo.context] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [utils.worklets.profile-header :as header-worklet])) + +(def ^:private button-type + {:white :grey + :neutral-5 :dark-grey + :neutral-90 :grey + :neutral-95 :dark-grey + :neutral-100 :black + :photo :grey + :blur :grey}) + +(defn- page-nav-base + [{:keys [margin-top background on-press accessibility-label icon-name behind-overlay? align-center?] + :or {background :white}} + & children] + (into [rn/view {:style (style/container margin-top)} + (when icon-name + [rn/view (when align-center? {:style style/icon-container}) + [button/button + {:type (button-type background) + :icon-only? true + :size 32 + :on-press on-press + :background (if behind-overlay? + :blur + (when (button-properties/backgrounds background) background)) + :accessibility-label accessibility-label} + icon-name]])] + children)) + +(defn- right-section-spacing [] [rn/view {:style style/right-actions-spacing}]) + +(defmulti add-button + (fn [{:keys [button-props]}] + (:content-type button-props))) + +(defmethod add-button :account-switcher + [{:keys [support-account-switcher? button-props]}] + (when support-account-switcher? + (let [{:keys [customization-color on-press emoji type]} button-props] + [rn/pressable {:on-press on-press} + [account-avatar/view + {:emoji emoji + :size 32 + :type (or type :default) + :customization-color customization-color}]]))) + +(defmethod add-button :default + [{:keys [background behind-overlay? button-props]}] + (let [{:keys [label icon-name]} button-props] + [button/button + (assoc button-props + :type (button-type background) + :icon-only? (boolean icon-name) + :size 32 + :accessible true + :background (if behind-overlay? + :blur + (when (button-properties/backgrounds background) background))) + (or label icon-name)])) + +(defn- add-right-buttons-xf + [max-actions background behind-overlay? support-account-switcher?] + (comp (filter map?) + (take max-actions) + (map (fn [button-props] + (add-button {:background background + :behind-overlay? behind-overlay? + :support-account-switcher? support-account-switcher? + :button-props button-props}))) + (interpose [right-section-spacing]))) + +(defn- right-content + [{:keys [background content max-actions min-size? support-account-switcher? + behind-overlay? centered-content?] + :or {support-account-switcher? true + centered-content? true}}] + [rn/view (style/right-content min-size? centered-content?) + (when (coll? content) + (into [rn/view {:style style/right-actions-container}] + (add-right-buttons-xf max-actions background behind-overlay? support-account-switcher?) + content))]) + +(def header-height 155) +(def page-nav-height 25) +(def threshold (- header-height page-nav-height)) + +(defn- title-center + [{:keys [title scroll-y center-content-container-style]}] + (let [animated-style (when scroll-y + (header-worklet/profile-header-animation scroll-y + threshold + page-nav-height))] + [reanimated/view + {:style [center-content-container-style animated-style]} + [text/text + {:weight :medium + :size :paragraph-1 + :number-of-lines 1} + title]])) + +(defn- dropdown-center + [{:keys [background dropdown-on-press dropdown-selected? dropdown-text + center-content-container-style]}] + (let [theme (quo.context/use-theme) + dropdown-type (cond + (= background :photo) :grey + (and (= theme :dark) (= background :blur)) :grey + :else :ghost) + dropdown-state (if dropdown-selected? :active :default)] + [reanimated/view {:style center-content-container-style} + [dropdown/view + {:type dropdown-type + :state dropdown-state + :size :size-32 + :background (when (dropdown-properties/backgrounds background) background) + :on-press dropdown-on-press} + dropdown-text]])) + +(defn- token-center + [{:keys [background token-logo token-name token-abbreviation center-content-container-style]}] + (let [theme (quo.context/use-theme)] + [reanimated/view {:style center-content-container-style} + [rn/image {:style style/token-logo :source token-logo}] + [text/text + {:style style/token-name + :weight :semi-bold + :size :paragraph-1 + :number-of-lines 1} + token-name] + [text/text + {:style (style/token-abbreviation theme background) + :weight :medium + :size :paragraph-2 + :number-of-lines 1} + token-abbreviation]])) + +(defn- channel-center + [{:keys [background channel-emoji channel-name channel-icon center-content-container-style]}] + (let [theme (quo.context/use-theme)] + [reanimated/view {:style center-content-container-style} + [rn/text {:style style/channel-emoji} + channel-emoji] + [text/text + {:style style/channel-name + :weight :semi-bold + :size :paragraph-1 + :number-of-lines 1} + (str "# " channel-name)] + [icons/icon channel-icon {:size 16 :color (style/channel-icon-color theme background)}]])) + +(defn- title-description-center + [{:keys [background picture title description center-content-container-style]}] + (let [theme (quo.context/use-theme)] + [reanimated/view {:style center-content-container-style} + (when picture + [rn/view {:style style/group-avatar-picture} + [group-avatar/view {:picture picture :size :size-28}]]) + [rn/view {:style style/title-description-container} + [text/text + {:style style/title-description-title + :weight :semi-bold + :size :paragraph-1 + :number-of-lines 1} + title] + [text/text + {:style (style/title-description-description theme background) + :weight :medium + :size :paragraph-2 + :number-of-lines 1} + description]]])) + +(defn- community-network-center + [{:keys [type community-logo network-logo community-name network-name center-content-container-style]}] + (let [community? (= type :community) + shown-logo (if community? community-logo network-logo) + shown-name (if community? community-name network-name)] + [reanimated/view {:style center-content-container-style} + [rn/image + {:style style/community-network-logo + :source shown-logo}] + [text/text + {:weight :semi-bold + :size :paragraph-1 + :number-of-lines 1} + shown-name]])) + +(defn- wallet-networks-center + [{:keys [networks networks-filtered? networks-on-press show-new-chain-indicator? background + center-content-container-style]}] + [reanimated/view {:style center-content-container-style} + [network-dropdown/view + {:state :default + :networks-filtered? networks-filtered? + :on-press networks-on-press + :blur? (= background :blur) + :show-new-chain-indicator? show-new-chain-indicator?} + networks]]) + +(defn page-nav + "Props: + - type: defaults to `:no-title`. + - background: + `:white`, `:neutral-5`, `:neutral-90`, `:neutral-95`, `:neutral-100`, `:photo` or `:blur` + - accessibility-label + - on-press: callback for left button + - icon-name: icon for left button + - right-side (optional): + - vector of maps to render buttons, e.g.: + {:icon-name :i/my-icon + :on-press (fn callback [] nil) + :accessibility-label \"an optional label\"} + + Depending on the `type` selected, different properties are accepted: + `:title` + - title + - text-align: `:center` or `:left` + - scroll-y: a shared value (optional) + `:dropdown` + - dropdown-on-press: a callback + - dropdown-selected?: a boolean + - dropdown-text + `:token` + - token-logo: a valid rn/image `:source` value + - token-name: string + - token-abbreviation: string + `:channel` + - channel-emoji: an emoji in a string + - channel-name + - channel-icon: an icon keyword (:i/members, :i/lock, etc.) + `:title-description` + - title + - description + - picture: a valid rn/image `:source` value + `:wallet-networks` + - networks: a vector of network image source + - networks-on-press: a callback + `:community` + - community-name + - community-logo: a valid rn/image `:source` value + `:network` + - network-name + - network-logo a valid rn/image `:source` value" + [{:keys [type right-side background text-align behind-overlay? center-opacity] + :or {type :no-title + text-align :center + right-side :none + background :white} + :as props}] + (let [centered-content? (case type + :title (= text-align :center) + (:dropdown :wallet-networks) true + false) + center-content-container-style (reanimated/apply-animations-to-style + (if center-opacity + {:opacity center-opacity} + nil) + (style/center-content-container centered-content?)) + props-with-style (assoc props + :center-content-container-style + center-content-container-style)] + (case type + :no-title + [page-nav-base props + [right-content + {:background background + :content right-side + :max-actions 3 + :behind-overlay? behind-overlay?}]] + + :title + (let [centered? (= text-align :center)] + [page-nav-base props + [title-center props-with-style] + [right-content + {:background background + :content right-side + :max-actions (if centered? 1 3) + :min-size? centered?}]]) + + :dropdown + [page-nav-base props + [dropdown-center props-with-style] + [right-content + {:background background + :content right-side + :max-actions 1 + :support-account-switcher? false}]] + + :token + [page-nav-base props + [token-center props-with-style] + [right-content + {:background background + :content right-side + :max-actions 3}]] + + :channel + [page-nav-base props + [channel-center props-with-style] + [right-content + {:background background + :content right-side + :max-actions 3 + :support-account-switcher? false}]] + + :title-description + [page-nav-base props + [title-description-center props-with-style] + [right-content + {:background background + :content right-side + :max-actions 2 + :support-account-switcher? false}]] + + :wallet-networks + [page-nav-base props + [wallet-networks-center props-with-style] + [right-content + {:background background + :content right-side + :max-actions 3 + :min-size? true}]] + + (:community :network) + [page-nav-base props + [community-network-center props-with-style] + [right-content + {:background background + :content right-side + :max-actions 3 + :support-account-switcher? false + :centered-content? centered-content?}]] + + nil))) diff --git a/src/quo/components/navigation/top_nav/component_spec.cljs b/src/quo/components/navigation/top_nav/component_spec.cljs new file mode 100644 index 00000000000..db9c6cf137e --- /dev/null +++ b/src/quo/components/navigation/top_nav/component_spec.cljs @@ -0,0 +1,39 @@ +(ns quo.components.navigation.top-nav.component-spec + (:require + [quo.components.navigation.top-nav.view :as top-nav] + [test-helpers.component :as h])) + +(h/describe "Top Nav component" + (h/test "Renders default" + (h/render-with-theme-provider [top-nav/view]) + (h/is-truthy (h/get-by-label-text :open-scanner-button)) + (h/is-truthy (h/get-by-label-text :open-activity-center-button)) + (h/is-truthy (h/get-by-label-text :show-qr-button)) + (h/is-truthy (h/get-by-label-text :open-profile))) + + (h/test "On press works for all buttons and avatar" + (let [avatar-on-press (h/mock-fn) + scan-on-press (h/mock-fn) + activity-center-on-press (h/mock-fn) + qr-code-on-press (h/mock-fn)] + + (h/render-with-theme-provider + [top-nav/view + {:avatar-on-press avatar-on-press + :scan-on-press scan-on-press + :activity-center-on-press activity-center-on-press + :qr-code-on-press qr-code-on-press}]) + + (h/fire-event :press (h/get-by-label-text :open-scanner-button)) + (h/was-called scan-on-press) + + (h/fire-event :press (h/get-by-label-text :open-activity-center-button)) + (h/was-called activity-center-on-press) + + (h/fire-event :press (h/get-by-label-text :show-qr-button)) + (h/was-called qr-code-on-press) + + (h/fire-event :press (h/get-by-label-text :open-profile)) + (h/was-called avatar-on-press)))) + + diff --git a/src/quo/components/navigation/top_nav/style.cljs b/src/quo/components/navigation/top_nav/style.cljs new file mode 100644 index 00000000000..e877c95d13a --- /dev/null +++ b/src/quo/components/navigation/top_nav/style.cljs @@ -0,0 +1,34 @@ +(ns quo.components.navigation.top-nav.style) + +(defn unread-indicator + [unread-count max-value] + (let [right-offset (cond + (> unread-count max-value) + -14 + ;; Greater than 9 means we'll need 2 digits to represent the text. + (> unread-count 9) + -10 + :else -6)] + {:position :absolute + :top -6 + :right right-offset + :z-index 4})) + +(def unread-dot + {:position :absolute + :top -2 + :bottom 0 + :right 0 + :left 38}) + +(def right-section + {:flex-direction :row}) + +(def top-nav-container + {:height 56}) + +(def top-nav-inner-container + {:flex 1 + :flex-direction :row + :justify-content :space-between + :align-items :center}) diff --git a/src/quo/components/navigation/top_nav/view.cljs b/src/quo/components/navigation/top_nav/view.cljs new file mode 100644 index 00000000000..7b133582162 --- /dev/null +++ b/src/quo/components/navigation/top_nav/view.cljs @@ -0,0 +1,147 @@ +(ns quo.components.navigation.top-nav.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.buttons.button.view :as button] + [quo.components.common.notification-dot.view :as notification-dot] + [quo.components.counter.counter.view :as counter] + [quo.components.navigation.top-nav.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.hole-view :as hole-view])) + +(def notification-dot-hole + [{:x 37 + :y -2 + :width 9 + :height 9 + :borderRadius 4}]) + +(defn unread-counter-hole + [notification-count] + (let [x (case (count (str notification-count)) + (1 2) 33 + 29) + width (case (count (str notification-count)) + 1 16 + 2 20 + 28)] + (if (pos? (js/Number notification-count)) + [{:x x + :y -5 + :width width + :height 16 + :borderRadius 6}] + []))) + +(defn- get-button-common-props + [{:keys [theme blur?]}] + {:icon-only? true + :size 32 + :container-style {:margin-left 12} + :type (cond + (and (not blur?) (= :dark theme)) :dark-grey + :else :grey) + :background (when blur? :blur)}) + +(defn- unread-indicator + [{:keys [notification-count max-value customization-color type]}] + (when (pos? notification-count) + [counter/view + {:accessibility-label :activity-center-unread-count + :type type + :customization-color customization-color + :container-style (style/unread-indicator notification-count + max-value)} + notification-count])) + +(defn unread-dot + [{:keys [customization-color blur? notification]}] + [notification-dot/view + {:style style/unread-dot + :blur? blur? + :customization-color (when (= notification :notification) customization-color)}]) + +(defn notification-highlight + [{:keys [notification notification-count + max-unread-notifications customization-color] + :as props}] + (case notification + (:seen :notification) [unread-dot props] + (:mention :mention-seen) [unread-indicator + {:type (if (= notification :mention-seen) :grey :default) + :notification-count notification-count + :max-value max-unread-notifications + :customization-color customization-color}] + [:<>])) + +(defn- left-section + [{:keys [avatar-props on-press customization-color]}] + [rn/pressable + {:on-press on-press + :accessibility-label :open-profile} + [user-avatar/user-avatar + (merge {:status-indicator? true + :customization-color customization-color + :size :small} + avatar-props)]]) + +(defn- right-section + [{:keys [blur? + notification + notification-count + activity-center-on-press + scan-on-press + qr-code-on-press + right-section-content] + :as props}] + (let [theme (quo.context/use-theme) + button-common-props (get-button-common-props {:theme theme + :blur? blur?})] + [rn/view {:style style/right-section} + (when right-section-content + right-section-content) + [button/button + (assoc button-common-props :accessibility-label :open-scanner-button :on-press scan-on-press) + :i/scan] + [button/button + (merge button-common-props + {:accessibility-label :show-qr-button + :on-press qr-code-on-press}) + :i/qr-code] + [rn/view + [hole-view/hole-view + {:key (hash (str notification notification-count)) + :holes (case notification + (:seen :notification) notification-dot-hole + (:mention :mention-seen) (unread-counter-hole notification-count) + [])} + [button/button + (merge button-common-props + {:accessibility-label :open-activity-center-button + :on-press activity-center-on-press}) + :i/activity-center]] + [notification-highlight props]]])) + +(defn view + ":container-style style map merged with outer view for margins/paddings + :customization-color custom colors + :blur? true/false + :theme :light/:dark + :notification :mention/:seen/:notification (TODO :mention-seen temporarily used while resolving https://github.com/status-im/status-mobile/issues/17102 ) + :avatar-props qu2/user-avatar props + :avatar-on-press callback + :scan-on-press callback + :activity-center-on-press callback + :qr-code-on-press callback + :notification-count number + :max-unread-notifications used to specify max number for counter + :right-section-content + " + [{:keys [avatar-on-press avatar-props customization-color container-style] :as props}] + [rn/view {:style (merge style/top-nav-container container-style)} + [rn/view {:style style/top-nav-inner-container} + [left-section + {:avatar-props avatar-props + :on-press avatar-on-press + :customization-color customization-color}] + [right-section (dissoc props :avatar-props :avatar-on-press)]]]) diff --git a/src/quo/components/notifications/activity_log/style.cljs b/src/quo/components/notifications/activity_log/style.cljs new file mode 100644 index 00000000000..d75c1a064e5 --- /dev/null +++ b/src/quo/components/notifications/activity_log/style.cljs @@ -0,0 +1,80 @@ +(ns quo.components.notifications.activity-log.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:flex-direction :row + :flex-grow 1 + :align-items :flex-start + :padding-top 8 + :padding-bottom 12}) + +(def icon + {:height 32 + :width 32 + :border-radius 100 + :margin-top 10 + :border-width 1 + :border-color colors/white-opa-5 + :align-items :center + :justify-content :center}) + +(def message-title + {:color colors/white-opa-40 + :margin-bottom 2}) + +(def message-body + {:color colors/white}) + +(defn message-container + [attachment text-with-photos?] + {:border-radius 12 + :margin-top 10 + :padding-horizontal 12 + :padding-vertical (if (and + (not text-with-photos?) + (#{:photo :gif} attachment)) + 12 + 8) + :background-color colors/white-opa-5}) + +(def footer-container + {:margin-top 12 + :flex-direction :row}) + +(defn title + [] + {:flex-shrink 1 + :color colors/white}) + +(def timestamp + {:text-transform :none + :margin-left 8 + :margin-top 4 + :flex-shrink 0 + :color colors/neutral-40}) + +(defn unread-dot + [customization-color] + {:background-color (colors/resolve-color (or customization-color :blue) nil) + :border-radius 4 + :width 8 + :height 8}) + +(def unread-dot-container + {:margin-left 8 + :padding-horizontal 12 + :padding-vertical 7}) + +(def context-container + {:flex-direction :row + :align-items :center + :justify-content :flex-start + :flex-wrap :wrap}) + +(def top-section-container + {:flex-direction :row}) + +(def title-container + {:flex 1 + :flex-direction :row}) diff --git a/src/quo/components/notifications/activity_log/view.cljs b/src/quo/components/notifications/activity_log/view.cljs new file mode 100644 index 00000000000..9fb61a8a83b --- /dev/null +++ b/src/quo/components/notifications/activity_log/view.cljs @@ -0,0 +1,199 @@ +(ns quo.components.notifications.activity-log.view + (:require + [clojure.string :as string] + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.inputs.input.view :as input] + [quo.components.markdown.text :as text] + [quo.components.notifications.activity-log.style :as style] + [quo.components.tags.status-tags :as status-tags] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn- activity-reply-text-input + [{:keys [on-update-reply max-reply-length valid-reply?]} reply-input set-reply-input] + [rn/view + [rn/view + {:style {:margin-top 16 + :margin-bottom 8 + :flex-direction :row}} + [text/text + {:weight :medium + :style {:flex-grow 1 + :color colors/neutral-40}} + (i18n/label :t/your-answer)] + [text/text + {:style {:flex-shrink 1 + :color (if (valid-reply? reply-input) + colors/neutral-40 + colors/danger-60)}} + (str (count reply-input) "/" max-reply-length)]] + [input/input + {:on-change-text #(do (set-reply-input %) + (when on-update-reply + (on-update-reply %))) + :auto-capitalize :none + :auto-focus false + :accessibility-label :identity-verification-reply-text-input + :placeholder (i18n/label :t/type-something) + :return-key-type :none + :multiline false + :auto-correct false}]]) + +(defn- activity-icon + [icon] + [rn/view {:style style/icon} + [icon/icon icon {:color colors/white}]]) + +(defn- activity-context + [context replying?] + (let [first-line-offset (if replying? 4 0) + gap-between-lines 5] + (into [rn/view {:style (assoc style/context-container :margin-top first-line-offset)}] + (mapcat + (fn [detail] + ^{:key (hash detail)} + (if (string? detail) + (map (fn [s] + [rn/view + {:style {:margin-right 4 + :margin-top 0}} + [text/text + {:size :paragraph-2 + :style {:color colors/white}} + s]]) + (string/split detail #"\s+")) + [[rn/view + {:margin-right 4 + :margin-top gap-between-lines} + detail]])) + context)))) + +(defn- hiccup-props + [body] + (and (vector? body) + (map? (second body)) + (second body))) + +(defn- activity-message + [{:keys [title body title-number-of-lines body-number-of-lines attachment]}] + (let [{:keys [photos message-text]} (hiccup-props body) + text-with-photos? (and (not (string/blank? message-text)) + (seq photos))] + [rn/view {:style (style/message-container attachment text-with-photos?)} + (when title + [text/text + {:size :paragraph-2 + :accessibility-label :activity-message-title + :style style/message-title + :number-of-lines title-number-of-lines} + title]) + (if (string? body) + [text/text + {:style style/message-body + :accessibility-label :activity-message-body + :size :paragraph-1 + :number-of-lines body-number-of-lines} + body] + body)])) + +(defn- activity-title + [title replying?] + [text/text + {:weight :semi-bold + :accessibility-label :activity-title + :style (style/title) + :size (if replying? :heading-2 :paragraph-1)} + title]) + +(defn- activity-timestamp + [timestamp] + [text/text + {:size :label + :accessibility-label :activity-timestamp + :style style/timestamp} + timestamp]) + +(defn- activity-unread-dot + [customization-color] + [rn/view + {:accessibility-label :activity-unread-indicator + :style style/unread-dot-container} + [rn/view {:style (style/unread-dot customization-color)}]]) + +(defmulti footer-item-view (fn [item _ _] (:type item))) + +(defmethod footer-item-view :button + [{:keys [label subtype disable-when] :as button} replying? reply-input] + (let [size (if replying? 40 24) + common-style (when replying? + {:padding-vertical 9 + :flex-grow 1 + :flex-basis 0})] + [button/button + (-> button + (assoc :size size) + (assoc :type subtype) + (assoc :disabled? (and replying? disable-when (disable-when reply-input))) + (assoc :inner-style + {:justify-content :center + :padding-bottom 0 + :padding-top 0}) + (update :container-style merge common-style {:margin-right 8})) + label])) + +(defmethod footer-item-view :status + [{:keys [label subtype blur? theme]} _ _] + [quo.context/provider {:theme theme} + [status-tags/status-tag + {:size :small + :label label + :status {:type subtype} + :blur? blur?}]]) + +(defn- footer + [{:keys [replying? items] :as props}] + (let [[reply-input set-reply-input] (rn/use-state "")] + [:<> + (when replying? + [activity-reply-text-input props reply-input set-reply-input]) + (when items + [rn/view style/footer-container + (for [item items] + ^{:key (:key item)} + [footer-item-view item replying? reply-input])])])) + +(defn view + [{:keys [icon + message + context + timestamp + title + replying? + unread? + customization-color] + :as props}] + [rn/view + {:accessibility-label :activity + :style style/container + :on-layout (:on-layout props)} + (when-not replying? + [activity-icon icon]) + [rn/view + {:style {:padding-left (when-not replying? 8) + :flex 1}} + [rn/view + [rn/view {:style style/top-section-container} + [rn/view {:style style/title-container} + [activity-title title replying?] + (when-not replying? + [activity-timestamp timestamp])] + (when (and unread? (not replying?)) + [activity-unread-dot customization-color])] + (when context + [activity-context context replying?])] + (when message + [activity-message message]) + [footer props]]]) diff --git a/src/quo/components/notifications/activity_logs_photos/style.cljs b/src/quo/components/notifications/activity_logs_photos/style.cljs new file mode 100644 index 00000000000..e0c99129770 --- /dev/null +++ b/src/quo/components/notifications/activity_logs_photos/style.cljs @@ -0,0 +1,15 @@ +(ns quo.components.notifications.activity-logs-photos.style) + +(def text {:margin-bottom 8}) + +(def photos-container + {:flex 1 + :height 40 + :flex-direction :row}) + +(defn photo + [index] + {:width 40 + :height 40 + :border-radius 10 + :margin-left (if (= index 0) 0 8)}) diff --git a/src/quo/components/notifications/activity_logs_photos/view.cljs b/src/quo/components/notifications/activity_logs_photos/view.cljs new file mode 100644 index 00000000000..d5a08194bbb --- /dev/null +++ b/src/quo/components/notifications/activity_logs_photos/view.cljs @@ -0,0 +1,26 @@ +(ns quo.components.notifications.activity-logs-photos.view + (:require + [clojure.string :as string] + [quo.components.markdown.text :as text] + [quo.components.notifications.activity-logs-photos.style :as style] + [react-native.core :as rn])) + +(defn view + [{:keys [photos message-text]}] + [:<> + (when (not (string/blank? message-text)) + [text/text + {:size :paragraph-1 + :weight :regular + :style style/text + :number-of-lines 2 + :accessibility-label :activity-log-title} + message-text]) + [rn/view {:style style/photos-container} + (map-indexed + (fn [index photo] + ^{:key index} + [rn/image + {:source photo + :style (style/photo index)}]) + photos)]]) diff --git a/src/quo/components/notifications/count_down_circle.cljs b/src/quo/components/notifications/count_down_circle.cljs new file mode 100644 index 00000000000..f74d89d7fa9 --- /dev/null +++ b/src/quo/components/notifications/count_down_circle.cljs @@ -0,0 +1,100 @@ +(ns quo.components.notifications.count-down-circle + (:require + [goog.string :as gstring] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.svg :as svg])) + +(defn- get-path-props + [size stroke-width rotation] + (let [half-size (/ size 2) + half-stroke-width (/ stroke-width 2) + arc-radius (- half-size half-stroke-width) + arc-diameter (* arc-radius 2) + rotation-indicator (if (= rotation :clockwise) "1,0" "0,1") + path-length (* js/Math.PI arc-diameter) + path (gstring/format + "m %s,%s a %s,%s 0 %s 0,%s a %s,%s 0 %s 0 ,-%s" + half-size + half-stroke-width + arc-radius + arc-radius + rotation-indicator + arc-diameter + arc-radius + arc-radius + rotation-indicator + arc-diameter)] + {:path path + :path-length path-length})) + +(defn- linear-ease + [input-time start goal duration] + (if (zero? duration) + start + (-> input-time + (/ duration) + (* goal) + (+ start)))) + +(defn- get-start-at + [duration initial-remaining-time] + (cond (or (zero? duration) (= duration initial-remaining-time)) 0 + (number? initial-remaining-time) (- duration initial-remaining-time) + :else 0)) + +(def ^:private themes + {:color {:dark colors/neutral-80-opa-40 + :light colors/white-opa-40}}) + +(defn circle-timer + [{:keys [color duration size stroke-width trail-color rotation initial-remaining-time]}] + (let [theme (quo.context/use-theme) + rotation (or rotation :clockwise) + duration (or duration 4) + stroke-width (or stroke-width 1) + size (or size 9) + max-stroke-width (max stroke-width 0) + {:keys [path path-length]} (get-path-props size max-stroke-width rotation) + start-at (get-start-at duration initial-remaining-time) + elapsed-time (rn/use-ref-atom 0) + prev-frame-time (rn/use-ref-atom nil) + frame-request (rn/use-ref-atom nil) + [display-time set-display-time] (rn/use-state start-at) + swap-elapsed-time-each-frame (fn swap-elapsed-time-each-frame [frame-time] + (if (nil? @prev-frame-time) + (do (reset! prev-frame-time frame-time) + (reset! frame-request (js/requestAnimationFrame + swap-elapsed-time-each-frame))) + (let [delta (- (/ frame-time 1000) + (/ @prev-frame-time 1000)) + current-elapsed (swap! elapsed-time + delta) + current-display-time (+ start-at current-elapsed) + completed? (>= current-display-time + duration)] + (set-display-time + (if completed? duration current-display-time)) + (when-not completed? + (reset! prev-frame-time frame-time)))))] + (rn/use-effect #(reset! frame-request (js/requestAnimationFrame swap-elapsed-time-each-frame))) + (rn/use-unmount #(js/cancelAnimationFrame @frame-request)) + [rn/view + {:style {:position :relative + :width size + :height size}} + [svg/svg + {:view-box (str "0 0 " size " " size) + :width size + :height size} + [svg/path + {:d path :fill :none :stroke (or trail-color :transparent) :stroke-width stroke-width}] + (when-not (= display-time duration) + [svg/path + {:d path + :fill :none + :stroke (or color (get-in themes [:color theme])) + :stroke-linecap :square + :stroke-width stroke-width + :stroke-dasharray path-length + :stroke-dashoffset (linear-ease display-time 0 path-length duration)}])]])) diff --git a/src/quo/components/notifications/notification/component_spec.cljs b/src/quo/components/notifications/notification/component_spec.cljs new file mode 100644 index 00000000000..841b65e5123 --- /dev/null +++ b/src/quo/components/notifications/notification/component_spec.cljs @@ -0,0 +1,38 @@ +(ns quo.components.notifications.notification.component-spec + (:require + [quo.components.markdown.text :as text] + [quo.components.notifications.notification.view :as notification] + [test-helpers.component :as h])) + +(h/describe "notification" + (h/test "empty notification" + (h/render [notification/notification {}]) + (h/is-null (h/query-by-label-text :notification-avatar)) + (h/is-null (h/query-by-label-text :notification-header)) + (h/is-null (h/query-by-label-text :notification-body))) + (h/test "notification with title and text" + (h/render [notification/notification + {:title "title" + :title-weight :medium + :text "text"}]) + (-> (h/expect (h/get-by-label-text :notification-header)) + (.toHaveTextContent "title")) + (-> (h/expect (h/get-by-label-text :notification-body)) + (.toHaveTextContent "text"))) + (h/test "notification with custom input" + (h/render [notification/notification + {:header [text/text {:accessibility-label :header} "custom header"] + :avatar [text/text {:accessibility-label :avatar} "custom avatar"] + :body [text/text {:accessibility-label :body} "custom body"]}]) + (h/is-truthy (h/get-by-label-text :notification-avatar)) + (h/is-truthy (h/get-by-label-text :notification-header)) + (h/is-truthy (h/get-by-label-text :notification-body)) + (h/is-truthy (h/get-by-label-text :avatar)) + (h/is-truthy (h/get-by-label-text :header)) + (h/is-truthy (h/get-by-label-text :body)) + (-> (h/expect (h/get-by-label-text :notification-header)) + (.toHaveTextContent "custom header")) + (-> (h/expect (h/get-by-label-text :notification-avatar)) + (.toHaveTextContent "custom avatar")) + (-> (h/expect (h/get-by-label-text :notification-body)) + (.toHaveTextContent "custom body")))) diff --git a/src/quo/components/notifications/notification/style.cljs b/src/quo/components/notifications/notification/style.cljs new file mode 100644 index 00000000000..a9d746ba9f7 --- /dev/null +++ b/src/quo/components/notifications/notification/style.cljs @@ -0,0 +1,46 @@ +(ns quo.components.notifications.notification.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows] + [react-native.platform :as platform])) + +(def box-container + {:margin-horizontal 8 + :border-radius 16 + :overflow :hidden}) + +(def blur-container + {:height "100%" + :width "100%" + :position :absolute + :padding-vertical 8 + :padding-left 10 + :padding-right 8 + :background-color (when platform/ios? :transparent)}) + +(defn content-container + [theme] + (merge + (shadows/get 1 theme) + {:background-color (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + :flex-direction :row + :padding-vertical 8 + :padding-horizontal 12})) + +(def right-side-container + {:flex 1 + :justify-content :center}) + +(defn title + [theme] + {:color (colors/theme-colors colors/white colors/neutral-100 theme)}) + +(defn text + [theme] + {:color (colors/theme-colors colors/white colors/neutral-100 theme)}) + +(defn avatar-container + [{:keys [multiline?]}] + {:margin-right 8 + :align-self (when-not multiline? :center) + :margin-top (if multiline? 4 0)}) diff --git a/src/quo/components/notifications/notification/view.cljs b/src/quo/components/notifications/notification/view.cljs new file mode 100644 index 00000000000..6623ae815d6 --- /dev/null +++ b/src/quo/components/notifications/notification/view.cljs @@ -0,0 +1,89 @@ +(ns quo.components.notifications.notification.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.blur.view :as blur] + [quo.components.markdown.text :as text] + [quo.components.notifications.notification.style :as style] + [quo.context] + [react-native.core :as rn])) + +(defn header-container + [& children] + [into + [rn/view {:accessibility-label :notification-header}] children]) + +(defn body-container + [& children] + [into [rn/view {:accessibility-label :notification-body}] children]) + +(defn avatar-container + [{:keys [multiline?]} & children] + [into + [rn/view + {:style (style/avatar-container {:multiline? multiline?}) + :accessibility-label :notification-avatar}] + children]) + +(defn title + ([{:keys [text weight theme multiline?]}] + [text/text + {:size :paragraph-1 + :weight (or weight (if multiline? :semi-bold :medium)) + :style (style/title theme) + :accessibility-label :notification-title} + text])) + +(defn message + [text theme] + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/text theme) + :number-of-lines 2 + :accessibility-label :notification-content} + text]) + +(defn- notification-container + [{:keys [avatar header body container-style theme]}] + [rn/view + {:style (merge style/box-container container-style)} + [blur/view + {:style style/blur-container + :blur-amount 13 + :blur-radius 10 + :blur-type :transparent + :overlay-color :transparent}] + [rn/view + {:style (style/content-container theme)} + avatar + [rn/view + {:style style/right-side-container} + header + body]]]) + +(defn notification + [{title-text :title :keys [avatar user header title-weight text body container-style theme]}] + (let [context-theme (quo.context/use-theme) + theme (or theme context-theme) + body (or body (when text [message text theme])) + header (or header + (when title-text + [title + {:text title-text + :weight title-weight + :theme theme + :multiline? (some? body)}])) + header (when header [header-container header]) + body (when body [body-container body]) + user-avatar (or avatar (when user (user-avatar/user-avatar user))) + avatar (when user-avatar + [avatar-container + {:multiline? (and header body)} + user-avatar])] + [quo.context/provider {:theme theme} + [notification-container + {:avatar avatar + :header header + :body body + :container-style container-style + :theme theme}]])) diff --git a/src/quo/components/notifications/toast/style.cljs b/src/quo/components/notifications/toast/style.cljs new file mode 100644 index 00000000000..1b3716343ee --- /dev/null +++ b/src/quo/components/notifications/toast/style.cljs @@ -0,0 +1,64 @@ +(ns quo.components.notifications.toast.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows] + [react-native.platform :as platform])) + +(defn box-container + [theme] + (merge (shadows/get 1 theme) + {:margin-horizontal 12 + :border-radius 12 + :overflow :hidden})) + +(def blur-container + {:height "100%" + :width "100%" + :position :absolute + :padding-vertical 8 + :padding-left 10 + :padding-right 8 + :background-color (when platform/ios? :transparent)}) + +(defn content-container + [theme] + {:background-color (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + :flex-direction :row + :justify-content :space-between + :padding 8 + :border-radius 12}) + +(defn title + [theme] + {:color (colors/theme-colors colors/white colors/neutral-100 theme)}) + +(defn text + [theme] + {:color (colors/theme-colors colors/white colors/neutral-100 theme)}) + +(defn icon + [toast-type theme] + {:color (case toast-type + :negative (colors/resolve-color :danger theme) + :positive (colors/resolve-color :success theme) + :neutral (colors/theme-colors colors/white-opa-40 + colors/neutral-80-opa-40 + theme)) + :container-style {:width 20 :height 20}}) + +(def left-side-container + {:padding 2 + :padding-top 3}) + +(def right-side-container + {:padding 4 + :flex 1}) + +(defn action-container + [theme] + {:background-color (colors/theme-colors colors/white-opa-5 colors/neutral-80-opa-5 theme) + :flex-direction :row + :padding-vertical 3 + :padding-horizontal 8 + :align-items :center + :border-radius 8}) diff --git a/src/quo/components/notifications/toast/view.cljs b/src/quo/components/notifications/toast/view.cljs new file mode 100644 index 00000000000..416f5b4b8a5 --- /dev/null +++ b/src/quo/components/notifications/toast/view.cljs @@ -0,0 +1,98 @@ +(ns quo.components.notifications.toast.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.blur.view :as blur] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.notifications.count-down-circle :as count-down-circle] + [quo.components.notifications.toast.style :as style] + [quo.context] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn toast-action-container + [{:keys [on-press style theme]} & children] + [rn/touchable-highlight + {:on-press on-press + :underlay-color :transparent} + (into + [rn/view + {:style (merge (style/action-container theme) style)}] + children)]) + +(defn toast-undo-action + [{:keys [undo-duration undo-on-press]}] + (let [theme (quo.context/use-theme)] + [toast-action-container + {:on-press undo-on-press + :accessibility-label :toast-undo-action + :theme theme} + [rn/view {:style {:margin-right 5}} + [count-down-circle/circle-timer {:duration undo-duration}]] + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/text theme)} + [i18n/label :t/undo]]])) + +(defn toast-container + [{:keys [left title text right container-style]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (merge (style/box-container theme) container-style)} + [blur/view + {:style style/blur-container + :blur-amount 13 + :blur-radius 10 + :blur-type :transparent + :overlay-color :transparent}] + [rn/view {:style (style/content-container theme)} + [rn/view {:style style/left-side-container} + left] + [rn/view {:style style/right-side-container} + (when title + [text/text + {:size :paragraph-1 + :weight :semi-bold + :style (style/title theme) + :accessibility-label :toast-title} + title]) + (when text + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/text theme) + :accessibility-label :toast-content} + text])] + right]])) + +(defn toast + "Options: + + :type => :neutral/:negative/:positive + " + [{:keys [type icon title text action undo-duration undo-on-press container-style theme user] + :or {type :neutral icon :i/placeholder}}] + (let [theme (or theme (quo.context/use-theme)) + icon-name (case type + :positive (if (= theme :light) + :i/correct + :i/correct-dark) + :negative (if (= theme :light) + :i/incorrect + :i/incorrect-dark) + :neutral icon)] + [quo.context/provider {:theme theme} + [toast-container + {:left (cond user + [user-avatar/user-avatar user] + icon-name + [icon/icon icon-name (style/icon type theme)]) + :title title + :text text + :right (if undo-duration + [toast-undo-action + {:undo-duration undo-duration + :undo-on-press undo-on-press}] + action) + :container-style container-style}]])) + diff --git a/src/quo/components/numbered_keyboard/keyboard_key/component_spec.cljs b/src/quo/components/numbered_keyboard/keyboard_key/component_spec.cljs new file mode 100644 index 00000000000..1305481d2c6 --- /dev/null +++ b/src/quo/components/numbered_keyboard/keyboard_key/component_spec.cljs @@ -0,0 +1,51 @@ +(ns quo.components.numbered-keyboard.keyboard-key.component-spec + (:require + [quo.components.numbered-keyboard.keyboard-key.view :as component] + [test-helpers.component :as h])) + +(h/describe "Keyboard Key" + (h/test "render digit type" + (h/render [component/view + {:disabled? false + :on-press #(js/alert "pressed") + :blur? false + :type :digit} 1]) + (h/is-truthy (h/query-by-label-text :text-label))) + + (h/test "render key type" + (h/render [component/view + {:disabled? false + :on-press #(js/alert "pressed") + :blur? false + :type :key} :i/delete]) + (h/is-truthy (h/query-by-label-text :icon-label))) + + (h/test "render derivation path type" + (h/render [component/view + {:disabled? false + :on-press #(js/alert "pressed") + :blur? false + :type :derivation-path}]) + (h/is-truthy (h/query-by-label-text :derivation-path-label))) + + (h/test "Is pressable when disabled is false" + (let [on-press (h/mock-fn)] + (h/render [component/view + {:disabled? false + :on-press #(on-press) + :blur? false + :type :digit} 1]) + (h/is-truthy (h/query-by-label-text :text-label)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-1)) + (h/was-called on-press))) + + (h/test "Is not pressable when disabled is true" + (let [on-press (h/mock-fn)] + (h/render [component/view + {:disabled? true + :on-press #(on-press) + :blur? false + :type :digit} 1]) + (h/is-truthy (h/query-by-label-text :text-label)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-1)) + (h/was-not-called on-press)))) diff --git a/src/quo/components/numbered_keyboard/keyboard_key/style.cljs b/src/quo/components/numbered_keyboard/keyboard_key/style.cljs new file mode 100644 index 00000000000..ebdc1c12577 --- /dev/null +++ b/src/quo/components/numbered_keyboard/keyboard_key/style.cljs @@ -0,0 +1,29 @@ +(ns quo.components.numbered-keyboard.keyboard-key.style + (:require + [quo.foundations.colors :as colors])) + +(defn get-label-color + [disabled? theme blur?] + (cond + (and disabled? (or (= :dark theme) blur?)) colors/white-opa-30 + (and disabled? (or (= :light theme) blur?)) colors/neutral-30 + (or (= :dark theme) blur?) colors/white + :else colors/neutral-100)) + +(defn toggle-background-color + [pressed-in? blur? theme] + (if pressed-in? + (cond + blur? colors/white-opa-10 + (= :light theme) colors/neutral-10 + (= :dark theme) colors/neutral-80) + :transparent)) + +(defn container + [color] + {:width 48 + :height 48 + :justify-content :center + :align-items :center + :border-radius 999 + :background-color color}) diff --git a/src/quo/components/numbered_keyboard/keyboard_key/view.cljs b/src/quo/components/numbered_keyboard/keyboard_key/view.cljs new file mode 100644 index 00000000000..f23e4430070 --- /dev/null +++ b/src/quo/components/numbered_keyboard/keyboard_key/view.cljs @@ -0,0 +1,51 @@ +(ns quo.components.numbered-keyboard.keyboard-key.view + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.numbered-keyboard.keyboard-key.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn- label->accessibility-label + [label] + (let [label-name (if (keyword? label) (name label) label)] + (keyword (str "keyboard-key-" label-name)))) + +(defn view + [{:keys [disabled? blur? on-press on-long-press type]} label] + (let [theme (quo.context/use-theme) + [pressed? set-pressed?] (rn/use-state false) + on-press (rn/use-callback #(when on-press (on-press label)) [on-press label]) + on-long-press (rn/use-callback #(when (fn? on-long-press) (on-long-press label)) + [on-long-press label]) + on-press-in (rn/use-callback #(set-pressed? true)) + on-press-out (rn/use-callback #(set-pressed? false)) + label-color (style/get-label-color disabled? theme blur?) + background-color (style/toggle-background-color pressed? blur? theme)] + [rn/pressable + {:accessibility-label (label->accessibility-label label) + :disabled (or disabled? (not label)) + :on-press on-press + :on-long-press on-long-press + :allow-multiple-presses? true + :on-press-in on-press-in + :on-press-out on-press-out + :hit-slop {:top 8 :bottom 8 :left 25 :right 25} + :style (style/container background-color)} + (case type + :key [icons/icon + label + {:color label-color + :accessibility-label :icon-label}] + :digit [text/text + {:accessibility-label :text-label + :weight :regular + :size :heading-1 + :style {:color label-color}} + label] + :derivation-path [icons/icon + :i/derivation-path + {:color label-color + :size 32 + :accessibility-label :derivation-path-label}] + nil)])) diff --git a/src/quo/components/numbered_keyboard/numbered_keyboard/style.cljs b/src/quo/components/numbered_keyboard/numbered_keyboard/style.cljs new file mode 100644 index 00000000000..39e2cded743 --- /dev/null +++ b/src/quo/components/numbered_keyboard/numbered_keyboard/style.cljs @@ -0,0 +1,12 @@ +(ns quo.components.numbered-keyboard.numbered-keyboard.style) + +(def container + {:display :flex + :padding-top 8 + :padding-horizontal 48}) + +(def row-container + {:flex-direction :row + :justify-content :space-between + :margin-bottom 12 + :align-items :center}) diff --git a/src/quo/components/numbered_keyboard/numbered_keyboard/view.cljs b/src/quo/components/numbered_keyboard/numbered_keyboard/view.cljs new file mode 100644 index 00000000000..de4dc9565cd --- /dev/null +++ b/src/quo/components/numbered_keyboard/numbered_keyboard/view.cljs @@ -0,0 +1,74 @@ +(ns quo.components.numbered-keyboard.numbered-keyboard.view + (:require + [quo.components.numbered-keyboard.keyboard-key.view :as keyboard-key] + [quo.components.numbered-keyboard.numbered-keyboard.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn keyboard-item + [{:keys [item type disabled? on-press on-long-press blur? theme]}] + [keyboard-key/view + {:disabled? disabled? + :on-press on-press + :on-long-press on-long-press + :blur? blur? + :theme theme + :type type} + item]) + +(defn view + [] + (fn [{:keys [disabled? blur? left-action delete-key? on-press on-delete on-long-press-delete + container-style] + :or {left-action :none}}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge style/container + container-style)} + (for [row-index (range 1 4)] + ^{:key row-index} + [rn/view {:style style/row-container} + (for [column-index (range 1 4)] + ^{:key (str row-index column-index)} + [keyboard-item + {:item (+ (* (dec row-index) 3) column-index) + :type :digit + :disabled? disabled? + :on-press on-press + :blur? blur? + :theme theme}])]) + ;; bottom row + [rn/view {:style style/row-container} + (case left-action + :dot [keyboard-item + {:item "." + :type :digit + :disabled? disabled? + :on-press on-press + :blur? blur? + :theme theme}] + :face-id [keyboard-item + {:item :i/faceid-key + :type :key + :disabled? disabled? + :on-press on-press + :blur? blur? + :theme theme}] + :none [keyboard-item]) + [keyboard-item + {:item "0" + :type :digit + :disabled? disabled? + :on-press on-press + :blur? blur? + :theme theme}] + (if delete-key? + [keyboard-item + {:item :i/backspace + :type :key + :disabled? disabled? + :on-press on-delete + :on-long-press on-long-press-delete + :blur? blur? + :theme theme}] + [keyboard-item])]]))) diff --git a/src/quo/components/onboarding/small_option_card/component_spec.cljs b/src/quo/components/onboarding/small_option_card/component_spec.cljs new file mode 100644 index 00000000000..3f27a38b34b --- /dev/null +++ b/src/quo/components/onboarding/small_option_card/component_spec.cljs @@ -0,0 +1,53 @@ +(ns quo.components.onboarding.small-option-card.component-spec + (:require + [quo.components.onboarding.small-option-card.view :as small-option-card] + [test-helpers.component :as h])) + +(defn- testing-small-option-card + [variant + {:keys [title subtitle image on-press] + :or {title "title" subtitle "subtitle"}}] + [small-option-card/small-option-card + {:variant variant + :title title + :subtitle subtitle + :image image + :on-press on-press}]) + +(h/describe "small-option-card" + (h/describe "Title & subtitle are rendered" + (let [title "A title" + subtitle "A subtitle" + component-props {:title title :subtitle subtitle}] + (h/test "`:main` variant" + (h/render (testing-small-option-card :main component-props)) + (-> (h/get-by-text title) h/expect .toBeTruthy) + (-> (h/get-by-text subtitle) h/expect .toBeTruthy)) + + (h/test "`:icon` variant" + (h/render (testing-small-option-card :icon component-props)) + (-> (h/get-by-text title) h/expect .toBeTruthy) + (-> (h/get-by-text subtitle) h/expect .toBeTruthy)))) + + (h/describe "Card pressed" + (let [on-press-fn (h/mock-fn) + component-props {:on-press on-press-fn}] + (h/test "`:main` variant" + (h/render (testing-small-option-card :main component-props)) + (h/fire-event :press (h/get-by-label-text :small-option-card)) + (-> on-press-fn js/expect .toHaveBeenCalled)) + + (h/test "`:icon` variant" + (h/render (testing-small-option-card :icon component-props)) + (h/fire-event :press (h/get-by-label-text :small-option-card)) + (-> on-press-fn js/expect .toHaveBeenCalled)))) + + (h/describe "Image rendered" + (let [component-props {:image nil}] + (h/test "`:main` variant" + (h/render (testing-small-option-card :main component-props)) + (-> (h/get-by-label-text :small-option-card-main-image) h/expect .-not .toBeNull)) + + (h/test "`:icon` variant" + (h/render (testing-small-option-card :icon component-props)) + (-> (h/get-by-label-text :small-option-card-icon-image) h/expect .-not .toBeNull))))) diff --git a/src/quo/components/onboarding/small_option_card/style.cljs b/src/quo/components/onboarding/small_option_card/style.cljs new file mode 100644 index 00000000000..6c7a228bbf5 --- /dev/null +++ b/src/quo/components/onboarding/small_option_card/style.cljs @@ -0,0 +1,60 @@ +(ns quo.components.onboarding.small-option-card.style + (:require + [quo.foundations.colors :as colors])) + +(def main-variant-height 335) +(def icon-variant-height 56) + +(def text-container {:flex 1}) + +(def icon-title + {:color colors/white + :height 22}) + +(def main-title + {:color colors/white + :height 26}) + +(def subtitle + {:color colors/white-opa-70 + :height 18}) + +(def icon-variant + {:flex-direction :row + :padding-vertical 8 + :padding-horizontal 12}) + +(def icon-variant-image-container + {:width 32 + :height 40 + :justify-content :center + :align-items :center + :padding-vertical 4 + :margin-right 8}) + +(def icon-variant-image + {:flex 1 + :width 32 + :height 32}) + +(def main-variant + {:flex 1}) + +(def main-variant-text-container + {:height 62 + :padding-top 10 + :padding-horizontal 12}) + +(defn main-variant-image + [max-height] + {:flex 1 + :max-height max-height}) + +(def main-button + {:padding-horizontal 12 :margin-bottom 12 :margin-top 8}) + +(defn card + [height] + {:background-color colors/white-opa-5 + :border-radius 16 + :height height}) diff --git a/src/quo/components/onboarding/small_option_card/view.cljs b/src/quo/components/onboarding/small_option_card/view.cljs new file mode 100644 index 00000000000..9d54d5ce411 --- /dev/null +++ b/src/quo/components/onboarding/small_option_card/view.cljs @@ -0,0 +1,89 @@ +(ns quo.components.onboarding.small-option-card.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.markdown.text :as text] + [quo.components.onboarding.small-option-card.style :as style] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn- icon-variant + [{:keys [title subtitle image accessibility-label on-press]}] + [rn/pressable + {:accessibility-label accessibility-label + :style style/icon-variant + :underlay-color colors/white-opa-5 + :on-press on-press} + [rn/view {:style style/icon-variant-image-container} + [fast-image/fast-image + {:accessibility-label :small-option-card-icon-image + :style style/icon-variant-image + :resize-mode :contain + :source image}]] + [rn/view {:style style/text-container} + [text/text + {:style style/icon-title + :size :paragraph-1 + :weight :semi-bold + :number-of-lines 1} + title] + [text/text + {:style style/subtitle + :size :paragraph-2 + :weight :regular + :number-of-lines 1} + subtitle]]]) + +(defn- main-variant + [{:keys [title subtitle button-label image max-height accessibility-label on-press button-props]}] + [rn/view {:style style/main-variant} + [rn/view {:style style/main-variant-text-container} + [text/text + {:style style/main-title + :size :heading-2 + :weight :semi-bold + :number-of-lines 1} + title] + [text/text + {:style style/subtitle + :size :paragraph-2 + :weight :regular + :number-of-lines 1} + subtitle]] + [fast-image/fast-image + {:accessibility-label :small-option-card-main-image + :style (style/main-variant-image max-height) + :resize-mode :contain + :source image}] + [button/button + (merge + {:on-press on-press + :accessibility-label accessibility-label + :type :grey + :size 40 + :container-style style/main-button + :theme :dark + :background :blur} + button-props) + button-label]]) + +(defn small-option-card + [{:keys [variant title subtitle button-label image max-height on-press accessibility-label + button-props container-style] + :or {variant :main accessibility-label :small-option-card}}] + (let [main-variant? (= variant :main) + card-component (if main-variant? main-variant icon-variant) + card-height (cond + (not main-variant?) style/icon-variant-height + max-height (min max-height style/main-variant-height) + :else style/main-variant-height)] + [rn/view {:style (merge (style/card card-height) container-style)} + [card-component + {:title title + :subtitle subtitle + :button-label button-label + :on-press on-press + :accessibility-label accessibility-label + :image image + :max-height max-height + :button-props button-props}]])) diff --git a/src/quo/components/overlay/style.cljs b/src/quo/components/overlay/style.cljs new file mode 100644 index 00000000000..ed1e51214fe --- /dev/null +++ b/src/quo/components/overlay/style.cljs @@ -0,0 +1,22 @@ +(ns quo.components.overlay.style + (:require [quo.foundations.colors :as colors])) + +(defn overlay-background + [type] + (let [background-color (case type + :shell colors/overlay-background-blur + :drawer colors/neutral-100-opa-70-blur + nil)] + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :background-color background-color})) + +(def container + {:flex 1}) + +(def blur-container + {:flex 1 + :background-color :transparent}) diff --git a/src/quo/components/overlay/view.cljs b/src/quo/components/overlay/view.cljs new file mode 100644 index 00000000000..adb6f4f8800 --- /dev/null +++ b/src/quo/components/overlay/view.cljs @@ -0,0 +1,25 @@ +(ns quo.components.overlay.view + (:require + [quo.components.blur.view :as blur] + [quo.components.overlay.style :as style] + [react-native.core :as rn] + [react-native.safe-area :as safe-area])) + +(defn view + [{:keys [type container-style top-inset? bottom-inset? insets?]} & children] + (let [top-style (when (or insets? top-inset?) {:padding-top safe-area/top}) + bottom-style (when (or insets? bottom-inset?) {:padding-bottom safe-area/bottom})] + [rn/view {:style (style/overlay-background type)} + (if (= type :shell) + [blur/view + {:blur-amount 20 + :blur-radius 25 + :blur-type :transparent + :overlay-color :transparent + :style style/container} + (into [rn/view + {:style [style/blur-container top-style bottom-style container-style]}] + children)] + (into [rn/view + {:style [style/blur-container top-style bottom-style container-style]}] + children))])) diff --git a/src/quo/components/password/password_tips/style.cljs b/src/quo/components/password/password_tips/style.cljs new file mode 100644 index 00000000000..15e37212ccc --- /dev/null +++ b/src/quo/components/password/password_tips/style.cljs @@ -0,0 +1,6 @@ +(ns quo.components.password.password-tips.style) + +(def password-tips + {:flex-direction :row + :margin-horizontal 20 + :justify-content :space-between}) diff --git a/src/quo/components/password/password_tips/view.cljs b/src/quo/components/password/password_tips/view.cljs new file mode 100644 index 00000000000..cd031731c06 --- /dev/null +++ b/src/quo/components/password/password_tips/view.cljs @@ -0,0 +1,30 @@ +(ns quo.components.password.password-tips.view + (:require [quo.components.password.password-tips.style :as style] + [quo.components.password.tips.view :as tips] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def ?schema + [:=> + [:cat + [:map {:closed true} + [:lower-case? :boolean] + [:upper-case? :boolean] + [:numbers? :boolean] + [:symbols? :boolean]]] + :any]) + +(defn- view-internal + [{:keys [lower-case? upper-case? numbers? symbols?]}] + [rn/view {:style style/password-tips} + [tips/view {:completed? lower-case?} + (i18n/label :t/password-creation-tips-1)] + [tips/view {:completed? upper-case?} + (i18n/label :t/password-creation-tips-2)] + [tips/view {:completed? numbers?} + (i18n/label :t/password-creation-tips-3)] + [tips/view {:completed? symbols?} + (i18n/label :t/password-creation-tips-4)]]) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/password/tips/component_spec.cljs b/src/quo/components/password/tips/component_spec.cljs new file mode 100644 index 00000000000..bfe52c2d00f --- /dev/null +++ b/src/quo/components/password/tips/component_spec.cljs @@ -0,0 +1,21 @@ +(ns quo.components.password.tips.component-spec + (:require + [quo.components.password.tips.view :as quo] + [test-helpers.component :as h])) + +(h/describe "password tips component" + (h/test "render component" + (h/render [quo/view]) + (-> (h/expect (h/get-by-label-text :password-tips)) + (.toBeTruthy))) + (h/test "render component with proper text" + (h/render [quo/view {:completed? false} "Upper case"]) + (-> (h/expect (h/get-by-text "Upper case")) + (.toBeTruthy))) + (h/test "render component with completed proper text & completed state" + (h/render [quo/view {:completed? true} "Numbers"]) + (-> (h/expect (h/get-by-text "Numbers")) + (.toBeTruthy)) + (-> (h/expect (h/get-by-label-text :password-tips-completed)) + (.toBeTruthy)))) + diff --git a/src/quo/components/password/tips/style.cljs b/src/quo/components/password/tips/style.cljs new file mode 100644 index 00000000000..d66953499c3 --- /dev/null +++ b/src/quo/components/password/tips/style.cljs @@ -0,0 +1,19 @@ +(ns quo.components.password.tips.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:align-self :flex-start}) + +(defn tip-text + [completed?] + {:color (if completed? colors/white-opa-40 colors/white-opa-70)}) + +(def strike-through + {:position :absolute + :background-color colors/white + :top 10 + :left 0 + :right 0 + :height 0.8}) + diff --git a/src/quo/components/password/tips/view.cljs b/src/quo/components/password/tips/view.cljs new file mode 100644 index 00000000000..296f71c8cb0 --- /dev/null +++ b/src/quo/components/password/tips/view.cljs @@ -0,0 +1,26 @@ +(ns quo.components.password.tips.view + (:require + [quo.components.markdown.text :as text] + [quo.components.password.tips.style :as style] + [react-native.core :as rn])) + +(defn view + "Options + - `completed?` Password tip state + + `text` Password tip string + " + [{:keys [completed?]} text] + [rn/view + {:style style/container + :accessibility-label :password-tips} + [text/text + {:style (style/tip-text completed?) + :weight :regular + :size :paragraph-2} + text] + (when completed? + [rn/view + {:style style/strike-through + :accessibility-label :password-tips-completed}])]) + diff --git a/src/quo/components/pin_input/pin/view.cljs b/src/quo/components/pin_input/pin/view.cljs new file mode 100644 index 00000000000..396bc28e3c4 --- /dev/null +++ b/src/quo/components/pin_input/pin/view.cljs @@ -0,0 +1,43 @@ +(ns quo.components.pin-input.pin.view + (:require quo.context + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn view + [{:keys [theme state blur?]}] + (let [app-theme (quo.context/use-theme) + theme (or theme app-theme)] + [rn/view {:style {:width 36 :height 36 :align-items :center :justify-content :center}} + (case state + :active + [rn/view + {:style {:width 16 + :height 16 + :border-radius 8 + :background-color (if blur? + colors/white-opa-20 + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))}}] + :filled + [rn/view + {:style {:width 16 + :height 16 + :border-radius 8 + :background-color (if blur? + colors/white + (colors/theme-colors colors/neutral-100 colors/white theme))}}] + :error + [rn/view + {:style {:width 16 + :height 16 + :border-radius 8 + :background-color (if blur? + colors/danger-60 + (colors/theme-colors colors/danger-50 colors/danger-60 theme))}}] + [rn/view + {:style + {:width 12 + :height 12 + :border-radius 6 + :background-color (if blur? + colors/white-opa-20 + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))}}])])) diff --git a/src/quo/components/pin_input/view.cljs b/src/quo/components/pin_input/view.cljs new file mode 100644 index 00000000000..8ab7fe3ca98 --- /dev/null +++ b/src/quo/components/pin_input/view.cljs @@ -0,0 +1,27 @@ +(ns quo.components.pin-input.view + (:require [quo.components.markdown.text :as text] + [quo.components.pin-input.pin.view :as pin] + quo.context + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn view + [{:keys [number-of-pins number-of-filled-pins error? info info-error?] + :or {number-of-pins 6 number-of-filled-pins 0}}] + (let [theme (quo.context/use-theme)] + [rn/view {:style {:align-items :center}} + [rn/view {:style {:flex-direction :row}} + (for [i (range 1 (inc number-of-pins))] + ^{:key i} + [pin/view + {:state (cond + error? :error + (<= i number-of-filled-pins) :filled + (= i (inc number-of-filled-pins)) :active)}])] + (when info + [text/text + {:style {:color (if (or error? info-error?) + (colors/theme-colors colors/danger-50 colors/danger-60 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))} + :size :paragraph-2} + info])])) diff --git a/src/quo/components/profile/collectible/style.cljs b/src/quo/components/profile/collectible/style.cljs new file mode 100644 index 00000000000..bfc7031eb8d --- /dev/null +++ b/src/quo/components/profile/collectible/style.cljs @@ -0,0 +1,69 @@ +(ns quo.components.profile.collectible.style + (:require + [quo.foundations.colors :as colors])) + +(def tile-style-by-size + {:xl {:width 160 + :height 160 + :border-radius 12} + :lg {:width 104 + :height 104 + :border-radius 10} + :md {:width 76 + :height 76 + :border-radius 10} + :sm {:width 48 + :height 48 + :border-radius 8} + :xs {:width 36 + :height 36 + :border-radius 8}}) + +(def tile-outer-container + {:width 176 + :height 176 + :padding 8}) + +(def tile-inner-container + {:position :relative + :flex 1}) + +(def tile-sub-container + {:position :absolute + :width 76 + :height 76 + :bottom 0 + :right 0}) + +(def top-left + {:position :absolute + :top 0 + :left 0}) + +(def top-right + {:position :absolute + :top 0 + :right 0}) + +(def bottom-left + {:position :absolute + :bottom 0 + :left 0}) + +(def bottom-right + {:position :absolute + :bottom 0 + :right 0}) + +(defn remaining-tiles + [theme] + (let [bg-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + tile-size (tile-style-by-size :xs)] + (assoc tile-size + :justify-content :center + :align-items :center + :background-color bg-color))) + +(defn remaining-tiles-text + [theme] + {:color (colors/theme-colors colors/neutral-60 colors/neutral-40 theme)}) diff --git a/src/quo/components/profile/collectible/view.cljs b/src/quo/components/profile/collectible/view.cljs new file mode 100644 index 00000000000..6c7092b9baa --- /dev/null +++ b/src/quo/components/profile/collectible/view.cljs @@ -0,0 +1,133 @@ +(ns quo.components.profile.collectible.view + (:require + [clojure.string :as string] + [quo.components.markdown.text :as text] + [quo.components.profile.collectible.style :as style] + [react-native.core :as rn])) + +(defn remaining-tiles + [amount theme] + [rn/view {:style (merge style/bottom-right (style/remaining-tiles theme))} + [text/text + {:style (style/remaining-tiles-text theme) + :size :paragraph-2 + :weight :medium} + (str "+" amount)]]) + +(defn tile + [{:keys [style size resource]}] + (let [svg? (and (map? resource) (:svg? resource)) + image-style (style/tile-style-by-size size)] + [rn/view {:style style} + (cond + svg? + [rn/view + {:style (assoc image-style + :border-radius (:border-radius image-style) + :overflow :hidden + :justify-content :center + :align-items :center + :background-color :lightblue)} + [text/text "SVG Content"]] + + (or (string/blank? resource) (string/blank? (:uri resource))) + [rn/view + {:style (assoc image-style + :border-radius (:border-radius image-style) + :overflow :hidden + :justify-content :center + :align-items :center + :background-color :lightgray)} + [text/text "Missing image"]] + + :else + ;; NOTE: using react-native-fast-image here causes a crash on devices when used inside a + ;; large flatlist. The library seems to have issues with memory consumption when used with + ;; large images/GIFs. + ;; + ;; https://github.com/DylanVann/react-native-fast-image/issues/195 + [rn/image + {:style image-style + :source (if (string? resource) + {:uri resource + :priority :low} + resource)}])])) + +(defn two-tiles + [{:keys [images size]}] + [:<> + [tile + {:style style/top-left + :size size + :resource (first images)}] + [tile + {:style style/bottom-right + :size size + :resource (second images)}]]) + +(defn three-tiles + [{:keys [tiles size]}] + (let [[image-1 image-2 image-3 & _] tiles] + [:<> + [tile + {:style style/top-left + :size size + :resource image-1}] + [tile + {:style style/top-right + :size size + :resource image-2}] + [tile + {:style style/bottom-left + :size size + :resource image-3}]])) + +(defn tile-container + [{:keys [images]}] + (let [num-images (count images)] + (case num-images + 1 [tile + {:resource (first images) + :size :xl}] + 2 [two-tiles + {:images images + :size :lg}] + 3 [three-tiles + {:tiles images + :size :md}] + (let [[first-three-images remaining-images] (split-at 3 images)] + [:<> + [three-tiles {:tiles first-three-images :size :md}] + [rn/view {:style style/tile-sub-container} + (case num-images + 4 [tile + {:resource (nth images 3 nil) + :size :md}] + 5 [two-tiles + {:images (take 2 remaining-images) + :size :sm}] + 6 [three-tiles + {:tiles (take 3 remaining-images) + :size :xs}] + 7 [:<> + [three-tiles + {:tiles (take 3 remaining-images) + :size :xs}] + [tile + {:style style/bottom-right + :size :xs + :resource (nth remaining-images 3 nil)}]] + [:<> + [three-tiles + {:tiles (take 3 remaining-images) + :size :xs}] + [remaining-tiles (- (count remaining-images) 3)]])]])))) + +(defn collectible + [{:keys [images on-press on-long-press]}] + [rn/view {:style style/tile-outer-container} + [rn/pressable + {:on-press on-press + :on-long-press on-long-press + :style style/tile-inner-container} + [tile-container {:images images}]]]) diff --git a/src/quo/components/profile/collectible_list_item/style.cljs b/src/quo/components/profile/collectible_list_item/style.cljs new file mode 100644 index 00000000000..7d8f1c9dd25 --- /dev/null +++ b/src/quo/components/profile/collectible_list_item/style.cljs @@ -0,0 +1,130 @@ +(ns quo.components.profile.collectible-list-item.style + (:require [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows])) + +(def container-border-radius 12) +(def card-image-padding-vertical 3) +(def card-image-padding-horizontal 3) +(def default-opacity-for-loader 1) +(def default-opacity-for-image 0) + +(defn fallback + [{:keys [theme opacity]}] + [{:opacity opacity} + {:opacity 1 + :background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme) + :border-style :dashed + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :border-width 1 + :border-radius container-border-radius + :width "100%" + :aspect-ratio 1 + :align-items :center + :justify-content :center}]) + +(def collectible-counter + {:position :absolute + :top 12 + :right 12}) + +(def avatar + {:position :absolute + :bottom 12 + :left 12}) + +(defn card-view-container + [theme] + (merge + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :padding-top card-image-padding-vertical + :padding-left card-image-padding-horizontal + :padding-right card-image-padding-horizontal + :padding-bottom card-image-padding-vertical + :border-radius 14 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)} + (shadows/get 2 theme))) + +(def image + {:width "100%" + :flex 1 + :border-radius container-border-radius}) + +(def card-details-container + {:flex-direction :row + :height 27 + :margin-top 3 + :margin-bottom 2 + :align-items :center + :padding-left 8 + :padding-right 0 + :padding-top 4}) + +(defn card-detail-text + [empty-name? theme] + {:flex 1 + :margin-right 8 + :color (if empty-name? + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + (colors/theme-colors colors/neutral-100 colors/white theme))}) + +(defn card-loader + [opacity] + [{:opacity opacity} + {:position :absolute + :opacity default-opacity-for-loader + :left 8 + :align-items :center + :flex-direction :row}]) + +(def image-view-container + {:aspect-ratio 1 + :border-radius container-border-radius}) + +(defn loading-square + [theme] + {:height 20 + :width 20 + :border-radius 6 + :align-self :center + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme)}) + +(defn loading-message + [theme] + {:height 14 + :width 72 + :margin-left 8 + :border-radius 6 + :align-self :center + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme)}) + +(defn loading-image + [theme] + {:position :absolute + :opacity default-opacity-for-loader + :top 0 + :bottom 0 + :left 0 + :right 0 + :border-radius container-border-radius + :background-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur theme) + :z-index 2}) + +(defn loading-image-with-opacity + [theme opacity] + [{:opacity opacity} + (loading-image theme)]) + +(defn avatar-container + [opacity] + [{:opacity opacity} + {:flex 1 + :flex-direction :row + :column-gap 8 + :opacity default-opacity-for-image}]) + +(defn supported-file + [opacity] + [{:opacity opacity} + {:aspect-ratio 1 + :opacity default-opacity-for-image}]) diff --git a/src/quo/components/profile/collectible_list_item/view.cljs b/src/quo/components/profile/collectible_list_item/view.cljs new file mode 100644 index 00000000000..6cf5c79ea71 --- /dev/null +++ b/src/quo/components/profile/collectible_list_item/view.cljs @@ -0,0 +1,292 @@ +(ns quo.components.profile.collectible-list-item.view + (:require + [clojure.string :as string] + [quo.components.avatars.collection-avatar.view :as collection-avatar] + [quo.components.counter.collectible-counter.view :as collectible-counter] + [quo.components.icon :as icon] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.components.profile.collectible-list-item.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.gradients :as gradients] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [schema.core :as schema] + [utils.datetime :as datetime] + [utils.i18n :as i18n])) + +(def timing-options-out 650) +(def timing-options-in 1000) +(def first-load-time 500) +(def cached-load-time 200) +(def error-wait-time 800) + +(defn animate-loading-image + [{:keys [load-time set-state loader-opacity image-opacity]}] + (reanimated/animate loader-opacity 0 timing-options-out) + (reanimated/animate image-opacity 1 timing-options-in) + (if (> load-time cached-load-time) + (js/setTimeout + (fn [] + (set-state (fn [prev-state] + (assoc prev-state :image-loaded? true)))) + first-load-time) + (set-state (fn [prev-state] + (assoc prev-state :image-loaded? true))))) + +(defn on-load + [evt set-state] + (let [source (.. evt -nativeEvent -source) + aspect-ratio (/ (.-width source) (.-height source))] + (set-state (fn [prev-state] (assoc prev-state :image-aspect-ratio aspect-ratio))))) + +(defn on-load-error + [set-state] + (js/setTimeout (fn [] + (set-state (fn [prev-state] (assoc prev-state :image-error? true)))) + error-wait-time)) + +(defn on-load-avatar + [{:keys [load-time set-state loader-opacity avatar-opacity]}] + (reanimated/animate loader-opacity 0 timing-options-out) + (reanimated/animate avatar-opacity 1 timing-options-in) + (if (> load-time cached-load-time) + (js/setTimeout + (fn [] + (set-state (fn [prev-state] + (assoc prev-state :avatar-loaded? true)))) + first-load-time) + (set-state (fn [prev-state] + (assoc prev-state :avatar-loaded? true))))) + +(defn- fallback-view + [{:keys [label theme image-opacity on-load-end]}] + (rn/use-mount on-load-end) + [reanimated/view {:style (style/fallback {:opacity image-opacity :theme theme})} + [rn/view + [icon/icon :i/sad + {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}]] + [rn/view {:style {:height 4}}] + [text/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}} + label]]) + +(defn- loading-square + [theme] + [rn/view {:style (style/loading-square theme)}]) + +(defn- loading-message + [theme] + [rn/view {:style (style/loading-message theme)}]) + +(defn- loading-image + [{:keys [theme gradient-color-index loader-opacity]}] + [reanimated/view + {:style (style/loading-image-with-opacity theme loader-opacity)} + [gradients/view + {:theme theme + :container-style (style/loading-image theme) + :color-index gradient-color-index}]]) + +(defn- card-details + [{:keys [community? avatar-image-src collectible-name theme state set-state loading?]}] + (let [loader-opacity (reanimated/use-shared-value 1) + avatar-opacity (reanimated/use-shared-value 0) + [load-time set-load-time] (rn/use-state (datetime/now)) + set-avatar-loaded (fn [] + (on-load-avatar {:set-state set-state + :load-time load-time + :loader-opacity loader-opacity + :avatar-opacity avatar-opacity})) + empty-name? (string/blank? collectible-name)] + (rn/use-mount (fn [] + (when (and (string/blank? avatar-image-src) (not loading?)) + (set-avatar-loaded)))) + [rn/view {:style style/card-details-container} + [reanimated/view {:style (style/avatar-container avatar-opacity)} + (cond + (string/blank? avatar-image-src) + [loading-square theme] + + community? + [preview-list/view {:type :communities :size :size-20} + [avatar-image-src]] + + :else + [collection-avatar/view + {:size :size-20 + :on-start #(set-load-time (fn [start-time] (- (datetime/now) start-time))) + :on-load-end set-avatar-loaded + :image avatar-image-src}]) + [text/text + {:size :paragraph-1 + :weight :semi-bold + :ellipsize-mode :tail + :number-of-lines 1 + :style (style/card-detail-text empty-name? theme)} + (if empty-name? + (i18n/label :t/unknown) + collectible-name)]] + + (when-not (:avatar-loaded? state) + [reanimated/view {:style (style/card-loader loader-opacity)} + [loading-square theme] + [loading-message theme]])])) + +(defn- card-view + [{:keys [avatar-image-src collectible-name community? counter state set-state + gradient-color-index image-src supported-file? loading?]}] + (let [theme (quo.context/use-theme) + loader-opacity (reanimated/use-shared-value (if supported-file? 1 0)) + image-opacity (reanimated/use-shared-value (if supported-file? 0 1)) + [load-time set-load-time] (rn/use-state (datetime/now)) + set-image-loaded (fn [] + (animate-loading-image {:load-time load-time + :set-state set-state + :loader-opacity loader-opacity + :image-opacity image-opacity}))] + [rn/view {:style (style/card-view-container theme)} + [rn/view {:style {:aspect-ratio 1}} + (cond + (:image-error? state) [fallback-view + {:image-opacity image-opacity + :on-load-end set-image-loaded + :theme theme + :label (i18n/label :t/cant-fetch-info)}] + (not supported-file?) [fallback-view + {:image-opacity image-opacity + :on-load-end set-image-loaded + :theme theme + :label (i18n/label :t/unsupported-file)}] + (or (not (:image-loaded? state)) + loading?) [loading-image + {:loader-opacity loader-opacity + :theme theme + :gradient-color-index gradient-color-index}]) + + (when (and supported-file? (not loading?)) + [reanimated/view {:style (style/supported-file image-opacity)} + [rn/image + {:style style/image + :on-load #(on-load % set-state) + :on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time))) + :on-load-end set-image-loaded + :on-error #(on-load-error set-state) + :source image-src}]])] + + (when (and (or (:image-loaded? state) + (not supported-file?)) + (not (:image-error? state)) + counter) + [collectible-counter/view + {:container-style style/collectible-counter + :size :size-24 + :value counter}]) + [card-details + {:state state + :set-state set-state + :community? community? + :avatar-image-src avatar-image-src + :collectible-name collectible-name + :loading? loading? + :theme theme}]])) + +(defn- image-view + [{:keys [avatar-image-src community? counter state set-state + gradient-color-index image-src supported-file?]}] + (let [theme (quo.context/use-theme) + loader-opacity (reanimated/use-shared-value (if supported-file? 1 0)) + image-opacity (reanimated/use-shared-value (if supported-file? 0 1)) + [load-time set-load-time] (rn/use-state (datetime/now))] + [rn/view {:style style/image-view-container} + (cond + (:image-error? state) + [fallback-view + {:image-opacity image-opacity + :theme theme + :label (i18n/label :t/cant-fetch-info)}] + + (not supported-file?) + [fallback-view + {:image-opacity image-opacity + :theme theme + :label (i18n/label :t/unsupported-file)}] + + (not (:image-loaded? state)) + [loading-image + {:loader-opacity loader-opacity + :theme theme + :gradient-color-index gradient-color-index}]) + (when supported-file? + [reanimated/view {:style (style/supported-file image-opacity)} + [rn/image + {:style style/image + :on-load #(on-load % set-state) + :on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time))) + :on-load-end #(animate-loading-image {:load-time load-time + :set-state set-state + :loader-opacity loader-opacity + :image-opacity image-opacity}) + :on-error #(on-load-error set-state) + :source image-src}]]) + (when (and (:image-loaded? state) (not (:image-error? state)) counter) + [collectible-counter/view + {:container-style style/collectible-counter + :size :size-24 + :value counter}]) + (when (and (:image-loaded? state) (not (:image-error? state)) community?) + [preview-list/view + {:container-style style/avatar + :type :communities + :size :size-24} + [avatar-image-src]])])) + +(defn- view-internal + [{:keys [container-style type on-press on-long-press supported-file?] + :as props}] + (let [[state set-state] (rn/use-state {:image-loaded? false + :image-aspect-ratio nil + :image-error? false + :avatar-loaded? false}) + collectible-ready? (or (:image-loaded? state) (not supported-file?))] + [rn/pressable + {:on-press (when collectible-ready? #(on-press (:image-aspect-ratio state))) + :on-long-press (when collectible-ready? on-long-press) + :accessibility-label :collectible-list-item + :style container-style} + (if (= type :card) + [card-view + (assoc props + :state state + :set-state set-state + :supported-file? supported-file?)] + [image-view + (assoc props + :state state + :set-state set-state + :supported-file? supported-file?)])])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:avatar-image-src {:optional true} [:maybe :schema.common/image-source]] + [:collectible-name {:optional true} [:maybe string?]] + [:supported-file? {:optional true} [:maybe boolean?]] + [:native-ID {:optional true} [:maybe [:or string? keyword?]]] + [:community? {:optional true} [:maybe boolean?]] + [:loading? {:optional true} [:maybe boolean?]] + [:counter {:optional true} [:maybe [:or :string :int]]] + [:gradient-color-index {:optional true} + [:maybe [:enum :gradient-1 :gradient-2 :gradient-3 :gradient-4 :gradient-5]]] + [:image-src {:optional true} [:maybe :schema.common/image-source]] + [:on-press {:optional true} [:maybe fn?]] + [:on-long-press {:optional true} [:maybe fn?]] + [:type [:enum :card :image]] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/profile/expanded_collectible/style.cljs b/src/quo/components/profile/expanded_collectible/style.cljs new file mode 100644 index 00000000000..2a757ecbf5e --- /dev/null +++ b/src/quo/components/profile/expanded_collectible/style.cljs @@ -0,0 +1,70 @@ +(ns quo.components.profile.expanded-collectible.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [aspect-ratio] + (cond-> {:align-items :center + :justify-content :center + :border-radius 16} + aspect-ratio (assoc :width "100%" + :aspect-ratio aspect-ratio))) + +(defn image + [square? aspect-ratio theme] + {:width "100%" + :aspect-ratio (if square? 1 aspect-ratio) + :border-radius 16 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(defn collectible-border + [theme] + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme)}) + +(defn fallback + ([theme] + (fallback theme 1)) + ([theme aspect-ratio] + {:background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme) + :border-style :dashed + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :border-width 1 + :border-radius 16 + :width "100%" + :aspect-ratio aspect-ratio + :align-items :center + :justify-content :center})) + +(def counter + {:position :absolute + :top 12 + :right 12}) + +(defn loading-image-with-opacity + [opacity] + [{:align-items :center + :aspect-ratio 1 + :border-radius 16 + :justify-content :center + :opacity opacity + :position :absolute + :width "100%" + :z-index 1}]) + +(defn gradient-view + [aspect-ratio] + {:border-radius 16 + :width "100%" + :aspect-ratio aspect-ratio + :align-items :center + :justify-content :center}) + +(defn supported-file + [opacity] + {:opacity opacity}) diff --git a/src/quo/components/profile/expanded_collectible/view.cljs b/src/quo/components/profile/expanded_collectible/view.cljs new file mode 100644 index 00000000000..b9b8cf46b39 --- /dev/null +++ b/src/quo/components/profile/expanded_collectible/view.cljs @@ -0,0 +1,138 @@ +(ns quo.components.profile.expanded-collectible.view + (:require + [clojure.string :as string] + [quo.components.counter.collectible-counter.view :as collectible-counter] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.profile.expanded-collectible.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.gradients :as gradients] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def loader-out 650) +(def image-in 1000) +(def error-wait-time 800) + +(defn on-load-end + [{:keys [loader-opacity image-opacity]}] + (reanimated/animate loader-opacity 0 loader-out) + (reanimated/animate image-opacity 1 image-in)) + +(defn on-load-error + [set-error] + (js/setTimeout set-error error-wait-time)) + +(defn- loading-image + [{:keys [theme gradient-color-index loader-opacity aspect-ratio]}] + [reanimated/view {:style (style/loading-image-with-opacity loader-opacity)} + [gradients/view + {:theme theme + :container-style (style/gradient-view aspect-ratio) + :color-index gradient-color-index}]]) + +(defn- counter-view + [counter] + [collectible-counter/view + {:container-style style/counter + :value counter}]) + +(defn- fallback-view + [{:keys [label theme counter on-mount]}] + (rn/use-mount on-mount) + [rn/view {:style (style/fallback theme)} + [counter-view counter] + [rn/view + [icon/icon :i/sad {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}]] + [rn/view {:style {:height 4}}] + [text/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}} + label]]) + +(defn- collectible-image + [{:keys [aspect-ratio theme gradient-color-index supported-file? set-error native-ID + square? image-src on-collectible-load counter]}] + (let [loader-opacity (reanimated/use-shared-value (if supported-file? 1 0)) + image-opacity (reanimated/use-shared-value (if supported-file? 0 1))] + [:<> + [loading-image + {:aspect-ratio aspect-ratio + :loader-opacity loader-opacity + :theme theme + :gradient-color-index gradient-color-index}] + + [reanimated/view {:style (style/supported-file image-opacity)} + [rn/image + {:style (style/image square? aspect-ratio theme) + :source image-src + :native-ID native-ID + :on-load-end (fn [] + (on-load-end {:loader-opacity loader-opacity + :image-opacity image-opacity})) + :on-error #(on-load-error set-error) + :on-load on-collectible-load}] + (when counter + [counter-view counter]) + [rn/view {:style (style/collectible-border theme)}]]])) + +(defn invalid-image? + [image-src] + (or (nil? image-src) + (string/blank? image-src))) + +(defn view-internal + [{:keys [container-style square? on-press counter image-src native-ID supported-file? + on-collectible-load aspect-ratio gradient-color-index] + :or {gradient-color-index :gradient-1 + on-collectible-load (fn [])}}] + (let [theme (quo.context/use-theme) + [error? set-error] (rn/use-state (invalid-image? image-src))] + (rn/use-effect #(set-error (invalid-image? image-src)) + [image-src]) + [rn/pressable + {:style (merge container-style (style/container aspect-ratio)) + :accessibility-label :expanded-collectible + :on-press (when (and (not error?) supported-file?) + on-press)} + (if (or (not supported-file?) error?) + [fallback-view + {:label (i18n/label (if-not supported-file? + :t/unsupported-file + :t/cant-fetch-info)) + :counter counter + :theme theme + :on-mount on-collectible-load}] + [collectible-image + {:aspect-ratio aspect-ratio + :theme theme + :supported-file? supported-file? + :set-error set-error + :native-ID native-ID + :square? square? + :image-src image-src + :on-collectible-load on-collectible-load + :counter counter + :gradient-color-index gradient-color-index}])])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:aspect-ratio {:optional true} [:maybe number?]] + [:image-src {:optional true} [:maybe string?]] + [:supported-file? {:optional true} [:maybe boolean?]] + [:container-style {:optional true} [:maybe :map]] + [:native-ID {:optional true} [:maybe [:or string? keyword?]]] + [:square? {:optional true} [:maybe boolean?]] + [:counter {:optional true} [:maybe string?]] + [:on-press {:optional true} [:maybe fn?]] + [:on-collectible-load {:optional true} [:maybe fn?]] + [:gradient-color-index {:optional true} [:maybe keyword?]]]]] + :any]) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/profile/link_card/component_spec.cljs b/src/quo/components/profile/link_card/component_spec.cljs new file mode 100644 index 00000000000..2c80d8f9337 --- /dev/null +++ b/src/quo/components/profile/link_card/component_spec.cljs @@ -0,0 +1,20 @@ +(ns quo.components.profile.link-card.component-spec + (:require [quo.core :as quo] + [test-helpers.component :as h])) + +(h/describe "Link Card Component" + (h/test "component renders with address" + (h/render [quo/link-card + {:address "some address" + :icon :social/link}]) + (h/is-truthy (h/query-by-label-text :address)) + (h/is-truthy (h/query-by-label-text :social-icon))) + + (h/test "component renders with title and address" + (h/render [quo/link-card + {:title "Website" + :icon :social/link + :address "bento.me/fracesca"}]) + (h/is-truthy (h/query-by-label-text :address)) + (h/is-truthy (h/query-by-label-text :title)) + (h/is-truthy (h/query-by-label-text :social-icon)))) diff --git a/src/quo/components/profile/link_card/properties.cljs b/src/quo/components/profile/link_card/properties.cljs new file mode 100644 index 00000000000..1f55910e9d7 --- /dev/null +++ b/src/quo/components/profile/link_card/properties.cljs @@ -0,0 +1,10 @@ +(ns quo.components.profile.link-card.properties + (:require [quo.foundations.colors :as colors])) + +(defn gradient-start-color + [theme customization-color] + (colors/resolve-color customization-color theme 0)) + +(defn gradient-end-color + [theme customization-color] + (colors/resolve-color customization-color theme 6)) diff --git a/src/quo/components/profile/link_card/style.cljs b/src/quo/components/profile/link_card/style.cljs new file mode 100644 index 00000000000..f1080d6ba8d --- /dev/null +++ b/src/quo/components/profile/link_card/style.cljs @@ -0,0 +1,23 @@ +(ns quo.components.profile.link-card.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:height 88 + :border-width 1 + :border-radius 16 + :padding 12 + :border-color (colors/theme-colors + colors/neutral-80-opa-5 + colors/white-opa-5 + theme)}) + +(def icon-container + {:margin-bottom 4}) + +(defn address + [theme] + {:color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}) diff --git a/src/quo/components/profile/link_card/view.cljs b/src/quo/components/profile/link_card/view.cljs new file mode 100644 index 00000000000..982d0a71d73 --- /dev/null +++ b/src/quo/components/profile/link_card/view.cljs @@ -0,0 +1,36 @@ +(ns quo.components.profile.link-card.view + (:require [quo.components.markdown.text :as text] + [quo.components.profile.link-card.properties :as properties] + [quo.components.profile.link-card.style :as style] + [quo.components.utilities.social.view :as social] + [quo.context] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient])) + +(defn view + [{:keys [address on-press icon title customization-color container-style]}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:accessibility-label :link-card + :on-press on-press} + [linear-gradient/linear-gradient + {:colors [(properties/gradient-start-color theme customization-color) + (properties/gradient-end-color theme customization-color)] + :start {:x 0 :y 1} + :end {:x 1 :y 1} + :style (merge (style/container theme) container-style)} + [rn/view {:style style/icon-container} + [social/view + {:accessibility-label :social-icon + :social icon}]] + [text/text + {:accessibility-label :title + :number-of-lines 1 + :weight :semi-bold} + title] + [text/text + {:accessibility-label :address + :size :paragraph-2 + :numberOfLines 1 + :style (style/address theme)} + address]]])) diff --git a/src/quo/components/profile/profile_card/style.cljs b/src/quo/components/profile/profile_card/style.cljs new file mode 100644 index 00000000000..a84b01ac00b --- /dev/null +++ b/src/quo/components/profile/profile_card/style.cljs @@ -0,0 +1,44 @@ +(ns quo.components.profile.profile-card.style + (:require + [quo.foundations.colors :as colors])) + +(defn card-container + [{:keys [customization-color padding-bottom border-bottom-radius]}] + {:padding-horizontal 12 + :padding-top 12 + :padding-bottom padding-bottom + :flex 1 + :border-top-left-radius 16 + :border-top-right-radius 16 + :border-bottom-left-radius border-bottom-radius + :border-bottom-right-radius border-bottom-radius + :background-color (colors/resolve-color customization-color :light 40)}) + +(def card-header + {:flex-direction :row + :justify-content :space-between}) + +(def name-container + {:flex-direction :row + :margin-top 8 + :align-items :center + :padding-right 12}) + +(def user-name + {:color colors/white}) + +(def emoji-hash + {:margin-top 12 + :letter-spacing 1.5}) + +(def user-hash + {:margin-top 2 + :color colors/white-opa-60}) + +(def keycard-icon + {:margin-left 4 + :color colors/white-opa-40}) + +(def option-button + {:background-color colors/white-opa-5 + :margin-left 8}) diff --git a/src/quo/components/profile/profile_card/view.cljs b/src/quo/components/profile/profile_card/view.cljs new file mode 100644 index 00000000000..9b3b22cf137 --- /dev/null +++ b/src/quo/components/profile/profile_card/view.cljs @@ -0,0 +1,104 @@ +(ns quo.components.profile.profile-card.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.profile.profile-card.style :as style] + [quo.components.tags.tag :as tag] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [utils.i18n :as i18n])) + +(defn profile-card + [{:keys [keycard-account? profile-picture name + customization-color emoji-hash on-options-press + show-emoji-hash? show-options-button? show-user-hash? + show-logged-in? on-card-press login-card? last-item? card-style] + :or {show-emoji-hash? false + show-user-hash? false + customization-color :turquoise + show-options-button? false + show-logged-in? false + keycard-account? false + login-card? false + last-item? false + card-style {:padding-horizontal 20 + :flex 1}} + :as args}] + (let [{:keys [width]} (rn/get-window) + padding-bottom (cond + login-card? 38 + show-emoji-hash? 12 + :else 10) + border-bottom-radius (if (or (not login-card?) last-item?) 16 0)] + [rn/pressable + {:on-press on-card-press + :accessibility-label :profile-card} + [hole-view/hole-view + {:key (str name last-item?) ;; Key is required to force removal of holes + :style (merge {:flex-direction :row} card-style) + :holes (if (or (not login-card?) last-item?) + [] + [{:x 20 + :y 108 + :width (- width 40) + :height 50 + :borderRadius 16}])} + [rn/view + {:style (style/card-container + {:customization-color customization-color + :padding-bottom padding-bottom + :border-bottom-radius border-bottom-radius})} + [rn/view + {:style style/card-header} + [user-avatar/user-avatar + {:full-name name + :profile-picture profile-picture + :size :medium + :status-indicator? false + :static? true}] + [rn/view {:flex-direction :row} + (when show-logged-in? + [tag/tag + {:type :icon + :size 32 + :blurred? true + :labelled? true + :resource :i/check + :accessibility-label :logged-in-tag + :icon-color colors/success-50 + :override-theme :dark + :label (i18n/label :t/logged-in)}]) + (when show-options-button? + [button/button + {:size 32 + :type :grey + :background :blur + :icon-only? true + :container-style style/option-button + :on-press on-options-press + :accessibility-label :profile-card-options} + :i/options])]] + [rn/view + {:style style/name-container} + [text/text + {:size :heading-2 + :weight :semi-bold + :number-of-lines 1 + :style style/user-name} name] + (when keycard-account? + [icon/icon + :i/keycard + style/keycard-icon])] + (when show-user-hash? + [text/text + {:weight :monospace + :style style/user-hash} + (:hash args)]) + (when (and show-emoji-hash? emoji-hash) + [text/text + {:weight :monospace + :number-of-lines 1 + :style style/emoji-hash} emoji-hash])]]])) diff --git a/src/quo/components/profile/select_profile/component_spec.cljs b/src/quo/components/profile/select_profile/component_spec.cljs new file mode 100644 index 00000000000..fe2d8bae57d --- /dev/null +++ b/src/quo/components/profile/select_profile/component_spec.cljs @@ -0,0 +1,17 @@ +(ns quo.components.profile.select-profile.component-spec + (:require + [quo.components.profile.select-profile.view :as select-profile] + [test-helpers.component :as h])) + +(h/describe "select-profile component" + (h/test "render component" + (h/render-with-theme-provider [select-profile/view]) + (-> (h/expect (h/get-by-label-text :select-profile)) + (.toBeTruthy))) + (h/test "call on-change handler when clicked" + (let [on-change (h/mock-fn)] + (h/render-with-theme-provider [select-profile/view {:on-change on-change}]) + (h/fire-event :on-press (h/get-by-label-text :select-profile)) + (-> (h/expect on-change) + (.toHaveBeenCalledTimes 1))))) + diff --git a/src/quo/components/profile/select_profile/style.cljs b/src/quo/components/profile/select_profile/style.cljs new file mode 100644 index 00000000000..d7af80d9b2b --- /dev/null +++ b/src/quo/components/profile/select_profile/style.cljs @@ -0,0 +1,38 @@ +(ns quo.components.profile.select-profile.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [customization-color selected?] + {:padding 12 + :background-color (colors/resolve-color customization-color :light 40) + :border-width 1 + :border-radius 16 + :flex 1 + :border-color (if selected? colors/white-opa-40 :transparent)}) + +(def header + {:flex-direction :row + :justify-content :space-between}) + +(def profile-name + {:margin-top 8 + :color colors/white}) + +(defn select-radio + [selected?] + {:background-color (if selected? :transparent colors/white-opa-5) + :border-width 1.2 + :width 20 + :height 20 + :border-radius 10 + :justify-content :center + :align-items :center + :border-color (if selected? colors/white colors/white-opa-40)}) + +(def select-radio-inner + {:background-color colors/white + :border-radius 7 + :width 14 + :height 14}) + diff --git a/src/quo/components/profile/select_profile/view.cljs b/src/quo/components/profile/select_profile/view.cljs new file mode 100644 index 00000000000..51d413367e2 --- /dev/null +++ b/src/quo/components/profile/select_profile/view.cljs @@ -0,0 +1,35 @@ +(ns quo.components.profile.select-profile.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.markdown.text :as text] + [quo.components.profile.select-profile.style :as style] + [react-native.core :as rn])) + +(defn view + "Options + - `selected?` Selected state. Use when the component is controlled. + - `name` Full name + - `profile-picture` Profile picture + - `customization-color` Customization color + - `(on-change selected?)` On change event handler + " + [{:keys [selected? profile-picture name customization-color on-change] + :or {customization-color :turquoise}}] + [rn/touchable-opacity + {:style (style/container customization-color selected?) + :on-press #(when on-change (on-change (not selected?))) + :accessibility-label :select-profile} + [rn/view {:style style/header} + [user-avatar/user-avatar + {:full-name name + :status-indicator? false + :size :medium + :profile-picture profile-picture}] + [rn/view {:style (style/select-radio selected?)} + (when selected? + [rn/view {:style style/select-radio-inner}])]] + [text/text + {:size :heading-2 + :weight :semi-bold + :style style/profile-name} + name]]) diff --git a/src/quo/components/profile/showcase_nav/component_spec.cljs b/src/quo/components/profile/showcase_nav/component_spec.cljs new file mode 100644 index 00000000000..85873ef132f --- /dev/null +++ b/src/quo/components/profile/showcase_nav/component_spec.cljs @@ -0,0 +1,43 @@ +(ns quo.components.profile.showcase-nav.component-spec + (:require + [quo.components.profile.showcase-nav.view :as view] + [test-helpers.component :as h])) + +(def nav-data + [{:icon :i/recent + :id :recent} + {:icon :i/profile + :id :profile} + {:icon :i/communities + :id :communities} + {:icon :i/wallet + :id :wallet} + {:icon :i/nft + :id :nft} + {:icon :i/token + :id :token}]) + +(h/describe "Profile - Showcase nav" + (h/test "default render" + (h/render [view/view {:data nav-data}]) + (-> (count (h/query-all-by-label-text :showcase-nav-item)) + (h/expect) + (.toEqual (count nav-data)))) + + (h/test "on press button" + (let [event (h/mock-fn)] + (h/render [view/view + {:data nav-data + :on-press #(event)}]) + (h/fire-event :press (get (h/get-all-by-label-text :showcase-nav-item) 0)) + (-> (h/expect event) + (.toHaveBeenCalled)))) + + (h/test "active id updated" + (let [active-id (atom :recent)] + (h/render [view/view + {:data nav-data + :on-press #(reset! active-id %)}]) + (h/fire-event :press (get (h/get-all-by-label-text :showcase-nav-item) 3)) + (-> (h/expect @active-id) + (.toStrictEqual :wallet))))) diff --git a/src/quo/components/profile/showcase_nav/style.cljs b/src/quo/components/profile/showcase_nav/style.cljs new file mode 100644 index 00000000000..5516cbb37eb --- /dev/null +++ b/src/quo/components/profile/showcase_nav/style.cljs @@ -0,0 +1,15 @@ +(ns quo.components.profile.showcase-nav.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [state theme] + {:padding-top 12 + :padding-left 20 + :padding-bottom 12 + :background-color (case state + :default (colors/theme-colors colors/white colors/neutral-95 theme) + :transparent)}) + +(def button-container + {:margin-right 8}) diff --git a/src/quo/components/profile/showcase_nav/view.cljs b/src/quo/components/profile/showcase_nav/view.cljs new file mode 100644 index 00000000000..c576263b124 --- /dev/null +++ b/src/quo/components/profile/showcase_nav/view.cljs @@ -0,0 +1,40 @@ +(ns quo.components.profile.showcase-nav.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.profile.showcase-nav.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn- render-button + [{: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)] + [button/button + {:accessibility-label :showcase-nav-item + :size 32 + :type button-type + :background (when scroll-state? :blur) + :icon-only? true + :on-press (fn [] + (when on-press + (on-press id index))) + :container-style style/button-container} + icon])) + +(defn view + [{:keys [container-style default-active state data on-press active-id]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style container-style + :accessibility-label :showcase-nav} + [rn/flat-list + {:data data + :key-fn :id + :horizontal true + :shows-horizontal-scroll-indicator false + :content-container-style (style/container state theme) + :render-fn render-button + :render-data {:state state + :on-press on-press + :active-id (or active-id default-active)}}]])) diff --git a/src/quo/components/record_audio/record_audio/buttons/delete_button.cljs b/src/quo/components/record_audio/record_audio/buttons/delete_button.cljs new file mode 100644 index 00000000000..e49e0c52746 --- /dev/null +++ b/src/quo/components/record_audio/record_audio/buttons/delete_button.cljs @@ -0,0 +1,95 @@ +(ns quo.components.record-audio.record-audio.buttons.delete-button + (:require + [quo.components.icon :as icons] + [quo.components.record-audio.record-audio.helpers :as helpers] + [quo.components.record-audio.record-audio.style :as style] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated])) + +(defn delete-button + [recording? ready-to-delete? reviewing-audio? force-show-controls?] + (let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0)) + translate-x (reanimated/use-shared-value (if force-show-controls? 35 20)) + scale (reanimated/use-shared-value (if force-show-controls? 0.75 1)) + connector-opacity (reanimated/use-shared-value 0) + connector-width (reanimated/use-shared-value 24) + connector-height (reanimated/use-shared-value 12) + border-radius-first-half (reanimated/use-shared-value 8) + border-radius-second-half (reanimated/use-shared-value 8) + start-x-animation (rn/use-callback + (fn [] + (helpers/animate-linear-with-delay translate-x 12 50 133.33) + (helpers/animate-easing-with-delay connector-opacity 1 0 93.33) + (helpers/animate-easing-with-delay connector-width 56 83.33 80) + (helpers/animate-easing-with-delay connector-height 56 83.33 80) + (helpers/animate-easing-with-delay border-radius-first-half + 28 + 83.33 + 80) + (helpers/animate-easing-with-delay border-radius-second-half + 28 + 83.33 + 80))) + reset-x-animation (rn/use-callback + (fn [] + (helpers/animate-linear translate-x 0 100) + (helpers/set-value connector-opacity 0) + (helpers/set-value connector-width 24) + (helpers/set-value connector-height 12) + (helpers/set-value border-radius-first-half 8) + (helpers/set-value border-radius-second-half 16))) + fade-in-animation (rn/use-callback + (fn [] + (helpers/animate-linear translate-x 0 200) + (helpers/animate-linear opacity 1 200))) + fade-out-animation (rn/use-callback + (fn [] + (helpers/animate-linear + translate-x + (if reviewing-audio? 35 20) + 200) + (if reviewing-audio? + (helpers/animate-linear scale 0.75 200) + (helpers/animate-linear opacity 0 200)) + (helpers/set-value connector-opacity 0) + (helpers/set-value connector-width 24) + (helpers/set-value connector-height 12) + (helpers/set-value border-radius-first-half 8) + (helpers/set-value border-radius-second-half 16)) + [reviewing-audio?]) + fade-out-reset-animation (rn/use-callback + (fn [] + (helpers/animate-linear opacity 0 200) + (helpers/animate-linear-with-delay translate-x 20 0 200) + (helpers/animate-linear-with-delay scale 1 0 200)))] + (rn/use-effect (fn [] + (if recording? + (fade-in-animation) + (fade-out-animation))) + [recording?]) + (rn/use-effect (fn [] + (when-not reviewing-audio? + (fade-out-reset-animation))) + [reviewing-audio?]) + (rn/use-effect (fn [] + (cond + ready-to-delete? + (start-x-animation) + recording? + (reset-x-animation))) + [ready-to-delete?]) + [:<> + [reanimated/view {:style (style/delete-button-container opacity)} + [reanimated/view + {:style (style/delete-button-connector connector-opacity + connector-width + connector-height + border-radius-first-half + border-radius-second-half)}]] + [reanimated/view + {:style (style/delete-button scale translate-x opacity) + :pointer-events :none} + [icons/icon :i/delete + {:color colors/white + :size 20}]]])) diff --git a/src/quo/components/record_audio/record_audio/buttons/lock_button.cljs b/src/quo/components/record_audio/record_audio/buttons/lock_button.cljs new file mode 100644 index 00000000000..99f85c89bf9 --- /dev/null +++ b/src/quo/components/record_audio/record_audio/buttons/lock_button.cljs @@ -0,0 +1,87 @@ +(ns quo.components.record-audio.record-audio.buttons.lock-button + (:require + [quo.components.icon :as icons] + [quo.components.record-audio.record-audio.helpers :as helpers] + [quo.components.record-audio.record-audio.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated])) + +(defn lock-button + [recording? ready-to-lock? locked?] + (let [theme (quo.context/use-theme) + translate-x-y (reanimated/use-shared-value 20) + opacity (reanimated/use-shared-value 0) + connector-opacity (reanimated/use-shared-value 0) + width (reanimated/use-shared-value 24) + height (reanimated/use-shared-value 12) + border-radius-first-half (reanimated/use-shared-value 8) + border-radius-second-half (reanimated/use-shared-value 8) + start-x-y-animation (rn/use-callback + (fn [] + (helpers/animate-linear-with-delay translate-x-y 8 50 116.66) + (helpers/animate-easing-with-delay connector-opacity 1 0 80) + (helpers/animate-easing-with-delay width 56 83.33 63.33) + (helpers/animate-easing-with-delay height 56 83.33 63.33) + (helpers/animate-easing-with-delay border-radius-first-half + 28 + 83.33 + 63.33) + (helpers/animate-easing-with-delay border-radius-second-half + 28 + 83.33 + 63.33))) + reset-x-y-animation (rn/use-callback + (fn [] + (helpers/animate-linear translate-x-y 0 100) + (helpers/set-value connector-opacity 0) + (helpers/set-value width 24) + (helpers/set-value height 12) + (helpers/set-value border-radius-first-half 8) + (helpers/set-value border-radius-second-half 16))) + fade-in-animation (rn/use-callback + (fn [] + (helpers/animate-linear translate-x-y 0 220) + (helpers/animate-linear opacity 1 220))) + fade-out-animation (rn/use-callback + (fn [] + (helpers/animate-linear translate-x-y 20 200) + (helpers/animate-linear opacity 0 200) + (helpers/set-value connector-opacity 0) + (helpers/set-value width 24) + (helpers/set-value height 12) + (helpers/set-value border-radius-first-half 8) + (helpers/set-value border-radius-second-half 16)))] + (rn/use-effect (fn [] + (if recording? + (fade-in-animation) + (fade-out-animation))) + [recording?]) + (rn/use-effect (fn [] + (cond + ready-to-lock? + (start-x-y-animation) + (and recording? (not locked?)) + (reset-x-y-animation))) + [ready-to-lock?]) + (rn/use-effect (fn [] + (if locked? + (fade-out-animation) + (reset-x-y-animation))) + [locked?]) + [:<> + [reanimated/view {:style (style/lock-button-container opacity)} + [reanimated/view + {:style (style/lock-button-connector connector-opacity + width + height + border-radius-first-half + border-radius-second-half + theme)}]] + [reanimated/view + {:style (style/lock-button translate-x-y opacity theme) + :pointer-events :none} + [icons/icon (if ready-to-lock? :i/locked :i/unlocked) + {:color (colors/theme-colors colors/black colors/white theme) + :size 20}]]])) diff --git a/src/quo/components/record_audio/record_audio/buttons/record_button.cljs b/src/quo/components/record_audio/record_audio/buttons/record_button.cljs new file mode 100644 index 00000000000..48a9d0ecd15 --- /dev/null +++ b/src/quo/components/record_audio/record_audio/buttons/record_button.cljs @@ -0,0 +1,23 @@ +(ns quo.components.record-audio.record-audio.buttons.record-button + (:require + [quo.components.buttons.button.view :as button] + [quo.components.record-audio.record-audio.helpers :as helpers] + [quo.components.record-audio.record-audio.style :as style] + [react-native.core :as rn] + [react-native.reanimated :as reanimated])) + +(defn record-button + [recording? reviewing-audio?] + (let [opacity (reanimated/use-shared-value 1)] + (rn/use-effect (fn [] + (if (or recording? reviewing-audio?) + (helpers/set-value opacity 0) + (helpers/set-value opacity 1))) + [recording? reviewing-audio?]) + [reanimated/view {:style (style/record-button-container opacity)} + [button/button + {:type :outline + :size 32 + :accessibility-label :mic-button + :icon-only? true} + :i/audio]])) diff --git a/src/quo/components/record_audio/record_audio/buttons/record_button_big.cljs b/src/quo/components/record_audio/record_audio/buttons/record_button_big.cljs new file mode 100644 index 00000000000..457b1d51913 --- /dev/null +++ b/src/quo/components/record_audio/record_audio/buttons/record_button_big.cljs @@ -0,0 +1,219 @@ +(ns quo.components.record-audio.record-audio.buttons.record-button-big + (:require + [cljs-bean.core :as bean] + [quo.components.icon :as icons] + [quo.components.record-audio.record-audio.helpers :as helpers] + [quo.components.record-audio.record-audio.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.audio-toolkit :as audio] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [reagent.core :as reagent] + [taoensso.timbre :as log] + [utils.worklets.record-audio :as worklets.record-audio])) + +(def ^:private scale-to-each 1.8) +(def ^:private scale-to-total 2.6) +(def ^:private scale-padding 0.16) +(def ^:private opacity-from-lock 1) +(def ^:private opacity-from-default 0.5) +(def ^:private signal-anim-duration 3900) +(def ^:private signal-anim-duration-2 1950) + +(def ^:private animated-ring + (reagent/adapt-react-class + (rn/memo + (fn [props] + (let [{:keys [scale opacity color]} (bean/bean props)] + (reagent/as-element + [reanimated/view {:style (style/animated-circle scale opacity color)}])))))) + +(defn record-button-big + [{:keys [recording? set-recording ready-to-send? set-ready-to-send ready-to-lock? set-ready-to-lock + ready-to-delete? set-ready-to-delete record-button-is-animating? + record-button-at-initial-position? locked? set-locked reviewing-audio? recording-length-ms + set-recording-length-ms + clear-timeout touch-active? recorder-ref reload-recorder-fn idle? on-send on-cancel]}] + (let [theme (quo.context/use-theme) + scale (reanimated/use-shared-value 1) + opacity (reanimated/use-shared-value 0) + opacity-from (if ready-to-lock? opacity-from-lock opacity-from-default) + animations (map + (fn [index] + (let [ring-scale (worklets.record-audio/ring-scale scale + (* scale-padding + index))] + {:scale ring-scale + :opacity (reanimated/interpolate ring-scale + [1 scale-to-each] + [opacity-from 0])})) + (range 0 5)) + rings-color (cond + ready-to-lock? (colors/theme-colors colors/neutral-80-opa-5-opaque + colors/neutral-80 + theme) + ready-to-delete? colors/danger-50 + :else colors/primary-50) + translate-y (reanimated/use-shared-value 0) + translate-x (reanimated/use-shared-value 0) + button-color colors/primary-50 + icon-color (if (and (not (= :dark theme)) ready-to-lock?) colors/black colors/white) + icon-opacity (reanimated/use-shared-value 1) + red-overlay-opacity (reanimated/use-shared-value 0) + gray-overlay-opacity (reanimated/use-shared-value 0) + complete-animation + (rn/use-callback + (fn [] + (cond + (and ready-to-lock? (not @record-button-is-animating?)) + (do + (set-locked true) + (set-ready-to-lock false)) + (and (not locked?) (not reviewing-audio?)) + (audio/stop-recording + @recorder-ref + (fn [] + (cond + ready-to-send? + (when on-send + (on-send {:file-path (audio/get-recorder-file-path @recorder-ref) + :duration recording-length-ms})) + ready-to-delete? + (when on-cancel + (on-cancel))) + (reload-recorder-fn) + (set-recording false) + (set-ready-to-send false) + (set-ready-to-delete false) + (set-ready-to-lock false) + (reset! idle? true) + (js/setTimeout #(reset! idle? false) 1000) + (set-recording-length-ms 0) + (log/debug "[record-audio] stop recording - success")) + #(log/error "[record-audio] stop recording - error: " %)))) + [ready-to-lock? locked? reviewing-audio? ready-to-send? recording-length-ms ready-to-delete? + idle?]) + start-animation (rn/use-callback + (fn [] + (helpers/set-value opacity 1) + (helpers/animate-linear scale 2.6 signal-anim-duration) + ;; TODO: Research if we can implement this with withSequence method from + ;; Reanimated 2. GitHub issue [#14561]: + ;; https://github.com/status-im/status-mobile/issues/14561 + (reset! clear-timeout + (js/setTimeout + (fn [] + (helpers/set-value scale scale-to-each) + (helpers/animate-linear-with-delay-loop scale + scale-to-total + signal-anim-duration-2 + 0)) + signal-anim-duration)))) + stop-animation (rn/use-callback + (fn [] + (helpers/set-value opacity 0) + (reanimated/cancel-animation scale) + (helpers/set-value scale 1) + (when @clear-timeout (js/clearTimeout @clear-timeout)))) + start-y-animation (rn/use-callback + (fn [] + (reset! record-button-at-initial-position? false) + (reset! record-button-is-animating? true) + (helpers/animate-easing translate-y -64 250) + (helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66) + (js/setTimeout (fn [] + (reset! record-button-is-animating? false) + (when-not @touch-active? (complete-animation))) + 250)) + [complete-animation]) + reset-y-animation (rn/use-callback + (fn [] + (helpers/animate-easing translate-y 0 300) + (helpers/animate-linear icon-opacity 1 500) + (js/setTimeout (fn [] + (reset! record-button-at-initial-position? true)) + 500))) + start-x-animation (rn/use-callback + (fn [] + (reset! record-button-at-initial-position? false) + (reset! record-button-is-animating? true) + (helpers/animate-easing translate-x -64 250) + (helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66) + (helpers/animate-linear red-overlay-opacity 1 33.33) + (js/setTimeout (fn [] + (reset! record-button-is-animating? false) + (when-not @touch-active? (complete-animation))) + 250)) + [complete-animation]) + reset-x-animation (rn/use-callback + (fn [] + (helpers/animate-easing translate-x 0 300) + (helpers/animate-linear icon-opacity 1 500) + (helpers/animate-linear red-overlay-opacity 0 100) + (js/setTimeout (fn [] + (reset! record-button-at-initial-position? true)) + 500))) + start-x-y-animation (rn/use-callback + (fn [] + (reset! record-button-at-initial-position? false) + (reset! record-button-is-animating? true) + (helpers/animate-easing translate-y -44 200) + (helpers/animate-easing translate-x -44 200) + (helpers/animate-linear-with-delay icon-opacity 0 33.33 33.33) + (helpers/animate-linear gray-overlay-opacity 1 33.33) + (js/setTimeout (fn [] + (reset! record-button-is-animating? false) + (when-not @touch-active? (complete-animation))) + 200)) + [complete-animation]) + reset-x-y-animation (rn/use-callback + (fn [] + (helpers/animate-easing translate-y 0 300) + (helpers/animate-easing translate-x 0 300) + (helpers/animate-linear icon-opacity 1 500) + (helpers/animate-linear gray-overlay-opacity 0 800) + (js/setTimeout (fn [] + (reset! record-button-at-initial-position? true)) + 800)))] + (rn/use-effect (fn [] + (cond + recording? + (start-animation) + (not ready-to-lock?) + (stop-animation))) + [recording?]) + (rn/use-effect (fn [] + (if ready-to-lock? + (start-x-y-animation) + (reset-x-y-animation))) + [ready-to-lock?]) + (rn/use-effect (fn [] + (if ready-to-send? + (start-y-animation) + (reset-y-animation))) + [ready-to-send?]) + (rn/use-effect (fn [] + (if ready-to-delete? + (start-x-animation) + (reset-x-animation))) + [ready-to-delete?]) + [reanimated/view + {:style (style/record-button-big-container translate-x translate-y opacity) + :pointer-events :none} + [:<> + (map-indexed + (fn [id animation] + ^{:key id} + [animated-ring + {:scale (:scale animation) + :opacity (:opacity animation) + :color rings-color}]) + animations)] + [rn/view {:style (style/record-button-big-body button-color)} + [reanimated/view {:style (style/record-button-big-red-overlay red-overlay-opacity)}] + [reanimated/view {:style (style/record-button-big-gray-overlay gray-overlay-opacity theme)}] + [reanimated/view {:style (style/record-button-big-icon-container icon-opacity)} + (if locked? + [rn/view {:style style/stop-icon}] + [icons/icon :i/audio {:color icon-color}])]]])) diff --git a/src/quo/components/record_audio/record_audio/buttons/send_button.cljs b/src/quo/components/record_audio/record_audio/buttons/send_button.cljs new file mode 100644 index 00000000000..85065e1c211 --- /dev/null +++ b/src/quo/components/record_audio/record_audio/buttons/send_button.cljs @@ -0,0 +1,98 @@ +(ns quo.components.record-audio.record-audio.buttons.send-button + (:require + [quo.components.icon :as icons] + [quo.components.record-audio.record-audio.helpers :as helpers] + [quo.components.record-audio.record-audio.style :as style] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated])) + +(defn send-button + [recording? ready-to-send? reviewing-audio? force-show-controls?] + (let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0)) + translate-y (reanimated/use-shared-value (if force-show-controls? 76 20)) + connector-opacity (reanimated/use-shared-value 0) + width (reanimated/use-shared-value 12) + height (reanimated/use-shared-value 24) + border-radius-first-half (reanimated/use-shared-value 16) + border-radius-second-half (reanimated/use-shared-value 8) + start-y-animation (rn/use-callback + (fn [] + (helpers/animate-linear-with-delay translate-y 12 50 133.33) + (helpers/animate-easing-with-delay connector-opacity 1 0 93.33) + (helpers/animate-easing-with-delay width 56 83.33 80) + (helpers/animate-easing-with-delay height 56 83.33 80) + (helpers/animate-easing-with-delay border-radius-first-half + 28 + 83.33 + 80) + (helpers/animate-easing-with-delay border-radius-second-half + 28 + 83.33 + 80))) + reset-y-animation (rn/use-callback + (fn [] + (helpers/animate-linear translate-y 0 100) + (helpers/set-value connector-opacity 0) + (helpers/set-value width 12) + (helpers/set-value height 24) + (helpers/set-value border-radius-first-half 16) + (helpers/set-value border-radius-second-half 8))) + fade-in-animation (rn/use-callback + (fn [] + (helpers/animate-linear translate-y 0 200) + (helpers/animate-linear opacity 1 200))) + fade-out-animation (rn/use-callback + (fn [] + (when-not force-show-controls? + (helpers/animate-linear + translate-y + (if reviewing-audio? 76 20) + 200)) + (when-not reviewing-audio? + (helpers/animate-linear opacity 0 200)) + (helpers/set-value connector-opacity 0) + (helpers/set-value width 24) + (helpers/set-value height 12) + (helpers/set-value border-radius-first-half 8) + (helpers/set-value border-radius-second-half 16)) + [reviewing-audio? force-show-controls?]) + fade-out-reset-animation (rn/use-callback + (fn [] + (helpers/animate-linear opacity 0 200) + (helpers/animate-linear-with-delay translate-y 20 0 200) + (helpers/set-value connector-opacity 0) + (helpers/set-value width 24) + (helpers/set-value height 12) + (helpers/set-value border-radius-first-half 8) + (helpers/set-value border-radius-second-half 16)))] + (rn/use-effect (fn [] + (if recording? + (fade-in-animation) + (fade-out-animation))) + [recording?]) + (rn/use-effect (fn [] + (when-not reviewing-audio? + (fade-out-reset-animation))) + [reviewing-audio?]) + (rn/use-effect (fn [] + (cond + ready-to-send? + (start-y-animation) + recording? (reset-y-animation))) + [ready-to-send?]) + [:<> + [reanimated/view {:style (style/send-button-container opacity)} + [reanimated/view + {:style (style/send-button-connector connector-opacity + width + height + border-radius-first-half + border-radius-second-half)}]] + [reanimated/view + {:style (style/send-button translate-y opacity) + :pointer-events :none} + [icons/icon :i/arrow-up + {:color colors/white + :size 20 + :container-style style/send-icon-container}]]])) diff --git a/src/quo/components/record_audio/record_audio/component_spec.cljs b/src/quo/components/record_audio/record_audio/component_spec.cljs new file mode 100644 index 00000000000..20f895342ae --- /dev/null +++ b/src/quo/components/record_audio/record_audio/component_spec.cljs @@ -0,0 +1,252 @@ +(ns quo.components.record-audio.record-audio.component-spec + (:require + [quo.components.record-audio.record-audio.view :as record-audio] + [react-native.audio-toolkit :as audio] + [test-helpers.component :as h] + [utils.datetime :as datetime])) + +(h/describe "record audio component" + (h/setup-fake-timers) + + (h/test "renders record-audio" + (h/render [record-audio/record-audio]) + (-> (h/wait-for #(h/get-by-test-id "record-audio")) + (.then (fn [] + (-> (h/expect (h/get-by-test-id "record-audio")) + (.toBeTruthy)))))) + + (h/test "record-audio on-start-recording works" + (let [event (js/jest.fn)] + (h/render [record-audio/record-audio + {:on-start-recording event + :record-audio-permission-granted true}]) + (h/fire-event + :on-start-should-set-responder + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 0 + :identifier 0}}) + (-> (h/expect event) + (.toHaveBeenCalledTimes 1)))) + + (h/test "record-audio on-reviewing-audio works" + (let [event (js/jest.fn) + on-meter (atom nil)] + (h/render [record-audio/record-audio + {:on-reviewing-audio event + :record-audio-permission-granted true}]) + (with-redefs [audio/new-recorder (fn [_ on-meter-fn _] + (reset! on-meter on-meter-fn)) + audio/start-recording (fn [_ on-start _] + (on-start) + (js/setInterval @on-meter 100)) + audio/get-recorder-file-path (fn [] "file-path")] + (h/fire-event + :on-start-should-set-responder + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 0 + :identifier 0}}) + + (with-redefs [datetime/timestamp (fn [] (+ (.now js/Date) 1000))] + (h/advance-timers-by-time 100) + (h/fire-event + :on-responder-release + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 200 + :identifier 0}}) + (h/advance-timers-by-time 250) + (-> (h/expect event) + (.toHaveBeenCalledTimes 1)))))) + + (h/test "record-audio on-send works after reviewing audio" + (let [event (js/jest.fn) + on-meter (atom nil)] + (h/render [record-audio/record-audio + {:on-send event + :record-audio-permission-granted true}]) + (with-redefs [audio/new-recorder (fn [_ on-meter-fn _] + (reset! on-meter on-meter-fn)) + audio/start-recording (fn [_ on-start _] + (on-start) + (js/setInterval #(@on-meter) 100)) + audio/get-recorder-file-path (fn [] "audio-file-path") + audio/get-player-duration (fn [] 5000)] + (h/fire-event + :on-start-should-set-responder + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 0 + :identifier 0}}) + (with-redefs [datetime/timestamp (fn [] (+ (.now js/Date) 1000))] + (h/advance-timers-by-time 100) + (h/fire-event + :on-responder-release + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 200 + :identifier 0}}) + (h/fire-event + :on-start-should-set-responder + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 0 + :identifier 0}}) + (h/advance-timers-by-time 500) + (h/fire-event + :on-responder-release + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 80 + :locationY 80 + :timestamp 200 + :identifier 0}}) + (h/advance-timers-by-time 250) + (-> (js/expect event) + (.toHaveBeenCalledTimes 1)) + (-> (js/expect event) + (.toHaveBeenCalledWith {:file-path "audio-file-path" + :duration 5000})))))) + + (h/test "record-audio on-send works after sliding to the send button" + (let [event (js/jest.fn) + on-meter (atom nil) + last-now-ms (atom nil) + duration-ms (atom nil)] + (h/render [record-audio/record-audio + {:on-send event + :record-audio-permission-granted true}]) + (with-redefs [audio/new-recorder (fn [_ on-meter-fn _] + (reset! on-meter on-meter-fn) + #js {:destroy #()}) + audio/start-recording (fn [_ on-start _] + (on-start) + (js/setInterval #(@on-meter) 100)) + audio/stop-recording (fn [_ on-stop _] + (on-stop)) + audio/get-recorder-file-path (fn [] "audio-file-path") + datetime/timestamp (fn [] + (let [now-ms (.now js/Date)] + (reset! last-now-ms now-ms) + now-ms))] + (h/fire-event + :on-start-should-set-responder + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 0 + :identifier 0}}) + (with-redefs [datetime/timestamp (fn [] + (let [now-plus-ms (+ (.now js/Date) 1000) + time-diff-ms (- now-plus-ms @last-now-ms)] + (when-not @duration-ms + (reset! duration-ms time-diff-ms)) + now-plus-ms))] + (h/advance-timers-by-time 100) + (h/fire-event + :on-responder-move + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 80 + :locationY -30 + :pageX 80 + :pageY -30 + :identifier 0}}) + (h/fire-event + :on-responder-release + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 40 + :locationY 80 + :timestamp 200 + :identifier 0}}) + (h/advance-timers-by-time 250) + (-> (js/expect event) + (.toHaveBeenCalledTimes 1)) + (-> (js/expect event) + (.toHaveBeenCalledWith {:file-path "audio-file-path" + :duration @duration-ms})))))) + + (h/test "record-audio on-cancel works after reviewing audio" + (let [event (js/jest.fn) + on-meter (atom nil)] + (h/render [record-audio/record-audio + {:on-cancel event + :record-audio-permission-granted true}]) + (with-redefs [audio/new-recorder (fn [_ on-meter-fn _] + (reset! on-meter on-meter-fn) + #js {:destroy #()}) + audio/start-recording (fn [_ on-start _] + (on-start) + (js/setInterval @on-meter 100))] + (h/fire-event + :on-start-should-set-responder + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 0 + :identifier 0}}) + (with-redefs [datetime/timestamp (fn [] (+ (.now js/Date) 1000))] + (h/advance-timers-by-time 500) + (h/fire-event + :on-responder-release + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 200 + :identifier 0}}) + (h/advance-timers-by-time 250) + (h/fire-event + :on-responder-release + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 40 + :locationY 80 + :timestamp 200 + :identifier 0}}) + (-> (js/expect event) + (.toHaveBeenCalledTimes 1)))))) + + (h/test "record-audio on-cancel works after sliding to the cancel button" + (let [event (js/jest.fn) + on-meter (atom nil)] + (h/render [record-audio/record-audio + {:on-cancel event + :record-audio-permission-granted true}]) + (with-redefs [audio/new-recorder (fn [_ on-meter-fn _] + (reset! on-meter on-meter-fn)) + audio/start-recording (fn [_ on-start _] + (on-start) + (js/setInterval #(@on-meter) 100)) + audio/stop-recording (fn [_ on-stop _] + (on-stop))] + (h/fire-event + :on-start-should-set-responder + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX 70 + :locationY 70 + :timestamp 0 + :identifier 0}}) + (with-redefs [datetime/timestamp (fn [] (+ (.now js/Date) 1000))] + (h/advance-timers-by-time 500) + (h/fire-event + :on-responder-move + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX -30 + :locationY 80 + :pageX -30 + :pageY 80 + :identifier 0}}) + (h/fire-event + :on-responder-release + (h/get-by-test-id "record-audio") + {:nativeEvent {:locationX -10 + :locationY 70 + :timestamp 200 + :identifier 0}}) + (h/advance-timers-by-time 250) + (-> (js/expect event) + (.toHaveBeenCalledTimes 1))))))) diff --git a/src/quo/components/record_audio/record_audio/constants.cljs b/src/quo/components/record_audio/record_audio/constants.cljs new file mode 100644 index 00000000000..36261fb731b --- /dev/null +++ b/src/quo/components/record_audio/record_audio/constants.cljs @@ -0,0 +1,81 @@ +(ns quo.components.record-audio.record-audio.constants + (:require [react-native.audio-toolkit :as audio] + [react-native.platform :as platform] + [utils.datetime :as datetime])) + +(def min-audio-duration-ms 1000) +(def max-audio-duration-ms (if platform/ios? 120800 120500)) +(def metering-interval 25) +(def base-filename "am") +(def default-format ".aac") + +(def min-touch-duration 150) + +(def record-button-area-big + {:width 56 + :height 56 + :x 64 + :y 64}) + +(def record-button-area + {:width 48 + :height 48 + :x 68 + :y 68}) + +(defn delete-button-area + [{:keys [active? reviewing-audio?]}] + {:width (cond + active? 72 + reviewing-audio? 32 + :else 82) + :height (if reviewing-audio? 32 56) + :x (cond + active? -16 + reviewing-audio? 36 + :else -32) + :y (cond + active? 64 + reviewing-audio? 76 + :else 70)}) + +(defn lock-button-area + [{:keys [active?]}] + {:width (if active? 72 100) + :height (if active? 72 102) + :x -32 + :y -32}) + +(defn send-button-area + [{:keys [active? reviewing-audio?]}] + {:width (if reviewing-audio? 32 56) + :height (cond + active? 72 + reviewing-audio? 47 + :else 92) + :x (if reviewing-audio? 76 32) + :y (cond + active? -16 + reviewing-audio? 76 + :else -32)}) + +(defn touch-inside-area? + [{:keys [location-x location-y ignore-min-y? ignore-max-y? ignore-min-x? ignore-max-x?]} + {:keys [width height x y]}] + (let [max-x (+ x width) + max-y (+ y height)] + (and + (and + (or ignore-min-x? (>= location-x x)) + (or ignore-max-x? (<= location-x max-x))) + (and + (or ignore-min-y? (>= location-y y)) + (or ignore-max-y? (<= location-y max-y)))))) + +(def rec-options + (merge + audio/default-recorder-options + {:filename (str base-filename + (datetime/timestamp) + default-format) + :meteringInterval metering-interval})) diff --git a/src/quo/components/record_audio/record_audio/handlers.cljs b/src/quo/components/record_audio/record_audio/handlers.cljs new file mode 100644 index 00000000000..26b0887dddf --- /dev/null +++ b/src/quo/components/record_audio/record_audio/handlers.cljs @@ -0,0 +1,345 @@ +(ns quo.components.record-audio.record-audio.handlers + (:require [oops.core :as oops] + [quo.components.record-audio.record-audio.constants :as record-audio.constants] + [react-native.audio-toolkit :as audio] + [taoensso.timbre :as log] + [utils.datetime :as datetime])) + +(def get-reload-player + (memoize + (fn + [[player-ref destroy-player set-playing-audio playing-timer set-audio-current-time-ms + set-seeking-audio]] + (fn [audio-file] + (when @player-ref + (destroy-player)) + (reset! player-ref + (audio/new-player + (or audio-file (:filename record-audio.constants/rec-options)) + {:autoDestroy false + :continuesToPlayInBackground false + :category audio/PLAYBACK} + (fn [] + (set-playing-audio false) + (when @playing-timer + (js/clearInterval @playing-timer) + (reset! playing-timer nil) + (set-audio-current-time-ms 0) + (set-seeking-audio false))))) + (audio/prepare-player + @player-ref + #(log/debug "[record-audio] prepare player - success") + #(log/error "[record-audio] prepare player - error: " %)))))) + +(def get-recorder-on-meter + (memoize + (fn + [[recording-start-ms set-recording-length-ms reached-max-duration? locked? set-locked + set-reviewing-audio + idle? set-recording + set-ready-to-lock set-ready-to-send set-ready-to-delete recorder-ref output-file reload-player + on-reviewing-audio]] + (fn [] + (when @recording-start-ms + (let [now-ms (datetime/timestamp) + recording-duration (- now-ms @recording-start-ms)] + (set-recording-length-ms recording-duration) + (when (>= recording-duration record-audio.constants/max-audio-duration-ms) + (reset! reached-max-duration? (not locked?)) + (set-reviewing-audio true) + (reset! idle? false) + (set-locked false) + (set-recording false) + (set-ready-to-lock false) + (set-ready-to-send false) + (set-ready-to-delete false) + (audio/stop-recording + @recorder-ref + (fn [] + (reset! output-file (audio/get-recorder-file-path @recorder-ref)) + (reload-player nil) + (log/debug "[record-audio] stop recording - success")) + #(log/error "[record-audio] stop recording - error: " %)) + (js/setTimeout #(reset! idle? false) 1000) + (set-recording-length-ms 0) + (reset! recording-start-ms nil) + (when on-reviewing-audio + (on-reviewing-audio (audio/get-recorder-file-path @recorder-ref)))) + (log/debug "[record-audio] new recorder - on meter"))))))) + +(def get-reload-recorder + (memoize + (fn + [[recorder-ref recorder-on-meter]] + (fn [] + (when @recorder-ref + (audio/destroy-recorder @recorder-ref) + (reset! recorder-ref nil)) + (reset! recorder-ref (audio/new-recorder + record-audio.constants/rec-options + recorder-on-meter + #(log/debug "[record-audio] new recorder - on ended"))))))) + +(def get-on-start-should-set-responder + (memoize + (fn + [[locked? idle? disabled? recorder-on-meter touch-timestamp touch-identifier + reviewing-audio? record-audio-permission-granted set-recording set-playing-audio output-file + recorder-ref recording-start-ms set-audio-current-time-ms on-start-recording + on-request-record-audio-permission touch-active?]] + (fn [e] + (when-not (or locked? @idle? (nil? e) @disabled?) + (let [pressed-record-button? (record-audio.constants/touch-inside-area? + {:location-x (oops/oget e "nativeEvent.locationX") + :location-y (oops/oget e "nativeEvent.locationY") + :ignore-min-y? false + :ignore-max-y? false + :ignore-min-x? false + :ignore-max-x? false} + record-audio.constants/record-button-area) + new-recorder (audio/new-recorder + record-audio.constants/rec-options + recorder-on-meter + #(log/debug "[record-audio] new recorder - on ended"))] + (reset! touch-timestamp (oops/oget e "nativeEvent.timestamp")) + (reset! touch-identifier (oops/oget e "nativeEvent.identifier")) + (when-not reviewing-audio? + (if record-audio-permission-granted + (do + (when (not @idle?) + (set-recording pressed-record-button?)) + (when pressed-record-button? + (set-playing-audio false) + (reset! output-file nil) + (reset! recorder-ref new-recorder) + (audio/start-recording + new-recorder + (fn [] + (reset! recording-start-ms (datetime/timestamp)) + (set-audio-current-time-ms 0) + (log/debug "[record-audio] start recording - success")) + #(log/error "[record-audio] start recording - error: " %)) + (when on-start-recording + (on-start-recording)))) + (when on-request-record-audio-permission + (on-request-record-audio-permission)))) + (when record-audio-permission-granted + (reset! touch-active? true)))) + (and (not @idle?) (not @disabled?)))))) + +(def get-on-responder-move + (memoize + (fn + [[locked? ready-to-send? set-ready-to-send ready-to-delete? set-ready-to-delete ready-to-lock? + set-ready-to-lock touch-identifier + record-button-at-initial-position? recording?]] + (fn [^js e] + (when-not locked? + (let [location-x (oops/oget e "nativeEvent.locationX") + location-y (oops/oget e "nativeEvent.locationY") + page-x (oops/oget e "nativeEvent.pageX") + page-y (oops/oget e "nativeEvent.pageY") + identifier (oops/oget e "nativeEvent.identifier") + moved-to-send-button? (record-audio.constants/touch-inside-area? + {:location-x location-x + :location-y location-y + :ignore-min-y? true + :ignore-max-y? false + :ignore-min-x? false + :ignore-max-x? true} + (record-audio.constants/send-button-area + {:active? ready-to-send? + :reviewing-audio? false})) + moved-to-delete-button? (record-audio.constants/touch-inside-area? + {:location-x location-x + :location-y location-y + :ignore-min-y? false + :ignore-max-y? true + :ignore-min-x? true + :ignore-max-x? false} + (record-audio.constants/delete-button-area + {:active? ready-to-delete? + :reviewing-audio? false})) + moved-to-lock-button? (record-audio.constants/touch-inside-area? + {:location-x location-x + :location-y location-y + :ignore-min-y? false + :ignore-max-y? false + :ignore-min-x? false + :ignore-max-x? false} + (record-audio.constants/lock-button-area {:active? + ready-to-lock?})) + moved-to-record-button? (and + (record-audio.constants/touch-inside-area? + {:location-x location-x + :location-y location-y + :ignore-min-y? false + :ignore-max-y? false + :ignore-min-x? false + :ignore-max-x? false} + record-audio.constants/record-button-area-big) + (not= location-x page-x) + (not= location-y page-y))] + (when (= identifier @touch-identifier) + (cond + (and + (or + (and moved-to-record-button? ready-to-lock?) + (and (not locked?) moved-to-lock-button? @record-button-at-initial-position?)) + (not ready-to-delete?) + (not ready-to-send?) + recording?) + (set-ready-to-lock moved-to-lock-button?) + (and + (or + (and moved-to-record-button? ready-to-delete?) + (and moved-to-delete-button? @record-button-at-initial-position?)) + (not ready-to-lock?) + (not ready-to-send?) + recording?) + (set-ready-to-delete moved-to-delete-button?) + (and + (or + (and moved-to-record-button? ready-to-send?) + (and moved-to-send-button? @record-button-at-initial-position?)) + (not ready-to-lock?) + (not ready-to-delete?) + recording?) + (set-ready-to-send moved-to-send-button?))))))))) + +(def get-on-responder-release + (memoize + (fn + [[idle? reached-max-duration? touch-timestamp recording-length-ms set-recording-length-ms + reviewing-audio? set-reviewing-audio + set-audio-current-time-ms set-force-show-controls on-send output-file player-ref + destroy-player on-cancel record-button-is-animating? ready-to-lock? set-ready-to-lock + locked? set-locked on-lock ready-to-delete? set-ready-to-delete ready-to-send? set-ready-to-send + disabled? on-reviewing-audio recorder-ref set-recording reload-player recording-start-ms + touch-active?]] + (fn [^js e] + (when (and + (not @idle?) + (not @reached-max-duration?)) + (let [touch-area {:location-x (oops/oget e "nativeEvent.locationX") + :location-y (oops/oget e "nativeEvent.locationY") + :ignore-min-y? false + :ignore-max-y? false + :ignore-min-x? false + :ignore-max-x? false} + on-record-button? (record-audio.constants/touch-inside-area? + touch-area + record-audio.constants/record-button-area-big) + on-send-button? (record-audio.constants/touch-inside-area? + touch-area + (record-audio.constants/send-button-area + {:active? false + :reviewing-audio? true})) + on-delete-button? (record-audio.constants/touch-inside-area? + touch-area + (record-audio.constants/delete-button-area + {:active? false + :reviewing-audio? true})) + release-touch-timestamp (oops/oget e "nativeEvent.timestamp") + touch-timestamp-diff (- release-touch-timestamp @touch-timestamp) + audio-length recording-length-ms] + (cond + (and reviewing-audio? on-send-button?) + (do + (set-reviewing-audio false) + (set-audio-current-time-ms 0) + (set-force-show-controls false) + (when on-send + (on-send {:file-path @output-file + :duration (min record-audio.constants/max-audio-duration-ms + (int (audio/get-player-duration @player-ref)))})) + (when @player-ref + (audio/stop-playing + @player-ref + (fn [] + (destroy-player) + (log/debug "[record-audio] stop playing - success")) + #(log/error "[record-audio] stop playing - error: " %)))) + + (and reviewing-audio? on-delete-button?) + (do + (set-reviewing-audio false) + (set-audio-current-time-ms 0) + (set-force-show-controls false) + (destroy-player) + (when on-cancel + (on-cancel))) + + (and ready-to-lock? (not @record-button-is-animating?)) + (do + (set-locked true) + (set-ready-to-lock false) + (when on-lock + (on-lock))) + + (and (not reviewing-audio?) + (or on-record-button? + (and (not ready-to-delete?) + (not ready-to-lock?) + (not ready-to-send?)))) + (do + (reset! disabled? (<= touch-timestamp-diff record-audio.constants/min-touch-duration)) + (js/setTimeout + (fn [] + (if (>= recording-length-ms record-audio.constants/min-audio-duration-ms) + (do (set-reviewing-audio true) + (reset! idle? false) + (when on-reviewing-audio + (on-reviewing-audio (audio/get-recorder-file-path @recorder-ref)))) + (do (when on-cancel + (on-cancel)) + (reset! idle? true))) + (set-locked false) + (set-recording false) + (set-ready-to-lock false) + (audio/stop-recording + @recorder-ref + (fn [] + (reset! output-file (audio/get-recorder-file-path @recorder-ref)) + (when (>= audio-length record-audio.constants/min-audio-duration-ms) + (reload-player nil)) + (log/debug "[record-audio] stop recording - success")) + #(log/error "[record-audio] stop recording - error: " %)) + (js/setTimeout #(reset! idle? false) 1000) + (set-recording-length-ms 0) + (reset! recording-start-ms nil) + (reset! disabled? false)) + (if (> touch-timestamp-diff record-audio.constants/min-touch-duration) 0 250))) + + (and (not locked?) (not reviewing-audio?) (not @record-button-is-animating?)) + (do + (reset! disabled? (<= touch-timestamp-diff record-audio.constants/min-touch-duration)) + (js/setTimeout + (fn [] + (audio/stop-recording + @recorder-ref + (fn [] + (cond + ready-to-send? + (when on-send + (on-send {:file-path (audio/get-recorder-file-path @recorder-ref) + :duration recording-length-ms})) + ready-to-delete? + (when on-cancel + (on-cancel))) + (set-recording false) + (set-ready-to-send false) + (set-ready-to-delete false) + (set-ready-to-lock false) + (reset! idle? true) + (js/setTimeout #(reset! idle? false) 1000) + (set-recording-length-ms 0) + (reset! recording-start-ms nil) + (reset! disabled? false) + (log/debug "[record-audio] stop recording - success")) + #(log/error "[record-audio] stop recording - error: " %))) + (if (> touch-timestamp-diff record-audio.constants/min-touch-duration) 0 250))))) + (reset! touch-active? false)) + (when @reached-max-duration? + (reset! reached-max-duration? false)) + (reset! touch-timestamp nil))))) diff --git a/src/quo/components/record_audio/record_audio/helpers.cljs b/src/quo/components/record_audio/record_audio/helpers.cljs new file mode 100644 index 00000000000..7d58672d94c --- /dev/null +++ b/src/quo/components/record_audio/record_audio/helpers.cljs @@ -0,0 +1,51 @@ +(ns quo.components.record-audio.record-audio.helpers + (:require + [react-native.reanimated :as reanimated])) + +(defn animate-linear + [shared-value value duration] + (reanimated/animate-shared-value-with-timing + shared-value + value + duration + :linear)) + +(defn animate-linear-with-delay + [shared-value value duration delay-ms] + (reanimated/animate-shared-value-with-delay + shared-value + value + duration + :linear + delay-ms)) + +(defn animate-linear-with-delay-loop + [shared-value value duration delay-ms] + (reanimated/animate-shared-value-with-delay-repeat + shared-value + value + duration + :linear + delay-ms + -1)) + +(defn animate-easing + [shared-value value duration] + (reanimated/animate-shared-value-with-timing + shared-value + value + duration + :easing1)) + +(defn animate-easing-with-delay + [shared-value value duration delay-ms] + (reanimated/animate-shared-value-with-delay + shared-value + value + duration + :easing1 + delay-ms)) + +(defn set-value + [shared-value value] + (reanimated/set-shared-value shared-value value)) diff --git a/src/quo/components/record_audio/record_audio/style.cljs b/src/quo/components/record_audio/record_audio/style.cljs new file mode 100644 index 00000000000..b7687d702f8 --- /dev/null +++ b/src/quo/components/record_audio/record_audio/style.cljs @@ -0,0 +1,288 @@ +(ns quo.components.record-audio.record-audio.style + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(defn animated-circle + [scale opacity color] + (reanimated/apply-animations-to-style + {:transform [{:scale scale}] + :opacity opacity} + {:width 56 + :height 56 + :border-width 1 + :border-color color + :border-radius 28 + :position :absolute + :justify-content :center + :align-items :center + :z-index 0})) + +(defn record-button-big-container + [translate-x translate-y opacity] + (reanimated/apply-animations-to-style + {:transform [{:translateY translate-y} + {:translateX translate-x}] + :opacity opacity} + {:position :absolute + :bottom 0 + :right 0 + :width 96 + :height 96 + :align-items :center + :justify-content :center + :z-index 0})) + +(defn record-button-big-body + [button-color] + {:width 56 + :height 56 + :border-radius 28 + :justify-content :center + :align-items :center + :background-color button-color + :overflow :hidden}) + +(defn record-button-big-red-overlay + [red-overlay-opacity] + (reanimated/apply-animations-to-style + {:opacity red-overlay-opacity} + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :background-color colors/danger-50})) + +(defn record-button-big-gray-overlay + [gray-overlay-opacity theme] + (reanimated/apply-animations-to-style + {:opacity gray-overlay-opacity} + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :background-color (colors/theme-colors colors/neutral-80-opa-5-opaque colors/neutral-80 theme)})) + +(defn record-button-big-icon-container + [icon-opacity] + (reanimated/apply-animations-to-style + {:opacity icon-opacity} + {})) + +(def stop-icon + {:width 13 + :height 13 + :border-radius 4 + :background-color colors/white}) + +(defn send-button-container + [opacity] + (reanimated/apply-animations-to-style + {:opacity opacity} + {:justify-content :center + :align-items :center + :position :absolute + :width 56 + :height 56 + :top 0 + :right 20})) + +(defn send-button-connector + [opacity width height border-radius-first-half border-radius-second-half] + (reanimated/apply-animations-to-style + {:opacity opacity + :width width + :height height + :border-bottom-left-radius border-radius-second-half + :border-top-left-radius border-radius-first-half + :border-top-right-radius border-radius-first-half + :border-bottom-right-radius border-radius-second-half} + {:justify-content :center + :align-items :center + :align-self :center + :background-color colors/primary-50 + :z-index 0})) + +(defn send-button + [translate-y opacity] + (reanimated/apply-animations-to-style + {:transform [{:translateY translate-y}] + :opacity opacity} + {:justify-content :center + :align-items :center + :background-color colors/primary-50 + :width 32 + :height 32 + :border-radius 16 + :position :absolute + :top 0 + :right 32 + :z-index 10})) + +(def send-icon-container + {:z-index 10}) + +(defn lock-button-container + [opacity] + (reanimated/apply-animations-to-style + {:opacity opacity} + {:transform [{:rotate "45deg"}] + :justify-content :center + :align-items :center + :position :absolute + :width 56 + :height 56 + :top 20 + :left 20})) + +(defn lock-button-connector + [opacity width height border-radius-first-half border-radius-second-half theme] + (reanimated/apply-animations-to-style + {:opacity opacity + :width width + :height height + :border-bottom-left-radius border-radius-first-half + :border-top-left-radius border-radius-first-half + :border-top-right-radius border-radius-second-half + :border-bottom-right-radius border-radius-second-half} + {:justify-content :center + :align-items :center + :align-self :center + :background-color (colors/theme-colors colors/neutral-80-opa-5-opaque colors/neutral-80 theme) + :overflow :hidden})) + +(defn lock-button + [translate-x-y opacity theme] + (reanimated/apply-animations-to-style + {:transform [{:translateX translate-x-y} + {:translateY translate-x-y}] + :opacity opacity} + {:width 32 + :height 32 + :justify-content :center + :align-items :center + :background-color (colors/theme-colors colors/neutral-80-opa-5-opaque colors/neutral-80 theme) + :border-radius 16 + :position :absolute + :top 24 + :left 24 + :overflow :hidden + :z-index 12})) + +(defn delete-button-container + [opacity] + (reanimated/apply-animations-to-style + {:opacity opacity} + {:justify-content :center + :align-items :center + :position :absolute + :width 56 + :height 56 + :bottom 20 + :left 0})) + +(defn delete-button-connector + [opacity width height border-radius-first-half border-radius-second-half] + (reanimated/apply-animations-to-style + {:opacity opacity + :width width + :height height + :border-bottom-left-radius border-radius-first-half + :border-top-left-radius border-radius-first-half + :border-top-right-radius border-radius-second-half + :border-bottom-right-radius border-radius-second-half} + {:justify-content :center + :align-items :center + :align-self :center + :background-color colors/danger-50 + :z-index 0})) + +(defn delete-button + [scale translate-x opacity] + (reanimated/apply-animations-to-style + {:transform [{:translateX translate-x} + {:scale scale}] + :opacity opacity} + {:width 32 + :height 32 + :justify-content :center + :align-items :center + :background-color colors/danger-50 + :border-radius 16 + :position :absolute + :top 76 + :left 0 + :z-index 11})) + +(defn record-button-container + [opacity] + (reanimated/apply-animations-to-style + {:opacity opacity} + {:margin-bottom 32 + :margin-right 32})) + +(def button-container + {:width 140 + :height 140 + :align-items :flex-end + :justify-content :flex-end + :position :absolute + :right -10}) + +(def bar-container + {:flex 1 + :height 128}) + +(defn recording-bar-container + [theme] + {:height 4 + :border-radius 2 + :background-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :overflow :hidden + :position :absolute + :left 80 + :right 148 + :bottom 34}) + +(defn recording-bar + [fill-percentage ready-to-delete? theme] + {:width (str fill-percentage "%") + :height 4 + :border-radius 2 + :background-color (if ready-to-delete? + (colors/theme-colors colors/danger-50 colors/danger-60 theme) + (colors/theme-colors colors/primary-50 colors/primary-60 theme))}) + +(defn timer-container + [reviewing-audio?] + {:position :absolute + :left (if reviewing-audio? 67 20) + :bottom 28.5 + :flex-direction :row + :align-items :center}) + +(defn timer-circle + [theme] + {:width 8 + :height 8 + :border-radius 4 + :margin-right 6 + :background-color (colors/theme-colors colors/danger-50 colors/danger-60 theme)}) + +(defn timer-text + [theme] + {:color (colors/theme-colors colors/danger-50 colors/danger-60 theme)}) + +(defn play-button + [theme] + {:position :absolute + :bottom 20 + :left 20 + :width 32 + :height 32 + :border-radius 16 + :align-items :center + :justify-content :center + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)}) diff --git a/src/quo/components/record_audio/record_audio/view.cljs b/src/quo/components/record_audio/record_audio/view.cljs new file mode 100644 index 00000000000..da4863cb816 --- /dev/null +++ b/src/quo/components/record_audio/record_audio/view.cljs @@ -0,0 +1,274 @@ +(ns quo.components.record-audio.record-audio.view + (:require + [clojure.string :as string] + [goog.string :as gstring] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.record-audio.record-audio.buttons.delete-button :as delete-button] + [quo.components.record-audio.record-audio.buttons.lock-button :as lock-button] + [quo.components.record-audio.record-audio.buttons.record-button :as record-button] + [quo.components.record-audio.record-audio.buttons.record-button-big :as record-button-big] + [quo.components.record-audio.record-audio.buttons.send-button :as send-button] + [quo.components.record-audio.record-audio.constants :as record-audio.constants] + [quo.components.record-audio.record-audio.handlers :as handlers] + [quo.components.record-audio.record-audio.style :as style] + [quo.components.record-audio.soundtrack.view :as soundtrack] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.audio-toolkit :as audio] + [react-native.core :as rn] + [taoensso.timbre :as log] + [utils.datetime :as datetime])) + +(defn- recording-bar + [recording-length-ms ready-to-delete?] + (let [theme (quo.context/use-theme) + fill-percentage (/ (* recording-length-ms 100) record-audio.constants/max-audio-duration-ms)] + [rn/view {:style (style/recording-bar-container theme)} + [rn/view {:style (style/recording-bar fill-percentage ready-to-delete? theme)}]])) + +(defn- time-counter + [recording? recording-length-ms ready-to-delete? reviewing-audio? audio-current-time-ms] + (let [theme (quo.context/use-theme) + s (quot (if recording? recording-length-ms audio-current-time-ms) 1000) + time-str (gstring/format "%02d:%02d" (quot s 60) (mod s 60))] + [rn/view {:style (style/timer-container reviewing-audio?)} + (when-not reviewing-audio? + [rn/view {:style (style/timer-circle theme)}]) + [text/text + (merge + {:size :label + :weight :semi-bold} + (when ready-to-delete? + {:style (style/timer-text theme)})) + time-str]])) + +(defn- play-button + [playing-audio? set-playing-audio player-ref playing-timer set-audio-current-time-ms seeking-audio? + set-seeking-audio + max-duration-ms] + (let [theme (quo.context/use-theme) + on-play (fn [] + (set-playing-audio true) + (reset! playing-timer + (js/setInterval + (fn [] + (let [current-time (audio/get-player-current-time @player-ref) + player-state (audio/get-state @player-ref) + playing? (= player-state audio/PLAYING)] + (when (and playing? + (not seeking-audio?) + (> current-time 0) + (< current-time max-duration-ms)) + (set-audio-current-time-ms current-time)) + (when (>= current-time max-duration-ms) + (audio/stop-playing + @player-ref + (fn [] + (set-playing-audio false) + (when @playing-timer + (js/clearInterval @playing-timer) + (reset! playing-timer nil) + (set-audio-current-time-ms 0) + (set-seeking-audio false))) + #(log/error "[record-audio] stop player - error: " %))))) + 100))) + on-pause (fn [] + (set-playing-audio false) + (when @playing-timer + (js/clearInterval @playing-timer) + (reset! playing-timer nil)) + (log/debug "[record-audio] toggle play / pause - success")) + on-press (fn [] + (audio/toggle-playpause-player + @player-ref + on-play + on-pause + #(log/error "[record-audio] toggle play / pause - error: " %)))] + [rn/touchable-opacity + {:style (style/play-button theme) + :on-press on-press} + [icons/icon + (if playing-audio? :i/pause :i/play) + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}]])) + +(defn record-audio + [{:keys [on-init on-start-recording on-send on-cancel on-reviewing-audio audio-file on-lock + max-duration-ms on-check-audio-permissions record-audio-permission-granted + on-request-record-audio-permission]}] + (let [;;STATE + [recording? set-recording] (rn/use-state false) + [locked? set-locked] (rn/use-state false) + [ready-to-send? set-ready-to-send] (rn/use-state false) + [ready-to-lock? set-ready-to-lock] (rn/use-state false) + [ready-to-delete? + set-ready-to-delete] (rn/use-state false) + [reviewing-audio? + set-reviewing-audio] (rn/use-state (some? audio-file)) + [playing-audio? + set-playing-audio] (rn/use-state false) + [recording-length-ms + set-recording-length-ms] (rn/use-state 0) + [audio-current-time-ms + set-audio-current-time-ms] (rn/use-state 0) + [seeking-audio? set-seeking-audio] (rn/use-state false) + [force-show-controls? + set-force-show-controls] (rn/use-state (some? audio-file)) + ;;ATOMS + recording-start-ms (rn/use-ref-atom (datetime/timestamp)) + clear-timeout (rn/use-ref-atom nil) + record-button-at-initial-position? (rn/use-ref-atom true) + record-button-is-animating? (rn/use-ref-atom false) + idle? (rn/use-ref-atom false) + recorder-ref (rn/use-ref-atom nil) + player-ref (rn/use-ref-atom nil) + touch-active? (rn/use-ref-atom false) + playing-timer (rn/use-ref-atom nil) + output-file (rn/use-ref-atom audio-file) + reached-max-duration? (rn/use-ref-atom false) + touch-timestamp (rn/use-ref-atom nil) + touch-identifier (rn/use-ref-atom nil) + disabled? (rn/use-ref-atom false) + app-state-listener (rn/use-ref-atom nil) + ;;HANDLERS + destroy-player (rn/use-callback + (fn [] + (audio/destroy-player @player-ref) + (reset! player-ref nil))) + reload-player (handlers/get-reload-player + [player-ref destroy-player set-playing-audio playing-timer + set-audio-current-time-ms set-seeking-audio]) + recorder-on-meter (handlers/get-recorder-on-meter + [recording-start-ms set-recording-length-ms + reached-max-duration? + locked? set-locked set-reviewing-audio idle? set-recording + set-ready-to-lock set-ready-to-send set-ready-to-delete + recorder-ref + output-file reload-player + on-reviewing-audio]) + reload-recorder (handlers/get-reload-recorder [recorder-ref + recorder-on-meter]) + on-start-should-set-responder (handlers/get-on-start-should-set-responder + [locked? idle? disabled? recorder-on-meter touch-timestamp + touch-identifier + reviewing-audio? record-audio-permission-granted + set-recording set-playing-audio output-file + recorder-ref recording-start-ms set-audio-current-time-ms + on-start-recording + on-request-record-audio-permission touch-active?]) + on-responder-move (handlers/get-on-responder-move + [locked? ready-to-send? set-ready-to-send + ready-to-delete? set-ready-to-delete + ready-to-lock? set-ready-to-lock + touch-identifier + record-button-at-initial-position? + recording?]) + on-responder-release (handlers/get-on-responder-release + [idle? reached-max-duration? touch-timestamp + recording-length-ms set-recording-length-ms + reviewing-audio? set-reviewing-audio + set-audio-current-time-ms set-force-show-controls on-send + output-file player-ref + destroy-player on-cancel record-button-is-animating? + ready-to-lock? set-ready-to-lock + locked? set-locked on-lock ready-to-delete? + set-ready-to-delete ready-to-send? set-ready-to-send + disabled? on-reviewing-audio + recorder-ref set-recording reload-player recording-start-ms + touch-active?])] + + ;;ON MOUNT + (rn/use-mount + (fn [] + (when on-check-audio-permissions (on-check-audio-permissions)) + (when on-init + (on-init + (fn reset-recorder [] + (set-recording false) + (set-reviewing-audio false) + (set-locked false) + (set-ready-to-send false) + (set-ready-to-lock false) + (set-ready-to-delete false) + (set-audio-current-time-ms 0) + (set-recording-length-ms 0) + (set-seeking-audio false) + (set-playing-audio false) + (reset! touch-active? false) + (reset! reached-max-duration? false) + (reset! output-file nil) + (reset! idle? false) + (reset! record-button-is-animating? false) + (reset! record-button-at-initial-position? true) + (when @clear-timeout + (js/clearTimeout @clear-timeout) + (reset! clear-timeout nil)) + (when @playing-timer + (js/clearInterval @playing-timer) + (reset! playing-timer nil)) + (reload-recorder)))) + (when audio-file + (let [filename (last (string/split audio-file "/"))] + (reload-player filename))) + (reset! app-state-listener + (.addEventListener rn/app-state + "change" + #(when (= % "background") + (set-playing-audio false)))))) + ;;ON UNMOUNT + (rn/use-unmount #(.remove @app-state-listener)) + + [rn/view + {:style style/bar-container + :pointer-events :box-none} + (when reviewing-audio? + [:<> + [play-button playing-audio? set-playing-audio player-ref playing-timer set-audio-current-time-ms + seeking-audio? set-seeking-audio max-duration-ms] + [soundtrack/soundtrack + {:audio-current-time-ms audio-current-time-ms + :set-audio-current-time-ms set-audio-current-time-ms + :player-ref @player-ref + :seeking-audio? seeking-audio? + :set-seeking-audio set-seeking-audio + :max-audio-duration-ms max-duration-ms}]]) + (when (or recording? reviewing-audio?) + [time-counter recording? recording-length-ms ready-to-delete? reviewing-audio? + audio-current-time-ms]) + (when recording? + [recording-bar recording-length-ms ready-to-delete?]) + [rn/view + {:test-ID "record-audio" + :style style/button-container + :hit-slop {:top -70 :bottom 0 :left 0 :right 0} + :pointer-events :box-only + :on-start-should-set-responder on-start-should-set-responder + :on-responder-move on-responder-move + :on-responder-release on-responder-release} + [delete-button/delete-button recording? ready-to-delete? reviewing-audio? force-show-controls?] + [lock-button/lock-button recording? ready-to-lock? locked?] + [send-button/send-button recording? ready-to-send? reviewing-audio? force-show-controls?] + [record-button-big/record-button-big + {:recording? recording? + :set-recording set-recording + :ready-to-send? ready-to-send? + :set-ready-to-send set-ready-to-send + :ready-to-lock? ready-to-lock? + :set-ready-to-lock set-ready-to-lock + :ready-to-delete? ready-to-delete? + :set-ready-to-delete set-ready-to-delete + :record-button-is-animating? record-button-is-animating? + :record-button-at-initial-position? record-button-at-initial-position? + :locked? locked? + :set-locked set-locked + :reviewing-audio? reviewing-audio? + :recording-length-ms recording-length-ms + :set-recording-length-ms set-recording-length-ms + :clear-timeout clear-timeout + :touch-active? touch-active? + :recorder-ref recorder-ref + :reload-recorder-fn reload-recorder + :idle? idle? + :on-send on-send + :on-cancel on-cancel}] + [record-button/record-button recording? reviewing-audio?]]])) diff --git a/src/quo/components/record_audio/soundtrack/component_spec.cljs b/src/quo/components/record_audio/soundtrack/component_spec.cljs new file mode 100644 index 00000000000..dd851a828ac --- /dev/null +++ b/src/quo/components/record_audio/soundtrack/component_spec.cljs @@ -0,0 +1,74 @@ +(ns quo.components.record-audio.soundtrack.component-spec + (:require + [quo.components.record-audio.soundtrack.view :as soundtrack] + [react-native.audio-toolkit :as audio] + [test-helpers.component :as h])) + +(h/describe "soundtrack component" + (h/setup-fake-timers) + + (h/test "renders soundtrack" + (with-redefs [audio/get-player-duration (fn [] 2000)] + (h/render [soundtrack/soundtrack + {:player-ref {} + :audio-current-time-ms 0}]) + (-> (h/expect (h/get-by-test-id "soundtrack")) + (.toBeTruthy)))) + + (h/test "soundtrack on-sliding-start works" + (with-redefs [audio/get-player-duration (fn [] 2000)] + (let [seeking-audio? (atom false)] + (h/render [soundtrack/soundtrack + {:seeking-audio? seeking-audio? + :set-seeking-audio #(reset! seeking-audio? %) + :player-ref {} + :audio-current-time-ms 0 + :set-audio-current-time-ms #()}]) + (h/fire-event + :on-sliding-start + (h/get-by-test-id "soundtrack")) + (-> (h/expect @seeking-audio?) + (.toBe true))))) + + (h/test "soundtrack on-sliding-complete works" + (with-redefs [audio/get-player-duration (fn [] 2000) + audio/seek-player (js/jest.fn)] + (let [seeking-audio? (atom false)] + (h/render [soundtrack/soundtrack + {:seeking-audio? seeking-audio? + :set-seeking-audio #(reset! seeking-audio? %) + :player-ref {} + :audio-current-time-ms 0 + :set-audio-current-time-ms #()}]) + (h/fire-event + :on-sliding-start + (h/get-by-test-id "soundtrack")) + (h/fire-event + :on-sliding-complete + (h/get-by-test-id "soundtrack") + 1000) + (-> (h/expect @seeking-audio?) + (.toBe false)) + (-> (h/expect audio/seek-player) + (.toHaveBeenCalledTimes 1))))) + + (h/test "soundtrack on-value-change when seeking audio works" + (with-redefs [audio/get-player-duration (fn [] 2000) + audio/seek-player (js/jest.fn)] + (let [seeking-audio? (atom false) + audio-current-time-ms (atom 0)] + (h/render [soundtrack/soundtrack + {:seeking-audio? seeking-audio? + :set-seeking-audio #(reset! seeking-audio? %) + :player-ref {} + :audio-current-time-ms audio-current-time-ms + :set-audio-current-time-ms #(reset! audio-current-time-ms %)}]) + (h/fire-event + :on-sliding-start + (h/get-by-test-id "soundtrack")) + (h/fire-event + :on-value-change + (h/get-by-test-id "soundtrack") + 1000) + (-> (h/expect @audio-current-time-ms) + (.toBe 1000)))))) diff --git a/src/quo/components/record_audio/soundtrack/style.cljs b/src/quo/components/record_audio/soundtrack/style.cljs new file mode 100644 index 00000000000..65b740a604e --- /dev/null +++ b/src/quo/components/record_audio/soundtrack/style.cljs @@ -0,0 +1,17 @@ +(ns quo.components.record-audio.soundtrack.style + (:require + [react-native.platform :as platform])) + +(defn player-slider-container + [] + (merge + {:position :absolute + :left (if platform/ios? 115 104) + :right (if platform/ios? 108 92) + :bottom (if platform/ios? 16 27)} + (when platform/android? + ;; Workaround to increase the thickness of the slider track on Android + ;; which is currently not supported by the Slider library and remove + ;; the thumb shadow that appears when dragging. + {:transform [{:scaleY 2}] + :background-color :transparent}))) diff --git a/src/quo/components/record_audio/soundtrack/view.cljs b/src/quo/components/record_audio/soundtrack/view.cljs new file mode 100644 index 00000000000..3d8a99b6acd --- /dev/null +++ b/src/quo/components/record_audio/soundtrack/view.cljs @@ -0,0 +1,47 @@ +(ns quo.components.record-audio.soundtrack.view + (:require + [quo.components.icons.icons :as icons] + [quo.components.record-audio.soundtrack.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.audio-toolkit :as audio] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.slider :as slider] + [taoensso.timbre :as log])) + +(def ^:private thumb-light (icons/icon-source :thumb-light12)) +(def ^:private thumb-dark (icons/icon-source :thumb-dark12)) + +(defn soundtrack + [{:keys [audio-current-time-ms set-audio-current-time-ms player-ref style + seeking-audio? set-seeking-audio max-audio-duration-ms]}] + (let [audio-duration-ms (min max-audio-duration-ms (audio/get-player-duration player-ref)) + theme (quo.context/use-theme) + on-sliding-start (rn/use-callback #(set-seeking-audio true)) + on-sliding-complete (rn/use-callback + (fn [seek-time] + (set-seeking-audio false) + (audio/seek-player + player-ref + seek-time + #(log/debug "[record-audio] on seek - seek time: " seek-time) + #(log/error "[record-audio] on seek - error: " %))) + [player-ref]) + on-value-change (rn/use-callback #(when seeking-audio? (set-audio-current-time-ms %)) + [seeking-audio?])] + [slider/slider + {:test-ID "soundtrack" + :style (merge (style/player-slider-container) style) + :minimum-value 0 + :maximum-value audio-duration-ms + :value audio-current-time-ms + :on-sliding-start on-sliding-start + :on-sliding-complete on-sliding-complete + :on-value-change on-value-change + :thumb-image (if (= theme :light) thumb-light thumb-dark) + :minimum-track-tint-color (colors/theme-colors colors/primary-50 colors/primary-60 theme) + :maximum-track-tint-color (colors/theme-colors + (if platform/ios? colors/neutral-20 colors/neutral-40) + (if platform/ios? colors/neutral-80 colors/neutral-60) + theme)}])) diff --git a/src/quo/components/selectors/disclaimer/component_spec.cljs b/src/quo/components/selectors/disclaimer/component_spec.cljs new file mode 100644 index 00000000000..c5c90892bb1 --- /dev/null +++ b/src/quo/components/selectors/disclaimer/component_spec.cljs @@ -0,0 +1,36 @@ +(ns quo.components.selectors.disclaimer.component-spec + (:require + [quo.components.selectors.disclaimer.view :as disclaimer] + [test-helpers.component :as h])) + +(h/describe "Disclaimer tests" + (h/test "Default render of toggle component" + (h/render [disclaimer/view {:on-change (h/mock-fn)} "test"]) + (h/is-truthy (h/get-by-label-text :checkbox-off))) + + (h/test "Renders its text" + (let [text "I accept this disclaimer"] + (h/render [disclaimer/view {} text]) + (h/is-truthy (h/get-by-text text)))) + + (h/test "On change event gets fire after press" + (let [mock-fn (h/mock-fn)] + (h/render [disclaimer/view {:on-change mock-fn} "test"]) + (h/fire-event :press (h/get-by-label-text :checkbox-off)) + (h/was-called mock-fn))) + + (h/describe "It's rendered according to its `checked?` property" + (h/test "checked? true" + (h/render [disclaimer/view {:checked? true} "test"]) + (h/is-null (h/query-by-label-text :checkbox-off)) + (h/is-truthy (h/query-by-label-text :checkbox-on))) + (h/test "checked? false" + (h/render [disclaimer/view {:checked? false} "test"]) + (h/is-null (h/query-by-label-text :checkbox-on)) + (h/is-truthy (h/query-by-label-text :checkbox-off))))) + +(h/test "Renders icon" + (let [text "I accept this disclaimer"] + (h/render [disclaimer/view {:icon :i/locked} text]) + (h/is-truthy (h/query-by-label-text :disclaimer-icon)))) + diff --git a/src/quo/components/selectors/disclaimer/style.cljs b/src/quo/components/selectors/disclaimer/style.cljs new file mode 100644 index 00000000000..669e8639e24 --- /dev/null +++ b/src/quo/components/selectors/disclaimer/style.cljs @@ -0,0 +1,23 @@ +(ns quo.components.selectors.disclaimer.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [blur? theme] + (let [dark-background (if blur? colors/white-opa-5 colors/neutral-80-opa-40) + dark-border (if blur? colors/white-opa-10 colors/neutral-70)] + {:flex-direction :row + :background-color (colors/theme-colors colors/neutral-5 dark-background theme) + :padding 11 + :align-self :stretch + :border-radius 12 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-20 dark-border theme)})) + +(def text + {:flex 1 + :margin-left 8}) + +(def icon-container + {:align-items :flex-end + :flex 1}) diff --git a/src/quo/components/selectors/disclaimer/view.cljs b/src/quo/components/selectors/disclaimer/view.cljs new file mode 100644 index 00000000000..108732e062f --- /dev/null +++ b/src/quo/components/selectors/disclaimer/view.cljs @@ -0,0 +1,38 @@ +(ns quo.components.selectors.disclaimer.view + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.selectors.disclaimer.style :as style] + [quo.components.selectors.selectors.view :as selectors] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn view + [{:keys [checked? blur? accessibility-label container-style on-change icon customization-color]} label] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:on-press (when on-change + #(on-change (not checked?))) + :accessibility-label :disclaimer-touchable-opacity + :style (merge container-style (style/container blur? theme))} + [selectors/view + {:type :checkbox + :accessibility-label accessibility-label + :blur? blur? + :checked? checked? + :on-change on-change + :customization-color customization-color}] + [text/text + {:size :paragraph-2 + :style style/text} + label] + (when icon + [rn/view {:style style/icon-container} + [icons/icon icon + {:accessibility-label :disclaimer-icon + :color (if blur? + (colors/white-opa-70) + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme))}]])])) diff --git a/src/quo/components/selectors/filter/component_spec.cljs b/src/quo/components/selectors/filter/component_spec.cljs new file mode 100644 index 00000000000..88babcdb465 --- /dev/null +++ b/src/quo/components/selectors/filter/component_spec.cljs @@ -0,0 +1,17 @@ +(ns quo.components.selectors.filter.component-spec + (:require + [quo.components.selectors.filter.view :as quo] + [test-helpers.component :as h])) + +(h/describe "selector filter component" + (h/test "renders component" + (h/render [quo/view]) + (-> (js/expect (h/get-by-label-text :selector-filter)) + (.toBeTruthy))) + + (h/test "calls custom event handler when on-press-out is triggered" + (let [on-press-out-mock (js/jest.fn)] + (h/render [quo/view {:on-press-out on-press-out-mock}]) + (h/fire-event :press-out (h/get-by-label-text :selector-filter)) + (-> (js/expect on-press-out-mock) + (.toHaveBeenCalledTimes 1))))) diff --git a/src/quo/components/selectors/filter/style.cljs b/src/quo/components/selectors/filter/style.cljs new file mode 100644 index 00000000000..27fb26a2a4b --- /dev/null +++ b/src/quo/components/selectors/filter/style.cljs @@ -0,0 +1,61 @@ +(ns quo.components.selectors.filter.style + (:require + [quo.foundations.colors :as colors])) + +(def container-default + {:width 32 + :height 32 + :border-radius 10 + :align-items :center + :justify-content :center + :padding 6}) + +(defn container-border-color + [pressed? blur? theme] + (let [dark? (= :dark theme)] + (cond + (and (not pressed?) (not dark?) (not blur?)) + colors/neutral-20 + + (and (not pressed?) dark? (not blur?)) + colors/neutral-80 + + (and pressed? (not dark?) blur?) + colors/neutral-80-opa-20 + + (or (and pressed? (not dark?) (not blur?)) + (and (not pressed?) (not dark?) blur?)) + colors/neutral-80-opa-10 + + (or (and pressed? dark? (not blur?)) + (and (not pressed?) dark? blur?) + (and pressed? dark? blur?)) + colors/white-opa-10 + + :else + nil))) + +(defn container-background-color + [customization-color pressed? theme] + (when pressed? + (if customization-color + (colors/resolve-color (or customization-color :blue) nil) + (colors/theme-colors colors/primary-50 colors/primary-60 theme)))) + +(defn container-outer + [customization-color pressed? theme] + (merge container-default + {:background-color (container-background-color customization-color pressed? theme)})) + +(defn container-inner + [pressed? blur? theme] + (merge container-default + {:border-width 1 + :border-color (container-border-color pressed? blur? theme)})) + +(defn icon-color + [pressed? theme] + (if (and (not pressed?) + (= :light theme)) + colors/neutral-100 + colors/white)) diff --git a/src/quo/components/selectors/filter/view.cljs b/src/quo/components/selectors/filter/view.cljs new file mode 100644 index 00000000000..99a6f571915 --- /dev/null +++ b/src/quo/components/selectors/filter/view.cljs @@ -0,0 +1,22 @@ +(ns quo.components.selectors.filter.view + (:require + [quo.components.icon :as icon] + [quo.components.selectors.filter.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [blur? customization-color on-press-out pressed?]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state pressed?) + on-press-out (fn [] + (set-pressed (not pressed?)) + (when on-press-out (on-press-out pressed?)))] + [rn/pressable + {:accessibility-label :selector-filter + :on-press-out on-press-out + :style (style/container-outer customization-color pressed? theme)} + [rn/view {:style (style/container-inner pressed? blur? theme)} + [icon/icon :i/unread + {:color (style/icon-color pressed? theme) + :size 20}]]])) diff --git a/src/quo/components/selectors/react/style.cljs b/src/quo/components/selectors/react/style.cljs new file mode 100644 index 00000000000..11ce12c23b8 --- /dev/null +++ b/src/quo/components/selectors/react/style.cljs @@ -0,0 +1,12 @@ +(ns quo.components.selectors.react.style) + +(def container + {:flex-direction :row + :justify-content :flex-start + :flex 1 + :flex-wrap :wrap + :margin-bottom -6}) + +(def reaction-container + {:margin-bottom 6 + :margin-right 6}) diff --git a/src/quo/components/selectors/react/view.cljs b/src/quo/components/selectors/react/view.cljs new file mode 100644 index 00000000000..43a87b0654b --- /dev/null +++ b/src/quo/components/selectors/react/view.cljs @@ -0,0 +1,28 @@ +(ns quo.components.selectors.react.view + (:require + [quo.components.selectors.react-selector.view :as react-selector] + [quo.components.selectors.react.style :as style] + [react-native.core :as rn])) + +(defn view + [{:keys [reactions on-press on-long-press hide-new-reaction-button? on-press-add use-case + container-style]}] + [rn/view {:style (merge style/container container-style)} + (for [emoji-reaction reactions + :let [{:keys [emoji emoji-id emoji-reaction-id quantity own]} emoji-reaction]] + [react-selector/view + {:key emoji-reaction-id + :emoji emoji + :state (if own :pressed :not-pressed) + :use-case use-case + :container-style style/reaction-container + :clicks quantity + :on-press #(on-press emoji-reaction) + :on-long-press #(on-long-press emoji-reaction) + :accessibility-label (str "emoji-reaction-" emoji-id)}]) + (when-not hide-new-reaction-button? + [react-selector/view + {:on-press on-press-add + :state :add-reaction + :use-case use-case + :accessibility-label "emoji-add-reaction"}])]) diff --git a/src/quo/components/selectors/react_selector/style.cljs b/src/quo/components/selectors/react_selector/style.cljs new file mode 100644 index 00000000000..56f3c9ea6e5 --- /dev/null +++ b/src/quo/components/selectors/react_selector/style.cljs @@ -0,0 +1,45 @@ +(ns quo.components.selectors.react-selector.style + (:require + [quo.foundations.colors :as colors])) + +(defn add-reaction + [pinned? theme] + (let [border-color (if pinned? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/theme-colors colors/neutral-20 colors/neutral-70 theme))] + (cond-> {:justify-content :center + :align-items :center + :padding-horizontal 7 + :border-radius 8 + :border-width 1 + :height 24 + :border-color border-color}))) + +(defn reaction + [pressed? pinned? theme] + (let [background-color (cond + (not pressed?) :transparent + pinned? (colors/theme-colors colors/neutral-80-opa-5 + colors/white-opa-5 + theme) + :else (colors/theme-colors colors/neutral-10 + colors/neutral-80-opa-40 + theme)) + border-color (if pinned? + (colors/theme-colors colors/neutral-80-opa-5 + colors/white-opa-5 + theme) + (colors/theme-colors colors/neutral-20 + colors/neutral-80 + theme))] + {:flex-direction :row + :justify-content :center + :align-items :center + :padding-horizontal 8 + :border-radius 8 + :height 24 + :border-width 1 + :background-color background-color + :border-color border-color})) + +(def reaction-count {:margin-left 4}) diff --git a/src/quo/components/selectors/react_selector/view.cljs b/src/quo/components/selectors/react_selector/view.cljs new file mode 100644 index 00000000000..2a4078f8add --- /dev/null +++ b/src/quo/components/selectors/react_selector/view.cljs @@ -0,0 +1,44 @@ +(ns quo.components.selectors.react-selector.view + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.selectors.react-selector.style :as style] + [quo.components.selectors.reaction-resource :as reaction.resource] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn view + [{:keys [emoji clicks state use-case on-press accessibility-label on-long-press container-style]}] + (let [theme (quo.context/use-theme) + numeric-value (int clicks) + icon-color (if (= :pinned use-case) + (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))] + (if (= :add-reaction state) + [rn/touchable-opacity + {:on-press on-press + :accessibility-label :emoji-reaction-add + :style (style/add-reaction + (= :pinned use-case) + theme)} + [icons/icon :i/add-reaction + {:size 20 + :color icon-color}]] + + [rn/touchable-opacity + {:on-press on-press + :on-long-press on-long-press + :accessibility-label accessibility-label + :style (merge (style/reaction (= :pressed state) + (= :pinned use-case) + theme) + container-style)} + [rn/text + {:accessibility-label :emoji} + (reaction.resource/system-emojis emoji)] + [text/text + {:size :paragraph-2 + :weight :semi-bold + :style style/reaction-count} + (str (if (pos? numeric-value) numeric-value 1))]]))) diff --git a/src/quo/components/selectors/reaction_resource.cljs b/src/quo/components/selectors/reaction_resource.cljs new file mode 100644 index 00000000000..f4e2c3370d2 --- /dev/null +++ b/src/quo/components/selectors/reaction_resource.cljs @@ -0,0 +1,10 @@ +(ns quo.components.selectors.reaction-resource) + +(def system-emojis + {:reaction/thumbs-up "👍" + :reaction/thumbs-down "👎" + :reaction/love "❤" + :reaction/laugh "😂" + :reaction/sad "😢" + :reaction/angry "😡"}) + diff --git a/src/quo/components/selectors/reactions_selector/component_spec.cljs b/src/quo/components/selectors/reactions_selector/component_spec.cljs new file mode 100644 index 00000000000..59970ba71bf --- /dev/null +++ b/src/quo/components/selectors/reactions_selector/component_spec.cljs @@ -0,0 +1,27 @@ +(ns quo.components.selectors.reactions-selector.component-spec + (:require + [quo.components.selectors.reactions-selector.view :as view] + [test-helpers.component :as h])) + +(h/describe "Selectors > Reactions" + (h/test "renders component" + (h/render [view/view {:emoji :reaction/sad}]) + (h/is-truthy (h/get-by-label-text :reaction))) + + (h/describe "on-press event" + (h/test "starts with released state" + (let [on-press (h/mock-fn)] + (h/render [view/view + {:emoji :reaction/love + :on-press on-press}]) + (h/fire-event :press (h/get-by-label-text :reaction)) + (h/was-called on-press))) + + (h/test "starts with pressed state" + (let [on-press (h/mock-fn)] + (h/render [view/view + {:emoji :reaction/love + :on-press on-press + :start-pressed? true}]) + (h/fire-event :press (h/get-by-label-text :reaction)) + (h/was-called on-press))))) diff --git a/src/quo/components/selectors/reactions_selector/style.cljs b/src/quo/components/selectors/reactions_selector/style.cljs new file mode 100644 index 00000000000..439db7f9e78 --- /dev/null +++ b/src/quo/components/selectors/reactions_selector/style.cljs @@ -0,0 +1,31 @@ +(ns quo.components.selectors.reactions-selector.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn container + [pressed? theme] + (merge + {:border-radius 12 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :background-color (when pressed? + (colors/theme-colors colors/neutral-10 colors/neutral-80-opa-40 theme))} + (if platform/ios? + {:padding 9.5 + :padding-horizontal 8} + {:padding 8}))) + +(def emoji-text-style + (merge + {:line-height 20 + :text-align :center} + (if platform/ios? + {:line-height 20 + :font-size 17 + :text-align :center} + {:font-size 18 + :height 24 + :width 24 + :text-align :center + :text-align-vertical :center}))) diff --git a/src/quo/components/selectors/reactions_selector/view.cljs b/src/quo/components/selectors/reactions_selector/view.cljs new file mode 100644 index 00000000000..f4bce3f2cef --- /dev/null +++ b/src/quo/components/selectors/reactions_selector/view.cljs @@ -0,0 +1,25 @@ +(ns quo.components.selectors.reactions-selector.view + (:require + [quo.components.selectors.reaction-resource :as reactions.resource] + [quo.components.selectors.reactions-selector.style :as style] + [quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [emoji container-style on-press + accessibility-label start-pressed?] + :or {accessibility-label :reaction}}] + (let [[pressed? set-pressed] (rn/use-state start-pressed?) + theme (quo.context/use-theme) + on-press (fn [e] + (set-pressed (not pressed?)) + (when on-press (on-press e)))] + [rn/pressable + {:accessibility-label accessibility-label + :allow-multiple-presses? true + :style (merge (style/container pressed? theme) + container-style) + :on-press on-press} + [rn/text + {:style style/emoji-text-style} + (reactions.resource/system-emojis emoji)]])) diff --git a/src/quo/components/selectors/selectors/component_spec.cljs b/src/quo/components/selectors/selectors/component_spec.cljs new file mode 100644 index 00000000000..5685e4bd044 --- /dev/null +++ b/src/quo/components/selectors/selectors/component_spec.cljs @@ -0,0 +1,69 @@ +(ns quo.components.selectors.selectors.component-spec + (:require + [quo.components.selectors.selectors.view :as selectors] + [reagent.core :as reagent] + [test-helpers.component :as h])) + +(defn render-toggle + ([] + (render-toggle {:type :toggle})) + ([opts] + (h/render (reagent/as-element [selectors/view (assoc opts :type :toggle)])))) + +(defn render-checkbox + ([] + (render-checkbox {:type :checkbox})) + ([opts] + (h/render (reagent/as-element [selectors/view (assoc opts :type :checkbox)])))) + +(defn render-filled-checkbox + ([] + (render-filled-checkbox {:type :filled-checkbox})) + ([opts] + (h/render (reagent/as-element [selectors/view (assoc opts :type :filled-checkbox)])))) + +(defn render-radio + ([] + (render-radio {:type :radio})) + ([opts] + (h/render (reagent/as-element [selectors/view (assoc opts :type :radio)])))) + +(h/test "default render of toggle component" + (render-toggle) + (h/is-truthy (h/get-by-test-id "toggle-component"))) + +(h/test "toggle component on change is working" + (let [mock-fn (h/mock-fn)] + (render-toggle {:on-change mock-fn}) + (h/fire-event :press (h/get-by-test-id "toggle-component")) + (h/was-called mock-fn))) + +(h/test "default render of radio component" + (render-radio) + (h/is-truthy (h/get-by-test-id "radio-component"))) + +(h/test "radio component on change is working" + (let [mock-fn (h/mock-fn)] + (render-radio {:on-change mock-fn}) + (h/fire-event :press (h/get-by-test-id "radio-component")) + (h/was-called mock-fn))) + +(h/test "default render of checkbox component" + (render-checkbox) + (h/is-truthy (h/get-by-test-id "checkbox-component"))) + +(h/test "checkbox component on change is working" + (let [mock-fn (h/mock-fn)] + (render-checkbox {:on-change mock-fn}) + (h/fire-event :press (h/get-by-test-id "checkbox-component")) + (h/was-called mock-fn))) + +(h/test "default render of filled-checkbox component" + (render-filled-checkbox) + (h/is-truthy (h/get-by-test-id "filled-checkbox-component"))) + +(h/test "filled-checkbox component on change is working" + (let [mock-fn (h/mock-fn)] + (render-filled-checkbox {:on-change mock-fn}) + (h/fire-event :press (h/get-by-test-id "filled-checkbox-component")) + (h/was-called mock-fn))) diff --git a/src/quo/components/selectors/selectors/style.cljs b/src/quo/components/selectors/selectors/style.cljs new file mode 100644 index 00000000000..417591f7560 --- /dev/null +++ b/src/quo/components/selectors/selectors/style.cljs @@ -0,0 +1,128 @@ +(ns quo.components.selectors.selectors.style + (:require + [quo.foundations.colors :as colors])) + +(defn- toggle-background-color + [customization-color theme] + {:normal {:checked (colors/resolve-color customization-color theme) + :unchecked (colors/theme-colors colors/neutral-30 colors/neutral-80 theme)} + :blur {:checked (colors/resolve-color customization-color :light) + :unchecked (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-10 theme)}}) + +(defn- radio-border-color + [customization-color theme] + {:normal {:checked (colors/resolve-color customization-color theme) + :unchecked (colors/theme-colors colors/neutral-30 colors/neutral-70 theme)} + :blur {:checked (colors/resolve-color customization-color :light) + :unchecked (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-40 theme)}}) + +(defn- radio-background-unchecked-color + [theme] + {:normal (colors/theme-colors colors/white-opa-40 colors/neutral-80-opa-40 theme) + :blur colors/white-opa-5}) + +(defn- checkbox-background-color + [customization-color theme] + {:normal {:checked (colors/resolve-color customization-color theme) + :unchecked nil} + :blur {:checked (colors/resolve-color customization-color :light) + :unchecked nil}}) + +(defn- checkbox-border-unchecked-color + [theme] + {:normal (colors/theme-colors colors/neutral-30 colors/neutral-70 theme) + :blur (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme)}) + +(defn- filled-checkbox-background-color + [theme] + {:normal (colors/theme-colors colors/neutral-30 colors/neutral-80 theme) + :blur (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme)}) + +(defn- get-color + [color-map & [blur? checked?]] + (let [blur-type (if blur? :blur :normal)] + (if (some? checked?) + (get-in color-map [blur-type (if checked? :checked :unchecked)]) + (get color-map blur-type)))) + +(defn toggle + [{:keys [checked? disabled? blur? container-style customization-color theme]}] + (assoc container-style + :height 20 + :width 30 + :border-radius 20 + :opacity (if disabled? 0.3 1) + :background-color (get-color (toggle-background-color customization-color theme) + blur? + checked?))) + +(defn toggle-inner + [{:keys [checked?]}] + {:height 16 + :width 16 + :background-color colors/white + :border-radius 20 + :margin-left (if checked? 12 2) + :margin-right :auto + :margin-top :auto + :margin-bottom :auto}) + +(defn radio + [{:keys [checked? disabled? blur? container-style customization-color theme]}] + (assoc container-style + :height 20 + :width 20 + :border-radius 20 + :border-width 1.2 + :opacity (if disabled? 0.3 1) + :background-color (when-not checked? + (get-color (radio-background-unchecked-color theme) blur?)) + :border-color (get-color (radio-border-color customization-color theme) blur? checked?))) + +(defn radio-inner + [{:keys [checked? blur? customization-color theme]}] + {:height 14 + :width 14 + :margin 1.8 + :border-radius 7 + :background-color (when checked? + (get-color (radio-border-color customization-color theme) blur? checked?))}) + +(defn checkbox + [{:keys [checked? disabled? blur? container-style customization-color theme]}] + (assoc container-style + :height 20 + :width 20 + :border-radius 6 + :opacity (if disabled? 0.3 1) + :border-width (if checked? 0 1.2) + :background-color (get-color (checkbox-background-color customization-color theme) + blur? + checked?) + :border-color (when-not checked? + (get-color (checkbox-border-unchecked-color theme) blur?)))) + +(defn common-checkbox-inner + [{:keys [checked?]}] + (let [size (if checked? 20 0)] + {:height size :width size})) + +(defn checkbox-check + [_checked? _blur? _theme] + {:size 20 + :color colors/white}) + +(defn filled-checkbox + [{:keys [disabled? blur? container-style theme]}] + (assoc container-style + :height 21 + :width 21 + :border-radius 6 + :opacity (if disabled? 0.3 1) + :background-color (get-color (filled-checkbox-background-color theme) blur?))) + +(defn filled-checkbox-check + [checked? _blur? theme] + {:size 20 + :color (when checked? (colors/theme-colors colors/neutral-100 colors/white theme))}) + diff --git a/src/quo/components/selectors/selectors/view.cljs b/src/quo/components/selectors/selectors/view.cljs new file mode 100644 index 00000000000..23a53c3a62f --- /dev/null +++ b/src/quo/components/selectors/selectors/view.cljs @@ -0,0 +1,93 @@ +(ns quo.components.selectors.selectors.view + (:require + [quo.components.icon :as icons] + [quo.components.selectors.selectors.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn- base-selector + [{:keys [default-checked? checked? disabled? blur? customization-color on-change container-style + label-prefix outer-style-fn inner-style-fn icon-style-fn]}] + (let [theme (quo.context/use-theme) + customization-color (if customization-color customization-color :blue) + controlled-component? (some? checked?) + [internal-checked? + set-internal-checked?] (rn/use-state (when-not controlled-component? + (or default-checked? false))) + actual-checked? (if controlled-component? checked? internal-checked?) + accessibility-label (str label-prefix "-" (if actual-checked? "on" "off")) + test-id (str label-prefix "-component") + outer-styles (outer-style-fn {:checked? actual-checked? + :disabled? disabled? + :blur? blur? + :container-style container-style + :customization-color customization-color + :theme theme}) + on-press (rn/use-callback + (fn [] + (when-not (nil? internal-checked?) + (set-internal-checked? (not internal-checked?))) + (when on-change (on-change (not actual-checked?)))) + [internal-checked? actual-checked? on-change])] + [rn/pressable + (when-not disabled? + {:on-press on-press + :allow-multiple-presses? true + :hit-slop 12}) + [rn/view + {:style outer-styles + :needs-offscreen-alpha-compositing true + :accessibility-label accessibility-label + :testID test-id} + [rn/view + {:style (inner-style-fn {:theme theme + :checked? actual-checked? + :blur? blur? + :customization-color customization-color})} + (when (and icon-style-fn actual-checked?) + [icons/icon :i/check-small (icon-style-fn actual-checked? blur? theme)])]]])) + +(defn- toggle + [props] + [base-selector + (assoc props + :label-prefix "toggle" + :outer-style-fn style/toggle + :inner-style-fn style/toggle-inner)]) + +(defn- radio + [props] + [base-selector + (assoc props + :label-prefix "radio" + :outer-style-fn style/radio + :inner-style-fn style/radio-inner)]) + +(defn- checkbox + [props] + [base-selector + (assoc props + :label-prefix "checkbox" + :outer-style-fn style/checkbox + :inner-style-fn style/common-checkbox-inner + :icon-style-fn style/checkbox-check)]) + +(defn- filled-checkbox + [props] + [base-selector + (assoc props + :label-prefix "filled-checkbox" + :outer-style-fn style/filled-checkbox + :inner-style-fn style/common-checkbox-inner + :icon-style-fn style/filled-checkbox-check)]) + +(defn view + [{:keys [type] + :or {type :toggle} + :as props}] + (case type + :toggle [toggle props] + :radio [radio props] + :checkbox [checkbox props] + :filled-checkbox [filled-checkbox props] + nil)) diff --git a/src/quo/components/settings/accounts/style.cljs b/src/quo/components/settings/accounts/style.cljs new file mode 100644 index 00000000000..f19d16d09a6 --- /dev/null +++ b/src/quo/components/settings/accounts/style.cljs @@ -0,0 +1,75 @@ +(ns quo.components.settings.accounts.style + (:require + [quo.foundations.colors :as colors])) + +(def card + {:position :relative + :padding 8 + :border-radius 16 + :height 160 + :width 160}) + +(defn background-top + [custom-color theme] + {:position :absolute + :top 0 + :border-top-left-radius 16 + :border-top-right-radius 16 + :width 160 + :height 64 + :background-color (colors/resolve-color custom-color theme 20)}) + +(defn background-bottom + [theme] + {:position :absolute + :top 40 + :border-radius 16 + :width 160 + :height 120 + :background-color (colors/theme-colors + colors/white + colors/neutral-90 + theme)}) + +(defn avatar-border + [theme] + {:margin 2 + :justify-content :center + :align-items :center + :width 52 + :height 52 + :border-radius 14 + :border-width 2 + :border-color (colors/theme-colors + colors/white + colors/neutral-90 + theme)}) + +(def menu-button-container + {:justify-content :center + :align-items :center + :align-self :flex-start}) + +(defn menu-button-color + [theme] + {:background-color (colors/theme-colors + colors/white-opa-40 + colors/neutral-80-opa-40 + theme)}) + +(defn address-text + [theme] + {:color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}) + +(def card-top + {:flex-direction :row + :align-items :center + :justify-content :space-between}) + +(def card-bottom + {:flex 1 + :margin-horizontal 4 + :margin-top 4}) diff --git a/src/quo/components/settings/accounts/view.cljs b/src/quo/components/settings/accounts/view.cljs new file mode 100644 index 00000000000..217c1cc48c2 --- /dev/null +++ b/src/quo/components/settings/accounts/view.cljs @@ -0,0 +1,56 @@ +(ns quo.components.settings.accounts.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.buttons.button.view :as button] + [quo.components.markdown.text :as text] + [quo.components.settings.accounts.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn card-background + [{:keys [customization-color theme]}] + [:<> + [rn/view {:style (style/background-top customization-color theme)}] + [rn/view {:style (style/background-bottom theme)}]]) + +(defn avatar + [avatar-props theme] + [rn/view {:style (style/avatar-border theme)} + [account-avatar/view (assoc avatar-props :size 48)]]) + +(defn menu-button + [{:keys [on-press theme]}] + [rn/view {:style style/menu-button-container} + [button/button + {:container-style (style/menu-button-color theme) + :type :grey + :icon-only? true + :size 24 + :on-press on-press} + :i/more]]) + +(defn account + [{:keys [account-name account-address avatar-icon customization-color on-press-menu]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/card} + [card-background + {:customization-color customization-color + :theme theme}] + [rn/view {:style style/card-top} + [avatar + {:color customization-color + :icon avatar-icon} + theme] + [menu-button + {:on-press on-press-menu + :theme theme}]] + [rn/view {:style style/card-bottom} + [text/text + {:size :paragraph-1 + :weight :semi-bold} + account-name] + [text/text + {:style (style/address-text theme) + :size :paragraph-2 + :weight :medium} + account-address]]])) diff --git a/src/quo/components/settings/category/component_spec.cljs b/src/quo/components/settings/category/component_spec.cljs new file mode 100644 index 00000000000..5286935c3ea --- /dev/null +++ b/src/quo/components/settings/category/component_spec.cljs @@ -0,0 +1,74 @@ +(ns quo.components.settings.category.component-spec + (:require + [quo.components.settings.category.view :as category] + [test-helpers.component :as h])) + +(h/describe "Settings Category tests" + (h/test "category label renders" + (h/render [category/category + {:list-type :settings + :label "Label" + :data [{:title "Item 1" + :left-icon :i/browser + :chevron? true}]}]) + (h/is-truthy (h/get-by-text "Label"))) + + (h/test "category item renders" + (h/render [category/category + {:list-type :settings + :label "Label" + :data [{:title "Item 1" + :left-icon :i/browser + :chevron? true}]}]) + (h/is-truthy (h/get-by-text "Item 1")))) + + +(h/describe "Reorder Category tests" + (h/test "category label renders" + (h/render [category/category + {:list-type :reorder + :label "Label" + :data [{:title "Item 1" + :right-icon :i/globe + :chevron? true}]}]) + (h/is-truthy (h/get-by-text "Label"))) + + (h/test "category item renders" + (h/render [category/category + {:list-type :reorder + :label "Label" + :data [{:title "Item 1" + :right-icon :i/globe + :chevron? true}]}]) + (h/is-truthy (h/get-by-text "Item 1"))) + + (h/test "category item subtitle renders" + (h/render [category/category + {:list-type :reorder + :label "Label" + :data [{:title "Item 1" + :subtitle "subtitle" + :right-icon :i/globe + :chevron? true}]}]) + (h/is-truthy (h/get-by-text "subtitle")))) + +(h/describe "Data Item Category tests" + (h/test "category renders" + (h/render [category/category + {:list-type :data-item + :label "Label" + :data [{:title "Item 1" + :subtitle "subtitle" + :right-icon :i/globe}]}]) + (h/is-truthy (h/get-by-text "Label")) + (h/is-truthy (h/get-by-text "Item 1")) + (h/is-truthy (h/get-by-text "subtitle"))) + + (h/test "category renders without label" + (h/render [category/category + {:list-type :data-item + :data [{:title "Item 1" + :subtitle "subtitle" + :right-icon :i/globe}]}]) + (h/is-falsy (h/query-by-label-text "Label")) + (h/is-truthy (h/get-by-text "Item 1")))) diff --git a/src/quo/components/settings/category/data_item/view.cljs b/src/quo/components/settings/category/data_item/view.cljs new file mode 100644 index 00000000000..2168b454a64 --- /dev/null +++ b/src/quo/components/settings/category/data_item/view.cljs @@ -0,0 +1,31 @@ +(ns quo.components.settings.category.data-item.view + (:require + [quo.components.markdown.text :as text] + [quo.components.settings.category.style :as style] + [quo.components.settings.data-item.view :as data-item] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [label data container-style blur?]}] + (let [theme (quo.context/use-theme) + last-item (rn/use-memo #(last data) [data])] + [rn/view {:style [(style/container label) container-style]} + (when label + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/label blur? theme)} + label]) + [rn/view {:style (style/settings-items blur? theme)} + (for [item data + ;; NOTE: overwriting the background of the data-item in favor of the category bg + :let [data-item-container-style (-> item :container-style (assoc :background-color nil)) + data-item-props (assoc item + :blur? blur? + :container-style data-item-container-style)]] + ^{:key item} + [:<> + [data-item/view data-item-props] + (when-not (= item last-item) + [rn/view {:style (style/settings-separator blur? theme)}])])]])) diff --git a/src/quo/components/settings/category/reorder/view.cljs b/src/quo/components/settings/category/reorder/view.cljs new file mode 100644 index 00000000000..8d46e9fc0f5 --- /dev/null +++ b/src/quo/components/settings/category/reorder/view.cljs @@ -0,0 +1,39 @@ +(ns quo.components.settings.category.reorder.view + (:require + [quo.components.markdown.text :as text] + [quo.components.settings.category.style :as style] + [quo.components.settings.reorder-item.types :as types] + [quo.components.settings.reorder-item.view :as reorder-item] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.draggable-flatlist :as draggable-flatlist])) + +(defn key-fn [item index] (str (:title item) index)) + +(defn reorder-category + [{:keys [label data blur? container-style]}] + (let [theme (quo.context/use-theme) + [atom-data set-atom-data] (rn/use-state data) + render-fn (rn/use-callback + (fn [item _ _ _ _ drag] + [reorder-item/reorder-item item types/item + {:blur? blur? :drag drag}]) + [blur?]) + on-drag-end-fn (rn/use-callback (fn [_ _ data] (set-atom-data data))) + separator (rn/use-memo (fn [] [rn/view + {:style (style/reorder-separator blur? theme)}]) + [blur? theme])] + [rn/view {:style [(style/container label) container-style]} + [text/text + {:weight :medium + :size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + label] + [draggable-flatlist/draggable-flatlist + {:data atom-data + :key-fn key-fn + :style style/reorder-items + :render-fn render-fn + :on-drag-end-fn on-drag-end-fn + :separator separator}]])) diff --git a/src/quo/components/settings/category/settings/view.cljs b/src/quo/components/settings/category/settings/view.cljs new file mode 100644 index 00000000000..e41dfc9783f --- /dev/null +++ b/src/quo/components/settings/category/settings/view.cljs @@ -0,0 +1,32 @@ +(ns quo.components.settings.category.settings.view + (:require + [quo.components.markdown.text :as text] + [quo.components.settings.category.style :as style] + [quo.components.settings.settings-item.view :as settings-item] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn settings-category + [{:keys [label data blur? container-style customization-color]}] + (let [theme (quo.context/use-theme) + settings-items (remove nil? data) + last-index (dec (count settings-items))] + [rn/view {:style [(style/container label) container-style]} + (when label + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/label blur? theme)} + label]) + [rn/view {:style (style/settings-items blur? theme)} + (map-indexed + (fn [index item] + ^{:key (str label (:title item))} + [:<> + [settings-item/view + (assoc item + :customization-color customization-color + :blur? blur?)] + (when-not (= last-index index) + [rn/view {:style (style/settings-separator blur? theme)}])]) + settings-items)]])) diff --git a/src/quo/components/settings/category/style.cljs b/src/quo/components/settings/category/style.cljs new file mode 100644 index 00000000000..002acc9af5f --- /dev/null +++ b/src/quo/components/settings/category/style.cljs @@ -0,0 +1,46 @@ +(ns quo.components.settings.category.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [label] + {:left 0 + :right 0 + :padding-horizontal 20 + :padding-top (if label 12 8) + :padding-bottom 8}) + +(defn settings-items + [blur? theme] + {:border-radius 16 + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/white colors/neutral-95 theme)) + :border-width (if blur? 0 1) + :border-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) + +(defn label + [blur? theme] + {:margin-bottom 12 + :color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(def reorder-items + {:margin-top 12}) + +(defn settings-separator + [blur? theme] + {:height 1 + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) + +(defn reorder-separator + [blur? theme] + {:height 4 + :background-color (if blur? + :transparent + (colors/theme-colors colors/neutral-5 colors/neutral-95 theme))}) diff --git a/src/quo/components/settings/category/view.cljs b/src/quo/components/settings/category/view.cljs new file mode 100644 index 00000000000..a8deab9211e --- /dev/null +++ b/src/quo/components/settings/category/view.cljs @@ -0,0 +1,13 @@ +(ns quo.components.settings.category.view + (:require + [quo.components.settings.category.data-item.view :as data-item] + [quo.components.settings.category.reorder.view :as reorder] + [quo.components.settings.category.settings.view :as settings])) + +(defn category + [{:keys [list-type] :as props}] + (condp = list-type + :settings [settings/settings-category props] + :data-item [data-item/view props] + :reorder [reorder/reorder-category props] + nil)) diff --git a/src/quo/components/settings/data_item/component_spec.cljs b/src/quo/components/settings/data_item/component_spec.cljs new file mode 100644 index 00000000000..aa0857d6190 --- /dev/null +++ b/src/quo/components/settings/data_item/component_spec.cljs @@ -0,0 +1,177 @@ +(ns quo.components.settings.data-item.component-spec + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [test-helpers.component :as h])) + +(h/describe + "date item tests" + (h/test "title is visible" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/is-truthy (h/get-by-text "Label"))) + + (h/test "data item renders correctly if card? is false" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? false + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/has-style (h/query-by-label-text :data-item) + {:borderWidth nil})) + + (h/test "data item renders correctly if card? is true and size is small" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :small + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/has-style (h/query-by-label-text :data-item) + {:borderWidth nil})) + + (h/test "data item renders correctly if card? is true" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/has-style (h/query-by-label-text :data-item) + {:borderWidth 1})) + + (h/test "subtitle is visible when status is not loading" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/is-truthy (h/get-by-text "Subtitle"))) + + (h/test "right icon is not visible when icon-right? is false" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/is-falsy (h/query-by-label-text :icon-right))) + + (h/test "right icon is visible when icon-right? is true" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :right-icon :i/chevron-right + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/is-truthy (h/query-by-label-text :icon-right))) + + (h/test "icon is visible when subtitle type is icon" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :icon + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/is-truthy (h/query-by-label-text :subtitle-type-icon))) + + (h/test "image is visible when subtitle type is network" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :network + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow + :network-image (quo.resources/get-network :ethereum)}]) + (h/is-truthy (h/query-by-label-text :subtitle-type-image))) + + (h/test "emoji is visible when subtitle type is account" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :account + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/is-truthy (h/query-by-label-text :account-emoji))) + + (h/test "edit icon is visible when subtitle type is editable" + (h/render [quo/data-item + {:on-press (h/mock-fn) + :blur? false + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :subtitle-type :editable + :icon :i/placeholder + :emoji "🎮" + :customization-color :yellow}]) + (h/is-truthy (h/query-by-label-text :edit-icon)))) diff --git a/src/quo/components/settings/data_item/style.cljs b/src/quo/components/settings/data_item/style.cljs new file mode 100644 index 00000000000..dcd3e6d4fe7 --- /dev/null +++ b/src/quo/components/settings/data_item/style.cljs @@ -0,0 +1,98 @@ +(ns quo.components.settings.data-item.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [{:keys [size card? blur? actionable? theme]}] + {:flex-direction :row + :justify-content :space-between + :padding-vertical (case size + :default 8 + :large 12 + nil) + :padding-horizontal (case size + :default 12 + :large 16 + nil) + :border-radius 16 + :border-width (when (and card? (not= size :small)) 1) + :background-color (if blur? + (if actionable? :transparent colors/white-opa-5) + (colors/theme-colors + (if actionable? colors/white colors/neutral-2_5) + (if actionable? colors/neutral-95 colors/neutral-90) + theme)) + :border-color (if blur? + colors/white-opa-10 + (colors/theme-colors colors/neutral-10 + colors/neutral-80 + theme))}) + +(defn loading-container + [size blur? theme] + {:width (case size + :large 132 + :small 72 + 132) + :height (case size + :large 16 + :small 10 + 16) + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-5 + colors/neutral-90 + theme)) + :border-radius (case size + :large 6 + :small 3 + 6) + :margin-vertical (case size + :large 4 + :small 3 + 4)}) + +(def subtitle-container + {:flex-direction :row + :align-items :center + :margin-bottom 1}) + +(def right-container + {:flex-direction :row + :align-items :center}) + +(defn subtitle-icon-container + [subtitle-type] + {:margin-right (when-not (contains? #{:editable :default} subtitle-type) 4) + :justify-content :center}) + +(defn title + [blur? theme] + {:color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme)) + :margin-right 4}) + +(def title-container + {:flex-direction :row + :align-items :center}) + +(def image + {:width 16 + :height 16}) + +(defn description + [subtitle-color blur? theme] + {:color (or subtitle-color + (if blur? + colors/white + (colors/theme-colors colors/neutral-100 + colors/white + theme)))}) +(def right-icon + {:margin-left 12}) + +(def left-side + {:flex 1}) diff --git a/src/quo/components/settings/data_item/view.cljs b/src/quo/components/settings/data_item/view.cljs new file mode 100644 index 00000000000..2c94604bc1b --- /dev/null +++ b/src/quo/components/settings/data_item/view.cljs @@ -0,0 +1,182 @@ +(ns quo.components.settings.data-item.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.icon :as icons] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.components.settings.data-item.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- left-loading + [{:keys [size blur?]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/loading-container size blur? theme)}])) + +(defn- left-subtitle + [{:keys [size subtitle-text-props subtitle-type subtitle-color icon icon-color blur? subtitle + customization-color emoji + network-image] + :or {subtitle-type :default}}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/subtitle-container} + (when (and subtitle-type (not= :small size)) + [rn/view {:style (style/subtitle-icon-container subtitle-type)} + (case subtitle-type + :icon [icons/icon icon + {:accessibility-label :subtitle-type-icon + :size 16 + :color icon-color}] + :account [account-avatar/view + {:customization-color customization-color + :size 16 + :emoji emoji + :type :default}] + :network [rn/image + {:accessibility-label :subtitle-type-image + :source network-image + :style style/image}] + nil)]) + [text/text + (merge {:weight :medium + :size :paragraph-2 + :style (style/description subtitle-color blur? theme)} + subtitle-text-props) + subtitle] + (when (= subtitle-type :editable) + [icons/icon :i/edit + {:accessibility-label :edit-icon + :size 12 + :container-style {:margin-left 2} + :color (if blur? + colors/neutral-40 + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme))}])])) + +(defn- left-title + [{:keys [title blur? title-icon]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/title-container} + [text/text + {:weight :regular + :size :paragraph-2 + :style (style/title blur? theme)} + title] + (when title-icon + [icons/icon title-icon + {:accessibility-label :title-icon + :size 12 + :color (if blur? + colors/neutral-40 + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme))}])])) + +(defn- left-side + "The description can either be given as a string `subtitle-type` or a component `custom-subtitle`" + [{:keys [title subtitle-text-props status size blur? custom-subtitle icon subtitle subtitle-type + subtitle-color icon-color + customization-color network-image emoji title-icon] + :as props}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/left-side} + [rn/view + [left-title + {:title title + :title-icon title-icon + :blur? blur?}]] + (if (= status :loading) + [left-loading + {:size size + :blur? blur? + :theme theme}] + (if custom-subtitle + [custom-subtitle props] + [left-subtitle + {:theme theme + :subtitle-text-props subtitle-text-props + :size size + :subtitle-type subtitle-type + :subtitle-color subtitle-color + :icon icon + :icon-color icon-color + :blur? blur? + :subtitle subtitle + :customization-color customization-color + :emoji emoji + :network-image network-image}]))])) + +(defn- right-side + [{:keys [right-icon right-content icon-color]}] + (let [{:keys [type data size] + :or {size :size-24}} right-content] + [rn/view {:style style/right-container} + (when type + [preview-list/view + {:type type + :number (count data) + :size size} + data]) + (when right-icon + [rn/view {:style style/right-icon} + [icons/icon right-icon + {:accessibility-label :icon-right + :color icon-color + :size 20}]])])) + + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:blur? {:optional true} [:maybe :boolean]] + [:card? {:optional true} [:maybe :boolean]] + [:right-icon {:optional true} [:maybe :keyword]] + [:right-content {:optional true} [:maybe :map]] + [:icon-color {:optional true} [:maybe :schema.common/customization-color]] + [:status {:optional true} [:maybe [:enum :default :loading]]] + [:subtitle-type {:optional true} [:maybe [:enum :default :icon :network :account :editable]]] + [:subtitle-color {:optional true} [:maybe :schema.common/customization-color]] + [:size {:optional true} [:maybe [:enum :default :small :large]]] + [:title :string] + [:subtitle {:optional true} [:maybe [:or :string :double]]] + [:subtitle-text-props {:optional true} [:maybe :map]] + [:custom-subtitle {:optional true} [:maybe fn?]] + [:icon {:optional true} [:maybe :keyword]] + [:title-icon {:optional true} [:maybe :keyword]] + [:emoji {:optional true} [:maybe :string]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:network-image {:optional true} [:maybe :schema.common/image-source]] + [:on-press {:optional true} [:maybe fn?]] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) + +(defn view-internal + [{:keys [blur? card? right-icon right-content status size on-press container-style] + :as props}] + (let [theme (quo.context/use-theme) + icon-color (if (or blur? (= :dark theme)) + colors/neutral-40 + colors/neutral-50)] + [rn/pressable + {:accessibility-label :data-item + :disabled (nil? on-press) + :on-press on-press + :style (merge (style/container {:size size + :card? card? + :blur? blur? + :actionable? on-press + :theme theme}) + container-style)} + [left-side props] + (when (and (= :default status) (not= :small size)) + [right-side + {:right-icon right-icon + :right-content right-content + :icon-color icon-color}])])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/settings/page_setting/component_spec.cljs b/src/quo/components/settings/page_setting/component_spec.cljs new file mode 100644 index 00000000000..dcb36abd95f --- /dev/null +++ b/src/quo/components/settings/page_setting/component_spec.cljs @@ -0,0 +1,11 @@ +(ns quo.components.settings.page-setting.component-spec + (:require + [quo.core :as quo] + [test-helpers.component :as h])) + +(h/describe + "renders basic text" + (h/test "title is visible" + (h/render [quo/page-setting + {:setting-text "sample text"}]) + (h/is-truthy (h/get-by-text "sample text")))) diff --git a/src/quo/components/settings/page_setting/style.cljs b/src/quo/components/settings/page_setting/style.cljs new file mode 100644 index 00000000000..49ee62300fb --- /dev/null +++ b/src/quo/components/settings/page_setting/style.cljs @@ -0,0 +1,14 @@ +(ns quo.components.settings.page-setting.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:flex-direction :row + :justify-content :space-between + :background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme) + :padding-vertical 13 + :padding-horizontal 12 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :border-radius 16}) diff --git a/src/quo/components/settings/page_setting/view.cljs b/src/quo/components/settings/page_setting/view.cljs new file mode 100644 index 00000000000..276a383870d --- /dev/null +++ b/src/quo/components/settings/page_setting/view.cljs @@ -0,0 +1,24 @@ +(ns quo.components.settings.page-setting.view + (:require + [quo.components.markdown.text :as text] + [quo.components.selectors.selectors.view :as selectors] + [quo.components.settings.page-setting.style :as style] + [quo.context] + [react-native.core :as rn])) + +(defn page-setting + [{:keys [setting-text customization-color checked? container-style on-change disabled?]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge (style/container theme) + container-style)} + [text/text + {:weight :medium + :number-of-lines 1} setting-text] + [selectors/view + {:type :toggle + :checked? checked? + :customization-color customization-color + :accessibility-label :user-list-toggle-check + :disabled? disabled? + :on-change (when on-change on-change)}]])) diff --git a/src/quo/components/settings/privacy_option/style.cljs b/src/quo/components/settings/privacy_option/style.cljs new file mode 100644 index 00000000000..21a1c9892a6 --- /dev/null +++ b/src/quo/components/settings/privacy_option/style.cljs @@ -0,0 +1,94 @@ +(ns quo.components.settings.privacy-option.style + (:require + [quo.foundations.colors :as colors])) + +(def bullet-container + {:width 20 + :height 20 + :margin-right 8 + :align-self :flex-start}) + +(defn bullet + [theme] + {:background-color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme) + :border-radius 100 + :width 8 + :height 8 + :position :absolute + :left 6 + :top 6}) + +(def list-container + {:margin-right 12}) + +(def list-item + {:flex 1 + :flex-direction :row + :align-items :center + :padding-vertical 6 + :padding-horizontal 12}) + +(defn selection-indicator-container + [active? theme] + {:height 20 + :width 20 + :border-radius 20 + :border-width 1 + :border-color (if active? + (colors/theme-colors colors/primary-50 colors/primary-60 theme) + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme))}) + +(defn selection-indicator + [active? theme] + {:margin-left :auto + :height 14 + :width 14 + :background-color (if active? + (colors/theme-colors colors/primary-50 colors/primary-60 theme) + (colors/theme-colors colors/white-opa-40 colors/neutral-80-opa-40 theme)) + :border-radius 20 + :margin-right :auto + :margin-top :auto + :margin-bottom :auto}) + +(def card-footer-label-container + {:flex 0.9 + :margin-left 16}) + +(def card-footer-toggle-container + {:flex 0.1 + :margin-horizontal 12 + :align-self :center}) + +(defn card-footer + [theme] + {:flex 1 + :flex-direction :row + :justify-content :space-between + :margin-horizontal 12 + :margin-top 8 + :margin-bottom 12 + :padding-vertical 12 + :border-radius 12 + :border-width 1 + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme) + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +(def card-header-container + {:flex-direction :row + :align-items :center + :padding-top 12 + :padding-bottom 8 + :padding-horizontal 12}) + +(def card-header-label-container + {:flex 1 + :margin-left 4}) + +(defn privacy-option-card + [active? theme] + {:border-radius 16 + :border-width 1 + :border-color (if active? + (colors/theme-colors colors/primary-50 colors/primary-60 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))}) diff --git a/src/quo/components/settings/privacy_option/view.cljs b/src/quo/components/settings/privacy_option/view.cljs new file mode 100644 index 00000000000..f9467d2f2b7 --- /dev/null +++ b/src/quo/components/settings/privacy_option/view.cljs @@ -0,0 +1,75 @@ +(ns quo.components.settings.privacy-option.view + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.selectors.selectors.view :as selectors] + [quo.components.settings.privacy-option.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- bullet + [theme] + [rn/view {:style style/bullet-container} + [rn/view {:style (style/bullet theme)}]]) + +(defn- unordered-list + [{:keys [container-style theme]} list-items] + [rn/view {:style (merge style/list-container container-style)} + (for [item list-items] + ^{:key (hash item)} + [rn/view {:style style/list-item} + [bullet theme] + [text/text {:size :paragraph-2} item]])]) + +(defn- card-footer + [{:keys [active? label on-toggle theme]}] + [rn/pressable + [rn/view {:style (style/card-footer theme)} + [rn/view {:style style/card-footer-label-container} + [text/text {:size :paragraph-2} label]] + [rn/view {:style style/card-footer-toggle-container} + [selectors/view + {:type :toggle + :disabled? (not active?) + :on-change on-toggle}]]]]) + +(defn- selection-indicator + [active? theme] + [rn/view {:style (style/selection-indicator-container active? theme)} + [rn/view {:style (style/selection-indicator active? theme)}]]) + +(defn- card-header + [{:keys [icon label active? theme]}] + [rn/view {:style style/card-header-container} + [icons/icon icon + {:size 20 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}] + [rn/view {:style style/card-header-label-container} + [text/text {:weight :semi-bold} label]] + [selection-indicator active?]]) + +(defn view + [{:keys [active? header footer list-items icon on-select on-toggle] + :or {icon :i/world + active? false}}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:on-press on-select + :accessibility-label :privacy-option-card + :testID :privacy-option-card + :style (style/privacy-option-card active? theme)} + [card-header + {:theme theme + :active? active? + :icon icon + :label header}] + [unordered-list + {:theme theme + :container-style (when-not footer {:margin-bottom 8})} list-items] + (when footer + [card-footer + {:theme theme + :active? active? + :label footer + :on-toggle on-toggle}])])) diff --git a/src/quo/components/settings/reorder_item/component_spec.cljs b/src/quo/components/settings/reorder_item/component_spec.cljs new file mode 100644 index 00000000000..be852aaf926 --- /dev/null +++ b/src/quo/components/settings/reorder_item/component_spec.cljs @@ -0,0 +1,30 @@ +(ns quo.components.settings.reorder-item.component-spec + (:require + [quo.components.settings.reorder-item.types :as types] + [quo.core :as quo] + [test-helpers.component :as h])) + +(h/describe + "sortable list items tests" + (h/test "renders item" + (h/render [quo/reorder-item + {:id 1 + :type "item" + :image-size 24 + :title "Item 1"} types/item]) + (h/is-truthy (h/get-by-text "Item 1"))) + + (h/test "renders item placeholder" + (h/render [quo/reorder-item {:label "Item 1"} types/placeholder]) + (h/is-truthy (h/get-by-text "Item 1"))) + + (h/test "renders item skeleton" + (let [component (h/render [quo/reorder-item nil types/skeleton])] + (h/is-truthy component))) + + (h/test "renders item tab" + (h/render [quo/reorder-item + {:data [{:id 1 + :label "Item 1"} + ]} types/tab]) + (h/is-truthy (h/get-by-text "Item 1")))) diff --git a/src/quo/components/settings/reorder_item/items/item.cljs b/src/quo/components/settings/reorder_item/items/item.cljs new file mode 100644 index 00000000000..47d221468c4 --- /dev/null +++ b/src/quo/components/settings/reorder_item/items/item.cljs @@ -0,0 +1,49 @@ +(ns quo.components.settings.reorder-item.items.item + (:require + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.settings.reorder-item.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn view + [{:keys + [title subtitle image image-size right-text right-icon on-press]} + blur? + drag] + (let [theme (quo.context/use-theme)] + [rn/touchable-opacity + {:on-press on-press + :on-long-press drag + :style (merge (style/item-container blur? theme) + (when subtitle style/item-container-extended))} + [icon/icon :main-icons/drag + {:color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}] + [rn/view + {:style style/body-container} + [rn/view + {:style style/image-container} + [fast-image/fast-image + {:source image + :style (style/image image-size)}]] + [rn/view + {:style style/text-container} + [rn/view + [text/text + {:weight :medium} + title] + (when subtitle + [text/text + {:style style/item-subtitle + :size :paragraph-2} + subtitle])] + (when right-text + [text/text {:style style/right-text} right-text]) + (when right-icon + [rn/view {:style style/right-icon-container} [icon/icon right-icon (style/right-icon theme)]])]] + [icon/icon :tiny-icons/chevron-right (style/chevron theme)]])) diff --git a/src/quo/components/settings/reorder_item/items/item_placeholder.cljs b/src/quo/components/settings/reorder_item/items/item_placeholder.cljs new file mode 100644 index 00000000000..61009111a3c --- /dev/null +++ b/src/quo/components/settings/reorder_item/items/item_placeholder.cljs @@ -0,0 +1,16 @@ +(ns quo.components.settings.reorder-item.items.item-placeholder + (:require + [quo.components.markdown.text :as text] + [quo.components.settings.reorder-item.style :as style] + [react-native.core :as rn])) + +(defn view + [item theme] + (let [label (:label item)] + [rn/view + {:accessibility-label :reorder-placerholder-drag-handle + :style (style/placeholder-container theme)} + [text/text + {:style (style/placeholder-text theme) + :weight :regular} + label]])) diff --git a/src/quo/components/settings/reorder_item/items/item_skeleton.cljs b/src/quo/components/settings/reorder_item/items/item_skeleton.cljs new file mode 100644 index 00000000000..c92cfaf23a2 --- /dev/null +++ b/src/quo/components/settings/reorder_item/items/item_skeleton.cljs @@ -0,0 +1,9 @@ +(ns quo.components.settings.reorder-item.items.item-skeleton + (:require + [quo.components.settings.reorder-item.style :as style] + [react-native.core :as rn])) + +(defn view + [theme] + [rn/view + {:style (style/skeleton-container theme)}]) diff --git a/src/quo/components/settings/reorder_item/items/item_tabs.cljs b/src/quo/components/settings/reorder_item/items/item_tabs.cljs new file mode 100644 index 00000000000..07bcb783c1e --- /dev/null +++ b/src/quo/components/settings/reorder_item/items/item_tabs.cljs @@ -0,0 +1,44 @@ +(ns quo.components.settings.reorder-item.items.item-tabs + (:require + [quo.components.icon :as quo-icons] + [quo.components.markdown.text :as text] + [quo.components.settings.reorder-item.style :as style] + [quo.components.tabs.segmented-tab :as quo] + [quo.context] + [react-native.core :as rn])) + +(defn render-tab-item + [item] + (let [theme (quo.context/use-theme) + tab-image (cond + (item :image) [rn/image + {:source (:image item) + :style style/tab-item-image}] + (item :icon) [rn/view {:style style/tab-item-image} + (quo-icons/icon (:icon item) (style/tab-icon theme))])] + [rn/view + {:style style/tab-item-container} + tab-image + [text/text + {:size :paragraph-1 + :weight :medium} + (:label item)]])) + +(defn transform-data + [data] + (map #(hash-map :id (:id %) :label (render-tab-item %)) data)) + +(defn view + [{:keys [data default-active on-change] + :or {default-active 1 + on-change (constantly nil)}}] + (let [theme (quo.context/use-theme)] + [quo/segmented-control + {:default-active default-active + :size 32 + :blur? false + :container-style (style/tab-container theme) + :item-container-style (style/segmented-tab-item-container theme) + :active-item-container-style (style/active-segmented-tab-item-container theme) + :data (transform-data data) + :on-change on-change}])) diff --git a/src/quo/components/settings/reorder_item/style.cljs b/src/quo/components/settings/reorder_item/style.cljs new file mode 100644 index 00000000000..e6422b9a59f --- /dev/null +++ b/src/quo/components/settings/reorder_item/style.cljs @@ -0,0 +1,151 @@ +(ns quo.components.settings.reorder-item.style + (:require + [quo.foundations.colors :as colors])) + +(defn item-container + [blur? theme] + {:flex-direction :row + :align-items :center + :border-radius 16 + :padding-horizontal 12 + :padding-vertical 12 + :height 48 + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors + colors/white + colors/neutral-90 + theme))}) + +(def item-container-extended + {:height 56}) + +(def body-container + {:flex 1 + :margin-left 12 + :flex-direction :row + :align-items :center + :margin-right -6}) + +(def image-container + {:margin-right 8}) + +(defn image + [size] + {:width size + :height size}) + +(defn chevron + [theme] + {:color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme) + :height 14 + :width 14}) + +(def item-subtitle + {:color colors/neutral-50}) + +(def right-text + {:font-size 15 + :color colors/neutral-40}) + +(def text-container + {:flex 1 + :flex-direction :row + :justify-content :space-between + :margin-right 8}) + +(defn right-icon + [theme] + {:height 20 + :width 20 + :color (colors/theme-colors + colors/neutral-40 + colors/neutral-40 + theme)}) + +(def right-icon-container + {:justify-content :center}) + +(defn placeholder-container + [theme] + {:background-color :transparent + :border-width 1 + :border-color (colors/theme-colors + colors/neutral-30 + colors/neutral-80 + theme) + :padding 12 + :justify-content :center + :align-items :center + :border-radius 16 + :margin-bottom 24 + :border-style :dashed}) + +(defn placeholder-text + [theme] + {:color (colors/theme-colors + colors/neutral-40 + colors/neutral-50 + theme) + :font-size 13}) + +(defn skeleton-container + [theme] + {:background-color (colors/theme-colors + colors/neutral-5 + colors/neutral-95 + theme) + :border-radius 16 + :margin-bottom 24 + :height 48}) + +(defn tab-container + [theme] + {:background-color (colors/theme-colors + colors/neutral-5 + colors/neutral-95 + theme) + :padding-horizontal 4 + :padding-vertical 6 + :margin-bottom 24}) + +(defn segmented-tab-item-container + [theme] + {:height 40 + :border-width 1 + :border-style :dashed + :margin-horizontal 2 + :border-color (colors/theme-colors + colors/neutral-30 + colors/neutral-60 + theme)}) + +(defn active-segmented-tab-item-container + [theme] + {:height 40 + :background-color (colors/theme-colors + colors/neutral-30 + colors/neutral-90 + theme)}) + +(def tab-item-container + {:flex-direction :row + :justify-content :center + :align-items :center}) + +(def tab-item-image + {:height 19 + :width 19 + :margin-right 3}) + +(defn tab-icon + [theme] + {:height 16 + :width 16 + :color (colors/theme-colors + colors/neutral-40 + colors/neutral-40 + theme)}) diff --git a/src/quo/components/settings/reorder_item/types.cljs b/src/quo/components/settings/reorder_item/types.cljs new file mode 100644 index 00000000000..6ded05c34a3 --- /dev/null +++ b/src/quo/components/settings/reorder_item/types.cljs @@ -0,0 +1,6 @@ +(ns quo.components.settings.reorder-item.types) + +(def ^:const item 0) +(def ^:const placeholder 1) +(def ^:const skeleton 2) +(def ^:const tab 3) diff --git a/src/quo/components/settings/reorder_item/view.cljs b/src/quo/components/settings/reorder_item/view.cljs new file mode 100644 index 00000000000..be941bd428d --- /dev/null +++ b/src/quo/components/settings/reorder_item/view.cljs @@ -0,0 +1,16 @@ +(ns quo.components.settings.reorder-item.view + (:require + [quo.components.settings.reorder-item.items.item :as item] + [quo.components.settings.reorder-item.items.item-placeholder :as placeholder] + [quo.components.settings.reorder-item.items.item-skeleton :as skeleton] + [quo.components.settings.reorder-item.items.item-tabs :as tab] + [quo.components.settings.reorder-item.types :as types])) + +(defn reorder-item + [item type {:keys [blur? drag]}] + (condp = type + types/item [item/view item blur? drag] + types/placeholder [placeholder/view item] + types/skeleton [skeleton/view] + types/tab [tab/view item] + nil)) diff --git a/src/quo/components/settings/section_label/view.cljs b/src/quo/components/settings/section_label/view.cljs new file mode 100644 index 00000000000..a6fb48c3332 --- /dev/null +++ b/src/quo/components/settings/section_label/view.cljs @@ -0,0 +1,48 @@ +(ns quo.components.settings.section-label.view + (:require + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- get-text-color + [theme blur?] + (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme))) + +(defn label-style + [color description?] + (cond-> {:color color} + + description? + (assoc :margin-bottom 2))) + +(defn view + "Props: + - section - the label of the section + - description (optional) - description of the section + - blur? (optional) - use blurred styling + - theme - light or dark" + [{:keys [section description blur? container-style]}] + (let [theme (quo.context/use-theme) + color (get-text-color theme (or blur? false)) + description? (not (nil? description)) + root-view (if (seq container-style) rn/view :<>) + root-style (when (seq container-style) + {:style container-style})] + [root-view root-style + [text/text + {:number-of-lines 1 + :size (if description? :paragraph-1 :paragraph-2) + :weight :medium + :style (label-style color description?)} + section] + (when description? + [text/text + {:size :paragraph-1 + :weight :regular + :style {:color color}} + description])])) diff --git a/src/quo/components/settings/settings_item/component_spec.cljs b/src/quo/components/settings/settings_item/component_spec.cljs new file mode 100644 index 00000000000..3404d29ea0c --- /dev/null +++ b/src/quo/components/settings/settings_item/component_spec.cljs @@ -0,0 +1,58 @@ +(ns quo.components.settings.settings-item.component-spec + (:require + [quo.components.settings.settings-item.view :as settings-item] + [test-helpers.component :as h])) + +(def props + {:title "Account" + :accessibility-label :settings-item + :action :arrow + :image :icon + :image-props :i/browser}) + +(h/describe "Settings list tests" + (h/test "Default render of Setting list component" + (h/render-with-theme-provider [settings-item/view props]) + (h/is-truthy (h/get-by-label-text :settings-item))) + + (h/test "It renders a title" + (h/render-with-theme-provider [settings-item/view props]) + (h/is-truthy (h/get-by-text "Account"))) + + (h/test "its gets passed an on press event" + (let [event (h/mock-fn)] + (h/render-with-theme-provider [settings-item/view + (merge props {:on-press event})]) + (h/fire-event :press (h/get-by-text "Account")) + (h/was-called event))) + + (h/test "on change event gets fired for toggle" + (let [on-change (h/mock-fn)] + (h/render-with-theme-provider [settings-item/view + (merge props + {:action :selector + :action-props {:on-change on-change}})]) + (h/fire-event :press (h/get-by-label-text :toggle-off)) + (h/was-called on-change))) + + (h/test "It renders a label" + (h/render-with-theme-provider [settings-item/view (merge props {:label :color})]) + (h/is-truthy (h/get-by-label-text :label-component))) + + (h/test "It renders a status tag component" + (h/render-with-theme-provider [settings-item/view + (merge props + {:tag :context + :tag-props {:context "Test Tag" + :icon :i/placeholder}})]) + (h/is-truthy (h/get-by-text "Test Tag"))) + + (h/test "on press event gets fired for button" + (let [event (h/mock-fn)] + (h/render-with-theme-provider [settings-item/view + (merge props + {:action :button + :action-props {:button-text "test button" + :on-press event}})]) + (h/fire-event :press (h/get-by-text "test button")) + (h/was-called event)))) diff --git a/src/quo/components/settings/settings_item/style.cljs b/src/quo/components/settings/settings_item/style.cljs new file mode 100644 index 00000000000..280b6e58c11 --- /dev/null +++ b/src/quo/components/settings/settings_item/style.cljs @@ -0,0 +1,90 @@ +(ns quo.components.settings.settings-item.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:padding-horizontal 12 + :padding-top 12 + :padding-bottom 14}) + +(defn left-sub-container + [{:keys [tag description]}] + {:flex-direction :row + :flex 1 + :margin-right 12 + :align-items (if (or tag description) :flex-start :center)}) + +(defn sub-container + [align-action] + {:flex-direction :row + :padding-right 0.5 + :align-items (or align-action :center)}) + +(defn left-container + [image?] + {:margin-horizontal (if image? 12 0) + :flex 1 + :height "100%" + :justify-content :flex-start}) + +(defn image-container + [description tag image] + {:height (if (= image :icon-avatar) 32 20) + :margin-top (if (or tag description) 1 0) + :margin-left (when (nil? image) 4)}) + +(def status-container + {:flex-direction :row + :align-items :center}) + +(defn status-dot + [online? theme] + {:width 8 + :height 8 + :border-radius 8 + :margin-right 6 + :background-color (if online? + (colors/theme-colors colors/success-50 colors/success-60 theme) + (colors/theme-colors colors/danger-50 colors/danger-60 theme))}) + +(defn icon-color + [blur? theme] + {:color (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(defn color + [blur? theme] + {:color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(defn label-dot + [background-color] + {:width 15 + :height 15 + :border-radius 12 + :margin-right 4 + :background-color background-color}) + +(def status-tag-container + {:margin-top 7 + :margin-bottom 2 + :margin-left -1}) + +(def new-feature-tag-container + {:margin-left 6 + :border-radius 12 + :justify-content :center}) + +(def new-feature-tag-gradient + {:border-radius 12 + :position :absolute + :left 0 + :right 0 + :top 0 + :bottom 0}) + +(def new-feature-tag-text + {:color colors/white + :margin-horizontal 5}) diff --git a/src/quo/components/settings/settings_item/view.cljs b/src/quo/components/settings/settings_item/view.cljs new file mode 100644 index 00000000000..1324ab6470c --- /dev/null +++ b/src/quo/components/settings/settings_item/view.cljs @@ -0,0 +1,148 @@ +(ns quo.components.settings.settings-item.view + (:require + [clojure.string :as string] + [quo.components.avatars.icon-avatar :as icon-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.buttons.button.view :as button] + [quo.components.common.new-feature-gradient :as new-feature-gradient] + [quo.components.icon :as icon] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.components.selectors.selectors.view :as selectors] + [quo.components.settings.settings-item.style :as style] + [quo.components.tags.context-tag.view :as context-tag] + [quo.components.tags.status-tags :as status-tags] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn status-description + [{:keys [description-props blur?]}] + (let [theme (quo.context/use-theme) + {:keys [online? text]} description-props] + [rn/view {:style style/status-container} + [rn/view {:style (style/status-dot online? blur?)}] + [text/text + {:size :paragraph-2 + :style (style/color blur? theme)} + (if online? (i18n/label :t/online-now) text)]])) + +(defn text-description + [{:keys [description-props blur?]}] + (let [theme (quo.context/use-theme) + {:keys [text icon]} description-props] + [rn/view {:style (style/sub-container :center)} + [text/text + {:size :paragraph-2 + :style (style/color blur? theme)} + text] + (when icon + [icon/icon icon + (merge (style/color blur? theme) + {:size 16 + :container-style {:margin-left 4}})])])) + +(defn description-component + [{:keys [description] :as props}] + (case description + :text [text-description props] + :text-plus-icon [text-description props] + :status [status-description props] + nil)) + +(defn emoji-component + [image-props] + [rn/text + (when image-props (string/trim image-props))]) + +(defn image-component + [{:keys [image image-props description tag blur?]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/image-container description tag image)} + (case image + :icon [icon/icon image-props (style/icon-color blur? theme)] + :avatar [user-avatar/user-avatar image-props] + :icon-avatar [icon-avatar/icon-avatar image-props] + :token [token/view image-props] + :emoji [emoji-component image-props] + nil)])) + +(defn tag-component + [{:keys [tag tag-props]}] + (case tag + :positive [status-tags/status-tag + {:status {:type :positive} + :label (:label tag-props) + :no-icon? true + :size :small + :container-style style/status-tag-container}] + :context [context-tag/view + (merge tag-props + {:type :icon + :size 24 + :container-style {:margin-top 8 + :align-self :flex-start}})] + nil)) + +(defn label-component + [{:keys [label label-props label-icon-props blur? theme preview-size]}] + [rn/view {:accessibility-label :label-component} + (case label + :text [text/text {:style (style/color blur? theme)} + label-props] + :color [rn/view + {:style (style/label-dot label-props)}] + :preview [preview-list/view {:type (:type label-props) :size (or preview-size :size-24)} + (:data label-props)] + :icon [icon/icon label-props label-icon-props] + nil)]) + +(defn action-component + [{:keys [action action-props blur? theme customization-color]}] + [rn/view {:style {:margin-left 12}} + (case action + :arrow [icon/icon (or (:icon action-props) :i/chevron-right) (style/color blur? theme)] + :button [button/button + (merge action-props + {:type :outline + :size 24}) + (:button-text action-props)] + :selector [selectors/view (assoc action-props :customization-color customization-color)] + nil)]) + +(defn view + [{:keys [title show-new-feature-tag? on-press action-props accessibility-label blur? + container-style content] + :as props}] + [rn/pressable + {:style (merge style/container container-style) + :on-press (or on-press (:on-change action-props)) + :accessibility-label accessibility-label} + [rn/view + {:style {:flex-direction :row + :justify-content :space-between}} + [rn/view {:style (style/left-sub-container props)} + [image-component props] + [rn/view {:style (style/left-container (:image props))} + [rn/view {:flex-direction :row} + [text/text + {:weight :medium + :style {:color (when blur? colors/white)}} + title] + (when show-new-feature-tag? + [rn/view {:style style/new-feature-tag-container} + [new-feature-gradient/view {:style style/new-feature-tag-gradient}] + [text/text + {:weight :semi-bold + :size :label + :style style/new-feature-tag-text} + (string/upper-case (i18n/label :t/new))]])] + [description-component props] + [tag-component props]]] + [rn/view {:style (style/sub-container (:alignment action-props))} + [label-component props] + [action-component props]]] + (when content + content)]) diff --git a/src/quo/components/share/qr_code/style.cljs b/src/quo/components/share/qr_code/style.cljs new file mode 100644 index 00000000000..baef44043ba --- /dev/null +++ b/src/quo/components/share/qr_code/style.cljs @@ -0,0 +1,46 @@ +(ns quo.components.share.qr-code.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [size] + (if size + {:width size + :height size} + {:flex 1})) + +(def avatar-overlay + {:justify-content :center + :align-items :center}) + +(defn qr-image + [size] + {:width (or size "100%") + :height (or size "100%") + :background-color colors/white-opa-70 + :border-radius 12 + :aspect-ratio 1}) + +(def ^:private avatar-container-common + {:width 68 + :height 68 + :justify-content :center + :align-items :center + :background-color colors/white}) + +(def avatar-container-circular + (assoc avatar-container-common :border-radius 33)) + +(def avatar-container-rounded + (assoc avatar-container-common :border-radius 16)) + +(def big-avatar-container-rounded + (assoc avatar-container-common + :width 84 + :height 84 + :border-radius 42)) + +(def community-logo-image + {:width 64 + :height 64 + :border-radius 32}) diff --git a/src/quo/components/share/qr_code/view.cljs b/src/quo/components/share/qr_code/view.cljs new file mode 100644 index 00000000000..9295d6db729 --- /dev/null +++ b/src/quo/components/share/qr_code/view.cljs @@ -0,0 +1,77 @@ +(ns quo.components.share.qr-code.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.channel-avatar.view :as channel-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.avatars.wallet-user-avatar.view :as wallet-avatar] + [quo.components.share.qr-code.style :as style] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn- avatar-image + [{avatar-type :avatar + :as props}] + [rn/view {:style [rn/stylesheet-absolute-fill style/avatar-overlay]} + [rn/view + {:style (case avatar-type + :wallet-account style/avatar-container-rounded + :saved-address style/big-avatar-container-rounded + style/avatar-container-circular)} + (case avatar-type + :profile + [user-avatar/user-avatar + (assoc props + :size :size-64 + :status-indicator? false + :online? false)] + + :wallet-account + [account-avatar/view (assoc props :size :size-64 :type :default)] + + :community + [rn/image + {:style style/community-logo-image + :source (:picture props)}] + + :channel + [channel-avatar/view (assoc props :locked? nil :size :size-64)] + + :saved-address + [wallet-avatar/wallet-user-avatar (assoc props :size :size-80)] + + nil)]]) + +(defn view + "Receives a URL to show a QR code with an avatar (optional) over it. + Parameters: + - qr-image-uri: A valid uri representing the QR code to display using `fast-image` + - size: if not provided, the QR code image will grow according to its parent + - avatar: Type of the avatar, defaults to `:none` and it can be: + `:profile`, `:wallet-account`, `:community`, `:channel` or `:saved-address` + + Depending on the type selected, different properties are accepted: + `:profile`: + - profile-picture + - full-name + - customization-color + `:wallet-account` + - emoji + - customization-color + `:community` + - picture + `:channel` + - emoji + - customization-color + `:saved-address` + - f-name + - l-name + - customization-color" + [{:keys [avatar size qr-image-uri] + :or {avatar :none} + :as props}] + [rn/view {:style (style/container size)} + [fast-image/fast-image + {:style (style/qr-image size) + :source {:uri qr-image-uri}}] + (when-not (= avatar :none) + [avatar-image props])]) diff --git a/src/quo/components/share/share_qr_code/component_spec.cljs b/src/quo/components/share/share_qr_code/component_spec.cljs new file mode 100644 index 00000000000..86a3a96581a --- /dev/null +++ b/src/quo/components/share/share_qr_code/component_spec.cljs @@ -0,0 +1,119 @@ +(ns quo.components.share.share-qr-code.component-spec + (:require [quo.components.share.share-qr-code.view :as share-qr-code] + [test-helpers.component :as h])) + +(def qr-label "Text shown below QR") + +(def default-props + {:qr-data qr-label + :qr-image-uri "test-uri" + :full-name "Default name"}) + +(def profile-props + {:type :profile + :profile-picture 123}) + +(def text-press-event + {:test-name "Text pressed" + :accessibility-label :share-qr-code-info-text + :event-name :press + :callback-prop-key :on-text-press}) + +(def text-long-press-event + {:test-name "Text long pressed" + :accessibility-label :share-qr-code-info-text + :event-name :long-press + :callback-prop-key :on-text-long-press}) + +(def share-press-event + {:test-name "Share button" + :accessibility-label :link-to-profile + :event-name :press + :callback-prop-key :on-share-press}) + +(defn render-share-qr-code + [props] + ;; NOTE: rendering the profile type first, so that the re-render is triggered. + ;; Passing a :key breaks it for some reason. + (let [component-rendered (h/render [share-qr-code/view + (merge + default-props + profile-props + {:qr-data "initial-render"})]) + rerender-fn (h/get-rerender-fn component-rendered) + share-qr-code (h/get-by-label-text :share-qr-code)] + ;; Fires on-layout since it's needed to render the content + (h/fire-event :layout share-qr-code #js {:nativeEvent #js {:layout #js {:width 500}}}) + (rerender-fn [share-qr-code/view props]))) + +(h/describe "Share QR Code component" + (h/describe "Renders share-qr-code component in all types" + (h/test "Profile" + (render-share-qr-code (merge default-props + profile-props)) + (h/is-truthy (h/get-by-text qr-label))) + + (h/test "Wallet Legacy" + (render-share-qr-code (assoc default-props + :type + :wallet)) + (h/is-truthy (h/get-by-text qr-label))) + + (h/test "Saved address" + (render-share-qr-code (assoc default-props + :type + :saved-address)) + (h/is-truthy (h/get-by-text qr-label))) + + + (h/test "Watched address" + (render-share-qr-code (assoc default-props + :type + :watched-address)) + (h/is-truthy (h/get-by-text qr-label)))) + + (h/describe "Fires all events for all types" + (letfn [(test-fire-events [props test-seq] + (doseq [{:keys [test-name event-name + callback-prop-key + accessibility-label]} test-seq + :let [event-fn (h/mock-fn)]] + (h/test test-name + (render-share-qr-code (assoc props callback-prop-key event-fn)) + (h/fire-event event-name (h/get-by-label-text accessibility-label)) + (h/was-called-times event-fn 1))))] + + (h/describe "Profile" + (test-fire-events + (merge default-props + profile-props) + [text-press-event + text-long-press-event + share-press-event])) + + (h/describe "Wallet Legacy" + (test-fire-events + (assoc default-props + :type :wallet + :emoji "👻") + [text-press-event + text-long-press-event + share-press-event])) + + (h/describe "Saved Address" + (test-fire-events + (assoc default-props + :type + :saved-address) + [text-press-event + text-long-press-event + share-press-event])) + + (h/describe "Watched Address" + (test-fire-events + (assoc default-props + :type :watched-address + :emoji "👽") + [text-press-event + text-long-press-event + share-press-event]))))) diff --git a/src/quo/components/share/share_qr_code/schema.cljs b/src/quo/components/share/share_qr_code/schema.cljs new file mode 100644 index 00000000000..20fec14f1fc --- /dev/null +++ b/src/quo/components/share/share_qr_code/schema.cljs @@ -0,0 +1,52 @@ +(ns quo.components.share.share-qr-code.schema) + +(def ?base + [:map + [:type [:enum :profile :wallet :saved-address :watched-address]] + [:full-name :string] + [:qr-image-uri :string] + [:qr-data :string] + [:on-text-press {:optional true} [:maybe fn?]] + [:on-text-long-press {:optional true} [:maybe fn?]] + [:on-share-press {:optional true} [:maybe fn?]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:unblur-on-android? {:optional true} [:maybe :boolean]]]) + +(def ?emoji :string) + +(def ?profile + [:map + [:type [:= :profile]] + [:profile-picture [:maybe :schema.quo/profile-picture-source]]]) + +(def ?channel + [:map + [:type [:= :channel]] + [:emoji {:optional true} [:maybe ?emoji]]]) + +(def ?wallet + [:map + [:type [:= :wallet]] + [:emoji {:optional true} [:maybe ?emoji]]]) + +(def ?saved-address + [:map + [:type [:= :saved-address]]]) + +(def ?watched-address + [:map + [:type [:= :watched-address]] + [:emoji {:optional true} [:maybe ?emoji]]]) + +(def ?schema + [:=> + [:catn + [:props + [:multi {:dispatch :type} + [:channel [:merge ?base ?channel]] + [:profile [:merge ?base ?profile]] + [:wallet [:merge ?base ?wallet]] + [:saved-address [:merge ?base ?saved-address]] + [:watched-address [:merge ?base ?watched-address]]]]] + :any]) + diff --git a/src/quo/components/share/share_qr_code/style.cljs b/src/quo/components/share/share_qr_code/style.cljs new file mode 100644 index 00000000000..388a4f082eb --- /dev/null +++ b/src/quo/components/share/share_qr_code/style.cljs @@ -0,0 +1,55 @@ +(ns quo.components.share.share-qr-code.style + (:require [quo.foundations.colors :as colors])) + +(def outer-container + {:border-radius 16 + :width "100%" + :overflow :hidden}) + +(def overlay-color colors/white-opa-5) + +(def ^:private padding-horizontal 12) + +(def content-container + {:z-index 1 + :padding-horizontal padding-horizontal + :padding-top 12 + :padding-bottom 8}) + +;;; QR code +(defn qr-code-size + [total-width] + (- total-width (* 2 padding-horizontal))) + +(def share-qr-container + {:flex-direction :row + :justify-content :space-between + :margin-bottom 20}) + +(def share-qr-inner-container + {:flex-direction :row + :align-items :center}) + +;;; Bottom part +(def bottom-container + {:margin-top 8 + :flex-direction :row + :justify-content :space-between}) + +(def title {:color colors/white-opa-40}) + +(def share-button-size 32) +(def ^:private share-button-gap 16) + +(defn share-button-container + [alignment] + {:justify-content (if (= alignment :top) :flex-start :center) + :margin-left share-button-gap}) + +(defn data-text + [total-width] + {:width (- total-width (* 2 padding-horizontal) share-button-size share-button-gap)}) + +;;; Wallet variants +(def watched-account-icon + {:margin-left 4}) diff --git a/src/quo/components/share/share_qr_code/view.cljs b/src/quo/components/share/share_qr_code/view.cljs new file mode 100644 index 00000000000..c57b03e9b0b --- /dev/null +++ b/src/quo/components/share/share_qr_code/view.cljs @@ -0,0 +1,156 @@ +(ns quo.components.share.share-qr-code.view + (:require [clojure.set] + [oops.core :as oops] + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.channel-avatar.view :as channel-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [quo.components.buttons.button.view :as button] + [quo.components.gradient.gradient-cover.view :as gradient-cover] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.share.qr-code.view :as qr-code] + [quo.components.share.share-qr-code.schema :as component-schema] + [quo.components.share.share-qr-code.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(defn- info-label + [share-qr-code-type] + [text/text {:size :paragraph-2 :weight :medium :style style/title} + (when (= share-qr-code-type :profile) + (i18n/label :t/link-to-profile))]) + +(defn- info-text + [{:keys [width on-press on-long-press ellipsize?]} qr-data-text] + [rn/pressable + {:accessibility-label :share-qr-code-info-text + :style (style/data-text width) + :on-press on-press + :on-long-press on-long-press} + [text/text + (cond-> {:size :paragraph-1 + :weight :monospace} + ellipsize? (assoc :number-of-lines 1 + :ellipsize-mode :middle)) + qr-data-text]]) + +(defn- share-button + [{:keys [alignment on-press]}] + [rn/view {:style (style/share-button-container alignment)} + [button/button + {:icon-only? true + :type :grey + :background :blur + :size style/share-button-size + :accessibility-label :link-to-profile + :on-press on-press} + :i/share]]) + +(defn- profile-bottom + [{:keys [component-width qr-data on-text-press on-text-long-press share-qr-type]}] + [rn/view + [info-label share-qr-type] + [info-text + {:width component-width + :ellipsize? true + :on-press on-text-press + :on-long-press on-text-long-press} + qr-data]]) + +(defn- wallet-bottom + [{:keys [component-width qr-data on-text-press on-text-long-press]}] + [info-text + {:width component-width + :on-press on-text-press + :on-long-press on-text-long-press} + qr-data]) + +(defn- header-icon + [{:keys [share-qr-type customization-color emoji profile-picture full-name]}] + (case share-qr-type + :profile [user-avatar/user-avatar + {:size :small + :status-indicator? false + :profile-picture profile-picture + :customization-color customization-color}] + (:wallet :watched-address) [account-avatar/view + {:customization-color customization-color + :emoji emoji + :size 32}] + :saved-address [wallet-user-avatar/wallet-user-avatar + {:size :size-32 + :customization-color customization-color + :full-name full-name}] + :channel [channel-avatar/view + {:size :size-32 + :emoji emoji + :locked-state :not-set + :customization-color customization-color + :full-name full-name}] + nil)) + +(defn- share-qr-code + [{:keys [share-qr-type qr-image-uri component-width customization-color full-name + profile-picture emoji on-share-press] + :as props}] + [:<> + [gradient-cover/view {:customization-color customization-color :height 463}] + [rn/view {:style style/content-container} + [rn/view {:style style/share-qr-container} + [rn/view {:style style/share-qr-inner-container} + [header-icon props] + [text/text + {:size :heading-2 + :weight :semi-bold + :style {:margin-left 8}} full-name] + (when (= share-qr-type :watched-address) + [icons/icon + :i/reveal + {:color colors/white-opa-40 + :container-style style/watched-account-icon}])] + [share-button {:on-press on-share-press}]] + [quo.context/provider {:theme :light} + [qr-code/view + {:qr-image-uri qr-image-uri + :size (style/qr-code-size component-width) + :avatar (case share-qr-type + :profile :profile + (:watched-address :wallet) :wallet-account + :saved-address :saved-address + :channel :channel + nil) + :customization-color customization-color + :full-name full-name + :profile-picture profile-picture + :emoji emoji}]] + [rn/view {:style style/bottom-container} + (case share-qr-type + :profile + [profile-bottom props] + (:wallet :watched-address :saved-address) + [wallet-bottom props] + nil)]]]) + +(defn- view-internal + [{provided-width :width :as props}] + (let [[calculated-width + set-component-width] (rn/use-state nil) + on-layout (rn/use-callback #(set-component-width + (oops/oget % "nativeEvent.layout.width"))) + props (-> props + (assoc :component-width (or provided-width calculated-width)) + (clojure.set/rename-keys {:type :share-qr-type}))] + [quo.context/provider {:theme :dark} + [rn/view + {:accessibility-label :share-qr-code + :style style/outer-container + :on-layout (when-not provided-width on-layout)} + [rn/view {:style {:background-color style/overlay-color}} + (when (:component-width props) + [share-qr-code props])]]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/slideshow/slider_bar/component_spec.cljs b/src/quo/components/slideshow/slider_bar/component_spec.cljs new file mode 100644 index 00000000000..bc9cc1e8d0c --- /dev/null +++ b/src/quo/components/slideshow/slider_bar/component_spec.cljs @@ -0,0 +1,25 @@ +(ns quo.components.slideshow.slider-bar.component-spec + (:require + [quo.components.slideshow.slider-bar.view :as slider-bar] + [test-helpers.component :as h])) + +(h/describe "Slideshow: slider-bar" + (h/test "default render" + (h/render-with-theme-provider [slider-bar/view {:accessibility-label :slider-bar}]) + (h/is-truthy (h/query-by-label-text :slider-bar))) + + (h/test "render with total-amount and active-index" + (h/render-with-theme-provider [slider-bar/view + {:total-amount 4 + :active-index 1 + :accessibility-label :slider-bar}]) + (h/is-truthy (h/query-by-label-text :slider-bar)) + (h/is-equal 4 (count (h/query-all-by-label-text :slide-bar-item)))) + + (h/test "render with total-amount active-index and possible scroll" + (h/render-with-theme-provider [slider-bar/view + {:total-amount 10 + :active-index 7 + :accessibility-label :slider-bar}]) + (h/is-truthy (h/query-by-label-text :slider-bar)) + (h/is-equal 10 (count (h/query-all-by-label-text :slide-bar-item))))) diff --git a/src/quo/components/slideshow/slider_bar/schema.cljs b/src/quo/components/slideshow/slider_bar/schema.cljs new file mode 100644 index 00000000000..e479cc950d6 --- /dev/null +++ b/src/quo/components/slideshow/slider_bar/schema.cljs @@ -0,0 +1,14 @@ +(ns quo.components.slideshow.slider-bar.schema) + +(def ?schema + [:=> + [:cat + [:map {:closed true} + [:total-amount {:optional true} [:maybe :int]] + [:active-index {:optional true} :int] + [:customization-color {:optional true} + [:maybe :schema.common/customization-color]] + [:blur? {:optional true} [:maybe :boolean]] + [:accessibility-label {:optional true} [:maybe :keyword]] + [:container-style {:optional true} [:maybe :map]]]] + :any]) diff --git a/src/quo/components/slideshow/slider_bar/style.cljs b/src/quo/components/slideshow/slider_bar/style.cljs new file mode 100644 index 00000000000..504a1e289d5 --- /dev/null +++ b/src/quo/components/slideshow/slider_bar/style.cljs @@ -0,0 +1,31 @@ +(ns quo.components.slideshow.slider-bar.style + (:require + [quo.foundations.colors :as colors])) + +(def list-wrapper + {:align-items :center}) + +(defn list-bar + [bar-width] + {:width bar-width}) + +(defn item-wrapper + [{:keys [size spacing]}] + {:width size + :height size + :margin-left spacing + :flex 1 + :align-items :center + :justify-content :center}) + +(defn item + [{:keys [size spacing active? customization-color theme blur?]}] + {:width size + :height size + :border-radius spacing + :background-color + (cond + (and blur? active?) colors/white + (and blur? (not active?)) colors/white-opa-10 + (and (not blur?) active?) (colors/resolve-color customization-color theme) + :else (colors/theme-colors colors/neutral-20 colors/neutral-80 theme))}) diff --git a/src/quo/components/slideshow/slider_bar/view.cljs b/src/quo/components/slideshow/slider_bar/view.cljs new file mode 100644 index 00000000000..960ec699f5e --- /dev/null +++ b/src/quo/components/slideshow/slider_bar/view.cljs @@ -0,0 +1,150 @@ +(ns quo.components.slideshow.slider-bar.view + (:require + [quo.components.slideshow.slider-bar.schema :as component-schema] + [quo.components.slideshow.slider-bar.style :as style] + [quo.context] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [schema.core :as schema])) + +(def item-size 8) +(def item-spacing item-size) +(def max-length 6) +(def bar-width (* (+ item-spacing item-size) max-length)) + +(def micro-scale 0.25) +(def small-scale 0.5) +(def medium-scale 0.75) +(def default-scale 1) + +(def items-before-scroll + (/ max-length 2)) + +(defn- calc-relative-index + "Calculates item index in visible area" + [index active-index total-length] + (let [last-index (dec total-length)] + (cond + (< active-index items-before-scroll) index + (> active-index + (- last-index items-before-scroll)) (+ (- index total-length) max-length) + :else (+ (- index active-index) items-before-scroll)))) + +(defn- calc-first-item-scale + "Calculates scale for first item in visible area" + [active-index] + (cond + (= active-index items-before-scroll) medium-scale + (> active-index items-before-scroll) small-scale + :else default-scale)) + +(defn- calc-second-item-scale + "Calculates scale for second item in visible area" + [active-index] + (if (> active-index items-before-scroll) + medium-scale + default-scale)) + +(defn- calc-last-item-scale + "Calculates scale for last item in visible area" + [active-index total-length] + (let [last-index (dec total-length)] + (if (> active-index (- last-index items-before-scroll)) + medium-scale + small-scale))) + +(defn- calc-last-but-one-item-scale + "Calculates scale for before last item in visible area" + [active-index total-length] + (if (> active-index (- (dec total-length) items-before-scroll)) + default-scale + medium-scale)) + +(defn- get-scale + [index active-index total-length] + (let [relative-index (calc-relative-index index active-index total-length)] + (if (or (= index active-index) + (< total-length max-length)) + default-scale + (cond + (< relative-index 0) micro-scale + (= relative-index 0) (calc-first-item-scale active-index) + (= relative-index 1) (calc-second-item-scale active-index) + (= relative-index 4) (calc-last-but-one-item-scale active-index total-length) + (= relative-index 5) (calc-last-item-scale active-index total-length) + (> relative-index 5) micro-scale + :else + default-scale)))) + +(defn- bar-item + [index _ _ {:keys [active-index total-length customization-color theme blur?]}] + (let [active? (= index active-index) + shared-scale (reanimated/use-shared-value (get-scale index active-index total-length))] + (rn/use-effect (fn [] + (let [new-scale-value (get-scale index active-index total-length)] + (reanimated/animate shared-scale new-scale-value))) + [index active-index total-length]) + [rn/view + {:key index + :style (style/item-wrapper {:size item-size + :spacing item-spacing})} + [reanimated/view + {:accessibility-label :slide-bar-item + :style + (reanimated/apply-animations-to-style + {:transform [{:scale shared-scale}]} + (style/item {:size item-size + :spacing item-spacing + :active? active? + :customization-color customization-color + :theme theme + :blur? blur?}))}]])) + +(defn- get-item-layout + [_ index] + (let [length (+ item-size item-spacing)] + #js {:length length + :index index + :offset (* index length)})) + +(defn- view-internal + [{:keys [customization-color blur? accessibility-label container-style] + :as props}] + (let [active-index (or (:active-index props) 0) + total-amount (or (:total-amount props) 1) + theme (quo.context/use-theme) + flat-list-ref (rn/use-ref-atom nil) + set-flat-list-ref (rn/use-callback #(reset! flat-list-ref %)) + center-position 0.5 + data (range total-amount) + scroll-to-index (rn/use-callback + (fn [] + (some-> ^js @flat-list-ref + (.scrollToIndex #js {:animated true + :index active-index + :viewOffset item-spacing + :viewPosition center-position}))) + [flat-list-ref active-index])] + (rn/use-effect scroll-to-index [active-index]) + [rn/view + {:style (merge style/list-wrapper container-style) + :accessibility-label accessibility-label} + [reanimated/flat-list + {:style (style/list-bar bar-width) + :data data + :scroll-enabled false + :ref set-flat-list-ref + :shows-horizontal-scroll-indicator false + :bounces false + :horizontal true + :extra-data (str active-index) + :render-data {:active-index active-index + :total-length total-amount + :customization-color customization-color + :theme theme + :blur? blur?} + :get-item-layout get-item-layout + :render-fn bar-item + :key-fn identity}]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/tabs/account_selector.cljs b/src/quo/components/tabs/account_selector.cljs new file mode 100644 index 00000000000..6391d31710c --- /dev/null +++ b/src/quo/components/tabs/account_selector.cljs @@ -0,0 +1,71 @@ +(ns quo.components.tabs.account-selector + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.markdown.text :as quo] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(def themes + {:light {:default {:bg colors/neutral-10 + :account-text colors/neutral-100 + :label-text colors/neutral-50} + :transparent {:bg colors/neutral-80-opa-5 + :account-text colors/neutral-100 + :label-text colors/neutral-80-opa-40}} + + :dark {:default {:bg colors/neutral-80-opa-80 + :account-text colors/white + :label-text colors/neutral-40} + :transparent {:bg colors/white-opa-5 + :account-text colors/white + :label-text colors/neutral-40}}}) + +(defn account-container-row + [background-color] + {:padding-vertical 4 + :flex-direction :row + :align-items :center + :background-color background-color + :border-radius 12}) + +(def account-avatar-container + {:margin-left 4 + :margin-right 8}) + +(defn get-color-by-type + [type k theme] + (get-in themes [theme type k])) + +(defn account-selector + "[account-selector opts] + opts + {:show-label? true/false ;; hide or show the label + :transparent? true/false ;; implement transparent background styles + :style style ;; any other styling can be passed + :label-text \"Label\" ;; content to show where the label should be shown + :account-text \"My Savings\" ;; content in place of account name + }" + [{:keys [show-label? account-text account-emoji transparent? label-text style]}] + (let [theme (quo.context/use-theme) + background-color (get-color-by-type (if transparent? :transparent :default) :bg theme) + account-text-color (get-color-by-type (if transparent? :transparent :default) + :account-text + theme) + label-text-color (get-color-by-type (if transparent? :transparent :default) :label-text theme)] + [rn/view {:style style} + (when show-label? + [quo/text + {:weight :medium + :size :paragraph-2 + :style {:color label-text-color + :margin-bottom 8}} + label-text]) + [rn/view {:style (account-container-row background-color)} + [rn/view {:style account-avatar-container} + [account-avatar/view {:emoji account-emoji :size 32}]] + [quo/text + {:weight :medium + :size :paragraph-1 + :style {:color account-text-color}} + account-text]]])) diff --git a/src/quo/components/tabs/segmented_tab.cljs b/src/quo/components/tabs/segmented_tab.cljs new file mode 100644 index 00000000000..e2cc7fe53ac --- /dev/null +++ b/src/quo/components/tabs/segmented_tab.cljs @@ -0,0 +1,53 @@ +(ns quo.components.tabs.segmented-tab + (:require + [quo.components.tabs.tab.view :as tab] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(def themes-for-blur + {:light {:background-color colors/neutral-80-opa-5} + :dark {:background-color colors/white-opa-5}}) + +(def themes + {:light {:background-color colors/neutral-10} + :dark {:background-color colors/neutral-90}}) + +(defn segmented-control + [{:keys [data size blur? container-style item-container-style + active-item-container-style default-active on-change]}] + (let [theme (quo.context/use-theme) + [active-tab-id + set-active-tab-id] (rn/use-state default-active) + on-press (rn/use-callback + (fn [tab-id] + (set-active-tab-id tab-id) + (when on-change (on-change tab-id))) + [on-change])] + [rn/view + (merge + {:flex-direction :row + :background-color (get-in (if blur? themes-for-blur themes) + [theme :background-color]) + :border-radius (case size + 32 10 + 28 10 + 24 8 + 20 6) + :padding 2} + container-style) + (for [[indx {:keys [label id]}] (map-indexed vector data)] + ^{:key id} + [rn/view + {:margin-left (if (= 0 indx) 0 2) + :flex 1} + [tab/view + {:id id + :active-item-container-style active-item-container-style + :item-container-style item-container-style + :segmented? true + :size size + :blur? blur? + :active (= id active-tab-id) + :on-press on-press} + label]])])) diff --git a/src/quo/components/tabs/tab/style.cljs b/src/quo/components/tabs/tab/style.cljs new file mode 100644 index 00000000000..b735648bffd --- /dev/null +++ b/src/quo/components/tabs/tab/style.cljs @@ -0,0 +1,108 @@ +(ns quo.components.tabs.tab.style + (:require + [quo.foundations.colors :as colors])) + +(def tab-background-opacity 0.3) + +(defn size->padding-left + [size] + (case size + 32 12 + 28 12 + 24 8 + 20 8 + nil)) + +(defn size->border-radius + [size] + (case size + 32 10 + 28 8 + 24 8 + 20 6 + nil)) + +(def notification-dot + {:position :absolute + :top -2 + :right -2}) + +(def container + {:flex-direction :row}) + +(defn tab + [{:keys [background-color + disabled + segmented? + show-notification-dot? + size]}] + (let [border-radius (size->border-radius size) + padding (size->padding-left size)] + (merge {:height size + :align-items :center + :justify-content :center + :flex-direction :row + :border-top-left-radius border-radius + :border-bottom-left-radius border-radius + :background-color background-color + :padding-left padding} + ;; The minimum padding right of 1 is a mandatory workaround. Without + ;; it, the SVG rendered besides the tab will have a 1px margin. This + ;; issue still exists in the latest react-native-svg versions. + (if show-notification-dot? + {:padding-right 1} + {:border-radius border-radius + :padding-right padding}) + (when segmented? + {:flex 1}) + (when disabled + {:opacity tab-background-opacity})))) + +(def themes + {:light {:default {:background-color colors/neutral-10 + :icon-color colors/neutral-50 + :label {:style {:color colors/neutral-100}}} + :active {:background-color colors/neutral-50 + :icon-color colors/white + :label {:style {:color colors/white}}} + :disabled {:background-color colors/neutral-10 + :icon-color colors/neutral-50 + :label {:style {:color colors/neutral-100}}}} + :dark {:default {:background-color colors/neutral-90 + :icon-color colors/neutral-40 + :label {:style {:color colors/white}}} + :active {:background-color colors/neutral-60 + :icon-color colors/white + :label {:style {:color colors/white}}} + :disabled {:background-color colors/neutral-90 + :icon-color colors/neutral-40 + :label {:style {:color colors/white}}}}}) + +(def themes-for-blur-background + {:light {:default {:background-color colors/neutral-80-opa-5 + :icon-color colors/neutral-80-opa-40 + :label {:style {:color colors/neutral-100}}} + :active {:background-color colors/neutral-80-opa-60 + :icon-color colors/white + :label {:style {:color colors/white}}} + :disabled {:background-color colors/neutral-80-opa-5 + :icon-color colors/neutral-80-opa-40 + :label {:style {:color colors/neutral-100}}}} + :dark {:default {:background-color colors/white-opa-5 + :icon-color colors/white + :label {:style {:color colors/white}}} + :active {:background-color colors/white-opa-20 + :icon-color colors/white + :label {:style {:color colors/white}}} + :disabled {:background-color colors/white-opa-5 + :icon-color colors/neutral-40 + :label {:style {:color colors/white}}}}}) + +(defn by-theme + [{:keys [disabled active blur? theme]}] + (let [state (cond disabled :disabled + active :active + :else :default)] + + (get-in (if blur? themes-for-blur-background themes) + [theme state]))) diff --git a/src/quo/components/tabs/tab/view.cljs b/src/quo/components/tabs/tab/view.cljs new file mode 100644 index 00000000000..4e1525080c3 --- /dev/null +++ b/src/quo/components/tabs/tab/view.cljs @@ -0,0 +1,95 @@ +(ns quo.components.tabs.tab.view + (:require + [quo.components.common.notification-dot.view :as notification-dot] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tabs.tab.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.svg :as svg])) + +(defn- right-side-with-cutout + "SVG exported from Figma." + [{:keys [height width background-color disabled]}] + ;; Do not add a view-box property, it'll cause an artifact where the SVG is + ;; rendered slightly smaller than the proper width and height. + [svg/svg + {:width width + :height height + :fill background-color + :fill-opacity (when disabled style/tab-background-opacity)} + [svg/path + {:d + "M 11.468 6.781 C 11.004 6.923 10.511 7 10 7 C 7.239 7 5 4.761 5 2 C 5 + 1.489 5.077 0.996 5.219 0.532 C 4.687 0.351 4.134 0.213 3.564 0.123 C 2.787 + 0 1.858 0 0 0 L 0 32 C 1.858 32 2.787 32 3.564 31.877 C 7.843 31.199 11.199 + 27.843 11.877 23.564 C 12 22.787 12 21.858 12 20 L 12 12 C 12 10.142 12 + 9.213 11.877 8.436 C 11.787 7.866 11.649 7.313 11.468 6.781 Z" + :clip-path "url(#clip0_5514_84289)"}] + [svg/defs + [svg/clip-path {:id "clip0_5514_84289"} + [svg/rect {:width width :height height :fill :none}]]]]) + +(defn- content + [{:keys [size label]} children] + [rn/view + (cond + (string? children) + [text/text + (merge {:size (case size + 24 :paragraph-2 + 20 :label + nil) + :weight :medium + :number-of-lines 1} + label) + children] + + (vector? children) + children)]) + +(defn view + [{:keys [accessibility-label active before item-container-style active-item-container-style blur? + disabled id on-press segmented? size notification-dot? customization-color] + :or {size 32}} + children] + (let [theme (quo.context/use-theme) + show-notification-dot? (and notification-dot? (= size 32)) + {:keys [icon-color + background-color + label]} (style/by-theme {:theme theme + :blur? blur? + :disabled disabled + :active active})] + [rn/pressable + (merge {:disabled disabled + :accessibility-label accessibility-label + :style style/container} + (when on-press + {:on-press (fn [] + (on-press id))})) + (when show-notification-dot? + [notification-dot/view + {:style style/notification-dot + :customization-color customization-color}]) + [rn/view + {:style (merge + (style/tab + {:size size + :background-color (if (and segmented? (not active)) + :transparent + background-color) + :disabled disabled + :segmented? segmented? + :show-notification-dot? show-notification-dot?}) + (if active active-item-container-style item-container-style))} + (when before + [rn/view + [icons/icon before {:color icon-color}]]) + [content {:size size :label label} children]] + (when show-notification-dot? + [right-side-with-cutout + {:width (style/size->padding-left size) + :height size + :disabled disabled + :background-color background-color}])])) diff --git a/src/quo/components/tabs/tabs/schema.cljs b/src/quo/components/tabs/tabs/schema.cljs new file mode 100644 index 00000000000..e7d35323e69 --- /dev/null +++ b/src/quo/components/tabs/tabs/schema.cljs @@ -0,0 +1,32 @@ +(ns quo.components.tabs.tabs.schema) + +(def ^:private ?data + [:sequential + [:maybe + [:map + [:id [:or :int :keyword [:set :int]]] + [:label [:maybe :string]] + [:accessibility-label {:optional true} [:maybe [:or :keyword :string]]] + [:notification-dot? {:optional true} [:maybe :boolean]]]]]) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:default-active {:optional true} [:maybe [:or :int :keyword]]] + [:active-tab-id {:optional true} [:maybe [:or :int :keyword]]] + [:data ?data] + [:fade-end-percentage {:optional true} [:or :double :string]] + [:fade-end? {:optional true} [:maybe :boolean]] + [:blur? {:optional true} [:maybe :boolean]] + [:on-change {:optional true} [:maybe fn?]] + [:on-scroll {:optional true} [:maybe fn?]] + [:scroll-on-press? {:optional true} [:maybe :boolean]] + [:scrollable? {:optional true} [:maybe :boolean]] + [:style {:optional true} [:maybe :map]] + [:container-style {:optional true} [:maybe :map]] + [:size {:optional true} [:maybe [:or :keyword :int]]] + [:in-scroll-view? {:optional true} [:maybe :boolean]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]] + :any]) diff --git a/src/quo/components/tabs/tabs/style.cljs b/src/quo/components/tabs/tabs/style.cljs new file mode 100644 index 00000000000..3cb767c2c87 --- /dev/null +++ b/src/quo/components/tabs/tabs/style.cljs @@ -0,0 +1,13 @@ +(ns quo.components.tabs.tabs.style + (:require + [utils.number])) + +(def linear-gradient + {:width "100%" + :height "100%"}) + +(defn tab + [{:keys [size default-tab-size number-of-items index style]}] + {:margin-right (if (= size default-tab-size) 12 8) + :padding-right (when (= index (dec number-of-items)) + (:padding-left style))}) diff --git a/src/quo/components/tabs/tabs/view.cljs b/src/quo/components/tabs/tabs/view.cljs new file mode 100644 index 00000000000..696af6c07ad --- /dev/null +++ b/src/quo/components/tabs/tabs/view.cljs @@ -0,0 +1,193 @@ +(ns quo.components.tabs.tabs.view + (:require + [oops.core :refer [oget]] + [quo.components.tabs.tab.view :as tab] + [quo.components.tabs.tabs.schema :as component-schema] + [quo.components.tabs.tabs.style :as style] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.linear-gradient :as linear-gradient] + [react-native.masked-view :as masked-view] + [reagent.core :as reagent] + [schema.core :as schema] + [utils.collection :as utils.collection] + [utils.number])) + +(def default-tab-size 32) +(def unread-count-offset 3) + +(defn- calculate-fade-end-percentage + [{:keys [offset-x content-width layout-width max-fade-percentage]}] + (let [fade-percentage (max max-fade-percentage + (/ (+ layout-width offset-x) + content-width))] + ;; Truncate to avoid unnecessary rendering. + (if (> fade-percentage 0.99) + 0.99 + max-fade-percentage))) + +(defn- masked-view-wrapper + [{:keys [fade-end-percentage fade-end?]} & children] + (if fade-end? + (into [masked-view/masked-view + {:mask-element + (reagent/as-element + [linear-gradient/linear-gradient + {:colors [:black :transparent] + :locations [fade-end-percentage 1] + :start {:x 0 :y 0} + :end {:x 1 :y 0} + :pointer-events :none + :style style/linear-gradient}])}] + children) + (into [:<>] children))) + +(defn- on-scroll-handler + [{:keys [on-scroll fading set-fading fade-end-percentage fade-end?]} ^js e] + (when fade-end? + (let [offset-x (oget e "nativeEvent.contentOffset.x") + content-width (oget e "nativeEvent.contentSize.width") + layout-width (oget e "nativeEvent.layoutMeasurement.width") + new-percentage (calculate-fade-end-percentage + {:offset-x offset-x + :content-width content-width + :layout-width layout-width + :max-fade-percentage fade-end-percentage})] + ;; Avoid unnecessary re-rendering. + (when (not= new-percentage (get fading :fade-end-percentage)) + (set-fading new-percentage)))) + (when on-scroll + (on-scroll e))) + +(defn- tab-view + [{:keys [id label notification-dot? accessibility-label]} + index _ + {:keys [active-tab-id + blur? + customization-color + number-of-items + on-press + size + style]}] + [rn/view + {:style (style/tab {:size size + :default-tab-size default-tab-size + :number-of-items number-of-items + :index index + :style style})} + [tab/view + {:id id + :notification-dot? notification-dot? + :customization-color customization-color + :accessibility-label accessibility-label + :size size + :blur? blur? + :active (= id active-tab-id) + :on-press #(on-press % index)} + label]]) + +(defn- view-internal + [{:keys [default-active data fade-end-percentage fade-end? on-change on-scroll scroll-on-press? + scrollable? style container-style size blur? in-scroll-view? customization-color + active-tab-id] + :or {fade-end-percentage 0.8 + fade-end? false + scrollable? false + scroll-on-press? false + size default-tab-size} + :as props}] + (let [[active-tab-internal-id + set-active-tab-internal-id] (rn/use-state default-active) + tab-id (or active-tab-id active-tab-internal-id) + + [fading set-fading] (rn/use-state fade-end-percentage) + flat-list-ref (rn/use-ref-atom nil) + tabs-data (rn/use-memo (fn [] (filterv some? data)) + [data]) + clean-props (dissoc props + :default-active + :fade-end-percentage + :fade-end? + :on-change + :scroll-on-press? + :size) + on-scroll (rn/use-callback + (partial on-scroll-handler + {:fade-end-percentage fade-end-percentage + :fade-end? fade-end? + :fading fading + :set-fading set-fading + :on-scroll on-scroll}) + [fade-end? fading]) + set-initial-scroll-poisition (rn/use-callback + (fn [] + (reagent/next-tick + (fn [] + (.scrollToIndex + @flat-list-ref + #js + {:animated false + :index + (utils.collection/first-index + #(= tab-id (:id %)) + tabs-data)})))) + [tab-id tabs-data]) + on-tab-press (rn/use-callback (fn [id index] + (set-active-tab-internal-id id) + (when (and scroll-on-press? @flat-list-ref) + (.scrollToIndex ^js @flat-list-ref + #js + {:animated true + :index index + :viewPosition 0.5})) + (when on-change + (on-change id))) + [set-active-tab-internal-id scroll-on-press? + on-change])] + (if scrollable? + [rn/view {:style {:margin-top (- (dec unread-count-offset))}} + [masked-view-wrapper + {:fade-end-percentage fading :fade-end? fade-end?} + [(if in-scroll-view? + gesture/flat-list + rn/flat-list) + (merge + clean-props + {:ref #(reset! flat-list-ref %) + :style style + ;; The padding-top workaround is needed because on Android + ;; {:overflow :visible} doesn't work on components inheriting + ;; from ScrollView (e.g. FlatList). There are open issues, here's just one about this + ;; topic: Https://github.com/facebook/react-native/issues/31218 + :content-container-style + (assoc container-style :padding-top (dec unread-count-offset)) + :horizontal true + :scroll-event-throttle 64 + :shows-horizontal-scroll-indicator false + :data tabs-data + :key-fn (comp str :id) + :on-scroll-to-index-failed identity + :on-scroll on-scroll + :on-layout set-initial-scroll-poisition + :render-fn tab-view + :render-data {:active-tab-id tab-id + :blur? blur? + :customization-color customization-color + :number-of-items (count tabs-data) + :size size + :on-press on-tab-press + :style style}})]]] + [rn/view (merge style {:flex-direction :row}) + (map-indexed (fn [index item] + ^{:key (:id item)} + [tab-view item index nil + {:active-tab-id tab-id + :blur? blur? + :customization-color customization-color + :number-of-items (count tabs-data) + :size size + :on-press on-tab-press + :style style}]) + tabs-data)]))) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/tags/base_tag.cljs b/src/quo/components/tags/base_tag.cljs new file mode 100644 index 00000000000..80649af30f1 --- /dev/null +++ b/src/quo/components/tags/base_tag.cljs @@ -0,0 +1,36 @@ +(ns quo.components.tags.base-tag + (:require + [react-native.core :as rn])) + +(defn style-container + [size disabled border-color border-width background-color labelled? type] + (merge {:height size + :border-color border-color + :background-color background-color + :border-width border-width + :border-radius size + :align-items :center + :justify-content :center} + (when disabled + {:opacity 0.3}) + (when (and (or (= type :icon) (= type :emoji)) (not labelled?)) + {:width size}))) + +(defn base-tag + [{:keys [id size disabled? border-color border-width background-color on-press + accessibility-label type labelled?] + :or {size 32}} + children] + [rn/pressable + (merge {:disabled disabled? + :accessibility-label accessibility-label + :style (style-container size + disabled? + border-color + border-width + background-color + labelled? + type)} + (when on-press + {:on-press #(on-press id)})) + children]) diff --git a/src/quo/components/tags/collectible_tag/component_spec.cljs b/src/quo/components/tags/collectible_tag/component_spec.cljs new file mode 100644 index 00000000000..d16023b380a --- /dev/null +++ b/src/quo/components/tags/collectible_tag/component_spec.cljs @@ -0,0 +1,52 @@ +(ns quo.components.tags.collectible-tag.component-spec + (:require + [quo.components.tags.collectible-tag.view :as collectible-tag] + [test-helpers.component :as h])) + +(def collectible-name "Collectible") +(def collectible-id "#123") + +(defn get-test-data + [{:keys [options blur? size]}] + {:collectible-name collectible-name + :collectible-id collectible-id + :collectible-img-src {:uri "https://example.com/image.jpg"} + :options options + :blur? (or blur? false) + :size (or size :size-24)}) + +(h/describe "Collectible_tag tests" + (h/test "Renders Default option" + (let [data (get-test-data {})] + (h/render-with-theme-provider [collectible-tag/view data]) + (h/is-truthy (h/get-by-text collectible-name)))) + + (h/test "Renders Add option" + (let [data (get-test-data {:options :add})] + (h/render-with-theme-provider [collectible-tag/view data]) + (h/is-truthy (h/get-by-text collectible-name)))) + + (h/test "Renders Hold option" + (let [data (get-test-data {:options :hold})] + (h/render-with-theme-provider [collectible-tag/view data]) + (h/is-truthy (h/get-by-text collectible-name)))) + + (h/test "Renders with Blur" + (let [data (get-test-data {:blur? true})] + (h/render-with-theme-provider [collectible-tag/view data]) + (h/is-truthy (h/get-by-text collectible-name)))) + + (h/test "Renders without Blur" + (let [data (get-test-data {:blur? false})] + (h/render-with-theme-provider [collectible-tag/view data]) + (h/is-truthy (h/get-by-text collectible-name)))) + + (h/test "Renders with Size 24" + (let [data (get-test-data {:size :size-24})] + (h/render-with-theme-provider [collectible-tag/view data]) + (h/is-truthy (h/get-by-text collectible-name)))) + + (h/test "Renders with Size 32" + (let [data (get-test-data {:size :size-32})] + (h/render-with-theme-provider [collectible-tag/view data]) + (h/is-truthy (h/get-by-text collectible-name))))) diff --git a/src/quo/components/tags/collectible_tag/schema.cljs b/src/quo/components/tags/collectible_tag/schema.cljs new file mode 100644 index 00000000000..a2292584fbc --- /dev/null +++ b/src/quo/components/tags/collectible_tag/schema.cljs @@ -0,0 +1,14 @@ +(ns quo.components.tags.collectible-tag.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:options {:optional true} [:maybe [:enum :add :hold]]] + [:size {:optional true} [:maybe [:enum :size-24 :size-32]]] + [:blur? {:optional true} [:maybe :boolean]] + [:collectible-img-src :schema.common/image-source] + [:collectible-name :string] + [:collectible-id {:optional true} [:maybe :string]]]]] + :any]) diff --git a/src/quo/components/tags/collectible_tag/style.cljs b/src/quo/components/tags/collectible_tag/style.cljs new file mode 100644 index 00000000000..37fddc472a6 --- /dev/null +++ b/src/quo/components/tags/collectible_tag/style.cljs @@ -0,0 +1,50 @@ +(ns quo.components.tags.collectible-tag.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [size options blur? theme] + (let [hold? (= options :hold)] + (merge {:background-color (if blur? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)) + :flex-direction :row + :align-items :center + :padding-left 2 + :border-width (if hold? 1 0) + :border-radius 0 + :border-color (colors/resolve-color :success theme)} + (condp = size + :size-24 {:height (if hold? 26 24) + :padding-right 10 + :border-radius 8} + :size-32 {:height (if hold? 34 32) + :padding-right 12 + :border-radius 10})))) + +(defn options-icon + [size] + (assoc + (condp = size + :size-24 {:right -8 + :top -8} + :size-32 {:right -6 + :top -6}) + :position + :absolute)) + +(defn collectible-img + [size] + (condp = size + :size-24 {:width 20 + :height 20 + :margin-right 6 + :border-radius 6} + :size-32 {:width 28 + :height 28 + :margin-right 8 + :border-radius 8})) + +(defn label + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) diff --git a/src/quo/components/tags/collectible_tag/view.cljs b/src/quo/components/tags/collectible_tag/view.cljs new file mode 100644 index 00000000000..562b5e09d26 --- /dev/null +++ b/src/quo/components/tags/collectible_tag/view.cljs @@ -0,0 +1,57 @@ +(ns quo.components.tags.collectible-tag.view + (:require + [oops.core :as oops] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tags.collectible-tag.schema :as component-schema] + [quo.components.tags.collectible-tag.style :as style] + [quo.context] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [schema.core :as schema])) + +(defn- view-internal + [{:keys [options blur? collectible-img-src collectible-name collectible-id] :as props}] + (let [theme (quo.context/use-theme) + [container-width + set-container-width] (rn/use-state 0) + on-layout (rn/use-callback + #(set-container-width (oops/oget % :nativeEvent :layout :width))) + size (or (:size props) :size-24)] + [rn/view {:on-layout on-layout} + [hole-view/hole-view + {:holes (if options + [{:x (- container-width + (case size + :size-24 10 + :size-32 12 + nil)) + :y (case size + :size-24 -6 + :size-32 -4 + nil) + :width 16 + :height 16 + :borderRadius 8}] + [])} + [rn/view {:style (style/container size options blur? theme)} + [rn/image {:style (style/collectible-img size) :source collectible-img-src}] + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/label theme)} + collectible-name] + (when collectible-id + [text/text + {:size :paragraph-2 + :weight :medium + :margin-left 5 + :style (style/label theme)} + collectible-id])]] + (when options + [rn/view {:style (style/options-icon size)} + [icons/icon (if (= options :hold) :i/hold :i/add-token) + {:size 20 + :no-color true}]])])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/tags/context_tag/schema.cljs b/src/quo/components/tags/context_tag/schema.cljs new file mode 100644 index 00000000000..e4fd74ea0d9 --- /dev/null +++ b/src/quo/components/tags/context_tag/schema.cljs @@ -0,0 +1,110 @@ +(ns quo.components.tags.context-tag.schema + (:require [malli.core :as malli])) + +(def ^:private ?context-base + [:map + [:type {:optional true} + [:maybe + [:enum :default :multiuser :group :channel :community :token :network :multinetwork :account + :collectible :address :icon :audio :wallet-user :dapp]]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:container-style {:optional true} [:maybe :map]] + [:blur? {:optional true} [:maybe :boolean]] + [:state {:optional true} [:maybe [:enum :selected :default]]]]) + +(def ^:private ?size + [:map + [:size {:optional true} [:maybe [:enum 24 32]]]]) + +(def ^:private ?default + [:map + [:profile-picture {:optional true} [:maybe :schema.quo/profile-picture-source]] + [:full-name {:optional true} [:maybe :string]]]) + +(def ^:private ?multiuser + [:map + [:users {:optional true} + [:maybe + [:sequential + [:map + [:profile-picture {:optional true} [:maybe :schema.quo/profile-picture-source]] + [:full-name {:optional true} [:maybe :string]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]]]]) + +(def ^:private ?group + [:map + [:group-name {:optional true} [:maybe :string]]]) + +(def ^:private ?channel + [:map + [:community-name {:optional true} [:maybe :string]] + [:community-logo {:optional true} [:maybe :schema.common/image-source]] + [:channel-name {:optional true} [:maybe :string]]]) + +(def ^:private ?community + [:map + [:community-name {:optional true} [:maybe :string]]]) + +(def ^:private ?token + [:map + [:amount {:optional true} [:maybe [:or :string :int]]] + [:token {:optional true} [:maybe :string]]]) + +(def ^:private ?network + [:map + [:network-logo {:optional true} [:maybe :schema.common/image-source]] + [:network-name {:optional true} [:maybe [:or :string :keyword]]]]) + +(def ^:private ?multinetwork + [:map + [:networks {:optional true} [:maybe [:sequential ?network]]]]) + +(def ^:private ?account + [:map + [:account-name {:optional true} [:maybe :string]] + [:emoji {:optional true} [:maybe :string]]]) + +(def ^:private ?collectible + [:map + [:collectible {:optional true} [:maybe :schema.common/image-source]] + [:collectible-name {:optional true} [:maybe :string]] + [:collectible-number {:optional true} [:maybe [:or :string :int]]]]) + +(def ^:private ?address + [:map + [:address {:optional true} [:maybe :string]]]) + +(def ^:private ?icon + [:map + [:icon {:optional true} [:maybe :keyword]] + [:context {:optional true} [:maybe :string]]]) + +(def ^:private ?audio + [:map + [:duration {:optional true} [:maybe :string]]]) + +(def ^:private ?wallet-user + [:map + [:full-name {:optional true} [:maybe :string]]]) + +(def ?schema + [:=> + [:catn + [:props + [:multi {:dispatch :type} + [::malli/default [:merge ?default ?size ?context-base]] + [:default [:merge ?default ?size ?context-base]] + [:multiuser [:merge ?multiuser ?context-base]] + [:group [:merge ?group ?size ?context-base]] + [:channel [:merge ?channel ?size ?context-base]] + [:community [:merge ?community ?size ?context-base]] + [:token [:merge ?token ?size ?context-base]] + [:network [:merge ?network ?context-base]] + [:multinetwork [:merge ?multinetwork ?context-base]] + [:account [:merge ?account ?size ?context-base]] + [:collectible [:merge ?collectible ?size ?context-base]] + [:address [:merge ?address ?size ?context-base]] + [:icon [:merge ?icon ?size ?context-base]] + [:audio [:merge ?audio ?context-base]] + [:wallet-user [:merge ?wallet-user ?size ?context-base]]]]] + :any]) diff --git a/src/quo/components/tags/context_tag/style.cljs b/src/quo/components/tags/context_tag/style.cljs new file mode 100644 index 00000000000..0e3ba67f3c1 --- /dev/null +++ b/src/quo/components/tags/context_tag/style.cljs @@ -0,0 +1,113 @@ +(ns quo.components.tags.context-tag.style + (:require + [quo.foundations.colors :as colors])) + +(defn context-tag-icon-color + [theme blur?] + (if blur? + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn audio-tag-icon-container + [customization-color theme] + {:width 20 + :height 20 + :border-radius 10 + :align-items :center + :justify-content :center + :background-color (colors/resolve-color customization-color theme)}) + +(def audio-tag-icon-color colors/white) + +(defn container + [{:keys [theme type size state blur? customization-color]}] + (let [background-color (if blur? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)) + border-radius (cond + (not (#{:account :collectible} type)) 16 + (= size 24) 8 + :else 10) + border-color (colors/resolve-color customization-color theme)] + (cond-> {:padding 2 + :padding-right 8 + :flex-direction :row + :align-items :center + :height size + :background-color background-color + :border-radius border-radius + :flex-shrink 1} + (= state :selected) (assoc :height (+ size 2) + :border-color border-color + :border-width 1)))) + +(defn tag-container + [size] + {:margin-right (if (= size 24) 6 10) + :flex-direction :row + :flex-shrink 1 + :align-items :center}) + +(defn tag-spacing + [size shrinkable?] + (cond-> {:margin-left (if (= size 24) 4 8)} + shrinkable? (assoc :flex-shrink 1))) + +(defn text + ([theme] + (text theme false)) + ([theme gray-text?] + {:color (if gray-text? + colors/neutral-50 + (colors/theme-colors colors/neutral-100 colors/white theme))})) + +(defn token-logo + [size] + {:border-radius (if (= size 24) 10 14)}) + +(defn circle-logo + [size] + (if (= size 24) + {:width 20 :height 20 :border-radius 10} + {:width 28 :height 28 :border-radius 14})) + +(defn rounded-logo + [size] + (if (= size 24) + {:width 20 + :height 20 + :border-radius 6} + {:width 28 + :height 28 + :border-radius 8})) + +(defn address + [size] + (if (= size 24) + {:margin-horizontal 6 + :margin-vertical 1 + :flex-direction :row + :align-items :center} + {:margin-horizontal 10 + :margin-vertical 3 + :flex-direction :row + :align-items :center})) + +(defn icon + [size] + (if (= size 24) + {:margin-horizontal 6 + :margin-vertical 1 + :flex-direction :row + :align-items :center} + {:margin-left 8 + :margin-right 10 + :margin-vertical 3 + :flex-direction :row + :align-items :center})) + +(defn icon-spacing + [size] + (if (= size 24) + {:margin-left 4} + {:margin-left 2})) diff --git a/src/quo/components/tags/context_tag/view.cljs b/src/quo/components/tags/context_tag/view.cljs new file mode 100644 index 00000000000..2c4e07339c4 --- /dev/null +++ b/src/quo/components/tags/context_tag/view.cljs @@ -0,0 +1,201 @@ +(ns quo.components.tags.context-tag.view + (:require + [clojure.string :as string] + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.group-avatar.view :as group-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [quo.components.icon :as icons] + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.schema :as component-schema] + [quo.components.tags.context-tag.style :as style] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [schema.core :as schema])) + +(defn- tag-skeleton + [{:keys [size text theme shrinkable? gray-text?] + :or {size 24 + gray-text? false + theme (quo.context/use-theme)}} + logo-component] + [rn/view {:style (style/tag-container size)} + logo-component + [rn/view {:style (style/tag-spacing size shrinkable?)} + [text/text + {:style (style/text theme gray-text?) + :weight :medium + :size (if (= size 24) :paragraph-2 :paragraph-1) + :number-of-lines 1 + :ellipsize-mode :middle} + text]]]) + +(defn- communities-tag + [{:keys [size community-logo community-name blur? channel? channel-name]}] + (let [theme (quo.context/use-theme) + text-size (if (= size 24) :paragraph-2 :paragraph-1) + icon-size (if (= size 24) 16 20)] + [rn/view {:style (style/tag-container size)} + [fast-image/fast-image {:style (style/circle-logo size) :source community-logo}] + [rn/view {:style (style/tag-spacing size false)} + [text/text + {:style (style/text theme) + :weight :medium + :size text-size} + community-name]] + (when channel? + [:<> + [icons/icon :i/chevron-right + {:color (style/context-tag-icon-color theme blur?) + :size icon-size}] + [text/text + {:style (style/text theme) + :weight :medium + :size text-size} + (str "# " channel-name)]])])) + +(defn- trim-public-key + [pk] + (str (subs pk 0 5) "..." (subs pk (- (count pk) 3)))) + +(defn- address-tag + [{:keys [size address]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/address size)} + [text/text + {:style (style/text theme) + :weight :monospace ;; TODO: fix this style (issue #17009) + :size (if (= size 24) :paragraph-2 :paragraph-1)} + (trim-public-key address)]])) + +(defn- icon-tag + [{:keys [size icon blur? context]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/icon size)} + [icons/icon icon + {:color (style/context-tag-icon-color theme blur?) + :size (if (= size 24) 12 20)}] + [rn/view {:style (style/icon-spacing size)} + [text/text + {:style (style/text theme) + :weight :medium + :size (if (= size 24) :paragraph-2 :paragraph-1)} + context]]])) + +(defn- view-internal + [{:keys [type theme size state blur? customization-color profile-picture full-name users + group-name amount token network-logo network-name networks + account-name emoji collectible collectible-name collectible-number + dapp-name dapp-logo duration container-style] + :or {customization-color :blue + type :default + state :default + theme (quo.context/use-theme)} + :as props}] + (let [[image-error? set-image-error] (rn/use-state false)] + [rn/view {:style [{:align-items :flex-start} container-style]} + [rn/view + {:style (style/container {:theme theme + :type type + :size size + :state state + :blur? blur? + :customization-color customization-color}) + :accessibility-label :context-tag} + (case type + :default + [tag-skeleton {:theme theme :size size :text full-name} + [user-avatar/user-avatar + {:full-name full-name + :profile-picture profile-picture + :size (if (= size 24) :xxs 28) + :status-indicator? false + :customization-color customization-color}]] + + :multiuser + [preview-list/view {:type :user :size :size-20} + users] + + :multinetwork + [preview-list/view {:type :network :size :size-20} + networks] + + :audio + [tag-skeleton {:theme theme :text (str duration)} + [rn/view {:style (style/audio-tag-icon-container customization-color theme)} + [icons/icon :i/play {:color style/audio-tag-icon-color :size 12}]]] + + :group + [tag-skeleton {:theme theme :size size :text group-name} + [group-avatar/view + {:icon-name :i/members + :size (if (= size 24) :size-20 :size-28) + :customization-color (colors/resolve-color customization-color :light)}]] + + (:channel :community) + [communities-tag (assoc props :channel? (= type :channel))] + + :token + [tag-skeleton {:theme theme :size size :text (str amount " " token)} + [token/view + {:style (style/token-logo size) + :token token + :size (if (= size 24) :size-20 :size-28)}]] + + :network + [tag-skeleton {:theme theme :size size :text network-name} + [rn/image {:style (style/circle-logo size) :source network-logo}]] + + :collectible + (let [gray-text? (string/blank? collectible-name) + collectible-text (cond + gray-text? "UNKNOWN" + collectible-number (str collectible-name + " #" + collectible-number) + :else collectible-name) + nft-placeholder? (or image-error? (string/blank? collectible))] + [tag-skeleton + {:theme theme + :size size + :text collectible-text + :shrinkable? true + :gray-text? gray-text?} + (if nft-placeholder? + [icons/icon :i/nft {:size 20}] + [rn/image + {:style (style/rounded-logo size) + :source collectible + :on-error #(set-image-error true)}])]) + + :account + [tag-skeleton {:theme theme :size size :text account-name} + [account-avatar/view + {:customization-color customization-color + :emoji emoji + :size (if (= size 24) 20 28)}]] + + :address + [address-tag props] + + :icon + [icon-tag props] + + :wallet-user + [tag-skeleton {:theme theme :size size :text full-name} + [wallet-user-avatar/wallet-user-avatar + {:full-name full-name + :size (if (= size 24) :size-20 :size-24) + :customization-color customization-color}]] + + :dapp + [tag-skeleton {:theme theme :size size :text dapp-name} + [rn/image {:style (style/circle-logo size) :source dapp-logo}]] + + nil)]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/tags/network_status_tag/component_spec.cljs b/src/quo/components/tags/network_status_tag/component_spec.cljs new file mode 100644 index 00000000000..cb52000fbad --- /dev/null +++ b/src/quo/components/tags/network_status_tag/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo.components.tags.network-status-tag.component-spec + (:require + [quo.components.tags.network-status-tag.view :as network-status-tag] + [test-helpers.component :as h])) + +(h/describe "Network status component test" + (h/test "5 min ago render" + (h/render [network-status-tag/view + {:label "Updated 5 min ago"}]) + (h/is-truthy (h/get-by-text "Updated 5 min ago"))) + (h/test "15 min ago render" + (h/render [network-status-tag/view + {:label "Updated 15 min ago"}]) + (h/is-truthy (h/get-by-text "Updated 15 min ago")))) diff --git a/src/quo/components/tags/network_status_tag/style.cljs b/src/quo/components/tags/network_status_tag/style.cljs new file mode 100644 index 00000000000..b033846d28d --- /dev/null +++ b/src/quo/components/tags/network_status_tag/style.cljs @@ -0,0 +1,28 @@ +(ns quo.components.tags.network-status-tag.style + (:require + [quo.foundations.colors :as colors])) + +(def main + {:justify-content :center + :align-items :center + :height 24}) + +(defn inner + [theme] + {:border-width 1 + :border-radius 12 + :flex 1 + :flex-direction :row + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :align-items :center + :padding-horizontal 8}) + +(def label + {:color colors/warning-50 + :margin-left 6}) + +(def dot + {:width 8 + :height 8 + :border-radius 8 + :background-color colors/warning-50}) diff --git a/src/quo/components/tags/network_status_tag/view.cljs b/src/quo/components/tags/network_status_tag/view.cljs new file mode 100644 index 00000000000..c6926586ed8 --- /dev/null +++ b/src/quo/components/tags/network_status_tag/view.cljs @@ -0,0 +1,19 @@ +(ns quo.components.tags.network-status-tag.view + (:require + [quo.components.markdown.text :as text] + [quo.components.tags.network-status-tag.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [label]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/main} + [rn/view {:style (style/inner theme)} + [rn/view {:style style/dot}] + [text/text + {:style style/label + :weight :medium + :size :label + :align :center} + label]]])) diff --git a/src/quo/components/tags/network_tags/component_spec.cljs b/src/quo/components/tags/network_tags/component_spec.cljs new file mode 100644 index 00000000000..7c1aa2ee478 --- /dev/null +++ b/src/quo/components/tags/network_tags/component_spec.cljs @@ -0,0 +1,20 @@ +(ns quo.components.tags.network-tags.component-spec + (:require + [quo.components.tags.network-tags.view :as network-tags] + [test-helpers.component :as h])) + +(h/describe "network-tags component" + (h/test "renders network tags with single network" + (h/render [network-tags/view + {:title "Network Tags" + :networks [{:source "network-icon.png"}]}]) + (h/is-truthy (h/get-by-text "Network Tags"))) + + (h/test "renders network tags with multiple networks" + (h/render [network-tags/view + {:title "Multiple Networks" + :networks [{:source "network-icon1.png"} + {:source "network-icon2.png"} + {:source "network-icon3.png"}] + :size :size-32}]) + (h/is-truthy (h/get-by-text "Multiple Networks")))) diff --git a/src/quo/components/tags/network_tags/style.cljs b/src/quo/components/tags/network_tags/style.cljs new file mode 100644 index 00000000000..673c32e6860 --- /dev/null +++ b/src/quo/components/tags/network_tags/style.cljs @@ -0,0 +1,58 @@ +(ns quo.components.tags.network-tags.style + (:require + [quo.foundations.colors :as colors])) + +(defn default-border-colors + [theme] + (colors/theme-colors + colors/neutral-20 + colors/neutral-80 + theme)) + +(defn container + [{:keys [status theme blur?]}] + {:flex-direction :row + :align-self :flex-start + :background-color (condp = status + :error (colors/theme-colors + (colors/override-color :danger 10 50) + (colors/override-color :danger 10 60) + theme) + :warning (colors/theme-colors + (colors/override-color :warning 10 50) + (colors/override-color :warning 10 60) + theme) + nil) + :border-width 1 + :border-color (condp = status + :error (colors/theme-colors + (colors/override-color :danger 20 50) + (colors/override-color :danger 20 60) + theme) + + :warning (colors/theme-colors + (colors/override-color :warning 20 50) + (colors/override-color :warning 20 60) + theme) + + :default (if blur? + (colors/theme-colors + colors/neutral-80-opa-5 + colors/white-opa-5 + theme) + (default-border-colors theme)) + (default-border-colors theme)) + :border-radius 8 + :padding-left 5 + :padding-right 5 + :padding-top 3 + :padding-bottom 2}) + +(defn title-style + [{:keys [status theme networks-shown?]}] + {:padding-left (if networks-shown? 4 0) + :margin-top -1 + :color (condp = status + :error (colors/resolve-color :danger theme) + :warning (colors/resolve-color :warning theme) + nil)}) diff --git a/src/quo/components/tags/network_tags/view.cljs b/src/quo/components/tags/network_tags/view.cljs new file mode 100644 index 00000000000..e877e394ff0 --- /dev/null +++ b/src/quo/components/tags/network_tags/view.cljs @@ -0,0 +1,29 @@ +(ns quo.components.tags.network-tags.view + (:require + [quo.components.list-items.preview-list.view :as preview-list] + [quo.components.markdown.text :as text] + [quo.components.tags.network-tags.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [title networks status blur? container-style] :or {status :default}}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge (style/container {:status status + :theme theme + :blur? blur?}) + container-style)} + (when networks + [preview-list/view + {:type :network + :number (count networks) + :size :size-16} + networks]) + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/title-style {:status status + :theme theme + :networks-shown? networks})} + title]])) diff --git a/src/quo/components/tags/number_tag/component_spec.cljs b/src/quo/components/tags/number_tag/component_spec.cljs new file mode 100644 index 00000000000..f13589b707f --- /dev/null +++ b/src/quo/components/tags/number_tag/component_spec.cljs @@ -0,0 +1,21 @@ +(ns quo.components.tags.number-tag.component-spec + (:require + [quo.components.tags.number-tag.view :as number-tag] + [test-helpers.component :as h])) + +(h/describe + "number tag component test" + (h/test "+3 render" + (h/render [number-tag/view + {:type :rounded + :number "3" + :size :size-32 + :blur? false}]) + (h/is-truthy (h/get-by-text "+3"))) + (h/test "+48 render" + (h/render [number-tag/view + {:type :squared + :number "48" + :size :size-24 + :blur? true}]) + (h/is-truthy (h/get-by-text "+48")))) diff --git a/src/quo/components/tags/number_tag/style.cljs b/src/quo/components/tags/number_tag/style.cljs new file mode 100644 index 00000000000..1d1fff83061 --- /dev/null +++ b/src/quo/components/tags/number_tag/style.cljs @@ -0,0 +1,66 @@ +(ns quo.components.tags.number-tag.style + (:require + [quo.foundations.colors :as colors])) + +(def sizes + {:size-32 {:size 32 + :width-extra 40 + :border-radius {:rounded 16 :squared 10} + :icon-size 20} + :size-24 {:size 24 + :width-extra 32 + :border-radius {:rounded 12 :squared 8} + :icon-size 16} + :size-20 {:size 20 + :width-extra 24 + :border-radius {:rounded 10 :squared 8} + :icon-size 12} + :size-16 {:size 16 + :width-extra 20 + :border-radius {:rounded 8 :squared 8} + :icon-size 12} + :size-14 {:size 14 + :width-extra 16 + :border-radius {:rounded 7 :squared 7} + :icon-size 12}}) + +(defn get-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-70 + colors/white-opa-70 + theme) + (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme))) + +(defn get-bg-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-5 + colors/white-opa-5 + theme) + (colors/theme-colors colors/neutral-20 + colors/neutral-90 + theme))) + +(defn get-shape-value + [size attribute shape] + (let [shapes (get-in sizes [size attribute])] + (when shapes (or (shape shapes) (first (vals shapes)))))) + +(defn get-width + [size number] + (let [size-value (get-in sizes [size :size]) + widen? (and (> size-value 20) (= (count number) 2))] + (get-in sizes [size (if widen? :width-extra :size)]))) + +(defn container + [{:keys [type number size blur? container-style]} theme] + {:style (assoc container-style + :width (get-width size number) + :height (get-in sizes [size :size]) + :border-radius (get-shape-value size :border-radius type) + :justify-content :center + :align-items :center + :background-color (get-bg-color blur? theme))}) diff --git a/src/quo/components/tags/number_tag/view.cljs b/src/quo/components/tags/number_tag/view.cljs new file mode 100644 index 00000000000..c1c1d537d0f --- /dev/null +++ b/src/quo/components/tags/number_tag/view.cljs @@ -0,0 +1,25 @@ +(ns quo.components.tags.number-tag.view + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tags.number-tag.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [number size blur?] :as props}] + (let [theme (quo.context/use-theme) + size-value (get-in style/sizes [size :size]) + icon-size (get-in style/sizes [size :icon-size])] + [rn/view (style/container props theme) + (if (and (> size-value 20) (< (count number) 3)) + [text/text + {:size (if (= size :size-32) + :paragraph-2 + :label) + :weight :medium + :style {:color (style/get-color blur? theme)}} + (str "+" number)] + [icons/icon :i/options + {:size icon-size + :color (style/get-color blur? theme)}])])) diff --git a/src/quo/components/tags/permission_tag.cljs b/src/quo/components/tags/permission_tag.cljs new file mode 100644 index 00000000000..8ac4a237ed1 --- /dev/null +++ b/src/quo/components/tags/permission_tag.cljs @@ -0,0 +1,177 @@ +(ns quo.components.tags.permission-tag + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tags.base-tag :as base-tag] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn outer-resource-container + [size background-color] + {:background-color background-color + :border-radius size + :width size + :height size + :margin-left (case size + 32 -12 + 24 -8) + :align-items :center + :justify-content :center}) + +(defn extra-count-styles + [size theme] + {:background-color (colors/theme-colors + colors/neutral-20 + colors/neutral-70 + theme) + :height (case size + 32 28 + 24 20) + :width (case size + 32 28 + 24 20) + :border-radius size + :justify-content :center + :align-items :center}) + +(defn extra-count + [total-group-count selected-count size background-color] + (let [theme (quo.context/use-theme) + extra-group-count (- total-group-count selected-count)] + (when (> extra-group-count 0) + [rn/view (outer-resource-container size background-color) + [rn/view (extra-count-styles size theme) + (if (< extra-group-count 4) + [text/text + {:size :label + :style {:align-items :center + :color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}} + (str "+" extra-group-count)] + [icons/icon :i/pending-default + {:container-style {:align-items :center + :justify-content :center} + :color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme) + :size 12}])]]))) + +(defn selected-token-count + [group] + (cond + (= (count group) 3) 3 + + (> (count group) 3) 2 + + :else + (count group))) + +(defn token-group + [] + (fn [{:keys [group size last-group background-color] + :or {size 24}}] + (let [theme (quo.context/use-theme) + tokens-count (count group) + selected-tokens (take (selected-token-count group) group)] + [rn/view + {:flex-direction :row + :align-items :center} + (for [{:keys [token-icon id]} selected-tokens] + ^{:key id} + [rn/view {:flex-direction :row} + [rn/view (outer-resource-container size background-color) + [rn/image + {:source token-icon + :style {:height (case size + 32 28 + 24 20) + :width (case size + 32 28 + 24 20) + :border-radius size}}]]]) + + [extra-count tokens-count (count selected-tokens) size] + (when-not last-group + [rn/view {:align-items :center} + [text/text + {:weight :medium + :size (case size + 32 :paragraph-2 + 24 :label) + :style {:color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme) + :padding-left 4 + :text-transform :lowercase + :padding-right (case size + 32 16 + 24 12)}} + "or"]])]))) + +(defn selected-group-count + [tokens] + (cond + (> (count tokens) 3) 3 + + :else + (count tokens))) + +(defn tag-tokens + [] + (fn [{:keys [tokens size background-color] + :or {size 24}}] + (let [selected-groups (take (selected-group-count tokens) tokens) + last-group-id ((last selected-groups) :id)] + [rn/view + {:flex-direction :row + :align-items :center} + (for [{:keys [id group]} selected-groups] + ^{:key id} + [token-group + {:group group + :size size + :last-group (if (= id last-group-id) true false) + :background-color background-color}])]))) + +(defn tag + [{:keys [locked? tokens size background-color on-press accessibility-label] + :or {size 24}}] + (let [theme (quo.context/use-theme)] + [base-tag/base-tag + {:accessibility-label accessibility-label + :background-color background-color + :size size + :type :permission + :on-press on-press} + [rn/view + {:flex-direction :row + :align-items :center + :justify-content :flex-end} + [rn/view + {:padding-left (case 32 + 8 24 + 6) + :padding-right (case size + 32 16 + 24 12)} + [icons/icon + (if locked? + :i/locked + :i/unlocked) + {:resize-mode :center + :size (case size + 32 20 + 24 16) + :color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}]] + [tag-tokens + {:tokens tokens + :size size + :background-color background-color}]]])) diff --git a/src/quo/components/tags/status_tags.cljs b/src/quo/components/tags/status_tags.cljs new file mode 100644 index 00000000000..e050ae3327e --- /dev/null +++ b/src/quo/components/tags/status_tags.cljs @@ -0,0 +1,130 @@ +(ns quo.components.tags.status-tags + (:require + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(def default-container-style + {:border-radius 20 + :border-width 1}) + +(def small-container-style + (merge default-container-style + {:min-height 26 + :padding-horizontal 8 + :padding-vertical 3})) + +(def large-container-style + (merge default-container-style + {:min-height 32 + :padding-horizontal 11 + :padding-vertical 4})) + +(defn base-tag + [_] + (fn + [{:keys [size + border-color + background-color + icon + text-color + label + accessibility-label + container-style]}] + (let [paragraph-size (if (= size :small) :paragraph-2 :paragraph-1)] + [rn/view + {:accessible true + :accessibility-label accessibility-label + :style (merge + (assoc (if (= size :small) + small-container-style + large-container-style) + :align-self :flex-start + :border-width 1 + :border-color border-color + :background-color background-color) + container-style)} + [rn/view + {:flex-direction :row + :align-items :center + :flex 1} + [rn/view + {:style {:justify-content :center + :align-items :center}} + (when icon + [icon/icon + icon + {:no-color true + :size (if (= size :large) 20 12)}])] + [text/text + {:size paragraph-size + :weight :medium + :style {:padding-left (if icon 5 0) + :color text-color}} label]]]))) + +(defn- positive + [size theme label _ no-icon? container-style] + [base-tag + {:accessibility-label :status-tag-positive + :container-style container-style + :size size + :icon (when-not no-icon? :i/positive-state) + :background-color colors/success-50-opa-10 + :border-color colors/success-50-opa-20 + :label label + ;; The positive tag uses the same color for `light` and `dark blur` variant + :text-color (if (= theme :dark) colors/success-60 colors/success-50)}]) + +(defn- negative + [size theme label _ no-icon? container-style] + [base-tag + {:accessibility-label :status-tag-negative + :container-style container-style + :size size + :icon (when-not no-icon? :i/negative-state) + :background-color colors/danger-50-opa-10 + :border-color colors/danger-50-opa-20 + :label label + ;; The negative tag uses the same color for `dark` and `dark blur` variant + :text-color (colors/theme-colors colors/danger-50 colors/danger-60 theme)}]) + +(defn- pending + [size theme label blur? no-icon? container-style] + [base-tag + {:accessibility-label :status-tag-pending + :container-style container-style + :size size + :label label + :icon (when-not no-icon? + (if blur? + :i/pending-dark-blur + (if (= theme :light) :i/pending-light :i/pending-dark))) + :background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-10 colors/neutral-80-opa-40 theme)) + :border-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)) + + :text-color (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}]) + +(defn status-tag + [{:keys [status size label blur? no-icon? container-style]}] + (when status + (let [theme (quo.context/use-theme)] + (when-let [status-component (case (:type status) + :positive positive + :negative negative + :pending pending + nil)] + [status-component + size + theme + label + blur? + no-icon? + container-style])))) diff --git a/src/quo/components/tags/status_tags_component_spec.cljs b/src/quo/components/tags/status_tags_component_spec.cljs new file mode 100644 index 00000000000..ed2455c86f1 --- /dev/null +++ b/src/quo/components/tags/status_tags_component_spec.cljs @@ -0,0 +1,34 @@ +(ns quo.components.tags.status-tags-component-spec + (:require + [quo.components.tags.status-tags :as quo] + [test-helpers.component :as h])) + +(defn render-status-tag + [opts] + (h/render [quo/status-tag opts])) + +(h/describe "status tag component" + (h/test "renders status tag with positive type" + (render-status-tag {:status {:type :positive} + :label "Positive" + :size :small}) + (-> (h/expect (h/get-all-by-label-text :status-tag-positive)) + (.toBeTruthy)) + (-> (h/expect (h/get-by-text "Positive")) + (.toBeTruthy))) + (h/test "renders status tag with negative type" + (render-status-tag {:status {:type :negative} + :label "Negative" + :size :small}) + (-> (h/expect (h/get-all-by-label-text :status-tag-negative)) + (.toBeTruthy)) + (-> (h/expect (h/get-by-text "Negative")) + (.toBeTruthy))) + (h/test "renders status tag with pending type" + (render-status-tag {:status {:type :pending} + :label "Pending" + :size :small}) + (-> (h/expect (h/get-all-by-label-text :status-tag-pending)) + (.toBeTruthy)) + (-> (h/expect (h/get-by-text "Pending")) + (.toBeTruthy)))) diff --git a/src/quo/components/tags/summary_tag/component_spec.cljs b/src/quo/components/tags/summary_tag/component_spec.cljs new file mode 100644 index 00000000000..13795e34a0c --- /dev/null +++ b/src/quo/components/tags/summary_tag/component_spec.cljs @@ -0,0 +1,54 @@ +(ns quo.components.tags.summary-tag.component-spec + (:require + [quo.components.tags.summary-tag.view :as summary-tag] + [test-helpers.component :as h])) + +(h/describe "Summary tag component tests" + (h/test "Basic render" + (h/render [summary-tag/view]) + (h/is-truthy (h/query-by-label-text :container))) + + (h/test "Account view render" + (h/render [summary-tag/view + {:type :account + :label "Alice's Account" + :customization-color "#ff0000" + :emoji "🎉"}]) + (h/is-truthy (h/get-by-text "Alice's Account"))) + + (h/test "Network view render" + (h/render [summary-tag/view + {:type :network + :label "Ethereum Network" + :image-source "path/to/ethereum-logo.png"}]) + (h/is-truthy (h/get-by-text "Ethereum Network"))) + + (h/test "Saved address view render" + (h/render [summary-tag/view + {:type :saved-address + :label "Charlie's Wallet" + :customization-color "#00ff00"}]) + (h/is-truthy (h/get-by-text "Charlie's Wallet"))) + + (h/test "Collectible view render" + (h/render [summary-tag/view + {:type :collectible + :label "Rare Artifact" + :image-source "path/to/artifact-image.png"}]) + (h/is-truthy (h/get-by-text "Rare Artifact"))) + + (h/test "User view render" + (h/render-with-theme-provider + [summary-tag/view + {:type :user + :label "Bob Smith" + :image-source "path/to/profile-pic.png" + :customization-color "#0000ff"}]) + (h/is-truthy (h/get-by-text "Bob Smith"))) + + (h/test "Token view render" + (h/render [summary-tag/view + {:type :token + :label "1,000 SNT" + :token :eth}]) + (h/is-truthy (h/get-by-text "1,000 SNT")))) diff --git a/src/quo/components/tags/summary_tag/style.cljs b/src/quo/components/tags/summary_tag/style.cljs new file mode 100644 index 00000000000..92ae774dd9e --- /dev/null +++ b/src/quo/components/tags/summary_tag/style.cljs @@ -0,0 +1,39 @@ +(ns quo.components.tags.summary-tag.style + (:require + [quo.foundations.colors :as colors])) + +(defn main + [{:keys [type customization-color]} theme] + {:justify-content :flex-start + :align-items :center + :height 32 + :padding-left 4 + :padding-right 10 + :flex-direction :row + :flex-shrink 1 + :border-radius (if (#{:account :collectible} type) 10 16) + :background-color (colors/resolve-color customization-color theme 10)}) + +(defn label + [type theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :flex-grow 1 + :margin-left (if (= type :address) 6 4)}) + +(def collectible-image + {:width 24 + :height 24 + :border-radius 8}) + +(def network + {:width 24 + :height 24 + :border-radius 12}) + +(def dapp + {:width 24 + :height 24 + :border-radius 12}) + +(def token-image + {:border-radius 12}) diff --git a/src/quo/components/tags/summary_tag/view.cljs b/src/quo/components/tags/summary_tag/view.cljs new file mode 100644 index 00000000000..36cb58e8acd --- /dev/null +++ b/src/quo/components/tags/summary_tag/view.cljs @@ -0,0 +1,77 @@ +(ns quo.components.tags.summary-tag.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [quo.components.markdown.text :as text] + [quo.components.tags.summary-tag.style :as style] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- left-view + [{:keys [label type customization-color emoji image-source token]}] + (case type + :account + [account-avatar/view + {:customization-color customization-color + :size 24 + :emoji emoji + :type :default}] + :network + [rn/image + {:source image-source + :style style/network}] + :dapp + [rn/image + {:source image-source + :style style/dapp}] + :saved-address + [wallet-user-avatar/wallet-user-avatar + {:full-name label + :size :size-24 + :customization-color customization-color}] + :collectible + [rn/image + {:source image-source + :style style/collectible-image}] + :user + [user-avatar/user-avatar + {:full-name label + :size :xs + :profile-picture image-source + :customization-color customization-color}] + :token + [token/view + {:token token + :size :size-24 + :style style/token-image}] + nil)) + +(defn view + "Options: + - :label - string - tag label + - :customization-color - color - It will be passed down to components that + should vary based on a custom color. + - :type - :token / :user / :collectible / :saved-address / :network / :account / :dapp + - :emoji - string - emoji used for displaying account avatar + - :image-source - resource - image to display on :network, :collectible and :user + - :theme - :light / :dark" + [{:keys [label customization-color type container-style] + :as props + :or {customization-color colors/neutral-80-opa-5}}] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :container + :style (merge (style/main (assoc props :customization-color customization-color) + theme) + container-style)} + [left-view props] + [text/text + {:style (style/label type theme) + :ellipsize-mode :tail + :weight :semi-bold + :number-of-lines 1 + :size :heading-1} + label]])) diff --git a/src/quo/components/tags/tag.cljs b/src/quo/components/tags/tag.cljs new file mode 100644 index 00000000000..6f807a11236 --- /dev/null +++ b/src/quo/components/tags/tag.cljs @@ -0,0 +1,115 @@ +(ns quo.components.tags.tag + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tags.base-tag :as base-tag] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(def themes + {:light {:default {:border-color colors/neutral-20 + :blurred-border-color colors/neutral-80-opa-5 + :text-color {:style {:color colors/neutral-100}}} + :active {:border-color colors/neutral-30 + :blurred-border-color colors/neutral-80-opa-10 + :text-color {:style {:color colors/neutral-100}}} + :disabled {:border-color colors/neutral-20 + :blurred-border-color colors/neutral-80-opa-5 + :text-color {:style {:color colors/neutral-100}}}} + :dark {:default {:border-color colors/neutral-70 + :blurred-border-color colors/white-opa-10 + :text-color {:style {:color colors/white}}} + :active {:border-color colors/neutral-60 + :blurred-border-color colors/white-opa-20 + :text-color {:style {:color colors/white}}} + :disabled {:border-color colors/neutral-70 + :blurred-border-color colors/white-opa-10 + :text-color {:style {:color colors/white}}}}}) + +(defn- emoji-comp + [size resource] + (let [dimension (case size + 32 20 + 24 12 + nil)] + (if (string? resource) + [rn/text {:style {:margin-right 4 :font-size dimension}} + resource] + [rn/image + {:source resource + :style {:margin-right 4 :width dimension :height dimension}}]))) + +(defn tag-resources + [size type resource icon-color label text-color labelled?] + [rn/view + {:style (merge {:flex-direction :row + :align-items :center + :justify-content :center} + (when label + {:padding-horizontal (case size + 32 12 + 24 8)}))} + (when (= type :icon) + [icons/icon resource + {:container-style (when label + {:margin-right 4}) + :resize-mode :center + :size (case size + 32 20 + 24 12) + :color icon-color}]) + (when (= type :emoji) + [emoji-comp size resource]) + (when labelled? + [text/text + (merge {:size (case size + 32 :paragraph-1 + 24 :paragraph-2 + 20 :label + nil) + :weight :medium + :number-of-lines 1} + text-color) + label])]) + +(defn tag + "opts + {:type :icon/:emoji/:label + :label string + :size 32/24 + :on-press fn + :blurred? true/false + :resource icon/image/text(emojis) + :labelled? true/false + :disabled? true/false} + + opts + - `blurred` boolean: use to determine border color if the background is blurred + - `type` can be icon or emoji with or without a tag label + - `labelled` boolean: is true if tag has label else false" + [{:keys [id on-press disabled? size active accessibility-label label resource type + labelled? blurred? icon-color] + :or {size 32}}] + (let [theme (quo.context/use-theme) + state (cond + disabled? :disabled + active :active + :else :default) + {:keys [border-color + blurred-border-color + text-color]} (get-in themes [theme state])] + [rn/view {:style {:align-items :center}} + [base-tag/base-tag + {:id id + :size size + :border-width 1 + :border-color (if blurred? + blurred-border-color + border-color) + :on-press on-press + :accessibility-label accessibility-label + :disabled? disabled? + :type type + :labelled? (if (= type :label) true labelled?)} + [tag-resources size type resource icon-color label text-color labelled?]]])) diff --git a/src/quo/components/tags/tags.cljs b/src/quo/components/tags/tags.cljs new file mode 100644 index 00000000000..07fda483f3f --- /dev/null +++ b/src/quo/components/tags/tags.cljs @@ -0,0 +1,180 @@ +(ns quo.components.tags.tags + (:require + [oops.core :refer [oget]] + [quo.components.tags.tag :as tag] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [react-native.masked-view :as masked-view] + [reagent.core :as reagent] + [utils.collection :as utils.collection] + [utils.number :as number-utils])) + +(def default-tab-size 32) + +(defn calculate-fade-end-percentage + [{:keys [offset-x content-width layout-width max-fade-percentage]}] + (let [fade-percentage (max max-fade-percentage + (/ (+ layout-width offset-x) + content-width))] + ;; Truncate to avoid unnecessary rendering. + (if (> fade-percentage 0.99) + 0.99 + (number-utils/naive-round fade-percentage 2)))) + +(defn tags + "Usage: + {:type :icon/:emoji/:label + :component tag/tab + :size 32/24 + :on-press fn + :blurred? true/false + :labelled? true/false + :disabled? true/false + :scroll-on-press? true + :scrollable? false + :fade-end? true + :on-change fn + :default-active tag-id + :data [{:id :label \"\" :resource \"url\"} + {:id :label \"\" :resource \"url\"}]} + Opts: + - `component` this is to determine which component is to be rendered since the + logic in this view is shared between tab and tag component + - `blurred` boolean: use to determine border color if the background is blurred + - `type` can be icon or emoji with or without a tag label + - `labelled` boolean: is true if tag has label else false + - `size` number + - `scroll-on-press?` When non-nil, clicking on a tag centers it the middle + (with animation enabled). + - `fade-end?` When non-nil, causes the end of the scrollable view to fade out. + - `fade-end-percentage` Percentage where fading starts relative to the total + layout width of the `flat-list` data." + + [{:keys [default-active data fade-end-percentage fade-end? on-change on-scroll scroll-event-throttle + scrollable? + scroll-on-press? size type labelled? disabled? blurred? icon-color] + :or {fade-end-percentage 0.8 + fade-end? false + scroll-event-throttle 64 + scrollable? false + scroll-on-press? false + size default-tab-size} + :as props}] + (let [[active-tab-id + set-active-tab-id] (rn/use-state default-active) + [fading set-fading] (rn/use-state {:fade-end-percentage fade-end-percentage}) + flat-list-ref (rn/use-ref-atom nil) + maybe-mask-wrapper (rn/use-memo + (fn [] + (if fade-end? + [masked-view/masked-view + {:mask-element (reagent/as-element + [linear-gradient/linear-gradient + {:colors [:black :transparent] + :locations [(get fading :fade-end-percentage) + 1] + :start {:x 0 :y 0} + :end {:x 1 :y 0} + :pointer-events :none + :style {:width "100%" + :height "100%"}}])}] + [:<>])) + [fade-end? fading]) + on-scroll (rn/use-context + (fn [^js e] + (when fade-end? + (let [offset-x (oget e "nativeEvent.contentOffset.x") + content-width (oget e "nativeEvent.contentSize.width") + layout-width (oget e "nativeEvent.layoutMeasurement.width") + new-percentage (calculate-fade-end-percentage + {:offset-x offset-x + :content-width content-width + :layout-width layout-width + :max-fade-percentage fade-end-percentage})] + ;; Avoid unnecessary re-rendering. + (when (not= new-percentage (get fading :fade-end-percentage)) + (set-fading (assoc fading :fade-end-percentage new-percentage))))) + (when on-scroll + (on-scroll e))) + [fade-end? fading fade-end-percentage]) + data-count (count data) + style-padding-left (get-in props [:style :padding-left]) + render-fn (rn/use-callback + (fn [{:keys [id label resource]} index] + [rn/view + {:style {:margin-right (if (= size default-tab-size) 12 8) + :padding-right (when (= index (dec data-count)) + style-padding-left)}} + + [tag/tag + {:id id + :size size + :active (= id active-tab-id) + :resource resource + :blurred? blurred? + :icon-color icon-color + :disabled? disabled? + :label (if labelled? + label + (when (= type :label) label)) + :type type + :labelled? labelled? + :on-press (fn [id] + (set-active-tab-id id) + (when scroll-on-press? + (.scrollToIndex ^js @flat-list-ref + #js + {:animated true + :index index + :viewPosition 0.5})) + (when on-change + (on-change id)))}]]) + [size default-tab-size data-count style-padding-left active-tab-id blurred? + icon-color disabled? labelled? type scroll-on-press? on-change]) + on-press (rn/use-callback #(do (set-active-tab-id %) (when on-change (on-change %))) + [on-change]) + key-fn (rn/use-callback (comp str :id)) + ref (rn/use-callback #(reset! flat-list-ref %)) + clean-props (dissoc props + :default-active + :fade-end-percentage + :fade-end? + :on-change + :scroll-on-press? + :size)] + (if scrollable? + (conj + maybe-mask-wrapper + [rn/flat-list + (merge + clean-props + (when scroll-on-press? + {:initial-scroll-index (utils.collection/first-index #(= active-tab-id (:id %)) data)}) + {:ref ref + :extra-data (str active-tab-id) + :horizontal true + :scroll-event-throttle scroll-event-throttle + :shows-horizontal-scroll-indicator false + :data data + :key-fn key-fn + :on-scroll on-scroll + :render-fn render-fn})]) + + [rn/view {:style {:flex-direction :row}} + (for [{:keys [id label resource]} data] + ^{:key id} + [rn/view {:style {:margin-right 8}} + [tag/tag + {:id id + :size size + :type type + :label (if labelled? + label + (when (= type :label) label)) + :active (= id active-tab-id) + :disabled? disabled? + :blurred? blurred? + :icon-color icon-color + :labelled? (if (= type :label) true labelled?) + :resource (if (= type :icon) :i/placeholder resource) + :on-press on-press}]])]))) diff --git a/src/quo/components/tags/tiny_tag/component_spec.cljs b/src/quo/components/tags/tiny_tag/component_spec.cljs new file mode 100644 index 00000000000..0fe5021a634 --- /dev/null +++ b/src/quo/components/tags/tiny_tag/component_spec.cljs @@ -0,0 +1,16 @@ +(ns quo.components.tags.tiny-tag.component-spec + (:require + [quo.components.tags.tiny-tag.view :as tiny-tag] + [test-helpers.component :as h])) + +(h/describe "Tiny tag component test" + (h/test "1,000 SNT render" + (h/render [tiny-tag/view + {:label "1,000 SNT" + :blur? false}]) + (h/is-truthy (h/get-by-text "1,000 SNT"))) + (h/test "2,000 SNT render with blur" + (h/render [tiny-tag/view + {:label "2,000 SNT" + :blur? true}]) + (h/is-truthy (h/get-by-text "2,000 SNT")))) diff --git a/src/quo/components/tags/tiny_tag/style.cljs b/src/quo/components/tags/tiny_tag/style.cljs new file mode 100644 index 00000000000..e7c7236792a --- /dev/null +++ b/src/quo/components/tags/tiny_tag/style.cljs @@ -0,0 +1,34 @@ +(ns quo.components.tags.tiny-tag.style + (:require + [quo.foundations.colors :as colors])) + +(defn get-border-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-10 theme) + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme))) + +(defn get-label-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(def main + {:justify-content :center + :align-items :center + :height 16}) + +(defn inner + [{:keys [blur?]} theme] + {:border-width 1 + :border-radius 6 + :border-color (get-border-color blur? theme) + :justify-content :center + :align-items :center + :padding-left 2 + :padding-right 3}) + +(defn label + [{:keys [blur?]} theme] + {:color (get-label-color blur? theme)}) diff --git a/src/quo/components/tags/tiny_tag/view.cljs b/src/quo/components/tags/tiny_tag/view.cljs new file mode 100644 index 00000000000..89d2ba98ad4 --- /dev/null +++ b/src/quo/components/tags/tiny_tag/view.cljs @@ -0,0 +1,17 @@ +(ns quo.components.tags.tiny-tag.view + (:require + [quo.components.markdown.text :as text] + [quo.components.tags.tiny-tag.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn])) + +(defn view + [{:keys [label] :as props}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/main} + [rn/view {:style (style/inner props theme)} + [text/text + {:style (style/label props theme) + :weight :medium + :size :label + :align :center} label]]])) diff --git a/src/quo/components/tags/token_tag/style.cljs b/src/quo/components/tags/token_tag/style.cljs new file mode 100644 index 00000000000..57c6a7501f3 --- /dev/null +++ b/src/quo/components/tags/token_tag/style.cljs @@ -0,0 +1,45 @@ +(ns quo.components.tags.token-tag.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [size options blur? theme] + (let [hold? (= options :hold)] + (merge {:background-color (if blur? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)) + :flex-direction :row + :align-items :center + :padding-left 2 + :border-width (if hold? 1 0) + :border-radius 20 + :border-color (colors/theme-colors colors/success-50 colors/success-60 theme)} + (condp = size + :size-24 {:height (if hold? 26 24) + :padding-right 10 + :border-radius (if hold? 13 12)} + :size-32 {:height (if hold? 34 32) + :padding-right 12 + :border-radius (if hold? 17 16)})))) + +(defn options-icon + [size] + (assoc + (condp = size + :size-24 {:right -8 + :top -8} + :size-32 {:right -6 + :top -6}) + :position + :absolute)) + +(defn token-img + [size] + (case size + :size-24 {:margin-right 6 + :border-radius 10} + :size-32 {:margin-right 8 + :border-radius 14})) + +(defn label + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) diff --git a/src/quo/components/tags/token_tag/view.cljs b/src/quo/components/tags/token_tag/view.cljs new file mode 100644 index 00000000000..97122ff90a2 --- /dev/null +++ b/src/quo/components/tags/token_tag/view.cljs @@ -0,0 +1,60 @@ +(ns quo.components.tags.token-tag.view + (:require + [oops.core :refer [oget]] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tags.token-tag.style :as style] + [quo.components.utilities.token.view :as token] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.hole-view :as hole-view])) + +(defn view + "Options: + - :options - false / :add / :hold (default false) + - :size - :size-24 / :size-32 (default :size-24) + - :blur? - boolean (default false) + - :theme - :light / :dark + - :token-value - string - token value + - :token-symbol - string" + [{:keys [options size blur? token-value token-img-src token-symbol] + :or {size :size-24}}] + (let [theme (quo.context/use-theme) + [container-width + set-container-width] (rn/use-state 0) + on-layout (rn/use-callback #(set-container-width + (oget % :nativeEvent :layout :width)))] + [rn/view {:on-layout on-layout} + [hole-view/hole-view + {:holes (if options + [{:x (- container-width + (case size + :size-24 10 + :size-32 12 + nil)) + :y (case size + :size-24 -6 + :size-32 -4 + nil) + :width 16 + :height 16 + :borderRadius 8}] + [])} + [rn/view {:style (style/container size options blur? theme)} + [token/view + {:style (style/token-img size) + :token token-symbol + :size (case size + :size-24 :size-20 + :size-32 :size-28) + :image-source token-img-src}] + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/label theme)} + (str token-value " " token-symbol)]]] + (when options + [rn/view {:style (style/options-icon size)} + [icons/icon (if (= options :hold) :i/hold :i/add-token) + {:size 20 + :no-color true}]])])) diff --git a/src/quo/components/text_combinations/channel_name/component_spec.cljs b/src/quo/components/text_combinations/channel_name/component_spec.cljs new file mode 100644 index 00000000000..5cb10886741 --- /dev/null +++ b/src/quo/components/text_combinations/channel_name/component_spec.cljs @@ -0,0 +1,28 @@ +(ns quo.components.text-combinations.channel-name.component-spec + (:require [quo.components.text-combinations.channel-name.view :as channel-name] + [test-helpers.component :as h])) + +(h/describe "Channel name" + (h/test "Renders Default" + (h/render [channel-name/view {:channel-name "Test channel"}]) + (h/is-truthy (h/get-by-text "# Test channel"))) + + (h/test "Renders unlocked icon" + (h/render [channel-name/view + {:channel-name "Test channel" + :unlocked? true}]) + (h/is-truthy (h/get-by-label-text :channel-name-unlocked-icon))) + + (h/test "Renders muted icon" + (h/render [channel-name/view + {:channel-name "Test channel" + :muted? true}]) + (h/is-truthy (h/get-by-label-text :channel-name-muted-icon))) + + (h/test "Renders muted and unlocked icon" + (h/render [channel-name/view + {:channel-name "Test channel" + :muted? true + :unlocked? true}]) + (h/is-truthy (h/get-by-label-text :channel-name-unlocked-icon)) + (h/is-truthy (h/get-by-label-text :channel-name-muted-icon)))) diff --git a/src/quo/components/text_combinations/channel_name/style.cljs b/src/quo/components/text_combinations/channel_name/style.cljs new file mode 100644 index 00000000000..86fd9455d17 --- /dev/null +++ b/src/quo/components/text_combinations/channel_name/style.cljs @@ -0,0 +1,30 @@ +(ns quo.components.text-combinations.channel-name.style + (:require [quo.foundations.colors :as colors])) + +(def container {:flex-direction :row}) + +(def icons-container + {:flex-direction :row + :padding-top 8 + :padding-bottom 4 + :margin-left 6}) + +(def icon {:width 20 :height 20}) + +(def icons-gap {:width 4}) + +(defn- blur-icon-color + [theme] + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme)) + +(defn unlocked-icon-color + [theme blur?] + (if blur? + (blur-icon-color theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn muted-icon-color + [theme blur?] + (if blur? + (blur-icon-color theme) + (colors/theme-colors colors/neutral-40 colors/neutral-60 theme))) diff --git a/src/quo/components/text_combinations/channel_name/view.cljs b/src/quo/components/text_combinations/channel_name/view.cljs new file mode 100644 index 00000000000..415c3f6d3d5 --- /dev/null +++ b/src/quo/components/text_combinations/channel_name/view.cljs @@ -0,0 +1,39 @@ +(ns quo.components.text-combinations.channel-name.view + (:require [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.text-combinations.channel-name.style :as style] + [quo.context] + [react-native.core :as rn])) + +(defn icons + [{:keys [unlocked? muted? blur?]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/icons-container} + (when unlocked? + [rn/view + {:style style/icon + :accessibility-label :channel-name-unlocked-icon} + [icon/icon :i/unlocked + {:color (style/unlocked-icon-color theme blur?) + :size 20}]]) + + (when (and unlocked? muted?) + [rn/view {:style style/icons-gap}]) + + (when muted? + [rn/view + {:style style/icon + :accessibility-label :channel-name-muted-icon} + [icon/icon :i/muted + {:color (style/muted-icon-color theme blur?) + :size 20}]])])) + +(defn view + [{:keys [unlocked? muted? channel-name] :as props}] + [rn/view {:style style/container} + [text/text + {:size :heading-1 + :weight :semi-bold} + (str "# " channel-name)] + (when (or unlocked? muted?) + [icons props])]) diff --git a/src/quo/components/text_combinations/page_top/component_spec.cljs b/src/quo/components/text_combinations/page_top/component_spec.cljs new file mode 100644 index 00000000000..ff41b6d76cb --- /dev/null +++ b/src/quo/components/text_combinations/page_top/component_spec.cljs @@ -0,0 +1,122 @@ +(ns quo.components.text-combinations.page-top.component-spec + (:require [quo.components.text-combinations.page-top.view :as page-top] + [test-helpers.component :as h])) + +(defonce mock-picture {:uri (js/require "../resources/images/mock2/user_picture_male4.png")}) + +(def context-tag-data + {:type :community + :state :default + :customization-color :army + :community-logo mock-picture + :community-name "Coinbase" + :emoji "😝"}) + +(h/describe "Page Top" + (h/test "Default render" + (h/render [page-top/view {:title "Title"}]) + (h/is-truthy (h/get-by-text "Title"))) + + (h/test "Avatar and Title" + (h/render [page-top/view + {:title "Title" + :avatar {:emoji "🥨" + :customization-color :army}}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :channel-avatar))) + + (h/describe "Description" + (h/test "Text" + (h/render [page-top/view + {:title "Title" + :description :text + :description-text "This is a textual description"}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-text "This is a textual description"))) + + (h/test "Context tag" + (h/render-with-theme-provider [page-top/view + {:title "Title" + :description :context-tag + :context-tag context-tag-data}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :context-tag))) + + (h/test "Summary" + (h/render-with-theme-provider [page-top/view + {:title "Title" + :description :summary + :summary {:row-1 {:text-1 "Send" + :text-2 "from" + :context-tag-1 context-tag-data + :context-tag-2 context-tag-data} + :row-2 {:text-1 "to" + :text-2 "via" + :context-tag-1 context-tag-data + :context-tag-2 context-tag-data}}}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-text "Send")) + (h/is-truthy (h/get-by-text "from")) + (h/is-truthy (h/get-by-text "to")) + (h/is-truthy (h/get-by-text "via")) + (h/is-equal (count (js->clj (h/get-all-by-label-text :context-tag))) 4)) + + (h/test "Collection" + (h/render [page-top/view + {:title "Title" + :description :collection + :collection-text "Collectible Collection" + :collection-image mock-picture}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-text "Collectible Collection")) + (h/is-truthy (h/get-by-label-text :collection-avatar))) + + (h/test "Community" + (h/render [page-top/view + {:title "Title" + :description :community + :community-text "Doodles" + :community-image mock-picture}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-text "Doodles")) + (h/is-truthy (h/get-by-label-text :community-logo)))) + + (h/test "Emoji dash" + (let [emoji-dash ["❤️" "✏️" "💬" "😋" "📱" "🚓" "💹" "😝" "👊" "👤" "😚" "🚉" "👻" "\uD83D\uDC6F"]] + (h/render [page-top/view + {:title "Title" + :emoji-dash emoji-dash}]) + + (doseq [emoji emoji-dash] + (h/is-truthy (h/get-by-text emoji))) + (h/is-truthy (h/get-by-text "Title")))) + + (h/test "Search input" + (h/render [page-top/view + {:title "Title" + :input :search}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :search-input))) + + (h/test "Address input" + (h/render [page-top/view + {:title "Title" + :input :address}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :address-text-input))) + + (h/test "Recovery phrase" + (h/render [page-top/view + {:title "Title" + :input :recovery-phrase}]) + + (h/is-truthy (h/get-by-text "Title")) + (h/is-truthy (h/get-by-label-text :recovery-phrase-input)))) diff --git a/src/quo/components/text_combinations/page_top/style.cljs b/src/quo/components/text_combinations/page_top/style.cljs new file mode 100644 index 00000000000..87e4ece7746 --- /dev/null +++ b/src/quo/components/text_combinations/page_top/style.cljs @@ -0,0 +1,80 @@ +(ns quo.components.text-combinations.page-top.style + (:require [quo.foundations.colors :as colors])) + +(def top-container + {:padding-vertical 12 + :padding-horizontal 20 + :row-gap 8}) + +(def header + {:flex-direction :row + :justify-content :space-between}) + +(def header-title + {:flex 1 + :flex-direction :row + :column-gap 8}) + +(def header-counter + {:margin-left 20 + :margin-bottom 2 + :justify-content :flex-end}) + +(def context-tag-description + {:align-items :flex-start}) + +(def summary-description + {:row-gap 4}) + +(def summary-description-row + {:flex-direction :row + :align-items :center + :column-gap 4}) + +(def image-text-description + {:flex-direction :row + :column-gap 8 + :align-items :center}) + +(def community-logo + {:width 24 + :height 24 + :border-radius 12}) + +(def community-logo-ring + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :border-radius 12 + :border-width 1 + :border-color colors/neutral-80-opa-5}) + +(def emoji-dash + {:flex-direction :row}) + +(def emoji + {:width 20 + :height 20}) + +(def header-counter-text {:color colors/neutral-40}) + +(defn input-container + [theme input blur?] + (when-not (= input :recovery-phrase) + {:border-bottom-width 1 + :border-color (if blur? + (colors/theme-colors colors/neutral-80-opa-5 colors/white-opa-5 theme) + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme))})) + +(def search-input-container + {:flex 0 + :margin-top 8 + :margin-bottom 12 + :margin-horizontal 20}) + +(def recovery-phrase-container + {:padding-vertical nil + :padding-top 4 + :padding-bottom 12}) diff --git a/src/quo/components/text_combinations/page_top/view.cljs b/src/quo/components/text_combinations/page_top/view.cljs new file mode 100644 index 00000000000..9cd1221a703 --- /dev/null +++ b/src/quo/components/text_combinations/page_top/view.cljs @@ -0,0 +1,151 @@ +(ns quo.components.text-combinations.page-top.view + (:require [clojure.string :as string] + [quo.components.avatars.channel-avatar.view :as channel-avatar] + [quo.components.avatars.collection-avatar.view :as collection-avatar] + [quo.components.avatars.group-avatar.view :as group-avatar] + [quo.components.inputs.address-input.view :as address-input] + [quo.components.inputs.recovery-phrase.view :as recovery-phrase] + [quo.components.inputs.search-input.view :as search-input] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.view :as context-tag] + [quo.components.text-combinations.page-top.style :as style] + [quo.components.text-combinations.standard-title.view :as standard-title] + [quo.context] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [utils.number])) + +(defn- format-counter + [n] + (let [num (utils.number/parse-int n)] + (if (<= num 9) + (str "0" num) + (str num)))) + +(defn- header-counter + [counter-top counter-bottom] + [rn/view {:style style/header-counter} + [text/text + {:style style/header-counter-text + :weight :regular + :size :paragraph-2} + (str (format-counter counter-top) + "/" + (format-counter counter-bottom))]]) + +(defn- header + [{:keys [title title-accessibility-label input counter-top counter-bottom + title-right title-right-props] + avatar-props :avatar}] + (let [title-props (assoc title-right-props + :title title + :right title-right + :accessibility-label title-accessibility-label)] + [rn/view {:style style/header} + [rn/view {:style style/header-title} + (when avatar-props + (let [avatar-props (assoc avatar-props :size :size-32)] + (if (:group? avatar-props) + [group-avatar/view avatar-props] + [channel-avatar/view avatar-props]))) + [standard-title/view title-props]] + (when (= input :recovery-phrase) + [header-counter counter-top counter-bottom])])) + +(defn- summary-description + [{:keys [row-1 row-2] :as _summary-props} blur?] + (let [text-props {:size :paragraph-2 + :weight :medium}] + [rn/view {:style style/summary-description} + (when-let [{:keys [text-1 text-2 context-tag-1 context-tag-2]} row-1] + [rn/view {:style style/summary-description-row} + [text/text text-props text-1] + [context-tag/view (assoc context-tag-1 :size 24 :blur? blur?)] + [text/text text-props text-2] + [context-tag/view (assoc context-tag-2 :size 24 :blur? blur?)]]) + (when-let [{:keys [text-1 text-2 context-tag-1 context-tag-2]} row-2] + [rn/view {:style style/summary-description-row} + [text/text text-props text-1] + [context-tag/view (assoc context-tag-1 :size 24 :blur? blur?)] + [text/text text-props text-2] + [context-tag/view (assoc context-tag-2 :size 24 :blur? blur?)]])])) + +(defn- community-logo + [image] + [rn/view {:accessibility-label :community-logo} + [fast-image/fast-image + {:source image + :style style/community-logo}] + [rn/view {:style style/community-logo-ring}]]) + +(defn- description-container + [{:keys [description description-text collection-text community-text blur? + collection-image community-image description-accessibility-label] + context-tag-props :context-tag + summary-props :summary}] + [rn/view {:accessibility-label description-accessibility-label} + (cond + (and (= description :text) (not (string/blank? description-text))) + [text/text + {:weight :regular + :size :paragraph-1} + description-text] + + (and (= description :context-tag) context-tag-props) + [rn/view {:style style/context-tag-description} + [context-tag/view (assoc context-tag-props :size 24 :blur? blur?)]] + + (and (= description :summary) summary-props) + [summary-description summary-props blur?] + + (= description :collection) + [rn/view {:style style/image-text-description} + [collection-avatar/view {:image collection-image}] + [text/text {:weight :semi-bold :size :paragraph-1} + collection-text]] + + (= description :community) + [rn/view {:style style/image-text-description} + [community-logo community-image] + [text/text {:weight :semi-bold :size :paragraph-1} + community-text]])]) + +(defn- emoji-dash + [emojis] + (into [rn/view {:style style/emoji-dash}] + (map (fn [emoji] + [rn/view {:style style/emoji} + [rn/text {:adjusts-font-size-to-fit true} emoji]])) + emojis)) + +(defn view + [{:keys [description title input blur? input-props container-style] + emojis :emoji-dash + :as props}] + (let [theme (quo.context/use-theme)] + [rn/view {:style container-style} + [rn/view {:style style/top-container} + (when (or title input) + [header props]) + (when description + [description-container props]) + (when emojis + [emoji-dash emojis])] + (when input + [rn/view {:style (style/input-container theme input blur?)} + (case input + :search + [search-input/search-input + (assoc input-props + :container-style style/search-input-container + :blur? blur?)] + + :address + [address-input/address-input (assoc input-props :blur? blur?)] + + :recovery-phrase + [recovery-phrase/recovery-phrase-input + (assoc input-props + :container-style style/recovery-phrase-container + :blur? blur?)] + nil)])])) diff --git a/src/quo/components/text_combinations/standard_title/component_spec.cljs b/src/quo/components/text_combinations/standard_title/component_spec.cljs new file mode 100644 index 00000000000..a41cf4d6277 --- /dev/null +++ b/src/quo/components/text_combinations/standard_title/component_spec.cljs @@ -0,0 +1,60 @@ +(ns quo.components.text-combinations.standard-title.component-spec + (:require [quo.components.markdown.text :as text] + [quo.components.text-combinations.standard-title.view :as standard-title] + [test-helpers.component :as h])) + +(h/describe "Text combinations - Standard title" + (h/test "Default render" + (h/render [standard-title/view {:title "This is a title"}]) + (h/is-truthy (h/get-by-text "This is a title"))) + + (h/test "Counter variant" + (h/render [standard-title/view + {:title "This is a title" + :right :counter + :counter-left 50 + :counter-right 100}]) + (h/is-truthy (h/get-by-text "50/100"))) + + (h/describe "Action variant" + (h/test "Default render" + (let [on-press-fn (h/mock-fn)] + (h/render [standard-title/view + {:title "This is a title" + :right :action + :on-press on-press-fn}]) + (h/is-truthy (h/get-by-text "This is a title")))) + + (h/test "Action fired" + (let [on-press-fn (h/mock-fn)] + (h/render [standard-title/view + {:title "This is a title" + :right :action + :on-press on-press-fn}]) + + (h/fire-event :on-press (h/get-by-label-text :standard-title-action)) + (h/was-called-times on-press-fn 1)))) + + (h/describe "Tag variant" + (h/test "Default render" + (h/render [standard-title/view + {:title "This is a title" + :right :tag}]) + (h/is-truthy (h/get-by-text "This is a title"))) + + (h/test "Tag callback fired" + (let [on-press-fn (h/mock-fn)] + (h/render [standard-title/view + {:title "This is a title" + :right :tag + :on-press on-press-fn}]) + (h/fire-event :on-press (h/get-by-label-text :standard-title-tag)) + (h/was-called-times on-press-fn 1)))) + + (h/describe "Custom content variant" + (h/test "Default render" + (h/render [standard-title/view + {:title "This is a title" + :right [text/text "Right"]}]) + (h/is-truthy (h/get-by-text "This is a title")) + (h/is-truthy (h/get-by-text "Right"))))) diff --git a/src/quo/components/text_combinations/standard_title/style.cljs b/src/quo/components/text_combinations/standard_title/style.cljs new file mode 100644 index 00000000000..81fe292047c --- /dev/null +++ b/src/quo/components/text_combinations/standard_title/style.cljs @@ -0,0 +1,22 @@ +(ns quo.components.text-combinations.standard-title.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:flex 1 + :flex-direction :row + :justify-content :space-between}) + +(def right-counter + {:padding-top 12 + :padding-bottom 2}) + +(defn right-tag-icon-color + [blur? theme] + (if blur? + (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(def text + {:flex 1 + ;; NOTE: assures the ellipses are not cut off when text is too long + :padding-right 2}) diff --git a/src/quo/components/text_combinations/standard_title/view.cljs b/src/quo/components/text_combinations/standard_title/view.cljs new file mode 100644 index 00000000000..66abf07e246 --- /dev/null +++ b/src/quo/components/text_combinations/standard_title/view.cljs @@ -0,0 +1,62 @@ +(ns quo.components.text-combinations.standard-title.view + (:require [clojure.string :as string] + [quo.components.buttons.button.view :as button] + [quo.components.counter.fraction-counter.view :as fraction-counter] + [quo.components.markdown.text :as text] + [quo.components.tags.tag :as tag] + [quo.components.text-combinations.standard-title.style :as style] + [quo.context] + [react-native.core :as rn] + [utils.number])) + +(defn- right-counter + [{:keys [blur? counter-left counter-right counter-suffix show-counter-warning?]}] + [rn/view {:style style/right-counter} + [fraction-counter/view + {:blur? blur? + :show-counter-warning? show-counter-warning? + :left-value counter-left + :right-value counter-right + :suffix counter-suffix}]]) + +(defn- right-action + [{:keys [customization-color on-press icon] + :or {icon :i/placeholder}}] + [button/button + {:accessibility-label :standard-title-action + :size 32 + :icon-only? true + :customization-color customization-color + :on-press on-press} + icon]) + +(defn- right-tag + [{:keys [blur? on-press icon label] + :or {icon :i/placeholder}}] + (let [theme (quo.context/use-theme) + labelled? (not (string/blank? label))] + [tag/tag + {:accessibility-label :standard-title-tag + :size 32 + :type :icon + :resource icon + :on-press on-press + :labelled? labelled? + :label (when labelled? label) + :blurred? blur? + :icon-color (style/right-tag-icon-color blur? theme)}])) + +(defn view + [{:keys [title right accessibility-label container-style] :as props}] + [rn/view {:style (merge style/container container-style)} + [text/text + {:size :heading-1 + :weight :semi-bold + :style style/text + :accessibility-label accessibility-label} + title] + (case right + :counter [right-counter props] + :action [right-action props] + :tag [right-tag props] + right)]) diff --git a/src/quo/components/text_combinations/style.cljs b/src/quo/components/text_combinations/style.cljs new file mode 100644 index 00000000000..f5c53664cf6 --- /dev/null +++ b/src/quo/components/text_combinations/style.cljs @@ -0,0 +1,32 @@ +(ns quo.components.text-combinations.style + (:require [quo.foundations.colors :as colors])) + +(def title-container + {:flex-direction :row + :flex 1 + :align-items :center}) + +(def avatar-container + {:margin-right 9 + :text-align :center}) + +(def description-description-text + {:margin-top 8}) + +(def emoji-hash + {:margin-top 8 + :letter-spacing 2 + :font-size 13 + :line-height 20.5}) + +(defn textual-emoji + [size customization-color theme] + {:border-radius size + :margin-top -5 + :border-width 0 + :border-color :transparent + :width size + :height size + :justify-content :center + :align-items :center + :background-color (colors/resolve-color customization-color theme 10)}) diff --git a/src/quo/components/text_combinations/username/component_spec.cljs b/src/quo/components/text_combinations/username/component_spec.cljs new file mode 100644 index 00000000000..a410cb33328 --- /dev/null +++ b/src/quo/components/text_combinations/username/component_spec.cljs @@ -0,0 +1,68 @@ +(ns quo.components.text-combinations.username.component-spec + (:require [quo.components.text-combinations.username.view :as username] + [test-helpers.component :as h])) + +(defn test-all-status + [component-to-render component-props] + (h/test "Verified status" + (h/render [component-to-render (assoc component-props :status :verified)]) + (h/is-truthy (h/get-by-label-text :username-status-icon))) + + (h/test "Contact status" + (h/render [component-to-render (assoc component-props :status :contact)]) + (h/is-truthy (h/get-by-label-text :username-status-icon))) + + (h/test "Untrustworthy status" + (h/render [component-to-render (assoc component-props :status :untrustworthy)]) + (h/is-truthy (h/get-by-label-text :username-status-icon))) + + (h/test "Untrustworthy contact status" + (h/render [component-to-render (assoc component-props :status :untrustworthy-contact)]) + (let [icons (h/get-all-by-label-text :username-status-icon)] + (h/is-truthy (aget icons 0)) + (h/is-truthy (aget icons 1)))) + + (h/test "Blocked status" + (h/render [component-to-render (assoc component-props :status :blocked)]) + (h/is-truthy (h/get-by-label-text :username-status-icon)))) + +(h/describe "Text combinations - Username" + (h/test "Renders default" + (h/render [username/view {:username "Test username"}]) + (h/is-truthy (h/get-by-text "Test username"))) + + (h/describe "Render different :name-type values" + (h/describe "default" + (let [props {:name-type :default + :username "Test username"}] + + (h/test "default render" + (h/render [username/view props]) + (h/is-truthy (h/get-by-text "Test username"))) + + (h/describe "All status are rendered" + (test-all-status username/view props)))) + + (h/describe "ens" + (let [props {:name-type :ens + :username "test-username.eth"}] + + (h/test "no status render" + (h/render [username/view props]) + (h/is-truthy (h/get-by-text "test-username.eth"))) + + (h/describe "All status are rendered" + (test-all-status username/view props)))) + + (h/describe "nickname" + (let [props {:name-type :nickname + :username "Nickname" + :name "Real name"}] + + (h/test "no status render" + (h/render [username/view props]) + (h/is-truthy (h/get-by-text "Nickname")) + (h/is-truthy (h/get-by-text "Real name"))) + + (h/describe "All status are rendered" + (test-all-status username/view props)))))) diff --git a/src/quo/components/text_combinations/username/style.cljs b/src/quo/components/text_combinations/username/style.cljs new file mode 100644 index 00000000000..ec6aa64aff0 --- /dev/null +++ b/src/quo/components/text_combinations/username/style.cljs @@ -0,0 +1,35 @@ +(ns quo.components.text-combinations.username.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:flex-direction :row + :justify-content :flex-start + :height 32}) + +(def username-text-container + {:flex-direction :row + :align-items :flex-end + :flex-shrink 1}) + +(defn real-name-text + [theme blur?] + {:color (if blur? + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme) + (colors/theme-colors colors/neutral-60 colors/neutral-40 theme)) + :flex-shrink 1}) + +(defn real-name-dot + [theme blur?] + (merge (real-name-text theme blur?) + {:margin-horizontal 6 + :flex-shrink 0})) + +(defn status-icon-container + [name-type status] + (cond-> {:flex-direction :row + :margin-left 4} + (#{:default :ens} name-type) (assoc :margin-top 8 + :margin-bottom 4) + (= :nickname name-type) (assoc :margin-top 10 + :margin-bottom 2) + (= status :untrustworthy-contact) (assoc :margin-right 2))) diff --git a/src/quo/components/text_combinations/username/view.cljs b/src/quo/components/text_combinations/username/view.cljs new file mode 100644 index 00000000000..33529d9c997 --- /dev/null +++ b/src/quo/components/text_combinations/username/view.cljs @@ -0,0 +1,60 @@ +(ns quo.components.text-combinations.username.view + (:require [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.text-combinations.username.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn- username-text + [{:keys [name-type username accessibility-label blur?] + real-name :name}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/username-text-container} + [text/text + {:size :heading-1 + :accessibility-label accessibility-label + :weight :semi-bold} + username] + (when (= name-type :nickname) + [:<> + [text/text + {:style (style/real-name-dot theme blur?) + :size :paragraph-1 + :weight :medium} + "∙"] + [text/text + {:style (style/real-name-text theme blur?) + :size :paragraph-1 + :accessibility-label :real-name + :weight :medium + :number-of-lines 1} + real-name]])])) + +(defn- icon-20 + [icon-name color] + (let [theme (quo.context/use-theme)] + [icon/icon icon-name + {:accessibility-label :username-status-icon + :size 20 + :color (colors/resolve-color color theme)}])) + +(defn status-icon + [{:keys [theme name-type status] + :or {name-type :default}}] + [rn/view {:style (style/status-icon-container name-type status)} + (case status + :verified [icon-20 :i/verified theme :success] + :contact [icon-20 :i/contact theme :blue] + :untrustworthy [icon-20 :i/untrustworthy theme :danger] + :blocked [icon-20 :i/block theme :danger] + :untrustworthy-contact [:<> + [icon-20 :i/untrustworthy theme :danger] + [icon-20 :i/contact theme :blue]] + nil)]) + +(defn view + [props] + [rn/view {:style style/container} + [username-text props] + [status-icon props]]) diff --git a/src/quo/components/text_combinations/view.cljs b/src/quo/components/text_combinations/view.cljs new file mode 100644 index 00000000000..cddb5f334c8 --- /dev/null +++ b/src/quo/components/text_combinations/view.cljs @@ -0,0 +1,65 @@ +(ns quo.components.text-combinations.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.markdown.text :as text] + [quo.components.text-combinations.style :as style] + [quo.context] + [react-native.core :as rn])) + +(defn icon + [{:keys [source size customization-color theme]}] + (if customization-color + [rn/view {:style (style/textual-emoji size customization-color theme)} + [text/text + source]] + [rn/image + {:source source + :style {:border-radius 50 + :border-width 0 + :border-color :transparent + :width size + :height size}}])) + +(defn view + [{:keys [container-style title title-number-of-lines avatar title-accessibility-label description emoji + description-accessibility-label button-icon button-on-press customization-color emoji-hash] + :or {title-number-of-lines 1}}] + (let [theme (quo.context/use-theme)] + [rn/view {:style container-style} + [rn/view + {:style {:flex-direction :row + :justify-content :space-between}} + [rn/view {:style style/title-container} + (when avatar + [rn/view {:style style/avatar-container} + [icon {:source avatar :size 32}]]) + (when emoji + [rn/view {:style style/avatar-container} + [icon {:source emoji :size 32 :customization-color customization-color :theme theme}]]) + [text/text + {:accessibility-label title-accessibility-label + :weight :semi-bold + :ellipsize-mode :tail + :style {:flex 1} + :number-of-lines title-number-of-lines + :size :heading-1} + title]] + (when button-icon + [button/button + {:icon-only? true + :on-press button-on-press + :customization-color customization-color + :size 32} button-icon])] + (when description + [text/text + {:accessibility-label description-accessibility-label + :weight :regular + :size :paragraph-1 + :style style/description-description-text} + description]) + (when emoji-hash + [text/text + {:number-of-lines 1 + :accessibility-label :emoji-hash + :style style/emoji-hash} + emoji-hash])])) diff --git a/src/quo/components/utilities/social/loader.clj b/src/quo/components/utilities/social/loader.clj new file mode 100644 index 00000000000..f410bdb3de1 --- /dev/null +++ b/src/quo/components/utilities/social/loader.clj @@ -0,0 +1,39 @@ +(ns quo.components.utilities.social.loader + (:require [clojure.java.io :as io] + [clojure.string :as string])) + +(def ^:private icon-path "./resources/images/socials/") + +(defn- require-icon + [size path social-type] + (fn [el] + (let [s (str "." path el ".png") + k (-> el + (string/replace "_" "-") + (string/replace " " "-") + (string/lower-case) + (str size) + (str social-type))] + [k `(js/require ~s)]))) + +(defn- get-files + [path] + (->> (io/file path) + file-seq + (filter #(string/ends-with? % "png")) + (map #(first (string/split (.getName %) #"@"))) + distinct)) + +(defn- get-socials + [size social-type] + (let [path (str icon-path size "/" social-type "/")] + (into {} (map (require-icon size path social-type) (get-files path))))) + + +(defmacro resolve-socials + [] + (merge + (get-socials "default" "default") + (get-socials "bigger" "default") + (get-socials "default" "solid") + (get-socials "bigger" "solid"))) diff --git a/src/quo/components/utilities/social/loader.cljs b/src/quo/components/utilities/social/loader.cljs new file mode 100644 index 00000000000..9274a8ba6a5 --- /dev/null +++ b/src/quo/components/utilities/social/loader.cljs @@ -0,0 +1,12 @@ +(ns quo.components.utilities.social.loader + (:require-macros [quo.components.utilities.social.loader :as loader]) + (:require [taoensso.timbre :as log])) + +(def ^:private socials (loader/resolve-socials)) + +(defn get-social-image + [social] + (if-let [res (get socials social)] + res + (log/error "could not find source for " social " social icon"))) + diff --git a/src/quo/components/utilities/social/view.cljs b/src/quo/components/utilities/social/view.cljs new file mode 100644 index 00000000000..e0e05306f4a --- /dev/null +++ b/src/quo/components/utilities/social/view.cljs @@ -0,0 +1,44 @@ +(ns quo.components.utilities.social.view + (:require [quo.components.utilities.social.loader :as social-loader] + [react-native.core :as rn] + [schema.core :as schema] + [utils.number])) + +(def ?schema + [:=> + [:cat + [:map {:closed true} + [:accessibility-label {:optional true} [:or keyword? string?]] + [:size {:optional true :default :default} [:or keyword? string?]] + [:type {:optional true :default :default} [:or keyword? string?]] + [:social [:or keyword? string?]] + [:style {:optional true} map?]]] + :any]) + +(defn- social-style + [style] + (assoc style + :width 20 + :height 20)) + +(defn view-internal + "Render a social image. + Props: + - accessibility-label accessibility-label to the rn/image + - style: extra styles to apply to the `rn/image` component. + - size: `:default` or `:bigger` + - type: `:default` or `:solid` + - social: string or keyword, it can contain upper case letters or not. + E.g. all of these are valid and resolve to the same: + :social/github | :github | :GITHUB | \"GITHUB\" | \"github\". + " + [{:keys [social size style accessibility-label type] + :or {size :default + type :default}}] + (let [source (social-loader/get-social-image (str (name social) (name size) (name type)))] + [rn/image + {:accessibility-label accessibility-label + :style (social-style style) + :source source}])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/utilities/token/loader.clj b/src/quo/components/utilities/token/loader.clj new file mode 100644 index 00000000000..69cf6154797 --- /dev/null +++ b/src/quo/components/utilities/token/loader.clj @@ -0,0 +1,34 @@ +(ns quo.components.utilities.token.loader + (:require [clojure.java.io :as io] + [clojure.string :as string])) + +(def ^:private token-path "./resources/images/tokens/mainnet/") + +(defn- clean-filename + "Return a (string) filename without the .png extension and @x suffix." + [file] + (first (string/split (str file) #"@|\.png"))) + +(defn- get-js-require + [filename] + (let [require-path (str "." filename ".png")] + `(js/require ~require-path))) + +(defn- get-file-key + [filename] + (-> filename + (string/split #"\/") + (peek) + (string/lower-case))) + +(defn get-tokens + [] + (let [files (file-seq (io/file token-path)) + png-filenames (keep (fn [file] + (when (string/ends-with? file "png") + (clean-filename file))) + files)] + (zipmap (map get-file-key png-filenames) + (map get-js-require png-filenames)))) + +(defmacro resolve-tokens [] (get-tokens)) diff --git a/src/quo/components/utilities/token/loader.cljs b/src/quo/components/utilities/token/loader.cljs new file mode 100644 index 00000000000..292ead19da3 --- /dev/null +++ b/src/quo/components/utilities/token/loader.cljs @@ -0,0 +1,22 @@ +(ns quo.components.utilities.token.loader + (:require-macros [quo.components.utilities.token.loader :as loader]) + (:require [clojure.string :as string])) + +(def ^:private tokens (loader/resolve-tokens)) +(def ^:private safe-lower-case (comp string/lower-case str)) + +;; NOTE: temporarily workaround added to show USDC and USDT on different chains +;; with different decimals. This should be removed when the we move to CoinGecko API +(def ^:private tokens-with-different-decimals #{"usdc (evm)" "usdc (bsc)" "usdt (evm)" "usdt (bsc)"}) + +(defn- get-token-image* + [token] + (let [lower-case-token-symbol (cond-> token + (keyword? token) name + :always safe-lower-case) + token-symbol (if (tokens-with-different-decimals lower-case-token-symbol) + (-> (string/split lower-case-token-symbol #" ") first) + lower-case-token-symbol)] + (get tokens token-symbol))) + +(def get-token-image (memoize get-token-image*)) diff --git a/src/quo/components/utilities/token/view.cljs b/src/quo/components/utilities/token/view.cljs new file mode 100644 index 00000000000..d6d44cbc22d --- /dev/null +++ b/src/quo/components/utilities/token/view.cljs @@ -0,0 +1,64 @@ +(ns quo.components.utilities.token.view + (:require + [clojure.string :as string] + [quo.components.utilities.token.loader :as token-loader] + [react-native.core :as rn] + [schema.core :as schema] + [utils.number])) + +(def ?schema + [:=> + [:cat + [:map {:closed true} + [:size {:optional true :default 32} [:or keyword? pos-int?]] + [:token {:optional true} [:maybe [:or keyword? string?]]] + [:style {:optional true} map?] + ;; Ignores `token` and uses this as parameter to `rn/image`'s source. + [:image-source {:optional true} [:maybe :schema.common/image-source]]]] + :any]) + +(defn- size->number + "Remove `size-` prefix in size keywords and returns a number useful for styling." + [size] + (-> size name (subs 5) utils.number/parse-int)) + +(defn- token-style + [style size] + (let [size-number (if (keyword? size) + (size->number size) + size)] + (assoc style + :width size-number + :height size-number))) + +(def ^:private b64-png-image-prefix "data:image/png;base64,") +(def ^:const default-token-image (js/require "../resources/images/tokens/default-token.png")) + +(defn- normalize-b64-string + [b64-image] + (if (string/starts-with? b64-image "data:image/") + b64-image + (str b64-png-image-prefix b64-image))) + +(defn view-internal + "Render a token image. + Props: + - style: extra styles to apply to the `rn/image` component. + - size: `:size-nn` or just `nn`, being `nn` any number. Defaults to 32. + - token: string or keyword, it can contain upper case letters or not. + E.g. all of these are valid and resolve to the same: + :token/snt | :snt | :SNT | \"SNT\" | \"snt\". + - image-source: Ignores `token` and uses this as parameter to `rn/image`'s source, it + can be a b64 string representing an image. + " + [{:keys [token size style image-source] + :or {size 32}}] + (let [img-src (if (string? image-source) + (normalize-b64-string image-source) + image-source) + token-image (or img-src (token-loader/get-token-image token) default-token-image)] + [rn/image + {:style (token-style style size) + :source token-image}])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/wallet/account_card/component_spec.cljs b/src/quo/components/wallet/account_card/component_spec.cljs new file mode 100644 index 00000000000..06a2dd5c90a --- /dev/null +++ b/src/quo/components/wallet/account_card/component_spec.cljs @@ -0,0 +1,75 @@ +(ns quo.components.wallet.account-card.component-spec + (:require + [quo.components.wallet.account-card.view :as account-card] + [test-helpers.component :as h])) + +(def username "Alisher account") +(def empty-username "Account 1") +(def ^:private theme :light) + +(defn get-test-data + [{:keys [type watch-only? empty-type? loading? no-metrics?]}] + {:name (if empty-type? empty-username username) + :balance "€1,000.00" + :percentage-value "50%" + :amount "€500.00" + :customization-color :blue + :watch-only? watch-only? + :loading? loading? + :metrics? (not no-metrics?) + :type type}) + +(h/describe "Account_card tests" + (h/test "Renders Default" + (let [data (get-test-data {:type :default})] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-truthy (h/get-by-text username)))) + + (h/test "Renders Watch-Only" + (let [data (get-test-data {:type :watch-only + :watch-only? true})] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-truthy (h/get-by-text username)))) + + (h/test "Renders Add-Account" + (let [data {:type :add-account}] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-truthy (h/get-by-label-text :add-account)))) + + (h/test "Renders Empty" + (let [data (get-test-data {:type :empty + :empty-type? true})] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-truthy (h/get-by-text empty-username)))) + + (h/test "Renders Missing Keypair" + (let [data (get-test-data {:type :missing-keypair})] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-truthy (h/get-by-text username)))) + + (h/test "Add account on press fires correctly" + (let [on-press (h/mock-fn) + data {:type :add-account + :on-press on-press}] + (h/render-with-theme-provider [account-card/view data] theme) + (h/fire-event :on-press (h/get-by-label-text :add-account)) + (h/was-called on-press))) + + (h/test "Renders component without metrics" + (let [data (get-test-data {:type :default + :no-metrics? true})] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-falsy (h/query-by-label-text :metrics)))) + + (h/test "Renders loading state" + (let [data (get-test-data {:type :default + :loading? true})] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-truthy (h/get-by-label-text :loading)))) + + (h/test "Renders loading state without metrics" + (let [data (get-test-data {:type :default + :no-metrics? true + :loading? true})] + (h/render-with-theme-provider [account-card/view data] theme) + (h/is-falsy (h/query-by-label-text :metrics))))) diff --git a/src/quo/components/wallet/account_card/properties.cljs b/src/quo/components/wallet/account_card/properties.cljs new file mode 100644 index 00000000000..23a1dfb83e4 --- /dev/null +++ b/src/quo/components/wallet/account_card/properties.cljs @@ -0,0 +1,14 @@ +(ns quo.components.wallet.account-card.properties + (:require [quo.foundations.colors :as colors])) + +(defn gradient-start-color + [theme customization-color] + (colors/resolve-color customization-color theme 0)) + +(defn gradient-end-color + [theme customization-color] + (colors/resolve-color customization-color theme 6)) + +(defn alert-icon-color + [theme] + (colors/theme-colors colors/neutral-80-opa-60 colors/white-70-blur theme)) diff --git a/src/quo/components/wallet/account_card/schema.cljs b/src/quo/components/wallet/account_card/schema.cljs new file mode 100644 index 00000000000..996665206dd --- /dev/null +++ b/src/quo/components/wallet/account_card/schema.cljs @@ -0,0 +1,32 @@ +(ns quo.components.wallet.account-card.schema) + +(def ^:private ?base + [:map + [:type {:optional true} [:enum :default :watch-only :add-account :empty :missing-keypair]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:metrics? {:optional true} [:maybe :boolean]] + [:on-press {:optional true} [:maybe fn?]]]) + +(def ^:private ?amount + [:map + [:amount {:optional true} [:maybe :string]]]) + +(def ^:private ?card + [:map + [:balance {:optional true} [:maybe :string]] + [:loading? {:optional true} [:maybe :boolean]] + [:name {:optional true} [:maybe :string]] + [:percentage-value {:optional true} [:maybe :string]] + [:emoji {:optional true} [:maybe :string]]]) + +(def ?schema + [:=> + [:catn + [:props + [:multi {:dispatch :type} + [:default [:merge ?base ?amount ?card]] + [:watch-only [:merge ?base ?amount ?card]] + [:missing-keypair [:merge ?base ?amount ?card]] + [:empty [:merge ?base ?card]] + [:add-account ?base]]]] + :any]) diff --git a/src/quo/components/wallet/account_card/style.cljs b/src/quo/components/wallet/account_card/style.cljs new file mode 100644 index 00000000000..03824b7d861 --- /dev/null +++ b/src/quo/components/wallet/account_card/style.cljs @@ -0,0 +1,134 @@ +(ns quo.components.wallet.account-card.style + (:require + [quo.foundations.colors :as colors])) + +(defn text-color + [type theme] + (if (and (or (= :missing-keypair type) + (= :watch-only type)) + (= :light theme)) + colors/neutral-100 + colors/white)) + +(defn card + [{:keys [customization-color type theme pressed? metrics?]}] + (let [missing-keypair? (= :missing-keypair type) + watch-only? (= :watch-only type)] + {:width 161 + :height (if metrics? 88 68) + :background-color (when (and (not missing-keypair?) (not watch-only?)) + (colors/theme-colors + (colors/resolve-color customization-color + theme + (when missing-keypair? (if pressed? 20 10))) + (colors/resolve-color customization-color + theme + (when missing-keypair? (if pressed? 30 20))) + theme)) + :border-radius 16 + :border-style (if missing-keypair? :dashed :solid) + :border-width 1 + :border-color (cond + watch-only? + (colors/theme-colors + (if pressed? colors/neutral-80-opa-10 colors/neutral-80-opa-5) + (if pressed? colors/white-opa-10 colors/white-opa-5) + theme) + missing-keypair? + (colors/theme-colors + (if pressed? colors/neutral-40 colors/neutral-30) + (if pressed? colors/neutral-70 colors/neutral-80) + theme) + :else + colors/neutral-80-opa-10) + :padding-horizontal 12 + :padding-top 6 + :padding-bottom 9})) + +(def profile-container + {:margin-bottom 8 + :flex-direction :row}) + +(def metrics-container + {:flex-direction :row + :align-items :center}) + +(defn account-name + [type theme] + {:color (text-color type theme) + :margin-left 2}) + +(def watch-only-container + {:flex-direction :row + :justify-content :space-between + :align-items :center + :flex 1}) + +(defn account-value + [type theme] + {:color (text-color type theme)}) + +(defn metrics + [type theme] + {:color (if (and (or (= :missing-keypair type) + (= :watch-only type)) + (= :light theme)) + colors/neutral-80-opa-60 + colors/white-opa-70)}) + +(defn separator + [type theme] + {:width 2 + :height 2 + :border-radius 20 + :background-color (if (and (or (= :missing-keypair type) + (= :watch-only type)) + (= :light theme)) + colors/neutral-80-opa-20 + colors/white-opa-40) + :margin-horizontal 4}) + +(defn add-account-container + [{:keys [theme metrics? pressed?]}] + {:width 161 + :height (if metrics? 88 68) + :border-color (colors/theme-colors + (if pressed? colors/neutral-40 colors/neutral-30) + (if pressed? colors/neutral-70 colors/neutral-80) + theme) + :border-width 1 + :border-style :dashed + :align-items :center + :justify-content :center + :border-radius 16 + :padding-vertical 12 + :padding-horizontal 10}) + +(def emoji + {:font-size 10 + :line-height 20}) + +(defn loader-view + [{:keys [width height watch-only? theme missing-keypair?]}] + {:width width + :height height + :background-color (if (and (or missing-keypair? watch-only?) (= :light theme)) + colors/neutral-80-opa-5 + colors/white-opa-10) + :border-radius 6}) + +(def loader-container + {:flex-direction :row + :align-items :center}) + +(def metrics-icon-container + {:margin-left 4}) + +(def gradient-view + {:position :absolute + :bottom 0 + :top 0 + :left 0 + :right 0 + :border-radius 16 + :z-index -1}) diff --git a/src/quo/components/wallet/account_card/view.cljs b/src/quo/components/wallet/account_card/view.cljs new file mode 100644 index 00000000000..df677f5d027 --- /dev/null +++ b/src/quo/components/wallet/account_card/view.cljs @@ -0,0 +1,191 @@ +(ns quo.components.wallet.account-card.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.wallet.account-card.properties :as properties] + [quo.components.wallet.account-card.schema :as component-schema] + [quo.components.wallet.account-card.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.customization-colors :as customization-colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [schema.core :as schema])) + +(defn- loading-view + [{:keys [customization-color type theme metrics?]}] + (let [missing-keypair? (= :missing-keypair type) + watch-only? (= :watch-only type) + empty-type? (= :empty type)] + [rn/view + {:accessibility-label :loading + :style (style/card {:customization-color customization-color + :type type + :theme theme + :pressed? false + :metrics? metrics?})} + [rn/view {:style style/loader-container} + [rn/view + {:style (assoc (style/loader-view {:width 16 + :height 16 + :missing-keypair? missing-keypair? + :watch-only? watch-only? + :theme theme}) + :margin-right 8 + :margin-top 2)}] + [rn/view {:style style/watch-only-container} + [rn/view + {:style (style/loader-view {:width 57 + :height 8 + :missing-keypair? missing-keypair? + :watch-only? watch-only? + :theme theme})}] + (when watch-only? [icon/icon :i/reveal {:color colors/neutral-50 :size 12}])]] + [rn/view + {:style (assoc (style/loader-view {:width (if empty-type? 56 80) + :height 16 + :missing-keypair? missing-keypair? + :watch-only? watch-only? + :theme theme}) + :margin-top + 13)}] + (when metrics? + [rn/view + {:accessibility-label :metrics + :style (assoc (style/loader-view {:width (if empty-type? 37 96) + :height 8 + :missing-keypair? missing-keypair? + :watch-only? watch-only? + :theme theme}) + :margin-top + 10)}])])) + +(defn- metrics-percentage + [type theme account-percentage] + [text/text + {:weight :medium + :size :paragraph-2 + :accessibility-label :metrics + :style (style/metrics type theme)} + account-percentage]) + +(defn- metrics-info + [type theme account-amount] + [:<> + [rn/view (style/separator type theme)] + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/metrics type theme)} + account-amount] + [rn/view {:style style/metrics-icon-container} + [icon/icon + :i/positive + {:color (colors/theme-colors (if (or (= :missing-keypair type) + (= :watch-only type)) + colors/neutral-50 + colors/white-opa-70) + colors/white-opa-70 + theme) + :size 16}]]]) + +(defn- gradient-overview + [theme customization-color] + [linear-gradient/linear-gradient + {:colors [(properties/gradient-start-color theme customization-color) + (properties/gradient-end-color theme customization-color)] + :style style/gradient-view + :start {:x 0 :y 0} + :end {:x 1 :y 0}}]) + +(defn- user-account + [{:keys [name balance percentage-value loading? amount customization-color type emoji metrics? + on-press]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false)) + watch-only? (= :watch-only type) + missing-keypair? (= :missing-keypair type)] + (if loading? + [loading-view + {:customization-color customization-color + :type type + :theme theme + :metrics? metrics?}] + [rn/pressable + {:on-press-in on-press-in + :on-press-out on-press-out + :style (style/card {:customization-color customization-color + :type type + :theme theme + :pressed? pressed? + :metrics? metrics?}) + :on-press on-press} + (when (and customization-color (not watch-only?) (not missing-keypair?)) + [customization-colors/overlay + {:customization-color customization-color + :border-radius 16 + :theme theme + :pressed? pressed?}]) + [rn/view {:style style/profile-container} + [rn/view {:style {:padding-bottom 2 :margin-right 2}} + [text/text {:style style/emoji} emoji]] + [rn/view {:style style/watch-only-container} + [text/text + {:size :paragraph-2 + :weight :medium + :number-of-lines 1 + :max-width 110 + :margin-right 4 + :ellipis-mode :tail + :style (style/account-name type theme)} + name] + (when watch-only? [icon/icon :i/reveal {:color colors/neutral-50 :size 12}]) + (when missing-keypair? + [icon/icon :i/alert {:color (properties/alert-icon-color theme) :size 12}])]] + [text/text + {:size :heading-2 + :weight :semi-bold + :style (style/account-value type theme)} + balance] + (when metrics? + [rn/view {:style style/metrics-container} + [metrics-percentage type theme percentage-value] + (when (not= :empty type) + [metrics-info type theme amount])]) + (when watch-only? + [gradient-overview theme customization-color])]))) + +(defn- add-account-view + [{:keys [on-press metrics?]}] + (let [theme (quo.context/use-theme) + [pressed? set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/pressable + {:on-press on-press + :on-press-in on-press-in + :on-press-out on-press-out + :style (style/add-account-container {:theme theme + :metrics? metrics? + :pressed? pressed?})} + [button/button + {:on-press on-press + :type :ghost + :size 32 + :icon true + :accessibility-label :add-account + :pressed? pressed? + :icon-only? true} + :i/add]])) + +(defn- view-internal + [{:keys [type] :as props}] + (case type + (:watch-only :default :empty :missing-keypair) [user-account props] + :add-account [add-account-view props] + nil)) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/account_origin/component_spec.cljs b/src/quo/components/wallet/account_origin/component_spec.cljs new file mode 100644 index 00000000000..bffa1117f6b --- /dev/null +++ b/src/quo/components/wallet/account_origin/component_spec.cljs @@ -0,0 +1,106 @@ +(ns quo.components.wallet.account-origin.component-spec + (:require + [quo.core :as quo] + [test-helpers.component :as h])) + +(def ^:private theme :light) + +(h/describe "account origin tests" + (h/test "component renders" + (h/render-with-theme-provider [quo/account-origin + {:type :recovery-phrase + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (-> (h/expect (h/get-by-translation-text :t/origin)) + (.toBeTruthy))) + + (h/test "recovery phrase icon is visible when :type is :recovery-phrase" + (h/render-with-theme-provider [quo/account-origin + {:type :recovery-phrase + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (h/is-truthy (h/get-by-label-text :recovery-phrase-icon))) + + (h/test "recovery phrase icon is visible when :type is :recovery-phrase" + (h/render-with-theme-provider [quo/account-origin + {:type :recovery-phrase + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (h/is-truthy (h/get-by-label-text :recovery-phrase-icon))) + + (h/test "private-key-icon is visible when :type is :private-key" + (h/render-with-theme-provider [quo/account-origin + {:type :private-key + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (h/is-truthy (h/get-by-label-text :private-key-icon))) + + (h/test "derivation-path-icon is visible when :type is :default-keypair" + (h/render-with-theme-provider [quo/account-origin + {:type :default-keypair + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (h/is-truthy (h/get-by-label-text :derivation-path-icon))) + + (h/test "derivation-path-icon is visible when :type is :recovery-phrase" + (h/render-with-theme-provider [quo/account-origin + {:type :recovery-phrase + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (h/is-truthy (h/get-by-label-text :derivation-path-icon))) + + (h/test "on-device text is visible when :stored is :on-device" + (h/render-with-theme-provider [quo/account-origin + {:type :recovery-phrase + :stored :on-device + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (-> (h/expect (h/get-by-translation-text :t/on-device)) + (.toBeTruthy))) + + (h/test "on-keycard text is visible when :stored is :on-keycard" + (h/render-with-theme-provider [quo/account-origin + {:type :recovery-phrase + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name"}] + theme) + (-> (h/expect (h/get-by-translation-text :t/on-keycard)) + (.toBeTruthy))) + + (h/test "on-press is called when :type is :recovery-phrase" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [quo/account-origin + {:type :recovery-phrase + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name" + :on-press on-press}] + theme) + (h/fire-event :press (h/query-by-label-text :derivation-path-button)) + (h/was-called on-press))) + + (h/test "on-press is called when :type is :default-keypair" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [quo/account-origin + {:type :default-keypair + :stored :on-keycard + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Test Name" + :on-press on-press}] + theme) + (h/fire-event :press (h/query-by-label-text :derivation-path-button)) + (h/was-called on-press)))) diff --git a/src/quo/components/wallet/account_origin/schema.cljs b/src/quo/components/wallet/account_origin/schema.cljs new file mode 100644 index 00000000000..0949e6d8cd0 --- /dev/null +++ b/src/quo/components/wallet/account_origin/schema.cljs @@ -0,0 +1,32 @@ +(ns quo.components.wallet.account-origin.schema) + +(def ^:private ?base + [:map + [:type {:optional true} [:enum :default-keypair :recovery-phrase :private-key]] + [:stored {:optional true} [:enum :on-device :on-keycard]]]) + +(def ^:private ?keypair-name + [:map + [:keypair-name {:optional true} [:maybe :string]]]) + +(def ^:private ?default-keypair + [:map + [:profile-picture {:optional true} [:maybe :schema.common/image-source]] + [:derivation-path {:optional true} [:maybe :string]] + [:on-press {:optional true} [:maybe fn?]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]) + +(def ^:private ?recovery-phrase + [:map + [:derivation-path {:optional true} [:maybe :string]] + [:on-press {:optional true} [:maybe fn?]]]) + +(def ?schema + [:=> + [:catn + [:props + [:multi {:dispatch :type} + [:default-keypair [:merge ?base ?default-keypair ?keypair-name]] + [:recovery-phrase [:merge ?base ?recovery-phrase ?keypair-name]] + [:private-key ?base]]]] + :any]) diff --git a/src/quo/components/wallet/account_origin/style.cljs b/src/quo/components/wallet/account_origin/style.cljs new file mode 100644 index 00000000000..28cda4ce943 --- /dev/null +++ b/src/quo/components/wallet/account_origin/style.cljs @@ -0,0 +1,45 @@ +(ns quo.components.wallet.account-origin.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +(defn title + [color] + {:margin-horizontal 12 + :margin-top 8 + :margin-bottom 4 + :color color}) + +(defn row-container + [type theme] + {:flex-direction :row + :padding-horizontal 12 + :padding-top 8 + :margin (when (= :derivation-path type) 8) + :border-radius 12 + :border-width (when (= :derivation-path type) 1) + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :padding-bottom (if (not= :private-key type) 8 12)}) + +(def icon-container + {:margin-right 8}) + +(defn stored-title + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :margin-right 4}) + +(def row-subtitle-container + {:flex-direction :row + :align-items :center}) + +(def right-icon-container + {:justify-content :center}) + +(def row-content-container + {:flex 1}) diff --git a/src/quo/components/wallet/account_origin/view.cljs b/src/quo/components/wallet/account_origin/view.cljs new file mode 100644 index 00000000000..a08d1211604 --- /dev/null +++ b/src/quo/components/wallet/account_origin/view.cljs @@ -0,0 +1,111 @@ +(ns quo.components.wallet.account-origin.view + (:require + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.wallet.account-origin.schema :as component-schema] + [quo.components.wallet.account-origin.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(defn- row-title + [type keypair-name] + [text/text + {:weight :medium + :size :paragraph-1} + (case type + :default-keypair (i18n/label :t/user-keypair {:name keypair-name}) + :derivation-path (i18n/label :t/derivation-path) + keypair-name)]) + +(defn- row-icon + [customization-color profile-picture type secondary-color] + (case type + :default-keypair [user-avatar/user-avatar + {:size :xxs + :customization-color customization-color + :profile-picture profile-picture}] + :recovery-phrase [icons/icon + :i/seed + {:accessibility-label :recovery-phrase-icon + :color secondary-color}] + :private-key [icons/icon + :i/key + {:accessibility-label :private-key-icon + :color secondary-color}] + :derivation-path [icons/icon + :i/derivated-path + {:accessibility-label :derivation-path-icon + :color secondary-color}] + nil)) + +(defn- row-view + [{:keys [type theme secondary-color customization-color profile-picture title stored subtitle + on-press]}] + [rn/view {:style (style/row-container type theme)} + [rn/view {:style style/icon-container} + [row-icon customization-color profile-picture type secondary-color]] + [rn/view + {:style style/row-content-container} + [row-title type title] + [rn/view {:style style/row-subtitle-container} + [text/text + {:weight :regular + :size :paragraph-2 + :style (style/stored-title theme)} + subtitle] + (when (= :on-keycard stored) + [icons/icon + :i/keycard-card + {:color secondary-color}])]] + (when (and on-press (= :derivation-path type)) + [rn/pressable + {:accessibility-label :derivation-path-button + :on-press on-press + :style style/right-icon-container} + [icons/icon + :i/options + {:color secondary-color}]])]) + +(defn- list-view + [{:keys [type stored customization-color profile-picture keypair-name theme secondary-color]}] + (let [stored-name (if (= :on-device stored) + (i18n/label :t/on-device) + (i18n/label :t/on-keycard))] + [row-view + {:type type + :stored stored + :customization-color customization-color + :profile-picture profile-picture + :title keypair-name + :subtitle stored-name + :theme theme + :secondary-color secondary-color}])) + +(defn- card-view + [theme derivation-path secondary-color on-press] + [row-view + {:type :derivation-path + :subtitle derivation-path + :theme theme + :on-press on-press + :secondary-color secondary-color}]) + +(defn view-internal + [{:keys [type derivation-path on-press] :as props}] + (let [theme (quo.context/use-theme) + secondary-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)] + [rn/view {:style (style/container theme)} + [text/text + {:weight :regular + :size :paragraph-2 + :style (style/title secondary-color)} + (i18n/label :t/origin)] + [list-view (assoc props :secondary-color secondary-color)] + (when (not= :private-key type) + [card-view theme derivation-path secondary-color on-press])])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/account_overview/component_spec.cljs b/src/quo/components/wallet/account_overview/component_spec.cljs new file mode 100644 index 00000000000..dcb36aa2424 --- /dev/null +++ b/src/quo/components/wallet/account_overview/component_spec.cljs @@ -0,0 +1,66 @@ +(ns quo.components.wallet.account-overview.component-spec + (:require + [quo.components.wallet.account-overview.view :as account-overview] + [test-helpers.component :as h])) + +(h/describe "Account overview test" + (h/test "renders correct account name" + (h/render [account-overview/view + {:state :default + :account-name "Diamond Hand"}]) + (h/is-truthy (h/get-by-text "Diamond Hand"))) + + (h/test "renders correct account value" + (h/render [account-overview/view + {:current-value "20.0$"}]) + (h/is-truthy (h/get-by-text "20.0$"))) + + (h/test "renders correct account changes" + (h/render [account-overview/view + {:currency-change "€0.00" + :percentage-change "0.00%"}]) + (h/is-truthy (h/get-by-text "0.00%")) + (h/is-truthy (h/get-by-text "€0.00"))) + + (h/test "renders correct timeframe" + (h/render [account-overview/view + {:time-frame :custom + :time-frame-string "15 May" + :time-frame-to-string "19 May"}]) + (h/is-truthy (h/get-by-text "15 May")) + (h/is-truthy (h/get-by-text "19 May"))) + + (h/test "renders correct account timeframe" + (h/render [account-overview/view + {:time-frame :one-week + :currency-change "€0.00" + :percentage-change "0.00%"}]) + (h/is-truthy (h/get-by-translation-text :t/one-week-int))) + + (h/test "renders correct account timeframe" + (h/render [account-overview/view + {:time-frame :one-month + :currency-change "€0.00" + :percentage-change "0.00%"}]) + (h/is-truthy (h/get-by-translation-text :t/one-month-int))) + + (h/test "renders correct account timeframe" + (h/render [account-overview/view + {:time-frame :three-months + :currency-change "€0.00" + :percentage-change "0.00%"}]) + (h/is-truthy (h/get-by-translation-text :t/three-months-int))) + + (h/test "renders correct account timeframe" + (h/render [account-overview/view + {:time-frame :one-year + :currency-change "€0.00" + :percentage-change "0.00%"}]) + (h/is-truthy (h/get-by-translation-text :t/one-year))) + + (h/test "renders correct account timeframe" + (h/render [account-overview/view + {:time-frame :all-time + :currency-change "€0.00" + :percentage-change "0.00%"}]) + (h/is-truthy (h/get-by-translation-text :t/all-time)))) diff --git a/src/quo/components/wallet/account_overview/schema.cljs b/src/quo/components/wallet/account_overview/schema.cljs new file mode 100644 index 00000000000..6914757cc25 --- /dev/null +++ b/src/quo/components/wallet/account_overview/schema.cljs @@ -0,0 +1,20 @@ +(ns quo.components.wallet.account-overview.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:account-name {:optional true} [:maybe :string]] + [:currency-change {:optional true} [:maybe :string]] + [:current-value {:optional true} [:maybe :string]] + [:percentage-change {:optional true} [:maybe :string]] + [:time-frame-string {:optional true} [:maybe :string]] + [:time-frame-to-string {:optional true} [:maybe :string]] + [:state {:optional true} [:enum :default :loading]] + [:metrics {:optional true} [:enum :negative :positive]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:account {:optional true} [:enum :default :watched-address]] + [:time-frame {:optional true} + [:enum :one-week :one-month :three-months :one-year :all-time :custom]]]]] + :any]) diff --git a/src/quo/components/wallet/account_overview/style.cljs b/src/quo/components/wallet/account_overview/style.cljs new file mode 100644 index 00000000000..b736578dbee --- /dev/null +++ b/src/quo/components/wallet/account_overview/style.cljs @@ -0,0 +1,95 @@ +(ns quo.components.wallet.account-overview.style + (:require + [quo.foundations.colors :as colors])) + +(def account-overview-wrapper + {:height 110 + :align-items :center + :justify-content :center}) + +(defn account-name + [color] + {:margin-bottom 4 + :color color}) + +(def reveal-icon + {:margin-left 4 + :margin-top 2}) + +(def current-value + {:margin-bottom 2}) + +(def row-centered + {:flex-direction :row + :justify-content :center}) + +(defn bottom-time-text + [margin-right?] + {:margin-right (when margin-right? 8) + :color colors/neutral-80-opa-40}) + +(def right-arrow + {:margin-top 1 + :margin-right 1}) + +(def bottom-time-to-text + {:margin-right 8 + :color colors/neutral-80-opa-40}) + +(defn percentage-change + [customization-color theme] + {:color (colors/resolve-color customization-color theme) + :margin-right 4}) + +(defn dot-separator + [customization-color theme] + {:background-color (colors/theme-colors + (colors/override-color customization-color 40 60) + (colors/override-color customization-color 40 50) + theme) + :margin-right 4 + :margin-top 8 + :width 2 + :height 2}) + +(defn currency-change + [customization-color theme] + {:color (colors/resolve-color customization-color theme) + :margin-right 4}) + +(defn loading-bar-margin-bottom + [{:keys [width height color margin-bottom]}] + {:style {:width width + :height height + :border-radius 6 + :background-color color + :margin-bottom margin-bottom}}) + +(defn loading-bar-margin-right + [color] + {:width 32 + :height 10 + :border-radius 6 + :background-color color + :margin-right 2}) + +(defn loading-bar-margin-right-extra + [{:keys [color width height margin-right]}] + {:width width + :height height + :border-radius 6 + :background-color color + :margin-right margin-right}) + +(defn loading-bar-tiny + [color] + {:width 10 + :height 10 + :border-radius 6 + :background-color color + :margin-right 8}) + +(defn icon-props + [customization-color theme] + {:color (colors/resolve-color customization-color theme) + :size 16}) diff --git a/src/quo/components/wallet/account_overview/view.cljs b/src/quo/components/wallet/account_overview/view.cljs new file mode 100644 index 00000000000..e05f74b84b5 --- /dev/null +++ b/src/quo/components/wallet/account_overview/view.cljs @@ -0,0 +1,131 @@ +(ns quo.components.wallet.account-overview.view + (:require + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.wallet.account-overview.schema :as component-schema] + [quo.components.wallet.account-overview.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(defn- loading-state + [color] + [rn/view + {:style {:height 130 + :align-items :center}} + [rn/view + (style/loading-bar-margin-bottom {:color color + :width 104 + :height 14 + :margin-bottom 14})] + [rn/view + (style/loading-bar-margin-bottom {:color color + :width 152 + :height 20 + :margin-bottom 12})] + [rn/view {:style {:flex-direction :row}} + [rn/view {:style (style/loading-bar-margin-right color)}] + [rn/view {:style (style/loading-bar-margin-right color)}] + [rn/view + {:style (style/loading-bar-margin-right-extra {:color color + :width 62 + :height 10 + :margin-right 4})}] + [rn/view {:style (style/loading-bar-tiny color)}]]]) + +(defn- time-string + [time-frame time-frame-string] + (case time-frame + :one-week (i18n/label :t/one-week-int) + :one-month (i18n/label :t/one-month-int) + :three-months (i18n/label :t/three-months-int) + :one-year (i18n/label :t/one-year) + :all-time (i18n/label :t/all-time) + time-frame-string)) + +(defn- custom-time-frame + [time-frame-to-string] + [:<> + [icons/icon :i/positive-right + {:size 16 + :container-style style/right-arrow + :color colors/neutral-80-opa-90}] + [text/text + {:weight :medium + :size :paragraph-2 + :style style/bottom-time-to-text} + time-frame-to-string]]) + +(defn- numeric-changes + [percentage-change currency-change customization-color theme up?] + [:<> + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/percentage-change customization-color theme)} + percentage-change] + [rn/view + {:style (style/dot-separator customization-color theme)}] + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/currency-change customization-color theme)} + currency-change] + [icons/icon + (if up? + :i/positive + :i/negative) + (style/icon-props customization-color theme)]]) + +(defn- account-details + [account-name account theme] + [rn/view {:style {:flex-direction :row}} + [text/text + {:weight :medium + :size :paragraph-1 + :style (style/account-name (colors/theme-colors colors/neutral-100 colors/white theme))} + account-name] + (when (= account :watched-address) + [icons/icon :i/reveal + {:container-style style/reveal-icon + :color (colors/theme-colors colors/neutral-80-opa-40 + colors/white-opa-40 + theme)}])]) + +(defn- view-internal + [{:keys [state account time-frame time-frame-string time-frame-to-string account-name current-value + percentage-change currency-change metrics customization-color container-style] + :or {customization-color :blue}}] + (let [theme (quo.context/use-theme) + time-frame-string (time-string time-frame time-frame-string) + up? (= metrics :positive)] + [rn/view {:style (merge container-style style/account-overview-wrapper)} + (if (= :loading state) + [loading-state (colors/theme-colors colors/neutral-5 colors/neutral-90 theme)] + [rn/view + {:style {:height 130 + :align-items :center}} + [account-details account-name account theme] + [text/text + {:weight :semi-bold + :size :heading-1 + :style style/current-value} + current-value] + [rn/view {:style style/row-centered} + (when (seq time-frame-string) + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/bottom-time-text (and (not= :custom time-frame) + (seq time-frame-to-string)))} + time-frame-string]) + (when (and (= :custom time-frame) + (seq time-frame-to-string)) + [custom-time-frame time-frame-to-string]) + (when (and (seq percentage-change) + (seq currency-change)) + [numeric-changes percentage-change currency-change customization-color theme up?])]])])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/account_permissions/component_spec.cljs b/src/quo/components/wallet/account_permissions/component_spec.cljs new file mode 100644 index 00000000000..41eb4ab4830 --- /dev/null +++ b/src/quo/components/wallet/account_permissions/component_spec.cljs @@ -0,0 +1,78 @@ +(ns quo.components.wallet.account-permissions.component-spec + (:require + [quo.components.wallet.account-permissions.view :as account-permissions] + [test-helpers.component :as h] + [utils.address :as utils])) + +(defn- render + [component] + (h/render-with-theme-provider component :light)) + +(def ^:private account + {:name "Trip to Vegas" + :address "0x2f0fbf0a93c5999e9b4410848403a02b38983eb2" + :emoji "😊" + :customization-color :blue}) + +(h/describe "Wallet: Account Permissions" + (h/test "basic render" + (render [account-permissions/view + {:account account}]) + (h/is-truthy (h/get-by-label-text :wallet-account-permissions))) + + (h/test "render with correct account name, emoji and address" + (render [account-permissions/view + {:account account}]) + (h/is-truthy (h/get-by-text (:name account))) + (h/is-truthy (h/get-by-text (:emoji account))) + (h/is-truthy (h/get-by-text (utils/get-short-wallet-address (:address account))))) + + (h/test "render without keycard" + (render [account-permissions/view + {:account account}]) + (h/is-falsy (h/query-by-label-text :wallet-account-permissions-keycard))) + + (h/test "render with keycard" + (render [account-permissions/view + {:account account + :keycard? true}]) + (h/is-truthy (h/get-by-label-text :wallet-account-permissions-keycard))) + + (h/test "render with token details" + (render [account-permissions/view + {:account account + :token-details [{:token "SNT" + :amount "100" + :type :token}]}]) + (h/is-truthy (h/get-by-text "100 SNT"))) + + (h/test "render with multiple token details" + (render [account-permissions/view + {:account account + :token-details [{:token "SNT" + :amount "100" + :type :token} + {:token "ETH" + :amount "18" + :type :token} + {:token "BTM" + :amount "1000" + :type :token}]}]) + (h/is-truthy (h/get-by-text "100 SNT")) + (h/is-truthy (h/get-by-text "18 ETH")) + (h/is-truthy (h/get-by-text "1000 BTM"))) + + (h/test "render with no relevant tokens" + (render [account-permissions/view + {:account account + :token-details []}]) + (h/is-truthy (h/get-by-translation-text :t/no-relevant-tokens))) + + (h/test "render checked? & on-change" + (let [mock-on-change (h/mock-fn)] + (render [account-permissions/view + {:account account + :on-change mock-on-change + :checked? true}]) + (h/fire-event :press (h/get-by-label-text :checkbox-on)) + (h/was-called-with mock-on-change false)))) diff --git a/src/quo/components/wallet/account_permissions/schema.cljs b/src/quo/components/wallet/account_permissions/schema.cljs new file mode 100644 index 00000000000..7eebfe0327b --- /dev/null +++ b/src/quo/components/wallet/account_permissions/schema.cljs @@ -0,0 +1,22 @@ +(ns quo.components.wallet.account-permissions.schema + (:require [quo.components.wallet.required-tokens.schema :as required-tokens-schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:account + [:map + [:name [:maybe :string]] + [:address [:maybe :string]] + [:emoji [:maybe :string]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]] + [:token-details {:optional true} [:maybe [:sequential required-tokens-schema/?schema]]] + [:keycard? {:optional true} [:maybe :boolean]] + [:checked? {:optional true} [:maybe :boolean]] + [:disabled? {:optional true} [:maybe :boolean]] + [:on-change {:optional true} [:maybe fn?]] + [:container-style {:optional true} [:maybe :map]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]] + :any]) diff --git a/src/quo/components/wallet/account_permissions/style.cljs b/src/quo/components/wallet/account_permissions/style.cljs new file mode 100644 index 00000000000..8446ca72075 --- /dev/null +++ b/src/quo/components/wallet/account_permissions/style.cljs @@ -0,0 +1,37 @@ +(ns quo.components.wallet.account-permissions.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:font-size 30 + :border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)}) + +(def row1 + {:flex-direction :row + :height 56 + :padding-horizontal 12 + :align-items :center}) + +(def account-details + {:flex 1 + :margin-horizontal 8}) + +(def name-and-keycard + {:flex-direction :row + :align-items :center + :gap 4}) + +(def row2-content + {:flex-direction :row + :flex-wrap :wrap + :margin-vertical 4 + :margin-horizontal 8}) + +(def no-relevant-tokens + {:color colors/neutral-40 + :margin 4}) + +(def token-and-text + {:margin 4}) diff --git a/src/quo/components/wallet/account_permissions/view.cljs b/src/quo/components/wallet/account_permissions/view.cljs new file mode 100644 index 00000000000..754745197d8 --- /dev/null +++ b/src/quo/components/wallet/account_permissions/view.cljs @@ -0,0 +1,89 @@ +(ns quo.components.wallet.account-permissions.view + (:require [clojure.string :as string] + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.dividers.divider-line.view :as divider-line] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.selectors.selectors.view :as selectors] + [quo.components.wallet.account-permissions.schema :as component-schema] + [quo.components.wallet.account-permissions.style :as style] + [quo.components.wallet.address-text.view :as address-text] + [quo.components.wallet.required-tokens.view :as required-tokens] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(defn- token-details-section + [tokens] + (when tokens + [:<> + [divider-line/view] + [rn/view {:style style/row2-content} + + (if (empty? tokens) + [text/text + {:size :paragraph-2 + :style style/no-relevant-tokens} + (i18n/label :t/no-relevant-tokens)] + + (let [token-length (dec (count tokens))] + (map-indexed + (fn [idx + {:keys [type token amount collectible-name collectible-img-src + token-img-src] + collectible-symbol :symbol}] + ^{:key idx} + [required-tokens/view + {:container-style style/token-and-text + :type type + :amount amount + :token token + :token-img-src token-img-src + :collectible-img-src collectible-img-src + :collectible-name (if (string/blank? collectible-name) + collectible-symbol + collectible-name) + :divider? (not= token-length idx)}]) + tokens)))]])) + +(defn- view-internal + [{:keys + [checked? disabled? on-change token-details keycard? container-style customization-color] + {:keys + [name address emoji] + :as account} :account + :or {customization-color :blue}}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge (style/container theme) container-style) + :accessibility-label :wallet-account-permissions} + [rn/view {:style style/row1} + [account-avatar/view + {:size 32 + :emoji emoji + :customization-color (:customization-color account)}] + [rn/view {:style style/account-details} + [rn/view {:style style/name-and-keycard} + [text/text + {:size :paragraph-1 + :weight :semi-bold} name] + (when keycard? + [icons/icon :i/keycard-card + {:accessibility-label :wallet-account-permissions-keycard + :size 20 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}])] + [address-text/view + {:address address + :format :short}]] + [selectors/view + {:type :checkbox + :checked? checked? + :customization-color customization-color + :disabled? disabled? + :on-change on-change}]] + + [token-details-section token-details]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/address_text/schema.cljs b/src/quo/components/wallet/address_text/schema.cljs new file mode 100644 index 00000000000..6f917403e23 --- /dev/null +++ b/src/quo/components/wallet/address_text/schema.cljs @@ -0,0 +1,18 @@ +(ns quo.components.wallet.address-text.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:address {:optional true} [:maybe :string]] + [:blur? {:optional true} [:maybe :boolean]] + [:format {:optional true} [:enum :short :long]] + [:networks {:optional true} + [:maybe [:sequential [:map [:network-name :keyword] [:short-name :string]]]]] + [:full-address? {:optional true} [:maybe :boolean]] + ;; TODO: size and weight are text schemas and should be imported here + ;; https://github.com/status-im/status-mobile/issues/19443 + [:size {:optional true} [:maybe :keyword]] + [:weight {:optional true} [:maybe :keyword]]]]] + :any]) diff --git a/src/quo/components/wallet/address_text/style.cljs b/src/quo/components/wallet/address_text/style.cljs new file mode 100644 index 00000000000..396c0ad15bd --- /dev/null +++ b/src/quo/components/wallet/address_text/style.cljs @@ -0,0 +1,11 @@ +(ns quo.components.wallet.address-text.style + (:require [quo.foundations.colors :as colors])) + +(defn address-text + [format blur? theme] + (if (and (= format :long) blur?) + {:color colors/white} + (when (= format :short) + {:color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}))) diff --git a/src/quo/components/wallet/address_text/view.cljs b/src/quo/components/wallet/address_text/view.cljs new file mode 100644 index 00000000000..fdd275c998c --- /dev/null +++ b/src/quo/components/wallet/address_text/view.cljs @@ -0,0 +1,48 @@ +(ns quo.components.wallet.address-text.view + (:require [clojure.string :as string] + [quo.components.markdown.text :as text] + [quo.components.wallet.address-text.schema :as component-schema] + [quo.components.wallet.address-text.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [schema.core :as schema] + [utils.address :as utils])) + +(defn- colored-network-text + [{:keys [theme network size weight]}] + [text/text + {:size size + :weight weight + :style {:color (colors/resolve-color (keyword network) theme)}} + (str network ":")]) + +(defn- view-internal + [{:keys [networks address blur? format full-address? size weight] + :or {size :paragraph-2}}] + (let [theme (quo.context/use-theme) + network-colored-text (map #(colored-network-text {:theme theme + :network % + :weight weight + :size size})) + [splitted-networks splitted-address] (and full-address? + (as-> address $ + (string/split $ ":") + [(butlast $) (last $)])) + address-internal (if full-address? splitted-address address) + networks-internal (if full-address? + splitted-networks + (map :short-name networks)) + address-text [text/text + {:size size + ;; TODO: monospace font + ;; https://github.com/status-im/status-mobile/issues/17009 + :weight (or weight :monospace) + :style (style/address-text format blur? theme)} + (if (= format :short) + (utils/get-short-wallet-address address-internal) + address-internal)]] + (as-> networks-internal $ + (into [text/text] network-colored-text $) + (conj $ address-text)))) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/amount_input/component_spec.cljs b/src/quo/components/wallet/amount_input/component_spec.cljs new file mode 100644 index 00000000000..276bd233126 --- /dev/null +++ b/src/quo/components/wallet/amount_input/component_spec.cljs @@ -0,0 +1,35 @@ +(ns quo.components.wallet.amount-input.component-spec + (:require + [oops.core :as oops] + [quo.components.wallet.amount-input.view :as amount-input] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(defn- render + [component] + (h/render-with-theme-provider component :light)) + +(h/describe "Amount input component" + (h/test "Renders with default value" + (let [text-expected 0] + (render [amount-input/view {:value text-expected}]) + (h/is-truthy (h/query-by-label-text :amount-input)))) + + (h/test "When the value = minimum dec button is disabled" + (render [amount-input/view + {:value 0 + :min-value 0}]) + (h/is-truthy + (oops/oget (h/get-by-label-text :amount-input-dec-button) "props" "accessibilityState" "disabled"))) + + (h/test "When the value = maximum inc button is disabled" + (render [amount-input/view + {:value 100 + :max-value 100}]) + (h/is-truthy + (oops/oget (h/get-by-label-text :amount-input-inc-button) "props" "accessibilityState" "disabled"))) + + (h/test "Renders the error state" + (render [amount-input/view {:status :error :value 10}]) + (h/is-equal (colors/resolve-color :danger :light) + (oops/oget (h/get-by-label-text :amount-input) "props" "style" "color")))) diff --git a/src/quo/components/wallet/amount_input/schema.cljs b/src/quo/components/wallet/amount_input/schema.cljs new file mode 100644 index 00000000000..394cdc65dae --- /dev/null +++ b/src/quo/components/wallet/amount_input/schema.cljs @@ -0,0 +1,15 @@ +(ns quo.components.wallet.amount-input.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:status {:optional true} [:maybe [:enum :default :error]]] + [:on-inc-press {:optional true} [:maybe fn?]] + [:on-dec-press {:optional true} [:maybe fn?]] + [:container-style {:optional true} [:maybe :map]] + [:min-value {:optional true} [:maybe :int]] + [:max-value {:optional true} [:maybe :int]] + [:value [:maybe :int]]]]] + :any]) diff --git a/src/quo/components/wallet/amount_input/style.cljs b/src/quo/components/wallet/amount_input/style.cljs new file mode 100644 index 00000000000..5832d7c79f3 --- /dev/null +++ b/src/quo/components/wallet/amount_input/style.cljs @@ -0,0 +1,18 @@ +(ns quo.components.wallet.amount-input.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:flex-direction :row + :justify-content :center + :align-items :center}) + +(def input-container {:flex 1}) + +(defn input-text + [theme type] + {:padding 0 + :align :center + :color (if (= type :error) + (colors/resolve-color :danger theme) + (colors/theme-colors colors/neutral-100 colors/white theme))}) diff --git a/src/quo/components/wallet/amount_input/view.cljs b/src/quo/components/wallet/amount_input/view.cljs new file mode 100644 index 00000000000..c52667df1ab --- /dev/null +++ b/src/quo/components/wallet/amount_input/view.cljs @@ -0,0 +1,54 @@ +(ns quo.components.wallet.amount-input.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.markdown.text :as text] + [quo.components.wallet.amount-input.schema :as amount-input.schema] + [quo.components.wallet.amount-input.style :as style] + [quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- amount-button + [{:keys [theme accessibility-label disabled? icon on-press]}] + [button/button + {:icon-only? true + :theme theme + :disabled? disabled? + :type :outline + :accessibility-label accessibility-label + :size 32 + :on-press on-press} + icon]) + +(defn- view-internal + [{:keys [on-inc-press on-dec-press status value min-value max-value + container-style] + :or {value 0 + min-value 0 + max-value 999999999}}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge style/container container-style)} + [amount-button + {:theme theme + :accessibility-label :amount-input-dec-button + :icon :i/remove + :on-press on-dec-press + :disabled? (>= min-value value)}] + [rn/view {:style style/input-container} + [text/text + {:number-of-lines 1 + :accessibility-label :amount-input + :weight :semi-bold + :size :heading-1 + :align-self :center + :style (style/input-text theme (or status :default))} + value]] + [amount-button + {:theme theme + :icon :i/add + :accessibility-label :amount-input-inc-button + :on-press on-inc-press + :disabled? (>= value max-value)}]])) + +(def view (schema/instrument #'view-internal amount-input.schema/?schema)) diff --git a/src/quo/components/wallet/approval_label/component_spec.cljs b/src/quo/components/wallet/approval_label/component_spec.cljs new file mode 100644 index 00000000000..6dfc61a5f04 --- /dev/null +++ b/src/quo/components/wallet/approval_label/component_spec.cljs @@ -0,0 +1,46 @@ +(ns quo.components.wallet.approval-label.component-spec + (:require [quo.components.wallet.approval-label.view :as approval-label] + [test-helpers.component :as h])) + +(h/describe "Wallet: Approval Label" + (h/test "with :approve status" + (h/render-with-theme-provider + [approval-label/view + {:status :approve + :customization-color :blue + :token-value "100" + :token-symbol "SNT"}]) + (h/is-truthy (h/get-by-translation-text :t/approve-amount-symbol + {:token-amount "100" :token-symbol "SNT"}))) + + (h/test "with :approving status" + (h/render-with-theme-provider + [approval-label/view + {:status :approving + :customization-color :blue + :token-value "50" + :token-symbol "DAI"}]) + (h/is-truthy (h/get-by-translation-text :t/approving-amount-symbol + {:token-amount "50" :token-symbol "DAI"}))) + + (h/test "with :approved status" + (h/render-with-theme-provider + [approval-label/view + {:status :approved + :customization-color :blue + :token-value "5" + :token-symbol "ETH"}]) + (h/is-truthy (h/get-by-translation-text :t/approved-amount-symbol + {:token-amount "5" :token-symbol "ETH"}))) + + (h/test "on-press event is called when button is pressed" + (let [mock-fn (h/mock-fn)] + (h/render-with-theme-provider + [approval-label/view + {:status :approve + :customization-color :blue + :token-value "11" + :token-symbol "DAI" + :button-props {:on-press mock-fn}}]) + (h/fire-event :press (h/get-by-translation-text :t/approve)) + (h/was-called mock-fn)))) diff --git a/src/quo/components/wallet/approval_label/schema.cljs b/src/quo/components/wallet/approval_label/schema.cljs new file mode 100644 index 00000000000..02e91233d07 --- /dev/null +++ b/src/quo/components/wallet/approval_label/schema.cljs @@ -0,0 +1,15 @@ +(ns quo.components.wallet.approval-label.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:status [:enum :approve :approving :approved]] + [:token-value :string] + [:token-symbol :string] + [:container-style {:optional true} [:maybe :map]] + [:show-view-button? {:optional true} [:maybe :boolean]] + [:button-props {:optional true} [:maybe :map]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]] + :any]) diff --git a/src/quo/components/wallet/approval_label/style.cljs b/src/quo/components/wallet/approval_label/style.cljs new file mode 100644 index 00000000000..a6888f5b405 --- /dev/null +++ b/src/quo/components/wallet/approval_label/style.cljs @@ -0,0 +1,28 @@ +(ns quo.components.wallet.approval-label.style + (:require [quo.foundations.colors :as colors])) + +(def ^:const top-hole-view-height 24) + +(defn container + [customization-color theme] + {:background-color (colors/resolve-color customization-color theme 5) + :align-items :center + :padding-horizontal 12 + :padding-vertical 8 + :padding-top (+ 8 top-hole-view-height) + :gap 8 + :border-bottom-left-radius 16 + :border-bottom-right-radius 16 + :flex-direction :row}) + +(def content + {:flex-direction :row + :align-items :center + :flex 1 + :padding-vertical 4 + :gap 4}) + +(defn message + [theme] + {:flex 1 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo/components/wallet/approval_label/view.cljs b/src/quo/components/wallet/approval_label/view.cljs new file mode 100644 index 00000000000..ec0d374ea3a --- /dev/null +++ b/src/quo/components/wallet/approval_label/view.cljs @@ -0,0 +1,72 @@ +(ns quo.components.wallet.approval-label.view + (:require [oops.core :as oops] + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.wallet.approval-label.schema :as approval-label.schema] + [quo.components.wallet.approval-label.style :as style] + quo.context + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def ^:private status-icons + {:approve :i/alert + :approving :i/pending-state + :approved :i/check}) + +(def ^:private status-message + {:approve :t/approve-amount-symbol + :approving :t/approving-amount-symbol + :approved :t/approved-amount-symbol}) + +(defn- view-internal + [{:keys [status token-value token-symbol + container-style button-props show-view-button?] + :as props}] + (let [theme (quo.context/use-theme) + customization-color (or (:customization-color props) :blue) + [container-width set-container-width] (rn/use-state 0) + on-layout (rn/use-callback + #(set-container-width + (oops/oget % :nativeEvent :layout :width)))] + [hole-view/hole-view + {:holes [{:x 0 + :y 0 + :height style/top-hole-view-height + :width container-width + :borderBottomStartRadius 16 + :borderBottomEndRadius 16}] + :style {:margin-top (- style/top-hole-view-height)} + :on-layout on-layout} + [rn/view + {:style (merge (style/container customization-color theme) + container-style) + :accessibility-label :approval-label} + [rn/view {:style style/content} + [icon/icon + (status-icons status) + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :size 16}] + [text/text + {:size :paragraph-2 + :style (style/message theme)} + (i18n/label (status-message status) + {:token-amount token-value + :token-symbol token-symbol})]] + (when (and button-props + (or (= status :approve) + (and (not= status :approve) show-view-button?))) + [button/button + (merge {:type (if (= status :approve) :primary :grey) + :background (when-not (= status :approve) :blur) + :customization-color customization-color + :size 24} + button-props) + (i18n/label (if (= status :approve) + :t/approve + :t/view))])]])) + +(def view (schema/instrument #'view-internal approval-label.schema/?schema)) diff --git a/src/quo/components/wallet/confirmation_progress/component_spec.cljs b/src/quo/components/wallet/confirmation_progress/component_spec.cljs new file mode 100644 index 00000000000..b1ca1f3bf2c --- /dev/null +++ b/src/quo/components/wallet/confirmation_progress/component_spec.cljs @@ -0,0 +1,115 @@ +(ns quo.components.wallet.confirmation-progress.component-spec + (:require [quo.core :as quo] + [test-helpers.component :as h])) + + +(defn- get-test-data + [{:keys [state network] + :or {state :pending network :mainnet}}] + {:counter 0 + :total-box 85 + :progress-value 10 + :network network + :state state + :customization-color :blue}) + +(h/describe "Confirmation Progress" + (h/test "component renders when state is sending and network is optimism" + (h/render-with-theme-provider [quo/confirmation-progress + (get-test-data {:state :sending + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :progress-box)))) + +(h/test "component renders when state is confirmed and network is optimism" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :confirmed + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is finalising and network is optimism" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :finalising + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is finalized and network is optimism" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :finalized + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is error and network is optimism" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :error + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is sending and network is arbitrum" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :sending + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is confirmed and network is arbitrum" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :confirmed + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is finalising and network is arbitrum" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :finalising + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is finalized and network is arbitrum" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :finalized + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is error and network is arbitrum" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :error + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :progress-box))) + +(h/test "component renders when state is pending and network is mainnet" + (h/render-with-theme-provider + [quo/confirmation-progress (get-test-data {})]) + (h/is-truthy (h/get-by-label-text :mainnet-progress-box))) + +(h/test "component renders when state is sending and network is mainnet" + (h/render-with-theme-provider + [quo/confirmation-progress (get-test-data {:state :sending})]) + (h/is-truthy (h/get-by-label-text :mainnet-progress-box))) + +(h/test "component renders when state is confirmed and network is mainnet" + (h/render-with-theme-provider + [quo/confirmation-progress (get-test-data {:state :confirmed})]) + (h/is-truthy (h/get-by-label-text :mainnet-progress-box))) + +(h/test "component renders when state is finalising and network is mainnet" + (h/render-with-theme-provider + [quo/confirmation-progress (get-test-data {:state :finalising})]) + (h/is-truthy (h/get-by-label-text :mainnet-progress-box))) + +(h/test "component renders when state is finalized and network is mainnet" + (h/render-with-theme-provider + [quo/confirmation-progress (get-test-data {:state :finalized})]) + (h/is-truthy (h/get-by-label-text :mainnet-progress-box))) + +(h/test "component renders when state is error and network is mainnet" + (h/render-with-theme-provider + [quo/confirmation-progress + (get-test-data {:state :error})]) + (h/is-truthy (h/get-by-label-text :mainnet-progress-box))) diff --git a/src/quo/components/wallet/confirmation_progress/schema.cljs b/src/quo/components/wallet/confirmation_progress/schema.cljs new file mode 100644 index 00000000000..a6dcb19eb24 --- /dev/null +++ b/src/quo/components/wallet/confirmation_progress/schema.cljs @@ -0,0 +1,14 @@ +(ns quo.components.wallet.confirmation-progress.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:total-box {:optional true} [:maybe :int]] + [:counter {:optional true} [:maybe :int]] + [:progress-value {:optional true} [:maybe :int]] + [:network {:optional true} [:enum :mainnet :optimism :arbitrum]] + [:state {:optional true} [:enum :pending :sending :confirmed :finalising :finalized :error]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]] + :any]) diff --git a/src/quo/components/wallet/confirmation_progress/style.cljs b/src/quo/components/wallet/confirmation_progress/style.cljs new file mode 100644 index 00000000000..511ba496f86 --- /dev/null +++ b/src/quo/components/wallet/confirmation_progress/style.cljs @@ -0,0 +1,9 @@ +(ns quo.components.wallet.confirmation-progress.style) + +(def progress-box-container + {:flex-direction :row + :align-items :center + :padding-horizontal 12 + :padding-bottom 10 + :padding-top 4 + :flex-wrap :wrap}) diff --git a/src/quo/components/wallet/confirmation_progress/view.cljs b/src/quo/components/wallet/confirmation_progress/view.cljs new file mode 100644 index 00000000000..173ead81311 --- /dev/null +++ b/src/quo/components/wallet/confirmation_progress/view.cljs @@ -0,0 +1,70 @@ +(ns quo.components.wallet.confirmation-progress.view + (:require [quo.components.wallet.confirmation-progress.schema :as component-schema] + [quo.components.wallet.confirmation-progress.style :as style] + [quo.components.wallet.progress-bar.view :as progress-box] + [react-native.core :as rn] + [schema.core :as schema])) + +(def ^:private max-progress 100) +(def ^:private min-progress 0) + +(defn- calculate-box-state + [state counter index] + (cond + (and (= state :sending) (>= counter index) (< index 3)) :confirmed + (and (= state :confirmed) (>= counter index) (< index 5)) :confirmed + (and (= state :finalising) (>= counter index) (< index 5)) :confirmed + (and (= state :finalising) (>= counter index) (> index 4) (< index 20)) :finalized + (and (= state :finalized) (>= counter index) (< index 5)) :confirmed + (and (= state :finalized) (>= counter index) (> index 4)) :finalized + (and (= state :error) (>= counter index) (< index 2)) :error + :else :pending)) + +(defn- calculate-box-state-sidenet + [state] + (case state + :error :error + (:confirmed :finalising :finalized) :finalized + :pending)) + +(defn- calculate-progressed-value + [state progress-value] + (case state + :finalising progress-value + :finalized max-progress + min-progress)) + +(defn- progress-boxes-mainnet + [{:keys [state counter total-box]}] + [rn/view + {:accessibility-label :mainnet-progress-box + :style style/progress-box-container} + (let [numbers (range 1 total-box)] + (doall (for [n numbers] + [progress-box/view + {:state (calculate-box-state state counter n) + :customization-color :blue + :key n}])))]) + +(defn- progress-boxes-sidenet + [{:keys [state progress-value]}] + [rn/view + {:accessibility-label :progress-box + :style style/progress-box-container} + [progress-box/view + {:state (calculate-box-state-sidenet state) + :customization-color :success}] + [progress-box/view + {:state (calculate-box-state-sidenet state) + :full-width? true + :progressed-value (calculate-progressed-value state progress-value) + :customization-color :blue}]]) + +(defn- view-internal + [{:keys [network] :as props}] + (case network + :mainnet [progress-boxes-mainnet props] + (:arbitrum :optimism) [progress-boxes-sidenet props] + nil)) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/keypair/component_spec.cljs b/src/quo/components/wallet/keypair/component_spec.cljs new file mode 100644 index 00000000000..74c4fbd4b08 --- /dev/null +++ b/src/quo/components/wallet/keypair/component_spec.cljs @@ -0,0 +1,68 @@ +(ns quo.components.wallet.keypair.component-spec + (:require + [quo.components.wallet.keypair.view :as keypair] + [test-helpers.component :as h])) + +(def ^:private theme :light) + +(def accounts + [{:account-props {:customization-color :turquoise + :size 32 + :emoji "\uD83C\uDFB2" + :type :default + :name "Trip to Vegas" + :address "0x0ah...71a"} + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}] + :action :none}]) + +(def default-details + {:full-name "John Doe" + :address "zQ3...6fBd2"}) + +(def other-details {:full-name "Metamask"}) + +(h/describe "Wallet: Keypair" + (h/test "Default keypair title renders" + (h/render-with-theme-provider [keypair/view + {:accounts accounts + :customization-color :blue + :type :default-keypair + :stored :on-device + :action :selector + :details default-details}] + theme) + (h/is-truthy (h/get-by-label-text :title))) + + (h/test "On device renders" + (h/render-with-theme-provider [keypair/view + {:accounts accounts + :customization-color :blue + :type :other + :stored :on-device + :action :selector + :details other-details}] + theme) + (h/is-truthy (h/get-by-label-text :details))) + + (h/test "Selector action renders" + (h/render-with-theme-provider [keypair/view + {:accounts accounts + :customization-color :blue + :type :other + :stored :on-keycard + :action :selector + :details other-details}] + theme) + (h/is-truthy (h/get-by-label-text :radio-off))) + + (h/test "Options action renders" + (h/render-with-theme-provider [keypair/view + {:accounts accounts + :customization-color :blue + :type :other + :stored :on-keycard + :action :options + :details other-details}] + theme) + (h/is-truthy (h/get-by-label-text :options-button)))) diff --git a/src/quo/components/wallet/keypair/style.cljs b/src/quo/components/wallet/keypair/style.cljs new file mode 100644 index 00000000000..ebd63791478 --- /dev/null +++ b/src/quo/components/wallet/keypair/style.cljs @@ -0,0 +1,41 @@ +(ns quo.components.wallet.keypair.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn container + [{:keys [blur? customization-color theme selected? container-style]}] + (merge {:border-radius 16 + :border-width 1 + :padding-bottom 8 + :border-color (cond + (and selected? blur?) colors/white + selected? (colors/resolve-color customization-color theme) + blur? colors/white-opa-5 + :else (colors/theme-colors colors/neutral-10 + colors/neutral-80 + theme))} + container-style)) + +(def header-container + {:padding-horizontal 12 + :padding-top 8 + :padding-bottom 12 + :flex-direction :row + :align-items :center}) + +(def title-container + {:flex-direction :row + :align-items :center + :justify-content :space-between}) + +(defn subtitle + [blur? theme] + {:color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) + +(defn dot + [blur? theme] + (merge (subtitle blur? theme) + {:bottom (if platform/ios? 2 -2)})) diff --git a/src/quo/components/wallet/keypair/view.cljs b/src/quo/components/wallet/keypair/view.cljs new file mode 100644 index 00000000000..a4e16360194 --- /dev/null +++ b/src/quo/components/wallet/keypair/view.cljs @@ -0,0 +1,116 @@ +(ns quo.components.wallet.keypair.view + (:require + [clojure.string :as string] + [quo.components.avatars.icon-avatar :as icon-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.icon :as icon] + [quo.components.list-items.account-list-card.view :as account-list-card] + [quo.components.markdown.text :as text] + [quo.components.selectors.selectors.view :as selectors] + [quo.components.wallet.keypair.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn keypair-string + [full-name] + (let [first-name (first (string/split full-name #" "))] + (i18n/label :t/keypair-title {:name first-name}))) + +(defn avatar + [{:keys [blur?] + {:keys [full-name]} :details + avatar-type :type + customization-color :customization-color + profile-picture :profile-picture}] + (if (= avatar-type :default-keypair) + [user-avatar/user-avatar + {:full-name full-name + :size :small + :status-indicator? false + :customization-color customization-color + :profile-picture profile-picture}] + [icon-avatar/icon-avatar + {:size :size-32 + :icon :i/seed + :blur? blur? + :border? true}])) + +(defn title-view + [{:keys [details action selected? type blur? customization-color on-options-press]}] + (let [theme (quo.context/use-theme) + {:keys [full-name]} details] + [rn/view + {:style style/title-container + :accessibility-label :title} + [text/text {:weight :semi-bold} + (if (= type :default-keypair) (keypair-string full-name) full-name)] + (case action + :none nil + :selector [selectors/view + {:type :radio + :checked? selected? + :blur? blur? + :customization-color customization-color}] + [rn/pressable {:on-press on-options-press} + [icon/icon :i/options + {:color (if blur? + colors/white-opa-70 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)) + :accessibility-label :options-button}]])])) + +(defn details-view + [{:keys [details stored type blur? theme]}] + (let [{:keys [address]} details] + [rn/view + {:style {:flex-direction :row + :align-items :center} + :accessibility-label :details} + [text/text + {:size :paragraph-2 + :style (style/subtitle blur? theme)} + address] + (when (= type :default-keypair) + [text/text + {:size :paragraph-2 + :style (style/dot blur? theme)} + " ∙ "]) + [text/text + {:size :paragraph-2 + :style (style/subtitle blur? theme)} + (if (= stored :on-device) (i18n/label :t/on-device) (i18n/label :t/on-keycard))] + (when (= stored :on-keycard) + [rn/view {:style {:margin-left 4}} + [icon/icon :i/keycard-card + {:size 16 + :color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}]])])) + +(defn- acc-list-card + [item & _rest] + [account-list-card/view item]) + +(defn view + [{:keys [accounts action container-style selected? on-press] :as props}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:style (style/container (assoc props + :selected? selected? + :container-style container-style + :theme theme)) + :on-press #(when (= action :selector) (on-press)) + :pointer-events (when (= action :selector) :box-only)} + [rn/view {:style style/header-container} + [avatar props] + [rn/view + {:style {:margin-left 8 + :flex 1}} + [title-view (assoc props :selected? selected?)] + [details-view props]]] + [rn/flat-list + {:data accounts + :render-fn acc-list-card + :separator [rn/view {:style {:height 8}}] + :style {:padding-horizontal 8}}]])) diff --git a/src/quo/components/wallet/missing_keypairs/component_spec.cljs b/src/quo/components/wallet/missing_keypairs/component_spec.cljs new file mode 100644 index 00000000000..775aadcd758 --- /dev/null +++ b/src/quo/components/wallet/missing_keypairs/component_spec.cljs @@ -0,0 +1,24 @@ +(ns quo.components.wallet.missing-keypairs.component-spec + (:require + [quo.components.wallet.missing-keypairs.view :as missing-keypairs] + [test-helpers.component :as h])) + +(def ^:private theme :dark) + +(def props + {:blur? true + :container-style {} + :on-options-press (fn []) + :keypairs [{:type :seed + :name name + :key-uid "123" + :accounts [{:customization-color :turquoise + :emoji "\uD83C\uDFB2" + :type :default}]}]}) + +(h/describe "Wallet: Missing key pairs" + (h/test "Missing key pair title renders" + (h/render-with-theme-provider [missing-keypairs/view props] + theme) + (h/is-truthy (h/get-by-label-text :title)) + (h/is-truthy (h/get-by-label-text :t/import-to-use-derived-accounts)))) diff --git a/src/quo/components/wallet/missing_keypairs/style.cljs b/src/quo/components/wallet/missing_keypairs/style.cljs new file mode 100644 index 00000000000..e0ae6175d33 --- /dev/null +++ b/src/quo/components/wallet/missing_keypairs/style.cljs @@ -0,0 +1,34 @@ +(ns quo.components.wallet.missing-keypairs.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:border-width 1 + :border-radius 16 + :padding 8 + :border-color colors/warning-50-opa-20 + :background-color colors/warning-50-opa-5}) + +(def title-icon-container + {:top 1}) + +(def title-info-container + {:padding-left 8 + :flex 1}) + +(def title-row + {:display :flex + :flex-direction :row + :justify-content :space-between}) + +(def title-container + {:align-items :flex-start + :flex-direction :row + :padding-left 4 + :padding-bottom 12}) + +(defn subtitle + [blur? theme] + {:color (if blur? + colors/white-opa-40 + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}) diff --git a/src/quo/components/wallet/missing_keypairs/view.cljs b/src/quo/components/wallet/missing_keypairs/view.cljs new file mode 100644 index 00000000000..3dc08ce4f7f --- /dev/null +++ b/src/quo/components/wallet/missing_keypairs/view.cljs @@ -0,0 +1,62 @@ +(ns quo.components.wallet.missing-keypairs.view + (:require + [quo.components.buttons.button.view :as button] + [quo.components.icon :as icon] + [quo.components.list-items.missing-keypair.view :as missing-keypair] + [quo.components.markdown.text :as text] + [quo.components.wallet.missing-keypairs.style :as style] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [utils.i18n :as i18n])) + +(defn title-view + [{:keys [keypairs blur? on-import-press show-import-all?]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :title + :style style/title-container} + [rn/view + {:style style/title-icon-container} + [icon/icon :i/info + {:size 20 + :color colors/warning-60}]] + [rn/view + {:style style/title-info-container} + [rn/view {:style style/title-row} + [text/text + {:weight :medium + :style {:color colors/warning-60}} + (i18n/label :t/amount-missing-keypairs + {:amount (str (count keypairs))})] + (when show-import-all? + [button/button + {:type :outline + :background :blur + :size 24 + :on-press on-import-press} + (i18n/label :t/import)])] + [text/text + {:size :paragraph-2 + :style (style/subtitle blur? theme)} + (i18n/label :t/import-to-use-derived-accounts)]]])) + +(defn- missing-keypair-item + [keypair _index _separators + {:keys [blur? on-options-press]}] + [missing-keypair/view + {:keypair keypair + :blur? blur? + :on-options-press on-options-press}]) + +(defn view + [{:keys [blur? keypairs container-style on-options-press] :as props}] + [rn/view + {:style (merge style/container container-style)} + [title-view props] + [rn/flat-list + {:data keypairs + :render-fn missing-keypair-item + :render-data {:blur? blur? + :on-options-press on-options-press} + :separator [rn/view {:style {:height 8}}]}]]) diff --git a/src/quo/components/wallet/network_amount/component_spec.cljs b/src/quo/components/wallet/network_amount/component_spec.cljs new file mode 100644 index 00000000000..c2d5811a559 --- /dev/null +++ b/src/quo/components/wallet/network_amount/component_spec.cljs @@ -0,0 +1,12 @@ +(ns quo.components.wallet.network-amount.component-spec + (:require + [quo.components.wallet.network-amount.view :as network-amount] + [test-helpers.component :as h])) + +(h/describe "Wallet: Network Amount" + (h/test "Amount renders" + (h/render-with-theme-provider + [network-amount/view + {:amount "5.123" + :token :eth}]) + (h/is-truthy (h/get-by-text "5.123 ETH")))) diff --git a/src/quo/components/wallet/network_amount/schema.cljs b/src/quo/components/wallet/network_amount/schema.cljs new file mode 100644 index 00000000000..12ebd1fc133 --- /dev/null +++ b/src/quo/components/wallet/network_amount/schema.cljs @@ -0,0 +1,10 @@ +(ns quo.components.wallet.network-amount.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:amount {:optional true} [:maybe :string]] + [:token {:optional true} [:or :keyword :string]]]]] + :any]) diff --git a/src/quo/components/wallet/network_amount/style.cljs b/src/quo/components/wallet/network_amount/style.cljs new file mode 100644 index 00000000000..f8bd84f73af --- /dev/null +++ b/src/quo/components/wallet/network_amount/style.cljs @@ -0,0 +1,18 @@ +(ns quo.components.wallet.network-amount.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:flex-direction :row + :align-items :center + :height 18}) + +(def text + {:margin-left 4 + :margin-right 8}) + +(defn divider + [theme] + {:width 1 + :height 8 + :background-color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}) diff --git a/src/quo/components/wallet/network_amount/view.cljs b/src/quo/components/wallet/network_amount/view.cljs new file mode 100644 index 00000000000..d699bf04fea --- /dev/null +++ b/src/quo/components/wallet/network_amount/view.cljs @@ -0,0 +1,25 @@ +(ns quo.components.wallet.network-amount.view + (:require + [clojure.string :as string] + [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.components.wallet.network-amount.schema :as network-amount-schema] + [quo.components.wallet.network-amount.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- view-internal + [{:keys [amount token]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/container} + [token/view {:token token :size :size-12}] + [text/text + {:weight :medium + :size :paragraph-2 + :style style/text} + (str amount " " (string/upper-case (clj->js token)))] + [rn/view + {:style (style/divider theme)}]])) + +(def view (schema/instrument #'view-internal network-amount-schema/?schema)) diff --git a/src/quo/components/wallet/network_bridge/component_spec.cljs b/src/quo/components/wallet/network_bridge/component_spec.cljs new file mode 100644 index 00000000000..949a800c7b5 --- /dev/null +++ b/src/quo/components/wallet/network_bridge/component_spec.cljs @@ -0,0 +1,40 @@ +(ns quo.components.wallet.network-bridge.component-spec + (:require + [quo.components.wallet.network-bridge.view :as network-bridge] + [test-helpers.component :as h])) + +(h/describe "Wallet: Network Bridge" + (h/test "Amount render-with-theme-providers" + (h/render-with-theme-provider [network-bridge/view + {:amount "50 SNT" + :network :ethereum + :status :default}]) + (h/is-truthy (h/get-by-text "50 SNT"))) + + (h/test "Network label render" + (h/render-with-theme-provider [network-bridge/view + {:amount "50 SNT" + :network :optimism + :status :default}]) + (h/is-truthy (h/get-by-text "Optimism"))) + + (h/test "Locked state" + (h/render-with-theme-provider [network-bridge/view + {:amount "50 SNT" + :network :optimism + :status :locked}]) + (h/is-truthy (h/get-by-label-text :lock))) + + (h/test "Loading state" + (h/render-with-theme-provider [network-bridge/view + {:amount "50 SNT" + :network :optimism + :status :loading}]) + (h/is-truthy (h/get-by-label-text :loading))) + + (h/test "Disabled state" + (h/render-with-theme-provider [network-bridge/view + {:amount "50 SNT" + :network :optimism + :status :disabled}]) + (h/has-style (h/get-by-label-text :container) {:opacity 0.3}))) diff --git a/src/quo/components/wallet/network_bridge/schema.cljs b/src/quo/components/wallet/network_bridge/schema.cljs new file mode 100644 index 00000000000..5d9f76f3615 --- /dev/null +++ b/src/quo/components/wallet/network_bridge/schema.cljs @@ -0,0 +1,16 @@ +(ns quo.components.wallet.network-bridge.schema) + +(def ^:private ?network-bridge-status + [:enum :edit :loading :locked :disabled :default]) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:network {:optional true} [:maybe :keyword]] + [:status {:optional true} [:maybe ?network-bridge-status]] + [:amount {:optional true} [:maybe :string]] + [:container-style {:optional true} [:maybe :map]] + [:on-press {:optional true} [:maybe fn?]]]]] + :any]) diff --git a/src/quo/components/wallet/network_bridge/style.cljs b/src/quo/components/wallet/network_bridge/style.cljs new file mode 100644 index 00000000000..477a2c4e9e8 --- /dev/null +++ b/src/quo/components/wallet/network_bridge/style.cljs @@ -0,0 +1,36 @@ +(ns quo.components.wallet.network-bridge.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [network state theme] + {:width 136 + :height 44 + :border-width 1 + :border-radius 12 + :padding-vertical 5 + :padding-horizontal 8 + :border-color (colors/resolve-color network theme) + :opacity (when (= state :disabled) 0.3)}) + +(defn add-container + [theme] + {:border-style :dashed + :border-color (colors/theme-colors colors/neutral-30 colors/neutral-70 theme) + :justify-content :center + :align-items :center + :padding-vertical 0 + :padding-horizontal 0}) + +(defn loading-skeleton + [theme] + {:width 32 + :height 10 + :border-radius 3 + :margin-vertical 4 + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme)}) + +(def network-icon + {:width 12 + :height 12 + :margin-right 4}) diff --git a/src/quo/components/wallet/network_bridge/view.cljs b/src/quo/components/wallet/network_bridge/view.cljs new file mode 100644 index 00000000000..098bfd95287 --- /dev/null +++ b/src/quo/components/wallet/network_bridge/view.cljs @@ -0,0 +1,73 @@ +(ns quo.components.wallet.network-bridge.view + (:require + [clojure.string :as string] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.wallet.network-bridge.schema :as network-bridge-schema] + [quo.components.wallet.network-bridge.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn network-bridge-add + [{:keys [network state theme container-style on-press]}] + [rn/pressable + {:style [(style/container network state theme) + (style/add-container theme) + container-style] + :on-press on-press} + [icon/icon :i/edit + {:size 12 + :color (colors/theme-colors colors/neutral-50 + colors/neutral-60 + theme)}]]) + +(defn- network->text + [network] + (cond (not network) "" + (= network :mainnet) "Ethereum" + :else (string/capitalize (name network)))) + +(defn view-internal + [{:keys [network status amount container-style on-press on-long-press] :as args}] + (let [theme (quo.context/use-theme)] + (if (= status :edit) + [network-bridge-add (assoc args :theme theme)] + [rn/pressable + {:style [(style/container network status theme) container-style] + :accessible true + :accessibility-label :container + :on-press on-press + :on-long-press on-long-press} + (if (= status :loading) + [rn/view + {:style (style/loading-skeleton theme) + :accessible true + :accessibility-label :loading}] + [rn/view + {:style {:flex-direction :row + :justify-content :space-between}} + [text/text + {:size :paragraph-2 + :weight :medium} amount] + (when (= status :locked) + [icon/icon :i/locked + {:size 12 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :accessible true + :accessibility-label :lock}])]) + [rn/view + {:style {:flex-direction :row + :align-items :center}} + [rn/image + {:source (resources/get-network network) + :style style/network-icon}] + [text/text + {:size :label + :weight :medium + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + (network->text network)]]]))) + +(def view (schema/instrument #'view-internal network-bridge-schema/?schema)) diff --git a/src/quo/components/wallet/network_link/helpers.cljs b/src/quo/components/wallet/network_link/helpers.cljs new file mode 100644 index 00000000000..14217480982 --- /dev/null +++ b/src/quo/components/wallet/network_link/helpers.cljs @@ -0,0 +1,23 @@ +(ns quo.components.wallet.network-link.helpers) + +(def ^:private central-figure-width 63) + +(defn calculate-side-lines-path-1x + "Calculates the `d` attribute for the side lines based on the SVG width." + [width] + (let [side-offset (/ (- width central-figure-width) 2)] + {:left (str "M0 57 L" side-offset " 57") + :right (str "M" (+ side-offset central-figure-width) " 1 L" width " 1")})) + +(defn calculate-transform + "Calculates the transform attribute for the central figure based on the SVG width." + [width] + (let [translate-x (/ (- width central-figure-width) 2)] + (str "translate(" translate-x " 0)"))) + +(defn calculate-side-lines-path-2x + "Calculates the `d` attribute for the side lines based on the SVG width." + [width] + (let [side-offset (/ (- width central-figure-width) 2)] + {:left (str "M0 113 L" side-offset " 113") + :right (str "M" (+ side-offset central-figure-width) " 1 L" width " 1")})) diff --git a/src/quo/components/wallet/network_link/schema.cljs b/src/quo/components/wallet/network_link/schema.cljs new file mode 100644 index 00000000000..47bc057e7e2 --- /dev/null +++ b/src/quo/components/wallet/network_link/schema.cljs @@ -0,0 +1,11 @@ +(ns quo.components.wallet.network-link.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:shape {:optional true} [:maybe [:enum :linear :1x :2x]]] + [:source {:optional true} [:maybe :schema.quo/networks]] + [:destination {:optional true} [:maybe :schema.quo/networks]]]]] + :any]) diff --git a/src/quo/components/wallet/network_link/style.cljs b/src/quo/components/wallet/network_link/style.cljs new file mode 100644 index 00000000000..63e335f4add --- /dev/null +++ b/src/quo/components/wallet/network_link/style.cljs @@ -0,0 +1,34 @@ +(ns quo.components.wallet.network-link.style) + +(def left-circle-container + {:position :absolute + :left -3}) + +(def right-circle-container + {:position :absolute + :right -3}) + +(def bottom-left-circle-container + {:position :absolute + :bottom -3 + :left -3}) + +(def top-right-circle-container + {:position :absolute + :top -3 + :right -3}) + +(def link-linear-container + {:flex-direction :row + :align-items :center + :height 10}) + +(def link-1x-container + {:flex 1 + :height 57 + :justify-content :center}) + +(def link-2x-container + {:flex 1 + :height 112 + :justify-content :center}) diff --git a/src/quo/components/wallet/network_link/view.cljs b/src/quo/components/wallet/network_link/view.cljs new file mode 100644 index 00000000000..2258f277ffc --- /dev/null +++ b/src/quo/components/wallet/network_link/view.cljs @@ -0,0 +1,178 @@ +(ns quo.components.wallet.network-link.view + (:require + [oops.core :refer [oget]] + [quo.components.wallet.network-link.helpers :as helpers] + [quo.components.wallet.network-link.schema :as component-schema] + [quo.components.wallet.network-link.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.svg :as svg] + [schema.core :as schema])) + +(defn- circle + [fill stroke] + [svg/svg + {:height "8" + :width "8"} + [svg/circle + {:cx "4" + :cy "4" + :r "3.5" + :fill fill + :stroke stroke + :strokeWidth "1"}]]) + +(defn- line + [{:keys [stroke-color source-color destination-color width theme]}] + [svg/svg + {:height "10" + :width "100%" + :view-box (str "0 0 " width " 10")} + [svg/path + {:d (str "M0,5 L" width ",5") + :stroke stroke-color + :stroke-width "1"}] + [svg/defs + [svg/linear-gradient + {:id "gradient" + :x1 "0%" + :x2 "100%" + :y1 "0%" + :y2 "0%" + :gradient-units "objectBoundingBox"} + [svg/stop {:offset "0%" :stop-color (colors/resolve-color source-color theme)}] + [svg/stop {:offset "100%" :stop-color (colors/resolve-color destination-color theme)}]]]]) + +(defn link-linear + [{:keys [source destination]}] + (let [theme (quo.context/use-theme) + [container-width + set-container-width] (rn/use-state 100) + stroke-color "url(#gradient)" + source-color (colors/resolve-color source theme) + destination-color (colors/resolve-color destination theme) + fill-color (colors/theme-colors colors/white colors/neutral-90 theme) + on-layout (rn/use-callback #(set-container-width + (oget % :nativeEvent :layout :width)))] + [rn/view + {:style style/link-linear-container + :on-layout on-layout} + [line + {:stroke-color stroke-color + :source-color source-color + :destination-color destination-color + :width container-width + :theme theme}] + [rn/view {:style style/left-circle-container} + [circle fill-color source-color]] + [rn/view {:style style/right-circle-container} + [circle fill-color destination-color]]])) + +(defn link-1x + [{:keys [source destination]}] + (let [theme (quo.context/use-theme) + [container-width + set-container-width] (rn/use-state 100) + stroke-color "url(#gradient)" + source-color (colors/resolve-color source theme) + destination-color (colors/resolve-color destination theme) + fill-color (colors/theme-colors colors/white colors/neutral-90 theme) + view-box (str "0 0 " container-width " 58") + side-lines-path (helpers/calculate-side-lines-path-1x container-width) + central-transform (helpers/calculate-transform container-width) + on-layout (rn/use-callback #(set-container-width + (oget % :nativeEvent :layout :width)))] + [rn/view + {:style style/link-1x-container + :on-layout on-layout} + [svg/svg + {:xmlns "http://www.w3.org/2000/svg" + :height "100%" + :width "100%" + :view-box view-box + :fill :none} + [svg/path + {:d (:left side-lines-path) + :stroke source-color}] + [svg/path + {:d + "M63 1L53.6356 1C42.5899 1 33.6356 9.9543 33.6356 21L33.6356 37C33.6356 48.0457 24.6813 57 13.6356 57L2.85889e-05 57" + :transform central-transform + :stroke stroke-color}] + [svg/path + {:d (:right side-lines-path) + :stroke destination-color}] + [svg/defs + [svg/linear-gradient + {:id "gradient" + :x1 "72.271" + :x2 "82.385" + :y1 "5" + :y2 "34.155" + :gradient-units "userSpaceOnUse"} + [svg/stop {:stop-color (colors/resolve-color destination theme)}] + [svg/stop {:offset "1" :stop-color (colors/resolve-color source theme)}]]]] + [rn/view {:style style/bottom-left-circle-container} + [circle fill-color source-color]] + [rn/view {:style style/top-right-circle-container} + [circle fill-color destination-color]]])) + +(defn link-2x + [{:keys [source destination]}] + (let [theme (quo.context/use-theme) + [container-width + set-container-width] (rn/use-state 100) + stroke-color "url(#gradient)" + source-color (colors/resolve-color source theme) + destination-color (colors/resolve-color destination theme) + fill-color (colors/theme-colors colors/white colors/neutral-90 theme) + view-box (str "0 0 " container-width " 114") + side-lines-path (helpers/calculate-side-lines-path-2x container-width) + central-transform (helpers/calculate-transform container-width) + on-layout (rn/use-callback #(set-container-width + (oget % :nativeEvent :layout :width)))] + [rn/view + {:style style/link-2x-container + :on-layout on-layout} + [svg/svg + {:xmlns "http://www.w3.org/2000/svg" + :height "100%" + :width "100%" + :view-box view-box + :fill :none} + [svg/path + {:d (:left side-lines-path) + :stroke source-color}] + [svg/path + {:d + "M62.9999 1L53.6356 1C42.5899 1 33.6356 9.9543 33.6356 21L33.6356 93C33.6356 104.046 24.6813 113 13.6356 113L5.71778e-05 113" + :transform central-transform + :stroke stroke-color}] + [svg/path + {:d (:right side-lines-path) + :stroke destination-color}] + [svg/defs + [svg/linear-gradient + {:id "gradient" + :x1 "72.2711" + :y1 "5.00001" + :x2 "102.867" + :y2 "49.0993" + :gradient-units "userSpaceOnUse"} + [svg/stop {:stop-color (colors/resolve-color destination theme)}] + [svg/stop {:offset "1" :stop-color (colors/resolve-color source theme)}]]]] + [rn/view {:style style/bottom-left-circle-container} + [circle fill-color source-color]] + [rn/view {:style style/top-right-circle-container} + [circle fill-color destination-color]]])) + +(defn- view-internal + [{:keys [shape container-style] :as props}] + [rn/view {:style container-style} + (case shape + :linear [link-linear props] + :1x [link-1x props] + :2x [link-2x props])]) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/network_routing/animation.cljs b/src/quo/components/wallet/network_routing/animation.cljs new file mode 100644 index 00000000000..7dab4c8019d --- /dev/null +++ b/src/quo/components/wallet/network_routing/animation.cljs @@ -0,0 +1,115 @@ +(ns quo.components.wallet.network-routing.animation + (:require [react-native.reanimated :as reanimated])) + +(def ^:private slider-timing 300) + +(defn show-slider + [opacity-shared-value] + (reanimated/animate opacity-shared-value 1 slider-timing)) + +(defn hide-slider + [opacity-shared-value] + (reanimated/animate opacity-shared-value 0 slider-timing)) + +(defn increase-slider + [width-shared-value height-shared-value] + (reanimated/animate width-shared-value 8 slider-timing) + (reanimated/animate height-shared-value 40 slider-timing)) + +(defn decrease-slider + [width-shared-value height-shared-value] + (reanimated/animate width-shared-value 4 slider-timing) + (reanimated/animate height-shared-value 32 slider-timing)) + +(def ^:private pressed-bar-timing 600) + +(defn move-previous-bars + [{:keys [bars bars-widths-negative]}] + (doseq [[bar-idx bar] (map-indexed vector bars) + :let [new-translation-x (->> (take (inc bar-idx) bars-widths-negative) + (reduce +) + (dec))]] + (reanimated/animate (:translate-x-shared-value bar) + new-translation-x + pressed-bar-timing))) + +(defn move-pressed-bar + [{:keys [bars-widths-negative number-previous-bars] + {:keys [translate-x-shared-value]} :bar}] + (let [new-translation-x (reduce + (take number-previous-bars bars-widths-negative))] + (reanimated/animate translate-x-shared-value + new-translation-x + (- pressed-bar-timing 20)))) + +(defn move-next-bars + [{:keys [bars bars-widths-negative number-previous-bars extra-offset add-new-timeout]}] + (doseq [[bar-idx bar] (map-indexed vector bars) + :let [number-bars-before (+ number-previous-bars + (inc bar-idx)) + new-translation-x (->> (take number-bars-before bars-widths-negative) + (reduce +) + (* 1.05))]] + (reanimated/animate (:translate-x-shared-value bar) new-translation-x pressed-bar-timing) + (add-new-timeout + (keyword (str "fix-next-bars-position-" bar-idx)) + (fn [] + (let [translate-x-value (reanimated/get-shared-value (:translate-x-shared-value bar)) + hidden-position (- translate-x-value extra-offset)] + (reanimated/set-shared-value (:translate-x-shared-value bar) hidden-position))) + pressed-bar-timing))) + +(def ^:private max-limit-bar-timing 300) + +(defn show-max-limit-bar + [max-limit-bar-opacity] + (reanimated/animate max-limit-bar-opacity 1 max-limit-bar-timing)) + +(defn hide-max-limit-bar + [max-limit-bar-opacity] + (reanimated/animate max-limit-bar-opacity 0 max-limit-bar-timing)) + +(defn reset-bars-positions + [bars unlock-press-fn add-new-timeout] + (let [bars-reset-timing 500] + (doseq [{:keys [translate-x-shared-value]} bars] + (reanimated/animate translate-x-shared-value 0 bars-reset-timing)) + (add-new-timeout :unlock-press unlock-press-fn bars-reset-timing))) + +(defn align-bars-off-screen + [{:keys [new-network-values network-bars amount->width add-new-timeout]}] + (let [width-to-off-screen (->> new-network-values + (reduce #(- %1 (:amount %2)) 0) + (amount->width))] + (doseq [[bar-idx {new-amount :amount} bar] (map vector (range) new-network-values network-bars)] + (reanimated/set-shared-value (:amount-shared-value bar) new-amount) + (reanimated/set-shared-value (:translate-x-shared-value bar) (* 2 width-to-off-screen)) + (add-new-timeout + (keyword (str "align-bar-" bar-idx)) + #(reanimated/set-shared-value (:translate-x-shared-value bar) width-to-off-screen) + 1)))) + +(def ^:private hide-bar-timing 400) + +(defn hide-pressed-bar + [{:keys [translate-x-shared-value amount-shared-value]} amount->width] + (let [bar-width (amount->width (reanimated/get-shared-value amount-shared-value)) + new-translation-x (- (reanimated/get-shared-value translate-x-shared-value) + bar-width)] + (reanimated/animate translate-x-shared-value new-translation-x hide-bar-timing))) + +(defn update-bar-values-and-reset-animations + [{:keys [new-network-values network-bars amount->width reset-values-fn add-new-timeout + lock-press-fn unlock-press-fn]}] + (lock-press-fn) + (add-new-timeout + :update-bars-values + (fn [] + (align-bars-off-screen {:new-network-values new-network-values + :network-bars network-bars + :amount->width amount->width + :add-new-timeout add-new-timeout}) + (reset-values-fn) + (add-new-timeout :reset-bars + #(reset-bars-positions network-bars unlock-press-fn add-new-timeout) + 100)) + hide-bar-timing)) diff --git a/src/quo/components/wallet/network_routing/component_spec.cljs b/src/quo/components/wallet/network_routing/component_spec.cljs new file mode 100644 index 00000000000..7c91e1958e6 --- /dev/null +++ b/src/quo/components/wallet/network_routing/component_spec.cljs @@ -0,0 +1,30 @@ +(ns quo.components.wallet.network-routing.component-spec + (:require [oops.core :as oops] + [quo.components.wallet.network-routing.view :as network-routing] + [quo.context] + [reagent.core :as reagent] + [test-helpers.component :as h])) + +(h/describe "Network-routing tests" + (let [network {:amount 250 :max-amount 300 :network-name :unknown} + default-props {:networks [network network network] + :total-amount 500 + :requesting-data? false + :on-amount-selected (fn [_new-amount _network-idx] nil)}] + (h/test "Renders Default" + (h/render-with-theme-provider [network-routing/view default-props]) + (h/is-truthy (h/get-by-label-text :network-routing))) + + (h/test "Renders bars inside" + (let [component (h/render-with-theme-provider [network-routing/view default-props]) + rerender-fn #((oops/oget component "rerender") (reagent/as-element %)) + component (h/get-by-label-text :network-routing)] + ;; Fires on-layout callback since the total width is required + (h/fire-event :layout component #js {:nativeEvent #js {:layout #js {:width 1000}}}) + ;; Update props to trigger rerender, otherwise it won't be updated + (rerender-fn [quo.context/provider {:theme :light} + [network-routing/view (assoc default-props :requesting-data? true)]]) + ;; Check number of networks rendered + (->> (js->clj (h/query-all-by-label-text :network-routing-bar)) + (count) + (h/is-equal 3)))))) diff --git a/src/quo/components/wallet/network_routing/schema.cljs b/src/quo/components/wallet/network_routing/schema.cljs new file mode 100644 index 00000000000..25ae71df0ac --- /dev/null +++ b/src/quo/components/wallet/network_routing/schema.cljs @@ -0,0 +1,16 @@ +(ns quo.components.wallet.network-routing.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:networks {:optional true} + [:maybe + [:sequential + [:map + [:amount :int] + [:max-amount :int] + [:network-name [:or :string :keyword]]]]]] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) diff --git a/src/quo/components/wallet/network_routing/style.cljs b/src/quo/components/wallet/network_routing/style.cljs new file mode 100644 index 00000000000..681359e52ec --- /dev/null +++ b/src/quo/components/wallet/network_routing/style.cljs @@ -0,0 +1,79 @@ +(ns quo.components.wallet.network-routing.style + (:require [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(defn container + [container-style theme] + (assoc container-style + :flex-direction :row + :height 64 + :background-color (colors/theme-colors colors/neutral-100-opa-5 colors/neutral-90 theme) + :border-radius 20 + :overflow :hidden)) + +(defn max-limit-bar + [{:keys [opacity-shared-value width]}] + (reanimated/apply-animations-to-style + {:opacity opacity-shared-value} + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :width width + :z-index -1 + :flex-direction :row})) + +(defn max-limit-bar-background + [network-name] + {:flex 1 + :background-color (colors/resolve-color network-name nil 10)}) + +(defn network-bar + [{:keys [bar-max-width on-top? bar-division? theme] + {:keys [network-name translate-x-shared-value]} :bar} + width-shared-value] + (reanimated/apply-animations-to-style + {:width width-shared-value + :transform [{:translate-x translate-x-shared-value}]} + {:max-width bar-max-width + :flex-direction :row + :justify-content :flex-end + :background-color (colors/resolve-color network-name nil) + :z-index (if on-top? 1 0) + :border-right-width (if bar-division? 0 1) + :border-color (colors/theme-colors colors/white colors/neutral-95 theme)})) + +(def slider-container + {:width 40 + :background-color :transparent + :justify-content :center + :align-items :center + :right -20}) + +(def ^:private slider-fixed-styles + {:background-color colors/white + :height 32 + :width 4 + :border-radius 4}) + +(defn slider + [{:keys [width-shared-value height-shared-value opacity-shared-value]}] + (reanimated/apply-animations-to-style + {:width width-shared-value + :height height-shared-value + :opacity opacity-shared-value} + slider-fixed-styles)) + +(def dashed-line + {:width 1 + :height "100%" + :margin-left -1 + :margin-top -1.5}) + +(defn dashed-line-line + [network-name] + {:background-color (colors/resolve-color network-name nil) + :height 3 + :width 1}) + +(def dashed-line-space {:height 4 :width 1}) diff --git a/src/quo/components/wallet/network_routing/view.cljs b/src/quo/components/wallet/network_routing/view.cljs new file mode 100644 index 00000000000..3716fbc1bd1 --- /dev/null +++ b/src/quo/components/wallet/network_routing/view.cljs @@ -0,0 +1,217 @@ +(ns quo.components.wallet.network-routing.view + (:require + [oops.core :as oops] + [quo.components.wallet.network-routing.animation :as animation] + [quo.components.wallet.network-routing.schema :as network-routing-schema] + [quo.components.wallet.network-routing.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [schema.core :as schema] + [utils.number])) + +(def ^:private timeouts (atom {})) + +(defn- add-new-timeout + [k f ms] + (letfn [(exec-fn-and-remove-timeout [] + (f) + (swap! timeouts dissoc k))] + (js/clearTimeout (k @timeouts)) + (swap! timeouts assoc k (js/setTimeout exec-fn-and-remove-timeout ms)))) + +(def get-gesture + (memoize + (fn + [[detecting-gesture? slider-width-shared-value max-amount + slider-height-shared-value + amount-shared-value amount-on-gesture-start width->amount + slider-opacity-shared-value on-new-amount set-detecting-gesture]] + (-> (gesture/gesture-pan) + (gesture/enabled detecting-gesture?) + (gesture/on-begin + (fn [_] + (animation/increase-slider slider-width-shared-value slider-height-shared-value) + (reset! amount-on-gesture-start (reanimated/get-shared-value amount-shared-value)))) + (gesture/on-update + (fn [event] + (let [new-amount (-> (oops/oget event "translationX") + (width->amount) + (+ @amount-on-gesture-start) + (utils.number/value-in-range 1 max-amount))] + (reanimated/set-shared-value amount-shared-value new-amount)))) + (gesture/on-finalize + (fn [_] + (animation/decrease-slider slider-width-shared-value slider-height-shared-value) + (animation/hide-slider slider-opacity-shared-value) + (on-new-amount (reanimated/get-shared-value amount-shared-value)) + (add-new-timeout :turn-off-gesture #(set-detecting-gesture false) 20))))))) + +(defn network-bar + [{:keys [total-width total-amount on-press on-new-amount allow-press? + bar-idx bar-width bar-max-width] + {:keys [amount-shared-value + max-amount] + :as bar} :bar + :as props}] + (let [[detecting-gesture? + set-detecting-gesture] (rn/use-state false) + amount-on-gesture-start (rn/use-ref-atom 0) + slider-width-shared-value (reanimated/use-shared-value 4) + slider-height-shared-value (reanimated/use-shared-value 32) + slider-opacity-shared-value (reanimated/use-shared-value 0) + network-bar-shared-value (reanimated/interpolate amount-shared-value + [0 total-amount] + [0 total-width]) + width->amount #(/ (* % total-amount) total-width) + gesture (get-gesture + [detecting-gesture? slider-width-shared-value max-amount + slider-height-shared-value + amount-shared-value amount-on-gesture-start width->amount + slider-opacity-shared-value on-new-amount set-detecting-gesture]) + on-press (rn/use-callback + (fn [] + (when (and (not detecting-gesture?) allow-press?) + (on-press bar bar-idx bar-width bar-max-width) + (set-detecting-gesture true) + (animation/show-slider slider-opacity-shared-value))) + [detecting-gesture? allow-press? on-press])] + [rn/pressable + {:on-press on-press} + [reanimated/view + {:style (style/network-bar props network-bar-shared-value) + :accessibility-label :network-routing-bar} + [gesture/gesture-detector {:gesture gesture} + [rn/view {:style style/slider-container} + [reanimated/view + {:style (style/slider {:width-shared-value slider-width-shared-value + :height-shared-value slider-height-shared-value + :opacity-shared-value slider-opacity-shared-value})}]]]]])) + +(defn- add-bar-shared-values + [{:keys [amount] :as network}] + (assoc network + :amount-shared-value (reanimated/use-shared-value amount) + :translate-x-shared-value (reanimated/use-shared-value 0))) + +(def ^:private get-negative-amount + (comp - reanimated/get-shared-value :amount-shared-value)) + +(defn- dashed-line + [network-name] + (into [rn/view {:style style/dashed-line}] + (take 19 + (interleave (repeat [rn/view {:style (style/dashed-line-line network-name)}]) + (repeat [rn/view {:style style/dashed-line-space}]))))) + +(defn- network-routing-bars + [{:keys [networks total-width total-amount requesting-data? on-amount-selected]}] + (let [[selected-network-idx + set-selected-network-idx] (rn/use-state nil) + [press-locked? + set-press-locked] (rn/use-state false) + lock-press (rn/use-callback #(set-press-locked true)) + unlock-press (rn/use-callback #(set-press-locked false)) + reset-state-values (rn/use-callback #(set-selected-network-idx nil)) + bar-opacity-shared-value (reanimated/use-shared-value 0) + network-bars (map add-bar-shared-values networks) + amount->width #(* % (/ total-width total-amount)) + bars-widths-negative (map #(-> % get-negative-amount amount->width) + network-bars) + last-bar-idx (dec (count network-bars)) + network-bar-on-press (rn/use-callback + (fn [bar bar-idx bar-width bar-max-width] + (when-not selected-network-idx + (let [[previous-bars + [_ & next-bars]] (split-at bar-idx network-bars)] + (animation/move-previous-bars + {:bars previous-bars + :bars-widths-negative bars-widths-negative}) + (animation/move-pressed-bar + {:bar bar + :bars-widths-negative bars-widths-negative + :number-previous-bars bar-idx}) + (animation/move-next-bars + {:bars next-bars + :bars-widths-negative bars-widths-negative + :number-previous-bars (inc bar-idx) + :extra-offset (max 0 (- bar-max-width bar-width)) + :add-new-timeout add-new-timeout})) + (animation/show-max-limit-bar bar-opacity-shared-value) + (set-selected-network-idx bar-idx))) + [selected-network-idx bars-widths-negative network-bars]) + on-new-amount (rn/use-callback + (fn [new-amount] + (animation/hide-max-limit-bar bar-opacity-shared-value) + (when on-amount-selected + (on-amount-selected new-amount selected-network-idx))) + [on-amount-selected selected-network-idx])] + (rn/use-effect + #(when (and (not requesting-data?) selected-network-idx) + (let [bar (nth network-bars selected-network-idx)] + (animation/hide-pressed-bar bar amount->width)) + (animation/update-bar-values-and-reset-animations + {:new-network-values networks + :network-bars network-bars + :amount->width amount->width + :reset-values-fn reset-state-values + :lock-press-fn lock-press + :unlock-press-fn unlock-press + :add-new-timeout add-new-timeout})) + [requesting-data?]) + [:<> + (doall + (for [[bar-idx bar] (map-indexed vector network-bars) + :let [bar-max-width (amount->width (:max-amount bar)) + bar-width (-> (:amount-shared-value bar) + (reanimated/get-shared-value) + (amount->width)) + hide-division? (or (= last-bar-idx bar-idx) selected-network-idx) + this-bar-selected? (= selected-network-idx bar-idx)]] + ^{:key (str "network-bar-" bar-idx)} + [network-bar + {:bar bar + :bar-idx bar-idx + :bar-width bar-width + :bar-max-width bar-max-width + :total-width total-width + :total-amount total-amount + :bar-division? hide-division? + :on-top? this-bar-selected? + :allow-press? (and (or (not selected-network-idx) this-bar-selected?) + (not requesting-data?) + (not press-locked?)) + :on-press network-bar-on-press + :on-new-amount on-new-amount}])) + + (let [{:keys [max-amount network-name]} (some->> selected-network-idx + (nth network-bars)) + limit-bar-width (amount->width max-amount)] + [reanimated/view + {:style (style/max-limit-bar + {:opacity-shared-value bar-opacity-shared-value + :width limit-bar-width})} + [rn/view {:style (style/max-limit-bar-background network-name)}] + [dashed-line network-name]])])) + +(defn view-internal + [{:keys [networks container-style] :as params}] + (let [theme (quo.context/use-theme) + [total-width + set-total-width] (rn/use-state nil) + on-layout (rn/use-callback #(let [width (oops/oget % "nativeEvent.layout.width")] + (when (not= width total-width) + (set-total-width width))))] + (rn/use-unmount (fn [] + (doseq [[_ living-timeout] @timeouts] + (js/clearTimeout living-timeout)))) + [rn/view + {:accessibility-label :network-routing + :style (style/container container-style theme) + :on-layout on-layout} + (when total-width + ^{:key (str "network-routing-" (count networks))} + [network-routing-bars (assoc params :total-width total-width)])])) + +(def view (schema/instrument #'view-internal network-routing-schema/?schema)) diff --git a/src/quo/components/wallet/progress_bar/component_spec.cljs b/src/quo/components/wallet/progress_bar/component_spec.cljs new file mode 100644 index 00000000000..b5612b36615 --- /dev/null +++ b/src/quo/components/wallet/progress_bar/component_spec.cljs @@ -0,0 +1,114 @@ +(ns quo.components.wallet.progress-bar.component-spec + (:require + [quo.components.wallet.progress-bar.view :as progress-bar] + [quo.foundations.colors :as colors] + [test-helpers.component :as h])) + +(h/describe "Progress bar" + (h/test "pending state in light mode" + (let [theme :light + props {:state :pending + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/neutral-80-opa-5 + :backgroundColor colors/neutral-5}))) + + (h/test "pending state in dark mode" + (let [theme :dark + props {:state :pending + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/neutral-70 + :backgroundColor colors/neutral-80}))) + + (h/test "finalized state with customtization-color blue in light mode" + (let [theme :light + props {:state :finalized + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/neutral-80-opa-5 + :backgroundColor (colors/resolve-color (:customization-color props) theme)}))) + + (h/test "finalized state with customtization-color blue in dark mode" + (let [theme :dark + props {:state :finalized + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/neutral-80-opa-5 + :backgroundColor (colors/resolve-color (:customization-color props) theme)}))) + + (h/test "finalized state with customtization-color army in light mode" + (let [theme :light + props {:state :finalized + :customization-color :army}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/neutral-80-opa-5 + :backgroundColor (colors/resolve-color (:customization-color props) theme)}))) + + (h/test "confirmed state in light mode" + (let [theme :light + props {:state :confirmed + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/neutral-80-opa-5 + :backgroundColor (colors/resolve-color :success theme)}))) + + (h/test "confirmed state in dark mode" + (let [theme :dark + props {:state :confirmed + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/white-opa-5 + :backgroundColor colors/success-60}))) + + (h/test "error state in light mode" + (let [theme :light + props {:state :error + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/neutral-80-opa-5 + :backgroundColor (colors/resolve-color :danger theme)}))) + + (h/test "error state in dark mode" + (let [theme :dark + props {:state :error + :customization-color :blue}] + (h/render-with-theme-provider [progress-bar/view props] theme) + (h/has-style (h/query-by-label-text :progress-bar) + {:height 12 + :width 8 + :borderRadius 3 + :borderColor colors/white-opa-5 + :backgroundColor (colors/resolve-color :danger theme)})))) diff --git a/src/quo/components/wallet/progress_bar/schema.cljs b/src/quo/components/wallet/progress_bar/schema.cljs new file mode 100644 index 00000000000..25be8a42682 --- /dev/null +++ b/src/quo/components/wallet/progress_bar/schema.cljs @@ -0,0 +1,11 @@ +(ns quo.components.wallet.progress-bar.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:progressed-value {:optional true} [:maybe [:or :string :int]]] + [:full-width? {:optional true} [:maybe :boolean]]]]] + :any]) diff --git a/src/quo/components/wallet/progress_bar/style.cljs b/src/quo/components/wallet/progress_bar/style.cljs new file mode 100644 index 00000000000..a8fb77f1175 --- /dev/null +++ b/src/quo/components/wallet/progress_bar/style.cljs @@ -0,0 +1,50 @@ +(ns quo.components.wallet.progress-bar.style + (:require + [quo.foundations.colors :as colors])) + +(defn- border-and-background-color + [customization-color theme] + {:light {:pending {:border-color colors/neutral-80-opa-5 + :background-color colors/neutral-5} + :confirmed {:border-color colors/neutral-80-opa-5 + :background-color (colors/resolve-color :success theme)} + :finalized {:border-color colors/neutral-80-opa-5 + :background-color (colors/resolve-color customization-color theme)} + :error {:border-color colors/neutral-80-opa-5 + :background-color (colors/resolve-color :danger theme)}} + :dark {:pending {:border-color colors/neutral-70 + :background-color colors/neutral-80} + :confirmed {:border-color colors/white-opa-5 + :background-color (colors/resolve-color :success theme)} + :finalized {:border-color colors/neutral-80-opa-5 + :background-color (colors/resolve-color customization-color theme)} + :error {:border-color colors/white-opa-5 + :background-color (colors/resolve-color :danger theme)}}}) + +(def max-value 100) + +(defn root-container + [{:keys [customization-color state full-width?]} theme] + (let [{:keys [background-color border-color]} (get-in (border-and-background-color customization-color + theme) + [theme (if full-width? :pending state)])] + {:height 12 + :flex (when full-width? 1) + :width (when (not full-width?) 8) + :border-radius 3 + :border-width 1 + :border-color border-color + :background-color background-color + :margin-horizontal 1 + :margin-vertical 2})) + +(defn progressed-bar + [{:keys [customization-color state progressed-value]} theme] + (let [{:keys [background-color]} (get-in (border-and-background-color customization-color theme) + [theme state]) + progress (if (> progressed-value max-value) max-value progressed-value)] + {:height 12 + :margin-top -1 + :width (str progress "%") + :border-radius 3 + :background-color background-color})) diff --git a/src/quo/components/wallet/progress_bar/view.cljs b/src/quo/components/wallet/progress_bar/view.cljs new file mode 100644 index 00000000000..0e8ef9bd54e --- /dev/null +++ b/src/quo/components/wallet/progress_bar/view.cljs @@ -0,0 +1,18 @@ +(ns quo.components.wallet.progress-bar.view + (:require + [quo.components.wallet.progress-bar.schema :as progress-bar-schema] + [quo.components.wallet.progress-bar.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- view-internal + [{:keys [full-width?] :as props}] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :progress-bar + :style (style/root-container props theme)} + (when full-width? + [rn/view {:style (style/progressed-bar props theme)}])])) + +(def view (schema/instrument #'view-internal progress-bar-schema/?schema)) diff --git a/src/quo/components/wallet/required_tokens/component_spec.cljs b/src/quo/components/wallet/required_tokens/component_spec.cljs new file mode 100644 index 00000000000..d3a7c03e21b --- /dev/null +++ b/src/quo/components/wallet/required_tokens/component_spec.cljs @@ -0,0 +1,30 @@ +(ns quo.components.wallet.required-tokens.component-spec + (:require + [quo.components.wallet.required-tokens.view :as required-tokens] + [test-helpers.component :as h])) + +(defn render + [component] + (h/render-with-theme-provider component :light)) + +(h/describe "Wallet: Required Tokens" + (h/test "basic render" + (render [required-tokens/view + {:token "SNT" + :type :token + :amount 100}]) + (h/is-truthy (h/get-by-label-text :wallet-required-tokens))) + + (h/test "render collectible" + (render [required-tokens/view + {:type :collectible + :collectible-img-src (js/require "../resources/images/mock2/collectible.png") + :collectible-name "Diamond"}]) + (h/is-truthy (h/get-by-text "Diamond"))) + + (h/test "render token with correct amount & symbol" + (render [required-tokens/view + {:type :token + :token "ETH" + :amount 100}]) + (h/is-truthy (h/get-by-text "100 ETH")))) diff --git a/src/quo/components/wallet/required_tokens/schema.cljs b/src/quo/components/wallet/required_tokens/schema.cljs new file mode 100644 index 00000000000..6ad329d869b --- /dev/null +++ b/src/quo/components/wallet/required_tokens/schema.cljs @@ -0,0 +1,16 @@ +(ns quo.components.wallet.required-tokens.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:type [:enum :token :collectible]] + [:amount {:optional true} [:maybe [:or :string :int]]] + [:token {:optional true} [:maybe :string]] + [:token-img-src {:optional true} [:maybe :schema.common/image-source]] + [:collectible-img-src {:optional true} [:maybe :schema.common/image-source]] + [:collectible-name {:optional true} [:maybe :string]] + [:divider? {:optional true} [:maybe :boolean]] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) diff --git a/src/quo/components/wallet/required_tokens/style.cljs b/src/quo/components/wallet/required_tokens/style.cljs new file mode 100644 index 00000000000..b4c568dce75 --- /dev/null +++ b/src/quo/components/wallet/required_tokens/style.cljs @@ -0,0 +1,21 @@ +(ns quo.components.wallet.required-tokens.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:align-items :center + :flex-direction :row}) + +(def collectible-img + {:width 14 + :height 14 + :border-radius 4}) + +(defn divider + [theme] + {:width 2 + :height 2 + :border-radius 1 + :margin-left 8 + :background-color (colors/theme-colors colors/neutral-40 + colors/neutral-50 + theme)}) diff --git a/src/quo/components/wallet/required_tokens/view.cljs b/src/quo/components/wallet/required_tokens/view.cljs new file mode 100644 index 00000000000..da6de4329ab --- /dev/null +++ b/src/quo/components/wallet/required_tokens/view.cljs @@ -0,0 +1,40 @@ +(ns quo.components.wallet.required-tokens.view + (:require [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.components.wallet.required-tokens.schema :as required-tokens-schema] + [quo.components.wallet.required-tokens.style :as style] + quo.context + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- view-internal + [{:keys [type amount token token-img-src collectible-img-src collectible-name divider? + container-style]}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (merge style/container container-style) + :accessibility-label :wallet-required-tokens} + (case type + :token [token/view + (assoc (if token-img-src + {:image-source token-img-src} + {:token token}) + :size + 14)] + :collectible [rn/image + {:style style/collectible-img + :source collectible-img-src}] + nil) + [text/text + {:size :paragraph-2 + :weight :medium + :style {:margin-left 4}} + (case type + :token (str amount " " token) + :collectible (str amount " " collectible-name) + nil)] + (when divider? + [rn/view + {:style (style/divider theme)}])])) + +(def view (schema/instrument #'view-internal required-tokens-schema/?schema)) diff --git a/src/quo/components/wallet/summary_info/component_spec.cljs b/src/quo/components/wallet/summary_info/component_spec.cljs new file mode 100644 index 00000000000..04487c7f0cf --- /dev/null +++ b/src/quo/components/wallet/summary_info/component_spec.cljs @@ -0,0 +1,65 @@ +(ns quo.components.wallet.summary-info.component-spec + (:require + [quo.components.wallet.summary-info.view :as summary-info] + [test-helpers.component :as h])) + +(def status-account-props + {:customization-color :purple + :size 32 + :emoji "🍑" + :type :default + :name "Collectibles vault" + :address "0x0ah...78b"}) + +(h/describe "Wallet: Summary Info" + (h/test "Type of `status-account` title renders" + (h/render-with-theme-provider [summary-info/view + {:type :status-account + :networks-to-show {:ethereum {:amount 150 + :token-symbol "ETH"} + :optimism {:amount 50 + :token-symbol "ETH"} + :arbitrum {:amount 50 + :token-symbol "ETH"}} + :account-props status-account-props}]) + (h/is-truthy (h/get-by-text "Collectibles vault"))) + + (h/test "Type of `user` title renders" + (h/render-with-theme-provider [summary-info/view + {:type :user + :networks-to-show {:ethereum {:amount 150 + :token-symbol "ETH"} + :optimism {:amount 50 + :token-symbol "ETH"} + :arbitrum {:amount 25 + :token-symbol "ETH"}} + :account-props {:full-name "M L" + :status-indicator? false + :size :small + :customization-color :blue + :name "Mark Libot" + :address "0x0ah...78b" + :status-account (merge status-account-props + {:size 16})}}]) + (h/is-truthy (h/get-by-text "Mark Libot")) + (h/is-truthy (h/get-by-text "Collectibles vault"))) + + (h/test "Networks specified render" + (h/render-with-theme-provider [summary-info/view + {:type :status-account + :networks-to-show {:ethereum {:amount 150 + :token-symbol "ETH"} + :optimism {:amount 50 + :token-symbol "ETH"} + :arbitrum {:amount 25 + :token-symbol "ETH"}} + :account-props status-account-props}]) + (h/is-truthy (h/get-by-label-text :networks)))) + +(h/describe "Wallet: network summary info" + (h/test "Type of `network` title renders" + (h/render-with-theme-provider [summary-info/view + {:type :network + :network-props {:full-name "Ethereum" + :network-name :ethereum}}]) + (h/is-truthy (h/get-by-text "Ethereum")))) diff --git a/src/quo/components/wallet/summary_info/schema.cljs b/src/quo/components/wallet/summary_info/schema.cljs new file mode 100644 index 00000000000..06a3997bbf2 --- /dev/null +++ b/src/quo/components/wallet/summary_info/schema.cljs @@ -0,0 +1,18 @@ +(ns quo.components.wallet.summary-info.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:type [:enum :status-account :saved-account :account :user :token :network]] + [:account-props {:optional true} [:maybe :map]] + [:network-props {:optional true} [:maybe :map]] + [:token-props {:optional true} [:maybe :map]] + [:networks-to-show {:optional true} + [:maybe + [:map-of :schema.quo/networks + [:map + [:amount [:or pos-int? string?]] + [:token-symbol string?]]]]]]]] + :any]) diff --git a/src/quo/components/wallet/summary_info/style.cljs b/src/quo/components/wallet/summary_info/style.cljs new file mode 100644 index 00000000000..535b478de32 --- /dev/null +++ b/src/quo/components/wallet/summary_info/style.cljs @@ -0,0 +1,45 @@ +(ns quo.components.wallet.summary-info.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [networks-to-show? theme] + {:width "100%" + :height (if networks-to-show? 90 56) + :border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :margin-bottom (if networks-to-show? 4 8)}) + +(def info-container + {:flex-direction :row + :height 55 + :padding-horizontal 12 + :align-items :center}) + +(defn dot-divider + [theme] + {:width 2 + :height 2 + :border-radius 2 + :margin-horizontal 8 + :background-color (colors/theme-colors colors/neutral-50 + colors/neutral-60 + theme)}) + +(defn line-divider + [theme] + {:width "100%" + :height 1 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +(def networks-container + {:padding-horizontal 12 + :padding-vertical 4 + :height 32 + :flex-direction :row + :align-items :center}) + +(def network-icon + {:width 32 + :height 32}) diff --git a/src/quo/components/wallet/summary_info/view.cljs b/src/quo/components/wallet/summary_info/view.cljs new file mode 100644 index 00000000000..c53b5e0d64c --- /dev/null +++ b/src/quo/components/wallet/summary_info/view.cljs @@ -0,0 +1,103 @@ +(ns quo.components.wallet.summary-info.view + (:require + [quo.components.avatars.account-avatar.view :as account-avatar] + [quo.components.avatars.user-avatar.view :as user-avatar] + [quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar] + [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.components.wallet.summary-info.schema :as summary-info-schema] + [quo.components.wallet.summary-info.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- network-amount + [{:keys [network amount divider? theme]}] + [:<> + [rn/image + {:style {:width 14 + :height 14} + :source (resources/get-network network)}] + [text/text + {:size :paragraph-2 + :weight :medium + :style {:margin-left 4}} amount] + (when divider? + [rn/view + {:style (style/dot-divider theme)}])]) + +(defn networks + [networks-to-show theme] + (->> networks-to-show + (map-indexed + (fn [i [k {:keys [amount token-symbol]}]] + (when (or (pos? amount) + (= amount "<0.01")) + [network-amount + {:network k + :amount (str amount " " token-symbol) + :divider? (not= (inc i) (-> networks-to-show keys count)) + :theme theme}]))) + (remove nil?) + (into [rn/view + {:style style/networks-container + :accessibility-label :networks}]))) + +(defn- view-internal + [{:keys [type account-props network-props token-props networks-to-show]}] + (let [theme (quo.context/use-theme) + address (or (:address account-props) (:address token-props))] + [rn/view + {:style (style/container (boolean networks-to-show) theme)} + [rn/view + {:style style/info-container} + (case type + :token [token/view (select-keys token-props #{:token :size})] + :status-account [account-avatar/view account-props] + :saved-account [wallet-user-avatar/wallet-user-avatar (assoc account-props :size :size-32)] + :account [wallet-user-avatar/wallet-user-avatar + (assoc account-props + :size :size-32 + :neutral? true)] + :network [rn/image + {:source (resources/get-network (:network-name network-props)) + :style style/network-icon}] + [user-avatar/user-avatar account-props]) + [rn/view {:style {:margin-left 8}} + (when (not (some #{type} [:account :network])) + [text/text + {:weight :semi-bold} + (or (:name account-props) (:label token-props))]) + (when (= type :network) + [text/text + {:weight :semi-bold} + (:full-name network-props)]) + [rn/view + {:style {:flex-direction :row + :align-items :center}} + (when (= type :user) + [:<> + [rn/view {:style {:margin-right 4}} + [account-avatar/view (:status-account account-props)]] + [text/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + (get-in account-props [:status-account :name])] + [rn/view + {:style (style/dot-divider theme)}]]) + (when address + [text/text + {:size (when (not= type :account) :paragraph-2) + :weight (when (= type :account) :semi-bold) + :style {:color (when (not= type :account) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}} + address])]]] + (when networks-to-show + [:<> + [rn/view + {:style (style/line-divider theme)}] + [networks networks-to-show theme]])])) + +(def view (schema/instrument #'view-internal summary-info-schema/?schema)) diff --git a/src/quo/components/wallet/swap_input/component_spec.cljs b/src/quo/components/wallet/swap_input/component_spec.cljs new file mode 100644 index 00000000000..607ac1ebbae --- /dev/null +++ b/src/quo/components/wallet/swap_input/component_spec.cljs @@ -0,0 +1,31 @@ +(ns quo.components.wallet.swap-input.component-spec + (:require [quo.components.wallet.swap-input.view :as swap-input] + [test-helpers.component :as h])) + +(h/describe "Wallet: Swap Input" + (h/test "should render correctly with props" + (h/render-with-theme-provider + [swap-input/view + {:type :pay + :error? false + :token "SNT" + :status :default + :currency-symbol "€" + :value "5" + :fiat-value "1.50" + :network-tag-props {:title "Max: 200 SNT"}}]) + (h/is-truthy (h/get-by-label-text :swap-input)) + (h/is-truthy (h/get-by-text "SNT")) + (h/is-truthy (h/get-by-text "€1.50")) + (h/is-truthy (h/get-by-text "Max: 200 SNT"))) + + (h/test "should render correctly with approval label" + (h/render-with-theme-provider + [swap-input/view + {:type :pay + :show-approval-label? true + :approval-label-props + {:status :approve + :token-value "10" + :token-symbol "SNT"}}]) + (h/is-truthy (h/get-by-text "Approve 10 SNT")))) diff --git a/src/quo/components/wallet/swap_input/style.cljs b/src/quo/components/wallet/swap_input/style.cljs new file mode 100644 index 00000000000..a0cd8343989 --- /dev/null +++ b/src/quo/components/wallet/swap_input/style.cljs @@ -0,0 +1,97 @@ +(ns quo.components.wallet.swap-input.style + (:require [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows] + [quo.foundations.typography :as typography])) + +(defn- border-color + [theme] + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)) + +(defn- loader-color + [theme] + (colors/theme-colors colors/neutral-5 colors/neutral-90 theme)) + +(defn content + [typing? theme] + (merge + {:border-width 1 + :border-radius 16 + :border-color (border-color theme) + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)} + (when typing? (shadows/get 1 theme)))) + +(defn row-1 + [loading?] + {:padding 12 + :gap 8 + :align-items (if loading? :center :flex-end) + :flex-direction :row}) + +(defn row-1-loader + [theme] + {:width 74 + :height 14 + :border-radius 6 + :background-color (loader-color theme)}) + +(def input-container + {:flex 1 + :flex-direction :row + :gap 5 + :height 32 + :align-items :flex-end}) + +(defn input + [disabled? error? theme] + (assoc typography/font-semi-bold + :font-size 27 + :flex-shrink 1 + :padding 0 + :color (cond + error? (colors/resolve-color :danger theme) + disabled? (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :else (colors/theme-colors colors/neutral-100 colors/white theme)) + :line-height 32)) + +(defn token-symbol + [theme] + {:padding-bottom 3 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn row-2 + [align-right?] + {:flex-direction :row + :justify-content (if align-right? :flex-end :space-between) + :align-items :center + :padding 12}) + +(defn row-2-loader + [theme] + {:width 80 + :height 10 + :margin-vertical 7 + :border-radius 6 + :background-color (loader-color theme)}) + +(def fiat-amount + {:color colors/neutral-50}) + +(def gradient-common + {:width 64 + :position :absolute + :top 0 + :bottom 0 + :z-index 1}) + +(def gradient-start + (assoc gradient-common :left 0)) + +(def gradient-end + (assoc gradient-common :right 0)) + +(defn button-loader + [theme] + {:width 100 + :height 24 + :border-radius 6 + :background-color (loader-color theme)}) diff --git a/src/quo/components/wallet/swap_input/view.cljs b/src/quo/components/wallet/swap_input/view.cljs new file mode 100644 index 00000000000..f1ddcaa27bd --- /dev/null +++ b/src/quo/components/wallet/swap_input/view.cljs @@ -0,0 +1,195 @@ +(ns quo.components.wallet.swap-input.view + (:require [oops.core :as oops] + [quo.components.avatars.token-avatar.view :as token-avatar] + [quo.components.buttons.button.view :as buttons] + [quo.components.dividers.divider-line.view :as divider-line] + [quo.components.markdown.text :as text] + [quo.components.tags.network-tags.view :as network-tag] + [quo.components.wallet.approval-label.schema :as approval-label.schema] + [quo.components.wallet.approval-label.view :as approval-label] + [quo.components.wallet.swap-input.style :as style] + quo.context + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [schema.core :as schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:get-ref {:optional true} [:maybe fn?]] + [:type {:optional true} [:maybe [:enum :pay :receive]]] + [:status {:optional true} [:maybe [:enum :default :typing :disabled :loading]]] + [:token {:optional true} [:maybe :string]] + [:value {:optional true} [:maybe :string]] + [:default-value {:optional true} [:maybe :string]] + [:currency-symbol {:optional true} [:maybe :string]] + [:fiat-value {:optional true} [:maybe :string]] + [:show-approval-label? {:optional true} [:maybe :boolean]] + [:auto-focus? {:optional true} [:maybe :boolean]] + [:input-disabled? {:optional true} [:maybe :boolean]] + [:error? {:optional true} [:maybe :boolean]] + [:show-keyboard? {:optional true} [:maybe :boolean]] + [:approval-label-props {:optional true} [:maybe approval-label.schema/?schema]] + [:network-tag-props {:optional true} [:maybe :map]] + [:on-change-text {:optional true} [:maybe fn?]] + [:enable-swap? {:optional true} [:maybe :boolean]] + [:on-swap-press {:optional true} [:maybe fn?]] + [:on-input-focus {:optional true} [:maybe fn?]] + [:on-token-press {:optional true} [:maybe fn?]] + [:on-max-press {:optional true} [:maybe fn?]] + [:max-loading? {:optional true} [:maybe :boolean]] + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:container-style {:optional true} [:maybe :map]]]]] + :any]) + +(def icon-size 32) +(def container-padding (* 2 (:padding (style/row-1 false)))) +(def max-cursor-position 5) + +(defn view-internal + [{:keys [type status token value fiat-value show-approval-label? error? network-tag-props + approval-label-props default-value auto-focus? input-disabled? enable-swap? + currency-symbol on-change-text show-keyboard? get-ref + container-style on-swap-press on-token-press on-max-press max-loading? on-input-focus]}] + (let [theme (quo.context/use-theme) + pay? (= type :pay) + disabled? (= status :disabled) + loading? (= status :loading) + typing? (= status :typing) + controlled-input? (some? value) + [container-width + set-container-width] (rn/use-state) + [overflow? + set-overflow?] (rn/use-state false) + [cursor-close-to-start? + set-cursor-close-to-start?] (rn/use-state false) + [label-width + set-label-width] (rn/use-state 0) + input-ref (rn/use-ref-atom nil) + set-input-ref (rn/use-callback (fn [ref] + (reset! input-ref ref) + (when get-ref (get-ref ref))) + []) + focus-input (rn/use-callback (fn [] + (some-> @input-ref + (oops/ocall "focus"))) + [input-ref]) + on-layout-container (rn/use-callback + (fn [e] + (let [width (oops/oget e "nativeEvent.layout.width")] + (set-container-width width)))) + on-layout-text-input (rn/use-callback + (fn [e] + (let [width (oops/oget e "nativeEvent.layout.width") + max-width (- container-width + icon-size + container-padding + (* label-width 2))] + (set-overflow? (> width max-width)))) + [container-width label-width]) + on-layout-label (rn/use-callback + (fn [e] + (let [width (oops/oget e "nativeEvent.layout.width")] + (set-label-width width)))) + on-selection-change (rn/use-callback + (fn [e] + (let [selection-start (oops/oget e + "nativeEvent.selection.start")] + (set-cursor-close-to-start? + (< selection-start max-cursor-position)))))] + [rn/view + {:style container-style + :accessibility-label :swap-input + :on-layout on-layout-container} + [rn/view {:style (style/content typing? theme)} + [rn/view + {:style (style/row-1 loading?)} + [rn/pressable {:on-press on-token-press} + [token-avatar/view + {:type :asset + :token token}]] + (if loading? + [rn/view {:style (style/row-1-loader theme)}] + [:<> + [rn/pressable + {:style style/input-container + :on-press focus-input} + [rn/view {:style {:flex-shrink 1}} + (when (and overflow? typing? (not cursor-close-to-start?)) + [linear-gradient/linear-gradient + {:start {:x 0 :y 0} + :end {:x 1 :y 0} + :colors [(colors/theme-colors colors/white colors/neutral-95 theme) + (colors/theme-colors colors/white-opa-10 colors/neutral-95-opa-10 theme)] + :style style/gradient-start}]) + (when (and overflow? disabled?) + [linear-gradient/linear-gradient + {:start {:x 0 :y 0} + :end {:x 1 :y 0} + :colors [(colors/theme-colors colors/white-opa-10 colors/neutral-95-opa-10 theme) + (colors/theme-colors colors/white colors/neutral-95 theme)] + :style style/gradient-end}]) + (if-not input-disabled? + [rn/text-input + (cond-> {:ref set-input-ref + :style (style/input disabled? error? theme) + :placeholder-text-color (colors/theme-colors colors/neutral-40 + colors/neutral-50 + theme) + :keyboard-type :numeric + :editable (not input-disabled?) + :auto-focus auto-focus? + :on-focus on-input-focus + :on-change-text on-change-text + :on-layout on-layout-text-input + :on-selection-change on-selection-change + :show-soft-input-on-focus show-keyboard? + :default-value default-value + :placeholder "0"} + controlled-input? (assoc :value value))] + [rn/text + {:style (style/input disabled? error? theme) + :on-layout on-layout-text-input} + (or value "0")])] + [text/text + {:size :paragraph-2 + :weight :semi-bold + :style (style/token-symbol theme) + :on-layout on-layout-label} + token]] + (when (and pay? enable-swap?) + [buttons/button + {:type :outline + :size 32 + :on-press on-swap-press + :icon-only? true} + :i/reorder])])] + [divider-line/view] + [rn/view + {:style (style/row-2 (or (not pay?) loading?))} + (when-not loading? + [:<> + (when pay? + (if max-loading? + [rn/view {:style (style/button-loader theme)}] + [rn/pressable {:on-press on-max-press} + [network-tag/view + (assoc network-tag-props + :status + (if error? :error :default))]])) + (when fiat-value + [text/text + {:size :paragraph-2 + :style style/fiat-amount + :weight :medium} + (str currency-symbol fiat-value)])]) + (when loading? + [rn/view {:style (style/row-2-loader theme)}])]] + (when (and (not= status :loading) (= type :pay) show-approval-label?) + [approval-label/view + approval-label-props])])) + +(def view (schema/instrument #'view-internal ?schema)) diff --git a/src/quo/components/wallet/token_input/component_spec.cljs b/src/quo/components/wallet/token_input/component_spec.cljs new file mode 100644 index 00000000000..856bd813022 --- /dev/null +++ b/src/quo/components/wallet/token_input/component_spec.cljs @@ -0,0 +1,25 @@ +(ns quo.components.wallet.token-input.component-spec + (:require + [quo.components.wallet.token-input.view :as token-input] + [test-helpers.component :as h])) + +(h/describe "Wallet: Token Input" + (h/test "Token label renders" + (h/render-with-theme-provider [token-input/view + {:token-symbol :snt + :currency-symbol :snt + :on-swap nil + :error? false + :value "1" + :converted-value "10"}]) + (h/is-truthy (h/get-by-text "SNT"))) + + (h/test "Amount renders" + (h/render-with-theme-provider [token-input/view + {:token-symbol :snt + :currency-symbol :snt + :on-swap nil + :error? false + :value "1" + :converted-value "€0.00"}]) + (h/is-truthy (h/get-by-text "€0.00")))) diff --git a/src/quo/components/wallet/token_input/schema.cljs b/src/quo/components/wallet/token_input/schema.cljs new file mode 100644 index 00000000000..e3aa9383357 --- /dev/null +++ b/src/quo/components/wallet/token_input/schema.cljs @@ -0,0 +1,19 @@ +(ns quo.components.wallet.token-input.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map {:closed true} + [:token-symbol [:maybe [:or :string :keyword]]] + [:currency-symbol [:maybe [:or :string :keyword]]] + [:hint-component {:optional true} [:maybe :schema.common/hiccup]] + [:on-token-press {:optional true} [:maybe fn?]] + [:on-swap {:optional true} [:maybe fn?]] + [:container-style {:optional true} [:maybe :map]] + [:error? [:maybe :boolean]] + [:show-token-icon? {:optional true} [:maybe :boolean]] + [:value [:maybe :string]] + [:converted-value {:optional true} [:maybe :string]] + [:swappable? {:optional true} [:maybe :boolean]]]]] + :any]) diff --git a/src/quo/components/wallet/token_input/style.cljs b/src/quo/components/wallet/token_input/style.cljs new file mode 100644 index 00000000000..e41d813cc33 --- /dev/null +++ b/src/quo/components/wallet/token_input/style.cljs @@ -0,0 +1,67 @@ +(ns quo.components.wallet.token-input.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.typography :as typography])) + +(defn main-container + [width] + {:padding-vertical 8 + :width width}) + +(def amount-container + {:padding-horizontal 20 + :padding-bottom 4 + :height 36 + :flex-direction :row}) + +(defn token-name + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :padding-bottom 3}) + +(def input-container + {:flex 1 + :margin-left 8 + :margin-right 8 + :flex-direction :row + :align-items :flex-end}) + +(def text-input-dimensions + (-> typography/heading-1 + (dissoc :letter-spacing) + (assoc :font-weight "600" + :margin-right 5 + :padding-left 0 + :padding-right 0 + :padding-top 0 + :padding-bottom 0 + :height "100%"))) + +(defn text-input + [theme error?] + (assoc text-input-dimensions + :color + (if error? + (colors/resolve-color :danger theme) + (colors/theme-colors colors/neutral-100 colors/white theme)) + :flex-shrink 1)) + +(defn placeholder-text + [theme] + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)) + +(defn divider + [theme] + {:margin-vertical 8 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)}) + +(def data-container + {:padding-top 4 + :padding-horizontal 20 + :flex-direction :row + :justify-content :space-between + :align-items :center}) + +(defn converted-amount + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo/components/wallet/token_input/view.cljs b/src/quo/components/wallet/token_input/view.cljs new file mode 100644 index 00000000000..e8589125b31 --- /dev/null +++ b/src/quo/components/wallet/token_input/view.cljs @@ -0,0 +1,91 @@ +(ns quo.components.wallet.token-input.view + (:require + [clojure.string :as string] + [quo.components.buttons.button.view :as button] + [quo.components.dividers.divider-line.view :as divider-line] + [quo.components.markdown.text :as text] + [quo.components.utilities.token.view :as token] + [quo.components.wallet.token-input.schema :as component-schema] + [quo.components.wallet.token-input.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema])) + +(defn- token-name-text + [theme text] + [text/text + {:style (style/token-name theme) + :size :paragraph-2 + :weight :semi-bold} + (string/upper-case (or (clj->js text) ""))]) + +(defn input-section + [{:keys [token-symbol on-token-press value error? on-swap currency-symbol show-token-icon? + swappable?]}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:style {:width "100%" + :flex-direction :row} + :on-press on-token-press} + (when show-token-icon? + [token/view + {:token token-symbol + :size :size-32}]) + [rn/view {:style style/input-container} + [rn/text-input + {:style (style/text-input theme error?) + :placeholder-text-color (style/placeholder-text theme) + :placeholder "0" + :keyboard-type :numeric + :editable false + :value value}] + [token-name-text theme currency-symbol]] + (when swappable? + [button/button + {:icon true + :icon-only? true + :size 32 + :on-press #(when on-swap (on-swap)) + :type :outline + :accessibility-label :reorder} + :i/reorder])])) + +(defn- view-internal + [{:keys [token-symbol + value + on-token-press + error? + container-style + on-swap + converted-value + hint-component + show-token-icon? + currency-symbol + swappable?] + :or {show-token-icon? true + swappable? true}}] + (let [theme (quo.context/use-theme) + width (:width (rn/get-window))] + [rn/view {:style (merge (style/main-container width) container-style)} + [rn/view {:style style/amount-container} + [input-section + {:theme theme + :token-symbol token-symbol + :on-token-press on-token-press + :value value + :error? error? + :on-swap on-swap + :currency-symbol currency-symbol + :show-token-icon? show-token-icon? + :swappable? swappable?}]] + [divider-line/view {:container-style (style/divider theme)}] + [rn/view {:style style/data-container} + hint-component + (when swappable? + [text/text + {:size :paragraph-2 + :weight :medium + :style (style/converted-amount theme)} + converted-value])]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/transaction_progress/component_spec.cljs b/src/quo/components/wallet/transaction_progress/component_spec.cljs new file mode 100644 index 00000000000..efecd10eaf0 --- /dev/null +++ b/src/quo/components/wallet/transaction_progress/component_spec.cljs @@ -0,0 +1,237 @@ +(ns quo.components.wallet.transaction-progress.component-spec + (:require [quo.core :as quo] + [test-helpers.component :as h])) + +(defn- get-test-data + [{:keys [state network] + :or {state :pending network :mainnet}}] + {:title "Title" + :tag-name "Doodle" + :tag-number 120 + :network network + :customization-color :blue + :networks [{:network :mainnet + :full-name "Ethereum" + :state state + :counter 0 + :total-box 85 + :progress 30 + :epoch-number "123"} + {:network :optimism + :full-name "Optimism" + :state state + :progress 50 + :epoch-number "123"} + {:network :arbitrum + :full-name "Arbitrum" + :state state + :progress 30 + :epoch-number "123"}] + :on-press (fn [] + (js/alert "Transaction progress item pressed"))}) + +(h/describe "Transaction Progress" + (h/test "component renders without props" + (h/render-with-theme-provider + [quo/transaction-progress {}]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is pending and network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is sending and network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :sending + :network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is confirmed and network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :confirmed + :network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalising and network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :finalising + :network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalized and network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :finalized + :network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is error and network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :error + :network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is sending and network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :sending + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is confirmed and network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :confirmed + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalising and network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :finalising + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalized and network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :finalized + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is error and network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :error + :network :optimism})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is sending and network is arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :sending + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is confirmed and network is arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :confirmed + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalising and network is arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :finalising + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalized and network is arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :finalized + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is error and network is arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress + (get-test-data {:state :error + :network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is pending and network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is sending and network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:state :sending})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is confirmed and network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:state :confirmed})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalising and network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:state :finalising})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is finalized and network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:state :finalized})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "component renders when state is error and network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:state :error})]) + (h/is-truthy (h/get-by-label-text :transaction-progress))) + + (h/test "mainnet progress box is visible network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {})]) + (h/is-truthy (h/get-by-label-text :mainnet-progress-box))) + + (h/test "arbitrum-optimism progress box is visible network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :optimism-arbitrum})]) + (h/is-truthy (h/get-all-by-label-text :progress-box))) + + (h/test "arbitrum progress box is visible network is arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :arbitrum})]) + (h/is-truthy (h/get-all-by-label-text :progress-box))) + + (h/test "optimism progress box is visible network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :optimism})]) + (h/is-truthy (h/get-all-by-label-text :progress-box))) + + (h/test "title is visible network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-text "Title"))) + + (h/test "title is visible network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {})]) + (h/is-truthy (h/get-by-text "Title"))) + + (h/test "title is visible network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :optimism})]) + (h/is-truthy (h/get-by-text "Title"))) + + (h/test "title is visible network is arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :arbitrum})]) + (h/is-truthy (h/get-by-text "Title"))) + + (h/test "context tag is visible network is optimism-arbitrum" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :optimism-arbitrum})]) + (h/is-truthy (h/get-by-label-text :context-tag))) + + (h/test "context tag is visible network is mainnet" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {})]) + (h/is-truthy (h/get-by-label-text :context-tag))) + + (h/test "context tag is visible network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :optimism})]) + (h/is-truthy (h/get-by-label-text :context-tag))) + + (h/test "context tag is visible network is optimism" + (h/render-with-theme-provider + [quo/transaction-progress (get-test-data {:network :arbitrum})]) + (h/is-truthy (h/get-by-label-text :context-tag)))) diff --git a/src/quo/components/wallet/transaction_progress/schema.cljs b/src/quo/components/wallet/transaction_progress/schema.cljs new file mode 100644 index 00000000000..946d377c61e --- /dev/null +++ b/src/quo/components/wallet/transaction_progress/schema.cljs @@ -0,0 +1,26 @@ +(ns quo.components.wallet.transaction-progress.schema) + +(def ^:private ?network + [:map + [:network {:optional true} [:maybe :keyword]] + [:full-name {:optional true} [:maybe :string]] + [:state {:optional true} [:maybe [:enum :pending :sending :confirmed :finalising :finalized :error]]] + [:counter {:optional true} [:maybe :int]] + [:total-box {:optional true} [:maybe :int]] + [:epoch-number {:optional true} [:maybe :string]] + [:progress {:optional true} [:maybe :int]]]) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:customization-color {:optional true} [:maybe :schema.common/customization-color]] + [:title {:optional true} [:maybe :string]] + [:network {:optional true} [:maybe :keyword]] + [:tag-name {:optional true} [:maybe :string]] + [:tag-number {:optional true} [:maybe [:or :string :int]]] + [:tag-photo {:optional true} [:maybe :schema.common/image-source]] + [:on-press {:optional true} [:maybe fn?]] + [:networks {:optional true} [:maybe [:sequential ?network]]]]]] + :any]) diff --git a/src/quo/components/wallet/transaction_progress/style.cljs b/src/quo/components/wallet/transaction_progress/style.cljs new file mode 100644 index 00000000000..dd6c07c55b2 --- /dev/null +++ b/src/quo/components/wallet/transaction_progress/style.cljs @@ -0,0 +1,38 @@ +(ns quo.components.wallet.transaction-progress.style + (:require [quo.foundations.colors :as colors])) + +(def title-text-container + {:flex 1}) + +(def icon + {:margin-right 4}) + +(defn box-style + [theme] + {:border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +(def title-container + {:align-items :center + :flex-direction :row + :padding-left 12 + :padding-top 8 + :padding-right 8 + :padding-bottom 4}) + +(def status-row-container + {:flex-direction :row + :align-items :center + :flex 1 + :padding-horizontal 12 + :padding-top 8 + :padding-bottom 4}) + +(defn context-tag-container + [theme] + {:padding-horizontal 12 + :padding-bottom 8 + :flex-direction :row + :border-bottom-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) diff --git a/src/quo/components/wallet/transaction_progress/view.cljs b/src/quo/components/wallet/transaction_progress/view.cljs new file mode 100644 index 00000000000..69ecd852a8d --- /dev/null +++ b/src/quo/components/wallet/transaction_progress/view.cljs @@ -0,0 +1,174 @@ +(ns quo.components.wallet.transaction-progress.view + (:require [quo.components.buttons.button.view :as button] + [quo.components.icon :as icons] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.view :as context-tag] + [quo.components.wallet.confirmation-progress.view :as confirmation-progress] + [quo.components.wallet.transaction-progress.schema :as component-schema] + [quo.components.wallet.transaction-progress.style :as style] + [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def ^:private max-mainnet-verifications 4) +(def ^:private max-sidenet-verifications 1) + +(defn- icon-internal + ([icon] + (icon-internal icon nil 20)) + ([icon color] + (icon-internal icon color 20)) + ([icon color size] + [rn/view {:style style/icon} + [icons/icon + icon + {:color color + :size size + :no-color (when (not color) true)}]])) + +(defn- text-internal + [{:keys [weight size accessibility-label color] + :or {weight :semi-bold + size :paragraph-1}} + title] + [text/text + {:accessibility-label accessibility-label + :ellipsize-mode :tail + :number-of-lines 1 + :weight weight + :size size + :style {:color color}} + title]) + +(defn- network-type-text + [state] + (case state + (:sending :pending) (i18n/label :t/pending-on) + (:confirmed :finalising) (i18n/label :t/confirmed-on) + :finalized (i18n/label :t/finalized-on) + :error (i18n/label :t/failed-on) + nil)) + +(defn- calculate-counter + [counter] + (if (< counter 4) counter max-mainnet-verifications)) + +(defn mainnet-label + [counter] + (str counter "/" max-mainnet-verifications)) + +(defn subnet-label + [counter] + (str counter "/" max-sidenet-verifications)) + +(defn- text-steps + [network state epoch-number counter] + (let [steps (if (= network :mainnet) + {:pending (mainnet-label 0) + :sending (mainnet-label (calculate-counter counter)) + :confirmed (mainnet-label (calculate-counter counter)) + :finalising (mainnet-label (calculate-counter counter)) + :finalized (i18n/label :t/epoch-number {:number epoch-number}) + :error (mainnet-label 0)} + {:pending (subnet-label 0) + :sending (subnet-label 0) + :confirmed (subnet-label 0) + :finalising (subnet-label 1) + :finalized (i18n/label :t/epoch-number {:number epoch-number}) + :error (subnet-label 0)})] + (get steps state))) + +(defn- get-status-icon + [theme state] + (case state + (:pending :sending) {:icon :i/pending-state + :color (colors/theme-colors colors/neutral-50 + colors/neutral-40 + theme)} + (:confirmed :finalising) {:icon :i/positive-state + :color (colors/resolve-color :success theme)} + :finalized {:icon :i/diamond} + :error {:icon :i/negative-state + :color (colors/resolve-color :danger theme)} + nil)) + +(defn- calculate-error-state + [networks] + (some #(= (:state %) :error) networks)) + +(defn- title-internal + [{:keys [title theme networks]}] + [rn/view {:style style/title-container} + [icon-internal :i/placeholder (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)] + [rn/view {:style style/title-text-container} + [text-internal nil title]] + (when (calculate-error-state networks) + [button/button + {:size 24 + :icon-left :i/refresh} + (i18n/label :t/retry)])]) + +(defn- tag-internal + [tag-photo tag-name tag-number theme] + [rn/view {:style (style/context-tag-container theme)} + [context-tag/view + {:size 24 + :collectible tag-photo + :collectible-name tag-name + :collectible-number tag-number + :type :collectible}]]) + +(defn- status-row + [{:keys [theme state network epoch-number counter full-name]}] + (let [{:keys [icon color]} (get-status-icon theme state)] + [rn/view {:style style/status-row-container} + [icon-internal icon color 16] + [rn/view {:style style/title-text-container} + [text-internal + {:weight :regular + :size :paragraph-2} + (str (network-type-text state) " " full-name)]] + [rn/view + [text-internal + {:weight :regular + :size :paragraph-2 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)} + (text-steps network state epoch-number counter)]]])) + +(defn- view-network + [{:keys [theme state epoch-number counter total-box progress network full-name]}] + [:<> + [status-row + {:theme theme + :state state + :network network + :epoch-number epoch-number + :counter counter + :full-name full-name}] + [confirmation-progress/view + {:state state + :network network + :counter counter + :total-box total-box + :progress-value progress}]]) + +(defn- view-internal + [{:keys [title on-press accessibility-label tag-photo tag-name tag-number networks] + :or {accessibility-label :transaction-progress}}] + (let [theme (quo.context/use-theme)] + [rn/pressable + {:on-press on-press + :accessibility-label accessibility-label} + [rn/view {:style (style/box-style theme)} + [title-internal + {:title title + :theme theme + :networks networks}] + [tag-internal tag-photo tag-name tag-number theme] + (for [network networks] + ^{:key (:network network)} + [view-network (assoc network :theme theme)])]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/transaction_summary/component_spec.cljs b/src/quo/components/wallet/transaction_summary/component_spec.cljs new file mode 100644 index 00000000000..25507435720 --- /dev/null +++ b/src/quo/components/wallet/transaction_summary/component_spec.cljs @@ -0,0 +1,29 @@ +(ns quo.components.wallet.transaction-summary.component-spec + (:require + [quo.components.wallet.transaction-summary.view :as transaction-summary] + [test-helpers.component :as h])) + +(h/describe "Transaction summary" + (h/test "default render" + (h/render-with-theme-provider [transaction-summary/view {}]) + (h/is-truthy (h/query-by-label-text :transaction-summary))) + + (h/test "icon displayed" + (h/render-with-theme-provider [transaction-summary/view {:transaction :send}]) + (h/is-truthy (h/query-by-label-text :header-icon))) + + (h/test "Context tag rendered" + (h/render-with-theme-provider [transaction-summary/view + {:transaction :send + :first-tag {:size 24 + :type :token + :token "SNT" + :amount 1500}}]) + (h/is-truthy (h/query-by-label-text :context-tag)))) + + + + + + + diff --git a/src/quo/components/wallet/transaction_summary/schema.cljs b/src/quo/components/wallet/transaction_summary/schema.cljs new file mode 100644 index 00000000000..4cfe9e2173f --- /dev/null +++ b/src/quo/components/wallet/transaction_summary/schema.cljs @@ -0,0 +1,22 @@ +(ns quo.components.wallet.transaction-summary.schema + (:require [quo.components.tags.context-tag.schema :as context-tag-schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:transaction {:optional true} [:maybe [:enum :send :swap :bridge]]] + [:first-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:second-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:third-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:fourth-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:fifth-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:second-tag-prefix {:optional true} [:maybe :keyword]] + [:third-tag-prefix {:optional true} [:maybe :keyword]] + [:fourth-tag-prefix {:optional true} [:maybe :keyword]] + [:max-fees {:optional true} [:maybe :string]] + [:nonce {:optional true} [:maybe :int]] + [:input-data {:optional true} [:maybe :string]] + [:on-press {:optional true} [:maybe fn?]]]]] + :any]) diff --git a/src/quo/components/wallet/transaction_summary/style.cljs b/src/quo/components/wallet/transaction_summary/style.cljs new file mode 100644 index 00000000000..a08f2765089 --- /dev/null +++ b/src/quo/components/wallet/transaction_summary/style.cljs @@ -0,0 +1,72 @@ +(ns quo.components.wallet.transaction-summary.style + (:require + [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:border-radius 16 + :padding-top 9 + :padding-bottom 8 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :border-width 1}) + +(def transaction-header-container + {:flex-direction :row + :align-items :center + :margin-bottom 9 + :padding-horizontal 12}) + +(defn transaction-header + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :margin-left 4}) + +(defn prop-text + [theme] + {:margin-right 4 + :color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(def prop-tag + {:margin-right 4}) + +(def icon-container + {:width 20 + :height 20 + :align-items :center + :justify-content :center}) + +(def content + {:margin-bottom 8 + :padding-horizontal 12}) + +(def content-line + {:flex-direction :row + :margin-top 4 + :align-items :center}) + +(defn icon-color + [theme] + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)) + +(defn divider + [theme] + {:height 1 + :margin-top 4 + :margin-bottom 3 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +(defn extra-info-header + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn extra-info-content + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(def extra-info-container + {:margin-horizontal 12}) + +(def extras-container + {:flex-direction :row + :justify-content :space-between + :margin-top 4}) diff --git a/src/quo/components/wallet/transaction_summary/view.cljs b/src/quo/components/wallet/transaction_summary/view.cljs new file mode 100644 index 00000000000..d4855dc6bf8 --- /dev/null +++ b/src/quo/components/wallet/transaction_summary/view.cljs @@ -0,0 +1,106 @@ +(ns quo.components.wallet.transaction-summary.view + (:require + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.view :as context-tag] + [quo.components.wallet.transaction-summary.schema :as component-schema] + [quo.components.wallet.transaction-summary.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def transaction-translation + {:send (i18n/label :t/send) + :swap (i18n/label :t/swap) + :bridge (i18n/label :t/bridge)}) + +(def transaction-icon + {:send :i/send + :swap :i/transaction + :bridge :i/bridge}) + +(defn transaction-header + [{:keys [transaction + theme] + :or {transaction :send}}] + (let [icon (transaction-icon transaction) + translation (transaction transaction-translation)] + [rn/view + {:style style/transaction-header-container} + [rn/view {:style style/icon-container} + (when icon + [icon/icon icon + {:color (style/icon-color theme) + :accessibility-label :header-icon}])] + (when translation + [text/text + {:weight :semi-bold + :size :paragraph-1 + :style (style/transaction-header theme)} + translation])])) + +(defn prop-text + [label theme] + [text/text + {:weight :regular + :size :paragraph-2 + :style (style/prop-text theme)} + (i18n/label label)]) + +(defn prop-tag + [props] + [rn/view {:style style/prop-tag} + [context-tag/view (assoc props :size 24)]]) + +(defn extra-info + [{:keys [header content theme]}] + [rn/view {:style style/extra-info-container} + [text/text + {:weight :regular + :size :paragraph-2 + :style (style/extra-info-header theme)} + header] + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/extra-info-content theme)} + content]]) + +(defn- view-internal + [{:keys [first-tag second-tag third-tag fourth-tag second-tag-prefix + third-tag-prefix fourth-tag-prefix fifth-tag max-fees + nonce input-data] + :as props}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (style/container theme) + :accessibility-label :transaction-summary} + [transaction-header props] + [rn/view {:style style/content} + [rn/view {:style style/content-line} + (when first-tag [prop-tag first-tag]) + (when second-tag-prefix [prop-text second-tag-prefix theme]) + (when second-tag [prop-tag second-tag])] + [rn/view {:style style/content-line} + (when third-tag-prefix [prop-text third-tag-prefix theme]) + (when third-tag [prop-tag third-tag]) + (when fourth-tag-prefix [prop-text fourth-tag-prefix theme]) + (when fourth-tag [prop-tag fourth-tag]) + (when fifth-tag [prop-tag fifth-tag])]] + [rn/view {:style (style/divider theme)}] + [rn/view {:style style/extras-container} + [extra-info + {:header (i18n/label :t/max-fees) + :content max-fees + :theme theme}] + [extra-info + {:header (i18n/label :t/nonce) + :content nonce + :theme theme}] + [extra-info + {:header (i18n/label :t/input-data) + :content input-data + :theme theme}]]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/wallet_activity/component_spec.cljs b/src/quo/components/wallet/wallet_activity/component_spec.cljs new file mode 100644 index 00000000000..eaf64c28c2c --- /dev/null +++ b/src/quo/components/wallet/wallet_activity/component_spec.cljs @@ -0,0 +1,16 @@ +(ns quo.components.wallet.wallet-activity.component-spec + (:require + [quo.components.wallet.wallet-activity.view :as wallet-activity] + [test-helpers.component :as h])) + +(h/describe "Wallet activity" + (h/test "default render" + (h/render-with-theme-provider [wallet-activity/view {}]) + (h/is-truthy (h/query-by-label-text :wallet-activity))) + + (h/test "Should call :on-press" + (let [on-press (h/mock-fn)] + (h/render-with-theme-provider [wallet-activity/view {:on-press on-press}]) + (h/is-truthy (h/query-by-label-text :wallet-activity)) + (h/fire-event :press (h/query-by-label-text :wallet-activity)) + (h/was-called on-press)))) diff --git a/src/quo/components/wallet/wallet_activity/schema.cljs b/src/quo/components/wallet/wallet_activity/schema.cljs new file mode 100644 index 00000000000..d6fa966782e --- /dev/null +++ b/src/quo/components/wallet/wallet_activity/schema.cljs @@ -0,0 +1,24 @@ +(ns quo.components.wallet.wallet-activity.schema + (:require [quo.components.tags.context-tag.schema :as context-tag-schema])) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:transaction {:optional true} + [:maybe [:enum :receive :send :swap :bridge :buy :destroy :mint :approval]]] + [:status {:optional true} [:maybe [:enum :pending :confirmed :finalised :failed]]] + [:counter {:optional true} [:maybe :int]] + [:timestamp {:optional true} [:maybe :string]] + [:blur? {:optional true} [:maybe :boolean]] + [:on-press {:optional true} [:maybe fn?]] + [:state {:optional true} [:maybe [:= :disabled]]] + [:second-tag-prefix {:optional true} [:maybe :keyword]] + [:third-tag-prefix {:optional true} [:maybe :keyword]] + [:fourth-tag-prefix {:optional true} [:maybe :keyword]] + [:first-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:second-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:third-tag {:optional true} [:maybe context-tag-schema/?schema]] + [:fourth-tag {:optional true} [:maybe context-tag-schema/?schema]]]]] + :any]) diff --git a/src/quo/components/wallet/wallet_activity/style.cljs b/src/quo/components/wallet/wallet_activity/style.cljs new file mode 100644 index 00000000000..864306038cf --- /dev/null +++ b/src/quo/components/wallet/wallet_activity/style.cljs @@ -0,0 +1,103 @@ +(ns quo.components.wallet.wallet-activity.style + (:require + [quo.foundations.colors :as colors])) + +(defn wallet-activity-container + [{:keys [pressed? + theme + blur?]}] + (merge + {:border-radius 16 + :padding-top 8 + :padding-left 12 + :padding-right 12 + :padding-bottom 12} + (when pressed? + {:background-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-5 colors/neutral-90 theme))}))) + +(def transaction-header-container + {:flex-direction :row + :align-items :center}) + +(defn transaction-header + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :margin-right 4}) + +(defn transaction-counter + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn transaction-counter-container + [theme blur?] + {:border-width 1 + :border-radius 6 + :margin-right 4 + :padding-horizontal 2 + :border-color (if-not blur? + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + colors/white-opa-10)}) + +(def timestamp-container + {:margin-left 4 + :flex 1}) + +(defn timestamp + [theme blur?] + {:color (if-not blur? + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme) + colors/white-opa-40)}) + +(defn prop-text + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(def icon-container + {:width 32 + :height 32 + :margin-top 8}) + +(def container + {:flex-direction :row + :flex 1 + :column-gap 8}) + +(def content-line + {:flex-direction :row + :margin-top 2 + :align-items :center + :column-gap 4 + :justify-content :flex-start}) + +(def icon-hole-view + {:width 32 + :height 32 + :align-items :center + :justify-content :center}) + +(defn icon-circle-border + [theme blur?] + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :width 32 + :height 32 + :border-width 1 + :border-radius 16 + :border-color (if-not blur? + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + colors/white-opa-5)}) + +(defn icon-color + [theme] + (colors/theme-colors colors/neutral-100 colors/white theme)) + +(def icon-status-container + {:position :absolute + :bottom 0 + :right 0}) + diff --git a/src/quo/components/wallet/wallet_activity/view.cljs b/src/quo/components/wallet/wallet_activity/view.cljs new file mode 100644 index 00000000000..c7529efcabc --- /dev/null +++ b/src/quo/components/wallet/wallet_activity/view.cljs @@ -0,0 +1,148 @@ +(ns quo.components.wallet.wallet-activity.view + (:require + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.tags.context-tag.view :as context-tag] + [quo.components.wallet.wallet-activity.schema :as component-schema] + [quo.components.wallet.wallet-activity.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def transaction-translation + {:receive [i18n/label :t/receive] + :send [i18n/label :t/send] + :swap [i18n/label :t/swap] + :bridge [i18n/label :t/bridge] + :buy [i18n/label :t/buy] + :destroy [i18n/label :t/destroy] + :mint [i18n/label :t/mint] + :approval [i18n/label :t/set-spending-cap]}) + +(def transaction-icon + {:receive :i/receive + :send :i/send + :swap :i/transaction + :bridge :i/bridge + :buy :i/buy + :destroy :i/destroy + :mint :i/mint + :approval :i/token-sales}) + +(def status-icon + {:pending :i/pending-state + :confirmed :i/positive-state + :finalised :i/diamond + :failed :i/negative-state}) + +(defn transaction-header + [{:keys [transaction + timestamp + counter + blur? + on-press-options] + :or {transaction :receive + counter 1}} + theme] + [rn/view + {:style style/transaction-header-container} + [text/text + {:weight :semi-bold + :size :paragraph-1 + :style (style/transaction-header theme) + :accessibility-label :transaction-header} + (transaction transaction-translation)] + (when (> counter 1) + [rn/view (style/transaction-counter-container theme blur?) + [text/text + {:weight :medium + :size :label + :style (style/transaction-counter theme)} + (i18n/label :t/x-counter {:counter counter})]]) + [rn/view {:style style/timestamp-container} + [text/text + {:weight :regular + :size :label + :style (style/timestamp theme blur?) + :accessibility-label :transaction-timestamp} + timestamp]] + (when on-press-options + [rn/pressable + {:on-press on-press-options + :hit-slop {:top 10 :bottom 10 :left 10 :right 10}} + [icon/icon :i/options + {:size 20}]])]) + +(defn transaction-icon-view + [{:keys [blur? transaction status] + :or {transaction :receive + status :pending}} + theme] + [rn/view {:style style/icon-container} + [rn/view {:style style/icon-status-container} + [icon/icon (status-icon status) + {:size 12 + :no-color :true}]] + [hole-view/hole-view + {:style style/icon-hole-view + :holes [{:x 20 + :y 20 + :right 0 + :width 12 + :height 12 + :borderRadius 6}]} + [icon/icon (transaction-icon transaction :i/placeholder) + {:color (style/icon-color theme)}] + [rn/view {:style (style/icon-circle-border theme blur?)}]]]) + +(defn prop-text + [label theme] + [text/text + {:weight :regular + :size :paragraph-2 + :style (style/prop-text theme)} + [i18n/label label]]) + +(defn prop-tag + [props blur?] + [context-tag/view + (assoc props + :size 24 + :blur? blur? + :container-style {:flex-shrink 1})]) + +(defn- view-internal + [{:keys [state blur? first-tag second-tag third-tag fourth-tag on-press + second-tag-prefix third-tag-prefix fourth-tag-prefix] + :as props}] + (let [theme (quo.context/use-theme) + [pressed? + set-pressed] (rn/use-state false) + on-press-in (rn/use-callback #(set-pressed true)) + on-press-out (rn/use-callback #(set-pressed false))] + [rn/pressable + {:style (style/wallet-activity-container {:pressed? pressed? + :theme theme + :blur? blur?}) + :accessibility-label :wallet-activity + :disabled (= state :disabled) + :on-press on-press + :on-press-in (when on-press on-press-in) + :on-press-out (when on-press on-press-out)} + [rn/view {:style style/container} + [transaction-icon-view props theme] + [rn/view {:style {:flex 1}} + [transaction-header props theme] + [rn/view {:style style/content-line} + (when first-tag [prop-tag first-tag blur?]) + (when second-tag-prefix [prop-text second-tag-prefix theme]) + (when second-tag [prop-tag second-tag blur?])] + [rn/view {:style style/content-line} + (when third-tag-prefix [prop-text third-tag-prefix theme]) + (when third-tag [prop-tag third-tag blur?]) + (when fourth-tag-prefix [prop-text fourth-tag-prefix theme]) + (when fourth-tag [prop-tag fourth-tag blur?])]]]])) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/components/wallet/wallet_overview/component_spec.cljs b/src/quo/components/wallet/wallet_overview/component_spec.cljs new file mode 100644 index 00000000000..ff691cee525 --- /dev/null +++ b/src/quo/components/wallet/wallet_overview/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo.components.wallet.wallet-overview.component-spec + (:require + [quo.components.wallet.wallet-overview.view :as wallet-overview] + [test-helpers.component :as h])) + +(h/describe + "Wallet overview test" + (h/test "renders correct balance" + (h/render-with-theme-provider [wallet-overview/view + {:state :default + :time-frame :one-week + :metrics :positive + :balance "€0.01"}]) + (h/is-truthy (h/get-by-text "€0.01")))) diff --git a/src/quo/components/wallet/wallet_overview/schema.cljs b/src/quo/components/wallet/wallet_overview/schema.cljs new file mode 100644 index 00000000000..e0cfdf81342 --- /dev/null +++ b/src/quo/components/wallet/wallet_overview/schema.cljs @@ -0,0 +1,23 @@ +(ns quo.components.wallet.wallet-overview.schema) + +(def ?schema + [:=> + [:catn + [:props + [:map + [:state {:optional true} [:maybe [:enum :default :loading]]] + [:time-frame {:optional true} + [:maybe [:enum :none :selected :one-week :one-month :three-months :one-year :all-time :custom]]] + [:metrics {:optional true} [:maybe [:enum :none :negative :positive]]] + [:balance {:optional true} [:maybe :string]] + [:date {:optional true} [:maybe :string]] + [:begin-date {:optional true} [:maybe :string]] + [:end-date {:optional true} [:maybe :string]] + [:currency-change {:optional true} [:maybe :string]] + [:percentage-change {:optional true} [:maybe :string]] + [:dropdown-on-press {:optional true} [:maybe fn?]] + [:networks {:optional true} + [:maybe [:sequential [:map [:source [:maybe :schema.common/image-source]]]]]] + [:dropdown-state {:optional true} [:maybe [:enum :default :disabled]]] + [:show-new-chain-indicator? {:optional true} [:maybe :boolean]]]]] + :any]) diff --git a/src/quo/components/wallet/wallet_overview/style.cljs b/src/quo/components/wallet/wallet_overview/style.cljs new file mode 100644 index 00000000000..5d0c7183377 --- /dev/null +++ b/src/quo/components/wallet/wallet_overview/style.cljs @@ -0,0 +1,56 @@ +(ns quo.components.wallet.wallet-overview.style + (:require + [quo.foundations.colors :as colors])) + +(defn container-info + [{:keys [metrics]}] + {:padding-horizontal 20 + :padding-top 12 + :padding-bottom 32 + :flex-grow 1 + :height (if (= metrics :none) 78 98)}) + +(def container-info-top + {:flex-direction :row + :justify-content :space-between}) + +(def container-info-bottom + {:flex-direction :row + :padding-top 4}) + +(defn color-metrics + [metrics theme] + (if (= metrics :positive) + (colors/theme-colors colors/success-50 colors/success-60 theme) + (colors/theme-colors colors/danger-50 colors/danger-60 theme))) + +(defn color-text-heading + [theme] + (colors/theme-colors colors/neutral-100 colors/white theme)) + +(defn color-text-paragraph + [theme] + (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 theme)) + +(defn style-text-heading + [theme] + {:color (color-text-heading theme)}) + +(defn style-text-paragraph + [theme] + {:color (color-text-paragraph theme)}) + +(defn dot-separator + [metrics theme] + {:background-color (color-metrics metrics theme) + :margin-horizontal 4 + :width 2 + :height 2}) + +(defn loading-bar + [width height margin-right theme] + {:width width + :height height + :border-radius 6 + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme) + :margin-right margin-right}) diff --git a/src/quo/components/wallet/wallet_overview/view.cljs b/src/quo/components/wallet/wallet_overview/view.cljs new file mode 100644 index 00000000000..c37715fa5a7 --- /dev/null +++ b/src/quo/components/wallet/wallet_overview/view.cljs @@ -0,0 +1,126 @@ +(ns quo.components.wallet.wallet-overview.view + (:require + [quo.components.dropdowns.network-dropdown.view :as network-dropdown] + [quo.components.icon :as icon] + [quo.components.markdown.text :as text] + [quo.components.wallet.wallet-overview.schema :as component-schema] + [quo.components.wallet.wallet-overview.style :as style] + [quo.context :as quo.context] + [react-native.core :as rn] + [schema.core :as schema] + [utils.i18n :as i18n])) + +(def ^:private time-frames + {:one-week (i18n/label :t/one-week-int) + :one-month (i18n/label :t/one-month-int) + :three-months (i18n/label :t/three-months-int) + :one-year (i18n/label :t/one-year) + :all-time (i18n/label :t/all-time)}) + +(defn- loading-bars + [bars theme] + (map-indexed (fn [index {:keys [width height margin]}] + ^{:key index} + [rn/view {:style (style/loading-bar width height margin theme)}]) + bars)) + +(defn- view-info-top + [{:keys [state balance networks networks-filtered? dropdown-on-press dropdown-state + show-new-chain-indicator?]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/container-info-top} + (if (= state :loading) + (loading-bars [{:width 201 :height 20 :margin 0}] theme) + [text/text + {:weight :semi-bold + :size :heading-1 + :style (style/style-text-heading theme)} + balance]) + [network-dropdown/view + {:state (or dropdown-state :default) + :networks-filtered? networks-filtered? + :blur? true + :on-press dropdown-on-press + :show-new-chain-indicator? show-new-chain-indicator?} + networks]])) + +(defn- view-metrics + [{:keys [metrics currency-change percentage-change theme]}] + [rn/view + {:style {:flex-direction :row + :align-items :center}} + [text/text + {:weight :medium + :size :paragraph-2 + :style {:color (style/color-metrics metrics theme)}} + percentage-change] + [rn/view + {:style (style/dot-separator metrics theme)}] + [text/text + {:weight :medium + :size :paragraph-2 + :style {:color (style/color-metrics metrics theme) + :margin-right 4}} + currency-change] + [icon/icon + (if (= metrics :positive) :i/positive :i/negative) + {:color (style/color-metrics metrics theme) + :size 16}]]) + +(defn- view-info-bottom + [{:keys [state time-frame metrics date begin-date end-date + currency-change percentage-change]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/container-info-bottom} + (when (= state :loading) + [rn/view + {:style {:flex-direction :row + :align-items :center}} + (loading-bars [{:width 32 :height 10 :margin 8} + {:width 32 :height 10 :margin 2} + {:width 62 :height 10 :margin 4} + {:width 10 :height 10 :margin 0}] + theme)]) + (when (and (= state :default) (= time-frame :selected)) + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/style-text-paragraph theme)} + date]) + (when (and (= state :default) (= time-frame :custom)) + [rn/view {:style {:flex-direction :row}} + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/style-text-paragraph theme)} + begin-date] + [icon/icon :i/positive-right + {:color (style/color-text-paragraph theme) + :size 16}] + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/style-text-paragraph theme)} + end-date]]) + (when (and (= state :default) (not (#{:none :selected} time-frame))) + [rn/view {:style {:flex-direction :row}} + [text/text + {:weight :medium + :size :paragraph-2 + :style {:color (style/color-text-paragraph theme) + :margin-right 8}} + (time-frame time-frames)] + (when (and (= state :default) (not= metrics :none)) + [view-metrics + {:metrics metrics + :currency-change currency-change + :percentage-change percentage-change + :theme theme}])])])) + +(defn- view-internal + [props] + [rn/view {:style (style/container-info props)} + [view-info-top props] + [view-info-bottom props]]) + +(def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/quo/context.cljs b/src/quo/context.cljs new file mode 100644 index 00000000000..8f1137c79a9 --- /dev/null +++ b/src/quo/context.cljs @@ -0,0 +1,31 @@ +(ns quo.context + (:require + ["react" :as react] + [oops.core :as oops] + [react-native.core :as rn])) + +(defonce ^:private context (react/createContext nil)) + +(defn provider + [data & children] + (into [:> (.-Provider context) {:value #js {:cljData data}}] + children)) + +(defn use-theme + "A hook that returns the current theme keyword." + [] + (if-let [data (rn/use-context context)] + (:theme (oops/oget data :cljData)) + :light)) + +(defn use-screen-id + "A hook that returns the current screen id." + [] + (when-let [data (rn/use-context context)] + (:screen-id (oops/oget data :cljData)))) + +(defn use-screen-params + "A hook that returns the current screen params" + [] + (when-let [data (rn/use-context context)] + (:screen-params (oops/oget data :cljData)))) diff --git a/src/quo/core.cljs b/src/quo/core.cljs new file mode 100644 index 00000000000..97d1e7d77f0 --- /dev/null +++ b/src/quo/core.cljs @@ -0,0 +1,489 @@ +(ns quo.core + (:require + quo.components.animated-header-flatlist.view + quo.components.avatars.account-avatar.view + quo.components.avatars.channel-avatar.view + quo.components.avatars.collection-avatar.view + quo.components.avatars.community-avatar.view + quo.components.avatars.dapp-avatar.view + quo.components.avatars.group-avatar.view + quo.components.avatars.icon-avatar + quo.components.avatars.token-avatar.view + quo.components.avatars.user-avatar.view + quo.components.avatars.wallet-user-avatar.view + quo.components.banners.alert-banner.view + quo.components.banners.banner.view + quo.components.blur.view + quo.components.browser.browser-input.view + quo.components.buttons.button.view + quo.components.buttons.composer-button.view + quo.components.buttons.dynamic-button.view + quo.components.buttons.logout-button.view + quo.components.buttons.predictive-keyboard.view + quo.components.buttons.slide-button.view + quo.components.buttons.swap-order-button.view + quo.components.buttons.wallet-button.view + quo.components.buttons.wallet-ctas.view + quo.components.calendar.calendar-day.view + quo.components.calendar.calendar-year.view + quo.components.calendar.calendar.view + quo.components.cards.wallet-card.view + quo.components.code.snippet-preview.view + quo.components.code.snippet.view + quo.components.colors.color-picker.view + quo.components.colors.color.view + quo.components.common.dot-separator + quo.components.common.list.view + quo.components.common.new-feature-gradient + quo.components.common.notification-dot.view + quo.components.common.separator.view + quo.components.community.banner.view + quo.components.community.channel-action.view + quo.components.community.channel-actions.view + quo.components.community.community-card-view + quo.components.community.community-detail-token-gating.view + quo.components.community.community-list-view + quo.components.community.community-stat.view + quo.components.community.community-token-gating.view + quo.components.community.community-view + quo.components.community.icon + quo.components.counter.collectible-counter.view + quo.components.counter.counter.view + quo.components.counter.fraction-counter.view + quo.components.counter.step.view + quo.components.dividers.date + quo.components.dividers.divider-label.view + quo.components.dividers.divider-line.view + quo.components.dividers.new-messages + quo.components.dividers.strength-divider.view + quo.components.drawers.action-drawers.view + quo.components.drawers.bottom-actions.view + quo.components.drawers.documentation-drawers.view + quo.components.drawers.drawer-action.view + quo.components.drawers.drawer-buttons.view + quo.components.drawers.drawer-top.view + quo.components.drawers.permission-context.view + quo.components.dropdowns.dropdown-input.view + quo.components.dropdowns.dropdown.view + quo.components.dropdowns.network-dropdown.view + quo.components.empty-state.empty-state.view + quo.components.gradient.gradient-cover.view + quo.components.graph.interactive-graph.view + quo.components.graph.wallet-graph.view + quo.components.icon + quo.components.info.info-message.view + quo.components.info.information-box.view + quo.components.inputs.address-input.view + quo.components.inputs.input.view + quo.components.inputs.locked-input.view + quo.components.inputs.profile-input.view + quo.components.inputs.recovery-phrase.view + quo.components.inputs.search-input.view + quo.components.inputs.title-input.view + quo.components.ios.drawer-bar.view + quo.components.keycard.view + quo.components.links.internal-link-card.view + quo.components.links.link-preview.view + quo.components.links.url-preview-list.view + quo.components.links.url-preview.view + quo.components.list-items.account-list-card.view + quo.components.list-items.account.view + quo.components.list-items.address.view + quo.components.list-items.approval-info.view + quo.components.list-items.channel.view + quo.components.list-items.community.view + quo.components.list-items.dapp.view + quo.components.list-items.market-token.view + quo.components.list-items.menu-item + quo.components.list-items.missing-keypair.view + quo.components.list-items.network-list.view + quo.components.list-items.preview-list.view + quo.components.list-items.quiz-item.view + quo.components.list-items.saved-address.view + quo.components.list-items.saved-contact-address.view + quo.components.list-items.status-list-item.view + quo.components.list-items.token-info.view + quo.components.list-items.token-network.view + quo.components.list-items.token-value.view + quo.components.list-items.user + quo.components.loaders.skeleton-list.view + quo.components.markdown.list.view + quo.components.markdown.text + quo.components.messages.author.view + quo.components.messages.gap + quo.components.messages.system-message.view + quo.components.navigation.bottom-nav-tab.view + quo.components.navigation.floating-shell-button.view + quo.components.navigation.page-nav.view + quo.components.navigation.top-nav.view + quo.components.notifications.activity-log.view + quo.components.notifications.activity-logs-photos.view + quo.components.notifications.count-down-circle + quo.components.notifications.notification.view + quo.components.notifications.toast.view + quo.components.numbered-keyboard.keyboard-key.view + quo.components.numbered-keyboard.numbered-keyboard.view + quo.components.onboarding.small-option-card.view + quo.components.overlay.view + quo.components.password.password-tips.view + quo.components.password.tips.view + quo.components.pin-input.view + quo.components.profile.collectible-list-item.view + quo.components.profile.collectible.view + quo.components.profile.expanded-collectible.view + quo.components.profile.link-card.view + quo.components.profile.profile-card.view + quo.components.profile.select-profile.view + quo.components.profile.showcase-nav.view + quo.components.record-audio.record-audio.view + quo.components.record-audio.soundtrack.view + quo.components.selectors.disclaimer.view + quo.components.selectors.filter.view + quo.components.selectors.react-selector.view + quo.components.selectors.react.view + quo.components.selectors.reactions-selector.view + quo.components.selectors.selectors.view + quo.components.settings.accounts.view + quo.components.settings.category.view + quo.components.settings.data-item.view + quo.components.settings.page-setting.view + quo.components.settings.privacy-option.view + quo.components.settings.reorder-item.view + quo.components.settings.section-label.view + quo.components.settings.settings-item.view + quo.components.share.qr-code.view + quo.components.share.share-qr-code.view + quo.components.slideshow.slider-bar.view + quo.components.tabs.account-selector + quo.components.tabs.segmented-tab + quo.components.tabs.tabs.view + quo.components.tags.collectible-tag.view + quo.components.tags.context-tag.view + quo.components.tags.network-status-tag.view + quo.components.tags.network-tags.view + quo.components.tags.number-tag.view + quo.components.tags.permission-tag + quo.components.tags.status-tags + quo.components.tags.summary-tag.view + quo.components.tags.tag + quo.components.tags.tags + quo.components.tags.tiny-tag.view + quo.components.tags.token-tag.view + quo.components.text-combinations.channel-name.view + quo.components.text-combinations.page-top.view + quo.components.text-combinations.standard-title.view + quo.components.text-combinations.username.view + quo.components.text-combinations.view + quo.components.utilities.token.view + quo.components.wallet.account-card.view + quo.components.wallet.account-origin.view + quo.components.wallet.account-overview.view + quo.components.wallet.account-permissions.view + quo.components.wallet.address-text.view + quo.components.wallet.amount-input.view + quo.components.wallet.approval-label.view + quo.components.wallet.confirmation-progress.view + quo.components.wallet.keypair.view + quo.components.wallet.missing-keypairs.view + quo.components.wallet.network-amount.view + quo.components.wallet.network-bridge.view + quo.components.wallet.network-link.view + quo.components.wallet.network-routing.view + quo.components.wallet.progress-bar.view + quo.components.wallet.required-tokens.view + quo.components.wallet.summary-info.view + quo.components.wallet.swap-input.view + quo.components.wallet.token-input.view + quo.components.wallet.transaction-progress.view + quo.components.wallet.transaction-summary.view + quo.components.wallet.wallet-activity.view + quo.components.wallet.wallet-overview.view) + (:refer-clojure :exclude [filter])) + +;; Common +(def separator quo.components.common.separator.view/separator) +(def new-feature-gradient quo.components.common.new-feature-gradient/view) +(def item-list quo.components.common.list.view/view) +(def dot-separator quo.components.common.dot-separator/view) + +;;;; Avatar +(def account-avatar quo.components.avatars.account-avatar.view/view) +(def channel-avatar quo.components.avatars.channel-avatar.view/view) +(def collection-avatar quo.components.avatars.collection-avatar.view/view) +(def community-avatar quo.components.avatars.community-avatar.view/view) +(def dapp-avatar quo.components.avatars.dapp-avatar.view/view) +(def group-avatar quo.components.avatars.group-avatar.view/view) +(def icon-avatar quo.components.avatars.icon-avatar/icon-avatar) +(def token-avatar quo.components.avatars.token-avatar.view/view) +(def user-avatar quo.components.avatars.user-avatar.view/user-avatar) +(def wallet-user-avatar quo.components.avatars.wallet-user-avatar.view/wallet-user-avatar) + +;;;; Banner +(def alert-banner quo.components.banners.alert-banner.view/view) +(def banner quo.components.banners.banner.view/view) + +;;;; Buttons +(def button quo.components.buttons.button.view/button) +(def composer-button quo.components.buttons.composer-button.view/view) +(def dynamic-button quo.components.buttons.dynamic-button.view/view) +(def logout-button quo.components.buttons.logout-button.view/view) +(def predictive-keyboard quo.components.buttons.predictive-keyboard.view/view) +(def slide-button quo.components.buttons.slide-button.view/view) +(def swap-order-button quo.components.buttons.swap-order-button.view/view) +(def wallet-button quo.components.buttons.wallet-button.view/view) +(def wallet-ctas quo.components.buttons.wallet-ctas.view/view) + +;;;; Browser +(def browser-input quo.components.browser.browser-input.view/view) + +;;;; Blur +(def blur quo.components.blur.view/view) + +;;;; Calendar +(def calendar quo.components.calendar.calendar.view/view) +(def calendar-day quo.components.calendar.calendar-day.view/view) +(def calendar-year quo.components.calendar.calendar-year.view/view) + +;;;; Code +(def snippet quo.components.code.snippet.view/view) +(def snippet-preview quo.components.code.snippet-preview.view/view) + +;;;; Cards +(def small-option-card quo.components.onboarding.small-option-card.view/small-option-card) +(def keycard quo.components.keycard.view/keycard) +(def wallet-card quo.components.cards.wallet-card.view/view) + +;;;; Colors +(def color-picker quo.components.colors.color-picker.view/view) +(def color quo.components.colors.color.view/view) + +;;;; Community +(def community-card-view-item quo.components.community.community-card-view/view) +(def community-detail-token-gating quo.components.community.community-detail-token-gating.view/view) +(def community-token-gating quo.components.community.community-token-gating.view/view) +(def communities-membership-list-item + quo.components.community.community-list-view/communities-membership-list-item) +(def community-stat quo.components.community.community-stat.view/view) +(def community-tags quo.components.community.community-view/community-tags) +(def discover-card quo.components.community.banner.view/view) +(def channel-action quo.components.community.channel-action.view/view) +(def channel-actions quo.components.community.channel-actions.view/view) + +;;;; Counter +(def collectible-counter quo.components.counter.collectible-counter.view/view) +(def fraction-counter quo.components.counter.fraction-counter.view/view) +(def counter quo.components.counter.counter.view/view) +(def step #'quo.components.counter.step.view/view) + +;;;; Dividers +(def divider-label quo.components.dividers.divider-label.view/view) +(def divider-line quo.components.dividers.divider-line.view/view) +(def new-messages quo.components.dividers.new-messages/view) +(def divider-date quo.components.dividers.date/date) +(def strength-divider quo.components.dividers.strength-divider.view/view) + +;;;; Drawers +(def action-drawer quo.components.drawers.action-drawers.view/action-drawer) +(def drawer-action quo.components.drawers.drawer-action.view/view) +(def documentation-drawers quo.components.drawers.documentation-drawers.view/view) +(def drawer-buttons quo.components.drawers.drawer-buttons.view/view) +(def drawer-top quo.components.drawers.drawer-top.view/view) +(def permission-context quo.components.drawers.permission-context.view/view) +(def bottom-actions quo.components.drawers.bottom-actions.view/view) + +;;;; Dropdowns +(def dropdown quo.components.dropdowns.dropdown.view/view) +(def dropdown-input quo.components.dropdowns.dropdown-input.view/view) +(def network-dropdown quo.components.dropdowns.network-dropdown.view/view) + +;;;; Empty State +(def empty-state quo.components.empty-state.empty-state.view/empty-state) + +;;;; Graph +(def interactive-graph quo.components.graph.interactive-graph.view/view) +(def wallet-graph quo.components.graph.wallet-graph.view/view) + +;;;; Header +(def animated-header-list quo.components.animated-header-flatlist.view/animated-header-list) + +;;;; Gradient +(def gradient-cover quo.components.gradient.gradient-cover.view/view) + +;;;; Icon +(def icon quo.components.icon/icon) + +;;;; Info +(def info-message quo.components.info.info-message.view/view) +(def information-box quo.components.info.information-box.view/view) + +;;;; Inputs +(def input quo.components.inputs.input.view/input) +(def address-input quo.components.inputs.address-input.view/address-input) +(def locked-input quo.components.inputs.locked-input.view/locked-input) +(def profile-input quo.components.inputs.profile-input.view/profile-input) +(def recovery-phrase-input quo.components.inputs.recovery-phrase.view/recovery-phrase-input) +(def search-input quo.components.inputs.search-input.view/search-input) +(def title-input quo.components.inputs.title-input.view/view) + +;;;; iOS +(def drawer-bar quo.components.ios.drawer-bar.view/view) + +;;;; Numbered Keyboard +(def keyboard-key quo.components.numbered-keyboard.keyboard-key.view/view) +(def numbered-keyboard quo.components.numbered-keyboard.numbered-keyboard.view/view) + +;;;; PIN input +(def pin-input quo.components.pin-input.view/view) + +;;;; Links +(def internal-link-card quo.components.links.internal-link-card.view/view) +(def link-preview quo.components.links.link-preview.view/view) +(def url-preview quo.components.links.url-preview.view/view) +(def url-preview-list quo.components.links.url-preview-list.view/view) + +;;;; List items +(def account-item quo.components.list-items.account.view/view) +(def account-list-card quo.components.list-items.account-list-card.view/view) +(def address quo.components.list-items.address.view/view) +(def approval-info quo.components.list-items.approval-info.view/view) +(def channel quo.components.list-items.channel.view/view) +(def community-list quo.components.list-items.community.view/view) +(def dapp quo.components.list-items.dapp.view/view) +(def menu-item quo.components.list-items.menu-item/menu-item) +(def missing-keypair quo.components.list-items.missing-keypair.view/view) +(def network-list quo.components.list-items.network-list.view/view) +(def preview-list quo.components.list-items.preview-list.view/view) +(def quiz-item quo.components.list-items.quiz-item.view/view) +(def saved-address quo.components.list-items.saved-address.view/view) +(def saved-contact-address quo.components.list-items.saved-contact-address.view/view) +(def token-info quo.components.list-items.token-info.view/view) +(def token-network quo.components.list-items.token-network.view/view) +(def token-value quo.components.list-items.token-value.view/view) +(def market-token quo.components.list-items.market-token.view/view) +(def user quo.components.list-items.user/user) +(def status-list-item quo.components.list-items.status-list-item.view/view) + +;;;; Loaders +(def skeleton-list quo.components.loaders.skeleton-list.view/view) + +;;;; Navigation +(def bottom-nav-tab quo.components.navigation.bottom-nav-tab.view/view) +(def floating-shell-button quo.components.navigation.floating-shell-button.view/view) +(def page-nav quo.components.navigation.page-nav.view/page-nav) +(def top-nav quo.components.navigation.top-nav.view/view) + +;;;; Markdown +(def markdown-list quo.components.markdown.list.view/view) +(def text quo.components.markdown.text/text) + +;;;; Messages +(def author quo.components.messages.author.view/view) +(def gap quo.components.messages.gap/gap) +(def system-message quo.components.messages.system-message.view/system-message) + +;;;; Notifications +(def activity-log quo.components.notifications.activity-log.view/view) +(def activity-logs-photos quo.components.notifications.activity-logs-photos.view/view) +(def notification quo.components.notifications.notification.view/notification) +(def toast quo.components.notifications.toast.view/toast) + +;;;; Overlay +(def overlay quo.components.overlay.view/view) + +;;;; Password +(def tips quo.components.password.tips.view/view) +(def password-tips quo.components.password.password-tips.view/view) + +;;;; Profile +(def collectible quo.components.profile.collectible.view/collectible) +(def collectible-list-item quo.components.profile.collectible-list-item.view/view) +(def expanded-collectible quo.components.profile.expanded-collectible.view/view) +(def link-card quo.components.profile.link-card.view/view) +(def profile-card quo.components.profile.profile-card.view/profile-card) +(def select-profile quo.components.profile.select-profile.view/view) +(def showcase-nav quo.components.profile.showcase-nav.view/view) + +;;;; Record Audio +(def record-audio quo.components.record-audio.record-audio.view/record-audio) +(def soundtrack quo.components.record-audio.soundtrack.view/soundtrack) + +;;;; Selectors +(def disclaimer quo.components.selectors.disclaimer.view/view) +(def filter quo.components.selectors.filter.view/view) +(def reactions-selector quo.components.selectors.reactions-selector.view/view) +(def react quo.components.selectors.react.view/view) +(def react-selector quo.components.selectors.react-selector.view/view) +(def selectors quo.components.selectors.selectors.view/view) + +;;;; Settings +(def account quo.components.settings.accounts.view/account) +(def category quo.components.settings.category.view/category) +(def data-item quo.components.settings.data-item.view/view) +(def page-setting quo.components.settings.page-setting.view/page-setting) +(def privacy-option quo.components.settings.privacy-option.view/view) +(def reorder-item quo.components.settings.reorder-item.view/reorder-item) +(def section-label quo.components.settings.section-label.view/view) +(def settings-item quo.components.settings.settings-item.view/view) + +;;;; Share +(def qr-code quo.components.share.qr-code.view/view) +(def share-qr-code quo.components.share.share-qr-code.view/view) + +;;;; Slideshow +(def slider-bar quo.components.slideshow.slider-bar.view/view) + +;;;; Tabs +(def tabs quo.components.tabs.tabs.view/view) +(def segmented-control quo.components.tabs.segmented-tab/segmented-control) +(def account-selector quo.components.tabs.account-selector/account-selector) + +;;;; Tags +(def collectible-tag quo.components.tags.collectible-tag.view/view) +(def context-tag quo.components.tags.context-tag.view/view) +(def network-tags quo.components.tags.network-tags.view/view) +(def number-tag quo.components.tags.number-tag.view/view) +(def network-status-tag quo.components.tags.network-status-tag.view/view) +(def permission-tag quo.components.tags.permission-tag/tag) +(def status-tag quo.components.tags.status-tags/status-tag) +(def summary-tag quo.components.tags.summary-tag.view/view) +(def tag quo.components.tags.tag/tag) +(def tags quo.components.tags.tags/tags) +(def tiny-tag quo.components.tags.tiny-tag.view/view) +(def token-tag quo.components.tags.token-tag.view/view) + +;;;; Text combinations +(def channel-name quo.components.text-combinations.channel-name.view/view) +(def page-top quo.components.text-combinations.page-top.view/view) +(def standard-title quo.components.text-combinations.standard-title.view/view) +(def ^{:deprecated "quo.components.text-combinations.page-top.view should be used instead"} + text-combinations + quo.components.text-combinations.view/view) +(def username quo.components.text-combinations.username.view/view) + +;;;; Utilities +(def token quo.components.utilities.token.view) + +;;;; Wallet +(def account-card quo.components.wallet.account-card.view/view) +(def account-origin quo.components.wallet.account-origin.view/view) +(def account-overview quo.components.wallet.account-overview.view/view) +(def account-permissions quo.components.wallet.account-permissions.view/view) +(def address-text quo.components.wallet.address-text.view/view) +(def amount-input quo.components.wallet.amount-input.view/view) +(def approval-label quo.components.wallet.approval-label.view/view) +(def confirmation-progress quo.components.wallet.confirmation-progress.view/view) +(def keypair quo.components.wallet.keypair.view/view) +(def missing-keypairs quo.components.wallet.missing-keypairs.view/view) +(def network-amount quo.components.wallet.network-amount.view/view) +(def network-bridge quo.components.wallet.network-bridge.view/view) +(def network-routing quo.components.wallet.network-routing.view/view) +(def progress-bar quo.components.wallet.progress-bar.view/view) +(def required-tokens quo.components.wallet.required-tokens.view/view) +(def summary-info quo.components.wallet.summary-info.view/view) +(def swap-input quo.components.wallet.swap-input.view/view) +(def network-link quo.components.wallet.network-link.view/view) +(def token-input quo.components.wallet.token-input.view/view) +(def wallet-overview quo.components.wallet.wallet-overview.view/view) +(def wallet-activity quo.components.wallet.wallet-activity.view/view) +(def transaction-progress quo.components.wallet.transaction-progress.view/view) +(def transaction-summary quo.components.wallet.transaction-summary.view/view) diff --git a/src/quo/core_spec.cljs b/src/quo/core_spec.cljs new file mode 100644 index 00000000000..c972eb22ab3 --- /dev/null +++ b/src/quo/core_spec.cljs @@ -0,0 +1,120 @@ +(ns quo.core-spec + (:require + quo.components.avatars.account-avatar.component-spec + quo.components.avatars.community-avatar.component-spec + quo.components.avatars.dapp-avatar.component-spec + quo.components.avatars.token-avatar.component-spec + quo.components.avatars.user-avatar.component-spec + quo.components.banners.alert-banner.component-spec + quo.components.banners.banner.component-spec + quo.components.browser.browser-input.component-spec + quo.components.buttons.button.component-spec + quo.components.buttons.composer-button.component-spec + quo.components.buttons.logout-button.component-spec + quo.components.buttons.predictive-keyboard.component-spec + quo.components.buttons.slide-button.component-spec + quo.components.buttons.swap-order-button.component-spec + quo.components.buttons.wallet-button.component-spec + quo.components.buttons.wallet-ctas.component-spec + quo.components.calendar.calendar-day.component-spec + quo.components.calendar.calendar-year.component-spec + quo.components.calendar.calendar.component-spec + quo.components.calendar.calendar.month-picker.component-spec + quo.components.colors.color-picker.component-spec + quo.components.community.community-detail-token-gating.component-spec + quo.components.community.community-stat.component-spec + quo.components.community.community-token-gating.component-spec + quo.components.counter.collectible-counter.component-spec + quo.components.counter.counter.component-spec + quo.components.counter.step.component-spec + quo.components.dividers.divider-label.component-spec + quo.components.dividers.strength-divider.component-spec + quo.components.drawers.action-drawers.component-spec + quo.components.drawers.bottom-actions.component-spec + quo.components.drawers.documentation-drawers.component-spec + quo.components.drawers.drawer-action.component-spec + quo.components.drawers.drawer-buttons.component-spec + quo.components.drawers.drawer-top.component-spec + quo.components.drawers.permission-context.component-spec + quo.components.dropdowns.dropdown-input.component-spec + quo.components.dropdowns.dropdown.component-spec + quo.components.dropdowns.network-dropdown.component-spec + quo.components.gradient.gradient-cover.component-spec + quo.components.graph.wallet-graph.component-spec + quo.components.info.info-message.component-spec + quo.components.inputs.address-input.component-spec + quo.components.inputs.input.component-spec + quo.components.inputs.locked-input.component-spec + quo.components.inputs.profile-input.component-spec + quo.components.inputs.recovery-phrase.component-spec + quo.components.inputs.title-input.component-spec + quo.components.keycard.component-spec + quo.components.links.internal-link-card.component-spec + quo.components.links.link-preview.component-spec + quo.components.links.url-preview-list.component-spec + quo.components.links.url-preview.component-spec + quo.components.list-items.account.component-spec + quo.components.list-items.address.component-spec + quo.components.list-items.approval-info.component-spec + quo.components.list-items.channel.component-spec + quo.components.list-items.community.component-spec + quo.components.list-items.dapp.component-spec + quo.components.list-items.network-list.component-spec + quo.components.list-items.quiz-item.component-spec + quo.components.list-items.saved-address.component-spec + quo.components.list-items.saved-contact-address.component-spec + quo.components.list-items.status-list-item.component-spec + quo.components.list-items.token-info.component-spec + quo.components.list-items.token-network.component-spec + quo.components.list-items.token-value.component-spec + quo.components.loaders.skeleton-list.component-spec + quo.components.markdown.list.component-spec + quo.components.markdown.text-component-spec + quo.components.navigation.top-nav.component-spec + quo.components.notifications.notification.component-spec + quo.components.numbered-keyboard.keyboard-key.component-spec + quo.components.onboarding.small-option-card.component-spec + quo.components.password.tips.component-spec + quo.components.profile.link-card.component-spec + quo.components.profile.select-profile.component-spec + quo.components.profile.showcase-nav.component-spec + quo.components.record-audio.record-audio.component-spec + quo.components.record-audio.soundtrack.component-spec + quo.components.selectors.disclaimer.component-spec + quo.components.selectors.filter.component-spec + quo.components.selectors.reactions-selector.component-spec + quo.components.selectors.selectors.component-spec + quo.components.settings.category.component-spec + quo.components.settings.data-item.component-spec + quo.components.settings.reorder-item.component-spec + quo.components.settings.settings-item.component-spec + quo.components.share.share-qr-code.component-spec + quo.components.slideshow.slider-bar.component-spec + quo.components.tags.collectible-tag.component-spec + quo.components.tags.network-tags.component-spec + quo.components.tags.status-tags-component-spec + quo.components.tags.summary-tag.component-spec + quo.components.tags.tiny-tag.component-spec + quo.components.text-combinations.channel-name.component-spec + quo.components.text-combinations.page-top.component-spec + quo.components.text-combinations.username.component-spec + quo.components.wallet.account-card.component-spec + quo.components.wallet.account-origin.component-spec + quo.components.wallet.account-overview.component-spec + quo.components.wallet.account-permissions.component-spec + quo.components.wallet.amount-input.component-spec + quo.components.wallet.approval-label.component-spec + quo.components.wallet.confirmation-progress.component-spec + quo.components.wallet.keypair.component-spec + quo.components.wallet.network-amount.component-spec + quo.components.wallet.network-bridge.component-spec + quo.components.wallet.network-routing.component-spec + quo.components.wallet.progress-bar.component-spec + quo.components.wallet.required-tokens.component-spec + quo.components.wallet.summary-info.component-spec + quo.components.wallet.swap-input.component-spec + quo.components.wallet.token-input.component-spec + quo.components.wallet.transaction-progress.component-spec + quo.components.wallet.transaction-summary.component-spec + quo.components.wallet.wallet-activity.component-spec + quo.components.wallet.wallet-overview.component-spec)) diff --git a/src/quo/foundations/colors.cljs b/src/quo/foundations/colors.cljs new file mode 100644 index 00000000000..4de625c413f --- /dev/null +++ b/src/quo/foundations/colors.cljs @@ -0,0 +1,387 @@ +(ns quo.foundations.colors + (:require + [clojure.string :as string] + [quo.context] + [react-native.platform :as platform])) + +(def account-colors + [:blue :yellow :purple :turquoise :magenta :sky :orange :army :pink :camel :copper]) + +(defn alpha + [value opacity] + (when value + (if (string/starts-with? value "#") + (let [hex (string/replace value #"#" "") + r (js/parseInt (subs hex 0 2) 16) + g (js/parseInt (subs hex 2 4) 16) + b (js/parseInt (subs hex 4 6) 16)] + (str "rgba(" r "," g "," b "," opacity ")")) + (let [rgb (string/split value #",")] + (str (string/join "," (butlast rgb)) "," opacity ")"))))) + +(defn- alpha-opaque + [value opacity] + (when value + (if (string/starts-with? value "#") + (let [hex (string/replace value #"#" "") + r (- 255 (* opacity (- 255 (js/parseInt (subs hex 0 2) 16)))) + g (- 255 (* opacity (- 255 (js/parseInt (subs hex 2 4) 16)))) + b (- 255 (* opacity (- 255 (js/parseInt (subs hex 4 6) 16))))] + (str "rgb(" r "," g "," b ")")) + (let [rgb (string/split value #",") + r (- 255 (* opacity (- 255 (get rgb 0)))) + g (- 255 (* opacity (- 255 (get rgb 1)))) + b (- 255 (* opacity (- 255 (get rgb 2))))] + (str "rgb(" r "," g "," b ")"))))) + +(def theme-alpha + (memoize + (fn + ([color light-opacity dark-opacity theme] + (theme-alpha color light-opacity color dark-opacity theme)) + ([light-color light-opacity dark-color dark-opacity theme] + (if (= :dark theme) + (alpha light-color light-opacity) + (alpha dark-color dark-opacity)))))) + +(defn valid-color? + [color] + (or (keyword? color) + (and (string? color) + (or (string/starts-with? color "#") + (string/starts-with? color "rgb"))))) + + +;;;;Neutral + +;;Solid + +(def neutral-2_5 "#FAFBFC") +(def neutral-5 "#F5F6F8") +(def neutral-10 "#F0F2F5") +(def neutral-20 "#E7EAEE") +(def neutral-30 "#DCE0E5") +(def neutral-40 "#A1ABBD") +(def neutral-50 "#647084") +(def neutral-60 "#303D55") +(def neutral-70 "#202C42") +(def neutral-80 "#1B273D") +(def neutral-90 "#131D2F") +(def neutral-95 "#0D1625") +(def neutral-100 "#09101C") + +(def neutral-50-opa-40 (alpha neutral-50 0.4)) + +;;80 with transparency +(def neutral-80-opa-5 (alpha neutral-80 0.05)) +(def neutral-80-opa-10 (alpha neutral-80 0.1)) +(def neutral-80-opa-15 (alpha neutral-80 0.15)) +(def neutral-80-opa-20 (alpha neutral-80 0.2)) +(def neutral-80-opa-30 (alpha neutral-80 0.3)) +(def neutral-80-opa-40 (alpha neutral-80 0.4)) +(def neutral-80-opa-50 (alpha neutral-80 0.5)) +(def neutral-80-opa-60 (alpha neutral-80 0.6)) +(def neutral-80-opa-70 (alpha neutral-80 0.7)) +(def neutral-80-opa-80 (alpha neutral-80 0.8)) +(def neutral-80-opa-90 (alpha neutral-80 0.9)) +(def neutral-80-opa-95 (alpha neutral-80 0.95)) + +;;90 with transparency +(def neutral-90-opa-0 (alpha neutral-90 0)) + +;;95 with transparency +(def neutral-95-opa-0 (alpha neutral-95 0)) +(def neutral-95-opa-10 (alpha neutral-95 0.1)) +(def neutral-95-opa-60 (alpha neutral-95 0.6)) +(def neutral-95-opa-70 (alpha neutral-95 0.7)) +(def neutral-95-opa-80 (alpha neutral-95 0.8)) +(def neutral-95-opa-90 (alpha neutral-95 0.9)) +(def neutral-95-opa-95 (alpha neutral-95 0.95)) + +;;100 with transparency +(def neutral-100-opa-0 (alpha neutral-100 0)) +(def neutral-100-opa-5 (alpha neutral-100 0.05)) +(def neutral-100-opa-8 (alpha neutral-100 0.08)) +(def neutral-100-opa-10 (alpha neutral-100 0.1)) +(def neutral-100-opa-30 (alpha neutral-100 0.3)) +(def neutral-100-opa-50 (alpha neutral-100 0.5)) +(def neutral-100-opa-60 (alpha neutral-100 0.6)) +(def neutral-100-opa-70 (alpha neutral-100 0.7)) +(def neutral-100-opa-80 (alpha neutral-100 0.8)) +(def neutral-100-opa-90 (alpha neutral-100 0.9)) +(def neutral-100-opa-95 (alpha neutral-100 0.95)) +(def neutral-100-opa-100 (alpha neutral-100 1)) + +;;80 with transparency opaque +(def neutral-80-opa-5-opaque (alpha-opaque neutral-80 0.05)) + +;;;;White + +;;Solid +(def white "#ffffff") + +;; with transparency +(def white-opa-0 (alpha white 0)) +(def white-opa-5 (alpha white 0.05)) +(def white-opa-10 (alpha white 0.1)) +(def white-opa-20 (alpha white 0.2)) +(def white-opa-30 (alpha white 0.3)) +(def white-opa-40 (alpha white 0.4)) +(def white-opa-50 (alpha white 0.5)) +(def white-opa-60 (alpha white 0.6)) +(def white-opa-70 (alpha white 0.7)) +(def white-opa-80 (alpha white 0.8)) +(def white-opa-90 (alpha white 0.9)) +(def white-opa-95 (alpha white 0.95)) + +;;;;Blur +(def white-70-blur (alpha white 0.7)) +(def white-70-blur-opaque (alpha-opaque white 0.7)) +(def neutral-80-opa-1-blur (alpha "#192438" 0.1)) +(def neutral-5-opa-70-blur (alpha neutral-5 0.7)) +(def neutral-10-opa-10-blur (alpha neutral-10 0.1)) +(def neutral-10-opa-40-blur (alpha neutral-10 0.4)) +;; https://github.com/status-im/status-mobile/issues/14903 +(def neutral-80-opa-80-blur (if platform/android? (alpha "#192438" 0.8) "#1E2430CC")) +(def neutral-90-opa-10-blur (alpha neutral-90 0.1)) +(def neutral-90-opa-40-blur (alpha neutral-90 0.4)) +(def neutral-90-opa-70-blur (alpha neutral-90 0.7)) +(def neutral-95-opa-70-blur neutral-95-opa-70) +(def neutral-100-opa-70-blur (if platform/android? neutral-100-opa-70 "#0D1014B3")) + +(def overlay-background-blur (if platform/android? (alpha "#192438" 0.8) "#242830cc")) +(def bottom-sheet-background-blur (if platform/ios? "#ED9D1D0D" neutral-100-opa-90)) + +;;;;Black + +;;Solid +(def black "#000000") +(def black-opa-0 (alpha black 0)) +(def black-opa-30 (alpha black 0.3)) +(def black-opa-60 (alpha black 0.6)) +(def onboarding-header-black neutral-100) +(def border-avatar-light + "Simulates a blurred, transparent border for avatars in light mode" + "#475060") +;;;;Primary + +;;Solid +(def primary-50 "#4360DF") +(def primary-60 "#354DB2") + +;;50 with transparency +(def primary-50-opa-5 (alpha primary-50 0.05)) +(def primary-50-opa-10 (alpha primary-50 0.1)) +(def primary-50-opa-20 (alpha primary-50 0.2)) +(def primary-50-opa-30 (alpha primary-50 0.3)) +(def primary-50-opa-40 (alpha primary-50 0.4)) + +;;;;Success + +;;Solid +(def success-50 "#23ADA0") +(def success-60 "#1C8A80") + +;;50 with transparency +(def success-50-opa-5 (alpha success-50 0.05)) +(def success-50-opa-10 (alpha success-50 0.1)) +(def success-50-opa-20 (alpha success-50 0.2)) +(def success-50-opa-30 (alpha success-50 0.3)) +(def success-50-opa-40 (alpha success-50 0.4)) + +(def success-60-opa-5 (alpha success-60 0.05)) +(def success-60-opa-10 (alpha success-60 0.1)) +(def success-60-opa-20 (alpha success-60 0.2)) +(def success-60-opa-30 (alpha success-60 0.3)) +(def success-60-opa-40 (alpha success-60 0.4)) + +;;Solid +(def danger-50 "#E95460") +(def danger-60 "#BA434D") + +(def system-yellow "#FFD60A") +(def keycard-color "#FF6400") + +;;50 with transparency +(def danger-50-opa-0 (alpha danger-50 0)) +(def danger-50-opa-5 (alpha danger-50 0.05)) +(def danger-50-opa-10 (alpha danger-50 0.1)) +(def danger-50-opa-20 (alpha danger-50 0.2)) +(def danger-50-opa-30 (alpha danger-50 0.3)) +(def danger-50-opa-40 (alpha danger-50 0.4)) + +;;60 with transparency +(def danger-60-opa-5 (alpha danger-60 0.05)) +(def danger-60-opa-10 (alpha danger-60 0.1)) + +;;;;Warning +(def warning-50 "#FF7D46") +(def warning-60 "#CC6438") + +;;50 with transparency +(def warning-50-opa-5 (alpha warning-50 0.05)) +(def warning-50-opa-10 (alpha warning-50 0.1)) +(def warning-50-opa-20 (alpha warning-50 0.2)) +(def warning-50-opa-30 (alpha warning-50 0.3)) +(def warning-50-opa-40 (alpha warning-50 0.4)) + +;; Colors for customizing users account +(def customization + {:blue {50 "#2A4AF5" + 60 "#223BC4"} + :yellow {50 "#F6B03C" + 60 "#C58D30"} + :turquoise {50 "#2A799B" + 60 "#22617C"} + :copper {50 "#CB6256" + 60 "#A24E45"} + :sky {50 "#1992D7" + 60 "#1475AC"} + :camel {50 "#C78F67" + 60 "#9F7252"} + :orange {50 "#FF7D46" + 60 "#CC6438"} + :army {50 "#216266" + 60 "#1A4E52"} + :pink {50 "#F66F8F" + 60 "#C55972"} + :purple {50 "#7140FD" + 60 "#5A33CA"} + :magenta {50 "#EC266C" + 60 "#BD1E56"}}) + +;;;; Networks +(def ^:private networks + {:ethereum "#758EEB" + :mainnet "#758EEB" + :optimism "#E76E6E" + :arbitrum "#6BD5F0" + :zkSync "#9FA0FE" + :hermez "#EB8462" + :xDai "#3FC0BD" + :polygon "#AD71F3" + :unknown "#EEF2F5"}) + +(def ^:private get-network-full-name + {:eth :ethereum + :oeth :optimism + :arb1 :arbitrum}) + +(def ^:private networks-short-name + (reduce (fn [acc [k v]] + (assoc acc k (get networks v))) + {} + get-network-full-name)) + +(def socials + {:social/link "#647084" + :social/facebook "#1877F2" + :social/github "#000000" + :social/instagram "#D8408E0F" + :social/lens "#00501E" + :social/linkedin "#0B86CA" + :social/mirror "#3E7EF7" + :social/opensea "#2081E2" + :social/pinterest "#CB2027" + :social/rarible "#FEDA03" + :social/snapchat "#FFFC00" + :social/spotify "#00DA5A" + :social/superrare "#000000" + :social/tumblr "#37474F" + :social/twitch "#673AB7" + :social/twitter "#262E35" + :social/youtube "#FF3000"}) + +(def ^:private colors-map + (merge {:primary {50 primary-50 + 60 primary-60} + :beige {50 "#CAAE93" + 60 "#AA927C"} + :green {50 "#5BCC95" + 60 "#4CAB7D"} + :brown {50 "#99604D" + 60 "#805141"} + :red {50 "#F46666" + 60 "#CD5656"} + :indigo {50 "#496289" + 60 "#3D5273"} + :danger {50 danger-50 + 60 danger-60} + :success {50 success-50 + 60 success-60} + :warning {50 warning-50 + 60 warning-60}} + customization + networks + networks-short-name + socials)) + +(def new-feature-gradient-pink "#FF33A3") + +(defn hex-string? + [s] + (and (string? s) (string/starts-with? s "#"))) + +(def fallback-color (customization :blue)) + +(defn- get-from-colors-map + [color suffix] + (let [color-without-suffix (get colors-map color fallback-color)] + (if (hex-string? color-without-suffix) + color-without-suffix + (get color-without-suffix suffix)))) + +(defn- resolve-color* + ([color theme] + (resolve-color* color theme nil)) + ([color theme opacity] + (let [suffix (cond + (not (keyword? color)) nil + (or opacity (= theme :light)) 50 + :else 60)] + (cond-> color + suffix (get-from-colors-map suffix) + opacity (alpha (/ opacity 100)))))) + +(defn- override-color* + ([color] + (resolve-color* color nil nil)) + ([color opacity] + (resolve-color* color opacity nil)) + ([color opacity color-suffix] + (let [suffix (cond + (not (keyword? color)) nil + color-suffix color-suffix + opacity 50 + :else 60)] + (cond-> color + suffix (get-from-colors-map suffix) + opacity (alpha (/ opacity 100)))))) + +(def override-color + "(override-color color opacity suffix) + color hex string or keyword - (resolves from custom, network and semantic colors) + opacity 0-100 (optional) + suffix optionally override the color suffix to use when retrieving the color" + (memoize override-color*)) + +(def resolve-color + "(resolve-color color theme opacity) + color hex string or keyword (resolves from custom, network and semantic colors) + theme :light/:dark + opacity 0-100 (optional) - if set theme is ignored and goes to 50 suffix internally" + (memoize resolve-color*)) + +(def shadow "rgba(9,16,28,0.08)") + +(defn theme-colors + "(theme-colors light dark override-theme)" + [light dark theme] + (if (= theme :light) light dark)) + +(defn invert-theme + [theme] + (case theme + :light :dark + :dark :light + nil)) diff --git a/src/quo/foundations/colors_test.cljs b/src/quo/foundations/colors_test.cljs new file mode 100644 index 00000000000..4d49ea4b8a3 --- /dev/null +++ b/src/quo/foundations/colors_test.cljs @@ -0,0 +1,34 @@ +(ns quo.foundations.colors-test + (:require + [cljs.test :refer-macros [deftest testing is]] + [quo.foundations.colors :as colors])) + +(deftest resolve-color-test + (testing "community color - resolves a hex string and ignores theme" + (is (= "#fff" (colors/resolve-color "#fff" :light)))) + (testing "community color - resolves a hex string and ignores theme" + (is (= "#fff" (colors/resolve-color "#fff" :dark)))) + (testing "user/wallet/chat colors - resolves a keyword from the customization map with light theme" + (is (= "#2A4AF5" (colors/resolve-color :blue :light)))) + (testing "user/wallet/chat colors - resolves a keyword from the colors map with dark theme" + (is (= "#223BC4" (colors/resolve-color :blue :dark)))) + (testing "network colors - resolves a keyword with from the networks map" + (is (= "#758EEB" (colors/resolve-color :ethereum :light))))) + +(deftest resolve-color-with-opacity-test + (testing "community color with 10% opacity- resolves a hex string and ignores theme" + (is (= "rgba(255,15,NaN,0.1)" (colors/resolve-color "#fff" :light 10)))) + (testing "community color with 10% opacity- resolves a hex string and ignores theme" + (is (= "rgba(255,15,NaN,0.9)" (colors/resolve-color "#fff" :light 90)))) + (testing + "user/wallet/chat colors with 10% opacity - resolves a keyword from the colors map with light theme" + (is (= "rgba(42,74,245,0.1)" (colors/resolve-color :blue :light 10)))) + (testing + "user/wallet/chat colors with 10% opacity - resolves a keyword from the colors map with dark theme" + (is (= "rgba(42,74,245,0.1)" (colors/resolve-color :blue :dark 10)))) + (testing + "when using opacity theme is ignored and uses the light suffix resolver" + (is (colors/resolve-color :blue :light 10) (colors/resolve-color :blue :dark 10)))) + +(deftest account-colors-customization-test + (is (every? #(contains? colors/customization %) colors/account-colors))) diff --git a/src/quo/foundations/customization_colors.cljs b/src/quo/foundations/customization_colors.cljs new file mode 100644 index 00000000000..cb6c822a94d --- /dev/null +++ b/src/quo/foundations/customization_colors.cljs @@ -0,0 +1,22 @@ +(ns quo.foundations.customization-colors + (:require + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn get-overlay-color + [theme pressed? customization-color] + (let [community-color (string? customization-color)] + (if (or community-color (= theme :light)) + (colors/alpha colors/black (if pressed? 0.2 0)) + (colors/alpha colors/black (if pressed? 0 0.2))))) + +(defn overlay + [{:keys [theme pressed? customization-color border-radius]}] + [rn/view + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :border-radius border-radius + :background-color (get-overlay-color theme pressed? customization-color)}]) diff --git a/src/quo/foundations/gradients.cljs b/src/quo/foundations/gradients.cljs new file mode 100644 index 00000000000..9e0b739370b --- /dev/null +++ b/src/quo/foundations/gradients.cljs @@ -0,0 +1,34 @@ +(ns quo.foundations.gradients + (:require + [quo.context] + [quo.foundations.colors :as colors] + [react-native.linear-gradient :as linear-gradient])) + +(defn- gradient-colors + [index theme] + (case index + :gradient-1 [(colors/resolve-color :yellow theme) + (colors/resolve-color :sky theme) + (colors/resolve-color :purple theme)] + :gradient-2 [(colors/resolve-color :orange theme) + (colors/resolve-color :purple theme) + (colors/resolve-color :blue theme)] + :gradient-3 [(colors/resolve-color :blue theme) + (colors/resolve-color :magenta theme) + (colors/resolve-color :yellow theme)] + :gradient-4 [(colors/resolve-color :army theme) + (colors/resolve-color :orange theme) + (colors/resolve-color :blue theme)] + [(colors/resolve-color :purple theme) + (colors/resolve-color :army theme) + (colors/resolve-color :yellow theme)])) + +(defn view + [{:keys [color-index container-style] :or {color-index 1}}] + (let [theme (quo.context/use-theme)] + [linear-gradient/linear-gradient + {:style (merge {:border-radius 16} container-style) + :accessibility-label :gradient-overlay + :colors (gradient-colors color-index theme) + :start {:x 0 :y 0} + :end {:x 0 :y 1}}])) diff --git a/src/quo/foundations/resources.cljs b/src/quo/foundations/resources.cljs new file mode 100644 index 00000000000..0308f3bd552 --- /dev/null +++ b/src/quo/foundations/resources.cljs @@ -0,0 +1,61 @@ +(ns quo.foundations.resources) + +(def ui + {:keycard-logo (js/require "../resources/images/ui2/keycard-logo.png") + :keycard-chip-light (js/require "../resources/images/ui2/keycard-chip-light.png") + :keycard-chip-dark (js/require "../resources/images/ui2/keycard-chip-dark.png") + :keycard-orange (js/require "../resources/images/ui2/keycard-orange.png") + :bored-ape (js/require "../resources/images/mock2/bored-ape.png")}) + +(def ui-themed + {:no-funds + {:light (js/require "../resources/images/ui2/no-funds-light.png") + :dark (js/require "../resources/images/ui2/no-funds-dark.png")} + :no-contacts-to-chat + {:light (js/require "../resources/images/ui2/no-contacts-to-chat-light.png") + :dark (js/require "../resources/images/ui2/no-contacts-to-chat-dark.png")}}) + +(defn get-themed-image + [k theme] + (get-in ui-themed [k theme])) + +(defn get-image + [k] + (get ui k)) + +(def dapps + {:coingecko (js/require "../resources/images/dapps/CoinGecko.png") + :1inch (js/require "../resources/images/dapps/1inch.png") + :aave (js/require "../resources/images/dapps/Aave.png") + :uniswap (js/require "../resources/images/dapps/Uniswap.png") + :zapper (js/require "../resources/images/dapps/Zapper.png") + :zerion (js/require "../resources/images/dapps/Zerion.png") + :wallet-connect (js/require "../resources/images/dapps/WalletConnect.png")}) + +(defn get-dapp + [k] + (get dapps k)) + +(def ^:private networks + {:arbitrum (js/require "../resources/images/networks/Arbitrum.png") + :bsc (js/require "../resources/images/networks/Binance.png") + :ethereum (js/require "../resources/images/networks/Ethereum.png") + :mainnet (js/require "../resources/images/networks/Ethereum.png") + :gnosis (js/require "../resources/images/networks/Gnosis.png") + :hermez (js/require "../resources/images/networks/Hermez.png") + :optimism (js/require "../resources/images/networks/Optimism.png") + :paraswap (js/require "../resources/images/networks/Paraswap.png") + :base (js/require "../resources/images/networks/Base.png") + :polygon (js/require "../resources/images/networks/Polygon.png") + :scroll (js/require "../resources/images/networks/Scroll.png") + :status (js/require "../resources/images/networks/Status.png") + :taiko (js/require "../resources/images/networks/Taiko.png") + :unknown (js/require "../resources/images/networks/Unknown.png") + :xdai (js/require "../resources/images/networks/xDAI.png") + :zksync (js/require "../resources/images/networks/zkSync.png")}) + +(def supported-networks (keys networks)) + +(defn get-network + [k] + (get networks k)) diff --git a/src/quo/foundations/shadows.cljs b/src/quo/foundations/shadows.cljs new file mode 100644 index 00000000000..431378f4826 --- /dev/null +++ b/src/quo/foundations/shadows.cljs @@ -0,0 +1,96 @@ +(ns quo.foundations.shadows + (:refer-clojure :exclude [get]) + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [utils.number])) + +;; Set radius coefficient based on Android Version. +;; A good way to know is to check if shadow color is supported i.e API level 28 (Android 9) above +(def ^:private shadow-radius-coefficient (if platform/is-shadow-color-supported? 0.8 0.1)) + +(defn- get-shadow-color + "Calculates the shadow color based on a given color and opacity, with an optional coefficient. + - Arity [color opacity]: Uses a default coefficient value of 5 (empirically determined) to compute the shadow color. + - Arity [color opacity coefficient]: Uses the specified coefficient to compute the shadow color. + On Android platforms, the shadow color's alpha is adjusted based on the product of opacity and + coefficient, constrained between 0 and 1. Returns a map with :shadow-color and :shadow-opacity keys." + ([color opacity] + (get-shadow-color color opacity 5)) + ([color opacity coefficient] + {:shadow-color (if platform/android? + (colors/alpha color (utils.number/value-in-range (* opacity coefficient) 0 1)) + color) + :shadow-opacity opacity})) + +(defn- get-shadow-radius + "Computes shadow radius and elevation properties based on a given radius, with an optional coefficient. + - Arity [radius]: Uses a default coefficient of 0.8 for API level 28 and above and 0.1 otherwise (empirically determined) to compute the elevation. + - Arity [radius coefficient]: Uses the specified coefficient to compute the elevation. + Returns a map with :elevation, computed as the product of radius and coefficient, and :shadow-radius keys." + ([radius] + (get-shadow-radius radius shadow-radius-coefficient)) + ([radius coefficient] + {:elevation (* radius coefficient) + :shadow-radius radius})) + +(def ^:private shadows + (let [dark-normal {1 (merge {:shadow-offset {:width 0 :height 4}} + (get-shadow-color colors/neutral-100 0.5) + (get-shadow-radius 20)) + 2 (merge {:shadow-offset {:width 0 :height 4}} + (get-shadow-color colors/neutral-100 0.64) + (get-shadow-radius 20)) + 3 (merge {:shadow-offset {:width 0 :height 12}} + (get-shadow-color colors/neutral-100 0.64) + (get-shadow-radius 20)) + 4 (merge {:shadow-offset {:width 0 :height 16}} + (get-shadow-color colors/neutral-100 0.72) + (get-shadow-radius 20))} + dark-normal-inverted (-> dark-normal + (update-in [:soft :shadow-offset :height] -) + (update-in [:medium :shadow-offset :height] -) + (update-in [:intense :shadow-offset :height] -) + (update-in [:strong :shadow-offset :height] -)) + light-normal {1 (merge {:shadow-offset {:width 0 :height 4}} + (get-shadow-color colors/neutral-100 0.04) + (get-shadow-radius 16)) + + 2 (merge {:shadow-offset {:width 0 :height 4}} + (get-shadow-color colors/neutral-100 0.08) + (get-shadow-radius 16)) + 3 (merge {:shadow-offset {:width 0 :height 12}} + (get-shadow-color colors/neutral-100 0.12) + (get-shadow-radius 16)) + 4 (merge {:shadow-offset {:width 0 :height 16}} + (get-shadow-color colors/neutral-100 0.16) + (get-shadow-radius 16))} + light-normal-inverted (-> light-normal + (update-in [:soft :shadow-offset :height] -) + (update-in [:medium :shadow-offset :height] -) + (update-in [:intense :shadow-offset :height] -) + (update-in [:strong :shadow-offset :height] -))] + {:dark {:normal dark-normal :inverted dark-normal-inverted} + :light {:normal light-normal :inverted light-normal-inverted}})) + +(defn get + "Get the appropriate shadow map for a given shadow `weight`, `theme`, and `scale-type`. + + Return nil if no shadow is found. + + `weight` - int (required) from 1 to 4. + `theme` - :light/:dark (optional). + `scale-type` - :normal/:inverted (optional). + " + ([weight theme] + (get weight theme :normal)) + ([weight theme scale-type] + (get-in shadows [theme scale-type weight]))) + +(def inner-shadow + {:shadow-color (colors/alpha colors/neutral-100 0.08) + :shadow-offset {:width 0 + :height 0} + :shadow-opacity 1 + :shadow-radius 16 + :elevation 13}) diff --git a/src/quo/foundations/typography.cljs b/src/quo/foundations/typography.cljs new file mode 100644 index 00000000000..e6b54dc9065 --- /dev/null +++ b/src/quo/foundations/typography.cljs @@ -0,0 +1,54 @@ +(ns quo.foundations.typography) + +(defn- tracking-fn + "Check inter dynamic metric https://rsms.me/inter/dynmetrics/" + [font-size] + (let [a -0.0223 + b 0.185 + c -0.1745 + e js/Math.E] + (->> font-size + (* c) + (js/Math.pow e) + (* b) + (+ a) + (* font-size)))) + +(def tracking (memoize tracking-fn)) + +(def heading-1 + {:font-size 27 + :line-height 32 + :letter-spacing (tracking 27)}) + +(def heading-2 + {:font-size 19 + :line-height 25.65 + :letter-spacing (tracking 19)}) + +(def paragraph-1 + {:font-size 15 + :line-height 21.75 + :letter-spacing (tracking 15)}) + +(def paragraph-2 + {:font-size 13 + :line-height 18.2 + :letter-spacing (tracking 13)}) + +(def label + {:font-size 11 + :line-height 15.62 + :letter-spacing (tracking 11)}) + +(def font-regular {:font-family "Inter-Regular"}) ; 400 + +(def font-medium {:font-family "Inter-Medium"}) ; 500 ff + +(def font-semi-bold {:font-family "Inter-SemiBold"}) ; 600 + +(def font-bold {:font-family "Inter-Bold"}) ; 700 + +(def monospace {:font-family "InterStatus-Regular"}) + +(def code {:font-family "UbuntuMono-Regular"}) diff --git a/src/react_native/async_storage.cljs b/src/react_native/async_storage.cljs new file mode 100644 index 00000000000..c43db17ba12 --- /dev/null +++ b/src/react_native/async_storage.cljs @@ -0,0 +1,66 @@ +(ns react-native.async-storage + (:require + ["@react-native-async-storage/async-storage" :default async-storage] + [cognitect.transit :as transit] + goog.functions + [taoensso.timbre :as log])) + +(def ^:private debounce-ms 250) + +(def ^:private reader (transit/reader :json)) +(def ^:private writer (transit/writer :json)) + +(defn clj->transit [o] (transit/write writer o)) +(defn transit->clj + [o] + (try (transit/read reader o) + (catch :default e + (log/error e)))) + +(defn set-item! + ([k value] (set-item! k value identity)) + ([k value cb] + (-> ^js async-storage + (.setItem (str k) + (clj->transit value)) + (.then (fn [_] (cb))) + (.catch (fn [error] + (log/error "[async-storage]" error)))))) + +(defn set-item-factory + [] + (let [tmp-storage (atom {}) + debounced (goog.functions/debounce (fn [] + (doseq [[k v] @tmp-storage] + (swap! tmp-storage dissoc k) + (set-item! k v))) + debounce-ms)] + (fn [items] + (swap! tmp-storage merge items) + (debounced)))) + +(defn get-items + [ks cb] + (-> ^js async-storage + (.multiGet (to-array (map str ks))) + (.then (fn [^js data] + (cb (->> (js->clj data) + (map (comp transit->clj second)) + (zipmap ks))))) + (.catch (fn [error] + (cb nil) + (log/error "[async-storage]" error))))) + +(defn get-item + ([k] (get-item k identity)) + ([k cb] + (-> ^js async-storage + (.getItem (str k)) + (.then (fn [^js data] + (-> data + js->clj + transit->clj + cb))) + (.catch (fn [error] + (cb nil) + (log/error "[async-storage]" error)))))) diff --git a/src/react_native/audio_toolkit.cljs b/src/react_native/audio_toolkit.cljs new file mode 100644 index 00000000000..c0d791e064a --- /dev/null +++ b/src/react_native/audio_toolkit.cljs @@ -0,0 +1,144 @@ +(ns react-native.audio-toolkit + (:require + ["@react-native-community/audio-toolkit" :refer + (Player Recorder MediaStates PlaybackCategories)])) + +;; get mediastates from react module +(def PLAYING (.-PLAYING ^js MediaStates)) +(def PAUSED (.-PAUSED ^js MediaStates)) +(def RECORDING (.-RECORDING ^js MediaStates)) +(def IDLE (.-IDLE ^js MediaStates)) +(def DESTROYED (.-DESTROYED ^js MediaStates)) + +;; get PlaybackCategories from react module +(def PLAYBACK (.-Playback ^js PlaybackCategories)) + +(def default-recorder-options + {:filename "recording.aac" + :bitrate 32000 + :channels 1 + :sampleRate 22050 + :quality "medium" ; ios only + :meteringInterval 50 + :category PLAYBACK}) + +(defn get-state + [player-recorder] + (when player-recorder + (.-state ^js player-recorder))) + +(defn new-recorder + [options on-meter on-ended] + (let [recorder (new ^js Recorder + (:filename options) + (clj->js options))] + (when on-meter + (.on ^js recorder "meter" on-meter)) + (when on-ended + (.on ^js recorder "ended" on-ended)) + recorder)) + +(defn new-player + [audio options on-ended] + (let [player (new ^js Player + audio + (clj->js options))] + (when on-ended + (.on ^js player "ended" on-ended)) + player)) + +(defn prepare-player + [player on-prepared on-error] + (when (and player (.-canPrepare ^js player)) + (.prepare ^js player + (fn [^js err] + (if err + (on-error {:error (.-err err) :message (.-message err)}) + (on-prepared)))))) + +(defn start-recording + [recorder on-start on-error] + (when (and recorder + (or + (.-canRecord ^js recorder) + (.-canPrepare ^js recorder))) + (.record ^js recorder + (fn [^js err] + (if err + (on-error {:error (.-err err) :message (.-message err)}) + (on-start)))))) + +(defn stop-recording + [recorder on-stop on-error] + (if (and recorder (#{RECORDING PAUSED} (get-state recorder))) + (.stop ^js recorder + (fn [^js err] + (if err + (on-error {:error (.-err err) :message (.-message err)}) + (on-stop)))) + (on-stop))) + +(defn stop-playing + [player on-stop on-error] + (if (and player (.-isPlaying ^js player)) + (.stop ^js player + (fn [^js err] + (if err + (on-error {:error (.-err err) :message (.-message err)}) + (on-stop)))) + (on-stop))) + +(defn get-recorder-file-path + [recorder] + (when recorder + (.-fsPath ^js recorder))) + +(defn get-player-duration + [player] + (when (and player (.-canPlay ^js player)) + (.-duration ^js player))) + +(defn get-player-current-time + [player] + (when (and player (.-canPlay ^js player)) + (.-currentTime ^js player))) + +(defn set-player-wake-lock + [player wake-lock?] + (when player + (set! (.-wakeLock player) wake-lock?))) + +(defn toggle-playpause-player + [player on-play on-pause on-error] + (when (and player (.-canPlay ^js player)) + (.playPause ^js player + (fn [^js error pause?] + (if error + (on-error {:error (.-err error) :message (.-message error)}) + (if pause? + (on-pause) + (on-play))))))) + +(defn seek-player + [player value on-seek on-error] + (when (and player (.-canPlay ^js player)) + (.seek ^js player + value + (fn [^js err] + (if err + (on-error {:error (.-err err) :message (.-message err)}) + (on-seek)))))) + +(defn destroy-recorder + [recorder] + (stop-recording recorder + #(when (and recorder (.-destroy ^js recorder) (not= (get-state recorder) DESTROYED)) + (.destroy ^js recorder)) + #())) + +(defn destroy-player + [player] + (stop-playing player + #(when (and player (.-destroy ^js player) (not= (get-state player) IDLE)) + (.destroy ^js player)) + #())) diff --git a/src/react_native/background_timer.cljs b/src/react_native/background_timer.cljs new file mode 100644 index 00000000000..736d1d9a7ab --- /dev/null +++ b/src/react_native/background_timer.cljs @@ -0,0 +1,11 @@ +(ns react-native.background-timer + (:require + ["react-native-background-timer" :default background-timer])) + +(defn set-timeout + [cb ms] + (.setTimeout background-timer cb ms)) + +(defn clear-timeout + [id] + (.clearTimeout background-timer id)) diff --git a/src/react_native/biometrics.cljs b/src/react_native/biometrics.cljs new file mode 100644 index 00000000000..189dc89ca89 --- /dev/null +++ b/src/react_native/biometrics.cljs @@ -0,0 +1,79 @@ +(ns react-native.biometrics + (:require + ["react-native-biometrics" :default Biometrics] + [clojure.string :as string] + [oops.core :as oops] + [react-native.platform :as platform] + [schema.core :as schema])) + +(def ^:private ^:const android-not-available-error-message "Biometric hardware unavailable") +(def ^:private ^:const android-not-enrolled-error-message "No fingerprints enrolled.") +(def ^:private ^:const android-too-many-attempts-error-message + "Too many attempts. Use screen lock instead.") +(def ^:private ^:const ios-not-enrolled-error-message "No identities are enrolled") + +(defn get-supported-type + "Returns a JS promise that resolves with the biometrics types supported by the + device, regardless of whether it's enabled or disabled. + + Resolved values: `:Biometrics` `:FaceID` `:TouchID`" + [] + (-> (Biometrics.) + (.isSensorAvailable) + (.then (fn [result] + (-> result + (oops/oget :biometryType) + keyword))) + (.catch (constantly nil)))) + +(defn get-available + "Returns a JS promise that resolves to a boolean, which signifies whether + biometrics is enabled/disabled on the device." + [] + (-> (Biometrics.) + (.isSensorAvailable) + (.then (fn [result] + (oops/oget result :available))))) + +;; NOTE: the react-native-biometrics package error codes/messages differ across platforms. +;; On android we get error messages while on iOS it's a stringified Obj-C error object. +(defn- convert-auth-error-message + [message] + (let [cause (if platform/android? + (condp = message + android-not-enrolled-error-message :biometrics/fingerprints-not-enrolled-error + android-not-available-error-message :biometrics/not-available-error + android-too-many-attempts-error-message :biometric/too-many-attempts + :biometrics/unknown-error) + + (condp #(string/includes? %2 %1) message + ios-not-enrolled-error-message :biometrics/ios-not-enrolled-error + :biometrics/unknown-error))] + (ex-info "Failed to authenticate with biometrics" + {:orig-error-message message} + cause))) + +(defn authenticate + "Returns a JS promise that resolves with a boolean auth success state: `true` for + success and `false` when canceled by user." + [{:keys [prompt-message fallback-prompt-message cancel-button-text]}] + (-> (Biometrics.) + (.simplePrompt #js + {"promptMessage" prompt-message + "fallbackPromptMessage" fallback-prompt-message + "cancelButtonText" cancel-button-text}) + (.then (fn [result] + (oops/oget result :success))) + (.catch (fn [error] + (-> (.-message error) + convert-auth-error-message + throw))))) + +(schema/=> authenticate + [:=> + [:cat + [:map {:closed true} + [:prompt-message string?] + [:fallback-prompt-message {:optional true} string?] + [:cancel-button-text {:optional true} string?]]] + :any]) diff --git a/src/react_native/blob.cljs b/src/react_native/blob.cljs new file mode 100644 index 00000000000..e4846d4823a --- /dev/null +++ b/src/react_native/blob.cljs @@ -0,0 +1,10 @@ +(ns react-native.blob + (:require ["react-native-blob-util" :default ReactNativeBlobUtil] + [taoensso.timbre :as log])) + +(defn fetch + [base64-uri config on-success] + (-> (.config ReactNativeBlobUtil (clj->js config)) + (.fetch "GET" base64-uri) + (.then #(on-success (.path %))) + (.catch #(log/error "could not download uri" {:error %})))) diff --git a/src/react_native/blur.cljs b/src/react_native/blur.cljs new file mode 100644 index 00000000000..e69f08fa993 --- /dev/null +++ b/src/react_native/blur.cljs @@ -0,0 +1,10 @@ +(ns react-native.blur + (:require + ["@react-native-community/blur" :as blur] + [react-native.core :as rn] + [reagent.core :as reagent])) + +(def view + (if blur + (reagent/adapt-react-class (.-BlurView blur)) + rn/view)) diff --git a/src/react_native/camera_kit.cljs b/src/react_native/camera_kit.cljs new file mode 100644 index 00000000000..c8a7dd5db68 --- /dev/null +++ b/src/react_native/camera_kit.cljs @@ -0,0 +1,17 @@ +(ns react-native.camera-kit + (:require + ["react-native-camera-kit" :refer (Camera CameraType)] + [oops.core :as oops] + [reagent.core :as reagent] + [taoensso.timbre :as log])) + +(def camera (reagent/adapt-react-class Camera)) + +(def camera-type-front (.-Front CameraType)) +(def camera-type-back (.-Back CameraType)) + +(defn capture + [^js camera-ref on-success] + (-> (.capture camera-ref) + (.then #(on-success (oops/oget % :uri))) + (.catch #(log/warn "couldn't capture photo" {:error %})))) diff --git a/src/react_native/cameraroll.cljs b/src/react_native/cameraroll.cljs new file mode 100644 index 00000000000..2b4f92ae517 --- /dev/null +++ b/src/react_native/cameraroll.cljs @@ -0,0 +1,30 @@ +(ns react-native.cameraroll + (:require + ["@react-native-camera-roll/camera-roll" :refer [CameraRoll]] + [react-native.fs :as fs] + [taoensso.timbre :as log] + [utils.transforms :as transforms])) + +(defn get-photos + [opts callback] + (-> (.getPhotos CameraRoll (clj->js opts)) + (.then #(callback (transforms/js->clj %))) + (.catch #(log/warn "could not get camera roll photos" %)))) + +(defn get-albums + [opts callback] + (-> (.getAlbums CameraRoll (clj->js opts)) + (.then #(callback (transforms/js->clj %))) + (.catch #(log/warn "could not get camera roll albums" %)))) + +(defn save-image + [path] + (-> (.save CameraRoll (clj->js path)) + (.then #(fs/unlink path)) + (.catch #(fs/unlink path)))) + +(defn get-photos-count-ios + [cb] + (-> (.getPhotosCountiOS CameraRoll) + (.then #(cb %)) + (.catch #(js/console.error %)))) diff --git a/src/react_native/charts.cljs b/src/react_native/charts.cljs new file mode 100644 index 00000000000..b53928a5509 --- /dev/null +++ b/src/react_native/charts.cljs @@ -0,0 +1,6 @@ +(ns react-native.charts + (:require + ["react-native-gifted-charts" :as gifted-charts] + [reagent.core :as reagent])) + +(def line-chart (reagent/adapt-react-class (.-LineChart gifted-charts))) diff --git a/src/react_native/clipboard.cljs b/src/react_native/clipboard.cljs new file mode 100644 index 00000000000..e5f9675c40a --- /dev/null +++ b/src/react_native/clipboard.cljs @@ -0,0 +1,11 @@ +(ns react-native.clipboard + (:require + ["@react-native-clipboard/clipboard" :default Clipboard])) + +(defn set-string + [text] + (.setString ^js Clipboard text)) + +(defn get-string + [callback] + (.then (.getString ^js Clipboard) #(callback %))) diff --git a/src/react_native/config.cljs b/src/react_native/config.cljs new file mode 100644 index 00000000000..e2653f33b06 --- /dev/null +++ b/src/react_native/config.cljs @@ -0,0 +1,9 @@ +(ns react-native.config + (:require + ["react-native-config" :default react-native-config])) + +(def config (js->clj react-native-config :keywordize-keys true)) + +(defn get-config + ([k] (get config k)) + ([k not-found] (get config k not-found))) diff --git a/src/react_native/core.cljs b/src/react_native/core.cljs new file mode 100644 index 00000000000..6e5a7752c43 --- /dev/null +++ b/src/react_native/core.cljs @@ -0,0 +1,215 @@ +(ns react-native.core + (:require + ["react" :as react] + ["react-native" :as react-native] + [promesa.core :as promesa] + [react-native.flat-list :as flat-list] + [react-native.platform :as platform] + [react-native.section-list :as section-list] + [react-native.utils :as utils] + [reagent.core :as reagent])) + +(def app-state ^js (.-AppState ^js react-native)) + +;; Only use this component for exceptional cases, otherwise use Reanimated, e.g. +;; when using interpolated values exposed by RN Gesture Handler > Swipeable. +(def animated-view (reagent/adapt-react-class react-native/Animated.View)) + +(def view (reagent/adapt-react-class (.-View ^js react-native))) +(def scroll-view (reagent/adapt-react-class (.-ScrollView ^js react-native))) +(def safe-area-view (reagent/adapt-react-class (.-SafeAreaView ^js react-native))) + +(def ^:private image-native + (reagent/adapt-react-class (.-Image ^js react-native))) + +(defn image + [{:keys [source] :as props}] + (let [props (cond-> props + platform/ios? + (dissoc :resize-method) + (and (:style props) platform/ios?) + (update :style dissoc :resize-method))] + [image-native + (if (string? source) + (assoc props :source {:uri source}) + props)])) + +(defn image-get-size + [uri] + (promesa/create (fn [res rej] + (.getSize ^js (.-Image ^js react-native) + uri + (fn [width height] (res [width height])) + rej)))) +(def text (reagent/adapt-react-class (.-Text ^js react-native))) +(def text-input (reagent/adapt-react-class (.-TextInput ^js react-native))) + +(def pressable-class (reagent/adapt-react-class (.-Pressable ^js react-native))) +(def touchable-opacity-class (reagent/adapt-react-class (.-TouchableOpacity ^js react-native))) +(def touchable-highlight-class (reagent/adapt-react-class (.-TouchableHighlight ^js react-native))) +(def touchable-without-feedback-class + (reagent/adapt-react-class (.-TouchableWithoutFeedback ^js react-native))) + +(defn pressable + [props & children] + (into [pressable-class (utils/custom-pressable-props props)] children)) + +(defn touchable-opacity + [props & children] + (into [touchable-opacity-class (utils/custom-pressable-props props)] children)) + +(defn touchable-highlight + [props & children] + (into [touchable-highlight-class (utils/custom-pressable-props props)] children)) + +(defn touchable-without-feedback + {:deprecated "pressable should be used instead"} + [props child] + [touchable-without-feedback-class (utils/custom-pressable-props props) child]) + +(def flat-list flat-list/flat-list) + +(def section-list section-list/section-list) + +(def activity-indicator (reagent/adapt-react-class (.-ActivityIndicator ^js react-native))) + +(def modal (reagent/adapt-react-class (.-Modal ^js react-native))) +(def refresh-control (reagent/adapt-react-class (.-RefreshControl ^js react-native))) + +(def keyboard ^js (.-Keyboard ^js react-native)) + +(def dismiss-keyboard! #(.dismiss keyboard)) + +(defn hide-splash-screen + [] + (.hide ^js (-> react-native .-NativeModules .-SplashScreen))) + +(defn alert + [title message buttons options] + (.alert (.-Alert ^js react-native) title message (clj->js buttons) (clj->js options))) + +(def appearance ^js (.-Appearance ^js react-native)) + +(defn get-color-scheme + [] + (.getColorScheme appearance)) + +(defn appearance-add-change-listener + [handler] + (.addChangeListener appearance handler)) + +(def get-window + (memoize + (fn [] + (js->clj (.get (.-Dimensions ^js react-native) "window") :keywordize-keys true)))) + +(def get-screen + (memoize + (fn [] + (js->clj (.get (.-Dimensions ^js react-native) "screen") :keywordize-keys true)))) + +(def small-screen? + (let [height (:height (get-screen))] + (< height 700))) + +(defn hw-back-add-listener + [callback] + (.addEventListener (.-BackHandler ^js react-native) "hardwareBackPress" callback)) + +(defn hw-back-remove-listener + [callback] + (.removeEventListener (.-BackHandler ^js react-native) "hardwareBackPress" callback)) + +(def keyboard-avoiding-view-class (reagent/adapt-react-class (.-KeyboardAvoidingView react-native))) + +(defn keyboard-avoiding-view + [props & children] + (into [keyboard-avoiding-view-class + (merge (when platform/ios? {:behavior :padding}) + props)] + children)) + +(def memo react/memo) + +(def use-state react/useState) + +(def use-ref react/useRef) + +(def use-context react/useContext) + +(defn use-ref-atom + [value] + (let [ref (use-ref (atom value))] + (.-current ^js ref))) + +(defn get-js-deps + [deps] + (if deps + (if (empty? deps) + #js [true] + (let [prev-state (use-ref-atom {:value false :deps nil}) + prev-deps (:deps @prev-state) + prev-value (:value @prev-state)] + (if (and (not (nil? prev-deps)) (not= (count deps) (count prev-deps))) + (throw (js/Error. "Hooks can't have a different number of dependencies across re-renders")) + (if (not= deps prev-deps) + (let [new-value (not prev-value)] + (reset! prev-state {:value new-value + :deps deps}) + #js [new-value]) + #js [prev-value])))) + js/undefined)) + +(defn use-effect + ([handler] + (use-effect handler nil)) + ([handler deps] + (react/useEffect + #(let [ret (handler)] (if (fn? ret) ret js/undefined)) + (get-js-deps deps)))) + +(defn use-mount + [handler] + (use-effect handler [])) + +(defn use-unmount + [handler] + (use-mount (fn [] handler))) + +(defn use-callback + ([handler] + (use-callback handler [])) + ([handler deps] + (react/useCallback handler (get-js-deps deps)))) + +(defn use-memo + [handler deps] + (react/useMemo handler (get-js-deps deps))) + +(defn delay-render + [content] + (let [[render? set-render] (use-state false)] + (use-mount + (fn [] + (js/setTimeout #(set-render true) 0))) + (when render? + content))) + +(def layout-animation (.-LayoutAnimation ^js react-native)) +(def configure-next (.-configureNext ^js layout-animation)) + +(def layout-animation-presets + {:ease-in-ease-out (-> ^js layout-animation .-Presets .-easeInEaseOut) + :linear (-> ^js layout-animation .-Presets .-linear) + :spring (-> ^js layout-animation .-Presets .-spring)}) + +(defn selectable-text-input-manager + [] + (when (exists? (.-NativeModules ^js react-native)) + (.-RNSelectableTextInputManager ^js (.-NativeModules ^js react-native)))) + +(def linking (.-Linking react-native)) + +(defn open-url [link] (.openURL ^js linking link)) + +(def stylesheet-absolute-fill ^js (.. react-native -StyleSheet -absoluteFill)) diff --git a/src/react_native/draggable_flatlist.cljs b/src/react_native/draggable_flatlist.cljs new file mode 100644 index 00000000000..d18cc25b2fb --- /dev/null +++ b/src/react_native/draggable_flatlist.cljs @@ -0,0 +1,11 @@ +(ns react-native.draggable-flatlist + (:require + ["react-native-draggable-flatlist" :default DraggableFlatList] + [react-native.flat-list :as rn-flat-list] + [reagent.core :as reagent])) + +(def rn-draggable-flatlist (reagent/adapt-react-class DraggableFlatList)) + +(defn draggable-flatlist + [props] + [rn-draggable-flatlist (rn-flat-list/base-list-props props)]) diff --git a/src/react_native/fast_image.cljs b/src/react_native/fast_image.cljs new file mode 100644 index 00000000000..e8481a2b218 --- /dev/null +++ b/src/react_native/fast_image.cljs @@ -0,0 +1,79 @@ +(ns react-native.fast-image + (:require + ["react-native-fast-image" :as FastImage] + [clojure.string :as string] + [react-native.core :as rn] + [reagent.core :as reagent] + [utils.transforms :as transforms])) + +(defn- build-source + [source] + (if (string? source) + {:uri source + :priority :high} + source)) + +(defn- remove-port + [source] + (cond + (string? source) (string/replace-first source #":\d+" "") + (:uri source) (some-> source + :uri + (string/replace-first #":\d+" "")) + :else source)) + +(defn- placeholder + [{:keys [style fallback-content error? loaded?]}] + [rn/view + {:style (assoc style + :flex 1 + :justify-content :center + :align-items :center)} + (cond + (and error? fallback-content) fallback-content + error? [rn/text "X"] + (not loaded?) [rn/activity-indicator {:animating true}])]) + +;; We cannot use hooks since `reactify-component` seems to ignore the functional compiler +(defn- internal-fast-image + [_] + (let [loaded? (reagent/atom false) + error? (reagent/atom false) + on-image-error (fn [event on-error] + (when (fn? on-error) (on-error event)) + (reset! error? true)) + on-image-loaded (fn [event on-load] + (when (fn? on-load) (on-load event)) + (reset! loaded? true) + (reset! error? false))] + (fn [{:keys [source fallback-content on-error on-load] :as props}] + [:> FastImage + (assoc props + :source (build-source source) + :on-error #(on-image-error % on-error) + :on-load #(on-image-loaded % on-load)) + (when (or @error? (not @loaded?)) + [placeholder + {:style (js->clj (:style props)) + :fallback-content fallback-content + :error? @error? + :loaded? @loaded?}])]))) + +(defn- compare-props + [old-props new-props] + (let [old-props-clj (transforms/js->clj old-props) + new-props-clj (transforms/js->clj new-props) + old-source (some-> old-props-clj + :source + remove-port) + new-source (some-> new-props-clj + :source + remove-port)] + (and (= old-source new-source) + (= (dissoc old-props-clj :source) (dissoc new-props-clj :source))))) + +(def fast-image + (-> internal-fast-image + (reagent/reactify-component) + (rn/memo compare-props) + (reagent/adapt-react-class))) diff --git a/src/react_native/firebase.cljc b/src/react_native/firebase.cljc new file mode 100644 index 00000000000..dbb612b3f4d --- /dev/null +++ b/src/react_native/firebase.cljc @@ -0,0 +1,18 @@ +(ns react-native.firebase + (:require + #?@(:fdroid + [[react-native.firebase.firebase-fdroid]] + :google + [[react-native.firebase.firebase-google]] + :cljs + [[react-native.firebase.firebase-unsupported]]))) + +#?(:fdroid + [(def request-remote-token react-native.firebase.firebase-fdroid/request-remote-token) + (def revoke-remote-token react-native.firebase.firebase-fdroid/revoke-remote-token)] + :google + [(def request-remote-token react-native.firebase.firebase-google/request-remote-token) + (def revoke-remote-token react-native.firebase.firebase-google/revoke-remote-token)] + :cljs + [(def request-remote-token react-native.firebase.firebase-unsupported/request-remote-token) + (def revoke-remote-token react-native.firebase.firebase-unsupported/revoke-remote-token)]) diff --git a/src/react_native/firebase/firebase_fdroid.cljs b/src/react_native/firebase/firebase_fdroid.cljs new file mode 100644 index 00000000000..37f2a3a64de --- /dev/null +++ b/src/react_native/firebase/firebase_fdroid.cljs @@ -0,0 +1,15 @@ +(ns react-native.firebase.firebase-fdroid + (:require + [promesa.core :as promesa])) + +(defn request-remote-token + [_options] + (promesa/rejected + (ex-info "Failed to register for FCM token" + {:description "Cannot register FCM token on FDroid"}))) + +(defn revoke-remote-token + [_options] + (promesa/rejected + (ex-info "Failed to revoke a FCM token" + {:description "Cannot revoke a FCM token on FDroid"}))) diff --git a/src/react_native/firebase/firebase_google.cljs b/src/react_native/firebase/firebase_google.cljs new file mode 100644 index 00000000000..d8fed59c3a2 --- /dev/null +++ b/src/react_native/firebase/firebase_google.cljs @@ -0,0 +1,13 @@ +(ns react-native.firebase.firebase-google + (:require + ["@react-native-firebase/messaging" :as firebase.messaging] + [promesa.core :as promesa])) + +(defn request-remote-token + [_options] + (-> (.registerDeviceForRemoteMessages (firebase.messaging/default)) + (promesa/then #(.getToken (firebase.messaging/default))))) + +(defn revoke-remote-token + [_options] + (.deleteToken (firebase.messaging/default))) diff --git a/src/react_native/firebase/firebase_unsupported.cljs b/src/react_native/firebase/firebase_unsupported.cljs new file mode 100644 index 00000000000..4152cf6c476 --- /dev/null +++ b/src/react_native/firebase/firebase_unsupported.cljs @@ -0,0 +1,15 @@ +(ns react-native.firebase.firebase-unsupported + (:require + [promesa.core :as promesa])) + +(defn request-remote-token + [_options] + (promesa/rejected + (ex-info "Failed to register for FCM token" + {:description "Cannot register FCM token without Firebase"}))) + +(defn revoke-remote-token + [_options] + (promesa/rejected + (ex-info "Failed to revoke a FCM token" + {:description "Cannot revoke a FCM token without Firebase"}))) diff --git a/src/react_native/flat_list.cljs b/src/react_native/flat_list.cljs new file mode 100644 index 00000000000..097d9ef76b3 --- /dev/null +++ b/src/react_native/flat_list.cljs @@ -0,0 +1,54 @@ +(ns react-native.flat-list + (:require + ["react-native" :as react-native] + [reagent.core :as reagent])) + +(def react-native-flat-list (reagent/adapt-react-class (.-FlatList ^js react-native))) + +(defn- wrap-render-fn + [f render-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 [^js data] + (f (.-from data) (.-to data) (.-data data)))) + +(defn- wrap-key-fn + [f] + (fn [data index] + (when f + (f data index)))) + +(defn dissoc-custom-props + [props] + (dissoc props + :data + :header + :footer + :empty-component + :separator + :render-fn + :key-fn + :on-drag-end-fn + :render-data)) + +(defn base-list-props + [{:keys [key-fn data render-fn empty-component header footer separator render-data on-drag-end-fn] + :as props}] + (cond-> {:data (to-array data)} + key-fn (assoc :keyExtractor (wrap-key-fn key-fn)) + render-fn (assoc :renderItem (wrap-render-fn render-fn render-data)) + separator (assoc :ItemSeparatorComponent (fn [] (reagent/as-element separator))) + empty-component (assoc :ListEmptyComponent (fn [] (reagent/as-element empty-component))) + header (assoc :ListHeaderComponent (reagent/as-element header)) + footer (assoc :ListFooterComponent (reagent/as-element footer)) + on-drag-end-fn (assoc :onDragEnd (wrap-on-drag-end-fn on-drag-end-fn)) + :always (merge (dissoc-custom-props props)))) + +(defn flat-list + [props] + [react-native-flat-list (base-list-props props)]) diff --git a/src/react_native/fs.cljs b/src/react_native/fs.cljs new file mode 100644 index 00000000000..81d2b11d780 --- /dev/null +++ b/src/react_native/fs.cljs @@ -0,0 +1,53 @@ +(ns react-native.fs + (:require + ["react-native-fs" :as react-native-fs])) + +(defn move-file + [src dst] + (.moveFile react-native-fs src dst)) + +(defn stat + [path on-stat on-error] + (-> (.stat react-native-fs path) + (.then on-stat) + (.catch on-error))) + +(defn read-file + [path encoding on-read on-error] + (-> (.readFile react-native-fs path encoding) + (.then on-read) + (.catch on-error))) + +(defn write-file + [path content encoding on-write on-error] + (-> (.writeFile react-native-fs path content encoding) + (.then on-write) + (.catch on-error))) + +(defn read-dir + [path] + (.readDir react-native-fs path)) + +(defn mkdir + [path] + (.mkdir react-native-fs path)) + +(defn unlink + [path] + (.unlink react-native-fs path)) + +(defn file-exists? + [path] + (.exists react-native-fs path)) + +(defn cache-dir + [] + (.-CachesDirectoryPath ^js react-native-fs)) + +(defn copy-assets + [src dest] + (.copyFileAssets ^js react-native-fs src dest)) + +(defn main-bundle-path + [] + (.-MainBundlePath ^js react-native-fs)) diff --git a/src/react_native/gesture.cljs b/src/react_native/gesture.cljs new file mode 100644 index 00000000000..69f660080c4 --- /dev/null +++ b/src/react_native/gesture.cljs @@ -0,0 +1,136 @@ +(ns react-native.gesture + (:require + ["react-native-gesture-handler" :refer + (Gesture + GestureDetector + Directions + RectButton + Swipeable + TouchableWithoutFeedback + gestureHandlerRootHOC + FlatList + ScrollView)] + [react-native.core :as rn] + [react-native.flat-list :as rn-flat-list] + [reagent.core :as reagent])) + +(def directions (js->clj Directions :keywordize-keys true)) + +(def gesture-detector (reagent/adapt-react-class GestureDetector)) + +(def gesture-handler-root-hoc gestureHandlerRootHOC) + +(defn gesture-tap [] (.Tap ^js Gesture)) + +(defn gesture-pan [] (.Pan ^js Gesture)) + +(defn gesture-native [] (.Native ^js Gesture)) + +(defn gesture-long-press [] (.LongPress ^js Gesture)) + +(defn gesture-pinch [] (.Pinch ^js Gesture)) + +(defn gesture-fling [] (.Fling ^js Gesture)) + +(defn on-begin [gesture handler] (.onBegin ^js gesture handler)) + +(defn on-start [gesture handler] (.onStart ^js gesture handler)) + +(defn on-update [gesture handler] (.onUpdate ^js gesture handler)) + +(defn on-end [gesture handler] (.onEnd ^js gesture handler)) + +(defn on-finalize [gesture handler] (.onFinalize ^js gesture handler)) + +(defn max-pointers [gesture amount] (.maxPointers ^js gesture amount)) + +(defn min-distance [gesture dist] (.minDistance ^js gesture dist)) + +(defn number-of-taps [gesture amount] (.numberOfTaps ^js gesture amount)) + +(defn enabled [gesture enabled?] (.enabled ^js gesture enabled?)) + +(defn average-touches [gesture average-touches?] (.averageTouches ^js gesture average-touches?)) + +(defn with-test-ID [gesture test-ID] (.withTestId ^js gesture (str test-ID))) + +(defn simultaneous + ([g1 g2] (.Simultaneous ^js Gesture g1 g2)) + ([g1 g2 g3] (.Simultaneous ^js Gesture g1 g2 g3))) + +(defn exclusive [g1 g2] (.Exclusive ^js Gesture g1 g2)) + +;; RN Gesture Handler touchables are drop-in replacements for the RN ones. In +;; some cases, it's the only touchable that works with Swipeable components. +(def touchable-without-feedback (reagent/adapt-react-class TouchableWithoutFeedback)) + +(def rect-button (reagent/adapt-react-class RectButton)) + +(def ^:private swipeable-component + (reagent/adapt-react-class Swipeable)) + +(defn swipeable + [{:keys [render-left-actions render-right-actions] :as props} & children] + (into [swipeable-component + (cond-> props + render-left-actions + (assoc :render-left-actions + (fn [& args] + (reagent/as-element (apply render-left-actions args)))) + + render-right-actions + (assoc :render-right-actions + (fn [& args] + (reagent/as-element (apply render-right-actions args)))))] + children)) + +(def gesture-flat-list (reagent/adapt-react-class FlatList)) + +(defn flat-list + [props] + [gesture-flat-list (rn-flat-list/base-list-props props)]) + +(def scroll-view (reagent/adapt-react-class ScrollView)) + +;;; Custom gesture section-list +(defn- flatten-sections + [sections] + (mapcat (fn [{:keys [title data]}] + (into [{:title title :header? true}] data)) + sections)) + +(defn- find-sticky-indices + [data] + (->> data + (map-indexed (fn [index item] (when (:header? item) index))) + (remove nil?) + (vec))) + +(defn- is-last-item-in-section + [data index] + (let [next-item (nth data (inc index) nil)] + (or (nil? next-item) (:header? next-item)))) + +(defn section-list + [{:keys [sections sticky-section-headers-enabled render-section-header-fn render-section-footer-fn + render-fn] + :or {sticky-section-headers-enabled true} + :as props}] + (let [data (flatten-sections sections) + render-item (rn/use-callback + (fn [p1 p2 p3 p4] + [:<> + (if (:header? p1) + [render-section-header-fn p1 p2 p3 p4] + [render-fn p1 p2 p3 p4]) + (when (and render-section-footer-fn + (is-last-item-in-section data p2)) + [render-section-footer-fn p1 p2 p3 p4])]) + [render-fn render-section-header-fn render-section-footer-fn])] + [flat-list + (merge props + {:data data + :render-fn render-item + :sticky-header-indices + (when sticky-section-headers-enabled + (find-sticky-indices data))})])) diff --git a/src/react_native/hole_view.cljs b/src/react_native/hole_view.cljs new file mode 100644 index 00000000000..0bc51e97b0f --- /dev/null +++ b/src/react_native/hole_view.cljs @@ -0,0 +1,6 @@ +(ns react-native.hole-view + (:require + ["react-native-hole-view" :refer (RNHoleView)] + [reagent.core :as reagent])) + +(def hole-view (reagent/adapt-react-class RNHoleView)) diff --git a/src/react_native/hooks.cljs b/src/react_native/hooks.cljs new file mode 100644 index 00000000000..960bd3b3afc --- /dev/null +++ b/src/react_native/hooks.cljs @@ -0,0 +1,59 @@ +(ns react-native.hooks + (:require + ["@react-native-community/hooks" :as hooks] + [oops.core :as oops] + [react-native.core :as rn])) + +(defn- add-keyboard-listener + [listener callback] + (oops/ocall rn/keyboard "addListener" listener callback)) + +(defn use-keyboard + [] + (let [[keyboard-height set-keyboard-height] (rn/use-state 0) + [did-show? set-did-show] (rn/use-state false) + [will-show? set-will-show] (rn/use-state false)] + (rn/use-mount + (fn [] + (let [will-show-listener (add-keyboard-listener "keyboardWillShow" #(set-will-show true)) + did-show-listener (add-keyboard-listener "keyboardDidShow" + (fn [e] + (set-did-show true) + (set-keyboard-height + (oops/oget e "endCoordinates.height")))) + will-hide-listener (add-keyboard-listener "keyboardWillHide" #(set-will-show false)) + did-hide-listener (add-keyboard-listener "keyboardDidHide" + (fn [e] + (set-did-show false) + (when e + (oops/oget e "endCoordinates.height"))))] + (fn [] + (oops/ocall will-show-listener "remove") + (oops/ocall did-show-listener "remove") + (oops/ocall will-hide-listener "remove") + (oops/ocall did-hide-listener "remove"))))) + {:keyboard-shown did-show? + :keyboard-will-show? will-show? + :keyboard-height keyboard-height})) + +(defn use-back-handler + [handler] + (.useBackHandler hooks handler)) + +(defn use-interval + [cb cleanup-cb delay-ms] + (let [saved-callback (rn/use-ref)] + (rn/use-effect + (fn [] + (oops/oset! saved-callback "current" cb)) + [cb]) + + (rn/use-effect + (fn [] + (let [tick (oops/oget saved-callback "current")] + (when delay-ms + (let [id (js/setInterval tick delay-ms)] + (fn [] + (cleanup-cb) + (js/clearInterval id)))))) + [delay-ms]))) diff --git a/src/react_native/image_crop_picker.cljs b/src/react_native/image_crop_picker.cljs new file mode 100644 index 00000000000..31cf65b78d6 --- /dev/null +++ b/src/react_native/image_crop_picker.cljs @@ -0,0 +1,30 @@ +(ns react-native.image-crop-picker + (:require ["react-native-image-crop-picker" :default image-picker])) + +(defn show-access-error + [o] + (js/console.log (.-message ^js o))) + +(defn show-image-picker + ([callback] + (show-image-picker callback nil nil)) + ([callback + {:keys [media-type] + :or {media-type "any"} + :as props} + finally-callback] + (-> ^js image-picker + (.openPicker (clj->js (merge {:mediaType media-type} props))) + (.then #(callback (.-path ^js %))) + (.catch show-access-error) + (.finally finally-callback)))) + +(defn show-image-picker-camera + ([callback] + (show-image-picker-camera callback nil nil)) + ([callback props finally-callback] + (-> ^js image-picker + (.openCamera (clj->js props)) + (.then #(callback (.-path ^js %))) + (.catch show-access-error) + (.finally finally-callback)))) diff --git a/src/react_native/image_resizer.cljs b/src/react_native/image_resizer.cljs new file mode 100644 index 00000000000..4c4a96ffe25 --- /dev/null +++ b/src/react_native/image_resizer.cljs @@ -0,0 +1,7 @@ +(ns react-native.image-resizer + (:require + ["react-native-image-resizer" :default image-resizer])) + +(defn resize + [{:keys [path max-width max-height quality]}] + (.createResizedImage image-resizer path max-width max-height "JPEG" quality 0 nil false)) diff --git a/src/react_native/keychain.cljs b/src/react_native/keychain.cljs new file mode 100644 index 00000000000..221910bba3a --- /dev/null +++ b/src/react_native/keychain.cljs @@ -0,0 +1,83 @@ +(ns react-native.keychain + (:require + ["react-native-keychain" :as react-native-keychain] + [clojure.string :as string] + [taoensso.timbre :as log])) + +;; ******************************************************************************** +;; Storing / Retrieving a user password to/from Keychain +;; ******************************************************************************** +;; +;; We are using set/get/reset internet credentials there because they are bound +;; to an address (`server`) property. + +(defn enum-val + [enum-name value-name] + (get-in (js->clj ^js react-native-keychain) [enum-name value-name])) + +;; We need a more strict access mode for keychain entries that save user password. +;; iOS +;; see this article for more details: +;; https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility?language=objc +(def keychain-restricted-availability + ;; From Apple's documentation: > The kSecAttrAccessible attribute enables you to control item + ;; availability > relative to the lock state of the device. > It also lets you specify eligibility + ;; for restoration to a new device. > If the attribute ends with the string ThisDeviceOnly, > the + ;; item can be restored to the same device that created a backup, > but it isn’t migrated when + ;; restoring another device’s backup data. > ... > For extremely sensitive data > THAT YOU NEVER + ;; WANT STORED IN iCloud, > you might choose kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly. That + ;; is exactly what we use there. Note that the password won't be stored if the device isn't locked + ;; by a passcode. + #js {:accessible (enum-val "ACCESSIBLE" "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY")}) + +(def keychain-secure-hardware + ;; (Android) Requires storing the encryption key for the entry in secure hardware + ;; or StrongBox (see + ;; https://developer.android.com/training/articles/keystore#ExtractionPrevention) + #js {:accessible (enum-val "SECURITY_LEVEL" "SECURE_HARDWARE")}) + +;; Android only +(defn secure-hardware-available? + [callback] + (-> (.getSecurityLevel ^js react-native-keychain) + (.then (fn [level] + (callback (= level "SECURE_HARDWARE")))))) + +;; iOS only +(defn device-encrypted? + [callback] + (-> (.canImplyAuthentication + ^js react-native-keychain + (clj->js + {:authenticationType + (enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")})) + (.then callback))) + +(defn save-credentials + "Stores the credentials for the address to the Keychain" + ([server username password] + (save-credentials server username password identity)) + ([server username password callback] + (when-not (empty? server) + (-> (.setInternetCredentials ^js react-native-keychain + (string/lower-case server) + username + password + keychain-secure-hardware + keychain-restricted-availability) + (.then callback))))) + +(defn get-credentials + "Gets the credentials for a specified server from the Keychain" + ([server] + (get-credentials server identity)) + ([server callback] + (when-not (empty? server) + (-> (.getInternetCredentials ^js react-native-keychain (string/lower-case server)) + (.then callback))))) + +(defn reset-credentials + [server] + (when-not (empty? server) + (-> (.resetInternetCredentials ^js react-native-keychain (string/lower-case server)) + (.then #(when-not % (log/error "Error while clearing saved password.")))))) diff --git a/src/react_native/linear_gradient.cljs b/src/react_native/linear_gradient.cljs new file mode 100644 index 00000000000..e454b6b577a --- /dev/null +++ b/src/react_native/linear_gradient.cljs @@ -0,0 +1,39 @@ +(ns react-native.linear-gradient + (:require + ["react-native-linear-gradient" :default LinearGradient] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [taoensso.timbre :as log])) + +(def ^:private linear-gradient* (reagent/adapt-react-class LinearGradient)) + +(defn- split-valid-colors + [acc idx color] + (let [color? (colors/valid-color? color)] + (cond-> acc + :always (update :safe-colors conj (if color? color "transparent")) + (not color?) (update :wrong-colors conj [idx color])))) + +(defn- wrong-colors-str + [colors] + (reduce-kv (fn [s idx color] + (str s "Index: " idx ", color: " (prn-str color))) + "Invalid color values in vector passed to Linear Gradient:\n" + colors)) + +(defn linear-gradient + [props & children] + (when ^boolean js/goog.DEBUG + (assert (vector? (:colors props)))) + (let [{:keys [wrong-colors safe-colors]} (rn/use-memo + (fn [] + (reduce-kv split-valid-colors + {:safe-colors [] + :wrong-colors {}} + (:colors props))) + [(:colors props)])] + (when (seq wrong-colors) + (log/error (wrong-colors-str wrong-colors))) + (into [linear-gradient* (assoc props :colors safe-colors)] + children))) diff --git a/src/react_native/masked_view.cljs b/src/react_native/masked_view.cljs new file mode 100644 index 00000000000..801267f0609 --- /dev/null +++ b/src/react_native/masked_view.cljs @@ -0,0 +1,6 @@ +(ns react-native.masked-view + (:require + ["@react-native-community/masked-view" :default MaskedView] + [reagent.core :as reagent])) + +(def masked-view (reagent/adapt-react-class MaskedView)) diff --git a/src/react_native/mmkv.cljs b/src/react_native/mmkv.cljs new file mode 100644 index 00000000000..5c476cc3ccf --- /dev/null +++ b/src/react_native/mmkv.cljs @@ -0,0 +1,103 @@ +(ns react-native.mmkv + (:require + ["react-native-mmkv" :refer [MMKV]] + [cognitect.transit :as transit] + [native-module.core :as native-module] + [oops.core :as oops] + [taoensso.timbre :as log]) + (:refer-clojure :exclude [set])) + +;; Create a single MMKV instance to be used throughout the app with the path from native code +(defonce ^:private storage + (let [mmkv-path (try + (native-module/get-mmkv-storage-path) + (catch :default _ + ;; Fallback for tests + "/tmp/test-mmkv"))] + (MMKV. #js {:id "mmkv.status" :path mmkv-path}))) + +;; Transit serialization/deserialization for ClojureScript data structures +(def ^:private reader (transit/reader :json)) +(def ^:private writer (transit/writer :json)) + +(defn- clj->transit + [o] + (transit/write writer o)) + +(defn- transit->clj + [o] + (try + (when o + (transit/read reader o)) + (catch :default e + (log/error "[mmkv] Error parsing transit data:" e) + nil))) + +;; Basic MMKV operations +(defn set + "Store a value in MMKV." + [k v] + (oops/ocall storage "set" k v)) + +(defn get-string + "Get a string value from MMKV" + ([k] + (get-string k nil)) + ([k default-value] + (if-let [v (oops/ocall storage "getString" k)] + v + default-value))) + +(defn get-boolean + "Get a boolean value from MMKV" + ([k] + (get-boolean k false)) + ([k default-value] + (let [v (oops/ocall storage "getBoolean" k)] + (if (nil? v) + default-value + v)))) + +(defn get-number + "Get a number value from MMKV" + ([k] + (get-number k 0)) + ([k default-value] + (if-let [v (oops/ocall storage "getNumber" k)] + v + default-value))) + +(defn set-object + "Store a ClojureScript data structure in MMKV using transit serialization" + [k v] + (let [transit-str (clj->transit v)] + (set k transit-str))) + +(defn get-object + "Get a ClojureScript data structure from MMKV using transit deserialization" + ([k] + (get-object k nil)) + ([k default-value] + (if-let [transit-str (oops/ocall storage "getString" k)] + (or (transit->clj transit-str) default-value) + default-value))) + +(defn contains-key? + "Check if MMKV contains a key" + [k] + (oops/ocall storage "contains" k)) + +(defn delete-key + "Delete a key from MMKV" + [k] + (oops/ocall storage "delete" k)) + +(defn clear-all + "Clear all data from MMKV" + [] + (oops/ocall storage "clearAll")) + +(defn get-all-keys + "Get all keys stored in MMKV" + [] + (js->clj (oops/ocall storage "getAllKeys"))) diff --git a/src/react_native/navigation.cljs b/src/react_native/navigation.cljs new file mode 100644 index 00000000000..433fffa4b5a --- /dev/null +++ b/src/react_native/navigation.cljs @@ -0,0 +1,80 @@ +(ns react-native.navigation + (:refer-clojure :exclude [pop]) + (:require + ["react-native-navigation" :refer (Navigation)])) + +(defn set-default-options + [opts] + (.setDefaultOptions ^js Navigation (clj->js opts))) + +(defn register-component [arg1 arg2 arg3] (.registerComponent ^js Navigation arg1 arg2 arg3)) +(defn set-lazy-component-registrator [handler] (.setLazyComponentRegistrator ^js Navigation handler)) + +(defn set-root + [root] + (.setRoot ^js Navigation (clj->js root))) + +(defn push + [arg1 arg2] + (.push ^js Navigation arg1 (clj->js arg2))) + +(defn pop + [component] + (.pop ^js Navigation component)) + +(defn show-modal + [component] + (.showModal ^js Navigation (clj->js component))) + +(defn dismiss-modal + [component] + (.dismissModal ^js Navigation component)) + +(defn show-overlay + [component] + (.showOverlay Navigation (clj->js component))) + +(defn pop-to + [component] + (.popTo Navigation (clj->js component))) + +(defn pop-to-root + [tab] + (.popToRoot Navigation (clj->js tab))) + +(defn dissmiss-overlay + [component] + (.catch (.dismissOverlay Navigation component) #())) + +(defn reg-app-launched-listener + [handler] + (.registerAppLaunchedListener ^js (.events ^js Navigation) handler)) + +(defn reg-button-pressed-listener + [handler] + (.registerNavigationButtonPressedListener + (.events Navigation) + (fn [^js evn] + (handler (.-buttonId evn))))) + +(defn reg-modal-dismissed-listener + [handler] + (.registerModalDismissedListener ^js (.events ^js Navigation) handler)) + +(defn merge-options + [id opts] + (.mergeOptions Navigation id (clj->js opts))) + +(def constants (atom nil)) + +(defn status-bar-height + [] + (:status-bar-height @constants)) + +(.then (.constants Navigation) + (fn [^js consts] + (reset! constants {:top-bar-height (.-topBarHeight consts) + :bottom-tabs-height (.-bottomTabsHeight consts) + :status-bar-height (.-statusBarHeight consts)}))) + + diff --git a/src/react_native/net_info.cljs b/src/react_native/net_info.cljs new file mode 100644 index 00000000000..e8ab3faf745 --- /dev/null +++ b/src/react_native/net_info.cljs @@ -0,0 +1,8 @@ +(ns react-native.net-info + (:require + ["@react-native-community/netinfo" :default net-info])) + +(defn add-net-info-listener + [callback] + (when net-info + (.addEventListener ^js net-info callback))) diff --git a/src/react_native/orientation.cljs b/src/react_native/orientation.cljs new file mode 100644 index 00000000000..476b61b699c --- /dev/null +++ b/src/react_native/orientation.cljs @@ -0,0 +1,33 @@ +(ns react-native.orientation + (:require + ["react-native-orientation-locker" :default orientation :refer (useDeviceOrientationChange)] + [react-native.navigation :as navigation])) + +(def use-device-orientation-change useDeviceOrientationChange) + +(def get-auto-rotate-state (.-getAutoRotateState orientation)) + +(def portrait-options + (clj->js {:layout {:orientation ["portrait"]} + :statusBar {:visible true}})) + +(def landscape-option-1 (clj->js {:layout {:orientation ["landscape"]}})) +(def landscape-option-2 (clj->js {:statusBar {:visible false}})) + +(defn lock-to-portrait + [id] + (navigation/merge-options id portrait-options)) + +(defn lock-to-landscape + [id] + (navigation/merge-options id landscape-option-1) + ;; On Android, hiding the status-bar while changing orientation causes a flicker, so we enqueue it + (js/setTimeout #(navigation/merge-options id landscape-option-2) 0)) + +(def ^:const portrait "PORTRAIT") + +(def ^:const landscape "LANDSCAPE") + +(def ^:const landscape-left "LANDSCAPE-LEFT") + +(def ^:const landscape-right "LANDSCAPE-RIGHT") diff --git a/src/react_native/pdf_viewer.cljs b/src/react_native/pdf_viewer.cljs new file mode 100644 index 00000000000..ec70d62c00b --- /dev/null +++ b/src/react_native/pdf_viewer.cljs @@ -0,0 +1,6 @@ +(ns react-native.pdf-viewer + (:require + ["react-native-pdf" :default rn-pdf] + [reagent.core :as reagent])) + +(def view (reagent/adapt-react-class rn-pdf)) diff --git a/src/react_native/permissions.cljs b/src/react_native/permissions.cljs new file mode 100644 index 00000000000..f7f03b60790 --- /dev/null +++ b/src/react_native/permissions.cljs @@ -0,0 +1,101 @@ +(ns react-native.permissions + (:require + ["react-native-permissions" :refer + [check checkNotifications openSettings PERMISSIONS requestMultiple + requestNotifications RESULTS]] + [clojure.string :as string] + [promesa.core :as promesa] + [react-native.platform :as platform] + [taoensso.timbre :as log])) + +(def permissions-map + {:read-external-storage (cond + platform/android? (.-READ_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS))) + :write-external-storage (cond + platform/low-device? (.-WRITE_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS))) + :read-media-images (cond + platform/android? (.-READ_MEDIA_IMAGES (.-ANDROID PERMISSIONS))) + :post-notifications (cond + platform/android? (.-POST_NOTIFICATIONS (.-ANDROID PERMISSIONS))) + :camera (cond + platform/android? (.-CAMERA (.-ANDROID PERMISSIONS)) + platform/ios? (.-CAMERA (.-IOS PERMISSIONS))) + :record-audio (cond + platform/android? (.-RECORD_AUDIO (.-ANDROID PERMISSIONS)) + platform/ios? (.-MICROPHONE (.-IOS PERMISSIONS)))}) + +(defn all-granted? + [permissions] + (let [permission-vals (distinct (vals permissions))] + (and (= (count permission-vals) 1) + (not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} (first permission-vals)))))) + +(defn request-permissions + [{:keys [permissions on-allowed on-denied] + :or {on-allowed #() + on-denied #()}}] + (let [permissions (remove nil? (mapv #(get permissions-map %) permissions))] + (if (empty? permissions) + (on-allowed) + (-> (requestMultiple (clj->js permissions)) + (.then #(if (all-granted? (js->clj %)) + (on-allowed) + (on-denied))) + (.catch on-denied))))) + +(defn permission-granted? + [permission on-result on-error] + (-> (check (get permissions-map permission)) + (.then #(on-result (not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} %)))) + (.catch #(on-error %)))) + +(defn request-notifications + "`notification-options` is only used on iOS. + A map with `:status` and `settings` (only for iOS) is passed to the callbacks. + See https://github.com/zoontek/react-native-permissions?tab=readme-ov-file#requestnotifications." + [{:keys [notification-options on-allowed on-denied] + :or {notification-options #js ["alert"]}}] + (-> (requestNotifications notification-options) + (.then (fn [js-response] + (let [response (js->clj js-response :keywordize-keys true)] + (if (= (:status response) "granted") + (do + (on-allowed response) + (log/debug "Notification permission were allowed" response)) + (do + (on-denied response) + (log/debug "Notification permission were denied" response)))))))) + +(defn- format-permission-status + [raw-permission-status] + (-> raw-permission-status string/lower-case keyword)) + +(defn- notification-permissions->notification-permission-statuses + [raw-notification-permissions] + (let [notification-permissions (js->clj raw-notification-permissions + :keywordize-keys + true) + permission-status (-> notification-permissions + :status + format-permission-status)] + {:authorized? (= permission-status :granted) + :denied? (= permission-status :blocked) + :undetermined? (= permission-status :denied) + :provisional? (true? (-> notification-permissions + :settings + :provisional))})) + +(defn request-notification-permissions + [some-options] + (let [options (if (nil? some-options) [:alert] some-options)] + (-> (requestNotifications (clj->js options)) + (promesa/then notification-permissions->notification-permission-statuses)))) + +(defn check-notification-permissions + [] + (-> (checkNotifications) + (promesa/then notification-permissions->notification-permission-statuses))) + +(defn open-notification-settings + [] + (openSettings "notifications")) diff --git a/src/react_native/platform.cljs b/src/react_native/platform.cljs new file mode 100644 index 00000000000..47f34ad4479 --- /dev/null +++ b/src/react_native/platform.cljs @@ -0,0 +1,18 @@ +(ns react-native.platform + (:require + ["react-native" :as react-native])) + +(def platform (.-Platform ^js react-native)) + +(def os (when platform (.-OS ^js platform))) + +(def version (when platform (.-Version ^js platform))) + +(def android? (= os "android")) +(def ios? (= os "ios")) + +(def low-device? (and android? (< version 29))) + +(def is-below-android-13? (and android? (< version 33))) + +(def is-shadow-color-supported? (and android? (> version 27))) diff --git a/src/react_native/push_notification_ios.cljs b/src/react_native/push_notification_ios.cljs new file mode 100644 index 00000000000..93b2e5c7e8c --- /dev/null +++ b/src/react_native/push_notification_ios.cljs @@ -0,0 +1,84 @@ +(ns react-native.push-notification-ios + (:require + ["@react-native-community/push-notification-ios" :default pn-ios] + [promesa.core :as promesa] + [react-native.platform :as platform])) + +(defn present-local-notification + [title message user-info] + (.presentLocalNotification ^js pn-ios #js {:alertBody message :alertTitle title :userInfo user-info})) + +(defn add-listener + [event callback] + (.addEventListener ^js pn-ios event callback) + #js {:remove (fn [] + (.removeEventListener ^js pn-ios event))}) + +(defn request-permissions + [] + (-> (.requestPermissions ^js pn-ios) + (.then #()) + (.catch #()))) + +(defn abandon-permissions + [] + (.abandonPermissions ^js pn-ios)) + +(defn remove-all-delivered-notifications + [] + (.removeAllDeliveredNotifications ^js pn-ios)) + +(defn- create-registration-success-listener + [on-registered-success] + (add-listener "register" + (fn [token] + (on-registered-success token)))) + +(defn- create-registration-fail-listener + [on-registered-fail] + (add-listener "registrationError" + (fn [event] + (on-registered-fail + (ex-info "Failed to register for remote token" + {:code (.-code event) + :description (.-message event) + :details (.-details event)}))))) + +(defn- remove-registration-listeners + [listeners] + (doseq [listener listeners] + (.remove listener))) + +(defn- start-registration + [_options] + (request-permissions)) + +(defn- remote-token-registration-process + [options] + (let [state (atom {:listeners []})] + (promesa/create + (fn [resolver rejector] + (let [on-finish-registration + (fn [result] + (remove-registration-listeners (:listeners @state)) + (if (:error result) + (rejector result) + (resolver result)))] + (swap! state assoc + :listeners + [(create-registration-success-listener on-finish-registration) + (create-registration-fail-listener on-finish-registration)]) + (start-registration options)))))) + +(defn- fail-request-for-android + [_options] + (promesa/rejected + (ex-info "Failed to register for APNS token" + {:description "Cannot register APNS token on Android"}))) + +(defn request-remote-token + "Requests and registers a device token for remote push-notifications for iOS." + [options] + (if platform/ios? + (remote-token-registration-process options) + (fail-request-for-android options))) diff --git a/src/react_native/reanimated.cljs b/src/react_native/reanimated.cljs new file mode 100644 index 00000000000..c570ace9fb2 --- /dev/null +++ b/src/react_native/reanimated.cljs @@ -0,0 +1,183 @@ +(ns react-native.reanimated + (:require + ["@react-native-community/blur" :as blur] + ["react-native" :as rn] + ["react-native-fast-image" :as FastImage] + ["react-native-linear-gradient" :default LinearGradient] + ["react-native-reanimated" :default reanimated :refer + (useSharedValue useAnimatedStyle + withTiming + withDelay + withSpring + withRepeat + withSequence + withDecay + Easing + cancelAnimation + SlideInUp + SlideOutUp + LinearTransition + useAnimatedScrollHandler + scrollTo + useAnimatedRef)] + ["react-native-redash" :refer (withPause)] + [react-native.flat-list :as rn-flat-list] + [react-native.platform :as platform] + [reagent.core :as reagent] + [utils.worklets.core :as worklets.core])) + +(def ^:const default-duration 300) + +;; Animations +(def slide-in-up-animation SlideInUp) +(def slide-out-up-animation SlideOutUp) +(def linear-transition LinearTransition) + +;; Animated Components +(def create-animated-component (comp reagent/adapt-react-class (.-createAnimatedComponent reanimated))) + +(def view (reagent/adapt-react-class (.-View reanimated))) + +(def scroll-view (reagent/adapt-react-class (.-ScrollView reanimated))) +(def image (reagent/adapt-react-class (.-Image reanimated))) + +(def reanimated-flat-list (reagent/adapt-react-class (.-FlatList reanimated))) +(defn flat-list + [props] + [reanimated-flat-list (rn-flat-list/base-list-props props)]) + +(def touchable-opacity (create-animated-component (.-TouchableOpacity ^js rn))) +(def linear-gradient (create-animated-component LinearGradient)) +(def fast-image (create-animated-component FastImage)) +(def blur-view (if platform/ios? (create-animated-component (.-BlurView blur)) view)) + +(def scroll-to scrollTo) +;; Hooks +(def use-shared-value useSharedValue) +(def use-animated-style useAnimatedStyle) +(def use-animated-scroll-handler useAnimatedScrollHandler) +(def use-animated-ref useAnimatedRef) +;; Animations +(def with-timing withTiming) +(def with-delay withDelay) +(def with-spring withSpring) +(def with-decay withDecay) +(def with-repeat withRepeat) +(def with-sequence withSequence) +(def with-pause withPause) +(def cancel-animation cancelAnimation) + + +;; Easings +(def bezier (.-bezier ^js Easing)) + +(def in-out + (.-inOut ^js Easing)) + +;; trying to put default-easing inside easings map causes test to fail +(defn default-easing [] (in-out (.-quad ^js Easing))) + +(def easings + {:linear (bezier 0 0 1 1) + :easing1 (bezier 0.25 0.1 0.25 1) + :easing2 (bezier 0 0.3 0.6 0.9) + :easing3 (bezier 0.3 0.3 0.3 0.9) + :easing4 (bezier 0.3 0.3 1 1)}) + +;; Helper functions +(defn get-shared-value + [anim] + (when anim + (.-value anim))) + +(defn set-shared-value + [anim v] + (when (and anim (some? v)) + (set! (.-value anim) v))) + +(defn interpolate + ([shared-value input-range output-range] + (interpolate shared-value input-range output-range nil)) + ([shared-value input-range output-range extrapolation] + (worklets.core/interpolate-value + shared-value + (clj->js input-range) + (clj->js output-range) + (clj->js extrapolation)))) + +(defn apply-animations-to-style + [animations style] + (use-animated-style + (worklets.core/apply-animations-to-style (clj->js animations) (clj->js style)))) + +;; Animators +(defn animate-shared-value-with-timing + [anim v duration easing] + (set-shared-value anim + (with-timing v + (js-obj "duration" duration + "easing" (get easings easing))))) + +(defn animate-shared-value-with-delay + [anim v duration easing delay-ms] + (set-shared-value anim + (with-delay delay-ms + (with-timing v + (js-obj "duration" duration + "easing" (get easings easing)))))) + +(defn animate-delay + ([animation v delay-ms] + (animate-delay animation v delay-ms default-duration)) + ([animation v delay-ms duration] + (set-shared-value animation + (with-delay delay-ms + (with-timing v + (clj->js {:duration duration + :easing (default-easing)})))))) + +(defn animate-shared-value-with-repeat + [anim v duration easing number-of-repetitions reverse?] + (set-shared-value anim + (with-repeat (with-timing v + (js-obj "duration" duration + "easing" (get easings easing))) + number-of-repetitions + reverse?))) + +(defn animate-shared-value-with-delay-repeat + ([anim v duration easing delay-ms number-of-repetitions] + (animate-shared-value-with-delay-repeat anim v duration easing delay-ms number-of-repetitions false)) + ([anim v duration easing delay-ms number-of-repetitions reverse?] + (set-shared-value anim + (with-delay delay-ms + (with-repeat + (with-timing v + #js + {:duration duration + :easing (get easings easing)}) + number-of-repetitions + reverse?))))) + +(defn animate-shared-value-with-spring + [anim v {:keys [mass stiffness damping]}] + (set-shared-value anim + (with-spring v + (js-obj "mass" mass + "damping" damping + "stiffness" stiffness)))) + +(defn animate-shared-value-with-decay + [anim velocity clamp] + (set-shared-value anim + (with-decay (clj->js {:velocity velocity + :clamp clamp})))) + +(defn animate + ([animation value] + (animate animation value default-duration)) + ([animation value duration] + (set-shared-value animation + (with-timing value + (clj->js {:duration duration + :easing (default-easing)}))))) diff --git a/src/react_native/safe_area.cljs b/src/react_native/safe_area.cljs new file mode 100644 index 00000000000..fe9a6bed06e --- /dev/null +++ b/src/react_native/safe_area.cljs @@ -0,0 +1,45 @@ +(ns react-native.safe-area + (:require [react-native.platform :as platform] + [react-native.safe-area-context :as safe-area-context])) + +(def ^:private safe-area-top-fix + "Subtract to the top safe area to match a standard design. + The values depend on our components padding (usually 12)" + {:android {:default 0} + :iphone {62 11 ;; 16 + + 59 12 ;; 14Pro -> 15Pro + :default 8}}) ;; 11 -> 14, 13mini + +(def ^:private safe-area-bottom-fix + "Subtract to the bottom safe area to match a standard design. + The values depend on our components padding (usually 12)" + {:android {:default 0} + :iphone {:default 12}}) + +(defn- get-top-fix + [raw-top-safe-area] + (let [platform (if platform/ios? :iphone :android) + default (-> safe-area-top-fix platform :default)] + (get-in safe-area-top-fix [platform raw-top-safe-area] default))) + +(defn- get-bottom-fix + [] + (let [platform (if platform/ios? :iphone :android)] + (-> safe-area-bottom-fix platform :default))) + +(defn- get-top-for-views + "Safe area top that must be used when creating screens" + [original-top-safe-area] + (- original-top-safe-area (get-top-fix original-top-safe-area))) + +(defn- get-bottom-for-views + "Safe area bottom that must be used when creating screens" + [original-bottom-safe-area] + (if platform/ios? + (- original-bottom-safe-area (get-bottom-fix)) + 0)) + +(def top (-> safe-area-context/initial-window-metrics :insets :top get-top-for-views)) +(def bottom (-> safe-area-context/initial-window-metrics :insets :bottom get-bottom-for-views)) +(def insets {:top top :bottom bottom}) +(def window (:frame safe-area-context/initial-window-metrics)) diff --git a/src/react_native/safe_area_context.cljs b/src/react_native/safe_area_context.cljs new file mode 100644 index 00000000000..ae35a6e883a --- /dev/null +++ b/src/react_native/safe_area_context.cljs @@ -0,0 +1,7 @@ +(ns react-native.safe-area-context + (:require ["react-native-safe-area-context" + :refer [initialWindowMetrics SafeAreaProvider]] + [reagent.core :as reagent])) + +(def initial-window-metrics (js->clj initialWindowMetrics :keywordize-keys true)) +(def safe-area-provider (reagent/adapt-react-class SafeAreaProvider)) diff --git a/src/react_native/section_list.cljs b/src/react_native/section_list.cljs new file mode 100644 index 00000000000..86efd9acda2 --- /dev/null +++ b/src/react_native/section_list.cljs @@ -0,0 +1,49 @@ +(ns react-native.section-list + (:require + ["react-native" :as react-native] + [react-native.flat-list :as flat-list] + [reagent.core :as reagent])) + +(def section-list-class (reagent/adapt-react-class (.-SectionList react-native))) + +(def memo-wrap-render-fn + (memoize + (fn [f render-data] + (fn [^js data] + (reagent/as-element [f (.-item data) (.-index data) (.-separators data) render-data]))))) + +(defn- wrap-render-section-header-fn + [f] + (fn [^js data] + (let [^js section (.-section data)] + (reagent/as-element [f + {:index (.-index section) + :title (.-title section) + :data (.-data section)}])))) + +(defn- wrap-per-section-render-fn + [index props] + (-> (if-let [f (:render-fn props)] + (assoc (dissoc props :render-fn :render-data) + :renderItem + (memo-wrap-render-fn f (:render-data props))) + props) + (update :data to-array) + (assoc :index index))) + +(defn section-list + "A wrapper for SectionList. + To render something on empty sections, use renderSectionFooter and conditionally + render on empty data + See https://facebook.github.io/react-native/docs/sectionlist.html" + [{:keys [sections render-section-header-fn render-section-footer-fn style] :as props}] + [section-list-class + (merge (flat-list/base-list-props props) + props + (when render-section-footer-fn + {:renderSectionFooter (wrap-render-section-header-fn render-section-footer-fn)}) + {:sections (->> sections + (map-indexed wrap-per-section-render-fn) + (clj->js)) + :renderSectionHeader (wrap-render-section-header-fn render-section-header-fn) + :style style})]) diff --git a/src/react_native/shadow.cljs b/src/react_native/shadow.cljs new file mode 100644 index 00000000000..2514b55e3c9 --- /dev/null +++ b/src/react_native/shadow.cljs @@ -0,0 +1,6 @@ +(ns react-native.shadow + (:require + ["react-native-shadow-2" :refer [Shadow]] + [reagent.core :as reagent])) + +(def view (reagent/adapt-react-class Shadow)) diff --git a/src/react_native/shake.cljs b/src/react_native/shake.cljs new file mode 100644 index 00000000000..2652f434312 --- /dev/null +++ b/src/react_native/shake.cljs @@ -0,0 +1,7 @@ +(ns react-native.shake + (:require + ["react-native-shake" :as react-native-shake])) + +(defn add-shake-listener + [handler] + (.addEventListener react-native-shake "ShakeEvent" handler)) diff --git a/src/react_native/share.cljs b/src/react_native/share.cljs new file mode 100644 index 00000000000..5ae770dc26b --- /dev/null +++ b/src/react_native/share.cljs @@ -0,0 +1,8 @@ +(ns react-native.share + (:require + ["react-native-share" :default react-native-share])) + +(defn open + ([options] + (-> ^js react-native-share + (.open (clj->js options))))) diff --git a/src/react_native/slider.cljs b/src/react_native/slider.cljs new file mode 100644 index 00000000000..d73668c5ff2 --- /dev/null +++ b/src/react_native/slider.cljs @@ -0,0 +1,6 @@ +(ns react-native.slider + (:require + ["@react-native-community/slider" :default Slider] + [reagent.core :as reagent])) + +(def slider (reagent/adapt-react-class Slider)) diff --git a/src/react_native/svg.cljs b/src/react_native/svg.cljs new file mode 100644 index 00000000000..9bdbac0d006 --- /dev/null +++ b/src/react_native/svg.cljs @@ -0,0 +1,14 @@ +(ns react-native.svg + (:require + ["react-native-svg" :as Svg] + [reagent.core :as reagent])) + +(def svg (reagent/adapt-react-class Svg/default)) +(def path (reagent/adapt-react-class Svg/Path)) +(def rect (reagent/adapt-react-class Svg/Rect)) +(def clip-path (reagent/adapt-react-class Svg/ClipPath)) +(def defs (reagent/adapt-react-class Svg/Defs)) +(def circle (reagent/adapt-react-class Svg/Circle)) +(def svg-xml (reagent/adapt-react-class Svg/SvgXml)) +(def linear-gradient (reagent/adapt-react-class Svg/LinearGradient)) +(def stop (reagent/adapt-react-class Svg/Stop)) diff --git a/src/react_native/syntax_highlighter.cljs b/src/react_native/syntax_highlighter.cljs new file mode 100644 index 00000000000..633aa3dd39b --- /dev/null +++ b/src/react_native/syntax_highlighter.cljs @@ -0,0 +1,12 @@ +(ns react-native.syntax-highlighter + (:require + ["react-native" :as react-native] + ["react-syntax-highlighter" :default Highlighter])) + + +(defn highlighter + [props code-string] + [:> Highlighter + ;; Default props to adapt Highlighter for react-native. + (assoc props :Code-tag react-native/View :Pre-tag react-native/View) + code-string]) diff --git a/src/react_native/utils.cljs b/src/react_native/utils.cljs new file mode 100644 index 00000000000..16fcd758ada --- /dev/null +++ b/src/react_native/utils.cljs @@ -0,0 +1,53 @@ +(ns react-native.utils + (:require [goog.object :as gobj] + [utils.transforms :as transforms])) + +(defonce ^:private throttle (atom {})) + +(defn- wrapped-ref + [{:keys [ref]} throttle-id] + (fn [ref-value] + (when ref-value + (when ref + (ref ref-value)) + (reset! throttle-id ref-value)))) + +(defn- throttled-on-press + [{:keys [on-press allow-multiple-presses? throttle-duration]} throttle-id] + (if allow-multiple-presses? + on-press + (fn [] + (let [id @throttle-id] + (when (and id (not (get @throttle id))) + (swap! throttle assoc id true) + (on-press) + (js/setTimeout + #(swap! throttle dissoc id) + (or throttle-duration 500))))))) + +(defn custom-pressable-props + [{:keys [on-press] :as props}] + (let [throttle-id (atom nil)] + (cond-> props + on-press + (assoc :on-press (throttled-on-press props throttle-id) + :ref (wrapped-ref props throttle-id))))) + +(defn get-props-and-children + [argv] + (let [first-child (first argv) + props (when (map? first-child) first-child) + children (if props (rest argv) argv)] + [props children])) + +(defn kebab-case-map->camelCase-obj + "Takes a Clojure map with kebab-case keys and returns a JS object with camelCase keys. + Not recursive" + [m] + (if (map? m) + (reduce-kv (fn [o k v] + (doto o + (gobj/set (transforms/->camelCaseString k) v))) + #js {} + m) + m)) diff --git a/src/react_native/wallet_connect.cljs b/src/react_native/wallet_connect.cljs new file mode 100644 index 00000000000..e9f5e6b45c0 --- /dev/null +++ b/src/react_native/wallet_connect.cljs @@ -0,0 +1,97 @@ +(ns react-native.wallet-connect + (:require + ["@reown/walletkit$default" :as WalletKit] + ["@walletconnect/core" :as wc-core] + ["@walletconnect/utils" :as wc-utils] + [cljs-bean.core :as bean] + [oops.core :as oops])) + +(defn- wallet-connect-core + [project-id] + (new ^js wc-core/Core (clj->js {:projectId project-id}))) + +(defn init + [project-id metadata] + (let [core (wallet-connect-core project-id)] + (oops/ocall WalletKit + "init" + (bean/->js {:core core + :metadata metadata})))) + +(defn build-approved-namespaces + [proposal supported-namespaces] + (oops/ocall wc-utils + "buildApprovedNamespaces" + (bean/->js {:proposal proposal + :supportedNamespaces (clj->js supported-namespaces)}))) + +;; Get an error from this list: +;; https://github.com/WalletConnect/walletconnect-monorepo/blob/c6e9529418a0c81d4efcc6ac4e61f242a50b56c5/packages/utils/src/errors.ts +(defn get-sdk-error + [error-key] + (oops/ocall wc-utils "getSdkError" error-key)) + +(defn parse-uri + [uri] + (-> (oops/ocall wc-utils "parseUri" uri) + (bean/->clj))) + +(defn respond-session-request + [{:keys [web3-wallet topic id result error]}] + (oops/ocall web3-wallet + "respondSessionRequest" + (bean/->js {:topic topic + :response + (merge {:id id + :jsonrpc "2.0"} + (when result + {:result result}) + (when error + {:error error}))}))) + +(defn reject-session + [{:keys [web3-wallet id reason]}] + (oops/ocall web3-wallet + "rejectSession" + (bean/->js {:id id + :reason reason}))) + +(defn approve-session + [{:keys [web3-wallet id approved-namespaces]}] + (oops/ocall web3-wallet + "approveSession" + (bean/->js {:id id + :namespaces approved-namespaces}))) + +(defn disconnect-session + [{:keys [web3-wallet reason topic]}] + (oops/ocall web3-wallet + "disconnectSession" + (bean/->js {:topic topic + :reason reason}))) + +(defn get-active-sessions + [web3-wallet] + (oops/ocall web3-wallet "getActiveSessions")) + +(defn core-pairing-pair + [web3-wallet url] + (oops/ocall web3-wallet + "core.pairing.pair" + (bean/->js {:uri url}))) + +(defn register-handler + [{:keys [web3-wallet event handler]}] + (oops/ocall web3-wallet + "on" + event + #(-> (bean/->clj %) + handler))) + +(defn unregister-handler + [{:keys [web3-wallet event handler]}] + (oops/ocall web3-wallet + "off" + event + #(-> (bean/->clj %) + handler))) diff --git a/src/reagent/config.cljs b/src/reagent/config.cljs new file mode 100644 index 00000000000..d54e74b0515 --- /dev/null +++ b/src/reagent/config.cljs @@ -0,0 +1,56 @@ +(ns reagent.config + (:require [goog.object :as gobj] + [reagent.core] + [reagent.impl.template :as template] + [reagent.impl.util :as reagent.util] + [utils.transforms :as transforms])) + +(def ^:dynamic ^js *keys-to-convert* #js {}) + +(declare convert-prop-value) + +(defn kv-conv + [convert-in-vectors?] + (fn [o k v] + (let [prop-name (template/cached-prop-name k) + recursively-convert-in-vectors? (or convert-in-vectors? + (.hasOwnProperty *keys-to-convert* prop-name))] + (doto o + (gobj/set prop-name + (convert-prop-value v recursively-convert-in-vectors?)))))) + +(defn convert-prop-value + "Based on `reagent.impl.template/kv-conv`. + Takes the prop map of a reagent component and returns a React-valid property JS Object + by transforming kebab-case keys -> camelCase, maps -> JS Objects and vectors -> arrays. + + This version adds support to recursively transform properties inside vectors, to have a + more consistent developer experience in React Native." + ([x] + (convert-prop-value x false)) + ([x convert-in-vectors?] + (cond + (reagent.util/js-val? x) x + (reagent.util/named? x) (name x) + (map? x) (reduce-kv (kv-conv convert-in-vectors?) #js {} x) + (and convert-in-vectors? + (vector? x)) (transforms/map-array #(convert-prop-value % true) x) + (coll? x) (clj->js x) + (ifn? x) (fn [& args] + (apply x args)) + :else (clj->js x)))) + +(defn set-convert-props-in-vectors! + "We override the default reagent implementation with the one that supports vectors." + [keys-to-convert] + (set! *keys-to-convert* (reduce (fn [o k] + (doto o + (gobj/set (template/cached-prop-name k) true))) + #js {} + keys-to-convert)) + (set! template/convert-prop-value convert-prop-value)) + +;; Config applied for all the app +(def functional-compiler (reagent.core/create-compiler {:function-components true})) +(reagent.core/set-default-compiler! functional-compiler) +(set-convert-props-in-vectors! #{:style :holes :content-container-style}) diff --git a/src/reagent/config_test.cljs b/src/reagent/config_test.cljs new file mode 100644 index 00000000000..4b5cfa2f413 --- /dev/null +++ b/src/reagent/config_test.cljs @@ -0,0 +1,78 @@ +(ns reagent.config-test + (:require + [cljs.test :refer [deftest is testing]] + [reagent.config :as sut])) + +(sut/set-convert-props-in-vectors! #{:foo :bar :foo-bar-qux}) + +(deftest convert-prop-value-test + ;; `test-fn` transforms the result to be easily compared during testing + (let [test-fn (comp js->clj sut/convert-prop-value)] + (testing "camelCase keys are kept as is" + (let [props {:foo nil :bar nil}] + (is (= {"foo" nil + "bar" nil} + (test-fn props))))) + + (testing "kebab-case keys are transformed" + (let [props {:foo nil :bar nil}] + (is (= {"foo" nil + "bar" nil} + (test-fn props))))) + + (testing "kebab-case keys are transformed when passed inside a vector" + (let [props-in-vector [{:foo nil :bar nil}]] + (is (= [{"foo" nil + "bar" nil}] + (test-fn props-in-vector))))) + + (testing "kebab-case keys are transformed recursively when the structure has vectors" + (let [props-with-vectors {:foo [{:foo-bar nil + :foo-baz nil}] + :bar [{:bar-baz nil} + {:bar-qux nil}]}] + (is (= {"foo" [{"fooBar" nil + "fooBaz" nil}] + "bar" [{"barBaz" nil} {"barQux" nil}]} + (test-fn props-with-vectors)))) + (testing "Complex example" + (let [complex-props {:foo [{:foo-bar nil :foo-baz nil}] + :bar [{:bar-baz nil} {:bar-qux nil}] + :qux {:foo-qux :bar-qux} + :foo-bar-qux {:foo [{:bar :qux} + {:bar-qux nil}] + :foo-bar :qux + :foo-bar-qux [:foo :bar :qux {:foo-bar :foo-bar}]}}] + (is (= {"foo" [{"fooBar" nil "fooBaz" nil}] + "bar" [{"barBaz" nil} {"barQux" nil}] + "qux" {"fooQux" "bar-qux"} + "fooBarQux" {"foo" [{"bar" "qux"} {"barQux" nil}] + "fooBar" "qux" + "fooBarQux" ["foo" "bar" "qux" {"fooBar" "foo-bar"}]}} + (test-fn complex-props))))) + + (testing "Keep `:qux` without transformations because it isn't specified in config" + (let [props-with-vector {:qux [{:foo-qux :bar-qux} + {:baz-qux nil}]}] + (is (= {"qux" [{"foo-qux" "bar-qux"} + {"baz-qux" nil}]} + (test-fn props-with-vector))))) + + (testing "Keep `:qux` without transformations but transforms :foo due to config" + (let [props-with-vectors {:foo {:foo-foo [{:foo-foo [{:foo-foo-1 nil + :foo-foo-2 nil}]} + {:foo-bar [{:foo-bar-1 nil + :foo-bar-2 nil}]}]} + :qux {:qux-qux [{:qux-qux [{:qux-qux-1 nil + :qux-qux-2 nil}]} + {:qux-bar [{:qux-qux-1 nil + :qux-qux-2 nil}]}]}}] + (is (= {"foo" {"fooFoo" + ;; This vector is transformed + [{"fooFoo" [{"fooFoo1" nil "fooFoo2" nil}]} + {"fooBar" [{"fooBar1" nil "fooBar2" nil}]}]} + "qux" {"quxQux" + ;; This vector is not + [{"qux-qux" [{"qux-qux-1" nil "qux-qux-2" nil}]} + {"qux-bar" [{"qux-qux-1" nil "qux-qux-2" nil}]}]}} + (test-fn props-with-vectors)))))))) diff --git a/src/schema/README.md b/src/schema/README.md new file mode 100644 index 00000000000..b6513d7f237 --- /dev/null +++ b/src/schema/README.md @@ -0,0 +1,68 @@ +# Schemas + +This document will grow to describe how we use Malli in the project and our +conventions. It's still early days 🐪 + +## Guidelines +### Use var quote `#'` when aliasing instrumented vars + +It is common in this repository to have aliases to vars. For example, `view` +referring to `var-internal`, or `quo.core/button` referring to +`quo.components.buttons.button.view`. + +If the original var being aliased is instrumented, the alias var MUST [var +quote](https://clojure.org/guides/weird_characters#_var_quote) the original var. +If you don't do this, the aliased var will not be instrumented. + +```clojure +;; bad, view-internal is instrumented, but both aliases don't use a var quote. +(schema.core/=> view-internal ?schema) +(def view (quo.context/with-theme view-internal)) +(def button quo.components.buttons.button.view/button) + +;; good +(schema.core/=> view-internal ?schema) +(def view (quo.context/with-theme #'view-internal)) +(def button #'quo.components.buttons.button.view/button) +``` + +### Prefix schema references with `?` + +Prefix schema bindings and vars with a question mark `?`. This is the naming +convention used by malli itself when functions receive instances of schemas and +it's an unambiguous way to avoid naming clashes. + +```clojure +;; bad +(def message-type [:enum ...]) + +(defn view + [message-type] ; Shadows `message-type` schema + (do-something message-type)) + +;; good +(def ?message-type [:enum ...]) + +(defn view + [message-type] ; Unambiguous naming strategy + (do-something message-type)) +``` + +### Define schemas as functions when needed + +Malli has many utility functions to manipulate schemas as data, and they will +automatically check if the schemas were already defined in the registry. + +For schemas we want to conveniently access from the global registry, like +`:schema.common/theme`, they must be registered before Malli tries to use them. + +```clojure +;; bad, will fail if :schema.common/bar is not registered. +(def ^:private ?foo + (malli.util/select-keys :schema.common/bar [:id :name])) + +;; good, execution will be delayed until the schema ?foo is correctly registered. +(defn- ?foo + [] + (malli.util/select-keys :schema.common/bar [:id :name])) +``` diff --git a/src/schema/common.cljs b/src/schema/common.cljs new file mode 100644 index 00000000000..87f4909a35f --- /dev/null +++ b/src/schema/common.cljs @@ -0,0 +1,54 @@ +(ns schema.common + (:require [schema.registry :as registry])) + +(def ^:private ?theme + [:enum :light :dark]) + +(def ^:private ?customization-color + [:or :string :keyword]) + +(def ^:private ?public-key + [:re #"^0x04[0-9a-f]{128}$"]) + +(def ^:private ?image-source + [:or + :int + :string + [:map + [:uri [:maybe [:string]]]] + ;; NOTE: jest tests resolve requires as #js objects + [:fn object?]]) + +(def ^:private ?rpc-call + [:sequential + {:min 1} + [:map + {:closed true} + [:method [:or :keyword :string]] + [:params [:sequential :any]] + [:js-response {:optional true} :any] + [:on-success [:or fn? [:cat keyword? [:* :any]]]] + [:on-error [:or fn? [:cat keyword? [:* :any]]]]]]) + +(def ^:private ?exception + [:fn {:error/message "schema.common/exception should be of type ExceptionInfo"} + (fn [v] (instance? ExceptionInfo v))]) + +(def ^:private ?hiccup + vector?) + +;; NOTE: malli doesn't have generators for the `:re` schema in cljs, so returning a static value instead +(def ^:private default-generator-return "0x59682f00") +(def ^:private ?hex + [:and {:gen/return default-generator-return} [:re #"^0x[0-9a-fA-F]+$"] :string]) + +(defn register-schemas + [] + (registry/register ::theme ?theme) + (registry/register ::customization-color ?customization-color) + (registry/register ::public-key ?public-key) + (registry/register ::image-source ?image-source) + (registry/register ::rpc-call ?rpc-call) + (registry/register ::exception ?exception) + (registry/register ::hiccup ?hiccup) + (registry/register ::hex ?hex)) diff --git a/src/schema/core.clj b/src/schema/core.clj new file mode 100644 index 00000000000..1402eb9c144 --- /dev/null +++ b/src/schema/core.clj @@ -0,0 +1,23 @@ +(ns schema.core) + +(defmacro => + "Similar to `malli.core/=>`, but instrumentation code will be completely removed + in non-debug environments. + + `value` is first transformed via `malli.core/schema` to make sure we fail fast + and only register valid schemas. + + Pass raw schema `value` instead of schema instance when it's `:function` schema + https://github.com/status-im/status-mobile/pull/18364#issuecomment-1875543794" + [sym value] + `(if ^boolean js/goog.DEBUG + (try + (let [schema-instance# (malli.core/schema ~value)] + (if (and (seq ~value) (= :function (first ~value))) + (malli.core/=> ~sym ~value) + (malli.core/=> ~sym schema-instance#))) + (catch js/Error e# + (taoensso.timbre/error "Failed to instrument function" + {:symbol ~sym :error e#}) + (throw e#))) + ~sym)) diff --git a/src/schema/core.cljs b/src/schema/core.cljs new file mode 100644 index 00000000000..ffa74ddf13a --- /dev/null +++ b/src/schema/core.cljs @@ -0,0 +1,95 @@ +(ns schema.core + (:require-macros schema.core) + (:require + [malli.core :as malli] + [malli.dev.pretty :as malli.pretty] + schema.state + [taoensso.timbre :as log])) + +(goog-define throw-on-error? false) + +(defn- ui-reporter + "Prints to STDOUT and signals a schema error should be displayed on screen." + [schema-id printer] + (let [report (malli.pretty/reporter printer)] + (fn [type data] + (report type data) + (swap! schema.state/errors conj schema-id)))) + +(defn- thrower + "Similar to `malli.dev.pretty/thrower`, but this reporter uses js/Error instead + of ex-info, otherwise invalid schema errors will be printed in one long and + incomprehensible line in unit test failures." + ([] (thrower (malli.pretty/-printer))) + ([printer] + (let [report (malli.pretty/reporter printer)] + (fn [type data] + (let [message (with-out-str (report type data))] + (throw (js/Error. (str "\n" message)))))))) + +(defn reporter + ([] + (reporter nil nil)) + ([schema-id] + (reporter schema-id nil)) + ([schema-id opts] + (let [printer (malli.pretty/-printer + (merge {:width 60 + :print-length 6 + :print-level 3 + :print-meta false} + opts))] + (if throw-on-error? + ;; The thrower reporter should be used to short-circuit tests on schema errors. + (thrower printer) + (ui-reporter schema-id printer))))) + +(defn- with-clear-schema-error + "Clears current schema error on app if args passed to `f` are valid." + [schema-id validate-input f] + (fn [& args] + (when (validate-input args) + (swap! schema.state/errors disj schema-id)) + (apply f args))) + +(defn instrument + "Similar to `malli/-instrument`, but will automatically clear up visible schema + errors if `f` is called with valid arguments. + + We use a validator cached by `malli.core/validator`, so that validation is + performed once. + + If `?schema` is invalid, then behave like a nop, log the error and return `f`. + + `schema-id` is optional, but should be passed when instrumenting anonymous + functions. Consider using a namespaced keyword. For example: + + (schema/instrument ::foo + (fn [x] (inc x)) + [:=> [:cat :int] :int]) + " + ([f ?schema] + (if ^boolean js/goog.DEBUG + (let [schema-id (when (var? f) (symbol f))] + (when-not schema-id + (log/warn "Anonymous function instrumented without an explicit identifier." + {:schema ?schema})) + (instrument schema-id f ?schema)) + f)) + ([schema-id f ?schema] + (if ^boolean js/goog.DEBUG + (try + (let [?schema (malli/schema ?schema) + {schema-input :input} (malli/-function-info ?schema) + [validate-input _] (malli/-vmap malli/validator [schema-input])] + (malli/-instrument {:schema ?schema + :report (reporter schema-id + {:title (str "Schema error at " schema-id)})} + (with-clear-schema-error schema-id validate-input f))) + (catch js/Error e + (log/error "Failed to instrument function" + {:schema-id schema-id + :error e + :function f}) + (throw e))) + f))) diff --git a/src/schema/quo.cljs b/src/schema/quo.cljs new file mode 100644 index 00000000000..3c48419d7d7 --- /dev/null +++ b/src/schema/quo.cljs @@ -0,0 +1,80 @@ +(ns schema.quo + (:require + [quo.foundations.colors :as colors] + [quo.foundations.resources :as resources] + [schema.registry :as registry])) + +(def ^:private ?customization-color (into [:enum :primary] colors/account-colors)) + +(def ^:private ?profile-picture-options + [:map + [:length :int] + [:full-name :string] + [:font-size :int] + [:indicator-size {:optional true} [:maybe :int]] + [:indicator-color {:optional true} [:maybe :string]] + [:indicator-center-to-edge {:optional true} [:maybe :int]] + [:theme :schema.common/theme] + [:color :string] + [:size :int]]) + +(def ^:private ?account-image-uri-options + [:map + [:port :int] + [:ratio :double] + [:key-uid :string] + [:image-name :string] + [:theme :schema.common/theme]]) + +(def ^:private ?initials-image-uri-options + [:map + [:port :int] + [:ratio :double] + [:uppercase-ratio :double] + [:font-file :string] + [:theme :schema.common/theme] + [:customization-color ?customization-color] + [:key-uid {:optional true} [:maybe :string]] + [:public-key {:optional true} [:maybe :string]]]) + +(def ^:private ?contact-image-uri-options + [:map + [:port :int] + [:clock :int] + [:ratio :double] + [:image-name :string] + [:public-key :string] + [:theme :schema.common/theme]]) + +(def ^:private ?image-uri-config + [:multi {:dispatch :type} + [:account + [:map + [:type [:= :account]] + [:options ?account-image-uri-options]]] + [:contact + [:map + [:type [:= :contact]] + [:options ?contact-image-uri-options]]] + [:initials + [:map + [:type [:= :initials]] + [:options ?initials-image-uri-options]]]]) + +(def ^:private ?profile-picture-source + [:or + :schema.common/image-source + [:map + [:config ?image-uri-config]]]) + +(def ^:private ?networks + (-> (concat [:enum] resources/supported-networks) + vec)) + +(defn register-schemas + [] + (registry/register ::customization-color ?customization-color) + (registry/register ::image-uri-config ?image-uri-config) + (registry/register ::profile-picture-source ?profile-picture-source) + (registry/register ::profile-picture-options ?profile-picture-options) + (registry/register ::networks ?networks)) diff --git a/src/schema/re_frame.cljs b/src/schema/re_frame.cljs new file mode 100644 index 00000000000..6f12af5aa87 --- /dev/null +++ b/src/schema/re_frame.cljs @@ -0,0 +1,18 @@ +(ns schema.re-frame + (:require + [schema.registry :as registry])) + +(def ^:private ?cofx + [:map]) + +(def ^:private ?event + [:and + [:vector {:min 1} :any] + [:catn + [:event-id keyword?] + [:event-args [:* :any]]]]) + +(defn register-schemas + [] + (registry/register ::cofx ?cofx) + (registry/register ::event ?event)) diff --git a/src/schema/registry.cljs b/src/schema/registry.cljs new file mode 100644 index 00000000000..6af72064f44 --- /dev/null +++ b/src/schema/registry.cljs @@ -0,0 +1,26 @@ +(ns schema.registry + (:refer-clojure :exclude [merge]) + (:require + [malli.core :as malli] + malli.registry)) + +(defonce ^:private registry + (atom (malli/default-schemas))) + +(defn init-global-registry + [] + (malli.registry/set-default-registry! (malli.registry/mutable-registry registry))) + +(defn register + "Defines a new schema in mutable `registry`. + + We normalize `?schema` by always registering it as a proper instance of + `malli.core/Schema` to avoid inconsistencies down the road." + [type ?schema] + (swap! registry assoc type (malli/schema ?schema)) + ?schema) + +(defn merge + [& schemas] + (apply swap! registry cljs.core/merge schemas) + schemas) diff --git a/src/schema/state.cljs b/src/schema/state.cljs new file mode 100644 index 00000000000..cd46cc64a42 --- /dev/null +++ b/src/schema/state.cljs @@ -0,0 +1,11 @@ +(ns schema.state + (:require [reagent.core :as reagent])) + +(def errors + "Set of schema identifiers, usually namespaced keywords. When the set is empty, + no schema errors will be displayed on the app. See `schema.view/view`." + (reagent/atom #{})) + +(defn clear-errors + [] + (reset! errors #{})) diff --git a/src/schema/style.cljs b/src/schema/style.cljs new file mode 100644 index 00000000000..f782de037f2 --- /dev/null +++ b/src/schema/style.cljs @@ -0,0 +1,28 @@ +(ns schema.style) + +(defn container + [{:keys [bottom-inset]}] + {:align-items :center + :flex-direction :row + :background-color "#cc0000" + :border-bottom-left-radius 8 + :border-top-left-radius 8 + :justify-content :center + :padding-vertical 6 + :padding-right 16 + :position :absolute + :bottom (+ 12 bottom-inset) + :right 0 + :z-index 10000000}) + +(def icon + {:margin-horizontal 8}) + +(def text + {:font-family "Inter-SemiBold" + :font-size 13 + :color "#ddd"}) + +(def text-suffix + {:font-style :italic + :font-size 9}) diff --git a/src/schema/view.cljs b/src/schema/view.cljs new file mode 100644 index 00000000000..45b7d7f17d2 --- /dev/null +++ b/src/schema/view.cljs @@ -0,0 +1,18 @@ +(ns schema.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + schema.state + [schema.style :as style])) + +(defn view + [] + (when (seq @schema.state/errors) + [rn/pressable + {:on-press schema.state/clear-errors + :style (style/container {:bottom-inset safe-area/bottom})} + [quo/icon :i/close {:size 12 :color "#ddd" :container-style style/icon}] + [rn/text {:style style/text} + "Schema error(s)" + [rn/text {:style (merge style/text style/text-suffix)} " check logs"]]])) diff --git a/src/status_im/android/core.cljs b/src/status_im/android/core.cljs deleted file mode 100644 index 42bf21ab091..00000000000 --- a/src/status_im/android/core.cljs +++ /dev/null @@ -1,77 +0,0 @@ -(ns status-im.android.core - (:require [reagent.core :as reagent] - [re-frame.core :refer [subscribe dispatch dispatch-sync]] - status-im.utils.db - status-im.ui.screens.db - status-im.ui.screens.events - status-im.ui.screens.subs - status-im.data-store.core - [status-im.ui.screens.views :as views] - [status-im.ui.components.react :as react] - [status-im.native-module.core :as status] - [status-im.utils.notifications :as notifications] - [status-im.core :as core] - [status-im.utils.snoopy :as snoopy])) - -(defn init-back-button-handler! [] - (let [new-listener (fn [] - ;; todo: it might be better always return false from - ;; this listener and handle application's closing - ;; in handlers - (let [stack (subscribe [:get :navigation-stack]) - result-box (subscribe [:get-current-chat-ui-prop :result-box]) - webview (subscribe [:get :webview-bridge]) - view-id (subscribe [:get :view-id]) - chat-id (subscribe [:get-current-chat-id])] - (cond - - (and @webview (:can-go-back? @result-box)) - (do (.goBack @webview) true) - - (< 1 (count @stack)) - (do (dispatch [:navigate-back]) true) - - :else false)))] - (.addEventListener react/back-handler "hardwareBackPress" new-listener))) - -(defn app-state-change-handler [state] - (dispatch [:app-state-change state])) - -(defn app-root [] - (let [keyboard-height (subscribe [:get :keyboard-height])] - (reagent/create-class - {:component-will-mount - (fn [] - (.addListener react/keyboard - "keyboardDidShow" - (fn [e] - (let [h (.. e -endCoordinates -height)] - (dispatch [:hide-tab-bar]) - (when-not (= h @keyboard-height) - (dispatch [:set :keyboard-height h]) - (dispatch [:set :keyboard-max-height h]))))) - (.addListener react/keyboard - "keyboardDidHide" - (fn [_] - (dispatch [:show-tab-bar]) - (when (zero? @keyboard-height) - (dispatch [:set :keyboard-height 0])))) - (.hide react/splash-screen) - (.addEventListener react/app-state "change" app-state-change-handler)) - :component-did-mount - (fn [] - (notifications/on-refresh-fcm-token) - ;; TODO(oskarth): Background click_action handler - (notifications/on-notification)) - :component-will-unmount - (fn [] - (.stop react/http-bridge) - (.removeEventListener react/app-state "change" app-state-change-handler)) - :display-name "root" - :reagent-render views/main}))) - -(defn init [] - (status/set-soft-input-mode status/adjust-resize) - (init-back-button-handler!) - (core/init app-root) - (snoopy/subscribe!)) diff --git a/src/status_im/android/platform.cljs b/src/status_im/android/platform.cljs deleted file mode 100644 index 8f9e048982f..00000000000 --- a/src/status_im/android/platform.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.android.platform) - -(def fonts - {:light {:font-family "Roboto-Light"} - :default {:font-family "Roboto-Regular"} - :medium {:font-family "Roboto-Medium"} - - :toolbar-title {:font-family "Roboto-Medium"} - :toolbar-subtitle {:font-family "Roboto-Regular"} - :roboto-mono {:font-family "RobotoMono-Medium"}}) - -;; Structure to be exported - -(def platform-specific - {:fonts fonts - :tabs {:tab-shadows? true} - :chats {:action-button? true - :new-chat-in-toolbar? false - :render-separator? false} - :contacts {:action-button? true - :new-contact-in-toolbar? false} - :group-block-shadows? true - :discover {:uppercase-subtitles? false} - :status-bar-default-height 25}) diff --git a/src/status_im/app/README.md b/src/status_im/app/README.md new file mode 100644 index 00000000000..ec363064bef --- /dev/null +++ b/src/status_im/app/README.md @@ -0,0 +1,15 @@ +## Application layer + +### Description +This folder contains "application" layer. +Here belongs all the code responsible for managing user-driven flows such as: +- **Sending crypto to someone** (tracks step-by-step progress in the transaction flow). +- **Browsing NFT collectibles** (stores filters, sorting preferences, and current selection). + + +### Responsibilities +- reacting to user actions +- storing states of user flows + +### Rules +- Doesn't store permanent data diff --git a/src/status_im/app/market/events.cljs b/src/status_im/app/market/events.cljs new file mode 100644 index 00000000000..4aa232e5787 --- /dev/null +++ b/src/status_im/app/market/events.cljs @@ -0,0 +1,27 @@ +(ns status-im.app.market.events + (:require + [utils.re-frame :as rf])) + +(rf/reg-event-fx :app.market/load-leaderboard + (fn [{:keys [_db]}] + {:fx [[:dispatch [:gate.market/start-fetching-tokens]]]})) + +(rf/reg-event-fx :app.market/load-more-leaderboard-tokens + (fn [{:keys [_db]}] + {:fx [[:dispatch [:gate.market/fetch-more-tokens]]]})) + +(rf/reg-event-fx :app.market/ensure-tokens-updating + (fn [{:keys [db]} [first-token-to-update tokens-amount]] + (let [token-position (get-in db [:domain.market/leaderboard first-token-to-update])] + {:fx [[:dispatch [:gate.market/ensure-tokens-updating token-position tokens-amount]]]}))) + +(rf/reg-event-fx :app.market/close-leaderboard + (fn [{:keys [_db]}] + {:fx [[:dispatch [:gate.market/stop-fetching-tokens]] + [:dispatch [:domain.market/clear-market-data]]]})) + +(rf/reg-event-fx :app.market/reload-leaderboard + (fn [{:keys [_db]}] + {:fx [[:dispatch [:app.market/close-leaderboard]] + [:dispatch [:app.market/load-leaderboard]]]})) + diff --git a/src/status_im/app/wallet/events.cljs b/src/status_im/app/wallet/events.cljs new file mode 100644 index 00000000000..0e6cce27025 --- /dev/null +++ b/src/status_im/app/wallet/events.cljs @@ -0,0 +1,21 @@ +(ns status-im.app.wallet.events + (:require + [utils.re-frame :as rf])) + +;; TODO(volodymyr.kozieiev): Logic for starting swap was extracted from wallet home ui. +;; It needs to be rewritten to get the accounts data right from the rf-db +(rf/reg-event-fx :app.wallet/start-swap + (fn [{:keys [_db]} [operable-accounts]] + (let [multiple-accounts? (> (count operable-accounts) 1) + first-account-address (:address (first operable-accounts)) + modal-to-open (if multiple-accounts? + :screen/wallet.swap-select-account + :screen/wallet.swap-select-asset-to-pay)] + {:fx [[:dispatch [:wallet/clean-send-data]] + [:dispatch [:wallet/clean-swap]] + (when-not multiple-accounts? + [:dispatch + [:wallet/switch-current-viewing-account + first-account-address]]) + [:dispatch [:open-modal modal-to-open]]]}))) + diff --git a/src/status_im/app_build/core.clj b/src/status_im/app_build/core.clj new file mode 100644 index 00000000000..818806271a3 --- /dev/null +++ b/src/status_im/app_build/core.clj @@ -0,0 +1,28 @@ +(ns status-im.app-build.core + (:require + [clojure.java.io :as io] + [clojure.java.shell :as shell] + [clojure.string :as string])) + +(defmacro get-build-no + [] + (-> (shell/sh "bash" "./scripts/version/build_no.sh") + :out + (string/replace "\n" ""))) + +(defmacro get-current-sha + "fetches the latest commit sha from the current branch" + [] + (-> (shell/sh "git" "rev-parse" "HEAD") + :out + (string/replace "\n" ""))) + +(defmacro git-short-version + [] + (let [version-file-path "VERSION" + version-file (io/file version-file-path)] + (if (.exists version-file) + (string/trim (slurp version-file-path)) + (-> (shell/sh "git" "rev-parse" "--short" "HEAD") + :out + (string/replace "\n" ""))))) diff --git a/src/status_im/app_build/core.cljs b/src/status_im/app_build/core.cljs new file mode 100644 index 00000000000..c9aa0fe30cc --- /dev/null +++ b/src/status_im/app_build/core.cljs @@ -0,0 +1,9 @@ +(ns status-im.app-build.core + (:require-macros [status-im.app-build.core :as build])) + +(def commit-hash (build/get-current-sha)) +(def build-no (build/get-build-no)) +(def version (build/git-short-version)) + +(def app-short-version + (str version " (" build-no ")")) diff --git a/src/status_im/chat/commands/core.cljs b/src/status_im/chat/commands/core.cljs deleted file mode 100644 index 5f6759fc581..00000000000 --- a/src/status_im/chat/commands/core.cljs +++ /dev/null @@ -1,216 +0,0 @@ -(ns status-im.chat.commands.core - (:require [clojure.set :as set] - [clojure.string :as string] - [status-im.constants :as constants] - [status-im.chat.constants :as chat-constants] - [status-im.chat.commands.protocol :as protocol] - [status-im.chat.commands.impl.transactions :as transactions] - [status-im.chat.models :as chat-model] - [status-im.chat.models.input :as input-model] - [status-im.chat.models.message :as message-model] - [status-im.utils.handlers-macro :as handlers-macro])) - -(def register - "Register of all commands. Whenever implementing a new command, - provide the implementation in the `status-im.chat.commands.impl.*` ns, - and add its instance here." - #{(transactions/PersonalSendCommand.) - (transactions/PersonalRequestCommand.)}) - -(def command-id (juxt protocol/id protocol/scope)) - -(defn command-name - "Given the command instance, returns command name as displayed in chat input, - with leading `/` character." - [type] - (str chat-constants/command-char (protocol/id type))) - -(defn command-description - "Returns description for the command." - [type] - (protocol/description type)) - -(defn accessibility-label - "Returns accessibility button label for command, derived from its id" - [type] - (keyword (str (protocol/id type) "-button"))) - -(defn generate-short-preview - "Returns short preview for command" - [{:keys [type]} command-message] - (protocol/short-preview type command-message)) - -(defn generate-preview - "Returns preview for command" - [{:keys [type]} command-message] - (protocol/preview type command-message)) - -(defn- prepare-params - "Prepares parameters sequence of command by providing suggestion components with - selected-event injected with correct arg indexes and `last-arg?` flag." - [command] - (let [parameters (protocol/parameters command) - last-param-idx (dec (count parameters))] - (into [] - (map-indexed (fn [idx {:keys [suggestions] :as param}] - (if suggestions - (update param :suggestions partial - (fn [value] - [:set-command-parameter - (= idx last-param-idx) idx value])) - param)) - parameters)))) - -(defn- add-exclusive-choices [initial-scope exclusive-choices] - (reduce (fn [scopes-set exclusive-choices] - (reduce (fn [scopes-set scope] - (let [exclusive-match (set/intersection scope exclusive-choices)] - (if (seq exclusive-match) - (reduce conj - (disj scopes-set scope) - (map (partial conj - (set/difference scope exclusive-match)) - exclusive-match)) - scopes-set))) - scopes-set - scopes-set)) - #{initial-scope} - exclusive-choices)) - -(defn index-commands - "Takes collecton of things implementing the command protocol, and - correctly indexes them by their composite ids and access scopes." - [commands {:keys [db]}] - (let [id->command (reduce (fn [acc command] - (assoc acc (command-id command) - {:type command - :params (prepare-params command)})) - {} - commands) - access-scope->command-id (reduce-kv (fn [acc command-id {:keys [type]}] - (let [access-scopes (add-exclusive-choices - (protocol/scope type) - protocol/or-scopes)] - (reduce (fn [acc access-scope] - (update acc - access-scope - (fnil conj #{}) - command-id)) - acc - access-scopes))) - {} - id->command)] - {:db (assoc db - :id->command id->command - :access-scope->command-id access-scope->command-id)})) - -(defn chat-commands - "Takes `id->command`, `access-scope->command-id` and `chat` parameters and returns - entries map of `id->command` map eligible for given chat. - Note that the result map is keyed just by `protocol/id` of command entries, - not the unique composite ids of the global `id->command` map. - That's because this function is already returning local commands for particular - chat and locally, they should always have unique `protocol/id`." - [id->command access-scope->command-id {:keys [chat-id group-chat public?]}] - (let [global-access-scope (cond-> #{} - (not group-chat) (conj :personal-chats) - (and group-chat (not public?)) (conj :group-chats) - public? (conj :public-chats)) - chat-access-scope #{chat-id}] - (reduce (fn [acc command-id] - (let [{:keys [type] :as command-props} (get id->command command-id)] - (assoc acc (protocol/id type) command-props))) - {} - (concat (get access-scope->command-id global-access-scope) - (get access-scope->command-id chat-access-scope))))) - -(defn- current-param-position [input-text selection] - (when selection - (when-let [subs-input-text (subs input-text 0 selection)] - (let [input-params (input-model/split-command-args subs-input-text) - param-index (dec (count input-params)) - wrapping-count (get (frequencies subs-input-text) chat-constants/arg-wrapping-char 0)] - (if (and (string/ends-with? subs-input-text chat-constants/spacing-char) - (even? wrapping-count)) - param-index - (dec param-index)))))) - -(defn- command-completion [input-params params] - (let [input-params-count (count input-params) - params-count (count params)] - (cond - (= input-params-count params-count) :complete - (< input-params-count params-count) :less-then-needed - (> input-params-count params-count) :more-than-needed))) - -(defn selected-chat-command - "Takes input text, text-selection and `protocol/id->command-props` map (result of - the `chat-commands` fn) and returns the corresponding `command-props` entry, - or nothing if input text doesn't match any available command. - Besides keys `:params` and `:type`, the returned map contains: - * `:input-params` - parsed parameters from the input text as map of `param-id->entered-value` - # `:current-param-position` - index of the parameter the user is currently focused on (cursor position - in relation to parameters), could be nil if the input is not selected - # `:command-completion` - indication of command completion, possible values are `:complete`, - `:less-then-needed` and `more-then-needed`" - [input-text text-selection id->command-props] - (when (input-model/starts-as-command? input-text) - (let [[command-name & input-params] (input-model/split-command-args input-text)] - (when-let [{:keys [params] :as command-props} (get id->command-props (subs command-name 1))] ;; trim leading `/` for lookup - command-props - (let [input-params (into {} - (keep-indexed (fn [idx input-value] - (when (not (string/blank? input-value)) - (when-let [param-name (get-in params [idx :id])] - [param-name input-value])))) - input-params)] - (assoc command-props - :input-params input-params - :current-param-position (current-param-position input-text text-selection) - :command-completion (command-completion input-params params))))))) - -(defn set-command-parameter - "Set value as command parameter for the current chat" - [last-param? param-index value {:keys [db]}] - (let [{:keys [current-chat-id chats]} db - [command & params] (-> (get-in chats [current-chat-id :input-text]) - input-model/split-command-args) - param-count (count params) - ;; put the new value at the right place in parameters array - new-params (cond-> (into [] params) - (< param-index param-count) (assoc param-index value) - (>= param-index param-count) (conj value)) - ;; if the parameter is not the last one for the command, add space - input-text (cond-> (str command chat-constants/spacing-char - (input-model/join-command-args - new-params)) - (and (not last-param?) - (or (= 0 param-count) - (= param-index (dec param-count)))) - (str chat-constants/spacing-char))] - {:db (-> db - (chat-model/set-chat-ui-props {:validation-messages nil}) - (assoc-in [:chats current-chat-id :input-text] input-text))})) - -(defn select-chat-input-command - "Takes command and (optional) map of input-parameters map and sets it as current-chat input" - [{:keys [type params]} input-params {:keys [db]}] - (let [{:keys [current-chat-id chat-ui-props]} db] - {:db (-> db - (chat-model/set-chat-ui-props {:show-suggestions? false - :validation-messages nil}) - (assoc-in [:chats current-chat-id :input-text] - (str (command-name type) - chat-constants/spacing-char - (input-model/join-command-args input-params))))})) - -(defn parse-parameters - "Parses parameters from input for defined command params, - returns map of `param-name->param-value`" - [params input-text] - (let [input-params (->> (input-model/split-command-args input-text) rest (into []))] - (into {} - (keep-indexed (fn [idx {:keys [id]}] - (when-let [value (get input-params idx)] - [id value]))) - params))) diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs deleted file mode 100644 index 06bbc6a435e..00000000000 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ /dev/null @@ -1,392 +0,0 @@ -(ns status-im.chat.commands.impl.transactions - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [reagent.core :as reagent] - [re-frame.core :as re-frame] - [status-im.chat.commands.protocol :as protocol] - [status-im.chat.commands.impl.transactions.styles :as transactions-styles] - [status-im.data-store.requests :as requests-store] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.chat-preview :as chat-preview] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.animation :as animation] - [status-im.i18n :as i18n] - [status-im.constants :as constants] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.datetime :as datetime] - [status-im.utils.money :as money] - [status-im.ui.screens.wallet.db :as wallet.db] - [status-im.ui.screens.wallet.choose-recipient.events :as choose-recipient.events] - [status-im.wallet.transactions :as wallet.transactions] - [status-im.ui.screens.navigation :as navigation])) - -;; common `send/request` functionality - -(defn- render-asset [selected-event-creator] - (fn [{:keys [name symbol amount decimals] :as asset}] - [react/touchable-highlight - {:on-press #(re-frame/dispatch (selected-event-creator (clojure.core/name symbol)))} - [react/view transactions-styles/asset-container - [react/view transactions-styles/asset-main - [react/image {:source (-> asset :icon :source) - :style transactions-styles/asset-icon}] - [react/text {:style transactions-styles/asset-symbol} symbol] - [react/text {:style transactions-styles/asset-name} name]] - ;;TODO(goranjovic) : temporarily disabled to fix https://github.com/status-im/status-react/issues/4963 - ;;until the resolution of https://github.com/status-im/status-react/issues/4972 - #_[react/text {:style transactions-styles/asset-balance} - (str (money/internal->formatted amount symbol decimals))]]])) - -(def assets-separator [react/view transactions-styles/asset-separator]) - -(defview choose-asset [selected-event-creator] - (letsubs [assets [:wallet/visible-assets-with-amount]] - [react/view - [list/flat-list {:data (filter #(not (:nft? %)) assets) - :key-fn (comp name :symbol) - :render-fn (render-asset selected-event-creator) - :enableEmptySections true - :separator assets-separator - :keyboardShouldPersistTaps :always - :bounces false}]])) - -(defn choose-asset-suggestion [selected-event-creator] - [choose-asset selected-event-creator]) - -(defn personal-send-request-short-preview - [label-key {:keys [content]}] - (let [{:keys [amount asset]} (:params content)] - [chat-preview/text {} - (str (i18n/label label-key) - (i18n/label-number amount) - " " - asset)])) - -(def personal-send-request-params - [{:id :asset - :type :text - :placeholder "Currency" - ;; Suggestion components should be structured in such way that they will just take - ;; one argument, event-creator fn used to construct event to fire whenever something - ;; is selected. - :suggestions choose-asset-suggestion} - {:id :amount - :type :number - :placeholder "Amount"}]) - -;;TODO(goranjovic): currently we only allow tokens which are enabled in Manage assets here -;; because balances are only fetched for them. Revisit this decision with regard to battery/network consequences -;; if we were to update all balances. -(defn- allowed-assets [{:account/keys [account] :keys [chain]}] - (let [chain-keyword (keyword chain) - visible-tokens (get-in account [:settings :wallet :visible-tokens chain-keyword])] - (into {"ETH" 18} - (comp (filter #(and (not (:nft? %)) - (contains? visible-tokens (:symbol %)))) - (map (juxt (comp name :symbol) :decimals))) - (tokens/tokens-for chain-keyword)))) - -(defn- personal-send-request-validation [{:keys [asset amount]} {:keys [db]}] - (let [asset-decimals (get (allowed-assets db) asset)] - (cond - - (not asset-decimals) - {:title "Invalid Asset" - :description (str "Unknown token - " asset)} - - (not amount) - {:title "Amount" - :description "Amount must be specified"} - - :else - (let [sanitised-str (string/replace amount #"," ".") - portions (string/split sanitised-str ".") - decimals (count (get portions 1)) - amount (js/parseFloat sanitised-str)] - (cond - - (or (js/isNaN amount) - (> (count portions) 2)) - {:title "Amount" - :description "Amount is not valid number"} - - (and decimals (> decimals asset-decimals)) - {:title "Amount" - :description (str "Max number of decimals is " asset-decimals)}))))) - -;; `/send` command - -(defview send-status [tx-hash outgoing] - (letsubs [confirmed? [:transaction-confirmed? tx-hash] - tx-exists? [:wallet-transaction-exists? tx-hash]] - [react/touchable-highlight {:on-press #(when tx-exists? - (re-frame/dispatch [:show-transaction-details tx-hash]))} - [react/view transactions-styles/command-send-status-container - [vector-icons/icon (if confirmed? :icons/check :icons/dots) - {:color colors/blue - :container-style (transactions-styles/command-send-status-icon outgoing)}] - [react/view - [react/text {:style transactions-styles/command-send-status-text} - (i18n/label (cond - confirmed? :status-confirmed - tx-exists? :status-pending - :else :status-tx-not-found))]]]])) - -(defview send-preview - [{:keys [content timestamp-str outgoing group-chat]}] - (letsubs [network [:network-name]] - (let [{{:keys [amount fiat-amount tx-hash asset currency] send-network :network} :params} content - recipient-name (get-in content [:params :bot-db :public :recipient]) - network-mismatch? (and (seq send-network) (not= network send-network))] - [react/view transactions-styles/command-send-message-view - [react/view - [react/view transactions-styles/command-send-amount-row - [react/view transactions-styles/command-send-amount - [react/text {:style transactions-styles/command-send-amount-text - :font :medium} - amount - [react/text {:style (transactions-styles/command-amount-currency-separator outgoing)} - "."] - [react/text {:style (transactions-styles/command-send-currency-text outgoing) - :font :default} - asset]]]] - (when fiat-amount - [react/view transactions-styles/command-send-fiat-amount - [react/text {:style transactions-styles/command-send-fiat-amount-text} - (str "~ " fiat-amount " " (or currency (i18n/label :usd-currency)))]]) - (when (and group-chat - recipient-name) - [react/text {:style transactions-styles/command-send-recipient-text} - (str - (i18n/label :send-sending-to) - " " - recipient-name)]) - [react/view - [react/text {:style (transactions-styles/command-send-timestamp outgoing)} - (str (i18n/label :sent-at) " " timestamp-str)]] - [send-status tx-hash outgoing] - (when network-mismatch? - [react/text send-network])]]))) - -;; TODO(goranjovic) - update to include tokens in https://github.com/status-im/status-react/issues/3233 -(defn- transaction-details [contact symbol] - (-> contact - (select-keys [:name :address :whisper-identity]) - (assoc :symbol symbol - :gas (ethereum/estimate-gas symbol) - :from-chat? true))) - -(defn- inject-network-price-info [{:keys [amount asset] :as parameters} {:keys [db]}] - (let [{:keys [chain prices]} db - currency (-> db - (get-in [:account/account :settings :wallet :currency] :usd) - name - string/upper-case)] - (assoc parameters - :network chain - ;; TODO(janherich) - shouldn't this be rather computed on the receiver side ? - :fiat-amount (money/fiat-amount-value amount - (keyword asset) - (keyword currency) - prices) - :currency currency))) - -(deftype PersonalSendCommand [] - protocol/Command - (id [_] "send") - (scope [_] #{:personal-chats}) - (description [_] "Send a payment") - (parameters [_] personal-send-request-params) - (validate [_ parameters cofx] - ;; Only superficial/formatting validation, "real validation" will be performed - ;; by the wallet, where we yield control in the next step - (personal-send-request-validation parameters cofx)) - (on-send [_ {:keys [chat-id]} {:keys [db]}] - (when-let [{:keys [responding-to]} (get-in db [:chats chat-id :input-metadata])] - {:db (update-in db [:chats chat-id :requests] dissoc responding-to) - :data-store/tx [(requests-store/mark-request-as-answered-tx chat-id responding-to)]})) - (on-receive [_ command-message cofx] - (when-let [tx-hash (get-in command-message [:content :params :tx-hash])] - (wallet.transactions/store-chat-transaction-hash tx-hash cofx))) - (short-preview [_ command-message] - (personal-send-request-short-preview :command-sending command-message)) - (preview [_ command-message] - (send-preview command-message)) - protocol/Yielding - (yield-control [_ {:keys [amount asset]} {:keys [db]}] - ;; Prefill wallet and navigate there - (let [recipient-contact (get-in db [:contacts/contacts (:current-chat-id db)]) - sender-account (:account/account db) - chain (keyword (:chain db)) - symbol (keyword asset) - {:keys [decimals]} (tokens/asset-for chain symbol) - {:keys [value error]} (wallet.db/parse-amount amount decimals)] - {:db (-> db - (assoc-in [:wallet :send-transaction :amount] (money/formatted->internal value symbol decimals)) - (assoc-in [:wallet :send-transaction :amount-text] amount) - (assoc-in [:wallet :send-transaction :amount-error] error) - (choose-recipient.events/fill-request-details - (transaction-details recipient-contact symbol)) - (update-in [:wallet :send-transaction] dissoc :id :password :wrong-password?) - (navigation/navigate-to - (if (:wallet-set-up-passed? sender-account) - :wallet-send-transaction-chat - :wallet-onboarding-setup))) - ;; TODO(janherich) - refactor wallet send events, updating gas price - ;; is generic thing which shouldn't be defined in wallet.send, then - ;; we can include the utility helper without running into circ-dep problem - :update-gas-price {:web3 (:web3 db) - :success-event :wallet/update-gas-price-success - :edit? false}})) - protocol/EnhancedParameters - (enhance-parameters [_ parameters cofx] - (inject-network-price-info parameters cofx))) - -;; `/request` command - -(def request-message-icon-scale-delay 600) - -(def min-scale 1) -(def max-scale 1.3) - -(defn button-animation [val to-value loop? answered?] - (animation/anim-sequence - [(animation/anim-delay - (if (and @loop? (not @answered?)) - request-message-icon-scale-delay - 0)) - (animation/spring val {:toValue to-value - :useNativeDriver true})])) - -(defn request-button-animation-logic - [{:keys [to-value val loop? answered?] :as context}] - (animation/start - (button-animation val to-value loop? answered?) - #(if (and @loop? (not @answered?)) - (let [new-value (if (= to-value min-scale) max-scale min-scale) - context' (assoc context :to-value new-value)] - (request-button-animation-logic context')) - (animation/start - (button-animation val min-scale loop? answered?))))) - -(defn request-button-label - "The request button label will be in the form of `request-the-command-name`" - [command-name] - (keyword (str "request-" (name command-name)))) - -(defn request-button [message-id _ on-press-handler] - (let [scale-anim-val (animation/create-value min-scale) - answered? (re-frame/subscribe [:is-request-answered? message-id]) - loop? (reagent/atom true) - context {:to-value max-scale - :val scale-anim-val - :answered? answered? - :loop? loop?}] - (reagent/create-class - {:display-name "request-button" - :component-did-mount - (if (or (nil? on-press-handler) @answered?) (fn []) #(request-button-animation-logic context)) - :component-will-unmount - #(reset! loop? false) - :reagent-render - (fn [message-id {command-icon :icon :as command} on-press-handler] - (when command - [react/touchable-highlight - {:on-press on-press-handler - :style transactions-styles/command-request-image-touchable - :accessibility-label (request-button-label (:name command))} - [react/animated-view {:style (transactions-styles/command-request-image-view command scale-anim-val)} - (when command-icon - [react/icon command-icon transactions-styles/command-request-image])]]))}))) - -(defview request-preview - [{:keys [message-id content outgoing timestamp timestamp-str group-chat]}] - (letsubs [id->command [:get-id->command] - answered? [:is-request-answered? message-id] - status-initialized? [:get :status-module-initialized?] - network [:network-name] - prices [:prices]] - (let [{:keys [amount asset fiat-amount currency] request-network :network} (:params content) - recipient-name (get-in content [:params :bot-db :public :recipient]) - network-mismatch? (and request-network (not= request-network network)) - command (get id->command ["send" #{:personal-chats}]) - on-press-handler (cond - network-mismatch? - nil - (and (not answered?) - status-initialized?) - #(re-frame/dispatch [:select-chat-input-command - command - [(or asset "ETH") amount] - {:responding-to message-id}]))] - [react/view - [react/touchable-highlight - {:on-press on-press-handler} - [react/view (transactions-styles/command-request-message-view outgoing) - [react/view - [react/view - [react/text {:style (transactions-styles/command-request-header-text outgoing)} - (i18n/label :transaction-request)]] - [react/view transactions-styles/command-request-row - [react/text {:style transactions-styles/command-request-amount-text - :font :medium} - amount - [react/text {:style (transactions-styles/command-amount-currency-separator outgoing)} - "."] - [react/text {:style (transactions-styles/command-request-currency-text outgoing) - :font :default} - asset]]] - [react/view transactions-styles/command-request-fiat-amount-row - [react/text {:style transactions-styles/command-request-fiat-amount-text} - (str "~ " fiat-amount " " (or currency (i18n/label :usd-currency)))]] - (when (and group-chat recipient-name) - [react/text {:style transactions-styles/command-request-recipient-text} - (str - (i18n/label :request-requesting-from) - " " - recipient-name)]) - (when network-mismatch? - [react/text {:style transactions-styles/command-request-network-text} - (str (i18n/label :on) " " request-network)]) - [react/view transactions-styles/command-request-timestamp-row - [react/text {:style (transactions-styles/command-request-timestamp-text outgoing)} - (str - (datetime/timestamp->mini-date timestamp) - " " - (i18n/label :at) - " " - timestamp-str)]] - (when-not outgoing - [react/view - [react/view transactions-styles/command-request-separator-line] - [react/view transactions-styles/command-request-button - [react/text {:style (transactions-styles/command-request-button-text answered?) - :on-press on-press-handler} - (i18n/label (if answered? :command-button-sent :command-button-send))]]])]]]]))) - -(deftype PersonalRequestCommand [] - protocol/Command - (id [_] "request") - (scope [_] #{:personal-chats}) - (description [_] "Send a payment") - (parameters [_] personal-send-request-params) - (validate [_ parameters cofx] - (personal-send-request-validation parameters cofx)) - (on-send [_ _ _]) - (on-receive [_ {:keys [message-id chat-id]} {:keys [db]}] - (let [request {:chat-id chat-id - :message-id message-id - :response "send" - :status "open"}] - {:db (assoc-in db [:chats chat-id :requests message-id] request) - :data-store/tx [(requests-store/save-request-tx request)]})) - (short-preview [_ command-message] - (personal-send-request-short-preview :command-requesting command-message)) - (preview [_ command-message] - (request-preview command-message)) - protocol/EnhancedParameters - (enhance-parameters [_ parameters cofx] - (inject-network-price-info parameters cofx))) diff --git a/src/status_im/chat/commands/impl/transactions/styles.cljs b/src/status_im/chat/commands/impl/transactions/styles.cljs deleted file mode 100644 index 375488d0df5..00000000000 --- a/src/status_im/chat/commands/impl/transactions/styles.cljs +++ /dev/null @@ -1,192 +0,0 @@ -(ns status-im.chat.commands.impl.transactions.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :as styles])) - -(def asset-container - {:flex-direction :row - :align-items :center - :justify-content :space-between - :padding-vertical 11}) - -(def asset-main - {:flex 1 - :flex-direction :row - :align-items :center}) - -(def asset-icon - {:width 30 - :height 30 - :margin-left 14 - :margin-right 12}) - -(def asset-symbol - {:color colors/black}) - -(def asset-name - {:color colors/gray - :padding-left 4}) - -(def asset-balance - {:color colors/gray - :padding-right 14}) - -(def asset-separator - {:height 1 - :background-color colors/gray-light - :margin-left 56}) - -(def command-send-status-container - {:margin-top 6 - :flex-direction :row}) - -(defn command-send-status-icon [outgoing] - {:background-color (if outgoing - colors/blue-darker - colors/blue-transparent) - :width 24 - :height 24 - :border-radius 16 - :padding-top 4 - :padding-left 4}) - -(defstyle command-send-status-text - {:color colors/blue - :android {:margin-top 3} - :ios {:margin-top 4} - :margin-left 6 - :font-size 12}) - -(def command-send-message-view - {:flex-direction :column - :align-items :flex-start}) - -(def command-send-amount-row - {:flex-direction :row - :justify-content :space-between}) - -(def command-send-amount - {:flex-direction :column - :align-items :flex-end - :max-width 250}) - -(defstyle command-send-amount-text - {:font-size 22 - :color colors/blue - :ios {:letter-spacing -0.5}}) - -(def command-send-currency - {:flex-direction :column - :align-items :flex-end}) - -(defn command-amount-currency-separator [outgoing] - {:opacity 0 - :color (if outgoing colors/hawkes-blue colors/white)}) - -(defn command-send-currency-text [outgoing] - {:font-size 22 - :margin-left 4 - :letter-spacing 1 - :color (if outgoing colors/wild-blue-yonder colors/blue-transparent-40)}) - -(defn command-request-currency-text [outgoing] - {:font-size 22 - :letter-spacing 1 - :color (if outgoing colors/wild-blue-yonder colors/gray)}) - -(defn command-request-timestamp-text [outgoing] - {:font-size 12 - :color (if outgoing colors/wild-blue-yonder colors/gray)}) - -(def command-send-fiat-amount - {:flex-direction :column - :justify-content :flex-end - :margin-top 6}) - -(def command-send-fiat-amount-text - {:font-size 12 - :color colors/black}) - -(def command-send-recipient-text - {:color colors/blue - :font-size 14 - :line-height 18}) - -(defn command-send-timestamp [outgoing] - {:color (if outgoing colors/wild-blue-yonder colors/gray) - :margin-top 6 - :font-size 12}) - -(def command-request-image-touchable - {:position :absolute - :top 0 - :right -8 - :align-items :center - :justify-content :center - :width 48 - :height 48}) - -(defn command-request-image-view [command scale] - {:width 32 - :height 32 - :border-radius 16 - :background-color (:color command) - :transform [{:scale scale}]}) - -(def command-request-image - {:position :absolute - :top 9 - :left 10 - :width 12 - :height 13}) - -(defn command-request-message-view [outgoing] - {:border-radius 14 - :padding-vertical 4 - :background-color (if outgoing colors/hawkes-blue styles/color-white)}) - -(defn command-request-header-text [outgoing] - {:font-size 12 - :color (if outgoing colors/wild-blue-yonder colors/gray)}) - -(def command-request-row - {:flex-direction :row - :margin-top 6}) - -(defstyle command-request-amount-text - {:font-size 22 - :ios {:letter-spacing -0.5} - :color colors/black}) - -(def command-request-separator-line - {:background-color colors/gray-light - :height 1 - :border-radius 8 - :margin-top 10}) - -(def command-request-button - {:align-items :center - :padding-top 8}) - -(defn command-request-button-text [answered?] - {:font-size 15 - :color (if answered? colors/gray colors/blue)}) - -(def command-request-fiat-amount-row - {:margin-top 6}) - -(def command-request-fiat-amount-text - {:font-size 12 - :color colors/black}) - -(def command-request-recipient-text - {:color colors/blue - :font-size 14 - :line-height 18}) - -(def command-request-network-text - {:color colors/red}) - -(def command-request-timestamp-row - {:margin-top 6}) - diff --git a/src/status_im/chat/commands/protocol.cljs b/src/status_im/chat/commands/protocol.cljs deleted file mode 100644 index e466bb0846d..00000000000 --- a/src/status_im/chat/commands/protocol.cljs +++ /dev/null @@ -1,64 +0,0 @@ -(ns status-im.chat.commands.protocol) - -(def or-scopes - "Scope contexts representing OR choices" - [#{:personal-chats :group-chats :public-chats}]) - -(defprotocol Command - "Protocol for defining command message behaviour" - (id [this] "Identifier of the command, used to look-up command display name as well") - (scope [this] - "Scope of the command, defined as set of values representing contexts - where in which command is available, together with `id` it forms unique - identifier for each command. - Available values for the set are: - `id-of-the-any-chat` - command if available only for the specified chat - `:personal-chats` - command is available for any personal 1-1 chat - `:group-chats` - command is available for any group chat - `:public-chats` - command is available for any public chat ") - (description [this] "Description of the command") - (parameters [this] - "Ordered sequence of command parameter templates, where each parameter - is defined as map consisting of mandatory `:id`, `:title` and `:type` keys, - and optional `:suggestions` field. - When used, `:suggestions` containes reference to any generic helper component - rendering suggestions for the argument (input code will handle when and where - to render it)") - (validate [this parameters cofx] - "Function validating the parameters once command is send. Takes parameters map - and `cofx` map as argument, returns either `nil` meaning that no errors were - found and command send workflow can proceed, or one/more errors to display. - Each error is represented by the map containing `:title` and `:description` keys.") - (on-send [this command-message cofx] - "Function which can provide any extra effects to be produced in addition to - normal message effects which happen whenever message is sent") - (on-receive [this command-message cofx] - "Function which can provide any extre effects to be produced in addition to - normal message effects which happen when particular command message is received") - (short-preview [this command-message] - "Function rendering the short-preview of the command message, used when - displaying the last message in list of chats on home tab. - There is no argument names `parameters` anymore, as the message object - contains everything needed for short-preview/preview to render.") - (preview [this command-message] - "Function rendering preview of the command message in message stream")) - -(defprotocol Yielding - "Protocol for defining commands yielding control back to application during the send flow" - (yield-control [this parameters cofx] - "Function, which steps out of the normal command workflow (`validate-and-send`) - and yields control back to application before sending. - Useful for cases where we want to use command input handling (parameters) and/or - validating, but we don't want to send message before yielding control elsewhere.")) - -(defprotocol EnhancedParameters - "Protocol for command messages which wish to modify/inject additional data into parameters, - other then those collected from the chat input. - Good example would be the `/send` and `/receive` commands - we would like to indicate - network selected in sender's device, but we of course don't want to force user to type - it in when it could be effortlessly looked up from context. - Another usage would be for example command where one of the input parameters will be - hashed after validation and we would want to avoid the original unhashed parameter - to be ever saved on the sender device, nor to be sent over the wire." - (enhance-parameters [this parameters cofx] - "Function which takes original parameters + cofx map and returns new map of parameters")) diff --git a/src/status_im/chat/commands/receiving.cljs b/src/status_im/chat/commands/receiving.cljs deleted file mode 100644 index ad565b3cbf4..00000000000 --- a/src/status_im/chat/commands/receiving.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.chat.commands.receiving - (:require [status-im.chat.commands.protocol :as protocol])) - -;; TODO(janherich) remove after couple of releases when danger of messages -;; with old command references will be low -(def ^:private old->new-path - {["transactor" :command 83 "send"] ["send" #{:personal-chats}] - ["transactor" :command 83 "request"] ["request" #{:personal-chats}]}) - -(defn lookup-command-by-ref - "Function which takes message object and looks up associated entry from the - `id->command` map if it can be found" - [{:keys [content]} id->command] - (when-let [path (or (:command-path content) - (:command-ref content))] - (or (get id->command path) - (get id->command (get old->new-path path))))) - -(defn receive - "Performs receive effects for command message. Does nothing - when message is not of the command type or command can't be found." - [message {:keys [db] :as cofx}] - (let [id->command (:id->command db)] - (when-let [{:keys [type]} (lookup-command-by-ref message id->command)] - (protocol/on-receive type message cofx)))) diff --git a/src/status_im/chat/commands/sending.cljs b/src/status_im/chat/commands/sending.cljs deleted file mode 100644 index ea442c3a5a8..00000000000 --- a/src/status_im/chat/commands/sending.cljs +++ /dev/null @@ -1,78 +0,0 @@ -(ns status-im.chat.commands.sending - (:require [status-im.constants :as constants] - [status-im.chat.commands.protocol :as protocol] - [status-im.chat.commands.core :as commands] - [status-im.chat.models :as chat-model] - [status-im.chat.models.input :as input-model] - [status-im.chat.models.message :as message-model] - [status-im.utils.handlers-macro :as handlers-macro])) - -;; TODO(janherich) remove after couple of releases when danger of messages -;; with old command references will be low -(defn- new->old - [path parameter-map] - (get {["send" #{:personal-chats}] {:content {:command-ref ["transactor" :command 83 "send"] - :command "send" - :bot "transactor" - :command-scope-bitmask 83} - :content-type constants/content-type-command} - ["request" #{:personal-chats}] {:content {:command-ref ["transactor" :command 83 "request"] - :request-command-ref ["transactor" :command 83 "send"] - :command "request" - :request-command "send" - :bot "transactor" - :command-scope-bitmask 83 - :prefill [(get parameter-map :asset) - (get parameter-map :amount)]} - :content-type constants/content-type-command-request}} - path)) - -(defn- create-command-message - "Create message map from chat-id, command & input parameters" - [chat-id type parameter-map cofx] - (let [command-path (commands/command-id type) - ;; TODO(janherich) this is just for backward compatibility, can be removed later - {:keys [content content-type]} (new->old command-path parameter-map)] - {:chat-id chat-id - :content-type content-type - :content (merge {:command-path command-path - :params (if (satisfies? protocol/EnhancedParameters type) - (protocol/enhance-parameters type parameter-map cofx) - parameter-map)} - content)})) - -(defn validate-and-send - "Validates and sends command in current chat" - [input-text {:keys [type params] :as command} {:keys [db now random-id] :as cofx}] - (let [chat-id (:current-chat-id db) - parameter-map (commands/parse-parameters params input-text)] - (if-let [validation-error (protocol/validate type parameter-map cofx)] - ;; errors during validation - {:db (chat-model/set-chat-ui-props db {:validation-messages validation-error - :sending-in-progress? false})} - ;; no errors, define clean-up effects which will have to be performed in all cases - (let [cleanup-fx (handlers-macro/merge-fx cofx - {:db (chat-model/set-chat-ui-props - db {:sending-in-progress? false})} - (input-model/set-chat-input-text nil))] - (if (satisfies? protocol/Yielding type) - ;; yield control implemented, don't send the message - (handlers-macro/merge-fx cofx - cleanup-fx - (protocol/yield-control type parameter-map)) - ;; no yield control, proceed with sending the command message - (let [command-message (create-command-message chat-id type parameter-map cofx)] - (handlers-macro/merge-fx cofx - cleanup-fx - (protocol/on-send type command-message) - (input-model/set-chat-input-metadata nil) - (message-model/send-message command-message)))))))) - -(defn send - "Sends command with given parameters in particular chat" - [chat-id {:keys [type]} parameter-map cofx] - (let [command-message (create-command-message chat-id type parameter-map cofx)] - (handlers-macro/merge-fx cofx - (protocol/on-send type command-message) - (input-model/set-chat-input-metadata nil) - (message-model/send-message command-message)))) diff --git a/src/status_im/chat/commands/specs.cljs b/src/status_im/chat/commands/specs.cljs deleted file mode 100644 index 25814e6ec2f..00000000000 --- a/src/status_im/chat/commands/specs.cljs +++ /dev/null @@ -1,5 +0,0 @@ -(ns status-im.chat.commands.specs - (:require [cljs.spec.alpha :as spec])) - -(spec/def :chat/id->command (spec/nilable map?)) -(spec/def :chat/access-scope->command-id (spec/nilable map?)) diff --git a/src/status_im/chat/commands/styles/validation.cljs b/src/status_im/chat/commands/styles/validation.cljs deleted file mode 100644 index e993788de02..00000000000 --- a/src/status_im/chat/commands/styles/validation.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.chat.commands.styles.validation - (:require [status-im.ui.components.styles :as common])) - -(defn root [bottom] - {:flex-direction :column - :left 0 - :right 0 - :bottom bottom - :position :absolute}) - -(def message-container - {:background-color common/color-red - :padding 16}) - -(def message-title - {:color common/color-white - :font-size 12}) - -(def message-description - {:color common/color-white - :font-size 12 - :opacity 0.9}) diff --git a/src/status_im/chat/constants.cljs b/src/status_im/chat/constants.cljs deleted file mode 100644 index 7f151758492..00000000000 --- a/src/status_im/chat/constants.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.chat.constants) - -(def command-char "/") -(def spacing-char " ") -(def arg-wrapping-char "\"") - -(def input-height 56) -(def input-spacing-top 16) - -(def console-chat-id "console") - -(def spam-message-frequency-threshold 4) -(def spam-interval-ms 1000) -(def default-cooldown-period-ms 10000) -(def cooldown-reset-threshold 3) -(def cooldown-periods-ms - {1 2000 - 2 5000 - 3 10000}) diff --git a/src/status_im/chat/core.cljs b/src/status_im/chat/core.cljs deleted file mode 100644 index 4dc61b6bfe8..00000000000 --- a/src/status_im/chat/core.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.chat.core - (:require [status-im.data-store.user-statuses :as user-statuses-store])) - -;; Seen messages -(defn receive-seen - [chat-id sender {:keys [message-ids]} {:keys [db js-obj]}] - (merge - {:confirm-messages-processed [{:web3 (:web3 db) - :js-obj js-obj}]} - (when-let [seen-messages-ids (-> (get-in db [:chats chat-id :messages]) - (select-keys message-ids) - keys)] - (let [statuses (map (fn [message-id] - {:chat-id chat-id - :message-id message-id - :whisper-identity sender - :status :seen}) - seen-messages-ids)] - {:db (reduce (fn [acc {:keys [message-id] :as status}] - (assoc-in acc [:chats chat-id :message-statuses - message-id sender] - status)) - db - statuses) - :data-store/tx [(user-statuses-store/save-statuses-tx statuses)]})))) diff --git a/src/status_im/chat/events.cljs b/src/status_im/chat/events.cljs deleted file mode 100644 index 95d806eb1a1..00000000000 --- a/src/status_im/chat/events.cljs +++ /dev/null @@ -1,402 +0,0 @@ -(ns status-im.chat.events - (:require [clojure.set :as set] - [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.i18n :as i18n] - [status-im.chat.models :as models] - [status-im.chat.models.message :as models.message] - [status-im.chat.commands.core :as commands] - [status-im.ui.screens.navigation :as navigation] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.contacts :as utils.contacts] - [status-im.utils.utils :as utils] - [status-im.transport.message.core :as transport.message] - [status-im.transport.message.v1.protocol :as protocol] - [status-im.transport.message.v1.public-chat :as public-chat] - [status-im.transport.message.v1.group-chat :as group-chat] - [status-im.data-store.chats :as chats-store] - [status-im.data-store.messages :as messages-store] - [status-im.data-store.user-statuses :as user-statuses-store] - [status-im.data-store.contacts :as contacts-store] - status-im.chat.events.input - status-im.chat.events.requests - status-im.chat.events.send-message - status-im.chat.events.receive-message)) - -;;;; Effects - - -(re-frame/reg-fx - :show-cooldown-warning - (fn [_] - (utils/show-popup nil - (i18n/label :cooldown/warning-message) - #()))) - -;;;; Handlers - -(handlers/register-handler-db - :set-chat-ui-props - [re-frame/trim-v] - (fn [db [kvs]] - (models/set-chat-ui-props db kvs))) - -(handlers/register-handler-db - :toggle-chat-ui-props - [re-frame/trim-v] - (fn [db [ui-element]] - (models/toggle-chat-ui-prop db ui-element))) - -(handlers/register-handler-db - :show-message-details - [re-frame/trim-v] - (fn [db [details]] - (models/set-chat-ui-props db {:show-bottom-info? true - :bottom-info details}))) - -(handlers/register-handler-db - :show-message-options - [re-frame/trim-v] - (fn [db [options]] - (models/set-chat-ui-props db {:show-message-options? true - :message-options options}))) - -(def index-messages (partial into {} (map (juxt :message-id identity)))) - -(handlers/register-handler-db - :message-appeared - [re-frame/trim-v] - (fn [db [{:keys [chat-id message-id]}]] - (update-in db [:chats chat-id :messages message-id] assoc :appearing? false))) - -(handlers/register-handler-fx - :update-message-status - [re-frame/trim-v] - (fn [{:keys [db]} [chat-id message-id user-id status]] - (let [new-status {:chat-id chat-id - :message-id message-id - :whisper-identity user-id - :status status}] - {:db (assoc-in db - [:chats chat-id :message-statuses message-id user-id] - new-status) - :data-store/tx [(user-statuses-store/save-status-tx new-status)]}))) - -;; Change status of own messages which are still in "sending" status to "not-sent" -;; (If signal from status-go has not been received) -(handlers/register-handler-fx - :process-pending-messages - [re-frame/trim-v] - (fn [{:keys [db]} []] - (let [me (:current-public-key db) - pending-statuses (->> (vals (:chats db)) - (mapcat :message-statuses) - (mapcat (fn [[_ user-id->status]] - (filter (comp (partial = :sending) :status) - (get user-id->status me))))) - updated-statuses (map #(assoc % :status :not-sent) pending-statuses)] - {:data-store/tx [(user-statuses-store/save-statuses-tx updated-statuses)] - :db (reduce - (fn [acc {:keys [chat-id message-id status whisper-identity]}] - (assoc-in acc - [:chats chat-id :message-status message-id - whisper-identity :status] - status)) - db - updated-statuses)}))) - -(defn- add-default-contacts - [{:keys [db default-contacts] :as cofx}] - (let [new-contacts (-> {} - (into (map (fn [[id props]] - (let [contact-id (name id)] - [contact-id {:whisper-identity contact-id - :address (utils.contacts/public-key->address contact-id) - :name (-> props :name :en) - :photo-path (:photo-path props) - :public-key (:public-key props) - :unremovable? (-> props :unremovable? boolean) - :hide-contact? (-> props :hide-contact? boolean) - :pending? (-> props :pending? boolean) - :dapp? (:dapp? props) - :dapp-url (-> props :dapp-url :en) - :bot-url (:bot-url props) - :description (:description props)}]))) - default-contacts)) - existing-contacts (:contacts/contacts db) - contacts-to-add (select-keys new-contacts (set/difference (set (keys new-contacts)) - (set (keys existing-contacts))))] - {:db (update db :contacts/contacts merge contacts-to-add) - :data-store/tx [(contacts-store/save-contacts-tx (vals contacts-to-add))]})) - -(defn- group-chat-messages - [{:keys [db]}] - (reduce-kv (fn [fx chat-id {:keys [messages]}] - (models.message/group-messages chat-id (vals messages) fx)) - {:db db} - (:chats db))) - -(handlers/register-handler-fx - :initialize-chats - [(re-frame/inject-cofx :get-default-contacts) - (re-frame/inject-cofx :get-default-dapps) - (re-frame/inject-cofx :data-store/all-chats) - (re-frame/inject-cofx :data-store/get-messages) - (re-frame/inject-cofx :data-store/get-user-statuses) - (re-frame/inject-cofx :data-store/unviewed-messages) - (re-frame/inject-cofx :data-store/message-ids) - (re-frame/inject-cofx :data-store/get-unanswered-requests) - (re-frame/inject-cofx :data-store/get-local-storage-data)] - (fn [{:keys [db - default-dapps - all-stored-chats - stored-unanswered-requests - get-stored-messages - get-stored-user-statuses - stored-unviewed-messages - stored-message-ids] :as cofx} _] - (let [chat->message-id->request (reduce (fn [acc {:keys [chat-id message-id] :as request}] - (assoc-in acc [chat-id message-id] request)) - {} - stored-unanswered-requests) - chats (reduce (fn [acc {:keys [chat-id] :as chat}] - (let [chat-messages (index-messages (get-stored-messages chat-id)) - message-ids (keys chat-messages) - unviewed-ids (get stored-unviewed-messages chat-id)] - (assoc acc chat-id - (assoc chat - :unviewed-messages unviewed-ids - :requests (get chat->message-id->request chat-id) - :messages chat-messages - :message-statuses (get-stored-user-statuses chat-id message-ids) - :not-loaded-message-ids (set/difference (get stored-message-ids chat-id) - (set message-ids)))))) - {} - all-stored-chats)] - (handlers-macro/merge-fx cofx - {:db (assoc db - :chats chats - :contacts/dapps default-dapps)} - (group-chat-messages) - (add-default-contacts) - (commands/index-commands commands/register))))) - -(defn- send-messages-seen [chat-id message-ids {:keys [db] :as cofx}] - (when (and (not (get-in db [:chats chat-id :public?])) - (not (models/bot-only-chat? db chat-id))) - (transport.message/send (protocol/map->MessagesSeen {:message-ids message-ids}) chat-id cofx))) - -;; TODO (janherich) - ressurect `constants/system` messages for group chats in the future -(defn mark-messages-seen - [chat-id {:keys [db] :as cofx}] - (when-let [all-unviewed-ids (seq (get-in db [:chats chat-id :unviewed-messages]))] - (let [me (:current-public-key db) - updated-statuses (keep (fn [message-id] - (some-> db - (get-in [:chats chat-id :message-statuses - message-id me]) - (assoc :status :seen))) - all-unviewed-ids) - loaded-unviewed-ids (map :message-id updated-statuses)] - (when (seq loaded-unviewed-ids) - (handlers-macro/merge-fx - cofx - {:db (-> (reduce (fn [acc {:keys [message-id status]}] - (assoc-in acc [:chats chat-id :message-statuses - message-id me :status] - status)) - db - updated-statuses) - (update-in [:chats chat-id :unviewed-messages] - #(apply disj % loaded-unviewed-ids))) - :data-store/tx [(user-statuses-store/save-statuses-tx updated-statuses)]} - (send-messages-seen chat-id loaded-unviewed-ids)))))) - -(defn- fire-off-chat-loaded-event - [chat-id {:keys [db]}] - (when-let [event (get-in db [:chats chat-id :chat-loaded-event])] - {:db (update-in db [:chats chat-id] dissoc :chat-loaded-event) - :dispatch event})) - -(defn- preload-chat-data - "Takes chat-id and coeffects map, returns effects necessary when navigating to chat" - [chat-id {:keys [db] :as cofx}] - (handlers-macro/merge-fx cofx - {:db (-> (assoc db :current-chat-id chat-id) - (models/set-chat-ui-props {:validation-messages nil}))} - (fire-off-chat-loaded-event chat-id) - (mark-messages-seen chat-id))) - -(handlers/register-handler-fx - :add-chat-loaded-event - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [chat-id event]] - (if (get (:chats db) chat-id) - {:db (assoc-in db [:chats chat-id :chat-loaded-event] event)} - (-> (models/upsert-chat {:chat-id chat-id} cofx) ; chat not created yet, we have to create it - (assoc-in [:db :chats chat-id :chat-loaded-event] event))))) - -(defn- navigate-to-chat - "Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data" - [chat-id {:keys [navigation-replace?]} {:keys [db] :as cofx}] - (if navigation-replace? - (handlers-macro/merge-fx cofx - (navigation/replace-view :chat) - (preload-chat-data chat-id)) - (handlers-macro/merge-fx cofx - ;; TODO janherich - refactor `navigate-to` so it can be used with `merge-fx` macro - {:db (navigation/navigate-to db :chat)} - (preload-chat-data chat-id)))) - -(handlers/register-handler-fx - :navigate-to-chat - [re-frame/trim-v] - (fn [cofx [chat-id opts]] - (navigate-to-chat chat-id opts cofx))) - -(handlers/register-handler-fx - :load-more-messages - [(re-frame/inject-cofx :data-store/get-messages) - (re-frame/inject-cofx :data-store/get-user-statuses)] - (fn [{{:keys [current-chat-id] :as db} :db - get-stored-messages :get-stored-messages - get-stored-user-statuses :get-stored-user-statuses :as cofx} _] - (when-not (get-in db [:chats current-chat-id :all-loaded?]) - (let [loaded-count (count (get-in db [:chats current-chat-id :messages])) - new-messages (get-stored-messages current-chat-id loaded-count) - indexed-messages (index-messages new-messages) - new-message-ids (keys indexed-messages) - new-statuses (get-stored-user-statuses current-chat-id new-message-ids)] - (handlers-macro/merge-fx - cofx - {:db (-> db - (update-in [:chats current-chat-id :messages] merge indexed-messages) - (update-in [:chats current-chat-id :message-statuses] merge new-statuses) - (update-in [:chats current-chat-id :not-loaded-message-ids] - #(apply disj % new-message-ids)) - (assoc-in [:chats current-chat-id :all-loaded?] - (> constants/default-number-of-messages (count new-messages))))} - (models.message/group-messages current-chat-id new-messages) - (mark-messages-seen current-chat-id)))))) - -(defn start-chat - "Start a chat, making sure it exists" - [chat-id opts {:keys [db] :as cofx}] - ;; don't allow to open chat with yourself - (when (not= (:current-public-key db) chat-id) - (handlers-macro/merge-fx cofx - (models/upsert-chat {:chat-id chat-id - :is-active true}) - (navigate-to-chat chat-id opts)))) - -(handlers/register-handler-fx - :start-chat - [re-frame/trim-v] - (fn [cofx [contact-id opts]] - (start-chat contact-id opts cofx))) - -;; TODO(janherich): remove this unnecessary event in the future (only model function `update-chat` will stay) -(handlers/register-handler-fx - :update-chat! - [re-frame/trim-v] - (fn [cofx [chat]] - (models/upsert-chat chat cofx))) - -(defn remove-chat-and-navigate-home [cofx [chat-id]] - (handlers-macro/merge-fx cofx - (models/remove-chat chat-id) - (navigation/replace-view :home))) - -(handlers/register-handler-fx - :remove-chat-and-navigate-home - [re-frame/trim-v] - remove-chat-and-navigate-home) - -(handlers/register-handler-fx - :remove-chat-and-navigate-home? - [re-frame/trim-v] - (fn [_ [chat-id group?]] - {:show-confirmation {:title (i18n/label :t/delete-confirmation) - :content (i18n/label :t/delete-chat-confirmation) - :confirm-button-text (i18n/label :t/delete) - :on-accept #(re-frame/dispatch [:remove-chat-and-navigate-home chat-id])}})) - -(handlers/register-handler-fx - :clear-history - (fn [{{:keys [current-chat-id]} :db :as cofx} _] - (models/clear-history current-chat-id cofx))) - -(handlers/register-handler-fx - :clear-history? - (fn [_ _] - {:show-confirmation {:title (i18n/label :t/clear-history-confirmation) - :content (i18n/label :t/clear-history-confirmation-content) - :confirm-button-text (i18n/label :t/clear) - :on-accept #(re-frame/dispatch [:clear-history])}})) - -(defn create-new-public-chat [topic {:keys [db now] :as cofx}] - (handlers-macro/merge-fx cofx - (models/add-public-chat topic) - (navigation/navigate-to-clean :home) - (navigate-to-chat topic {}) - (public-chat/join-public-chat topic))) - -(handlers/register-handler-fx - :create-new-public-chat - [re-frame/trim-v] - (fn [cofx [topic]] - (create-new-public-chat topic cofx))) - -(defn- group-name-from-contacts [selected-contacts all-contacts username] - (->> selected-contacts - (map (comp :name (partial get all-contacts))) - (cons username) - (string/join ", "))) - -(handlers/register-handler-fx - :create-new-group-chat-and-open - [re-frame/trim-v (re-frame/inject-cofx :random-id)] - (fn [{:keys [db random-id] :as cofx} [group-name]] - (let [selected-contacts (:group/selected-contacts db) - chat-name (if-not (string/blank? group-name) - group-name - (group-name-from-contacts selected-contacts - (:contacts/contacts db) - (:username db)))] - (handlers-macro/merge-fx cofx - {:db (assoc db :group/selected-contacts #{})} - (models/add-group-chat random-id chat-name (:current-public-key db) selected-contacts) - (navigation/navigate-to-clean :home) - (navigate-to-chat random-id {}) - (transport.message/send (group-chat/GroupAdminUpdate. chat-name selected-contacts) random-id))))) - -(defn show-profile [identity keep-navigation? {:keys [db] :as cofx}] - (cond->> {:db (assoc db :contacts/identity identity)} - keep-navigation? (navigation/navigate-to-cofx :profile nil) - :else (navigation/navigate-forget :profile))) - -(handlers/register-handler-fx - :show-profile - [re-frame/trim-v] - (fn [cofx [identity keep-navigation?]] - (show-profile identity keep-navigation? cofx))) - -(handlers/register-handler-fx - :resend-message - [re-frame/trim-v] - (fn [cofx [chat-id message-id]] - (models.message/resend-message chat-id message-id cofx))) - -(handlers/register-handler-fx - :delete-message - [re-frame/trim-v] - (fn [cofx [chat-id message-id]] - (models.message/delete-message chat-id message-id cofx))) - -(handlers/register-handler-db - :disable-cooldown - [re-frame/trim-v] - (fn [db] - (assoc db :chat/cooldown-enabled? false))) diff --git a/src/status_im/chat/events/input.cljs b/src/status_im/chat/events/input.cljs deleted file mode 100644 index 1f93f0a305f..00000000000 --- a/src/status_im/chat/events/input.cljs +++ /dev/null @@ -1,144 +0,0 @@ -(ns status-im.chat.events.input - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.constants :as constants] - [status-im.chat.constants :as chat-constants] - [status-im.chat.models :as model] - [status-im.chat.models.input :as input-model] - [status-im.chat.models.message :as message-model] - [status-im.chat.commands.core :as commands] - [status-im.chat.commands.sending :as commands-sending] - [status-im.ui.components.react :as react-comp] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro])) - -;;;; Effects - -(re-frame/reg-fx - ::focus-rn-component - (fn [ref] - (try - (.focus ref) - (catch :default e - (log/debug "Cannot focus the reference"))))) - -(re-frame/reg-fx - ::blur-rn-component - (fn [ref] - (try - (.blur ref) - (catch :default e - (log/debug "Cannot blur the reference"))))) - -(re-frame/reg-fx - ::dismiss-keyboard - (fn [_] - (react-comp/dismiss-keyboard!))) - -(re-frame/reg-fx - ::set-native-props - (fn [{:keys [ref props]}] - (.setNativeProps ref (clj->js props)))) - -;;;; Helper functions - -(defn chat-input-focus - "Returns fx for focusing on active chat input reference" - [ref {{:keys [current-chat-id chat-ui-props]} :db}] - (when-let [cmp-ref (get-in chat-ui-props [current-chat-id ref])] - {::focus-rn-component cmp-ref})) - -;;;; Handlers - -(handlers/register-handler-fx - :set-chat-input-text - [re-frame/trim-v] - (fn [cofx [text]] - (input-model/set-chat-input-text text cofx))) - -(handlers/register-handler-fx - :select-chat-input-command - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [command params metadata]] - (handlers-macro/merge-fx cofx - (input-model/set-chat-input-metadata metadata) - (commands/select-chat-input-command command params) - (chat-input-focus :input-ref)))) - -(handlers/register-handler-fx - :set-command-parameter - [re-frame/trim-v] - (fn [cofx [last-param? index value]] - (commands/set-command-parameter last-param? index value cofx))) - -(handlers/register-handler-fx - :chat-input-focus - [re-frame/trim-v] - (fn [cofx [ref]] - (chat-input-focus ref cofx))) - -(handlers/register-handler-fx - :chat-input-blur - [re-frame/trim-v] - (fn [{{:keys [current-chat-id chat-ui-props]} :db} [ref]] - (when-let [cmp-ref (get-in chat-ui-props [current-chat-id ref])] - {::blur-rn-component cmp-ref}))) - -(defn command-complete-fx - "command is complete, set `:sending-in-progress?` flag and proceed with command processing" - [input-text command {:keys [db now random-id] :as cofx}] - (handlers-macro/merge-fx - cofx - {:db (model/set-chat-ui-props db {:sending-in-progress? true})} - (commands-sending/validate-and-send input-text command))) - -(defn command-not-complete-fx - "command is not complete, just add space after command if necessary" - [input-text current-chat-id {:keys [db]}] - {:db (cond-> db - (not (input-model/text-ends-with-space? input-text)) - (assoc-in [:chats current-chat-id :input-text] - (str input-text chat-constants/spacing-char)))}) - -(defn plain-text-message-fx - "no command detected, when not empty, proceed by sending text message without command processing" - [input-text current-chat-id {:keys [db] :as cofx}] - (when-not (string/blank? input-text) - (handlers-macro/merge-fx - cofx - (input-model/set-chat-input-text nil) - (input-model/set-chat-input-metadata nil) - (message-model/send-message {:chat-id current-chat-id - :content-type constants/text-content-type - :content input-text})))) - -(handlers/register-handler-fx - :send-current-message - message-model/send-interceptors - (fn [{{:keys [current-chat-id id->command access-scope->command-id] :as db} :db :as cofx} _] - (when-not (get-in db [:chat-ui-props current-chat-id :sending-in-progress?]) - (let [input-text (get-in db [:chats current-chat-id :input-text]) - command (commands/selected-chat-command - input-text nil (commands/chat-commands id->command - access-scope->command-id - (get-in db [:chats current-chat-id])))] - (if command - ;; Returns true if current input contains command - (if (= :complete (:command-completion command)) - (command-complete-fx input-text command cofx) - (command-not-complete-fx input-text current-chat-id cofx)) - (plain-text-message-fx input-text current-chat-id cofx)))))) - -(handlers/register-handler-db - :update-text-selection - [re-frame/trim-v] - (fn [db [selection]] - (model/set-chat-ui-props db {:selection selection}))) - -(handlers/register-handler-db - :show-suggestions - (fn [db _] - (-> db - (model/toggle-chat-ui-prop :show-suggestions?) - (model/set-chat-ui-props {:validation-messages nil})))) diff --git a/src/status_im/chat/events/receive_message.cljs b/src/status_im/chat/events/receive_message.cljs deleted file mode 100644 index 9e0efc3b893..00000000000 --- a/src/status_im/chat/events/receive_message.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.chat.events.receive-message - (:require [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.chat.models.message :as message-model] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.wallet.transactions :as wallet.transactions])) - -;;;; Handlers - -(re-frame.core/reg-fx - :chat-received-message/add-fx - (fn [messages] - (re-frame/dispatch [:chat-received-message/add messages]))) - -(defn- filter-messages [messages cofx] - (:accumulated (reduce (fn [{:keys [seen-ids] :as acc} - {:keys [message-id] :as message}] - (if (and (message-model/add-to-chat? cofx message) - (not (seen-ids message-id))) - (-> acc - (update :seen-ids conj message-id) - (update :accumulated conj message)) - acc)) - {:seen-ids #{} - :accumulated []} - messages))) - -(handlers/register-handler-fx - :chat-received-message/add - message-model/receive-interceptors - (fn [cofx [messages]] - (message-model/receive-many (filter-messages messages cofx) cofx))) - diff --git a/src/status_im/chat/events/requests.cljs b/src/status_im/chat/events/requests.cljs deleted file mode 100644 index f6134938900..00000000000 --- a/src/status_im/chat/events/requests.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.chat.events.requests - (:require [status-im.constants :as constants] - [status-im.data-store.requests :as requests-store])) - -;; Functions - -(defn request-answered - "Takes chat-id, message-id and cofx, returns fx necessary data for marking request as answered" - [chat-id message-id {:keys [db]}] - (when message-id - {:db (update-in db [:chats chat-id :requests] dissoc message-id) - :data-store/tx [(requests-store/mark-request-as-answered-tx chat-id message-id)]})) - -(defn add-request - "Takes chat-id, message-id + cofx and returns fx with necessary data for adding new request" - [chat-id message-id {:keys [db]}] - (let [{:keys [content-type content]} (get-in db [:chats chat-id :messages message-id])] - (when (= content-type constants/content-type-command-request) - (let [request {:chat-id chat-id - :message-id message-id - :response (:request-command content) - :status "open"}] - {:db (assoc-in db [:chats chat-id :requests message-id] request) - :data-store/tx [(requests-store/save-request-tx request)]})))) diff --git a/src/status_im/chat/events/send_message.cljs b/src/status_im/chat/events/send_message.cljs deleted file mode 100644 index 8ad40fcd1e8..00000000000 --- a/src/status_im/chat/events/send_message.cljs +++ /dev/null @@ -1,15 +0,0 @@ -(ns status-im.chat.events.send-message - (:require [taoensso.timbre :as log] - [re-frame.core :as re-frame] - [status-im.chat.models.message :as message-model] - [status-im.native-module.core :as status] - [status-im.utils.handlers :as handlers] - [status-im.utils.types :as types])) - -(re-frame/reg-fx - :send-notification - (fn [{:keys [message payload tokens]}] - (let [payload-json (types/clj->json payload) - tokens-json (types/clj->json tokens)] - (log/debug "send-notification message: " message " payload-json: " payload-json " tokens-json: " tokens-json) - (status/notify-users {:message message :payload payload-json :tokens tokens-json} #(log/debug "send-notification cb result: " %))))) diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs deleted file mode 100644 index c2b0357f2fe..00000000000 --- a/src/status_im/chat/models.cljs +++ /dev/null @@ -1,127 +0,0 @@ -(ns status-im.chat.models - (:require [status-im.ui.components.styles :as styles] - [status-im.utils.gfycat.core :as gfycat] - [status-im.transport.utils :as transport.utils] - [status-im.utils.clocks :as utils.clocks] - [status-im.transport.message.core :as transport.message] - [status-im.data-store.chats :as chats-store] - [status-im.transport.message.v1.group-chat :as transport.group-chat] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.data-store.messages :as messages-store])) - -(defn multi-user-chat? [chat-id cofx] - (get-in cofx [:db :chats chat-id :group-chat])) - -(defn group-chat? [chat-id cofx] - (and (multi-user-chat? chat-id cofx) - (not (get-in cofx [:db :chats chat-id :public?])))) - -(defn public-chat? [chat-id cofx] - (get-in cofx [:db :chats chat-id :public?])) - -(defn set-chat-ui-props - "Updates ui-props in active chat by merging provided kvs into them" - [{:keys [current-chat-id] :as db} kvs] - (update-in db [:chat-ui-props current-chat-id] merge kvs)) - -(defn toggle-chat-ui-prop - "Toggles chat ui prop in active chat" - [{:keys [current-chat-id] :as db} ui-element] - (update-in db [:chat-ui-props current-chat-id ui-element] not)) - -(defn- create-new-chat - [chat-id {:keys [db now]}] - (let [name (get-in db [:contacts/contacts chat-id :name])] - {:chat-id chat-id - :name (or name (gfycat/generate-gfy chat-id)) - :color (styles/random-chat-color) - :group-chat false - :is-active true - :timestamp now - :contacts [chat-id] - :last-clock-value 0})) - -(defn upsert-chat - "Upsert chat when not deleted" - [{:keys [chat-id] :as chat-props} {:keys [db] :as cofx}] - (let [chat (merge - (or (get (:chats db) chat-id) - (create-new-chat chat-id cofx)) - chat-props)] - - (if (:is-active chat) - {:db (update-in db [:chats chat-id] merge chat) - :data-store/tx [(chats-store/save-chat-tx chat)]} - ;; when chat is deleted, don't change anything - {:db db}))) - -(defn add-public-chat - "Adds new public group chat to db & realm" - [topic cofx] - (upsert-chat {:chat-id topic - :is-active true - :name topic - :group-chat true - :contacts [] - :public? true} cofx)) - -(defn add-group-chat - "Adds new private group chat to db & realm" - [chat-id chat-name admin participants cofx] - (upsert-chat {:chat-id chat-id - :name chat-name - :is-active true - :group-chat true - :group-admin admin - :contacts participants} cofx)) - -(defn clear-history [chat-id {:keys [db] :as cofx}] - (let [{:keys [messages - deleted-at-clock-value]} (get-in db [:chats chat-id]) - last-message-clock-value (or (->> messages - vals - (sort-by (comp unchecked-negate :clock-value)) - first - :clock-value) - deleted-at-clock-value - (utils.clocks/send 0))] - {:db (update-in db [:chats chat-id] merge - {:messages {} - :message-groups {} - :unviewed-messages #{} - :not-loaded-message-ids #{} - :deleted-at-clock-value last-message-clock-value}) - :data-store/tx [(chats-store/clear-history-tx chat-id last-message-clock-value) - (messages-store/delete-messages-tx chat-id)]})) - -(defn- remove-transport [chat-id {:keys [db] :as cofx}] - ;; if this is private group chat, we have to broadcast leave and unsubscribe after that - (if (group-chat? chat-id cofx) - (transport.message/send (transport.group-chat/GroupLeave.) chat-id cofx) - (transport.utils/unsubscribe-from-chat chat-id cofx))) - -(defn- deactivate-chat [chat-id {:keys [db now] :as cofx}] - (assoc-in {:db db - :data-store/tx [(chats-store/deactivate-chat-tx chat-id now)]} - [:db :chats chat-id :is-active] false)) - -;; TODO: There's a race condition here, as the removal of the filter (async) -;; is done at the same time as the removal of the chat, so a message -;; might come between and restore the chat. Multiple way to handle this -;; (remove chat only after the filter has been removed, probably the safest, -;; flag the chat to ignore new messages, change receive method for public/group chats) -;; For now to keep the code simplier and avoid significant changes, best to leave as it is. -(defn remove-chat [chat-id {:keys [db now] :as cofx}] - (letfn [(remove-transport-fx [chat-id cofx] - (when (multi-user-chat? chat-id cofx) - (remove-transport chat-id cofx)))] - (handlers-macro/merge-fx - cofx - (remove-transport-fx chat-id) - (deactivate-chat chat-id) - (clear-history chat-id)))) - -(defn bot-only-chat? [db chat-id] - (let [{:keys [group-chat contacts]} (get-in db [:chats chat-id])] - (and (not group-chat) - (get-in db [:contacts/contacts (first contacts) :dapp?])))) diff --git a/src/status_im/chat/models/group_chat.cljs b/src/status_im/chat/models/group_chat.cljs deleted file mode 100644 index 46d999a018d..00000000000 --- a/src/status_im/chat/models/group_chat.cljs +++ /dev/null @@ -1,77 +0,0 @@ -(ns status-im.chat.models.group-chat - (:require - [clojure.set :as set] - [status-im.i18n :as i18n] - [status-im.transport.utils :as transport.utils] - [status-im.ui.screens.group.core :as group] - [status-im.chat.models :as models.chat] - [status-im.transport.message.core :as message] - [status-im.transport.message.v1.group-chat :as transport.group-chat] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.chat.models.message :as models.message])) - -(defn- participants-diff [existing-participants-set new-participants-set] - {:removed (set/difference existing-participants-set new-participants-set) - :added (set/difference new-participants-set existing-participants-set)}) - -(defn- prepare-system-message [admin-name added-participants removed-participants contacts] - (let [added-participants-names (map #(get-in contacts [% :name] %) added-participants) - removed-participants-names (map #(get-in contacts [% :name] %) removed-participants)] - (cond - (and (seq added-participants) (seq removed-participants)) - (str admin-name " " - (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names)) - " and " - (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names))) - - (seq added-participants) - (str admin-name " " (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names))) - - (seq removed-participants) - (str admin-name " " (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names)))))) - -(defn handle-group-admin-update [{:keys [chat-name participants]} chat-id signature {:keys [now db random-id] :as cofx}] - (let [me (:current-public-key db)] - ;; we have to check if we already have a chat, or it's a new one - (if-let [{:keys [group-admin contacts] :as chat} (get-in db [:chats chat-id])] - ;; update for existing group chat - (when (and (= signature group-admin) ;; make sure that admin is the one making changes - (not= (set contacts) (set participants))) ;; make sure it's actually changing something - (let [{:keys [removed added]} (participants-diff (set contacts) (set participants)) - admin-name (or (get-in db [:contacts/contacts group-admin :name]) - group-admin)] - (if (removed me) ;; we were removed - (handlers-macro/merge-fx cofx - (models.message/receive - (models.message/system-message chat-id random-id now - (str admin-name " " (i18n/label :t/removed-from-chat)))) - (models.chat/upsert-chat {:chat-id chat-id - :removed-from-at now - :is-active false}) - (transport.utils/unsubscribe-from-chat chat-id)) - (handlers-macro/merge-fx cofx - (models.message/receive - (models.message/system-message chat-id random-id now - (prepare-system-message admin-name - added - removed - (:contacts/contacts db)))) - (group/participants-added chat-id added) - (group/participants-removed chat-id removed))))) - ;; first time we hear about chat -> create it if we are among participants - (when (get (set participants) me) - (models.chat/add-group-chat chat-id chat-name signature participants cofx))))) - -(defn handle-group-leave [chat-id signature {:keys [db random-id now] :as cofx}] - (let [me (:current-public-key db) - participant-leaving-name (or (get-in db [:contacts/contacts signature :name]) - signature)] - (when (and - (not= signature me) - (get-in db [:chats chat-id])) ;; chat is present - (handlers-macro/merge-fx cofx - (models.message/receive - (models.message/system-message chat-id random-id now - (str participant-leaving-name " " (i18n/label :t/left)))) - (group/participants-removed chat-id #{signature}) - (transport.group-chat/send-new-group-key nil chat-id))))) diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs deleted file mode 100644 index 4eea74a02b7..00000000000 --- a/src/status_im/chat/models/input.cljs +++ /dev/null @@ -1,134 +0,0 @@ -(ns status-im.chat.models.input - (:require [clojure.string :as str] - [goog.object :as object] - [status-im.chat.constants :as const] - [status-im.chat.models :as chat-model] - [status-im.utils.config :as config] - [status-im.utils.datetime :as datetime] - [status-im.js-dependencies :as dependencies])) - -(def space-char " ") - -(defn text->emoji - "Replaces emojis in a specified `text`" - [text] - (when text - (str/replace text - #":([a-z_\-+0-9]*):" - (fn [[original emoji-id]] - (if-let [emoji-map (object/get (object/get dependencies/emojis "lib") emoji-id)] - (object/get emoji-map "char") - original))))) - -(defn text-ends-with-space? [text] - (and text (str/ends-with? text const/spacing-char))) - -(defn starts-as-command? - "Returns true if `text` may be treated as a command. - To make sure that text is command we need to use `possible-chat-actions` function." - [text] - (and text (str/starts-with? text const/command-char))) - -(defn split-command-args - "Returns a list of command's arguments including the command's name. - - Examples: - Input: '/send Jarrad 1.0' - Output: ['/send' 'Jarrad' '1.0'] - - Input: '/send \"Complex name with space in between\" 1.0' - Output: ['/send' 'Complex name with space in between' '1.0'] - - All the complex logic inside this function aims to support wrapped arguments." - [command-text] - (when command-text - (let [space? (text-ends-with-space? command-text) - command-text (if space? - (str command-text ".") - command-text) - command-text-normalized (if command-text - (str/replace (str/trim command-text) #" +" " ") - command-text) - splitted (cond-> (str/split command-text-normalized const/spacing-char) - space? (drop-last))] - (->> splitted - (reduce (fn [[list command-started?] arg] - (let [quotes-count (count (filter #(= % const/arg-wrapping-char) arg)) - has-quote? (and (= quotes-count 1) - (str/index-of arg const/arg-wrapping-char)) - arg (str/replace arg (re-pattern const/arg-wrapping-char) "") - new-list (if command-started? - (let [index (dec (count list))] - (update list index str const/spacing-char arg)) - (conj list arg)) - command-continues? (or (and command-started? (not has-quote?)) - (and (not command-started?) has-quote?))] - [new-list command-continues?])) - [[] false]) - (first))))) - -(defn join-command-args - "Transforms a list of args to a string. The opposite of `split-command-args`. - - Examples: - Input: ['/send' 'Jarrad' '1.0'] - Output: '/send Jarrad 1.0' - - Input: ['/send' '\"Jarrad\"' '1.0'] - Output: '/send Jarrad 1.0' - - Input: ['/send' 'Complex name with space in between' '1.0'] - Output: '/send \"Complex name with space in between\" 1.0'" - [args] - (when args - (->> args - (map (fn [arg] - (let [arg (str/replace arg (re-pattern const/arg-wrapping-char) "")] - (if (not (str/index-of arg const/spacing-char)) - arg - (str const/arg-wrapping-char arg const/arg-wrapping-char))))) - (str/join const/spacing-char)))) - -(defn set-chat-input-text - "Set input text for current-chat. Takes db and input text and cofx - as arguments and returns new fx. Always clear all validation messages." - [new-input {{:keys [current-chat-id] :as db} :db}] - {:db (-> (chat-model/set-chat-ui-props db {:validation-messages nil}) - (assoc-in [:chats current-chat-id :input-text] (text->emoji new-input)))}) - -(defn set-chat-input-metadata - "Sets user invisible chat input metadata for current-chat" - [metadata {:keys [db] :as cofx}] - (let [current-chat-id (:current-chat-id db)] - {:db (assoc-in db [:chats current-chat-id :input-metadata] metadata)})) - -(defn- start-cooldown [{:keys [db]} cooldowns] - {:dispatch-later [{:dispatch [:disable-cooldown] - :ms (const/cooldown-periods-ms cooldowns - const/default-cooldown-period-ms)}] - :show-cooldown-warning nil - :db (assoc db - :chat/cooldowns (if (= const/cooldown-reset-threshold cooldowns) - 0 - cooldowns) - :chat/spam-messages-frequency 0 - :chat/cooldown-enabled? true)}) - -(defn process-cooldown [{{:keys [chat/last-outgoing-message-sent-at - chat/cooldowns - chat/spam-messages-frequency - current-chat-id] :as db} :db :as cofx}] - (when (and - config/spam-button-detection-enabled? - (chat-model/public-chat? current-chat-id cofx)) - (let [spamming-fast? (< (- (datetime/timestamp) last-outgoing-message-sent-at) - (+ const/spam-interval-ms (* 1000 cooldowns))) - spamming-frequently? (= const/spam-message-frequency-threshold spam-messages-frequency)] - (cond-> {:db (assoc db - :chat/last-outgoing-message-sent-at (datetime/timestamp) - :chat/spam-messages-frequency (if spamming-fast? - (inc spam-messages-frequency) - 0))} - - (and spamming-fast? spamming-frequently?) - (start-cooldown (inc cooldowns)))))) diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs deleted file mode 100644 index 76549f07f88..00000000000 --- a/src/status_im/chat/models/message.cljs +++ /dev/null @@ -1,316 +0,0 @@ -(ns status-im.chat.models.message - (:require [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.utils.core :as utils] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.datetime :as time] - [status-im.chat.models :as chat-model] - [status-im.chat.models.input :as input] - [status-im.chat.commands.receiving :as commands-receiving] - [status-im.utils.clocks :as utils.clocks] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.money :as money] - [status-im.transport.utils :as transport.utils] - [status-im.transport.message.core :as transport] - [status-im.transport.message.v1.protocol :as protocol] - [status-im.data-store.messages :as messages-store] - [status-im.data-store.user-statuses :as user-statuses-store] - [status-im.ui.screens.currency-settings.subs :as currency-settings] - [status-im.utils.datetime :as datetime] - [clojure.string :as string])) - -(def receive-interceptors - [(re-frame/inject-cofx :random-id) - re-frame/trim-v]) - -(defn- emoji-only-content? - [content] - (and (string? content) (re-matches constants/regx-emoji content))) - -(defn- prepare-message - [{:keys [content] :as message} chat-id current-chat?] - ;; TODO janherich: enable the animations again once we can do them more efficiently - (cond-> (assoc message :appearing? true) - (not current-chat?) (assoc :appearing? false) - (emoji-only-content? content) (assoc :content-type constants/content-type-emoji))) - -(defn- re-index-message-groups - "Relative datemarks of message groups can get obsolete with passing time, - this function re-indexes them for given chat" - [chat-id {:keys [db]}] - (let [chat-messages (get-in db [:chats chat-id :messages])] - {:db (update-in db - [:chats chat-id :message-groups] - (partial reduce-kv (fn [groups datemark message-refs] - (let [new-datemark (->> message-refs - first - :message-id - (get chat-messages) - :timestamp - time/day-relative)] - (if (= datemark new-datemark) - ;; nothing to re-index - (assoc groups datemark message-refs) - ;; relative datemark shifted, reindex - (assoc groups new-datemark message-refs)))) - {}))})) - -(defn- sort-references - "Sorts message-references sequence primary by clock value, - breaking ties by `:message-id`" - [messages message-references] - (sort-by (juxt (comp :clock-value (partial get messages) :message-id) - :message-id) - message-references)) - -(defn- group-messages - "Takes chat-id, new messages + cofx and properly groups them - into the `:message-groups`index in db" - [chat-id messages {:keys [db]}] - {:db (reduce (fn [db [datemark grouped-messages]] - (update-in db [:chats chat-id :message-groups datemark] - (fn [message-references] - (->> grouped-messages - (map (fn [{:keys [message-id timestamp]}] - {:message-id message-id - :timestamp-str (time/timestamp->time timestamp)})) - (into (or message-references '())) - (sort-references (get-in db [:chats chat-id :messages])))))) - db - (group-by (comp time/day-relative :timestamp) - (filter :show? messages)))}) - -(defn- add-own-status - [chat-id message-id status {:keys [db]}] - (let [me (:current-public-key db) - status {:chat-id chat-id - :message-id message-id - :whisper-identity me - :status status}] - {:db (assoc-in db - [:chats chat-id :message-statuses message-id me] - status) - :data-store/tx [(user-statuses-store/save-status-tx status)]})) - -(defn add-outgoing-status [{:keys [from] :as message} {:keys [db]}] - (assoc message :outgoing (= from (:current-public-key db)))) - -(defn- add-message - [batch? {:keys [chat-id message-id clock-value content] :as message} current-chat? {:keys [db] :as cofx}] - (let [prepared-message (-> message - (prepare-message chat-id current-chat?) - (add-outgoing-status cofx))] - (let [fx {:db (cond-> - (-> db - (update-in [:chats chat-id :messages] assoc message-id prepared-message) - ;; this will increase last-clock-value twice when sending our own messages - (update-in [:chats chat-id :last-clock-value] (partial utils.clocks/receive clock-value))) - (not current-chat?) - (update-in [:chats chat-id :unviewed-messages] (fnil conj #{}) message-id)) - :data-store/tx [(messages-store/save-message-tx prepared-message)]}] - (if batch? - fx - (handlers-macro/merge-fx cofx - fx - (re-index-message-groups chat-id) - (group-messages chat-id [message])))))) - -(def ^:private- add-single-message (partial add-message false)) -(def ^:private- add-batch-message (partial add-message true)) - -(defn- send-message-seen [chat-id message-id send-seen? cofx] - (when send-seen? - (transport/send (protocol/map->MessagesSeen {:message-ids #{message-id}}) chat-id cofx))) - -(defn ensure-clock-value [{:keys [clock-value] :as message} {:keys [last-clock-value]}] - (if clock-value - message - (assoc message :clock-value (utils.clocks/send last-clock-value)))) - -(defn- update-legacy-type [{:keys [content-type] :as message}] - (cond-> message - (= constants/content-type-command-request content-type) - (assoc :content-type constants/content-type-command))) - -(defn- add-received-message - [batch? - {:keys [from message-id chat-id content content-type clock-value js-obj] :as raw-message} - {:keys [db now] :as cofx}] - (let [{:keys [web3 current-chat-id view-id]} db - current-chat? (and (= :chat view-id) (= current-chat-id chat-id)) - {:keys [public?] :as chat} (get-in db [:chats chat-id]) - add-message-fn (if batch? add-batch-message add-single-message) - message (-> raw-message - (ensure-clock-value chat) - ;; TODO (cammellos): Refactor so it's not computed twice - (add-outgoing-status cofx) - ;; TODO (janherich): Remove after couple of releases - update-legacy-type)] - (handlers-macro/merge-fx cofx - {:confirm-messages-processed [{:web3 web3 - :js-obj js-obj}]} - (add-message-fn message current-chat?) - ;; Checking :outgoing here only works for now as we don't have a :seen - ;; status for public chats, if we add processing of our own messages - ;; for 1-to-1 care needs to be taken not to override the :seen status - (add-own-status chat-id message-id (cond (:outgoing message) :sent - current-chat? :seen - :else :received)) - (commands-receiving/receive message) - (send-message-seen chat-id message-id (and (not public?) - current-chat? - (not (chat-model/bot-only-chat? db chat-id)) - (not (= constants/system from)) - (not (:outgoing message))))))) - -(def ^:private add-single-received-message (partial add-received-message false)) -(def ^:private add-batch-received-message (partial add-received-message true)) - -(defn receive - [{:keys [chat-id message-id] :as message} {:keys [now] :as cofx}] - (handlers-macro/merge-fx cofx - (chat-model/upsert-chat {:chat-id chat-id - ;; We activate a chat again on new messages - :is-active true - :timestamp now}) - (add-single-received-message message))) - -(defn receive-many - [messages {:keys [now] :as cofx}] - (let [chat->message (group-by :chat-id messages) - chat-ids (keys chat->message) - chat-effects (handlers-macro/merge-effects - cofx - (fn [chat-id cofx] - (chat-model/upsert-chat {:chat-id chat-id - :is-active true - :timestamp now} - cofx)) - chat-ids) - message-effects (handlers-macro/merge-effects - chat-effects cofx add-batch-received-message messages)] - (handlers-macro/merge-effects - message-effects - cofx - (fn [chat-id cofx] - (handlers-macro/merge-fx cofx - (re-index-message-groups chat-id) - (group-messages chat-id (get chat->message chat-id)))) - chat-ids))) - -(defn system-message [chat-id message-id timestamp content] - {:message-id message-id - :chat-id chat-id - :from constants/system - :username constants/system - :timestamp timestamp - :show? true - :content content - :content-type constants/text-content-type}) - -(defn group-message? [{:keys [message-type]}] - (#{:group-user-message :public-group-user-message} message-type)) - -(defn add-to-chat? - [{:keys [db]} {:keys [chat-id clock-value message-id] :as message}] - (let [{:keys [deleted-at-clock-value messages not-loaded-message-ids]} - (get-in db [:chats chat-id])] - (not (or (get messages message-id) - (get not-loaded-message-ids message-id) - (>= deleted-at-clock-value clock-value))))) - -;;;; Send message - -(def send-interceptors - [(re-frame/inject-cofx :random-id) - (re-frame/inject-cofx :random-id-seq) - re-frame/trim-v]) - -(defn- send - [chat-id message-id send-record {{:keys [network-status current-public-key]} :db :as cofx}] - (if (= network-status :offline) - {:dispatch-later [{:ms 10000 - :dispatch [:update-message-status chat-id message-id current-public-key :not-sent]}]} - (transport/send send-record chat-id cofx))) - -(defn add-message-type [message {:keys [chat-id group-chat public?]}] - (cond-> message - (not group-chat) - (assoc :message-type :user-message) - (and group-chat public?) - (assoc :message-type :public-group-user-message) - (and group-chat (not public?)) - (assoc :message-type :group-user-message))) - -(def ^:private transport-keys [:content :content-type :message-type :clock-value :timestamp]) - -(defn- upsert-and-send [{:keys [chat-id] :as message} {:keys [now] :as cofx}] - (let [send-record (protocol/map->Message (select-keys message transport-keys)) - message-id (transport.utils/message-id send-record) - message-with-id (assoc message :message-id message-id)] - (handlers-macro/merge-fx cofx - (input/process-cooldown) - (chat-model/upsert-chat {:chat-id chat-id - :timestamp now}) - (add-single-message message-with-id true) - (add-own-status chat-id message-id :sending) - (send chat-id message-id send-record)))) - -(defn send-push-notification [fcm-token status cofx] - (when (and fcm-token (= status :sent)) - {:send-notification {:message "message" - :payload {:title "Status" :body "You have a new message"} - :tokens [fcm-token]}})) - -(defn update-message-status [{:keys [chat-id message-id from]} status {:keys [db]}] - (let [updated-status (-> db - (get-in [:chats chat-id :message-statuses message-id from]) - (assoc :status status))] - {:db (assoc-in db - [:chats chat-id :message-statuses message-id from] - updated-status) - :data-store/tx [(user-statuses-store/save-status-tx updated-status)]})) - -(defn resend-message [chat-id message-id cofx] - (let [message (get-in cofx [:db :chats chat-id :messages message-id]) - send-record (-> message - (select-keys transport-keys) - (update :message-type keyword) - protocol/map->Message)] - (handlers-macro/merge-fx cofx - (send chat-id message-id send-record) - (update-message-status message :sending)))) - -(defn- remove-message-from-group [chat-id {:keys [timestamp message-id]} {:keys [db]}] - (let [datemark (time/day-relative timestamp)] - {:db (update-in db [:chats chat-id :message-groups] - (fn [groups] - (let [message-references (get groups datemark)] - (if (= 1 (count message-references)) - ;; message removed is the only one in group, remove whole group - (dissoc groups datemark) - ;; remove message from `message-references` list - (assoc groups datemark - (remove (comp (partial = message-id) :message-id) - message-references))))))})) - -(defn delete-message - "Deletes chat message, along its occurence in all references, like `:message-groups`" - [chat-id message-id {:keys [db] :as cofx}] - (handlers-macro/merge-fx - cofx - {:db (update-in db [:chats chat-id :messages] dissoc message-id) - :data-store/tx [(messages-store/delete-message-tx message-id)]} - (remove-message-from-group chat-id (get-in db [:chats chat-id :messages message-id])))) - -(defn send-message [{:keys [chat-id] :as message} {:keys [db now] :as cofx}] - (let [{:keys [current-public-key chats]} db - {:keys [last-clock-value] :as chat} (get chats chat-id) - message-data (-> message - (assoc :from current-public-key - :timestamp now - :clock-value (utils.clocks/send - last-clock-value) - :show? true) - (add-message-type chat))] - (upsert-and-send message-data cofx))) diff --git a/src/status_im/chat/screen.cljs b/src/status_im/chat/screen.cljs deleted file mode 100644 index 7a1790a83de..00000000000 --- a/src/status_im/chat/screen.cljs +++ /dev/null @@ -1,161 +0,0 @@ -(ns status-im.chat.screen - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.i18n :as i18n] - [status-im.chat.models :as models.chat] - [status-im.models.contact :as models.contact] - [status-im.chat.styles.screen :as style] - [status-im.utils.platform :as platform] - [status-im.chat.views.toolbar-content :as toolbar-content] - [status-im.chat.views.message.message :as message] - [status-im.chat.views.message.datemark :as message-datemark] - [status-im.chat.views.input.input :as input] - [status-im.chat.views.actions :as actions] - [status-im.chat.views.bottom-info :as bottom-info] - [status-im.chat.views.message.options :as message-options] - [status-im.chat.views.message.datemark :as message-datemark] - [status-im.chat.views.message.message :as message] - [status-im.chat.views.toolbar-content :as toolbar-content] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.connectivity.view :as connectivity] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.colors :as colors])) - -(defview add-contact-bar [contact-identity] - (letsubs [contact [:get-contact-by-identity contact-identity]] - (when (models.contact/can-add-to-contacts? contact) - [react/touchable-highlight - {:on-press #(re-frame/dispatch [:add-contact contact-identity]) - :accessibility-label :add-to-contacts-button} - [react/view style/add-contact - [react/i18n-text {:style style/add-contact-text :key :add-to-contacts}]]]))) - -(defn- on-options [chat-id chat-name group-chat? public?] - (list-selection/show {:title chat-name - :options (actions/actions group-chat? chat-id public?)})) - -(defview chat-toolbar [public?] - (letsubs [name [:get-current-chat-name] - {:keys [group-chat chat-id contacts]} [:get-current-chat]] - [react/view - [status-bar/status-bar] - (if (= chat-id constants/console-chat-id) - [toolbar/simple-toolbar name] - [toolbar/platform-agnostic-toolbar {} - (toolbar/nav-back-count {:home? true}) - [toolbar-content/toolbar-content-view] - [toolbar/actions [{:icon :icons/wallet - :icon-opts {:color :black - :accessibility-label :wallet-modal-button} - :handler #(re-frame/dispatch [:navigate-to-modal :wallet-modal])} - {:icon :icons/options - :icon-opts {:color :black - :accessibility-label :chat-menu-button} - :handler #(on-options chat-id name group-chat public?)}]]]) - (when-not (or public? group-chat) [add-contact-bar (first contacts)])])) - -(defmulti message-row (fn [{{:keys [type]} :row}] type)) - -(defmethod message-row :datemark - [{{:keys [value]} :row}] - [message-datemark/chat-datemark value]) - -(defmethod message-row :default - [{:keys [group-chat current-public-key row]}] - [message/chat-message (assoc row - :group-chat group-chat - :current-public-key current-public-key)]) - -(defview messages-view-animation [message-view] - ;; smooths out appearance of message-view - (letsubs [opacity (animation/create-value 0) - duration (if platform/android? 100 200) - timeout (if platform/android? 50 0)] - {:component-did-mount (fn [_] - (animation/start - (animation/anim-sequence - [(animation/anim-delay timeout) - (animation/spring opacity {:toValue 1 - :duration duration - :useNativeDriver true})])))} - [react/with-activity-indicator - {:style style/message-view-preview - :preview [react/view style/message-view-preview]} - [react/touchable-without-feedback - {:on-press (fn [_] - (re-frame/dispatch [:set-chat-ui-props {:messages-focused? true}]) - (react/dismiss-keyboard!))} - [react/animated-view {:style (style/message-view-animated opacity)} - message-view]]])) - -(defview empty-chat-container [{:keys [group-chat chat-id]}] - (letsubs [contact [:get-contact-by-identity chat-id]] - (let [one-to-one (and (not group-chat) - (not (:dapp? contact)))] - [react/view style/empty-chat-container - (when one-to-one - [vector-icons/icon :icons/lock]) - [react/text {:style style/empty-chat-text} - (cond - (= chat-id constants/console-chat-id) - (i18n/label :t/empty-chat-description-console) - - one-to-one - [react/text style/empty-chat-container-one-to-one - (i18n/label :t/empty-chat-description-one-to-one) - [react/text {:style style/empty-chat-text-name} (:name contact)]] - - :else - (i18n/label :t/empty-chat-description))]]))) - -(defview messages-view [group-chat] - (letsubs [messages [:get-current-chat-messages-stream] - chat [:get-current-chat] - current-public-key [:get-current-public-key]] - {:component-did-mount #(re-frame/dispatch [:set-chat-ui-props {:messages-focused? true - :input-focused? false}])} - (if (empty? messages) - [empty-chat-container chat] - [list/flat-list {:data messages - :key-fn #(or (:message-id %) (:value %)) - :render-fn (fn [message] - [message-row {:group-chat group-chat - :current-public-key current-public-key - :row message}]) - :inverted true - :onEndReached #(re-frame/dispatch [:load-more-messages]) - :enableEmptySections true - :keyboardShouldPersistTaps :handled}]))) - -(defview chat [] - (letsubs [{:keys [group-chat public? input-text]} [:get-current-chat] - show-bottom-info? [:get-current-chat-ui-prop :show-bottom-info?] - show-message-options? [:get-current-chat-ui-prop :show-message-options?] - current-view [:get :view-id]] - ;; this scroll-view is a hack that allows us to use on-blur and on-focus on Android - ;; more details here: https://github.com/facebook/react-native/issues/11071 - [react/scroll-view {:scroll-enabled false - :style style/scroll-root - :content-container-style style/scroll-root - :keyboard-should-persist-taps :handled} - [react/view {:style style/chat-view - :on-layout (fn [e] - (re-frame/dispatch [:set :layout-height (-> e .-nativeEvent .-layout .-height)]))} - [chat-toolbar public?] - (when (= :chat current-view) - [messages-view-animation - [messages-view group-chat]]) - [input/container {:text-empty? (string/blank? input-text)}] - (when show-bottom-info? - [bottom-info/bottom-info-view]) - (when show-message-options? - [message-options/view]) - [connectivity/error-view {:top (get platform/platform-specific :status-bar-default-height)}]]])) diff --git a/src/status_im/chat/specs.cljs b/src/status_im/chat/specs.cljs deleted file mode 100644 index f3246d9bdd9..00000000000 --- a/src/status_im/chat/specs.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.chat.specs - (:require [cljs.spec.alpha :as s] - status-im.chat.commands.specs)) - -(s/def :chat/chats (s/nilable map?)) ; {id (string) chat (map)} active chats on chat's tab -(s/def :chat/current-chat-id (s/nilable string?)) ; current or last opened chat-id -(s/def :chat/chat-id (s/nilable string?)) ; what is the difference ? ^ -(s/def :chat/new-chat-name (s/nilable string?)) ; we have name in the new-chat why do we need this field -(s/def :chat/chat-animations (s/nilable map?)) ; {id (string) props (map)} -(s/def :chat/chat-ui-props (s/nilable map?)) ; {id (string) props (map)} -(s/def :chat/chat-list-ui-props (s/nilable map?)) -(s/def :chat/layout-height (s/nilable number?)) ; height of chat's view layout -(s/def :chat/selected-participants (s/nilable set?)) -(s/def :chat/chat-loaded-callbacks (s/nilable map?)) -(s/def :chat/public-group-topic (s/nilable string?)) -(s/def :chat/public-group-topic-error (s/nilable string?)) -(s/def :chat/messages (s/nilable map?)) ; messages indexed by message-id -(s/def :chat/message-groups (s/nilable map?)) ; grouped/sorted messages -(s/def :chat/message-statuses (s/nilable map?)) ; message/user statuses indexed by two level index -(s/def :chat/not-loaded-message-ids (s/nilable set?)) ; set of message-ids not yet fully loaded from persisted state -(s/def :chat/last-clock-value (s/nilable number?)) ; last logical clock value of messages in chat -(s/def :chat/loaded-chats (s/nilable seq?)) -(s/def :chat/bot-db (s/nilable map?)) -(s/def :chat/cooldowns (s/nilable number?)) ; number of cooldowns given for spamming send button -(s/def :chat/cooldown-enabled? (s/nilable boolean?)) -(s/def :chat/last-outgoing-message-sent-at (s/nilable number?)) -(s/def :chat/spam-messages-frequency (s/nilable number?)) ; number of consecutive spam messages sent diff --git a/src/status_im/chat/styles/animations.cljs b/src/status_im/chat/styles/animations.cljs deleted file mode 100644 index f62bedca5b2..00000000000 --- a/src/status_im/chat/styles/animations.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.chat.styles.animations - (:require [status-im.ui.components.styles :as common])) - -(def header-draggable-icon "rgba(73, 84, 93, 0.23)") - -(def overlap-container - {:position :absolute - :left 0 - :top 0 - :right 0 - :bottom 0}) - -(defn expandable-container [anim-value bottom max-height] - {:background-color common/color-white - :height anim-value - :left 0 - :right 0 - :bottom bottom - :position :absolute - :elevation 2 - :max-height max-height}) - -(def header-container - {:min-height 19 - :background-color common/color-white - :align-items :center}) - -(def header-icon - {:background-color header-draggable-icon - :margin-top 8 - :margin-bottom 6 - :width 24 - :border-radius 1.5 - :height 3}) diff --git a/src/status_im/chat/styles/input/input.cljs b/src/status_im/chat/styles/input/input.cljs deleted file mode 100644 index a5c82a966c3..00000000000 --- a/src/status_im/chat/styles/input/input.cljs +++ /dev/null @@ -1,116 +0,0 @@ -(ns status-im.chat.styles.input.input - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def min-input-height 36) -(def padding-vertical 8) -(def border-height 1) -(def max-input-height (* 5 min-input-height)) - -(defnstyle root [margin-bottom] - {:background-color colors/white - :margin-bottom margin-bottom - :flex-direction :column - :border-top-width border-height - :border-top-color colors/gray-light - :elevation 2}) - -(def input-container - {:flex-direction :row - :align-items :flex-end - :padding-left 14}) - -(def input-root - {:padding-top padding-vertical - :padding-bottom padding-vertical - :flex 1}) - -(def input-animated - {:align-items :flex-start - :flex-direction :row - :flex-grow 1 - :min-height min-input-height - :max-height max-input-height}) - -(defnstyle input-view [single-line-input?] - {:flex 1 - :font-size 15 - :padding-top 9 - :padding-bottom 5 - :padding-right 12 - :min-height min-input-height - :max-height (if single-line-input? - min-input-height - max-input-height) - :android {:padding-top 3}}) - -(def invisible-input-text - {:font-size 15 - :position :absolute - :left 0 - :background-color :transparent - :color :transparent}) - -(defnstyle invisible-input-text-height [container-width] - {:width container-width - :flex 1 - :font-size 15 - :padding-top 5 - :padding-bottom 5 - :android {:padding-top 3} - :position :absolute - :left 0 - :background-color :transparent - :color :transparent}) - -(defnstyle input-helper-view [left opacity] - {:opacity opacity - :position :absolute - :height min-input-height - :android {:left (+ 4 left)} - :ios {:left left}}) - -(defnstyle input-helper-text [left] - {:color colors/gray - :font-size 15 - :text-align-vertical :center - :flex 1 - :android {:top -1} - :ios {:line-height min-input-height}}) - -(defnstyle seq-input-text [left container-width] - {:min-width (- container-width left) - :font-size 15 - :position :absolute - :text-align-vertical :center - :align-items :center - :android {:left (+ 2 left) - :height (+ 2 min-input-height) - :top 0.5} - :ios {:line-height min-input-height - :height min-input-height - :left left}}) - -(def input-commands-icon - {:margin 14 - :height 24 - :width 24}) - -(def input-clear-container - {:width 24 - :height 24 - :margin-top 7 - :align-items :center}) - -(def commands-root - {:flex-direction :row - :align-items :center}) - -(def command-list-icon-container - {:width 32 - :height 32 - :padding 4}) - -(def commands-list-icon - {:height 24 - :width 24}) diff --git a/src/status_im/chat/styles/input/parameter_box.cljs b/src/status_im/chat/styles/input/parameter_box.cljs deleted file mode 100644 index 191564e8c9f..00000000000 --- a/src/status_im/chat/styles/input/parameter_box.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.chat.styles.input.parameter-box - (:require [status-im.ui.components.styles :as common] - [status-im.ui.components.colors :as colors])) - -(def root - {:background-color common/color-white - :border-bottom-color colors/gray-light - :border-bottom-width 1}) - diff --git a/src/status_im/chat/styles/input/send_button.cljs b/src/status_im/chat/styles/input/send_button.cljs deleted file mode 100644 index b0b505a9005..00000000000 --- a/src/status_im/chat/styles/input/send_button.cljs +++ /dev/null @@ -1,17 +0,0 @@ -(ns status-im.chat.styles.input.send-button - (:require [status-im.ui.components.colors :as colors])) - -(defn send-message-container [rotation] - {:background-color colors/blue - :width 30 - :height 30 - :border-radius 15 - :margin 10 - :padding 4 - :margin-left 8 - :margin-bottom 11 - :transform [{:rotate rotation}]}) - -(def send-message-icon - {:height 22 - :width 22}) diff --git a/src/status_im/chat/styles/input/suggestions.cljs b/src/status_im/chat/styles/input/suggestions.cljs deleted file mode 100644 index 6cab42b3f95..00000000000 --- a/src/status_im/chat/styles/input/suggestions.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.chat.styles.input.suggestions - (:require-macros [status-im.utils.styles :refer [defnstyle]]) - (:require [status-im.ui.components.styles :as common] - [status-im.ui.components.colors :as colors] - [status-im.utils.platform :as platform])) - -(def item-height 52) -(def border-height 1) - -(def root - {:background-color common/color-white - :border-top-color colors/gray-light - :border-top-width 1}) - -(def item-suggestion-container - {:flex-direction :row - :align-items :center - :height item-height - :padding-horizontal 14 - :border-bottom-color colors/gray-light - :border-bottom-width border-height}) - -(def item-suggestion-name - {:color common/color-black - :font-size 15}) - -(def item-suggestion-description - {:flex 1 - :font-size 15 - :margin-left 10 - :color colors/gray}) diff --git a/src/status_im/chat/styles/input/validation_message.cljs b/src/status_im/chat/styles/input/validation_message.cljs deleted file mode 100644 index 9677559f519..00000000000 --- a/src/status_im/chat/styles/input/validation_message.cljs +++ /dev/null @@ -1,23 +0,0 @@ -(ns status-im.chat.styles.input.validation-message - (:require [status-im.chat.constants :as constants] - [status-im.ui.components.styles :as common])) - -(defn root [bottom] - {:flex-direction :column - :left 0 - :right 0 - :bottom bottom - :position :absolute}) - -(def message-container - {:background-color common/color-red - :padding 16}) - -(def message-title - {:color common/color-white - :font-size 12}) - -(def message-description - {:color common/color-white - :font-size 12 - :opacity 0.9}) \ No newline at end of file diff --git a/src/status_im/chat/styles/message/command_pill.cljs b/src/status_im/chat/styles/message/command_pill.cljs deleted file mode 100644 index 7c9a6cc1308..00000000000 --- a/src/status_im/chat/styles/message/command_pill.cljs +++ /dev/null @@ -1,15 +0,0 @@ -(ns status-im.chat.styles.message.command-pill - (:require [status-im.utils.platform :as p] - [status-im.ui.components.styles :refer [color-white]])) - -(defn pill [command] - {:background-color (:color command) - :height 24 - :border-radius 50 - :padding-top (if p/ios? 4 3) - :paddingHorizontal 12 - :text-align :left}) - -(def pill-text - {:font-size 12 - :color color-white}) diff --git a/src/status_im/chat/styles/message/datemark.cljs b/src/status_im/chat/styles/message/datemark.cljs deleted file mode 100644 index d58d24838ad..00000000000 --- a/src/status_im/chat/styles/message/datemark.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns status-im.chat.styles.message.datemark - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.styles :as common])) - -(def datemark-wrapper - {:flex 1 - :align-items :center}) - -(def datemark - {:margin-top 16 - :height 22}) - -(defstyle datemark-text - {:color common/color-gray4 - :ios {:letter-spacing -0.2} - :font-size 15}) diff --git a/src/status_im/chat/styles/message/message.cljs b/src/status_im/chat/styles/message/message.cljs deleted file mode 100644 index 35cc61d1b81..00000000000 --- a/src/status_im/chat/styles/message/message.cljs +++ /dev/null @@ -1,247 +0,0 @@ -(ns status-im.chat.styles.message.message - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.chat.styles.photos :as photos] - [status-im.ui.components.colors :as colors] - [status-im.constants :as constants])) - -(defstyle style-message-text - {:font-size 15 - :color styles/text1-color - :letter-spacing -0.2 - :android {:line-height 22} - :ios {:line-height 22}}) - -(def style-sub-text - {:top -2 - :font-size 12 - :color styles/text2-color - :line-height 14 - :height 16}) - -(defn message-padding-top - [{:keys [first-in-group? display-username?]}] - (if (and display-username? - first-in-group?) - 6 - 2)) - -(def message-empty-spacing - {:height 16}) - -(defn last-message-padding - [{:keys [last? typing]}] - (when (and last? (not typing)) - {:padding-bottom 16})) - -(defn message-body - [{:keys [outgoing display-photo?] :as message}] - (let [align (if outgoing :flex-end :flex-start) - direction (if outgoing :row-reverse :row)] - (merge {:flex-direction direction - :width 230 - :padding-top (message-padding-top message) - :align-self align - :align-items align} - (when display-photo? - {:padding-right 8 - :padding-left 8})))) - -(def message-timestamp - {:font-size 10 - :letter-spacing 0.1 - :align-self :flex-end}) - -(defn message-timestamp-text [justify-timestamp? outgoing rtl?] - (merge message-timestamp - {:color (if outgoing colors/wild-blue-yonder colors/gray)} - (when justify-timestamp? {:position :absolute - :bottom 8 - (if rtl? :left :right) 12}))) - -(defn message-timestamp-placeholder-text [outgoing] - (assoc message-timestamp - :color - (if outgoing colors/hawkes-blue styles/color-white))) - -(def message-expand-button - {:color colors/gray - :font-size 12 - :opacity 0.7 - :margin-bottom 1}) - -(def selected-message - {:margin-top 18 - :margin-left 40 - :font-size 12 - :color styles/text2-color}) - -(defn group-message-wrapper [message] - (merge {:flex-direction :column} - (last-message-padding message))) - -(defn timestamp-content-wrapper [{:keys [outgoing]}] - {:flex-direction (if outgoing :row-reverse :row)}) - -(defn group-message-view - [outgoing] - (let [align (if outgoing :flex-end :flex-start)] - {:flex-direction :column - :width 230 - :padding-left 8 - :padding-right 8 - :align-items align})) - -(defn delivery-status [outgoing] - (if outgoing - {:align-self :flex-end - :padding-right 8} - {:align-self :flex-start - :padding-left 8})) - -(def message-author - {:width photos/default-size - :align-self :flex-end}) - -(def delivery-view - {:flex-direction :row - :margin-top 2}) - -(def delivery-text - {:color styles/color-gray4 - :font-size 12}) - -(def not-sent-view - (assoc delivery-view - :margin-bottom 2 - :padding-top 2)) - -(def not-sent-text - (assoc delivery-text - :color styles/color-red - :text-align :right - :padding-top 4)) - -(def not-sent-icon - {:padding-top 3 - :padding-left 3}) - -(def message-activity-indicator - {:padding-top 4}) - -(defn text-message - [collapsed?] - (assoc style-message-text :margin-bottom (if collapsed? 2 0))) - -(defnstyle emoji-message - [{:keys [incoming-group]}] - {:font-size 40 - :color styles/text1-color - :android {:line-height 45} - :ios {:line-height 46} - :margin-top (if incoming-group 4 0)}) - -(defn message-view - [{:keys [content-type outgoing group-chat first-in-group?]}] - (merge {:padding-vertical 6 - :padding-horizontal 12 - :border-radius 8 - :margin-top (if (and first-in-group? - (or outgoing - (not group-chat))) - 16 - 4)} - (when-not (= content-type constants/content-type-emoji) - {:background-color (if outgoing colors/hawkes-blue styles/color-white)}) - (when (= content-type constants/content-type-command) - {:padding-top 12 - :padding-bottom 10}))) - -(def author - {:color styles/color-gray4 - :margin-bottom 4 - :font-size 12}) - -(def audio-container - {:flex-direction :row - :align-items :center}) - -(def play-view - {:width 33 - :height 33 - :border-radius 16 - :elevation 1}) - -(def play-image - {:width 33 - :height 33}) - -(def track-container - {:margin-top 10 - :margin-left 10 - :width 120 - :height 26 - :elevation 1}) - -(def track - {:position :absolute - :top 4 - :width 120 - :height 2 - :background-color :#EC7262}) - -(def track-mark - {:position :absolute - :left 0 - :top 0 - :width 2 - :height 10 - :background-color :#4A5258}) - -(def track-duration-text - {:position :absolute - :left 1 - :top 11 - :font-size 11 - :color :#4A5258 - :letter-spacing 1 - :line-height 15}) - -(def status-container - {:flex 1 - :align-self :center - :align-items :center - :width 249 - :padding-bottom 16}) - -(def status-image-view - {:margin-top 20}) - -(def status-from - {:margin-top 20 - :font-size 18 - :color styles/text1-color}) - -(def status-text - {:margin-top 10 - :font-size 14 - :line-height 20 - :text-align :center - :color styles/text2-color}) - -(defn message-animated-container [height] - {:height height}) - -(defn message-container [window-width] - {:position :absolute - :width window-width}) - -(defn new-message-container [margin on-top?] - {:background-color styles/color-white - :margin-bottom margin - :elevation (if on-top? 6 5)}) - -(def message-author-name - {:font-size 12 - :padding-top 6 - :color colors/gray}) diff --git a/src/status_im/chat/styles/message/options.cljs b/src/status_im/chat/styles/message/options.cljs deleted file mode 100644 index ee9ba78a914..00000000000 --- a/src/status_im/chat/styles/message/options.cljs +++ /dev/null @@ -1,35 +0,0 @@ -(ns status-im.chat.styles.message.options - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.ui.components.colors :as colors] - [status-im.constants :as constants])) - -(defstyle row - {:flex-direction :row - :background-color :white - :align-items :center - :padding-horizontal 16 - :ios {:height 36} - :android {:height 36}}) - -(def title - {:padding-horizontal 16 - :padding-top 10 - :padding-bottom 10}) - -(def title-text - {:font-weight :bold - :line-height 20}) - -(def label - {:padding-horizontal 16}) - -(def label-text - {:font-size 12}) - -(def icon - {:width 40 - :height 40 - :border-radius 20 - :align-items :center - :justify-content :center}) diff --git a/src/status_im/chat/styles/photos.cljs b/src/status_im/chat/styles/photos.cljs deleted file mode 100644 index 1c826713272..00000000000 --- a/src/status_im/chat/styles/photos.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.chat.styles.photos - (:require [status-im.ui.components.colors :as colors])) - -(def default-size 36) - -(defn radius [size] (/ size 2)) - -(defn photo-container [size] - {:position :relative - :border-radius (radius size)}) - -(defn photo-border [size] - {:position :absolute - :width size - :height size - :opacity 0.4 - :border-color colors/photo-border-color - :border-width 1 - :border-radius (radius size)}) - -(defn photo [size] - {:border-radius (radius size) - :width size - :height size}) diff --git a/src/status_im/chat/styles/screen.cljs b/src/status_im/chat/styles/screen.cljs deleted file mode 100644 index 3ddee18233a..00000000000 --- a/src/status_im/chat/styles/screen.cljs +++ /dev/null @@ -1,231 +0,0 @@ -(ns status-im.chat.styles.screen - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as component.styles] - [status-im.ui.components.colors :as colors])) - -(def scroll-root - {:flex 1}) - -(def chat-view - {:flex 1 - :background-color component.styles/chat-background}) - -(def toolbar-container - {:flex 1 - :flex-direction :row - :align-items :stretch - :margin-left 3}) - -(def messages-container - {:flex 1 - :padding-bottom 0 - :margin-bottom 0}) - -(def toolbar-view - {:flex-direction :row - :height 56 - :background-color component.styles/color-white - :elevation 2}) - -(def action - {:width 56 - :height 56 - :top 0 - :align-items :center - :justify-content :center}) - -(def icon-view - {:width 56 - :height 56}) - -(def back-icon - {:margin-top 21 - :margin-left 23 - :width 8 - :height 14}) - -(def chat-toolbar-contents - {:flex-direction :row - :flex 1}) - -(def chat-name-view - {:flex 1 - :justify-content :center - :margin-bottom 2}) - -(def chat-name-text - {:color component.styles/color-black - :line-height 18 - :font-size 15}) - -(def group-icon - {:margin-top 4 - :margin-bottom 2.7 - :width 14 - :height 9}) - -(def up-icon - {:width 14 - :height 8}) - -(defstyle toolbar-subtitle - {:color component.styles/text4-color - :line-height 15 - :font-size 13 - :ios {:margin-top 4}}) - -(defstyle last-activity-text - {:color component.styles/text4-color - :line-height 15 - :ios {:font-size 14 - :margin-top 4} - :android {:font-size 13}}) - -(defn actions-wrapper [status-bar-height] - {:background-color component.styles/color-white - :elevation 2 - :position :absolute - :top (+ 55 status-bar-height) - :left 0 - :right 0}) - -(def actions-separator - {:margin-left 16 - :height 1.5 - :background-color component.styles/separator-color}) - -(def actions-view - {:margin-vertical 10}) - -(def action-icon-row - {:flex-direction :row - :height 56}) - -(def action-icon-view - (merge icon-view - {:align-items :center - :justify-content :center})) - -(def action-view - {:flex 1 - :align-items :flex-start - :justify-content :center}) - -(def action-title - {:margin-top -2.5 - :color component.styles/text1-color - :font-size 14}) - -(def action-subtitle - {:margin-top 1 - :color component.styles/text2-color - :font-size 12}) - -(def typing-all - {:marginBottom 20}) - -(def typing-view - {:width 260 - :margin-top 10 - :padding-left 8 - :padding-right 8 - :align-items :flex-start - :align-self :flex-start}) - -(def typing-text - {:margin-top -2 - :font-size 12 - :color component.styles/text2-color}) - -(def overlay-highlight - {:flex 1}) - -;; this map looks a bit strange -;; but this way of setting elevation seems to be the only way to set z-index (in RN 0.30) -(def bottom-info-overlay - {:position :absolute - :top -16 - :bottom -16 - :left -16 - :right -16 - :background-color "#00000055" - :elevation 8}) - -(defn bottom-info-container [height] - {:background-color component.styles/color-white - :elevation 2 - :position :absolute - :bottom 16 - :left 16 - :right 16 - :height height}) - -(def bottom-info-list-container - {:padding-left 16 - :padding-right 16 - :padding-top 8 - :padding-bottom 8}) - -(def item-height 60) - -(def bottom-info-row - {:flex-direction "row" - :padding-top 4 - :padding-bottom 4}) - -(def bottom-info-row-photo-size 42) - -(def bottom-info-row-text-container - {:margin-left 16 - :margin-right 16}) - -(def bottom-info-row-text1 - {:color "black"}) - -(def bottom-info-row-text2 - {:color "#888888"}) - -(def add-contact - {:height 35 - :background-color :white - :justify-content :center}) - -(def add-contact-text - {:text-align :center - :text-align-vertical :center - :color :#7099e6}) - -(defstyle actions-list-view - {:ios {:border-bottom-color component.styles/color-gray3 - :border-bottom-width 0.5}}) - -(def message-view-preview - {:flex 1 - :align-items :center - :justify-content :center}) - -(defn message-view-animated [opacity] - {:opacity opacity - :flex 1}) - -(def empty-chat-container-one-to-one - {:margin-top 10}) - -(def empty-chat-container - {:flex 1 - :flex-direction :column - :justify-content :center - :align-items :center - :padding-vertical 50 - :margin-right 6}) - -(def empty-chat-text - {:color colors/gray - :width 280 - :font-size 15 - :line-height 22 - :letter-spacing -0.2 - :text-align :center}) - -(def empty-chat-text-name - {:color colors/black}) diff --git a/src/status_im/chat/subs.cljs b/src/status_im/chat/subs.cljs deleted file mode 100644 index 7f9a49dfc7f..00000000000 --- a/src/status_im/chat/subs.cljs +++ /dev/null @@ -1,371 +0,0 @@ -(ns status-im.chat.subs - (:require [clojure.string :as string] - [re-frame.core :refer [reg-sub subscribe]] - [status-im.chat.constants :as chat-constants] - [status-im.chat.models.input :as input-model] - [status-im.chat.commands.core :as commands] - [status-im.utils.datetime :as time] - [status-im.utils.platform :as platform] - [status-im.utils.gfycat.core :as gfycat] - [status-im.i18n :as i18n] - [status-im.constants :as const] - [status-im.wallet.transactions :as transactions])) - -(reg-sub :get-chats :chats) - -(reg-sub :get-current-chat-id :current-chat-id) - -(reg-sub :chat-ui-props :chat-ui-props) - -(reg-sub :get-id->command :id->command) - -(reg-sub :get-access-scope->command-id :access-scope->command-id) - -(reg-sub - :get-current-chat-ui-props - :<- [:chat-ui-props] - :<- [:get-current-chat-id] - (fn [[chat-ui-props id]] - (get chat-ui-props id))) - -(defn chat-name [{:keys [group-chat - chat-id - public? - name]} - {contact-name :name}] - (cond - public? (str "#" name) - group-chat name - :else (i18n/get-contact-translated - chat-id - :name - (or contact-name - (gfycat/generate-gfy chat-id))))) -(reg-sub - :get-current-chat-name - :<- [:get-current-chat-contact] - :<- [:get-current-chat] - (fn [[contact chat]] - (chat-name chat contact))) - -(reg-sub - :get-chat-name - :<- [:get-contacts] - :<- [:get-chats] - (fn [[contacts chats] [_ chat-id]] - (chat-name (get chats chat-id) (get contacts chat-id)))) - -(reg-sub - :get-current-chat-ui-prop - :<- [:get-current-chat-ui-props] - (fn [ui-props [_ prop]] - (get ui-props prop))) - -(reg-sub - :validation-messages - :<- [:get-current-chat-ui-props] - (fn [ui-props] - (some-> ui-props :validation-messages))) - -(reg-sub - :chat-input-margin - :<- [:get :keyboard-height] - (fn [kb-height] - (cond - (and platform/iphone-x? (> kb-height 0)) (- kb-height 34) - platform/ios? kb-height - :default 0))) - -(defn- active-chat? [[_ chat]] - (and (:is-active chat) - (not= const/console-chat-id (:chat-id chat)))) - -(defn active-chats [chats] - (into {} (filter active-chat? chats))) - -(reg-sub - :get-active-chats - :<- [:get-chats] - active-chats) - -(reg-sub - :get-chat - :<- [:get-active-chats] - (fn [chats [_ chat-id]] - (get chats chat-id))) - -(reg-sub - :get-current-chat - :<- [:get-active-chats] - :<- [:get-current-chat-id] - (fn [[chats current-chat-id]] - (get chats current-chat-id))) - -(reg-sub - :get-current-chat-message - :<- [:get-current-chat] - (fn [{:keys [messages]} [_ message-id]] - (get messages message-id))) - -(reg-sub - :get-current-chat-messages - :<- [:get-current-chat] - (fn [{:keys [messages]}] - (or messages {}))) - -(reg-sub - :get-current-chat-message-groups - :<- [:get-current-chat] - (fn [{:keys [message-groups]}] - (or message-groups {}))) - -(reg-sub - :get-current-chat-message-statuses - :<- [:get-current-chat] - (fn [{:keys [message-statuses]}] - (or message-statuses {}))) - -(defn sort-message-groups - "Sorts message groups according to timestamp of first message in group " - [message-groups messages] - (sort-by - (comp unchecked-negate :timestamp (partial get messages) :message-id first second) - message-groups)) - -(defn messages-with-datemarks-and-statuses - "Converts message groups into sequence of messages interspersed with datemarks, - with correct user statuses associated into message" - [message-groups messages message-statuses] - (mapcat (fn [[datemark message-references]] - (into (list {:value datemark - :type :datemark}) - (map (fn [{:keys [message-id timestamp-str]}] - (assoc (get messages message-id) - :datemark datemark - :timestamp-str timestamp-str - :user-statuses (get message-statuses message-id)))) - message-references)) - message-groups)) - -(defn- set-previous-message-info [stream] - (let [{:keys [display-photo?] :as previous-message} (peek stream)] - (conj (pop stream) (assoc previous-message - :display-username? display-photo? - :first-in-group? true)))) - -(defn display-photo? [{:keys [outgoing message-type]}] - (and (not outgoing) - (not= message-type :user-message))) - -; any message that comes after this amount of ms will be grouped separately -(def ^:private group-ms 60000) - -(defn add-positional-metadata - "Reduce step which adds positional metadata to a message and conditionally - update the previous message with :first-in-group?." - [{:keys [stream last-outgoing-seen]} - {:keys [type message-type from datemark outgoing timestamp] :as message}] - (let [previous-message (peek stream) - ; Was the previous message from a different author or this message - ; comes after x ms - last-in-group? (or (not= from (:from previous-message)) - (> (- (:timestamp previous-message) timestamp) group-ms)) - same-direction? (= outgoing (:outgoing previous-message)) - ; Have we seen an outgoing message already? - last-outgoing? (and (not last-outgoing-seen) - outgoing) - datemark? (= :datemark (:type message)) - ; If this is a datemark or this is the last-message of a group, - ; then the previous message was the first - previous-first-in-group? (or datemark? - last-in-group?) - new-message (assoc message - :display-photo? (display-photo? message) - :same-direction? same-direction? - :last-in-group? last-in-group? - :last-outgoing? last-outgoing?)] - {:stream (cond-> stream - previous-first-in-group? - ; update previuous message if necessary - set-previous-message-info - - :always - (conj new-message)) - ; mark the last message sent by the user - :last-outgoing-seen (or last-outgoing-seen last-outgoing?)})) - -(defn messages-stream - "Enhances the messages in message sequence interspersed with datemarks - with derived stream context information, like: - `:first-in-group?`, `last-in-group?`, `:same-direction?`, `:last?` and `:last-outgoing?` flags." - [ordered-messages] - (when (seq ordered-messages) - (let [initial-message (first ordered-messages) - message-with-metadata (assoc initial-message - :last-in-group? true - :last? true - :display-photo? (display-photo? initial-message) - :last-outgoing? (:outgoing initial-message))] - (->> (rest ordered-messages) - (reduce add-positional-metadata - {:stream [message-with-metadata] - :last-outgoing-seen (:last-outgoing? message-with-metadata)}) - :stream)))) - -(reg-sub - :get-current-chat-messages-stream - :<- [:get-current-chat-messages] - :<- [:get-current-chat-message-groups] - :<- [:get-current-chat-message-statuses] - (fn [[messages message-groups message-statuses]] - (-> (sort-message-groups message-groups messages) - (messages-with-datemarks-and-statuses messages message-statuses) - messages-stream))) - -(reg-sub - :get-commands-for-chat - :<- [:get-id->command] - :<- [:get-access-scope->command-id] - :<- [:get-current-chat] - (fn [[id->command access-scope->command-id chat]] - (commands/chat-commands id->command access-scope->command-id chat))) - -(def ^:private map->sorted-seq (comp (partial map second) (partial sort-by first))) - -(defn- available-commands [[commands {:keys [input-text]}]] - (->> commands - map->sorted-seq - (filter (fn [{:keys [type]}] - (when (input-model/starts-as-command? input-text) - (string/includes? (commands/command-name type) input-text)))))) - -(reg-sub - :get-available-commands - :<- [:get-commands-for-chat] - :<- [:get-current-chat] - available-commands) - -(reg-sub - :get-all-available-commands - :<- [:get-commands-for-chat] - (fn [commands] - (map->sorted-seq commands))) - -(reg-sub - :selected-chat-command - :<- [:get-current-chat] - :<- [:get-current-chat-ui-prop :selection] - :<- [:get-commands-for-chat] - (fn [[{:keys [input-text]} selection commands]] - (commands/selected-chat-command input-text selection commands))) - -(reg-sub - :chat-input-placeholder - :<- [:get-current-chat] - :<- [:selected-chat-command] - (fn [[{:keys [input-text]} {:keys [params current-param-position]}]] - (when (string/ends-with? (or input-text "") chat-constants/spacing-char) - (get-in params [current-param-position :placeholder])))) - -(reg-sub - :chat-parameter-box - :<- [:get-current-chat] - :<- [:selected-chat-command] - (fn [[current-chat {:keys [current-param-position params]}]] - (when (and params current-param-position) - (get-in params [current-param-position :suggestions])))) - -(reg-sub - :show-parameter-box? - :<- [:chat-parameter-box] - :<- [:show-suggestions?] - :<- [:validation-messages] - (fn [[chat-parameter-box show-suggestions? validation-messages]] - (and chat-parameter-box - (not validation-messages) - (not show-suggestions?)))) - -(reg-sub - :show-suggestions-view? - :<- [:get-current-chat-ui-prop :show-suggestions?] - :<- [:get-current-chat] - :<- [:get-all-available-commands] - (fn [[show-suggestions? {:keys [input-text]} commands]] - (and (or show-suggestions? - (input-model/starts-as-command? (string/trim (or input-text "")))) - (seq commands)))) - -(reg-sub - :show-suggestions? - :<- [:show-suggestions-view?] - :<- [:selected-chat-command] - (fn [[show-suggestions-box? selected-command]] - (and show-suggestions-box? (not selected-command)))) - -(reg-sub - :is-request-answered? - :<- [:get-current-chat] - (fn [{:keys [requests]} [_ message-id]] - (not= "open" (get-in requests [message-id :status])))) - -(reg-sub - :unviewed-messages-count - (fn [[_ chat-id]] - (subscribe [:get-chat chat-id])) - (fn [{:keys [unviewed-messages]}] - (count unviewed-messages))) - -(reg-sub - :get-photo-path - :<- [:get-contacts] - (fn [contacts [_ id]] - (:photo-path (contacts id)))) - -(reg-sub - :get-last-message - (fn [[_ chat-id]] - (subscribe [:get-chat chat-id])) - (fn [{:keys [messages message-groups]}] - (->> (sort-message-groups message-groups messages) - first - second - last - :message-id - (get messages)))) - -(reg-sub - :chat-animations - (fn [db [_ key type]] - (let [chat-id (subscribe [:get-current-chat-id])] - (get-in db [:chat-animations @chat-id key type])))) - -(reg-sub - :get-chats-unread-messages-number - :<- [:get-active-chats] - (fn [chats _] - (apply + (map (comp count :unviewed-messages) (vals chats))))) - -(reg-sub - :transaction-confirmed? - (fn [db [_ tx-hash]] - (-> (get-in db [:wallet :transactions tx-hash :confirmations] "0") - (js/parseInt) - (>= transactions/confirmations-count-threshold)))) - -(reg-sub - :wallet-transaction-exists? - (fn [db [_ tx-hash]] - (not (nil? (get-in db [:wallet :transactions tx-hash]))))) - -(reg-sub - :chat/cooldown-enabled? - (fn [db] - (:chat/cooldown-enabled? db))) - -(reg-sub - :chat-cooldown-enabled? - :<- [:get-current-chat] - :<- [:chat/cooldown-enabled?] - (fn [[{:keys [public?]} cooldown-enabled?]] - (and public? - cooldown-enabled?))) diff --git a/src/status_im/chat/views/actions.cljs b/src/status_im/chat/views/actions.cljs deleted file mode 100644 index 446cda82853..00000000000 --- a/src/status_im/chat/views/actions.cljs +++ /dev/null @@ -1,39 +0,0 @@ -(ns status-im.chat.views.actions - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n])) - -(defn view-profile [chat-id] - {:label (i18n/label :t/view-profile) - :action #(re-frame/dispatch [:show-profile chat-id])}) - -(defn group-info [chat-id] - {:label (i18n/label :t/group-info) - :action #(re-frame/dispatch [:show-group-chat-profile chat-id])}) - -(defn- clear-history [] - {:label (i18n/label :t/clear-history) - :action #(re-frame/dispatch [:clear-history?])}) - -(defn- delete-chat [chat-id group?] - {:label (i18n/label :t/delete-chat) - :action #(re-frame/dispatch [:remove-chat-and-navigate-home? chat-id group?])}) - -(defn- chat-actions [chat-id] - [(view-profile chat-id) - (clear-history) - (delete-chat chat-id false)]) - -(defn- group-chat-actions [chat-id] - [(group-info chat-id) - (clear-history) - (delete-chat chat-id true)]) - -(defn- public-chat-actions [chat-id] - [(clear-history) - (delete-chat chat-id true)]) - -(defn actions [group-chat? chat-id public?] - (cond - public? (public-chat-actions chat-id) - group-chat? (group-chat-actions chat-id) - :else (chat-actions chat-id))) diff --git a/src/status_im/chat/views/bottom_info.cljs b/src/status_im/chat/views/bottom_info.cljs deleted file mode 100644 index fb4880a965f..00000000000 --- a/src/status_im/chat/views/bottom_info.cljs +++ /dev/null @@ -1,83 +0,0 @@ -(ns status-im.chat.views.bottom-info - (:require-macros [status-im.utils.views :refer [defview]]) - (:require [re-frame.core :as re-frame] - [reagent.core :as reagent] - [clojure.string :as string] - [status-im.ui.components.react :as react] - [status-im.chat.styles.screen :as styles] - [status-im.i18n :as i18n] - [status-im.ui.components.animation :as anim] - [status-im.ui.components.list.views :as list] - [status-im.chat.views.photos :as photos] - [status-im.utils.core :as utils] - [status-im.utils.identicon :as identicon])) - -(defn- container-animation-logic [{:keys [to-value val]}] - (fn [_] - (anim/start - (anim/spring val {:toValue to-value - :friction 6 - :tension 40})))) - -(defn overlay [{:keys [on-click-outside]} items] - [react/view styles/bottom-info-overlay - [react/touchable-highlight {:on-press on-click-outside - :style styles/overlay-highlight} - [react/view nil]] - items]) - -(defn container [height & _] - (let [anim-value (anim/create-value 1) - context {:to-value height - :val anim-value} - on-update (container-animation-logic context)] - (reagent/create-class - {:component-did-update - on-update - :display-name "container" - :reagent-render - (fn [height & children] - [react/animated-view {:style (styles/bottom-info-container height)} - (into [react/view] children)])}))) - -(defn- message-status-row [{:keys [photo-path name]} {:keys [whisper-identity status]}] - [react/view styles/bottom-info-row - [photos/photo - (or photo-path (identicon/identicon whisper-identity)) - styles/bottom-info-row-photo-size] - [react/view styles/bottom-info-row-text-container - [react/text {:style styles/bottom-info-row-text1 - :number-of-lines 1} - (utils/truncate-str (if-not (string/blank? name) - name - whisper-identity) 30)] - [react/text {:style styles/bottom-info-row-text2 - :number-of-lines 1} - (i18n/message-status-label (or status :sending))]]]) - -(defn- render-status [contacts] - (fn [{:keys [whisper-identity] :as row} _ _] - (let [contact (get contacts whisper-identity)] - [message-status-row contact row]))) - -(defn bottom-info-view [] - (let [bottom-info (re-frame/subscribe [:get-current-chat-ui-prop :bottom-info]) - contacts (re-frame/subscribe [:get-contacts])] - (reagent/create-class - {:display-name "bottom-info-view" - :reagent-render - (fn [] - (let [{:keys [user-statuses message-status participants]} @bottom-info - participants (->> participants - (map (fn [{:keys [identity]}] - [identity {:whisper-identity identity - :status message-status}])) - (into {})) - statuses (vals (merge participants user-statuses))] - [overlay {:on-click-outside #(re-frame/dispatch [:set-chat-ui-props {:show-bottom-info? false}])} - [container (* styles/item-height (count statuses)) - [list/flat-list {:contentContainerStyle styles/bottom-info-list-container - :data statuses - :key-fn :address - :render-fn (render-status @contacts) - :enableEmptySections true}]]]))}))) diff --git a/src/status_im/chat/views/input/animations/expandable.cljs b/src/status_im/chat/views/input/animations/expandable.cljs deleted file mode 100644 index b1def3dd4b3..00000000000 --- a/src/status_im/chat/views/input/animations/expandable.cljs +++ /dev/null @@ -1,39 +0,0 @@ -(ns status-im.chat.views.input.animations.expandable - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [reagent.core :as reagent] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.react :as react] - [status-im.chat.styles.animations :as style] - [status-im.chat.styles.input.input :as input-style] - [status-im.utils.platform :as platform])) - -(def top-offset 100) - -(defn expandable-view-on-update [anim-value animation-height] - (when animation-height - (animation/start - (animation/spring anim-value {:toValue animation-height - :friction 10 - :tension 60})))) - -(defview expandable-view [{:keys [key]} & elements] - (letsubs [anim-value (animation/create-value 0) - input-height [:get-current-chat-ui-prop :input-height] - input-focused? [:get-current-chat-ui-prop :input-focused?] - messages-focused? [:get-current-chat-ui-prop :messages-focused?] - chat-input-margin [:chat-input-margin] - keyboard-height [:get :keyboard-height] - chat-layout-height [:get :layout-height]] - (let [input-height (or input-height (+ input-style/padding-vertical - input-style/min-input-height - input-style/padding-vertical - input-style/border-height)) - bottom (+ input-height chat-input-margin) - max-height (- chat-layout-height (when platform/ios? keyboard-height) input-height top-offset)] - [react/view style/overlap-container - [react/animated-view {:style (style/expandable-container anim-value bottom max-height)} - (into [react/scroll-view {:keyboard-should-persist-taps :always - :on-content-size-change #(expandable-view-on-update anim-value %2) - :bounces false}] - (when (or input-focused? (not messages-focused?)) - elements))]]))) diff --git a/src/status_im/chat/views/input/input.cljs b/src/status_im/chat/views/input/input.cljs deleted file mode 100644 index 21d07384204..00000000000 --- a/src/status_im/chat/views/input/input.cljs +++ /dev/null @@ -1,127 +0,0 @@ -(ns status-im.chat.views.input.input - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [reagent.core :as reagent] - [re-frame.core :as re-frame] - [status-im.chat.constants :as constants] - [status-im.chat.styles.input.input :as style] - [status-im.chat.views.input.parameter-box :as parameter-box] - [status-im.chat.views.input.send-button :as send-button] - [status-im.chat.views.input.suggestions :as suggestions] - [status-im.chat.views.input.validation-messages :as validation-messages] - [status-im.i18n :as i18n] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vi] - [status-im.utils.platform :as platform] - [status-im.utils.utils :as utils])) - -(defview basic-text-input [{:keys [set-container-width-fn height single-line-input?]}] - (letsubs [{:keys [input-text]} [:get-current-chat] - cooldown-enabled? [:chat-cooldown-enabled?]] - [react/text-input - (merge - {:ref #(when % (re-frame/dispatch [:set-chat-ui-props {:input-ref %}])) - :accessibility-label :chat-message-input - :multiline (not single-line-input?) - :default-value (or input-text "") - :editable (not cooldown-enabled?) - :blur-on-submit false - :on-focus #(re-frame/dispatch [:set-chat-ui-props {:input-focused? true - :messages-focused? false}]) - :on-blur #(re-frame/dispatch [:set-chat-ui-props {:input-focused? false}]) - :on-submit-editing #(when single-line-input? - (re-frame/dispatch [:send-current-message])) - :on-layout #(set-container-width-fn (.-width (.-layout (.-nativeEvent %)))) - :on-change #(re-frame/dispatch [:set-chat-input-text (.-text (.-nativeEvent %))]) - :on-selection-change #(let [s (-> (.-nativeEvent %) - (.-selection)) - end (.-end s)] - (re-frame/dispatch [:update-text-selection end])) - :style (style/input-view single-line-input?) - :placeholder-text-color colors/gray - :auto-capitalize :sentences} - (when cooldown-enabled? - {:placeholder (i18n/label :cooldown/text-input-disabled)}))])) - -(defview invisible-input [{:keys [set-layout-width-fn value]}] - (letsubs [{:keys [input-text]} [:get-current-chat]] - [react/text {:style style/invisible-input-text - :on-layout #(let [w (-> (.-nativeEvent %) - (.-layout) - (.-width))] - (set-layout-width-fn w))} - (or input-text "")])) - -(defn- input-helper-view-on-update [{:keys [opacity-value placeholder]}] - (fn [_] - (let [to-value (if @placeholder 1 0)] - (animation/start - (animation/timing opacity-value {:toValue to-value - :duration 300}))))) - -(defview input-helper [{:keys [width]}] - (letsubs [placeholder [:chat-input-placeholder] - opacity-value (animation/create-value 0) - on-update (input-helper-view-on-update {:opacity-value opacity-value - :placeholder placeholder})] - {:component-did-update on-update} - [react/animated-view {:style (style/input-helper-view width opacity-value)} - [react/text {:style (style/input-helper-text width)} - placeholder]])) - -(defn get-options [type] - (case (keyword type) - :phone {:keyboard-type "phone-pad"} - :password {:secure-text-entry true} - :number {:keyboard-type "numeric"} - nil)) - -(defview input-view [{:keys [single-line-input?]}] - (letsubs [command [:selected-chat-command]] - (let [component (reagent/current-component) - set-layout-width-fn #(reagent/set-state component {:width %}) - set-container-width-fn #(reagent/set-state component {:container-width %}) - {:keys [width]} (reagent/state component)] - [react/view {:style style/input-root} - [react/animated-view {:style style/input-animated} - [invisible-input {:set-layout-width-fn set-layout-width-fn}] - [basic-text-input {:set-container-width-fn set-container-width-fn - :single-line-input? single-line-input?}] - [input-helper {:width width}]]]))) - -(defview commands-button [] - (letsubs [commands [:get-all-available-commands]] - (when (seq commands) - [react/touchable-highlight - {:on-press #(do (re-frame/dispatch [:set-chat-input-text constants/command-char]) - (re-frame/dispatch [:chat-input-focus :input-ref])) - :accessibility-label :chat-commands-button} - [react/view - [vi/icon :icons/input-commands {:container-style style/input-commands-icon - :color :dark}]]]))) - -(defview input-container [] - (letsubs [margin [:chat-input-margin] - {:keys [input-text]} [:get-current-chat] - result-box [:get-current-chat-ui-prop :result-box]] - (let [single-line-input? (:singleLineInput result-box)] - [react/view {:style (style/root margin) - :on-layout #(let [h (-> (.-nativeEvent %) - (.-layout) - (.-height))] - (when (> h 0) - (re-frame/dispatch [:set-chat-ui-props {:input-height h}])))} - [react/view {:style style/input-container} - [input-view {:single-line-input? single-line-input?}] - (if (string/blank? input-text) - [commands-button] - [send-button/send-button-view])]]))) - -(defn container [] - [react/view - [parameter-box/parameter-box-view] - [suggestions/suggestions-view] - [validation-messages/validation-messages-view] - [input-container]]) diff --git a/src/status_im/chat/views/input/parameter_box.cljs b/src/status_im/chat/views/input/parameter_box.cljs deleted file mode 100644 index 0afffb70993..00000000000 --- a/src/status_im/chat/views/input/parameter_box.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.chat.views.input.parameter-box - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.chat.views.input.animations.expandable :as expandable] - [status-im.chat.styles.input.parameter-box :as style] - [status-im.ui.components.react :as react] - [taoensso.timbre :as log])) - -(defview parameter-box-container [] - (letsubs [parameter-box [:chat-parameter-box]] - (when parameter-box - [react/view style/root - [parameter-box]]))) - -(defview parameter-box-view [] - (letsubs [show-box? [:show-parameter-box?]] - (when show-box? - [react/view] - [expandable/expandable-view {:key :parameter-box} - [parameter-box-container]]))) diff --git a/src/status_im/chat/views/input/send_button.cljs b/src/status_im/chat/views/input/send_button.cljs deleted file mode 100644 index 8aa501b8bba..00000000000 --- a/src/status_im/chat/views/input/send_button.cljs +++ /dev/null @@ -1,38 +0,0 @@ -(ns status-im.chat.views.input.send-button - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.chat.styles.input.send-button :as style] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vi] - [status-im.utils.utils :as utils])) - -(defn send-button-view-on-update [{:keys [spin-value command-completion]}] - (fn [_] - (let [to-spin-value (if (#{:complete :no-command} command-completion) 1 0)] - (animation/start - (animation/timing spin-value {:toValue to-spin-value - :duration 300}))))) - -(defn sendable? [input-text] - (let [trimmed (string/trim input-text)] - (not (or (string/blank? trimmed) (= trimmed "/"))))) - -(defview send-button-view [] - (letsubs [{:keys [command-completion]} [:selected-chat-command] - {:keys [input-text seq-arg-input-text]} [:get-current-chat] - spin-value (animation/create-value 1)] - {:component-did-update (send-button-view-on-update {:spin-value spin-value - :command-completion command-completion})} - (when (and (sendable? input-text) - (or (not command-completion) - (#{:complete :less-than-needed} command-completion))) - [react/touchable-highlight {:on-press #(re-frame/dispatch [:send-current-message])} - (let [spin (.interpolate spin-value (clj->js {:inputRange [0 1] - :outputRange ["0deg" "90deg"]}))] - [react/animated-view - {:style (style/send-message-container spin) - :accessibility-label :send-message-button} - [vi/icon :icons/input-send {:container-style style/send-message-icon - :color :white}]])]))) diff --git a/src/status_im/chat/views/input/suggestions.cljs b/src/status_im/chat/views/input/suggestions.cljs deleted file mode 100644 index f6552cca2c3..00000000000 --- a/src/status_im/chat/views/input/suggestions.cljs +++ /dev/null @@ -1,36 +0,0 @@ -(ns status-im.chat.views.input.suggestions - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.chat.styles.input.suggestions :as style] - [status-im.chat.views.input.animations.expandable :as expandable] - [status-im.chat.commands.core :as commands] - [status-im.i18n :as i18n] - [taoensso.timbre :as log])) - -(defn suggestion-item [{:keys [on-press name description last? accessibility-label]}] - [react/touchable-highlight (cond-> {:on-press on-press} - accessibility-label (assoc :accessibility-label accessibility-label)) - [react/view style/item-suggestion-container - [react/text {:style style/item-suggestion-name} - name] - [react/text {:style style/item-suggestion-description - :number-of-lines 2} - description]]]) - -(defview suggestions-view [] - (letsubs [available-commands [:get-available-commands]] - [expandable/expandable-view {:key :suggestions} - [react/view - [react/scroll-view {:keyboard-should-persist-taps :always - :bounces false} - (when (seq available-commands) - (map-indexed - (fn [i {:keys [type] :as command}] - ^{:key i} - [suggestion-item {:on-press #(re-frame/dispatch [:select-chat-input-command command nil]) - :name (commands/command-name type) - :description (commands/command-description type) - :last? (= i (dec (count available-commands))) - :accessibility-label (commands/accessibility-label type)}]) - available-commands))]]])) diff --git a/src/status_im/chat/views/input/utils.cljs b/src/status_im/chat/views/input/utils.cljs deleted file mode 100644 index e1d5e154b78..00000000000 --- a/src/status_im/chat/views/input/utils.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.chat.views.input.utils - (:require [taoensso.timbre :as log] - [status-im.ui.components.toolbar.styles :as toolbar-st] - [status-im.utils.platform :as p])) - -(def min-height 19) -(def default-height 300) - -(defn default-container-area-height [bottom screen-height] - (let [status-bar-height (get p/platform-specific :status-bar-default-height)] - (if (> (+ bottom default-height status-bar-height) screen-height) - (- screen-height bottom status-bar-height) - default-height))) - -(defn max-container-area-height [bottom screen-height] - (let [status-bar-height (get p/platform-specific :status-bar-default-height) - toolbar-height (:height toolbar-st/toolbar) - margin-top (+ status-bar-height (/ toolbar-height 2))] - (- screen-height bottom margin-top))) diff --git a/src/status_im/chat/views/input/validation_messages.cljs b/src/status_im/chat/views/input/validation_messages.cljs deleted file mode 100644 index c04b71f605d..00000000000 --- a/src/status_im/chat/views/input/validation_messages.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.chat.views.input.validation-messages - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.chat.styles.input.validation-message :as style] - [status-im.i18n :as i18n])) - -(defn validation-message [{:keys [title description]}] - [react/view style/message-container - [react/text {:style style/message-title} - title] - [react/text {:style style/message-description} - description]]) - -(defn- messages-list [markup] - [react/view {:flex 1} - markup]) - -(defview validation-messages-view [] - (letsubs [chat-input-margin [:chat-input-margin] - input-height [:get-current-chat-ui-prop :input-height] - validation-result [:validation-messages]] - (when validation-result - (let [message (if (string? validation-result) - {:title (i18n/label :t/error) - :description validation-result} - validation-result)] - [react/view (style/root (+ input-height chat-input-margin)) - [messages-list [validation-message message]]])))) diff --git a/src/status_im/chat/views/message/datemark.cljs b/src/status_im/chat/views/message/datemark.cljs deleted file mode 100644 index 352832286d1..00000000000 --- a/src/status_im/chat/views/message/datemark.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns status-im.chat.views.message.datemark - (:require [status-im.ui.components.react :as react] - [clojure.string :as str] - [status-im.chat.styles.message.datemark :as st])) - -(defn chat-datemark [value] - [react/view st/datemark-wrapper - [react/view st/datemark - [react/text {:style st/datemark-text} - (str/capitalize value)]]]) diff --git a/src/status_im/chat/views/message/message.cljs b/src/status_im/chat/views/message/message.cljs deleted file mode 100644 index 79633ec3872..00000000000 --- a/src/status_im/chat/views/message/message.cljs +++ /dev/null @@ -1,366 +0,0 @@ -(ns status-im.chat.views.message.message - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [reagent.core :as reagent] - [status-im.ui.components.react :as react] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.action-sheet :as action-sheet] - [status-im.chat.commands.core :as commands] - [status-im.chat.commands.receiving :as commands-receiving] - [status-im.chat.styles.message.message :as style] - [status-im.chat.styles.message.command-pill :as pill-style] - [status-im.chat.views.photos :as photos] - [status-im.constants :as constants] - [status-im.ui.components.chat-icon.screen :as chat-icon.screen] - [status-im.utils.core :as utils] - [status-im.utils.identicon :as identicon] - [status-im.utils.gfycat.core :as gfycat] - [status-im.utils.platform :as platform] - [status-im.i18n :as i18n] - [status-im.ui.components.colors :as colors] - [clojure.string :as string])) - -(defview message-content-status [] - (letsubs [{:keys [chat-id group-id name color public-key]} [:get-current-chat] - members [:get-current-chat-contacts]] - (let [{:keys [status]} (if group-id - {:status nil} - (first members))] - [react/view style/status-container - [chat-icon.screen/chat-icon-message-status chat-id group-id name color false] - [react/text {:style style/status-from - :font :default - :number-of-lines 1} - (if (string/blank? name) - (gfycat/generate-gfy public-key) - (or (i18n/get-contact-translated chat-id :name name) - (i18n/label :t/chat-name)))] - (when status - [react/text {:style style/status-text - :font :default} - status])]))) - -(defn message-content-audio [_] - [react/view style/audio-container - [react/view style/play-view - [react/image {:style style/play-image}]] - [react/view style/track-container - [react/view style/track] - [react/view style/track-mark] - [react/text {:style style/track-duration-text - :font :default} - "03:39"]]]) - -(defview message-content-command - [command-message] - (letsubs [id->command [:get-id->command]] - (when-let [command (commands-receiving/lookup-command-by-ref command-message id->command)] - (commands/generate-preview command command-message)))) - -(def rtl-characters-regex #"[^\u0591-\u06EF\u06FA-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]*?[\u0591-\u06EF\u06FA-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]") - -(defn right-to-left-text? [content] - (let [char (first content)] - (re-matches rtl-characters-regex char))) - -(defview message-timestamp [t justify-timestamp? outgoing command? content] - (when-not command? - (let [rtl? (right-to-left-text? content)] - [react/text {:style (style/message-timestamp-text justify-timestamp? outgoing rtl?)} t]))) - -(defn message-view - [{:keys [timestamp-str outgoing content] :as message} message-content {:keys [justify-timestamp?]}] - [react/view (style/message-view message) - message-content - [message-timestamp timestamp-str justify-timestamp? outgoing (or (get content :command-path) - (get content :command-ref)) - content]]) - -(def replacements - {"\\*[^*]+\\*" {:font-weight :bold} - "~[^~]+~" {:font-style :italic}}) - -(def regx-styled (re-pattern (string/join "|" (map first replacements)))) - -(def regx-url #"(?i)(?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9\-]+[.][a-z]{1,4}/?)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'\".,<>?«»“”‘’]){0,}") - -(defn- parse-str-regx [string regx matched-fn unmatched-fn] - (if (string? string) - (let [unmatched-text (as-> (->> (string/split string regx) - (remove nil?) - vec) $ - (if (zero? (count $)) - [nil] - (unmatched-fn $))) - matched-text (as-> (->> string - (re-seq regx) - matched-fn - vec) $ - (if (> (count unmatched-text) - (count $)) - (conj $ nil) - $))] - (mapcat vector unmatched-text matched-text)) - (str string))) - -(defn parse-url [string] - (parse-str-regx string - regx-url - (fn [text-seq] - (map (fn [[text]] {:text text :url? true}) text-seq)) - (fn [text-seq] - (map (fn [text] {:text text :url? false}) text-seq)))) - -(defn- autolink [string event-on-press] - (->> (parse-url string) - (map-indexed (fn [idx {:keys [text url?]}] - (if url? - (let [[url _ _ _ text] (re-matches #"(?i)^((\w+://)?(www\d{0,3}[.])?)?(.*)$" text)] - [react/text - {:key idx - :style {:color colors/blue} - :on-press #(re-frame/dispatch [event-on-press url])} - url]) - text))) - vec)) - -(defn get-style [string] - (->> replacements - (into [] (comp (map first) - (map #(vector % (re-pattern %))) - (drop-while (fn [[_ regx]] (not (re-matches regx string)))) - (take 1))) - ffirst - replacements)) - -;; todo rewrite this, naive implementation -(defn- parse-text [string event-on-press] - (parse-str-regx string - regx-styled - (fn [text-seq] - (map-indexed (fn [idx string] - (let [style (get-style string)] - [react/text - {:key (str idx "_" string) - :style style} - (subs string 1 (dec (count string)))])) - text-seq)) - (fn [text-seq] - (map-indexed (fn [idx string] - (apply react/text - {:key (str idx "_" string)} - (autolink string event-on-press))) - text-seq)))) - -; We can't use CSS as nested Text element don't accept margins nor padding -; so we pad the invisible placeholder with some spaces to avoid having too -; close to the text. -(defn timestamp-with-padding [t] - (str " " t)) - -(def cached-parse-text (memoize parse-text)) - -(def ^:private ^:const number-of-lines 20) -(def ^:private ^:const number-of-chars 600) - -(defn- should-collapse? [text group-chat?] - (and group-chat? - (or (<= number-of-chars (count text)) - (<= number-of-lines (count (re-seq #"\n" text)))))) - -(defn- expand-button [collapsed? on-press] - [react/text {:style style/message-expand-button - :on-press on-press} - (i18n/label (if @collapsed? :show-more :show-less))]) - -(defn text-message - [{:keys [content timestamp-str group-chat outgoing] :as message}] - [message-view message - (let [parsed-text (cached-parse-text content :browse-link-from-message) - ref (reagent/atom nil) - collapsible? (should-collapse? content group-chat) - collapsed? (reagent/atom collapsible?) - on-press (when collapsible? - #(do - (.setNativeProps @ref - (clj->js {:numberOfLines - (when-not @collapsed? - number-of-lines)})) - (reset! collapsed? (not @collapsed?))))] - [react/view - [react/text {:style (style/text-message collapsible?) - :number-of-lines (when collapsible? number-of-lines) - :ref (partial reset! ref)} - parsed-text - [react/text {:style (style/message-timestamp-placeholder-text outgoing)} (timestamp-with-padding timestamp-str)]] - (when collapsible? - [expand-button collapsed? on-press])]) - {:justify-timestamp? true}]) - -(defn emoji-message - [{:keys [content] :as message}] - [message-view message - [react/text {:style (style/emoji-message message)} content]]) - -(defmulti message-content (fn [_ message _] (message :content-type))) - -(defmethod message-content constants/text-content-type - [wrapper message] - [wrapper message [text-message message]]) - -(defmethod message-content constants/content-type-log-message - [wrapper message] - [wrapper message [text-message message]]) - -(defmethod message-content constants/content-type-status - [_ _] - [message-content-status]) - -;; TODO(janherich) in the future, `content-type-command-request` will be deprecated -;; as it's the same thing as command -(defmethod message-content constants/content-type-command-request - [wrapper message] - [wrapper message - [message-view message [message-content-command message]]]) - -(defmethod message-content constants/content-type-command - [wrapper message] - [wrapper message - [message-view message [message-content-command message]]]) - -(defmethod message-content constants/content-type-emoji - [wrapper message] - [wrapper message [emoji-message message]]) - -(defmethod message-content :default - [wrapper {:keys [content-type content] :as message}] - [wrapper message - [message-view message - [message-content-audio {:content content - :content-type content-type}]]]) - -(defn- text-status [status] - [react/view style/delivery-view - [react/text {:style style/delivery-text - :font :default} - (i18n/message-status-label status)]]) - -(defview group-message-delivery-status [{:keys [message-id current-public-key user-statuses] :as msg}] - (letsubs [{participants :contacts} [:get-current-chat] - contacts [:get-contacts]] - (let [outgoing-status (or (get-in user-statuses [current-public-key :status]) :sending) - delivery-statuses (dissoc user-statuses current-public-key) - delivery-statuses-count (count delivery-statuses) - seen-by-everyone (and (= delivery-statuses-count (count participants)) - (every? (comp (partial = :seen) :status second) delivery-statuses) - :seen-by-everyone)] - (if (or seen-by-everyone (zero? delivery-statuses-count)) - [text-status (or seen-by-everyone outgoing-status)] - [react/touchable-highlight - {:on-press #(re-frame/dispatch [:show-message-details {:message-status outgoing-status - :user-statuses delivery-statuses - :participants participants}])} - [react/view style/delivery-view - (for [[whisper-identity] (take 3 delivery-statuses)] - ^{:key whisper-identity} - [react/image {:source {:uri (or (get-in contacts [whisper-identity :photo-path]) - (identicon/identicon whisper-identity))} - :style {:width 16 - :height 16 - :border-radius 8}}]) - (if (> delivery-statuses-count 3) - [react/text {:style style/delivery-text - :font :default} - (str "+ " (- delivery-statuses-count 3))])]])))) - -(defn message-activity-indicator [] - [react/view style/message-activity-indicator - [react/activity-indicator {:animating true}]]) - -(defn message-not-sent-text [chat-id message-id] - [react/touchable-highlight {:on-press (fn [] (if platform/ios? - (action-sheet/show {:title (i18n/label :message-not-sent) - :options [{:label (i18n/label :resend-message) - :action #(re-frame/dispatch [:resend-message chat-id message-id])} - {:label (i18n/label :delete-message) - :destructive? true - :action #(re-frame/dispatch [:delete-message chat-id message-id])}]}) - (re-frame/dispatch - [:show-message-options {:chat-id chat-id - :message-id message-id}])))} - [react/view style/not-sent-view - [react/text {:style style/not-sent-text} - (i18n/message-status-label :not-sent)] - [react/view style/not-sent-icon - [vector-icons/icon :icons/warning {:color colors/red}]]]]) - -(defview command-status [{{:keys [network]} :params}] - (letsubs [current-network [:network-name]] - (when (and network (not= current-network network)) - [react/view style/not-sent-view - [react/text {:style style/not-sent-text} - (i18n/label :network-mismatch)] - [react/view style/not-sent-icon - [vector-icons/icon :icons/warning {:color colors/red}]]]))) - -(defn message-delivery-status - [{:keys [chat-id message-id current-public-key user-statuses content last-outgoing? outgoing message-type] :as message}] - (let [outgoing-status (or (get-in user-statuses [current-public-key :status]) :not-sent) - delivery-status (get-in user-statuses [chat-id :status]) - status (or delivery-status outgoing-status)] - (case status - :sending [message-activity-indicator] - :not-sent [message-not-sent-text chat-id message-id] - (if (and (not outgoing) - (:command content)) - [command-status content] - (when last-outgoing? - (if (= message-type :group-user-message) - [group-message-delivery-status message] - (if outgoing - [text-status status]))))))) - -(defview message-author-name [from message-username] - (letsubs [username [:get-contact-name-by-identity from]] - [react/text {:style style/message-author-name} (or username - message-username - (gfycat/generate-gfy from))])) ; TODO: We defensively generate the name for now, to be revisited when new protocol is defined - -(defn message-body - [{:keys [last-in-group? - display-photo? - display-username? - from - outgoing - username] :as message} content] - [react/view (style/group-message-wrapper message) - [react/view (style/message-body message) - (when display-photo? - [react/view style/message-author - (when last-in-group? - [react/touchable-highlight {:on-press #(re-frame/dispatch [:show-profile from])} - [react/view - [photos/member-photo from]]])]) - [react/view (style/group-message-view outgoing) - (when display-username? - [message-author-name from username]) - [react/view {:style (style/timestamp-content-wrapper message)} - content]]] - [react/view (style/delivery-status outgoing) - [message-delivery-status message]]]) - -(defn chat-message [{:keys [outgoing group-chat current-public-key content-type content] :as message}] - [react/view - [react/touchable-highlight {:on-press (fn [_] - (re-frame/dispatch [:set-chat-ui-props {:messages-focused? true}]) - (react/dismiss-keyboard!)) - :on-long-press #(when (= content-type constants/text-content-type) - (list-selection/share content (i18n/label :t/message)))} - [react/view {:accessibility-label :chat-item} - (let [incoming-group (and group-chat (not outgoing))] - [message-content message-body (merge message - {:current-public-key current-public-key - :group-chat group-chat - :incoming-group incoming-group})])]]]) diff --git a/src/status_im/chat/views/message/options.cljs b/src/status_im/chat/views/message/options.cljs deleted file mode 100644 index eb6fbb9c10c..00000000000 --- a/src/status_im/chat/views/message/options.cljs +++ /dev/null @@ -1,45 +0,0 @@ -(ns status-im.chat.views.message.options - (:require-macros [status-im.utils.views :refer [defview]]) - (:require [re-frame.core :as re-frame] - [reagent.core :as reagent] - [clojure.string :as string] - [status-im.ui.components.react :as react] - [status-im.ui.components.colors :as colors] - [status-im.chat.views.bottom-info :as bottom-info] - [status-im.chat.styles.screen :as styles] - [status-im.chat.styles.message.options :as options.styles] - [status-im.i18n :as i18n] - [status-im.ui.components.animation :as anim] - [status-im.ui.components.list.views :as list] - [status-im.utils.core :as utils] - [status-im.utils.identicon :as identicon] - [status-im.ui.components.icons.vector-icons :as vector-icons])) - -(defn action-item [{:keys [label icon style on-press]}] - [react/touchable-highlight {:on-press on-press} - [react/view options.styles/row - [react/view - [vector-icons/icon icon style]] - [react/view (merge options.styles/label style) - [react/i18n-text {:style (merge options.styles/label-text style) - :key label}]]]]) - -(defn view [] - (let [{:keys [chat-id message-id]} @(re-frame/subscribe [:get-current-chat-ui-prop :message-options]) - close-message-options-fn #(re-frame/dispatch [:set-chat-ui-props {:show-message-options? false}])] - [bottom-info/overlay {:on-click-outside close-message-options-fn} - [bottom-info/container (* styles/item-height 2) - [react/view - [react/view options.styles/title - [react/i18n-text {:style options.styles/title-text :key :message-not-sent}]] - [action-item {:label :resend-message - :icon :icons/refresh - :on-press #(do - (close-message-options-fn) - (re-frame/dispatch [:resend-message chat-id message-id]))}] - [action-item {:label :delete-message - :icon :icons/delete - :style {:color colors/red} - :on-press #(do - (close-message-options-fn) - (re-frame/dispatch [:delete-message chat-id message-id]))}]]]])) diff --git a/src/status_im/chat/views/photos.cljs b/src/status_im/chat/views/photos.cljs deleted file mode 100644 index 6732387acc9..00000000000 --- a/src/status_im/chat/views/photos.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.chat.views.photos - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.components.react :as react] - [status-im.chat.styles.photos :as style] - [status-im.utils.identicon :as identicon] - [clojure.string :as string] - [status-im.react-native.resources :as resources])) - -(defn- source [photo-path] - (if (and (not (string/blank? photo-path)) - (string/starts-with? photo-path "contacts://")) - (->> (string/replace photo-path #"contacts://" "") - (keyword) - (get resources/contacts)) - {:uri photo-path})) - -(defn photo [photo-path {:keys [size - accessibility-label]}] - [react/view {:style (style/photo-container size)} - [react/image {:source (source photo-path) - :style (style/photo size) - :accessibility-label (or accessibility-label :chat-icon)}] - [react/view {:style (style/photo-border size)}]]) - -(defview member-photo [from] - (letsubs [photo-path [:get-photo-path from]] - (photo (if (string/blank? photo-path) - (identicon/identicon from) - photo-path) - {:accessibility-label :member-photo - :size style/default-size}))) diff --git a/src/status_im/chat/views/toolbar_content.cljs b/src/status_im/chat/views/toolbar_content.cljs deleted file mode 100644 index a246f1c0896..00000000000 --- a/src/status_im/chat/views/toolbar_content.cljs +++ /dev/null @@ -1,91 +0,0 @@ -(ns status-im.chat.views.toolbar-content - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [cljs-time.core :as t] - [status-im.ui.components.react :as react] - - [status-im.i18n :as i18n] - [status-im.chat.views.photos :as photos] - [status-im.chat.styles.screen :as st] - [status-im.utils.datetime :as time] - [status-im.utils.platform :refer [platform-specific]] - [status-im.utils.gfycat.core :refer [generate-gfy]] - [status-im.constants :refer [console-chat-id]] - [status-im.ui.components.chat-icon.screen :as chat-icon.screen] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.styles :as common.styles])) - -(defn- online-text [contact chat-id] - (cond - (= console-chat-id chat-id) (i18n/label :t/available) - contact (let [last-online (get contact :last-online) - last-online-date (time/to-date last-online) - now-date (t/now)] - (if (and (pos? last-online) - (<= last-online-date now-date)) - (time/time-ago last-online-date) - (i18n/label :t/active-unknown))) - :else (i18n/label :t/active-unknown))) - -(defn- in-progress-text [{:keys [highestBlock currentBlock startBlock]}] - (let [total (- highestBlock startBlock) - ready (- currentBlock startBlock) - percentage (if (zero? ready) - 0 - (->> (/ ready total) - (* 100) - (.round js/Math)))] - - (str (i18n/label :t/sync-in-progress) " " percentage "% " currentBlock))) - -(defview last-activity [{:keys [sync-state accessibility-label]}] - (letsubs [state [:get :sync-data]] - [react/text {:style st/last-activity-text - :font :toolbar-subtitle - :accessibility-label accessibility-label} - (case sync-state - :in-progress (in-progress-text state) - :synced (i18n/label :t/sync-synced))])) - -(defn- group-last-activity [{:keys [contacts sync-state public?]}] - (if (or (= sync-state :in-progress) - (= sync-state :synced)) - [last-activity {:sync-state sync-state}] - (if public? - [react/view {:flex-direction :row} - [react/text {:style st/toolbar-subtitle - :font :toolbar-subtitle} - (i18n/label :t/public-group-status)]] - [react/view {:flex-direction :row} - [react/text {:style st/toolbar-subtitle - :font :toolbar-subtitle} - (if public? - (i18n/label :t/public-group-status) - (let [cnt (inc (count contacts))] - (i18n/label-pluralize cnt :t/members-active)))]]))) - -(defview toolbar-content-view [] - (letsubs [{:keys [group-chat color online contacts - public? chat-id] :as chat} [:get-current-chat] - chat-name [:get-current-chat-name] - show-actions? [:get-current-chat-ui-prop :show-actions?] - accounts [:get-accounts] - contact [:get-current-chat-contact] - sync-state [:sync-state]] - (let [has-subtitle? (or group-chat (not= :done sync-state))] - [react/view {:style st/toolbar-container} - [react/view {:margin-right 8} - [chat-icon.screen/chat-icon-view-toolbar chat-id group-chat chat-name color online]] - [react/view {:style st/chat-name-view} - [react/text {:style st/chat-name-text - :number-of-lines 1 - :font :toolbar-title - :accessibility-label :chat-name-text} - chat-name] - (if group-chat - [group-last-activity {:contacts contacts - :public? public? - :sync-state sync-state}] - (when has-subtitle? - [last-activity {:sync-state sync-state - :accessibility-label :last-seen-text}]))]]))) diff --git a/src/status_im/common/alert/effects.cljs b/src/status_im/common/alert/effects.cljs new file mode 100644 index 00000000000..ded9469c8b2 --- /dev/null +++ b/src/status_im/common/alert/effects.cljs @@ -0,0 +1,60 @@ +(ns status-im.common.alert.effects + (:require + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn show-popup + ([title content] + (show-popup title content nil nil)) + ([title content on-dismiss] + (show-popup title content on-dismiss nil)) + ([title content on-dismiss action-button] + (let [dismiss-button + (merge {:text "OK" + :style "cancel" + :accessibility-label :cancel-button} + (when on-dismiss {:onPress on-dismiss}))] + (rn/alert + title + content + (if action-button + (vector + action-button + dismiss-button) + (vector dismiss-button)) + (when on-dismiss + {:cancelable false}))))) + +(rf/reg-fx :effects.utils/show-popup + (fn [{:keys [title content on-dismiss]}] + (show-popup title content on-dismiss))) + +(defn show-confirmation + [{:keys [title content confirm-button-text on-accept on-cancel cancel-button-text + extra-options]}] + (rn/alert + title + content + ;; Styles are only relevant on iOS. On Android first button is 'neutral' and second is 'positive' + (concat + (vector (merge {:text (or cancel-button-text (i18n/label :t/cancel)) + :style "cancel" + :accessibility-label :cancel-button} + (when on-cancel {:onPress on-cancel})) + {:text (or confirm-button-text (i18n/label :t/ok)) + :onPress on-accept + :style "default" + :accessibility-label :confirm-button}) + (or extra-options nil)) + {:cancelable false})) + +(rf/reg-fx :effects.utils/show-confirmation + (fn [{:keys [title content confirm-button-text on-accept on-cancel cancel-button-text extra-options]}] + (show-confirmation {:title title + :content content + :confirm-button-text confirm-button-text + :cancel-button-text cancel-button-text + :on-accept on-accept + :on-cancel on-cancel + :extra-options extra-options}))) diff --git a/src/status_im/common/alert_banner/events.cljs b/src/status_im/common/alert_banner/events.cljs new file mode 100644 index 00000000000..a9e6420c13a --- /dev/null +++ b/src/status_im/common/alert_banner/events.cljs @@ -0,0 +1,47 @@ +(ns status-im.common.alert-banner.events + (:require [re-frame.core :as re-frame])) + +(defn add-alert-banner + [{:keys [db]} [banner]] + (let [current-banners-count (count (get db :alert-banners)) + db (assoc-in db [:alert-banners (:type banner)] banner)] + (cond-> {:db db} + (zero? current-banners-count) + (assoc :show-alert-banner [(:view-id db) (:theme db)])))) + +(defn remove-alert-banner + [{:keys [db]} [banner-type]] + (let [db (update-in db [:alert-banners] dissoc banner-type) + new-count (count (get db :alert-banners))] + (cond-> {:db db} + (zero? new-count) + (assoc :hide-alert-banner [(:view-id db) (:theme db)])))) + +(defn remove-all-alert-banners + [{:keys [db]}] + {:db (dissoc db :alert-banners) + :hide-alert-banner [(:view-id db) (:theme db)]}) + +;; Hide/Unhide will only toggle the visibility of alert banners without removing them. +;; Required for ios image picker, which doesn't allow top margin +(defn hide-alert-banners + [{:keys [db]}] + {:db (assoc db :alert-banners/hide? true)}) + +(defn unhide-alert-banners + [{:keys [db]}] + {:db (dissoc db :alert-banners/hide?)}) + +;; When push notifications are enabled and the app is closed then overlays get dismissed, +;; we need to restore them once app is relaunched +(defn restore-alert-banners + [{:keys [db]}] + (when-not (zero? (count (get db :alert-banners))) + {:show-alert-banner [(:view-id db) (:theme db)]})) + +(re-frame/reg-event-fx :alert-banners/add add-alert-banner) +(re-frame/reg-event-fx :alert-banners/remove remove-alert-banner) +(re-frame/reg-event-fx :alert-banners/remove-all remove-all-alert-banners) +(re-frame/reg-event-fx :alert-banners/hide hide-alert-banners) +(re-frame/reg-event-fx :alert-banners/unhide unhide-alert-banners) +(re-frame/reg-event-fx :alert-banners/restore-alert-banners restore-alert-banners) diff --git a/src/status_im/common/alert_banner/events_test.cljs b/src/status_im/common/alert_banner/events_test.cljs new file mode 100644 index 00000000000..3e6a7f42792 --- /dev/null +++ b/src/status_im/common/alert_banner/events_test.cljs @@ -0,0 +1,42 @@ +(ns status-im.common.alert-banner.events-test + (:require + [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.common.alert-banner.events :as events])) + +(deftest add-alert-banner-test + (testing "Alert banner is added" + (is (match? {:db {:alert-banners + {:alert {:text "Alert" + :type :alert}}} + :show-alert-banner [nil nil]} + (events/add-alert-banner {:db {}} + [{:text "Alert" + :type :alert}]))))) + +(deftest remove-alert-banner-test + (testing "Alert banner is removed" + (is (match? {:db {} + :hide-alert-banner [nil nil]} + (events/remove-alert-banner {:db {:alert-banners + {:alert {:text "Alert" + :type :alert}}}} + [:alert])))) + (testing "Alert banner is not removed" + (is (match? {:db {:alert-banners + {:alert {:text "Alert" + :type :alert}}}} + (events/remove-alert-banner {:db {:alert-banners + {:alert {:text "Alert" + :type :alert}}}} + [:error]))))) + +(deftest remove-all-alert-banners-test + (testing "All Alert banners are removed" + (is (match? {:db {} + :hide-alert-banner [nil nil]} + (events/remove-all-alert-banners {:db {:alert-banners + {:alert {:text "Alert" + :type :alert} + :error {:text "Error" + :type :error}}}}))))) diff --git a/src/status_im/common/alert_banner/style.cljs b/src/status_im/common/alert_banner/style.cljs new file mode 100644 index 00000000000..acbbbde4cc0 --- /dev/null +++ b/src/status_im/common/alert_banner/style.cljs @@ -0,0 +1,16 @@ +(ns status-im.common.alert-banner.style) + +(def border-radius 20) + +(def second-banner-wrapper + {:margin-top (- border-radius) + :overflow :hidden + :border-top-left-radius border-radius + :border-top-right-radius border-radius}) + +(defn hole-view + [background-color] + {:padding-top 11 + :align-items :center + :height 60 + :background-color background-color}) diff --git a/src/status_im/common/alert_banner/view.cljs b/src/status_im/common/alert_banner/view.cljs new file mode 100644 index 00000000000..88d39dd143c --- /dev/null +++ b/src/status_im/common/alert_banner/view.cljs @@ -0,0 +1,74 @@ +(ns status-im.common.alert-banner.view + (:require quo.context + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [react-native.safe-area :as safe-area] + [status-im.common.alert-banner.style :as style] + [status-im.constants :as constants] + [utils.re-frame :as rf])) + +(defn get-colors-map + [theme] + {:alert + {:background-color (colors/resolve-color :warning theme 20) + :text-color (colors/resolve-color :warning theme)} + :error + {:background-color (colors/resolve-color :danger theme 20) + :text-color (colors/resolve-color :danger theme)}}) + +(defn- banner + [{:keys [text type second-banner? colors-map on-press]}] + [rn/pressable + {:on-press #(when on-press (on-press)) + :style (when second-banner? style/second-banner-wrapper)} + [hole-view/hole-view + {:style (style/hole-view (get-in colors-map [type :background-color])) + :holes (if second-banner? + [] + [{:x 0 + :y constants/alert-banner-height + :width (:width (rn/get-window)) + :height constants/alert-banner-height + :borderRadius style/border-radius}])} + [quo/text + {:size :paragraph-2 + :weight :medium + :style {:color (get-in colors-map [type :text-color])}} + text]]]) + +(defn view + [] + (let [banners (rf/sub [:alert-banners]) + hide-banners? (rf/sub [:alert-banners/hide?]) + theme (quo.context/use-theme) + banners-count (count banners) + alert-banner (:alert banners) + error-banner (:error banners) + safe-area-top safe-area/top + colors-map (get-colors-map theme)] + (when-not hide-banners? + [hole-view/hole-view + ;; required for fix flicker issue https://github.com/status-im/status-mobile/issues/19490 + {:style {:padding-bottom 0.5} + :holes [{:x 0 + :y (+ safe-area-top (* constants/alert-banner-height banners-count)) + :width (:width (rn/get-window)) + :height constants/alert-banner-height + :borderRadius style/border-radius}]} + [rn/view {:style {:background-color colors/neutral-100}} + [rn/view + {:style {:height safe-area-top + :background-color (get-in colors-map + [(if error-banner :error :alert) :background-color])}}] + (when error-banner + [banner + (assoc error-banner + :colors-map colors-map + :second-banner? false)]) + (when alert-banner + [banner + (assoc alert-banner + :colors-map colors-map + :second-banner? (= 2 banners-count))])]]))) diff --git a/src/status_im/common/app_monitoring/effects.cljs b/src/status_im/common/app_monitoring/effects.cljs new file mode 100644 index 00000000000..7939d6f942c --- /dev/null +++ b/src/status_im/common/app_monitoring/effects.cljs @@ -0,0 +1,8 @@ +(ns status-im.common.app-monitoring.effects + (:require + [native-module.core :as native-module] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.app-monitoring/intended-panic + (fn [message] + (native-module/intended-panic message))) diff --git a/src/status_im/common/app_monitoring/events.cljs b/src/status_im/common/app_monitoring/events.cljs new file mode 100644 index 00000000000..d66ab5c6ef8 --- /dev/null +++ b/src/status_im/common/app_monitoring/events.cljs @@ -0,0 +1,8 @@ +(ns status-im.common.app-monitoring.events + (:require + status-im.common.app-monitoring.effects + [utils.re-frame :as rf])) + +(rf/reg-event-fx :app-monitoring/intended-panic + (fn [_ [message]] + {:fx [[:effects.app-monitoring/intended-panic message]]})) diff --git a/src/status_im/common/async_storage/effects.cljs b/src/status_im/common/async_storage/effects.cljs new file mode 100644 index 00000000000..9ee9722ee33 --- /dev/null +++ b/src/status_im/common/async_storage/effects.cljs @@ -0,0 +1,11 @@ +(ns status-im.common.async-storage.effects + (:require + [react-native.async-storage :as async-storage] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.async-storage/set + (async-storage/set-item-factory)) + +(rf/reg-fx :effects.async-storage/get + (fn [{ks :keys cb :cb}] + (async-storage/get-items ks cb))) diff --git a/src/status_im/common/avatar_picture_picker/view.cljs b/src/status_im/common/avatar_picture_picker/view.cljs new file mode 100644 index 00000000000..0a99191c845 --- /dev/null +++ b/src/status_im/common/avatar_picture_picker/view.cljs @@ -0,0 +1,61 @@ +(ns status-im.common.avatar-picture-picker.view + (:require + [quo.core :as quo] + [react-native.permissions :as permissions] + [react-native.platform :as platform] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def crop-size 1000) + +(def crop-opts + {:cropping true + :cropperCircleOverlay true + :width crop-size + :height crop-size}) + +(defn hide-sheet-and-dispatch + [event] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch event)) + +(defn view + [{:keys [on-result has-picture?]}] + [quo/action-drawer + [[{:icon :i/camera + :accessibility-label :take-photo-button + :label (i18n/label :t/profile-pic-take) + :on-press (fn [] + (permissions/request-permissions + {:permissions [(if platform/is-below-android-13? + :read-external-storage + :read-media-images) + :write-external-storage] + :on-allowed #(hide-sheet-and-dispatch [:image-crop-picker/show-camera + on-result crop-opts]) + :on-denied #(log/info + "user has denied permissions to click picture")}))} + {:icon :i/image + :accessibility-label :select-from-gallery-button + :label (i18n/label :t/profile-pic-pick) + :on-press (fn [] + (permissions/request-permissions + {:permissions [(if platform/is-below-android-13? + :read-external-storage + :read-media-images) + :write-external-storage] + :on-allowed #(hide-sheet-and-dispatch [:image-crop-picker/show on-result + crop-opts]) + :on-denied #(log/info + "user has denied permissions to select picture")}))} + (when has-picture? + {:accessibility-label :remove-profile-picture + :add-divider? true + :danger? true + :blur? true + :icon :i/delete + :label (i18n/label :t/profile-pic-remove) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (on-result nil))})]]]) diff --git a/src/status_im/common/biometric/effects.cljs b/src/status_im/common/biometric/effects.cljs new file mode 100644 index 00000000000..54b0ad0116a --- /dev/null +++ b/src/status_im/common/biometric/effects.cljs @@ -0,0 +1,72 @@ +(ns status-im.common.biometric.effects + (:require + [react-native.biometrics :as biometrics] + [status-im.common.biometric.utils :as utils] + [status-im.common.keychain.events :as keychain] + [status-im.constants :as constant] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-fx + :effects.biometric/get-supported-type + (fn [] + ;;NOTE: if we can't save user password, we can't use biometric + (keychain/can-save-user-password? + (fn [can-save?] + (when (and can-save? (not utils/android-device-blacklisted?)) + (-> (biometrics/get-supported-type) + (.then (fn [type] + (rf/dispatch [:biometric/set-supported-type type]))))))))) + +(rf/reg-fx + :effects.biometric/authenticate + (fn [{:keys [prompt-message on-success on-fail on-cancel on-done] + :or {on-done identity + on-success identity + on-cancel identity + on-fail identity}}] + (-> (biometrics/authenticate + {:prompt-message (or prompt-message (i18n/label :t/biometric-auth-reason-login)) + :fallback-prompt-message (i18n/label + :t/biometric-auth-login-ios-fallback-label) + :cancel-button-text (i18n/label :t/cancel)}) + (.then (fn [success?] + (on-done) + (if success? + (on-success) + (on-cancel)))) + (.catch (fn [err] + (on-done) + (on-fail err)))))) + +(rf/reg-fx + :effects.biometric/check-if-available + (fn [{:keys [key-uid on-success on-fail] + :or {on-success identity + on-fail identity}}] + (keychain/can-save-user-password? + (fn [can-save?] + (if-not can-save? + (on-fail (ex-info "cannot-save-user-password" + {:effect :effects.biometric/check-if-available})) + (-> (biometrics/get-available) + (.then (fn [available?] + (when-not available? + (throw (js/Error. "biometric-not-available"))))) + (.then #(keychain/get-auth-method! key-uid)) + (.then (fn [auth-method] + (if (= auth-method constant/auth-method-biometric) + (on-success auth-method) + (throw (js/Error. "biometric-not-enabled"))))) + (.catch (fn [err] + (let [message (.-message err)] + (on-fail (ex-info message + {:err err + :effect :effects.biometric/check-if-available})) + (when-not (or (= message "biometric-not-available") + (= message "biometric-not-enabled")) + (log/error "Failed to check if biometrics is available" + {:error err + :key-uid key-uid + :effect :effects.biometric/check-biometric}))))))))))) diff --git a/src/status_im/common/biometric/events.cljs b/src/status_im/common/biometric/events.cljs new file mode 100644 index 00000000000..9736ad41f64 --- /dev/null +++ b/src/status_im/common/biometric/events.cljs @@ -0,0 +1,79 @@ +(ns status-im.common.biometric.events + (:require + status-im.common.biometric.effects + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn set-supported-type + [{:keys [db]} [supported-type]] + {:db (assoc-in db [:biometrics :supported-type] supported-type)}) + +(rf/reg-event-fx :biometric/set-supported-type set-supported-type) + +(defn show-message + [_ [code]] + (let [content (case code + (:biometrics/fingerprints-not-enrolled-error + :biometrics/not-available-error) + (i18n/label :t/grant-fingerprints-permissions) + + :biometrics/ios-not-enrolled-error + (i18n/label :t/grant-face-id-permissions) + + :biometric/too-many-attempts + (i18n/label :t/biometric-too-many-attempts) + + (i18n/label :t/biometric-auth-error {:code code}))] + {:fx [[:effects.utils/show-popup + {:title (i18n/label :t/biometric-auth-login-error-title) + :content content}]]})) + +(rf/reg-event-fx :biometric/show-message show-message) + +(defn on-authentication-done + [{:keys [db]}] + {:db (assoc-in db [:biometrics :auth-pending?] false)}) + +(rf/reg-event-fx :biometric/on-authentication-done on-authentication-done) + +(defn authenticate + [{:keys [db]} [opts]] + (let [pending? (get-in db [:biometrics :auth-pending?])] + ;;NOTE: prompting biometric check while another one is pending triggers error + (when-not pending? + {:db (assoc-in db [:biometrics :auth-pending?] true) + :fx [[:effects.biometric/authenticate + (assoc opts :on-done #(rf/dispatch [:biometric/on-authentication-done]))]]}))) + +(rf/reg-event-fx :biometric/authenticate authenticate) + +(defn enable-biometrics + [{:keys [db]} [password]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:db (assoc db :auth-method constants/auth-method-biometric) + :fx [[:dispatch + [:keychain/save-password-and-auth-method + {:key-uid key-uid + :masked-password password}]]]})) + +(rf/reg-event-fx :biometric/enable enable-biometrics) + +(defn enable-biometrics-keycard + [{:keys [db]} [keycard-keys]] + {:db (assoc db :auth-method constants/auth-method-biometric) + :fx [[:dispatch [:keychain/save-keycard-keys-and-auth-method keycard-keys]]]}) + +(rf/reg-event-fx :biometric/enable-keycard enable-biometrics-keycard) + +(defn disable-biometrics + [{:keys [db]}] + (let [key-uid (get-in db [:profile/profile :key-uid]) + keycard-profile? (not (nil? (get-in db [:profile/profile :keycard-pairing])))] + {:db (assoc db :auth-method constants/auth-method-none) + :fx [(if keycard-profile? + [:keychain/clear-keycard-keys key-uid] + [:keychain/clear-user-password key-uid])]})) + +(rf/reg-event-fx :biometric/disable disable-biometrics) + diff --git a/src/status_im/common/biometric/events_test.cljs b/src/status_im/common/biometric/events_test.cljs new file mode 100644 index 00000000000..1837673cb02 --- /dev/null +++ b/src/status_im/common/biometric/events_test.cljs @@ -0,0 +1,68 @@ +(ns status-im.common.biometric.events-test + (:require [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.common.biometric.events :as sut] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.security.core :as security])) + +(deftest set-supported-biometrics-type-test + (testing "successfully setting supported biometrics type" + (let [cofx {:db {}} + supported-type constants/biometrics-type-face-id + expected {:db (assoc-in (:db cofx) [:biometrics :supported-type] supported-type)}] + (is (match? expected (sut/set-supported-type cofx [supported-type])))))) + +(deftest show-message-test + (testing "informs the user to enable biometrics from settings" + (let [cofx {:db {}} + expected {:fx [[:effects.utils/show-popup + {:title (i18n/label :t/biometric-auth-login-error-title) + :content (i18n/label :t/grant-fingerprints-permissions)}]]}] + (is (match? expected + (sut/show-message cofx + [:biometrics/not-available-error]))))) + + (testing "shows a generic error message" + (let [cofx {:db {}} + error-cause :test-error + expected {:fx [[:effects.utils/show-popup + {:title (i18n/label :t/biometric-auth-login-error-title) + :content (i18n/label :t/biometric-auth-error {:code error-cause})}]]}] + (is (match? expected + (sut/show-message cofx + [error-cause])))))) + +(deftest authenticate-biometrics-test + (testing "passing the right args to authenticate" + (let [cofx {:db {}} + args {:on-success identity + :on-fail identity + :prompt-message "test"} + result (sut/authenticate cofx [args])] + (is (= (:prompt-message args) (get-in result [:fx 0 1 :prompt-message]))) + (is (not (nil? (get-in result [:fx 0 1 :on-success])))) + (is (not (nil? (get-in result [:fx 0 1 :on-fail])))))) + + (testing "skips biometric check if another one pending" + (let [cofx {:db {:biometrics {:auth-pending? true}}} + result (sut/authenticate cofx [{}])] + (is (nil? result))))) + +(deftest enable-biometrics-test + (testing "successfully enabling biometrics" + (let [key-uid "test-uid" + password (security/mask-data "test-pw") + cofx {:db {:profile/profile {:key-uid key-uid}}} + expected-db (assoc (:db cofx) :auth-method constants/auth-method-biometric) + result (sut/enable-biometrics cofx [password])] + (is (match? expected-db (:db result))) + (is (= password (get-in result [:fx 0 1 1 :masked-password])))))) + +(deftest disable-biometrics-test + (testing "successfully disabling biometrics" + (let [key-uid "test-uid" + cofx {:db {:profile/profile {:key-uid key-uid}}} + expected {:db (assoc (:db cofx) :auth-method constants/auth-method-none) + :fx [[:keychain/clear-user-password key-uid]]}] + (is (match? expected (sut/disable-biometrics cofx)))))) diff --git a/src/status_im/common/biometric/utils.cljs b/src/status_im/common/biometric/utils.cljs new file mode 100644 index 00000000000..3ec10689d4b --- /dev/null +++ b/src/status_im/common/biometric/utils.cljs @@ -0,0 +1,23 @@ +(ns status-im.common.biometric.utils + (:require + [native-module.core :as native-module] + [react-native.platform :as platform] + [status-im.constants :as constants] + [utils.i18n :as i18n])) + +(def android-device-blacklisted? + (and platform/android? (= (:brand (native-module/get-device-model-info)) "bannedbrand"))) + +(defn get-label-by-type + [biometric-type] + (condp = biometric-type + constants/biometrics-type-android (i18n/label :t/biometric-fingerprint) + constants/biometrics-type-face-id (i18n/label :t/biometric-faceid) + constants/biometrics-type-touch-id (i18n/label :t/biometric-touchid) + (i18n/label :t/biometric))) + +(defn get-icon-by-type + [biometric-type] + (condp = biometric-type + constants/biometrics-type-face-id :i/face-id + :i/touch-id)) diff --git a/src/status_im/common/bottom_sheet/style.cljs b/src/status_im/common/bottom_sheet/style.cljs new file mode 100644 index 00000000000..beae838a832 --- /dev/null +++ b/src/status_im/common/bottom_sheet/style.cljs @@ -0,0 +1,60 @@ +(ns status-im.common.bottom-sheet.style + (:require + [quo.foundations.colors :as colors])) + +(def ^:private sheet-border-radius 20) + +(defn sheet + [{:keys [max-height]}] + {:position :absolute + :bottom 0 + :left 0 + :right 0 + :z-index 1 + :max-height max-height + :border-top-left-radius sheet-border-radius + :border-top-right-radius sheet-border-radius}) + +(def shell-bg + {:background-color colors/bottom-sheet-background-blur + :flex 1}) + +(def shell-bg-container + {:border-top-left-radius sheet-border-radius + :border-top-right-radius sheet-border-radius + :left 0 + :right 0 + :top 0 + :bottom 0 + :position :absolute + :overflow :hidden}) + +(defn sheet-content + [{:keys [theme padding-bottom shell?]}] + {:overflow :scroll + :padding-bottom padding-bottom + :border-top-left-radius sheet-border-radius + :border-top-right-radius sheet-border-radius + :background-color (if shell? + :transparent + (colors/theme-colors colors/white colors/neutral-95 theme))}) + +(defn selected-item + [theme top bottom selected-item-smaller-than-sheet? border-radius] + {:position :absolute + :top (when-not selected-item-smaller-than-sheet? (- 0 top)) + :bottom bottom + :overflow :hidden + :left 0 + :right 0 + :border-radius border-radius + :margin-horizontal 8 + :background-color (colors/theme-colors colors/white colors/neutral-90 theme)}) + +(def drag-handle + {:height 50 + :position :absolute + :top 0 + :left 0 + :right 0 + :z-index 2}) diff --git a/src/status_im/common/bottom_sheet/view.cljs b/src/status_im/common/bottom_sheet/view.cljs new file mode 100644 index 00000000000..4c40430b2c0 --- /dev/null +++ b/src/status_im/common/bottom_sheet/view.cljs @@ -0,0 +1,168 @@ +(ns status-im.common.bottom-sheet.view + (:require + [oops.core :as oops] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.hooks :as hooks] + [react-native.reanimated :as reanimated] + [status-im.common.bottom-sheet.style :as style] + [utils.number] + [utils.re-frame :as rf])) + +(def duration 450) +(def timing-options #js {:duration duration}) +(def bottom-margin 8) + +(defn hide + [translate-y bg-opacity window-height on-close] + (rf/dispatch [:dismiss-keyboard]) + (when (fn? on-close) + (on-close)) + ;; it will be better to use animation callback, but it doesn't work + ;; so we have to use timeout, also we add 50ms for safety + (js/setTimeout #(rf/dispatch [:bottom-sheet-hidden]) (+ duration 50)) + (reanimated/set-shared-value translate-y + (reanimated/with-timing window-height timing-options)) + (reanimated/set-shared-value bg-opacity (reanimated/with-timing 0 timing-options))) + +(defn show + [translate-y bg-opacity] + (reanimated/set-shared-value translate-y (reanimated/with-timing 0 timing-options)) + (reanimated/set-shared-value bg-opacity (reanimated/with-timing 1 timing-options))) + +(def gesture-values (atom {})) + +(defn get-sheet-gesture + [translate-y bg-opacity window-height on-close] + (-> (gesture/gesture-pan) + (gesture/on-start + (fn [_] + (swap! gesture-values assoc :pan-y (reanimated/get-shared-value translate-y)))) + (gesture/on-update + (fn [evt] + (let [tY (oops/oget evt "translationY")] + (swap! gesture-values assoc :dy (- tY (:pdy @gesture-values))) + (swap! gesture-values assoc :pdy tY) + (when (pos? tY) + (reanimated/set-shared-value + translate-y + (+ tY (:pan-y @gesture-values))))))) + (gesture/on-end + (fn [_] + (if (< (:dy @gesture-values) 0) + (show translate-y bg-opacity) + (hide translate-y bg-opacity window-height on-close)))))) + +(defn- get-layout-height + [event] + (oops/oget event "nativeEvent.layout.height")) + +(defn view + [{:keys [hide? insets]} + {:keys [content selected-item padding-bottom-override border-radius on-close shell? + gradient-cover? customization-color hide-handle? blur-radius + hide-on-background-press? drag-content?] + :or {border-radius 12 + hide-on-background-press? true + drag-content? true}}] + (let [theme (quo.context/use-theme) + {window-height :height} (rn/get-window) + [sheet-height set-sheet-height] (rn/use-state 0) + [layout-height set-layout-height] (rn/use-state window-height) + handle-sheet-height (rn/use-callback (fn [e] + (when (= sheet-height 0) + (set-sheet-height + (get-layout-height e)))) + [sheet-height]) + handle-layout-height (rn/use-callback (fn [e] + (-> (get-layout-height e) + (set-layout-height)))) + [item-height set-item-height] (rn/use-state 0) + handle-item-height (rn/use-callback (fn [e] + (when (= item-height 0) + (set-item-height + (get-layout-height e)))) + [item-height]) + bg-opacity (reanimated/use-shared-value 0) + translate-y (reanimated/use-shared-value window-height) + sheet-gesture (rn/use-memo #(get-sheet-gesture translate-y + bg-opacity + window-height + on-close) + [window-height on-close]) + selected-item-smaller-than-sheet? (< item-height + (- window-height + sheet-height + (:top insets) + (:bottom insets) + bottom-margin)) + top (- window-height (:top insets) sheet-height) + bottom (if selected-item-smaller-than-sheet? + (+ sheet-height bottom-margin) + (:bottom insets)) + sheet-max-height (- layout-height + (:top insets)) + content-padding-bottom (or padding-bottom-override + (+ (:bottom insets) bottom-margin))] + (rn/use-effect + #(if hide? + (hide translate-y bg-opacity window-height on-close) + (show translate-y bg-opacity)) + [hide?]) + (hooks/use-back-handler (fn [] + (when (fn? on-close) + (on-close)) + (rf/dispatch [:hide-bottom-sheet]) + true)) + [rn/view + {:style {:flex 1} + :on-layout handle-layout-height} + ;; backdrop + [rn/pressable + {:on-press #(when hide-on-background-press? (rf/dispatch [:hide-bottom-sheet])) + :style {:flex 1}} + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity bg-opacity} + {:flex 1 + :background-color (if shell? colors/neutral-100-opa-60 colors/neutral-100-opa-70)})}]] + ;; sheet + [(if drag-content? gesture/gesture-detector :<>) + (when drag-content? {:gesture sheet-gesture}) + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:transform [{:translateY translate-y}]} + (style/sheet {:max-height sheet-max-height}))} + (when shell? + [rn/view {:style style/shell-bg-container} + [quo/blur + {:style style/shell-bg + :blur-radius (or blur-radius 20) + :blur-amount 15 + :blur-type :transparent + :overlay-color :transparent}]]) + (when selected-item + [rn/view + {:on-layout handle-item-height + :style + (style/selected-item theme top bottom selected-item-smaller-than-sheet? border-radius)} + [selected-item]]) + [rn/view + {:on-layout handle-sheet-height + :style (style/sheet-content {:theme theme + :shell? shell? + :padding-bottom content-padding-bottom})} + (when (and gradient-cover? customization-color) + [quo/gradient-cover + {:customization-color customization-color + :opacity 0.4}]) + (when-not hide-handle? + [:<> + [quo/drawer-bar] + (when-not drag-content? + [gesture/gesture-detector {:gesture sheet-gesture} + [rn/view {:style style/drag-handle}]])]) + [content]]]]])) diff --git a/src/status_im/common/bottom_sheet_screen/style.cljs b/src/status_im/common/bottom_sheet_screen/style.cljs new file mode 100644 index 00000000000..65dc4cb8d3a --- /dev/null +++ b/src/status_im/common/bottom_sheet_screen/style.cljs @@ -0,0 +1,51 @@ +(ns status-im.common.bottom-sheet-screen.style + (:require + [quo.context] + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated] + [status-im.constants :as constants])) + +(defn container + [padding-top] + {:flex 1 + :padding-top padding-top}) + +(defn background + [opacity] + (reanimated/apply-animations-to-style + {:opacity opacity} + {:background-color colors/neutral-100-opa-70 + :position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0})) + +(defn main-view + [translate-y theme] + (reanimated/apply-animations-to-style + {:transform [{:translate-y translate-y}]} + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :border-top-left-radius 20 + :border-top-right-radius 20 + :flex 1 + :overflow :hidden + :padding-top 20})) + +(def handle-container + {:left 0 + :right 0 + :top 0 + :height constants/sheet-screen-handle-height + :z-index 2 + :position :absolute + :justify-content :center + :align-items :center}) + +(defn handle + [theme] + {:width 32 + :height 4 + :border-radius 100 + :background-color (colors/theme-colors colors/neutral-100 colors/white theme) + :opacity (if (= theme :light) 0.05 0.1)}) diff --git a/src/status_im/common/bottom_sheet_screen/view.cljs b/src/status_im/common/bottom_sheet_screen/view.cljs new file mode 100644 index 00000000000..ee523600417 --- /dev/null +++ b/src/status_im/common/bottom_sheet_screen/view.cljs @@ -0,0 +1,99 @@ +(ns status-im.common.bottom-sheet-screen.view + (:require + [oops.core :as oops] + [quo.context] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.bottom-sheet-screen.style :as style] + [utils.re-frame :as rf])) + +(def ^:const drag-threshold 200) + +(defn drag-gesture + [{:keys [translate-y opacity scroll-enabled? curr-scroll close reset-open-sheet set-animating-true]}] + (-> (gesture/gesture-pan) + (gesture/on-start (fn [e] + (set-animating-true) + (when (< (oops/oget e "velocityY") 0) + (reset! scroll-enabled? true)))) + (gesture/on-update (fn [e] + (let [translation (oops/oget e "translationY") + progress (Math/abs (/ translation drag-threshold))] + (when (pos? translation) + (reanimated/set-shared-value translate-y translation) + (reanimated/set-shared-value opacity (- 1 (/ progress 5))))))) + (gesture/on-end (fn [e] + (if (> (oops/oget e "translationY") drag-threshold) + (close) + (reset-open-sheet)))) + (gesture/on-finalize (fn [e] + (when (and (>= (oops/oget e "velocityY") 0) + (<= @curr-scroll (if platform/ios? -1 0))) + (reset! scroll-enabled? false)))))) + +(defn on-scroll + [e curr-scroll] + (let [y (oops/oget e "nativeEvent.contentOffset.y")] + (reset! curr-scroll y))) + +(defn view + [_] + (let [scroll-enabled? (reagent/atom true) + curr-scroll (reagent/atom 0) + animating? (reagent/atom true) + set-animating-true #(reset! animating? true) + set-animating-false (fn [ms] + (js/setTimeout #(reset! animating? false) ms)) + on-scroll-update #(on-scroll % curr-scroll)] + (fn [{:keys [content skip-background?]}] + (let [theme (quo.context/use-theme) + {:keys [top] :as insets} safe-area/insets + alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + padding-top (+ alert-banners-top-margin + (if platform/ios? top (+ top 10))) + {:keys [height]} (rn/get-window) + opacity (reanimated/use-shared-value 0) + translate-y (reanimated/use-shared-value height) + close (fn [] + (set-animating-true) + (reanimated/animate translate-y height 300) + (reanimated/animate opacity 0 300) + (rf/dispatch [:navigate-back]) + true) + reset-open-sheet (fn [] + (reanimated/animate translate-y 0 300) + (reanimated/animate opacity 1 300) + (set-animating-false 300) + (reset! scroll-enabled? true))] + (rn/use-mount + (fn [] + (rn/hw-back-add-listener close) + (reanimated/animate translate-y 0 300) + (reanimated/animate opacity 1 300) + (set-animating-false 300) + #(rn/hw-back-remove-listener close))) + [rn/view {:style (style/container padding-top)} + (when-not skip-background? + [reanimated/view {:style (style/background opacity)}]) + [gesture/gesture-detector + {:gesture (drag-gesture {:translate-y translate-y + :opacity opacity + :scroll-enabled? scroll-enabled? + :curr-scroll curr-scroll + :close close + :reset-open-sheet reset-open-sheet + :set-animating-true set-animating-true})} + [reanimated/view {:style (style/main-view translate-y theme)} + [rn/view {:style style/handle-container} + [rn/view {:style (style/handle theme)}]] + [content + {:insets insets + :close close + :scroll-enabled? scroll-enabled? + :current-scroll curr-scroll + :on-scroll on-scroll-update + :sheet-animating? animating?}]]]])))) diff --git a/src/status_im/common/check_before_syncing/style.cljs b/src/status_im/common/check_before_syncing/style.cljs new file mode 100644 index 00000000000..3ed0eeed52b --- /dev/null +++ b/src/status_im/common/check_before_syncing/style.cljs @@ -0,0 +1,24 @@ +(ns status-im.common.check-before-syncing.style) + +(def buttons-container + {:flex-direction :row + :justify-content :space-between + :gap 12 + :padding-vertical 12}) + +(def button {:flex 1}) + +(def checkbox + {:margin-top 16 + :flex-direction :row + :align-items :center}) + +(def checkbox-text {:margin-left 10}) + +(def checkboxes-container {:margin-bottom 10 :padding-right 10}) + +(def header {:margin-bottom 8}) + +(def description {:margin-right -33}) + +(def view-container {:padding-horizontal 33}) diff --git a/src/status_im/common/check_before_syncing/view.cljs b/src/status_im/common/check_before_syncing/view.cljs new file mode 100644 index 00000000000..3c06009c21c --- /dev/null +++ b/src/status_im/common/check_before_syncing/view.cljs @@ -0,0 +1,74 @@ +(ns status-im.common.check-before-syncing.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.check-before-syncing.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def checks + [(i18n/label :t/check-before-syncing-doc-checkbox-1) + (i18n/label :t/check-before-syncing-doc-checkbox-2) + (i18n/label :t/check-before-syncing-doc-checkbox-3)]) + +(defn- checkbox + [on-change customization-color description] + [rn/view {:style style/checkbox} + [quo/selectors + {:type :checkbox + :blur? true + :customization-color customization-color + :on-change on-change}] + [quo/text {:style style/checkbox-text} description]]) + +(defn- compute-new-checked-count + [set-checked-count checked?] + (if checked? + (set-checked-count inc) + (set-checked-count dec))) + +(defn- hide-bottom-sheet + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [{:keys [on-submit customization-color]}] + (let [[checked-count set-checked-count] (rn/use-state 0) + on-change (partial compute-new-checked-count set-checked-count) + render-checkbox (rn/use-callback + (partial checkbox on-change customization-color) + [customization-color]) + on-cancel hide-bottom-sheet + on-continue (rn/use-callback + (fn [] + (hide-bottom-sheet) + (on-submit)) + [on-submit])] + [rn/view + {:style style/view-container + :accessibility-label :check-before-syncing-bottom-sheet} + [quo/text + {:weight :semi-bold + :size :heading-2 + :style style/header} (i18n/label :t/check-before-syncing)] + [quo/text {:style style/description} (i18n/label :t/check-before-syncing-doc-description)] + + (into + [rn/view {:style style/checkboxes-container}] + (mapv render-checkbox checks)) + + [rn/view {:style style/buttons-container} + [quo/button + {:type :grey + :container-style style/button + :accessibility-label :cancel-button + :on-press on-cancel} + (i18n/label :t/cancel)] + [quo/button + {:container-style style/button + :disabled? (not= checked-count (count checks)) + :customization-color (or customization-color :blue) + :accessibility-label :continue-button + + :on-press on-continue} + (i18n/label :t/continue)]]])) diff --git a/src/status_im/common/confirmation_drawer/style.cljs b/src/status_im/common/confirmation_drawer/style.cljs new file mode 100644 index 00000000000..581235183e3 --- /dev/null +++ b/src/status_im/common/confirmation_drawer/style.cljs @@ -0,0 +1,19 @@ +(ns status-im.common.confirmation-drawer.style + (:require + [quo.foundations.colors :as colors])) + +(defn context-container + [theme] + {:flex-direction :row + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :border-radius 20 + :align-items :center + :align-self :flex-start + :padding 2 + :margin-top 4 + :margin-bottom 16}) + +(def buttons-container + {:flex-direction :row + :justify-content :space-between + :margin-top 25}) diff --git a/src/status_im/common/confirmation_drawer/view.cljs b/src/status_im/common/confirmation_drawer/view.cljs new file mode 100644 index 00000000000..808a4cf2811 --- /dev/null +++ b/src/status_im/common/confirmation_drawer/view.cljs @@ -0,0 +1,79 @@ +(ns status-im.common.confirmation-drawer.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.confirmation-drawer.style :as style] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn avatar + [group-chat color display-name photo-path] + (if group-chat + [quo/group-avatar + {:customization-color color + :size :size-20}] + [quo/user-avatar + {:full-name display-name + :profile-picture photo-path + :size :xxs + :status-indicator? false}])) + +(defn extra-action-view + [extra-action extra-text extra-action-selected?] + (when extra-action + [rn/view {:style {:margin-top 16 :flex-direction :row}} + [quo/selectors + {:type :checkbox + :on-change #(reset! extra-action-selected? %)}] + [quo/text {:style {:margin-left 10}} extra-text]])) + +(defn confirmation-drawer + [{:keys [title description context button-text on-press extra-action extra-text accessibility-label + close-button-text]}] + (let [extra-action-selected? (reagent/atom false)] + (fn [] + (let [{:keys [group-chat chat-id public-key color chat-type + profile-picture name]} context + id (or chat-id public-key) + theme (quo.context/use-theme) + [primary-name _] (when-not (or group-chat + (= chat-type constants/public-chat-type)) + (rf/sub [:contacts/contact-two-names-by-identity id])) + display-name (cond + (= primary-name "Unknown") + name + (= primary-name nil) + name + :else + primary-name) + photo-path (or profile-picture (rf/sub [:chats/photo-path id]))] + [rn/view + {:style {:margin-horizontal 20} + :accessibility-label accessibility-label} + [quo/text + {:weight :semi-bold + :size :heading-2} title] + [rn/view {:style (style/context-container theme)} + [avatar group-chat color display-name photo-path] + [quo/text + {:weight :medium + :size :paragraph-2 + :style {:margin-left 4}} display-name]] + [quo/text description] + [extra-action-view extra-action extra-text extra-action-selected?] + [rn/view {:style style/buttons-container} + [quo/button + {:type :grey + :container-style {:flex 0.48} ;;WUT? 0.48 , whats that ? + :on-press #(rf/dispatch [:hide-bottom-sheet])} + (or close-button-text (i18n/label :t/close))] + [quo/button + {:type :danger + :container-style {:flex 0.48} + :on-press #(do + (when @extra-action-selected? (extra-action)) + (on-press))} + button-text]]])))) diff --git a/src/status_im/common/contact_list/style.cljs b/src/status_im/common/contact_list/style.cljs new file mode 100644 index 00000000000..cf45907a5c0 --- /dev/null +++ b/src/status_im/common/contact_list/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.common.contact-list.style + (:require [quo.foundations.colors :as colors])) + +(def contacts-section-footer + {:height 8}) + +(defn contacts-section-header + [theme] + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) diff --git a/src/status_im/common/contact_list/view.cljs b/src/status_im/common/contact_list/view.cljs new file mode 100644 index 00000000000..257d3a44244 --- /dev/null +++ b/src/status_im/common/contact_list/view.cljs @@ -0,0 +1,16 @@ +(ns status-im.common.contact-list.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.contact-list.style :as style])) + +(defn contacts-section-footer + [] + [rn/view style/contacts-section-footer]) + +(defn contacts-section-header + [{:keys [title]}] + (let [theme (quo.context/use-theme)] + [quo/divider-label {:container-style (style/contacts-section-header theme)} + title])) diff --git a/src/status_im/common/contact_list_item/view.cljs b/src/status_im/common/contact_list_item/view.cljs new file mode 100644 index 00000000000..90b88c5589c --- /dev/null +++ b/src/status_im/common/contact_list_item/view.cljs @@ -0,0 +1,30 @@ +(ns status-im.common.contact-list-item.view + (:require + [quo.core :as quo] + [utils.address :as address] + [utils.re-frame :as rf])) + +(defn contact-list-item + [{:keys [on-press on-long-press accessory allow-multiple-presses? disabled?]} + {:keys [primary-name secondary-name public-key compressed-key ens-verified added? + container-style]} + theme] + (let [photo-path (rf/sub [:chats/photo-path public-key]) + online? (rf/sub [:visibility-status-updates/online? public-key]) + customization-color (rf/sub [:profile/customization-color])] + [quo/user + {:customization-color customization-color + :allow-multiple-presses? allow-multiple-presses? + :theme theme + :short-chat-key (address/get-shortened-compressed-key (or compressed-key public-key)) + :primary-name primary-name + :secondary-name secondary-name + :photo-path photo-path + :online? online? + :verified? ens-verified + :contact? added? + :on-press on-press + :on-long-press on-long-press + :accessory accessory + :container-style container-style + :disabled? disabled?}])) diff --git a/src/status_im/common/controlled_input/utils.cljs b/src/status_im/common/controlled_input/utils.cljs new file mode 100644 index 00000000000..bc1dba6dabc --- /dev/null +++ b/src/status_im/common/controlled_input/utils.cljs @@ -0,0 +1,186 @@ +(ns status-im.common.controlled-input.utils + (:require + [clojure.string :as string] + [status-im.contexts.wallet.common.utils :as wallet-utils] + [utils.money :as money])) + +(def ^:private default-max-limit 12) + +(def init-state + {:value "" + :error? false + :upper-limit nil + :lower-limit nil}) + +(defn input-value + [state] + (:value state)) + +(defn value-numeric + [state] + (or (parse-double (input-value state)) 0)) + +(defn value-bn + [state] + (money/bignumber (input-value state))) + +(defn input-error + [state] + (:error? state)) + +(defn- set-input-error + [state error?] + (assoc state :error? error?)) + +(defn upper-limit + [state] + (:upper-limit state)) + +(defn upper-limit-bn + [state] + (money/bignumber (:upper-limit state))) + +(defn lower-limit + [state] + (:lower-limit state)) + +(defn lower-limit-bn + [state] + (money/bignumber (:lower-limit state))) + +(defn upper-limit-exceeded? + [state] + (and (upper-limit state) + (when (money/bignumber? (value-bn state)) + (money/greater-than (value-bn state) (upper-limit-bn state))))) + +(defn lower-limit-exceeded? + [state] + (and (lower-limit state) + (when (money/bignumber? (value-bn state)) + (money/less-than (value-bn state) (lower-limit-bn state))))) + +(defn- recheck-errorness + [state] + (set-input-error state + (or (upper-limit-exceeded? state) + (lower-limit-exceeded? state)))) + +(defn set-input-value + [state value] + (-> state + (assoc :value value) + recheck-errorness)) + +(defn set-value-numeric + [state value] + (set-input-value state (str value))) + +(defn set-upper-limit + [state limit] + (if limit + (-> state + (assoc :upper-limit limit) + recheck-errorness) + state)) + +(defn set-lower-limit + [state limit] + (if limit + (-> state + (assoc :lower-limit limit) + recheck-errorness) + state)) + +(defn increase + [state] + (set-input-value state (str (money/add (value-bn state) 1)))) + +(defn decrease + [state] + (set-input-value state (str (money/add (value-bn state) -1)))) + +(def ^:private not-digits-or-dot-pattern + #"[^0-9+\.]") + +(def ^:private dot ".") + +(defn- can-add-character? + [state character max-length] + (let [current (input-value state) + length-overflow? (>= (count current) max-length) + extra-dot? (and (= character dot) (string/includes? current dot)) + extra-leading-zero? (and (= current "0") (= "0" (str character))) + non-numeric? (re-find not-digits-or-dot-pattern (str character))] + (not (or non-numeric? extra-dot? extra-leading-zero? length-overflow?)))) + +(defn- normalize-value-as-numeric + [value character] + (cond + (and (string/blank? value) (= character dot)) + (str "0" character) + + (and (= value "0") (not= character dot)) + (str character) + + :else + (str value character))) + +(defn add-character + ([state character] + (add-character state character default-max-limit)) + ([state character max-length] + (if (can-add-character? state character max-length) + (set-input-value state + (normalize-value-as-numeric (input-value state) character)) + state))) + +(defn delete-last + ([state] + (delete-last state "")) + ([state default-value] + (let [value (input-value state) + new-value (subs value 0 (dec (count value)))] + (set-input-value state (if (string/blank? new-value) default-value new-value))))) + +(defn delete-all + [state] + (set-input-value state "")) + +(defn empty-value? + [state] + (string/blank? (:value state))) + +(defn zero-or-negative-value? + [state] + (<= (value-numeric state) 0)) + +(defn- fiat->crypto + [value conversion-rate] + (if-not (string/blank? value) + (-> value + (money/fiat->crypto conversion-rate) + (wallet-utils/cut-crypto-decimals-to-fit-usd-cents conversion-rate)) + "")) + +(defn- crypto->fiat + [value conversion-rate] + (if-not (string/blank? value) + (-> value + (money/crypto->fiat conversion-rate) + (wallet-utils/cut-fiat-balance-to-two-decimals)) + "")) + +(defn ->crypto + [state conversion-rate] + (-> state + (set-input-value (fiat->crypto (input-value state) conversion-rate)) + (set-upper-limit (fiat->crypto (upper-limit state) conversion-rate)) + (set-lower-limit (fiat->crypto (lower-limit state) conversion-rate)))) + +(defn ->fiat + [state conversion-rate] + (-> state + (set-input-value (crypto->fiat (input-value state) conversion-rate)) + (set-upper-limit (crypto->fiat (upper-limit state) conversion-rate)) + (set-lower-limit (crypto->fiat (lower-limit state) conversion-rate)))) diff --git a/src/status_im/common/data_confirmation_sheet/style.cljs b/src/status_im/common/data_confirmation_sheet/style.cljs new file mode 100644 index 00000000000..e0a465b93a3 --- /dev/null +++ b/src/status_im/common/data_confirmation_sheet/style.cljs @@ -0,0 +1,24 @@ +(ns status-im.common.data-confirmation-sheet.style + (:require [quo.foundations.colors :as colors])) + +(def heading + {:padding-left 20 + :padding-bottom 8}) + +(def message + {:padding-horizontal 20 + :padding-top 4}) + +(def warning + {:margin-horizontal 20 + :margin-top 10}) + +(def drawer-container + {:padding-horizontal 13 + :padding-top 16}) + +(def settings-subtext + {:color colors/white-opa-70 + :align-self :center + :margin-bottom 12}) + diff --git a/src/status_im/common/data_confirmation_sheet/view.cljs b/src/status_im/common/data_confirmation_sheet/view.cljs new file mode 100644 index 00000000000..5e0e4721630 --- /dev/null +++ b/src/status_im/common/data_confirmation_sheet/view.cljs @@ -0,0 +1,73 @@ +(ns status-im.common.data-confirmation-sheet.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.data-confirmation-sheet.style :as style] + [status-im.common.events-helper :as events-helper] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-choice-callback + [syncing-on-mobile-network?] + (rf/dispatch [:network/set-syncing-on-mobile-network syncing-on-mobile-network?]) + (events-helper/hide-bottom-sheet)) + +(def on-wifi-only (partial on-choice-callback false)) +(def on-mobile-and-wifi (partial on-choice-callback true)) + +(defn view + [] + (let [settings-drawer? (= (rf/sub [:view-id]) :screen/settings.syncing) + syncing-on-mobile-network? (rf/sub [:profile/syncing-on-mobile-network?])] + (rn/use-mount events-helper/dismiss-keyboard) + [:<> + [quo/text + {:weight :semi-bold + :size :heading-2 + :accessibility-label :data-confirmation-sheet-heading + :style style/heading} + (i18n/label (if settings-drawer? :t/sync-and-backup :t/which-connection-to-use))] + [quo/text + {:weight :regular + :size :paragraph-1 + :accessibility-label :data-confirmation-sheet-message + :style style/message} + (i18n/label :t/syncing-connection-message)] + [quo/information-box + {:type :default + :icon :i/info + :blur? true + :style style/warning} + (i18n/label :t/syncing-wifi-connection-warning)] + (if settings-drawer? + [rn/view {:style style/drawer-container} + [quo/drawer-action + {:title (i18n/label :t/mobile-data-and-wifi) + :state (when syncing-on-mobile-network? :selected) + :blur? true + :accessibility-label :mobile-data-and-wifi-action + :icon :i/connection + :on-press on-mobile-and-wifi}] + [quo/drawer-action + {:title (i18n/label :t/wifi-only) + :state (when-not syncing-on-mobile-network? :selected) + :blur? true + :accessibility-label :wifi-only-action + :icon :i/placeholder + :on-press on-wifi-only}]] + [quo/bottom-actions + {:actions :two-actions + :blur? true + :container-style {:margin-top 12} + :button-one-label (i18n/label :t/wifi-only) + :button-one-props {:type :grey + :on-press on-wifi-only} + :button-two-label (i18n/label :t/mobile-and-wifi) + :button-two-props {:on-press on-mobile-and-wifi}}]) + (if settings-drawer? + [quo/divider-line {:container-style {:padding-vertical 8}}] + [quo/text + {:weight :regular + :size :paragraph-2 + :style style/settings-subtext} + (i18n/label :t/you-can-change-later-in-settings)])])) diff --git a/src/status_im/common/device_permissions.cljs b/src/status_im/common/device_permissions.cljs new file mode 100644 index 00000000000..369c1f18108 --- /dev/null +++ b/src/status_im/common/device_permissions.cljs @@ -0,0 +1,16 @@ +(ns status-im.common.device-permissions + (:require + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn camera + [on-allowed] + (rf/dispatch + [:request-permissions + {:permissions [:camera] + :on-allowed on-allowed + :on-denied #(rf/dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text (i18n/label :t/camera-permission-denied)}])}])) diff --git a/src/status_im/common/emoji_picker/constants.cljs b/src/status_im/common/emoji_picker/constants.cljs new file mode 100644 index 00000000000..28c3c6d0e37 --- /dev/null +++ b/src/status_im/common/emoji_picker/constants.cljs @@ -0,0 +1,27 @@ +(ns status-im.common.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 categories-selector-height 56) + +(def ^:const emoji-item-margin-right + (/ (- (:width (rn/get-window)) + (* emoji-row-padding-horizontal 2) + (* emoji-size emojis-per-row)) + (dec emojis-per-row))) + +(def ^:const item-height (+ emoji-size emoji-row-separator-height)) diff --git a/src/status_im/common/emoji_picker/data.cljs b/src/status_im/common/emoji_picker/data.cljs new file mode 100644 index 00000000000..324cbd351ac --- /dev/null +++ b/src/status_im/common/emoji_picker/data.cljs @@ -0,0 +1,104 @@ +(ns status-im.common.emoji-picker.data + (:require + [status-im.common.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 [(-> group emoji-group->category :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)) diff --git a/src/status_im/common/emoji_picker/events.cljs b/src/status_im/common/emoji_picker/events.cljs new file mode 100644 index 00000000000..903a9b5db3c --- /dev/null +++ b/src/status_im/common/emoji_picker/events.cljs @@ -0,0 +1,8 @@ +(ns status-im.common.emoji-picker.events + (:require + [utils.re-frame :as rf])) + +(rf/defn open-emoji-picker + {:events [:emoji-picker/open]} + [_ {:keys [on-select]}] + {:dispatch [:open-modal :screen/emoji-picker {:on-select on-select}]}) diff --git a/src/status_im/common/emoji_picker/style.cljs b/src/status_im/common/emoji_picker/style.cljs new file mode 100644 index 00000000000..889cdaec2ea --- /dev/null +++ b/src/status_im/common/emoji_picker/style.cljs @@ -0,0 +1,53 @@ +(ns status-im.common.emoji-picker.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area] + [status-im.common.emoji-picker.constants :as constants])) + +(def flex-spacer {:flex 1}) + +(def category-nav-height (+ safe-area/bottom constants/categories-selector-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 constants/categories-selector-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}) diff --git a/src/status_im/common/emoji_picker/utils.cljs b/src/status_im/common/emoji_picker/utils.cljs new file mode 100644 index 00000000000..fbf4ebefac7 --- /dev/null +++ b/src/status_im/common/emoji_picker/utils.cljs @@ -0,0 +1,22 @@ +(ns status-im.common.emoji-picker.utils + (:require + [clojure.string :as string] + [status-im.common.emoji-picker.constants :as constants] + [status-im.common.emoji-picker.data :refer [emoji-data]])) + +(defn search-emoji + [search-query] + (let [cleaned-query (string/lower-case (string/trim search-query))] + (->> emoji-data + (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)))) + (partition-all constants/emojis-per-row)))) + +(defn random-emoji + [] + (:unicode (rand-nth emoji-data))) diff --git a/src/status_im/common/emoji_picker/utils_test.cljs b/src/status_im/common/emoji_picker/utils_test.cljs new file mode 100644 index 00000000000..d129a38e564 --- /dev/null +++ b/src/status_im/common/emoji_picker/utils_test.cljs @@ -0,0 +1,42 @@ +(ns status-im.common.emoji-picker.utils-test + (:require + [cljs.test :refer [deftest is testing]] + [status-im.common.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)))))) diff --git a/src/status_im/common/emoji_picker/view.cljs b/src/status_im/common/emoji_picker/view.cljs new file mode 100644 index 00000000000..c4c7fb61479 --- /dev/null +++ b/src/status_im/common/emoji_picker/view.cljs @@ -0,0 +1,210 @@ +(ns status-im.common.emoji-picker.view + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.platform :as platform] + [reagent.core :as reagent] + [status-im.common.emoji-picker.constants :as constants] + [status-im.common.emoji-picker.data :as emoji-picker.data] + [status-im.common.emoji-picker.style :as style] + [status-im.common.emoji-picker.utils :as emoji-picker.utils] + [utils.debounce :as debounce] + [utils.i18n :as i18n])) + +(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 (some-> event + (oops/oget "viewableItems") + (aget 0) + (oops/oget "item")) + header? (and (map? viewable-item) (:header? viewable-item)) + section-key (if header? + (:id viewable-item) + (-> viewable-item first :group emoji-picker.data/emoji-group->category :id))] + (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 [filtered-data on-viewable-items-changed scroll-enabled? on-scroll + on-select set-scroll-ref close sheet-animating?]}] + (let [theme (quo.context/use-theme)] + [gesture/flat-list + {:ref set-scroll-ref + :scroll-enabled @scroll-enabled? + :data (or filtered-data emoji-picker.data/flatten-data) + :initial-num-to-render 14 + :max-to-render-per-batch 10 + :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 + :window-size (if @sheet-animating? 1 10)}])) + +(defn- footer + [{:keys [active-category scroll-ref]}] + (let [theme (quo.context/use-theme) + 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} + [quo/blur + {:style style/category-blur-container + :blur-radius (if platform/android? 20 10) + :blur-amount (if platform/ios? 20 10) + :blur-type (if (= theme :light) (if platform/ios? :light :xlight) :dark) + :overlay-color (if (= theme :light) colors/white-70-blur colors/neutral-95-opa-70-blur)} + [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 sheet-view + [{:keys [search-active? on-change-text clear-states active-category scroll-ref] + :as sheet-opts}] + [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 + :on-change-text on-change-text + :clearable? search-active? + :on-clear clear-states}]] + [render-list sheet-opts] + (when-not search-active? + [footer + {:active-category active-category + :scroll-ref scroll-ref}])]]) + +(defn view + [_] + (let [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 [sheet-opts] + (let [{:keys [on-select]} (quo.context/use-screen-params)] + [sheet-view + (assoc sheet-opts + :search-active? (pos? (count @search-text)) + :on-change-text on-change-text + :clear-states clear-states + :filtered-data @filtered-data + :set-scroll-ref set-scroll-ref + :on-select on-select + :on-viewable-items-changed on-viewable-items-changed + :active-category active-category + :scroll-ref scroll-ref)])))) diff --git a/src/status_im/common/enter_seed_phrase/events.cljs b/src/status_im/common/enter_seed_phrase/events.cljs new file mode 100644 index 00000000000..c5974487323 --- /dev/null +++ b/src/status_im/common/enter_seed_phrase/events.cljs @@ -0,0 +1,11 @@ +(ns status-im.common.enter-seed-phrase.events + (:require + [utils.re-frame :as rf])) + +(rf/reg-event-fx :enter-seed-phrase/set-error + (fn [{:keys [db]} [error]] + {:db (assoc db :enter-seed-phrase/error error)})) + +(rf/reg-event-fx :enter-seed-phrase/clear-error + (fn [{:keys [db]}] + {:db (dissoc db :enter-seed-phrase/error)})) diff --git a/src/status_im/common/enter_seed_phrase/style.cljs b/src/status_im/common/enter_seed_phrase/style.cljs new file mode 100644 index 00000000000..50749277843 --- /dev/null +++ b/src/status_im/common/enter_seed_phrase/style.cljs @@ -0,0 +1,41 @@ +(ns status-im.common.enter-seed-phrase.style + (:require [react-native.platform :as platform])) + +(def full-layout {:flex 1}) + +(def page-container + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(defn recovery-phrase-container + [{:keys [banner-offset insets keyboard-shown?]}] + {:flex 1 + :padding-bottom (if keyboard-shown? + (when platform/ios? banner-offset) + (:bottom insets))}) + +(def form-container + {:flex 1 + :padding-horizontal 20 + :padding-vertical 12}) + +(def header-container + {:flex-direction :row + :justify-content :space-between}) + +(def word-count-container + {:justify-content :flex-end + :margin-bottom 2}) + +(def input-container + {:flex 1 + :margin-top 12 + :margin-horizontal -20}) + +(def continue-button + {:margin-top :auto}) + +(def keyboard-container {:margin-top :auto}) diff --git a/src/status_im/common/enter_seed_phrase/view.cljs b/src/status_im/common/enter_seed_phrase/view.cljs new file mode 100644 index 00000000000..246cca9b081 --- /dev/null +++ b/src/status_im/common/enter_seed_phrase/view.cljs @@ -0,0 +1,250 @@ +(ns status-im.common.enter-seed-phrase.view + (:require + [clojure.string :as string] + [legacy.status-im.ethereum.mnemonic :as mnemonic] + [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.enter-seed-phrase.style :as style] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(def ^:private max-seed-phrase-length + (apply max constants/seed-phrase-valid-length)) + +(defn- partial-word-in-dictionary? + [partial-word] + (some #(string/starts-with? % partial-word) mnemonic/dictionary)) + +(defn- word-in-dictionary? + [word] + (some #(= % word) mnemonic/dictionary)) + +(def ^:private partial-word-not-in-dictionary? + (comp not partial-word-in-dictionary?)) + +(def ^:private word-not-in-dictionary? + (comp not word-in-dictionary?)) + +(defn- header + [text seed-phrase-count] + [rn/view {:style style/header-container} + [quo/text {:weight :semi-bold :size :heading-1} + text] + [rn/view {:style style/word-count-container} + [quo/text + {:style {:color colors/white-opa-40} + :weight :regular + :size :paragraph-2} + (i18n/label-pluralize seed-phrase-count :t/words-n)]]]) + +(defn- clean-seed-phrase + [seed-phrase] + (-> seed-phrase + (string/lower-case) + (string/replace #", " " ") + (string/replace #"," " ") + (string/replace #"\s+" " ") + (string/trim))) + +(defn- secure-clean-seed-phrase + [seed-phrase] + (-> seed-phrase + security/safe-unmask-data + clean-seed-phrase + security/mask-data)) + +(defn- recovery-phrase-form + [{:keys [keypair title seed-phrase word-count on-change-seed-phrase ref]} & children] + (->> children + (into + [rn/view {:style style/form-container} + [header title word-count] + (when keypair + [quo/context-tag + {:type :icon + :container-style {:padding-top 8} + :icon :i/seed-phrase + :size 24 + :blur? true + :context (:name keypair)}]) + [rn/view {:style style/input-container} + [quo/recovery-phrase-input + {:accessibility-label :passphrase-input + :ref ref + :placeholder (i18n/label :t/seed-phrase-placeholder) + :placeholder-text-color colors/white-opa-30 + :auto-capitalize :none + :auto-correct false + :auto-focus true + :mark-errors? true + :word-limit max-seed-phrase-length + :error-pred-current-word partial-word-not-in-dictionary? + :error-pred-written-words word-not-in-dictionary? + :on-change-text on-change-seed-phrase} + seed-phrase]]]))) + +(defn keyboard-suggestions + [current-word] + (->> mnemonic/dictionary + (filter #(string/starts-with? % current-word)) + (take 7))) + +(defn recovery-phrase-screen + [{:keys [banner-offset initial-insets keypair title on-success render-controls]}] + (reagent/with-let [keyboard-shown? (reagent/atom false) + keyboard-show-listener (.addListener rn/keyboard + "keyboardDidShow" + #(reset! keyboard-shown? true)) + keyboard-hide-listener (.addListener rn/keyboard + "keyboardDidHide" + #(reset! keyboard-shown? false)) + invalid-seed-phrase? (reagent/atom false) + incorrect-seed-phrase? (reagent/atom false) + input-ref (reagent/atom nil) + focus-input #(some-> @input-ref + (oops/ocall "focus")) + set-incorrect-seed-phrase #(reset! incorrect-seed-phrase? true) + on-error #(reset! invalid-seed-phrase? true) + seed-phrase (reagent/atom "") + on-submit (fn [] + (swap! seed-phrase clean-seed-phrase) + (rf/dispatch + [:profile.recover/validate-recovery-phrase + (security/mask-data @seed-phrase) + {:on-success (fn [mnemonic key-uid] + (when on-success + (on-success + {:key-uid key-uid + :seed-phrase mnemonic + :on-error on-error}))) + :on-error on-error}]))] + (let [words-coll (mnemonic/passphrase->words @seed-phrase) + last-word (peek words-coll) + pick-suggested-word (fn [pressed-word] + (swap! seed-phrase str (subs pressed-word (count last-word)) " ")) + ;; Last word doesn't exist in dictionary while being written by the user, so + ;; it's validated checking whether is a substring of a dictionary word or not. + last-partial-word-valid? (partial-word-in-dictionary? last-word) + last-word-valid? (word-in-dictionary? last-word) + butlast-words-valid? (every? word-in-dictionary? (butlast words-coll)) + all-words-valid? (and butlast-words-valid? last-word-valid?) + word-count (mnemonic/words-count @seed-phrase) + words-exceeded? (> word-count max-seed-phrase-length) + error-in-words? (or (not last-partial-word-valid?) + (not butlast-words-valid?)) + upper-case? (boolean (re-find #"[A-Z]" @seed-phrase)) + global-error (rf/sub [:enter-seed-phrase/error]) + on-change-seed-phrase (fn [new-phrase] + (when global-error + (rf/dispatch [:enter-seed-phrase/clear-error])) + (when @invalid-seed-phrase? + (reset! invalid-seed-phrase? false)) + (when @incorrect-seed-phrase? + (reset! incorrect-seed-phrase? false)) + (reset! seed-phrase new-phrase)) + suggestions-state (cond + (or global-error + error-in-words? + words-exceeded? + @invalid-seed-phrase? + @incorrect-seed-phrase?) :error + (string/blank? @seed-phrase) :info + (string/ends-with? @seed-phrase " ") :empty + :else :words) + suggestions-text (cond + global-error global-error + upper-case? (i18n/label + :t/seed-phrase-words-uppercase) + words-exceeded? (i18n/label + :t/seed-phrase-words-exceeded) + error-in-words? (i18n/label :t/seed-phrase-error) + @invalid-seed-phrase? (i18n/label :t/seed-phrase-invalid) + @incorrect-seed-phrase? (i18n/label :t/seed-phrase-incorrect) + :else (i18n/label :t/seed-phrase-info)) + error-state? (= suggestions-state :error) + button-disabled? (or (and (not global-error) error-state?) + (not (constants/seed-phrase-valid-length word-count)) + (not all-words-valid?)) + button-label (if global-error + (i18n/label :t/enter-another-recovery-phrase) + (i18n/label :t/continue)) + reset-seed-phrase (fn [] + (reset! seed-phrase "") + (on-change-seed-phrase "")) + button-on-press (if global-error reset-seed-phrase on-submit)] + [rn/view + {:style (style/recovery-phrase-container {:insets initial-insets + :banner-offset banner-offset + :keyboard-shown? @keyboard-shown?})} + [recovery-phrase-form + {:title title + :keypair keypair + :seed-phrase @seed-phrase + :on-change-seed-phrase on-change-seed-phrase + :word-count word-count + :ref #(reset! input-ref %)} + (if (fn? render-controls) + (render-controls {:submit-disabled? button-disabled? + :keyboard-shown? @keyboard-shown? + :container-style style/continue-button + :prepare-seed-phrase secure-clean-seed-phrase + :focus-input focus-input + :seed-phrase (security/mask-data @seed-phrase) + :set-incorrect-seed-phrase set-incorrect-seed-phrase}) + [quo/button + {:container-style style/continue-button + :type :primary + :disabled? button-disabled? + :on-press button-on-press} + button-label])] + (when (or @keyboard-shown? error-state?) + [rn/view {:style style/keyboard-container} + [quo/predictive-keyboard + {:type suggestions-state + :blur? (not on-success) + :text suggestions-text + :container-style {:padding-right 20} + :words (keyboard-suggestions last-word) + :on-press pick-suggested-word}]])]) + (finally + (.remove keyboard-show-listener) + (.remove keyboard-hide-listener)))) + +(defn screen + [{:keys [title keypair navigation-icon on-success render-controls]}] + (let [[insets _] (rn/use-state safe-area/insets) + banner-offset (rf/sub [:alert-banners/top-margin])] + [rn/view {:style style/full-layout} + [rn/keyboard-avoiding-view {:style style/page-container} + [quo/page-nav + {:margin-top (:top insets) + :background :blur + :icon-name (or navigation-icon + (if on-success :i/close :i/arrow-left)) + :on-press #(rf/dispatch [:navigate-back])}] + [recovery-phrase-screen + {:title title + :keypair keypair + :render-controls render-controls + :banner-offset banner-offset + :initial-insets insets + :on-success on-success}]]])) + +(defn view + [] + (let [{:keys [on-success onboarding-flow?]} (quo.context/use-screen-params)] + (rn/use-unmount + (fn [] + (rf/dispatch [:enter-seed-phrase/clear-error]) + (when onboarding-flow? + (rf/dispatch [:onboarding/clear-navigated-to-enter-seed-phrase-from-screen])))) + [screen + {:title (i18n/label :t/use-recovery-phrase) + :on-success on-success}])) diff --git a/src/status_im/common/events_helper.cljs b/src/status_im/common/events_helper.cljs new file mode 100644 index 00000000000..aa08d4b8c28 --- /dev/null +++ b/src/status_im/common/events_helper.cljs @@ -0,0 +1,16 @@ +(ns status-im.common.events-helper + "Provides highly reusable dispatch functions to ensure consistency and avoid duplication in managing various events, + including navigation and other actions that affect application behavior." + (:require [utils.re-frame :as rf])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn hide-bottom-sheet + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn dismiss-keyboard + [] + (rf/dispatch [:dismiss-keyboard])) diff --git a/src/status_im/common/floating_button_page/component_spec.cljs b/src/status_im/common/floating_button_page/component_spec.cljs new file mode 100644 index 00000000000..960399cfc58 --- /dev/null +++ b/src/status_im/common/floating_button_page/component_spec.cljs @@ -0,0 +1,37 @@ +(ns status-im.common.floating-button-page.component-spec + (:require [quo.core :as quo] + [status-im.common.floating-button-page.view :as floating-button-page] + [test-helpers.component :as h])) + + +(def sub-mocks {:alert-banners/top-margin 0}) + +(h/describe "floating button page" + (h/test "renders with a header and standard button" + (h/setup-subs sub-mocks) + (h/render [floating-button-page/view + {:header [quo/page-nav + {:type :title-description + :title "floating button page" + :description "press right icon to swap button type" + :text-align :left + :background :blur + :icon-name :i/close}] + :footer [quo/button {} "continue"]}]) + (h/is-truthy (h/get-by-text "continue")) + (h/is-truthy (h/get-by-text "floating button page"))) + + (h/test "renders with a header and a slide button" + (h/setup-subs sub-mocks) + (h/render [floating-button-page/view + {:header [quo/page-nav + {:type :title-description + :title "floating button page" + :description "press right icon to swap button type" + :text-align :left + :background :blur + :icon-name :i/close}] + :footer [quo/slide-button + {:track-text "We gotta slide" + :track-icon :face-id}]}]) + (h/is-truthy (h/get-by-text "We gotta slide")))) diff --git a/src/status_im/common/floating_button_page/floating_container/style.cljs b/src/status_im/common/floating_button_page/floating_container/style.cljs new file mode 100644 index 00000000000..43a8984feac --- /dev/null +++ b/src/status_im/common/floating_button_page/floating_container/style.cljs @@ -0,0 +1,25 @@ +(ns status-im.common.floating-button-page.floating-container.style + (:require [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(defn content-container + [blur? keyboard-shown? {:keys [padding-vertical padding-horizontal]}] + (let [margin-bottom (if keyboard-shown? 0 safe-area/bottom)] + (cond-> {:margin-top :auto + :overflow :hidden + :margin-bottom margin-bottom + :padding-vertical (or padding-vertical 12) + :padding-horizontal (or padding-horizontal 20)} + blur? (dissoc :padding-vertical :padding-horizontal)))) + +(defn blur-inner-container + [{:keys [theme shell-overlay? padding-vertical padding-horizontal background-color]}] + {:background-color (or background-color + (colors/theme-colors + colors/white-70-blur + (if shell-overlay? + colors/neutral-80-opa-80-blur + colors/neutral-95-opa-70-blur) + theme)) + :padding-vertical (or padding-vertical 12) + :padding-horizontal (or padding-horizontal 20)}) diff --git a/src/status_im/common/floating_button_page/floating_container/view.cljs b/src/status_im/common/floating_button_page/floating_container/view.cljs new file mode 100644 index 00000000000..3442df5dba0 --- /dev/null +++ b/src/status_im/common/floating_button_page/floating_container/view.cljs @@ -0,0 +1,30 @@ +(ns status-im.common.floating-button-page.floating-container.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.floating-button-page.floating-container.style :as style])) + +(defn- blur-container + [shell-overlay? blur-options child] + (let [theme (quo.context/use-theme)] + [quo/blur + (or blur-options + {:blur-amount 20 + :blur-type :transparent + :overlay-color :transparent}) + [rn/view + {:style (style/blur-inner-container (assoc + blur-options + :theme theme + :shell-overlay? shell-overlay?))} + child]])) + +(defn view + [{:keys [on-layout keyboard-shown? blur? shell-overlay? blur-options]} child] + [rn/view + {:style (style/content-container blur? keyboard-shown? blur-options) + :on-layout on-layout} + (if blur? + [blur-container shell-overlay? blur-options child] + child)]) diff --git a/src/status_im/common/floating_button_page/style.cljs b/src/status_im/common/floating_button_page/style.cljs new file mode 100644 index 00000000000..72c3b5cc077 --- /dev/null +++ b/src/status_im/common/floating_button_page/style.cljs @@ -0,0 +1,25 @@ +(ns status-im.common.floating-button-page.style) + +(def page-container + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(def keyboard-avoiding-view + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(defn content-keyboard-avoiding-view + [{:keys [top bottom]}] + {:position :absolute + :top top + :left 0 + :right 0 + :bottom bottom}) + +(def scroll-view-container {:flex 1}) diff --git a/src/status_im/common/floating_button_page/view.cljs b/src/status_im/common/floating_button_page/view.cljs new file mode 100644 index 00000000000..1515083276b --- /dev/null +++ b/src/status_im/common/floating_button_page/view.cljs @@ -0,0 +1,148 @@ +(ns status-im.common.floating-button-page.view + (:require + [oops.core :as oops] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.floating-button-page.floating-container.view :as floating-container] + [status-im.common.floating-button-page.style :as style] + [utils.re-frame :as rf])) + +(defn- show-background + [{:keys [window-height keyboard-height footer-container-height content-scroll-y + content-container-height header-height keyboard-shown?]}] + (let [available-space (- window-height + safe-area/top + header-height + keyboard-height ; Already contains the bottom safe area value + footer-container-height) + scroll-view-height (- content-container-height content-scroll-y) + overlap? (< available-space scroll-view-height)] + (and keyboard-shown? overlap?))) + +(defn- set-height-on-layout + [ratom] + (fn [event] + (let [height (oops/oget event "nativeEvent.layout.height")] + (reset! ratom height)))) + +(defn- init-keyboard-listeners + [{:keys [on-did-show on-will-show scroll-view-ref]}] + (let [keyboard-will-show? (reagent/atom false) + keyboard-did-show? (reagent/atom false) + add-listener (fn [listener callback] + (oops/ocall rn/keyboard "addListener" listener callback)) + will-show-listener (add-listener "keyboardWillShow" + (fn [e] + (reset! keyboard-will-show? true) + (when on-will-show (on-will-show e)))) + did-show-listener (add-listener "keyboardDidShow" + (fn [e] + (reset! keyboard-did-show? true) + (when on-did-show (on-did-show e)))) + will-hide-listener (add-listener "keyboardWillHide" + (fn [] + (reset! keyboard-will-show? false) + (reagent/flush) + (.scrollTo @scroll-view-ref #js {:x 0 :y 0 :animated true}))) + did-hide-listener (add-listener "keyboardDidHide" + #(reset! keyboard-did-show? false)) + remove-listeners (fn [] + (doseq [listener [will-show-listener will-hide-listener + did-show-listener did-hide-listener]] + (oops/ocall listener "remove")))] + {:keyboard-will-show? keyboard-will-show? + :keyboard-did-show? keyboard-did-show? + :remove-listeners remove-listeners})) + +(defn view + [{:keys [header footer customization-color footer-container-padding header-container-style + content-container-style gradient-cover? keyboard-should-persist-taps shell-overlay? + blur-options content-avoid-keyboard? automatically-adjust-keyboard-insets + ;; Note: Provide `initial-header-height` to avoid a jump due to the on-layout 1-frame + ;; delay. Revisit this on RN 0.76 since the on-layout delay has been fixed. + initial-header-height] + :or {footer-container-padding safe-area/top}} + & children] + (reagent/with-let [scroll-view-ref (atom nil) + set-scroll-ref #(reset! scroll-view-ref %) + window-height (:height (rn/get-window)) + footer-container-height (reagent/atom 0) + header-height (reagent/atom nil) + content-container-height (reagent/atom 0) + content-scroll-y (reagent/atom 0) + keyboard-height (reagent/atom 0) + reset-keyboard-height #(reset! keyboard-height (oops/oget + % + "endCoordinates.height")) + {:keys [keyboard-will-show? + keyboard-did-show? + remove-listeners]} (init-keyboard-listeners + (cond-> {:scroll-view-ref scroll-view-ref} + platform/ios? + (assoc :on-will-show reset-keyboard-height) + (not platform/ios?) + (assoc :on-did-show reset-keyboard-height))) + set-header-height (set-height-on-layout header-height) + set-content-container-height (set-height-on-layout content-container-height) + set-footer-container-height (set-height-on-layout footer-container-height) + set-content-y-scroll (fn [event] + (reset! content-scroll-y + (oops/oget event "nativeEvent.contentOffset.y"))) + bottom-safe-area safe-area/bottom] + (let [keyboard-shown? (if platform/ios? @keyboard-will-show? @keyboard-did-show?) + footer-container-padding (+ footer-container-padding (rf/sub [:alert-banners/top-margin])) + show-background? (show-background + {:window-height window-height + :footer-container-height @footer-container-height + :keyboard-height @keyboard-height + :content-scroll-y @content-scroll-y + :content-container-height @content-container-height + :header-height @header-height + :keyboard-shown? keyboard-shown?})] + [:<> + (when gradient-cover? + [quo/gradient-cover {:customization-color customization-color}]) + [rn/view {:style style/page-container} + [rn/view + {:on-layout set-header-height + :style header-container-style} + header] + [(if content-avoid-keyboard? rn/keyboard-avoiding-view rn/view) + {:style + (if content-avoid-keyboard? + (style/content-keyboard-avoiding-view + {:top (or @header-height initial-header-height) + :bottom (if keyboard-shown? + @footer-container-height + (+ bottom-safe-area @footer-container-height))}) + style/scroll-view-container)} + [gesture/scroll-view + {:ref set-scroll-ref + :on-scroll set-content-y-scroll + :scroll-event-throttle 64 + :content-container-style {:flex-grow 1} + :always-bounce-vertical @keyboard-did-show? + :automatically-adjust-keyboard-insets automatically-adjust-keyboard-insets + :shows-vertical-scroll-indicator false + :keyboard-should-persist-taps keyboard-should-persist-taps} + (into [rn/view + {:style content-container-style + :on-layout set-content-container-height}] + children)]] + [rn/keyboard-avoiding-view + {:style style/keyboard-avoiding-view + :keyboard-vertical-offset (if platform/ios? footer-container-padding 0) + :pointer-events :box-none} + [floating-container/view + {:on-layout set-footer-container-height + :keyboard-shown? keyboard-shown? + :blur-options blur-options + :shell-overlay? shell-overlay? + :blur? show-background?} + footer]]]]) + (finally + (remove-listeners)))) diff --git a/src/status_im/common/font/effects.cljs b/src/status_im/common/font/effects.cljs new file mode 100644 index 00000000000..7b89c2341d0 --- /dev/null +++ b/src/status_im/common/font/effects.cljs @@ -0,0 +1,14 @@ +(ns status-im.common.font.effects + (:require + [react-native.platform :as platform] + [status-im.constants :as constants] + utils.image-server + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.font/get-font-file-for-initials-avatar + (fn [callback] + (utils.image-server/get-font-file-ready + (if platform/ios? + (:ios constants/initials-avatar-font-conf) + (:android constants/initials-avatar-font-conf)) + callback))) diff --git a/src/status_im/common/font/events.cljs b/src/status_im/common/font/events.cljs new file mode 100644 index 00000000000..e54d882b8ef --- /dev/null +++ b/src/status_im/common/font/events.cljs @@ -0,0 +1,11 @@ +(ns status-im.common.font.events + (:require + [clojure.string :as string] + status-im.common.font.effects + [utils.re-frame :as rf])) + +(rf/defn init-abs-root-path + {:events [:font/init-font-file-for-initials-avatar]} + [{:keys [db]} initials-avatar-font-file] + (when-not (string/blank? initials-avatar-font-file) + {:db (assoc db :initials-avatar-font-file initials-avatar-font-file)})) diff --git a/src/status_im/common/home/actions/view.cljs b/src/status_im/common/home/actions/view.cljs new file mode 100644 index 00000000000..397cb20623b --- /dev/null +++ b/src/status_im/common/home/actions/view.cljs @@ -0,0 +1,481 @@ +(ns status-im.common.home.actions.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [status-im.common.confirmation-drawer.view :as confirmation-drawer] + [status-im.common.mute-drawer.view :as mute-drawer] + [status-im.common.muting.helpers :refer [format-mute-till]] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.chat.actions.view :as chat-actions] + [status-im.contexts.chat.contacts.drawers.nickname-drawer.view :as nickname-drawer] + [status-im.contexts.communities.actions.chat.view :as communities-chat-actions] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- entry + [{:keys [icon label on-press danger? sub-label chevron? add-divider? accessibility-label]}] + {:pre [(keyword? icon) + (string? label) + (fn? on-press) + (boolean? danger?) + (boolean? chevron?)]} + {:icon icon + :label label + :on-press on-press + :danger? danger? + :sub-label sub-label + :right-icon (when chevron? :i/chevron-right) + :add-divider? add-divider? + :accessibility-label accessibility-label}) + +(defn hide-sheet-and-dispatch + [event] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch event)) + +(defn show-profile-action + [chat-id] + (hide-sheet-and-dispatch [:chat.ui/show-profile chat-id]) + (rf/dispatch [:pin-message/load-pin-messages chat-id])) + +(defn mark-all-read-action + [chat-id] + (hide-sheet-and-dispatch [:chat/mark-all-as-read chat-id])) + +(defn edit-nickname-action + [contact] + (hide-sheet-and-dispatch + [:show-bottom-sheet + {:content (fn [] + [nickname-drawer/nickname-drawer + {:title (i18n/label :t/add-nickname-title) + :description (i18n/label :t/nickname-visible-to-you) + :contact contact + :accessibility-label :edit-nickname}])}])) + +(defn mute-chat-action + [chat-id chat-type muted?] + (hide-sheet-and-dispatch [:show-bottom-sheet + {:content (fn [] + [mute-drawer/mute-drawer + {:id chat-id + :community? false + :muted? (not muted?) + :chat-type chat-type + :accessibility-label :mute-community-title}])}])) + +(defn unmute-chat-action + [chat-id] + (hide-sheet-and-dispatch [:chat.ui/mute chat-id false])) + +(defn clear-history-action + [{:keys [chat-id] :as item}] + (hide-sheet-and-dispatch + [:show-bottom-sheet + {:content (fn [] + [confirmation-drawer/confirmation-drawer + {:title (i18n/label :t/clear-history?) + :description (i18n/label :t/clear-history-confirmation-content) + :context item + :accessibility-label :clear-history-confirm + :button-text (i18n/label :t/clear-history) + :on-press #(hide-sheet-and-dispatch [:chat.ui/clear-history chat-id])}])}])) + +(defn close-chat-action + [{:keys [chat-id] :as item} inside-chat?] + (hide-sheet-and-dispatch + [:show-bottom-sheet + {:content (fn [] + [confirmation-drawer/confirmation-drawer + {:title (i18n/label :t/close-chat) + :description (i18n/label :t/close-chat-confirmation) + :context item + :accessibility-label :close-chat-confirm + :button-text (i18n/label :t/confirm) + :close-button-text (i18n/label :t/cancel) + :on-press (fn [] + (hide-sheet-and-dispatch [:chat.ui/close-and-remove-chat + chat-id]) + (when inside-chat? + (rf/dispatch [:navigate-back])))}])}])) + +(defn leave-group-action + [item chat-id] + (hide-sheet-and-dispatch + [:show-bottom-sheet + {:content (fn [] + [confirmation-drawer/confirmation-drawer + {:title (i18n/label :t/leave-group?) + :description (i18n/label :t/leave-chat-confirmation) + :context item + :accessibility-label :leave-group + :button-text (i18n/label :t/leave-group) + :on-press (fn [] + (hide-sheet-and-dispatch [:group-chats.ui/leave-chat-confirmed + chat-id]) + (rf/dispatch [:navigate-back]))}])}])) + +(defn block-user-action + [{:keys [public-key] :as item}] + (hide-sheet-and-dispatch + [:show-bottom-sheet + {:content (fn [] + [confirmation-drawer/confirmation-drawer + {:title (i18n/label :t/block-user?) + :description (i18n/label :t/block-contact-details) + :context item + :accessibility-label :block-user + :button-text (i18n/label :t/block-user) + :on-press #(hide-sheet-and-dispatch [:contact/block-contact + public-key])}])}])) + +(defn mute-chat-entry + [chat-id chat-type muted-till] + (let [muted? (rf/sub [:chats/muted chat-id])] + (entry {:icon (if muted? :i/activity-center :i/muted) + :label (i18n/label + (if muted? + :unmute-chat + :mute-chat)) + :sub-label (when (and muted? (some? muted-till)) + (i18n/label :t/muted-until {:duration (format-mute-till muted-till)})) + :on-press (if muted? + #(unmute-chat-action chat-id) + #(mute-chat-action chat-id chat-type muted?)) + :danger? false + :accessibility-label :mute-chat + :chevron? (not muted?)}))) + +(defn mark-as-read-entry + [chat-id needs-divider?] + (entry {:icon :i/mark-as-read + :label (i18n/label :t/mark-as-read) + :on-press #(mark-all-read-action chat-id) + :danger? false + :accessibility-label :mark-as-read + :sub-label nil + :chevron? false + :add-divider? needs-divider?})) + +(defn clear-history-entry + [chat needs-divider?] + (entry {:icon :i/delete + :label (i18n/label :t/clear-history) + :on-press #(clear-history-action chat) + :danger? true + :sub-label nil + :accessibility-label :clear-history + :chevron? false + :add-divider? needs-divider?})) + +(defn close-chat-entry + [item inside-chat? needs-divider?] + (entry {:icon :i/close-circle + :label (i18n/label :t/close-chat) + :on-press #(close-chat-action item inside-chat?) + :danger? true + :accessibility-label :close-chat + :sub-label nil + :chevron? false + :add-divider? needs-divider?})) + +(defn leave-group-entry + [item extra-data] + (entry + {:icon :i/log-out + :label (i18n/label :t/leave-group) + :on-press #(leave-group-action item (if extra-data (:chat-id extra-data) (:chat-id item))) + :danger? true + :accessibility-label :leave-group + :sub-label nil + :chevron? false + :add-divider? extra-data})) + +(defn view-profile-entry + [chat-id] + (entry {:icon :i/profile + :label (i18n/label :t/view-profile) + :on-press #(show-profile-action chat-id) + :danger? false + :accessibility-label :view-profile + :sub-label nil + :chevron? false})) + +(defn edit-nickname-entry + [chat-id] + (let [{:keys [nickname public-key secondary-name] + :as contact} (select-keys (rf/sub [:contacts/contact-by-address chat-id]) + [:primary-name :nickname :public-key :secondary-name]) + no-nickname? (string/blank? nickname)] + (entry + {:icon (if no-nickname? + :i/edit + :i/delete) + :label (i18n/label (if no-nickname? + :t/add-nickname-title + :t/remove-nickname)) + :on-press (fn [] + (if no-nickname? + (edit-nickname-action contact) + (do + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:toasts/upsert + {:id :remove-nickname + :type :positive + :text (i18n/label + :t/remove-nickname-toast + {:secondary-name secondary-name})}]) + (rf/dispatch [:contacts/update-nickname public-key ""])))) + :danger? false + :accessibility-label :add-nickname + :sub-label nil + :chevron? false}))) + +(defn edit-name-image-entry + [chat-id] + (entry {:icon :i/edit + :label (i18n/label :t/edit-name-and-image) + :on-press #(rf/dispatch [:open-modal :screen/group-update chat-id]) + :danger? false + :accessibility-label :edit-name-and-image + :sub-label nil + :chevron? false})) + +;; TODO(OmarBasem): Requires design input. +(defn notifications-entry + [add-divider?] + (entry {:icon :i/notifications + :label (i18n/label :t/notifications) + :on-press #(js/alert "TODO: to be implemented, requires design input") + :danger? false + :sub-label (i18n/label :t/all-messages) + :accessibility-label :manage-notifications + :chevron? true + :add-divider? add-divider?})) + + +(defn remove-from-contacts-entry + [contact] + (entry {:icon :i/remove-user + :label (i18n/label :t/remove-from-contacts) + :on-press #(hide-sheet-and-dispatch [:contact.ui/remove-contact-pressed contact]) + :danger? false + :accessibility-label :remove-from-contacts + :sub-label nil + :chevron? false})) + +(defn show-qr-entry + [public-key] + (entry {:icon :i/qr-code + :label (i18n/label :t/show-qr) + :on-press #(rf/dispatch [:contacts/show-qr-code public-key]) + :danger? false + :accessibility-label :show-qr-code + :sub-label nil + :chevron? false})) + +(defn share-profile-entry + [public-key] + (entry {:icon :i/share + :label (i18n/label :t/share-profile) + :on-press (fn [] + (rf/dispatch [:universal-links/generate-profile-url + {:public-key public-key + :on-success #(rf/dispatch [:open-share + {:options {:message %}}])}])) + :danger? false + :accessibility-label :share-profile + :sub-label nil + :chevron? false})) + +;; TODO(OmarBasem): to be implemented.chat/check-channel-muted? +(defn share-group-entry + [] + (entry {:icon :i/share + :label (i18n/label :t/share) + :on-press #(js/alert "TODO: to be implemented") + :danger? false + :accessibility-label :share-group + :sub-label nil + :chevron? false})) + +;; TODO(OmarBasem): Requires status-go impl. +(defn mark-untrustworthy-entry + [] + (entry {:icon :i/alert + :label (i18n/label :t/mark-untrustworthy) + :on-press #(js/alert "TODO: to be implemented, requires status-go impl.") + :danger? true + :accessibility-label :mark-untrustworthy + :sub-label nil + :chevron? false + :add-divider? true})) + +(defn block-user-entry + [item] + (entry {:icon :i/block + :label (i18n/label :t/block-user) + :on-press #(block-user-action item) + :danger? true + :accessibility-label :block-user + :sub-label nil + :chevron? false})) + +(defn remove-from-group-entry + [{:keys [public-key]} chat-id] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity public-key])] + (entry {:icon :i/placeholder + :label (i18n/label :t/remove-user-from-group {:username primary-name}) + :on-press #(hide-sheet-and-dispatch [:group-chats.ui/remove-member-pressed chat-id + public-key true]) + :danger? true + :accessibility-label :remove-from-group + :sub-label nil + :chevron? false + :add-divider? true}))) + +(defn group-details-entry + [chat-id] + (entry {:icon :i/members + :label (i18n/label :t/group-details) + :on-press (fn [] + (rf/dispatch [:chats-list/load-chat chat-id]) + (rf/dispatch [:pin-message/load-pin-messages chat-id]) + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:navigate-to :screen/group-details chat-id])) + :danger? false + :accessibility-label :group-details + :sub-label nil + :chevron? false})) + +(defn add-manage-members-entry + [chat-id admin?] + (entry {:icon :i/add-user + :label (i18n/label (if admin? :t/manage-members :t/add-members)) + :on-press #(rf/dispatch [:open-modal :screen/group-add-manage-members chat-id]) + :danger? false + :accessibility-label :manage-members + :sub-label nil + :chevron? false})) + +(defn edit-group-entry + [chat-id] + (entry {:icon :i/edit + :label (i18n/label :t/edit-name-and-image) + :on-press #(rf/dispatch [:open-modal :screen/group-update chat-id]) + :danger? false + :accessibility-label :edit-group + :sub-label nil + :chevron? false})) + +;; TODO(OmarBasem): to be implemented. +(defn group-privacy-entry + [] + (entry {:icon :i/privacy + :label (i18n/label :t/change-group-privacy) + :on-press #(js/alert "TODO: to be implemented") + :danger? false + :accessibility-label :group-privacy + :sub-label nil + :chevron? false})) + +(defn destructive-actions + [{:keys [group-chat chat-type] :as item} inside-chat?] + [(when (not group-chat) + (close-chat-entry item inside-chat? (not= chat-type constants/public-chat-type))) + (when-not (= chat-type constants/public-chat-type) + (clear-history-entry item group-chat)) + (when group-chat + (leave-group-entry item nil))]) + +(defn notification-actions + [{:keys [chat-id public? chat-type muted-till]} inside-chat? needs-divider?] + [(mark-as-read-entry chat-id needs-divider?) + (mute-chat-entry chat-id chat-type muted-till) + (when config/show-not-implemented-features? + (notifications-entry false)) + (when (and config/fetch-messages-enabled? inside-chat?) + (chat-actions/fetch-messages chat-id)) + (when public? + (when config/show-not-implemented-features? + (share-group-entry)))]) + +(defn group-actions + [{:keys [chat-id admins]} inside-chat?] + (let [current-pub-key (rf/sub [:multiaccount/public-key]) + admin? (get admins current-pub-key)] + [(group-details-entry chat-id) + (when inside-chat? + (add-manage-members-entry chat-id admin?)) + (when (and admin? inside-chat?) + (edit-group-entry chat-id)) + (when (and admin? inside-chat?) + (when config/show-not-implemented-features? + (group-privacy-entry)))])) + +(defn one-to-one-actions + [{:keys [chat-id] :as item} inside-chat?] + [quo/action-drawer + [[(view-profile-entry chat-id)] + (notification-actions item inside-chat? true) + (destructive-actions item inside-chat?)]]) + +(defn public-chat-actions + [item inside-chat?] + [quo/action-drawer + [(destructive-actions item inside-chat?)]]) + +(defn private-group-chat-actions + [item inside-chat?] + [quo/action-drawer + (let [show-group-actions? (:group-chat item)] + [(when show-group-actions? + (group-actions item inside-chat?)) + (notification-actions item inside-chat? show-group-actions?) + (destructive-actions item inside-chat?)])]) + +(defn contact-actions + [{:keys [public-key added?] :as contact} {:keys [chat-id admin?] :as extra-data}] + (let [current-pub-key (rf/sub [:multiaccount/public-key])] + [quo/action-drawer + [[(view-profile-entry public-key) + (when-not (= current-pub-key public-key) + (edit-nickname-entry public-key)) + (show-qr-entry public-key) + (share-profile-entry public-key)] + [(when-not (= current-pub-key public-key) + (when config/show-not-implemented-features? + (mark-untrustworthy-entry))) + (when added? (remove-from-contacts-entry contact)) + (when-not (= current-pub-key public-key) (block-user-entry contact))] + (when (and admin? chat-id) + [(if (= current-pub-key public-key) + (leave-group-entry contact extra-data) + (remove-from-group-entry contact chat-id))])]])) + +(defn chat-actions + ([{:keys [chat-type] :as chat} inside-chat? hide-show-members?] + (condp = chat-type + constants/one-to-one-chat-type + [one-to-one-actions chat inside-chat?] + constants/private-group-chat-type + [private-group-chat-actions chat inside-chat?] + constants/public-chat-type + [public-chat-actions chat inside-chat?] + constants/community-chat-type + [communities-chat-actions/actions chat inside-chat? hide-show-members?] + nil)) + ([chat inside-chat?] + (chat-actions chat inside-chat? nil))) + +(defn group-details-actions + [{:keys [chat-id admins] :as group}] + (let [current-pub-key (rf/sub [:multiaccount/public-key]) + admin? (get admins current-pub-key)] + [quo/action-drawer + [(when admin? + [(edit-name-image-entry chat-id)]) + [(when config/show-not-implemented-features? + (notifications-entry admin?))] + (destructive-actions group false)]])) diff --git a/src/status_im/common/home/banner/style.cljs b/src/status_im/common/home/banner/style.cljs new file mode 100644 index 00000000000..978cd8876dd --- /dev/null +++ b/src/status_im/common/home/banner/style.cljs @@ -0,0 +1,73 @@ +(ns status-im.common.home.banner.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.common.home.constants :as constants])) + +(def ^:private card-height (+ 56 16)) +(def ^:private max-scroll (+ card-height 8)) + +(def fill-space + {:position :absolute + :top 0 + :right 0 + :left 0 + :bottom 0}) + +(defn- animated-card-translation-y + [scroll-shared-value] + (reanimated/interpolate scroll-shared-value [0 max-scroll] [0 (- max-scroll)] :clamp)) + +(defn- animated-card-translation-y-reverse + [scroll-shared-value] + (reanimated/interpolate scroll-shared-value [0 max-scroll] [0 (+ max-scroll)] :clamp)) + +(defn banner-card-blur-layer + [scroll-shared-value theme] + (reanimated/apply-animations-to-style + {:transform [{:translate-y (animated-card-translation-y scroll-shared-value)}]} + {:overflow (if platform/ios? :visible :hidden) + :z-index 1 + :position :absolute + :top 0 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :right 0 + :left 0 + :height (+ safe-area/top (dec constants/header-height))})) + +(defn banner-card-hiding-layer + [scroll-shared-value theme] + (reanimated/apply-animations-to-style + {:transform [{:translate-y (animated-card-translation-y-reverse scroll-shared-value)}]} + {:z-index 2 + :position :absolute + :top 0 + :right 0 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :left 0 + :padding-top safe-area/top})) + +(defn animated-banner-card + [scroll-shared-value] + (reanimated/apply-animations-to-style + {:opacity (reanimated/interpolate scroll-shared-value [0 card-height] [1 0] :clamp) + :transform [{:translate-y (animated-card-translation-y scroll-shared-value)}]} + {})) + +(defn banner-card-tabs-layer + [scroll-shared-value theme] + (reanimated/apply-animations-to-style + {:transform [{:translate-y (animated-card-translation-y scroll-shared-value)}]} + {:z-index 3 + :position :absolute + :top (+ safe-area/top 184) + :right 0 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :left 0})) + +(def banner-card-tabs + {:padding-horizontal 20 + :padding-top 12 + :padding-bottom 16}) diff --git a/src/status_im/common/home/banner/view.cljs b/src/status_im/common/home/banner/view.cljs new file mode 100644 index 00000000000..970c0c50f36 --- /dev/null +++ b/src/status_im/common/home/banner/view.cljs @@ -0,0 +1,93 @@ +(ns status-im.common.home.banner.view + (:require + [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [reagent.core :as reagent] + [status-im.common.home.banner.style :as style] + [status-im.common.home.title-column.view :as title-column] + [status-im.common.home.top-nav.view :as top-nav] + [utils.re-frame :as rf])) + +(def card-banner-overflow-threshold 3) +(def card-banner-overflow (reagent/atom :visible)) + +(defn- reset-banner-animation + [scroll-shared-value] + (reanimated/animate-shared-value-with-timing scroll-shared-value 0 200 :easing3)) + +(defn- reset-scroll + [scroll-ref] + (cond + (.-scrollToLocation scroll-ref) + (oops/ocall! scroll-ref "scrollToLocation" #js {:itemIndex 0 :sectionIndex 0 :viewOffset 0}) + (.-scrollToOffset scroll-ref) + (oops/ocall! scroll-ref "scrollToOffset" #js {:offset 0}))) + +(defn- banner-card-blur-layer + [scroll-shared-value child] + (let [open-sheet? (-> (rf/sub [:bottom-sheet]) :sheets seq) + theme (quo.context/use-theme)] + [reanimated/view {:style (style/banner-card-blur-layer scroll-shared-value theme)} + [quo/blur + {:style style/fill-space + :blur-amount (if platform/ios? 20 10) + :blur-type (if (= theme :light) (if platform/ios? :light :xlight) :dark) + :overlay-color (if open-sheet? + (colors/theme-colors colors/white colors/neutral-95-opa-70 theme) + (if (= theme :light) nil colors/neutral-95-opa-70))} + child]])) + +(defn- banner-card-hiding-layer + [{:keys [title-props card-props scroll-shared-value theme]}] + (let [customization-color (rf/sub [:profile/customization-color])] + [reanimated/view {:style (style/banner-card-hiding-layer scroll-shared-value theme)} + [top-nav/view] + [title-column/view (assoc title-props :customization-color customization-color)] + [rn/view {:style {:overflow @card-banner-overflow}} + [reanimated/view {:style (style/animated-banner-card scroll-shared-value)} + [quo/discover-card card-props]]]])) + +(defn- banner-card-tabs-layer + [{:keys [selected-tab tabs on-tab-change scroll-ref scroll-shared-value customization-color theme]}] + [reanimated/view {:style (style/banner-card-tabs-layer scroll-shared-value theme)} + ^{:key (str "tabs-" selected-tab)} + [quo/tabs + {:style style/banner-card-tabs + :customization-color customization-color + :size 32 + :default-active selected-tab + :data tabs + :on-change (fn [tab] + (reset-banner-animation scroll-shared-value) + (some-> scroll-ref + deref + reset-scroll) + (on-tab-change tab))}]]) + +(defn animated-banner + [{:keys [scroll-ref tabs selected-tab on-tab-change scroll-shared-value content customization-color]}] + (let [theme (quo.context/use-theme)] + [:<> + [banner-card-blur-layer scroll-shared-value + [banner-card-hiding-layer + (assoc content :scroll-shared-value scroll-shared-value :theme theme)]] + [banner-card-tabs-layer + {:scroll-shared-value scroll-shared-value + :selected-tab selected-tab + :tabs tabs + :on-tab-change on-tab-change + :scroll-ref scroll-ref + :customization-color customization-color + :theme theme}]])) + +(defn set-scroll-shared-value + [{:keys [shared-value scroll-input]}] + (reanimated/set-shared-value shared-value scroll-input) + (let [new-overflow (if (<= scroll-input card-banner-overflow-threshold) :visible :hidden)] + (when-not (= new-overflow @card-banner-overflow) + (reset! card-banner-overflow new-overflow)))) diff --git a/src/status_im/common/home/constants.cljs b/src/status_im/common/home/constants.cljs new file mode 100644 index 00000000000..c0a66c00120 --- /dev/null +++ b/src/status_im/common/home/constants.cljs @@ -0,0 +1,3 @@ +(ns status-im.common.home.constants) + +(def header-height 245) diff --git a/src/status_im/common/home/empty_state/style.cljs b/src/status_im/common/home/empty_state/style.cljs new file mode 100644 index 00000000000..dd2eb707e2e --- /dev/null +++ b/src/status_im/common/home/empty_state/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.common.home.empty-state.style + (:require + [react-native.safe-area :as safe-area] + [status-im.common.home.constants :as constants])) + +(defn empty-state-container + [] + {:flex 1 + :margin-top (+ constants/header-height safe-area/top) + :margin-bottom 44 + :justify-content :center}) diff --git a/src/status_im/common/home/empty_state/view.cljs b/src/status_im/common/home/empty_state/view.cljs new file mode 100644 index 00000000000..24de84275a6 --- /dev/null +++ b/src/status_im/common/home/empty_state/view.cljs @@ -0,0 +1,17 @@ +(ns status-im.common.home.empty-state.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.home.empty-state.style :as style] + [utils.re-frame :as rf])) + +(defn view + [{:keys [selected-tab tab->content]}] + (let [{:keys [image title description]} (tab->content selected-tab) + customization-color (rf/sub [:profile/customization-color])] + [rn/view {:style (style/empty-state-container)} + [quo/empty-state + {:customization-color customization-color + :image image + :title title + :description description}]])) diff --git a/src/status_im/common/home/header_spacing/style.cljs b/src/status_im/common/home/header_spacing/style.cljs new file mode 100644 index 00000000000..0bbe00be825 --- /dev/null +++ b/src/status_im/common/home/header_spacing/style.cljs @@ -0,0 +1,8 @@ +(ns status-im.common.home.header-spacing.style + (:require + [react-native.safe-area :as safe-area] + [status-im.common.home.constants :as constants])) + +(defn header-spacing + [] + {:height (+ constants/header-height safe-area/top)}) diff --git a/src/status_im/common/home/header_spacing/view.cljs b/src/status_im/common/home/header_spacing/view.cljs new file mode 100644 index 00000000000..efb95512850 --- /dev/null +++ b/src/status_im/common/home/header_spacing/view.cljs @@ -0,0 +1,8 @@ +(ns status-im.common.home.header-spacing.view + (:require + [react-native.core :as rn] + [status-im.common.home.header-spacing.style :as style])) + +(defn view + [] + [rn/view {:style (style/header-spacing)}]) diff --git a/src/status_im/common/home/title_column/style.cljs b/src/status_im/common/home/title_column/style.cljs new file mode 100644 index 00000000000..37a539cacfe --- /dev/null +++ b/src/status_im/common/home/title_column/style.cljs @@ -0,0 +1,22 @@ +(ns status-im.common.home.title-column.style) + +(def title-column + {:flex-direction :row + :align-items :center + :height 56 + :padding-vertical 12 + :padding-horizontal 20 + :background-color :transparent}) + +(def title + {:flex 1 + :align-items :center + :flex-direction :row}) + +(def beta-label {:padding-top 6 :padding-bottom 2}) + +(def title-column-text + {:accessibility-label :communities-screen-title + :margin-right 6 + :weight :semi-bold + :size :heading-1}) diff --git a/src/status_im/common/home/title_column/view.cljs b/src/status_im/common/home/title_column/view.cljs new file mode 100644 index 00000000000..a36b076e3ae --- /dev/null +++ b/src/status_im/common/home/title_column/view.cljs @@ -0,0 +1,28 @@ +(ns status-im.common.home.title-column.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.home.title-column.style :as style] + [status-im.common.plus-button.view :as plus-button] + [utils.i18n :as i18n])) + +(defn view + [{:keys [beta? label handler accessibility-label customization-color]}] + [rn/view style/title-column + [rn/view {:style style/title} + [quo/text style/title-column-text + label] + (when beta? + [rn/view {:style style/beta-label} + [quo/tag + {:accessibility-label :communities-chat-beta-tag + :size 24 + :type :label + :label (i18n/label :t/beta) + :labelled? true + :blurred? false}]])] + (when handler + [plus-button/plus-button + {:on-press handler + :accessibility-label accessibility-label + :customization-color customization-color}])]) diff --git a/src/status_im/common/home/top_nav/style.cljs b/src/status_im/common/home/top_nav/style.cljs new file mode 100644 index 00000000000..7c9994688ed --- /dev/null +++ b/src/status_im/common/home/top_nav/style.cljs @@ -0,0 +1,4 @@ +(ns status-im.common.home.top-nav.style) + +(def top-nav-container + {:padding-horizontal 20}) diff --git a/src/status_im/common/home/top_nav/view.cljs b/src/status_im/common/home/top_nav/view.cljs new file mode 100644 index 00000000000..6fe08958072 --- /dev/null +++ b/src/status_im/common/home/top_nav/view.cljs @@ -0,0 +1,67 @@ +(ns status-im.common.home.top-nav.view + (:require + [cljs-time.core :as t] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.home.top-nav.style :as style] + [status-im.constants :as constants] + [status-im.contexts.profile.utils :as profile.utils] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- network-status-label + [now wallet-latest-update] + (when (t/after? now (t/plus wallet-latest-update (t/minutes 5))) + (let [units [{:name :t/datetime-second-short :limit 60 :in-second 1} + {:name :t/datetime-minute-mid :limit 3600 :in-second 60} + {:name :t/datetime-hour-short-lowercase :limit 86400 :in-second 3600} + {:name :t/datetime-day :limit nil :in-second 86400}] + time-ago (datetime/time-ago-long wallet-latest-update units)] + [rn/view {:style {:justify-content :center}} + [quo/network-status-tag {:label (i18n/label :t/wallet-updated-at {:at time-ago})}]]))) + +(defn view + "[top-nav props] + props + {:blur? true/false + :container-style passed to outer view of component}" + [{:keys [container-style blur?]}] + (let [{:keys [public-key] :as profile} (rf/sub [:profile/profile-with-image]) + online? (rf/sub [:visibility-status-updates/online? + public-key]) + customization-color (rf/sub [:profile/customization-color]) + wallet-latest-update (rf/sub [:wallet/latest-update]) + wallet-blockchain-status (rf/sub [:wallet/blockchain-status]) + avatar {:online? online? + :full-name (profile.utils/displayed-name profile) + :profile-picture (profile.utils/photo profile)} + unread-count (rf/sub [:activity-center/unread-count]) + indicator (rf/sub [:activity-center/unread-indicator]) + notification-type (case indicator + ; should be `seen` TODO discuss with design team + ; notifications for activity centre + :unread-indicator/seen :mention-seen + ; should be :notification TODO + ; https://github.com/status-im/status-mobile/issues/17102 + :unread-indicator/new :mention + nil) + screen-id (quo.context/use-screen-id) + initial-share-tab (if (= :screen/wallet-stack screen-id) :wallet :profile)] + [quo/top-nav + {:avatar-on-press #(rf/dispatch [:open-modal :screen/settings]) + :scan-on-press #(rf/dispatch [:open-modal :screen/shell-qr-reader]) + :activity-center-on-press #(rf/dispatch [:activity-center/open]) + :right-section-content (when (and (= (:status wallet-blockchain-status) "down") + wallet-latest-update) + [network-status-label (t/now) wallet-latest-update]) + :qr-code-on-press #(rf/dispatch [:open-modal :screen/share-shell + {:initial-tab initial-share-tab}]) + :container-style (merge style/top-nav-container container-style) + :blur? blur? + :customization-color customization-color + :avatar-props avatar + :max-unread-notifications constants/activity-center-max-unread-count + :notification-count unread-count + :notification notification-type}])) diff --git a/src/status_im/common/image_crop_picker/events.cljs b/src/status_im/common/image_crop_picker/events.cljs new file mode 100644 index 00000000000..cdad5b31e30 --- /dev/null +++ b/src/status_im/common/image_crop_picker/events.cljs @@ -0,0 +1,32 @@ +(ns status-im.common.image-crop-picker.events + (:require [react-native.image-crop-picker :as image-crop-picker] + [react-native.platform :as platform] + [utils.re-frame :as rf])) + +(rf/reg-fx :effect.image-crop-picker/show + (fn [[callback crop-opts]] + (when platform/ios? + (rf/dispatch [:alert-banners/hide])) + (image-crop-picker/show-image-picker + callback + crop-opts + #(rf/dispatch [:alert-banners/unhide])))) + +(rf/reg-fx :effect.image-crop-picker/show-camera + (fn [[callback crop-opts]] + (when platform/ios? + (rf/dispatch [:alert-banners/hide])) + (image-crop-picker/show-image-picker-camera + callback + crop-opts + #(rf/dispatch [:alert-banners/unhide])))) + +(rf/reg-event-fx + :image-crop-picker/show + (fn [_ [callback crop-opts]] + {:effect.image-crop-picker/show [callback crop-opts]})) + +(rf/reg-event-fx + :image-crop-picker/show-camera + (fn [_ [callback crop-opts]] + {:effect.image-crop-picker/show-camera [callback crop-opts]})) diff --git a/src/status_im/common/json_rpc/events.cljs b/src/status_im/common/json_rpc/events.cljs new file mode 100644 index 00000000000..255258f644e --- /dev/null +++ b/src/status_im/common/json_rpc/events.cljs @@ -0,0 +1,116 @@ +(ns status-im.common.json-rpc.events + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [promesa.core :as promesa] + [re-frame.core :as re-frame] + [react-native.background-timer :as background-timer] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defn- on-error-retry + [call-method + {method :method + number-of-retries :number-of-retries + delay-ms :delay + on-error :on-error + :as arg}] + (if (pos? number-of-retries) + (fn [error] + (let [updated-delay (if delay-ms + (min 2000 (* 2 delay-ms)) + 50)] + (log/debug "[on-error-retry]" method + "number-of-retries" number-of-retries + "delay" delay-ms + "error" error) + (background-timer/set-timeout #(call-method (-> arg + (update :number-of-retries dec) + (assoc :delay updated-delay))) + updated-delay))) + on-error)) + +(defn call + "Call private RPC endpoint. + + method: string - The name of an endpoint function in status-go, with the first + character lowercased and prefixed by wakuext_. For example, the BackupData + function should be represented as the string wakuext_backupData. + + params: sequence - A positional sequence of zero or more arguments. + + on-success/on-error: function/vector (optional) - When a function, it will be + called with the transformed response and id (request-id) as the argument. + When a vector, it is expected to be a valid re-frame event vector, and the event + will be dispatched with the transformed response and id conj'ed at the end. + + js-response: boolean - When non-nil, the successful response will not be + recursively converted to Clojure data structures. Default: nil. + + id: (optional) - id passed while making the RPC call will be returned in response + + number-of-retries: integer - The maximum number of retries in case of failure. + Default: nil. + + delay: integer - The number of milliseconds to wait between retries. Default: + nil. + + Note that on-error is optional, but if not provided, a default implementation + will be used. + " + [{:keys [method params on-success on-error js-response id] :as arg}] + (let [params (or params []) + on-error (or on-error + (on-error-retry call arg) + #(log/warn :json-rpc/error method :error % :params params))] + (native-module/call-private-rpc + (transforms/clj->json {:jsonrpc "2.0" + :id (or id 1) + :method method + :params params}) + (fn [raw-response] + (if (string/blank? raw-response) + (let [error {:message "Blank response"}] + (if (vector? on-error) + (rf/dispatch (conj on-error error)) + (on-error error))) + (let [^js response-js (transforms/json->js raw-response)] + (if-let [error (.-error response-js)] + (let [error (transforms/js->clj error)] + (if (vector? on-error) + (rf/dispatch (conj on-error error)) + (on-error error))) + (when on-success + (let [result (if js-response + (.-result response-js) + (transforms/js->clj (.-result response-js))) + request-id (.-id response-js)] + (if (vector? on-success) + (rf/dispatch (conj on-success result request-id)) + (on-success result request-id))))))))))) + +(defn call-async + "Helper to handle RPC calls to status-go as promises" + [method js-response? & args] + (promesa/create + (fn [p-resolve p-reject] + (call {:method method + :params args + :on-success p-resolve + :on-error p-reject + :js-response js-response?})))) + +(re-frame/reg-fx + :json-rpc/call + (fn [params] + (doseq [param params] + (call param)))) + +(defn log-rpc-error + [_ [{:keys [event params]} error]] + (log/error (str "Failed to " event) + {:params params + :error error})) + +(rf/reg-event-fx :log-rpc-error log-rpc-error) diff --git a/src/status_im/common/keychain/events.cljs b/src/status_im/common/keychain/events.cljs new file mode 100644 index 00000000000..6101722e90f --- /dev/null +++ b/src/status_im/common/keychain/events.cljs @@ -0,0 +1,221 @@ +(ns status-im.common.keychain.events + (:require + [native-module.core :as native-module] + [oops.core :as oops] + [re-frame.core :as re-frame] + [react-native.keychain :as keychain] + [react-native.platform :as platform] + [status-im.common.log :as log] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn- whisper-key-name + [address] + (str address "-whisper")) + +(defn- check-conditions + [callback & checks] + (if (= (count checks) 0) + (callback true) + (let [current-check-fn (first checks) + process-check-result (fn [callback-success callback-fail] + (fn [current-check-passed?] + (if current-check-passed? + (callback-success) + (callback-fail))))] + (current-check-fn (process-check-result + #(apply (partial check-conditions callback) (rest checks)) + #(callback false)))))) + +;; These helpers check if the device is okay to use for password storage +;; They resolve callback with `true` if the check is passed, with `false` otherwise. +;; Android only +(defn- device-not-rooted? + [callback] + (native-module/rooted-device? (fn [rooted?] (callback (not rooted?))))) + +(defn can-save-user-password? + [callback] + (log/log-debug "[keychain] can-save-user-password?") + (cond + platform/ios? + (check-conditions callback keychain/device-encrypted?) + + platform/android? + (check-conditions callback keychain/secure-hardware-available? device-not-rooted?) + + :else + (callback false))) + +(def auth-method-biometric "biometric") +(def auth-method-none "none") + +(defn auth-credentials + [key-uid] + (str key-uid "-auth")) + +(defn save-auth-method! + [key-uid method] + (-> (keychain/save-credentials + (auth-credentials key-uid) + key-uid + method) + (.catch (fn [err] + (log/log-error "Failed to save auth method in the keychain" + {:error err + :key-uid key-uid + :auth-method method}))))) + +(re-frame/reg-fx + :keychain/save-auth-method + (fn [[key-uid method]] + (when-not (empty? key-uid) ; key-uid may be nil after restore from local pairing + (save-auth-method! key-uid method)))) + +(defn get-auth-method! + [key-uid] + (-> (auth-credentials key-uid) + (keychain/get-credentials) + (.then (fn [value] + (if value + (oops/oget value "password") + auth-method-none))) + (.catch (constantly auth-method-none)))) + +(defn save-user-password! + [key-uid password] + (keychain/save-credentials key-uid key-uid (security/safe-unmask-data password))) + +(defn get-user-password! + [key-uid callback] + (keychain/get-credentials key-uid + (fn [value] + (some-> value + (oops/oget "password") + (security/mask-data) + (callback))))) + +(re-frame/reg-fx + :keychain/get-user-password + (fn [[key-uid callback]] + (get-user-password! key-uid callback))) + +(rf/defn get-user-password + [_ [key-uid callback]] + {:keychain/get-user-password [key-uid callback]}) + +(defn- password-migration-key-name + [key-uid] + (str key-uid "-password-migration")) + +(defn save-password-migration! + [key-uid] + (-> (keychain/save-credentials + (password-migration-key-name key-uid) + key-uid + ;; NOTE: using the key-uid as the password, but we don't really care about the + ;; value, we only care that it's there + key-uid) + (.catch (fn [error] + (log/log-error "Failed to get the keychain password migration flag" + {:error error + :key-uid key-uid}))))) + +(defn get-password-migration! + [key-uid callback] + (keychain/get-credentials + (password-migration-key-name key-uid) + (comp callback boolean))) + +(re-frame/reg-fx + :keychain/clear-user-password + (fn [key-uid] + (keychain/reset-credentials (password-migration-key-name key-uid)) + (keychain/reset-credentials (auth-credentials key-uid)) + (keychain/reset-credentials key-uid))) + +(re-frame/reg-fx + :keychain/clear-keycard-keys + (fn [key-uid] + (keychain/reset-credentials (auth-credentials key-uid)) + (keychain/reset-credentials (whisper-key-name key-uid)) + (keychain/reset-credentials key-uid))) + +(re-frame/reg-fx + :keychain/save-password-and-auth-method + (fn [{:keys [key-uid masked-password on-success on-error]}] + (-> (save-user-password! key-uid masked-password) + (.then #(save-auth-method! key-uid auth-method-biometric)) + (.then #(save-password-migration! key-uid)) + (.then #(when on-success (on-success))) + (.catch #(when on-error (on-error %)))))) + +(re-frame/reg-event-fx + :keychain/save-password-and-auth-method + (fn [_ [opts]] + {:keychain/save-password-and-auth-method opts})) + +;; NOTE: migrating the plaintext password in the keychain +;; with the hashed one. Added due to the sync onboarding +;; flow, where the password arrives already hashed. +(re-frame/reg-fx + :keychain/password-hash-migration + (fn [{:keys [key-uid callback] + :or {callback identity}}] + (keychain/get-credentials + (whisper-key-name key-uid) + (fn [whisper-key-data] + (if whisper-key-data + (callback) ;; we don't need to migrate keycard password + (-> (get-password-migration! key-uid identity) + (.then (fn [migrated?] + (if migrated? + (callback) + (-> (get-user-password! key-uid identity) + (.then security/hash-masked-password) + (.then #(save-user-password! key-uid %)) + (.then #(save-password-migration! key-uid)) + (.then callback))))) + (.catch (fn [err] + (log/log-error "Failed to migrate the keychain password" + {:error err + :key-uid key-uid + :event :keychain/password-hash-migration}))))))))) + +(re-frame/reg-fx + :effects.keychain/save-keycard-keys + (fn [keycard-keys] + (let [{:keys [key-uid encryption-public-key whisper-private-key]} + (security/safe-unmask-data keycard-keys)] + (keychain/save-credentials + key-uid + key-uid + encryption-public-key + #(when-not % (log/log-error "Error while saving encryption-public-key"))) + (keychain/save-credentials + (whisper-key-name key-uid) + key-uid + whisper-private-key + #(when-not % (log/log-error "Error while saving whisper-private-key")))))) + +(re-frame/reg-event-fx :keychain/save-keycard-keys-and-auth-method + (fn [_ [keycard-keys]] + (let [{:keys [key-uid]} (security/safe-unmask-data keycard-keys)] + {:effects.keychain/save-keycard-keys keycard-keys + :keychain/save-auth-method [key-uid auth-method-biometric]}))) + +(re-frame/reg-fx + :keychain/get-keycard-keys + (fn [[key-uid callback]] + (keychain/get-credentials + key-uid + (fn [encryption-key-data] + (if encryption-key-data + (keychain/get-credentials + (whisper-key-name key-uid) + (fn [whisper-key-data] + (if whisper-key-data + (callback [(oops/oget encryption-key-data "password") + (oops/oget whisper-key-data "password")]) + (callback nil)))) + (callback nil)))))) diff --git a/src/status_im/common/kv_storage/effects.cljs b/src/status_im/common/kv_storage/effects.cljs new file mode 100644 index 00000000000..2cded5e1423 --- /dev/null +++ b/src/status_im/common/kv_storage/effects.cljs @@ -0,0 +1,19 @@ +(ns status-im.common.kv-storage.effects + (:require + [react-native.mmkv :as mmkv] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.kv/set-object + (fn [{:keys [id value]}] + (mmkv/set-object id value))) + +(rf/reg-fx :effects.kv/merge-object + (fn [{key-id :key + :keys [value]}] + (let [kv-object (mmkv/get-object key-id {})] + (mmkv/set-object key-id + (merge kv-object value))))) + +(rf/reg-fx :effects.kv/delete-key + (fn [key-id] + (mmkv/delete-key key-id))) diff --git a/src/status_im/common/lightbox/animations.cljs b/src/status_im/common/lightbox/animations.cljs new file mode 100644 index 00000000000..837379c90dc --- /dev/null +++ b/src/status_im/common/lightbox/animations.cljs @@ -0,0 +1,22 @@ +(ns status-im.common.lightbox.animations + (:require + [react-native.reanimated :as reanimated])) + +;; TODO: Abstract Reanimated methods in a better way, issue: +;; https://github.com/status-im/status-mobile/issues/15176 +(def get-val reanimated/get-shared-value) + +(def set-val reanimated/set-shared-value) + +(def use-val reanimated/use-shared-value) + +(defn animate + ([animation value] + (animate animation value 300)) + ([animation value duration] + (set-val animation + (reanimated/with-timing value + (clj->js {:duration duration + :easing (reanimated/default-easing)}))))) + +(def animate-decay reanimated/animate-shared-value-with-decay) diff --git a/src/status_im/common/lightbox/bottom_view.cljs b/src/status_im/common/lightbox/bottom_view.cljs new file mode 100644 index 00000000000..f9675eec4a0 --- /dev/null +++ b/src/status_im/common/lightbox/bottom_view.cljs @@ -0,0 +1,83 @@ +(ns status-im.common.lightbox.bottom-view + (:require + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [status-im.common.lightbox.animations :as anim] + [status-im.common.lightbox.constants :as constants] + [status-im.common.lightbox.style :as style] + [status-im.common.lightbox.text-sheet.view :as text-sheet] + [utils.re-frame :as rf])) + +(defn get-small-item-layout + [_ index] + #js + {:length constants/small-image-size + :offset (* (+ constants/small-image-size 8) index) + :index index}) + +(defn- f-small-image + [item index _ {:keys [scroll-index props]}] + (let [size (if (= @scroll-index index) constants/focused-image-size constants/small-image-size) + size-value (anim/use-val size) + {:keys [scroll-index-lock? small-list-ref flat-list-ref]} + props] + (anim/animate size-value size) + [rn/touchable-opacity + {:active-opacity 1 + :on-press (fn [] + (rf/dispatch [:lightbox/zoom-out-signal @scroll-index]) + (reset! scroll-index-lock? true) + (js/setTimeout #(reset! scroll-index-lock? false) 500) + (js/setTimeout + (fn [] + (reset! scroll-index index) + (.scrollToIndex ^js @small-list-ref + #js {:animated true :index index}) + (.scrollToIndex ^js @flat-list-ref + #js {:animated true :index index})) + (if platform/ios? 50 150)) + (rf/dispatch [:lightbox/update-animation-shared-element-id (:id item)]))} + [reanimated/fast-image + {:source {:uri (:image item)} + :style (reanimated/apply-animations-to-style {:width size-value + :height size-value} + {:border-radius 10})}]])) + +(defn small-image + [item index _ render-data] + [:f> f-small-image item index _ render-data]) + +(defn bottom-view + [{:keys [images index scroll-index insets animations derived item-width props state transparent? + bottom-text-component]}] + (let [padding-horizontal (- (/ item-width 2) (/ constants/focused-image-size 2))] + [reanimated/linear-gradient + {:colors [colors/neutral-100-opa-100 colors/neutral-100-opa-80 colors/neutral-100-opa-0] + :location [0.2 0.9] + :start {:x 0 :y 1} + :end {:x 0 :y 0} + :style (style/gradient-container insets animations derived transparent?)} + (when bottom-text-component + [text-sheet/view + {:overlay-opacity (:overlay-opacity animations) + :overlay-z-index (:overlay-z-index state) + :text-sheet-lock? (:text-sheet-lock? props) + :text-component bottom-text-component}]) + [rn/flat-list + {:ref #(reset! (:small-list-ref props) %) + :key-fn :id + :style {:height constants/small-list-height} + :data images + :render-fn small-image + :render-data {:scroll-index scroll-index + :props props} + :horizontal true + :shows-horizontal-scroll-indicator false + :get-item-layout get-small-item-layout + :separator [rn/view {:style {:width 8}}] + :initial-scroll-index index + :content-container-style (style/content-container padding-horizontal)}] + ;; This is needed so that text does not show in the bottom inset part as it is transparent + [rn/view {:style (style/bottom-inset-cover-up insets)}]])) diff --git a/src/status_im/common/lightbox/constants.cljs b/src/status_im/common/lightbox/constants.cljs new file mode 100644 index 00000000000..85c7ffe74c3 --- /dev/null +++ b/src/status_im/common/lightbox/constants.cljs @@ -0,0 +1,16 @@ +(ns status-im.common.lightbox.constants) + +(def ^:const small-image-size 40) +(def ^:const focused-extra-size 16) +(def ^:const focused-image-size (+ small-image-size focused-extra-size)) +(def ^:const small-list-height 80) +(def ^:const small-list-padding-vertical 12) +(def ^:const top-view-height 56) +(def ^:const separator-width 16) +(def ^:const drag-threshold 100) + +;;; TEXT SHEET +(def ^:const text-margin 12) +(def ^:const bar-container-height 30) +(def ^:const line-height 22) +(def ^:const text-min-height (+ bar-container-height (* line-height 2) 4)) diff --git a/src/status_im/common/lightbox/effects.cljs b/src/status_im/common/lightbox/effects.cljs new file mode 100644 index 00000000000..5917020043c --- /dev/null +++ b/src/status_im/common/lightbox/effects.cljs @@ -0,0 +1,30 @@ +(ns status-im.common.lightbox.effects + (:require [react-native.blob :as blob] + [react-native.cameraroll :as cameraroll] + [react-native.fs :as fs] + [react-native.platform :as platform] + [utils.re-frame :as rf])) + +(def config + {:trusty platform/ios? + :path (str (fs/cache-dir) "/StatusIm_Image.jpeg")}) + +(rf/reg-fx :effects.lightbox/share-image + (fn [uri] + (blob/fetch uri + config + (fn [downloaded-url] + (rf/dispatch [:open-share + {:options {:url (str (when platform/android? "file://") + downloaded-url) + :isNewTask true} + :on-success #(fs/unlink downloaded-url) + :on-error #(fs/unlink downloaded-url)}]))))) + +(rf/reg-fx :effects.lightbox/save-image-to-gallery + (fn [[uri on-success]] + (blob/fetch uri + config + (fn [downloaded-url] + (cameraroll/save-image downloaded-url) + (on-success))))) diff --git a/src/status_im/common/lightbox/events.cljs b/src/status_im/common/lightbox/events.cljs new file mode 100644 index 00000000000..348d570d951 --- /dev/null +++ b/src/status_im/common/lightbox/events.cljs @@ -0,0 +1,37 @@ +(ns status-im.common.lightbox.events + (:require [reagent.core :as reagent] + status-im.common.lightbox.effects + [utils.re-frame :as rf])) + +(rf/reg-event-fx :lightbox/navigate-to-lightbox + (fn [{:keys [db]} [animation-shared-element-id screen-params]] + (reagent/next-tick #(rf/dispatch [:navigate-to :screen/lightbox screen-params])) + {:db (assoc db :animation-shared-element-id animation-shared-element-id)})) + +(rf/reg-event-fx :lightbox/update-animation-shared-element-id + (fn [{:keys [db]} [animation-shared-element-id]] + {:db (assoc db :animation-shared-element-id animation-shared-element-id)})) + +(rf/reg-event-fx :lightbox/exit-lightbox-signal + (fn [{:keys [db]} [value]] + {:db (assoc db :lightbox/exit-signal value)})) + +(rf/reg-event-fx :lightbox/zoom-out-signal + (fn [{:keys [db]} [value]] + {:db (assoc db :lightbox/zoom-out-signal value)})) + +(rf/reg-event-fx :lightbox/orientation-change + (fn [{:keys [db]} [value]] + {:db (assoc db :lightbox/orientation value)})) + +(rf/reg-event-fx :lightbox/lightbox-scale + (fn [{:keys [db]} [value]] + {:db (assoc db :lightbox/scale value)})) + +(rf/reg-event-fx :lightbox/share-image + (fn [_ [uri]] + {:effects.lightbox/share-image uri})) + +(rf/reg-event-fx :lightbox/save-image-to-gallery + (fn [_ [uri on-success]] + {:effects.lightbox/save-image-to-gallery [uri on-success]})) diff --git a/src/status_im/common/lightbox/style.cljs b/src/status_im/common/lightbox/style.cljs new file mode 100644 index 00000000000..0109394d8b6 --- /dev/null +++ b/src/status_im/common/lightbox/style.cljs @@ -0,0 +1,113 @@ +(ns status-im.common.lightbox.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [status-im.common.lightbox.constants :as constants])) + +;;;; VIEW +(defn image + [width height] + {:flex-direction :row + :width width + :height height + :align-items :center + :justify-content :center}) + +;;;; TOP-VIEW +(defn top-view-container + [top-inset window-width bg-color landscape? + {:keys [opacity rotate top-view-y top-view-x top-view-width top-view-bg]} + {:keys [top-layout]}] + (reanimated/apply-animations-to-style + (if platform/ios? + {:transform [{:translateY top-layout} + {:rotate rotate} + {:translateY top-view-y} + {:translateX top-view-x}] + :opacity opacity + :width top-view-width + :background-color top-view-bg} + {:transform [{:translateY top-layout}] + :opacity opacity}) + {:position :absolute + :padding-horizontal 20 + :top (if (or platform/ios? (not landscape?)) top-inset 0) + :height constants/top-view-height + :z-index 4 + :flex-direction :row + :justify-content :space-between + :width (when platform/android? window-width) + :background-color (when platform/android? bg-color) + :align-items :center})) + +(defn top-gradient + [insets] + {:position :absolute + :height (+ constants/top-view-height (:top insets) 0) + :top (- (:top insets)) + :left 0 + :right 0}) + +(def close-container + {:width 32 + :height 32 + :border-radius 12 + :justify-content :center + :align-items :center + :background-color colors/neutral-80-opa-40}) + +(def top-right-buttons + {:flex-direction :row}) + +;;;; BOTTOM-VIEW +(defn gradient-container + [insets {:keys [opacity]} {:keys [bottom-layout]} transparent?] + (reanimated/apply-animations-to-style + {:transform [{:translateY bottom-layout}] + :opacity opacity} + {:position :absolute + :overflow :visible + :display (if @transparent? :none :flex) + :bottom 0 + :padding-bottom (:bottom insets) + :padding-top constants/text-min-height + :z-index 3})) + +(defn content-container + [padding-horizontal] + {:padding-vertical constants/small-list-padding-vertical + :padding-horizontal padding-horizontal + :align-items :center + :justify-content :center}) + + +(defn background-bottom-gradient + [{:keys [overlay-opacity]} z-index] + (reanimated/apply-animations-to-style + {:opacity (reanimated/interpolate overlay-opacity [0 0.1 0.4 1] [0 0.1 1 1])} + {:position :absolute + :top 0 + :bottom 0 + :z-index z-index + :left 0 + :right 0})) + +(defn background-top-gradient + [{:keys [overlay-opacity]} z-index] + (reanimated/apply-animations-to-style + {:opacity (reanimated/interpolate overlay-opacity [0.3 1] [0 1])} + {:position :absolute + :top 0 + :bottom 0 + :z-index z-index + :left 0 + :right 0})) + +(defn bottom-inset-cover-up + [insets] + {:height (:bottom insets) + :position :absolute + :bottom 0 + :left 0 + :right 0}) diff --git a/src/status_im/common/lightbox/text_sheet/style.cljs b/src/status_im/common/lightbox/text_sheet/style.cljs new file mode 100644 index 00000000000..5999742d2a7 --- /dev/null +++ b/src/status_im/common/lightbox/text_sheet/style.cljs @@ -0,0 +1,61 @@ +(ns status-im.common.lightbox.text-sheet.style + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated] + [status-im.common.lightbox.constants :as constants])) + +(defn sheet-container + [{:keys [height top]}] + (reanimated/apply-animations-to-style + {:height height + :top top} + {:position :absolute + :left 0 + :right 0})) + +(def bar-container + {:height constants/bar-container-height + :left 0 + :right 0 + :top 0 + :justify-content :center + :align-items :center}) + +(def bar + {:width 32 + :height 4 + :border-radius 100 + :background-color colors/white-opa-10}) + +(defn top-gradient + [{:keys [derived-value]} {:keys [top]} insets max-height] + (let [initial-top (- (+ (:top insets) + constants/top-view-height)) + height (+ (:top insets) + constants/top-view-height + (* constants/line-height 2))] + (reanimated/apply-animations-to-style + {:opacity (reanimated/interpolate derived-value + [max-height (+ max-height constants/line-height)] + [0 1]) + :top (reanimated/interpolate top + [0 (- max-height)] + [initial-top (- initial-top max-height)] + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"})} + {:position :absolute + :left 0 + :height height + :right 0 + :z-index 1}))) + +(defn bottom-gradient + [bottom-inset] + (let [gradient-distance (+ constants/small-list-height bottom-inset)] + {:position :absolute + :left 0 + :right 0 + :height (+ gradient-distance (* 2 constants/line-height)) + :bottom (- gradient-distance) + :opacity 0.8 + :z-index 1})) diff --git a/src/status_im/common/lightbox/text_sheet/utils.cljs b/src/status_im/common/lightbox/text_sheet/utils.cljs new file mode 100644 index 00000000000..b751579cbe9 --- /dev/null +++ b/src/status_im/common/lightbox/text_sheet/utils.cljs @@ -0,0 +1,94 @@ +(ns status-im.common.lightbox.text-sheet.utils + (:require + [oops.core :as oops] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [reagent.core :as r] + [status-im.common.lightbox.constants :as constants])) + +(defn- collapse-sheet + [{:keys [derived-value overlay-opacity saved-top expanded? overlay-z-index]}] + (reanimated/animate derived-value constants/text-min-height) + (reanimated/animate overlay-opacity 0) + (reanimated/set-shared-value saved-top (- constants/text-min-height)) + (reset! expanded? false) + (js/setTimeout #(reset! overlay-z-index 0) 300)) + +(defn sheet-gesture + [{:keys [derived-value saved-top overlay-opacity gradient-opacity]} + expanded-height max-height full-height overlay-z-index expanded? dragging? expandable-text?] + (let [disable-gesture-update (r/atom false)] + (-> (gesture/gesture-pan) + (gesture/enabled expandable-text?) + (gesture/on-start (fn [] + (reset! overlay-z-index 1) + (reset! dragging? true) + (reset! disable-gesture-update false) + (when (not expanded?) + (reanimated/animate gradient-opacity 0)))) + (gesture/on-update + (fn [e] + (when-not @disable-gesture-update + (let [event-value (oops/oget e :translationY) + old-value (reanimated/get-shared-value saved-top) + new-value (+ old-value event-value) + progress (/ (- new-value) max-height) + reached-expanded? (< new-value (- max-height)) + upper-boundary? (< new-value (- full-height)) + lower-boundary? (and (> new-value (- constants/text-min-height)) + (pos? event-value))] + (when (and (not upper-boundary?) (not lower-boundary?)) + (reset! expanded? false) + (reanimated/set-shared-value overlay-opacity progress) + (reanimated/set-shared-value derived-value (- new-value))) + (when reached-expanded? (reset! expanded? true)) + (when lower-boundary? + (reset! disable-gesture-update true) + (collapse-sheet {:derived-value derived-value + :overlay-opacity overlay-opacity + :saved-top saved-top + :expanded? expanded? + :overlay-z-index overlay-z-index})))))) + (gesture/on-end + (fn [] + (let [shared-derived-value (reanimated/get-shared-value derived-value) + below-max-height? (< shared-derived-value max-height) + below-saved-top? (> (- shared-derived-value) + (reanimated/get-shared-value saved-top))] + (if (and below-max-height? below-saved-top?) + (collapse-sheet {:derived-value derived-value + :overlay-opacity overlay-opacity + :saved-top saved-top + :expanded? expanded? + :overlay-z-index overlay-z-index}) + (reanimated/set-shared-value saved-top + (- shared-derived-value))) + (when (= shared-derived-value expanded-height) + (reset! expanded? true)) + (reset! dragging? false))))))) + +(defn expand-sheet + [{:keys [derived-value overlay-opacity saved-top]} + expanded-height max-height overlay-z-index expanded? text-sheet-lock?] + (when-not @text-sheet-lock? + (reanimated/animate derived-value expanded-height) + (reanimated/animate overlay-opacity (/ expanded-height max-height)) + (reanimated/set-shared-value saved-top (- expanded-height)) + (reset! overlay-z-index 1) + (reset! expanded? true))) + +(defn on-layout + [e text-height] + (reset! text-height (oops/oget e "nativeEvent.layout.height"))) + +(defn init-animations + [overlay-opacity] + {:derived-value (reanimated/use-shared-value constants/text-min-height) + :saved-top (reanimated/use-shared-value (- constants/text-min-height)) + :gradient-opacity (reanimated/use-shared-value 0) + :overlay-opacity overlay-opacity}) + +(defn init-derived-animations + [{:keys [derived-value]}] + {:height derived-value + :top (reanimated/interpolate derived-value [0 1] [0 -1])}) diff --git a/src/status_im/common/lightbox/text_sheet/view.cljs b/src/status_im/common/lightbox/text_sheet/view.cljs new file mode 100644 index 00000000000..f19d171eb97 --- /dev/null +++ b/src/status_im/common/lightbox/text_sheet/view.cljs @@ -0,0 +1,86 @@ +(ns status-im.common.lightbox.text-sheet.view + (:require + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.linear-gradient :as linear-gradient] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.lightbox.constants :as constants] + [status-im.common.lightbox.text-sheet.style :as style] + [status-im.common.lightbox.text-sheet.utils :as utils])) + +(defn- text-sheet + [_] + (let [text-height (reagent/atom 0) + expanded? (reagent/atom false) + dragging? (atom false)] + (fn [{:keys [overlay-opacity overlay-z-index text-sheet-lock? text-component]}] + (let [insets safe-area/insets + window-height (:height (rn/get-window)) + max-height (- window-height + constants/text-min-height + constants/top-view-height + (:bottom insets) + (when platform/ios? (:top insets))) + full-height (+ constants/bar-container-height + constants/text-margin + constants/line-height + @text-height) + expanded-height (min max-height full-height) + animations (utils/init-animations overlay-opacity) + derived (utils/init-derived-animations animations) + expandable-text? (> @text-height (* constants/line-height 2))] + [rn/view + [reanimated/linear-gradient + {:colors [colors/neutral-100-opa-0 colors/neutral-100] + :pointer-events :none + :locations [0 0.3] + :start {:x 0 :y 1} + :end {:x 0 :y 0} + :style (style/top-gradient animations derived insets max-height)}] + [gesture/gesture-detector + {:gesture (utils/sheet-gesture animations + expanded-height + max-height + full-height + overlay-z-index + expanded? + dragging? + expandable-text?)} + [gesture/gesture-detector + {:gesture (-> (gesture/gesture-tap) + (gesture/enabled (and expandable-text? (not @expanded?))) + (gesture/on-start (fn [] + (utils/expand-sheet animations + expanded-height + max-height + overlay-z-index + expanded? + text-sheet-lock?))))} + [reanimated/view {:style (style/sheet-container derived)} + (when expandable-text? + [rn/view {:style style/bar-container} + [rn/view {:style style/bar}]]) + [linear-gradient/linear-gradient + {:colors [colors/neutral-100-opa-100 colors/neutral-100-opa-70 colors/neutral-100-opa-0] + :start {:x 0 :y 1} + :end {:x 0 :y 0} + :locations [0.7 0.8 1] + :style (style/bottom-gradient (:bottom insets))}] + + [gesture/scroll-view + {:scroll-enabled false + :scroll-event-throttle 16 + :bounces false + :style {:height (- max-height constants/bar-container-height)} + :content-container-style {:padding-top (when (not expandable-text?) + constants/bar-container-height)}} + [rn/view {:on-layout #(utils/on-layout % text-height)} + text-component]]]]]])))) + +(defn view + [props] + [:f> text-sheet props]) diff --git a/src/status_im/common/lightbox/top_view.cljs b/src/status_im/common/lightbox/top_view.cljs new file mode 100644 index 00000000000..c94491cccd5 --- /dev/null +++ b/src/status_im/common/lightbox/top_view.cljs @@ -0,0 +1,98 @@ +(ns status-im.common.lightbox.top-view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.orientation :as orientation] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [status-im.common.lightbox.animations :as anim] + [status-im.common.lightbox.constants :as constants] + [status-im.common.lightbox.style :as style] + [utils.re-frame :as rf] + [utils.url :as url])) + +(defn animate-rotation + [result screen-width screen-height insets + {:keys [rotate top-view-y top-view-x top-view-width top-view-bg]}] + (let [top-x (+ (/ constants/top-view-height 2) (:top insets))] + (cond + (= result orientation/landscape-left) + (do + (anim/animate rotate "90deg") + (anim/animate top-view-y 60) + (anim/animate top-view-x (- (/ screen-height 2) top-x)) + (anim/animate top-view-width screen-height) + (anim/animate top-view-bg colors/neutral-100-opa-70)) + (= result orientation/landscape-right) + (do + (anim/animate rotate "-90deg") + (anim/animate top-view-y (- (- screen-width) 4)) + (anim/animate top-view-x (+ (/ screen-height -2) top-x)) + (anim/animate top-view-width screen-height) + (anim/animate top-view-bg colors/neutral-100-opa-70)) + (= result orientation/portrait) + (do + (anim/animate rotate "0deg") + (anim/animate top-view-y 0) + (anim/animate top-view-x 0) + (anim/animate top-view-width screen-width) + (anim/animate top-view-bg colors/neutral-100-opa-0))))) + +(defn share-image + [images index] + (let [{:keys [image]} (nth images index) + uri (url/replace-port image (rf/sub [:mediaserver/port]))] + (rf/dispatch [:lightbox/share-image uri]))) + +(defn top-view + [images insets index animations derived landscape? screen-width on-options-press] + (let [{:keys [description header]} (nth images @index) + bg-color (if landscape? + colors/neutral-100-opa-70 + colors/neutral-100-opa-0) + {:keys [background-color opacity + overlay-opacity]} animations] + [reanimated/view + {:style + (style/top-view-container (:top insets) screen-width bg-color landscape? animations derived)} + [reanimated/linear-gradient + {:colors [(colors/alpha "#000000" 0.8) :transparent] + :start {:x 0 :y 0} + :end {:x 0 :y 1} + :style (style/top-gradient insets)}] + [rn/view + {:style {:flex-direction :row + :align-items :center}} + [rn/touchable-opacity + {:on-press (fn [] + (anim/animate background-color :transparent) + (anim/animate opacity 0) + (anim/animate overlay-opacity 0) + (rf/dispatch (if platform/ios? + [:lightbox/exit-lightbox-signal @index] + [:navigate-back]))) + :style style/close-container} + [quo/icon :close {:size 20 :color colors/white}]] + [rn/view {:style {:margin-left 12}} + [quo/text + {:weight :semi-bold + :size :paragraph-1 + :style {:color colors/white}} header] + [quo/text + {:weight :medium + :size :paragraph-2 + :style {:color colors/neutral-40}} description]]] + [rn/view {:style style/top-right-buttons} + [rn/touchable-opacity + {:active-opacity 1 + :accessibility-label :share-image + :on-press #(share-image images @index) + :style (merge style/close-container {:margin-right 12})} + [quo/icon :share {:size 20 :color colors/white}]] + [rn/touchable-opacity + {:active-opacity 1 + :accessibility-label :image-options + :on-press #(on-options-press images @index) + :style style/close-container} + [quo/icon :options {:size 20 :color colors/white}]]]])) diff --git a/src/status_im/common/lightbox/utils.cljs b/src/status_im/common/lightbox/utils.cljs new file mode 100644 index 00000000000..ebf3a436c98 --- /dev/null +++ b/src/status_im/common/lightbox/utils.cljs @@ -0,0 +1,188 @@ +(ns status-im.common.lightbox.utils + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.orientation :as orientation] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.lightbox.animations :as anim] + [status-im.common.lightbox.constants :as constants] + [status-im.common.lightbox.top-view :as top-view] + [utils.re-frame :as rf] + [utils.worklets.chat.messenger.lightbox :as worklet])) + +(defn clear-timers + [timers] + (js/clearTimeout (:mount-animation @timers)) + (js/clearTimeout (:mount-index-lock @timers)) + (js/clearTimeout (:hide-0 @timers)) + (js/clearTimeout (:hide-1 @timers)) + (js/clearTimeout (:show-0 @timers)) + (js/clearTimeout (:show-1 @timers)) + (js/clearTimeout (:show-2 @timers))) + +(defn effect + [{:keys [flat-list-ref scroll-index-lock? timers]} {:keys [opacity layout border]} index] + (rn/use-mount + (fn [] + (reagent/next-tick (fn [] + (when @flat-list-ref + (.scrollToOffset ^js @flat-list-ref + #js + {:animated false + :offset (* (+ (:width (rn/get-window)) + constants/separator-width) + index)})))) + (swap! timers assoc + :mount-animation + (js/setTimeout (fn [] + (anim/animate opacity 1) + (anim/animate layout 0) + (anim/animate border 16)) + (if platform/ios? 250 100))) + (swap! timers assoc :mount-index-lock (js/setTimeout #(reset! scroll-index-lock? false) 300)) + (fn [] + (rf/dispatch [:lightbox/zoom-out-signal nil]) + (when platform/android? + (rf/dispatch [:chat.ui/lightbox-scale 1])) + (clear-timers timers))))) + +(defn handle-orientation + [result {:keys [flat-list-ref]} {:keys [scroll-index]} animations] + (let [insets safe-area/insets + window (rn/get-window) + window-width (:width window) + window-height (:height window) + window-height (if platform/android? + (+ window-height (:top insets)) + window-height) + screen-width (if (or platform/ios? (= result orientation/portrait)) + window-width + window-height) + screen-height (if (or platform/ios? (= result orientation/portrait)) + window-height + window-width) + landscape? (string/includes? result orientation/landscape) + item-width (if (and landscape? platform/ios?) screen-height screen-width)] + (when (or landscape? (= result orientation/portrait)) + (rf/dispatch [:lightbox/orientation-change result])) + (cond + landscape? + (orientation/lock-to-landscape "lightbox") + (= result orientation/portrait) + (orientation/lock-to-portrait "lightbox")) + (js/setTimeout + (fn [] + (when @flat-list-ref + (.scrollToOffset + ^js @flat-list-ref + #js {:animated false :offset (* (+ item-width constants/separator-width) @scroll-index)}))) + 100) + (when platform/ios? + (top-view/animate-rotation result screen-width screen-height insets animations)))) + +(defn orientation-change + [props state animations] + (orientation/use-device-orientation-change + (fn [result] + (if platform/ios? + (handle-orientation result props state animations) + ;; `use-device-orientation-change` will always be called on Android, so need to check + (orientation/get-auto-rotate-state + (fn [enabled?] + ;; RNN does not support landscape-right + (when (and enabled? (not= result orientation/landscape-right)) + (handle-orientation result props state animations)))))))) + +(defn on-scroll + [e item-width {:keys [images-opacity]} landscape?] + (let [total-item-width (+ item-width constants/separator-width) + progress (/ (if landscape? + (oops/oget e "nativeEvent.contentOffset.y") + (oops/oget e "nativeEvent.contentOffset.x")) + total-item-width) + index-initial (max (Math/floor progress) 0) + index-final (inc index-initial) + decimal-part (- progress index-initial)] + (anim/set-val (nth images-opacity index-initial) (- 1 decimal-part)) + (when (< index-final (count images-opacity)) + (anim/set-val (nth images-opacity index-final) decimal-part)))) + +(defn drag-gesture + [{:keys [pan-x pan-y background-color opacity layout]} x? set-full-height?] + (-> + (gesture/gesture-pan) + (gesture/enabled true) + (gesture/max-pointers 1) + (gesture/on-start #(reset! set-full-height? false)) + (gesture/on-update + (fn [e] + (let [translation (if x? (oops/oget e "translationX") (oops/oget e "translationY")) + progress (Math/abs (/ translation constants/drag-threshold))] + (anim/set-val (if x? pan-x pan-y) translation) + (anim/set-val opacity (- 1 progress)) + (anim/set-val layout (* progress -20))))) + (gesture/on-end (fn [e] + (if (> (Math/abs (if x? (oops/oget e "translationX") (oops/oget e "translationY"))) + constants/drag-threshold) + (do + (anim/animate background-color "rgba(0,0,0,0)") + (anim/animate opacity 0) + (rf/dispatch [:navigate-back])) + (do + (reset! set-full-height? true) + (anim/animate (if x? pan-x pan-y) 0) + (anim/animate opacity 1) + (anim/animate layout 0))))))) + +(defn init-props + [] + {:flat-list-ref (atom nil) + :small-list-ref (atom nil) + :scroll-index-lock? (atom true) + :text-sheet-lock? (atom false) + :timers (atom {})}) + +(defn init-state + [images index] + ;; The initial value of data is the image that was pressed (and not the whole album) in order + ;; for the transition animation to execute properly, otherwise it would animate towards + ;; outside the screen (even if we have `initialScrollIndex` set). + {:data (reagent/atom (if (number? index) [(nth images index)] [])) + :scroll-index (reagent/atom index) + :transparent? (reagent/atom false) + :set-full-height? (reagent/atom false) + :overlay-z-index (reagent/atom 0)}) + +(defn initialize-opacity + [size selected-index] + (mapv #(if (= % selected-index) + (anim/use-val 1) + (anim/use-val 0)) + (range size))) + +(defn init-animations + [size index] + {:background-color (anim/use-val colors/neutral-100-opa-0) + :border (anim/use-val (if platform/ios? 0 16)) + :full-screen-scale (anim/use-val 1) + :opacity (anim/use-val 0) + :overlay-opacity (anim/use-val 0) + :images-opacity (initialize-opacity size index) + :rotate (anim/use-val "0deg") + :layout (anim/use-val -10) + :top-view-y (anim/use-val 0) + :top-view-x (anim/use-val 0) + :top-view-width (anim/use-val (:width (rn/get-window))) + :top-view-bg (anim/use-val colors/neutral-100-opa-0) + :pan-y (anim/use-val 0) + :pan-x (anim/use-val 0)}) + +(defn init-derived-animations + [{:keys [layout]}] + {:top-layout (worklet/info-layout layout true) + :bottom-layout (worklet/info-layout layout false)}) diff --git a/src/status_im/common/lightbox/view.cljs b/src/status_im/common/lightbox/view.cljs new file mode 100644 index 00000000000..4ce75a2d14c --- /dev/null +++ b/src/status_im/common/lightbox/view.cljs @@ -0,0 +1,168 @@ +(ns status-im.common.lightbox.view + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.orientation :as orientation] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.common.lightbox.animations :as anim] + [status-im.common.lightbox.bottom-view :as bottom-view] + [status-im.common.lightbox.constants :as constants] + [status-im.common.lightbox.style :as style] + [status-im.common.lightbox.top-view :as top-view] + [status-im.common.lightbox.utils :as utils] + [status-im.common.lightbox.zoomable-image.view :as zoomable-image] + [utils.re-frame :as rf])) + +(defn get-item-layout + [_ index item-width] + #js + {:length item-width + :offset (* (+ item-width constants/separator-width) index) + :index index}) + +(defn on-viewable-items-changed + [e {:keys [scroll-index-lock? small-list-ref]} {:keys [scroll-index]}] + (when-not @scroll-index-lock? + (let [changed (-> e (oops/oget :changed) first) + index (oops/oget changed :index)] + (reset! scroll-index index) + (when @small-list-ref + (.scrollToIndex ^js @small-list-ref #js {:animated true :index index})) + (rf/dispatch [:lightbox/update-animation-shared-element-id + (:id (oops/oget changed :item))])))) + +(defn image-view + [message index _ {:keys [screen-width screen-height] :as args}] + [rn/view + {:style (style/image (+ screen-width constants/separator-width) screen-height)} + [:f> zoomable-image/zoomable-image message index args] + [rn/view {:style {:width constants/separator-width}}]]) + +(defn lightbox-content + [{:keys [props state animations derived images index handle-items-changed bottom-text-component + on-options-press]}] + (let [{:keys [data transparent? scroll-index + set-full-height?]} state + insets safe-area/insets + window (rn/get-window) + window-width (:width window) + alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + window-height (- (if platform/android? + (+ (:height window) (:top insets)) + (:height window)) + alert-banners-top-margin) + curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait) + landscape? (string/includes? curr-orientation orientation/landscape) + horizontal? (or platform/android? (not landscape?)) + inverted? (and platform/ios? (= curr-orientation orientation/landscape-right)) + screen-width (if (or platform/ios? (= curr-orientation orientation/portrait)) + window-width + window-height) + screen-height (if (or platform/ios? (= curr-orientation orientation/portrait)) + window-height + window-width) + item-width (if (and landscape? platform/ios?) screen-height screen-width)] + [reanimated/view + {:style (reanimated/apply-animations-to-style {:background-color (:background-color animations)} + {:height screen-height})} + (when-not @transparent? + [:f> top-view/top-view images insets scroll-index animations derived landscape? + screen-width on-options-press]) + [gesture/gesture-detector + {:gesture (utils/drag-gesture animations (and landscape? platform/ios?) set-full-height?)} + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:transform [{:translateY (:pan-y animations)} + {:translateX (:pan-x animations)}]} + {})} + [reanimated/linear-gradient + {:colors [colors/neutral-100-opa-0 colors/neutral-100-opa-0 colors/neutral-100-opa-100 + colors/neutral-100] + :locations [0.3 0.4 0.6 1] + :pointer-events :none + :style (style/background-bottom-gradient animations @(:overlay-z-index state))}] + [reanimated/linear-gradient + {:colors [colors/neutral-100-opa-50 colors/neutral-100] + :pointer-events :none + :style (style/background-top-gradient animations @(:overlay-z-index state))}] + [gesture/flat-list + {:ref #(reset! (:flat-list-ref props) %) + :key-fn :message-id + :on-scroll #(utils/on-scroll % item-width animations landscape?) + :scroll-event-throttle 8 + :style {:width (+ screen-width constants/separator-width)} + :data @data + :render-fn image-view + :render-data {:opacity-value (:opacity animations) + :overlay-opacity (:overlay-opacity animations) + :border-value (:border animations) + :full-screen-scale (:full-screen-scale animations) + :images-opacity (:images-opacity animations) + :transparent? transparent? + :set-full-height? set-full-height? + :screen-height screen-height + :screen-width screen-width + :window-height window-height + :window-width window-width + :props props + :curr-orientation curr-orientation} + :horizontal horizontal? + :inverted inverted? + :paging-enabled true + :get-item-layout (fn [_ index] (get-item-layout _ index item-width)) + :viewability-config {:view-area-coverage-percent-threshold 50 + :wait-for-interaction true} + :shows-vertical-scroll-indicator false + :shows-horizontal-scroll-indicator false + :on-viewable-items-changed handle-items-changed}]]] + ;; NOTE: not un-mounting bottom-view based on `transparent?` (like we do with the top-view + ;; above), since we need to save the state of the text-sheet position. Instead, we use + ;; the `:display` style property to hide the bottom-sheet. + (when (and (not landscape?) (or bottom-text-component (> (count images) 1))) + [:f> bottom-view/bottom-view + {:images images + :index index + :scroll-index scroll-index + :insets insets + :animations animations + :derived derived + :item-width item-width + :props props + :state state + :transparent? transparent? + :bottom-text-component bottom-text-component}])])) + +(defn- lightbox-internal + [{:keys [images index bottom-text-component on-options-press]}] + (let [props (utils/init-props) + state (utils/init-state images index) + handle-items-changed (fn [e] (on-viewable-items-changed e props state))] + (fn [] + (let [animations (utils/init-animations (count images) index) + derived (utils/init-derived-animations animations)] + (anim/animate (:background-color animations) colors/neutral-100) + (reset! (:data state) images) + (when platform/ios? ; issue: https://github.com/wix/react-native-navigation/issues/7726 + (utils/orientation-change props state animations)) + (utils/effect props animations index) + [:f> lightbox-content + {:props props + :state state + :animations animations + :derived derived + :images images + :index index + :handle-items-changed handle-items-changed + :bottom-text-component bottom-text-component + :on-options-press on-options-press}])))) + +(defn lightbox + [] + (let [screen-params (quo.context/use-screen-params)] + [lightbox-internal screen-params])) diff --git a/src/status_im/common/lightbox/zoomable_image/constants.cljs b/src/status_im/common/lightbox/zoomable_image/constants.cljs new file mode 100644 index 00000000000..8e7b9953199 --- /dev/null +++ b/src/status_im/common/lightbox/zoomable_image/constants.cljs @@ -0,0 +1,17 @@ +(ns status-im.common.lightbox.zoomable-image.constants) + +(def ^:const min-scale 1) + +(def ^:const double-tap-scale 2) + +(def ^:const max-scale 5) + +(def ^:const init-offset 0) + +(def ^:const init-rotation "0deg") + +(def ^:const velocity-factor 0.5) + +(def ^:const default-duration 300) + +(def ^:const margin 16) diff --git a/src/status_im/common/lightbox/zoomable_image/style.cljs b/src/status_im/common/lightbox/zoomable_image/style.cljs new file mode 100644 index 00000000000..ef7cab3879c --- /dev/null +++ b/src/status_im/common/lightbox/zoomable_image/style.cljs @@ -0,0 +1,35 @@ +(ns status-im.common.lightbox.zoomable-image.style + (:require + [react-native.platform :as platform] + [react-native.reanimated :as reanimated])) + +(defn container + [{:keys [width height]} + {:keys [pan-x pan-y pinch-x pinch-y scale]} + full-screen-scale + set-full-height? + portrait?] + (reanimated/apply-animations-to-style + {:transform [{:translateX pan-x} + {:translateY pan-y} + {:translateX pinch-x} + {:translateY pinch-y} + {:scale scale} + {:scale full-screen-scale}]} + {:justify-content :center + :align-items :center + :width (if (or platform/ios? portrait?) width "100%") + :height (if set-full-height? "100%" height)})) + +(defn image + [{:keys [image-width image-height]} + {:keys [rotate rotate-scale]} + {:keys [border-value images-opacity]} + index] + (reanimated/apply-animations-to-style + {:transform [{:rotate rotate} + {:scale rotate-scale}] + :opacity (nth images-opacity index) + :border-radius border-value} + {:width image-width + :height image-height})) diff --git a/src/status_im/common/lightbox/zoomable_image/utils.cljs b/src/status_im/common/lightbox/zoomable_image/utils.cljs new file mode 100644 index 00000000000..0a529e78079 --- /dev/null +++ b/src/status_im/common/lightbox/zoomable_image/utils.cljs @@ -0,0 +1,241 @@ +(ns status-im.common.lightbox.zoomable-image.utils + (:require + [clojure.string :as string] + [react-native.core :as rn] + [react-native.navigation :as navigation] + [react-native.orientation :as orientation] + [react-native.platform :as platform] + [reagent.core :as reagent] + [status-im.common.lightbox.animations :as anim] + [status-im.common.lightbox.zoomable-image.constants :as constants] + [utils.re-frame :as rf])) + +;;; Helpers +(defn center-x + [{:keys [pinch-x pinch-x-start pan-x pan-x-start]} exit?] + (let [duration (if exit? 100 constants/default-duration)] + (anim/animate pinch-x constants/init-offset duration) + (anim/set-val pinch-x-start constants/init-offset) + (anim/animate pan-x constants/init-offset duration) + (anim/set-val pan-x-start constants/init-offset))) + +(defn center-y + [{:keys [pinch-y pinch-y-start pan-y pan-y-start]} exit?] + (let [duration (if exit? 100 constants/default-duration)] + (anim/animate pinch-y constants/init-offset duration) + (anim/set-val pinch-y-start constants/init-offset) + (anim/animate pan-y constants/init-offset duration) + (anim/set-val pan-y-start constants/init-offset))) + +(defn reset-values + [exit? animations {:keys [focal-x focal-y]}] + (center-x animations exit?) + (center-y animations exit?) + (reset! focal-x nil) + (reset! focal-y nil)) + +(defn rescale-image + [value + exit? + {:keys [x-threshold-scale y-threshold-scale]} + {:keys [scale saved-scale] :as animations} + {:keys [pan-x-enabled? pan-y-enabled?] :as state}] + (when (= value constants/min-scale) + (reset-values exit? animations state)) + (anim/animate scale value (if exit? 100 constants/default-duration)) + (anim/set-val saved-scale value) + (reset! pan-x-enabled? (> value x-threshold-scale)) + (reset! pan-y-enabled? (> value y-threshold-scale))) + +;;; Handlers +(defn handle-orientation-change + [curr-orientation + focused? + {:keys [landscape-scale-val x-threshold-scale y-threshold-scale]} + {:keys [rotate rotate-scale scale] :as animations} + {:keys [pan-x-enabled? pan-y-enabled?]}] + (if focused? + (cond + (= curr-orientation orientation/landscape-left) + (do + (anim/animate rotate "90deg") + (anim/animate rotate-scale landscape-scale-val)) + (= curr-orientation orientation/landscape-right) + (do + (anim/animate rotate "-90deg") + (anim/animate rotate-scale landscape-scale-val)) + (= curr-orientation orientation/portrait) + (do + (anim/animate rotate constants/init-rotation) + (anim/animate rotate-scale constants/min-scale))) + (cond + (= curr-orientation orientation/landscape-left) + (do + (anim/set-val rotate "90deg") + (anim/set-val rotate-scale landscape-scale-val)) + (= curr-orientation orientation/landscape-right) + (do + (anim/set-val rotate "-90deg") + (anim/set-val rotate-scale landscape-scale-val)) + (= curr-orientation orientation/portrait) + (do + (anim/set-val rotate constants/init-rotation) + (anim/set-val rotate-scale constants/min-scale)))) + (center-x animations false) + (center-y animations false) + (reset! pan-x-enabled? (> (anim/get-val scale) x-threshold-scale)) + (reset! pan-y-enabled? (> (anim/get-val scale) y-threshold-scale))) + +;; On ios, when attempting to navigate back while zoomed in, the shared-element transition animation +;; doesn't execute properly, so we need to zoom out first +(defn handle-exit-lightbox-signal + [exit-lightbox-signal index scale rescale set-full-height?] + (when (= exit-lightbox-signal index) + (reset! set-full-height? false) + (if (> scale constants/min-scale) + (do + (rescale constants/min-scale true) + (js/setTimeout #(rf/dispatch [:navigate-back]) 70)) + (rf/dispatch [:navigate-back])) + (js/setTimeout #(rf/dispatch [:lightbox/exit-lightbox-signal nil]) 500))) + +(defn handle-zoom-out-signal + "Zooms out when pressing on another photo from the small bottom list" + [zoom-out-signal index scale rescale] + (when (and (= zoom-out-signal index) (> scale constants/min-scale)) + (rescale constants/min-scale true))) + +(defn toggle-opacity + [index + {:keys [opacity-value overlay-opacity border-value full-screen-scale transparent? + props]} + portrait? last-overlay-opacity] + (let [{:keys [small-list-ref timers text-sheet-lock?]} props + scale-value (inc (/ constants/margin + (:width (rn/get-window))))] + (if @transparent? + (do + (js/clearTimeout (:hide-0 @timers)) + (js/clearTimeout (:hide-1 @timers)) + (reset! transparent? (not @transparent?)) + (swap! timers assoc + :show-0 + (js/setTimeout #((anim/animate opacity-value 1) + (anim/animate overlay-opacity @last-overlay-opacity) + (reset! last-overlay-opacity 0)) + 50)) + (swap! timers assoc + :show-1 + (js/setTimeout #(when @small-list-ref + (.scrollToIndex ^js @small-list-ref #js {:animated false :index index})) + 100)) + (anim/animate border-value 16) + (anim/animate full-screen-scale 1) + (when portrait? + (swap! timers assoc + :show-2 + (js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible true}}) + (if platform/ios? 150 50))))) + (do + (js/clearTimeout (:show-0 @timers)) + (js/clearTimeout (:show-1 @timers)) + (js/clearTimeout (:show-2 @timers)) + (swap! timers assoc + :hide-0 + (js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible false}}) + (if platform/ios? 75 0))) + (anim/animate opacity-value 0) + (reset! last-overlay-opacity (anim/get-val overlay-opacity)) + (anim/animate overlay-opacity 0) + (anim/animate border-value 0) + (anim/animate full-screen-scale scale-value) + (swap! timers assoc :hide-1 (js/setTimeout #(reset! transparent? (not @transparent?)) 400)) + (reset! text-sheet-lock? true) + (js/setTimeout #(reset! text-sheet-lock? false) 300))))) + +;;; Dimensions +(defn get-dimensions + "Calculates all required dimensions. Dimensions calculations are different on iOS and Android because landscape + mode is implemented differently.On Android, we just need to resize the content, and the OS takes care of the + animations. On iOS, we need to animate the content ourselves in code" + [pixels-width pixels-height curr-orientation + {:keys [window-width screen-width screen-height]}] + (let [landscape? (string/includes? curr-orientation orientation/landscape) + portrait-image-width window-width + portrait-image-height (* pixels-height (/ window-width pixels-width)) + landscape-image-width (* pixels-width (/ window-width pixels-height)) + width (if landscape? landscape-image-width portrait-image-width) + height (if landscape? screen-height portrait-image-height) + container-width (- (if platform/ios? window-width width) constants/margin) + container-height (- (if (and platform/ios? landscape?) landscape-image-width height) + constants/margin)] + ;; width and height used in style prop + {:image-width (- (if platform/ios? portrait-image-width width) constants/margin) + :image-height (- (if platform/ios? portrait-image-height height) constants/margin) + ;; container width and height, also used in animations calculations + :width container-width + :height container-height + ;; screen width and height used in calculations, and depends on platform + :screen-width screen-width + :screen-height screen-height + :x-threshold-scale (/ screen-width (min screen-width container-width)) + :y-threshold-scale (/ screen-height (min screen-height container-height)) + :landscape-scale-val (/ portrait-image-width portrait-image-height)})) + + +;;; MATH +(defn get-max-offset + [size screen-size scale] + (/ (- (* size (min scale constants/max-scale)) + screen-size) + 2)) + +(defn get-scale-diff + [new-scale saved-scale] + (- (dec new-scale) + (dec saved-scale))) + +(defn get-double-tap-offset + [size screen-size focal] + (let [center (/ size 2) + target-point (* (- center focal) constants/double-tap-scale) + max-offset (get-max-offset size screen-size constants/double-tap-scale) + translate-val (min (Math/abs target-point) max-offset)] + (if (neg? target-point) (- translate-val) translate-val))) + +(defn get-pinch-position + [scale-diff size focal] + (* (- (/ size 2) focal) scale-diff)) + +(defn get-focal + [focal size screen-size] + (let [min-size (/ (- screen-size size) 2) + max-size (+ min-size size)] + (if (or (> focal max-size) (< focal min-size)) + (/ screen-size 2) + focal))) + +;;; INITIALIZATIONS +(defn init-animations + [] + {:scale (anim/use-val constants/min-scale) + :saved-scale (anim/use-val constants/min-scale) + :pan-x-start (anim/use-val constants/init-offset) + :pan-x (anim/use-val constants/init-offset) + :pan-y-start (anim/use-val constants/init-offset) + :pan-y (anim/use-val constants/init-offset) + :pinch-x-start (anim/use-val constants/init-offset) + :pinch-x (anim/use-val constants/init-offset) + :pinch-y-start (anim/use-val constants/init-offset) + :pinch-y (anim/use-val constants/init-offset) + :pinch-x-max (anim/use-val js/Infinity) + :pinch-y-max (anim/use-val js/Infinity) + :rotate (anim/use-val constants/init-rotation) + :rotate-scale (anim/use-val constants/min-scale)}) + +(defn init-state + [] + {:pan-x-enabled? (reagent/atom false) + :pan-y-enabled? (reagent/atom false) + :focal-x (reagent/atom nil) + :focal-y (reagent/atom nil)}) diff --git a/src/status_im/common/lightbox/zoomable_image/view.cljs b/src/status_im/common/lightbox/zoomable_image/view.cljs new file mode 100644 index 00000000000..7f777114c72 --- /dev/null +++ b/src/status_im/common/lightbox/zoomable_image/view.cljs @@ -0,0 +1,273 @@ +(ns status-im.common.lightbox.zoomable-image.view + (:require + [oops.core :refer [oget]] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.orientation :as orientation] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [reagent.core :as r] + [status-im.common.lightbox.animations :as anim] + [status-im.common.lightbox.zoomable-image.constants :as c] + [status-im.common.lightbox.zoomable-image.style :as style] + [status-im.common.lightbox.zoomable-image.utils :as utils] + [utils.re-frame :as rf] + [utils.url :as url])) + +(defn tap-gesture + [on-tap] + (-> + (gesture/gesture-tap) + (gesture/on-start #(on-tap)))) + +(defn double-tap-gesture + [{:keys [width height screen-width screen-height y-threshold-scale x-threshold-scale]} + {:keys [scale pan-x pan-x-start pan-y pan-y-start]} + rescale + transparent? + toggle-opacity] + (-> + (gesture/gesture-tap) + (gesture/number-of-taps 2) + (gesture/on-start + (fn [e] + (if (= (anim/get-val scale) c/min-scale) + (let [translate-x (utils/get-double-tap-offset width screen-width (oget e "x")) + translate-y (utils/get-double-tap-offset height screen-height (oget e "y"))] + (when (> c/double-tap-scale x-threshold-scale) + (anim/animate pan-x translate-x) + (anim/set-val pan-x-start translate-x)) + (when (> c/double-tap-scale y-threshold-scale) + (anim/animate pan-y translate-y) + (anim/set-val pan-y-start translate-y)) + (rescale c/double-tap-scale) + (when (not @transparent?) + (toggle-opacity))) + (do + (rescale c/min-scale) + (when @transparent? + (toggle-opacity)))))))) + +;; not using on-finalize because on-finalize gets called always regardless the gesture executed or not +(defn finalize-pinch + [{:keys [width height screen-height screen-width x-threshold-scale y-threshold-scale]} + {:keys [scale pinch-x pinch-y pinch-x-start pinch-y-start pan-y pan-y-start pan-x + pan-x-start]} + {:keys [pan-x-enabled? pan-y-enabled?]}] + (let [curr-offset-y (+ (anim/get-val pan-y) (anim/get-val pinch-y)) + max-offset-y (utils/get-max-offset height screen-height (anim/get-val scale)) + max-offset-y (if (neg? curr-offset-y) (- max-offset-y) max-offset-y) + curr-offset-x (+ (anim/get-val pan-x) (anim/get-val pinch-x)) + max-offset-x (utils/get-max-offset width screen-width (anim/get-val scale)) + max-offset-x (if (neg? curr-offset-x) (- max-offset-x) max-offset-x)] + (when (and (> (anim/get-val scale) y-threshold-scale) + (< (anim/get-val scale) c/max-scale) + (> (Math/abs curr-offset-y) (Math/abs max-offset-y))) + (anim/animate pinch-y c/init-offset) + (anim/set-val pinch-y-start c/init-offset) + (anim/animate pan-y max-offset-y) + (anim/set-val pan-y-start max-offset-y)) + (when (and (> (anim/get-val scale) x-threshold-scale) + (< (anim/get-val scale) c/max-scale) + (> (Math/abs curr-offset-x) (Math/abs max-offset-x))) + (anim/animate pinch-x c/init-offset) + (anim/set-val pinch-x-start c/init-offset) + (anim/animate pan-x max-offset-x) + (anim/set-val pan-x-start max-offset-x)) + (reset! pan-x-enabled? (> (anim/get-val scale) x-threshold-scale)) + (reset! pan-y-enabled? (> (anim/get-val scale) y-threshold-scale)))) + +(defn pinch-gesture + [{:keys [width height screen-height screen-width x-threshold-scale y-threshold-scale] :as dimensions} + {:keys [saved-scale scale pinch-x pinch-y pinch-x-start pinch-y-start pinch-x-max pinch-y-max] + :as animations} + {:keys [focal-x focal-y] :as state} + rescale + transparent? + toggle-opacity] + (-> + (gesture/gesture-pinch) + (gesture/on-begin (fn [e] + (when platform/ios? + (reset! focal-x (oget e "focalX")) + (reset! focal-y (utils/get-focal (oget e "focalY") height screen-height))))) + (gesture/on-start (fn [e] + (when (and (= (anim/get-val saved-scale) c/min-scale) (not @transparent?)) + (toggle-opacity)) + (when platform/android? + (reset! focal-x (utils/get-focal (oget e "focalX") width screen-width)) + (reset! focal-y (utils/get-focal (oget e "focalY") height screen-height))))) + (gesture/on-update (fn [e] + (let [new-scale (* (oget e "scale") (anim/get-val saved-scale)) + scale-diff (utils/get-scale-diff new-scale (anim/get-val saved-scale)) + new-pinch-x (utils/get-pinch-position scale-diff screen-width @focal-x) + new-pinch-y (utils/get-pinch-position scale-diff screen-height @focal-y)] + (when (and (>= new-scale c/max-scale) + (= (anim/get-val pinch-x-max) js/Infinity)) + (anim/set-val pinch-x-max (anim/get-val pinch-x)) + (anim/set-val pinch-y-max (anim/get-val pinch-y))) + (anim/set-val pinch-x (+ new-pinch-x (anim/get-val pinch-x-start))) + (anim/set-val pinch-y (+ new-pinch-y (anim/get-val pinch-y-start))) + (anim/set-val scale new-scale)))) + (gesture/on-end + (fn [] + (cond + (< (anim/get-val scale) c/min-scale) + (do + (when @transparent? + (toggle-opacity)) + (rescale c/min-scale)) + (> (anim/get-val scale) c/max-scale) + (do + (anim/animate pinch-x (anim/get-val pinch-x-max)) + (anim/set-val pinch-x-start (anim/get-val pinch-x-max)) + (anim/set-val pinch-x-max js/Infinity) + (anim/animate pinch-y (anim/get-val pinch-y-max)) + (anim/set-val pinch-y-start (anim/get-val pinch-y-max)) + (anim/set-val pinch-y-max js/Infinity) + (anim/animate scale c/max-scale) + (anim/set-val saved-scale c/max-scale)) + :else + (do + (anim/set-val saved-scale (anim/get-val scale)) + (anim/set-val pinch-x-start (anim/get-val pinch-x)) + (anim/set-val pinch-y-start (anim/get-val pinch-y)) + (when (< (anim/get-val scale) x-threshold-scale) + (utils/center-x animations false)) + (when (< (anim/get-val scale) y-threshold-scale) + (utils/center-y animations false)))) + (finalize-pinch dimensions animations state))))) + +(defn pan-x-gesture + [{:keys [width screen-width x-threshold-scale]} + {:keys [scale pan-x-start pan-x pinch-x pinch-x-start]} + {:keys [pan-x-enabled?]} + rescale] + (-> + (gesture/gesture-pan) + (gesture/enabled @pan-x-enabled?) + (gesture/average-touches false) + (gesture/on-update (fn [e] + (anim/set-val pan-x (+ (anim/get-val pan-x-start) (oget e "translationX"))))) + (gesture/on-end + (fn [e] + (let [curr-offset (+ (anim/get-val pan-x) (anim/get-val pinch-x-start)) + max-offset (utils/get-max-offset width screen-width (anim/get-val scale)) + max-offset (if (neg? curr-offset) (- max-offset) max-offset) + velocity (* (oget e "velocityX") c/velocity-factor)] + (cond + (< (anim/get-val scale) x-threshold-scale) + (rescale c/min-scale) + (> (Math/abs curr-offset) (Math/abs max-offset)) + (do + (anim/animate pan-x max-offset) + (anim/set-val pan-x-start max-offset) + (anim/animate pinch-x c/init-offset) + (anim/set-val pinch-x-start c/init-offset)) + :else + (let [lower-bound (- (- (Math/abs max-offset)) (anim/get-val pinch-x-start)) + upper-bound (- (Math/abs max-offset) (anim/get-val pinch-x-start))] + (anim/set-val pan-x-start (anim/get-val pan-x)) + (anim/animate-decay pan-x velocity [lower-bound upper-bound]) + (anim/animate-decay pan-x-start velocity [lower-bound upper-bound])))))))) + + +(defn pan-y-gesture + [{:keys [height screen-height y-threshold-scale]} + {:keys [scale pan-y-start pan-y pinch-y pinch-y-start]} + {:keys [pan-y-enabled?]} + rescale] + (-> + (gesture/gesture-pan) + (gesture/enabled @pan-y-enabled?) + (gesture/average-touches false) + (gesture/on-update (fn [e] + (anim/set-val pan-y (+ (anim/get-val pan-y-start) (oget e "translationY"))))) + (gesture/on-end + (fn [e] + (let [curr-offset (+ (anim/get-val pan-y) (anim/get-val pinch-y-start)) + max-offset (utils/get-max-offset height screen-height (anim/get-val scale)) + max-offset (if (neg? curr-offset) (- max-offset) max-offset) + velocity (* (oget e "velocityY") c/velocity-factor)] + (cond + (< (anim/get-val scale) y-threshold-scale) + (rescale c/min-scale) + (> (Math/abs curr-offset) (Math/abs max-offset)) + (do + (anim/animate pan-y max-offset) + (anim/set-val pan-y-start max-offset) + (anim/animate pinch-y c/init-offset) + (anim/set-val pinch-y-start c/init-offset)) + :else + (let [lower-bound (- (- (Math/abs max-offset)) (anim/get-val pinch-y-start)) + upper-bound (- (Math/abs max-offset) (anim/get-val pinch-y-start))] + (anim/set-val pan-y-start (anim/get-val pan-y)) + (anim/animate-decay pan-y velocity [lower-bound upper-bound]) + (anim/animate-decay pan-y-start velocity [lower-bound upper-bound])))))))) + +(defn- f-zoomable-image + [dimensions animations state rescale curr-orientation image focused? index render-data + image-dimensions-nil?] + (let [{:keys [transparent? set-full-height?]} render-data + portrait? (= curr-orientation orientation/portrait) + last-overlay-opacity (r/atom 0) + on-tap #(utils/toggle-opacity index render-data portrait? last-overlay-opacity) + tap (tap-gesture on-tap) + double-tap (double-tap-gesture dimensions animations rescale transparent? on-tap) + pinch (pinch-gesture dimensions animations state rescale transparent? on-tap) + pan-x (pan-x-gesture dimensions animations state rescale) + pan-y (pan-y-gesture dimensions animations state rescale) + composed-gestures (gesture/exclusive + (gesture/simultaneous pinch pan-x pan-y) + (gesture/exclusive double-tap tap))] + [gesture/gesture-detector {:gesture composed-gestures} + [reanimated/view + {:style (style/container dimensions + animations + (:full-screen-scale render-data) + @set-full-height? + (= curr-orientation orientation/portrait))} + [reanimated/fast-image + (merge + {:source {:uri (url/replace-port image (rf/sub [:mediaserver/port]))} + :native-ID (when focused? :shared-element) + :style (style/image dimensions animations render-data index)} + (when image-dimensions-nil? + {:resize-mode :contain}))]]])) + +(defn zoomable-image + [] + (let [state (utils/init-state)] + (fn [{:keys [image-width image-height image id]} index render-data] + (let [animation-shared-element-id (rf/sub [:animation-shared-element-id]) + exit-lightbox-signal (rf/sub [:lightbox/exit-signal]) + zoom-out-signal (rf/sub [:lightbox/zoom-out-signal]) + {:keys [set-full-height? curr-orientation]} render-data + focused? (= animation-shared-element-id id) + ;; TODO - remove `image-dimensions` check, once + ;; https://github.com/status-im/status-desktop/issues/10944 is fixed + image-dimensions-nil? (not (and image-width image-height)) + dimensions (utils/get-dimensions + (or image-width (:screen-width render-data)) + (or image-height (:screen-height render-data)) + curr-orientation + render-data) + animations (utils/init-animations) + rescale (fn [value exit?] + (utils/rescale-image value + exit? + dimensions + animations + state))] + (rn/use-mount (fn [] + (js/setTimeout (fn [] (reset! set-full-height? true)) 500))) + (when platform/ios? + (utils/handle-orientation-change curr-orientation focused? dimensions animations state) + (utils/handle-exit-lightbox-signal exit-lightbox-signal + index + (anim/get-val (:scale animations)) + rescale + set-full-height?)) + (utils/handle-zoom-out-signal zoom-out-signal index (anim/get-val (:scale animations)) rescale) + [:f> f-zoomable-image dimensions animations state rescale curr-orientation image focused? + index render-data image-dimensions-nil?])))) diff --git a/src/status_im/common/log.cljs b/src/status_im/common/log.cljs new file mode 100644 index 00000000000..c484b957450 --- /dev/null +++ b/src/status_im/common/log.cljs @@ -0,0 +1,114 @@ +(ns status-im.common.log + (:require + [clojure.pprint :as pprint] + [clojure.string :as string] + [re-frame.core :as re-frame] + [taoensso.timbre :as log])) + +(def logs-queue (atom #queue [])) +(def max-log-entries 1000) + +(defn get-logs-queue [] @logs-queue) + +(defn add-log-entry + [entry] + (swap! logs-queue conj entry) + (when (>= (count @logs-queue) max-log-entries) + (swap! logs-queue pop))) + +(defn setup + [level] + (log/merge-config! {:ns-filter {:allow #{"*"} :deny #{"taoensso.sente"}}}) + (if (string/blank? level) + (log/set-min-level! :report) + (do + (log/set-min-level! (-> level + string/lower-case + keyword)) + (log/merge-config! + {:output-fn (fn [& data] + (let [res (apply log/default-output-fn data)] + (add-log-entry res) + res)) + :middleware [(fn [data] + (update data + :vargs + (partial mapv + #(if (string? %) % (with-out-str (pprint/pprint %))))))]})))) + +(re-frame/reg-fx + :logs/set-level + (fn [level] + (setup level))) + +(defn log-error + ([error] + (log/error error)) + ([message error] + (log/error message error)) + ([message context error] + (log/error message context error))) + +(re-frame/reg-event-fx + :log/error + (fn [_ args] + {:fx [[:effects.log/error args]]})) + +(re-frame/reg-fx + :effects.log/error + (fn [args] + (apply log-error args))) + +(defn log-info + ([info] + (log/info info)) + ([message info] + (log/info message info)) + ([message context info] + (log/info message context info))) + +(re-frame/reg-event-fx + :log/info + (fn [_ args] + {:fx [[:effects.log/info args]]})) + +(re-frame/reg-fx + :effects.log/info + (fn [args] + (apply log-info args))) + +(defn log-warn + ([warning] + (log/warn warning)) + ([message warning] + (log/warn message warning)) + ([message context warning] + (log/warn message context warning))) + +(re-frame/reg-event-fx + :log/warn + (fn [_ args] + {:fx [[:effects.log/warn args]]})) + +(re-frame/reg-fx + :effects.log/warn + (fn [args] + (apply log-warn args))) + +(defn log-debug + ([value] + (log/debug value)) + ([message value] + (log/debug message value)) + ([message context value] + (log/debug message context value))) + +(re-frame/reg-event-fx + :log/debug + (fn [_ args] + {:fx [[:effects.log/debug args]]})) + +(re-frame/reg-fx + :effects.log/debug + (fn [args] + (apply log-debug args))) diff --git a/src/status_im/common/mute_drawer/style.cljs b/src/status_im/common/mute_drawer/style.cljs new file mode 100644 index 00000000000..80a28d5d666 --- /dev/null +++ b/src/status_im/common/mute_drawer/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.common.mute-drawer.style + (:require + [quo.foundations.colors :as colors])) + +(defn header-text + [theme] + {:margin-left 20 + :margin-bottom 10 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/status_im/common/mute_drawer/view.cljs b/src/status_im/common/mute_drawer/view.cljs new file mode 100644 index 00000000000..59860ca08ea --- /dev/null +++ b/src/status_im/common/mute_drawer/view.cljs @@ -0,0 +1,103 @@ +(ns status-im.common.mute-drawer.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.mute-drawer.style :as style] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn not-community-chat? + [chat-type] + (contains? #{constants/public-chat-type + constants/private-group-chat-type + constants/one-to-one-chat-type} + chat-type)) + +(defn hide-sheet-and-dispatch + [{:keys [id community? muted? muted-type]}] + (rf/dispatch [:hide-bottom-sheet]) + (if community? + (rf/dispatch [:community/set-muted id muted? muted-type]) + (rf/dispatch [:chat.ui/mute id muted? muted-type]))) + +(defn mute-drawer + [{:keys [id accessibility-label muted? chat-type community?]}] + (let [theme (quo.context/use-theme)] + [rn/view {:accessibility-label accessibility-label} + [quo/text + {:weight :medium + :size :paragraph-2 + :style (style/header-text theme)} + (i18n/label + (if community? + :t/mute-community + (if (not-community-chat? chat-type) + :t/mute-chat-capitialized + :t/mute-channel)))] + [quo/menu-item + {:type :transparent + :title (i18n/label :t/mute-for-15-mins) + :icon-bg-color :transparent + :container-padding-vertical 12 + :title-column-style {:margin-left 2} + :on-press (fn [] + (hide-sheet-and-dispatch + {:id id + :chat-type chat-type + :muted? muted? + :community? community? + :muted-type constants/mute-for-15-mins-type}))}] + [quo/menu-item + {:type :transparent + :title (i18n/label :t/mute-for-1-hour) + :icon-bg-color :transparent + :container-padding-vertical 12 + :title-column-style {:margin-left 2} + :on-press (fn [] + (hide-sheet-and-dispatch + {:id id + :chat-type chat-type + :muted? muted? + :community? community? + :muted-type constants/mute-for-1-hour-type}))}] + [quo/menu-item + {:type :transparent + :title (i18n/label :t/mute-for-8-hours) + :icon-bg-color :transparent + :container-padding-vertical 12 + :title-column-style {:margin-left 2} + :on-press (fn [] + (hide-sheet-and-dispatch + {:id id + :chat-type chat-type + :muted? muted? + :community? community? + :muted-type constants/mute-for-8-hours-type}))}] + [quo/menu-item + {:type :transparent + :title (i18n/label :t/mute-for-1-week) + :icon-bg-color :transparent + :container-padding-vertical 12 + :title-column-style {:margin-left 2} + :on-press (fn [] + (hide-sheet-and-dispatch + {:id id + :chat-type chat-type + :muted? muted? + :community? community? + :muted-type constants/mute-for-1-week}))}] + [quo/menu-item + {:type :transparent + :title (i18n/label :t/mute-till-unmute) + :icon-bg-color :transparent + :container-padding-vertical 12 + :title-column-style {:margin-left 2} + :on-press (fn [] + (hide-sheet-and-dispatch + {:id id + :chat-type chat-type + :muted? muted? + :community? community? + :muted-type constants/mute-till-unmuted}))}]])) diff --git a/src/status_im/common/muting/helpers.cljs b/src/status_im/common/muting/helpers.cljs new file mode 100644 index 00000000000..5e073effee8 --- /dev/null +++ b/src/status_im/common/muting/helpers.cljs @@ -0,0 +1,39 @@ +(ns status-im.common.muting.helpers + (:require + [cljs-time.core :as t] + [utils.datetime :refer + [go-default-time int->weekday months time-zone-offset today? tomorrow?]] + [utils.i18n :as i18n]) + (:require [cljs-time.format :as t.format])) + +(defn- add-leading-zero + [input-string] + (if (> 10 input-string) + (str "0" input-string) + input-string)) + +(defn format-mute-till + [muted-till-string] + (let [parsed-time (t.format/parse (t.format/formatters :date-time-no-ms) muted-till-string) + hours-and-minutes (str (add-leading-zero (t/hour (t/plus parsed-time time-zone-offset))) + ":" + (add-leading-zero (t/minute (t/plus parsed-time time-zone-offset)))) + when-to-unmute (cond (= go-default-time + muted-till-string) (i18n/label :t/until-you-turn-it-back-on) + (today? parsed-time) (str hours-and-minutes " today") + (tomorrow? parsed-time) (str hours-and-minutes " tomorrow") + :else (str hours-and-minutes + " " + (i18n/label + (keyword "t" + (get int->weekday + (t/day-of-week + parsed-time)))) + " " + (t/day parsed-time) + " " + (i18n/label + (keyword "t" + (get months + (t/month parsed-time))))))] + when-to-unmute)) diff --git a/src/status_im/common/muting/helpers_test.cljs b/src/status_im/common/muting/helpers_test.cljs new file mode 100644 index 00000000000..3f85850990b --- /dev/null +++ b/src/status_im/common/muting/helpers_test.cljs @@ -0,0 +1,84 @@ +(ns status-im.common.muting.helpers-test + (:require + [cljs-time.coerce :as time-coerce] + [cljs-time.core :as t] + [cljs-time.format :as t.format] + [cljs.test :refer-macros [deftest testing is are]] + [clojure.string :as string] + [status-im.common.muting.helpers :as muting] + [utils.datetime :as datetime])) + +(def mock-current-time-epoch 1655731506000) + +(deftest format-mute-till-test + (with-redefs [t/*ms-fn* (constantly mock-current-time-epoch)] + (let [remove-msecs #(string/replace % #"\.\w*Z" "Z") + time-str-to-obj #(t.format/parse (remove-msecs (time-coerce/to-string %))) + curr-time (t/now) + custom-HH-MM-formatter (t.format/formatter "HH:mm") + custom-DD-formatter (t.format/formatter "DD") + get-hh-mm #(t.format/unparse custom-HH-MM-formatter %) + get-day #(t.format/unparse custom-DD-formatter %) + get-week-day #(->> % + t/day-of-week + (get datetime/int->weekday)) + mock-today (t.format/unparse (t.format/formatters :date-time-no-ms) curr-time) + in-n-days #(-> (time-str-to-obj mock-today) + (t/plus (t/days %))) + in-n-minutes #(-> (time-str-to-obj mock-today) + (t/plus (t/minutes %))) + in-n-hours #(-> (time-str-to-obj mock-today) + (t/plus (t/hours %))) + mock-tomorrow (in-n-days 1) + mock-in-two-days (in-n-days 2) + mock-in-three-days (in-n-days 3) + mock-in-four-days (in-n-days 4) + mock-in-five-days (in-n-days 5) + mock-in-six-days (in-n-days 6) + mock-in-15-minutes (in-n-minutes 15) + mock-in-1-hour (in-n-hours 1) + mock-in-8-hour (in-n-hours 8) + get-month-day-int #(int (get-day %)) + today? (fn [mocked curr-time] + (= + (t.format/unparse (t.format/formatter "MM:DD") mocked) + (t.format/unparse (t.format/formatter "MM:DD") curr-time))) + tomorrow? (fn [mocked curr-time] + (some #(= % + (- + (int (get-month-day-int mocked)) + (int (get-month-day-int curr-time)))) + [1 30 29 27])) + form-full-date #(str (get-hh-mm %) + " " (string/capitalize (get-week-day %)) + " " (get-month-day-int %) + " " (string/capitalize (get datetime/months (t/month %)))) + today-date #(str (get-hh-mm %) " today") + tomorrow-date #(str (get-hh-mm %) " tomorrow") + write-date #(cond (today? % curr-time) (today-date %) + (tomorrow? % curr-time) (tomorrow-date %) + :else (form-full-date %)) + will-unmute-in-1-hour (remove-msecs (time-coerce/to-string mock-in-1-hour)) + will-unmute-in-8-hours (remove-msecs (time-coerce/to-string mock-in-8-hour)) + will-unmute-in-15-mins (remove-msecs (time-coerce/to-string mock-in-15-minutes)) + will-unmute-in-two-days (remove-msecs (time-coerce/to-string mock-in-two-days)) + will-unmute-tomorrow (remove-msecs (time-coerce/to-string mock-tomorrow)) + will-unmute-in-three-days (remove-msecs (time-coerce/to-string mock-in-three-days)) + will-unmute-in-four-days (remove-msecs (time-coerce/to-string mock-in-four-days)) + will-unmute-in-five-days (remove-msecs (time-coerce/to-string mock-in-five-days)) + will-unmute-in-six-days (remove-msecs (time-coerce/to-string mock-in-six-days))] + (testing "Mute for minutes and hours" + (are [arg expected] (= (muting/format-mute-till arg) expected) + will-unmute-in-15-mins (write-date mock-in-15-minutes) + will-unmute-in-1-hour (write-date mock-in-1-hour) + will-unmute-in-8-hours (write-date mock-in-8-hour))) + (testing "Weekdays" + (are [arg expected] (= (muting/format-mute-till arg) expected) + will-unmute-tomorrow (write-date mock-tomorrow) + will-unmute-in-two-days (write-date mock-in-two-days) + will-unmute-in-three-days (write-date mock-in-three-days) + will-unmute-in-four-days (write-date mock-in-four-days) + will-unmute-in-five-days (write-date mock-in-five-days) + will-unmute-in-six-days (write-date mock-in-six-days))) + (testing "Until the user turns it back on" + (is (= "you turn it back on" (muting/format-mute-till datetime/go-default-time))))))) diff --git a/src/status_im/common/new_device_sheet/style.cljs b/src/status_im/common/new_device_sheet/style.cljs new file mode 100644 index 00000000000..1ccdca33ce8 --- /dev/null +++ b/src/status_im/common/new_device_sheet/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.common.new-device-sheet.style) + +(def heading + {:padding-left 20 + :padding-bottom 8}) + +(def message + {:padding-horizontal 20 + :padding-top 4}) diff --git a/src/status_im/common/new_device_sheet/view.cljs b/src/status_im/common/new_device_sheet/view.cljs new file mode 100644 index 00000000000..b10e03e6c7b --- /dev/null +++ b/src/status_im/common/new_device_sheet/view.cljs @@ -0,0 +1,75 @@ +(ns status-im.common.new-device-sheet.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.new-device-sheet.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- pair-and-sync + [installation-id] + (rf/dispatch [:pairing/pair-and-sync installation-id]) + (events-helper/hide-bottom-sheet)) + +(defn installation-request-receiver-view + [installation-id] + (rn/use-mount events-helper/dismiss-keyboard) + [:<> + [quo/text + {:weight :semi-bold + :size :heading-2 + :accessibility-label :new-device-sheet-heading + :style style/heading} + (i18n/label :t/pair-new-device-and-sync)] + [quo/text + {:weight :regular + :size :paragraph-1 + :accessibility-label :new-device-sheet-message + :style style/message} + (i18n/label :t/new-device-detected-recovered-device-message)] + [quo/text + {:weight :semi-bold + :size :heading-2 + :accessibility-label :new-device-installation-id + :style style/heading} + installation-id] + [quo/bottom-actions + {:actions :two-actions + :blur? true + :container-style {:margin-top 12} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press events-helper/hide-bottom-sheet} + :button-one-label (i18n/label :t/pair-and-sync) + :button-one-props {:on-press #(pair-and-sync installation-id)}}]]) + +(defn installation-request-creator-view + [installation-id] + (rn/use-mount events-helper/dismiss-keyboard) + [:<> + [quo/text + {:weight :semi-bold + :size :heading-2 + :accessibility-label :new-device-sheet-heading + :style style/heading} + (i18n/label :t/pair-this-device-and-sync)] + [quo/text + {:weight :regular + :size :paragraph-1 + :accessibility-label :new-device-sheet-message + :style style/message} + (i18n/label :t/new-device-detected-other-device-message)] + [quo/text + {:weight :semi-bold + :size :heading-2 + :accessibility-label :new-device-installation-id + :style style/heading} + installation-id] + [quo/bottom-actions + {:actions :one-action + :blur? true + :container-style {:margin-top 12} + :button-one-label (i18n/label :t/close) + :button-one-props {:type :grey + :on-press events-helper/hide-bottom-sheet}}]]) diff --git a/src/status_im/common/not_implemented.cljs b/src/status_im/common/not_implemented.cljs new file mode 100644 index 00000000000..2977d2c1e45 --- /dev/null +++ b/src/status_im/common/not_implemented.cljs @@ -0,0 +1,13 @@ +(ns status-im.common.not-implemented + (:require + [react-native.core :as rn])) + +(defn not-implemented + [content] + (when content + [rn/view {:border-color :red :border-width 1} + content])) + +(defn alert + [] + (js/alert "Feature not implemented.")) diff --git a/src/status_im/common/pairing/events.cljs b/src/status_im/common/pairing/events.cljs new file mode 100644 index 00000000000..e208cedce08 --- /dev/null +++ b/src/status_im/common/pairing/events.cljs @@ -0,0 +1,73 @@ +(ns status-im.common.pairing.events + (:require + [status-im.constants :as constants] + [status-im.contexts.communities.discover.events] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/defn handle-local-pairing-signals + [{:keys [db]} {:keys [type action data error] :as event}] + (log/info "local pairing signal received" + {:event event}) + (let [{:keys [account password chatKey]} data + role (get-in db [:syncing :role]) + receiver? (= role constants/local-pairing-role-receiver) + sender? (= role constants/local-pairing-role-sender) + connection-success? (and (= type + constants/local-pairing-event-connection-success) + (= action + constants/local-pairing-action-connect)) + connection-error? (= type + constants/local-pairing-event-connection-error) + error-on-pairing? (contains? constants/local-pairing-event-errors type) + completed-pairing? (and (= type + constants/local-pairing-event-transfer-success) + (= action + constants/local-pairing-action-pairing-installation)) + received-account? (and (= type + constants/local-pairing-event-received-account) + (= action + constants/local-pairing-action-pairing-account) + (some? account) + (some? password)) + multiaccount-data (when received-account? + (merge account + {:password password + :whisper-private-key chatKey})) + navigate-to-syncing-devices? (and (or connection-success? connection-error?) receiver?) + user-in-syncing-devices-screen? (or (= (:view-id db) :screen/onboarding.syncing-progress) + (= (:view-id db) :screen/profile.profiles) + (= (:view-id db) + :screen/onboarding.syncing-progress-intro)) + user-in-sign-in-intro-screen? (= (:view-id db) :screen/onboarding.sign-in-intro) + keystore-files-transfer-action? (= action + constants/local-pairing-action-keystore-files-transfer)] + (merge {:db (cond-> db + connection-success? + (assoc-in [:syncing :pairing-status] :connected) + + received-account? + (assoc-in [:syncing :profile] multiaccount-data) + + error-on-pairing? + (assoc-in [:syncing :pairing-status] :error) + + completed-pairing? + (assoc-in [:syncing :pairing-status] :completed))} + (cond + (and navigate-to-syncing-devices? (not user-in-syncing-devices-screen?)) + {:dispatch (if user-in-sign-in-intro-screen? + [:navigate-to-within-stack + [:screen/onboarding.syncing-progress-intro :screen/onboarding.sign-in-intro]] + [:open-modal :screen/onboarding.syncing-progress])} + + (and completed-pairing? sender?) + {:dispatch [:syncing/clear-states]} + + (and completed-pairing? receiver?) + {:dispatch [:profile.login/local-paired-user]} + + (and error-on-pairing? (some? error) (not keystore-files-transfer-action?)) + {:dispatch [:toasts/upsert + {:type :negative + :text error}]})))) diff --git a/src/status_im/common/password_with_hint/style.cljs b/src/status_im/common/password_with_hint/style.cljs new file mode 100644 index 00000000000..dea02271705 --- /dev/null +++ b/src/status_im/common/password_with_hint/style.cljs @@ -0,0 +1,4 @@ +(ns status-im.common.password-with-hint.style) + +(def info-message + {:margin-top 8}) diff --git a/src/status_im/common/password_with_hint/view.cljs b/src/status_im/common/password_with_hint/view.cljs new file mode 100644 index 00000000000..1308d0e5591 --- /dev/null +++ b/src/status_im/common/password_with_hint/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.common.password-with-hint.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.password-with-hint.style :as style])) + +(defn view + [{{:keys [text status shown?]} :hint :as input-props}] + [:<> + [quo/input + (-> input-props + (dissoc :hint) + (assoc :type :password + :blur? true))] + [rn/view {:style style/info-message} + (when shown? + [quo/info-message + (cond-> {:status status + :size :default} + (not= :success status) (assoc :icon :i/info) + (= :success status) (assoc :icon :i/check-circle) + (= :default status) (assoc :color colors/white-70-blur)) + text])]]) diff --git a/src/status_im/common/pdf_viewer/view.cljs b/src/status_im/common/pdf_viewer/view.cljs new file mode 100644 index 00000000000..9796b29ed9d --- /dev/null +++ b/src/status_im/common/pdf_viewer/view.cljs @@ -0,0 +1,20 @@ +(ns status-im.common.pdf-viewer.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.pdf-viewer :as pdf-viewer] + [status-im.common.events-helper :as events-helper])) + +(defn view + [] + (let [{:keys [uri pdf-viewer-props]} (quo.context/use-screen-params)] + [rn/view {:style {:flex 1}} + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back + :accessibility-label :pdf-viewer-nav}] + [pdf-viewer/view + (merge {:source {:uri uri} + :style {:flex 1}} + pdf-viewer-props)]])) diff --git a/src/status_im/common/peer_stats/events.cljs b/src/status_im/common/peer_stats/events.cljs new file mode 100644 index 00000000000..c726bae7bc3 --- /dev/null +++ b/src/status_im/common/peer_stats/events.cljs @@ -0,0 +1,17 @@ +(ns status-im.common.peer-stats.events + (:require + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :peer-stats/get-count + (fn [] + {:fx [[:json-rpc/call + [{:method :wakuext_peers + :on-success [:peer-stats/get-count-success] + :on-error (fn [error] + (log/error "failed to fetch wakuv2 peer count" + {:error error}))}]]]})) + +(rf/reg-event-fx :peer-stats/get-count-success + (fn [{:keys [db]} [response]] + {:db (assoc db :peer-stats/count (count response))})) diff --git a/src/status_im/common/pixel_ratio.cljs b/src/status_im/common/pixel_ratio.cljs new file mode 100644 index 00000000000..6670e1d7f84 --- /dev/null +++ b/src/status_im/common/pixel_ratio.cljs @@ -0,0 +1,5 @@ +(ns status-im.common.pixel-ratio + (:require + ["react-native" :as rn])) + +(def ratio (rn/PixelRatio.get)) diff --git a/src/status_im/common/plus_button/view.cljs b/src/status_im/common/plus_button/view.cljs new file mode 100644 index 00000000000..b09f0308107 --- /dev/null +++ b/src/status_im/common/plus_button/view.cljs @@ -0,0 +1,14 @@ +(ns status-im.common.plus-button.view + (:require + [quo.core :as quo])) + +(defn plus-button + [{:keys [on-press accessibility-label customization-color]}] + [quo/button + {:type :primary + :size 32 + :icon-only? true + :accessibility-label (or accessibility-label :plus-button) + :on-press on-press + :customization-color customization-color} + :i/add]) diff --git a/src/status_im/common/privacy/view.cljs b/src/status_im/common/privacy/view.cljs new file mode 100644 index 00000000000..e181e2317e5 --- /dev/null +++ b/src/status_im/common/privacy/view.cljs @@ -0,0 +1,20 @@ +(ns status-im.common.privacy.view + (:require-macros [legacy.status-im.utils.slurp :refer [slurp]]) + (:require [quo.core :as quo] + [react-native.gesture :as gesture] + [status-im.contexts.settings.common.header :as header] + [utils.i18n :as i18n])) + +(def privacy-statement-text (slurp "resources/privacy.mdwn")) + +(defn privacy-statement + [] + [gesture/scroll-view {:style {:margin 20}} + [quo/text privacy-statement-text]]) + +(defn view + [] + [quo/overlay {:type :shell} + [header/view {:title (i18n/label :t/privacy-policy)}] + [gesture/scroll-view {:style {:padding-horizontal 20}} + [quo/text privacy-statement-text]]]) diff --git a/src/status_im/common/privacy_mode/events.cljs b/src/status_im/common/privacy_mode/events.cljs new file mode 100644 index 00000000000..a76e21523ef --- /dev/null +++ b/src/status_im/common/privacy_mode/events.cljs @@ -0,0 +1,16 @@ +(ns status-im.common.privacy-mode.events + (:require [status-im.common.privacy-mode.view :as privacy-mode] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :privacy-mode/toggle-privacy-mode + (fn [{:keys [db]} [privacy-mode-enabled?]] + {:db (assoc db :privacy-mode/privacy-mode-enabled? (not privacy-mode-enabled?)) + :fx [[:dispatch [:hide-bottom-sheet]]]})) + +(rf/reg-event-fx :privacy-mode/show-bottom-sheet + (fn [{:keys [db]} [{:keys [theme shell?]}]] + {:fx [[:dispatch + [:show-bottom-sheet + {:content #(privacy-mode/view (:privacy-mode/privacy-mode-enabled? db)) + :theme theme + :shell? shell?}]]]})) diff --git a/src/status_im/common/privacy_mode/view.cljs b/src/status_im/common/privacy_mode/view.cljs new file mode 100644 index 00000000000..a952ca857c6 --- /dev/null +++ b/src/status_im/common/privacy_mode/view.cljs @@ -0,0 +1,57 @@ +(ns status-im.common.privacy-mode.view + (:require [quo.core :as quo] + [status-im.common.events-helper :as events-helper] + [status-im.common.not-implemented :as not-implemented] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [privacy-mode-enabled?] + [:<> + [quo/drawer-top + {:title (i18n/label :t/privacy-mode)}] + [quo/text {:style {:margin-vertical 4 :margin-horizontal 20}} + (i18n/label :t/privacy-mode-message-1)] + [quo/text + {:style {:margin-horizontal 20 :margin-top 16 :margin-bottom 4} + :weight :semi-bold + :size :paragraph-1} + (i18n/label :t/feature-that-will-be-unavailable)] + [quo/status-list-item + {:status :negative :title (i18n/label :t/feature-that-will-be-unavailable-item-1)}] + [quo/status-list-item + {:status :negative :title (i18n/label :t/feature-that-will-be-unavailable-item-2)}] + [quo/status-list-item + {:status :negative :title (i18n/label :t/feature-that-will-be-unavailable-item-3)}] + [quo/status-list-item + {:status :negative :title (i18n/label :t/feature-that-will-be-unavailable-item-4)}] + [quo/status-list-item + {:status :negative :title (i18n/label :t/feature-that-will-be-unavailable-item-5)}] + [quo/text + {:style {:margin-horizontal 20 :margin-top 16 :margin-bottom 4} + :weight :semi-bold + :size :paragraph-1} + (i18n/label :t/you-may-also-experience)] + [quo/status-list-item {:status :negative :title (i18n/label :t/you-may-also-experience-item-1)}] + [quo/status-list-item {:status :negative :title (i18n/label :t/you-may-also-experience-item-2)}] + [quo/text {:style {:margin-top 16 :margin-bottom 8 :margin-horizontal 20}} + (i18n/label :t/privacy-mode-message-2)] + [quo/button + {:type :outline + :size 24 + :icon-right :i/external + :container-style {:margin-top 8 :margin-bottom 12 :align-self :flex-start :margin-horizontal 20} + :on-press not-implemented/alert} + (i18n/label :t/privacy-mode-more-details)] + [quo/bottom-actions + {:actions :two-actions + :blur? true + :button-one-label (i18n/label :t/close) + :button-one-props {:on-press events-helper/hide-bottom-sheet + :type :grey} + :button-two-label (if privacy-mode-enabled? + (i18n/label :t/disable-privacy-mode) + (i18n/label :t/enable-privacy-mode)) + :button-two-props {:on-press #(rf/dispatch [:privacy-mode/toggle-privacy-mode + privacy-mode-enabled?]) + :customization-color :purple}}]]) diff --git a/src/status_im/common/qr_codes/view.cljs b/src/status_im/common/qr_codes/view.cljs new file mode 100644 index 00000000000..3d10dd457f9 --- /dev/null +++ b/src/status_im/common/qr_codes/view.cljs @@ -0,0 +1,73 @@ +(ns status-im.common.qr-codes.view + (:require + [quo.core :as quo] + [utils.image-server :as image-server] + [utils.re-frame :as rf])) + +(defn qr-code + "Receives a URL to show a QR code with an avatar (optional) over it. + Parameters: + - url: String to transform to QR + - size: if not provided, the QR code image will grow according to its parent + - avatar: Type of the avatar, defaults to `:none` and it can be: + `:profile`, `:wallet-account`, `:community`, `:channel` or `:saved-address` + + Depending on the type selected, different properties are accepted: + `:profile`: + - profile-picture + - full-name + - customization-color + `:wallet-account` + - emoji + - customization-color + `:community` + - picture + `:channel` + - emoji + - customization-color + `:saved-address` + - f-name + - l-name + - customization-color" + [{:keys [url size] :as props}] + (let [qr-media-server-uri (image-server/get-qr-image-uri-for-any-url + {:url url + :port (rf/sub [:mediaserver/port]) + :qr-size (or (and size (int size)) 400) + :error-level :highest})] + [quo/qr-code + (assoc props + :qr-image-uri + qr-media-server-uri)])) + +(defn share-qr-code + "Receives the following properties: + - type: :profile | :wallet | :saved-address | :watched-address + - qr-image-uri: Image source value. + - qr-data: Text to show below the QR code. + - on-text-press: Callback for the `qr-data` text. + - on-text-long-press: Callback for the `qr-data` text. + - on-share-press: Callback for the share button. + - customization-color: Custom color for the QR code component. + - unblur-on-android?: [Android only] disables blur for this component. + - full-name: User full name. + + Depending on the `type`, different properties are accepted: + `:profile` + - profile-picture: map ({:source image-source}) or any image source. + `:wallet` + - emoji: Emoji in a string to show in the QR code. + `:watched-address` + - emoji: Emoji in a string to show in the QR code. " + [{:keys [qr-data qr-data-label-shown] + :as props}] + (let [label (or qr-data-label-shown qr-data) + qr-media-server-uri (image-server/get-qr-image-uri-for-any-url + {:url qr-data + :port (rf/sub [:mediaserver/port]) + :qr-size 600 + :error-level :highest})] + [quo/share-qr-code + (assoc props + :qr-data label + :qr-image-uri qr-media-server-uri)])) diff --git a/src/status_im/common/raw_data_block/style.cljs b/src/status_im/common/raw_data_block/style.cljs new file mode 100644 index 00000000000..bd0c94dcb6a --- /dev/null +++ b/src/status_im/common/raw_data_block/style.cljs @@ -0,0 +1,14 @@ +(ns status-im.common.raw-data-block.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:flex 1 + :padding 10 + :margin-top 10.5 + :margin-bottom 0 + :border-width 1 + :border-color colors/neutral-10 + :border-radius 16}) + +(def content + {:padding-bottom 20}) diff --git a/src/status_im/common/raw_data_block/view.cljs b/src/status_im/common/raw_data_block/view.cljs new file mode 100644 index 00000000000..fcf695fc17b --- /dev/null +++ b/src/status_im/common/raw_data_block/view.cljs @@ -0,0 +1,14 @@ +(ns status-im.common.raw-data-block.view + (:require [quo.core :as quo] + [react-native.gesture :as gesture] + [status-im.common.raw-data-block.style :as style])) + +(defn view + [data] + [gesture/scroll-view + {:style style/container} + [quo/text + {:size :paragraph-2 + :style style/content + :weight :code} + data]]) diff --git a/src/status_im/common/refreshable_flat_list/view.cljs b/src/status_im/common/refreshable_flat_list/view.cljs new file mode 100644 index 00000000000..2f82f79f6cd --- /dev/null +++ b/src/status_im/common/refreshable_flat_list/view.cljs @@ -0,0 +1,10 @@ +(ns status-im.common.refreshable-flat-list.view + (:require [react-native.core :as rn] + [reagent.core :as reagent])) + +(defn view + [{:keys [refresh-control] :as props}] + [rn/flat-list + (merge {:refresh-control (reagent/as-element + refresh-control)} + (dissoc props :refresh-control))]) diff --git a/src/status_im/common/resources.cljs b/src/status_im/common/resources.cljs new file mode 100644 index 00000000000..137d1b6ae9b --- /dev/null +++ b/src/status_im/common/resources.cljs @@ -0,0 +1,172 @@ +(ns status-im.common.resources) + +(def ui + {:add-new-contact (js/require "../resources/images/ui2/add-contact.png") + :biometrics (js/require "../resources/images/ui2/biometrics.png") + :usage-data (js/require "../resources/images/ui2/usage-data.png") + :login-syncing (js/require "../resources/images/ui2/login-syncing.png") + :chat-privately-with-friends (js/require "../resources/images/ui2/chat-privately-with-friends.png") + :desktop-how-to-pair-sign-in (js/require "../resources/images/ui2/desktop-how-to-pair-sign-in.png") + :desktop-how-to-pair-logged-in (js/require + "../resources/images/ui2/desktop-how-to-pair-logged-in.png") + :mobile-how-to-pair-sign-in (js/require "../resources/images/ui2/mobile-how-to-pair-sign-in.png") + :mobile-how-to-pair-logged-in (js/require "../resources/images/ui2/mobile-how-to-pair-logged-in.png") + :find-sync-code-desktop (js/require "../resources/images/ui2/find-sync-code-desktop.png") + :find-sync-code-mobile (js/require "../resources/images/ui2/find-sync-code-mobile.png") + :lifestyle (js/require "../resources/images/ui2/lifestyle.png") + :music (js/require "../resources/images/ui2/music.png") + :podcasts (js/require "../resources/images/ui2/podcasts.png") + :generate-keys (js/require "../resources/images/ui2/generate-keys.png") + :add-key-to-keycard (js/require "../resources/images/ui2/add-key-to-keycard.png") + :import-key-to-keycard (js/require "../resources/images/ui2/import-key-to-keycard.png") + :ethereum-address (js/require "../resources/images/ui2/ethereum-address.png") + :use-keycard (js/require "../resources/images/ui2/keycard.png") + :use-recovery-phrase (js/require "../resources/images/ui2/recovery-phrase.png") + :check-your-keycard (js/require "../resources/images/ui2/keycard-check.png") + :qr-code (js/require "../resources/images/ui2/qr-code.png") + :keycard-logo (js/require "../resources/images/ui2/keycard-logo.png") + :keycard-buy (js/require "../resources/images/ui2/keycard-buy.png") + :keycard-backup-positive (js/require "../resources/images/ui2/keycard-backup-positive.png") + :keycard-change-pin (js/require "../resources/images/ui2/keycard-change-pin.png") + :keycard-change-pin-positive (js/require "../resources/images/ui2/keycard-change-pin-positive.png") + :keycard-change-pin-negative (js/require "../resources/images/ui2/keycard-change-pin-negative.png") + :keycard-migration (js/require "../resources/images/ui2/keycard-migration.png") + :keycard-migration-failed (js/require "../resources/images/ui2/keycard-migration-failed.png") + :keycard-migration-succeeded (js/require "../resources/images/ui2/keycard-migration-succeeded.png") + :keycard-not-same (js/require "../resources/images/ui2/keycard-not-same.png") + :keycard-not-empty (js/require "../resources/images/ui2/keycard-not-empty.png") + :keycard-profile-already-added (js/require + "../resources/images/ui2/keycard-profile-already-added.png") + :keycard-reset-positive (js/require "../resources/images/ui2/keycard-reset-positive.png") + :keycard-reset-negative (js/require "../resources/images/ui2/keycard-reset-negative.png") + :keycard-unblock (js/require "../resources/images/ui2/keycard-unblock.png") + :keycard-unblock-positive (js/require "../resources/images/ui2/keycard-unblock-positive.png") + :not-keycard (js/require "../resources/images/ui2/not-keycard.png") + :discover (js/require "../resources/images/ui2/discover.png") + :invite-friends (js/require "../resources/images/ui2/invite-friends.png") + :transaction-progress (js/require "../resources/images/ui2/transaction-progress.png") + :welcome-illustration (js/require "../resources/images/ui2/welcome_illustration.png") + :notifications (js/require "../resources/images/ui2/notifications.png") + :nfc-prompt (js/require "../resources/images/ui2/nfc-prompt.png") + :nfc-prompt-android (js/require "../resources/images/ui2/nfc-prompt-android.png") + :nfc-success (js/require "../resources/images/ui2/nfc-success.png") + :nfc-fail (js/require "../resources/images/ui2/nfc-fail.png") + :own-buy-and-swap-your-crypto (js/require "../resources/images/ui2/own-buy-and-swap-your-crypto.png") + :preparing-status (js/require "../resources/images/ui2/preparing-status.png") + :store-your-assets-on-keycard (js/require "../resources/images/ui2/store-your-assets-on-keycard.png") + :syncing-devices (js/require "../resources/images/ui2/syncing_devices.png") + :syncing-wrong (js/require "../resources/images/ui2/syncing_wrong.png") + :buy (js/require "../resources/images/ui2/buy.png") + :receive (js/require "../resources/images/ui2/receive.png")}) + +(def ui-themed + {:angry-man + {:light (js/require "../resources/images/ui2/angry-man-light.png") + :dark (js/require "../resources/images/ui2/angry-man-dark.png")} + :cat-in-box + {:light (js/require "../resources/images/ui2/cat-in-box-light.png") + :dark (js/require "../resources/images/ui2/cat-in-box-dark.png")} + :no-contacts + {:light (js/require "../resources/images/ui2/no-contacts-light.png") + :dark (js/require "../resources/images/ui2/no-contacts-dark.png")} + :no-contacts-to-chat + {:light (js/require "../resources/images/ui2/no-contacts-to-chat-light.png") + :dark (js/require "../resources/images/ui2/no-contacts-to-chat-dark.png")} + :keycard-chip + {:light (js/require "../resources/images/ui2/keycard-chip-light.png") + :dark (js/require "../resources/images/ui2/keycard-chip-dark.png")} + :no-group-chats + {:light (js/require "../resources/images/ui2/no-group-chats-light.png") + :dark (js/require "../resources/images/ui2/no-group-chats-dark.png")} + :no-sent-requests + {:light (js/require "../resources/images/ui2/no-sent-requests-light.png") + :dark (js/require "../resources/images/ui2/no-sent-requests-dark.png")} + :no-received-requests + {:light (js/require "../resources/images/ui2/no-received-requests-light.png") + :dark (js/require "../resources/images/ui2/no-received-requests-dark.png")} + :no-communities + {:light (js/require "../resources/images/ui2/no-communities-light.png") + :dark (js/require "../resources/images/ui2/no-communities-dark.png")} + :no-pending-communities + {:light (js/require "../resources/images/ui2/no-pending-communities-light.png") + :dark (js/require "../resources/images/ui2/no-pending-communities-dark.png")} + :no-opened-communities + {:light (js/require "../resources/images/ui2/no-opened-communities-light.png") + :dark (js/require "../resources/images/ui2/no-opened-communities-dark.png")} + :no-contacts-to-invite + {:light (js/require "../resources/images/ui2/no-contacts-to-invite-light.png") + :dark (js/require "../resources/images/ui2/no-contacts-to-invite-dark.png")} + :no-notifications + {:light (js/require "../resources/images/ui2/no-notifications-light.png") + :dark (js/require "../resources/images/ui2/no-notifications-dark.png")} + :no-activity + {:light (js/require "../resources/images/ui2/no-activity-light.png") + :dark (js/require "../resources/images/ui2/no-activity-dark.png")} + :no-collectibles + {:light (js/require "../resources/images/ui2/no-collectibles-light.png") + :dark (js/require "../resources/images/ui2/no-collectibles-dark.png")} + :no-dapps + {:light (js/require "../resources/images/ui2/no-dapps-light.png") + :dark (js/require "../resources/images/ui2/no-dapps-dark.png")} + :no-permissions + {:light (js/require "../resources/images/ui2/no-permissions-light.png") + :dark (js/require "../resources/images/ui2/no-permissions-dark.png")} + :sweating-man + {:light (js/require "../resources/images/ui2/sweating-man-light.png") + :dark (js/require "../resources/images/ui2/sweating-man-dark.png")} + :no-pinned-messages + {:light (js/require "../resources/images/ui2/no-pinned-messages-light.png") + :dark (js/require "../resources/images/ui2/no-pinned-messages-dark.png")} + :no-assets + {:light (js/require "../resources/images/ui2/no-assets-light.png") + :dark (js/require "../resources/images/ui2/no-assets-dark.png")}}) + +(def mock-images + {:bored-ape (js/require "../resources/images/mock2/bored-ape.png") + :coinbase (js/require "../resources/images/mock2/coinbase.png") + :collectible (js/require "../resources/images/mock2/collectible.png") + :collectible-monkey (js/require "../resources/images/mock2/collectible-monkey.png") + :collectible1 (js/require "../resources/images/mock2/collectible1.png") + :collectible2 (js/require "../resources/images/mock2/collectible2.png") + :collectible3 (js/require "../resources/images/mock2/collectible3.png") + :collectible4 (js/require "../resources/images/mock2/collectible4.png") + :collectible5 (js/require "../resources/images/mock2/collectible5.png") + :collectible6 (js/require "../resources/images/mock2/collectible6.png") + :contact (js/require "../resources/images/mock2/contact.png") + :community-banner (js/require "../resources/images/mock2/community-banner.png") + :community-logo (js/require "../resources/images/mock2/community-logo.png") + :community-cover (js/require "../resources/images/mock2/community-cover.png") + :dark-blur-bg (js/require "../resources/images/mock2/dark_blur_bg.png") + :dark-blur-background (js/require "../resources/images/mock2/dark-blur-background.png") + :decentraland (js/require "../resources/images/mock2/decentraland.png") + :diamond (js/require "../resources/images/mock2/diamond.png") + :gif (js/require "../resources/images/mock2/gif.png") + :monkey (js/require "../resources/images/mock2/monkey.png") + :light-blur-background (js/require "../resources/images/mock2/light-blur-background.png") + :photo1 (js/require "../resources/images/mock2/photo1.png") + :photo2 (js/require "../resources/images/mock2/photo2.png") + :photo3 (js/require "../resources/images/mock2/photo3.png") + :pinterest (js/require "../resources/images/mock2/pinterest.png") + :qr-code (js/require "../resources/images/mock2/qr-code.png") + :rarible (js/require "../resources/images/mock2/rarible.png") + :small-opt-card-icon (js/require "../resources/images/mock2/small_opt_card_icon.png") + :small-opt-card-main (js/require "../resources/images/mock2/small_opt_card_main.png") + :status-logo (js/require "../resources/images/mock2/status-logo.png") + :sticker (js/require "../resources/images/mock2/sticker.png") + :ring (js/require "../resources/images/mock2/ring.png") + :verified (js/require "../resources/images/mock2/verified.png") + :user-picture-female2 (js/require "../resources/images/mock2/user_picture_female2.png") + :user-picture-male4 (js/require "../resources/images/mock2/user_picture_male4.png") + :user-picture-male5 (js/require "../resources/images/mock2/user_picture_male5.png")}) + +(defn get-mock-image + [k] + (get mock-images k)) + +(defn get-image + [k] + (get ui k)) + +(defn get-themed-image + [k theme] + (get-in ui-themed [k theme])) diff --git a/src/status_im/common/router.cljs b/src/status_im/common/router.cljs new file mode 100644 index 00000000000..b9c1d262c54 --- /dev/null +++ b/src/status_im/common/router.cljs @@ -0,0 +1,361 @@ +(ns status-im.common.router + (:require + [bidi.bidi :as bidi] + [clojure.string :as string] + [legacy.status-im.ethereum.ens :as ens] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [status-im.common.validation.general :as validators] + [status-im.constants :as constants] + [status-im.contexts.chat.events :as chat.events] + [taoensso.timbre :as log] + [utils.address :as address] + [utils.ens.core :as utils.ens] + [utils.ens.stateofus :as stateofus] + [utils.ethereum.chain :as chain] + [utils.ethereum.eip.eip681 :as eip681] + [utils.security.core :as security] + [utils.transforms :as transforms] + [utils.url :as url])) + +(def ethereum-scheme "ethereum:") + +(def uri-schemes ["status-app://"]) + +(def web-prefixes ["https://" "http://" "https://www." "http://www."]) + +(def status-web2-domain "status.app") + +(def user-with-data-path "u/") +(def community-with-data-path "c/") +(def channel-path "cc/") + +(def status-web-urls (map #(str % status-web2-domain "/") web-prefixes)) + +(defn prepend-status-urls + [path] + (map #(str % path) status-web-urls)) + +(def handled-schemes (set (into uri-schemes status-web-urls))) + +(defn wc-normalize-uri + [uri] + (if (string/includes? uri "//wc:") (string/replace-first uri "/?" "?") uri)) + +(def group-chat-extractor + {[#"(.*)" :params] {"" :group-chat + "/" :group-chat}}) + +(def eip-extractor + {#{[:prefix "-" :address] [:address]} + {#{["@" :chain-id] ""} + {#{["/" :function] ""} :ethereum}}}) + +(def routes + ["" + {handled-schemes {[community-with-data-path :community-data] :community + [channel-path :community-data] :community-chat + ["p/" :chat-id] :private-chat + ["cr/" :community-id] :community-requests + "g/" group-chat-extractor + ["wallet/" :account] :wallet-account + [user-with-data-path :user-data] :user + "c" :community + "u" :user + #"wc:([^\?]+)" :wallet-connect + "wc" :wallet-connect} + ethereum-scheme eip-extractor}]) + +(defn parse-query-params + [url] + (let [url (goog.Uri. url)] + (url/query->map (.getQuery url)))) + +(defn parse-fragment + [url] + (let [url (goog.Uri. url) + fragment (.getFragment url)] + (when-not (string/blank? fragment) + fragment))) + +(defn remove-scheme + [uri] + (let [[_ url] (string/split uri #"://")] + url)) + +(defn match-uri + [uri] + (let [;; bidi has trouble parse path with `=` in it extract `=` here and add back to parsed + ;; base64url regex based on https://datatracker.ietf.org/doc/html/rfc4648#section-5 may + ;; include invalid base64 (invalid length, length of any base64 encoded string must be a + ;; multiple of 4) + ;; equal-end-of-base64url can be `=`, `==`, `nil` + equal-end-of-base64url + (last (re-find #"^(https|status-app)://(status\.app/)?(c|cc|u)/([a-zA-Z0-9_-]+)(={0,2})#" uri)) + + uri-without-equal-in-path + (if equal-end-of-base64url (string/replace-first uri equal-end-of-base64url "") uri) + + ;;bidi has issue to detect wc regex with /?, couldn't fine any other workaround + normalize-uri + (wc-normalize-uri uri-without-equal-in-path) + + ;; fragment is the one after `#`, usually user-id, ens-name, community-id + fragment (parse-fragment uri) + ens? (utils.ens/is-valid-eth-name? fragment) + compressed-key? (validators/valid-compressed-key? fragment) + + {:keys [handler route-params query-params] :as parsed} + (assoc (bidi/match-route routes normalize-uri) + :uri uri + :query-params (parse-query-params uri))] + (cond-> parsed + ens? + (assoc-in [:route-params :ens-name] fragment) + + (and (= handler :community) compressed-key?) + (assoc-in [:route-params :community-id] fragment) + + (and equal-end-of-base64url (= handler :community) (:community-data route-params)) + (update-in [:route-params :community-data] #(str % equal-end-of-base64url)) + + (and equal-end-of-base64url (= handler :community-chat) (:community-data route-params)) + (update-in [:route-params :community-data] #(str % equal-end-of-base64url)) + + (and compressed-key? (= handler :community-chat) (:community-data route-params)) + (assoc-in [:route-params :community-id] fragment) + + (and fragment + (= handler :community-chat) + (:community-data route-params) + (string? (:community-data route-params)) + (re-find constants/regx-starts-with-uuid (:community-data route-params))) + (assoc-in [:route-params :community-channel-id] (:community-data route-params)) + + (and fragment + (= handler :community-chat) + (:community-data route-params) + (string? (:community-data route-params)) + (not (re-find constants/regx-starts-with-uuid (:community-data route-params)))) + (assoc-in [:route-params :community-encoded-data?] true) + + (and equal-end-of-base64url (= handler :user) (:user-data route-params)) + (update-in [:route-params :user-data] #(str % equal-end-of-base64url)) + + (and (= handler :user) compressed-key?) + (assoc-in [:route-params :user-id] fragment) + + (= handler :wallet-connect) + (assoc-in [:route-params :uri] + (if (get query-params "uri") + (get query-params "uri") + (remove-scheme normalize-uri)))))) + +(defn match-contact-async + [chain {:keys [user-id ens-name]} callback] + (let [valid-public-key? (and (validators/valid-public-key? user-id) + (not= user-id utils.ens/default-key)) + valid-compressed-key? (validators/valid-compressed-key? user-id)] + (cond + valid-public-key? + (callback {:type :contact + :public-key user-id + :ens-name ens-name}) + + valid-compressed-key? + (native-module/compressed-key->public-key + user-id + constants/deserialization-key + (fn [response] + (let [{:keys [error]} (transforms/json->clj response)] + (when-not error + (match-contact-async + chain + {:user-id (str "0x" (subs response 5)) :ens-name ens-name} + callback))))) + + (and (not valid-public-key?) + (string? user-id) + (not (string/blank? user-id)) + (not= user-id "0x")) + (let [chain-id (chain/chain-keyword->chain-id chain) + ens-name (stateofus/ens-name-parse user-id) + on-success #(match-contact-async chain {:user-id % :ens-name ens-name} callback)] + (ens/pubkey chain-id ens-name on-success)) + + :else + (callback {:type :contact + :error :not-found})))) + +(defn match-group-chat + [chats {:strs [a a1 a2]}] + (let [[admin-pk encoded-chat-name chat-id] [a a1 a2] + chat-id-parts (when (not (string/blank? chat-id)) + (string/split chat-id #"-")) + chat-name (when (not (string/blank? encoded-chat-name)) + (js/decodeURI encoded-chat-name))] + (cond + (and (not (string/blank? chat-id)) + (not (string/blank? admin-pk)) + (not (string/blank? chat-name)) + (> (count chat-id-parts) 1) + (not (string/blank? (first chat-id-parts))) + (validators/valid-public-key? admin-pk) + (validators/valid-public-key? (last chat-id-parts))) + {:type :group-chat + :chat-id chat-id + :invitation-admin admin-pk + :chat-name chat-name} + + (and (not (string/blank? chat-id)) + (chat.events/group-chat? (get chats chat-id))) + (let [{:keys [chat-name invitation-admin]} (get chats chat-id)] + {:type :group-chat + :chat-id chat-id + :invitation-admin invitation-admin + :chat-name chat-name}) + + :else + {:error :invalid-group-chat-data}))) + +(defn match-private-chat-async + [chain {:keys [chat-id]} cb] + (match-contact-async chain + {:user-id chat-id} + (fn [{:keys [public-key]}] + (if public-key + (cb {:type :private-chat + :chat-id public-key}) + (cb {:type :private-chat + :error :invalid-chat-id}))))) + +(defn parse-shared-url + [uri] + (re-frame/dispatch [:shared-urls/parse-shared-url uri])) + +(defn match-community-channel-async + [{:keys [community-channel-id community-id]} cb] + (if (validators/valid-compressed-key? community-id) + (native-module/deserialize-and-compress-key + community-id + #(cb {:type :community-chat :chat-id (str % community-channel-id)})) + (cb {:type :community-chat + :error :not-found}))) + +(defn match-browser-string + [domain] + (if (security/safe-link? domain) + {:type :browser + :url domain} + {:type :browser + :error :unsafe-link})) + +;; NOTE(Ferossgp): Better to handle eip681 also with router instead of regexp. +(defn match-eip681 + [uri] + (if-let [message (eip681/parse-uri uri)] + (let [{:keys [paths ens-names]} + (reduce (fn [acc path] + (let [address (get-in message path)] + (if (utils.ens/is-valid-eth-name? address) + (-> acc + (update :paths conj path) + (update :ens-names conj address)) + acc))) + {:paths [] :ens-names []} + [[:address] [:function-arguments :address]])] + (if (empty? ens-names) + ;; if there are no ens-names, we dispatch request-uri-parsed immediately + {:type :eip681 + :message message + :uri uri} + {:type :eip681 + :uri uri + :message message + :paths paths + :ens-names ens-names})) + {:type :eip681 + :uri uri + :error :cannot-parse})) + +(defn address->eip681 + [address] + (match-eip681 (str ethereum-scheme address))) + +(defn match-wallet-account + [{:keys [account]}] + {:type :wallet-account + :account (when account (string/lower-case account))}) + +(defn handle-uri + [chain chats uri cb] + (let [{:keys [handler route-params query-params]} (match-uri uri)] + (log/info "[router] uri " uri " matched " handler " with " route-params) + (cond + + ;; ;; NOTE: removed in `match-uri`, might need this in the future + ;; (= handler :browser) + ;; (cb (match-browser uri route-params)) + + (= handler :ethereum) + (cb (match-eip681 uri)) + + (and (= handler :user) (:user-id route-params)) + (match-contact-async chain route-params cb) + + ;; NOTE: removed in `match-uri`, might need this in the future + (= handler :private-chat) + (match-private-chat-async chain route-params cb) + + ;; NOTE: removed in `match-uri`, might need this in the future + (= handler :group-chat) + (cb (match-group-chat chats query-params)) + + (validators/valid-public-key? uri) + (match-contact-async chain {:user-id uri} cb) + + ;; NOTE: removed in `match-uri`, might need this in the future + (= handler :community-requests) + (cb {:type handler :community-id (:community-id route-params)}) + + (and (= handler :community) (:community-id route-params)) + (cb {:type :community + :community-id (:community-id route-params)}) + + (and (= handler :community-chat) (:community-channel-id route-params) (:community-id route-params)) + (match-community-channel-async route-params cb) + + (and (= handler :community-chat) + (:community-encoded-data? route-params) + (:community-id route-params)) + (parse-shared-url uri) + + ;; NOTE: removed in `match-uri`, might need this in the future + (= handler :wallet-account) + (cb (match-wallet-account route-params)) + + (= handler :wallet-connect) + (cb {:type :wallet-connect + :uri (:uri route-params)}) + + (address/address? uri) + (cb (address->eip681 uri)) + + (url/url? uri) + (cb (match-browser-string uri)) + + (string/starts-with? uri constants/local-pairing-connection-string-identifier) + (cb {:type :localpairing :data uri}) + + (string/starts-with? uri constants/wc-connection-string-identifier) + (cb {:type :wallet-connect + :uri uri}) + + :else + (cb {:type :undefined + :data uri})))) + +(re-frame/reg-fx + :router/handle-uri + (fn [{:keys [chain chats uri cb]}] + (handle-uri chain chats uri cb))) diff --git a/src/status_im/common/router_test.cljs b/src/status_im/common/router_test.cljs new file mode 100644 index 00000000000..6b667b2b224 --- /dev/null +++ b/src/status_im/common/router_test.cljs @@ -0,0 +1,140 @@ +(ns status-im.common.router-test + (:require + [cljs.test :refer [are deftest]] + [status-im.common.router :as router])) + +(def public-key + "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073") +(def chat-id + "59eb36e6-9d4d-4724-9d3a-8a3cdc5e8a8e-0x04f383daedc92a66add4c90d8884004ef826cba113183a0052703c8c77fed1522f88f44550498d20679af98907627059a295e43212a1cd3c1f21a157704d608c13") +(def chat-name "Test group chat") + +(deftest parse-uris-test + (are [uri expected] (= (cond-> (router/match-uri uri) + (< (count expected) 3) + (assoc :query-params nil)) + {:handler (first expected) + :route-params (second expected) + :query-params (when (= 3 (count expected)) (last expected)) + :uri uri}) + + "https://status.app/u#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" + [:user + {:user-id "zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"}] + "https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" + [:user + {:user-data + "G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y" + :user-id "zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"}] + + "status-app://u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" + [:user + {:user-data + "G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y" + :user-id "zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"}] + + "status-app://u#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" + [:user + {:user-id "zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"}] + + "https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + [:community-chat + {:community-data + "G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=" + :community-encoded-data? true + :community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}] + + "status-app://cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + [:community-chat + {:community-data + "G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=" + :community-encoded-data? true + :community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}] + + "https://status.app/cc/c432709e-fc73-440d-bb67-cb3a0929dfda#zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo" + [:community-chat + {:community-data + "c432709e-fc73-440d-bb67-cb3a0929dfda" + :community-channel-id + "c432709e-fc73-440d-bb67-cb3a0929dfda" + :community-id "zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo"}] + + "status-app://cc/c432709e-fc73-440d-bb67-cb3a0929dfda#zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo" + [:community-chat + {:community-data + "c432709e-fc73-440d-bb67-cb3a0929dfda" + :community-channel-id + "c432709e-fc73-440d-bb67-cb3a0929dfda" + :community-id "zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo"}] + + "https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + [:community + {:community-data + "iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=" + :community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}] + "https://status.app/c#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + [:community + {:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}] + + "status-app://c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + [:community + {:community-data + "iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=" + :community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}] + + "status-app://c#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + [:community + {:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}] + + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + [:ethereum {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"}] + + "ethereum:pay-0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + [:ethereum + {:prefix "pay" + :address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"}] + "ethereum:foo-0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + [:ethereum + {:prefix "foo" + :address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"}] + + ;; FIXME: Should handle only first line + "ethereum:foo-state-of-us.eth" + [:ethereum + {:prefix "foo-state-of" + :address "us.eth"}] + + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@42" + [:ethereum + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + :chain-id "42"}] + + "ethereum:0x0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x12345&uint256=1" + [:ethereum + {:address "0x0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + :function "transfer"}] + + "ethereum:0x0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=2.014e18&gas=10&gasLimit=21000&gasPrice=50" + [:ethereum {:address "0x0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"}] + + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@1/transfer?uint256=1" + [:ethereum + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + :chain-id "1" + :function "transfer"}])) + +(def error {:error :invalid-group-chat-data}) + +(deftest match-group-chat-query-test + (are [query-params expected] (= (router/match-group-chat {} query-params) + expected) + nil error + {} error + {"b" public-key} error + {"a" public-key "a1" chat-name} error + {"a" "0x00ceded" "a1" chat-name "a2" chat-id} error + {"a" public-key "a1" chat-name "a2" public-key} error + {"a" public-key "a1" chat-name "a2" chat-id} {:type :group-chat + :chat-id chat-id + :invitation-admin public-key + :chat-name chat-name})) diff --git a/src/status_im/common/scalable_avatar/style.cljs b/src/status_im/common/scalable_avatar/style.cljs new file mode 100644 index 00000000000..ed8404145fc --- /dev/null +++ b/src/status_im/common/scalable_avatar/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.common.scalable-avatar.style) + +(defn wrapper + [border-color scale] + [{:transform-origin "bottom left" + :border-width 4 + :border-color border-color + :border-radius 100 + :transform [{:scale 1} {:translate-y 4}]} + {:transform [{:scale scale} {:translate-y 4}]}]) diff --git a/src/status_im/common/scalable_avatar/view.cljs b/src/status_im/common/scalable_avatar/view.cljs new file mode 100644 index 00000000000..7f539588863 --- /dev/null +++ b/src/status_im/common/scalable_avatar/view.cljs @@ -0,0 +1,19 @@ +(ns status-im.common.scalable-avatar.view + (:require [quo.core :as quo] + [react-native.reanimated :as reanimated] + [status-im.common.scalable-avatar.style :as style])) + +(def scroll-range #js [0 48]) +(def scale-range #js [1 0.4]) + +(defn view + [{:keys [scroll-y full-name online? profile-picture customization-color border-color]}] + (let [image-scale (reanimated/interpolate scroll-y scroll-range scale-range :clamp)] + [reanimated/view {:style (style/wrapper border-color image-scale)} + [quo/user-avatar + {:full-name full-name + :online? online? + :profile-picture profile-picture + :status-indicator? true + :customization-color customization-color + :size :big}]])) diff --git a/src/status_im/common/scan_qr_code/style.cljs b/src/status_im/common/scan_qr_code/style.cljs new file mode 100644 index 00000000000..ebf773da7a5 --- /dev/null +++ b/src/status_im/common/scan_qr_code/style.cljs @@ -0,0 +1,140 @@ +(ns status-im.common.scan-qr-code.style + (:require [quo.foundations.colors :as colors])) + +(def background + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0 + :background-color colors/neutral-95}) + +(def screen-padding 20) +(def flash-button-size 32) +(def flash-button-spacing 12) + +(def flex-spacer {:flex 1}) + +(def hole {:z-index 2 :opacity 0.95}) + +(defn root-container + [padding-top] + {:z-index 5 + :flex 1 + :padding-top padding-top}) + +(def header-container + {:flex-direction :row + :justify-content :space-between + :padding-horizontal screen-padding + :margin-vertical 12}) + +(defn header-text + [bottom-padding?] + {:padding-horizontal screen-padding + :padding-top 12 + :padding-bottom (when bottom-padding? 8) + :color colors/white}) + +(def header-sub-text + {:padding-horizontal screen-padding + :color colors/white}) + +(def scan-qr-code-container + {:margin-top 20}) + +(def qr-view-finder + {:margin-horizontal screen-padding + :height 1 + :display :flex}) + +(defn qr-view-finder-container + [size] + {:width size + :height size + :justify-content :space-between + :margin-left -1 + :margin-top -1}) + +(defn viewfinder-container + [viewfinder] + {:position :absolute + :left (:x viewfinder) + :top (:y viewfinder)}) + +(def view-finder-border-container + {:flex-direction :row + :justify-content :space-between}) + +(defn camera-flash-button + [viewfinder] + {:position :absolute + :top (- (+ (:y viewfinder) (:height viewfinder)) flash-button-size flash-button-spacing) + :right (+ screen-padding flash-button-spacing)}) + +(defn- get-border + [border-vertical-width border-horizontal-width corner-radius] + {:border-color colors/white + :width 78 + :height 78 + border-vertical-width 2 + border-horizontal-width 2 + corner-radius 16}) + +(def white-border + (let [base-tip {:background-color colors/white + :position :absolute + :height 1.9 ; 1.9 instead of 2 to fix the tips protruding + :width 1.9 + :border-radius 1}] + {:top-left + {:border (get-border :border-top-width :border-left-width :border-top-left-radius) + :tip-1 (assoc base-tip :right -1 :top 0) + :tip-2 (assoc base-tip :left 0 :bottom -1)} + :top-right + {:border (get-border :border-top-width :border-right-width :border-top-right-radius) + :tip-1 (assoc base-tip :right 0 :bottom -1) + :tip-2 (assoc base-tip :left -1 :top 0)} + :bottom-left + {:border (get-border :border-bottom-width :border-left-width :border-bottom-left-radius) + :tip-1 (assoc base-tip :right -1 :bottom 0) + :tip-2 (assoc base-tip :left 0 :top -1)} + :bottom-right + {:border (get-border :border-bottom-width :border-right-width :border-bottom-right-radius) + :tip-1 (assoc base-tip :right 0 :top -1) + :tip-2 (assoc base-tip :left -1 :bottom 0)}})) + +(def viewfinder-text + {:color colors/white-opa-70 + :text-align :center + :padding-top 16}) + +(def camera-permission-container + {:height 335 + :margin-top 19 + :margin-horizontal screen-padding + :background-color colors/white-opa-5 + :border-color colors/white-opa-10 + :border-width 1 + :border-radius 12 + :border-style :dashed + :align-items :center + :justify-content :center}) + +(def enable-camera-access-header + {:color colors/white}) + +(def enable-camera-access-sub-text + {:color colors/white-opa-70 + :margin-bottom 16}) + +(def camera-style + {:flex 1}) + +(def camera-container + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :border-radius 16}) diff --git a/src/status_im/common/scan_qr_code/view.cljs b/src/status_im/common/scan_qr_code/view.cljs new file mode 100644 index 00000000000..9df1be61013 --- /dev/null +++ b/src/status_im/common/scan_qr_code/view.cljs @@ -0,0 +1,258 @@ +(ns status-im.common.scan-qr-code.view + (:require [clojure.string :as string] + [oops.core :as oops] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.camera-kit :as camera-kit] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [react-native.permissions :as permissions] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.device-permissions :as device-permissions] + [status-im.common.scan-qr-code.style :as style] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defonce camera-permission-granted? (reagent/atom false)) + +(defn- header + [{:keys [title subtitle share-button?]}] + [:<> + [rn/view {:style style/header-container} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :close-scan-qr-code + :on-press #(rf/dispatch [:navigate-back])} + :i/close] + (when share-button? + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :show-qr-button + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/share-shell]))} + :i/qr-code])] + [quo/text + {:size :heading-1 + :weight :semi-bold + :style (style/header-text (when subtitle true))} + title] + (when subtitle + [quo/text + {:size :paragraph-1 + :weight :regular + :style style/header-sub-text} + subtitle])]) + +(defn get-labels-and-on-press-method + [] + {:title-label-key :t/enable-access-to-camera + :description-label-key :t/to-scan-a-qr-enable-your-camera + :button-icon :i/camera + :button-label :t/enable-camera + :accessibility-label :request-camera-permission + :on-press (fn [] + (device-permissions/camera #(reset! camera-permission-granted? true)))}) + +(defn- camera-permission-view + [] + (let [{:keys [title-label-key + description-label-key + button-icon + button-label + accessibility-label + on-press]} (get-labels-and-on-press-method)] + [rn/view {:style style/camera-permission-container} + [quo/text + {:size :paragraph-1 + :weight :medium + :style style/enable-camera-access-header} + (i18n/label title-label-key)] + [quo/text + {:size :paragraph-2 + :weight :regular + :style style/enable-camera-access-sub-text} + (i18n/label description-label-key)] + [quo/button + {:icon-left button-icon + :type :primary + :size 32 + :accessibility-label accessibility-label + :customization-color :blue + :on-press on-press} + (i18n/label button-label)]])) + +(defn- qr-scan-hole-area + [qr-view-finder] + [rn/view + {:style style/qr-view-finder + :on-layout (fn [event] + (let [layout (transforms/js->clj (oops/oget event "nativeEvent.layout")) + view-finder (assoc layout :height (:width layout))] + (reset! qr-view-finder view-finder)))}]) + +(defn- white-border + [corner] + (let [border-styles (style/white-border corner)] + [rn/view + [rn/view {:style (border-styles :border)}] + [rn/view {:style (border-styles :tip-1)}] + [rn/view {:style (border-styles :tip-2)}]])) + +(defn- white-square + [layout-size] + [rn/view {:style (style/qr-view-finder-container layout-size)} + [rn/view {:style style/view-finder-border-container} + [white-border :top-left] + [white-border :top-right]] + [rn/view {:style style/view-finder-border-container} + [white-border :bottom-left] + [white-border :bottom-right]]]) + +(defn- viewfinder + [qr-view-finder helper-text?] + (let [layout-size (+ (:width qr-view-finder) 2)] + [rn/view {:style (style/viewfinder-container qr-view-finder)} + [white-square layout-size] + (when helper-text? + [quo/text + {:size :paragraph-2 + :weight :regular + :style style/viewfinder-text} + (i18n/label :t/ensure-qr-code-is-in-focus-to-scan)])])) + +(defn- scan-qr-code-tab + [qr-view-finder helper-text?] + (if (and @camera-permission-granted? + (boolean (not-empty qr-view-finder))) + [viewfinder qr-view-finder helper-text?] + [camera-permission-view])) + +(defn- check-qr-code-and-navigate + [{:keys [event error-message validate-fn on-success-scan on-failed-scan scan-code?]}] + (let [scanned-value (string/trim (oops/oget event "nativeEvent.codeStringValue")) + validated? (if validate-fn (validate-fn scanned-value) true)] + ;; Note - camera-kit keeps scanning until the screen is dismissed, so disable scanning to make + ;; sure the on-read-code doesn't get executed multiple times + (reset! scan-code? false) + (if validated? + (on-success-scan scanned-value) + (do + (on-failed-scan) + (debounce/debounce-and-dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text error-message}] + 300))))) + +(defn- render-camera + [{:keys [torch-mode qr-view-finder scan-code? validate-fn error-message set-qr-code-succeeded + set-rescan-timeout]}] + [:<> + [rn/view {:style style/camera-container} + [camera-kit/camera + {:style style/camera-style + :camera-type camera-kit/camera-type-back + :zoom-mode :off + :torch-mode torch-mode + :scan-barcode true + :on-read-code #(when @scan-code? + (check-qr-code-and-navigate {:event % + :scan-code? scan-code? + :validate-fn validate-fn + :error-message error-message + :on-success-scan set-qr-code-succeeded + :on-failed-scan set-rescan-timeout}))}]] + [hole-view/hole-view + {:style [rn/stylesheet-absolute-fill style/hole] + :holes [(assoc qr-view-finder :border-radius 16)]} + [quo/blur + {:style rn/stylesheet-absolute-fill + :blur-amount 10 + :blur-type :transparent + :overlay-color colors/neutral-80-opa-80-blur + :background-color colors/neutral-80-opa-80-blur}]]]) + +(defn- set-listener-torch-off-on-app-inactive + [torch-atm] + (let [set-torch-off-fn #(when (not= % "active") (reset! torch-atm false)) + app-state-listener (.addEventListener rn/app-state "change" set-torch-off-fn)] + #(.remove app-state-listener))) + +(defn- navigate-back-handler + [] + (rf/dispatch [:navigate-back]) + true) + +(defn view + [{:keys [title subtitle validate-fn on-success-scan error-message share-button?]}] + (let [insets safe-area/insets + qr-code-succeed? (reagent/atom false) + qr-view-finder (reagent/atom {}) + torch? (reagent/atom false) + scan-code? (reagent/atom true) + set-rescan-timeout (fn [] + (reset! scan-code? false) + (js/setTimeout #(reset! scan-code? true) 3000))] + (fn [] + (let [torch-mode (if @torch? :on :off) + flashlight-icon (if @torch? :i/flashlight-on :i/flashlight-off) + show-camera? (and @camera-permission-granted? + (boolean (not-empty @qr-view-finder))) + camera-ready-to-scan? (and show-camera? + (not @qr-code-succeed?))] + (rn/use-mount + (fn [] + (rn/hw-back-add-listener navigate-back-handler) + (set-listener-torch-off-on-app-inactive torch?) + (when-not @camera-permission-granted? + (permissions/permission-granted? + :camera + #(reset! camera-permission-granted? %) + #(reset! camera-permission-granted? false))) + #(rn/hw-back-remove-listener navigate-back-handler))) + [:<> + [rn/view {:style style/background}] + (when camera-ready-to-scan? + [render-camera + {:torch-mode torch-mode + :qr-view-finder @qr-view-finder + :scan-code? scan-code? + :error-message error-message + :validate-fn validate-fn + :set-qr-code-succeeded (fn [value] + (when on-success-scan + (on-success-scan value)) + (rf/dispatch [:navigate-back])) + :set-rescan-timeout set-rescan-timeout}]) + [rn/view {:style (style/root-container (:top insets))} + [header + {:title title + :subtitle subtitle + :share-button? share-button?}] + (when (empty? @qr-view-finder) + [:<> + [rn/view {:style style/scan-qr-code-container}] + [qr-scan-hole-area qr-view-finder]]) + [scan-qr-code-tab @qr-view-finder true] + [rn/view {:style style/flex-spacer}] + (when show-camera? + [quo/button + {:icon-only? true + :type :grey + :background :photo + :size style/flash-button-size + :accessibility-label :camera-flash + :container-style (style/camera-flash-button @qr-view-finder) + :on-press #(swap! torch? not)} + flashlight-icon])]])))) diff --git a/src/status_im/common/scroll_page/style.cljs b/src/status_im/common/scroll_page/style.cljs new file mode 100644 index 00000000000..d977f5d281d --- /dev/null +++ b/src/status_im/common/scroll_page/style.cljs @@ -0,0 +1,55 @@ +(ns status-im.common.scroll-page.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated])) + +(defn blur-slider + [animation height theme] + (reanimated/apply-animations-to-style + {:transform [{:translateY animation}]} + {:z-index 5 + :position :absolute + :top 0 + :height height + :right 0 + :left 0 + :background-color (if platform/android? + (colors/theme-colors colors/white colors/neutral-80 theme) + :transparent)})) + +(defn children-container + [{:keys [border-radius background-color]}] + {:flex 1 + :border-top-left-radius border-radius + :border-top-right-radius border-radius + :background-color background-color}) + +(def picture-radius 40) +(def picture-diameter (* 2 picture-radius)) +(def picture-border-width 4) + +(defn display-picture-container + [animation theme] + (reanimated/apply-animations-to-style + {:transform [{:scale animation}]} + {:border-radius picture-diameter + :border-width picture-border-width + :border-color (colors/theme-colors colors/white colors/neutral-95 theme) + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :position :absolute + :top (- (+ picture-radius picture-border-width)) + :left (+ (/ picture-radius 2) picture-border-width)})) + +(defn display-picture + [theme] + {:border-radius picture-diameter + :width picture-diameter + :height picture-diameter + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(defn cover-background + [cover-color] + {:background-color cover-color + :height 167 + :margin-bottom -16}) diff --git a/src/status_im/common/scroll_page/view.cljs b/src/status_im/common/scroll_page/view.cljs new file mode 100644 index 00000000000..7fa5dc35f31 --- /dev/null +++ b/src/status_im/common/scroll_page/view.cljs @@ -0,0 +1,154 @@ +(ns status-im.common.scroll-page.view + (:require + [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.scroll-page.style :as style] + [utils.re-frame :as rf])) + +(def negative-scroll-position-0 0) +(def scroll-position-0 0) + +(defn diff-with-max-min + [value maximum minimum] + (->> + (+ value scroll-position-0) + (- maximum) + (max minimum) + (min maximum))) + +(defn page-header-threshold + [collapsed?] + (if collapsed? 50 170)) + +(defn f-scroll-page-header + [{:keys [scroll-height height page-nav-right-section-buttons sticky-header + top-nav title-colum navigate-back? collapsed? page-nav-props overlay-shown?]}] + (let [theme (quo.context/use-theme) + input-range [0 10] + output-range [-208 -45] + y (reanimated/use-shared-value scroll-height) + translate-animation (reanimated/interpolate y + input-range + output-range + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"}) + opacity-animation (reanimated/use-shared-value 0) + threshold (page-header-threshold collapsed?)] + (rn/use-effect + (fn [] + (reanimated/set-shared-value y scroll-height) + (reanimated/set-shared-value opacity-animation + (reanimated/with-timing (if (>= scroll-height threshold) 1 0) + (clj->js {:duration 300})))) + [scroll-height]) + [:<> + [reanimated/blur-view + {:blur-amount 20 + :blur-type :ultraThinMaterial + :overlay-color :transparent + :style (style/blur-slider translate-animation height theme)}] + [rn/view + {:style {:z-index 6 + :position :absolute + :top 0 + :left 0 + :right 0}} + (if top-nav + [rn/view {:style {:margin-top 0}} + top-nav] + [quo/page-nav + (cond-> {:margin-top safe-area/top + :type :no-title + :background (if (or (:blur? page-nav-props) + (= 1 (reanimated/get-shared-value opacity-animation))) + :blur + :photo) + :right-side page-nav-right-section-buttons + :center-opacity opacity-animation + :overlay-shown? overlay-shown?} + navigate-back? (assoc :icon-name :i/close + :accessibility-label :back-button + :on-press #(rf/dispatch [:navigate-back])) + page-nav-props (merge page-nav-props))]) + (when title-colum + title-colum) + sticky-header]])) + + +(defn f-display-picture + [scroll-height cover theme] + (let [input-range [0 150] + y (reanimated/use-shared-value scroll-height) + animation (reanimated/interpolate y + input-range + [1 0.5] + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"})] + (rn/use-effect #(do + (reanimated/set-shared-value y scroll-height) + js/undefined) + [scroll-height]) + [reanimated/view + {:style (style/display-picture-container animation theme)} + [rn/image + {:source cover + :style (style/display-picture theme)}]])) + +(defn scroll-page + [_ _ _] + (let [scroll-height (reagent/atom negative-scroll-position-0)] + (fn [{:keys [cover-image cover-color logo on-scroll + collapsed? height top-nav title-colum background-color navigate-back? page-nav-props + overlay-shown? sticky-header children-style]} + children] + (let [theme (quo.context/use-theme)] + [:<> + [f-scroll-page-header + {:scroll-height @scroll-height + :height height + :sticky-header sticky-header + :top-nav top-nav + :title-colum title-colum + :navigate-back? navigate-back? + :collapsed? collapsed? + :page-nav-props page-nav-props + :overlay-shown? overlay-shown?}] + [rn/scroll-view + {:bounces false + :content-container-style {:flex-grow 1} + :content-inset-adjustment-behavior :never + :shows-vertical-scroll-indicator false + :scroll-event-throttle 16 + :on-scroll (fn [^js event] + (reset! scroll-height (int + (oops/oget + event + "nativeEvent.contentOffset.y"))) + (when on-scroll + (on-scroll @scroll-height)))} + (when cover-color + [rn/view {:style (style/cover-background cover-color)}]) + (when cover-image + [rn/view {:style {:height (if collapsed? 110 151)}} + [rn/image + {:source cover-image + ;; Using negative margin-bottom as a workaround because on Android, + ;; ScrollView clips its children despite setting overflow: 'visible'. + ;; Related issue: https://github.com/facebook/react-native/issues/31218 + :style {:margin-bottom -16 + :flex 1}}]]) + (when children + [rn/view + {:style (style/children-container + (merge + children-style + {:border-radius (diff-with-max-min @scroll-height 16 0) + :background-color background-color}))} + (when (and (not collapsed?) cover-image) + [:f> f-display-picture @scroll-height logo theme]) + children])]])))) diff --git a/src/status_im/common/serialization.cljs b/src/status_im/common/serialization.cljs new file mode 100644 index 00000000000..8783008e3d2 --- /dev/null +++ b/src/status_im/common/serialization.cljs @@ -0,0 +1,27 @@ +(ns status-im.common.serialization + (:require + [native-module.core :as native-module] + [status-im.constants :as constants] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(rf/reg-fx :serialization/deserialize-and-compress-key + (fn [{:keys [serialized-key on-success on-error]}] + (native-module/deserialize-and-compress-key + serialized-key + (fn [resp] + (let [{:keys [error]} (transforms/json->clj resp)] + (if error + (on-error error) + (on-success resp))))))) + +(rf/reg-fx :serialization/decompress-public-key + (fn [{:keys [compressed-key on-success on-error]}] + (native-module/compressed-key->public-key + compressed-key + constants/deserialization-key + (fn [resp] + (let [{:keys [error]} (transforms/json->clj resp)] + (if error + (on-error error) + (on-success (str "0x" (subs resp 5))))))))) diff --git a/src/status_im/common/shared_urls/data_store.cljs b/src/status_im/common/shared_urls/data_store.cljs new file mode 100644 index 00000000000..f929d713d00 --- /dev/null +++ b/src/status_im/common/shared_urls/data_store.cljs @@ -0,0 +1,20 @@ +(ns status-im.common.shared-urls.data-store + (:require + [clojure.set :as set])) + +(defn <-rpc + [{:keys [community channel contact]}] + {:community (set/rename-keys + community + {:communityId :community-id + :displayName :display-name + :membersCount :members-count + :tagIndices :tag-indices}) + :channel (set/rename-keys + channel + {:displayName :display-name + :channelUuid :channel-uuid}) + :contact (set/rename-keys + contact + {:displayName :display-name + :publicKey :public-key})}) diff --git a/src/status_im/common/shared_urls/events.cljs b/src/status_im/common/shared_urls/events.cljs new file mode 100644 index 00000000000..f5c626d2ad0 --- /dev/null +++ b/src/status_im/common/shared_urls/events.cljs @@ -0,0 +1,33 @@ +(ns status-im.common.shared-urls.events + (:require + [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.common.shared-urls.data-store :as data-store] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn parse-shared-url-success + [url response] + (let [parsed-data (data-store/<-rpc response) + cb #(rf/dispatch [:universal-links/match-value url %])] + (cond + (:channel parsed-data) + (cb {:type :community-chat + :chat-id (str (get-in parsed-data [:community :community-id]) + (get-in parsed-data [:channel :channel-uuid])) + :community-id (get-in parsed-data [:community :community-id])}) + + (:community parsed-data) + (cb {:type :community :community-id (get-in parsed-data [:community :community-id])})))) + +(defn parse-shared-url + [_ [uri]] + (let [url (string/replace uri #"status-app:/" "https://status.app")] + {:json-rpc/call + [{:method "wakuext_parseSharedURL" + :params [url] + :on-success #(parse-shared-url-success url %) + :on-error (fn [err] + (log/error "failed to parse shared url" {:error err :url url}))}]})) + +(re-frame/reg-event-fx :shared-urls/parse-shared-url parse-shared-url) diff --git a/src/status_im/common/signals/events.cljs b/src/status_im/common/signals/events.cljs new file mode 100644 index 00000000000..5ea412bf352 --- /dev/null +++ b/src/status_im/common/signals/events.cljs @@ -0,0 +1,116 @@ +(ns status-im.common.signals.events + (:require + [legacy.status-im.chat.models.message :as models.message] + [legacy.status-im.mailserver.core :as mailserver] + [legacy.status-im.visibility-status-updates.core :as visibility-status-updates] + [oops.core :as oops] + [status-im.common.pairing.events :as pairing] + [status-im.contexts.chat.messenger.messages.transport.events :as messages.transport] + [status-im.contexts.communities.discover.events] + [status-im.contexts.profile.push-notifications.local.events :as local-notifications] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(rf/defn summary + [{:keys [db] :as cofx} peers-summary] + (let [peers-count (count peers-summary)] + (rf/merge cofx + {:db (assoc db + :peers-summary peers-summary + :peer-stats/count peers-count)} + (visibility-status-updates/peers-summary-change peers-count)))) + +(rf/defn process + {:events [:signals/signal-received]} + [{:keys [db] :as cofx} event-str] + ;; We only convert to clojure when strictly necessary or we know it + ;; won't impact performance, as it is a fairly costly operation on large-ish + ;; data structures + (let [^js data (.parse js/JSON event-str) + ^js event-js (.-event data) + type (.-type data)] + (log/debug "Signal received" {:type type}) + (log/trace "Signal received" {:payload event-str}) + (case type + "wallet" + {:fx [[:dispatch [:wallet/signal-received event-js]]]} + + "wallet.suggested.routes" + {:fx [[:dispatch [:wallet/handle-suggested-routes (transforms/js->clj event-js)]]]} + + "wallet.router.sign-transactions" + {:fx [[:dispatch [:wallet/sign-transactions-signal-received (transforms/js->clj event-js)]]]} + + "wallet.router.transactions-sent" + {:fx [[:dispatch [:wallet/transactions-sent-signal-received (transforms/js->clj event-js)]]]} + + "envelope.sent" + (messages.transport/update-envelopes-status + cofx + (:ids (transforms/js->clj event-js)) + :sent) + + "envelope.expired" + (messages.transport/update-envelopes-status + cofx + (:ids (transforms/js->clj event-js)) + :not-sent) + + "message.delivered" + (let [{:keys [chatID messageID]} (transforms/js->clj event-js)] + (models.message/update-message-status cofx chatID messageID :delivered)) + + "messages.new" + (messages.transport/sanitize-messages-and-process-response cofx event-js true) + + "mailserver.changed" + (mailserver/handle-mailserver-changed cofx (oops/oget event-js :id)) + + "mailserver.available" + (mailserver/handle-mailserver-available cofx (oops/oget event-js :id)) + + "discovery.summary" + (summary cofx (transforms/js->clj event-js)) + + "local-notifications" + (local-notifications/process cofx (transforms/js->clj event-js)) + + "community.found" + (let [community (transforms/js->clj event-js)] + {:fx [[:dispatch [:discover-community/maybe-found-unknown-contract-community community]]]}) + + "status.updates.timedout" + (visibility-status-updates/handle-visibility-status-updates cofx (transforms/js->clj event-js)) + + "localPairing" + (pairing/handle-local-pairing-signals cofx (transforms/js->clj event-js)) + + "curated.communities.update" + {:fx [[:dispatch [:fetched-contract-communities (transforms/js->clj event-js)]]]} + + "waku.backedup.profile" + {:fx [[:dispatch [:profile/update-profile-from-backup (transforms/js->clj event-js)]]]} + + "waku.backedup.settings" + {:fx [[:dispatch [:profile/update-setting-from-backup (transforms/js->clj event-js)]]]} + + "waku.backedup.keypair" + {:fx [[:dispatch [:wallet/process-keypair-from-backup (transforms/js->clj event-js)]]]} + + "waku.backedup.watch-only-account" + {:fx [[:dispatch [:wallet/process-watch-only-account-from-backup (transforms/js->clj event-js)]]]} + + "mediaserver.started" + {:db (assoc db :mediaserver/port (oops/oget event-js :port))} + + "node.login" + {:fx [[:dispatch [:profile.login/login-node-signal (transforms/js->clj event-js)]]]} + + "backup.performed" + ;; Delay to improve the UX, if no delay is added, messages appear/disappear too fast. + {:fx [[:dispatch-later + {:ms 1000 + :dispatch [:advanced-settings/backup-performed (oops/oget event-js :lastBackup)]}]]} + + (log/debug "Event " type " not handled")))) diff --git a/src/status_im/common/standard_authentication/core.cljs b/src/status_im/common/standard_authentication/core.cljs new file mode 100644 index 00000000000..d425462d0a5 --- /dev/null +++ b/src/status_im/common/standard_authentication/core.cljs @@ -0,0 +1,9 @@ +(ns status-im.common.standard-authentication.core + (:require + status-im.common.standard-authentication.password-input.view + status-im.common.standard-authentication.slide-auth + status-im.common.standard-authentication.slide-sign)) + +(def password-input status-im.common.standard-authentication.password-input.view/view) +(def slide-auth status-im.common.standard-authentication.slide-auth/view) +(def slide-sign status-im.common.standard-authentication.slide-sign/view) diff --git a/src/status_im/common/standard_authentication/enter_password/style.cljs b/src/status_im/common/standard_authentication/enter_password/style.cljs new file mode 100644 index 00000000000..84133df4b48 --- /dev/null +++ b/src/status_im/common/standard_authentication/enter_password/style.cljs @@ -0,0 +1,15 @@ +(ns status-im.common.standard-authentication.enter-password.style) + +(def enter-password-container + {:margin-horizontal 20 + :border-top-left-radius 12 + :border-top-right-radius 12}) + +(def enter-password-button + {:margin-top 45 + :margin-bottom 12}) + +(def context-tag + {:flex-direction :row + :margin-bottom 20 + :margin-top 8}) diff --git a/src/status_im/common/standard_authentication/enter_password/view.cljs b/src/status_im/common/standard_authentication/enter_password/view.cljs new file mode 100644 index 00000000000..ec3360c72eb --- /dev/null +++ b/src/status_im/common/standard_authentication/enter_password/view.cljs @@ -0,0 +1,55 @@ +(ns status-im.common.standard-authentication.enter-password.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.standard-authentication.core :as standard-authentication] + [status-im.common.standard-authentication.enter-password.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [on-enter-password on-press-biometrics button-label button-icon-left]}] + (let [{:keys [key-uid customization-color] :as profile} (rf/sub [:profile/profile-with-image]) + {:keys [error processing password]} (rf/sub [:profile/login]) + sign-in-enabled? (rf/sub [:sign-in-enabled?])] + [:<> + [rn/view {:style style/enter-password-container} + [rn/view + [quo/text + {:accessibility-label :sync-code-generated + :weight :semi-bold + :size :heading-2 + :style {:margin-bottom 4}} + (i18n/label :t/enter-password)] + [rn/view + {:style style/context-tag} + [quo/context-tag + {:type :default + :blur? true + :profile-picture (profile.utils/photo profile) + :full-name (profile.utils/displayed-name profile) + :customization-color customization-color + :size 24}]] + [standard-authentication/password-input + {:on-press-biometrics on-press-biometrics + :blur? true + :processing processing + :error {:error? (not (string/blank? error)) + :error-message error} + :default-password password + :sign-in-enabled? sign-in-enabled?}] + [quo/button + {:size 40 + :container-style style/enter-password-button + :type :primary + :customization-color (or customization-color :primary) + :accessibility-label :login-button + :icon-left button-icon-left + :disabled? (or (not sign-in-enabled?) processing) + :on-press (fn [] + (rf/dispatch [:set-in [:profile/login :key-uid] key-uid]) + (rf/dispatch [:profile.login/verify-database-password password + #(on-enter-password password)]))} + button-label]]]])) diff --git a/src/status_im/common/standard_authentication/events.cljs b/src/status_im/common/standard_authentication/events.cljs new file mode 100644 index 00000000000..4bbe9312053 --- /dev/null +++ b/src/status_im/common/standard_authentication/events.cljs @@ -0,0 +1,225 @@ +(ns status-im.common.standard-authentication.events + (:require + [schema.core :as schema] + [status-im.common.keychain.events :as keychain] + [status-im.common.standard-authentication.enter-password.view :as enter-password] + [status-im.common.standard-authentication.events-schema :as events-schema] + status-im.common.standard-authentication.keycard-events + [status-im.common.standard-authentication.utils :as utils] + [status-im.contexts.wallet.data-store :as data-store] + [taoensso.timbre :as log] + [utils.address] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn authorize-and-sign + [{:keys [db]} [{:keys [sign-payload on-sign-success on-sign-error] :as args}]] + (let [keypairs (get-in db [:wallet :keypairs]) + keycard-keypair? (->> sign-payload + first + :address + (utils/keycard-account? keypairs))] + {:fx [(cond + ;; NOTE: signing with both the keycard keypair and a non-keycard keypair (on-device) + ;; is not yet supported, since it would require both scanning the keycard and entering + ;; the password in the same flow, and should be fixed from the user-flow perspective + (and (utils/keycard-profile? db) + (utils/payloads-for-different-keypairs? keypairs sign-payload)) + [:dispatch + [:keycard/feature-unavailable-show + {:theme :dark + :feature-name + :standard-auth/multiple-keypair-signing}]] + + keycard-keypair? [:dispatch + [:standard-auth/authorize-with-keycard + {:on-complete (fn [pin] + (rf/dispatch + [:keycard/connect-and-sign-payloads + {:keycard-pin pin + :payloads sign-payload + :on-success (fn [signatures] + (rf/dispatch [:hide-bottom-sheet]) + (on-sign-success signatures)) + :on-failure on-sign-error}]))}]] + + + + :else + [:dispatch + [:standard-auth/authorize + (assoc args + :on-auth-success + (fn [masked-password] + (rf/dispatch [:standard-auth/sign-on-device + {:masked-password masked-password + :sign-payload sign-payload + :on-success on-sign-success + :on-error on-sign-error}])))]])]})) + +(schema/=> authorize-and-sign events-schema/?authorize-and-sign) +(rf/reg-event-fx :standard-auth/authorize-and-sign authorize-and-sign) + +(defn sign-on-device + [_ [{:keys [masked-password sign-payload on-success on-error]}]] + {:fx [[:effects.wallet/sign-payloads + {:payloads sign-payload + :password masked-password + :on-success on-success + :on-error on-error}]]}) + +(rf/reg-event-fx :standard-auth/sign-on-device sign-on-device) + +(defn authorize + [{:keys [db]} [args]] + {:fx [(cond + (utils/biometrics-enabled? db) [:dispatch [:standard-auth/authorize-with-biometric args]] + (utils/keycard-profile? db) [:dispatch [:standard-auth/authorize-with-keycard-key args]] + :else [:dispatch [:standard-auth/authorize-with-password args]])]}) + +(schema/=> authorize events-schema/?authorize) +(rf/reg-event-fx :standard-auth/authorize authorize) + +(defn authorize-with-biometric + [_ [{:keys [on-auth-success on-auth-fail] :as args}]] + {:fx [[:dispatch [:dismiss-keyboard]] + [:dispatch + [:biometric/authenticate + {:prompt-message (i18n/label :t/biometric-auth-confirm-message) + :on-cancel #(rf/dispatch [:standard-auth/fallback-for-biometrics args]) + :on-success (fn [] + (rf/dispatch [:standard-auth/on-biometric-success on-auth-success])) + :on-fail (fn [err] + (rf/dispatch [:standard-auth/fallback-for-biometrics args]) + (when on-auth-fail (on-auth-fail err)) + (rf/dispatch [:standard-auth/on-biometric-fail err]))}]]]}) + +(defn fallback-for-biometrics + [{:keys [db]} [args]] + {:fx [(if (utils/keycard-profile? db) + [:dispatch [:standard-auth/authorize-with-keycard-key args]] + [:dispatch [:standard-auth/authorize-with-password args]])]}) + +(rf/reg-event-fx :standard-auth/fallback-for-biometrics fallback-for-biometrics) + +(schema/=> authorize-with-biometric events-schema/?authorize-with-biometric) +(rf/reg-event-fx :standard-auth/authorize-with-biometric authorize-with-biometric) + +(defn get-keychain-key + [{:keys [db]} [on-done]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [(if (utils/keycard-profile? db) + [:keychain/get-keycard-keys [key-uid on-done]] + [:keychain/get-user-password [key-uid on-done]])]})) + +(rf/reg-event-fx :standard-auth/get-keychain-key get-keychain-key) + +(defn on-biometric-success + [_ [on-auth-success]] + {:fx [[:dispatch + [:standard-auth/get-keychain-key + (fn [masked-key] + (rf/dispatch [:standard-auth/finish-auth + {:on-auth-success on-auth-success + :masked-password masked-key}]))]]]}) + +(schema/=> on-biometric-success events-schema/?on-biometric-success) +(rf/reg-event-fx :standard-auth/on-biometric-success on-biometric-success) + +(defn on-biometric-fail + [_ [error]] + (log/error (ex-message error) + (-> error + ex-data + (assoc :code (ex-cause error) + :event :standard-auth/on-biometric-fail))) + {:fx [[:dispatch [:standard-auth/reset-login-password]] + [:dispatch [:biometric/show-message (ex-cause error)]]]}) + +(schema/=> on-biometric-fail events-schema/?on-biometrics-fail) +(rf/reg-event-fx :standard-auth/on-biometric-fail on-biometric-fail) + +(defn- bottom-sheet-password-view + [{:keys [on-auth-success auth-button-icon-left auth-button-label hide-biometrics-button?] :as args}] + (fn [] + (let [auth-method (rf/sub [:auth-method]) + biometric-enabled? (= auth-method keychain/auth-method-biometric)] + [enter-password/view + {:on-enter-password #(rf/dispatch [:standard-auth/finish-auth + {:masked-password (security/hash-masked-password %) + :on-auth-success on-auth-success}]) + :on-press-biometrics (when (and (not hide-biometrics-button?) biometric-enabled?) + #(rf/dispatch [:standard-auth/authorize-with-biometric args])) + :button-icon-left auth-button-icon-left + :button-label auth-button-label}]))) + +(defn authorize-with-password + [_ [{:keys [on-close theme blur?] :as args}]] + {:fx [[:dispatch [:standard-auth/reset-login-password]] + [:dispatch + [:show-bottom-sheet + {:on-close #(rf/dispatch [:standard-auth/close on-close]) + :theme theme + :shell? blur? + :content #(bottom-sheet-password-view args)}]]]}) + +(schema/=> authorize-with-password events-schema/?authorize-with-password) +(rf/reg-event-fx :standard-auth/authorize-with-password authorize-with-password) + +(rf/reg-event-fx + :standard-auth/reset-login-password + (fn [{:keys [db]}] + {:db (-> db + (update :profile/login dissoc :password :error) + (update :keycard dissoc :pin))})) + +(rf/reg-fx + :standard-auth/on-close + (fn [{:keys [on-close success?]}] + (when on-close + (on-close success?)))) + +(rf/reg-fx + :effects.standard-auth/on-auth-success + (fn [on-auth-success] + (when on-auth-success + (on-auth-success)))) + +(rf/reg-event-fx + :standard-auth/close + (fn [{:keys [db]} [on-close]] + {:db (assoc-in db [:profile/login :success?] false) + :fx [[:dispatch [:standard-auth/reset-login-password]] + [:standard-auth/on-close + {:on-close on-close + :success? (get-in db [:profile/login :success?])}]]})) + +(rf/reg-event-fx + :standard-auth/set-success + (fn [{:keys [db]} [success?]] + {:db (assoc-in db [:profile/login :success?] success?)})) + +(defn- finish-auth + [{:keys [db]} [{:keys [masked-password on-auth-success]}]] + (let [on-auth-success-callback #(on-auth-success masked-password) + has-partially-operable-accounts? (-> (get-in db [:wallet :accounts]) + data-store/partially-operable-accounts?) + keycard-profile? (utils/keycard-profile? db)] + {:fx [[:dispatch [:standard-auth/set-success true]] + [:dispatch [:standard-auth/reset-login-password]] + (if (and has-partially-operable-accounts? (not keycard-profile?)) + [:dispatch + [:wallet/make-partially-operable-accounts-fully-operable + {:password masked-password + :on-success on-auth-success-callback + :on-error on-auth-success-callback}]] + [:effects.standard-auth/on-auth-success on-auth-success-callback])]})) + +(rf/reg-event-fx :standard-auth/finish-auth finish-auth) + +(rf/reg-fx + :effects.standard-auth/on-auth-success + (fn [on-auth-success] + (when on-auth-success + (on-auth-success)))) diff --git a/src/status_im/common/standard_authentication/events_schema.cljs b/src/status_im/common/standard_authentication/events_schema.cljs new file mode 100644 index 00000000000..4db7c388628 --- /dev/null +++ b/src/status_im/common/standard_authentication/events_schema.cljs @@ -0,0 +1,80 @@ +(ns status-im.common.standard-authentication.events-schema) + +(def ^:private ?authorize-map + [:map {:closed true} + [:on-auth-success fn?] + [:on-auth-fail {:optional true} [:maybe fn?]] + [:on-close {:optional true} [:maybe fn?]] + [:auth-button-label {:optional true} [:maybe string?]] + [:auth-button-icon-left {:optional true} [:maybe keyword?]] + [:blur? {:optional true} [:maybe boolean?]] + [:theme {:optional true} [:maybe :schema.common/theme]]]) + +(def ^:private ?sign-payload + [:sequential {:min 1} + [:map {:closed true} + [:message :string] + [:address :string]]]) + +(def ?authorize-and-sign + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:tuple + [:map {:closed true} + [:sign-payload ?sign-payload] + [:on-sign-success fn?] + [:on-sign-error {:optional true} fn?] + [:on-close {:optional true} [:maybe fn?]] + [:auth-button-label {:optional true} [:maybe string?]] + [:auth-button-icon-left {:optional true} [:maybe keyword?]] + [:blur? {:optional true} [:maybe boolean?]] + [:theme {:optional true} [:maybe :schema.common/theme]]]]]] + :any]) + +(def ?authorize + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:tuple ?authorize-map]]] + :any]) + +(def ?authorize-with-biometric + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:tuple ?authorize-map]]] + :any]) + +(def ?on-biometric-success + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:tuple fn?]]] + :any]) + +(def ?on-biometrics-fail + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:tuple + [:maybe fn?] + [:maybe :schema.common/exception]]]] + :any]) + +(def ?authorize-with-password + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:tuple + [:merge + ?authorize-map + [:map {:closed true} + [:hide-biometrics-button? {:optional true} boolean?]]]]]] + :any]) diff --git a/src/status_im/common/standard_authentication/events_test.cljs b/src/status_im/common/standard_authentication/events_test.cljs new file mode 100644 index 00000000000..c3ce1473d8e --- /dev/null +++ b/src/status_im/common/standard_authentication/events_test.cljs @@ -0,0 +1,93 @@ +(ns status-im.common.standard-authentication.events-test + (:require [cljs.test :refer [are deftest is testing]] + matcher-combinators.test + [status-im.common.standard-authentication.events :as sut])) + +(def profile-keypair + {:key-uid "profile-key-uid" + :name "profile" + :type :profile + :lowest-operability :fully + :accounts [{:path "m/44'/60'/0'/0/0" + :key-uid "profile-key-uid" + :address "0x1" + :wallet true + :type "generated" + :operable :fully} + {:path "m/44'/60'/0'/0/1" + :key-uid "profile-key-uid" + :address "0x2" + :wallet true + :type "generated" + :operable :fully}]}) + +(def second-keypair + {:key-uid "second-key-uid" + :name "second" + :type :seed + :lowest-operability :fully + :accounts [{:path "m/44'/60'/0'/0/0" + :key-uid "second-key-uid" + :address "0x3" + :wallet true + :type "generated" + :operable :fully}]}) + +(def default-cofx + {:db {:wallet {:keypairs {"profile-key-uid" profile-keypair}}}}) + +(deftest authorize-and-sign-test + (testing "sign-payload not passing schema validation" + (are [args] (thrown? js/Error (sut/authorize-and-sign default-cofx [args])) + {:sign-payload [] + :on-sign-success identity} + {:sign-payload [{:address "0x0"}] + :on-sign-success identity} + {:sign-payload [{:message "0x0"}] + :on-sign-success identity} + {:sign-payload [{:message "0x0" + :address "0x1"}]} + {:on-sign-success identity})) + + (testing "sign-payload is passing schema validation" + (let [args {:sign-payload [{:message "0x0" :address "0x1"} + {:message "0x0" :address "0x2"}] + :on-sign-success identity}] + (is (match? {} (sut/authorize-and-sign default-cofx [args]))))) + + (testing "signing-payload with addresses from multiple keypairs is unsupported for keycard" + (let [cofx {:db {:wallet {:keypairs {"profile-key-uid" profile-keypair + "second-key-uid" second-keypair}} + :profile/profile {:keycard-pairing "pairing-key"}}} + args {:sign-payload [{:message "0x0" :address "0x1"} + {:message "0x0" :address "0x2"} + {:message "0x0" :address "0x3"}] + :on-sign-success identity} + expected {:fx [[:dispatch [:keycard/feature-unavailable-show {}]]]}] + (is (match? expected (sut/authorize-and-sign cofx [args]))))) + + (testing "signing with the keycard" + (let [keycard-keypair (assoc profile-keypair :keycards [{:key-uid "profile-key-uid"}]) + cofx {:db {:wallet {:keypairs {"profile-key-uid" keycard-keypair}} + :profile/profile {:keycard-pairing "pairing-key"}}} + args {:sign-payload [{:message "0x0" :address "0x1"} + {:message "0x0" :address "0x2"}] + :on-sign-success identity} + expected {:fx [[:dispatch [:standard-auth/authorize-with-keycard {}]]]}] + (is (match? expected (sut/authorize-and-sign cofx [args]))))) + + (testing "signing locally with encryption key stored on keycard" + (let [cofx {:db {:wallet {:keypairs {"profile-key-uid" profile-keypair}} + :profile/profile {:keycard-pairing "pairing-key"}}} + args {:sign-payload [{:message "0x0" :address "0x1"} + {:message "0x0" :address "0x2"}] + :on-sign-success identity} + expected {:fx [[:dispatch [:standard-auth/authorize {}]]]}] + (is (match? expected (sut/authorize-and-sign cofx [args]))))) + + (testing "signing locally with password/biometric" + (let [args {:sign-payload [{:message "0x0" :address "0x1"} + {:message "0x0" :address "0x2"}] + :on-sign-success identity} + expected {:fx [[:dispatch [:standard-auth/authorize {}]]]}] + (is (match? expected (sut/authorize-and-sign default-cofx [args])))))) diff --git a/src/status_im/common/standard_authentication/forgot_password_doc/style.cljs b/src/status_im/common/standard_authentication/forgot_password_doc/style.cljs new file mode 100644 index 00000000000..cca8434ce13 --- /dev/null +++ b/src/status_im/common/standard_authentication/forgot_password_doc/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.common.standard-authentication.forgot-password-doc.style) + +(def container {:margin-right 16}) +(def step-container {:flex-direction :row :margin-top 14}) +(def step-content {:margin-left 10}) +(def step-title {:flex-direction :row}) diff --git a/src/status_im/common/standard_authentication/forgot_password_doc/view.cljs b/src/status_im/common/standard_authentication/forgot_password_doc/view.cljs new file mode 100644 index 00000000000..4dfe9a7e331 --- /dev/null +++ b/src/status_im/common/standard_authentication/forgot_password_doc/view.cljs @@ -0,0 +1,48 @@ +(ns status-im.common.standard-authentication.forgot-password-doc.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.standard-authentication.forgot-password-doc.style :as style] + [utils.i18n :as i18n])) + +(defn view + [{:keys [shell?]}] + [quo/documentation-drawers + {:title (i18n/label :t/forgot-your-password-info-title) + :shell? shell?} + [rn/view {:style style/container} + [quo/text {:size :paragraph-2} (i18n/label :t/forgot-your-password-info-description)] + + [rn/view {:style style/step-container} + [quo/step {:in-blur-view? shell?} 1] + [rn/view {:style style/step-content} + [quo/text {:size :paragraph-2 :weight :semi-bold} + (i18n/label :t/forgot-your-password-info-remove-app)] + [quo/text {:size :paragraph-2} (i18n/label :t/forgot-your-password-info-remove-app-description)]]] + + [rn/view {:style style/step-container} + [quo/step {:in-blur-view? shell?} 2] + [rn/view + {:style style/step-content} + [quo/text {:size :paragraph-2 :weight :semi-bold} + (i18n/label :t/forgot-your-password-info-reinstall-app)] + [quo/text {:size :paragraph-2} + (i18n/label :t/forgot-your-password-info-reinstall-app-description)]]] + + [rn/view {:style style/step-container} + [quo/step {:in-blur-view? shell?} 3] + [rn/view {:style style/step-content} + [rn/view {:style style/step-title} + [quo/text {:size :paragraph-2} (str (i18n/label :t/sign-up) " ")] + [quo/text {:size :paragraph-2 :weight :semi-bold} + (i18n/label :t/forgot-your-password-info-signup-with-key)]] + [quo/text {:size :paragraph-2} + (i18n/label :t/forgot-your-password-info-signup-with-key-description)]]] + + [rn/view {:style style/step-container} + [quo/step {:in-blur-view? shell?} 4] + [rn/view {:style style/step-content} + [quo/text {:size :paragraph-2 :weight :semi-bold} + (i18n/label :t/forgot-your-password-info-create-new-password)] + [quo/text {:size :paragraph-2} + (i18n/label :t/forgot-your-password-info-create-new-password-description)]]]]]) diff --git a/src/status_im/common/standard_authentication/keycard_events.cljs b/src/status_im/common/standard_authentication/keycard_events.cljs new file mode 100644 index 00000000000..bec0d2d7535 --- /dev/null +++ b/src/status_im/common/standard_authentication/keycard_events.cljs @@ -0,0 +1,77 @@ +(ns status-im.common.standard-authentication.keycard-events + (:require [status-im.contexts.keycard.pin.view :as keycard.pin] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn authorize-with-keycard + [_ [{:keys [on-complete]}]] + {:fx [[:dispatch + [:show-bottom-sheet + {:hide-on-background-press? false + :drag-content? false + :on-close #(rf/dispatch [:standard-auth/reset-login-password]) + :content (fn [] + [keycard.pin/auth-sheet {:on-complete on-complete}])}]]]}) +(rf/reg-event-fx :standard-auth/authorize-with-keycard authorize-with-keycard) + +(defn- authorize-with-keycard-key + [{:keys [db]} [{:keys [on-auth-success on-auth-fail]}]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:dispatch + [:standard-auth/authorize-with-keycard + {:on-complete + (fn [pin] + (rf/dispatch + [:keycard/connect + {:key-uid key-uid + :on-success + (fn [] + (rf/dispatch + [:keycard/get-keys + {:pin pin + :on-success #(rf/dispatch [:standard-auth/on-keycard-key-success % + on-auth-success]) + :on-failure #(rf/dispatch [:standard-auth/on-keycard-key-fail % + on-auth-fail])}]))}]))}]]]})) +(rf/reg-event-fx :standard-auth/authorize-with-keycard-key authorize-with-keycard-key) + +(defn on-keycard-key-success + [_ [key-data on-auth-success]] + {:fx [[:dispatch [:keycard/disconnect]] + [:dispatch + [:standard-auth/finish-auth + {:on-auth-success on-auth-success + :masked-password (-> key-data + :encryption-public-key + security/mask-data)}]]]}) +(rf/reg-event-fx :standard-auth/on-keycard-key-success on-keycard-key-success) + +(defn- authorize-with-keycard-keys + [{:keys [db]} [{:keys [on-auth-success]}]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:dispatch + [:standard-auth/authorize-with-keycard + {:on-complete + (fn [pin] + (rf/dispatch + [:keycard/connect + {:key-uid key-uid + :on-success + (fn [] + (rf/dispatch + [:keycard/get-keys + {:pin pin + :on-success #(on-auth-success (security/mask-data %)) + :on-failure #(rf/dispatch [:standard-auth/on-keycard-key-fail])}]))}]))}]]]})) +(rf/reg-event-fx :standard-auth/authorize-with-keycard-keys authorize-with-keycard-keys) + +(defn on-keycard-key-fail + [_ [error on-auth-fail]] + {:fx [[:dispatch [:keycard/on-action-with-pin-error error]] + [:effects.standard-auth/on-keycard-key-fail error on-auth-fail]]}) +(rf/reg-event-fx :standard-auth/on-keycard-key-fail on-keycard-key-fail) + +(rf/reg-fx :effects.standard-auth/on-keycard-key-fail + (fn [error on-auth-fail] + (when on-auth-fail + (on-auth-fail error)))) diff --git a/src/status_im/common/standard_authentication/password_input/style.cljs b/src/status_im/common/standard_authentication/password_input/style.cljs new file mode 100644 index 00000000000..33ff117c37a --- /dev/null +++ b/src/status_im/common/standard_authentication/password_input/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.common.standard-authentication.password-input.style) + +(def error-message + {:margin-top 8 + :flex-direction :row + :gap 4 + :align-items :center}) + +(def auth-button + {:align-self :flex-end + :margin-left 8}) diff --git a/src/status_im/common/standard_authentication/password_input/view.cljs b/src/status_im/common/standard_authentication/password_input/view.cljs new file mode 100644 index 00000000000..4ea34df9701 --- /dev/null +++ b/src/status_im/common/standard_authentication/password_input/view.cljs @@ -0,0 +1,83 @@ +(ns status-im.common.standard-authentication.password-input.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.biometric.utils :as biometric] + [status-im.common.standard-authentication.forgot-password-doc.view :as forgot-password-doc] + [status-im.common.standard-authentication.password-input.style :as style] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn- error-info + [error-message processing shell?] + (let [theme (quo.context/use-theme) + on-press (rn/use-callback + (fn [] + (rn/dismiss-keyboard!) + (rf/dispatch [:show-bottom-sheet + {:content #(forgot-password-doc/view {:shell? shell?}) + :theme theme + :shell? shell?}])) + [theme])] + [rn/view {:style style/error-message} + [quo/info-message + {:status :error + :size :default + :icon :i/info} + error-message] + [rn/pressable + {:hit-slop {:top 6 :bottom 20 :left 0 :right 0} + :disabled processing + :on-press on-press} + [rn/text + {:style {:text-decoration-line :underline + :color (colors/resolve-color :danger theme)} + :size :paragraph-2 + :suppress-highlighting true} + (i18n/label :t/forgot-password)]]])) + +(defn view + [{:keys [shell? on-press-biometrics blur? processing error]}] + (let [{:keys [error? + error-message]} error + default-value (rn/use-ref-atom "") ;;bug on Android + ;;https://github.com/status-im/status-mobile/issues/19004 + theme (quo.context/use-theme) + on-change-password (rn/use-callback + (fn [entered-password] + (reset! default-value entered-password) + (debounce/debounce-and-dispatch + [:profile/on-password-input-changed + {:password (security/mask-data + entered-password) + :error ""}] + 100))) + biometric-type (rf/sub [:biometrics/supported-type])] + [:<> + [rn/view {:style {:flex-direction :row}} + [quo/input + {:container-style {:flex 1} + :type :password + :theme theme + :default-value @default-value + :blur? blur? + :disabled? processing + :placeholder (i18n/label :t/type-your-password) + :auto-focus true + :error? error? + :label (i18n/label :t/profile-password) + :on-change-text on-change-password}] + (when on-press-biometrics + [quo/button + {:container-style style/auth-button + :on-press on-press-biometrics + :icon-only? true + :background (when blur? :blur) + :type :outline} + (biometric/get-icon-by-type biometric-type)])] + (when error? + [error-info error-message processing shell?])])) diff --git a/src/status_im/common/standard_authentication/slide_auth.cljs b/src/status_im/common/standard_authentication/slide_auth.cljs new file mode 100644 index 00000000000..411a2b1ded4 --- /dev/null +++ b/src/status_im/common/standard_authentication/slide_auth.cljs @@ -0,0 +1,37 @@ +(ns status-im.common.standard-authentication.slide-auth + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [track-text customization-color auth-button-label on-success on-fail + auth-button-icon-left size blur? container-style disabled? dependencies + sign-payload] + :or {container-style {:flex 1}}}] + (let [theme (quo.context/use-theme) + slider-icon (rf/sub [:standard-auth/slider-icon]) + on-slider-complete (rn/use-callback + (fn [] + (rf/dispatch [:standard-auth/authorize + {:theme theme + :blur? blur? + :on-auth-success on-success + :on-auth-fail on-fail + :auth-button-label (or auth-button-label + (i18n/label :t/continue)) + :auth-button-icon-left auth-button-icon-left}])) + (vec (conj dependencies + on-success + on-fail + sign-payload)))] + [quo/slide-button + {:container-style container-style + :size size + :customization-color customization-color + :on-complete on-slider-complete + :track-icon slider-icon + :track-text track-text + :disabled? disabled?}])) diff --git a/src/status_im/common/standard_authentication/slide_sign.cljs b/src/status_im/common/standard_authentication/slide_sign.cljs new file mode 100644 index 00000000000..d1e441750ff --- /dev/null +++ b/src/status_im/common/standard_authentication/slide_sign.cljs @@ -0,0 +1,36 @@ +(ns status-im.common.standard-authentication.slide-sign + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [utils.re-frame :as rf])) + +(defn view + [{:keys [sign-payload on-success on-fail track-text customization-color auth-button-label + auth-button-icon-left size blur? container-style disabled? dependencies] + :or {container-style {:flex 1}}}] + (let [theme (quo.context/use-theme) + slider-icon (rf/sub [:standard-auth/slider-icon]) + on-slider-complete (rn/use-callback + (fn [] + (when (seq sign-payload) + (rf/dispatch [:standard-auth/authorize-and-sign + {:sign-payload sign-payload + :theme theme + :blur? blur? + :on-sign-success on-success + :on-sign-error on-fail + :auth-button-label auth-button-label + :auth-button-icon-left auth-button-icon-left}]))) + (vec (conj dependencies + on-success + on-fail + sign-payload)))] + [quo/slide-button + {:container-style container-style + :size size + :customization-color customization-color + :on-complete on-slider-complete + :track-icon slider-icon + :track-text track-text + :disabled? disabled?}])) diff --git a/src/status_im/common/standard_authentication/utils.cljs b/src/status_im/common/standard_authentication/utils.cljs new file mode 100644 index 00000000000..45bb0ec0ab1 --- /dev/null +++ b/src/status_im/common/standard_authentication/utils.cljs @@ -0,0 +1,50 @@ +(ns status-im.common.standard-authentication.utils + (:require [clojure.string :as string] + [status-im.common.keychain.events :as keychain])) + +(defn keycard-profile? + [db] + (boolean (get-in db [:profile/profile :keycard-pairing]))) + +(defn profile-keypair-keycards? + [{:keys [type keycards]}] + (and (= :profile type) (seq keycards))) + +(defn keycard-account? + [keypairs address] + (let [find-keycard-keypair (fn [kps] (some #(when (profile-keypair-keycards? %) %) kps)) + keypair-addresses (fn [kp] + (->> (:accounts kp) + (map :address) + set))] + (-> keypairs + vals + find-keycard-keypair + keypair-addresses + (contains? (string/lower-case address))))) + +(defn- keypair-by-address + [keypairs address] + (some (fn [keypair] + (let [keypair-addresses (->> keypair + :accounts + (map :address) + set)] + (when (contains? keypair-addresses address) + keypair))) + (vals keypairs))) + +(defn payloads-for-different-keypairs? + "Returns true if the payloads are for addresses that are derived from different keypairs" + [keypairs payloads] + (let [address-to-keypair (partial keypair-by-address keypairs) + payload-keypair-ids (->> payloads + (map :address) + (map address-to-keypair) + (map :key-uid) + set)] + (> (count payload-keypair-ids) 1))) + +(defn biometrics-enabled? + [db] + (= (:auth-method db) keychain/auth-method-biometric)) diff --git a/src/status_im/common/terms/view.cljs b/src/status_im/common/terms/view.cljs new file mode 100644 index 00000000000..362e31933d1 --- /dev/null +++ b/src/status_im/common/terms/view.cljs @@ -0,0 +1,20 @@ +(ns status-im.common.terms.view + (:require-macros [legacy.status-im.utils.slurp :refer [slurp]]) + (:require [quo.core :as quo] + [react-native.gesture :as gesture] + [status-im.contexts.settings.common.header :as header] + [utils.i18n :as i18n])) + +(def terms-of-use-text (slurp "resources/terms-of-use.mdwn")) + +(defn terms-of-use + [] + [gesture/scroll-view {:style {:margin 20}} + [quo/text terms-of-use-text]]) + +(defn view + [] + [quo/overlay {:type :shell} + [header/view {:title (i18n/label :t/terms-of-service)}] + [gesture/scroll-view {:style {:padding-horizontal 20}} + [quo/text terms-of-use-text]]]) diff --git a/src/status_im/common/theme/core.cljs b/src/status_im/common/theme/core.cljs new file mode 100644 index 00000000000..64df9a5350f --- /dev/null +++ b/src/status_im/common/theme/core.cljs @@ -0,0 +1,37 @@ +(ns status-im.common.theme.core + (:require + [legacy.status-im.ui.components.colors :as legacy-colors] + [oops.core :refer [oget]] + [react-native.core :as rn] + [react-native.platform :as platform] + [utils.re-frame :as rf])) + +(def device-theme (atom (rn/get-color-scheme))) + +(defn change-device-theme + [theme] + (when-not (= theme @device-theme) + (reset! device-theme theme) + (rf/dispatch [:system-theme-mode-changed (keyword theme)]))) + +;; Appearance change listener fires false events in ios when the app is in the background +;; So, we are ignoring those events and when the device returns form the background, +;; we are manually checking the device theme, in ::app-state-change-fx +;; https://github.com/status-im/status-mobile/issues/15708 +(defn add-device-theme-change-listener + [] + (rn/appearance-add-change-listener + #(when (or platform/android? + (not= (oget rn/app-state "currentState") "background")) + (change-device-theme (oget % "colorScheme"))))) + +(defn device-theme-dark? + [] + (= @device-theme "dark")) + +(defn set-legacy-theme + [value] + (reset! legacy-colors/theme (case value + :dark legacy-colors/dark-theme + legacy-colors/light-theme)) + (legacy-colors/set-legacy-theme-type value)) diff --git a/src/status_im/common/theme/effects.cljs b/src/status_im/common/theme/effects.cljs new file mode 100644 index 00000000000..03cd6ea9698 --- /dev/null +++ b/src/status_im/common/theme/effects.cljs @@ -0,0 +1,8 @@ +(ns status-im.common.theme.effects + (:require + [status-im.common.theme.core :as theme] + [utils.re-frame :as rf])) + +(rf/reg-fx :theme/legacy-theme-fx + (fn [theme] + (theme/set-legacy-theme theme))) diff --git a/src/status_im/common/theme/events.cljs b/src/status_im/common/theme/events.cljs new file mode 100644 index 00000000000..5cfbb0e6da3 --- /dev/null +++ b/src/status_im/common/theme/events.cljs @@ -0,0 +1,45 @@ +(ns status-im.common.theme.events + (:require + [re-frame.core :as re-frame] + [status-im.common.theme.core :as theme] + [status-im.constants :as constants])) + +(re-frame/reg-fx + :theme/init-theme + (fn [] + (theme/add-device-theme-change-listener))) + +(defn- appearance-type->theme + "Converts appearance type identifier to a theme keyword. + Returns `:light` or `:dark` based on `appearance-type`: + - `appearance-type-light` (1): Light theme. + - `appearance-type-dark` (2): Dark theme. + - `appearance-type-system` (0): Uses system preference." + [appearance-type] + (condp = appearance-type + constants/appearance-type-dark :dark + constants/appearance-type-light :light + constants/appearance-type-system (if (theme/device-theme-dark?) :dark :light) + :dark)) + +;; Switches the theme and triggers related effects. +;; +;; Parameters: +;; - `theme`: Optional theme keyword to apply. If not provided, defaults to the theme determined by +;; `appearance-type`. +;; - `appearance-type`: Optional appearance type to determine the theme. If not provided, defaults to +;; the appearance type from the user's profile. +;; - `view-id`: Optional view ID for updating status and navigation color. If not provided, uses the +;; current view ID from the database. `view-id` is required because status and navigation bar colors +;; depends on the screen. For example, on the home screen, the navigation color should always be dark, +;; regardless of the theme, due to the bottom tabs. +(re-frame/reg-event-fx + :theme/switch + (fn [{db :db} [{:keys [theme view-id appearance-type]}]] + (let [appearance-type (or appearance-type (get-in db [:profile/profile :appearance])) + theme (or theme (appearance-type->theme appearance-type))] + {:db (assoc db :theme theme) + :fx [[:theme/legacy-theme-fx theme] + [:dispatch + [:reload-status-nav-color + (or view-id (:view-id db))]]]}))) diff --git a/src/status_im/common/toasts/animation.cljs b/src/status_im/common/toasts/animation.cljs new file mode 100644 index 00000000000..efb389956b4 --- /dev/null +++ b/src/status_im/common/toasts/animation.cljs @@ -0,0 +1,58 @@ +(ns status-im.common.toasts.animation + (:require + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated])) + +(def slide-out-up-animation + (-> ^js reanimated/slide-out-up-animation + (.springify) + (.damping 20) + (.stiffness 12))) + +(def slide-in-up-animation + (-> ^js reanimated/slide-in-up-animation + (.springify) + (.damping 20) + (.stiffness 150))) + +(def linear-transition + (-> ^js reanimated/linear-transition + (.springify) + (.damping 20) + (.stiffness 170))) + +(defn- reset-translate-y + ([translate-y] + (reset-translate-y translate-y 0)) + ([translate-y spring-value] + (reanimated/animate-shared-value-with-spring + translate-y + spring-value + {:mass 1 :damping 20 :stiffness 300}))) + +(defn- dismiss + [translate-y set-dismissed-locally close-toast] + (reset-translate-y translate-y -500) + (set-dismissed-locally) + (close-toast)) + +(defn on-update-gesture + [translate-y set-dismissed-locally close-toast] + (fn [^js evt] + (let [evt-translation-y (.-translationY evt) + pan-down? (> evt-translation-y 100) + pan-up? (< evt-translation-y -30)] + (cond + pan-down? (reset-translate-y translate-y) + pan-up? (dismiss translate-y set-dismissed-locally close-toast) + :else (reanimated/set-shared-value translate-y evt-translation-y))))) + +(defn pan-gesture + [{:keys [clear-timer create-timer translate-y close-toast set-dismissed-locally + dismissed-locally?]}] + (-> (gesture/gesture-pan) + (gesture/on-start clear-timer) + (gesture/on-update (on-update-gesture translate-y set-dismissed-locally close-toast)) + (gesture/on-end #(when-not dismissed-locally? + (reanimated/set-shared-value translate-y 0) + (create-timer))))) diff --git a/src/status_im/common/toasts/events.cljs b/src/status_im/common/toasts/events.cljs new file mode 100644 index 00000000000..746321ed3ff --- /dev/null +++ b/src/status_im/common/toasts/events.cljs @@ -0,0 +1,57 @@ +(ns status-im.common.toasts.events + (:require + [utils.re-frame :as rf])) + +(rf/defn upsert + {:events [:toasts/upsert]} + [{:keys [db]} opts] + (let [{:keys [ordered toasts]} (:toasts db) + next-toast-number (get-in db [:toasts :next-toast-number] 1) + id (or (:id opts) + (str "toast-" next-toast-number)) + update? (some #(= % id) ordered) + ordered (if (not update?) + (conj ordered id) + ordered) + toasts (assoc toasts id (dissoc opts :id))] + (cond-> {:db (-> db + (update :toasts assoc :ordered ordered :toasts toasts) + (update :toasts dissoc :hide-toasts-timer-set))} + + (and (not update?) (= (count ordered) 1)) + (assoc :show-toasts [(:view-id db) (or (:theme opts) (:theme db))]) + + (not (:id opts)) + (update-in [:db :toasts :next-toast-number] inc)))) + +(rf/defn hide-toasts-with-check + {:events [:toasts/hide-with-check]} + [{:keys [db]}] + (when (get-in db [:toasts :hide-toasts-timer-set]) + {:db (update db :toasts dissoc :hide-toasts-timer-set) + :hide-toasts nil + :reload-status-nav-color-fx [(:view-id db) (:theme db)]})) + +(rf/defn close + {:events [:toasts/close]} + [{:keys [db]} id] + (when (get-in db [:toasts :toasts id]) + (let [{:keys [toasts ordered]} (:toasts db) + toasts (dissoc toasts id) + ordered (remove #(= % id) ordered) + empty-ordered? (not (seq ordered)) + db (update db :toasts assoc :ordered ordered :toasts toasts) + effect {:db db}] + (if empty-ordered? + (-> effect + (update-in [:db :toasts] assoc :hide-toasts-timer-set true) + (assoc :dispatch-later [{:ms 500 :dispatch [:toasts/hide-with-check]}])) + effect)))) + +(rf/defn close-all-toasts + {:events [:toasts/close-all-toasts]} + [{:keys [db]}] + {:dispatch-n (reduce (fn [acc toast] + (conj acc [:toasts/close (key toast)])) + [] + (get-in db [:toasts :toasts]))}) diff --git a/src/status_im/common/toasts/style.cljs b/src/status_im/common/toasts/style.cljs new file mode 100644 index 00000000000..f635c79631d --- /dev/null +++ b/src/status_im/common/toasts/style.cljs @@ -0,0 +1,15 @@ +(ns status-im.common.toasts.style + (:require [react-native.safe-area :as safe-area])) + +(defn outmost-transparent-container + [] + {:elevation 2 + :pointer-events :box-none + :padding-top (+ safe-area/top 6) + :flex-direction :column + :justify-content :center + :align-items :center}) + +(def each-toast-container + {:width "100%" + :margin-bottom 5}) diff --git a/src/status_im/common/toasts/view.cljs b/src/status_im/common/toasts/view.cljs new file mode 100644 index 00000000000..e541e13da7f --- /dev/null +++ b/src/status_im/common/toasts/view.cljs @@ -0,0 +1,64 @@ +(ns status-im.common.toasts.view + (:require + [quo.core :as quo] + [react-native.background-timer :as background-timer] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [reagent.core :as reagent] + [status-im.common.toasts.animation :as animation] + [status-im.common.toasts.style :as style] + [utils.re-frame :as rf])) + +(defn toast + [toast-id] + (let [{:keys [type user-public-key] :as toast-opts} (rf/sub [:toasts/toast toast-id]) + profile-picture (when user-public-key + (rf/sub [:chats/photo-path user-public-key])) + toast-opts-with-profile-picture (if profile-picture + (assoc-in toast-opts + [:user :profile-picture] + profile-picture) + toast-opts)] + (if (= type :notification) + [quo/notification toast-opts-with-profile-picture] + [quo/toast toast-opts-with-profile-picture]))) + +(defn f-container + [toast-id] + (let [dismissed-locally? (reagent/atom false) + set-dismissed-locally #(reset! dismissed-locally? true) + close-toast #(rf/dispatch [:toasts/close toast-id]) + timer (reagent/atom nil) + clear-timer #(background-timer/clear-timeout @timer)] + (fn [] + (let [duration (or (rf/sub [:toasts/toast-cursor toast-id :duration]) 3000) + on-dismissed (or (rf/sub [:toasts/toast-cursor toast-id :on-dismissed]) identity) + create-timer #(reset! timer (background-timer/set-timeout close-toast duration)) + translate-y (reanimated/use-shared-value 0) + pan-gesture (animation/pan-gesture {:clear-timer clear-timer + :create-timer create-timer + :translate-y translate-y + :close-toast close-toast + :set-dismissed-locally set-dismissed-locally + :dismissed-locally? @dismissed-locally?})] + ;; create auto dismiss timer, clear timer when unmount or duration changed + (rn/use-effect (fn [] (create-timer) clear-timer) [duration]) + (rn/use-unmount #(on-dismissed toast-id)) + + [gesture/gesture-detector {:gesture pan-gesture} + [reanimated/view + {:entering animation/slide-in-up-animation + :exiting animation/slide-out-up-animation + :layout animation/linear-transition + :style (reanimated/apply-animations-to-style + {:transform [{:translateY translate-y}]} + style/each-toast-container)} + [toast toast-id]]])))) + +(defn toasts + [] + (->> (rf/sub [:toasts]) + :ordered + (into [rn/view {:style (style/outmost-transparent-container)}] + (map #(with-meta [f-container %] {:key %}))))) diff --git a/src/status_im/common/universal_links.cljs b/src/status_im/common/universal_links.cljs new file mode 100644 index 00000000000..55a937dfa08 --- /dev/null +++ b/src/status_im/common/universal_links.cljs @@ -0,0 +1,260 @@ +(ns status-im.common.universal-links + (:require + [clojure.string :as string] + [goog.string :as gstring] + [re-frame.core :as re-frame] + [react-native.async-storage :as async-storage] + [react-native.core :as rn] + [schema.core :as schema] + [status-im.constants :as constants] + [status-im.contexts.communities.events :as communities.events] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.ethereum.chain :as chain] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +;; domains should be without the trailing slash +(def domains + {:external "https://status.app" + :internal "status-app:/"}) + +(def links + {:private-chat "%s/p/%s" + :user "%s/u#%s" + :browse "%s/b/%s"}) + +(defn universal-link? + [url] + (boolean + (re-matches constants/regx-universal-link url))) + +(defn deep-link? + [url] + (boolean + (re-matches constants/regx-deep-link url))) + +(defn generate-link + [link-type domain-type param] + (gstring/format (get links link-type) + (get domains domain-type) + param)) + +(rf/defn handle-browse + [_ {:keys [url]}] + (log/info "universal-links: handling browse" url) + {:browser/show-browser-selection url}) + +(rf/defn handle-group-chat + [_ params] + (log/info "universal-links: handling group" params) + {:dispatch [:group-chats/create-from-link params]}) + +(defn own-public-key? + [{:keys [profile/profile]} public-key] + (= (:public-key profile) public-key)) + +(rf/defn handle-private-chat + [{:keys [db]} {:keys [chat-id]}] + (log/info "universal-links: handling private chat" chat-id) + (when chat-id + (if-not (own-public-key? db chat-id) + {:dispatch [:chat.ui/start-chat chat-id]} + {:effects.utils/show-popup {:title (i18n/label :t/unable-to-read-this-code) + :content (i18n/label :t/can-not-add-yourself)}}))) + +(rf/defn handle-community-requests + [cofx {:keys [community-id]}] + (log/info "universal-links: handling community request " community-id) + (navigation/navigate-to cofx :screen/community-requests-to-join {:community-id community-id})) + +(rf/defn handle-view-profile + [{:keys [db] :as cofx} {:keys [public-key ens-name]}] + (log/info "universal-links: handling view profile" public-key) + (cond + (and public-key (own-public-key? db public-key)) + (rf/merge cofx + (navigation/pop-to-root :screen/shell-stack) + (navigation/navigate-to :screen/settings nil)) + + public-key + {:dispatch [:chat.ui/show-profile public-key ens-name]})) + +(rf/defn handle-eip681 + [cofx data] + (rf/merge cofx + {:dispatch [:wallet-legacy/parse-eip681-uri-and-resolve-ens data true]} + (navigation/navigate-to :wallet-legacy nil))) + +(defn existing-account? + [{:keys [db]} address] + (when address + (some #(when (= (string/lower-case (:address %)) + (string/lower-case address)) + %) + (:profile/wallet-accounts db)))) + +(rf/defn handle-wallet-account + [cofx {address :account}] + (when-let [account (existing-account? cofx address)] + (navigation/navigate-to cofx + :screen/wallet.accounts + account))) + +(defn handle-not-found + [full-url] + (log/info "universal-links: no handler for " full-url)) + +(rf/defn handle-wallet-connect + [_ uri] + (log/info "universal-links: handle-wallet-connect" uri) + {:dispatch [:wallet-connect/process-deeplink uri]}) + +(defn dispatch-url + "Dispatch url so we can get access to re-frame/db" + [url] + (if-not (nil? url) + (do + (async-storage/set-item! (str :chat-id) nil #(rf/dispatch [:universal-links/remove-handling])) + (re-frame/dispatch [:universal-links/handling]) + (re-frame/dispatch [:universal-links/handle-url url])) + (log/debug "universal-links: no url"))) + +(rf/defn on-handle + {:events [:universal-links/match-value]} + [cofx url {:keys [type chat-id community-id uri] :as data}] + (case type + :group-chat (handle-group-chat cofx data) + :private-chat (handle-private-chat cofx data) + :community-requests (handle-community-requests cofx data) + :community (communities.events/navigate-to-community-overview cofx [community-id]) + :community-chat (communities.events/navigate-to-community-chat cofx + [chat-id :pop-to-root + community-id]) + :contact (handle-view-profile cofx data) + :browser (handle-browse cofx data) + :eip681 (handle-eip681 cofx data) + :wallet-account (handle-wallet-account cofx data) + :wallet-connect (handle-wallet-connect cofx uri) + (handle-not-found url))) + +(rf/defn route-url + "Match a url against a list of routes and handle accordingly" + [{:keys [db]} url] + {:router/handle-uri {:chain (chain/chain-keyword db) + :chats (:chats db) + :uri url + :cb #(re-frame/dispatch [:universal-links/match-value url %])}}) + +(rf/defn store-url-for-later + "Store the url in the db to be processed on login" + [{:keys [db]} url] + (log/info :store-url-for-later) + {:db (assoc db :universal-links/url url)}) + +(rf/defn handling + {:events [:universal-links/handling]} + [{:keys [db]}] + {:db (assoc db :universal-links/handling true)}) + +(rf/defn remove-handling + {:events [:universal-links/remove-handling]} + [{:keys [db]}] + {:db (dissoc db :universal-links/handling)}) + +(rf/defn handle-url + "Store url in the database if the user is not logged in, to be processed + on login, otherwise just handle it." + {:events [:universal-links/handle-url]} + [{:keys [db] :as cofx} url] + (if (:profile/profile db) + (route-url cofx url) + (store-url-for-later cofx url))) + +(rf/defn process-stored-event + "Return an event description for processing a url if in the database" + {:events [:universal-links/process-stored-event]} + [{:keys [db] :as cofx}] + (when-let [url (:universal-links/url db)] + (rf/merge cofx + {:db (dissoc db :universal-links/url)} + (handle-url url)))) + +(defn generate-profile-url + [{:keys [db]} [{:keys [public-key on-success]}]] + (let [profile-public-key (get-in db [:profile/profile :public-key]) + profile? (or (not public-key) (= public-key profile-public-key)) + public-key (if profile? profile-public-key public-key)] + (when public-key + {:json-rpc/call + [{:method "wakuext_shareUserURLWithData" + :params [public-key] + :on-success (fn [url] + (rf/dispatch [:universal-links/save-profile-url public-key url]) + (when (fn? on-success) (on-success url))) + :on-error #(log/error "failed to wakuext_shareUserURLWithData" + {:error % + :public-key public-key})}]}))) + +(schema/=> generate-profile-url + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:schema + [:? + [:map + [:public-key {:optional true} :schema.common/public-key] + [:on-success {:optional true} fn?]]]]]] + [:map + [:json-rpc/call :schema.common/rpc-call]]]) + +(rf/reg-event-fx :universal-links/generate-profile-url generate-profile-url) + +(defn save-profile-url + [{:keys [db]} [public-key url]] + (let [contact-from-key (get-in db [:contacts/contacts public-key]) + profile-public-key? (= public-key (get-in db [:profile/profile :public-key]))] + (when url + {:db (cond-> db + contact-from-key + (assoc-in [:contacts/contacts public-key :universal-profile-url] url) + + profile-public-key? + (assoc-in [:profile/profile :universal-profile-url] url))}))) + +(schema/=> save-profile-url + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:schema [:cat :schema.common/public-key :string]]]] + [:maybe :map]]) + +(rf/reg-event-fx :universal-links/save-profile-url save-profile-url) + +(defn unwrap-js-url + [e] + (-> e + (js->clj :keywordize-keys true) + :url)) + +(def url-event-listener + (comp dispatch-url unwrap-js-url)) + +(defn initialize + "Add an event listener for handling background->foreground transition + and handles incoming url if the app has been started by clicking on a link" + [] + (log/debug "universal-links: initializing") + ;;NOTE: https://github.com/facebook/react-native/issues/15961 + ;; workaround for getInitialURL returning null when opening the + ;; app from a universal link after closing it with the back button + (js/setTimeout #(-> (.getInitialURL ^js rn/linking) + (.then dispatch-url)) + 200) + (.addEventListener ^js rn/linking "url" url-event-listener) + ;;StartSearchForLocalPairingPeers() shouldn't be called ATM from the UI It can be called after the + ;;error "route ip+net: netlinkrib: permission denied" is fixed on status-go side + #_(native-module/start-searching-for-local-pairing-peers + #(log/info "[local-pairing] errors from local-pairing-preflight-outbound-check ->" %))) diff --git a/src/status_im/common/universal_links_test.cljs b/src/status_im/common/universal_links_test.cljs new file mode 100644 index 00000000000..445a9cd77a9 --- /dev/null +++ b/src/status_im/common/universal_links_test.cljs @@ -0,0 +1,133 @@ +(ns status-im.common.universal-links-test + (:require + [cljs.test :refer-macros [deftest is are testing]] + matcher-combinators.test + [re-frame.core :as re-frame] + [status-im.common.universal-links :as links])) + +(def pubkey + "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073") + +(deftest handle-url-test + (testing "the user is not logged in" + (testing "it stores the url for later processing" + (is (match? {:db {:universal-links/url "some-url"}} + (links/handle-url {:db {}} "some-url"))))) + (testing "the user is logged in" + (let [db {:profile/profile {:public-key "pk"} + :app-state "active" + :universal-links/url "some-url"}] + (testing "it clears the url" + (is (nil? (get-in (links/handle-url {:db db} "some-url") + [:db :universal-links/url])))) + (testing "Handle a custom string" + (is (match? (get-in (links/handle-url {:db db} "https://status.app/u#statuse2e") + [:router/handle-uri :uri]) + "https://status.app/u#statuse2e")))))) + +(deftest url-event-listener-test + (testing "the url is not nil" + (testing "it dispatches the url" + (let [actual (atom nil)] + (with-redefs [re-frame/dispatch #(reset! actual %)] + (links/url-event-listener #js {:url "some-url"}) + (is (match? [:universal-links/handle-url "some-url"] @actual)))))) + (testing "the url is nil" + (testing "it does not dispatches the url" + (let [actual (atom nil)] + (with-redefs [re-frame/dispatch #(reset! actual %)] + (links/url-event-listener #js {}) + (is (match? nil @actual))))))) + +(deftest generate-profile-url-test + (testing "user has ens name" + (testing "it calls the ens rpc method with ens name as param" + (let [db {:profile/profile {:ens-name? true :public-key pubkey}} + rst (links/generate-profile-url {:db db} [])] + (are [result expected] (match? result expected) + "wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method) + pubkey (-> rst :json-rpc/call first :params first))))) + (testing "user has no ens name" + (testing "it calls the ens rpc method with public keyas param" + (let [db {:profile/profile {:public-key pubkey}} + rst (links/generate-profile-url {:db db} [])] + (are [result expected] (match? result expected) + "wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method) + pubkey (-> rst :json-rpc/call first :params first))))) + (testing "contact has ens name" + (testing "it calls the ens rpc method with ens name as param" + (let [ens "ensname.eth" + db {:contacts/contacts {pubkey {:ens-name ens}}} + rst (links/generate-profile-url {:db db} [{:public-key pubkey}])] + (are [result expected] (match? result expected) + "wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method) + pubkey (-> rst :json-rpc/call first :params first))))) + (testing "contact has no ens name" + (testing "it calls the ens rpc method with public keyas param" + (let [db {:contacts/contacts {pubkey {:public-key pubkey}}} + rst (links/generate-profile-url {:db db} [{:public-key pubkey}])] + (are [result expected] (match? result expected) + "wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method) + pubkey (-> rst :json-rpc/call first :params first)))))) + +(deftest save-profile-url-test + (testing "given a contact public key and profile url" + (testing "it updates the contact in db" + (let [url "url" + db {:contacts/contacts {pubkey {:public-key pubkey}}} + rst (links/save-profile-url {:db db} [pubkey url])] + (is (match? (get-in rst [:db :contacts/contacts pubkey :universal-profile-url]) url))))) + (testing "given a user public key and profile url" + (testing "it updates the user profile in db" + (let [url "url" + db {:profile/profile {:public-key pubkey}} + rst (links/save-profile-url {:db db} [pubkey url])] + (is (match? (get-in rst [:db :profile/profile :universal-profile-url]) url)))))) + +(deftest universal-link-test + (testing "universal-link?" + (are [l rst] (match? (links/universal-link? l) rst) + "status-app://blah" + false + "http://status.app/blah" + false + "http://status.app/c#zQ3shPyZJnxZK4Bwyx9QsaksNKDYTPmpwPvGSjMYVHoXHeEgB" + true + "http://status.app/u#zQ3shPyZJnxZK4Bwyx9QsaksNKDYTPmpwPvGSjMYVHoXHeEgB" + true + "https://status.app/u/Ow==#zQ3shsKnV5HJMWJR61c6dssWzHszdbLfBoMF1gcLtSQAYdw2d" + true + "https://status.app/c/Ow==#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + true + "https://status.app/c/Ow==#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc111" + false + "https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" + true + "https://status.app/c/G00AAGS9TbI9mSR-ZNmFrhRjNuEeXAAbcAIUaLLJyjMOG3ACJQ12oIHD78QhzO9s_T5bUeU7rnATWJg3mGgTUemrAg==#zQ3shYf5SquxkiY3FmCW6Nz2wuFWFcM6JEdUD62ApjAvE5YPv" + true + "http://status.app/c#zQ3shPyZJnxZK4Bwyx9QsaksNKDYTPmpwPvGSjMYVHoXHeEgBhttp://status.app/c#zQ3shPyZJnxZK4Bwyx9QsaksNKDYTPmpwPvGSjMYVHoXHeEgB" + false + "http://status.app/u#zQ3shPyZJnxZK4Bwyx9QsaksNKDYTPmpwPvGSjMYVHoXHeEgBhttp://status.app/u#zQ3shPyZJnxZK4Bwyx9QsaksNKDYTPmpwPvGSjMYVHoXHeEgB" + false + "https://status.app/u/Ow==#zQ3shsKnV5HJMWJR61c6dssWzHszdbLfBoMF1gcLtSQAYdw2dhttps://status.app/u/Ow==#zQ3shsKnV5HJMWJR61c6dssWzHszdbLfBoMF1gcLtSQAYdw2d" + false + "https://status.app/c/Ow==#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11https://status.app/c/Ow==#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" + false + "https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSjhttps://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" + false + "https://status.app/c/Ow==#zQ3shbmfT3hvh4mKa1v6uAjjyztQEroh8Mfn6Ckegjd7LT3XKhttps://status.app/c#zQ3shbmfT3hvh4mKa1v6uAjjyztQEroh8Mfn6Ckegjd7LT3XK" + false + "https://status.app/blah" + false + "https://status.app/browse/www.аррӏе.com" + false + "https://not.status.im/blah" + false + "http://not.status.im/blah" + false)) + + (testing "deep-link?" + (are [l rst] (match? (links/deep-link? l) rst) + "status-app://blah" true + "http://status.app/blah" false + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" true))) diff --git a/src/status_im/common/validation/general.cljs b/src/status_im/common/validation/general.cljs new file mode 100644 index 00000000000..b1de07cd61a --- /dev/null +++ b/src/status_im/common/validation/general.cljs @@ -0,0 +1,25 @@ +(ns status-im.common.validation.general + (:require + [status-im.constants :as constants] + [utils.emojilib :as emoji])) + +(defn valid-public-key? + [s] + (and (string? s) + (not-empty s) + (boolean (re-matches constants/regx-public-key s)))) + +(defn valid-compressed-key? + [s] + (and (string? s) + (not-empty s) + (boolean (re-matches constants/regx-compressed-key s)))) + +(defn has-emojis? [s] (boolean (re-find emoji/emoji-regex s))) + +(def no-special-chars-regex #"^[a-zA-Z0-9\-_ ]+$") +(defn has-special-characters? + [s] + (and (not (= s "")) + (not (re-find no-special-chars-regex s)))) + diff --git a/src/status_im/common/validation/general_test.cljs b/src/status_im/common/validation/general_test.cljs new file mode 100644 index 00000000000..863a7253187 --- /dev/null +++ b/src/status_im/common/validation/general_test.cljs @@ -0,0 +1,34 @@ +(ns status-im.common.validation.general-test + (:require + [cljs.test :refer-macros [deftest testing is]] + [status-im.common.validation.general :refer [valid-compressed-key?]])) + +(deftest valid-compressed-key-test + (testing "valid" + (is (valid-compressed-key? + "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA"))) + (testing "uncompressed key" + (is + (not + (valid-compressed-key? + "0x048a6773339d11ccf5fd81677b7e54daeec544a1287bd92b725047ad6faa9a9b9f8ea86ed5a226d2a994f5f46d0b43321fd8de7b7997a166e67905c8c73cd37ce")))) + (testing "nil" + (is (not (valid-compressed-key? nil)))) + (testing "empty string" + (is (not (valid-compressed-key? "")))) + (testing "too short" + (is (not (valid-compressed-key? "zQ3FGR5y6U5a6")))) + (testing "too long" + (is (not (valid-compressed-key? + "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA2")))) + (testing "0x prefix" + (is (not (valid-compressed-key? "0xFGR5y6U5a6")))) + (testing "contains I" + (is (not (valid-compressed-key? + "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gI")))) + (testing "contains O" + (is (not (valid-compressed-key? + "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gO")))) + (testing "contains l" + (is (not (valid-compressed-key? + "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gl"))))) diff --git a/src/status_im/common/validation/keypair.cljs b/src/status_im/common/validation/keypair.cljs new file mode 100644 index 00000000000..c7f4c0550a9 --- /dev/null +++ b/src/status_im/common/validation/keypair.cljs @@ -0,0 +1,25 @@ +(ns status-im.common.validation.keypair + (:require [clojure.string :as string] + [status-im.common.validation.general :as validators] + [status-im.constants :as constants] + utils.emojilib + [utils.i18n :as i18n])) + +(defn keypair-too-short? + [s] + (< (-> s str string/trim count) constants/key-pair-name-min-length)) + +(defn keypair-too-long? + [s] + (> (-> s str string/trim count) constants/key-pair-name-max-length)) + +(defn validation-keypair-name + [s existing-keypair-names] + (cond + (string/blank? s) nil + (validators/has-emojis? s) (i18n/label :t/key-name-error-emoji) + (validators/has-special-characters? s) (i18n/label :t/key-name-error-special-char) + (keypair-too-short? s) (i18n/label :t/key-name-error-too-short + {:count constants/key-pair-name-min-length}) + (keypair-too-long? s) (i18n/label :t/your-key-pair-name-is-too-long) + (contains? existing-keypair-names s) (i18n/label :t/key-name-error-taken))) diff --git a/src/status_im/common/validation/keypair_test.cljs b/src/status_im/common/validation/keypair_test.cljs new file mode 100644 index 00000000000..30a5faa8e13 --- /dev/null +++ b/src/status_im/common/validation/keypair_test.cljs @@ -0,0 +1,30 @@ +(ns status-im.common.validation.keypair-test + (:require + [cljs.test :refer-macros [deftest are]] + [status-im.common.validation.keypair :as keypair-validator] + [status-im.constants :as constants] + [utils.i18n :as i18n])) + +(deftest keypair-name-too-short-test + (are [arg expected] + (expected (keypair-validator/keypair-too-short? arg)) + "abc" true? + "abcdef" false?)) + +(deftest keypair-name-too-long-test + (are [arg expected] + (expected (keypair-validator/keypair-too-long? arg)) + (apply str (repeat 25 "a")) true? + "abcdef" false?)) + +(deftest validation-keypair-name-test + (are [arg expected] + (= (keypair-validator/validation-keypair-name arg #{"Collection"}) expected) + nil nil + "" nil + "name !" (i18n/label :t/key-name-error-special-char) + "Hello 😊" (i18n/label :t/key-name-error-emoji) + "abc" (i18n/label :t/key-name-error-too-short + {:count constants/key-pair-name-min-length}) + "Collection" (i18n/label :t/key-name-error-taken) + (apply str (repeat 25 "a")) (i18n/label :t/your-key-pair-name-is-too-long))) diff --git a/src/status_im/common/validation/password.cljs b/src/status_im/common/validation/password.cljs new file mode 100644 index 00000000000..98585b7cc77 --- /dev/null +++ b/src/status_im/common/validation/password.cljs @@ -0,0 +1,37 @@ +(ns status-im.common.validation.password + (:require + [status-im.constants :as constants] + [utils.string :as utils.string])) + +(defn validate-short-enough? + [password] + (utils.string/at-most-n-chars? password + constants/new-password-max-length)) + +(defn validate-long-enough? + [password] + (utils.string/at-least-n-chars? password + constants/new-password-min-length)) + +(defn validate + [password] + (let [validations (juxt + utils.string/has-lower-case? + utils.string/has-upper-case? + utils.string/has-numbers? + utils.string/has-symbols? + validate-long-enough? + validate-short-enough?)] + (->> password + validations + (zipmap (conj constants/password-tips + :long-enough? + :short-enough?))))) + +(def password-tip? (set constants/password-tips)) + +(defn strength + [validations] + (->> validations + (filter #(and (password-tip? (key %)) (val %))) + (count))) diff --git a/src/status_im/common/validation/profile.cljs b/src/status_im/common/validation/profile.cljs new file mode 100644 index 00000000000..fff84eaf15b --- /dev/null +++ b/src/status_im/common/validation/profile.cljs @@ -0,0 +1,64 @@ +(ns status-im.common.validation.profile + (:require [clojure.string :as string] + [status-im.common.validation.general :as validators] + [status-im.constants :as constants] + utils.emojilib + [utils.i18n :as i18n])) + +;; NOTE - validation should match with Desktop +;; https://github.com/status-im/status-desktop/blob/2ba96803168461088346bf5030df750cb226df4c/ui/imports/utils/Constants.qml#L468 +(def min-length 5) + +;; Combine ASCII and Emoji into one regex +(def bio-chars-regex + #"^[\u00a9\u00ae\u2000-\u3300\ud83c\ud000-\udfff\ud83d\ud000-\udfff\ud83e\ud000-\udfff\u0000-\u007F]+$") + +(def common-names ["Ethereum" "Bitcoin"]) + +(defn has-common-names? [s] (pos? (count (filter #(string/includes? s %) common-names)))) + +(defn name-too-short? [s] (< (count (string/trim (str s))) min-length)) + +(defn name-too-long? [s] (> (count (string/trim (str s))) constants/profile-name-max-length)) + +(defn bio-too-long? [s] (> (count (string/trim (str s))) constants/profile-bio-max-length)) + +(defn bio-invalid-characters? + [s] + (not (re-find bio-chars-regex s))) + +(defn validation-name + [s] + (cond + (string/blank? s) nil + (string/ends-with? s "-eth") (i18n/label :t/ending-not-allowed {:ending "-eth"}) + (string/ends-with? s "_eth") (i18n/label :t/ending-not-allowed {:ending "_eth"}) + (string/ends-with? s ".eth") (i18n/label :t/ending-not-allowed {:ending ".eth"}) + (string/starts-with? s " ") (i18n/label :t/start-with-space) + (string/ends-with? s " ") (i18n/label :t/ends-with-space) + (has-common-names? s) (i18n/label :t/are-not-allowed + {:check (i18n/label :t/common-names)}) + (validators/has-emojis? s) (i18n/label :t/are-not-allowed + {:check (i18n/label :t/emojis)}) + (validators/has-special-characters? s) (i18n/label :t/are-not-allowed + {:check (i18n/label :t/special-characters)}) + (name-too-short? s) (i18n/label :t/minimum-characters {:min-chars min-length}) + (name-too-long? s) (i18n/label :t/profile-name-is-too-long))) + +(defn validation-bio + [s] + (cond + (string/blank? s) nil + (bio-invalid-characters? s) (i18n/label :t/invalid-characters-bio) + (bio-too-long? s) (i18n/label :t/bio-is-too-long))) + +(defn validation-nickname + [s] + (cond + (string/blank? s) nil + (validators/has-emojis? s) (i18n/label :t/are-not-allowed + {:check (i18n/label :t/emojis)}) + (validators/has-special-characters? s) (i18n/label :t/are-not-allowed + {:check (i18n/label :t/special-characters)}) + (name-too-short? s) (i18n/label :t/minimum-characters {:min-chars min-length}) + (name-too-long? s) (i18n/label :t/nickname-is-too-long))) diff --git a/src/status_im/common/validation/profile_test.cljs b/src/status_im/common/validation/profile_test.cljs new file mode 100644 index 00000000000..84fd4c9873b --- /dev/null +++ b/src/status_im/common/validation/profile_test.cljs @@ -0,0 +1,53 @@ +(ns status-im.common.validation.profile-test + (:require + [cljs.test :refer-macros [deftest are]] + [status-im.common.validation.general :as validator] + [status-im.common.validation.profile :as profile-validator] + [utils.i18n :as i18n])) + +(deftest has-emojis-test + (are [arg expected] + (expected (validator/has-emojis? arg)) + "Hello 😊" true? + "Hello" false?)) + +(deftest has-common-names-test + (are [arg expected] + (expected (profile-validator/has-common-names? arg)) + "Ethereum" true? + "Hello" false?)) + +(deftest has-special-characters-test + (are [arg expected] + (expected (validator/has-special-characters? arg)) + "@name" true? + "name" false?)) + +(deftest name-too-short-test + (are [arg expected] + (expected (profile-validator/name-too-short? arg)) + "abc" true? + "abcdef" false?)) + +(deftest name-too-long-test + (are [arg expected] + (expected (profile-validator/name-too-long? arg)) + (apply str (repeat 25 "a")) true? + "abcdef" false?)) + +(deftest validation-name-test + (are [arg expected] + (= (profile-validator/validation-name arg) expected) + nil nil + "" nil + "@name" (i18n/label :t/are-not-allowed + {:check (i18n/label :t/special-characters)}) + "name-eth" (i18n/label :t/ending-not-allowed {:ending "-eth"}) + "name_eth" (i18n/label :t/ending-not-allowed {:ending "_eth"}) + "name.eth" (i18n/label :t/ending-not-allowed {:ending ".eth"}) + " name" (i18n/label :t/start-with-space) + "name " (i18n/label :t/ends-with-space) + "Ethereum" (i18n/label :t/are-not-allowed {:check (i18n/label :t/common-names)}) + "Hello 😊" (i18n/label :t/are-not-allowed {:check (i18n/label :t/emojis)}) + "abc" (i18n/label :t/minimum-characters {:min-chars 5}) + (apply str (repeat 25 "a")) (i18n/label :t/profile-name-is-too-long))) diff --git a/src/status_im/config.cljs b/src/status_im/config.cljs new file mode 100644 index 00000000000..3e681e1422e --- /dev/null +++ b/src/status_im/config.cljs @@ -0,0 +1,152 @@ +(ns status-im.config + (:require + [clojure.string :as string] + [react-native.config :as react-native-config] + [react-native.mmkv :as mmkv] + [status-im.constants :as constants] + [utils.ens.core :as utils.ens] + [utils.ethereum.chain :as chain])) + +(def get-config react-native-config/get-config) + +(defn enabled? [v] (= "1" v)) + +(goog-define INFURA_TOKEN "") +(goog-define POKT_TOKEN "3ef2018191814b7e1009b8d9") +(goog-define STATUS_BUILD_PROXY_USER "") +(goog-define STATUS_BUILD_PROXY_PASSWORD "") +(goog-define STATUS_MARKET_DATA_PROXY_URL "") +(goog-define STATUS_MARKET_DATA_PROXY_USER "") +(goog-define STATUS_MARKET_DATA_PROXY_PASSWORD "") +(goog-define OPENSEA_API_KEY "") +(goog-define RARIBLE_MAINNET_API_KEY "") +(goog-define RARIBLE_TESTNET_API_KEY "") +(goog-define ALCHEMY_ETHEREUM_MAINNET_TOKEN "") +(goog-define ALCHEMY_ETHEREUM_SEPOLIA_TOKEN "") +(goog-define ALCHEMY_ARBITRUM_MAINNET_TOKEN "") +(goog-define ALCHEMY_ARBITRUM_SEPOLIA_TOKEN "") +(goog-define ALCHEMY_OPTIMISM_MAINNET_TOKEN "") +(goog-define ALCHEMY_OPTIMISM_SEPOLIA_TOKEN "") +(goog-define ALCHEMY_BASE_MAINNET_TOKEN "") +(goog-define ALCHEMY_BASE_SEPOLIA_TOKEN "") +(goog-define WALLET_CONNECT_PROJECT_ID "87815d72a81d739d2a7ce15c2cfdefb3") +(goog-define STATUS_BUILD_ETH_RPC_PROXY_USER "") +(goog-define STATUS_BUILD_ETH_RPC_PROXY_PASSWORD "") +(goog-define STATUS_BUILD_ETH_RPC_PROXY_URL "") +(goog-define MIXPANEL_APP_ID "") +(goog-define MIXPANEL_APP_TOKEN "") + +(def mainnet-rpc-url (str "https://eth-archival.rpc.grove.city/v1/" POKT_TOKEN)) +(def sepolia-rpc-url (str "https://sepolia-archival.rpc.grove.city/v1/" POKT_TOKEN)) +(def opensea-link "https://opensea.io") +(def opensea-tesnet-link "https://testnets.opensea.io") + +;; These MixPanel values should be injected via the CI since they vary depending +;; on the environment, as we want to keep production and test/development +;; metrics separate. +;; +;; We primarily use two MixPanel projects: the production project is named +;; `status-im`, and the test project is `test.status-im`. +;; +;; During development, do not use the production app ID and token. +(def mixpanel-app-id MIXPANEL_APP_ID) +(def mixpanel-token MIXPANEL_APP_TOKEN) + +(def opensea-api-key OPENSEA_API_KEY) +(def status-proxy-enabled? true) +(def status-proxy-stage-name (get-config :STATUS_PROXY_STAGE_NAME "test")) +(def pairing-popup-disabled? (enabled? (get-config :PAIRING_POPUP_DISABLED "0"))) +(def cached-webviews-enabled? (enabled? (get-config :CACHED_WEBVIEWS_ENABLED 0))) + +(def fdroid? (enabled? (get-config :GOOGLE_FREE "0"))) + +;; NOTE: only disabled in releases +(def blank-preview? (enabled? (get-config :BLANK_PREVIEW "1"))) +(def tooltip-events? (enabled? (get-config :TOOLTIP_EVENTS "0"))) +(def debug-or-pr-build? (enabled? (get-config :DEBUG_OR_PR_BUILD "0"))) +(def debug-webview? (enabled? (get-config :DEBUG_WEBVIEW "0"))) +(def two-minutes-syncing? (enabled? (get-config :TWO_MINUTES_SYNCING "0"))) +(def show-not-implemented-features? (enabled? (get-config :SHOW_NOT_IMPLEMENTED_FEATURES "0"))) + +;; CONFIG VALUES +(defn log-level + [] + (string/upper-case + (mmkv/get-string constants/pre-login-log-level-key (get-config :LOG_LEVEL "")))) +(def api-logging-enabled? (enabled? (get-config :API_LOGGING_ENABLED "0"))) +(def fleet (get-config :FLEET "")) +(def apn-topic (get-config :APN_TOPIC "im.status.ethereum")) +(def max-installations 2) + +(defn env-variable->int + [env-var-name default-value] + (js/parseInt (get-config env-var-name default-value))) + +(def delete-message-for-me-undo-time-limit-ms + (env-variable->int :DELETE_MESSAGE_FOR_ME_UNDO_TIME_LIMIT + constants/delete-message-for-me-undo-time-limit-ms)) + +(def delete-message-undo-time-limit-ms + (env-variable->int :DELETE_MESSAGE_UNDO_TIME_LIMIT + constants/delete-message-undo-time-limit-ms)) + +(def verify-transaction-chain-id (js/parseInt (get-config :VERIFY_TRANSACTION_CHAIN_ID "1"))) +(def verify-transaction-url + (if (= :mainnet (chain/chain-id->chain-keyword verify-transaction-chain-id)) + mainnet-rpc-url + sepolia-rpc-url)) + +(def verify-ens-chain-id (js/parseInt (get-config :VERIFY_ENS_CHAIN_ID "1"))) +(def verify-ens-url + (if (= :mainnet (chain/chain-id->chain-keyword verify-ens-chain-id)) + mainnet-rpc-url + sepolia-rpc-url)) +(def verify-ens-contract-address + (get-config :VERIFY_ENS_CONTRACT_ADDRESS + ((chain/chain-id->chain-keyword verify-ens-chain-id) utils.ens/ens-registries))) + +(def fast-create-community-enabled? + (enabled? (get-config :FAST_CREATE_COMMUNITY_ENABLED "0"))) + +(def fetch-messages-enabled? (enabled? (get-config :FETCH_MESSAGES_ENABLED "1"))) +(def test-networks-enabled? (enabled? (get-config :TEST_NETWORKS_ENABLED "0"))) + +(def mobile-data-syncing-toggle-enabled? + (enabled? (get-config :MOBILE_DATA_SYNCING_TOGGLE_ENABLE "1"))) + +;; Alert banners are disabled for debug builds because alert banners overlay +;; interfere with react-native debug tools, such as inspector and Perf monitor +(def enable-alert-banner? (enabled? (get-config :ENABLE_ALERT_BANNER "0"))) + +;; enable using status backend server or not, otherwise it will use built-in status-go library +;; see doc/use-status-backend-server.md for more details +(goog-define STATUS_BACKEND_SERVER_ENABLED "0") +;; The host should contain an IP address and a port separated by a colon. +;; The port comes from your running status backend server. +;; If you run it by PORT=60000 make run-status-backend , then host will likely be 127.0.0.1:60000 +(goog-define STATUS_BACKEND_SERVER_HOST "") +;; enable media server over https or http +;; if you're using android simulator, set it to "0" +(goog-define STATUS_BACKEND_SERVER_MEDIA_SERVER_ENABLE_TLS "1") +;; /path/to/root/data/dir +;; make sure it exists, it should be in absolute path +(goog-define STATUS_BACKEND_SERVER_ROOT_DATA_DIR "") +;; if you're using android simulator, I suggest set the env variable to "http://10.0.2.2:" +(goog-define STATUS_BACKEND_SERVER_IMAGE_SERVER_URI_PREFIX "https://localhost:") + +(def re-frisk-host (get-config :RE_FRISK_HOST "localhost")) +(def re-frisk-port (get-config :RE_FRISK_PORT "4567")) +;;;; Sentry +;; Documentation: status-go/internal/sentry/README.md +(goog-define SENTRY_DSN_STATUS_GO "") +(def sentry-enabled? (enabled? (get-config :SENTRY_ENABLED "0"))) +(def sentry-dsn-status-go SENTRY_DSN_STATUS_GO) + +;; this flag should only be used for e2e builds, because +;; relying on UI to pull logs, share them somewhere, and +;; download via Appium doesn't seem like a solid long-term +;; approach for QA team +(def use-public-log-dir? (enabled? (get-config :USE_PUBLIC_LOG_DIR "0"))) + +;; A flag for enabling hidden view-id-tracker view, required for e2e testing +(def enable-view-id-tracker? (enabled? (get-config :VIEW_ID_TRACKER "0"))) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index f3e094266cb..90ca9497148 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -1,255 +1,497 @@ (ns status-im.constants - (:require [status-im.i18n :as i18n] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.types :as types] - [status-im.utils.config :as config])) - -(def ethereum-rpc-url "http://localhost:8545") - -(def text-content-type "text/plain") -(def content-type-log-message "log-message") -(def content-type-command "command") -(def content-type-command-request "command-request") -(def content-type-status "status") -(def content-type-placeholder "placeholder") -(def content-type-emoji "emoji") - -(def command-send "send") -(def command-request "request") -(def command-send-status-update-interval-ms 60000) - -(def min-password-length 6) -(def max-chat-name-length 20) -(def response-suggesstion-resize-duration 100) -(def default-number-of-messages 20) -(def blocks-per-hour 120) - -(def console-chat-id "console") - -(def inbox-password "status-offline-inbox") - -(def default-network config/default-network) - -(def system "system") - -(def default-wallet-transactions - {:filters - {:type [{:id :inbound :label (i18n/label :t/incoming) :checked? true} - {:id :outbound :label (i18n/label :t/outgoing) :checked? true} - {:id :pending :label (i18n/label :t/pending) :checked? true} - {:id :failed :label (i18n/label :t/failed) :checked? true} - ;; TODO(jeluard) Restore once we support postponing transaction - #_{:id :postponed :label (i18n/label :t/postponed) :checked? true}]}}) - -(def mainnet-networks - {"mainnet" {:id "mainnet", - :name "Mainnet", - :config {:NetworkId (ethereum/chain-keyword->chain-id :mainnet) - :DataDir "/ethereum/mainnet"}} - "mainnet_rpc" {:id "mainnet_rpc", - :name "Mainnet with upstream RPC", - :config {:NetworkId (ethereum/chain-keyword->chain-id :mainnet) - :DataDir "/ethereum/mainnet_rpc" - :UpstreamConfig {:Enabled true - :URL "https://mainnet.infura.io/z6GCTmjdP3FETEJmMBI4"}}}}) - -(def testnet-networks - {"testnet" {:id "testnet", - :name "Ropsten", - :config {:NetworkId (ethereum/chain-keyword->chain-id :testnet) - :DataDir "/ethereum/testnet"}} - "testnet_rpc" {:id "testnet_rpc", - :name "Ropsten with upstream RPC", - :config {:NetworkId (ethereum/chain-keyword->chain-id :testnet) - :DataDir "/ethereum/testnet_rpc" - :UpstreamConfig {:Enabled true - :URL "https://ropsten.infura.io/z6GCTmjdP3FETEJmMBI4"}}} - "rinkeby" {:id "rinkeby", - :name "Rinkeby", - :config {:NetworkId (ethereum/chain-keyword->chain-id :rinkeby) - :DataDir "/ethereum/rinkeby"}} - "rinkeby_rpc" {:id "rinkeby_rpc", - :name "Rinkeby with upstream RPC", - :config {:NetworkId (ethereum/chain-keyword->chain-id :rinkeby) - :DataDir "/ethereum/rinkeby_rpc" - :UpstreamConfig {:Enabled true - :URL "https://rinkeby.infura.io/z6GCTmjdP3FETEJmMBI4"}}}}) - -(defn network-enabled? [network] - (if config/rpc-networks-only? - (get-in (val network) [:config :UpstreamConfig :Enabled]) - true)) - -(def default-networks - (into {} (filter network-enabled? - (merge testnet-networks mainnet-networks)))) - -(def default-wnodes-without-custom - {:testnet {"mailserver-a" {:id "mailserver-a" ;mail-01.do-ams3.eth.beta - :name "Status mailserver A" - :password inbox-password - :address "enode://c42f368a23fa98ee546fd247220759062323249ef657d26d357a777443aec04db1b29a3a22ef3e7c548e18493ddaf51a31b0aed6079bd6ebe5ae838fcfaf3a49@206.189.243.162:30504"} - "mailserver-b" {:id "mailserver-b" ;mail-02.do-ams3.eth.beta - :name "Status mailserver B" - :password inbox-password - :address "enode://7aa648d6e855950b2e3d3bf220c496e0cae4adfddef3e1e6062e6b177aec93bc6cdcf1282cb40d1656932ebfdd565729da440368d7c4da7dbd4d004b1ac02bf8@206.189.243.169:30504"} - "mailserver-c" {:id "mailserver-c" ;mail-03.do-ams3.eth.beta - :name "Status mailserver C" - :password inbox-password - :address "enode://8a64b3c349a2e0ef4a32ea49609ed6eb3364be1110253c20adc17a3cebbc39a219e5d3e13b151c0eee5d8e0f9a8ba2cd026014e67b41a4ab7d1d5dd67ca27427@206.189.243.168:30504"} - "mailserver-d" {:id "mailserver-d" ;mail-01.gc-us-central1-a.eth.beta - :name "Status mailserver D" - :password inbox-password - :address "enode://7de99e4cb1b3523bd26ca212369540646607c721ad4f3e5c821ed9148150ce6ce2e72631723002210fac1fd52dfa8bbdf3555e05379af79515e1179da37cc3db@35.188.19.210:30504"} - "mailserver-e" {:id "mailserver-e" ;mail-02.gc-us-central1-a.eth.beta - :name "Status mailserver E" - :password inbox-password - :address "enode://015e22f6cd2b44c8a51bd7a23555e271e0759c7d7f52432719665a74966f2da456d28e154e836bee6092b4d686fe67e331655586c57b718be3997c1629d24167@35.226.21.19:30504"} - "mailserver-f" {:id "mailserver-f" ;mail-03.gc-us-central1-a.eth.beta - :name "Status mailserver F" - :password inbox-password - :address "enode://531e252ec966b7e83f5538c19bf1cde7381cc7949026a6e499b6e998e695751aadf26d4c98d5a4eabfb7cefd31c3c88d600a775f14ed5781520a88ecd25da3c6@35.225.227.79:30504"}} - :mainnet {"mailserver-a" {:id "mailserver-a" ;mail-01.do-ams3.eth.beta - :name "Status mailserver A" - :password inbox-password - :address "enode://c42f368a23fa98ee546fd247220759062323249ef657d26d357a777443aec04db1b29a3a22ef3e7c548e18493ddaf51a31b0aed6079bd6ebe5ae838fcfaf3a49@206.189.243.162:30504"} - "mailserver-b" {:id "mailserver-b" ;mail-02.do-ams3.eth.beta - :name "Status mailserver B" - :password inbox-password - :address "enode://7aa648d6e855950b2e3d3bf220c496e0cae4adfddef3e1e6062e6b177aec93bc6cdcf1282cb40d1656932ebfdd565729da440368d7c4da7dbd4d004b1ac02bf8@206.189.243.169:30504"} - "mailserver-c" {:id "mailserver-c" ;mail-03.do-ams3.eth.beta - :name "Status mailserver C" - :password inbox-password - :address "enode://8a64b3c349a2e0ef4a32ea49609ed6eb3364be1110253c20adc17a3cebbc39a219e5d3e13b151c0eee5d8e0f9a8ba2cd026014e67b41a4ab7d1d5dd67ca27427@206.189.243.168:30504"} - "mailserver-d" {:id "mailserver-d" ;mail-01.gc-us-central1-a.eth.beta - :name "Status mailserver D" - :password inbox-password - :address "enode://7de99e4cb1b3523bd26ca212369540646607c721ad4f3e5c821ed9148150ce6ce2e72631723002210fac1fd52dfa8bbdf3555e05379af79515e1179da37cc3db@35.188.19.210:30504"} - "mailserver-e" {:id "mailserver-e" ;mail-02.gc-us-central1-a.eth.beta - :name "Status mailserver E" - :password inbox-password - :address "enode://015e22f6cd2b44c8a51bd7a23555e271e0759c7d7f52432719665a74966f2da456d28e154e836bee6092b4d686fe67e331655586c57b718be3997c1629d24167@35.226.21.19:30504"} - "mailserver-f" {:id "mailserver-f" ;mail-03.gc-us-central1-a.eth.beta - :name "Status mailserver F" - :password inbox-password - :address "enode://531e252ec966b7e83f5538c19bf1cde7381cc7949026a6e499b6e998e695751aadf26d4c98d5a4eabfb7cefd31c3c88d600a775f14ed5781520a88ecd25da3c6@35.225.227.79:30504"}} - :rinkeby {"mailserver-a" {:id "mailserver-a" - :name "Status mailserver A" - :password inbox-password - :address "enode://43829580446ad138386dadb7fa50b6bd4d99f7c28659a0bc08115f8c0380005922a340962496f6af756a42b94a1522baa38a694fa27de59c3a73d4e08d5dbb31@206.189.6.48:30504"} - "mailserver-b" {:id "mailserver-b" - :name "Status mailserver B" - :password inbox-password - :address "enode://70a2004e78399075f566033c42e9a0b1d43c683d4742755bb5457d03191be66a1b48c2b4fb259696839f28646a5828a1958b900860e27897f984ad0fc8482404@206.189.56.154:30504"}}}) - -(def default-wnodes - (assoc default-wnodes-without-custom :custom (:testnet default-wnodes-without-custom))) - -(defn default-account-settings [] - {:wallet {:visible-tokens {:testnet #{:STT :ATT} - :mainnet #{:SNT} - :rinkeby #{:MOKSHA}}}}) - -(def currencies - {:aed {:id :aed :code "AED" :display-name (i18n/label :t/currency-display-name-aed) :symbol "د.إ"} - :afn {:id :afn :code "AFN" :display-name (i18n/label :t/currency-display-name-afn) :symbol "؋"} - :ars {:id :ars :code "ARS" :display-name (i18n/label :t/currency-display-name-ars) :symbol "$"} - :aud {:id :aud :code "AUD" :display-name (i18n/label :t/currency-display-name-aud) :symbol "$"} - :bbd {:id :bbd :code "BBD" :display-name (i18n/label :t/currency-display-name-bbd) :symbol "$"} - :bdt {:id :bdt :code "BDT" :display-name (i18n/label :t/currency-display-name-bdt) :symbol " Tk"} - :bgn {:id :bgn :code "BGN" :display-name (i18n/label :t/currency-display-name-bgn) :symbol "лв"} - :bhd {:id :bhd :code "BHD" :display-name (i18n/label :t/currency-display-name-bhd) :symbol "BD"} - :bnd {:id :bnd :code "BND" :display-name (i18n/label :t/currency-display-name-bnd) :symbol "$"} - :bob {:id :bob :code "BOB" :display-name (i18n/label :t/currency-display-name-bob) :symbol "$b"} - :brl {:id :brl :code "BRL" :display-name (i18n/label :t/currency-display-name-brl) :symbol "R$"} - :btn {:id :btn :code "BTN" :display-name (i18n/label :t/currency-display-name-btn) :symbol "Nu."} - :cad {:id :cad :code "CAD" :display-name (i18n/label :t/currency-display-name-cad) :symbol "$"} - :chf {:id :chf :code "CHF" :display-name (i18n/label :t/currency-display-name-chf) :symbol "CHF"} - :clp {:id :clp :code "CLP" :display-name (i18n/label :t/currency-display-name-clp) :symbol "$"} - :cny {:id :cny :code "CNY" :display-name (i18n/label :t/currency-display-name-cny) :symbol "¥"} - :cop {:id :cop :code "COP" :display-name (i18n/label :t/currency-display-name-cop) :symbol "$"} - :crc {:id :crc :code "CRC" :display-name (i18n/label :t/currency-display-name-crc) :symbol "₡"} - :czk {:id :czk :code "CZK" :display-name (i18n/label :t/currency-display-name-czk) :symbol "Kč"} - :dkk {:id :dkk :code "DKK" :display-name (i18n/label :t/currency-display-name-dkk) :symbol "kr"} - :dop {:id :dop :code "DOP" :display-name (i18n/label :t/currency-display-name-dop) :symbol "RD$"} - :egp {:id :egp :code "EGP" :display-name (i18n/label :t/currency-display-name-egp) :symbol "£"} - :etb {:id :etb :code "ETB" :display-name (i18n/label :t/currency-display-name-etb) :symbol "Br"} - :eur {:id :eur :code "EUR" :display-name (i18n/label :t/currency-display-name-eur) :symbol "€"} - :gbp {:id :gbp :code "GBP" :display-name (i18n/label :t/currency-display-name-gbp) :symbol "£"} - :gel {:id :gel :code "GEL" :display-name (i18n/label :t/currency-display-name-gel) :symbol "₾"} - :ghs {:id :ghs :code "GHS" :display-name (i18n/label :t/currency-display-name-ghs) :symbol "¢"} - :hkd {:id :hkd :code "HKD" :display-name (i18n/label :t/currency-display-name-hkd) :symbol "$"} - :hrk {:id :hrk :code "HRK" :display-name (i18n/label :t/currency-display-name-hrk) :symbol "kn"} - :huf {:id :huf :code "HUF" :display-name (i18n/label :t/currency-display-name-huf) :symbol "Ft"} - :idr {:id :idr :code "IDR" :display-name (i18n/label :t/currency-display-name-idr) :symbol "Rp"} - :ils {:id :ils :code "ILS" :display-name (i18n/label :t/currency-display-name-ils) :symbol "₪"} - :inr {:id :inr :code "INR" :display-name (i18n/label :t/currency-display-name-inr) :symbol "₹"} - :isk {:id :isk :code "ISK" :display-name (i18n/label :t/currency-display-name-isk) :symbol "kr"} - :jmd {:id :jmd :code "JMD" :display-name (i18n/label :t/currency-display-name-jmd) :symbol "J$"} - :jpy {:id :jpy :code "JPY" :display-name (i18n/label :t/currency-display-name-jpy) :symbol "¥"} - :kes {:id :kes :code "KES" :display-name (i18n/label :t/currency-display-name-kes) :symbol "KSh"} - :krw {:id :krw :code "KRW" :display-name (i18n/label :t/currency-display-name-krw) :symbol "₩"} - :kwd {:id :kwd :code "KWD" :display-name (i18n/label :t/currency-display-name-kwd) :symbol "د.ك"} - :kzt {:id :kzt :code "KZT" :display-name (i18n/label :t/currency-display-name-kzt) :symbol "лв"} - :lkr {:id :lkr :code "LKR" :display-name (i18n/label :t/currency-display-name-lkr) :symbol "₨"} - :mad {:id :mad :code "MAD" :display-name (i18n/label :t/currency-display-name-mad) :symbol "MAD"} - :mdl {:id :mdl :code "MDL" :display-name (i18n/label :t/currency-display-name-mdl) :symbol "MDL"} - :mur {:id :mur :code "MUR" :display-name (i18n/label :t/currency-display-name-mur) :symbol "₨"} - :mwk {:id :mwk :code "MWK" :display-name (i18n/label :t/currency-display-name-mwk) :symbol "MK"} - :mxn {:id :mxn :code "MXN" :display-name (i18n/label :t/currency-display-name-mxn) :symbol "$"} - :myr {:id :myr :code "MYR" :display-name (i18n/label :t/currency-display-name-myr) :symbol "RM"} - :mzn {:id :mzn :code "MZN" :display-name (i18n/label :t/currency-display-name-mzn) :symbol "MT"} - :nad {:id :nad :code "NAD" :display-name (i18n/label :t/currency-display-name-nad) :symbol "$"} - :ngn {:id :ngn :code "NGN" :display-name (i18n/label :t/currency-display-name-ngn) :symbol "₦"} - :nok {:id :nok :code "NOK" :display-name (i18n/label :t/currency-display-name-nok) :symbol "kr"} - :npr {:id :npr :code "NPR" :display-name (i18n/label :t/currency-display-name-npr) :symbol "₨"} - :nzd {:id :nzd :code "NZD" :display-name (i18n/label :t/currency-display-name-nzd) :symbol "$"} - :omr {:id :omr :code "OMR" :display-name (i18n/label :t/currency-display-name-omr) :symbol "﷼"} - :pen {:id :pen :code "PEN" :display-name (i18n/label :t/currency-display-name-pen) :symbol "S/."} - :pgk {:id :pgk :code "PGK" :display-name (i18n/label :t/currency-display-name-pgk) :symbol "K"} - :php {:id :php :code "PHP" :display-name (i18n/label :t/currency-display-name-php) :symbol "₱"} - :pkr {:id :pkr :code "PKR" :display-name (i18n/label :t/currency-display-name-pkr) :symbol "₨"} - :pln {:id :pln :code "PLN" :display-name (i18n/label :t/currency-display-name-pln) :symbol "zł"} - :pyg {:id :pyg :code "PYG" :display-name (i18n/label :t/currency-display-name-pyg) :symbol "Gs"} - :qar {:id :qar :code "QAR" :display-name (i18n/label :t/currency-display-name-qar) :symbol "﷼"} - :ron {:id :ron :code "RON" :display-name (i18n/label :t/currency-display-name-ron) :symbol "lei"} - :rsd {:id :rsd :code "RSD" :display-name (i18n/label :t/currency-display-name-rsd) :symbol "Дин."} - :rub {:id :rub :code "RUB" :display-name (i18n/label :t/currency-display-name-rub) :symbol "₽"} - :sar {:id :sar :code "SAR" :display-name (i18n/label :t/currency-display-name-sar) :symbol "﷼"} - :sek {:id :sek :code "SEK" :display-name (i18n/label :t/currency-display-name-sek) :symbol "kr"} - :sgd {:id :sgd :code "SGD" :display-name (i18n/label :t/currency-display-name-sgd) :symbol "$"} - :thb {:id :thb :code "THB" :display-name (i18n/label :t/currency-display-name-thb) :symbol "฿"} - :ttd {:id :ttd :code "TTD" :display-name (i18n/label :t/currency-display-name-ttd) :symbol "TT$"} - :twd {:id :twd :code "TWD" :display-name (i18n/label :t/currency-display-name-twd) :symbol "NT$"} - :tzs {:id :tzs :code "TZS" :display-name (i18n/label :t/currency-display-name-tzs) :symbol "TSh"} - :try {:id :try :code "TRY" :display-name (i18n/label :t/currency-display-name-try) :symbol "₺"} - :uah {:id :uah :code "UAH" :display-name (i18n/label :t/currency-display-name-uah) :symbol "₴"} - :ugx {:id :ugx :code "UGX" :display-name (i18n/label :t/currency-display-name-ugx) :symbol "USh"} - :uyu {:id :uyu :code "UYU" :display-name (i18n/label :t/currency-display-name-uyu) :symbol "$U"} - :usd {:id :usd :code "USD" :display-name (i18n/label :t/currency-display-name-usd) :symbol "$"} - :vef {:id :vef :code "VEF" :display-name (i18n/label :t/currency-display-name-vef) :symbol "Bs"} - :vnd {:id :vnd :code "VND" :display-name (i18n/label :t/currency-display-name-vnd) :symbol "₫"} - :zar {:id :zar :code "ZAR" :display-name (i18n/label :t/currency-display-name-zar) :symbol "R"}}) - -;; Used to generate topic for contact discoveries -(def contact-discovery "contact-discovery") - -(def ^:const send-transaction-no-error-code "0") -(def ^:const send-transaction-default-error-code "1") -(def ^:const send-transaction-password-error-code "2") -(def ^:const send-transaction-timeout-error-code "3") -(def ^:const send-transaction-discarded-error-code "4") - -(def ^:const web3-send-transaction "eth_sendTransaction") -(def ^:const web3-personal-sign "personal_sign") -(def ^:const web3-get-logs "eth_getLogs") - -(def ^:const event-transfer-hash - (ethereum/sha3 "Transfer(address,address,uint256)")) - -(def regx-emoji #"^((?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F|[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF])+$") - -(def ^:const dapp-permission-contact-code "CONTACT_CODE") -(def ^:const status-api-success "status-api-success") -(def ^:const status-api-request "status-api-request") -(def ^:const history-state-changed "history-state-changed") \ No newline at end of file + (:require + [clojure.string :as string])) + +(def ^:const ms-in-bg-for-require-bioauth 5000) + +(def ^:const content-type-text 1) +(def ^:const content-type-sticker 2) +(def ^:const content-type-status 3) +(def ^:const content-type-emoji 4) +(def ^:const content-type-system-text 6) +(def ^:const content-type-image 7) +(def ^:const content-type-audio 8) +(def ^:const content-type-community 9) +(def ^:const content-type-gap 10) +(def ^:const content-type-contact-request 11) +(def ^:const content-type-system-pinned-message 14) +(def ^:const content-type-system-message-mutual-event-sent 15) +(def ^:const content-type-system-message-mutual-event-accepted 16) +(def ^:const content-type-system-message-mutual-event-removed 17) +(def ^:const content-type-bridge-message 18) + +;; Not implemented in status-go, only used for testing/ui work +(def ^:const content-type-gif 100) +(def ^:const content-type-album 102) + +(def ^:const contact-request-state-none 0) +(def ^:const contact-request-state-mutual 1) +(def ^:const contact-request-state-sent 2) +(def ^:const contact-request-state-received 3) +(def ^:const contact-request-state-dismissed 4) + +(def ^:const contact-verification-status-pending 1) +(def ^:const contact-verification-status-accepted 2) +(def ^:const contact-verification-status-declined 3) +(def ^:const contact-verification-status-trusted 5) +(def ^:const contact-verification-status-untrustworthy 6) + +(def ^:const activity-center-membership-status-idle 0) +(def ^:const activity-center-membership-status-pending 1) +(def ^:const activity-center-membership-status-accepted 2) +(def ^:const activity-center-membership-status-declined 3) + +;; Choose the maximum number of notifications that *usually/safely* fit on +;; most screens, so that the UI doesn't have to needlessly render +;; notifications. +(def ^:const notifications-per-page 7) + +(def ^:const mute-for-15-mins-type 1) +(def ^:const mute-for-1-hour-type 2) +(def ^:const mute-for-8-hours-type 3) +(def ^:const mute-for-1-week 4) +(def ^:const mute-till-unmuted 5) +(def ^:const un-muted 0) + +(def ^:const mute-undo-time-limit-ms 4000) + +(def ^:const activity-center-mark-all-as-read-undo-time-limit-ms 4000) +(def ^:const activity-center-max-unread-count 99) + +(def ^:const emoji-reaction-love 1) +(def ^:const emoji-reaction-thumbs-up 2) +(def ^:const emoji-reaction-thumbs-down 3) +(def ^:const emoji-reaction-laugh 4) +(def ^:const emoji-reaction-sad 5) +(def ^:const emoji-reaction-angry 6) + +(def ^:const one-to-one-chat-type 1) +(def ^:const public-chat-type 2) +(def ^:const private-group-chat-type 3) +(def ^:const profile-chat-type 4) +(def ^:const timeline-chat-type 5) +(def ^:const community-chat-type 6) + +(def ^:const chat-preview-type-community 0) +(def ^:const chat-preview-type-non-community 1) + +(def ^:const contact-request-message-state-pending 1) +(def ^:const contact-request-message-state-accepted 2) +(def ^:const contact-request-message-state-declined 3) +(def ^:const contact-request-message-max-length 280) + +(def ^:const key-pair-name-max-length 20) +(def ^:const key-pair-name-min-length 5) + +(def request-to-join-pending-state 1) + +(def reactions + {emoji-reaction-love :reaction/love + emoji-reaction-thumbs-up :reaction/thumbs-up + emoji-reaction-thumbs-down :reaction/thumbs-down + emoji-reaction-laugh :reaction/laugh + emoji-reaction-sad :reaction/sad + emoji-reaction-angry :reaction/angry}) + +(def ^:const invitation-state-requested 1) +(def ^:const invitation-state-rejected 2) +(def ^:const invitation-state-approved 3) +(def ^:const message-type-one-to-one 1) +(def ^:const message-type-private-group-system-message 4) + + +(def ^:const profile-default-color :blue) +(def ^:const profile-name-max-length 20) +(def ^:const profile-bio-max-length 240) +(def ^:const profile-default-currency :usd) + +(def ^:const profile-pictures-show-to-contacts-only 1) +(def ^:const profile-pictures-show-to-everyone 2) +(def ^:const profile-pictures-show-to-none 3) + +(def ^:const profile-pictures-visibility-contacts-only 1) +(def ^:const profile-pictures-visibility-everyone 2) +(def ^:const profile-pictures-visibility-none 3) + +(def ^:const min-password-length 6) +(def ^:const pincode-length 6) +(def ^:const new-password-min-length 10) +(def ^:const new-password-max-length 100) +(def ^:const max-group-chat-participants 20) +(def ^:const max-group-chat-name-length 24) +(def ^:const default-number-of-messages 20) +(def ^:const default-number-of-pin-messages 3) + +(def ^:const password-tips + [:lower-case? + :upper-case? + :numbers? + :symbols?]) + +(def ^:const strength-status + {1 :very-weak + 2 :weak + 3 :okay + 4 :strong + 5 :very-strong}) + +(def ^:const mailserver-password "status-offline-inbox") + + +(def ^:const community-no-membership-access 1) +(def ^:const community-invitation-only-access 2) +(def ^:const community-on-request-access 3) + +(def ^:const community-token-permission-become-admin 1) +(def ^:const community-token-permission-become-member 2) +(def ^:const community-token-permission-become-token-master 5) +(def ^:const community-token-permission-become-token-owner 6) +(def ^:const community-role-permissions + #{community-token-permission-become-admin + community-token-permission-become-member + community-token-permission-become-token-master + community-token-permission-become-token-owner}) + +(def ^:const community-token-type-erc20 1) +(def ^:const community-token-type-erc721 2) + + +(def ^:const community-request-to-join-state-pending 1) +(def ^:const community-request-to-join-state-accepted 3) +(def ^:const community-id-length 68) + + +; BIP44 Wallet Root Key, the extended key from which any wallet can be derived +(def ^:const path-wallet-root "m/44'/60'/0'/0") +; EIP1581 Root Key, the extended key from which any whisper key/encryption key can be derived +(def ^:const path-eip1581 "m/43'/60'/1581'") +; BIP44-0 Wallet key, the default wallet key +(def ^:const path-default-wallet (str path-wallet-root "/0")) +; EIP1581 Chat Key 0, the default whisper key +(def ^:const path-whisper (str path-eip1581 "/0'/0")) + +(def ^:const path-ropsten-testnet "m/44'/1'/0") +(def ^:const path-ledger "m/44'/60'/0'") +(def ^:const path-ledger-live "m/44'/60'") +(def ^:const path-keepkey "m/44'/60'") + + +(def regx-string-universal-link-encoded-data-base64 + "(?:[A-Za-z0-9+/_-]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?") +(def regx-string-any-ascii "[\\x00-\\x7F]") +(def regx-string-compressed-key "zQ3[0-9A-HJ-NP-Za-km-z]{46}") +(def regx-string-public-key "0x04[0-9a-f]{128}") +(def regx-compressed-key (re-pattern (str "^" regx-string-compressed-key "$"))) +(def regx-public-key (re-pattern (str "^" regx-string-public-key "$"))) +(def regx-private-key-hex #"^0x[0-9a-fA-F]{64}$") +(def regx-private-key #"^[0-9a-fA-F]{64}$") +(def regx-emoji + #"^((?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F|[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF])+$") +(def regx-string-universal-link + (str "((^https?://status.app/)|(^status-app://))" + "(" + (string/join + "|" + [(str "u/" regx-string-universal-link-encoded-data-base64 "#" regx-string-compressed-key) + (str "u#" regx-string-compressed-key) + (str "c/" regx-string-universal-link-encoded-data-base64 "#" regx-string-compressed-key) + (str "c#" regx-string-compressed-key) + (str "cc/" regx-string-universal-link-encoded-data-base64 "#" regx-string-compressed-key) + (str "p/" regx-string-any-ascii "+") + (str "cr/" regx-string-any-ascii "+") + (str "g/" regx-string-any-ascii "+") + (str "wallet/" regx-string-any-ascii "+")]) + ")$")) +(def regx-universal-link (re-pattern regx-string-universal-link)) +(def regx-deep-link #"((^ethereum:.*)|(^status-app://[\x00-\x7F]+)|(^wc://.*))") +(def regx-ens #"^(?=.{5,255}$)([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$") +(def regx-starts-with-uuid #"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") + +;; Wallet Connect +(def ^:const wallet-connect-metadata-icon + "https://res.cloudinary.com/dhgck7ebz/image/upload/f_auto,c_limit,w_1080,q_auto/Brand/Logo%20Section/Mark/Mark_01") +(def ^:const wallet-connect-metadata-url "https://status.app") +(def ^:const wallet-connect-metadata-redirect-native "status-app://") + +(def ^:const wallet-connect-personal-sign-method "personal_sign") +(def ^:const wallet-connect-eth-sign-method "eth_sign") +(def ^:const wallet-connect-eth-send-transaction-method "eth_sendTransaction") +(def ^:const wallet-connect-eth-sign-transaction-method "eth_signTransaction") +(def ^:const wallet-connect-eth-sign-typed-method "eth_signTypedData") +(def ^:const wallet-connect-eth-sign-typed-v4-method "eth_signTypedData_v4") +(def ^:const wallet-connect-supported-methods + #{wallet-connect-personal-sign-method + wallet-connect-eth-send-transaction-method + wallet-connect-eth-sign-typed-method + wallet-connect-eth-sign-typed-v4-method}) +(def ^:const wallet-connect-supported-events #{"accountsChanged" "chainChanged"}) +(def ^:const wallet-connect-session-proposal-event "session_proposal") +(def ^:const wallet-connect-session-request-event "session_request") +(def ^:const wallet-connect-session-delete-event "session_delete") +(def ^:const wallet-connect-user-rejected-error-key "USER_REJECTED") +(def ^:const wallet-connect-user-disconnected-reason-key "USER_DISCONNECTED") +(def ^:const wallet-connect-user-rejected-chains-error-key "USER_REJECTED_CHAINS") +(def ^:const wallet-connect-message-signing-methods + #{wallet-connect-personal-sign-method + wallet-connect-eth-sign-typed-method + wallet-connect-eth-sign-typed-v4-method}) +(def ^:const wallet-connect-transaction-methods + #{wallet-connect-eth-send-transaction-method}) + +(def ^:const transaction-pending-type-wallet-connect-transfer "WalletConnectTransfer") + +(def ^:const token-sort-priority {"SNT" 1 "STT" 1 "ETH" 2 "DAI" 3}) + +(def ^:const token-display-precision 6) + +(def ^:const dapp-permission-contact-code "contact-code") +(def ^:const dapp-permission-web3 "web3") +(def ^:const dapp-permission-qr-code "qr-code") +(def ^:const api-response "api-response") +(def ^:const api-request "api-request") +(def ^:const history-state-changed "history-state-changed") +(def ^:const web3-send-async-read-only "web3-send-async-read-only") +(def ^:const web3-send-async-callback "web3-send-async-callback") +(def ^:const two-mins (* 2 60)) +(def ^:const one-day (* 60 60 24)) +(def ^:const three-days (* one-day 3)) +(def ^:const nine-days (* one-day 9)) +(def ^:const one-week (* one-day 7)) +(def ^:const one-month (* one-day 31)) + +(def ^:const create-account-link "https://status.app/help/wallet/create-wallet-accounts") +(def ^:const mobile-upvote-link "https://discuss.status.app/c/features/51") +(def ^:const status-help-link "https://status.app/help") + +(def ^:const visibility-status-unknown 0) +(def ^:const visibility-status-automatic 1) +(def ^:const visibility-status-dnd 2) +(def ^:const visibility-status-always-online 3) +(def ^:const visibility-status-inactive 4) + +(def ^:const sticker-pack-status-installed 1) +(def ^:const sticker-pack-status-pending 2) +(def ^:const sticker-pack-status-owned 3) + +(def ^:const community-member-role-moderator 3) + +(def ^:const delete-message-undo-time-limit-ms 4000) +(def ^:const delete-message-for-me-undo-time-limit-ms 4000) + +(def ^:const max-album-photos 6) +(def ^:const image-size 146) + +(def album-image-sizes ; sometimes we subtract 1 or 0.5 for padding + {2 {0 image-size + 1 image-size} + 3 {0 [(* image-size 2) (* image-size 1.25)] + 1 [(- image-size 0.5) (dec (* image-size 0.75))] + 2 [(- image-size 0.5) (dec (* image-size 0.75))]} + 4 {0 image-size + 1 image-size + 2 image-size + 3 image-size} + 5 {0 image-size + 1 image-size + 2 (dec (* image-size 0.67)) + 3 (dec (* image-size 0.67)) + 4 (dec (* image-size 0.67))} + :default {0 image-size + 1 image-size + 2 (- (* image-size 0.5) 0.5) + 3 (- (* image-size 0.5) 0.5) + 4 (- (* image-size 0.5) 0.5) + 5 (- (* image-size 0.5) 0.5)}}) + +;; any message that comes after this amount of ms will be grouped separately +(def ^:const group-ms 300000) + +(def ^:const local-pairing-connection-string-identifier + "If any string begins with cs we know its a connection string. + This is useful when we read QR codes we know it is a connection string if it begins with this identifier. + An example of a connection string is -> cs2:5vd6J6:Jfc:27xMmHKEYwzRGXcvTtuiLZFfXscMx4Mz8d9wEHUxDj4p7:EG7Z13QScfWBJNJ5cprszzDQ5fBVsYMirXo8MaQFJvpF:3 " + "cs") + +(def ^:const local-pairing-role-sender "sender") +(def ^:const local-pairing-role-receiver "receiver") + +;; sender and receiver events +(def ^:const local-pairing-event-connection-success "connection-success") +(def ^:const local-pairing-event-connection-error "connection-error") +(def ^:const local-pairing-event-transfer-success "transfer-success") +(def ^:const local-pairing-event-transfer-error "transfer-error") + + +;; receiver events +(def ^:const local-pairing-event-received-account "received-account") +(def ^:const local-pairing-event-process-error "process-error") +(def ^:const local-pairing-event-errors + #{local-pairing-event-connection-error + local-pairing-event-transfer-error + local-pairing-event-process-error}) + +(def ^:const local-pairing-action-connect 1) +(def ^:const local-pairing-action-pairing-account 2) +(def ^:const local-pairing-action-pairing-installation 4) +(def ^:const local-pairing-action-keystore-files-transfer 6) + +(def ^:const serialization-key + "We pass this serialization key as a parameter to MultiformatSerializePublicKey + function at status-go, This key determines the output base of the serialization. + according to https://specs.status.im/spec/2#public-key-serialization we serialize + keys with base58btc encoding" + "z") + +(def ^:const deserialization-key + "We pass this deserialization key as a parameter to MultiformatDeserializePublicKey + function at status-go, This key determines the output base of the deserialization. + according to https://specs.status.im/spec/2#public-key-serialization we deserialize + keys with base16 hexadecimal encoding" + "f") + +(def ^:const appearance-type-system 0) +(def ^:const appearance-type-light 1) +(def ^:const appearance-type-dark 2) + + +(def ^:const everyone-mention-id "0x00001") + +(def ^:const empty-category-id :communities/not-categorized) + +(def ^:const seed-phrase-valid-length #{12 18 24}) + +(def ^:const auth-method-password "password") +(def ^:const auth-method-biometric "biometric") +(def ^:const auth-method-none "none") + +;; NOTE all android biometrics falls under :Biometrics +(def ^:const biometrics-type-android :Biometrics) +(def ^:const biometrics-type-touch-id :TouchID) +(def ^:const biometrics-type-face-id :FaceID) + +(def ^:const audio-max-duration-ms 120000) + +(def ^:const onboarding-modal-animation-duration 300) +(def ^:const onboarding-modal-animation-delay 400) + +(def ^:const initials-avatar-font-conf + "we pass absolute font file path and uppercase ratio to status-go for media + server to serve initials avatar image + `:uppercase-ratio` is uppercase-height/line-height for Inter-Medium" + {:ios "Inter-Medium.otf" + :android "Inter-Medium.ttf" + :uppercase-ratio 0.603861228044709}) + +(def ^:const ens-action-type-register 1) +(def ^:const ens-action-type-set-pub-key 2) + +;; wallet + +(def opensea-url-names + {:ethereum "ethereum" + :sepolia "sepolia"}) + +(def ^:const mainnet-short-name "eth") + +(def ^:const layer-1-network 1) +(def ^:const layer-2-network 2) + +(def ^:const chain-id-separator ":") + +(def ^:const account-default-customization-color :blue) + +(def ^:const wallet-account-name-max-length 20) + +(def ^:const gas-rate-low 0) +(def ^:const gas-rate-medium 1) +(def ^:const gas-rate-high 2) +(def ^:const gas-rate-custom 3) +(def ^:const send-type-transfer 0) +(def ^:const send-type-bridge 5) +(def ^:const send-type-erc-721-transfer 6) +(def ^:const send-type-erc-1155-transfer 7) +(def ^:const send-type-swap 8) + +(def ^:const bridge-name-hop "Hop") + +;; remove the decimal from the token symbol once we move to CoinGecko token list +(def ^:const bridge-assets #{"ETH" "USDT (EVM)" "USDC (EVM)" "DAI"}) + +(def ^:const wallet-contract-type-erc-1155 3) + +(def ^:const alert-banner-height 40) + +(def ^:const sheet-screen-handle-height 20) + +(def ^:const status-hostname "status.app") +(def ^:const get-keycard-url "https://get.keycard.tech/") + +(def ^:const community-joined-notification-type "communityJoined") + +(def ^:const default-telemetry-server-url "https://telemetry.status.im") + +(def ^:const contact-item-height 56) +(def ^:const page-nav-height 56) +(def ^:const currency-item-height 64) + +(def ^:const slippages [0.1 0.5 1]) +(def ^:const default-slippage 0.5) +(def ^:const max-recommended-slippage 5) +(def ^:const max-slippage 30) +(def ^:const max-slippage-decimal-places 2) +(def ^:const swap-provider-paraswap + {:name :paraswap + :full-name "ParaSwap" + :color :blue + :contract-address "0x6a000f20005980200259b80c5102003040001068" + :terms-and-conditions-url "https://files.paraswap.io/tos_v4.pdf"}) +(def ^:const swap-providers + {:paraswap swap-provider-paraswap}) +(def ^:const swap-tokens-my :my) +(def ^:const swap-tokens-popular :popular) + +;; The 15% buffer accounts for Ethereum's EIP-1559 fee mechanism, where the base fee can increase +;; by up to 12.5% per block in periods of high congestion. The extra 2.5% provides additional +;; safety to prevent failed transactions due to rapid gas price fluctuations. +(def ^:const eth-max-fee-buffer-percent 15) + +(def ^:const transaction-status-success "Success") +(def ^:const transaction-status-pending "Pending") +(def ^:const transaction-status-failed "Failed") + +(def ^:const eth-send-amount-decimal 4) +(def ^:const min-token-decimals-to-display 6) +(def ^:const swap-proposal-refresh-interval-ms 15000) + +(def router-error-code-generic "0") +(def router-error-code-paraswap-custom-error "WPP-030") +(def router-error-code-price-timeout "WPP-037") +(def router-error-code-not-enough-liquidity "WPP-038") +(def router-error-code-price-impact-too-high "WPP-039") +(def router-error-code-not-enough-native-balance "WR-002") + +(def ^:const wallet-transaction-estimation + {:unknown 0 + :less-than-one-minute 1 + :less-than-three-minutes 2 + :less-than-five-minutes 3 + :more-than-five-minutes 4}) + +(def ^:const wallet-connect-transaction-refresh-interval-ms 10000) + +;; About app links +(def ^:const status-app-url "http://status.app") +(def ^:const go-waku-url "https://github.com/waku-org/go-waku") +(def ^:const status-go-url "https://github.com/status-im/status-go") +(def ^:const status-mobile-url "https://github.com/status-im/status-mobile") + +(def ^:const wc-connection-string-identifier "wc") + +(def ^:const max-allowed-watched-accounts 20) + +(def ^:const pre-login-log-level-key "pre-login-log-level") +(def ^:const report-email "error-reports@status.im") + +;; Community help links +(def ^:const create-community-help-url + "https://status.app/help/communities/create-a-status-community#create-a-status-community") diff --git a/src/status_im/contexts/centralized_metrics/effects.cljs b/src/status_im/contexts/centralized_metrics/effects.cljs new file mode 100644 index 00000000000..dfa2db377d7 --- /dev/null +++ b/src/status_im/contexts/centralized_metrics/effects.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.centralized-metrics.effects + (:require + [native-module.core :as native-module] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.centralized-metrics/toggle-metrics + (fn [enabled?] + (native-module/toggle-centralized-metrics enabled? #(log/debug "toggled-metrics" % enabled?)))) diff --git a/src/status_im/contexts/centralized_metrics/events.cljs b/src/status_im/contexts/centralized_metrics/events.cljs new file mode 100644 index 00000000000..037cdeb9e8a --- /dev/null +++ b/src/status_im/contexts/centralized_metrics/events.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.centralized-metrics.events + (:require + [native-module.core :as native-module] + [re-frame.interceptor :as interceptor] + status-im.contexts.centralized-metrics.effects + [status-im.contexts.centralized-metrics.tracking :as tracking] + [utils.re-frame :as rf])) + +(defn push-event? + [db] + (or (not (:centralized-metrics/user-confirmed? db)) + (:centralized-metrics/enabled? db))) + +(defn centralized-metrics-interceptor + [context] + (when (push-event? (interceptor/get-coeffect context :db)) + (when-let [event (tracking/metrics-event (interceptor/get-coeffect context :event))] + (if (or (seq? event) (vector? event)) + (doseq [e event] + (native-module/add-centralized-metric e)) + (native-module/add-centralized-metric event)))) + context) + +;; ignore warning, interceptor used from interceptor_metrics.cljc +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(def interceptor + (interceptor/->interceptor + :id :centralized-metrics + :after centralized-metrics-interceptor)) + +(rf/reg-event-fx :centralized-metrics/toggle-centralized-metrics + (fn [{:keys [db]} [enabled? onboarding?]] + {:fx [[:effects.centralized-metrics/toggle-metrics enabled?]] + :db (-> db + (assoc :centralized-metrics/user-confirmed? true) + (assoc :centralized-metrics/enabled? enabled?) + (assoc :centralized-metrics/onboarding-enabled? (and onboarding? enabled?)))})) + +(rf/reg-event-fx :centralized-metrics/check-user-confirmation + (fn [{:keys [db]}] + (when-not (:centralized-metrics/user-confirmed? db) + {:fx [[:dispatch [:navigate-to :screen/onboarding.share-usage]]]}))) + +(rf/reg-fx :effects.centralized-metrics/track + (fn [event] + (native-module/add-centralized-metric event))) + +(rf/reg-event-fx + :centralized-metrics/track + (fn [{:keys [db]} [event-name data]] + (let [event-id (name event-name)] + (when (push-event? db) + {:fx [[:effects.centralized-metrics/track + (tracking/key-value-event event-id data)]]})))) diff --git a/src/status_im/contexts/centralized_metrics/events_test.cljs b/src/status_im/contexts/centralized_metrics/events_test.cljs new file mode 100644 index 00000000000..feb394cfb16 --- /dev/null +++ b/src/status_im/contexts/centralized_metrics/events_test.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.centralized-metrics.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + matcher-combinators.test + [status-im.contexts.centralized-metrics.events :as events] + [status-im.contexts.centralized-metrics.tracking :as tracking] + [test-helpers.unit :as h])) + +(deftest push-event-test + (testing "returns correct boolean value" + (is (true? (events/push-event? {:centralized-metrics/user-confirmed? false}))) + (is (true? (events/push-event? {:centralized-metrics/enabled? true}))) + (is (false? (events/push-event? {:centralized-metrics/user-confirmed? true + :centralized-metrics/enabled? false}))))) + +(deftest centralized-metrics-interceptor-test + (testing "processes context correctly" + (with-redefs [tracking/metrics-event (fn [_] {:metric "mocked-event"}) + events/push-event? (fn [_] true)] + (let [context {:coeffects {:event [:some-event] + :db {:centralized-metrics/enabled? true}}}] + (is (= context (events/centralized-metrics-interceptor context))))))) + +(h/deftest-event :centralized-metrics/toggle-centralized-metrics + [event-id dispatch] + (testing "toggling value to true" + (let [enabled? true + expected-fxs {:db {:centralized-metrics/user-confirmed? true + :centralized-metrics/enabled? enabled?} + :fx [[:effects.centralized-metrics/toggle-metrics enabled?]]}] + (is (match? expected-fxs (dispatch [event-id enabled?]))))) + (testing "toggling value to false" + (let [enabled? false + expected-fxs {:db {:centralized-metrics/user-confirmed? true + :centralized-metrics/enabled? enabled?} + :fx [[:effects.centralized-metrics/toggle-metrics enabled?]]}] + (is (match? expected-fxs (dispatch [event-id enabled?])))))) diff --git a/src/status_im/contexts/centralized_metrics/tracking.cljs b/src/status_im/contexts/centralized_metrics/tracking.cljs new file mode 100644 index 00000000000..8baf987faa3 --- /dev/null +++ b/src/status_im/contexts/centralized_metrics/tracking.cljs @@ -0,0 +1,96 @@ +(ns status-im.contexts.centralized-metrics.tracking + (:require + [clojure.string] + [react-native.platform :as platform] + [status-im.app-build.core :as build] + [status-im.navigation.screens :as screens])) + +(defn key-value-event + ([event-name] + (key-value-event event-name nil)) + ([event-name event-value] + (cond-> {:metric + {:eventName event-name + :platform platform/os + :appVersion build/app-short-version}} + event-value + (assoc-in [:metric :eventValue] event-value)))) + +(defn user-journey-event + [action] + (key-value-event "user-journey" {:action action})) + +(defn navigation-event + [view-id] + (key-value-event "navigation" {:viewId view-id})) + +(defn screen-event + [screen event-data] + (let [screen-id (:name screen) + view-id (get-in screen [:metrics :alias-id] screen-id)] + {:metric + {:eventName "navigation" + :platform platform/os + :appVersion build/app-short-version + :eventValue (assoc event-data :viewId (name view-id))}})) + +(def ^:const app-started-event "app-started") + +(def ^:const view-ids-to-track + #{;; Tabs + :screen/communities-stack + :screen/chats-stack + :screen/wallet-stack + :screen/market-stack}) + +(defn track-view-id-event + [view-id] + (let [screen (get screens/screens-by-name view-id)] + (cond-> [] + (get-in screen [:metrics :track?]) + (conj (screen-event screen {})) + + (contains? view-ids-to-track view-id) + (conj (navigation-event (name view-id))) + + (#{:screen/onboarding.preparing-status :screen/onboarding.syncing-biometric} view-id) + (conj (key-value-event "onboarding-completed")) + + (= :screen/keycard.migrate.success view-id) + (conj (key-value-event "keycard-migration-succeeded")) + + (= :screen/keycard.migrate.fail view-id) + (conj (key-value-event "keycard-migration-failed"))))) + +(defn navigated-to-collectibles-tab-event + [location] + (key-value-event "navigated-to-collectibles-tab" {:location location})) + +(defn metrics-event + [[rf-event-name rf-event-parameter]] + (case rf-event-name + :profile/get-profiles-overview-success + (user-journey-event app-started-event) + + :centralized-metrics/toggle-centralized-metrics + (key-value-event "events.metrics-enabled" {:enabled rf-event-parameter}) + + :profile.login/non-critical-initialization + (key-value-event "user-logged-in") + + (:profile.create/create-and-login + :profile.recover/recover-and-login) + (key-value-event "onboarding-completed") + + :set-view-id + (track-view-id-event rf-event-parameter) + + :wallet/select-account-tab + (when (= rf-event-parameter :collectibles) + (navigated-to-collectibles-tab-event :account)) + + :wallet/select-home-tab + (when (= rf-event-parameter :collectibles) + (navigated-to-collectibles-tab-event :home)) + + nil)) diff --git a/src/status_im/contexts/centralized_metrics/tracking_test.cljs b/src/status_im/contexts/centralized_metrics/tracking_test.cljs new file mode 100644 index 00000000000..1752155e83c --- /dev/null +++ b/src/status_im/contexts/centralized_metrics/tracking_test.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.centralized-metrics.tracking-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [react-native.platform :as platform] + [status-im.app-build.core :as build] + [status-im.contexts.centralized-metrics.tracking :as tracking])) + +(def platform-os platform/os) +(def app-version build/app-short-version) + +(deftest key-value-event-test + (testing "creates correct key-value event" + (let [event-name "test-event" + val-key :test-key + value "test-value" + expected {:metric + {:eventName event-name + :platform platform-os + :appVersion app-version + :eventValue {val-key value}}}] + (is (= expected (tracking/key-value-event event-name {val-key value})))))) + +(deftest user-journey-event-test + (testing "creates correct user journey event" + (let [action "test-action" + expected {:metric + {:eventName "user-journey" + :platform platform-os + :appVersion app-version + :eventValue {:action action}}}] + (is (= expected (tracking/user-journey-event action)))))) + +(deftest navigation-event-test + (testing "creates correct navigation event" + (let [view-id :test-view-id + expected {:metric + {:eventName "navigation" + :platform platform-os + :appVersion app-version + :eventValue {:viewId view-id}}}] + (is (= expected (tracking/navigation-event view-id)))))) + +(deftest track-view-id-event-test + (testing "returns correct navigation event for view-id" + (is (= [{:metric + {:eventName "navigation" + :platform platform-os + :appVersion app-version + :eventValue {:viewId "communities-stack"}}}] + (tracking/track-view-id-event :screen/communities-stack))) + (is (= [{:metric + {:eventName "navigation" + :platform platform-os + :appVersion app-version + :eventValue {:viewId "onboarding.create-profile-password"}}}] + (tracking/track-view-id-event :screen/onboarding.create-profile-password))) + (is (= [] (tracking/track-view-id-event :unknown-stack))))) + +(deftest tracked-event-test + (testing "returns correct event for given inputs" + (is (= {:metric + {:eventName "user-journey" + :platform platform-os + :appVersion app-version + :eventValue {:action tracking/app-started-event}}} + (tracking/metrics-event [:profile/get-profiles-overview-success]))) + (is (= {:metric + {:eventName "events.metrics-enabled" + :platform platform-os + :appVersion app-version + :eventValue {:enabled true}}} + (tracking/metrics-event [:centralized-metrics/toggle-centralized-metrics true]))) + (is (= [{:metric + {:eventName "navigation" + :platform platform-os + :appVersion app-version + :eventValue {:viewId "wallet-stack"}}}] + (tracking/metrics-event [:set-view-id :screen/wallet-stack]))) + (is (nil? (tracking/metrics-event [:unknown-event]))) + (is (= [{:metric + {:eventName "navigation" + :platform platform-os + :appVersion app-version + :eventValue {:viewId "onboarding.intro"}}}] + (tracking/metrics-event [:set-view-id :screen/onboarding.intro]))))) diff --git a/src/status_im/contexts/chat/actions/view.cljs b/src/status_im/contexts/chat/actions/view.cljs new file mode 100644 index 00000000000..822352b6f89 --- /dev/null +++ b/src/status_im/contexts/chat/actions/view.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.chat.actions.view + (:require + [quo.core :as quo] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn fetch-messages + [chat-id] + {:icon :i/download + :right-icon :i/chevron-right + :accessibility-label :chat-fetch-messages + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:chat/fetch-messages chat-id])) + :label (i18n/label :t/fetch-messages)}) + +(defn new-chat + [] + [quo/action-drawer + [[{:icon :i/new-message + :accessibility-label :start-a-new-chat + :label (i18n/label :t/new-chat) + :on-press (fn [] + (rf/dispatch [:group-chat/clear-contacts]) + (debounce/throttle-and-dispatch + [:open-modal :screen/start-a-new-chat] + 1000))} + {:icon :i/add-user + :accessibility-label :add-a-contact + :label (i18n/label :t/add-a-contact) + :sub-label (i18n/label :t/enter-chat-key) + :add-divider? true + :on-press #(debounce/throttle-and-dispatch + [:open-modal :screen/new-contact] + 1000)}]]]) diff --git a/src/status_im/contexts/chat/contacts/drawers/nickname_drawer/style.cljs b/src/status_im/contexts/chat/contacts/drawers/nickname_drawer/style.cljs new file mode 100644 index 00000000000..2e540424183 --- /dev/null +++ b/src/status_im/contexts/chat/contacts/drawers/nickname_drawer/style.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.chat.contacts.drawers.nickname-drawer.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn context-container + [theme] + {:flex-direction :row + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :border-radius 20 + :align-items :center + :align-self :flex-start + :margin-top 4 + :margin-left -4 + :margin-bottom 20}) + +(def buttons-container + {:flex-direction :row + :justify-content :space-between + :margin-top 20}) + +(defn nickname-container + [insets] + {:margin-horizontal 20 + :margin-bottom (when platform/ios? (max (:bottom insets) 20))}) + +(defn nickname-description + [theme] + {:margin-left 4 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def nickname-description-container + {:flex-direction :row + :align-items :center + :margin-top 8}) diff --git a/src/status_im/contexts/chat/contacts/drawers/nickname_drawer/view.cljs b/src/status_im/contexts/chat/contacts/drawers/nickname_drawer/view.cljs new file mode 100644 index 00000000000..fbaf680f31a --- /dev/null +++ b/src/status_im/contexts/chat/contacts/drawers/nickname_drawer/view.cljs @@ -0,0 +1,82 @@ +(ns status-im.contexts.chat.contacts.drawers.nickname-drawer.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.chat.contacts.drawers.nickname-drawer.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn add-nickname-and-show-toast + [primary-name entered-nickname public-key] + (when-not (string/blank? entered-nickname) + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:toasts/upsert + {:id :add-nickname + :type :positive + :text (i18n/label + :t/set-nickname-toast + {:primary-name primary-name + :nickname (string/trim entered-nickname)})}]) + (rf/dispatch [:contacts/update-nickname public-key (string/trim entered-nickname)]))) + +(defn nickname-drawer + [{:keys [contact]}] + (let [{:keys [primary-name nickname public-key]} contact + entered-nickname (reagent/atom (or nickname "")) + photo-path (rf/sub [:chats/photo-path public-key]) + theme (quo.context/use-theme)] + (fn [{:keys [title description accessibility-label + close-button-text]}] + [rn/view + {:style (style/nickname-container safe-area/insets) + :accessibility-label accessibility-label} + [quo/text + {:weight :semi-bold + :size :heading-2} title] + [rn/view {:style (style/context-container theme)} + [quo/context-tag + {:type :default + :blur? false + :profile-picture photo-path + :full-name primary-name + :size 24}]] + [quo/input + {:type :text + :blur? false + :placeholder (i18n/label :t/type-nickname) + :auto-focus true + :max-length constants/profile-name-max-length + :label (i18n/label :t/nickname) + :char-limit constants/profile-name-max-length + :on-change-text (fn [nickname] + (reset! entered-nickname nickname)) + :on-submit-editing #(add-nickname-and-show-toast primary-name @entered-nickname public-key)}] + [rn/view + {:style style/nickname-description-container} + [quo/icon :i/info + {:size 16 + :color (colors/theme-colors colors/black colors/white theme)}] + [quo/text + {:weight :regular + :size :paragraph-2 + :style (style/nickname-description theme)} + description]] + [rn/view {:style style/buttons-container} + [quo/button + {:type :grey + :container-style {:flex 0.48} + :on-press #(rf/dispatch [:hide-bottom-sheet])} + (or close-button-text (i18n/label :t/cancel))] + + [quo/button + {:type :primary + :disabled? (string/blank? @entered-nickname) + :container-style {:flex 0.48} + :on-press #(add-nickname-and-show-toast primary-name @entered-nickname public-key)} + title]]]))) diff --git a/src/status_im/contexts/chat/contacts/events.cljs b/src/status_im/contexts/chat/contacts/events.cljs new file mode 100644 index 00000000000..046166035d0 --- /dev/null +++ b/src/status_im/contexts/chat/contacts/events.cljs @@ -0,0 +1,162 @@ +(ns status-im.contexts.chat.contacts.events + (:require + [oops.core :as oops] + [re-frame.core :as re-frame] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defn <-rpc-js + [^js js-contact] + {:public-key (oops/oget js-contact "id") + :compressed-key (oops/oget js-contact "compressedKey") + :primary-name (oops/oget js-contact "primaryName") + :secondary-name (.-secondaryName js-contact) + :ens-name (.-name js-contact) + :nickname (.-localNickname js-contact) + :images (transforms/js->clj (oops/oget js-contact "images")) + :ens-verified (oops/oget js-contact "ensVerified") + :contact-request-state (oops/oget js-contact "contactRequestState") + :last-updated (oops/oget js-contact "lastUpdated") + :active? (oops/oget js-contact "active") + :blocked? (oops/oget js-contact "blocked") + :added? (oops/oget js-contact "added") + :has-added-us? (oops/oget js-contact "hasAddedUs") + :mutual? (oops/oget js-contact "mutual") + :emoji-hash (oops/oget js-contact "emojiHash") + :bio (oops/oget js-contact "bio") + :customization-color (-> js-contact + (oops/oget "customizationColor") + keyword + ;; newly created accounts + (or :blue))}) + +(defn prepare-events-for-contact + [db chats-js] + (fn [events {:keys [public-key has-added-us? blocked? contact-request-state] :as contact}] + (let [was-blocked? (get-in db [:contacts/contacts public-key :blocked?])] + (cond-> events + (and (not has-added-us?) (= constants/contact-request-state-none contact-request-state)) + (conj [:activity-center/remove-pending-contact-request public-key]) + + (and blocked? (not was-blocked?)) + (conj [:contacts/blocked contact chats-js]))))) + +(defn update-contacts + [contacts-cljs] + (fn [contacts] + (reduce (fn [contacts {:keys [public-key] :as contact}] + (update contacts public-key merge contact)) + contacts + contacts-cljs))) + +(rf/defn process-js-contacts + [{:keys [db]} response-js] + (let [contacts-js (oops/oget response-js "contacts") + contacts-cljs (map <-rpc-js contacts-js) + chats-js (.-chatsForContacts response-js) + events (reduce + (prepare-events-for-contact db chats-js) + [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]] + contacts-cljs)] + (js-delete response-js "contacts") + (js-delete response-js "chatsForContacts") + (merge + {:db (update db :contacts/contacts (update-contacts contacts-cljs)) + :utils/dispatch-later [{:ms 20 :dispatch [:process-response response-js]}]} + (when (> (count events) 1) + {:dispatch-n events})))) + +(re-frame/reg-event-fx :contacts/contacts-loaded + (fn [{:keys [db]} [loaded-contacts]] + (let [contacts (->> loaded-contacts + (map <-rpc-js) + (mapv (fn [{:keys [public-key] :as contact}] [public-key contact])) + (into {}))] + {:db (assoc db :contacts/contacts contacts)}))) + +(re-frame/reg-fx :contacts/initialize-contacts + (fn [] + (json-rpc/call {:method "wakuext_contacts" + :params [] + :js-response true + :on-success [:contacts/contacts-loaded] + :on-error [:log/error "failed to fetch contacts"]}))) + +(defn send-contact-request + [{:keys [db]} [id message]] + (when (not= id (get-in db [:profile/profile :public-key])) + {:fx [[:json-rpc/call + [{:method "wakuext_sendContactRequest" + :js-response true + :params [{:id id :message (or message (i18n/label :t/add-me-to-your-contacts))}] + :on-error [:contacts/send-contact-request-error id] + :on-success [:transport/message-sent]}]]]})) + +(rf/reg-event-fx :contact.ui/send-contact-request send-contact-request) + +(defn send-contact-request-error + [_ [id error]] + {:fx [[:effects.log/error + ["Failed to send contact request" + {:id id + :error error + :event :contact.ui/send-contact-request}]]]}) + +(rf/reg-event-fx :contact.ui/send-contact-request-error send-contact-request-error) + +(defn remove-contact + "Remove a contact from current account's contact list" + [{:keys [db]} [{:keys [public-key]}]] + {:db (-> db + (assoc-in [:contacts/contacts public-key :added?] false) + (assoc-in [:contacts/contacts public-key :active?] false) + (assoc-in [:contacts/contacts public-key :contact-request-state] + constants/contact-request-state-none)) + :fx [[:json-rpc/call + [{:method "wakuext_retractContactRequest" + :params [{:id public-key}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:contacts/remove-contact-error public-key]}]]]}) + +(rf/reg-event-fx :contact.ui/remove-contact-pressed remove-contact) + +(defn remove-contact-error + [_ [public-key error]] + {:fx [[:effects.log/error ["failed to remove contact" public-key error]]]}) + +(rf/reg-event-fx :contacts/remove-contact-error remove-contact-error) + +(defn update-nickname + [_ [public-key nickname]] + {:fx [[:json-rpc/call + [{:method "wakuext_setContactLocalNickname" + :params [{:id public-key :nickname nickname}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:contacts/update-nickname-error public-key nickname]}]]]}) + +(rf/reg-event-fx :contacts/update-nickname update-nickname) + +(defn update-nickname-error + [_ [public-key nickname error]] + {:fx [[:effects.log/error + ["failed to set contact nickname" + {:public-key public-key + :nickname nickname} + error]]]}) + +(rf/reg-event-fx :contacts/update-nickname-error update-nickname-error) + +(rf/reg-event-fx + :contacts/show-qr-code + (fn [{db :db} [public-key]] + {:db (assoc db :contacts/identity public-key) + :fx [[:dispatch + [:universal-links/generate-profile-url + {:public-key public-key + :on-success #(rf/dispatch [:open-modal :screen/share-contact])}]]]})) diff --git a/src/status_im/contexts/chat/contacts/events_test.cljs b/src/status_im/contexts/chat/contacts/events_test.cljs new file mode 100644 index 00000000000..8bf70248f48 --- /dev/null +++ b/src/status_im/contexts/chat/contacts/events_test.cljs @@ -0,0 +1,81 @@ +(ns status-im.contexts.chat.contacts.events-test + (:require + [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.constants :as constants] + [status-im.contexts.chat.contacts.events :as chat.contacts] + [utils.i18n :as i18n])) + +(deftest send-contact-request-test + (testing "creates nothing when attempting send contact request to self" + (let [profile-public-key "0x1" + cofx {:db {:profile/profile {:public-key profile-public-key}}}] + (is (match? nil (chat.contacts/send-contact-request cofx [profile-public-key]))))) + + (testing "creates contact request rpc with default message" + (let [profile-public-key "0x1" + contact-public-key "0x2" + cofx {:db {:profile/profile {:public-key profile-public-key}}}] + (is (match? + {:fx [[:json-rpc/call + [{:method "wakuext_sendContactRequest" + :js-response true + :params [{:id contact-public-key + :message (i18n/label :t/add-me-to-your-contacts)}] + :on-error [:contacts/send-contact-request-error contact-public-key] + :on-success [:transport/message-sent]}]]]} + (chat.contacts/send-contact-request cofx [contact-public-key]))))) + + (testing "creates contact request rpc with custom message" + (let [profile-public-key "0x1" + contact-public-key "0x2" + custom-message "Hey there!" + cofx {:db {:profile/profile {:public-key profile-public-key}}}] + (is (match? + {:fx [[:json-rpc/call + [{:method "wakuext_sendContactRequest" + :js-response true + :params [{:id contact-public-key + :message custom-message}] + :on-error [:contacts/send-contact-request-error contact-public-key] + :on-success [:transport/message-sent]}]]]} + (chat.contacts/send-contact-request cofx [contact-public-key custom-message])))))) + +(deftest remove-contact-test + (testing "removes existing contact" + (let [public-key "0x2" + initial-db {:contacts/contacts + {public-key + {:added? true + :active? true + :contact-request-state constants/contact-request-state-mutual}}} + expected-db {:contacts/contacts + {public-key + {:added? false + :active? false + :contact-request-state constants/contact-request-state-none}}} + expected-fx [[:json-rpc/call + [{:method "wakuext_retractContactRequest" + :params [{:id public-key}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:contacts/remove-contact-error public-key]}]]]] + (is (match? + {:db expected-db + :fx expected-fx} + (chat.contacts/remove-contact {:db initial-db} + [{:public-key public-key}])))))) + +(deftest update-nickname-test + (testing "updates contact nickname" + (let [public-key "0x2" + new-nickname "Joe" + expected-fx [[:json-rpc/call + [{:method "wakuext_setContactLocalNickname" + :params [{:id public-key :nickname new-nickname}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:contacts/update-nickname-error public-key new-nickname]}]]]] + (is (match? + {:fx expected-fx} + (chat.contacts/update-nickname {:db {}} [public-key new-nickname])))))) diff --git a/src/status_im/contexts/chat/effects.cljs b/src/status_im/contexts/chat/effects.cljs new file mode 100644 index 00000000000..e4d803e34f1 --- /dev/null +++ b/src/status_im/contexts/chat/effects.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.chat.effects + (:require + [react-native.async-storage :as async-storage] + [status-im.contexts.chat.messenger.messages.list.state :as chat.state] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.chat/open-last-chat + (fn [key-uid] + (async-storage/get-item + :chat-id + (fn [chat-id] + (when chat-id + (async-storage/get-item + :key-uid + (fn [stored-key-uid] + (when (= stored-key-uid key-uid) + (rf/dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]))))))))) + +(rf/reg-fx :effects.chat/scroll-to-bottom + (fn [] + (some-> ^js @chat.state/messages-list-ref + (.scrollToOffset #js + {:animated true})))) diff --git a/src/status_im/contexts/chat/events.cljs b/src/status_im/contexts/chat/events.cljs new file mode 100644 index 00000000000..0dcfa035a24 --- /dev/null +++ b/src/status_im/contexts/chat/events.cljs @@ -0,0 +1,456 @@ +(ns status-im.contexts.chat.events + (:require + [clojure.set :as set] + [legacy.status-im.chat.models.loading :as loading] + [legacy.status-im.data-store.chats :as chats-store] + [re-frame.core :as re-frame] + status-im.common.lightbox.events + [status-im.common.muting.helpers :refer [format-mute-till]] + [status-im.constants :as constants] + [status-im.contexts.chat.contacts.events :as contacts-store] + status-im.contexts.chat.effects + status-im.contexts.chat.group.events + [status-im.contexts.chat.messenger.composer.link-preview.events :as link-preview] + status-im.contexts.chat.messenger.messages.content.reactions.events + [status-im.contexts.chat.messenger.messages.delete-message-for-me.events :as delete-for-me] + [status-im.contexts.chat.messenger.messages.delete-message.events :as delete-message] + [status-im.contexts.chat.messenger.messages.list.state :as chat.state] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defn- get-chat + [cofx chat-id] + (get-in cofx [:db :chats chat-id])) + +(defn multi-user-chat? + ([chat] + (:group-chat chat)) + ([cofx chat-id] + (multi-user-chat? (get-chat cofx chat-id)))) + +(defn public-chat? + ([{:keys [chat-type]}] + (= chat-type constants/public-chat-type)) + ([cofx chat-id] + (public-chat? (get-chat cofx chat-id)))) + +(defn community-chat? + ([{:keys [chat-type]}] + (= chat-type constants/community-chat-type)) + ([cofx chat-id] + (community-chat? (get-chat cofx chat-id)))) + +(defn group-chat? + ([chat] + (and (multi-user-chat? chat) + (not (:public? chat)) + (not= (:chat-type chat) constants/community-chat-type))) + ([cofx chat-id] + (group-chat? (get-chat cofx chat-id)))) + +(defn- create-new-chat + [chat-id {:keys [db now]}] + (let [name (get-in db [:contacts/contacts chat-id :name])] + {:chat-id chat-id + :name (or name "") + :chat-type constants/one-to-one-chat-type + :group-chat false + :timestamp now + :contacts #{chat-id} + :last-clock-value 0})) + +(defn map-chats + [{:keys [db] :as cofx}] + (fn [chat] + (let [base-chat (or (get (:chats db) (:chat-id chat)) + (create-new-chat (:chat-id chat) cofx))] + (assoc (merge + (cond-> base-chat + (comp not :muted) (dissoc base-chat :muted-till)) + chat) + :invitation-admin + (:invitation-admin chat))))) + +(defn ensure-chats + [{:keys [db] :as cofx} [chats]] + (let [{:keys [view-id current-chat-id]} db + {:keys [all-chats chats-home-list removed-chats]} + (reduce + (fn [acc {:keys [chat-id community-id active muted] :as chat}] + (if (not (or active muted)) + (update acc :removed-chats conj chat-id) + (cond-> acc + (and (not community-id) active) + (update :chats-home-list conj chat-id) + :always + (assoc-in [:all-chats chat-id] chat)))) + {:all-chats {} + :chats-home-list #{} + :removed-chats #{}} + (map (map-chats cofx) chats))] + {:db (-> db + (update :chats merge all-chats) + (update :chats-home-list set/union chats-home-list) + (update :chats #(apply dissoc % removed-chats)) + (update :chats-home-list set/difference removed-chats)) + :fx [(when (not-empty removed-chats) + [:effects/push-notifications-clear-message-notifications removed-chats]) + (when (and (= view-id :screen/chat) (removed-chats current-chat-id)) + [:dispatch [:navigate-back]])]})) + +(re-frame/reg-event-fx :chat/ensure-chats ensure-chats) + +(rf/defn clear-history + "Clears history of the particular chat" + [{:keys [db]} chat-id remove-chat?] + (let [{:keys [last-message public? + deleted-at-clock-value]} + (get-in db [:chats chat-id]) + last-message-clock-value (if (and public? remove-chat?) + 0 + (or (:clock-value last-message) + deleted-at-clock-value + (datetime/timestamp)))] + {:db (-> db + (assoc-in [:messages chat-id] {}) + (update-in [:message-lists] dissoc chat-id) + (update :chats + (fn [chats] + (if (contains? chats chat-id) + (update chats + chat-id + merge + {:last-message nil + :unviewed-messages-count 0 + :unviewed-mentions-count 0 + :deleted-at-clock-value last-message-clock-value}) + chats))))})) + +(rf/defn offload-messages + {:events [:chat/offload-messages]} + [{:keys [db]} chat-id] + {:db (-> db + (update :messages dissoc chat-id) + (update :message-lists dissoc chat-id) + (update :pagination-info dissoc chat-id))}) + +(rf/defn close-chat + {:events [:chat/close]} + [{:keys [db] :as cofx} chat-id] + (let [current-chat-id (:current-chat-id db) + chat-id (or chat-id current-chat-id)] + (when (and current-chat-id (= chat-id current-chat-id)) + (chat.state/reset-visible-item) + (rf/merge cofx + {:db (-> db + (dissoc :current-chat-id) + (assoc-in [:chat/inputs chat-id :focused?] false)) + :effects.async-storage/set {:chat-id nil + :key-uid nil}} + (link-preview/reset-all) + (fn [cofx] + (delete-for-me/sync-all cofx)) + (fn [cofx] + (delete-message/send-all cofx)) + (offload-messages chat-id))))) + +(rf/defn deactivate-chat + "Deactivate chat in db, no side effects" + [{:keys [db] :as cofx} chat-id] + (rf/merge + cofx + {:db (-> (if (get-in db [:chats chat-id :muted]) + (assoc-in db [:chats chat-id :active] false) + (update db :chats dissoc chat-id)) + (update :chats-home-list disj chat-id)) + :json-rpc/call [{:method "wakuext_deactivateChat" + :params [{:id chat-id :preserveHistory true}] + :on-success #() + :on-error #(log/error "failed to create public chat" chat-id %)}]} + (clear-history chat-id true) + (when (= chat-id (:current-chat-id db)) + (close-chat chat-id)))) + +(rf/defn force-close-chat + [{:keys [db] :as cofx} chat-id] + (do + (chat.state/reset-visible-item) + (rf/merge cofx + {:db (dissoc db :current-chat-id)} + (offload-messages chat-id)))) + +(rf/defn show-more-chats + {:events [:chat/show-more-chats]} + [{:keys [db]}] + (when (< (:home-items-show-number db) (count (:chats db))) + {:db (update db :home-items-show-number + 40)})) + +(rf/defn preload-chat-data + "Takes chat-id and coeffects map, returns effects necessary when navigating to chat" + {:events [:chat/preload-data]} + [cofx chat-id] + (loading/load-messages cofx chat-id)) + +(rf/defn navigate-to-chat + "Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data" + {:events [:chat/navigate-to-chat]} + [{db :db :as cofx} chat-id animation] + (rf/merge cofx + (when-not (:current-chat-id db) + {:dispatch [:navigate-to :screen/chat {:animation animation}]}) + (close-chat chat-id) + (force-close-chat chat-id) + (fn [{:keys [db]}] + {:db (assoc db :current-chat-id chat-id)}) + (preload-chat-data chat-id) + #(when (group-chat? cofx chat-id) + (loading/load-chat % chat-id)))) + +(rf/defn pop-to-root-and-navigate-to-chat + {:events [:chat/pop-to-root-and-navigate-to-chat]} + [cofx chat-id animation] + (rf/merge + cofx + {:dispatch-later {:ms 500 + :dispatch [:chat/navigate-to-chat chat-id animation]}} + (navigation/pop-to-root :screen/shell-stack))) + +(rf/defn handle-clear-history-response + {:events [:chat/history-cleared]} + [{:keys [db]} chat-id response] + (let [chat (chats-store/<-rpc (first (:chats response)))] + {:db (assoc-in db [:chats chat-id] chat)})) + +(rf/defn handle-one-to-one-chat-created + {:events [:chat/one-to-one-chat-created]} + [{:keys [db]} chat-id response-js] + (let [chat (chats-store/<-rpc (first (transforms/js->clj (.-chats ^js response-js)))) + contact-js (first (.-contacts ^js response-js)) + contact (when contact-js (contacts-store/<-rpc-js contact-js))] + {:db (cond-> db + contact + (assoc-in [:contacts/contacts chat-id] contact) + :always + (assoc-in [:chats chat-id] chat) + :always + (update :chats-home-list conj chat-id)) + :dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]})) + +(rf/defn start-chat + "Start a chat, making sure it exists" + {:events [:chat.ui/start-chat]} + [cofx chat-id ens-name] + (when (not= (get-in cofx [:db :profile/profile :public-key]) chat-id) + {:json-rpc/call [{:method "wakuext_createOneToOneChat" + :params [{:id chat-id :ensName ens-name}] + :js-response true + :on-success #(rf/dispatch [:chat/one-to-one-chat-created chat-id %]) + :on-error #(log/error "failed to create one-to-on chat" chat-id %)}]})) + +(rf/defn clear-history-handler + "Clears history of the particular chat" + {:events [:chat.ui/clear-history]} + [{:keys [db] :as cofx} chat-id remove-chat?] + (rf/merge cofx + {:db db + :json-rpc/call [{:method "wakuext_clearHistory" + :params [{:id chat-id}] + :on-success #(rf/dispatch [:chat/history-cleared chat-id %]) + :on-error #(log/error "failed to clear history " chat-id %)}]} + (clear-history chat-id remove-chat?))) + +(rf/defn close-and-remove-chat + "Closes the chat and removes it from chat list while retaining history, producing all necessary effects for that" + {:events [:chat.ui/close-and-remove-chat]} + [cofx chat-id] + (rf/merge cofx + {:effects/push-notifications-clear-message-notifications [chat-id]} + (deactivate-chat chat-id) + (offload-messages chat-id))) + +(rf/defn unmute-chat-community + {:events [:chat/unmute-chat-community]} + [{:keys [db]} chat-id] + (let [{:keys [community-id]} (get-in db [:chats chat-id])] + (when community-id + {:db (assoc-in db [:communities community-id :muted] false)}))) + +(rf/defn mute-chat-failed + {:events [:chat/mute-failed]} + [{:keys [db]} chat-id muted? error] + (log/error "mute chat failed" chat-id error) + {:db (assoc-in db [:chats chat-id :muted] (not muted?))}) + +(rf/defn mute-chat-toggled-successfully + {:events [:chat/mute-successfully]} + [{:keys [db]} chat-id muted-till mute-type muted? chat-type] + (log/debug "muted chat successfully" chat-id " for" muted-till) + (when-not muted? + (rf/dispatch [:chat/unmute-chat-community chat-id])) + (let [time-string (fn [duration-kw unmute-time] + (i18n/label duration-kw {:duration unmute-time})) + not-community-chat? #(contains? #{constants/public-chat-type + constants/private-group-chat-type + constants/one-to-one-chat-type} + %) + mute-duration-text + (fn [unmute-time] + (if unmute-time + (str + (condp = mute-type + constants/mute-for-15-mins-type (time-string + (if (not-community-chat? chat-type) + :t/chat-muted-for-15-minutes + :t/channel-muted-for-15-minutes) + unmute-time) + constants/mute-for-1-hour-type (time-string + (if (not-community-chat? chat-type) + :t/chat-muted-for-1-hour + :t/channel-muted-for-1-hour) + unmute-time) + constants/mute-for-8-hours-type (time-string + (if (not-community-chat? chat-type) + :t/chat-muted-for-8-hours + :t/channel-muted-for-8-hours) + unmute-time) + constants/mute-for-1-week (time-string + (if (not-community-chat? chat-type) + :t/chat-muted-for-1-week + :t/channel-muted-for-1-week) + unmute-time) + constants/mute-till-unmuted (time-string + (if (not-community-chat? chat-type) + :t/chat-muted-till-unmuted + :t/channel-muted-till-unmuted) + unmute-time))) + (i18n/label (if (not-community-chat? chat-type) + :t/chat-unmuted-successfully + :t/channel-unmuted-successfully))))] + {:db (assoc-in db [:chats chat-id :muted-till] muted-till) + :dispatch [:toasts/upsert + (cond-> {:type :positive + :id :mute-chat-toast + :text (mute-duration-text (when (some? muted-till) + (str (format-mute-till muted-till))))} + muted? (assoc :duration constants/mute-undo-time-limit-ms + :undo-duration (/ constants/mute-undo-time-limit-ms 1000) + :undo-on-press #(rf/dispatch [:chat.ui/undo-mute chat-id])))]})) + +(rf/defn mute-chat + {:events [:chat.ui/mute]} + [{:keys [db]} chat-id muted? mute-type] + (let [method (if muted? "wakuext_muteChatV2" "wakuext_unmuteChat") + params (if muted? [{:chatId chat-id :mutedType mute-type}] [chat-id]) + chat-type (get-in db [:chats chat-id :chat-type])] + {:db (assoc-in db [:chats chat-id :muted] muted?) + :json-rpc/call [{:method method + :params params + :on-error #(rf/dispatch [:chat/mute-failed chat-id muted? %]) + :on-success #(rf/dispatch [:chat/mute-successfully chat-id % mute-type + muted? chat-type])}]})) + +(rf/reg-event-fx + :chat.ui/undo-mute + (fn [_ [chat-id]] + {:fx [[:json-rpc/call + [{:method "wakuext_unmuteChat" + :params [chat-id] + :on-error #(rf/dispatch [:chat/mute-failed chat-id false %]) + :on-success #(rf/dispatch [:chat/undo-mute-success chat-id])}]]]})) + +(rf/reg-event-fx + :chat/undo-mute-success + (fn [{:keys [db]} [chat-id]] + {:db (update-in db + [:chats chat-id] + (fn [chat] + (-> chat + (dissoc :muted-till) + (assoc :muted false)))) + :fx [[:dispatch [:toasts/close :mute-chat-toast]] + [:dispatch [:chat/unmute-chat-community chat-id]]]})) + +(rf/defn show-clear-history-confirmation + {:events [:chat.ui/show-clear-history-confirmation]} + [_ chat-id] + {:ui/show-confirmation + {:title (i18n/label :t/clear-history-title) + :content (i18n/label :t/clear-history-confirmation-content) + :confirm-button-text (i18n/label :t/clear-history-action) + :on-accept #(do + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:chat.ui/clear-history chat-id false]))}}) + +(rf/defn show-remove-chat-confirmation + {:events [:chat.ui/show-close-confirmation]} + [_ chat-id] + {:ui/show-confirmation + {:title (i18n/label :t/delete-confirmation) + :content (i18n/label :t/close-chat-confirmation) + :confirm-button-text (i18n/label :t/delete) + :on-accept #(do + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:chat.ui/close-and-remove-chat chat-id]))}}) + +(rf/defn navigate-to-user-pinned-messages + "Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data" + {:events [:chat.ui/navigate-to-pinned-messages]} + [cofx chat-id] + (navigation/navigate-to cofx :chat-pinned-messages {:chat-id chat-id})) + +(rf/defn check-last-chat + {:events [:chat/check-last-chat]} + [{:keys [db]}] + {:effects.chat/open-last-chat (get-in db [:profile/profile :key-uid])}) + +(rf/defn status-tag-pressed + {:events [:communities/status-tag-pressed]} + [{:keys [db] :as cofx} community-id literal] + (let [{:keys [id]} (some #(when (= (:name %) literal) %) + (vals (get-in db [:communities community-id :chats])))] + (when (and id + (not= (:current-chat-id db) (str community-id id))) + (navigate-to-chat cofx (str community-id id) nil)))) + +(rf/defn fetch-messages + {:events [:chat/fetch-messages]} + [_ chat-id] + {:json-rpc/call [{:method "wakuext_fetchMessages" + :params [{:id chat-id}] + :on-success #() + :on-error #(log/error "failed to fetch messages for chat" chat-id %)}]}) + +(rf/defn scroll-to-bottom + {:events [:chat.ui/scroll-to-bottom]} + [_] + {:effects.chat/scroll-to-bottom nil}) + +(rf/reg-event-fx :chat.ui/clear-sending-images + (fn [{:keys [db]}] + {:db (update-in db [:chat/inputs (:current-chat-id db) :metadata] assoc :sending-image {})})) + +(rf/reg-event-fx :chat.ui/image-unselected + (fn [{:keys [db]} [original]] + (let [current-chat-id (:current-chat-id db)] + {:db (update-in db + [:chat/inputs current-chat-id :metadata :sending-image] + dissoc + (:uri original))}))) + +(rf/reg-event-fx :chat.ui/show-profile + (fn [{:keys [db]} [public-key ens-name]] + (let [my-public-key (get-in db [:profile/profile :public-key])] + (if (not= my-public-key public-key) + {:db (-> db + (assoc :contacts/identity public-key) + (assoc :contacts/ens-name ens-name)) + :dispatch [:contacts/build-contact + {:pubkey public-key + :ens ens-name + :success-fn (fn [_] + {:dispatch [:open-modal :screen/contact-profile]})}]} + {:dispatch [:open-modal :screen/settings]})))) diff --git a/src/status_im/contexts/chat/events_test.cljs b/src/status_im/contexts/chat/events_test.cljs new file mode 100644 index 00000000000..e0ad6ebce34 --- /dev/null +++ b/src/status_im/contexts/chat/events_test.cljs @@ -0,0 +1,81 @@ +(ns status-im.contexts.chat.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.chat.events :as chat] + [utils.datetime :as datetime])) + +(deftest clear-history-test + (let [chat-id "1" + cofx {:db {:message-lists {chat-id [{:something "a"}]} + :chats {chat-id {:last-message {:clock-value 10} + :unviewed-messages-count 1}}}}] + (testing "it deletes all the messages" + (let [actual (chat/clear-history cofx chat-id true)] + (is (= {} (get-in actual [:db :messages chat-id]))))) + (testing "it deletes all the message groups" + (let [actual (chat/clear-history cofx chat-id true)] + (is (= nil (get-in actual [:db :message-lists chat-id]))))) + (testing "it deletes unviewed messages set" + (let [actual (chat/clear-history cofx chat-id true)] + (is (= 0 (get-in actual [:db :chats chat-id :unviewed-messages-count]))))) + (testing "it sets a deleted-at-clock-value equal to the last message clock-value" + (let [actual (chat/clear-history cofx chat-id true)] + (is (= 10 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) + (testing "it does not override the deleted-at-clock-value when there are no messages" + (let [actual (chat/clear-history (update-in cofx + [:db :chats chat-id] + assoc + :last-message nil + :deleted-at-clock-value 100) + chat-id + true)] + (is (= 100 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) + (testing "it set the deleted-at-clock-value to now the chat has no messages nor previous deleted-at" + (with-redefs [datetime/timestamp (constantly 42)] + (let [actual (chat/clear-history (update-in cofx + [:db :chats chat-id] + assoc + :last-message + nil) + chat-id + true)] + (is (= 42 (get-in actual [:db :chats chat-id :deleted-at-clock-value])))))))) + +(deftest remove-chat-test + (let [chat-id "1" + cofx {:db {:current-chat-id chat-id + :chats-home-list #{chat-id}}}] + (testing "it removes the chat with given id" + (let [actual (chat/close-and-remove-chat cofx chat-id)] + (is (= nil (get-in actual [:db :chats-home-list chat-id]))))))) + +(deftest multi-user-chat?-test + (let [chat-id "1"] + (testing "it returns true if it's a group chat" + (let [cofx {:db {:chats {chat-id {:group-chat true}}}}] + (is (chat/multi-user-chat? cofx chat-id)))) + (testing "it returns true if it's a public chat" + (let [cofx {:db {:chats {chat-id {:public? true :group-chat true}}}}] + (is (chat/multi-user-chat? cofx chat-id)))) + (testing "it returns false if it's a 1-to-1 chat" + (let [cofx {:db {:chats {chat-id {}}}}] + (is (not (chat/multi-user-chat? cofx chat-id))))))) + +(deftest group-chat?-test + (let [chat-id "1"] + (testing "it returns true if it's a group chat" + (let [cofx {:db {:chats {chat-id {:group-chat true}}}}] + (is (chat/group-chat? cofx chat-id)))) + (testing "it returns false if it's a public chat" + (let [cofx {:db {:chats {chat-id {:public? true :group-chat true}}}}] + (is (not (chat/group-chat? cofx chat-id))))) + (testing "it returns false if it's a 1-to-1 chat" + (let [cofx {:db {:chats {chat-id {}}}}] + (is (not (chat/group-chat? cofx chat-id))))))) + +(deftest navigate-to-chat-test + (let [chat-id "test_chat" + db {:pagination-info {chat-id {:all-loaded? true}}}] + (testing "Pagination info should be reset on navigation" + (let [res (chat/navigate-to-chat {:db db} chat-id nil)] + (is (nil? (get-in res [:db :pagination-info chat-id :all-loaded?]))))))) diff --git a/src/status_im/contexts/chat/group/common/group_edit.cljs b/src/status_im/contexts/chat/group/common/group_edit.cljs new file mode 100644 index 00000000000..8cc8b3d172c --- /dev/null +++ b/src/status_im/contexts/chat/group/common/group_edit.cljs @@ -0,0 +1,134 @@ +(ns status-im.contexts.chat.group.common.group-edit + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.avatar-picture-picker.view :as avatar-picture-picker] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.constants :as constants] + [status-im.contexts.chat.group.common.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.debounce :as debounce] + utils.emojilib + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.responsiveness :as responsiveness])) + +(defn avatar + [{:keys [customization-color group-image set-group-image]}] + (let [on-press (rn/use-callback (fn [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [avatar-picture-picker/view + {:on-result set-group-image}])}])))] + [rn/view {:style style/avatar} + ;;NOTE with hole-view group-avatar doesn't change it's background color + #_[hole-view/hole-view + {:holes [style/hole]}] + [quo/group-avatar + {:customization-color customization-color + :size :size-80 + :picture group-image}] + [quo/button + {:on-press on-press + :container-style style/camera + :icon-only? true + :type :grey + :background :photo + :size 32} + :i/camera]])) + +(defn view + [{:keys [default-group-name default-group-color default-group-image contacts + submit-button-label submit-event back-button-icon chat-id close on-success editing-group?]}] + (let [theme (quo.context/use-theme) + {window-width :width} (rn/get-window) + contacts-count (count contacts) + [group-name set-group-name] (rn/use-state default-group-name) + [error-message + set-error-message] (rn/use-state nil) + group-name-empty? (not (and (string? group-name) (not-empty group-name))) + [group-color set-group-color] (rn/use-state default-group-color) + [group-image set-group-image] (rn/use-state default-group-image) + on-submit (rn/use-callback #(debounce/throttle-and-dispatch + [submit-event + {:group-name group-name + :group-color group-color + :group-image (when (not= group-image + default-group-image) + group-image) + :chat-id chat-id + :on-success on-success}] + 300) + [group-name group-color group-image]) + on-change-text (rn/use-callback + (fn [text] + (if (boolean (re-find utils.emojilib/emoji-regex text)) + (set-error-message (i18n/label :t/are-not-allowed + {:check (i18n/label + :t/emojis)})) + (set-error-message nil)) + (set-group-name text))) + disabled? (or group-name-empty? + error-message + (and editing-group? + (= group-name default-group-name) + (= group-color default-group-color) + (= group-image default-group-image)))] + [floating-button-page/view + {:customization-color group-color + :gradient-cover? true + :header-container-style {:margin-top 8} + :header [quo/page-nav + {:background :photo + :type :no-title + :icon-name back-button-icon + :on-press close}] + :footer [quo/button + {:customization-color group-color + :disabled? disabled? + :on-press on-submit} + submit-button-label]} + [:<> + [avatar + {:customization-color group-color + :group-image group-image + :set-group-image set-group-image}] + [quo/title-input + {:on-change-text on-change-text + :default-value default-group-name + :container-style {:padding-horizontal 20 :padding-top 12 :padding-bottom (if error-message 0 16)} + :placeholder (i18n/label :t/name-your-group) + :max-length constants/max-group-chat-name-length}] + (when error-message + [quo/info-message + {:status :error + :icon :i/info + :size :default + :container-style {:margin-top 8 :margin-left 20 :margin-bottom 16}} + error-message]) + [quo/divider-line] + [rn/view + {:style {:padding-vertical 7}} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/color-label theme)} + (i18n/label :t/accent-colour)] + [quo/color-picker + {:default-selected group-color + :on-change set-group-color + :container-style {:padding-top 15 + :padding-bottom 9 + :padding-left (responsiveness/iphone-11-Pro-20-pixel-from-width + window-width)}}]] + [quo/divider-label + (i18n/label :t/n-m-people {:n contacts-count :m constants/max-group-chat-participants})] + [rn/view {:style style/tags} + (for [contact contacts] + ^{:key contact} + [quo/context-tag + {:container-style style/tag + :size 24 + :profile-picture (profile.utils/photo contact) + :full-name (profile.utils/displayed-name contact)}])]]])) diff --git a/src/status_im/contexts/chat/group/common/style.cljs b/src/status_im/contexts/chat/group/common/style.cljs new file mode 100644 index 00000000000..3e4d4af94e9 --- /dev/null +++ b/src/status_im/contexts/chat/group/common/style.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.chat.group.common.style + (:require [quo.foundations.colors :as colors])) + +(def avatar {:width 88 :margin-top 12 :margin-left 20}) + +(def camera {:position :absolute :right 0 :bottom 0}) + +(defn color-label + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :padding-horizontal 20}) + +(def tags {:flex-direction :row :flex-wrap :wrap :padding-horizontal 20 :padding-top 12}) + +(def tag {:margin-right 8 :margin-bottom 8}) diff --git a/src/status_im/contexts/chat/group/create/view.cljs b/src/status_im/contexts/chat/group/create/view.cljs new file mode 100644 index 00000000000..0e2768ee23a --- /dev/null +++ b/src/status_im/contexts/chat/group/create/view.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.chat.group.create.view + (:require + [clojure.string :as string] + [quo.foundations.colors :as colors] + [status-im.constants :as constants] + [status-im.contexts.chat.group.common.group-edit :as group-edit] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [close]}] + (let [contacts (rf/sub [:selected-group-contacts]) + contact-string (string/join ", " (map profile.utils/displayed-name contacts)) + default-group-name (subs contact-string + 0 + (min (count contact-string) constants/max-group-chat-name-length)) + profile (rf/sub [:profile/profile-with-image]) + contacts (conj contacts profile)] + [group-edit/view + {:default-group-name default-group-name + :default-group-color (rand-nth colors/account-colors) + :contacts contacts + :submit-button-label (i18n/label :t/create-group-chat) + :submit-event :group-chat/create + :back-button-icon :i/arrow-left + :close close}])) diff --git a/src/status_im/contexts/chat/group/details/style.cljs b/src/status_im/contexts/chat/group/details/style.cljs new file mode 100644 index 00000000000..4fd1467a34e --- /dev/null +++ b/src/status_im/contexts/chat/group/details/style.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.chat.group.details.style + (:require + [quo.foundations.colors :as colors])) + +(def actions-view + {:margin-top 8 + :margin-bottom 20 + :padding-horizontal 20}) + +(defn close-icon + [theme] + {:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :margin-left 20 + :width 32 + :height 32 + :border-radius 10 + :justify-content :center + :align-items :center + :margin-bottom 24}) + +(defn bottom-container + [bottom theme] + {:padding-horizontal 20 + :padding-vertical 12 + :margin-bottom bottom + :background-color (colors/theme-colors colors/white colors/neutral-95-opa-70 theme) + :flex-direction :row}) + +(def manage-members-header + {:flex-direction :row + :justify-content :space-between + :align-items :flex-end + :margin-horizontal 20 + :margin-bottom 16}) + +(defn counter + [theme error?] + {:margin-bottom 2 + :color (if error? + (colors/theme-colors colors/danger-50 colors/danger-60 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))}) diff --git a/src/status_im/contexts/chat/group/details/view.cljs b/src/status_im/contexts/chat/group/details/view.cljs new file mode 100644 index 00000000000..fc607d71eec --- /dev/null +++ b/src/status_im/contexts/chat/group/details/view.cljs @@ -0,0 +1,207 @@ +(ns status-im.contexts.chat.group.details.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.safe-area :as safe-area] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.common.contact-list.view :as contact-list] + [status-im.common.home.actions.view :as actions] + [status-im.constants :as constants] + [status-im.contexts.chat.group.details.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn group-chat-member-toggle + [member? selected? public-key] + (if-not member? + (if selected? + (rf/dispatch [:select-participant public-key true]) + (rf/dispatch [:deselect-participant public-key true])) + (if selected? + (rf/dispatch [:undo-deselect-member public-key true]) + (rf/dispatch [:deselect-member public-key true])))) + +(defn add-member-contact-item-render + [{:keys [public-key] :as item} _ _ {:keys [group admin? current-pk chat-id]}] + (let [{:keys [contacts]} group + member? (contains? contacts public-key) + [checked? set-checked] (rn/use-state member?) + selected-contacts-count (rf/sub [:group-chat/manage-members-count chat-id]) + disabled? (or (= current-pk public-key) (and (not admin?) member?)) + on-toggle (rn/use-callback + (fn [value] + (let [error? (and value + (= constants/max-group-chat-participants + selected-contacts-count))] + (rf/dispatch [:group-chat/set-manage-members-error + {:chat-id chat-id + :error? error?}]) + (when-not error? + (group-chat-member-toggle member? + value + public-key) + (set-checked value)))) + [selected-contacts-count member? public-key])] + [contact-list-item/contact-list-item + {:on-press on-toggle + :allow-multiple-presses? true + :disabled? disabled? + :accessory {:type :checkbox + :disabled? disabled? + :checked? checked? + :on-check on-toggle}} + item])) + +(defn add-manage-members + [{:keys [on-scroll]}] + (let [theme (quo.context/use-theme) + selected-participants (rf/sub [:group-chat/selected-participants]) + deselected-members (rf/sub [:group-chat/deselected-members]) + chat-id (quo.context/use-screen-params) + {:keys [admins] :as group} (rf/sub [:chats/chat-by-id chat-id]) + current-pk (rf/sub [:multiaccount/public-key]) + admin? (get admins current-pk) + selected-contacts-count (rf/sub [:group-chat/manage-members-count chat-id]) + error? (rf/sub [:group-chat/manage-members-error-by-chat-id chat-id])] + (rn/use-mount (fn [] + (rf/dispatch [:group/clear-added-participants]) + (rf/dispatch [:group/clear-removed-members]))) + [rn/view {:flex 1 :margin-top 20} + [rn/touchable-opacity + {:on-press #(rf/dispatch [:navigate-back]) + :accessibility-label :close-manage-members + :style (style/close-icon theme)} + [quo/icon :i/close {:color (colors/theme-colors colors/neutral-100 colors/white theme)}]] + [rn/view + {:style style/manage-members-header} + [quo/text + {:size :heading-1 + :weight :semi-bold} + (i18n/label (if admin? :t/manage-members :t/add-members))] + [quo/text + {:size :paragraph-2 + :weight :regular + :style (style/counter theme error?)} + (i18n/label :t/selected-count-from-max + {:selected selected-contacts-count + :max constants/max-group-chat-participants})]] + [gesture/section-list + {:key-fn :public-key + :on-scroll on-scroll + :sticky-section-headers-enabled false + :sections (rf/sub [:contacts/grouped-by-first-letter]) + :render-section-header-fn contact-list/contacts-section-header + :render-section-footer-fn contact-list/contacts-section-footer + :content-container-style {:padding-bottom 20} + :render-data {:group group + :admin? admin? + :current-pk current-pk + :chat-id chat-id} + :render-fn add-member-contact-item-render}] + [rn/view {:style (style/bottom-container 30 theme)} + [quo/button + {:container-style {:flex 1} + :type :primary + :accessibility-label :save + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (js/setTimeout (fn [] + (rf/dispatch + [:group-chats.ui/remove-members-pressed chat-id])) + 500) + (rf/dispatch [:group-chats.ui/add-members-pressed chat-id])) + :disabled? (and (zero? (count selected-participants)) + (zero? (count deselected-members)))} + (i18n/label :t/save)]]])) + +(defn contact-item-render + [{:keys [public-key] :as item} _ _ extra-data] + (let [current-pk (rf/sub [:multiaccount/public-key]) + show-profile-actions #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [actions/contact-actions item + extra-data])}])] + [contact-list-item/contact-list-item + (when (not= public-key current-pk) + {:on-press #(rf/dispatch [:chat.ui/show-profile public-key]) + :allow-multiple-presses? true + :on-long-press show-profile-actions + :accessory {:type :options + :on-press show-profile-actions}}) + item])) + +(defn contacts-section-header + [{:keys [title]}] + [quo/divider-label {:tight? true} title]) + +(defn contacts-section-footer + [_] + [rn/view {:style {:height 8}}]) + +(defn view + [] + (let [chat-id (quo.context/use-screen-params) + {:keys [admins chat-id chat-name color muted contacts image] + :as group} (rf/sub [:chats/chat-by-id chat-id]) + members (rf/sub [:contacts/group-members-sections chat-id]) + pinned-messages (rf/sub [:chats/pinned chat-id]) + current-pk (rf/sub [:multiaccount/public-key]) + admin? (get admins current-pk)] + [:<> + [quo/gradient-cover + {:height 286 + :customization-color color}] + [quo/page-nav + {:type :no-title + :margin-top safe-area/top + :background :photo + :right-side [{:icon-name :i/options + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [actions/group-details-actions + group])}])}] + :icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back])}] + [quo/page-top + {:title chat-name + :avatar {:group? true + :picture (when image {:uri image}) + :customization-color color}}] + [quo/channel-actions + {:container-style style/actions-view + :actions [{:big? (not admin?) + :accessibility-label :pinned-messages + :label (i18n/label :t/pinned-messages) + :customization-color color + :icon :i/pin + :counter-value (count pinned-messages) + :on-press (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:pin-message/show-pins-bottom-sheet + chat-id]))} + {:accessibility-label :toggle-mute + :customization-color color + :icon (if muted :i/muted :i/activity-center) + :label (i18n/label (if muted :unmute-group :mute-group)) + :on-press #(rf/dispatch [:chat.ui/mute chat-id (not muted) + (when-not muted + constants/mute-till-unmuted)])} + {:accessibility-label :manage-members + :customization-color color + :icon :i/add-user + :label (i18n/label (if admin? :t/manage-members :t/add-members)) + :counter-value (count contacts) + :on-press #(rf/dispatch [:open-modal + :screen/group-add-manage-members + chat-id])}]}] + [rn/section-list + {:key-fn :title + :sticky-section-headers-enabled false + :sections members + :render-section-header-fn contacts-section-header + :render-section-footer-fn contacts-section-footer + :render-data {:chat-id chat-id + :admin? admin?} + :render-fn contact-item-render + :separator [rn/view {:style {:height 4}}]}]])) diff --git a/src/status_im/contexts/chat/group/events.cljs b/src/status_im/contexts/chat/group/events.cljs new file mode 100644 index 00000000000..32c3b21c004 --- /dev/null +++ b/src/status_im/contexts/chat/group/events.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.chat.group.events + (:require [clojure.string :as string] + [oops.core :as oops] + [re-frame.core :as rf] + [status-im.common.avatar-picture-picker.view :as avatar-picture-picker] + [status-im.constants :as constants] + [taoensso.timbre :as log] + [utils.i18n :as i18n])) + +(rf/reg-event-fx :group-chat/create + (fn [{:keys [db]} [{:keys [group-name group-color group-image]}]] + (let [selected-contacts (:group/selected-contacts db)] + {:json-rpc/call [{:method "wakuext_createGroupChatWithMembers" + :params [nil group-name (into [] selected-contacts)] + :js-response true + :on-success (fn [response] + (let [chat-id (-> (oops/oget response :chats) + first + (oops/oget :id))] + (rf/dispatch [:chat-updated response]) + (rf/dispatch [:group-chat/edit + {:chat-id chat-id + :group-name group-name + :group-color group-color + :group-image group-image}])))}]}))) + +(rf/reg-event-fx :group-chat/edit + (fn [_ [{:keys [chat-id group-name group-color group-image on-success]}]] + {:json-rpc/call [{:method "chat_editChat" + :params ["" chat-id group-name (name group-color) + {:imagePath (when group-image + (string/replace-first group-image #"file://" "")) + :x 0 + :y 0 + :width avatar-picture-picker/crop-size + :height avatar-picture-picker/crop-size}] + :js-response true + :on-error #(log/error "failed to edit group" {:error %}) + :on-success (fn [response] + (rf/dispatch [:chat-updated response true]) + (when on-success (on-success)))}]})) + +(rf/reg-event-fx :group-chat/set-manage-members-error + (fn [{:keys [db]} [{:keys [chat-id error?]}]] + {:db (assoc-in db + [:group-chat/manage-members-error chat-id] + error?) + :fx [(when error? + [:dispatch + [:toasts/upsert + {:type :negative + :text (i18n/label :t/new-group-limit + {:max-contacts + constants/max-group-chat-participants})}]])]})) diff --git a/src/status_im/contexts/chat/group/update/view.cljs b/src/status_im/contexts/chat/group/update/view.cljs new file mode 100644 index 00000000000..a5d40683509 --- /dev/null +++ b/src/status_im/contexts/chat/group/update/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.chat.group.update.view + (:require + [quo.context :as quo.context] + [status-im.contexts.chat.group.common.group-edit :as group-edit] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [close]}] + (let [chat-id (quo.context/use-screen-params) + {:keys [chat-name color image]} (rf/sub [:chats/chat-by-id chat-id]) + contacts (rf/sub [:contacts/contacts-by-chat chat-id])] + [group-edit/view + {:default-group-name chat-name + :default-group-color color + :default-group-image image + :contacts contacts + :submit-button-label (i18n/label :t/update-group-chat) + :submit-event :group-chat/edit + :back-button-icon :i/close + :chat-id chat-id + :close close + :on-success close + :editing-group? true}])) diff --git a/src/status_im/contexts/chat/home/add_new_contact/effects.cljs b/src/status_im/contexts/chat/home/add_new_contact/effects.cljs new file mode 100644 index 00000000000..4ff99915b4f --- /dev/null +++ b/src/status_im/contexts/chat/home/add_new_contact/effects.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.chat.home.add-new-contact.effects + (:require + [legacy.status-im.ethereum.ens :as ens] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.contacts/resolve-public-key-from-ens + (fn [{:keys [chain-id ens on-success on-error]}] + (ens/pubkey chain-id ens on-success on-error))) diff --git a/src/status_im/contexts/chat/home/add_new_contact/events.cljs b/src/status_im/contexts/chat/home/add_new_contact/events.cljs new file mode 100644 index 00000000000..63e53a27ba6 --- /dev/null +++ b/src/status_im/contexts/chat/home/add_new_contact/events.cljs @@ -0,0 +1,187 @@ +(ns status-im.contexts.chat.home.add-new-contact.events + (:require + [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.common.validation.general :as validators] + [status-im.contexts.chat.contacts.events :as data-store.contacts] + status-im.contexts.chat.home.add-new-contact.effects + [utils.ens.stateofus :as stateofus] + [utils.string :as utils.string])) + +(defn init-contact + "Create a new contact (persisted to app-db as [:contacts/new-identity]). + The following options are available: + + | key | description | + | -------------------|-------------| + | `:user-public-key` | user's public key (not the contact) + | `:input` | raw user input (untrimmed) + | `:scanned` | scanned user input (untrimmed) + | `:id` | public-key|compressed-key|ens + | `:type` | :empty|:public-key|:compressed-key|:ens + | `:ens` | id.eth|id.ens-stateofus + | `:public-key` | public-key (from decompression or ens resolution) + | `:state` | :empty|:invalid|:decompress-key|:resolve-ens|:valid + | `:msg` | keyword i18n msg" + ([] + (-> [:user-public-key :input :scanned :id :type :ens :public-key :state :msg] + (zipmap (repeat nil)))) + ([kv] (-> (init-contact) (merge kv)))) + +(def url-regex #"^https?://status.app/u(/([a-zA-Z0-9_-]+)(={0,2}))?#(.+)") + +(defn ->id + [{:keys [input] :as contact}] + (let [trimmed-input (utils.string/safe-trim input)] + (->> {:id (if (empty? trimmed-input) + nil + (if-some [id (last (re-matches url-regex trimmed-input))] + id + trimmed-input))} + (merge contact)))) + +(defn ->type + [{:keys [id] :as contact}] + (->> (cond + (empty? id) + {:type :empty} + + (validators/valid-public-key? id) + {:type :public-key + :public-key id} + + (validators/valid-compressed-key? id) + {:type :compressed-key} + + :else + {:type :ens + :ens (stateofus/ens-name-parse id)}) + (merge contact))) + +(defn ->state + [{:keys [id type public-key user-public-key] :as contact}] + (->> (cond + (empty? id) + {:state :empty} + + (= type :public-key) + {:state :invalid + :msg :t/not-a-chatkey} + + (= public-key user-public-key) + {:state :invalid + :msg :t/can-not-add-yourself} + + (and (= type :compressed-key) (empty? public-key)) + {:state :decompress-key} + + (and (= type :ens) (empty? public-key)) + {:state :resolve-ens} + + (and (or (= type :compressed-key) (= type :ens)) + (validators/valid-public-key? public-key)) + {:state :valid}) + (merge contact))) + +(def validate-contact (comp ->state ->type ->id)) + +(declare build-contact) + +(defn set-new-identity + [{:keys [db]} [{:keys [input build-success-fn failure-fn]}]] + (let [user-public-key (get-in db [:profile/profile :public-key]) + {:keys [input id ens state] + :as contact} (-> {:user-public-key user-public-key + :input input + :scanned input} + init-contact + validate-contact)] + (case state + :empty {:db (dissoc db :contacts/new-identity)} + (:valid :invalid) {:db (assoc db :contacts/new-identity contact)} + :decompress-key {:db (assoc db :contacts/new-identity contact) + :serialization/decompress-public-key + {:compressed-key id + :on-success + #(re-frame/dispatch [:contacts/set-new-identity-success + {:input input + :pubkey % + :build-success-fn build-success-fn}]) + :on-error + #(re-frame/dispatch [:contacts/set-new-identity-error + {:input input + :pubkey % + :failure-fn failure-fn}])}} + :resolve-ens {:db (assoc db :contacts/new-identity contact) + :effects.contacts/resolve-public-key-from-ens + {:ens ens + :chain-id 1 + :on-success + #(re-frame/dispatch [:contacts/set-new-identity-success + {:input input + :pubkey % + :build-success-fn build-success-fn}]) + :on-error + #(re-frame/dispatch [:contacts/set-new-identity-error + {:input input + :pubkey % + :failure-fn failure-fn}])}}))) + +(re-frame/reg-event-fx :contacts/set-new-identity set-new-identity) + +(defn- set-new-identity-success + [{:keys [db]} [{:keys [input pubkey build-success-fn]}]] + (let [contact (get-in db [:contacts/new-identity])] + (when (= (:input contact) input) + {:db (assoc db :contacts/new-identity (->state (assoc contact :public-key pubkey))) + :dispatch [:contacts/build-contact + {:pubkey pubkey + :ens (:ens contact) + :success-fn build-success-fn}]}))) + +(re-frame/reg-event-fx :contacts/set-new-identity-success set-new-identity-success) + +(defn- set-new-identity-error + [{:keys [db]} [{:keys [input err failure-fn]}]] + (let [contact (get-in db [:contacts/new-identity])] + (when (= (:input contact) input) + (let [state (cond + (and (string? err) (string/includes? err "invalid public key")) + {:state :invalid :msg :t/not-a-chatkey} + (and (string? (:message err)) + (or (string/includes? (:message err) "fallback failed") + (string/includes? (:message err) "no such host"))) + {:state :invalid :msg :t/lost-connection} + :else {:state :invalid})] + (merge {:db (assoc db :contacts/new-identity (merge contact state))} + (when failure-fn + (failure-fn))))))) + +(re-frame/reg-event-fx :contacts/set-new-identity-error set-new-identity-error) + +(defn- build-contact + [_ [{:keys [pubkey ens success-fn]}]] + {:json-rpc/call [{:method "wakuext_buildContact" + :params [{:publicKey pubkey + :ENSName ens}] + :js-response true + :on-success #(re-frame/dispatch [:contacts/build-contact-success + {:pubkey pubkey + :contact (data-store.contacts/<-rpc-js %) + :success-fn success-fn}])}]}) + +(re-frame/reg-event-fx :contacts/build-contact build-contact) + +(defn- build-contact-success + [{:keys [db]} [{:keys [pubkey contact success-fn]}]] + (merge {:db (assoc-in db [:contacts/contacts pubkey] contact)} + (when success-fn + (success-fn contact)))) + +(re-frame/reg-event-fx :contacts/build-contact-success build-contact-success) + +(defn- clear-new-identity + [{:keys [db]}] + {:db (dissoc db :contacts/new-identity)}) + +(re-frame/reg-event-fx :contacts/clear-new-identity clear-new-identity) diff --git a/src/status_im/contexts/chat/home/add_new_contact/events_test.cljs b/src/status_im/contexts/chat/home/add_new_contact/events_test.cljs new file mode 100644 index 00000000000..5112e8a650a --- /dev/null +++ b/src/status_im/contexts/chat/home/add_new_contact/events_test.cljs @@ -0,0 +1,139 @@ +(ns status-im.contexts.chat.home.add-new-contact.events-test + (:require + [cljs.test :refer-macros [deftest are]] + matcher-combinators.test + [re-frame.core :as re-frame] + [status-im.contexts.chat.home.add-new-contact.events :as events])) + +(def user-ukey + "0x04ca27ed9c7c4099d230c6d8853ad0cfaf084a019c543e9e433d3c04fac6de9147cf572b10e247cfe52f396b5aa10456b56dd1cf1d8a681e2b93993d44594b2e85") +(def user-ckey "zQ3shtFEo4PxpQiYGcNZZ8xhJmhD6WBXwnHPBueu5SRnvPXjk") +(def ukey + "0x045596a7ff87da36860a84b0908191ce60a504afc94aac93c1abd774f182967ce694f1bf2d8773cd59f4dd0863e951f9b7f7351c5516291a0fceb73f8c392a0e88") +(def ckey "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA") +(def ens "esep") +(def ens-stateofus-eth (str ens ".stateofus.eth")) +(def link-ckey (str "https://status.app/u#" ckey)) +(def link-ckey-with-encoded-data (str "https://status.app/u/CwSACgcKBVBhdmxvAw==#" ckey)) +(def link-ens (str "https://status.app/u#" ens)) + +;;; unit tests (no app-db involved) + +(deftest validate-contact-test + (are [i e] (match? (events/validate-contact (events/init-contact + {:user-public-key user-ukey + :input i})) + (events/init-contact e)) + + "" {:user-public-key user-ukey + :input "" + :type :empty + :state :empty} + + " " {:user-public-key user-ukey + :input " " + :type :empty + :state :empty} + + ukey {:user-public-key user-ukey + :input ukey + :id ukey + :type :public-key + :public-key ukey + :state :invalid + :msg :t/not-a-chatkey} + + ens {:user-public-key user-ukey + :input ens + :id ens + :type :ens + :ens ens-stateofus-eth + :state :resolve-ens} + + (str " " ens) {:user-public-key user-ukey + :input (str " " ens) + :id ens + :type :ens + :ens ens-stateofus-eth + :state :resolve-ens} + + ckey {:user-public-key user-ukey + :input ckey + :id ckey + :type :compressed-key + :state :decompress-key} + + link-ckey {:user-public-key user-ukey + :input link-ckey + :id ckey + :type :compressed-key + :state :decompress-key} + link-ckey-with-encoded-data {:user-public-key user-ukey + :input link-ckey-with-encoded-data + :id ckey + :type :compressed-key + :state :decompress-key} + + link-ens {:user-public-key user-ukey + :input link-ens + :id ens + :type :ens + :ens ens-stateofus-eth + :state :resolve-ens})) + +;;; event handler tests (no callbacks) + +(def db + {:profile/profile {:public-key user-ukey} + :networks/current-network "mainnet_rpc"}) + +(deftest set-new-identity-test + (with-redefs [re-frame/dispatch (fn [& args] args)] + (are [i edb] (match? (events/set-new-identity {:db db} [{:input i}]) edb) + + "" {:db db} + + ukey {:db (assoc db + :contacts/new-identity + (events/init-contact + {:user-public-key user-ukey + :input ukey + :id ukey + :type :public-key + :public-key ukey + :scanned ukey + :state :invalid + :msg :t/not-a-chatkey}))} + + ens {:db (assoc db + :contacts/new-identity + (events/init-contact + {:user-public-key user-ukey + :input ens + :id ens + :type :ens + :ens ens-stateofus-eth + :public-key nil ; not yet... + :scanned ens + :state :resolve-ens})) + :effects.contacts/resolve-public-key-from-ens + {:ens ens-stateofus-eth + :chain-id 1 + :on-success [:contacts/set-new-identity-success ens] + :on-error [:contacts/set-new-identity-error ens]}} + + ;; compressed-key & add-self-as-contact + user-ckey {:db (assoc db + :contacts/new-identity + (events/init-contact + {:user-public-key user-ukey + :input user-ckey + :id user-ckey + :type :compressed-key + :public-key nil ; not yet... + :scanned user-ckey + :state :decompress-key})) + :serialization/decompress-public-key + {:compressed-key user-ckey + :on-success [:contacts/set-new-identity-success user-ckey] + :on-error [:contacts/set-new-identity-error user-ckey]}}))) diff --git a/src/status_im/contexts/chat/home/add_new_contact/scan/scan_profile_qr_page.cljs b/src/status_im/contexts/chat/home/add_new_contact/scan/scan_profile_qr_page.cljs new file mode 100644 index 00000000000..a8a7ed90ac4 --- /dev/null +++ b/src/status_im/contexts/chat/home/add_new_contact/scan/scan_profile_qr_page.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.chat.home.add-new-contact.scan.scan-profile-qr-page + (:require [react-native.core :as rn] + [react-native.hooks :as hooks] + [status-im.common.scan-qr-code.view :as scan-qr-code] + [utils.debounce :as debounce] + [utils.i18n :as i18n])) + +(defn- f-internal-view + [] + (let [{:keys [keyboard-shown]} (hooks/use-keyboard)] + [:<> + (when keyboard-shown + (rn/dismiss-keyboard!)) + [scan-qr-code/view + {:title (i18n/label :t/scan-qr) + :on-success-scan #(debounce/debounce-and-dispatch [:contacts/set-new-identity {:input %}] + 300)}]])) + +(defn view + [] + [:f> f-internal-view]) diff --git a/src/status_im/contexts/chat/home/add_new_contact/style.cljs b/src/status_im/contexts/chat/home/add_new_contact/style.cljs new file mode 100644 index 00000000000..91406c8e9d2 --- /dev/null +++ b/src/status_im/contexts/chat/home/add_new_contact/style.cljs @@ -0,0 +1,83 @@ +(ns status-im.contexts.chat.home.add-new-contact.style + (:require [quo.foundations.colors :as colors])) + +(defn container-outer + [theme] + {:flex 1 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :margin-top 2 + :padding-horizontal 20}) + +(def container-invalid + {:flex-direction :row + :margin-top 8}) + +(defn text-description + [theme] + {:margin-bottom 6 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def icon-invalid + {:size 16 + :color colors/danger-50}) + +(def text-invalid + {:margin-left 4 + :color colors/danger-50}) + +(def input-and-scan-container + {:flex-direction :row + :align-items :flex-start}) + +(def scan-button-container + {:margin-left 12 + :height 66 + :justify-content :flex-end}) + +(def found-user + {:padding-top 16 + :flex-direction :column + :align-self :stretch}) + +(defn found-user-container + [theme] + {:flex-direction :row + :align-items :center + :padding-top 8 + :padding-left 12 + :padding-right 12 + :padding-bottom 8 + :color (colors/theme-colors + colors/black + colors/white + theme) + :background-color (colors/theme-colors + colors/white + colors/neutral-95 + theme) + :border-width 1 + :border-radius 12 + :border-color (colors/theme-colors + colors/neutral-20 + colors/neutral-80 + theme)}) + +(def found-user-text + {:margin-left 8 + :flex-direction :column}) + +(defn found-user-display-name + [theme] + {:color (colors/theme-colors + colors/black + colors/white + theme)}) + +(defn found-user-key + [theme] + {:color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}) + + diff --git a/src/status_im/contexts/chat/home/add_new_contact/views.cljs b/src/status_im/contexts/chat/home/add_new_contact/views.cljs new file mode 100644 index 00000000000..cbe71928921 --- /dev/null +++ b/src/status_im/contexts/chat/home/add_new_contact/views.cljs @@ -0,0 +1,155 @@ +(ns status-im.contexts.chat.home.add-new-contact.views + (:require [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.chat.home.add-new-contact.style :as style] + [utils.address :as address] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn found-contact + [public-key] + (let [{:keys [primary-name compressed-key]} (rf/sub [:contacts/contact-by-identity public-key]) + photo-path (rf/sub [:chats/photo-path public-key]) + theme (quo.context/use-theme)] + (when primary-name + [rn/view style/found-user + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/text-description theme)} + (i18n/label :t/user-found)] + [rn/view (style/found-user-container theme) + [quo/user-avatar + {:full-name primary-name + :profile-picture photo-path + :size :small + :status-indicator? false}] + [rn/view style/found-user-text + [quo/text + {:weight :semi-bold + :size :paragraph-1 + :style (style/found-user-display-name theme)} + primary-name] + [quo/text + {:weight :regular + :size :paragraph-2 + :style (style/found-user-key theme)} + (address/get-shortened-compressed-key compressed-key)]]]]))) + +(defn- search-input + [] + (let [[input-value set-input-value] (rn/use-state nil) + input-ref (rn/use-ref-atom nil) + on-ref (rn/use-callback #(reset! input-ref %)) + {:keys [scanned] + contact-public-key :id} (rf/sub [:contacts/new-identity]) + contact-public-key-or-scanned (or contact-public-key scanned) + empty-input? (and (string/blank? input-value) + (string/blank? contact-public-key-or-scanned)) + clear-input (rn/use-callback + (fn [] + (set-input-value nil) + (rf/dispatch [:contacts/clear-new-identity]))) + paste-on-input (rn/use-callback + (fn [] + (clipboard/get-string + (fn [clipboard-text] + (set-input-value clipboard-text) + (rf/dispatch [:contacts/set-new-identity + {:input clipboard-text}]))))) + on-change-text (rn/use-callback + (fn [new-text] + (set-input-value new-text) + (if (string/blank? contact-public-key-or-scanned) + (debounce/debounce-and-dispatch [:contacts/set-new-identity + {:input new-text}] + 600) + (rf/dispatch-sync [:contacts/set-new-identity + {:input new-text}]))) + [contact-public-key-or-scanned])] + (rn/use-unmount #(rf/dispatch [:contacts/clear-new-identity])) + [rn/view {:style style/input-and-scan-container} + [quo/input + {:accessibility-label :enter-contact-code-input + :ref on-ref + :container-style {:flex 1} + :auto-capitalize :none + :multiline? true + :blur-on-submit true + :return-key-type :done + :label (i18n/label :t/ens-or-chat-key) + :placeholder (i18n/label :t/type-some-chat-key) + :clearable? (not empty-input?) + :on-clear clear-input + :button (when empty-input? + {:on-press paste-on-input + :text (i18n/label :t/paste)}) + ;; NOTE: `contact-public-key-or-scanned` has priority over `input-value`, + ;; we clean it when the input is updated so that it's `nil` and + ;; `input-value`is shown. To fastly clean it, we use `dispatch-sync`. + ;; This call could be avoided if `::qr-scanner/scan-code` were able to + ;; receive a callback function, not only a re-frame event as callback. + :value (or contact-public-key-or-scanned input-value) + :on-change-text on-change-text}] + [rn/view {:style style/scan-button-container} + [quo/button + {:type :outline + :icon-only? true + :size 40 + :on-press #(rf/dispatch [:open-modal :screen/scan-profile-qr-code])} + :i/scan]]])) + +(defn- invalid-text + [message] + [rn/view style/container-invalid + [quo/icon :i/alert style/icon-invalid] + [quo/text + {:size :paragraph-2 + :style style/text-invalid} + (i18n/label (or message :t/invalid-ens-or-key))]]) + +(defn navigate-back [] (rf/dispatch [:navigate-back])) + +(defn new-contact + [] + (let [{:keys [public-key ens state msg]} (rf/sub [:contacts/new-identity]) + customization-color (rf/sub [:profile/customization-color]) + theme (quo.context/use-theme)] + [floating-button-page/view + {:header-container-style {:margin-top 8} + :header [quo/page-nav + {:type :no-title + :icon-name :i/close + :accessibility-label :new-contact-close-button + :on-press navigate-back}] + :footer [quo/button + {:type :primary + :customization-color customization-color + :size 40 + :accessibility-label :new-contact-button + :icon-left :i/profile + :disabled? (not= state :valid) + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:chat.ui/show-profile public-key + ens]) + (js/setTimeout #(rf/dispatch + [:contacts/clear-new-identity]) + 600))} + (i18n/label :t/view-profile)]} + [quo/page-top + {:title (i18n/label :t/add-a-contact) + :description :text + :description-text (i18n/label :t/find-your-friends) + :container-style {:padding-vertical 8}}] + [rn/view {:style (style/container-outer theme)} + [search-input] + (case state + :invalid [invalid-text msg] + :valid [found-contact public-key] + nil)]])) diff --git a/src/status_im/contexts/chat/home/chat_list_item/style.cljs b/src/status_im/contexts/chat/home/chat_list_item/style.cljs new file mode 100644 index 00000000000..8a58589e485 --- /dev/null +++ b/src/status_im/contexts/chat/home/chat_list_item/style.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.chat.home.chat-list-item.style + (:require + [quo.foundations.colors :as colors])) + +(def container + {:margin-horizontal 8 + :padding-vertical 8 + :padding-horizontal 12 + :border-radius 12 + :flex-direction :row + :align-items :center}) + +(def chat-data-container + {:flex 1 + :margin-left 8}) + +(def notification-container + {:margin-left :auto + :height 20 + :width 20 + :justify-content :center + :align-items :center}) + +(def notification-container-layout + {:flex-grow 1 + :justify-content :center + :margin-left 8}) + +;; TODO: duplicate of `quo.components.common.unread-grey-dot.style` +;; Replace it when this component is defined as part of `quo.components` +(defn grey-dot + [theme] + {:width 8 + :height 8 + :border-radius 4 + :background-color (colors/theme-colors colors/neutral-40 colors/neutral-60 theme)}) diff --git a/src/status_im/contexts/chat/home/chat_list_item/view.cljs b/src/status_im/contexts/chat/home/chat_list_item/view.cljs new file mode 100644 index 00000000000..567b7bb2394 --- /dev/null +++ b/src/status_im/contexts/chat/home/chat_list_item/view.cljs @@ -0,0 +1,292 @@ +(ns status-im.contexts.chat.home.chat-list-item.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.home.actions.view :as actions] + [status-im.constants :as constants] + [status-im.contexts.chat.home.chat-list-item.style :as style] + [utils.datetime :as datetime] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def max-subheader-length 50) + +(defn open-chat + [chat-id] + (fn [] + (rf/dispatch [:dismiss-keyboard]) + (debounce/throttle-and-dispatch [:chat/navigate-to-chat chat-id] 500))) + +(defn parsed-text-to-one-line + [parsed-text] + (reduce + (fn [acc {:keys [type literal children destination]}] + (case type + "paragraph" + (str acc (parsed-text-to-one-line children) " ") + + "mention" + (str acc "@" (rf/sub [:messages/resolve-mention literal])) + + "status-tag" + (str acc literal) + + "link" + (str acc destination) + + (str acc (string/replace literal #"\n" " ")))) + "" + parsed-text)) + +(defn extract-text-from-message + [{:keys [content]}] + (let [{:keys [parsed-text text]} content] + (if parsed-text + (parsed-text-to-one-line parsed-text) + (if text + (string/replace text #"\n" " ") + text)))) + +(defn preview-text-from-content + [group-chat primary-name {:keys [content-type album-images-count content outgoing] :as message}] + (let [content-text (extract-text-from-message message) + reply? (not (string/blank? (:response-to content))) + author (if outgoing + :you + (if group-chat + :other-person + :dont-show)) + preview-text + (cond + (= content-type constants/content-type-text) + (if reply? + (case author + :you (str (i18n/label :t/you-replied) ": " content-text) + :other-person (str (i18n/label :t/user-replied {:user primary-name}) ": " content-text) + :dont-show (str (i18n/label :t/replied) ": " content-text) + (str (i18n/label :t/replied) ": " content-text)) + (case author + :you (str (i18n/label :t/You) ": " content-text) + :other-person (str primary-name ": " content-text) + :dont-show content-text + content-text)) + + (= content-type constants/content-type-emoji) + (case author + :you (str (i18n/label :t/You) ": " content-text) + :other-person (str primary-name ": " content-text) + :dont-show content-text + content-text) + + (= content-type constants/content-type-system-text) + (case author + :you (i18n/label :t/you-pinned-a-message) + :other-person (i18n/label :t/user-pinned-a-message {:user primary-name}) + :dont-show (i18n/label :t/Pinned-a-message) + (i18n/label :t/Pinned-a-message)) + + (#{constants/content-type-contact-request + constants/content-type-system-message-mutual-event-removed + constants/content-type-system-message-mutual-event-accepted} + content-type) + (i18n/label :t/contact-request) + + (= content-type constants/content-type-sticker) + (case author + :you (i18n/label :t/you-sent-a-sticker) + :other-person (i18n/label :t/user-sent-a-sticker {:user primary-name}) + :dont-show (i18n/label :t/sent-a-sticker) + (i18n/label :t/sent-a-sticker)) + + (= content-type constants/content-type-image) + (let [sent-photos (if album-images-count + (case author + :you (i18n/label :t/you-sent-n-photos + {:number album-images-count}) + :other-person (i18n/label :t/user-sent-n-photos + {:number album-images-count + :user primary-name}) + :dont-show (i18n/label :t/sent-n-photos {:number album-images-count}) + (i18n/label :t/sent-n-photos {:number album-images-count})) + (case author + :you (i18n/label :t/you-sent-a-photo) + :other-person (i18n/label :t/user-sent-a-photo {:user primary-name}) + :dont-show (i18n/label :t/sent-a-photo) + (i18n/label :t/sent-a-photo)))] + (if (not (string/blank? content-text)) + (str sent-photos ": " content-text) + sent-photos)) + + (= content-type constants/content-type-audio) + (case author + :you (i18n/label :t/you-sent-audio-message) + :other-person (i18n/label :t/user-sent-audio-message {:user primary-name}) + :dont-show (i18n/label :t/sent-audio-message) + (i18n/label :t/sent-audio-message)) + + (= content-type constants/content-type-gif) + (case author + :you (i18n/label :t/you-sent-a-gif) + :other-person (i18n/label :t/user-sent-audio-message {:user primary-name}) + :dont-show (i18n/label :t/sent-a-gif) + (i18n/label :t/sent-a-gif)) + + (= content-type constants/content-type-community) + (case author + :you (i18n/label :t/you-shared-a-community) + :other-person (i18n/label :t/user-shared-a-community {:user primary-name}) + :dont-show (i18n/label :t/shared-a-community) + (i18n/label :t/shared-a-community)) + + :else + "")] + (subs preview-text 0 (min (count preview-text) max-subheader-length)))) + +(defn- last-message-color + [unread-messages? muted theme] + (cond + muted (colors/theme-colors colors/neutral-50 colors/neutral-60 theme) + unread-messages? (colors/theme-colors colors/neutral-100 colors/white theme) + :else (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) + +(defn last-message-preview + "Render the preview of a last message to a maximum of max-subheader-length characters" + [group-chat {:keys [deleted? outgoing from deleted-for-me?] :as message} muted unread-messages?] + (let [theme (quo.context/use-theme) + [primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from]) + preview-text (if deleted-for-me? + (i18n/label :t/you-deleted-a-message) + (if deleted? + (if outgoing + (i18n/label :t/you-deleted-a-message) + (if group-chat + (i18n/label :t/user-deleted-a-message {:user primary-name}) + (i18n/label :t/this-message-was-deleted))) + (preview-text-from-content group-chat primary-name message)))] + [quo/text + {:size :paragraph-2 + :style {:color (last-message-color unread-messages? muted theme) + :flex 1} + :number-of-lines 1 + :ellipsize-mode :tail + :accessibility-label :chat-message-text} + preview-text])) + +(defn avatar-view + [{:keys [contact chat-id full-name color image]}] + [rn/view {:style {:justify-content :center}} + (if contact ; `contact` is passed when it's not a group chat + (let [online? (rf/sub [:visibility-status-updates/online? chat-id]) + photo-path (rf/sub [:chats/photo-path chat-id])] + [quo/user-avatar + {:full-name full-name + :size :small + :online? online? + :profile-picture photo-path}]) + [quo/group-avatar + {:customization-color color + :picture (when image {:uri image}) + :size :size-32}])]) + +(defn- notification-layout + [child] + [rn/view + {:style style/notification-container-layout} + [rn/view {:style style/notification-container} + child]]) + +(defn notification + [{:keys [muted group-chat unviewed-messages-count unviewed-mentions-count]}] + (let [customization-color (rf/sub [:profile/customization-color]) + theme (quo.context/use-theme) + unread-messages? (pos? unviewed-messages-count) + unread-mentions? (pos? unviewed-mentions-count)] + (cond + muted + [notification-layout + [quo/icon :i/muted {:color colors/neutral-40}]] + + (and group-chat unread-mentions?) + [notification-layout + [quo/counter + {:container-style {:position :relative :right 0} + :customization-color customization-color + :accessibility-label :new-message-counter} + unviewed-mentions-count]] + + ;; TODO: use the grey-dot component when chat-list-item is moved to quo.components + (and group-chat unread-messages?) + [notification-layout + [rn/view + {:style (style/grey-dot theme) + :accessibility-label :unviewed-messages-public}]] + + unread-messages? + [notification-layout + [quo/counter + {:container-style {:position :relative :right 0} + :customization-color customization-color + :accessibility-label :new-message-counter} + unviewed-messages-count]]))) + +(defn chat-item + [{:keys [chat-id group-chat color name last-message timestamp muted image + chat-type unviewed-messages-count] + :as item}] + (let [[primary-name secondary-name] + (cond + group-chat + [name ""] + + (= chat-type constants/public-chat-type) + [(str "#" name) ""] + + :else + (rf/sub [:contacts/contact-two-names-by-identity chat-id])) + {:keys [ens-verified added?] :as contact} (when-not group-chat + (rf/sub [:contacts/contact-by-address chat-id])) + unread-messages? (pos? unviewed-messages-count)] + [rn/view {:style {:flex-direction :row}} + [avatar-view + {:contact contact + :chat-id chat-id + :full-name primary-name + :color color + :image image}] + [rn/view {:style {:flex-shrink 1}} + [rn/view {:style style/chat-data-container} + [quo/author + {:primary-name primary-name + :secondary-name secondary-name + :size 15 + :verified? ens-verified + :contact? added? + :muted? muted + :time-str (datetime/to-short-str timestamp) + :style {:flex-shrink 1}}] + [last-message-preview group-chat last-message muted unread-messages?]]] + [notification item]])) + +(defn chat-user + [item] + [rn/view {:style (assoc style/container :margin-horizontal 0)} + [chat-item item]]) + +(defn chat-list-item + [{:keys [chat-id chat-type] + :as item} theme] + (let [customization-color (rf/sub [:profile/customization-color])] + [rn/touchable-highlight + {:style style/container + :on-press (open-chat chat-id) + :underlay-color (colors/resolve-color customization-color theme 5) + :on-long-press #(rf/dispatch [:show-bottom-sheet + (cond-> {:content (fn [] [actions/chat-actions item false])} + (= chat-type constants/one-to-one-chat-type) + (assoc :selected-item + (fn [] [chat-user item])))])} + [chat-item item]])) diff --git a/src/status_im/contexts/chat/home/contact_request/style.cljs b/src/status_im/contexts/chat/home/contact_request/style.cljs new file mode 100644 index 00000000000..810d0545a2d --- /dev/null +++ b/src/status_im/contexts/chat/home/contact_request/style.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.chat.home.contact-request.style + (:require + [quo.foundations.colors :as colors])) + +(def contact-requests + {:flex-direction :row + :margin 8 + :padding-horizontal 12 + :padding-vertical 8 + :align-items :center}) + +(defn contact-requests-icon + [theme] + {:justify-content :center + :align-items :center + :width 32 + :height 32 + :border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)}) diff --git a/src/status_im/contexts/chat/home/contact_request/view.cljs b/src/status_im/contexts/chat/home/contact_request/view.cljs new file mode 100644 index 00000000000..ca455f790a7 --- /dev/null +++ b/src/status_im/contexts/chat/home/contact_request/view.cljs @@ -0,0 +1,65 @@ +(ns status-im.contexts.chat.home.contact-request.view + (:require + [clojure.string :as string] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.contexts.chat.home.contact-request.style :as style] + [status-im.contexts.shell.activity-center.notification-types :as notification-types] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn get-display-name + [{:keys [chat-id message]}] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity chat-id]) + no-ens-name (string/blank? (get-in message [:content :ens-name]))] + (if no-ens-name + (first (string/split primary-name " ")) + primary-name))) + +(defn requests-summary + [requests] + (case (count requests) + 1 + (get-display-name (first requests)) + 2 + (str (get-display-name (first requests)) + " " (i18n/label :t/and) + " " (get-display-name (second requests))) + (str (get-display-name (first requests)) + ", " (get-display-name (second requests)) + " " + (i18n/label :t/and) + " " (- (count requests) 2) + " " (i18n/label :t/more)))) + +(defn view + [{:keys [requests]}] + (let [theme (quo.context/use-theme) + customization-color (rf/sub [:profile/customization-color])] + [rn/touchable-opacity + {:active-opacity 1 + :accessibility-label :open-activity-center-contact-requests + :on-press (fn [] + (rf/dispatch [:activity-center/open + {:filter-status :unread + :filter-type notification-types/contact-request}])) + :style style/contact-requests} + [rn/view {:style (style/contact-requests-icon theme)} + [quo/icon :i/pending-user + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}]] + [rn/view {:style {:margin-left 8 :flex 1}} + [quo/text + {:size :paragraph-1 + :weight :semi-bold} + (i18n/label :t/pending-requests)] + [quo/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + (requests-summary requests)]] + [quo/counter + {:container-style {:margin-right 2} + :customization-color customization-color + :accessibility-label :pending-contact-requests-count} + (count requests)]])) diff --git a/src/status_im/contexts/chat/home/events.cljs b/src/status_im/contexts/chat/home/events.cljs new file mode 100644 index 00000000000..778e3be7eb5 --- /dev/null +++ b/src/status_im/contexts/chat/home/events.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.chat.home.events + (:require + [utils.re-frame :as rf])) + +(rf/defn messages-home-select-tab-event + {:events [:messages-home/select-tab]} + [{:keys [db]} tab] + {:db (assoc db :messages-home/selected-tab tab)}) diff --git a/src/status_im/contexts/chat/home/new_chat/styles.cljs b/src/status_im/contexts/chat/home/new_chat/styles.cljs new file mode 100644 index 00000000000..179bebbbdbd --- /dev/null +++ b/src/status_im/contexts/chat/home/new_chat/styles.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.chat.home.new-chat.styles + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(def contact-selection-heading + {:flex-direction :row + :justify-content :space-between + :align-items :flex-end + :margin-top 24 + :margin-bottom 16}) + +(defn chat-button-container + [theme] + {:position :absolute + :bottom 0 + :padding-bottom 33 + :background-color (colors/theme-colors colors/white-opa-70 colors/neutral-95-opa-70 theme) + :padding-top 12 + :padding-horizontal 20 + :left 0 + :right 0}) + +(defn no-contacts + [] + {:margin-bottom (+ 96 safe-area/bottom) + :flex 1 + :justify-content :center + :align-items :center}) diff --git a/src/status_im/contexts/chat/home/new_chat/view.cljs b/src/status_im/contexts/chat/home/new_chat/view.cljs new file mode 100644 index 00000000000..099bd5d7f6f --- /dev/null +++ b/src/status_im/contexts/chat/home/new_chat/view.cljs @@ -0,0 +1,152 @@ +(ns status-im.contexts.chat.home.new-chat.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [re-frame.core :as re-frame] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.common.contact-list.view :as contact-list] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [status-im.contexts.chat.home.new-chat.styles :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- no-contacts-view + [{:keys [theme]}] + (let [customization-color (rf/sub [:profile/customization-color])] + [rn/view + {:style (style/no-contacts)} + [rn/image {:source (resources/get-themed-image :no-contacts-to-chat theme)}] + [quo/text + {:weight :semi-bold + :size :paragraph-1 + :style {:margin-bottom 2 + :margin-top 12}} + (i18n/label :t/you-have-no-contacts)] + [quo/text + {:weight :regular + :size :paragraph-2} + (i18n/label :t/dont-yell-at-me)] + [quo/button + {:customization-color customization-color + :theme theme + :type :primary + :size 32 + :container-style {:margin-top 20 + :margin-bottom 12} + :on-press #(rf/dispatch [:invite.events/share-link])} + (i18n/label :t/invite-friends)] + [quo/button + {:customization-color customization-color + :theme theme + :type :grey + :size 32 + :on-press #(do + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/new-contact]))} + (i18n/label :t/add-a-contact)]])) + +(def ^:private contacts-selection-limit (dec constants/max-group-chat-participants)) + +(defn- toggle-selection + [public-key user-selected?] + (let [selected-contacts-count (rf/sub [:selected-contacts-count])] + (if user-selected? + (re-frame/dispatch [:deselect-contact public-key]) + (if (= contacts-selection-limit + selected-contacts-count) + (do + (rf/dispatch + [:toasts/upsert + {:id :remove-nickname + :type :negative + :text (i18n/label :t/new-group-limit + {:max-contacts + contacts-selection-limit})}]) + true) + (re-frame/dispatch [:select-contact public-key]))))) + +(defn contact-item-render + [{:keys [public-key] :as item} _ _ {:keys [set-has-error]}] + (let [user-selected? (rf/sub [:is-contact-selected? public-key]) + on-toggle (fn [] + (-> public-key + (toggle-selection user-selected?) + boolean + set-has-error))] + [contact-list-item/contact-list-item + {:on-press on-toggle + :allow-multiple-presses? true + :accessory {:type :checkbox + :checked? user-selected? + :on-check on-toggle}} + item])) + +(defn view + [{:keys [on-scroll close]}] + (let [theme (quo.context/use-theme) + contacts (rf/sub [:contacts/sorted-and-grouped-by-first-letter]) + selected-contacts-count (rf/sub [:selected-contacts-count]) + selected-contacts (rf/sub [:group/selected-contacts]) + customization-color (rf/sub [:profile/customization-color]) + one-contact-selected? (= selected-contacts-count 1) + [has-error? set-has-error] (rn/use-state false) + contacts-selected? (pos? selected-contacts-count) + {:keys [primary-name public-key]} (when one-contact-selected? + (rf/sub [:contacts/contact-by-identity + (first selected-contacts)]))] + (rn/use-unmount #(rf/dispatch [:group-chat/clear-contacts])) + [rn/view {:flex 1} + [rn/view {:padding-horizontal 20} + [quo/button + {:type :grey + :size 32 + :icon-only? true + :on-press close} :i/close] + [rn/view {:style style/contact-selection-heading} + [quo/text + {:weight :semi-bold + :size :heading-1 + :style {:color (colors/theme-colors colors/neutral-100 colors/white theme)}} + (if (or (not contacts-selected?) one-contact-selected?) + (i18n/label :t/new-chat) + (i18n/label :t/new-group-chat))] + (when (seq contacts) + [quo/text + {:size :paragraph-2 + :weight :regular + :style {:margin-bottom 2 + :color (if has-error? + (colors/theme-colors colors/danger-50 colors/danger-60 theme) + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))}} + (i18n/label :t/selected-count-from-max + {:selected selected-contacts-count + :max contacts-selection-limit})])]] + (if (empty? contacts) + [no-contacts-view {:theme theme}] + [gesture/section-list + {:key-fn :title + :sections (rf/sub [:contacts/filtered-active-sections]) + :render-section-header-fn contact-list/contacts-section-header + :render-section-footer-fn contact-list/contacts-section-footer + :content-container-style {:padding-bottom 70} + :render-fn contact-item-render + :render-data {:set-has-error set-has-error} + :on-scroll on-scroll}]) + (when contacts-selected? + [rn/view + {:style (style/chat-button-container theme)} + [quo/button + {:type :primary + :customization-color customization-color + :accessibility-label :next-button + :on-press (fn [] + (if one-contact-selected? + (rf/dispatch [:chat.ui/start-chat public-key]) + (rf/dispatch [:open-modal :screen/group-create])))} + (if one-contact-selected? + (i18n/label :t/chat-with {:selected-user primary-name}) + (i18n/label :t/setup-group-chat))]])])) diff --git a/src/status_im/contexts/chat/home/view.cljs b/src/status_im/contexts/chat/home/view.cljs new file mode 100644 index 00000000000..e5a6883a09d --- /dev/null +++ b/src/status_im/contexts/chat/home/view.cljs @@ -0,0 +1,178 @@ +(ns status-im.contexts.chat.home.view + (:require + [oops.core :as oops] + [quo.context :as quo.context] + [re-frame.core :as re-frame] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.common.contact-list.view :as contact-list] + [status-im.common.home.actions.view :as actions] + [status-im.common.home.banner.view :as common.banner] + [status-im.common.home.empty-state.view :as common.empty-state] + [status-im.common.home.header-spacing.view :as common.header-spacing] + [status-im.common.resources :as resources] + [status-im.contexts.chat.actions.view :as chat.actions.view] + [status-im.contexts.chat.home.chat-list-item.view :as chat-list-item] + [status-im.contexts.chat.home.contact-request.view :as contact-request] + [status-im.contexts.shell.constants :as shell.constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn get-item-layout + [_ index] + #js {:length 56 :offset (* 56 index) :index index}) + +(defn filter-and-sort-items-by-tab + [tab items] + (let [k (if (= tab :tab/groups) :group-chat :chat-id)] + (->> items + (filter k) + (sort-by :timestamp >)))) + +(defn empty-state-content + [theme] + #:tab{:contacts + {:title (i18n/label :t/no-contacts) + :description (i18n/label :t/no-contacts-description) + :image (resources/get-themed-image :no-contacts theme)} + :groups + {:title (i18n/label :t/no-group-chats) + :description (i18n/label :t/no-group-chats-description) + :image (resources/get-themed-image :no-group-chats theme)} + :recent + {:title (i18n/label :t/no-messages) + :description (i18n/label :t/no-messages-description) + :image (resources/get-themed-image :cat-in-box theme)}}) + +(defn chats + [{:keys [theme selected-tab set-scroll-ref scroll-shared-value]}] + (let [unfiltered-items (rf/sub [:chats/chats-stack-items]) + items (filter-and-sort-items-by-tab selected-tab unfiltered-items)] + (if (empty? items) + [common.empty-state/view + {:selected-tab selected-tab + :tab->content (empty-state-content theme)}] + [reanimated/flat-list + {:ref set-scroll-ref + :key-fn #(or (:chat-id %) (:public-key %) (:id %)) + :content-inset-adjustment-behavior :never + :header [common.header-spacing/view] + :get-item-layout get-item-layout + :on-end-reached #(re-frame/dispatch [:chat/show-more-chats]) + :keyboard-should-persist-taps :always + :data items + :render-data {:theme theme} + :render-fn chat-list-item/chat-list-item + :scroll-event-throttle 8 + :content-container-style {:padding-bottom + shell.constants/floating-shell-button-height + :padding-top 8} + :on-scroll #(common.banner/set-scroll-shared-value + {:scroll-input (oops/oget % "nativeEvent.contentOffset.y") + :shared-value scroll-shared-value})}]))) + +(defn contact-item-render + [{:keys [public-key] :as item} theme] + (let [current-pk (rf/sub [:multiaccount/public-key]) + show-profile-actions #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [actions/contact-actions item])}])] + [contact-list-item/contact-list-item + (when (not= public-key current-pk) + {:on-press #(rf/dispatch [:chat.ui/show-profile public-key]) + :on-long-press show-profile-actions + :accessory {:type :options + :on-press show-profile-actions}}) + item + theme])) + +(defn contacts + [{:keys [theme pending-contact-requests set-scroll-ref scroll-shared-value]}] + (let [items (rf/sub [:contacts/active-sections])] + (if (and (empty? items) (empty? pending-contact-requests)) + [common.empty-state/view + {:selected-tab :tab/contacts + :tab->content (empty-state-content theme)}] + [rn/section-list + {:ref (when (seq items) set-scroll-ref) + :key-fn :public-key + :get-item-layout get-item-layout + :content-inset-adjustment-behavior :never + :header [:<> + [common.header-spacing/view] + (when (seq pending-contact-requests) + [contact-request/view + {:requests pending-contact-requests}])] + :sections items + :sticky-section-headers-enabled false + :render-section-header-fn contact-list/contacts-section-header + :render-section-footer-fn contact-list/contacts-section-footer + :render-data {:theme theme} + :render-fn contact-item-render + :scroll-event-throttle 8 + :on-scroll #(common.banner/set-scroll-shared-value + {:scroll-input (oops/oget % "nativeEvent.contentOffset.y") + :shared-value scroll-shared-value})}]))) + +(defn- on-new-message-press + [screen-id] + (let [main-event [:show-bottom-sheet {:content chat.actions.view/new-chat :screen-id screen-id}]] + (rf/dispatch [:profile/check-profile-update-prompt main-event]))) + +(defn- banner-data + [profile-link] + (let [screen-id (quo.context/use-screen-id)] + {:title-props + {:beta? true + :label (i18n/label :t/messages) + :handler #(on-new-message-press screen-id) + :accessibility-label :new-chat-button} + :card-props + {:on-press #(rf/dispatch [:open-share {:options {:url profile-link}}]) + :banner (resources/get-image :invite-friends) + :title (i18n/label :t/invite-friends-to-status) + :description (i18n/label :t/share-invite-link)}})) + +(defn- on-tab-change + [tab] + (rf/dispatch [:messages-home/select-tab tab])) + +(defn view + [] + (let [theme (quo.context/use-theme) + scroll-ref (rn/use-ref-atom nil) + set-scroll-ref (rn/use-callback #(reset! scroll-ref %)) + {:keys [universal-profile-url]} (rf/sub [:profile/profile]) + customization-color (rf/sub [:profile/customization-color]) + pending-contact-requests (rf/sub [:activity-center/pending-contact-requests]) + selected-tab (or (rf/sub [:messages-home/selected-tab]) :tab/recent) + scroll-shared-value (reanimated/use-shared-value 0)] + [:<> + (if (= selected-tab :tab/contacts) + [contacts + {:pending-contact-requests pending-contact-requests + :set-scroll-ref set-scroll-ref + :scroll-shared-value scroll-shared-value + :theme theme}] + [chats + {:selected-tab selected-tab + :set-scroll-ref set-scroll-ref + :scroll-shared-value scroll-shared-value + :theme theme}]) + [common.banner/animated-banner + {:content (banner-data universal-profile-url) + :customization-color customization-color + :scroll-ref scroll-ref + :tabs [{:id :tab/recent + :label (i18n/label :t/recent) + :accessibility-label :tab-recent} + {:id :tab/groups + :label (i18n/label :t/groups) + :accessibility-label :tab-groups} + {:id :tab/contacts + :label (i18n/label :t/contacts) + :accessibility-label :tab-contacts + :notification-dot? (pos? (count pending-contact-requests))}] + :selected-tab selected-tab + :on-tab-change on-tab-change + :scroll-shared-value scroll-shared-value}]])) diff --git a/src/status_im/contexts/chat/messenger/camera/style.cljs b/src/status_im/contexts/chat/messenger/camera/style.cljs new file mode 100644 index 00000000000..88dab1192ca --- /dev/null +++ b/src/status_im/contexts/chat/messenger/camera/style.cljs @@ -0,0 +1,122 @@ +(ns status-im.contexts.chat.messenger.camera.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated])) + +(def screen-container + {:flex 1 + :background-color colors/black}) + +(defn flash-container + [rotate uri] + (reanimated/apply-animations-to-style + {:transform [{:rotate rotate}]} + {:position :absolute + :top 50 + :left (if uri -25 25)})) + +(def cancel-dash + {:width 1 + :height 32 + :top -4 + :left 12 + :z-index 1 + :transform [{:rotate "-45deg"}] + :background-color colors/white + :position :absolute}) + +(defn camera-window + [width height top] + {:width width + :height height + :top top}) + +(defn image + [width height top portrait?] + {:width width + :height (if portrait? height (* width 0.75)) + :top top}) + +(def zoom-button-container + {:width 37 + :height 37 + :justify-content :center + :align-items :center}) + +(defn zoom-container + [top insets] + {:width 157 + :height 43 + :border-radius 100 + :position :absolute + :background-color colors/black-opa-60 + :align-self :center + :justify-content :space-around + :align-items :center + :flex-direction :row + :bottom (+ top (:bottom insets) (when platform/android? (:top insets)) 18)}) + +(defn zoom-button + [size rotate] + (reanimated/apply-animations-to-style + {:width size + :height size + :transform [{:rotate rotate}]} + {:background-color colors/black-opa-30 + :justify-content :center + :align-items :center + :border-radius 50})) + +(defn bottom-area + [top insets uri] + {:left 20 + :right 20 + :opacity (if uri 0 1) + :z-index (if uri 0 1) + :position :absolute + :height (+ top (when platform/android? (:top insets))) + :bottom (:bottom insets)}) + +(def photo-text + {:color colors/system-yellow + :margin-top 18 + :font-size 14 + :align-self :center}) + +(def actions-container + {:flex-direction :row + :margin-top 20 + :align-items :center + :justify-content :space-between}) + +(def outer-circle + {:width 69 + :height 69 + :background-color colors/black + :border-radius 69 + :border-width 6 + :border-color colors/white + :justify-content :center + :align-items :center}) + +(def inner-circle + {:width 53 + :height 53 + :border-radius 53 + :background-color colors/white}) + +(defn confirmation-container + [insets uri] + {:position :absolute + :opacity (if uri 1 0) + :z-index (if uri 1 0) + :bottom 0 + :left 0 + :right 0 + :background-color "#131313" + :height (+ 69 (:bottom insets)) + :flex-direction :row + :padding-horizontal 20 + :justify-content :space-between + :padding-top 18}) diff --git a/src/status_im/contexts/chat/messenger/camera/view.cljs b/src/status_im/contexts/chat/messenger/camera/view.cljs new file mode 100644 index 00000000000..9b960e3390b --- /dev/null +++ b/src/status_im/contexts/chat/messenger/camera/view.cljs @@ -0,0 +1,180 @@ +(ns status-im.contexts.chat.messenger.camera.view + (:require + [oops.core :refer [oget]] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.camera-kit :as camera-kit] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.orientation :as orientation] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.contexts.chat.messenger.camera.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn retake + [flash uri] + (let [current-flash @flash] + (when platform/android? + ;; On Android, setting flash needs to be delayed until camera has initialized + (reset! flash false) + (js/setTimeout #(reset! flash current-flash) 300)) + (reset! uri nil))) + +(defn handle-orientation + [current-orientation rotate] + (orientation/use-device-orientation-change (fn [result] + (reset! current-orientation result) + (cond + (= result orientation/landscape-left) + (reanimated/animate rotate "90deg") + (= result orientation/landscape-right) + (reanimated/animate rotate "-90deg") + :else + (reanimated/animate rotate "0deg"))))) + +(defn- f-zoom-button + [{:keys [value current-zoom rotate]}] + (let [selected? (= @current-zoom value) + size (reanimated/use-shared-value (if selected? 37 25))] + (rn/use-effect #(reanimated/animate size (if selected? 37 25)) [@current-zoom]) + [rn/touchable-opacity + {:on-press #(reset! current-zoom value) + :style style/zoom-button-container + :accessibility-label (str "zoom-" value)} + [reanimated/view {:style (style/zoom-button size rotate)} + [quo/text + {:size (if selected? :paragraph-2 :label) + :weight :semi-bold + :number-of-lines 1 + :style {:color (if selected? + colors/system-yellow + colors/white)}} + (str value (when selected? "x"))]]])) + +(defn zoom-button + [args] + [:f> f-zoom-button args]) + +(defn snap-button + [camera-ref uri] + [rn/view + {:style style/outer-circle + :accessibility-label :snap} + [rn/touchable-opacity + {:on-press (fn [] + (camera-kit/capture @camera-ref #(reset! uri %))) + :style style/inner-circle}]]) + +(defn zoom-buttons + [] + (fn [top insets rotate current-zoom] + [rn/view {:style (style/zoom-container top insets)} + [zoom-button {:value 0.5 :current-zoom current-zoom :rotate rotate}] + [zoom-button {:value 1 :current-zoom current-zoom :rotate rotate}] + [zoom-button {:value 2 :current-zoom current-zoom :rotate rotate}] + [zoom-button {:value 3 :current-zoom current-zoom :rotate rotate}]])) + + +(defn- f-bottom-area + [{:keys [top insets uri camera-ref rotate]} back flip-camera] + [rn/view {:style (style/bottom-area top insets @uri)} + [quo/text {:style style/photo-text} (i18n/label :t/photo-caps)] + [rn/view {:style style/actions-container} + [quo/text + {:on-press back + :style {:font-size 17 + :color colors/white} + :accessibility-label :cancel} + (i18n/label :t/cancel)] + [snap-button camera-ref uri] + [reanimated/touchable-opacity + {:style (reanimated/apply-animations-to-style {:transform [{:rotate rotate}]} {}) + :on-press flip-camera} + [quo/icon :i/rotate-camera + {:size 48 :color colors/white :accessibility-label :flip-camera}]]]]) + +(defn bottom-area + [{:keys [flash camera-type] :as args}] + (let [back #(rf/dispatch [:navigate-back]) + flip-camera (fn [] + (reset! flash false) + (reset! camera-type (if (= @camera-type camera-kit/camera-type-back) + camera-kit/camera-type-front + camera-kit/camera-type-back)))] + [:f> f-bottom-area args back flip-camera])) + +(defn- f-camera-screen + [{:keys [camera-ref uri camera-type current-orientation flash toggle-flash current-zoom]}] + (let [window (rn/get-window) + {:keys [width height]} window + camera-window-height (* width 1.33) + insets safe-area/insets + top (/ (- height camera-window-height (:bottom insets)) 2) + top-landscape (/ (- height (* width 0.75) (:bottom insets)) 2) + portrait? (= @current-orientation orientation/portrait) + rotate (reanimated/use-shared-value "0deg") + on-press #(retake flash uri) + use-photo (fn [] + (rf/dispatch [:photo-selector/camera-roll-pick {:uri @uri}]) + (rf/dispatch [:navigate-back]))] + (handle-orientation current-orientation rotate) + [rn/view {:style style/screen-container} + [reanimated/touchable-opacity + {:active-opacity 1 + :on-press toggle-flash + :style (style/flash-container rotate @uri)} + (when-not @flash + [rn/view {:style style/cancel-dash}]) + [quo/icon :i/flash-camera + {:color colors/white + :size 24}]] + (if @uri + [fast-image/fast-image + {:style (style/image width camera-window-height (if portrait? top top-landscape) portrait?) + :source {:uri @uri}}] + [camera-kit/camera + {:ref #(reset! camera-ref %) + :style (style/camera-window width camera-window-height top) + :flash-mode (if @flash :on :off) + :camera-type @camera-type + :zoom @current-zoom + :max-zoom 3 + :on-zoom (fn [event] + (reset! current-zoom (oget event "nativeEvent.zoom")))}]) + (when-not @uri + [zoom-buttons top insets rotate current-zoom]) + [rn/view {:style (style/confirmation-container insets @uri)} + [quo/text + {:on-press on-press + :style {:font-size 17 + :color colors/white}} + (i18n/label :t/retake)] + [quo/text + {:on-press use-photo + :style {:font-size 17 + :color colors/white}} + (i18n/label :t/use-photo)]] + [bottom-area + {:top top + :insets insets + :uri uri + :camera-type camera-type + :camera-ref camera-ref + :flash flash + :rotate rotate}]])) + +(defn camera-screen + [] + (let [flash (reagent/atom false) + args {:camera-ref (atom nil) + :uri (reagent/atom nil) + :camera-type (reagent/atom camera-kit/camera-type-back) + :current-orientation (atom orientation/portrait) + :flash flash + :toggle-flash #(swap! flash not) + :current-zoom (reagent/atom 1)}] + [:f> f-camera-screen args])) diff --git a/src/status_im/contexts/chat/messenger/composer/actions/image/view.cljs b/src/status_im/contexts/chat/messenger/composer/actions/image/view.cljs new file mode 100644 index 00000000000..f0ffab15a91 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/actions/image/view.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.chat.messenger.composer.actions.image.view + (:require [quo.core :as quo] + [react-native.permissions :as permissions] + [react-native.platform :as platform] + [status-im.common.alert.effects :as alert.effects] + [status-im.common.device-permissions :as device-permissions] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn photo-limit-toast + [] + (rf/dispatch [:toasts/upsert + {:id :random-id + :type :negative + :text (i18n/label :t/hit-photos-limit + {:max-photos constants/max-album-photos})}])) + +(defn go-to-camera + [images-count] + (device-permissions/camera #(if (>= images-count constants/max-album-photos) + (photo-limit-toast) + (rf/dispatch [:navigate-to :screen/camera-screen])))) + +(defn camera-button + [] + (let [images-count (count (vals (rf/sub [:chats/sending-image])))] + [quo/composer-button + {:on-press #(go-to-camera images-count) + :accessibility-label :camera-button + :icon :i/camera + :container-style {:margin-right 12}}])) + +(defn open-photo-selector + [input-ref] + (permissions/request-permissions + {:permissions [(if platform/is-below-android-13? :read-external-storage :read-media-images) + :write-external-storage] + :on-allowed (fn [] + (when (and platform/android? @input-ref) + (.blur ^js @input-ref)) + (when platform/ios? + (rf/dispatch [:alert-banners/hide])) + (rf/dispatch [:photo-selector/navigate-to-photo-selector])) + :on-denied #(alert.effects/show-popup (i18n/label :t/error) + (i18n/label :t/external-storage-denied))})) + +(defn image-button + [input-ref] + [quo/composer-button + {:on-press #(open-photo-selector input-ref) + :accessibility-label :open-images-button + :container-style {:margin-right 12} + :icon :i/image}]) diff --git a/src/status_im/contexts/chat/messenger/composer/actions/style.cljs b/src/status_im/contexts/chat/messenger/composer/actions/style.cljs new file mode 100644 index 00000000000..d4b872f7f80 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/actions/style.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.chat.messenger.composer.actions.style + (:require + [react-native.reanimated :as reanimated] + [status-im.contexts.chat.messenger.composer.constants :as constants])) + +(def actions-container + {:height constants/actions-container-height + :justify-content :space-between + :align-items :center + :flex-direction :row}) + +(defn send-button + [opacity] + (reanimated/apply-animations-to-style + {:opacity opacity} + {:position :absolute + :right 0 + :padding-vertical 3 + :padding-left 2})) + diff --git a/src/status_im/contexts/chat/messenger/composer/actions/view.cljs b/src/status_im/contexts/chat/messenger/composer/actions/view.cljs new file mode 100644 index 00000000000..c4d9b029d8d --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/actions/view.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.chat.messenger.composer.actions.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.composer.actions.image.view :as actions.image] + [status-im.contexts.chat.messenger.composer.actions.style :as style] + [utils.re-frame :as rf])) + +(defn send-message-now + [{:keys [input-ref edit btn-opacity]}] + (when @input-ref + (.clear ^js @input-ref)) + (reanimated/animate btn-opacity 0) + (rf/dispatch [:chat.ui/send-current-message]) + (rf/dispatch [:chat.ui/set-chat-input-text nil]) + (when-not (some? edit) + (rf/dispatch [:chat.ui/scroll-to-bottom]))) + +(defn send-message-next-tick + "This function uses `reagent/next-tick` to call the `send-message` function. + + The usage `reagent/next-tick` will effectively schedule the `send-message` + function to run after any final modifications have been to the text-input. + + For example on iOS, tools like auto-correct will attempt to modify the + text-input when pressing the send button. Using `reagent/next-tick` allows + for that modification to happen and the related event dispatches to resolve + before call the `send-message` function." + [params] + (reagent/next-tick #(send-message-now params))) + +(def send-message + (if platform/ios? + send-message-next-tick + send-message-now)) + +(defn send-button + [input-ref edit] + (let [btn-opacity (reanimated/use-shared-value 0) + chat-input (rf/sub [:chats/current-chat-input]) + input-text (:input-text chat-input) + images? (boolean (seq (rf/sub [:chats/sending-image]))) + profile-customization-color (rf/sub [:profile/customization-color]) + {:keys [chat-id chat-type] + chat-color :color} (rf/sub [:chats/current-chat-chat-view]) + contact-customization-color (when (= chat-type constants/one-to-one-chat-type) + (rf/sub [:contacts/contact-customization-color-by-address + chat-id])) + on-press (rn/use-callback + #(send-message {:edit edit + :input-ref input-ref + :btn-opacity btn-opacity}) + [edit])] + (rn/use-effect (fn [] + ;; Handle send button opacity animation and z-index when input content changes + (if (or (seq input-text) images?) + (when (not= (reanimated/get-shared-value btn-opacity) 1) + (js/setTimeout #(reanimated/animate btn-opacity 1) 50)) + (when (not= (reanimated/get-shared-value btn-opacity) 0) + (reanimated/animate btn-opacity 0)))) + [(and (empty? input-text) (not images?))]) + [reanimated/view + {:style (style/send-button btn-opacity)} + [quo/button + {:icon-only? true + :size 32 + :customization-color (or contact-customization-color chat-color profile-customization-color) + :accessibility-label :send-message-button + :on-press on-press} + :i/arrow-up]])) + +(defn view + [input-ref] + (let [edit (rf/sub [:chats/edit-message])] + [rn/view {:style style/actions-container} + [rn/view {:style {:flex-direction :row}} + (when-not edit + [:<> + [actions.image/camera-button] + [actions.image/image-button input-ref edit]])] + [send-button input-ref edit]])) diff --git a/src/status_im/contexts/chat/messenger/composer/constants.cljs b/src/status_im/contexts/chat/messenger/composer/constants.cljs new file mode 100644 index 00000000000..418264d5d54 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/constants.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.chat.messenger.composer.constants) + +(def ^:const actions-container-height 56) + +(def ^:const images-padding-top 12) +(def ^:const images-padding-bottom 8) +(def ^:const images-container-height + (+ actions-container-height images-padding-top images-padding-bottom)) + +(def ^:const links-padding-top 12) +(def ^:const links-padding-bottom 8) +(def ^:const links-container-height + (+ actions-container-height links-padding-top links-padding-bottom)) + +(def ^:const reply-container-height 32) + +(def ^:const edit-container-height 32) + +(def ^:const mentions-max-height 240) + +(def ^:const max-text-size 4096) + +(def ^:const unfurl-debounce-ms + "Use a high threshold to prevent unnecessary rendering overhead." + 400) diff --git a/src/status_im/contexts/chat/messenger/composer/edit/style.cljs b/src/status_im/contexts/chat/messenger/composer/edit/style.cljs new file mode 100644 index 00000000000..c20b1562ed3 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/edit/style.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.chat.messenger.composer.edit.style) + +(def container + {:flex-direction :row + :height 24}) + +(def content-container + {:flex 1 + :flex-direction :row}) + +(def icon-container + {:position :absolute + :left 0 + :bottom -4 + :width 16 + :height 16}) + +(def text-container + {:position :absolute + :left 24 + :top 3 + :flex-direction :row + :align-items :center}) diff --git a/src/status_im/contexts/chat/messenger/composer/edit/view.cljs b/src/status_im/contexts/chat/messenger/composer/edit/view.cljs new file mode 100644 index 00000000000..1699a0d4de2 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/edit/view.cljs @@ -0,0 +1,48 @@ +(ns status-im.contexts.chat.messenger.composer.edit.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.contexts.chat.messenger.composer.constants :as constants] + [status-im.contexts.chat.messenger.composer.edit.style :as style] + [status-im.contexts.chat.messenger.composer.effects :as effects] + [status-im.contexts.chat.messenger.composer.utils :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn edit-message + [input-ref] + (let [theme (quo.context/use-theme)] + [rn/view + {:style style/container + :accessibility-label :edit-message} + [rn/view {:style style/content-container} + [quo/icon + :i/connector-dotted + {:size 16 + :color (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + :container-style style/icon-container}] + [rn/view {:style style/text-container} + [quo/text + {:weight :medium + :size :paragraph-2} + (i18n/label :t/editing-message)]]] + [quo/button + {:size 24 + :icon-only? true + :accessibility-label :edit-cancel-button + :on-press #(utils/cancel-edit-message input-ref) + :type :outline} + :i/close]])) + +(defn view + [input-ref] + (let [edit (rf/sub [:chats/edit-message]) + height (reanimated/use-shared-value (if edit constants/edit-container-height 0))] + (effects/use-edit input-ref edit) + (rn/use-effect #(reanimated/animate height (if edit constants/edit-container-height 0)) [edit]) + [reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})} + (when edit + [edit-message input-ref])])) diff --git a/src/status_im/contexts/chat/messenger/composer/effects.cljs b/src/status_im/contexts/chat/messenger/composer/effects.cljs new file mode 100644 index 00000000000..fb08314c690 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/effects.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.chat.messenger.composer.effects + (:require + [react-native.core :as rn] + [utils.number])) + +(defn use-edit + [input-ref edit] + (rn/use-effect + (fn [] + (when (and edit @input-ref) + (js/setTimeout #(.focus ^js @input-ref) 600))) + [(:message-id edit)])) + +(defn use-reply + [input-ref reply] + (rn/use-effect + (fn [] + (when (and reply @input-ref) + (js/setTimeout #(.focus ^js @input-ref) 600))) + [(:message-id reply)])) diff --git a/src/status_im/contexts/chat/messenger/composer/events.cljs b/src/status_im/contexts/chat/messenger/composer/events.cljs new file mode 100644 index 00000000000..61a2cb0c3e5 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/events.cljs @@ -0,0 +1,232 @@ +(ns status-im.contexts.chat.messenger.composer.events + (:require [clojure.string :as string] + [legacy.status-im.chat.models.mentions :as mentions] + [legacy.status-im.data-store.messages :as data-store.messages] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.composer.link-preview.events :as link-preview] + [status-im.contexts.chat.messenger.messages.transport.events :as messages.transport] + [taoensso.timbre :as log] + [utils.debounce :as debounce] + [utils.emojilib :as emoji] + [utils.re-frame :as rf] + utils.string)) + +(rf/defn set-chat-input-text + {:events [:chat.ui/set-chat-input-text]} + [{:keys [db] :as cofx} new-input chat-id] + (let [current-chat-id (or chat-id (:current-chat-id db))] + (rf/merge + cofx + {:db (assoc-in db [:chat/inputs current-chat-id :input-text] (emoji/text->emoji new-input))} + (when (empty? new-input) + (mentions/clear-mentions))))) + +(rf/defn set-chat-input-ref + {:events [:chat/set-input-ref]} + [{:keys [db]} input-ref] + (let [current-chat-id (:current-chat-id db)] + {:db (assoc-in db [:chat/inputs current-chat-id :input-ref] input-ref)})) + +(rf/defn set-input-focused + {:events [:chat.ui/setnn-input-focused]} + [{db :db} focused? chat-id] + (let [current-chat-id (or chat-id (:current-chat-id db))] + {:db (assoc-in db [:chat/inputs current-chat-id :focused?] focused?)})) + +(rf/defn reply-to-message + "Sets reference to previous chat message and focuses on input" + {:events [:chat.ui/reply-to-message]} + [{:keys [db]} message] + (let [current-chat-id (:current-chat-id db)] + {:db (-> db + (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] + message) + (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] nil) + (update-in [:chat/inputs current-chat-id :metadata] + dissoc + :sending-image))})) + +(rf/defn edit-message + "Sets reference to previous chat message and focuses on input" + {:events [:chat.ui/edit-message]} + [{:keys [db]} message] + (let [current-chat-id (:current-chat-id db) + text (get-in message [:content :text])] + {:db (-> db + (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] + message) + (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] nil) + (update-in [:chat/inputs current-chat-id :metadata] + dissoc + :sending-image)) + :dispatch [:mention/to-input-field text current-chat-id]})) + +(rf/defn cancel-message-reply + "Cancels stage message reply" + {:events [:chat.ui/cancel-message-reply]} + [{:keys [db]}] + (let [current-chat-id (:current-chat-id db)] + {:db (assoc-in db [:chat/inputs current-chat-id :metadata :responding-to-message] nil)})) + +(rf/defn clean-input + [{:keys [db] :as cofx} current-chat-id] + (rf/merge cofx + {:db (-> db + (assoc-in [:chat/inputs current-chat-id :metadata :sending-contact-request] nil) + (assoc-in [:chat/inputs current-chat-id :metadata :sending-image] nil) + (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] nil) + (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] nil))} + (set-chat-input-text nil current-chat-id))) + +(rf/defn cancel-message-edit + "Cancels stage message edit" + {:events [:chat.ui/cancel-message-edit]} + [{:keys [db] :as cofx}] + (let [current-chat-id (:current-chat-id db)] + (rf/merge cofx + (clean-input current-chat-id) + (link-preview/reset-unfurled) + (mentions/clear-mentions)))) + + +(defn emoji-only-content? + "Determines if text is just an emoji" + [{:keys [text response-to]}] + (and (not response-to) + (string? text) + (re-matches constants/regx-emoji text))) + +(defn build-image-messages + [{db :db} chat-id input-text] + (let [images (get-in db [:chat/inputs chat-id :metadata :sending-image]) + {:keys [message-id]} (get-in db [:chat/inputs chat-id :metadata :responding-to-message]) + album-id (str (random-uuid))] + (mapv (fn [[_ {:keys [resized-uri width height]}]] + {:chat-id chat-id + :album-id album-id + :content-type constants/content-type-image + :image-path (utils.string/safe-replace resized-uri #"file://" "") + :image-width width + :image-height height + :text input-text + :link-previews (map #(select-keys % [:url :title :description :thumbnail]) + (get-in db [:chat/link-previews :unfurled])) + :response-to message-id}) + images))) + +(defn- build-status-link-previews + [status-link-previews] + (map (fn [{{:keys [community-id color description display-name + members-count active-members-count icon banner]} + :community + url :url}] + {:url url + :community + {:community-id community-id + :color color + :description description + :display-name display-name + :members-count members-count + :active-members-count active-members-count + :icon {:data-uri (:data-uri icon) + :width (:width icon) + :height (:height icon)} + :banner {:data-uri (:data-uri banner) + :width (:width banner) + :height (:height banner)}}}) + status-link-previews)) + +(defn- build-link-previews + [link-previews] + (->> link-previews + (map #(select-keys % + [:url :title :description :thumbnail + :status-link-preview? :favicon])) + (filter (fn [{:keys [status-link-preview?]}] + (not status-link-preview?))))) + +(defn build-text-message + [{:keys [db]} input-text current-chat-id] + (when-not (string/blank? input-text) + (let [{:keys [message-id]} + (get-in db [:chat/inputs current-chat-id :metadata :responding-to-message]) + preferred-name (get-in db [:profile/profile :preferred-name]) + emoji? (emoji-only-content? {:text input-text + :response-to message-id})] + {:chat-id current-chat-id + :content-type (if emoji? + constants/content-type-emoji + constants/content-type-text) + :text input-text + :response-to message-id + :ens-name preferred-name + :link-previews (build-link-previews (get-in db [:chat/link-previews :unfurled])) + :status-link-previews (build-status-link-previews + (get-in db [:chat/status-link-previews :unfurled]))}))) + +(rf/defn send-messages + [{:keys [db] :as cofx} input-text current-chat-id] + (let [image-messages (build-image-messages cofx current-chat-id input-text) + text-message (when-not (seq image-messages) + (build-text-message cofx input-text current-chat-id)) + messages (keep identity (conj image-messages text-message))] + (when (seq messages) + (rf/merge cofx + (clean-input (:current-chat-id db)) + (link-preview/reset-unfurled) + (messages.transport/send-chat-messages messages))))) + +(defn- process-link-previews + [link-previews] + (->> link-previews + (map #(select-keys % + [:url :title :description :thumbnail + :status-link-preview? :favicon])) + (map data-store.messages/->link-preview-rpc) + (filter (fn [{:keys [status-link-preview?]}] + (not status-link-preview?))))) + +(rf/defn send-edited-message + [{:keys [db] + :as cofx} text {:keys [message-id quoted-message]}] + (rf/merge + cofx + {:json-rpc/call + [{:method "wakuext_editMessage" + :params [{:id message-id + :text text + :content-type (if (emoji-only-content? + {:text text + :response-to quoted-message}) + constants/content-type-emoji + constants/content-type-text) + :linkPreviews (process-link-previews (get-in db + [:chat/link-previews :unfurled])) + :statusLinkPreviews (map data-store.messages/->status-link-previews-rpc + (get-in db + [:chat/status-link-previews + :unfurled]))}] + :js-response true + :on-error #(log/error "failed to edit message " %) + :on-success (fn [result] + (rf/dispatch [:sanitize-messages-and-process-response + result]))}]} + (link-preview/reset-unfurled) + (cancel-message-edit))) + +(rf/defn send-current-message + "Sends message from current chat input" + {:events [:chat.ui/send-current-message]} + [{{:keys [current-chat-id] :as db} :db :as cofx}] + (let [{:keys [input-text metadata]} (get-in db [:chat/inputs current-chat-id]) + editing-message (:editing-message metadata)] + (debounce/clear :link-preview/unfurl-urls) + (debounce/clear :mention/on-change-text) + (if editing-message + (send-edited-message cofx input-text editing-message) + (send-messages cofx input-text current-chat-id)))) + +(rf/reg-fx :effects/set-input-text-value + (fn [[input-ref text-value]] + (when (and (not (string/blank? text-value)) input-ref) + (.setNativeProps ^js input-ref (clj->js {:text (emoji/text->emoji text-value)}))))) diff --git a/src/status_im/contexts/chat/messenger/composer/handlers.cljs b/src/status_im/contexts/chat/messenger/composer/handlers.cljs new file mode 100644 index 00000000000..38094342c81 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/handlers.cljs @@ -0,0 +1,16 @@ +(ns status-im.contexts.chat.messenger.composer.handlers + (:require + [clojure.string :as string] + [status-im.contexts.chat.messenger.composer.constants :as constants] + [utils.debounce :as debounce] + [utils.number] + [utils.re-frame :as rf])) + +(defn change-text + "Update `text-value`, update cursor selection, find links, find mentions" + [text] + (rf/dispatch [:chat.ui/set-chat-input-text text]) + (debounce/debounce-and-dispatch [:link-preview/unfurl-urls text] constants/unfurl-debounce-ms) + (if (string/ends-with? text "@") + (rf/dispatch [:mention/on-change-text text]) + (debounce/debounce-and-dispatch [:mention/on-change-text text] 300))) diff --git a/src/status_im/contexts/chat/messenger/composer/images/style.cljs b/src/status_im/contexts/chat/messenger/composer/images/style.cljs new file mode 100644 index 00000000000..26cb831d780 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/images/style.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.chat.messenger.composer.images.style + (:require + [quo.foundations.colors :as colors] + [status-im.contexts.chat.messenger.composer.constants :as constants])) + +(def image-container + {:padding-top constants/images-padding-top + :padding-bottom constants/images-padding-bottom + :padding-right 12}) + +(defn remove-photo-container + [theme] + {:width 16 + :height 16 + :border-radius 8 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :position :absolute + :top 5 + :right 9 + :justify-content :center + :align-items :center}) + + +(def remove-photo-inner-container + {:width 14 + :height 14 + :border-radius 7 + :background-color colors/neutral-50 + :justify-content :center + :align-items :center}) + +(def small-image + {:width 56 + :height 56 + :border-radius 12}) diff --git a/src/status_im/contexts/chat/messenger/composer/images/view.cljs b/src/status_im/contexts/chat/messenger/composer/images/view.cljs new file mode 100644 index 00000000000..0b79c37e6d0 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/images/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.chat.messenger.composer.images.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [status-im.contexts.chat.messenger.composer.constants :as constants] + [status-im.contexts.chat.messenger.composer.images.style :as style] + [utils.re-frame :as rf])) + +(defn image + [item theme] + [rn/view style/image-container + [rn/image + {:source {:uri (:resized-uri (val item))} + :style style/small-image}] + [rn/touchable-opacity + {:on-press #(rf/dispatch [:chat.ui/image-unselected (val item)]) + :style (style/remove-photo-container theme) + :hit-slop {:right 5 + :left 5 + :top 10 + :bottom 10}} + [rn/view {:style style/remove-photo-inner-container} + [quo/icon :i/clear {:size 20 :color colors/neutral-50 :color-2 colors/white}]]]]) + +(defn images-list + [] + (let [theme (quo.context/use-theme) + images (rf/sub [:chats/sending-image]) + height (reanimated/use-shared-value (if (seq images) constants/images-container-height 0))] + (rn/use-effect (fn [] + (reanimated/animate height + (if (seq images) constants/images-container-height 0))) + [images]) + [reanimated/view + {:style {:height height + :margin-horizontal -20 + :z-index 1}} + [gesture/flat-list + {:key-fn first + :render-fn (fn [item] + (image item theme)) + :data images + :content-container-style {:padding-horizontal 20} + :horizontal true + :shows-horizontal-scroll-indicator false + :keyboard-should-persist-taps :handled}]])) diff --git a/src/status_im/contexts/chat/messenger/composer/link_preview/events.cljs b/src/status_im/contexts/chat/messenger/composer/link_preview/events.cljs new file mode 100644 index 00000000000..985e0c1493f --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/link_preview/events.cljs @@ -0,0 +1,165 @@ +(ns status-im.contexts.chat.messenger.composer.link-preview.events + (:require + [clojure.set :as set] + [clojure.string :as string] + [legacy.status-im.data-store.messages :as data-store.messages] + [taoensso.timbre :as log] + [utils.collection] + [utils.re-frame :as rf])) + +(rf/defn unfurl-urls + {:events [:link-preview/unfurl-urls]} + [{:keys [db]} text] + (if (string/blank? text) + {:db (update db :chat/link-previews dissoc :unfurled :request-id)} + {:json-rpc/call + [{:method "wakuext_getTextURLs" + :params [text] + :on-success #(rf/dispatch [:link-preview/unfurl-parsed-urls %]) + :on-error (fn [error] + (log/error "Failed to parse text and extract URLs" + {:error error + :event :link-preview/unfurl-urls}))}]})) + +(defn- urls->previews + [preview-cache urls] + (->> urls + (map #(get preview-cache % {:url % :loading? true})) + (remove :failed?))) + +(def new-request-id (comp str random-uuid)) + +(rf/defn unfurl-parsed-urls + {:events [:link-preview/unfurl-parsed-urls]} + [{:keys [db]} urls] + (let [cleared (set (get-in db [:chat/link-previews :cleared]))] + (when (or (empty? urls) + (not= (set urls) cleared)) + (let [cache (get-in db [:chat/link-previews :cache]) + previews (urls->previews cache urls) + new-urls (->> previews + (filter (fn [{:keys [loading? status-link-preview?]}] + (or loading? status-link-preview?))) + (map :url)) + ;; `request-id` is a must because we need to process only the last + ;; unfurling event, as well as avoid needlessly updating the app db + ;; if the user changes the URLs in the input text when there are + ;; in-flight RPC requests. + request-id (new-request-id)] + (merge {:db (-> db + (assoc-in [:chat/link-previews :unfurled] previews) + (assoc-in [:chat/link-previews :request-id] request-id) + (update :chat/link-previews dissoc :cleared))} + (when (seq new-urls) + (log/debug "Unfurling URLs" {:urls new-urls :request-id request-id}) + {:json-rpc/call + [{:method "wakuext_unfurlURLs" + :params [new-urls] + :on-success #(rf/dispatch [:link-preview/unfurl-parsed-urls-success request-id %]) + :on-error #(rf/dispatch [:link-preview/unfurl-parsed-urls-error request-id + %])}]})))))) + +(defn- failed-previews + [curr-previews new-previews] + (let [curr-urls (set (->> curr-previews + (filter :loading?) + (map :url))) + new-urls (set (map :url new-previews))] + (map (fn [url] + {:url url :failed? true}) + (set/difference curr-urls new-urls)))) + +(defn- reconcile-unfurled + [curr-previews indexed-new-previews] + (reduce (fn [acc preview] + (if (:loading? preview) + (if-let [loaded-preview (get indexed-new-previews + (:url preview))] + (conj acc loaded-preview) + acc) + (conj acc preview))) + [] + curr-previews)) + +(defn- merge-preview-types + [new-previews new-status-link-previews] + (-> (map + (fn [{{:keys [display-name members-count banner icon + description]} :community + url :url}] + ;; Other types of previews need to be done here too + ;; Like User link preview and Channel link preview + {:url url + :display-name display-name + :members-count members-count + :banner banner + :icon icon + :description description + ;; Need to set status-link-preview? + ;; to true to not send status-link-previews + ;; with normal link-previews + :status-link-preview? true}) + new-status-link-previews) + (concat new-previews))) + +(rf/defn unfurl-parsed-urls-success + {:events [:link-preview/unfurl-parsed-urls-success]} + [{:keys [db]} request-id {new-previews :linkPreviews status-link-previews :statusLinkPreviews}] + (when (= request-id (get-in db [:chat/link-previews :request-id])) + (let [new-previews (map data-store.messages/<-link-preview-rpc new-previews) + new-status-link-previews (map data-store.messages/<-status-link-previews-rpc + status-link-previews) + merged-preview-types (merge-preview-types new-previews new-status-link-previews) + curr-previews (get-in db [:chat/link-previews :unfurled]) + indexed-new-previews (utils.collection/index-by :url merged-preview-types)] + (log/debug "URLs unfurled" + :chat/link-previews + {:event :link-preview/unfurl-parsed-urls-success + :previews (map #(update % :thumbnail dissoc :data-uri) merged-preview-types) + :request-id request-id}) + {:db (-> db + (assoc-in [:chat/status-link-previews :unfurled] new-status-link-previews) + (update-in [:chat/link-previews :unfurled] reconcile-unfurled indexed-new-previews) + (update-in [:chat/link-previews :cache] + merge + indexed-new-previews + (utils.collection/index-by :url + (failed-previews curr-previews + merged-preview-types))))}))) + +(rf/defn unfurl-parsed-urls-error + {:events [:link-preview/unfurl-parsed-urls-error]} + [_cofx request-id error] + (log/error "Failed to unfurl URLs" + {:request-id request-id + :error error + :event :link-preview/unfurl-parsed-urls})) + +(rf/defn reset-unfurled + "Reset preview state, but keep the cache. Use this event after a message is + sent." + {:events [:link-preview/reset-unfurled]} + [{:keys [db]}] + {:db (-> db + (update :chat/link-previews dissoc :unfurled :request-id :cleared) + (update :chat/status-link-previews dissoc :unfurled :request-id :cleared))}) + +(rf/defn reset-all + "Reset all preview state. It is especially important to delete any cached + URLs, as failing to do so results in its unbounded growth." + {:events [:link-preview/reset-all]} + [{:keys [db]}] + {:db (-> db + (dissoc :chat/link-previews) + (dissoc :chat/status-link-previews))}) + +(rf/defn clear-link-previews + "Mark current unfurled URLs as `cleared`, meaning the user won't see previews + until they insert/remove non-cleared URL(s)." + {:events [:link-preview/clear]} + [{:keys [db]}] + (let [unfurled-urls (set (map :url (get-in db [:chat/link-previews :unfurled])))] + {:db (-> db + (update :chat/link-previews dissoc :unfurled :request-id) + (update :chat/status-link-previews dissoc :unfurled :request-id) + (assoc-in [:chat/link-previews :cleared] unfurled-urls))})) diff --git a/src/status_im/contexts/chat/messenger/composer/link_preview/events_test.cljs b/src/status_im/contexts/chat/messenger/composer/link_preview/events_test.cljs new file mode 100644 index 00000000000..6359375f1d9 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/link_preview/events_test.cljs @@ -0,0 +1,218 @@ +(ns status-im.contexts.chat.messenger.composer.link-preview.events-test + (:require + [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.contexts.chat.messenger.composer.link-preview.events :as events])) + +(def url-github "https://github.com") +(def url-gitlab "https://gitlab.com") +(def url-community + "https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11") +(def preview-github {:url url-github :thumbnail nil}) +(def preview-gitlab {:url url-gitlab :thumbnail nil}) +(def request-id "abc123") + +(defn remove-rpc-callbacks + [effects] + (if (get-in effects [:json-rpc/call 0]) + (update-in effects [:json-rpc/call 0] dissoc :on-success :on-error) + effects)) + +(deftest unfurl-urls-test + (testing "clear up state when text is empty" + (let [cofx {:db {:chat/link-previews {:unfurled {} + :cache {} + :request-id "123"}}}] + (is (match? {:db {:chat/link-previews {:cache {}}}} + (events/unfurl-urls cofx " "))))) + + (testing "fetches parsed URLs" + (let [cofx {:db {:chat/link-previews {:unfurled {} + :cache {} + :request-id "123"}}}] + (is (match? {:json-rpc/call [{:method "wakuext_getTextURLs" + :params [url-github]}]} + (remove-rpc-callbacks + (events/unfurl-urls cofx url-github)))))) + (testing "fetches status community URLs" + (let [cofx {:db {:chat/link-previews {:unfurled {} + :cache {} + :request-id "123"}}}] + (is (match? {:json-rpc/call [{:method "wakuext_getTextURLs" + :params [url-community]}]} + (remove-rpc-callbacks + (events/unfurl-urls cofx url-community))))))) + +(deftest unfurl-parsed-urls-test + (with-redefs [events/new-request-id (constantly request-id)] + (testing "empty state with 2 stale URLs" + (let [cofx {:db {:chat/link-previews + {:cache {} + :unfurled []}}}] + (is (match? {:db {:chat/link-previews + {:request-id request-id + :cache {} + :unfurled [{:url url-github :loading? true} + {:url url-gitlab :loading? true}]}} + :json-rpc/call [{:method "wakuext_unfurlURLs" + :params [[url-github url-gitlab]]}]} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "nothing unfurled, 1 cached URL and the other stale" + (let [cache {url-github preview-github} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled []}}}] + (is (match? {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [preview-github + {:url url-gitlab :loading? true}]}} + :json-rpc/call [{:method "wakuext_unfurlURLs" + :params [[url-gitlab]]}]} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "does nothing when the URLs are identical to the last cleared ones" + (let [cofx {:db {:chat/link-previews + {:cache {url-github preview-github} + :cleared #{url-github url-gitlab} + :unfurled [preview-github preview-gitlab]}}}] + (is (nil? (events/unfurl-parsed-urls cofx [url-github url-gitlab]))))) + + (testing "app db has previews, but the new parsed text has no valid URLs" + (let [cache {url-github preview-github} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github]}}}] + (is (match? {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled []}}} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [])))))) + + (testing "2 unfurled, 2 cached URLs" + (let [cache {url-github preview-github url-gitlab preview-gitlab} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github preview-gitlab]}}}] + (is (match? {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [preview-github preview-gitlab]}}} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "1 unfurled, 1 successful in the cache, 1 failed in the cache" + (let [cache {url-github preview-github + url-gitlab (assoc preview-gitlab :failed? true)} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github]}}}] + (is (match? {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [preview-github]}}} + (events/unfurl-parsed-urls cofx [url-github url-gitlab]))))) + + (testing "nothing unfurled, 1 failed in the cache" + (let [cache {url-gitlab (assoc preview-gitlab :failed? true)} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled []}}}] + (is (match? {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [{:url url-github :loading? true}]}} + :json-rpc/call [{:method "wakuext_unfurlURLs" + :params [[url-github]]}]} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "empties unfurled collection when there are no valid previews" + (let [unknown-url "https://github.abcdef" + cache {unknown-url {:url unknown-url :failed? true} + url-github preview-github} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github]}}}] + (is (match? {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled []}}} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [unknown-url])))))))) + +(deftest unfurl-parsed-urls-success-test + (testing "does nothing if the success request-id is different than the current one" + (let [cofx {:db {:chat/link-previews + {:request-id request-id + :unfurled [] + :cache {}}}}] + (is (nil? (events/unfurl-parsed-urls-success cofx "banana" {:linkPreviews [preview-github]}))))) + + (testing "reconciles new previews with existing ones" + (let [cofx {:db {:chat/link-previews + {:request-id request-id + :unfurled [preview-github + {:url url-gitlab :loading? true}] + :cache {url-github preview-github}}}} + {db :db} (events/unfurl-parsed-urls-success + cofx + request-id + {:linkPreviews [preview-gitlab]})] + (is (match? {:chat/link-previews + {:request-id request-id + :unfurled [preview-github preview-gitlab] + :cache {url-github preview-github + url-gitlab preview-gitlab}}} + db)))) + + (testing "identify and write failed preview in the cache" + (let [preview-youtube {:url "https://youtube.com" :thumbnail nil} + cofx {:db {:chat/link-previews + {:request-id request-id + :unfurled [{:url url-github :loading? true} + preview-youtube + {:url url-gitlab :loading? true}] + :cache {(:url preview-youtube) preview-youtube}}}} + {db :db} (events/unfurl-parsed-urls-success + cofx + request-id + {:linkPreviews [preview-github + preview-youtube]})] + (is (match? {:chat/link-previews + {:request-id request-id + :unfurled [preview-github preview-youtube] + :cache {(:url preview-youtube) preview-youtube + url-github preview-github + url-gitlab {:url "https://gitlab.com" :failed? true}}}} + db))))) + +(deftest clear-link-previews-test + (let [cache {url-github preview-github} + effects (events/clear-link-previews + {:db {:chat/link-previews {:unfurled [preview-github] + :request-id request-id + :cache cache}}})] + (is (match? {:db {:chat/link-previews {:cache cache + :cleared #{url-github}}}} + effects)))) + +(deftest reset-unfurled-test + (let [cache {url-github preview-github}] + (is (match? {:db {:chat/link-previews {:cache cache}}} + (events/reset-unfurled + {:db {:chat/link-previews {:unfurled [preview-github] + :request-id request-id + :cleared #{url-github} + :cache cache}}}))))) + +(deftest reset-all-test + (is (match? {:db {:non-related-key :some-value}} + (events/reset-all + {:db {:non-related-key :some-value + :chat/link-previews {:request-id request-id + :any-other-key "farewell"}}})))) diff --git a/src/status_im/contexts/chat/messenger/composer/link_preview/style.cljs b/src/status_im/contexts/chat/messenger/composer/link_preview/style.cljs new file mode 100644 index 00000000000..9564af255d8 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/link_preview/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.chat.messenger.composer.link-preview.style + (:require [status-im.contexts.chat.messenger.composer.constants :as constants])) + +(def padding-horizontal 20) +(def preview-height 56) + +(def preview-list + {:padding-top constants/links-padding-top + :padding-bottom constants/links-padding-bottom + :margin-horizontal (- padding-horizontal) + ;; Keep a high index, otherwise the parent gesture detector used by the + ;; composer grabs the initiating gesture event. + :z-index 9999}) diff --git a/src/status_im/contexts/chat/messenger/composer/link_preview/view.cljs b/src/status_im/contexts/chat/messenger/composer/link_preview/view.cljs new file mode 100644 index 00000000000..71945d85899 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/link_preview/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.chat.messenger.composer.link-preview.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.common.resources :as resources] + [status-im.constants] + [status-im.contexts.chat.messenger.composer.constants :as constants] + [status-im.contexts.chat.messenger.composer.link-preview.events] + [status-im.contexts.chat.messenger.composer.link-preview.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- use-animated-height + [previews?] + (let [height (reanimated/use-shared-value (if previews? constants/links-container-height 0))] + (rn/use-effect + (fn [] + (reanimated/animate height (if previews? constants/links-container-height 0))) + [previews?]) + height)) + +(defn view + [] + (let [previews (rf/sub [:chats/link-previews-unfurled]) + height (use-animated-height (boolean (seq previews)))] + [reanimated/view + {:style (reanimated/apply-animations-to-style {:height height} {:z-index 1})} + [quo/url-preview-list + {:key-fn :url + :preview-width (- (:width (rn/get-window)) + (* 2 style/padding-horizontal)) + :container-style (when (seq previews) style/preview-list) + :container-style-item {:height style/preview-height} + :horizontal-spacing style/padding-horizontal + :loading-message (i18n/label :t/link-preview-loading-message) + :on-clear #(rf/dispatch [:link-preview/clear]) + :data (map + (fn [{:keys [title display-name thumbnail hostname loading? url favicon]}] + {:title (or display-name title) + :body (or (when-not display-name hostname) + status-im.constants/status-hostname) + :logo (if (string/starts-with? url "https://status.app") + (resources/get-mock-image :status-logo) + favicon) + :loading? loading? + :thumbnail (:data-uri thumbnail) + :url url}) + previews)}]])) diff --git a/src/status_im/contexts/chat/messenger/composer/mentions/style.cljs b/src/status_im/contexts/chat/messenger/composer/mentions/style.cljs new file mode 100644 index 00000000000..158aa2e254f --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/mentions/style.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.chat.messenger.composer.mentions.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [status-im.contexts.chat.messenger.composer.constants :as constants])) + +(defn- add-shadow + [theme styles] + (if platform/ios? + (assoc styles + :shadow-radius (colors/theme-colors 30 50 theme) + :shadow-opacity (colors/theme-colors 0.1 0.7 theme) + :shadow-color colors/neutral-100 + :shadow-offset {:width 0 :height (colors/theme-colors 8 12 theme)}) + (assoc styles :elevation 10))) + +(defn container + [opacity top theme] + [{:opacity opacity} + (add-shadow theme + {:position :absolute + :top (- (+ 8 top)) + :left 8 + :right 8 + :border-radius 16 + :z-index 4 + :max-height constants/mentions-max-height + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)})]) diff --git a/src/status_im/contexts/chat/messenger/composer/mentions/view.cljs b/src/status_im/contexts/chat/messenger/composer/mentions/view.cljs new file mode 100644 index 00000000000..c2ea6d905e7 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/mentions/view.cljs @@ -0,0 +1,45 @@ +(ns status-im.contexts.chat.messenger.composer.mentions.view + (:require + [quo.context] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.contexts.chat.messenger.composer.constants :as constants] + [status-im.contexts.chat.messenger.composer.mentions.style :as style] + [status-im.contexts.chat.messenger.messages.constants :as messages.constants] + [utils.re-frame :as rf])) + +(defn- mention-item + [[user-key user]] + (with-meta + [contact-list-item/contact-list-item {:on-press #(rf/dispatch [:chat.ui/select-mention user])} + user] + {:key user-key})) + +(defn view + [layout-height] + (let [suggestions (rf/sub [:chat/mention-suggestions]) + suggestions? (seq suggestions) + theme (quo.context/use-theme) + opacity (reanimated/use-shared-value (if suggestions? 1 0)) + [suggestions-state + set-suggestions-state] (rn/use-state suggestions) + top (min constants/mentions-max-height + (* (count suggestions-state) 56) + (- @layout-height + (+ safe-area/top + messages.constants/top-bar-height + 5)))] + (rn/use-effect + (fn [] + (if suggestions? + (set-suggestions-state suggestions) + (js/setTimeout #(set-suggestions-state suggestions) 300)) + (reanimated/animate opacity (if suggestions? 1 0))) + [suggestions]) + [reanimated/view {:style (style/container opacity top theme)} + [rn/scroll-view + {:accessibility-label :mentions-list + :keyboard-should-persist-taps :always} + (map mention-item suggestions-state)]])) diff --git a/src/status_im/contexts/chat/messenger/composer/reply/style.cljs b/src/status_im/contexts/chat/messenger/composer/reply/style.cljs new file mode 100644 index 00000000000..01c268946fb --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/reply/style.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.chat.messenger.composer.reply.style) + + +(defn container + [pin? in-chat-input?] + {:flex-direction :row + :height (when-not pin? 24) + :margin-left (when (and (not in-chat-input?) (not pin?)) 26) + :margin-bottom (when (and (not in-chat-input?) (not pin?)) 8)}) + +(defn reply-content + [pin?] + {:padding-right (when-not pin? 10) + :flex 1 + :flex-direction :row}) + +(defn quoted-message + [pin?] + (merge {:flex-direction :row + :flex 1 + :align-items :center + :margin-top 2} + (when-not pin? + {:left 22 + :margin-right 22}))) + +(def reply-from + {:flex-direction :row + :align-items :center}) + +(def message-author-text + {:margin-left 4}) + +(def message-text + {:text-transform :none + :margin-left 4 + :flex-shrink 1}) + +(def gradient + {:position :absolute + :right 0 + :top 0 + :bottom 0 + :width "50%"}) + +(def reply-deleted-message + {:text-transform :none + :margin-left 4 + :margin-top 2}) diff --git a/src/status_im/contexts/chat/messenger/composer/reply/view.cljs b/src/status_im/contexts/chat/messenger/composer/reply/view.cljs new file mode 100644 index 00000000000..a2a16813244 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/reply/view.cljs @@ -0,0 +1,175 @@ +(ns status-im.contexts.chat.messenger.composer.reply.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [react-native.reanimated :as reanimated] + [status-im.constants :as constant] + [status-im.contexts.chat.messenger.composer.constants :as constants] + [status-im.contexts.chat.messenger.composer.effects :as effects] + [status-im.contexts.chat.messenger.composer.reply.style :as style] + [status-im.contexts.chat.messenger.composer.utils :as utils] + [utils.ens.stateofus :as stateofus] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn get-quoted-text-with-mentions + [parsed-text] + (string/join + (mapv (fn [{:keys [type literal children]}] + (cond + (= type "paragraph") + (get-quoted-text-with-mentions children) + + (= type "mention") + (rf/sub [:messages/resolve-mention literal]) + + (= type "status-tag") + (str "#" literal) + + (seq children) + (get-quoted-text-with-mentions children) + + :else + literal)) + parsed-text))) + +(defn format-author + [contact-name] + (let [author (if (or (= (first contact-name) "@") + ;; in case of replies + (= (second contact-name) "@")) + (or (stateofus/username contact-name) + (subs contact-name 0 81)) + contact-name)] + author)) + +(defn format-reply-author + [from username current-public-key] + (or (and (= from current-public-key) + (i18n/label :t/You)) + (when username (format-author username)))) + +(defn reply-deleted-message + [] + [rn/view + {:style {:flex-direction :row + :align-items :center}} + [quo/icon :i/sad-face {:size 16}] + [quo/text + {:number-of-lines 1 + :size :label + :weight :regular + :accessibility-label :quoted-message + :style style/reply-deleted-message} + (i18n/label :t/message-deleted)]]) + +(defn- bridge-message-user-name + [user-name] + (when (string? user-name) + (-> user-name + (string/replace "" "") + (string/replace "" "")))) + +(defn reply-from + [{:keys [from contact-name current-public-key pin? bridge-message]}] + (let [{user-name :userName user-avatar :userAvatar} bridge-message + [primary-name _] (rf/sub [:contacts/contact-two-names-by-identity + from]) + photo-path (rf/sub [:chats/photo-path from])] + [rn/view {:style style/reply-from} + [quo/user-avatar + {:full-name (if bridge-message + (bridge-message-user-name user-name) + primary-name) + :profile-picture (or user-avatar photo-path) + :status-indicator? false + :size :xxxs}] + [quo/text + {:weight :semi-bold + :size (if pin? :label :paragraph-2) + :number-of-lines 1 + :style style/message-author-text} + (if bridge-message + user-name + (format-reply-author from contact-name current-public-key))]])) + +(defn quoted-message + [{:keys [from content-type contentType parsed-text content deleted? deleted-for-me? + album-images-count bridge-message]} + in-chat-input? pin? recording-audio? input-ref] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from]) + current-public-key (rf/sub [:multiaccount/public-key]) + theme (quo.context/use-theme) + content-type (or content-type contentType) + text (get-quoted-text-with-mentions (or parsed-text (:parsed-text content)))] + [rn/view + {:style (style/container pin? in-chat-input?) + :accessibility-label :reply-message} + [rn/view {:style (style/reply-content pin?)} + (when-not pin? + [quo/icon :i/connector + {:size 16 + :color (colors/theme-colors colors/neutral-40 colors/neutral-60 theme) + :container-style {:position :absolute :left 0 :bottom -4 :width 16 :height 16}}]) + (if (or deleted? deleted-for-me?) + [rn/view {:style (style/quoted-message pin?)} + [reply-deleted-message]] + [rn/view {:style (style/quoted-message pin?)} + [reply-from + {:pin? pin? + :from from + :contact-name primary-name + :current-public-key current-public-key + :bridge-message bridge-message}] + (when (not-empty text) + [quo/text + {:number-of-lines 1 + :size :label + :weight :regular + :accessibility-label :quoted-message + :ellipsize-mode :tail + :style style/message-text} + text]) + [quo/text + {:size :label + :weight :regular + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :margin-top 2}} + (str " " + (condp = (or content-type contentType) + constant/content-type-image (if (pos? album-images-count) + (i18n/label :t/album-images-count + {:album-images-count album-images-count}) + (i18n/label :t/photo)) + constant/content-type-sticker (i18n/label :t/sticker) + constant/content-type-audio (i18n/label :t/audio) + ""))]])] + (when (and in-chat-input? (not recording-audio?)) + [quo/button + {:icon-only? true + :size 24 + :accessibility-label :reply-cancel-button + :on-press #(utils/cancel-reply-message input-ref) + :type :outline} + :i/close]) + (when (and in-chat-input? recording-audio?) + [linear-gradient/linear-gradient + {:colors [(colors/theme-colors colors/white-opa-0 colors/neutral-90-opa-0 theme) + (colors/theme-colors colors/white colors/neutral-90 theme)] + :start {:x 0 :y 0} + :end {:x 0.7 :y 0} + :style style/gradient}])])) + +(defn view + [input-ref] + (let [reply (rf/sub [:chats/reply-message]) + height (reanimated/use-shared-value (if reply constants/reply-container-height 0))] + (effects/use-reply input-ref reply) + (rn/use-effect #(reanimated/animate height (if reply constants/reply-container-height 0)) [reply]) + [reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})} + (when reply + [quoted-message reply true false false input-ref])])) diff --git a/src/status_im/contexts/chat/messenger/composer/selection.cljs b/src/status_im/contexts/chat/messenger/composer/selection.cljs new file mode 100644 index 00000000000..e2451afe38e --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/selection.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.chat.messenger.composer.selection + (:require + [clojure.string :as string] + [oops.core :as oops] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(declare first-level-menu-items second-level-menu-items) + +(defn update-input-text + [{:keys [text-input]} text] + (rf/dispatch [:chat.ui/set-chat-input-text text]) + (.setNativeProps ^js text-input (clj->js {:text text}))) + +(defn calculate-input-text + [{:keys [full-text selection-start selection-end]} content] + (let [head (subs full-text 0 selection-start) + tail (subs full-text selection-end)] + (str head content tail))) + +(defn update-selection + [text-input-handle selection-start selection-end] + ;; to avoid something like this + ;; https://lightrun.com/answers/facebook-react-native-textinput-controlled-selection-broken-on-both-ios-and-android + ;; use native invoke instead! do not use setNativeProps! e.g. (.setNativeProps ^js text-input + ;; (clj->js + ;; {:selection {:start selection-start :end selection-end}})) + (let [manager (rn/selectable-text-input-manager)] + (oops/ocall manager :setSelection text-input-handle selection-start selection-end))) + +(defn reset-to-first-level-menu + [first-level? menu-items] + (reset! first-level? true) + (reset! menu-items first-level-menu-items)) + +(defn append-markdown-char + [{:keys [first-level? menu-items content selection-start selection-end text-input-handle + selection-event] + :as params} wrap-chars] + (let [content (str wrap-chars content wrap-chars) + new-text (calculate-input-text params content) + len-wrap-chars (count wrap-chars) + selection-start (+ selection-start len-wrap-chars) + selection-end (+ selection-end len-wrap-chars)] + ;; don't update selection directly here, process it within on-selection-change instead + ;; so that we can avoid java.lang.IndexOutOfBoundsException: setSpan.. + (reset! selection-event {:start selection-start + :end selection-end + :text-input-handle text-input-handle}) + (update-input-text params new-text) + (reset-to-first-level-menu first-level? menu-items))) + +(def first-level-menus + {:cut (fn [{:keys [content] :as params}] + (let [new-text (calculate-input-text params "")] + (clipboard/set-string content) + (update-input-text params new-text))) + + :copy-to-clipboard (fn [{:keys [content]}] + (clipboard/set-string content)) + + :paste (fn [params] + (let [callback (fn [paste-content] + (let [content (string/trim paste-content) + new-text (calculate-input-text params content)] + (update-input-text params new-text)))] + (clipboard/get-string callback))) + + :biu (fn [{:keys [first-level? text-input-handle menu-items selection-start + selection-end]}] + (reset! first-level? false) + (reset! menu-items second-level-menu-items) + (update-selection text-input-handle selection-start selection-end))}) + +(def first-level-menu-items (map i18n/label (keys first-level-menus))) +(def second-level-menus + {:bold #(append-markdown-char % "**") + + :italic #(append-markdown-char % "*") + + :strikethrough #(append-markdown-char % "~~")}) + +(def second-level-menu-items (map i18n/label (keys second-level-menus))) diff --git a/src/status_im/contexts/chat/messenger/composer/style.cljs b/src/status_im/contexts/chat/messenger/composer/style.cljs new file mode 100644 index 00000000000..720b373e9dd --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.chat.messenger.composer.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.typography :as typography])) + +(defn input-text + [theme] + (assoc typography/paragraph-1 + :color (colors/theme-colors :black :white theme) + :text-align-vertical :top + :top 0 + :left 0 + :max-height 150)) diff --git a/src/status_im/contexts/chat/messenger/composer/utils.cljs b/src/status_im/contexts/chat/messenger/composer/utils.cljs new file mode 100644 index 00000000000..a82199d6230 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/utils.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.chat.messenger.composer.utils + (:require + [utils.number] + [utils.re-frame :as rf])) + +(defn blur-input + [input-ref] + (when @input-ref + (rf/dispatch [:chat.ui/set-input-focused false]) + (.blur ^js @input-ref))) + +(defn cancel-reply-message + [input-ref] + (js/setTimeout #(blur-input input-ref) 100) + (rf/dispatch [:chat.ui/cancel-message-reply])) + +(defn cancel-edit-message + [input-ref] + ;; NOTE: adding a timeout to assure the input is blurred on the next tick + ;; after the `text-value` was cleared. Otherwise the height will be calculated + ;; with the old `text-value`, leading to wrong composer height after blur. + (js/setTimeout + (fn [] + (blur-input input-ref)) + 100) + (.setNativeProps ^js @input-ref (clj->js {:text ""})) + (rf/dispatch [:chat.ui/cancel-message-edit])) diff --git a/src/status_im/contexts/chat/messenger/composer/view.cljs b/src/status_im/contexts/chat/messenger/composer/view.cljs new file mode 100644 index 00000000000..8ef7b14dfb0 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/composer/view.cljs @@ -0,0 +1,56 @@ +(ns status-im.contexts.chat.messenger.composer.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.contexts.chat.messenger.composer.actions.view :as actions] + [status-im.contexts.chat.messenger.composer.constants :as constants] + [status-im.contexts.chat.messenger.composer.edit.view :as edit] + [status-im.contexts.chat.messenger.composer.handlers :as handler] + [status-im.contexts.chat.messenger.composer.images.view :as images] + [status-im.contexts.chat.messenger.composer.link-preview.view :as link-preview] + [status-im.contexts.chat.messenger.composer.mentions.view :as mentions] + [status-im.contexts.chat.messenger.composer.reply.view :as reply] + [status-im.contexts.chat.messenger.composer.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn input + [_ _] + (let [default-value (:input-text (rf/sub [:chats/current-chat-input]))] + (fn [set-ref theme] + [rn/text-input + {:ref set-ref + :on-change-text handler/change-text + :keyboard-appearance theme + :max-font-size-multiplier 1 + :multiline true + :placeholder (i18n/label :t/type-something) + :placeholder-text-color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme) + :max-length constants/max-text-size + :accessibility-label :chat-message-input + :style (style/input-text theme) + :default-value default-value}]))) + +(defn view + [layout-height] + (let [theme (quo.context/use-theme) + bottom safe-area/bottom + input-ref (rn/use-ref-atom nil) + set-ref (rn/use-callback (fn [value] + (rf/dispatch [:chat/set-input-ref value]) + (reset! input-ref value)))] + [rn/view {:style {:margin-bottom bottom}} + [mentions/view layout-height] + [quo/separator] + [rn/view {:style {:padding-horizontal 20 :padding-top 20}} + [:<> + [reply/view input-ref] + [edit/view input-ref]] + [input set-ref theme] + [:<> + [link-preview/view] + [images/images-list]] + [actions/view input-ref]]])) diff --git a/src/status_im/contexts/chat/messenger/menus/pinned_messages/style.cljs b/src/status_im/contexts/chat/messenger/menus/pinned_messages/style.cljs new file mode 100644 index 00000000000..ca3a9030624 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/menus/pinned_messages/style.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.chat.messenger.menus.pinned-messages.style + (:require + [react-native.platform :as platform])) + +(defn heading + [community?] + {:padding-horizontal 20 + :margin-bottom (when-not community? 12)}) + +(def community-tag-container + {:margin-horizontal 20 + :margin-top 4 + :margin-bottom 12}) + +(def no-pinned-messages-container + {:justify-content :center + :align-items :center}) + +(def list-footer + {:height (when platform/android? 12)}) diff --git a/src/status_im/contexts/chat/messenger/menus/pinned_messages/view.cljs b/src/status_im/contexts/chat/messenger/menus/pinned_messages/view.cljs new file mode 100644 index 00000000000..ee82208f323 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/menus/pinned_messages/view.cljs @@ -0,0 +1,70 @@ +(ns status-im.contexts.chat.messenger.menus.pinned-messages.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.common.resources :as resources] + [status-im.contexts.chat.messenger.menus.pinned-messages.style :as style] + [status-im.contexts.chat.messenger.messages.content.view :as message] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def list-key-fn #(or (:message-id %) (:value %))) + +(defn community-avatar + [community-images] + (when community-images + (:uri (or (:thumbnail community-images) + (:large community-images) + (first community-images))))) + +(defn message-render-fn + [message _ _ context] + [message/message message context (atom false)]) + +(defn empty-pinned-messages-state + [] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/no-pinned-messages-container} + [quo/empty-state + {:blur? false + :image (resources/get-themed-image :no-pinned-messages theme) + :title (i18n/label :t/no-pinned-messages) + :description (i18n/label :t/no-pinned-messages-desc)}]])) + +(defn view + [{:keys [chat-id]}] + (let [pinned (rf/sub [:chats/pinned-sorted-list chat-id]) + screen-id (quo.context/use-screen-id) + render-data (rf/sub [:chats/current-chat-message-list-view-context :in-pinned-view]) + current-chat (rf/sub [:chats/chat-by-id chat-id]) + {:keys [community-id]} current-chat + community (rf/sub [:communities/community community-id]) + community-images (rf/sub [:community/images community-id])] + [gesture/scroll-view + {:accessibility-label :pinned-messages-menu + :bounces false} + [:<> + [quo/text + {:size :heading-2 + :weight :semi-bold + :style (style/heading community)} + (i18n/label :t/pinned-messages)] + (when community + [quo/context-tag + {:type :channel + :size 24 + :container-style style/community-tag-container + :community-logo (community-avatar community-images) + :community-name (:name community) + :channel-name (:chat-name current-chat)}])] + (if (pos? (count pinned)) + [rn/flat-list + {:data pinned + :render-data (assoc render-data :disable-message-long-press? (not= screen-id :screen/chat)) + :render-fn message-render-fn + :footer [rn/view {:style style/list-footer}] + :key-fn list-key-fn + :separator [quo/separator {:style {:margin-vertical 8}}]}] + [empty-pinned-messages-state])])) diff --git a/src/status_im/contexts/chat/messenger/messages/avatar/view.cljs b/src/status_im/contexts/chat/messenger/messages/avatar/view.cljs new file mode 100644 index 00000000000..30d14a9cf14 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/avatar/view.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.chat.messenger.messages.avatar.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [utils.re-frame :as rf])) + +(defn avatar + [{:keys [public-key size]}] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity public-key]) + photo-path (rf/sub [:chats/photo-path public-key]) + online? (rf/sub [:visibility-status-updates/online? public-key])] + [rn/view {:style {:padding-top 4}} + [rn/touchable-opacity + {:active-opacity 1 + :on-press #(rf/dispatch [:chat.ui/show-profile public-key])} + [quo/user-avatar + {:full-name primary-name + :profile-picture photo-path + :online? online? + :size size}]]])) diff --git a/src/status_im/contexts/chat/messenger/messages/constants.cljs b/src/status_im/contexts/chat/messenger/messages/constants.cljs new file mode 100644 index 00000000000..edd58c4c2a8 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/constants.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.chat.messenger.messages.constants) + +;;;; Navigation +(def ^:const top-bar-height 56) +(def ^:const pinned-banner-height 40) diff --git a/src/status_im/contexts/chat/messenger/messages/contact_requests/bottom_drawer/style.cljs b/src/status_im/contexts/chat/messenger/messages/contact_requests/bottom_drawer/style.cljs new file mode 100644 index 00000000000..751c0beb18e --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/contact_requests/bottom_drawer/style.cljs @@ -0,0 +1,7 @@ +(ns status-im.contexts.chat.messenger.messages.contact-requests.bottom-drawer.style) + +(def container + {:position :absolute + :bottom 0 + :left 0 + :right 0}) diff --git a/src/status_im/contexts/chat/messenger/messages/contact_requests/bottom_drawer/view.cljs b/src/status_im/contexts/chat/messenger/messages/contact_requests/bottom_drawer/view.cljs new file mode 100644 index 00000000000..0df5f31a8f8 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/contact_requests/bottom_drawer/view.cljs @@ -0,0 +1,79 @@ +(ns status-im.contexts.chat.messenger.messages.contact-requests.bottom-drawer.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.contact-requests.bottom-drawer.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [contact-id]}] + (let [{:keys [contact-request-state + community-id + chat-name]} (rf/sub [:chats/current-chat-chat-view]) + chat-type (rf/sub [:chats/chat-type]) + [primary-name _] (if (= chat-type :public-chat) + [(str "#" chat-name) ""] + (rf/sub [:contacts/contact-two-names-by-identity contact-id])) + community-chat? (= chat-type :community-chat) + joined (when community-chat? + (rf/sub [:communities/community-joined community-id])) + pending? (when community-chat? + (rf/sub [:communities/my-pending-request-to-join community-id])) + contact-request-send? (or (not contact-request-state) + (= contact-request-state + constants/contact-request-state-none)) + contact-request-received? (= contact-request-state + constants/contact-request-state-received) + contact-request-pending? (= contact-request-state + constants/contact-request-state-sent) + contact-request-dismissed? (= contact-request-state + constants/contact-request-state-dismissed) + keycard? (rf/sub [:keycard/keycard-profile?]) + keycard-feature-unavailable (rn/use-callback + #(rf/dispatch [:keycard/feature-unavailable-show]))] + [rn/view {:style style/container} + [quo/permission-context + {:blur? true + :on-press (cond + (and community-chat? (not pending?) (not joined)) + (if keycard? + keycard-feature-unavailable + #(rf/dispatch [:open-modal :screen/community-account-selection-sheet + {:community-id community-id}])) + + (not community-chat?) + #(rf/dispatch [:chat.ui/show-profile contact-id])) + :type :action + :action-icon (cond + community-chat? :i/communities + contact-request-pending? :i/pending-state + :else :i/add-user) + :action-label (cond + community-chat? + (cond + pending? + (i18n/label :t/request-to-join-community-pending) + + joined + (i18n/label :t/no-permissions-to-post) + + :else + (i18n/label :t/join-community-to-post)) + + (= chat-type :group-chat) + (i18n/label :t/group-chat-not-member) + + contact-request-send? + (i18n/label :t/contact-request-chat-add {:name primary-name}) + + contact-request-received? + (i18n/label :t/contact-request-chat-received {:name primary-name}) + + contact-request-pending? + (i18n/label :t/contact-request-chat-pending) + + contact-request-dismissed? + (i18n/label :t/contact-request-chat-add {:name primary-name}))}]])) + diff --git a/src/status_im/contexts/chat/messenger/messages/content/album/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/album/style.cljs new file mode 100644 index 00000000000..afe483a5ff8 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/album/style.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.chat.messenger.messages.content.album.style + (:require + [quo.foundations.colors :as colors] + [status-im.constants :as constants])) + +(def max-album-height 292) + +(defn album-container + [portrait?] + {:flex-direction (if portrait? :column :row) + :flex-wrap :wrap + :max-height max-album-height + :border-radius 12 + :width (inc (* constants/image-size 2)) + :overflow :hidden}) + +(defn image + [dimensions index portrait? images-count] + {:width (:width dimensions) + :height (:height dimensions) + :margin-left (when (or (and (not= index 0) (not= index 2) (not= images-count 3)) + (and (not portrait?) (= images-count 3) (= index 2)) + (and portrait? (= images-count 3) (> index 0))) + 1) + :margin-bottom (when (< index 2) 1) + :align-self :flex-start}) + +(def overlay + {:position :absolute + :width 73 + :height 73 + :background-color colors/neutral-80-opa-60 + :justify-content :center + :align-items :center}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/album/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/album/view.cljs new file mode 100644 index 00000000000..5cb5b23c9dd --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/album/view.cljs @@ -0,0 +1,97 @@ +(ns status-im.contexts.chat.messenger.messages.content.album.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.content.album.style :as style] + [status-im.contexts.chat.messenger.messages.content.image.view :as image] + [status-im.contexts.chat.messenger.messages.content.lightbox.utils :as lightbox-utils] + [status-im.contexts.chat.messenger.messages.content.lightbox.view :as lightbox] + [status-im.contexts.chat.messenger.messages.content.text.view :as text] + [utils.re-frame :as rf] + [utils.url :as url])) + +(def rectangular-style-count 3) + +(defn find-size + [size-arr album-style] + (if (= album-style :landscape) + {:width (first size-arr) :height (second size-arr) :album-style album-style} + {:width (second size-arr) :height (first size-arr) :album-style album-style})) + +(defn album-message + [{:keys [albumize?] :as message} context on-long-press message-container-data] + (let [animation-shared-element-id (rf/sub [:animation-shared-element-id]) + media-server-port (rf/sub [:mediaserver/port]) + album-messages (:album message) + first-image (first album-messages) + album-style (if (> (:image-width first-image) (:image-height first-image)) + :landscape + :portrait) + images-count (count album-messages) + ;; album images are always square, except when we have 3 images, then they must be + ;; rectangular + ;; (portrait or landscape) + portrait? (and (= images-count rectangular-style-count) + (= album-style :portrait))] + (if (and albumize? (> images-count 1)) + [:<> + [rn/view {:style {:margin-bottom 4}} [text/text-content first-image]] + [rn/view + {:style (style/album-container portrait?) + :accessible true + :accessibility-label :image-container} + (map-indexed + (fn [index item] + (let [images-size-key (if (< images-count constants/max-album-photos) + images-count + :default) + size (get-in constants/album-image-sizes [images-size-key index]) + dimensions (if (not= images-count rectangular-style-count) + {:width size :height size} + (find-size size album-style))] + [rn/touchable-opacity + {:key (:message-id item) + :active-opacity 1 + :on-long-press #(on-long-press message context) + :on-press #(rf/dispatch + [:lightbox/navigate-to-lightbox + (:message-id item) + {:index index + :images (into [] + (map + lightbox-utils/convert-message-to-lightbox-image + album-messages)) + :bottom-text-component + [lightbox/bottom-text-for-lightbox + first-image] + :on-options-press (fn [images index] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [lightbox/drawer + images + index])}]))}])} + [fast-image/fast-image + {:style (style/image dimensions index portrait? images-count) + :source {:uri (url/replace-port (:image (:content item)) media-server-port)} + :native-ID (when (and (= animation-shared-element-id (:message-id item)) + (< index constants/max-album-photos)) + :shared-element)}] + (when (and (> images-count constants/max-album-photos) + (= index (dec constants/max-album-photos))) + [rn/view + {:style style/overlay} + [quo/text + {:weight :bold + :size :heading-2 + :style {:color colors/white}} + (str "+" (- images-count (dec constants/max-album-photos)))]])])) + album-messages)]] + [:<> + (map-indexed + (fn [index item] + [:<> {:key (:message-id item)} + [image/image-message index item {:on-long-press #(on-long-press message context)} + message-container-data]]) + album-messages)]))) diff --git a/src/status_im/contexts/chat/messenger/messages/content/audio/component_spec.cljs b/src/status_im/contexts/chat/messenger/messages/content/audio/component_spec.cljs new file mode 100644 index 00000000000..5ac787ee340 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/audio/component_spec.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.chat.messenger.messages.content.audio.component-spec + (:require + [react-native.audio-toolkit :as audio] + [status-im.contexts.chat.messenger.messages.content.audio.view :as audio-message] + [test-helpers.component :as h])) + +;; We can't rely on `with-redefs` with async code. +(set! audio/destroy-player #()) + +(def message + {:audio-duration-ms 5000 + :message-id "message-id"}) + +(def context + {:in-pinned-view? false}) + +(h/describe "audio message" + (h/setup-restorable-re-frame) + + (h/before-each + (fn [] + (h/setup-subs {:mediaserver/port 1000 + :app-state "active"}))) + + (h/test "renders correctly" + (h/render [audio-message/audio-message message context]) + (h/is-truthy (h/get-by-label-text :audio-message-container))) + + (h/test-skip "press play calls audio/toggle-playpause-player" + (with-redefs [audio/toggle-playpause-player (js/jest.fn) + audio/new-player (fn [_ _ _] {}) + audio/prepare-player (fn [_ on-success _] (on-success)) + audio-message/download-audio-http (fn [_ on-success] (on-success "audio-uri"))] + (h/render [audio-message/audio-message message context]) + (h/fire-event :on-press (h/get-by-label-text :play-pause-audio-message-button)) + (-> (h/expect audio/toggle-playpause-player) + (.toHaveBeenCalledTimes 1)))) + + (h/test "press play renders error" + (with-redefs [audio/new-player (fn [_ _ _] {}) + audio/prepare-player (fn [_ on-success _] (on-success)) + audio-message/download-audio-http (fn [_ on-success] (on-success "audio-uri"))] + (h/render [audio-message/audio-message message context]) + (h/fire-event :on-press (h/get-by-label-text :play-pause-audio-message-button))) + (with-redefs [audio/toggle-playpause-player (fn [_ _ _ on-error] (on-error))] + (h/fire-event :on-press (h/get-by-label-text :play-pause-audio-message-button)) + (h/wait-for #(h/get-by-label-text :audio-error-label))))) diff --git a/src/status_im/contexts/chat/messenger/messages/content/audio/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/audio/style.cljs new file mode 100644 index 00000000000..201cbc49932 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/audio/style.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.chat.messenger.messages.content.audio.style + (:require + [quo.context] + [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:width 295 + :height 56 + :border-radius 12 + :border-width 1 + :padding 12 + :flex-direction :row + :align-items :center + :justify-content :space-between + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-80-opa-40 theme)}) + +(def slider-container + {:position :absolute + :left 60 + :right 71 + :bottom nil}) + +(defn play-pause-container + [theme] + {:background-color (get-in colors/customization [:blue (if (= :dark theme) 60 50)]) + :width 32 + :height 32 + :border-radius 16 + :align-items :center + :justify-content :center}) + +(def timestamp + {:margin-left 4}) + +(def error-label + {:margin-bottom 16}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/audio/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/audio/view.cljs new file mode 100644 index 00000000000..e3370ade9be --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/audio/view.cljs @@ -0,0 +1,236 @@ +(ns status-im.contexts.chat.messenger.messages.content.audio.view + (:require + ["react-native-blob-util" :default ReactNativeBlobUtil] + [goog.string :as gstring] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.audio-toolkit :as audio] + [react-native.core :as rn] + [react-native.platform :as platform] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.content.audio.style :as style] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:const media-server-uri-prefix "https://localhost:") +(def ^:const audio-path "/messages/audio") +(def ^:const uri-param "?messageId=") + +(defonce active-players (atom {})) +(defonce audio-uris (atom {})) +(defonce progress-timer (atom nil)) +(defonce current-player-key (reagent/atom nil)) + +(defn get-player-key + [message-id in-pinned-view?] + (str in-pinned-view? message-id)) + +(defn destroy-player + [player-key] + (when-let [player (@active-players player-key)] + (audio/destroy-player player) + (swap! active-players dissoc player-key))) + +(defn update-state + [state new-state] + (when-not (= @state new-state) + (reset! state new-state))) + +(defn seek-player + [player-key player-state value on-success] + (when-let [player (@active-players player-key)] + (audio/seek-player + player + value + #(when on-success (on-success)) + #(update-state player-state :error)) + (update-state player-state :seeking))) + +(defn download-audio-http + [base64-uri on-success] + (-> (.config ReactNativeBlobUtil (clj->js {:trusty platform/ios?})) + (.fetch "GET" (str base64-uri)) + (.then #(on-success (.base64 ^js %))) + (.catch #(log/error "could not fetch audio " base64-uri)))) + +(defn create-player + [{:keys [progress player-state player-key]} audio-url on-success] + (download-audio-http + audio-url + (fn [base64-data] + (let [player (audio/new-player + (str "data:audio/aac;base64," base64-data) + {:autoDestroy false + :continuesToPlayInBackground false} + (fn [] + (update-state player-state :ready-to-play) + (reset! progress 0) + (when (and @progress-timer (= @current-player-key player-key)) + (js/clearInterval @progress-timer) + (reset! progress-timer nil))))] + (audio/set-player-wake-lock player true) + (swap! active-players assoc player-key player) + (audio/prepare-player + player + #(when on-success (on-success)) + #(update-state player-state :error))))) + (update-state player-state :preparing)) + +(defn play-pause-player + [{:keys [player-key player-state progress message-id audio-duration-ms seeking-audio? + user-interaction? mediaserver-port] + :as params}] + (let [audio-uri (str media-server-uri-prefix + mediaserver-port + audio-path + uri-param + message-id) + player (@active-players player-key) + playing? (= @player-state :playing)] + (when-not playing? + (reset! current-player-key player-key)) + (if (and player + (= (@audio-uris player-key) audio-uri) + (not= (audio/get-state player) audio/IDLE)) + (audio/toggle-playpause-player + player + (fn [] + (update-state player-state :playing) + (when @progress-timer + (js/clearInterval @progress-timer)) + (reset! progress-timer + (js/setInterval + (fn [] + (let [player (@active-players player-key) + current-time (audio/get-player-current-time player) + playing? (= @player-state :playing)] + (when (and playing? + (not @seeking-audio?) + (> current-time 0) + (< current-time constants/audio-max-duration-ms)) + (reset! progress current-time)) + (when (>= current-time constants/audio-max-duration-ms) + (audio/stop-playing + player + (fn [] + (update-state player-state :ready-to-play) + (reset! progress 0) + (when (and @progress-timer (= @current-player-key player-key)) + (js/clearInterval @progress-timer) + (reset! progress-timer nil))) + #(log/error "[audio-message] stop player - error: " %))))) + 100))) + (fn [] + (update-state player-state :ready-to-play) + (when (and @progress-timer user-interaction?) + (js/clearInterval @progress-timer) + (reset! progress-timer nil))) + #(update-state player-state :error)) + (do + (swap! audio-uris assoc player-key audio-uri) + (destroy-player player-key) + (create-player params + audio-uri + (fn [] + (reset! seeking-audio? false) + (if (> @progress 0) + (let [seek-time (* audio-duration-ms @progress) + checked-seek-time (if (<= @progress 1) seek-time @progress)] + (seek-player + player-key + player-state + checked-seek-time + #(play-pause-player params))) + (play-pause-player params)))))))) + +(defn f-audio-message + [player-state progress seeking-audio? {:keys [audio-duration-ms message-id]} + {:keys [in-pinned-view?]}] + (let [theme (quo.context/use-theme) + player-key (get-player-key message-id in-pinned-view?) + player (@active-players player-key) + duration (min constants/audio-max-duration-ms audio-duration-ms) + time-secs (quot + (if (or @seeking-audio? (#{:playing :seeking} @player-state)) + (if (<= @progress 1) (* duration @progress) @progress) + duration) + 1000) + paused? (= (audio/get-state player) audio/PAUSED) + app-state (rf/sub [:app-state]) + mediaserver-port (rf/sub [:mediaserver/port])] + (rn/use-mount (fn [] #(destroy-player player-key))) + (rn/use-effect + (fn [] + (when (or + (and (some? @current-player-key) + (not= @current-player-key player-key) + (= @player-state :playing)) + (and platform/ios? + (= @current-player-key player-key) + (not= app-state "active") + (= @player-state :playing))) + (play-pause-player {:player-key player-key + :player-state player-state + :progress progress + :message-id message-id + :audio-duration-ms duration + :seeking-audio? seeking-audio? + :user-interaction? false + :mediaserver-port mediaserver-port}))) + [@current-player-key app-state mediaserver-port]) + (if (= @player-state :error) + [quo/text + {:style style/error-label + :accessibility-label :audio-error-label + :weight :medium + :size :paragraph-2} + (i18n/label :t/error-loading-audio)] + [rn/view + {:accessibility-label :audio-message-container + :style (style/container theme)} + [rn/touchable-opacity + {:accessibility-label :play-pause-audio-message-button + :allow-multiple-presses? true + :on-press #(play-pause-player {:player-key player-key + :player-state player-state + :progress progress + :message-id message-id + :audio-duration-ms duration + :seeking-audio? seeking-audio? + :user-interaction? true + :mediaserver-port mediaserver-port}) + :style (style/play-pause-container theme)} + [quo/icon + (cond + (= @player-state :preparing) + :i/loading + (and (= @player-state :playing) (not paused?)) + :i/pause-audio + :else :i/play-audio) + {:size 20 + :color colors/white}]] + [quo/soundtrack + {:style style/slider-container + :audio-current-time-ms @progress + :set-audio-current-time-ms #(reset! progress %) + :player-ref (@active-players player-key) + :seeking-audio? @seeking-audio? + :set-seeking-audio? #(reset! seeking-audio? %) + :max-audio-duration-ms constants/audio-max-duration-ms}] + [quo/text + {:style style/timestamp + :accessibility-label :audio-duration-label + :weight :medium + :size :paragraph-2} + (gstring/format "%02d:%02d" (quot time-secs 60) (mod time-secs 60))]]))) + +(defn audio-message + [message context] + (let [player-state (reagent/atom :not-loaded) + progress (reagent/atom 0) + seeking-audio? (reagent/atom false)] + (fn [] + [:f> f-audio-message player-state progress seeking-audio? message context]))) diff --git a/src/status_im/contexts/chat/messenger/messages/content/deleted/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/deleted/view.cljs new file mode 100644 index 00000000000..83b0679fdfe --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/deleted/view.cljs @@ -0,0 +1,79 @@ +(ns status-im.contexts.chat.messenger.messages.content.deleted.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn user-xxx-deleted-this-message + [{:keys [display-name profile-picture]}] + [rn/view + {:style {:flex-direction :row + :align-items :center + :flex-shrink 1}} + [rn/view + {:style {:margin-right 4 + :align-self :center}} + [quo/user-avatar + {:full-name display-name + :profile-picture profile-picture + :status-indicator? false + :size :xxxs}]] + [quo/text + {:weight :semi-bold + :number-of-lines 1 + :style {:flex-shrink 1 :margin-right 4} + :size :paragraph-2} + display-name] + [quo/text + {:size :paragraph-2 + :flex-shrink 0 + :number-of-lines 1} + (i18n/label :t/deleted-this-message)]]) + +(defn deleted-by-message + [{:keys [deleted-by timestamp-str from on-long-press animation-duration]}] + (let [;; deleted message with nil deleted-by is deleted by (:from message) + [primary-name _] (rf/sub [:contacts/contact-two-names-by-identity (or deleted-by from)]) + photo-path (rf/sub [:chats/photo-path (or deleted-by from)])] + [quo/system-message + {:type :deleted + :animate-bg-color? animation-duration + :bg-color-animation-duration animation-duration + :on-long-press on-long-press + :timestamp timestamp-str + :child [user-xxx-deleted-this-message + {:display-name primary-name :profile-picture photo-path}]}])) + +(defn deleted-message + [{:keys [deleted? deleted-for-me? deleted-by pinned timestamp-str from + on-long-press deleted-undoable-till deleted-for-me-undoable-till] + :as message} + {:keys [message-pin-enabled in-pinned-view?]}] + (let [pub-key (rf/sub [:multiaccount/public-key]) + deleted-by-me? (= (or deleted-by from) pub-key) + animation-duration (when-let [deleted-till (or deleted-undoable-till + deleted-for-me-undoable-till)] + (- deleted-till (datetime/timestamp))) + ;; enable long press only when undo delete timer timedout + ;; message pinned and user has permission to unpin + on-long-press (when (and (not animation-duration) + (or (and (or in-pinned-view? pinned) message-pin-enabled) + (and (not deleted?) deleted-for-me?))) + on-long-press)] + (if (and deleted? (not deleted-by-me?)) + [deleted-by-message + (assoc message + :on-long-press on-long-press + :animation-duration animation-duration)] + [quo/system-message + {:type :deleted + :animate-bg-color? animation-duration + :bg-color-animation-duration animation-duration + :on-long-press on-long-press + :label (i18n/label + (if deleted? + :t/message-deleted-for-everyone + :t/message-deleted-for-you)) + :timestamp timestamp-str}]))) diff --git a/src/status_im/contexts/chat/messenger/messages/content/emoji_message/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/emoji_message/style.cljs new file mode 100644 index 00000000000..83707368c8f --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/emoji_message/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.chat.messenger.messages.content.emoji-message.style) + +(defn emoji-container + [margin-top] + {:flex-direction :row + :margin-top margin-top}) + +(def emoji-text + {:font-size 36 + :line-height 42}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/emoji_message/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/emoji_message/view.cljs new file mode 100644 index 00000000000..1233788f0d0 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/emoji_message/view.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.chat.messenger.messages.content.emoji-message.view + (:require [react-native.core :as rn] + [status-im.contexts.chat.messenger.messages.content.emoji-message.style :as style])) + +(defn view + [{:keys [content last-in-group? pinned in-pinned-view?]}] + (let [margin-top (if (or last-in-group? in-pinned-view? pinned) 8 0)] + [rn/view {:style (style/emoji-container margin-top)} + [rn/text {:style style/emoji-text} + (:text content)]])) diff --git a/src/status_im/contexts/chat/messenger/messages/content/image/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/image/view.cljs new file mode 100644 index 00000000000..a8cc0ceb047 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/image/view.cljs @@ -0,0 +1,70 @@ +(ns status-im.contexts.chat.messenger.messages.content.image.view + (:require + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.safe-area :as safe-area] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.content.lightbox.utils :as lightbox-utils] + [status-im.contexts.chat.messenger.messages.content.lightbox.view :as lightbox] + [status-im.contexts.chat.messenger.messages.content.text.view :as text] + [utils.re-frame :as rf] + [utils.url :as url])) + +;; these constants are used when there is no width or height defined for a specific image +(def ^:const fallback-image-width 1000) +(def ^:const fallback-image-height 1000) + +(defn calculate-dimensions + [width height max-container-width max-container-height] + (let [max-width (if (> width height) max-container-width (* 1.5 constants/image-size)) + max-height (if (> width height) max-container-height (* 2 constants/image-size))] + {:width (min width max-width) :height (min height max-height)})) + +(defn image-message + [index + {:keys [content image-width image-height message-id] + :as message + :or {image-width fallback-image-width + image-height fallback-image-height}} + {:keys [on-long-press]} + message-container-data] + (let [{:keys [window-width padding-left padding-right avatar-container-width + message-margin-left]} message-container-data + max-container-width (- window-width + padding-left + padding-right + avatar-container-width + message-margin-left) + max-container-height (* (/ image-height image-width) max-container-width) + dimensions (calculate-dimensions image-width + image-height + max-container-width + max-container-height) + animation-shared-element-id (rf/sub [:animation-shared-element-id]) + image-local-url (url/replace-port (:image content) (rf/sub [:mediaserver/port]))] + [:<> + (when (= index 0) + [text/text-content message]) + [rn/touchable-opacity + {:active-opacity 1 + :style {:margin-top 4} + :on-long-press on-long-press + :on-press #(rf/dispatch [:lightbox/navigate-to-lightbox + message-id + {:images [(lightbox-utils/convert-message-to-lightbox-image + message)] + :index 0 + :insets safe-area/insets + :bottom-text-component + [lightbox/bottom-text-for-lightbox message] + :on-options-press (fn [images index] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] + [lightbox/drawer + images + index])}]))}])} + [fast-image/fast-image + {:source {:uri image-local-url} + :style (merge dimensions {:border-radius 12}) + :native-ID (when (= animation-shared-element-id message-id) :shared-element) + :accessibility-label :image-message}]]])) diff --git a/src/status_im/contexts/chat/messenger/messages/content/lightbox/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/lightbox/style.cljs new file mode 100644 index 00000000000..85bd1ff0a38 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/lightbox/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.chat.messenger.messages.content.lightbox.style + (:require + [quo.foundations.colors :as colors])) + +(defn bottom-text + [expandable-text?] + {:color colors/white + :margin-horizontal 20 + :align-items (when-not expandable-text? :center) + :flex-grow 1}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/lightbox/utils.cljs b/src/status_im/contexts/chat/messenger/messages/content/lightbox/utils.cljs new file mode 100644 index 00000000000..34fb8687a6d --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/lightbox/utils.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.chat.messenger.messages.content.lightbox.utils + (:require + [utils.datetime :as datetime] + [utils.re-frame :as rf])) + +(defn convert-message-to-lightbox-image + [{:keys [timestamp image-width image-height message-id from content]}] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from])] + {:image (:image content) + :image-width image-width + :image-height image-height + :id message-id + :header primary-name + :description (when timestamp (datetime/to-short-str timestamp))})) diff --git a/src/status_im/contexts/chat/messenger/messages/content/lightbox/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/lightbox/view.cljs new file mode 100644 index 00000000000..ef6c1f24016 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/lightbox/view.cljs @@ -0,0 +1,40 @@ +(ns status-im.contexts.chat.messenger.messages.content.lightbox.view + (:require + [oops.core :as oops] + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.lightbox.constants :as constants] + [status-im.contexts.chat.messenger.messages.content.lightbox.style :as style] + [status-im.contexts.chat.messenger.messages.content.text.view :as message-view] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.url :as url])) + +(defn bottom-text-for-lightbox + [_] + (let [text-height (reagent/atom 0)] + (fn [{:keys [content chat-id] :as _message}] + (let [expandable-text? (> @text-height (* constants/line-height 2))] + [message-view/render-parsed-text + {:content content + :chat-id chat-id + :style-override (style/bottom-text expandable-text?) + :on-layout #(reset! text-height (oops/oget % "nativeEvent.layout.height"))}])))) + +(defn drawer + [images index] + (let [{:keys [image]} (nth images index) + uri (url/replace-port image (rf/sub [:mediaserver/port]))] + [quo/action-drawer + [[{:icon :i/save + :accessibility-label :save-image + :label (i18n/label :t/save-image-library) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:lightbox/save-image-to-gallery + uri + #(rf/dispatch [:toasts/upsert + {:id :random-id + :type :positive + :text (i18n/label :t/photo-saved)}])]))}]]])) diff --git a/src/status_im/contexts/chat/messenger/messages/content/link_preview/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/link_preview/view.cljs new file mode 100644 index 00000000000..74e98feccc4 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/link_preview/view.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.chat.messenger.messages.content.link-preview.view + (:require + [quo.core :as quo] + [utils.re-frame :as rf])) + +(defn nearly-square? + [{:keys [width height]}] + (if (or (zero? width) (zero? height)) + false + (let [ratio (/ (max width height) + (min width height))] + (< (Math/abs (dec ratio)) 0.1)))) + +(defn view + [{:keys [chat-id message-id]}] + (let [previews (rf/sub [:chats/message-link-previews chat-id message-id])] + (when (seq previews) + [:<> + (for [{:keys [url title description thumbnail hostname favicon]} previews] + ^{:key url} + [quo/link-preview + {:title title + :description description + :link hostname + :logo (:url favicon) + :thumbnail (:url thumbnail) + :thumbnail-size (when (nearly-square? thumbnail) :large) + :container-style {:margin-top 8}}])]))) diff --git a/src/status_im/contexts/chat/messenger/messages/content/link_preview/view_test.cljs b/src/status_im/contexts/chat/messenger/messages/content/link_preview/view_test.cljs new file mode 100644 index 00000000000..1c204dfe81f --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/link_preview/view_test.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.chat.messenger.messages.content.link-preview.view-test + (:require + [cljs.test :refer [are deftest is]] + [status-im.contexts.chat.messenger.messages.content.link-preview.view :as view])) + +(deftest nearly-square?-test + (are [pred width height] (is (pred (view/nearly-square? {:width width :height height}))) + false? 0 0 + true? 1 1 + false? 100 89 + false? 100 90 + true? 100 91 + true? 100 92 + true? 100 101 + true? 100 109 + false? 100 110 + false? 100 111)) diff --git a/src/status_im/contexts/chat/messenger/messages/content/pin/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/pin/style.cljs new file mode 100644 index 00000000000..5fce92ffe3f --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/pin/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.chat.messenger.messages.content.pin.style + (:require + [quo.foundations.colors :as colors])) + +(def pin-indicator-container + {:padding-left 42 + :margin-bottom 2 + :align-items :center + :flex-direction :row}) + +(def pin-author-text + {:color colors/primary-50 + :margin-left 2}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/pin/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/pin/view.cljs new file mode 100644 index 00000000000..603cebc4bcd --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/pin/view.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.chat.messenger.messages.content.pin.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.contexts.chat.messenger.composer.reply.view :as reply] + [status-im.contexts.chat.messenger.messages.content.pin.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn pinned-by-view + [pinned-by] + (let [{:keys [public-key]} (rf/sub [:multiaccount/contact]) + [primary-name _] (rf/sub [:contacts/contact-two-names-by-identity pinned-by]) + author-name (if (= pinned-by public-key) + (i18n/label :t/You) + primary-name)] + [rn/view + {:style style/pin-indicator-container + :accessibility-label :pinned-by} + [quo/icon :i/pin + {:color colors/primary-50 + :size 16}] + [quo/text + {:size :label + :weight :medium + :style style/pin-author-text} + author-name]])) + +(defn pinned-message + [{:keys [from quoted-message timestamp-str]}] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from]) + one-to-one-chat? (rf/sub [:chats/current-chat-one-to-one?]) + current-chat-color (rf/sub [:chats/current-chat-color]) + contact-customization-color (rf/sub [:contacts/contact-customization-color-by-address from])] + [quo/system-message + {:type :pinned + :pinned-by primary-name + :customization-color (if one-to-one-chat? + contact-customization-color + current-chat-color) + :child [reply/quoted-message quoted-message false true] + :timestamp timestamp-str}])) diff --git a/src/status_im/contexts/chat/messenger/messages/content/reactions/events.cljs b/src/status_im/contexts/chat/messenger/messages/content/reactions/events.cljs new file mode 100644 index 00000000000..3368fe391c9 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/reactions/events.cljs @@ -0,0 +1,106 @@ +(ns status-im.contexts.chat.messenger.messages.content.reactions.events + (:require [clojure.set :as set] + [status-im.constants :as constants] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn update-reaction + [acc retracted chat-id message-id emoji-id emoji-reaction-id reaction] + ;; NOTE: For a better performance, better to not keep in db all retracted reactions + ;; retraction will always come after the reaction so there shouldn't be a conflict + (if retracted + (update-in acc [chat-id message-id emoji-id] dissoc emoji-reaction-id) + (assoc-in acc [chat-id message-id emoji-id emoji-reaction-id] reaction))) + +(defn process-reactions + [_] + (fn [reactions new-reactions] + ;; NOTE: handling own reaction in subscription could be expensive, + ;; for better performance we can here separate own reaction into 2 maps + (reduce + (fn [acc + {:keys [chat-id message-id emoji-id emoji-reaction-id retracted] + :as reaction}] + (update-reaction acc retracted chat-id message-id emoji-id emoji-reaction-id reaction)) + reactions + new-reactions))) + +(defn- earlier-than-deleted-at? + [{:keys [db]} {:keys [chat-id clock-value]}] + (let [{:keys [deleted-at-clock-value]} + (get-in db [:chats chat-id])] + (>= deleted-at-clock-value clock-value))) + +(rf/defn receive-signal + [{:keys [db] :as cofx} reactions] + (let [reactions (filter (partial earlier-than-deleted-at? cofx) reactions)] + {:db (update db :reactions (process-reactions (:chats db)) reactions)})) + +(defn <-rpc + [message] + (-> message + (dissoc :chat_id) + (set/rename-keys {:messageId :message-id + :localChatId :chat-id + :emojiId :emoji-id + :messageType :message-type + :id :emoji-reaction-id}))) + +(rf/reg-event-fx :reactions/load-more + (fn [{:keys [db]} [cursor chat-id]] + (when-let [session-id (get-in db [:pagination-info chat-id :messages-initialized?])] + {:json-rpc/call [{:method "wakuext_emojiReactionsByChatID" + :params [chat-id cursor constants/default-number-of-messages] + :on-success #(rf/dispatch [:reactions/loaded chat-id session-id (map <-rpc %)]) + :on-error #(log/error "failed loading reactions" chat-id %)}]}))) + +(rf/reg-event-fx :reactions/loaded + (fn [{db :db} [chat-id session-id reactions]] + (when-not (and (get-in db [:pagination-info chat-id :messages-initialized?]) + (not= session-id + (get-in db [:pagination-info chat-id :messages-initialized?]))) + (let [reactions-w-chat-id (map #(assoc % :chat-id chat-id) reactions)] + {:db (update db :reactions (process-reactions (:chats db)) reactions-w-chat-id)})))) + +(defn- format-authors-response + [response] + (->> response + (map (fn [item] + {:compressed-key (:compressedKey item) + :emoji-id (:emojiId item) + :from (:from item)})) + (group-by :emoji-id))) + +(rf/reg-event-fx :reactions/get-authors-by-message-id + (fn [{:keys [db]} [{:keys [message-id on-success]}]] + {:db (dissoc db :reactions/authors) + :json-rpc/call [{:method "wakuext_emojiReactionsByChatIDMessageID" + :params [(:current-chat-id db) message-id] + :on-error #(log/error "failed to fetch emoji reaction by message-id: " + {:message-id message-id :error %}) + :on-success #(when on-success + (on-success (format-authors-response %)))}]})) + +(rf/reg-event-fx :reactions/save-authors + (fn [{:keys [db]} [reaction-authors]] + {:db (assoc db :reactions/authors reaction-authors)})) + +(rf/reg-event-fx :reactions/clear-authors + (fn [{:keys [db]}] + {:db (dissoc db :reactions/authors)})) + +(rf/reg-event-fx :reactions/send-emoji-reaction + (fn [{{:keys [current-chat-id]} :db} [{:keys [message-id emoji-id]}]] + {:json-rpc/call [{:method "wakuext_sendEmojiReaction" + :params [current-chat-id message-id emoji-id] + :js-response true + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(log/error "failed to send a reaction" %)}]})) + +(rf/reg-event-fx :reactions/send-emoji-reaction-retraction + (fn [_ [emoji-reaction-id]] + {:json-rpc/call [{:method "wakuext_sendEmojiReactionRetraction" + :params [emoji-reaction-id] + :js-response true + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(log/error "failed to send a reaction retraction" %)}]})) diff --git a/src/status_im/contexts/chat/messenger/messages/content/reactions/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/reactions/view.cljs new file mode 100644 index 00000000000..157ebf86f93 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/reactions/view.cljs @@ -0,0 +1,76 @@ +(ns status-im.contexts.chat.messenger.messages.content.reactions.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.drawers.view :as drawers] + [utils.re-frame :as rf])) + +(defn- on-press + [{:keys [own message-id emoji-id emoji-reaction-id]}] + (if own + (rf/dispatch [:reactions/send-emoji-reaction-retraction emoji-reaction-id]) + (rf/dispatch [:reactions/send-emoji-reaction + {:message-id message-id + :emoji-id emoji-id}]))) + +(defn- on-long-press + [{:keys [message-id emoji-id user-message-content-render-fn reactions-order theme]}] + (rf/dispatch + [:reactions/get-authors-by-message-id + {:message-id message-id + :on-success (fn [response] + (rf/dispatch [:reactions/save-authors + {:reaction-authors-list response + :selected-reaction emoji-id}]) + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:show-bottom-sheet + {:on-close #(rf/dispatch + [:reactions/clear-authors]) + :content (fn [] + [drawers/reaction-authors + {:reactions-order reactions-order + :theme theme}]) + :selected-item (fn [] [user-message-content-render-fn + {:hide-reactions? true}]) + :padding-bottom-override 0}]))}])) + +(defn- on-press-add + [{:keys [chat-id message-id user-message-content-render-fn]}] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] [drawers/reactions + {:chat-id chat-id + :message-id message-id}]) + :selected-item (fn [] [user-message-content-render-fn])}])) + +(defn- add-emoji-key + [reaction] + (assoc reaction + :emoji + (get constants/reactions (:emoji-id reaction)))) + +(defn message-reactions-row + [{:keys [message-id chat-id pinned-by hide-new-reaction-button?]} user-message-content-render-fn] + (let [theme (quo.context/use-theme) + reactions (rf/sub [:chats/message-reactions message-id chat-id])] + (when (seq reactions) + [quo/react + {:container-style {:margin-left 44 + :margin-top 8} + :reactions (map add-emoji-key reactions) + :hide-new-reaction-button? hide-new-reaction-button? + :use-case (when pinned-by :pinned) + :on-press #(on-press (assoc % :message-id message-id)) + :on-long-press #(on-long-press (assoc % + :message-id message-id + :theme theme + :reactions-order (map :emoji-id + reactions) + :user-message-content-render-fn + user-message-content-render-fn)) + :on-press-add #(on-press-add {:chat-id chat-id + :message-id message-id + :user-message-content-render-fn + user-message-content-render-fn})}]))) diff --git a/src/status_im/contexts/chat/messenger/messages/content/status/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/status/style.cljs new file mode 100644 index 00000000000..c83ed4d6ac7 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/status/style.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.chat.messenger.messages.content.status.style + (:require + [quo.foundations.colors :as colors])) + +(def status-container + {:flex-direction :row + :align-items :center}) + +(defn message-status-text + [theme] + {:margin-left 4 + :color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/status/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/status/view.cljs new file mode 100644 index 00000000000..b0f0f1e7883 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/status/view.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.chat.messenger.messages.content.status.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.contexts.chat.messenger.messages.content.status.style :as style] + [utils.i18n :as i18n])) + +(defn status + [outgoing-status] + (let [theme (quo.context/use-theme)] + [rn/view + {:accessibility-label :message-status + :style style/status-container} + [quo/icon + (if (= outgoing-status :delivered) + :i/delivered + :i/sent) + {:size 12 + :color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}] + [quo/text + {:size :label + :style (style/message-status-text theme)} + (if (= outgoing-status :delivered) + (i18n/label :t/delivered) + (i18n/label :t/sent))]])) diff --git a/src/status_im/contexts/chat/messenger/messages/content/status_link_preview/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/status_link_preview/view.cljs new file mode 100644 index 00000000000..ce4310ec8fd --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/status_link_preview/view.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.chat.messenger.messages.content.status-link-preview.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [utils.re-frame :as rf])) + +(defn view + [{:keys [chat-id message-id]}] + (let [status-link-previews (rf/sub [:chats/message-status-link-previews chat-id message-id]) + link-previews? (rf/sub [:chats/message-link-previews? chat-id message-id])] + (when (seq status-link-previews) + [rn/view {:style {:margin-top (when link-previews? 8)}} + (for [{:keys [url community]} status-link-previews] + (when (:display-name community) + (let [{community-description :description + community-icon :icon + community-banner :banner + community-name :display-name + members-count :members-count} community] + ^{:key url} + [quo/internal-link-card + {:type :community + :size :message + :description community-description + :members-count members-count + :title community-name + :banner (:url community-banner) + :icon (:url community-icon) + :on-press #(rf/dispatch [:universal-links/handle-url url])}])))]))) diff --git a/src/status_im/contexts/chat/messenger/messages/content/sticker_message/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/sticker_message/view.cljs new file mode 100644 index 00000000000..4fddbdedae2 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/sticker_message/view.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.chat.messenger.messages.content.sticker-message.view + (:require [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn view + [{:keys [url]}] + [rn/view {:style {:margin-top 6 :margin-bottom 4}} + [fast-image/fast-image + {:style {:width 120 :height 120} + :source {:uri (str url "&download=true")}}]]) diff --git a/src/status_im/contexts/chat/messenger/messages/content/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/style.cljs new file mode 100644 index 00000000000..82e7f982a18 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/style.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.chat.messenger.messages.content.style + (:require + [quo.foundations.colors :as colors])) + +(def ^:private message-padding-scaling-ratio 4.5) + +(defn message-container + [{:keys [in-pinned-view? pinned-by mentioned last-in-group? system-message?]}] + (cond-> {:border-radius 16 + :margin-horizontal 8} + + (and (not in-pinned-view?) (or mentioned pinned-by)) + (assoc :background-color colors/primary-50-opa-5 :margin-bottom 4) + + (and (not in-pinned-view?) (not system-message?) (or mentioned pinned-by last-in-group?)) + (assoc :margin-top 4))) + +(defn user-message-content + [{:keys [outgoing outgoing-status six-reactions? window-scale small-screen?]}] + {:border-radius 16 + :padding-horizontal 8 + :padding-top 4 + :padding-bottom (if (or small-screen? + (and + (> 3 window-scale) + six-reactions?)) + (* message-padding-scaling-ratio window-scale) + 4) + :opacity (if (and outgoing (= outgoing-status :sending)) + 0.5 + 1)}) + +(def drawer-message-container + {:padding-top 4 + :padding-bottom 4}) + +(def group-member-mention + {:flex-direction :row + :align-items :center}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/system/text/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/system/text/view.cljs new file mode 100644 index 00000000000..902e5d87986 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/system/text/view.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.chat.messenger.messages.content.system.text.view + (:require + [legacy.status-im.ui.screens.chat.message.legacy-view :as old-message] + [react-native.core :as rn])) + +(defn text-content + [message-data] + [rn/view {:accessibility-label :chat-item :padding-horizontal 12 :padding-vertical 3} + [old-message/render-parsed-text message-data]]) diff --git a/src/status_im/contexts/chat/messenger/messages/content/text/style.cljs b/src/status_im/contexts/chat/messenger/messages/content/text/style.cljs new file mode 100644 index 00000000000..209c9aa28ae --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/text/style.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.chat.messenger.messages.content.text.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def block + {:border-radius 6 + :padding-horizontal 3}) + +(def quote + {:border-left-width 1 + :padding-left 10 + :border-left-color colors/neutral-40}) + +(defn mention-tag-wrapper + [first-child-mention] + {:flex-direction :row + :align-items :center + :height (if platform/ios? 22 21) + :background-color colors/primary-50-opa-10 + :padding-horizontal 3 + :border-radius 6 + :transform [{:translateY (if platform/ios? (if first-child-mention 4.5 3) 4.5)}]}) + +(defn mention-tag-text + [theme] + {:color (colors/theme-colors colors/primary-50 + colors/primary-60 + theme) + :selection-color :transparent + :suppress-highlighting true}) + +(defn code + [theme] + {:background-color (colors/theme-colors colors/neutral-5 colors/white-opa-5 theme) + :border-width 1 + :border-color (colors/theme-colors colors/neutral-20 colors/white-opa-20 theme)}) diff --git a/src/status_im/contexts/chat/messenger/messages/content/text/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/text/view.cljs new file mode 100644 index 00000000000..6a673fb17c6 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/text/view.cljs @@ -0,0 +1,172 @@ +(ns status-im.contexts.chat.messenger.messages.content.text.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.contexts.chat.messenger.messages.content.link-preview.view :as link-preview] + [status-im.contexts.chat.messenger.messages.content.status-link-preview.view :as status-link-preview] + [status-im.contexts.chat.messenger.messages.content.text.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn render-inline + [units {:keys [type literal destination]} chat-id style-override first-child-mention theme] + (let [show-as-plain-text? (seq style-override)] + (case (keyword type) + :code + (conj units + [quo/text + {:style (if show-as-plain-text? + {:color colors/white} + (merge style/block (style/code theme))) + :weight :code} literal]) + + :emph + (conj units + [quo/text + {:style {:font-style :italic + :color (when show-as-plain-text? colors/white)}} literal]) + + :strong + (conj units + [quo/text + (cond-> {:weight :bold} + show-as-plain-text? (assoc :style {:color colors/white})) literal]) + + :strong-emph + (conj units + [quo/text + {:weight :bold + :style {:font-style :italic + :color (when show-as-plain-text? colors/white)}} literal]) + + :del + (conj units + [quo/text + {:style {:text-decoration-line :line-through + :color (when show-as-plain-text? colors/white)}} literal]) + + :link + (conj units + [quo/text + {:style {:color (colors/theme-colors colors/primary-50 colors/primary-60 theme)} + :on-press #(rf/dispatch [:browser.ui/message-link-pressed destination])} + destination]) + + :mention + (conj + units + [rn/pressable + {:on-press #(rf/dispatch [:chat.ui/show-profile literal]) + :style (style/mention-tag-wrapper first-child-mention)} + [quo/text + {:weight :medium + :style (style/mention-tag-text theme) + :size :paragraph-1} + (rf/sub [:messages/resolve-mention literal])]]) + + :edited + (conj units + [quo/text + {:weight :medium + :style {:font-size 11 ; Font-size must be used instead of props or the + ; styles will clash with original message text + :color (colors/theme-colors colors/neutral-40 + colors/neutral-50 + theme)}} + literal]) + :status-tag + (let [community-id (rf/sub [:community-id-by-chat-id chat-id])] + (conj units + [rn/text + (when community-id + {:style {:color :blue + :text-decoration-line :underline} + :on-press #(rf/dispatch [:communities/status-tag-pressed community-id literal])}) + "#" + literal])) + + (conj units literal)))) + +(defn first-child-mention + [children] + (and (> (count children) 0) + (= (keyword (:type (second children))) :mention) + (empty? (get-in children [0 :literal])))) + +(defn render-block + [blocks {:keys [type literal children]} chat-id style-override theme] + (let [mention-first (first-child-mention children)] + (case (keyword type) + :paragraph + (conj blocks + [rn/view + (reduce + (fn [acc e] + (render-inline acc e chat-id style-override mention-first theme)) + [quo/text + {:size :paragraph-1 + :key (rand-int 1000000) ;; https://github.com/status-im/status-mobile/pull/19203 + :style {:margin-bottom (if mention-first (if platform/ios? 4 0) 2) + :margin-top (if mention-first (if platform/ios? -4 0) -1) + :color (when (seq style-override) colors/white) + :line-height 22.75}}] + children)]) + + :edited-block + (conj blocks + (reduce + (fn [acc e] + (render-inline acc e chat-id style-override first-child-mention theme)) + [quo/text {:size :paragraph-1}] + children)) + + :blockquote + (conj blocks + [rn/view {:style style/quote} + [quo/text literal]]) + + :codeblock + (conj blocks + [rn/view {:style (merge style/block (style/code theme))} + [quo/text (subs literal 0 (dec (count literal)))]]) + blocks))) + +(def edited-tag + {:literal (str "(" (i18n/label :t/edited) ")") + :type :edited}) + +(defn add-edited-tag + [parsed-text] + (let [items-count (count parsed-text) + last-item (get parsed-text (dec items-count))] + (if (= (keyword (:type last-item)) :paragraph) + (update parsed-text + (dec items-count) + (fn [last-literal] + (update last-literal :children into [{:literal " "} edited-tag]))) + (conj parsed-text {:type :edited-block :children [edited-tag]})))) + +(defn render-parsed-text + [{:keys [content chat-id edited-at style-override on-layout]}] + ^{:key (:parsed-text content)} + (let [theme (quo.context/use-theme)] + [rn/view + {:style style-override + :on-layout on-layout + :accessibility-label :message-text-content} + (reduce (fn [acc e] + (render-block acc e chat-id style-override theme)) + [:<>] + (cond-> (:parsed-text content) + edited-at + add-edited-tag))])) + +(defn text-content + [message-data] + [:<> + [render-parsed-text message-data] + [link-preview/view message-data] + [status-link-preview/view message-data]]) diff --git a/src/status_im/contexts/chat/messenger/messages/content/unknown/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/unknown/view.cljs new file mode 100644 index 00000000000..7e1b7ff3263 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/unknown/view.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.chat.messenger.messages.content.unknown.view + (:require + [react-native.core :as rn])) + +(defn unknown-content + [{:keys [content-type content]}] + [rn/text + (if (seq (:text content)) + (:text content) + (str "Unhandled content-type " content-type))]) diff --git a/src/status_im/contexts/chat/messenger/messages/content/view.cljs b/src/status_im/contexts/chat/messenger/messages/content/view.cljs new file mode 100644 index 00000000000..5ea35f682be --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/content/view.cljs @@ -0,0 +1,404 @@ +(ns status-im.contexts.chat.messenger.messages.content.view + (:require + [clojure.string :as string] + [legacy.status-im.ui.screens.chat.message.legacy-view :as old-message] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.gesture :as gesture] + [react-native.platform :as platform] + [reagent.core :as reagent] + [status-im.common.not-implemented :as not-implemented] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.composer.reply.view :as reply] + [status-im.contexts.chat.messenger.messages.avatar.view :as avatar] + [status-im.contexts.chat.messenger.messages.content.album.view :as album] + [status-im.contexts.chat.messenger.messages.content.audio.view :as audio] + [status-im.contexts.chat.messenger.messages.content.deleted.view :as content.deleted] + [status-im.contexts.chat.messenger.messages.content.emoji-message.view :as emoji-message] + [status-im.contexts.chat.messenger.messages.content.image.view :as image] + [status-im.contexts.chat.messenger.messages.content.pin.view :as pin] + [status-im.contexts.chat.messenger.messages.content.reactions.view :as reactions] + [status-im.contexts.chat.messenger.messages.content.status.view :as status] + [status-im.contexts.chat.messenger.messages.content.sticker-message.view :as sticker-message] + [status-im.contexts.chat.messenger.messages.content.style :as style] + [status-im.contexts.chat.messenger.messages.content.system.text.view :as system.text] + [status-im.contexts.chat.messenger.messages.content.text.view :as content.text] + [status-im.contexts.chat.messenger.messages.content.unknown.view :as content.unknown] + [status-im.contexts.chat.messenger.messages.drawers.view :as drawers] + [utils.address :as address] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def delivery-state-showing-time-ms 3000) + +(defn- bridge-message-user-name + [user-name] + (when (string? user-name) + (-> user-name + (string/replace "" "") + (string/replace "" "")))) + +(defn avatar-container + [{:keys [content last-in-group? pinned-by quoted-message from bridge-message]} + hide-reactions? in-reaction-or-action-menu? show-user-info?] + (cond + (:user-avatar bridge-message) + [fast-image/fast-image + {:source {:uri (:user-avatar bridge-message)} + :fallback-content [quo/user-avatar + {:full-name (bridge-message-user-name (:user-name bridge-message)) + :online? false + :status-indicator? false + :size :small}] + :style {:width 32 + :margin-top 4 + :border-radius 16 + :height 32}}] + + (or (and (seq (:response-to content)) + quoted-message) + last-in-group? + show-user-info? + pinned-by + hide-reactions? + in-reaction-or-action-menu?) + [avatar/avatar + {:public-key from + :size :small}] + + :else + [rn/view {:padding-top 4 :width 32}])) + +(defn author + [{:keys [bridge-message + content + compressed-key + last-in-group? + pinned-by + quoted-message + from + timestamp]} + hide-reactions? + in-reaction-or-action-menu? + show-user-info?] + (when (or (and (seq (:response-to content)) quoted-message) + (seq bridge-message) + last-in-group? + pinned-by + show-user-info? + hide-reactions? + in-reaction-or-action-menu?) + (let [{:keys [user-name bridge-name]} bridge-message + [primary-name secondary-name] (when-not bridge-message + (rf/sub [:contacts/contact-two-names-by-identity from])) + {:keys [ens-verified added?]} (when-not bridge-message + (rf/sub [:contacts/contact-by-address from])) + user-name (bridge-message-user-name user-name)] + [quo/author + {:primary-name (or user-name primary-name) + :secondary-name secondary-name + :short-chat-key (if bridge-message + (i18n/label :t/bridge-from {:bridge-name bridge-name}) + (address/get-shortened-compressed-key (or compressed-key from))) + :time-str (datetime/timestamp->time timestamp) + :contact? (when-not bridge-message added?) + :verified? (when-not bridge-message ens-verified)}]))) + +(defn system-message-contact-request + [{:keys [chat-id timestamp-str from]} type] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity chat-id]) + {:keys [images] + contact-customization-color + :customization-color} (rf/sub [:contacts/contact-by-address chat-id]) + photo-path (when (seq images) (rf/sub [:chats/photo-path chat-id])) + public-key (rf/sub [:profile/public-key])] + [quo/system-message + {:type type + :timestamp timestamp-str + :display-name primary-name + :customization-color contact-customization-color + :photo-path photo-path + :incoming? (not= public-key from)}])) + +(defn group-member + [literal] + (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity literal]) + photo-path (rf/sub [:chats/photo-path literal])] + [rn/view {:style style/group-member-mention} + [quo/user-avatar + {:full-name primary-name + :profile-picture photo-path + :status-indicator? false + :size :xxxs}] + [quo/text + {:weight :semi-bold + :style {:margin-left 4}} + primary-name]])) + +(defn resolve-group-system-message + [{:keys [children]}] + (reduce (fn [acc {:keys [type literal]}] + (if (= type "mention") + (conj acc [group-member literal]) + (conj acc + [rn/view + [quo/text literal]]))) + [:<>] + children)) + + +(defn render-parsed-text + [content] + (reduce (fn [acc e] + (conj acc (resolve-group-system-message e))) + [rn/view + {:style {:flex-direction :row + :flex-wrap :wrap}}] + (:parsed-text content))) + +(defn group-system-message + [{:keys [chat-id timestamp-str content]} type] + (let [{:keys [customization-color]} (rf/sub [:contacts/contact-by-address chat-id])] + [quo/system-message + {:type type + :timestamp timestamp-str + :customization-color customization-color + :child (render-parsed-text content)}])) + +(defn system-message-content + [{:keys [content-type quoted-message] :as message-data}] + + (if quoted-message + [pin/pinned-message message-data] + (condp = content-type + + constants/content-type-system-text + [group-system-message message-data :group] + + constants/content-type-system-pinned-message + [system.text/text-content message-data] + + constants/content-type-community + [not-implemented/not-implemented + [old-message/community message-data]] + + constants/content-type-system-message-mutual-event-accepted + [system-message-contact-request message-data :added] + + constants/content-type-system-message-mutual-event-removed + [system-message-contact-request message-data :removed] + + constants/content-type-system-message-mutual-event-sent + [system-message-contact-request message-data :contact-request]))) + +(declare on-long-press) + +(defn- bridge-message-content + [content] + [quo/text + {:size :paragraph-1 + :style {:line-height 22.75}} + content]) + +(defn user-message-content + [] + (let [show-delivery-state? (reagent/atom false)] + (fn [{:keys [message-data context keyboard-shown? hide-reactions? + in-reaction-or-action-menu? show-user-info?]}] + (let [theme (quo.context/use-theme) + {:keys [content-type quoted-message content outgoing outgoing-status pinned-by + pinned bridge-message last-in-group? message-id + chat-id]} message-data + first-image (first (:album message-data)) + outgoing-status (if (= content-type + constants/content-type-album) + (:outgoing-status first-image) + outgoing-status) + outgoing (if (= content-type + constants/content-type-album) + (:outgoing first-image) + outgoing) + context (assoc context + :on-long-press + #(on-long-press message-data + context + keyboard-shown?)) + response-to (:response-to content) + height (rf/sub [:dimensions/window-height]) + {window-width :width + window-scale :scale} (rn/get-window) + message-container-data {:window-width window-width + :padding-right 20 + :padding-left 20 + :avatar-container-width 32 + :message-margin-left 8} + reactions (rf/sub [:chats/message-reactions message-id + chat-id]) + six-reactions? (-> reactions + count + (= 6)) + content-type (if (and + (= content-type + constants/content-type-bridge-message) + (seq (:parsed-text content))) + constants/content-type-text + content-type)] + [rn/touchable-highlight + {:accessibility-label (if (and outgoing (= outgoing-status :sending)) + :message-sending + :message-sent) + :underlay-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme) + :style (style/user-message-content + {:first-in-group? (:first-in-group? message-data) + :outgoing outgoing + :outgoing-status outgoing-status + :small-screen? rn/small-screen? + :window-scale window-scale + :six-reactions? six-reactions? + :in-reaction-or-action-menu? in-reaction-or-action-menu?}) + :on-press (fn [] + (if (and platform/ios? keyboard-shown?) + (do + (rf/dispatch [:chat.ui/set-input-focused false]) + (rn/dismiss-keyboard!)) + (when (and outgoing + (not= outgoing-status :sending) + (not @show-delivery-state?)) + (reset! show-delivery-state? true) + (js/setTimeout #(reset! show-delivery-state? false) + delivery-state-showing-time-ms)))) + :on-long-press (when-not (:disable-message-long-press? context) + #(on-long-press message-data context keyboard-shown?))} + [:<> + (when pinned-by + [pin/pinned-by-view pinned-by]) + (when (and (seq response-to) quoted-message) + [reply/quoted-message quoted-message]) + [rn/view + {:style {:padding-horizontal 4 + :flex-direction :row}} + [avatar-container message-data hide-reactions? in-reaction-or-action-menu? show-user-info?] + (into + (if hide-reactions? + [gesture/scroll-view] + [rn/view]) + [{:style {:margin-left 8 + :flex 1 + :max-height (when hide-reactions? (* 0.4 height))}} + [author message-data hide-reactions? in-reaction-or-action-menu? show-user-info?] + (condp = content-type + + constants/content-type-text + [content.text/text-content message-data context] + + constants/content-type-bridge-message + [bridge-message-content (:content bridge-message)] + + constants/content-type-contact-request + [content.text/text-content message-data context] + + constants/content-type-emoji + [emoji-message/view + {:content content + :last-in-group? last-in-group? + :pinned pinned + :in-pinned-view? (:in-pinned-view? context)}] + + constants/content-type-sticker + [sticker-message/view {:url (-> message-data :content :sticker :url)}] + + constants/content-type-audio + [audio/audio-message message-data context] + + constants/content-type-image + [image/image-message 0 message-data context 0 message-container-data] + + constants/content-type-album + [album/album-message message-data context on-long-press message-container-data] + + constants/content-type-gap + [rn/view] + + [not-implemented/not-implemented + [content.unknown/unknown-content message-data]]) + + (when @show-delivery-state? + [status/status outgoing-status])])] + (when-not hide-reactions? + [reactions/message-reactions-row (assoc message-data :hide-new-reaction-button? true) + (fn [override-opts] + [rn/view + {:pointer-events :none + :style style/drawer-message-container} + [user-message-content + (merge {:theme theme + :message-data message-data + :context context + :in-reaction-or-action-menu? true + :keyboard-shown? keyboard-shown?} + override-opts)]])])]])))) + +(defn on-long-press + [{:keys [deleted? deleted-for-me?] :as message-data} context keyboard-shown?] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch + [:show-bottom-sheet + {:content (drawers/reactions-and-actions message-data context) + :border-radius 16 + :selected-item (if (or deleted? deleted-for-me?) + (fn [] [content.deleted/deleted-message message-data]) + (fn [] + [rn/view + {:pointer-events :none + :style style/drawer-message-container} + [user-message-content + {:message-data message-data + :context context + :keyboard-shown? keyboard-shown? + :in-reaction-or-action-menu? true + :show-user-info? false}]]))}])) + +(defn check-if-system-message? + [content-type] + (#{constants/content-type-system-text + constants/content-type-community + constants/content-type-system-message-mutual-event-accepted + constants/content-type-system-message-mutual-event-removed + constants/content-type-system-message-mutual-event-sent + constants/content-type-system-pinned-message} + content-type)) + +(defn message + [{:keys [pinned-by mentioned content-type last-in-group? deleted? deleted-for-me?] + :as message-data} {:keys [in-pinned-view?] :as context} keyboard-shown?] + (let [system-message? (check-if-system-message? content-type)] + [rn/view + {:style (style/message-container + {:in-pinned-view? in-pinned-view? + :pinned-by pinned-by + :mentioned mentioned + :last-in-group? last-in-group? + :system-message? system-message?}) + :accessibility-label :chat-item} + (cond + system-message? + [system-message-content message-data] + + (or deleted? deleted-for-me?) + [content.deleted/deleted-message + (assoc message-data + :on-long-press + #(on-long-press message-data + context + keyboard-shown?)) + context] + + :else + [user-message-content + {:message-data message-data + :context context + :keyboard-shown? keyboard-shown?}])])) diff --git a/src/status_im/contexts/chat/messenger/messages/delete_message/events.cljs b/src/status_im/contexts/chat/messenger/messages/delete_message/events.cljs new file mode 100644 index 00000000000..f2b6e4ab15a --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/delete_message/events.cljs @@ -0,0 +1,216 @@ +(ns status-im.contexts.chat.messenger.messages.delete-message.events + (:require + [status-im.contexts.chat.messenger.messages.list.events :as message-list] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- update-db-clear-undo-timer + [db chat-id message-id] + (when (get-in db [:messages chat-id message-id]) + (update-in db + [:messages chat-id message-id] + dissoc + :deleted-undoable-till))) + +(defn- update-db-delete-locally + "Delete message in re-frame db and set the undo timelimit" + [db chat-id message-id undo-time-limit-ms deleted-by] + (when (get-in db [:messages chat-id message-id]) + (update-in db + [:messages chat-id message-id] + assoc + :deleted? true + :deleted-by deleted-by + :deleted-undoable-till (+ (datetime/timestamp) undo-time-limit-ms)))) + +(defn- update-db-undo-locally + "Restore deleted message if called within timelimit" + [db chat-id message-id] + (let [{:keys [deleted? deleted-undoable-till]} + (get-in db [:messages chat-id message-id])] + (if (and deleted? + (> deleted-undoable-till (datetime/timestamp))) + (update-in db + [:messages chat-id message-id] + dissoc + :deleted? + :deleted-by + :deleted-undoable-till) + (update-db-clear-undo-timer db chat-id message-id)))) + +(defn- should-and-able-to-unpin-to-be-deleted-message + "check + 1. if the to-be deleted message is pinned + 2. if user has the permission to unpin message in current chat" + [db {:keys [chat-id message-id]}] + (let [{:keys [group-chat chat-id public? community-id + admins]} (get-in db [:chats chat-id]) + community (get-in db [:communities community-id]) + community-admin? (get community :admin false) + pub-key (get-in db [:profile/profile :public-key]) + group-admin? (get admins pub-key) + message-pin-enabled (and (not public?) + (or (not group-chat) + (and group-chat + (or group-admin? + community-admin?)))) + pinned (get-in db + [:pin-messages chat-id + message-id])] + (and pinned message-pin-enabled))) + +(defn toast-undo-delete-message-for-everyone + [] + (rf/dispatch [:chat.ui/undo-all-delete-message]) + (rf/dispatch [:toasts/close + :delete-message-for-everyone])) + +(defn delete + "Delete message now locally and broadcast after undo time limit timeout" + [{:keys [db]} [{:keys [chat-id message-id]} undo-time-limit-ms]] + (when-let [message (get-in db [:messages chat-id message-id])] + ;; all delete message toast are the same toast with id :delete-message-for-everyone new delete + ;; operation will reset prev pending deletes' undo timelimit undo will undo all pending deletes + ;; all pending deletes are stored in toast + (let [unpin? (should-and-able-to-unpin-to-be-deleted-message + db + {:chat-id chat-id :message-id message-id}) + + pub-key (get-in db [:profile/profile :public-key]) + + ;; compute deleted by + message-from (get message :from) + deleted-by (when (not= pub-key message-from) pub-key) + + existing-undo-toast (get-in db [:toasts :toasts :delete-message-for-everyone]) + toast-count (inc (get existing-undo-toast :message-deleted-for-everyone-count 0)) + existing-undos + (-> existing-undo-toast + (get :message-deleted-for-everyone-undos []) + (conj {:message-id message-id :chat-id chat-id :should-pin-back? unpin?}))] + (assoc + (message-list/rebuild-message-list + {:db (reduce + ;; sync all pending deletes' undo timelimit, extend to the latest one + (fn [db-acc {:keys [chat-id message-id]}] + (update-db-delete-locally db-acc chat-id message-id undo-time-limit-ms deleted-by)) + db + existing-undos)} + chat-id) + + :dispatch-n + [[:toasts/close :delete-message-for-everyone] + [:toasts/upsert + {:id :delete-message-for-everyone + :type :negative + :message-deleted-for-everyone-count toast-count + :message-deleted-for-everyone-undos existing-undos + :text (i18n/label-pluralize + toast-count + :t/message-deleted-for-everyone-count) + :duration undo-time-limit-ms + :undo-duration (/ undo-time-limit-ms 1000) + :undo-on-press toast-undo-delete-message-for-everyone}] + (when unpin? + [:pin-message/send-pin-message-locally + {:chat-id chat-id :message-id message-id :pinned false}])] + :utils/dispatch-later (mapv (fn [{:keys [chat-id message-id]}] + {:dispatch [:chat.ui/delete-message-and-send + {:chat-id chat-id :message-id message-id}] + :ms undo-time-limit-ms}) + existing-undos))))) + +(rf/reg-event-fx :chat.ui/delete-message delete) + +(defn undo + [{:keys [db]} [{:keys [chat-id message-id should-pin-back?]}]] + (when (get-in db [:messages chat-id message-id]) + (let [effects (message-list/rebuild-message-list + {:db (update-db-undo-locally db chat-id message-id)} + chat-id) + message (get-in effects [:db :messages chat-id message-id])] + (cond-> effects + should-pin-back? + (assoc :dispatch + [:pin-message/send-pin-message-locally (assoc message :pinned true)]))))) + +(rf/reg-event-fx :chat.ui/undo-delete-message undo) + +(defn undo-all + [{:keys [db]}] + (when-let [pending-undos (get-in db + [:toasts :toasts :delete-message-for-everyone + :message-deleted-for-everyone-undos])] + {:dispatch-n (mapv #(vector :chat.ui/undo-delete-message %) pending-undos)})) + +(rf/reg-event-fx :chat.ui/undo-all-delete-message undo-all) + +(defn- check-before-delete-and-send + "make sure message alredy deleted? locally and undo timelimit has passed" + [db chat-id message-id] + (let [message (get-in db [:messages chat-id message-id]) + {:keys [deleted? deleted-undoable-till]} message] + (and deleted? + deleted-undoable-till + (>= (datetime/timestamp) deleted-undoable-till)))) + +(defn delete-and-send + [{:keys [db]} [{:keys [message-id chat-id]} force?]] + (when (get-in db [:messages chat-id message-id]) + (when (or force? (check-before-delete-and-send db chat-id message-id)) + (let [unpin-locally? + ;; this only check against local client data generally msg is already unpinned at delete + ;; locally phase when user has unpin permission + ;; + ;; will be true only if + ;; 1. admin delete an unpinned msg + ;; 2. another admin pin the msg within the undo time limit + ;; 3. msg finally deleted + (should-and-able-to-unpin-to-be-deleted-message db + {:chat-id chat-id + :message-id message-id})] + {:db (update-db-clear-undo-timer db chat-id message-id) + :fx [[:json-rpc/call + [{:method "wakuext_deleteMessageAndSend" + :params [message-id] + :js-response true + :on-error [:chat/delete-and-send-error message-id] + :on-success [:sanitize-messages-and-process-response]}]] + [:dispatch + [:pin-message/send-pin-message + {:chat-id chat-id + :message-id message-id + :pinned false + :remote-only? (not unpin-locally?)}]]]})))) + +(rf/reg-event-fx :chat.ui/delete-message-and-send delete-and-send) + +(defn delete-and-send-error + [_ [message-id error]] + {:fx [[:effects.log/error + ["failed to delete message" + {:message-id message-id + :error error}]]]}) + +(rf/reg-event-fx :chat/delete-and-send-error delete-and-send-error) + +(defn- filter-pending-send-messages + "traverse all messages find not yet synced deleted? messages" + [acc chat-id messages] + (->> messages + (filter (fn [[_ {:keys [deleted? deleted-undoable-till]}]] (and deleted? deleted-undoable-till))) + (map (fn [message] {:chat-id chat-id :message-id (first message)})) + (concat acc))) + +(defn send-all + "Get all deleted messages that not yet synced with status-go and send them" + [{:keys [db] :as cofx}] + (let [pending-send-messages (reduce-kv filter-pending-send-messages [] (:messages db)) + pending-effects (map (fn [message] + (fn [cofx] + (delete-and-send cofx [message true]))) + pending-send-messages)] + (apply rf/merge cofx pending-effects))) + +(rf/reg-event-fx :chat.ui/send-all-deleted-messages send-all) diff --git a/src/status_im/contexts/chat/messenger/messages/delete_message/events_test.cljs b/src/status_im/contexts/chat/messenger/messages/delete_message/events_test.cljs new file mode 100644 index 00000000000..8abde2db95a --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/delete_message/events_test.cljs @@ -0,0 +1,232 @@ +(ns status-im.contexts.chat.messenger.messages.delete-message.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.chat.messenger.messages.delete-message.events :as delete-message] + [utils.datetime :as datetime])) + +(def mid "message-id") +(def cid "chat-id") + +(deftest delete-test + (with-redefs [datetime/timestamp (constantly 1)] + (let [pub-key "0x1" + db {:profile/profile {:public-key pub-key} + :messages {cid {mid {:id mid + :from pub-key + :whisper-timestamp 1}}}} + message {:message-id mid :chat-id cid}] + (testing "delete" + (testing "dispatch right db fx when deleting own message" + (let [effects (delete-message/delete {:db db} [message 1000]) + result-messages (get-in effects [:db :messages] effects) + expected-messages {cid + {mid + {:id mid + :from pub-key + :whisper-timestamp 1 + :deleted? true + :deleted-by nil + :deleted-undoable-till 1001}}}] + (is (match? result-messages + expected-messages)))) + (testing "dispatch right db fx when deleting another user message" + (let [other-pub-key "0x2" + mod-db (assoc-in db [:messages cid mid :from] other-pub-key) + effects (delete-message/delete {:db mod-db} [message 1000]) + result-messages (get-in effects [:db :messages] effects) + expected-messages {cid + {mid + {:id mid + :from other-pub-key + :whisper-timestamp 1 + :deleted? true + :deleted-by pub-key + :deleted-undoable-till 1001}}}] + (is (match? result-messages + expected-messages)))) + (testing "delete with pending deletes" + (let [db (-> db + (update-in [:messages cid "pending-delete-message"] + assoc + :deleted? true + :deleted-undoable-till 0 + :whisper-timestamp 0) + (update-in [:toasts :toasts :delete-message-for-everyone] + assoc + :message-deleted-for-everyone-count 1 + :message-deleted-for-everyone-undos [{:message-id + "pending-delete-message" + :chat-id cid}])) + effects (delete-message/delete {:db db} [message 1000])] + (is (= (get-in effects [:db :messages cid mid :deleted-undoable-till]) + (get-in effects [:db :messages cid "pending-delete-message" :deleted-undoable-till]) + 1001) + "sync all pending delete undo timelimit") + (let [upsert-toast (-> effects :dispatch-n second)] + (is (= (-> upsert-toast last :message-deleted-for-everyone-count) 2) + "+1 pending deletes") + (is + (and + (-> upsert-toast + last + :message-deleted-for-everyone-undos + first + :message-id + (= "pending-delete-message")) + (-> upsert-toast + last + :message-deleted-for-everyone-undos + second + :message-id + (= mid))) + "pending deletes are in order")))) + (testing "return nil if message not in db" + (is (= (delete-message/delete {:db {:messages []}} [message 1000]) nil))))))) + +(deftest undo-delete-test + (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}} + message {:message-id mid :chat-id cid}] + (testing "undo delete" + (testing "undo in time" + (let [db (update-in db + [:messages cid mid] + assoc + :deleted? true + :deleted-undoable-till (+ (datetime/timestamp) 1000)) + effects (delete-message/undo {:db db} [message]) + result-message (get-in effects [:db :messages cid mid])] + (is (match? result-message + {:id mid + :whisper-timestamp 1})))) + (testing "remain deleted when undo after timelimit" + (let [db (update-in db + [:messages cid mid] + assoc + :deleted? true + :deleted-undoable-till (- (datetime/timestamp) 1000)) + effects (delete-message/undo {:db db} [message]) + result-message (get-in effects [:db :messages cid mid])] + (is (match? result-message + {:id mid + :whisper-timestamp 1 + :deleted? true})))) + (testing "return nil if message not in db" + (is (= (delete-message/undo {:db {:messages []}} [message]) nil)))))) + +(deftest undo-all-test + (testing "does nothing when there are no pending todos" + (let [cofx {:db {:toasts + {:toasts + {:delete-message-for-everyone + {:message-deleted-for-everyone-undos nil}}}}} + effects (delete-message/undo-all cofx) + expected nil] + (is (match? effects expected)))) + + (testing "undo all pending undos" + (let [pending-undo {:undo-id 1} + pending-undos [pending-undo] + cofx {:db {:toasts + {:toasts + {:delete-message-for-everyone + {:message-deleted-for-everyone-undos pending-undos}}}}} + effects (delete-message/undo-all cofx) + expected {:dispatch-n [[:chat.ui/undo-delete-message pending-undo]]}] + (is (match? effects expected))))) + +(deftest delete-and-send-test + (let [db {:messages {cid {mid {:id mid :deleted? true :deleted-undoable-till 0}}}} + message {:message-id mid :chat-id cid}] + (testing "delete and send" + (testing "dispatch right rpc call fx" + (let [effects (delete-message/delete-and-send {:db db} [message false]) + expected-db {:messages + {cid + {mid + {:id mid + :deleted? true}}}} + expected-fx [[:json-rpc/call + [{:method "wakuext_deleteMessageAndSend" + :params [mid] + :js-response true + :on-error [:chat/delete-and-send-error mid] + :on-success [:sanitize-messages-and-process-response]}]] + [:dispatch + [:pin-message/send-pin-message + {:chat-id cid + :message-id mid + :pinned false + :remote-only? true}]]]] + (is (match? effects + {:db expected-db + :fx expected-fx})))) + (testing "clean undo timer" + (let [expected-db {:messages {cid {mid {:id mid :deleted? true}}}} + effects (delete-message/delete-and-send + {:db (update-in db + [:messages cid mid :deleted-undoable-till] + (constantly (datetime/timestamp)))} + [message false]) + result-db (:db effects)] + (is (= result-db expected-db)))) + (testing "before deleted locally" + (let [effects (delete-message/delete-and-send + {:db (update-in db [:messages cid mid] dissoc :deleted?)} + [message false])] + (is (-> effects :db nil?) "not delete and send"))) + (testing "before undo timelimit" + (with-redefs [datetime/timestamp (constantly 1)] + (let [effects (delete-message/delete-and-send + {:db (update-in db [:messages cid mid] assoc :deleted-undoable-till 2)} + [message false])] + (is (-> effects :db nil?))))) + (testing "return nil if message not in db" + (is (= (delete-message/delete-and-send {:db {:messages []}} [message false]) + nil)))))) + +(deftest send-all-test + (testing "sends all delete messages" + (let [chat-id "0x1" + message-id-1 "1x1" + message-id-2 "1x2" + pending-deleted-message {:deleted? true + :deleted-undoable-till (datetime/timestamp)} + cofx {:db + {:messages + {chat-id + {message-id-1 pending-deleted-message + message-id-2 pending-deleted-message}}}} + effects (delete-message/send-all cofx) + expected {:db + {:messages + {chat-id + {message-id-1 {:deleted? true} + message-id-2 {:deleted? true}}}} + :fx [[:json-rpc/call + [{:method "wakuext_deleteMessageAndSend" + :params [message-id-1] + :js-response true + :on-error [:chat/delete-and-send-error message-id-1] + :on-success [:sanitize-messages-and-process-response]}]] + [:dispatch + [:pin-message/send-pin-message + {:chat-id chat-id + :message-id message-id-1 + :pinned false + :remote-only? true}]] + [:json-rpc/call + [{:method "wakuext_deleteMessageAndSend" + :params [message-id-2] + :js-response true + :on-error [:chat/delete-and-send-error message-id-2] + :on-success [:sanitize-messages-and-process-response]}]] + [:dispatch + [:pin-message/send-pin-message + {:chat-id chat-id + :message-id message-id-2 + :pinned false + :remote-only? true}]]]}] + (is (match? effects expected))))) + +(comment + (cljs.test/run-tests)) diff --git a/src/status_im/contexts/chat/messenger/messages/delete_message_for_me/events.cljs b/src/status_im/contexts/chat/messenger/messages/delete_message_for_me/events.cljs new file mode 100644 index 00000000000..0b86d66c38c --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/delete_message_for_me/events.cljs @@ -0,0 +1,148 @@ +(ns status-im.contexts.chat.messenger.messages.delete-message-for-me.events + (:require + [status-im.contexts.chat.messenger.messages.list.events :as message-list] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- update-db-clear-undo-timer + [db chat-id message-id] + (when (get-in db [:messages chat-id message-id]) + (update-in db + [:messages chat-id message-id] + dissoc + :deleted-for-me-undoable-till))) + +(defn- update-db-delete-locally + "Delete message for me in re-frame db and set the undo timelimit" + [db chat-id message-id undo-time-limit-ms] + (when (get-in db [:messages chat-id message-id]) + (update-in db + [:messages chat-id message-id] + assoc + :deleted-for-me? true + :deleted-for-me-undoable-till (+ (datetime/timestamp) + undo-time-limit-ms)))) + +(defn- update-db-undo-locally + "Restore deleted-for-me message if called within timelimit" + [db chat-id message-id] + (let [{:keys [deleted-for-me? deleted-for-me-undoable-till]} + (get-in db [:messages chat-id message-id])] + (if (and deleted-for-me? + (> deleted-for-me-undoable-till (datetime/timestamp))) + (update-in db + [:messages chat-id message-id] + dissoc + :deleted-for-me? + :deleted-for-me-undoable-till) + (update-db-clear-undo-timer db chat-id message-id)))) + +(defn delete + "Delete message for me now locally and broadcast after undo time limit timeout" + [{:keys [db]} [{:keys [chat-id message-id]} undo-time-limit-ms]] + (when (get-in db [:messages chat-id message-id]) + (let [existing-undo-toast (get-in db [:toasts :toasts :delete-message-for-me]) + toast-count (inc (get existing-undo-toast :message-deleted-for-me-count 0)) + existing-undos (-> existing-undo-toast + (get :message-deleted-for-me-undos []) + (conj {:message-id message-id :chat-id chat-id}))] + (assoc + (message-list/rebuild-message-list + {:db (reduce + ;; sync all pending deletes' undo timelimit, extend to the latest one + (fn [db-acc {:keys [message-id chat-id]}] + (update-db-delete-locally db-acc chat-id message-id undo-time-limit-ms)) + db + existing-undos)} + chat-id) + :dispatch-n [[:toasts/close :delete-message-for-me] + [:toasts/upsert + {:id :delete-message-for-me + :type :negative + :message-deleted-for-me-count toast-count + :message-deleted-for-me-undos existing-undos + :text (i18n/label-pluralize toast-count + :t/message-deleted-for-you-count) + :duration undo-time-limit-ms + :undo-duration (/ undo-time-limit-ms 1000) + :undo-on-press #(do (rf/dispatch + [:chat.ui/undo-all-delete-message-for-me]) + (rf/dispatch [:toasts/close + :delete-message-for-me]))}]] + :utils/dispatch-later [{:dispatch [:chat.ui/delete-message-for-me-and-sync + {:chat-id chat-id :message-id message-id}] + :ms undo-time-limit-ms}])))) + +(rf/reg-event-fx :chat.ui/delete-message-for-me delete) + +(defn undo + [{:keys [db]} [{:keys [chat-id message-id]}]] + (when (get-in db [:messages chat-id message-id]) + (message-list/rebuild-message-list + {:db (update-db-undo-locally db chat-id message-id)} + chat-id))) + +(rf/reg-event-fx :chat.ui/undo-delete-message-for-me undo) + +(defn undo-all + [{:keys [db]}] + (when-let [pending-undos (get-in db + [:toasts :toasts :delete-message-for-me + :message-deleted-for-me-undos])] + {:dispatch-n (mapv #(vector :chat.ui/undo-delete-message-for-me %) pending-undos)})) + +(rf/reg-event-fx :chat.ui/undo-all-delete-message-for-me undo-all) + +(defn- check-before-delete-and-sync + "Make sure message alredy deleted-for-me? locally and undo timelimit has passed" + [db chat-id message-id] + (let [message (get-in db [:messages chat-id message-id]) + {:keys [deleted-for-me? deleted-for-me-undoable-till]} message] + (and deleted-for-me? + deleted-for-me-undoable-till + (>= (datetime/timestamp) deleted-for-me-undoable-till)))) + +(defn delete-and-sync + [{:keys [db]} [{:keys [message-id chat-id]} force?]] + (when-let [_message (get-in db [:messages chat-id message-id])] + (when (or force? (check-before-delete-and-sync db chat-id message-id)) + {:db (update-db-clear-undo-timer db chat-id message-id) + :fx [[:json-rpc/call + [{:method "wakuext_deleteMessageForMeAndSync" + :params [chat-id message-id] + :js-response true + :on-error [:chat/delete-message-for-me-and-sync-error message-id] + :on-success [:sanitize-messages-and-process-response]}]]]}))) + +(rf/reg-event-fx :chat.ui/delete-message-for-me-and-sync delete-and-sync) + +(defn delete-and-sync-error + [_ [message-id error]] + {:fx [[:effects.log/error + ["failed to delete message for me, message id:" + {:message-id message-id + :error error}]]]}) + +(rf/reg-event-fx :chat/delete-message-for-me-and-sync-error delete-and-sync-error) + +(defn- filter-pending-sync-messages + "traverse all messages find not yet synced deleted-for-me? messages" + [acc chat-id messages] + (->> messages + (filter (fn [[_ {:keys [deleted-for-me? deleted-for-me-undoable-till]}]] + (and deleted-for-me? deleted-for-me-undoable-till))) + (map (fn [message] {:chat-id chat-id :message-id (first message)})) + (concat acc))) + +(defn sync-all + "Get all deleted-for-me messages that not yet synced with status-go and sync them" + [{:keys [db] :as cofx}] + (let [pending-sync-messages (reduce-kv filter-pending-sync-messages [] (:messages db)) + pending-effects (map (fn [message] + (fn [cofx] + (delete-and-sync cofx [message true]))) + pending-sync-messages)] + (apply rf/merge cofx pending-effects))) + +(rf/reg-event-fx :chat.ui/sync-all-deleted-for-me-messages sync-all) diff --git a/src/status_im/contexts/chat/messenger/messages/delete_message_for_me/events_test.cljs b/src/status_im/contexts/chat/messenger/messages/delete_message_for_me/events_test.cljs new file mode 100644 index 00000000000..11e76833dbc --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/delete_message_for_me/events_test.cljs @@ -0,0 +1,190 @@ +(ns status-im.contexts.chat.messenger.messages.delete-message-for-me.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + matcher-combinators.test + [status-im.contexts.chat.messenger.messages.delete-message-for-me.events :as + delete-message-for-me] + [utils.datetime :as datetime])) + +(def mid "message-id") +(def cid "chat-id") + +(deftest delete-for-me-test + (with-redefs [datetime/timestamp (constantly 1)] + (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}} + message {:message-id mid :chat-id cid}] + (testing "delete for me" + (testing "dispatch right db fx" + (let [result-message (get-in (delete-message-for-me/delete {:db db} [message 1000]) + [:db :messages cid mid])] + (is (= (:id result-message) mid)) + (is (true? (:deleted-for-me? result-message))) + (is (= (:deleted-for-me-undoable-till result-message) 1001)))) + (testing "delete with pending deletes" + (let [db (-> db + (update-in [:messages cid "pending-delete-message"] + assoc + :deleted-for-me? true + :deleted-for-me-undoable-till 0 + :whisper-timestamp 0) + (update-in [:toasts :toasts :delete-message-for-me] + assoc + :message-deleted-for-me-count 1 + :message-deleted-for-me-undos [{:message-id + "pending-delete-message" + :chat-id cid}])) + effects (delete-message-for-me/delete {:db db} [message 1000])] + (is (= (get-in effects [:db :messages cid mid :deleted-for-me-undoable-till]) + (get-in effects + [:db :messages cid "pending-delete-message" + :deleted-for-me-undoable-till]) + 1001) + "sync all pending delete undo timelimit") + (let [upsert-toast (-> effects :dispatch-n second)] + (is (= (-> upsert-toast last :message-deleted-for-me-count) 2) + "+1 pending deletes") + (is + (and + (-> upsert-toast + last + :message-deleted-for-me-undos + first + :message-id + (= "pending-delete-message")) + (-> upsert-toast + last + :message-deleted-for-me-undos + second + :message-id + (= mid))) + "pending deletes are in order")))) + (testing "return nil if message not in db" + (is (= (delete-message-for-me/delete {:db {:messages []}} [message 1000]) + nil))))))) + +(deftest undo-delete-for-me-test + (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}} + message {:message-id mid :chat-id cid}] + (testing "undo delete for me" + (testing "in time" + (let [db (update-in db + [:messages cid mid] + assoc + :deleted-for-me? true + :deleted-for-me-undoable-till + (+ (datetime/timestamp) 1000)) + result-message (get-in (delete-message-for-me/undo {:db db} [message]) + [:db :messages cid mid])] + (is (= (:id result-message) mid)) + (is (nil? (:deleted-for-me? result-message))) + (is (nil? (:deleted-for-me-undoable-till result-message))))) + + (testing "remain deleted for me when undo after timelimit" + (let [db (update-in db + [:messages cid mid] + assoc + :deleted-for-me? true + :deleted-for-me-undoable-till (- (datetime/timestamp) 1000)) + result-message (get-in (delete-message-for-me/undo {:db db} [message]) + [:db :messages cid mid])] + (is (= (:id result-message) mid)) + (is (nil? (:deleted-for-me-undoable-till result-message))) + (is (true? (:deleted-for-me? result-message))))) + + (testing "return nil if message not in db" + (is (= (delete-message-for-me/undo {:db {:messages []}} [message]) + nil)))))) + +(deftest undo-all-test + (testing "does nothing when there are no pending todos" + (let [cofx {:db {:toasts + {:toasts + {:delete-message-for-me + {:message-deleted-for-me-undos nil}}}}} + effects (delete-message-for-me/undo-all cofx) + expected nil] + (is (match? effects expected)))) + + (testing "undo all pending undos" + (let [pending-undo {:undo-id 1} + pending-undos [pending-undo] + cofx {:db {:toasts + {:toasts + {:delete-message-for-me + {:message-deleted-for-me-undos pending-undos}}}}} + effects (delete-message-for-me/undo-all cofx) + expected {:dispatch-n [[:chat.ui/undo-delete-message-for-me pending-undo]]}] + (is (match? effects expected))))) + +(deftest delete-for-me-and-sync-test + (let [db {:messages {cid {mid {:id mid :deleted-for-me? true :deleted-for-me-undoable-till 0}}}} + message {:message-id mid :chat-id cid}] + (testing "delete for me and sync" + (testing "dispatch right rpc call" + (let [expected-db {:messages {cid {mid {:id mid :deleted-for-me? true}}}} + expected-fx [[:json-rpc/call + [{:method "wakuext_deleteMessageForMeAndSync" + :params [cid mid] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:chat/delete-message-for-me-and-sync-error mid]}]]] + effects (delete-message-for-me/delete-and-sync {:db db} [message false])] + (is (match? effects + {:db expected-db + :fx expected-fx})))) + (testing "clean undo timer" + (let [expected-db {:messages {cid {mid {:id mid :deleted-for-me? true}}}} + effects (delete-message-for-me/delete-and-sync + {:db (update-in db + [:messages cid mid :deleted-for-me-undoable-till] + (constantly (datetime/timestamp)))} + [message false]) + result-db (:db effects)] + (is (= result-db expected-db)))) + (testing "before deleted locally" + (let [effects (delete-message-for-me/delete-and-sync + {:db (update-in db [:messages cid mid] dissoc :deleted-for-me?)} + [message false])] + (is (-> effects :db nil?) "not delete and send"))) + (testing "before undo timelimit" + (with-redefs [datetime/timestamp (constantly 1)] + (let [effects (delete-message-for-me/delete-and-sync + {:db (update-in db [:messages cid mid] assoc :deleted-for-me-undoable-till 2)} + [message false])] + (is (-> effects :db nil?))))) + (testing "return nil if message not in db" + (is (= (delete-message-for-me/delete-and-sync {:db {:messages []}} [message false]) nil)))))) + +(deftest sync-all-test + (testing "syncs all delete-for-me messages" + (let [chat-id "0x1" + message-id-1 "1x1" + message-id-2 "1x2" + pending-deleted-message {:deleted-for-me? true + :deleted-for-me-undoable-till (datetime/timestamp)} + cofx {:db + {:messages + {chat-id + {message-id-1 pending-deleted-message + message-id-2 pending-deleted-message}}}} + effects (delete-message-for-me/sync-all cofx) + expected {:db + {:messages + {chat-id + {message-id-1 {:deleted-for-me? true} + message-id-2 {:deleted-for-me? true}}}} + :fx [[:json-rpc/call + [{:method "wakuext_deleteMessageForMeAndSync" + :params [chat-id message-id-1] + :js-response true + :on-error [:chat/delete-message-for-me-and-sync-error + message-id-1] + :on-success [:sanitize-messages-and-process-response]}]] + [:json-rpc/call + [{:method "wakuext_deleteMessageForMeAndSync" + :params [chat-id message-id-2] + :js-response true + :on-error [:chat/delete-message-for-me-and-sync-error + message-id-2] + :on-success [:sanitize-messages-and-process-response]}]]]}] + (is (match? effects expected))))) diff --git a/src/status_im/contexts/chat/messenger/messages/drawers/style.cljs b/src/status_im/contexts/chat/messenger/messages/drawers/style.cljs new file mode 100644 index 00000000000..908163ab81f --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/drawers/style.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.chat.messenger.messages.drawers.style + (:require + [quo.foundations.colors :as colors])) + +(def tab + {:flex-direction :row + :align-items :center + :justify-content :center}) + +(def tab-icon + {:margin-right 4}) + +(defn tab-count + [active? theme] + {:color (if (or active? (= :dark theme)) colors/white colors/neutral-100)}) + +(def tabs-container + {:flex 1 + :align-self :stretch + :padding-left 20 + :padding-right 8 + :margin-bottom 12}) + +(def authors-list + {:height 320 + :flex 1}) diff --git a/src/status_im/contexts/chat/messenger/messages/drawers/view.cljs b/src/status_im/contexts/chat/messenger/messages/drawers/view.cljs new file mode 100644 index 00000000000..5f7e8ec4d0e --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/drawers/view.cljs @@ -0,0 +1,294 @@ +(ns status-im.contexts.chat.messenger.messages.drawers.view + (:require + [quo.components.selectors.reaction-resource :as reactions.resource] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [reagent.core :as reagent] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.composer.reply.view :as reply] + [status-im.contexts.chat.messenger.messages.drawers.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn contact-list-item-fn + [{:keys [from compressed-key]}] + (let [[primary-name secondary-name] (rf/sub [:contacts/contact-two-names-by-identity from]) + {:keys [ens-verified added?]} (rf/sub [:contacts/contact-by-address from])] + ^{:key compressed-key} + [contact-list-item/contact-list-item + {:on-press #(rf/dispatch [:chat.ui/show-profile from])} + {:primary-name primary-name + :secondary-name secondary-name + :public-key from + :compressed-key compressed-key + :ens-verified ens-verified + :added? added?}])) + +(defn- get-tabs-data + [{:keys [reaction-authors selected-tab reactions-order theme]}] + (map (fn [reaction-type-int] + (let [author-details (get reaction-authors reaction-type-int)] + {:id reaction-type-int + :accessibility-label (keyword (str "authors-for-reaction-" reaction-type-int)) + :label [rn/view {:style style/tab} + [rn/text + {:style style/tab-icon} + (reactions.resource/system-emojis + (get constants/reactions reaction-type-int))] + [quo/text + {:weight :medium + :size :paragraph-1 + :style (style/tab-count (= selected-tab + reaction-type-int) + theme)} + (count author-details)]]})) + reactions-order)) + +(defn- reaction-authors-comp + [{:keys [selected-tab reaction-authors reactions-order theme]}] + [:<> + [rn/view style/tabs-container + [quo/tabs + {:size 32 + :scrollable? true + :in-scroll-view? true + :on-change #(reset! selected-tab %) + :default-active @selected-tab + :data (get-tabs-data {:reaction-authors reaction-authors + :selected-tab @selected-tab + :reactions-order reactions-order + :theme theme})}]] + [gesture/flat-list + {:data (for [contact (get reaction-authors @selected-tab)] + contact) + :render-fn contact-list-item-fn + :key-fn :from + :style style/authors-list}]]) + +(defn reaction-authors + [{:keys [reactions-order theme]}] + (let [{:keys [reaction-authors-list + selected-reaction]} (rf/sub [:reactions/authors]) + selected-tab (reagent/atom (or selected-reaction + (first (keys reaction-authors-list))))] + (fn [] + [reaction-authors-comp + {:selected-tab selected-tab + :reaction-authors reaction-authors-list + :reactions-order reactions-order + :theme theme}]))) + +(defn pin-message + [{:keys [chat-id pinned pinned-by] :as message-data}] + (let [pinned-messages (rf/sub [:chats/pinned chat-id]) + message-not-pinned? (and (empty? pinned-by) (not pinned))] + (if (and message-not-pinned? (> (count pinned-messages) 2)) + (do + (js/setTimeout (fn [] (rf/dispatch [:dismiss-keyboard])) 500) + (rf/dispatch [:pin-message/show-pin-limit-modal chat-id])) + (rf/dispatch [:pin-message/send-pin-message + (assoc message-data :pinned message-not-pinned?)])))) + +(defn get-actions + [{:keys [outgoing content pinned-by outgoing-status deleted? deleted-for-me? content-type + bridge-message message-id] + :as message-data} + {:keys [able-to-send-message? community? community-member? can-delete-message-for-everyone? + message-pin-enabled group-chat group-admin? debug-log-level?]}] + (let [edit-message? (and (or community-member? (not community?)) + outgoing + (not (or deleted? deleted-for-me?)) + ;; temporarily disable edit image message until + ;; https://github.com/status-im/status-mobile/issues/15298 is + ;; implemented + (not= content-type constants/content-type-image) + (not= content-type constants/content-type-audio)) + reply? (and able-to-send-message? + (not= outgoing-status :sending) + (not (or deleted? deleted-for-me?))) + copy-text? (and (not (or deleted? deleted-for-me?)) + (not= content-type constants/content-type-audio)) + ;; pinning images are temporarily disabled + pin-message? (and message-pin-enabled + (not= content-type constants/content-type-image) + (or community-member? (not community?))) + delete-for-me? (and (or community-member? (not community?)) + (not (or deleted? deleted-for-me? bridge-message))) + delete-for-everyone? (and (or community-member? (not community?)) + (cond + deleted? false + outgoing true + community? can-delete-message-for-everyone? + group-chat group-admin? + bridge-message false + :else false))] + (cond-> [] + reply? + (conj {:type :main + :on-press #(rf/dispatch [:chat.ui/reply-to-message message-data]) + :label (i18n/label :t/message-reply) + :icon :i/reply + :accessibility-label :reply-message + :id :reply}) + + edit-message? + (conj {:type :main + :on-press #(rf/dispatch [:chat.ui/edit-message message-data]) + :label (i18n/label :t/edit-message) + :icon :i/edit + :accessibility-label :edit-message + :id :edit}) + + copy-text? + (conj {:type :main + :on-press #(clipboard/set-string + (reply/get-quoted-text-with-mentions + (get content :parsed-text))) + :label (i18n/label :t/copy-text) + :accessibility-label :copy-text + :icon :i/copy + :id :copy}) + + debug-log-level? + (conj {:type :main + :on-press #(clipboard/set-string message-id) + :label (i18n/label :t/copy-message-id) + :accessibility-label :copy-message-id + :icon :i/copy + :id :copy-message-id}) + + pin-message? + (conj {:type :main + :on-press #(pin-message message-data) + :label (i18n/label (if pinned-by + (if community? :t/unpin-from-channel :t/unpin-from-chat) + (if community? :t/pin-to-channel :t/pin-to-chat))) + :accessibility-label (if pinned-by :unpin-message :pin-message) + :icon :i/pin + :id (if pinned-by :unpin :pin)}) + + delete-for-me? + (conj {:type :danger + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:chat.ui/delete-message-for-me message-data + config/delete-message-for-me-undo-time-limit-ms])) + + :label (i18n/label :t/delete-for-me) + :accessibility-label :delete-for-me + :icon :i/delete + :id :delete-for-me}) + + delete-for-everyone? + (conj {:type :danger + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:chat.ui/delete-message message-data + config/delete-message-undo-time-limit-ms])) + :label (i18n/label :t/delete-for-everyone) + :accessibility-label :delete-for-everyone + :icon :i/delete + :id :delete-for-all})))) + +(defn reactions + [{:keys [chat-id message-id]}] + (let [msg-reactions (rf/sub [:chats/message-reactions message-id chat-id]) + own-reactions (reduce (fn [acc {:keys [emoji-id own emoji-reaction-id]}] + (if own + (assoc acc emoji-id emoji-reaction-id) + acc)) + {} + msg-reactions)] + [rn/view + {:style {:flex-direction :row + :justify-content :space-between + :padding-horizontal 22 + :padding-top 5 + :padding-bottom 15}} + (for [[id reaction-name] constants/reactions + :let [emoji-reaction-id (get own-reactions id)]] + ^{:key id} + [quo/reactions-selector + {:emoji reaction-name + :start-pressed? (boolean emoji-reaction-id) + :accessibility-label (str "reaction-" (name reaction-name)) + :on-press + (fn [] + (if emoji-reaction-id + (rf/dispatch [:reactions/send-emoji-reaction-retraction emoji-reaction-id]) + (rf/dispatch [:reactions/send-emoji-reaction + {:message-id message-id + :emoji-id id}])) + (rf/dispatch [:hide-bottom-sheet]))}])])) + +(defn reactions-and-actions + [message-data {:keys [chat-id community? community-member?] :as context}] + (fn [] + (let [data (if (contains? message-data :album-id) + (first (:album message-data)) + message-data) + {:keys [deleted-for-me? + outgoing-status + deleted? + message-id]} data + actions (get-actions data context) + {main-actions :main + danger-actions :danger + admin-actions :admin} (group-by :type actions)] + [:<> + ;; REACTIONS + (when (and (or community-member? (not community?)) + (not= outgoing-status :sending) + (not (or deleted? deleted-for-me?))) + [reactions {:chat-id chat-id :message-id message-id}]) + ;; MAIN ACTIONS + [rn/view {:style {:padding-horizontal 8}} + (for [action main-actions] + (let [on-press (:on-press action)] + ^{:key (:id action)} + [quo/drawer-action + {:type :main + :title (:label action) + :accessibility-label (:accessibility-label action) + :icon (:icon action) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (when on-press (on-press)))}]))] + + (when-not (empty? danger-actions) + [quo/separator {:style {:margin-vertical 8}}]) + + ;; DANGER ACTIONS + [rn/view {:style {:padding-horizontal 8}} + (for [action danger-actions] + (let [on-press (:on-press action)] + ^{:key (:id action)} + [quo/drawer-action + {:type :danger + :title (:label action) + :accessibility-label (:accessibility-label action) + :icon (:icon action) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (when on-press (on-press)))}]))] + + (when-not (empty? admin-actions) + [quo/separator {:style {:margin-vertical 8}}]) + + ;; ADMIN ACTIONS + [rn/view {:style {:padding-horizontal 8}} + (for [action admin-actions] + (let [on-press (:on-press action)] + ^{:key (:id action)} + [quo/drawer-action + {:type :danger + :title (:label action) + :accessibility-label (:accessibility-label action) + :icon (:icon action) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (when on-press (on-press)))}]))]]))) diff --git a/src/status_im/contexts/chat/messenger/messages/list/events.cljs b/src/status_im/contexts/chat/messenger/messages/list/events.cljs new file mode 100644 index 00000000000..7b741b013e5 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/list/events.cljs @@ -0,0 +1,203 @@ +(ns status-im.contexts.chat.messenger.messages.list.events + (:require + [status-im.constants :as constants] + [utils.datetime :as datetime] + [utils.re-frame :as rf] + [utils.red-black-tree :as red-black-tree])) + +(def is-system-message-content-type? + #{constants/content-type-system-text + constants/content-type-community + constants/content-type-system-message-mutual-event-accepted + constants/content-type-system-message-mutual-event-removed + constants/content-type-system-message-mutual-event-sent + constants/content-type-system-pinned-message}) + +(defn is-system-message? + [content-type message-type] + (or + (is-system-message-content-type? content-type) + (= constants/message-type-private-group-system-message + message-type))) + +(defn- add-datemark + [{:keys [whisper-timestamp] :as msg}] + ;;NOTE(performance) this is slow + (assoc msg :datemark (datetime/day-relative whisper-timestamp))) + +(defn- add-timestamp + [{:keys [whisper-timestamp] :as msg}] + (assoc msg :timestamp-str (datetime/timestamp->time whisper-timestamp))) + +(defn prepare-message + [{:keys [message-id + clock-value + album-id + message-type + content-type + from + outgoing + whisper-timestamp + deleted? + deleted-for-me? + albumize?]}] + (-> {:whisper-timestamp whisper-timestamp + :from from + :one-to-one? (= constants/message-type-one-to-one message-type) + :system-message? (or + (is-system-message? content-type message-type) + deleted? + deleted-for-me?) + :clock-value clock-value + :type :message + :message-id message-id + :outgoing (boolean outgoing) + ;; always albumize outgoing (image already loaded) messages + :albumize? (if (and album-id outgoing) true albumize?)} + add-datemark + add-timestamp)) + +(defn same-group? + "Whether a message is in the same group as the one after it. + We check the time, and the author" + [a b] + (and + (not (:system-message? a)) + (not (:system-message? b)) + (= (:from a) (:from b)) + (<= (js/Math.abs (- (:whisper-timestamp a) (:whisper-timestamp b))) constants/group-ms))) + +(defn display-photo? + "We display photos for other users, and not in 1-to-1 chats" + [{:keys [system-message? one-to-one? outgoing]}] + (or system-message? + (and + (not outgoing) + (not one-to-one?)))) + +(defn compare-fn + "Compare two messages, first compare by clock-value, and break ties by message-id, + which gives us total ordering across all clients" + [a b] + (let [initial-comparison (compare (:clock-value b) (:clock-value a))] + (if (= initial-comparison 0) + (compare (:message-id a) (:message-id b)) + initial-comparison))) + +(defn add-group-info + "Add positional data to a message, based on the next and previous message. + We divide messages in groups. Messages are sorted descending so :first? is + the most recent message, similarly :first-in-group? is the most recent message + in a group." + [{:keys [system-message? + one-to-one? outgoing] + :as current-message} + {:keys [outgoing-seen?] :as previous-message} + next-message] + (let [last-in-group? (or (nil? next-message) + (not (same-group? current-message next-message)))] + (assoc current-message + :first? (nil? previous-message) + :first-outgoing? (and outgoing + (not outgoing-seen?)) + :outgoing-seen? (or outgoing-seen? + outgoing) + :first-in-group? (or (nil? previous-message) + (not (same-group? current-message previous-message))) + :last-in-group? last-in-group? + :display-username? (and last-in-group? + (not system-message?) + (not outgoing) + (not one-to-one?)) + :display-photo? (display-photo? current-message)))) + +(defn update-next-message + "Update next message in the list, we set :first? to false, and check if it + :first-outgoing? state has changed because of the insertion" + [current-message next-message] + (assoc + next-message + :first? false + :first-outgoing? (and + (not (:first-outgoing? current-message)) + (:first-outgoing? next-message)) + :outgoing-seen? (:outgoing-seen? current-message) + :first-in-group? + (not (same-group? current-message next-message)))) + +(defn update-previous-message + "If this is a new group, we mark the previous as the last one in the group" + [current-message + {:keys [one-to-one? + system-message? + outgoing] + :as previous-message}] + (let [last-in-group? (not (same-group? current-message previous-message))] + (assoc previous-message + :display-username? (and last-in-group? + (not system-message?) + (not outgoing) + (not one-to-one?)) + :last-in-group? last-in-group?))) + +(defn update-message + "Update the message and siblings with positional info" + [tree message] + (let [iterator (red-black-tree/find tree message) + previous-message (red-black-tree/get-prev iterator) + next-message (red-black-tree/get-next iterator) + message-with-pos-data (add-group-info message previous-message next-message)] + (cond-> (red-black-tree/update iterator message-with-pos-data) + next-message + (-> (red-black-tree/find next-message) + (red-black-tree/update (update-next-message message-with-pos-data next-message))) + + (and previous-message + (not= :datemark (:type previous-message))) + (-> (red-black-tree/find previous-message) + (red-black-tree/update (update-previous-message message-with-pos-data previous-message)))))) + +(defn remove-message + "Remove a message in the list" + [tree prepared-message] + (let [iterator (red-black-tree/find tree prepared-message)] + (if (not iterator) + tree + (let [new-tree (red-black-tree/remove iterator) + next-message (red-black-tree/get-next iterator)] + (if (not next-message) + new-tree + (update-message new-tree next-message)))))) + +(defn insert-message + "Insert a message in the list, pull it's left and right messages, calculate + its positional metadata, and update the left & right messages if necessary, + this operation is O(logN) for insertion, and O(logN) for the updates, as + we need to re-find (there's probably a better way)" + [old-message-list prepared-message] + (let [tree (red-black-tree/insert old-message-list prepared-message)] + (update-message tree prepared-message))) + +(defn add + [message-list message] + (insert-message (or message-list (red-black-tree/tree compare-fn)) (prepare-message message))) + +(defn add-many + [message-list messages] + (reduce add + message-list + messages)) + +(defn ->seq + [message-list] + (if message-list + (array-seq (red-black-tree/get-values message-list)) + [])) + +;; NOTE(performance): this is too expensive, probably we could mark message somehow and just hide it in +;; the UI +(rf/defn rebuild-message-list + [{:keys [db]} chat-id] + {:db (assoc-in db + [:message-lists chat-id] + (add-many nil (vals (get-in db [:messages chat-id]))))}) diff --git a/src/status_im/contexts/chat/messenger/messages/list/events_test.cljs b/src/status_im/contexts/chat/messenger/messages/list/events_test.cljs new file mode 100644 index 00000000000..5c386769cb8 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/list/events_test.cljs @@ -0,0 +1,207 @@ +(ns status-im.contexts.chat.messenger.messages.list.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.chat.messenger.messages.list.events :as s] + [taoensso.tufte :as tufte :refer-macros [defnp profile]])) + +(deftest message-stream-tests-test + (testing "building the list" + (let [m1 {:from "1" + :clock-value 1 + :message-id "message-1" + :whisper-timestamp 1 + :outgoing false} + m2 {:from "2" + :clock-value 2 + :message-id "message-2" + :whisper-timestamp 2 + :outgoing true} + m3 {:from "2" + :clock-value 3 + :message-id "message-3" + :whisper-timestamp 3 + :outgoing true} + messages (shuffle [m1 m2 m3]) + [actual-m1 + actual-m2 + actual-m3] + (s/->seq (s/add-many nil messages))] + (testing "it sorts them correclty" + (is (= "message-3" (:message-id actual-m1))) + (is (= "message-2" (:message-id actual-m2))) + (is (= "message-1" (:message-id actual-m3)))) + (testing "it marks only the first message as :first?" + (is (:first? actual-m1)) + (is (not (:first? actual-m2))) + (is (not (:first? actual-m3)))) + (testing "it marks the first outgoing message as :first-outgoing?" + (is (:first-outgoing? actual-m1)) + (is (not (:first-outgoing? actual-m2))) + (is (not (:first-outgoing? actual-m3)))) + (testing "it marks messages from the same author next to another with :first-in-group?" + (is (:first-in-group? actual-m1)) + (is (not (:first-in-group? actual-m2))) + (is (:first-in-group? actual-m3))) + (testing "it marks messages with display-photo? when they are not outgoing and are first-in-group?" + (is (not (:display-photo? actual-m1))) + (is (not (:display-photo? actual-m2))) + (is (:display-photo? actual-m3))) + (testing "it marks the last message from the same author with :last-in-group?" + (is (not (:last-in-group? actual-m1))) + (is (:last-in-group? actual-m2)) + (is (:last-in-group? actual-m3)))))) + +(defnp append-to-message-list + [l message] + (s/add l message)) + +(defnp prepend-to-message-list + [l message] + (s/add l message)) + +(defnp insert-close-to-head-message-list + [l message] + (s/add l message)) + +(defnp insert-middle-message-list + [l message] + (s/add l message)) + +(tufte/add-basic-println-handler! {:format-pstats-opts {:columns [:n :mean :min :max :clock :sum] + :format-id-fn name}}) + +(deftest ^:benchmark benchmark-list-test + (let [messages (sort-by + :timestamp + (mapv (fn [i] + (let [i (+ 100000 i 1)] + {:timestamp i :clock-value i :message-id (str i) :whisper-timestamp i})) + (range 100))) + built-list (s/add-many nil messages)] + (testing "prepending to list" + (profile {} + (dotimes [_ 10] + (prepend-to-message-list + built-list + {:clock-value 200000 + :message-id "200000" + :whisper-timestamp 21 + :timestamp 21})))) + (testing "append to list" + (profile {} + (dotimes [_ 10] + (append-to-message-list + built-list + {:clock-value 100000 + :message-id "100000" + :whisper-timestamp 100000 + :timestamp 100000})))) + (testing "insert close to head" + (profile {} + (dotimes [_ 10] + (insert-close-to-head-message-list + built-list + {:clock-value 109970 + :message-id "109970" + :whisper-timestamp 1000 + :timestamp 1000})))) + (testing "insert into the middle list" + (profile {} + (dotimes [_ 10] + (insert-middle-message-list + built-list + {:clock-value 105000 + :message-id "10500" + :whisper-timestamp 1000 + :timestamp 1000})))))) + +(deftest message-list-test + (let [current-messages [{:clock-value 109 + :message-id "109" + :timestamp 9 + :whisper-timestamp 9} + {:clock-value 106 + :message-id "106" + :timestamp 6 + :whisper-timestamp 6} + {:clock-value 103 + :message-id "103" + :timestamp 3 + :whisper-timestamp 3}] + current-list (s/add-many nil current-messages)] + (testing "removing a message" + (let [updated-list (-> (s/remove-message current-list + {:clock-value 106 + :message-id "106"}) + (s/->seq))] + (is (= 2 (count updated-list))) + (is (= 103 (-> (nth updated-list 1) :clock-value))))) + (testing "inserting a newer message" + (let [new-message {:timestamp 12 + :clock-value 112 + :message-id "112" + :whisper-timestamp 12}] + (is (= 112 + (-> (s/add current-list new-message) + (s/->seq) + first + :clock-value))))) + (testing "inserting an older message" + (let [new-message {:timestamp 0 + :clock-value 100 + :message-id "100" + :whisper-timestamp 0}] + (is (= 100 + (-> (s/add current-list new-message) + (s/->seq) + last + :clock-value))))) + (testing "inserting in the middle of the list" + (let [new-message {:timestamp 7 + :clock-value 107 + :message-id "107" + :whisper-timestamp 7}] + (is (= 107 + (-> (s/add current-list new-message) + (s/->seq) + (nth 1) + :clock-value))))) + (testing "inserting in the middle of the list, clock-value clash" + (let [new-message {:timestamp 6 + :clock-value 106 + :message-id "106a" + :whisper-timestamp 6}] + (is (= "106a" + (-> (s/add current-list new-message) + (s/->seq) + (nth 2) + :message-id))))))) + +(deftest same-group-test + (testing "two messages from the same author and close together" + (is (s/same-group? {:whisper-timestamp 1 + :from "1"} + {:whisper-timestamp 2 + :from "1"}))) + (testing "two messages from the same author, close together, first system-message" + (is (not (s/same-group? {:whisper-timestamp 1 + :system-message? true + :from "1"} + {:whisper-timestamp 2 + :from "1"})))) + (testing "two messages from the same author, close together, second system-message" + (is (not (s/same-group? {:whisper-timestamp 1 + :from "1"} + {:whisper-timestamp 2 + :system-message? true + :from "1"})))) + (testing "two messages from the same author and far apart" + (is (not (s/same-group? {:whisper-timestamp 1 + :from "1"} + {:whisper-timestamp 2000000 + :from "1"})))) + (testing "two messages not from the same author and close together" + (is (not (s/same-group? {:whisper-timestamp 1 + :from "2"} + {:whisper-timestamp 2 + :from "1"}))))) diff --git a/src/status_im/contexts/chat/messenger/messages/list/state.cljs b/src/status_im/contexts/chat/messenger/messages/list/state.cljs new file mode 100644 index 00000000000..8d74349ed30 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/list/state.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.chat.messenger.messages.list.state) + +(defonce messages-list-ref (atom nil)) +(defonce first-not-visible-item (atom nil)) + +(defonce scrolling (atom nil)) + +(defn start-scrolling + [] + (reset! scrolling true)) + +(defn stop-scrolling + [] + (reset! scrolling false)) + +(defn reset-visible-item + [] + (reset! first-not-visible-item nil)) diff --git a/src/status_im/contexts/chat/messenger/messages/list/style.cljs b/src/status_im/contexts/chat/messenger/messages/list/style.cljs new file mode 100644 index 00000000000..dfdf3999dd5 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/list/style.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.chat.messenger.messages.list.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area] + [status-im.contexts.chat.messenger.messages.constants :as messages.constants])) + +(def permission-context-height 46) +(def distance-from-last-message 4) + +(defn keyboard-avoiding-container + [theme] + {:flex 1 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :z-index 2}) + +(def permission-context-sheet {:flex 3}) ; Pushes composer to bottom + +(defn list-paddings + [add-padding-bottom?] + (let [{:keys [top bottom]} safe-area/insets] + ;; WARNING: the flat-list is reversed, so the paddings are applied inverted + {:padding-top (if add-padding-bottom? + (+ distance-from-last-message permission-context-height bottom) + distance-from-last-message) + :padding-bottom (+ top messages.constants/top-bar-height)})) diff --git a/src/status_im/contexts/chat/messenger/messages/list/view.cljs b/src/status_im/contexts/chat/messenger/messages/list/view.cljs new file mode 100644 index 00000000000..f91ed947f2a --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/list/view.cljs @@ -0,0 +1,183 @@ +(ns status-im.contexts.chat.messenger.messages.list.view + (:require + [legacy.status-im.ui.screens.chat.group :as chat.group] + [oops.core :as oops] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.background-timer :as background-timer] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.content.view :as message] + [status-im.contexts.chat.messenger.messages.list.state :as state] + [status-im.contexts.chat.messenger.messages.list.style :as style] + [utils.re-frame :as rf] + [utils.worklets.chat.messenger.messages :as worklets])) + +(def ^:const loading-indicator-page-loading-height 100) + +(defn list-key-fn [{:keys [message-id value]}] (or message-id value)) +(defn list-ref [ref] (reset! state/messages-list-ref ref)) + +(defn on-viewable-items-changed + [e] + (when @state/messages-list-ref + (reset! state/first-not-visible-item + (when-let [last-visible-element (aget (oops/oget e "viewableItems") + (dec (oops/oget e "viewableItems.length")))] + (let [index (oops/oget last-visible-element "index") + ;; Get first not visible element, if it's a datemark/gap + ;; we might add messages on receiving as they do not have + ;; a clock value, but usually it will be a message + first-not-visible (aget (oops/oget @state/messages-list-ref "props.data") (inc index))] + (when (and first-not-visible + (= :message (:type first-not-visible))) + first-not-visible)))))) + +(defn list-on-end-reached + [distance-from-list-top] + ;; FIXME: that's a bit of a hack but we need to update `distance-from-list-top` once the new + ;; messages are fetched in order for the header to work properly + (let [on-loaded (fn [n] + (reanimated/set-shared-value distance-from-list-top + (+ (reanimated/get-shared-value distance-from-list-top) + (* n 200))))] + (if @state/scrolling + (rf/dispatch [:chat.ui/load-more-messages-for-current-chat on-loaded]) + (background-timer/set-timeout #(rf/dispatch [:chat.ui/load-more-messages-for-current-chat + on-loaded]) + (if platform/low-device? 700 100))))) + +(defn loading-view + [{:keys [chat-id window-height]}] + (let [messages (rf/sub [:chats/raw-chat-messages-stream chat-id]) + loading-first-page? (= (count messages) 0) + parent-height (if loading-first-page? + window-height + loading-indicator-page-loading-height)] + [quo/skeleton-list + {:content :messages + :parent-height parent-height + :animated? false}])) + +(defn more-messages-loader + [{:keys [chat-id] :as props}] + (let [loading-messages? (rf/sub [:chats/loading-messages? chat-id]) + all-loaded? (rf/sub [:chats/all-loaded? chat-id])] + (when (or loading-messages? (not all-loaded?)) + [loading-view props]))) + +(defn list-group-chat-header + [{:keys [chat-id invitation-admin]}] + [chat.group/group-chat-footer chat-id invitation-admin]) + +(defn render-fn + [{:keys [type value] :as message-data} _ _ + {:keys [context keyboard-shown?]}] + (cond + (= type :datemark) + [quo/divider-date value] + + :else + [message/message message-data context keyboard-shown?])) + +(defn on-content-size-change + [{:keys [content-height distance-atom distance-from-list-top]}] + (fn [_ content-height-new] + ;; Updates to shared values are asynchronous and give the wrong value when accessed + ;; simultaneously(in on-layout event), that's why we are using distance atom here + (let [change (- content-height-new @content-height) + new-distance (+ @distance-atom change)] + (when-not (= change 0) + (reanimated/set-shared-value distance-from-list-top new-distance) + (reset! distance-atom new-distance) + (reset! content-height content-height-new))))) + +(defn on-layout + [{:keys [event layout-height distance-atom distance-from-list-top on-layout-done?]}] + (let [layout-height-new (oops/oget event "nativeEvent.layout.height") + change (- layout-height-new @layout-height) + new-distance (- @distance-atom change)] + (when (and (not= change 0) (not= @layout-height layout-height-new)) + (reanimated/set-shared-value distance-from-list-top new-distance) + (reset! distance-atom new-distance) + (reset! layout-height layout-height-new)) + (reset! on-layout-done? true))) + +(defn on-scroll-fn + [distance-atom layout-height-atom] + (fn [layout-height new-distance] + (when (not= layout-height @layout-height-atom) + (reset! layout-height-atom layout-height)) + (reset! distance-atom new-distance))) + +(defn messages-list-content + [{:keys [insets distance-from-list-top layout-height chat-list-scroll-y on-layout-done?]}] + (let [content-height (rn/use-ref-atom 0) + distance-atom (rn/use-ref-atom 0) + theme (quo.context/use-theme) + {:keys [keyboard-shown]} (hooks/use-keyboard) + {:keys [chat-type chat-id] + :as chat} (rf/sub [:chats/current-chat-chat-view]) + community-channel? (= constants/community-chat-type chat-type) + {window-height :height} (rn/get-window) + context (rf/sub [:chats/current-chat-message-list-view-context]) + able-to-send-message? (:able-to-send-message? context) + messages (rf/sub [:chats/raw-chat-messages-stream chat-id]) + add-padding-bottom? (and community-channel? (not able-to-send-message?)) + recording? (rf/sub [:chats/recording?])] + [rn/view {:style style/permission-context-sheet} + [rn/view {:style {:flex-shrink 1}} ;; Keeps flat list on top + [reanimated/flat-list + {:key-fn list-key-fn + :ref list-ref + :bounces false + :header (when (= (:chat-type chat) constants/private-group-chat-type) + [list-group-chat-header chat]) + :footer [more-messages-loader + {:chat-id chat-id + :window-height window-height}] + :data messages + :render-data {:theme theme + :context context + :keyboard-shown? keyboard-shown + :insets insets} + :render-fn render-fn + :on-viewable-items-changed on-viewable-items-changed + :on-content-size-change (on-content-size-change + {:content-height content-height + :distance-atom distance-atom + :distance-from-list-top distance-from-list-top}) + :on-end-reached #(list-on-end-reached distance-from-list-top) + :on-scroll-to-index-failed identity + :keyboard-dismiss-mode :interactive + :keyboard-should-persist-taps :always + :on-scroll-begin-drag (fn [] + (rf/dispatch [:chat.ui/set-input-focused false]) + (rn/dismiss-keyboard!)) + :on-momentum-scroll-begin state/start-scrolling + :on-momentum-scroll-end state/stop-scrolling + :scroll-event-throttle 16 + :on-scroll (reanimated/use-animated-scroll-handler + (worklets/messages-list-on-scroll + distance-from-list-top + chat-list-scroll-y + (on-scroll-fn distance-atom layout-height))) + :style {:height "100%" + :background-color (colors/theme-colors colors/white + colors/neutral-95 + theme)} + :content-container-style (style/list-paddings add-padding-bottom?) + :inverted true + :on-layout #(on-layout + {:event % + :layout-height layout-height + :distance-atom distance-atom + :distance-from-list-top distance-from-list-top + :on-layout-done? on-layout-done?}) + :scroll-enabled (not recording?) + :content-inset-adjustment-behavior :never + :scroll-indicator-insets {:right 1}}]]])) diff --git a/src/status_im/contexts/chat/messenger/messages/navigation/style.cljs b/src/status_im/contexts/chat/messenger/messages/navigation/style.cljs new file mode 100644 index 00000000000..359e65e01e6 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/navigation/style.cljs @@ -0,0 +1,51 @@ +(ns status-im.contexts.chat.messenger.messages.navigation.style + (:require [quo.foundations.colors :as colors] + [status-im.contexts.chat.messenger.messages.constants :as messages.constants])) + +(defn navigation-view + [navigation-view-height] + {:top 0 + :left 0 + :right 0 + :position :absolute + :pointer-events :box-none + :height (+ navigation-view-height messages.constants/pinned-banner-height) + :z-index 1}) + +(defn background + [navigation-view-height] + {:height navigation-view-height + :top 0 + :left 0 + :right 0 + :overflow :hidden + :position :absolute}) + +(defn header-container + [top-insets] + {:margin-top top-insets + :flex-direction :row + :padding-horizontal 20 + :overflow :hidden + :height messages.constants/top-bar-height + :align-items :center}) + +;;;; Content + +(def header-content-container + {:flex-direction :row + :align-items :center + :flex 1 + :margin-horizontal 12 + :height 40}) + +(def header-text-container + {:margin-left 8}) + +(defn header-display-name + [theme] + {:color (colors/theme-colors colors/black colors/white theme)}) + +(defn header-status + [theme] + {:color (colors/theme-colors colors/neutral-80-opa-50 colors/white-opa-40 theme)}) diff --git a/src/status_im/contexts/chat/messenger/messages/navigation/view.cljs b/src/status_im/contexts/chat/messenger/messages/navigation/view.cljs new file mode 100644 index 00000000000..efa0eae6ef6 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/navigation/view.cljs @@ -0,0 +1,120 @@ +(ns status-im.contexts.chat.messenger.messages.navigation.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [re-frame.db] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.common.home.actions.view :as actions] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.constants :as messages.constants] + [status-im.contexts.chat.messenger.messages.navigation.style :as style] + [status-im.contexts.chat.messenger.messages.pin.banner.view :as pin.banner] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn header-content-container + [{:keys [chat-id group-chat chat-type chat-name emoji color] :as _chat}] + (let [theme (quo.context/use-theme) + display-name (cond + (= chat-type constants/one-to-one-chat-type) + (first (rf/sub [:contacts/contact-two-names-by-identity chat-id])) + + (= chat-type constants/community-chat-type) + (str "# " chat-name) + + :else + (str emoji chat-name)) + online? (when-not group-chat (rf/sub [:visibility-status-updates/online? chat-id])) + photo-path (if group-chat + (rf/sub [:chats/group-chat-image chat-id]) + (rf/sub [:chats/photo-path chat-id])) + community-channel? (= chat-type constants/community-chat-type)] + [rn/view {:style style/header-content-container} + (cond + community-channel? + [quo/channel-avatar + {:size :size-32 + :full-name chat-name + :customization-color color + :emoji (when-not (string/blank? emoji) + (string/trim emoji))}] + group-chat + [quo/group-avatar + {:customization-color color + :size :size-32 + :picture photo-path + :chat-name chat-name}] + + :else + [quo/user-avatar + {:full-name display-name + :online? online? + :profile-picture photo-path + :size :small}]) + [rn/view {:style style/header-text-container} + [quo/text + {:weight :semi-bold + :size :paragraph-1 + :number-of-lines 1 + :style (style/header-display-name theme)} + display-name] + (when-not group-chat + [quo/text + {:number-of-lines 1 + :weight :medium + :size :paragraph-2 + :style (style/header-status theme)} + (i18n/label + (if online? :t/online :t/offline))])]])) + +(defn header-background + [{:keys [chat-id navigation-view-height]}] + (let [theme (quo.context/use-theme)] + [:<> + [rn/view {:style (style/background navigation-view-height)} + [quo/blur + {:style {:flex 1} + :blur-amount 20 + :blur-type :transparent + :overlay-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur theme) + :blur-radius (if platform/ios? 20 10)}]] + [pin.banner/banner + {:chat-id chat-id + :top-offset navigation-view-height}]])) + +(defn view + [] + (let [{:keys [chat-id chat-type] + :as chat} (rf/sub [:chats/current-chat-chat-view]) + top-insets safe-area/top + screen-id (quo.context/use-screen-id) + navigation-view-height (+ top-insets messages.constants/top-bar-height)] + [rn/view {:style (style/navigation-view navigation-view-height)} + [header-background + {:chat-id chat-id + :navigation-view-height navigation-view-height}] + [rn/view {:style (style/header-container top-insets)} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :back-button + :on-press #(rf/dispatch [:navigate-back screen-id])} + (if (= chat-type constants/community-chat-type) :i/arrow-left :i/close)] + [header-content-container chat] + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :options-button + :on-press (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [actions/chat-actions chat true])}]))} + :i/options]]])) diff --git a/src/status_im/contexts/chat/messenger/messages/pin/banner/style.cljs b/src/status_im/contexts/chat/messenger/messages/pin/banner/style.cljs new file mode 100644 index 00000000000..9c561c74f53 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/pin/banner/style.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.chat.messenger.messages.pin.banner.style + (:require + [status-im.contexts.chat.messenger.messages.constants :as messages.constants])) + +(def container + {:position :absolute + :overflow :hidden + :left 0 + :right 0 + :height messages.constants/pinned-banner-height}) + +(defn container-with-top-offset + [top-offset] + (assoc container :top top-offset)) diff --git a/src/status_im/contexts/chat/messenger/messages/pin/banner/view.cljs b/src/status_im/contexts/chat/messenger/messages/pin/banner/view.cljs new file mode 100644 index 00000000000..f85c6879ac3 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/pin/banner/view.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.chat.messenger.messages.pin.banner.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.contexts.chat.messenger.messages.pin.banner.style :as style] + [utils.re-frame :as rf])) + +(defn banner + [{:keys [chat-id top-offset]}] + (let [theme (quo.context/use-theme) + latest-pin-text (rf/sub [:chats/last-pinned-message-text chat-id]) + pins-count (rf/sub [:chats/pin-messages-count chat-id])] + (when (> pins-count 0) + [rn/view {:style (style/container-with-top-offset top-offset)} + [quo/blur + {:style style/container + :blur-radius (if platform/ios? 20 10) + :blur-type theme + :blur-amount 20}] + [quo/banner + {:latest-pin-text latest-pin-text + :pins-count pins-count + :on-press (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:pin-message/show-pins-bottom-sheet chat-id]))}]]))) diff --git a/src/status_im/contexts/chat/messenger/messages/pin/events.cljs b/src/status_im/contexts/chat/messenger/messages/pin/events.cljs new file mode 100644 index 00000000000..639db780f16 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/pin/events.cljs @@ -0,0 +1,135 @@ +(ns status-im.contexts.chat.messenger.messages.pin.events + (:require + [legacy.status-im.data-store.messages :as data-store.messages] + [legacy.status-im.data-store.pin-messages :as data-store.pin-messages] + [re-frame.core :as re-frame] + [status-im.common.toasts.events :as toasts] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.menus.pinned-messages.view :as pinned-messages-menu] + [status-im.contexts.chat.messenger.messages.list.events :as message-list] + [status-im.navigation.events :as navigation] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/defn handle-failed-loading-pin-messages + {:events [:pin-message/failed-loading-pin-messages]} + [{:keys [db]} current-chat-id _ err] + (log/error "failed loading pin messages" current-chat-id err) + (when current-chat-id + {:db (assoc-in db [:pagination-info current-chat-id :loading-pin-messages?] false)})) + +(rf/defn pin-messages-loaded + {:events [:pin-message/pin-messages-loaded]} + [{db :db} chat-id {:keys [cursor pinned-messages]}] + (let [all-messages (reduce (fn [acc {:keys [message-id] :as message}] + (assoc acc message-id message)) + {} + pinned-messages)] + {:db (-> db + (assoc-in [:pagination-info chat-id :loading-pin-messages?] false) + (assoc-in [:pin-messages chat-id] all-messages) + (assoc-in [:pin-message-lists chat-id] (message-list/add-many nil (vals all-messages))) + (assoc-in [:pagination-info chat-id :all-pin-loaded?] + (empty? cursor)))})) + +(defn remove-pinned-message + [db pinned-message] + (update-in db + [:pin-messages (aget pinned-message "localChatId")] + dissoc + (aget pinned-message "message_id"))) + +(defn add-pinned-message + [db pinned-message] + (let [message (aget pinned-message "pinnedMessage")] + (if (and message + (aget message "message")) + (assoc-in db + [:pin-messages + (aget pinned-message "localChatId") + (aget pinned-message "message_id")] + (-> (aget message "message") + (js->clj :keywordize-keys true) + data-store.messages/<-rpc + (assoc :pinned-by (aget message "pinnedBy") + :pinned-at (aget message "pinnedAt")))) + db))) + +(rf/defn receive-signal + [{:keys [db]} pin-messages] + (let [current-chat-id (db :current-chat-id) + db (reduce (fn [db pin-message] + (let [pinned? (aget pin-message "pinned") + chat-id (aget pin-message "localChatId")] + (cond + (not= chat-id current-chat-id) + db + pinned? + (add-pinned-message db pin-message) + :else + (remove-pinned-message db pin-message)))) + db + pin-messages)] + {:db + (assoc-in db + [:pin-message-lists current-chat-id] + (message-list/add-many nil (vals (get-in db [:pin-messages current-chat-id]))))})) + +(rf/defn send-pin-message-locally + "Pin message, rebuild pinned messages list locally" + {:events [:pin-message/send-pin-message-locally]} + [{:keys [db] :as cofx} {:keys [chat-id message-id pinned] :as pin-message}] + (let [current-public-key (get-in db [:profile/profile :public-key]) + message (merge pin-message {:pinned-by current-public-key}) + pin-message-lists-exist? (some? (get-in db [:pin-message-lists chat-id]))] + (rf/merge cofx + {:db (cond-> db + pinned + (-> + (update-in [:pin-message-lists chat-id] message-list/add message) + (assoc-in [:pin-messages chat-id message-id] message)) + (and (not pinned) pin-message-lists-exist?) + (-> + (update-in [:pin-message-lists chat-id] message-list/remove-message pin-message) + (update-in [:pin-messages chat-id] dissoc message-id)))}))) + +(rf/defn send-pin-message + "Pin message, rebuild pinned messages list" + {:events [:pin-message/send-pin-message]} + [cofx {:keys [remote-only?] :as pin-message}] + (rf/merge cofx + (when-not remote-only? (send-pin-message-locally pin-message)) + (data-store.pin-messages/send-pin-message {:chat-id (pin-message :chat-id) + :message_id (pin-message :message-id) + :pinned (pin-message :pinned)}))) + +(rf/defn load-pin-messages + {:events [:pin-message/load-pin-messages]} + [{:keys [db]} chat-id] + (let [not-loading-pin-messages? (not (get-in db [:pagination-info chat-id :loading-pin-messages?]))] + (when not-loading-pin-messages? + (rf/merge + {:db (assoc-in db [:pagination-info chat-id :loading-pin-messages?] true)} + (data-store.pin-messages/pinned-message-by-chat-id-rpc + chat-id + nil + constants/default-number-of-pin-messages + #(re-frame/dispatch [:pin-message/pin-messages-loaded chat-id %]) + #(re-frame/dispatch [:pin-message/failed-loading-pin-messages chat-id %])))))) + +(rf/defn show-pin-limit-modal + {:events [:pin-message/show-pin-limit-modal]} + [cofx] + (toasts/upsert cofx + {:type :negative + :text (i18n/label :t/pin-limit-reached)})) + +(rf/defn show-pins-bottom-sheet + {:events [:pin-message/show-pins-bottom-sheet]} + [cofx chat-id] + (navigation/show-bottom-sheet + cofx + {:padding-bottom-override 21 + :content (fn [] [pinned-messages-menu/view + {:chat-id chat-id}])})) diff --git a/src/status_im/contexts/chat/messenger/messages/pin/events_test.cljs b/src/status_im/contexts/chat/messenger/messages/pin/events_test.cljs new file mode 100644 index 00000000000..bfa942b97e4 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/pin/events_test.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.chat.messenger.messages.pin.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.chat.messenger.messages.pin.events :as events])) + +(deftest receive-signal-test + (let [chat-id "chat-id" + message-id-1 "0x1" + message-id-2 "0x2" + message-id-3 "0x3" + message-1 {:id message-id-1 :whisperTimestamp 1 :timestamp 1 :clock 2} + message-3 {:id message-id-3 :whisperTimestamp 1 :timestamp 1 :clock 2} + db {:current-chat-id chat-id + :pin-messages {chat-id {message-id-1 {} + message-id-2 {}}}}] + (testing "receiving a pinned messages update" + (let [pinned-messages-signal (clj->js [{:pinned true + :localChatId chat-id + :message_id message-id-1 + :pinnedMessage {:pinnedAt 1 + :pinnedBy "0x1" + :message message-1}} + {:pinned false + :localChatId chat-id + :message_id message-id-2} + {:pinned true + :localChatId chat-id + :message_id message-id-3 + :pinnedMessage {:pinnedAt 1 + :pinnedBy "0x1" + :message message-3}}]) + actual (events/receive-signal {:db db} + pinned-messages-signal)] + + (is (not (get-in actual [:db :pin-messages chat-id message-id-2]))) + (is (get-in actual [:db :pin-messages chat-id message-id-1 :message-id])) + (is (get-in actual [:db :pin-messages chat-id message-id-3 :message-id])))))) diff --git a/src/status_im/contexts/chat/messenger/messages/resolver/message_resolver.cljs b/src/status_im/contexts/chat/messenger/messages/resolver/message_resolver.cljs new file mode 100644 index 00000000000..c626816978a --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/resolver/message_resolver.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.chat.messenger.messages.resolver.message-resolver + (:require + [utils.re-frame :as rf])) + +(defn resolve-message + [parsed-text] + (reduce + (fn [acc {:keys [type literal destination] :as some-text}] + (str acc + (case type + "paragraph" + (resolve-message (:children some-text)) + + "mention" + (rf/sub [:messages/resolve-mention literal]) + + "status-tag" + (str "#" literal) + + "link" + destination + + literal))) + "" + parsed-text)) diff --git a/src/status_im/contexts/chat/messenger/messages/resolver/message_resolver_test.cljs b/src/status_im/contexts/chat/messenger/messages/resolver/message_resolver_test.cljs new file mode 100644 index 00000000000..9257c49faa9 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/resolver/message_resolver_test.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.chat.messenger.messages.resolver.message-resolver-test + (:require + [cljs.test :as t] + [status-im.contexts.chat.messenger.messages.resolver.message-resolver :as resolver] + [utils.re-frame :as rf])) + +(def mentions + {"0xsome_id" "foobar"}) + +(defn sub + [[_ mention]] + (get mentions mention mention)) + +(def parsed-text + [{:type "paragraph" + :children + [{:literal ""} + {:type "mention" + :literal + "0xsome_id"} + {:literal " i just mention you here to debug this issue "} + {:type "link" + :children + [{:literal "https://foo.bar"}] + :literal "" + :title "" + :destination "https://foo.bar"} + {:literal " , no worries"}]}]) + +(t/deftest resolve-message-test + (with-redefs [rf/sub sub] + (t/testing "" + (let [text (resolver/resolve-message parsed-text)] + (t/is (= text + "foobar i just mention you here to debug this issue https://foo.bar , no worries")))))) diff --git a/src/status_im/contexts/chat/messenger/messages/scroll_to_bottom/style.cljs b/src/status_im/contexts/chat/messenger/messages/scroll_to_bottom/style.cljs new file mode 100644 index 00000000000..1204ba23ca0 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/scroll_to_bottom/style.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.chat.messenger.messages.scroll-to-bottom.style + (:require [react-native.safe-area :as safe-area] + [status-im.contexts.shell.constants :as shell.constants])) + +(def ^:private bottom-drawer-height 48) + +(defn shell-button-container + [able-to-send-messages?] + {:z-index 1 + :background-color :red + :bottom (if-not able-to-send-messages? + (+ shell.constants/floating-shell-button-height + safe-area/bottom + bottom-drawer-height) + shell.constants/floating-shell-button-height)}) + +(def scroll-to-bottom-button + {:position :absolute + :right 0 + :left 0}) diff --git a/src/status_im/contexts/chat/messenger/messages/scroll_to_bottom/view.cljs b/src/status_im/contexts/chat/messenger/messages/scroll_to_bottom/view.cljs new file mode 100644 index 00000000000..7db27643019 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/scroll_to_bottom/view.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.chat.messenger.messages.scroll-to-bottom.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.chat.messenger.messages.scroll-to-bottom.style :as style] + [utils.re-frame :as rf] + [utils.worklets.chat.messenger.composer :as worklets])) + +(defn button + [chat-list-scroll-y] + (let [{window-height :height} (rn/get-window) + scroll-down-button-opacity (worklets/scroll-down-button-opacity + chat-list-scroll-y + false + window-height) + able-to-send-message? (rf/sub [:chats/able-to-send-message?])] + [rn/view {:style (style/shell-button-container able-to-send-message?)} + [quo/floating-shell-button + {:scroll-to-bottom {:on-press #(rf/dispatch [:chat.ui/scroll-to-bottom])}} + style/scroll-to-bottom-button + scroll-down-button-opacity]])) diff --git a/src/status_im/contexts/chat/messenger/messages/transport/events.cljs b/src/status_im/contexts/chat/messenger/messages/transport/events.cljs new file mode 100644 index 00000000000..f96d9c535d5 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/transport/events.cljs @@ -0,0 +1,425 @@ +(ns status-im.contexts.chat.messenger.messages.transport.events + (:require + [clojure.set :as set] + [clojure.string :as string] + [legacy.status-im.browser.core :as browser] + [legacy.status-im.chat.models.message :as models.message] + [legacy.status-im.data-store.activities :as data-store.activities] + [legacy.status-im.data-store.chats :as data-store.chats] + [legacy.status-im.data-store.communities :as data-store.communities] + [legacy.status-im.data-store.invitations :as data-store.invitations] + [legacy.status-im.data-store.messages :as data-store.messages] + [legacy.status-im.group-chats.core :as models.group] + [legacy.status-im.multiaccounts.update.core :as update.core] + [legacy.status-im.pairing.core :as models.pairing] + [legacy.status-im.utils.deprecated-types :as types] + [legacy.status-im.visibility-status-updates.core :as models.visibility-status-updates] + [status-im.constants :as constants] + [status-im.contexts.chat.contacts.events :as models.contact] + [status-im.contexts.chat.events :as chat.events] + [status-im.contexts.chat.messenger.messages.content.reactions.events :as reactions] + [status-im.contexts.chat.messenger.messages.pin.events :as messages.pin] + [status-im.contexts.communities.events :as communities] + [status-im.contexts.shell.activity-center.events :as activity-center] + [status-im.contexts.wallet.data-store :as wallet.data-store] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/defn process-next + {:events [:transport/process-next]} + [cofx ^js response-js sync-handler] + (if sync-handler + (sync-handler cofx response-js true) + {:utils/dispatch-later [{:ms 20 :dispatch [:process-response response-js]}]})) + +(rf/defn process-response + {:events [:process-response]} + [cofx ^js response-js process-async] + (let [^js communities (.-communities response-js) + ^js requests-to-join-community (.-requestsToJoinCommunity response-js) + ^js chats (.-chats response-js) + ^js contacts (.-contacts response-js) + ^js installations (.-installations response-js) + ^js messages (.-messages response-js) + ^js emoji-reactions (.-emojiReactions response-js) + ^js invitations (.-invitations response-js) + ^js removed-chats (.-removedChats response-js) + ^js activity-notifications (.-activityCenterNotifications response-js) + ^js activity-center-state (.-activityCenterState response-js) + ^js pin-messages-js (.-pinMessages response-js) + ^js removed-messages (.-removedMessages response-js) + ^js visibility-status-updates (.-statusUpdates response-js) + ^js current-visibility-status (.-currentStatus response-js) + ^js bookmarks (.-bookmarks response-js) + ^js settings (remove nil? (.-settings response-js)) + ^js cleared-histories (.-clearedHistories response-js) + ^js identity-images (.-identityImages response-js) + ^js accounts (.-accounts response-js) + ^js keypairs (.-keypairs response-js) + ^js ens-username-details-js (.-ensUsernameDetails response-js) + ^js customization-color-js (.-customizationColor response-js) + ^js saved-addresses-js (.-savedAddresses response-js) + ^js watch-only-accounts (.-watchOnlyAccounts response-js) + sync-handler (when-not process-async process-response)] + (cond + + (seq chats) + (do + (js-delete response-js "chats") + (rf/merge cofx + (process-next response-js sync-handler) + #(chat.events/ensure-chats % [(map data-store.chats/<-rpc (types/js->clj chats))]))) + + (seq messages) + (models.message/receive-many cofx response-js) + + (seq activity-notifications) + (let [notifications (->> activity-notifications + types/js->clj + (map data-store.activities/<-rpc))] + (js-delete response-js "activityCenterNotifications") + (rf/merge cofx + (activity-center/notifications-reconcile notifications) + (activity-center/show-toasts notifications) + (process-next response-js sync-handler))) + + (some? activity-center-state) + (let [seen? (-> activity-center-state + types/js->clj + data-store.activities/<-rpc-seen-state)] + (js-delete response-js "activityCenterState") + (rf/merge cofx + (activity-center/reconcile-seen-state seen?) + (process-next response-js sync-handler))) + + (seq installations) + (let [installations-clj (types/js->clj installations)] + (js-delete response-js "installations") + (rf/merge cofx + (process-next response-js sync-handler) + (models.pairing/handle-installations installations-clj))) + + (seq contacts) + (models.contact/process-js-contacts cofx response-js) + + (seq communities) + (do + (js-delete response-js "communities") + (rf/merge cofx + (process-next response-js sync-handler) + (communities/handle-communities communities))) + + (seq bookmarks) + (let [bookmarks-clj (types/js->clj bookmarks)] + (js-delete response-js "bookmarks") + (rf/merge cofx + (process-next response-js sync-handler) + (browser/handle-bookmarks bookmarks-clj))) + + (seq pin-messages-js) + (do + (js-delete response-js "pinMessages") + (rf/merge cofx + (process-next response-js sync-handler) + (messages.pin/receive-signal pin-messages-js))) + + (seq removed-chats) + (let [removed-chats-clj (types/js->clj removed-chats)] + (js-delete response-js "removedChats") + (rf/merge cofx + (process-next response-js sync-handler) + (communities/handle-removed-chats removed-chats-clj))) + + (seq requests-to-join-community) + (let [requests (->> requests-to-join-community + types/js->clj + (map data-store.communities/<-request-to-join-community-rpc))] + (js-delete response-js "requestsToJoinCommunity") + (rf/merge cofx + (process-next response-js sync-handler) + (communities/handle-requests-to-join requests))) + + (seq emoji-reactions) + (let [reactions (types/js->clj emoji-reactions)] + (js-delete response-js "emojiReactions") + (rf/merge cofx + (process-next response-js sync-handler) + (reactions/receive-signal (map reactions/<-rpc reactions)))) + + (seq invitations) + (let [invitations (types/js->clj invitations)] + (js-delete response-js "invitations") + (rf/merge cofx + (process-next response-js sync-handler) + (models.group/handle-invitations (map data-store.invitations/<-rpc invitations)))) + + (seq removed-messages) + (let [removed-messages-clj (types/js->clj removed-messages)] + (js-delete response-js "removedMessages") + (rf/merge cofx + (process-next response-js sync-handler) + (models.message/handle-removed-messages removed-messages-clj))) + + (seq cleared-histories) + (let [cleared-histories-clj (types/js->clj cleared-histories)] + (js-delete response-js "clearedHistories") + (rf/merge cofx + (process-next response-js sync-handler) + (models.message/handle-cleared-histories-messages cleared-histories-clj))) + + (seq visibility-status-updates) + (let [visibility-status-updates-clj (types/js->clj visibility-status-updates)] + (js-delete response-js "statusUpdates") + (rf/merge cofx + (process-next response-js sync-handler) + (models.visibility-status-updates/handle-visibility-status-updates + visibility-status-updates-clj))) + + (seq accounts) + (do + (js-delete response-js "accounts") + (rf/merge cofx + (process-next response-js sync-handler))) + + (seq watch-only-accounts) + (do + (js-delete response-js "watchOnlyAccounts") + (rf/merge cofx + {:fx [[:dispatch + [:wallet/reconcile-watch-only-accounts + (types/js->clj watch-only-accounts)]]]} + (process-next response-js sync-handler))) + + (seq keypairs) + (do + (js-delete response-js "keypairs") + (rf/merge cofx + {:fx [[:dispatch [:wallet/reconcile-keypairs (types/js->clj keypairs)]]]} + (process-next response-js sync-handler))) + + (seq settings) + (do + (js-delete response-js "settings") + (rf/merge cofx + (process-next response-js sync-handler) + (update.core/set-many-js settings))) + + (seq identity-images) + (let [images-clj (map types/js->clj identity-images)] + (js-delete response-js "identityImages") + (rf/merge cofx + (process-next response-js sync-handler) + (update.core/optimistic :images images-clj))) + + (some? current-visibility-status) + (let [current-visibility-status-clj (types/js->clj current-visibility-status)] + (js-delete response-js "currentStatus") + (rf/merge cofx + (process-next response-js sync-handler) + (models.visibility-status-updates/sync-visibility-status-update + current-visibility-status-clj))) + + (seq saved-addresses-js) + (let [saved-addresses (-> saved-addresses-js types/js->clj wallet.data-store/rpc->saved-addresses)] + (js-delete response-js "savedAddresses") + (rf/merge cofx + {:fx [[:dispatch [:wallet/reconcile-saved-addresses saved-addresses]]]} + (process-next response-js sync-handler))) + + (seq ens-username-details-js) + (let [ens-username-details-clj (types/js->clj ens-username-details-js)] + (js-delete response-js "ensUsernameDetails") + (rf/merge cofx + (process-next response-js sync-handler) + (rf/dispatch [:ens/update-usernames ens-username-details-clj]))) + + (seq customization-color-js) + (let [customization-color (types/js->clj customization-color-js)] + (js-delete response-js "customizationColor") + (rf/merge cofx + (process-next response-js sync-handler) + (rf/dispatch [:profile/save-local-accent-color (keyword customization-color)])))))) + +(defn group-by-and-update-unviewed-counts + "group messages by current chat, profile updates, transactions and update unviewed counters in db for not curent chats" + [{:keys [current-chat-id db] :as acc} ^js message-js] + (let [chat-id (.-localChatId message-js) + message-type (.-messageType message-js) + from (.-from message-js) + mentioned (.-mentioned message-js) + new-message (.-new message-js) + current (= current-chat-id chat-id) + should-update-unviewed? (and (not current) + new-message + (not (= message-type + constants/message-type-private-group-system-message)) + (not (= from (get-in db [:profile/profile :public-key])))) + tx-hash (and (.-commandParameters message-js) + (.-commandParameters.transactionHash message-js))] + (cond-> acc + current + (update :messages conj message-js) + + ;;update counter + should-update-unviewed? + (update-in [:db :chats chat-id :unviewed-messages-count] inc) + + (and should-update-unviewed? + mentioned) + (update-in [:db :chats chat-id :unviewed-mentions-count] inc) + + ;;conj incoming transaction for :watch-tx + (not (string/blank? tx-hash)) + (update :transactions conj tx-hash) + + :always + (update :chats conj chat-id)))) + +(defn sort-js-messages! + "sort messages, so we can start process latest first,in that case we only need to process frist 20 and drop others" + [^js response-js messages] + (if (seq messages) + (set! (.-messages response-js) + (.sort (to-array messages) + (fn [^js a ^js b] + (- (.-clock b) (.-clock a))))) + (js-delete response-js "messages"))) + +(rf/defn sanitize-messages-and-process-response + "before processing we want to filter and sort messages, so we can process first only messages which will be showed" + {:events [:sanitize-messages-and-process-response]} + [{:keys [db] :as cofx} ^js response-js process-async] + (when response-js + (let [current-chat-id (:current-chat-id db) + {:keys [db messages transactions statuses]} + (reduce group-by-and-update-unviewed-counts + {:db db + :chats #{} + :transactions #{} + :statuses [] + :messages [] + :current-chat-id current-chat-id} + (.-messages response-js))] + (sort-js-messages! response-js messages) + (rf/merge cofx + {:db db + :utils/dispatch-later (concat [] + (when (seq statuses) + [{:ms 100 :dispatch [:process-statuses statuses]}]) + (when (seq transactions) + (for [transaction-hash transactions] + {:ms 100 + :dispatch [:watch-tx nil transaction-hash]})))} + (process-response response-js process-async))))) + +(rf/defn remove-hash + [{:keys [db]} envelope-hash] + {:db (update db :transport/message-envelopes dissoc envelope-hash)}) + +(rf/defn check-confirmations + [{:keys [db] :as cofx} status chat-id message-id] + (when-let [{:keys [pending-confirmations not-sent]} + (get-in db [:transport/message-ids->confirmations message-id])] + (if (zero? (dec pending-confirmations)) + (rf/merge cofx + {:db (update db + :transport/message-ids->confirmations + dissoc + message-id)} + (models.message/update-message-status chat-id + message-id + (if not-sent + :not-sent + status)) + (remove-hash message-id)) + (let [confirmations {:pending-confirmations (dec pending-confirmations) + :not-sent (or not-sent + (= :not-sent status))}] + {:db (assoc-in db + [:transport/message-ids->confirmations message-id] + confirmations)})))) + +(rf/defn update-envelope-status + [{:keys [db] :as cofx} message-id status] + (if-let [{:keys [chat-id]} + (get-in db [:transport/message-envelopes message-id])] + (when-let [{:keys [_from]} (get-in db [:messages chat-id message-id])] + (check-confirmations cofx status chat-id message-id)) + ;; We don't have a message-envelope for this, might be that the confirmation came too early + {:db (update-in db [:transport/message-confirmations message-id] conj status)})) + +(rf/defn update-envelopes-status + [{:keys [db] :as cofx} message-id status] + (when (or (not= status :not-sent) + (= :online (:network db))) + (apply rf/merge cofx (map #(update-envelope-status % status) message-id)))) + +(rf/defn set-message-envelope-hash + "message-type is used for tracking" + [{:keys [db] :as cofx} chat-id message-id message-type] + ;; Check first if the confirmation has already arrived + (let [statuses (get-in db [:transport/message-confirmations message-id]) + check-confirmations-fx (map + #(check-confirmations % chat-id message-id) + statuses) + + add-envelope-data (fn [{:keys [db]}] + {:db (-> db + (update :transport/message-confirmations dissoc message-id) + (assoc-in [:transport/message-envelopes message-id] + {:chat-id chat-id + :message-type message-type}) + (update-in [:transport/message-ids->confirmations message-id] + #(or % {:pending-confirmations 1})))})] + (apply rf/merge cofx (conj check-confirmations-fx add-envelope-data)))) + +(rf/defn transport-message-sent + {:events [:transport/message-sent]} + [cofx ^js response-js] + (let [set-hash-fxs (map (fn [{:keys [localChatId id messageType]}] + (set-message-envelope-hash localChatId id messageType)) + (types/js->clj (.-messages response-js)))] + (apply rf/merge + cofx + (conj set-hash-fxs + #(sanitize-messages-and-process-response % response-js false))))) + +(defn- image->rpc [image] (set/rename-keys image {:data-uri :dataUri})) + +(defn- link-preview->rpc + [preview] + (-> preview + (update :thumbnail image->rpc) + (update :favicon image->rpc))) + +(defn build-message + [msg] + (-> msg + (update :link-previews #(map link-preview->rpc %)) + (update :status-link-previews #(set (map data-store.messages/->status-link-previews-rpc %))) + (set/rename-keys + {:album-id :albumId + :audio-duration-ms :audioDurationMs + :audio-path :audioPath + :chat-id :chatId + :community-id :communityId + :content-type :contentType + :ens-name :ensName + :image-height :imageHeight + :image-path :imagePath + :image-width :imageWidth + :link-previews :linkPreviews + :status-link-previews :statusLinkPreviews + :response-to :responseTo + :sticker :sticker + :text :text}))) + +(rf/defn send-chat-messages + [_ messages] + {:json-rpc/call [{:method "wakuext_sendChatMessages" + :params [(mapv build-message messages)] + :js-response true + :on-success #(rf/dispatch [:transport/message-sent %]) + :on-error #(do + (log/warn "failed to send a message" %) + (js/alert (str "failed to send a message: " %)))}]}) diff --git a/src/status_im/contexts/chat/messenger/messages/view.cljs b/src/status_im/contexts/chat/messenger/messages/view.cljs new file mode 100644 index 00000000000..92f7a0ae55b --- /dev/null +++ b/src/status_im/contexts/chat/messenger/messages/view.cljs @@ -0,0 +1,62 @@ +(ns status-im.contexts.chat.messenger.messages.view + (:require + [clojure.string :as string] + [quo.context :as quo.context] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.contexts.chat.messenger.composer.view :as composer] + [status-im.contexts.chat.messenger.messages.contact-requests.bottom-drawer.view :as + contact-requests.bottom-drawer] + [status-im.contexts.chat.messenger.messages.list.style :as style] + [status-im.contexts.chat.messenger.messages.list.view :as list.view] + [status-im.contexts.chat.messenger.messages.navigation.view :as messages.navigation] + [status-im.contexts.chat.messenger.messages.scroll-to-bottom.view :as scroll-to-bottom] + [status-im.contexts.chat.messenger.placeholder.view :as placeholder.view] + [utils.re-frame :as rf])) + +(defn- footer + [layout-height] + (let [current-chat-id (rf/sub [:chats/current-chat-id]) + able-to-send-message? (rf/sub [:chats/able-to-send-message?])] + (when-not (string/blank? current-chat-id) + (if able-to-send-message? + [composer/view layout-height] + [contact-requests.bottom-drawer/view {:contact-id current-chat-id}])))) + +(defn- chat-screen + [on-layout-done?] + (let [theme (quo.context/use-theme) + layout-height (rn/use-ref-atom 0) + distance-from-list-top (reanimated/use-shared-value 0) + chat-list-scroll-y (reanimated/use-shared-value 0) + alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + insets safe-area/insets] + [rn/keyboard-avoiding-view + {:style (style/keyboard-avoiding-container theme) + :keyboard-vertical-offset (- (if platform/ios? alert-banners-top-margin 0) + (:bottom insets))} + [:<> + [list.view/messages-list-content + {:insets insets + :distance-from-list-top distance-from-list-top + :chat-list-scroll-y chat-list-scroll-y + :layout-height layout-height + :on-layout-done? on-layout-done?}] + [scroll-to-bottom/button chat-list-scroll-y]] + [messages.navigation/view] + [footer layout-height]])) + +(defn chat + [] + (let [on-layout-done? (reagent/atom false) + first-render-done? (reagent/atom false)] + (fn [] + (let [chat-exists? (rf/sub [:chats/current-chat-exist?])] + (rn/use-mount #(reset! first-render-done? true)) + [:<> + (when (and chat-exists? @first-render-done?) + [chat-screen on-layout-done?]) + [placeholder.view/view on-layout-done?]])))) diff --git a/src/status_im/contexts/chat/messenger/photo_selector/album_selector/style.cljs b/src/status_im/contexts/chat/messenger/photo_selector/album_selector/style.cljs new file mode 100644 index 00000000000..932ed6d4b80 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/photo_selector/album_selector/style.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.chat.messenger.photo-selector.album-selector.style + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(defn selector-container + [top theme] + (reanimated/apply-animations-to-style {:top top} + {:position :absolute + :z-index 1 + :background-color (colors/theme-colors + colors/white + colors/neutral-100 + theme) + :left 0 + :right 0})) + +(defn album-container + [selected?] + {:flex-direction :row + :padding-horizontal 12 + :padding-vertical 8 + :margin-horizontal 8 + :border-radius 12 + :align-items :center + :background-color (when selected? colors/primary-50-opa-5)}) + +(def cover + {:width 40 + :height 40 + :border-radius 10}) + +(def divider + {:padding-horizontal 20 + :margin-top 12 + :margin-bottom 8}) diff --git a/src/status_im/contexts/chat/messenger/photo_selector/album_selector/view.cljs b/src/status_im/contexts/chat/messenger/photo_selector/album_selector/view.cljs new file mode 100644 index 00000000000..5789a0eb6fd --- /dev/null +++ b/src/status_im/contexts/chat/messenger/photo_selector/album_selector/view.cljs @@ -0,0 +1,100 @@ +(ns status-im.contexts.chat.messenger.photo-selector.album-selector.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [status-im.contexts.chat.messenger.photo-selector.album-selector.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn render-album + [{title :title size :count uri :uri} index _ {:keys [album? selected-album top]}] + (let [selected? (= selected-album title) + theme (quo.context/use-theme)] + [rn/touchable-opacity + {:on-press (fn [] + (rf/dispatch [:chat.ui/camera-roll-select-album title]) + (rf/dispatch [:photo-selector/get-photos-for-selected-album]) + (reanimated/animate top (:height (rn/get-window))) + (js/setTimeout #(reset! album? false) 300)) + :style (style/album-container selected?) + :accessibility-label (str "album-" index)} + [rn/image + {:source {:uri uri} + :style style/cover}] + [rn/view {:style {:margin-left 12}} + [quo/text + {:weight :medium + :ellipsize-mode :tail + :number-of-lines 1 + :style {:margin-right 50}} + title] + [quo/text + {:size :paragraph-2 + :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}} + (when size + (str size + " " + (if (= size 1) + (i18n/label :t/image) + (i18n/label :t/images))))]] + (when selected? + [rn/view + {:style {:position :absolute + :right 16}} + [quo/icon :i/check + {:size 20 :color (colors/theme-colors colors/primary-50 colors/primary-60 theme)}]])])) + +(def no-title "no-title") + +(defn section-header + [{:keys [title]}] + (when-not (= title no-title) + [quo/divider-label + {:container-style style/divider + :tight? false} + title])) + +(defn key-fn + [item index] + (str (:title item) index)) + +(defn- f-album-selector + [{:keys [scroll-enabled? on-scroll]} album? selected-album top] + (let [theme (quo.context/use-theme) + albums (rf/sub [:camera-roll/albums]) + total-photos-count-android (rf/sub [:camera-roll/total-photos-count-android]) + total-photos-count-ios (rf/sub [:camera-roll/total-photos-count-ios]) + albums-sections [{:title no-title + :data [(assoc (:smart-album albums) + :count + (if platform/ios? + total-photos-count-ios + total-photos-count-android))]} + {:title (i18n/label :t/my-albums) + :data (:my-albums albums)}] + window-height (:height (rn/get-window))] + [reanimated/view {:style (style/selector-container top theme)} + [gesture/section-list + {:data albums-sections + :sections albums-sections + :render-data {:album? album? + :selected-album selected-album + :top top} + :render-fn render-album + :sticky-section-headers-enabled false + :render-section-header-fn section-header + :content-container-style {:padding-top 64 + :padding-bottom 40} + :key-fn key-fn + :scroll-enabled @scroll-enabled? + :on-scroll on-scroll + :style {:height window-height}}]])) + +(defn album-selector + [sheet album? selected-album top] + [:f> f-album-selector sheet album? selected-album top]) diff --git a/src/status_im/contexts/chat/messenger/photo_selector/effects.cljs b/src/status_im/contexts/chat/messenger/photo_selector/effects.cljs new file mode 100644 index 00000000000..7068b40d0b3 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/photo_selector/effects.cljs @@ -0,0 +1,98 @@ +(ns status-im.contexts.chat.messenger.photo-selector.effects + (:require + [clojure.string :as string] + [promesa.core :as promesa] + [react-native.cameraroll :as cameraroll] + [react-native.core :as rn] + [react-native.image-resizer :as image-resizer] + [react-native.permissions :as permissions] + [react-native.platform :as platform] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:private maximum-image-size-px 2000) + +(rf/reg-fx :effects.camera-roll/request-permissions-and-get-photos + (fn [[num end-cursor album]] + (permissions/request-permissions + {:permissions [(if platform/is-below-android-13? :read-external-storage :read-media-images)] + :on-allowed + (fn [] + (cameraroll/get-photos + (merge {:first num + :assetType "Photos" + :groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums") + :groupName (if (not= album (i18n/label :t/recent)) album "") + :include ["imageSize"]} + (when end-cursor + {:after end-cursor})) + #(rf/dispatch [:on-camera-roll-get-photos (:edges %) (:page_info %) end-cursor])))}))) + +(defn- resize-photo + [uri] + (promesa/let [[width height] (rn/image-get-size uri) + resize? (> (max width height) + maximum-image-size-px) + resized-uri (-> (image-resizer/resize + {:max-width (if resize? maximum-image-size-px width) + :max-height (if resize? maximum-image-size-px height) + :path uri + :quality 60}) + (promesa/then (fn [^js resized-image] + (let [path (.-path resized-image)] + (if (string/starts-with? path "file") + path + (str "file://" path))))))] + {:resized-uri resized-uri + :width width + :height height})) + +(rf/reg-fx :effects.camera-roll/image-selected + (fn [[image chat-id]] + (-> (resize-photo (:uri image)) + (promesa/then #(rf/dispatch + [:photo-selector/image-selected chat-id image %])) + (promesa/catch #(log/error "could not resize image" %))))) + +(defn- get-albums + [callback] + (let [albums (atom {:smart-album [] + :my-albums []})] + ;; Get the "recent" album first + (cameraroll/get-photos + {:first 1 :groupTypes "All" :assetType "Photos"} + (fn [res-recent] + (swap! albums assoc + :smart-album + {:title (i18n/label :t/recent) + :uri (get-in (first (:edges res-recent)) [:node :image :uri])}) + ;; Get albums, then loop over albums and get each one's cover (first photo) + (cameraroll/get-albums + {:assetType "Photos"} + (fn [res-albums] + (let [response-count (count res-albums)] + (if (pos? response-count) + (doseq [album res-albums] + (cameraroll/get-photos + {:first 1 :groupTypes "Albums" :groupName (:title album) :assetType "Photos"} + (fn [res] + (let [uri (get-in (first (:edges res)) [:node :image :uri])] + (swap! albums update :my-albums conj (merge album {:uri uri})) + (when (= (count (:my-albums @albums)) response-count) + (swap! albums update :my-albums #(sort-by :title %)) + (callback @albums)))))) + (callback @albums))))))))) + +(rf/reg-fx :effects.camera-roll/get-albums + (fn [] + (get-albums #(rf/dispatch [:on-camera-roll-get-albums %])))) + +(defn get-photos-count-ios-fx + [cb] + (cameraroll/get-photos-count-ios cb)) + +(rf/reg-fx :effects.camera-roll/get-photos-count-ios + (fn [] + (when platform/ios? + (get-photos-count-ios-fx #(rf/dispatch [:on-camera-roll-get-images-count-ios %]))))) diff --git a/src/status_im/contexts/chat/messenger/photo_selector/events.cljs b/src/status_im/contexts/chat/messenger/photo_selector/events.cljs new file mode 100644 index 00000000000..98529f09fac --- /dev/null +++ b/src/status_im/contexts/chat/messenger/photo_selector/events.cljs @@ -0,0 +1,83 @@ +(ns status-im.contexts.chat.messenger.photo-selector.events + (:require + [re-frame.core :as re-frame] + [status-im.constants :as constants] + status-im.contexts.chat.messenger.photo-selector.effects + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/defn on-camera-roll-get-albums + {:events [:on-camera-roll-get-albums]} + [{:keys [db]} albums] + {:db (assoc db :camera-roll/albums albums) + :dispatch [:photo-selector/camera-roll-get-ios-photo-count]}) + +(rf/defn get-photos-count-ios + {:events [:on-camera-roll-get-images-count-ios]} + [{:keys [db]} cnt] + {:db (assoc db :camera-roll/ios-images-count cnt)}) + +(rf/defn camera-roll-get-albums + {:events [:photo-selector/camera-roll-get-albums]} + [_] + {:effects.camera-roll/get-albums nil}) + +(rf/defn camera-roll-get-ios-photo-count + {:events [:photo-selector/camera-roll-get-ios-photo-count]} + [_] + {:effects.camera-roll/get-photos-count-ios nil}) + +(rf/defn camera-roll-select-album + {:events [:chat.ui/camera-roll-select-album]} + [{:keys [db]} album] + {:db (assoc db :camera-roll/selected-album album)}) + +(rf/defn image-selected + {:events [:photo-selector/image-selected]} + [{:keys [db]} current-chat-id original {:keys [resized-uri width height]}] + {:db + (update-in db + [:chat/inputs current-chat-id :metadata :sending-image (:uri original)] + merge + original + {:resized-uri resized-uri + :width width + :height height})}) + +(rf/defn on-camera-roll-get-photos + {:events [:on-camera-roll-get-photos]} + [{:keys [db]} photos page-info end-cursor] + (let [photos_x (when end-cursor (:camera-roll/photos db))] + {:db (-> db + (assoc :camera-roll/photos (concat photos_x (map #(get-in % [:node :image]) photos))) + (assoc :camera-roll/end-cursor (:end_cursor page-info)) + (assoc :camera-roll/has-next-page (:has_next_page page-info)) + (assoc :camera-roll/loading-more false))})) + +(rf/defn get-photos-for-selected-album + {:events [:photo-selector/get-photos-for-selected-album]} + [{:keys [db]} end-cursor] + {:effects.camera-roll/request-permissions-and-get-photos + [21 end-cursor + (or (:camera-roll/selected-album db) + (i18n/label :t/recent))]}) + +(rf/defn camera-roll-loading-more + {:events [:photo-selector/camera-roll-loading-more]} + [{:keys [db]} is-loading] + {:db (assoc db :camera-roll/loading-more is-loading)}) + +(rf/defn camera-roll-pick + {:events [:photo-selector/camera-roll-pick]} + [{:keys [db]} image chat-id] + (let [current-chat-id (or chat-id (:current-chat-id db)) + images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])] + (when (and (< (count images) constants/max-album-photos) + (not (some #(= (:uri image) (:uri %)) images))) + {:effects.camera-roll/image-selected [image current-chat-id]}))) + +(re-frame/reg-event-fx :photo-selector/navigate-to-photo-selector + (fn [] + {:fx [[:dispatch [:open-modal :screen/photo-selector]] + [:dispatch [:photo-selector/get-photos-for-selected-album]] + [:dispatch [:photo-selector/camera-roll-get-albums]]]})) diff --git a/src/status_im/contexts/chat/messenger/photo_selector/style.cljs b/src/status_im/contexts/chat/messenger/photo_selector/style.cljs new file mode 100644 index 00000000000..794e38d879a --- /dev/null +++ b/src/status_im/contexts/chat/messenger/photo_selector/style.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.chat.messenger.photo-selector.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn gradient-container + [bottom-inset] + {:left 0 + :right 0 + :height (+ bottom-inset (if platform/ios? 51 85)) + :position :absolute + :bottom 0}) + +(def buttons-container + {:position :absolute + :flex-direction :row + :left 0 + :right 0 + :top 20 + :justify-content :center + :z-index 1}) + +(def clear-container + {:position :absolute + :right 20}) + +(defn image + [window-width index] + {:width (- (/ window-width 3) 0.67) + :height (/ window-width 3) + :margin-left (when (not= (mod index 3) 0) 1) + :margin-bottom 1 + :border-top-left-radius (when (= index 0) 20) + :border-top-right-radius (when (= index 2) 20)}) + +(defn overlay + [window-width theme] + {:position :absolute + :width (- (/ window-width 3) 0.67) + :height (/ window-width 3) + :background-color (colors/theme-colors colors/white-opa-40 colors/neutral-80-opa-40 theme)}) + +(def image-count + {:position :absolute + :top 8 + :right 8}) + +(def photo-limit-toast-container + {:top (if platform/ios? 6 16)}) diff --git a/src/status_im/contexts/chat/messenger/photo_selector/view.cljs b/src/status_im/contexts/chat/messenger/photo_selector/view.cljs new file mode 100644 index 00000000000..beb54419d13 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/photo_selector/view.cljs @@ -0,0 +1,157 @@ +(ns status-im.contexts.chat.messenger.photo-selector.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.linear-gradient :as linear-gradient] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.photo-selector.album-selector.view :as album-selector] + [status-im.contexts.chat.messenger.photo-selector.style :as style] + utils.collection + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def min-scroll-to-blur 5) + +(defn show-photo-limit-toast + [] + (rf/dispatch [:toasts/upsert + {:id :random-id + :type :negative + :container-style style/photo-limit-toast-container + :text (i18n/label :t/hit-photos-limit + {:max-photos constants/max-album-photos})}])) + +(defn on-press-confirm-selection + [selected close] + (rf/dispatch [:chat.ui/clear-sending-images]) + (doseq [item selected] + (rf/dispatch [:photo-selector/camera-roll-pick item])) + (close)) + +(defn confirm-button + [{:keys [selected-images sending-image close customization-color]}] + (when (not= selected-images sending-image) + [linear-gradient/linear-gradient + {:colors [:black :transparent] + :start {:x 0 :y 1} + :end {:x 0 :y 0} + :style (style/gradient-container safe-area/bottom)} + [quo/button + {:customization-color customization-color + :container-style {:align-self :stretch + :margin-horizontal 20 + :margin-top 12} + :on-press #(on-press-confirm-selection selected-images close) + :accessibility-label :confirm-selection} + (i18n/label :t/confirm-selection)]])) + +(defn clear-button + [album? selected blur-active?] + (when (and (not album?) (seq @selected)) + [rn/view {:style style/clear-container} + [quo/button + {:type :grey + :size 32 + :accessibility-label :clear + :on-press #(reset! selected []) + :background (when blur-active? :photo)} + (i18n/label :t/clear)]])) + +(defn remove-selected + [coll item] + (vec (remove #(= (:uri item) (:uri %)) coll))) + +(defn render-image + [item index _ {:keys [window-width selected]}] + (let [theme (quo.context/use-theme) + customization-color (rf/sub [:profile/customization-color]) + item-selected? (some #(= (:uri item) (:uri %)) @selected)] + [rn/touchable-opacity + {:on-press (fn [] + (if item-selected? + (swap! selected remove-selected item) + (if (>= (count @selected) constants/max-album-photos) + (show-photo-limit-toast) + (swap! selected conj item)))) + :allow-multiple-presses? true + :accessibility-label (str "image-" index)} + [rn/image + {:source {:uri (:uri item)} + :style (style/image window-width index)}] + (when item-selected? + [:<> + [rn/view {:style (style/overlay window-width theme)}] + [quo/counter + {:container-style style/image-count + :customization-color customization-color + :accessibility-label (str "count-" index)} + (inc (utils.collection/first-index #(= (:uri item) (:uri %)) @selected))]])])) + +(defn photo-selector + [{:keys [scroll-enabled? on-scroll current-scroll close] :as sheet}] + (let [album? (reagent/atom false) + customization-color (rf/sub [:profile/customization-color]) + sending-image (into [] (vals (rf/sub [:chats/sending-image]))) + selected-images (reagent/atom sending-image) + window-width (:width (rn/get-window))] + [:f> + (fn [] + (let [camera-roll-photos (rf/sub [:camera-roll/photos]) + end-cursor (rf/sub [:camera-roll/end-cursor]) + loading? (rf/sub [:camera-roll/loading-more]) + has-next-page? (rf/sub [:camera-roll/has-next-page]) + selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent)) + blur-active? (> @current-scroll min-scroll-to-blur) + window-height (:height (rn/get-window)) + top (reanimated/use-shared-value window-height) + show-blur? (and (not @album?) blur-active?) + dropdown-type (if show-blur? :grey :ghost) + dropdown-state (if @album? :active :default) + dropdown-background (when show-blur? :photo) + dropdown-on-press (fn [] + (if-not @album? + (do + (reset! album? true) + (reanimated/animate top 0)) + (do + (reanimated/animate top window-height) + (js/setTimeout #(reset! album? false) 300))))] + [rn/view {:style {:flex 1 :margin-top -20}} + (when @album? + [album-selector/album-selector sheet album? selected-album top]) + [:<> + [gesture/flat-list + {:key-fn #(hash (:uri %)) + :render-fn render-image + :render-data {:window-width window-width :selected selected-images} + :data camera-roll-photos + :num-columns 3 + :content-container-style {:width "100%" + :padding-bottom (+ safe-area/bottom 100) + :padding-top 64} + :on-scroll on-scroll + :scroll-enabled @scroll-enabled? + :on-end-reached (fn [] + (when (and (not loading?) has-next-page?) + (rf/dispatch [:photo-selector/camera-roll-loading-more true]) + (rf/dispatch [:photo-selector/get-photos-for-selected-album + end-cursor])))}] + [confirm-button + {:close close + :customization-color customization-color + :selected-images @selected-images + :sending-image sending-image}]] + [rn/view {:style style/buttons-container} + [quo/dropdown + {:type dropdown-type + :size :size-32 + :state dropdown-state + :on-press dropdown-on-press + :background dropdown-background} + selected-album] + [clear-button @album? selected-images blur-active?]]]))])) diff --git a/src/status_im/contexts/chat/messenger/placeholder/style.cljs b/src/status_im/contexts/chat/messenger/placeholder/style.cljs new file mode 100644 index 00000000000..6d4c2acca28 --- /dev/null +++ b/src/status_im/contexts/chat/messenger/placeholder/style.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.chat.messenger.placeholder.style + (:require [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(defn container + [theme on-layout-done?] + {:position :absolute + :padding-top safe-area/top + :top 0 + :left 0 + :right 0 + :bottom 0 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :opacity (if on-layout-done? 0 1) + :z-index (if on-layout-done? 0 2)}) diff --git a/src/status_im/contexts/chat/messenger/placeholder/view.cljs b/src/status_im/contexts/chat/messenger/placeholder/view.cljs new file mode 100644 index 00000000000..47540fca28d --- /dev/null +++ b/src/status_im/contexts/chat/messenger/placeholder/view.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.chat.messenger.placeholder.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.chat.messenger.placeholder.style :as style] + [utils.re-frame :as rf])) + +(defn- loading-skeleton + [] + [quo/skeleton-list + {:content :messages + :parent-height (:height (rn/get-window)) + :animated? false}]) + +(defn view + [on-layout-done?] + (let [theme (quo.context/use-theme) + screen-id (quo.context/use-screen-id) + chat-exist? (rf/sub [:chats/current-chat-exist?])] + [rn/view {:style (style/container theme @on-layout-done?)} + (when-not chat-exist? + [quo/page-nav + {:icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back screen-id])}]) + [loading-skeleton]])) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/effects.cljs b/src/status_im/contexts/communities/actions/accounts_selection/effects.cljs new file mode 100644 index 00000000000..3a4e6359632 --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/effects.cljs @@ -0,0 +1,103 @@ +(ns status-im.contexts.communities.actions.accounts-selection.effects + (:require + [clojure.string :as string] + [promesa.core :as promesa] + [schema.core :as schema] + [status-im.common.json-rpc.events :as rpc] + [status-im.contexts.wallet.rpc :as wallet-rpc] + [utils.re-frame :as rf] + [utils.signatures :as signatures])) + +(def ^:private ?addresses-to-reveal + [:or [:set string?] + [:sequential string?]]) + +(defn- generate-requests-for-signing + [pub-key community-id addresses-to-reveal] + (rpc/call-async "wakuext_generateJoiningCommunityRequestsForSigning" + false + pub-key + community-id + (or addresses-to-reveal []))) + +(schema/=> generate-requests-for-signing + [:=> + [:catn + [:community-id string?] + [:pub-key string?] + [:addresses-to-reveal ?addresses-to-reveal]] + :any]) + +(rf/reg-fx + :effects.community/generate-requests-for-signing + (fn [{:keys [pub-key community-id addresses-to-reveal on-success on-error]}] + (-> (generate-requests-for-signing pub-key community-id addresses-to-reveal) + (promesa/then (fn [requests] + (promesa/all + (for [{:keys [data account]} requests] + (promesa/let [hashed-data (wallet-rpc/hash-message-eip-191 data)] + {:message hashed-data + :address (string/lower-case account)}))))) + (promesa/then on-success) + (promesa/catch on-error)))) + +(defn- edit-shared-addresses-for-community + [community-id signatures addresses-to-reveal airdrop-address _share-future-addresses?] + (rpc/call-async "wakuext_editSharedAddressesForCommunity" + true + {:communityId community-id + :signatures (map signatures/adjust-legacy-ecdsa-signature signatures) + :addressesToReveal addresses-to-reveal + :airdropAddress airdrop-address})) + +(schema/=> edit-shared-addresses-for-community + [:=> + [:catn + [:community-id string?] + [:signatures [:sequential string?]] + [:addresses-to-reveal ?addresses-to-reveal] + [:airdrop-address string?] + [:_share-future-addresses? [:maybe boolean?]]] + :any]) + +(rf/reg-fx :effects.community/edit-shared-addresses + (fn [{:keys [on-success on-error community-id signatures addresses-to-reveal airdrop-address + share-future-addresses?]}] + (-> (edit-shared-addresses-for-community community-id + signatures + addresses-to-reveal + airdrop-address + share-future-addresses?) + (promesa/then on-success) + (promesa/catch on-error)))) + +(defn- request-to-join + [community-id signatures addresses-to-reveal airdrop-address share-future-addresses?] + (rpc/call-async "wakuext_requestToJoinCommunity" + true + {:communityId community-id + :signatures (map signatures/adjust-legacy-ecdsa-signature signatures) + :addressesToReveal addresses-to-reveal + :airdropAddress airdrop-address + :shareFutureAddresses share-future-addresses?})) + +(schema/=> request-to-join + [:=> + [:catn + [:community-id string?] + [:signatures [:sequential string?]] + [:addresses-to-reveal ?addresses-to-reveal] + [:airdrop-address string?] + [:share-future-addresses? [:maybe boolean?]]] + :any]) + +(rf/reg-fx :effects.community/request-to-join + (fn [{:keys [on-success on-error community-id signatures addresses-to-reveal airdrop-address + share-future-addresses?]}] + (-> (request-to-join community-id + signatures + addresses-to-reveal + airdrop-address + share-future-addresses?) + (promesa/then on-success) + (promesa/catch on-error)))) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/events.cljs b/src/status_im/contexts/communities/actions/accounts_selection/events.cljs new file mode 100644 index 00000000000..7afd1807419 --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/events.cljs @@ -0,0 +1,135 @@ +(ns status-im.contexts.communities.actions.accounts-selection.events + (:require + status-im.contexts.communities.actions.accounts-selection.effects + [status-im.contexts.communities.utils :as utils] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn initialize-permission-addresses + [{:keys [db]} [community-id]] + (let [{:keys [joined]} (get-in db [:communities community-id])] + {:fx [(if joined + [:dispatch + [:communities/get-revealed-accounts community-id + [:communities/do-init-permission-addresses community-id]]] + [:dispatch [:communities/do-init-permission-addresses community-id]])]})) + +(rf/reg-event-fx :communities/initialize-permission-addresses initialize-permission-addresses) + +(defn do-init-permission-addresses + [{:keys [db]} [community-id revealed-accounts share-future-addresses?]] + (let [wallet-accounts (utils/sorted-operable-non-watch-only-accounts db) + addresses-to-reveal (if (seq revealed-accounts) + (set (keys revealed-accounts)) + ;; Reveal all addresses as fallback. + (set (map :address wallet-accounts))) + ;; When there are no revealed addresses, such as when joining a + ;; community, use first address for airdrops. + airdrop-address (or (->> revealed-accounts + vals + (filter :airdrop-address?) + first + :address) + (->> wallet-accounts + first + :address))] + {:db (-> db + (assoc-in [:communities/selected-share-all-addresses community-id] share-future-addresses?) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal) + (assoc-in [:communities/all-airdrop-addresses community-id] airdrop-address)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + ;; Pre-fetch permissions check so that when first opening the + ;; Addresses for Permissions screen the highest permission role is + ;; already available and no incorrect information flashes on screen. + [:dispatch + [:communities/check-permissions-to-join-during-selection community-id + addresses-to-reveal]]]})) + +(rf/reg-event-fx :communities/do-init-permission-addresses do-init-permission-addresses) + +(defn sign-shared-addresses + "This event will effectively persist the choice of addresses and airdrop address + in status-go. It can either take addresses to share AND an airdrop address, or + just addresses to share OR an airdrop address. This is because when the user + is selecting an airdrop address, we must submit to status-go the current + choice of addresses to share, and vice-versa. If we omit addresses to share, + status-go will default to all available." + [{:keys [db]} + [{:keys [community-id addresses]} :as args]] + (let [pub-key (get-in db [:profile/profile :public-key]) + addresses-to-reveal (if (seq addresses) + (set addresses) + (get-in db [:communities/all-addresses-to-reveal community-id]))] + {:fx [[:effects.community/generate-requests-for-signing + {:pub-key pub-key + :community-id community-id + :addresses-to-reveal addresses-to-reveal + :on-success (fn [signing-requests] + (rf/dispatch + [:standard-auth/authorize-and-sign + {:sign-payload signing-requests + :on-sign-success #(rf/dispatch + [:communities/edit-shared-addresses + (assoc args :signature-data %)]) + :on-sign-error #(rf/dispatch + [:communities/edit-shared-addresses-failure + community-id %]) + :auth-button-label (i18n/label :t/confirm-changes)}])) + :on-error (fn [error] + (rf/dispatch [:communities/edit-shared-addresses-failure community-id + error]))}]]})) + +(rf/reg-event-fx :communities/sign-shared-addresses sign-shared-addresses) + +(defn edit-shared-addresses + [{:keys [db]} + [{:keys [community-id signature-data airdrop-address share-future-addresses? on-success]}]] + (let [pub-key (get-in db [:profile/profile :public-key]) + addresses-to-reveal (set (map :address signature-data)) + new-airdrop-address (if (contains? addresses-to-reveal airdrop-address) + airdrop-address + (->> (utils/sorted-operable-non-watch-only-accounts db) + (filter #(contains? addresses-to-reveal (:address %))) + first + :address)) + share-future-addresses? (if (nil? share-future-addresses?) + (get-in db [:communities/selected-share-all-addresses community-id]) + share-future-addresses?) + signatures (utils/extract-join-request-signatures signature-data)] + {:fx [[:effects.community/edit-shared-addresses + {:community-id community-id + :signatures signatures + :pub-key pub-key + :addresses-to-reveal addresses-to-reveal + :share-future-addresses? share-future-addresses? + :airdrop-address new-airdrop-address + :on-success (fn [] + (when (fn? on-success) + (on-success addresses-to-reveal + new-airdrop-address + share-future-addresses?)) + (rf/dispatch [:communities/edit-shared-addresses-success + community-id addresses-to-reveal + airdrop-address])) + :on-error (fn [error] + [:communities/edit-shared-addresses-failure community-id + error])}]]})) + +(rf/reg-event-fx :communities/edit-shared-addresses edit-shared-addresses) + +(rf/reg-event-fx :communities/edit-shared-addresses-success + (fn [_ [community-id addresses-to-reveal airdrop-address share-future-addresses?]] + {:fx [[:dispatch [:communities/set-airdrop-address community-id airdrop-address]] + [:dispatch [:communities/set-share-all-addresses community-id share-future-addresses?]] + [:dispatch [:communities/set-addresses-to-reveal community-id addresses-to-reveal]]]})) + +(rf/reg-event-fx :communities/edit-shared-addresses-failure + (fn [{:keys [db]} [community-id error]] + (log/error "failed to edit shared addresses" + {:event :communities/sign-shared-addresses + :community-id community-id + :error error}) + {:db (assoc-in db [:password-authentication :error] error)})) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/events_test.cljs b/src/status_im/contexts/communities/actions/accounts_selection/events_test.cljs new file mode 100644 index 00000000000..015efba642d --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/events_test.cljs @@ -0,0 +1,177 @@ +(ns status-im.contexts.communities.actions.accounts-selection.events-test + (:require [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.contexts.communities.actions.accounts-selection.events :as sut])) + +(def community-id "0x99") + +(def wallet-accounts + {"0xA" {:address "0xA" + :watch-only? true + :operable? true + :position 2 + :color :red + :emoji "🦇"} + "0xB" {:address "0xB" + :operable? true + :position 0 + :color :blue + :emoji "🐈"} + "0xC" {:address "0xC" + :operable? true + :position 1 + :color :orange + :emoji "🛏️"} + "0xD" {:address "0xD" + :operable? false + :position 3 + :color :pink + :emoji "🦩"}}) + +(deftest initialize-permission-addresses-test + (testing "fetches revealed accounts when joined" + (let [cofx {:db {:communities {community-id {:joined true}}}}] + (is (match? + {:fx [[:dispatch + [:communities/get-revealed-accounts community-id + [:communities/do-init-permission-addresses community-id]]]]} + (sut/initialize-permission-addresses cofx [community-id]))))) + + (testing "does not fetch revealed accounts when not joined" + (let [cofx {:db {:communities {community-id {:joined false}} + :communities/my-pending-requests-to-join {}}}] + (is (match? + {:fx [[:dispatch [:communities/do-init-permission-addresses community-id]]]} + (sut/initialize-permission-addresses cofx [community-id])))))) + +(deftest do-init-permission-addresses-test + (testing "uses already revealed accounts to initialize shareable accounts" + (let [cofx {:db {:wallet {:accounts wallet-accounts}}} + revealed-accounts {"0xC" {:address "0xC" + :airdrop-address? true + :position 1}} + airdrop-address "0xC" + addresses-to-reveal #{"0xC"}] + (is + (match? + {:db (-> (:db cofx) + (assoc-in [:communities/selected-share-all-addresses community-id] true) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal) + (assoc-in [:communities/all-airdrop-addresses community-id] airdrop-address)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + ;; Pre-fetch permissions check so that when first opening the + ;; Addresses for Permissions screen the highest permission role is + ;; already available and no incorrect information flashes on screen. + [:dispatch + [:communities/check-permissions-to-join-during-selection community-id + addresses-to-reveal]]]} + (sut/do-init-permission-addresses cofx [community-id revealed-accounts true]))))) + + ;; Expect to mark all addresses to be revealed and first one to receive + ;; airdrops when no addresses were previously revealed. + (testing "handles case where there are no previously revealed addresses" + (let [cofx {:db {:wallet {:accounts wallet-accounts}}} + addresses-to-reveal #{"0xB" "0xC"} + revealed-accounts []] + (is + (match? + {:db (-> (:db cofx) + (assoc-in [:communities/selected-share-all-addresses community-id] true) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + [:dispatch + [:communities/check-permissions-to-join-during-selection community-id + addresses-to-reveal]]]} + (sut/do-init-permission-addresses cofx [community-id revealed-accounts true])))))) + +(deftest sign-shared-addresses-test + (testing + "when addresses to reveal is not passed, fallback to all wallet addresses" + (let [pub-key "abcdef" + revealed-addresses #{"0xB" "0xC"} + share-future-addresses? true + cofx {:db {:profile/profile {:public-key pub-key} + :communities/all-addresses-to-reveal {community-id + revealed-addresses}}} + airdrop-address "0xB" + actual (sut/sign-shared-addresses + cofx + [{:community-id community-id + :airdrop-address airdrop-address + :share-future-addresses? share-future-addresses? + :on-success identity}])] + (is (match? + {:fx [[:effects.community/generate-requests-for-signing + {:community-id community-id + :pub-key pub-key + :addresses-to-reveal revealed-addresses + :on-success fn? + :on-error fn?}]]} + actual))))) + +(deftest edit-shared-addresses-test + (testing + "signatures and addresses should be derived from signature-data and if airdrop address is not passed, + should fallback to first operable wallet address." + (let [pub-key "abcdef" + cofx {:db {:profile/profile {:public-key pub-key} + :wallet {:accounts wallet-accounts} + ;; 0xA is watch-only + :communities/all-addresses-to-reveal {community-id #{"0xA" "0xB" "0xC"}}}} + + actual (sut/edit-shared-addresses + cofx + [{:community-id community-id + :share-future-addresses? true + :signature-data [{:address "0xB" + :signature "sigB" + :message "mesB"} + {:address "0xC" + :signature "sigC" + :message "mesC"}] + :on-success identity}])] + (is (match? + {:fx [[:effects.community/edit-shared-addresses + {:community-id community-id + :signatures '("0xsigB" "0xsigC") + :pub-key pub-key + :addresses-to-reveal #{"0xB" "0xC"} + :share-future-addresses? true + :airdrop-address "0xB" + :on-success fn? + :on-error fn?}]]} + actual)))) + + (testing "if share-future-addresses flag is not passed, should use the db value" + (let [pub-key "abcdef" + cofx {:db {:profile/profile {:public-key pub-key} + :wallet {:accounts wallet-accounts} + :communities/selected-share-all-addresses {community-id false} + :communities/all-addresses-to-reveal {community-id #{"0xA" "0xB"}}}} + + actual (sut/edit-shared-addresses + cofx + [{:community-id community-id + :airdrop-address "0xB" + :signature-data [{:address "0xA" + :signature "sigA" + :message "mesA"} + {:address "0xB" + :signature "sigB" + :message "mesB"}] + :on-success identity}])] + (is (match? + {:fx [[:effects.community/edit-shared-addresses + {:community-id community-id + :signatures '("0xsigA" "0xsigB") + :pub-key pub-key + :addresses-to-reveal #{"0xA" "0xB"} + :share-future-addresses? false + :airdrop-address "0xB" + :on-success fn? + :on-error fn?}]]} + actual))))) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/style.cljs b/src/status_im/contexts/communities/actions/accounts_selection/style.cljs new file mode 100644 index 00000000000..f0258f0db49 --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/style.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.communities.actions.accounts-selection.style + (:require + [quo.foundations.colors :as colors])) + +(def screen-horizontal-padding 20) + +(def container + {:flex 1 + :margin-top -12}) + +(def section-title + {:padding-top 12 + :padding-bottom 4 + :padding-horizontal screen-horizontal-padding}) + +(defn bottom-actions + [theme] + {:height 48 + :margin-vertical 12 + :padding-horizontal screen-horizontal-padding + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/view.cljs b/src/status_im/contexts/communities/actions/accounts_selection/view.cljs new file mode 100644 index 00000000000..80f4acb81e4 --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/view.cljs @@ -0,0 +1,165 @@ +(ns status-im.contexts.communities.actions.accounts-selection.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.communities.actions.accounts-selection.style :as style] + [status-im.contexts.communities.actions.addresses-for-permissions.view :as addresses-for-permissions] + [status-im.contexts.communities.actions.airdrop-addresses.view :as airdrop-addresses] + [status-im.contexts.communities.actions.community-rules.view :as community-rules] + [status-im.contexts.communities.actions.permissions-sheet.view :as permissions-sheet] + [status-im.contexts.communities.utils :as communities.utils] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn- bottom-authentication + [{:keys [id]}] + (let [theme (quo.context/use-theme) + can-edit-addresses? (rf/sub [:communities/can-edit-shared-addresses? id]) + revealed-accounts (rf/sub [:communities/accounts-to-reveal id]) + {:keys [color]} (rf/sub [:communities/community id]) + join-requests-for-signing (rf/sub [:communities/join-requests-for-signing-by-id id]) + confirm-choices (rn/use-callback + (fn [signature-data] + (rf/dispatch [:communities/request-to-join + {:community-id id + :signature-data signature-data}]) + (navigate-back)) + [can-edit-addresses? id])] + + (rn/use-effect + #(when (and (not can-edit-addresses?) (seq revealed-accounts)) + (rf/dispatch [:communities/generate-requests-for-signing id])) + [can-edit-addresses? revealed-accounts]) + + (when-not can-edit-addresses? + [rn/view {:style (style/bottom-actions theme)} + [standard-auth/slide-sign + {:sign-payload join-requests-for-signing + :disabled? (not (seq join-requests-for-signing)) + :size :size-48 + :track-text (i18n/label :t/slide-to-request-to-join) + :auth-button-label (if can-edit-addresses? + (i18n/label :t/edit-shared-addresses) + (i18n/label :t/request-to-join)) + :customization-color color + :on-success confirm-choices}]]))) + +(defn view + [] + (let [{id :community-id} (quo.context/use-screen-params) + {:keys [name images joined]} (rf/sub [:communities/community id]) + has-permissions? (rf/sub [:communities/has-permissions? id]) + airdrop-account (rf/sub [:communities/airdrop-account id]) + revealed-accounts (rf/sub [:communities/accounts-to-reveal id]) + revealed-accounts-count (count revealed-accounts) + wallet-accounts-count (count (rf/sub [:wallet/operable-accounts])) + addresses-shared-text (if (= revealed-accounts-count wallet-accounts-count) + (i18n/label :t/all-addresses) + (i18n/label-pluralize + revealed-accounts-count + :t/address-count)) + {:keys [highest-permission-role]} (rf/sub [:community/token-gated-overview id]) + highest-role-text (i18n/label (communities.utils/role->translation-key + highest-permission-role + :t/member)) + can-edit-addresses? (rf/sub [:communities/can-edit-shared-addresses? id]) + show-addresses-for-permissions (rn/use-callback + (fn [] + (if can-edit-addresses? + (rf/dispatch [:open-modal + :screen/addresses-for-permissions + {:community-id id}]) + (rf/dispatch [:show-bottom-sheet + {:community-id id + :content + addresses-for-permissions/view}]))) + [can-edit-addresses?]) + show-airdrop-addresses (rn/use-callback + (fn [] + (if can-edit-addresses? + (rf/dispatch [:open-modal :screen/address-for-airdrop + {:community-id id}]) + (rf/dispatch [:show-bottom-sheet + {:community-id id + :content airdrop-addresses/view}]))) + [can-edit-addresses?]) + open-permission-sheet (rn/use-callback + (fn [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [permissions-sheet/view id])}])) + [id])] + (rn/use-mount + (fn [] + (rf/dispatch [:communities/initialize-permission-addresses id]))) + + [rn/safe-area-view {:style style/container} + [quo/page-nav + (cond-> {:text-align :left + :icon-name :i/close + :on-press navigate-back + :accessibility-label :back-button} + (and has-permissions? (ff/enabled? ::ff/community.view-token-requirements)) + (assoc :right-side + [{:icon-left :i/unlocked + :on-press open-permission-sheet + :label (i18n/label :t/permissions)}]))] + [quo/page-top + {:title (if can-edit-addresses? + (i18n/label :t/edit-shared-addresses) + (i18n/label :t/request-to-join)) + :description :context-tag + :context-tag {:type :community + :size 24 + :community-logo (get-in images [:thumbnail :uri]) + :community-name name}}] + [gesture/scroll-view + [:<> + (when-not can-edit-addresses? + [quo/text + {:style style/section-title + :accessibility-label :community-rules-title + :weight :semi-bold + :size :paragraph-1} + (i18n/label :t/address-to-share)]) + [quo/category + {:list-type :settings + :data [{:title (if joined + (i18n/label :t/you-are-a-role {:role highest-role-text}) + (i18n/label :t/join-as {:role highest-role-text})) + :on-press show-addresses-for-permissions + :description :text + :action :arrow + :label :preview + :label-props {:type :accounts + :data revealed-accounts} + :preview-size :size-32 + :description-props {:text addresses-shared-text}} + {:title (i18n/label :t/for-airdrops) + :on-press show-airdrop-addresses + :description :text + :action :arrow + :label :preview + :label-props {:type :accounts + :data [airdrop-account]} + :preview-size :size-32 + :description-props {:text (:name airdrop-account)}}]}] + (when-not can-edit-addresses? + [quo/text + {:style style/section-title + :accessibility-label :community-rules-title + :weight :semi-bold + :size :paragraph-1} + (i18n/label :t/community-rules)]) + (when-not can-edit-addresses? + [community-rules/view id])]] + [bottom-authentication {:id id}]])) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/events.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/events.cljs new file mode 100644 index 00000000000..bebc79d8794 --- /dev/null +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/events.cljs @@ -0,0 +1,159 @@ +(ns status-im.contexts.communities.actions.addresses-for-permissions.events + (:require + [schema.core :as schema] + [status-im.contexts.communities.utils :as utils] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn get-permissioned-balances + [{[event-id _] :original-event} [community-id]] + {:fx [[:json-rpc/call + [{:method :wakuext_getCommunityPermissionedBalances + :params [{:communityId community-id}] + :on-success [:communities/get-permissioned-balances-success community-id] + :on-error (fn [error] + (log/error "failed to get balances" + {:community-id community-id + :event event-id + :error error}))}]]]}) + +(schema/=> get-permissioned-balances + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args [:schema [:catn [:community-id :string]]]]] + [:map {:closed true} + [:fx + [:tuple + [:tuple [:= :json-rpc/call] :schema.common/rpc-call]]]]]) + +(rf/reg-event-fx :communities/get-permissioned-balances get-permissioned-balances) + +(defn get-permissioned-balances-success + [{:keys [db]} [community-id response]] + {:db (assoc-in db [:communities/permissioned-balances community-id] response)}) + +(def ^:private ?account-address keyword?) + +(def ^:private ?permissioned-balances-response + [:map-of + ?account-address + [:sequential + [:map + [:type int?] + [:symbol string?] + [:decimals int?] + [:amount string?]]]]) + +(schema/=> get-permissioned-balances-success + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:schema + [:catn + [:community-id string?] + [:response ?permissioned-balances-response]]]]] + map?]) + +(rf/reg-event-fx :communities/get-permissioned-balances-success get-permissioned-balances-success) + +(rf/reg-event-fx :communities/addresses-for-permissions-cancel-request + (fn [_ [request-id]] + {:fx [[:dispatch [:communities/cancel-request-to-join request-id]] + [:dispatch [:pop-to-root :screen/shell-stack]] + [:dispatch [:hide-bottom-sheet]]]})) + +(rf/reg-event-fx :communities/addresses-for-permissions-leave + (fn [_ [community-id]] + {:fx [[:dispatch [:communities/leave community-id]] + [:dispatch [:hide-bottom-sheet]] + [:dispatch [:dismiss-modal :screen/addresses-for-permissions]] + [:dispatch [:pop-to-root :screen/shell-stack]]]})) + +(defn check-permissions-to-join-for-selection + [{:keys [db]} [community-id addresses]] + (let [request-id (rand-int 10000000)] + (if (empty? addresses) + ;; When there are no addresses we can't just check permissions, otherwise + ;; status-go will consider all possible addresses and the user will see the + ;; incorrect highest permission role. + {:db (update db :communities/permissions-checks-for-selection dissoc community-id)} + {:db (update-in db + [:communities/permissions-checks-for-selection community-id] + assoc + :checking? true + :latest-request-id request-id) + :fx [[:json-rpc/call + [{:method :wakuext_checkPermissionsToJoinCommunity + :params [{:communityId community-id :addresses addresses}] + :id request-id + :on-success [:communities/check-permissions-to-join-during-selection-success + community-id] + :on-error [:communities/check-permissions-to-join-during-selection-failure + community-id addresses]}]]]}))) + +;; This event should be used to check permissions temporarily because it won't +;; mutate the state `:communities/permissions-check` (used by many other +;; screens). +(rf/reg-event-fx :communities/check-permissions-to-join-during-selection + check-permissions-to-join-for-selection) + +(rf/reg-event-fx :communities/check-permissions-to-join-during-selection-success + (fn [{:keys [db]} [community-id result request-id]] + (when (= (get-in db [:communities/permissions-checks-for-selection community-id :latest-request-id]) + request-id) + {:db (assoc-in db + [:communities/permissions-checks-for-selection community-id] + {:checking? false + :based-on-client-selection? true + :check result})}))) + +(rf/reg-event-fx :communities/check-permissions-to-join-during-selection-failure + (fn [_ [community-id addresses error]] + (log/error "failed to check permissions for currently selected addresses" + {:event :communities/check-permissions-to-join-during-selection + :community-id community-id + :addresses addresses + :error error}))) + +(defn set-permissioned-accounts + [{:keys [db]} [community-id addresses-to-reveal]] + (let [addresses-to-reveal (set addresses-to-reveal) + wallet-accounts (utils/sorted-operable-non-watch-only-accounts db) + current-airdrop-address (get-in db [:communities/all-airdrop-addresses community-id]) + new-airdrop-address (if (contains? addresses-to-reveal current-airdrop-address) + current-airdrop-address + (->> wallet-accounts + (filter #(contains? addresses-to-reveal (:address %))) + first + :address))] + {:db (-> db + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal) + (assoc-in [:communities/all-airdrop-addresses community-id] new-airdrop-address)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + [:dispatch [:hide-bottom-sheet]]]})) + +(rf/reg-event-fx :communities/set-addresses-to-reveal set-permissioned-accounts) + +(defn set-share-all-addresses + [{:keys [db]} [community-id new-value]] + (let [current-addresses (get-in db [:communities/all-addresses-to-reveal community-id]) + addresses-to-reveal (if new-value + (->> (utils/sorted-operable-non-watch-only-accounts db) + (map :address) + set) + current-addresses)] + {:db (-> db + (assoc-in [:communities/selected-share-all-addresses community-id] new-value) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal)) + ;; We should check permissions again because the flag is being enabled and + ;; different addresses will be revealed. + :fx [(when new-value + [:dispatch + [:communities/check-permissions-to-join-during-selection + community-id addresses-to-reveal]])]})) + +(rf/reg-event-fx :communities/set-share-all-addresses set-share-all-addresses) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/events_test.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/events_test.cljs new file mode 100644 index 00000000000..0c4e0ceded9 --- /dev/null +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/events_test.cljs @@ -0,0 +1,112 @@ +(ns status-im.contexts.communities.actions.addresses-for-permissions.events-test + (:require + [cljs.test :refer [deftest is testing]] + [status-im.contexts.communities.actions.addresses-for-permissions.events :as sut])) + +(def community-id "0x1") + +(deftest get-permissioned-balances-test + (let [cofx {:db {}}] + (is (match? {:fx [[:json-rpc/call + [{:method :wakuext_getCommunityPermissionedBalances + :params [{:communityId community-id}] + :on-success [:communities/get-permissioned-balances-success community-id] + :on-error fn?}]]]} + (sut/get-permissioned-balances cofx [community-id]))))) + +(deftest set-permissioned-accounts-test + (testing "new accounts contain the current airdrop address" + (let [cofx {:db {:communities/all-addresses-to-reveal + {community-id #{"0xA" "0xB"}}}} + addresses-to-reveal ["0xA"] + expected-addresses-to-reveal (set addresses-to-reveal)] + (is (match? + {:db {:communities/all-addresses-to-reveal + {community-id expected-addresses-to-reveal}} + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id expected-addresses-to-reveal :based-on-client-selection]] + [:dispatch [:hide-bottom-sheet]]]} + (sut/set-permissioned-accounts cofx [community-id addresses-to-reveal]))))) + + (testing "new accounts do not contain the current airdrop address" + (let [cofx + {:db {:communities/all-addresses-to-reveal {community-id #{"0xA" "0xB" "0xC"}} + :communities/all-airdrop-addresses {community-id "0xB"} + :wallet {:accounts {"0xB" {:address "0xB" + :operable? true + :position 0} + "0xA" {:address "0xA" + :operable? true + :position 1} + "0xC" {:address "0xC" + :operable? true + :position 2}}}}} + addresses-to-reveal ["0xA" "0xC"]] + (is (match? + {:db {:communities/all-addresses-to-reveal + {community-id (set addresses-to-reveal)} + :communities/all-airdrop-addresses + {community-id "0xA"}} + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id (set addresses-to-reveal) :based-on-client-selection]] + [:dispatch [:hide-bottom-sheet]]]} + (sut/set-permissioned-accounts cofx [community-id addresses-to-reveal])))))) + +(deftest set-share-all-addresses-test + (testing "sets flag from false -> true will mark all addresses to be revealed" + (let [cofx {:db + {:wallet + {:accounts {"0xB" {:address "0xB" :operable? true :position 0} + "0xA" {:address "0xA" :operable? true :position 1} + "0xC" {:address "0xC" :operable? true :position 2}}} + :communities/all-addresses-to-reveal {community-id #{"0xA"}} + :communities/selected-share-all-addresses {community-id false}}} + addresses-to-reveal #{"0xA" "0xB" "0xC"}] + (is (match? + {:db {:communities/selected-share-all-addresses {community-id true} + :communities/all-addresses-to-reveal + {community-id addresses-to-reveal}} + :fx [[:dispatch + [:communities/check-permissions-to-join-during-selection + community-id addresses-to-reveal]]]} + (sut/set-share-all-addresses cofx [community-id true]))))) + + (testing "sets flag from true -> false will not change addresses to be revealed" + (let [cofx {:db {:communities/all-addresses-to-reveal {community-id #{"0xA"}}}}] + (is (match? + {:db {:communities/selected-share-all-addresses {community-id false} + :communities/all-addresses-to-reveal {community-id #{"0xA"}}} + :fx [nil]} + (sut/set-share-all-addresses cofx [community-id false])))))) + +(deftest check-permissions-to-join-for-selection-test + (testing "when no addresses are passed don't check permissions" + (let [addresses [] + cofx {:db {:foo :bar + :communities/permissions-checks-for-selection + {"0xC" :another-check + community-id :some-check}}}] + (is (match? + {:db {:foo :bar + :communities/permissions-checks-for-selection {"0xC" :another-check}}} + (sut/check-permissions-to-join-for-selection cofx [community-id addresses]))))) + + (testing "when there are addresses to check permissions and not currently checking" + (let [addresses ["0xA"] + cofx {:db {:communities/permissions-checks-for-selection + {"other-comm-id" {} + community-id {:checking? false}}}}] + (is (match? + {:db {:communities/permissions-checks-for-selection + {"other-comm-id" {} + community-id {:checking? true}}} + :fx [[:json-rpc/call + [{:method :wakuext_checkPermissionsToJoinCommunity + :params [{:communityId community-id :addresses addresses}] + :on-success [:communities/check-permissions-to-join-during-selection-success + community-id] + :on-error [:communities/check-permissions-to-join-during-selection-failure + community-id addresses]}]]]} + (sut/check-permissions-to-join-for-selection cofx [community-id addresses])))))) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/style.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/style.cljs new file mode 100644 index 00000000000..5b5c0b177ce --- /dev/null +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.communities.actions.addresses-for-permissions.style) + +(def drawer-body + {:padding-top 4 + :padding-horizontal 20 + :padding-bottom 12}) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/view.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/view.cljs new file mode 100644 index 00000000000..8bbc8112b94 --- /dev/null +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/view.cljs @@ -0,0 +1,326 @@ +(ns status-im.contexts.communities.actions.addresses-for-permissions.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.constants :as constants] + [status-im.contexts.communities.actions.addresses-for-permissions.style :as style] + [status-im.contexts.communities.actions.permissions-sheet.view :as permissions-sheet] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) + +(defn- role-keyword + [role] + (condp = role + constants/community-token-permission-become-token-owner :token-owner + constants/community-token-permission-become-token-master :token-master + constants/community-token-permission-become-admin :admin + constants/community-token-permission-become-member :member + nil)) + +(defn- balances->components-props + [balances images-by-symbol] + (for [{:keys [amount decimals type name] sym :symbol :as balance} balances] + (cond-> balance + true + (assoc :type + (condp = type + constants/community-token-type-erc20 :token + constants/community-token-type-erc721 :collectible + :token)) + + (= type constants/community-token-type-erc721) + (assoc :collectible-name name + :collectible-img-src (images-by-symbol sym)) + + (= type constants/community-token-type-erc20) + (assoc :amount (str (money/token->unit amount decimals)) + :token (:symbol balance) + :token-img-src (images-by-symbol sym))))) + +(defn- confirm-discard-drawer + [community-id] + (let [{:keys [name logo color]} (rf/sub [:communities/for-context-tag community-id])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :community + :title (i18n/label :t/discard-changes?) + :community-name name + :community-logo logo + :customization-color color}] + [rn/view {:style style/drawer-body} + [quo/text (i18n/label :t/all-changes-will-be-discarded)]] + [quo/bottom-actions + {:actions :two-actions + + :button-one-label (i18n/label :t/discard) + :button-one-props {:customization-color color + :on-press + (fn [] + (rf/dispatch [:dismiss-modal :screen/addresses-for-permissions]) + (rf/dispatch [:hide-bottom-sheet]))} + + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) + +(defn- leave-community-drawer + [community-id outro-message] + (let [{:keys [name logo color]} (rf/sub [:communities/for-context-tag community-id])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :community + :title (i18n/label :t/leave-community?) + :community-name name + :community-logo logo + :customization-color color}] + [rn/view {:style style/drawer-body} + [quo/text + (if (string/blank? outro-message) + (i18n/label :t/leave-community-farewell) + outro-message)]] + [quo/bottom-actions + {:actions :two-actions + + :button-one-label (i18n/label :t/confirm-and-leave) + :button-one-props {:customization-color color + :on-press + #(rf/dispatch [:communities/addresses-for-permissions-leave community-id])} + + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) + +(defn- account-item + [{:keys [customization-color address name emoji]} _ _ + [addresses-to-reveal set-addresses-to-reveal community-id community-color share-all-addresses?]] + (let [balances (rf/sub [:communities/permissioned-balances-by-address community-id address]) + images-by-symbol (rf/sub [:communities/token-images-by-symbol community-id]) + checked? (contains? addresses-to-reveal address) + toggle-address (fn [] + (let [new-addresses (if checked? + (disj addresses-to-reveal address) + (conj addresses-to-reveal address))] + (set-addresses-to-reveal new-addresses)))] + [quo/account-permissions + {:account {:name name + :address address + :emoji emoji + :customization-color customization-color} + :token-details (when-not share-all-addresses? + (balances->components-props balances images-by-symbol)) + :checked? checked? + :disabled? share-all-addresses? + :on-change toggle-address + :container-style {:margin-bottom 8} + :customization-color community-color}])) + +(defn- page-top + [{:keys [community-id identical-choices? can-edit-addresses?]}] + (let [{:keys [name logo color]} (rf/sub [:communities/for-context-tag community-id]) + has-permissions? (rf/sub [:communities/has-permissions? community-id]) + confirm-discard-changes (rn/use-callback + (fn [] + (if identical-choices? + (rf/dispatch [:dismiss-modal :screen/addresses-for-permissions]) + (rf/dispatch [:show-bottom-sheet + {:content (fn [] + [confirm-discard-drawer + community-id])}]))) + [identical-choices? community-id]) + open-permission-sheet (rn/use-callback + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] + [permissions-sheet/view + community-id])}])) + [community-id])] + [:<> + (when can-edit-addresses? + [quo/page-nav + {:type :no-title + :icon-name :i/arrow-left + :on-press confirm-discard-changes}]) + + (if can-edit-addresses? + [quo/page-top + {:title (i18n/label :t/addresses-for-permissions) + :title-accessibility-label :title-label + :description :context-tag + :context-tag {:type :community + :community-logo logo + :community-name name}}] + [quo/drawer-top + (cond-> {:type :context-tag + :title (i18n/label :t/addresses-for-permissions) + :context-tag-type :community + :community-name name + :community-logo logo + :customization-color color} + (and has-permissions? (ff/enabled? ::ff/community.view-token-requirements)) + (assoc :button-icon :i/info + :button-type :grey + :on-button-press open-permission-sheet))])])) + +(defn selection-bottom-actions + [id + {:keys [flag-share-all-addresses + addresses-to-reveal + cancel-selection + can-edit-addresses? + identical-choices?]}] + (let [color (rf/sub [:communities/community-color id]) + outro-message (rf/sub [:communities/community-outro-message id]) + joined (rf/sub [:communities/community-joined id]) + checking? (rf/sub [:communities/permissions-check-for-selection-checking? id]) + + leave-community + (rn/use-callback + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [leave-community-drawer id outro-message])}])) + [outro-message]) + + confirm-changes + (fn [] + (if can-edit-addresses? + (do + (rf/dispatch [:communities/sign-shared-addresses + {:community-id id + :addresses addresses-to-reveal + :on-success (fn [] + (rf/dispatch [:dismiss-modal + :screen/addresses-for-permissions]) + (rf/dispatch [:hide-bottom-sheet]))}]) + (rf/dispatch [:communities/set-share-all-addresses id flag-share-all-addresses])) + (do + (rf/dispatch [:communities/set-share-all-addresses id flag-share-all-addresses]) + (rf/dispatch [:communities/set-addresses-to-reveal id addresses-to-reveal])))) + highest-role (rf/sub [:communities/highest-role-for-selection id]) + [unmodified-role _] (rn/use-state highest-role)] + + [quo/bottom-actions + (cond-> {:role (role-keyword highest-role) + :description (if highest-role + :top + :top-error) + :button-one-props {:customization-color color + :on-press confirm-changes + :disabled? (or checking? + (empty? addresses-to-reveal) + (not highest-role) + identical-choices?)}} + can-edit-addresses? + (-> + (assoc :actions :one-action + :button-one-label (if (and joined (not highest-role)) + (i18n/label :t/leave-community) + (i18n/label :t/confirm-changes)) + :description-top-text (cond + (and joined (= highest-role unmodified-role)) + (i18n/label :t/you-are-a) + + (and joined (not= highest-role unmodified-role)) + (i18n/label :t/you-will-be-a)) + :error-message (when (and joined (not highest-role)) + (i18n/label :t/membership-requirements-not-met))) + (update :button-one-props + merge + (cond (and joined (not highest-role)) + {:type :danger + :disabled? false + :on-press leave-community}))) + + (not can-edit-addresses?) + (assoc :actions :two-actions + :button-one-label (i18n/label :t/confirm-changes) + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press cancel-selection} + :error-message (cond + (empty? addresses-to-reveal) + (i18n/label :t/no-addresses-selected) + + (not highest-role) + (i18n/label :t/addresses-dont-contain-tokens-needed))))])) + +(defn view + [] + (let [{id :community-id} (quo.context/use-screen-params) + + color (rf/sub [:communities/community-color id]) + + can-edit-addresses? (rf/sub [:communities/can-edit-shared-addresses? id]) + + wallet-accounts (rf/sub [:wallet/operable-accounts]) + joined (rf/sub [:communities/community-joined id]) + unmodified-addresses-to-reveal (rf/sub [:communities/addresses-to-reveal id]) + [addresses-to-reveal set-addresses-to-reveal] (rn/use-state unmodified-addresses-to-reveal) + + unmodified-flag-share-all-addresses (rf/sub [:communities/share-all-addresses? id]) + [flag-share-all-addresses + set-flag-share-all-addresses] (rn/use-state unmodified-flag-share-all-addresses) + + identical-choices? (and (= unmodified-addresses-to-reveal addresses-to-reveal) + (= unmodified-flag-share-all-addresses flag-share-all-addresses)) + + cancel-selection + (fn [] + (rf/dispatch [:communities/check-permissions-to-join-community + id + unmodified-addresses-to-reveal + :based-on-client-selection]) + (rf/dispatch [:hide-bottom-sheet])) + + toggle-flag-share-all-addresses + (fn [] + (let [new-value (not flag-share-all-addresses)] + (set-flag-share-all-addresses new-value) + (when new-value + (set-addresses-to-reveal (set (map :address wallet-accounts))))))] + (rn/use-mount + (fn [] + (when-not flag-share-all-addresses + (rf/dispatch [:communities/get-permissioned-balances id])))) + + (rn/use-effect + (fn [] + (rf/dispatch [:communities/check-permissions-to-join-during-selection id addresses-to-reveal])) + [id addresses-to-reveal]) + [:<> + [page-top + {:community-id id + :identical-choices? identical-choices? + :can-edit-addresses? can-edit-addresses?}] + + [gesture/flat-list + {:render-fn account-item + :render-data [addresses-to-reveal + set-addresses-to-reveal + id + color + flag-share-all-addresses] + :header [quo/page-setting + {:checked? flag-share-all-addresses + :disabled? joined + :customization-color color + :on-change toggle-flag-share-all-addresses + :setting-text (i18n/label + :t/share-all-current-and-future-addresses) + :container-style {:margin-bottom 16}}] + :content-container-style {:padding-horizontal 20} + :key-fn :address + :data wallet-accounts}] + + [selection-bottom-actions id + {:flag-share-all-addresses flag-share-all-addresses + :addresses-to-reveal addresses-to-reveal + :cancel-selection cancel-selection + :can-edit-addresses? can-edit-addresses? + :identical-choices? identical-choices?}]])) diff --git a/src/status_im/contexts/communities/actions/airdrop_addresses/events.cljs b/src/status_im/contexts/communities/actions/airdrop_addresses/events.cljs new file mode 100644 index 00000000000..bd366da2903 --- /dev/null +++ b/src/status_im/contexts/communities/actions/airdrop_addresses/events.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.communities.actions.airdrop-addresses.events + (:require + [utils.re-frame :as rf])) + +(defn set-airdrop-address + [{:keys [db]} [community-id airdrop-address]] + {:db (assoc-in db [:communities/all-airdrop-addresses community-id] airdrop-address)}) + +(rf/reg-event-fx :communities/set-airdrop-address set-airdrop-address) diff --git a/src/status_im/contexts/communities/actions/airdrop_addresses/events_test.cljs b/src/status_im/contexts/communities/actions/airdrop_addresses/events_test.cljs new file mode 100644 index 00000000000..7ec97de5a37 --- /dev/null +++ b/src/status_im/contexts/communities/actions/airdrop_addresses/events_test.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.communities.actions.airdrop-addresses.events-test + (:require + [cljs.test :refer [deftest is testing]] + [status-im.contexts.communities.actions.airdrop-addresses.events :as sut])) + +(def community-id "community-id") + +(deftest set-airdrop-address-test + (testing "updates all reveal? flags" + (let [new-airdrop-address "0xA" + cofx {:db {:communities/all-airdrop-addresses + {"another-community" "0xF" + community-id "0xB"}}}] + (is (match? + (assoc-in cofx + [:db :communities/all-airdrop-addresses community-id] + new-airdrop-address) + (sut/set-airdrop-address cofx [community-id new-airdrop-address])))))) diff --git a/src/status_im/contexts/communities/actions/airdrop_addresses/style.cljs b/src/status_im/contexts/communities/actions/airdrop_addresses/style.cljs new file mode 100644 index 00000000000..6f17e333cde --- /dev/null +++ b/src/status_im/contexts/communities/actions/airdrop_addresses/style.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.communities.actions.airdrop-addresses.style) + +(def account-list-container + {:padding-horizontal 8 + :padding-bottom 8}) diff --git a/src/status_im/contexts/communities/actions/airdrop_addresses/view.cljs b/src/status_im/contexts/communities/actions/airdrop_addresses/view.cljs new file mode 100644 index 00000000000..c5b513eb887 --- /dev/null +++ b/src/status_im/contexts/communities/actions/airdrop_addresses/view.cljs @@ -0,0 +1,82 @@ +(ns status-im.contexts.communities.actions.airdrop-addresses.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.common.not-implemented :as not-implemented] + [status-im.config :as config] + [status-im.contexts.communities.actions.airdrop-addresses.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- account-item + [{:keys [address emoji] :as account} + _ _ + [community-id airdrop-address can-edit-addresses?]] + (let [airdrop-address? (= address airdrop-address) + on-press + (when-not airdrop-address? + (fn [] + (if can-edit-addresses? + (rf/dispatch + [:communities/sign-shared-addresses + {:community-id community-id + :airdrop-address address + :on-success (fn [] + (rf/dispatch + [:dismiss-modal + :screen/address-for-airdrop]) + (rf/dispatch + [:hide-bottom-sheet]))}]) + (do + (rf/dispatch [:communities/set-airdrop-address community-id address]) + (rf/dispatch [:hide-bottom-sheet])))))] + [quo/account-item + {:account-props account + :emoji emoji + :state (if airdrop-address? :selected :default) + :on-press on-press}])) + +(defn view + [] + (let [{id :community-id} (quo.context/use-screen-params) + {:keys [name logo color]} (rf/sub [:communities/for-context-tag id]) + accounts (rf/sub [:communities/accounts-to-reveal id]) + airdrop-address (rf/sub [:communities/airdrop-address id]) + can-edit-addresses? (rf/sub [:communities/can-edit-shared-addresses? id]) + go-back (rn/use-callback #(rf/dispatch [:dismiss-modal + :screen/address-for-airdrop]))] + [:<> + (when can-edit-addresses? + [quo/page-nav + {:type :no-title + :icon-name :i/arrow-left + :on-press go-back}]) + + (if can-edit-addresses? + [quo/page-top + {:title (i18n/label :t/airdrop-addresses) + :title-accessibility-label :title-label + :description :context-tag + :context-tag {:type :community + :community-logo logo + :community-name name}}] + (when config/show-not-implemented-features? + [quo/drawer-top + {:type :context-tag + :context-tag-type :community + :title (i18n/label :t/airdrop-addresses) + :community-name name + :button-icon :i/info + :button-type :grey + :on-button-press not-implemented/alert + :community-logo logo + :customization-color color}])) + + [gesture/flat-list + {:data accounts + :render-fn account-item + :render-data [id airdrop-address can-edit-addresses?] + :content-container-style style/account-list-container + :key-fn :address}]])) diff --git a/src/status_im/contexts/communities/actions/channel_view_details/style.cljs b/src/status_im/contexts/communities/actions/channel_view_details/style.cljs new file mode 100644 index 00000000000..bd439237322 --- /dev/null +++ b/src/status_im/contexts/communities/actions/channel_view_details/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.communities.actions.channel-view-details.style) + +(def channel-actions-wrapper {:height 102}) + +(def footer {:margin-top 8}) + +(def text-combinations {:margin-top 12 :margin-horizontal 20}) + +(def wrapper {:padding 20}) diff --git a/src/status_im/contexts/communities/actions/channel_view_details/view.cljs b/src/status_im/contexts/communities/actions/channel_view_details/view.cljs new file mode 100644 index 00000000000..8f4ca932dbc --- /dev/null +++ b/src/status_im/contexts/communities/actions/channel_view_details/view.cljs @@ -0,0 +1,131 @@ +(ns status-im.contexts.communities.actions.channel-view-details.view + (:require [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.common.contact-list.view :as contact-list] + [status-im.common.home.actions.view :as home.actions] + [status-im.constants :as constants] + [status-im.contexts.communities.actions.channel-view-details.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- contact-item + [public-key _ _ {:keys [theme]}] + (let [show-profile-actions (rn/use-callback + (fn [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] [home.actions/contact-actions + {:public-key public-key}])}])) + [public-key]) + on-press (rn/use-callback + (fn [] + (rf/dispatch [:chat.ui/show-profile public-key])) + [public-key]) + [primary-name + secondary-name] (rf/sub [:contacts/contact-two-names-by-identity public-key]) + {:keys [ens-verified + added? + compressed-key]} (rf/sub [:contacts/contact-by-address public-key])] + [contact-list-item/contact-list-item + {:on-press on-press + :on-long-press show-profile-actions + :accessory {:type :options + :on-press show-profile-actions}} + {:primary-name primary-name + :secondary-name secondary-name + :compressed-key compressed-key + :public-key public-key + :ens-verified ens-verified + :added? added? + ;; We hardcode the height of the container to match exactly the height + ;; used in the `get-item-layout` function. + :container-style {:height constants/contact-item-height}} + theme])) + +(defn- footer + [] + [rn/view {:style style/footer}]) + +(defn- get-item-layout + [_ index] + #js {:length constants/contact-item-height + :offset (* constants/contact-item-height index) + :index index}) + +(defn- members + [community-id chat-id theme] + (let [online-members (rf/sub [:communities/chat-members-sorted community-id chat-id :online]) + offline-members (rf/sub [:communities/chat-members-sorted community-id chat-id :offline])] + [rn/section-list + {:key-fn :public-key + :content-container-style {:padding-bottom 20} + :get-item-layout get-item-layout + :content-inset-adjustment-behavior :never + :sections [{:title (i18n/label :t/online) + :data online-members} + {:title (i18n/label :t/offline) + :data offline-members}] + :sticky-section-headers-enabled false + :render-section-header-fn contact-list/contacts-section-header + :render-section-footer-fn footer + :render-data {:theme theme} + :render-fn contact-item + :scroll-event-throttle 32}])) + +(defn view + [] + (let [{:keys [chat-id community-id]} (quo.context/use-screen-params) + {:keys [description chat-name emoji muted chat-type color] + :as chat} (rf/sub [:chats/chat-by-id chat-id]) + pins-count (rf/sub [:chats/pin-messages-count chat-id]) + theme (quo.context/use-theme)] + (rn/use-mount (fn [] + (rf/dispatch [:pin-message/load-pin-messages chat-id]))) + [:<> + [quo/gradient-cover {:customization-color color :opacity 0.4}] + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back]) + :right-side [{:icon-name :i/options + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [home.actions/chat-actions + chat + false + true])}])}]}] + [quo/text-combinations + {:container-style style/text-combinations + :title [quo/channel-name + {:channel-name chat-name + :unlocked? true}] + :theme theme + :emoji (when (not (string/blank? emoji)) (string/trim emoji)) + :customization-color color + :title-accessibility-label :welcome-title + :description description + :description-accessibility-label :welcome-sub-title}] + [rn/view {:style style/wrapper} + [rn/view + {:style style/channel-actions-wrapper} + [quo/channel-actions + {:actions + [{:big? true + :label (i18n/label :t/pinned-messages-2) + :customization-color color + :icon :i/pin + :counter-value pins-count + :on-press (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:pin-message/show-pins-bottom-sheet + chat-id]))} + {:label (if muted (i18n/label :t/unmute-channel) (i18n/label :t/mute-channel)) + :customization-color color + :icon (if muted :i/muted :i/activity-center) + :on-press (fn [] + (if muted + (home.actions/unmute-chat-action chat-id) + (home.actions/mute-chat-action chat-id chat-type muted)))}]}]]] + [members community-id chat-id theme]])) diff --git a/src/status_im/contexts/communities/actions/chat/view.cljs b/src/status_im/contexts/communities/actions/chat/view.cljs new file mode 100644 index 00000000000..52e8772320b --- /dev/null +++ b/src/status_im/contexts/communities/actions/chat/view.cljs @@ -0,0 +1,154 @@ +(ns status-im.contexts.communities.actions.chat.view + (:require + [quo.core :as quo] + [status-im.common.mute-drawer.view :as mute-drawer] + [status-im.common.muting.helpers :refer [format-mute-till]] + [status-im.common.not-implemented :as not-implemented] + [status-im.config :as config] + [status-im.contexts.chat.actions.view :as chat-actions] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hide-sheet-and-dispatch + [event] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch event)) + +(defn- mute-channel-action + [chat-id chat-type muted?] + (hide-sheet-and-dispatch [:show-bottom-sheet + {:content (fn [] + [mute-drawer/mute-drawer + {:id chat-id + :community? false + :muted? (not muted?) + :chat-type chat-type + :accessibility-label :mute-community-title}])}])) + +(defn- unmute-channel-action + [chat-id] + (hide-sheet-and-dispatch [:chat.ui/mute chat-id false 0])) + +(defn- action-view-members-and-details + [community-id chat-id] + {:icon :i/members + :accessibility-label :chat-view-members-and-details + :label (i18n/label :t/view-channel-members-and-details) + :on-press #(rf/dispatch [:navigate-to :screen/chat.view-channel-members-and-details + {:community-id community-id :chat-id chat-id}])}) + +(defn- action-token-requirements + [] + (when config/show-not-implemented-features? + {:icon :i/token + :right-icon :i/chevron-right + :accessibility-label :chat-view-token-requirements + :on-press not-implemented/alert + :label (i18n/label :t/view-token-gating)})) + +(defn- action-mark-as-read + [chat-id] + {:icon :i/mark-as-read + :accessibility-label :chat-mark-as-read + :on-press #(hide-sheet-and-dispatch [:chat.ui/mark-all-read-pressed chat-id]) + :label (i18n/label :t/mark-as-read)}) + +(defn- action-toggle-muted + [id muted? muted-till chat-type] + (let [muted (and muted? (some? muted-till)) + time-string (fn [mute-title mute-duration] + (i18n/label mute-title {:duration mute-duration}))] + (cond-> {:icon :i/muted + :accessibility-label :chat-toggle-muted + :sub-label (when (and muted? (some? muted-till)) + (time-string :t/muted-until (format-mute-till muted-till))) + :on-press (if muted? + #(unmute-channel-action id) + #(mute-channel-action id chat-type muted?)) + :label (i18n/label (if muted + :t/unmute-channel + :t/mute-channel))} + (not muted?) (assoc :right-icon :i/chevron-right)))) + +(defn- action-notification-settings + [] + (when config/show-not-implemented-features? + {:icon :i/notifications + :right-icon :i/chevron-right + :accessibility-label :chat-notification-settings + :on-press not-implemented/alert + :label (i18n/label :t/notification-settings) + :sub-label (i18n/label :t/only-mentions)})) + +(defn- action-pinned-messages + [chat-id] + {:icon :i/pin + :right-icon :i/chevron-right + :accessibility-label :chat-pinned-messages + :on-press (fn [] + (rf/dispatch [:pin-message/load-pin-messages chat-id]) + (rf/dispatch [:pin-message/show-pins-bottom-sheet chat-id])) + :label (i18n/label :t/pinned-messages)}) + + +(defn- action-invite-people + [] + (when config/show-not-implemented-features? + {:icon :i/add-user + :accessibility-label :chat-invite-people + :on-press not-implemented/alert + :label (i18n/label :t/invite-people-from-contacts)})) + +(defn- action-qr-code + [chat-id] + {:icon :i/qr-code + :accessibility-label :chat-show-qr-code + :on-press #(rf/dispatch [:communities/share-community-channel-url-qr-code chat-id]) + :label (i18n/label :t/show-qr)}) + +(defn- action-share + [chat-id] + {:icon :i/share + :accessibility-label :chat-share + :on-press #(rf/dispatch [:communities/share-community-channel-url-with-data chat-id]) + :label (i18n/label :t/share-channel)}) + +(defn actions + [{:keys [locked? chat-id community-id]} inside-chat? hide-view-members?] + (let [{:keys [muted muted-till chat-type]} (rf/sub [:chats/chat-by-id chat-id]) + {:keys [token-gated?]} (rf/sub [:chats/community-chat-by-id community-id chat-id])] + (cond + locked? + [quo/action-drawer + [[(action-invite-people) + (when token-gated? + (action-token-requirements)) + (action-qr-code chat-id) + (action-share chat-id)]]] + + (and (not inside-chat?) (not locked?)) + [quo/action-drawer + [[(when-not hide-view-members? (action-view-members-and-details community-id chat-id)) + (action-mark-as-read chat-id) + (action-toggle-muted chat-id muted muted-till chat-type) + (action-notification-settings) + (action-pinned-messages chat-id) + (action-invite-people) + (action-qr-code chat-id) + (action-share chat-id)]]] + + (and inside-chat? (not locked?)) + [quo/action-drawer + [[(action-view-members-and-details community-id chat-id) + (when token-gated? + (action-token-requirements)) + (action-mark-as-read chat-id) + (action-toggle-muted chat-id muted muted-till chat-type) + (action-notification-settings) + (when config/fetch-messages-enabled? + (chat-actions/fetch-messages chat-id)) + (action-invite-people) + (action-qr-code chat-id) + (action-share chat-id)]]] + + :else nil))) diff --git a/src/status_im/contexts/communities/actions/community_options/component_spec.cljs b/src/status_im/contexts/communities/actions/community_options/component_spec.cljs new file mode 100644 index 00000000000..52d99527808 --- /dev/null +++ b/src/status_im/contexts/communities/actions/community_options/component_spec.cljs @@ -0,0 +1,141 @@ +(ns status-im.contexts.communities.actions.community-options.component-spec + (:require + [status-im.contexts.communities.actions.community-options.view :as options] + [test-helpers.component :as h])) + +(h/describe "community options for bottom sheets" + (h/setup-restorable-re-frame) + + (h/test "joined options - Non token Gated" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:joined true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/view-members)) + (h/is-truthy (h/get-by-translation-text :t/view-community-rules)) + (h/is-truthy (h/get-by-translation-text :t/mark-as-read)) + (h/is-truthy (h/get-by-translation-text :t/mute-community)) + ;(h/is-truthy (h/get-by-translation-text :t/notification-settings)) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community)) + (h/is-truthy (h/get-by-translation-text :t/leave-community))) + + (h/test "joined options - Token Gated" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:joined true + :role-permissions? true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/view-members)) + (h/is-truthy (h/get-by-translation-text :t/view-community-rules)) + ;(h/is-truthy (h/get-by-translation-text :t/view-token-gating)) + (h/is-truthy (h/get-by-translation-text :t/mark-as-read)) + (h/is-truthy (h/get-by-translation-text :t/mute-community)) + ;(h/is-truthy (h/get-by-translation-text :t/notification-settings)) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community)) + (h/is-truthy (h/get-by-translation-text :t/leave-community))) + + (h/test "admin options - Non token Gated" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:admin true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/view-members)) + (h/is-truthy (h/get-by-translation-text :t/view-community-rules)) + (h/is-truthy (h/get-by-translation-text :t/mark-as-read)) + (h/is-truthy (h/get-by-translation-text :t/mute-community)) + ;(h/is-truthy (h/get-by-translation-text :t/notification-settings)) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community))) + + (h/test "admin options - Token Gated" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:admin true + :role-permissions? true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/view-members)) + (h/is-truthy (h/get-by-translation-text :t/view-community-rules)) + (h/is-truthy (h/get-by-translation-text :t/mark-as-read)) + (h/is-truthy (h/get-by-translation-text :t/mute-community)) + ;(h/is-truthy (h/get-by-translation-text :t/notification-settings)) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community))) + + (h/test "request sent options - Non token Gated" + (h/setup-subs {:communities/my-pending-request-to-join "mock-id" + :communities/community {} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/view-members)) + (h/is-truthy (h/get-by-translation-text :t/view-community-rules)) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community)) + (h/is-truthy (h/get-by-translation-text :t/cancel-request-to-join))) + + (h/test "request sent options - Token Gated" + (h/setup-subs {:communities/my-pending-request-to-join "mock-id" + :communities/community {:role-permissions? true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + ;(h/is-truthy (h/get-by-translation-text :t/view-token-gating)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community)) + (h/is-truthy (h/get-by-translation-text :t/cancel-request-to-join))) + + (h/test "banned options - Non token Gated" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:banList true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/view-members)) + (h/is-truthy (h/get-by-translation-text :t/view-community-rules)) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community))) + + (h/test "banned options - Token Gated" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:banList 100 + :role-permissions? true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + ;(h/is-truthy (h/get-by-translation-text :t/view-token-gating)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community))) + + (h/test "banned options - Token Gated" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:banList 100 + :role-permissions? true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/invite-people-from-contacts)) + ;(h/is-truthy (h/get-by-translation-text :t/view-token-gating)) + (h/is-truthy (h/get-by-translation-text :t/show-qr)) + (h/is-truthy (h/get-by-translation-text :t/share-community))) + + (h/test "joined and muted community" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:joined true + :muted true + :role-permissions? true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/unmute-community))) + + (h/test "opened community" + (h/setup-subs {:communities/my-pending-request-to-join nil + :communities/community {:joined false + :spectated true} + :profile/test-networks-enabled? false}) + (h/render [options/community-options-bottom-sheet {:id "test"}]) + (h/is-truthy (h/get-by-translation-text :t/close-community)))) diff --git a/src/status_im/contexts/communities/actions/community_options/events.cljs b/src/status_im/contexts/communities/actions/community_options/events.cljs new file mode 100644 index 00000000000..9c2fbe05c3f --- /dev/null +++ b/src/status_im/contexts/communities/actions/community_options/events.cljs @@ -0,0 +1,74 @@ +(ns status-im.contexts.communities.actions.community-options.events + (:require [status-im.common.muting.helpers :as muting.helpers] + [status-im.constants :as constants] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :community/update-community-chats-mute-status + (fn [{:keys [db]} [community-id muted? muted-till]] + {:db + (reduce + #(update-in %1 [:chats (str community-id %2)] assoc :muted muted? :muted-till muted-till) + db + (keys (get-in db [:communities community-id :chats])))})) + +(rf/reg-event-fx + :community/mute-community-failed + (fn [{:keys [db]} [community-id muted? error]] + (log/error "mute community failed" community-id error) + {:db (assoc-in db [:communities community-id :muted] (not muted?)) + :dispatch [:community/update-community-chats-mute-status community-id muted? error]})) + +(rf/reg-event-fx :community/mute-community-successful + (fn [{:keys [db]} [community-id muted? muted-till]] + (let [time-string (fn [mute-title mute-duration] + (i18n/label mute-title {:duration mute-duration}))] + {:db (assoc-in db [:communities community-id :muted-till] muted-till) + :fx [[:dispatch [:community/update-community-chats-mute-status community-id muted? muted-till]] + [:dispatch + [:toasts/upsert + (cond-> {:type :positive + :id :mute-community-toast + :text (if muted? + (when (some? muted-till) + (time-string :t/muted-until + (muting.helpers/format-mute-till muted-till))) + (i18n/label :t/community-unmuted))} + muted? (assoc :duration constants/mute-undo-time-limit-ms + :undo-duration (/ constants/mute-undo-time-limit-ms 1000) + :undo-on-press #(rf/dispatch [:community/undo-mute + community-id])))]]]}))) + +(rf/reg-event-fx + :community/undo-mute + (fn [_ [community-id]] + {:fx [[:json-rpc/call + [{:method "wakuext_unMuteCommunityChats" + :params [community-id] + :on-error #(rf/dispatch [:community/mute-community-failed community-id false %]) + :on-success #(rf/dispatch [:community/undo-mute-success community-id])}]]]})) + +(rf/reg-event-fx + :community/undo-mute-success + (fn [{:keys [db]} [community-id]] + {:db (update-in db + [:communities community-id] + (fn [community-id] + (-> community-id + (dissoc :muted-till) + (assoc :muted false)))) + :fx [[:dispatch [:toasts/close :mute-community-toast]] + [:dispatch [:community/update-community-chats-mute-status community-id false nil]]]})) + +(rf/reg-event-fx :community/set-muted + (fn [{:keys [db]} [community-id muted? muted-type]] + (let [params (if muted? [{:communityId community-id :mutedType muted-type}] [community-id]) + method (if muted? "wakuext_muteCommunityChats" "wakuext_unMuteCommunityChats")] + {:db (assoc-in db [:communities community-id :muted] muted?) + :json-rpc/call [{:method method + :params params + :on-error #(rf/dispatch [:community/mute-community-failed community-id + muted? %]) + :on-success #(rf/dispatch [:community/mute-community-successful + community-id muted? %])}]}))) diff --git a/src/status_im/contexts/communities/actions/community_options/view.cljs b/src/status_im/contexts/communities/actions/community_options/view.cljs new file mode 100644 index 00000000000..de0c16248f2 --- /dev/null +++ b/src/status_im/contexts/communities/actions/community_options/view.cljs @@ -0,0 +1,232 @@ +(ns status-im.contexts.communities.actions.community-options.view + (:require + [quo.core :as quo] + [status-im.common.mute-drawer.view :as mute-options] + [status-im.common.muting.helpers :refer [format-mute-till]] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.communities.actions.leave.view :as leave-menu] + [status-im.contexts.communities.actions.permissions-sheet.view :as permissions-sheet] + [status-im.contexts.communities.actions.see-rules.view :as see-rules] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hide-sheet-and-dispatch + [event] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch event)) + +(defn view-members + [id] + {:icon :i/members + :accessibility-label :view-members + :label (i18n/label :t/view-members) + :on-press #(hide-sheet-and-dispatch [:navigate-to :screen/legacy-community-members + {:community-id id}])}) + +(defn view-rules + [id intro-message] + {:icon :i/bullet-list + :right-icon :i/chevron-right + :accessibility-label :view-community-rules + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [see-rules/view id intro-message])}]) + :label (i18n/label :t/view-community-rules)}) + +(defn view-token-gating + [id] + {:icon :i/token + :right-icon :i/chevron-right + :accessibility-label :view-token-gating + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] + [permissions-sheet/view id]) + :padding-bottom-override 16}]) + :label (i18n/label :t/view-token-gating)}) + +(defn edit-shared-addresses + [id] + {:icon :i/wallet + :right-icon :i/chevron-right + :accessibility-label :edit-shared-addresses + :on-press (fn [] + (rf/dispatch [:open-modal :screen/community-account-selection + {:community-id id}])) + :label (i18n/label :t/edit-shared-addresses)}) + +(defn mark-as-read + [id] + {:icon :i/mark-as-read + :accessibility-label :mark-as-read + :label (i18n/label :t/mark-as-read) + :on-press #(hide-sheet-and-dispatch [:chat.ui/mark-all-read-in-community-pressed id])}) + +(defn mute-community + [id muted? muted-till] + (let [time-string (fn [mute-title mute-duration] + (i18n/label mute-title {:duration mute-duration}))] + {:icon (if muted? :i/activity-center :i/muted) + :accessibility-label (if muted? :unmute-community :mute-community) + :label (i18n/label (if muted? :t/unmute-community :t/mute-community)) + :sub-label (when (and muted? (some? muted-till)) + (time-string :t/muted-until (format-mute-till muted-till))) + :right-icon :i/chevron-right + :on-press (if muted? + #(hide-sheet-and-dispatch [:community/set-muted id (not muted?) + constants/un-muted]) + #(hide-sheet-and-dispatch + [:show-bottom-sheet + {:content (fn [] + [mute-options/mute-drawer + {:id id + :community? true + :muted? (not muted?) + :accessibility-label :mute-community-title}])}]))})) + +(defn community-notification-settings + [id] + (when config/show-not-implemented-features? + {:icon :i/notifications + :accessibility-label :community-notification-settings + :label (i18n/label :t/notification-settings) + :on-press #(js/alert (str "implement action" id)) + :right-icon :i/chevron-right})) + +(defn invite-contacts + [id] + {:icon :i/add-user + :accessibility-label :invite-people-from-contacts + :label (i18n/label :t/invite-people-from-contacts) + :on-press #(hide-sheet-and-dispatch [:communities/invite-people-pressed id])}) + +(defn show-qr + [id] + (let [on-success #(rf/dispatch [:open-modal :screen/share-community + {:community-id id + :url %}])] + {:icon :i/qr-code + :accessibility-label :show-qr + :on-press #(rf/dispatch [:communities/get-community-share-data id on-success]) + :label (i18n/label :t/show-qr)})) + +(defn share-community + [id] + {:icon :i/share + :accessibility-label :share-community + :label (i18n/label :t/share-community) + :on-press #(rf/dispatch [:communities/share-community-pressed id])}) + +(defn leave-community + [id color] + {:icon :i/log-out + :label (i18n/label :t/leave-community) + :accessibility-label :leave-community + :danger? true + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [leave-menu/leave-sheet id color])}])}) + +(defn close-community + [id] + {:icon :i/close-circle + :label (i18n/label :t/close-community) + :accessibility-label :close-community + :danger? true + :on-press #(rf/dispatch [:communities/leave id])}) + +(defn cancel-request-to-join + [id request-id] + {:icon :i/block + :label (i18n/label :t/cancel-request-to-join) + :accessibility-label :cancel-request-to-join + :danger? true + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [leave-menu/cancel-request-sheet id + request-id])}])}) + +(defn not-joined-options + [{:keys [id join-pending? spectated? token-gated? intro-message test-networks-enabled?]}] + (let [common [(show-qr id) (share-community id)] + specific (cond + (and token-gated? + (not test-networks-enabled?) + (ff/enabled? ::ff/community.view-token-requirements)) + [(invite-contacts id) (view-token-gating id)] + + token-gated? + [(invite-contacts id)] + + (not token-gated?) + [(view-members id) (view-rules id intro-message) (invite-contacts id)])] + [(concat specific + common + (when (and spectated? (not join-pending?)) + [(assoc (close-community id) :add-divider? true)]))])) + +(defn join-request-sent-options + [{:keys [id request-id] :as config}] + [(conj (first (not-joined-options config)) + (assoc (cancel-request-to-join id request-id) :add-divider? true))]) + +(defn banned-options + [config] + (not-joined-options config)) + +(defn joined-options + [{:keys [id token-gated? muted? muted-till color intro-message]}] + [[(view-members id) + (view-rules id intro-message) + (when (and token-gated? (ff/enabled? ::ff/community.view-token-requirements)) + (view-token-gating id)) + (when (ff/enabled? ::ff/community.edit-account-selection) + (edit-shared-addresses id)) + (mark-as-read id) + (mute-community id muted? muted-till) + (community-notification-settings id) + (invite-contacts id) + (show-qr id) + (share-community id)] + [(assoc (leave-community id color) :add-divider? true)]]) + +(defn owner-options + [{:keys [id token-gated? muted? muted-till intro-message]}] + [[(view-members id) + (view-rules id intro-message) + (when token-gated? (view-token-gating id)) + (mark-as-read id) + (mute-community id muted? muted-till) + (community-notification-settings id) + (invite-contacts id) + (show-qr id) + (share-community id)]]) + +(defn get-context-drawers + [{:keys [id]}] + (let [{:keys + [id admin joined + banList color muted muted-till + spectated role-permissions? + intro-message]} (rf/sub [:communities/community id]) + request-id (rf/sub [:communities/my-pending-request-to-join id]) + test-networks? (rf/sub [:profile/test-networks-enabled?]) + config {:id id + :color color + :muted-till muted-till + :muted? muted + :spectated? spectated + :token-gated? role-permissions? + :intro-message intro-message + :test-networks? test-networks? + :join-pending? (boolean request-id) + :request-id request-id}] + (cond + admin (owner-options config) + joined (joined-options config) + request-id (join-request-sent-options config) + banList (banned-options config) + :else (not-joined-options config)))) + +(defn community-options-bottom-sheet + [id] + [quo/action-drawer + (get-context-drawers {:id id})]) diff --git a/src/status_im/contexts/communities/actions/community_rules/style.cljs b/src/status_im/contexts/communities/actions/community_rules/style.cljs new file mode 100644 index 00000000000..1deb6960e76 --- /dev/null +++ b/src/status_im/contexts/communities/actions/community_rules/style.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.communities.actions.community-rules.style) + +(def community-rule + {:flex 1 + :align-items :flex-start + :padding-top 8 + :margin-bottom 12 + :padding-horizontal 20}) diff --git a/src/status_im/contexts/communities/actions/community_rules/view.cljs b/src/status_im/contexts/communities/actions/community_rules/view.cljs new file mode 100644 index 00000000000..f89b122e650 --- /dev/null +++ b/src/status_im/contexts/communities/actions/community_rules/view.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.communities.actions.community-rules.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.communities.actions.community-rules.style :as style] + [utils.re-frame :as rf])) + +(defn view + [id] + (let [rules (rf/sub [:communities/rules id])] + [rn/view {:style style/community-rule} + [quo/text + {:weight :regular + :size :paragraph-2} + rules]])) diff --git a/src/status_im/contexts/communities/actions/community_rules_list/style.cljs b/src/status_im/contexts/communities/actions/community_rules_list/style.cljs new file mode 100644 index 00000000000..213e42d9ca5 --- /dev/null +++ b/src/status_im/contexts/communities/actions/community_rules_list/style.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.communities.actions.community-rules-list.style) + +(def community-rule + {:flex-direction :row + :flex 1 + :align-items :flex-start}) + +(def community-rule-index + {:margin-left 4}) + +(def community-rule-text + {:flex 1}) diff --git a/src/status_im/contexts/communities/actions/community_rules_list/view.cljs b/src/status_im/contexts/communities/actions/community_rules_list/view.cljs new file mode 100644 index 00000000000..b26fba10be8 --- /dev/null +++ b/src/status_im/contexts/communities/actions/community_rules_list/view.cljs @@ -0,0 +1,53 @@ +(ns status-im.contexts.communities.actions.community-rules-list.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.communities.actions.community-rules-list.style :as style])) + +(def standard-rules + [{:index 1 + :title "Be respectful" + :content + "You must respect all users, regardless of your liking towards them. Treat others the way you want to be treated."} + {:index 2 + :title "No inappropriate language" + :content + "The use of profanity should be kept to a minimum. However, any derogatory language towards any user is prohibited."} + {:index 3 + :title "No spamming" + :content + "Don't send a lot of small messages right after each other. Do not disrupt chat by spamming."} + {:index 4 + :title "No pornographic, adult or NSFW material" + :content "This is a community server and not meant to share this kind of material."} + {:index 5 + :title "No advertisements" + :content + "We do not tolerate any kind of advertisements, whether it be for other communities or streams."} + {:index 6 + :title "No offensive names and profile pictures" + :content "You will be asked to change your name or picture if the staff deems them inappropriate."}]) + +(defn community-rule-item + [{:keys [title content index]}] + [rn/view {:style style/community-rule} + [quo/text + {:style style/community-rule-index + :weight :regular + :size :paragraph-2} + (str index ". ")] + [quo/text + {:style style/community-rule-text + :accessibility-label :communities-rule-index + :weight :regular + :size :paragraph-2} + (str title ": " content)]]) + +(defn view + [rules scroll-enabled?] + [rn/flat-list + {:shows-horizontal-scroll-indicator false + :scroll-enabled scroll-enabled? + :data rules + :separator [rn/view {:margin-top 1}] + :render-fn community-rule-item}]) diff --git a/src/status_im/contexts/communities/actions/generic_menu/style.cljs b/src/status_im/contexts/communities/actions/generic_menu/style.cljs new file mode 100644 index 00000000000..b57e99e314e --- /dev/null +++ b/src/status_im/contexts/communities/actions/generic_menu/style.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.communities.actions.generic-menu.style) + +(defn container + [max-height] + {:flex 1 + :max-height max-height + :margin-left 20 + :margin-right 20}) + +(defn scroll-view-style + [max-height] + {:margin-bottom 12 + :max-height max-height}) + +(def inner-container + {:display :flex + :flex-direction :row + :align-items :center + :justify-content :space-between}) + +(def community-tag + {:margin-right :auto + :margin-top 4}) diff --git a/src/status_im/contexts/communities/actions/generic_menu/view.cljs b/src/status_im/contexts/communities/actions/generic_menu/view.cljs new file mode 100644 index 00000000000..17a01cbb17b --- /dev/null +++ b/src/status_im/contexts/communities/actions/generic_menu/view.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.communities.actions.generic-menu.view + (:require + [oops.core :as oops] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.safe-area :as safe-area] + [status-im.contexts.communities.actions.generic-menu.style :as style] + [utils.re-frame :as rf])) + +(defn view + [{:keys [id title]} children] + (let [{:keys [name images]} (rf/sub [:communities/community id]) + [content-height set-content-height] (rn/use-state 0) + [sheet-header-height set-header-height] (rn/use-state 0) + insets safe-area/insets + {window-height :height} (rn/get-window) + sheet-max-height (- window-height (:top insets)) + max-height (- sheet-max-height sheet-header-height)] + [rn/view {:style (style/container max-height)} + [rn/view + {:style {:padding-bottom 12} + :on-layout (fn [event] + (set-header-height (oops/oget + event + "nativeEvent.layout.height")))} + [rn/view {:style style/inner-container} + [quo/text + {:accessibility-label :communities-join-community + :weight :semi-bold + :size :heading-2} + title]] + [rn/view {:style style/community-tag} + [quo/context-tag + {:type :community + :size 24 + :community-logo (:thumbnail images) + :community-name name}]]] + [gesture/scroll-view + {:scroll-enabled (> content-height max-height) + :scroll-event-throttle 16 + :style (style/scroll-view-style max-height) + :shows-vertical-scroll-indicator false} + [rn/view + {:on-layout (fn [event] + (set-content-height (oops/oget + event + "nativeEvent.layout.height")))} + children]]])) diff --git a/src/status_im/contexts/communities/actions/invite_contacts/style.cljs b/src/status_im/contexts/communities/actions/invite_contacts/style.cljs new file mode 100644 index 00000000000..57da40d3c5c --- /dev/null +++ b/src/status_im/contexts/communities/actions/invite_contacts/style.cljs @@ -0,0 +1,48 @@ +(ns status-im.contexts.communities.actions.invite-contacts.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(def contact-selection-heading + {:flex-direction :row + :justify-content :space-between + :align-items :flex-end + :margin-top 24 + :margin-bottom 16}) + +(def chat-button + {:position :absolute + :bottom safe-area/bottom + :left 20 + :right 20}) + +(defn no-contacts + [] + {:margin-bottom (+ 96 safe-area/bottom) + :flex 1 + :justify-content :center + :align-items :center}) + +(def context-tag + {:align-self :flex-start + :margin-top -8 + :margin-bottom 12}) + +(def no-contacts-text + {:margin-bottom 2 + :margin-top 12}) + +(def no-contacts-button-container + {:margin-top 20 + :margin-bottom 12}) + +(defn section-list-container-style + [theme] + {:padding-bottom 70 + :background-color (colors/theme-colors colors/white + colors/neutral-95 + theme)}) + +(defn invite-to-community-text + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) diff --git a/src/status_im/contexts/communities/actions/invite_contacts/view.cljs b/src/status_im/contexts/communities/actions/invite_contacts/view.cljs new file mode 100644 index 00000000000..ac8ed41e847 --- /dev/null +++ b/src/status_im/contexts/communities/actions/invite_contacts/view.cljs @@ -0,0 +1,140 @@ +(ns status-im.contexts.communities.actions.invite-contacts.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.share :as share] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.common.contact-list.view :as contact-list] + [status-im.contexts.communities.actions.invite-contacts.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- no-contacts-view + [{:keys [theme id]}] + (let [customization-color (rf/sub [:profile/customization-color]) + {:keys [universal-profile-url]} (rf/sub [:profile/profile]) + on-press-share-community (rn/use-callback + #(rf/dispatch [:communities/share-community-url-with-data + id])) + on-press-share-profile (rn/use-callback #(share/open {:url universal-profile-url}) + [universal-profile-url])] + [rn/view + {:style (style/no-contacts)} + [rn/image {:source (resources/get-themed-image :no-contacts-to-chat theme)}] + [quo/text + {:weight :semi-bold + :size :paragraph-1 + :style style/no-contacts-text} + (i18n/label :t/you-have-no-contacts)] + [quo/text + {:weight :regular + :size :paragraph-2} + (i18n/label :t/dont-yell-at-me)] + [quo/button + {:customization-color customization-color + :theme theme + :type :primary + :size 32 + :container-style style/no-contacts-button-container + :on-press on-press-share-community} + (i18n/label :t/send-community-link)] + [quo/button + {:customization-color customization-color + :theme theme + :type :grey + :size 32 + :on-press on-press-share-profile} + (i18n/label :t/invite-friends-to-status)]])) + +(defn- contact-item + [{:keys [public-key] + :as item}] + (let [user-selected? (rf/sub [:is-contact-selected? public-key]) + {:keys [id]} (quo.context/use-screen-params) + community-members-keys (set (rf/sub [:communities/community-members id])) + community-member? (boolean (community-members-keys public-key)) + on-toggle (fn [] + (when-not community-member? + (if user-selected? + (rf/dispatch [:deselect-contact public-key]) + (rf/dispatch [:select-contact public-key]))))] + [contact-list-item/contact-list-item + {:on-press on-toggle + :allow-multiple-presses? true + :accessory {:type :checkbox + :disabled? community-member? + :checked? (or community-member? user-selected?) + :on-check on-toggle} + :disabled? community-member?} + item])) + +(defn view + [] + (fn [] + (rn/use-unmount #(rf/dispatch [:group-chat/clear-contacts])) + (let [theme (quo.context/use-theme) + customization-color (rf/sub [:profile/customization-color]) + {:keys [id]} (quo.context/use-screen-params) + contacts (rf/sub [:contacts/filtered-active-sections]) + selected (rf/sub [:group/selected-contacts]) + {:keys [name images]} (rf/sub [:communities/community id]) + selected-contacts-count (count selected) + on-press (fn [] + (rf/dispatch [:communities/share-community-confirmation-pressed + selected id]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:toasts/upsert + {:type :positive + :theme theme + :text (if (= 1 selected-contacts-count) + (i18n/label :t/one-user-was-invited) + (i18n/label + :t/n-users-were-invited + {:count selected-contacts-count}))}])) + {window-height :height} (rn/get-window)] + [rn/view {:style {:flex 1}} + [rn/view {:style {:padding-horizontal 20}} + [quo/button + {:type :grey + :size 32 + :icon-only? true + :on-press #(rf/dispatch [:navigate-back])} + :i/close] + [rn/view {:style style/contact-selection-heading} + [quo/text + {:weight :semi-bold + :size :heading-1 + :style (style/invite-to-community-text theme)} + (i18n/label :t/invite-to-community)]] + [quo/context-tag + {:type :community + :size 24 + :community-logo (:thumbnail images) + :community-name name + :container-style style/context-tag}]] + (if (empty? contacts) + [no-contacts-view + {:theme theme + :id id}] + [:<> + [gesture/section-list + {:key-fn :public-key + :sticky-section-headers-enabled true + :sections contacts + :render-section-header-fn contact-list/contacts-section-header + :content-container-style (style/section-list-container-style theme) + :render-fn contact-item + :style {:height window-height}}] + (when (pos? selected-contacts-count) + [quo/button + {:type :primary + :accessibility-label :next-button + :customization-color customization-color + :container-style style/chat-button + :on-press on-press} + (if (= 1 selected-contacts-count) + (i18n/label :t/invite-1-user) + (i18n/label :t/invite-n-users {:count selected-contacts-count}))])])]))) diff --git a/src/status_im/contexts/communities/actions/leave/events.cljs b/src/status_im/contexts/communities/actions/leave/events.cljs new file mode 100644 index 00000000000..ef73fbdefe2 --- /dev/null +++ b/src/status_im/contexts/communities/actions/leave/events.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.communities.actions.leave.events + (:require [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :communities/cancel-request-to-join-success + (fn [_ [response-js]] + {:fx [[:dispatch [:sanitize-messages-and-process-response response-js]] + [:dispatch + [:toasts/upsert + {:type :positive + :text (i18n/label :t/you-canceled-the-request)}]]]})) + +(rf/reg-event-fx :communities/cancel-request-to-join + (fn [_ [request-to-join-id]] + {:json-rpc/call + [{:method "wakuext_cancelRequestToJoinCommunity" + :params [{:id request-to-join-id}] + :on-success #(rf/dispatch [:communities/cancel-request-to-join-success %]) + :js-response true + :on-error #(log/error "failed to cancel request to join community" + request-to-join-id + %)}]})) + +(rf/reg-event-fx :communities/left + (fn [_ [response-js]] + (let [community-name (aget response-js "communities" 0 "name")] + {:fx [[:dispatch [:sanitize-messages-and-process-response response-js]] + [:dispatch + [:toasts/upsert + {:type :positive + :text (i18n/label :t/left-community {:community community-name})}]] + [:dispatch [:activity-center.notifications/fetch-unread-count]] + [:dispatch [:hide-bottom-sheet]] + [:dispatch [:navigate-back]]]}))) + +(rf/reg-event-fx :communities/leave + (fn [{:keys [db]} [community-id on-success]] + (let [community-chat-ids (map #(str community-id %) + (keys (get-in db [:communities community-id :chats])))] + {:effects/push-notifications-clear-message-notifications community-chat-ids + :json-rpc/call + [{:method "wakuext_leaveCommunity" + :params [community-id] + :js-response true + :on-success (fn [^js response] + (when (fn? on-success) + (on-success)) + (rf/dispatch [:communities/left response])) + :on-error #(log/error "failed to leave community" community-id %)}]}))) diff --git a/src/status_im/contexts/communities/actions/leave/style.cljs b/src/status_im/contexts/communities/actions/leave/style.cljs new file mode 100644 index 00000000000..1b9ebf0d76a --- /dev/null +++ b/src/status_im/contexts/communities/actions/leave/style.cljs @@ -0,0 +1,16 @@ +(ns status-im.contexts.communities.actions.leave.style) + +(def button-container + {:margin-top 20 + :flex 1 + :flex-direction :row + :align-items :center + :justify-content :space-evenly}) + +(def cancel-button + {:flex 1 + :margin-right 12}) + +(def action-button {:flex 1}) + +(def text {:margin-top 16}) diff --git a/src/status_im/contexts/communities/actions/leave/view.cljs b/src/status_im/contexts/communities/actions/leave/view.cljs new file mode 100644 index 00000000000..6e97c7b68cd --- /dev/null +++ b/src/status_im/contexts/communities/actions/leave/view.cljs @@ -0,0 +1,62 @@ +(ns status-im.contexts.communities.actions.leave.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.communities.actions.generic-menu.view :as generic-menu] + [status-im.contexts.communities.actions.leave.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hide-sheet-and-dispatch + [event] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch event)) + +(defn leave-sheet + [id color] + [generic-menu/view + {:id id + :title (i18n/label :t/leave-community?)} + [:<> + [quo/text + {:accessibility-label :communities-join-community + :size :paragraph-1 + :style style/text} + (i18n/label :t/leave-community-message)] + [rn/view + {:style style/button-container} + [quo/button + {:on-press #(rf/dispatch [:hide-bottom-sheet]) + :type :grey + :container-style style/cancel-button} + (i18n/label :t/cancel)] + [quo/button + {:on-press #(hide-sheet-and-dispatch [:communities/leave id]) + :customization-color color + :container-style style/action-button} + (i18n/label :t/leave-community)]]]]) + +(defn cancel-request-sheet + [id request-id] + [generic-menu/view + {:id id + :title (i18n/label :t/cancel-request?)} + [:<> + [quo/text + {:accessibility-label :communities-join-community + :size :paragraph-1 + :style style/text} + (i18n/label :t/if-you-cancel)] + [rn/view + {:style style/button-container} + [quo/button + {:accessibility-label :cancel-button + :on-press #(rf/dispatch [:hide-bottom-sheet]) + :type :grey + :container-style style/cancel-button} + (i18n/label :t/close)] + [quo/button + {:accessibility-label :confirm-button + :on-press #(hide-sheet-and-dispatch [:communities/cancel-request-to-join request-id]) + :container-style style/action-button} + (i18n/label :t/confirm)]]]]) diff --git a/src/status_im/contexts/communities/actions/permissions_sheet/view.cljs b/src/status_im/contexts/communities/actions/permissions_sheet/view.cljs new file mode 100644 index 00000000000..8be9f00ad54 --- /dev/null +++ b/src/status_im/contexts/communities/actions/permissions_sheet/view.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.communities.actions.permissions-sheet.view + (:require + [quo.core :as quo] + [react-native.gesture :as gesture] + [utils.re-frame :as rf])) + +(defn view + [id] + (let [permissions (rf/sub [:community/token-permissions id])] + [gesture/scroll-view + [quo/community-detail-token-gating {:permissions permissions}]])) diff --git a/src/status_im/contexts/communities/actions/see_rules/view.cljs b/src/status_im/contexts/communities/actions/see_rules/view.cljs new file mode 100644 index 00000000000..10781178521 --- /dev/null +++ b/src/status_im/contexts/communities/actions/see_rules/view.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.communities.actions.see-rules.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.communities.actions.community-rules-list.view :as community-rules] + [status-im.contexts.communities.actions.generic-menu.view :as generic-menu] + [utils.i18n :as i18n])) + +(defn view + [id intro-message] + [generic-menu/view + {:id id + :title (i18n/label :t/community-rules)} + [rn/view {:style {:padding-top 8}} + (if (empty? intro-message) + [community-rules/view community-rules/standard-rules] + [quo/text + {:accessibility-label :communities-rules + :weight :regular + :size :paragraph-2} + intro-message])]]) diff --git a/src/status_im/contexts/communities/actions/share_community/style.cljs b/src/status_im/contexts/communities/actions/share_community/style.cljs new file mode 100644 index 00000000000..14b73be5024 --- /dev/null +++ b/src/status_im/contexts/communities/actions/share_community/style.cljs @@ -0,0 +1,64 @@ +(ns status-im.contexts.communities.actions.share-community.style + (:require [quo.foundations.colors :as colors])) + +(def horizontal-padding 20) +(def vertical-padding 12) +(def gradient-cover-padding 20) +(def qr-code-padding 12) + +(def header-container + {:padding-horizontal horizontal-padding + :padding-vertical vertical-padding}) + +(def scan-notice + {:color colors/white-70-blur + :margin-top 16 + :margin-left :auto + :margin-right :auto}) + +(defn community-name + [thumbnail] + {:margin-left (when thumbnail 8)}) + +(def qr-code-wrapper + {:padding-horizontal horizontal-padding + :margin-top 8}) + +(defn qr-code-size + [total-width] + (- total-width (* gradient-cover-padding 2) (* qr-code-padding 2))) + +(defn gradient-cover-size + [total-width] + (- total-width (* gradient-cover-padding 2))) + +(defn gradient-cover-wrapper + [width] + {:width (gradient-cover-size width) + :border-radius 16 + :margin-left 20 + :height "100%"}) + +(def share-button-container {:justify-self :flex-end}) + +(def qr-top-wrapper + {:margin 12 + :margin-bottom 0 + :flex-direction :row + :align-items :center + :justify-content :space-between}) + +(def community-avatar-dimension 32) + +(def community-avatar + {:border-radius community-avatar-dimension + :width community-avatar-dimension + :height community-avatar-dimension}) + +(def qr-code-view + {:padding-vertical vertical-padding + :align-items :center}) + +(def text-wrapper + {:flex-direction :row + :align-items :center}) diff --git a/src/status_im/contexts/communities/actions/share_community/view.cljs b/src/status_im/contexts/communities/actions/share_community/view.cljs new file mode 100644 index 00000000000..7501a3677d5 --- /dev/null +++ b/src/status_im/contexts/communities/actions/share_community/view.cljs @@ -0,0 +1,97 @@ +(ns status-im.contexts.communities.actions.share-community.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.common.qr-codes.view :as qr-codes] + [status-im.common.resources :as resources] + [status-im.contexts.communities.actions.share-community.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [params (quo.context/use-screen-params) + ;; NOTE(seanstrom): We need to store these screen params for when the modal closes + ;; because the screen params will be cleared. + {:keys [url community-id]} @(rn/use-ref-atom params) + window-width (rf/sub [:dimensions/window-width]) + {thumbnail-uri :logo + color :color + community-name :name} (rf/sub [:communities/for-context-tag community-id]) + on-press-share (rn/use-callback + (fn [] + (rf/dispatch + [:open-share + {:options (if platform/ios? + {:activityItemSources + [{:placeholderItem {:type :text + :content url} + :item {:default {:type :text + :content url}} + :linkMetadata {:title (i18n/label + :t/share-community)}}]} + {:title (i18n/label :t/share-community) + :subject (i18n/label :t/share-community) + :message url + :isNewTask true})}])) + [url])] + [quo/overlay {:type :shell} + [rn/view + {:style {:padding-top safe-area/top} + :key :share-community} + [quo/page-nav + {:icon-name :i/close + :on-press navigate-back + :background :blur + :accessibility-label :top-bar}] + [quo/text-combinations + {:container-style style/header-container + :title (i18n/label :t/share-community)}] + [rn/view {:style style/qr-code-wrapper} + [quo/gradient-cover + {:container-style (style/gradient-cover-wrapper window-width) + :customization-color color + :bottom-color-override colors/white-opa-5}] + [rn/view + {:style style/qr-top-wrapper} + [rn/view {:style style/text-wrapper} + (when thumbnail-uri + [fast-image/fast-image + {:source {:uri thumbnail-uri} + :style style/community-avatar}]) + [quo/text + {:size :heading-2 + :weight :semi-bold + :style (style/community-name thumbnail-uri)} + community-name]] + [rn/view {:style style/share-button-container} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :link-to-community + :on-press on-press-share} + :i/share]]] + [rn/view + {:style style/qr-code-view} + [qr-codes/qr-code + {:size (style/qr-code-size window-width) + :url url + :avatar :community + :customization-color color + :picture (or thumbnail-uri (resources/get-mock-image :status-logo))}]]] + [quo/text + {:size :paragraph-2 + :weight :regular + :style style/scan-notice} + (i18n/label :t/scan-with-status-app)]]])) diff --git a/src/status_im/contexts/communities/actions/share_community_channel/style.cljs b/src/status_im/contexts/communities/actions/share_community_channel/style.cljs new file mode 100644 index 00000000000..df81455e993 --- /dev/null +++ b/src/status_im/contexts/communities/actions/share_community_channel/style.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.communities.actions.share-community-channel.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [safe-area-top] + {:padding-top safe-area-top}) + +(def header-container + {:padding-horizontal 20 + :padding-vertical 12}) + +(def scan-notice + {:color colors/white-70-blur + :margin-top 16 + :margin-left :auto + :margin-right :auto}) + +(def qr-code-wrapper + {:padding-horizontal 20 + :margin-top 8 + :align-items :center}) + diff --git a/src/status_im/contexts/communities/actions/share_community_channel/view.cljs b/src/status_im/contexts/communities/actions/share_community_channel/view.cljs new file mode 100644 index 00000000000..df860c3a050 --- /dev/null +++ b/src/status_im/contexts/communities/actions/share_community_channel/view.cljs @@ -0,0 +1,51 @@ +(ns status-im.contexts.communities.actions.share-community-channel.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.qr-codes.view :as qr-codes] + [status-im.contexts.communities.actions.share-community-channel.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back [] (rf/dispatch [:navigate-back])) + +(defn view + [] + (fn [] + (let [params (quo.context/use-screen-params) + ;; NOTE(seanstrom): We need to store these screen params for when the modal closes + ;; because the screen params will be cleared. + {:keys [url chat-id]} @(rn/use-ref-atom params) + {:keys [color emoji chat-name]} (rf/sub [:chats/community-channel-ui-details-by-id chat-id]) + on-share-community-channel (rn/use-callback + #(rf/dispatch + [:communities/share-community-channel-url-with-data + chat-id]) + [chat-id])] + [quo/overlay {:type :shell} + [rn/view + {:style (style/container safe-area/top) + :key :share-community} + [quo/page-nav + {:icon-name :i/close + :on-press navigate-back + :background :blur + :accessibility-label :top-bar}] + [quo/text-combinations + {:container-style style/header-container + :title (i18n/label :t/share-channel)}] + [rn/view {:style style/qr-code-wrapper} + [qr-codes/share-qr-code + {:type :channel + :qr-data url + :customization-color color + :emoji emoji + :full-name chat-name + :on-share-press on-share-community-channel}]] + [quo/text + {:size :paragraph-2 + :weight :regular + :style style/scan-notice} + (i18n/label :t/scan-with-status-app)]]]))) diff --git a/src/status_im/contexts/communities/discover/events.cljs b/src/status_im/contexts/communities/discover/events.cljs new file mode 100644 index 00000000000..3a52b35658e --- /dev/null +++ b/src/status_im/contexts/communities/discover/events.cljs @@ -0,0 +1,92 @@ +(ns status-im.contexts.communities.discover.events + (:require + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(def community-keys-renamed + {:requestedAccessAt :requested-access-at + :fileSize :file-size + :communityTokensMetadata :community-tokens-metadata + :activeMembersCount :active-members-count + :unknownCommunities :unknown-communities + :canRequestAccess :can-request-access? + :adminSettings :admin-settings + :canManageUsers :can-manage-users? + :categoryID :category-id + :canPost :can-post? + :tokenGated :token-gated? + :isControlNode :is-control-node? + :pinMessageAllMembersEnabled :pin-message-all-members-enabled + :isMember :is-member? + :canDeleteMessageForEveryone :can-delete-message-for-everyone? + :tokenPermissions :token-permissions + :muteTill :mute-till + :contractCommunities :contract-communities + :banList :ban-list + :keyUid :key-uid + :memberRole :member-role + :introMessage :intro-message + :contractFeaturedCommunities :contract-featured-communities + :canJoin :can-join? + :outroMessage :outro-message + :resizeTarget :resize-target}) + +(defn rename-contract-community-key + [k] + (let [s (name k) + starts-with-digit? (re-matches #"^\d.*" s) + existing-rename (k community-keys-renamed)] + (cond starts-with-digit? s + existing-rename existing-rename + :else (keyword s)))) + +(defn rename-contract-community-keys + [m] + (reduce (fn [acc [k v]] + (let [new-key (if (keyword? k) (rename-contract-community-key k) k)] + (cond + (map? v) (assoc acc new-key (rename-contract-community-keys v)) + :else (assoc acc new-key v)))) + {} + m)) + +(defn maybe-found-unknown-contract-community + [{:keys [db]} [{:keys [id] :as community}]] + (let [{:keys [contract-communities]} db + {:keys [unknown-featured unknown-other]} contract-communities] + {:db (cond-> db + ((set unknown-featured) id) + (assoc-in [:contract-communities :featured id] community) + ((set unknown-featured) id) + (assoc-in [:contract-communities :unknown-featured] (remove #{id} unknown-featured)) + + ((set unknown-other) id) + (assoc-in [:contract-communities :other id] community) + ((set unknown-other) id) + (assoc-in [:contract-communities :unknown-other] (remove #{id} unknown-other)))})) + +(rf/reg-event-fx :discover-community/maybe-found-unknown-contract-community + maybe-found-unknown-contract-community) + +(defn handle-contract-communities + [{:keys [db]} [contract-communities]] + (let [cc (rename-contract-community-keys contract-communities) + featured (:contract-featured-communities cc) + unknown (:unknown-communities cc) + other (remove (set featured) (:contract-communities cc))] + {:db (assoc db + :contract-communities + {:featured (select-keys (:communities cc) featured) + :unknown-featured (filter (set unknown) featured) + :unknown-other (filter (set unknown) other) + :other (select-keys (:communities cc) other)})})) + +(rf/reg-event-fx :fetched-contract-communities handle-contract-communities) + +(rf/defn fetch-contract-communities + {:events [:fetch-contract-communities]} + [_] + {:json-rpc/call [{:method "wakuext_curatedCommunities" + :params [] + :on-success #(rf/dispatch [:fetched-contract-communities %]) + :on-error #(log/error "failed to fetch contract communities" %)}]}) diff --git a/src/status_im/contexts/communities/discover/events_test.cljs b/src/status_im/contexts/communities/discover/events_test.cljs new file mode 100644 index 00000000000..536eb446014 --- /dev/null +++ b/src/status_im/contexts/communities/discover/events_test.cljs @@ -0,0 +1,160 @@ +(ns status-im.contexts.communities.discover.events-test + (:require + [cljs.test :refer-macros [deftest are]] + matcher-combinators.test + [status-im.contexts.communities.discover.events :as events])) + +(deftest rename-contract-community-key-test + (are [i e] (= (events/rename-contract-community-key i) e) + :requestedAccessAt :requested-access-at + :canDeleteMessageForEveryone :can-delete-message-for-everyone? + :name :name + :0x025d27e58 "0x025d27e58" + :093b4684-92f0 "093b4684-92f0" + :3f9e77b8-97c7 "3f9e77b8-97c7")) + +(deftest rename-contract-community-keys-test + (are [i e] (match? (events/rename-contract-community-keys i) e) + + {:contractCommunities + ["0x032aa2439b14eb2caaf724223951da89de1530fbfa5d5a639b93b82cd5341713fd" + "0x025d27e58f1bece7d9675e6fe907da6e3b5007f06a327dd20db04d68851b9c153d" + "0x03769999b26cfd88788c882b56232fa5f58735795bdd463820d127b9a23d4415d4"] + :contractFeaturedCommunities + ["0x03769999b26cfd88788c882b56232fa5f58735795bdd463820d127b9a23d4415d4"] + :communities + {:0x025d27e58f1bece7d9675e6fe907da6e3b5007f06a327dd20db04d68851b9c153d + {:name "foo" + :tokenPermissions nil + :isMember false + :canDeleteMessageForEveryone false} + :0x032aa2439b14eb2caaf724223951da89de1530fbfa5d5a639b93b82cd5341713fd + {:name "bar" + :adminSettings {:pinMessageAllMembersEnabled false} + :chats + {:8aa21892-a768-488a-8655-7ed31f527642 + {:name "web" + :categoryID "d87621f7-315a-4579-a800-fde42d86ddf2" + :canPost false} + :6efc1767-22e5-46c1-9865-87206e04c9f9 + {:name "mobile-ui" + :categoryID "7445461a-bd10-4c54-86fe-70403913faf4" + :canPost false}}} + :0x03769999b26cfd88788c882b56232fa5f58735795bdd463820d127b9a23d4415d4 + {:name "baz" + :categories + {:18444776-8720-4955-b200-a55758889f43 {:name "bar" :position 0} + :093b4684-92f0-42dd-977c-05affc451ec8 {:name "foo" :position 1}} + :members + {:0x04a25e0a58ab97c6f93298ad87ab73aae5995365088a1ba150633ea25ff0b80cc1ddba18b122d117e477963cec1962219362a9a77226a6bf9a15d8d02c35fe8c9b + {} + :0x0490d2bb47388504e4b615052566e5830662bf202eb179251e9118587ce628c6c76e1f4550f9cd52058cf9dbdb5b788eea10b7c765cd7565675daa5f822acab8f4 + {}}}} + :unknownCommunities nil} + + {:contract-communities + ["0x032aa2439b14eb2caaf724223951da89de1530fbfa5d5a639b93b82cd5341713fd" + "0x025d27e58f1bece7d9675e6fe907da6e3b5007f06a327dd20db04d68851b9c153d" + "0x03769999b26cfd88788c882b56232fa5f58735795bdd463820d127b9a23d4415d4"] + :contract-featured-communities + ["0x03769999b26cfd88788c882b56232fa5f58735795bdd463820d127b9a23d4415d4"] + :communities + {"0x025d27e58f1bece7d9675e6fe907da6e3b5007f06a327dd20db04d68851b9c153d" + {:name "foo" + :token-permissions nil + :is-member? false + :can-delete-message-for-everyone? false} + "0x032aa2439b14eb2caaf724223951da89de1530fbfa5d5a639b93b82cd5341713fd" + {:name "bar" + :admin-settings {:pin-message-all-members-enabled false} + :chats + {"8aa21892-a768-488a-8655-7ed31f527642" + {:name "web" + :category-id "d87621f7-315a-4579-a800-fde42d86ddf2" + :can-post? false} + "6efc1767-22e5-46c1-9865-87206e04c9f9" + {:name "mobile-ui" + :category-id "7445461a-bd10-4c54-86fe-70403913faf4" + :can-post? false}}} + "0x03769999b26cfd88788c882b56232fa5f58735795bdd463820d127b9a23d4415d4" + {:name "baz" + :categories + {"18444776-8720-4955-b200-a55758889f43" {:name "bar" :position 0} + "093b4684-92f0-42dd-977c-05affc451ec8" {:name "foo" :position 1}} + :members + {"0x04a25e0a58ab97c6f93298ad87ab73aae5995365088a1ba150633ea25ff0b80cc1ddba18b122d117e477963cec1962219362a9a77226a6bf9a15d8d02c35fe8c9b" + {} + "0x0490d2bb47388504e4b615052566e5830662bf202eb179251e9118587ce628c6c76e1f4550f9cd52058cf9dbdb5b788eea10b7c765cd7565675daa5f822acab8f4" + {}}}} + :unknown-communities nil})) + +(deftest handle-contract-communities-test + (are [input-contract-communities + expected-contract-communities-in-db] + (match? + (get-in (events/handle-contract-communities {:db nil} [input-contract-communities]) + [:db :contract-communities]) + expected-contract-communities-in-db) + + {:communities {} + :contractFeaturedCommunities [] + :contractCommunities [] + :unknownCommunities []} + {:featured {} + :other {} + :unknown-featured '() + :unknown-other '()} + + {:communities {"a" {:id "a"}} + :contractFeaturedCommunities ["a" "b"] + :contractCommunities ["a" "b" "c"] + :unknownCommunities ["b" "c"]} + {:featured {"a" {:id "a"}} + :other {} + :unknown-featured '("b") + :unknown-other '("c")})) + +(deftest maybe-found-unknown-contract-community-test + (are [contract-communities-already-in-db + input-found-community + expected-contract-communities-in-db] + (match? + (get-in (events/maybe-found-unknown-contract-community + {:db {:contract-communities contract-communities-already-in-db}} + [input-found-community]) + [:db :contract-communities]) + expected-contract-communities-in-db) + + nil {:id "community-a"} nil + + {:featured {"a" {:id "a"}} + :other {"b" {:id "b"}} + :unknown-featured '("c") + :unknown-other '("d")} + {:id "c"} + {:featured {"a" {:id "a"} + "c" {:id "c"}} + :other {"b" {:id "b"}} + :unknown-featured '() + :unknown-other '("d")} + + {:featured {"a" {:id "a"}} + :other {"b" {:id "b"}} + :unknown-featured '("c") + :unknown-other '("d")} + {:id "d"} + {:featured {"a" {:id "a"}} + :other {"b" {:id "b"} + "d" {:id "d"}} + :unknown-featured '("c") + :unknown-other '()} + + {:featured {"a" {:id "a"}} + :other {"b" {:id "b"}} + :unknown-featured '("c") + :unknown-other '("d")} + {:id "e"} + {:featured {"a" {:id "a"}} + :other {"b" {:id "b"}} + :unknown-featured '("c") + :unknown-other '("d")})) diff --git a/src/status_im/contexts/communities/discover/style.cljs b/src/status_im/contexts/communities/discover/style.cljs new file mode 100644 index 00000000000..818b7e444e0 --- /dev/null +++ b/src/status_im/contexts/communities/discover/style.cljs @@ -0,0 +1,76 @@ +(ns status-im.contexts.communities.discover.style + (:require + [status-im.contexts.shell.constants :as shell.constants])) + +(def header-height 56) + +(defn screen-title-container + [safe-area-top] + {:height header-height + :padding-vertical 12 + :justify-content :center + :margin-horizontal 20 + :margin-top (+ header-height safe-area-top)}) + +(def featured-communities-header + {:flex-direction :row + :height 30 + :padding-top 6 + :margin-bottom 7 + :padding-right 20 + :margin-left 20 + :align-items :center + :justify-content :space-between}) + +(def featured-communities-title-container + {:flex-direction :row + :align-items :center}) + +(def featured-list-container + {:flex-direction :row + :overflow :hidden}) + +(def flat-list-container + {:padding-bottom 24 + :padding-horizontal 20}) + +(def other-communities-container + {:flex 1 + :padding-bottom (+ shell.constants/floating-shell-button-height 34) + :margin-horizontal 20}) + +(defn discover-communities-segments + [fixed?] + (merge + {:padding-vertical 12 + :height header-height + :background-color :transparent} + (when-not fixed? + {:margin-top 12 + :margin-horizontal 20 + :margin-bottom 4}))) + +(defn discover-screen-container + [background-color] + {:background-color background-color + :position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(def communities-header-container + {:align-items :center + :justify-content :center}) + +(defn blur-tabs-header + [safe-area-top] + {:padding-horizontal 20 + :position :absolute + :top (+ header-height safe-area-top) + :height header-height + :right 0 + :left 0 + :justify-content :center + :flex 1 + :background-color :transparent}) diff --git a/src/status_im/contexts/communities/discover/view.cljs b/src/status_im/contexts/communities/discover/view.cljs new file mode 100644 index 00000000000..b62aaa7a81c --- /dev/null +++ b/src/status_im/contexts/communities/discover/view.cljs @@ -0,0 +1,234 @@ +(ns status-im.contexts.communities.discover.view + (:require + [oops.core :as oops] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.scroll-page.view :as scroll-page] + [status-im.config :as config] + [status-im.contexts.communities.actions.community-options.view :as options] + [status-im.contexts.communities.discover.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn community-list-item + [community-item _ _ {:keys [width view-type]}] + (let [item community-item + cover {:uri (get-in (:images item) [:banner :uri])}] + (if (= view-type :card-view) + [quo/community-card-view-item + {:community (assoc item :cover cover) + :width width + :on-press #(rf/dispatch [:communities/navigate-to-community-overview (:id item)])}] + [quo/community-list + {:on-press (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:communities/navigate-to-community-overview (:id item)])) + :on-long-press #(rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [options/community-options-bottom-sheet (:id item)])}])}]))) + +(defn screen-title + [] + [rn/view {:style (style/screen-title-container safe-area/top)} + [quo/text + {:accessibility-label :communities-screen-title + :weight :semi-bold + :size :heading-1} + (i18n/label :t/discover-communities)]]) + +(defn featured-communities-header + [communities-count] + (let [theme (quo.context/use-theme)] + [rn/view + {:style style/featured-communities-header} + [rn/view + {:style style/featured-communities-title-container} + [quo/text + {:accessibility-label :featured-communities-title + :weight :semi-bold + :size :paragraph-1 + :style {:margin-right 6}} + (i18n/label :t/featured)] + [quo/counter {:type :grey} communities-count]] + [quo/icon :i/info + {:container-style style/communities-header-container + :resize-mode :center + :size 20 + :color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme)}]])) + +(defn discover-communities-segments + [selected-tab fixed] + [rn/view + {:style (style/discover-communities-segments fixed)} + [quo/tabs + {:size 32 + :on-change #(reset! selected-tab %) + :default-active :all + :data [{:id :all + :label (i18n/label :t/all) + :accessibility-label :all-communities-tab} + (when config/show-not-implemented-features? + {:id :open + :label (i18n/label :t/open) + :accessibility-label :open-communities-tab}) + (when config/show-not-implemented-features? + {:id :gated + :label (i18n/label :t/gated) + :accessibility-label :gated-communities-tab})]}]]) + +(defn loading-community-item + [_ _ _ {:keys [width]}] + [quo/community-card-view-item + {:width width + :loading? true}]) + +(def loading-instances-to-show 3) + +(defn featured-list + [communities view-type] + (let [view-size (reagent/atom 0) + set-view-size (fn [e] + (reset! view-size (- (oops/oget e "nativeEvent.layout.width") 40))) + loaded? (and communities (pos? (count communities)))] + (fn [] + [rn/view + {:style style/featured-list-container + :on-layout set-view-size} + (when-not (= @view-size 0) + [rn/flat-list + {:key-fn :id + :horizontal true + :keyboard-should-persist-taps :always + :shows-horizontal-scroll-indicator false + :nestedScrollEnabled true + :separator [rn/view {:width 12}] + :data (if loaded? communities (range loading-instances-to-show)) + :render-fn (if loaded? community-list-item loading-community-item) + :render-data {:width @view-size + :view-type view-type} + :content-container-style style/flat-list-container}])]))) + +(defn discover-communities-header + [{:keys [featured-communities-count + featured-communities + view-type + selected-tab]}] + [rn/view + [screen-title] + (when (pos? featured-communities-count) + [:<> + [featured-communities-header featured-communities-count] + [featured-list featured-communities view-type] + [quo/separator {:style {:margin-horizontal 20}}]]) + [discover-communities-segments selected-tab false]]) + +(defn other-communities-list + [{:keys [communities view-type]}] + [rn/view {:style style/other-communities-container} + (if (seq communities) + (map-indexed (fn [inner-index {community-id :id :as community}] + (let [cover {:uri (-> community :images :banner :uri)}] + [rn/view + {:key (str inner-index community-id) + :style {:margin-bottom 16}} + (if (= view-type :card-view) + [quo/community-card-view-item + {:community (assoc community + :cover cover + :members-count (count (:members community))) + :on-press #(rf/dispatch [:communities/navigate-to-community-overview + community-id])}] + [quo/community-list + {:on-press (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:communities/navigate-to-community-overview + community-id])) + :on-long-press #(js/alert "TODO: to be implemented")} + community])])) + communities) + [:<> + [rn/view {:margin-bottom 16} [quo/community-card-view-item {:loading? true}]] + [rn/view {:margin-bottom 16} [quo/community-card-view-item {:loading? true}]] + [quo/community-card-view-item {:loading? true}]])]) + +(defn communities-lists + [selected-tab view-type] + [rn/view {:style {:flex 1}} + (case @selected-tab + :all + [other-communities-list + {:communities (rf/sub [:communities/other-contract-communities]) + :view-type view-type}] + + :open + [:<>] + + :gated + [:<>] + + [quo/information-box + {:type :error + :icon :i/info} + (i18n/label :t/error)])]) + +(defn render-communities + [{:keys [selected-tab featured-communities-count featured-communities view-type]}] + [rn/view + [discover-communities-header + {:selected-tab selected-tab + :view-type view-type + :featured-communities-count featured-communities-count + :featured-communities featured-communities}] + [communities-lists selected-tab view-type]]) + +(defn render-sticky-header + [{:keys [selected-tab scroll-height]}] + (when (> @scroll-height 360) + [rn/view + {:style (style/blur-tabs-header safe-area/top)} + [discover-communities-segments selected-tab true]])) + +(defn discover-screen-content + [] + (let [view-type (reagent/atom :card-view) + selected-tab (reagent/atom :all) + scroll-height (reagent/atom 0)] + (fn [] + (let [theme (quo.context/use-theme) + featured-communities (rf/sub [:communities/featured-contract-communities]) + featured-communities-count (count featured-communities)] + [scroll-page/scroll-page + {:on-scroll #(reset! scroll-height %) + :page-nav-props {:background :blur} + :navigate-back? :true + :height (if (> @scroll-height 360) + 208 + 148) + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :sticky-header [render-sticky-header + {:selected-tab selected-tab + :scroll-height scroll-height}]} + [render-communities + {:selected-tab selected-tab + :featured-communities-count featured-communities-count + :featured-communities featured-communities + :view-type @view-type}]])))) + +(defn view + [] + (let [theme (quo.context/use-theme)] + (rn/use-mount #(rf/dispatch [:fetch-contract-communities])) + [rn/view + {:style (style/discover-screen-container (colors/theme-colors + colors/white + colors/neutral-95 + theme))} + [discover-screen-content]])) diff --git a/src/status_im/contexts/communities/events.cljs b/src/status_im/contexts/communities/events.cljs new file mode 100644 index 00000000000..425dcf7e4a0 --- /dev/null +++ b/src/status_im/contexts/communities/events.cljs @@ -0,0 +1,425 @@ +(ns status-im.contexts.communities.events + (:require + [clojure.string :as string] + [legacy.status-im.data-store.communities :as data-store.communities] + [oops.core :as oops] + [schema.core :as schema] + [status-im.constants :as constants] + status-im.contexts.communities.actions.accounts-selection.events + status-im.contexts.communities.actions.addresses-for-permissions.events + status-im.contexts.communities.actions.airdrop-addresses.events + status-im.contexts.communities.actions.community-options.events + status-im.contexts.communities.actions.leave.events + [status-im.contexts.communities.utils :as utils] + [status-im.navigation.events :as navigation] + [status-im.navigation.transitions :as transitions] + [taoensso.timbre :as log] + [utils.collection :as collection-utils] + [utils.re-frame :as rf])) + +(defn handle-community + [{:keys [db]} [community-js]] + (when community-js + (let [{:keys [clock token-permissions-check id last-opened-at] + :as community} (data-store.communities/<-rpc community-js) + previous-last-opened-at (get-in db [:communities id :last-opened-at])] + (when (and id (>= clock (get-in db [:communities id :clock]))) + {:db (assoc-in db + [:communities id] + (assoc community :last-opened-at (max last-opened-at previous-last-opened-at))) + ;; NOTE(cammellos): these two looks suspicious, we should not check for permissions at + ;; every event signalled + :fx [[:dispatch + [:communities/check-permissions-to-join-community-with-all-addresses id]] + (when (nil? token-permissions-check) + [:dispatch [:communities/check-permissions-to-join-community id]])]})))) + +(rf/reg-event-fx :communities/handle-community handle-community) + +(rf/defn handle-removed-chats + [{:keys [db]} chat-ids] + {:db (reduce (fn [db chat-id] + (update db :chats dissoc chat-id)) + db + chat-ids)}) + +(defn- handle-my-request + [db {:keys [community-id state deleted] :as request}] + (cond (and (= constants/community-request-to-join-state-pending state) (not deleted)) + (assoc-in db [:communities/my-pending-requests-to-join community-id] request) + (and (= constants/community-request-to-join-state-accepted state) (not deleted)) + (update-in db [:communities/my-pending-requests-to-join] dissoc community-id) + :else (update-in db [:communities/my-pending-requests-to-join] dissoc community-id))) + +(defn handle-admin-request + [db {:keys [id community-id deleted] :as request}] + (if deleted + (update-in db [:communities/requests-to-join community-id] dissoc id) + (assoc-in db [:communities/requests-to-join community-id id] request))) + +(rf/defn handle-requests-to-join + [{:keys [db]} requests] + (let [my-public-key (get-in db [:profile/profile :public-key])] + {:db (reduce (fn [db {:keys [public-key] :as request}] + (let [my-request? (= my-public-key public-key)] + (if my-request? + (handle-my-request db request) + (handle-admin-request db request)))) + db + requests)})) + +(rf/defn handle-communities + {:events [:community/fetch-success]} + [_ communities-js] + {:fx (map (fn [c] [:dispatch [:communities/handle-community c]]) + communities-js)}) + +(rf/reg-event-fx :communities/request-to-join-result + (fn [{:keys [db]} [community-id request-id response-js]] + {:db (update-in db [:communities/requests-to-join community-id] dissoc request-id) + :dispatch-n [[:sanitize-messages-and-process-response response-js] + [:activity-center.notifications/mark-as-read request-id]]})) + +(rf/reg-event-fx :communities/decline-request-to-join-pressed + (fn [_ [community-id request-id]] + {:json-rpc/call + [{:method "wakuext_declineRequestToJoinCommunity" + :params [{:id request-id}] + :js-response true + :on-success #(rf/dispatch [:communities/request-to-join-result community-id request-id %]) + :on-error #(log/error "failed to decline " community-id request-id)}]})) + +(rf/reg-event-fx :communities/accept-request-to-join-pressed + (fn [_ [community-id request-id]] + {:json-rpc/call + [{:method "wakuext_acceptRequestToJoinCommunity" + :params [{:id request-id}] + :js-response true + :on-success #(rf/dispatch [:communities/request-to-join-result community-id request-id %]) + :on-error #(log/error "failed to accept requests-to-join" community-id request-id %)}]})) + +(rf/reg-event-fx :communities/get-user-requests-to-join-success + (fn [{:keys [db]} [requests]] + {:db (assoc db + :communities/my-pending-requests-to-join + (data-store.communities/<-requests-to-join-community-rpc requests :communityId))})) + +(rf/reg-event-fx :communities/get-user-requests-to-join + (fn [_] + {:json-rpc/call [{:method "wakuext_myPendingRequestsToJoin" + :params [] + :on-success #(rf/dispatch [:communities/get-user-requests-to-join-success %]) + :on-error #(log/error "failed to get requests to join community")}]})) + +(rf/reg-event-fx :communities/fetched-collapsed-categories-success + (fn [{:keys [db]} [categories]] + {:db (assoc db + :communities/collapsed-categories + (reduce + (fn [acc {:keys [communityId categoryId]}] + (assoc-in acc [communityId categoryId] true)) + {} + categories))})) + +(rf/reg-event-fx :community/fetch-low-priority + (fn [] + {:fx [[:json-rpc/call + [{:method "wakuext_checkAndDeletePendingRequestToJoinCommunity" + :params [] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error #(log/info "failed to fetch communities" %)} + {:method "wakuext_collapsedCommunityCategories" + :params [] + :on-success [:communities/fetched-collapsed-categories-success] + :on-error #(log/error "failed to fetch collapsed community categories" %)}]]]})) + +(rf/reg-event-fx :community/fetch + (fn [_] + {:fx [[:json-rpc/call + [{:method "wakuext_serializedCommunities" + :params [] + :on-success [:community/fetch-success] + :js-response true + :on-error #(log/error "failed to fetch communities" %)}]] + ;; Dispatch a little after 1000ms because other higher-priority events + ;; after login are being processed at the 1000ms mark. + [:dispatch-later [{:ms 1200 :dispatch [:community/fetch-low-priority]}]]]})) + +(defn update-previous-permission-addresses + [{:keys [db]} [community-id]] + (when community-id + (let [accounts (utils/sorted-operable-non-watch-only-accounts db) + selected-permission-addresses (get-in db + [:communities community-id + :selected-permission-addresses]) + selected-accounts (filter #(contains? selected-permission-addresses (:address %)) + accounts) + current-airdrop-address (get-in db [:communities community-id :airdrop-address]) + share-all-addresses? (get-in db [:communities community-id :share-all-addresses?])] + {:db (update-in db + [:communities community-id] + assoc + :previous-share-all-addresses? share-all-addresses? + :previous-permission-addresses selected-permission-addresses + :airdrop-address (if (contains? selected-permission-addresses + current-airdrop-address) + current-airdrop-address + (:address (first selected-accounts))))}))) + +(rf/reg-event-fx :communities/update-previous-permission-addresses + update-previous-permission-addresses) + +(defn toggle-selected-permission-address + [{:keys [db]} [address community-id]] + (let [selected-permission-addresses + (get-in db [:communities community-id :selected-permission-addresses]) + updated-selected-permission-addresses + (if (contains? selected-permission-addresses address) + (disj selected-permission-addresses address) + (conj selected-permission-addresses address))] + {:db (assoc-in db + [:communities community-id :selected-permission-addresses] + updated-selected-permission-addresses) + :fx [(when community-id + [:dispatch + [:communities/check-permissions-to-join-community community-id + updated-selected-permission-addresses :based-on-client-selection]])]})) + +(rf/reg-event-fx :communities/toggle-selected-permission-address + toggle-selected-permission-address) + +(defn toggle-share-all-addresses + [{:keys [db]} [community-id]] + (let [share-all-addresses? (get-in db [:communities community-id :share-all-addresses?]) + next-share-all-addresses? (not share-all-addresses?) + accounts (utils/sorted-operable-non-watch-only-accounts db) + addresses (set (map :address accounts))] + {:db (update-in db + [:communities community-id] + assoc + :share-all-addresses? next-share-all-addresses? + :selected-permission-addresses addresses) + :fx [(when (and community-id next-share-all-addresses?) + [:dispatch + [:communities/check-permissions-to-join-community community-id + addresses :based-on-client-selection]])]})) + +(rf/reg-event-fx :communities/toggle-share-all-addresses + toggle-share-all-addresses) + +(rf/reg-event-fx :communities/reset-selected-permission-addresses + (fn [{:keys [db]} [community-id]] + (when community-id + {:db (update-in db + [:communities community-id] + assoc + :selected-permission-addresses + (get-in db [:communities community-id :previous-permission-addresses]) + :share-all-addresses? + (get-in db [:communities community-id :previous-share-all-addresses?])) + :fx [[:dispatch [:communities/check-permissions-to-join-community community-id]]]}))) + +(defn community-fetched + [{:keys [db]} [community-id community-js]] + (when community-js + {:db (update db :communities/fetching-communities dissoc community-id) + :fx [[:dispatch [:communities/handle-community community-js]] + [:dispatch [:chat.ui/spectate-community community-id]]]})) + +(rf/reg-event-fx :chat.ui/community-fetched community-fetched) + +(defn community-failed-to-fetch + [{:keys [db]} [community-id]] + {:db (update db :communities/fetching-communities dissoc community-id)}) + +(rf/reg-event-fx :chat.ui/community-failed-to-fetch community-failed-to-fetch) + +(defn- failed-to-fetch-community + [community-id err] + (rf/dispatch [:chat.ui/community-failed-to-fetch community-id]) + (log/error {:message + "Failed to request community info from mailserver" + :error err})) + +(defn fetch-community + [{:keys [db]} [{:keys [community-id]}]] + (when (and community-id + (not (get-in db [:communities community-id])) + (not (get-in db [:communities/fetching-communities community-id]))) + {:db (assoc-in db [:communities/fetching-communities community-id] true) + :json-rpc/call [{:method "wakuext_fetchCommunity" + :params [{:CommunityKey community-id + :TryDatabase true + :WaitForResponse true}] + :js-response true + :on-success (fn [community] + (if community + (rf/dispatch [:chat.ui/community-fetched community-id + community]) + (failed-to-fetch-community + community-id + "community wasn't found at the store node"))) + :on-error (partial failed-to-fetch-community community-id)}]})) + +(schema/=> fetch-community + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:schema + [:vector + [:map {:closed true} + [:community-id {:optional true} :string]]]]]] + [:maybe + [:map + [:db map?] + [:json-rpc/call :schema.common/rpc-call]]]]) + +(rf/reg-event-fx :communities/fetch-community fetch-community) + +(defn spectate-community-success + [{:keys [db]} [response-js]] + (when response-js + (let [communities (oops/oget response-js "communities") + community (first communities) + community-id (when community (oops/oget community "id"))] + (when community + {:db (-> db + (assoc-in [:communities community-id :spectated] true)) + :fx [[:dispatch [:communities/handle-community community]]]})))) + +(rf/reg-event-fx :chat.ui/spectate-community-success spectate-community-success) + +(defn spectate-community + [{:keys [db]} [community-id]] + (let [{:keys [spectated joined]} (get-in db [:communities community-id])] + (when (and (not joined) (not spectated)) + {:json-rpc/call [{:method "wakuext_spectateCommunity" + :params [community-id] + :js-response true + :on-success [:chat.ui/spectate-community-success] + :on-error (fn [err] + (log/error {:message + "Failed to spectate community" + :error err}))}]}))) + +(schema/=> spectate-community + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:schema [:catn [:community-id [:? :string]]]]]] + [:maybe + [:map + [:json-rpc/call :schema.common/rpc-call]]]]) + +(rf/reg-event-fx :chat.ui/spectate-community spectate-community) + +(defn navigate-to-serialized-community + [community-id] + {:serialization/deserialize-and-compress-key + {:serialized-key community-id + :on-success #(rf/dispatch [:communities/navigate-to-community-overview %]) + :on-error #(log/error {:message "Failed to decompress community-id" + :error % + :community-id community-id})}}) + +(rf/defn navigate-to-community-overview + [{:keys [db] :as cofx} [community-id]] + (let [current-view-id (:view-id db)] + (if (string/starts-with? community-id constants/serialization-key) + (navigate-to-serialized-community community-id) + (rf/merge + cofx + {:fx [[:dispatch [:chat.ui/spectate-community community-id]] + [:dispatch [:communities/update-last-opened-at community-id]] + [:dispatch + [:communities/fetch-community {:community-id community-id}]] + [:dispatch [:navigate-to :screen/community-overview community-id]] + (when (get-in db [:communities community-id :joined]) + [:dispatch + [:activity-center.notifications/dismiss-community-overview community-id]])]} + (when-not (#{:screen/communities-stack :screen/discover-communities} current-view-id) + (navigation/pop-to-root :screen/shell-stack)))))) + +(rf/reg-event-fx :communities/navigate-to-community-overview navigate-to-community-overview) + +(defn navigate-to-community-chat + [{:keys [db]} [chat-id pop-to-root? community-id]] + (let [community-id-in-chats (get-in db [:chats chat-id :community-id]) + community-id (or community-id community-id-in-chats)] + (cond-> {:fx [(when community-id + [:dispatch [:communities/fetch-community {:community-id community-id}]]) + (if pop-to-root? + [:dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]] + [:dispatch + [:chat/navigate-to-chat chat-id transitions/stack-slide-transition]])]} + (not community-id-in-chats) + (assoc :db (assoc-in db [:chats chat-id :community-id] community-id))))) + +(rf/reg-event-fx :communities/navigate-to-community-chat navigate-to-community-chat) + +(defn get-revealed-accounts + [{:keys [db]} [community-id on-success]] + (let [{:keys [joined fetching-revealed-accounts] + :as community} (get-in db [:communities community-id])] + (when (and community joined (not fetching-revealed-accounts)) + {:db (assoc-in db [:communities community-id :fetching-revealed-accounts] true) + :json-rpc/call + [{:method "wakuext_latestRequestToJoinForCommunity" + :params [community-id] + :on-success [:communities/get-revealed-accounts-success community-id on-success] + :on-error (fn [err] + (log/error {:message "failed to fetch revealed accounts" + :community-id community-id + :err err}) + (rf/dispatch [:communities/get-revealed-accounts-failed community-id]))}]}))) + +(rf/reg-event-fx :communities/get-revealed-accounts get-revealed-accounts) + +(schema/=> get-revealed-accounts + [:=> + [:catn + [:cofx :schema.re-frame/cofx] + [:args + [:schema + [:catn + [:community-id [:? :string]] + [:on-success [:? :schema.re-frame/event]]]]]] + [:maybe + [:map + [:db map?] + [:json-rpc/call :schema.common/rpc-call]]]]) + +(rf/reg-event-fx :communities/get-revealed-accounts-success + (fn [{:keys [db]} [community-id on-success request-to-join]] + (when-let [community (get-in db [:communities community-id])] + (let [revealed-accounts (collection-utils/index-by :address (:revealedAccounts request-to-join)) + share-future-addresses? (:shareFutureAddresses request-to-join) + community-with-revealed-accounts + (-> community + (assoc :revealed-accounts revealed-accounts) + (assoc :share-future-addresses? share-future-addresses?) + (dissoc :fetching-revealed-accounts))] + {:db (assoc-in db [:communities community-id] community-with-revealed-accounts) + :fx [(when (vector? on-success) + [:dispatch (conj on-success revealed-accounts share-future-addresses?)])]})))) + +(rf/reg-event-fx :communities/get-revealed-accounts-failed + (fn [{:keys [db]} [community-id]] + (when (get-in db [:communities community-id]) + {:db (update-in db [:communities community-id] dissoc :fetching-revealed-accounts)}))) + +(rf/reg-event-fx :communities/update-last-opened-at + (fn [_ [community-id]] + {:json-rpc/call [{:method "wakuext_communityUpdateLastOpenedAt" + :params [community-id] + :on-success #(rf/dispatch [:communities/update-last-opened-at-success community-id + %]) + :on-error #(log/error (str "failed to update last opened at for community " + %))}]})) + +(rf/reg-event-fx :communities/update-last-opened-at-success + (fn [{:keys [db]} [community-id last-opened-at]] + {:db (assoc-in db [:communities community-id :last-opened-at] last-opened-at)})) diff --git a/src/status_im/contexts/communities/events_test.cljs b/src/status_im/contexts/communities/events_test.cljs new file mode 100644 index 00000000000..4edd6376676 --- /dev/null +++ b/src/status_im/contexts/communities/events_test.cljs @@ -0,0 +1,186 @@ +(ns status-im.contexts.communities.events-test + (:require + [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.contexts.communities.events :as events])) + +(def community-id "community-id") + +(deftest fetch-community-test + (testing "with community id" + (testing "update fetching indicator in db" + (is (match? + {:db {:communities/fetching-communities {community-id true}}} + (events/fetch-community {} [{:community-id community-id}])))) + (testing "call the fetch community rpc method with correct community id" + (is (match? + {:json-rpc/call [{:method "wakuext_fetchCommunity" + :params [{:CommunityKey community-id + :TryDatabase true + :WaitForResponse true}]}]} + (events/fetch-community {} [{:community-id community-id}]))))) + (testing "with no community id" + (testing "do nothing" + (is (match? + nil + (events/fetch-community {} [{}])))))) + +(deftest community-failed-to-fetch-test + (testing "given a community id" + (testing "remove community id from fetching indicator in db" + (is (match? + nil + (get-in (events/community-failed-to-fetch {:db {:communities/fetching-communities + {community-id true}}} + [community-id]) + [:db :communities/fetching-communities community-id])))))) + +(deftest community-fetched-test + (testing "given a community" + (let [cofx {:db {:communities/fetching-communities {community-id true}}} + arg [community-id {:id community-id}]] + (testing "remove community id from fetching indicator in db" + (is (match? + nil + (get-in (events/community-fetched cofx arg) + [:db :communities/fetching-communities community-id])))) + (testing "dispatch fxs" + (is (match? + {:fx [[:dispatch [:communities/handle-community {:id community-id}]] + [:dispatch [:chat.ui/spectate-community community-id]]]} + (events/community-fetched cofx arg)))))) + (testing "given a joined community" + (let [cofx {:db {:communities/fetching-communities {community-id true}}} + arg [community-id {:id community-id :joined true}]] + (testing "dispatch fxs, do not spectate community" + (is (match? + {:fx [[:dispatch [:communities/handle-community {:id community-id}]] + [:dispatch [:chat.ui/spectate-community community-id]]]} + (events/community-fetched cofx arg)))))) + (testing "given a token-gated community" + (let [cofx {:db {:communities/fetching-communities {community-id true}}} + arg [community-id {:id community-id :tokenPermissions [1]}]] + (testing "dispatch fxs, do not spectate community" + (is (match? + {:fx [[:dispatch [:communities/handle-community {:id community-id}]] + [:dispatch [:chat.ui/spectate-community community-id]]]} + (events/community-fetched cofx arg)))))) + (testing "given nil community" + (testing "do nothing" + (is (match? + nil + (events/community-fetched {} [community-id nil])))))) + +(deftest spectate-community-test + (testing "given a joined community" + (testing "do nothing" + (is (match? + nil + (events/spectate-community {:db {:communities {community-id {:joined true}}}} + [community-id]))))) + (testing "given a spectated community" + (testing "do nothing" + (is (match? + nil + (events/spectate-community {:db {:communities {community-id {:spectated true}}}} + [community-id]))))) + + (testing "given a community" + (testing "call spectate community rpc with correct community id" + (is (match? + {:json-rpc/call [{:method "wakuext_spectateCommunity" + :params [community-id]}]} + (events/spectate-community {:db {:communities {community-id {}}}} [community-id])))))) + +(deftest spectate-community-success-test + (let [community-id-1 1 + community-id-2 2] + (testing "given communities" + (testing "mark first community spectated true" + (is (match? + {:db {:communities {community-id-1 {:spectated true}}}} + (events/spectate-community-success {} + (clj->js [{:communities [{:id community-id-1} + {:id community-id-2}]}]))))) + (testing "dispatch fxs for first community" + ;; Convert the JavaScript object back to a Clojure map because `match?` does not work well + ;; with JavaScript objects. + (is (match? + {:fx [[:dispatch [:communities/handle-community {:id community-id-1}]]]} + (let [result (events/spectate-community-success {} + (clj->js [{:communities + [{:id community-id-1} + {:id community-id-2}]}]))] + (update-in result [:fx 0 1] #(js->clj % :keywordize-keys true))))))) + (testing "given empty community" + (testing "do nothing" + (is (match? + nil + (events/spectate-community-success {} + (clj->js [{:communities []}])))))) + (testing "given nil community" + (testing "do nothing" + (is (match? + nil + (events/spectate-community-success {} + (clj->js [])))))))) + + +(deftest get-revealed-accounts-test + (let [community {:id community-id}] + (testing "given a unjoined community" + (is (match? + nil + (events/get-revealed-accounts {:db {:communities {community-id community}}} [community-id])))) + + (testing "given a already :fetching-revealed-accounts community" + (is (match? + nil + (events/get-revealed-accounts + {:db {:communities {community-id (assoc community :fetching-revealed-accounts true)}}} + [community-id])))) + + (testing "given joined community" + (let [community (assoc community :joined true) + db {:communities {community-id community} + :profile/profile {:public-key "profile-public-key"}} + effects (events/get-revealed-accounts {:db db} [community-id])] + (is (match? (assoc-in db [:communities community-id :fetching-revealed-accounts] true) + (:db effects))) + (is (match? {:method "wakuext_latestRequestToJoinForCommunity" + :params [community-id]} + (-> effects :json-rpc/call first (select-keys [:method :params])))))))) + +(deftest handle-community-test + (let [community #js {:id community-id :clock 2}] + (testing "given a unjoined community" + (let [effects (events/handle-community {} [community])] + (is (match? community-id + (-> effects :db :communities (get community-id) :id))) + (is (match? + [[:dispatch + [:communities/check-permissions-to-join-community-with-all-addresses community-id]] + [:dispatch [:communities/check-permissions-to-join-community community-id]]] + (filter some? (:fx effects)))))) + + (testing "given a joined community" + (let [community #js {:id community-id :clock 2 :joined true} + effects (events/handle-community {} [community])] + (is (match? + [[:dispatch + [:communities/check-permissions-to-join-community-with-all-addresses community-id]] + [:dispatch [:communities/check-permissions-to-join-community community-id]]] + (filter some? (:fx effects)))))) + + (testing "given a community with token-permissions-check" + (let [community #js + {:id community-id :clock 2 :token-permissions-check :fake-token-permissions-check} + effects (events/handle-community {} [community])] + (is (match? + [[:dispatch + [:communities/check-permissions-to-join-community-with-all-addresses community-id]]] + (filter some? (:fx effects)))))) + + (testing "given a community with lower clock" + (let [effects (events/handle-community {:db {:communities {community-id {:clock 3}}}} [community])] + (is (nil? effects)))))) diff --git a/src/status_im/contexts/communities/home/events.cljs b/src/status_im/contexts/communities/home/events.cljs new file mode 100644 index 00000000000..3d2b6468e06 --- /dev/null +++ b/src/status_im/contexts/communities/home/events.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.communities.home.events + (:require + [utils.re-frame :as rf])) + +(rf/defn communities-select-tab-event + {:events [:communities/select-tab]} + [{:keys [db]} tab] + {:db (assoc db :communities/selected-tab tab)}) diff --git a/src/status_im/contexts/communities/home/view.cljs b/src/status_im/contexts/communities/home/view.cljs new file mode 100644 index 00000000000..837ad0b4334 --- /dev/null +++ b/src/status_im/contexts/communities/home/view.cljs @@ -0,0 +1,175 @@ +(ns status-im.contexts.communities.home.view + (:require + [oops.core :as oops] + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.common.home.banner.view :as common.banner] + [status-im.common.home.empty-state.view :as common.empty-state] + [status-im.common.home.header-spacing.view :as common.header-spacing] + [status-im.common.resources :as resources] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.communities.actions.community-options.view :as options] + [status-im.contexts.shell.constants :as shell.constants] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.number] + [utils.re-frame :as rf])) + +(defn item-render + [{:keys [id] :as item}] + (let [unviewed-counts (rf/sub [:communities/unviewed-counts id]) + customization-color (rf/sub [:profile/customization-color]) + item (merge item unviewed-counts)] + [quo/communities-membership-list-item + {:customization-color customization-color + :style {:padding-horizontal 20} + :on-press #(debounce/throttle-and-dispatch + [:communities/navigate-to-community-overview id] + 500) + :on-long-press #(rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [options/community-options-bottom-sheet id]) + :selected-item (fn [] + [quo/communities-membership-list-item {} true item])}])} + false + item])) + +(def tabs-data + [{:id :joined :label (i18n/label :t/joined) :accessibility-label :joined-tab} + {:id :pending :label (i18n/label :t/pending) :accessibility-label :pending-tab} + {:id :opened :label (i18n/label :t/opened) :accessibility-label :opened-tab}]) + +(defn- community-creation-options-testing + [] + [rn/view {:style {:padding-vertical 12 :row-gap 12}} + [quo/divider-line] + [rn/view + [quo/action-drawer + [[{:icon :i/communities + :accessibility-label :create-closed-community + :label "Create closed community (only for testing)" + :on-press #(rf/dispatch [:fast-create-community/create-closed-community])} + {:icon :i/communities + :accessibility-label :create-open-community + :label "Create open community (only for testing)" + :on-press #(rf/dispatch [:fast-create-community/create-open-community])} + {:icon :i/communities + :accessibility-label :create-token-gated-community + :label "Create token-gated community (only for testing)" + :on-press #(rf/dispatch + [:fast-create-community/create-token-gated-community])}]]]]]) + +(defn- open-learn-more-link + [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:browser.ui/open-url constants/create-community-help-url])) + +(defn- hide-bottom-sheet + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn- create-community-sheet + [] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title (i18n/label :t/want-to-create-community)}] + [quo/text {:style {:padding-horizontal 20 :padding-bottom 12}} + (i18n/label :t/communities-only-available-in-desktop)] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/learn-more) + :button-one-props {:disabled? false + :customization-color customization-color + :on-press open-learn-more-link + :icon-right :i/external} + :button-two-label (i18n/label :t/maybe-later) + :button-two-props {:type :grey + :on-press hide-bottom-sheet}}] + (when config/fast-create-community-enabled? + [community-creation-options-testing])])) + +(defn empty-state-content + [theme] + {:joined + {:title (i18n/label :t/no-communities) + :description [:<> + [rn/text {:style {:text-decoration-line :line-through}} + (i18n/label :t/no-communities-description-strikethrough)] + " " + (i18n/label :t/no-communities-description)] + :image (resources/get-themed-image :no-communities theme)} + :pending + {:title (i18n/label :t/no-pending-communities) + :description (i18n/label :t/no-pending-communities-description) + :image (resources/get-themed-image :no-pending-communities theme)} + :opened + {:title (i18n/label :t/no-opened-communities) + :description (i18n/label :t/no-opened-communities-description) + :image (resources/get-themed-image :no-opened-communities theme)}}) + +(def ^:private banner-data + {:title-props + {:beta? true + :label (i18n/label :t/communities) + :handler #(rf/dispatch [:show-bottom-sheet {:content create-community-sheet}]) + :accessibility-label :new-communities-button} + :card-props + {:on-press #(rf/dispatch [:navigate-to :screen/discover-communities]) + :title (i18n/label :t/discover) + :description (i18n/label :t/favorite-communities) + :banner (resources/get-image :discover) + :accessibility-label :communities-home-discover-card}}) + +(defn on-tab-change + [tab] + (rf/dispatch [:communities/select-tab tab])) + +(defn view + [] + (let [flat-list-ref (rn/use-ref-atom nil) + set-flat-list-ref (rn/use-callback #(reset! flat-list-ref %)) + theme (quo.context/use-theme) + customization-color (rf/sub [:profile/customization-color]) + selected-tab (or (rf/sub [:communities/selected-tab]) :joined) + {:keys [joined pending opened]} (rf/sub [:communities/grouped-by-status]) + selected-items (case selected-tab + :joined joined + :pending pending + :opened opened) + scroll-shared-value (reanimated/use-shared-value 0) + on-scroll (rn/use-callback + (fn [event] + (common.banner/set-scroll-shared-value + {:scroll-input (oops/oget event + "nativeEvent.contentOffset.y") + :shared-value scroll-shared-value})))] + [:<> + (if (empty? selected-items) + [common.empty-state/view + {:selected-tab selected-tab + :tab->content (empty-state-content theme)}] + [reanimated/flat-list + {:ref set-flat-list-ref + :key-fn :id + :content-inset-adjustment-behavior :never + :header [common.header-spacing/view] + :render-fn item-render + :style {:margin-top -1} + :data selected-items + :scroll-event-throttle 8 + :content-container-style {:padding-bottom + shell.constants/floating-shell-button-height} + :on-scroll on-scroll}]) + [common.banner/animated-banner + {:content banner-data + :customization-color customization-color + :scroll-ref flat-list-ref + :tabs tabs-data + :selected-tab selected-tab + :on-tab-change on-tab-change + :scroll-shared-value scroll-shared-value}]])) diff --git a/src/status_im/contexts/communities/overview/events.cljs b/src/status_im/contexts/communities/overview/events.cljs new file mode 100644 index 00000000000..d60bd660e48 --- /dev/null +++ b/src/status_im/contexts/communities/overview/events.cljs @@ -0,0 +1,150 @@ +(ns status-im.contexts.communities.overview.events + (:require + [status-im.contexts.communities.utils :as utils] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :communities/check-permissions-to-join-community-success + (fn [{:keys [db]} [community-id based-on-client-selection? result]] + (let [token-permissions-check (cond-> result + based-on-client-selection? (assoc :based-on-client-selection? true))] + {:db (-> db + (assoc-in [:communities/permissions-check community-id] + {:checking? false + :check token-permissions-check}))}))) + +(rf/reg-event-fx :communities/check-permissions-to-join-community-failed + (fn [{:keys [db]} [community-id]] + {:db (assoc-in db [:communities/permissions-check community-id :checking?] false)})) + +(rf/reg-event-fx :communities/check-permissions-to-join-community + (fn [{:keys [db]} [community-id addresses based-on-client-selection?]] + (when-let [community (get-in db [:communities community-id])] + (when-not (:checking-permissions? community) + {:db (assoc-in db [:communities/permissions-check community-id :checking?] true) + :json-rpc/call [{:method "wakuext_checkPermissionsToJoinCommunity" + :params [(cond-> {:communityId community-id} + addresses + (assoc :addresses addresses))] + :on-success [:communities/check-permissions-to-join-community-success + community-id based-on-client-selection?] + :on-error (fn [err] + (rf/dispatch + [:communities/check-permissions-to-join-community-failed + community-id]) + (log/error "failed to request to join community" + community-id + err))}]})))) + +(rf/reg-event-fx :communities/check-permissions-to-join-community-with-all-addresses-success + (fn [{:keys [db]} [community-id result]] + {:db (assoc-in db + [:communities/permissions-check-all community-id] + {:checking? false + :check result})})) + +(rf/reg-event-fx :communities/check-permissions-to-join-community-with-all-addresses-failed + (fn [{:keys [db]} [community-id]] + {:db (assoc-in db [:communities/permissions-check-all community-id :checking?] false)})) + +(rf/reg-event-fx :communities/check-permissions-to-join-community-with-all-addresses + (fn [{:keys [db]} [community-id]] + (let [accounts (utils/sorted-operable-non-watch-only-accounts db) + addresses (set (map :address accounts))] + {:db (assoc-in db [:communities/permissions-check community-id :checking?] true) + :json-rpc/call [{:method "wakuext_checkPermissionsToJoinCommunity" + :params [(cond-> {:communityId community-id} + (seq addresses) + (assoc :addresses addresses))] + :on-success + [:communities/check-permissions-to-join-community-with-all-addresses-success + community-id] + :on-error + (fn [err] + (rf/dispatch + [:communities/check-permissions-to-join-community-with-all-addresses-failed + community-id]) + (log/error "failed to check permissions for all addresses" + community-id + err))}]}))) + +(rf/reg-event-fx :communities/requested-to-join-error + (fn [{:keys [db]} [community-id error]] + (log/error "failed to request to join community" + {:community-id community-id + :error error + :event :communities/requested-to-join-error}) + {:db (assoc-in db [:password-authentication :error] error)})) + +(rf/reg-event-fx :communities/requested-to-join + (fn [_ [response-js]] + (let [community-name (aget response-js "communities" 0 "name")] + {:fx [[:dispatch [:sanitize-messages-and-process-response response-js]] + [:dispatch [:hide-bottom-sheet]] + [:dispatch + [:toasts/upsert + {:type :positive + :text (i18n/label + :t/requested-to-join-community + {:community community-name})}]]]}))) + +(rf/reg-event-fx :communities/toggled-collapsed-category-success + (fn [{:keys [db]} [community-id category-id collapsed?]] + {:db (assoc-in db [:communities/collapsed-categories community-id category-id] collapsed?)})) + +(rf/reg-event-fx :communities/toggle-collapsed-category + (fn [_ [community-id category-id collapse?]] + {:json-rpc/call + [{:method "wakuext_toggleCollapsedCommunityCategory" + :params [{:communityId community-id + :categoryId category-id + :collapsed collapse?}] + :on-success #(rf/dispatch + [:communities/toggled-collapsed-category-success community-id category-id collapse?]) + :on-error #(log/error "failed to toggle collapse category" + {:error % + :community-id community-id + :event :communities/toggle-collapsed-category + :category-id category-id + :collapse? collapse?})}]})) + +(defn generate-requests-for-signing + [{:keys [db]} [community-id]] + (let [pub-key (get-in db [:profile/profile :public-key]) + addresses-to-reveal (get-in db [:communities/all-addresses-to-reveal community-id])] + {:fx [[:effects.community/generate-requests-for-signing + {:pub-key pub-key + :community-id community-id + :addresses-to-reveal addresses-to-reveal + :on-success #(rf/dispatch [:communities/on-join-requests-prepared + community-id + %])}]]})) + +(rf/reg-event-fx :communities/generate-requests-for-signing generate-requests-for-signing) + +(defn on-join-requests-prepared + [{:keys [db]} [community-id signing-requests]] + {:db (assoc-in db + [:communities/join-requests-for-signing community-id] + signing-requests)}) + +(rf/reg-event-fx :communities/on-join-requests-prepared on-join-requests-prepared) + +(defn request-to-join + [{:keys [db]} [{:keys [community-id signature-data]}]] + (let [share-future-addresses? (get-in db [:communities/selected-share-all-addresses community-id]) + airdrop-address (get-in db [:communities/all-airdrop-addresses community-id]) + addresses-to-reveal (map :address signature-data) + signatures (utils/extract-join-request-signatures signature-data)] + {:fx [[:effects.community/request-to-join + {:community-id community-id + :signatures signatures + :addresses-to-reveal addresses-to-reveal + :airdrop-address airdrop-address + :share-future-addresses? share-future-addresses? + :on-success #(rf/dispatch [:communities/requested-to-join %]) + :on-error #(rf/dispatch [:communities/requested-to-join-error community-id + %])}]]})) + +(rf/reg-event-fx :communities/request-to-join request-to-join) diff --git a/src/status_im/contexts/communities/overview/style.cljs b/src/status_im/contexts/communities/overview/style.cljs new file mode 100644 index 00000000000..391747576a6 --- /dev/null +++ b/src/status_im/contexts/communities/overview/style.cljs @@ -0,0 +1,137 @@ +(ns status-im.contexts.communities.overview.style + (:require + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area])) + +(defn fetching-placeholder + [top-inset] + {:flex 1 + :margin-top top-inset}) + +(def community-overview-container + {:flex 1}) + +(defn header-cover-image + [background-color] + {:width "100%" + :background-color background-color + :height (+ 20 ;; Area hidden by sheet on top but visible with rounded borders + 92 + safe-area/top)}) + +(def cover-image {:flex 1}) + +(defn cover-image-blur-container + [header-opacity] + [rn/stylesheet-absolute-fill + {:opacity 0} + {:opacity header-opacity}]) + +(defn cover-image-blur-layer + [theme] + [rn/stylesheet-absolute-fill + {:background-color (colors/theme-colors colors/white-70-blur + colors/neutral-95-opa-70-blur + theme)}]) + +(def ^:private page-nav-container-base-style + {:height (+ 12 ;; padding-top + 12 ;; padding-bottom + 32) ;; button size + :width "100%"}) + +(defn page-nav-container + [opposite-header-opacity] + [rn/stylesheet-absolute-fill + page-nav-container-base-style + {:top (- safe-area/top 12) ;; -12 to place the button next to the safe-area + :opacity 1} + {:opacity opposite-header-opacity}]) + +(defn page-nav-container-blur + [header-opacity] + [rn/stylesheet-absolute-fill + page-nav-container-base-style + {:top (- safe-area/top 12) ;; -12 to place the button next to the safe-area + :opacity 0} + {:opacity header-opacity}]) + +(def community-logo + {:position :absolute + :z-index 1 + :width 88 + :height 88 + :border-radius 44 + :justify-content :center + :align-items :center + :background-color :white + :transform [{:translate-x 20} {:scale 1}] + :transform-origin "bottom left"}) + +(defn community-logo-bg-color + [theme] + {:background-color (colors/theme-colors colors/white + colors/neutral-95 + theme)}) + +(def community-logo-image + {:width 80 + :height 80 + :border-radius 40}) + +(defn community-info + [theme] + {:border-top-left-radius 20 + :border-top-right-radius 20 + :padding-top 44 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(def status-tag-position + {:position :absolute :top 12 :right 12 :opacity 0}) + +(def community-name-and-description + {:padding-vertical 12 + :padding-horizontal 20 + :row-gap 8}) + +(def community-stats + {:flex-direction :row + :column-gap 12 + :height 22 + :padding-horizontal 20 + :opacity 0}) + +(def community-tags + {:margin-top 16 + :padding-horizontal 20 + :padding-bottom 20}) + +(def community-tags-last-item + {:margin-right (* 2 20)}) + +(defn request-to-join-button + [tags?] + {:padding-left 20 + :padding-right 20 + :padding-bottom 20 + :padding-top (when-not tags? 20)}) + +(def request-to-join-as + {:padding-left 20 + :padding-right 20 + :padding-top 4 + :padding-bottom 25}) + +(defn channel-listing + [theme height] + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :height height + :z-index 1}) + +(defn category-divider + [theme] + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(def community-sheet-position + {:top (+ -20 -40)}) diff --git a/src/status_im/contexts/communities/overview/view.cljs b/src/status_im/contexts/communities/overview/view.cljs new file mode 100644 index 00000000000..e8f22b63a3f --- /dev/null +++ b/src/status_im/contexts/communities/overview/view.cljs @@ -0,0 +1,523 @@ +(ns status-im.contexts.communities.overview.view + (:require + [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events.helper] + [status-im.common.home.actions.view :as actions] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [status-im.contexts.communities.actions.community-options.view :as options] + [status-im.contexts.communities.overview.style :as style] + [status-im.contexts.communities.utils :as communities.utils] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.worklets.communities :as worklets])) + +;; NOTE: values compared against `scroll-amount` to trigger animations. +(def expand-header-threshold + "Dragging distance to collapse/extend the community." + 150) + +(def sheet-displacement-threshold + "Dragging distance to round sheet borders and move the sheet 8 units." + (+ expand-header-threshold 20)) + +(def text-movement-threshold + "Dragging distance to start the text movement from/to the bottom to/from the right." + (* expand-header-threshold 0.7)) + +(def info-opacity-threshold + "Dragging distance to appear/disappear the community info (description, tags & stats)." + (* expand-header-threshold 0.5)) + +(def snap-header-threshold + "Threshold to automatically move the header to a collapsed/expanded state and avoid an + intermediate state." + (* expand-header-threshold 0.75)) + +(def expand-header-limit + "Max dragging distance where the header animation ends. It works to identify when to + start the flat-list scrolling." + (+ sheet-displacement-threshold 56)) + +(defn- collapse-category + [community-id category-id collapsed?] + (rf/dispatch + [:communities/toggle-collapsed-category community-id category-id (not collapsed?)])) + +(defn- get-access-type + [access] + (condp = access + constants/community-no-membership-access :open + constants/community-invitation-only-access :invite-only + constants/community-on-request-access :request-access + :unknown-access)) + +(defn- show-join-modal + [community-id] + (fn [] + (rf/dispatch [:open-modal :screen/community-account-selection-sheet + {:community-id community-id}]))) + +(defn token-gated-communities-info + [] + [quo/documentation-drawers {:title (i18n/label :t/token-gated-communities)} + [quo/text {:size :paragraph-2} + (i18n/label :t/token-gated-communities-info)]]) + +(defn- on-join-info-press + [] + (rf/dispatch [:show-bottom-sheet {:content token-gated-communities-info}])) + +(defn- token-requirements + [community-id role-permissions? color tags?] + (let [{:keys [can-request-access? no-member-permission? networks-not-supported? + highest-permission-role + tokens]} (rf/sub [:community/token-gated-overview community-id]) + on-request-access-press (show-join-modal community-id)] + (cond + networks-not-supported? + [rn/view {:style (style/request-to-join-button tags?)} + [quo/text (i18n/label :t/network-not-supported)]] + + (or (not role-permissions?) no-member-permission?) + [rn/view {:style (style/request-to-join-button tags?)} + [quo/button + {:on-press on-request-access-press + :accessibility-label :show-request-to-join-screen-button + :customization-color color + :icon-left :i/communities} + (i18n/label :t/request-to-join)]] + + :else + (let [highest-role-text (i18n/label + (communities.utils/role->translation-key + highest-permission-role + :t/member))] + [rn/view {:style style/request-to-join-as} + [quo/community-token-gating + {:role highest-role-text + :tokens tokens + :community-color color + :satisfied? can-request-access? + :on-press on-request-access-press + :on-press-info on-join-info-press}]])))) + +(defn- join-community + [{:keys [community-id joined? permissions role-permissions? color tags?]}] + (let [pending? (rf/sub [:communities/my-pending-request-to-join community-id]) + access-type (get-access-type (:access permissions))] + (when-not (or joined? pending? (#{:unknown-access :invite-only} access-type)) + [token-requirements community-id role-permissions? color tags?]))) + +(defn- status-tag + [{:keys [community-id joined? info-styles]}] + (let [pending? (rf/sub [:communities/my-pending-request-to-join community-id])] + (when (or pending? joined?) + [reanimated/view {:style [style/status-tag-position info-styles]} + [quo/status-tag + {:status {:type (if joined? :positive :pending)} + :label (if joined? (i18n/label :t/joined) (i18n/label :t/pending))}]]))) + +(defn- category-divider + [community-id + {collapsed? :collapsed? + category-name :name + category-id :id}] + (let [theme (quo.context/use-theme)] + [rn/view + {:style (style/category-divider theme) + :blur-amount 20 + :blur-type :transparent + :overlay-color :transparent} + [quo/divider-label + {:on-press (fn [] (collapse-category community-id category-id collapsed?)) + :chevron-icon (if collapsed? :i/chevron-right :i/chevron-down) + :chevron :left + :blur? true} + category-name]])) + +(defn- notification-type + [{:keys [muted? mentions-count unread-messages?] :as _chat}] + (cond + muted? :mute + (> mentions-count 0) :mention + unread-messages? :notification)) + +(defn- navigate-to-chat + [chat-id] + (rf/dispatch [:dismiss-keyboard]) + (debounce/throttle-and-dispatch + [:communities/navigate-to-community-chat chat-id] + 1000)) + +(defn- show-chat-actions + [chat-data] + (rf/dispatch + [:show-bottom-sheet + {:selected-item (fn [] [quo/channel chat-data]) + :content (fn [] [actions/chat-actions chat-data false])}])) + +(defn channel-item + [{:keys [color locked?] + channel-id :id + :as chat} + {:keys [community-id joined-or-spectated?] :as _community-data}] + [rn/view {:style {:padding-horizontal 8}} + (let [chat-id (str community-id channel-id) + chat-data (assoc chat + :community-id community-id + :chat-type constants/community-chat-type + :chat-id chat-id + :customization-color color + :notification (notification-type chat)) + on-press (rn/use-callback + (fn [] + (when (and (not locked?) channel-id joined-or-spectated?) + (navigate-to-chat chat-id))) + [joined-or-spectated? locked? channel-id]) + on-long-press (rn/use-callback + (fn [] + (when (and (not locked?) channel-id) + (show-chat-actions chat-data))) + [locked? channel-id])] + [quo/channel + (assoc chat-data + :on-press on-press + :on-long-press on-long-press)])]) + +(defn channel-listing-item + [{:keys [community-id] :as community-data}] + (fn [{:keys [render-as] :as item-data} _ _ _] + (case render-as + :separator [rn/view {:style {:height 8}}] + :category [category-divider community-id item-data] + :channel [channel-item item-data community-data] + nil))) + +(def channel-component-heights + {:category 34 + :channel 48 + :separator 8}) + +(def unusable-area-height + (+ 32 11 safe-area/bottom)) ;;top page buttons, button's padding & safe area + +(defn- calc-scrollable-content + [scrollable-height] + (- scrollable-height + (- (:height safe-area/window) unusable-area-height))) + +(defn- calc-listing-height + [] + (+ (- (:height safe-area/window) unusable-area-height) + safe-area/bottom)) + +(defn- channel-listing + [{:keys [community-id scroll-amount header-height set-max-scroll]}] + (let [theme (quo.context/use-theme) + channels-styles (worklets/use-channels-styles + {:scroll-amount scroll-amount + :header-height header-height + :expand-header-threshold expand-header-threshold + :sheet-displacement-threshold sheet-displacement-threshold + :expand-header-limit expand-header-limit}) + flat-list-ref (reanimated/use-animated-ref) + _scroll-to-animation (worklets/use-scroll-to + {:animated-ref flat-list-ref + :scroll-amount scroll-amount + :expand-header-limit expand-header-limit}) + {:keys [joined? + spectated?]} (rf/sub [:communities/community-overview community-id]) + joined-or-spectated? (or joined? spectated?) + render-fn (rn/use-callback + (channel-listing-item {:community-id community-id + :joined-or-spectated? joined-or-spectated?}) + [joined-or-spectated?]) + flatten-channels (rf/sub [:communities/flatten-channels-and-categories community-id]) + categories-indexes (keep-indexed (fn [idx {:keys [render-as]}] + (when (= render-as :category) idx)) + flatten-channels) + scrollable-area-height (->> flatten-channels + (map (comp channel-component-heights :render-as)) + (reduce +)) + listing-height (calc-listing-height)] + (rn/use-effect + (fn [] + (let [max-scroll-offset (calc-scrollable-content scrollable-area-height)] + (if (neg? max-scroll-offset) + (set-max-scroll 0) + (set-max-scroll max-scroll-offset)))) + [scrollable-area-height]) + [reanimated/flat-list + {:ref flat-list-ref + :style [(style/channel-listing theme listing-height) channels-styles] + :data flatten-channels + :content-container-style [(when platform/ios? {:padding-bottom safe-area/bottom})] + :sticky-header-indices categories-indexes + :scroll-enabled false + :render-fn render-fn + :key-fn :id}])) + +(defn- header-cover-image + [{:keys [cover-image background-color header-opacity]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/header-cover-image background-color)} + [reanimated/image {:style style/cover-image :source {:uri cover-image}}] + [reanimated/view {:style (style/cover-image-blur-container header-opacity)} + [rn/image + {:style style/cover-image + :source {:uri cover-image} + :blur-radius 20}] + [rn/view {:style (style/cover-image-blur-layer theme)}]]])) + +(defn- open-community-options + [community-id] + (fn [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] [options/community-options-bottom-sheet community-id])}]))) + +(defn- page-nav + [{:keys [blur-version? nav-content-opacity community-name community-logo community-id]}] + (let [on-options-press (rn/use-callback (open-community-options community-id)) + base-props {:right-side [{:icon-name :i/options + :accessibility-label :community-options-for-community + :on-press on-options-press}] + :icon-name :i/close + :on-press events.helper/navigate-back + :accessibility-label :back-button}] + (if blur-version? + [quo/page-nav + (assoc base-props + :type :community + :background :blur + :center-opacity nav-content-opacity + :community-name community-name + :community-logo community-logo)] + [quo/page-nav + (assoc base-props + :type :no-title + :background :photo)]))) + +(defn- header + [community-id scroll-amount] + (let [header-opacity (worklets/use-header-opacity + {:scroll-amount scroll-amount + :expand-header-threshold expand-header-threshold + :sheet-displacement-threshold sheet-displacement-threshold}) + opposite-header-opacity (worklets/use-opposite-header-opacity header-opacity) + nav-content-opacity (worklets/use-nav-content-opacity + {:scroll-amount scroll-amount + :sheet-displacement-threshold sheet-displacement-threshold + :expand-header-limit expand-header-limit}) + {:keys [community-name color logo + cover-image]} (rf/sub [:communities/community-overview community-id])] + [:<> + [header-cover-image + {:cover-image cover-image + :background-color color + :header-opacity header-opacity}] + [reanimated/view {:style (style/page-nav-container opposite-header-opacity)} + [page-nav + {:blur-version? false + :community-id community-id}]] + [reanimated/view {:style (style/page-nav-container-blur header-opacity)} + [page-nav + {:blur-version? true + :community-id community-id + :nav-content-opacity nav-content-opacity + :community-name community-name + :community-logo logo}]]])) + +(defn- community-logo + [{:keys [scroll-amount community-id]}] + (let [theme (quo.context/use-theme) + logo-styles (worklets/use-logo-styles + {:scroll-amount scroll-amount + :expand-header-threshold expand-header-threshold + :sheet-displacement-threshold sheet-displacement-threshold + :text-movement-threshold text-movement-threshold}) + {:keys [logo]} (rf/sub [:communities/community-overview community-id])] + [reanimated/view + {:style [style/community-logo + (style/community-logo-bg-color theme) + logo-styles]} + [rn/image {:style style/community-logo-image :source logo}]])) + +(defn- name-and-description + [{:keys [scroll-amount community-name community-description info-styles]}] + (let [name-styles (worklets/use-name-styles + {:scroll-amount scroll-amount + :expand-header-threshold expand-header-threshold + :text-movement-threshold text-movement-threshold})] + [rn/view {:style style/community-name-and-description} + [reanimated/view {:style name-styles} + [quo/text + {:accessibility-label :community-title + :weight :semi-bold + :size :heading-1 + :number-of-lines 1} + community-name]] + [reanimated/view {:style [{:opacity 0} info-styles]} + [quo/text + {:accessibility-label :community-description + :weight :regular + :size :paragraph-1} + community-description]]])) + +(defn- community-info-tags + [community-tags info-styles] + (when (seq community-tags) + [reanimated/view {:style info-styles} + [quo/community-tags + {:tags community-tags + :last-item-style style/community-tags-last-item + :container-style style/community-tags}]])) + +(defn- community-info-stats + [{:keys [members-count active-members-count info-styles]}] + [reanimated/view {:style [style/community-stats info-styles]} + [rn/view {:style {:flex-direction :row :column-gap 12}} + [quo/community-stat + {:accessibility-label :stats-members-count + :icon :i/group + :value members-count}] + [quo/community-stat + {:accessibility-label :stats-active-count + :icon :i/active-members + :value active-members-count}]]]) + +(defn- community-info + [{:keys [scroll-amount header-height community-id]}] + (let [theme (quo.context/use-theme) + sheet-styles (worklets/use-sheet-styles + {:scroll-amount scroll-amount + :expand-header-threshold expand-header-threshold + :sheet-displacement-threshold sheet-displacement-threshold}) + info-styles (worklets/use-info-styles + {:scroll-amount scroll-amount + :info-opacity-threshold info-opacity-threshold}) + set-header-height (rn/use-callback + (fn [e] + (let [height (oops/oget e "nativeEvent.layout.height")] + (reanimated/set-shared-value header-height (or height 0))))) + {:keys [community-name description active-members-count tags role-permissions? + permissions color + joined?]} (rf/sub [:communities/community-overview community-id]) + members-count (count (rf/sub [:communities/community-members community-id]))] + [reanimated/view + {:style [(style/community-info theme) sheet-styles] + :on-layout set-header-height} + [status-tag + {:community-id community-id + :joined? joined? + :info-styles info-styles}] + [name-and-description + {:scroll-amount scroll-amount + :community-name community-name + :community-description description + :info-styles info-styles}] + [community-info-stats + {:members-count members-count + :active-members-count active-members-count + :info-styles info-styles}] + [community-info-tags tags info-styles] + [join-community + {:community-id community-id + :joined? joined? + :tags? (seq tags) + :permissions permissions + :role-permissions? role-permissions? + :color color}]])) + +(defn- community-sheet + [{:keys [community-id scroll-amount set-max-scroll]}] + (let [header-height (reanimated/use-shared-value 0)] + [rn/view {:style style/community-sheet-position} + [community-logo + {:community-id community-id + :scroll-amount scroll-amount}] + [community-info + {:scroll-amount scroll-amount + :header-height header-height + :community-id community-id}] + [channel-listing + {:community-id community-id + :scroll-amount scroll-amount + :header-height header-height + :set-max-scroll set-max-scroll}]])) + +(defn- community-overview + [community-id collapsed?] + (let [max-scroll (reanimated/use-shared-value 0) + set-max-scroll (rn/use-callback + (fn [max-scroll-amount] + (reanimated/set-shared-value max-scroll max-scroll-amount))) + scroll-start (reanimated/use-shared-value (if collapsed? (- expand-header-threshold) 0)) + scroll-amount (reanimated/use-shared-value (if collapsed? expand-header-threshold 0)) + on-pan-start (worklets/on-pan-start scroll-start scroll-amount) + on-pan-update (worklets/on-pan-update + {:scroll-start scroll-start + :scroll-amount scroll-amount + :max-scroll max-scroll + :expand-header-limit expand-header-limit}) + on-pan-end (worklets/on-pan-end + {:scroll-start scroll-start + :scroll-amount scroll-amount + :max-scroll max-scroll + :expand-header-limit expand-header-limit + :expand-header-threshold expand-header-threshold + :snap-header-threshold snap-header-threshold + :animation-duration 300}) + pan-gesture (-> (gesture/gesture-pan) + (gesture/on-start on-pan-start) + (gesture/on-update on-pan-update) + (gesture/on-end on-pan-end))] + [gesture/gesture-detector {:gesture pan-gesture} + [rn/view {:style {:flex 1}} + [header community-id scroll-amount] + [community-sheet + {:community-id community-id + :scroll-amount scroll-amount + :set-max-scroll set-max-scroll}]]])) + +(defn- community-fetching-placeholder + [id] + (let [theme (quo.context/use-theme) + fetching? (rf/sub [:communities/fetching-community id])] + [rn/view + {:style (style/fetching-placeholder safe-area/top) + :accessibility-label (if fetching? + :fetching-community-overview + :failed-to-fetch-community-overview)} + [quo/page-nav + {:title (i18n/label :t/community-overview) + :type :title + :text-align :left + :icon-name :i/close + :on-press events.helper/navigate-back}] + [quo/empty-state + {:image (resources/get-themed-image :cat-in-box theme) + :description (when-not fetching? (i18n/label :t/here-is-a-cat-in-a-box-instead)) + :title (if fetching? + (i18n/label :t/fetching-community) + (i18n/label :t/failed-to-fetch-community)) + :container-style {:flex 1 :justify-content :center}}]])) + +(defn view + [id] + (let [community-id (or id (quo.context/use-screen-params)) + community (rf/sub [:communities/community-overview community-id]) + collapsed? (:joined? community)] + [rn/view {:style style/community-overview-container} + (if community + [community-overview community-id collapsed?] + [community-fetching-placeholder community-id])])) diff --git a/src/status_im/contexts/communities/sharing/events.cljs b/src/status_im/contexts/communities/sharing/events.cljs new file mode 100644 index 00000000000..d00927d6e39 --- /dev/null +++ b/src/status_im/contexts/communities/sharing/events.cljs @@ -0,0 +1,98 @@ +(ns status-im.contexts.communities.sharing.events + (:require [legacy.status-im.data-store.chats :as data-store.chats] + [react-native.platform :as platform] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :communities/invite-people-pressed + (fn [{:keys [db]} [id]] + {:db (assoc db :communities/community-id-input id) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch [:open-modal :screen/invite-people-community {:id id}]]]})) + +(rf/reg-event-fx :communities/share-community-pressed + (fn [{:keys [db]} [id]] + {:db (assoc db :communities/community-id-input id) + :fx [[:dispatch [:communities/share-community-url-with-data id]]]})) + +(rf/reg-event-fx :communities/share-community-confirmation-pressed + (fn [_ [users-public-keys community-id]] + {:fx [[:json-rpc/call + [{:method "wakuext_shareCommunity" + :params [{:communityId community-id + :users users-public-keys}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error (fn [err] + (log/error {:message "failed to share community" + :community-id community-id + :err err}))}]]]})) + +(rf/reg-event-fx :communities/share-community-channel-url-qr-code + (fn [_ [chat-id]] + (let [on-success #(rf/dispatch [:open-modal :screen/share-community-channel + {:chat-id chat-id + :url %}])] + {:fx [[:dispatch [:communities/get-community-channel-share-data chat-id on-success]]]}))) + +(rf/reg-event-fx :communities/share-community-channel-url-with-data + (fn [_ [chat-id]] + (let [title (i18n/label :t/channel-on-status) + on-success (fn [url] + (rf/dispatch [:open-share + {:options (if platform/ios? + {:activityItemSources + [{:placeholderItem {:type :text + :content title} + :item {:default {:type :url + :content url}} + :linkMetadata {:title title}}]} + {:title title + :subject title + :url url + :isNewTask true})}]))] + {:fx [[:dispatch [:communities/get-community-channel-share-data chat-id on-success]]]}))) + +(rf/reg-event-fx :communities/get-community-channel-share-data + (fn [_ [chat-id on-success]] + (let [{:keys [community-id channel-id]} (data-store.chats/decode-chat-id chat-id)] + {:json-rpc/call + [{:method "wakuext_shareCommunityChannelURLWithData" + :params [{:CommunityID community-id :ChannelID channel-id}] + :on-success on-success + :on-error (fn [err] + (log/error "failed to retrieve community channel url with data" + {:error err + :chat-id chat-id + :event :communities/get-community-channel-share-data}))}]}))) + +(rf/reg-event-fx :communities/share-community-url-with-data + (fn [_ [community-id]] + (let [title (i18n/label :t/community-on-status) + on-success (fn [url] + (rf/dispatch [:open-share + {:options (if platform/ios? + {:activityItemSources + [{:placeholderItem {:type :text + :content title} + :item {:default {:type :url + :content url}} + :linkMetadata {:title title}}]} + {:title title + :subject title + :url url + :isNewTask true})}]))] + {:fx [[:dispatch [:communities/get-community-share-data community-id on-success]]]}))) + +(rf/reg-event-fx :communities/get-community-share-data + (fn [_ [community-id on-success]] + {:json-rpc/call + [{:method "wakuext_shareCommunityURLWithData" + :params [community-id] + :on-success on-success + :on-error (fn [err] + (log/error "failed to retrieve community url with data" + {:error err + :community-id community-id + :event :communities/get-community-share-data}))}]})) diff --git a/src/status_im/contexts/communities/utils.cljs b/src/status_im/contexts/communities/utils.cljs new file mode 100644 index 00000000000..6028ccb39a6 --- /dev/null +++ b/src/status_im/contexts/communities/utils.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.communities.utils + (:require + [schema.core :as schema] + [status-im.constants :as constants] + [utils.hex :as hex])) + +(defn role->translation-key + ([role] (role->translation-key role nil)) + ([role fallback-to] + (condp = role + constants/community-token-permission-become-token-owner :t/token-owner + constants/community-token-permission-become-token-master :t/token-master + constants/community-token-permission-become-admin :t/admin + constants/community-token-permission-become-member :t/member + fallback-to))) + +(defn sorted-operable-non-watch-only-accounts + [db] + (->> (get-in db [:wallet :accounts]) + (vals) + (remove :watch-only?) + (filter :operable?) + (sort-by :position))) + +(defn extract-join-request-signatures + [signature-data] + (->> signature-data + (map :signature) + (map hex/prefix-hex))) + +(schema/=> extract-join-request-signatures + [:=> + [:cat + [:sequential + [:map {:closed true} + [:signature string?] + [:address string?] + [:message string?]]]] + [:sequential string?]]) diff --git a/src/status_im/contexts/contact/blocking/events.cljs b/src/status_im/contexts/contact/blocking/events.cljs new file mode 100644 index 00000000000..b7e337a81f0 --- /dev/null +++ b/src/status_im/contexts/contact/blocking/events.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.contact.blocking.events + (:require + [legacy.status-im.data-store.chats :as chats-store] + [re-frame.core :as re-frame] + [status-im.contexts.chat.messenger.messages.list.events :as message-list] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(rf/defn clean-up-chat + [{:keys [db]} public-key + {:keys [chat-id unviewed-messages-count unviewed-mentions-count last-message]}] + (let [ids (keep (fn [[message-id {:keys [from]}]] + (when (= from public-key) message-id)) + (get-in db [:messages chat-id])) + db (-> db + ;; remove messages + (update-in [:messages chat-id] #(apply dissoc % ids)) + (update-in [:chats chat-id] + assoc + :unviewed-messages-count unviewed-messages-count + :unviewed-mentions-count unviewed-mentions-count + :last-message last-message))] + {:db (assoc-in db + [:message-lists chat-id] + (message-list/add-many nil (vals (get-in db [:messages chat-id]))))})) + +(rf/defn contact-blocked + {:events [:contact/blocked]} + [{:keys [db] :as cofx} {:keys [public-key]} chats-js] + (let [fxs (when chats-js + (map #(->> (chats-store/<-rpc %) + (clean-up-chat public-key)) + (transforms/js->clj chats-js)))] + (apply + rf/merge + cofx + {:db (-> db + (update :chats dissoc public-key) + (update :chats-home-list disj public-key) + (assoc-in [:contacts/contacts public-key :added?] false)) + :fx [[:activity-center.notifications/fetch-unread-count] + [:effects/push-notifications-clear-message-notifications [public-key]] + [:dispatch-later + [{:ms 500 + :dispatch [:chat.ui/close-and-remove-chat public-key]}]]]} + fxs))) + +(rf/reg-event-fx :contact/block-contact + (fn [{:keys [db]} [public-key]] + (let [contact (-> (get (:contacts/contacts db) public-key {:public-key public-key}) + (assoc :blocked? true + :added? false + :active? false)) + current-chat-id (:current-chat-id db) + from-one-to-one-chat? (not (get-in db [:chats current-chat-id :group-chat]))] + {:db (assoc-in db [:contacts/contacts public-key] contact) + :fx [[:json-rpc/call + [{:method "wakuext_blockContact" + :params [public-key] + :js-response true + :on-success (fn [^js response] + (re-frame/dispatch [:contact/blocked contact (.-chats response)]) + (re-frame/dispatch [:sanitize-messages-and-process-response response])) + :on-error #(log/error "failed to block contact" % public-key)}]] + [:dispatch [:hide-popover]] + ;; reset navigation to avoid going back to non existing one to one chat + (when current-chat-id + (if from-one-to-one-chat? + [:dispatch [:pop-to-root :screen/shell-stack]] + [:dispatch [:navigate-back]]))]}))) + +(rf/reg-event-fx :contact/unblocked + (fn [{:keys [db]} [contact-id]] + {:db (update-in db + [:contacts/contacts contact-id] + #(assoc % :blocked? false))})) + +(rf/reg-event-fx :contact/unblock-contact + (fn [_ [contact-id]] + {:json-rpc/call + [{:method "wakuext_unblockContact" + :params [contact-id] + :on-success #(re-frame/dispatch [:contact/unblocked contact-id]) + :on-error #(log/error "failed to unblock contact" % contact-id)}]})) diff --git a/src/status_im/contexts/keycard/authorise/view.cljs b/src/status_im/contexts/keycard/authorise/view.cljs new file mode 100644 index 00000000000..08839907db1 --- /dev/null +++ b/src/status_im/contexts/keycard/authorise/view.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.keycard.authorise.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.common.standard-authentication.core :as standard-auth] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile-name (rf/sub [:profile/name]) + profile-picture (rf/sub [:profile/image]) + customization-color (rf/sub [:profile/customization-color]) + {:keys [on-success]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/authorise-with-password) + :description :context-tag + :context-tag {:full-name profile-name + :profile-picture profile-picture + :customization-color customization-color}}] + [rn/view {:style {:flex 1 :padding-horizontal 20}} + [quo/text (i18n/label :t/migrate-key-pair-authorise)] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (- (:width (rn/get-window)) 40)} + :source (resources/get-image :keycard-migration)}] + [standard-auth/slide-auth + {:size :size-48 + :container-style {} + :customization-color customization-color + :track-text (i18n/label :t/slide-to-authorise) + :on-success #(when on-success (on-success %)) + :auth-button-label (i18n/label :t/confirm)}]]])) diff --git a/src/status_im/contexts/keycard/backup/events.cljs b/src/status_im/contexts/keycard/backup/events.cljs new file mode 100644 index 00000000000..9dd01c30b7c --- /dev/null +++ b/src/status_im/contexts/keycard/backup/events.cljs @@ -0,0 +1,78 @@ +(ns status-im.contexts.keycard.backup.events + (:require [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :keycard/backup.generate-and-load-key + (fn [{:keys [db]}] + (let [{:keys [masked-phrase masked-pin]} (get-in db [:keycard :backup])] + {:fx [[:effects.keycard/generate-and-load-key + {:mnemonic (security/safe-unmask-data masked-phrase) + :pin (security/safe-unmask-data masked-pin) + :on-success (fn [] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/keycard.backup.success])) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]]}))) + +;; STEP 2: connect and load keys +(rf/reg-event-fx :keycard/backup.ready-to-add-connect + (fn [{:keys [db]}] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :instance-uid (get-in db [:keycard :backup :instance-uid]) + :on-success #(rf/dispatch [:keycard/backup.generate-and-load-key])}]]]})) + +(rf/reg-event-fx :keycard/backup.phrase-entered + (fn [{:keys [db]} [{:keys [key-uid seed-phrase]}]] + (if (not= key-uid (get-in db [:profile/profile :key-uid])) + {:fx [[:dispatch [:enter-seed-phrase/set-error (i18n/label :t/seed-phrase-dont-match)]]]} + {:db (assoc-in db [:keycard :backup :masked-phrase] seed-phrase) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:open-modal :screen/keycard.backup.ready-to-add + {:on-press #(rf/dispatch [:keycard/backup.ready-to-add-connect])}]]]}))) + +(rf/reg-event-fx :keycard/backup.save-instance-uid-and-pin + (fn [{:keys [db]} [masked-pin]] + {:db (-> db + (assoc-in [:keycard :backup :masked-pin] masked-pin) + (assoc-in [:keycard :backup :instance-uid] + (get-in db [:keycard :application-info :instance-uid])))})) + +(defn- save-pin-and-navigate-to-phrase + [pin] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/backup.save-instance-uid-and-pin (security/mask-data pin)]) + (rf/dispatch + [:open-modal :screen/use-recovery-phrase-dark + {:on-success #(rf/dispatch [:keycard/backup.phrase-entered %])}])) + +(defn- show-not-empty-screen + [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:open-modal :screen/keycard.backup.not-empty + {:on-press #(rf/dispatch [:keycard/backup.scan-empty-card])}])) + +(rf/reg-event-fx :keycard/backup-on-empty-card + (fn [_] + {:fx [[:dispatch + [:keycard/init.create-or-enter-pin + {:on-success save-pin-and-navigate-to-phrase}]]]})) + +;; STEP 1: connect empty keycard +(rf/reg-event-fx :keycard/backup.scan-empty-card + (fn [_] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :on-success + (fn [{:keys [has-master-key?]}] + (if has-master-key? + (show-not-empty-screen) + (do + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:keycard/backup-on-empty-card]))))}]]]})) diff --git a/src/status_im/contexts/keycard/backup/view.cljs b/src/status_im/contexts/keycard/backup/view.cljs new file mode 100644 index 00000000000..b32520f376a --- /dev/null +++ b/src/status_im/contexts/keycard/backup/view.cljs @@ -0,0 +1,104 @@ +(ns status-im.contexts.keycard.backup.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.contexts.keycard.common.view :as common.view] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn ready-to-add + [] + (let [{:keys [on-press]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/ready-add-keypair-keycard)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-migration)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/scan-keycard) + :button-one-props {:on-press on-press}}]])) + +(defn success-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/backup-keycard-created)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-backup-positive)}] + [rn/view {:style {:padding-horizontal 20 :padding-vertical 12}} + [quo/button {:on-press events-helper/navigate-back} + (i18n/label :t/done)]]]) + +(defn not-empty-view + [] + (let [{:keys [on-press]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-not-empty) + :description :text + :description-text (i18n/label :t/scan-empty-keycard)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-not-empty)}] + [common.view/tips] + [rn/view {:style {:padding-horizontal 20}} + [quo/button {:on-press on-press} + (i18n/label :t/try-again)]]])) + +(defn scan-empty + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/scan-empty-keycard) + :description :text + :description-text (i18n/label :t/backup-empty-keycard-only)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :check-your-keycard)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/ready-to-scan) + :button-one-props {:on-press #(rf/dispatch [:keycard/backup.scan-empty-card])}}]]) + +(defn sheet + [{:keys [on-continue]}] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title (i18n/label :t/create-backup-keycard)}] + [quo/text + {:style {:padding-horizontal 20 + :padding-vertical 8}} + (i18n/label :t/backup-keycard-instructions)] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/continue) + :button-one-props {:customization-color customization-color + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (on-continue))} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) diff --git a/src/status_im/contexts/keycard/change_pin/events.cljs b/src/status_im/contexts/keycard/change_pin/events.cljs new file mode 100644 index 00000000000..b80a4db677e --- /dev/null +++ b/src/status_im/contexts/keycard/change_pin/events.cljs @@ -0,0 +1,77 @@ +(ns status-im.contexts.keycard.change-pin.events + (:require [status-im.contexts.keycard.utils :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :keycard/change-pin.enter-current-pin + (fn [{:keys [db]}] + {:db (update db :keycard dissoc :change-pin) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:open-modal :screen/keycard.pin.enter + {:title (i18n/label :t/enter-current-keycard-pin) + :on-complete (fn [current-pin] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/change-pin.save-current-pin current-pin]) + (rf/dispatch [:keycard/change-pin.enter-new-pin]))}]]]})) + +(rf/reg-event-fx :keycard/change-pin.save-current-pin + (fn [{:keys [db]} [pin]] + {:db (assoc-in db [:keycard :change-pin :current-pin] (security/mask-data pin))})) + +(rf/reg-event-fx :keycard/change-pin.enter-new-pin + (fn [_] + {:fx [[:dispatch + [:open-modal :screen/keycard.pin.create + {:title (i18n/label :t/create-new-keycard-pin) + :repeat-stage-title (i18n/label :t/repeat-new-keycard-pin) + :on-complete (fn [new-pin] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/change-pin.save-new-pin new-pin]) + (rf/dispatch [:open-modal :screen/keycard.ready-to-change-pin]))}]]]})) + +(rf/reg-event-fx :keycard/change-pin.save-new-pin + (fn [{:keys [db]} [pin]] + {:db (assoc-in db [:keycard :change-pin :new-pin] (security/mask-data pin))})) + +(defn- change-pin-and-continue + [change-pin-data] + (rf/dispatch [:keycard/change-pin + {:on-success (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:open-modal :screen/keycard.pin-change-success])) + :on-failure (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:open-modal :screen/keycard.pin-change-failed])) + :current-pin (security/safe-unmask-data (:current-pin change-pin-data)) + :new-pin (security/safe-unmask-data (:new-pin change-pin-data))}])) + +(defn- verify-pin-and-continue + [change-pin-data] + (rf/dispatch + [:keycard/verify-pin + {:pin (security/safe-unmask-data (:current-pin change-pin-data)) + :on-success (fn [] + (rf/dispatch [:keycard/connect + {:next-step? true + :key-uid (:key-uid change-pin-data) + :on-success #(change-pin-and-continue change-pin-data)}])) + :on-failure (fn [error] + (when-not (utils/tag-lost? (:error error)) + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:keycard/on-action-with-pin-error error]) + (rf/dispatch [:keycard/change-pin.enter-current-pin])))}])) + +(rf/reg-event-fx :keycard/change-pin.verify-current-pin-and-continue + (fn [{:keys [db]}] + (let [key-uid (get-in db [:profile/profile :key-uid]) + change-pin-data (assoc (get-in db [:keycard :change-pin]) :key-uid key-uid)] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :steps 2 + :key-uid key-uid + :on-success #(verify-pin-and-continue change-pin-data)}]]]}))) diff --git a/src/status_im/contexts/keycard/change_pin/view.cljs b/src/status_im/contexts/keycard/change_pin/view.cljs new file mode 100644 index 00000000000..3bd2b5a670e --- /dev/null +++ b/src/status_im/contexts/keycard/change_pin/view.cljs @@ -0,0 +1,86 @@ +(ns status-im.contexts.keycard.change-pin.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.contexts.keycard.common.view :as common.view] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn change-pin-confirmation-sheet + [] + [quo/documentation-drawers + {:title (i18n/label :t/change-pin-keycard) + :shell? true} + [rn/view + [quo/text {:size :paragraph-2} + (i18n/label :t/change-pin-keycard-message)] + [quo/bottom-actions + {:actions :two-actions + :container-style {:margin-horizontal -20} + :blur? true + :button-one-label (i18n/label :t/continue) + :button-one-props {:on-press #(rf/dispatch [:keycard/change-pin.enter-current-pin])} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:on-press events-helper/hide-bottom-sheet + :type :grey}}]]]) + +(defn ready-to-change-pin + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/ready-to-change-pin)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-change-pin)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/scan-keycard) + :button-one-props {:on-press #(rf/dispatch + [:keycard/change-pin.verify-current-pin-and-continue])}}]]) + +(defn pin-change-success + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-pin-changed) + :description :text + :description-text (i18n/label :t/keycard-pin-changed-description)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-change-pin-positive)}] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/done) + :button-one-props {:on-press events-helper/navigate-back}}]]) + +(defn pin-change-failed + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-pin-change-failed) + :description :text + :description-text (i18n/label :t/keycard-pin-change-failed-description)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-change-pin-negative)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/try-again) + :button-one-props {:on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/keycard.ready-to-change-pin]))}}]]) diff --git a/src/status_im/contexts/keycard/check/view.cljs b/src/status_im/contexts/keycard/check/view.cljs new file mode 100644 index 00000000000..23764b89b99 --- /dev/null +++ b/src/status_im/contexts/keycard/check/view.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.keycard.check.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [status-im.contexts.keycard.common.view :as common.view] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- keycard-documentation + [] + [quo/documentation-drawers + {:title (i18n/label :t/keycard) + :shell? true} + [rn/view + [quo/text {:size :paragraph-2} + (i18n/label :t/keycard-documentation)] + [quo/button + {:size 40 + :type :primary + :container-style {:margin-top 24 :margin-bottom 12} + :background :blur + :icon-right :i/external + :on-press #(rf/dispatch [:browser.ui/open-url constants/get-keycard-url]) + :accessibility-label :get-keycard} + (i18n/label :t/buy-keycard)]]]) + +(defn view + [] + (let [{:keys [on-press]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/arrow-left + :on-press events-helper/navigate-back + :right-side [{:icon-name :i/info + :on-press #(rf/dispatch [:show-bottom-sheet + {:content keycard-documentation + :theme :dark + :shell? true}])}]}] + [quo/page-top + {:title (i18n/label :t/check-keycard) + :description :text + :description-text (i18n/label :t/see-keycard-ready)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :check-your-keycard)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/ready-to-scan) + :button-one-props {:on-press on-press}}]])) diff --git a/src/status_im/contexts/keycard/common/view.cljs b/src/status_im/contexts/keycard/common/view.cljs new file mode 100644 index 00000000000..d24c12e01b9 --- /dev/null +++ b/src/status_im/contexts/keycard/common/view.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.keycard.common.view + (:require [quo.core :as quo] + [utils.i18n :as i18n])) + +(defn tips + [] + [:<> + [quo/divider-label (i18n/label :t/tips-scan-keycard)] + [quo/markdown-list {:description (i18n/label :t/remove-phone-case)}] + [quo/markdown-list {:description (i18n/label :t/keep-card-steady)}]]) diff --git a/src/status_im/contexts/keycard/create/events.cljs b/src/status_im/contexts/keycard/create/events.cljs new file mode 100644 index 00000000000..b51469f923d --- /dev/null +++ b/src/status_im/contexts/keycard/create/events.cljs @@ -0,0 +1,158 @@ +(ns status-im.contexts.keycard.create.events + (:require [clojure.string :as string] + [legacy.status-im.ethereum.mnemonic :as mnemonic] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :keycard/create.generate-and-load-keys + (fn [{:keys [db]} [{:keys [has-master-key?]}]] + (let [{:keys [masked-phrase masked-pin + instance-uid]} (get-in db [:keycard :create]) + pin (security/safe-unmask-data masked-pin)] + (if has-master-key? + {:fx [[:effects.keycard/get-more-keys + {:pin pin + :on-success #(rf/dispatch [:keycard.login/recover-profile-and-login %]) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]]} + {:fx [[:effects.keycard/generate-and-load-key + {:mnemonic (security/safe-unmask-data masked-phrase) + :pin pin + :on-success (fn [] + (rf/dispatch [:keycard/connect + {:next-step? true + :instance-uid instance-uid + :on-success #(rf/dispatch + [:keycard/create.generate-and-load-keys + %])}])) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]]})))) + +;; STEP 3: connect and load keys +(rf/reg-event-fx :keycard/create.connect-and-load-keys + (fn [{:keys [db]}] + (let [{:keys [instance-uid]} (get-in db [:keycard :create])] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :steps 2 + :instance-uid instance-uid + :on-success + #(rf/dispatch [:keycard/create.generate-and-load-keys %])}]]]}))) + +(rf/reg-event-fx :keycard/create.seed-phrase-entered + (fn [{:keys [db]} [{:keys [key-uid seed-phrase]}]] + (if (contains? (:profile/profiles-overview db) key-uid) + {:fx [[:dispatch [:enter-seed-phrase/set-error (i18n/label :t/account-already-exist-error)]]]} + {:db (assoc-in db [:keycard :create :masked-phrase] seed-phrase) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:open-modal :screen/keycard.create.ready-to-add + {:on-continue #(rf/dispatch [:keycard/create.connect-and-load-keys])}]]]}))) + +(rf/reg-event-fx :keycard/create.phrase-backed-up + (fn [{:keys [db]} [masked-phrase-vector]] + {:db (assoc-in db + [:keycard :create :masked-phrase] + (->> masked-phrase-vector + security/safe-unmask-data + (string/join " ") + security/mask-data)) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:open-modal :screen/keycard.create.ready-to-add + {:on-continue #(rf/dispatch [:keycard/create.connect-and-load-keys])}]]]})) + +(defn- backup-recovery-phrase-success + [masked-seed-phrase] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/confirm-backup-dark + {:masked-seed-phrase masked-seed-phrase + :theme :dark + :shell? true + :on-try-again #(rf/dispatch [:open-modal :screen/backup-recovery-phrase-dark + {:on-success backup-recovery-phrase-success + :masked-seed-phrase (->> masked-seed-phrase + security/safe-unmask-data + (string/join " ") + security/mask-data) + :revealed? true}]) + :on-success #(rf/dispatch [:keycard/create.phrase-backed-up + masked-seed-phrase])}])) + +(rf/reg-event-fx :keycard/create.generate-phrase + (fn [] + {:effects.keycard/generate-mnemonic + {:words (string/join "\n" mnemonic/dictionary) + :on-success (fn [phrase] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/backup-recovery-phrase-dark + {:on-success backup-recovery-phrase-success + :masked-seed-phrase (security/mask-data phrase)}]))}})) + +;; STEP 2: connect and generate mnemonic +(rf/reg-event-fx :keycard/create.connect-and-generate-phrase + (fn [{:keys [db]}] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :instance-uid (get-in db [:keycard :create :instance-uid]) + :on-success #(rf/dispatch [:keycard/create.generate-phrase])}]]]})) + +(rf/reg-event-fx :keycard/create.save-instance-uid-and-pin + (fn [{:keys [db]} [masked-pin]] + {:db (-> db + (assoc-in [:keycard :create :masked-pin] masked-pin) + (assoc-in [:keycard :create :instance-uid] + (get-in db [:keycard :application-info :instance-uid])))})) + +(defn- save-pin-and-navigate-to-phrase + [pin create?] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/create.save-instance-uid-and-pin (security/mask-data pin)]) + (if create? + (rf/dispatch + [:open-modal :screen/keycard.create.ready-to-generate + {:on-continue #(rf/dispatch [:keycard/create.connect-and-generate-phrase])}]) + (rf/dispatch + [:open-modal :screen/use-recovery-phrase-dark + {:on-success #(rf/dispatch [:keycard/create.seed-phrase-entered %])}]))) + +(rf/reg-event-fx :keycard/create.open-empty + (fn [] + {:fx [[:dispatch + [:open-modal :screen/keycard.create.empty + {:on-create + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/init.create-or-enter-pin + {:on-success #(save-pin-and-navigate-to-phrase % true)}])) + :on-import + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/init.create-or-enter-pin + {:on-success #(save-pin-and-navigate-to-phrase % false)}]))}]]]})) + +(defn- on-login + [db key-uid] + (rf/dispatch [:navigate-back]) + (if (contains? (:profile/profiles-overview db) key-uid) + (rf/dispatch [:open-modal :screen/keycard.login.already-added]) + (rf/dispatch [:open-modal :screen/keycard.pin.enter + {:on-complete + #(rf/dispatch [:keycard.login/prepare-for-profile-recovery %])}]))) + +;; STEP 1: connect empty keycard +(rf/reg-event-fx :keycard/create.check-empty-card + (fn [{:keys [db]}] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :on-success + (fn [{:keys [has-master-key? key-uid]}] + (rf/dispatch [:keycard/disconnect]) + (if has-master-key? + (rf/dispatch [:open-modal :screen/keycard.create.not-empty + {:on-login #(on-login db key-uid)}]) + (rf/dispatch [:keycard/create.open-empty])))}]]]})) diff --git a/src/status_im/contexts/keycard/create/view.cljs b/src/status_im/contexts/keycard/create/view.cljs new file mode 100644 index 00000000000..d549d46aed5 --- /dev/null +++ b/src/status_im/contexts/keycard/create/view.cljs @@ -0,0 +1,158 @@ +(ns status-im.contexts.keycard.create.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [status-im.contexts.keycard.common.view :as common.view] + [status-im.contexts.keycard.factory-reset.view :as factory-reset] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- ready-to-add-documentation + [] + [quo/documentation-drawers + {:shell? true} + [quo/text {:size :paragraph-2} + (i18n/label :t/ready-add-keypair-keycard-documentation)]]) + +(defn ready-to-add + [] + (let [{:keys [on-continue]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back + :right-side [{:icon-name :i/info + :on-press #(rf/dispatch [:show-bottom-sheet + {:content ready-to-add-documentation + :theme :dark + :shell? true}])}]}] + [quo/page-top + {:title (i18n/label :t/ready-add-keypair-keycard)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :align-self :center :margin-vertical 37} + :source (resources/get-image :add-key-to-keycard)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/scan-keycard) + :button-one-props {:on-press on-continue}}]])) + +(defn ready-to-generate + [] + (let [{:keys [on-continue]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/ready-generate-keypair)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :align-self :center :margin-vertical 37} + :source (resources/get-image :add-key-to-keycard)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/scan-keycard) + :button-one-props {:on-press on-continue}}]])) + +(defn not-empty-view + [] + (let [{:keys [on-login]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-not-empty) + :description :text + :description-text (i18n/label :t/cant-store-new-keys)}] + [quo/keycard] + [quo/category + {:list-type :settings + :label (i18n/label :t/what-you-can-do) + :blur? true + :data + [{:title (i18n/label :t/login-with-keycard) + :image :icon + :image-props :i/profile + :action :arrow + :description :text + :description-props {:text (i18n/label :t/use-keypair-keycard)} + :on-press on-login} + {:title (i18n/label :t/factory-reset) + :image :icon + :image-props :i/revert + :action :arrow + :description :text + :description-props {:text (i18n/label :t/remove-keycard-content)} + :on-press (fn [] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :shell? true + :content factory-reset/sheet}]))}]}]])) + +(defn empty-view + [] + (let [{:keys [on-create on-import]} (quo.context/use-screen-params)] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-empty) + :description :text + :description-text (i18n/label :t/what-to-do)}] + [quo/small-option-card + {:variant :main + :title (i18n/label :t/create-new-profile) + :subtitle (i18n/label :t/new-key-pair-keycard) + :button-label (i18n/label :t/lets-go) + :accessibility-label :create-new-profile-keycard + :container-style {:margin-horizontal 20 :margin-top 8} + :image (resources/get-image :keycard-buy) + :button-props {:type :primary} + :on-press on-create}] + [quo/small-option-card + {:variant :icon + :title (i18n/label :t/import-recovery-phrase-to-keycard) + :subtitle (i18n/label :t/store-key-pair-on-keycard) + :accessibility-label :import-recovery-phrase-to-keycard + :container-style {:margin 20} + :image (resources/get-image :use-keycard) + :on-press on-import}]])) + +(defn view + [] + [:<> + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/create-profile-keycard)}] + [rn/view {:style {:padding-horizontal 20 :padding-top 20}} + [quo/small-option-card + {:variant :main + :title (i18n/label :t/check-keycard) + :subtitle (i18n/label :t/see-keycard-ready) + :button-label (i18n/label :t/scan-keycard) + :button-props {:type :primary :icon-left :i/keycard} + :accessibility-label :get-keycard + :image (resources/get-image :check-your-keycard) + :on-press #(rf/dispatch [:keycard/create.check-empty-card])}] + [rn/view {:style {:height 12}}] + [quo/small-option-card + {:variant :icon + :title (i18n/label :t/learn-more-keycard) + :subtitle (i18n/label :t/secure-wallet-card) + :accessibility-label :setup-keycard + :image (resources/get-image :use-keycard) + :on-press #(rf/dispatch [:browser.ui/open-url constants/get-keycard-url])}]] + [rn/view {:style {:flex 1}}] + [common.view/tips]]) diff --git a/src/status_im/contexts/keycard/different_card/view.cljs b/src/status_im/contexts/keycard/different_card/view.cljs new file mode 100644 index 00000000000..45ba23e9fe4 --- /dev/null +++ b/src/status_im/contexts/keycard/different_card/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.keycard.different-card.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [utils.i18n :as i18n])) + +(defn view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/different-keycard) + :description :text + :description-text (i18n/label :t/scan-previous-keycard)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-not-same)}] + [rn/view {:style {:padding-horizontal 20 :padding-vertical 12}} + [quo/button {:on-press events-helper/navigate-back} + (i18n/label :t/try-again)]]]) diff --git a/src/status_im/contexts/keycard/effects.cljs b/src/status_im/contexts/keycard/effects.cljs new file mode 100644 index 00000000000..4ee29e8708a --- /dev/null +++ b/src/status_im/contexts/keycard/effects.cljs @@ -0,0 +1,129 @@ +(ns status-im.contexts.keycard.effects + (:require-macros [utils.promesa :as utils.promesa]) + (:require [keycard.keycard :as keycard] + [native-module.core :as native-module] + [promesa.core :as promesa] + [react-native.async-storage :as async-storage] + [react-native.platform :as platform] + status-im.contexts.keycard.nfc.effects + [status-im.contexts.keycard.utils :as keycard.utils] + [status-im.contexts.profile.config :as profile.config] + [utils.hex :as hex] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defonce ^:private active-listeners (atom [])) + +(defn unregister-card-events + [] + (doseq [listener @active-listeners] + (keycard/remove-event-listener listener)) + (reset! active-listeners nil)) + +(defn register-card-events + [] + (unregister-card-events) + (reset! active-listeners + [(keycard/on-card-connected #(rf/dispatch [:keycard/on-card-connected])) + (keycard/on-card-disconnected #(rf/dispatch [:keycard/on-card-disconnected])) + (keycard/on-card-new-pairing #(rf/dispatch [:keycard/on-card-new-pairing (transforms/js->clj %)])) + (when platform/ios? + (keycard/on-nfc-user-cancelled #(rf/dispatch [:keycard.ios/on-nfc-user-cancelled]))) + (when platform/ios? + (keycard/on-nfc-timeout #(rf/dispatch [:keycard.ios/on-nfc-timeout]))) + (keycard/on-nfc-enabled #(rf/dispatch [:keycard/on-check-nfc-enabled-success true])) + (keycard/on-nfc-disabled #(rf/dispatch [:keycard/on-check-nfc-enabled-success false]))])) + +(rf/reg-fx :effects.keycard/register-card-events register-card-events) +(rf/reg-fx :effects.keycard/unregister-card-events unregister-card-events) + +(rf/reg-fx :effects.keycard/get-application-info + (fn [args] + (keycard/get-application-info (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/get-keys + (fn [args] + (keycard/get-keys (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/get-more-keys + (fn [args] + (keycard/import-keys (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/export-key + (fn [args] + (keycard/export-key (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/factory-reset + (fn [args] + (keycard/factory-reset (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/verify-pin + (fn [args] + (keycard/verify-pin (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/change-pin + (fn [args] + (keycard/change-pin (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/sign + (fn [args] + (-> (keycard/sign args) + (promesa/then (keycard.utils/get-on-success args)) + (promesa/catch (keycard.utils/get-on-failure args))))) + +(rf/reg-fx :effects.keycard/sign-payloads + (fn [{:keys [payloads pin on-success] :as args}] + ;; NOTE: we can't run parallel operations on the keycard (rejects with an error), so we should + ;; execute the signings sequentially. Unfortunately, `promesa` has no built-in function that + ;; executes promises sequentially which also returns all the results, hence this macro. + (-> (utils.promesa/all-seq + (for [{:keys [path message address]} payloads] + (-> (keycard/sign {:pin pin + :path path + :hash-data (hex/normalize-hex message)}) + (promesa/then (fn [signature] + {:signature signature + :address address + :message message}))))) + (promesa/then on-success) + (promesa/catch (keycard.utils/get-on-failure args))))) + +(rf/reg-fx :effects.keycard/init-card + (fn [args] + (keycard/init-card (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/generate-mnemonic + (fn [args] + (keycard/generate-mnemonic (keycard.utils/wrap-handlers args)))) + +(rf/reg-fx :effects.keycard/generate-and-load-key + (fn [{:keys [mnemonic on-failure] :as args}] + (-> mnemonic + (native-module/validate-mnemonic) + (promesa/then #(keycard/generate-and-load-key + (keycard.utils/wrap-handlers args))) + (promesa/catch on-failure)))) + +(rf/reg-fx :effects.keycard/login-with-keycard + (fn [{:keys [key-uid password whisper-private-key]}] + (native-module/login-account + (assoc (merge (profile.config/login) + (profile.config/fix-node-config-migration)) + :keyUid key-uid + :password password + :keycardWhisperPrivateKey whisper-private-key)))) + +(rf/reg-fx :effects.keycard/set-pairing-to-keycard + (fn [pairings] + (keycard/set-pairings pairings))) + +(rf/reg-fx + :keycard/persist-pairings + (fn [pairings] + (async-storage/set-item! "status-keycard-pairings" pairings))) + +(rf/reg-fx :effects.keycard/retrieve-pairings + (fn [] + (async-storage/get-item + "status-keycard-pairings" + #(rf/dispatch [:keycard/on-retrieve-pairings-success %])))) diff --git a/src/status_im/contexts/keycard/error/view.cljs b/src/status_im/contexts/keycard/error/view.cljs new file mode 100644 index 00000000000..8ec0c5540a6 --- /dev/null +++ b/src/status_im/contexts/keycard/error/view.cljs @@ -0,0 +1,57 @@ +(ns status-im.contexts.keycard.error.view + (:require [quo.core :as quo] + [status-im.common.events-helper :as events-helper] + [status-im.contexts.keycard.factory-reset.view :as factory-reset] + [status-im.contexts.keycard.unblock.view :as keycard.unblock] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def titles + {:keycard/error.keycard-unpaired {:title (i18n/label :t/all-slots-occupied) + :description (i18n/label :t/cant-use-keycard-device)} + :keycard/error.keycard-frozen {:title (i18n/label :t/keycard-blocked) + :description (i18n/label :t/cant-use-right-now)} + :keycard/error.keycard-locked {:title (i18n/label :t/keycard-blocked) + :description (i18n/label :t/cant-use-right-now)}}) + +(defn view + [] + (let [error (rf/sub [:keycard/application-info-error]) + {:keys [title description]} (get titles error)] + (fn [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title title + :description :text + :description-text description}] + [quo/keycard] + [quo/category + {:list-type :settings + :label (i18n/label :t/what-you-can-do) + :blur? true + :data + [(when (or (= error :keycard/error.keycard-frozen) + (= error :keycard/error.keycard-locked)) + {:title (i18n/label :t/unblock-keycard) + :image :icon + :image-props :i/unlocked + :action :arrow + :description :text + :description-props {:text (i18n/label :t/with-recovery-phrase)} + :on-press (fn [] + (rf/dispatch [:show-bottom-sheet + {:content keycard.unblock/sheet}]))}) + {:title (i18n/label :t/factory-reset) + :image :icon + :image-props :i/revert + :action :arrow + :description :text + :description-props {:text (i18n/label :t/remove-keycard-content)} + :on-press (fn [] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :shell? true + :content factory-reset/sheet}]))}]}]]))) diff --git a/src/status_im/contexts/keycard/events.cljs b/src/status_im/contexts/keycard/events.cljs new file mode 100644 index 00000000000..2a6bb279dcf --- /dev/null +++ b/src/status_im/contexts/keycard/events.cljs @@ -0,0 +1,187 @@ +(ns status-im.contexts.keycard.events + (:require [re-frame.core :as rf] + status-im.contexts.keycard.backup.events + status-im.contexts.keycard.create.events + status-im.contexts.keycard.init.events + status-im.contexts.keycard.login.events + status-im.contexts.keycard.manage.events + status-im.contexts.keycard.migrate.events + status-im.contexts.keycard.migrate.re-encrypting.events + status-im.contexts.keycard.nfc.events + status-im.contexts.keycard.nfc.sheets.events + status-im.contexts.keycard.pin.events + status-im.contexts.keycard.sign.events + status-im.contexts.keycard.unblock.events + [status-im.contexts.keycard.utils :as keycard.utils] + [utils.address :as address] + utils.datetime + [utils.ethereum.eip.eip55 :as eip55])) + +(rf/reg-event-fx :keycard/on-card-connected + (fn [{:keys [db]} _] + {:db (assoc-in db [:keycard :card-connected?] true) + :fx [(when-let [event (get-in db [:keycard :on-card-connected-event-vector])] + [:dispatch event])]})) + +(rf/reg-event-fx :keycard/on-card-disconnected + (fn [{:keys [db]} _] + {:db (assoc-in db [:keycard :card-connected?] false) + :fx [(when-let [event (get-in db [:keycard :on-card-disconnected-event-vector])] + [:dispatch event])]})) + +(rf/reg-event-fx :keycard/on-card-new-pairing + (fn [{:keys [db]} [{:keys [pairing instanceUID]}]] + (when (and instanceUID pairing) + (let [pairings (get-in db [:keycard :pairings]) + new-pairings (assoc pairings + instanceUID + {:pairing pairing + :paired-on (utils.datetime/timestamp)})] + {:db (assoc-in db [:keycard :pairings] new-pairings) + :keycard/persist-pairings new-pairings})))) + +(rf/reg-event-fx :keycard/on-retrieve-pairings-success + (fn [{:keys [db]} [pairings]] + {:db (assoc-in db [:keycard :pairings] pairings) + :fx [[:effects.keycard/set-pairing-to-keycard pairings]]})) + +(rf/reg-event-fx :keycard/on-action-with-pin-error + (fn [{:keys [db]} [error]] + (let [tag-was-lost? (keycard.utils/tag-lost? (:error error)) + pin-retries-count (keycard.utils/pin-retries (:error error))] + (when-not (or tag-was-lost? (nil? pin-retries-count)) + {:db (-> db + (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries-count) + (assoc-in [:keycard :pin :text] "") + (assoc-in [:keycard :pin :status] :error)) + :fx [[:dispatch [:keycard/disconnect]] + (when (zero? pin-retries-count) + [:dispatch + [:keycard/on-application-info-error + :keycard/error.keycard-locked]])]})))) + +(rf/reg-event-fx :keycard/init-card + (fn [_ [data]] + {:effects.keycard/init-card data})) + +(rf/reg-event-fx :keycard/get-keys + (fn [_ [data]] + {:effects.keycard/get-keys data})) + +(rf/reg-event-fx :keycard/get-more-keys + (fn [_ [data]] + {:effects.keycard/get-more-keys data})) + +(rf/reg-event-fx :keycard/export-key + (fn [_ [data]] + {:effects.keycard/export-key data})) + +(rf/reg-event-fx :keycard/factory-reset + (fn [_ [data]] + {:effects.keycard/factory-reset data})) + +(rf/reg-event-fx :keycard/verify-pin + (fn [_ [data]] + {:effects.keycard/verify-pin data})) + +(rf/reg-event-fx :keycard/change-pin + (fn [_ [data]] + {:effects.keycard/change-pin data})) + +(rf/reg-event-fx :keycard/connect-derive-address-and-add-account + (fn [_ [{:keys [pin derivation-path key-uid account-preferences]}]] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :key-uid key-uid + :on-success + (fn [] + (rf/dispatch + [:keycard/export-key + {:pin pin + :path derivation-path + :on-success (fn [public-key] + (let [derived-account {:public-key (str "0x" public-key) + :address (eip55/address->checksum + (str "0x" + (address/public-key->address + (subs public-key 2)))) + :path derivation-path}] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:wallet/add-account + (assoc account-preferences :key-uid key-uid) + derived-account]))) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]))}]]]})) + +(rf/reg-event-fx :keycard/cancel-connection + (fn [{:keys [db]}] + {:db (update db + :keycard + dissoc + :card-connected? + :on-card-connected-event-vector + :on-nfc-cancelled-event-vector)})) + +(rf/reg-event-fx :keycard/disconnect + (fn [_ _] + {:fx [[:dispatch [:keycard/cancel-connection]] + [:dispatch [:keycard/hide-connection-sheet {:success? true}]]]})) + +(rf/reg-event-fx :keycard/on-application-info-error + (fn [{:keys [db]} [error]] + {:db (assoc-in db [:keycard :application-info-error] error) + :fx [[:dispatch [:keycard/disconnect]] + [:dispatch + [:open-modal + (case error + :keycard/error.not-keycard :screen/keycard.not-keycard + :keycard/error.keycard-different :screen/keycard.different-card + :screen/keycard.error)]]]})) + +(rf/reg-event-fx :keycard/update-application-info + (fn [{:keys [db]} [app-info]] + {:db (update db + :keycard + #(-> % + (assoc :application-info app-info) + (dissoc :application-info-error)))})) + +(rf/reg-event-fx :keycard/get-application-info + (fn [_ [{:keys [on-success on-error instance-uid key-uid]}]] + {:effects.keycard/get-application-info + {:on-success (fn [app-info] + (rf/dispatch [:keycard/update-application-info app-info]) + (if-let [error (keycard.utils/validate-application-info app-info + instance-uid + key-uid)] + (if on-error + (on-error error app-info) + (rf/dispatch [:keycard/on-application-info-error error])) + (when on-success (on-success app-info)))) + :on-failure (fn [error] + (when-not (keycard.utils/tag-lost? (:error error)) + (if on-error + (on-error :keycard/error.not-keycard) + (rf/dispatch [:keycard/on-application-info-error + :keycard/error.not-keycard]))))}})) + +;; instance-uid is used if we need to know if it's the same card flow was started with +;; key-uid is used if we want to make sure it's card with the specific profile (on login for example) +(rf/reg-event-fx :keycard/connect + (fn [{:keys [db]} [{:keys [on-success on-error theme next-step? instance-uid key-uid steps]}]] + (let [event-vector [:keycard/get-application-info + {:instance-uid instance-uid + :key-uid key-uid + :on-success on-success + :on-error on-error}]] + {:db (-> db + (assoc-in [:keycard :on-card-connected-event-vector] event-vector) + (update-in [:keycard :steps] #(if next-step? % steps)) + (update-in [:keycard :current-step] #(if next-step? (inc %) 1))) + :fx [(when-not next-step? + [:dispatch + [:keycard/show-connection-sheet + {:on-cancel-event-vector [:keycard/cancel-connection] + :theme theme}]]) + (when (get-in db [:keycard :card-connected?]) + [:dispatch event-vector])]}))) diff --git a/src/status_im/contexts/keycard/factory_reset/view.cljs b/src/status_im/contexts/keycard/factory_reset/view.cljs new file mode 100644 index 00000000000..fa3d0a52870 --- /dev/null +++ b/src/status_im/contexts/keycard/factory_reset/view.cljs @@ -0,0 +1,106 @@ +(ns status-im.contexts.keycard.factory-reset.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- reset-card + [] + (rf/dispatch + [:keycard/factory-reset + {:on-success + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:open-modal :screen/keycard.factory-reset.success])) + :on-failure + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:open-modal :screen/keycard.factory-reset.fail]))}])) + +(defn- connect-and-reset + [key-uid] + (rf/dispatch + [:keycard/connect + {:theme :dark + :on-success (fn [{:keys [initialized?] :as app-info}] + (if (and initialized? (= key-uid (:key-uid app-info))) + (reset-card) + (do + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:open-modal :screen/keycard.different-card])))) + :on-error (fn [error] + (if (or (= error :keycard/error.keycard-frozen) + (= error :keycard/error.keycard-locked) + (= error :keycard/error.keycard-unpaired)) + (reset-card) + (do + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/on-application-info-error error]))))}])) + +(defn success-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-reset-success) + :description :text + :description-text (i18n/label :t/keycard-empty-ready)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-reset-positive)}] + [rn/view {:style {:padding-horizontal 20 :padding-vertical 12}} + [quo/button {:on-press events-helper/navigate-back} + (i18n/label :t/done)]]]) + +(defn failed-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-reset-failed)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-reset-negative)}] + [rn/view {:style {:padding-horizontal 20 :padding-vertical 12}} + [quo/button {:on-press events-helper/navigate-back} + (i18n/label :t/try-again)]]]) + +(defn sheet + [] + (let [customization-color (rf/sub [:profile/customization-color]) + key-uid (rf/sub [:keycard/key-uid]) + [checked? set-checked] (rn/use-state false)] + [:<> + [quo/drawer-top {:title (i18n/label :t/factory-reset-keycard)}] + [quo/text + {:style {:padding-horizontal 20 + :padding-top 8 + :padding-bottom 8}} + (i18n/label :t/factory-reset-warning)] + [quo/disclaimer + {:checked? checked? + :container-style {:margin-horizontal 20} + :on-change #(set-checked (not checked?))} + (i18n/label :t/key-pair-erased)] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/continue) + :button-one-props {:disabled? (not checked?) + :customization-color customization-color + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (connect-and-reset key-uid))} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]))}}]])) diff --git a/src/status_im/contexts/keycard/feature_unavailable/events.cljs b/src/status_im/contexts/keycard/feature_unavailable/events.cljs new file mode 100644 index 00000000000..4193b2d3f2a --- /dev/null +++ b/src/status_im/contexts/keycard/feature_unavailable/events.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.keycard.feature-unavailable.events + (:require + [status-im.constants :as constants] + [status-im.contexts.keycard.feature-unavailable.view :as feature-unavailable] + [utils.re-frame :as rf])) + +(rf/reg-event-fx + :keycard/feature-unavailable-upvote + (fn [_ [{:keys [feature-name]}]] + {:fx [[:dispatch [:open-url constants/mobile-upvote-link]] + [:dispatch + [:centralized-metrics/track + :metric/feature-unavailable-upvote + {:feature-name feature-name}]]]})) + +(rf/reg-event-fx + :keycard/feature-unavailable-show + (fn [_ [{:keys [feature-name theme] :as options}]] + {:fx [[:dispatch + [:show-bottom-sheet + {:theme theme + :content (fn [] + (feature-unavailable/view options))}]] + [:dispatch + [:centralized-metrics/track + :metric/feature-unavailable + {:feature-name feature-name}]]]})) diff --git a/src/status_im/contexts/keycard/feature_unavailable/view.cljs b/src/status_im/contexts/keycard/feature_unavailable/view.cljs new file mode 100644 index 00000000000..f549e225804 --- /dev/null +++ b/src/status_im/contexts/keycard/feature_unavailable/view.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.keycard.feature-unavailable.view + (:require + [quo.core :as quo] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-upvote + [feature-name] + (rf/dispatch [:keycard/feature-unavailable-upvote {:feature-name feature-name}])) + +(defn view + [{:keys [feature-name]}] + [:<> + [quo/drawer-top + {:title (i18n/label :t/feature-unavailable) + :description (i18n/label :t/feature-unavailable-for-keycard-description)}] + [quo/information-box + {:type :default + :icon :i/info + :style {:margin-top 8 :margin-horizontal 20}} + [:<> + (i18n/label :t/feature-unavailable-info) + [quo/text + {:style {:text-decoration-line :underline} + :size :paragraph-2 + :on-press #(on-upvote feature-name)} + (i18n/label :t/upvote-it)]]]]) diff --git a/src/status_im/contexts/keycard/init/events.cljs b/src/status_im/contexts/keycard/init/events.cljs new file mode 100644 index 00000000000..0a3febd3f40 --- /dev/null +++ b/src/status_im/contexts/keycard/init/events.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.keycard.init.events + (:require [utils.re-frame :as rf])) + +(defn- show-different-card-screen + [] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:open-modal :screen/keycard.different-card])) + +(defn- verify-entered-pin-and-continue + [on-success instance-uid] + (fn [pin] + (rf/dispatch + [:keycard/connect + {:theme :dark + :instance-uid instance-uid + :on-success + (fn [] + (rf/dispatch + [:keycard/verify-pin + {:pin pin + :on-success #(on-success pin) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]))}]))) + +(defn- init-card-with-pin-and-continue + [on-success] + (fn [pin] + (rf/dispatch + [:keycard/connect + {:theme :dark + :on-success + (fn [{:keys [initialized?]}] + (if initialized? + (show-different-card-screen) + (rf/dispatch + [:keycard/init-card + {:pin pin + :on-success #(rf/dispatch + [:keycard/get-application-info + {:on-success + (fn [{:keys [has-master-key?]}] + (if has-master-key? + (show-different-card-screen) + (on-success pin)))}])}])))}]))) + +(rf/reg-event-fx :keycard/init.create-or-enter-pin + (fn [{:keys [db]} [{:keys [on-success]}]] + (let [{:keys [initialized? instance-uid]} (get-in db [:keycard :application-info])] + {:fx [(if initialized? + [:dispatch + [:open-modal :screen/keycard.pin.enter + {:on-complete (verify-entered-pin-and-continue on-success instance-uid)}]] + [:dispatch + [:open-modal :screen/keycard.pin.create + {:on-complete (init-card-with-pin-and-continue on-success)}]])]}))) diff --git a/src/status_im/contexts/keycard/login/events.cljs b/src/status_im/contexts/keycard/login/events.cljs new file mode 100644 index 00000000000..77dba503fad --- /dev/null +++ b/src/status_im/contexts/keycard/login/events.cljs @@ -0,0 +1,100 @@ +(ns status-im.contexts.keycard.login.events + (:require [status-im.constants :as constants] + [status-im.contexts.profile.config :as profile.config] + [utils.address :as address] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :keycard.login/on-get-keys-success + (fn [{:keys [db]} [data]] + (let [{:keys [key-uid encryption-public-key + whisper-private-key]} data + profile (get-in db [:profile/profiles-overview key-uid])] + {:db + (-> db + (dissoc :keycard) + (update :profile/login assoc + :password encryption-public-key + :key-uid key-uid + :name (:name profile))) + :fx [[:dispatch [:keycard/disconnect]] + [:effects.keycard/login-with-keycard + {:password encryption-public-key + :whisper-private-key whisper-private-key + :key-uid key-uid}]]}))) + +(rf/reg-event-fx :keycard.login/on-get-keys-from-keychain-success + (fn [{:keys [db]} [key-uid [encryption-public-key whisper-private-key]]] + (when (and encryption-public-key whisper-private-key) + (let [profile (get-in db [:profile/profiles-overview key-uid])] + {:db + (-> db + (dissoc :keycard) + (update :profile/login assoc + :password encryption-public-key + :key-uid key-uid + :name (:name profile))) + :fx [[:dispatch [:keycard/disconnect]] + [:effects.keycard/login-with-keycard + {:password encryption-public-key + :whisper-private-key whisper-private-key + :key-uid key-uid}]]})))) + +(rf/reg-event-fx :keycard.login/check-card + (fn [{:keys [db]}] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :on-success + (fn [{:keys [has-master-key? key-uid]}] + (rf/dispatch [:keycard/disconnect]) + (if has-master-key? + (if (contains? (:profile/profiles-overview db) key-uid) + (rf/dispatch [:open-modal :screen/keycard.login.already-added]) + (rf/dispatch [:open-modal :screen/keycard.pin.enter + {:on-complete #(rf/dispatch [:keycard.login/prepare-for-profile-recovery + %])}])) + (rf/dispatch [:open-modal :screen/keycard.login.empty])))}]]]})) + +(rf/reg-event-fx :keycard.login/prepare-for-profile-recovery + (fn [{:keys [db]} [pin]] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :key-uid (get-in db [:keycard :application-info :key-uid]) + :on-success (fn [] + (rf/dispatch + [:keycard/get-more-keys + {:pin pin + :on-success #(rf/dispatch [:keycard.login/recover-profile-and-login %]) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]))}]]]})) + +(rf/reg-event-fx :keycard.login/recover-profile-and-login + (fn [{:keys [db]} + [{:keys [key-uid encryption-public-key whisper-private-key instance-uid whisper-public-key + whisper-address address wallet-public-key wallet-address wallet-root-address]}]] + (let [{:keys [pairing]} (get-in db [:keycard :pairings instance-uid])] + {:db + (-> db + (assoc :onboarding/recovered-account? true) + (assoc-in [:syncing :login-sha3-password] encryption-public-key)) + + :fx [[:dispatch [:keycard/disconnect]] + [:dispatch [:navigate-to :screen/onboarding.preparing-status]] + [:effects.profile/restore-and-login + (assoc (profile.config/create) + :keycardInstanceUID instance-uid + :keycardPairingKey pairing + :keycard {:keyUID key-uid + :address (address/normalized-hex address) + :whisperPrivateKey (address/normalized-hex whisper-private-key) + :whisperPublicKey (address/normalized-hex whisper-public-key) + :whisperAddress (address/normalized-hex whisper-address) + :walletPublicKey (address/normalized-hex wallet-public-key) + :walletAddress (address/normalized-hex wallet-address) + :walletRootAddress (address/normalized-hex wallet-root-address) + :encryptionPublicKey encryption-public-key + :eip1581Address ""} + :displayName "" + :password encryption-public-key + :customizationColor constants/profile-default-color + :fetchBackup true)]]}))) diff --git a/src/status_im/contexts/keycard/login/view.cljs b/src/status_im/contexts/keycard/login/view.cljs new file mode 100644 index 00000000000..9ec56d91915 --- /dev/null +++ b/src/status_im/contexts/keycard/login/view.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.keycard.login.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn empty-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-empty) + :description :text + :description-text (i18n/label :t/no-key-pair-keycard)}] + [quo/keycard] + [quo/category + {:list-type :settings + :label (i18n/label :t/what-you-can-do) + :blur? true + :data + [{:title (i18n/label :t/create-new-profile) + :image :icon + :image-props :i/profile + :action :arrow + :description :text + :description-props {:text (i18n/label :t/new-key-pair-keycard)} + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/create.open-empty]))}]}]]) + +(defn already-added-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/profile-already-added) + :description :text + :description-text (i18n/label :t/profile-linked-keycard)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-profile-already-added)}] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/select-profile) + :button-one-props {:on-press #(rf/dispatch [:pop-to-root :screen/profile.profiles])}}]]) diff --git a/src/status_im/contexts/keycard/manage/events.cljs b/src/status_im/contexts/keycard/manage/events.cljs new file mode 100644 index 00000000000..8fedebd1785 --- /dev/null +++ b/src/status_im/contexts/keycard/manage/events.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.keycard.manage.events + (:require [utils.re-frame :as rf])) + +(rf/reg-event-fx :keycard/manage.check-card + (fn [{:keys [db]}] + (let [keycard-profile? (not (nil? (get-in db [:profile/profile :keycard-pairing]))) + profile-key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :on-success + (fn [{:keys [has-master-key? key-uid]}] + (rf/dispatch [:keycard/disconnect]) + + (cond + (and (not keycard-profile?) (not has-master-key?)) + (rf/dispatch [:open-modal :screen/keycard.manage.empty-import]) + + (and keycard-profile? (not has-master-key?)) + (rf/dispatch [:open-modal :screen/keycard.manage.empty-backup]) + + (and (not keycard-profile?) has-master-key? (= profile-key-uid key-uid)) + (rf/dispatch [:open-modal :screen/keycard.migrate.profile-keys]) + + (and keycard-profile? has-master-key? (= profile-key-uid key-uid)) + (rf/dispatch [:open-modal :screen/keycard.manage.profile-keys]) + + :else + (rf/dispatch [:open-modal :screen/keycard.manage.not-empty-logout])))}]]]}))) diff --git a/src/status_im/contexts/keycard/manage/profile_keys/view.cljs b/src/status_im/contexts/keycard/manage/profile_keys/view.cljs new file mode 100644 index 00000000000..60287d4f0a3 --- /dev/null +++ b/src/status_im/contexts/keycard/manage/profile_keys/view.cljs @@ -0,0 +1,65 @@ +(ns status-im.contexts.keycard.manage.profile-keys.view + (:require [quo.core :as quo] + [status-im.common.events-helper :as events-helper] + [status-im.contexts.keycard.backup.view :as backup.view] + [status-im.contexts.keycard.change-pin.view :as change-pin.view] + [status-im.contexts.keycard.factory-reset.view :as factory-reset] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- backup-sheet + [] + [backup.view/sheet + {:on-continue + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/keycard.backup.scan-empty]))}]) + +(defn view + [] + (let [profile-name (rf/sub [:profile/name])] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title profile-name}] + [quo/keycard] + [quo/category + {:list-type :settings + :label (i18n/label :t/what-you-can-do) + :blur? true + :data + [{:title (i18n/label :t/change-pin-keycard) + :image :icon + :image-props :i/pincode + :action :arrow + :description :text + :description-props {:text (i18n/label :t/change-pin-keycard-description)} + :on-press (fn [] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :shell? true + :content change-pin.view/change-pin-confirmation-sheet}]))} + {:title (i18n/label :t/factory-reset) + :image :icon + :image-props :i/revert + :action :arrow + :description :text + :description-props {:text (i18n/label :t/remove-keycard-content)} + :on-press (fn [] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :shell? true + :content factory-reset/sheet}]))} + {:title (i18n/label :t/backup-keycard) + :image :icon + :image-props :i/copy + :action :arrow + :description :text + :description-props {:text (i18n/label :t/copy-keycard)} + :on-press (fn [] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :shell? true + :content backup-sheet}]))}]}]])) diff --git a/src/status_im/contexts/keycard/manage/view.cljs b/src/status_im/contexts/keycard/manage/view.cljs new file mode 100644 index 00000000000..770ac2de1ce --- /dev/null +++ b/src/status_im/contexts/keycard/manage/view.cljs @@ -0,0 +1,104 @@ +(ns status-im.contexts.keycard.manage.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.contexts.keycard.factory-reset.view :as factory-reset] + [status-im.contexts.keycard.migrate.sheets.view :as sheets.migrate] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn empty-import-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-empty) + :description :text + :description-text (i18n/label :t/what-to-do)}] + [rn/view {:style {:padding-horizontal 28 :padding-top 20}} + [quo/small-option-card + {:variant :main + :title (i18n/label :t/import-key-pair-keycard) + :subtitle (i18n/label :t/use-keycard-login-sign) + :button-label (i18n/label :t/import-profile-key-pair) + :button-props {:type :primary} + :accessibility-label :import-key-pair-keycard + :image (resources/get-image :keycard-buy) + :on-press (fn [] + (rf/dispatch + [:show-bottom-sheet + {:theme :dark + :content + (fn [] + [sheets.migrate/view + {:on-continue #(rf/dispatch [:keycard/migration.get-phrase])}])}]))}]] + [quo/information-box + {:type :default + :style {:margin-top 32 :margin-horizontal 28}} + (i18n/label :t/empty-card-info)]]) + +(defn empty-backup-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-empty) + :description :text + :description-text (i18n/label :t/no-key-pair-keycard)}] + [quo/keycard] + [quo/category + {:list-type :settings + :label (i18n/label :t/what-you-can-do) + :blur? true + :data + [{:title (i18n/label :t/use-backup-keycard) + :image :icon + :image-props :i/copy + :action :arrow + :description :text + :description-props {:text (i18n/label :t/create-backup-profile-keycard)} + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/backup-on-empty-card]))}]}]]) + +(defn not-empty-logout-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-not-empty) + :description :text + :description-text (i18n/label :t/cant-store-new-keys)}] + [quo/keycard] + [quo/category + {:list-type :settings + :label (i18n/label :t/what-you-can-do) + :blur? true + :data + [{:title (i18n/label :t/logout-login-keycard) + :image :icon + :image-props :i/log-out + :action :arrow + :description :text + :description-props {:text (i18n/label :t/keycard-has-keypair)} + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:profile/logout]))} + {:title (i18n/label :t/factory-reset) + :image :icon + :image-props :i/revert + :action :arrow + :description :text + :description-props {:text (i18n/label :t/remove-keycard-content)} + :on-press (fn [] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :shell? true + :content factory-reset/sheet}]))}]}]]) diff --git a/src/status_im/contexts/keycard/migrate/events.cljs b/src/status_im/contexts/keycard/migrate/events.cljs new file mode 100644 index 00000000000..68cac3c0500 --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/events.cljs @@ -0,0 +1,114 @@ +(ns status-im.contexts.keycard.migrate.events + (:require [clojure.string :as string] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :keycard/migration.generate-and-load-keys + (fn [{:keys [db]} [{:keys [has-master-key?]}]] + (let [{:keys [masked-phrase masked-pin + instance-uid]} (get-in db [:keycard :migration]) + pin (security/safe-unmask-data masked-pin)] + (if has-master-key? + {:fx [[:effects.keycard/get-keys + {:pin pin + :on-success #(rf/dispatch [:keycard/migration.convert-to-keycard-profile %]) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]]} + {:fx [[:effects.keycard/generate-and-load-key + {:mnemonic (security/safe-unmask-data masked-phrase) + :pin pin + :on-success (fn [] + (rf/dispatch + [:keycard/connect + {:next-step? true + :instance-uid instance-uid + :on-success #(rf/dispatch + [:keycard/migration.generate-and-load-keys %])}])) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]]})))) + +;; STEP 2: connect and load keys +(rf/reg-event-fx :keycard/migration.connect-and-load-keys + (fn [{:keys [db]}] + {:fx [[:dispatch + [:keycard/connect + {:theme :dark + :instance-uid (get-in db [:keycard :migration :instance-uid]) + :steps 2 + :on-success #(rf/dispatch [:keycard/migration.generate-and-load-keys %])}]]]})) + +(rf/reg-event-fx :keycard/migration.save-instance-uid-and-pin + (fn [{:keys [db]} [masked-pin]] + {:db (-> db + (assoc-in [:keycard :migration :masked-pin] masked-pin) + (assoc-in [:keycard :migration :instance-uid] + (get-in db [:keycard :application-info :instance-uid])))})) + +(defn- save-pin-and-show-migrate + [pin] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:keycard/migration.save-instance-uid-and-pin (security/mask-data pin)]) + (rf/dispatch [:open-modal :screen/keycard.migrate])) + +;; STEP 1: connect and create or enter pin +(rf/reg-event-fx :keycard/migration.authorisation-success + (fn [{:keys [db]} [masked-password]] + {:db (assoc-in db [:keycard :migration :masked-password] masked-password) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:keycard/init.create-or-enter-pin + {:on-success save-pin-and-show-migrate}]]]})) + +(rf/reg-event-fx :keycard/migration.phrase-backed-up + (fn [{:keys [db]}] + {:db (assoc-in db + [:keycard :migration :masked-phrase] + (security/mask-data (get-in db [:profile/profile :mnemonic]))) + :fx [[:dispatch [:profile.settings/profile-update :mnemonic nil]] + [:dispatch [:navigate-back]] + [:dispatch + [:open-modal :screen/keycard.authorise + {:on-success #(rf/dispatch [:keycard/migration.authorisation-success %])}]]]})) + +(defn- backup-recovery-phrase-success + [masked-seed-phrase] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/confirm-backup-dark + {:masked-seed-phrase masked-seed-phrase + :theme :dark + :shell? true + :on-try-again + #(rf/dispatch [:open-modal :screen/backup-recovery-phrase-dark + {:on-success backup-recovery-phrase-success + :masked-seed-phrase (->> masked-seed-phrase + security/safe-unmask-data + (string/join " ") + security/mask-data) + :revealed? true}]) + :on-success + #(rf/dispatch [:keycard/migration.phrase-backed-up])}])) + +(rf/reg-event-fx :keycard/migration.phrase-entered + (fn [{:keys [db]} [{:keys [seed-phrase key-uid]}]] + (if (not= key-uid (get-in db [:profile/profile :key-uid])) + {:fx [[:dispatch + [:enter-seed-phrase/set-error (i18n/label :t/seed-phrase-dont-match-migrate-flow)]]]} + {:db (assoc-in db [:keycard :migration :masked-phrase] seed-phrase) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:open-modal :screen/keycard.authorise + {:on-success #(rf/dispatch [:keycard/migration.authorisation-success %])}]]]}))) + +(rf/reg-event-fx :keycard/migration.get-phrase + (fn [{:keys [db]}] + (let [mnemonic (get-in db [:profile/profile :mnemonic])] + {:db (assoc-in db [:keycard :migration] nil) + :fx [[:dispatch [:navigate-back]] + (if (string/blank? mnemonic) + [:dispatch + [:open-modal :screen/use-recovery-phrase-dark + {:on-success #(rf/dispatch [:keycard/migration.phrase-entered %])}]] + [:dispatch + [:open-modal :screen/backup-recovery-phrase-dark + {:on-success backup-recovery-phrase-success + :masked-seed-phrase (security/mask-data mnemonic)}]])]}))) diff --git a/src/status_im/contexts/keycard/migrate/fail/view.cljs b/src/status_im/contexts/keycard/migrate/fail/view.cljs new file mode 100644 index 00000000000..46f5aa60927 --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/fail/view.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.keycard.migrate.fail.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile-name (rf/sub [:profile/name]) + profile-picture (rf/sub [:profile/image]) + customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/page-top + {:title (i18n/label :t/something-didnt-go-as-planned) + :description :context-tag + :context-tag {:full-name profile-name + :profile-picture profile-picture + :customization-color customization-color} + :container-style {:margin-top constants/page-nav-height}}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-migration-failed)}] + [quo/divider-label (i18n/label :t/what-you-can-do)] + [quo/markdown-list {:description (i18n/label :t/keycard-migration-failed-instruction-1)}] + [quo/markdown-list {:description (i18n/label :t/keycard-migration-failed-instruction-2)}] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/logout) + :button-one-props {:on-press #(rf/dispatch [:profile/logout])}}]])) diff --git a/src/status_im/contexts/keycard/migrate/profile_keys/view.cljs b/src/status_im/contexts/keycard/migrate/profile_keys/view.cljs new file mode 100644 index 00000000000..c2e0acd3541 --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/profile_keys/view.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.keycard.migrate.profile-keys.view + (:require [quo.core :as quo] + [status-im.common.events-helper :as events-helper] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-contains-key-pair) + :description :text + :description-text (i18n/label :t/finalise-migration-instructions)}] + [quo/keycard] + [quo/category + {:list-type :settings + :label (i18n/label :t/what-you-can-do) + :blur? true + :data + [{:title (i18n/label :t/finalise-migration) + :image :icon + :image-props :i/move-up + :action :arrow + :description :text + :description-props {:text (i18n/label :t/use-keycard-instead-password)} + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/keycard.authorise + {:on-success + #(rf/dispatch [:keycard/migration.authorisation-success + %])}]))}]}]]) diff --git a/src/status_im/contexts/keycard/migrate/re_encrypting/events.cljs b/src/status_im/contexts/keycard/migrate/re_encrypting/events.cljs new file mode 100644 index 00000000000..a10aad2ec1c --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/re_encrypting/events.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.keycard.migrate.re-encrypting.events + (:require [clojure.string :as string] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :keycard/migration.convert-to-keycard-profile + (fn [{:keys [db]} [{:keys [key-uid instance-uid encryption-public-key]}]] + (let [{:keys [masked-password]} (get-in db [:keycard :migration]) + {:keys [pairing paired-on]} (get-in db [:keycard :pairings instance-uid]) + {:keys [kdfIterations]} (:profile/profile db)] + {:fx [[:dispatch [:keycard/disconnect]] + [:dispatch [:navigate-back]] + [:dispatch [:open-modal :screen/keycard.re-encrypting]] + [:effects.profile/convert-to-keycard-profile + {:profile + {:key-uid key-uid + :keycard-pairing pairing + :kdfIterations kdfIterations} + :settings + {:keycard-instance-uid instance-uid + :keycard-paired-on paired-on + :keycard-pairing pairing} + :password + (security/safe-unmask-data masked-password) + :new-password + encryption-public-key + :callback + (fn [{:keys [error]}] + (if (string/blank? error) + (do + (rf/dispatch [:biometric/disable]) + (rf/dispatch [:navigate-to :screen/keycard.migrate.success])) + (rf/dispatch [:navigate-to :screen/keycard.migrate.fail])))}]]}))) diff --git a/src/status_im/contexts/keycard/migrate/re_encrypting/view.cljs b/src/status_im/contexts/keycard/migrate/re_encrypting/view.cljs new file mode 100644 index 00000000000..274cb5daa22 --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/re_encrypting/view.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.keycard.migrate.re-encrypting.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile-name (rf/sub [:profile/name]) + profile-picture (rf/sub [:profile/image]) + customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/page-top + {:title (i18n/label :t/re-encrypting-data) + :description :context-tag + :context-tag {:full-name profile-name + :profile-picture profile-picture + :customization-color customization-color} + :container-style {:margin-top constants/page-nav-height}}] + [quo/text {:style {:padding-horizontal 20}} + (i18n/label :t/do-not-quit)] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-migration)}]])) diff --git a/src/status_im/contexts/keycard/migrate/sheets/view.cljs b/src/status_im/contexts/keycard/migrate/sheets/view.cljs new file mode 100644 index 00000000000..886aef46548 --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/sheets/view.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.keycard.migrate.sheets.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [on-continue]}] + (let [profile-name (rf/sub [:profile/name]) + profile-picture (rf/sub [:profile/image]) + customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :default + :title (i18n/label :t/migrate-key-pair-keycard) + :full-name profile-name + :profile-picture profile-picture + :customization-color customization-color}] + [rn/view {:style {:padding-horizontal 20}} + [quo/text {} + (i18n/label :t/migrate-key-pair-keycard-default-key {:name profile-name})] + [quo/information-box + {:type :default + :style {:margin-top 20 :margin-bottom 12}} + (i18n/label :t/migrate-key-pair-keycard-info)]] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/continue) + :button-one-props {:on-press on-continue} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) diff --git a/src/status_im/contexts/keycard/migrate/success/view.cljs b/src/status_im/contexts/keycard/migrate/success/view.cljs new file mode 100644 index 00000000000..55dc46290dd --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/success/view.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.keycard.migrate.success.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile-name (rf/sub [:profile/name]) + profile-picture (rf/sub [:profile/image]) + customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/page-top + {:title (i18n/label :t/key-pair-migrated-successfully) + :description :context-tag + :context-tag {:full-name profile-name + :profile-picture profile-picture + :customization-color customization-color} + :container-style {:margin-top constants/page-nav-height}}] + [quo/text {:style {:padding-horizontal 20}} + (i18n/label :t/use-keycard-for-status)] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-migration-succeeded)}] + [quo/button + {:on-press #(rf/dispatch [:profile/logout]) + :container-style {:margin-horizontal 20 :margin-vertical 12}} + (i18n/label :t/logout)]])) diff --git a/src/status_im/contexts/keycard/migrate/view.cljs b/src/status_im/contexts/keycard/migrate/view.cljs new file mode 100644 index 00000000000..8f6969b8941 --- /dev/null +++ b/src/status_im/contexts/keycard/migrate/view.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.keycard.migrate.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.contexts.keycard.common.view :as common.view] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile-name (rf/sub [:profile/name]) + profile-picture (rf/sub [:profile/image]) + customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/ready-to-migrate-key-pair) + :description :context-tag + :context-tag {:full-name profile-name + :profile-picture profile-picture + :customization-color customization-color}}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-migration)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/scan-keycard) + :button-one-props {:on-press #(rf/dispatch [:keycard/migration.connect-and-load-keys])}}]])) diff --git a/src/status_im/contexts/keycard/nfc/effects.cljs b/src/status_im/contexts/keycard/nfc/effects.cljs new file mode 100644 index 00000000000..0fe03b4372d --- /dev/null +++ b/src/status_im/contexts/keycard/nfc/effects.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.keycard.nfc.effects + (:require [keycard.keycard :as keycard] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.keycard/check-nfc-enabled + (fn [] + (keycard/check-nfc-enabled + {:on-success #(rf/dispatch [:keycard/on-check-nfc-enabled-success %])}))) + +(rf/reg-fx :effects.keycard/start-nfc keycard/start-nfc) +(rf/reg-fx :effects.keycard/stop-nfc keycard/stop-nfc) diff --git a/src/status_im/contexts/keycard/nfc/events.cljs b/src/status_im/contexts/keycard/nfc/events.cljs new file mode 100644 index 00000000000..5cbc6ebcfd3 --- /dev/null +++ b/src/status_im/contexts/keycard/nfc/events.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.keycard.nfc.events + (:require [utils.re-frame :as rf])) + +(rf/reg-event-fx :keycard.ios/start-nfc + (fn [_] + {:fx [[:effects.keycard/start-nfc]]})) + +(rf/reg-event-fx :keycard.ios/on-start-nfc-success + (fn [{:keys [db]} [{:keys [on-cancel-event-vector]}]] + {:db (assoc-in db [:keycard :on-nfc-cancelled-event-vector] on-cancel-event-vector)})) + +(rf/reg-event-fx :keycard.ios/on-nfc-timeout + (fn [{:keys [db]} _] + {:db (assoc-in db [:keycard :card-connected?] false) + :fx [[:dispatch-later [{:ms 500 :dispatch [:keycard.ios/start-nfc]}]]]})) + +(rf/reg-event-fx :keycard/on-check-nfc-enabled-success + (fn [{:keys [db]} [nfc-enabled?]] + {:db (assoc-in db [:keycard :nfc-enabled?] nfc-enabled?)})) + +(rf/reg-event-fx :keycard.ios/on-nfc-user-cancelled + (fn [{:keys [db]}] + {:db (assoc-in db [:keycard :on-nfc-cancelled-event-vector] nil) + :fx [(when-let [on-nfc-cancelled-event-vector (get-in db [:keycard :on-nfc-cancelled-event-vector])] + [:dispatch on-nfc-cancelled-event-vector])]})) diff --git a/src/status_im/contexts/keycard/nfc/sheets/events.cljs b/src/status_im/contexts/keycard/nfc/sheets/events.cljs new file mode 100644 index 00000000000..58fcf2e047d --- /dev/null +++ b/src/status_im/contexts/keycard/nfc/sheets/events.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.keycard.nfc.sheets.events + (:require [re-frame.core :as rf] + [react-native.platform :as platform])) + +(rf/reg-event-fx :keycard/show-connection-sheet + (fn [{:keys [db]} [{:keys [on-cancel-event-vector theme]} :as args]] + (if platform/android? + {:db (assoc-in db + [:keycard :connection-sheet-opts] + {:on-close #(rf/dispatch on-cancel-event-vector) + :theme theme}) + :fx [[:effects.keycard/start-nfc nil] + [:dismiss-keyboard true] + [:show-nfc-sheet nil]]} + {:effects.keycard/start-nfc + {:on-success #(rf/dispatch [:keycard.ios/on-start-nfc-success args])}}))) + +(rf/reg-event-fx :keycard/hide-connection-sheet + (fn [{:keys [db]} [{:keys [success?]}]] + (if platform/android? + {:db (-> db + (assoc-in [:keycard :connection-sheet-opts :hide?] true) + (assoc-in [:keycard :connection-sheet-opts :success?] success?)) + :fx [[:effects.keycard/stop-nfc nil]]} + {:effects.keycard/stop-nfc nil}))) + +(rf/reg-event-fx :keycard/connection-sheet-hidden + (fn [{:keys [db]}] + {:db (assoc-in db [:keycard :connection-sheet-opts] nil) + :hide-nfc-sheet nil})) diff --git a/src/status_im/contexts/keycard/nfc/sheets/style.cljs b/src/status_im/contexts/keycard/nfc/sheets/style.cljs new file mode 100644 index 00000000000..558cf1b255e --- /dev/null +++ b/src/status_im/contexts/keycard/nfc/sheets/style.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.keycard.nfc.sheets.style + (:require [quo.foundations.colors :as colors])) + +(def ^:private sheet-border-radius 38) + +(defn sheet + [theme] + {:position :absolute + :height 380 + :bottom 0 + :left 0 + :right 0 + :z-index 1 + :align-items :center + :padding-horizontal 20 + :padding-top 20 + :padding-bottom 34 + :background-color (colors/theme-colors colors/white colors/neutral-100 theme) + :border-top-left-radius sheet-border-radius + :border-top-right-radius sheet-border-radius}) diff --git a/src/status_im/contexts/keycard/nfc/sheets/view.cljs b/src/status_im/contexts/keycard/nfc/sheets/view.cljs new file mode 100644 index 00000000000..4e31f52872d --- /dev/null +++ b/src/status_im/contexts/keycard/nfc/sheets/view.cljs @@ -0,0 +1,105 @@ +(ns status-im.contexts.keycard.nfc.sheets.view + (:require quo.context + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.common.resources :as resources] + [status-im.contexts.keycard.nfc.sheets.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def duration 450) +(def timing-options #js {:duration duration}) + +(defn hide + [translate-y bg-opacity window-height on-close] + (when (fn? on-close) + (on-close)) + ;; it will be better to use animation callback, but it doesn't work + ;; so we have to use timeout, also we add 50ms for safety + (js/setTimeout #(rf/dispatch [:keycard/connection-sheet-hidden]) (+ duration 250)) + ;; we want to give user some time (200ms) to see a success icon + (js/setTimeout + (fn [] + (reanimated/set-shared-value translate-y + (reanimated/with-timing window-height timing-options)) + (reanimated/set-shared-value bg-opacity (reanimated/with-timing 0 timing-options))) + 200)) + +(defn show + [translate-y bg-opacity] + (reanimated/set-shared-value translate-y (reanimated/with-timing 0 timing-options)) + (reanimated/set-shared-value bg-opacity (reanimated/with-timing 1 timing-options))) + +(defn android-view + [] + (let [connected? (rf/sub [:keycard/connected?]) + current-step (rf/sub [:keycard/current-step]) + steps (rf/sub [:keycard/steps]) + [was-connected-once? set-was-connected-once?] (rn/use-state false) + {:keys [on-close theme hide? success?]} (rf/sub [:keycard/connection-sheet-opts]) + theme (or theme (quo.context/use-theme)) + {window-height :height} (rn/get-window) + bg-opacity (reanimated/use-shared-value 0) + translate-y (reanimated/use-shared-value window-height) + disconnected? (and was-connected-once? + (not connected?) + (not success?))] + (rn/use-effect #(if hide? + (hide translate-y bg-opacity window-height on-close) + (show translate-y bg-opacity)) + [hide?]) + (rn/use-effect #(when connected? (set-was-connected-once? true) [connected?])) + [rn/view {:style {:flex 1}} + ;; backdrop + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity bg-opacity} + {:flex 1 + :background-color colors/neutral-100-opa-70})}] + ;; sheet + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:transform [{:translateY translate-y}]} + (style/sheet theme))} + (if success? + [:<> + [rn/image + {:source (resources/get-image :nfc-success) + :style {:margin-top 64}}] + [rn/text + {:style {:font-size 16 + :text-align :center + :color (colors/theme-colors colors/neutral-100 colors/white theme) + :margin-vertical 32}} + (i18n/label :t/success)]] + [:<> + [rn/text + {:style {:font-size 26 + :color (colors/theme-colors colors/neutral-100 colors/white theme) + :margin-bottom 32}} + (if connected? + (i18n/label :t/scanning) + (if disconnected? + (i18n/label :t/disconnected) + (i18n/label :t/ready-to-scan)))] + [rn/image + {:source (resources/get-image (if disconnected? :nfc-fail :nfc-prompt-android))}] + [rn/text + {:style {:font-size 16 + :text-align :center + :color (colors/theme-colors colors/neutral-100 colors/white theme) + :margin-vertical 32}} + (if connected? + (i18n/label :t/connected-dont-move) + (i18n/label :t/hold-phone-near-keycard)) + (when steps + (str "\n" (i18n/label :t/step-i-of-n {:step current-step :number steps})))] + [rn/view {:style {:flex-direction :row}} + [quo/button + {:type :dark-grey + :container-style {:flex 1} + :accessibility-label :cancel-button + :on-press #(rf/dispatch [:keycard/hide-connection-sheet {:success? false}])} + (i18n/label :t/cancel)]]])]])) diff --git a/src/status_im/contexts/keycard/not_keycard/view.cljs b/src/status_im/contexts/keycard/not_keycard/view.cljs new file mode 100644 index 00000000000..bce44270af6 --- /dev/null +++ b/src/status_im/contexts/keycard/not_keycard/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.keycard.not-keycard.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [utils.i18n :as i18n])) + +(defn view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/oops-not-keycard) + :description :text + :description-text (i18n/label :t/make-sure-keycard)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :not-keycard)}] + [rn/view {:style {:padding-horizontal 20 :padding-vertical 12}} + [quo/button {:on-press events-helper/navigate-back} + (i18n/label :t/try-again)]]]) diff --git a/src/status_im/contexts/keycard/pin/create/view.cljs b/src/status_im/contexts/keycard/pin/create/view.cljs new file mode 100644 index 00000000000..ffbc846e140 --- /dev/null +++ b/src/status_im/contexts/keycard/pin/create/view.cljs @@ -0,0 +1,69 @@ +(ns status-im.contexts.keycard.pin.create.view + (:require [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.constants :as constants] + [utils.i18n :as i18n])) + +(defn view + [] + (let [{:keys [on-complete title repeat-stage-title]} (quo.context/use-screen-params) + [pin set-pin] (rn/use-state "") + [first-pin set-first-pin] (rn/use-state "") + [error? set-error] (rn/use-state false) + [stage set-stage] (rn/use-state :create) + pin-empty? (string/blank? pin) + on-delete (rn/use-callback + (fn [] + (set-error false) + (if (= (count pin) 1) + (do + (set-pin "") + (set-stage :create)) + (when (and pin (pos? (count pin))) + (set-pin (.slice pin 0 -1))))) + [pin]) + on-press (rn/use-callback + (fn [new-symbol] + (let [new-pin (str pin new-symbol)] + (when (<= (count new-pin) + constants/pincode-length) + (set-pin new-pin) + (when (= constants/pincode-length + (count new-pin)) + (if (= :repeat stage) + (if (= first-pin new-pin) + (do + (set-pin "") + (on-complete new-pin)) + (set-error true)) + (do + (set-pin "") + (set-first-pin new-pin) + (set-stage :repeat))))))) + [pin stage first-pin])] + [rn/view {:style {:padding-bottom 12 :flex 1}} + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (if (= :create stage) + (or title (i18n/label :t/create-keycard-pin)) + (or repeat-stage-title (i18n/label :t/repeat-keycard-pin))) + :description :text + :description-text (i18n/label :t/pin-needed-login-sign)}] + [rn/view {:style {:justify-content :space-between :flex 1}} + [quo/keycard] + [rn/view {:style {:justify-content :center :align-items :center :padding-vertical 34}} + [quo/pin-input + {:blur? false + :number-of-pins constants/pincode-length + :number-of-filled-pins (count pin) + :error? error? + :info (when error? (i18n/label :t/pin-not-match))}]] + [quo/numbered-keyboard + {:delete-key? (not pin-empty?) + :on-delete on-delete + :on-press on-press}]]])) diff --git a/src/status_im/contexts/keycard/pin/enter/view.cljs b/src/status_im/contexts/keycard/pin/enter/view.cljs new file mode 100644 index 00000000000..9e7f1bb0f08 --- /dev/null +++ b/src/status_im/contexts/keycard/pin/enter/view.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.keycard.pin.enter.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.contexts.keycard.pin.view :as keycard.pin] + [utils.i18n :as i18n])) + +(defn view + [] + (let [{:keys [on-complete title]} (quo.context/use-screen-params)] + [rn/view {:style {:padding-bottom 12 :flex 1}} + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top {:title (or title (i18n/label :t/enter-keycard-pin))}] + [quo/keycard] + [keycard.pin/auth {:on-complete on-complete}]])) diff --git a/src/status_im/contexts/keycard/pin/events.cljs b/src/status_im/contexts/keycard/pin/events.cljs new file mode 100644 index 00000000000..9834d95c8ce --- /dev/null +++ b/src/status_im/contexts/keycard/pin/events.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.keycard.pin.events + (:require [utils.re-frame :as rf])) + +(rf/reg-event-fx :keycard.pin/delete-pressed + (fn [{:keys [db]}] + (let [pin (get-in db [:keycard :pin :text])] + (when (and pin (pos? (count pin))) + {:db (-> db + (assoc-in [:keycard :pin :text] (.slice pin 0 -1)) + (assoc-in [:keycard :pin :status] nil))})))) + +(rf/reg-fx :effects.keycard.pin/dispatch-on-complete + (fn [[on-complete new-pin]] + (on-complete new-pin))) + +(rf/reg-event-fx :keycard.pin/number-pressed + (fn [{:keys [db]} [number max-numbers on-complete]] + (let [pin (get-in db [:keycard :pin :text]) + new-pin (str pin number) + last-number? (= max-numbers (count new-pin))] + (when (<= (count new-pin) max-numbers) + {:db (-> db + (assoc-in [:keycard :pin :text] (when-not last-number? new-pin)) + (assoc-in [:keycard :pin :status] nil)) + :fx [(when (and on-complete last-number?) + [:effects.keycard.pin/dispatch-on-complete [on-complete new-pin]])]})))) + +(rf/reg-event-fx :keycard.pin/clear + (fn [{:keys [db]}] + {:db (assoc-in db [:keycard :pin] nil)})) diff --git a/src/status_im/contexts/keycard/pin/view.cljs b/src/status_im/contexts/keycard/pin/view.cljs new file mode 100644 index 00000000000..517e32955b7 --- /dev/null +++ b/src/status_im/contexts/keycard/pin/view.cljs @@ -0,0 +1,42 @@ +(ns status-im.contexts.keycard.pin.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn auth + [{:keys [on-complete error]}] + (let [{:keys [error? error-message]} error + {:keys [text status]} (rf/sub [:keycard/pin]) + pin-retry-counter (rf/sub [:keycard/pin-retry-counter]) + error? (or error? (= status :error))] + (rn/use-unmount #(rf/dispatch [:keycard.pin/clear])) + [rn/view + {:style {:flex 1 + :gap 34 + :padding-bottom 12}} + [rn/view {:style {:flex 1 :justify-content :center :align-items :center}} + [quo/pin-input + {:blur? false + :number-of-pins constants/pincode-length + :number-of-filled-pins (count text) + :info-error? error? + :info (when error? + (if (not (string/blank? error-message)) + error-message + (i18n/label-pluralize pin-retry-counter :t/pin-retries-left)))}]] + [quo/numbered-keyboard + {:delete-key? true + :on-delete #(rf/dispatch [:keycard.pin/delete-pressed]) + :on-press #(rf/dispatch [:keycard.pin/number-pressed % constants/pincode-length + on-complete])}]])) + +(defn auth-sheet + [params] + [:<> + [quo/drawer-top + {:container-style {:padding-horizontal 20 :padding-bottom 34} + :title (i18n/label :t/enter-keycard-pin)}] + [auth params]]) diff --git a/src/status_im/contexts/keycard/sign/events.cljs b/src/status_im/contexts/keycard/sign/events.cljs new file mode 100644 index 00000000000..bb1c5d0cb14 --- /dev/null +++ b/src/status_im/contexts/keycard/sign/events.cljs @@ -0,0 +1,48 @@ +(ns status-im.contexts.keycard.sign.events + (:require [status-im.contexts.keycard.utils :as keycard.utils] + [utils.address] + [utils.re-frame :as rf])) + +(defn- append-account-path + [db payload] + (let [accounts (get-in db [:wallet :accounts])] + (->> (:address payload) + (get accounts) + :path + (assoc payload :path)))) + +(defn- add-paths-to-payloads + [db payloads] + (reduce (fn [acc message-data] + (conj acc (append-account-path db message-data))) + [] + payloads)) + +(rf/reg-event-fx :keycard/sign-payloads + (fn [_ [data]] + {:effects.keycard/sign-payloads data})) + +(rf/reg-event-fx + :keycard/connect-and-sign-payloads + (fn [{:keys [db]} [{:keys [keycard-pin payloads on-success on-failure]}]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:dispatch + [:keycard/connect + {:key-uid key-uid + :on-success + (fn [] + (rf/dispatch + [:keycard/sign-payloads + {:pin keycard-pin + :payloads (add-paths-to-payloads db payloads) + :on-success (fn [signatures] + (rf/dispatch [:keycard/disconnect]) + (when on-success (on-success signatures))) + :on-failure (fn [error] + ;; NOTE: pass down the error only if it's not the "wrong PIN" + ;; error, as that's already handled in + ;; `:keycard/on-action-with-pin-error` + (rf/dispatch [:keycard/on-action-with-pin-error error]) + (when (and on-failure (not (keycard.utils/pin-retries (:error error)))) + (on-failure (ex-info "Failed to sign with the keycard" + {:error error}))))}]))}]]]}))) diff --git a/src/status_im/contexts/keycard/unblock/events.cljs b/src/status_im/contexts/keycard/unblock/events.cljs new file mode 100644 index 00000000000..9120d1f7361 --- /dev/null +++ b/src/status_im/contexts/keycard/unblock/events.cljs @@ -0,0 +1,93 @@ +(ns status-im.contexts.keycard.unblock.events + (:require [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn show-different-card + [] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/keycard.different-card])) + +(defn show-success + [] + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/keycard.unblock.success])) + +(rf/reg-event-fx :keycard/unblock.pin-created + (fn [{:keys [db]} [masked-pin]] + {:db (-> db + (assoc-in [:keycard :unblock :masked-pin] masked-pin) + (assoc-in [:keycard :unblock :instance-uid] + (get-in db [:keycard :application-info :instance-uid]))) + :fx [[:dispatch [:navigate-back]] + [:dispatch [:open-modal :screen/keycard.unblock.ready-to-unblock]]]})) + +(rf/reg-event-fx :keycard/unblock.phrase-entered + (fn [{:keys [db]} [{:keys [seed-phrase key-uid]}]] + (if (not= key-uid (get-in db [:keycard :application-info :key-uid])) + {:fx [[:dispatch + [:enter-seed-phrase/set-error (i18n/label :t/seed-phrase-dont-match-migrate-flow)]]]} + {:db (assoc-in db [:keycard :unblock :masked-phrase] seed-phrase) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:open-modal :screen/keycard.pin.create + {:on-complete #(rf/dispatch [:keycard/unblock.pin-created (security/mask-data %)])}]]]}))) + +(rf/reg-event-fx :keycard/unblock.generate-and-load-key + (fn [{:keys [db]}] + (let [{:keys [masked-phrase masked-pin]} (get-in db [:keycard :unblock])] + {:fx [[:effects.keycard/generate-and-load-key + {:mnemonic (security/safe-unmask-data masked-phrase) + :pin (security/safe-unmask-data masked-pin) + :on-success show-success}]]}))) + +;; third stage, import keys +;; it's tricky to compare instance-uid for reseted card +(defn import-keys + [] + (rf/dispatch [:keycard/connect + {:next-step? true + :on-success + (fn [{:keys [has-master-key?]}] + (if has-master-key? + (show-success) + (rf/dispatch [:keycard/unblock.generate-and-load-key])))}])) + +;; second stage, init card with pin +;; there is no way to compare instance-uid for reseted card +(defn init-with-pin + [masked-pin] + (rf/dispatch + [:keycard/connect + {:next-step? true + :on-success + (fn [{:keys [initialized? has-master-key?]}] + (if has-master-key? + (show-different-card) + (if initialized? + (import-keys) + (rf/dispatch [:keycard/init-card + {:pin (security/safe-unmask-data masked-pin) + :on-success import-keys}]))))}])) + +;; first stage, reset card +(rf/reg-event-fx :keycard/unblock + (fn [{:keys [db]}] + (let [masked-pin (get-in db [:keycard :unblock :masked-pin])] + {:fx [[:dispatch + [:keycard/connect + {:instance-uid (get-in db [:keycard :unblock :instance-uid]) + :steps 3 + :on-success + (fn [{:keys [initialized?]}] + (if initialized? + (show-different-card) + (init-with-pin masked-pin))) + :on-error + (fn [error] + (if (or (= error :keycard/error.keycard-frozen) + (= error :keycard/error.keycard-locked)) + (rf/dispatch [:keycard/factory-reset {:on-success #(init-with-pin masked-pin)}]) + (rf/dispatch [:keycard/on-application-info-error error])))}]]]}))) diff --git a/src/status_im/contexts/keycard/unblock/view.cljs b/src/status_im/contexts/keycard/unblock/view.cljs new file mode 100644 index 00000000000..0d53bdf3631 --- /dev/null +++ b/src/status_im/contexts/keycard/unblock/view.cljs @@ -0,0 +1,69 @@ +(ns status-im.contexts.keycard.unblock.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.contexts.keycard.common.view :as common.view] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn success-view + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard-unblocked) + :description :text + :description-text (i18n/label :t/keycard-unblocked-description)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-unblock-positive)}] + [rn/view {:style {:padding-horizontal 20 :padding-vertical 12}} + [quo/button {:on-press events-helper/navigate-back} + (i18n/label :t/done)]]]) + +(defn ready-to-unblock + [] + [:<> + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/ready-to-unblock-keycard)}] + [rn/image + {:resize-mode :contain + :style {:flex 1 :width (:width (rn/get-window))} + :source (resources/get-image :keycard-unblock)}] + [common.view/tips] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/scan-keycard) + :button-one-props {:on-press #(rf/dispatch [:keycard/unblock])}}]]) + +(defn sheet + [] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title (i18n/label :t/unblock-keycard-recovery)}] + [quo/text + {:style {:padding-horizontal 20 + :padding-vertical 8}} + (i18n/label :t/unblock-keycard-instructions)] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/continue) + :button-one-props {:customization-color + customization-color + :on-press + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:navigate-back]) + (rf/dispatch + [:open-modal :screen/use-recovery-phrase-dark + {:on-success #(rf/dispatch [:keycard/unblock.phrase-entered %])}]))} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) diff --git a/src/status_im/contexts/keycard/utils.cljs b/src/status_im/contexts/keycard/utils.cljs new file mode 100644 index 00000000000..99bb320e3e7 --- /dev/null +++ b/src/status_im/contexts/keycard/utils.cljs @@ -0,0 +1,97 @@ +(ns status-im.contexts.keycard.utils + (:require [clojure.string :as string] + [utils.address :as address] + [utils.transforms :as transforms])) + +(def pin-mismatch-error #"Unexpected error SW, 0x63C(\d+)|wrongPIN\(retryCounter: (\d+)\)") + +(defn pin-retries + [error] + (some->> error + (re-matches pin-mismatch-error) + (filter some?) + second + js/parseInt)) + +(defn tag-lost? + [error] + (when error + (or + (= error "Tag was lost.") + (= error "NFCError:100") + (= error "NFCError:102") + (= error "Malformed card response") + (re-matches #".*NFCError:100.*" error)))) + +(defn validate-application-info + [{:keys [has-master-key? paired? pin-retry-counter puk-retry-counter] :as application-info} + instance-uid key-uid] + + (cond + (empty? application-info) + :keycard/error.not-keycard + + (or (and instance-uid (not= (:instance-uid application-info) instance-uid)) + (and key-uid (not= (:key-uid application-info) key-uid))) + :keycard/error.keycard-different + + (and (zero? pin-retry-counter) + (or (nil? puk-retry-counter) + (pos? puk-retry-counter))) + :keycard/error.keycard-frozen + + (zero? puk-retry-counter) + :keycard/error.keycard-locked + + (not has-master-key?) + nil + + (not paired?) + :keycard/error.keycard-unpaired + + :else + nil)) + +(defn- error-object->map + [^js object] + {:code (.-code object) + :error (.-message object)}) + +(defn normalize-key-uid + [{:keys [key-uid] :as data}] + (if (string/blank? key-uid) + data + (update data :key-uid address/normalized-hex))) + +(defn format-success-data + [data] + (normalize-key-uid (transforms/js->clj data))) + +(defn get-on-success + [{:keys [on-success]}] + #(when on-success (on-success (format-success-data %)))) + +(defn get-on-failure + [{:keys [on-failure]}] + #(when on-failure (on-failure (error-object->map %)))) + +(defn wrap-handlers + [args] + (assoc + args + :on-success (get-on-success args) + :on-failure (get-on-failure args))) + +(defn keycard-address? + [keypairs address] + (let [find-keycard-keypair (fn [kps] (some #(when-not (empty? (:keycards %)) %) kps)) + keypair-addresses (fn [kp] + (->> (:accounts kp) + (map :address) + set))] + (when-not (nil? address) + (-> keypairs + vals + find-keycard-keypair + keypair-addresses + (contains? (string/lower-case address)))))) diff --git a/src/status_im/contexts/market/token/style.cljs b/src/status_im/contexts/market/token/style.cljs new file mode 100644 index 00000000000..e1ba11166a0 --- /dev/null +++ b/src/status_im/contexts/market/token/style.cljs @@ -0,0 +1,114 @@ +(ns status-im.contexts.market.token.style + (:require [quo.foundations.colors :as colors])) + +(defn header-page-nav + [theme] + {:background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme)}) + +(defn header-top + [theme] + {:padding-vertical 12 + :padding-horizontal 20 + :row-gap 8 + :background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme)}) + +(def header-title-row + {:flex 1 + :flex-direction :row + :align-items :flex-end + :column-gap 8}) + +(def header-token-name-text + {}) + +(defn header-token-ticker-text + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-50 theme) + :padding-bottom 1}) + +(def header-buttons + {:flex-direction :row + :display :flex + :column-gap 12 + :margin-top 12 + :margin-bottom 4}) + +(def header-button + {:flex 1}) + +(defn content-container + [theme] + {:border-top-width 1 + :border-top-color (colors/theme-colors colors/neutral-10 colors/neutral-90 theme) + :padding-left 20 + :padding-right 20 + :padding-top 8 + :column-gap 12}) + +(def content-row + {:flex-direction :row}) + +(def token-overview + {:flex 1 + :padding-top 12 + :padding-bottom 16}) + +(def token-overview-info-row + {:flex-direction :row + :column-gap 6 + :align-items :center}) + +(defn token-overview-icon-props + [theme positive?] + {:color (if positive? + (colors/theme-colors colors/success-50 colors/success-60 theme) + (colors/theme-colors colors/danger-50 colors/danger-60 theme)) + :size 16}) + +(defn token-overview-change-text + [theme positive?] + {:color (if positive? + (colors/theme-colors colors/success-50 colors/success-60 theme) + (colors/theme-colors colors/danger-50 colors/danger-60 theme))}) + +(defn token-overview-change-time-text + [theme] + {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}) + +(defn token-parameter + [first?] + {:flex 1 + :padding-left (if first? 0 16) + :padding-right 16 + :padding-top 12 + :padding-bottom 12}) + +(defn token-parameter-title + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-50 theme)}) + +(defn token-parameter-value + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn dashed-line-outer-container + [theme type] + {:width (if (= type :horizontal) "100%" 1) + :height (if (= type :horizontal) 1 "100%") + :border-radius 1 + :border-width 1 + :border-style :dashed + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :z-index 0}) + +(defn dashed-line-inner-container + [theme type] + {:position :absolute + :left (if (= type :horizontal) -1 0) + :right (if (= type :horizontal) -1 0) + :top (if (= type :horizontal) 0 -1) + :bottom (if (= type :horizontal) 0 -1) + :height (if (= type :horizontal) 1 "100%") + :width (if (= type :horizontal) "100%" 1) + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :z-index 1}) diff --git a/src/status_im/contexts/market/token/view.cljs b/src/status_im/contexts/market/token/view.cljs new file mode 100644 index 00000000000..b21d054367e --- /dev/null +++ b/src/status_im/contexts/market/token/view.cljs @@ -0,0 +1,112 @@ +(ns status-im.contexts.market.token.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.market.token.style :as style] + [utils.i18n :as i18n] + [utils.number])) + +(defn token-overview + [{:keys [theme value change]}] + [rn/view {:style style/token-overview} + [quo/text + {:size :heading-2 + :weight :semi-bold} + value] + [rn/view {:style style/token-overview-info-row} + [quo/icon + (if (pos? change) + :i/positive + :i/negative) + (style/token-overview-icon-props theme (pos? change))] + [quo/text + {:style (style/token-overview-change-text theme (pos? change)) + :size :paragraph-2} + (utils.number/format-as-percentage change) "%"] + [quo/text + {:style (style/token-overview-change-time-text theme) + :size :paragraph-2 + :weight :medium} + (i18n/label :t/time-24h)]]]) + +(defn token-parameter + [{:keys [theme title value first?]}] + [rn/view {:style (style/token-parameter first?)} + [quo/text + {:size :paragraph-2 + :style (style/token-parameter-title theme)} + title] + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/token-parameter-value theme)} + value]]) + +(defn dashed-line + [{:keys [type]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/dashed-line-outer-container theme type)} + [rn/view {:style (style/dashed-line-inner-container theme type)}]])) + +(defn view + [] + (let [theme (quo.context/use-theme)] + [floating-button-page/view + {:footer-container-padding 0 + :header [rn/view {:style (style/header-page-nav theme)} + [quo/page-nav + {:type :no-title + :icon-name :i/close + :on-press events-helper/navigate-back + :margin-top safe-area/top + :background :blur}]]} + [rn/view {:style (style/header-top theme)} + [rn/view {:style style/header-title-row} + [quo/token {:token :eth :size :size-32}] + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/header-token-name-text + :accessibility-label :token-name} + "Ethereum"] + [quo/text + {:size :heading-2 + :weight :medium + :style (style/header-token-ticker-text theme)} + "ETH"]] + [rn/view {:style style/header-buttons} + [rn/view {:style style/header-button} + [quo/button + {:size 40 + :icon-left :i/swap + :type :primary} + (i18n/label :t/swap)]] + [rn/view {:style style/header-button} + [quo/button + {:size 40 + :icon-left :i/buy + :type :outline} + (i18n/label :t/buy)]]]] + [rn/view {:style (style/content-container theme)} + [rn/view {:style style/content-row} + [token-overview + {:theme theme + :value "€575.56" + :change -0.101}]] + [dashed-line {:type :horizontal}] + [rn/view {:style style/content-row} + [token-parameter + {:theme theme + :title "Market cap" + :value "€84,817,829,837" + :first? true}] + [dashed-line {:type :vertical}] + [token-parameter + {:theme theme + :title "24h Volume" + :value "280,080,208" + :first? false}]]]])) diff --git a/src/status_im/contexts/market/view.cljs b/src/status_im/contexts/market/view.cljs new file mode 100644 index 00000000000..37af185c811 --- /dev/null +++ b/src/status_im/contexts/market/view.cljs @@ -0,0 +1,72 @@ +(ns status-im.contexts.market.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.home.top-nav.view :as common.top-nav] + [status-im.common.refreshable-flat-list.view :as refreshable-flat-list] + [status-im.contexts.market.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn swap-header + [] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/swap-header-container theme)} + [quo/text + {:weight :semi-bold + :size :heading-1 + :style (style/market-header-text theme)} + (i18n/label :t/market)] + [quo/button + {:size 32 + :icon-left :i/swap + :on-press #()} + (i18n/label :t/swap)]])) + +(defn sort-header + [] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/sort-header-container theme)} + [quo/text + {:size :paragraph-2 + :style (style/sort-text theme)} + (i18n/label :t/market-cap)] + [quo/icon :i/arrow-down + {:size 12 + :color colors/neutral-50 + :container-style {:margin-left 4}}]])) + +(defn token + [token-data] + [quo/market-token + (assoc token-data + :on-press + (fn [] + (rf/dispatch [:open-modal :screen/market.token])))]) + +(defn view + [] + (let [tokens-loading? (rf/sub [:wallet/home-tokens-loading?]) + tokens (rf/sub [:market/tokens]) + [init-loaded? set-init-loaded] (rn/use-state false)] + (rn/use-effect + #(when (and (boolean? tokens-loading?) (not tokens-loading?) (not init-loaded?)) + (set-init-loaded true)) + [tokens-loading?]) + [rn/view {:style (style/home-container)} + [common.top-nav/view] + [refreshable-flat-list/view + {:refresh-control [rn/refresh-control + {:refreshing (and tokens-loading? init-loaded?) + :colors [colors/neutral-40] + :tint-color colors/neutral-40 + :on-refresh #()}] + :header [rn/view + [swap-header] + [sort-header]] + :content-container-style style/list-container + :sticky-header-indices [0] + :data tokens + :render-fn token}]])) diff --git a/src/status_im/contexts/network/db.cljs b/src/status_im/contexts/network/db.cljs new file mode 100644 index 00000000000..93f7a677f51 --- /dev/null +++ b/src/status_im/contexts/network/db.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.network.db) + +(defn online? + [{:network/keys [status]}] + (= :online status)) diff --git a/src/status_im/contexts/network/effects.cljs b/src/status_im/contexts/network/effects.cljs new file mode 100644 index 00000000000..e227e335cde --- /dev/null +++ b/src/status_im/contexts/network/effects.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.network.effects + (:require + [native-module.core :as native-module] + [react-native.net-info :as net-info] + [utils.re-frame :as rf])) + +(rf/reg-fx + :effects.network/listen-to-network-info + (fn [] + (net-info/add-net-info-listener + #(rf/dispatch [:network/on-state-change + (js->clj % :keywordize-keys true)])))) + +(rf/reg-fx + :effects.network/notify-status-go + (fn [network-type expensive?] + (native-module/connection-change network-type expensive?))) diff --git a/src/status_im/contexts/network/events.cljs b/src/status_im/contexts/network/events.cljs new file mode 100644 index 00000000000..f9c03fcb0ea --- /dev/null +++ b/src/status_im/contexts/network/events.cljs @@ -0,0 +1,76 @@ +(ns status-im.contexts.network.events + (:require + [status-im.common.data-confirmation-sheet.view :as data-confirmation-sheet] + [status-im.config :as config] + [status-im.feature-flags :as ff] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :network/set-syncing-on-mobile-network + (fn [{:keys [db]} [syncing-on-mobile-network?]] + (when config/mobile-data-syncing-toggle-enabled? + {:db (update db + :profile/profile + assoc + :syncing-on-mobile-network? + syncing-on-mobile-network? + :remember-syncing-choice? + true) + :fx [[:json-rpc/call + [{:method "wakuext_setSyncingOnMobileNetwork" + :params [{:enabled syncing-on-mobile-network?}] + :on-success #(log/debug "successfully set syncing-on-mobile-network" + syncing-on-mobile-network?) + :on-error #(log/error "could not set syncing-on-mobile-network" %)}]]]}))) + +(rf/reg-event-fx :network/check-expensive-connection + (fn [{:keys [db]}] + (when + (and (:network/expensive? db) + config/mobile-data-syncing-toggle-enabled? + (:profile/profile db) + (not (get-in db [:profile/profile :remember-syncing-choice?]))) + ;; Note here we are only updating :remember-syncing-choice? temporarily to avoid opening + ;; multiple bottom sheets and the user might again see the sheet on the next login. Unless the + ;; user makes a syncing choice that will persist this key in the status-go + {:db (assoc-in db [:profile/profile :remember-syncing-choice?] true) + :fx [[:dispatch + [:show-bottom-sheet + {:content (fn [] [data-confirmation-sheet/view]) + :shell? true + :theme :dark}]]]}))) + +(rf/reg-event-fx + :network/on-state-change + (fn [{:keys [db]} [{:keys [isConnected type details]}]] + (let [old-network-status (:network/status db) + old-network-type (:network/type db) + connectivity-status (if isConnected :online :offline) + status-changed? (not= connectivity-status old-network-status) + type-changed? (not= type old-network-type) + expensive? (:isConnectionExpensive details)] + (log/debug "[net-info]" + "old-network-status" old-network-status + "old-network-type" old-network-type + "connectivity-status" connectivity-status + "type" type + "details" details) + {:db (assoc db :network/expensive? expensive?) + :fx [[:dispatch [:network/check-expensive-connection]] + (when status-changed? + [:dispatch [:network/on-network-status-change connectivity-status]]) + (when type-changed? + [:dispatch [:network/on-network-type-change type expensive?]])]}))) + +(rf/reg-event-fx + :network/on-network-type-change + (fn [{:keys [db]} [network-type expensive?]] + {:db (assoc db :network/type network-type) + :fx [[:effects.network/notify-status-go network-type expensive?]]})) + +(rf/reg-event-fx + :network/on-network-status-change + (fn [{:keys [db]} [connectivity-status]] + {:db (assoc db :network/status connectivity-status) + :fx [(when (ff/enabled? ::ff/wallet.wallet-connect) + [:dispatch [:wallet-connect/reload-on-network-change (= :online connectivity-status)]])]})) diff --git a/src/status_im/contexts/onboarding/common/background/style.cljs b/src/status_im/contexts/onboarding/common/background/style.cljs new file mode 100644 index 00000000000..c2dde181877 --- /dev/null +++ b/src/status_im/contexts/onboarding/common/background/style.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.onboarding.common.background.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def background-container + {:background-color colors/neutral-100 + :flex 1 + :overflow :hidden}) + +(defn background-image-container + [width] + {:flex 1 + :flex-direction :row + :width width}) + +(def background-blur-overlay + {:position :absolute + :left 0 + :top 0 + :bottom 0 + :right 0 + :background-color (when platform/ios? colors/neutral-80-opa-80-blur)}) diff --git a/src/status_im/contexts/onboarding/common/background/view.cljs b/src/status_im/contexts/onboarding/common/background/view.cljs new file mode 100644 index 00000000000..efb6030f8a0 --- /dev/null +++ b/src/status_im/contexts/onboarding/common/background/view.cljs @@ -0,0 +1,74 @@ +(ns status-im.contexts.onboarding.common.background.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [status-im.common.resources :as resources] + [status-im.contexts.onboarding.common.background.style :as style] + [status-im.contexts.onboarding.common.carousel.animation :as carousel.animation] + [status-im.contexts.onboarding.common.carousel.view :as carousel] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def header-text + [{:text (i18n/label :t/own-your-crypto) + :sub-text (i18n/label :t/use-the-multichain-wallet)} + {:text (i18n/label :t/chat-with-friends) + :sub-text (i18n/label :t/with-full-encryption)} + {:text (i18n/label :t/store-your-asets-on-keycard) + :sub-text (i18n/label :t/a-card-shaped-hardware-wallet)}]) + +(defn background-image + [window-width] + [rn/view {:style (style/background-image-container (* window-width 3))} + [rn/image + {:source (resources/get-image :own-buy-and-swap-your-crypto) + :style {:align-self :center :width window-width}}] + [rn/image + {:source (resources/get-image :chat-privately-with-friends) + :style {:align-self :center :width window-width}}] + [rn/image + {:source (resources/get-image :store-your-assets-on-keycard) + :style {:align-self :center :width window-width}}]]) + +(defonce progress (atom nil)) +(defonce paused? (atom nil)) +(defonce is-dragging? (atom nil)) +(defonce drag-amount (atom nil)) + +(defn view + [dark-overlay?] + (let [view-id (rf/sub [:view-id]) + animate? (not dark-overlay?) + window-width (rf/sub [:dimensions/window-width])] + (when animate? + (carousel.animation/use-initialize-animation progress paused? animate? is-dragging? drag-amount)) + + (rn/use-effect + (fn [] + (reanimated/set-shared-value @paused? (not= view-id :screen/onboarding.intro)) + (fn [] + (when (= view-id :screen/onboarding.preparing-status) + (carousel.animation/cleanup-animation progress paused?)))) + [view-id]) + + [rn/view + {:style style/background-container} + [carousel/view + {:animate? animate? + :progress progress + :paused? paused? + :header-text header-text + :is-dragging? is-dragging? + :drag-amount drag-amount + :header-background true + :gesture :swipeable + :background [background-image window-width]}] + (when dark-overlay? + [quo/blur + {:style style/background-blur-overlay + :blur-amount (if platform/android? 30 20) + :blur-radius (if platform/android? 25 10) + :blur-type :transparent + :overlay-color :transparent}])])) diff --git a/src/status_im/contexts/onboarding/common/carousel/animation.cljs b/src/status_im/contexts/onboarding/common/carousel/animation.cljs new file mode 100644 index 00000000000..8ea3fb05a81 --- /dev/null +++ b/src/status_im/contexts/onboarding/common/carousel/animation.cljs @@ -0,0 +1,167 @@ +(ns status-im.contexts.onboarding.common.carousel.animation + (:require + [oops.core :as oops] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [utils.worklets.onboarding-carousel :as worklets.onboarding-carousel])) + +(def ^:const progress-bar-animation-delay 300) +(def ^:const progress-bar-animation-duration 4000) +(def ^:const initial-division-quotient 4) +(def ^:const progress-threshold 25) +(def ^:const drag-limit 75) + +(defn slide-animation + [progress-percentage & [duration]] + (reanimated/with-delay + progress-bar-animation-delay + (reanimated/with-timing + progress-percentage + (js-obj "duration" (or duration progress-bar-animation-duration) + "easing" (:linear reanimated/easings))))) + +(defn calculate-remainder + [dividend divisor] + (if (and (pos-int? dividend) (zero? (mod dividend divisor))) + divisor + (mod dividend divisor))) + +(defn animation-value + [q index] + (let [next-quotient (+ q index) + dividend (if (>= next-quotient initial-division-quotient) + (* (- next-quotient initial-division-quotient) progress-threshold) + (* next-quotient progress-threshold))] + (calculate-remainder dividend drag-limit))) + +(defn animate-progress-value + [value] + (if (zero? value) (slide-animation value 0) (slide-animation value))) + +(defn animate-progress + [progress paused? next-progress] + (let [q (quot next-progress 25)] + (reanimated/set-shared-value + progress + (reanimated/with-pause + (reanimated/with-repeat + (reanimated/with-sequence + (animate-progress-value (animation-value q 1)) + (animate-progress-value (animation-value q 2)) + (animate-progress-value (animation-value q 3)) + (animate-progress-value (calculate-remainder next-progress drag-limit))) + -1) + paused?)))) + +(defn get-next-progress + [progress] + (let [current-progress (reanimated/get-shared-value @progress)] + (if (>= current-progress drag-limit) + drag-limit + (-> (quot current-progress progress-threshold) + inc + (* progress-threshold))))) + +(defn get-previous-progress + [progress] + (let [current-progress (reanimated/get-shared-value @progress)] + (if (< current-progress progress-threshold) + 0 + (-> (quot current-progress progress-threshold) + dec + (* progress-threshold))))) + +(defn use-initialize-animation + [progress paused? animate? is-dragging? drag-amount] + (reset! progress (reanimated/use-shared-value 0)) + (reset! paused? (reanimated/use-shared-value false)) + (reset! is-dragging? (reanimated/use-shared-value false)) + (reset! drag-amount (reanimated/use-shared-value 0)) + (rn/use-effect + (fn [] + (animate-progress @progress @paused? 0)) + [animate?])) + +(defn cleanup-animation + [progress paused?] + (fn [] + (reanimated/cancel-animation @progress) + (reanimated/cancel-animation @paused?))) + +(defn update-progress + [progress paused? new-progress] + (reanimated/set-shared-value @progress new-progress) + (animate-progress @progress @paused? new-progress)) + +(defn handle-drag + [event paused? is-dragging? drag-amount] + (let [translation-x (oops/oget event "translationX")] + (reanimated/set-shared-value @paused? true) + (reanimated/set-shared-value @is-dragging? true) + (reanimated/set-shared-value @drag-amount translation-x))) + +(defn drag-gesture + [progress paused? is-dragging? drag-amount] + (-> (gesture/gesture-pan) + (gesture/max-pointers 1) + (gesture/on-start + (fn [event] + (handle-drag event paused? is-dragging? drag-amount))) + (gesture/on-update + (fn [event] + (handle-drag event paused? is-dragging? drag-amount))) + (gesture/on-end + (fn [] + (reanimated/set-shared-value @is-dragging? false) + (reanimated/set-shared-value @paused? false))) + (gesture/on-finalize + (fn [event] + (let [next? (< (oops/oget event "translationX") (- drag-limit)) + previous? (> (oops/oget event "translationX") drag-limit)] + (when next? + (update-progress progress paused? (get-next-progress progress))) + (when previous? + (update-progress progress paused? (get-previous-progress progress)))))))) + +(defn long-press-gesture + [paused?] + (-> + (gesture/gesture-long-press) + (gesture/enabled true) + (gesture/on-start + (fn [] + (reanimated/set-shared-value @paused? true))) + (gesture/on-finalize + (fn [] + (reanimated/set-shared-value @paused? false))))) + +(defn composed-gestures + [progress paused? is-dragging? drag-amount] + (let [long-press (long-press-gesture paused?) + drag (drag-gesture progress paused? is-dragging? drag-amount)] + (gesture/simultaneous long-press drag))) + +(defn tap-gesture + [progress paused] + (-> (gesture/gesture-tap) + (gesture/on-end #(update-progress progress paused (get-next-progress progress))))) + +(defn carousel-left-position + [window-width animate? progress is-dragging? drag-amount] + (if animate? + (worklets.onboarding-carousel/carousel-left-position window-width + @progress + @is-dragging? + @drag-amount) + (-> (or (reanimated/get-shared-value @progress) 0) + (quot -25) + (* window-width)))) + +(defn dynamic-progress-bar-width + [progress-bar-width animate? progress] + (if animate? + (worklets.onboarding-carousel/dynamic-progress-bar-width progress-bar-width @progress) + (-> (or (reanimated/get-shared-value @progress) 0) + (* progress-bar-width) + (/ 100)))) diff --git a/src/status_im/contexts/onboarding/common/carousel/style.cljs b/src/status_im/contexts/onboarding/common/carousel/style.cljs new file mode 100644 index 00000000000..e4f4439cb34 --- /dev/null +++ b/src/status_im/contexts/onboarding/common/carousel/style.cljs @@ -0,0 +1,67 @@ +(ns status-im.contexts.onboarding.common.carousel.style + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(defn header-container + [status-bar-height content-width index header-background] + {:margin-left (* content-width index) + :padding-top (+ 30 status-bar-height) + :width content-width + :flex-direction :row + :background-color (when header-background colors/onboarding-header-black)}) + +(defn header-text-view + [window-width] + {:flex-direction :column + :width window-width + :padding-horizontal 20}) + +(def carousel-text + {:color colors/white}) + +(def carousel-sub-text + {:color colors/white + :margin-top 2}) + +(defn progress-bar-item + [static? end?] + {:height 2 + :flex 1 + :background-color (if static? colors/white-opa-10 colors/white) + :margin-right (if end? 0 8) + :border-radius 4}) + +(defn progress-bar + [width] + {:position :absolute + :top 0 + :width width + :flex-direction :row}) + +(defn dynamic-progress-bar + [width animate?] + (let [normal-style {:height 2 + :border-radius 4 + :overflow :hidden + :width width}] + (if animate? + (reanimated/apply-animations-to-style + {:width width} + normal-style) + normal-style))) + +(defn progress-bar-container + [progress-bar-width status-bar-height] + {:position :absolute + :width progress-bar-width + :margin-left 20 + :top (+ 12 status-bar-height)}) + +(defn carousel-container + [left animate? width] + (cond->> {:flex-direction :row + :flex 1 + :width width + :margin-left left} + animate? (reanimated/apply-animations-to-style {:margin-left left}))) diff --git a/src/status_im/contexts/onboarding/common/carousel/view.cljs b/src/status_im/contexts/onboarding/common/carousel/view.cljs new file mode 100644 index 00000000000..e93234f0b0b --- /dev/null +++ b/src/status_im/contexts/onboarding/common/carousel/view.cljs @@ -0,0 +1,89 @@ +(ns status-im.contexts.onboarding.common.carousel.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.navigation :as navigation] + [react-native.reanimated :as reanimated] + [status-im.contexts.onboarding.common.carousel.animation :as animation] + [status-im.contexts.onboarding.common.carousel.style :as style] + [utils.re-frame :as rf])) + +(defn header-text-view + [index window-width header-text] + [rn/view {:style (style/header-text-view window-width)} + [quo/text + {:style style/carousel-text + :weight :semi-bold + :size :heading-2} + (get-in header-text [index :text])] + [quo/text + {:style style/carousel-sub-text} + (get-in header-text [index :sub-text])]]) + +(defn content-view + [{:keys [window-width status-bar-height index header-text header-background]} content] + (let [content-width (* 3 window-width)] + [rn/view {:style {:flex 1}} + [rn/view {:style (style/header-container status-bar-height content-width index header-background)} + (for [index (range 3)] + ^{:key index} + [header-text-view index window-width header-text])] + (when content content)])) + +(defn progress-bar + [{:keys [static? progress-bar-width]}] + [rn/view + {:style (style/progress-bar progress-bar-width)} + [rn/view {:style (style/progress-bar-item static? false)}] + [rn/view {:style (style/progress-bar-item static? false)}] + [rn/view {:style (style/progress-bar-item static? true)}]]) + +(defn f-dynamic-progress-bar + [{:keys [progress-bar-width animate? progress]}] + (let [width (animation/dynamic-progress-bar-width progress-bar-width animate? progress) + container-view (if animate? reanimated/view rn/view)] + [container-view {:style (style/dynamic-progress-bar width animate?)} + [progress-bar + {:static? false + :progress-bar-width progress-bar-width}]])) + +(defn f-view + [{:keys [animate? progress paused? header-text background header-background gesture is-dragging? + drag-amount]}] + (let [window-width (rf/sub [:dimensions/window-width]) + status-bar-height (:status-bar-height @navigation/constants) + progress-bar-width (- window-width 40) + carousel-left + (animation/carousel-left-position window-width animate? progress is-dragging? drag-amount) + container-view (if animate? reanimated/view rn/view) + identified-gesture + (case gesture + :swipeable (animation/composed-gestures progress paused? is-dragging? drag-amount) + :tappable (animation/tap-gesture progress paused?) + nil)] + [rn/view {:style {:flex 1}} + [gesture/gesture-detector {:gesture identified-gesture} + [container-view {:style (style/carousel-container carousel-left animate? (* 3 window-width))} + (for [index (range 1)] + ^{:key index} + [content-view + {:window-width window-width + :status-bar-height status-bar-height + :index index + :header-text header-text + :header-background header-background} + background])]] + [rn/view + {:style (style/progress-bar-container + progress-bar-width + status-bar-height)} + [progress-bar + {:static? true + :progress-bar-width progress-bar-width}] + [:f> f-dynamic-progress-bar + {:progress-bar-width progress-bar-width + :animate? animate? + :progress progress}]]])) + +(defn view [props] [:f> f-view props]) diff --git a/src/status_im/contexts/onboarding/common/overlay/events.cljs b/src/status_im/contexts/onboarding/common/overlay/events.cljs new file mode 100644 index 00000000000..42f07ade1df --- /dev/null +++ b/src/status_im/contexts/onboarding/common/overlay/events.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.onboarding.common.overlay.events + (:require + [re-frame.core :as re-frame] + [status-im.contexts.onboarding.common.overlay.view :as overlay] + [status-im.contexts.profile.profiles.view :as profiles] + [utils.re-frame :as rf])) + +(re-frame/reg-fx + :onboarding/overlay-dismiss-fx + (fn [] + (when-let [blur-dismiss-fn @overlay/blur-dismiss-fn-atom] + (blur-dismiss-fn)) + (when-let [pop-animation-fn @profiles/pop-animation-fn-atom] + (pop-animation-fn)))) + +(rf/defn overlay-dismiss + {:events [:onboarding/overlay-dismiss]} + [_] + {:onboarding/overlay-dismiss-fx nil}) diff --git a/src/status_im/contexts/onboarding/common/overlay/style.cljs b/src/status_im/contexts/onboarding/common/overlay/style.cljs new file mode 100644 index 00000000000..3bfdfdd43db --- /dev/null +++ b/src/status_im/contexts/onboarding/common/overlay/style.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.onboarding.common.overlay.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated])) + +(defn blur-container + [opacity] + (reanimated/apply-animations-to-style + {:opacity opacity} + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0})) + +(def blur-style + {:flex 1 + :background-color (when platform/ios? colors/neutral-80-opa-80-blur)}) diff --git a/src/status_im/contexts/onboarding/common/overlay/view.cljs b/src/status_im/contexts/onboarding/common/overlay/view.cljs new file mode 100644 index 00000000000..f3f5e246399 --- /dev/null +++ b/src/status_im/contexts/onboarding/common/overlay/view.cljs @@ -0,0 +1,77 @@ +(ns status-im.contexts.onboarding.common.overlay.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.onboarding.common.overlay.style :as style])) + +(def max-blur-amount 30) + +(defonce timer-interval (atom nil)) +(defonce blur-dismiss-fn-atom (atom nil)) +(defonce blur-show-fn-atom (atom nil)) + +(defn blur-show + [opacity blur-amount] + (reanimated/animate opacity + 1 + (/ constants/onboarding-modal-animation-duration 2)) + (js/clearInterval @timer-interval) + (reset! timer-interval + (js/setInterval + (fn [] + (if (< @blur-amount max-blur-amount) + (swap! blur-amount + 1) + (js/clearInterval @timer-interval))) + (/ constants/onboarding-modal-animation-duration + max-blur-amount + 2)))) + +(defn blur-dismiss + [opacity blur-amount] + (reanimated/animate-delay opacity + 0 + (/ constants/onboarding-modal-animation-duration 2) + (/ constants/onboarding-modal-animation-duration 2)) + (js/clearInterval @timer-interval) + (reset! timer-interval + (js/setInterval + (fn [] + (if (> @blur-amount 0) + (swap! blur-amount dec) + (js/clearInterval @timer-interval))) + (/ constants/onboarding-modal-animation-duration + max-blur-amount + 2)))) + +;; we had to register it here, because of hotreload, overwise on hotreload it will be reseted +(defonce blur-amount (reagent/atom 0)) + +(defn f-view + [] + (let [opacity (reanimated/use-shared-value (if (zero? @blur-amount) 0 1)) + blur-show-fn #(blur-show opacity blur-amount) + blur-dismiss-fn #(blur-dismiss opacity blur-amount)] + (rn/use-mount + (fn [] + (reset! blur-show-fn-atom blur-show-fn) + (reset! blur-dismiss-fn-atom blur-dismiss-fn) + (fn [] + (reset! blur-show-fn-atom nil) + (reset! blur-dismiss-fn-atom nil)))) + [reanimated/view + {:pointer-events :none + :style (style/blur-container opacity)} + [quo/blur + {:blur-amount @blur-amount + :blur-radius (if platform/android? 25 10) + :blur-type :transparent + :overlay-color :transparent + :style style/blur-style}]])) + +(defn view + [] + [:f> f-view]) diff --git a/src/status_im/contexts/onboarding/create_password/style.cljs b/src/status_im/contexts/onboarding/create_password/style.cljs new file mode 100644 index 00000000000..7efe438b976 --- /dev/null +++ b/src/status_im/contexts/onboarding/create_password/style.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.onboarding.create-password.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def heading {:margin-bottom 20}) +(def heading-subtitle {:color colors/white}) +(def heading-title (assoc heading-subtitle :margin-bottom 8)) + +(def space-between-inputs {:height 16}) + +(def password-tips + {:flex-direction :row + :justify-content :space-between + :padding-horizontal 20}) + +(def form-container + {:justify-content :space-between + :padding-top 12 + :padding-horizontal 20}) + +(def footer-container {:padding-bottom 12}) +(def footer-button-container {:margin-top 20 :padding-horizontal 20}) + +(def blur-options + {:blur-amount 34 + :blur-radius 20 + :blur-type :transparent + :overlay-color :transparent + :background-color (if platform/android? + colors/neutral-100 + colors/neutral-80-opa-1-blur) + :padding-vertical 0 + :padding-horizontal 0}) + +(def page-nav-height 56) diff --git a/src/status_im/contexts/onboarding/create_password/view.cljs b/src/status_im/contexts/onboarding/create_password/view.cljs new file mode 100644 index 00000000000..0bd7f79a03a --- /dev/null +++ b/src/status_im/contexts/onboarding/create_password/view.cljs @@ -0,0 +1,183 @@ +(ns status-im.contexts.onboarding.create-password.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button] + [status-im.common.password-with-hint.view :as password-with-hint] + [status-im.common.validation.password :as password] + [status-im.constants :as constants] + [status-im.contexts.onboarding.create-password.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn header + [] + [rn/view {:style style/heading} + [quo/text + {:style style/heading-title + :weight :semi-bold + :size :heading-1} + (i18n/label :t/password-creation-title)] + [quo/text + {:style style/heading-subtitle + :weight :regular + :size :paragraph-1} + (i18n/label :t/password-creation-subtitle)]]) + +(defn password-inputs + [{:keys [set-password set-repeat-password same-passwords? password repeat-password + password-long-enough? password-short-enough?]}] + (let [hint-1-status (if password-long-enough? :success :default) + hint-2-status (if same-passwords? :success :error) + hint-2-text (if same-passwords? + (i18n/label :t/password-creation-match) + (i18n/label :t/password-creation-dont-match)) + [error? set-error] (rn/use-state false)] + (rn/use-effect + (fn [] + (set-error (and (>= (count repeat-password) (count password)) + password-long-enough? + (not same-passwords?))) + [password repeat-password same-passwords?])) + [:<> + [password-with-hint/view + {:hint (if (not password-short-enough?) + {:text (i18n/label :t/password-creation-max-length-hint) + :status :error + :shown? true} + {:text (i18n/label :t/password-creation-hint) + :status hint-1-status + :shown? true}) + :placeholder (i18n/label :t/password-creation-placeholder-1) + :on-change-text set-password + :auto-focus true}] + [rn/view {:style style/space-between-inputs}] + [password-with-hint/view + {:hint {:text hint-2-text + :status hint-2-status + :shown? (or same-passwords? error?)} + :error? error? + :placeholder (i18n/label :t/password-creation-placeholder-2) + :on-change-text set-repeat-password}]])) + +(defn help + [{:keys [lower-case? upper-case? numbers? symbols?] :as validations}] + (let [password-strength (constants/strength-status (password/strength validations) :info)] + [rn/view + [quo/strength-divider {:type password-strength} + (i18n/label :t/password-creation-tips-title)] + [rn/view {:style style/password-tips} + [quo/tips {:completed? lower-case?} + (i18n/label :t/password-creation-tips-1)] + [quo/tips {:completed? upper-case?} + (i18n/label :t/password-creation-tips-2)] + [quo/tips {:completed? numbers?} + (i18n/label :t/password-creation-tips-3)] + [quo/tips {:completed? symbols?} + (i18n/label :t/password-creation-tips-4)]]])) + +(defn- use-password-checks + [password] + (rn/use-memo + (fn [] + (-> password + (password/validate) + (assoc :non-empty? (seq password)))) + [password])) + +(defn- use-repeat-password-checks + [password repeat-password] + (rn/use-memo + (fn [] + {:same-password-length? (and (seq password) + (= (count password) (count repeat-password))) + :same-passwords? (and (seq password) + (= password repeat-password))}) + [password repeat-password])) + +(defn create-password-doc + [] + [quo/documentation-drawers + {:title (i18n/label :t/create-profile-password-info-box-title) + :shell? true} + [rn/view + [quo/text {:size :paragraph-2} + (i18n/label :t/create-profile-password-info-box-description)]]]) + +(defn- on-press-info + [] + (rn/dismiss-keyboard!) + (rf/dispatch [:show-bottom-sheet + {:content create-password-doc + :shell? true}])) + +(defn- navigate-back [] (rf/dispatch [:navigate-back])) + +(defn- page-nav + [top] + [quo/page-nav + {:margin-top top + :background :blur + :icon-name :i/arrow-left + :on-press navigate-back + :right-side [{:icon-name :i/info + :on-press on-press-info}]}]) + +(defn- help-and-confirm-button + [{:keys [password-validations same-passwords? on-submit]}] + (let [{customization-color :color} (rf/sub [:onboarding/profile]) + all-requirements-met? (and (:non-empty? password-validations) + (:long-enough? password-validations) + (:short-enough? password-validations) + same-passwords?)] + [rn/view {:style style/footer-container} + [help password-validations] + [quo/button + {:container-style style/footer-button-container + :disabled? (not all-requirements-met?) + :customization-color customization-color + :on-press on-submit} + (i18n/label :t/password-creation-confirm)]])) + +(defn- on-confirm-password + [password] + (rf/dispatch [:onboarding/password-set (security/mask-data password)])) + +(defn create-password + [] + (let [[password set-password] (rn/use-state "") + [repeat-password + set-repeat-password] (rn/use-state "") + {:keys [long-enough? short-enough? non-empty?] + :as password-validations} (use-password-checks password) + {:keys [same-password-length? + same-passwords?]} (use-repeat-password-checks password repeat-password) + on-submit (rn/use-callback + #(on-confirm-password password) + [password])] + [floating-button/view + {:header [page-nav safe-area/top] + :initial-header-height (+ style/page-nav-height safe-area/top) + :keyboard-should-persist-taps :handled + :content-avoid-keyboard? true + :automatically-adjust-keyboard-insets true + :blur-options style/blur-options + :footer-container-padding 0 + :footer [help-and-confirm-button + {:password-validations password-validations + :same-passwords? same-passwords? + :on-submit on-submit}]} + [rn/view {:style style/form-container} + [header] + [password-inputs + {:password-long-enough? long-enough? + :password-short-enough? short-enough? + :non-empty-password? non-empty? + :password password + :repeat-password repeat-password + :same-passwords? same-passwords? + :same-password-length? same-password-length? + :set-password set-password + :set-repeat-password set-repeat-password}]]])) diff --git a/src/status_im/contexts/onboarding/create_profile/style.cljs b/src/status_im/contexts/onboarding/create_profile/style.cljs new file mode 100644 index 00000000000..599479ae018 --- /dev/null +++ b/src/status_im/contexts/onboarding/create_profile/style.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.onboarding.create-profile.style + (:require + [quo.foundations.colors :as colors])) + +(def content-container + {:flex 1}) + +(def options-container + {:padding-top 12 + :padding-horizontal 20}) + +(def title + {:color colors/white + :margin-bottom 20}) + +(def subtitle + {:margin-bottom 12 + :color colors/white-opa-70}) + +(def subtitle-container + {:margin-top 24}) + +(def space-between-suboptions {:height 12}) diff --git a/src/status_im/contexts/onboarding/create_profile/view.cljs b/src/status_im/contexts/onboarding/create_profile/view.cljs new file mode 100644 index 00000000000..731c05a9213 --- /dev/null +++ b/src/status_im/contexts/onboarding/create_profile/view.cljs @@ -0,0 +1,99 @@ +(ns status-im.contexts.onboarding.create-profile.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.config :as config] + [status-im.contexts.onboarding.create-profile.style :as style] + [status-im.contexts.onboarding.getting-started-doc.view :as getting-started-doc] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- navigate-to-create-profile-password + [] + (debounce/throttle-and-dispatch + [:onboarding/navigate-to-create-profile-password] + 1000)) + +(defn- navigate-to-sign-in-by-recovery-phrase + [] + (rf/dispatch [:syncing/clear-syncing-fallback-flow]) + (rf/dispatch [:onboarding/navigate-to-sign-in-by-seed-phrase :screen/onboarding.create-profile])) + +(defn- option-card-max-height + [window-height] + (- window-height + (* 2 56) ;; two other list items + (* 2 16) ;; spacing between items + 220)) ;; extra spacing (top bar) + +(defn- start-fresh-main-card + [window-height] + [quo/small-option-card + {:variant :main + :title (i18n/label :t/start-fresh) + :subtitle (i18n/label :t/start-fresh-subtitle) + :button-label (i18n/label :t/lets-go) + :accessibility-label :start-fresh-main-card + :image (resources/get-image :generate-keys) + :max-height (option-card-max-height window-height) + :on-press navigate-to-create-profile-password}]) + +(defn- use-recovery-phrase-icon-card + [] + [quo/small-option-card + {:variant :icon + :title (i18n/label :t/use-a-recovery-phrase) + :subtitle (i18n/label :t/use-a-recovery-phrase-subtitle) + :accessibility-label :use-a-recovery-phrase-icon-card + :image (resources/get-image :ethereum-address) + :on-press navigate-to-sign-in-by-recovery-phrase}]) + +(defn- use-empty-keycard-icon-card + [] + [quo/small-option-card + {:variant :icon + :title (i18n/label :t/use-an-empty-keycard) + :subtitle (i18n/label :t/use-an-empty-keycard-subtitle) + :accessibility-label :use-an-empty-keycard-icon-card + :image (resources/get-image :use-keycard) + :on-press #(rf/dispatch [:open-modal :screen/keycard.create-profile])}]) + +(defn- navigate-to-quo-preview + [] + (rf/dispatch [:navigate-to :screen/quo-preview])) + +(defn view + [] + (let [window-height (rf/sub [:dimensions/window-height])] + [rn/view {:style style/content-container} + [quo/page-nav + {:margin-top safe-area/top + :type :no-title + :background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back + :right-side [{:icon-name :i/info + :on-press getting-started-doc/show-as-bottom-sheet} + (when config/debug-or-pr-build? + {:icon-name :i/reveal-whitelist + :on-press navigate-to-quo-preview})]}] + [rn/view {:style style/options-container} + [quo/text + {:style style/title + :size :heading-1 + :weight :semi-bold} + (i18n/label :t/create-profile)] + [start-fresh-main-card window-height] + [rn/view {:style style/subtitle-container} + [quo/text + {:style style/subtitle + :size :paragraph-2 + :weight :medium} + (i18n/label :t/other-options)]] + [use-recovery-phrase-icon-card] + [rn/view {:style style/space-between-suboptions}] + [use-empty-keycard-icon-card]]])) diff --git a/src/status_im/contexts/onboarding/enable_biometrics/style.cljs b/src/status_im/contexts/onboarding/enable_biometrics/style.cljs new file mode 100644 index 00000000000..ef8124affa8 --- /dev/null +++ b/src/status_im/contexts/onboarding/enable_biometrics/style.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.onboarding.enable-biometrics.style) + +(def default-margin 20) + +(def title-container + {:margin-horizontal 20 + :padding-bottom 20 + :padding-top 12}) + +(defn page-container + [insets] + {:flex 1 + :padding-top (+ (:top insets) 56)}) + +(defn page-illustration + [width] + {:flex 1 + :margin-top 12 + :margin-bottom 10 + :width width}) + +(defn buttons + [insets] + {:margin default-margin + :margin-bottom (+ 12 (:bottom insets))}) diff --git a/src/status_im/contexts/onboarding/enable_biometrics/view.cljs b/src/status_im/contexts/onboarding/enable_biometrics/view.cljs new file mode 100644 index 00000000000..a27943b7c87 --- /dev/null +++ b/src/status_im/contexts/onboarding/enable_biometrics/view.cljs @@ -0,0 +1,64 @@ +(ns status-im.contexts.onboarding.enable-biometrics.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.biometric.utils :as biometric] + [status-im.common.resources :as resources] + [status-im.contexts.onboarding.enable-biometrics.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn page-title + [] + [quo/text-combinations + {:container-style style/title-container + :title (i18n/label :t/enable-biometrics) + :title-accessibility-label :enable-biometrics-title + :description (i18n/label :t/use-biometrics) + :description-accessibility-label :enable-biometrics-sub-title}]) + +(defn enable-biometrics-buttons + [insets] + (let [supported-biometric-type (rf/sub [:biometrics/supported-type]) + bio-type-label (biometric/get-label-by-type supported-biometric-type) + onboarding-profile (rf/sub [:onboarding/profile]) + profile-color (or (:color onboarding-profile) + (rf/sub [:profile/customization-color])) + syncing? (:syncing? onboarding-profile) + biometric-type (rf/sub [:biometrics/supported-type])] + [rn/view {:style (style/buttons insets)} + [quo/button + {:size 40 + :accessibility-label :enable-biometrics-button + :icon-left (biometric/get-icon-by-type biometric-type) + :customization-color profile-color + :on-press #(rf/dispatch [:onboarding/biometrics-setup-start + {:enable-biometrics? true + :syncing? syncing?}])} + (i18n/label :t/biometric-enable-button {:bio-type-label bio-type-label})] + [quo/button + {:accessibility-label :maybe-later-button + :background :blur + :type :grey + :on-press #(rf/dispatch [:onboarding/biometrics-setup-start + {:enable-biometrics? false + :syncing? syncing?}]) + :container-style {:margin-top 12}} + (i18n/label :t/maybe-later)]])) + +(defn enable-biometrics-simple + [] + (let [width (:width (rn/get-window))] + [rn/image + {:resize-mode :contain + :style (style/page-illustration width) + :source (resources/get-image :biometrics)}])) + +(defn view + [] + (let [insets safe-area/insets] + [rn/view {:style (style/page-container insets)} + [page-title] + [enable-biometrics-simple] + [enable-biometrics-buttons insets]])) diff --git a/src/status_im/contexts/onboarding/enable_notifications/style.cljs b/src/status_im/contexts/onboarding/enable_notifications/style.cljs new file mode 100644 index 00000000000..d8a3e13c030 --- /dev/null +++ b/src/status_im/contexts/onboarding/enable_notifications/style.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.onboarding.enable-notifications.style) + +(def default-margin 20) + +(defn page-container + [insets] + {:flex 1 + :justify-content :space-between + :padding-top (:top insets)}) + +(defn page-illustration + [width] + {:flex 1 + :width width}) + +(def page-title + {:margin-top 12 + :margin-horizontal 20 + :margin-bottom 8}) + +(def page-heading {:z-index 1}) + +(defn buttons + [insets] + {:margin default-margin + :margin-bottom (+ 14 (:bottom insets))}) + +(def news-notifications-checkbox-container + {:flex-direction :row + :gap 8 + :padding-top 8 + :padding-bottom 12 + :padding-horizontal 20}) + +(def news-notifications-checkbox-text + {:flex 1}) diff --git a/src/status_im/contexts/onboarding/enable_notifications/view.cljs b/src/status_im/contexts/onboarding/enable_notifications/view.cljs new file mode 100644 index 00000000000..c8be269397b --- /dev/null +++ b/src/status_im/contexts/onboarding/enable_notifications/view.cljs @@ -0,0 +1,131 @@ +(ns status-im.contexts.onboarding.enable-notifications.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.common.resources :as resources] + [status-im.contexts.onboarding.common.background.view :as background] + [status-im.contexts.onboarding.enable-notifications.style :as style] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn page-title + [] + [quo/text-combinations + {:container-style style/page-title + :title (i18n/label :t/intro-wizard-title6) + :title-accessibility-label :notifications-title + :description (i18n/label :t/enable-notifications-sub-title) + :description-accessibility-label :notifications-sub-title}]) + +(defn on-notifications-setup-start + [params] + (rf/dispatch [:onboarding/notifications-setup-start params])) + +(defn notifications-info-view + [{:keys [blur?]}] + [quo/documentation-drawers + {:title (i18n/label :t/enable-notifications) + :show-button? true + :shell? blur? + :button-label (i18n/label :t/read-more) + :button-icon :i/info} + [quo/text (i18n/label :t/enable-notifications-info-description)]]) + +(defn on-open-info + [{:keys [blur? theme] + :or {blur? true}}] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] + [notifications-info-view {:blur? blur?}]) + :theme theme + :shell? blur?}])) + +(defn enable-notification-form + [{:keys [insets params]}] + (let [profile-color (rf/sub [:onboarding/customization-color + {:onboarding? (:onboarding? params)}]) + [third-party-checked? + set-third-party-checked] (rn/use-state + (boolean? (ff/enabled? ::ff/settings.news-notifications))) + on-enable-notifications (rn/use-callback + (fn [] + (on-notifications-setup-start + (assoc params + :enable-notifications? true + :enable-news-notifications? third-party-checked?))) + [params third-party-checked?]) + on-skip-notifications (rn/use-callback + (fn [] + (on-notifications-setup-start + (assoc params + :enable-notifications? false + :enable-news-notifications? false))) + [params])] + [rn/view + (when (and platform/android? + (ff/enabled? ::ff/settings.news-notifications)) + [rn/view + {:style style/news-notifications-checkbox-container} + [quo/selectors + {:type :checkbox + :blur? true + :customization-color profile-color + :checked? third-party-checked? + :on-change set-third-party-checked}] + [quo/text + {:size :paragraph-2 + :style style/news-notifications-checkbox-text} + (i18n/label :t/enable-news-notifications-third-party)]]) + [rn/view {:style (style/buttons insets)} + [quo/button + {:on-press on-enable-notifications + :type :primary + :icon-left :i/notifications + :accessibility-label :enable-notifications-button + :customization-color profile-color} + (i18n/label :t/intro-wizard-title6)] + [quo/button + {:on-press on-skip-notifications + :accessibility-label :enable-notifications-later-button + :type :grey + :background :blur + :container-style {:margin-top 12}} + (i18n/label :t/maybe-later)]]])) + +(defn enable-notifications-illustration + [] + (let [width (:width (rn/get-window))] + [rn/image + {:resize-mode :contain + :style (style/page-illustration width) + :source (resources/get-image :notifications)}])) + +(defn background-image + [] + [rn/view {:style rn/stylesheet-absolute-fill} + [background/view true]]) + +(defn view + [] + (let [insets safe-area/insets + params (quo.context/use-screen-params)] + [:<> + (when-not (:onboarding? params) + [background-image]) + [rn/view {:style (style/page-container insets)} + [rn/view {:style style/page-heading} + [quo/page-nav + {:type :no-title + :background :blur + :right-side [{:icon-name :i/info + :on-press on-open-info + :accessibility-label :notifications-info-button}]}] + [page-title]] + [enable-notifications-illustration] + [enable-notification-form + {:insets insets + :params params}]]])) diff --git a/src/status_im/contexts/onboarding/events.cljs b/src/status_im/contexts/onboarding/events.cljs new file mode 100644 index 00000000000..5450b18cc90 --- /dev/null +++ b/src/status_im/contexts/onboarding/events.cljs @@ -0,0 +1,335 @@ +(ns status-im.contexts.onboarding.events + (:require + [quo.foundations.colors :as colors] + status-im.common.biometric.events + [status-im.constants :as constants] + [status-im.contexts.onboarding.interceptors :as onboarding.interceptors] + [status-im.contexts.shell.constants :as shell.constants] + [status-im.feature-flags :as ff] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn notifications-setup-start + [{:keys [db]} + [{:keys [enable-notifications? + enable-news-notifications? + biometrics? + onboarding? + syncing?]}]] + {:db (if-not onboarding? + db + (update-in db + [:onboarding/profile] + assoc + :enable-notifications? enable-notifications? + :enable-news-notifications? enable-news-notifications?)) + :fx (cond-> [] + (and (not onboarding?) enable-notifications? enable-news-notifications?) + (conj [:dispatch [:notifications/news-notifications-switch true]]) + + (and (not onboarding?) enable-notifications?) + (conj [:dispatch [:push-notifications/switch true]]) + + :always + (conj [:dispatch + [:onboarding/notifications-setup-done + {:onboarding? onboarding? + :syncing? syncing? + :biometrics? biometrics?}]]))}) + +(rf/reg-event-fx :onboarding/notifications-setup-start notifications-setup-start) + +(defn notifications-setup-done + [{:keys [_db]} [{:keys [biometrics? onboarding? syncing?]}]] + {:fx (cond-> [] + (and onboarding? syncing?) + (conj [:dispatch [:onboarding/finalize-setup]]) + + (and onboarding? syncing? (not biometrics?)) + (conj [:dispatch [:onboarding/finish-onboarding]]) + + (and onboarding? (not syncing?)) + (conj [:dispatch [:onboarding/create-account-and-login]]) + + (and (not onboarding?) (not syncing?)) + (conj [:dispatch [:shell/show-root-view]]))}) + + +(rf/reg-event-fx :onboarding/notifications-setup-done notifications-setup-done) + +(defn notifications-setup + [{:keys [db]} + [{:keys [biometrics-supported? biometrics? syncing? onboarding?]}]] + (let [db-view-id (get-in db [:view-id]) + current-view-id (if (= db-view-id :screen/onboarding.syncing-biometric) + :screen/onboarding.enable-biometrics + db-view-id)] + {:db (assoc-in db [:onboarding/profile :notifications-prompted?] true) + :fx [[:dispatch + (if (ff/enabled? ::ff/settings.news-notifications) + [:navigate-to-within-stack + [:screen/onboarding.enable-notifications current-view-id] + {:syncing? syncing? + :onboarding? onboarding? + :biometrics? (and biometrics-supported? biometrics?)}] + [:onboarding/notifications-setup-done + {:onboarding? onboarding? + :syncing? syncing? + :biometrics? (and biometrics-supported? biometrics?)}])]]})) + +(rf/reg-event-fx :onboarding/notifications-setup notifications-setup) + +(defn biometrics-setup-start + [_ [{:keys [enable-biometrics? syncing?]}]] + {:fx (cond-> [] + enable-biometrics? + (conj [:dispatch + [:onboarding/enable-biometrics + {:on-done [:onboarding/biometrics-setup-done + {:on-success [:onboarding/notifications-setup + {:biometrics? true + :biometrics-supported? true + :onboarding? true + :syncing? syncing?}] + :on-fail [:onboarding/biometrics-fail]}]}]]) + + (not enable-biometrics?) + (conj [:dispatch + [:onboarding/notifications-setup + {:biometrics? false + :biometrics-supported? true + :onboarding? true + :syncing? syncing?}]]))}) + +(rf/reg-event-fx :onboarding/biometrics-setup-start biometrics-setup-start) + +(defn biometrics-setup-done + [{:keys [db]} [{:keys [on-success on-fail]} {:keys [error]}]] + {:db (assoc-in db [:onboarding/profile :auth-method] constants/auth-method-biometric) + :fx [(if (some? error) + [:dispatch (conj on-fail error)] + [:dispatch on-success])]}) + +(rf/reg-event-fx :onboarding/biometrics-setup-done biometrics-setup-done) + +(rf/reg-event-fx + :onboarding/biometrics-fail + (fn [_ [error]] + {:dispatch [:biometric/show-message (ex-cause error)]})) + +(rf/reg-event-fx :onboarding/enable-biometrics + (fn [_ [{:keys [on-done]}]] + {:fx [[:dispatch + [:biometric/authenticate + {:on-success #(rf/dispatch on-done) + :on-fail #(rf/dispatch (conj on-done %))}]]]})) + +(rf/reg-event-fx :shell/show-root-view + [onboarding.interceptors/local-profile-storage-interceptor] + (fn [{:keys [db local-profile-storage]} [{:keys [notifications-prompt-skip?]}]] + (let [{:keys [key-uid]} (get-in db [:profile/profile]) + onboarding-profile (get-in db [:onboarding/profile])] + (if (ff/enabled? ::ff/settings.news-notifications) + (if (or (:notifications-prompted? local-profile-storage) + (:notifications-prompted? onboarding-profile) + notifications-prompt-skip?) + {:fx (cond-> [] + (not (:notifications-prompted? local-profile-storage)) + (conj [:dispatch + [:profile/save-notifications-prompted + {:key-uid key-uid}]]) + :else + (conj [:dispatch [:update-theme-and-init-root :screen/shell-stack]] + [:dispatch [:profile/toggle-testnet-mode-banner]]))} + {:fx [[:dispatch + [:profile/save-notifications-prompted + {:key-uid key-uid}]] + [:dispatch + [:onboarding/notifications-setup + {:biometrics? false + :syncing? false + :onboarding? false}]]]}) + {:fx [[:dispatch [:update-theme-and-init-root :screen/shell-stack]] + [:dispatch [:profile/toggle-testnet-mode-banner]]]})))) + +(rf/reg-event-fx + :onboarding/finish-onboarding + (fn [_ []] + {:fx [[:dispatch [:shell/change-tab shell.constants/default-selected-stack]] + [:dispatch [:shell/show-root-view]] + [:dispatch [:universal-links/process-stored-event]]]})) + +(rf/reg-event-fx :onboarding/navigate-to-sign-in-by-seed-phrase + (fn [{:keys [db]} [from-screen]] + {:db (assoc db :onboarding/navigated-to-enter-seed-phrase-from-screen from-screen) + :fx [[:dispatch + [:navigate-to-within-stack [:screen/onboarding.enter-seed-phrase from-screen] + {:on-success #(rf/dispatch [:onboarding/seed-phrase-validated %]) + :onboarding-flow? true}]]]})) + +(rf/reg-event-fx + :onboarding/clear-navigated-to-enter-seed-phrase-from-screen + (fn [{:keys [db]}] + {:db (dissoc db :onboarding/navigated-to-enter-seed-phrase-from-screen)})) + +(rf/reg-event-fx :onboarding/navigate-to-enable-notifications + (fn [{:keys [db]}] + {:dispatch [:navigate-to-within-stack + [:screen/onboarding.enable-notifications + (get db + :onboarding/navigated-to-enter-seed-phrase-from-screen + :screen/onboarding.create-profile)]]})) + +(rf/reg-event-fx :onboarding/create-account-and-login + (fn [{:keys [db]}] + (let [{:keys [seed-phrase] + :as profile} (:onboarding/profile db) + syncing-account-recovered? (and (seq (:syncing/key-uid db)) + (= (:syncing/key-uid db) + (get-in db [:onboarding/profile :key-uid])))] + {:db (-> db + (dissoc :profile/login) + (dissoc :auth-method) + (assoc :onboarding/new-account? true)) + :fx [[:dispatch + [:navigate-to-within-stack + [:screen/onboarding.preparing-status + (get db + :onboarding/navigated-to-enter-seed-phrase-from-screen + :screen/onboarding.create-profile)]]] + (when-not syncing-account-recovered? + [:dispatch [:syncing/clear-syncing-installation-id]]) + (if seed-phrase + [:dispatch [:profile.recover/recover-and-login profile]] + [:dispatch [:profile.create/create-and-login profile]])]}))) + +(rf/defn on-delete-profile-success + {:events [:onboarding/on-delete-profile-success]} + [{:keys [db]} key-uid] + (let [multiaccounts (dissoc (:profile/profiles-overview db) key-uid)] + {:db (assoc db :profile/profiles-overview multiaccounts) + :fx (cond-> [] + (ff/enabled? ::ff/settings.news-notifications) + (conj [:dispatch + [:profile/remove-local-profile-storage + {:key-uid key-uid}]]) + (not (seq multiaccounts)) + (conj [:dispatch + [:update-theme-and-init-root :screen/onboarding.intro]]))})) + +(rf/reg-event-fx + :onboarding/password-set + (fn [{:keys [db]} [masked-password]] + (let [biometric-supported-type (get-in db [:biometrics :supported-type]) + syncing? (get-in db [:onboarding/profile :syncing?]) + from-screen (get db + :onboarding/navigated-to-enter-seed-phrase-from-screen + :screen/onboarding.create-profile)] + {:db (-> db + (assoc-in [:onboarding/profile :password] masked-password) + (assoc-in [:onboarding/profile :auth-method] constants/auth-method-password)) + :fx [[:dispatch + (cond + biometric-supported-type + [:navigate-to-within-stack [:screen/onboarding.enable-biometrics from-screen]] + + (ff/enabled? ::ff/settings.news-notifications) + [:onboarding/notifications-setup + {:onboarding? true + :biometrics? false + :syncing? syncing?}] + + :else + [:onboarding/create-account-and-login])]]}))) + +(rf/reg-event-fx + :onboarding/seed-phrase-validated + (fn [{:keys [db]} [{:keys [seed-phrase key-uid]}]] + (let [next-screen :screen/onboarding.create-profile-password + from-screen (get db + :onboarding/navigated-to-enter-seed-phrase-from-screen + :screen/onboarding.create-profile)] + (if (contains? (:profile/profiles-overview db) key-uid) + {:fx [[:dispatch [:enter-seed-phrase/set-error (i18n/label :t/account-already-exist-error)]]]} + {:db (-> db + (assoc-in [:onboarding/profile :seed-phrase] seed-phrase) + (assoc-in [:onboarding/profile :key-uid] key-uid) + (assoc-in [:onboarding/profile :color] constants/profile-default-color)) + :fx [[:dispatch [:navigate-to-within-stack [next-screen from-screen]]]]})))) + +(rf/reg-event-fx + :onboarding/navigate-to-create-profile-password + (fn [{:keys [db]}] + {:db (-> db + (assoc-in [:onboarding/profile :color] (rand-nth colors/account-colors)) + (update :onboarding/profile dissoc :image-path)) + :fx [[:dispatch + [:navigate-to-within-stack + [:screen/onboarding.create-profile-password :screen/onboarding.create-profile]]]]})) + +(rf/reg-event-fx :onboarding/navigate-to-sign-in-by-syncing + (fn [{:keys [db]}] + ;; Restart the flow + {:db (dissoc db :onboarding/profile) + :dispatch [:navigate-to-within-stack + [:screen/onboarding.sign-in-intro :screen/onboarding.log-in]]})) + +(rf/reg-event-fx :onboarding/set-auth-method + (fn [{:keys [db]} [auth-method]] + {:db (assoc db :auth-method auth-method)})) + +(def ^:const temp-display-name + "While creating a profile, we cannot use an empty string; this value works as a + placeholder that will be updated later once the compressed key exists. See + `status-im.contexts.profile.edit.name.events/get-default-display-name` for more details." + "temporal username") + +(rf/reg-event-fx + :onboarding/use-temporary-display-name + (fn [{:keys [db]} [temporary-display-name?]] + {:db (assoc db + :onboarding/profile + {:temporary-display-name? temporary-display-name? + :display-name (if temporary-display-name? + temp-display-name + "")})})) + +(rf/reg-event-fx + :onboarding/finalize-setup + (fn [{db :db}] + (let [{:keys [password syncing? auth-method + temporary-display-name? + enable-news-notifications? + enable-notifications?]} (:onboarding/profile db) + {:keys [key-uid] :as profile} (:profile/profile db) + biometric-enabled? (= auth-method constants/auth-method-biometric)] + {:db (assoc db :onboarding/generated-keys? true) + :fx (cond-> [] + temporary-display-name? + (conj [:dispatch [:profile/set-default-profile-name profile]]) + + biometric-enabled? + (conj [:keychain/save-password-and-auth-method + {:key-uid key-uid + :masked-password (if syncing? + password + (security/hash-masked-password password)) + :on-success (fn [] + (rf/dispatch [:onboarding/set-auth-method auth-method]) + (when syncing? + (rf/dispatch + [:onboarding/finish-onboarding]))) + :on-error #(log/error "failed to save biometrics" + {:key-uid key-uid + :error %})}]) + + (and enable-notifications? + enable-news-notifications?) + (conj [:dispatch + [:notifications/news-notifications-switch true]]) + + enable-notifications? + (conj [:dispatch + [:push-notifications/switch true]]))}))) diff --git a/src/status_im/contexts/onboarding/getting_started_doc/style.cljs b/src/status_im/contexts/onboarding/getting_started_doc/style.cljs new file mode 100644 index 00000000000..b9e04f33e6b --- /dev/null +++ b/src/status_im/contexts/onboarding/getting_started_doc/style.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.onboarding.getting-started-doc.style + (:require + [quo.foundations.colors :as colors])) + +(def main-content + {:color colors/white + :margin-bottom 4}) + +(def subtitle + {:margin-top 20 + :margin-bottom 4}) + +(def content + {:color colors/white-opa-70}) diff --git a/src/status_im/contexts/onboarding/getting_started_doc/view.cljs b/src/status_im/contexts/onboarding/getting_started_doc/view.cljs new file mode 100644 index 00000000000..d062baa59d2 --- /dev/null +++ b/src/status_im/contexts/onboarding/getting_started_doc/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.onboarding.getting-started-doc.view + (:require + [quo.core :as quo] + [status-im.contexts.onboarding.getting-started-doc.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn getting-started-doc + [] + [quo/documentation-drawers + {:title (i18n/label :t/getting-started-with-status) + :shell? true} + [:<> + [quo/text + {:size :paragraph-2 + :style style/main-content} + (i18n/label :t/getting-started-description)] + [quo/text + {:size :paragraph-1 + :style style/subtitle + :weight :semi-bold} + (i18n/label :t/generate-keys)] + [quo/text + {:size :paragraph-2 + :style style/content} + (i18n/label :t/getting-started-generate-keys-description)] + [quo/text + {:size :paragraph-1 + :style style/subtitle + :weight :semi-bold} + (i18n/label :t/getting-started-generate-keys-from-recovery-phrase)] + [quo/text + {:size :paragraph-2 + :style style/content} + (i18n/label :t/getting-started-generate-keys-from-recovery-phrase-description)] + [quo/text + {:size :paragraph-1 + :style style/subtitle + :weight :semi-bold} + (i18n/label :t/getting-started-generate-keys-on-keycard)] + [quo/text + {:size :paragraph-2 + :style style/content} + (i18n/label :t/getting-started-generate-keys-on-keycard-description)]]]) + +(defn show-as-bottom-sheet + [] + (rf/dispatch [:show-bottom-sheet + {:content getting-started-doc + :shell? true}])) diff --git a/src/status_im/contexts/onboarding/interceptors.cljs b/src/status_im/contexts/onboarding/interceptors.cljs new file mode 100644 index 00000000000..0fd418d284e --- /dev/null +++ b/src/status_im/contexts/onboarding/interceptors.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.onboarding.interceptors + (:require + [re-frame.interceptor :as interceptor] + [react-native.mmkv :as mmkv])) + +(defn inject-local-profile-storage + [context] + (let [db (interceptor/get-coeffect context :db) + key-uid (get-in db [:profile/profile :key-uid]) + local-profile (mmkv/get-object key-uid)] + (assoc-in context + [:coeffects :local-profile-storage] + local-profile))) + +(def local-profile-storage-interceptor + (interceptor/->interceptor + :id :local-profile-storage-interceptor + :before inject-local-profile-storage)) diff --git a/src/status_im/contexts/onboarding/intro/style.cljs b/src/status_im/contexts/onboarding/intro/style.cljs new file mode 100644 index 00000000000..aa863f1f7f0 --- /dev/null +++ b/src/status_im/contexts/onboarding/intro/style.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.onboarding.intro.style + (:require + [quo.foundations.colors :as colors])) + +(def page-container + {:flex 1 + :justify-content :flex-end}) + +(def terms-privacy-container + {:padding-horizontal 20 + :padding-vertical 8 + :align-items :center}) + +(def plain-text + {:color colors/white-opa-70}) + +(def highlighted-text + {:color colors/white}) + +(def danger-text + {:color colors/danger-50}) + +(defn bottom-actions-container + [bottom-insets] + {:background-color colors/onboarding-header-black + :flex-shrink 1 + :padding-bottom (+ 20 bottom-insets)}) diff --git a/src/status_im/contexts/onboarding/intro/view.cljs b/src/status_im/contexts/onboarding/intro/view.cljs new file mode 100644 index 00000000000..627e1cbef9c --- /dev/null +++ b/src/status_im/contexts/onboarding/intro/view.cljs @@ -0,0 +1,122 @@ +(ns status-im.contexts.onboarding.intro.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.privacy.view :as privacy] + [status-im.common.terms.view :as terms] + [status-im.contexts.onboarding.common.background.view :as background] + [status-im.contexts.onboarding.common.overlay.view :as overlay] + [status-im.contexts.onboarding.intro.style :as style] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- show-terms-of-use + [] + (rf/dispatch [:show-bottom-sheet {:content terms/terms-of-use :shell? true}])) + +(defn- show-privacy-policy + [] + (rf/dispatch [:show-bottom-sheet {:content privacy/privacy-statement :shell? true}])) + +(defn- show-privacy-mode-sheet + [] + (rf/dispatch [:privacy-mode/show-bottom-sheet {:theme :dark :shell? true}])) + +(defn- third-party-services-text + [privacy-mode-enabled?] + [rn/view {:style {:flex-direction :row :margin-bottom 8}} + [quo/text + {:on-press (when privacy-mode-enabled? show-privacy-mode-sheet) + :style (if privacy-mode-enabled? style/danger-text style/plain-text) + :size :paragraph-2 + :weight :medium} + (i18n/label :t/third-party-services)] + [quo/text + {:on-press show-privacy-mode-sheet + :style (if privacy-mode-enabled? style/danger-text style/highlighted-text) + :size :paragraph-2 + :weight :medium} + (i18n/label (if privacy-mode-enabled? :t/disabled-2 :t/enabled))]]) + +(defn- terms + [privacy-mode-enabled?] + [rn/view {:style style/terms-privacy-container} + (when (ff/enabled? ::ff/privacy-mode-ui) + [third-party-services-text privacy-mode-enabled?]) + [quo/text + {:style style/plain-text + :size :paragraph-2} + (str (i18n/label :t/accept-status-tos-prefix))] + [rn/view {:style {:flex-direction :row}} + [quo/text + {:on-press show-terms-of-use + :style style/highlighted-text + :size :paragraph-2 + :weight :medium} + (i18n/label :t/terms-of-service)] + [quo/text + {:style style/plain-text + :size :paragraph-2} + " " (i18n/label :t/and) " "] + [quo/text + {:on-press show-privacy-policy + :style style/highlighted-text + :size :paragraph-2 + :weight :medium} + (i18n/label :t/intro-privacy-policy)]]]) + +(defn- explore-new-status + [] + (rf/dispatch [:profile/explore-new-status])) + +(defn- open-next-screen + [privacy-mode-enabled? use-temporary-display-name? next-screen] + (when-let [blur-show-fn @overlay/blur-show-fn-atom] + (blur-show-fn)) + (rf/dispatch [:onboarding/use-temporary-display-name use-temporary-display-name?]) + (if privacy-mode-enabled? + (rf/dispatch [:open-modal next-screen]) + (rf/dispatch [:open-modal :screen/onboarding.share-usage {:next-screen next-screen}]))) + +(defn view + [] + (let [has-profiles-and-unaccepted-terms? (rf/sub [:profile/has-profiles-and-unaccepted-terms?]) + privacy-mode-enabled? (rf/sub [:privacy-mode/privacy-mode-enabled?]) + sync-or-recover-profile (rn/use-callback + #(open-next-screen privacy-mode-enabled? + false + :screen/onboarding.log-in) + [privacy-mode-enabled?]) + create-profile (rn/use-callback + #(open-next-screen privacy-mode-enabled? + true + :screen/onboarding.create-profile) + [privacy-mode-enabled?])] + [rn/view {:style style/page-container} + [background/view false] + [quo/bottom-actions + (cond-> + {:container-style (style/bottom-actions-container safe-area/bottom) + :description :bottom + :description-text [terms privacy-mode-enabled?]} + + has-profiles-and-unaccepted-terms? + (assoc + :actions :one-action + :button-one-label (i18n/label :t/explore-the-new-status) + :button-one-props {:accessibility-label :explore-new-status + :on-press explore-new-status}) + + (not has-profiles-and-unaccepted-terms?) + (assoc + :actions :two-vertical-actions + :button-one-label (i18n/label :t/log-in) + :button-one-props {:type :dark-grey + :accessibility-label :log-in + :on-press sync-or-recover-profile} + :button-two-label (i18n/label :t/create-profile) + :button-two-props {:accessibility-label :new-to-status-button + :on-press create-profile}))] + [overlay/view]])) diff --git a/src/status_im/contexts/onboarding/log_in/view.cljs b/src/status_im/contexts/onboarding/log_in/view.cljs new file mode 100644 index 00000000000..699f4c029a9 --- /dev/null +++ b/src/status_im/contexts/onboarding/log_in/view.cljs @@ -0,0 +1,111 @@ +(ns status-im.contexts.onboarding.log-in.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.check-before-syncing.view :as check-before-syncing] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.config :as config] + [status-im.contexts.onboarding.create-profile.style :as style] + [status-im.contexts.onboarding.getting-started-doc.view :as getting-started-doc] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- navigate-to-sign-in-by-syncing + [] + (rf/dispatch [:syncing/clear-syncing-fallback-flow]) + (debounce/throttle-and-dispatch + [:onboarding/navigate-to-sign-in-by-syncing] + 1000)) + +(defn- show-check-before-syncing + [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] [check-before-syncing/view + {:on-submit navigate-to-sign-in-by-syncing}]) + :shell? true}])) + +(defn- navigate-to-sign-in-by-recovery-phrase + [] + (rf/dispatch [:syncing/clear-syncing-fallback-flow]) + (rf/dispatch [:onboarding/navigate-to-sign-in-by-seed-phrase :screen/onboarding.log-in])) + +(defn- option-card-max-height + [window-height] + (- window-height + (* 2 56) ;; two other list items + (* 2 16) ;; spacing between items + 220)) ;; extra spacing (top bar) + +(defn- log-in-with-recovery-phrase-main-card + [window-height] + [quo/small-option-card + {:variant :main + :title (i18n/label :t/log-in-with-recovery-phrase) + :subtitle (i18n/label :t/log-in-with-recovery-phrase-subtitle) + :button-label (i18n/label :t/use-recovery-phrase) + :accessibility-label :log-in-with-recovery-phrase-main-card + :image (resources/get-image :use-recovery-phrase) + :max-height (option-card-max-height window-height) + :on-press navigate-to-sign-in-by-recovery-phrase}]) + +(defn- log-in-by-syncing-icon-card + [] + [quo/small-option-card + {:variant :icon + :title (i18n/label :t/log-in-by-syncing) + :subtitle (i18n/label :t/log-in-by-syncing-subtitle) + :accessibility-label :log-in-by-syncing-icon-card + :image (resources/get-image :login-syncing) + :on-press show-check-before-syncing}]) + +(defn- log-in-with-keycard-icon-card + [] + [quo/small-option-card + {:variant :icon + :title (i18n/label :t/log-in-with-keycard) + :subtitle (i18n/label :t/log-in-with-keycard-subtitle) + :accessibility-label :log-in-with-keycard + :image (resources/get-image :use-keycard) + :on-press (fn [] + (rf/dispatch [:open-modal :screen/keycard.check + {:on-press #(rf/dispatch [:keycard.login/check-card])}]))}]) + +(defn- navigate-to-quo-preview + [] + (rf/dispatch [:navigate-to :screen/quo-preview])) + +(defn view + [] + (let [window-height (rf/sub [:dimensions/window-height])] + [rn/view {:style style/content-container} + [quo/page-nav + {:margin-top safe-area/top + :type :no-title + :background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back + :right-side [{:icon-name :i/info + :on-press getting-started-doc/show-as-bottom-sheet} + (when config/debug-or-pr-build? + {:icon-name :i/reveal-whitelist + :on-press navigate-to-quo-preview})]}] + [rn/view {:style style/options-container} + [quo/text + {:style style/title + :size :heading-1 + :weight :semi-bold} + (i18n/label :t/log-in)] + [log-in-with-recovery-phrase-main-card window-height] + [rn/view {:style style/subtitle-container} + [quo/text + {:style style/subtitle + :size :paragraph-2 + :weight :medium} + (i18n/label :t/other-options)]] + [log-in-by-syncing-icon-card] + [rn/view {:style style/space-between-suboptions}] + [log-in-with-keycard-icon-card]]])) diff --git a/src/status_im/contexts/onboarding/preparing_status/style.cljs b/src/status_im/contexts/onboarding/preparing_status/style.cljs new file mode 100644 index 00000000000..86aff1c1c99 --- /dev/null +++ b/src/status_im/contexts/onboarding/preparing_status/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.onboarding.preparing-status.style) + +(defn page-container + [insets] + {:flex 1 + :padding-top (:top insets)}) + +(defn page-illustration + [width] + {:flex 1 + :width width}) diff --git a/src/status_im/contexts/onboarding/preparing_status/view.cljs b/src/status_im/contexts/onboarding/preparing_status/view.cljs new file mode 100644 index 00000000000..e320d4537b4 --- /dev/null +++ b/src/status_im/contexts/onboarding/preparing_status/view.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.onboarding.preparing-status.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.resources :as resources] + [status-im.contexts.onboarding.preparing-status.style :as style] + [utils.i18n :as i18n])) + +(defn title + [] + [rn/view + {:style {:margin-top 56 + :height 56 + :margin-bottom 10}} + [quo/text-combinations + {:container-style {:margin-horizontal 20 + :margin-vertical 12} + :title (i18n/label :t/preparing-status-for-you) + :description (i18n/label :t/hang-in-there)}]]) + +(defn content + [] + (let [width (:width (rn/get-window))] + [rn/image + {:resize-mode :stretch + :style (style/page-illustration width) + :source (resources/get-image :preparing-status)}])) + +(defn view + [] + [rn/view {:style (style/page-container safe-area/insets)} + [:<> + [title] + [content]]]) diff --git a/src/status_im/contexts/onboarding/share_usage/learn_more_sheet.cljs b/src/status_im/contexts/onboarding/share_usage/learn_more_sheet.cljs new file mode 100644 index 00000000000..6d955de3ce1 --- /dev/null +++ b/src/status_im/contexts/onboarding/share_usage/learn_more_sheet.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.onboarding.share-usage.learn-more-sheet + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.privacy.view :as privacy] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:private bullet-points-data + [[:t/help-us-improve-status-bullet-point-1 true] + [:t/help-us-improve-status-bullet-point-2 true] + [:t/help-us-improve-status-bullet-point-3 false] + [:t/help-us-improve-status-bullet-point-4 false] + [:t/help-us-improve-status-bullet-point-5 false]]) + +(defn- privacy-policy-text + [] + [rn/view {:style {:margin-horizontal 20 :margin-vertical 8}} + [quo/text + [quo/text + {:style {:color colors/white-opa-50} + :size :paragraph-2} + (i18n/label :t/more-details-in-privacy-policy-1-onboarding)] + [quo/text + {:size :paragraph-2 + :weight :bold + :on-press #(rf/dispatch [:show-bottom-sheet {:content privacy/privacy-statement :shell? true}])} + (i18n/label :t/more-details-in-privacy-policy-2)]]]) + +(defn- bullet-points + [] + [:<> + (for [[label collected?] bullet-points-data] + ^{:key label} + [quo/markdown-list + {:description (i18n/label label) + :blur? true + :type :custom-icon + :container-style {:padding-vertical 5} + :icon (if collected? :i/check-circle :i/clear) + :icon-props (if collected? + {:no-color true} + {:size 20 :color colors/danger-60 :color-2 colors/white})}])]) + +(defn view + [] + [:<> + [quo/drawer-top + {:title (i18n/label :t/help-us-improve-status)}] + [quo/text + {:size :paragraph-1 + :style {:padding-horizontal 20}} + (i18n/label :t/help-us-improve-status-subtitle)] + [rn/view {:style {:padding-vertical 8}} + [bullet-points]] + [privacy-policy-text]]) diff --git a/src/status_im/contexts/onboarding/share_usage/style.cljs b/src/status_im/contexts/onboarding/share_usage/style.cljs new file mode 100644 index 00000000000..556034e2de8 --- /dev/null +++ b/src/status_im/contexts/onboarding/share_usage/style.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.onboarding.share-usage.style + (:require [quo.foundations.colors :as colors])) + +(defn page-illustration + [width] + {:flex 1 + :margin-top 12 + :margin-bottom 10 + :width width}) + +(defn buttons + [insets] + {:margin 20 + :margin-bottom (+ 12 (:bottom insets))}) + +(def page + {:flex 1 + :background-color colors/neutral-100}) diff --git a/src/status_im/contexts/onboarding/share_usage/view.cljs b/src/status_im/contexts/onboarding/share_usage/view.cljs new file mode 100644 index 00000000000..e8f81a5ac5c --- /dev/null +++ b/src/status_im/contexts/onboarding/share_usage/view.cljs @@ -0,0 +1,65 @@ +(ns status-im.contexts.onboarding.share-usage.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.contexts.onboarding.share-usage.learn-more-sheet :as learn-more-sheet] + [status-im.contexts.onboarding.share-usage.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- share-usage-data-fn + [enabled? next-screen] + (rf/dispatch [:centralized-metrics/toggle-centralized-metrics enabled? true]) + (if next-screen + (rf/dispatch [:navigate-to-within-stack [next-screen :screen/onboarding.share-usage]]) ;; Onboarding + (rf/dispatch [:navigate-back]))) ;; Login Screen + +(defn- learn-more + [] + (rf/dispatch [:show-bottom-sheet + {:content learn-more-sheet/view + :shell? true}])) + +(defn view + [] + (let [next-screen (:next-screen (quo.context/use-screen-params)) + share-usage-data (rn/use-callback #(share-usage-data-fn true next-screen)) + maybe-later (rn/use-callback #(share-usage-data-fn false next-screen))] + [rn/view {:style style/page} + [quo/page-nav + {:margin-top safe-area/top + :background :blur + :icon-name :i/close + :on-press events-helper/navigate-back + :right-side [{:icon-left :i/info + :accessibility-label :learn-more + :label (i18n/label :t/learn-more) + :on-press learn-more}]}] + [quo/page-top + {:title (i18n/label :t/help-us-improve-status) + :title-accessibility-label :share-usage-title + :description :text + :description-text (i18n/label :t/collecting-usage-data) + :description-accessibility-label :share-usage-subtitle}] + [rn/image + {:resize-mode :contain + :style (style/page-illustration (:width (rn/get-window))) + :source (resources/get-image :usage-data)}] + [rn/view {:style (style/buttons safe-area/insets)} + [quo/button + {:size 40 + :accessibility-label :share-usage-data + :on-press share-usage-data} + (i18n/label :t/agree)] + [quo/button + {:size 40 + :accessibility-label :maybe-later-button + :background :blur + :type :grey + :on-press maybe-later + :container-style {:margin-top 12}} + (i18n/label :t/maybe-later)]]])) diff --git a/src/status_im/contexts/onboarding/sign_in/view.cljs b/src/status_im/contexts/onboarding/sign_in/view.cljs new file mode 100644 index 00000000000..f67db9062e1 --- /dev/null +++ b/src/status_im/contexts/onboarding/sign_in/view.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.onboarding.sign-in.view + (:require + [status-im.contexts.onboarding.common.background.view :as background] + [status-im.contexts.syncing.scan-sync-code.view :as scan-sync-code] + [utils.i18n :as i18n])) + +(defn view + [] + [scan-sync-code/view + {:title (i18n/label :t/sign-in-by-syncing) + :background [background/view true] + :animated? false + :screen-name "sign-in"}]) + +(defn animated-view + [] + [scan-sync-code/view + {:title (i18n/label :t/sign-in-by-syncing) + :animated? true + :screen-name "sign-in-intro"}]) diff --git a/src/status_im/contexts/onboarding/syncing/progress/style.cljs b/src/status_im/contexts/onboarding/syncing/progress/style.cljs new file mode 100644 index 00000000000..37f4c178597 --- /dev/null +++ b/src/status_im/contexts/onboarding/syncing/progress/style.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.onboarding.syncing.progress.style + (:require + [quo.foundations.colors :as colors])) + +(def absolute-fill + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(defn page-container + [in-onboarding?] + {:flex 1 + :position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0 + :padding-bottom 20 + :background-color (when-not in-onboarding? colors/neutral-80-opa-80-blur)}) + +(defn page-illustration + [width pairing-progress?] + {:flex 1 + :width width + :align-items :center + :margin-vertical (when pairing-progress? 24) + :align-self :center + :justify-content :center}) diff --git a/src/status_im/contexts/onboarding/syncing/progress/view.cljs b/src/status_im/contexts/onboarding/syncing/progress/view.cljs new file mode 100644 index 00000000000..7052ca305ef --- /dev/null +++ b/src/status_im/contexts/onboarding/syncing/progress/view.cljs @@ -0,0 +1,95 @@ +(ns status-im.contexts.onboarding.syncing.progress.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.onboarding.common.background.view :as background] + [status-im.contexts.onboarding.syncing.progress.style :as style] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn pairing-progress + [status] + (not= status :error)) + +(defn page-title + [pairing-progress?] + [quo/text-combinations + {:container-style {:margin-top 56 :margin-horizontal 20} + :title (i18n/label (if pairing-progress? + :t/sync-profile-title + :t/sync-devices-error-title)) + :description (when-not pairing-progress? + (i18n/label :t/sync-devices-error-sub-title)) + :title-accessibility-label :progress-screen-title + :description-accessibility-label :progress-screen-sub-title}]) + +(defn- navigate-to-enter-seed-phrase + [] + (rf/dispatch [:syncing/set-syncing-fallback-flow]) + (debounce/debounce-and-dispatch + [:onboarding/navigate-to-sign-in-by-seed-phrase :screen/onboarding.log-in] + 500)) + +(defn- try-again + [logged-in?] + (rf/dispatch [:syncing/clear-states]) + (if logged-in? + (rf/dispatch [:navigate-back]) + (rf/dispatch [:navigate-back-to :screen/onboarding.log-in]))) + +(defn try-again-button + [profile-color logged-in?] + (let [two-vertical-actions-height 116] + [quo/bottom-actions + {:actions (if logged-in? :one-action :two-vertical-actions) + :blur? true + :container-style {:height (when-not logged-in? two-vertical-actions-height)} + :button-two-label (i18n/label :t/use-recovery-phrase) + :button-two-props {:type :primary + :accessibility-label :try-seed-phrase-button + :customization-color profile-color + :size 40 + :on-press navigate-to-enter-seed-phrase} + :button-one-label + (i18n/label :t/try-again) + :button-one-props + {:type (if logged-in? :primary :grey) + :accessibility-label :try-again-later-button + :customization-color profile-color + :size 40 + :on-press #(try-again logged-in?)}}])) + +(defn- illustration + [pairing-progress?] + [rn/image + {:resize-mode :contain + :style (style/page-illustration (:width (rn/get-window)) pairing-progress?) + :source (resources/get-image (if pairing-progress? :syncing-devices :syncing-wrong))}]) + +(defn view + [in-onboarding?] + (let [pairing-status (rf/sub [:pairing/pairing-status]) + logged-in? (rf/sub [:multiaccount/logged-in?]) + pairing-progress? (pairing-progress pairing-status) + profile-color (or (:color (rf/sub [:onboarding/profile])) + (rf/sub [:profile/customization-color]))] + [rn/view {:style (style/page-container in-onboarding?)} + (when-not in-onboarding? + [rn/view {:style style/absolute-fill} + [background/view true]]) + [quo/page-nav {:type :no-title :background :blur}] + [page-title pairing-progress?] + [illustration pairing-progress?] + (if pairing-progress? + [quo/information-box + {:type :default + :blur? true + :style {:margin-vertical 11 :margin-horizontal 12}} + (i18n/label :t/sync-devices-sub-title)] + [try-again-button profile-color logged-in?])])) + +(defn view-onboarding + [] + [view true]) diff --git a/src/status_im/contexts/preview/feature_flags/style.cljs b/src/status_im/contexts/preview/feature_flags/style.cljs new file mode 100644 index 00000000000..ba0d92973dd --- /dev/null +++ b/src/status_im/contexts/preview/feature_flags/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.preview.feature-flags.style) + +(def container + {:flex 1 + :margin-left 20 + :margin-bottom 20}) diff --git a/src/status_im/contexts/preview/feature_flags/view.cljs b/src/status_im/contexts/preview/feature_flags/view.cljs new file mode 100644 index 00000000000..4a4c11e5b73 --- /dev/null +++ b/src/status_im/contexts/preview/feature_flags/view.cljs @@ -0,0 +1,42 @@ +(ns status-im.contexts.preview.feature-flags.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [re-frame.core :as rf] + [react-native.core :as rn] + [status-im.contexts.preview.feature-flags.style :as style] + [status-im.feature-flags :as ff])) + +(defn view + [] + [rn/view {:style {:flex 1}} + [quo/page-nav + {:type :title + :text-align :left + :title "Features Flags" + :icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back]) + :right-side [{:icon-name :i/rotate + :on-press #(ff/reset-flags)}]}] + [rn/scroll-view + (doall + (for [context-name ff/feature-flags-categories + :let [context-flags (filter (fn [[k]] + (= context-name + (first (string/split (str (name k)) ".")))) + (ff/feature-flags))]] + ^{:key (str context-name)} + [rn/view {:style style/container} + [quo/text + context-name] + (doall + (for [i (range (count context-flags)) + :let [[flag] (nth context-flags i)]] + ^{:key (str context-name flag i)} + [rn/view {:style {:flex-direction :row}} + [quo/selectors + {:type :toggle + :checked? (ff/enabled? flag) + :container-style {:margin-right 8} + :on-change #(ff/toggle flag)}] + [quo/text (second (string/split (name flag) "."))]]))]))]]) diff --git a/src/status_im/contexts/preview/quo/animated_header_list/animated_header_list.cljs b/src/status_im/contexts/preview/quo/animated_header_list/animated_header_list.cljs new file mode 100644 index 00000000000..3b9af2492d5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/animated_header_list/animated_header_list.cljs @@ -0,0 +1,86 @@ +(ns status-im.contexts.preview.quo.animated-header-list.animated-header-list + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [re-frame.core :as rf] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.reanimated :as reanimated])) + +(def data [0 1 2 3 4 5 6 7 8 9 10]) + +(defn child + [] + [rn/view + {:style {:height 100 + :background-color colors/primary-50-opa-40 + :margin 10 + :justify-content :center + :padding-left 10}} + [quo/text "This is some message!!!!!"]]) + + +(defn main-comp + [] + [rn/flat-list + {:data data + :render-fn child + :key-fn (fn [item] (str item)) + :header + [rn/view + {:style {:height 70 + :padding-top 8 + :padding-horizontal 20}} + [quo/text {:size :paragraph-2} + "Some random description • Developer • Designer • Olympic gold winner • President • Super Hero"]]}]) + +(defn f-display-picture-comp + [animation] + [reanimated/fast-image + {:style (reanimated/apply-animations-to-style + {:width animation + :height animation} + {:border-radius 72}) + :source + {:uri + "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"}}]) + +(defn header-comp + [] + [rn/view + {:style {:flex-direction :row + :justify-content :center + :align-items :center}} + [fast-image/fast-image + {:source {:uri + "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"} + :style {:width 32 + :height 32 + :border-radius 16 + :margin-right 8}}] + [quo/text {:weight :semi-bold} "Alecia Keys"]]) + +(defn title-comp + [] + [quo/text + {:weight :semi-bold + :size :heading-1 + :style {:margin-top 56 + :margin-left 20}} "Alicia Keys"]) + +(defn parameters + [theme] + {:theme-color (colors/theme-alpha "#5BCC95" 0.2 0.2 theme) + :cover-uri + "https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/kitten-playing-with-toy-mouse-royalty-free-image-590055188-1542816918.jpg?crop=1.00xw:0.758xh;0,0.132xh&resize=480:*" + :f-display-picture-comp f-display-picture-comp + :header-comp header-comp + :title-comp title-comp + :main-comp main-comp + :back-button-on-press #(rf/dispatch [:navigate-back])}) + +(defn mock-screen + [] + (let [theme (quo.context/use-theme)] + [quo/animated-header-list (parameters theme)])) diff --git a/src/status_im/contexts/preview/quo/avatars/account_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/account_avatar.cljs new file mode 100644 index 00000000000..05aec13a41a --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/account_avatar.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.preview.quo.avatars.account-avatar + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.re-frame :as rf])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :default} + {:key :watch-only}]} + {:key :size + :type :select + :options [{:key 16} + {:key 20} + {:key 24} + {:key 28} + {:key 32} + {:key 48} + {:key :size-64} + {:key 80}]} + {:key "Emoji" + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:customization-color :purple + :size 80 + :emoji "🍑" + :type :default})] + (fn [] + [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"]]))) diff --git a/src/status_im/contexts/preview/quo/avatars/channel_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/channel_avatar.cljs new file mode 100644 index 00000000000..11575854092 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/channel_avatar.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.preview.quo.avatars.channel-avatar + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :size-64} + {:key :size-32} + {:key :default}]} + {:key :emoji + :type :text} + {:key :full-name + :type :text} + (preview/customization-color-option) + {:key :locked-state + :type :select + :options [{:key :not-set} + {:key :unlocked} + {:key :locked}]}]) + +(defn view + [] + (let [state (reagent/atom {:size :size-32 + :locked-state :not-set + :emoji "🍑" + :full-name "Some channel" + :customization-color :blue})] + (fn [] + (let [locked? (case (:locked-state @state) + :not-set nil + :unlocked false + :locked true + nil)] + [preview/preview-container {:state state :descriptor descriptor} + [quo/channel-avatar + (assoc @state + :locked? + locked?)]])))) diff --git a/src/status_im/contexts/preview/quo/avatars/collection_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/collection_avatar.cljs new file mode 100644 index 00000000000..c6471fbec84 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/collection_avatar.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.avatars.collection-avatar + + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :size-24} + {:key :size-20}]} + {:key :image + :type :select + :options [{:key (resources/get-mock-image :bored-ape) + :value "Bored ape"} + {:key (resources/get-mock-image :ring) + :value "Circle"}]}]) + +(defn view + [] + (let [state (reagent/atom {:image (resources/get-mock-image :bored-ape)})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:align-items :center}} + [quo/collection-avatar @state]]))) diff --git a/src/status_im/contexts/preview/quo/avatars/community_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/community_avatar.cljs new file mode 100644 index 00000000000..002118afc27 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/community_avatar.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.preview.quo.avatars.community-avatar + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :select + :key :size + :options [{:key :size-32} + {:key :size-24}]}]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:size :size-32})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [quo/community-avatar + (assoc state + :image + (resources/get-mock-image :community-logo))]])) diff --git a/src/status_im/contexts/preview/quo/avatars/dapp_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/dapp_avatar.cljs new file mode 100644 index 00000000000..34f0d6f96b2 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/dapp_avatar.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.preview.quo.avatars.dapp-avatar + (:require + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :boolean + :key :context?} + {:type :select + :key :size + :options [{:key :size-32} + {:key :size-64}]}]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:context? false + :size :size-32})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [quo/dapp-avatar + (assoc state + :network-image (resources/get-network :ethereum) + :image (resources/get-dapp :coingecko))]])) diff --git a/src/status_im/contexts/preview/quo/avatars/group_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/group_avatar.cljs new file mode 100644 index 00000000000..24c7a0d9c28 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/group_avatar.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.preview.quo.avatars.group-avatar + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :size-20 + :value "20"} + {:key :size-28 + :value "28"} + {:key :size-32 + :value "32"} + {:key :size-48 + :value "48"} + {:key :size-80 + :value "80"}]} + {:label "Avatar:" + :key :picture? + :type :boolean} + (preview/customization-color-option)]) + +(def avatar (resources/get-mock-image :photo1)) + +(defn view + [] + (let [state (reagent/atom {:customization-color :blue + :size :size-20 + :picture? false})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/group-avatar + (cond-> @state + (:picture? @state) + (assoc :picture avatar))]]))) diff --git a/src/status_im/contexts/preview/quo/avatars/icon_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/icon_avatar.cljs new file mode 100644 index 00000000000..2ad869816e7 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/icon_avatar.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.avatars.icon-avatar + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :size-20} + {:key :size-24} + {:key :size-32} + {:key :size-48}]} + {:key :icon + :type :select + :options [{:key :i/placeholder20 + :value "Placeholder"} + {:key :i/wallet} + {:key :i/play}]} + (preview/customization-color-option {:key :color})]) + +(defn view + [] + (let [state (reagent/atom {:size :size-48 + :icon :i/placeholder20 + :color :primary})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/icon-avatar @state]]))) diff --git a/src/status_im/contexts/preview/quo/avatars/token_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/token_avatar.cljs new file mode 100644 index 00000000000..d7b81aaeddf --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/token_avatar.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.avatars.token-avatar + (:require + [quo.core :as quo] + [quo.foundations.resources :as foundations.resources] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :select + :key :type + :options [{:key :asset} + {:key :collectible}]} + {:type :boolean + :key :context?}]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:type :asset + :context? false})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [quo/token-avatar + (assoc state + :network-image (foundations.resources/get-network :optimism) + :image (resources/get-mock-image + (if (= (state :type) :asset) :status-logo :collectible1)))]])) diff --git a/src/status_im/contexts/preview/quo/avatars/user_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/user_avatar.cljs new file mode 100644 index 00000000000..96cf39d97c2 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/user_avatar.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.preview.quo.avatars.user-avatar + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :big} + {:key :size-64} + {:key :medium} + {:key :small} + {:key :xs} + {:key :xxs} + {:key :xxxs}]} + (preview/customization-color-option) + {:key :online? + :type :boolean} + {:key :status-indicator? + :type :boolean} + {:key :full-name + :type :text} + {:key :profile-picture + :type :select + :options [{:value "None" + :key nil} + {:value "Alicia Keys" + :key (resources/get-mock-image :user-picture-female2)} + {:value "pedro.eth" + :key (resources/get-mock-image :user-picture-male4)}]}]) + +(defn view + [] + (let [state (reagent/atom {:full-name "A Y" + :status-indicator? true + :online? true + :size :medium + :customization-color :blue})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/user-avatar @state]]))) diff --git a/src/status_im/contexts/preview/quo/avatars/wallet_user_avatar.cljs b/src/status_im/contexts/preview/quo/avatars/wallet_user_avatar.cljs new file mode 100644 index 00000000000..be3fdfc5cd0 --- /dev/null +++ b/src/status_im/contexts/preview/quo/avatars/wallet_user_avatar.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.preview.quo.avatars.wallet-user-avatar + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :full-name + :type :text} + {:key :size + :type :select + :options [{:key :size-20} + {:key :size-24} + {:key :size-32} + {:key :size-48} + {:key :size-64} + {:key :size-80}]} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:full-name "empty name" + :size :size-80 + :customization-color :blue})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/wallet-user-avatar @state]]))) diff --git a/src/status_im/contexts/preview/quo/banners/alert_banner.cljs b/src/status_im/contexts/preview/quo/banners/alert_banner.cljs new file mode 100644 index 00000000000..8400043548b --- /dev/null +++ b/src/status_im/contexts/preview/quo/banners/alert_banner.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.preview.quo.banners.alert-banner + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :text + :type :text} + {:key :button-text + :type :text} + {:key :action? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:action? true + :text "Alert text" + :button-text "Button" + :on-button-press #(js/alert "pressed")})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/alert-banner @state]]))) diff --git a/src/status_im/contexts/preview/quo/banners/banner.cljs b/src/status_im/contexts/preview/quo/banners/banner.cljs new file mode 100644 index 00000000000..c55f0ea2c81 --- /dev/null +++ b/src/status_im/contexts/preview/quo/banners/banner.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.preview.quo.banners.banner + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :latest-pin-text + :type :text} + {:key :pins-count + :type :text} + {:key :hide-pin? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:hide-pin? false + :pins-count 2 + :latest-pin-text + "Be respectful of fellow community members, even if they"})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/banner @state]]))) diff --git a/src/status_im/contexts/preview/quo/browser/browser_input.cljs b/src/status_im/contexts/preview/quo/browser/browser_input.cljs new file mode 100644 index 00000000000..08b522f6352 --- /dev/null +++ b/src/status_im/contexts/preview/quo/browser/browser_input.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.browser.browser-input + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :favicon? :type :boolean} + {:key :locked? :type :boolean} + {:key :blur? :type :boolean} + {:key :placeholder :type :text} + {:key :disabled? :type :boolean} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:blur? false + :disabled? false + :favicon? false + :placeholder "Search or enter dapp domain" + :locked? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/browser-input + (assoc @state + :favicon + (when (:favicon? @state) :i/verified))]]))) diff --git a/src/status_im/contexts/preview/quo/buttons/button.cljs b/src/status_im/contexts/preview/quo/buttons/button.cljs new file mode 100644 index 00000000000..df91d93fd18 --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/button.cljs @@ -0,0 +1,87 @@ +(ns status-im.contexts.preview.quo.buttons.button + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :primary} + {:key :positive} + {:key :grey} + {:key :dark-grey} + {:key :outline} + {:key :ghost} + {:key :danger} + {:key :black}]} + {:key :size + :type :select + :options [{:key 56 + :value "56"} + {:key 40 + :value "40"} + {:key 32 + :value "32"} + {:key 24 + :value "24"}]} + {:key :background + :type :select + :options [{:key :blur} + {:key :photo}]} + {:key :icon-only? + :type :boolean} + {:key :icon-top + :type :boolean} + {:key :icon-right + :type :boolean} + {:key :icon-left + :type :boolean} + {:key :disabled? + :type :boolean} + {:key :label + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:label "Press Me" + :size 40 + :type :primary + :customization-color :blue}) + label (reagent/cursor state [:label]) + icon-left (reagent/cursor state [:icon-left]) + icon-right (reagent/cursor state [:icon-right]) + icon-top (reagent/cursor state [:icon-top]) + icon-only? (reagent/cursor state [:icon-only?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:align-items :center}} + (when (= :photo (:background @state)) + [rn/image + {:source (resources/get-mock-image :community-cover) + :style {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0}}]) + [quo/button + (merge (dissoc @state + :theme + :customization-color + :icon-left + :icon-right) + {:background (:background @state) + :on-press #(println "Hello world!")} + (when (= :primary (:type @state)) {:customization-color (:customization-color @state)}) + (when @icon-top + {:icon-top :i/placeholder}) + (when @icon-left + {:icon-left :i/placeholder}) + (when @icon-right + {:icon-right :i/placeholder})) + (if @icon-only? :i/placeholder @label)]]))) diff --git a/src/status_im/contexts/preview/quo/buttons/composer_button.cljs b/src/status_im/contexts/preview/quo/buttons/composer_button.cljs new file mode 100644 index 00000000000..62ccc8e9a51 --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/composer_button.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.preview.quo.buttons.composer-button + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? + :type :boolean} + {:key :disabled? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:icon :i/placeholder + :blur? false + :on-press #(js/alert "pressed") + :on-long-press #(js/alert "long pressed")})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/composer-button @state]]))) diff --git a/src/status_im/contexts/preview/quo/buttons/dynamic_button.cljs b/src/status_im/contexts/preview/quo/buttons/dynamic_button.cljs new file mode 100644 index 00000000000..c4addcee85b --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/dynamic_button.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.preview.quo.buttons.dynamic-button + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.i18n :as i18n])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :mention} + {:key :notification-down} + {:key :notification-up} + {:key :search} + {:key :search-with-label} + {:key :scroll-to-bottom}]} + {:key :count + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:count "5" + :type :search-with-label + :labels {:search-with-label (i18n/label :t/back)}})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:align-items :center}} + [quo/dynamic-button @state]]))) diff --git a/src/status_im/contexts/preview/quo/buttons/predictive_keyboard.cljs b/src/status_im/contexts/preview/quo/buttons/predictive_keyboard.cljs new file mode 100644 index 00000000000..43bca5cf88c --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/predictive_keyboard.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.preview.quo.buttons.predictive-keyboard + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :error} + {:key :empty} + {:key :info} + {:key :words}]} + {:key :text + :type :text} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:type :info + :text "Enter 12, 18 or 24 words separated by a space"}) + blur? (reagent/cursor state [:blur?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? @blur? + :blur-container-style {:background-color colors/neutral-80-opa-70} + :blur-view-props {:overlay-color :transparent}} + [quo/predictive-keyboard + (merge @state + {:words ["label" "label" "labor" "ladder" "lady" "lake"] + :on-press #(js/alert (str "Pressed: " %))})]]))) diff --git a/src/status_im/contexts/preview/quo/buttons/slide_button.cljs b/src/status_im/contexts/preview/quo/buttons/slide_button.cljs new file mode 100644 index 00000000000..c21009ce17c --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/slide_button.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.preview.quo.buttons.slide-button + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :size-48} + {:key :size-40}]} + {:key :type + :type :select + :options [{:key :default} + {:key :danger}]} + {:key :disabled? + :type :boolean} + {:key :blur? + :type :boolean} + {:key :keep-at-end-after-slide? + :type :boolean} + (preview/customization-color-option {:key :color})]) + +(defn f-view + [] + (let [state (reagent/atom {:disabled? false + :color :blue + :size :size-48 + :keep-at-end-after-slide? false}) + color (reagent/cursor state [:color]) + blur? (reagent/cursor state [:blur?]) + keep-at-end-after-slide? (reagent/cursor state [:keep-at-end-after-slide?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style (when-not @blur? (:align-items :center)) + :blur? @blur? + :show-blur-background? true} + [quo/slide-button + {:track-text "We gotta slide" + :track-icon :face-id + :customization-color @color + :size (:size @state) + :disabled? (:disabled? @state) + :blur? @blur? + :type (:type @state) + :on-complete (fn [reset-fn] + (js/alert "Slide complete") + (reset-fn @keep-at-end-after-slide?))}]]))) + +(defn view [] [:f> f-view]) diff --git a/src/status_im/contexts/preview/quo/buttons/swap_order_button.cljs b/src/status_im/contexts/preview/quo/buttons/swap_order_button.cljs new file mode 100644 index 00000000000..5d4c41eae81 --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/swap_order_button.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.preview.quo.buttons.swap-order-button + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :boolean + :key :disabled?}]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:disabled? false})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [quo/swap-order-button + (assoc state + :on-press + #(js/alert "Pressed"))]])) diff --git a/src/status_im/contexts/preview/quo/buttons/wallet_button.cljs b/src/status_im/contexts/preview/quo/buttons/wallet_button.cljs new file mode 100644 index 00000000000..77827fec5a7 --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/wallet_button.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.preview.quo.buttons.wallet-button + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :disabled? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:icon :i/placeholder + :on-press #(js/alert "pressed") + :on-long-press #(js/alert "long pressed")})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:align-items :center}} + [quo/wallet-button @state]]))) diff --git a/src/status_im/contexts/preview/quo/buttons/wallet_ctas.cljs b/src/status_im/contexts/preview/quo/buttons/wallet_ctas.cljs new file mode 100644 index 00000000000..159acbcbfac --- /dev/null +++ b/src/status_im/contexts/preview/quo/buttons/wallet_ctas.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.preview.quo.buttons.wallet-ctas + (:require + [quo.core :as quo] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn view + [] + [preview/preview-container {} + [quo/wallet-ctas + {:container-style {:padding-top 24 + :padding-bottom 12 + :padding-horizontal 20 + :flex-direction :row} + :buy-action #(js/alert "Buy button pressed") + :send-action #(js/alert "Send button pressed") + :receive-action #(js/alert "Receive button pressed") + :bridge-action #(js/alert "Bridge button pressed") + :swap-action #(js/alert "Swap button pressed")}]]) diff --git a/src/status_im/contexts/preview/quo/calendar/calendar.cljs b/src/status_im/contexts/preview/quo/calendar/calendar.cljs new file mode 100644 index 00000000000..9a2e910a333 --- /dev/null +++ b/src/status_im/contexts/preview/quo/calendar/calendar.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.preview.quo.calendar.calendar + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.datetime :as datetime])) + +(def descriptor + [{:key :start-date + :type :text} + {:key :end-date + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:start-date nil :end-date nil}) + period (reagent/atom {:start-date nil :end-date nil})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/calendar + {:start-date (:start-date @period) + :end-date (:end-date @period) + :on-change (fn [new-range] + (reset! state + {:start-date (datetime/format-date (:start-date new-range)) + :end-date (datetime/format-date (:end-date new-range))}) + (reset! period new-range))}]]))) diff --git a/src/status_im/contexts/preview/quo/calendar/calendar_day.cljs b/src/status_im/contexts/preview/quo/calendar/calendar_day.cljs new file mode 100644 index 00000000000..f7bca5de7cd --- /dev/null +++ b/src/status_im/contexts/preview/quo/calendar/calendar_day.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.preview.quo.calendar.calendar-day + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [(preview/customization-color-option) + {:key :state + :type :select + :options [{:key :default} + {:key :selected} + {:key :disabled} + {:key :today}]}]) + +(defn view + [] + (let [state (reagent/atom {:state :default + :customization-color :blue})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/calendar-day @state 12]]))) diff --git a/src/status_im/contexts/preview/quo/calendar/calendar_year.cljs b/src/status_im/contexts/preview/quo/calendar/calendar_year.cljs new file mode 100644 index 00000000000..3d44124e491 --- /dev/null +++ b/src/status_im/contexts/preview/quo/calendar/calendar_year.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.preview.quo.calendar.calendar-year + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :selected? :type :boolean} + {:key :disabled? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:selected? false + :disabled? false})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/calendar-year @state "2023"]]))) diff --git a/src/status_im/contexts/preview/quo/cards/wallet_card.cljs b/src/status_im/contexts/preview/quo/cards/wallet_card.cljs new file mode 100644 index 00000000000..9c09bd78d08 --- /dev/null +++ b/src/status_im/contexts/preview/quo/cards/wallet_card.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.cards.wallet-card + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title + :type :text} + {:key :subtitle + :type :text} + {:key :dismissible? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:image (resources/get-image :buy) + :title "Buy" + :subtitle "Start investing now" + :on-press #(js/alert "Item pressed") + :on-press-close #(js/alert "Close pressed") + :dismissible? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:align-items :center}} + [quo/wallet-card @state]]))) diff --git a/src/status_im/contexts/preview/quo/code/snippet.cljs b/src/status_im/contexts/preview/quo/code/snippet.cljs new file mode 100644 index 00000000000..8279f37f1ab --- /dev/null +++ b/src/status_im/contexts/preview/quo/code/snippet.cljs @@ -0,0 +1,102 @@ +(ns status-im.contexts.preview.quo.code.snippet + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def go-example + "func (s *Server) listenAndServe() { + cfg := &tls.Config{Certificates: []tls.Certificate{*s.cert}, ServerName: s.hostname, MinVersion: tls.VersionTLS12} + + // in case of restart, we should use the same port as the first start in order not to break existing links + listener, err := tls.Listen(\"tcp\", s.getHost(), cfg) + if err != nil { + s.logger.Error(\"failed to start server, retrying\", zap.Error(err)) + s.ResetPort() + err = s.Start() + if err != nil { + s.logger.Error(\"server start failed, giving up\", zap.Error(err)) + } + return + } + + err = s.SetPort(listener.Addr().(*net.TCPAddr).Port) + if err != nil { + s.logger.Error(\"failed to set Server.port\", zap.Error(err)) + return + } + + if s.afterPortChanged != nil { + s.afterPortChanged(s.port) + } + s.run = true + + err = s.server.Serve(listener) + if err != http.ErrServerClosed { + s.logger.Error(\"server failed unexpectedly, restarting\", zap.Error(err)) + err = s.Start() + if err != nil { + s.logger.Error(\"server start failed, giving up\", zap.Error(err)) + } + return + } + + s.run = false +}") + +(def clojure-example + "(defn request->xhrio-options + [{:as request + :keys [on-success on-failure] + :or {on-success [:http-no-on-success] + on-failure [:http-no-on-failure]}}] + ; wrap events in cljs-ajax callback + (let [api (new goog.net.XhrIo)] + (-> request + (assoc + :api api + :handler (partial ajax-xhrio-handler + #(dispatch (conj on-success %)) + #(dispatch (conj on-failure %)) + api)) + (dissoc :on-success :on-failure :on-request))))") + +(def examples + {:clojure {:language :clojure + :text clojure-example} + :go {:language :go + :text go-example}}) + +(def descriptor + [{:key :language + :type :select + :options [{:key :clojure} + {:key :go}]} + {:key :max-lines + :type :select + :options (map (fn [n] + {:key n + :value (str n " lines")}) + (range 0 41 5))} + {:label "Syntax highlight?" + :key :syntax + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:language :clojure + :max-lines 40 + :syntax true})] + (fn [] + (let [language (if (:syntax @state) (:language @state) :text) + text (-> (:language @state) examples :text) + max-lines (as-> (:max-lines @state) max-lines + (js/parseInt max-lines) + (when-not (js/Number.isNaN max-lines) + max-lines))] + [preview/preview-container {:state state :descriptor descriptor} + [quo/snippet + {:language language + :max-lines max-lines + :on-copy-press #(js/alert %)} + text]])))) diff --git a/src/status_im/contexts/preview/quo/code/snippet_preview.cljs b/src/status_im/contexts/preview/quo/code/snippet_preview.cljs new file mode 100644 index 00000000000..14c14a9b082 --- /dev/null +++ b/src/status_im/contexts/preview/quo/code/snippet_preview.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.preview.quo.code.snippet-preview + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def go-example + "for(let ind") + +(def clojure-example + "(for [{:keys [url title description thumbnail hostname]} previews]") + +(def examples + {:clojure {:language :clojure + :text clojure-example} + :go {:language :go + :text go-example}}) + +(def descriptor + [{:key :language + :type :select + :options [{:key :clojure} + {:key :go}]} + {:label "Syntax highlight?" + :key :syntax + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:language :clojure + :max-lines 1 + :syntax true})] + (fn [] + (let [language (if (:syntax @state) (:language @state) :text) + text (-> (:language @state) examples :text)] + [preview/preview-container {:state state :descriptor descriptor} + [quo/snippet-preview + {:language language} text]])))) diff --git a/src/status_im/contexts/preview/quo/colors/color.cljs b/src/status_im/contexts/preview/quo/colors/color.cljs new file mode 100644 index 00000000000..2900865206b --- /dev/null +++ b/src/status_im/contexts/preview/quo/colors/color.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.preview.quo.colors.color + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [(preview/customization-color-option {:feng-shui? true}) + {:key :selected? + :type :boolean} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:selected? false + :customization-color :blue + :blur? false + :on-press #(js/alert "pressed")})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/color (assoc @state :color (:customization-color @state))]]))) diff --git a/src/status_im/contexts/preview/quo/colors/color_picker.cljs b/src/status_im/contexts/preview/quo/colors/color_picker.cljs new file mode 100644 index 00000000000..0d0f4718111 --- /dev/null +++ b/src/status_im/contexts/preview/quo/colors/color_picker.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.preview.quo.colors.color-picker + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? + :type :boolean}]) + +(defn view + [] + (let [default-selected :blue + selected (reagent/atom default-selected) + on-change #(reset! selected %) + state (reagent/atom {:customization-color :blue + :blur? false + :feng-shui? true})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [rn/view {:style {:padding-bottom 20}} + [quo/text (str "Selected color: " (name @selected))]] + [quo/color-picker + (assoc @state + :default-selected default-selected + :on-change on-change + :color (:customization-color @state))]]))) diff --git a/src/status_im/contexts/preview/quo/common.cljs b/src/status_im/contexts/preview/quo/common.cljs new file mode 100644 index 00000000000..21bc362349d --- /dev/null +++ b/src/status_im/contexts/preview/quo/common.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.preview.quo.common + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [utils.re-frame :as rf])) + + +(defn navigation-bar + [{:keys [title]}] + (let [theme (quo.context/use-theme) + logged-in? (rf/sub [:multiaccount/logged-in?]) + has-profiles? (boolean (rf/sub [:profile/profiles-overview])) + root (if has-profiles? :screen/profile.profiles :screen/onboarding.intro) + light? (= theme :light)] + [quo/page-nav + {:type :title + :title title + :text-align :left + :icon-name :i/close + :right-side [{:icon-name (if light? :i/dark :i/light) + :on-press #(if light? + (rf/dispatch [:theme/switch {:theme :dark}]) + (rf/dispatch [:theme/switch {:theme :light}]))}] + :on-press #(if (or logged-in? (not= (rf/sub [:view-id]) :screen/quo-preview)) + (rf/dispatch [:navigate-back]) + (do + (rf/dispatch [:theme/switch {:theme :dark}]) + (rf/dispatch [:update-theme-and-init-root root])))}])) diff --git a/src/status_im/contexts/preview/quo/community/channel_action.cljs b/src/status_im/contexts/preview/quo/community/channel_action.cljs new file mode 100644 index 00000000000..78a1da6a915 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/channel_action.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.preview.quo.community.channel-action + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :label + :type :text} + {:key :big? + :type :boolean} + {:key :counter-value + :type :number} + {:key :icon + :type :select + :options [{:key :i/muted} + {:key :i/pin}]} + (preview/customization-color-option) + {:key :disabled? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:label "Action" + :icon :i/muted + :customization-color :blue + :counter-value nil + :disabled? false + :big? true})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/channel-action @state]]))) diff --git a/src/status_im/contexts/preview/quo/community/channel_actions.cljs b/src/status_im/contexts/preview/quo/community/channel_actions.cljs new file mode 100644 index 00000000000..d076bc35f4e --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/channel_actions.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.preview.quo.community.channel-actions + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn view + [] + [preview/preview-container + [quo/channel-actions + {:actions [{:big? true + :label "Pinned Messages" + :customization-color :blue + :icon :i/pin + :counter-value 0}]}] + [rn/view {:height 50}] + [quo/channel-actions + {:actions [{:label "Pinned Messages" + :customization-color :blue + :icon :i/pin + :counter-value 5} + {:label "Mute chat" + :customization-color :blue + :icon :i/muted}]}] + [rn/view {:height 50}] + [quo/channel-actions + {:actions + [{:big? true + :label "Pinned Messages" + :customization-color :blue + :icon :i/pin + :counter-value 5} + {:label "Mute chat" + :customization-color :blue + :icon :i/muted}]}] + [rn/view {:height 50}] + [quo/channel-actions + {:actions + [{:big? true + :label "Pinned Messages sometimes can get very long and we have to take care of that" + :customization-color :blue + :icon :i/pin + :counter-value 5} + {:label "Mute chat" + :customization-color :blue + :icon :i/muted}]}] + [rn/view {:height 50}] + [quo/channel-actions + {:actions [{:label "Pinned Messages" + :customization-color :blue + :icon :i/pin + :counter-value 5} + {:label "Mute chat" + :customization-color :blue + :icon :i/muted} + {:label "Something else" + :customization-color :blue + :icon :i/placeholder}]}]]) diff --git a/src/status_im/contexts/preview/quo/community/community_card_view.cljs b/src/status_im/contexts/preview/quo/community/community_card_view.cljs new file mode 100644 index 00000000000..7b39ebf4fe8 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/community_card_view.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.preview.quo.community.community-card-view + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [utils.i18n :as i18n])) + +(def community-data + {:id "id" + :name "Status" + :description + "Status is a secure messaging app, crypto wallet and web3 browser built with the state of the art technology" + :cover (resources/get-mock-image :community-cover) + :community-icon (resources/get-mock-image :status-logo) + :customization-color :blue + :tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]}] + :tags [{:id 1 + :name (i18n/label :t/music) + :emoji (resources/get-image :music)} + {:id 2 + :name (i18n/label :t/lifestyle) + :emoji "🧩"} + {:id 3 + :name (i18n/label :t/podcasts) + :emoji "🎶"}]}) + +(def descriptor + [{:key :status + :type :select + :options [{:key :gated} + {:key :open}]} + {:key :locked? + :type :boolean} + {:key :loading? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:status :gated + :locked? true + :loading? false})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/community-card-view-item + {:community (merge @state community-data) + :loading? (:loading? @state)}]]))) diff --git a/src/status_im/contexts/preview/quo/community/community_detail_token_gating.cljs b/src/status_im/contexts/preview/quo/community/community_detail_token_gating.cljs new file mode 100644 index 00000000000..6b66bdb70e4 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/community_detail_token_gating.cljs @@ -0,0 +1,68 @@ +(ns status-im.contexts.preview.quo.community.community-detail-token-gating + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :role + :type :select + :options [{:key :one-role} + {:key :two-roles} + {:key :three-roles} + {:key :four-roles}]}]) + +(def roles + {:member + {:role 2 + :role-text "Member" + :satisfied? true + :tokens + [[{:symbol "ETH" :sufficient? true :collectible? false :loading? false :amount 0.8 :img-src nil}] + [{:symbol "ETH" :sufficient? false :collectible? false :loading? false :amount 1 :img-src nil} + {:symbol "STT" :sufficient? false :collectible? false :loading? false :amount 10 :img-src nil}]]} + :admin + {:role 1 + :role-text "Admin" + :satisfied? true + :tokens + [[{:symbol "ETH" :sufficient? true :collectible? false :loading? false :amount 2 :img-src nil}]]} + :token-master + {:role 5 + :role-text "Token Master" + :satisfied? true + :tokens + [[{:symbol "TMANI" + :sufficient? true + :collectible? true + :loading? false + :amount nil + :img-src (resources/mock-images :collectible1)}]]} + :token-owner + {:role 6 + :role-text "Token Owner" + :satisfied? true + :tokens + [[{:symbol "TOANI" + :sufficient? true + :collectible? true + :loading? false + :amount nil + :img-src (resources/mock-images :collectible)}]]}}) + +(def permissions + {:one-role [(:member roles)] + :two-roles [(:admin roles) (:member roles)] + :three-roles [(:token-master roles) (:admin roles) (:member roles)] + :four-roles [(:token-owner roles) (:token-master roles) (:admin roles) (:member roles)]}) + +(defn view + [] + (let [state (reagent/atom {:role :one-role})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/community-detail-token-gating {:permissions ((:role @state) permissions)}]]))) diff --git a/src/status_im/contexts/preview/quo/community/community_membership_list_view.cljs b/src/status_im/contexts/preview/quo/community/community_membership_list_view.cljs new file mode 100644 index 00000000000..a3c2579b4e8 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/community_membership_list_view.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.preview.quo.community.community-membership-list-view + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.community.data :as data] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :notifications + :type :select + :options [{:key :muted} + {:key :unread-mentions-count} + {:key :unread-messages-count}]} + {:key :status + :type :select + :options [{:key :gated} + {:key :open}]} + {:key :locked? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:locked? true + :notifications :muted + :status :gated})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/communities-membership-list-item {} + false + (cond-> (merge @state data/community) + (= :muted (:notifications @state)) + (assoc :muted? true) + + (= :unread-mentions-count (:notifications @state)) + (assoc :unread-mentions-count 5) + + (= :unread-messages-count (:notifications @state)) + (assoc :unread-messages? true))]]))) diff --git a/src/status_im/contexts/preview/quo/community/community_stat.cljs b/src/status_im/contexts/preview/quo/community/community_stat.cljs new file mode 100644 index 00000000000..212a9d8c4e7 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/community_stat.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.preview.quo.community.community-stat + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :value + :type :number} + {:key :icon + :type :select + :options [{:key :i/members} + {:key :i/active-members}]}]) + +(defn view + [] + (let [state (reagent/atom {:value 5000 + :icon :i/members})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/community-stat @state]]))) diff --git a/src/status_im/contexts/preview/quo/community/community_token_gating.cljs b/src/status_im/contexts/preview/quo/community/community_token_gating.cljs new file mode 100644 index 00000000000..4715d95e6b4 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/community_token_gating.cljs @@ -0,0 +1,61 @@ +(ns status-im.contexts.preview.quo.community.community-token-gating + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [utils.i18n :as i18n])) + +(def descriptor + [{:key :role + :type :select + :options [{:key :member} + {:key :admin} + {:key :token-master} + {:key :token-owner}]} + {:key :satisfied? + :type :boolean} + (preview/customization-color-option {:key :community-color})]) + +(def tokens + {:member + [[{:symbol "ETH" :sufficient? true :collectible? false :amount "0.8" :img-src nil}] + [{:symbol "ETH" :sufficient? false :collectible? false :amount "1" :img-src nil} + {:symbol "STT" :sufficient? false :collectible? false :amount "10" :img-src nil}]] + :admin + [[{:symbol "ETH" :sufficient? true :collectible? false :amount "2" :img-src nil}]] + :token-master + [[{:symbol "TMANI" + :sufficient? true + :collectible? true + :amount nil + :img-src (resources/mock-images :collectible1)}]] + :token-owner + [[{:symbol "TOANI" + :sufficient? true + :collectible? true + :amount nil + :img-src (resources/mock-images :collectible)}]]}) + +(def role + {:member (i18n/label :t/member) + :admin (i18n/label :t/admin) + :token-master (i18n/label :t/token-master) + :token-owner (i18n/label :t/token-owner)}) + +(defn view + [] + (let [state (reagent/atom {:role :member + :satisfied? true + :community-color :blue})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :show-blur-background? false} + [quo/community-token-gating + {:community-color (:community-color @state) + :role ((:role @state) role) + :satisfied? (:satisfied? @state) + :on-press #(js/alert "On press 'Request to join'") + :on-press-info #(js/alert "On press info") + :tokens ((:role @state) tokens)}]]))) diff --git a/src/status_im/contexts/preview/quo/community/data.cljs b/src/status_im/contexts/preview/quo/community/data.cljs new file mode 100644 index 00000000000..1d859db6207 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/data.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.preview.quo.community.data + (:require + [status-im.common.resources :as resources] + [utils.i18n :as i18n])) + +(def thumbnail + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII") + +(def community + {:id "0xsomeid" + :name "Status" + :description + "Status is a secure messaging app, crypto wallet and web3 browser built with the state of the art technology" + :customization-color :blue + :community-icon thumbnail + :tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]}] + :tags [{:id 1 :tag-label (i18n/label :t/music) :resource (resources/get-image :music)} + {:id 2 + :tag-label (i18n/label :t/lifestyle) + :resource (resources/get-image :lifestyle)} + {:id 3 + :tag-label (i18n/label :t/podcasts) + :resource (resources/get-image :podcasts)}]}) diff --git a/src/status_im/contexts/preview/quo/community/discover_card.cljs b/src/status_im/contexts/preview/quo/community/discover_card.cljs new file mode 100644 index 00000000000..c70b6bc2873 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/discover_card.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.preview.quo.community.discover-card + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title :type :text} + {:key :description :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:title "Discover" + :description "Your favourite communities" + :joined? false})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/discover-card @state]]))) diff --git a/src/status_im/contexts/preview/quo/community/list_item.cljs b/src/status_im/contexts/preview/quo/community/list_item.cljs new file mode 100644 index 00000000000..73bcfb8b499 --- /dev/null +++ b/src/status_im/contexts/preview/quo/community/list_item.cljs @@ -0,0 +1,109 @@ +(ns status-im.contexts.preview.quo.community.list-item + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.community.data :as data] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor-type + {:type :select + :key :type + :options [{:key :discover} + {:key :engage} + {:key :share}]}) + +(def descriptor-locked + {:type :boolean :key :locked?}) + +(def descriptor-unread-count + {:type :number :key :unread-count}) + +(def descriptor-title + {:type :text :key :title}) + +(def descriptor-blur + {:type :boolean :key :blur?}) + +(def descriptor-member-stats + [{:type :number :key :members-count} + {:type :number :key :active-count}]) + +(def descriptors-base + [descriptor-type + descriptor-title + (preview/customization-color-option)]) + +(def descriptors-type-discover + (conj descriptors-base + {:type :select + :key :info + :options [{:key :token-gated} + {:key :default}]} + {:label "Member stats?" + :type :boolean + :key :members?})) + +(def descriptors-type-engage + (conj descriptors-base + {:type :select + :key :info + :options [{:key :notification} + {:key :mention} + {:key :muted} + {:key :token-gated} + {:key :navigation} + {:key :default}]})) + +(def descriptors-type-share + (conj descriptors-base {:type :text :key :subtitle})) + +(defn descriptors + [{:keys [members? info] :as state} theme] + (let [descs (case (:type state) + :discover (cond-> descriptors-type-discover + members? + (into descriptor-member-stats) + + (= info :token-gated) + (into [descriptor-locked])) + :engage (cond-> descriptors-type-engage + (= info :token-gated) + (into [descriptor-locked]) + + (= info :mention) + (into [descriptor-unread-count])) + :share descriptors-type-share + nil)] + (if (= :dark theme) + (into [descriptor-blur] descs) + descs))) + +(defn view + [] + (let [state (reagent/atom {:blur? false + :customization-color :blue + :info :token-gated + :type :discover + :members? false + :locked? false + :title "Status" + :subtitle "Subtitle" + :members-count 629200 + :active-count 112100 + :unread-count 5})] + (fn [] + (let [theme (quo.context/use-theme)] + [preview/preview-container {:state state :descriptor (descriptors @state theme)} + [quo/community-list + (merge @state + {:container-style {:width 335} + :logo (resources/get-mock-image :status-logo) + :tokens (:tokens data/community) + :on-press #(js/alert "List item pressed") + :on-long-press #(js/alert "Long pressed item") + :on-press-info #(js/alert "Info pressed") + :members (when (:members? @state) + {:members-count (:members-count @state) + :active-count (:active-count @state)})})]])))) diff --git a/src/status_im/contexts/preview/quo/component_preview/events.cljs b/src/status_im/contexts/preview/quo/component_preview/events.cljs new file mode 100644 index 00000000000..f2bcc096a36 --- /dev/null +++ b/src/status_im/contexts/preview/quo/component_preview/events.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.preview.quo.component-preview.events + (:require + [quo.core :as quo] + [re-frame.core :as re-frame])) + +(def preview-screen-name :screen/dev-component-preview) + +(when js/goog.DEBUG + (re-frame/reg-event-fx :dev/preview-component + (fn [{:keys [db]} [[component-tag props & args]]] + (let [view-id (:view-id db) + ;; NOTE: re-render on every evaluation e.g. reset component state without changes + component (into [component-tag (assoc props :key (random-uuid))] args) + navigate? (not= view-id preview-screen-name)] + (cond-> {:db (assoc db :dev/previewed-component component)} + navigate? (assoc :fx [[:dispatch [:navigate-to preview-screen-name]]])))))) + +;; Usage example +(comment + (re-frame/dispatch [:dev/preview-component + [quo/slide-button + {:track-icon :face-id + :track-text ":debug-component example" + :customization-color :blue + :on-complete identity}]])) diff --git a/src/status_im/contexts/preview/quo/component_preview/view.cljs b/src/status_im/contexts/preview/quo/component_preview/view.cljs new file mode 100644 index 00000000000..17d164fe4fc --- /dev/null +++ b/src/status_im/contexts/preview/quo/component_preview/view.cljs @@ -0,0 +1,16 @@ +(ns status-im.contexts.preview.quo.component-preview.view + (:require + [react-native.core :as rn] + [utils.re-frame :as rf])) + +(def ^:private container-style + {:flex 1 + :padding-horizontal 20 + :padding-top 80 + :align-items :center}) + +(defn view + [] + (let [component (rf/sub [:dev/previewed-component])] + [rn/view {:style container-style} + (when component component)])) diff --git a/src/status_im/contexts/preview/quo/counter/collectible_counter.cljs b/src/status_im/contexts/preview/quo/counter/collectible_counter.cljs new file mode 100644 index 00000000000..2430036d2fb --- /dev/null +++ b/src/status_im/contexts/preview/quo/counter/collectible_counter.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.preview.quo.counter.collectible-counter + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :status + :type :select + :options [{:key :default} + {:key :error}]} + {:key :size + :type :select + :options [{:key :size-32} + {:key :size-24}]} + {:key :value + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:value "x500" + :status :default + :size :size-32})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :show-blur-background? true + :blur? true} + [quo/collectible-counter @state]]))) diff --git a/src/status_im/contexts/preview/quo/counter/counter.cljs b/src/status_im/contexts/preview/quo/counter/counter.cljs new file mode 100644 index 00000000000..c721654841a --- /dev/null +++ b/src/status_im/contexts/preview/quo/counter/counter.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.preview.quo.counter.counter + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :default} + {:key :secondary} + {:key :grey} + {:key :outline}]} + {:key :value + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:value "5" + :type :default})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/counter @state (:value @state)]]))) diff --git a/src/status_im/contexts/preview/quo/counter/step.cljs b/src/status_im/contexts/preview/quo/counter/step.cljs new file mode 100644 index 00000000000..f7e7bb3d7ea --- /dev/null +++ b/src/status_im/contexts/preview/quo/counter/step.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.counter.step + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :neutral} + {:key :active} + {:key :complete}]} + {:key :in-blur-view? + :type :boolean} + {:key :value + :type :text}]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:value "5" + :type :neutral + :in-blur-view? false})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor + :blur? (:in-blur-view? state) + :show-blur-background? (:in-blur-view? state)} + [quo/step (dissoc state :value) (:value state)]])) diff --git a/src/status_im/contexts/preview/quo/dividers/date.cljs b/src/status_im/contexts/preview/quo/dividers/date.cljs new file mode 100644 index 00000000000..4d330d958a8 --- /dev/null +++ b/src/status_im/contexts/preview/quo/dividers/date.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.preview.quo.dividers.date + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :label :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:label "Today"})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/divider-date (@state :label)]]))) diff --git a/src/status_im/contexts/preview/quo/dividers/divider_label.cljs b/src/status_im/contexts/preview/quo/dividers/divider_label.cljs new file mode 100644 index 00000000000..e080f367bba --- /dev/null +++ b/src/status_im/contexts/preview/quo/dividers/divider_label.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.preview.quo.dividers.divider-label + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :label + :type :text} + {:key :chevron + :type :select + :options [{:key :left + :value "Left"} + {:key :right + :value "Right"} + {:key nil + :value "None"}]} + {:key :chevron-icon + :type :select + :options [{:key :i/chevron-down + :value "Chevron Down"} + {:key :i/chevron-right + :value "Chevron Right"}]} + {:key :tight? + :type :boolean} + {:key :counter? + :type :boolean} + {:key :counter-value + :type :text} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:label "Welcome" + :chevron nil + :chevron-icon nil + :tight? true + :counter? false + :counter-value 0 + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/divider-label + (assoc @state + :on-press + #(js/alert "Divider label pressed!")) + (:label @state)]]))) diff --git a/src/status_im/contexts/preview/quo/dividers/divider_line.cljs b/src/status_im/contexts/preview/quo/dividers/divider_line.cljs new file mode 100644 index 00000000000..17aeb306cdb --- /dev/null +++ b/src/status_im/contexts/preview/quo/dividers/divider_line.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.preview.quo.dividers.divider-line + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/divider-line @state]]))) diff --git a/src/status_im/contexts/preview/quo/dividers/new_messages.cljs b/src/status_im/contexts/preview/quo/dividers/new_messages.cljs new file mode 100644 index 00000000000..9348c06eda8 --- /dev/null +++ b/src/status_im/contexts/preview/quo/dividers/new_messages.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.preview.quo.dividers.new-messages + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :label + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:label "New messages" + :color :primary})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/new-messages @state]]))) diff --git a/src/status_im/contexts/preview/quo/dividers/strength_divider.cljs b/src/status_im/contexts/preview/quo/dividers/strength_divider.cljs new file mode 100644 index 00000000000..a9542b36a08 --- /dev/null +++ b/src/status_im/contexts/preview/quo/dividers/strength_divider.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.dividers.strength-divider + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :very-weak} + {:key :weak} + {:key :okay} + {:key :strong} + {:key :very-strong} + {:key :alert} + {:key :info}]} + {:label "Text (only works for info/alert)" + :key :text + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:text "Common password, shouldn’t be used" + :type :alert})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/strength-divider + {:type (:type @state)} + (:text @state)]]))) diff --git a/src/status_im/contexts/preview/quo/drawers/action_drawers.cljs b/src/status_im/contexts/preview/quo/drawers/action_drawers.cljs new file mode 100644 index 00000000000..58eaab431db --- /dev/null +++ b/src/status_im/contexts/preview/quo/drawers/action_drawers.cljs @@ -0,0 +1,70 @@ +(ns status-im.contexts.preview.quo.drawers.action-drawers + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.re-frame :as rf])) + +(def descriptor + [{:key :muted? + :type :boolean} + {:key :mark-as-read-disabled? + :type :boolean} + {:key :show-red-options? + :type :boolean} + {:key :theme + :type :select + :options [{:key :dark} + {:key :light} + {:key nil :value "System"}]}]) + +(def options-with-consequences + [{:icon :i/delete + :danger? true + :label "Clear history" + :add-divider? true + :on-press #(js/alert "clear history")}]) + +(defn action-sheet + [state] + [quo/action-drawer + (cond-> + [[{:icon :i/friend + :label "View channel members and details" + :on-press #(js/alert "View channel members and details")} + {:icon :i/communities + :label "Mark as read" + :disabled? (:mark-as-read-disabled? @state) + :on-press #(js/alert "Mark as read")} + {:icon :i/muted + :label (if (:muted? @state) "Unmute channel" "Mute channel") + :on-press #(js/alert (if (:muted? @state) "Unmute channel" "Mute channel")) + :right-icon :i/chevron-right + :sub-label (when (:muted? @state) "Muted for 15 min")} + {:icon :i/scan + :on-press #(js/alert "Fetch messages") + :right-icon :i/chevron-right + :right-text "3" + :label "Fetch messages"} + {:icon :i/add-user + :on-press #(js/alert "Share link to the channel") + :label "Share link to the channel"}]] + + (:show-red-options? @state) + (conj options-with-consequences))]) + +(defn view + [] + (let [state (reagent/atom {:muted? true + :show-red-options? true})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/button + {:container-style {:margin-horizontal 40} + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [action-sheet state]) + :theme (:theme @state)}])} + "See in bottom sheet"] + [rn/view {:padding-vertical 60} + [action-sheet state]]]))) diff --git a/src/status_im/contexts/preview/quo/drawers/bottom_actions.cljs b/src/status_im/contexts/preview/quo/drawers/bottom_actions.cljs new file mode 100644 index 00000000000..882471bb67e --- /dev/null +++ b/src/status_im/contexts/preview/quo/drawers/bottom_actions.cljs @@ -0,0 +1,118 @@ +(ns status-im.contexts.preview.quo.drawers.bottom-actions + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def button-two "Cancel") +(def button-one "Request to join") +(def description "Joining the community will reveal your public addresses to the node owner") +(def button-options + [{:key :primary} + {:key :grey} + {:key :danger} + {:key :positive}]) +(defn button-press + [id] + #(js/alert (str "Button " id " Pressed"))) + +(def descriptor + [{:type :select + :key :actions + :options [{:key :one-action} + {:key :two-actions}]} + {:key :button-one-label + :type :text} + {:label "Button 1 type" + :type :select + :key :type + :options button-options + :path [:button-one-props]} + {:label "Button 1 disabled?" + :type :boolean + :key :disabled? + :path [:button-one-props]} + {:key :button-two-label + :type :text} + {:label "Button 2 type" + :type :select + :key :type + :options button-options + :path [:button-two-props]} + {:label "Button 2 disabled?" + :type :boolean + :key :disabled? + :path [:button-two-props]} + {:key :blur? + :type :boolean} + {:key :scroll? + :type :boolean} + {:key :description + :type :select + :options [{:key nil + :value :none} + {:key :top} + {:key :bottom} + {:key :top-error}]}]) + +(def role-descriptor + {:key :role + :type :select + :options [{:key nil + :value :none} + {:key :owner} + {:key :token-master} + {:key :admin} + {:key :member}]}) + +(def description-top-descriptor + {:key :description-top-text + :type :text}) + +(def description-descriptor + {:key :description-text + :type :text}) + +(def error-descriptor + {:key :error-message + :type :text}) + +(defn view + [] + (let [[state set-state] (rn/use-state + {:actions :two-actions + :description :bottom + :description-text description + :description-top-text "Eligible to join as" + :error-message "Error message" + :button-one-label button-one + :button-two-label button-two + :button-one-props {:on-press (button-press 1) + :type :primary} + :button-two-props {:on-press (button-press 2) + :type :grey} + :blur? false + :role nil + :context-tag-props {:size 24 + :type :token + :token "USDT" + :amount "99.97"} + :scroll? false})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor (cond-> descriptor + (= (:description state) :top) + (conj role-descriptor description-top-descriptor) + + (= (:description state) :bottom) + (conj description-descriptor) + + (= (:description state) :top-error) + (conj error-descriptor)) + :blur? (:blur? state) + :show-blur-background? true + :blur-dark-only? true + :component-container-style {:margin-top 40 + :padding-horizontal 0}} + [quo/bottom-actions state]])) diff --git a/src/status_im/contexts/preview/quo/drawers/documentation_drawers.cljs b/src/status_im/contexts/preview/quo/drawers/documentation_drawers.cljs new file mode 100644 index 00000000000..ee4a12a85c1 --- /dev/null +++ b/src/status_im/contexts/preview/quo/drawers/documentation_drawers.cljs @@ -0,0 +1,110 @@ +(ns status-im.contexts.preview.quo.drawers.documentation-drawers + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.re-frame :as rf])) + +(def descriptor + [{:key :title + :type :text} + {:key :show-button? + :type :boolean} + {:key :button-label + :type :text} + {:key :shell? + :type :boolean}]) + +(defn documentation-content + [theme] + [quo/text {:style {:color (colors/theme-colors colors/neutral-100 colors/white theme)}} + "Group chats are conversations of more than two people. To invite someone to a group chat, you need to have them on your Status contact list."]) + +(defn documentation-content-full + [theme] + (let [text-color (colors/theme-colors colors/neutral-100 colors/white theme) + text-style {:color text-color :margin-bottom 10}] + [rn/view + [quo/text {:style text-style} + "Group chats are conversations of more than two people. To invite someone to a group chat, you need to have them on your Status contact list."] + [quo/text {:style text-style} + "Group chats are different to communities, as they're meant to unite smaller groups of people or be centred around specific topics. For more information about group chats, see Understand group chats."] + [quo/text {:size :paragraph-1 :weight :semi-bold :style {:margin-top 16 :color text-color}} + "What to expect"] + [quo/text {:style text-style} + "You can invite up to 20 members to your group chat. If you need more, consider creating a community."] + [quo/text {:style text-style} + "Once you create your group chat, you can customize it and add members, as well as remove them."] + [quo/text {:style text-style} + "Group chats are always end-to-end encrypted with secure cryptographic keys. Only the group chat members will have access to the messages in it. Status doesn't have the keys and can't access any messages by design."] + [quo/text {:size :paragraph-1 :weight :semi-bold :style {:margin-top 16 :color text-color}} + "What to expect"] + [quo/text {:style text-style} + "You can invite up to 20 members to your group chat. If you need more, consider creating a community."] + [quo/text {:style text-style} + "Once you create your group chat, you can customize it and add members, as well as remove them."] + [quo/text {:style text-style} + "Group chats are always end-to-end encrypted with secure cryptographic keys. Only the group chat members will have access to the messages in it. Status doesn't have the keys and can't access any messages by design."] + [quo/text {:size :paragraph-1 :weight :semi-bold :style {:margin-top 16 :color text-color}} + "What to expect"] + [quo/text {:style text-style} + "You can invite up to 20 members to your group chat. If you need more, consider creating a community."] + [quo/text {:style text-style} + "Once you create your group chat, you can customize it and add members, as well as remove them."] + [quo/text {:style text-style} + "Group chats are always end-to-end encrypted with secure cryptographic keys. Only the group chat members will have access to the messages in it. Status doesn't have the keys and can't access any messages by design."] + [quo/text {:size :paragraph-1 :weight :semi-bold :style {:margin-top 16 :color text-color}} + "What to expect"] + [quo/text {:style text-style} + "You can invite up to 20 members to your group chat. If you need more, consider creating a community."] + [quo/text {:style text-style} + "Once you create your group chat, you can customize it and add members, as well as remove them."] + [quo/text {:style text-style} + "Group chats are always end-to-end encrypted with secure cryptographic keys. Only the group chat members will have access to the messages in it. Status doesn't have the keys and can't access any messages by design."] + [quo/text {:size :paragraph-1 :weight :semi-bold :style {:margin-top 16 :color text-color}} + "What to expect"] + [quo/text {:style text-style} + "You can invite up to 20 members to your group chat. If you need more, consider creating a community."] + [quo/text {:style text-style} + "Once you create your group chat, you can customize it and add members, as well as remove them."] + [quo/text {:style text-style} + "Group chats are always end-to-end encrypted with secure cryptographic keys. Only the group chat members will have access to the messages in it. Status doesn't have the keys and can't access any messages by design."]])) + +(defn documentation-drawer + [title show-button? button-label expanded? shell?] + [quo/documentation-drawers + {:title title + :show-button? (and show-button? (not @expanded?)) + :button-label button-label + :button-icon :info + :shell? shell? + :on-press-button #(swap! expanded? not)} + (if @expanded? + [documentation-content-full (when shell? :dark)] + [documentation-content (when shell? :dark)])]) + +(defn view + [] + (let [state (reagent/atom {:title "Create a group chat" + :button-label "Read more"}) + title (reagent/cursor state [:title]) + show-button? (reagent/cursor state [:show-button?]) + button-label (reagent/cursor state [:button-label]) + shell? (reagent/cursor state [:shell?]) + expanded? (reagent/atom false)] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/button + {:container-style {:margin-horizontal 40 + :margin-bottom 20} + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (constantly [documentation-drawer @title + @show-button? + @button-label expanded? @shell?]) + :expandable? @show-button? + :shell? @shell? + :expanded? @expanded?}])} + "Open drawer"] + [documentation-drawer @title @show-button? @button-label expanded?]]))) diff --git a/src/status_im/contexts/preview/quo/drawers/drawer_action.cljs b/src/status_im/contexts/preview/quo/drawers/drawer_action.cljs new file mode 100644 index 00000000000..de54523bfc5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/drawers/drawer_action.cljs @@ -0,0 +1,45 @@ +(ns status-im.contexts.preview.quo.drawers.drawer-action + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title + :type :text} + {:key :state + :type :select + :options [{:key :selected}]} + {:key :icon + :type :select + :options [{:key :i/placeholder} + {:key :i/info} + {:key :i/browser}]} + {:key :action + :type :select + :options [{:key :arrow} + {:key :toggle} + {:key :input}]} + {:key :description + :type :text} + {:key :blur? + :type :boolean} + (preview/customization-color-option)]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:title "Action" + :description "This is a description" + :customization-color :blue + :on-press #(js/alert "Pressed!") + :input-props {:placeholder "Type something" + :right-icon {:icon-name :i/placeholder + :style-fn identity}}})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor + :blur? (:blur? state) + :show-blur-background? true + :blur-dark-only? true} + [quo/drawer-action state]])) diff --git a/src/status_im/contexts/preview/quo/drawers/drawer_buttons.cljs b/src/status_im/contexts/preview/quo/drawers/drawer_buttons.cljs new file mode 100644 index 00000000000..a9441fcdae5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/drawers/drawer_buttons.cljs @@ -0,0 +1,51 @@ +(ns status-im.contexts.preview.quo.drawers.drawer-buttons + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :top-heading + :type :text} + {:key :top-sub-heading + :type :text} + {:key :bottom-heading + :type :text}]) + +(defn text-with-link + [] + [quo/text + {:style {:flex 1 + :flex-wrap :wrap}} + [quo/text + {:size :paragraph-2 + :style {:flex 1 + :color (colors/alpha colors/white 0.7)} + :weight :semi-bold} + "By continuing you accept our "] + [quo/text + {:on-press #(js/alert "Terms of use clicked") + :size :paragraph-2 + :style {:flex 1 + :color colors/white} + :weight :semi-bold} + "Terms of Use"]]) + +(defn view + [] + (let [state (reagent/atom {:top-heading "Sign in" + :top-sub-heading "You already use Status" + :bottom-heading "I'm new to Status"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:margin-top 40}} + [quo/drawer-buttons + {:top-card {:on-press #(js/alert "top card clicked") + :heading (:top-heading @state)} + :bottom-card {:on-press #(js/alert "bottom card clicked") + :heading (:bottom-heading @state)}} + (:top-sub-heading @state) + [text-with-link]]]))) diff --git a/src/status_im/contexts/preview/quo/drawers/drawer_top.cljs b/src/status_im/contexts/preview/quo/drawers/drawer_top.cljs new file mode 100644 index 00000000000..63010c4feb7 --- /dev/null +++ b/src/status_im/contexts/preview/quo/drawers/drawer_top.cljs @@ -0,0 +1,75 @@ +(ns status-im.contexts.preview.quo.drawers.drawer-top + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [status-im.contexts.profile.utils :as profile.utils] + [utils.re-frame :as rf])) + +(def descriptor + [{:type :select + :key :type + :options [{:key :default} + {:key :default-keypair} + {:key :account} + {:key :keypair} + {:key :info} + {:key :context-tag} + {:key :label}]} + {:type :select + :key :button-icon + :options [{:key :i/placeholder} + {:key nil + :value "null"}]} + {:key :blur? + :type :boolean} + {:key :stored + :type :select + :options [{:key :on-device} + {:key :on-keycard} + {:key :missing}]} + {:key :title + :type :text} + {:key :description + :type :text} + {:key :community-name + :type :text} + {:key :label + :type :text}]) + +(defn view + [] + (let [profile (rf/sub [:profile/profile-with-image]) + state (reagent/atom + {:blur? false + :title "Title" + :type :default + :label "Drawer label" + :stored :on-device + :networks [{:network-name :ethereum :short-name "eth"}] + :description "0x62b...0a5" + :button-icon :i/placeholder + :community-name "Coinbase" + :community-logo (resources/mock-images :coinbase) + :account-avatar-emoji "🍿" + :customization-color :purple + :icon-avatar :i/placeholder + :on-button-press #(js/alert "on press") + :on-button-long-press #(js/alert "on long press") + :profile-picture (profile.utils/photo profile)})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/button + {:container-style {:margin-horizontal 40} + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [quo/drawer-top @state]) + :theme (:theme @state)}])} + "See in bottom sheet"] + [rn/view {:style {:margin-top 20}} + [quo/drawer-top @state]]]))) diff --git a/src/status_im/contexts/preview/quo/drawers/permission_drawers.cljs b/src/status_im/contexts/preview/quo/drawers/permission_drawers.cljs new file mode 100644 index 00000000000..44a80bbe211 --- /dev/null +++ b/src/status_im/contexts/preview/quo/drawers/permission_drawers.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.preview.quo.drawers.permission-drawers + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :action} + {:key :single-token-gating} + {:key :multiple-token-gating}]} + {:key :blur? + :type :boolean}]) + +(def single-token-gating-props + [{:token-value "50" + :token-symbol "SNT"} + {:token-value "0.01" + :token-symbol "ETH"}]) + +(def multiple-token-gating-props + [{:token-groups [[:eth :knc :snt :rare :mana]]} + {:token-groups [[:snt :eth :knc] + [:eth :knc :snt :rare :mana]]} + {:token-groups [[:snt :eth :knc :rare :mana :snt] + [:eth :knc :snt :rare :mana] + [:rare :mana]]}]) + +(def action-props + [{:action-label "You sure you know this guy?" + :action-icon :i/pending-state} + {:action-label "Join community to post" + :action-icon :i/communities}]) + +(defn view + [] + (let [state (reagent/atom {:type :multiple-token-gating + :blur? true}) + blur? (reagent/cursor state [:blur?]) + type (reagent/cursor state [:type])] + (fn [] + [preview/preview-container + {:state state + :blur? @blur? + :show-blur-background? true + :component-container-style {:padding-horizontal 0} + :blur-container-style {:padding-horizontal 0} + :descriptor descriptor} + (->> (condp = @type + :action action-props + :single-token-gating single-token-gating-props + :multiple-token-gating multiple-token-gating-props) + (map-indexed (fn [idx props] + [:<> + ^{:key idx} + [quo/permission-context + (merge @state props {:container-style {:margin-bottom 8}})]])))]))) diff --git a/src/status_im/contexts/preview/quo/dropdowns/dropdown.cljs b/src/status_im/contexts/preview/quo/dropdowns/dropdown.cljs new file mode 100644 index 00000000000..a77ddbf3f7b --- /dev/null +++ b/src/status_im/contexts/preview/quo/dropdowns/dropdown.cljs @@ -0,0 +1,83 @@ +(ns status-im.contexts.preview.quo.dropdowns.dropdown + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :outline} + {:key :grey} + {:key :ghost} + {:key :customization}]} + {:key :state + :type :select + :options [{:key :default} + {:key :active} + {:key :disabled}]} + {:key :size + :type :select + :options [{:key :size-40 + :value "40"} + {:key :size-32 + :value "32"} + {:key :size-24 + :value "24"}]} + {:key :background + :type :select + :options [{:key :photo} + {:key :blur}]} + {:key :icon? + :type :boolean} + {:key :icon-name + :type :select + :options [{:key :i/wallet} + {:key :i/group} + {:key :i/locked}]} + {:key :emoji? + :type :boolean} + {:key :label + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:type :outline + :state :default + :size :size-40 + :label "Dropdown" + :icon? false + :emoji? false + :customization-color :purple}) + label (reagent/cursor state [:label]) + emoji? (reagent/cursor state [:emoji?]) + background (reagent/cursor state [:background])] + [:f> + (fn [] + (rn/use-effect (fn [] + (swap! state assoc :label (if @emoji? "🍑" "Dropdown"))) + [@emoji?]) + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style (when-not (= @background :blur) {:align-items :center}) + :blur-container-style {:align-items :center} + :blur? (= @background :blur) + :show-blur-background? true} + (when (= :photo (:background @state)) + [rn/image + {:source (resources/get-mock-image :dark-blur-bg) + :style {:position :absolute + :top 12 + :left 20 + :right 0 + :bottom 0 + :border-radius 12} + :height 250 + :width "100%"}]) + [quo/dropdown + (assoc @state :on-press #(js/alert "Pressed dropdown")) + @label]])])) diff --git a/src/status_im/contexts/preview/quo/dropdowns/dropdown_input.cljs b/src/status_im/contexts/preview/quo/dropdowns/dropdown_input.cljs new file mode 100644 index 00000000000..cb6eb38ce33 --- /dev/null +++ b/src/status_im/contexts/preview/quo/dropdowns/dropdown_input.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.preview.quo.dropdowns.dropdown-input + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :state + :type :select + :options [{:key :default} + {:key :active} + {:key :disabled}]} + {:key :icon? + :type :boolean} + {:key :label? + :type :boolean} + {:key :blur? + :type :boolean} + {:key :header-label + :type :text} + {:key :label + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:state :default + :label "Dropdown" + :header-label "Label" + :icon? true + :label? true + :blur? false}) + label (reagent/cursor state [:label]) + blur? (reagent/cursor state [:blur?])] + [:f> + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style (when-not @blur? + {:align-items :center + :margin-horizontal 30}) + :blur-container-style {:align-items :center} + :blur? @blur? + :show-blur-background? true} + [quo/dropdown-input + (assoc @state :on-press #(js/alert "Pressed dropdown")) + @label]])])) diff --git a/src/status_im/contexts/preview/quo/dropdowns/network_dropdown.cljs b/src/status_im/contexts/preview/quo/dropdowns/network_dropdown.cljs new file mode 100644 index 00000000000..9e618102f06 --- /dev/null +++ b/src/status_im/contexts/preview/quo/dropdowns/network_dropdown.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.preview.quo.dropdowns.network-dropdown + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :state + :type :select + :options [{:key :default} + {:key :disabled}]} + {:key :blur? + :type :select + :options [{:key true} + {:key false}]} + {:key :amount + :type :select + :options [{:key 1} + {:key 2} + {:key 3}]} + {:key :dropdown-icon? + :type :select + :options [{:key true} + {:key false}]} + {:key :label + :type :text}]) + +(def ^:private networks-list + [{:source (quo.resources/get-network :ethereum)} + {:source (quo.resources/get-network :optimism)} + {:source (quo.resources/get-network :arbitrum)}]) + +(defn view + [] + (let [component-state (reagent/atom {:state :default + :blur? false + :amount 3 + :dropdown-icon? false + :label nil})] + (fn [] + [preview/preview-container + {:state component-state + :descriptor descriptor + :blur? (:blur? @component-state) + :show-blur-background? true} + [rn/view {:style {:align-self :center}} + [quo/network-dropdown + (merge {:on-press #(js/alert "Dropdown pressed")} + @component-state) + (take (:amount @component-state) networks-list)]]]))) diff --git a/src/status_im/contexts/preview/quo/empty_state/empty_state.cljs b/src/status_im/contexts/preview/quo/empty_state/empty_state.cljs new file mode 100644 index 00000000000..04b62e48adc --- /dev/null +++ b/src/status_im/contexts/preview/quo/empty_state/empty_state.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.preview.quo.empty-state.empty-state + (:require + [quo.context] + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title + :type :text} + {:key :description + :type :text} + {:key :image + :type :select + :options [{:key :cat-in-box} + {:key :no-contacts}]} + {:key :upper-button-text + :type :text} + {:key :lower-button-text + :type :text} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [theme (quo.context/use-theme) + state (reagent/atom {:image :no-contacts + :title "A big friendly title" + :description "Some cool description will be here" + :blur? false + :upper-button-text "Send community link" + :lower-button-text "Invite friends to Status"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :blur-height 300 + :show-blur-background? true} + [quo/empty-state + (-> @state + (assoc :upper-button + {:text (:upper-button-text @state) + :on-press #(js/alert "Upper button")}) + (assoc :lower-button + {:text (:lower-button-text @state) + :on-press #(js/alert "Lower button")}) + (assoc :image (resources/get-themed-image (:image @state) theme)))]]))) diff --git a/src/status_im/contexts/preview/quo/foundations/gradients.cljs b/src/status_im/contexts/preview/quo/foundations/gradients.cljs new file mode 100644 index 00000000000..5a3e0fdc041 --- /dev/null +++ b/src/status_im/contexts/preview/quo/foundations/gradients.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.preview.quo.foundations.gradients + (:require [quo.foundations.gradients :as quo.gradients] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def ^:private descriptor + [{:key :color-index + :type :select + :options [{:key :gradient-1} + {:key :gradient-2} + {:key :gradient-3} + {:key :gradient-4} + {:key :gradient-5}]}]) + +(defn view + [] + (let [state (reagent/atom {:color-index 1})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? (:blur? @state)} + [quo.gradients/view + {:container-style {:align-self :center + :height 200 + :width 200} + :color-index (:color-index @state)}]]))) diff --git a/src/status_im/contexts/preview/quo/foundations/shadows.cljs b/src/status_im/contexts/preview/quo/foundations/shadows.cljs new file mode 100644 index 00000000000..bce291d76cc --- /dev/null +++ b/src/status_im/contexts/preview/quo/foundations/shadows.cljs @@ -0,0 +1,61 @@ +(ns status-im.contexts.preview.quo.foundations.shadows + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [quo.foundations.shadows :as shadows] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn demo-box + [shadow? description shadow-style theme] + [rn/view + {:style {:margin-left :auto + :margin-right :auto + :margin-top 8 + :margin-bottom 8 + :align-items :center}} + [quo/text {} description] + [rn/view + {:style (merge {:width 60 + :height 60 + :border-radius 16 + :background-color (colors/theme-colors colors/white colors/neutral-90 theme)} + (when shadow? shadow-style))}]]) + +(def descriptor + [{:key :shadow? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:shadow? true}) + shadow? (reagent/cursor state [:shadow?]) + theme (quo.context/use-theme)] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/text + {:style {:margin-left :auto + :margin-right :auto + :align-items :center}} + "Normal Scales"] + [demo-box @shadow? "Shadow 1" (shadows/get 1 theme) theme] + [demo-box @shadow? "Shadow 2" (shadows/get 2 theme) theme] + [demo-box @shadow? "Shadow 3" (shadows/get 3 theme) theme] + [demo-box @shadow? "Shadow 4" (shadows/get 4 theme) theme] + [quo/text + {:style {:margin-left :auto + :margin-right :auto + :align-items :center}} + "Inverted Scales"] + [demo-box @shadow? "Shadow 1" (shadows/get 1 theme :inverted) theme] + [demo-box @shadow? "Shadow 2" (shadows/get 2 theme :inverted) theme] + [demo-box @shadow? "Shadow 3" (shadows/get 3 theme :inverted) theme] + [demo-box @shadow? "Shadow 4" (shadows/get 4 theme :inverted) theme] + [quo/text + {:style {:margin-left :auto + :margin-right :auto + :align-items :center}} + "Inverted Scales"] + [demo-box @shadow? "Inner Shadow" shadows/inner-shadow]]))) diff --git a/src/status_im/contexts/preview/quo/gradient/gradient_cover.cljs b/src/status_im/contexts/preview/quo/gradient/gradient_cover.cljs new file mode 100644 index 00000000000..e3566205f5b --- /dev/null +++ b/src/status_im/contexts/preview/quo/gradient/gradient_cover.cljs @@ -0,0 +1,78 @@ +(ns status-im.contexts.preview.quo.gradient.gradient-cover + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [utils.re-frame :as rf])) + +(defn render-action-sheet + [customization-color] + [:<> + [quo/drawer-top + {:type :account + :blur? false + :title "Collectibles vault" + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}] + :description "0x0ah...78b" + :account-avatar-emoji "🍿" + :customization-color (or customization-color :blue)}] + [quo/action-drawer + [[{:icon :i/edit + :label "Edit account" + :on-press #(js/alert "Edit account")} + {:icon :i/copy + :label "Copy address" + :on-press #(js/alert "Copy address")} + {:icon :i/share + :label "Share account" + :on-press #(js/alert "Share account")} + {:icon :i/delete + :label "Remove account" + :danger? true + :on-press #(js/alert "Remove account") + :add-divider? true}]]]]) + +(def descriptor + [(preview/customization-color-option) + {:label "Blur (dark only)?" + :key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:customization-color :blue :blur? false}) + blur? (reagent/cursor state [:blur?]) + customization-color (reagent/cursor state [:customization-color])] + [:f> + (fn [] + (rn/use-effect (fn [] + (when @blur? + (rf/dispatch [:theme/switch {:theme :dark}]))) + [@blur?]) + [preview/preview-container {:state state :descriptor descriptor} + [rn/view + {:style {:height 332 + :margin-top 24 + :overflow :hidden + :border-radius 12}} + (when @blur? + [rn/image + {:style {:height 332} + :source (resources/get-mock-image :dark-blur-bg)}]) + [(if @blur? quo/blur rn/view) + {:style {:height 332 + :padding-vertical 40} + :blur-type :dark} + [quo/gradient-cover @state]]] + [quo/button + {:container-style {:margin-horizontal 40} + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] + [render-action-sheet + @customization-color]) + :gradient-cover? true + :customization-color @customization-color}])} + "See in bottom sheet"]])])) diff --git a/src/status_im/contexts/preview/quo/graph/interactive_graph.cljs b/src/status_im/contexts/preview/quo/graph/interactive_graph.cljs new file mode 100644 index 00000000000..714337d0d1f --- /dev/null +++ b/src/status_im/contexts/preview/quo/graph/interactive_graph.cljs @@ -0,0 +1,157 @@ +(ns status-im.contexts.preview.quo.graph.interactive-graph + (:require + [goog.string :as gstring] + [quo.components.graph.utils :as utils] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def weekly-data + [{:value 123 + :date "Sun"} + {:value 160 + :date "Mon"} + {:value 435 + :date "Tue"} + {:value 2345 + :date "Wed"} + {:value 1444 + :date "Thu"} + {:value 931 + :date "Fri"} + {:value 1200 + :date "Sat"}]) + +(defn generate-crypto-token-prices + [num-elements volatility] + (loop [n num-elements + prices [] + prev-price (rand-int 100000) + volatility volatility + current-day (rand-int 31) ; Start with a random day + months ["Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"] + current-month (rand-nth months)] ; Start with a random month + (if (zero? n) + (vec (reverse prices)) + (let [fluctuation (* prev-price volatility) + random-delta (- (rand fluctuation) (/ fluctuation 2)) + new-price (max 1 (+ prev-price random-delta)) + new-day (if (= current-day 1) 31 (dec current-day)) ; Decrease the day + new-month (if (= current-day 1) + (let [prev-month-index (dec (.indexOf months current-month))] + (if (>= prev-month-index 0) + (nth months prev-month-index) + (nth months (dec (count months))))) + current-month) + new-prices (conj prices + {:value new-price + :date (str new-day " " new-month)})] + (recur (dec n) new-prices new-price volatility new-day months new-month))))) + + +(def descriptor + [{:label "State:" + :key :state + :type :select + :options [{:key :positive + :value "Positive"} + {:key :negative + :value "Negative"}]} + {:label "Time frame:" + :key :time-frame + :type :select + :options [{:key :empty + :value "Empty"} + {:key :1-week + :value "1 Week"} + {:key :1-month + :value "1 Month"} + {:key :3-months + :value "3 Months"} + {:key :1-year + :value "1 Year"} + {:key :all-time + :value "All time (500 years data)"}]} + {:label "Reference value:" + :key :reference-value + :type :number} + {:label "Reference prefix:" + :key :reference-prefix + :type :text} + {:label "Reference decimal separator:" + :key :decimal-separator + :type :select + :options [{:key :dot + :value "Dot (.)"} + {:key :comma + :value "Comma (,)"}]} + (preview/customization-color-option)]) + +(defn generate-data + [time-frame] + (let [data-points (case time-frame + :empty 0 + :1-week 7 + :1-month 30 + :3-months 90 + :1-year 365 + (* 365 500)) + volatility (case time-frame + :empty 0 + :1-week 2 + :1-month 1 + :3-months 0.5 + :1-year 0.05 + 0.005)] + (if (= time-frame :1-week) + weekly-data + (generate-crypto-token-prices data-points volatility)))) + +(defn f-view + [state] + (fn [] + (let [theme (quo.context/use-theme)] + (rn/use-effect (fn [] + (let [time-frame (:time-frame @state) + data (generate-data time-frame) + highest-value (utils/find-highest-value data) + lowest-value (utils/find-lowest-value data) + average-value (gstring/format "%.2f" (/ (+ highest-value lowest-value) 2))] + (swap! state assoc :data data :reference-value average-value))) + [(:time-frame @state)]) + [rn/pressable {:on-press rn/dismiss-keyboard!} + [rn/view {:padding-bottom 150} + [preview/customizer state descriptor theme] + [quo/interactive-graph + {:data (:data @state) + :state (:state @state) + :reference-value (:reference-value @state) + :reference-prefix (:reference-prefix @state) + :customization-color (:customization-color @state) + :decimal-separator (:decimal-separator @state)}]]]))) + +(defn view + [] + (let [theme (quo.context/use-theme) + data (generate-data :1-week) + highest-value (utils/find-highest-value data) + lowest-value (utils/find-lowest-value data) + average-value (gstring/format "%.2f" (/ (+ highest-value lowest-value) 2)) + state (reagent/atom {:state :positive + :time-frame :1-week + :customization-color :blue + :reference-value average-value + :reference-prefix "$" + :decimal-separator :dot + :data data})] + [rn/scroll-view + {:style + {:background-color (colors/theme-colors + colors/white + colors/neutral-95 + theme) + :flex 1}} + [:f> f-view state]])) diff --git a/src/status_im/contexts/preview/quo/graph/wallet_graph.cljs b/src/status_im/contexts/preview/quo/graph/wallet_graph.cljs new file mode 100644 index 00000000000..f35b3ed209e --- /dev/null +++ b/src/status_im/contexts/preview/quo/graph/wallet_graph.cljs @@ -0,0 +1,68 @@ +(ns status-im.contexts.preview.quo.graph.wallet-graph + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn generate-crypto-token-prices + [num-elements volatility] + (loop [n num-elements + prices [] + prev-price (rand-int 100000) + volatility volatility] + (if (zero? n) + (vec (reverse prices)) + (let [fluctuation (* prev-price volatility) + random-delta (- (rand fluctuation) (/ fluctuation 2)) + new-price (max 1 (+ prev-price random-delta)) + new-prices (conj prices {:value new-price})] + (recur (dec n) new-prices new-price volatility))))) + +(def descriptor + [{:key :state + :type :select + :options [{:key :positive} + {:key :negative}]} + {:key :time-frame + :type :select + :options [{:key :empty} + {:key :1-week} + {:key :1-month} + {:key :3-months} + {:key :1-year} + {:key :all-time + :value "All time (500 years data)"}]} + (preview/customization-color-option)]) + +(defn generate-data + [time-frame] + (let [data-points (case time-frame + :empty 0 + :1-week 7 + :1-month 30 + :3-months 90 + :1-year 365 + (* 365 500)) + volatility (case time-frame + :empty 0 + :1-week 2 + :1-month 1 + :3-months 0.5 + :1-year 0.05 + 0.005)] + (generate-crypto-token-prices data-points volatility))) + +(defn view + [] + (let [state (reagent/atom {:state :positive + :time-frame :1-week})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-horizontal 0 :margin-top 200}} + [quo/wallet-graph + {:data (generate-data (:time-frame @state)) + :state (:state @state) + :time-frame (:time-frame @state) + :customization-color (:customization-color @state)}]]))) diff --git a/src/status_im/contexts/preview/quo/info/info_message.cljs b/src/status_im/contexts/preview/quo/info/info_message.cljs new file mode 100644 index 00000000000..88ff838f037 --- /dev/null +++ b/src/status_im/contexts/preview/quo/info/info_message.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.preview.quo.info.info-message + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :status + :type :select + :options [{:key :default} + {:key :success} + {:key :error} + {:key :warning}]} + {:key :size + :type :select + :options [{:key :default} + {:key :tiny}]} + {:key :blur? + :type :boolean} + {:key :icon + :type :select + :options [{:key :i/placeholder} + {:key :i/info}]} + {:key :message + :type :text}]) + +(defn view + [] + (let [[state set-state] (rn/use-state + {:status :default + :size :default + :message "This is a message" + :blur? false + :icon :i/placeholder}) + blur? (:blur? state)] + [preview/preview-container + {:state state + :descriptor descriptor + :blur-dark-only? true + :show-blur-background? blur? + :blur? blur? + :set-state set-state} + [quo/info-message (dissoc state :message) (:message state)]])) diff --git a/src/status_im/contexts/preview/quo/info/information_box.cljs b/src/status_im/contexts/preview/quo/info/information_box.cljs new file mode 100644 index 00000000000..6b0ee78003c --- /dev/null +++ b/src/status_im/contexts/preview/quo/info/information_box.cljs @@ -0,0 +1,40 @@ +(ns status-im.contexts.preview.quo.info.information-box + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :default} + {:key :informative} + {:key :error}]} + {:key :closable? + :type :boolean} + {:key :message + :type :text} + {:key :button-label + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:type :default + :closable? true + :message (str "If you registered a stateofus.eth name " + "you might be eligible to connect $ENS") + :button-label "Button"}) + closable? (reagent/cursor state [:closable?]) + closed? (reagent/cursor state [:closed?]) + on-close (fn [] + (reset! closed? true) + (js/setTimeout (fn [] (reset! closed? false)) + 2000))] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/information-box + (merge {:icon :i/info + :style {:width 335} + :on-close (when @closable? on-close)} + @state) + (:message @state)]]))) diff --git a/src/status_im/contexts/preview/quo/inputs/address_input.cljs b/src/status_im/contexts/preview/quo/inputs/address_input.cljs new file mode 100644 index 00000000000..bcc48390be5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/inputs/address_input.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.preview.quo.inputs.address-input + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:label "Scanned value:" + :key :scanned-value + :type :text} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:scanned-value "" + :blur? false + :valid-ens-or-address? false}) + timer (atom nil)] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/address-input + (merge (dissoc @state :scanned-value) + {:on-scan (fn [on-result] (on-result (:scanned-value @state))) + :ens-regex constants/regx-ens + :on-detect-ens (fn [_] + (swap! state assoc :valid-ens-or-address? false) + (when @timer + (js/clearTimeout @timer)) + (reset! timer (js/setTimeout + #(swap! state assoc :valid-ens-or-address? true) + 2000)))})]]))) diff --git a/src/status_im/contexts/preview/quo/inputs/input.cljs b/src/status_im/contexts/preview/quo/inputs/input.cljs new file mode 100644 index 00000000000..6aab51f96ea --- /dev/null +++ b/src/status_im/contexts/preview/quo/inputs/input.cljs @@ -0,0 +1,74 @@ +(ns status-im.contexts.preview.quo.inputs.input + (:require + [clojure.string :as string] + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :text} + {:key :password}]} + {:key :blur? + :type :boolean} + {:key :error? + :type :boolean} + {:key :icon-name + :type :boolean} + {:key :disabled? + :type :boolean} + {:key :clearable? + :type :boolean} + {:key :small? + :type :boolean} + {:key :multiline? + :type :boolean} + {:key :button + :type :boolean} + {:key :label + :type :text} + {:key :char-limit + :type :select + :options [{:key 10 + :value "10"} + {:key 50 + :value "50"} + {:key 100 + :value "100"}]} + {:key :value + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:type :text + :blur? false + :placeholder "Type something" + :error false + :icon-name false + :value "" + :clearable false + :on-char-limit-reach #(js/alert + (str "Char limit reached: " %))})] + (fn [] + (let [blank-label? (string/blank? (:label @state))] + [preview/preview-container + {:component-container-style {:margin-bottom 200} + :state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/input + (cond-> (assoc @state + :on-clear? #(swap! state assoc :value "") + :on-change-text #(swap! state assoc :value %)) + (:button @state) + (assoc :button + {:on-press #(js/alert "Button pressed!") + :text "My button"}) + + blank-label? + (dissoc :label) + + (:icon-name @state) + (assoc :icon-name :i/placeholder))]])))) diff --git a/src/status_im/contexts/preview/quo/inputs/locked_input.cljs b/src/status_im/contexts/preview/quo/inputs/locked_input.cljs new file mode 100644 index 00000000000..f1e423adbc4 --- /dev/null +++ b/src/status_im/contexts/preview/quo/inputs/locked_input.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.preview.quo.inputs.locked-input + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :icon :type :boolean} + {:key :label :type :text} + {:key :value :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:value "$1,648.34" + :label "Network fee"})] + (fn [] + (let [{:keys [label value icon]} @state] + [preview/preview-container {:state state :descriptor descriptor} + [quo/locked-input + {:icon (when icon :i/gas) + :label label + :container-style {:margin-right 20 + :margin-horizontal 20 + :flex 1}} + value]])))) diff --git a/src/status_im/contexts/preview/quo/inputs/profile_input.cljs b/src/status_im/contexts/preview/quo/inputs/profile_input.cljs new file mode 100644 index 00000000000..95ea9a50748 --- /dev/null +++ b/src/status_im/contexts/preview/quo/inputs/profile_input.cljs @@ -0,0 +1,42 @@ +(ns status-im.contexts.preview.quo.inputs.profile-input + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :disabled? + :type :boolean} + {:key :image-selected? + :type :boolean} + (preview/customization-color-option {:key :color})]) + +(defn view + [] + (let [state (reagent/atom {:color :blue + :image-selected? false + :disabled? false}) + max-length 24 + value (reagent/atom "") + on-change-text #(reset! value %)] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? true + :show-blur-background? true} + [rn/view + [quo/profile-input + {:default-value "" + :on-change-text on-change-text + :customization-color (:color @state) + :placeholder "Your Name" + :on-press #(js/alert "show image selector") + :image-picker-props {:profile-picture (when (:image-selected? @state) + (resources/get-mock-image :user-picture-male5)) + :full-name @value} + :title-input-props {:disabled? (:disabled? @state) + :max-length max-length + :on-change-text on-change-text}}]]]))) diff --git a/src/status_im/contexts/preview/quo/inputs/recovery_phrase_input.cljs b/src/status_im/contexts/preview/quo/inputs/recovery_phrase_input.cljs new file mode 100644 index 00000000000..74ca32bf64e --- /dev/null +++ b/src/status_im/contexts/preview/quo/inputs/recovery_phrase_input.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.preview.quo.inputs.recovery-phrase-input + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :text + :type :text} + {:key :placeholder + :type :text} + {:key :blur? + :type :boolean} + {:key :mark-errors? + :type :boolean} + (preview/customization-color-option) + {:key :word-limit + :type :select + :options [{:key nil :value "No limit"} + {:key 5 :value "5 words"} + {:key 10 :value "10 words"} + {:key 20 :value "20 words"}]}]) + +(defn view + [] + (let [state (reagent/atom {:text "" + :placeholder "Type or paste your recovery phrase" + :customization-color :blue + :word-limit 20 + :blur? false + :mark-errors? true})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/text {:size :paragraph-2} + "(Any word with at least 6 chars is marked as error)"] + [rn/view {:style {:height 150}} + [quo/recovery-phrase-input + {:mark-errors? (:mark-errors? @state) + :error-pred #(> (count %) 5) + :on-change-text #(swap! state assoc :text %) + :placeholder (:placeholder @state) + :customization-color (:customization-color @state) + :word-limit (:word-limit @state)} + (:text @state)]]]))) diff --git a/src/status_im/contexts/preview/quo/inputs/search_input.cljs b/src/status_im/contexts/preview/quo/inputs/search_input.cljs new file mode 100644 index 00000000000..1cefb59a6aa --- /dev/null +++ b/src/status_im/contexts/preview/quo/inputs/search_input.cljs @@ -0,0 +1,59 @@ +(ns status-im.contexts.preview.quo.inputs.search-input + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :value + :type :text} + {:key :blur? + :type :boolean} + {:key :disabled? + :type :boolean} + {:key :number-tags + :type :select + :options (map (fn [n] + {:key n :value (str n)}) + (range 0 5))}]) + +(defn example-tags + [blur?] + [[quo/context-tag + {:blur? blur? + :size 24 + :profile-picture (resources/get-mock-image :user-picture-male5) + :full-name "alisher.eth"}] + [quo/context-tag + {:blur? blur? + :size 24 + :profile-picture (resources/get-mock-image :user-picture-male4) + :full-name "Pedro"}] + [quo/context-tag + {:blur? blur? + :size 24 + :profile-picture (resources/get-mock-image :user-picture-female2) + :full-name "Freya Odinson"}]]) + +(defn view + [] + (let [state (reagent/atom {:blur? false + :disabled? false + :number-tags 0 + :placeholder "Search..." + :value ""}) + on-change-text (fn [new-text] + (swap! state assoc :value new-text))] + (fn [] + (let [tags (take (:number-tags @state) (example-tags (:blur? @state)))] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/search-input + (assoc @state + :on-clear #(swap! state assoc :value "") + :tags tags + :on-change-text on-change-text)]])))) diff --git a/src/status_im/contexts/preview/quo/inputs/title_input.cljs b/src/status_im/contexts/preview/quo/inputs/title_input.cljs new file mode 100644 index 00000000000..573603346ce --- /dev/null +++ b/src/status_im/contexts/preview/quo/inputs/title_input.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.preview.quo.inputs.title-input + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :disabled? + :type :boolean} + {:key :placeholder + :type :text} + {:key :max-length + :type :number} + {:key :blur? + :type :boolean} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:color nil + :placeholder "Type something here" + :max-length 24 + :blur? false + :disabled? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/title-input (assoc @state :default-value "")]]))) diff --git a/src/status_im/contexts/preview/quo/ios/drawer_bar.cljs b/src/status_im/contexts/preview/quo/ios/drawer_bar.cljs new file mode 100644 index 00000000000..80ffbb0370a --- /dev/null +++ b/src/status_im/contexts/preview/quo/ios/drawer_bar.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.preview.quo.ios.drawer-bar + (:require + [quo.core :as quo] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn view + [] + [preview/preview-container {} + [quo/drawer-bar]]) diff --git a/src/status_im/contexts/preview/quo/keycard/keycard.cljs b/src/status_im/contexts/preview/quo/keycard/keycard.cljs new file mode 100644 index 00000000000..717b2355347 --- /dev/null +++ b/src/status_im/contexts/preview/quo/keycard/keycard.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.preview.quo.keycard.keycard + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :holder-name :type :text} + {:key :locked? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:holder-name "" + :locked? true})] + (fn + [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/keycard @state]]))) diff --git a/src/status_im/contexts/preview/quo/links/internal_link_card.cljs b/src/status_im/contexts/preview/quo/links/internal_link_card.cljs new file mode 100644 index 00000000000..c6a049628bb --- /dev/null +++ b/src/status_im/contexts/preview/quo/links/internal_link_card.cljs @@ -0,0 +1,60 @@ +(ns status-im.contexts.preview.quo.links.internal-link-card + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :message} + {:key :full-page}]} + {:type :text + :key :title} + {:type :text + :key :subtitle} + {:type :text + :key :description} + {:type :text + :key :emoji-hash} + {:type :boolean + :key :loading?} + {:type :number + :key :members-count} + {:type :number + :key :active-members-count} + {:key :type + :type :select + :options [{:key :community} + {:key :channel} + {:key :user}]} + {:key :banner + :type :select + :options (mapv (fn [k] {:key k}) + (keys resources/mock-images))} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom + {:title "Doodles" + :description "Coloring the world with joy • ᴗ •" + :members-count 24 + :emoji-hash "🌟🚀🐠🌈🏰🔮🦉🐼🍉🎨🚲🌙🍔🌵" + :active-members-count 12 + :loading? false + :customization-color :purple + :banner :light-blur-background + :type :user + :subtitle "Web 3.0 Designer @ethstatus - DJ, Producer - Dad - YouTuber"})] + (fn [] + (let [banner (get resources/mock-images (:banner @state) :bored-ape)] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/internal-link-card + (assoc @state + :banner banner + :icon (resources/get-mock-image :status-logo) + :on-press #(js/alert "You clicked me!"))]])))) diff --git a/src/status_im/contexts/preview/quo/links/link_preview.cljs b/src/status_im/contexts/preview/quo/links/link_preview.cljs new file mode 100644 index 00000000000..df20d6960ce --- /dev/null +++ b/src/status_im/contexts/preview/quo/links/link_preview.cljs @@ -0,0 +1,57 @@ +(ns status-im.contexts.preview.quo.links.link-preview + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :text :key :title} + {:type :text :key :description} + {:type :text :key :link} + {:type :number :key :width} + {:type :boolean :key :with-logo?} + {:type :boolean :key :with-description?} + {:type :boolean :key :with-thumbnail?} + {:type :text :key :disabled-text} + {:type :boolean :key :enabled?} + {:key :thumbnail + :type :select + :options (mapv (fn [k] {:key k}) + (keys resources/mock-images))} + {:key :thumbnail-size + :type :select + :options [{:key :normal} + {:key :large}]}]) + +(defn view + [] + (let [state (reagent/atom + {:description "Turn your products or services into publicly tradeable items" + :disabled-text "Enable Preview" + :enabled? true + :link "rarible.com" + :thumbnail :collectible + :thumbnail-size :normal + :title "Rarible - NFT Marketplace" + :width 295 + :with-description? true + :with-logo? true + :with-thumbnail? true})] + (fn [] + (let [thumbnail (get resources/mock-images (:thumbnail @state))] + [preview/preview-container {:state state :descriptor descriptor} + [quo/link-preview + {:logo (when (:with-logo? @state) + (resources/get-mock-image :status-logo)) + :title (:title @state) + :description (when (:with-description? @state) + (:description @state)) + :enabled? (:enabled? @state) + :on-enable #(js/alert "Button pressed") + :disabled-text (:disabled-text @state) + :link (:link @state) + :thumbnail (when (:with-thumbnail? @state) + thumbnail) + :thumbnail-size (:thumbnail-size @state) + :container-style {:width (:width @state)}}]])))) diff --git a/src/status_im/contexts/preview/quo/links/url_preview.cljs b/src/status_im/contexts/preview/quo/links/url_preview.cljs new file mode 100644 index 00000000000..e75808ff452 --- /dev/null +++ b/src/status_im/contexts/preview/quo/links/url_preview.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.preview.quo.links.url-preview + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :text :key :title} + {:type :text :key :body} + {:type :boolean :key :with-logo?} + {:type :boolean :key :loading?} + {:type :text :key :loading-message}]) + +(defn view + [] + (let [state (reagent/atom + {:title "Status - Private, Secure Communication" + :body "Status.im" + :with-logo? true + :loading? false + :loading-message "Generating preview"})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/url-preview + {:title (:title @state) + :body (:body @state) + :logo (when (:with-logo? @state) + (resources/get-mock-image :status-logo)) + :loading? (:loading? @state) + :loading-message (:loading-message @state) + :on-clear #(js/alert "Clear button pressed")}]]))) diff --git a/src/status_im/contexts/preview/quo/links/url_preview_list.cljs b/src/status_im/contexts/preview/quo/links/url_preview_list.cljs new file mode 100644 index 00000000000..b3c0160a6a6 --- /dev/null +++ b/src/status_im/contexts/preview/quo/links/url_preview_list.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.preview.quo.links.url-preview-list + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :number :key :previews-length}]) + +(defn view + [] + (let [state (reagent/atom {:previews-length 3}) + padding 20] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-horizontal 0}} + [quo/url-preview-list + {:horizontal-spacing padding + :preview-width (- (:width (rn/get-window)) + (* 2 padding)) + :on-clear #(js/alert "Clear button pressed") + :key-fn :url + :data (for [index (range (:previews-length @state)) + :let [index (inc index)]] + {:title (str "Title " index) + :body (str "status.im." index) + :logo (resources/get-mock-image + :status-logo) + :loading? false + :url (str "status.im." index)})}]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/account.cljs b/src/status_im/contexts/preview/quo/list_items/account.cljs new file mode 100644 index 00000000000..8c3af81ab0e --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/account.cljs @@ -0,0 +1,56 @@ +(ns status-im.contexts.preview.quo.list-items.account + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :default} {:key :balance-positive} {:key :balance-neutral} {:key :balance-negative} + {:key :tag} {:key :action}]} + {:key :title-icon? :type :boolean} + {:key :state + :type :select + :options [{:key :active} {:key :selected}]} + {:key :emoji + :type :text} + (preview/customization-color-option {:key :account-color}) + {:key :blur? :type :boolean} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:type :default + :title-icon? false + :state :default + :customization-color :blue + :account-color :purple + :emoji "🍑" + :title "New House" + :address "0x21a...49e" + :balance-props {:crypto-value "0.00" + :fiat-value "€0.00" + :percentage-change "0.0" + :fiat-change "€0.00"} + :token-props {:symbol "SNT" + :value "1,000"} + :on-options-press #(js/alert "Options button pressed!")})] + (fn [] [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/account-item + (dissoc (merge @state + {:title-icon (when (:title-icon? @state) :i/keycard) + :account-props {:name (:title @state) + :address (:address @state) + :emoji (:emoji @state) + :customization-color (:account-color @state)}}) + :emoji + :address + :title-icon? + :account-color + :title)]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/account_list_card.cljs b/src/status_im/contexts/preview/quo/list_items/account_list_card.cljs new file mode 100644 index 00000000000..826308c56a3 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/account_list_card.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.preview.quo.list-items.account-list-card + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :action + :type :select + :options [{:key :none} + {:key :icon}]} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:account-props {:customization-color :purple + :size 32 + :emoji "🍑" + :type :default + :name "Trip to Vegas" + :address "0x0ah...78b"} + :action :none + :on-press (fn [] (js/alert "Item pressed")) + :on-options-press (fn [] (js/alert "Options pressed")) + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/account-list-card @state]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/address.cljs b/src/status_im/contexts/preview/quo/list_items/address.cljs new file mode 100644 index 00000000000..a248aff1308 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/address.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.preview.quo.list-items.address + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :active-state? :type :boolean} + {:key :show-alert-on-press? :type :boolean} + {:key :blur? :type :boolean} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:address "0x0ah...78b" + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}]})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/address + (merge @state + (when (:show-alert-on-press? @state) + {:on-press #(js/alert "Pressed!")}))]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/approval_info.cljs b/src/status_im/contexts/preview/quo/list_items/approval_info.cljs new file mode 100644 index 00000000000..a716a99388d --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/approval_info.cljs @@ -0,0 +1,80 @@ +(ns status-im.contexts.preview.quo.list-items.approval-info + (:require + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [status-im.common.resources :as common.resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :select + :key :type + :options [{:key :spending-cap} + {:key :token-contract} + {:key :account} + {:key :spending-contract} + {:key :network} + {:key :date-signed} + {:key :collectible} + {:key :collectible-contract} + {:key :address} + {:key :community}]} + {:type :text + :key :label} + {:type :text + :key :description} + {:type :boolean + :key :blur?} + {:type :boolean + :key :unlimited-icon?} + {:type :text + :key :button-label} + {:type :text + :key :tag-label} + {:type :select + :key :option-icon + :options [{:key nil + :value "None"} + {:key :i/options} + {:key :i/chevron-right}]}]) + +(defn- get-avatar-props + [type] + (case type + :collectible {:image (common.resources/get-mock-image :collectible2)} + :account {:customization-color :orange + :emoji "😇"} + :collectible-contract {:image (common.resources/get-mock-image :bored-ape)} + :spending-contract {:network-image (resources/get-network :ethereum) + :image (resources/get-dapp :coingecko)} + :date-signed {:icon :i/signature} + :address {:customization-color :blue + :full-name "0 x"} + :community {:image (common.resources/get-mock-image :status-logo) + :size :size-32} + {:image (common.resources/get-mock-image :status-logo)})) + +(defn view + [] + (let [[state set-state] (rn/use-state {:type :spending-cap + :label "Label" + :description "Description" + :blur? false + :unlimited-icon? false + :button-label "Edit" + :tag-label "31,283.77 EUR" + :option-icon :i/options})] + [preview/preview-container + {:state state + :set-state set-state + :blur? (:blur? state) + :show-blur-background? true + :blur-dark-only? true + :descriptor descriptor} + [quo/approval-info + (assoc state + :button-icon :i/edit + :on-button-press #(js/alert "Button Pressed") + :on-avatar-press #(js/alert "Token Pressed") + :avatar-props (get-avatar-props (:type state)) + :on-option-press #(js/alert "Option Pressed"))]])) diff --git a/src/status_im/contexts/preview/quo/list_items/channel.cljs b/src/status_im/contexts/preview/quo/list_items/channel.cljs new file mode 100644 index 00000000000..d7a7e9ab033 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/channel.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.preview.quo.list-items.channel + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :name + :type :text} + {:key :notification + :type :select + :options [{:key nil + :value "None"} + {:key :notification + :value :notification} + {:key :mute + :value :mute} + {:key :mention + :value :mention}]} + {:key :mentions-count + :type :text} + {:key :emoji + :type :text} + (preview/customization-color-option) + {:key :locked? + :type :select + :options [{:key nil + :value "None"} + {:key false + :value "Unlocked"} + {:key true + :value "Locked"}]}]) + +(defn view + [] + (let [state (reagent/atom {:name "channel" + :notification nil + :mentions-count "5" + :emoji "🍑" + :customization-color :blue + :locked? nil})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/channel @state]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/dapp.cljs b/src/status_im/contexts/preview/quo/list_items/dapp.cljs new file mode 100644 index 00000000000..6530797f96f --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/dapp.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.preview.quo.list-items.dapp + (:require + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :state + :type :select + :options [{:key :default + :value "Default"} + {:key :active + :value "Active"}]} + {:key :dapp + :type :select + :options [{:value "With icon" + :key {:avatar (resources/get-dapp :coingecko) + :name "Coingecko" + :value "coingecko.com"}} + {:value "Without icon" + :key {:avatar nil + :name "Coingecko" + :value "coingecko.com" + :customization-color :blue}}]}]) + +(defn preview + [] + (let [state (reagent/atom {:dapp {:avatar (resources/get-dapp :coingecko) + :name "Coingecko" + :value "coingecko.com"} + :state :default + :blur? false + :customization-color :blue + :on-press (fn [{:keys [name]}] + (js/alert (str name " got pressed")))})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [rn/view + {:padding-vertical 60 + :flex-direction :row + :justify-content :center} + [quo/dapp @state]]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/market_token.cljs b/src/status_im/contexts/preview/quo/list_items/market_token.cljs new file mode 100644 index 00000000000..911aa29fdd6 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/market_token.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.preview.quo.list-items.market-token + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :token + :type :select + :options [{:key :eth} + {:key :snt} + {:key :btc}]} + {:key :token-rank + :type :number} + {:key :percentage-change + :type :number} + {:key :token-name + :type :text} + {:key :market-cap + :type :text} + {:key :price + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:token :snt + :token-rank 10 + :token-name "Status" + :market-cap "$332.1B" + :price "$0.8" + :percentage-change 5.2 + :customization-color :blue})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:align-items :center + :margin-top 50}} + [quo/market-token @state]]))) + diff --git a/src/status_im/contexts/preview/quo/list_items/missing_keypair.cljs b/src/status_im/contexts/preview/quo/list_items/missing_keypair.cljs new file mode 100644 index 00000000000..2ecd787a025 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/missing_keypair.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.preview.quo.list-items.missing-keypair + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? + :type :boolean}]) + +(def component-props + {:blur? false + :keypair {:type :seed + :key-uid "0x01" + :name "Trip to Vegas" + :accounts [{:type :default + :emoji "🍑" + :customization-color :purple}]}}) + +(defn view + [] + (let [state (reagent/atom component-props)] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [rn/view {:style {:align-items :flex-start}} + [quo/missing-keypair @state]]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/network_list.cljs b/src/status_im/contexts/preview/quo/list_items/network_list.cljs new file mode 100644 index 00000000000..f8fdf8f6e01 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/network_list.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.preview.quo.list-items.network-list + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :state + :type :select + :options [{:key :default} + {:key :active} + {:key :pressed}]} + {:key :network-image + :type :select + :options [{:key (quo.resources/get-network :ethereum) + :value :ethereum} + {:key (quo.resources/get-network :arbitrum) + :value :arbitrum} + {:key (quo.resources/get-network :optimism) + :value :optimism}]} + {:key :label + :type :text} + {:key :token-value + :type :text} + {:key :fiat-value + :type :text} + {:key :show-alert-on-press? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:network-image (quo.resources/get-network :ethereum) + :label "Ethereum" + :token-value "0.00 ETH" + :fiat-value "€0.00" + :state :default + :customization-color :blue})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/network-list + (merge @state + (when (:show-alert-on-press? @state) + {:on-press #(js/alert "Pressed!")}))]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/preview_lists.cljs b/src/status_im/contexts/preview/quo/list_items/preview_lists.cljs new file mode 100644 index 00000000000..f556cc36a23 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/preview_lists.cljs @@ -0,0 +1,122 @@ +(ns status-im.contexts.preview.quo.list-items.preview-lists + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :user + :value "User"} + {:key :communities + :value "Communities"} + {:key :collectibles + :value "Collectibles"} + {:key :tokens + :value "Tokens"} + {:key :dapps + :value "dApps"} + {:key :accounts + :value "Accounts"} + {:key :network + :value "Network"}]} + {:key :size + :type :select + :options [{:key :size-32 + :value "32"} + {:key :size-24 + :value "24"} + {:key :size-20 + :value "20"} + {:key :size-16 + :value "16"} + {:key :size-14 + :value "14"}]} + {:key :number + :type :text} + {:key :blur? + :type :boolean}]) + +(def user-list + [{:full-name "A Y" + :customization-color :blue} + {:full-name "B Z" + :profile-picture (resources/get-mock-image :user-picture-male4) + :customization-color :army} + {:full-name "X R" + :customization-color :orange} + {:full-name "T R" + :profile-picture (resources/get-mock-image :user-picture-male5) + :customization-color :army}]) + +(def accounts-list + [{:customization-color :purple + :emoji "🍑" + :type :default} + {:customization-color :army + :emoji "🍓" + :type :default} + {:customization-color :orange + :emoji "🍑" + :type :default} + {:customization-color :blue + :emoji "🍓" + :type :default}]) + +(def tokens-list [:snt :eth :knc :mana :rare]) + +(def communities-list + [{:source (resources/get-mock-image :coinbase)} + {:source (resources/get-mock-image :decentraland)} + {:source (resources/get-mock-image :rarible)} + {:source (resources/get-mock-image :photo1)} + {:source (resources/get-mock-image :photo2)} + {:source (resources/get-mock-image :photo3)}]) + +(def collectibles-list + [{:source (resources/get-mock-image :collectible)} + {:source (resources/get-mock-image :photo2)} + {:source (resources/get-mock-image :photo3)} + {:source (resources/get-mock-image :photo1)} + {:source (resources/get-mock-image :photo2)} + {:source (resources/get-mock-image :photo3)}]) + +(def dapps-list + [{:source (quo.resources/get-dapp :coingecko)} + {:source (quo.resources/get-dapp :aave)} + {:source (quo.resources/get-dapp :1inch)} + {:source (quo.resources/get-dapp :zapper)} + {:source (quo.resources/get-dapp :uniswap)}]) + +(def networks-list + [{:source (quo.resources/get-network :ethereum)} + {:source (quo.resources/get-network :optimism)} + {:source (quo.resources/get-network :arbitrum)} + {:source (quo.resources/get-network :zksync)} + {:source (quo.resources/get-network :polygon)}]) + +(defn view + [] + (let [state (reagent/atom {:type :accounts + :size :size-32 + :number 4}) + type (reagent/cursor state [:type]) + blur? (reagent/cursor state [:blur?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? true + :show-blur-background? @blur?} + [quo/preview-list @state + (case @type + :user user-list + :communities communities-list + :accounts accounts-list + :tokens tokens-list + :collectibles collectibles-list + :dapps dapps-list + :network networks-list)]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/quiz_item.cljs b/src/status_im/contexts/preview/quo/list_items/quiz_item.cljs new file mode 100644 index 00000000000..687aea9c97a --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/quiz_item.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.preview.quo.list-items.quiz-item + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :state + :type :select + :options [{:key :empty} + {:key :disabled} + {:key :success} + {:key :error}]} + {:key :blur? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:state :empty + :word "collapse" + :number 8 + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/quiz-item @state]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/saved_address.cljs b/src/status_im/contexts/preview/quo/list_items/saved_address.cljs new file mode 100644 index 00000000000..e1f505ae9b6 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/saved_address.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.preview.quo.list-items.saved-address + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [(preview/customization-color-option {:key :account-color}) + {:key :blur? :type :boolean} + {:key :title :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:type :default + :customization-color :blue + :account-color :pink + :title "Alisher Yakupov" + :address "0x21a...49e" + :on-options-press #(js/alert "Options button pressed!")})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/saved-address + (assoc @state + :user-props + {:name (:title @state) + :address (:address @state) + :emoji (:emoji @state) + :customization-color (:account-color @state)})]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/saved_contact_address.cljs b/src/status_im/contexts/preview/quo/list_items/saved_contact_address.cljs new file mode 100644 index 00000000000..521cbf32de1 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/saved_contact_address.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.preview.quo.list-items.saved-contact-address + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :accounts-number :type :number} + {:key :account-emoji :type :text} + {:key :account-name :type :text} + {:key :full-name :type :text} + {:key :show-alert-on-press? :type :boolean} + {:key :active-state? :type :boolean} + (preview/customization-color-option {:key :account-color}) + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:customization-color :blue + :accounts-number 1 + :account-name "New House" + :account-address "0x21a...49e" + :account-emoji "🍔" + :account-color :blue + :active-state? true + :full-name "Mark Villacampa" + :show-alert-on-press? false})] + (fn [] + (let [account-props {:name (:account-name @state) + :address (:account-address @state) + :emoji (:account-emoji @state) + :customization-color (:account-color @state)} + contact-props {:full-name (:full-name @state) + :profile-picture (resources/get-mock-image + :user-picture-male4) + :customization-color :purple}] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/saved-contact-address + (merge @state + {:accounts (repeat (:accounts-number @state) account-props) + :contact-props contact-props} + (when (:show-alert-on-press? @state) + {:on-press #(js/alert "Pressed!")}))]])))) diff --git a/src/status_im/contexts/preview/quo/list_items/status_list_item.cljs b/src/status_im/contexts/preview/quo/list_items/status_list_item.cljs new file mode 100644 index 00000000000..f0026688c49 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/status_list_item.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.preview.quo.list-items.status-list-item + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :status + :type :select + :options [{:key :positive + :value "Positive"} + {:key :negative + :value "Negative"}]} + {:key :title + :type :text} + {:key :description + :type :text} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:status :positive + :title "Account" + :description "This is a description" + :blur? false}) + blur? (reagent/cursor state [:blur?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? true + :show-blur-background? @blur?} + [quo/status-list-item @state]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/token_network.cljs b/src/status_im/contexts/preview/quo/list_items/token_network.cljs new file mode 100644 index 00000000000..431c570dd73 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/token_network.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.preview.quo.list-items.token-network + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def networks-list + [{:source (quo.resources/get-network :ethereum)} + {:source (quo.resources/get-network :optimism)} + {:source (quo.resources/get-network :arbitrum)} + {:source (quo.resources/get-network :zksync)} + {:source (quo.resources/get-network :polygon)}]) + +(def descriptor + [{:key :state + :type :select + :options [{:key :default} + {:key :active} + {:key :selected}]} + {:key :token + :type :select + :options [{:key :eth} + {:key :snt} + {:key :dai}]} + {:key :label + :type :text} + {:key :token-value + :type :text} + {:key :fiat-value + :type :text} + (preview/customization-color-option) + {:key :show-alert-on-press? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:token :snt + :label "Status" + :token-value "0.00 SNT" + :fiat-value "€0.00" + :networks networks-list + :state :default + :customization-color :blue})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/token-network + (merge @state + (when (:show-alert-on-press? @state) + {:on-press #(js/alert "Pressed!")}))]]))) diff --git a/src/status_im/contexts/preview/quo/list_items/token_value.cljs b/src/status_im/contexts/preview/quo/list_items/token_value.cljs new file mode 100644 index 00000000000..91da0903b54 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/token_value.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.preview.quo.list-items.token-value + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :token + :type :select + :options [{:key :eth} + {:key :snt}]} + {:key :status + :type :select + :options [{:key :empty} + {:key :positive} + {:key :negative}]} + (preview/customization-color-option) + {:key :metrics? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:token :snt + :token-name "Status" + :state :default + :status :empty + :customization-color :blue + :metrics? true + :values {:crypto-value "0.00" + :fiat-value "€0.00" + :percentage-change "0.00" + :fiat-change "€0.00"}})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:align-items :center + :margin-top 50}} + [quo/token-value @state]]))) + diff --git a/src/status_im/contexts/preview/quo/list_items/user_list.cljs b/src/status_im/contexts/preview/quo/list_items/user_list.cljs new file mode 100644 index 00000000000..09d931bb7a1 --- /dev/null +++ b/src/status_im/contexts/preview/quo/list_items/user_list.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.preview.quo.list-items.user-list + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [utils.address :as address])) + +(def descriptor + [{:key :primary-name + :type :text + :limit 24} + {:key :secondary-name :type :text} + {:key :contact? :type :boolean} + {:key :verified? :type :boolean} + {:key :untrustworthy? :type :boolean} + {:key :online? :type :boolean} + {:key :accessory + :type :select + :options [{:key {:type :options} + :value "Options"} + {:key {:type :checkbox} + :value "Checkbox"} + {:key {:type :close} + :value "Close"}]}]) + +(defn view + [] + (let [state (reagent/atom {:primary-name "Alisher Yakupov" + :short-chat-key (address/get-shortened-compressed-key + "zQ3ssgRy5TtB47MMiMKMKaGyaawkCgMqqbrnAUYrZJ1sgt5N") + :photo-path (resources/mock-images :user-picture-female2) + :ens-verified true + :contact? false + :verified? false + :untrustworthy? false + :online? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 30 + :padding-horizontal 15}} + [quo/user @state]]))) diff --git a/src/status_im/contexts/preview/quo/loaders/skeleton_list.cljs b/src/status_im/contexts/preview/quo/loaders/skeleton_list.cljs new file mode 100644 index 00000000000..535571f01c4 --- /dev/null +++ b/src/status_im/contexts/preview/quo/loaders/skeleton_list.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.preview.quo.loaders.skeleton-list + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :content + :type :select + :options [{:key :list-items} + {:key :notifications} + {:key :messages} + {:key :assets}]} + {:key :blur? :type :boolean} + {:key :animated? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:content :messages + :blur? false + :animated? true + :parent-height 600})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/skeleton-list @state]]))) diff --git a/src/status_im/contexts/preview/quo/main.cljs b/src/status_im/contexts/preview/quo/main.cljs new file mode 100644 index 00000000000..513b7c4a420 --- /dev/null +++ b/src/status_im/contexts/preview/quo/main.cljs @@ -0,0 +1,632 @@ +(ns status-im.contexts.preview.quo.main + (:refer-clojure :exclude [filter]) + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.animated-header-list.animated-header-list + :as animated-header-list] + [status-im.contexts.preview.quo.avatars.account-avatar :as account-avatar] + [status-im.contexts.preview.quo.avatars.channel-avatar :as channel-avatar] + [status-im.contexts.preview.quo.avatars.collection-avatar :as collection-avatar] + [status-im.contexts.preview.quo.avatars.community-avatar :as community-avatar] + [status-im.contexts.preview.quo.avatars.dapp-avatar :as dapp-avatar] + [status-im.contexts.preview.quo.avatars.group-avatar :as group-avatar] + [status-im.contexts.preview.quo.avatars.icon-avatar :as icon-avatar] + [status-im.contexts.preview.quo.avatars.token-avatar :as token-avatar] + [status-im.contexts.preview.quo.avatars.user-avatar :as user-avatar] + [status-im.contexts.preview.quo.avatars.wallet-user-avatar :as + wallet-user-avatar] + [status-im.contexts.preview.quo.banners.alert-banner :as alert-banner] + [status-im.contexts.preview.quo.banners.banner :as banner] + [status-im.contexts.preview.quo.browser.browser-input :as browser-input] + [status-im.contexts.preview.quo.buttons.button :as button] + [status-im.contexts.preview.quo.buttons.composer-button :as + composer-button] + [status-im.contexts.preview.quo.buttons.dynamic-button :as dynamic-button] + [status-im.contexts.preview.quo.buttons.predictive-keyboard :as + predictive-keyboard] + [status-im.contexts.preview.quo.buttons.slide-button :as slide-button] + [status-im.contexts.preview.quo.buttons.swap-order-button :as swap-order-button] + [status-im.contexts.preview.quo.buttons.wallet-button :as wallet-button] + [status-im.contexts.preview.quo.buttons.wallet-ctas :as wallet-ctas] + [status-im.contexts.preview.quo.calendar.calendar :as calendar] + [status-im.contexts.preview.quo.calendar.calendar-day :as calendar-day] + [status-im.contexts.preview.quo.calendar.calendar-year :as calendar-year] + [status-im.contexts.preview.quo.cards.wallet-card :as wallet-card] + [status-im.contexts.preview.quo.code.snippet :as code-snippet] + [status-im.contexts.preview.quo.code.snippet-preview :as code-snippet-preview] + [status-im.contexts.preview.quo.colors.color :as color] + [status-im.contexts.preview.quo.colors.color-picker :as color-picker] + [status-im.contexts.preview.quo.common :as common] + [status-im.contexts.preview.quo.community.channel-action :as + channel-action] + [status-im.contexts.preview.quo.community.channel-actions :as + channel-actions] + [status-im.contexts.preview.quo.community.community-card-view :as + community-card] + [status-im.contexts.preview.quo.community.community-detail-token-gating :as + community-detail-token-gating] + [status-im.contexts.preview.quo.community.community-membership-list-view + :as community-membership-list-view] + [status-im.contexts.preview.quo.community.community-stat :as community-stat] + [status-im.contexts.preview.quo.community.community-token-gating :as community-token-gating] + [status-im.contexts.preview.quo.community.discover-card :as discover-card] + [status-im.contexts.preview.quo.community.list-item :as community-list-item] + [status-im.contexts.preview.quo.counter.collectible-counter :as collectible-counter] + [status-im.contexts.preview.quo.counter.counter :as counter] + [status-im.contexts.preview.quo.counter.step :as step] + [status-im.contexts.preview.quo.dividers.date :as divider-date] + [status-im.contexts.preview.quo.dividers.divider-label :as divider-label] + [status-im.contexts.preview.quo.dividers.divider-line :as divider-line] + [status-im.contexts.preview.quo.dividers.new-messages :as new-messages] + [status-im.contexts.preview.quo.dividers.strength-divider :as + strength-divider] + [status-im.contexts.preview.quo.drawers.action-drawers :as action-drawers] + [status-im.contexts.preview.quo.drawers.bottom-actions :as bottom-actions] + [status-im.contexts.preview.quo.drawers.documentation-drawers :as + documentation-drawers] + [status-im.contexts.preview.quo.drawers.drawer-action :as drawer-action] + [status-im.contexts.preview.quo.drawers.drawer-buttons :as drawer-buttons] + [status-im.contexts.preview.quo.drawers.drawer-top :as drawer-top] + [status-im.contexts.preview.quo.drawers.permission-drawers :as + permission-drawers] + [status-im.contexts.preview.quo.dropdowns.dropdown :as dropdown] + [status-im.contexts.preview.quo.dropdowns.dropdown-input :as + dropdown-input] + [status-im.contexts.preview.quo.dropdowns.network-dropdown :as + network-dropdown] + [status-im.contexts.preview.quo.empty-state.empty-state :as empty-state] + [status-im.contexts.preview.quo.foundations.gradients :as gradients] + [status-im.contexts.preview.quo.foundations.shadows :as shadows] + [status-im.contexts.preview.quo.gradient.gradient-cover :as gradient-cover] + [status-im.contexts.preview.quo.graph.interactive-graph :as + interactive-graph] + [status-im.contexts.preview.quo.graph.wallet-graph :as wallet-graph] + [status-im.contexts.preview.quo.info.info-message :as info-message] + [status-im.contexts.preview.quo.info.information-box :as information-box] + [status-im.contexts.preview.quo.inputs.address-input :as address-input] + [status-im.contexts.preview.quo.inputs.input :as input] + [status-im.contexts.preview.quo.inputs.locked-input :as locked-input] + [status-im.contexts.preview.quo.inputs.profile-input :as profile-input] + [status-im.contexts.preview.quo.inputs.recovery-phrase-input :as + recovery-phrase-input] + [status-im.contexts.preview.quo.inputs.search-input :as search-input] + [status-im.contexts.preview.quo.inputs.title-input :as title-input] + [status-im.contexts.preview.quo.ios.drawer-bar :as drawer-bar] + [status-im.contexts.preview.quo.keycard.keycard :as keycard] + [status-im.contexts.preview.quo.links.internal-link-card :as internal-link-card] + [status-im.contexts.preview.quo.links.link-preview :as link-preview] + [status-im.contexts.preview.quo.links.url-preview :as url-preview] + [status-im.contexts.preview.quo.links.url-preview-list :as + url-preview-list] + [status-im.contexts.preview.quo.list-items.account :as + account-item] + [status-im.contexts.preview.quo.list-items.account-list-card :as + account-list-card] + [status-im.contexts.preview.quo.list-items.address :as address] + [status-im.contexts.preview.quo.list-items.approval-info :as approval-info] + [status-im.contexts.preview.quo.list-items.channel :as channel] + [status-im.contexts.preview.quo.list-items.dapp :as dapp] + [status-im.contexts.preview.quo.list-items.market-token :as market-token] + [status-im.contexts.preview.quo.list-items.missing-keypair :as missing-keypair] + [status-im.contexts.preview.quo.list-items.network-list :as network-list] + [status-im.contexts.preview.quo.list-items.preview-lists :as preview-lists] + [status-im.contexts.preview.quo.list-items.quiz-item :as quiz-item] + [status-im.contexts.preview.quo.list-items.saved-address :as saved-address] + [status-im.contexts.preview.quo.list-items.saved-contact-address :as + saved-contact-address] + [status-im.contexts.preview.quo.list-items.status-list-item :as status-list-item] + [status-im.contexts.preview.quo.list-items.token-network :as token-network] + [status-im.contexts.preview.quo.list-items.token-value :as token-value] + [status-im.contexts.preview.quo.list-items.user-list :as user-list] + [status-im.contexts.preview.quo.loaders.skeleton-list :as skeleton-list] + [status-im.contexts.preview.quo.markdown.list :as markdown-list] + [status-im.contexts.preview.quo.markdown.text :as text] + [status-im.contexts.preview.quo.messages.author :as messages-author] + [status-im.contexts.preview.quo.messages.gap :as messages-gap] + [status-im.contexts.preview.quo.messages.system-message :as system-message] + [status-im.contexts.preview.quo.navigation.bottom-nav-tab :as + bottom-nav-tab] + [status-im.contexts.preview.quo.navigation.floating-shell-button :as + floating-shell-button] + [status-im.contexts.preview.quo.navigation.page-nav :as page-nav] + [status-im.contexts.preview.quo.navigation.top-nav :as top-nav] + [status-im.contexts.preview.quo.notifications.activity-logs :as + activity-logs] + [status-im.contexts.preview.quo.notifications.activity-logs-photos :as + activity-logs-photos] + [status-im.contexts.preview.quo.notifications.notification :as + notification] + [status-im.contexts.preview.quo.notifications.toast :as toast] + [status-im.contexts.preview.quo.numbered-keyboard.keyboard-key :as + keyboard-key] + [status-im.contexts.preview.quo.numbered-keyboard.numbered-keyboard :as + numbered-keyboard] + [status-im.contexts.preview.quo.onboarding.small-option-card :as + small-option-card] + [status-im.contexts.preview.quo.password.password-tips :as password-tips] + [status-im.contexts.preview.quo.password.tips :as tips] + [status-im.contexts.preview.quo.pin-input.pin-input :as pin-input] + [status-im.contexts.preview.quo.profile.collectible :as collectible] + [status-im.contexts.preview.quo.profile.collectible-list-item :as collectible-list-item] + [status-im.contexts.preview.quo.profile.expanded-collectible :as expanded-collectible] + [status-im.contexts.preview.quo.profile.link-card :as link-card] + [status-im.contexts.preview.quo.profile.profile-card :as profile-card] + [status-im.contexts.preview.quo.profile.select-profile :as select-profile] + [status-im.contexts.preview.quo.profile.showcase-nav :as showcase-nav] + [status-im.contexts.preview.quo.record-audio.record-audio :as record-audio] + [status-im.contexts.preview.quo.selectors.disclaimer :as disclaimer] + [status-im.contexts.preview.quo.selectors.filter :as filter] + [status-im.contexts.preview.quo.selectors.react :as react] + [status-im.contexts.preview.quo.selectors.react-selector :as react-selector] + [status-im.contexts.preview.quo.selectors.reactions-selector :as reactions-selector] + [status-im.contexts.preview.quo.selectors.selectors :as selectors] + [status-im.contexts.preview.quo.settings.accounts :as accounts] + [status-im.contexts.preview.quo.settings.category :as category] + [status-im.contexts.preview.quo.settings.data-item :as data-item] + [status-im.contexts.preview.quo.settings.page-setting :as page-setting] + [status-im.contexts.preview.quo.settings.privacy-option :as privacy-option] + [status-im.contexts.preview.quo.settings.reorder-item :as reorder-item] + [status-im.contexts.preview.quo.settings.section-label :as section-label] + [status-im.contexts.preview.quo.settings.settings-item :as settings-item] + [status-im.contexts.preview.quo.share.qr-code :as qr-code] + [status-im.contexts.preview.quo.share.share-qr-code :as share-qr-code] + [status-im.contexts.preview.quo.slideshow.slider-bar :as slider-bar] + [status-im.contexts.preview.quo.style :as style] + [status-im.contexts.preview.quo.tabs.account-selector :as account-selector] + [status-im.contexts.preview.quo.tabs.segmented-tab :as segmented] + [status-im.contexts.preview.quo.tabs.tabs :as tabs] + [status-im.contexts.preview.quo.tags.collectible-tag :as collectible-tag] + [status-im.contexts.preview.quo.tags.context-tags :as context-tags] + [status-im.contexts.preview.quo.tags.network-status-tag :as network-status-tag] + [status-im.contexts.preview.quo.tags.network-tags :as network-tags] + [status-im.contexts.preview.quo.tags.number-tag :as number-tag] + [status-im.contexts.preview.quo.tags.permission-tag :as permission-tag] + [status-im.contexts.preview.quo.tags.status-tags :as status-tags] + [status-im.contexts.preview.quo.tags.summary-tag :as summary-tag] + [status-im.contexts.preview.quo.tags.tag :as tag] + [status-im.contexts.preview.quo.tags.tags :as tags] + [status-im.contexts.preview.quo.tags.tiny-tag :as tiny-tag] + [status-im.contexts.preview.quo.tags.token-tag :as token-tag] + [status-im.contexts.preview.quo.text-combinations.channel-name :as channel-name] + [status-im.contexts.preview.quo.text-combinations.page-top :as page-top] + [status-im.contexts.preview.quo.text-combinations.preview :as text-combinations] + [status-im.contexts.preview.quo.text-combinations.standard-title :as standard-title] + [status-im.contexts.preview.quo.text-combinations.username :as username] + [status-im.contexts.preview.quo.wallet.account-card :as account-card] + [status-im.contexts.preview.quo.wallet.account-origin :as account-origin] + [status-im.contexts.preview.quo.wallet.account-overview :as + account-overview] + [status-im.contexts.preview.quo.wallet.account-permissions :as account-permissions] + [status-im.contexts.preview.quo.wallet.amount-input :as amount-input] + [status-im.contexts.preview.quo.wallet.approval-label :as approval-label] + [status-im.contexts.preview.quo.wallet.confirmation-progress :as + confirmation-progress] + [status-im.contexts.preview.quo.wallet.keypair :as keypair] + [status-im.contexts.preview.quo.wallet.missing-keypairs :as missing-keypairs] + [status-im.contexts.preview.quo.wallet.network-amount :as network-amount] + [status-im.contexts.preview.quo.wallet.network-bridge :as network-bridge] + [status-im.contexts.preview.quo.wallet.network-link :as network-link] + [status-im.contexts.preview.quo.wallet.network-routing :as network-routing] + [status-im.contexts.preview.quo.wallet.progress-bar :as progress-bar] + [status-im.contexts.preview.quo.wallet.required-tokens :as required-tokens] + [status-im.contexts.preview.quo.wallet.summary-info :as summary-info] + [status-im.contexts.preview.quo.wallet.swap-input :as swap-input] + [status-im.contexts.preview.quo.wallet.token-input :as token-input] + [status-im.contexts.preview.quo.wallet.transaction-progress :as transaction-progress] + [status-im.contexts.preview.quo.wallet.transaction-summary :as + transaction-summary] + [status-im.contexts.preview.quo.wallet.wallet-activity :as wallet-activity] + [status-im.contexts.preview.quo.wallet.wallet-overview :as wallet-overview] + [utils.re-frame :as rf])) + +(def screens-categories + {:foundations [{:name :screen/gradients + :component gradients/view} + {:name :screen/shadows + :component shadows/view}] + :animated-list [{:name :screen/animated-header-list + :component animated-header-list/mock-screen}] + :avatar [{:name :screen/community-avatar + :component community-avatar/view} + {:name :screen/dapp-avatar + :component dapp-avatar/view} + {:name :screen/group-avatar + :component group-avatar/view} + {:name :screen/icon-avatar + :component icon-avatar/view} + {:name :screen/token-avatar + :component token-avatar/view} + {:name :screen/user-avatar + :component user-avatar/view} + {:name :screen/wallet-user-avatar + :component wallet-user-avatar/view} + {:name :screen/channel-avatar + :component channel-avatar/view} + {:name :screen/collection-avatar + :component collection-avatar/view} + {:name :screen/account-avatar + :component account-avatar/view}] + :banner [{:name :screen/alert-banners + :component alert-banner/view} + {:name :screen/banner + :component banner/view}] + :buttons [{:name :screen/button + :component button/view} + {:name :screen/composer-button + :component composer-button/view} + {:name :screen/dynamic-button + :component dynamic-button/view} + {:name :screen/slide-button + :component slide-button/view} + {:name :screen/swap-order-button + :component swap-order-button/view} + {:name :screen/predictive-keyboard + :component predictive-keyboard/view} + {:name :screen/wallet-button + :component wallet-button/view} + {:name :screen/wallet-ctas + :component wallet-ctas/view}] + :browser [{:name :screen/browser-input + :component browser-input/view}] + :calendar [{:name :screen/calendar + :component calendar/view} + {:name :screen/calendar-day + :component calendar-day/view} + {:name :screen/calendar-year + :component calendar-year/view}] + :cards [{:name :screen/wallet-card + :component wallet-card/view}] + :code [{:name :screen/snippet + :component code-snippet/view} + {:name :screen/snippet-preview + :component code-snippet-preview/view}] + :colors [{:name :screen/color-picker + :component color-picker/view} + {:name :screen/color + :component color/view}] + :community [{:name :screen/community-card-view + :component community-card/view} + {:name :screen/community-detail-token-gating + :component community-detail-token-gating/view} + {:name :screen/community-token-gating + :component community-token-gating/view} + {:name :screen/community-membership-list-view + :component community-membership-list-view/view} + {:name :screen/community-stat + :component community-stat/view} + {:name :screen/discover-card + :component discover-card/view} + {:name :screen/channel-action + :options {:insets {:bottom? true}} + :component channel-action/view} + {:name :screen/channel-actions + :options {:insets {:bottom? true}} + :component channel-actions/view}] + :counter [{:name :screen/collectible-counter + :component collectible-counter/view} + {:name :screen/counter + :component counter/view} + {:name :screen/step + :component step/view}] + :dividers [{:name :screen/divider-label + :component divider-label/view} + {:name :screen/divider-line + :component divider-line/view} + {:name :screen/new-messages + :component new-messages/view} + {:name :screen/divider-date + :component divider-date/view} + {:name :screen/strength-divider + :component strength-divider/view}] + :drawers [{:name :screen/action-drawers + :component action-drawers/view} + {:name :screen/documentation-drawer + :component documentation-drawers/view} + {:name :screen/drawer-action + :component drawer-action/view} + {:name :screen/drawer-buttons + :component drawer-buttons/view} + {:name :screen/drawer-top + :component drawer-top/view} + {:name :screen/permission-drawers + :component permission-drawers/view} + {:name :screen/bottom-actions + :component + bottom-actions/view}] + :dropdowns [{:name :screen/dropdown + :component dropdown/view} + {:name :screen/network-dropdown + :component network-dropdown/view} + {:name :screen/dropdown-input + :component dropdown-input/view}] + :empty-state [{:name :screen/empty-state + :component empty-state/view}] + :gradient [{:name :screen/gradient-cover + :component gradient-cover/view}] + :graph [{:name :screen/interactive-graph + :options {:topBar {:visible true}} + :component interactive-graph/view} + {:name :screen/wallet-graph + :options {:topBar {:visible true}} + :component wallet-graph/view}] + :info [{:name :screen/info-message + :component info-message/view} + {:name :screen/information-box + :component information-box/view}] + :inputs [{:name :screen/input + :component input/view} + {:name :screen/address-input + :component address-input/view} + {:name :screen/locked-input + :component locked-input/view} + {:name :screen/profile-input + :component profile-input/view} + {:name :screen/recovery-phrase-input + :component recovery-phrase-input/view} + {:name :screen/search-input + :component search-input/view} + {:name :screen/title-input + :component title-input/view}] + :ios [{:name :screen/drawer-bar + :component drawer-bar/view}] + :numbered-keyboard [{:name :screen/keyboard-key + :component keyboard-key/view} + {:name :screen/numbered-keyboard + :component numbered-keyboard/view}] + :pin-input [{:name :screen/pin-input + :component pin-input/view}] + :links [{:name :screen/internal-link-card + :options {:insets {:top true}} + :component internal-link-card/view} + {:name :screen/url-preview + :options {:insets {:top? true}} + :component url-preview/view} + {:name :screen/url-preview-list + :options {:insets {:top? true}} + :component url-preview-list/view} + {:name :screen/link-preview + :options {:insets {:top? true}} + :component link-preview/view}] + :list-items [{:name :screen/account + :component account-item/view} + {:name :screen/account-list-card + :component account-list-card/view} + {:name :screen/address + :component address/view} + {:name :screen/approval-info + :component approval-info/view} + {:name :screen/channel + :component channel/view} + {:name :screen/community-list + :options {:insets {:top? true}} + :component community-list-item/view} + {:name :screen/dapp + :component dapp/preview} + {:name :screen/missing-keypair + :component missing-keypair/view} + {:name :screen/network-list + :component network-list/view} + {:name :screen/preview-lists + :component preview-lists/view} + {:name :screen/quiz-item + :component quiz-item/view} + {:name :screen/saved-address + :component saved-address/view} + {:name :screen/saved-contact-address + :component saved-contact-address/view} + {:name :screen/token-network + :component token-network/view} + {:name :screen/token-value + :component token-value/view} + {:name :screen/market-token + :component market-token/view} + {:name :screen/user-list + :options {:topBar {:visible true}} + :component user-list/view} + {:name :screen/status-list-item + :component status-list-item/view}] + :loaders [{:name :screen/skeleton-list + :options {:topBar {:visible true}} + :component skeleton-list/view}] + :markdown [{:name :screen/texts + :component text/view} + {:name :screen/markdown-list + :component markdown-list/view}] + :messages [{:name :screen/author + :component messages-author/view} + {:name :screen/gap + :component messages-gap/view} + {:name :screen/system-messages + :component system-message/view}] + :navigation [{:name :screen/bottom-nav-tab + :component bottom-nav-tab/view} + {:name :screen/top-nav + :component top-nav/view} + {:name :screen/page-nav + :component page-nav/view} + {:name :screen/floating-shell-button + :component floating-shell-button/view}] + :notifications [{:name :screen/activity-logs + :component activity-logs/view} + {:name :screen/activity-logs-photos + :component activity-logs-photos/view} + {:name :screen/toast + :component toast/view} + {:name :screen/notification + :component notification/view}] + :onboarding [{:name :screen/small-option-card + :component small-option-card/view}] + :password [{:name :screen/tips + :component tips/view} + {:name :screen/password-tips + :component password-tips/view}] + :profile [{:name :screen/collectible + :component collectible/view} + {:name :screen/collectible-list-item + :component collectible-list-item/view} + {:name :screen/expanded-collectible + :component expanded-collectible/view} + {:name :screen/link-card + :component link-card/view} + {:name :screen/profile-card + :component profile-card/view} + {:name :screen/select-profile + :component select-profile/view} + {:name :screen/showcase-nav + :component showcase-nav/view}] + :record-audio [{:name :screen/record-audio + :component record-audio/view}] + :selectors [{:name :screen/disclaimer + :component disclaimer/view} + {:name :screen/filter + :component filter/view} + {:name :screen/selectors + :component selectors/view} + {:name :screen/reactions-selector + :component reactions-selector/preview} + {:name :screen/react-selector + :component (react-selector/preview-react-selector)} + {:name :screen/react + :component react/preview-react}] + :settings [{:name :screen/privacy-option + :component privacy-option/view} + {:name :screen/page-setting + :component page-setting/view} + {:name :screen/accounts + :component accounts/view} + {:name :screen/settings-item + :component settings-item/view} + {:name :screen/reorder-item + :component reorder-item/view} + {:name :screen/category + :component category/view} + {:name :screen/data-item + :component data-item/view} + {:name :screen/section-label + :component section-label/view}] + :share [{:name :screen/qr-code + :component qr-code/view} + {:name :screen/share-qr-code + :component share-qr-code/view}] + :slideshow [{:name :screen/slider-bar + :component slider-bar/view}] + :tabs [{:name :screen/segmented + :component segmented/view} + {:name :screen/tabs + :component tabs/view} + {:name :screen/account-selector + :component account-selector/view}] + :tags [{:name :screen/collectible-tag + :component collectible-tag/view} + {:name :screen/context-tags + :component context-tags/view} + {:name :screen/network-status-tag + :component network-status-tag/view} + {:name :screen/network-tags + :component network-tags/view} + {:name :screen/number-tag + :component number-tag/view} + {:name :screen/permission-tag + :component permission-tag/view} + {:name :screen/status-tags + :component status-tags/view} + {:name :screen/summary-tag + :component summary-tag/view} + {:name :screen/tag + :component tag/view} + {:name :screen/tags + :component tags/view} + {:name :screen/tiny-tag + :component tiny-tag/view} + {:name :screen/token-tag + :component token-tag/view}] + :text-combinations [{:name :screen/text-combinations + :component text-combinations/view} + {:name :screen/channel-name + :component channel-name/view} + {:name :screen/page-top + :component page-top/view} + {:name :screen/standard-title + :component standard-title/view} + {:name :screen/username + :component username/view}] + :wallet [{:name :screen/account-card :component account-card/view} + {:name :screen/account-origin :component account-origin/view} + {:name :screen/account-overview + :component account-overview/view} + {:name :screen/account-permissions + :component account-permissions/view} + {:name :screen/amount-input + :component amount-input/view} + {:name :screen/approval-label + :component approval-label/view} + {:name :screen/confirmation-progress + :component confirmation-progress/view} + {:name :screen/keypair :component keypair/view} + {:name :screen/missing-keypairs + :component missing-keypairs/view} + {:name :screen/network-amount :component network-amount/view} + {:name :screen/network-bridge :component network-bridge/view} + {:name :screen/network-link :component network-link/view} + {:name :screen/network-routing :component network-routing/view} + {:name :screen/progress-bar :component progress-bar/view} + {:name :screen/required-tokens + :component required-tokens/view} + {:name :screen/summary-info :component summary-info/view} + {:name :screen/swap-input :component swap-input/view} + {:name :screen/token-input :component token-input/view} + {:name :screen/wallet-activity :component wallet-activity/view} + {:name :screen/transaction-progress :component transaction-progress/view} + {:name :screen/transaction-summary :component transaction-summary/view} + {:name :screen/wallet-overview + :component wallet-overview/view}] + :keycard [{:name :screen/keycard-component :component keycard/view}]}) + +(defn- category-view + [] + (let [open? (reagent/atom false) + on-press #(swap! open? not)] + (fn [category] + [rn/view {:style {:margin-vertical 8}} + [quo/dropdown + {:type :grey + :state (if @open? :active :default) + :on-press on-press} + (name (key category))] + (when @open? + (for [{category-name :name} (val category)] + ^{:key category-name} + [quo/button + {:type :outline + :container-style {:margin-vertical 8} + :on-press #(rf/dispatch [:open-modal category-name])} + (name category-name)]))]))) + +(defn- main-screen + [] + (let [theme (quo.context/use-theme)] + [:<> + [common/navigation-bar {:title "Quo components preview"}] + [rn/scroll-view {:style (style/main theme)} + (for [category (sort screens-categories)] + ^{:key (first category)} + [category-view category])]])) + +(def screens + (->> screens-categories + (map val) + flatten + (map (fn [subcategory] + (update-in subcategory + [:options :topBar] + merge + {:visible false}))))) + +(def main-screens + [{:name :screen/quo-preview + :options {:topBar {:visible false} + :insets {:top? true}} + :component main-screen}]) diff --git a/src/status_im/contexts/preview/quo/markdown/list.cljs b/src/status_im/contexts/preview/quo/markdown/list.cljs new file mode 100644 index 00000000000..84f932c0840 --- /dev/null +++ b/src/status_im/contexts/preview/quo/markdown/list.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.preview.quo.markdown.list + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title :type :text} + {:key :description :type :text} + {:key :tag-name :type :text} + {:key :description-after-tag :type :text} + {:key :step-number :type :text} + {:key :blur? :type :boolean} + {:key :type + :type :select + :options [{:key :bullet} + {:key :step} + {:key :lock}]} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:title "Be respectful" + :description "Lorem ipsum dolor sit amet."})] + (fn [] + (let [{:keys [title tag-name]} @state + tag-picture (when tag-name (resources/get-mock-image :monkey))] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? (:blur? @state)} + [quo/markdown-list + (assoc @state + :title (when (pos? (count title)) title) + :tag-picture tag-picture)]])))) diff --git a/src/status_im/contexts/preview/quo/markdown/text.cljs b/src/status_im/contexts/preview/quo/markdown/text.cljs new file mode 100644 index 00000000000..610e178a2cd --- /dev/null +++ b/src/status_im/contexts/preview/quo/markdown/text.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.preview.quo.markdown.text + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :heading-1} + {:key :heading-2} + {:key :paragraph-1} + {:key :paragraph-2} + {:key :label}]} + {:key :weight + :type :select + :options [{:key :regular} + {:key :medium} + {:key :semi-bold} + {:key :monospace}]}]) + +(defn view + [] + (let [state (reagent/atom {})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60}} + [quo/text @state + "The quick brown fox jumped over the lazy dog."]]))) diff --git a/src/status_im/contexts/preview/quo/messages/author.cljs b/src/status_im/contexts/preview/quo/messages/author.cljs new file mode 100644 index 00000000000..9d6f8e27f31 --- /dev/null +++ b/src/status_im/contexts/preview/quo/messages/author.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.preview.quo.messages.author + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :primary-name :type :text :limit 24} + {:key :secondary-name :type :text} + {:key :short-chat-key :type :text} + {:key :time-str :type :text :limit 5} + {:key :contact? :type :boolean} + {:key :verified? :type :boolean} + {:key :muted? :type :boolean} + {:key :untrustworthy? :type :boolean} + {:key :size + :type :select + :options [{:key 13 :value "13"} + {:key 15 :value "15"}]}]) + +(defn view + [] + (let [state (reagent/atom {:primary-name "Alisher Yakupov" + :time-str "09:30" + :size 13})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 50 + :padding-horizontal 20}} + [quo/author @state]]))) diff --git a/src/status_im/contexts/preview/quo/messages/gap.cljs b/src/status_im/contexts/preview/quo/messages/gap.cljs new file mode 100644 index 00000000000..dab793029ca --- /dev/null +++ b/src/status_im/contexts/preview/quo/messages/gap.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.preview.quo.messages.gap + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.i18n :as i18n])) + +(def descriptor + [{:key :timestamp-far :type :text} + {:key :timestamp-near :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:timestamp-far "Jan 8 · 09:12" + :timestamp-near "Mar 8 · 22:42" + :on-info-button-pressed identity + :on-press #(println "fill gaps") + :warning-label (i18n/label :t/messages-gap-warning)})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60}} + [quo/gap @state]]))) diff --git a/src/status_im/contexts/preview/quo/messages/system_message.cljs b/src/status_im/contexts/preview/quo/messages/system_message.cljs new file mode 100644 index 00000000000..d05f7f84c7b --- /dev/null +++ b/src/status_im/contexts/preview/quo/messages/system_message.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.preview.quo.messages.system-message + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:label "Message Content" + :key :content + :type :text} + {:label "Message Type" + :key :type + :type :select + :options [{:value "Message pinned" + :key :pinned} + {:key :contact-request} + {:value "User added" + :key :added} + {:value "User removed" + :key :removed} + {:value "Message deleted" + :key :deleted}]} + {:key :pinned-by :type :text} + {:key :incoming? :type :boolean} + (preview/customization-color-option)]) + +(defn finalize-state + [state] + (merge @state + {:child (when (= (:type @state) :pinned) [rn/text (:content @state)]) + :display-name (:pinned-by @state) + :photo-path (resources/mock-images :user-picture-female2)})) + +(defn view + [] + (let [state (reagent/atom {:type :pinned + :pinned-by "Steve" + :timestamp "09:41" + :content "Hello! This is an example of a content!"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60}} + [quo/system-message (finalize-state state)]]))) diff --git a/src/status_im/contexts/preview/quo/navigation/bottom_nav_tab.cljs b/src/status_im/contexts/preview/quo/navigation/bottom_nav_tab.cljs new file mode 100644 index 00000000000..f2856ba4840 --- /dev/null +++ b/src/status_im/contexts/preview/quo/navigation/bottom_nav_tab.cljs @@ -0,0 +1,63 @@ +(ns status-im.contexts.preview.quo.navigation.bottom-nav-tab + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :icon + :type :select + :options [{:key :i/communities + :value "Communities"} + {:key :i/messages + :value "Messages"} + {:key :i/wallet + :value "Wallet"} + {:key :i/browser + :value "Browser"}]} + {:key :selected? :type :boolean} + {:key :pass-through? :type :boolean} + {:key :new-notifications? :type :boolean} + {:key :notification-indicator + :type :select + :options [{:key :counter} + {:key :unread-dot}]} + {:key :counter-label :type :text} + (preview/customization-color-option)]) + +(defn get-icon-color + [selected? pass-through?] + (cond + selected? colors/white + pass-through? colors/white-opa-40 + :else colors/neutral-50)) + +(defn- f-bottom-tab + [state selected? pass-through?] + (let [icon-color-anim (reanimated/use-shared-value colors/white)] + (reanimated/set-shared-value + icon-color-anim + (get-icon-color selected? pass-through?)) + [quo/bottom-nav-tab + (merge state {:icon-color-anim icon-color-anim}) + (:value state)])) + +(defn view + [] + (let [state (reagent/atom {:icon :i/communities + :new-notifications? true + :notification-indicator :counter + :counter-label 8 + :preview-label-color colors/white + :customization-color :turquoise}) + selected? (reagent/cursor state [:selected?]) + pass-through? (reagent/cursor state [:pass-through?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60 + :align-items :center}} + [:f> f-bottom-tab @state @selected? @pass-through?]]))) diff --git a/src/status_im/contexts/preview/quo/navigation/floating_shell_button.cljs b/src/status_im/contexts/preview/quo/navigation/floating_shell_button.cljs new file mode 100644 index 00000000000..5340de56c96 --- /dev/null +++ b/src/status_im/contexts/preview/quo/navigation/floating_shell_button.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.preview.quo.navigation.floating-shell-button + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :show-search? :type :boolean} + {:key :show-mention? :type :boolean} + {:key :scroll-type + :type :select + :options [{:key :notification-up} + {:key :notification-down} + {:key :scroll-to-bottom}]}]) + +(defn mock-data + [{:keys [show-search? show-mention? scroll-type]}] + (cond-> {} + show-search? + (assoc :search {:on-press #()}) + show-mention? + (assoc :mention {:on-press #() :count 6 :customization-color :turquoise}) + (= scroll-type :notification-up) + (assoc :notification-up {:on-press #() :count 8}) + (= scroll-type :notification-down) + (assoc :notification-down {:on-press #() :count 8}) + (= scroll-type :scroll-to-bottom) + (assoc :scroll-to-bottom {:on-press #()}))) + +(defn view + [] + (let [state (reagent/atom {:scroll-type :notification-down})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60 + :align-items :center}} + [quo/floating-shell-button (mock-data @state) nil]]))) diff --git a/src/status_im/contexts/preview/quo/navigation/page_nav.cljs b/src/status_im/contexts/preview/quo/navigation/page_nav.cljs new file mode 100644 index 00000000000..5b810b65357 --- /dev/null +++ b/src/status_im/contexts/preview/quo/navigation/page_nav.cljs @@ -0,0 +1,234 @@ +(ns status-im.contexts.preview.quo.navigation.page-nav + (:require + [clojure.string :as string] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [quo.foundations.resources :as quo.resources] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def ^:private networks + [{:source (quo.resources/get-network :ethereum)} + {:source (quo.resources/get-network :optimism)} + {:source (quo.resources/get-network :arbitrum)}]) + +(def ^:private descriptor + [{:key :type + :type :select + :options [{:key :no-title} + {:key :title} + {:key :dropdown} + {:key :token} + {:key :channel} + {:key :title-description + :value "Title + Description"} + {:key :wallet-networks} + {:key :community} + {:key :network}]} + {:key :background + :type :select + :options (map (fn [bg-type] + {:key bg-type + :value (string/capitalize (name bg-type))}) + [:white :neutral-5 :neutral-90 :neutral-95 :neutral-100 :photo :blur])} + {:key :icon-name + :type :select + :options [{:key :i/placeholder + :value "Placeholder"} + {:key :i/arrow-left + :value "Arrow left"}]}]) + + +(def account-switcher-option + {:content-type :account-switcher + :customization-color :purple + :on-press #(js/alert "Pressed Account Switcher") + :emoji "🍑"}) + +(def right-side-options + (let [options [{:icon-name :i/save :on-press #(js/alert "SAVE")} + {:icon-name :i/mark-as-read :on-press #(js/alert "MARK AS READ")} + {:icon-name :i/mention :on-press #(js/alert "A MENTION!")}]] + [{:key [] + :value "No actions"} + {:key (take 1 options) + :value "1 action"} + {:key (take 2 options) + :value "2 actions"} + {:key (take 3 options) + :value "3 actions"} + {:key (conj (take 1 options) account-switcher-option) + :value "1 action + account switcher"} + {:key (conj (take 2 options) account-switcher-option) + :value "2 actions + account switcher"}])) + +(def no-title-descriptor + [{:key :right-side + :type :select + :options right-side-options}]) + +(def title-descriptor + [{:key :right-side + :type :select + :options right-side-options} + {:key :title :type :text} + {:key :text-align + :type :select + :options [{:key :left} + {:key :center}]}]) + +(def dropdown-descriptor + [{:key :dropdown-selected? :type :boolean} + {:key :dropdown-text :type :text}]) + +(def token-descriptor + [{:key :right-side + :type :select + :options right-side-options} + {:key :token-logo + :type :select + :options [{:key (resources/get-mock-image :status-logo) + :value "Status logo"} + {:key (resources/get-mock-image :rarible) + :value "Rarible"}]} + {:key :token-name + :type :text} + {:key :token-abbreviation + :type :text}]) + +(def channel-descriptor + [{:key :right-side + :type :select + :options right-side-options} + {:key :channel-emoji + :type :select + :options [{:key "🍇" + :value "🍇"} + {:key "🍑" + :value "🍑"}]} + + {:key :channel-name :type :text} + {:key :channel-icon + :type :select + :options [{:key :i/locked + :value "Locked"} + {:key :i/unlocked + :value "Unlocked"}]}]) + +(def title-description-descriptor + [{:key :right-side + :type :select + :options (butlast right-side-options)} + {:key :title :type :text} + {:key :description :type :text} + {:key :picture + :type :select + :options [{:key nil + :value "No picture"} + {:key (resources/get-mock-image :photo1) + :value "Photo 1"} + {:key (resources/get-mock-image :photo2) + :value "Photo 2"}]}]) + +(def wallet-networks-descriptor + [{:key :right-side + :type :select + :options right-side-options}]) + +(def community-descriptor + [{:key :right-side + :type :select + :options right-side-options} + {:key :community-logo + :type :select + :options [{:key (resources/get-mock-image :diamond) + :value "Diamond"} + {:key (resources/get-mock-image :coinbase) + :value "Coinbase"}]} + {:key :community-name :type :text}]) + +(def network-descriptor + [{:key :right-side + :type :select + :options right-side-options} + {:key :network-logo + :type :select + :options [{:key (resources/get-mock-image :diamond) + :value "Diamond"} + {:key (resources/get-mock-image :coinbase) + :value "Coinbase"}]} + {:key :network-name + :type :text}]) + +(defn- photo-bg + [background] + (when (#{:photo} background) + [rn/image + {:style {:position :absolute + :top 0 + :bottom 0 + :left 20 + :right 0 + :width "100%" + :height 200} + :source (resources/get-mock-image :photo2)}])) + +(defn view + [{:keys [theme]}] + (let [state (reagent/atom + {:type :title-description + :background (if (= theme :light) :white :neutral-90) + :icon-name :i/placeholder + :on-press #(js/alert "Left icon pressed!") + :right-side [{:icon-name :i/save :on-press #(js/alert "SAVE")} + {:icon-name :i/mark-as-read :on-press #(js/alert "MARK AS READ")} + {:icon-name :i/mention :on-press #(js/alert "A MENTION!")}] + :title "Page title" + :text-align :center + :dropdown-on-press #(js/alert "Dropdown pressed!") + :dropdown-selected? false + :dropdown-text "Recent" + :token-logo (resources/get-mock-image :status-logo) + :token-name "Status" + :token-abbreviation "SNT" + :channel-emoji "🍇" + :channel-name "general" + :channel-icon :i/locked + :description "Description" + :picture (resources/get-mock-image :photo1) + :community-name "Rarible" + :community-logo (resources/get-mock-image :coinbase) + :network-name "Ethereum" + :network-logo (resources/get-mock-image :diamond) + :networks networks})] + (fn [] + [preview/preview-container + {:state state + :descriptor (concat descriptor + (case (:type @state) + :no-title no-title-descriptor + :title title-descriptor + :dropdown dropdown-descriptor + :token token-descriptor + :channel channel-descriptor + :title-description title-description-descriptor + :wallet-networks wallet-networks-descriptor + :community community-descriptor + :network network-descriptor + nil)) + :blur? (= :blur (:background @state)) + :show-blur-background? (= :blur (:background @state)) + :component-container-style {:background-color (case (:background @state) + :white colors/white + :neutral-5 colors/neutral-5 + :neutral-90 colors/neutral-90 + :neutral-95 colors/neutral-95 + :neutral-100 colors/neutral-100 + nil) + :margin-vertical 40 + :width "100%"}} + + [photo-bg (:background @state)] + [quo/page-nav @state]]))) diff --git a/src/status_im/contexts/preview/quo/navigation/top_nav.cljs b/src/status_im/contexts/preview/quo/navigation/top_nav.cljs new file mode 100644 index 00000000000..5d20c005683 --- /dev/null +++ b/src/status_im/contexts/preview/quo/navigation/top_nav.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.preview.quo.navigation.top-nav + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :notification + :type :select + :options [{:key :mention} + {:key :notification} + {:key :seen}]} + {:key :blur? + :type :boolean} + {:key :notification-count + :type :number} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:notification-count 0 + :customization-color :blue})] + (fn [] + (let [blur? (:blur? @state) + customization-color (:customization-color @state) + notification (:notification @state) + notification-count (:notification-count @state)] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? blur? + :show-blur-background? blur? + :component-container-style {:padding-vertical 60 + :padding-horizontal 20}} + [quo/top-nav + {:container-style {:flex 1 :z-index 2} + :max-unread-notifications 99 + :blur? blur? + :notification notification + :customization-color customization-color + :notification-count notification-count + :avatar-props {:online? true + :full-name "Test User" + :profile-picture (resources/mock-images :user-picture-female2)} + :avatar-on-press #(js/alert "avatar pressed") + :scan-on-press #(js/alert "scan pressed") + :activity-center-on-press #(js/alert "activity-center pressed") + :qr-code-on-press #(js/alert "qr pressed")}]])))) + diff --git a/src/status_im/contexts/preview/quo/notifications/activity_logs.cljs b/src/status_im/contexts/preview/quo/notifications/activity_logs.cljs new file mode 100644 index 00000000000..97c85aadb2a --- /dev/null +++ b/src/status_im/contexts/preview/quo/notifications/activity_logs.cljs @@ -0,0 +1,183 @@ +(ns status-im.contexts.preview.quo.notifications.activity-logs + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [status-im.contexts.preview.quo.tags.status-tags :as status-tags])) + +(def descriptor + [{:label "Unread?" + :key :unread? + :type :boolean} + {:label "Replying?" + :key :replying? + :type :boolean} + {:label "Icon" + :key :icon + :type :select + :options [{:key :placeholder + :value :placeholder}]} + {:label "Title" + :key :title + :type :text} + {:label "Context" + :key :context + :type :select + :options [{:key :basic-user-action + :value "Basic user action"} + {:key :complex-user-action + :value "Complex user action"}]} + {:label "Message" + :key :message + :type :select + :options [{:value "Simple" + :key :simple} + {:value "With mention" + :key :with-mention} + {:value "With title" + :key :with-title}]} + {:label "Timestamp" + :key :timestamp + :type :text} + {:label "Button 1 type" + :key :button-1-type + :type :select + :options [{:value "Danger" + :key :danger} + {:value "Primary" + :key :primary} + {:value "Positive" + :key :positive}]} + {:label "Button 1 label" + :key :button-1-label + :type :text} + {:label "Button 2 type" + :key :button-2-type + :type :select + :options [{:value "Danger" + :key :danger} + {:value "Primary" + :key :primary} + {:value "Positive" + :key :positive}]} + {:label "Button 2 label" + :key :button-2-label + :type :text} + status-tags/status-tags-options]) + +(def basic-user-action + [[quo/context-tag + {:size 24 + :full-name "Name" + :blur? true}] + "did something here."]) + +(def complex-user-action + [[quo/context-tag {:size 24 :full-name "Alice" :blur? true}] + "from" + [quo/context-tag {:size 24 :full-name "Ethereum" :blur? true}] + "to" + [quo/context-tag {:size 24 :full-name "Optimism" :blur? true}] + "on" + [quo/context-tag {:size 24 :full-name "My savings" :blur? true}]]) + +(def message-with-mention + (let [common-text-style {:style {:color colors/white} + :size :paragraph-1}] + {:body [rn/view + {:flex 1 + :flex-direction :row} + [quo/text common-text-style "Hello"] + [quo/text + {:style {:background-color colors/primary-50-opa-10 + :border-radius 6 + :color colors/primary-50 + :margin-horizontal 3 + :padding-horizontal 3 + :size :paragraph-1}} + "@name"] + [quo/text common-text-style "! How are you feeling?"]]})) + +(def message-with-title + {:body "Your favorite color is Turquoise." + :title "What's my favorite color?"}) + +(defn view + [] + (let [state (reagent/atom {:button-1-label "Decline" + :button-1-type :danger + :button-2-label "Accept" + :button-2-type :primary + :context :complex-user-action + :icon :placeholder + :message :with-title + :timestamp "Today 00:00" + :title "Activity Title" + :unread? true + :items []})] + (fn [] + (let [theme (quo.context/use-theme) + {:keys [button-1-type + button-1-label + button-2-type + button-2-label + status]} + @state + props (cond-> @state + (and (seq button-1-label) + button-1-type) + (update :items + conj + {:type :button + :label button-1-label + :subtype button-1-type + :on-press #(js/alert "Button 1 Clicked")}) + + (and (seq button-2-label) + button-2-type) + (update :items + conj + {:type :button + :label button-2-label + :subtype button-2-type + :on-press #(js/alert "Button 2 Clicked")}) + + (= (:message @state) :simple) + (assoc :message {:body "The quick brown fox forgot to jump."}) + + (= (:message @state) :with-mention) + (assoc :message message-with-mention) + + (some? status) + (update :items + conj + {:type :status + :subtype status + :blur? true + :label (name status)}) + + (= (:message @state) :with-title) + (assoc :message message-with-title) + + (= (:context @state) :basic-user-action) + (assoc :context basic-user-action) + + (= (:context @state) :complex-user-action) + (assoc :context complex-user-action))] + [preview/preview-container + {:state state + :descriptor descriptor} + [rn/view + {:flex 1 + :padding 16} + [preview/customizer state descriptor theme]] + [quo.context/provider {:theme :dark} + [rn/view + {:background-color colors/neutral-90 + :flex-direction :row + :justify-content :center + :padding-vertical 60} + [quo/activity-log props]]]])))) diff --git a/src/status_im/contexts/preview/quo/notifications/activity_logs_photos.cljs b/src/status_im/contexts/preview/quo/notifications/activity_logs_photos.cljs new file mode 100644 index 00000000000..a11b48165a3 --- /dev/null +++ b/src/status_im/contexts/preview/quo/notifications/activity_logs_photos.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.preview.quo.notifications.activity-logs-photos + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:label "Photos Count:" + :key :count + :type :select + :options [{:key 1 + :value "1"} + {:key 2 + :value "2"} + {:key 3 + :value "3"} + {:key 4 + :value "4"} + {:key 5 + :value "5"} + {:key 6 + :value "6"}]}]) + +(def mock-photos + [(resources/get-mock-image :photo1) + (resources/get-mock-image :photo2) + (resources/get-mock-image :photo3) + (resources/get-mock-image :photo1) + (resources/get-mock-image :photo2) + (resources/get-mock-image :photo3)]) + +(defn view + [] + (let [state (reagent/atom {:count 1})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [rn/view {:padding-bottom 150} + [rn/view + [quo/activity-logs-photos {:photos (take (:count @state) mock-photos)}]]]]))) diff --git a/src/status_im/contexts/preview/quo/notifications/notification.cljs b/src/status_im/contexts/preview/quo/notifications/notification.cljs new file mode 100644 index 00000000000..1b0c956bc65 --- /dev/null +++ b/src/status_im/contexts/preview/quo/notifications/notification.cljs @@ -0,0 +1,94 @@ +(ns status-im.contexts.preview.quo.notifications.notification + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.code.snippet :as snippet-preview] + [status-im.contexts.preview.quo.preview :as preview] + [utils.re-frame :as rf])) + +(defn notification-button + ([id opts] (notification-button id id opts)) + ([text id opts] + (let [notification-opts (rf/sub [:toasts/toast id]) + dismiss! #(rf/dispatch [:toasts/close id]) + notification! #(rf/dispatch [:toasts/upsert (assoc opts :id id)]) + dismissed? (not notification-opts)] + [rn/view {:style {:margin-bottom 10}} + [quo/button + {:size 32 + :on-press #(if dismissed? (notification!) (dismiss!))} + (if dismissed? text (str "DISMISS " text))]]))) + +(defn notification-button-0 + [] + [notification-button + "Notification: empty" + {:duration 4000 + :type :notification}]) + +(defn notification-button-1 + [] + [notification-button + "Notification: with title(header)" + {:user {:full-name "A Y" + :status-indicator? true + :online? true + :size :small + :customization-color :blue} + :title "Alisher Yakupov accepted your contact request" + :duration 4000 + :title-weight :medium + :type :notification}]) + +(defn notification-button-2 + [] + [notification-button + "with title and body" + {:user {:full-name "A Y" + :status-indicator? true + :online? true + :size :small + :customization-color :blue} + :title "Default to semibold title" + :text "The quick brown fox jumped over the lazy dog and ate a potatoe." + :duration 4000 + :type :notification}]) + +(defn notification-button-3 + [] + [notification-button + "with anything as header & body" + {:user {:full-name "A Y" + :status-indicator? true + :online? true + :size :small + :customization-color :blue} + :header [rn/view + [quo/info-message + {:status :success + :size :tiny + :icon :i/placeholder} + "info-message as title"]] + :body [quo/snippet {:language :clojure :max-lines 15 :syntax true} + snippet-preview/clojure-example] + :duration 3000 + :type :notification}]) + +(defn view + [] + (fn [] + [preview/preview-container + [rn/view + {:background-color "#508485" + :flex-direction :column + :justify-content :flex-start + :height 300}] + [into + [rn/view + {:flex 1 + :padding 16 + :margin-top 60}] + [^{:key :0} [notification-button-0] + ^{:key :1} [notification-button-1] + ^{:key :2} [notification-button-2] + ^{:key :3} [notification-button-3]]]])) diff --git a/src/status_im/contexts/preview/quo/notifications/toast.cljs b/src/status_im/contexts/preview/quo/notifications/toast.cljs new file mode 100644 index 00000000000..02587fd742c --- /dev/null +++ b/src/status_im/contexts/preview/quo/notifications/toast.cljs @@ -0,0 +1,107 @@ +(ns status-im.contexts.preview.quo.notifications.toast + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [utils.re-frame :as rf])) + +(defn toast-button + ([id opts] (toast-button id id opts)) + ([text id opts] + (let [toast-opts (rf/sub [:toasts/toast id]) + dismiss! #(rf/dispatch [:toasts/close id]) + toast! #(rf/dispatch [:toasts/upsert (assoc opts :id id)]) + dismissed? (not toast-opts)] + [rn/view {:style {:margin-bottom 10}} + [quo/button + {:size 32 + :on-press #(if dismissed? (toast!) (dismiss!))} + (if dismissed? text (str "DISMISS " text))]]))) + +(defn toast-button-basic + [] + [toast-button + "Toast: basic" + {:text "This is an example toast"}]) + +(defn toast-button-with-undo-action + [] + [toast-button + "Toast: with undo action" + {:type :negative + :text "This is an example toast" + :duration 4000 + :undo-duration 4 + :undo-on-press #(do + (rf/dispatch [:toasts/upsert + {:type :positive + :text "Undo pressed"}]) + (rf/dispatch [:toasts/close + "Toast: with undo action"]))}]) + +(defn toast-button-multiline + [] + [toast-button + "Toast: multiline" + {:type :positive + :text + "This is an example multiline toast This is an example multiline toast This is an example multiline toast" + :undo-duration 4 + :undo-on-press + #(do + (rf/dispatch + [:toasts/upsert + {:type :positive :text "Undo pressed"}]) + (rf/dispatch [:toasts/close "Toast: with undo action"]))}]) + +(defn toast-button-30s-duration + [] + [toast-button + "Toast: 30s duration" + {:type :positive + :text "This is an example toast" + :duration 30000}]) + +(defn toast-button-with-user-avatar + [] + [toast-button + "Toast: with user-avatar" + {:text "This is an example toast" + :user {:profile-picture (resources/mock-images :user-picture-female2) + :size :small}}]) + +(defn update-toast-button + [] + (let [suffix (reagent/atom 0)] + (fn [] + (let [toast-opts (rf/sub [:toasts/toast "Toast: 30s duration"])] + (when toast-opts + [rn/view {:style {:margin-bottom 10}} + [quo/button + {:size 32 + :on-press + #(rf/dispatch + [:toasts/upsert + {:id "Toast: 30s duration" + :type :negative + :text (str "This is an updated example toast" " - " (swap! suffix inc)) + :duration 3000}])} + "update above toast"]]))))) + +(defn view + [] + [preview/preview-container + {:component-container-style + {:flex-direction :column + :justify-content :flex-start}} + [into + [rn/view {:style {:flex 1 :padding 16}} + [toast-button-basic] + [toast-button-with-undo-action] + [toast-button-multiline] + [toast-button-30s-duration] + [toast-button-with-user-avatar] + [update-toast-button] + [update-toast-button]]]]) diff --git a/src/status_im/contexts/preview/quo/numbered_keyboard/keyboard_key.cljs b/src/status_im/contexts/preview/quo/numbered_keyboard/keyboard_key.cljs new file mode 100644 index 00000000000..9b865acdc9a --- /dev/null +++ b/src/status_im/contexts/preview/quo/numbered_keyboard/keyboard_key.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.preview.quo.numbered-keyboard.keyboard-key + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? :type :boolean} + {:key :disabled? :type :boolean} + {:type :select + :key :type + :options [{:key :digit} + {:key :key} + {:key :derivation-path}]}]) + +(defn view + [] + (let [state (reagent/atom {:disabled? false + :on-press #(js/alert "pressed" %) + :blur? false + :type :digit})] + (fn [] + (let [value (case (:type @state) + :key :i/delete + :derivation-path nil + :digit 1 + nil)] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? (:blur? @state)} + [quo/keyboard-key @state value]])))) diff --git a/src/status_im/contexts/preview/quo/numbered_keyboard/numbered_keyboard.cljs b/src/status_im/contexts/preview/quo/numbered_keyboard/numbered_keyboard.cljs new file mode 100644 index 00000000000..d8d471ad823 --- /dev/null +++ b/src/status_im/contexts/preview/quo/numbered_keyboard/numbered_keyboard.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.preview.quo.numbered-keyboard.numbered-keyboard + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? :type :boolean} + {:key :disabled? :type :boolean} + {:key :delete-key? :type :boolean} + {:type :select + :key :left-action + :options [{:key :dot} + {:key :face-id} + {:key :none}]}]) + +(defn view + [] + (let [state (reagent/atom {:disabled? false + :on-press (fn [item] (js/alert (str item " pressed"))) + :blur? false + :delete-key? true + :left-action :dot})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? (:blur? @state) + :blur-height 300} + [quo/numbered-keyboard @state]]))) diff --git a/src/status_im/contexts/preview/quo/onboarding/small_option_card.cljs b/src/status_im/contexts/preview/quo/onboarding/small_option_card.cljs new file mode 100644 index 00000000000..60daa42028f --- /dev/null +++ b/src/status_im/contexts/preview/quo/onboarding/small_option_card.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.preview.quo.onboarding.small-option-card + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :variant + :type :select + :options [{:key :main} + {:key :icon}]} + {:key :image + :type :select + :options [{:key (resources/get-mock-image :small-opt-card-main) + :value "Image 1"} + {:key (resources/get-mock-image :small-opt-card-icon) + :value "Image 2"}]} + {:key :title :type :text} + {:key :subtitle :type :text} + {:key :button-label :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:variant :main + :image (-> descriptor second :options first :key) + :title "Generate keys " + :subtitle "Your new self-sovereign identity in Status" + :button-label "Let's go!" + :on-press #(js/alert "Small option card pressed!")})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding 20 + :background-color colors/neutral-80}} + [quo/small-option-card @state]]))) diff --git a/src/status_im/contexts/preview/quo/password/password_tips.cljs b/src/status_im/contexts/preview/quo/password/password_tips.cljs new file mode 100644 index 00000000000..e3df263aa9f --- /dev/null +++ b/src/status_im/contexts/preview/quo/password/password_tips.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.password.password-tips + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [reagent.core :as reagent] + [status-im.constants :as constant] + [status-im.contexts.preview.quo.preview :as preview])) + +(def init-state + (reduce (fn [acc tip] (assoc acc tip false)) {} constant/password-tips)) + +(defn- make-tip-descriptor + [tip] + {:key tip + :type :boolean}) + +(def descriptor + (map make-tip-descriptor constant/password-tips)) + +(defn view + [] + (let [state (reagent/atom init-state)] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding 20 + :background-color colors/neutral-95}} + [quo/password-tips @state]]))) diff --git a/src/status_im/contexts/preview/quo/password/tips.cljs b/src/status_im/contexts/preview/quo/password/tips.cljs new file mode 100644 index 00000000000..e08ccca7673 --- /dev/null +++ b/src/status_im/contexts/preview/quo/password/tips.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.preview.quo.password.tips + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :text :type :text} + {:key :completed? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:text "Lower case" + :completed? false}) + text (reagent/cursor state [:text]) + completed? (reagent/cursor state [:completed?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding 20 + :background-color colors/neutral-95}} + [quo/tips {:completed? @completed?} @text]]))) diff --git a/src/status_im/contexts/preview/quo/pin_input/pin_input.cljs b/src/status_im/contexts/preview/quo/pin_input/pin_input.cljs new file mode 100644 index 00000000000..6cb22736ae5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/pin_input/pin_input.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.pin-input.pin-input + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? :type :boolean} + {:type :number + :key :number-of-pins} + {:type :number + :key :number-of-filled-pins} + {:type :boolean + :key :error?} + {:type :boolean + :key :info-error?} + {:type :text + :key :info}]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:blur? false + :number-of-pins 6 + :number-of-filled-pins 0})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [rn/view {:style {:padding-vertical 40 :align-items :center :justify-content :center}} + [quo/pin-input state]]])) diff --git a/src/status_im/contexts/preview/quo/preview.cljs b/src/status_im/contexts/preview/quo/preview.cljs new file mode 100644 index 00000000000..3953d6ebecf --- /dev/null +++ b/src/status_im/contexts/preview/quo/preview.cljs @@ -0,0 +1,430 @@ +(ns status-im.contexts.preview.quo.preview + (:require + [camel-snake-kebab.core :as camel-snake-kebab] + cljs.pprint + [clojure.string :as string] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.common :as common] + [status-im.contexts.preview.quo.style :as style] + utils.number + [utils.re-frame :as rf])) + +(defn- label-view + [_ label theme] + [rn/view {:style style/label-container} + [rn/text {:style (style/label theme)} + label]]) + +(defn- humanize + [k] + ;; We explicitly convert `k` to string because sometimes it's a number and + ;; Clojure would throw an exception. + (-> (if (keyword? k) k (str k)) + camel-snake-kebab/->kebab-case-keyword + name + (string/replace "-" " ") + string/capitalize)) + +(defn- key->boolean-label + [k] + (let [label (humanize k)] + (if (string/ends-with? label "?") + label + (str label "?")))) + +(defn- key->text-label + [k] + (str (humanize k) ":")) + +(defn- customizer-boolean + [{:keys [label state set-state] :as args}] + (let [theme (quo.context/use-theme) + label (or label (key->boolean-label (:key args))) + field-cursor (when-not (fn? set-state) + (reagent/cursor state [(:key args)])) + field-value (if (fn? set-state) + (get state (:key args)) + @field-cursor) + set-field-value (fn [value] + (if (fn? set-state) + (set-state (assoc state (:key args) value)) + (reset! field-cursor value)))] + [rn/view {:style style/field-row} + [label-view state label theme] + [rn/view {:style (style/boolean-container)} + [rn/pressable + {:style (style/boolean-button {:active? field-value :left? true} theme) + :on-press #(set-field-value true)} + [rn/text {:style (style/field-text field-value theme)} + "True"]] + [rn/pressable + {:style (style/boolean-button {:active? (not field-value) :left? false} theme) + :on-press #(set-field-value false)} + [rn/text {:style (style/field-text (not field-value) theme)} + "False"]]]])) + +(defn- customizer-text + [{:keys [label state set-state limit suffix] :as args} theme] + (let [label (or label (key->text-label (:key args))) + field-cursor (when-not (fn? set-state) + (reagent/cursor state [(:key args)])) + field-value (if (fn? set-state) + (get state (:key args)) + @field-cursor) + set-field-value (fn [value] + (if (fn? set-state) + (set-state (assoc state (:key args) value)) + (reset! field-cursor value)))] + [rn/view {:style style/field-row} + [label-view state label theme] + [rn/view {:style style/field-column} + [rn/text-input + (merge + {:value field-value + :show-cancel false + :style (style/field-container false theme) + :keyboard-appearance theme + :on-change-text (fn [text] + (set-field-value (if (and suffix + (> (count text) (count field-value))) + (str (string/replace text suffix "") suffix) + text)) + (when-not (fn? set-state) + (reagent/flush)))} + (when limit + {:max-length limit}))]]])) + +(defn- customizer-number + [{:keys [label state set-state default] :as args} theme] + (let [label (or label (key->text-label (:key args))) + field-cursor (when-not (fn? set-state) + (reagent/cursor state [(:key args)])) + field-value (if (fn? set-state) + (get state (:key args)) + @field-cursor) + set-field-value (fn [value] + (if (fn? set-state) + (set-state (assoc state (:key args) value)) + (reset! field-cursor value)))] + [rn/view {:style style/field-row} + [label-view state label theme] + [rn/view {:style style/field-column} + [rn/text-input + {:value (str field-value) + :show-cancel false + :style (style/field-container false theme) + :keyboard-appearance theme + :on-change-text (fn [text] + (set-field-value (if (or (= "-" text) (string/ends-with? text ".")) + text + (utils.number/parse-float text default))) + (when-not (fn? set-state) + (reagent/flush)))}]]])) + +(defn- find-selected-option + [id v] + (first (filter #(= (:key %) id) v))) + +(defn- customizer-select-modal + [{:keys [open options field-value set-field-value]}] + (let [theme (quo.context/use-theme)] + [rn/modal + {:visible @open + :on-request-close #(reset! open false) + :status-bar-translucent true + :transparent true + :animation :slide} + [rn/view {:style (style/modal-overlay theme)} + [rn/view {:style (style/modal-container theme)} + [rn/scroll-view {:shows-vertical-scroll-indicator false} + (doall + (for [{k :key v :value} options + :let [v (or v (humanize k))]] + ^{:key k} + [rn/pressable + {:style (style/select-option (= field-value k) theme) + :on-press (fn [] + (reset! open false) + (set-field-value k))} + [rn/text {:style (style/field-text (= field-value k) theme)} + v]]))] + [rn/view {:style (style/footer theme)} + [rn/pressable + {:style (style/select-button theme) + :on-press (fn [] + (set-field-value nil) + (reset! open false))} + [rn/text {:style (style/field-text false theme)} + "Clear"]] + [rn/view {:style {:width 16}}] + [rn/touchable-opacity + {:style (style/select-button theme) + :on-press #(reset! open false)} + [rn/text {:style (style/field-text false theme)} + "Close"]]]]]])) + +(defn- customizer-select-button + [{:keys [open selected-option]} theme] + [rn/pressable + {:style (style/select-container theme) + :on-press #(reset! open true)} + [rn/text + {:style (style/field-select theme) + :number-of-lines 1} + (if selected-option + (or (:value selected-option) (humanize (:key selected-option))) + "Select option")] + [rn/view + [quo/icon :i/chevron-right]]]) + +(defn- customizer-select + [] + (let [open (reagent/atom nil)] + (fn [{:keys [label state set-state options] :as args}] + (let [theme (quo.context/use-theme) + label (or label (key->text-label (:key args))) + field-cursor (when-not (fn? set-state) + (reagent/cursor state [(:key args)])) + field-value (if (fn? set-state) + (get state (:key args)) + @field-cursor) + set-field-value (fn [value] + (if (fn? set-state) + (set-state (assoc state (:key args) value)) + (reset! field-cursor value))) + selected-option (find-selected-option field-value options)] + [rn/view {:style style/field-row} + [label-view state label theme] + [rn/view {:style style/field-column} + [customizer-select-modal + {:open open + :options options + :field-value field-value + :set-field-value set-field-value}] + [customizer-select-button {:open open :selected-option selected-option} theme]]])))) + +(defn- customizer-multi-select-modal + [{:keys [open-atom options field-value set-field-value]}] + (let [theme (quo.context/use-theme)] + [rn/modal + {:visible @open-atom + :on-request-close #(reset! open-atom false) + :status-bar-translucent true + :transparent true + :animation :slide} + [rn/view {:style (style/modal-overlay theme)} + [rn/view {:style (style/modal-container theme)} + [rn/scroll-view {:shows-vertical-scroll-indicator false} + (doall + (for [{k :key v :value} options + :let [v (or v (humanize k))]] + ^{:key k} + (let [checked? (boolean (some #(= k %) field-value)) + remove-key (fn [v] (filterv #(not= % k) v)) + on-press (fn [] + (set-field-value + (if checked? + (remove-key field-value) + (conj field-value k))))] + [rn/pressable + {:style (style/multi-select-option theme) + :on-press on-press} + [rn/text {:style (style/field-text false theme)} v] + [quo/selectors + {:type :checkbox + :checked? checked? + :on-change on-press}]])))] + [rn/view {:style (style/footer theme)} + [rn/pressable + {:style (style/select-button theme) + :on-press (fn [] + (set-field-value nil) + (reset! open-atom false))} + [rn/text {:style (style/field-text false theme)} + "Clear"]] + [rn/view {:style {:width 16}}] + [rn/touchable-opacity + {:style (style/select-button theme) + :on-press #(reset! open-atom false)} + [rn/text {:style (style/field-text false theme)} + "Close"]]]]]])) + +(defn filter-by-keys + [items ks] + (filter (fn [item] + (some #(= (:key item) %) ks)) + items)) + +(defn- customizer-multi-select-button + [{:keys [open selected-options]} theme] + [rn/pressable + {:style (style/select-container theme) + :on-press #(reset! open true)} + [rn/text + {:style (style/field-select theme) + :number-of-lines 1} + (if (seq selected-options) + (string/join ", " (map :value selected-options)) + "Select options")] + [rn/view + [quo/icon :i/chevron-right]]]) + +(defn- customizer-multi-select + [] + (let [open (reagent/atom nil)] + (fn [{:keys [label state set-state options] :as args}] + (let [theme (quo.context/use-theme) + label (or label (key->text-label (:key args))) + field-cursor (when-not (fn? set-state) + (reagent/cursor state [(:key args)])) + field-value (if (fn? set-state) + (get state (:key args)) + @field-cursor) + set-field-value (fn [value] + (if (fn? set-state) + (set-state (assoc state (:key args) value)) + (reset! field-cursor value))) + selected-options (filter-by-keys options field-value)] + [rn/view {:style style/field-row} + [label-view state label theme] + [rn/view {:style style/field-column} + [customizer-multi-select-modal + {:open-atom open + :field-value field-value + :set-field-value set-field-value + :options options}] + [customizer-multi-select-button {:open open :selected-options selected-options} theme]]])))) + +(defn customizer + [state set-state descriptors theme] + [rn/view + {:style {:flex-shrink 1 + :padding-horizontal 20}} + (doall + (for [desc descriptors + :let [desc-path (:path desc) + descriptor (if (fn? set-state) + (assoc desc :state state :set-state set-state) + (let [new-state (if desc-path + (reagent/cursor state desc-path) + state)] + (assoc desc :state new-state)))]] + ^{:key (:key desc)} + (case (:type desc) + :boolean [customizer-boolean descriptor] + :text [customizer-text descriptor theme] + :number [customizer-number descriptor theme] + :select [customizer-select descriptor] + :multi-select [customizer-multi-select descriptor] + nil)))]) + +(defn customization-color-option + ([] + (customization-color-option {})) + ([{:keys [feng-shui?] :as opts}] + (merge {:key :customization-color + :type :select + :options (->> (merge colors/customization (when feng-shui? {:feng-shui nil})) + keys + sort + (map (fn [k] + {:key k :value (string/capitalize (name k))})))} + opts))) + +(defn blur-view + [{:keys [show-blur-background? image height blur-view-props style theme]} & children] + [rn/view + {:style {:flex 1 + :padding-vertical 16}} + (when show-blur-background? + [rn/view + {:style {:height (or height 100) + :border-radius 16 + :overflow :hidden}} + [rn/image + {:source (or image (resources/get-mock-image :dark-blur-bg)) + :style {:height "100%" :width "100%"}}] + [quo/blur + (merge {:style {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0} + :blur-amount 10 + :overlay-color (colors/theme-colors colors/white-opa-70 colors/neutral-80-opa-80 theme)} + blur-view-props)]]) + (into [rn/view + {:style (merge {:position :absolute + :top 32 + :padding-horizontal 16} + style)}] + children)]) + +(defn- f-preview-container + [{:keys [title state set-state descriptor blur? blur-dark-only? + component-container-style + blur-container-style blur-view-props blur-height show-blur-background? full-screen?] + :or {blur-height 200}} + & children] + (let [theme (quo.context/use-theme) + title (or title (rf/sub [:view-id]))] + (rn/use-effect (fn [] + (when blur-dark-only? + (if blur? + (rf/dispatch [:theme/switch {:theme :dark}]) + (rf/dispatch [:theme/switch {:theme :light}])))) + [blur? blur-dark-only?]) + [rn/view + {:style {:top safe-area/top + :flex 1}} + [common/navigation-bar {:title title}] + [rn/scroll-view + {:style (style/panel-basic theme) + :shows-vertical-scroll-indicator false + :content-container-style (when full-screen? {:flex 1})} + [:<> + [rn/pressable + {:style (when full-screen? {:flex 1}) + :on-press rn/dismiss-keyboard!} + (when descriptor + [rn/view {:style style/customizer-container} + [customizer state set-state descriptor theme]]) + (if blur? + [rn/view {:style (merge style/component-container component-container-style)} + (into [blur-view + {:theme theme + :show-blur-background? show-blur-background? + :height blur-height + :style (merge {:width "100%" + :flex-grow 1} + (when-not show-blur-background? + {:padding-horizontal 0 + :top 0}) + blur-container-style) + :blur-view-props (merge {:blur-type theme} + blur-view-props)}] + children)] + (into [rn/view {:style (merge style/component-container component-container-style)}] + children))] + (when state + (let [actual-state (if (fn? set-state) state @state) + decr-state (if descriptor + (select-keys actual-state (mapv :key (flatten descriptor))) + actual-state) + state-str (with-out-str (cljs.pprint/pprint decr-state))] + [rn/view {:style {:margin 50}} + [quo/text {:style {:margin-bottom 10}} "State map (click on map to copy)"] + [rn/pressable + {:on-press #(clipboard/set-string state-str)} + [quo/text state-str]]]))]]])) + +(defn preview-container + [& args] + (into [:f> f-preview-container] args)) diff --git a/src/status_im/contexts/preview/quo/profile/collectible.cljs b/src/status_im/contexts/preview/quo/profile/collectible.cljs new file mode 100644 index 00000000000..e894b690940 --- /dev/null +++ b/src/status_im/contexts/preview/quo/profile/collectible.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.profile.collectible + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(defonce test-image (resources/get-mock-image :collectible)) +(def test-images (repeat 10 test-image)) + +(def descriptor + [{:key :num-images + :type :select + :options (map (fn [n] + {:key n :value (str n " images")}) + (range 1 (inc (count test-images))))}]) + +(defn view + [] + (let [state (reagent/atom {:num-images 1})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 20 + :align-items :center}} + [quo/collectible + {:images (repeat (:num-images @state) test-image) + :on-press #(js/alert "Pressed")}]]))) diff --git a/src/status_im/contexts/preview/quo/profile/collectible_list_item.cljs b/src/status_im/contexts/preview/quo/profile/collectible_list_item.cljs new file mode 100644 index 00000000000..d887945efb9 --- /dev/null +++ b/src/status_im/contexts/preview/quo/profile/collectible_list_item.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.preview.quo.profile.collectible-list-item + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(defonce test-image (resources/get-mock-image :collectible)) +(defonce test-avatar (resources/get-mock-image :monkey)) + +(def descriptor + [{:key :type + :type :select + :options [{:key :card} + {:key :image}]} + {:key :community? + :type :boolean} + {:key :gradient-color-index + :type :select + :options [{:key :gradient-1} + {:key :gradient-2} + {:key :gradient-3} + {:key :gradient-4} + {:key :gradient-5}]} + {:key :counter + :type :text} + {:key :collectible-name + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:type :card + :collectible-name "Doodle #6822" + :gradient-color-index :gradient-1 + :community? false + :counter ""})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 20 + :margin-horizontal 95}} + [quo/collectible-list-item + (assoc @state + :counter (when (seq (:counter @state)) (:counter @state)) + :gradient-color-index (:gradient-color-index @state) + :image-src test-image + :supported-file? true + :avatar-image-src test-avatar + :on-press #(js/alert "Pressed"))]]))) diff --git a/src/status_im/contexts/preview/quo/profile/expanded_collectible.cljs b/src/status_im/contexts/preview/quo/profile/expanded_collectible.cljs new file mode 100644 index 00000000000..076ca75c84c --- /dev/null +++ b/src/status_im/contexts/preview/quo/profile/expanded_collectible.cljs @@ -0,0 +1,40 @@ +(ns status-im.contexts.preview.quo.profile.expanded-collectible + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(defonce vertical-image + "https://images.unsplash.com/photo-1526512340740-9217d0159da9?q=80&w=1000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8dmVydGljYWx8ZW58MHx8MHx8fDA%3D") +(defonce horizontal-image + "https://media.istockphoto.com/id/603164912/photo/suburb-asphalt-road-and-sun-flowers.jpg?s=612x612&w=0&k=20&c=qLoQ5QONJduHrQ0kJF3fvoofmGAFcrq6cL84HbzdLQM=") + +(def descriptor + [{:key :square? + :type :boolean} + {:key :counter + :type :text} + {:type :select + :key :image-type + :options [{:key :vertical} + {:key :horizontal}]}]) + +(defn view + [] + (let [state (reagent/atom {:square? false + :counter "" + :image-type :horizontal})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 20 + :margin-horizontal 35}} + [quo/expanded-collectible + (assoc (dissoc @state :image-type) + :image-src (if (= :vertical (:image-type @state)) + vertical-image + horizontal-image) + :counter (when (seq (:counter @state)) (:counter @state)) + :supported-file? true + :on-press #(js/alert "Pressed"))]]))) diff --git a/src/status_im/contexts/preview/quo/profile/link_card.cljs b/src/status_im/contexts/preview/quo/profile/link_card.cljs new file mode 100644 index 00000000000..3e9a354a51f --- /dev/null +++ b/src/status_im/contexts/preview/quo/profile/link_card.cljs @@ -0,0 +1,112 @@ +(ns status-im.contexts.preview.quo.profile.link-card + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn- initial-state + [social] + (case social + :link {:title "Website" + :icon :social/link + :customization-color :social/link + :address "bento.me/fracesca"} + :facebook {:title "Facebook" + :icon :social/facebook + :customization-color :social/facebook + :address "@francescab"} + :github {:title "GitHub" + :icon :social/github + :customization-color :social/github + :address "@francescab"} + :instagram {:title "Instagram" + :icon :social/instagram + :customization-color :social/instagram + :address "@francescab"} + :lens {:title "Lens" + :icon :social/lens + :customization-color :social/lens + :address "@francescab"} + :linkedin {:title "LinkedIn" + :icon :social/linkedin + :customization-color :social/linkedin + :address "@francescab"} + :mirror {:title "Mirror" + :icon :social/mirror + :customization-color :social/mirror + :address "@francescab"} + :opensea {:title "Opensea" + :icon :social/opensea + :customization-color :social/opensea + :address "@francescab"} + :pinterest {:title "Pinterest" + :icon :social/pinterest + :customization-color :social/pinterest + :address "@francescab"} + :rarible {:title "Rarible" + :icon :social/rarible + :customization-color :social/rarible + :address "@francescab"} + :snapchat {:title "Snapchat" + :icon :social/snapchat + :customization-color :social/snapchat + :address "@francescab"} + :spotify {:title "Spotify" + :icon :social/spotify + :customization-color :social/spotify + :address "@francescab"} + :superrare {:title "SuperRare" + :icon :social/superrare + :customization-color :social/superrare + :address "@francescab"} + :tumblr {:title "Tumblr" + :icon :social/tumblr + :customization-color :social/tumblr + :address "@francescab"} + :twitch {:title "Twitch" + :icon :social/twitch + :customization-color :social/twitch + :address "@francescab"} + :twitter {:title "Twitter" + :icon :social/twitter + :customization-color :social/twitter + :address "@francescab"} + :youtube {:title "YouTube" + :icon :social/youtube + :customization-color :social/youtube + :address "@francescab"} + nil)) + +(def descriptor + [{:key :link + :type :select + :options [{:key :link} + {:key :facebook} + {:key :github} + {:key :instagram} + {:key :lens} + {:key :linkedin} + {:key :mirror} + {:key :opensea} + {:key :pinterest} + {:key :rarible} + {:key :snapchat} + {:key :spotify} + {:key :superrare} + {:key :tumblr} + {:key :twitch} + {:key :twitter} + {:key :youtube}]}]) + +(defn view + [] + (let [state (reagent/atom {:link :link})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-bottom 50 + :width 200}} + [quo/link-card + (assoc (initial-state (:link @state)) + :on-press + #(js/alert "pressed"))]]))) diff --git a/src/status_im/contexts/preview/quo/profile/profile_card.cljs b/src/status_im/contexts/preview/quo/profile/profile_card.cljs new file mode 100644 index 00000000000..593cb22041b --- /dev/null +++ b/src/status_im/contexts/preview/quo/profile/profile_card.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.preview.quo.profile.profile-card + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :keycard-account? :type :boolean} + {:key :show-emoji-hash? :type :boolean} + {:key :show-user-hash? :type :boolean} + {:key :show-options-button? :type :boolean} + {:key :show-logged-in? :type :boolean} + {:key :login-card? :type :boolean} + {:key :last-item? :type :boolean} + {:key :name :type :text} + {:key :hash :type :text} + {:key :emoji-hash :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:keycard-account? false + :name "Matt Grote" + :on-options-press nil + :on-card-press nil + :show-options-button? true + :show-logged-in? true + :show-user-hash? false + :login-card? false + :last-item? true + :on-press-sign nil + :customization-color :turquoise + :profile-picture (resources/get-mock-image :user-picture-male5) + :show-emoji-hash? false + :hash "zQ3k83euenmcikw7474hfu73t5N" + :emoji-hash "😄😂🫣🍑😇🤢😻🥷🏻🦸🏻‍♀️🦸🏻🦸🏻‍♂️🦹🏻‍♀️🧑🏻‍🎄🎅🏻"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-bottom 50}} + [quo/profile-card @state]]))) diff --git a/src/status_im/contexts/preview/quo/profile/select_profile.cljs b/src/status_im/contexts/preview/quo/profile/select_profile.cljs new file mode 100644 index 00000000000..79f10faeab0 --- /dev/null +++ b/src/status_im/contexts/preview/quo/profile/select_profile.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.preview.quo.profile.select-profile + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [(preview/customization-color-option) + {:key :name :type :text} + {:key :selected? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:selected? false + :name "Alisher Yakupov" + :customization-color :turquoise + :profile-picture (resources/get-mock-image :user-picture-male5)}) + selected? (reagent/cursor state [:selected?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding 20}} + [quo/select-profile (merge @state {:on-change #(reset! selected? %)})]]))) diff --git a/src/status_im/contexts/preview/quo/profile/showcase_nav.cljs b/src/status_im/contexts/preview/quo/profile/showcase_nav.cljs new file mode 100644 index 00000000000..5cd9019f888 --- /dev/null +++ b/src/status_im/contexts/preview/quo/profile/showcase_nav.cljs @@ -0,0 +1,40 @@ +(ns status-im.contexts.preview.quo.profile.showcase-nav + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def generate-nav-data + (->> (range 15) + (map inc) + (mapv (fn [index] + {:icon (rand-nth [:i/recent :i/profile :i/communities :i/wallet :i/nft :i/token + :i/delete]) + :id index})))) + +(def descriptor + [{:type :number :key :previews-length} + {:type :select + :key :state + :options [{:key :default} + {:key :scroll}]}]) + +(defn view + [] + (let [state (reagent/atom {:state :default + :previews-length 10 + :active-id 3}) + component-state (reagent/cursor state [:state]) + previews-length (reagent/cursor state [:previews-length]) + active-id (reagent/cursor state [:active-id]) + nav-data generate-nav-data] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-horizontal 0}} + [quo/showcase-nav + {:state @component-state + :active-id @active-id + :data (take (or @previews-length 1) nav-data) + :on-press #(swap! state assoc :active-id %)}]]))) diff --git a/src/status_im/contexts/preview/quo/record_audio/record_audio.cljs b/src/status_im/contexts/preview/quo/record_audio/record_audio.cljs new file mode 100644 index 00000000000..4a3b8ed7ce8 --- /dev/null +++ b/src/status_im/contexts/preview/quo/record_audio/record_audio.cljs @@ -0,0 +1,57 @@ +(ns status-im.contexts.preview.quo.record-audio.record-audio + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.permissions :as permissions] + [reagent.core :as reagent] + [status-im.common.alert.effects :as alert.effects] + [status-im.constants :as constants] + [status-im.contexts.preview.quo.preview :as preview] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defonce record-audio-permission-granted (reagent/atom false)) + +(defn view + [] + (let [message (reagent/atom + "Press & hold the mic button to start recording...") + on-send #(reset! message (str "onSend event triggered. File path: " + %)) + on-start-recording #(reset! message "onStartRecording event triggered.") + on-reviewing-audio #(reset! message "onReviewingAudio event triggered.") + on-cancel #(reset! message "onCancel event triggered.") + on-check-audio-permissions (fn [] + (permissions/permission-granted? + :record-audio + #(reset! record-audio-permission-granted %) + #(reset! record-audio-permission-granted false))) + on-request-record-audio-permission (fn [] + (rf/dispatch + [:request-permissions + {:permissions [:record-audio] + :on-allowed + #(reset! record-audio-permission-granted true) + :on-denied + #(js/setTimeout + (fn [] + (alert.effects/show-popup + (i18n/label :t/audio-recorder-error) + (i18n/label + :t/audio-recorder-permissions-error))) + 50)}]))] + (fn [] + [preview/preview-container {:component-container-style {:justify-content :center}} + [rn/view + {:align-items :center + :justify-content :center} + [quo/record-audio + {:record-audio-permission-granted @record-audio-permission-granted + :on-send on-send + :on-start-recording on-start-recording + :on-reviewing-audio on-reviewing-audio + :on-cancel on-cancel + :on-check-audio-permissions on-check-audio-permissions + :on-request-record-audio-permission on-request-record-audio-permission + :max-duration-ms constants/audio-max-duration-ms}]] + [quo/text @message]]))) diff --git a/src/status_im/contexts/preview/quo/selectors/disclaimer.cljs b/src/status_im/contexts/preview/quo/selectors/disclaimer.cljs new file mode 100644 index 00000000000..e6bd5fdd0cd --- /dev/null +++ b/src/status_im/contexts/preview/quo/selectors/disclaimer.cljs @@ -0,0 +1,48 @@ +(ns status-im.contexts.preview.quo.selectors.disclaimer + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :checked? :type :boolean} + {:key :text :type :text} + {:key :icon :type :boolean} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:checked? false + :blur? false + :text "I agree with the community rules" + :icon false + :customization-color :blue})] + (fn [] + (let [{:keys [blur? checked? text icon customization-color]} @state + theme (quo.context/use-theme) + blur? (if (= :light theme) false blur?)] + [preview/preview-container + {:state state + :descriptor (if (= :light theme) + descriptor + (vec (conj descriptor {:key :blur? :type :boolean}))) + :blur? blur? + :show-blur-background? true + :component-container-style {:padding 20}} + [rn/view {:style {:margin-bottom 20}} + [quo/disclaimer + {:blur? blur? + :checked? checked? + :icon (when icon + (if checked? + :i/locked + :i/unlocked)) + :on-change #(swap! state update :checked? not) + :customization-color customization-color} + text]] + [quo/button + {:disabled? (not checked?) + :customization-color customization-color} + "submit"]])))) diff --git a/src/status_im/contexts/preview/quo/selectors/filter.cljs b/src/status_im/contexts/preview/quo/selectors/filter.cljs new file mode 100644 index 00000000000..f49dadae3ed --- /dev/null +++ b/src/status_im/contexts/preview/quo/selectors/filter.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.preview.quo.selectors.filter + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :component-container-style {:align-items :center}} + [quo/filter @state]]))) diff --git a/src/status_im/contexts/preview/quo/selectors/react.cljs b/src/status_im/contexts/preview/quo/selectors/react.cljs new file mode 100644 index 00000000000..fa6e38b1bed --- /dev/null +++ b/src/status_im/contexts/preview/quo/selectors/react.cljs @@ -0,0 +1,79 @@ +(ns status-im.contexts.preview.quo.selectors.react + (:require + [clojure.string :as string] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn gen-quantity + [max-count _] + (rand-int max-count)) + +(def memo-gen-quantity (memoize gen-quantity)) + +(def descriptor + [{:key :hide-new-reaction-button? + :type :boolean} + {:label "Reactions" + :key :reaction-ids + :type :multi-select + :options (for [[reaction-id reaction] constants/reactions] + {:key reaction-id + :value (string/capitalize + (last + (string/split (name reaction) #"/")))})} + {:label "Max rand. reactions (helper)" + :key :max-count + :type :number} + {:key :use-case + :type :select + :options [{:key :default + :value "Default"} + {:key :pinned + :value "Pinned"}]}]) + +(defn preview-react + [] + (let [[state set-state] + (rn/use-state {:hide-new-reaction-button? true + :max-count 1000 + :reaction-ids [1 2 3] + :use-case :default}) + + [pressed-reactions set-pressed-reactions] (rn/use-state #{1}) + + reactions (mapv (fn [reaction-id] + {:emoji-reaction-id reaction-id + :emoji-id reaction-id + :emoji (get constants/reactions reaction-id) + :quantity (memo-gen-quantity (:max-count state) reaction-id) + :own (contains? pressed-reactions reaction-id)}) + (:reaction-ids state)) + + on-press (fn [reaction] + (let [reaction-id (:emoji-id reaction)] + (if (contains? pressed-reactions reaction-id) + (set-pressed-reactions (disj pressed-reactions reaction-id)) + (set-pressed-reactions (conj pressed-reactions + reaction-id)))))] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [rn/view + {:padding-bottom 150 + :padding-vertical 60 + :padding-horizontal 20 + :border-radius 16 + :background-color (when (= :pinned (:use-case state)) + (colors/resolve-color :blue :light 10)) + :align-items :flex-start} + [quo/react + {:reactions reactions + :hide-new-reaction-button? (:hide-new-reaction-button? state) + :use-case (:use-case state) + :on-press on-press + :on-long-press identity + :on-press-new identity}]]])) diff --git a/src/status_im/contexts/preview/quo/selectors/react_selector.cljs b/src/status_im/contexts/preview/quo/selectors/react_selector.cljs new file mode 100644 index 00000000000..b26c56bfbb3 --- /dev/null +++ b/src/status_im/contexts/preview/quo/selectors/react_selector.cljs @@ -0,0 +1,56 @@ +(ns status-im.contexts.preview.quo.selectors.react-selector + (:require + [clojure.string :as string] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :clicks + :type :text} + {:key :emoji + :type :select + :options (for [reaction (vals constants/reactions)] + {:key reaction + :value (string/capitalize (name reaction))})} + {:key :state + :type :select + :options [{:key :not-pressed + :value "Not pressed by me"} + {:key :pressed + :value "Pressed by me"} + {:key :add-reaction + :value "Add reaction"}]} + {:key :use-case + :type :select + :options [{:key :default + :value "Default"} + {:key :pinned + :value "Pinned"}]}]) + +(defn preview-react-selector + [] + (let [state (reagent/atom {:emoji :reaction/love + :state :not-pressed + :use-case :default})] + (fn [] + (println @state) + [preview/preview-container + {:state state + :descriptor descriptor} + [rn/view + {:padding-bottom 150 + :align-items :center} + [rn/view + {:width 100 + :padding-vertical 60 + :border-radius 16 + :background-color (when (= :pinned (:use-case @state)) + (colors/resolve-color :blue :light 10)) + :justify-content :space-evenly + :flex-direction :row + :align-items :center} + [quo/react-selector @state]]]]))) diff --git a/src/status_im/contexts/preview/quo/selectors/reactions_selector.cljs b/src/status_im/contexts/preview/quo/selectors/reactions_selector.cljs new file mode 100644 index 00000000000..b96063fd7e0 --- /dev/null +++ b/src/status_im/contexts/preview/quo/selectors/reactions_selector.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.preview.quo.selectors.reactions-selector + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as r] + [status-im.constants :as constants] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :emoji + :type :select + :options (for [reaction (vals constants/reactions)] + {:key reaction + :value (string/capitalize (name reaction))})}]) + +(defn preview + [] + (let [state (r/atom {:emoji :reaction/love})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [rn/view + {:style {:margin-top 40 + :align-items :center}} + [quo/reactions-selector {:emoji (:emoji @state)}]]]))) + diff --git a/src/status_im/contexts/preview/quo/selectors/selectors.cljs b/src/status_im/contexts/preview/quo/selectors/selectors.cljs new file mode 100644 index 00000000000..31adf7f8ea5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/selectors/selectors.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.preview.quo.selectors.selectors + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :disabled? :type :boolean} + {:key :blur? :type :boolean} + (preview/customization-color-option)]) + +(defn selector-preview + [text type {:keys [disabled? blur? customization-color]}] + [rn/view + {:style {:margin 6 + :align-items :center}} + [quo/text {:size :paragraph-1} text] + [quo/selectors + {:type type + :container-style {:margin 4} + :disabled? disabled? + :blur? blur? + :customization-color customization-color}]]) + +(defn view + [] + (let [state (reagent/atom {:disabled? false + :blur? false + :customization-color :blue})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-height 300} + [selector-preview "Toggle" :toggle @state] + [selector-preview "Radio" :radio @state] + [selector-preview "Checkbox" :checkbox @state] + [selector-preview "Filled Checkbox" :filled-checkbox @state]]))) diff --git a/src/status_im/contexts/preview/quo/settings/accounts.cljs b/src/status_im/contexts/preview/quo/settings/accounts.cljs new file mode 100644 index 00000000000..3a11e61c6c6 --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/accounts.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.preview.quo.settings.accounts + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [(preview/customization-color-option) + {:key :account-name :type :text} + {:key :account-address :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:customization-color :blue + :account-name "Booze for Dubai" + :account-address "0x21a ... 49e" + :avatar-icon :i/placeholder + :on-press-menu (fn [] + (js/alert "Menu button pressed"))})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 100 + :align-items :center}} + [quo/account @state]]))) diff --git a/src/status_im/contexts/preview/quo/settings/category.cljs b/src/status_im/contexts/preview/quo/settings/category.cljs new file mode 100644 index 00000000000..64ce484c56a --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/category.cljs @@ -0,0 +1,73 @@ +(ns status-im.contexts.preview.quo.settings.category + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn create-item-array + [n] + (vec + (for [i (range n)] + {:title (str "Item " i) + :subtitle "subtitle" + :action :arrow + :right-icon :i/globe + :image (resources/get-mock-image :diamond) + :image-props :i/browser + :image-size 32}))) + +(defn create-data-item-array + [n] + (vec + (for [i (range n)] + {:blur? false + :description :default + :icon-right? true + :right-icon :i/chevron-right + :icon-color colors/neutral-10 + :card? false + :label :preview + :status :default + :size :default + :right-content {:type :accounts + :data [{:emoji "🔥" :customization-color :yellow}]} + :title (str "Item title " i) + :subtitle "Item subtitle"}))) + +(def descriptor + [{:key :blur? :type :boolean} + {:key :list-type + :type :select + :options [{:key :settings + :value :settings} + {:key :reorder + :value :reorder} + {:key :data-item + :value :data-item}]}]) + +(def ^:constant n-items 5) + +(defn view + [] + (let [state (reagent/atom {:label "Label" + :blur? false + :list-type :settings})] + (fn [] + (let [list-type (:list-type @state) + data (if (= list-type :data-item) + (create-data-item-array n-items) + (create-item-array n-items))] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true + :blur-height 400} + [quo/category + {:list-type (:list-type @state) + :label (:label @state) + :data data + :blur? (:blur? @state)}]])))) diff --git a/src/status_im/contexts/preview/quo/settings/data_item.cljs b/src/status_im/contexts/preview/quo/settings/data_item.cljs new file mode 100644 index 00000000000..bae7208a96e --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/data_item.cljs @@ -0,0 +1,91 @@ +(ns status-im.contexts.preview.quo.settings.data-item + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def communities-list + [{:source (resources/get-mock-image :coinbase)} + {:source (resources/get-mock-image :decentraland)} + {:source (resources/get-mock-image :rarible)}]) + +(def networks-list + [{:source (quo.resources/get-network :ethereum)} + {:source (quo.resources/get-network :optimism)} + {:source (quo.resources/get-network :arbitrum)}]) + +(def descriptor + [{:key :blur? :type :boolean} + {:key :card? :type :boolean} + {:type :select + :key :right-icon + :options [{:key :i/chevron-right} + {:key :i/copy} + {:key nil + :value "None"}]} + {:type :select + :key :title-icon + :options [{:key :i/chevron-right} + {:key :i/copy} + {:key nil + :value "None"}]} + {:type :select + :key :right-content + :options [{:key nil + :value "None"} + {:key {:type :communities + :data communities-list} + :value "Communities"} + {:key {:type :network + :data networks-list} + :value "Networks"} + {:key {:type :accounts + :data [{:emoji "🔥" :customization-color :yellow}]} + :value "Account (size-24)"} + {:key {:type :accounts + :data [{:emoji "🔥" :customization-color :yellow}] + :size :size-32} + :value "Account (size-32)"}]} + {:type :select + :key :subtitle-type + :options [{:key :default} + {:key :icon} + {:key :network} + {:key :account} + {:key :editable}]} + {:type :select + :key :status + :options [{:key :default} + {:key :loading}]} + {:type :select + :key :size + :options [{:key :default} + {:key :small} + {:key :large}]}]) + +(defn view + [] + (let [state (reagent/atom {:on-press #(js/alert "pressed") + :blur? false + :subtitle-type :account + :card? true + :status :default + :size :default + :title "Label" + :subtitle "Subtitle" + :icon :i/placeholder + :right-icon :i/chevron-right + :right-content nil + :emoji "🎮" + :customization-color :yellow + :network-image (quo.resources/get-network :ethereum)})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/data-item @state]]))) diff --git a/src/status_im/contexts/preview/quo/settings/page_setting.cljs b/src/status_im/contexts/preview/quo/settings/page_setting.cljs new file mode 100644 index 00000000000..76fd19ad23e --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/page_setting.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.preview.quo.settings.page-setting + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [(preview/customization-color-option {:feng-shui? true}) + {:key :setting-text :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:setting-text "Sample text" + :customization-color :blue})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/page-setting @state]]))) diff --git a/src/status_im/contexts/preview/quo/settings/privacy_option.cljs b/src/status_im/contexts/preview/quo/settings/privacy_option.cljs new file mode 100644 index 00000000000..ec14be0121e --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/privacy_option.cljs @@ -0,0 +1,86 @@ +(ns status-im.contexts.preview.quo.settings.privacy-option + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :header :type :text} + {:key :footer :type :text} + {:label "Line 1:" + :key :li1 + :type :text} + {:label "Line 2:" + :key :li2 + :type :text} + {:label "Line 3:" + :key :li3 + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:selected :contacts + :header "header" + :footer "footer" + :li1 "line item 1" + :li2 "line item 2" + :li3 "line item 3"})] + (fn [] + (let [header (:header @state) + footer (:footer @state) + list-items (->> (select-keys @state [:li1 :li2 :li3]) + vals + (remove empty?))] + [preview/preview-container + {:state state + :descriptor descriptor} + [rn/view + {:margin-horizontal 20 + :padding 16 + :flex 1} + [rn/view {:margin-vertical 2} + [quo/text {:size :paragraph-2} "Dynamic sample"]] + [rn/view {:margin-vertical 8} + [quo/privacy-option + (cond-> {:on-select #(swap! state assoc :selected :preview) + :on-toggle #(js/alert "dynamic card toggled") + :active? (= :preview (:selected @state))} + (not-empty header) (assoc :header header) + (not-empty footer) (assoc :footer footer) + (not-empty list-items) (assoc :list-items list-items))]] + + [rn/view {:margin-vertical 2} + [quo/text {:size :paragraph-2} "Static samples"]] + [rn/view {:margin-vertical 8} + [quo/privacy-option + {:header "Contacts only" + :icon :i/contact-book + :on-select #(swap! state assoc :selected :contacts) + :active? (= :contacts (:selected @state)) + :list-items ["Only add people from your contact list" + "Added members can add their own contacts" + "There is no link or QR for this group" + "No public information available"]}]] + [rn/view {:margin-vertical 8} + [quo/privacy-option + {:icon :i/world + :header "Anyone can request to join" + :on-select #(swap! state assoc :selected :anyone) + :on-toggle #(js/alert "card toggled") + :active? (= :anyone (:selected @state)) + :footer "Members can approve join requests" + :list-items ["Add people from your contact list" + "Invite prople from outside your contact list" + "Group is shareable via link" + "Group name and number of people is public"]}]] + [rn/view {:margin-vertical 8} + [quo/privacy-option + {:icon :i/world + :header "Sample card with very long text to test proper overflow behavior" + :on-select #(swap! state assoc :selected :overflow) + :on-toggle #(js/alert "card toggled") + :active? (= :overflow (:selected @state)) + :footer "This footer is exceedingly long to test the overflowing behavior of text in it" + :list-items + ["A very, very very long text line that serves to test the overflow behavior of this component"]}]]]])))) diff --git a/src/status_im/contexts/preview/quo/settings/reorder_item.cljs b/src/status_im/contexts/preview/quo/settings/reorder_item.cljs new file mode 100644 index 00000000000..541e3cfd032 --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/reorder_item.cljs @@ -0,0 +1,82 @@ +(ns status-im.contexts.preview.quo.settings.reorder-item + (:require + [quo.components.settings.reorder-item.types :as types] + [quo.core :as quo] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def mock-data + [{:id 1 + :type types/item + :data {:image (resources/get-mock-image :diamond) + :image-size 21 + :right-icon :i/world + :title "Trip to Bahamas"}} + {:id 2 + :type types/item + :data {:image (resources/get-mock-image :status-logo) + :image-size 21 + :right-icon :i/world + :title "Status"}} + {:id 3 + :type types/item + :data {:image (resources/get-mock-image :status-logo) + :image-size 21 + :right-icon :i/world + :title "Status"}} + {:id 4 + :type types/item + :data {:image (resources/get-mock-image :monkey) + :image-size 30 + :right-icon :i/world + :title "3045" + :subtitle "Bored Ape Yatch Club"}} + {:id 5 + :type types/item + :data {:image (resources/get-mock-image :pinterest) + :image-size 21 + :right-text "@sheralito" + :title "Pinterest"}} + {:id 6 + :type types/placeholder + :data {:label "Label"}} + {:id 7 + :type types/placeholder + :data {:label "Label"}} + {:id 8 + :type types/skeleton} + {:id 9 + :type types/skeleton} + {:id 10 + :type types/tab + :data {:data [{:id 1 + :label "Everyone" + :icon :i/world} + {:id 2 + :label "Contacts" + :image (resources/get-mock-image :contact)} + {:id 3 + :label "Verified" + :image (resources/get-mock-image :verified)}] + :on-change (fn [tab-id] (println tab-id)) + :default-active 2}} + {:id 11 + :type types/tab + :data {:data [{:id 1 + :label "Everyone" + :icon :i/world} + {:id 2 + :label "Contacts" + :image (resources/get-mock-image :contact)} + {:id 3 + :label "Verified" + :image (resources/get-mock-image :verified)}] + :on-change (fn [tab-id] (println tab-id)) + :default-active 1}}]) + +(defn view + [] + (fn [] + [preview/preview-container {} + (for [item mock-data] + [quo/reorder-item (item :data) (item :type)])])) diff --git a/src/status_im/contexts/preview/quo/settings/section_label.cljs b/src/status_im/contexts/preview/quo/settings/section_label.cljs new file mode 100644 index 00000000000..2195e1841d2 --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/section_label.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.preview.quo.settings.section-label + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def ^:private descriptor + [{:key :section + :type :text} + {:key :description + :type :text} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:section "Section label" + :description "" + :blur? false}) + description (reagent/cursor state [:description]) + section (reagent/cursor state [:section]) + blur? (reagent/cursor state [:blur?])] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :show-blur-background? true + :blur-dark-only? true + :blur? @blur? + :blur-height 150} + [quo/section-label + {:section @section + :description (when-not (empty? @description) + @description) + :blur? @blur?}]]))) diff --git a/src/status_im/contexts/preview/quo/settings/settings_item.cljs b/src/status_im/contexts/preview/quo/settings/settings_item.cljs new file mode 100644 index 00000000000..9e703e1a3aa --- /dev/null +++ b/src/status_im/contexts/preview/quo/settings/settings_item.cljs @@ -0,0 +1,120 @@ +(ns status-im.contexts.preview.quo.settings.settings-item + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title + :type :text} + {:key :action + :type :select + :options [{:key nil + :value :none} + {:key :arrow + :value :arrow} + {:key :button + :value :button} + {:key :selector + :value :selector}]} + {:key :label + :type :select + :options [{:key nil + :value :none} + {:key :color + :value :color} + {:key :preview + :value :preview} + {:key :text + :value :text}]} + {:key :image + :type :select + :options [{:key nil + :value :none} + {:key :icon + :value :icon} + {:key :avatar + :value :avatar} + {:key :icon-avatar + :value :icon-avatar} + {:key :emoji + :value :emoji}]} + {:key :description + :type :select + :options [{:key nil + :value :none} + {:key :text + :value :text} + {:key :text-plus-icon + :value :text-plus-icon} + {:key :status + :value :status}]} + {:key :tag + :type :select + :options [{:key nil + :value :none} + {:key :positive + :value :positive} + {:key :context + :value :context}]} + {:key :blur? + :type :boolean}]) + +(def communities-list + [{:source (resources/get-mock-image :coinbase)} + {:source (resources/get-mock-image :decentraland)} + {:source (resources/get-mock-image :rarible)}]) + +(defn get-props + [data] + (when (:toggle-props data) (js/console.warn data)) + (merge + data + {:image-props (case (:image data) + :icon :i/browser + :avatar {:full-name "A Y" + :size :xxs + :customization-color :blue} + :icon-avatar {:size :medium + :icon :i/placeholder + :color :blue} + :emoji "🍿" + nil) + :description-props (case (:description data) + :text {:text "This is a description"} + :text-plus-icon {:text "This is a description" + :icon :i/placeholder} + :status {:online? true} + nil) + :action-props (case (:action data) + :button {:on-press #(js/alert "Button pressed!") + :button-text "Button"} + nil) + :label-props (case (:label data) + :text "Label" + :color :blue + :preview {:type :communities + :data communities-list} + nil) + :tag-props (case (:tag data) + :context {:icon :i/placeholder + :context "Context"} + nil)})) + +(defn view + [] + (let [state (reagent/atom {:title "Account" + :accessibility-label :settings-item + :action :arrow + :image :icon + :blur? false + :on-press (fn [] (js/alert "Settings list item pressed"))})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true} + [quo/settings-item (get-props @state)]]))) diff --git a/src/status_im/contexts/preview/quo/share/qr_code.cljs b/src/status_im/contexts/preview/quo/share/qr_code.cljs new file mode 100644 index 00000000000..0bb3820d4c7 --- /dev/null +++ b/src/status_im/contexts/preview/quo/share/qr_code.cljs @@ -0,0 +1,106 @@ +(ns status-im.contexts.preview.quo.share.qr-code + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [utils.image-server :as image-server] + [utils.re-frame :as rf])) + +(def descriptor + [{:key :url + :type :text} + {:key :avatar + :type :select + :options [{:key :none} + {:key :profile} + {:key :wallet-account} + {:key :community} + {:key :channel} + {:key :saved-address}]} + {:key :size + :type :select + :options [{:key 250} {:key 311} {:key 350}]}]) + +(def profile-descriptor + [{:key :profile-picture + :type :select + :options [{:key (resources/get-mock-image :user-picture-male5) + :value "User"} + {:key nil + :value "None"}]} + (preview/customization-color-option) + {:key :full-name + :type :text}]) + +(def wallet-account-descriptor + [{:key :emoji + :type :select + :options [{:key "🍒"} + {:key "🐧"} + {:key "🍨"}]} + (preview/customization-color-option)]) + +(def channel-descriptor + [{:key :emoji + :type :select + :options [{:key "🍒"} + {:key "🐧"} + {:key "🍨"}]} + (preview/customization-color-option)]) + +(def saved-address-descriptor + [{:key :f-name + :type :text} + {:key :l-name + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [media-server-port (rf/sub [:mediaserver/port]) + state (reagent/atom + {:url "https://status.app/u#zQ34e1zlOdas0pKnvrweeedsasas12adjie8" + :size 250 + :avatar :none + :profile-picture (resources/get-mock-image :user-picture-male5) + :customization-color :army + :full-name "Full Name" + :emoji "🍒" + :picture (resources/get-mock-image :community-logo) + :f-name "First Name" + :l-name "Last Name"})] + (fn [] + (let [qr-media-server-uri (image-server/get-qr-image-uri-for-any-url + {:url (:url @state) + :qr-size (:size @state) + :port media-server-port + :error-level :highest})] + [preview/preview-container + {:component-container-style {:flex 1 + :justify-content :center + :align-items :center + :margin-vertical 12} + :state state + :descriptor (concat descriptor + (case (:avatar @state) + :profile profile-descriptor + :wallet-account wallet-account-descriptor + :channel channel-descriptor + :saved-address saved-address-descriptor + nil))} + [quo/qr-code + (cond-> @state + :always + (assoc :qr-image-uri qr-media-server-uri) + + ;; `:channel` variant receives colors as hex strings instead of keywords + (= (:avatar @state) :channel) + (assoc :customization-color + (colors/resolve-color (:customization-color @state) nil)))] + + [rn/view {:style {:margin 12}} + [quo/text "URL:"] + [quo/text (:url @state)]]])))) diff --git a/src/status_im/contexts/preview/quo/share/share_qr_code.cljs b/src/status_im/contexts/preview/quo/share/share_qr_code.cljs new file mode 100644 index 00000000000..69a80011ee8 --- /dev/null +++ b/src/status_im/contexts/preview/quo/share/share_qr_code.cljs @@ -0,0 +1,151 @@ +(ns status-im.contexts.preview.quo.share.share-qr-code + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview] + [utils.image-server :as image-server] + [utils.re-frame :as rf])) + +(def descriptor + [{:key :qr-data + :label "QR data:" + :type :text} + {:key :type + :type :select + :options [{:key :profile} + {:key :wallet} + {:key :saved-address} + {:key :watched-address} + {:key :channel}]}]) + +(def possible-networks [:ethereum :optimism :arbitrum :myNet]) + +(def networks-selector + {:key :networks + :type :select + :options [{:key (take 1 possible-networks) + :value "Ethereum"} + {:key (take 2 possible-networks) + :value "Ethereum and Optimism"} + {:key (take 3 possible-networks) + :value "Ethereum, Optimism and Arbitrum"} + {:key (take 4 possible-networks) + :value "Ethereum, Optimism, Arbitrum and unknown"}]}) + +(def profile-descriptor + [{:key :profile-picture + :type :select + :options [{:key (resources/get-mock-image :user-picture-female2) + :value "User 1"} + {:key (resources/get-mock-image :user-picture-male4) + :value "User 2"}]} + {:key :full-name + :type :text} + (preview/customization-color-option)]) + +(def wallet-descriptor + [{:key :address + :type :select + :options [{:key :legacy} + {:key :multichain}]} + {:key :emoji + :type :select + :options [{:key "🐈"} + {:key "👻"} + {:key "🐧"}]} + networks-selector + (preview/customization-color-option)]) + +(def saved-address-descriptor + [{:key :address + :type :select + :options [{:key :legacy} + {:key :multichain}]} + networks-selector + (preview/customization-color-option)]) + +(def watched-address-descriptor + [{:key :address + :type :select + :options [{:key :legacy} + {:key :multichain}]} + {:key :emoji + :type :select + :options [{:key "🐈"} + {:key "👻"} + {:key "🐧"}]} + networks-selector + (preview/customization-color-option)]) + +(defn- get-network-short-name-url + [network] + (case network + :ethereum "eth:" + :optimism "oeth:" + :arbitrum "arb1:" + (str (name network) ":"))) + +(def ^:private profile-link + "https://status.app/u/CwWACgkKB0VlZWVlZWUD#zQ3shUeRSwU6rnUk5JfK2k5HRiM5Hy3wU3UZQrKVzopmAHcQv") + +(def ^:private wallet-address "0x39cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2") + +(defn- set-qr-data-based-on-type + [_ state-atom {old-type :type :as _old-state} {new-type :type :as _new-state}] + (when (not= old-type new-type) + (swap! state-atom assoc + :qr-data + (if (= new-type :profile) + profile-link + wallet-address)))) + +(defn view + [] + (let [state (reagent/atom {:type :profile + :address :legacy + :qr-data profile-link + :on-share-press #(js/alert "share pressed") + :on-text-press #(js/alert "text pressed") + :on-text-long-press #(js/alert "text long press") + :profile-picture (resources/get-mock-image :user-picture-female2) + :full-name "My User" + :customization-color :purple + :emoji "🐈" + :on-legacy-press #(js/alert (str "Tab " % " pressed")) + :on-multichain-press #(js/alert (str "Tab " % " pressed")) + :networks (take 2 possible-networks) + :on-settings-press #(js/alert "Settings pressed")}) + _ (add-watch state :change set-qr-data-based-on-type)] + (fn [] + (let [qr-url (if (and (= (:address @state) :multichain) + (not= (:type @state) :profile)) + (as-> (:networks @state) $ + (map get-network-short-name-url $) + (apply str $) + (str $ (:qr-data @state))) + (:qr-data @state)) + qr-media-server-uri (image-server/get-qr-image-uri-for-any-url + {:url qr-url + :port (rf/sub [:mediaserver/port]) + :qr-size 500 + :error-level :highest}) + typed-descriptor (concat descriptor + (case (:type @state) + :profile profile-descriptor + :wallet wallet-descriptor + :saved-address saved-address-descriptor + :watched-address watched-address-descriptor + nil))] + [preview/preview-container + {:state state + :descriptor typed-descriptor + :blur? true + :blur-height 500 + :component-container-style {:padding-horizontal 0} + :show-blur-background? true + :blur-dark-only? true} + [quo/share-qr-code + (assoc @state + :qr-image-uri qr-media-server-uri + :qr-data qr-url)]])))) diff --git a/src/status_im/contexts/preview/quo/slideshow/slider_bar.cljs b/src/status_im/contexts/preview/quo/slideshow/slider_bar.cljs new file mode 100644 index 00000000000..c2d332305b9 --- /dev/null +++ b/src/status_im/contexts/preview/quo/slideshow/slider_bar.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.preview.quo.slideshow.slider-bar + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :total-amount + :type :number} + {:label "Blur (dark only)?" + :key :blur? + :type :boolean} + (preview/customization-color-option)]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:total-amount 10 + :active-index 0 + :blur? false + :customization-color :blue})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor + :blur? (:blur? state) + :show-blur-background? true + :blur-dark-only? true} + [rn/view + {:style {:flex-direction :row + :justify-content :center + :gap 10 + :padding 10}} + [quo/button + {:type :outline + :style {:disabled true} + :disabled? (zero? (:active-index state)) + :icon-only? true + :on-press (fn [] + (set-state (update state :active-index dec)))} + :i/chevron-left] + [quo/button + {:type :outline + :disabled? (= (:active-index state) + (dec (:total-amount state))) + :icon-only? true + :on-press (fn [] + (set-state (update state :active-index inc)))} + :i/chevron-right]] + [quo/slider-bar state]])) diff --git a/src/status_im/contexts/preview/quo/style.cljs b/src/status_im/contexts/preview/quo/style.cljs new file mode 100644 index 00000000000..72962288632 --- /dev/null +++ b/src/status_im/contexts/preview/quo/style.cljs @@ -0,0 +1,184 @@ +(ns status-im.contexts.preview.quo.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.typography :as typography])) + +;;;; Form fields + +(def field-border-radius 12) +(def field-flex-percentage 0.6) +(def text-default typography/paragraph-1) + +(defn field-active-bg-color + [theme] + (colors/theme-colors colors/primary-50 colors/primary-60 theme)) + +(defn field-default-color + [theme] + (colors/theme-colors colors/neutral-100 colors/white theme)) + +(defn field-default-bg-color + [theme] + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)) + +(defn field-default-border-color + [theme] + (colors/theme-colors colors/neutral-30 colors/neutral-70 theme)) + +(def field-row + {:flex-direction :row + :padding-vertical 6 + :align-items :center}) + +(def field-column + {:flex field-flex-percentage}) + +(defn field-container + [active? theme] + (merge text-default + {:color (if active? + (colors/theme-colors colors/white colors/white-opa-95 theme) + (colors/theme-colors colors/neutral-100 colors/white theme)) + :border-width 1 + :border-color (field-default-border-color theme) + :border-radius field-border-radius + :padding-vertical 9 + :padding-horizontal 12})) + +(defn field-text + [active? theme] + (merge text-default + {:color (if active? + (colors/theme-colors colors/white colors/white-opa-95 theme) + (field-default-color theme))})) + +(def customizer-container + {:flex-shrink 1 + :padding-top 12}) + +(defn multi-select-option + [theme] + (merge (field-container false theme) + {:justify-content :space-between + :align-items :space-between + :flex 1 + :flex-direction :row + :margin-vertical 4 + :background-color (field-default-bg-color theme)})) + +(defn select-container + [theme] + (merge (field-container false theme) + {:flex-direction :row + :align-items :center + :border-radius field-border-radius + :background-color (field-default-bg-color theme) + :border-width 1 + :border-color (field-default-border-color theme)})) + +(defn field-select + [theme] + (merge text-default + {:flex-grow 1 + :color (field-default-color theme)})) + +(defn select-option + [selected? theme] + (merge (field-container selected? theme) + {:justify-content :center + :flex 1 + :margin-vertical 4} + (if selected? + {:border-color (field-active-bg-color theme) + :background-color (field-active-bg-color theme)} + {:background-color (field-default-bg-color theme)}))) + +(defn select-button + [theme] + (merge (select-option false theme) {:align-items :center})) + +(def label-container + {:flex (- 1 field-flex-percentage) + :padding-right 8}) + +(defn label + [theme] + (merge text-default + typography/font-medium + {:color (field-default-color theme)})) + +(defn boolean-container + [] + {:flex-direction :row + :flex field-flex-percentage + :border-radius field-border-radius}) + +(defn boolean-button + [{:keys [active? left?]} theme] + (cond-> {:flex 1 + :align-items :center + :justify-content :center + :padding-vertical 9 + :padding-horizontal 12 + :border-color (if active? + (field-active-bg-color theme) + (field-default-border-color theme)) + :border-top-width 1 + :border-bottom-width 1 + :background-color (if active? + (field-active-bg-color theme) + (field-default-bg-color theme))} + left? + (assoc :border-top-left-radius field-border-radius + :border-bottom-left-radius field-border-radius + :border-left-width 1) + + (not left?) + (assoc :border-top-right-radius field-border-radius + :border-bottom-right-radius field-border-radius + :border-right-width 1))) + +;;;; Modal + +(defn modal-overlay + [theme] + {:flex 1 + :justify-content :center + :padding-horizontal 24 + :background-color (colors/theme-colors colors/neutral-80-opa-60 colors/neutral-80-opa-80 theme)}) + +(defn modal-container + [theme] + {:padding-horizontal 16 + :padding-vertical 8 + :border-radius 12 + :margin-vertical 100 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(defn footer + [theme] + {:flex-direction :row + :padding-top 10 + :margin-top 10 + :border-top-width 1 + :border-top-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +;;;; Misc + +(defn panel-basic + [theme] + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :flex 1}) + +(def component-container + {:flex-grow 1 + :min-height 200 + :padding-vertical 20 + :padding-horizontal 20}) + +(defn main + [theme] + {:flex 1 + :padding-bottom 8 + :padding-horizontal 16 + :background-color (colors/theme-colors colors/white colors/neutral-90 theme)}) diff --git a/src/status_im/contexts/preview/quo/tabs/account_selector.cljs b/src/status_im/contexts/preview/quo/tabs/account_selector.cljs new file mode 100644 index 00000000000..52826b948e4 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tabs/account_selector.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.preview.quo.tabs.account-selector + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :show-label? :type :boolean} + {:key :transparent? :type :boolean} + {:key :account-text :type :text} + {:key :label-text :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:show-label? true + :transparent? false + :style {:width :100%} + :account-text "My Savings" + :account-emoji "🍑" + :label-text "Label"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60}} + [quo/account-selector @state]]))) diff --git a/src/status_im/contexts/preview/quo/tabs/segmented_tab.cljs b/src/status_im/contexts/preview/quo/tabs/segmented_tab.cljs new file mode 100644 index 00000000000..441a76b0a76 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tabs/segmented_tab.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.preview.quo.tabs.segmented-tab + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key 28 + :value "28"} + {:key 20 + :value "20"}]} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:size 32 + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :component-container-style {:padding-vertical 60}} + [:<> + [quo/segmented-control + (assoc @state + :default-active 1 + :data [{:id 1 :label "Tab 1"} + {:id 2 :label "Tab 2"} + {:id 3 :label "Tab 3"} + {:id 4 :label "Tab 4"}] + :on-change #(println "Active tab" %))] + [rn/view {:style {:padding-top 24}} + [quo/segmented-control + (assoc @state + :default-active 1 + :data [{:id 1 :label "Tab 1"} + {:id 2 :label "Tab 2"}] + :on-change #(println "Active tab" %))]]]]))) diff --git a/src/status_im/contexts/preview/quo/tabs/tabs.cljs b/src/status_im/contexts/preview/quo/tabs/tabs.cljs new file mode 100644 index 00000000000..ff90e5f8c37 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tabs/tabs.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.preview.quo.tabs.tabs + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key 32 + :value "32"} + {:key 24 + :value "24"}]} + {:key :unread-indicators? :type :boolean} + {:key :scrollable? :type :boolean}]) + +(defn generate-tab-items + [length unread-indicators?] + (for [index (range length)] + ^{:key index} + {:id index + :label (str "Tab " (inc index)) + :notification-dot? (and unread-indicators? (zero? (rem index 2)))})) + +(defn view + [] + (let [state (reagent/atom {:size 32 + :scrollable? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60 + :padding-horizontal 20 + :flex-direction :row + :justify-content :center}} + [quo/tabs + (merge @state + {:default-active 1 + :data (generate-tab-items (if (:scrollable? @state) 15 4) + (:unread-indicators? @state)) + :on-change #(println "Active tab" %)} + (when (:scrollable? @state) + {:scroll-on-press? true + :fade-end-percentage 0.4 + :fade-end? true}))]]))) diff --git a/src/status_im/contexts/preview/quo/tags/collectible_tag.cljs b/src/status_im/contexts/preview/quo/tags/collectible_tag.cljs new file mode 100644 index 00000000000..784ca819f1e --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/collectible_tag.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.preview.quo.tags.collectible-tag + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :size-24 + :value "Size 24"} + {:key :size-32 + :value "Size 32"}]} + {:key :options + :type :select + :options [{:key :add + :value :add} + {:key :hold + :value :hold}]} + {:key :blur? + :type :boolean} + {:key :collectible-name + :type :text} + {:key :collectible-id + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:size :size-24 + :collectible-name "Collectible" + :collectible-id "#123" + :collectible-img-src (resources/mock-images :collectible) + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :blur? (:blur? @state) + :show-blur-background? true + :descriptor descriptor} + [rn/view {:style {:align-items :center}} + [quo/collectible-tag @state]]]))) diff --git a/src/status_im/contexts/preview/quo/tags/context_tags.cljs b/src/status_im/contexts/preview/quo/tags/context_tags.cljs new file mode 100644 index 00000000000..6fa61b994a7 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/context_tags.cljs @@ -0,0 +1,224 @@ +(ns status-im.contexts.preview.quo.tags.context-tags + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def example-pk + "0x04fcf40c526b09ff9fb22f4a5dbd08490ef9b64af700870f8a0ba2133f4251d5607ed83cd9047b8c2796576bc83fa0de23a13a4dced07654b8ff137fe744047917") + +(def avatar-options + [{:key nil + :value "Default avatar"} + {:key (resources/mock-images :user-picture-male5) + :value "User pic male 5"} + {:key (resources/mock-images :user-picture-male4) + :value "User pic male 4"} + {:key (resources/mock-images :user-picture-female2) + :value "User pic female 2"}]) + +(def size-descriptor + {:key :size + :type :select + :options [{:key 24} + {:key 32}]}) + +(def descriptor + [{:key :type + :type :select + :options [{:key :default} + {:key :multiuser} + {:key :group} + {:key :channel} + {:key :community} + {:key :token} + {:key :network} + {:key :multinetwork} + {:key :account} + {:key :collectible} + {:key :address} + {:key :icon} + {:key :audio}]} + {:key :blur? + :type :boolean} + {:key :state + :type :select + :options [{:key :default} + {:key :selected}]} + (preview/customization-color-option)]) + +(def default-descriptor + [size-descriptor + {:key :profile-picture + :type :select + :options avatar-options} + {:key :full-name + :type :text}]) + +(def multiuser-descriptor + (let [users [(resources/mock-images :user-picture-male5) + (resources/mock-images :user-picture-male4) + nil + (resources/mock-images :user-picture-female2) + nil]] + [{:key :users + :type :select + :options (map (fn [idx] + {:key (mapv (fn [picture idx-name] + {:profile-picture picture + :full-name (str (inc idx-name)) + :customization-color (rand-nth (keys colors/customization))}) + (take idx (cycle users)) + (range)) + :value (str idx)}) + (range 1 10))}])) + +(def group-descriptor + [size-descriptor + {:key :group-name + :type :text}]) + +(def channel-descriptor + [size-descriptor + {:key :community-name + :type :text} + {:key :channel-name + :type :text}]) + +(def community-descriptor + [size-descriptor + {:key :community-name + :type :text}]) + +(def token-descriptor + [size-descriptor + {:key :amount + :type :text} + {:key :token + :type :select + :options [{:key "ETH"} + {:key "SNT"} + {:key "DAI"}]}]) + +(def network-descriptor + [size-descriptor + {:key :network-name + :type :text}]) + +(def multinetwork-descriptor + (let [networks (cycle [(resources/mock-images :monkey) (resources/mock-images :diamond)])] + [{:key :networks + :type :select + :options (map (fn [size] + {:key (take size networks) + :value (str size)}) + (range 1 10))}])) + +(def account-descriptor + [size-descriptor + {:key :account-name + :type :text} + {:key :emoji + :type :select + :options [{:key "🐷" :value "🐷"} + {:key "🍇" :value "🍇"} + {:key "🐱" :value "🐱"}]}]) + +(def collectible-descriptor + [size-descriptor + {:key :collectible-name + :type :text} + {:key :collectible-number + :type :text}]) + +(def address-descriptor + [size-descriptor]) + +(def icon-descriptor + [size-descriptor + {:key :context + :type :text} + {:key :icon + :type :select + :options [{:key :i/placeholder :value "Placeholder"} + {:key :i/add :value "Add"} + {:key :i/alert :value "Alert"}]}]) + +(def audio-descriptor + [{:key :duration + :type :text}]) + +(defn f-view + [state type] + (rn/use-effect (fn [] + (when (#{:multiuser :multinetwork :audio} @type) + (swap! state assoc :size 24))) + [@type]) + [preview/preview-container + {:state state + :descriptor (concat descriptor + (case (:type @state) + :default default-descriptor + :multiuser multiuser-descriptor + :group group-descriptor + :channel channel-descriptor + :community community-descriptor + :token token-descriptor + :network network-descriptor + :multinetwork multinetwork-descriptor + :account account-descriptor + :collectible collectible-descriptor + :address address-descriptor + :icon icon-descriptor + :audio audio-descriptor + default-descriptor)) + :blur-height 80 + :blur? (:blur? @state) + :show-blur-background? true} + [rn/view {:style {:align-items :center}} + [quo/context-tag @state]]]) + +(defn view + [] + (let [state + (reagent/atom + {:size 32 + :type :group + :blur? false + :state :selected + :customization-color :army + :profile-picture nil + :full-name "Full Name" + :users [{:profile-picture (resources/mock-images :user-picture-male5) + :full-name "1" + :customization-color (rand-nth (keys colors/customization))} + {:profile-picture nil + :full-name "3" + :customization-color (rand-nth (keys colors/customization))} + {:profile-picture (resources/mock-images :user-picture-male5) + :full-name "2" + :customization-color (rand-nth (keys colors/customization))}] + :group-name "Group" + :community-logo (resources/mock-images :coinbase) + :community-name "Community" + :channel-name "my channel" + :token "ETH" + :amount "10" + :network-logo (resources/mock-images :monkey) + :network-name "Network" + :networks [(resources/mock-images :monkey) + (resources/mock-images :diamond)] + :account-name "Account name" + :emoji "😝" + :collectible (resources/mock-images :collectible) + :collectible-name "Collectible" + :collectible-number "123" + :address example-pk + :icon :i/placeholder + :context "Context" + :duration "00:32"}) + type (reagent/cursor state [:type])] + [:f> f-view state type])) diff --git a/src/status_im/contexts/preview/quo/tags/network_status_tag.cljs b/src/status_im/contexts/preview/quo/tags/network_status_tag.cljs new file mode 100644 index 00000000000..4e3084603a5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/network_status_tag.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.preview.quo.tags.network-status-tag + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :label :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:label "Updated 5 min ago"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [rn/view {:style {:align-items :center}} + [quo/network-status-tag @state]]]))) diff --git a/src/status_im/contexts/preview/quo/tags/network_tags.cljs b/src/status_im/contexts/preview/quo/tags/network_tags.cljs new file mode 100644 index 00000000000..99686a95c43 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/network_tags.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.preview.quo.tags.network-tags + (:require + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def community-networks + [[{:source (resources/get-network :ethereum)}] + [{:source (resources/get-network :arbitrum)} + {:source (resources/get-network :ethereum)}] + [{:source (resources/get-network :arbitrum)} + {:source (resources/get-network :optimism)} + {:source (resources/get-network :ethereum)}]]) + +(def descriptor + [{:type :select + :key :status + :options [{:key :error} + {:key :warning} + {:key :default}]} + {:type :text + :key :title} + {:type :select + :key :networks + :options [{:key 0} + {:key 1} + {:key 2} + {:key 3}]} + {:type :boolean + :key :blur?}]) + +(defn view + [] + (let [state (reagent/atom {:title "Tag" + :status :default + :networks 3})] + (fn [] + (let [selected-networks-id (dec (:networks @state))] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [rn/view + {:style {:align-self :center + :justify-content :center + :flex 1}} + [quo/network-tags + (assoc @state + :networks + (when (pos? selected-networks-id) + (nth community-networks selected-networks-id)))]]])))) diff --git a/src/status_im/contexts/preview/quo/tags/number_tag.cljs b/src/status_im/contexts/preview/quo/tags/number_tag.cljs new file mode 100644 index 00000000000..0517c8a8f7b --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/number_tag.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.preview.quo.tags.number-tag + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :rounded} + {:key :squared}]} + {:key :number + :type :text} + {:key :size + :type :select + :options [{:key :size-32 + :value "32"} + {:key :size-24 + :value "24"} + {:key :size-20 + :value "20"} + {:key :size-16 + :value "16"} + {:key :size-14 + :value "14"}]} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:type :squared + :number "148" + :size :size-32 + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [quo/number-tag @state]]))) diff --git a/src/status_im/contexts/preview/quo/tags/permission_tag.cljs b/src/status_im/contexts/preview/quo/tags/permission_tag.cljs new file mode 100644 index 00000000000..ca9fdb45837 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/permission_tag.cljs @@ -0,0 +1,191 @@ +(ns status-im.contexts.preview.quo.tags.permission-tag + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key 32 + :value "32"} + {:key 24 + :value "24"}]} + {:key :locked? + :type :boolean}]) + +(def community-tokens + [{:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 1 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)} + {:id 5 :token-icon (resources/get-mock-image :status-logo)} + {:id 6 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)} + {:id 5 :token-icon (resources/get-mock-image :status-logo)} + {:id 6 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)} + {:id 5 :token-icon (resources/get-mock-image :status-logo)} + {:id 6 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)} + {:id 5 :token-icon (resources/get-mock-image :status-logo)} + {:id 6 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 3 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 3 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)} + {:id 5 :token-icon (resources/get-mock-image :status-logo)} + {:id 6 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 3 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]}]} + {:tokens [{:id 1 :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 2 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)}]} + {:id 3 + :group [{:id 1 :token-icon (resources/get-mock-image :status-logo)} + {:id 2 :token-icon (resources/get-mock-image :status-logo)} + {:id 3 :token-icon (resources/get-mock-image :status-logo)} + {:id 4 :token-icon (resources/get-mock-image :status-logo)} + {:id 5 :token-icon (resources/get-mock-image :status-logo)} + {:id 6 :token-icon (resources/get-mock-image :status-logo)}]}]}]) + +(defn view + [] + (let [state (reagent/atom {:size 32})] + (fn [] + (let [theme (quo.context/use-theme)] + [preview/preview-container + {:state state + :component-container-style {:margin-bottom 40} + :descriptor descriptor} + (when @state + (for [{:keys [tokens]} community-tokens] + ^{:key tokens} + [rn/view + {:margin-top 20 + :align-self :flex-end} + [quo/permission-tag + (merge @state + {:tokens tokens + :background-color (colors/theme-colors + colors/neutral-10 + colors/neutral-80 + theme)})]]))])))) diff --git a/src/status_im/contexts/preview/quo/tags/status_tags.cljs b/src/status_im/contexts/preview/quo/tags/status_tags.cljs new file mode 100644 index 00000000000..ed81c5c8dcc --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/status_tags.cljs @@ -0,0 +1,55 @@ +(ns status-im.contexts.preview.quo.tags.status-tags + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.i18n :as i18n])) + +(def status-tags-options + {:label "Status" + :key :status + :type :select + :options [{:value "Positive" + :key :positive} + {:value "Negative" + :key :negative} + {:value "Pending" + :key :pending}]}) + +(def descriptor + [status-tags-options + {:label "Size" + :key :size + :type :select + :options [{:value "Small" + :key :small} + {:value "Large" + :key :large}]} + {:label "Blur?" + :key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:status :positive + :size :small + :blur? false})] + (fn [] + (let [props (case (:status @state) + :positive (-> @state + (assoc :status {:type :positive}) + (assoc :label (i18n/label :t/positive))) + :negative (-> @state + (assoc :status {:type :negative}) + (assoc :label (i18n/label :t/negative))) + :pending (-> @state + (assoc :status {:type :pending}) + (assoc :label (i18n/label :t/pending))))] + [preview/preview-container + {:state state + :show-blur-background? true + :blur? (:blur? @state) + :descriptor descriptor} + [rn/view {:style {:flex-direction :row}} + [quo/status-tag props]]])))) diff --git a/src/status_im/contexts/preview/quo/tags/summary_tag.cljs b/src/status_im/contexts/preview/quo/tags/summary_tag.cljs new file mode 100644 index 00000000000..b901dd47a07 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/summary_tag.cljs @@ -0,0 +1,66 @@ +(ns status-im.contexts.preview.quo.tags.summary-tag + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(defn data + [type] + (case type + :token + {:customization-color "#9999991A" + :label "150 ETH" + :token :eth} + :address + {:label "0x39c...Bd2"} + :user + {:image-source (resources/mock-images :user-picture-male4) + :customization-color :blue + :label "Mark Libot"} + :collectible + {:label "Isekai #1" + :image-source (resources/mock-images :collectible2) + :customization-color :yellow} + :network + {:image-source (quo.resources/get-network :ethereum) + :label "Ethereum"} + :saved-address + {:customization-color :pink + :label "Peter Lambo"} + :account + {:label "Account" + :emoji "🍿" + :customization-color :purple})) + +(def descriptor + [{:key :type + :type :select + :options [{:value "Token" + :key :token} + {:value "Address" + :key :address} + {:value "User" + :key :user} + {:value "Collectible" + :key :collectible} + {:value "Network" + :key :network} + {:value "Saved address" + :key :saved-address} + {:value "Account" + :key :account} + {:value "Dapp" + :key :dapp}]}]) + +(defn view + [] + (let [state (reagent/atom (assoc (data :token) :type :token))] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [rn/view {:style {:align-items :center}} + [quo/summary-tag (merge @state (data (:type @state)))]]]))) diff --git a/src/status_im/contexts/preview/quo/tags/tag.cljs b/src/status_im/contexts/preview/quo/tags/tag.cljs new file mode 100644 index 00000000000..b1bde147bc9 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/tag.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.preview.quo.tags.tag + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key 32 + :value "32"} + {:key 24 + :value "24"}]} + {:key :type + :type :select + :options [{:key :emoji + :value "Emoji"} + {:key :icon + :value "Icons"} + {:key :label + :value "Label"}]} + {:key :labelled? + :type :boolean} + {:key :disabled? + :type :boolean} + {:key :blurred? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:size 32 + :labelled? true + :type :emoji})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :show-blur-background? true + :blur? (:blurred? @state)} + [quo/tag + (merge @state + {:id 1 + :label "Tag" + :labelled? (if (= (:type @state) :label) + true + (:labelled? @state)) + :resource (if (= :emoji (:type @state)) + (resources/get-image :music) + :i/placeholder)})]]))) diff --git a/src/status_im/contexts/preview/quo/tags/tags.cljs b/src/status_im/contexts/preview/quo/tags/tags.cljs new file mode 100644 index 00000000000..8efc9612f05 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/tags.cljs @@ -0,0 +1,64 @@ +(ns status-im.contexts.preview.quo.tags.tags + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key 32 + :value "32"} + {:key 24 + :value "24"}]} + {:key :type + :type :select + :options [{:key :emoji + :value "Emoji"} + {:key :icon + :value "Icons"} + {:key :label + :value "Label"}]} + {:key :scrollable? + :type :boolean} + {:key :fade-end-percentage + :type :select + :options [{:key 1 + :value "1%"} + {:key 0.4 + :value "0.4%"}]} + {:key :labelled? + :type :boolean} + {:key :disabled? + :type :boolean} + {:key :blurred? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:size 32 + :labelled? true + :type :emoji + :fade-end-percentage 0.4 + :scrollable? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blurred? @state) + :show-blur-background? true} + [quo/tags + (merge + @state + {:default-active 1 + :component :tags + :labelled? (if (= :label (:type @state)) true (:labelled? @state)) + :data [{:id 1 :label "Music" :resource (resources/get-image :music)} + {:id 2 :label "Lifestyle" :resource (resources/get-image :lifestyle)} + {:id 2 :label "Podcasts" :resource (resources/get-image :podcasts)} + {:id 2 :label "Music" :resource (resources/get-image :music)} + {:id 3 :label "Lifestyle" :resource (resources/get-image :lifestyle)}]} + (when (:scrollable? @state) + {:scroll-on-press? true + :fade-end? true}))]]))) diff --git a/src/status_im/contexts/preview/quo/tags/tiny_tag.cljs b/src/status_im/contexts/preview/quo/tags/tiny_tag.cljs new file mode 100644 index 00000000000..6dd5ec257a6 --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/tiny_tag.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.preview.quo.tags.tiny-tag + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? :type :boolean} + {:key :label :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:blur? false + :label "1,000 SNT"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true} + [rn/view {:style {:align-items :center}} + [quo/tiny-tag @state]]]))) diff --git a/src/status_im/contexts/preview/quo/tags/token_tag.cljs b/src/status_im/contexts/preview/quo/tags/token_tag.cljs new file mode 100644 index 00000000000..a61a355d33b --- /dev/null +++ b/src/status_im/contexts/preview/quo/tags/token_tag.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.preview.quo.tags.token-tag + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :size + :type :select + :options [{:key :size-24 + :value "Size 24"} + {:key :size-32 + :value "Size 32"}]} + {:key :token-value + :type :select + :options [{:key 0 + :value "0"} + {:key 10 + :value "10"} + {:key 100 + :value "100"} + {:key 1000 + :value "1000"} + {:key 10000 + :value "10000"}]} + {:key :options + :type :select + :options [{:key false + :value false} + {:key :add + :value :add} + {:key :hold + :value :hold}]} + {:key :blur? + :type :boolean} + {:key :token-symbol + :type :select + :options [{:key "ETH" + :value "ETH"} + {:key "SNT" + :value "SNT"}]}]) + +(defn view + [] + (let [state (reagent/atom {:size :size-24 + :token-value 10 + :token-symbol "ETH" + :options false + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :blur? (:blur? @state) + :show-blur-background? true + :descriptor descriptor} + [rn/view {:style {:align-items :center}} + [quo/token-tag @state]]]))) diff --git a/src/status_im/contexts/preview/quo/text_combinations/channel_name.cljs b/src/status_im/contexts/preview/quo/text_combinations/channel_name.cljs new file mode 100644 index 00000000000..aed3c7ef5d9 --- /dev/null +++ b/src/status_im/contexts/preview/quo/text_combinations/channel_name.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.text-combinations.channel-name + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + + +(def descriptor + [{:key :channel-name + :type :text} + {:key :unlocked? + :type :boolean} + {:key :muted? + :type :boolean} + {:key :blur? + :type :boolean}]) + +(defn view + [] + (let [state (reagent/atom {:channel-name "random" + :unlocked? true + :muted? true + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :show-blur-background? true + :blur? (:blur? @state)} + [quo/channel-name @state]]))) diff --git a/src/status_im/contexts/preview/quo/text_combinations/page_top.cljs b/src/status_im/contexts/preview/quo/text_combinations/page_top.cljs new file mode 100644 index 00000000000..c3fd0e87716 --- /dev/null +++ b/src/status_im/contexts/preview/quo/text_combinations/page_top.cljs @@ -0,0 +1,166 @@ +(ns status-im.contexts.preview.quo.text-combinations.page-top + (:require [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def avatar-1 + {:emoji "🥨" + :customization-color :army}) + +(def avatar-2 + {:emoji "🍑" + :customization-color :blue}) + +(def context-tag-1 + {:type :community + :state :default + :customization-color :army + :community-logo (resources/mock-images :coinbase) + :community-name "Coinbase" + :emoji "😝"}) + +(def context-tag-2 + {:type :collectible + :state :default + :customization-color :army + :collectible (resources/mock-images :collectible) + :collectible-name "Collectible" + :collectible-number "123"}) + +(def context-tag-3 + {:type :token + :state :default + :token "SNT" + :amount "250,000"}) + +(def context-tag-4 + {:type :account + :blur? false + :state :default + :customization-color :sky + :account-name "Trip to vegas" + :emoji "⚡"}) +(def context-tag-5 + {:type :default + :customization-color :army + :profile-picture nil + :full-name "Random user"}) +(def context-tag-6 + {:type :network + :customization-color :army + :network-logo (quo.resources/get-network :optimism) + :network-name "Optimism"}) + +(def emoji-dash-1 + ["❤️" "✏️" "💬" "😋" "📱" "🚓" "💹" "😝" "👊" "👤" "😚" "🚉" "👻" "\uD83D\uDC6F"]) + +(def emoji-dash-2 + ["🍩" "🤖" "🏀" "🔥" "🌂" "💎" "🚨" "😍" "🐷" "🌶" "🍑" "😈" "🦄" "🕵️‍♀️"]) + +(def main-descriptor + [{:key :blur? + :type :boolean} + {:key :title + :type :text} + {:key :avatar + :type :select + :options [{:key nil + :value "(No avatar)"} + {:key avatar-1 + :value "Avatar variation 1"} + {:key avatar-2 + :value "Avatar variation 2"}]} + {:key :description + :type :select + :options [{:key nil + :value "(No description)"} + {:key :text} + {:key :context-tag} + {:key :summary} + {:key :collection} + {:key :community}]} + {:key :emoji-dash + :type :select + :options [{:key nil + :value "(No emoji dash)"} + {:key emoji-dash-1 + :value "Emoji dash variation 1"} + {:key emoji-dash-2 + :value "Emoji dash variation 2"}]} + {:key :input + :type :select + :options [{:key nil + :value "(No input)"} + {:key :search} + {:key :address} + {:key :recovery-phrase}]}]) + +(def description-text-descriptor + [{:key :description-text + :type :text}]) + +(def context-tag-descriptor + [{:key :context-tag + :type :select + :options [{:key context-tag-1 + :value "Context tag variation 1"} + {:key context-tag-2 + :value "Context tag variation 2"} + {:key context-tag-3 + :value "Context tag variation 3"} + {:key context-tag-4 + :value "Context tag variation 4"} + {:key context-tag-5 + :value "Context tag variation 5"} + {:key context-tag-6 + :value "Context tag variation 6"}]}]) + +(def recovery-phrase-descriptor + [{:key :counter-top + :type :text} + {:key :counter-bottom + :type :text}]) + +(defn view + [] + (let [state (reagent/atom + {:blur? true + :title "Title" + :description nil + :description-text (str "Share random funny stuff with the community " + "and then something more here.") + :context-tag context-tag-1 + :summary {:row-1 {:text-1 "Send" + :text-2 "from" + :context-tag-1 context-tag-3 + :context-tag-2 context-tag-4} + :row-2 {:text-1 "to" + :text-2 "via" + :context-tag-1 context-tag-5 + :context-tag-2 context-tag-6}} + :emoji-dash nil + :input nil + :collection-text "Collectible Collection" + :collection-image (resources/get-mock-image :collectible-monkey) + :community-image (resources/get-mock-image :community-logo) + :community-text "Doodles" + :input-props {:placeholder "Input placeholder"} + :counter-top "50" + :counter-bottom "100"})] + (fn [] + (let [descriptor (concat main-descriptor + (case (:description @state) + :text description-text-descriptor + :context-tag context-tag-descriptor + nil) + (when (= (:input @state) :recovery-phrase) + recovery-phrase-descriptor))] + [preview/preview-container + {:state state + :descriptor descriptor + :show-blur-background? true + :blur? (:blur? @state) + :component-container-style {:padding-horizontal 10}} + [quo/page-top @state]])))) diff --git a/src/status_im/contexts/preview/quo/text_combinations/preview.cljs b/src/status_im/contexts/preview/quo/text_combinations/preview.cljs new file mode 100644 index 00000000000..f1d471fa8f6 --- /dev/null +++ b/src/status_im/contexts/preview/quo/text_combinations/preview.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.preview.quo.text-combinations.preview + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title :type :text} + {:key :avatar :type :boolean} + {:key :description :type :text} + {:key :emoji-hash :type :text}]) + +(defn state->text-combinations-props + [state] + (if (:avatar state) + (assoc state :avatar (resources/get-mock-image :user-picture-male4)) + state)) + +(defn view + [] + (let [state (reagent/atom {:title "Title" + :title-accessibility-label :title + :description "" + :description-accessibility-label :subtitle + :emoji-hash "🐲🍀🎭🌟🚀🐠🌈🏰🔮🦉🐼🍉🎨🚲🌙🍔🌵"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60}} + [quo/text-combinations (state->text-combinations-props @state)]]))) diff --git a/src/status_im/contexts/preview/quo/text_combinations/standard_title.cljs b/src/status_im/contexts/preview/quo/text_combinations/standard_title.cljs new file mode 100644 index 00000000000..04ae7cefbe0 --- /dev/null +++ b/src/status_im/contexts/preview/quo/text_combinations/standard_title.cljs @@ -0,0 +1,66 @@ +(ns status-im.contexts.preview.quo.text-combinations.standard-title + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :title :type :text} + {:key :right + :type :select + :options [{:key nil + :value "Nothing (nil)"} + {:key :counter} + {:key :action} + {:key :tag}]} + {:key :blur? :type :boolean}]) + +(def counter-descriptor + [{:key :counter-left + :type :select + :options (mapv (fn [n] {:key n}) + (range 0 101 25))} + {:key :counter-right + :type :select + :options (mapv (fn [n] {:key n}) + (range 0 101 25))}]) + +(def action-descriptor + [(preview/customization-color-option) + {:key :icon + :type :select + :options [{:key :i/placeholder} + {:key :i/info} + {:key :i/key}]}]) + +(def tag-descriptor + [{:key :icon + :type :select + :options [{:key :i/placeholder} + {:key :i/info} + {:key :i/key}]} + {:key :label + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:title "Title" + :blur? true + :right :counter + :counter-left 50 + :counter-right 100 + :on-press #(js/alert "Button clicked!") + :customization-color :army + :icon :i/placeholder + :label ""})] + (fn [] + (let [typed-descriptor (case (:right @state) + :counter counter-descriptor + :action action-descriptor + :tag tag-descriptor + nil)] + [preview/preview-container + {:state state + :descriptor (concat descriptor typed-descriptor) + :show-blur-background? true + :blur? (:blur? @state)} + [quo/standard-title @state]])))) diff --git a/src/status_im/contexts/preview/quo/text_combinations/username.cljs b/src/status_im/contexts/preview/quo/text_combinations/username.cljs new file mode 100644 index 00000000000..5af209ad836 --- /dev/null +++ b/src/status_im/contexts/preview/quo/text_combinations/username.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.preview.quo.text-combinations.username + (:require [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :name-type + :type :select + :options [{:key :default} + {:key :ens + :value "ENS"} + {:key :nickname}]} + {:key :username + :type :text} + {:key :name + :type :text} + {:key :status + :type :select + :options [{:key nil + :value "(Nothing)"} + {:key :verified} + {:key :contact} + {:key :untrustworthy} + {:key :untrustworthy-contact} + {:key :blocked}]} + {:key :blur? + :type :boolean}]) + +(defn- set-username-based-on-name-type + [_ ratom {previous-type :name-type} {new-type :name-type}] + (when (not= previous-type new-type) + (swap! ratom assoc + :username + (if (= new-type :ens) + "juan.eth" + "Juanito Mdz")))) + +(defn view + [] + (let [state (reagent/atom {:name-type :default + :username "Juanito Mdz" + :name "Juan Méndez" + :status nil + :blur? true}) + _ (add-watch state :on-state-change set-username-based-on-name-type)] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :show-blur-background? true + :blur? (:blur? @state)} + [quo/username @state]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/account_card.cljs b/src/status_im/contexts/preview/quo/wallet/account_card.cljs new file mode 100644 index 00000000000..fd1984d270a --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/account_card.cljs @@ -0,0 +1,86 @@ +(ns status-im.contexts.preview.quo.wallet.account-card + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.collection])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :default} + {:key :watch-only} + {:key :add-account} + {:key :empty} + {:key :missing-keypair}]} + (preview/customization-color-option) + {:key :name :type :text} + {:key :balance :type :text} + {:key :percentage-value :type :text} + {:key :amount :type :text} + {:key :metrics? :type :boolean} + {:key :loading? :type :boolean} + {:key :emoji :type :text}]) + +(defn initial-state + [type] + (case type + :default + {:name "Alisher account" + :balance "€2,269.12" + :percentage-value "16.9%" + :amount "€570.24" + :customization-color :turquoise + :metrics? true + :type :default + :emoji "💎"} + + :empty + {:name "Account 1" + :balance "€0.00" + :percentage-value "€0.00" + :customization-color :blue + :metrics? true + :type :empty + :emoji "🍑"} + + :watch-only + {:name "Ben’s fortune" + :balance "€2,269.12" + :percentage-value "16.9%" + :amount "€570.24" + :metrics? true + :type :watch-only + :customization-color :army + :emoji "💸"} + + :missing-keypair + {:name "Trip to Vegas" + :balance "€2,269.12" + :percentage-value "16.9%" + :amount "€570.24" + :metrics? true + :customization-color :turquoise + :type :missing-keypair + :emoji "🎲"} + + :add-account + {:customization-color :blue + :on-press #(js/alert "Button pressed") + :metrics? true + :type :add-account})) + + +(defn view + [] + (let [state (reagent/atom (initial-state :default))] + [:f> + (fn [] + (rn/use-effect + (fn [] (reset! state (initial-state (:type @state)))) + [(:type @state)]) + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/account-card @state]])])) diff --git a/src/status_im/contexts/preview/quo/wallet/account_origin.cljs b/src/status_im/contexts/preview/quo/wallet/account_origin.cljs new file mode 100644 index 00000000000..4afc299e548 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/account_origin.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.preview.quo.wallet.account-origin + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :select + :key :type + :options [{:key :default-keypair} + {:key :recovery-phrase} + {:key :private-key}]} + {:type :select + :key :stored + :options [{:key :on-device} + {:key :on-keycard}]}]) + +(defn view + [] + (let [state (reagent/atom {:type :default-keypair + :stored :on-keycard + :profile-picture (resources/get-mock-image :user-picture-male5) + :derivation-path "m / 44’ / 60’ / 0’ / 0’ / 2" + :user-name "Alisher Yakupov" + :on-press #(js/alert "pressed")})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/account-origin @state]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/account_overview.cljs b/src/status_im/contexts/preview/quo/wallet/account_overview.cljs new file mode 100644 index 00000000000..4d32822769a --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/account_overview.cljs @@ -0,0 +1,64 @@ +(ns status-im.contexts.preview.quo.wallet.account-overview + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :time-frame-string :type :text} + {:key :time-frame-to-string :type :text} + {:key :percentage-change :type :text} + {:key :currency-change :type :text} + {:key :current-value :type :text} + {:key :state + :type :select + :options [{:key :default} + {:key :loading}]} + {:key :metrics + :type :select + :options [{:key :positive} + {:key :negative}]} + {:key :account-name :type :text} + (preview/customization-color-option) + {:key :account + :type :select + :options [{:key :watched-address} + {:key :default}]} + {:key :time-frame + :type :select + :options [{:key :one-week + :value "1 Week"} + {:key :one-month + :value "1 Month"} + {:key :three-months + :value "3 Months"} + {:key :one-year + :value "1 Year"} + {:key :all-time + :value "All time"} + {:key :custom + :value "Custom"}]}]) + +(defn view + [] + (let [state (reagent/atom {:metrics :positive + :currency-change "€0.00" + :percentage-change "0.00%" + :current-value "€0.00" + :account-name "Diamond Hand" + :time-frame :custom + :time-frame-string "16 May" + :time-frame-to-string "25 May" + :account :default + :customization-color :blue + :container-style {:padding-top 24 + :padding-horizontal 20 + :padding-bottom 20}})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60 + :flex-direction :row + :justify-content :center}} + [quo/account-overview @state]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/account_permissions.cljs b/src/status_im/contexts/preview/quo/wallet/account_permissions.cljs new file mode 100644 index 00000000000..ea276b05a3f --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/account_permissions.cljs @@ -0,0 +1,75 @@ +(ns status-im.contexts.preview.quo.wallet.account-permissions + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def ^:private descriptor + [{:key :name + :type :text} + {:key :address + :type :text} + {:key :emoji + :type :text} + (preview/customization-color-option {:key :account-color}) + {:key :keycard? + :type :boolean} + {:key :checked? + :type :boolean} + {:key :disabled? + :type :boolean} + {:key :token-details + :type :select + :options [{:key :no-tokens} + {:key :empty-token-list} + {:key :1-token} + {:key :3-tokens} + {:key :5-tokens}]} + (preview/customization-color-option {:key :customization-color})]) + +(def ^:private token-details + {:no-tokens nil + :empty-token-list [] + :1-token [{:token "SNT" + :amount "100"}] + :3-tokens [{:token "SNT" + :amount "100"} + {:token "ETH" + :amount "18"} + {:token "BTM" + :amount "1000"}] + :5-tokens [{:token "SNT" + :amount "100"} + {:token "ETH" + :amount "18"} + {:token "BTM" + :amount "1000"} + {:token "CFI" + :amount "1"} + {:token "CK" + :amount "18"}]}) + +(defn view + [] + (let [state (reagent/atom {:name "Trip to Vegas" + :address "0x2f0fbf0a93c5999e9b4410848403a02b38983eb2" + :emoji "😊" + :account-color :blue + :token-details :no-tokens + :customization-color :blue + :keycard? true + :checked? true + :disabled? false})] + (fn [] + [preview/preview-container {:state state :descriptor descriptor} + [quo/account-permissions + {:account {:name (:name @state) + :address (:address @state) + :emoji (:emoji @state) + :customization-color (:account-color @state)} + :token-details (token-details (:token-details @state)) + :keycard? (:keycard? @state) + :checked? (:checked? @state) + :disabled? (:disabled? @state) + :customization-color (:customization-color @state) + :on-change (fn [checked?] (swap! state assoc :checked? checked?))}]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/amount_input.cljs b/src/status_im/contexts/preview/quo/wallet/amount_input.cljs new file mode 100644 index 00000000000..8038d44dc46 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/amount_input.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.preview.quo.wallet.amount-input + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :max-value + :type :number} + {:key :min-value + :type :number} + {:key :value + :type :number} + {:type :select + :key :status + :options [{:key :default} + {:key :error}]}]) + +(defn view + [] + (let [state (reagent/atom {:max-value 10000 + :min-value 0 + :value 1 + :status :default}) + on-inc-press (fn [] (swap! state #(assoc % :value (inc (:value %))))) + on-dec-press (fn [] (swap! state #(assoc % :value (dec (:value %)))))] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor} + [quo/amount-input + (merge + @state + {:on-dec-press on-dec-press + :on-inc-press on-inc-press})]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/approval_label.cljs b/src/status_im/contexts/preview/quo/wallet/approval_label.cljs new file mode 100644 index 00000000000..be6f046d58b --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/approval_label.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.preview.quo.wallet.approval-label + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :select + :key :status + :options [{:key :approve} + {:key :approving} + {:key :approved}]} + {:type :text + :key :token-value} + {:type :select + :key :token-symbol + :options [{:key "SNT"} + {:key "DAI"} + {:key "ETH"}]} + (preview/customization-color-option)]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:status :approve + :customization-color :blue + :token-value "100" + :token-symbol "SNT"})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor + :component-container-style {:padding-top 50}} + [quo/approval-label + (assoc state + :button-props + {:on-press + #(js/alert "Pressed")})]])) diff --git a/src/status_im/contexts/preview/quo/wallet/confirmation_progress.cljs b/src/status_im/contexts/preview/quo/wallet/confirmation_progress.cljs new file mode 100644 index 00000000000..cd66166338a --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/confirmation_progress.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.preview.quo.wallet.confirmation-progress + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :number + :key :progress-value} + {:type :select + :key :network + :options [{:key :mainnet} + {:key :optimism} + {:key :arbitrum}]} + {:type :select + :key :state + :options [{:key :pending} + {:key :sending} + {:key :confirmed} + {:key :finalising} + {:key :finalized} + {:key :error}]} + (preview/customization-color-option)]) + +(def total-box 85) +(def counter (reagent/atom 0)) +(def interval-id (reagent/atom nil)) +(def interval-ms 50) + +(defn- stop-interval + [] + (when @interval-id + (js/clearInterval @interval-id) + (reset! interval-id nil))) + +(defn- clear-counter + [] + (reset! counter 0)) + +(defn- update-counter + [state] + (let [new-counter-value (inc @counter)] + (if (or (and (= state :pending) (> new-counter-value 0)) + (and (= state :sending) (> new-counter-value 2)) + (and (= state :confirmed) (> new-counter-value 4)) + (and (= state :finalising) (> new-counter-value 18)) + (and (= state :finalized) (> new-counter-value total-box)) + (and (= state :error) (> new-counter-value 2))) + (stop-interval) + (reset! counter new-counter-value)))) + +(defn- start-interval + [state] + (reset! interval-id + (js/setInterval + (fn [] + (update-counter state)) + interval-ms))) + +(defn- f-view + [state] + (fn [] + (rn/use-effect + (fn [] + (start-interval (:state @state)) + (clear-counter) + (fn [] + (stop-interval))) + [(:state @state)]) + [preview/preview-container {:state state :descriptor descriptor} + [quo/confirmation-progress + (assoc @state + :counter + @counter)]])) + +(defn view + [] + (let [state (reagent/atom + {:total-box total-box + :progress-value 10 + :network :mainnet + :state :pending + :customization-color :blue})] + [:f> f-view state])) diff --git a/src/status_im/contexts/preview/quo/wallet/keypair.cljs b/src/status_im/contexts/preview/quo/wallet/keypair.cljs new file mode 100644 index 00000000000..56d0600917d --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/keypair.cljs @@ -0,0 +1,110 @@ +(ns status-im.contexts.preview.quo.wallet.keypair + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def accounts + [{:account-props {:customization-color :turquoise + :size 32 + :emoji "\uD83C\uDFB2" + :type :default + :name "Trip to Vegas" + :address "0x0ah...71a"} + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}] + :state :default + :action :none} + {:account-props {:customization-color :purple + :size 32 + :emoji "\uD83C\uDF7F" + :type :default + :name "My savings" + :address "0x0ah...72b"} + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}] + :state :default + :action :none} + {:account-props {:customization-color :army + :size 32 + :emoji "\uD83D\uDCC8" + :type :default + :name "Coin vault" + :address "0x0ah...73c"} + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}] + :state :default + :action :none} + {:account-props {:customization-color :orange + :size 32 + :emoji "\uD83C\uDFF0" + :type :default + :name "Crypto fortress" + :address "0x0ah...74e"} + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}] + :state :default + :action :none} + {:account-props {:customization-color :yellow + :size 32 + :emoji "\uD83C\uDFDD️" + :type :default + :name "Block treasure" + :address "0x0ah...75f"} + :networks [{:network-name :ethereum :short-name "eth"} + {:network-name :optimism :short-name "oeth"}] + :state :default + :action :none}]) + +(defn get-accounts + [blur?] + (map (fn [account] (assoc account :blur? blur?)) accounts)) + +(def descriptor + [{:key :stored + :type :select + :options [{:key :on-device} + {:key :on-keycard}]} + {:key :action + :type :select + :options [{:key :selector} + {:key :options}]} + {:key :type + :type :select + :options [{:key :default-keypair} + {:key :other}]} + (preview/customization-color-option) + {:key :blur? :type :boolean}]) + +(def default-details + {:full-name "John Doe" + :address "zQ3...6fBd2"}) + +(def other-details {:full-name "Metamask"}) + +(defn view + [] + (let [state (reagent/atom {:customization-color :blue + :type :default-keypair + :stored :on-device + :on-options-press #(js/alert "Options pressed") + :action :selector + :blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true + :blur-height 400 + :component-container-style {:padding-vertical 30 + :flex-direction :row + :justify-content :center}} + [rn/view {:style {:flex 1}} + [quo/keypair + (merge + @state + {:details (if (= (:type @state) :default-keypair) default-details other-details) + :accounts (get-accounts (:blur? @state))})]]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/missing_keypairs.cljs b/src/status_im/contexts/preview/quo/wallet/missing_keypairs.cljs new file mode 100644 index 00000000000..22e21f3f6ab --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/missing_keypairs.cljs @@ -0,0 +1,57 @@ +(ns status-im.contexts.preview.quo.wallet.missing-keypairs + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def keypair + {:key-uid "0x01" + :type "seed" + :name "My Key Pair" + :blur? false}) + +(def accounts + [{:customization-color :turquoise + :emoji "\uD83C\uDFB2" + :type :default} + {:customization-color :purple + :emoji "\uD83C\uDF7F" + :type :default} + {:customization-color :army + :emoji "\uD83D\uDCC8" + :type :default} + {:customization-color :orange + :emoji "\uD83C\uDFF0" + :type :default} + {:customization-color :yellow + :emoji "\uD83C\uDFDD️" + :type :default}]) + +(def descriptor + [{:key :blur? :type :boolean}]) + +(def component-props + {:blur? false + :keypairs [{:name (:name keypair) + :key-uid (:key-uid keypair) + :type (keyword (:type keypair)) + :accounts accounts}]}) + +(defn view + [] + (let [state (reagent/atom {:blur? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :blur? (:blur? @state) + :show-blur-background? true + :blur-dark-only? true + :blur-height 400 + :component-container-style {:padding-vertical 30 + :flex-direction :row + :justify-content :center}} + [rn/view {:style {:flex 1}} + [quo/missing-keypairs + (merge component-props @state)]]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/network_amount.cljs b/src/status_im/contexts/preview/quo/wallet/network_amount.cljs new file mode 100644 index 00000000000..ee2af7684a2 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/network_amount.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.preview.quo.wallet.network-amount + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + + +(def descriptor + [{:key :amount :type :text} + {:key :token + :type :select + :options [{:key :eth} + {:key :snt}]}]) + +(defn view + [] + (let [state (reagent/atom {:amount "5.123456" + :token :eth})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-horizontal 20}} + [quo/network-amount @state]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/network_bridge.cljs b/src/status_im/contexts/preview/quo/wallet/network_bridge.cljs new file mode 100644 index 00000000000..6e7d00448b9 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/network_bridge.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.preview.quo.wallet.network-bridge + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + + +(def descriptor + [{:key :network + :type :select + :options [{:key :ethereum} + {:key :optimism} + {:key :arbitrum}]} + {:key :status + :type :select + :options [{:key :default :value :default} + {:key :locked :value :locked} + {:key :loading :value :loading} + {:key :disabled :value :disabled} + {:key :edit :value :edit}]}]) + +(defn view + [] + (let [state (reagent/atom {:network :mainnet + :status :default + :amount "50 SNT"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-horizontal 20 + :margin-top 50 + :align-items :center}} + [quo/network-bridge @state]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/network_link.cljs b/src/status_im/contexts/preview/quo/wallet/network_link.cljs new file mode 100644 index 00000000000..e816c5bd0e2 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/network_link.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.preview.quo.wallet.network-link + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def networks + [{:key :ethereum + :value "Ethereum"} + {:key :optimism + :value "Optimism"} + {:key :arbitrum + :vault "Arbitrum"}]) + +(def descriptor + [{:key :shape + :type :select + :options [{:key :linear} + {:key :1x} + {:key :2x}]} + {:key :source + :type :select + :options networks} + {:key :destination + :type :select + :options networks} + {:key :width + :type :number}]) + +(defn view + [] + (let [state (reagent/atom {:shape :linear + :source :ethereum + :destination :optimism + :width 63})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-top 40 + :align-items :center}} + [rn/view {:style {:width (max (:width @state) 63)}} + [quo/network-link @state]]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/network_routing.cljs b/src/status_im/contexts/preview/quo/wallet/network_routing.cljs new file mode 100644 index 00000000000..35440bd66fa --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/network_routing.cljs @@ -0,0 +1,94 @@ +(ns status-im.contexts.preview.quo.wallet.network-routing + (:require [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :number-networks + :type :select + :options [{:key 2} {:key 3} {:key 4} {:key 5}]}]) + +(defn- fake-call-to-get-amounts + [{:keys [new-amount fixed-index current-values on-success]}] + (let [number-networks (count current-values) + amount-difference (- (get current-values fixed-index) new-amount) + difference-distributed (/ amount-difference (dec number-networks)) + new-values (assoc (mapv #(+ % difference-distributed) current-values) + fixed-index + new-amount)] + (js/setTimeout #(on-success new-values) (rand-nth (range 700 5000 250))))) + +(defn preview-internal + [{:keys [total-amount number-networks] :as descriptor-state}] + (let [initial-amount (/ total-amount number-networks) + networks (reagent/atom + [{:amount initial-amount + :max-amount (descriptor-state :max-amount-0) + :network-name :ethereum} + {:amount initial-amount + :max-amount (descriptor-state :max-amount-1) + :network-name :arbitrum} + {:amount initial-amount + :max-amount (descriptor-state :max-amount-2) + :network-name :xDai} + {:amount initial-amount + :max-amount (descriptor-state :max-amount-3) + :network-name :optimism} + {:amount initial-amount + :max-amount (descriptor-state :max-amount-4) + :network-name :polygon}]) + requesting-data? (reagent/atom false)] + (fn [_] + (let [asked-networks (vec (take number-networks @networks)) + on-success-fn (fn [new-network-amounts] + (reset! requesting-data? false) + (swap! networks + #(map (fn [network new-amount] + (assoc network :amount new-amount)) + % + new-network-amounts)))] + [rn/view + [quo/network-routing + {:total-amount total-amount + :networks asked-networks + :requesting-data? @requesting-data? + :on-amount-selected (fn [new-amount selected-idx] + (reset! requesting-data? true) + (fake-call-to-get-amounts + {:new-amount new-amount + :fixed-index selected-idx + :current-values (mapv :amount asked-networks) + :on-success on-success-fn}))}] + (reduce (fn [acc {:keys [amount max-amount network-name]}] + (conj acc + [rn/view + {:style {:flex-direction :row + :margin-vertical 12}} + [rn/view + {:style {:background-color (colors/resolve-color network-name nil) + :width 24 + :height 24 + :margin-right 12}}] + [quo/text + "Max limit: " max-amount " Amount: " (subs (str amount) 0 6)]])) + [rn/view {:style {:margin-vertical 12}} + [quo/text "Total amount: " (reduce + (map :amount asked-networks))]] + asked-networks)])))) + +(defn view + [] + (let [descriptor-state (reagent/atom {:total-amount 400 + :number-networks 4 + :max-amount-0 350 + :max-amount-1 350 + :max-amount-2 300 + :max-amount-3 250 + :max-amount-4 200})] + (fn [] + [preview/preview-container + {:state descriptor-state + :descriptor descriptor + :component-container-style {:margin-vertical 28}} + [preview-internal @descriptor-state]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/progress_bar.cljs b/src/status_im/contexts/preview/quo/wallet/progress_bar.cljs new file mode 100644 index 00000000000..ae82c3ea45c --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/progress_bar.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.preview.quo.wallet.progress-bar + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :state + :type :select + :options [{:key :pending} + {:key :confirmed} + {:key :finalized} + {:key :error}]} + {:key :full-width? + :type :boolean} + {:key :progressed-value + :type :text} + (preview/customization-color-option)]) + +(defn view + [] + (let [state (reagent/atom {:state :pending + :full-width? false + :progressed-value "10" + :customization-color :blue})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-top 40 + :align-items :center}} + [rn/view {:style {:flex-direction :row}} + [quo/progress-bar @state]]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/required_tokens.cljs b/src/status_im/contexts/preview/quo/wallet/required_tokens.cljs new file mode 100644 index 00000000000..7263b34ea53 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/required_tokens.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.preview.quo.wallet.required-tokens + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :type + :type :select + :options [{:key :token} + {:key :collectible}]} + {:key :divider? + :type :boolean}]) + +(def token-descriptor + [{:key :amount + :type :text} + {:key :token + :type :select + :options [{:key "SNT"} + {:key "ETH"}]}]) + +(def collectible-descriptor + [{:key :collectible-name + :type :text}]) + +(defn view + [] + (let + [state + (reagent/atom + {:type :token + :collectible-img-src (resources/mock-images :collectible) + :collectible-name "Collectible name" + :token "SNT" + :amount "100" + :divider? false})] + (fn [] + [preview/preview-container + {:state state + :descriptor (concat descriptor + (case (:type @state) + :token token-descriptor + :collectible collectible-descriptor + nil))} + [quo/required-tokens @state]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/summary_info.cljs b/src/status_im/contexts/preview/quo/wallet/summary_info.cljs new file mode 100644 index 00000000000..91471adc998 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/summary_info.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.preview.quo.wallet.summary-info + (:require + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + + +(def descriptor + [{:key :type + :type :select + :options [{:key :status-account} + {:key :user} + {:key :saved-account} + {:key :account}]}]) + + +(defn view + [] + (let [state (reagent/atom {:type :status-account + :networks-to-show {:ethereum {:amount 150 + :token-symbol "ETH"} + :optimism {:amount 50 + :token-symbol "ETH"} + :arbitrum {:amount 25 + :token-symbol "ETH"}}}) + status-account-props {:customization-color :purple + :size 32 + :emoji "🍑" + :type :default + :name "Collectibles vault" + :address "0x0ah...78b"} + user-props {:full-name "M L" + :status-indicator? false + :size :small + :ring-background (resources/get-mock-image :ring) + :customization-color :blue + :name "Mark Libot" + :address "0x0ah...78b" + :status-account (merge status-account-props {:size 16})}] + (fn [] + (let [account-props (if (= (:type @state) :status-account) status-account-props user-props)] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-horizontal 20}} + [quo/summary-info (merge @state {:account-props account-props})]])))) diff --git a/src/status_im/contexts/preview/quo/wallet/swap_input.cljs b/src/status_im/contexts/preview/quo/wallet/swap_input.cljs new file mode 100644 index 00000000000..ffdcec20227 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/swap_input.cljs @@ -0,0 +1,68 @@ +(ns status-im.contexts.preview.quo.wallet.swap-input + (:require + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :select + :key :type + :options [{:key :pay} + {:key :receive}]} + {:type :select + :key :status + :options [{:key :default} + {:key :disabled} + {:key :loading}]} + {:type :select + :key :value + :options [{:key :token} + {:key :fiat}]} + {:type :boolean + :key :error?} + {:type :boolean + :key :enable-swap?} + {:type :boolean + :key :show-approval-label?} + (preview/customization-color-option)]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:type :pay + :error? false + :token "SNT" + :customization-color :blue + :show-approval-label? false + :enable-swap? true + :status :default + :currency-symbol "€"}) + [value set-value] (rn/use-state "") + on-press (fn [v] (set-value (str value v))) + delete (fn [] (set-value #(subs % 0 (dec (count %)))))] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [quo/swap-input + (assoc state + :on-swap-press #(js/alert "Swap Pressed") + :on-token-press #(js/alert "Token Pressed") + :on-max-press #(js/alert "Max Pressed") + :value value + :fiat-value (str (.toFixed (* value 0.3) 2)) + :container-style {:margin-bottom 20} + :network-tag-props {:title "Max: 200 SNT" + :networks [{:source (resources/get-network :ethereum)}]} + :approval-label-props + {:status :approve + :token-value "10" + :button-props {:on-press + #(js/alert "Approve Pressed")} + :customization-color (:customization-color state) + :token-symbol "SNT"})] + [quo/numbered-keyboard + {:left-action :dot + :delete-key? true + :on-press on-press + :on-delete delete}]])) diff --git a/src/status_im/contexts/preview/quo/wallet/token_input.cljs b/src/status_im/contexts/preview/quo/wallet/token_input.cljs new file mode 100644 index 00000000000..013f5160f75 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/token_input.cljs @@ -0,0 +1,100 @@ +(ns status-im.contexts.preview.quo.wallet.token-input + (:require + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.controlled-input.utils :as controlled-input] + [status-im.contexts.preview.quo.preview :as preview] + [status-im.contexts.wallet.common.utils :as utils] + [utils.money :as money])) + +(def networks + [{:source (resources/get-network :arbitrum)} + {:source (resources/get-network :optimism)} + {:source (resources/get-network :ethereum)}]) + +(def title "Max: 200 SNT") +(def conversion-rate 3450.28) + +(def descriptor + [{:key :token-symbol + :type :select + :options [{:key :eth} + {:key :snt} + {:key :gwei} + {:key :units}]} + {:key :currency + :type :select + :options [{:key "$"} + {:key "€"}]} + {:key :error? + :type :boolean} + {:key :show-token-icon? + :type :boolean} + {:key :swappable? + :type :boolean}]) + + +(defn view + [] + (let [state (reagent/atom {:token-symbol :eth + :currency "$" + :crypto? true + :error? false + :show-token-icon? true + :swappable? true})] + (fn [] + (let [{:keys [currency token-symbol crypto? error?]} @state + [input-state set-input-state] (rn/use-state controlled-input/init-state) + input-amount (controlled-input/input-value input-state) + swap-between-fiat-and-crypto (fn [] + (if crypto? + (set-input-state #(controlled-input/->fiat + % + conversion-rate)) + (set-input-state + #(controlled-input/->crypto + % + conversion-rate)))) + converted-value (if crypto? + (utils/prettify-balance currency + (money/crypto->fiat + input-amount + conversion-rate)) + (utils/prettify-crypto-balance + (or (clj->js token-symbol) "") + (money/fiat->crypto input-amount + conversion-rate) + conversion-rate))] + [preview/preview-container + {:state state + :descriptor descriptor + :full-screen? true + :component-container-style {:flex 1 + :justify-content :space-between}} + [quo/token-input + {:token-symbol token-symbol + :currency-symbol (if crypto? token-symbol currency) + :error? error? + :value input-amount + :converted-value converted-value + :on-swap (fn [] + (swap! state assoc :crypto? (not crypto?)) + (swap-between-fiat-and-crypto)) + :hint-component [quo/network-tags + {:networks networks + :title title + :status (when (:error? @state) :error)}] + :show-token-icon? (:show-token-icon? @state) + :swappable? (:swappable? @state)}] + [quo/numbered-keyboard + {:container-style {:padding-bottom safe-area/top} + :left-action :dot + :delete-key? true + :on-press (fn [c] + (set-input-state #(controlled-input/add-character % c))) + + :on-delete (fn [] + (set-input-state controlled-input/delete-last))}]])))) diff --git a/src/status_im/contexts/preview/quo/wallet/transaction_progress.cljs b/src/status_im/contexts/preview/quo/wallet/transaction_progress.cljs new file mode 100644 index 00000000000..7a98ec98f00 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/transaction_progress.cljs @@ -0,0 +1,158 @@ +(ns status-im.contexts.preview.quo.wallet.transaction-progress + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:type :text + :key :title} + {:type :text + :key :tag-name} + {:type :text + :key :epoch-number-mainnet} + {:type :text + :key :epoch-number-optimism} + {:type :text + :key :epoch-number-arbitrum} + {:type :text + :key :tag-number} + {:type :number + :key :optimism-progress-percentage} + {:type :number + :key :arbitrum-progress-percentage} + {:type :select + :key :network + :options [{:key :mainnet} + {:key :optimism} + {:key :arbitrum} + {:key :optimism-arbitrum}]} + {:type :select + :key :state-mainnet + :options [{:key :pending} + {:key :sending} + {:key :confirmed} + {:key :finalising} + {:key :finalized} + {:key :error}]} + {:type :select + :key :state-optimism + :options [{:key :pending} + {:key :sending} + {:key :confirmed} + {:key :finalising} + {:key :finalized} + {:key :error}]} + {:type :select + :key :state-arbitrum + :options [{:key :pending} + {:key :sending} + {:key :confirmed} + {:key :finalising} + {:key :finalized} + {:key :error}]} + (preview/customization-color-option)]) + +(def total-box 85) +(def counter (reagent/atom 0)) +(def interval-id (reagent/atom nil)) +(def interval-ms 50) + +(defn- stop-interval + [] + (when @interval-id + (js/clearInterval @interval-id) + (reset! interval-id nil))) + +(defn- clear-counter + [] + (reset! counter 0)) + +(defn- update-counter + [state] + (let [new-counter-value (inc @counter)] + (if (or (and (= state :pending) (> new-counter-value 0)) + (and (= state :sending) (> new-counter-value 2)) + (and (= state :confirmed) (> new-counter-value 4)) + (and (= state :finalising) (> new-counter-value 18)) + (and (= state :finalized) (> new-counter-value total-box)) + (and (= state :error) (> new-counter-value 2))) + (stop-interval) + (reset! counter new-counter-value)))) + +(defn- start-interval + [state] + (reset! interval-id + (js/setInterval + (fn [] + (update-counter state)) + interval-ms))) + +(defn- get-networks + [state] + (case (:network state) + :mainnet [{:network :mainnet + :full-name "Ethereum" + :state (:state-mainnet state) + :counter @counter + :total-box total-box + :epoch-number (:epoch-number-mainnet state)}] + :optimism [{:network :optimism + :full-name "Optimism" + :state (:state-optimism state) + :progress (:optimism-progress-percentage state) + :epoch-number (:epoch-number-optimism state)}] + :arbitrum [{:network :arbitrum + :full-name "Arbitrum" + :state (:state-arbitrum state) + :progress (:arbitrum-progress-percentage state) + :epoch-number (:epoch-number-arbitrum state)}] + :optimism-arbitrum [{:network :optimism + :full-name "Optimism" + :state (:state-optimism state) + :progress (:optimism-progress-percentage state) + :epoch-number (:epoch-number-optimism state)} + {:network :arbitrum + :full-name "Arbitrum" + :state (:state-arbitrum state) + :progress (:arbitrum-progress-percentage state) + :epoch-number (:epoch-number-arbitrum state)}])) + +(defn- f-view + [state] + (fn [] + (rn/use-effect + (fn [] + (start-interval (:state-mainnet @state)) + (clear-counter) + (fn [] + (stop-interval))) + [(:state-mainnet @state)]) + [preview/preview-container {:state state :descriptor descriptor} + [quo/transaction-progress + (assoc @state + :networks + (get-networks @state))]])) + +(defn view + [] + (let [state (reagent/atom + {:title "Title" + :tag-name "Doodle" + :tag-number "120" + :epoch-number-mainnet "181,329" + :epoch-number-optimism "181,329" + :epoch-number-arbitrum "181,329" + :optimism-progress-percentage 10 + :arbitrum-progress-percentage 10 + :network :mainnet + :state-mainnet :pending + :state-arbitrum :pending + :state-optimism :pending + :customization-color :blue + :tag-photo (resources/get-mock-image :collectible) + :on-press (fn [] + (js/alert "Transaction progress item pressed"))})] + [:f> f-view state])) diff --git a/src/status_im/contexts/preview/quo/wallet/transaction_summary.cljs b/src/status_im/contexts/preview/quo/wallet/transaction_summary.cljs new file mode 100644 index 00000000000..d709de26903 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/transaction_summary.cljs @@ -0,0 +1,224 @@ +(ns status-im.contexts.preview.quo.wallet.transaction-summary + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def asset-snt + {:size 24 + :type :token + :token "SNT" + :amount 1500}) + +(def asset-eth + {:size 24 + :type :token + :token "ETH" + :amount 75}) + +(def asset-dai + {:size 24 + :type :token + :token "DAI" + :amount 2400}) + +(def asset-collectible + {:size 24 + :type :collectible + :collectible (resources/mock-images :collectible) + :collectible-name "Collectible" + :collectible-number "123"}) + +(def trip-to-vegas + {:size 24 + :type :account + :account-name "Trip to Vegas" + :emoji "🤑"}) + +(def piggy-bank + {:size 24 + :type :account + :account-name "Piggy bank" + :emoji "🐷"}) + +(def collectibles-vault + {:size 24 + :type :account + :account-name "Collectibles vault" + :customization-color :orange + :emoji "🎮"}) + +(def aretha-gosling + {:size 24 + :type :default + :full-name "Aretha Gosling" + :profile-picture (resources/mock-images :user-picture-female2)}) + +(def james-bond + {:size 24 + :type :default + :full-name "James Bond" + :profile-picture (resources/mock-images :user-picture-male4)}) + +(def mark-libot + {:size 24 + :type :default + :full-name "Mark Libot" + :profile-picture (resources/mock-images :user-picture-male4)}) + +(def mainnet + {:size 24 + :type :network + :network-logo (quo.resources/get-network :ethereum) + :network-name "Ethereum"}) + +(def optimism + {:size 24 + :type :network + :network-logo (quo.resources/get-network :optimism) + :network-name "Optimism"}) + +(def arbitrum + {:size 24 + :type :network + :network-logo (quo.resources/get-network :arbitrum) + :network-name "Arbitrum"}) + +(def multinetwork + {:size 24 + :type :multinetwork + :networks [(quo.resources/get-network :ethereum) + (quo.resources/get-network :arbitrum) + (quo.resources/get-network :optimism)]}) + +(def moonpay + {:size 24 + :type :network + :network-logo (quo.resources/get-network :ethereum) + :network-name "Moonpay"}) + +(def binance + {:size 24 + :type :network + :network-logo (quo.resources/get-network :ethereum) + :network-name "Binance"}) + +(def context-tags + [{:key asset-snt + :value "SNT"} + {:key asset-eth + :value "ETH"} + {:key asset-dai + :value "UNK"} + {:key asset-collectible + :value "Collectible"} + {:key trip-to-vegas + :value "Account: Trip to Vegas"} + {:key piggy-bank + :value "Account: Piggy bank"} + {:key collectibles-vault + :value "Account: Collectibles vault"} + {:key aretha-gosling + :value "Person: Aretha Gosling"} + {:key mark-libot + :value "Person: Mark Libot"} + {:key james-bond + :value "Person: James Bond"} + {:key mainnet + :value "Network: Mainnet"} + {:key optimism + :value "Network: Optimism"} + {:key arbitrum + :value "Network: Arbitrum"} + {:key multinetwork + :value "Network: Multinetwork"} + {:key moonpay + :value "Market: Moonpay"} + {:key binance + :value "Market: Binance"}]) + +(def prefixes + [{:key :t/to + :value "To"} + {:key :t/in + :value "In"} + {:key :t/via + :value "Via"} + {:key :t/from + :value "From"} + {:key :t/on + :value "On"} + {:key :t/at + :value "At"}]) + +(def descriptor + (concat + [{:key :transaction + :type :select + :options [{:key :send} + {:key :swap} + {:key :bridge}]} + {:label "Slot 1" + :key :first-tag + :type :select + :options context-tags} + {:label "Slot 2 prefix" + :key :second-tag-prefix + :type :select + :options prefixes} + {:label "Slot 2" + :key :second-tag + :type :select + :options context-tags} + {:label "Slot 3 prefix" + :key :third-tag-prefix + :type :select + :options prefixes} + {:label "Slot 3" + :key :third-tag + :type :select + :options context-tags} + {:label "Slot 4 prefix" + :key :fourth-tag-prefix + :type :select + :options prefixes} + {:label "Slot 4" + :key :fourth-tag + :type :select + :options context-tags} + {:label "Slot 5" + :key :fifth-tag + :type :select + :options context-tags} + {:key :max-fees + :type :text} + {:key :nonce + :type :number} + {:key :input-data + :type :text}])) + +(defn view + [] + (let [component-state (reagent/atom {:transaction :send + :first-tag asset-snt + :second-tag-prefix :t/from + :second-tag piggy-bank + :third-tag-prefix nil + :third-tag aretha-gosling + :fourth-tag-prefix :t/via + :fourth-tag mainnet + :fifth-tag optimism + :max-fees "€55.57" + :nonce 26 + :input-data "Hello from Porto"})] + (fn [] + [preview/preview-container + {:state component-state + :descriptor descriptor + :show-blur-background? true + :component-container-style {:padding-bottom 50}} + [quo/transaction-summary + (merge {:on-press #(js/alert "Dropdown pressed")} + @component-state)]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/wallet_activity.cljs b/src/status_im/contexts/preview/quo/wallet/wallet_activity.cljs new file mode 100644 index 00000000000..41431a1deb5 --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/wallet_activity.cljs @@ -0,0 +1,227 @@ +(ns status-im.contexts.preview.quo.wallet.wallet-activity + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.preview.quo.preview :as preview])) + +(def asset-snt + {:size 24 + :type :token + :token "SNT" + :amount 1500}) + +(def asset-dai + {:size 24 + :type :token + :token "DAI" + :amount 2400}) + +(def asset-collectible + {:size 24 + :type :collectible + :collectible (resources/mock-images :collectible) + :collectible-name "Collectible" + :collectible-number "123"}) + +(def trip-to-vegas + {:size 24 + :type :account + :account-name "Trip to Vegas" + :emoji "🤑"}) + +(def piggy-bank + {:size 24 + :type :account + :account-name "Piggy bank" + :emoji "🐷"}) + +(def aretha-gosling + {:size 24 + :type :default + :full-name "Aretha Gosling" + :profile-picture (resources/mock-images :user-picture-female2)}) + +(def jessica-stewart + {:size 24 + :type :default + :full-name "Jessica Stewart" + :profile-picture (resources/mock-images :user-picture-female2)}) + +(def james-bond + {:size 24 + :type :default + :full-name "James Bond" + :profile-picture (resources/mock-images :user-picture-male4)}) + +(def mainnet + {:size 24 + :type :network + :network-logo (quo.resources/get-network :ethereum) + :network-name "Ethereum"}) + +(def optimism + {:size 24 + :type :network + :network-logo (quo.resources/get-network :optimism) + :network-name "Optimism"}) + +(def arbitrum + {:size 24 + :type :network + :network-logo (quo.resources/get-network :arbitrum) + :network-name "Arbitrum"}) + +(def multinetwork + {:size 24 + :type :multinetwork + :networks [(quo.resources/get-network :ethereum) + (quo.resources/get-network :arbitrum) + (quo.resources/get-network :optimism)]}) + +(def moonpay + {:size 24 + :type :network + :network-logo (quo.resources/get-network :ethereum) + :network-name "Moonpay"}) + +(def binance + {:size 24 + :type :network + :network-logo (quo.resources/get-network :ethereum) + :network-name "Binance"}) + +(def context-tags + [{:key asset-snt + :value "SNT"} + {:key asset-dai + :value "UNK"} + {:key asset-collectible + :value "Collectible"} + {:key trip-to-vegas + :value "Account: Trip to Vegas"} + {:key piggy-bank + :value "Account: Piggy bank"} + {:key aretha-gosling + :value "Person: Aretha Gosling"} + {:key jessica-stewart + :value "Person: Jessica Stewart"} + {:key james-bond + :value "Person: James Bond"} + {:key mainnet + :value "Network: Mainnet"} + {:key optimism + :value "Network: Optimism"} + {:key arbitrum + :value "Network: Arbitrum"} + {:key multinetwork + :value "Network: Multinetwork"} + {:key moonpay + :value "Market: Moonpay"} + {:key binance + :value "Market: Binance"}]) + +(def prefixes + [{:key :t/to + :value "To"} + {:key :t/in + :value "In"} + {:key :t/via + :value "Via"} + {:key :t/from + :value "From"} + {:key :t/on + :value "On"} + {:key :t/at + :value "At"}]) + +(def descriptor + + (concat + [{:key :blur? + :type :select + :options [{:key true} + {:key false}]} + {:key :transaction + :type :select + :options [{:key :receive} + {:key :send} + {:key :swap} + {:key :bridge} + {:key :buy} + {:key :destroy} + {:key :mint} + {:key :approval}]} + {:key :status + :type :select + :options [{:key :pending} + {:key :confirmed} + {:key :finalised} + {:key :failed}]} + {:key :timestamp + :type :text} + {:key :counter + :type :select + :options [{:key 1 + :value 1} + {:key 2 + :value 2} + {:key 3 + :value 3}]} + + {:label "Slot 1" + :key :first-tag + :type :select + :options context-tags} + {:label "Slot 2 prefix" + :key :second-tag-prefix + :type :select + :options prefixes} + {:label "Slot 2" + :key :second-tag + :type :select + :options context-tags} + {:label "Slot 3 prefix" + :key :third-tag-prefix + :type :select + :options prefixes} + {:label "Slot 3" + :key :third-tag + :type :select + :options context-tags} + {:label "Slot 4 prefix" + :key :fourth-tag-prefix + :type :select + :options prefixes} + {:label "Slot 4" + :key :fourth-tag + :type :select + :options context-tags}])) + +(defn view + [] + (let [component-state (reagent/atom {:transaction :send + :timestamp "Today 22:20" + :status :pending + :counter 1 + :first-tag asset-snt + :second-tag-prefix :t/from + :second-tag piggy-bank + :third-tag-prefix :t/to + :third-tag aretha-gosling + :fourth-tag-prefix :t/via + :fourth-tag mainnet + :blur? false})] + (fn [] + [preview/preview-container + {:state component-state + :descriptor descriptor + :blur? (:blur? @component-state) + :show-blur-background? true} + [rn/view {:style {:flex 1}} + [quo/wallet-activity + (merge {:on-press #(js/alert "Activity pressed") + :on-press-options #(js/alert "Options pressed")} + @component-state)]]]))) diff --git a/src/status_im/contexts/preview/quo/wallet/wallet_overview.cljs b/src/status_im/contexts/preview/quo/wallet/wallet_overview.cljs new file mode 100644 index 00000000000..bdcb338388a --- /dev/null +++ b/src/status_im/contexts/preview/quo/wallet/wallet_overview.cljs @@ -0,0 +1,60 @@ +(ns status-im.contexts.preview.quo.wallet.wallet-overview + (:require + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :state + :type :select + :options [{:key :loading} + {:key :default}]} + {:key :time-frame + :type :select + :options [{:key :none} + {:key :selected} + {:key :one-week + :value "1 Week"} + {:key :one-month + :value "1 Month"} + {:key :three-months + :value "3 Months"} + {:key :one-year + :value "1 Year"} + {:key :all-time + :value "All time"} + {:key :custom}]} + {:key :metrics + :type :select + :options [{:key :none} + {:key :positive} + {:key :negative}]}]) + +(def ^:private networks-list + [{:source (quo.resources/get-network :ethereum)} + {:source (quo.resources/get-network :optimism)} + {:source (quo.resources/get-network :arbitrum)}]) + +(defn view + [] + (let [state (reagent/atom {:state :default + :time-frame :one-week + :metrics :positive + :balance "€0.00" + :date "20 Nov 2023" + :begin-date "16 May" + :end-date "25 May" + :currency-change "€0.00" + :percentage-change "0.00%"})] + (fn [] + [preview/preview-container + {:state state + :descriptor descriptor + :component-container-style {:padding-vertical 60 + :flex-direction :row + :justify-content :center}} + [quo/wallet-overview + (assoc @state + :networks networks-list + :dropdown-on-press #(js/alert "On pressed dropdown"))]]))) diff --git a/src/status_im/contexts/preview/status_im/banners/alert_banner.cljs b/src/status_im/contexts/preview/status_im/banners/alert_banner.cljs new file mode 100644 index 00000000000..5fac9295bc8 --- /dev/null +++ b/src/status_im/contexts/preview/status_im/banners/alert_banner.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.preview.status-im.banners.alert-banner + (:require + [reagent.core :as reagent] + [status-im.contexts.preview.quo.preview :as preview] + [utils.re-frame :as rf])) + +(def descriptor + [{:key :alert-banner? + :type :boolean} + {:key :alert-message + :type :text} + {:key :error-banner? + :type :boolean} + {:key :error-message + :type :text}]) + +(defn view + [] + (let [state (reagent/atom {:alert-banner? false + :alert-message "Testnet mode enabled" + :error-banner? false + :error-message "Main and failover JSON RPC URLs offline"}) + alert-banner? (reagent/cursor state [:alert-banner?]) + alert-message (reagent/cursor state [:alert-message]) + error-banner? (reagent/cursor state [:error-banner?]) + error-message (reagent/cursor state [:error-message])] + (fn [] + (if @alert-banner? + (rf/dispatch [:alert-banners/add + {:type :alert + :text @alert-message}]) + (rf/dispatch [:alert-banners/remove :alert])) + (if @error-banner? + (rf/dispatch [:alert-banners/add + {:type :error + :text @error-message}]) + (rf/dispatch [:alert-banners/remove :error])) + [preview/preview-container {:state state :descriptor descriptor}]))) diff --git a/src/status_im/contexts/preview/status_im/common/floating_button_page/style.cljs b/src/status_im/contexts/preview/status_im/common/floating_button_page/style.cljs new file mode 100644 index 00000000000..c73abc6575b --- /dev/null +++ b/src/status_im/contexts/preview/status_im/common/floating_button_page/style.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.preview.status-im.common.floating-button-page.style + (:require [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(defn container + [] + {:flex 1 + :margin-top safe-area/top}) + +(def background-image + {:position :absolute + :top (- safe-area/top) + :left 0 + :right 0 + :bottom 200}) + +(defn page-content + [height] + {:flex 1 + :height height + :overflow :hidden + :background-color (colors/resolve-color :army 30)}) diff --git a/src/status_im/contexts/preview/status_im/common/floating_button_page/view.cljs b/src/status_im/contexts/preview/status_im/common/floating_button_page/view.cljs new file mode 100644 index 00000000000..2632ac6a49d --- /dev/null +++ b/src/status_im/contexts/preview/status_im/common/floating_button_page/view.cljs @@ -0,0 +1,63 @@ +(ns status-im.contexts.preview.status-im.common.floating-button-page.view + (:require [quo.core :as quo] + [re-frame.core :as rf] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.common.resources :as resources] + [status-im.contexts.preview.status-im.common.floating-button-page.style :as + style])) + +(defn view + [] + (let [content-height (reagent/atom 450) + gradient-cover? (reagent/atom false) + slide? (reagent/atom false)] + (fn [] + [rn/view {:style (style/container)} + (when-not @slide? + [rn/image + {:style style/background-image + :source (resources/get-mock-image :dark-blur-bg)}]) + [floating-button-page/view + {:header [quo/page-nav + {:type :title-description + :title "floating button page" + :description "press right icon to swap button type" + :text-align :left + :right-side [{:icon-name :i/transaction + :on-press #(swap! slide? not)}] + :background :blur + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + :footer (if @slide? + [quo/slide-button + {:track-text "We gotta slide" + :track-icon :face-id + :container-style {:z-index 2} + :customization-color :blue + :on-complete #(js/alert "button slid")} + "Save address"] + [quo/button + {:container-style {:z-index 2} + :on-press #(js/alert "button pressed")} + "Save address"]) + :gradient-cover? @gradient-cover? + :customization-color :orange} + [rn/view {:style (style/page-content @content-height)} + [quo/text {:size :heading-1} "Page Content"] + [quo/input + {:auto-focus true + :value ""}] + [quo/button + {:type :outline + :on-press #(swap! content-height (fn [v] (+ v 10)))} + "increase height"] + [quo/button + {:type :outline + :on-press #(swap! content-height (fn [v] (- v 10)))} + "decrease height"] + [quo/button + {:type :outline + :on-press #(swap! gradient-cover? not)} + "gradient cover?"]]]]))) diff --git a/src/status_im/contexts/preview/status_im/main.cljs b/src/status_im/contexts/preview/status_im/main.cljs new file mode 100644 index 00000000000..d8c44cf977e --- /dev/null +++ b/src/status_im/contexts/preview/status_im/main.cljs @@ -0,0 +1,66 @@ +(ns status-im.contexts.preview.status-im.main + (:refer-clojure :exclude [filter]) + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.preview.quo.common :as common] + [status-im.contexts.preview.status-im.banners.alert-banner :as alert-banner] + [status-im.contexts.preview.status-im.common.floating-button-page.view :as + floating-button-page] + [status-im.contexts.preview.status-im.style :as style] + [utils.re-frame :as rf])) + +(def screens-categories + {:common [{:name :screen/floating-button-page + :component floating-button-page/view}] + :banner + [{:name :screen/alert-banner-preview + :component alert-banner/view}]}) + +(defn- category-view + [] + (let [open? (reagent/atom false) + on-press #(swap! open? not)] + (fn [category] + [rn/view {:style {:margin-vertical 8}} + [quo/dropdown + {:type :grey + :state (if @open? :active :default) + :on-press on-press} + (name (key category))] + (when @open? + (for [{category-name :name} (val category)] + ^{:key category-name} + [quo/button + {:type :outline + :container-style {:margin-vertical 8} + :on-press #(rf/dispatch [:navigate-to category-name])} + (name category-name)]))]))) + +(defn- main-screen + [] + (let [theme (quo.context/use-theme)] + [:<> + [common/navigation-bar {:title "Status IM components"}] + [rn/scroll-view {:style (style/main theme)} + (for [category (sort screens-categories)] + ^{:key (first category)} + [category-view category])]])) + +(def screens + (->> screens-categories + (map val) + flatten + (map (fn [subcategory] + (update-in subcategory + [:options :topBar] + merge + {:visible false}))))) + +(def main-screens + [{:name :screen/status-im-preview + :options {:topBar {:visible false} + :insets {:top? true}} + :component main-screen}]) diff --git a/src/status_im/contexts/preview/status_im/style.cljs b/src/status_im/contexts/preview/status_im/style.cljs new file mode 100644 index 00000000000..195ca9296f8 --- /dev/null +++ b/src/status_im/contexts/preview/status_im/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.preview.status-im.style + (:require + [quo.foundations.colors :as colors])) + +(defn main + [theme] + {:flex 1 + :padding-bottom 8 + :padding-horizontal 16 + :background-color (colors/theme-colors colors/white colors/neutral-90 theme)}) diff --git a/src/status_im/contexts/profile/backup_recovery_phrase/style.cljs b/src/status_im/contexts/profile/backup_recovery_phrase/style.cljs new file mode 100644 index 00000000000..742ba6bf0d6 --- /dev/null +++ b/src/status_im/contexts/profile/backup_recovery_phrase/style.cljs @@ -0,0 +1,82 @@ +(ns status-im.contexts.profile.backup-recovery-phrase.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(defn seed-phrase-container + [theme shell?] + {:padding-horizontal 12 + :border-width (if shell? 0 1) + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme) + :border-radius 16 + :background-color (if shell? + colors/white-opa-10 + (colors/theme-colors colors/white colors/neutral-80-opa-40 theme)) + :flex-direction :row}) + +(def word-item + {:align-items :center + :flex-direction :row + :column-gap 4}) + +(defn separator + [theme shell?] + {:margin-vertical 12 + :margin-horizontal 12.5 + :border-width (when platform/ios? 1) + :border-right-width (when platform/android? 1) + :border-color (if shell? + colors/white-opa-10 + (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)) + :border-style :dashed}) + +(def step-item + {:flex-direction :row + :padding-vertical 8 + :align-items :center + :column-gap 12}) + +(def blur-container + {:border-radius 16 + :overflow :hidden + :margin 1}) + +(defn blur + [theme] + {:style {:flex 1} + :blur-radius 25 + :blur-type theme + :blur-amount 20}) + +(def slide-button + {:position :absolute + :bottom 12 + :left 0 + :right 0}) + +(def container {:flex 1}) + +(def seed-phrase {:margin-horizontal 20}) + +(def words-colum + {:flex 1 + :row-gap 12 + :padding-vertical 8}) + +(defn seed-phrase-blur-overlay + [shell? theme] + (cond-> {:flex 1} + ;; Variant for android + shell because we have no blur, so we provide an opaque color + (and platform/android? shell?) (assoc :background-color colors/neutral-60) + (and platform/ios? shell?) (assoc :background-color colors/white-opa-5) + (and platform/ios? (not shell?)) (assoc :background-color + (colors/theme-colors colors/white-70-blur + colors/neutral-95-opa-70 + theme)))) + +(def instructions + {:padding-horizontal 20 + :padding-top 20}) + +(def instructions-header + {:margin-bottom 8}) diff --git a/src/status_im/contexts/profile/backup_recovery_phrase/view.cljs b/src/status_im/contexts/profile/backup_recovery_phrase/view.cljs new file mode 100644 index 00000000000..5d8bcd73869 --- /dev/null +++ b/src/status_im/contexts/profile/backup_recovery_phrase/view.cljs @@ -0,0 +1,168 @@ +(ns status-im.contexts.profile.backup-recovery-phrase.view + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.contexts.profile.backup-recovery-phrase.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(def ^:private step-labels + [:t/backup-step-1 :t/backup-step-2 :t/backup-step-3 :t/backup-step-4]) + +(defn- header + [{:keys [back-button? shell?]}] + [:<> + [quo/page-nav + (cond-> {:icon-name (if back-button? :i/arrow-left :i/close) + :on-press events-helper/navigate-back + :accessibility-label :top-bar} + shell? (assoc :background :blur))] + [quo/page-top + {:title (i18n/label :t/backup-recovery-phrase) + :description :text + :description-text (i18n/label :t/backup-recovery-phrase-description)}]]) + +(defn- word-item + [{:keys [index text]}] + [rn/view {:style style/word-item} + [quo/counter {:type :grey :blur? true} + index] + [quo/text text]]) + +(defn words-column + [{:keys [starting-index words]}] + [rn/view {:style style/words-colum} + (map-indexed (fn [idx word] + (let [word-number (+ starting-index idx)] + (with-meta [word-item {:index word-number :text word}] + {:key word-number}))) + words)]) + +(defn- seed-phrase-words + [{:keys [seed-phrase theme shell?]}] + (let [[first-column second-column] (split-at 6 seed-phrase)] + [rn/view {:style (style/seed-phrase-container theme shell?)} + [words-column + {:starting-index 1 + :words first-column}] + [rn/view {:style (style/separator theme shell?)}] + [words-column + {:starting-index 7 + :words second-column}]])) + +(defn- seed-phrase-container + [{:keys [shell? seed-phrase revealed?]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/seed-phrase} + [seed-phrase-words + {:seed-phrase seed-phrase + :theme theme + :shell? shell?}] + (when-not revealed? + [rn/view {:style [rn/stylesheet-absolute-fill style/blur-container]} + [quo/blur + {:style (style/seed-phrase-blur-overlay shell? theme) + :blur-type :transparent + :blur-amount 25}]])])) + +(defn- step-item + [{:keys [step-idx toggle-step-check shell?]}] + (let [step-text (get step-labels step-idx) + on-press #(toggle-step-check step-idx)] + (fn [{:keys [checked?]}] + (let [customization-color (rf/sub [:profile/customization-color])] + [rn/pressable + {:style style/step-item + :hit-slop {:top 8 :bottom 8} + :on-press on-press} + [quo/selectors + {:type :checkbox + :checked? checked? + :customization-color customization-color + :on-change on-press + :blur? (when shell? true)}] + [quo/text (i18n/label step-text)]])))) + +(defn- instructions + [{:keys [step-checks toggle-step-check shell?]}] + [rn/view {:style style/instructions} + [quo/text + {:style style/instructions-header + :weight :semi-bold} + (i18n/label :t/how-to-backup)] + (map-indexed (fn [idx step-label] + ^{:key (str step-label)} + [step-item + {:step-idx idx + :toggle-step-check toggle-step-check + :checked? (get step-checks idx) + :shell? shell?}]) + step-labels)]) + +(defn bottom-button + [{:keys [step-checks revealed? customization-color shell? on-success reveal-seed-phrase]}] + (let [text (i18n/label (if revealed? :t/i-have-written :t/reveal-phrase)) + disabled? (and (some false? step-checks) (not revealed?)) + on-press (if revealed? on-success reveal-seed-phrase)] + [quo/bottom-actions + {:container-style {:margin-top :auto} + :actions :one-action + :blur? shell? + :button-one-label text + :button-one-props {:disabled? disabled? + :customization-color customization-color + :on-press on-press} + :description (when revealed? :bottom) + :description-text (when revealed? (i18n/label :t/next-you-will))}])) + +(defn view + [] + (let [{:keys [on-success masked-seed-phrase shell? back-button?] + initial-revealed? :revealed?} (quo.context/use-screen-params) + [revealed? set-revealed] (rn/use-state initial-revealed?) + reveal-seed-phrase (rn/use-callback #(set-revealed true)) + [seed-phrase set-seed-phrase] (rn/use-state (or (some-> masked-seed-phrase + (security/safe-unmask-data) + (string/split #"\s")) + (vec (repeat 12 "-")))) + on-complete (rn/use-callback + #(on-success (security/mask-data seed-phrase)) + [seed-phrase]) + [step-checks set-step-checks] (rn/use-state (-> step-labels count (repeat false) vec)) + toggle-step-check (rn/use-callback + (fn [idx] + (set-step-checks #(update % idx not)))) + customization-color (rf/sub [:profile/customization-color])] + (rn/use-mount + (fn [] + (when-not masked-seed-phrase + (native-module/get-random-mnemonic + (fn [new-phrase] + (set-seed-phrase (string/split new-phrase #"\s"))))))) + [quo/overlay + {:type (when shell? :shell) + :insets? true} + [header + {:back-button? back-button? + :shell? shell?}] + [seed-phrase-container + {:shell? shell? + :seed-phrase seed-phrase + :revealed? revealed?}] + (when-not revealed? + [instructions + {:step-checks step-checks + :toggle-step-check toggle-step-check + :shell? shell?}]) + [bottom-button + {:step-checks step-checks + :revealed? revealed? + :customization-color customization-color + :shell? shell? + :on-success on-complete + :reveal-seed-phrase reveal-seed-phrase}]])) diff --git a/src/status_im/contexts/profile/config.cljs b/src/status_im/contexts/profile/config.cljs new file mode 100644 index 00000000000..6cae878f3b1 --- /dev/null +++ b/src/status_im/contexts/profile/config.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.profile.config + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [status-im.config :as config] + [utils.transforms :as transforms])) + +(defn login + [] + {;; Temporary fix until https://github.com/status-im/status-go/issues/3024 is resolved + :wakuV2Nameserver "8.8.8.8" + :kdfIterations 3200 + :ethRpcProxyUser config/STATUS_BUILD_ETH_RPC_PROXY_USER + :ethRpcProxyPassword config/STATUS_BUILD_ETH_RPC_PROXY_PASSWORD + :ethRpcProxyUrl config/STATUS_BUILD_ETH_RPC_PROXY_URL + :statusProxyEnabled config/status-proxy-enabled? + :statusProxyStageName config/status-proxy-stage-name + :statusProxyMarketUser config/STATUS_BUILD_PROXY_USER + :statusProxyMarketPassword config/STATUS_BUILD_PROXY_PASSWORD + :statusProxyBlockchainUser config/STATUS_BUILD_PROXY_USER + :statusProxyBlockchainPassword config/STATUS_BUILD_PROXY_PASSWORD + :marketDataProxyUrl config/STATUS_MARKET_DATA_PROXY_URL + :marketDataProxyUser config/STATUS_MARKET_DATA_PROXY_USER + :marketDataProxyPassword config/STATUS_MARKET_DATA_PROXY_PASSWORD + :openseaAPIKey config/opensea-api-key + :poktToken config/POKT_TOKEN + :infuraToken config/INFURA_TOKEN + :raribleMainnetAPIKey config/RARIBLE_MAINNET_API_KEY + :raribleTestnetAPIKey config/RARIBLE_TESTNET_API_KEY + :alchemyEthereumMainnetToken config/ALCHEMY_ETHEREUM_MAINNET_TOKEN + :alchemyEthereumSepoliaToken config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN + :alchemyOptimismMainnetToken config/ALCHEMY_OPTIMISM_MAINNET_TOKEN + :alchemyOptimismSepoliaToken config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN + :alchemyArbitrumMainnetToken config/ALCHEMY_ARBITRUM_MAINNET_TOKEN + :alchemyArbitrumSepoliaToken config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN + :alchemyBaseMainnetToken config/ALCHEMY_BASE_MAINNET_TOKEN + :alchemyBaseSepoliaToken config/ALCHEMY_BASE_SEPOLIA_TOKEN}) + +(defn- common-config + [] + {:verifyTransactionURL config/verify-transaction-url + :verifyENSURL config/verify-ens-url + :verifyENSContractAddress config/verify-ens-contract-address + :verifyTransactionChainID config/verify-transaction-chain-id + :wakuV2LightClient true + :wakuV2EnableMissingMessageVerification true + :wakuV2EnableStoreConfirmationForMessagesSent false}) + +(defn fix-node-config-migration + [] + (common-config)) + +(defn create + [] + (let [log-enabled? (boolean (not-empty (config/log-level)))] + (merge + (login) + (common-config) + {:deviceName (native-module/get-installation-name) + :rootDataDir (native-module/backup-disabled-data-dir) + :rootKeystoreDir (native-module/keystore-dir) + :logLevel (when log-enabled? (config/log-level)) + :logEnabled log-enabled? + :logFilePath (native-module/log-file-directory config/use-public-log-dir?) + :wakuV2Fleet config/fleet + :previewPrivacy config/blank-preview? + :testNetworksEnabled config/test-networks-enabled?}))) + +(defn strip-file-prefix + [path] + (when path + (string/replace-first path "file://" ""))) + +(re-frame/reg-event-fx :profile.config/get-node-config-callback + (fn [{:keys [db]} [node-config-json]] + (let [node-config (transforms/json->clj node-config-json)] + {:db (assoc-in db + [:profile/profile :wakuv2-config] + (get node-config :WakuV2Config))}))) + +(re-frame/reg-fx :profile.config/get-node-config + (fn [] + (native-module/get-node-config + #(re-frame/dispatch [:profile.config/get-node-config-callback %])))) diff --git a/src/status_im/contexts/profile/contact/actions/view.cljs b/src/status_im/contexts/profile/contact/actions/view.cljs new file mode 100644 index 00000000000..808a158a8f2 --- /dev/null +++ b/src/status_im/contexts/profile/contact/actions/view.cljs @@ -0,0 +1,107 @@ +(ns status-im.contexts.profile.contact.actions.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.not-implemented :as not-implemented] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.profile.contact.add-nickname.view :as add-nickname] + [status-im.contexts.profile.contact.block-contact.view :as block-contact] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-add-nickname + [] + (rf/dispatch [:show-bottom-sheet + {:content + (fn [] [add-nickname/view])}])) + +(defn on-block-contact + [] + (rf/dispatch [:show-bottom-sheet + {:content + (fn [] [block-contact/view])}])) + +(defn view + [] + (let [{:keys [nickname public-key contact-request-state blocked?] + :as contact} (rf/sub [:contacts/current-contact]) + full-name (profile.utils/displayed-name contact) + on-remove-nickname (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:toasts/upsert + {:id :remove-nickname + :type :positive + :text (i18n/label :t/nickname-removed)}]) + (rf/dispatch [:contacts/update-nickname public-key ""])) + [public-key]) + on-show-qr (rn/use-callback + (fn [] + (rf/dispatch [:universal-links/generate-profile-url + {:public-key public-key + :on-success #(rf/dispatch [:open-modal + :screen/share-contact])}])) + [public-key]) + has-nickname? (rn/use-memo (fn [] (not (string/blank? nickname))) [nickname]) + on-share-profile (rn/use-callback + (fn [] + (rf/dispatch [:universal-links/generate-profile-url + {:public-key public-key + :on-success #(rf/dispatch [:open-share + {:options {:message %}}])}])) + [public-key]) + on-remove-contact (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:toasts/upsert + {:id :remove-contact + :type :positive + :text (->> (i18n/label :t/removed-from-contacts) + (string/lower-case) + (str full-name " "))}]) + (rf/dispatch [:contact.ui/remove-contact-pressed contact])) + [public-key full-name])] + [quo/action-drawer + [(concat + [{:icon :i/edit + :label (if has-nickname? + (i18n/label :t/edit-nickname) + (i18n/label :t/add-nickname-title)) + :on-press on-add-nickname + :accessibility-label (if nickname :edit-nickname :add-nickname)}] + (when-not blocked? + [{:icon :i/qr-code + :label (i18n/label :t/show-qr) + :on-press on-show-qr + :accessibility-label :show-qr-code} + {:icon :i/share + :label (i18n/label :t/share-profile) + :on-press on-share-profile + :accessibility-label :share-profile} + (when has-nickname? + {:icon :i/delete + :label (i18n/label :t/remove-nickname) + :on-press on-remove-nickname + :add-divider? true + :accessibility-label :remove-nickname + :danger? true}) + (when config/show-not-implemented-features? + {:icon :i/untrustworthy + :label (i18n/label :t/mark-untrustworthy) + :on-press not-implemented/alert + :accessibility-label :mark-untrustworthy + :add-divider? (when-not has-nickname? true) + :danger? true}) + (when (= constants/contact-request-state-mutual contact-request-state) + {:icon :i/remove-user + :label (i18n/label :t/remove-contact) + :on-press on-remove-contact + :accessibility-label :remove-contact + :danger? true}) + {:icon :i/block + :label (i18n/label :t/block-user) + :on-press on-block-contact + :accessibility-label :block-user + :danger? true}]))]])) diff --git a/src/status_im/contexts/profile/contact/add_nickname/style.cljs b/src/status_im/contexts/profile/contact/add_nickname/style.cljs new file mode 100644 index 00000000000..53ed0fb9bc6 --- /dev/null +++ b/src/status_im/contexts/profile/contact/add_nickname/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.profile.contact.add-nickname.style) + +(def input-wrapper + {:padding-vertical 8 + :padding-horizontal 20 + :gap 8}) diff --git a/src/status_im/contexts/profile/contact/add_nickname/view.cljs b/src/status_im/contexts/profile/contact/add_nickname/view.cljs new file mode 100644 index 00000000000..dc4f0dfff39 --- /dev/null +++ b/src/status_im/contexts/profile/contact/add_nickname/view.cljs @@ -0,0 +1,89 @@ +(ns status-im.contexts.profile.contact.add-nickname.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.validation.profile :as profile-validator] + [status-im.constants :as constants] + [status-im.contexts.profile.contact.add-nickname.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [{:keys [public-key primary-name nickname customization-color] + :as profile} (rf/sub [:contacts/current-contact]) + customization-color customization-color + full-name (profile.utils/displayed-name profile) + profile-picture (profile.utils/photo profile) + [unsaved-nickname set-unsaved-nickname] (rn/use-state nickname) + [error-msg set-error-msg] (rn/use-state nil) + [typing? set-typing?] (rn/use-state false) + has-nickname? (rn/use-memo (fn [] (not (string/blank? nickname))) + [nickname]) + validate-nickname (rn/use-callback + (debounce/debounce + (fn [name] + (set-error-msg + (profile-validator/validation-nickname name)) + (set-typing? false)) + 300)) + on-cancel (rn/use-callback #(rf/dispatch [:hide-bottom-sheet])) + on-nickname-change (rn/use-callback (fn [text] + (set-typing? true) + (set-unsaved-nickname text) + (validate-nickname text))) + on-nickname-submit (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:toasts/upsert + {:id :add-nickname + :type :positive + :text (i18n/label + (if (string/blank? nickname) + :t/nickname-added + :t/nickname-updated) + {:primary-name primary-name})}]) + (rf/dispatch [:contacts/update-nickname public-key + (string/trim unsaved-nickname)])) + [public-key unsaved-nickname])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :default + :title (if has-nickname? + (i18n/label :t/edit-nickname) + (i18n/label :t/add-nickname-title)) + :full-name full-name + :profile-picture profile-picture + :customization-color customization-color}] + [rn/view {:style style/input-wrapper} + [quo/input + {:type :text + :char-limit constants/profile-name-max-length + :max-length constants/profile-name-max-length + :auto-focus true + :default-value unsaved-nickname + :error? (not (string/blank? error-msg)) + :label (i18n/label :t/nickname) + :on-change-text on-nickname-change + :on-submit-editing on-nickname-submit}] + [quo/info-message + {:icon :i/info + :size :default + :status (if-not (string/blank? error-msg) :error :default)} + (if-not (string/blank? error-msg) + error-msg + (i18n/label :t/nickname-visible-to-you))]] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label (if has-nickname? :t/update-nickname-title :t/add-nickname-title)) + :button-one-props {:disabled? (or typing? + (string/blank? unsaved-nickname) + (not (string/blank? error-msg))) + :customization-color customization-color + :on-press on-nickname-submit} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press on-cancel}}]])) diff --git a/src/status_im/contexts/profile/contact/block_contact/style.cljs b/src/status_im/contexts/profile/contact/block_contact/style.cljs new file mode 100644 index 00000000000..069de20b114 --- /dev/null +++ b/src/status_im/contexts/profile/contact/block_contact/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.profile.contact.block-contact.style) + +(def content-wrapper + {:padding-vertical 8 + :padding-horizontal 20 + :gap 12}) + +(def checkbox-wrapper + {:flex-direction :row + :align-items :center + :gap 8}) diff --git a/src/status_im/contexts/profile/contact/block_contact/view.cljs b/src/status_im/contexts/profile/contact/block_contact/view.cljs new file mode 100644 index 00000000000..75e5142ce46 --- /dev/null +++ b/src/status_im/contexts/profile/contact/block_contact/view.cljs @@ -0,0 +1,68 @@ +(ns status-im.contexts.profile.contact.block-contact.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.profile.contact.block-contact.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-close [] (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [] + (let [{:keys [customization-color contact-request-state public-key] + :as contact} (rf/sub [:contacts/current-contact]) + customization-color customization-color + full-name (profile.utils/displayed-name contact) + profile-picture (profile.utils/photo contact) + on-block-press (rn/use-callback + (fn [] + (rf/dispatch [:toasts/upsert + {:id :user-blocked + :type :positive + :text (i18n/label :t/user-blocked + {:username + full-name})}]) + (rf/dispatch [:contact/block-contact + public-key]) + (rf/dispatch [:contact.ui/remove-contact-pressed + {:public-key public-key}]) + (on-close)) + [public-key full-name])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :default + :title (i18n/label :t/block) + :full-name full-name + :profile-picture profile-picture + :customization-color customization-color}] + [rn/view {:style style/content-wrapper} + [quo/text + {:weight :medium + :size :paragraph-1} + (i18n/label :t/block-user-title-message {:username full-name})] + [quo/information-box + {:icon :i/info + :type :default} + (i18n/label :t/blocking-a-user-message {:username full-name})] + (when (= constants/contact-request-state-mutual contact-request-state) + [rn/view + {:style style/checkbox-wrapper} + [quo/selectors + {:type :checkbox + :customization-color customization-color + :checked? true + :disabled? true}] + [quo/text (i18n/label :t/remove-contact)]])] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/block) + :button-one-props {:type :danger + :accessibility-label :block-contact + :on-press on-block-press} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :accessibility-label :cancel + :on-press on-close}}]])) diff --git a/src/status_im/contexts/profile/contact/contact_request/style.cljs b/src/status_im/contexts/profile/contact/contact_request/style.cljs new file mode 100644 index 00000000000..b28684443b6 --- /dev/null +++ b/src/status_im/contexts/profile/contact/contact_request/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.profile.contact.contact-request.style) + +(def message-prompt-wrapper + {:padding-top 4 + :padding-bottom 8 + :padding-horizontal 20}) + +(def message-input-wrapper + {:padding-vertical 8 + :flex-shrink 1 + :padding-horizontal 20}) diff --git a/src/status_im/contexts/profile/contact/contact_request/view.cljs b/src/status_im/contexts/profile/contact/contact_request/view.cljs new file mode 100644 index 00000000000..deed16ef3bf --- /dev/null +++ b/src/status_im/contexts/profile/contact/contact_request/view.cljs @@ -0,0 +1,72 @@ +(ns status-im.contexts.profile.contact.contact-request.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.constants :as constants] + [status-im.contexts.profile.contact.contact-request.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [{:keys [public-key customization-color] + :as profile} (rf/sub [:contacts/current-contact]) + customization-color customization-color + full-name (profile.utils/displayed-name profile) + profile-picture (profile.utils/photo profile) + input-ref (rn/use-ref-atom nil) + [message set-message] (rn/use-state "") + on-message-change (rn/use-callback #(set-message %)) + on-message-submit (rn/use-callback (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:contact.ui/send-contact-request + public-key message]) + (rf/dispatch [:toasts/upsert + {:id :send-contact-request + :type :positive + :text (i18n/label + :t/contact-request-was-sent)}])) + [public-key message])] + (rn/use-mount + (fn [] + (let [listener (.addListener rn/keyboard + "keyboardDidHide" + (fn [_event] + (when (and platform/android? @input-ref) + (.blur ^js @input-ref))))] + #(.remove ^js listener)))) + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :default + :title (i18n/label :t/send-contact-request) + :full-name full-name + :profile-picture profile-picture + :customization-color customization-color}] + [quo/text {:style style/message-prompt-wrapper} + (i18n/label :t/contact-request-message-prompt)] + [rn/view {:style style/message-input-wrapper} + [quo/input + {:type :text + :ref #(reset! input-ref %) + :multiline? true + :char-limit constants/contact-request-message-max-length + :max-length constants/contact-request-message-max-length + :placeholder (i18n/label :t/type-something) + :auto-focus true + :accessibility-label :contact-request-message + :label (i18n/label :t/message) + :on-change-text on-message-change + :container-style {:flex-shrink 1} + :input-container-style {:flex-shrink 1}}]] + [quo/bottom-actions + {:container-style {:style {:flex 1}} + :actions :one-action + :button-one-props {:disabled? (string/blank? message) + :accessibility-label :send-contact-request + :customization-color customization-color + :on-press on-message-submit} + :button-one-label (i18n/label :t/send-contact-request)}]])) + diff --git a/src/status_im/contexts/profile/contact/contact_review/style.cljs b/src/status_im/contexts/profile/contact/contact_review/style.cljs new file mode 100644 index 00000000000..c8c6a878b73 --- /dev/null +++ b/src/status_im/contexts/profile/contact/contact_review/style.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.profile.contact.contact-review.style) + +(def message-input-wrapper + {:padding-vertical 8 + :padding-horizontal 20}) + +(def bottom-actions-wrapper + {:padding-vertical 4}) diff --git a/src/status_im/contexts/profile/contact/contact_review/view.cljs b/src/status_im/contexts/profile/contact/contact_review/view.cljs new file mode 100644 index 00000000000..7051032eb8a --- /dev/null +++ b/src/status_im/contexts/profile/contact/contact_review/view.cljs @@ -0,0 +1,62 @@ +(ns status-im.contexts.profile.contact.contact-review.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.profile.contact.contact-review.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [{:keys [public-key customization-color] + :as profile} (rf/sub [:contacts/current-contact]) + {contact-request-id :id + :keys [message]} (rf/sub [:activity-center/pending-contact-request-from-contact-id + public-key]) + ;; TODO(@seanstrom): https://github.com/status-im/status-mobile/issues/18733 + customization-color (or customization-color constants/profile-default-color) + full-name (profile.utils/displayed-name profile) + profile-picture (profile.utils/photo profile) + on-contact-accept (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:activity-center.contact-requests/accept + contact-request-id]) + (rf/dispatch [:toasts/upsert + {:id :accept-contact-request + :type :positive + :text (i18n/label + :t/contact-request-was-accepted)}])) + [contact-request-id]) + on-contact-ignore (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:activity-center.contact-requests/decline + contact-request-id]) + (rf/dispatch [:toasts/upsert + {:id :ignore-contact-request + :type :positive + :text (i18n/label + :t/contact-request-was-ignored)}])) + [contact-request-id])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :default + :title (i18n/label :t/contact-request-review) + :full-name full-name + :profile-picture profile-picture + :customization-color customization-color}] + [rn/view {:style style/message-input-wrapper} + [quo/locked-input {} + (-> message :content :text)]] + [rn/view {:style style/bottom-actions-wrapper} + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/accept) + :button-one-props {:type :positive + :on-press on-contact-accept} + :button-two-props {:type :danger + :on-press on-contact-ignore} + :button-two-label (i18n/label :t/ignore)}]]])) diff --git a/src/status_im/contexts/profile/contact/header/style.cljs b/src/status_im/contexts/profile/contact/header/style.cljs new file mode 100644 index 00000000000..b452604ab9b --- /dev/null +++ b/src/status_im/contexts/profile/contact/header/style.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.profile.contact.header.style + (:require [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(def avatar-wrapper + {:margin-top -40 + :padding-left 20 + :align-items :flex-start}) + +(def button-wrapper + {:margin-top 8 + :margin-bottom 16 + :padding-horizontal 20}) + +(def username-wrapper + {:margin-top 12 + :padding-horizontal 20}) + +(defn header-container + [border-radius theme margin-top] + (reanimated/apply-animations-to-style + {:border-top-left-radius border-radius + :border-top-right-radius border-radius} + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :padding-horizontal 20 + :margin-top margin-top})) + +(def status-tag-wrapper + {:flex-direction :row + :padding-top 12 + :padding-right 12}) + +(def header-top-wrapper + {:flex-direction :row + :justify-content :space-between}) diff --git a/src/status_im/contexts/profile/contact/header/view.cljs b/src/status_im/contexts/profile/contact/header/view.cljs new file mode 100644 index 00000000000..462ee764b7f --- /dev/null +++ b/src/status_im/contexts/profile/contact/header/view.cljs @@ -0,0 +1,121 @@ +(ns status-im.contexts.profile.contact.header.view + (:require [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.not-implemented] + [status-im.common.scalable-avatar.view :as avatar] + [status-im.constants :as constants] + [status-im.contexts.profile.contact.contact-request.view :as contact-request] + [status-im.contexts.profile.contact.contact-review.view :as contact-review] + [status-im.contexts.profile.contact.header.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-send-contact-request + [] + (let [main-event [:show-bottom-sheet {:content contact-request/view}]] + (rf/dispatch [:profile/check-profile-update-prompt main-event]))) + +(defn on-contact-review + [] + (rf/dispatch [:show-bottom-sheet {:content contact-review/view}])) + +(defn view + [{:keys [scroll-y]}] + (let [{:keys [public-key customization-color ens-name nickname secondary-name + emoji-hash bio blocked? contact-request-state] + :as contact} (rf/sub [:contacts/current-contact]) + customization-color (or customization-color constants/profile-default-color) + full-name (profile.utils/displayed-name contact) + profile-picture (profile.utils/photo contact) + online? (rf/sub [:visibility-status-updates/online? public-key]) + theme (quo.context/use-theme) + contact-status (rn/use-memo + (fn [] + (cond + (= contact-request-state + constants/contact-request-state-mutual) :contact + blocked? :blocked + :else nil)) + [blocked? contact-request-state]) + on-start-chat (rn/use-callback + #(rf/dispatch [:chat.ui/start-chat public-key ens-name]) + [ens-name public-key]) + on-unblock-press (rn/use-callback + (fn [] + (rf/dispatch [:contact/unblock-contact + public-key]) + (rf/dispatch [:toasts/upsert + {:id :user-unblocked + :type :positive + :text (i18n/label :t/user-unblocked + {:username full-name})}])) + [public-key full-name])] + [rn/view {:style style/header-container} + [rn/view {:style style/header-top-wrapper} + [rn/view {:style style/avatar-wrapper} + [avatar/view + {:scroll-y scroll-y + :full-name full-name + :online? online? + :profile-picture profile-picture + :border-color (colors/theme-colors colors/white colors/neutral-95 theme) + :customization-color customization-color}]] + (when (= contact-request-state constants/contact-request-state-sent) + [rn/view {:style style/status-tag-wrapper} + [quo/status-tag + {:label (i18n/label :t/contact-profile-request-pending) + :status {:type :pending} + :size :large}]])] + [rn/view {:style style/username-wrapper} + [quo/username + {:name-type (if-not (string/blank? nickname) :nickname :default) + :accessibility-label :contact-name + :username full-name + :status contact-status + :name secondary-name}]] + [quo/page-top + {:description :text + :description-text (when-not blocked? bio) + :emoji-dash emoji-hash}] + + (when blocked? + [quo/button + {:container-style style/button-wrapper + :on-press on-unblock-press + :customization-color customization-color + :icon-left :i/block} + (i18n/label :t/unblock)]) + + (cond + (and (not blocked?) + (or (not contact-request-state) + (= contact-request-state constants/contact-request-state-none) + (= contact-request-state constants/contact-request-state-dismissed))) + [quo/button + {:container-style style/button-wrapper + :on-press on-send-contact-request + :customization-color customization-color + :icon-left :i/add-user} + (i18n/label :t/send-contact-request)] + + (= contact-request-state constants/contact-request-state-received) + [quo/button + {:container-style style/button-wrapper + :on-press on-contact-review + :customization-color customization-color + :icon-left :i/add-user} + (i18n/label :t/contact-request-review)] + + (= contact-request-state constants/contact-request-state-mutual) + [quo/button + {:container-style style/button-wrapper + :on-press on-start-chat + :customization-color customization-color + :icon-left :i/messages} + (i18n/label :t/send-message)] + + :else nil)])) diff --git a/src/status_im/contexts/profile/contact/share/style.cljs b/src/status_im/contexts/profile/contact/share/style.cljs new file mode 100644 index 00000000000..5ca5597b8a9 --- /dev/null +++ b/src/status_im/contexts/profile/contact/share/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.profile.contact.share.style) + +(def header-heading + {:padding-top 2 + :padding-bottom 1}) + +(def qr-code-container + {:margin-horizontal 20 + :margin-top 8}) diff --git a/src/status_im/contexts/profile/contact/share/view.cljs b/src/status_im/contexts/profile/contact/share/view.cljs new file mode 100644 index 00000000000..0eb09a48bd3 --- /dev/null +++ b/src/status_im/contexts/profile/contact/share/view.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.profile.contact.share.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.qr-codes.view :as qr-codes] + [status-im.contexts.profile.contact.share.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.address :as address] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [{:keys [universal-profile-url customization-color] + :as profile} (rf/sub [:contacts/current-contact]) + abbreviated-url (rn/use-memo + (fn [] + (address/get-abbreviated-profile-url universal-profile-url)) + [universal-profile-url]) + on-share-press (rn/use-callback + #(rf/dispatch [:open-share + {:options {:message universal-profile-url}}]) + [universal-profile-url]) + on-copy-press (rn/use-callback + (fn [] + (rf/dispatch + [:share/copy-text-and-show-toast + {:text-to-copy universal-profile-url + :post-copy-message (i18n/label :t/link-to-profile-copied)}])) + [universal-profile-url])] + [quo/overlay {:type :shell} + [rn/view + {:style {:padding-top safe-area/top} + :key :share-community} + [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back + :background :blur + :accessibility-label :top-bar}] + [quo/page-top + {:container-style style/header-heading + :title (i18n/label :t/share-profile)}] + [rn/view {:style style/qr-code-container} + [qr-codes/share-qr-code + {:type :profile + :qr-data universal-profile-url + :qr-data-label-shown abbreviated-url + :on-share-press on-share-press + :on-text-press on-copy-press + :on-text-long-press on-copy-press + :profile-picture (profile.utils/photo profile) + :full-name (profile.utils/displayed-name profile) + :customization-color (or customization-color :blue)}]]]])) diff --git a/src/status_im/contexts/profile/contact/unblock_contact/view.cljs b/src/status_im/contexts/profile/contact/unblock_contact/view.cljs new file mode 100644 index 00000000000..bf4999c2276 --- /dev/null +++ b/src/status_im/contexts/profile/contact/unblock_contact/view.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.profile.contact.unblock-contact.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.profile.contact.block-contact.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-close [] (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [contact] + (let [{:keys [customization-color + public-key]} contact + full-name (profile.utils/displayed-name contact) + profile-picture (profile.utils/photo contact) + on-unblock-press (rn/use-callback + (fn [] + (rf/dispatch [:toasts/upsert + {:id :user-unblocked + :type :positive + :theme :dark + :text (i18n/label :t/user-unblocked + {:username + full-name})}]) + (rf/dispatch [:contact/unblock-contact + public-key]) + (on-close)) + [public-key full-name])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :default + :title (i18n/label :t/unblock) + :full-name full-name + :profile-picture profile-picture + :customization-color customization-color}] + [rn/view {:style style/content-wrapper} + [quo/text + {:weight :medium + :size :paragraph-1} + (i18n/label :t/unblocking-a-user-message {:username full-name})]] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/unblock) + :button-one-props {:type :danger + :accessibility-label :block-contact + :on-press on-unblock-press} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :accessibility-label :cancel + :on-press on-close}}]])) diff --git a/src/status_im/contexts/profile/contact/view.cljs b/src/status_im/contexts/profile/contact/view.cljs new file mode 100644 index 00000000000..bed0c9485d8 --- /dev/null +++ b/src/status_im/contexts/profile/contact/view.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.profile.contact.view + (:require [quo.context] + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated] + [status-im.common.scroll-page.view :as scroll-page] + [status-im.contexts.profile.contact.actions.view :as actions] + [status-im.contexts.profile.contact.header.view :as contact-header] + [utils.re-frame :as rf])) + +(defn on-show-actions + [] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [actions/view])}])) + +(defn view + [] + (let [{:keys [customization-color]} (rf/sub [:contacts/current-contact]) + scroll-y (reanimated/use-shared-value 0) + theme (quo.context/use-theme)] + [:<> + [scroll-page/scroll-page + {:navigate-back? true + :height 148 + :on-scroll #(reanimated/set-shared-value scroll-y %) + :cover-color (colors/resolve-color customization-color + theme + 20) + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :page-nav-props {:right-side [{:icon-name :i/options + :on-press on-show-actions + :accessibility-label :contact-actions}]}} + [contact-header/view {:scroll-y scroll-y}]]])) diff --git a/src/status_im/contexts/profile/create/effects.cljs b/src/status_im/contexts/profile/create/effects.cljs new file mode 100644 index 00000000000..fbb8f53911a --- /dev/null +++ b/src/status_im/contexts/profile/create/effects.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.profile.create.effects + (:require + [native-module.core :as native-module] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.profile/create-and-login + (fn [request] + ;;"node.login" signal will be triggered as a callback + (native-module/create-account-and-login request))) diff --git a/src/status_im/contexts/profile/create/events.cljs b/src/status_im/contexts/profile/create/events.cljs new file mode 100644 index 00000000000..1d46a26689b --- /dev/null +++ b/src/status_im/contexts/profile/create/events.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.profile.create.events + (:require + [native-module.core :as native-module] + [status-im.contexts.profile.config :as profile.config] + status-im.contexts.profile.create.effects + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :profile.create/create-and-login + (fn [{:keys [db]} [{:keys [display-name password image-path color]}]] + (let [login-sha3-password (native-module/sha3 (security/safe-unmask-data password))] + {:db (assoc-in db [:syncing :login-sha3-password] login-sha3-password) + :fx [[:effects.profile/create-and-login + (assoc (profile.config/create) + :displayName display-name + :password login-sha3-password + :imagePath (profile.config/strip-file-prefix image-path) + :customizationColor color)]]}))) diff --git a/src/status_im/contexts/profile/data_store.cljs b/src/status_im/contexts/profile/data_store.cljs new file mode 100644 index 00000000000..9436b39d7cc --- /dev/null +++ b/src/status_im/contexts/profile/data_store.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.profile.data-store) + +(defn accepted-terms? + [accounts] + (some :hasAcceptedTerms accounts)) + +(defn recently-opened-profile + [profiles] + (first (sort-by :timestamp > (vals profiles)))) diff --git a/src/status_im/contexts/profile/db.cljs b/src/status_im/contexts/profile/db.cljs new file mode 100644 index 00000000000..9c1f958d0f2 --- /dev/null +++ b/src/status_im/contexts/profile/db.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.profile.db) + +(defn testnet? + [db] + (boolean (get-in db [:profile/profile :test-networks-enabled?]))) diff --git a/src/status_im/contexts/profile/edit/accent_colour/events.cljs b/src/status_im/contexts/profile/edit/accent_colour/events.cljs new file mode 100644 index 00000000000..ef7b5197a6b --- /dev/null +++ b/src/status_im/contexts/profile/edit/accent_colour/events.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.profile.edit.accent-colour.events + (:require [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :profile/edit-accent-colour-success + (fn [_ [{:keys [customization-color navigate-back? show-toast?]}]] + {:fx [[:dispatch [:profile/save-local-accent-color customization-color]] + (when navigate-back? + [:dispatch [:navigate-back]]) + (when show-toast? + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/accent-colour-updated)}]])]})) + +(rf/reg-event-fx :profile/save-local-accent-color + (fn [{:keys [db]} [customization-color]] + {:db (assoc-in db [:profile/profile :customization-color] customization-color)})) + +(defn edit-accent-colour + [{:keys [db]} + [{:keys [color navigate-back? show-toast?] + :or {navigate-back? true + show-toast? true}}]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:json-rpc/call + [{:method "wakuext_setCustomizationColor" + :params [{:customizationColor color + :keyUid key-uid}] + :on-success [:profile/edit-accent-colour-success + {:customization-color color + :navigate-back? navigate-back? + :show-toast? show-toast?}] + :on-error #(log/error "failed to edit accent color." {:error %})}]]]})) + +(rf/reg-event-fx :profile/edit-accent-colour edit-accent-colour) diff --git a/src/status_im/contexts/profile/edit/accent_colour/events_test.cljs b/src/status_im/contexts/profile/edit/accent_colour/events_test.cljs new file mode 100644 index 00000000000..0c042442556 --- /dev/null +++ b/src/status_im/contexts/profile/edit/accent_colour/events_test.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.profile.edit.accent-colour.events-test + (:require [cljs.test :refer [deftest is]] + matcher-combinators.test + [status-im.contexts.profile.edit.accent-colour.events :as sut])) + +(deftest edit-accent-color-test + (let [new-color :yellow + key-uid "key-uid" + cofx {:db {:profile/profile {:key-uid key-uid}}} + expected {:fx [[:json-rpc/call + [{:method "wakuext_setCustomizationColor" + :params [{:customizationColor new-color + :keyUid key-uid}] + :on-success [:profile/edit-accent-colour-success + {:customization-color new-color + :navigate-back? true + :show-toast? true}] + :on-error fn?}]]]}] + (is (match? expected + (sut/edit-accent-colour cofx [{:color new-color}]))))) diff --git a/src/status_im/contexts/profile/edit/accent_colour/style.cljs b/src/status_im/contexts/profile/edit/accent_colour/style.cljs new file mode 100644 index 00000000000..571bf77eb2f --- /dev/null +++ b/src/status_im/contexts/profile/edit/accent_colour/style.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.profile.edit.accent-colour.style + (:require [quo.foundations.colors :as colors])) + +(defn page-wrapper + [insets] + {:padding-top (:top insets) + :padding-bottom (:bottom insets) + :padding-horizontal 1 + :flex 1}) + +(def screen-container + {:flex 1 + :padding-top 14 + :justify-content :space-between}) + +(def padding-horizontal + {:padding-horizontal 20}) + +(def color-title + {:color colors/white-70-blur + :margin-top 20 + :margin-bottom 16}) + +(def button-wrapper + {:margin-vertical 12 + :padding-horizontal 20}) + +(def profile-card + {:margin-top 22 + :margin-bottom 5}) diff --git a/src/status_im/contexts/profile/edit/accent_colour/view.cljs b/src/status_im/contexts/profile/edit/accent_colour/view.cljs new file mode 100644 index 00000000000..f3dd9042706 --- /dev/null +++ b/src/status_im/contexts/profile/edit/accent_colour/view.cljs @@ -0,0 +1,67 @@ +(ns status-im.contexts.profile.edit.accent-colour.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.contexts.profile.edit.accent-colour.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn f-view + [{:keys [customization-color]}] + (let [{window-width :width} (rn/get-window) + unsaved-custom-color (reagent/atom customization-color constants/profile-default-color) + on-change #(reset! unsaved-custom-color %)] + (fn [{:keys [customization-color]}] + (let [profile (rf/sub [:profile/profile-with-image + {:customization-color @unsaved-custom-color}]) + profile-picture (profile.utils/photo profile) + display-name (profile.utils/displayed-name profile)] + (rn/use-effect + #(on-change customization-color) + [customization-color]) + [quo/overlay + {:type :shell + :container-style (style/page-wrapper safe-area/insets)} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back])}] + [rn/view + {:key :content + :style style/screen-container} + [rn/view {:style {:flex 1}} + [rn/view {:style style/padding-horizontal} + [quo/text-combinations {:title (i18n/label :t/accent-colour)}] + [quo/profile-card + {:profile-picture profile-picture + :name display-name + :card-style style/profile-card + :customization-color @unsaved-custom-color}] + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/color-title} + (i18n/label :t/accent-colour)]] + [quo/color-picker + {:blur? true + :default-selected @unsaved-custom-color + :on-change on-change + :container-style style/padding-horizontal + :window-width window-width}]] + [rn/view {:style style/button-wrapper} + [quo/button + {:type :primary + :customization-color @unsaved-custom-color + :on-press (fn [] + (rf/dispatch [:profile/edit-accent-colour + {:color @unsaved-custom-color}]))} + (i18n/label :t/save-colour)]]]])))) + +(defn view + [] + (let [customization-color (rf/sub [:profile/customization-color])] + [:f> f-view {:customization-color customization-color}])) diff --git a/src/status_im/contexts/profile/edit/bio/events.cljs b/src/status_im/contexts/profile/edit/bio/events.cljs new file mode 100644 index 00000000000..0285cc8cac6 --- /dev/null +++ b/src/status_im/contexts/profile/edit/bio/events.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.profile.edit.bio.events + (:require [clojure.string :as string] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :profile/edit-profile-bio-success + (fn [_ [{:keys [bio added?]}]] + {:fx [[:dispatch [:profile/save-local-profile-bio bio]] + [:dispatch [:navigate-back]] + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (if added? + (i18n/label :t/bio-added) + (i18n/label :t/bio-updated))}]]]})) + +(rf/reg-event-fx :profile/save-local-profile-bio + (fn [{:keys [db]} [bio]] + {:db (assoc-in db [:profile/profile :bio] bio)})) + +(defn edit-profile-bio + [{:keys [db]} [new-bio]] + (let [{:keys [bio]} (:profile/profile db)] + {:fx [[:json-rpc/call + [{:method "wakuext_setBio" + :params [new-bio] + :on-success [:profile/edit-profile-bio-success + {:bio new-bio + :added? (string/blank? bio)}] + :on-error #(log/error "failed to set bio " %)}]]]})) + +(rf/reg-event-fx :profile/edit-bio edit-profile-bio) diff --git a/src/status_im/contexts/profile/edit/bio/events_test.cljs b/src/status_im/contexts/profile/edit/bio/events_test.cljs new file mode 100644 index 00000000000..1ce173e51e3 --- /dev/null +++ b/src/status_im/contexts/profile/edit/bio/events_test.cljs @@ -0,0 +1,16 @@ +(ns status-im.contexts.profile.edit.bio.events-test + (:require [cljs.test :refer [deftest is]] + matcher-combinators.test + [status-im.contexts.profile.edit.bio.events :as sut])) + +(deftest edit-bio-test + (let [new-bio "New Bio text" + cofx {:db {:profile/profile {:bio "Old Bio text"}}} + expected {:fx [[:json-rpc/call + [{:method "wakuext_setBio" + :params [new-bio] + :on-success [:profile/edit-profile-bio-success + {:bio new-bio + :added? false}]}]]]}] + (is (match? expected + (sut/edit-profile-bio cofx [new-bio]))))) diff --git a/src/status_im/contexts/profile/edit/bio/style.cljs b/src/status_im/contexts/profile/edit/bio/style.cljs new file mode 100644 index 00000000000..ab1b92cc9d3 --- /dev/null +++ b/src/status_im/contexts/profile/edit/bio/style.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.profile.edit.bio.style) + +(defn page-wrapper + [insets] + {:padding-top (:top insets) + :padding-bottom (:bottom insets) + :padding-horizontal 1 + :flex 1}) + +(def screen-container + {:flex 1 + :padding-top 14 + :padding-horizontal 20 + :justify-content :space-between}) + +(def button-wrapper + {:margin-vertical 12}) diff --git a/src/status_im/contexts/profile/edit/bio/view.cljs b/src/status_im/contexts/profile/edit/bio/view.cljs new file mode 100644 index 00000000000..a79796f8059 --- /dev/null +++ b/src/status_im/contexts/profile/edit/bio/view.cljs @@ -0,0 +1,74 @@ +(ns status-im.contexts.profile.edit.bio.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.common.validation.profile :as profile-validator] + [status-im.constants :as constants] + [status-im.contexts.profile.edit.bio.style :as style] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile (rf/sub [:profile/profile-with-image]) + customization-color (rf/sub [:profile/customization-color]) + alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + current-bio (:bio profile) + [bio set-bio] (rn/use-state current-bio) + [error-msg set-error-msg] (rn/use-state nil) + [typing? set-typing] (rn/use-state false) + validate-bio (debounce/debounce (fn [bio] + (set-error-msg (profile-validator/validation-bio + bio)) + (set-typing false)) + 300) + on-change-text (fn [s] + (set-typing true) + (set-bio s) + (validate-bio s))] + [quo/overlay + {:type :shell + :container-style (style/page-wrapper safe-area/insets)} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back])}] + [rn/keyboard-avoiding-view + {:key :content + :keyboard-vertical-offset (if platform/ios? alert-banners-top-margin 0) + :style style/screen-container} + [rn/view {:style {:gap 22}} + [quo/text-combinations {:title (i18n/label :t/bio)}] + [quo/input + {:blur? true + :multiline? true + :error? (not (string/blank? error-msg)) + :container-style {:margin-bottom -11} + :default-value bio + :auto-focus true + :max-height 200 + :char-limit constants/profile-bio-max-length + :label (i18n/label :t/profile-bio) + :placeholder (i18n/label :t/something-about-you) + :on-change-text on-change-text}] + (when-not (string/blank? error-msg) + [quo/info-message + {:status :error + :size :default + :icon :i/info} + error-msg])] + [rn/view {:style style/button-wrapper} + [quo/button + {:type :primary + :customization-color customization-color + :on-press (fn [] + (rf/dispatch [:profile/edit-bio bio])) + :disabled? (boolean (or typing? + (and (string/blank? current-bio) + (string/blank? bio)) + (not (string/blank? error-msg))))} + (i18n/label :t/save-bio)]]]])) diff --git a/src/status_im/contexts/profile/edit/header/events.cljs b/src/status_im/contexts/profile/edit/header/events.cljs new file mode 100644 index 00000000000..5082d416b39 --- /dev/null +++ b/src/status_im/contexts/profile/edit/header/events.cljs @@ -0,0 +1,61 @@ +(ns status-im.contexts.profile.edit.header.events + (:require [clojure.string :as string] + [status-im.common.avatar-picture-picker.view :as profile-picture-picker] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :profile/update-local-picture + (fn [{:keys [db]} [images]] + {:db (if images + (assoc-in db [:profile/profile :images] images) + (update db :profile/profile dissoc :images))})) + +(rf/reg-event-fx :profile/edit-profile-picture-success + (fn [{db :db} [{:keys [show-toast?]} images]] + (let [has-picture? (-> db :profile/profile :images count pos?)] + {:fx [[:dispatch [:profile/update-local-picture (reverse images)]] + (when show-toast? + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label (if has-picture? + :t/profile-picture-updated + :t/profile-picture-added))}]])]}))) + +(defn edit-profile-picture + [{:keys [db]} + [{:keys [picture crop-width crop-height show-toast?] + :or {show-toast? true}}]] + (let [key-uid (get-in db [:profile/profile :key-uid]) + crop-width (or crop-width profile-picture-picker/crop-size) + crop-height (or crop-height profile-picture-picker/crop-size) + path (string/replace-first picture #"file://" "")] + {:fx [[:json-rpc/call + [{:method "multiaccounts_storeIdentityImage" + :params [key-uid path 0 0 crop-width crop-height] + :on-success [:profile/edit-profile-picture-success {:show-toast? show-toast?}]}]]]})) + +(rf/reg-event-fx :profile/edit-picture edit-profile-picture) + +(rf/reg-event-fx :profile/delete-profile-picture-success + (fn [_ [{:keys [show-toast?]}]] + {:fx [[:dispatch [:profile/update-local-picture nil]] + (when show-toast? + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/profile-picture-removed)}]])]})) + +(defn delete-profile-picture + [{:keys [db]} + [{:keys [show-toast?] + :or {show-toast? true}}]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:json-rpc/call + [{:method "multiaccounts_deleteIdentityImage" + :params [key-uid] + :on-success [:profile/delete-profile-picture-success {:show-toast? show-toast?}]}]]]})) + +(rf/reg-event-fx :profile/delete-picture delete-profile-picture) diff --git a/src/status_im/contexts/profile/edit/header/events_test.cljs b/src/status_im/contexts/profile/edit/header/events_test.cljs new file mode 100644 index 00000000000..e250542dc7f --- /dev/null +++ b/src/status_im/contexts/profile/edit/header/events_test.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.profile.edit.header.events-test + (:require [cljs.test :refer [deftest is]] + matcher-combinators.test + [status-im.common.avatar-picture-picker.view :as profile-picture-picker] + [status-im.contexts.profile.edit.header.events :as sut])) + +(deftest edit-picture-test + (let [picture "new-picture" + key-uid "key-uid" + cofx {:db {:profile/profile {:key-uid key-uid}}} + expected {:fx [[:json-rpc/call + [{:method "multiaccounts_storeIdentityImage" + :params [key-uid picture 0 0 profile-picture-picker/crop-size + profile-picture-picker/crop-size] + :on-success [:profile/edit-profile-picture-success {:show-toast? true}]}]]]}] + (is (match? expected + (sut/edit-profile-picture cofx [{:picture picture}]))))) + +(deftest delete-picture-test + (let [key-uid "key-uid" + cofx {:db {:profile/profile {:key-uid key-uid}}} + expected {:fx [[:json-rpc/call + [{:method "multiaccounts_deleteIdentityImage" + :params [key-uid] + :on-success [:profile/delete-profile-picture-success {:show-toast? true}]}]]]}] + (is (match? expected + (sut/delete-profile-picture cofx [{}]))))) diff --git a/src/status_im/contexts/profile/edit/header/view.cljs b/src/status_im/contexts/profile/edit/header/view.cljs new file mode 100644 index 00000000000..b7e4a203c0b --- /dev/null +++ b/src/status_im/contexts/profile/edit/header/view.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.profile.edit.header.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.avatar-picture-picker.view :as profile-picture-picker] + [status-im.contexts.profile.edit.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile (rf/sub [:profile/profile-with-image]) + customization-color (rf/sub [:profile/customization-color]) + full-name (profile.utils/displayed-name profile) + profile-picture (profile.utils/photo profile) + has-picture? (rf/sub [:profile/has-picture]) + on-change-profile-pic (fn [picture] + (if picture + (rf/dispatch [:profile/edit-picture {:picture picture}]) + (rf/dispatch [:profile/delete-picture])))] + [rn/view + {:key :edit-profile + :style style/screen-container} + [quo/text-combinations {:title (i18n/label :t/edit-profile)}] + [rn/view style/avatar-wrapper + [quo/user-avatar + {:full-name full-name + :profile-picture profile-picture + :customization-color customization-color + :status-indicator? false + :size :big}] + [quo/button + {:on-press (fn [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [profile-picture-picker/view + {:on-result on-change-profile-pic + :has-picture? has-picture?}]) + :theme :dark + :shell? true}])) + :container-style style/camera-button + :icon-only? true + :type :grey + :background :photo + :size 32} + :i/camera]]])) diff --git a/src/status_im/contexts/profile/edit/introduce_yourself/view.cljs b/src/status_im/contexts/profile/edit/introduce_yourself/view.cljs new file mode 100644 index 00000000000..573d8cdf458 --- /dev/null +++ b/src/status_im/contexts/profile/edit/introduce_yourself/view.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.profile.edit.introduce-yourself.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn sheet + [{:keys [pending-event]}] + (let [on-accept (fn [] + (rf/dispatch [:open-modal :screen/profile.edit-profile-modal + {:pending-event pending-event}])) + on-reject #(rf/dispatch pending-event)] + [:<> + [rn/view {:style {:padding-horizontal 20}} + [quo/text + {:style {:padding-bottom 4} + :size :heading-2 + :weight :semi-bold} + (i18n/label :t/just-introduce-yourself)] + [quo/text + {:style {:padding-top 4 :padding-bottom 12} + :size :paragraph-1 + :weight :regular} + (i18n/label :t/add-display-name-and-picture)]] + [quo/bottom-actions + {:actions :two-actions + :buttons-style {:flex-shrink 1 :flex-basis 0} + :button-one-label (i18n/label :t/edit-profile) + :button-one-props {:type :primary + :on-press on-accept} + :button-two-label (i18n/label :t/skip) + :button-two-props {:type :grey + :on-press on-reject}}]])) diff --git a/src/status_im/contexts/profile/edit/list_items.cljs b/src/status_im/contexts/profile/edit/list_items.cljs new file mode 100644 index 00000000000..72f6d60efef --- /dev/null +++ b/src/status_im/contexts/profile/edit/list_items.cljs @@ -0,0 +1,71 @@ +(ns status-im.contexts.profile.edit.list-items + (:require [quo.foundations.colors :as colors] + [status-im.common.not-implemented :as not-implemented] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.profile.edit.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.string :as utils.string])) + +(defn items + [theme] + (let [{:keys [bio ens-name?] :as profile} (rf/sub [:profile/profile-with-image]) + customization-color (rf/sub [:profile/customization-color]) + full-name (profile.utils/displayed-name profile)] + [{:label (i18n/label :t/profile) + :items [(cond-> {:title (i18n/label :t/name) + :blur? true + :label :text + :label-props (utils.string/truncate full-name + constants/profile-name-max-length) + :container-style style/item-container} + (not ens-name?) + (assoc :on-press #(rf/dispatch [:open-modal :screen/edit-name]) + :action :arrow)) + {:title (i18n/label :t/bio) + :on-press #(rf/dispatch [:open-modal :screen/edit-bio]) + :blur? true + :label :text + :label-props (utils.string/truncate bio 15) + :action :arrow + :container-style style/item-container} + {:title (i18n/label :t/accent-colour) + :on-press #(rf/dispatch [:open-modal :screen/edit-accent-colour]) + :label :color + :label-props (colors/resolve-color customization-color theme) + :blur? true + :action :arrow + :container-style style/item-container}]} + + (when config/show-not-implemented-features? + {:label (i18n/label :t/showcase) + :items [{:title (i18n/label :t/communities) + :on-press not-implemented/alert + :blur? true + :action :arrow + :container-style style/item-container} + {:title (i18n/label :t/accounts) + :on-press not-implemented/alert + :blur? true + :action :arrow + :container-style style/item-container} + {:title (i18n/label :t/collectibles) + :on-press not-implemented/alert + :blur? true + :action :arrow + :container-style style/item-container} + {:title (i18n/label :t/assets) + :on-press not-implemented/alert + :blur? true + :action :arrow + :container-style style/item-container}]}) + + (when config/show-not-implemented-features? + {:label (i18n/label :t/on-the-web) + :items [{:title (i18n/label :t/links) + :on-press not-implemented/alert + :blur? true + :action :arrow + :container-style style/item-container}]})])) diff --git a/src/status_im/contexts/profile/edit/modal/events.cljs b/src/status_im/contexts/profile/edit/modal/events.cljs new file mode 100644 index 00000000000..4ef066e24c7 --- /dev/null +++ b/src/status_im/contexts/profile/edit/modal/events.cljs @@ -0,0 +1,62 @@ +(ns status-im.contexts.profile.edit.modal.events + (:require [status-im.contexts.profile.edit.introduce-yourself.view :as introduce-yourself] + [utils.re-frame :as rf])) + +(rf/reg-event-fx + :profile/edit-profile + (fn [_ [{:keys [display-name picture color on-success]}]] + {:fx (cond-> [] + display-name + (conj [:dispatch + [:profile/edit-name + {:display-name display-name + :navigate-back? false + :show-toast? false}]]) + color + (conj [:dispatch + [:profile/edit-accent-colour + {:color color + :navigate-back? false + :show-toast? false}]]) + picture + (conj [:dispatch + [:profile/edit-picture + {:picture picture + :show-toast? false}]]) + + (nil? picture) + (conj [:dispatch [:profile/delete-picture {:show-toast? false}]]) + + :always + (conj [:dispatch [:navigate-back]] + [:dispatch on-success]))})) + +(defn- profile-update-asked-storage-key + [key-uid] + (keyword :update-profile-asked key-uid)) + +(rf/reg-event-fx + :profile/set-profile-update-as-asked + (fn [{db :db}] + (let [storage-key (-> db :profile/profile :key-uid profile-update-asked-storage-key)] + {:fx [[:effects.async-storage/set {storage-key true}]]}))) + +(rf/reg-event-fx + :profile/ask-profile-update + (fn [_ [pending-event]] + {:fx [[:dispatch [:profile/set-profile-update-as-asked]] + [:dispatch + [:show-bottom-sheet + {:content (fn [] + [introduce-yourself/sheet {:pending-event pending-event}])}]]]})) + +(rf/reg-event-fx + :profile/check-profile-update-prompt + (fn [{db :db} [pending-event]] + (let [storage-key (-> db :profile/profile :key-uid profile-update-asked-storage-key)] + {:fx [[:effects.async-storage/get + {:keys [storage-key] + :cb (fn [profile-updated-data] + (rf/dispatch (if (storage-key profile-updated-data) + pending-event + [:profile/ask-profile-update pending-event])))}]]}))) diff --git a/src/status_im/contexts/profile/edit/modal/style.cljs b/src/status_im/contexts/profile/edit/modal/style.cljs new file mode 100644 index 00000000000..48a647de5dd --- /dev/null +++ b/src/status_im/contexts/profile/edit/modal/style.cljs @@ -0,0 +1,55 @@ +(ns status-im.contexts.profile.edit.modal.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def continue-button + {:width "100%" + :margin-left :auto + :margin-top (if platform/android? :auto 0) + :margin-right :auto}) + +(def button-container + {:width "100%" + :padding-left 20 + :padding-right 20 + :padding-top 12 + :align-self :flex-end + :height 64}) + +(defn view-button-container + [keyboard-shown?] + (merge button-container + (if platform/ios? + {:margin-bottom (if keyboard-shown? 0 34)} + {:margin-bottom (if keyboard-shown? 12 34)}))) + +(def blur-button-container + (merge button-container + (when platform/android? {:padding-bottom 12}))) + +(def page-container + {:flex 1}) + +(def info-message + {:margin-top 8}) + +(def title + {:color colors/white + :margin-top 12 + :margin-bottom 18}) + +(def color-title + {:color colors/white-70-blur + :margin-top 20 + :margin-bottom 16}) + +(def content-container + {:padding-horizontal 20}) + +(def input-container + {:align-items :flex-start}) + +(def profile-input-container + {:flex-direction :row + :justify-content :center}) diff --git a/src/status_im/contexts/profile/edit/modal/view.cljs b/src/status_im/contexts/profile/edit/modal/view.cljs new file mode 100644 index 00000000000..3d463baf1c3 --- /dev/null +++ b/src/status_im/contexts/profile/edit/modal/view.cljs @@ -0,0 +1,270 @@ +(ns status-im.contexts.profile.edit.modal.view + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.avatar-picture-picker.view :as profile-picture-picker] + [status-im.common.events-helper :as events-helper] + [status-im.common.validation.profile :as profile-validator] + [status-im.constants :as c] + [status-im.contexts.profile.edit.modal.events] + [status-im.contexts.profile.edit.modal.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.responsiveness :as responsiveness])) + +(def scroll-view-height (reagent/atom 0)) +(def content-container-height (reagent/atom 0)) +(def set-scroll-view-height #(reset! scroll-view-height %)) + +(defn set-content-container-height + [e] + (reset! content-container-height (oops/oget e "nativeEvent.layout.height"))) + +(defn show-button-background? + [keyboard-height keyboard-shown content-scroll-y] + (let [button-container-height 64 + keyboard-view-height (+ keyboard-height button-container-height)] + (when keyboard-shown + (cond + platform/android? + (< (- @scroll-view-height button-container-height) @content-container-height) + + platform/ios? + (< (- @scroll-view-height keyboard-view-height) (- @content-container-height content-scroll-y)) + + :else + false)))) + +(defn button-container + [show-keyboard? keyboard-shown show-background? keyboard-height children] + (let [height (reagent/atom 0)] + (reset! height (if show-keyboard? (if keyboard-shown keyboard-height 0) 0)) + [rn/view {:style {:margin-top :auto}} + (cond + (and (> @height 0) show-background?) + [quo/blur + (when keyboard-shown + {:blur-amount 34 + :blur-type :transparent + :overlay-color :transparent + :background-color (if platform/android? + colors/neutral-100 + colors/neutral-80-opa-1-blur) + :style style/blur-button-container}) + children] + + (and (> @height 0) (not show-background?)) + [rn/view {:style (style/view-button-container true)} + children] + + (not show-keyboard?) + [rn/view {:style (style/view-button-container false)} + children])])) + +(defn- add-keyboard-listener + [e callback] + (oops/ocall rn/keyboard "addListener" e callback)) + +(defn- on-keyboard-show + [f] + (add-keyboard-listener (if platform/android? "keyboardDidShow" "keyboardWillShow") f)) + +(defn- on-keyboard-hide + [f] + (add-keyboard-listener (if platform/android? "keyboardDidHide" "keyboardWillHide") f)) + +(defn floating-button + [{:keys [custom-color scroll-y on-submit disabled?]}] + (reagent/with-let [show-keyboard? (reagent/atom false) + show-listener (on-keyboard-show #(reset! show-keyboard? true)) + hide-listener (on-keyboard-hide #(reset! show-keyboard? false))] + (let [{:keys [keyboard-shown keyboard-height]} (hooks/use-keyboard) + show-background? (show-button-background? keyboard-height + keyboard-shown + scroll-y)] + [rn/keyboard-avoiding-view + {:style {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0} + :pointer-events :box-none} + [button-container @show-keyboard? keyboard-shown show-background? keyboard-height + [quo/button + {:accessibility-label :submit-create-profile-button + :type :primary + :customization-color custom-color + :on-press on-submit + :container-style style/continue-button + :disabled? disabled?} + (i18n/label :t/continue)]]]) + (finally + (oops/ocall show-listener "remove") + (oops/ocall hide-listener "remove")))) + +(defn- content + [{:keys [set-display-name set-validation-msg picture-picker]}] + (let [open-picture-picker (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:show-bottom-sheet + {:content picture-picker + :shell? true + :theme :dark}])) + on-change-text (fn [s] + (set-validation-msg s) + (set-display-name s))] + (fn [{:keys [profile-image custom-color display-name validation-msg valid-name? + name-too-short? initial-display-name set-scroll set-scroll-height + set-custom-color]}] + (let [{window-width :width} (rn/get-window) + info-type (cond + validation-msg :error + name-too-short? :default + :else :success) + info-message (or validation-msg + (i18n/label :t/minimum-characters + {:min-chars profile-validator/min-length})) + full-name (if (seq display-name) + display-name + initial-display-name)] + [rn/scroll-view + {:on-layout set-scroll-height + :on-scroll set-scroll + :scroll-event-throttle 64 + :content-container-style {:flex-grow 1}} + [rn/view {:on-layout set-content-container-height} + [rn/view {:style style/content-container} + [quo/text + {:style style/title + :size :heading-1 + :weight :semi-bold} + (i18n/label :t/edit-profile)] + [rn/view {:style style/input-container} + [rn/view {:style style/profile-input-container} + [quo/profile-input + {:customization-color custom-color + :placeholder initial-display-name + :on-press open-picture-picker + :image-picker-props {:profile-picture profile-image + :full-name full-name + :customization-color custom-color} + :title-input-props {:default-value "" + :auto-focus true + :max-length c/profile-name-max-length + :on-change-text on-change-text}}]] + [quo/info-message + {:status info-type + :size :default + :icon (if valid-name? :i/positive-state :i/info) + :color (when (= :default info-type) colors/white-70-blur) + :container-style style/info-message} + info-message] + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/color-title} + (i18n/label :t/colour)]]] + [quo/color-picker + {:blur? true + :default-selected custom-color + :on-change set-custom-color + :window-width window-width + :container-style {:padding-left (responsiveness/iphone-11-Pro-20-pixel-from-width + window-width)}}]]])))) + +(defn- submittable-state? + [{:keys [new-color? new-picture? new-name? valid-name?]}] + (let [acceptable-name? (or (not new-name?) valid-name?)] + (or (and new-color? acceptable-name?) + (and new-picture? acceptable-name?) + (and new-name? valid-name?)))) + +(defn view-internal + [{:keys [pending-event]}] + (let [{initial-display-name :display-name + initial-color :customization-color + [initial-image] :images} (rf/sub [:profile/profile]) + scroll-y (reagent/atom 0) + display-name (reagent/atom "") + validation-msg (reagent/atom (profile-validator/validation-name @display-name)) + custom-color (reagent/atom (or initial-color c/profile-default-color)) + profile-image (reagent/atom initial-image) + set-display-name #(reset! validation-msg (profile-validator/validation-name %)) + set-validation-msg #(reset! display-name (string/trim %)) + set-custom-color #(reset! custom-color %) + set-scroll (fn [e] + (reset! scroll-y (oops/oget e "nativeEvent.contentOffset.y"))) + set-scroll-height (fn [e] + (reset! scroll-y 0) + (-> e + (oops/oget "nativeEvent.layout.height") + set-scroll-view-height)) + picture-picker (fn [] + [profile-picture-picker/view + {:on-result #(reset! profile-image %) + :has-picture? (some? @profile-image)}]) + update-profile (fn [] + (rf/dispatch + [:profile/edit-profile + (cond-> {:on-success pending-event} + (and (seq @display-name) + (not= initial-display-name @display-name)) + (assoc :display-name @display-name) + + (not= initial-image @profile-image) + (assoc :picture @profile-image) + + (not= initial-color @custom-color) + (assoc :color @custom-color))])) + on-close (fn [] + (events-helper/navigate-back) + (rf/dispatch pending-event))] + (fn [] + (let [name-too-short? (profile-validator/name-too-short? @display-name) + valid-name? (and (not @validation-msg) (not name-too-short?)) + disabled? (not + (submittable-state? + {:new-color? (not= initial-color @custom-color) + :new-picture? (not= @profile-image initial-image) + :new-name? (and (not= initial-display-name @display-name) + (seq @display-name)) + :valid-name? valid-name?}))] + [quo/overlay {:type :shell} + [rn/view {:style style/page-container} + [quo/page-nav + {:margin-top safe-area/top + :background :blur + :icon-name :i/close + :on-press on-close}] + [content + {:profile-image @profile-image + :custom-color @custom-color + :display-name @display-name + :validation-msg @validation-msg + :valid-name? valid-name? + :name-too-short? name-too-short? + :picture-picker picture-picker + :initial-display-name initial-display-name + :set-scroll set-scroll + :set-scroll-height set-scroll-height + :set-custom-color set-custom-color + :set-display-name set-display-name + :set-validation-msg set-validation-msg}] + [floating-button + {:custom-color @custom-color + :scroll-y @scroll-y + :on-submit update-profile + :disabled? disabled?}]]])))) + +(defn view + [] + (let [screen-params (quo.context/use-screen-params)] + [view-internal screen-params])) diff --git a/src/status_im/contexts/profile/edit/name/events.cljs b/src/status_im/contexts/profile/edit/name/events.cljs new file mode 100644 index 00000000000..e651769da1a --- /dev/null +++ b/src/status_im/contexts/profile/edit/name/events.cljs @@ -0,0 +1,42 @@ +(ns status-im.contexts.profile.edit.name.events + (:require [clojure.string :as string] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :profile/edit-profile-name-success + (fn [{:keys [db]} [display-name navigate-back? show-toast?]] + {:db (assoc-in db [:profile/profile :display-name] display-name) + :fx [(when navigate-back? + [:dispatch [:navigate-back]]) + (when show-toast? + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/name-updated)}]])]})) + +(defn edit-profile-name + [_ + [{:keys [display-name navigate-back? show-toast?] + :or {navigate-back? true + show-toast? true}}]] + {:fx [[:json-rpc/call + [{:method "wakuext_setDisplayName" + :params [display-name] + :on-success [:profile/edit-profile-name-success display-name navigate-back? show-toast?]}]]]}) + +(rf/reg-event-fx :profile/edit-name edit-profile-name) + +(defn display-name-from-compressed-key + [profile] + (-> profile :compressed-key (string/split #"zQ3") second (subs 0 constants/profile-name-max-length))) + +(rf/reg-event-fx + :profile/set-default-profile-name + (fn [{db :db} [profile]] + (let [default-display-name (display-name-from-compressed-key profile)] + {:db (assoc-in db [:profile/profile :display-name] default-display-name) + :fx [[:json-rpc/call + [{:method "wakuext_setDisplayName" + :params [default-display-name]}]]]}))) diff --git a/src/status_im/contexts/profile/edit/name/events_test.cljs b/src/status_im/contexts/profile/edit/name/events_test.cljs new file mode 100644 index 00000000000..d9a439ec222 --- /dev/null +++ b/src/status_im/contexts/profile/edit/name/events_test.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.profile.edit.name.events-test + (:require [cljs.test :refer [deftest is]] + matcher-combinators.test + [status-im.contexts.profile.edit.name.events :as sut])) + +(deftest edit-name-test + (let [new-name "John Doe" + cofx {:db {:profile/profile {:display-name "Old name"}}} + expected {:fx [[:json-rpc/call + [{:method "wakuext_setDisplayName" + :params [name] + :on-success [:profile/edit-profile-name-success new-name true true]}]]]}] + (is (match? expected + (sut/edit-profile-name cofx [{:display-name new-name}]))))) diff --git a/src/status_im/contexts/profile/edit/name/style.cljs b/src/status_im/contexts/profile/edit/name/style.cljs new file mode 100644 index 00000000000..3a94cbe9a41 --- /dev/null +++ b/src/status_im/contexts/profile/edit/name/style.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.profile.edit.name.style) + +(defn page-wrapper + [insets] + {:padding-top (:top insets) + :padding-bottom (:bottom insets) + :padding-horizontal 1 + :flex 1}) + +(def screen-container + {:flex 1 + :padding-top 14 + :padding-horizontal 20 + :justify-content :space-between}) + +(def button-wrapper + {:margin-vertical 12}) diff --git a/src/status_im/contexts/profile/edit/name/view.cljs b/src/status_im/contexts/profile/edit/name/view.cljs new file mode 100644 index 00000000000..33db2fcfb70 --- /dev/null +++ b/src/status_im/contexts/profile/edit/name/view.cljs @@ -0,0 +1,74 @@ +(ns status-im.contexts.profile.edit.name.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.validation.profile :as profile-validator] + [status-im.constants :as constants] + [status-im.contexts.profile.edit.name.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [profile (rf/sub [:profile/profile-with-image]) + customization-color (rf/sub [:profile/customization-color]) + display-name (profile.utils/displayed-name profile) + alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + full-name (reagent/atom display-name) + error-msg (reagent/atom nil) + typing? (reagent/atom false) + validate-name (debounce/debounce (fn [name] + (reset! error-msg + (profile-validator/validation-name name)) + (reset! typing? false)) + 300) + on-change-text (fn [s] + (reset! typing? true) + (reset! full-name s) + (validate-name s))] + (fn [] + [quo/overlay + {:type :shell + :container-style (style/page-wrapper safe-area/insets)} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back])}] + [rn/keyboard-avoiding-view + {:key :content + :keyboard-vertical-offset (if platform/ios? alert-banners-top-margin 0) + :style style/screen-container} + [rn/view {:style {:gap 22}} + [quo/text-combinations {:title (i18n/label :t/name)}] + [quo/input + {:blur? true + :error? (not (string/blank? @error-msg)) + :container-style {:margin-bottom -11} + :default-value @full-name + :auto-focus true + :char-limit constants/profile-name-max-length + :label (i18n/label :t/profile-name) + :on-change-text on-change-text}] + (when-not (string/blank? @error-msg) + [quo/info-message + {:status :error + :size :default + :icon :i/info} + @error-msg])] + [rn/view {:style style/button-wrapper} + [quo/button + {:type :primary + :customization-color customization-color + :on-press (fn [] + (rf/dispatch [:profile/edit-name {:display-name @full-name}]) + (rf/dispatch [:profile/set-profile-update-as-asked])) + :disabled? (boolean (or @typing? + (string/blank? @full-name) + (not (string/blank? @error-msg))))} + (i18n/label :t/save-name)]]]]))) diff --git a/src/status_im/contexts/profile/edit/style.cljs b/src/status_im/contexts/profile/edit/style.cljs new file mode 100644 index 00000000000..5a5edb357cc --- /dev/null +++ b/src/status_im/contexts/profile/edit/style.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.profile.edit.style) + +(defn page-wrapper + [inset] + {:padding-top inset + :padding-horizontal 1}) + +(def screen-container + {:padding-top 14 + :padding-horizontal 20}) + +(def avatar-wrapper + {:width 88 + :margin-top 22 + :margin-bottom 12}) + +(def camera-button + {:position :absolute + :overflow :hidden + :right 0 + :bottom 0}) + +(def item-container + {:padding-top 14}) diff --git a/src/status_im/contexts/profile/edit/view.cljs b/src/status_im/contexts/profile/edit/view.cljs new file mode 100644 index 00000000000..953c5d42df3 --- /dev/null +++ b/src/status_im/contexts/profile/edit/view.cljs @@ -0,0 +1,48 @@ +(ns status-im.contexts.profile.edit.view + (:require [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.not-implemented :as not-implemented] + [status-im.config :as config] + [status-im.contexts.profile.edit.header.view :as header] + [status-im.contexts.profile.edit.list-items :as edit.items] + [status-im.contexts.profile.edit.style :as style] + [utils.re-frame :as rf])) + +(defn- item-view + [data] + [quo/category + {:container-style {:padding-bottom 9.5} + :list-type :settings + :blur? true + :label (:label data) + :data (:items data)}]) + +(defn- get-item-layout + [_ index] + #js {:length 100 :offset (* 100 index) :index index}) + +(defn view + [] + (let [theme (quo.context/use-theme)] + [quo/overlay + {:type :shell + :container-style (style/page-wrapper safe-area/top)} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back]) + :right-side (when config/show-not-implemented-features? + [{:icon-name :i/reveal :on-press not-implemented/alert}])}] + [rn/flat-list + {:key :list + :header [header/view] + :data (edit.items/items theme) + :key-fn :label + :get-item-layout get-item-layout + :initial-num-to-render 3 + :max-to-render-per-batch 3 + :shows-vertical-scroll-indicator false + :render-fn item-view}]])) diff --git a/src/status_im/contexts/profile/effects.cljs b/src/status_im/contexts/profile/effects.cljs new file mode 100644 index 00000000000..f6e9314c5b0 --- /dev/null +++ b/src/status_im/contexts/profile/effects.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.profile.effects + (:require + [native-module.core :as native-module] + [promesa.core :as promesa] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.profile/accept-terms + (fn [{:keys [on-success]}] + (-> (native-module/accept-terms) + (promesa/then (fn [] + (rf/call-continuation on-success))) + (promesa/catch (fn [error] + (log/error "Failed to accept terms" {:error error})))))) diff --git a/src/status_im/contexts/profile/events.cljs b/src/status_im/contexts/profile/events.cljs new file mode 100644 index 00000000000..f643dab058d --- /dev/null +++ b/src/status_im/contexts/profile/events.cljs @@ -0,0 +1,141 @@ +(ns status-im.contexts.profile.events + (:require + [clojure.string :as string] + [legacy.status-im.data-store.settings :as data-store.settings] + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [native-module.core :as native-module] + [status-im.config :as config] + [status-im.contexts.profile.data-store :as profile.data-store] + [status-im.contexts.profile.edit.accent-colour.events] + [status-im.contexts.profile.edit.bio.events] + [status-im.contexts.profile.edit.header.events] + [status-im.contexts.profile.edit.name.events] + status-im.contexts.profile.effects + status-im.contexts.profile.login.events + status-im.contexts.profile.logout.events + [status-im.contexts.profile.rpc :as profile.rpc] + [utils.re-frame :as rf])) + +(defn- select-profile + [profile key-uid] + (-> profile + (assoc :key-uid key-uid) + (dissoc :error :password))) + +(defn- reduce-profiles + [profiles] + (reduce + (fn [acc {:keys [key-uid] :as profile}] + (assoc acc key-uid (profile.rpc/rpc->profiles-overview profile))) + {} + profiles)) + +(rf/reg-fx + :profile/get-profiles-overview + (fn [callback] + (native-module/initialize-application + {:dataDir (native-module/backup-disabled-data-dir) + :mixpanelAppId config/mixpanel-app-id + :mixpanelToken config/mixpanel-token + :sentryDSN (if config/sentry-enabled? config/sentry-dsn-status-go "") + :mediaServerEnableTLS (config/enabled? config/STATUS_BACKEND_SERVER_MEDIA_SERVER_ENABLE_TLS) + :logEnabled (not (string/blank? (config/log-level))) + :logLevel (config/log-level) + :logDir (native-module/log-file-directory config/use-public-log-dir?) + :apiLoggingEnabled config/api-logging-enabled?} + callback))) + +(rf/reg-event-fx + :profile/profile-selected + (fn [{:keys [db]} [key-uid]] + {:db (update db :profile/login select-profile key-uid)})) + +(rf/reg-event-fx + :profile/set-profile-overview-auth-method + (fn [{db :db} [key-uid auth-method]] + {:db (assoc-in db [:profile/profiles-overview key-uid :auth-method] auth-method)})) + +(rf/reg-event-fx + :profile/get-profiles-auth-method + (fn [_ [key-uids]] + (let [auth-method-fx (fn [key-uid] + [:effects.biometric/check-if-available + {:key-uid key-uid + :on-success (fn [auth-method] + (rf/dispatch + [:profile/set-profile-overview-auth-method + key-uid + auth-method]))}])] + {:fx (map auth-method-fx key-uids)}))) + +(rf/reg-event-fx + :profile/set-already-logged-out + (fn [{:keys [db]}] + {:db (dissoc db :profile/logging-out?)})) + +(rf/reg-event-fx + :profile/get-profiles-overview-success + (fn [{:keys [db]} + [{accounts :accounts + {:keys [userConfirmed enabled userID]} :centralizedMetricsInfo} + {:keys [logout-phase?]}]] + (let [profiles (reduce-profiles accounts) + profiles-key-uids (keys profiles) + new-db (cond-> db + :always + (assoc :centralized-metrics/user-confirmed? userConfirmed + :centralized-metrics/enabled? enabled + :centralized-metrics/user-id userID) + + (seq profiles) + (assoc :profile/profiles-overview profiles))] + {:db new-db + :fx [(when-not logout-phase? + [:dispatch + [:profile.login/select-profile-and-login-with-biometric-if-available + (-> profiles profile.data-store/recently-opened-profile :key-uid)]]) + [:dispatch [:profile/get-profiles-auth-method profiles-key-uids]] + (if (profile.data-store/accepted-terms? accounts) + [:dispatch + [:update-theme-and-init-root :screen/profile.profiles + {:default-screen-profiles? logout-phase?}]] + [:dispatch [:update-theme-and-init-root :screen/onboarding.intro]]) + ;; dispatch-later makes sure that the logout button subscribed is always disabled + [:dispatch-later + {:ms 100 + :dispatch [:profile/set-already-logged-out]}]]}))) + +(rf/reg-event-fx + :profile/update-setting-from-backup + (fn [{:keys [db]} [{:keys [backedUpSettings]}]] + (let [setting (update backedUpSettings :name keyword) + {:keys [name value]} (data-store.settings/rpc->setting-value setting)] + {:db (assoc-in db [:profile/profile name] value)}))) + +(rf/reg-event-fx + :profile/update-profile-from-backup + (fn [_ [{{:keys [ensUsernameDetails]} :backedUpProfile}]] + {:fx [[:dispatch [:ens/update-usernames ensUsernameDetails]]]})) + +(rf/reg-event-fx :profile/update-messages-from-contacts-only + (fn [{:keys [db] :as cofx}] + (multiaccounts.update/multiaccount-update + cofx + :messages-from-contacts-only + (not (get-in db [:profile/profile :messages-from-contacts-only])) + {}))) + +(rf/reg-event-fx :profile/explore-new-status + (fn [] + {:fx [[:effects.profile/accept-terms + {:on-success [:navigate-to :screen/profile.profiles]}]]})) + +(rf/reg-event-fx :profile/save-notifications-prompted + (fn [{:keys [_db]} [{:keys [key-uid]}]] + {:fx [[:effects.kv/merge-object + {:key key-uid + :value {:notifications-prompted? true}}]]})) + +(rf/reg-event-fx :profile/remove-local-profile-storage + (fn [{:keys [_db]} [{:keys [key-uid]}]] + {:fx [[:effects.kv/delete-key key-uid]]})) diff --git a/src/status_im/contexts/profile/login/effects.cljs b/src/status_im/contexts/profile/login/effects.cljs new file mode 100644 index 00000000000..7ae51033ed1 --- /dev/null +++ b/src/status_im/contexts/profile/login/effects.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.profile.login.effects + (:require + [native-module.core :as native-module] + [status-im.contexts.profile.config :as profile.config] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.profile/login + (fn [[key-uid hashed-password]] + ;;"node.login" signal will be triggered as a callback + (native-module/login-account + (assoc (merge (profile.config/login) + (profile.config/fix-node-config-migration)) + :keyUid key-uid + :password hashed-password)))) + +(rf/reg-fx :effects.profile/enable-local-notifications + (fn [] + (native-module/start-local-notifications))) + +(rf/reg-fx :effects.profile/convert-to-keycard-profile + (fn [{:keys [profile settings password new-password callback]}] + (native-module/convert-to-keycard-profile profile settings password new-password callback))) diff --git a/src/status_im/contexts/profile/login/events.cljs b/src/status_im/contexts/profile/login/events.cljs new file mode 100644 index 00000000000..ebf7589d665 --- /dev/null +++ b/src/status_im/contexts/profile/login/events.cljs @@ -0,0 +1,293 @@ +(ns status-im.contexts.profile.login.events + (:require + [legacy.status-im.data-store.settings :as data-store.settings] + [legacy.status-im.mailserver.core :as mailserver] + [native-module.core :as native-module] + [oops.core :as oops] + [status-im.common.keychain.events :as keychain] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.profile.db :as profile.db] + status-im.contexts.profile.login.effects + [status-im.contexts.profile.rpc :as profile.rpc] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(rf/reg-event-fx :profile.login/login + (fn [{:keys [db]}] + (let [{:keys [key-uid password]} (:profile/login db) + login-sha3-password (native-module/sha3 (security/safe-unmask-data password))] + {:db (-> db + (assoc-in [:profile/login :processing] true) + (assoc-in [:syncing :login-sha3-password] login-sha3-password)) + :fx [[:effects.profile/login [key-uid login-sha3-password]]]}))) + +(rf/reg-event-fx :profile.login/local-paired-user + (fn [{:keys [db]}] + (let [{:keys [key-uid password keycard-pairing + whisper-private-key]} (get-in db [:syncing :profile]) + login-sha3-password (get-in db [:syncing :login-sha3-password]) + password (if-not (nil? login-sha3-password) ;; already logged in + login-sha3-password + password) + masked-password (security/mask-data password)] + {:db (-> db + (assoc-in [:onboarding/profile :password] masked-password) + (assoc-in [:onboarding/profile :syncing?] true)) + :fx [(if keycard-pairing + [:effects.keycard/login-with-keycard + {:password password + :whisper-private-key whisper-private-key + :key-uid key-uid}] + [:effects.profile/login [key-uid password]])]}))) + +;; login phase 1: we want to load and show chats faster, so we split login into 2 phases +(rf/reg-event-fx :profile.login/login-existing-profile + (fn [{:keys [db]} [settings-data account]] + (let [settings (data-store.settings/rpc->settings settings-data) + profile-overview (profile.rpc/rpc->profiles-overview account) + log-level (or (:log-level settings) (config/log-level)) + pairing-completed? (= (get-in db [:syncing :pairing-status]) :completed) + biometric-supported-type (get-in db [:biometrics :supported-type]) + new-db (-> db + (assoc :profile/profile + (merge profile-overview + settings + {:log-level log-level})) + (assoc-in [:activity-center :loading?] true) + (dissoc :centralized-metrics/onboarding-enabled?)) + keycard? (get-in new-db [:profile/profile :keycard-pairing])] + {:db (cond-> new-db + pairing-completed? (dissoc :syncing)) + :fx (into [[:json-rpc/call + [{:method "wakuext_startMessenger" + :js-response true + :on-success [:profile.login/messenger-started] + :on-error #(log/error "failed to start messenger" %)}]] + [:dispatch [:community/fetch]] + + ;; Wallet initialization can be delayed a little bit because we + ;; need to free the queue for heavier events first, such as + ;; loading chats and communities. This globally helps alleviate + ;; stuttering immediately after login. + [:dispatch-later [{:ms 500 :dispatch [:wallet/initialize]}]] + ;; Fetching the currencies along with wallet initialization as + ;; we rely on it for displaying currency symbol + [:dispatch-later [{:ms 500 :dispatch [:settings/get-currencies]}]] + + [:logs/set-level log-level] + + ;; Immediately try to open last chat. We can't wait until the + ;; messenger has started and has processed all chats because + ;; the whole process can take a handful of seconds. + (when-not (:universal-links/handling db) + [:effects.chat/open-last-chat (:key-uid profile-overview)]) + + (when (:centralized-metrics/onboarding-enabled? db) + [:dispatch [:profile.settings/toggle-telemetry true]]) + + (when keycard? + [:dispatch [:centralized-metrics/track :metric/keycard-login]])] + (cond + (and pairing-completed? biometric-supported-type) + [[:dispatch [:update-theme-and-init-root :screen/onboarding.syncing-biometric]]] + + (or pairing-completed? (get db :onboarding/new-account?)) + [[:dispatch [:onboarding/finalize-setup]] + [:dispatch [:onboarding/finish-onboarding]]] + + :else + [[:dispatch [:shell/show-root-view]]]))}))) + +;; login phase 2: we want to load and show chats faster, so we split login into 2 phases +(rf/reg-event-fx :profile.login/get-chats-callback + (fn [{:keys [db]}] + (let [{:keys [notifications-enabled? + news-notifications-enabled? + messenger-notifications-enabled? + key-uid]} (:profile/profile db)] + {:db db + :fx [[:effects.profile/enable-local-notifications] + [:contacts/initialize-contacts] + ;; The delay is arbitrary. We just want to give some time for the + ;; thread to process more important events first, but we can't delay + ;; too much otherwise the UX may degrade due to stale data. + [:dispatch-later [{:ms 1500 :dispatch [:profile.login/non-critical-initialization]}]] + [:dispatch [:network/check-expensive-connection]] + [:profile.settings/get-profile-picture key-uid] + (when notifications-enabled? + [:effects/push-notifications-enable + (cond-> #{} + messenger-notifications-enabled? (conj :enable-chat-notifications?) + news-notifications-enabled? (conj :enable-news-notifications?))])]}))) + +;; Login phase 3: events at this phase can wait a bit longer to be processed in +;; order to leave room for higher-priority or heavy weight events. +(rf/reg-event-fx :profile.login/non-critical-initialization + (fn [{:keys [db]}] + (let [{:keys [preview-privacy?]} (:profile/profile db)] + {:fx [[:browser/initialize-browser] + [:logging/initialize-web3-client-version] + [:group-chats/get-group-chat-invitations] + [:profile.settings/blank-preview-flag-changed preview-privacy?] + [:visibility-status-updates/fetch] + [:dispatch [:universal-links/generate-profile-url]] + [:push-notifications/load-preferences] + [:profile.config/get-node-config] + [:activity-center.notifications/fetch-pending-contact-requests-fx] + [:activity-center/update-seen-state] + [:activity-center.notifications/fetch-unread-count] + [:pairing/get-our-installations] + [:json-rpc/call + [{:method "admin_nodeInfo" + :on-success [:profile.login/node-info-fetched] + :on-error #(log/error "node-info: failed error" %)}]]]}))) + +(rf/reg-event-fx :profile.login/messenger-started + (fn [{:keys [db]} [response-js]] + (let [mailservers (transforms/js->clj (oops/oget response-js :mailservers)) + new-account? (get db :onboarding/new-account?)] + {:db (-> db + (assoc :messenger/started? true) + (mailserver/add-mailservers mailservers)) + :fx [[:fetch-chats-preview + {:chat-preview-type constants/chat-preview-type-non-community + :on-success (fn [result] + (rf/dispatch [:chats-list/load-success result]) + (rf/dispatch [:profile.login/get-chats-callback]))}] + [:fetch-chats-preview + {:chat-preview-type constants/chat-preview-type-community + :on-success (fn [result] + (rf/dispatch [:chats-list/load-success result]) + (rf/dispatch [:communities/get-user-requests-to-join]))}] + (when (and (:syncing/fallback-flow? db) (:syncing/installation-id db)) + [:dispatch [:pairing/finish-seed-phrase-fallback-syncing]]) + (when-not new-account? + [:dispatch [:universal-links/process-stored-event]])]}))) + +(rf/reg-event-fx :profile.login/node-info-fetched + (fn [{:keys [db]} [node-info]] + {:db (assoc db :node-info node-info)})) + +(rf/reg-event-fx + :profile.login/login-node-signal + (fn [{db :db} [{:keys [settings account ensUsernames error]}]] + (log/debug "[signals] node.login" "error" error) + (if error + {:db (update db :profile/login #(-> % (dissoc :processing) (assoc :error error)))} + {:db (dissoc db :profile/login) + :fx [[:dispatch-later [{:ms 2000 :dispatch [:ens/update-usernames ensUsernames]}]] + [:dispatch [:profile.login/login-existing-profile settings account]]]}))) + +(rf/reg-event-fx + :profile.login/select-profile-and-login-with-biometric-if-available + (fn [_ [key-uid]] + {:fx [[:dispatch [:profile/profile-selected key-uid]] + [:effects.biometric/check-if-available + {:key-uid key-uid + :on-success (fn [auth-method] + (rf/dispatch + [:profile.login/check-biometric-success + key-uid auth-method]))}]]})) + +(rf/reg-event-fx + :profile.login/check-biometric-success + (fn [{:keys [db]} [key-uid auth-method]] + {:db (assoc db :auth-method auth-method) + :fx [(when (= auth-method keychain/auth-method-biometric) + [:keychain/password-hash-migration + {:key-uid key-uid + :callback (fn [] + (rf/dispatch + [:biometric/authenticate + {:on-success #(rf/dispatch + [:profile.login/biometric-success]) + :on-fail #(rf/dispatch + [:profile.login/biometric-auth-fail %])}]))}])]})) + +(rf/reg-event-fx + :profile.login/get-user-password-success + (fn [{:keys [db]} [password]] + (when password + {:db (-> db + (assoc-in [:profile/login :password] password) + (assoc-in [:profile/login :processing] true)) + :fx [[:dispatch [:update-theme-and-init-root :screen/progress]] + [:effects.profile/login + [(get-in db [:profile/login :key-uid]) + (security/safe-unmask-data password)]]]}))) + +(rf/reg-event-fx + :profile.login/biometric-success + (fn [{:keys [db]}] + (let [key-uid (get-in db [:profile/login :key-uid]) + keycard? (get-in db [:profile/profiles-overview key-uid :keycard-pairing])] + {:fx [(if keycard? + [:keychain/get-keycard-keys + [key-uid #(rf/dispatch [:keycard.login/on-get-keys-from-keychain-success key-uid %])]] + [:keychain/get-user-password + [key-uid #(rf/dispatch [:profile.login/get-user-password-success %])]])]}))) + +(rf/reg-event-fx + :profile.login/biometric-auth-fail + (fn [_ [error]] + (log/error (ex-message error) + (-> error + ex-data + (assoc :code (ex-cause error) + :event :profile.login/biometric-auth-fail))) + {:dispatch [:biometric/show-message (ex-cause error)]})) + +(rf/reg-event-fx + :profile.login/verify-database-password + (fn [_ [entered-password cb]] + (let [hashed-password (-> entered-password + security/safe-unmask-data + native-module/sha3)] + {:json-rpc/call [{:method "accounts_verifyPassword" + :params [hashed-password] + :on-success #(rf/dispatch [:profile.login/verified-database-password % cb]) + :on-error #(log/error "accounts_verifyPassword error" %)}]}))) + +(rf/reg-event-fx + :profile.login/verified-database-password + (fn [{:keys [db]} [valid? callback]] + (if valid? + (do + (when (fn? callback) + (callback)) + {:db (update db + :profile/login + dissoc + :processing :error)}) + {:db (update db + :profile/login + #(-> % + (dissoc :processing) + (assoc :error "Invalid password")))}))) + +(rf/reg-event-fx + :profile/on-password-input-changed + (fn [{:keys [db]} [{:keys [password error]}]] + {:db (update db :profile/login assoc :password password :error error)})) + +(rf/reg-event-fx + :profile/toggle-testnet-mode-banner + (fn [{:keys [db]}] + (when config/enable-alert-banner? + (let [testnet? (profile.db/testnet? db) + banner-added? (-> db :alert-banners :alert seq)] + {:fx [(cond + (and testnet? (not banner-added?)) + [:dispatch + [:alert-banners/add + {:type :alert + :text (i18n/label :t/testnet-mode-enabled) + :on-press #(rf/dispatch [:wallet/show-disable-testnet-mode-confirmation])}]] + + (and (not testnet?) banner-added?) + [:dispatch + [:alert-banners/remove :alert]])]})))) diff --git a/src/status_im/contexts/profile/logout/effects.cljs b/src/status_im/contexts/profile/logout/effects.cljs new file mode 100644 index 00000000000..3d2af826984 --- /dev/null +++ b/src/status_im/contexts/profile/logout/effects.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.profile.logout.effects + (:require [native-module.core :as native-module] + [re-frame.core :as rf])) + +(rf/reg-fx :effects.profile/logout native-module/logout) diff --git a/src/status_im/contexts/profile/logout/events.cljs b/src/status_im/contexts/profile/logout/events.cljs new file mode 100644 index 00000000000..c6661163387 --- /dev/null +++ b/src/status_im/contexts/profile/logout/events.cljs @@ -0,0 +1,61 @@ +(ns status-im.contexts.profile.logout.events + (:require [status-im.config :as config] + [status-im.contexts.profile.logout.effects] + [status-im.db :as db] + [utils.re-frame :as rf])) + +(rf/reg-event-fx + :profile.logout/disable-notifications + (fn [{:keys [db]}] + {:fx [[:effects/push-notifications-disable + (let [{:keys [notifications-enabled? + news-notifications-enabled? + messenger-notifications-enabled?]} (:profile/profile db)] + (when notifications-enabled? + (cond-> #{:disable-chat-notifications?} + news-notifications-enabled? (conj :disable-news-notifications?) + messenger-notifications-enabled? (conj :disable-chat-notifications?))))] + [:dispatch [:alert-banners/remove-all]]]})) + +(defn- restart-app-db + [{:keys [initials-avatar-font-file keycard biometrics + network/status network/expensive? + centralized-metrics/user-confirmed?] + network-type :network/network-type + :as db}] + (assoc db/app-db + ;; We must carry over the current `:selected-stack-id`, otherwise the + ;; app will start with `:view-id` as nil. + :shell/selected-stack-id (:shell/selected-stack-id db) + :profile/logging-out? true + :centralized-metrics/user-confirmed? user-confirmed? + :network/type network-type + :network/status status + :network/expensive? expensive? + :initials-avatar-font-file initials-avatar-font-file + :keycard (dissoc keycard :secrets :pin :application-info) + :biometrics biometrics + :syncing nil + :log-level/pre-login-log-level (config/log-level))) + +(rf/reg-event-fx + :profile.logout/reset-state + (fn [{db :db}] + {:db (restart-app-db db) + :fx [[:hide-popover nil] + [:effects.profile/logout nil] + [:profile.settings/webview-debug-changed false] + [:profile/get-profiles-overview + #(rf/dispatch [:profile/get-profiles-overview-success % {:logout-phase? true}])]]})) + +(rf/reg-event-fx + :profile/logout + (fn [{db :db}] + {:db (assoc db :profile/logging-out? true) + ;; We need to disable notifications before starting the logout process + :fx [[:dispatch [:hide-bottom-sheet]] ;; otherwise the login screen isn't interactable + [:dispatch [:profile.logout/disable-notifications]] + [:dispatch [:wallet-connect/unregister-event-listeners]] + [:dispatch-later + {:ms 100 + :dispatch [:profile.logout/reset-state]}]]})) diff --git a/src/status_im/contexts/profile/profiles/style.cljs b/src/status_im/contexts/profile/profiles/style.cljs new file mode 100644 index 00000000000..c3655557797 --- /dev/null +++ b/src/status_im/contexts/profile/profiles/style.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.profile.profiles.style + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +;; Profiles Section + +(defn profiles-profile-card + [last-item?] + ;; This part needs to be improved, inverted shadow is not supported in android + ;; https://reactnative.dev/docs/shadow-props#shadowoffset-ios + ;; + ;; (merge (shadows/get 1 :dark :inverted) ...) + ;; + {:padding-horizontal 20 + :margin-bottom (when-not last-item? -24)}) + +(defn profiles-container + [translate-x] + (reanimated/apply-animations-to-style + {:transform [{:translate-x translate-x}]} + {:position :absolute + :left 0 + :top 0 + :bottom 0 + :right 0})) + +(defn profiles-header + [top] + {:flex-direction :row + :padding-horizontal 20 + :padding-top (+ 56 top) + :margin-bottom 20}) + +(def profiles-header-text + {:color colors/white + :flex 1}) + +;; Login Section + +(defn login-container + [top] + {:position :absolute + :left 0 + :top 0 + :right 0 + :bottom 0 + :padding-top top + :padding-horizontal 20}) + +(def multi-profile-button-container + {:flex-direction :row + :padding-vertical 12 + :justify-content :flex-end + :margin-bottom 8}) + +(def login-profile-card + {:margin-bottom 20}) diff --git a/src/status_im/contexts/profile/profiles/view.cljs b/src/status_im/contexts/profile/profiles/view.cljs new file mode 100644 index 00000000000..4011fc50463 --- /dev/null +++ b/src/status_im/contexts/profile/profiles/view.cljs @@ -0,0 +1,297 @@ +(ns status-im.contexts.profile.profiles.view + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.common.confirmation-drawer.view :as confirmation-drawer] + [status-im.common.standard-authentication.core :as standard-authentication] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.keycard.pin.view :as keycard.pin] + [status-im.contexts.onboarding.common.background.view :as background] + [status-im.contexts.profile.profiles.style :as style] + [taoensso.timbre :as log] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defonce push-animation-fn-atom (atom nil)) +(defonce pop-animation-fn-atom (atom nil)) +;; we need this for hotreload, overwise on hotreload translate-x will be reseted +(defonce translate-x-atom (atom 0)) + +(defn push-animation + [translate-x] + (let [window-width (:width (rn/get-window))] + (reset! translate-x-atom (- window-width)) + (reanimated/animate-shared-value-with-delay translate-x + (- window-width) + constants/onboarding-modal-animation-duration + :linear + 100))) + +(defn pop-animation + [translate-x] + (reset! translate-x-atom 0) + (reanimated/animate-shared-value-with-delay translate-x + 0 + constants/onboarding-modal-animation-duration + :linear + 50)) + +(defn- navigate-to-new-to-status + [] + (when @push-animation-fn-atom + (@push-animation-fn-atom)) + (rf/dispatch [:onboarding/use-temporary-display-name true]) + (debounce/throttle-and-dispatch + [:open-modal :screen/onboarding.create-profile] + 1000)) + +(defn- navigate-to-sync-or-recover-profile + [] + (when @push-animation-fn-atom + (@push-animation-fn-atom)) + (rf/dispatch [:onboarding/use-temporary-display-name false]) + (debounce/throttle-and-dispatch + [:open-modal :screen/onboarding.log-in] + 1000)) + +(defn new-account-options + [] + [quo/action-drawer + [[{:icon :i/profile + :label (i18n/label :t/create-new-profile) + :on-press navigate-to-new-to-status + :accessibility-label :create-new-profile} + {:icon :i/multi-profile + :label (i18n/label :t/sync-or-recover-profile) + :on-press navigate-to-sync-or-recover-profile + :accessibility-label :multi-profile}]]]) + +(defn show-new-account-options + [] + (rf/dispatch [:show-bottom-sheet + {:content new-account-options + :shell? true}])) + +(defn delete-profile-confirmation + [key-uid context] + [confirmation-drawer/confirmation-drawer + {:title (i18n/label :t/remove-profile?) + :description (i18n/label :t/remove-profile-confirm-message) + :accessibility-label :remove-profile-confirm + :context context + :button-text (i18n/label :t/remove) + :close-button-text (i18n/label :t/cancel) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (native-module/delete-multiaccount + key-uid + (fn [result] + (let [{:keys [error]} (transforms/json->clj result)] + (rf/dispatch [:onboarding/on-delete-profile-success key-uid]) + (log/info "profile deleted: error" error)))))}]) + +(defn show-confirmation + [key-uid context] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [delete-profile-confirmation key-uid context])}])) + +(defn profile-options + [key-uid context] + [quo/action-drawer + [[{:icon :i/delete + :label (i18n/label :t/remove-profile-message) + :on-press #(show-confirmation key-uid context) + :accessibility-label :remove-profile + :danger? true}]]]) + +(defn show-profile-options + [key-uid context] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [profile-options key-uid context]) + :shell? true}])) + +(defn profile-card + [{:keys [name key-uid customization-color keycard-pairing]} + index + _ + {:keys [last-index set-hide-profiles]}] + (let [last-item? (= last-index index) + profile-picture (rf/sub [:profile/login-profiles-picture key-uid])] + [quo/profile-card + {:name name + :login-card? true + :last-item? (= last-index index) + :customization-color (or customization-color :primary) + :keycard-account? keycard-pairing + :show-options-button? true + :profile-picture profile-picture + :card-style (style/profiles-profile-card last-item?) + :on-options-press #(show-profile-options + key-uid + {:name name + :color customization-color + :profile-picture profile-picture}) + :on-card-press (fn [] + (rf/dispatch + [:profile.login/select-profile-and-login-with-biometric-if-available + key-uid]) + (set-hide-profiles))}])) + +(defn- profiles-section + [{:keys [hide-profiles]}] + (let [{:keys [top bottom]} safe-area/insets + profiles (vals (rf/sub [:profile/profiles-overview])) + translate-x (reanimated/use-shared-value @translate-x-atom)] + (rn/use-mount (fn [] + (reset! push-animation-fn-atom #(push-animation translate-x)) + (reset! pop-animation-fn-atom #(pop-animation translate-x)) + (fn [] + (when-let [pop-animation-fn @pop-animation-fn-atom] + (when (not= translate-x 0) + (pop-animation-fn))) + (reset! push-animation-fn-atom nil) + (reset! pop-animation-fn-atom nil)))) + [reanimated/view {:style (style/profiles-container translate-x)} + [rn/view {:style (style/profiles-header top)} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/profiles-header-text} + (i18n/label :t/profiles-on-device)] + [quo/button + {:type :primary + :customization-color :blue + :size 32 + :icon-only? true + :on-press show-new-account-options + :accessibility-label :show-new-account-options} + :main-icons/add]] + [rn/flat-list + {:data (sort-by :timestamp > profiles) + :key-fn :key-uid + :content-container-style {:padding-bottom (+ bottom 20)} + :render-data {:last-index (dec (count profiles)) + :set-hide-profiles hide-profiles} + :render-fn profile-card}]])) + +(defn password-input + [processing error auth-method] + (let [on-press-biometrics (fn [] + (rf/dispatch + [:biometric/authenticate + {:on-success #(rf/dispatch + [:profile.login/biometric-success]) + :on-fail #(rf/dispatch + [:profile.login/biometric-auth-fail %])}]))] + [standard-authentication/password-input + {:processing processing + :error error + :shell? true + :blur? true + :on-press-biometrics (when (= auth-method constants/auth-method-biometric) + on-press-biometrics)}])) + +(defn- get-error-message + [error] + (if (and (some? error) + (or (= error "file is not a database") + (string/starts-with? (string/lower-case error) "failed"))) + (i18n/label :t/oops-wrong-password) + error)) + +(defn login-section + [{:keys [show-profiles]}] + (let [{:keys [top bottom]} safe-area/insets + {:keys [key-uid name keycard-pairing auth-method + customization-color]} (rf/sub [:profile/login-profile]) + sign-in-enabled? (rf/sub [:sign-in-enabled?]) + profile-picture (rf/sub [:profile/login-profiles-picture key-uid]) + {:keys [error processing]} (rf/sub [:profile/login]) + error-message (rn/use-memo #(get-error-message error) [error]) + error {:error? (boolean (seq error-message)) + :error-message error-message} + login-profile (rn/use-callback #(rf/dispatch [:profile.login/login]))] + [rn/keyboard-avoiding-view + (cond-> {:style (style/login-container top) + :keyboard-vertical-offset (- bottom)} + platform/android? (assoc :behavior :height)) + [rn/view {:style style/multi-profile-button-container} + (when config/debug-or-pr-build? + [quo/button + {:size 32 + :type :grey + :background :blur + :icon-only? true + :on-press #(rf/dispatch [:navigate-to :screen/quo-preview]) + :disabled? processing + :accessibility-label :quo-preview + :container-style {:margin-right 12}} + :i/reveal-whitelist]) + [quo/button + {:size 32 + :type :grey + :background :blur + :icon-only? true + :on-press show-profiles + :disabled? processing + :accessibility-label :show-profiles} + :i/multi-profile]] + [(if keycard-pairing rn/view rn/scroll-view) + {:keyboard-should-persist-taps :always + :style {:flex 1}} + [quo/profile-card + {:name name + :customization-color (or customization-color :primary) + :profile-picture profile-picture + :card-style style/login-profile-card}] + (if keycard-pairing + [keycard.pin/auth + {:error error + :on-complete + (fn [pin] + (rf/dispatch + [:keycard/connect + {:key-uid key-uid + :on-success + (fn [] + (rf/dispatch + [:keycard/get-keys + {:pin pin + :on-success #(rf/dispatch [:keycard.login/on-get-keys-success %]) + :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]))}]))}] + [password-input processing error auth-method])] + (when-not keycard-pairing + [quo/button + {:size 40 + :type :primary + :customization-color (or customization-color :primary) + :accessibility-label :login-button + :icon-left :i/unlocked + :disabled? (or (not sign-in-enabled?) processing) + :on-press login-profile + :container-style {:margin-bottom (+ bottom 12)}} + (i18n/label :t/log-in)])])) + +(defn view + [] + (let [{:keys [default-screen-profiles?]} (quo.context/use-screen-params) + [show-profiles? set-show-profiles] (rn/use-state default-screen-profiles?) + show-profiles (rn/use-callback #(set-show-profiles true)) + privacy-mode-enabled? (rf/sub [:privacy-mode/privacy-mode-enabled?]) + hide-profiles (rn/use-callback #(set-show-profiles false))] + (when-not privacy-mode-enabled? + (rn/use-mount #(rf/dispatch [:centralized-metrics/check-user-confirmation]))) + [:<> + [background/view true] + (if show-profiles? + [profiles-section {:hide-profiles hide-profiles}] + [login-section {:show-profiles show-profiles}])])) diff --git a/src/status_im/contexts/profile/push_notifications/effects.cljs b/src/status_im/contexts/profile/push_notifications/effects.cljs new file mode 100644 index 00000000000..95769affc2f --- /dev/null +++ b/src/status_im/contexts/profile/push_notifications/effects.cljs @@ -0,0 +1,141 @@ +(ns status-im.contexts.profile.push-notifications.effects + (:require + [native-module.push-notifications :as native-module.pn] + [promesa.core :as promesa] + [react-native.platform :as platform] + [status-im.common.log :as logger] + [status-im.config :as config] + [status-im.contexts.push-notifications.notifications :as pn-notifications] + [status-im.contexts.push-notifications.permissions :as pn-permissions] + [status-im.contexts.push-notifications.remote-token :as pn-remote-token] + [utils.re-frame :as rf])) + +(defn- attempt-enable-notifications + [] + (-> (pn-permissions/request-notification-permissions) + (promesa/then + (fn [permissions] + (if (:authorized? permissions) + permissions + (promesa/rejected + (ex-info + "Failed to authorize push notifications" + {:description "Requested and failed to authorize permissions for push notifications"}))))))) + +(defn- enable-news-notifications + [] + (-> (attempt-enable-notifications) + (promesa/then + (fn [_permissions] + (pn-remote-token/request-fcm-token {}))) + (promesa/then + (fn [{:keys [_fcm-token]}] + (logger/log-debug "registered fcm token for status news" + ;; NOTE(@seanstrom): temporarily avoid logging fcm-token + #_{:fcm-token fcm-token}))) + (promesa/catch + (fn [error] + (logger/log-debug "failed to register fcm token for status news" + {:error error}))))) + +(defn- disable-news-notifications + [] + (pn-remote-token/revoke-fcm-token {})) + +(defn- enable-chat-notifications-ios + [] + (-> (attempt-enable-notifications) + (promesa/then + (fn [_permissions] + (pn-remote-token/request-apns-token {}))) + (promesa/then + (fn [{:keys [apns-token]}] + ;; NOTE(seanstrom): We're only registering iOS devices for remote chat push notifications + ;; because Android will use local notifications from the background for chat + ;; notifications. This is a temporary workaround that attempts to prevent Android users + ;; from receiving two push notifications for each chat message when the app is running + ;; the background. This workaround can be removed after we support remote push + ;; notifications with message previews that decrypt locally. + (when platform/ios? + (rf/dispatch [:push-notifications/registered-for-push-notifications apns-token])))))) + +(defn- disable-chat-notifications-ios + [] + (pn-permissions/release-notification-permissions) + (rf/dispatch [:push-notifications/unregistered-from-push-notifications])) + +(defn- enable-ios-notifications + [settings] + (when (contains? settings :enable-chat-notifications?) + (enable-chat-notifications-ios)) + (when (contains? settings :enable-news-notifications?) + (enable-news-notifications))) + +(defn- disable-ios-notifications + [settings] + (when (contains? settings :disable-chat-notifications?) + (disable-chat-notifications-ios)) + (when (contains? settings :disable-news-notifications?) + (disable-news-notifications))) + +(defn- enable-chat-notifications-android + [] + (-> (attempt-enable-notifications) + (promesa/then + (fn [_permissions] + (native-module.pn/create-channel + {:channel-id "status-im-notifications" + :channel-name "Status push notifications"}) + (native-module.pn/enable-notifications))))) + +(defn- enable-android-notifications + [settings] + (when (and (not config/fdroid?) + (contains? settings :enable-news-notifications?)) + (enable-news-notifications)) + (when (contains? settings :enable-chat-notifications?) + (enable-chat-notifications-android))) + +(defn- enable-push-notifications + [settings] + (if platform/android? + (enable-android-notifications settings) + (enable-ios-notifications settings))) + +(defn- disable-android-notifications + [settings] + (when (contains? settings :disable-chat-notifications?) + (native-module.pn/disable-notifications) + (native-module.pn/clear-all-message-notifications)) + (when (contains? settings :disable-news-notifications?) + (disable-news-notifications))) + +(rf/reg-fx + :effects/push-notifications-enable + (fn [settings] + (enable-push-notifications settings))) + +(rf/reg-fx + :effects/push-notifications-disable + (fn [settings] + (if platform/android? + (disable-android-notifications settings) + (disable-ios-notifications settings)))) + +(rf/reg-fx + :effects/push-notifications-clear-message-notifications + (fn [chat-ids] + (if platform/android? + (doseq [chat-id chat-ids] + (native-module.pn/clear-message-notifications chat-id)) + (pn-notifications/clear-received-notifications)))) + +(rf/reg-fx :effects/check-notifications-permissions + (fn [{:keys [on-success on-error]}] + (-> (pn-permissions/check-notification-permissions) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(rf/reg-fx :effects/open-notifications-settings + (fn [] + (pn-permissions/open-notifications-settings))) diff --git a/src/status_im/contexts/profile/push_notifications/events.cljs b/src/status_im/contexts/profile/push_notifications/events.cljs new file mode 100644 index 00000000000..4195b78977d --- /dev/null +++ b/src/status_im/contexts/profile/push_notifications/events.cljs @@ -0,0 +1,229 @@ +(ns status-im.contexts.profile.push-notifications.events + (:require + [cljs-bean.core :as bean] + [legacy.status-im.multiaccounts.update.core :as multiaccounts.update] + [native-module.push-notifications :as native-module.pn] + [re-frame.core :as re-frame] + [react-native.async-storage :as async-storage] + [react-native.platform :as platform] + [react-native.push-notification-ios :as pn-ios] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.config :as config] + status-im.contexts.profile.push-notifications.effects + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(def apn-token-type 1) +(def firebase-token-type 2) + +(defn handle-notification-press + [{{deep-link :deepLink} :userInfo + interaction :userInteraction}] + (async-storage/set-item! (str :chat-id) nil #(rf/dispatch [:universal-links/remove-handling])) + (when (and deep-link (or platform/ios? (and platform/android? interaction))) + (rf/dispatch [:universal-links/handling]) + (rf/dispatch [:universal-links/handle-url deep-link]))) + +(defn listen-notifications + [] + (if platform/ios? + (pn-ios/add-listener "localNotification" + #(handle-notification-press {:userInfo (bean/bean (.getData ^js %))})) + (native-module.pn/add-listener "remoteNotificationReceived" + #(handle-notification-press (transforms/json->clj %))))) + +(rf/defn handle-enable-notifications-event + {:events [:push-notifications/registered-for-push-notifications]} + [_ token] + {:json-rpc/call [{:method "wakuext_registerForPushNotifications" + :params [token (when platform/ios? config/apn-topic) + (if platform/ios? apn-token-type firebase-token-type)] + :on-success #(log/info "[push-notifications] register-success" %) + :on-error #(log/info "[push-notifications] register-error" %)}]}) + +(rf/defn handle-disable-notifications-event + {:events [:push-notifications/unregistered-from-push-notifications]} + [_] + {:json-rpc/call [{:method "wakuext_unregisterFromPushNotifications" + :params [] + :on-success #(log/info "[push-notifications] unregister-success" %) + :on-error #(log/info "[push-notifications] unregister-error" %)}]}) + +(rf/defn handle-preferences-load + {:events [:push-notifications/preferences-loaded]} + [{:keys [db]} preferences] + {:db (assoc db :push-notifications/preferences preferences)}) + +(re-frame/reg-fx :push-notifications/load-preferences + (fn [] + (json-rpc/call {:method "localnotifications_notificationPreferences" + :params [] + :on-success [:push-notifications/preferences-loaded]}))) + +(rf/defn notification-non-contacts-error + {:events [:push-notifications/non-contacts-update-error]} + [cofx enabled?] + (multiaccounts.update/optimistic cofx + :push-notifications-from-contacts-only? + (not (boolean enabled?)))) + +(rf/defn notification-block-mentions-error + {:events [:push-notifications/block-mentions-update-error]} + [cofx enabled?] + (multiaccounts.update/optimistic cofx :push-notifications-block-mentions? (not (boolean enabled?)))) + +(rf/defn notification-non-contacts + {:events [:push-notifications/switch-non-contacts]} + [cofx enabled?] + (let [method (if enabled? + "wakuext_enablePushNotificationsFromContactsOnly" + "wakuext_disablePushNotificationsFromContactsOnly")] + (rf/merge + cofx + {:json-rpc/call [{:method method + :params [] + :on-success #(log/info "[push-notifications] contacts-notification-success" %) + :on-error #(log/info "[push-notifications] contacts-notification-error" %)}]} + (multiaccounts.update/optimistic :push-notifications-from-contacts-only? (boolean enabled?))))) + +(rf/defn notification-block-mentions + {:events [:push-notifications/switch-block-mentions]} + [cofx enabled?] + (let [method (if enabled? + "wakuext_enablePushNotificationsBlockMentions" + "wakuext_disablePushNotificationsBlockMentions")] + (rf/merge cofx + {:json-rpc/call [{:method method + :params [] + :on-success #(log/info "[push-notifications] block-mentions-success" %) + :on-error #(rf/dispatch + [:push-notifications/block-mentions-update-error enabled? + %])}]} + + (multiaccounts.update/optimistic :push-notifications-block-mentions? (boolean enabled?))))) + +(defn- change-profile-setting-db + [db {:keys [setting value]}] + (assoc-in db [:profile/profile setting] value)) + +(defn- save-profile-setting-fx + [{:keys [setting value on-success]}] + [:json-rpc/call + [{:method "settings_saveSetting" + :params [setting value] + :on-success (or on-success #())}]]) + +(defn notifications-switch + [{:keys [db]} [enabled?]] + (let [{:keys [notifications-enabled? + news-notifications-enabled? + messenger-notifications-enabled?]} (:profile/profile db) + enable-notifications? (and (not notifications-enabled?) enabled?) + disable-notifications? (and notifications-enabled? (not enabled?)) + should-enable-messenger-notifications? (and enabled? (nil? messenger-notifications-enabled?)) + permission-setting {:setting :notifications-enabled? + :value enabled?} + messenger-setting {:setting :messenger-notifications-enabled? + :value (if (some? messenger-notifications-enabled?) + messenger-notifications-enabled? + true)}] + {:db (cond-> db + should-enable-messenger-notifications? + (change-profile-setting-db messenger-setting) + :always + (change-profile-setting-db permission-setting)) + :fx [(cond + enable-notifications? + [:effects/push-notifications-enable + (cond-> #{} + (not (false? messenger-notifications-enabled?)) (conj :enable-chat-notifications?) + news-notifications-enabled? (conj :enable-news-notifications?))] + disable-notifications? + [:effects/push-notifications-disable + (cond-> #{} + news-notifications-enabled? (conj :disable-news-notifications?) + messenger-notifications-enabled? (conj :disable-chat-notifications?))] + :else nil) + (save-profile-setting-fx permission-setting) + (when should-enable-messenger-notifications? + (save-profile-setting-fx messenger-setting))]})) + +(rf/reg-event-fx :push-notifications/switch notifications-switch) + +(defn messenger-notifications-switch + [{:keys [db]} [enabled?]] + (let [prev-enabled? (get-in db [:profile/profile :messenger-notifications-enabled?]) + enable-chat-notifications? (and (not prev-enabled?) enabled?) + disable-chat-notifications? (and prev-enabled? (not enabled?)) + setting {:setting :messenger-notifications-enabled? + :value enabled?}] + {:db (change-profile-setting-db db setting) + :fx [(cond + enable-chat-notifications? + [:effects/push-notifications-enable + #{:enable-chat-notifications?}] + disable-chat-notifications? + [:effects/push-notifications-disable + #{:disable-chat-notifications?}] + :else nil) + (when (some? enabled?) + (save-profile-setting-fx setting))]})) + +(rf/reg-event-fx :notifications/messenger-notifications-switch messenger-notifications-switch) + +(defn news-notifications-switch + [{:keys [db]} [enabled?]] + (let [prev-enabled? (get-in db [:profile/profile :news-notifications-enabled?]) + enable-news-notifications? (and (not prev-enabled?) enabled?) + disable-news-notifications? (and prev-enabled? (not enabled?)) + setting {:setting :news-notifications-enabled? + :value enabled?}] + {:db (change-profile-setting-db db setting) + :fx [(cond + enable-news-notifications? + [:effects/push-notifications-enable + #{:enable-news-notifications?}] + disable-news-notifications? + [:effects/push-notifications-disable + #{:disable-news-notifications?}] + :else nil) + (when (some? enabled?) + (save-profile-setting-fx setting))]})) + +(rf/reg-event-fx :notifications/news-notifications-switch news-notifications-switch) + +(defn check-notifications-blocked + [_] + {:fx [[:effects/check-notifications-permissions + {:on-success [:notifications/determine-notifications-blocked] + :on-error [:log/debug "failed to check notification permissions"]}]]}) + +(rf/reg-event-fx :notifications/check-notifications-blocked check-notifications-blocked) + +(defn determine-notifications-blocked + [{:keys [db]} [{:keys [denied? undetermined? authorized?]}]] + (let [{:keys [notifications-enabled? + messenger-notifications-enabled? + news-notifications-enabled? + notifications-blocked?]} (get-in db [:profile/profile]) + blocked? (if platform/ios? + denied? + (and notifications-enabled? undetermined?))] + {:db (assoc-in db [:profile/profile :notifications-blocked?] (boolean blocked?)) + :fx [(when (and notifications-blocked? + authorized? + notifications-enabled? + messenger-notifications-enabled?) + [:effects/push-notifications-enable #{:enable-chat-notifications?}]) + (when (and notifications-blocked? + authorized? + notifications-enabled? + news-notifications-enabled?) + [:effects/push-notifications-enable #{:enable-news-notifications?}])]})) + +(rf/reg-event-fx :notifications/determine-notifications-blocked determine-notifications-blocked) + +(rf/reg-event-fx :notifications/open-notifications-settings + (fn [_ _] + {:fx [[:effects/open-notifications-settings]]})) diff --git a/src/status_im/contexts/profile/push_notifications/local/effects.cljs b/src/status_im/contexts/profile/push_notifications/local/effects.cljs new file mode 100644 index 00000000000..7bf2b0a1c05 --- /dev/null +++ b/src/status_im/contexts/profile/push_notifications/local/effects.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.profile.push-notifications.local.effects + (:require + [cljs-bean.core :as bean] + [native-module.push-notifications :as native-module.pn] + [react-native.push-notification-ios :as pn-ios] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects/push-notifications-local-present-ios + (fn [{:keys [title message user-info body-type]}] + (when (not= body-type "message") + (pn-ios/present-local-notification title + message + (bean/->js (merge user-info + {:notificationType + "local-notification"})))))) + +(rf/reg-fx :effects/push-notifications-local-present-android + (fn [notification] + (native-module.pn/present-local-notification notification))) diff --git a/src/status_im/contexts/profile/push_notifications/local/events.cljs b/src/status_im/contexts/profile/push_notifications/local/events.cljs new file mode 100644 index 00000000000..8840e538abd --- /dev/null +++ b/src/status_im/contexts/profile/push_notifications/local/events.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.profile.push-notifications.local.events + (:require + [legacy.status-im.notifications.wallet :as notifications.wallet] + [react-native.platform :as platform] + [status-im.constants :as constants] + status-im.contexts.profile.push-notifications.local.effects + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:private push-notification-types + #{"transaction" "message"}) + +(defn foreground-chat? + [{{:keys [current-chat-id view-id]} :db} chat-id] + (and (= current-chat-id chat-id) + (= view-id :screen/chat))) + +(defn show-message-pn? + [{{:keys [app-state profile/profile]} :db :as cofx} + notification] + (let [chat-id (get-in notification [:body :chat :id]) + notification-author (get-in notification [:notificationAuthor :id])] + (and + (not= notification-author (:public-key profile)) + (or (= app-state "background") + (not (foreground-chat? cofx chat-id)))))) + +(defn show-community-joined-toast + [{:keys [bodyType title]}] + (when (= bodyType constants/community-joined-notification-type) + {:dispatch [:toasts/upsert + {:id :joined-community + :type :positive + :text (i18n/label :t/joined-community {:community title})}]})) + +(defn create-notification + [cofx {:keys [bodyType] :as notification}] + (assoc + (case bodyType + "message" (when (show-message-pn? cofx notification) notification) + "transaction" (notifications.wallet/create-transfer-notification cofx notification) + nil) + :body-type + bodyType)) + +(rf/defn process + [cofx {:keys [bodyType] :as event}] + (if (push-notification-types bodyType) + (if platform/ios? + {:effects/push-notifications-local-present-ios (create-notification nil event)} + {:effects/push-notifications-local-present-android (create-notification cofx event)}) + (show-community-joined-toast event))) diff --git a/src/status_im/contexts/profile/recover/effects.cljs b/src/status_im/contexts/profile/recover/effects.cljs new file mode 100644 index 00000000000..42b7858d759 --- /dev/null +++ b/src/status_im/contexts/profile/recover/effects.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.profile.recover.effects + (:require + [native-module.core :as native-module] + [promesa.core :as promesa] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(rf/reg-fx :effects.profile/restore-and-login + (fn [request] + ;;"node.login" signal will be triggered as a callback + (native-module/restore-account-and-login request))) + +(defn validate-mnemonic + [mnemonic] + (-> mnemonic + (security/safe-unmask-data) + (native-module/validate-mnemonic) + (promesa/then (fn [result] + (let [{:keys [keyUID]} (transforms/json->clj result)] + {:key-uid keyUID}))))) + +(rf/reg-fx :effects.profile/validate-recovery-phrase + (fn [[mnemonic on-success on-error]] + (-> (validate-mnemonic mnemonic) + (promesa/then (fn [{:keys [key-uid]}] + (when (fn? on-success) + (on-success mnemonic key-uid)))) + (promesa/catch (fn [error] + (when (and error (fn? on-error)) + (on-error error))))))) diff --git a/src/status_im/contexts/profile/recover/events.cljs b/src/status_im/contexts/profile/recover/events.cljs new file mode 100644 index 00000000000..a514816bd0c --- /dev/null +++ b/src/status_im/contexts/profile/recover/events.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.profile.recover.events + (:require + [native-module.core :as native-module] + [status-im.constants :as constants] + [status-im.contexts.profile.config :as profile.config] + status-im.contexts.profile.recover.effects + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :profile.recover/recover-and-login + (fn [{:keys [db]} [{:keys [display-name password image-path color seed-phrase]}]] + (let [login-sha3-password (native-module/sha3 (security/safe-unmask-data password))] + {:db + (-> db + (assoc :onboarding/recovered-account? true) + (assoc-in [:syncing :login-sha3-password] login-sha3-password)) + :fx + [[:effects.profile/restore-and-login + (assoc (profile.config/create) + :displayName display-name + :mnemonic (security/safe-unmask-data seed-phrase) + :password login-sha3-password + :imagePath (profile.config/strip-file-prefix image-path) + :customizationColor (or color constants/profile-default-color) + :fetchBackup true)]]}))) + +(rf/reg-event-fx :profile.recover/validate-recovery-phrase + (fn [_ [phrase {:keys [on-success on-error]}]] + {:effects.profile/validate-recovery-phrase [phrase on-success on-error]})) diff --git a/src/status_im/contexts/profile/rpc.cljs b/src/status_im/contexts/profile/rpc.cljs new file mode 100644 index 00000000000..d35156e72db --- /dev/null +++ b/src/status_im/contexts/profile/rpc.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.profile.rpc + (:require + clojure.set + [clojure.string :as string] + [utils.ens.core :as utils.ens])) + +(defn rpc->wakuv2-config + [wakuv2-config] + (clojure.set/rename-keys wakuv2-config {:LightClient :light-client})) + +(defn rpc->profiles-overview + [{:keys [customizationColor keycard-pairing] :as profile}] + (if (map? profile) + (-> profile + (update :wakuv2-config rpc->wakuv2-config) + (dissoc :customizationColor) + (assoc :customization-color (keyword customizationColor)) + (assoc :ens-name? (utils.ens/is-valid-eth-name? (:name profile))) + (assoc :keycard-pairing (when-not (string/blank? keycard-pairing) keycard-pairing))) + profile)) diff --git a/src/status_im/contexts/profile/settings/effects.cljs b/src/status_im/contexts/profile/settings/effects.cljs new file mode 100644 index 00000000000..ac44d745447 --- /dev/null +++ b/src/status_im/contexts/profile/settings/effects.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.profile.settings.effects + (:require [native-module.core :as native-module] + [re-frame.core :as re-frame] + [react-native.platform :as platform])) + +(re-frame/reg-fx + :profile.settings/blank-preview-flag-changed + (fn [flag] + (native-module/set-blank-preview-flag flag))) + +(re-frame/reg-fx + :profile.settings/webview-debug-changed + (fn [value] + (when platform/android? + (native-module/toggle-webview-debug value)))) diff --git a/src/status_im/contexts/profile/settings/events.cljs b/src/status_im/contexts/profile/settings/events.cljs new file mode 100644 index 00000000000..8bb203d1f1f --- /dev/null +++ b/src/status_im/contexts/profile/settings/events.cljs @@ -0,0 +1,175 @@ +(ns status-im.contexts.profile.settings.events + (:require [clojure.string :as string] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.constants :as constants] + [status-im.contexts.profile.db :as profile.db] + status-im.contexts.profile.settings.effects + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- set-setting-value + [db setting setting-value] + (if setting-value + (assoc-in db [:profile/profile setting] setting-value) + (update db :profile/profile dissoc setting))) + +(rf/reg-event-fx :profile.settings/profile-update + (fn [{:keys [db]} [setting setting-value {:keys [dont-sync? on-success]}]] + {:db (set-setting-value db setting setting-value) + :fx [[:json-rpc/call + [{:method "settings_saveSetting" + :params [setting setting-value] + :on-success on-success}]] + + (when (#{:name :preferred-name} setting) + [:profile/get-profiles-overview #(rf/dispatch [:multiaccounts.ui/update-name %])]) + + (when (and (not dont-sync?) (#{:name :preferred-name} setting)) + (let [{:keys [name preferred-name display-name]} (:profile/profile db)] + [:json-rpc/call + [{:method "wakuext_sendContactUpdates" + :params [(or preferred-name display-name name) "" ""] + :on-success #(log/debug "sent contact update")}]]))]})) + +(rf/reg-event-fx :profile.settings/change-webview-debug + (fn [_ [value]] + (let [value' (boolean value)] + {:fx [[:dispatch [:profile.settings/profile-update :webview-debug value']] + [:profile.settings/webview-debug-changed value']]}))) + +(rf/reg-event-fx :profile.settings/toggle-test-networks + (fn [{:keys [db]}] + (let [test-networks-disabled? (not (profile.db/testnet? db))] + {:fx [[:ui/show-confirmation + {:title (i18n/label :t/testnet-mode-prompt-title) + :content (i18n/label :t/testnet-mode-prompt-content) + :on-accept (fn [] + (rf/dispatch [:profile.settings/profile-update :test-networks-enabled? + test-networks-disabled? + {:on-success #(rf/dispatch [:profile/logout])}])) + :on-cancel nil}]]}))) + +(rf/reg-event-fx :profile.settings/change-preview-privacy + (fn [_ [private?]] + (let [private?' (boolean private?)] + {:fx [[:dispatch [:profile.settings/profile-update :preview-privacy? private?']] + [:profile.settings/blank-preview-flag-changed private?']]}))) + +(rf/reg-event-fx :profile.settings/change-profile-pictures-show-to + (fn [{:keys [db]} [id]] + {:db (assoc-in db [:profile/profile :profile-pictures-show-to] id) + :fx [[:json-rpc/call + [{:method "wakuext_changeIdentityImageShowTo" + :params [id] + :on-success #(log/debug "picture settings changed successfully")}]]]})) + +(rf/reg-event-fx :profile.settings/toggle-peer-syncing + (fn [{:keys [db]}] + (let [value (get-in db [:profile/profile :peer-syncing-enabled?]) + new-value (not value)] + {:db (assoc-in db [:profile/profile :peer-syncing-enabled?] new-value) + :fx [[:json-rpc/call + [{:method "wakuext_togglePeerSyncing" + :params [{:enabled new-value}] + :on-error #(log/error "failed to toggle peer syncing" new-value %)}]]]}))) + +(rf/reg-event-fx :profile.settings/toggle-telemetry + (fn [{:keys [db]} [enable?]] + (let [enable? (if (nil? enable?) + (string/blank? (get-in db [:profile/profile :telemetry-server-url])) + enable?) + new-value (if enable? constants/default-telemetry-server-url "")] + {:dispatch [:profile.settings/profile-update :telemetry-server-url new-value]}))) + +(rf/reg-event-fx :profile.settings/change-appearance + (fn [_ [theme]] + {:fx [[:dispatch [:profile.settings/profile-update :appearance theme]] + [:dispatch [:theme/switch {:appearance-type theme}]]]})) + +(rf/reg-fx :profile.settings/get-profile-picture + (fn [key-uid] + (json-rpc/call {:method "multiaccounts_getIdentityImages" + :params [key-uid] + :on-success [:profile.settings/update-local-picture]}))) + +(rf/reg-event-fx :profile.settings/save-profile-picture + (fn [{:keys [db]} [path ax ay bx by]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:db (assoc db :bottom-sheet/show? false) + :fx [[:json-rpc/call + [{:method "multiaccounts_storeIdentityImage" + :params [key-uid (string/replace-first path #"file://" "") ax ay bx + by] + :on-success [:profile.settings/update-local-picture]}]] + [:dispatch [:hide-bottom-sheet]]]}))) + +(rf/reg-event-fx :profile.settings/save-profile-picture-from-url + (fn [{:keys [db]} [url]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:db (assoc db :bottom-sheet/show? false) + :fx [[:json-rpc/call + [{:method "multiaccounts_storeIdentityImageFromURL" + :params [key-uid url] + :on-error #(log/error "::save-profile-picture-from-url error" %) + :on-success [:profile.settings/update-local-picture]}]] + [:dispatch [:hide-bottom-sheet]]]}))) + +(rf/reg-event-fx :profile.settings/delete-profile-picture + (fn [{:keys [db]}] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:db (-> db + (update :profile/profile dissoc :images) + (assoc :bottom-sheet/show? false)) + :fx [[:json-rpc/call + [{:method "multiaccounts_deleteIdentityImage" + :params [key-uid] + ;; NOTE: In case of an error we could fallback to previous image in + ;; UI with a toast error + :on-success #(log/info "[profile] Delete profile image" %)}]] + [:dispatch [:hide-bottom-sheet]]]}))) + +(rf/reg-event-fx :profile.settings/update-local-picture + (fn [{:keys [db]} [images]] + {:db (assoc-in db [:profile/profile :images] images)})) + +(rf/reg-event-fx :profile.settings/mnemonic-was-shown + (fn [_] + {:fx [[:json-rpc/call + [{:method "settings_mnemonicWasShown" + :on-success #(log/debug "mnemonic was marked as shown") + :on-error #(log/error "mnemonic was not marked as shown" %)}]]]})) + +(defn token-symbols-from-accounts + [accounts] + (reduce-kv (fn [acc _ account] + (into acc (map :symbol (:tokens account)))) + #{} + accounts)) + +(rf/reg-event-fx :profile.settings/update-currency + (fn [{:keys [db]} [currency]] + (let [accounts (get-in db [:wallet :accounts]) + symbols (token-symbols-from-accounts accounts)] + {:fx [[:dispatch [:profile.settings/profile-update :currency currency]] + [:effects.wallet.tokens/fetch-market-values + {:symbols symbols + :currency currency + :on-success [:wallet.tokens/store-market-values] + :on-error [:wallet.tokens/fetch-market-values-failed]}] + [:effects.wallet.tokens/fetch-prices + {:symbols symbols + :currencies [constants/profile-default-currency currency] + :on-success [:wallet.tokens/store-prices] + :on-error [:wallet.tokens/fetch-prices-failed]}]]}))) + +;; Logout process +(rf/reg-event-fx + :profile.settings/ask-logout + (fn [_ _] + {:fx [[:ui/show-confirmation + {:title (i18n/label :t/logout-title) + :content (i18n/label :t/logout-are-you-sure) + :confirm-button-text (i18n/label :t/logout) + :on-accept #(rf/dispatch [:profile/logout]) + :on-cancel nil}]]})) diff --git a/src/status_im/contexts/profile/settings/header/header_shape.cljs b/src/status_im/contexts/profile/settings/header/header_shape.cljs new file mode 100644 index 00000000000..3f1d17e3c12 --- /dev/null +++ b/src/status_im/contexts/profile/settings/header/header_shape.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.profile.settings.header.header-shape + (:require [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [react-native.svg :as svg] + [status-im.contexts.profile.settings.header.style :as style])) + +(defn left-radius + [background-color] + [svg/svg {:width "20" :height "20" :viewBox "0 0 20 20" :fill "none"} + [svg/path + {:d "M20 0C7 2 0 10 0 20V0H15Z" + :fill background-color}]]) + +(defn right-radius + [background-color] + [svg/svg {:width "20" :height "21" :viewBox "0 0 20 21" :fill "none"} + [svg/path + {:d "M20 20V0H0C11 0 20 9 20 20Z" + :fill background-color}]]) + +(defn view + [{:keys [scroll-y customization-color theme]}] + (let [background-color (colors/resolve-color customization-color theme 40) + opacity-animation (reanimated/interpolate scroll-y + [0 45 50] + [1 1 0])] + [:<> + [rn/view {:style (style/header-middle-shape background-color)}] + [reanimated/view {:style (style/radius-container opacity-animation)} + [left-radius background-color] + [right-radius background-color]]])) diff --git a/src/status_im/contexts/profile/settings/header/style.cljs b/src/status_im/contexts/profile/settings/header/style.cljs new file mode 100644 index 00000000000..52a60b59e07 --- /dev/null +++ b/src/status_im/contexts/profile/settings/header/style.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.profile.settings.header.style + (:require [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def avatar-row-wrapper + {:padding-left 20 + :padding-right 12 + :margin-top -65 + :align-items :flex-end + :justify-content :space-between + :flex-direction :row}) + +(defn header-middle-shape + [background-color] + {:background-color background-color + :height 48 + :flex-grow 1}) + +(defn radius-container + [opacity-animation] + {:opacity opacity-animation + :flex-direction :row + :justify-content :space-between}) + +(defn avatar-border-color + [theme] + (if platform/android? + colors/neutral-80-opa-80 ;; Fix is not needed because Android doesn't use blur + (colors/theme-colors colors/border-avatar-light colors/neutral-80-opa-80 theme))) diff --git a/src/status_im/contexts/profile/settings/header/utils.cljs b/src/status_im/contexts/profile/settings/header/utils.cljs new file mode 100644 index 00000000000..e10d6b3bb1d --- /dev/null +++ b/src/status_im/contexts/profile/settings/header/utils.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.profile.settings.header.utils + (:require [status-im.constants :as constants] + [utils.i18n :as i18n])) + +(defn visibility-status-type-data + [{:keys [status-type]}] + (condp = status-type + constants/visibility-status-automatic + {:status-title (i18n/label :t/status-automatic) + :status-icon :i/automatic} + + constants/visibility-status-always-online + {:status-title (i18n/label :t/online) + :status-icon :i/online} + + constants/visibility-status-inactive + {:status-title (i18n/label :t/offline) + :status-icon :i/offline} + + {:status-title (i18n/label :t/offline) + :status-icon :i/offline})) diff --git a/src/status_im/contexts/profile/settings/header/view.cljs b/src/status_im/contexts/profile/settings/header/view.cljs new file mode 100644 index 00000000000..1673fe79824 --- /dev/null +++ b/src/status_im/contexts/profile/settings/header/view.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.profile.settings.header.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.scalable-avatar.view :as avatar] + [status-im.contexts.profile.settings.header.header-shape :as header.shape] + [status-im.contexts.profile.settings.header.style :as style] + [status-im.contexts.profile.settings.header.utils :as header.utils] + [status-im.contexts.profile.settings.visibility-sheet.view :as visibility-sheet] + [status-im.contexts.profile.utils :as profile.utils] + [utils.re-frame :as rf])) + +(defn- on-state-dropdown-press + [] + (rf/dispatch [:show-bottom-sheet + {:shell? true + :theme :dark + :content visibility-sheet/view}])) + +(defn view + [{:keys [scroll-y]}] + (let [app-theme (rf/sub [:theme]) + {:keys [public-key emoji-hash bio] + :as profile} (rf/sub [:profile/profile-with-image]) + online? (rf/sub [:visibility-status-updates/online? public-key]) + status (rf/sub [:visibility-status-updates/visibility-status-update public-key]) + customization-color (rf/sub [:profile/customization-color]) + full-name (profile.utils/displayed-name profile) + profile-picture (profile.utils/photo profile) + {:keys [status-title + status-icon]} (header.utils/visibility-status-type-data status)] + [:<> + [header.shape/view + {:scroll-y scroll-y + :customization-color customization-color}] + [rn/view {:style style/avatar-row-wrapper} + [avatar/view + {:scroll-y scroll-y + :full-name full-name + :online? online? + :border-color (style/avatar-border-color app-theme) + :customization-color customization-color + :profile-picture profile-picture}] + [quo/dropdown + {:background :blur + :size :size-32 + :type :outline + :icon? true + :no-icon-color? true + :icon-name status-icon + :on-press on-state-dropdown-press} + status-title]] + [quo/page-top + {:title-accessibility-label :username + :emoji-dash emoji-hash + :description :text + :description-text bio + :title full-name}]])) diff --git a/src/status_im/contexts/profile/settings/list_items.cljs b/src/status_im/contexts/profile/settings/list_items.cljs new file mode 100644 index 00000000000..d6cd388cbd1 --- /dev/null +++ b/src/status_im/contexts/profile/settings/list_items.cljs @@ -0,0 +1,133 @@ +(ns status-im.contexts.profile.settings.list-items + (:require [status-im.common.not-implemented :as not-implemented] + [status-im.config :as config] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn items + [mnemonic on-backup-seed-press] + [(when mnemonic + [{:title (i18n/label :t/back-up-seed-phrase) + :on-press (on-backup-seed-press mnemonic) + :image-props :i/seed + :image :icon + :label :icon + :label-props :i/danger + :label-icon-props {:no-color true} + :blur? true + :action :arrow}]) + [{:title (i18n/label :t/edit-profile) + :on-press #(rf/dispatch [:open-modal :screen/edit-profile]) + :image-props :i/edit + :image :icon + :blur? true + :action :arrow} + {:title (i18n/label :t/password) + :on-press #(rf/dispatch [:open-modal :screen/settings-password]) + :image-props :i/password + :image :icon + :blur? true + :action :arrow}] + [{:title (i18n/label :t/messages) + :on-press #(rf/dispatch [:open-modal :screen/settings-messages]) + :image-props :i/messages + :image :icon + :blur? true + :action :arrow} + {:title (i18n/label :t/wallet) + :on-press #(rf/dispatch [:open-modal :screen/settings.wallet]) + :image-props :i/wallet + :image :icon + :blur? true + :action :arrow} + (when config/show-not-implemented-features? + {:title (i18n/label :t/dapps) + :on-press not-implemented/alert + :image-props :i/dapps + :image :icon + :blur? true + :action :arrow}) + (when config/show-not-implemented-features? + {:title (i18n/label :t/browser) + :on-press not-implemented/alert + :image-props :i/browser + :image :icon + :blur? true + :action :arrow}) + {:title (i18n/label :t/keycard) + :on-press #(rf/dispatch [:open-modal :screen/settings.keycard]) + :image-props :i/keycard + :image :icon + :blur? true + :action :arrow}] + [{:title (i18n/label :t/network-settings) + :blur? true + :image :icon + :image-props :i/networks + :on-press #(rf/dispatch [:open-modal :screen/settings.network-settings]) + :action :arrow} + {:title (i18n/label :t/privacy-and-security) + :on-press #(rf/dispatch [:open-modal :screen/settings-privacy-and-security]) + :image-props :i/privacy + :image :icon + :blur? true + :action :arrow} + {:title (i18n/label :t/syncing) + :on-press #(rf/dispatch [:open-modal :screen/settings.syncing]) + :image-props :i/syncing + :image :icon + :blur? true + :action :arrow} + {:title (i18n/label :t/notifications) + :on-press #(rf/dispatch [:open-modal :screen/settings.notifications]) + :image-props :i/activity-center + :image :icon + :blur? true + :action :arrow} + {:title (i18n/label :t/appearance) + :on-press #(rf/dispatch [:open-modal :screen/legacy-appearance]) + :image-props :i/light + :image :icon + :blur? true + :action :arrow} + {:title (i18n/label :t/language-and-currency) + :on-press #(rf/dispatch [:open-modal :screen/settings.language-and-currency]) + :image-props :i/globe + :image :icon + :blur? true + :action :arrow}] + [(when config/show-not-implemented-features? + {:title (i18n/label :t/data-usage) + :on-press not-implemented/alert + :image-props :i/mobile + :image :icon + :blur? true + :action :arrow}) + {:title (i18n/label :t/advanced) + :on-press #(rf/dispatch [:open-modal :screen/settings.advanced]) + :image-props :i/settings + :image :icon + :blur? true + :action :arrow}] + [(when config/debug-or-pr-build? + {:title "Quo preview" + :on-press #(rf/dispatch [:open-modal :screen/quo-preview]) + :action :arrow + :image :icon + :blur? true + :image-props :i/light}) + (when config/debug-or-pr-build? + {:title (i18n/label :t/feature-flags) + :on-press #(rf/dispatch [:open-modal :screen/feature-flags]) + :action :arrow + :image :icon + :blur? true + :image-props :i/light})] + [{:title (i18n/label :t/about) + :on-press #(rf/dispatch [:open-modal :screen/settings.about]) + :action :arrow + :blur? true} + {:title (i18n/label :t/status-help) + :on-press #(rf/dispatch [:open-modal :screen/help-center]) + :action :arrow + :blur? true}]]) diff --git a/src/status_im/contexts/profile/settings/screens/messages/blocked_users/view.cljs b/src/status_im/contexts/profile/settings/screens/messages/blocked_users/view.cljs new file mode 100644 index 00000000000..5b7b322da06 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/messages/blocked_users/view.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.profile.settings.screens.messages.blocked-users.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.contact-list-item.view :as contact-list-item] + [status-im.contexts.profile.contact.unblock-contact.view :as unblock-contact] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn list-item + [user _ _ _] + [contact-list-item/contact-list-item + {:accessory {:type :custom + :child [quo/button + {:on-press #(rf/dispatch [:show-bottom-sheet + {:theme :dark + :content + (fn [] [unblock-contact/view user])}]) + :type :outline + :size 24 + :background :blur + :customization-color :blue} + (i18n/label :t/unblock)]}} + user]) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [contacts (rf/sub [:contacts/blocked])] + [quo/overlay {:type :shell :top-inset? true} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [quo/page-top {:title (i18n/label :t/blocked-users)}] + [rn/flat-list + {:data contacts + :key-fn :key + :render-fn list-item + :accessibility-label :contacts-list}]])) diff --git a/src/status_im/contexts/profile/settings/screens/messages/view.cljs b/src/status_im/contexts/profile/settings/screens/messages/view.cljs new file mode 100644 index 00000000000..eece5ce4e61 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/messages/view.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.profile.settings.screens.messages.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- open-blocked-users + [] + (rf/dispatch [:open-modal :screen/settings-blocked-users])) + +(defn view + [] + (let [allow-new-contact-requests? (rf/sub [:profile/allow-new-contact-requests?]) + toggle-allow-new-contact-requests (rn/use-callback + (fn [] + (rf/dispatch [:profile/update-messages-from-contacts-only])) + [allow-new-contact-requests?])] + [quo/overlay {:type :shell :top-inset? true} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/page-top {:title (i18n/label :t/messages)}] + [quo/category + {:label (i18n/label :t/contacts) + :data [{:title (i18n/label :t/allow-new-contact-requests) + :blur? true + :action :selector + :action-props {:on-change toggle-allow-new-contact-requests + :checked? allow-new-contact-requests?}} + {:title (i18n/label :t/blocked-users) + :on-press open-blocked-users + :blur? true + :action :arrow}] + :blur? true + :list-type :settings}]])) diff --git a/src/status_im/contexts/profile/settings/screens/notifications/styles.cljs b/src/status_im/contexts/profile/settings/screens/notifications/styles.cljs new file mode 100644 index 00000000000..f9bccb8030f --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/notifications/styles.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.profile.settings.screens.notifications.styles + (:require + [quo.foundations.colors :as colors])) + +(def information-box + {:margin-horizontal 20 + :margin-vertical 12}) + +(def information-box-button-label + {:gap 4 + :align-items :center + :flex-direction :row}) + +(def settings-group-container + {:margin-horizontal 20 + :margin-vertical 8 + :border-width 1 + :border-radius 20 + :border-color colors/white-opa-5}) + +(def settings-group-header + {:flex-direction :row + :padding-left 16 + :padding-right 12 + :padding-vertical 13 + :gap 12}) + +(def settings-group-item-container + {:padding-horizontal 8 + :padding-top 0 + :paddong-bottom 0}) diff --git a/src/status_im/contexts/profile/settings/screens/notifications/view.cljs b/src/status_im/contexts/profile/settings/screens/notifications/view.cljs new file mode 100644 index 00000000000..cbad467257f --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/notifications/view.cljs @@ -0,0 +1,201 @@ +(ns status-im.contexts.profile.settings.screens.notifications.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.common.events-helper :as events-helper] + [status-im.config :as config] + [status-im.contexts.profile.settings.screens.notifications.styles :as styles] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn toggle-notifications-enabled + [notifications-enabled?] + (rf/dispatch [:push-notifications/switch (not notifications-enabled?)])) + +(defn toggle-messenger-notifications + [messenger-notifications-enabled?] + (rf/dispatch [:notifications/messenger-notifications-switch (not messenger-notifications-enabled?)])) + +(defn toggle-news-notifications + [news-notifications-enabled?] + (rf/dispatch [:notifications/news-notifications-switch (not news-notifications-enabled?)])) + +(defn toggle-non-contact-notifications + [non-contact-notifications-enabled?] + (rf/dispatch [:push-notifications/switch-non-contacts non-contact-notifications-enabled?])) + +(defn toggle-community-mentions-notifications + [community-mentions-notifications-enabled?] + (rf/dispatch [:push-notifications/switch-block-mentions community-mentions-notifications-enabled?])) + +(defn notifications-enabled-setting + [{:keys [notifications-blocked? notifications-enabled?]}] + (let [on-change (rn/use-callback + #(toggle-notifications-enabled notifications-enabled?) + [notifications-enabled?])] + {:blur? true + :title (i18n/label :t/show-notifications) + :action :selector + :action-props {:on-change on-change + :checked? (and (not notifications-blocked?) + notifications-enabled?)}})) + +(defn chat-non-contacts-notifications-setting + [{:keys [notifications-enabled? + messenger-notifications-enabled? + non-contact-notifications-enabled?]}] + (let [disabled? (or (not notifications-enabled?) + (not messenger-notifications-enabled?)) + on-change (rn/use-callback + #(toggle-non-contact-notifications non-contact-notifications-enabled?) + [non-contact-notifications-enabled?])] + {:blur? true + :title (i18n/label :t/notifications-non-contacts) + :description :text + :description-props {:text (i18n/label :t/notifications-non-contacts-description)} + :action :selector + :action-props {:on-change (when-not disabled? on-change) + :disabled? disabled? + :checked? (if (ff/enabled? ::ff/settings.news-notifications) + non-contact-notifications-enabled? + (and (not disabled?) + non-contact-notifications-enabled?))}})) + +(defn chat-community-mentions-notifications-setting + [{:keys [notifications-enabled? + messenger-notifications-enabled? + community-mentions-notifications-enabled?]}] + (let [disabled? (or (not notifications-enabled?) + (not messenger-notifications-enabled?)) + on-change (rn/use-callback + #(toggle-community-mentions-notifications community-mentions-notifications-enabled?) + [community-mentions-notifications-enabled?])] + {:blur? true + :title (i18n/label :t/communities) + :description :text + :description-props {:text (i18n/label :t/allow-community-mentions-notifications-description)} + :action :selector + :action-props {:on-change (when-not disabled? on-change) + :disabled? disabled? + :checked? (if (ff/enabled? ::ff/settings.news-notifications) + community-mentions-notifications-enabled? + (and (not disabled?) + community-mentions-notifications-enabled?))}})) + +(defn messenger-notifications-setting + [{:keys [notifications-enabled? messenger-notifications-enabled?]}] + (let [disabled? (not notifications-enabled?) + on-change (rn/use-callback + #(toggle-messenger-notifications messenger-notifications-enabled?) + [messenger-notifications-enabled?])] + (cond-> {:blur? true + :title (i18n/label :t/allow-messenger-notifications) + :action :selector + :action-props {:on-change (when-not disabled? on-change) + :disabled? disabled? + :checked? messenger-notifications-enabled?}} + platform/android? + (assoc :title (i18n/label :t/allow-messages-and-communities-notifications) + :description :text + :description-props {:text (i18n/label + :t/allow-messages-and-communities-notifications-description)})))) + +(defn news-notifications-setting + [{:keys [notifications-enabled? news-notifications-enabled?]}] + (let [disabled? (not notifications-enabled?) + on-change (rn/use-callback + #(toggle-news-notifications news-notifications-enabled?) + [news-notifications-enabled?])] + (cond-> {:blur? true + :title (i18n/label :t/allow-news-notifications) + :action :selector + :action-props {:on-change (when-not disabled? on-change) + :disabled? disabled? + :checked? news-notifications-enabled?}} + platform/android? + (assoc :description :text + :description-props {:text (i18n/label :t/allow-news-notifications-description)})))) + +(defn- settings-group-item + [item & _rest] + [quo/category + {:blur? true + :list-type :settings + :container-style styles/settings-group-item-container + :data [item]}]) + +(defn- messenger-notifications-settings + [notifications-settings] + (let [{:keys [action-props title]} (messenger-notifications-setting notifications-settings)] + [rn/pressable + {:on-press (:on-change action-props) + :style styles/settings-group-container} + [rn/view + {:style styles/settings-group-header} + [quo/text {:style {:flex 1}} title] + [quo/selectors + {:type :toggle + :checked? (:checked? action-props) + :disabled? (:disabled? action-props) + :on-change (:on-change action-props)}]] + [rn/flat-list + {:data [(chat-non-contacts-notifications-setting notifications-settings) + (chat-community-mentions-notifications-setting notifications-settings)] + :render-fn settings-group-item + :separator [rn/view {:style {:height 0}}]}]])) + +(defn view + [] + (let [notifications-settings (rf/sub [:profile/notifications-settings])] + (rn/use-mount #(rf/dispatch [:notifications/check-notifications-blocked])) + [quo/overlay {:type :shell :top-inset? true} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/page-top {:title (i18n/label :t/notifications)}] + (when (:notifications-blocked? notifications-settings) + [quo/information-box + {:type :error + :style styles/information-box + :blur? false + :on-button-press #(rf/dispatch [:notifications/open-notifications-settings]) + :button-label [rn/view {:style styles/information-box-button-label} + [quo/text {:size :paragraph-2} + (i18n/label :t/enabled-push-notifications)] + [quo/icon :i/external {:size 12}]]} + (i18n/label (if platform/ios? + :t/push-notifications-blocked-ios + :t/push-notifications-blocked-android))]) + [quo/category + {:blur? true + :list-type :settings + :data [(notifications-enabled-setting notifications-settings)]}] + (if (ff/enabled? ::ff/settings.news-notifications) + [:<> + (cond + platform/ios? + [messenger-notifications-settings notifications-settings] + + platform/android? + [quo/category + {:blur? true + :list-type :settings + :data [(messenger-notifications-setting notifications-settings)]}] + + :else nil) + + (when-not config/fdroid? + [quo/category + {:blur? true + :list-type :settings + :data [(news-notifications-setting notifications-settings)]}])] + + (when platform/ios? + [quo/category + {:blur? true + :list-type :settings + :data [(chat-non-contacts-notifications-setting notifications-settings) + (chat-community-mentions-notifications-setting notifications-settings)]}]))])) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/effects.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/effects.cljs new file mode 100644 index 00000000000..0993878d025 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/effects.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.effects + (:require [native-module.core :as native-module] + [promesa.core :as promesa] + [status-im.common.keychain.events :as keychain] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-fx :effects.change-password/change-password + (fn [{:keys [key-uid old-password new-password on-success on-fail]}] + (let [hash-masked-password (fn [password] + (-> password security/hash-masked-password security/safe-unmask-data)) + old-password-hashed (hash-masked-password old-password) + new-password-hashed (hash-masked-password new-password)] + (-> (native-module/reset-password key-uid old-password-hashed new-password-hashed) + (promesa/then #(keychain/save-user-password! key-uid new-password-hashed)) + (promesa/then on-success) + (promesa/catch on-fail))))) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/events.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/events.cljs new file mode 100644 index 00000000000..09624b4df5b --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/events.cljs @@ -0,0 +1,68 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.events + (:require [status-im.contexts.profile.settings.screens.password.change-password.effects] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx :change-password/verify-old-password + (fn [_ [entered-password]] + (let [hashed-password (-> entered-password + security/hash-masked-password + security/safe-unmask-data)] + {:json-rpc/call [{:method "accounts_verifyPassword" + :params [hashed-password] + :on-success (fn [valid?] + (rf/dispatch [:change-password/password-verify-success valid? + entered-password])) + :on-error (fn [error] + (log/error "accounts_verifyPassword error" + {:error error + :event :password-settings/change-password}))}]}))) +(rf/reg-event-fx :change-password/password-verify-success + (fn [{:keys [db]} [valid? old-password]] + {:db (if valid? + (-> db + (assoc-in [:settings/change-password :old-password] old-password) + (assoc-in [:settings/change-password :current-step] :new-password)) + (assoc-in db [:settings/change-password :verify-error] true))})) + +(rf/reg-event-fx :change-password/reset-error + (fn [{:keys [db]}] + {:db (assoc-in db [:settings/change-password :verify-error] false)})) + +(rf/reg-event-fx :change-password/reset + (fn [{:keys [db]}] + {:db (assoc db :settings/change-password {})})) + +(rf/reg-event-fx :change-password/confirm-new-password + (fn [{:keys [db]} [new-password]] + {:db (assoc-in db [:settings/change-password :new-password] new-password) + :fx [[:dispatch [:change-password/submit]]]})) + +(rf/reg-event-fx :change-password/submit + (fn [{:keys [db]}] + (let [key-uid (get-in db [:profile/profile :key-uid]) + {:keys [new-password old-password]} (get db :settings/change-password)] + {:db (assoc-in db [:settings/change-password :loading?] true) + :fx [[:dispatch [:dismiss-keyboard]] + [:dispatch [:open-modal :screen/change-password-loading]] + [:effects.change-password/change-password + {:key-uid key-uid + :old-password old-password + :new-password new-password + :on-success (fn [] + (rf/dispatch [:change-password/submit-success])) + :on-fail (fn [error] + (rf/dispatch [:change-password/submit-fail error]))}]]}))) + +(rf/reg-event-fx :change-password/submit-fail + (fn [_ [error]] + (log/error "failed to change the password" + {:error error + :event :change-password/submit}) + {:fx [[:dispatch [:change-password/reset]] + [:dispatch [:navigate-back]]]})) + +(rf/reg-event-fx :change-password/submit-success + (fn [{:keys [db]}] + {:db (assoc-in db [:settings/change-password :loading?] false)})) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/header.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/header.cljs new file mode 100644 index 00000000000..5f0703d1d93 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/header.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.header + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.profile.settings.screens.password.change-password.style :as style] + [utils.i18n :as i18n])) + +(defn view + [] + [rn/view {:style style/heading} + [quo/text + {:style style/heading-title + :weight :semi-bold + :size :heading-1} + (i18n/label :t/change-password)] + [quo/text + {:style style/heading-subtitle + :weight :regular + :size :paragraph-1} + (i18n/label :t/change-password-description)]]) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/loading.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/loading.cljs new file mode 100644 index 00000000000..587f4805468 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/loading.cljs @@ -0,0 +1,53 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.loading + (:require [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.contexts.profile.settings.screens.password.change-password.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +;; NOTE: The duration of the password change depends on the device hardware, so to avoid +;; quick changes in UI when mounted, we're waiting for a bit. +(def ^:private minimum-loading-time 5000) + +(defn- handle-logout + [] + (rf/dispatch [:profile.settings/ask-logout]) + (rf/dispatch [:change-password/reset])) + +(defn view + [] + (let [[minimum-loading-timeout-done? + set-minimum-loading-timeout-done] (rn/use-state false) + loading? (rf/sub [:settings/change-password-loading]) + logging-out? (rf/sub [:profile/logging-out?]) + done? (and (not loading?) minimum-loading-timeout-done?)] + (rn/use-mount (fn [] + (js/setTimeout + #(set-minimum-loading-timeout-done true) + minimum-loading-time))) + [quo/overlay {:type :shell} + [rn/view + {:key :change-password-loading + :style (style/loading-container safe-area/insets)} + [quo/page-nav] + [quo/page-top + {:description :text + :title (if done? + (i18n/label :t/change-password-done-header) + (i18n/label :t/change-password-loading-header)) + :description-text (if done? + (i18n/label :t/change-password-done-description) + (i18n/label :t/change-password-loading-description))}] + [rn/view {:style style/loading-content} + (when-not done? + [quo/information-box + {:type :error + :style {:margin-top 12} + :icon :i/info} + (i18n/label :t/change-password-loading-warning)])] + (when done? + [quo/logout-button + {:container-style style/logout-container + :disabled? logging-out? + :on-press handle-logout}])]])) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/new_password_form.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/new_password_form.cljs new file mode 100644 index 00000000000..f66a5f63a34 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/new_password_form.cljs @@ -0,0 +1,129 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.new-password-form + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.password-with-hint.view :as password-with-hint] + [status-im.common.validation.password :as password] + [status-im.constants :as constant] + [status-im.contexts.profile.settings.screens.password.change-password.events] + [status-im.contexts.profile.settings.screens.password.change-password.header :as header] + [status-im.contexts.profile.settings.screens.password.change-password.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn- help + [{:keys [validations]}] + (let [{:keys [lower-case? upper-case? numbers? symbols?]} validations + password-strength (password/strength validations)] + [rn/view + [quo/strength-divider {:type (constant/strength-status password-strength :info)} + (i18n/label :t/password-creation-tips-title)] + [rn/view {:style style/password-tips} + [quo/tips {:completed? lower-case?} + (i18n/label :t/password-creation-tips-1)] + [quo/tips {:completed? upper-case?} + (i18n/label :t/password-creation-tips-2)] + [quo/tips {:completed? numbers?} + (i18n/label :t/password-creation-tips-3)] + [quo/tips {:completed? symbols?} + (i18n/label :t/password-creation-tips-4)]]])) + +(def not-blank? (complement string/blank?)) + +(defn view + [] + (let [customization-color (rf/sub [:profile/customization-color]) + [password set-password] (rn/use-state "") + [repeat-password set-repeat-password] (rn/use-state "") + [disclaimer-accepted? set-disclaimer-accepted] (rn/use-state false) + [focused? set-focused] (rn/use-state false) + [show-validation? set-show-validation] (rn/use-state false) + + {:keys [long-enough? short-enough?] + :as validations} (password/validate password) + empty-password? (string/blank? password) + same-passwords? (and (not empty-password?) + (= password repeat-password)) + meet-requirements? (and (not empty-password?) + long-enough? + short-enough? + same-passwords? + disclaimer-accepted?) + error? (and show-validation? + (not same-passwords?) + (not empty-password?)) + + ;; handlers + on-change-password (fn [new-value] + (set-password new-value) + (when (and (not-blank? new-value) + (= (count new-value) + (count repeat-password))) + (set-show-validation true))) + on-change-repeat-password (fn [new-value] + (set-repeat-password new-value) + (when (and (not-blank? new-value) + (= (count new-value) + (count password))) + (set-show-validation true))) + on-blur-repeat-password (fn [] + (if empty-password? + (set-show-validation false) + (set-show-validation true))) + on-input-focus (fn [] (set-focused true)) + on-disclaimer-change (fn [] + (set-disclaimer-accepted + (not disclaimer-accepted?))) + on-submit (fn [] + (rf/dispatch + [:change-password/confirm-new-password + (security/mask-data password)]))] + [:<> + [rn/scroll-view {:style style/form-container} + [header/view] + [password-with-hint/view + {:hint (if (not short-enough?) + {:text (i18n/label :t/password-creation-max-length-hint) + :status :error + :shown? true} + {:text (i18n/label :t/password-creation-hint) + :status (if long-enough? :success :default) + :shown? true}) + :placeholder (i18n/label :t/change-password-new-password-placeholder) + :label (i18n/label :t/change-password-new-password-label) + :on-change-text on-change-password + :on-focus on-input-focus + :auto-focus true}] + [rn/view {:style style/space-between-inputs}] + [password-with-hint/view + {:hint {:text (if same-passwords? + (i18n/label :t/password-creation-match) + (i18n/label :t/password-creation-dont-match)) + :status (if same-passwords? :success :error) + :shown? (and (not empty-password?) + show-validation?)} + :error? error? + :placeholder (i18n/label :t/change-password-repeat-password-placeholder) + :on-change-text on-change-repeat-password + :on-focus on-input-focus + :on-blur on-blur-repeat-password}]] + [rn/view {:style style/bottom-part} + (when same-passwords? + [rn/view {:style style/disclaimer-container} + [quo/disclaimer + {:blur? true + :customization-color customization-color + :on-change on-disclaimer-change + :checked? disclaimer-accepted?} + (i18n/label :t/password-creation-disclaimer)]]) + (when (and focused? (not same-passwords?)) + [help + {:validations validations}]) + [rn/view {:style style/button-container} + [quo/button + {:disabled? (not meet-requirements?) + :customization-color customization-color + :on-press on-submit} + (i18n/label :t/password-creation-confirm)]]]])) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/old_password_form.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/old_password_form.cljs new file mode 100644 index 00000000000..7daef3d8eeb --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/old_password_form.cljs @@ -0,0 +1,63 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.old-password-form + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constant] + [status-im.contexts.profile.settings.screens.password.change-password.events] + [status-im.contexts.profile.settings.screens.password.change-password.header :as header] + [status-im.contexts.profile.settings.screens.password.change-password.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.string :as utils.string])) + +(defn view + [] + (let [error (rf/sub [:settings/change-password-error]) + customization-color (rf/sub [:profile/customization-color]) + [password set-password] (rn/use-state "") + on-change-password (fn [new-value] + (when error + (rf/dispatch [:change-password/reset-error])) + (set-password new-value)) + meet-requirements? (and ((complement string/blank?) password) + (utils.string/at-least-n-chars? password + constant/min-password-length)) + on-submit (fn [] + (when meet-requirements? + (rf/dispatch + [:change-password/verify-old-password + (security/mask-data password)])))] + [:<> + [rn/scroll-view {:content-container-style style/form-container} + [header/view] + [quo/input + {:placeholder (i18n/label :t/change-password-old-password-placeholder) + :label (i18n/label :t/change-password-old-password-label) + :on-change-text on-change-password + :auto-focus true + :type :password + :blur? true}] + [rn/view + {:style style/error-container} + (when error + [quo/info-message + {:status :error + :size :default + :icon :i/info} + (i18n/label :t/oops-wrong-password)])] + [quo/information-box + {:type :error + :style style/warning-container + :icon :i/info} + (i18n/label :t/change-password-confirm-warning)]] + [rn/view + {:style (assoc style/bottom-part + :margin-horizontal 20 + :margin-top 12)} + [quo/button + {:disabled? (not meet-requirements?) + :customization-color customization-color + :on-press on-submit} + (i18n/label :t/continue)]]])) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/style.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/style.cljs new file mode 100644 index 00000000000..7657a2de4df --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/style.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area])) + +(def form-container + {:margin-horizontal 20}) + +(def heading + {:margin-bottom 20 + :margin-top 12}) + +(def heading-subtitle {:color colors/white}) + +(def heading-title (assoc heading-subtitle :margin-bottom 8)) + +(def space-between-inputs {:height 16}) + +(def error-container + {:margin-top 8 + :flex-direction :row + :align-items :center}) + +(def warning-container + {:margin-top 12}) + +(defn loading-container + [{:keys [top bottom]}] + {:flex 1 + :justify-content :space-between + :padding-top top + :padding-bottom bottom}) + +(def loading-content + {:flex 1 + :padding-horizontal 20}) + +(def logout-container + {:margin-vertical 12 + :margin-horizontal 20}) + +(def password-tips + {:flex-direction :row + :margin-horizontal 20 + :justify-content :space-between}) + +(def bottom-part + {:margin-bottom (if platform/ios? safe-area/bottom 12) + :justify-content :flex-end}) + +(def disclaimer-container + {:margin-horizontal 20 + :margin-vertical 4}) + +(def button-container + {:margin-top 12 + :margin-horizontal 20}) diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/view.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/view.cljs new file mode 100644 index 00000000000..072e7fadc05 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/view.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.profile.settings.screens.password.change-password.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.contexts.profile.settings.screens.password.change-password.events] + [status-im.contexts.profile.settings.screens.password.change-password.new-password-form :as + new-password-form] + [status-im.contexts.profile.settings.screens.password.change-password.old-password-form :as + old-password-form] + [utils.re-frame :as rf])) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [{:keys [top bottom]} safe-area/insets + alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + current-step (rf/sub [:settings/change-password-current-step])] + (rn/use-unmount #(rf/dispatch [:change-password/reset])) + [quo/overlay {:type :shell} + [rn/pressable + {:key :change-password + :on-press rn/dismiss-keyboard! + :accessible false + :style {:flex 1}} + [quo/page-nav + {:margin-top top + :background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [rn/keyboard-avoiding-view + {:style {:flex 1} + :keyboard-vertical-offset (if platform/ios? + (-> 12 + (+ alert-banners-top-margin) + (- bottom)) + 0)} + (condp = current-step + :old-password [old-password-form/view] + :new-password [new-password-form/view])]]])) diff --git a/src/status_im/contexts/profile/settings/screens/password/style.cljs b/src/status_im/contexts/profile/settings/screens/password/style.cljs new file mode 100644 index 00000000000..81f1340f803 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/style.cljs @@ -0,0 +1 @@ +(ns status-im.contexts.profile.settings.screens.password.style) diff --git a/src/status_im/contexts/profile/settings/screens/password/view.cljs b/src/status_im/contexts/profile/settings/screens/password/view.cljs new file mode 100644 index 00000000000..755dd16e0fc --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/password/view.cljs @@ -0,0 +1,89 @@ +(ns status-im.contexts.profile.settings.screens.password.view + (:require [quo.context :as quo.context] + [quo.core :as quo] + [status-im.common.biometric.utils :as biometric] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- on-press-biometric-enable + [button-label theme keycard-profile?] + (fn [] + (if keycard-profile? + (rf/dispatch [:standard-auth/authorize-with-keycard-keys + {:on-auth-success (fn [keycard-keys] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:keycard/disconnect]) + (rf/dispatch + [:biometric/authenticate + {:on-success #(rf/dispatch [:biometric/enable-keycard + keycard-keys]) + :on-fail #(rf/dispatch [:biometric/show-message + (ex-cause %)])}]))}]) + (rf/dispatch + [:standard-auth/authorize-with-password + {:blur? true + :hide-biometrics-button? true + :theme theme + :auth-button-label (i18n/label :t/biometric-enable-button {:bio-type-label button-label}) + :on-auth-success (fn [password] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:biometric/authenticate + {:on-success #(rf/dispatch [:biometric/enable password]) + :on-fail #(rf/dispatch [:biometric/show-message + (ex-cause %)])}]))}])))) + +(defn- get-biometric-item + [theme] + (let [auth-method (rf/sub [:auth-method]) + biometric-type (rf/sub [:biometrics/supported-type]) + keycard-profile? (rf/sub [:keycard/keycard-profile?]) + label (biometric/get-label-by-type biometric-type) + icon (biometric/get-icon-by-type biometric-type) + supported? (boolean biometric-type) + enabled? (= auth-method constants/auth-method-biometric) + biometric-on? (and supported? enabled?) + press-handler (when supported? + (if biometric-on? + (fn [] (rf/dispatch [:biometric/disable])) + (on-press-biometric-enable label theme keycard-profile?)))] + {:title label + :image-props icon + :image :icon + :blur? true + :action :selector + :action-props {:on-change press-handler + :checked? biometric-on?} + :on-press press-handler})) + +(defn- get-change-password-item + [] + {:title (i18n/label :t/change-password) + :on-press #(rf/dispatch [:open-modal :screen/change-password]) + :blur? true + :image :icon + :image-props :i/password + :action :arrow}) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [theme (quo.context/use-theme) + keycard-profile? (rf/sub [:keycard/keycard-profile?])] + [quo/overlay {:type :shell :top-inset? true} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [quo/page-top {:title (i18n/label :t/password)}] + [quo/category + {:key :category + :data [(get-biometric-item theme) + (when-not keycard-profile? + (get-change-password-item))] + :blur? true + :list-type :settings}]])) diff --git a/src/status_im/contexts/profile/settings/screens/syncing/view.cljs b/src/status_im/contexts/profile/settings/screens/syncing/view.cljs new file mode 100644 index 00000000000..04361683975 --- /dev/null +++ b/src/status_im/contexts/profile/settings/screens/syncing/view.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.profile.settings.screens.syncing.view + (:require + [quo.core :as quo] + [status-im.common.data-confirmation-sheet.view :as data-confirmation-sheet] + [status-im.common.events-helper :as events-helper] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- open-paired-devices-list + [] + (rf/dispatch [:open-modal :screen/paired-devices])) + +(defn- open-data-confirmation-sheet + [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] [data-confirmation-sheet/view]) + :shell? true + :theme :dark}])) + +(defn view + [] + (let [pairing-devices-count (rf/sub [:pairing/paired-devices-count]) + syncing-on-mobile-network? (rf/sub [:profile/syncing-on-mobile-network?])] + [quo/overlay {:type :shell :top-inset? true} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/page-top {:title (i18n/label :t/syncing)}] + [quo/category + {:data [{:title (i18n/label :t/sync-and-backup) + :description :text + :description-props {:text (if syncing-on-mobile-network? + (i18n/label :t/wifi-and-mobile-data) + (i18n/label :t/mobile-network-use-wifi))} + :on-press open-data-confirmation-sheet + :blur? true + :image-props :i/syncing + :image :icon + :action :arrow} + {:title (i18n/label :t/paired-devices) + :description :text + :description-props {:text (i18n/label :t/devices-count + {:number pairing-devices-count})} + :on-press open-paired-devices-list + :image-props :i/mobile + :image :icon + :blur? true + :action :arrow}] + :blur? true + :list-type :settings}]])) diff --git a/src/status_im/contexts/profile/settings/style.cljs b/src/status_im/contexts/profile/settings/style.cljs new file mode 100644 index 00000000000..a20514c878e --- /dev/null +++ b/src/status_im/contexts/profile/settings/style.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.profile.settings.style + (:require [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [status-im.contexts.shell.constants :as shell.constants])) + +(defn navigation-wrapper + [{:keys [customization-color inset theme]}] + {:padding-top inset + :background-color (colors/resolve-color customization-color theme 40)}) + +(def ^:const footer-padding 20) +(def ^:const ios-bottom-offset -10) + +(defn footer-container + [bottom] + {:padding-horizontal footer-padding + :padding-top 16 + :padding-bottom (+ shell.constants/floating-shell-button-height + footer-padding + (if platform/ios? ios-bottom-offset bottom))}) diff --git a/src/status_im/contexts/profile/settings/view.cljs b/src/status_im/contexts/profile/settings/view.cljs new file mode 100644 index 00000000000..96b8102269f --- /dev/null +++ b/src/status_im/contexts/profile/settings/view.cljs @@ -0,0 +1,120 @@ +(ns status-im.contexts.profile.settings.view + (:require [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.contexts.profile.settings.header.view :as settings.header] + [status-im.contexts.profile.settings.list-items :as settings.items] + [status-im.contexts.profile.settings.style :as style] + [status-im.contexts.profile.utils :as profile.utils] + [status-im.feature-flags :as ff] + [utils.debounce :as debounce] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn show-settings-item? + [{:keys [feature-flag]}] + (or (ff/enabled? feature-flag) + (nil? feature-flag))) + +(defn- settings-category-view + [data] + (rn/delay-render + [quo/category + {:list-type :settings + :container-style {:padding-top 8 + :padding-bottom 4} + :blur? true + :data (doall (filter show-settings-item? data))}])) + +(defn scroll-handler + [event scroll-y] + (let [current-y (oops/oget event "nativeEvent.contentOffset.y")] + (reanimated/set-shared-value scroll-y current-y))) + +(defn- logout-press + [] + (rf/dispatch [:profile.settings/ask-logout])) + +(defn- footer + [{:keys [bottom]}] + (rn/delay-render + (let [logging-out? (rf/sub [:profile/logging-out?])] + [rn/view {:style (style/footer-container bottom)} + [quo/logout-button + {:on-press logout-press + :disabled? logging-out?}]]))) + +(defn- get-item-layout + [_ index] + #js {:length 100 :offset (* 100 index) :index index}) + +(defn- navigate-to-backup-challenge + [masked-seed-phrase] + (rf/dispatch + [:open-modal :screen/confirm-backup-on-shell + {:masked-seed-phrase masked-seed-phrase + :on-try-again (fn []) ;; Needed due to impl. checks + :on-success (fn [] + (rf/dispatch [:dismiss-modal :screen/backup-recovery-phrase-on-shell]) + (rf/dispatch [:dismiss-modal :screen/confirm-backup-on-shell]) + (rf/dispatch [:my-profile/finish])) + :back-button? true + :shell? true}])) + +(defn navigate-to-backup-seed + [mnemonic] + (fn [] + (rf/dispatch + [:open-modal + :screen/backup-recovery-phrase-on-shell + {:back-button? true + :shell? true + :masked-seed-phrase (security/mask-data mnemonic) + :on-success navigate-to-backup-challenge}]))) + +(defn view + [] + (let [theme (quo.context/use-theme) + insets safe-area/insets + customization-color (rf/sub [:profile/customization-color]) + scroll-y (reanimated/use-shared-value 0) + profile (rf/sub [:profile/profile]) + mnemonic (rf/sub [:profile/mnemonic]) + full-name (profile.utils/displayed-name profile) + on-scroll (rn/use-callback #(scroll-handler % scroll-y)) + on-backup-seed-press (rn/use-callback #(navigate-to-backup-seed mnemonic))] + [quo/overlay {:type :shell} + [rn/view + {:style (style/navigation-wrapper {:customization-color customization-color + :inset (:top insets) + :theme theme})} + [quo/page-nav + {:title full-name + :background :blur + :type :title + :text-align :left + :scroll-y scroll-y + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back]) + :right-side [{:icon-name :i/qr-code + :on-press #(debounce/throttle-and-dispatch [:open-modal :screen/share-shell + {:initial-tab :profile}] + 1000)} + {:icon-name :i/share + :on-press #(rf/dispatch [:open-share + {:options {:message (:universal-profile-url + profile)}}])}]}]] + [rn/flat-list + {:header [settings.header/view {:scroll-y scroll-y}] + :data (settings.items/items mnemonic on-backup-seed-press) + :shows-vertical-scroll-indicator false + :render-fn settings-category-view + :get-item-layout get-item-layout + :footer [footer insets] + :scroll-event-throttle 16 + :on-scroll on-scroll + :bounces false + :over-scroll-mode :never}]])) diff --git a/src/status_im/contexts/profile/settings/visibility_sheet/view.cljs b/src/status_im/contexts/profile/settings/visibility_sheet/view.cljs new file mode 100644 index 00000000000..74ccfca1e54 --- /dev/null +++ b/src/status_im/contexts/profile/settings/visibility_sheet/view.cljs @@ -0,0 +1,51 @@ +(ns status-im.contexts.profile.settings.visibility-sheet.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn change-visibility-status + [status-type] + (rf/dispatch + [:visibility-status-updates/delayed-visibility-status-update status-type]) + (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [] + (let [{:keys [public-key]} (rf/sub [:profile/profile]) + {:keys [status-type]} (rf/sub + [:visibility-status-updates/visibility-status-update + public-key]) + customization-color (rf/sub [:profile/customization-color])] + (rn/use-mount + (fn [] + (rf/dispatch [:peer-stats/get-count]))) + + [quo/action-drawer + [[{:icon :i/online + :no-icon-color? true + :blur? true + :state (when (= status-type constants/visibility-status-always-online) :selected) + :customization-color customization-color + :accessibility-label :online + :label (i18n/label :t/online) + :on-press #(change-visibility-status constants/visibility-status-always-online)} + {:icon :i/offline + :no-icon-color? true + :blur? true + :state (when (= status-type constants/visibility-status-inactive) :selected) + :accessibility-label :offline + :customization-color customization-color + :label (i18n/label :t/offline) + :sub-label (i18n/label :t/status-inactive-subtitle) + :on-press #(change-visibility-status constants/visibility-status-inactive)} + {:icon :i/automatic + :no-icon-color? true + :blur? true + :state (when (= status-type constants/visibility-status-automatic) :selected) + :accessibility-label :automatic + :customization-color customization-color + :label (i18n/label :t/status-automatic) + :sub-label (i18n/label :t/status-automatic-subtitle) + :on-press #(change-visibility-status constants/visibility-status-automatic)}]]])) diff --git a/src/status_im/contexts/profile/utils.cljs b/src/status_im/contexts/profile/utils.cljs new file mode 100644 index 00000000000..d687402ef89 --- /dev/null +++ b/src/status_im/contexts/profile/utils.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.profile.utils + (:require [clojure.string :as string])) + +(defn displayed-name + [{:keys [name display-name preferred-name alias ens-verified primary-name]}] + ;; `preferred-name` is our own name otherwise we make sure the `name` is verified and use it + (let [display-name (when-not (string/blank? display-name) + display-name) + preferred-name (when-not (string/blank? preferred-name) + preferred-name) + ens-name (or preferred-name + display-name + name)] + (if (or preferred-name (and ens-verified name)) + ens-name + (or display-name primary-name alias name)))) + +(defn photo + [{:keys [images]}] + (or (:large images) + (:thumbnail images) + (first images))) diff --git a/src/status_im/contexts/profile/utils_test.cljs b/src/status_im/contexts/profile/utils_test.cljs new file mode 100644 index 00000000000..b13151a292a --- /dev/null +++ b/src/status_im/contexts/profile/utils_test.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.profile.utils-test + (:require + [cljs.test :refer [are deftest]] + [status-im.contexts.profile.utils :as sut])) + +(deftest displayed-name-test + (are [expected arg] (= expected (sut/displayed-name arg)) + "Display Name" {:display-name "Display Name"} + "Primary Name" {:primary-name "Primary Name"} + "Alias" {:alias "Alias"} + "Name" {:name "Name"} + nil {:preferred-name " "} + "Preferred Name" {:preferred-name "Preferred Name"} + "Preferred Name" {:preferred-name "Preferred Name" :display-name "Display Name"} + "Preferred Name" {:preferred-name "Preferred Name" :display-name "Display Name" :name "Name"} + "Display Name" {:ens-verified true :name "Name" :display-name "Display Name"} + "Name" {:ens-verified true :name "Name" :display-name " "} + "Name" {:ens-verified true :name "Name"} + "Name" {:name "Name"})) diff --git a/src/status_im/contexts/push_notifications/notifications.cljs b/src/status_im/contexts/push_notifications/notifications.cljs new file mode 100644 index 00000000000..ad738afecb6 --- /dev/null +++ b/src/status_im/contexts/push_notifications/notifications.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.push-notifications.notifications + (:require + [react-native.push-notification-ios :as pn-ios])) + +(def clear-received-notifications pn-ios/remove-all-delivered-notifications) diff --git a/src/status_im/contexts/push_notifications/permissions.cljs b/src/status_im/contexts/push_notifications/permissions.cljs new file mode 100644 index 00000000000..e5b34ae188a --- /dev/null +++ b/src/status_im/contexts/push_notifications/permissions.cljs @@ -0,0 +1,16 @@ +(ns status-im.contexts.push-notifications.permissions + (:require + [react-native.permissions :as permissions] + [react-native.platform :as platform] + [react-native.push-notification-ios :as pn-ios])) + +(def request-notification-permissions permissions/request-notification-permissions) + +(def check-notification-permissions permissions/check-notification-permissions) + +(def open-notifications-settings permissions/open-notification-settings) + +(defn release-notification-permissions + [] + (when platform/ios? + (pn-ios/abandon-permissions))) diff --git a/src/status_im/contexts/push_notifications/remote_token.cljs b/src/status_im/contexts/push_notifications/remote_token.cljs new file mode 100644 index 00000000000..4ff20c91377 --- /dev/null +++ b/src/status_im/contexts/push_notifications/remote_token.cljs @@ -0,0 +1,45 @@ +(ns status-im.contexts.push-notifications.remote-token + (:require + [promesa.core :as promesa] + [react-native.firebase :as firebase] + [react-native.platform :as platform] + [react-native.push-notification-ios :as pn-ios])) + +(defn request-remote-token + "Request the remote token for the mobile platform. + On iOS this will retrieve a APNS token, + and on Android this will retrieve a FCM token." + [options] + (-> (if platform/ios? + (pn-ios/request-remote-token options) + (firebase/request-remote-token options)) + (promesa/then (fn [token] {:token token})))) + +(defn request-fcm-token + [options] + (-> (firebase/request-remote-token options) + (promesa/then (fn [token] {:fcm-token token})))) + +(defn revoke-fcm-token + [options] + (firebase/revoke-remote-token options)) + +(defn request-apns-token + [options] + (-> (pn-ios/request-remote-token options) + (promesa/then (fn [token] {:apns-token token})))) + +(defn request-apns-and-fcm-token + [options] + (-> (promesa/all [(request-apns-token options) + (request-fcm-token options)]) + (promesa/then + (fn [labeled-tokens] + (apply merge labeled-tokens))))) + +(defn select-platform-token + [tokens] + (cond + platform/ios? (:apns-token tokens) + platform/android? (:fcm-token tokens) + :else (:token tokens))) diff --git a/src/status_im/contexts/settings/about/style.cljs b/src/status_im/contexts/settings/about/style.cljs new file mode 100644 index 00000000000..649c46a426f --- /dev/null +++ b/src/status_im/contexts/settings/about/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.settings.about.style) + +(def category-spacing {:padding-bottom 12}) + +(def app-info-container + {:padding-horizontal 20 + :padding-top 8 + :padding-bottom 16 + :row-gap 16}) diff --git a/src/status_im/contexts/settings/about/view.cljs b/src/status_im/contexts/settings/about/view.cljs new file mode 100644 index 00000000000..90ac5fea6f2 --- /dev/null +++ b/src/status_im/contexts/settings/about/view.cljs @@ -0,0 +1,100 @@ +(ns status-im.contexts.settings.about.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.app-build.core :as build] + [status-im.constants :as constants] + [status-im.contexts.settings.about.style :as style] + [status-im.contexts.settings.common.header :as header] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- open-link + [url] + (rf/dispatch [:browser.ui/open-url url])) + +(defn- copy + [data item-name] + (rf/dispatch [:share/copy-text-and-show-toast + {:text-to-copy data + :post-copy-message (str item-name + " " + (i18n/label :t/sharing-copied-to-clipboard))}])) + +(def about-data + [{:app-info? true} + {:category-label (i18n/label :t/website) + :items [{:title "status.app" + :on-press #(open-link constants/status-app-url) + :blur? true + :action :arrow + :action-props {:icon :i/external}}]} + {:category-label (i18n/label :t/github-repos) + :items [{:title "status-mobile" + :on-press #(open-link constants/status-mobile-url) + :blur? true + :action :arrow + :action-props {:icon :i/external}} + {:title "status-go" + :on-press #(open-link constants/status-go-url) + :blur? true + :action :arrow + :action-props {:icon :i/external}} + {:title "go-waku" + :on-press #(open-link constants/go-waku-url) + :blur? true + :action :arrow + :action-props {:icon :i/external}}]} + {:category-label (i18n/label :t/documents) + :items [{:title (i18n/label :t/privacy-policy) + :on-press #(rf/dispatch [:open-modal :screen/settings.privacy-policy]) + :blur? true + :action :arrow} + {:title (i18n/label :t/terms-of-service) + :on-press #(rf/dispatch [:open-modal :screen/settings.terms-of-use]) + :blur? true + :action :arrow}]}]) + +(defn info-item + [{:keys [title info]}] + [quo/data-item + {:size :default + :status :default + :right-icon :i/copy + :card? true + :blur? true + :title title + :on-press #(copy info title) + :subtitle info + :subtitle-type :default + :subtitle-text-props {:number-of-lines 1 + :ellipsize-mode :middle}}]) + +(defn- app-info + [] + (let [node-version (rf/sub [:get-app-node-version])] + [rn/view {:style style/app-info-container} + [info-item {:title (i18n/label :t/version) :info build/app-short-version}] + [info-item {:title (i18n/label :t/app-commit) :info build/commit-hash}] + [info-item {:title (i18n/label :t/node-version) :info node-version}]])) + +(defn category + [{:keys [app-info? category-label items]}] + (if app-info? + [app-info] + [quo/category + {:label category-label + :list-type :settings + :container-style style/category-spacing + :blur? true + :data items}])) + +(defn view + [] + [quo/overlay {:type :shell} + [header/view {:title (i18n/label :t/about)}] + [rn/flat-list + {:data about-data + :shows-vertical-scroll-indicator false + :render-fn category + :bounces false + :over-scroll-mode :never}]]) diff --git a/src/status_im/contexts/settings/advanced/events.cljs b/src/status_im/contexts/settings/advanced/events.cljs new file mode 100644 index 00000000000..a014c5f19cc --- /dev/null +++ b/src/status_im/contexts/settings/advanced/events.cljs @@ -0,0 +1,125 @@ +(ns status-im.contexts.settings.advanced.events + (:require [re-frame.core :as rf] + [taoensso.timbre :as log] + [utils.i18n :as i18n])) + +(rf/reg-event-fx + :advanced-settings/toggle-waku-backup + (fn [{:keys [db]}] + (let [backup-enabled? (-> db :profile/profile :backup-enabled?)] + {:fx [[:dispatch [:multiaccounts.ui/update :backup-enabled? (not backup-enabled?) {}]]]}))) + +(rf/reg-event-fx + :advanced-settings/backup-performed + (fn [{:keys [db]} [last-backup]] + {:db (-> db + (dissoc :backup/performing-backup) + (assoc-in [:profile/profile :last-backup] last-backup))})) + +(rf/reg-event-fx + :advanced-settings/backup-failed + (fn [{:keys [db]}] + {:db (dissoc db :backup/performing-backup)})) + +(rf/reg-event-fx + :advanced-settings/perform-backup + (fn [{:keys [db]}] + {:db (assoc db :backup/performing-backup true) + :json-rpc/call [{:method "wakuext_backupData" + :params [] + :on-error (fn [error] + (log/error "Failed to perform backup" error) + (rf/dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text "Failed to perform backup, please try again later"}]) + (rf/dispatch [:advanced-settings/backup-failed error]))}]})) + +(rf/reg-event-fx + :advanced-settings/change-log-level + (fn [{:keys [db]} [log-level]] + (let [current-log-level (get-in db [:profile/profile :log-level])] + (when (not= current-log-level log-level) + (let [need-set-profile-log-enabled? (or (empty? current-log-level) (empty? log-level)) + profile-log-enabled? (boolean (seq log-level))] + {:fx [[:log-level/set-profile-log-level log-level] + (when need-set-profile-log-enabled? + [:log-level/set-profile-log-enabled profile-log-enabled?]) + ;; update log level in taoensso.timbre + [:logs/set-level log-level] + [:dispatch [:multiaccounts.ui/update :log-level log-level]]]}))))) + +(rf/reg-event-fx + :advanced-settings/change-fleet + (fn [{db :db} [new-fleet]] + (let [current-fleet (-> db :profile/profile :fleet keyword)] + (when (not= current-fleet new-fleet) + {:ui/show-confirmation + {:title (i18n/label :t/close-app-title) + :content (i18n/label :t/change-fleet {:fleet new-fleet}) + :confirm-button-text (i18n/label :t/close-app-button) + :on-accept #(rf/dispatch [:fleet.ui/save-fleet-confirmed + (some-> new-fleet + name)]) + :on-cancel nil}})))) + +(rf/reg-event-fx + :advanced-settings/toggle-light-client + (fn [{:keys [db]}] + (let [previously-enabled? (-> db :profile/profile :wakuv2-config :LightClient) + new-setting (not previously-enabled?)] + {:ui/show-confirmation + {:title (i18n/label :t/close-app-title) + :content (if previously-enabled? + (i18n/label :t/disable-light-client) + (i18n/label :t/enable-light-client)) + :on-accept #(rf/dispatch [:advanced-settings/change-light-client new-setting]) + :confirm-button-text (i18n/label :t/close-app-button) + :on-cancel nil}}))) + +(rf/reg-event-fx + :advanced-settings/change-light-client + (fn [{:keys [db]} [new-setting]] + {:db (assoc-in db [:profile/profile :wakuv2-config :LightClient] new-setting) + :json-rpc/call [{:method "wakuext_setLightClient" + :params [{:enabled new-setting}] + :on-success (fn [] + (log/info "Light client set successfully" new-setting) + (rf/dispatch [:profile/logout])) + :on-error #(log/error "Failed to set light client" + {:error % + :enabled? new-setting})}]})) + +(rf/reg-event-fx + :advanced-settings/toggle-store-confirmations + (fn [{:keys [db]}] + (let [previously-enabled? (-> db + :profile/profile + :wakuv2-config + :EnableStoreConfirmationForMessagesSent) + new-setting (not previously-enabled?)] + {:ui/show-confirmation + {:title (i18n/label :t/close-app-title) + :content (if previously-enabled? + (i18n/label :t/disable-store-confirmations) + (i18n/label :t/enable-store-confirmations)) + :on-accept #(rf/dispatch [:advanced-settings/change-store-confirmations + new-setting]) + :confirm-button-text (i18n/label :t/close-app-button) + :on-cancel nil}}))) + +(rf/reg-event-fx + :advanced-settings/change-store-confirmations + (fn [{:keys [db]} [new-setting]] + {:db (assoc-in db + [:profile/profile :wakuv2-config :EnableStoreConfirmationForMessagesSent] + new-setting) + :json-rpc/call [{:method "wakuext_setStoreConfirmationForMessagesSent" + :params [{:enabled new-setting}] + :on-success (fn [] + (log/info "Store confirmation set successfully" new-setting) + (rf/dispatch [:profile/logout])) + :on-error #(log/error "Failed to set store confirmation" + {:error % + :enabled? new-setting})}]})) diff --git a/src/status_im/contexts/settings/advanced/style.cljs b/src/status_im/contexts/settings/advanced/style.cljs new file mode 100644 index 00000000000..89872e017c1 --- /dev/null +++ b/src/status_im/contexts/settings/advanced/style.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.settings.advanced.style + (:require [react-native.safe-area :as safe-area])) + +(def header + {:top 0 + :left 0 + :right 0 + :padding-top safe-area/top}) + +(def advanced-item {:padding-top 8}) + +(def waku-backup-toggle + {:padding-horizontal 20}) + +(def option-picker-sheet + {:padding-horizontal 20 + :row-gap 8}) diff --git a/src/status_im/contexts/settings/advanced/view.cljs b/src/status_im/contexts/settings/advanced/view.cljs new file mode 100644 index 00000000000..1a943694214 --- /dev/null +++ b/src/status_im/contexts/settings/advanced/view.cljs @@ -0,0 +1,266 @@ +(ns status-im.contexts.settings.advanced.view + (:require [clojure.string :as string] + [legacy.status-im.fleet.core :as fleets] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events.helper] + [status-im.config :as config] + [status-im.contexts.settings.advanced.events] + [status-im.contexts.settings.advanced.style :as style] + [status-im.feature-flags :as ff] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- static-header + [] + [rn/view {:style style/header} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press events.helper/navigate-back}]]) + +(defn- advanced-item + [{:keys [label data] :as _item-data}] + (let [customization-color (rf/sub [:profile/customization-color])] + [quo/category + {:container-style style/advanced-item + :label label + :customization-color customization-color + :data data + :blur? true + :list-type :settings}])) + +(defn- format-timestamp + [prefix-string last-backup-timestamp] + (-> (str prefix-string " " (datetime/timestamp->relative (* 1000 last-backup-timestamp))) + (string/capitalize) + (string/replace #"am|pm" {"am" "AM" "pm" "PM"}))) + +(defn- waku-backup-settings + [] + (let [{:keys [backup-enabled? + last-backup]} (rf/sub [:profile/profile]) + performing-backup? (rf/sub [:backup/performing-backup]) + customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title (i18n/label :t/waku-backup)}] + (comment + [quo/text + {:style {:padding-horizontal 20 + :margin-bottom 16}} + "Explanation and implications of the toggle"]) + [rn/view {:style style/waku-backup-toggle} + [quo/drawer-action + {:title (i18n/label :t/waku-backup-enabled) + :action :toggle + :state (when backup-enabled? :selected) + :blur? true + :accessibility-label :waku-backup-switch + :icon :i/revert + :customization-color customization-color + :on-press #(rf/dispatch [:advanced-settings/toggle-waku-backup])}]] + [quo/bottom-actions + {:actions :one-action + :blur? true + :description :bottom + :description-text (if performing-backup? + (i18n/label :t/backing-up) + (format-timestamp (i18n/label :t/last-backup-performed) last-backup)) + :button-one-label (i18n/label :t/perform-backup) + :button-one-props {:customization-color customization-color + :on-press #(rf/dispatch [:advanced-settings/perform-backup]) + :disabled? performing-backup?}}]])) + +(defn- option-picker-sheet + [{:keys [title options option-selected? option-title-fn on-press]}] + (let [option-xf (map (fn [option] + [quo/drawer-action + {:title (option-title-fn option) + :state (when (option-selected? option) :selected) + :blur? true + :on-press #(on-press option)}]))] + [:<> + [quo/drawer-top {:title title}] + (into [rn/view {:style style/option-picker-sheet}] + option-xf + options)])) + +(def ^:private log-levels + "The status-go log library (zap) doesn't support the trace level, so we remove + the trace option from the UI. When trace is enabled, an error will happen while + trying to log in (the user will see the error 'wrong password')." + {"" "DISABLED" + "ERROR" "ERROR" + "WARN" "WARN" + "INFO" "INFO" + "DEBUG" "DEBUG"}) + +(defn- log-level-sheet + [] + (let [current-log-level (rf/sub [:log-level/current-profile-log-level])] + [option-picker-sheet + {:title (i18n/label :t/log-level) + :options (keys log-levels) + :option-selected? (fn [log-level-id] + (= (log-levels log-level-id) current-log-level)) + :option-title-fn #(-> % log-levels string/capitalize) + :on-press (fn [log-level-id] + (rf/dispatch [:advanced-settings/change-log-level log-level-id]) + (rf/dispatch [:hide-bottom-sheet]))}])) + +(defn- fleet-sheet + [] + (let [current-fleet (rf/sub [:fleets/current-fleet])] + [option-picker-sheet + {:title (i18n/label :t/fleet) + :options (map keyword fleets/fleets) + :option-selected? (fn [fleet] + (= fleet current-fleet)) + :option-title-fn name + :on-press (fn [fleet] + (rf/dispatch [:advanced-settings/change-fleet fleet]))}])) + +(defn- copy-string-callback + [content property-copied] + (fn [] + (clipboard/set-string (str content)) + (rf/dispatch + [:toasts/upsert + {:id :string-copied + :type :positive + :text (str property-copied " " (i18n/label :t/copied-to-clipboard))}]))) + +(defn- open-waku-settings + [] + (rf/dispatch [:show-bottom-sheet + {:content waku-backup-settings + :shell? true + :blur? true + :theme :dark}])) + +(defn- toggle-peer-syncing + [] + (rf/dispatch [:profile.settings/toggle-peer-syncing])) + +(defn- open-log-level-sheet + [] + (rf/dispatch [:show-bottom-sheet + {:content log-level-sheet + :theme :dark + :shell? true + :blur? true}])) + +(defn- open-fleet-sheet + [] + (rf/dispatch [:show-bottom-sheet + {:content fleet-sheet + :theme :dark + :shell? true + :blur? true}])) + +(defn- force-crash + [] + (rf/dispatch [:app-monitoring/intended-panic "status-mobile intentional panic"])) + +(defn- toggle-light-client + [] + (rf/dispatch [:advanced-settings/toggle-light-client])) + +(defn- get-options + [{:keys [log-level backup-enabled? last-backup peers-count peer-syncing-enabled? + current-mailserver light-client-enabled? current-fleet analytics-user-id]}] + [{:label (i18n/label :t/syncing) + :data [{:title (i18n/label :t/waku-backup) + :accessibility-label :backup-settings-button + :on-press open-waku-settings + :description :text + :action :arrow + :label :text + :label-props (if backup-enabled? + (i18n/label :t/backup-enabled) + (i18n/label :t/backup-disabled)) + :description-props {:text (format-timestamp (i18n/label :t/latest) last-backup)}} + {:title (i18n/label :t/peer-syncing) + :accessibility-label :peer-syncing + :action :selector + :action-props {:on-change toggle-peer-syncing + :checked? peer-syncing-enabled?}} + {:title (i18n/label :t/history-nodes) + :on-press (copy-string-callback current-mailserver (i18n/label :t/history-nodes)) + :description :text + :description-props {:text current-mailserver}}]} + {:label (i18n/label :t/debugging) + :data [{:title (i18n/label :t/log-level) + :accessibility-label :log-level-settings-button + :on-press open-log-level-sheet + :action :arrow + :label :text + :label-props (some-> log-level + log-levels + string/capitalize)} + {:title (i18n/label :t/fleet) + :accessibility-label :fleet-settings-button + :on-press open-fleet-sheet + :action :arrow + :label :text + :label-props current-fleet} + {:title (i18n/label :t/peers-stats) + :accessibility-label :peers-stats + :description :text + :description-props {:text (str (i18n/label :t/peers-count) ": " peers-count)} + :on-press (copy-string-callback peers-count (i18n/label :t/peers-count))} + (when (ff/enabled? ::ff/analytics.copy-user-id) + {:title "Copy analytics user ID" + :accessibility-label :copy-analytics-user-id + :on-press (copy-string-callback analytics-user-id + "Analytics user ID")}) + (when (ff/enabled? ::ff/app-monitoring.intentional-crash) + {:size :small + :title (str "Force crash immediately" + (when (string/blank? config/sentry-dsn-status-go) + " (Sentry DSN is not set)")) + :accessibility-label :intended-panic + :on-press force-crash})]} + {:label (i18n/label :t/other) + :data [{:title (i18n/label :t/light-client-enabled) + :accessibility-label :light-client-enabled + :action :selector + :action-props {:on-change toggle-light-client + :checked? light-client-enabled?}}]}]) + +(defn view + [] + (let [log-level (rf/sub [:log-level/current-profile-log-level]) + {:keys [backup-enabled? + last-backup]} (rf/sub [:profile/profile]) + peers-count (rf/sub [:peer-stats/count]) + peer-syncing-enabled? (rf/sub [:profile/peer-syncing-enabled?]) + current-mailserver (rf/sub [:mailserver/current-name]) + light-client-enabled? (rf/sub [:profile/light-client-enabled?]) + current-fleet (rf/sub [:fleets/current-fleet]) + analytics-user-id (rf/sub [:centralized-metrics/user-id]) + options (rn/use-memo + (fn [] + (get-options {:log-level log-level + :backup-enabled? backup-enabled? + :last-backup last-backup + :peers-count peers-count + :peer-syncing-enabled? peer-syncing-enabled? + :current-mailserver current-mailserver + :light-client-enabled? light-client-enabled? + :current-fleet current-fleet + :analytics-user-id analytics-user-id})) + [backup-enabled? last-backup log-level peers-count + peer-syncing-enabled?])] + (rn/use-mount #(rf/dispatch [:peer-stats/get-count])) + [quo/overlay {:type :shell} + [static-header] + [rn/flat-list + {:header [quo/page-top {:title (i18n/label :t/advanced)}] + :data options + :render-fn advanced-item + :content-container-style {:padding-bottom safe-area/bottom} + :shows-vertical-scroll-indicator false}]])) diff --git a/src/status_im/contexts/settings/common/header.cljs b/src/status_im/contexts/settings/common/header.cljs new file mode 100644 index 00000000000..191916161ab --- /dev/null +++ b/src/status_im/contexts/settings/common/header.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.settings.common.header + (:require [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper])) + +(defn view + [{:keys [title]}] + [rn/view {:style {:padding-top safe-area/top}} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/page-top {:title title}]]) diff --git a/src/status_im/contexts/settings/keycard/style.cljs b/src/status_im/contexts/settings/keycard/style.cljs new file mode 100644 index 00000000000..b5175c1b107 --- /dev/null +++ b/src/status_im/contexts/settings/keycard/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.settings.keycard.style + (:require [quo.foundations.colors :as colors])) + +(def container {:padding-horizontal 28 :padding-top 20}) + +(def text-container {:margin-top 24}) + +(def text + {:margin-bottom 1 + :color colors/white-opa-70}) diff --git a/src/status_im/contexts/settings/keycard/view.cljs b/src/status_im/contexts/settings/keycard/view.cljs new file mode 100644 index 00000000000..1e030aaff9b --- /dev/null +++ b/src/status_im/contexts/settings/keycard/view.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.settings.keycard.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [status-im.contexts.settings.keycard.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + [:<> + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/keycard)}] + [rn/view {:style style/container} + [quo/small-option-card + {:variant :main + :title (i18n/label :t/get-keycard) + :subtitle (i18n/label :t/secure-wallet-card) + :button-label (i18n/label :t/buy-keycard) + :button-props {:type :primary + :icon-right :i/external} + :accessibility-label :get-keycard + :image (resources/get-image :keycard-buy) + :on-press #(rf/dispatch [:browser.ui/open-url constants/get-keycard-url])}] + [rn/view {:style style/text-container} + [quo/text + {:style style/text + :size :paragraph-2 + :weight :medium} + (i18n/label :t/own-keycard)]] + [quo/small-option-card + {:variant :icon + :title (i18n/label :t/manage-keycard) + :subtitle (i18n/label :t/setup-keycard-description) + :accessibility-label :setup-keycard + :image (resources/get-image :use-keycard) + :on-press (fn [] + (rf/dispatch [:open-modal :screen/keycard.check + {:on-press + #(rf/dispatch [:keycard/manage.check-card])}]))}]]]) diff --git a/src/status_im/contexts/settings/language_and_currency/currency/style.cljs b/src/status_im/contexts/settings/language_and_currency/currency/style.cljs new file mode 100644 index 00000000000..c860bcc58b2 --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/currency/style.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.settings.language-and-currency.currency.style) + +(def input-container + {:padding-horizontal 20 + :padding-vertical 8}) + +(def empty-results + {:margin-top 100}) diff --git a/src/status_im/contexts/settings/language_and_currency/currency/utils.cljs b/src/status_im/contexts/settings/language_and_currency/currency/utils.cljs new file mode 100644 index 00000000000..69632036f82 --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/currency/utils.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.settings.language-and-currency.currency.utils + (:require [status-im.constants :as constants])) + +(defn make-currency-item + "This function generates props for quo/category component item" + [{:keys [currency selected-currency on-change]}] + {:title (if (:token? currency) + (:short-name currency) + (str (:short-name currency) " · " (:symbol currency))) + :description :text + :description-props {:text (:name currency)} + :container-style {:height constants/currency-item-height} + :image (when (:token? currency) :token) + :image-props {:token (:id currency) + :size :size-20} + :action :selector + :action-props {:type :radio + :blur? true + :checked? (= selected-currency (:id currency)) + :on-change #(on-change (:id currency))}}) diff --git a/src/status_im/contexts/settings/language_and_currency/currency/view.cljs b/src/status_im/contexts/settings/language_and_currency/currency/view.cljs new file mode 100644 index 00000000000..6cb785b10b4 --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/currency/view.cljs @@ -0,0 +1,84 @@ +(ns status-im.contexts.settings.language-and-currency.currency.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.constants :as constants] + [status-im.contexts.settings.language-and-currency.currency.style :as style] + [status-im.contexts.settings.language-and-currency.currency.utils :as utils] + [status-im.contexts.settings.language-and-currency.data-store :as data-store] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn- get-item-layout + [_ index] + #js {:length constants/currency-item-height + :offset (* constants/currency-item-height index) + :index index}) + +(defn- settings-category-view + [{:keys [title data]} _ _ {:keys [selected-currency on-currency-press]}] + [rn/delay-render + [quo/category + {:label title + :data (map (fn [currency] + (utils/make-currency-item + {:currency currency + :selected-currency selected-currency + :on-change on-currency-press})) + data) + :blur? true + :list-type :settings}]]) + +(defn view + [] + (let [[search-text set-search-text] (rn/use-state "") + on-currency-press (rn/use-callback #(rf/dispatch [:profile.settings/update-currency + %])) + selected-currency (rf/sub [:profile/currency]) + currencies (rf/sub [:currencies/categorized search-text]) + on-change-text (rn/use-callback + (debounce/debounce + #(set-search-text %) + 300)) + formatted-data (rn/use-memo + #(data-store/get-formatted-currency-data currencies) + [search-text currencies])] + [quo/overlay + {:type :shell + :top-inset? true} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [quo/page-top + {:title (i18n/label :t/currency)}] + [quo/input + {:small? true + :blur? true + :container-style style/input-container + :placeholder (i18n/label :t/search-currencies) + :icon-name :i/search + :on-change-text on-change-text}] + (when (zero? (:total currencies)) + [quo/empty-state + {:title (i18n/label :t/no-result) + :image (resources/get-themed-image :no-assets :dark) + :container-style style/empty-results + :description (i18n/label :t/try-with-different-currency)}]) + [rn/flat-list + {:data formatted-data + :render-data {:selected-currency selected-currency + :on-currency-press on-currency-press} + :render-fn settings-category-view + :get-item-layout get-item-layout + :initial-num-to-render 14 + :max-to-render-per-batch 20 + :scroll-event-throttle 16 + :shows-vertical-scroll-indicator false + :bounces false + :over-scroll-mode :never}]])) diff --git a/src/status_im/contexts/settings/language_and_currency/data_store.cljs b/src/status_im/contexts/settings/language_and_currency/data_store.cljs new file mode 100644 index 00000000000..fd08c1b1516 --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/data_store.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.settings.language-and-currency.data-store + (:require [clojure.set :as set] + [utils.i18n :as i18n])) + +(defn rpc->currency + [currency] + (some-> currency + (dissoc :unicode) + (set/rename-keys + {:shortName :short-name + :isPopular :popular? + :isToken :token?}) + (update :id keyword))) + +(defn rpc->currencies + [currencies] + (map rpc->currency currencies)) + +(defn get-formatted-currency-data + [{:keys [popular crypto other]}] + (concat + (when (seq popular) + [{:title (i18n/label :t/popular-currencies) + :data popular}]) + (when (seq crypto) + [{:title (i18n/label :t/crypto) + :data crypto}]) + (when (seq other) + [{:title (i18n/label :t/all-currencies) + :data other}]))) diff --git a/src/status_im/contexts/settings/language_and_currency/data_store_test.cljs b/src/status_im/contexts/settings/language_and_currency/data_store_test.cljs new file mode 100644 index 00000000000..d20fac0a8da --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/data_store_test.cljs @@ -0,0 +1,56 @@ +(ns status-im.contexts.settings.language-and-currency.data-store-test + (:require + [cljs.test :refer-macros [deftest is testing]] + matcher-combinators.test + [status-im.contexts.settings.language-and-currency.data-store :as sut])) + +(def raw-currency-popular + {:id "usd" + :shortName "USD" + :name "US Dollar" + :symbol "$" + :emoji "🇺🇸" + :isPopular true + :isToken false + :imageSource "https://example.com/image.png"}) + +(def raw-currency-token + {:id "btc" + :shortName "BTC" + :name "Bitcoin" + :symbol "" + :emoji "" + :isPopular false + :isToken true + :imageSource "https://example.com/image.png"}) + +(deftest rpc->currency-test + (testing "transforms a currency" + (is + (match? {:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false} + (sut/rpc->currency raw-currency-popular))))) + +(deftest rpc->currencies-test + (testing "transforms and sorts raw keypairs" + (is + (match? [(sut/rpc->currency raw-currency-popular) + (sut/rpc->currency raw-currency-token)] + (sut/rpc->currencies [raw-currency-popular + raw-currency-token]))))) + +(deftest get-formatted-currency-data-test + (testing "returns formatted currency data" + (is + (match? [{:title "Popular currencies" + :data [(sut/rpc->currency raw-currency-popular)]} + {:title "Crypto" + :data [(sut/rpc->currency raw-currency-token)]}] + (sut/get-formatted-currency-data {:popular [(sut/rpc->currency raw-currency-popular)] + :crypto [(sut/rpc->currency raw-currency-token)] + :other []}))))) diff --git a/src/status_im/contexts/settings/language_and_currency/events.cljs b/src/status_im/contexts/settings/language_and_currency/events.cljs new file mode 100644 index 00000000000..50da7bf5dae --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/events.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.settings.language-and-currency.events + (:require [status-im.contexts.settings.language-and-currency.data-store :as data-store] + [utils.collection] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :settings/get-currencies-success + (fn [{:keys [db]} [currencies]] + (let [all-currencies (data-store/rpc->currencies currencies)] + {:db (assoc db + :currencies + (utils.collection/index-by :id all-currencies))}))) + +(rf/reg-event-fx :settings/get-currencies + (fn [] + {:fx [[:json-rpc/call + [{:method "appgeneral_getCurrencies" + :on-success [:settings/get-currencies-success] + :on-error [:log-rpc-error {:event :settings/get-currencies}]}]]]})) diff --git a/src/status_im/contexts/settings/language_and_currency/events_test.cljs b/src/status_im/contexts/settings/language_and_currency/events_test.cljs new file mode 100644 index 00000000000..85fd9e74f3a --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/events_test.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.settings.language-and-currency.events-test + (:require + [cljs.test :refer-macros [is]] + matcher-combinators.test + status-im.contexts.settings.language-and-currency.events + [test-helpers.unit :as h])) + +(def raw-currency-popular + {:id "usd" + :shortName "USD" + :name "US Dollar" + :symbol "$" + :emoji "🇺🇸" + :isPopular true + :isToken false + :imageSource "https://example.com/image.png"}) + +(h/deftest-event :settings/get-currencies-success + [event-id dispatch] + (let [expected-effects {:db {:currencies {:usd {:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false}}}}] + (is (match? expected-effects (dispatch [event-id [raw-currency-popular]]))))) diff --git a/src/status_im/contexts/settings/language_and_currency/view.cljs b/src/status_im/contexts/settings/language_and_currency/view.cljs new file mode 100644 index 00000000000..ccdba1c7e8f --- /dev/null +++ b/src/status_im/contexts/settings/language_and_currency/view.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.settings.language-and-currency.view + (:require [quo.core :as quo] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn- on-currency-press + [] + (rf/dispatch [:open-modal :screen/settings.currency-selection])) + +(defn view + [] + (let [{:keys [name short-name token?] + :as currency} (rf/sub [:profile/currency-info]) + currency-title (if token? name (str short-name " · " (:symbol currency)))] + [quo/overlay + {:type :shell + :top-inset? true} + [quo/page-nav + {:background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [quo/page-top + {:title (i18n/label :t/language-and-currency)}] + [quo/category + {:label (i18n/label :t/currency) + :data [{:title currency-title + :on-press on-currency-press + :description :text + :action :arrow + :description-props {:text name}}] + :blur? true + :list-type :settings}]])) diff --git a/src/status_im/contexts/settings/privacy_and_security/profile_picture/view.cljs b/src/status_im/contexts/settings/privacy_and_security/profile_picture/view.cljs new file mode 100644 index 00000000000..014ef762538 --- /dev/null +++ b/src/status_im/contexts/settings/privacy_and_security/profile_picture/view.cljs @@ -0,0 +1,118 @@ +(ns status-im.contexts.settings.privacy-and-security.profile-picture.view + (:require + [quo.context] + [quo.core :as quo] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def pictures-visibility->label + {constants/profile-pictures-visibility-everyone (i18n/label :t/everyone) + constants/profile-pictures-visibility-contacts-only (i18n/label :t/contacts) + constants/profile-pictures-visibility-none (i18n/label :t/no-one)}) + +(def pictures-show-to->label + {constants/profile-pictures-show-to-everyone (i18n/label :t/everyone) + constants/profile-pictures-show-to-contacts-only (i18n/label :t/contacts) + constants/profile-pictures-show-to-none (i18n/label :t/no-one)}) + +(defn- update-profile + [k v] + (rf/dispatch [:profile.settings/profile-update k v]) + (rf/dispatch [:hide-bottom-sheet])) + +(defn- update-pictures-visibility-everyone + [] + (update-profile :profile-pictures-visibility constants/profile-pictures-visibility-everyone)) + +(defn- update-pictures-visibility-contacts-only + [] + (update-profile :profile-pictures-visibility constants/profile-pictures-visibility-contacts-only)) + +(defn- update-pictures-visibility-nobody + [] + (update-profile :profile-pictures-visibility constants/profile-pictures-visibility-none)) + +(defn- update-pictures-show-to-everyone + [] + (update-profile :profile-pictures-show-to constants/profile-pictures-show-to-everyone)) + +(defn- update-pictures-show-to-contacts-only + [] + (update-profile :profile-pictures-show-to constants/profile-pictures-show-to-contacts-only)) + +(defn- update-pictures-show-to-nobody + [] + (update-profile :profile-pictures-show-to constants/profile-pictures-show-to-none)) + +(defn options-for-profile-pictures-visibility + [setting] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title (i18n/label :t/show-profile-pictures)}] + [quo/action-drawer + [[{:icon :i/globe + :accessibility-label :see-profile-pictures-from-everyone + :label (i18n/label :t/everyone) + :on-press update-pictures-visibility-everyone + :state (when (= constants/profile-pictures-visibility-everyone setting) + :selected)} + {:icon :i/contact + :icon-color customization-color + :accessibility-label :see-profile-pictures-from-contacts + :label (i18n/label :t/contacts) + :on-press update-pictures-visibility-contacts-only + :state (when (= constants/profile-pictures-visibility-contacts-only setting) + :selected)} + {:icon :i/hide + :accessibility-label :see-profile-pictures-from-nobody + :label (i18n/label :t/no-one) + :add-divider? true + :on-press update-pictures-visibility-nobody + :state (when (= constants/profile-pictures-visibility-none setting) + :selected)}]]]])) + +(defn options-for-profile-pictures-show-to + [setting] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title (i18n/label :t/show-profile-pictures-to)}] + [quo/action-drawer + [[{:icon :i/globe + :accessibility-label :show-profile-pictures-to-everyone + :label (i18n/label :t/everyone) + :on-press update-pictures-show-to-everyone + :state (when (= constants/profile-pictures-show-to-everyone setting) + :selected)} + {:icon :i/contact + :icon-color customization-color + :accessibility-label :show-profile-pictures-to-contacts + :label (i18n/label :t/contacts) + :on-press update-pictures-show-to-contacts-only + :state (when (= constants/profile-pictures-show-to-contacts-only setting) + :selected)} + {:icon :i/hide + :accessibility-label :show-profile-pictures-to-nobody + :label (i18n/label :t/no-one) + :on-press update-pictures-show-to-nobody + :state (when (= constants/profile-pictures-show-to-none setting) + :selected) + :add-divider? true}]]]])) + +(defn setting-profile-pictures-visibility + [pictures-visibility on-press] + {:title (i18n/label :t/show-profile-pictures) + :description :text + :description-props {:text (pictures-visibility->label pictures-visibility)} + :blur? true + :action :arrow + :on-press on-press}) + +(defn setting-profile-pictures-show-to + [pictures-show-to on-press] + {:title (i18n/label :t/show-profile-pictures-to) + :description :text + :description-props {:text (pictures-show-to->label pictures-show-to)} + :blur? true + :action :arrow + :on-press on-press}) diff --git a/src/status_im/contexts/settings/privacy_and_security/share_usage/view.cljs b/src/status_im/contexts/settings/privacy_and_security/share_usage/view.cljs new file mode 100644 index 00000000000..591c6120331 --- /dev/null +++ b/src/status_im/contexts/settings/privacy_and_security/share_usage/view.cljs @@ -0,0 +1,114 @@ +(ns status-im.contexts.settings.privacy-and-security.share-usage.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.privacy.view :as privacy] + [status-im.contexts.settings.privacy-and-security.style :as privacy-and-security.style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:private network-behavior-points + [:t/number-of-messages-sent + :t/connected-peers + :t/successful-messages-rate + :t/connection-type + :t/os-app-version-bandwidth]) + +(def ^:private app-interactions-points + [:t/action-logs + :t/ip-addresses-uuid]) + +(def ^:private not-receive-points + [:t/your-profile-information + :t/your-addresses + :t/information-you-input-and-send]) + +(defn- get-category-data + [{:keys [title description points on-toggle toggle-checked? lock?]}] + {:title title + :description (when description :text) + :description-props (when description {:text description}) + :blur? true + :action (when on-toggle :selector) + :action-props (when on-toggle + {:on-change on-toggle + :checked? toggle-checked?}) + :content [rn/view {:style {:margin-top 8}} + (for [label points] + ^{:key label} + [quo/markdown-list + {:description (i18n/label label) + :blur? true + :type (when lock? :lock)}])]}) + +(defn- on-privacy-policy-press + [] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [quo.context/provider {:theme :dark} + [privacy/privacy-statement]]) + :shell? true}])) + +(defn- privacy-policy-text + [] + [quo/text {:style {:text-align :center}} + [quo/text + {:style {:color colors/white-opa-50} + :size :paragraph-2} + (i18n/label :t/more-details-in-privacy-policy-settings-1)] + [quo/text + {:size :paragraph-2 + :on-press on-privacy-policy-press} + (i18n/label :t/more-details-in-privacy-policy-2)]]) + +(defn view + [] + (let [insets safe-area/insets + telemetry-enabled? (rf/sub [:profile/telemetry-enabled?]) + centralized-metrics-enabled? (rf/sub [:centralized-metrics/enabled?])] + [quo/overlay + {:type :shell + :container-style (privacy-and-security.style/page-wrapper (:top insets))} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/page-top + {:title (i18n/label :t/share-usage-data) + :title-accessibility-label :title-label + :description :text + :description-text (i18n/label :t/collecting-usage-data)}] + [rn/scroll-view {:style {:flex 1}} + [quo/category + {:data [(get-category-data + {:toggle-checked? telemetry-enabled? + :on-toggle #(rf/dispatch [:profile.settings/toggle-telemetry]) + :title (i18n/label :t/network-behavior) + :description (i18n/label :t/will-be-shared-from-the-current-profile) + :points network-behavior-points}) + (get-category-data {:toggle-checked? centralized-metrics-enabled? + :on-toggle #(rf/dispatch + [:centralized-metrics/toggle-centralized-metrics + (not centralized-metrics-enabled?)]) + :title (i18n/label :t/app-interactions) + :description (i18n/label :t/will-be-shared-from-all-profiles) + :points app-interactions-points})] + :blur? true + :list-type :settings}] + [quo/category + {:data [(get-category-data {:title (i18n/label :t/what-we-wont-receive) + :points not-receive-points + :lock? true})] + :blur? true + :list-type :settings}]] + [rn/view + {:align-items :center + :padding-horizontal 20 + :padding-bottom (:bottom insets)} + [privacy-policy-text]]])) diff --git a/src/status_im/contexts/settings/privacy_and_security/style.cljs b/src/status_im/contexts/settings/privacy_and_security/style.cljs new file mode 100644 index 00000000000..3786a649e7a --- /dev/null +++ b/src/status_im/contexts/settings/privacy_and_security/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.settings.privacy-and-security.style) + +(def title-container + {:flex 0 + :padding-horizontal 20 + :margin-vertical 12}) + +(defn page-wrapper + [top-inset] + {:padding-top top-inset + :flex 1}) diff --git a/src/status_im/contexts/settings/privacy_and_security/view.cljs b/src/status_im/contexts/settings/privacy_and_security/view.cljs new file mode 100644 index 00000000000..1956de16b19 --- /dev/null +++ b/src/status_im/contexts/settings/privacy_and_security/view.cljs @@ -0,0 +1,127 @@ +(ns status-im.contexts.settings.privacy-and-security.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.contexts.settings.privacy-and-security.profile-picture.view :as profile-picture.view] + [status-im.contexts.settings.privacy-and-security.style :as style] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- setting-preview-privacy + [preview-privacy? customization-color on-change] + {:title (i18n/label (if platform/android? + :t/hide-content-when-switching-apps + :t/hide-content-when-switching-apps-ios)) + :blur? true + :action :selector + :action-props {:on-change on-change + :checked? preview-privacy? + :id :preview-privacy + :customization-color customization-color}}) + +(defn- show-privacy-mode-sheet + [] + (rf/dispatch [:privacy-mode/show-bottom-sheet {:theme :dark :shell? true}])) + +(defn- setting-privacy-mode + [privacy-mode-enabled? customization-color] + {:title (i18n/label :t/privacy-mode) + :blur? true + :action :selector + :action-props {:on-change show-privacy-mode-sheet + :checked? privacy-mode-enabled? + :customization-color customization-color}}) + +(defn view + [] + (let [customization-color (rf/sub [:profile/customization-color]) + privacy-mode-enabled? (rf/sub [:privacy-mode/privacy-mode-enabled?]) + preview-privacy? (rf/sub [:profile/preview-privacy?]) + news-feed-enabled? (rf/sub [:profile/news-feed-enabled?]) + see-profile-pictures-from (rf/sub [:profile/pictures-visibility]) + show-profile-pictures-to (rf/sub [:multiaccount/profile-pictures-show-to]) + + toggle-preview-privacy + (rn/use-callback (fn [] + (rf/dispatch [:profile.settings/change-preview-privacy + (not preview-privacy?)])) + [preview-privacy?]) + + toggle-news-feed + (rn/use-callback (fn [] + (rf/dispatch [:profile.settings/profile-update :news-feed-enabled? + (not news-feed-enabled?)]) + (rf/dispatch [:profile.settings/profile-update :news-rss-enabled? + (not news-feed-enabled?)])) + [news-feed-enabled?]) + + open-see-profile-pictures-from-options + (rn/use-callback (fn [] + (rf/dispatch + [:show-bottom-sheet + {:theme :dark + :blur? true + :content (fn [] + [profile-picture.view/options-for-profile-pictures-visibility + see-profile-pictures-from])}])) + [see-profile-pictures-from]) + + open-show-profile-pictures-to-options + (rn/use-callback (fn [] + (rf/dispatch + [:show-bottom-sheet + {:theme :dark + :blur? true + :content (fn [] + [profile-picture.view/options-for-profile-pictures-show-to + show-profile-pictures-to])}])) + [show-profile-pictures-to])] + [quo/overlay + {:type :shell + :container-style (style/page-wrapper safe-area/top)} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press events-helper/navigate-back}] + [quo/standard-title + {:title (i18n/label :t/privacy-and-security) + :container-style style/title-container + :accessibility-label :privacy-and-security-header + :customization-color customization-color}] + [quo/category + {:key :category + :data [(when (ff/enabled? ::ff/profile-pictures-visibility) + (profile-picture.view/setting-profile-pictures-visibility + see-profile-pictures-from + open-see-profile-pictures-from-options)) + (when (ff/enabled? ::ff/profile-pictures-visibility) + (profile-picture.view/setting-profile-pictures-show-to + show-profile-pictures-to + open-show-profile-pictures-to-options)) + (when (ff/enabled? ::ff/privacy-mode-ui) + (setting-privacy-mode privacy-mode-enabled? customization-color)) + (setting-preview-privacy preview-privacy? customization-color toggle-preview-privacy) + {:title (i18n/label :t/share-usage-data) + :description :text + :description-props {:text (i18n/label :t/from-all-profiles-on-device)} + :blur? true + :action :arrow + :action-props {:on-change #(rf/dispatch [:open-modal + :screen/settings.share-usage-data])}} + {:title (i18n/label :t/status-news-rss) + :description :text + :description-props {:text (i18n/label :t/rss-privacy-warning)} + :blur? true + :action :selector + :action-props {:on-change toggle-news-feed + :checked? news-feed-enabled? + :id :news-feed + :customization-color customization-color}}] + :blur? true + :list-type :settings}]])) diff --git a/src/status_im/contexts/settings/wallet/data_store.cljs b/src/status_im/contexts/settings/wallet/data_store.cljs new file mode 100644 index 00000000000..1979aceb0bd --- /dev/null +++ b/src/status_im/contexts/settings/wallet/data_store.cljs @@ -0,0 +1,63 @@ +(ns status-im.contexts.settings.wallet.data-store) + +(defn make-accounts-fully-operable + "Updates accounts to be fully operable based on specified key-uids and an optional operable condition. + + Parameters: + - :accounts (map): A map of account addresses to account details. + - :key-uids-set (set): A set of key-uids that need to be checked and updated. + - :operable-condition (keyword, optional): The condition that the account's operability must meet to be updated. + If nil or not provided, the function will update all accounts. + + Returns: + - A new map with accounts updated to be fully operable where the conditions are met." + [{:keys [accounts key-uids-set operable-condition]}] + (reduce-kv + (fn [acc k {:keys [key-uid] :as account}] + (if (and (contains? key-uids-set key-uid) + (or (nil? operable-condition) + (= (:operable account) operable-condition))) + (assoc acc k (assoc account :operable :fully)) + (assoc acc k account))) + {} + accounts)) + +(defn- make-keypairs-accounts-fully-operable + [accounts operable-condition] + (map (fn [account] + (if (or (nil? operable-condition) (= (:operable account) operable-condition)) + (assoc account :operable :fully) + account)) + accounts)) + +(defn make-keypairs-fully-operable + "Updates keypairs' accounts to be fully operable based on specified key-uids and an optional operable condition. + + Parameters: + - :keypairs (map): A map of keypair key-uid to keypair details. + - :key-uids-set (set): A set of key-uids that need to be checked and updated. + - :operable-condition (keyword, optional): The condition that the keypair's accounts' operability must meet to be updated. + If nil or not provided, the function will update all keypairs' accounts. + + Returns: + - A new map with keypairs updated to be fully operable where the conditions are met." + [{:keys [keypairs key-uids-set operable-condition]}] + (reduce-kv (fn [acc k keypair] + (if (contains? key-uids-set k) + (assoc acc + k + (-> keypair + (update :accounts make-keypairs-accounts-fully-operable operable-condition) + (assoc :lowest-operability :fully))) + (assoc acc k keypair))) + {} + keypairs)) + +(defn get-keypair-key-uids-set-from-addresses + [db addresses] + (reduce (fn [key-uid-set address] + (if-let [account-key-uid (get-in db [:wallet :accounts address :key-uid])] + (conj key-uid-set account-key-uid) + key-uid-set)) + #{} + addresses)) diff --git a/src/status_im/contexts/settings/wallet/effects.cljs b/src/status_im/contexts/settings/wallet/effects.cljs new file mode 100644 index 00000000000..3ed7219e000 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/effects.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.settings.wallet.effects + (:require [native-module.core :as native-module] + [promesa.core :as promesa] + [status-im.contexts.syncing.utils :as sync-utils] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(rf/reg-fx :effects.syncing/export-keypairs-keystores + (fn [{:keys [key-uid sha3-pwd keypair-key-uid on-success on-fail]}] + (let [config-map (transforms/clj->json {:senderConfig {:loggedInKeyUid key-uid + :keystorePath "" + :keypairsToExport [keypair-key-uid] + :password (security/safe-unmask-data + sha3-pwd)} + :serverConfig {:timeout 0}})] + (-> (native-module/get-connection-string-for-exporting-keypairs-keystores + config-map) + (promesa/then (fn [response] + (if (sync-utils/valid-connection-string? response) + (on-success response) + (on-fail (js/Error. + "generic-error: failed to get connection string"))))) + (promesa/catch on-fail))))) + +(rf/reg-fx :effects.syncing/import-keypairs-keystores + (fn [{:keys [key-uid sha3-pwd keypairs-key-uids connection-string on-success on-fail]}] + (let [config-map (transforms/clj->json {:receiverConfig + {:loggedInKeyUid key-uid + :keystorePath "" + :password (security/safe-unmask-data + sha3-pwd) + :keypairsToImport keypairs-key-uids}})] + (-> (native-module/input-connection-string-for-importing-keypairs-keystores + connection-string + config-map) + (promesa/then #(on-success keypairs-key-uids)) + (promesa/catch on-fail))))) diff --git a/src/status_im/contexts/settings/wallet/events.cljs b/src/status_im/contexts/settings/wallet/events.cljs new file mode 100644 index 00000000000..ed66e399be6 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/events.cljs @@ -0,0 +1,309 @@ +(ns status-im.contexts.settings.wallet.events + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [react-native.platform :as platform] + [status-im.contexts.settings.wallet.data-store :as data-store] + [status-im.contexts.settings.wallet.network-settings.testnet-mode.view :as testnet] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(rf/reg-event-fx + :wallet/rename-keypair-success + (fn [{:keys [db]} [key-uid name]] + {:db (update-in db [:wallet :keypairs key-uid] assoc :name name) + :fx [[:dispatch [:navigate-back]] + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/key-pair-name-updated)}]]]})) + +(defn rename-keypair + [_ [{:keys [key-uid keypair-name]}]] + {:fx [[:json-rpc/call + [{:method "accounts_updateKeypairName" + :params [key-uid keypair-name] + :on-success [:wallet/rename-keypair-success key-uid keypair-name] + :on-error #(log/info "failed to rename keypair " %)}]]]}) + +(rf/reg-event-fx :wallet/rename-keypair rename-keypair) + +(defn get-keypair-export-connection + [{:keys [db]} [{:keys [sha3-pwd keypair-key-uid callback]}]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:effects.syncing/export-keypairs-keystores + {:key-uid key-uid + :sha3-pwd sha3-pwd + :keypair-key-uid keypair-key-uid + :on-success (fn [connect-string] + (callback connect-string) + (rf/dispatch [:hide-bottom-sheet])) + :on-fail (fn [error] + (rf/dispatch [:toasts/upsert + {:type :negative + :text (.-message error)}]))}]]})) + +(rf/reg-event-fx :wallet/get-keypair-export-connection get-keypair-export-connection) + +(rf/reg-event-fx :wallet/remove-keypair-success + (fn [{:keys [db]} [key-uid]] + {:db (update-in db [:wallet :keypairs] dissoc key-uid) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/key-pair-removed)}]]]})) + +(defn remove-keypair + [_ [key-uid]] + {:fx [[:json-rpc/call + [{:method "accounts_deleteKeypair" + :params [key-uid] + :on-success [:wallet/remove-keypair-success key-uid] + :on-error #(log/error "failed to remove keypair " {:error %})}]]]}) + +(rf/reg-event-fx :wallet/remove-keypair remove-keypair) + +(defn make-keypairs-accounts-fully-operable + [{:keys [db]} [key-uids-to-update]] + (let [key-uids-set (set key-uids-to-update) + single-keypair-to-update? (= (count key-uids-to-update) 1) + keypair-name (when single-keypair-to-update? + (let [key-uid (first key-uids-set)] + (get-in db [:wallet :keypairs key-uid :name])))] + {:db (-> + db + (update-in [:wallet :accounts] + #(data-store/make-accounts-fully-operable {:accounts % + :key-uids-set key-uids-set})) + (update-in [:wallet :keypairs] + #(data-store/make-keypairs-fully-operable {:keypairs % + :key-uids-set key-uids-set}))) + :fx [[:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (if single-keypair-to-update? + (i18n/label :t/key-pair-imported-successfully {:name keypair-name}) + (i18n/label :t/key-pairs-successfully-imported + {:count (count key-uids-to-update)}))}]]]})) + +(rf/reg-event-fx :wallet/make-keypairs-accounts-fully-operable make-keypairs-accounts-fully-operable) + + +(rf/reg-event-fx :wallet/connection-string-for-import-keypairs-failed + (fn [{:keys [db]} [keypairs-key-uids error]] + (let [error-message (-> error ex-data :error) + incorrect-keypair? (string/includes? + error-message + "one or more expected keystore files are not found among the sent files") + single-keypair-to-update? (= (count keypairs-key-uids) 1) + keypair-name (when single-keypair-to-update? + (let [key-uid (first keypairs-key-uids)] + (get-in db [:wallet :keypairs key-uid :name]))) + toast-message (cond + (and single-keypair-to-update? incorrect-keypair?) + (i18n/label + :t/this-qr-does-not-contain-key-pair + {:name keypair-name}) + + (and (not single-keypair-to-update?) incorrect-keypair?) + (i18n/label + :t/this-qr-does-not-contain-any-missing-key-pair) + + :else + error-message)] + (log/error "failed to import missing key pairs with connection string" + {:error error-message}) + (rf/dispatch [:toasts/upsert + {:type :negative + :theme :dark + :text toast-message}])))) + +(defn connection-string-for-import-keypairs + [{:keys [db]} [{:keys [sha3-pwd keypairs-key-uids connection-string]}]] + (let [key-uid (get-in db [:profile/profile :key-uid])] + {:fx [[:effects.syncing/import-keypairs-keystores + {:key-uid key-uid + :sha3-pwd sha3-pwd + :keypairs-key-uids keypairs-key-uids + :connection-string connection-string + :on-success (fn [key-uids] + (rf/dispatch [:wallet/make-keypairs-accounts-fully-operable key-uids])) + :on-fail (fn [error] + (rf/dispatch [:wallet/connection-string-for-import-keypairs-failed + keypairs-key-uids + error]))}]]})) + +(rf/reg-event-fx :wallet/connection-string-for-import-keypairs connection-string-for-import-keypairs) + +(defn success-keypair-qr-scan + [_ [connection-string keypairs-key-uids]] + {:fx [[:dispatch + [:standard-auth/authorize-with-password + {:blur? true + :hide-biometrics-button? true + :theme :dark + :auth-button-label (i18n/label :t/confirm) + :on-auth-success (fn [password] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:wallet/connection-string-for-import-keypairs + {:connection-string connection-string + :keypairs-key-uids keypairs-key-uids + :sha3-pwd password}]))}]]]}) + +(rf/reg-event-fx :wallet/success-keypair-qr-scan success-keypair-qr-scan) + +(defn wallet-validate-seed-phrase + [_ [seed-phrase on-success on-error]] + {:fx [[:effects.profile/validate-recovery-phrase [seed-phrase on-success on-error]]]}) + +(rf/reg-event-fx :wallet/validate-seed-phrase wallet-validate-seed-phrase) + +(defn make-seed-phrase-keypair-fully-operable + [_ [mnemonic password on-success on-error]] + {:fx [[:json-rpc/call + [{:method "accounts_makeSeedPhraseKeypairFullyOperable" + :params [(security/safe-unmask-data mnemonic) + (-> password security/safe-unmask-data native-module/sha3)] + :on-success on-success + :on-error on-error}]]]}) + +(rf/reg-event-fx :wallet/make-seed-phrase-keypair-fully-operable make-seed-phrase-keypair-fully-operable) + +(defn make-private-key-keypair-fully-operable + [_ [private-key password on-success on-error]] + {:fx [[:json-rpc/call + [{:method "accounts_makePrivateKeyKeypairFullyOperable" + :params [(security/safe-unmask-data private-key) + (security/safe-unmask-data password)] + :on-success on-success + :on-error on-error}]]]}) + +(rf/reg-event-fx :wallet/make-private-key-keypair-fully-operable make-private-key-keypair-fully-operable) + +(defn create-account-from-private-key + [_ [private-key on-success on-error]] + {:fx [[:effects.wallet/create-account-from-private-key + {:private-key (security/safe-unmask-data private-key) + :on-success on-success + :on-error on-error}]]}) + +(rf/reg-event-fx :wallet/create-account-from-private-key create-account-from-private-key) + +(defn verify-private-key-for-keypair + [_ [keypair-key-uid private-key on-success on-error]] + {:fx [[:effects.wallet/verify-private-key-for-keypair + {:keypair-key-uid keypair-key-uid + :private-key (security/safe-unmask-data private-key) + :on-success on-success + :on-error on-error}]]}) + +(rf/reg-event-fx :wallet/verify-private-key-for-keypair verify-private-key-for-keypair) + +(defn import-missing-keypair-by-seed-phrase + [_ [{:keys [keypair-key-uid seed-phrase password on-success on-error]}]] + {:fx [[:effects.wallet/import-missing-keypair-by-seed-phrase + {:keypair-key-uid keypair-key-uid + :seed-phrase seed-phrase + :password password + :on-success (fn [] + (rf/dispatch [:wallet/make-keypairs-accounts-fully-operable + #{keypair-key-uid}]) + (rf/call-continuation on-success)) + :on-error (fn [error] + (rf/dispatch [:wallet/import-missing-keypair-by-seed-phrase-failed error]) + (log/error "failed to import missing keypair with seed phrase" + {:error error}) + (rf/call-continuation on-error error))}]]}) + +(rf/reg-event-fx :wallet/import-missing-keypair-by-seed-phrase import-missing-keypair-by-seed-phrase) + +(defn import-missing-keypair-by-seed-phrase-failed + [_ [error]] + (let [error-type (-> error ex-message keyword) + error-data (ex-data error)] + (when-not (= error-type :import-missing-keypair-by-seed-phrase/import-error) + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text (:error error-data)}]]]}))) + +(rf/reg-event-fx :wallet/import-missing-keypair-by-seed-phrase-failed + import-missing-keypair-by-seed-phrase-failed) + +(defn import-missing-keypair-by-private-key + [_ [{:keys [keypair-key-uid private-key password on-success on-error]}]] + {:fx [[:dispatch + [:wallet/make-private-key-keypair-fully-operable private-key password + (fn [] + (rf/dispatch [:wallet/make-keypairs-accounts-fully-operable #{keypair-key-uid}]) + (rf/call-continuation on-success)) + (fn [error] + (rf/dispatch [:wallet/import-missing-keypair-by-private-key-failed error]) + (log/error "failed to import missing keypair with private key" {:error error}) + (rf/call-continuation on-error error))]]]}) + +(rf/reg-event-fx :wallet/import-missing-keypair-by-private-key import-missing-keypair-by-private-key) + +(defn import-missing-keypair-by-private-key-failed + [_ [error]] + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text error}]]]}) + +(rf/reg-event-fx :wallet/import-missing-keypair-by-private-key-failed + import-missing-keypair-by-private-key-failed) + +(defn make-partially-operable-accounts-fully-operable-success + [{:keys [db]} [addresses]] + (let [key-uids-to-update (data-store/get-keypair-key-uids-set-from-addresses db addresses)] + {:db (-> db + (update-in [:wallet :accounts] + #(data-store/make-accounts-fully-operable {:accounts % + :key-uids-set key-uids-to-update + :operable-condition :partially})) + (update-in [:wallet :keypairs] + #(data-store/make-keypairs-fully-operable {:keypairs % + :key-uids-set key-uids-to-update + :operable-condition :partially})))})) + +(rf/reg-event-fx :wallet/make-partially-operable-accounts-fully-operable-success + make-partially-operable-accounts-fully-operable-success) + +(defn make-partially-operable-accounts-fully-operable + [_ [{:keys [password on-success on-error]}]] + {:fx [[:json-rpc/call + [{:method "accounts_makePartiallyOperableAccoutsFullyOperable" + :params [(security/safe-unmask-data password)] + :on-success (fn [addresses] + (when addresses + (rf/dispatch [:wallet/make-partially-operable-accounts-fully-operable-success + addresses])) + (on-success)) + :on-error (fn [error] + (log/error "failed to make partially accounts fully operable" {:error error}) + (on-error error))}]]]}) + +(rf/reg-event-fx :wallet/make-partially-operable-accounts-fully-operable + make-partially-operable-accounts-fully-operable) + +(rf/reg-event-fx :wallet/show-disable-testnet-mode-confirmation + (fn [{:keys [db]} _] + (let [theme (:theme db) + blur? (and (not platform/android?) (= theme :dark))] + {:fx [[:dispatch + [:show-bottom-sheet + {:content (fn [] [testnet/view + {:enable? false + :blur? blur?}]) + :shell? blur? + :theme (when-not platform/android? theme)}]]]}))) diff --git a/src/status_im/contexts/settings/wallet/events_test.cljs b/src/status_im/contexts/settings/wallet/events_test.cljs new file mode 100644 index 00000000000..0858a236527 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/events_test.cljs @@ -0,0 +1,144 @@ +(ns status-im.contexts.settings.wallet.events-test + (:require + [cljs.test :refer-macros [deftest is]] + matcher-combinators.test + [native-module.core :as native-module] + [status-im.contexts.settings.wallet.events :as sut] + [utils.security.core :as security])) + +(def test-profile {:key-uid "test-key-uid"}) +(def test-keypair-key-uid "key-1") + +(deftest rename-keypair-test + (let [new-keypair-name "key pair new" + cofx {:db {}} + expected {:fx [[:json-rpc/call + [{:method "accounts_updateKeypairName" + :params [test-keypair-key-uid new-keypair-name] + :on-success [:wallet/rename-keypair-success test-keypair-key-uid + new-keypair-name] + :on-error fn?}]]]}] + (is (match? expected + (sut/rename-keypair cofx + [{:key-uid test-keypair-key-uid + :keypair-name new-keypair-name}]))))) + +(deftest get-keypair-export-connection-test + (let [cofx {:db {:profile/profile test-profile}} + sha3-pwd "test-password" + user-key-uid "test-key-uid" + callback (fn [connect-string] (println "callback" connect-string)) + expected {:fx [[:effects.syncing/export-keypairs-keystores + {:key-uid user-key-uid + :sha3-pwd sha3-pwd + :keypair-key-uid test-keypair-key-uid + :on-success fn? + :on-fail fn?}]]}] + (is (match? + expected + (sut/get-keypair-export-connection + cofx + [{:sha3-pwd sha3-pwd :keypair-key-uid test-keypair-key-uid :callback callback}]))))) + +(deftest remove-keypair-test + (let [cofx {:db {}} + expected {:fx [[:json-rpc/call + [{:method "accounts_deleteKeypair" + :params [test-keypair-key-uid] + :on-success [:wallet/remove-keypair-success test-keypair-key-uid] + :on-error fn?}]]]}] + (is (match? expected + (sut/remove-keypair cofx [test-keypair-key-uid]))))) + +(deftest make-keypairs-accounts-fully-operable-test + (let [db {:wallet {:keypairs {test-keypair-key-uid {:key-uid + test-keypair-key-uid + :lowest-operability :no + :accounts + [{:key-uid + test-keypair-key-uid + :operable :no}]}} + :accounts {"0x1" {:key-uid test-keypair-key-uid + :operable :no}}} + :profile/profile test-profile} + key-uids-to-update [test-keypair-key-uid] + effects (sut/make-keypairs-accounts-fully-operable {:db db} [key-uids-to-update]) + result-db (:db effects) + updated-keypair (get-in result-db [:wallet :keypairs test-keypair-key-uid]) + updated-account (get-in result-db [:wallet :accounts "0x1"])] + (is (= (-> updated-keypair :accounts first :operable) :fully)) + (is (= (:operable updated-account) :fully)) + (is (= (:lowest-operability updated-keypair) :fully)))) + +(deftest connection-string-for-import-keypair-test + (let [cofx {:db {:profile/profile test-profile}} + sha3-pwd "test-password" + user-key-uid "test-key-uid" + connection-string "test-connection-string" + expected {:fx [[:effects.syncing/import-keypairs-keystores + {:key-uid user-key-uid + :sha3-pwd sha3-pwd + :keypairs-key-uids [test-keypair-key-uid] + :connection-string connection-string + :on-success fn? + :on-fail fn?}]]}] + (is (match? expected + (sut/connection-string-for-import-keypairs cofx + [{:sha3-pwd sha3-pwd + :keypairs-key-uids [test-keypair-key-uid] + :connection-string + connection-string}]))))) + +(deftest success-keypair-qr-scan-test + (let [connection-string "valid-connection-string" + keypairs-key-uids [test-keypair-key-uid] + effects (sut/success-keypair-qr-scan nil [connection-string keypairs-key-uids]) + fx (:fx effects)] + (is (some? fx)))) + +(deftest wallet-validate-seed-phrase-test + (let [cofx {:db {}} + seed-phrase-masked (security/mask-data "seed phrase") + on-success #(prn "success") + on-error #(prn "error") + expected {:fx [[:effects.profile/validate-recovery-phrase + [seed-phrase-masked on-success on-error]]]}] + (is (= expected + (sut/wallet-validate-seed-phrase + cofx + [seed-phrase-masked on-success on-error]))))) + +(deftest make-seed-phrase-keypair-fully-operable-test + (let [cofx {:db {}} + mnemonic "seed phrase" + password "password" + mnemonic-masked (security/mask-data mnemonic) + password-masked (security/mask-data password) + on-success #(prn "success") + on-error #(prn "error") + expected {:fx [[:json-rpc/call + [{:method "accounts_makeSeedPhraseKeypairFullyOperable" + :params [mnemonic (native-module/sha3 password)] + :on-success fn? + :on-error fn?}]]]}] + (is (match? expected + (sut/make-seed-phrase-keypair-fully-operable + cofx + [mnemonic-masked password-masked on-success on-error]))))) + +(deftest make-partly-operable-accounts-fully-operable-test + (let [cofx {:db {}} + password-masked (security/mask-data "password") + on-success #(prn "success") + on-error #(prn "error") + expected {:fx [[:json-rpc/call + [{:method "accounts_makePartiallyOperableAccoutsFullyOperable" + :params [(security/safe-unmask-data password-masked)] + :on-success fn? + :on-error fn?}]]]}] + (is (match? expected + (sut/make-partially-operable-accounts-fully-operable + cofx + [{:password password-masked + :on-success on-success + :on-error on-error}]))))) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs new file mode 100644 index 00000000000..970fa191bd3 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs @@ -0,0 +1,79 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.actions.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.settings.wallet.keypairs-and-accounts.remove.view :as remove-keypair] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [drawer-props keypair]}] + (let [has-paired-device (rf/sub [:pairing/has-paired-devices]) + missing-keypair? (= (:stored drawer-props) :missing) + on-scan-qr (rn/use-callback #(rf/dispatch [:open-modal + :screen/settings.scan-keypair-qr + [(:key-uid keypair)]]) + [keypair]) + on-show-qr (rn/use-callback #(rf/dispatch [:open-modal + :screen/settings.encrypted-keypair-qr + keypair]) + [keypair]) + on-remove-keypair (rn/use-callback #(rf/dispatch + [:show-bottom-sheet + {:theme :dark + :content (fn [] + [remove-keypair/view keypair])}]) + [keypair]) + on-rename-keypair (rn/use-callback #(rf/dispatch [:open-modal :screen/settings.rename-keypair + keypair]) + [keypair]) + on-import-seed-phrase (rn/use-callback + #(rf/dispatch [:open-modal + :screen/settings.missing-keypair.import-seed-phrase + keypair]) + [keypair]) + on-import-private-key (rn/use-callback + #(rf/dispatch [:open-modal + :screen/settings.missing-keypair-import-private-key + keypair]) + [keypair])] + [:<> + [quo/drawer-top drawer-props] + [quo/action-drawer + [(when has-paired-device + (if-not missing-keypair? + [{:blur? true + :icon :i/qr-code + :accessibility-label :show-key-pr-qr + :label (i18n/label :t/show-encrypted-qr-of-key-pair) + :on-press on-show-qr}] + [{:blur? true + :icon :i/scan + :accessibility-label :import-by-scan-qr + :label (i18n/label :t/import-by-scanning-encrypted-qr) + :on-press on-scan-qr}])) + (when (= (:type drawer-props) :keypair) + [(when missing-keypair? + (case (:type keypair) + :seed {:blur? true + :icon :i/seed + :accessibility-label :import-seed-phrase + :label (i18n/label :t/import-by-entering-recovery-phrase) + :on-press on-import-seed-phrase} + :key {:blur? true + :icon :i/key + :accessibility-label :import-private-key + :label (i18n/label :t/import-by-entering-private-key) + :on-press on-import-private-key} + nil)) + {:blur? true + :icon :i/edit + :accessibility-label :rename-key-pair + :label (i18n/label :t/rename-key-pair) + :on-press on-rename-keypair} + {:blur? true + :icon :i/delete + :accessibility-label :remove-key-pair + :add-divider? true + :danger? true + :label (i18n/label :t/remove-key-pair-and-derived-accounts) + :on-press on-remove-keypair}])]]])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/countdown/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/countdown/view.cljs new file mode 100644 index 00000000000..c88ddb3a1c7 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/countdown/view.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.encrypted-qr.countdown.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [utils.datetime :as datetime] + [utils.i18n :as i18n])) + +(def code-valid-for-ms 120000) +(def one-min-ms 60000) + +(defn current-ms + [] + (* 1000 (js/Math.ceil (/ (datetime/timestamp) 1000)))) + +(defn view + [on-clear] + (let [[valid-for-ms set-valid-for-ms] (rn/use-state code-valid-for-ms) + [timestamp set-timestamp] (rn/use-state current-ms) + clock (rn/use-callback (fn [] + (let [remaining (- code-valid-for-ms + (- (current-ms) + timestamp))] + (when (pos? remaining) + (set-valid-for-ms remaining)) + (when (zero? remaining) + (set-timestamp (current-ms)) + (set-valid-for-ms code-valid-for-ms) + (on-clear)))) + [code-valid-for-ms])] + (hooks/use-interval clock on-clear 1000) + [quo/text + {:size :paragraph-2 + :style {:color (if (< valid-for-ms one-min-ms) + colors/danger-60 + colors/white-opa-40)}} + (i18n/label :t/valid-for-time {:valid-for (datetime/ms-to-duration valid-for-ms)})])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/style.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/style.cljs new file mode 100644 index 00000000000..bc3c9319fd9 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/style.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.encrypted-qr.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(defn container-main + [] + {:background-color colors/neutral-95 + :padding-top safe-area/top + :flex 1}) + +(def page-container + {:margin-top 14 + :margin-horizontal 20}) + +(def title-container + {:flex-direction :row + :align-items :center + :justify-content :space-between}) + +(def standard-auth + {:margin-top 12 + :flex 1}) + +(def qr-container + {:margin-top 12 + :background-color colors/white-opa-5 + :border-radius 20 + :flex 1 + :padding 12}) + +(def sub-text-container + {:margin-bottom 8 + :justify-content :space-between + :align-items :center + :flex-direction :row}) + +(def valid-cs-container + {:flex 1 + :margin 12}) + +(def warning-text + {:margin-horizontal 16 + :margin-top 20 + :text-align :center + :color colors/white-opa-70}) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/view.cljs new file mode 100644 index 00000000000..a9ad43796fe --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/encrypted_qr/view.cljs @@ -0,0 +1,101 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.encrypted-qr.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [status-im.common.qr-codes.view :as qr-codes] + [status-im.common.resources :as resources] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.encrypted-qr.countdown.view + :as countdown] + [status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.encrypted-qr.style + :as style] + [status-im.contexts.syncing.utils :as sync-utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back [] (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [{:keys [key-uid]} (quo.context/use-screen-params) + {:keys [customization-color]} (rf/sub [:profile/profile-with-image]) + [code set-code] (rn/use-state nil) + valid-connection-string? (rn/use-memo #(sync-utils/valid-connection-string? code) [code]) + validate-and-set-code (rn/use-callback (fn [connection-string] + (when (sync-utils/valid-connection-string? + connection-string) + (set-code connection-string)))) + cleanup-clock (rn/use-callback #(set-code nil)) + on-auth-success (rn/use-callback (fn [entered-password] + (rf/dispatch + [:wallet/get-keypair-export-connection + {:sha3-pwd entered-password + :keypair-key-uid key-uid + :callback validate-and-set-code}])) + [key-uid])] + [rn/view {:style (style/container-main)} + [rn/scroll-view + [quo/page-nav + {:type :no-title + :icon-name :i/close + :background :blur + :on-press navigate-back}] + [rn/view {:style style/page-container} + [rn/view {:style style/title-container} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style {:color colors/white}} + (i18n/label :t/encrypted-key-pairs)]] + [rn/view {:style style/qr-container} + (if valid-connection-string? + [qr-codes/qr-code {:url code}] + [rn/view {:style {:flex-direction :row}} + [rn/image + {:source (resources/get-image :qr-code) + :style {:width "100%" + :background-color colors/white-opa-70 + :border-radius 12 + :aspect-ratio 1}}]]) + (when valid-connection-string? + [rn/view {:style style/valid-cs-container} + [rn/view {:style style/sub-text-container} + [quo/text + {:size :paragraph-2 + :style {:color colors/white-opa-40}} + (i18n/label :t/encrypted-key-pairs-code)] + [countdown/view cleanup-clock]] + [quo/input + {:default-value code + :type :password + :default-shown? true + :editable false}] + [quo/button + {:on-press (fn [] + (clipboard/set-string code) + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label + :t/sharing-copied-to-clipboard)}])) + :type :grey + :container-style {:margin-top 12} + :icon-left :i/copy} + (i18n/label :t/copy-qr)]]) + (when-not valid-connection-string? + [rn/view {:style style/standard-auth} + [standard-auth/slide-auth + {:blur? true + :size :size-40 + :track-text (i18n/label :t/slide-to-reveal-qr-code) + :customization-color customization-color + :on-success on-auth-success + :auth-button-label (i18n/label :t/reveal-qr-code) + :auth-button-icon-left :i/reveal}]])]] + (when-not valid-connection-string? + [quo/text + {:size :paragraph-2 + :style style/warning-text} + (i18n/label :t/make-sure-no-camera-warning)])]])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_private_key/style.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_private_key/style.cljs new file mode 100644 index 00000000000..a2b52a68d42 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_private_key/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.import-private-key.style) + +(def form-container + {:row-gap 8 + :padding-top 8 + :padding-horizontal 20}) + +(def slide-container + {:flex-direction :row}) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_private_key/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_private_key/view.cljs new file mode 100644 index 00000000000..a3a4d505bcb --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_private_key/view.cljs @@ -0,0 +1,138 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.import-private-key.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.import-private-key.style + :as style] + [status-im.contexts.wallet.common.validation :as validation] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [keypair (quo.context/use-screen-params) + blur? true + customization-color (rf/sub [:profile/customization-color]) + [private-key set-private-key] (rn/use-state "") + [flow-state set-flow-state] (rn/use-state nil) + error? (case flow-state + (:incorrect-private-key + :invalid-private-key) true + false) + clear-errors (rn/use-callback + #(set-flow-state nil)) + show-invalid (rn/use-callback + #(set-flow-state :invalid-private-key)) + show-correct (rn/use-callback + #(set-flow-state :correct-private-key)) + show-incorrect (rn/use-callback + #(set-flow-state :incorrect-private-key)) + verify-private-key (rn/use-callback + (fn [input] + (rf/dispatch [:wallet/verify-private-key-for-keypair + (:key-uid keypair) + (security/mask-data input) + show-correct + show-incorrect])) + [keypair]) + validate-private-key (rn/use-callback + (debounce/debounce + (fn [input] + (if-not (validation/private-key? input) + (show-invalid) + (do (clear-errors) + (verify-private-key input)))) + 500) + [verify-private-key]) + on-change (rn/use-callback + (fn [input] + (set-private-key input) + (validate-private-key input)) + [validate-private-key]) + on-paste (rn/use-callback + #(clipboard/get-string + (fn [clipboard] + (when-not (empty? clipboard) + (on-change clipboard)))) + [on-change]) + on-import-error (rn/use-callback + (fn [_error] + (rf/dispatch [:hide-bottom-sheet]))) + on-import-success (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:navigate-back])) + []) + on-auth-success (rn/use-callback + (fn [password] + (rf/dispatch [:wallet/import-missing-keypair-by-private-key + {:keypair-key-uid (:key-uid keypair) + :private-key (security/mask-data private-key) + :password password + :on-success on-import-success + :on-error on-import-error}])) + [keypair private-key on-import-success on-import-error])] + [quo/overlay {:type :shell} + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:margin-top safe-area/top + :background :blur + :icon-name :i/close + :on-press navigate-back}] + :footer [rn/view {:style style/slide-container} + [standard-auth/slide-auth + {:blur? true + :size :size-48 + :customization-color customization-color + :track-text (i18n/label :t/slide-to-import) + :on-success on-auth-success + :auth-button-label (i18n/label :t/import-key-pair) + :auth-button-icon-left :i/key + :disabled? (or error? (string/blank? private-key)) + :dependencies [on-auth-success]}]]} + [quo/page-top + {:blur? true + :title (i18n/label :t/import-private-key) + :description :context-tag + :context-tag {:type :icon + :icon :i/password + :size 24 + :context (:name keypair)}}] + [rn/view {:style style/form-container} + [quo/input + {:accessibility-label :import-private-key + :placeholder (i18n/label :t/enter-private-key-placeholder) + :label (i18n/label :t/private-key) + :type :password + :blur? blur? + :error? error? + :return-key-type :done + :auto-focus true + :on-change-text on-change + :button (when (empty? private-key) + {:on-press on-paste + :text (i18n/label :t/paste)}) + :default-value private-key}] + (when flow-state + [quo/info-message + {:status (if (= flow-state :correct-private-key) :success :error) + :size :default + :icon :i/info} + (case flow-state + :correct-private-key (i18n/label :t/correct-private-key) + :invalid-private-key (i18n/label :t/invalid-private-key) + :incorrect-private-key (i18n/label :t/incorrect-private-key) + nil)])]]])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_seed_phrase/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_seed_phrase/view.cljs new file mode 100644 index 00000000000..c780178975d --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/import_seed_phrase/view.cljs @@ -0,0 +1,67 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.import-seed-phrase.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.enter-seed-phrase.view :as enter-seed-phrase] + [status-im.common.standard-authentication.core :as standard-auth] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn import-seed-phrase-controls + [{:keys [submit-disabled? + container-style + prepare-seed-phrase + seed-phrase + set-incorrect-seed-phrase + focus-input]}] + (let [keypair (quo.context/use-screen-params) + customization-color (rf/sub [:profile/customization-color]) + show-errors (rn/use-callback + (fn [_error] + (js/setTimeout + (fn [] + (focus-input) + (reagent/next-tick set-incorrect-seed-phrase)) + 600))) + on-import-error (rn/use-callback + (fn [_error] + (rf/dispatch [:hide-bottom-sheet]) + (show-errors))) + on-import-success (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:navigate-back]))) + on-auth-success (rn/use-callback + (fn [password] + (rf/dispatch [:wallet/import-missing-keypair-by-seed-phrase + {:keypair-key-uid (:key-uid keypair) + :seed-phrase (prepare-seed-phrase seed-phrase) + :password password + :on-success on-import-success + :on-error on-import-error}])) + [keypair seed-phrase on-import-success on-import-error])] + [standard-auth/slide-auth + {:blur? true + :size :size-48 + :customization-color customization-color + :track-text (i18n/label :t/slide-to-import) + :on-success on-auth-success + :auth-button-label (i18n/label :t/import-key-pair) + :auth-button-icon-left :i/seed + :container-style container-style + :disabled? submit-disabled? + :dependencies [on-auth-success]}])) + +(defn view + [] + (let [keypair (quo.context/use-screen-params)] + [quo/overlay {:type :shell} + [enter-seed-phrase/screen + {:keypair keypair + :navigation-icon :i/close + :render-controls import-seed-phrase-controls + :title (i18n/label :t/enter-recovery-phrase) + :initial-insets safe-area/insets}]])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/scan_qr/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/scan_qr/view.cljs new file mode 100644 index 00000000000..98295e0edfd --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/missing_keypairs/scan_qr/view.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.scan-qr.view + (:require + [quo.context] + [react-native.core :as rn] + [status-im.common.scan-qr-code.view :as scan-qr-code] + [status-im.contexts.communities.events] + [status-im.contexts.syncing.utils :as sync-utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [keypairs-key-uids (quo.context/use-screen-params) + on-success-scan (rn/use-callback (fn [scanned-text] + (rf/dispatch [:wallet/success-keypair-qr-scan + scanned-text + keypairs-key-uids]) + [keypairs-key-uids]))] + [scan-qr-code/view + {:title (i18n/label :t/scan-key-pairs-qr-code) + :subtitle (i18n/label :t/find-it-in-setting) + :share-button? false + :validate-fn sync-utils/valid-connection-string? + :error-message (i18n/label :t/invalid-key-pair-qr) + :on-success-scan on-success-scan}])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/remove/style.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/remove/style.cljs new file mode 100644 index 00000000000..ffc2f00aef8 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/remove/style.cljs @@ -0,0 +1,7 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.remove.style) + +(def header-container + {:margin-bottom 8}) + +(def content + {:margin-horizontal 20}) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/remove/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/remove/view.cljs new file mode 100644 index 00000000000..d04200c95b4 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/remove/view.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.remove.view + (:require [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.settings.wallet.keypairs-and-accounts.remove.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [name key-uid]}] + (let [on-remove (rn/use-callback #(rf/dispatch + [:wallet/remove-keypair key-uid]) + [key-uid])] + [:<> + [quo/drawer-top + {:container-style style/header-container + :title (i18n/label :t/remove-key-pair-and-derived-accounts) + :type :context-tag + :context-tag-type :icon + :context name + :icon :i/seed-phrase}] + [rn/view {:style style/content} + [quo/text + {:style {:margin-top 4} + :weight :regular + :size :paragraph-1} + (i18n/label :t/the-key-pair-and-derived-accounts-will-be-removed)] + [standard-auth/slide-auth + {:size :size-48 + :track-text (i18n/label :t/slide-to-remove-key-pair) + :container-style {:margin-top 34} + :customization-color colors/danger-50 + :on-success on-remove + :auth-button-label (i18n/label :t/confirm)}]]])) + diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/rename/style.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/rename/style.cljs new file mode 100644 index 00000000000..27e0142baaa --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/rename/style.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.rename.style) + +(def header-container + {:margin-bottom 8}) + +(def bottom-action + {:margin-horizontal -20 + :margin-vertical -12}) + +(def error-container + {:margin-left 20 + :margin-vertical 8}) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/rename/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/rename/view.cljs new file mode 100644 index 00000000000..1a8161a9db6 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/rename/view.cljs @@ -0,0 +1,101 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view + (:require [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.common.validation.keypair :as keypair-validator] + [status-im.constants :as constants] + [status-im.contexts.settings.wallet.keypairs-and-accounts.rename.style :as style] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back [] (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [{existing-keypair-name :name + :keys [key-uid]} (quo.context/use-screen-params) + existing-keypair-names (rf/sub [:wallet/keypair-names]) + customization-color (rf/sub [:profile/customization-color]) + [unsaved-keypair-name set-unsaved-keypair-name] (rn/use-state existing-keypair-name) + [error-msg set-error-msg] (rn/use-state nil) + [typing? set-typing?] (rn/use-state false) + validate-keypair-name (rn/use-callback + (debounce/debounce + (fn [input-name] + (set-error-msg + (keypair-validator/validation-keypair-name + input-name + existing-keypair-names)) + (set-typing? false)) + 300) + [existing-keypair-names]) + on-change-text (rn/use-callback (fn [text] + (set-typing? true) + (set-unsaved-keypair-name + text) + (validate-keypair-name text))) + on-clear (rn/use-callback (fn [] + (on-change-text ""))) + on-save (rn/use-callback #(rf/dispatch + [:wallet/rename-keypair + {:key-uid key-uid + :keypair-name + unsaved-keypair-name}]) + [unsaved-keypair-name key-uid])] + [quo/overlay {:type :shell} + [floating-button-page/view + {:footer-container-padding 0 + :blur? true + :header [quo/page-nav + {:margin-top safe-area/top + :icon-name :i/close + :background :blur + :on-press navigate-back + :accessibility-label :top-bar}] + :footer [quo/bottom-actions + {:actions :one-action + :blur? true + :button-one-label (i18n/label :t/save) + :button-one-props {:blur? true + :disabled? (or typing? + (= existing-keypair-name + unsaved-keypair-name) + (string/blank? + unsaved-keypair-name) + (not (string/blank? + error-msg))) + :customization-color customization-color + :on-press on-save} + :container-style style/bottom-action}]} + [quo/page-top + {:container-style style/header-container + :title (i18n/label :t/rename-key-pair) + :description :context-tag + :blur? true + :context-tag {:type :icon + :size 24 + :context existing-keypair-name + :icon :i/seed-phrase}}] + [quo/input + {:blur? true + :container-style {:margin-horizontal 20} + :placeholder (i18n/label :t/keypair-name-input-placeholder) + :label (i18n/label :t/keypair-name) + :default-value unsaved-keypair-name + :char-limit constants/key-pair-name-max-length + :auto-focus true + :clearable? (not (string/blank? unsaved-keypair-name)) + :on-clear on-clear + :on-change-text on-change-text + :error? (not (string/blank? error-msg))}] + (when-not (string/blank? error-msg) + [quo/info-message + {:status :error + :size :default + :icon :i/info + :container-style style/error-container} + error-msg])]])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/style.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/style.cljs new file mode 100644 index 00000000000..cb5deaf7e56 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/style.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.style) + +(def title-container + {:flex 0 + :padding-horizontal 20 + :margin-vertical 12}) + +(defn page-wrapper + [top-inset] + {:padding-top top-inset + :flex 1}) + +(defn list-container + [bottom-inset] + {:padding-bottom bottom-inset}) + +(def keypair-container-style + {:margin-horizontal 20 + :margin-vertical 8}) + +(def missing-keypairs-container-style + {:margin-horizontal 20 + :margin-vertical 8}) + +(def settings-keypairs-container + {:flex 1}) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/view.cljs new file mode 100644 index 00000000000..bb41c83bfb8 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/view.cljs @@ -0,0 +1,136 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.contexts.settings.wallet.keypairs-and-accounts.actions.view :as actions] + [status-im.contexts.settings.wallet.keypairs-and-accounts.style :as style] + [status-im.feature-flags :as ff] + [utils.address :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn on-options-press + [{:keys [drawer-props keypair]}] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [actions/view + {:drawer-props drawer-props + :keypair keypair}]) + + :theme (:theme drawer-props) + :shell? true}])) + +(defn options-drawer-props + [{{:keys [name]} :keypair + :keys [type stored theme shortened-key customization-color profile-picture]}] + (cond-> {:theme theme + :type type + :blur? true + :title name + :stored stored} + (= type :default-keypair) + (assoc :description shortened-key + :customization-color customization-color + :profile-picture profile-picture) + (= type :keypair) + (assoc :icon-avatar :i/seed))) + +(defn- keypair + [{keypair-type :type + :keys [accounts name keycard?] + :as item} + _ _ + {:keys [profile-picture compressed-key customization-color]}] + (let [theme (quo.context/use-theme) + default-keypair? (= keypair-type :profile) + shortened-key (when default-keypair? + (utils/get-shortened-compressed-key compressed-key)) + on-press (rn/use-callback + (fn [] + (on-options-press + {:keypair item + :drawer-props (options-drawer-props + {:theme theme + :keypair item + :type (if default-keypair? + :default-keypair + :keypair) + :stored (if keycard? :on-keycard :on-device) + :shortened-key shortened-key + :customization-color customization-color + :profile-picture profile-picture})})) + [customization-color default-keypair? item + profile-picture shortened-key theme])] + [quo/keypair + {:blur? true + :status-indicator false + :stored (if keycard? :on-keycard :on-device) + :action (if default-keypair? :none :options) + :accounts accounts + :customization-color customization-color + :container-style style/keypair-container-style + :profile-picture (when default-keypair? profile-picture) + :type (if default-keypair? :default-keypair :other) + :on-options-press on-press + :details {:full-name name + :address shortened-key}}])) + +(defn on-missing-keypair-options-press + [_event keypair-data] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :shell? true + :content (fn [] [actions/view + {:keypair keypair-data + :drawer-props (options-drawer-props + {:theme :dark + :type :keypair + :stored :missing + :blur? true + :keypair keypair-data})}])}])) + +(defn view + [] + (let [insets safe-area/insets + compressed-key (rf/sub [:profile/compressed-key]) + profile-picture (rf/sub [:profile/image]) + customization-color (rf/sub [:profile/customization-color]) + {missing-keypairs :missing + operable-keypairs :operable} (rf/sub [:wallet/settings-keypairs-accounts]) + on-import-press (rn/use-callback #(rf/dispatch [:open-modal + :screen/settings.scan-keypair-qr + (map :key-uid missing-keypairs)]) + [missing-keypairs])] + [quo/overlay + {:type :shell + :container-style (style/page-wrapper (:top insets))} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [quo/standard-title + {:title (i18n/label :t/keypairs-and-accounts) + :container-style style/title-container + :accessibility-label :keypairs-and-accounts-header + :customization-color customization-color}] + [rn/view {:style style/settings-keypairs-container} + [rn/flat-list + {:data operable-keypairs + :render-fn keypair + :header (when (seq missing-keypairs) + [quo/missing-keypairs + {:blur? true + :show-import-all? (ff/enabled? ::ff/settings.import-all-keypairs) + :keypairs missing-keypairs + :on-import-press on-import-press + :container-style style/missing-keypairs-container-style + :on-options-press on-missing-keypair-options-press}]) + :render-data {:profile-picture profile-picture + :compressed-key compressed-key + :customization-color customization-color} + :content-container-style (style/list-container (:bottom insets))}]]])) diff --git a/src/status_im/contexts/settings/wallet/network_settings/max_active_networks_sheet.cljs b/src/status_im/contexts/settings/wallet/network_settings/max_active_networks_sheet.cljs new file mode 100644 index 00000000000..0332834a871 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/network_settings/max_active_networks_sheet.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.settings.wallet.network-settings.max-active-networks-sheet + (:require [quo.core :as quo] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hide-bottom-sheet + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [] + (let [customization-color (rf/sub [:profile/customization-color]) + max-active-networks (rf/sub [:wallet/max-available-active-networks])] + [:<> + [quo/drawer-top + {:title (i18n/label :t/max-active-networks-title + {:count max-active-networks}) + :blur? true + :container-style {:padding-bottom 16 + :padding-top 12}}] + [quo/text + {:style {:padding-horizontal 20 + :padding-bottom 8}} + (i18n/label :t/max-active-networks-message)] + [quo/bottom-actions + {:actions :one-action + :blur? true + :button-one-label (i18n/label :t/close) + :button-one-props {:accessibility-label :confirm-testnet-mode-change + :on-press hide-bottom-sheet + :type :grey + :customization-color customization-color}}]])) diff --git a/src/status_im/contexts/settings/wallet/network_settings/style.cljs b/src/status_im/contexts/settings/wallet/network_settings/style.cljs new file mode 100644 index 00000000000..14d63ef15dd --- /dev/null +++ b/src/status_im/contexts/settings/wallet/network_settings/style.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.settings.wallet.network-settings.style) + +(def title-container + {:padding-vertical 12 + :padding-horizontal 20 + :flex-direction :row + :justify-content :space-between + :align-items :center}) + +(defn page-wrapper + [inset-top] + {:padding-top inset-top + :flex 1}) + +(defn settings-container + [inset-bottom] + {:padding-bottom (+ inset-bottom 8)}) + +(def networks-container + {:padding-top 4}) + +(def advanced-settings-container + {:padding-bottom 4}) diff --git a/src/status_im/contexts/settings/wallet/network_settings/testnet_mode/style.cljs b/src/status_im/contexts/settings/wallet/network_settings/testnet_mode/style.cljs new file mode 100644 index 00000000000..d096fa7cb57 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/network_settings/testnet_mode/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.settings.wallet.network-settings.testnet-mode.style) + +(def description + {:padding-top 4 + :padding-bottom 12 + :padding-horizontal 20}) + +(def drawer-top + {:padding-bottom 4 + :padding-top 12}) + +(def bottom-actions-container + {:flex 1}) diff --git a/src/status_im/contexts/settings/wallet/network_settings/testnet_mode/view.cljs b/src/status_im/contexts/settings/wallet/network_settings/testnet_mode/view.cljs new file mode 100644 index 00000000000..98b5139f176 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/network_settings/testnet_mode/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.settings.wallet.network-settings.testnet-mode.view + (:require [quo.core :as quo] + [status-im.contexts.settings.wallet.network-settings.testnet-mode.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hide-bottom-sheet + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn toggle-testnet-mode + [] + (hide-bottom-sheet) + (rf/dispatch [:wallet/toggle-testnet-mode])) + +(defn testnet-mode-confirm-change-sheet + [{:keys [title blur? description]}] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top + {:title title + :blur? blur? + :container-style style/drawer-top}] + [quo/text {:style style/description} description] + [quo/bottom-actions + {:container-style {:style style/bottom-actions-container} + :actions :two-actions + :blur? blur? + :button-one-label (i18n/label :t/confirm) + :button-one-props {:accessibility-label :confirm-testnet-mode-change + :on-press toggle-testnet-mode + :type :primary + :customization-color customization-color} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:accessibility-label :cancel-testnet-mode-change + :type :grey + :on-press hide-bottom-sheet}}]])) + +(defn view + [{:keys [blur?]}] + (let [testnet-mode? (rf/sub [:profile/test-networks-enabled?])] + (if (not testnet-mode?) + [testnet-mode-confirm-change-sheet + {:blur? blur? + :title (i18n/label :t/turn-on-testnet-mode) + :description (i18n/label :t/testnet-mode-enable-description)}] + [testnet-mode-confirm-change-sheet + {:blur? blur? + :title (i18n/label :t/turn-off-testnet-mode) + :description (i18n/label :t/testnet-mode-disable-description)}]))) diff --git a/src/status_im/contexts/settings/wallet/network_settings/view.cljs b/src/status_im/contexts/settings/wallet/network_settings/view.cljs new file mode 100644 index 00000000000..4badce03270 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/network_settings/view.cljs @@ -0,0 +1,103 @@ +(ns status-im.contexts.settings.wallet.network-settings.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.contexts.settings.wallet.network-settings.style :as style] + [status-im.contexts.settings.wallet.network-settings.testnet-mode.view :as testnet] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn make-network-settings-item + [customization-color network] + (let [{:keys [full-name source active? deactivatable? chain-id]} network] + {:blur? true + :action :selector + :action-props {:type :toggle + :blur? false + :customization-color customization-color + :disabled? (not deactivatable?) + :checked? active? + :on-change #(rf/dispatch + [:wallet/toggle-network-active + chain-id])} + :title full-name + :image :icon-avatar + :image-props {:icon source + :size :size-20}})) + +(defn networks-list + [] + (let [networks (rf/sub [:wallet/networks]) + customization-color (rf/sub [:profile/customization-color])] + [quo/category + {:key :layer-2-settings + :data (mapv (partial make-network-settings-item customization-color) networks) + :blur? true + :list-type :settings}])) + +(defn on-change-testnet + [theme] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [testnet/view + {:blur? true}]) + :theme theme + :shell? true}])) + +(defn advanced-settings + [] + (let [theme (quo.context/use-theme) + testnet-mode? (rf/sub [:profile/test-networks-enabled?]) + on-press-testnet (rn/use-callback + (fn [] + (if-not testnet-mode? + (on-change-testnet theme) + (on-change-testnet theme))) + [theme])] + [quo/category + {:key :advanced-settings + :data [{:blur? true + :title (i18n/label :t/testnet-mode) + :action :selector + :image :icon + :image-props :i/settings + :action-props {:on-change on-press-testnet + :checked? (boolean testnet-mode?)}}] + :blur? true + :list-type :settings}])) + +(defn view + [] + (let [insets safe-area/insets + max-active-networks (rf/sub [:wallet/max-available-active-networks]) + active-networks-count (rf/sub [:wallet/active-networks-count])] + [quo/overlay + {:type :shell + :container-style (style/page-wrapper (:top insets))} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [rn/view {:style style/title-container} + [quo/standard-title + {:title (i18n/label :t/network-settings) + :blur? true + :accessibility-label :network-settings-header}] + [quo/fraction-counter + {:blur? true + :show-counter-warning? true + :left-value active-networks-count + :right-value max-active-networks + :suffix (i18n/label :t/active)}]] + [rn/scroll-view + {:style {:flex 1} + :content-container-style (style/settings-container (:bottom insets))} + [rn/view {:style style/advanced-settings-container} + [advanced-settings]] + [rn/view {:style style/networks-container} + [networks-list]]]])) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/style.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/style.cljs new file mode 100644 index 00000000000..8e8b8b5d8b8 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/style.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.add-address-to-save.style + (:require [quo.foundations.colors :as colors])) + +(def header-container + {:padding-bottom 8}) + +(def input-container + {:flex-direction :row + :margin-horizontal 20}) + +(def input + {:flex 1 + :margin-right 12}) + +(def scan-button + {:margin-top 26}) + +(def info-message + {:margin-top 8 + :margin-left 20}) + +(def existing-saved-address-container + {:padding-horizontal 20 + :padding-vertical 12}) + +(def existing-saved-address-text + {:color colors/white-opa-40}) + +(def saved-address-item + {:margin-top 4 + :background-color colors/white-opa-5}) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/view.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/view.cljs new file mode 100644 index 00000000000..417a29256b6 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/view.cljs @@ -0,0 +1,207 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.add-address-to-save.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.settings.wallet.saved-addresses.add-address-to-save.style :as style] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.common.validation :as validation] + [utils.address :as utils-address] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn- validate-input + [account-addresses saved-addresses user-input] + (let [address-without-prefix (utils-address/extract-address-without-chains-info user-input)] + (cond + (string/blank? user-input) + nil + + (contains? saved-addresses address-without-prefix) + :existing-saved-address + + (contains? account-addresses address-without-prefix) + :own-account + + (not + (or (utils-address/eip-3770-address? user-input) + (validation/ens-name? user-input))) + :invalid-address-or-ens))) + +(defn- address-input + [{:keys [input-value on-change-text paste-into-input clear-input]}] + (let [empty-input? (string/blank? input-value) + on-scan-address (rn/use-callback (fn [] + (rf/dispatch [:wallet/clean-scanned-address]) + (rf/dispatch [:open-modal :screen/wallet.scan-address + {:on-result on-change-text}])))] + [rn/view {:style style/input-container} + [quo/input + {:accessibility-label :add-address-to-save + :placeholder (i18n/label :t/address-placeholder) + :container-style style/input + :blur? true + :label (i18n/label :t/address-or-ens-name) + :auto-capitalize :none + :multiline? true + :on-clear clear-input + :return-key-type :done + :auto-focus true + :clearable? (not empty-input?) + :on-change-text on-change-text + :button (when empty-input? + {:on-press paste-into-input + :text (i18n/label :t/paste)}) + :value input-value}] + [quo/button + {:type :outline + :on-press on-scan-address + :container-style style/scan-button + :background :blur + :size 40 + :icon-only? true} + :i/scan]])) + +(defn- error-view + [{:keys [error]}] + (let [error-msg (condp = error + :existing-saved-address + (i18n/label :t/this-address-is-already-saved) + + :own-account + (i18n/label :t/you-cannot-add-your-own-account-as-a-saved-address) + + :invalid-address-or-ens + (i18n/label :t/this-is-not-an-eth-address-or-ens-name) + + :ens-not-registered + (i18n/label :t/this-ens-name-is-not-registered-yet) + nil)] + (when error-msg + [quo/info-message + {:accessibility-label :error-message + :size :default + :icon :i/alert + :status :error + :container-style style/info-message} + error-msg]))) + +(defn- existing-saved-address + [{:keys [address]}] + (let [address-without-prefix (utils-address/extract-address-without-chains-info address) + {:keys [name customization-color ens ens?]} + (rf/sub [:wallet/saved-address-by-address address-without-prefix])] + [rn/view {:style style/existing-saved-address-container} + [quo/text + {:size :paragraph-1 + :weight :medium + :style style/existing-saved-address-text} + (i18n/label :t/existing-saved-address)] + [quo/saved-address + {:blur? true + :active-state? true + :user-props {:name name + :address address-without-prefix + :ens (when ens? ens) + :customization-color customization-color + :blur? true} + :container-style style/saved-address-item}]])) + +(defn view + [] + (let [screen-id (quo.context/use-screen-id) + profile-color (rf/sub [:profile/customization-color]) + accounts-addresses (rf/sub [:wallet/addresses]) + saved-addresses-addresses (rf/sub [:wallet/saved-addresses-addresses]) + [address-or-ens set-address-or-ens] (rn/use-state "") + [ens-address set-ens-address] (rn/use-state "") + [error set-error] (rn/use-state nil) + error? (some? error) + ens-name? (validation/ens-name? address-or-ens) + address (if ens-name? ens-address address-or-ens) + button-disabled? (or (string/blank? address-or-ens) + (and ens-name? (string/blank? ens-address)) + error?) + validate #(validate-input accounts-addresses + saved-addresses-addresses + %) + clear-input (rn/use-callback + (fn [] + (set-address-or-ens "") + (set-ens-address "") + (set-error nil))) + on-ens-resolve (rn/use-callback + (fn [resolved-address] + (set-error (validate resolved-address)) + (set-ens-address resolved-address))) + on-change-text (rn/use-callback + (fn [new-value] + (let [trimmed-value (string/trim new-value)] + (set-address-or-ens trimmed-value) + (set-ens-address "") + (set-error (validate (string/lower-case trimmed-value))) + (when (validation/ens-name? trimmed-value) + (debounce/debounce-and-dispatch + [:wallet/resolve-ens + {:ens new-value + :on-success on-ens-resolve + :on-error #(set-error :ens-not-registered)}] + 300))))) + paste-into-input (rn/use-callback + #(clipboard/get-string + (fn [clipboard-text] + (when-not (string/blank? clipboard-text) + (-> clipboard-text + utils/on-paste-address-or-ens + on-change-text))))) + on-press-continue (rn/use-callback + (fn [] + (rf/dispatch + [:open-modal :screen/settings.save-address + {:address address + :ens (when ens-name? address-or-ens) + :ens? ens-name?}])) + [address ens-name? address-or-ens])] + (rn/use-unmount #(rf/dispatch [:wallet/clean-scanned-address])) + [quo/overlay {:type :shell} + [floating-button-page/view + {:footer-container-padding 0 + :keyboard-should-persist-taps :handled + :header [quo/page-nav + {:type :no-title + :icon-name :i/close + :behind-overlay? true + :on-press navigate-back + :margin-top safe-area/top + :accessibility-label :add-address-to-save-page-nav}] + :footer (when (= screen-id :screen/settings.add-address-to-save) + [quo/button + {:customization-color profile-color + :disabled? button-disabled? + :on-press on-press-continue} + (i18n/label :t/continue)])} + [quo/page-top + {:container-style style/header-container + :blur? true + :title (i18n/label :t/add-address) + :description :text + :description-text (i18n/label :t/add-address-to-save-description)}] + [address-input + {:input-value address-or-ens + :on-change-text on-change-text + :paste-into-input paste-into-input + :clear-input clear-input}] + (when error? + [:<> + [error-view {:error error}] + (when (= error :existing-saved-address) + [existing-saved-address {:address address}])])]])) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/events.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/events.cljs new file mode 100644 index 00000000000..f4b500c2279 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/events.cljs @@ -0,0 +1,156 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.events + (:require + [status-im.contexts.profile.db :as profile.db] + [status-im.contexts.wallet.data-store :as data-store] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn save-address + [{:keys [db]} + [{:keys [address name customization-color on-success on-error ens]}]] + (let [test-networks-enabled? (profile.db/testnet? db) + address-to-save {:address address + :name name + :colorId customization-color + :ens ens + :isTest test-networks-enabled?}] + {:fx [[:json-rpc/call + [{:method "wakuext_upsertSavedAddress" + :params [address-to-save] + :on-success on-success + :on-error on-error}]]]})) + +(rf/reg-event-fx :wallet/save-address save-address) + +(defn- update-saved-addresses + [saved-addresses-db new-saved-addresses] + (reduce + (fn [acc {:keys [address removed? test?] :as saved-address}] + (let [db-key (if test? :test :prod)] + (if removed? + (update acc db-key dissoc address) + (assoc-in acc [db-key address] saved-address)))) + (or saved-addresses-db + {:test {} + :prod {}}) + new-saved-addresses)) + +(defn reconcile-saved-addresses + [{:keys [db]} [saved-addresses]] + {:db (update-in db [:wallet :saved-addresses] update-saved-addresses saved-addresses)}) + +(rf/reg-event-fx :wallet/reconcile-saved-addresses reconcile-saved-addresses) + +(defn get-saved-addresses-success + [_ [raw-saved-addresses]] + (let [saved-addresses (data-store/rpc->saved-addresses raw-saved-addresses)] + {:fx [[:dispatch [:wallet/reconcile-saved-addresses saved-addresses]]]})) + +(rf/reg-event-fx :wallet/get-saved-addresses-success get-saved-addresses-success) + +(defn saved-addresses-rpc-error + [_ [action error]] + (log/warn (str "[wallet] [saved-addresses] Failed to " action) + {:error error})) + +(rf/reg-event-fx :wallet/saved-addresses-rpc-error saved-addresses-rpc-error) + +(defn get-saved-addresses + [_] + {:fx [[:json-rpc/call + [{:method "wakuext_getSavedAddresses" + :on-success [:wallet/get-saved-addresses-success] + :on-error [:wallet/saved-addresses-rpc-error :get-saved-addresses]}]]]}) + +(rf/reg-event-fx :wallet/get-saved-addresses get-saved-addresses) + +(defn delete-saved-address-success + [{:keys [db]} [{:keys [address toast-message]}]] + (let [db-key (if (profile.db/testnet? db) :test :prod) + saved-address (get-in db [:wallet :saved-addresses db-key address])] + {:fx [[:dispatch [:wallet/reconcile-saved-addresses [(assoc saved-address :removed? true)]]] + [:dispatch [:hide-bottom-sheet]] + [:dispatch-later + {:ms 100 + :dispatch [:toasts/upsert + {:type :positive + :theme :dark + :text toast-message}]}]]})) + +(rf/reg-event-fx :wallet/delete-saved-address-success delete-saved-address-success) + +(defn delete-saved-address-failed + [_ [error]] + {:fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch-later + {:ms 100 + :dispatch [:toasts/upsert + {:type :negative + :theme :dark + :text error}]}]]}) + +(rf/reg-event-fx :wallet/delete-saved-address-failed delete-saved-address-failed) + +(defn delete-saved-address + [{:keys [db]} [{:keys [address toast-message]}]] + (let [test-networks-enabled? (profile.db/testnet? db)] + {:fx [[:json-rpc/call + [{:method "wakuext_deleteSavedAddress" + :params [address test-networks-enabled?] + :on-success [:wallet/delete-saved-address-success + {:address address + :toast-message toast-message}] + :on-error [:wallet/delete-saved-address-failed]}]]]})) + +(rf/reg-event-fx :wallet/delete-saved-address delete-saved-address) + +(defn add-saved-address-success + [_ [toast-message]] + {:fx [[:dispatch [:wallet/get-saved-addresses]] + [:dispatch [:dismiss-modal :screen/settings.add-address-to-save]] + [:dispatch [:dismiss-modal :screen/settings.save-address]] + [:dispatch-later + {:ms 100 + :dispatch [:toasts/upsert + {:type :positive + :theme :dark + :text toast-message}]}]]}) + +(rf/reg-event-fx :wallet/add-saved-address-success add-saved-address-success) + +(defn edit-saved-address-success + [_] + {:fx [[:dispatch [:wallet/get-saved-addresses]] + [:dispatch [:dismiss-modal :screen/settings.edit-saved-address]] + [:dispatch-later + {:ms 100 + :dispatch [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/address-edited)}]}]]}) + +(rf/reg-event-fx :wallet/edit-saved-address-success edit-saved-address-success) + +(defn add-saved-address-failed + [_ [error]] + {:fx [[:dispatch [:wallet/saved-addresses-rpc-error :add-save-address error]] + [:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text error}]]]}) + +(rf/reg-event-fx :wallet/add-saved-address-failed add-saved-address-failed) + +(defn check-remaining-capacity-for-saved-addresses + [{:keys [db]} [{:keys [on-success on-error]}]] + (let [test-networks-enabled? (profile.db/testnet? db)] + {:fx [[:json-rpc/call + [{:method "wakuext_remainingCapacityForSavedAddresses" + :params [test-networks-enabled?] + :on-success on-success + :on-error on-error}]]]})) + +(rf/reg-event-fx :wallet/check-remaining-capacity-for-saved-addresses + check-remaining-capacity-for-saved-addresses) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/events_test.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/events_test.cljs new file mode 100644 index 00000000000..4b1b51d3ed1 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/events_test.cljs @@ -0,0 +1,249 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + matcher-combinators.test + [status-im.contexts.settings.wallet.saved-addresses.events :as events] + [status-im.contexts.wallet.data-store :as data-store])) + +(deftest get-saved-addresses-test + (testing "get saved addresses - dispatches RPC call" + (let [cofx {:db {}} + effects (events/get-saved-addresses cofx) + result-fx (:fx effects) + expected-fx [[:json-rpc/call + [{:method "wakuext_getSavedAddresses" + :on-success [:wallet/get-saved-addresses-success] + :on-error [:wallet/saved-addresses-rpc-error :get-saved-addresses]}]]]] + (is (match? expected-fx result-fx))))) + +(def saved-address-rpc-1 + {:isTest false + :address "0x1" + :mixedcaseAddress "0x1" + :chainShortNames "eth:arb1:oeth:" + :name "Amy" + :createdAt 1716826806 + :ens "" + :colorId "purple" + :removed false}) + +(def saved-address-1 + {:test? true + :address "0x1" + :mixedcase-address "0x1" + :chain-short-names "eth:oeth:" + :network-preferences-names `(:mainnet :optimism) + :name "Bob" + :created-at 1716826714 + :ens "" + :ens? false + :customization-color :blue + :removed? false}) + +(def saved-address-2 + {:test? false + :address "0x2" + :mixedcase-address "0x2" + :chain-short-names "eth:arb1:oeth:" + :network-preferences-names `(:mainnet :arbitrum :optimism) + :name "Alicia Keys" + :created-at 1716826806 + :ens "alicia.eth" + :ens? true + :customization-color :purple + :removed? false}) + +(deftest get-saved-addresses-success-test + (let [cofx {:db {}} + raw-saved-addresses [saved-address-rpc-1] + effects (events/get-saved-addresses-success cofx [raw-saved-addresses]) + saved-addresses (data-store/rpc->saved-addresses raw-saved-addresses) + result-fx (:fx effects) + expected-fx [[:dispatch [:wallet/reconcile-saved-addresses saved-addresses]]]] + (is (match? expected-fx result-fx)))) + +(deftest reconcile-saved-addresses-test + (testing "no saved addresses" + (let [cofx {:db {}} + effects (events/reconcile-saved-addresses cofx nil) + result-db (:db effects) + expected-db {:wallet {:saved-addresses {:test {} + :prod {}}}}] + (is (match? expected-db result-db)))) + + (testing "one test saved address" + (let [cofx {:db {}} + effects (events/reconcile-saved-addresses cofx [[saved-address-1]]) + result-db (:db effects) + expected-db {:wallet {:saved-addresses + {:test {"0x1" {:test? true + :address "0x1" + :mixedcase-address "0x1" + :chain-short-names "eth:oeth:" + :ens? false + :network-preferences-names `(:mainnet :optimism) + :name "Bob" + :created-at 1716826714 + :ens "" + :customization-color :blue + :removed? false}} + :prod {}}}}] + (is (match? expected-db result-db)))) + + (testing "two saved addresses (test and prod)" + (let [cofx {:db {}} + effects (events/reconcile-saved-addresses cofx [[saved-address-1 saved-address-2]]) + result-db (:db effects) + expected-db {:wallet {:saved-addresses + {:test {"0x1" {:test? true + :address "0x1" + :mixedcase-address "0x1" + :chain-short-names "eth:oeth:" + :ens? false + :network-preferences-names `(:mainnet :optimism) + :name "Bob" + :created-at 1716826714 + :ens "" + :customization-color :blue + :removed? false}} + :prod {"0x2" {:test? false + :address "0x2" + :mixedcase-address "0x2" + :chain-short-names "eth:arb1:oeth:" + :network-preferences-names `(:mainnet :arbitrum :optimism) + :ens? true + :name "Alicia Keys" + :created-at 1716826806 + :ens "alicia.eth" + :customization-color :purple + :removed? false}}}}}] + (is (match? expected-db result-db)))) + + (testing "remove a test saved addresses" + (let [cofx {:db {:wallet {:saved-addresses + {:test {"0x1" {:test? true + :address "0x1" + :mixedcase-address "0x1" + :chain-short-names "eth:oeth:" + :ens? false + :network-preferences-names `(:mainnet :optimism) + :name "Bob" + :created-at 1716826714 + :ens "" + :customization-color :blue + :removed? false}} + :prod {"0x2" {:test? false + :address "0x2" + :mixedcase-address "0x2" + :chain-short-names "eth:arb1:oeth:" + :network-preferences-names `(:mainnet :arbitrum + :optimism) + :ens? true + :name "Alicia Keys" + :created-at 1716826806 + :ens "alicia.eth" + :customization-color :purple + :removed? false}}}}}} + effects (events/reconcile-saved-addresses cofx + [[(assoc saved-address-1 :removed? true) + saved-address-2]]) + result-db (:db effects) + expected-db {:wallet {:saved-addresses + {:test {} + :prod {"0x2" {:test? false + :address "0x2" + :mixedcase-address "0x2" + :chain-short-names "eth:arb1:oeth:" + :network-preferences-names `(:mainnet :arbitrum :optimism) + :ens? true + :name "Alicia Keys" + :created-at 1716826806 + :ens "alicia.eth" + :customization-color :purple + :removed? false}}}}}] + (is (match? expected-db result-db))))) + +(deftest save-address-test + (testing "save address - dispatches RPC call" + (let [test-networks-enabled? false + cofx {:db {:profile/profile {:test-networks-enabled? + test-networks-enabled?}}} + on-success [:some-success-event] + on-error [:some-failure-event] + name "Bob" + address "0x3" + ens "bobby.eth" + customization-color :yellow + args {:on-success on-success + :on-error on-error + :name name + :address address + :customization-color customization-color + :ens ens} + effects (events/save-address cofx [args]) + result-fx (:fx effects) + expected-fx [[:json-rpc/call + [{:method "wakuext_upsertSavedAddress" + :params [{:address address + :name name + :colorId customization-color + :ens ens + :isTest test-networks-enabled?}] + :on-success on-success + :on-error on-error}]]]] + (is (match? expected-fx result-fx))))) + +(deftest delete-saved-addresses-test + (testing "delete saved addresses - dispatches RPC call" + (let [test-networks-enabled? true + cofx {:db {:profile/profile {:test-networks-enabled? + test-networks-enabled?}}} + address "0x1f69b0904160bf1ce98dabeaf9c2fe147569498d" + toast-message "Saved addresses deleted successfully" + args {:address address + :toast-message toast-message} + effects (events/delete-saved-address cofx [args]) + result-fx (:fx effects) + expected-fx [[:json-rpc/call + [{:method "wakuext_deleteSavedAddress" + :params [address test-networks-enabled?] + :on-success [:wallet/delete-saved-address-success + {:address address + :toast-message toast-message}] + :on-error [:wallet/delete-saved-address-failed]}]]]] + (is (match? expected-fx result-fx))))) + +(deftest add-saved-address-success-test + (testing "add saved address success test - gets saved addresses, dismiss modals and dispatch toast" + (let [cofx {:db {}} + toast-message "Address saved" + effects (events/add-saved-address-success cofx [toast-message]) + result-fx (:fx effects) + expected-fx [[:dispatch [:wallet/get-saved-addresses]] + [:dispatch [:dismiss-modal :screen/settings.add-address-to-save]] + [:dispatch [:dismiss-modal :screen/settings.save-address]] + [:dispatch-later + {:ms 100 + :dispatch [:toasts/upsert + {:type :positive + :theme :dark + :text toast-message}]}]]] + (is (= (count result-fx) 4)) + (is (match? expected-fx result-fx))))) + +(deftest edit-saved-address-success-test + (testing "edit saved address success test - gets saved addresses, dismiss modals and dispatch toast" + (let [cofx {:db {}} + toast-message "Address edited" + effects (events/edit-saved-address-success cofx) + result-fx (:fx effects) + expected-fx [[:dispatch [:wallet/get-saved-addresses]] + [:dispatch [:dismiss-modal :screen/settings.edit-saved-address]] + [:dispatch-later + {:ms 100 + :dispatch [:toasts/upsert + {:type :positive + :theme :dark + :text toast-message}]}]]] + (is (= (count result-fx) 3)) + (is (match? expected-fx result-fx))))) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/save_address/style.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/save_address/style.cljs new file mode 100644 index 00000000000..2386ae399b1 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/save_address/style.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.save-address.style) + +(def title-input + {:padding-horizontal 20 + :padding-top 12 + :padding-bottom 16}) + +(def avatar + {:margin-horizontal 20 + :margin-top 12}) + +(def color-picker-top-divider + {:margin-bottom 12}) + +(def section-label + {:padding-horizontal 20 + :padding-bottom 4}) + +(def color-picker + {:padding-horizontal 20 + :padding-vertical 12}) + +(def color-picker-bottom-divider + {:margin-top 8 + :margin-bottom 12}) + +(def data-item + {:margin-horizontal 20 + :padding-vertical 8 + :margin-top 8 + :margin-bottom 12}) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/save_address/view.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/save_address/view.cljs new file mode 100644 index 00000000000..f9b3cd6af1d --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/save_address/view.cljs @@ -0,0 +1,116 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.save-address.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.settings.wallet.saved-addresses.save-address.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [{:keys [address name customization-color ens + ens? edit?]} (quo.context/use-screen-params) + [address-label set-address-label] (rn/use-state (or name "")) + [address-color set-address-color] (rn/use-state (or customization-color + (rand-nth colors/account-colors))) + placeholder (i18n/label :t/address-name) + address-text (rn/use-callback + (fn [] + [quo/address-text + {:full-address? true + :address address + :format :long}]) + [address]) + on-press-save (rn/use-callback + (fn [] + (rf/dispatch [:wallet/save-address + {:on-success + (if edit? + [:wallet/edit-saved-address-success] + [:wallet/add-saved-address-success + (i18n/label :t/address-saved)]) + :on-error + [:wallet/add-saved-address-failed] + :name address-label + :ens (when ens? ens) + :address address + :customization-color address-color}])) + [address address-label + address-color]) + data-item-props (rn/use-memo + #(cond-> {:status :default + :size :default + :subtitle-type :default + :blur? true + :card? true + :title (i18n/label :t/address) + :subtitle ens + :custom-subtitle address-text + :container-style style/data-item} + ens? + (dissoc :custom-subtitle)) + [ens ens? address-text])] + [quo/overlay {:type :shell} + [floating-button-page/view + {:footer-container-padding (if edit? (+ safe-area/bottom 12) 0) + :keyboard-should-persist-taps :handled + :header [quo/page-nav + {:type :no-title + :background :blur + :icon-name (if edit? :i/close :i/arrow-left) + :on-press navigate-back + :margin-top (when-not edit? safe-area/top) + :accessibility-label :save-address-page-nav}] + :footer [quo/button + {:accessibility-label :save-address-button + :type :primary + :customization-color address-color + :disabled? (string/blank? address-label) + :on-press on-press-save} + (i18n/label :t/save-address)] + :customization-color address-color + :gradient-cover? true + :shell-overlay? true} + [quo/wallet-user-avatar + {:full-name (if (string/blank? address-label) + placeholder + address-label) + :customization-color address-color + :blur? true + :size :size-80 + :container-style style/avatar}] + [quo/title-input + {:blur? true + :auto-focus (not edit?) + :max-length 24 + :size :heading-1 + :placeholder placeholder + :default-value address-label + :on-change-text set-address-label + :customization-color address-color + :container-style style/title-input}] + [quo/divider-line + {:blur? true + :container-style style/color-picker-top-divider}] + [quo/section-label + {:section (i18n/label :t/colour) + :blur? true + :container-style style/section-label}] + [quo/color-picker + {:default-selected address-color + :on-change set-address-color + :blur? true + :container-style style/color-picker}] + [quo/divider-line + {:blur? true + :container-style style/color-picker-bottom-divider}] + [quo/data-item data-item-props]]])) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/share_address/style.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/share_address/style.cljs new file mode 100644 index 00000000000..c69ed138902 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/share_address/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.share-address.style) + +(def screen-container + {:flex 1}) + +(def top-container + {:margin-bottom 8}) + +(def qr-wrapper + {:padding-horizontal 20}) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/share_address/view.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/share_address/view.cljs new file mode 100644 index 00000000000..f66420323d9 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/share_address/view.cljs @@ -0,0 +1,64 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.share-address.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.contexts.settings.wallet.saved-addresses.share-address.style :as style] + [utils.i18n :as i18n] + [utils.image-server :as image-server] + [utils.re-frame :as rf])) + +(def qr-size 500) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn- share-action + [address share-title] + (rf/dispatch [:open-share + {:options (if platform/ios? + {:activityItemSources [{:placeholderItem {:type :text + :content address} + :item {:default {:type :text + :content + address}} + :linkMetadata {:title share-title}}]} + {:title share-title + :subject share-title + :message address + :isNewTask true})}])) + +(defn view + [] + (let [{:keys [name address customization-color]} (quo.context/use-screen-params) + share-title (str name " " (i18n/label :t/address)) + qr-url address + qr-media-server-uri (rn/use-memo + #(image-server/get-qr-image-uri-for-any-url + {:url qr-url + :port (rf/sub [:mediaserver/port]) + :qr-size qr-size + :error-level :highest}) + [qr-url])] + [quo/overlay + {:type :shell + :top-inset? true} + [rn/view {:style style/screen-container} + [quo/page-nav + {:icon-name :i/close + :on-press navigate-back + :background :blur + :accessibility-label :top-bar}] + [quo/page-top + {:title (i18n/label :t/share-address) + :container-style style/top-container}] + [rn/view {:style style/qr-wrapper} + [quo/share-qr-code + {:type :saved-address + :qr-image-uri qr-media-server-uri + :qr-data qr-url + :on-share-press #(share-action qr-url share-title) + :full-name name + :customization-color customization-color}]]]])) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/sheets/address_options/view.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/sheets/address_options/view.cljs new file mode 100644 index 00000000000..fb36cb6ee09 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/sheets/address_options/view.cljs @@ -0,0 +1,100 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.sheets.address-options.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.contexts.settings.wallet.saved-addresses.sheets.remove-address.view :as remove-address] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn chain-explorer-options + [address networks] + (->> networks + (map (fn [{:keys [chain-id block-explorer-name]}] + {:icon :i/link + :right-icon :i/external + :label (i18n/label :t/view-address-on-block-explorer + {:block-explorer-name block-explorer-name}) + :blur? true + :on-press #(rf/dispatch [:wallet/navigate-to-chain-explorer chain-id address]) + :accessibility-label :view-on-block-explorer})))) + +(defn view + [{:keys [name address customization-color] :as address-details}] + (let [networks (rf/sub [:wallet/active-networks]) + open-send-flow (rn/use-callback + (fn [] + (rf/dispatch [:wallet/init-send-flow-for-address + {:address address + :recipient + {:label name + :customization-color + customization-color + :recipient-type :saved-address} + :stack-id :screen/settings.saved-addresses}])) + [name customization-color]) + open-share (rn/use-callback + #(rf/dispatch + [:open-share + {:options (if platform/ios? + {:activityItemSources + [{:placeholderItem {:type :text + :content address} + :item {:default {:type :text + :content + address}} + :linkMetadata {:title address}}]} + {:title address + :message address + :isNewTask true})}]) + [address]) + open-remove-confirmation-sheet (rn/use-callback + #(rf/dispatch + [:show-bottom-sheet + {:theme :dark + :shell? true + :content (fn [] + [remove-address/view address-details])}]) + [address-details]) + open-show-address-qr (rn/use-callback + #(rf/dispatch [:open-modal + :screen/settings.share-saved-address + address-details]) + [address-details]) + open-edit-saved-address (rn/use-callback + (fn [] + (rf/dispatch [:open-modal + :screen/settings.edit-saved-address + (merge {:edit? true} + address-details)])) + [address-details])] + [quo/action-drawer + [(concat + [{:icon :i/arrow-up + :label (i18n/label :t/send-to-user {:user name}) + :blur? true + :on-press open-send-flow + :accessibility-label :send-to-user}] + (chain-explorer-options address networks) + [{:icon :i/share + :on-press open-share + :label (i18n/label :t/share-address) + :blur? true + :accessibility-label :share-saved-address} + {:icon :i/qr-code + :label (i18n/label :t/show-address-qr) + :blur? true + :on-press open-show-address-qr + :accessibility-label :show-address-qr-code} + {:icon :i/edit + :label (i18n/label :t/edit-details) + :blur? true + :on-press open-edit-saved-address + :accessibility-label :edit-saved-address} + {:icon :i/delete + :label (i18n/label :t/remove-address) + :blur? true + :on-press open-remove-confirmation-sheet + :danger? true + :accessibility-label :remove-saved-address + :add-divider? true}])]])) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/sheets/remove_address/view.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/sheets/remove_address/view.cljs new file mode 100644 index 00000000000..327d4f0f738 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/sheets/remove_address/view.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.sheets.remove-address.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- hide-bottom-sheet + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [{:keys [name address customization-color]}] + (let [on-press-remove (rn/use-callback + #(rf/dispatch [:wallet/delete-saved-address + {:address address + :toast-message (i18n/label :t/saved-address-removed)}]) + [address])] + [:<> + [quo/drawer-top + {:title (i18n/label :t/remove-saved-address) + :customization-color customization-color + :type :context-tag + :blur? true + :context-tag-type :wallet-user + :full-name name}] + [quo/text + {:style {:padding-horizontal 20 + :margin-bottom 8}} + (i18n/label :t/remove-saved-address-description {:name name})] + [quo/bottom-actions + {:actions :two-actions + :blur? true + :button-one-label (i18n/label :t/remove) + :button-one-props {:on-press on-press-remove + :type :danger} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:on-press hide-bottom-sheet + :type :grey}}]])) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/style.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/style.cljs new file mode 100644 index 00000000000..0943e597dfc --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/style.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.style) + +(def empty-container-style + {:justify-content :center + :flex 1}) diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/view.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/view.cljs new file mode 100644 index 00000000000..8434b99d78e --- /dev/null +++ b/src/status_im/contexts/settings/wallet/saved_addresses/view.cljs @@ -0,0 +1,168 @@ +(ns status-im.contexts.settings.wallet.saved-addresses.view + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.common.resources :as resources] + [status-im.contexts.settings.wallet.saved-addresses.sheets.address-options.view :as address-options] + [status-im.contexts.settings.wallet.saved-addresses.style :as style] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- empty-list + [] + (let [theme (quo.context/use-theme)] + [quo/empty-state + {:title (i18n/label :t/no-saved-addresses) + :description (i18n/label :t/you-like-to-type-43-characters) + :image (resources/get-themed-image :sweating-man theme) + :container-style style/empty-container-style}])) + +(defn- empty-result + [] + (let [theme (quo.context/use-theme)] + [quo/empty-state + {:title (i18n/label :t/nothing-found) + :description (i18n/label :t/try-to-search-something-else) + :image (resources/get-themed-image :no-contacts theme) + :container-style style/empty-container-style}])) + +(defn- saved-address + [{:keys [name address customization-color ens? ens]}] + (let [on-press-saved-address (rn/use-callback + #(rf/dispatch + [:show-bottom-sheet + {:theme :dark + :shell? true + :blur-background colors/bottom-sheet-background-blur + :content (fn [] + [address-options/view + {:address address + :ens? ens? + :ens ens + :name name + :customization-color customization-color}])}]) + [address name customization-color])] + [quo/saved-address + {:blur? true + :user-props {:name name + :address address + :ens (when ens? ens) + :customization-color customization-color + :blur? true} + :container-style {:margin-horizontal 8} + :on-press on-press-saved-address}])) + +(defn- header + [{:keys [title index]}] + [quo/divider-label + {:tight? true + :blur? true + :container-style (when (pos? index) {:margin-top 8})} + title]) + +(defn- filtered-list + [{:keys [search-text]}] + (let [search-result (rf/sub [:wallet/filtered-saved-addresses search-text])] + (if (empty? search-result) + [empty-result] + [rn/flat-list + {:key-fn :address + :data search-result + :render-fn saved-address + :shows-vertical-scroll-indicator false + :keyboard-should-persist-taps :always + :content-container-style {:flex-grow 1} + :bounces false + :over-scroll-mode :never}]))) + +(defn- unfiltered-list + [{:keys [grouped-saved-addresses]}] + (if (empty? grouped-saved-addresses) + [empty-list] + [rn/section-list + {:key-fn :title + :shows-vertical-scroll-indicator false + :sticky-section-headers-enabled false + :keyboard-should-persist-taps :always + :render-section-header-fn header + :sections grouped-saved-addresses + :render-fn saved-address + :bounces false + :over-scroll-mode :never + :content-container-style {:flex-grow 1}}])) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn- add-address-to-save + [] + (rf/dispatch [:wallet/check-remaining-capacity-for-saved-addresses + {:on-success #(rf/dispatch [:open-modal :screen/settings.add-address-to-save]) + :on-error #(rf/dispatch [:toasts/upsert + {:type :negative + :theme :dark + :text (i18n/label + :t/saved-addresses-limit-reached-toast)}])}])) + +(defn view + [] + (let [alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + customization-color (rf/sub [:profile/customization-color]) + has-saved-addresses? (rf/sub [:wallet/has-saved-addresses?]) + grouped-saved-addresses (rf/sub [:wallet/grouped-saved-addresses]) + input-ref (rn/use-ref-atom nil) + [search-text set-search-text] (rn/use-state "") + set-input-ref (rn/use-callback #(reset! input-ref %)) + on-clear-input (rn/use-callback + (fn [] + (some-> @input-ref + (oops/ocall "clear")) + (set-search-text ""))) + on-change-text (rn/use-callback + (debounce/debounce + #(set-search-text %) + 500)) + search-active? (not (string/blank? search-text)) + page-top-props (rn/use-memo + #(cond-> {:title (i18n/label :t/saved-addresses) + :accessibility-label :saved-addresses-header + :title-right :action + :title-right-props {:icon :i/add + :customization-color + customization-color + :on-press add-address-to-save} + :blur? true} + + has-saved-addresses? + (assoc + :input :search + :input-props {:placeholder (i18n/label + :t/name-ens-or-address) + :ref set-input-ref + :on-change-text on-change-text + :show-clear-button? search-active? + :on-clear on-clear-input + :customization-color customization-color})) + [has-saved-addresses? customization-color search-text])] + [quo/overlay + {:type :shell + :top-inset? true} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [quo/page-top page-top-props] + [rn/keyboard-avoiding-view + {:style {:flex 1} + :keyboard-vertical-offset (if platform/ios? alert-banners-top-margin 0)} + (if search-active? + [filtered-list {:search-text search-text}] + [unfiltered-list {:grouped-saved-addresses grouped-saved-addresses}])]])) diff --git a/src/status_im/contexts/settings/wallet/wallet_options/style.cljs b/src/status_im/contexts/settings/wallet/wallet_options/style.cljs new file mode 100644 index 00000000000..552aa2ebb6d --- /dev/null +++ b/src/status_im/contexts/settings/wallet/wallet_options/style.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.settings.wallet.wallet-options.style) + +(defn page-wrapper + [inset-top] + {:padding-top inset-top}) diff --git a/src/status_im/contexts/settings/wallet/wallet_options/view.cljs b/src/status_im/contexts/settings/wallet/wallet_options/view.cljs new file mode 100644 index 00000000000..6c7382e195c --- /dev/null +++ b/src/status_im/contexts/settings/wallet/wallet_options/view.cljs @@ -0,0 +1,53 @@ +(ns status-im.contexts.settings.wallet.wallet-options.view + (:require [quo.core :as quo] + [react-native.safe-area :as safe-area] + [status-im.contexts.settings.wallet.wallet-options.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn open-saved-addresses-settings-modal + [] + (rf/dispatch [:open-modal :screen/settings.saved-addresses])) + +(defn open-keypairs-and-accounts-settings-modal + [] + (rf/dispatch [:open-modal :screen/settings.keypairs-and-accounts])) + +(defn basic-settings-options + [] + [{:title (i18n/label :t/keypairs-and-accounts) + :blur? true + :on-press open-keypairs-and-accounts-settings-modal + :action :arrow} + {:title (i18n/label :t/saved-addresses) + :blur? true + :on-press open-saved-addresses-settings-modal + :action :arrow}]) + +(defn basic-settings + [] + [quo/category + {:key :basic-wallet-settings + :label (i18n/label :t/keypairs-accounts-and-addresses) + :data (basic-settings-options) + :blur? true + :list-type :settings}]) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + [quo/overlay + {:type :shell + :container-style (style/page-wrapper safe-area/top)} + [quo/page-nav + {:key :header + :background :blur + :icon-name :i/arrow-left + :on-press navigate-back}] + [quo/page-top + {:title (i18n/label :t/wallet) + :title-accessibility-label :wallet-settings-header}] + [basic-settings]]) diff --git a/src/status_im/contexts/shell/activity_center/context.cljs b/src/status_im/contexts/shell/activity_center/context.cljs new file mode 100644 index 00000000000..696708c4381 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/context.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.shell.activity-center.context + (:require + ["react" :as react] + [oops.core :as oops] + [react-native.core :as rn])) + +(defonce ^:private context + (react/createContext {})) + +(defn provider + [state & children] + (into [:> (oops/oget context :Provider) {:value state}] + children)) + +(defn use-context + [] + (let [ctx (rn/use-context context)] + {:active-swipeable (oops/oget ctx :activeSwipeable)})) diff --git a/src/status_im/contexts/shell/activity_center/drawer/view.cljs b/src/status_im/contexts/shell/activity_center/drawer/view.cljs new file mode 100644 index 00000000000..8e9d6db2690 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/drawer/view.cljs @@ -0,0 +1,23 @@ +(ns status-im.contexts.shell.activity-center.drawer.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn options + [] + (let [unread-count (rf/sub [:activity-center/unread-count])] + [quo/action-drawer + [[{:icon :i/mark-as-read + :label (i18n/label :t/mark-all-notifications-as-read) + :disabled? (zero? unread-count) + :on-press (fn [] + (rf/dispatch [:activity-center.notifications/mark-all-as-read-locally + (fn [] + {:icon :up-to-date + :icon-color colors/success-50 + :text (i18n/label :t/notifications-marked-as-read + {:count unread-count}) + :theme :dark})]) + (rf/dispatch [:hide-bottom-sheet]))}]]])) diff --git a/src/status_im/contexts/shell/activity_center/events.cljs b/src/status_im/contexts/shell/activity_center/events.cljs new file mode 100644 index 00000000000..9578281617d --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/events.cljs @@ -0,0 +1,554 @@ +(ns status-im.contexts.shell.activity-center.events + (:require + [legacy.status-im.data-store.activities :as activities] + [legacy.status-im.data-store.chats :as data-store.chats] + [re-frame.core :as re-frame] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.common.toasts.events :as toasts] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.notification-types :as types] + [taoensso.timbre :as log] + [utils.collection :as collection] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def defaults + {:filter-status :unread + :filter-type types/no-type + :notifications-per-page constants/notifications-per-page}) + +;;;; Navigation + +(defn open-activity-center + [{:keys [db]} [{:keys [filter-type filter-status]}]] + {:db (cond-> db + filter-status + (assoc-in [:activity-center :filter :status] filter-status) + + filter-type + (assoc-in [:activity-center :filter :type] filter-type)) + :fx [[:dispatch [:open-modal :screen/activity-center {}]] + ;; We delay marking as seen so that the user doesn't see the unread bell icon + ;; change while the Activity Center modal is opening. + [:dispatch-later + [{:ms 1000 + :dispatch [:activity-center/mark-as-seen]}]]]}) + +(re-frame/reg-event-fx :activity-center/open open-activity-center) + +;;;; Misc + +(rf/defn process-notification-failure + {:events [:activity-center/process-notification-failure]} + [_ notification-id action error] + (log/warn (str "Failed to " action) + {:notification-id notification-id :error error})) + +(defn get-notification + [db notification-id] + (->> (get-in db [:activity-center :notifications]) + (filter #(= notification-id (:id %))) + first)) + +;;;; Reconciliation + +(defn- update-notifications + [db-notifications new-notifications {filter-type :type filter-status :status}] + (->> new-notifications + (reduce (fn [acc {:keys [id type deleted read] :as notification}] + (if (or deleted + (and (= :unread filter-status) read) + (and (set? filter-type) + (not (contains? filter-type type))) + (and (not (set? filter-type)) + (not= filter-type types/no-type) + (not= filter-type type))) + (dissoc acc id) + (assoc acc id notification))) + (collection/index-by :id db-notifications)) + (vals) + (sort-by (juxt :timestamp :id) + #(compare %2 %1)))) + +(rf/defn notifications-reconcile + {:events [:activity-center.notifications/reconcile]} + [{:keys [db]} new-notifications] + (when (seq new-notifications) + {:db (update-in db + [:activity-center :notifications] + update-notifications + new-notifications + (get-in db [:activity-center :filter])) + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]})) + +(rf/defn notifications-reconcile-from-response + {:events [:activity-center/reconcile-notifications-from-response]} + [cofx response] + (->> response + :activityCenterNotifications + (map activities/<-rpc) + (notifications-reconcile cofx))) + +(rf/defn notifications-remove-pending-contact-request + {:events [:activity-center/remove-pending-contact-request]} + [{:keys [db]} contact-id] + {:db (update-in db + [:activity-center :notifications] + (fn [notifications] + (remove #(activities/pending-contact-request? contact-id %) + notifications)))}) + +(rf/defn reconcile-seen-state + {:events [:activity-center/reconcile-seen-state]} + [{:keys [db]} seen?] + (cond-> {:db (assoc-in db [:activity-center :seen?] seen?)} + + (= (:view-id db) :activity-center) + (assoc :dispatch [:activity-center/mark-as-seen]))) + +;;;; Status changes (read/dismissed/deleted) + +(rf/defn mark-as-read + {:events [:activity-center.notifications/mark-as-read]} + [{:keys [db]} notification-id] + (when-let [notification (get-notification db notification-id)] + {:json-rpc/call [{:method "wakuext_markActivityCenterNotificationsRead" + :params [[notification-id]] + :on-success [:activity-center.notifications/mark-as-read-success notification] + :on-error [:activity-center/process-notification-failure notification-id + :notification/mark-as-read]}]})) + +(defn mark-as-read-success + [_ [{:keys [chat-id] :as notification}]] + {:fx [(when chat-id + [:dispatch [:chats-list/load-chat chat-id]]) + [:dispatch [:activity-center.notifications/reconcile [(assoc notification :read true)]]]]}) + +(rf/reg-event-fx :activity-center.notifications/mark-as-read-success mark-as-read-success) + +(rf/defn mark-as-unread + {:events [:activity-center.notifications/mark-as-unread]} + [{:keys [db]} notification-id] + (when-let [notification (get-notification db notification-id)] + {:json-rpc/call [{:method "wakuext_markActivityCenterNotificationsUnread" + :params [[notification-id]] + :on-success [:activity-center.notifications/mark-as-unread-success notification] + :on-error [:activity-center/process-notification-failure notification-id + :notification/mark-as-unread]}]})) + +(rf/defn mark-as-unread-success + {:events [:activity-center.notifications/mark-as-unread-success]} + [cofx notification] + (notifications-reconcile cofx [(assoc notification :read false)])) + +(rf/defn mark-all-as-read + {:events [:activity-center.notifications/mark-all-as-read]} + [{:keys [db now]}] + (when-let [undoable-till (get-in db [:activity-center :mark-all-as-read-undoable-till])] + (when (>= now undoable-till) + {:json-rpc/call [{:method "wakuext_markAllActivityCenterNotificationsRead" + :params [] + :on-success [:activity-center.notifications/mark-all-as-read-success] + :on-error [:activity-center/process-notification-failure nil + :notification/mark-all-as-read]}]}))) + +(defn merge-chats + [db chats] + (update db :chats merge (collection/index-by :chat-id chats))) + +(defn mark-all-as-read-success + [{:keys [db]} [{:keys [chats] :as _messenger-response}]] + {:db (-> (reduce (fn [acc notification-type] + (assoc-in acc [:activity-center :unread-counts-by-type notification-type] 0)) + db + types/all-supported) + (update :activity-center dissoc :mark-all-as-read-undoable-till) + (merge-chats (map data-store.chats/<-rpc chats)))}) + +(rf/reg-event-fx :activity-center.notifications/mark-all-as-read-success mark-all-as-read-success) + +(rf/defn undo-mark-all-as-read + {:events [:activity-center.notifications/undo-mark-all-as-read-locally]} + [{:keys [db]} {:keys [notifications]}] + {:db (-> db + (update-in [:activity-center :notifications] + update-notifications + notifications + (get-in db [:activity-center :filter])) + (update :activity-center dissoc :mark-all-as-read-undoable-till))}) + +(rf/defn mark-all-as-read-locally + {:events [:activity-center.notifications/mark-all-as-read-locally]} + [{:keys [db now]} get-toast-ui-props] + (let [unread-notifications (filter #(not (:read %)) + (get-in db [:activity-center :notifications])) + undo-time-limit-ms constants/activity-center-mark-all-as-read-undo-time-limit-ms + undoable-till (+ now undo-time-limit-ms)] + {:db (-> db + (update-in [:activity-center :notifications] + update-notifications + (activities/mark-notifications-as-read + unread-notifications) + (get-in db [:activity-center :filter])) + (assoc-in [:activity-center :mark-all-as-read-undoable-till] + undoable-till)) + :dispatch [:toasts/upsert + (merge + {:id :activity-center-mark-all-as-read + :duration undo-time-limit-ms + :undo-duration (/ undo-time-limit-ms 1000) + :undo-on-press + (fn [] + (rf/dispatch + [:activity-center.notifications/undo-mark-all-as-read-locally + {:notifications unread-notifications}]) + (rf/dispatch [:toasts/close :activity-center-mark-all-as-read]))} + (get-toast-ui-props))] + :utils/dispatch-later [{:dispatch [:activity-center.notifications/mark-all-as-read] + :ms undo-time-limit-ms}]})) + +(re-frame/reg-event-fx :activity-center.notifications/dismiss-community-overview + (fn [_ [community-id]] + {:json-rpc/call [{:method "wakuext_dismissActivityCenterNotificationsByCommunity" + :params [{:communityId community-id}] + :on-success [:activity-center.notifications/fetch-unread-count]}]})) + +(rf/defn accept-notification + {:events [:activity-center.notifications/accept]} + [_ notification-id] + {:json-rpc/call [{:method "wakuext_acceptActivityCenterNotifications" + :params [[notification-id]] + :on-success [:activity-center.notifications/accept-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/accept]}]}) + +(defn accept-notification-success + [{:keys [db]} [notification-id {:keys [chats]}]] + (when-let [notification (get-notification db notification-id)] + (let [new-notifications [(assoc notification :read true :accepted true)]] + {:fx [[:dispatch [:chat/ensure-chats (map data-store.chats/<-rpc chats)]] + [:dispatch [:activity-center.notifications/reconcile new-notifications]]]}))) + +(re-frame/reg-event-fx :activity-center.notifications/accept-success accept-notification-success) + +(rf/defn dismiss-notification + {:events [:activity-center.notifications/dismiss]} + [_ notification-id] + {:json-rpc/call [{:method "wakuext_dismissActivityCenterNotifications" + :params [[notification-id]] + :on-success [:activity-center.notifications/dismiss-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/dismiss]}]}) + +(rf/defn dismiss-notification-success + {:events [:activity-center.notifications/dismiss-success]} + [{:keys [db] :as cofx} notification-id] + (when-let [notification (get-notification db notification-id)] + (notifications-reconcile cofx [(assoc notification :read true :dismissed true)]))) + +(rf/defn delete-notification + {:events [:activity-center.notifications/delete]} + [_ notification-id] + {:json-rpc/call [{:method "wakuext_deleteActivityCenterNotifications" + :params [[notification-id]] + :on-success [:activity-center.notifications/delete-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/delete]}]}) + +(rf/defn delete-notification-success + {:events [:activity-center.notifications/delete-success]} + [{:keys [db] :as cofx} notification-id] + (let [notification (get-notification db notification-id)] + (notifications-reconcile cofx [(assoc notification :deleted true)]))) + +;;;; Contact verification + +(rf/defn contact-verification-decline + {:events [:activity-center.contact-verification/decline]} + [_ notification-id] + {:json-rpc/call [{:method "wakuext_declineContactVerificationRequest" + :params [notification-id] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/decline]}]}) + +(rf/defn contact-verification-reply + {:events [:activity-center.contact-verification/reply]} + [_ notification-id reply] + {:json-rpc/call [{:method "wakuext_acceptContactVerificationRequest" + :params [notification-id reply] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/reply]}]}) + +(rf/defn contact-verification-mark-as-trusted + {:events [:activity-center.contact-verification/mark-as-trusted]} + [_ notification-id] + {:json-rpc/call [{:method "wakuext_verifiedTrusted" + :params [{:id notification-id}] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-trusted]}]}) + +(rf/defn contact-verification-mark-as-untrustworthy + {:events [:activity-center.contact-verification/mark-as-untrustworthy]} + [_ notification-id] + {:json-rpc/call [{:method "wakuext_verifiedUntrustworthy" + :params [{:id notification-id}] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-untrustworthy]}]}) + +;;;; Notifications fetching and pagination + +(def start-or-end-cursor + "") + +(defn- fetch-more? + [cursor] + (and (some? cursor) + (not= cursor start-or-end-cursor))) + +(def ^:const read-type-read 1) +(def ^:const read-type-unread 2) +(def ^:const read-type-all 3) + +(defn ->rpc-read-type + [read-type] + (case read-type + :read read-type-read + :unread read-type-unread + :all read-type-all + ;; Send invalid type, so the backend fails fast. + -1)) + +(defn filter-type->rpc-param + [filter-type] + (cond + (coll? filter-type) + filter-type + + ;; A "no-type" notification shouldn't be sent to the backend. If, for + ;; instance, the mobile client needs notifications of any type (as in the + ;; `All` tab), then just don't filter by type at all. + (= types/no-type filter-type) + nil + + :else + [filter-type])) + +(rf/defn notifications-fetch + [{:keys [db]} {:keys [cursor per-page filter-type filter-status reset-data?]}] + (when-not (get-in db [:activity-center :loading?]) + (let [per-page (or per-page (defaults :notifications-per-page))] + {:db (assoc-in db [:activity-center :loading?] true) + :json-rpc/call [{:method "wakuext_activityCenterNotifications" + :params [{:cursor cursor + :limit per-page + :activityTypes (filter-type->rpc-param filter-type) + :readType (->rpc-read-type filter-status)}] + :on-success [:activity-center.notifications/fetch-success reset-data?] + :on-error [:activity-center.notifications/fetch-error filter-type + filter-status]}]}))) + +(rf/defn notifications-fetch-first-page + {:events [:activity-center.notifications/fetch-first-page]} + [{:keys [db] :as cofx} {:keys [filter-type filter-status]}] + (let [filter-type (or filter-type + (get-in db + [:activity-center :filter :type] + (defaults :filter-type))) + filter-status (or filter-status + (get-in db + [:activity-center :filter :status] + (defaults :filter-status)))] + (rf/merge cofx + {:db (-> db + (assoc-in [:activity-center :filter :type] filter-type) + (assoc-in [:activity-center :filter :status] filter-status))} + (notifications-fetch {:cursor start-or-end-cursor + :filter-type filter-type + :filter-status filter-status + :reset-data? true})))) + +(rf/defn notifications-fetch-next-page + {:events [:activity-center.notifications/fetch-next-page]} + [{:keys [db] :as cofx}] + (let [{:keys [type status]} (get-in db [:activity-center :filter]) + cursor (get-in db [:activity-center :cursor])] + (when (fetch-more? cursor) + (notifications-fetch cofx + {:cursor cursor + :filter-type type + :filter-status status + :reset-data? false})))) + +(rf/defn notifications-fetch-success + {:events [:activity-center.notifications/fetch-success]} + [{:keys [db]} reset-data? {:keys [cursor notifications]}] + (let [processed (map activities/<-rpc notifications)] + {:db (-> db + (assoc-in [:activity-center :cursor] cursor) + (update :activity-center dissoc :loading?) + (update-in [:activity-center :notifications] + (if reset-data? + (constantly processed) + #(concat % processed))))})) + +(re-frame/reg-fx :activity-center.notifications/fetch-pending-contact-requests-fx + (fn [] + (json-rpc/call {:method "wakuext_activityCenterNotifications" + :params [{:cursor start-or-end-cursor + :limit 20 + :activityTypes [types/contact-request] + :readType (->rpc-read-type :unread)}] + :on-success [:activity-center.notifications/fetch-pending-contact-requests-success] + :on-error [:activity-center.notifications/fetch-error types/contact-request + :unread]}))) + +(rf/defn notifications-fetch-pending-contact-requests + "Unread contact requests are, in practical terms, the same as pending contact + requests in the Activity Center, because pending contact requests are always + marked as unread in status-go, and once the user declines/accepts the request, + they are marked as read. + + If this relationship ever changes, we will probably need to change the backend + to explicitly support fetching notifications for 'pending' contact requests." + {:events [:activity-center.notifications/fetch-pending-contact-requests]} + [{:keys [db]}] + {:db (assoc-in db [:activity-center :loading?] true) + :fx [[:activity-center.notifications/fetch-pending-contact-requests-fx]]}) + +(rf/defn notifications-fetch-pending-contact-requests-success + {:events [:activity-center.notifications/fetch-pending-contact-requests-success]} + [{:keys [db]} {:keys [notifications]}] + {:db (-> db + (update :activity-center dissoc :loading?) + (assoc-in [:activity-center :contact-requests] + (->> notifications + (map activities/<-rpc) + (filter (fn [notification] + (= constants/contact-request-message-state-pending + (get-in notification [:message :contact-request-state])))))))}) + +(rf/defn notifications-fetch-error + {:events [:activity-center.notifications/fetch-error]} + [{:keys [db]} error] + (log/warn "Failed to load Activity Center notifications" error) + {:db (update db :activity-center dissoc :loading?)}) + +;;;; Unread counters + +(re-frame/reg-fx :activity-center/update-seen-state + (fn [] + (json-rpc/call {:method "wakuext_hasUnseenActivityCenterNotifications" + :params [] + :on-success [:activity-center/update-seen-state-success] + :on-error [:activity-center/update-seen-state-error]}))) + +(rf/defn update-seen-state-success + {:events [:activity-center/update-seen-state-success]} + [{:keys [db]} unseen?] + {:db (assoc-in db [:activity-center :seen?] (not unseen?))}) + +(rf/defn update-seen-state-error + {:events [:activity-center/update-seen-state-error]} + [_ error] + (log/error "Failed to update Activity Center seen state" + {:error error + :event :activity-center/update-seen-state})) + +(rf/defn mark-as-seen + {:events [:activity-center/mark-as-seen]} + [_] + {:json-rpc/call + [{:method "wakuext_markAsSeenActivityCenterNotifications" + :params [] + :on-success [:activity-center/mark-as-seen-success] + :on-error [:activity-center/mark-as-seen-error]}]}) + +(rf/defn mark-as-seen-success + {:events [:activity-center/mark-as-seen-success]} + [{:keys [db]} response] + {:db (assoc-in db + [:activity-center :seen?] + (get-in response [:activityCenterState :hasSeen]))}) + +(rf/defn mark-as-seen-error + {:events [:activity-center/mark-as-seen-error]} + [_ error] + (log/error "Failed to mark Activity Center as seen" + {:error error + :event :activity-center/mark-as-seen})) + +(re-frame/reg-fx :activity-center.notifications/fetch-unread-count + (fn [] + (json-rpc/call {:method "wakuext_activityCenterNotificationsCount" + :params [{:activityTypes types/all-supported + :readType (->rpc-read-type :unread)}] + :on-success [:activity-center.notifications/fetch-unread-count-success] + :on-error [:activity-center.notifications/fetch-unread-count-error]}))) + +(rf/defn notifications-fetch-unread-count + {:events [:activity-center.notifications/fetch-unread-count]} + [_] + {:json-rpc/call + [{:method "wakuext_activityCenterNotificationsCount" + :params [{:activityTypes types/all-supported + :readType (->rpc-read-type :unread)}] + :on-success [:activity-center.notifications/fetch-unread-count-success] + :on-error [:activity-center.notifications/fetch-unread-count-error]}]}) + +(rf/defn notifications-fetch-unread-count-success + {:events [:activity-center.notifications/fetch-unread-count-success]} + [{:keys [db]} response] + {:db (assoc-in db + [:activity-center :unread-counts-by-type] + (activities/parse-notification-counts-response response))}) + +(rf/defn notifications-fetch-unread-count-error + {:events [:activity-center.notifications/fetch-unread-count-error]} + [_ error] + (log/error "Failed to fetch count of notifications" {:error error}) + nil) + +;;;; Toasts + +(rf/defn show-toasts + {:events [:activity-center.notifications/show-toasts]} + [{:keys [db]} new-notifications] + (let [my-public-key (get-in db [:profile/profile :public-key])] + (reduce (fn [cofx {:keys [author chat-id type accepted dismissed message name]}] + (let [user-avatar {:full-name name + :status-indicator? true + :online? nil + :size :small}] + (cond + (and (not= author my-public-key) + (= type types/contact-request) + (not accepted) + (not dismissed)) + (toasts/upsert cofx + {:type :notification + :user user-avatar + :user-public-key author + :title (i18n/label :t/contact-request-sent-toast + {:name name}) + :text (get-in message [:content :text])}) + + (and (= author my-public-key) ;; we show it for user who sent the request + (= type types/contact-request) + accepted + (not dismissed)) + (toasts/upsert cofx + {:type :notification + :user user-avatar + ;; user public key who accepted the request + :user-public-key chat-id + :title (i18n/label :t/contact-request-accepted-toast + {:name (or name (:alias message))})}) + :else + cofx))) + {:db db} + new-notifications))) diff --git a/src/status_im/contexts/shell/activity_center/events_test.cljs b/src/status_im/contexts/shell/activity_center/events_test.cljs new file mode 100644 index 00000000000..9aeeff32141 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/events_test.cljs @@ -0,0 +1,403 @@ +(ns status-im.contexts.shell.activity-center.events-test + (:require + [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.contexts.shell.activity-center.events :as events] + [status-im.contexts.shell.activity-center.notification-types :as types] + [test-helpers.unit :as h])) + +(h/use-log-fixture) + +(def notification-id "0x1") + +;;;; Misc + +(deftest open-activity-center-test + (testing "opens the activity center with default filters" + (is (match? {:db {} + :fx [[:dispatch [:open-modal :screen/activity-center {}]] + [:dispatch-later [{:ms 1000 :dispatch [:activity-center/mark-as-seen]}]]]} + (events/open-activity-center {:db {}} [nil])))) + + (testing "opens the activity center with filters enabled" + (is (match? {:db {:activity-center {:filter {:status :unread :type types/contact-request}}} + :fx [[:dispatch [:open-modal :screen/activity-center {}]] + [:dispatch-later [{:ms 1000 :dispatch [:activity-center/mark-as-seen]}]]]} + (events/open-activity-center {:db {}} + [{:filter-type types/contact-request + :filter-status :unread}]))))) + +(deftest process-notification-failure-test + (testing "logs and returns nil" + (is (nil? (events/process-notification-failure + {:db {}} + notification-id + :some-action-name + :some-error))) + (is (match? {:args ["Failed to :some-action-name" + {:notification-id notification-id + :error :some-error}] + :level :warn} + (last @h/logs))))) + +;;;; Mark as read/unread + +(deftest mark-as-read-test + (testing "does nothing if the notification ID cannot be found in the app db" + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read false + :type types/one-to-one-chat}]}}}] + (is (nil? (events/mark-as-read cofx "0x99"))))) + + (testing "dispatches RPC call" + (let [notif {:id "0x1" :read false :type types/one-to-one-chat} + cofx {:db {:activity-center {:notifications [notif]}}}] + (is (match? {:json-rpc/call + [{:method "wakuext_markActivityCenterNotificationsRead" + :params [[(:id notif)]] + :on-success [:activity-center.notifications/mark-as-read-success notif] + :on-error [:activity-center/process-notification-failure (:id notif) + :notification/mark-as-read]}]} + (events/mark-as-read cofx (:id notif))))))) + +(deftest mark-as-read-success-test + (let [cofx {:db {}} + notif {:id "0x1" :read false :type types/one-to-one-chat :chat-id "chat-id"} + expected-fx {:fx [[:dispatch [:chats-list/load-chat "chat-id"]] + [:dispatch + [:activity-center.notifications/reconcile [(assoc notif :read true)]]]]}] + (is (match? expected-fx (events/mark-as-read-success cofx [notif]))))) + +(deftest mark-as-unread-test + (testing "does nothing if the notification ID cannot be found in the app db" + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read true + :type types/one-to-one-chat}]}}}] + (is (nil? (events/mark-as-unread cofx "0x99"))))) + + (testing "dispatches RPC call" + (let [notif {:id "0x1" :read true :type types/one-to-one-chat} + cofx {:db {:activity-center {:notifications [notif]}}}] + (is (match? {:json-rpc/call + [{:method "wakuext_markActivityCenterNotificationsUnread" + :params [[(:id notif)]] + :on-success [:activity-center.notifications/mark-as-unread-success notif] + :on-error [:activity-center/process-notification-failure (:id notif) + :notification/mark-as-unread]}]} + (events/mark-as-unread cofx (:id notif))))))) + +(deftest mark-as-unread-success-test + (let [f-args (atom []) + cofx {:db {}} + notif {:id "0x1" :read true :type types/one-to-one-chat}] + (with-redefs [events/notifications-reconcile + (fn [& args] + (reset! f-args args) + :reconciliation-result)] + (is (= :reconciliation-result (events/mark-as-unread-success cofx notif))) + (is (match? [cofx [(assoc notif :read false)]] + @f-args))))) + +;;;; Acceptance/dismissal + +(deftest accept-notification-test + (is (match? {:json-rpc/call + [{:method "wakuext_acceptActivityCenterNotifications" + :params [[notification-id]] + :on-success [:activity-center.notifications/accept-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/accept]}]} + (events/accept-notification {:db {}} notification-id)))) + +(deftest accept-notification-success-test + (testing "does nothing if the notification ID cannot be found in the app db" + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read false + :type types/one-to-one-chat}]}}}] + (is (nil? (events/accept-notification-success cofx ["0x99" nil]))))) + + (testing "marks notification as accepted and read, then reconciles" + (let [notif-1 {:id "0x1" :type types/private-group-chat} + notif-2 {:id "0x2" :type types/private-group-chat} + notif-2-accepted (assoc notif-2 :accepted true :read true) + cofx {:db {:activity-center {:filter {:type types/no-type :status :all} + :notifications [notif-2 notif-1]}}}] + (is (match? {:fx [[:dispatch [:chat/ensure-chats []]] + [:dispatch [:activity-center.notifications/reconcile [notif-2-accepted]]]]} + (events/accept-notification-success cofx [(:id notif-2) nil])))))) + +(deftest dismiss-notification-test + (is (match? {:json-rpc/call + [{:method "wakuext_dismissActivityCenterNotifications" + :params [[notification-id]] + :on-success [:activity-center.notifications/dismiss-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/dismiss]}]} + (events/dismiss-notification {:db {}} notification-id)))) + +(deftest dismiss-notification-success-test + (testing "does nothing if the notification ID cannot be found in the app db" + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read false + :type types/one-to-one-chat}]}}}] + (is (nil? (events/dismiss-notification-success cofx "0x99"))))) + + (testing "marks notification as dismissed and read, then reconciles" + (let [notif-1 {:id "0x1" :type types/private-group-chat} + notif-2 {:id "0x2" :type types/private-group-chat} + notif-2-dismissed (assoc notif-2 :dismissed true :read true) + cofx {:db {:activity-center {:filter {:type types/no-type :status :all} + :notifications [notif-2 notif-1]}}}] + (is (match? {:db {:activity-center {:filter {:type 0 :status :all} + :notifications [notif-2-dismissed notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/dismiss-notification-success cofx (:id notif-2))))))) + +;;;; Contact verification + +(deftest contact-verification-decline-test + (is (match? {:json-rpc/call + [{:method "wakuext_declineContactVerificationRequest" + :params [notification-id] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/decline]}]} + (events/contact-verification-decline {:db {}} notification-id)))) + +(deftest contact-verification-reply-test + (let [reply "The answer is 42"] + (is (match? {:json-rpc/call + [{:method "wakuext_acceptContactVerificationRequest" + :params [notification-id reply] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/reply]}]} + (events/contact-verification-reply {:db {}} notification-id reply))))) + +(deftest contact-verification-mark-as-trusted-test + (is (match? {:json-rpc/call + [{:method "wakuext_verifiedTrusted" + :params [{:id notification-id}] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-trusted]}]} + (events/contact-verification-mark-as-trusted {:db {}} notification-id)))) + +(deftest contact-verification-mark-as-untrustworthy-test + (is (match? {:json-rpc/call + [{:method "wakuext_verifiedUntrustworthy" + :params [{:id notification-id}] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-untrustworthy]}]} + (events/contact-verification-mark-as-untrustworthy {:db {}} notification-id)))) + +;;;; Notification reconciliation + +(deftest notifications-reconcile-test + (testing "All tab + All filter" + (let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat} + notif-2 {:id "0x2" :read false :type types/system} + new-notif-3 {:id "0x3" :read false :type types/system} + new-notif-4 {:id "0x4" :read true :type types/system} + new-notif-2 (assoc notif-2 :read true) + cofx {:db {:activity-center + {:filter {:type types/no-type :status :all} + :notifications [notif-2 notif-1]}}}] + (is (match? {:db {:activity-center + {:filter {:type types/no-type :status :all} + :notifications [new-notif-4 new-notif-3 new-notif-2]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [(assoc notif-1 :deleted true) ; will be removed + new-notif-2 + new-notif-3 + new-notif-4]))))) + + (testing "All tab + Unread filter" + (let [notif-1 {:id "0x1" :read false :type types/one-to-one-chat} + notif-2 {:id "0x2" :read false :type types/system} + new-notif-2 (assoc notif-2 :read true) + new-notif-3 {:id "0x3" :read false :type types/system} + new-notif-4 {:id "0x4" :read true :type types/system} + notif-5 {:id "0x5" :type types/system} + cofx {:db {:activity-center + {:filter {:type types/no-type :status :unread} + :notifications [notif-5 notif-2 notif-1]}}}] + (is (match? {:db {:activity-center + {:filter {:type types/no-type :status :unread} + :notifications [new-notif-3 notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [new-notif-2 ; will be removed because it's read + new-notif-3 ; will be inserted + new-notif-4 ; will be ignored because it's read + (assoc notif-5 :deleted true)]))))) ; will be removed + + + (testing "Contact request tab + All filter" + (let [notif-1 {:id "0x1" :read true :type types/contact-request} + notif-2 {:id "0x2" :read false :type types/contact-request} + new-notif-2 (assoc notif-2 :read true) + new-notif-3 {:id "0x3" :read false :type types/contact-request} + new-notif-4 {:id "0x4" :read true :type types/system} + notif-5 {:id "0x5" :read false :type types/contact-request} + cofx {:db {:activity-center + {:filter {:type types/contact-request :status :all} + :notifications [notif-5 notif-2 notif-1]}}}] + (is (match? {:db {:activity-center + {:filter {:type types/contact-request :status :all} + :notifications [new-notif-3 new-notif-2 notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [new-notif-2 ; will be updated + new-notif-3 ; will be inserted + new-notif-4 ; will be ignored because it's not a contact request + (assoc notif-5 :deleted true)]))))) ; will be removed + + + (testing "Contact request tab + Unread filter" + (let [notif-1 {:id "0x1" :read false :type types/contact-request} + notif-2 {:id "0x2" :read false :type types/contact-request} + new-notif-2 (assoc notif-2 :read true) + new-notif-3 {:id "0x3" :read false :type types/contact-request} + new-notif-4 {:id "0x4" :read true :type types/contact-request} + new-notif-5 {:id "0x5" :read true :type types/system} + notif-6 {:id "0x6" :read false :type types/contact-request} + cofx {:db {:activity-center + {:filter {:type types/contact-request + :status :unread} + :notifications [notif-6 notif-2 notif-1]}}}] + (is (match? {:db {:activity-center + {:filter {:type types/contact-request + :status :unread} + :notifications [new-notif-3 notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [new-notif-2 ; will be removed because it's read + new-notif-3 ; will be inserted + new-notif-4 ; will be ignored because it's read + new-notif-5 ; will be ignored because it's not a contact request + (assoc notif-6 :deleted true)]))))) ; will be removed + + + ;; Sorting by timestamp and ID is compatible with what the backend does when + ;; returning paginated results. + (testing "sorts notifications by timestamp and id in descending order" + (let [notif-1 {:id "0x1" :timestamp 1} + notif-2 {:id "0x2" :timestamp 1} + notif-3 {:id "0x3" :timestamp 50} + notif-4 {:id "0x4" :timestamp 100} + notif-5 {:id "0x5" :timestamp 100} + new-notif-1 (assoc notif-1 :last-message {}) + new-notif-4 (assoc notif-4 :last-message {}) + cofx {:db {:activity-center + {:notifications [notif-1 notif-3 notif-4 notif-2 + notif-5]}}}] + (is (match? {:db {:activity-center + {:notifications [notif-5 + new-notif-4 + notif-3 + notif-2 + new-notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile cofx [new-notif-1 new-notif-4])))))) + +(deftest remove-pending-contact-request-test + (testing "removes notification from all related filter types and status" + (let [author "0x99" + notif-1 {:id "0x1" :read true :type types/contact-request} + notif-2 {:id "0x2" :read false :type types/contact-request :author author} + notif-3 {:id "0x3" :read false :type types/private-group-chat :author author} + cofx {:db {:activity-center + {:notifications + [notif-3 ; will be ignored because it's not a contact request + notif-2 ; will be removed + notif-1]}}}] ; will be ignored because it's not from the same author + + (is (match? {:db {:activity-center {:notifications [notif-3 notif-1]}}} + (events/notifications-remove-pending-contact-request cofx author)))))) + +;;;; Notifications fetching and pagination + +(deftest notifications-fetch-first-page-test + (testing "fetches first page" + (let [cofx {:db {}}] + (is (match? {:db {:activity-center {:filter {:type types/one-to-one-chat + :status :unread} + :loading? true}} + :json-rpc/call [{:method "wakuext_activityCenterNotifications" + :params [{:cursor "" + :limit (:notifications-per-page + events/defaults) + :activityTypes [types/one-to-one-chat] + :readType events/read-type-unread}] + :on-success [:activity-center.notifications/fetch-success true] + :on-error [:activity-center.notifications/fetch-error + types/one-to-one-chat :unread]}]} + (events/notifications-fetch-first-page cofx {:filter-type types/one-to-one-chat})))))) + +(deftest notifications-fetch-next-next-page-test + (testing "does not fetch next page when pagination cursor reached the end" + (is (nil? (events/notifications-fetch-next-page + {:db {:activity-center {:cursor events/start-or-end-cursor}}})))) + + (testing "fetches the next page" + (let [f-args (atom []) + cursor "abc" + cofx {:db {:activity-center {:cursor cursor + :filter {:type types/one-to-one-chat + :status :unread}}}}] + (with-redefs [events/notifications-fetch + (fn [& args] + (reset! f-args args) + :result)] + (is (= :result (events/notifications-fetch-next-page cofx))) + (is (match? [cofx + {:cursor cursor + :filter-type types/one-to-one-chat + :filter-status :unread + :reset-data? false}] + @f-args)))))) + +(deftest notifications-fetch-error-test + (testing "resets loading state" + (let [cofx {:db {:activity-center + {:loading? true + :filter {:status :unread + :type types/one-to-one-chat} + :cursor ""}}}] + (is (match? {:db {:activity-center {:filter {:status :unread + :type types/one-to-one-chat} + :cursor ""}}} + (events/notifications-fetch-error cofx :dummy-error)))))) + +(deftest seen-state-test + (testing "update seen state" + (is (match? {:db {:activity-center {:seen? true}}} + (events/reconcile-seen-state {:db {}} true)))) + + (testing "update seen state when the user is on other screen" + (is (match? {:db {:view-id :screen/chats-stack + :activity-center {:seen? false}}} + (events/reconcile-seen-state {:db {:view-id :screen/chats-stack}} false)))) + + (testing "update seen state when the user is on activity center" + (is (match? {:db {:view-id :activity-center + :activity-center {:seen? false}} + :dispatch [:activity-center/mark-as-seen]} + (events/reconcile-seen-state {:db {:view-id :activity-center}} false))))) diff --git a/src/status_im/contexts/shell/activity_center/header/view.cljs b/src/status_im/contexts/shell/activity_center/header/view.cljs new file mode 100644 index 00000000000..54296dce7d0 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/header/view.cljs @@ -0,0 +1,57 @@ +(ns status-im.contexts.shell.activity-center.header.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.shell.activity-center.drawer.view :as drawer] + [status-im.contexts.shell.activity-center.style :as style] + [status-im.contexts.shell.activity-center.tabs.view :as tabs] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn filter-selector-read-toggle + [] + (let [customization-color (rf/sub [:profile/customization-color]) + unread-filter-enabled? (rf/sub [:activity-center/filter-status-unread-enabled?])] + [quo/filter + {:pressed? unread-filter-enabled? + :customization-color customization-color + :blur? true + :on-press-out #(rf/dispatch [:activity-center.notifications/fetch-first-page + {:filter-status (if unread-filter-enabled? + :all + :unread)}])}])) + +(defn header + [] + [rn/view + [rn/view {:style style/header-container} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :close-activity-center + :on-press #(rf/dispatch [:navigate-back])} + :i/close] + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :activity-center-open-more + :on-press #(rf/dispatch [:show-bottom-sheet + {:content drawer/options + :theme :dark + :shell? true + :blur-radius 20}])} + :i/options]] + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/header-heading} + (i18n/label :t/notifications)] + [rn/view {:style style/tabs-and-filter-container} + [rn/view {:style style/tabs-container} + [tabs/tabs]] + [rn/view {:style style/filter-toggle-container} + [filter-selector-read-toggle]]]]) diff --git a/src/status_im/contexts/shell/activity_center/notification/admin/view.cljs b/src/status_im/contexts/shell/activity_center/notification/admin/view.cljs new file mode 100644 index 00000000000..c06633e7f86 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/admin/view.cljs @@ -0,0 +1,121 @@ +(ns status-im.contexts.shell.activity-center.notification.admin.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.notification.common.style :as common-style] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- swipe-button-accept + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-success-container style) + :icon :i/done + :text (i18n/label :t/accept)}]) + +(defn- swipe-button-decline + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-danger-container style) + :icon :i/decline + :text (i18n/label :t/decline)}]) + +(defn- swipeable + [{:keys [notification extra-fn]} child] + (let [{:keys [community-id id + membership-status]} notification + accept (rn/use-callback + (fn [] + (rf/dispatch [:communities/accept-request-to-join-pressed + community-id id])) + [community-id id]) + decline (rn/use-callback + (fn [] + (rf/dispatch [:communities/decline-request-to-join-pressed + community-id id])) + [community-id id])] + (cond + (#{constants/activity-center-membership-status-accepted + constants/activity-center-membership-status-declined} + membership-status) + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child] + + (= membership-status constants/activity-center-membership-status-pending) + [common/swipeable + {:left-button swipe-button-accept + :left-on-press accept + :right-button swipe-button-decline + :right-on-press decline + :extra-fn extra-fn} + child] + + :else + child))) + +(defn view + [{:keys [notification] :as props}] + (let [{:keys [author community-id id membership-status + read timestamp]} notification + community-name (rf/sub [:communities/name community-id]) + community-logo (rf/sub [:communities/logo community-id]) + customization-color (rf/sub [:profile/customization-color])] + [swipeable props + [quo/activity-log + {:title (i18n/label :t/join-request) + :customization-color customization-color + :icon :i/add-user + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [[common/user-avatar-tag author] + (i18n/label :t/wants-to-join) + [quo/context-tag + {:type :community + :size 24 + :blur? true + :community-logo community-logo + :community-name community-name}]] + :items (condp = membership-status + constants/activity-center-membership-status-accepted + [{:type :status + :subtype :positive + :key :status-accepted + :blur? true + :label (i18n/label :t/accepted)}] + + constants/activity-center-membership-status-declined + [{:type :status + :subtype :negative + :key :status-declined + :blur? true + :label (i18n/label :t/declined)}] + + constants/activity-center-membership-status-pending + [{:type :button + :subtype :danger + :key :button-decline + :label (i18n/label :t/decline) + :accessibility-label :decline-join-request + :on-press (fn [] + (rf/dispatch + [:communities/decline-request-to-join-pressed + community-id id]))} + {:type :button + :subtype :positive + :key :button-accept + :label (i18n/label :t/accept) + :accessibility-label :accept-join-request + :on-press (fn [] + (rf/dispatch + [:communities/accept-request-to-join-pressed + community-id id]))}] + + nil)}]])) diff --git a/src/status_im/contexts/shell/activity_center/notification/common/style.cljs b/src/status_im/contexts/shell/activity_center/notification/common/style.cljs new file mode 100644 index 00000000000..f0613ffc1ff --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/common/style.cljs @@ -0,0 +1,61 @@ +(ns status-im.contexts.shell.activity-center.notification.common.style + (:require + [quo.foundations.colors :as colors])) + +(def swipe-action-width 80) +(def swipe-button-border-radius 16) +(def swipe-button-margin 8) + +(def user-avatar-tag-text + {:color colors/white}) + +(def left-swipe-opacity-interpolation-js + (clj->js {:inputRange [0 swipe-action-width] + :outputRange [0 1] + :extrapolate :clamp})) + +(def left-swipe-translate-x-interpolation-js + (clj->js {:inputRange [0 swipe-action-width] + :outputRange [(- swipe-action-width) 0] + :extrapolate :clamp})) + +(def right-swipe-opacity-interpolation-js + (clj->js {:inputRange [(- swipe-action-width) 0] + :outputRange [1 0] + :extrapolate :clamp})) + +(def right-swipe-translate-x-interpolation-js + (clj->js {:inputRange [(- swipe-action-width) 0] + :outputRange [0 swipe-action-width] + :extrapolate :clamp})) + +(def swipe-base + {:align-items :center + :justify-content :center + :border-radius swipe-button-border-radius + :width swipe-action-width}) + +(defn swipe-success-container + [style] + (merge swipe-base + {:background-color colors/success-60} + style)) + +(defn swipe-danger-container + [style] + (merge swipe-base + {:background-color colors/danger-60} + style)) + +(defn swipe-primary-container + [style] + (merge swipe-base + {:background-color colors/primary-60} + style)) + +(def swipe-text + {:margin-top 5 + :color colors/white}) + +(def swipe-text-wrapper + {:align-items :center}) diff --git a/src/status_im/contexts/shell/activity_center/notification/common/view.cljs b/src/status_im/contexts/shell/activity_center/notification/common/view.cljs new file mode 100644 index 00000000000..8b0508ee8ea --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/common/view.cljs @@ -0,0 +1,150 @@ +(ns status-im.contexts.shell.activity-center.notification.common.view + (:require + [oops.core :as oops] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.contexts.profile.utils :as profile.utils] + [status-im.contexts.shell.activity-center.context :as ac.context] + [status-im.contexts.shell.activity-center.notification.common.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn user-avatar-tag + [user-id] + (let [profile (rf/sub [:contacts/contact-by-identity user-id])] + [rn/view + {:on-start-should-set-responder + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:chat.ui/show-profile user-id]) + true)} + [quo/context-tag + {:blur? true + :size 24 + :full-name (profile.utils/displayed-name profile) + :profile-picture (profile.utils/photo profile)}]])) + +(defn- render-swipe-action + [{:keys [active-swipeable + extra-fn + interpolation-opacity + interpolation-translate-x + on-press + swipe-button + swipeable-ref + style]}] + (fn [_ drag-x] + (let [extra (extra-fn) + opacity (oops/ocall drag-x :interpolate interpolation-opacity) + translate-x (oops/ocall drag-x :interpolate interpolation-translate-x)] + [gesture/rect-button + {:style (merge {:border-radius style/swipe-button-border-radius} style) + :accessibility-label :notification-swipe-action-button + :on-press (fn [] + (when @swipeable-ref + (oops/ocall @swipeable-ref :close) + (reset! active-swipeable nil)) + (on-press extra))} + [swipe-button + {:style {:opacity opacity + :transform [{:translateX translate-x}] + :flex 1 + :width style/swipe-action-width}} + extra]]))) + +(defn swipe-button-container + [{:keys [style icon text]}] + [rn/animated-view + {:accessibility-label :notification-swipe + :style style} + [rn/view {:style style/swipe-text-wrapper} + [quo/icon icon + {:color colors/white}] + [quo/text + {:style style/swipe-text + :size :paragraph-2 + :weight :medium} + text]]]) + +(defn swipe-button-read-or-unread + [{:keys [style]} {:keys [notification]}] + [swipe-button-container + {:style (style/swipe-primary-container style) + :icon (if (:read notification) + :i/notifications + :i/check) + :text (if (:read notification) + (i18n/label :t/unread) + (i18n/label :t/read))}]) + +(defn swipe-button-delete + [{:keys [style]}] + [swipe-button-container + {:style (style/swipe-danger-container style) + :icon :i/delete + :text (i18n/label :t/delete)}]) + +(defn swipe-on-press-toggle-read + [{:keys [notification]}] + (if (:read notification) + (rf/dispatch [:activity-center.notifications/mark-as-unread (:id notification)]) + (rf/dispatch [:activity-center.notifications/mark-as-read (:id notification)]))) + +(defn swipe-on-press-delete + [{:keys [notification]}] + (rf/dispatch [:activity-center.notifications/delete (:id notification)])) + +(defn swipeable + [{:keys [extra-fn + left-button + left-on-press + right-button + right-on-press]} + child] + (let [{:keys [active-swipeable]} (ac.context/use-context) + this-swipeable (rn/use-ref-atom nil) + set-this-swipeable (rn/use-callback #(reset! this-swipeable %) + [this-swipeable]) + on-swipeable-will-open (rn/use-callback + (fn [] + (when (and @active-swipeable + (not= @active-swipeable @this-swipeable)) + (oops/ocall @active-swipeable :close)) + (reset! active-swipeable @this-swipeable)) + [@active-swipeable @this-swipeable])] + [gesture/swipeable + (cond-> {:ref set-this-swipeable + :accessibility-label :notification-swipeable + :friction 2 + :on-swipeable-will-open on-swipeable-will-open + :children-container-style {:padding-horizontal 20}} + left-button + (assoc :overshoot-left false + :left-threshold style/swipe-action-width + :render-left-actions (render-swipe-action + {:active-swipeable active-swipeable + :extra-fn extra-fn + :interpolation-opacity style/left-swipe-opacity-interpolation-js + :interpolation-translate-x + style/left-swipe-translate-x-interpolation-js + :on-press left-on-press + :swipe-button left-button + :swipeable-ref this-swipeable + :style {:left style/swipe-button-margin}})) + + right-button + (assoc :overshoot-right false + :right-threshold style/swipe-action-width + :render-right-actions (render-swipe-action + {:active-swipeable active-swipeable + :extra-fn extra-fn + :interpolation-opacity style/right-swipe-opacity-interpolation-js + :interpolation-translate-x + style/right-swipe-translate-x-interpolation-js + :on-press right-on-press + :swipe-button right-button + :swipeable-ref this-swipeable + :style {:right (- style/swipe-button-margin)}}))) + child])) diff --git a/src/status_im/contexts/shell/activity_center/notification/community_kicked/view.cljs b/src/status_im/contexts/shell/activity_center/notification/community_kicked/view.cljs new file mode 100644 index 00000000000..598d2ba094c --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/community_kicked/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.shell.activity-center.notification.community-kicked.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.contexts.shell.activity-center.notification.common.style :as common-style] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- swipeable + [{:keys [extra-fn]} child] + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child]) + +(defn view + [{:keys [notification extra-fn]}] + (let [{:keys [id community-id read + timestamp]} notification + community (rf/sub [:communities/community community-id]) + community-name (:name community) + community-image (get-in community [:images :thumbnail :uri]) + customization-color (rf/sub [:profile/customization-color]) + on-press (rn/use-callback + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:activity-center.notifications/mark-as-read id])) + [id])] + [swipeable {:extra-fn extra-fn} + [gesture/touchable-without-feedback {:on-press on-press} + [quo/activity-log + {:title (i18n/label :t/community-kicked-heading) + :customization-color customization-color + :icon :i/placeholder + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [[quo/text {:style common-style/user-avatar-tag-text} + (i18n/label :t/community-kicked-body)] + [quo/context-tag + {:type :community + :size 24 + :blur? true + :community-logo community-image + :community-name community-name}]]}]]])) diff --git a/src/status_im/contexts/shell/activity_center/notification/community_request/view.cljs b/src/status_im/contexts/shell/activity_center/notification/community_request/view.cljs new file mode 100644 index 00000000000..905ab5803e8 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/community_request/view.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.shell.activity-center.notification.community-request.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.notification.common.style :as common-style] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- swipeable + [{:keys [extra-fn]} child] + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child]) + +(defn- get-header-text-and-context + [community-logo community-name community-permissions membership-status] + (let [open? (not= 3 (:access community-permissions)) + community-context-tag [quo/context-tag + {:type :community + :size 24 + :blur? true + :community-logo community-logo + :community-name community-name}]] + (cond + (= membership-status constants/activity-center-membership-status-idle) + {:header-text (i18n/label :t/community-request-not-accepted) + :context [[quo/text {:style common-style/user-avatar-tag-text} + (i18n/label :t/community-request-not-accepted-body-text-prefix)] + community-context-tag + [quo/text {:style common-style/user-avatar-tag-text} + (i18n/label :t/community-request-not-accepted-body-text-suffix)]]} + + (= membership-status constants/activity-center-membership-status-pending) + {:header-text (i18n/label :t/community-request-pending) + :context [[quo/text {:style common-style/user-avatar-tag-text} + (i18n/label :t/community-request-pending-body-text)] + community-context-tag]} + + (= membership-status constants/activity-center-membership-status-accepted) + {:header-text (i18n/label (if open? + :t/join-open-community + :t/community-request-accepted)) + :context [[quo/text {:style common-style/user-avatar-tag-text} + (i18n/label (if open? + :t/joined-community + :t/community-request-accepted-body-text) + (when open? {:community community-name}))] + community-context-tag]}))) + +(defn view + [{:keys [notification extra-fn]}] + (let [{:keys [community-id membership-status read + timestamp]} notification + community-name (rf/sub [:communities/name community-id]) + community-logo (rf/sub [:communities/logo community-id]) + community-permissions (rf/sub [:communities/permissions community-id]) + customization-color (rf/sub [:profile/customization-color]) + {:keys [header-text + context]} (get-header-text-and-context community-logo + community-name + community-permissions + membership-status) + on-press (rn/use-callback + (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:communities/navigate-to-community-overview + community-id])) + [community-id])] + [swipeable {:extra-fn extra-fn} + [gesture/touchable-without-feedback {:on-press on-press} + [quo/activity-log + {:title header-text + :customization-color customization-color + :icon :i/communities + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context context}]]])) diff --git a/src/status_im/contexts/shell/activity_center/notification/contact_requests/events.cljs b/src/status_im/contexts/shell/activity_center/notification/contact_requests/events.cljs new file mode 100644 index 00000000000..1ed1593fbf4 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/contact_requests/events.cljs @@ -0,0 +1,40 @@ +(ns status-im.contexts.shell.activity-center.notification.contact-requests.events + (:require + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/defn accept-contact-request + {:events [:activity-center.contact-requests/accept]} + [_ contact-id] + {:fx [[:json-rpc/call + [{:method "wakuext_acceptContactRequest" + :params [{:id contact-id}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:activity-center.contact-requests/accept-error contact-id]}]]]}) + +(rf/defn accept-contact-request-error + {:events [:activity-center.contact-requests/accept-error]} + [_ contact-id error] + (log/error "Failed to accept contact-request" + {:error error + :event :activity-center.contact-requests/accept + :contact-id contact-id})) + +(rf/defn decline-contact-request + {:events [:activity-center.contact-requests/decline]} + [_ contact-id] + {:fx [[:json-rpc/call + [{:method "wakuext_declineContactRequest" + :params [{:id contact-id}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:activity-center.contact-requests/decline-error contact-id]}]]]}) + +(rf/defn decline-contact-request-error + {:events [:activity-center.contact-requests/decline-error]} + [_ contact-id error] + (log/error "Failed to decline contact-request" + {:error error + :event :activity-center.contact-requests/decline + :contact-id contact-id})) diff --git a/src/status_im/contexts/shell/activity_center/notification/contact_requests/events_test.cljs b/src/status_im/contexts/shell/activity_center/notification/contact_requests/events_test.cljs new file mode 100644 index 00000000000..6561efaf4df --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/contact_requests/events_test.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.shell.activity-center.notification.contact-requests.events-test + (:require + [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.contexts.shell.activity-center.notification.contact-requests.events :as + contact-requests])) + +(deftest accept-contact-request-test + (testing "creates effect for accepting a contact request" + (let [contact-id "0x2" + cofx {:db {}}] + (is (match? + {:fx [[:json-rpc/call + [{:method "wakuext_acceptContactRequest" + :params [{:id contact-id}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:activity-center.contact-requests/accept-error contact-id]}]]]} + (contact-requests/accept-contact-request cofx contact-id)))))) + +(deftest decline-contact-request-test + (testing "creates effect for declining a contact request" + (let [contact-id "0x2" + cofx {:db {}}] + (is (match? + {:fx [[:json-rpc/call + [{:method "wakuext_declineContactRequest" + :params [{:id contact-id}] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error [:activity-center.contact-requests/decline-error contact-id]}]]]} + (contact-requests/decline-contact-request cofx contact-id)))))) diff --git a/src/status_im/contexts/shell/activity_center/notification/contact_requests/view.cljs b/src/status_im/contexts/shell/activity_center/notification/contact_requests/view.cljs new file mode 100644 index 00000000000..3107fc1c2be --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/contact_requests/view.cljs @@ -0,0 +1,183 @@ +(ns status-im.contexts.shell.activity-center.notification.contact-requests.view + (:require + quo.context + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.notification.common.style :as common-style] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- swipe-button-accept + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-success-container style) + :icon :i/done + :text (i18n/label :t/accept)}]) + +(defn- swipe-button-decline + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-danger-container style) + :icon :i/decline + :text (i18n/label :t/decline)}]) + +(defn- swipeable + [{:keys [extra-fn notification]} child] + (let [{:keys [id author message]} notification + {:keys [contact-request-state]} message + {:keys [public-key]} (rf/sub [:multiaccount/contact]) + outgoing? (= public-key author) + accept (rn/use-callback + #(rf/dispatch [:activity-center.contact-requests/accept id]) + [id]) + decline (rn/use-callback + #(rf/dispatch [:activity-center.contact-requests/decline id]) + [id])] + (cond + (or (#{constants/contact-request-message-state-accepted + constants/contact-request-message-state-declined} + contact-request-state) + (and outgoing? (= contact-request-state constants/contact-request-message-state-pending))) + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child] + + (and (= contact-request-state constants/contact-request-message-state-pending) + (not outgoing?)) + [common/swipeable + {:left-button swipe-button-accept + :left-on-press accept + :right-button swipe-button-decline + :right-on-press decline + :extra-fn extra-fn} + child] + + :else + child))) + +(defn- outgoing-contact-request-view + [{:keys [notification]} theme] + (let [{:keys [chat-id message last-message accepted]} notification + {:keys [contact-request-state] :as message} (or message last-message) + customization-color (rf/sub [:profile/customization-color])] + (if accepted + [quo/activity-log + {:title (i18n/label :t/contact-request-was-accepted) + :customization-color customization-color + :icon :i/add-user + :timestamp (datetime/timestamp->relative (:timestamp notification)) + :unread? (not (:read notification)) + :context [[common/user-avatar-tag chat-id] + (i18n/label :t/contact-request-is-now-a-contact)]} + :message {:body (get-in message [:content :text])} + :items []] + [quo/activity-log + {:title (i18n/label :t/contact-request) + :customization-color customization-color + :icon :i/add-user + :timestamp (datetime/timestamp->relative (:timestamp notification)) + :unread? (not (:read notification)) + :context [(i18n/label :t/contact-request-outgoing) + [common/user-avatar-tag chat-id]] + :message {:body (get-in message [:content :text])} + :items (condp = contact-request-state + constants/contact-request-message-state-pending + [{:type :status + :subtype :pending + :key :status-pending + :blur? true + :label (i18n/label :t/pending) + :theme theme}] + + constants/contact-request-message-state-declined + [{:type :status + :subtype :pending + :key :status-pending + :blur? true + :label (i18n/label :t/pending) + :theme theme}] + nil)}]))) + +(defn- incoming-contact-request-view + [{:keys [notification]} theme] + (let [{:keys [id author message + last-message]} notification + customization-color (rf/sub [:profile/customization-color]) + message (or message last-message) + accept (rn/use-callback + (fn [] + (rf/dispatch [:activity-center.contact-requests/accept id])) + [id]) + decline (rn/use-callback + (fn [] + (rf/dispatch [:activity-center.contact-requests/decline id])) + [id])] + [quo/activity-log + {:title (i18n/label :t/contact-request) + :customization-color customization-color + :icon :i/add-user + :timestamp (datetime/timestamp->relative (:timestamp notification)) + :unread? (not (:read notification)) + :context [[common/user-avatar-tag author] + (i18n/label :t/contact-request-sent)] + :message {:body (get-in message [:content :text]) + :body-number-of-lines 2} + :items + (condp = (:contact-request-state message) + constants/contact-request-message-state-accepted + [{:type :status + :subtype :positive + :key :status-accepted + :blur? true + :label (i18n/label :t/accepted) + :theme theme}] + + constants/contact-request-message-state-declined + [{:type :status + :subtype :negative + :key :status-declined + :blur? true + :label (i18n/label :t/declined) + :theme theme}] + + constants/contact-request-message-state-pending + [{:type :button + :subtype :danger + :key :button-decline + :label (i18n/label :t/decline) + :accessibility-label :decline-contact-request + :on-press decline} + {:type :button + :subtype :positive + :key :button-accept + :label (i18n/label :t/accept) + :accessibility-label :accept-contact-request + :on-press accept}] + + nil)}])) + +(defn view + [{:keys [notification] :as props}] + (let [{:keys [author message last-message]} notification + {:keys [public-key]} (rf/sub [:multiaccount/contact]) + {:keys [contact-request-state]} (or message last-message) + theme (quo.context/use-theme)] + [swipeable props + (cond + (= public-key author) + [outgoing-contact-request-view props theme] + + (= contact-request-state constants/contact-request-message-state-accepted) + [rn/pressable + {:on-press #(rf/dispatch [:chat.ui/start-chat author])} + [incoming-contact-request-view props theme]] + + :else + [incoming-contact-request-view props theme])])) diff --git a/src/status_im/contexts/shell/activity_center/notification/contact_verification/view.cljs b/src/status_im/contexts/shell/activity_center/notification/contact_verification/view.cljs new file mode 100644 index 00000000000..9e6ab82d99c --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/contact_verification/view.cljs @@ -0,0 +1,246 @@ +(ns status-im.contexts.shell.activity-center.notification.contact-verification.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.notification.common.style :as common-style] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- swipe-button-decline + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-danger-container style) + :icon :i/decline + :text (i18n/label :t/decline)}]) + +(defn- swipe-button-reply + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-primary-container style) + :icon :i/reply + :text (i18n/label :t/message-reply)}]) + +(defn- swipe-button-untrustworthy + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-danger-container style) + :icon :i/alert + :text (i18n/label :t/untrustworthy)}]) + +(defn- swipe-button-trust + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-success-container style) + :icon :i/done + :text (i18n/label :t/accept)}]) + +(defn- context-tags + [challenger? {:keys [author contact-verification-status]}] + [[common/user-avatar-tag author] + (if challenger? + (when (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-trusted) + (= contact-verification-status constants/contact-verification-status-untrustworthy)) + (str (string/lower-case (i18n/label :t/replied)) ":")) + (when (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-pending) + (= contact-verification-status constants/contact-verification-status-declined)) + (str (i18n/label :t/identity-verification-request-sent) ":")))]) + +(defn- activity-message + [challenger? {:keys [contact-verification-status message reply-message]}] + (if challenger? + (when (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-trusted) + (= contact-verification-status constants/contact-verification-status-untrustworthy)) + {:title (get-in message [:content :text]) + :body (get-in reply-message [:content :text])}) + (when (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-pending) + (= contact-verification-status constants/contact-verification-status-declined)) + {:body (get-in message [:content :text])}))) + +(def ^:private max-reply-length + 280) + +(defn- valid-reply? + [reply] + (<= (count reply) max-reply-length)) + +(def ^:private invalid-reply? + (comp not valid-reply?)) + +(declare view) + +(defn- decline-challenge + [id] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:activity-center.contact-verification/decline id]) + (rf/dispatch [:activity-center.notifications/mark-as-read id])) + +(defn- prepare-challenge-reply + [props] + (rf/dispatch [:show-bottom-sheet + {:content view + :theme :dark} + (assoc props :replying? true)])) + +(defn- send-challenge-reply + [id reply] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:activity-center.contact-verification/reply id reply]) + (rf/dispatch [:activity-center.notifications/mark-as-read id])) + +(defn- mark-challenge-untrustworthy + [id] + (rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy id]) + (rf/dispatch [:activity-center.notifications/mark-as-read id])) + +(defn- mark-challenge-trusted + [id] + (rf/dispatch [:activity-center.contact-verification/mark-as-trusted id]) + (rf/dispatch [:activity-center.notifications/mark-as-read id])) + +(defn- swipeable + [{:keys [extra-fn notification replying?] :as props} child] + (let [{:keys [id message + contact-verification-status]} notification + challenger? (:outgoing message)] + (cond + replying? + child + + (and (not challenger?) + (= contact-verification-status constants/contact-verification-status-pending)) + [common/swipeable + {:left-button swipe-button-reply + :left-on-press #(prepare-challenge-reply props) + :right-button swipe-button-decline + :right-on-press #(decline-challenge id) + :extra-fn extra-fn} + child] + + (and challenger? + (= contact-verification-status constants/contact-verification-status-accepted)) + [common/swipeable + {:left-button swipe-button-trust + :left-on-press #(mark-challenge-trusted id) + :right-button swipe-button-untrustworthy + :right-on-press #(mark-challenge-untrustworthy id) + :extra-fn extra-fn} + child] + + (#{constants/contact-verification-status-accepted + constants/contact-verification-status-declined + constants/contact-verification-status-trusted} + contact-verification-status) + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child] + + :else + child))) + +(defn view + [_] + (let [reply (atom "")] + (fn [{:keys [notification replying?] :as props}] + (let [customization-color (rf/sub [:profile/customization-color]) + {:keys [id message + contact-verification-status]} notification + challenger? (:outgoing message)] + ;; TODO(@ilmotta): Declined challenges should only be displayed for the challengee, not the + ;; challenger. Https://github.com/status-im/status-mobile/issues/14354 + (when-not + (and challenger? + (= contact-verification-status constants/contact-verification-status-declined)) + [swipeable props + [quo/activity-log + {:title (i18n/label :t/identity-verification-request) + :customization-color customization-color + :icon :i/friend + :timestamp (datetime/timestamp->relative (:timestamp notification)) + :unread? (not (:read notification)) + :on-update-reply #(reset! reply %) + :replying? replying? + :max-reply-length max-reply-length + :valid-reply? valid-reply? + :context (context-tags challenger? notification) + :message (activity-message challenger? notification) + :items + (cond-> [] + (and challenger? + (= contact-verification-status constants/contact-verification-status-accepted)) + (concat + [{:type :button + :subtype :danger + :key :button-mark-as-untrustworthy + :label (i18n/label :t/untrustworthy) + :accessibility-label :mark-contact-verification-as-untrustworthy + :on-press #(mark-challenge-untrustworthy id)} + {:type :button + :subtype :positive + :key :button-accept + :label (i18n/label :t/accept) + :accessibility-label :mark-contact-verification-as-trusted + :on-press #(mark-challenge-trusted id)}]) + + (and challenger? + (= contact-verification-status constants/contact-verification-status-trusted)) + (concat [{:type :status + :subtype :positive + :key :status-trusted + :label (i18n/label :t/status-confirmed)}]) + + (and challenger? + (= contact-verification-status + constants/contact-verification-status-untrustworthy)) + (concat [{:type :status + :subtype :negative + :key :status-untrustworthy + :label (i18n/label :t/untrustworthy)}]) + + (and (not challenger?) + (= contact-verification-status constants/contact-verification-status-accepted)) + (concat [{:type :status + :subtype :positive + :key :status-accepted + :label (i18n/label :t/replied)}]) + + (and (not challenger?) + (= contact-verification-status constants/contact-verification-status-declined)) + (concat [{:type :status + :subtype :negative + :key :status-declined + :label (i18n/label :t/declined)}]) + + (and (not challenger?) + (= contact-verification-status constants/contact-verification-status-pending)) + (concat + [{:type :button + :subtype :danger + :key :button-decline + :label (i18n/label :t/decline) + :accessibility-label :decline-contact-verification + :on-press #(decline-challenge id)} + (if replying? + {:type :button + :subtype :primary + :key :button-reply + :label (i18n/label :t/send-reply) + :accessibility-label :reply-to-contact-verification + :disable-when invalid-reply? + :on-press #(send-challenge-reply id @reply)} + {:type :button + :subtype :primary + :key :button-send-reply + :label (i18n/label :t/message-reply) + :accessibility-label :send-reply-to-contact-verification + :on-press #(prepare-challenge-reply props)})]))}]]))))) diff --git a/src/status_im/contexts/shell/activity_center/notification/membership/view.cljs b/src/status_im/contexts/shell/activity_center/notification/membership/view.cljs new file mode 100644 index 00000000000..811fc021261 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/membership/view.cljs @@ -0,0 +1,103 @@ +(ns status-im.contexts.shell.activity-center.notification.membership.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.contexts.shell.activity-center.notification.common.style :as common-style] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- pressable + [{:keys [accepted chat-id]} child] + (if accepted + [gesture/touchable-without-feedback + {:on-press (fn [] + (rf/dispatch [:hide-popover]) + (rf/dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]))} + child] + child)) + +(defn- swipe-button-accept + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-success-container style) + :icon :i/done + :text (i18n/label :t/accept)}]) + +(defn- swipe-button-decline + [{:keys [style]} _] + [common/swipe-button-container + {:style (common-style/swipe-danger-container style) + :icon :i/decline + :text (i18n/label :t/decline)}]) + +(defn- swipeable + [{:keys [notification extra-fn]} child] + (let [{:keys [accepted dismissed + id]} notification + accept (rn/use-callback + (fn [] (rf/dispatch [:activity-center.notifications/accept id])) + [id]) + dismiss (rn/use-callback + (fn [] (rf/dispatch [:activity-center.notifications/dismiss id])) + [id])] + (if (or accepted dismissed) + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child] + [common/swipeable + {:left-button swipe-button-accept + :left-on-press accept + :right-button swipe-button-decline + :right-on-press dismiss + :extra-fn extra-fn} + child]))) + +(defn view + [{:keys [notification] :as props}] + (let [{:keys [id accepted dismissed author read + timestamp chat-name + chat-id]} notification + customization-color (rf/sub [:profile/customization-color]) + accept (rn/use-callback + (fn [] + (rf/dispatch [:activity-center.notifications/accept id])) + [id]) + dismiss (rn/use-callback + (fn [] + (rf/dispatch [:activity-center.notifications/dismiss id])) + [id])] + [swipeable props + [pressable {:accepted accepted :chat-id chat-id} + [quo/activity-log + {:title (i18n/label :t/added-to-group-chat) + :customization-color customization-color + :icon :i/add-user + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [[common/user-avatar-tag author] + (i18n/label :t/added-you-to) + [quo/context-tag + {:type :group + :blur? true + :size 24 + :group-name chat-name}]] + :items (when-not (or accepted dismissed) + [{:type :button + :subtype :positive + :key :button-accept + :label (i18n/label :t/accept) + :accessibility-label :accept-group-chat-invitation + :on-press accept} + {:type :button + :subtype :danger + :key :button-decline + :label (i18n/label :t/decline) + :accessibility-label :decline-group-chat-invitation + :on-press dismiss}])}]]])) diff --git a/src/status_im/contexts/shell/activity_center/notification/mentions/style.cljs b/src/status_im/contexts/shell/activity_center/notification/mentions/style.cljs new file mode 100644 index 00000000000..f1d2f95ae1e --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/mentions/style.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.shell.activity-center.notification.mentions.style + (:require + [quo.foundations.colors :as colors])) + +(def tag-text + {:color colors/white}) + +(def mention-text + {:color colors/white + :border-radius 6 + :padding-horizontal 3 + :background-color colors/white-opa-10}) diff --git a/src/status_im/contexts/shell/activity_center/notification/mentions/view.cljs b/src/status_im/contexts/shell/activity_center/notification/mentions/view.cljs new file mode 100644 index 00000000000..2d688a3e6e8 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/mentions/view.cljs @@ -0,0 +1,99 @@ +(ns status-im.contexts.shell.activity-center.notification.mentions.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.gesture :as gesture] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [status-im.contexts.shell.activity-center.notification.mentions.style :as style] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- mention-text + [literal] + (let [mention (rf/sub [:messages/resolve-mention literal])] + [quo/text + {:style style/mention-text + :size :paragraph-1} + (str "@" mention)])) + +(defn- parsed-text->hiccup + [{:keys [literal destination] message-type :type}] + (case message-type + "mention" [mention-text literal] + "link" destination + literal)) + +(defn- message-body + [{:keys [container parsed-text]}] + (into container (map parsed-text->hiccup) parsed-text)) + +(defn- simple-message + [content] + (let [parsed-text (get-in content [:content :parsed-text 0 :children])] + [message-body + {:container [quo/text + {:number-of-lines 1 + :style style/tag-text + :accessibility-label :activity-message-body + :size :paragraph-1}] + :parsed-text parsed-text}])) + +(defn- album-message + [content] + (let [parsed-text (get-in content [0 :parsedText 0 :children]) + images (map :image content)] + [quo/activity-logs-photos + {:photos images + :message-text [message-body + {:container [:<>] + :parsed-text parsed-text}]}])) + +(defn- swipeable + [{:keys [extra-fn]} child] + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child]) + +(defn view + [{:keys [notification extra-fn]}] + (let [{:keys [author chat-name community-id chat-id message read timestamp + album-messages]} notification + community-chat? (not (string/blank? community-id)) + community-name (rf/sub [:communities/name community-id]) + community-logo (rf/sub [:communities/logo community-id]) + customization-color (rf/sub [:profile/customization-color])] + [swipeable {:extra-fn extra-fn} + [gesture/touchable-without-feedback + {:on-press (fn [] + (rf/dispatch [:hide-popover]) + (rf/dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]))} + [quo/activity-log + {:title (i18n/label :t/mention) + :customization-color customization-color + :icon :i/mention + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [[common/user-avatar-tag author] + [quo/text {:style style/tag-text} + (string/lower-case (i18n/label :t/on-capitalized))] + (if community-chat? + [quo/context-tag + {:type :channel + :blur? true + :size 24 + :community-logo community-logo + :community-name community-name + :channel-name chat-name}] + [quo/context-tag + {:type :group + :group-name chat-name + :blur? true + :size 24}])] + :message {:body (if album-messages + [album-message album-messages] + [simple-message message])}}]]])) diff --git a/src/status_im/contexts/shell/activity_center/notification/news/view.cljs b/src/status_im/contexts/shell/activity_center/notification/news/view.cljs new file mode 100644 index 00000000000..838641034cb --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/news/view.cljs @@ -0,0 +1,90 @@ +(ns status-im.contexts.shell.activity-center.notification.news.view + (:require [clojure.string :as string] + [promesa.core :as promesa] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.gesture :as gesture] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn auto-resized-image + [url] + (let [[height set-height] (rn/use-state nil) + window-width (- (:width (rn/get-window)) 40)] + (rn/use-effect #(-> (rn/image-get-size url) + (promesa/then (fn [[w h]] + (let [scale (/ window-width w) + new-height (* h scale)] + (set-height new-height)))))) + (if height + [fast-image/fast-image + {:source url + :style {:width window-width + :height height + :align-self :center + :border-radius 12}}] + [rn/view {:style {:height 200 :align-items :center :justify-content :center}} + [rn/activity-indicator]]))) + +(defn news-sheet + [{:keys [news-image-url news-title news-content news-link news-link-label]} timestamp] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title news-title :description timestamp}] + [rn/scroll-view {:style {:flex 1}} + (when (not (string/blank? news-image-url)) + [auto-resized-image news-image-url]) + [quo/text + {:style {:padding-horizontal 20 + :padding-vertical 8}} + news-content]] + (when (and (not (string/blank? news-link)) (not (string/blank? news-link-label))) + [quo/bottom-actions + {:button-one-label news-link-label + :button-one-props {:customization-color customization-color + :icon-right :i/external + :on-press (fn [] + (rf/dispatch [:browser.ui/open-url news-link]) + (rf/dispatch [:hide-bottom-sheet]))}}])])) + +(defn view + [{:keys [notification extra-fn]}] + (let [customization-color (rf/sub [:profile/customization-color]) + {:keys [news-title + news-description + timestamp + read + id]} notification + timestamp (datetime/timestamp->relative timestamp) + show-bottom-preview (rn/use-callback + (fn [] + (rf/dispatch [:activity-center.notifications/mark-as-read id]) + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :content (fn [] + [news-sheet notification timestamp])}])))] + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + [gesture/touchable-without-feedback + {:on-press show-bottom-preview} + [quo/activity-log + {:title news-title + :customization-color customization-color + :icon :i/status-logo-bw + :timestamp timestamp + :unread? (not read) + :context [[quo/text {} news-description]] + :items [{:type :button + :subtype :primary + :key :button-reply + :customization-color customization-color + :label (i18n/label :t/read-more) + :accessibility-label :read-more + :on-press show-bottom-preview}]}]]])) diff --git a/src/status_im/contexts/shell/activity_center/notification/reply/style.cljs b/src/status_im/contexts/shell/activity_center/notification/reply/style.cljs new file mode 100644 index 00000000000..a61f5d3dae8 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/reply/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.shell.activity-center.notification.reply.style + (:require + [quo.foundations.colors :as colors])) + +(def tag-text + {:color colors/white}) + +(def lowercase-text + {:color colors/white + :text-transform :lowercase}) diff --git a/src/status_im/contexts/shell/activity_center/notification/reply/view.cljs b/src/status_im/contexts/shell/activity_center/notification/reply/view.cljs new file mode 100644 index 00000000000..a319cbd564d --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/reply/view.cljs @@ -0,0 +1,118 @@ +(ns status-im.contexts.shell.activity-center.notification.reply.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.gesture :as gesture] + [status-im.common.not-implemented :as not-implemented] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.content.sticker-message.view :as sticker-message] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [status-im.contexts.shell.activity-center.notification.reply.style :as style] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.url :as url])) + +;; NOTE: Replies support text, image and stickers only. +(defn- get-message-content + [{:keys [content-type] :as message} album-messages media-server-port] + (condp = content-type + constants/content-type-text + [quo/text {:style style/tag-text} + (get-in message [:content :text])] + + constants/content-type-image + (let [images (or album-messages message) + image-urls (if album-messages + (map :image images) + [(get-in message [:content :image])]) + image-local-urls (map (fn [url] + {:uri (url/replace-port url media-server-port)}) + image-urls)] + [quo/activity-logs-photos + {:photos image-local-urls + :message-text (get-in message [:content :text])}]) + + constants/content-type-sticker + [sticker-message/view {:url (-> message :content :sticker :url)}] + + constants/content-type-system-pinned-message + [not-implemented/not-implemented + [quo/text {:style style/tag-text} + (get-in message [:content :text])]] + + ;; NOTE: The following type (system-text) doesn't have a design yet. + ;; https://github.com/status-im/status-mobile/issues/14915 + constants/content-type-system-text + [not-implemented/not-implemented + [quo/text {:style style/tag-text} + (get-in message [:content :text])]] + + nil)) + +(defn- swipeable + [{:keys [extra-fn]} child] + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child]) + +(defn view + [{:keys [notification extra-fn]}] + (let [{:keys [author chat-name community-id chat-id + message read timestamp + album-messages]} notification + community-chat? (not (string/blank? community-id)) + community-name (rf/sub [:communities/name community-id]) + community-logo (rf/sub [:communities/logo community-id]) + customization-color (rf/sub [:profile/customization-color]) + media-server-port (rf/sub [:mediaserver/port])] + [swipeable {:extra-fn extra-fn} + [gesture/touchable-without-feedback + {:on-press (fn [] + (rf/dispatch [:hide-popover]) + (rf/dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]))} + [quo/activity-log + {:title (i18n/label :t/message-reply) + :customization-color customization-color + :icon :i/reply + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [[common/user-avatar-tag author] + [quo/text {:style style/lowercase-text} (i18n/label :t/on-capitalized)] + (if community-chat? + [quo/context-tag + {:type :channel + :blur? true + :size 24 + :community-logo community-logo + :community-name community-name + :channel-name chat-name}] + [quo/context-tag + {:type :group + :group-name chat-name + :blur? true + :size 24}])] + :message {:body-number-of-lines 1 + :attachment (cond + (= (:content-type message) + constants/content-type-text) + :text + + (= (:content-type message) + constants/content-type-image) + :photo + + (= (:content-type message) + constants/content-type-sticker) + :sticker + + (= (:content-type message) + constants/content-type-gif) + :gif) + :body (get-message-content message + album-messages + media-server-port)}}]]])) diff --git a/src/status_im/contexts/shell/activity_center/notification/syncing/view.cljs b/src/status_im/contexts/shell/activity_center/notification/syncing/view.cljs new file mode 100644 index 00000000000..9e320377e07 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification/syncing/view.cljs @@ -0,0 +1,78 @@ +(ns status-im.contexts.shell.activity-center.notification.syncing.view + (:require + quo.context + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.new-device-sheet.view :as new-device-sheet] + [status-im.contexts.shell.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- swipeable + [{:keys [extra-fn]} child] + [common/swipeable + {:left-button common/swipe-button-read-or-unread + :left-on-press common/swipe-on-press-toggle-read + :right-button common/swipe-button-delete + :right-on-press common/swipe-on-press-delete + :extra-fn extra-fn} + child]) + +(defn installation-created-view + [{:keys [notification extra-fn]}] + (let [{:keys [installation-id read timestamp]} notification + customization-color (rf/sub [:profile/customization-color]) + theme (quo.context/use-theme) + more-details (rn/use-callback + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content + (fn [] + [new-device-sheet/installation-request-creator-view + installation-id])}])) + [installation-id])] + [swipeable {:extra-fn extra-fn} + [quo/activity-log + {:title (i18n/label :t/sync-your-profile) + :customization-color customization-color + :icon :i/mobile + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [(i18n/label :t/check-other-device-for-pairing)] + :items [{:type :button + :subtype :positive + :key :review-pairing-request + :blur? true + :label (i18n/label :t/more-details) + :theme theme + :on-press more-details}]}]])) + +(defn installation-received-view + [{:keys [notification extra-fn]}] + (let [{:keys [installation-id read timestamp]} notification + customization-color (rf/sub [:profile/customization-color]) + theme (quo.context/use-theme) + review-pairing-request (rn/use-callback + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content + (fn [] + [new-device-sheet/installation-request-receiver-view + installation-id])}])) + [installation-id])] + [swipeable {:extra-fn extra-fn} + [quo/activity-log + {:title (i18n/label :t/new-device-detected) + :customization-color customization-color + :icon :i/mobile + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [(i18n/label :t/new-device-detected)] + :items [{:type :button + :subtype :positive + :key :review-pairing-request + :blur? true + :label (i18n/label :t/review-pairing-request) + :theme theme + :on-press review-pairing-request}]}]])) diff --git a/src/status_im/contexts/shell/activity_center/notification_types.cljs b/src/status_im/contexts/shell/activity_center/notification_types.cljs new file mode 100644 index 00000000000..ded40a0a547 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/notification_types.cljs @@ -0,0 +1,42 @@ +(ns status-im.contexts.shell.activity-center.notification-types) + +(def ^:const no-type 0) +(def ^:const one-to-one-chat 1) +(def ^:const private-group-chat 2) +(def ^:const mention 3) +(def ^:const reply 4) +(def ^:const contact-request 5) +(def ^:const community-request 7) +(def ^:const admin 8) +(def ^:const community-kicked 9) +(def ^:const contact-verification 10) +(def ^:const new-installation-received 23) +(def ^:const new-installation-created 24) +(def ^:const news-feed 29) + +(def ^:const all-supported + #{one-to-one-chat + private-group-chat + mention + reply + contact-request + community-request + admin + community-kicked + contact-verification + new-installation-received + new-installation-created + news-feed}) + +;; TODO: Replace with correct enum values once status-go implements them. +(def ^:const tx 66612) +(def ^:const system + #{new-installation-received new-installation-created}) + +(def ^:const membership + "Membership is like a logical group of notifications with different types, i.e. + it doesn't have a corresponding type in the backend. Think of the collection + as a composite key of actual types." + #{private-group-chat + community-request + community-kicked}) diff --git a/src/status_im/contexts/shell/activity_center/style.cljs b/src/status_im/contexts/shell/activity_center/style.cljs new file mode 100644 index 00000000000..168c8dd379f --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/style.cljs @@ -0,0 +1,42 @@ +(ns status-im.contexts.shell.activity-center.style + (:require + [quo.foundations.colors :as colors])) + +(def screen-padding 20) + +(def header-container + {:flex-direction :row + :justify-content :space-between + :padding-horizontal screen-padding + :margin-vertical 12}) + +(def header-heading + {:padding-horizontal screen-padding + :padding-vertical 12 + :color colors/white}) + +(defn notification-container + [index] + {:margin-top (if (zero? index) 0 4)}) + +(def tabs + {:padding-left screen-padding}) + +(def tabs-container + {:flex 1 + :align-self :stretch}) + +(def filter-toggle-container + {:flex-grow 0 + :margin-left 16 + :padding-right screen-padding}) + +(def tabs-and-filter-container + {:flex-direction :row + :padding-vertical 12}) + +(def empty-container + {:align-items :center + :flex 1 + :justify-content :center + :padding-bottom 20}) diff --git a/src/status_im/contexts/shell/activity_center/tabs/empty_tab/view.cljs b/src/status_im/contexts/shell/activity_center/tabs/empty_tab/view.cljs new file mode 100644 index 00000000000..13170ab5c3c --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/tabs/empty_tab/view.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.shell.activity-center.tabs.empty-tab.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.shell.activity-center.notification-types :as types] + [status-im.contexts.shell.activity-center.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:private empty-tab-description + {types/no-type (i18n/label :t/empty-notifications-all-tab) + types/admin (i18n/label :t/empty-notifications-admin-tab) + types/mention (i18n/label :t/empty-notifications-mentions-tab) + types/reply (i18n/label :t/empty-notifications-replies-tab) + types/contact-request (i18n/label :t/empty-notifications-contact-requests-tab) + types/contact-verification (i18n/label :t/empty-notifications-identity-verification-tab) + types/tx (i18n/label :t/empty-notifications-transactions-tab) + types/membership (i18n/label :t/empty-notifications-membership-tab) + types/system (i18n/label :t/empty-notifications-system-tab)}) + +(defn empty-tab + [] + (let [theme (quo.context/use-theme) + filter-type (rf/sub [:activity-center/filter-type]) + description (get empty-tab-description filter-type nil)] + [rn/view {:style style/empty-container} + [quo/empty-state + {:blur? true + :image (resources/get-themed-image :no-notifications theme) + :title (i18n/label :t/empty-notifications-title-unread) + :description description}]])) diff --git a/src/status_im/contexts/shell/activity_center/tabs/view.cljs b/src/status_im/contexts/shell/activity_center/tabs/view.cljs new file mode 100644 index 00000000000..f412a2ef029 --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/tabs/view.cljs @@ -0,0 +1,78 @@ +(ns status-im.contexts.shell.activity-center.tabs.view + (:require + [clojure.set :as set] + [quo.core :as quo] + [status-im.config :as config] + [status-im.contexts.shell.activity-center.notification-types :as types] + [status-im.contexts.shell.activity-center.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn tabs + [] + (let [customization-color (rf/sub [:profile/customization-color]) + filter-type (rf/sub [:activity-center/filter-type]) + types-with-unread (rf/sub [:activity-center/notification-types-with-unread]) + is-mark-all-as-read-undoable? (boolean (rf/sub + [:activity-center/mark-all-as-read-undoable-till]))] + [quo/tabs + {:size 32 + :scrollable? true + :customization-color customization-color + :blur? true + :style style/tabs + :fade-end-percentage 0.79 + :scroll-on-press? true + :fade-end? true + :on-change #(rf/dispatch [:activity-center.notifications/fetch-first-page + {:filter-type %}]) + :default-active filter-type + :data [{:id types/no-type + :label (i18n/label :t/all)} + {:id types/news-feed + :label (i18n/label :t/news) + :accessibility-label :tab-news + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread types/news-feed))} + {:id types/admin + :label (i18n/label :t/admin) + :accessibility-label :tab-admin + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread types/admin))} + {:id types/mention + :label (i18n/label :t/mentions) + :accessibility-label :tab-mention + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread types/mention))} + {:id types/reply + :label (i18n/label :t/replies) + :accessibility-label :tab-reply + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread types/reply))} + {:id types/contact-request + :label (i18n/label :t/contact-requests) + :accessibility-label :tab-contact-request + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread types/contact-request))} + (when config/show-not-implemented-features? + {:id types/contact-verification + :label (i18n/label :t/identity-verification) + :accessibility-label :tab-contact-verification + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread + types/contact-verification))}) + {:id types/tx + :label (i18n/label :t/transactions) + :accessibility-label :tab-tx + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread types/tx))} + {:id types/membership + :label (i18n/label :t/membership) + :accessibility-label :tab-membership + :notification-dot? (when-not is-mark-all-as-read-undoable? + (set/subset? types/membership types-with-unread))} + {:id types/system + :label (i18n/label :t/system) + :accessibility-label :tab-system + :notification-dot? (when-not is-mark-all-as-read-undoable? + (contains? types-with-unread types/system))}]}])) diff --git a/src/status_im/contexts/shell/activity_center/view.cljs b/src/status_im/contexts/shell/activity_center/view.cljs new file mode 100644 index 00000000000..2fe1bc69a0d --- /dev/null +++ b/src/status_im/contexts/shell/activity_center/view.cljs @@ -0,0 +1,98 @@ +(ns status-im.contexts.shell.activity-center.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.navigation :as navigation] + [status-im.constants :as constants] + [status-im.contexts.shell.activity-center.context :as ac.context] + [status-im.contexts.shell.activity-center.header.view :as header] + [status-im.contexts.shell.activity-center.notification-types :as types] + [status-im.contexts.shell.activity-center.notification.admin.view :as admin] + [status-im.contexts.shell.activity-center.notification.community-kicked.view :as + community-kicked] + [status-im.contexts.shell.activity-center.notification.community-request.view :as + community-request] + [status-im.contexts.shell.activity-center.notification.contact-requests.view :as + contact-requests] + [status-im.contexts.shell.activity-center.notification.contact-verification.view :as + contact-verification] + [status-im.contexts.shell.activity-center.notification.membership.view :as membership] + [status-im.contexts.shell.activity-center.notification.mentions.view :as mentions] + [status-im.contexts.shell.activity-center.notification.news.view :as news] + [status-im.contexts.shell.activity-center.notification.reply.view :as reply] + [status-im.contexts.shell.activity-center.notification.syncing.view :as syncing] + [status-im.contexts.shell.activity-center.style :as style] + [status-im.contexts.shell.activity-center.tabs.empty-tab.view :as empty-tab] + [utils.re-frame :as rf])) + +(defn notification-component + [{notification-type :type :as notification} index] + (let [extra-fn (rn/use-callback + (fn [] + {:notification notification}) + [notification]) + props {:notification notification + :extra-fn extra-fn}] + [rn/view {:style (style/notification-container index)} + (cond + (= notification-type types/contact-verification) + [contact-verification/view props] + + (= notification-type types/contact-request) + [contact-requests/view props] + + (= notification-type types/mention) + [mentions/view props] + + (= notification-type types/reply) + [reply/view props] + + (= notification-type types/admin) + [admin/view props] + + (= notification-type types/new-installation-received) + [syncing/installation-received-view props] + + (= notification-type types/new-installation-created) + [syncing/installation-created-view props] + + (= notification-type types/news-feed) + [news/view props] + + (types/membership notification-type) + (condp = notification-type + types/private-group-chat [membership/view props] + types/community-request [community-request/view props] + types/community-kicked [community-kicked/view props] + nil))])) + +(defn- fetch-next-page + [] + (rf/dispatch [:activity-center.notifications/fetch-next-page])) + +(defn view + [] + (let [notifications (rf/sub [:activity-center/notifications]) + ;; We globally control the active swipeable for all notifications + ;; because when a swipe left/right gesture initiates, the previously + ;; active swiped notification (if any) must be removed & closed with + ;; animation. + active-swipeable (rn/use-ref-atom nil)] + (rn/use-mount + (fn [] + (rf/dispatch [:activity-center.notifications/fetch-first-page]))) + + [ac.context/provider {:active-swipeable active-swipeable} + [quo/overlay {:type :shell} + [rn/view {:style {:flex 1 :padding-top (navigation/status-bar-height)}} + [header/header] + (rn/delay-render + [rn/flat-list + {:data notifications + :initial-num-to-render constants/notifications-per-page + :content-container-style {:flex-grow 1} + :empty-component [empty-tab/empty-tab] + :key-fn :id + :on-scroll-to-index-failed identity + :on-end-reached fetch-next-page + :render-fn notification-component}])]]])) diff --git a/src/status_im/contexts/shell/bottom_tabs/style.cljs b/src/status_im/contexts/shell/bottom_tabs/style.cljs new file mode 100644 index 00000000000..81dd6e3461b --- /dev/null +++ b/src/status_im/contexts/shell/bottom_tabs/style.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.shell.bottom-tabs.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [status-im.contexts.shell.constants :as constants] + [status-im.contexts.shell.utils :as utils])) + +(defn bottom-tabs-container + [privacy-mode-enabled?] + [{:background-color (if privacy-mode-enabled? + colors/neutral-80-opa-40 + colors/neutral-100) + :align-items :center + :margin-top (- constants/home-stack-radius) + :height (utils/bottom-tabs-container-height) + :accessibility-label :bottom-tabs-container}]) + +(defn bottom-tabs + [] + {:flex-direction :row + :position :absolute + :bottom (if platform/android? 8 34) + :flex 1 + :accessibility-label :bottom-tabs}) diff --git a/src/status_im/contexts/shell/bottom_tabs/view.cljs b/src/status_im/contexts/shell/bottom_tabs/view.cljs new file mode 100644 index 00000000000..74f6176955d --- /dev/null +++ b/src/status_im/contexts/shell/bottom_tabs/view.cljs @@ -0,0 +1,57 @@ +(ns status-im.contexts.shell.bottom-tabs.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [status-im.config :as config] + [status-im.contexts.shell.bottom-tabs.style :as style] + [status-im.contexts.shell.constants :as shell.constants] + [status-im.feature-flags :as ff] + [utils.re-frame :as rf])) + +(defn bottom-tab + [icon stack-id shared-values privacy-mode-enabled?] + (let [notifications-data (rf/sub [:shell/bottom-tabs-notifications-data]) + customization-color (rf/sub [:profile/customization-color]) + on-press (rn/use-callback #(rf/dispatch [:shell/change-tab stack-id])) + icon-color (->> stack-id + (get shell.constants/tabs-icon-color-keywords) + (get shared-values))] + [quo/bottom-nav-tab + (-> notifications-data + (get stack-id) + (assoc :test-ID stack-id + :icon icon + :icon-color-anim icon-color + :on-press on-press + :pass-through? privacy-mode-enabled? + :accessibility-label (str (name stack-id) "-tab") + :customization-color customization-color))])) + +(defn view + [shared-values privacy-mode-enabled?] + (let [communities-double-tab-gesture (-> (gesture/gesture-tap) + (gesture/number-of-taps 2) + (gesture/on-start + (fn [_event] + (rf/dispatch [:communities/select-tab :joined])))) + messages-double-tap-gesture (-> (gesture/gesture-tap) + (gesture/number-of-taps 2) + (gesture/on-start + (fn [_event] + (rf/dispatch [:messages-home/select-tab :tab/recent]))))] + [quo.context/provider {:theme :dark} + [reanimated/view + {:style (style/bottom-tabs-container privacy-mode-enabled?)} + [rn/view {:style (style/bottom-tabs)} + [bottom-tab :i/wallet :screen/wallet-stack shared-values privacy-mode-enabled?] + (when (ff/enabled? ::ff/market) + [bottom-tab :i/swap :screen/market-stack shared-values privacy-mode-enabled?]) + [gesture/gesture-detector {:gesture messages-double-tap-gesture} + [bottom-tab :i/messages :screen/chats-stack shared-values privacy-mode-enabled?]] + [gesture/gesture-detector {:gesture communities-double-tab-gesture} + [bottom-tab :i/communities :screen/communities-stack shared-values privacy-mode-enabled?]] + (when config/show-not-implemented-features? + [bottom-tab :i/browser :screen/browser-stack shared-values privacy-mode-enabled?])]]])) diff --git a/src/status_im/contexts/shell/constants.cljs b/src/status_im/contexts/shell/constants.cljs new file mode 100644 index 00000000000..d95177adada --- /dev/null +++ b/src/status_im/contexts/shell/constants.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.shell.constants) + +(def ^:const floating-shell-button-height 44) +(def ^:const home-stack-radius 20) + +(def ^:const default-selected-stack :screen/wallet-stack) + +;; Bottom tabs +(def ^:const bottom-tabs-container-height-android 57) +(def ^:const bottom-tabs-container-height-ios 82) + +;; Stacks +(def stacks-ids + #{:screen/communities-stack :screen/chats-stack :screen/wallet-stack :screen/browser-stack + :screen/market-stack}) + +;; Keywords +(def ^:const stacks-opacity-keywords + {:screen/communities-stack :communities-stack-opacity + :screen/chats-stack :chats-stack-opacity + :screen/wallet-stack :wallet-stack-opacity + :screen/market-stack :market-stack-opacity + :screen/browser-stack :browser-stack-opacity}) + +(def ^:const tabs-icon-color-keywords + {:screen/communities-stack :communities-tab-icon-color + :screen/chats-stack :chats-tab-icon-opacity + :screen/wallet-stack :wallet-tab-icon-opacity + :screen/market-stack :market-tab-icon-opacity + :screen/browser-stack :browser-tab-icon-opacity}) + +(def ^:const stacks-z-index-keywords + {:screen/communities-stack :communities-stack-z-index + :screen/chats-stack :chats-stack-z-index + :screen/wallet-stack :wallet-stack-z-index + :screen/market-stack :market-stack-z-index + :screen/browser-stack :browser-stack-z-index}) diff --git a/src/status_im/contexts/shell/effects.cljs b/src/status_im/contexts/shell/effects.cljs new file mode 100644 index 00000000000..1db56255c59 --- /dev/null +++ b/src/status_im/contexts/shell/effects.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.shell.effects + (:require + [react-native.reanimated :as reanimated] + [status-im.contexts.shell.constants :as shell.constants] + [status-im.contexts.shell.state :as state] + [status-im.contexts.shell.utils :as utils] + [utils.re-frame :as rf])) + +(rf/reg-fx :effects.shell/change-tab + (fn [stack-id] + (when (and (not= stack-id @state/selected-stack-id-value) + (shell.constants/stacks-ids stack-id)) + (some-> @state/selected-stack-id-shared-value + (reanimated/set-shared-value (name stack-id))) + (utils/change-selected-stack stack-id)))) diff --git a/src/status_im/contexts/shell/events.cljs b/src/status_im/contexts/shell/events.cljs new file mode 100644 index 00000000000..efdb929516a --- /dev/null +++ b/src/status_im/contexts/shell/events.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.shell.events + (:require + [re-frame.core :as re-frame])) + +(re-frame/reg-event-fx :shell/change-tab + (fn [{:keys [db]} [stack-id]] + {:db (-> db + (assoc :view-id stack-id) + (assoc :shell/selected-stack-id stack-id)) + :fx [[:effects.shell/change-tab stack-id]]})) diff --git a/src/status_im/contexts/shell/home_stack/style.cljs b/src/status_im/contexts/shell/home_stack/style.cljs new file mode 100644 index 00000000000..3254a580048 --- /dev/null +++ b/src/status_im/contexts/shell/home_stack/style.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.shell.home-stack.style + (:require [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.contexts.shell.constants :as shell.constants])) + +(defn stack-view + [shared-values stack-id] + [{:opacity (->> stack-id + (get shell.constants/stacks-opacity-keywords) + (get shared-values)) + :z-index (->> stack-id + (get shell.constants/stacks-z-index-keywords) + (get shared-values))} + rn/stylesheet-absolute-fill]) + +(defn home-stack + [theme] + {:border-bottom-left-radius 20 + :z-index 2 + :border-bottom-right-radius 20 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :flex 1}) diff --git a/src/status_im/contexts/shell/home_stack/view.cljs b/src/status_im/contexts/shell/home_stack/view.cljs new file mode 100644 index 00000000000..1846a4dfbe4 --- /dev/null +++ b/src/status_im/contexts/shell/home_stack/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.shell.home-stack.view + (:require + [legacy.status-im.ui.screens.browser.stack :as browser.stack] + [quo.context] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.contexts.chat.home.view :as chat] + [status-im.contexts.communities.home.view :as communities] + [status-im.contexts.shell.home-stack.style :as style] + [status-im.contexts.shell.state :as state] + [status-im.contexts.wallet.home.view :as wallet] + [status-im.ui.market.view :as market])) + +(defn load-stack? + [stack-id] + (case stack-id + :screen/communities-stack @state/load-communities-stack? + :screen/chats-stack @state/load-chats-stack? + :screen/browser-stack @state/load-browser-stack? + :screen/wallet-stack @state/load-wallet-stack? + :screen/market-stack @state/load-market-stack?)) + +(defn- stack-view + [stack-id shared-values] + [reanimated/view + {:style (style/stack-view shared-values stack-id) + :accessibility-label stack-id} + (case stack-id + :screen/communities-stack [communities/view] + :screen/chats-stack [chat/view] + :screen/wallet-stack [wallet/view] + :screen/market-stack [market/view] + :screen/browser-stack [browser.stack/browser-stack] + [:<>])]) + +(defn lazy-screen + [stack-id shared-values theme] + (when (load-stack? stack-id) + [quo.context/provider {:theme theme :screen-id stack-id} + [stack-view stack-id shared-values]])) + +(defn view + [shared-values] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/home-stack theme)} + [lazy-screen :screen/communities-stack shared-values theme] + [lazy-screen :screen/chats-stack shared-values theme] + [lazy-screen :screen/browser-stack shared-values theme] + [lazy-screen :screen/wallet-stack shared-values theme] + [lazy-screen :screen/market-stack shared-values theme]])) diff --git a/src/status_im/contexts/shell/qr_reader/events.cljs b/src/status_im/contexts/shell/qr_reader/events.cljs new file mode 100644 index 00000000000..f981d1bd5b4 --- /dev/null +++ b/src/status_im/contexts/shell/qr_reader/events.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.shell.qr-reader.events + (:require + [status-im.contexts.shell.qr-reader.sheets.scanned-wallet-address :as scanned-address] + [utils.re-frame :as rf])) + +(rf/reg-event-fx + :generic-scanner/scan-success + (fn [_ [address]] + {:fx [[:dispatch [:navigate-back]] + [:dispatch [:show-bottom-sheet {:content #(scanned-address/view address)}]]]})) diff --git a/src/status_im/contexts/shell/qr_reader/sheets/scanned_wallet_address.cljs b/src/status_im/contexts/shell/qr_reader/sheets/scanned_wallet_address.cljs new file mode 100644 index 00000000000..b70768b0a7b --- /dev/null +++ b/src/status_im/contexts/shell/qr_reader/sheets/scanned_wallet_address.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.shell.qr-reader.sheets.scanned-wallet-address + (:require + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- copy-address + [address] + (clipboard/set-string address) + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label :t/address-copied)}])) + +(defn- send-to-address + [address] + (rf/dispatch + [:wallet/init-send-flow-for-address + {:address address + :recipient {:recipient-type :address + :label (utils/get-shortened-address address)} + :stack-id :wallet-select-address}])) + +(defn view + [address] + [:<> + [quo/drawer-top {:title address :type :address}] + [quo/action-drawer + [[{:icon :i/copy + :accessibility-label :send-asset + :label (i18n/label :t/copy-address) + :on-press #(copy-address address)} + {:icon :i/send + :accessibility-label :send-asset + :label (i18n/label :t/send-to-this-address) + :on-press #(send-to-address address)} + (when (ff/enabled? :ff/wallet.saved-addresses) + {:icon :i/save + :accessibility-label :save-address + :label (i18n/label :t/save-address) + :on-press #(js/alert "feature not implemented")})]]]]) diff --git a/src/status_im/contexts/shell/qr_reader/view.cljs b/src/status_im/contexts/shell/qr_reader/view.cljs new file mode 100644 index 00000000000..9a6960190d7 --- /dev/null +++ b/src/status_im/contexts/shell/qr_reader/view.cljs @@ -0,0 +1,119 @@ +(ns status-im.contexts.shell.qr-reader.view + (:require [clojure.string :as string] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [status-im.common.router :as router] + [status-im.common.scan-qr-code.view :as scan-qr-code] + [status-im.common.validation.general :as validators] + [status-im.contexts.communities.events] + [status-im.contexts.wallet.wallet-connect.utils.uri :as wc-uri] + [status-im.feature-flags :as ff] + [utils.address :as utils-address] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.url :as url])) + +(def invalid-qr-toast + {:type :negative + :theme :dark + :text (i18n/label :t/invalid-qr)}) + +(defn- text-a-status-url-for-path? + [text path] + (some #(string/starts-with? text %) (router/prepend-status-urls path))) + +(defn- extract-id + [scanned-text] + (let [index (string/index-of scanned-text "#")] + (subs scanned-text (inc index)))) + +(defn pairing-qr-code? + [_] + false) + +(defn url? + [scanned-text] + (url/url? scanned-text)) + +(defn load-and-show-profile + [address] + (debounce/debounce-and-dispatch + [:contacts/set-new-identity + {:input address + :build-success-fn (fn [{:keys [public-key ens-name]}] + {:dispatch-n [[:chat.ui/show-profile public-key ens-name] + [:contacts/clear-new-identity]]}) + :failure-fn (fn [] + {:dispatch [:toasts/upsert invalid-qr-toast]})}] + 300)) + +(defn show-invalid-qr-toast + [] + (debounce/debounce-and-dispatch + [:toasts/upsert invalid-qr-toast] + 300)) + +(defn- handle-wallet-connect + [scanned-text] + (debounce/debounce-and-dispatch + [:wallet-connect/on-scan-connection scanned-text] + 300)) + +(defn- on-qr-code-scanned + [scanned-text] + (cond + (or + (text-a-status-url-for-path? scanned-text router/community-with-data-path) + (text-a-status-url-for-path? scanned-text router/channel-path)) + (debounce/debounce-and-dispatch [:universal-links/handle-url scanned-text] 300) + + (text-a-status-url-for-path? scanned-text router/user-with-data-path) + (let [address (extract-id scanned-text)] + (load-and-show-profile address)) + + (or (validators/valid-public-key? scanned-text) + (validators/valid-compressed-key? scanned-text)) + (load-and-show-profile scanned-text) + + (utils-address/supported-address? scanned-text) + (when-let [address (utils-address/supported-address->eth-address scanned-text)] + (debounce/debounce-and-dispatch [:generic-scanner/scan-success address] 300) + (debounce/debounce-and-dispatch [:shell/change-tab :screen/wallet-stack] 300)) + + (utils-address/eip-681-address? scanned-text) + (do + (debounce/debounce-and-dispatch [:generic-scanner/scan-success + (utils-address/eip-681-address->eth-address scanned-text)] + 300) + (debounce/debounce-and-dispatch [:shell/change-tab :screen/wallet-stack] 300)) + + (pairing-qr-code? scanned-text) + ;; TODO: https://github.com/status-im/status-mobile/issues/18744 + nil + + (and + (wc-uri/valid-uri? scanned-text) + (ff/enabled? ::ff/wallet.wallet-connect)) + (handle-wallet-connect scanned-text) + + (url? scanned-text) + (debounce/debounce-and-dispatch [:browser.ui/open-url scanned-text] 300) + + :else + (show-invalid-qr-toast))) + +(defn- f-internal-view + [] + (let [{:keys [keyboard-shown]} (hooks/use-keyboard)] + (rn/use-mount + (fn [] + (when keyboard-shown + (rn/dismiss-keyboard!)))) + [scan-qr-code/view + {:title (i18n/label :t/scan-qr) + :share-button? true + :on-success-scan on-qr-code-scanned}])) + +(defn view + [] + [:f> f-internal-view]) diff --git a/src/status_im/contexts/shell/share/events.cljs b/src/status_im/contexts/shell/share/events.cljs new file mode 100644 index 00000000000..40fe18f6898 --- /dev/null +++ b/src/status_im/contexts/shell/share/events.cljs @@ -0,0 +1,16 @@ +(ns status-im.contexts.shell.share.events + (:require + [status-im.common.toasts.events :as toasts] + [utils.re-frame :as rf])) + +(rf/defn copy-text-and-show-toast + {:events [:share/copy-text-and-show-toast]} + [cofx {:keys [text-to-copy post-copy-message]}] + (rf/merge cofx + {:copy-to-clipboard text-to-copy} + (toasts/upsert + {:icon :i/correct + :id :successful-copy-toast-message + :type :positive + :theme :dark + :text post-copy-message}))) diff --git a/src/status_im/contexts/shell/share/profile/view.cljs b/src/status_im/contexts/shell/share/profile/view.cljs new file mode 100644 index 00000000000..aefff9e7be1 --- /dev/null +++ b/src/status_im/contexts/shell/share/profile/view.cljs @@ -0,0 +1,76 @@ +(ns status-im.contexts.shell.share.profile.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.qr-codes.view :as qr-codes] + [status-im.contexts.profile.utils :as profile.utils] + [status-im.contexts.shell.share.style :as style] + [utils.address :as address] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn profile-tab + [] + (let [{:keys [emoji-hash + universal-profile-url] + :as profile} (rf/sub [:profile/profile-with-image]) + {window-width :width} (rn/get-window) + customization-color (rf/sub [:profile/customization-color]) + abbreviated-url (address/get-abbreviated-profile-url + universal-profile-url) + emoji-hash-string (string/join emoji-hash)] + [rn/scroll-view + {:content-container-style {:padding-bottom 16}} + [rn/view {:style style/qr-code-container} + [qr-codes/share-qr-code + {:type :profile + :width (- window-width (* style/screen-padding 2)) + :qr-data universal-profile-url + :qr-data-label-shown abbreviated-url + :on-share-press #(rf/dispatch [:open-share {:options {:message universal-profile-url}}]) + :on-text-press #(rf/dispatch [:share/copy-text-and-show-toast + {:text-to-copy universal-profile-url + :post-copy-message (i18n/label :t/link-to-profile-copied)}]) + :on-text-long-press #(rf/dispatch [:share/copy-text-and-show-toast + {:text-to-copy universal-profile-url + :post-copy-message (i18n/label :t/link-to-profile-copied)}]) + :profile-picture (profile.utils/photo profile) + :full-name (profile.utils/displayed-name profile) + :customization-color customization-color}]] + + [rn/view {:style style/emoji-hash-container} + [rn/view {:style style/emoji-address-container} + [rn/view {:style style/emoji-address-column} + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/emoji-hash-label} + (i18n/label :t/emoji-hash)] + [rn/touchable-highlight + {:active-opacity 1 + :underlay-color colors/neutral-80-opa-1-blur + :background-color :transparent + :on-press #(rf/dispatch [:share/copy-text-and-show-toast + {:text-to-copy emoji-hash-string + :post-copy-message (i18n/label :t/emoji-hash-copied)}]) + :on-long-press #(rf/dispatch [:share/copy-text-and-show-toast + {:text-to-copy emoji-hash-string + :post-copy-message (i18n/label :t/emoji-hash-copied)}])} + [rn/text {:style style/emoji-hash-content} emoji-hash-string]]]] + [rn/view {:style style/emoji-share-button-container} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :link-to-profile + :container-style {:margin-right 12} + :on-press #(rf/dispatch [:share/copy-text-and-show-toast + {:text-to-copy emoji-hash-string + :post-copy-message (i18n/label :t/emoji-hash-copied)}]) + :on-long-press #(rf/dispatch [:share/copy-text-and-show-toast + {:text-to-copy emoji-hash-string + :post-copy-message (i18n/label :t/emoji-hash-copied)}])} + :i/copy]]]])) diff --git a/src/status_im/contexts/shell/share/style.cljs b/src/status_im/contexts/shell/share/style.cljs new file mode 100644 index 00000000000..64d38c7ae1d --- /dev/null +++ b/src/status_im/contexts/shell/share/style.cljs @@ -0,0 +1,67 @@ +(ns status-im.contexts.shell.share.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def screen-padding 20) + +(def header-row + {:flex-direction :row + :justify-content :space-between + :padding-horizontal screen-padding + :margin-vertical 12}) + +(def header-button + {:margin-bottom 12 + :background-color colors/white-opa-5}) + +(def header-heading + {:padding-horizontal screen-padding + :padding-vertical 12 + :color colors/white}) + +(def qr-code-container + {:margin-horizontal 20 + :margin-top 8}) + +(def emoji-hash-container + {:border-radius 16 + :margin-top 12 + :margin-horizontal screen-padding + :background-color colors/white-opa-5 + :flex-direction :row + :justify-content :flex-start + :align-items :flex-start}) + +(def emoji-address-column + {:flex-direction :column}) + +(def emoji-address-container + {:flex-direction :row + :justify-content :flex-start}) + +(def emoji-hash-label + {:color colors/white-opa-40 + :margin-top 8 + :padding-bottom (if platform/ios? 2 0) + :padding-left 12}) + +(def emoji-share-button-container + {:position :absolute + :right 0 + :top 12}) + +(def emoji-hash-content + {:color colors/white + :align-self :flex-start + :padding-top 2 + :padding-bottom 12 + :padding-left 12 + :font-size 12}) + +(def tabs-container + {:padding-horizontal screen-padding + :margin-top 8 + :margin-bottom 16}) + + diff --git a/src/status_im/contexts/shell/share/view.cljs b/src/status_im/contexts/shell/share/view.cljs new file mode 100644 index 00000000000..93df49f49e5 --- /dev/null +++ b/src/status_im/contexts/shell/share/view.cljs @@ -0,0 +1,72 @@ +(ns status-im.contexts.shell.share.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.contexts.shell.share.profile.view :as profile-view] + [status-im.contexts.shell.share.style :as style] + [status-im.contexts.shell.share.wallet.view :as wallet-view] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- header + [] + (let [{:keys [status]} (quo.context/use-screen-params)] + [:<> + [rn/view {:style style/header-row} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :close-shell-share-tab + :container-style style/header-button + :on-press #(rf/dispatch [:navigate-back])} + :i/close] + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :shell-scan-button + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/shell-qr-reader]))} + :i/scan]] + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/header-heading} + (if (= :receive status) + (i18n/label :t/receive) + (i18n/label :t/share))]])) + +(defn- tab-content + [] + (let [{:keys [initial-tab hide-tab-selector?] + :or {initial-tab :profile + hide-tab-selector? false}} (quo.context/use-screen-params) + [selected-tab set-selected-tab] (rn/use-state initial-tab)] + [rn/view {:style {:padding-top safe-area/top}} + [header] + (when-not hide-tab-selector? + [rn/view {:style style/tabs-container} + [quo/segmented-control + {:size 28 + :blur? true + :on-change set-selected-tab + :default-active selected-tab + :data [{:id :profile + :label (i18n/label :t/profile)} + {:id :wallet + :label (i18n/label :t/wallet)}]}]]) + (if (= selected-tab :wallet) + [wallet-view/wallet-tab] + [profile-view/profile-tab])])) + +(defn view + [] + [quo/overlay {:type :shell} + [rn/view {:key :share} + [tab-content]]]) diff --git a/src/status_im/contexts/shell/share/wallet/component_spec.cljs b/src/status_im/contexts/shell/share/wallet/component_spec.cljs new file mode 100644 index 00000000000..28437241885 --- /dev/null +++ b/src/status_im/contexts/shell/share/wallet/component_spec.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.shell.share.wallet.component-spec + (:require + [status-im.contexts.shell.share.wallet.view :as wallet-view] + status-im.contexts.wallet.events + [test-helpers.component :as h])) + +(defn render-wallet-view + [] + (let [component-rendered (h/render [wallet-view/wallet-tab]) + rerender-fn (h/get-rerender-fn component-rendered) + share-qr-code (h/get-by-label-text :share-qr-code)] + ;; Fires on-layout since it's needed to render the content + (h/fire-event :layout share-qr-code #js {:nativeEvent #js {:layout #js {:width 500}}}) + (rerender-fn [wallet-view/wallet-tab]))) + +(h/describe "share wallet addresses" + (h/setup-restorable-re-frame) + (h/before-each + (fn [] + (h/setup-subs {:dimensions/window-width 500 + :mediaserver/port 200 + :wallet/accounts [{:address "0x707f635951193ddafbb40971a0fcaab8a6415160" + :name "Wallet One" + :emoji "😆" + :color :blue}]}))) + + (h/test "should display the wallet tab" + (render-wallet-view) + (h/is-truthy (h/query-by-text "Wallet One")) + (h/is-truthy (h/query-by-text "0x707f635951193ddafbb40971a0fcaab8a6415160")))) diff --git a/src/status_im/contexts/shell/share/wallet/view.cljs b/src/status_im/contexts/shell/share/wallet/view.cljs new file mode 100644 index 00000000000..5bdc0682ab3 --- /dev/null +++ b/src/status_im/contexts/shell/share/wallet/view.cljs @@ -0,0 +1,98 @@ +(ns status-im.contexts.shell.share.wallet.view + (:require + [oops.core :as oops] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [reagent.core :as reagent] + [status-im.contexts.shell.share.style :as style] + [utils.i18n :as i18n] + [utils.image-server :as image-server] + [utils.number] + [utils.re-frame :as rf])) + +(def qr-size 500) + +(defn- share-action + [address share-title] + (rf/dispatch [:open-share + {:options (if platform/ios? + {:activityItemSources [{:placeholderItem {:type :text + :content address} + :item {:default {:type :text + :content + address}} + :linkMetadata {:title share-title}}]} + {:title share-title + :subject share-title + :message address + :isNewTask true})}])) + +(defn- wallet-qr-code-item + [{:keys [account index]}] + (let [{window-width :width} (rn/get-window)] + (fn [] + (let [share-title (str (:name account) " " (i18n/label :t/address)) + qr-url (:address account) + qr-media-server-uri (image-server/get-qr-image-uri-for-any-url + {:url qr-url + :port (rf/sub [:mediaserver/port]) + :qr-size qr-size + :error-level :highest})] + [rn/view {:style {:width window-width :margin-left (if (zero? index) 0 -30)}} + [rn/view {:style style/qr-code-container} + [quo/share-qr-code + {:type :wallet + :width (- window-width (* style/screen-padding 2)) + :qr-image-uri qr-media-server-uri + :qr-data qr-url + :on-share-press #(share-action qr-url share-title) + :profile-picture nil + :full-name (:name account) + :customization-color (:color account) + :emoji (:emoji account)}]]])))) + +(defn render-item + [{:keys [position] :as account}] + [wallet-qr-code-item + {:account account + :index position}]) + +(defn- qr-code-visualized-index + [offset qr-code-size num-qr-codes] + (-> (+ (/ offset qr-code-size) 0.5) + (int) + (utils.number/value-in-range 0 (dec num-qr-codes)))) + +(defn wallet-tab + [] + (let [current-index (reagent/atom 0) + {window-width :width} (rn/get-window) + qr-code-size (- window-width 30)] + (fn [] + (let [accounts (rf/sub [:wallet/accounts]) + num-accounts (count accounts) + on-scroll (rn/use-callback + (fn [e] + (let [offset-x (oops/oget e "nativeEvent.contentOffset.x") + index (qr-code-visualized-index offset-x qr-code-size num-accounts)] + (reset! current-index index))))] + [rn/view + [rn/flat-list + {:horizontal true + :deceleration-rate 0.9 + :snap-to-alignment :start + :snap-to-interval qr-code-size + :disable-interval-momentum true + :scroll-event-throttle 64 + :data accounts + :directional-lock-enabled true + :shows-horizontal-scroll-indicator false + :on-scroll on-scroll + :render-fn render-item}] + (when (> num-accounts 1) + [rn/view {:style {:margin-top 20}} + [quo/slider-bar + {:total-amount num-accounts + :active-index @current-index + :blur? true}]])])))) diff --git a/src/status_im/contexts/shell/shared_values.cljs b/src/status_im/contexts/shell/shared_values.cljs new file mode 100644 index 00000000000..5b163d557b1 --- /dev/null +++ b/src/status_im/contexts/shell/shared_values.cljs @@ -0,0 +1,37 @@ +(ns status-im.contexts.shell.shared-values + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated] + [status-im.contexts.shell.constants :as shell.constants] + [status-im.contexts.shell.state :as state] + [status-im.contexts.shell.utils :as utils] + [utils.worklets.shell :as worklets.shell])) + +(defn stacks-and-bottom-tabs-derived-values + [selected-stack-id unselected-tab-color] + (reduce + (fn [acc id] + (let [tabs-icon-color-keyword (get shell.constants/tabs-icon-color-keywords id) + stack-opacity-keyword (get shell.constants/stacks-opacity-keywords id) + stack-z-index-keyword (get shell.constants/stacks-z-index-keywords id)] + (assoc + acc + stack-opacity-keyword + (worklets.shell/stack-opacity (name id) selected-stack-id) + stack-z-index-keyword + (worklets.shell/stack-z-index (name id) selected-stack-id) + tabs-icon-color-keyword + (worklets.shell/bottom-tab-icon-color + (name id) + selected-stack-id + colors/white + unselected-tab-color)))) + {} + shell.constants/stacks-ids)) + +(defn calculate-and-set-shared-values + [unselected-tab-color] + (let [selected-stack-id (reanimated/use-shared-value (name @state/selected-stack-id-value))] + (utils/reset-bottom-tabs) ;; Reset the state of loaded tabs for faster re-login and hot reloads + (reset! state/selected-stack-id-shared-value selected-stack-id) + (stacks-and-bottom-tabs-derived-values selected-stack-id unselected-tab-color))) diff --git a/src/status_im/contexts/shell/state.cljs b/src/status_im/contexts/shell/state.cljs new file mode 100644 index 00000000000..277b579e29b --- /dev/null +++ b/src/status_im/contexts/shell/state.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.shell.state + (:require + [reagent.core :as reagent] + [status-im.contexts.shell.constants :as shell.constants])) + +;; Atoms +(def selected-stack-id-value (atom shell.constants/default-selected-stack)) +(def selected-stack-id-shared-value (atom nil)) + +;; Reagent atoms used for lazily loading home screen tabs +(def load-communities-stack? (reagent/atom false)) +(def load-chats-stack? (reagent/atom false)) +(def load-wallet-stack? (reagent/atom false)) +(def load-market-stack? (reagent/atom false)) +(def load-browser-stack? (reagent/atom false)) diff --git a/src/status_im/contexts/shell/utils.cljs b/src/status_im/contexts/shell/utils.cljs new file mode 100644 index 00000000000..dfd79ad9445 --- /dev/null +++ b/src/status_im/contexts/shell/utils.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.shell.utils + (:require + [react-native.async-storage :as async-storage] + [react-native.platform :as platform] + [status-im.contexts.shell.constants :as shell.constants] + [status-im.contexts.shell.state :as state])) + +(defn bottom-tabs-container-height + [] + (+ (if platform/android? + shell.constants/bottom-tabs-container-height-android + shell.constants/bottom-tabs-container-height-ios) + shell.constants/home-stack-radius)) + +(defn load-stack + [stack-id] + (case stack-id + :screen/communities-stack (reset! state/load-communities-stack? true) + :screen/chats-stack (reset! state/load-chats-stack? true) + :screen/wallet-stack (reset! state/load-wallet-stack? true) + :screen/market-stack (reset! state/load-market-stack? true) + :screen/browser-stack (reset! state/load-browser-stack? true) + "")) + +(defn change-selected-stack + [stack-id] + (when stack-id + (load-stack stack-id) + (reset! state/selected-stack-id-value stack-id) + (async-storage/set-item! :selected-stack-id stack-id))) + +(defn reset-bottom-tabs + [] + (let [selected-stack-id @state/selected-stack-id-value] + (reset! state/load-communities-stack? (= selected-stack-id :screen/communities-stack)) + (reset! state/load-chats-stack? (= selected-stack-id :screen/chats-stack)) + (reset! state/load-wallet-stack? (= selected-stack-id :screen/wallet-stack)) + (reset! state/load-market-stack? (= selected-stack-id :screen/market-stack)) + (reset! state/load-browser-stack? (= selected-stack-id :screen/browser-stack)))) diff --git a/src/status_im/contexts/shell/view.cljs b/src/status_im/contexts/shell/view.cljs new file mode 100644 index 00000000000..2b04008694f --- /dev/null +++ b/src/status_im/contexts/shell/view.cljs @@ -0,0 +1,45 @@ +(ns status-im.contexts.shell.view + (:require + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.config :as config] + [status-im.contexts.shell.bottom-tabs.view :as bottom-tabs] + [status-im.contexts.shell.home-stack.view :as home-stack] + [status-im.contexts.shell.shared-values :as shared-values] + [status-im.navigation.state :as navigation.state] + [utils.re-frame :as rf])) + +(defn- navigate-back-handler + [] + (when (or (seq @navigation.state/modals) + (> (count (navigation.state/get-navigation-state)) 1)) + (rf/dispatch [:navigate-back]) + true)) + +;; A hidden view-id-tracker view, required for e2e testing +(defn- view-id-tracker + [] + (let [view-id (rf/sub [:view-id])] + [rn/view + {:accessible true + :accessibility-label :view-id-tracker + :style {:width 1 :height 1 :margin-top -1}} + [rn/text {:color :transparent} view-id]])) + +(defn shell-stack + [] + (let [privacy-mode-enabled? (rf/sub [:privacy-mode/privacy-mode-enabled?]) + unselected-tab-color (if privacy-mode-enabled? colors/white-opa-40 colors/neutral-50) + shared-values (shared-values/calculate-and-set-shared-values unselected-tab-color) + background-color (if privacy-mode-enabled? + (colors/resolve-color :purple :dark) + colors/neutral-100)] + (rn/use-mount + (fn [] + (rn/hw-back-add-listener navigate-back-handler) + #(rn/hw-back-remove-listener navigate-back-handler))) + [rn/view {:style {:background-color background-color :flex 1}} + [home-stack/view shared-values] + (when config/enable-view-id-tracker? + [view-id-tracker]) + [bottom-tabs/view shared-values privacy-mode-enabled?]])) diff --git a/src/status_im/contexts/syncing/device/style.cljs b/src/status_im/contexts/syncing/device/style.cljs new file mode 100644 index 00000000000..1aa696bb0ea --- /dev/null +++ b/src/status_im/contexts/syncing/device/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.syncing.device.style + (:require + [quo.foundations.colors :as colors])) + +(def device-container + {:border-color colors/white-opa-5 + :border-radius 16 + :border-width 1 + :margin-top 11}) diff --git a/src/status_im/contexts/syncing/device/view.cljs b/src/status_im/contexts/syncing/device/view.cljs new file mode 100644 index 00000000000..10a65974e6f --- /dev/null +++ b/src/status_im/contexts/syncing/device/view.cljs @@ -0,0 +1,66 @@ +(ns status-im.contexts.syncing.device.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.syncing.device.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- hide-sheet + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn- unpair-confirmation + [installation-id name] + (fn [] + (let [unpair-device #(rf/dispatch [:syncing/disable-installation installation-id])] + [rn/view + [quo/drawer-top + {:title (i18n/label :t/unpair-device) + :description (i18n/label :t/unpair-device-description {:name name})}] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/unpair) + :button-one-props {:type :danger + :on-press unpair-device} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press hide-sheet}}]]))) + +(defn view + [{:keys [name + installation-id + this-device? + device-type + enabled? + show-button?]}] + (let [paired? (and (not this-device?) enabled?) + unpaired? (not enabled?) + show-unpair-confirmation (fn [] + (rf/dispatch [:show-bottom-sheet + {:theme :dark + :content (unpair-confirmation installation-id name)}])) + pair-device (fn [] + (rf/dispatch [:syncing/enable-installation installation-id]))] + [quo/settings-item + (cond-> + {:container-style style/device-container + :title name + :blur? true + :image :icon + :image-props (cond (#{:mobile :ios :android} (keyword device-type)) + :i/mobile + :else :i/desktop)} + (and show-button? unpaired?) (assoc + :action :button + :action-props + {:button-text (i18n/label :t/pair) + :on-press pair-device}) + (and show-button? paired?) (assoc + :action :button + :action-props + {:button-text (i18n/label :t/unpair) + :on-press show-unpair-confirmation}) + this-device? (assoc + :tag :positive + :tag-props {:label (i18n/label :t/this-device)}))])) diff --git a/src/status_im/contexts/syncing/enter_sync_code/style.cljs b/src/status_im/contexts/syncing/enter_sync_code/style.cljs new file mode 100644 index 00000000000..350484ef88e --- /dev/null +++ b/src/status_im/contexts/syncing/enter_sync_code/style.cljs @@ -0,0 +1,59 @@ +(ns status-im.contexts.syncing.enter-sync-code.style + (:require + [quo.foundations.colors :as colors] + [quo.foundations.typography :as typography])) + +(def container-text-input + {:flex-direction :row + :justify-content :space-between + :padding-horizontal 20}) + +(defn text-input-container + [invalid?] + {:padding-left 12 + :padding-right 7 + :min-height 38 + :flex 1 + :flex-direction :row + :border-width 1 + :border-radius 12 + :border-color (if invalid? + colors/danger-50-opa-40 + colors/neutral-60)}) + +(defn text-input + [] + (merge typography/monospace + typography/paragraph-1 + {:flex 1 + :padding-top 6 + :padding-bottom 8 + :color colors/white + :text-align-vertical :top})) + +(def button-paste + {:margin-top 8}) + +(def clear-icon + {:size 20 + :color colors/neutral-80-opa-30}) + +(def right-icon-touchable-area + {:margin-left 8 + :padding-right 4 + :padding-top 6 + :margin-bottom 4}) + +(def label-pairing + {:color colors/white-opa-40}) + +(def label-container + {:flex-direction :row + :margin-left 20 + :line-height 18 + :margin-top 20 + :margin-bottom 8}) + +(def continue-button-container + {:margin-top 12 + :padding-horizontal 22}) diff --git a/src/status_im/contexts/syncing/enter_sync_code/view.cljs b/src/status_im/contexts/syncing/enter_sync_code/view.cljs new file mode 100644 index 00000000000..5160a4058c2 --- /dev/null +++ b/src/status_im/contexts/syncing/enter_sync_code/view.cljs @@ -0,0 +1,76 @@ +(ns status-im.contexts.syncing.enter-sync-code.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.syncing.enter-sync-code.style :as style] + [status-im.contexts.syncing.utils :as sync-utils] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [sync-code-value (reagent/atom "")] + (fn [] + (let [invalid? false + show-paste-button? (string/blank? @sync-code-value) + profile-color (rf/sub [:profile/customization-color])] + [:<> + [rn/view + {:style style/label-container} + [quo/text + {:style style/label-pairing + :weight :medium + :size :paragraph-2} + (i18n/label :t/type-pairing-code)]] + [rn/view {:style style/container-text-input} + [rn/view {:style (style/text-input-container invalid?)} + [rn/text-input + {:style (style/text-input) + :value @sync-code-value + :placeholder (i18n/label :t/scan-sync-code-placeholder) + :on-change-text (fn [scan-code] + (reset! sync-code-value scan-code) + (reagent/flush)) + :blur-on-submit true + :return-key-type :done + :accessibility-label :enter-sync-code-input + :auto-capitalize :none + :placeholder-text-color colors/white-opa-40 + :multiline true}] + (if show-paste-button? + [quo/button + {:on-press (fn [_] + (clipboard/get-string #(reset! sync-code-value %))) + :type :outline + :container-style style/button-paste + :size 24} + (i18n/label :t/paste)] + + [rn/pressable + {:accessibility-label :input-right-icon + :style style/right-icon-touchable-area + :on-press (fn [_] + (reset! sync-code-value nil))} + [quo/icon :i/clear style/clear-icon]])]] + [quo/button + {:type :primary + :disabled? (string/blank? @sync-code-value) + :customization-color profile-color + :container-style style/continue-button-container + :on-press (fn [_] + (if (sync-utils/valid-connection-string? @sync-code-value) + (debounce/debounce-and-dispatch + [:syncing/input-connection-string-for-bootstrapping + @sync-code-value] + 300) + (rf/dispatch [:toasts/upsert + {:type :negative + :theme :dark + :text (i18n/label + :t/error-this-is-not-a-sync-qr-code)}])))} + (i18n/label :t/confirm)]])))) diff --git a/src/status_im/contexts/syncing/events.cljs b/src/status_im/contexts/syncing/events.cljs new file mode 100644 index 00000000000..2d388b5c0b0 --- /dev/null +++ b/src/status_im/contexts/syncing/events.cljs @@ -0,0 +1,166 @@ +(ns status-im.contexts.syncing.events + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [react-native.platform :as platform] + [status-im.constants :as constants] + [status-im.contexts.profile.config :as profile.config] + [status-im.contexts.syncing.utils :as sync-utils] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(rf/defn local-pairing-update-role + {:events [:syncing/update-role]} + [{:keys [db]} role] + {:db (assoc-in db [:syncing :role] role)}) + +(rf/defn local-pairing-clear-states + {:events [:syncing/clear-states]} + [{:keys [db]}] + {:db (dissoc db :syncing)}) + +(defn- input-connection-string-callback + [res] + (log/info "[local-pairing] input-connection-string-for-bootstrapping callback" + {:response res + :event :syncing/input-connection-string-for-bootstrapping}) + (let [response (transforms/json->clj res) + installation-id (:installationId response) + key-uid (:keyUID response) + error (:error response)] + (when (seq installation-id) + (rf/dispatch [:syncing/set-syncing-installation-id installation-id key-uid])) + (when (seq error) + (rf/dispatch [:toasts/upsert + {:type :negative + :text error}])))) + +(rf/defn set-syncing-installation-id + {:events [:syncing/set-syncing-installation-id]} + [{:keys [db]} installation-id key-uid] + {:db (assoc db + :syncing/key-uid key-uid + :syncing/installation-id installation-id)}) + +(defn clear-syncing-installation-id + [{:keys [db]}] + {:db (dissoc + db + :syncing/key-uid + :syncing/installation-id)}) + +(re-frame/reg-event-fx :syncing/clear-syncing-installation-id clear-syncing-installation-id) + +(defn set-syncing-fallback-flow + [{:keys [db]}] + {:db (assoc db :syncing/fallback-flow? true)}) + +(re-frame/reg-event-fx :syncing/set-syncing-fallback-flow set-syncing-fallback-flow) + +(defn clear-syncing-fallback-flow + [{:keys [db]}] + {:db (dissoc db :syncing/fallback-flow?)}) + +(re-frame/reg-event-fx :syncing/clear-syncing-fallback-flow clear-syncing-fallback-flow) + +(rf/defn preflight-outbound-check-for-local-pairing + {:events [:syncing/preflight-outbound-check]} + [_ set-checks-passed] + (let [callback + (fn [raw-response] + (log/info "Local pairing preflight check" + {:response raw-response + :event :syncing/preflight-outbound-check}) + (let [^js response-js (transforms/json->js raw-response) + error (transforms/js->clj (.-error response-js))] + (set-checks-passed (empty? error))))] + (native-module/local-pairing-preflight-outbound-check callback))) + +(rf/defn initiate-local-pairing-with-connection-string + {:events [:syncing/input-connection-string-for-bootstrapping]} + [_ connection-string] + (let [config-map (.stringify js/JSON + (clj->js + {:receiverConfig + {:createAccount (profile.config/create)}}))] + (rf/dispatch [:syncing/update-role constants/local-pairing-role-receiver]) + (native-module/input-connection-string-for-bootstrapping + connection-string + config-map + input-connection-string-callback))) + +(rf/defn preparations-for-connection-string + {:events [:syncing/get-connection-string]} + [{:keys [db]} sha3-pwd on-valid-connection-string] + (let [error (get-in db [:profile/login :error]) + handle-connection (fn [response] + (when (sync-utils/valid-connection-string? response) + (on-valid-connection-string response) + (rf/dispatch [:syncing/update-role constants/local-pairing-role-sender]) + (rf/dispatch [:hide-bottom-sheet])))] + (when-not (and error (string/blank? error)) + (let [key-uid (get-in db [:profile/profile :key-uid]) + config-map (.stringify js/JSON + (clj->js {:senderConfig {:keyUID key-uid + :keystorePath "" + :password (security/safe-unmask-data + sha3-pwd) + :deviceType platform/os} + :serverConfig {:timeout 0}}))] + (native-module/get-connection-string-for-bootstrapping-another-device + config-map + handle-connection))))) + +(rf/reg-event-fx + :syncing/enable-installation + (fn [_ [installation-id]] + {:fx [[:json-rpc/call + [{:method "wakuext_enableInstallation" + :params [installation-id] + :on-success #(rf/dispatch [:syncing/on-enable-installation-success installation-id]) + :on-error #(rf/dispatch [:syncing/on-toggle-installation-failed % installation-id])}]]]})) + +(rf/reg-event-fx + :syncing/disable-installation + (fn [_ [installation-id]] + {:fx [[:json-rpc/call + [{:method "wakuext_disableInstallation" + :params [installation-id] + :on-success #(rf/dispatch [:syncing/on-disable-installation-success installation-id]) + :on-error #(rf/dispatch [:syncing/on-toggle-installation-failed % installation-id])}]]]})) + +(rf/reg-event-fx + :syncing/on-enable-installation-success + (fn [{:keys [db]} [installation-id]] + {:db (assoc-in db + [:pairing/installations installation-id :enabled?] + true) + :fx [[:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/pair-device-toast)}]]]})) + +(rf/reg-event-fx + :syncing/on-disable-installation-success + (fn [{:keys [db]} [installation-id]] + {:db (assoc-in db + [:pairing/installations installation-id :enabled?] + false) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch + [:toasts/upsert + {:type :positive + :theme :dark + :text (i18n/label :t/unpair-device-toast)}]]]})) + +(rf/reg-event-fx + :syncing/on-toggle-installation-failed + (fn [_ [error installation-id]] + (log/error "Failed to toggle installation" + {:error error + :installation-id installation-id}))) diff --git a/src/status_im/contexts/syncing/find_sync_code/view.cljs b/src/status_im/contexts/syncing/find_sync_code/view.cljs new file mode 100644 index 00000000000..780c30b8421 --- /dev/null +++ b/src/status_im/contexts/syncing/find_sync_code/view.cljs @@ -0,0 +1,32 @@ +(ns status-im.contexts.syncing.find-sync-code.view + (:require + [status-im.contexts.syncing.syncing-instructions.view :as syncing-instructions])) + +(defn view + [] + [syncing-instructions/instructions + {:title-label-key :t/find-sync-code + :mobile [{:image {:source :find-sync-code-mobile + :type :image} + :list [[[:text :t/open-status-on-your-other-device]] + [[:text :t/open-your] + [:context-tag + {:label :t/profile + :source :user-picture-male5}]] + [[:text :t/go-to] + [:button-grey-placeholder :t/syncing]] + [[:text :t/tap] + [:button-grey :t/sync-new-device] + [:text :t/and] + [:button-primary :t/set-up-sync]] + [[:text :t/scan-the-qr-code-or-copy-the-sync-code]]]}] + :desktop [{:image {:source :find-sync-code-desktop + :type :image} + :list [[[:text :t/open-status-on-your-other-device]] + [[:text :t/open] + [:button-grey-placeholder :t/settings] + [:text :t/and-go-to] + [:button-grey-placeholder :t/syncing]] + [[:text :t/tap] + [:button-primary :t/set-up-sync]] + [[:text :t/scan-the-qr-code-or-copy-the-sync-code]]]}]}]) diff --git a/src/status_im/contexts/syncing/how_to_pair/view.cljs b/src/status_im/contexts/syncing/how_to_pair/view.cljs new file mode 100644 index 00000000000..4057023c89a --- /dev/null +++ b/src/status_im/contexts/syncing/how_to_pair/view.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.syncing.how-to-pair.view + (:require + [status-im.contexts.syncing.syncing-instructions.view :as syncing-instructions])) + +(defn view + [] + [syncing-instructions/instructions + {:title-label-key :t/how-to-pair + :mobile [{:title :t/signing-in-from-another-device + :image {:source :mobile-how-to-pair-sign-in + :type :image} + :list [[[:text :t/open-status-on-your-other-device]] + [[:text :t/tap-on] + [:button-grey :t/im-new]] + [[:button-primary :t/enable-camera] + [:text :t/or-tap] + [:button-grey :t/enter-sync-code]] + [[:text :t/scan-or-enter-sync-code-seen-on-this-device]]]} + {:title :t/already-logged-in-on-the-other-device + :image {:source :mobile-how-to-pair-logged-in + :type :image} + :list [[[:text :t/tap-on] + [:context-tag + {:label :t/profile + :source :user-picture-male5}] + [:text :t/and] + [:button-grey-placeholder :t/syncing]] + [[:text :t/press] + [:button-grey-placeholder :t/scan-or-enter-sync-code]] + [[:button-primary :t/enable-camera] + [:text :t/or-tap] + [:button-grey :t/enter-sync-code]] + [[:text :t/scan-or-enter-sync-code-seen-on-this-device]]]}] + :desktop [{:title :t/signing-in-from-another-device + :image {:source :desktop-how-to-pair-sign-in + :type :image} + :list [[[:text :t/open-status-on-your-other-device]] + [[:text :t/open] + [:button-grey-placeholder :t/settings] + [:text :t/and-go-to] + [:button-grey-placeholder :t/syncing]] + [[:text :t/tap] + [:button-grey :t/scan-or-enter-a-sync-code]] + [[:text :t/scan-the-qr-code-or-copy-the-sync-code]]]} + {:title :t/already-logged-in-on-the-other-device + :image {:source :desktop-how-to-pair-logged-in + :type :container-image} + :list [[[:text :t/open-status-on-your-other-device]] + [[:text :t/tap-on] + [:button-grey :t/im-new]] + [[:button-primary :t/enable-camera] + [:text :t/or-tap] + [:button-grey :t/enter-sync-code]] + [[:text :t/scan-or-enter-sync-code-seen-on-this-device]]]}]}]) diff --git a/src/status_im/contexts/syncing/scan_sync_code/animation.cljs b/src/status_im/contexts/syncing/scan_sync_code/animation.cljs new file mode 100644 index 00000000000..59020e5d3b6 --- /dev/null +++ b/src/status_im/contexts/syncing/scan_sync_code/animation.cljs @@ -0,0 +1,62 @@ +(ns status-im.contexts.syncing.scan-sync-code.animation + (:require + [react-native.reanimated :as reanimated] + [status-im.constants :as constants])) + +(defn animate-subtitle + [subtitle-opacity] + (reanimated/animate-shared-value-with-delay + subtitle-opacity + 1 + constants/onboarding-modal-animation-duration + :easing4 + (/ constants/onboarding-modal-animation-delay 2))) + +(defn animate-title + [title-opacity] + (reanimated/animate-shared-value-with-delay + title-opacity + 1 + 0 + :easing4 + (+ constants/onboarding-modal-animation-duration + constants/onboarding-modal-animation-delay))) + +(defn animate-bottom + [bottom-view-translate-y] + (reanimated/animate-delay + bottom-view-translate-y + 0 + (+ constants/onboarding-modal-animation-duration + constants/onboarding-modal-animation-delay) + 100)) + +(defn animate-content + [content-opacity] + (reanimated/animate-shared-value-with-delay + content-opacity + 1 + constants/onboarding-modal-animation-duration + :easing4 + (/ constants/onboarding-modal-animation-delay 2))) + +(defn reset-animations + [{:keys [content-opacity subtitle-opacity title-opacity]}] + (reanimated/animate-shared-value-with-timing + content-opacity + 0 + (/ constants/onboarding-modal-animation-duration 8) + :easing4) + + (reanimated/animate-shared-value-with-timing + subtitle-opacity + 0 + (- constants/onboarding-modal-animation-duration + constants/onboarding-modal-animation-delay) + :easing4) + + (reanimated/animate-shared-value-with-timing + title-opacity + 0 + 0 + :easing4)) diff --git a/src/status_im/contexts/syncing/scan_sync_code/style.cljs b/src/status_im/contexts/syncing/scan_sync_code/style.cljs new file mode 100644 index 00000000000..4c24cf8d86d --- /dev/null +++ b/src/status_im/contexts/syncing/scan_sync_code/style.cljs @@ -0,0 +1,163 @@ +(ns status-im.contexts.syncing.scan-sync-code.style + (:require + [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(def screen-padding 20) +(def flash-button-size 32) +(def flash-button-spacing 12) + +(def flex-spacer {:flex 1}) + +(def absolute-fill + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0}) + +(def hole + (merge absolute-fill + {:z-index 2 :opacity 0.95})) + +(defn root-container + [padding-top] + {:z-index 5 + :flex 1 + :padding-top padding-top}) + +(def header-container + {:flex-direction :row + :justify-content :space-between + :padding-horizontal screen-padding + :margin-vertical 12}) + +(def header-text + {:padding-horizontal screen-padding + :padding-top 12 + :padding-bottom 8 + :color colors/white}) + +(def header-sub-text + {:padding-horizontal screen-padding + :color colors/white}) + +(def tabs-container + {:padding-horizontal screen-padding + :margin-top 20}) + +(def scan-qr-code-container + {:margin-top 19}) + +(def qr-view-finder + {:margin-horizontal screen-padding + :height 1 + :display :flex}) + +(defn qr-view-finder-container + [size] + {:width size + :height size + :justify-content :space-between + :margin-left -1 + :margin-top -1}) + +(defn viewfinder-container + [viewfinder] + {:position :absolute + :left (:x viewfinder) + :top 19}) + +(def view-finder-border-container + {:flex-direction :row + :justify-content :space-between}) + +(defn camera-flash-button + [viewfinder] + {:position :absolute + :top (- (+ (:y viewfinder) (:height viewfinder)) flash-button-size flash-button-spacing) + :right (+ screen-padding flash-button-spacing)}) + +(defn- get-border + [border-vertical-width border-horizontal-width corner-radius] + {:border-color colors/white + :width 78 + :height 78 + border-vertical-width 2 + border-horizontal-width 2 + corner-radius 16}) + +(def white-border + (let [base-tip {:background-color colors/white + :position :absolute + :height 1.9 ; 1.9 instead of 2 to fix the tips protruding + :width 1.9 + :border-radius 1}] + {:top-left + {:border (get-border :border-top-width :border-left-width :border-top-left-radius) + :tip-1 (assoc base-tip :right -1 :top 0) + :tip-2 (assoc base-tip :left 0 :bottom -1)} + :top-right + {:border (get-border :border-top-width :border-right-width :border-top-right-radius) + :tip-1 (assoc base-tip :right 0 :bottom -1) + :tip-2 (assoc base-tip :left -1 :top 0)} + :bottom-left + {:border (get-border :border-bottom-width :border-left-width :border-bottom-left-radius) + :tip-1 (assoc base-tip :right -1 :bottom 0) + :tip-2 (assoc base-tip :left 0 :top -1)} + :bottom-right + {:border (get-border :border-bottom-width :border-right-width :border-bottom-right-radius) + :tip-1 (assoc base-tip :right 0 :top -1) + :tip-2 (assoc base-tip :left -1 :bottom 0)}})) + +(def viewfinder-text + {:color colors/white-opa-70 + :text-align :center + :padding-top 16}) + +(def camera-permission-container + {:height 335 + :margin-top 19 + :margin-horizontal screen-padding + :background-color colors/white-opa-5 + :border-color colors/white-opa-10 + :border-width 1 + :border-radius 12 + :border-style :dashed + :align-items :center + :justify-content :center}) + +(def enable-camera-access-header + {:color colors/white}) + +(def enable-camera-access-sub-text + {:color colors/white-opa-70 + :margin-bottom 16}) + +(defn bottom-container + [translate-y padding-bottom] + (reanimated/apply-animations-to-style + {:transform [{:translate-y translate-y}]} + {:z-index 6 + :padding-top 12 + :padding-bottom padding-bottom + :background-color colors/white-opa-5 + :border-top-left-radius 20 + :border-top-right-radius 20 + :align-items :center + :justify-content :center})) + +(def bottom-text + {:color colors/white + :padding-bottom 12}) + +(def camera-style + {:flex 1}) + +(def camera-container + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :border-radius 16}) diff --git a/src/status_im/contexts/syncing/scan_sync_code/view.cljs b/src/status_im/contexts/syncing/scan_sync_code/view.cljs new file mode 100644 index 00000000000..dabc2d2f666 --- /dev/null +++ b/src/status_im/contexts/syncing/scan_sync_code/view.cljs @@ -0,0 +1,398 @@ +(ns status-im.contexts.syncing.scan-sync-code.view + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.camera-kit :as camera-kit] + [react-native.core :as rn] + [react-native.hole-view :as hole-view] + [react-native.permissions :as permissions] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.device-permissions :as device-permissions] + [status-im.constants :as constants] + [status-im.contexts.syncing.enter-sync-code.view :as enter-sync-code] + [status-im.contexts.syncing.scan-sync-code.animation :as animation] + [status-im.contexts.syncing.scan-sync-code.style :as style] + [status-im.contexts.syncing.utils :as sync-utils] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +;; Android allow local network access by default. So, we need this check on iOS only. +(defonce preflight-check-passed? (reagent/atom (if platform/ios? false true))) + +(defonce camera-permission-granted? (reagent/atom false)) +(defonce dismiss-animations (atom nil)) + +(defn perform-preflight-check + "Performing the check for the first time + will trigger local network access permission in iOS. + This permission is required for local pairing + https://github.com/status-im/status-mobile/issues/16135" + [] + (rf/dispatch [:syncing/preflight-outbound-check #(reset! preflight-check-passed? %)])) + +(defn- header + [{:keys [active-tab title title-opacity subtitle-opacity reset-animations-fn animated?]}] + (let [subtitle-translate-x (reanimated/interpolate subtitle-opacity [0 1] [-13 0]) + subtitle-translate-y (reanimated/interpolate subtitle-opacity [0 1] [-85 0]) + subtitle-scale (reanimated/interpolate subtitle-opacity [0 1] [0.9 1]) + controls-translate-y (reanimated/interpolate subtitle-opacity [0 1] [85 0])] + [:<> + [rn/view {:style style/header-container} + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity subtitle-opacity + :transform [{:translate-y controls-translate-y}]} + {})} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :close-sign-in-by-syncing + :on-press (fn [] + (if (and animated? reset-animations-fn) + (reset-animations-fn) + (rf/dispatch [:navigate-back])))} + :i/arrow-left]] + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity subtitle-opacity + :transform [{:translate-y controls-translate-y}]} + {})} + [quo/button + {:icon-left :i/info + :type :grey + :background :blur + :size 32 + :accessibility-label :screen/find-sync-code + :on-press #(rf/dispatch [:open-modal :screen/find-sync-code])} + (i18n/label :t/find-sync-code)]]] + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity title-opacity} + {})} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/header-text} + title]] + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity subtitle-opacity + :transform [{:translate-x subtitle-translate-x} + {:translate-y subtitle-translate-y} + {:scale subtitle-scale}]} + {})} + [quo/text + {:size :paragraph-1 + :weight :regular + :style style/header-sub-text} + (i18n/label :t/synchronise-your-data-across-your-devices)]] + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity subtitle-opacity + :transform [{:translate-y controls-translate-y}]} + style/tabs-container)} + [quo/segmented-control + {:size 32 + :blur? true + :default-active @active-tab + :data [{:id 1 :label (i18n/label :t/scan-sync-qr-code)} + {:id 2 :label (i18n/label :t/enter-sync-code)}] + :on-change (fn [id] + (reset! active-tab id))}]]])) + +(defn get-labels-and-on-press-method + [] + (if @camera-permission-granted? + {:title-label-key :t/enable-access-to-local-network + :description-label-key :t/to-pair-with-your-other-device-in-the-network + :button-icon :i/world + :button-label :t/enable-network-access + :accessibility-label :perform-preflight-check + :on-press perform-preflight-check} + {:title-label-key :t/enable-access-to-camera + :description-label-key :t/to-scan-a-qr-enable-your-camera + :button-icon :i/camera + :button-label :t/enable-camera + :accessibility-label :request-camera-permission + :on-press (fn [] + (device-permissions/camera #(reset! camera-permission-granted? true)))})) + +(defn- camera-and-local-network-access-permission-view + [] + (let [{:keys [title-label-key + description-label-key + button-icon + button-label + accessibility-label + on-press]} (get-labels-and-on-press-method)] + [rn/view {:style style/camera-permission-container} + [quo/text + {:size :paragraph-1 + :weight :medium + :style style/enable-camera-access-header} + (i18n/label title-label-key)] + [quo/text + {:size :paragraph-2 + :weight :regular + :style style/enable-camera-access-sub-text} + (i18n/label description-label-key)] + [quo/button + {:icon-left button-icon + :type :primary + :size 32 + :accessibility-label accessibility-label + :customization-color :blue + :on-press on-press} + (i18n/label button-label)]])) + +(defn- qr-scan-hole-area + [qr-view-finder] + [rn/view + {:style style/qr-view-finder + :on-layout (fn [event] + (let [layout (transforms/js->clj (oops/oget event "nativeEvent.layout")) + view-finder (assoc layout :height (:width layout))] + (reset! qr-view-finder view-finder)))}]) + +(defn- white-border + [corner] + (let [border-styles (style/white-border corner)] + [rn/view + [rn/view {:style (border-styles :border)}] + [rn/view {:style (border-styles :tip-1)}] + [rn/view {:style (border-styles :tip-2)}]])) + +(defn- white-square + [layout-size] + [rn/view {:style (style/qr-view-finder-container layout-size)} + [rn/view {:style style/view-finder-border-container} + [white-border :top-left] + [white-border :top-right]] + [rn/view {:style style/view-finder-border-container} + [white-border :bottom-left] + [white-border :bottom-right]]]) + +(defn- viewfinder + [qr-view-finder] + (let [layout-size (+ (:width qr-view-finder) 2)] + [rn/view {:style (style/viewfinder-container qr-view-finder)} + [white-square layout-size] + [quo/text + {:size :paragraph-2 + :weight :regular + :style style/viewfinder-text} + (i18n/label :t/ensure-both-devices-are-on-the-same-network)]])) + +(defn- scan-qr-code-tab + [qr-view-finder] + (if (and @preflight-check-passed? + @camera-permission-granted? + (boolean (not-empty qr-view-finder))) + [viewfinder qr-view-finder] + [camera-and-local-network-access-permission-view])) + +(defn- f-bottom-view + [insets translate-y] + [rn/pressable + {:on-press #(js/alert "Yet to be implemented")} + [reanimated/view + {:style (style/bottom-container translate-y (:bottom insets))} + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/bottom-text} + (i18n/label :t/i-dont-have-status-on-another-device)]]]) + +(defn- bottom-view + [insets translate-y] + [:f> f-bottom-view insets translate-y]) + +(defn- check-qr-code-and-navigate + [{:keys [event on-success-scan on-failed-scan]}] + (let [connection-string (string/trim (oops/oget event "nativeEvent.codeStringValue")) + valid-connection-string? (sync-utils/valid-connection-string? connection-string)] + ;; debounce-and-dispatch used because the QR code scanner performs callbacks too fast + (if valid-connection-string? + (do + (on-success-scan) + (debounce/debounce-and-dispatch + [:syncing/input-connection-string-for-bootstrapping connection-string] + 300)) + (do + (on-failed-scan) + (debounce/debounce-and-dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text (i18n/label :t/error-this-is-not-a-sync-qr-code)}] + 300))))) + +(defn- render-camera + [{:keys [torch-mode qr-view-finder scan-code? set-qr-code-succeeded set-rescan-timeout]}] + [:<> + [rn/view {:style style/camera-container} + [camera-kit/camera + {:style style/camera-style + :camera-type camera-kit/camera-type-back + :zoom-mode :off + :torch-mode torch-mode + :scan-barcode true + :on-read-code #(when scan-code? + (check-qr-code-and-navigate {:event % + :on-success-scan set-qr-code-succeeded + :on-failed-scan set-rescan-timeout}))}]] + [hole-view/hole-view + {:style style/hole + :holes [(assoc qr-view-finder :borderRadius 16)]} + [quo/blur + {:style style/absolute-fill + :blur-amount 10 + :blur-type :transparent + :overlay-color colors/neutral-80-opa-80 + :background-color colors/neutral-80-opa-80}]]]) + +(defn- reset-animations-and-navigate-fn + [{:keys [render-camera? show-camera?] :as params}] + (letfn [(reset-fn [] + (rf/dispatch [:navigate-back]) + (when @dismiss-animations (@dismiss-animations)) + (animation/reset-animations params))] + (fn [] + (reset! render-camera? false) + (js/setTimeout reset-fn (if show-camera? 500 0))))) + +(defn- set-listener-torch-off-on-app-inactive + [torch-atm] + (let [set-torch-off-fn #(when (not= % "active") (reset! torch-atm false)) + app-state-listener (.addEventListener rn/app-state "change" set-torch-off-fn)] + #(.remove app-state-listener))) + +(defn f-view + [_] + (let [insets safe-area/insets + qr-code-succeed? (reagent/atom false) + active-tab (reagent/atom 1) + qr-view-finder (reagent/atom {}) + render-camera? (reagent/atom false) + torch? (reagent/atom false) + scan-code? (reagent/atom true) + set-rescan-timeout (fn [] + (reset! scan-code? false) + (js/setTimeout (fn [] + (reset! scan-code? true) + (reset! qr-code-succeed? false)) + 3000))] + (fn [{:keys [title show-bottom-view? background animated?]}] + (let [torch-mode (if @torch? :on :off) + flashlight-icon (if @torch? :i/flashlight-on :i/flashlight-off) + scan-qr-code-tab? (= @active-tab 1) + view-id (rf/sub [:view-id]) + show-camera? (and scan-qr-code-tab? + @camera-permission-granted? + @preflight-check-passed? + (some #{view-id} + [:screen/onboarding.sign-in-intro + :screen/onboarding.sign-in + :screen/scan-sync-code-page]) + (boolean (not-empty @qr-view-finder))) + camera-ready-to-scan? (and (or (not animated?) @render-camera?) + show-camera? + (not @qr-code-succeed?)) + title-opacity (reanimated/use-shared-value (if animated? 0 1)) + subtitle-opacity (reanimated/use-shared-value (if animated? 0 1)) + content-opacity (reanimated/use-shared-value (if animated? 0 1)) + content-translate-y (reanimated/interpolate subtitle-opacity [0 1] [85 0]) + bottom-view-translate-y (reanimated/use-shared-value + (if animated? (+ 42.2 (:bottom insets)) 0)) + reset-animations-fn (reset-animations-and-navigate-fn + {:render-camera? render-camera? + :show-camera? show-camera? + :content-opacity content-opacity + :subtitle-opacity subtitle-opacity + :title-opacity title-opacity})] + + (rn/use-mount #(set-listener-torch-off-on-app-inactive torch?)) + + (when animated? + (rn/use-effect + (fn [] + (when (= view-id :screen/onboarding.sign-in-intro) + (rn/hw-back-add-listener reset-animations-fn) + #(rn/hw-back-remove-listener reset-animations-fn))) + [view-id]) + (animation/animate-subtitle subtitle-opacity) + (animation/animate-title title-opacity) + (animation/animate-bottom bottom-view-translate-y)) + + (rn/use-mount + (fn initialize-component [] + (when animated? + (animation/animate-content content-opacity) + (js/setTimeout #(reset! render-camera? true) + (+ constants/onboarding-modal-animation-duration + constants/onboarding-modal-animation-delay + 300))) + (when-not @camera-permission-granted? + (permissions/permission-granted? :camera + #(reset! camera-permission-granted? %) + #(reset! camera-permission-granted? false))))) + [:<> + [rn/view {:style style/absolute-fill} + background] + (when camera-ready-to-scan? + [render-camera + {:torch-mode torch-mode + :qr-view-finder @qr-view-finder + :scan-code? @scan-code? + :set-qr-code-succeeded (fn [] + (reset! qr-code-succeed? true) + (set-rescan-timeout)) + :set-rescan-timeout set-rescan-timeout}]) + [rn/view {:style (style/root-container (:top insets))} + [:f> header + {:active-tab active-tab + :title title + :title-opacity title-opacity + :subtitle-opacity subtitle-opacity + :reset-animations-fn reset-animations-fn + :animated? animated?}] + (when (empty? @qr-view-finder) + [:<> + [rn/view {:style style/scan-qr-code-container}] + [qr-scan-hole-area qr-view-finder]]) + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity content-opacity + :transform [{:translate-y content-translate-y}]} + {})} + (case @active-tab + 1 [scan-qr-code-tab @qr-view-finder] + 2 [enter-sync-code/view] + nil)] + [rn/view {:style style/flex-spacer}] + (when show-bottom-view? + [bottom-view insets bottom-view-translate-y]) + (when (and (or (not animated?) @render-camera?) + show-camera?) + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size style/flash-button-size + :accessibility-label :camera-flash + :container-style (style/camera-flash-button @qr-view-finder) + :on-press #(swap! torch? not)} + flashlight-icon])]])))) + +(defn view + [props] + [:f> f-view props]) diff --git a/src/status_im/contexts/syncing/scan_sync_code_page/style.cljs b/src/status_im/contexts/syncing/scan_sync_code_page/style.cljs new file mode 100644 index 00000000000..c2edb812d4b --- /dev/null +++ b/src/status_im/contexts/syncing/scan_sync_code_page/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.syncing.scan-sync-code-page.style + (:require + [quo.foundations.colors :as colors])) + +(def background + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0 + :background-color colors/neutral-95}) diff --git a/src/status_im/contexts/syncing/scan_sync_code_page/view.cljs b/src/status_im/contexts/syncing/scan_sync_code_page/view.cljs new file mode 100644 index 00000000000..764771e833b --- /dev/null +++ b/src/status_im/contexts/syncing/scan_sync_code_page/view.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.syncing.scan-sync-code-page.view + (:require + [react-native.core :as rn] + [status-im.contexts.syncing.scan-sync-code-page.style :as style] + [status-im.contexts.syncing.scan-sync-code.view :as scan-sync-code] + [utils.i18n :as i18n])) + +(defn view + [] + [scan-sync-code/view + {:title (i18n/label :t/scan-sync-code) + :background [rn/view + {:style style/background} true] + :screen-name "scan-sync-code-page"}]) diff --git a/src/status_im/contexts/syncing/setup_syncing/style.cljs b/src/status_im/contexts/syncing/setup_syncing/style.cljs new file mode 100644 index 00000000000..fa619db9a2e --- /dev/null +++ b/src/status_im/contexts/syncing/setup_syncing/style.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.syncing.setup-syncing.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(defn container-main + [] + {:background-color colors/neutral-95 + :padding-top safe-area/top + :flex 1}) + +(def page-container + {:margin-top 14 + :margin-horizontal 20}) + +(def title-container + {:flex-direction :row + :align-items :center + :justify-content :space-between}) + +(def sync-code + {:margin-top 20}) + +(def standard-auth + {:margin-top 12 + :flex 1}) + +(def qr-container + {:margin-top 12 + :background-color colors/white-opa-5 + :border-radius 20 + :flex 1 + :padding 12}) + +(def sub-text-container + {:margin-bottom 8 + :justify-content :space-between + :align-items :center + :flex-direction :row}) + +(def valid-cs-container + {:flex 1 + :margin 12}) diff --git a/src/status_im/contexts/syncing/setup_syncing/view.cljs b/src/status_im/contexts/syncing/setup_syncing/view.cljs new file mode 100644 index 00000000000..4397f476a45 --- /dev/null +++ b/src/status_im/contexts/syncing/setup_syncing/view.cljs @@ -0,0 +1,134 @@ +(ns status-im.contexts.syncing.setup-syncing.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [reagent.core :as reagent] + [status-im.common.qr-codes.view :as qr-codes] + [status-im.common.resources :as resources] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.syncing.setup-syncing.style :as style] + [status-im.contexts.syncing.utils :as sync-utils] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def code-valid-for-ms 120000) +(def one-min-ms 60000) + +(defn f-use-interval + [clock cleanup-clock delay-ms] + (hooks/use-interval clock cleanup-clock delay-ms) + nil) + +(defn view + [] + (let [{:keys [customization-color]} (rf/sub [:profile/profile-with-image]) + valid-for-ms (reagent/atom code-valid-for-ms) + code (reagent/atom nil) + delay-ms (reagent/atom nil) + timestamp (reagent/atom nil) + set-code (fn [connection-string] + (when (sync-utils/valid-connection-string? connection-string) + (reset! timestamp (* 1000 + (js/Math.ceil (/ (datetime/timestamp) + 1000)))) + (reset! delay-ms 1000) + (reset! code connection-string))) + clock (fn [] + (if (pos? (- code-valid-for-ms + (- (* 1000 + (js/Math.ceil (/ (datetime/timestamp) 1000))) + @timestamp))) + (swap! valid-for-ms (fn [_] + (- code-valid-for-ms + (- (* 1000 + (js/Math.ceil + (/ (datetime/timestamp) 1000))) + @timestamp)))) + (reset! delay-ms nil))) + cleanup-clock (fn [] + (reset! code nil) + (reset! timestamp nil) + (reset! valid-for-ms code-valid-for-ms)) + on-auth-success (fn [entered-password] + (rf/dispatch [:syncing/get-connection-string entered-password + set-code]))] + (fn [] + [rn/view {:style (style/container-main)} + [:f> f-use-interval clock cleanup-clock @delay-ms] + [rn/scroll-view {} + [quo/page-nav + {:type :no-title + :icon-name :i/close + :background :blur + :on-press #(rf/dispatch [:navigate-back]) + :right-side [{:icon-left :i/info + :label (i18n/label :t/how-to-pair) + :on-press #(rf/dispatch [:open-modal :screen/how-to-pair])}]}] + [rn/view {:style style/page-container} + [rn/view {:style style/title-container} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style {:color colors/white}} + (i18n/label :t/setup-syncing)]] + [rn/view {:style style/qr-container} + (if (sync-utils/valid-connection-string? @code) + [qr-codes/qr-code {:url @code}] + [rn/view {:style {:flex-direction :row}} + [rn/image + {:source (resources/get-image :qr-code) + :style {:width "100%" + :background-color colors/white-opa-70 + :border-radius 12 + :aspect-ratio 1}}]]) + (when (sync-utils/valid-connection-string? @code) + [rn/view + {:style style/valid-cs-container} + [rn/view + {:style style/sub-text-container} + [quo/text + {:size :paragraph-2 + :style {:color colors/white-opa-40}} + (i18n/label :t/sync-code)] + [quo/text + {:size :paragraph-2 + :style {:color (if (< @valid-for-ms one-min-ms) + colors/danger-60 + colors/white-opa-40)}} + (i18n/label :t/valid-for-time {:valid-for (datetime/ms-to-duration @valid-for-ms)})]] + [quo/input + {:default-value @code + :type :password + :default-shown? true + :editable false}] + [quo/button + {:on-press (fn [] + (clipboard/set-string @code) + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label + :t/sharing-copied-to-clipboard)}])) + :type :grey + :container-style {:margin-top 12} + :icon-left :i/copy} + (i18n/label :t/copy-qr)]]) + (when-not (sync-utils/valid-connection-string? @code) + [rn/view {:style style/standard-auth} + [standard-auth/slide-auth + {:blur? true + :size :size-40 + :track-text (i18n/label :t/slide-to-reveal-code) + :customization-color customization-color + :on-success on-auth-success + :auth-button-label (i18n/label :t/reveal-sync-code) + :auth-button-icon-left :i/reveal}]])]] + [rn/view {:style style/sync-code} + [quo/divider-label {:tight? false} (i18n/label :t/have-a-sync-code?)] + [quo/action-drawer + [[{:icon :i/scan + :on-press #(rf/dispatch [:open-modal :screen/scan-sync-code-page]) + :label (i18n/label :t/scan-or-enter-sync-code)}]]]]]]))) diff --git a/src/status_im/contexts/syncing/syncing_devices_list/style.cljs b/src/status_im/contexts/syncing/syncing_devices_list/style.cljs new file mode 100644 index 00000000000..590e6981975 --- /dev/null +++ b/src/status_im/contexts/syncing/syncing_devices_list/style.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.syncing.syncing-devices-list.style + (:require + [quo.foundations.colors :as colors])) + +(def page-container + {:flex 1 + :padding-horizontal 20}) + +(def title-container + {:flex-direction :row + :align-items :center + :justify-content :space-between + :margin-top 12 + :margin-bottom 12}) + +(def subtitle + {:margin-top 20 + :color colors/white-opa-40}) diff --git a/src/status_im/contexts/syncing/syncing_devices_list/view.cljs b/src/status_im/contexts/syncing/syncing_devices_list/view.cljs new file mode 100644 index 00000000000..bcf2ec1ec68 --- /dev/null +++ b/src/status_im/contexts/syncing/syncing_devices_list/view.cljs @@ -0,0 +1,93 @@ +(ns status-im.contexts.syncing.syncing-devices-list.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.check-before-syncing.view :as check-before-syncing] + [status-im.contexts.syncing.device.view :as device] + [status-im.contexts.syncing.syncing-devices-list.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn go-back + [] + (rf/dispatch [:navigate-back])) + +(defn open-setup-syncing + [customization-color] + (rf/dispatch + [:show-bottom-sheet + {:theme :dark + :shell? true + :content + (fn [] [check-before-syncing/view + {:customization-color customization-color + :on-submit #(rf/dispatch [:open-modal :screen/settings-setup-syncing])}])}])) + +(defn view + [] + (let [devices (rf/sub [:pairing/installations]) + devices-with-button (map #(assoc % :show-button? true) devices) + user-device (first devices-with-button) + other-devices (rest devices-with-button) + profile-color (rf/sub [:profile/customization-color]) + open-setup-syncing-with-customization-color (rn/use-callback (partial open-setup-syncing + profile-color) + [profile-color]) + {:keys [paired-devices unpaired-devices]} (group-by + #(if (:enabled? %) + :paired-devices + :unpaired-devices) + other-devices) + keycard? (rf/sub [:keycard/keycard-profile?]) + keycard-feature-unavailable (rn/use-callback + #(rf/dispatch + [:keycard/feature-unavailable-show + {:theme :dark + :feature-name :settings.setup-syncing}]))] + [quo/overlay {:type :shell :top-inset? true} + [quo/page-nav + {:type :no-title + :background :blur + :icon-name :i/arrow-left + :on-press go-back}] + [rn/scroll-view + {:content-container-style style/page-container + :style {:flex 1} + :shows-vertical-scroll-indicator false} + [rn/view {:style style/title-container} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style {:color colors/white}} + (i18n/label :t/paired-devices)] + [quo/button + {:size 32 + :type :primary + :customization-color profile-color + :icon-only? true + :on-press (if keycard? + keycard-feature-unavailable + open-setup-syncing-with-customization-color)} + :i/add]] + [device/view (merge user-device {:this-device? true})] + (when (seq paired-devices) + [rn/view + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/subtitle} + (i18n/label :t/paired-with-this-device)] + (for [device-props paired-devices] + ^{:key (:installation-id device-props)} + [device/view device-props])]) + (when (seq unpaired-devices) + [rn/view + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/subtitle} + (i18n/label :t/not-paired-with-this-device)] + (for [device-props unpaired-devices] + ^{:key (:installation-id device-props)} + [device/view device-props])])]])) diff --git a/src/status_im/contexts/syncing/syncing_instructions/style.cljs b/src/status_im/contexts/syncing/syncing_instructions/style.cljs new file mode 100644 index 00000000000..fb47e518c95 --- /dev/null +++ b/src/status_im/contexts/syncing/syncing_instructions/style.cljs @@ -0,0 +1,75 @@ +(ns status-im.contexts.syncing.syncing-instructions.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def container-outer + {:flex (if platform/ios? 4.5 5) + :padding-top 20 + :padding-bottom 8 + :padding-horizontal 16 + :background-color colors/neutral-95}) + +(def heading + {:margin-bottom 12 + :color colors/white}) + +(def tabs-container + {:margin-top 8 + :margin-bottom 12}) + +(def paragraph + {:margin-vertical 8 + :color colors/white}) + +(def hr + {:margin-top 12 + :margin-bottom 6 + :border-bottom-color colors/white-opa-10 + :border-bottom-width 1}) + +(def image + {:border-radius 16}) + +(def container-image + {:flex-direction :row + :aspect-ratio 1 + :justify-content :flex-end + :align-items :flex-end + :overflow :hidden + :background-color colors/white-opa-5 + :border-radius 16}) + +(def list-text + {:margin-horizontal 2 + :color colors/white}) + +(def list-icon + {:height 18 + :width 18 + :border-radius 6 + :border-width 1 + :border-color colors/white-opa-10 + :align-items :center + :justify-content :center + :margin-right 6}) + +(def list-icon-text + {:color colors/white}) + +(def button-grey + {:margin-horizontal 2}) + +(def button-grey-placeholder + {:margin-horizontal 2}) + +(def button-primary + {:margin-horizontal 2}) + +(def numbered-list + {:margin-top 12}) + +(def numbered-list-item + {:flex-direction :row + :align-items :center + :margin-vertical 6}) diff --git a/src/status_im/contexts/syncing/syncing_instructions/view.cljs b/src/status_im/contexts/syncing/syncing_instructions/view.cljs new file mode 100644 index 00000000000..36520de6fb7 --- /dev/null +++ b/src/status_im/contexts/syncing/syncing_instructions/view.cljs @@ -0,0 +1,123 @@ +(ns status-im.contexts.syncing.syncing-instructions.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [reagent.core :as reagent] + [status-im.common.resources :as resources] + [status-im.contexts.syncing.syncing-instructions.style :as style] + [utils.i18n :as i18n])) + +(defn- render-element + [[type value]] + (case type + :text + [quo/text + {:size :paragraph-2 + :weight :regular + :container-style style/list-text} + (i18n/label value)] + + :button-primary + [quo/button + {:type :primary + :customization-color :blue + :size 24 + :container-style style/button-primary} + (i18n/label value)] + + :button-grey + [quo/button + {:type :grey + :size 24 + :container-style style/button-grey} + (i18n/label value)] + + :button-grey-placeholder + [quo/button + {:type :grey + :size 24 + :before :i/placeholder + :container-style style/button-grey-placeholder} + (i18n/label value)] + + :context-tag + [quo/context-tag + {:blur? true + :size 24 + :profile-picture (resources/get-mock-image (:source value)) + :full-name (i18n/label (:label value))}])) + +(defn- render-item + [i list-item] + [rn/view + {:style style/numbered-list-item} + [rn/view + {:style style/list-icon} + [quo/text + {:size :label + :weight :medium + :style style/list-icon-text} i]] + (map-indexed (fn [idx item] + ^{:key idx} + [render-element item]) + list-item)]) + +(defn- render-instruction + [{title :title image :image coll :list}] + [rn/view + (when title + [quo/text + {:size :paragraph-1 + :weight :semi-bold + :style style/paragraph} (i18n/label title)]) + (case (:type image) + :container-image [rn/view {:style style/container-image} + [rn/image + {:source (resources/get-image + (:source image))}]] + :image [rn/image + {:source (resources/get-image (:source image)) + :style style/image}] + nil) + [rn/view {:style style/numbered-list} + (map-indexed (fn [i item] + ^{:key i} + [render-item (inc i) item]) + coll)]]) + +(defn- map-instructions + [idx instructions instructions-count] + [:<> + [render-instruction instructions] + (when-not (= (inc idx) instructions-count) + [rn/view {:style style/hr}])]) + +(defn instructions + [{:keys [title-label-key] :as props}] + (let [platform (reagent/atom :mobile) + platform-data [{:id :mobile + :label (i18n/label :t/mobile)} + {:id :desktop + :label (i18n/label :t/desktop)}]] + (fn [] + (let [sync-instructions (get props @platform) + instructions-count (count sync-instructions)] + [rn/view {:style style/container-outer} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/heading} (i18n/label title-label-key)] + [rn/view {:style style/tabs-container} + [quo/segmented-control + {:size 28 + :blur? true + :default-active :mobile + :data platform-data + :on-change #(reset! platform %)}]] + [gesture/scroll-view + (doall + (map-indexed (fn [idx item] + ^{:key idx} + [map-instructions idx item instructions-count]) + sync-instructions))]])))) diff --git a/src/status_im/contexts/syncing/utils.cljs b/src/status_im/contexts/syncing/utils.cljs new file mode 100644 index 00000000000..6d6171a4328 --- /dev/null +++ b/src/status_im/contexts/syncing/utils.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.syncing.utils + (:require + [clojure.string :as string] + [native-module.core :as native-module])) + +(defn validate-connection-string + [connection-string] + (native-module/validate-connection-string + connection-string)) + +(defn valid-connection-string? + [connection-string] + (some-> connection-string + validate-connection-string + string/blank?)) diff --git a/src/status_im/contexts/wallet/account/db.cljs b/src/status_im/contexts/wallet/account/db.cljs new file mode 100644 index 00000000000..f2d65418d8a --- /dev/null +++ b/src/status_im/contexts/wallet/account/db.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.wallet.account.db) + +(defn get-accounts + [db] + (get-in db [:wallet :accounts])) + +(defn get-accounts-addresses + [db] + (-> db get-accounts keys vec)) diff --git a/src/status_im/contexts/wallet/account/edit_account/style.cljs b/src/status_im/contexts/wallet/account/edit_account/style.cljs new file mode 100644 index 00000000000..2230fdffce4 --- /dev/null +++ b/src/status_im/contexts/wallet/account/edit_account/style.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.wallet.account.edit-account.style) + +(def data-item + {:margin-horizontal 20 + :margin-vertical 8}) diff --git a/src/status_im/contexts/wallet/account/edit_account/view.cljs b/src/status_im/contexts/wallet/account/edit_account/view.cljs new file mode 100644 index 00000000000..ab32da8eb09 --- /dev/null +++ b/src/status_im/contexts/wallet/account/edit_account/view.cljs @@ -0,0 +1,109 @@ +(ns status-im.contexts.wallet.account.edit-account.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.wallet.account.edit-account.style :as style] + [status-im.contexts.wallet.common.screen-base.create-or-edit-account.view + :as create-or-edit-account] + [status-im.contexts.wallet.common.utils :as common.utils] + [status-im.contexts.wallet.sheets.remove-account.view :as remove-account] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- show-save-account-toast + [updated-key] + (let [message (case updated-key + :name :t/edit-wallet-account-name-updated-message + :color :t/edit-wallet-account-colour-updated-message + :emoji :t/edit-wallet-account-emoji-updated-message + (:prod-preferred-chain-ids + :test-preferred-chain-ids) :t/edit-wallet-network-preferences-updated-message + nil)] + (rf/dispatch [:toasts/upsert + {:id :edit-account + :type :positive + :text (i18n/label message)}]))) + +(defn- save-account + [{:keys [account updated-key new-value]}] + (let [edited-account-data (assoc account updated-key new-value)] + (rf/dispatch [:wallet/save-account + {:account edited-account-data + :on-success #(show-save-account-toast updated-key)}]))) + +(defn view + [] + (let [edited-account-name (reagent/atom nil) + name-error (reagent/atom nil) + emoji-color-error (reagent/atom nil) + on-change-color (fn [edited-color {:keys [color] :as account}] + (when (not= edited-color color) + (save-account {:account account + :updated-key :color + :new-value edited-color}))) + on-change-emoji (fn [edited-emoji {:keys [emoji] :as account}] + (when (not= edited-emoji emoji) + (save-account {:account account + :updated-key :emoji + :new-value edited-emoji}))) + on-confirm-name (fn [account] + (rn/dismiss-keyboard!) + (save-account {:account account + :updated-key :name + :new-value @edited-account-name}))] + (fn [] + (let [{:keys [name emoji address color default-account?] + :as account} (rf/sub [:wallet/current-viewing-account]) + other-account-names (rf/sub [:wallet/accounts-names-without-current-account]) + other-emojis-and-colors (rf/sub [:wallet/accounts-emojis-and-colors-without-current-account]) + account-name (or @edited-account-name name) + input-error (or @emoji-color-error @name-error) + button-disabled? (or (string/blank? @edited-account-name) + (= name @edited-account-name) + (some? input-error))] + [create-or-edit-account/view + {:page-nav-right-side [(when-not default-account? + {:icon-name :i/delete + :on-press #(rf/dispatch [:show-bottom-sheet + {:content + (fn [] + [remove-account/view])}])})] + :account-name account-name + :placeholder (i18n/label :t/default-account-placeholder) + :account-emoji emoji + :account-color color + :on-change-name (fn [new-name] + (reset! edited-account-name new-name) + (reset! name-error (common.utils/get-account-name-error + @edited-account-name + other-account-names))) + :on-change-color (fn [new-color] + (if (other-emojis-and-colors [emoji new-color]) + (reset! emoji-color-error :emoji-and-color) + (do + (reset! emoji-color-error nil) + (on-change-color new-color account)))) + :on-change-emoji (fn [new-emoji] + (if (other-emojis-and-colors [new-emoji color]) + (reset! emoji-color-error :emoji-and-color) + (do + (reset! emoji-color-error nil) + (on-change-emoji new-emoji account)))) + :section-label :t/account-info + :bottom-action-label :t/confirm + :bottom-action-props {:customization-color color + :disabled? button-disabled? + :on-press #(on-confirm-name account)} + :error input-error} + [quo/data-item + {:status :default + :size :default + :subtitle-type :default + :blur? false + :card? true + :title (i18n/label :t/address) + :custom-subtitle (fn [] [quo/address-text + {:address address + :format :long}]) + :container-style style/data-item}]])))) diff --git a/src/status_im/contexts/wallet/account/share_address/style.cljs b/src/status_im/contexts/wallet/account/share_address/style.cljs new file mode 100644 index 00000000000..c51e68f913b --- /dev/null +++ b/src/status_im/contexts/wallet/account/share_address/style.cljs @@ -0,0 +1,3 @@ +(ns status-im.contexts.wallet.account.share-address.style) + +(def header-container {:margin-bottom 8}) diff --git a/src/status_im/contexts/wallet/account/share_address/view.cljs b/src/status_im/contexts/wallet/account/share_address/view.cljs new file mode 100644 index 00000000000..ded93637fc0 --- /dev/null +++ b/src/status_im/contexts/wallet/account/share_address/view.cljs @@ -0,0 +1,75 @@ +(ns status-im.contexts.wallet.account.share-address.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.contexts.wallet.account.share-address.style :as style] + [utils.i18n :as i18n] + [utils.image-server :as image-server] + [utils.re-frame :as rf])) + +(def qr-size 500) + +(defn- share-action + [address share-title] + (rf/dispatch [:open-share + {:options (if platform/ios? + {:activityItemSources [{:placeholderItem {:type :text + :content address} + :item {:default {:type :text + :content + address}} + :linkMetadata {:title share-title}}]} + {:title share-title + :subject share-title + :message address + :isNewTask true})}])) + +(defn view + [] + (let [padding-top (:top safe-area/insets)] + (fn [] + (let [{:keys [address color emoji watch-only?] + :as account} (rf/sub [:wallet/current-viewing-account]) + share-title (str (:name account) " " (i18n/label :t/address)) + qr-url address + qr-media-server-uri (image-server/get-qr-image-uri-for-any-url + {:url qr-url + :port (rf/sub [:mediaserver/port]) + :qr-size qr-size + :error-level :highest}) + + {:keys [status]} (quo.context/use-screen-params) + title (case status + :share (i18n/label :t/share-address) + :receive (i18n/label :t/receive) + nil)] + + [quo/overlay {:type :shell} + [rn/view + {:flex 1 + :padding-top padding-top} + [quo/page-nav + {:icon-name :i/close + :on-press #(rf/dispatch [:navigate-back]) + :background :blur + :right-side [{:icon-name :i/scan + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:open-modal :screen/shell-qr-reader]))}] + :accessibility-label :top-bar}] + [quo/page-top + {:title title + :container-style style/header-container}] + [rn/view {:style {:padding-horizontal 20}} + [quo/share-qr-code + {:type (if watch-only? :watched-address :wallet) + :qr-image-uri qr-media-server-uri + :qr-data qr-url + :on-share-press #(share-action qr-url share-title) + :profile-picture nil + :full-name (:name account) + :customization-color color + :emoji emoji}]]]])))) diff --git a/src/status_im/contexts/wallet/account/style.cljs b/src/status_im/contexts/wallet/account/style.cljs new file mode 100644 index 00000000000..b78b17499f7 --- /dev/null +++ b/src/status_im/contexts/wallet/account/style.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.wallet.account.style) + +(def tabs + {:padding-left 20 + :padding-vertical 12}) + +(def cta-buttons + {:padding-horizontal 20 + :padding-bottom 13 + :flex-direction :row}) + +(def account-overview + {:padding-top 60 + :margin-bottom 12 + :padding-horizontal 20}) diff --git a/src/status_im/contexts/wallet/account/tabs/about/style.cljs b/src/status_im/contexts/wallet/account/tabs/about/style.cljs new file mode 100644 index 00000000000..f91e97e9324 --- /dev/null +++ b/src/status_im/contexts/wallet/account/tabs/about/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.wallet.account.tabs.about.style) + +(def about-tab + {:flex 1 + :padding-horizontal 20 + :padding-vertical 8}) diff --git a/src/status_im/contexts/wallet/account/tabs/about/view.cljs b/src/status_im/contexts/wallet/account/tabs/about/view.cljs new file mode 100644 index 00000000000..f25d49d9bb2 --- /dev/null +++ b/src/status_im/contexts/wallet/account/tabs/about/view.cljs @@ -0,0 +1,93 @@ +(ns status-im.contexts.wallet.account.tabs.about.view + (:require + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [status-im.contexts.profile.utils :as profile.utils] + [status-im.contexts.shell.constants :as constants] + [status-im.contexts.wallet.account.tabs.about.style :as style] + [status-im.contexts.wallet.add-account.create-account.utils :as create-account.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn chain-explorer-options + [address networks] + (map (fn [{:keys [chain-id block-explorer-name]}] + {:icon :i/link + :accessibility-label :view-on-block-explorer + :label (i18n/label :t/view-on-block-explorer + {:block-explorer-name block-explorer-name}) + :right-icon :i/external + :on-press #(rf/dispatch + [:wallet/navigate-to-chain-explorer chain-id address])}) + networks)) + +(defn about-options + [] + (let [{:keys [address] :as account} (rf/sub [:wallet/current-viewing-account]) + networks (rf/sub [:wallet/active-networks]) + share-title (str (:name account) " " (i18n/label :t/address))] + [quo/action-drawer + [(concat (chain-explorer-options address networks) + [{:icon :i/copy + :accessibility-label :copy-address + :label (i18n/label :t/copy-address) + :on-press (fn [] + (clipboard/set-string address) + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label :t/address-copied)}]))} + {:icon :i/qr-code + :accessibility-label :show-address-qr + :label (i18n/label :t/show-address-qr) + :on-press #(rf/dispatch [:open-modal :screen/wallet.share-address + {:status :share}])} + {:icon :i/share + :accessibility-label :share-address + :label (i18n/label :t/share-address) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (js/setTimeout + #(rf/dispatch [:wallet/share-account + {:title share-title :content address}]) + 600))}])]])) + +(defn view + [] + (let [{:keys [customization-color] :as profile} (rf/sub [:profile/profile-with-image]) + {:keys [address path watch-only?]} (rf/sub [:wallet/current-viewing-account]) + {keypair-name :name + keypair-type :type + keycards :keycards} (rf/sub [:wallet/current-viewing-account-keypair]) + keypair-keycard? (boolean (seq keycards)) + origin-type (case keypair-type + :seed + :recovery-phrase + + :key + :private-key + + :default-keypair)] + [rn/scroll-view + {:style style/about-tab + :content-container-style {:padding-bottom (+ constants/floating-shell-button-height 8)}} + [quo/data-item + {:subtitle-type :default + :right-icon :i/options + :card? true + :status :default + :size :default + :title (if watch-only? (i18n/label :t/watched-address) (i18n/label :t/address)) + :custom-subtitle (fn [] [quo/address-text + {:address address + :format :long}]) + :container-style {:margin-bottom 12} + :on-press #(rf/dispatch [:show-bottom-sheet {:content about-options}])}] + (when (not watch-only?) + [quo/account-origin + {:type origin-type + :stored (if keypair-keycard? :on-keycard :on-device) + :profile-picture (profile.utils/photo profile) + :customization-color customization-color + :derivation-path (create-account.utils/normalize-path path) + :keypair-name keypair-name}])])) diff --git a/src/status_im/contexts/wallet/account/tabs/assets/style.cljs b/src/status_im/contexts/wallet/account/tabs/assets/style.cljs new file mode 100644 index 00000000000..cf892f320cb --- /dev/null +++ b/src/status_im/contexts/wallet/account/tabs/assets/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.wallet.account.tabs.assets.style + (:require [status-im.contexts.shell.constants :as constants])) + +(def list-container-style + {:padding-horizontal 8 + :padding-bottom constants/floating-shell-button-height}) diff --git a/src/status_im/contexts/wallet/account/tabs/assets/view.cljs b/src/status_im/contexts/wallet/account/tabs/assets/view.cljs new file mode 100644 index 00000000000..23c315690bc --- /dev/null +++ b/src/status_im/contexts/wallet/account/tabs/assets/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.wallet.account.tabs.assets.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.account.tabs.assets.style :as style] + [status-im.contexts.wallet.common.token-value.view :as token-value] + [utils.re-frame :as rf])) + +(defn view + [] + (let [tokens-loading? (rf/sub [:wallet/current-viewing-account-tokens-loading?]) + tokens (rf/sub [:wallet/current-viewing-account-token-values]) + {:keys [watch-only?]} (rf/sub [:wallet/current-viewing-account])] + (if tokens-loading? + [quo/skeleton-list + {:content :assets + :parent-height 560 + :animated? false}] + [rn/flat-list + {:render-fn token-value/view + :style {:flex 1} + :data tokens + :render-data {:watch-only? watch-only?} + :content-container-style style/list-container-style}]))) diff --git a/src/status_im/contexts/wallet/account/tabs/view.cljs b/src/status_im/contexts/wallet/account/tabs/view.cljs new file mode 100644 index 00000000000..1fa083092dd --- /dev/null +++ b/src/status_im/contexts/wallet/account/tabs/view.cljs @@ -0,0 +1,53 @@ +(ns status-im.contexts.wallet.account.tabs.view + (:require + [react-native.core :as rn] + [status-im.contexts.wallet.account.tabs.about.view :as about] + [status-im.contexts.wallet.account.tabs.assets.view :as assets] + [status-im.contexts.wallet.collectible.options.view :as options-drawer] + [status-im.contexts.wallet.common.activity-tab.view :as activity] + [status-im.contexts.wallet.common.collectibles-tab.view :as collectibles] + [status-im.contexts.wallet.common.empty-tab.view :as empty-tab] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def on-collectible-press #(rf/dispatch [:wallet/navigate-to-collectible-details %])) + +(defn- on-collectible-long-press + [{:keys [preview-url collectible-details id]}] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] + [options-drawer/view + {:name (:name collectible-details) + :image (:uri preview-url) + :id id}])}])) + +(defn- on-end-reached + [] + (rf/dispatch [:wallet/request-collectibles-for-current-viewing-account])) + +(defn- collectibles-tab + [] + (let [updating? (rf/sub [:wallet/current-viewing-account-collectibles-updating?]) + collectible-list (rf/sub + [:wallet/current-viewing-account-collectibles-in-filtered-networks]) + current-account-address (rf/sub [:wallet/current-viewing-account-address])] + [collectibles/view + {:loading? updating? + :collectibles collectible-list + :current-account-address current-account-address + :on-end-reached on-end-reached + :on-collectible-press on-collectible-press + :on-collectible-long-press on-collectible-long-press}])) + +(defn view + [{:keys [selected-tab]}] + [rn/view {:style {:flex 1}} + (case selected-tab + :assets [assets/view] + :collectibles [collectibles-tab] + :activity [activity/view] + :permissions [empty-tab/view + {:title (i18n/label :t/no-permissions) + :description (i18n/label :t/no-collectibles-description) + :placeholder? true}] + [about/view])]) diff --git a/src/status_im/contexts/wallet/account/view.cljs b/src/status_im/contexts/wallet/account/view.cljs new file mode 100644 index 00000000000..e37b89bdf9b --- /dev/null +++ b/src/status_im/contexts/wallet/account/view.cljs @@ -0,0 +1,79 @@ +(ns status-im.contexts.wallet.account.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.account.style :as style] + [status-im.contexts.wallet.account.tabs.view :as tabs] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [status-im.feature-flags :as ff] + [status-im.setup.hot-reload :as hot-reload] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def first-tab-id :assets) + +;; NOTE: If the id of the tabs are changed, please check 'wallet/select-account-tab' event as +;; a activity event depends on it +(defn tabs-data + [watch-only?] + [{:id :assets :label (i18n/label :t/assets) :accessibility-label :assets-tab} + {:id :collectibles :label (i18n/label :t/collectibles) :accessibility-label :collectibles-tab} + (when-not watch-only? + {:id :activity :label (i18n/label :t/activity) :accessibility-label :activity-tab}) + {:id :about :label (i18n/label :t/about) :accessibility-label :about}]) + +(defn- change-tab [id] (rf/dispatch [:wallet/select-account-tab id])) + +(defn view + [] + (let [selected-tab (or (rf/sub [:wallet/account-tab]) first-tab-id) + {:keys [name color formatted-balance + watch-only?]} (rf/sub [:wallet/current-viewing-account]) + testnet-mode? (rf/sub [:profile/test-networks-enabled?])] + (hot-reload/use-safe-unmount (fn [] + (rf/dispatch [:wallet/close-account-page]) + (rf/dispatch [:wallet/clean-current-viewing-account]))) + [rn/view {:style {:flex 1}} + [account-switcher/view + {:type :wallet-networks + :show-dapps-button? true + :on-press #(rf/dispatch [:pop-to-root :screen/shell-stack])}] + [quo/account-overview + {:container-style style/account-overview + :current-value formatted-balance + :account-name name + :account (if watch-only? :watched-address :default) + :customization-color color}] + (when (ff/enabled? ::ff/wallet.graph) + [quo/wallet-graph {:time-frame :empty}]) + (when (not watch-only?) + [quo/wallet-ctas + {:container-style style/cta-buttons + :send-action (fn [] + (rf/dispatch [:wallet/clean-send-data]) + (rf/dispatch [:wallet/wizard-navigate-forward + {:start-flow? true + :flow-id :wallet-send-flow}])) + :receive-action #(rf/dispatch [:open-modal :screen/wallet.share-address + {:status :receive}]) + :buy-action #(rf/dispatch [:show-bottom-sheet + {:content buy-token/view}]) + :bridge-action (fn [] + (rf/dispatch [:wallet/clean-send-data]) + (rf/dispatch [:wallet/start-bridge])) + :swap-action (fn [] + (rf/dispatch [:wallet/clean-send-data]) + (rf/dispatch [:wallet/clean-swap]) + (rf/dispatch [:open-modal :screen/wallet.swap-select-asset-to-pay])) + :bridge-disabled? testnet-mode? + :swap-disabled? testnet-mode?}]) + [quo/tabs + {:style style/tabs + :size 32 + :active-tab-id selected-tab + :data (tabs-data watch-only?) + :on-change change-tab + :scrollable? true + :scroll-on-press? true}] + [tabs/view {:selected-tab selected-tab}]])) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/component_spec.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/component_spec.cljs new file mode 100644 index 00000000000..a6463a3a57e --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/component_spec.cljs @@ -0,0 +1,35 @@ +(ns status-im.contexts.wallet.add-account.add-address-to-watch.component-spec + (:require + [status-im.contexts.wallet.add-account.add-address-to-watch.view :as add-address-to-watch] + status-im.contexts.wallet.events + [test-helpers.component :as h])) + +(h/describe "select address for watch only account" + (h/setup-restorable-re-frame) + + (h/before-each + (fn [] + (h/setup-subs {:wallet/scanned-address nil + :wallet/addresses #{"0x12E838Ae1f769147b12956485dc56e57138f3AC8" + "0x22E838Ae1f769147b12956485dc56e57138f3AC8"} + :alert-banners/top-margin 0 + :wallet/watch-address-activity-state nil + :profile/customization-color :blue}))) + + (h/test "validation messages show for already used addressed" + (h/render [add-address-to-watch/view]) + (h/is-falsy (h/query-by-label-text :error-message)) + (h/fire-event :change-text + (h/get-by-label-text :add-address-to-watch) + "0x12E838Ae1f769147b12956485dc56e57138f3AC8") + (-> (h/wait-for #(h/get-by-translation-text :t/address-already-in-use)) + (.then (fn [] + (h/is-truthy (h/get-by-translation-text :t/address-already-in-use)))))) + + (h/test "validation messages show for invalid address" + (h/render [add-address-to-watch/view]) + (h/is-falsy (h/query-by-label-text :error-message)) + (h/fire-event :change-text (h/get-by-label-text :add-address-to-watch) "0x12E838Ae1f769147b") + (-> (h/wait-for #(h/get-by-translation-text :t/invalid-address)) + (.then (fn [] + (h/is-truthy (h/get-by-translation-text :t/invalid-address))))))) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/component_spec.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/component_spec.cljs new file mode 100644 index 00000000000..f9e5c02e16f --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/component_spec.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.component-spec + (:require + [status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.view :as confirm-address] + [test-helpers.component :as h])) + +(h/describe "Add Watch Only Account Page" + (h/setup-restorable-re-frame) + + (h/test "Create Account button is disabled while no account name exists" + (h/setup-subs {:wallet/watch-only-accounts [] + :alert-banners/top-margin 0 + :wallet/accounts-names #{"My account 1" "My account 2"} + :wallet/accounts-emojis-and-colors #{["😊" :sky] ["😶" :army]}}) + (h/render-with-context-provider + [confirm-address/view] + {:theme :light + :screen-params {:address "0xmock-address"}}) + (h/is-truthy (h/get-by-text "0xmock-address")) + (h/is-disabled (h/get-by-label-text :confirm-button-label)))) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/style.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/style.cljs new file mode 100644 index 00000000000..155ba5987c2 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.style) + +(def container + {:flex 1}) + +(def data-item + {:margin-horizontal 20 + :padding-vertical 8 + :padding-horizontal 12}) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/view.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/view.cljs new file mode 100644 index 00000000000..a914c245773 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/view.cljs @@ -0,0 +1,84 @@ +(ns status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.emoji-picker.utils :as emoji-picker.utils] + [status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.style :as style] + [status-im.contexts.wallet.common.screen-base.create-or-edit-account.view :as + create-or-edit-account] + [status-im.contexts.wallet.common.utils :as common.utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [placeholder (i18n/label :t/default-watched-address-placeholder) + account-name (reagent/atom "") + account-color (reagent/atom (rand-nth colors/account-colors)) + account-emoji (reagent/atom (emoji-picker.utils/random-emoji)) + name-error (reagent/atom nil) + emoji-color-error (reagent/atom nil)] + (fn [] + (let [{:keys [address]} (quo.context/use-screen-params) + accounts-names (rf/sub [:wallet/accounts-names]) + accounts-emojis-and-colors (rf/sub [:wallet/accounts-emojis-and-colors]) + on-change-name (fn [new-name] + (reset! account-name new-name) + (reset! name-error (common.utils/get-account-name-error + @account-name + accounts-names))) + on-change-color (fn [new-color] + (reset! account-color new-color) + (reset! emoji-color-error + (when (accounts-emojis-and-colors + [@account-emoji @account-color]) + :emoji-and-color))) + on-change-emoji (fn [new-emoji] + (reset! account-emoji new-emoji) + (reset! emoji-color-error + (when (accounts-emojis-and-colors + [@account-emoji @account-color]) + :emoji-and-color))) + input-error (or @emoji-color-error @name-error)] + [rn/view {:style style/container} + [create-or-edit-account/view + {:placeholder placeholder + :account-name @account-name + :account-emoji @account-emoji + :account-color @account-color + :on-change-name on-change-name + :on-change-color on-change-color + :on-change-emoji on-change-emoji + :watch-only? true + :error input-error + :bottom-action-label :t/add-watched-address + :bottom-action-props {:customization-color @account-color + :disabled? (or (string/blank? @account-name) + (some? input-error)) + :accessibility-label :confirm-button-label + :on-press #(rf/dispatch [:wallet/add-account + {:type :watch + :account-name @account-name + :emoji @account-emoji + :color @account-color} + {:address address + :public-key ""}])}} + [quo/data-item + {:card? true + :emoji @account-emoji + :title (i18n/label :t/watched-address) + :subtitle address + :status :default + :size :default + :subtitle-type :default + :custom-subtitle (fn [] [quo/text + {:size :paragraph-2 + ;; TODO: monospace font + ;; https://github.com/status-im/status-mobile/issues/17009 + :weight :monospace} + address]) + :container-style style/data-item}]]])))) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/events.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/events.cljs new file mode 100644 index 00000000000..2a205026b49 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/events.cljs @@ -0,0 +1,55 @@ +(ns status-im.contexts.wallet.add-account.add-address-to-watch.events + (:require [clojure.string :as string] + [status-im.contexts.wallet.networks.db :as networks.db] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/reg-event-fx + :wallet/ens-not-found + (fn [{:keys [db]} _] + {:db (-> db + (assoc-in [:wallet :ui :add-address-to-watch :activity-state] :invalid-ens) + (assoc-in [:wallet :ui :add-address-to-watch :validated-address] nil))})) + +(rf/reg-event-fx + :wallet/store-address-activity + (fn [{:keys [db]} [address {:keys [hasActivity]}]] + (let [registered-addresses (-> db :wallet :accounts keys set) + address-already-registered? (registered-addresses address)] + (if address-already-registered? + {:db (-> db + (assoc-in [:wallet :ui :add-address-to-watch :activity-state] + :address-already-registered) + (assoc-in [:wallet :ui :add-address-to-watch :validated-address] nil))} + (let [state (if hasActivity :has-activity :no-activity)] + {:db (-> db + (assoc-in [:wallet :ui :add-address-to-watch :activity-state] state) + (assoc-in [:wallet :ui :add-address-to-watch :validated-address] address))}))))) + +(rf/reg-event-fx + :wallet/clear-address-activity + (fn [{:keys [db]}] + {:db (update-in db [:wallet :ui] dissoc :add-address-to-watch)})) + +(rf/reg-event-fx + :wallet/get-address-details + (fn [{db :db} [address-or-ens]] + (let [ens? (string/includes? address-or-ens ".") + chain-id (networks.db/get-chain-id db :mainnet) + request-params [chain-id address-or-ens]] + {:db (-> db + (assoc-in [:wallet :ui :add-address-to-watch :activity-state] :scanning) + (assoc-in [:wallet :ui :add-address-to-watch :validated-address] nil)) + :fx [(if ens? + [:json-rpc/call + [{:method "ens_addressOf" + :params request-params + :on-success [:wallet/get-address-details] + :on-error [:wallet/ens-not-found]}]] + [:json-rpc/call + [{:method "wallet_getAddressDetails" + :params request-params + :on-success [:wallet/store-address-activity address-or-ens] + :on-error #(log/info "failed to get address details" + {:error % + :event :wallet/get-address-details})}]])]}))) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/style.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/style.cljs new file mode 100644 index 00000000000..a97933ce2e1 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/style.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.wallet.add-account.add-address-to-watch.style) + +(def header-container {:padding-bottom 8}) + +(def scan + {:margin-top 26}) + +(def input + {:flex 1 + :margin-right 12}) + +(def input-container + {:flex-direction :row + :margin-horizontal 20}) + +(def info-message + {:margin-top 8 + :margin-left 20}) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/view.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/view.cljs new file mode 100644 index 00000000000..ad798342112 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/view.cljs @@ -0,0 +1,170 @@ +(ns status-im.contexts.wallet.add-account.add-address-to-watch.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.add-account.add-address-to-watch.style :as style] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.common.validation :as validation] + [status-im.subs.wallet.add-account.address-to-watch] + [utils.address :as utils-address] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- validate-address + [known-addresses user-input] + (cond + (or (nil? user-input) (= user-input "")) nil + (contains? known-addresses user-input) (i18n/label :t/address-already-in-use) + (not + (or (utils-address/supported-address? user-input) + (validation/ens-name? user-input))) (i18n/label :t/invalid-address))) + +(defn- address-input + [{:keys [input-value validation-msg validate clear-input]}] + (let [scanned-address (rf/sub [:wallet/scanned-address]) + empty-input? (and (string/blank? @input-value) + (string/blank? scanned-address)) + on-change-text (fn [new-text] + (reset! validation-msg (validate new-text)) + (reset! input-value new-text) + (reagent/flush) + (if (and (not-empty new-text) (nil? (validate new-text))) + (debounce/debounce-and-dispatch [:wallet/get-address-details new-text] + 500) + (rf/dispatch [:wallet/clear-address-activity])) + (when (and scanned-address (not= scanned-address new-text)) + (rf/dispatch [:wallet/clear-address-activity]) + (rf/dispatch [:wallet/clean-scanned-address]))) + paste-on-input #(clipboard/get-string + (fn [clipboard-text] + (-> clipboard-text + utils/on-paste-address-or-ens + on-change-text)))] + (rn/use-effect (fn [] + (when-not (string/blank? scanned-address) + (on-change-text scanned-address))) + [scanned-address]) + [rn/view {:style style/input-container} + [quo/input + {:accessibility-label :add-address-to-watch + :placeholder (i18n/label :t/address-placeholder) + :container-style style/input + :label (i18n/label :t/eth-or-ens) + :auto-capitalize :none + :multiline? true + :on-clear clear-input + :return-key-type :done + :clearable? (not empty-input?) + :on-change-text on-change-text + :button (when empty-input? + {:on-press paste-on-input + :text (i18n/label :t/paste)}) + :value (or scanned-address @input-value)}] + [quo/button + {:type :outline + :on-press (fn [] + (rn/dismiss-keyboard!) + (rf/dispatch [:open-modal :screen/wallet.scan-address])) + :container-style style/scan + :size 40 + :icon-only? true} + :i/scan]])) + +(defn activity-indicator + [activity-state] + (let [{:keys [message] + :as props} (case activity-state + :has-activity {:accessibility-label :account-has-activity + :icon :i/done + :type :success + :message :t/address-activity} + :no-activity {:accessibility-label :account-has-no-activity + :icon :i/info + :type :warning + :message :t/this-address-has-no-activity} + :invalid-ens {:accessibility-label :error-message + :icon :i/info + :type :error + :message :t/invalid-address} + :address-already-registered {:accessibility-label :error-message + :icon :i/info + :type :error + :message :t/address-already-in-use} + {:accessibility-label :searching-for-activity + :icon :i/pending-state + :type :default + :message :t/searching-for-activity})] + (when activity-state + [quo/info-message + (assoc props + :container-style style/info-message + :size :default) + (i18n/label message)]))) + +(defn view + [] + (let [addresses (rf/sub [:wallet/addresses]) + input-value (reagent/atom nil) + validate #(validate-address (set addresses) %) + validation-msg (reagent/atom nil) + clear-input (fn [] + (reset! input-value nil) + (reset! validation-msg nil) + (rf/dispatch [:wallet/clear-address-activity]) + (rf/dispatch [:wallet/clean-scanned-address])) + customization-color (rf/sub [:profile/customization-color])] + (rf/dispatch [:wallet/clean-scanned-address]) + (rf/dispatch [:wallet/clear-address-activity]) + (fn [] + (let [activity-state (rf/sub [:wallet/watch-address-activity-state]) + validated-address (rf/sub [:wallet/watch-address-validated-address])] + [rn/view {:style {:flex 1}} + [floating-button-page/view + {:header [quo/page-nav + {:type :no-title + :icon-name :i/close + :on-press (fn [] + (rf/dispatch [:wallet/clean-scanned-address]) + (rf/dispatch [:wallet/clear-address-activity]) + (rf/dispatch [:navigate-back]))}] + :footer [quo/button + {:customization-color customization-color + :disabled? (or (string/blank? @input-value) + (some? (validate @input-value)) + (= activity-state :invalid-ens) + (= activity-state :scanning) + (not validated-address)) + :on-press (fn [] + (rf/dispatch + [:navigate-to + :screen/wallet.confirm-address-to-watch + {:address + (utils-address/extract-address-without-chains-info + validated-address)}]) + (clear-input)) + :container-style {:z-index 2}} + (i18n/label :t/continue)]} + [quo/page-top + {:container-style style/header-container + :title (i18n/label :t/add-address-to-watch) + :description :text + :description-text (i18n/label :t/enter-eth)}] + [:f> address-input + {:input-value input-value + :validate validate + :validation-msg validation-msg + :clear-input clear-input}] + (if @validation-msg + [quo/info-message + {:accessibility-label :error-message + :size :default + :icon :i/info + :status :error + :container-style style/info-message} + @validation-msg] + [activity-indicator])]])))) diff --git a/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/path_format_sheet/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/path_format_sheet/view.cljs new file mode 100644 index 00000000000..e5a2ffb92cf --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/path_format_sheet/view.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.wallet.add-account.create-account.edit-derivation-path.path-format-sheet.view + (:require + [quo.context] + [quo.core :as quo] + [status-im.constants :as constants] + [utils.i18n :as i18n])) + +(defn view + [] + (let [{:keys [customization-color]} (quo.context/use-screen-params)] + [:<> + [quo/drawer-top {:title (i18n/label :t/path-format)}] + [quo/action-drawer + [[{:accessibility-label :default-ethereum-format + :label (i18n/label :t/default-ethereum-format) + :state :selected + :customization-color customization-color + :sub-label constants/path-wallet-root} + {:accessibility-label :ropsten-testnet + :label (i18n/label :t/ropsten-testnet) + :sub-label constants/path-ropsten-testnet + :customization-color customization-color} + {:accessibility-label :ledger + :label (i18n/label :t/ledger) + :sub-label constants/path-ledger + :customization-color customization-color} + {:accessibility-label :ledger-live + :label (i18n/label :t/ledger-live) + :sub-label constants/path-ledger-live + :customization-color customization-color} + {:accessibility-label :keepkey + :label (i18n/label :t/keep-key) + :sub-label constants/path-keepkey + :customization-color customization-color} + {:icon :i/customize + :accessibility-label :custom + :label (i18n/label :t/custom) + :sub-label (i18n/label :t/type-your-path) + :add-divider? true}]]]])) diff --git a/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/style.cljs new file mode 100644 index 00000000000..d5f6c1c2523 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/style.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.wallet.add-account.create-account.edit-derivation-path.style + (:require [quo.foundations.colors :as colors])) + + +(defn screen + [top] + {:flex 1 + :margin-top top}) + +(def header + {:padding-horizontal 20 + :padding-top 12 + :padding-bottom 8}) + +(def tag + {:padding-horizontal 20 + :flex-direction :row}) + +(def input-container + {:padding-horizontal 20 + :padding-top 20}) + +(defn save-button-container + [bottom] + {:flex 1 + :justify-content :flex-end + :padding-bottom bottom}) + +(def revealed-address-container + {:padding-horizontal 20 + :padding-top 24}) + +(defn revealed-address + [state theme] + {:border-width 1 + :border-color (if (= state :scanning) + (colors/theme-colors colors/neutral-20 colors/neutral-70 theme) + (colors/resolve-color (if (= state :has-activity) :success :warning) theme 40)) + :border-style :dashed + :border-radius 16 + :padding-horizontal 12 + :padding-vertical 7}) + +(def info + {:margin-vertical 9 + :padding-left 2}) + +(defn keyboard + [padding-bottom] + {:padding-bottom padding-bottom}) diff --git a/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/view.cljs new file mode 100644 index 00000000000..480b670c468 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/edit_derivation_path/view.cljs @@ -0,0 +1,146 @@ +(ns status-im.contexts.wallet.add-account.create-account.edit-derivation-path.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.contexts.wallet.add-account.create-account.edit-derivation-path.path-format-sheet.view :as + path-format-sheet] + [status-im.contexts.wallet.add-account.create-account.edit-derivation-path.style :as style] + [status-im.contexts.wallet.common.utils :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn view + "States: + default(filled) + | -> (reveal-action) -> show + | -> (clear-action) -> empty -> (derive-action) -> choose -> (choose-action) -> show" + [{:keys [on-reset]}] + (let [input-focused? (reagent/atom false) + path-value (reagent/atom "") + input-ref (reagent/atom nil) + reset-path-value (fn [_] + (reset! path-value "") + (when on-reset + (on-reset)))] + (fn [] + (let [theme (quo.context/use-theme) + + {:keys [public-key address]} (rf/sub [:profile/profile]) + {:keys [password current-derivation-path]} (quo.context/use-screen-params) + primary-name (first (rf/sub + [:contacts/contact-two-names-by-identity + public-key])) + profile-picture (rf/sub [:profile/image]) + show-path-format-sheet #(rf/dispatch [:show-bottom-sheet + {:content path-format-sheet/view}]) + derivation-path (rf/sub [:wallet/derivation-path]) + state (rf/sub [:wallet/derivation-path-state]) + navigate-back-handler #(if @input-focused? + (do + (.blur ^js @input-ref) + true) + (rf/dispatch [:navigate-to + :screen/wallet.create-account])) + on-change-text #(rf/dispatch + [:wallet/get-derived-addresses + {:password (security/safe-unmask-data + password) + :paths [(string/replace @path-value + #"\s" + "")] + :derived-from address}])] + (rn/use-mount (fn [] + (reset! path-value current-derivation-path) + (rf/dispatch [:wallet/get-derived-addresses + {:password (security/safe-unmask-data password) + :paths [(string/replace @path-value #"\s" "")] + :derived-from address}]) + (rn/hw-back-add-listener navigate-back-handler) + #(rn/hw-back-remove-listener navigate-back-handler))) + [rn/view + {:style (style/screen safe-area/top)} + [quo/page-nav + {:background :blur + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + [rn/view {:style {:padding-bottom 20}} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/header} + (i18n/label :t/edit-derivation-path)] + [rn/view {:style style/tag} + [quo/context-tag + {:size 24 + :profile-picture profile-picture + :style style/tag + :full-name primary-name}]]] + [rn/pressable {:on-press show-path-format-sheet} + [quo/input + {:small? true + :editable false + :placeholder (i18n/label :t/search-assets) + :right-icon {:on-press show-path-format-sheet + :icon-name :i/dropdown + :style-fn (fn [] + {:color (colors/theme-colors colors/neutral-20 + colors/neutral-80 + theme) + :color-2 (colors/theme-colors colors/neutral-100 + colors/white + theme)})} + :label (i18n/label :t/path-format) + :value (i18n/label :t/default-format) + :container-style {:margin-horizontal 20}}]] + [quo/input + {:ref #(reset! input-ref %) + :container-style style/input-container + :value @path-value + :on-focus #(reset! input-focused? true) + :on-blur #(reset! input-focused? false) + :show-soft-input-on-focus false + :editable true + :label (i18n/label :t/derivation-path) + :placeholder (utils/get-formatted-derivation-path 3) + :button {:on-press reset-path-value + :text (i18n/label :t/reset)}}] + [rn/view {:style style/revealed-address-container} + [rn/view {:style (style/revealed-address state theme)} + [quo/text + {:weight :monospace} + (:address derivation-path)]] + [quo/info-message + {:status (case state + :has-activity :success + :no-activity :warning + :default) + :icon (if (= state :scanning) :i/scanning :i/done) + :style style/info} + (i18n/label (case state + :has-activity :t/address-activity + :no-activity :t/address-no-activity + :t/scanning-for-activity))]] + [rn/view {:style (style/save-button-container safe-area/bottom)} + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/save) + :button-one-props {:type :primary + :on-press #(js/alert "Save!") + :disabled? true}}]] + (when @input-focused? + [quo/numbered-keyboard + {:left-action :dot + :delete-key? true + :container-style (style/keyboard safe-area/bottom) + :on-press (fn [value] + (reset! path-value (str @path-value value)) + (on-change-text)) + :on-delete (fn [] + (reset! path-value (subs @path-value 0 (dec (count @path-value)))) + (on-change-text))}])])))) diff --git a/src/status_im/contexts/wallet/add_account/create_account/events.cljs b/src/status_im/contexts/wallet/add_account/create_account/events.cljs new file mode 100644 index 00000000000..c56653b181a --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/events.cljs @@ -0,0 +1,293 @@ +(ns status-im.contexts.wallet.add-account.create-account.events + (:require [camel-snake-kebab.extras :as cske] + [clojure.string :as string] + [status-im.constants :as constants] + [status-im.contexts.wallet.add-account.create-account.utils :as create-account.utils] + [status-im.contexts.wallet.data-store :as data-store] + [taoensso.timbre :as log] + [utils.collection] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(defn get-keypairs-success + [{:keys [db]} [keypairs]] + (let [parsed-keypairs (data-store/rpc->keypairs keypairs) + default-key-uid (->> parsed-keypairs + (some #(when (= (:type %) :profile) %)) + :key-uid) + keycard? (get-in db [:profile/profile :keycard-pairing]) + keypair (first parsed-keypairs) + keycard (when (and keycard? (= (count parsed-keypairs) 1) (empty? (:keycards keypair))) + {:keycard-uid (get-in db [:profile/profile :keycard-instance-uid]) + :key-uid default-key-uid + :keycard-name "" + :accounts-addresses [(some #(when (:wallet %) (:address %)) + (:accounts keypair))]})] + {:db (-> db + (assoc-in [:wallet :keypairs] + (cond-> (utils.collection/index-by :key-uid parsed-keypairs) + keycard + (assoc-in [default-key-uid :keycards] [keycard]))) + (assoc-in [:wallet :ui :create-account :selected-keypair-uid] default-key-uid)) + ;; this is a temporary solution, should be done properly in statu-go instead + :fx [(when keycard + [:json-rpc/call + [{:method "accounts_saveOrUpdateKeycard" + :params [keycard true]}]])]})) +(rf/reg-event-fx :wallet/get-keypairs-success get-keypairs-success) + +(defn confirm-account-origin + [{:keys [db]} [key-uid]] + {:db (assoc-in db [:wallet :ui :create-account :selected-keypair-uid] key-uid) + :fx [[:dispatch [:navigate-back]]]}) + +(rf/reg-event-fx :wallet/confirm-account-origin confirm-account-origin) + +(defn store-new-seed-phrase + [{:keys [db]} [{:keys [seed-phrase]}]] + {:db (update-in db + [:wallet :ui :create-account :new-keypair] + assoc + :seed-phrase + seed-phrase)}) + +(rf/reg-event-fx :wallet/store-new-seed-phrase store-new-seed-phrase) + +(defn seed-phrase-validated + [{:keys [db]} [{:keys [seed-phrase key-uid on-error]}]] + (let [keypair-already-added? (-> db + :wallet + :keypairs + (contains? key-uid))] + (if keypair-already-added? + (do + (on-error) + {:fx [[:dispatch + [:toasts/upsert + {:id :already-existing-keypair + :type :negative + :text "This keypair already exists in the device"}]]]}) + {:db (assoc-in db [:wallet :ui :create-account :new-keypair :seed-phrase] seed-phrase) + :fx [[:dispatch [:navigate-to :screen/wallet.keypair-name {:workflow :recovery-phrase}]]]}))) + +(rf/reg-event-fx :wallet/seed-phrase-validated seed-phrase-validated) + +(defn store-account-generated-with-mnemonic + [{:keys [db]} [{:keys [new-account-data keypair-name]}]] + (let [new-account (update new-account-data :mnemonic security/mask-data)] + {:db (-> db + (update-in [:wallet :ui :create-account :new-keypair] + assoc + :new-account-data new-account + :keypair-name keypair-name) + (update-in [:wallet :ui :create-account :new-keypair] + dissoc + :seed-phrase + :random-phrase)) + :fx [[:dispatch [:navigate-back-to :screen/wallet.create-account]]]})) + +(rf/reg-event-fx :wallet/store-account-generated-with-mnemonic store-account-generated-with-mnemonic) + +(defn generate-account-for-keypair-with-mnemonic + [{:keys [db]} [{:keys [keypair-name]}]] + (let [seed-phrase (-> db :wallet :ui :create-account :new-keypair :seed-phrase)] + {:fx [[:effects.wallet/create-account-from-mnemonic + {:mnemonic-phrase (security/safe-unmask-data seed-phrase) + :paths [constants/path-default-wallet] + :on-success (fn [new-account-data] + (rf/dispatch [:wallet/store-account-generated-with-mnemonic + {:new-account-data new-account-data + :keypair-name keypair-name}]))}]]})) + +(rf/reg-event-fx :wallet/generate-account-for-keypair-with-mnemonic + generate-account-for-keypair-with-mnemonic) + +(defn clear-create-account-data + [{:keys [db]}] + {:db (update-in db [:wallet :ui :create-account] dissoc :new-keypair)}) + +(rf/reg-event-fx :wallet/clear-create-account clear-create-account-data) + +(defn get-derived-addresses + [{:keys [db]} [{:keys [password derived-from paths]}]] + {:db (assoc-in db [:wallet :ui :create-account :derivation-path-state] :scanning) + :json-rpc/call [{:method "wallet_getDerivedAddresses" + :params [password derived-from paths] + :on-success [:wallet/get-derived-addresses-success]}]}) + +(rf/reg-event-fx :wallet/get-derived-addresses get-derived-addresses) + +(rf/reg-event-fx + :wallet/next-derivation-path + (fn [_ [{:keys [on-success keypair-uid]}]] + {:fx [[:json-rpc/call + [{:method "accounts_resolveSuggestedPathForKeypair" + :params [keypair-uid] + :on-success on-success + :on-error (fn [error] + (log/error + "Failed to resolve next derivation path" + {:event :wallet/next-derivation-path + :method "accounts_resolveSuggestedPathForKeypair" + :error error + :params keypair-uid}))}]]]})) + +(defn get-derived-addresses-success + [{:keys [db]} [response]] + (let [derived-address (first response)] + {:db (-> db + (assoc-in [:wallet :ui :create-account :derivation-path-state] + (if (:has-activity derived-address) :has-activity :no-activity)) + (assoc-in [:wallet :ui :create-account :derivation-path] + (cske/transform-keys transforms/->kebab-case-keyword derived-address)))})) + +(rf/reg-event-fx :wallet/get-derived-addresses-success get-derived-addresses-success) + +(rf/reg-event-fx + :wallet/set-private-key + (fn [{:keys [db]} [value]] + {:db (assoc-in db [:wallet :ui :create-account :private-key] (security/mask-data value))})) + +(rf/reg-event-fx + :wallet/set-public-address + (fn [{:keys [db]} [value]] + {:db (assoc-in db [:wallet :ui :create-account :public-address] value)})) + +(rf/reg-event-fx + :wallet/clear-private-key-data + (fn [{:keys [db]} _] + {:db (update-in db [:wallet :ui :create-account] dissoc :private-key :public-address)})) + +(rf/reg-event-fx + :wallet/create-keypair-with-account + (fn [{db :db} [password account-preferences]] + (let [{:keys [workflow + keypair-name + new-account-data]} (-> db :wallet :ui :create-account :new-keypair) + keypair-type (if (= workflow :workflow/new-keypair.import-private-key) + :key + :seed) + keypair-with-account (create-account.utils/prepare-new-account + {:keypair-name keypair-name + :keypair-type keypair-type + :account-data new-account-data + :account-preferences account-preferences}) + new-address (some-> new-account-data + (create-account.utils/first-derived-account keypair-type) + (:address) + (string/lower-case)) + unmasked-password (security/safe-unmask-data password)] + {:fx [[:json-rpc/call + [{:method "accounts_addKeypair" + :params [unmasked-password keypair-with-account] + :on-success [:wallet/add-account-success new-address] + :on-error #(log/error "Failed to add Keypair and create account" %)}]]]}))) + +(defn import-mnemonic-and-create-keypair-with-account + [{db :db} [{:keys [password account-preferences]}]] + (let [account-data (-> db :wallet :ui :create-account :new-keypair :new-account-data) + unmasked-mnemonic (security/safe-unmask-data (:mnemonic account-data)) + unmasked-password (security/safe-unmask-data password)] + {:fx [[:json-rpc/call + [{:method "accounts_importMnemonic" + :params [unmasked-mnemonic unmasked-password] + :on-success #(rf/dispatch + [:wallet/create-keypair-with-account password account-preferences])}]]]})) + +(rf/reg-event-fx :wallet/import-mnemonic-and-create-keypair-with-account + import-mnemonic-and-create-keypair-with-account) + +(rf/reg-event-fx + :wallet/derive-address-and-add-account + (fn [_ [{:keys [password derived-from-address derivation-path account-preferences key-uid]}]] + {:fx [[:json-rpc/call + [{:method "wallet_getDerivedAddresses" + :params [(security/safe-unmask-data password) + derived-from-address + [derivation-path]] + :on-success (fn [[derived-account]] + (rf/dispatch [:wallet/add-account + (assoc account-preferences + :key-uid key-uid + :password password) + derived-account])) + :on-error [:wallet/log-rpc-error + {:event :wallet/derive-address-and-add-account + :params [derived-from-address [derivation-path]]}]}]]]})) + +(defn import-private-key-and-create-keypair-with-account + [{:keys [db]} [{:keys [password account-preferences]}]] + (let [private-key (get-in db + [:wallet :ui :create-account :new-keypair + :new-account-data :private-key]) + unmasked-private-key (security/safe-unmask-data private-key) + unmasked-password (security/safe-unmask-data password)] + {:json-rpc/call + [{:method "accounts_importPrivateKey" + :params [unmasked-private-key unmasked-password] + :on-success [:wallet/create-keypair-with-account password account-preferences] + :on-error #(log/error "Failed to import private key" %)}]})) + +(rf/reg-event-fx + :wallet/import-private-key-and-create-keypair-with-account + import-private-key-and-create-keypair-with-account) + +(defn store-account-generated-with-private-key + [{:keys [db]} [{:keys [new-account-data keypair-name]}]] + {:db (-> db + (update-in [:wallet :ui :create-account :new-keypair] + assoc + :new-account-data new-account-data + :keypair-name keypair-name) + (update-in [:wallet :ui :create-account :new-keypair] + dissoc + :private-key)) + :fx [[:dispatch [:navigate-back-to :screen/wallet.create-account]]]}) + +(rf/reg-event-fx :wallet/store-account-generated-with-private-key + store-account-generated-with-private-key) + +(rf/reg-event-fx + :wallet/import-private-key-and-generate-account-for-keypair + (fn [{:keys [db]} [{:keys [keypair-name]}]] + (let [private-key (get-in db [:wallet :ui :create-account :private-key])] + {:db (assoc-in db + [:wallet :ui :create-account :new-keypair :workflow] + :workflow/new-keypair.import-private-key) + :fx [[:effects.wallet/create-account-from-private-key + {:private-key private-key + :on-success (fn [new-account-data] + (rf/dispatch [:wallet/store-account-generated-with-private-key + {:keypair-name keypair-name + :new-account-data new-account-data}]))}]]}))) + +(rf/reg-event-fx + :wallet/authorize-derive-address + (fn [{:keys [db]} [{:keys [key-uid account-preferences derivation-path]}]] + (let [{:keys [keycards derived-from]} (get-in db [:wallet :keypairs key-uid]) + keycard-keypair? (boolean (seq keycards))] + {:fx [(if keycard-keypair? + [:dispatch + [:standard-auth/authorize-with-keycard + {:on-complete + (fn [pin] + (rf/dispatch + [:keycard/connect-derive-address-and-add-account + {:pin pin + :derived-from-address derived-from + :key-uid key-uid + :derivation-path (create-account.utils/normalize-path derivation-path) + :account-preferences account-preferences}]))}]] + [:dispatch + [:standard-auth/authorize + {:auth-button-label (i18n/label :t/continue) + :on-auth-success (fn [password] + (rf/dispatch + [:wallet/derive-address-and-add-account + {:password password + :derived-from-address derived-from + :key-uid key-uid + :derivation-path derivation-path + :account-preferences account-preferences}]))}]])]}))) diff --git a/src/status_im/contexts/wallet/add_account/create_account/events_test.cljs b/src/status_im/contexts/wallet/add_account/create_account/events_test.cljs new file mode 100644 index 00000000000..70d05a2ae6d --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/events_test.cljs @@ -0,0 +1,76 @@ +(ns status-im.contexts.wallet.add-account.create-account.events-test + (:require + [cljs.test :refer-macros [deftest is]] + [matcher-combinators.test] + [status-im.constants :as constants] + [status-im.contexts.wallet.add-account.create-account.events :as events] + [utils.security.core :as security])) + +(deftest confirm-account-origin-test + (let [db {:wallet {:ui {:create-account {}}}} + props ["key-uid"] + expected-db {:wallet {:ui {:create-account {:selected-keypair-uid "key-uid"}}}} + effects (events/confirm-account-origin {:db db} props) + result-db (:db effects)] + (is (match? result-db expected-db)))) + +(deftest store-account-generated-test + (let [db {:wallet {:ui {:create-account + {:new-keypair {:seed-phrase "test-secret" + :random-phrase "random-test"}}}}} + mnemonic "my mnemonic" + masked-mnemonic (security/mask-data mnemonic) + props [{:new-account-data {"test" "data" + :mnemonic mnemonic} + :keypair-name "new-keypair-name"}] + expected-db {:wallet {:ui {:create-account + {:new-keypair + {:new-account-data {"test" "data" + :mnemonic masked-mnemonic} + :keypair-name "new-keypair-name"}}}}} + effects (events/store-account-generated-with-mnemonic {:db db} props) + result-db (:db effects) + remove-mnemonic #(update-in % + [:wallet :ui :create-account :new-keypair :new-account-data] + dissoc + :mnemonic) + unmask-mnemonic #(-> % + :wallet + :ui + :create-account + :new-keypair + :new-account-data + :mnemonic + security/safe-unmask-data)] + (is (= (remove-mnemonic result-db) (remove-mnemonic expected-db))) + (is (= (unmask-mnemonic result-db) (unmask-mnemonic expected-db))))) + + +(deftest generate-account-for-keypair-test + (let [db {:wallet {:ui {:create-account {:new-keypair {:seed-phrase "test-secret"}}}}} + props [{:keypair-name "test-keypair"}] + expected-effects [[:effects.wallet/create-account-from-mnemonic + {:mnemonic-phrase "test-secret" + :paths [constants/path-default-wallet]}]] + effects (events/generate-account-for-keypair-with-mnemonic {:db db} props)] + (is (match? + (update-in effects [:fx 0 1] dissoc :on-success) + {:fx expected-effects})) + (is (some? (get-in effects [:fx 0 1 :on-success]))))) + +(deftest clear-create-account-data-test + (let [db {:wallet {:ui {:create-account {:new-keypair "test-keypair"}}}} + expected-db {:wallet {:ui {:create-account {}}}} + effects (events/clear-create-account-data {:db db})] + (is (match? (:db effects) expected-db)))) + +(deftest get-derived-addresses-test + (let [db {} + password "test-password" + derived-from "test-derive-from" + paths ["path1"] + event-args [{:password password :derived-from derived-from :paths paths}] + expected-db (assoc-in db [:wallet :ui :create-account :derivation-path-state] :scanning) + effects (events/get-derived-addresses {:db db} event-args) + result-db (:db effects)] + (is (match? result-db expected-db)))) diff --git a/src/status_im/contexts/wallet/add_account/create_account/import_private_key/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/style.cljs new file mode 100644 index 00000000000..432162dc4e7 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/style.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.wallet.add-account.create-account.import-private-key.style + (:require [quo.foundations.colors :as colors])) + +(def input + {:margin-top 12 + :padding-horizontal 20}) + +(def indicator + {:padding-horizontal 20 + :padding-vertical 8}) + +(def info-box + {:margin-bottom 20}) + +(def page-top + {:margin-top 2}) + +(def key-section + {:margin-top 22 + :padding-horizontal 20}) + +(def section-label + {:margin-bottom 8}) + +(defn public-address + [state theme] + (let [border-color (case state + :active-address (colors/resolve-color :success theme 40) + :inactive-address (colors/resolve-color :warning theme 40) + colors/neutral-20)] + {:border-width 1 + :border-color border-color + :border-style :dashed + :border-radius 16 + :padding-vertical 8 + :padding-horizontal 12})) diff --git a/src/status_im/contexts/wallet/add_account/create_account/import_private_key/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/view.cljs new file mode 100644 index 00000000000..1867c8dc226 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/view.cljs @@ -0,0 +1,155 @@ +(ns status-im.contexts.wallet.add-account.create-account.import-private-key.view + (:require + [clojure.string :as string] + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.add-account.create-account.import-private-key.style :as style] + [status-im.contexts.wallet.common.validation :as v] + [status-im.setup.hot-reload :as hot-reload] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- address-input + [{:keys [input-value set-flow-state error?]}] + (let [check-address + (rn/use-callback + (debounce/debounce + (fn [v] + (if (empty? v) + (set-flow-state nil) + ;; TODO check for validation + (if-not (v/private-key? v) + (set-flow-state :invalid-private-key) + ;; TODO add real requests + (do + (set-flow-state :scanning) + ;; TODO get real address + ;; Should be fixed in #18819 + (rf/dispatch [:wallet/set-public-address + "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]) + (js/setTimeout set-flow-state 400 (rand-nth [:active-address :inactive-address])))))) + 500)) + + on-change (rn/use-callback + (fn [v] + (rf/dispatch [:wallet/set-private-key v]) + (check-address v))) + on-paste (rn/use-callback + (fn [] + (clipboard/get-string + (fn [clipboard] + (when-not (empty? clipboard) + (on-change clipboard))))))] + [quo/input + {:accessibility-label :import-private-key + :placeholder (i18n/label :t/enter-private-key-placeholder) + :container-style style/input + :label (i18n/label :t/private-key) + :type :password + :error? error? + :return-key-type :done + :auto-focus true + :on-change-text on-change + :button (when (empty? input-value) + {:on-press on-paste + :text (i18n/label :t/paste)}) + :default-value input-value}])) + +(defn- activity-indicator + [state] + (let [{:keys [message] :as props} + (case state + :scanning + {:status :default + :icon :i/scanning + :message :t/scanning-for-activity} + + :inactive-address + {:status :warning + :icon :i/info + :message :t/this-account-has-no-activity} + + :active-address + {:status :success + :icon :i/done + :message :t/this-address-has-activity} + + :invalid-private-key + {:status :error + :icon :i/info + :message :t/invalid-private-key} + + nil)] + (when props + [quo/info-message + (-> props + (assoc :size :default :container-style style/indicator) + (dissoc :message)) + (i18n/label message)]))) + +(defn on-unmount + [] + (rf/dispatch [:wallet/clear-private-key-data])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn navigate-to-keypair-import + [] + (rf/dispatch [:navigate-to + :screen/wallet.keypair-name + {:workflow :import-private-key}])) + +(defn view + [] + (let [theme (quo.context/use-theme) + customization-color (rf/sub [:profile/customization-color]) + private-key (rf/sub [:wallet/import-private-key]) + public-address (rf/sub [:wallet/public-address]) + [flow-state set-flow-state] (rn/use-state nil) + error? (= :invalid-private-key flow-state)] + (hot-reload/use-safe-unmount on-unmount) + [rn/view {:flex 1} + [floating-button-page/view + {:customization-color customization-color + :header [quo/page-nav + {:background :white + :type :no-title + :icon-name :i/close + :on-press navigate-back}] + :footer [:<> + (when-not (#{:active-address :inactive-address} flow-state) + [quo/information-box + {:type :default + :icon :i/info + :style style/info-box} + (i18n/label :t/import-private-key-info)]) + [quo/button + {:customization-color customization-color + :disabled? (or (string/blank? private-key) error?) + :on-press navigate-to-keypair-import} + (i18n/label :t/continue)]]} + [quo/page-top + {:container-style style/page-top + :title (i18n/label :t/import-private-key) + :description :text + :description-text (i18n/label :t/enter-private-key)}] + [address-input + {:input-value private-key + :set-flow-state set-flow-state + :error? error?}] + (when (#{:scanning :active-address :inactive-address} flow-state) + [rn/view {:style style/key-section} + [quo/section-label + {:section (i18n/label :t/private-key-public-address) + :container-style style/section-label}] + (when (seq public-address) + [rn/view {:style (style/public-address flow-state theme)} + [quo/text public-address]])]) + (when (seq private-key) + [activity-indicator flow-state])]])) diff --git a/src/status_im/contexts/wallet/add_account/create_account/key_pair_name/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/key_pair_name/style.cljs new file mode 100644 index 00000000000..c880a1ee1ec --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/key_pair_name/style.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.wallet.add-account.create-account.key-pair-name.style) + +(def page-top + {:margin-top 2}) + +(def input + {:padding-top 12 + :padding-horizontal 20}) + +(def error-container + {:padding-horizontal 20 + :padding-vertical 8}) diff --git a/src/status_im/contexts/wallet/add_account/create_account/key_pair_name/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/key_pair_name/view.cljs new file mode 100644 index 00000000000..c7de0782788 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/key_pair_name/view.cljs @@ -0,0 +1,98 @@ +(ns status-im.contexts.wallet.add-account.create-account.key-pair-name.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.common.validation.general :as validators] + [status-im.constants :as constants] + [status-im.contexts.wallet.add-account.create-account.key-pair-name.style :as style] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def error-messages + {:too-long (i18n/label :t/key-name-error-length) + :too-short (i18n/label :t/key-name-error-too-short {:count constants/key-pair-name-min-length}) + :emoji (i18n/label :t/key-name-error-emoji) + :special-char (i18n/label :t/key-name-error-special-char)}) + +(defn- navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn- next-workflow-step + [workflow key-pair-name] + (case workflow + :import-private-key + (rf/dispatch [:wallet/import-private-key-and-generate-account-for-keypair + {:keypair-name key-pair-name}]) + + (:new-keypair :recovery-phrase) + (rf/dispatch [:wallet/generate-account-for-keypair-with-mnemonic + {:keypair-name key-pair-name}]) + + (do + (log/error "Unknown workflow" workflow) + (js/alert "Unknown workflow")))) + +(defn view + [] + (let [{:keys [workflow]} (quo.context/use-screen-params) + customization-color (rf/sub [:profile/customization-color]) + [key-pair-name set-key-pair-name] (rn/use-state "") + [error set-error] (rn/use-state nil) + on-change-text (rn/use-callback + (fn [value] + (set-key-pair-name value) + (cond + (> (count value) constants/key-pair-name-max-length) + (set-error :too-long) + + (< 0 (count value) constants/key-pair-name-min-length) + (set-error :too-short) + + (validators/has-emojis? value) + (set-error :emoji) + + (validators/has-special-characters? value) + (set-error :special-char) + + :else (set-error nil)))) + on-continue (rn/use-callback + #(next-workflow-step workflow key-pair-name) + [workflow key-pair-name]) + disabled? (or (some? error) (string/blank? key-pair-name))] + [rn/view {:style {:flex 1}} + [floating-button-page/view + {:customization-color customization-color + :header [quo/page-nav + {:icon-name :i/arrow-left + :on-press navigate-back + :accessibility-label :top-bar}] + :footer [quo/button + {:customization-color customization-color + :disabled? disabled? + :on-press on-continue} + (i18n/label :t/continue)]} + [quo/page-top + {:title (i18n/label :t/keypair-name) + :description-text (i18n/label :t/keypair-name-description) + :description :text + :container-style style/page-top}] + [quo/input + {:container-style style/input + :placeholder (i18n/label :t/keypair-name-input-placeholder) + :label (i18n/label :t/keypair-name) + :char-limit constants/key-pair-name-max-length + :auto-focus true + :on-change-text on-change-text + :error error}] + (when error + [quo/info-message + {:status :error + :size :default + :icon :i/info + :container-style style/error-container} + (get error-messages error)])]])) diff --git a/src/status_im/contexts/wallet/add_account/create_account/new_keypair/confirm_backup/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/new_keypair/confirm_backup/style.cljs new file mode 100644 index 00000000000..b6ebaabf7f6 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/new_keypair/confirm_backup/style.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.wallet.add-account.create-account.new-keypair.confirm-backup.style) + +(def buttons-container + {:padding-horizontal 20 + :padding-vertical 12 + :margin-top :auto}) + +(defn buttons-inner-container + [margin-bottom] + {:flex-direction :row + :margin-bottom margin-bottom}) + +(defn button + [margin-right] + {:flex 0.5 + :margin-right margin-right}) + +(def cheat-description + {:padding-horizontal 20 + :padding-top 8 + :padding-bottom 8}) diff --git a/src/status_im/contexts/wallet/add_account/create_account/new_keypair/confirm_backup/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/new_keypair/confirm_backup/view.cljs new file mode 100644 index 00000000000..5bc21ff27b6 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/new_keypair/confirm_backup/view.cljs @@ -0,0 +1,219 @@ +(ns status-im.contexts.wallet.add-account.create-account.new-keypair.confirm-backup.view + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.common.events-helper :as events-helper] + [status-im.contexts.wallet.add-account.create-account.new-keypair.confirm-backup.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(def secret-words-count 12) + +(def questions-count 4) + +(defn- random-selection + [] + (->> (range secret-words-count) + (shuffle) + (take questions-count) + vec)) + +(defn- random-words-with-string + [words given-string] + (let [random-words (->> words + (remove #(= % given-string)) + (shuffle) + (take (dec questions-count))) + result (shuffle (conj random-words given-string))] + [(subvec result 0 2) (subvec result 2 4)])) + +(defn- cheat-warning + [on-try-again shell?] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top {:title (i18n/label :t/do-not-cheat)}] + [quo/text {:style style/cheat-description} + (i18n/label :t/do-not-cheat-description)] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/see-recovery-phrase-again) + :button-one-props {:customization-color customization-color + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:navigate-back]) + (when on-try-again + (on-try-again)))} + :blur? shell?}]])) + +(defn- button + [{:keys [word margin-right on-press shell?]}] + [quo/button + {:type :grey + :on-press #(on-press word) + :container-style (style/button margin-right) + :background (when shell? :blur)} + word]) + +(defn- buttons-row + [{:keys [margin-bottom options shell?] :as params}] + [rn/view {:style (style/buttons-inner-container margin-bottom)} + [button + (assoc params + :word (first options) + :margin-right 12 + :shell? shell?)] + [button + (assoc params + :word (second options) + :shell? shell?)]]) + +(defn- complete-backup-sheet + [on-success on-try-again shell?] + (let [customization-color (rf/sub [:profile/customization-color]) + [checked? set-checked] (rn/use-state false) + change-checked (rn/use-callback #(set-checked not))] + [:<> + [quo/drawer-top {:title (i18n/label :t/complete-backup)}] + [quo/text + {:style style/cheat-description} + (i18n/label :t/ensure-written-recovery)] + [rn/view + {:style {:flex-direction :row + :padding-horizontal 20 + :padding-top 8 + :padding-bottom 12 + :column-gap 8}} + [quo/selectors + {:type :checkbox + :checked? checked? + :customization-color customization-color + :on-change change-checked + :blur? shell?}] + [rn/pressable + {:style {:flex 1} + :on-press change-checked} + [quo/text (i18n/label :t/written-seed-ready)]]] + + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/done) + :button-one-props {:disabled? (not checked?) + :customization-color customization-color + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (on-success))} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (when on-try-again + (rf/dispatch [:navigate-back]) + (on-try-again)))} + :blur? shell?}]])) + +(defn- view-internal + [{:keys [on-success on-try-again masked-seed-phrase back-button? theme shell?]}] + (let [random-indices (random-selection) + quiz-index (reagent/atom 0) + incorrect-count (reagent/atom 0) + show-error? (reagent/atom false) + random-phrase (reagent/atom [])] + (fn [] + (rn/use-mount + (fn [] + (native-module/get-random-mnemonic #(reset! random-phrase (string/split % #"\s"))))) + (when-not (empty? @random-phrase) + (let [unmasked-seed-phrase (security/safe-unmask-data masked-seed-phrase) + current-word-index (get random-indices + (min @quiz-index (dec questions-count))) + current-word (get unmasked-seed-phrase current-word-index) + [options-row-0 options-row-1] (random-words-with-string @random-phrase current-word) + on-button-press (fn [word] + (if (= word current-word) + (do + (when (< @quiz-index questions-count) + (reset! quiz-index (inc @quiz-index))) + (reset! incorrect-count 0) + (reset! show-error? false) + (when (and on-success (= @quiz-index questions-count)) + (rf/dispatch [:show-bottom-sheet + {:theme (if (or shell? shell?) + :dark + theme) + :hide-on-background-press? false + :shell? shell? + :content (fn [] [complete-backup-sheet + on-success + on-try-again + shell?])}]))) + (do + (when (> @incorrect-count 0) + (rf/dispatch [:show-bottom-sheet + {:theme (if shell? + :dark + theme) + :shell? shell? + :content (fn [] + [cheat-warning + on-try-again + shell?])}])) + (reset! incorrect-count (inc @incorrect-count)) + (reset! show-error? true))))] + + [quo/overlay + {:type (when shell? :shell) + :insets? true} + [quo/page-nav + (cond-> {:icon-name (if back-button? :i/arrow-left :i/close) + :on-press events-helper/navigate-back + :accessibility-label :top-bar} + shell? (assoc :background :blur))] + [quo/page-top + {:title (i18n/label :t/confirm-backup) + :description :text + :description-text (i18n/label :t/confirm-the-position)}] + [rn/flat-list + {:data random-indices + :render-fn (fn [num index] + [quo/quiz-item + {:state (cond + (and (= @quiz-index index) + (pos? @incorrect-count) + @show-error?) + :error + + (= @quiz-index index) + :empty + + (> @quiz-index index) + :success + + :else + :disabled) + :word (get unmasked-seed-phrase num) + :number (inc num) + :on-press #(when (= @quiz-index index) + (reset! show-error? false)) + :blur? shell?}]) + :separator [rn/view {:style {:height 8}}] + :content-container-style {:padding-horizontal 20}}] + [rn/view {:style style/buttons-container} + [buttons-row + {:on-press on-button-press + :margin-bottom 12 + :options options-row-0 + :shell? shell?}] + [buttons-row + {:on-press on-button-press + :options options-row-1 + :shell? shell?}]]]))))) + +(defn view + [] + (let [screen-params (quo.context/use-screen-params)] + [view-internal screen-params])) diff --git a/src/status_im/contexts/wallet/add_account/create_account/select_keypair/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/style.cljs new file mode 100644 index 00000000000..739201f3f91 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.wallet.add-account.create-account.select-keypair.style) + +(def header-container + {:margin-bottom 8}) + +(def bottom-action-container + {:position :absolute + :bottom 0 + :left 0 + :right 0}) diff --git a/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs new file mode 100644 index 00000000000..af61e5255a6 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs @@ -0,0 +1,123 @@ +(ns status-im.contexts.wallet.add-account.create-account.select-keypair.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.wallet.add-account.create-account.select-keypair.style :as style] + [status-im.feature-flags :as ff] + [utils.address :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- backup-recovery-phrase-success + [masked-seed-phrase] + (rf/dispatch [:navigate-to :screen/confirm-backup + {:masked-seed-phrase masked-seed-phrase + :on-success (fn [] + (rf/dispatch [:wallet/store-new-seed-phrase + {:seed-phrase + masked-seed-phrase}]) + (rf/dispatch [:navigate-to :screen/wallet.keypair-name + {:workflow :new-keypair}]))}])) + +(defn- keypair-options + [] + [quo/action-drawer + [[{:icon :i/add + :accessibility-label :generate-new-keypair + :label (i18n/label :t/generate-new-keypair) + :on-press #(rf/dispatch [:navigate-to :screen/backup-recovery-phrase + {:on-success backup-recovery-phrase-success}])} + {:icon :i/seed + :accessibility-label :import-using-phrase + :label (i18n/label :t/import-using-phrase) + :add-divider? true + :on-press (fn [] + (rf/dispatch [:navigate-to :screen/use-recovery-phrase + {:on-success #(rf/dispatch [:wallet/seed-phrase-validated + %])}]))} + (when (ff/enabled? ::ff/wallet.import-private-key) + {:icon :i/key + :accessibility-label :import-private-key + :label (i18n/label :t/import-private-key) + :on-press #(rf/dispatch [:navigate-to :screen/wallet.import-private-key])})]]]) + +(defn- parse-accounts + [given-accounts] + (->> given-accounts + (filter (fn [{:keys [path]}] + (not (string/starts-with? path constants/path-eip1581)))) + (map (fn [{:keys [color emoji name address]}] + {:account-props {:customization-color color + :size 32 + :emoji emoji + :type :default + :name name + :address address} + :state :default + :action :none})))) + +(defn- keypair + [item _ _ + {:keys [profile-picture compressed-key selected-key-uid set-selected-key-uid customization-color]}] + (let [profile-keypair? (= (:type item) :profile) + keycard? (boolean (seq (:keycards item))) + accounts (parse-accounts (:accounts item))] + [quo/keypair + {:customization-color customization-color + :profile-picture (when profile-keypair? profile-picture) + :status-indicator false + :type (if profile-keypair? :default-keypair :other) + :stored (if keycard? :on-keycard :on-device) + :on-options-press #(js/alert "Options pressed") + :action :selector + :blur? false + :details {:full-name (:name item) + :address (when profile-keypair? + (utils/get-shortened-compressed-key compressed-key))} + :on-press #(set-selected-key-uid (:key-uid item)) + :accounts accounts + :selected? (= selected-key-uid (:key-uid item)) + :container-style {:margin-horizontal 20 + :margin-vertical 8}}])) +(defn view + [] + (let [compressed-key (rf/sub [:profile/compressed-key]) + customization-color (rf/sub [:profile/customization-color]) + keypairs (rf/sub [:wallet/fully-operable-keypairs-list]) + selected-keypair (rf/sub [:wallet/selected-keypair-uid]) + profile-picture (rf/sub [:profile/image]) + [selected-key-uid set-selected-key-uid] (rn/use-state selected-keypair)] + [rn/view {:style {:flex 1}} + [quo/page-nav + {:icon-name :i/close + :on-press #(rf/dispatch [:navigate-back]) + :accessibility-label :top-bar}] + [quo/page-top + {:container-style style/header-container + :title (i18n/label :t/keypairs) + :title-right :action + :title-right-props {:icon :i/add + :customization-color customization-color + :on-press #(rf/dispatch + [:show-bottom-sheet {:content keypair-options}])} + :description :text + :description-text (i18n/label :t/keypairs-description)}] + [rn/flat-list + {:data keypairs + :render-fn keypair + :render-data {:profile-picture profile-picture + :compressed-key compressed-key + :selected-key-uid selected-key-uid + :set-selected-key-uid set-selected-key-uid + :customization-color customization-color} + :initial-num-to-render 1 + :content-container-style {:padding-bottom 60}}] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/confirm-account-origin) + :button-one-props {:customization-color customization-color + :on-press #(rf/dispatch [:wallet/confirm-account-origin + selected-key-uid])} + :container-style style/bottom-action-container}]])) diff --git a/src/status_im/contexts/wallet/add_account/create_account/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/style.cljs new file mode 100644 index 00000000000..2784cff0df4 --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/style.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.wallet.add-account.create-account.style + (:require + [quo.foundations.colors :as colors])) + +(def account-avatar-container + {:padding-horizontal 20 + :padding-top 12}) + +(def reaction-button-container + {:position :absolute + :bottom 0 + :left 80}) + +(defn title-input-container + [error?] + {:padding-horizontal 20 + :padding-top 12 + :padding-bottom (if error? 8 16)}) + +(def color-picker-container + {:padding-vertical 12}) + +(defn color-label + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme) + :padding-bottom 4 + :padding-horizontal 20}) + +(def slide-button-container {:z-index 1}) diff --git a/src/status_im/contexts/wallet/add_account/create_account/utils.cljs b/src/status_im/contexts/wallet/add_account/create_account/utils.cljs new file mode 100644 index 00000000000..d9746a0432c --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/utils.cljs @@ -0,0 +1,69 @@ +(ns status-im.contexts.wallet.add-account.create-account.utils + (:require [clojure.string :as string] + [status-im.constants :as constants])) + +(defn first-derived-account + [account-data keypair-type] + (if (= keypair-type :seed) + (some-> account-data + :derived + first + val) + account-data)) + +(defn get-account-details + [keypair-type account account-to-create] + (if (= keypair-type :key) + {:key-uid (:key-uid account) + :public-key (:public-key account) + :account-address (:address account)} + {:key-uid (:keyUid account) + :public-key (:publicKey account-to-create) + :account-address (:address account-to-create)})) + +(defn get-account-config + [{:keys [account-address key-uid keypair-type public-key account-name emoji color]}] + {:address account-address + :key-uid key-uid + :wallet false + :chat false + :type keypair-type + :path constants/path-default-wallet + :public-key public-key + :name account-name + :emoji emoji + :colorID color + :hidden false}) + +(defn prepare-new-account + [{:keys [keypair-name keypair-type] + {:keys [address] :as account} :account-data + {:keys [account-name color emoji]} :account-preferences}] + (let [account-to-create (first-derived-account account keypair-type) + {:keys [key-uid public-key account-address]} (get-account-details keypair-type + account + account-to-create) + account-config (get-account-config {:account-address + account-address + :key-uid key-uid + :keypair-type keypair-type + :public-key public-key + :account-name account-name + :emoji emoji + :color color})] + {:key-uid key-uid + :name keypair-name + :type keypair-type + :derived-from address + :last-used-derivation-index 0 + :accounts [account-config]})) + +(defn legacy-path? + [s] + (re-matches #"m/\d+" s)) + +(defn normalize-path + [path] + (if (legacy-path? path) + (str constants/path-wallet-root "/" (last (string/split path "/"))) + path)) diff --git a/src/status_im/contexts/wallet/add_account/create_account/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/view.cljs new file mode 100644 index 00000000000..520922ea7fe --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/view.cljs @@ -0,0 +1,340 @@ +(ns status-im.contexts.wallet.add-account.create-account.view + (:require + [clojure.string :as string] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.emoji-picker.utils :as emoji-picker.utils] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.constants :as constants] + [status-im.contexts.wallet.add-account.create-account.style :as style] + [status-im.contexts.wallet.common.utils :as common.utils] + [status-im.contexts.wallet.sheets.account-origin.view :as account-origin] + [status-im.feature-flags :as ff] + [status-im.setup.hot-reload :as hot-reload] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.responsiveness :as responsiveness] + [utils.string])) + +(defn- get-keypair-data + [{:keys [title primary-keypair? new-keypair? derivation-path customization-color keycard?]}] + [{:title title + :image (if primary-keypair? :avatar :icon) + :image-props (if primary-keypair? + {:full-name (utils.string/get-initials title 1) + :size :xxs + :customization-color customization-color} + :i/seed) + :action (when-not new-keypair? :button) + :action-props {:on-press #(rf/dispatch [:navigate-to :screen/wallet.select-keypair]) + :button-text (i18n/label :t/edit) + :alignment :flex-start} + :description :text + :description-props {:text (i18n/label (if keycard? :on-keycard :on-device))}} + (when-not (string/blank? derivation-path) + (let [formatted-path (string/replace derivation-path #"/" " / ") + on-auth-success (fn [password] + (rf/dispatch [:navigate-to + :screen/wallet.edit-derivation-path + {:password password + :current-derivation-path formatted-path}]))] + {:title (i18n/label :t/derivation-path) + :image :icon + :image-props :i/derivated-path + :action (if (ff/enabled? ::ff/wallet.edit-derivation-path) :button :none) + :action-props {:on-press #(rf/dispatch [:standard-auth/authorize + {:on-auth-success on-auth-success + :auth-button-label (i18n/label :t/continue)}]) + :button-text (i18n/label :t/edit) + :icon-left :i/face-id + :alignment :flex-start} + :description :text + :description-props {:text formatted-path}}))]) + +(defn- avatar + [{:keys [account-color emoji on-select-emoji]}] + [rn/view {:style style/account-avatar-container} + [quo/account-avatar + {:customization-color account-color + :size 80 + :emoji emoji + :type :default}] + [quo/button + {:size 32 + :type :grey + :background :photo + :icon-only? true + :on-press #(rf/dispatch [:emoji-picker/open {:on-select on-select-emoji}]) + :container-style style/reaction-button-container} + :i/edit]]) + +(defn- input + [_] + (let [placeholder (i18n/label :t/default-account-placeholder)] + (fn [{:keys [account-color account-name on-change-text error]}] + [rn/view + [quo/title-input + {:customization-color account-color + :placeholder placeholder + :on-change-text on-change-text + :max-length constants/wallet-account-name-max-length + :blur? true + :disabled? false + :default-value account-name + :container-style (style/title-input-container error)}] + (when error + [quo/info-message + {:status :error + :size :default + :icon :i/info + :container-style {:margin-left 20 + :margin-bottom 16}} + (case error + :emoji (i18n/label :t/key-name-error-emoji) + :special-character (i18n/label :t/key-name-error-special-char) + :existing-name (i18n/label :t/name-must-differ-error) + :emoji-and-color (i18n/label :t/emoji-and-colors-unique-error) + nil)])]))) + +(defn- color-picker + [_] + (let [{window-width :width} (rn/get-window) + color-picker-style {:padding-vertical 12 + :padding-left (responsiveness/iphone-11-Pro-20-pixel-from-width + window-width)}] + (fn [{:keys [account-color set-account-color]}] + [:<> + [quo/divider-line] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/color-picker-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/color-label theme)} + (i18n/label :t/colour)] + [quo/color-picker + {:default-selected account-color + :on-change set-account-color + :container-style color-picker-style}]])]))) + +(defn- new-account-origin + [{:keys [keypair-title derivation-path customization-color keycard?]}] + (let [{keypair-name :name} (rf/sub [:wallet/selected-keypair]) + primary? (rf/sub [:wallet/selected-primary-keypair?]) + keypair-name (or keypair-title + (if primary? + (i18n/label :t/keypair-title {:name keypair-name}) + keypair-name))] + [:<> + [quo/divider-line] + [quo/category + {:list-type :settings + :label (i18n/label :t/origin) + :data (get-keypair-data {:keycard? keycard? + :primary-keypair? primary? + :title keypair-name + :derivation-path derivation-path + :customization-color customization-color})}]])) + +(defn- floating-button + [_ & _] + (let [header [quo/page-nav + {:type :no-title + :background :blur + :right-side [{:icon-name :i/info + :on-press #(rf/dispatch [:show-bottom-sheet + {:content account-origin/view}])}] + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}]] + (fn [{:keys [footer account-color]} & children] + (into + [floating-button-page/view + {:gradient-cover? true + :footer-container-padding 0 + :header-container-style {:padding-top safe-area/top} + :customization-color account-color + :header header + :footer footer}] + children)))) + +(defn- on-auth-success-mnemonic + [password account-preferences] + (rf/dispatch + [:wallet/import-mnemonic-and-create-keypair-with-account + {:password password + :account-preferences account-preferences}])) + +(defn- on-auth-success-import-private-key + [password account-preferences] + (rf/dispatch + [:wallet/import-private-key-and-create-keypair-with-account + {:password password + :account-preferences account-preferences}])) + +(defn add-new-keypair-variant + [{{:keys [account-name account-color emoji]} :state}] + (fn [{:keys [on-change-text set-account-color set-emoji customization-color keypair-name error]}] + (let [{:keys [new-account-data workflow]} (rf/sub [:wallet/create-account-new-keypair]) + derivation-path (when (not= workflow + :workflow/new-keypair.import-private-key) + constants/path-default-wallet)] + [floating-button + {:account-color @account-color + :footer [standard-auth/slide-auth + {:on-success (fn [password] + (let [account-preferences {:account-name + @account-name + :color @account-color + :emoji @emoji}] + (if (= workflow + :workflow/new-keypair.import-private-key) + (on-auth-success-import-private-key + password + account-preferences) + (on-auth-success-mnemonic + password + account-preferences)))) + :disabled? (empty? @account-name) + :dependencies [new-account-data] + :size :size-48 + :auth-button-label (i18n/label :t/continue) + :track-text (i18n/label :t/slide-to-create-account) + :customization-color @account-color + :container-style style/slide-button-container}]} + [avatar + {:account-color @account-color + :emoji @emoji + :on-select-emoji set-emoji}] + [input + {:account-color @account-color + :account-name @account-name + :on-change-text on-change-text + :error error}] + [color-picker + {:account-color @account-color + :set-account-color set-account-color}] + [new-account-origin + {:derivation-path derivation-path + :customization-color customization-color + :keypair-title keypair-name}]]))) + +(defn derive-account-variant + [{{:keys [account-name account-color emoji]} :state}] + (let [derivation-path (reagent/atom "") + set-derivation-path #(reset! derivation-path %)] + (fn [{:keys [on-change-text set-account-color set-emoji customization-color error]}] + (let [{:keys [key-uid keycards]} (rf/sub [:wallet/selected-keypair]) + keycard? (boolean (seq keycards)) + slider-icon (rf/sub [:standard-auth/slider-icon]) + on-slide-complete (rn/use-callback + (fn [] + (rf/dispatch + [:wallet/authorize-derive-address + {:key-uid key-uid + :derivation-path @derivation-path + :account-preferences {:account-name @account-name + :color @account-color + :emoji @emoji}}])) + [key-uid])] + (rn/use-effect + #(rf/dispatch + [:wallet/next-derivation-path + {:on-success set-derivation-path + :keypair-uid key-uid}]) + [key-uid]) + + [floating-button + {:account-color @account-color + :footer [quo/slide-button + {:on-complete on-slide-complete + :disabled? (or (empty? @account-name) + (string/blank? @derivation-path) + (some? error)) + :size :size-48 + :track-text (i18n/label :t/slide-to-create-account) + :track-icon slider-icon + :customization-color @account-color + :container-style style/slide-button-container}]} + [avatar + {:account-color @account-color + :emoji @emoji + :on-select-emoji set-emoji}] + [input + {:account-color @account-color + :account-name @account-name + :on-change-text on-change-text + :error error}] + [color-picker + {:account-color @account-color + :set-account-color set-account-color}] + [new-account-origin + {:keycard? keycard? + :derivation-path @derivation-path + :customization-color customization-color}]])))) + +(defn view + [] + (let [account-name (reagent/atom "") + account-color (reagent/atom (rand-nth colors/account-colors)) + emoji (reagent/atom (emoji-picker.utils/random-emoji)) + account-name-error (reagent/atom nil) + emoji-and-color-error? (reagent/atom false) + state {:account-name account-name + :account-color account-color + :emoji emoji + :account-name-error account-name-error + :emoji-and-color-error? emoji-and-color-error?}] + (fn [] + (let [customization-color (rf/sub [:profile/customization-color]) + ;; Having a keypair means the user is importing it or creating it. + {:keys [keypair-name workflow]} (rf/sub [:wallet/create-account-new-keypair]) + accounts-names (rf/sub [:wallet/accounts-names]) + accounts-emojis-and-colors (rf/sub [:wallet/accounts-emojis-and-colors]) + on-change-text (rn/use-callback + (fn [new-text] + (reset! account-name new-text) + (reset! account-name-error + (common.utils/get-account-name-error new-text + accounts-names))) + [accounts-names accounts-emojis-and-colors]) + check-emoji-and-color-error (fn [emoji color] + (let [repeated? (accounts-emojis-and-colors [emoji color])] + (reset! emoji-and-color-error? + (when repeated? :emoji-and-color)))) + set-account-color (rn/use-callback + (fn [new-color] + (reset! account-color new-color) + (check-emoji-and-color-error @emoji new-color)) + [accounts-emojis-and-colors @emoji]) + set-emoji (rn/use-callback + (fn [new-emoji] + (reset! emoji new-emoji) + (check-emoji-and-color-error new-emoji @account-color)) + [accounts-emojis-and-colors @account-color]) + error (or @account-name-error @emoji-and-color-error?)] + + (rn/use-mount #(check-emoji-and-color-error @emoji @account-color)) + (hot-reload/use-safe-unmount #(rf/dispatch [:wallet/clear-create-account])) + + (if keypair-name + [add-new-keypair-variant + {:state state + :customization-color customization-color + :on-change-text on-change-text + :set-account-color set-account-color + :set-emoji set-emoji + :keypair-name keypair-name + :error error + :workflow workflow}] + [derive-account-variant + {:state state + :customization-color customization-color + :on-change-text on-change-text + :set-account-color set-account-color + :set-emoji set-emoji + :error error}]))))) diff --git a/src/status_im/contexts/wallet/bridge/bridge_to/style.cljs b/src/status_im/contexts/wallet/bridge/bridge_to/style.cljs new file mode 100644 index 00000000000..140b5c19ea7 --- /dev/null +++ b/src/status_im/contexts/wallet/bridge/bridge_to/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.wallet.bridge.bridge-to.style) + +(def content-container + {:padding-horizontal 8}) + +(def title-container + {:margin-horizontal 4}) + +(def subtitle-container + {:flex-direction :row + :margin-bottom 12 + :margin-horizontal 20 + :align-items :center}) diff --git a/src/status_im/contexts/wallet/bridge/bridge_to/view.cljs b/src/status_im/contexts/wallet/bridge/bridge_to/view.cljs new file mode 100644 index 00000000000..e5da6b560b0 --- /dev/null +++ b/src/status_im/contexts/wallet/bridge/bridge_to/view.cljs @@ -0,0 +1,88 @@ +(ns status-im.contexts.wallet.bridge.bridge-to.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [react-native.core :as rn] + [status-im.contexts.wallet.bridge.bridge-to.style :as style] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.setup.hot-reload :as hot-reload] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- bridge-token-component + [] + (fn [{:keys [chain-id full-name]} token] + (let [network (rf/sub [:wallet/network-by-id chain-id]) + currency (rf/sub [:profile/currency]) + currency-symbol (rf/sub [:profile/currency-symbol]) + prices-per-token (rf/sub [:wallet/prices-per-token]) + token-symbol (rf/sub [:wallet/wallet-send-token-symbol]) + balance (utils/calculate-total-token-balance token [chain-id]) + crypto-value (utils/get-standard-crypto-format token + balance + prices-per-token) + fiat-value (utils/calculate-token-fiat-value + {:currency currency + :balance balance + :token token + :prices-per-token prices-per-token}) + fiat-formatted (utils/fiat-formatted-for-ui currency-symbol + fiat-value)] + [quo/network-list + {:label full-name + :network-image (quo.resources/get-network (:network-name network)) + :token-value (str crypto-value " " token-symbol) + :fiat-value fiat-formatted + :on-press #(rf/dispatch [:wallet/select-bridge-network + {:network-chain-id chain-id + :stack-id :screen/wallet.bridge-to}])}]))) + +(defn view + [] + (let [account (rf/sub [:wallet/current-viewing-account]) + token (rf/sub [:wallet/wallet-send-token]) + network (rf/sub [:wallet/send-network]) + network-name (:full-name network) + token-symbol (:symbol token) + tokens (:tokens account) + to-networks (rf/sub [:wallet/bridge-to-networks]) + account-token (some #(when (= token-symbol (:symbol %)) %) tokens) + account-token (when account-token + (assoc account-token + :networks (:networks token) + :supported-networks (:supported-networks token))) + bridge-to-title (i18n/label :t/select-network-to-receive)] + (hot-reload/use-safe-unmount #(rf/dispatch [:wallet/clean-bridge-to-selection])) + [rn/view + [account-switcher/view + {:on-press #(rf/dispatch [:navigate-back]) + :icon-name :i/arrow-left + :accessibility-label :top-bar + :switcher-type :select-account}] + [quo/page-top + {:title bridge-to-title + :container-style {:padding-vertical 0}}] + [rn/view {:style style/subtitle-container} + [quo/context-tag + {:token token-symbol + :label token-symbol + :size 24 + :type :token}] + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/title-container + :accessibility-label :send-label} + (i18n/label :t/from)] + [quo/context-tag + {:network-name network-name + :type :network + :network-logo (:source network) + :size 24}]] + [rn/flat-list + {:data to-networks + :render-fn (fn [network] + [bridge-token-component network account-token]) + :content-container-style style/content-container}]])) diff --git a/src/status_im/contexts/wallet/bridge/flow_config.cljs b/src/status_im/contexts/wallet/bridge/flow_config.cljs new file mode 100644 index 00000000000..c34d8cba783 --- /dev/null +++ b/src/status_im/contexts/wallet/bridge/flow_config.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.wallet.bridge.flow-config + (:require [status-im.contexts.wallet.db-path :as db-path])) + +(def steps + [{:screen-id :screen/wallet.bridge-select-asset + :skip-step? (fn [db] (some? (get-in db (conj db-path/send :token))))} + {:screen-id :screen/wallet.bridge-to + :skip-step? (fn [db] (some? (get-in db (conj db-path/send :bridge-to-chain-id))))} + {:screen-id :screen/wallet.bridge-input-amount + :skip-step? (fn [db] (some? (get-in db (conj db-path/send :amount))))} + {:screen-id :screen/wallet.transaction-confirmation}]) diff --git a/src/status_im/contexts/wallet/bridge/input_amount/style.cljs b/src/status_im/contexts/wallet/bridge/input_amount/style.cljs new file mode 100644 index 00000000000..c20efaae2bb --- /dev/null +++ b/src/status_im/contexts/wallet/bridge/input_amount/style.cljs @@ -0,0 +1,4 @@ +(ns status-im.contexts.wallet.bridge.input-amount.style) + +(def bridge-send-wrapper + {:flex 1}) diff --git a/src/status_im/contexts/wallet/bridge/input_amount/view.cljs b/src/status_im/contexts/wallet/bridge/input_amount/view.cljs new file mode 100644 index 00000000000..6a95370e28a --- /dev/null +++ b/src/status_im/contexts/wallet/bridge/input_amount/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.contexts.wallet.bridge.input-amount.view + (:require + [react-native.core :as rn] + [status-im.contexts.wallet.bridge.input-amount.style :as style] + [status-im.contexts.wallet.send.input-amount.view :as input-amount] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + [rn/view {:style style/bridge-send-wrapper} + [input-amount/view + {:current-screen-id :screen/wallet.bridge-input-amount + :button-one-label (i18n/label :t/review-bridge) + :enabled-from-chain-ids (rf/sub + [:wallet/bridge-from-chain-ids]) + :from-enabled-networks (rf/sub [:wallet/bridge-from-networks]) + :on-confirm (fn [amount] + (rf/dispatch [:wallet/set-token-amount-to-bridge + {:amount amount + :stack-id :screen/wallet.bridge-input-amount}])) + :on-navigate-back (fn [] + (rf/dispatch-sync [:wallet/stop-and-clean-suggested-routes]) + (rf/dispatch [:wallet/clean-send-amount]))}]]) diff --git a/src/status_im/contexts/wallet/bridge/select_asset/style.cljs b/src/status_im/contexts/wallet/bridge/select_asset/style.cljs new file mode 100644 index 00000000000..4de398e134f --- /dev/null +++ b/src/status_im/contexts/wallet/bridge/select_asset/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.wallet.bridge.select-asset.style) + +(def input-container + {:padding-horizontal 20 + :padding-vertical 8}) + diff --git a/src/status_im/contexts/wallet/bridge/select_asset/view.cljs b/src/status_im/contexts/wallet/bridge/select_asset/view.cljs new file mode 100644 index 00000000000..c381b398f80 --- /dev/null +++ b/src/status_im/contexts/wallet/bridge/select_asset/view.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.wallet.bridge.select-asset.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.wallet.bridge.select-asset.style :as style] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.common.asset-list.view :as asset-list] + [status-im.contexts.wallet.networks.core :as networks] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [search-text (reagent/atom "")] + (fn [] + [rn/view {:style {:flex 1}} + [account-switcher/view + {:on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:wallet/clean-selected-token])) + :accessibility-label :top-bar + :switcher-type :select-account}] + [quo/page-top {:title (i18n/label :t/bridge)}] + [quo/input + {:container-style style/input-container + :icon-name :i/search + :on-change-text #(reset! search-text %) + :placeholder (i18n/label :t/search-assets)}] + [asset-list/view + {:search-text @search-text + :chain-ids (networks/get-bridge-supported-networks) + :on-token-press (fn [token] + (rf/dispatch [:wallet/bridge-select-token + {:token token + :stack-id :screen/wallet.bridge-select-asset}]))}]]))) diff --git a/src/status_im/contexts/wallet/buy_crypto/events.cljs b/src/status_im/contexts/wallet/buy_crypto/events.cljs new file mode 100644 index 00000000000..e21684c6b3c --- /dev/null +++ b/src/status_im/contexts/wallet/buy_crypto/events.cljs @@ -0,0 +1,73 @@ +(ns status-im.contexts.wallet.buy-crypto.events + (:require [re-frame.core :as rf] + [react-native.core :as rn] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.contexts.wallet.sheets.buy-network-selection.view :as buy-network-selection] + [status-im.contexts.wallet.sheets.select-asset.view :as select-asset] + [utils.i18n :as i18n])) + +(rf/reg-event-fx :wallet.buy-crypto/select-provider + (fn [{:keys [db]} [{:keys [account provider recurrent?]}]] + (let [{swap-network :network} (get-in db db-path/swap)] + {:db (-> db + (assoc-in [:wallet :ui :buy-crypto :account] account) + (assoc-in [:wallet :ui :buy-crypto :provider] provider) + (assoc-in [:wallet :ui :buy-crypto :recurrent?] recurrent?)) + :fx [(if swap-network + [:dispatch [:wallet.buy-crypto/select-network swap-network]] + [:dispatch + [:show-bottom-sheet + {:content + (fn [] + [buy-network-selection/view + {:provider provider + :account account}])}]])]}))) + +(rf/reg-event-fx :wallet.buy-crypto/select-network + (fn [{:keys [db]} [network]] + (let [provider (get-in db [:wallet :ui :buy-crypto :provider]) + network-chain-id (:chain-id network) + supported-tokens (:supported-tokens provider) + supported-tokens-on-network (set (map :symbol + (filter (fn [token] + (= (:chain-id token) network-chain-id)) + supported-tokens)))] + {:db (assoc-in db [:wallet :ui :buy-crypto :network] network) + :fx [[:dispatch + [:show-bottom-sheet + {:content + (fn [] + [select-asset/view + {:title (i18n/label :t/select-asset-to-buy) + :network network + :provider provider + :on-select (fn [token] + (rf/dispatch [:wallet/get-crypto-on-ramp-url {:token token}])) + :hide-token-fn (fn [_ token] + (not (contains? supported-tokens-on-network + (:symbol token))))}])}]]]}))) + +(rf/reg-event-fx + :wallet/get-crypto-on-ramp-url + (fn [{:keys [db]} [{:keys [token]}]] + (let [account (get-in db [:wallet :ui :buy-crypto :account]) + provider (get-in db [:wallet :ui :buy-crypto :provider]) + network (get-in db [:wallet :ui :buy-crypto :network]) + recurrent? (get-in db [:wallet :ui :buy-crypto :recurrent?]) + provider-id (:id provider) + parameters {:symbol (:symbol token) + :destAddress (:address account) + :chainID (:chain-id network) + :isRecurrent recurrent?}] + {:fx [[:json-rpc/call + [{:method "wallet_getCryptoOnRampURL" + :params [provider-id parameters] + :on-success (fn [url] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:wallet.buy-crypto/clean-all]) + (rn/open-url url)) + :on-error [:wallet/log-rpc-error {:event :wallet/get-crypto-on-ramps}]}]]]}))) + +(rf/reg-event-fx :wallet.buy-crypto/clean-all + (fn [{:keys [db]}] + {:db (update-in db [:wallet :ui] dissoc :buy-crypto)})) diff --git a/src/status_im/contexts/wallet/collectible/events.cljs b/src/status_im/contexts/wallet/collectible/events.cljs new file mode 100644 index 00000000000..83fc7fbf7d9 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/events.cljs @@ -0,0 +1,467 @@ +(ns status-im.contexts.wallet.collectible.events + (:require [camel-snake-kebab.extras :as cske] + [clojure.set] + [clojure.string :as string] + [react-native.platform :as platform] + [status-im.contexts.network.db :as network.db] + [status-im.contexts.profile.db :as profile.db] + [status-im.contexts.wallet.account.db :as account.db] + [status-im.contexts.wallet.collectible.utils :as collectible-utils] + [status-im.contexts.wallet.data-store :as data-store] + [status-im.contexts.wallet.networks.db :as networks.db] + [taoensso.timbre :as log] + [utils.collection] + [utils.number :as utils.number] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(def collectible-data-types + {:unique-id 0 + :header 1 + :details 2 + :community-header 3}) + +(def fetch-type + {:never-fetch 0 + :always-fetch 1 + :fetch-if-not-cached 2 + :fetch-if-cache-old 3}) + +(def max-cache-age-seconds 3600) +(def collectibles-request-batch-size 25) + +(def ownership-state + {:idle 1 + :delayed 2 + :updating 3 + :error 4}) + +(defn- move-collectibles-to-accounts + [accounts new-collectibles-per-account] + (reduce-kv (fn [acc account new-collectibles] + (update-in acc [account :collectibles] #(reduce conj (or % []) new-collectibles))) + accounts + new-collectibles-per-account)) + +(defn flush-collectibles + [{:keys [db]}] + (let [collectibles-per-account (get-in db [:wallet :ui :collectibles :fetched]) + updated-accounts (move-collectibles-to-accounts (get-in db [:wallet :accounts]) + collectibles-per-account) + has-collectibles? (some (fn [account] + (pos? (count (:collectibles account)))) + (vals updated-accounts))] + {:db (-> db + (update-in [:wallet :ui :collectibles] dissoc :pending-requests :fetched) + (assoc-in [:wallet :accounts] updated-accounts)) + :fx [[:dispatch + [:centralized-metrics/track :metric/wallet-collectibles-fetched + {:has-collectibles? has-collectibles?}]]]})) + +(rf/reg-event-fx :wallet/flush-collectibles-fetched flush-collectibles) + +(defn clear-stored-collectibles + [{:keys [db]}] + {:db (update-in db [:wallet :accounts] update-vals #(dissoc % :collectibles))}) + +(rf/reg-event-fx :wallet/clear-stored-collectibles clear-stored-collectibles) + +(rf/reg-event-fx + :wallet/request-new-collectibles-for-account + (fn [{:keys [db]} [{:keys [request-id account amount]}]] + (let [current-collectible-idx (get-in db [:wallet :accounts account :current-collectible-idx] 0) + collectibles-filter nil + data-type (collectible-data-types :header) + fetch-criteria {:fetch-type (fetch-type :fetch-if-cache-old) + :max-cache-age-seconds max-cache-age-seconds} + chain-ids (networks.db/get-active-chain-ids db) + request-params [request-id + chain-ids + [account] + collectibles-filter + current-collectible-idx + amount + data-type + fetch-criteria]] + {:fx [[:json-rpc/call + [{:method "wallet_getOwnedCollectiblesAsync" + :params request-params + :on-error (fn [error] + (log/error "failed to request collectibles for account" + {:event :wallet/request-new-collectibles-for-account + :error error + :params request-params}))}]]]}))) + +(defonce collectibles-request-ids (atom 0)) + +(defn- get-unique-collectible-request-id + [amount] + (let [initial-id (deref collectibles-request-ids) + last-id (+ initial-id amount)] + (reset! collectibles-request-ids last-id) + (range initial-id last-id))) + +(rf/reg-event-fx + :wallet/request-collectibles-for-all-accounts + (fn [{:keys [db]} [{:keys [new-request?]}]] + (let [updating-addresses (-> (get-in db [:wallet :ui :collectibles :updating]) + keys + set) + accounts (->> (get-in db [:wallet :accounts]) + (filter (fn [[_ {:keys [has-more-collectibles?]}]] + (or (nil? has-more-collectibles?) + (true? has-more-collectibles?)))) + (keys)) + ;; filter the addresses which are requested before and the collectibles are updating + requestable-addresses (clojure.set/difference (set accounts) + updating-addresses) + num-accounts (count requestable-addresses) + collectibles-per-account (quot collectibles-request-batch-size num-accounts) + ;; We need to pass unique IDs for simultaneous requests, otherwise they'll fail + request-ids (get-unique-collectible-request-id num-accounts) + collectible-requests (map (fn [id account] + [:dispatch + [:wallet/request-new-collectibles-for-account + {:request-id id + :account account + :amount collectibles-per-account}]]) + request-ids + requestable-addresses)] + {:db (cond-> db + :always (assoc-in [:wallet :ui :collectibles :pending-requests] num-accounts) + new-request? (update-in [:wallet :accounts] update-vals #(dissoc % :collectibles))) + :fx collectible-requests}))) + +(defn request-collectibles-for-account + [{:keys [db]} [address]] + (let [pending-requests (get-in db [:wallet :ui :collectibles :pending-requests] 0) + [request-id] (get-unique-collectible-request-id 1)] + {:db (assoc-in db [:wallet :ui :collectibles :pending-requests] (inc pending-requests)) + :fx [[:dispatch + [:wallet/request-new-collectibles-for-account + {:request-id request-id + :account address + :amount collectibles-request-batch-size}]]]})) + +(rf/reg-event-fx :wallet/request-collectibles-for-account request-collectibles-for-account) + +(rf/reg-event-fx + :wallet/reload-collectibles + (fn [{:keys [db]}] + (let [n-pending-requests (get-in db [:wallet :ui :collectibles :pending-requests] 0) + addresses (account.db/get-accounts-addresses db) + chain-ids (networks.db/get-active-chain-ids db) + request-id (last (get-unique-collectible-request-id 1)) + collectibles-filter nil + offset 0 + fetch-criteria {:fetch-type (fetch-type :fetch-if-not-cached) + :max-cache-age-seconds max-cache-age-seconds} + data-type (collectible-data-types :header) + request-params [request-id + chain-ids + addresses + collectibles-filter + offset + collectibles-request-batch-size + data-type + fetch-criteria]] + {:db (-> db + (assoc-in [:wallet :ui :collectibles :pending-requests] (inc n-pending-requests)) + (update-in [:wallet :accounts] + update-vals + #(dissoc % + :collectibles + :has-more-collectibles? + :current-collectible-idx))) + :fx [[:json-rpc/call + [{:method "wallet_getOwnedCollectiblesAsync" + :params request-params + :on-error (fn [error] + (log/error "failed to request collectibles for account" + {:event :wallet/request-new-collectibles-for-account + :error error + :params request-params}))}]]]}))) + +(rf/reg-event-fx + :wallet/request-collectibles-for-current-viewing-account + (fn [{:keys [db]} _] + (when (network.db/online? db) + (let [current-viewing-account (-> db :wallet :current-viewing-account-address)] + {:fx [[:dispatch [:wallet/request-collectibles-for-account current-viewing-account]]]})))) + +(rf/reg-event-fx + :wallet/collectible-ownership-update-finished-with-error + (fn [{:keys [db]} [{:keys [accounts message chainId]}]] + (let [address (first accounts) + pending-chain-ids (get-in db [:wallet :ui :collectibles :updating address]) + updated-chain-ids (disj pending-chain-ids chainId) + all-chain-updated? (and (some? pending-chain-ids) (empty? updated-chain-ids))] + {:db (cond-> db + (some? pending-chain-ids) + (assoc-in [:wallet :ui :collectibles :updating address] updated-chain-ids)) + :fx [[:dispatch + [:wallet/log-rpc-error + {:event :wallet/collectible-ownership-update-finished-with-error + :params {:address address + :chain-id chainId}} + message]] + (when all-chain-updated? + [:dispatch [:wallet/request-collectibles-for-account address]])]}))) + +(rf/reg-event-fx + :wallet/collectible-ownership-update-finished + (fn [{:keys [db]} [{:keys [accounts chainId message]}]] + (let [address (first accounts) + pending-chain-ids (get-in db [:wallet :ui :collectibles :updating address]) + updated-chain-ids (disj pending-chain-ids chainId) + all-chain-updated? (and (some? pending-chain-ids) (empty? updated-chain-ids)) + collectible-id (data-store/rpc->collectible-id message)] + {:db (cond-> db + (some? pending-chain-ids) + (assoc-in [:wallet :ui :collectibles :updating address] updated-chain-ids)) + :fx [(when all-chain-updated? + [:dispatch [:wallet/request-collectibles-for-account address]]) + (when collectible-id + (let [collectible-unique-id (collectible-utils/get-collectible-unique-id {:id + collectible-id}) + pending-collectible? (-> db + (get-in [:wallet :ui :collectibles :pending]) + (contains? collectible-unique-id))] + (when pending-collectible? + [:dispatch + [:wallet/update-pending-collectible-details collectible-id + collectible-unique-id]])))]}))) + +(rf/reg-event-fx + :wallet/update-pending-collectible-details + (fn [{:keys [db]} [collectible-id collectible-unique-id]] + {:db (update-in db [:wallet :ui :collectibles :pending] dissoc collectible-unique-id) + :fx [[:dispatch [:wallet/get-collectibles-by-unique-id-async collectible-id]]]})) + +(defn- update-collectibles-in-account + [existing-collectibles updated-collectibles] + (let [indexed-existing (utils.collection/index-by + :unique-id + existing-collectibles) + existing-ids (-> indexed-existing keys vec) + ;; pick collectibles only in the app-db + indexed-updated (-> (utils.collection/index-by + :unique-id + updated-collectibles) + (select-keys existing-ids))] + (-> (merge-with + merge + indexed-existing + indexed-updated) + vals + vec))) + +(rf/reg-event-fx + :wallet/collectibles-data-updated + (fn [{:keys [db]} [{:keys [message]}]] + (let [collectibles-by-address (->> message + transforms/json->clj + data-store/rpc->collectibles + (group-by #(-> % :ownership first :address)))] + {:db (update-in db + [:wallet :accounts] + #(reduce-kv + (fn [accounts address updated-collectibles] + (if (contains? accounts address) + (update-in accounts + [address :collectibles] + update-collectibles-in-account + updated-collectibles) + accounts)) + % + collectibles-by-address))}))) + +(rf/reg-event-fx + :wallet/set-collectibles-updating-status + (fn [{:keys [db]} [address chain-ids]] + {:db (assoc-in db [:wallet :ui :collectibles :updating address] chain-ids)})) + +(defn- update-fetched-collectibles-progress + [db owner-address collectibles offset has-more?] + (-> db + (update-in [:wallet :ui :collectibles :updating] dissoc owner-address) + (assoc-in [:wallet :ui :collectibles :fetched owner-address] collectibles) + (assoc-in [:wallet :accounts owner-address :current-collectible-idx] + (+ offset (count collectibles))) + (assoc-in [:wallet :accounts owner-address :has-more-collectibles?] has-more?))) + +(defn- updating-collectibles? + [status] + (and (= (:state status) (ownership-state :updating)) + (= (:timestamp status) -1))) + +(rf/reg-event-fx + :wallet/owned-collectibles-filtering-done + (fn [{:keys [db]} [{:keys [message]}]] + (let [{:keys [offset ownershipStatus collectibles + hasMore]} (transforms/json->clj message) + ownership-status-by-address (-> ownershipStatus + (update-keys name) + (update-vals #(update-keys % + (comp utils.number/parse-int name)))) + owner-address (some-> ownership-status-by-address + first + key) + ownership-status (get ownership-status-by-address owner-address) + collectibles (->> collectibles + data-store/rpc->collectibles + vec) + pending-requests (dec (get-in db [:wallet :ui :collectibles :pending-requests])) + ;; check if collectibles are updating (never fetched and cached before) for this address + updating-chains (-> (select-keys + ownership-status + (for [[k v] ownership-status + :when (updating-collectibles? v)] + k)) + keys + set) + updating? (-> updating-chains count pos?)] + {:db (cond-> db + :always (assoc-in [:wallet :ui :collectibles :pending-requests] pending-requests) + owner-address (update-fetched-collectibles-progress owner-address + collectibles + offset + (when-not updating? hasMore))) + :fx [(when (zero? pending-requests) + [:dispatch [:wallet/flush-collectibles-fetched]]) + (when updating? + [:dispatch [:wallet/set-collectibles-updating-status owner-address updating-chains]])]}))) + +(rf/reg-event-fx + :wallet/navigate-to-collectible-details + (fn [{:keys [db]} + [{{collectible-id :id :as collectible} :collectible + aspect-ratio :aspect-ratio + gradient-color :gradient-color}]] + {:db (assoc-in db + [:wallet :ui :collectible] + {:details collectible + :aspect-ratio aspect-ratio + :gradient-color gradient-color}) + :fx [[:dispatch [:wallet/get-collectibles-by-unique-id-async collectible-id]] + ;; We delay the navigation because we need re-frame to update the DB on time. + ;; By doing it, we skip a blink while visiting the collectible detail page. + [:dispatch-later + {:ms 20 + :dispatch [:open-modal :screen/wallet.collectible]}]]})) + +(rf/reg-event-fx + :wallet/get-collectibles-by-unique-id-async + (fn [_ [collectible-id]] + (let [request-id 0 + collectible-id-converted (cske/transform-keys transforms/->PascalCaseKeyword collectible-id) + data-type (collectible-data-types :details) + request-params [request-id [collectible-id-converted] data-type]] + {:fx [[:json-rpc/call + [{:method "wallet_getCollectiblesByUniqueIDAsync" + :params request-params + :on-error (fn [error] + (log/error "failed to request collectible" + {:event :wallet/navigate-to-collectible-details + :error error + :params request-params}))}]]]}))) + +(defn- keep-not-empty-value + [old-value new-value] + (if (or (string/blank? new-value) (nil? new-value)) + old-value + new-value)) + +(rf/reg-event-fx + :wallet/get-collectible-details-done + (fn [{db :db} [{:keys [message]}]] + (let [response (cske/transform-keys transforms/->kebab-case-keyword + (transforms/json->clj message)) + {[collectible] :collectibles} response + known-collectible-info (get-in db [:wallet :ui :collectible :details]) + current-account-address (get-in db [:wallet :current-viewing-account-address]) + total-owned (collectible-utils/collectible-balance collectible + current-account-address) + merged-collectible (as-> known-collectible-info c + (merge-with keep-not-empty-value + c + collectible) + (update c + :ownership + collectible-utils/remove-duplicates-in-ownership)) + updated-collectible (assoc merged-collectible + :total-owned + total-owned)] + (if collectible + {:db (assoc-in db [:wallet :ui :collectible :details] updated-collectible) + :fx [[:dispatch [:wallet/update-collectibles-data updated-collectible]]]} + (log/error "failed to get collectible details" + {:event :wallet/get-collectible-details-done + :response response}))))) + +(rf/reg-event-fx + :wallet/update-collectibles-data + (fn [{:keys [db]} [collectible]] + (let [collectibles-by-address (collectible-utils/group-collectibles-by-ownership-address collectible)] + {:db (update-in db + [:wallet :accounts] + #(reduce-kv + (fn [accounts address updated-collectibles] + (if (contains? accounts address) + (update-in accounts + [address :collectibles] + update-collectibles-in-account + [updated-collectibles]) + accounts)) + % + collectibles-by-address))}))) + +(rf/reg-event-fx + :wallet/clear-collectible-details + (fn [{:keys [db]}] + {:db (update-in db [:wallet :ui] dissoc :collectible)})) + +(rf/reg-event-fx :wallet/trigger-share-collectible + (fn [_ [{:keys [title uri]}]] + {:fx [[:dispatch + [:open-share + {:options (if platform/ios? + {:activityItemSources + [{:placeholderItem {:type :text + :content uri} + :item {:default {:type :url + :content uri}} + :linkMetadata {:title title}}]} + {:title title + :subject title + :url uri + :isNewTask true})}]]]})) + +(rf/reg-event-fx :wallet/share-collectible + (fn [{:keys [db]} [{:keys [title token-id contract-address chain-id]}]] + (let [network (networks.db/get-network-details db chain-id) + uri (collectible-utils/get-opensea-collectible-url + {:network-name (:network-name network) + :token-id token-id + :contract-address contract-address + :test-networks-enabled? (profile.db/testnet? db)})] + {:fx [[:dispatch + [:hide-bottom-sheet]] + [:dispatch-later + {:ms 600 + :dispatch [:wallet/trigger-share-collectible + {:title title + :uri uri}]}]]}))) + +(rf/reg-event-fx + :wallet/navigate-to-opensea + (fn [{:keys [db]} [chain-id token-id contract-address]] + (let [network (networks.db/get-network-details db chain-id)] + {:fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch + [:browser.ui/open-url + (collectible-utils/get-opensea-collectible-url + {:network-name (:network-name network) + :token-id token-id + :contract-address contract-address + :test-networks-enabled? (profile.db/testnet? db)})]]]}))) diff --git a/src/status_im/contexts/wallet/collectible/events_test.cljs b/src/status_im/contexts/wallet/collectible/events_test.cljs new file mode 100644 index 00000000000..91647ba0356 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/events_test.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.wallet.collectible.events-test + (:require + [cljs.test :refer-macros [deftest is testing]] + matcher-combinators.test + [status-im.contexts.wallet.collectible.events :as events])) + +(deftest store-collectibles-test + (testing "flush-collectibles" + (let [collectible-1 {:collectible-data {:image-url "https://..." :animation-url "https://..."} + :ownership [{:address "0x1" + :balance "1"}]} + collectible-2 {:collectible-data {:image-url "" :animation-url "https://..."} + :ownership [{:address "0x1" + :balance "1"}]} + collectible-3 {:collectible-data {:image-url "" :animation-url nil} + :ownership [{:address "0x2" + :balance "1"}]} + db {:wallet {:ui {:collectibles {:pending-requests 0 + :fetched {"0x1" [collectible-1 + collectible-2] + "0x2" [collectible-3]}}} + :accounts {"0x1" {} + "0x3" {}}}} + expected-db {:wallet {:ui {:collectibles {}} + :accounts {"0x1" {:collectibles (list collectible-1 collectible-2)} + "0x2" {:collectibles (list collectible-3)} + "0x3" {}}}} + result-db (:db (events/flush-collectibles {:db db}))] + + (is (match? result-db expected-db))))) + +(deftest clear-stored-collectibles-test + (let [db {:wallet {:accounts {"0x1" {:collectibles [{:id 1} {:id 2}]} + "0x2" {"some other stuff" "with any value" + :collectibles [{:id 3}]} + "0x3" {}}}}] + (testing "clear-stored-collectibles" + (let [expected-db {:wallet {:accounts {"0x1" {} + "0x2" {"some other stuff" "with any value"} + "0x3" {}}}} + effects (events/clear-stored-collectibles {:db db}) + result-db (:db effects)] + + (is (match? result-db expected-db)))))) + +(deftest request-collectibles-for-account-test + (testing "request collectibles for account" + (let [db {:wallet {}} + address "0x1" + expected {:db {:wallet {:ui {:collectibles {:pending-requests 1}}}} + :fx [[:dispatch + [:wallet/request-new-collectibles-for-account + {:request-id 0 + :account address + :amount events/collectibles-request-batch-size}]]]} + effects (events/request-collectibles-for-account {:db db} + [address])] + (is (match? expected effects))))) diff --git a/src/status_im/contexts/wallet/collectible/options/view.cljs b/src/status_im/contexts/wallet/collectible/options/view.cljs new file mode 100644 index 00000000000..2ba7b1d1c23 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/options/view.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.wallet.collectible.options.view + (:require + [quo.core :as quo] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.url :as url])) + +(defn view + [{:keys [image name id]}] + (let [chain-id (get-in id [:contract-id :chain-id]) + {:keys [block-explorer-name]} (rf/sub [:wallet/network-by-id chain-id]) + token-id (:token-id id) + contract-address (get-in id [:contract-id :address]) + uri (url/replace-port image (rf/sub [:mediaserver/port]))] + [quo/action-drawer + [[{:icon :i/link + :accessibility-label :view-on-block-explorer + :on-press (fn [] + (rf/dispatch [:wallet/navigate-to-chain-explorer chain-id + contract-address])) + :label (i18n/label :t/view-on-block-explorer + {:block-explorer-name block-explorer-name}) + :right-icon :i/external}] + [{:icon :i/save + :accessibility-label :save-image + :label (i18n/label :t/save-image-to-photos) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:lightbox/save-image-to-gallery + uri + #(rf/dispatch [:toasts/upsert + {:id :random-id + :type :positive + :text (i18n/label :t/photo-saved)}])]))}] + [{:icon :i/share + :accessibility-label :share-collectible + :label (i18n/label :t/share-collectible) + :on-press #(rf/dispatch [:wallet/share-collectible + {:token-id token-id + :contract-address contract-address + :chain-id chain-id + :title name}])}]]])) diff --git a/src/status_im/contexts/wallet/collectible/style.cljs b/src/status_im/contexts/wallet/collectible/style.cljs new file mode 100644 index 00000000000..41179ad65b6 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/style.cljs @@ -0,0 +1,88 @@ +(ns status-im.contexts.wallet.collectible.style + (:require [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area])) + +(def container {:margin-bottom 34}) + +(defn- header-height + [] + (+ 56 safe-area/top)) + +(defn preview-container + [] + {:padding-horizontal 8 + :margin-top (+ (header-height) 12)}) + +(def header + {:margin-horizontal 20 + :margin-top 16 + :margin-bottom 12}) + +(def collection-container + {:flex-direction :row + :margin-top 6}) + +(def collection-avatar-container + {:margin-right 8}) + +(def buttons-container + {:flex-direction :row + :align-items :stretch + :margin-horizontal 20 + :margin-top 8 + :margin-bottom 20}) + +(def tabs + {:margin-horizontal 20 + :margin-vertical 12}) + +(def send-button + {:flex 1 + :margin-right 6}) + +(def opensea-button + {:flex 1 + :margin-left 6}) + +(defn animated-header + [] + {:position :absolute + :top 0 + :left 0 + :right 0 + :height (header-height) + :z-index 1 + :overflow :hidden}) + +(defn scroll-view + [safe-area-top] + {:flex 1 + :margin-top (when platform/ios? (- safe-area-top))}) + +(def gradient-layer + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :flex 1 + :align-items :center + :overflow :hidden}) + +(def image-background + {:height (:height (rn/get-window)) + :aspect-ratio 1}) + +(def gradient + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0}) + +(defn background-color + [theme] + {:flex 1 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) diff --git a/src/status_im/contexts/wallet/collectible/tabs/about/style.cljs b/src/status_im/contexts/wallet/collectible/tabs/about/style.cljs new file mode 100644 index 00000000000..627d7a3f624 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/tabs/about/style.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.wallet.collectible.tabs.about.style) + +(def title + {:padding-horizontal 20 + :padding-top 8 + :padding-bottom 4}) + +(def description + {:padding-horizontal 20 + :padding-top 4 + :padding-bottom 12}) + +(def section-label + {:margin-horizontal 20 + :margin-top 12}) + +(def link-cards-container + {:margin-horizontal 20 + :margin-top 12 + :flex-direction :row + :justify-content :space-between + :flex-wrap :wrap}) + +(defn link-card + [item-width] + {:margin-bottom 16 + :width item-width}) diff --git a/src/status_im/contexts/wallet/collectible/tabs/about/view.cljs b/src/status_im/contexts/wallet/collectible/tabs/about/view.cljs new file mode 100644 index 00000000000..3ebd227add4 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/tabs/about/view.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.wallet.collectible.tabs.about.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.collectible.tabs.about.style :as style] + [utils.re-frame :as rf])) + +(def ^:private link-card-space 28) + +(defn view + [{:keys [collectible-data]}] + (let [window-width (rf/sub [:dimensions/window-width]) + item-width (- (/ window-width 2) link-card-space) + link-card-container-style (style/link-card item-width) + collectible-about {:cards []}] + [:<> + [rn/view {:style style/title} + [quo/text + {:size :heading-2 + :weight :semi-bold} + (:name collectible-data)]] + [rn/view {:style style/description} + [quo/text + {:size :paragraph-2} + (:description collectible-data)]] + (when (count collectible-about) + [rn/view {:style style/link-cards-container} + (for [item (:cards collectible-about)] + ^{:key (:title item)} + [quo/link-card (assoc item :container-style link-card-container-style)])])])) diff --git a/src/status_im/contexts/wallet/collectible/tabs/activity/view.cljs b/src/status_im/contexts/wallet/collectible/tabs/activity/view.cljs new file mode 100644 index 00000000000..245e49cde5f --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/tabs/activity/view.cljs @@ -0,0 +1,16 @@ +(ns status-im.contexts.wallet.collectible.tabs.activity.view + (:require [quo.core :as quo] + [react-native.core :as rn])) + +(defn activity-item + [item] + [:<> + [quo/divider-date (:timestamp item)] + [quo/wallet-activity item]]) + +(defn view + [] + [rn/flat-list + {:data nil + :style {:flex 1} + :render-fn activity-item}]) diff --git a/src/status_im/contexts/wallet/collectible/tabs/overview/style.cljs b/src/status_im/contexts/wallet/collectible/tabs/overview/style.cljs new file mode 100644 index 00000000000..1d0514909fb --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/tabs/overview/style.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.wallet.collectible.tabs.overview.style) + +(def info-container + {:flex-direction :row + :margin-horizontal 20 + :margin-top 8 + :margin-bottom 12}) + +(def account + {:margin-right 6 + :flex 1}) + +(def network + {:margin-left 6 + :flex 1}) + +(def traits-title-container + {:margin-left 20 + :margin-top 8}) + +(def traits-item + {:margin 6 + :flex 1}) + +(def traits-container + {:margin-horizontal 14 + :margin-vertical 12}) diff --git a/src/status_im/contexts/wallet/collectible/tabs/overview/view.cljs b/src/status_im/contexts/wallet/collectible/tabs/overview/view.cljs new file mode 100644 index 00000000000..8dc2a42ca8a --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/tabs/overview/view.cljs @@ -0,0 +1,71 @@ +(ns status-im.contexts.wallet.collectible.tabs.overview.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [react-native.core :as rn] + [status-im.contexts.wallet.collectible.tabs.overview.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- trait-item + [{:keys [trait-type value]}] + [quo/data-item + {:subtitle-type :default + :card? true + :status :default + :size :default + :title trait-type + :subtitle value + :container-style style/traits-item}]) + +(defn- traits-section + [traits] + [rn/view + [quo/section-label + {:section (i18n/label :t/traits) + :container-style style/traits-title-container}] + [rn/flat-list + {:render-fn trait-item + :data traits + :key :collectibles-list + :key-fn :id + :num-columns 2 + :content-container-style style/traits-container}]]) + +(defn- info + [{:keys [chain-id account]}] + (let [{:keys [network-name full-name]} (rf/sub [:wallet/network-by-id chain-id])] + [rn/view {:style style/info-container} + [rn/view {:style style/account} + [quo/data-item + {:card? true + :status :default + :size :default + :title (i18n/label :t/account-title) + :subtitle-type :account + :subtitle (:name account) + :emoji (:emoji account) + :customization-color (:color account)}]] + [rn/view {:style style/network} + [quo/data-item + {:subtitle-type :network + :card? true + :status :default + :size :default + :title (i18n/label :t/network) + :network-image (quo.resources/get-network network-name) + :subtitle full-name}]]])) + +(defn view + [collectible] + (let [owner-address (or (rf/sub [:wallet/current-viewing-account-address]) + (-> collectible :ownership first :address)) + owner-account (rf/sub [:wallet-connect/account-details-by-address owner-address]) + traits (-> collectible :collectible-data :traits)] + [:<> + [info + {:chain-id (-> collectible :id :contract-id :chain-id) + :account owner-account}] + (when (pos? (count traits)) + [traits-section traits])])) diff --git a/src/status_im/contexts/wallet/collectible/tabs/view.cljs b/src/status_im/contexts/wallet/collectible/tabs/view.cljs new file mode 100644 index 00000000000..e1cf0610789 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/tabs/view.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.wallet.collectible.tabs.view + (:require [quo.context] + [status-im.contexts.wallet.collectible.tabs.about.view :as about] + [status-im.contexts.wallet.collectible.tabs.activity.view :as activity] + [status-im.contexts.wallet.collectible.tabs.overview.view :as overview])) + +(defn view + [{:keys [selected-tab collectible]}] + (case selected-tab + :overview [overview/view collectible] + :about [about/view collectible] + :activity [activity/view] + nil)) diff --git a/src/status_im/contexts/wallet/collectible/utils.cljs b/src/status_im/contexts/wallet/collectible/utils.cljs new file mode 100644 index 00000000000..5ba6a4541c4 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/utils.cljs @@ -0,0 +1,105 @@ +(ns status-im.contexts.wallet.collectible.utils + (:require [clojure.string :as string] + [status-im.config :as config] + [status-im.constants :as constants] + [taoensso.timbre :as log] + [utils.number :as utils.number])) + +(defn- total-collectible-balance + ([ownership] + (reduce (fn [total {:keys [balance]}] + (+ total (or (utils.number/parse-int balance) 0))) + 0 + ownership))) + +(defn collectible-balance + ([{:keys [ownership]} address] + (let [balance (if address + (some #(when (= address (:address %)) + (:balance %)) + ownership) + (total-collectible-balance ownership))] + (utils.number/parse-int balance)))) + +(def supported-collectible-types + #{"image/jpeg" + "image/bmp" + "image/png" + "image/webp" + "image/avif"}) + +(defn supported-file? + [collectible-type] + (if (supported-collectible-types collectible-type) + true + (do + (log/debug "unsupported collectible file type:" (or collectible-type "Unknown type")) + false))) + +(defn collectible-owned-counter + [total] + (when (> total 1) (str "x" total))) + +(defn- get-opensea-network-name + [network-name test-networks-enabled?] + (let [mainnet? (= :mainnet network-name) + network-name (name network-name)] + (cond (and test-networks-enabled? mainnet?) + (:sepolia constants/opensea-url-names) + + test-networks-enabled? + (str network-name "-" (:sepolia constants/opensea-url-names)) + + mainnet? + (:ethereum constants/opensea-url-names) + + :else + network-name))) + +(defn- get-opensea-base-url + [test-networks-enabled?] + (cond + test-networks-enabled? config/opensea-tesnet-link + :else config/opensea-link)) + +(defn get-opensea-collectible-url + [{:keys [network-name token-id contract-address + test-networks-enabled?]}] + (let [base-link (get-opensea-base-url test-networks-enabled?) + opensea-network-name (get-opensea-network-name network-name + test-networks-enabled?)] + (str base-link "/assets/" opensea-network-name "/" contract-address "/" token-id))) + +(defn get-collectible-unique-id + [{:keys [id]}] + (let [chain-id (-> id :contract-id :chain-id) + contract-address (-> id :contract-id :address) + token-id (-> id :token-id)] + (str chain-id contract-address token-id))) + +(defn remove-duplicates-in-ownership + [ownership] + (->> ownership + (reduce (fn [acc {:keys [address timestamp] :as owner}] + (let [existing-owner (get acc address)] + (if (or (nil? existing-owner) (> timestamp (:timestamp existing-owner))) + (assoc acc address owner) + acc))) + {}) + vals)) + +(defn group-collectibles-by-ownership-address + [collectible] + (->> (:ownership collectible) + (map (fn [{:keys [address]}] + [address collectible])) + (into {}))) + +(defn sort-collectibles-by-name + [collectibles] + (sort-by (fn [collectible] + (let [name (-> collectible :collectible-data :name) + normalized-name (some-> name + string/lower-case)] + [(if (seq normalized-name) 0 1) normalized-name])) + collectibles)) diff --git a/src/status_im/contexts/wallet/collectible/utils_test.cljs b/src/status_im/contexts/wallet/collectible/utils_test.cljs new file mode 100644 index 00000000000..8cb98ebd380 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/utils_test.cljs @@ -0,0 +1,95 @@ +(ns status-im.contexts.wallet.collectible.utils-test + (:require + [cljs.test :refer [deftest is testing]] + [status-im.contexts.wallet.collectible.utils :as utils])) + +(def token-id "0xT") +(def contract-address "0xC") + +(def mainnet-name :mainnet) +(def arbitrum-name :arbitrum) + +(deftest get-opensea-collectible-url-test + (testing "get-opensea-collectible-url mainnet" + (is (= (utils/get-opensea-collectible-url {:network-name mainnet-name + :contract-address contract-address + :token-id token-id}) + "https://opensea.io/assets/ethereum/0xC/0xT"))) + + (testing "get-opensea-collectible-url mainnet arbitrum" + (is (= (utils/get-opensea-collectible-url {:network-name arbitrum-name + :contract-address contract-address + :token-id token-id}) + "https://opensea.io/assets/arbitrum/0xC/0xT"))) + + (testing "get-opensea-collectible-url sepolia" + (is (= (utils/get-opensea-collectible-url {:network-name mainnet-name + :contract-address contract-address + :token-id token-id + :test-networks-enabled? true}) + "https://testnets.opensea.io/assets/sepolia/0xC/0xT"))) + + (testing "get-opensea-collectible-url sepolia arbitrum" + (is (= (utils/get-opensea-collectible-url {:network-name arbitrum-name + :contract-address contract-address + :token-id token-id + :test-networks-enabled? true}) + "https://testnets.opensea.io/assets/arbitrum-sepolia/0xC/0xT")))) + +(deftest collectible-balance-test + (testing "Returns balance for a specific address" + (let [collectible {:ownership [{:address "0x123" :balance "10"} + {:address "0x456" :balance "20"}]}] + (is (= 10 (utils/collectible-balance collectible "0x123"))) + (is (= 20 (utils/collectible-balance collectible "0x456"))) + (is (= 0 (utils/collectible-balance collectible "0x789"))))) + + (testing "Returns total balance when no address is provided" + (let [collectible {:ownership [{:address "0x123" :balance "10"} + {:address "0x456" :balance "20"}]}] + (is (= 30 (utils/collectible-balance collectible nil))))) + + (testing "Returns 0 when ownership is empty" + (let [collectible {:ownership []}] + (is (= 0 (utils/collectible-balance collectible nil))))) + + (testing "Handles nil balance values correctly" + (let [collectible {:ownership [{:address "0x123" :balance nil} + {:address "0x456" :balance nil}]}] + (is (= 0 (utils/collectible-balance collectible nil)))))) + +(deftest group-collectibles-by-ownership-address-test + (testing "Groups collectibles by ownership address correctly" + (let [collectible {:id {:contract-id {:chain-id 10 :address "0xContract"} :token-id "1"} + :ownership [{:address "0x123" :balance 10} + {:address "0x456" :balance 20}]} + expected {"0x123" collectible + "0x456" collectible}] + (is (= expected (utils/group-collectibles-by-ownership-address collectible))))) + + (testing "Returns an empty map when there is no ownership data" + (let [collectible {:id {:contract-id {:chain-id 10 :address "0xContract"} :token-id "1"} + :ownership []}] + (is (= {} (utils/group-collectibles-by-ownership-address collectible))))) + + (testing "Handles duplicate ownership addresses" + (let [collectible {:id {:contract-id {:chain-id 10 :address "0xContract"} :token-id "1"} + :ownership [{:address "0x123" :balance 10} + {:address "0x123" :balance 5}]} + expected {"0x123" collectible}] + (is (= expected (utils/group-collectibles-by-ownership-address collectible)))))) + +(deftest sort-collectibles-by-name-test + (testing "Sorts collectibles by name, moving nil or empty names to the end" + (let [collectibles [{:collectible-data {:name "Alpha"}} + {:collectible-data {:name nil}} + {:collectible-data {:name "Beta"}} + {:collectible-data {:name ""}} + {:collectible-data {:name "Zeta"}}] + sorted-collectibles (utils/sort-collectibles-by-name collectibles) + expected [{:collectible-data {:name "Alpha"}} + {:collectible-data {:name "Beta"}} + {:collectible-data {:name "Zeta"}} + {:collectible-data {:name nil}} + {:collectible-data {:name ""}}]] + (is (= sorted-collectibles expected))))) diff --git a/src/status_im/contexts/wallet/collectible/view.cljs b/src/status_im/contexts/wallet/collectible/view.cljs new file mode 100644 index 00000000000..43479367092 --- /dev/null +++ b/src/status_im/contexts/wallet/collectible/view.cljs @@ -0,0 +1,275 @@ +(ns status-im.contexts.wallet.collectible.view + (:require + [oops.core :as oops] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.contexts.wallet.collectible.options.view :as options-drawer] + [status-im.contexts.wallet.collectible.style :as style] + [status-im.contexts.wallet.collectible.tabs.view :as tabs] + [status-im.contexts.wallet.collectible.utils :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.worklets.header-animations :as header-animations])) + +(defn header + [collectible-name collection-name collection-image-url set-title-ref] + [rn/view + {:style style/header + :ref set-title-ref + :collapsable false} + [quo/text + {:weight :semi-bold + :size :heading-1} + collectible-name] + [rn/view {:style style/collection-container} + [rn/view {:style style/collection-avatar-container} + [quo/collection-avatar {:image collection-image-url}]] + [quo/text + {:weight :semi-bold + :size :paragraph-1} + collection-name]]]) + +(defn cta-buttons + [{:keys [chain-id token-id contract-address collectible watch-only?]}] + (let [theme (quo.context/use-theme) + on-press-send (rn/use-callback + (fn [] + (rf/dispatch [:wallet/clean-send-data]) + (rf/dispatch + [:wallet/set-collectible-to-send + {:collectible collectible + :start-flow? true + :current-screen :screen/wallet.collectible}])))] + [rn/view {:style style/buttons-container} + (when-not watch-only? + [quo/button + {:container-style style/send-button + :type :outline + :size 40 + :icon-left :i/send + :on-press on-press-send} + (i18n/label :t/send)]) + [quo/button + {:container-style style/opensea-button + :type :outline + :size 40 + :on-press (fn [] + (rf/dispatch [:wallet/navigate-to-opensea chain-id token-id + contract-address])) + :icon-left :i/opensea + :icon-left-color (colors/theme-colors colors/neutral-100 colors/neutral-40 theme) + :icon-right :i/external + :icon-right-color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)} + (i18n/label :t/opensea)]])) + +(def tabs-data + [{:id :overview + :label (i18n/label :t/overview) + :accessibility-label :overview-tab} + {:id :about + :label (i18n/label :t/about) + :accessibility-label :about-tab}]) + +(defn navigate-back-and-clear-collectible + [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:wallet/clear-collectible-details])) + +(defn animated-header + [{:keys [scroll-amount title-opacity page-nav-type theme]}] + (let [blur-amount (header-animations/use-blur-amount scroll-amount) + layer-opacity (header-animations/use-layer-opacity + scroll-amount + (colors/theme-colors colors/white-opa-0 + colors/neutral-95-opa-0 + theme) + (colors/theme-colors colors/white-opa-50 + colors/neutral-95-opa-70-blur + theme)) + {{preview-uri :uri} :preview-url + {title :name} :collectible-data + {description :name} :collection-data + id :id} (rf/sub [:wallet/collectible-details])] + [rn/view {:style (style/animated-header)} + [reanimated/blur-view + {:style {:flex 1 + :background-color (when platform/android? + (colors/theme-colors colors/white colors/neutral-80 theme))} + :blur-type :transparent + :overlay-color :transparent + :blur-amount blur-amount + :blur-radius blur-amount} + [reanimated/view {:style layer-opacity} + [quo/page-nav + {:type page-nav-type + :picture preview-uri + :title title + :description description + :background :blur + :icon-name :i/close + :accessibility-label :back-button + :on-press navigate-back-and-clear-collectible + :right-side [{:icon-name :i/options + :on-press #(rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [options-drawer/view + {:name title + :image preview-uri + :id id}]) + :theme theme}])}] + :center-opacity title-opacity}]]]])) + +(defn on-scroll + [e scroll-amount title-opacity title-bottom-coord] + (let [scroll-y (oops/oget e "nativeEvent.contentOffset.y") + new-opacity (if (>= scroll-y @title-bottom-coord) 1 0)] + (reanimated/set-shared-value scroll-amount scroll-y) + (reanimated/set-shared-value title-opacity + (reanimated/with-timing new-opacity #js {:duration 300})))) + +(defn- gradient-layer + [image-uri] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/gradient-layer} + [rn/image + {:style style/image-background + :source {:uri image-uri} + :blur-radius 14}] + [rn/view {:style style/gradient} + [linear-gradient/linear-gradient + {:style {:flex 1} + :colors (colors/theme-colors + [colors/white-opa-70 colors/white colors/white] + [colors/neutral-95-opa-70 colors/neutral-95 colors/neutral-95] + theme) + :locations [0 0.9 1]}]]])) + +(defn collectible-details + [_] + (let [selected-tab (reagent/atom :overview) + on-tab-change #(reset! selected-tab %)] + (fn [{:keys [set-title-bottom theme]}] + (let [title-ref (rn/use-ref-atom nil) + set-title-ref (rn/use-callback #(reset! title-ref %)) + animation-shared-element-id (rf/sub [:animation-shared-element-id]) + collectible (rf/sub [:wallet/collectible-details]) + collectible-owner (rf/sub [:wallet/current-viewing-account]) + aspect-ratio (rf/sub [:wallet/collectible-aspect-ratio]) + gradient-color (rf/sub [:wallet/collectible-gradient-color]) + total-owned (:total-owned collectible) + {:keys [id + preview-url + collection-data + collectible-data]} collectible + {svg? :svg? + preview-uri :uri + :or {preview-uri ""}} preview-url + token-id (:token-id id) + chain-id (get-in id [:contract-id :chain-id]) + contract-address (get-in id [:contract-id :address]) + {collection-image :image-url + collection-name :name} collection-data + {collectible-name :name} collectible-data + collectible-image {:image preview-uri + :image-width 300 + ; collectibles don't have width/height + ; but we need to pass something + ; without it animation doesn't work smoothly + ; and :border-radius not applied + :image-height 300 + :id token-id + :header collectible-name + :description collection-name}] + [rn/view {:style style/container} + [rn/view + [gradient-layer preview-uri] + [quo/expanded-collectible + {:aspect-ratio aspect-ratio + :image-src preview-uri + :container-style (style/preview-container) + :gradient-color-index gradient-color + :counter (utils/collectible-owned-counter total-owned) + :native-ID (when (= animation-shared-element-id token-id) :shared-element) + :supported-file? (utils/supported-file? (:animation-media-type collectible-data)) + :on-press (fn [] + (if svg? + (js/alert "Can't visualize SVG images in lightbox") + (rf/dispatch + [:lightbox/navigate-to-lightbox + token-id + {:images [collectible-image] + :index 0 + :on-options-press #(rf/dispatch + [:show-bottom-sheet + {:content + (fn [] + [options-drawer/view + {:name collectible-name + :image preview-uri + :id id}])}])}]))) + :on-collectible-load (fn [] + ;; We need to delay the measurement because the + ;; navigation has an animation + (js/setTimeout + #(some-> @title-ref + (oops/ocall "measureInWindow" set-title-bottom)) + 300))}]] + [rn/view {:style (style/background-color theme)} + [header collectible-name collection-name collection-image set-title-ref] + [cta-buttons + {:chain-id chain-id + :token-id token-id + :contract-address contract-address + :watch-only? (:watch-only? collectible-owner) + :collectible collectible}] + [quo/tabs + {:size 32 + :style style/tabs + :scrollable? true + :default-active @selected-tab + :on-change on-tab-change + :data tabs-data}] + [tabs/view + {:selected-tab @selected-tab + :collectible collectible}]]])))) + +(defn- get-title-bottom-y-position + [y-element-position element-height] + (let [{:keys [top]} safe-area/insets + title-height -56] + (+ y-element-position + element-height + title-height + (when platform/ios? (* top -2))))) + +(defn view + [_] + (let [{:keys [top]} safe-area/insets + theme (quo.context/use-theme) + title-bottom-coord (rn/use-ref-atom 0) + set-title-bottom (rn/use-callback + (fn [_ y _ height] + (reset! title-bottom-coord + (get-title-bottom-y-position y height)))) + scroll-amount (reanimated/use-shared-value 0) + title-opacity (reanimated/use-shared-value 0)] + [rn/view {:style (style/background-color theme)} + [animated-header + {:scroll-amount scroll-amount + :title-opacity title-opacity + :page-nav-type :title-description + :theme theme}] + [reanimated/scroll-view + {:style (style/scroll-view top) + :on-scroll #(on-scroll % scroll-amount title-opacity title-bottom-coord)} + [collectible-details + {:set-title-bottom set-title-bottom + :theme theme}]]])) diff --git a/src/status_im/contexts/wallet/common/account_switcher/view.cljs b/src/status_im/contexts/wallet/common/account_switcher/view.cljs new file mode 100644 index 00000000000..7410755c1c0 --- /dev/null +++ b/src/status_im/contexts/wallet/common/account_switcher/view.cljs @@ -0,0 +1,65 @@ +(ns status-im.contexts.wallet.common.account-switcher.view + (:require + [quo.core :as quo] + [status-im.contexts.wallet.sheets.account-options.view :as account-options] + [status-im.contexts.wallet.sheets.network-filter.view :as network-filter] + [status-im.contexts.wallet.sheets.select-account.view :as select-account] + [status-im.feature-flags :as ff] + [utils.re-frame :as rf])) + +(defn get-bottom-sheet-args + [switcher-type params] + (case switcher-type + :account-options {:content account-options/view + :hide-handle? true} + :select-account {:content (fn [] + [select-account/view params])} + nil)) + +(defn- on-switcher-press + [switcher-type params] + (rf/dispatch [:show-bottom-sheet (get-bottom-sheet-args switcher-type params)])) + +(defn view + [{:keys [type on-press accessibility-label icon-name switcher-type margin-top params + show-dapps-button?] + :or {icon-name :i/close + accessibility-label :top-bar + switcher-type :account-options + show-dapps-button? false + type :no-title}}] + (let [{:keys [color emoji watch-only?]} (rf/sub [:wallet/current-viewing-account]) + networks (rf/sub [:wallet/filtered-networks]) + sending-collectible? (rf/sub [:wallet/sending-collectible?]) + show-new-chain-indicator? (rf/sub [:wallet/show-new-chain-indicator?]) + networks-filtered? (rf/sub [:wallet/network-filter?]) + on-press-networks (fn [] + (if networks-filtered? + (rf/dispatch [:wallet/reset-network-balances-filter]) + (do (rf/dispatch [:wallet/mark-new-networks-as-seen]) + (rf/dispatch [:show-bottom-sheet + {:content network-filter/view}]))))] + [quo/page-nav + {:type type + :icon-name icon-name + :margin-top margin-top + :background :blur + :on-press on-press + :accessibility-label accessibility-label + :networks networks + :networks-filtered? networks-filtered? + :align-center? true + :networks-on-press on-press-networks + :show-new-chain-indicator? show-new-chain-indicator? + :right-side [(when (and (ff/enabled? ::ff/wallet.wallet-connect) + (not watch-only?) + show-dapps-button?) + {:icon-name :i/dapps + :on-press #(rf/dispatch [:navigate-to + :screen/wallet.connected-dapps])}) + (when-not sending-collectible? + {:content-type :account-switcher + :customization-color color + :on-press #(on-switcher-press switcher-type params) + :emoji emoji + :type (when watch-only? :watch-only)})]}])) diff --git a/src/status_im/contexts/wallet/common/activity_tab/activity_types/view.cljs b/src/status_im/contexts/wallet/common/activity_tab/activity_types/view.cljs new file mode 100644 index 00000000000..3838e1092ac --- /dev/null +++ b/src/status_im/contexts/wallet/common/activity_tab/activity_types/view.cljs @@ -0,0 +1,104 @@ +(ns status-im.contexts.wallet.common.activity-tab.activity-types.view + (:require [quo.core :as quo] + [status-im.contexts.wallet.common.activity-tab.options.view :as activity-options] + [utils.re-frame :as rf])) + +;; Common helpers +(defn- open-options + [transactions] + (rf/dispatch + [:show-bottom-sheet + {:content #(activity-options/view (first transactions))}])) + +(defn- common-activity-props + [{:keys [tx-type relative-date status transactions]}] + {:transaction tx-type + :timestamp relative-date + :status status + :blur? false + :on-press-options #(open-options transactions)}) + +(defn- network-tag + [{:keys [network-name network-logo]}] + {:type :network + :network-name network-name + :network-logo network-logo}) + +(defn- collectible-tag + [{:keys [nft-url nft-name amount token-id]}] + {:type :collectible + :collectible nft-url + :collectible-name (if (> amount 1) + (str amount " " nft-name) + nft-name) + :collectible-number (when (not= token-id "0") + token-id)}) + +(defn- token-tag + [{:keys [token amount]}] + {:type :token + :token token + :amount amount}) + +;; Activity type views +(defn send-activity + [{:keys [symbol-out amount-out token-id sender-tag recipient-tag network-name-out network-logo-out] + :as activity-data}] + (let [base-props (common-activity-props activity-data) + first-tag (if token-id + (collectible-tag activity-data) + (token-tag {:token symbol-out :amount amount-out}))] + [quo/wallet-activity + (assoc base-props + :first-tag first-tag + :second-tag-prefix :t/from + :second-tag sender-tag + :third-tag-prefix :t/to + :third-tag recipient-tag + :fourth-tag-prefix :t/on + :fourth-tag (network-tag {:network-name network-name-out + :network-logo network-logo-out}))])) + +(defn bridge-activity + [{:keys [symbol-out amount-out network-name-out network-logo-out network-name-in network-logo-in + sender-tag] + :as activity-data}] + [quo/wallet-activity + (assoc (common-activity-props activity-data) + :first-tag (token-tag {:token symbol-out :amount amount-out}) + :second-tag-prefix :t/from + :second-tag (network-tag {:network-name network-name-out + :network-logo network-logo-out}) + :third-tag-prefix :t/to + :third-tag (network-tag {:network-name network-name-in + :network-logo network-logo-in}) + :fourth-tag-prefix :t/in + :fourth-tag sender-tag)]) + +(defn swap-activity + [{:keys [symbol-out amount-out symbol-in amount-in sender-tag network-name-out network-logo-out] + :as activity-data}] + [quo/wallet-activity + (assoc (common-activity-props activity-data) + :first-tag (token-tag {:token symbol-out :amount amount-out}) + :second-tag-prefix :t/to + :second-tag (token-tag {:token symbol-in :amount amount-in}) + :third-tag-prefix :t/in + :third-tag sender-tag + :fourth-tag-prefix :t/on + :fourth-tag (network-tag {:network-name network-name-out + :network-logo network-logo-out}))]) + +(defn approval-activity + [{:keys [symbol-out amount-out sender-tag spender-tag network-name-out network-logo-out] + :as activity-data}] + [quo/wallet-activity + (assoc (common-activity-props activity-data) + :first-tag (token-tag {:token symbol-out :amount amount-out}) + :second-tag-prefix :t/in + :second-tag sender-tag + :third-tag-prefix :t/for + :third-tag spender-tag + :fourth-tag-prefix :t/on + :fourth-tag (network-tag {:network-name network-name-out + :network-logo network-logo-out}))]) diff --git a/src/status_im/contexts/wallet/common/activity_tab/constants.cljs b/src/status_im/contexts/wallet/common/activity_tab/constants.cljs new file mode 100644 index 00000000000..67da50f6726 --- /dev/null +++ b/src/status_im/contexts/wallet/common/activity_tab/constants.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.wallet.common.activity-tab.constants) + +(def ^:const wallet-activity-type-send 0) +(def ^:const wallet-activity-type-receive 1) +(def ^:const wallet-activity-type-swap 3) +(def ^:const wallet-activity-type-bridge 4) +(def ^:const wallet-activity-type-approval 7) + +(def ^:const wallet-activity-status-failed 0) +(def ^:const wallet-activity-status-pending 1) +(def ^:const wallet-activity-status-confirmed 2) +(def ^:const wallet-activity-status-finalised 3) + +(def ^:const wallet-activity-token-type-erc-721 2) +(def ^:const wallet-activity-token-type-erc-1155 3) + +(def ^:const wallet-activity-status->name + {wallet-activity-status-failed :failed + wallet-activity-status-pending :pending + wallet-activity-status-confirmed :confirmed + wallet-activity-status-finalised :finalised}) diff --git a/src/status_im/contexts/wallet/common/activity_tab/events.cljs b/src/status_im/contexts/wallet/common/activity_tab/events.cljs new file mode 100644 index 00000000000..65a0b11ae47 --- /dev/null +++ b/src/status_im/contexts/wallet/common/activity_tab/events.cljs @@ -0,0 +1,127 @@ +(ns status-im.contexts.wallet.common.activity-tab.events + (:require [camel-snake-kebab.extras :as cske] + [status-im.contexts.wallet.networks.db :as networks.db] + [utils.collection :as collection] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(def ^:const limit-per-request 20) + +(defn- nested-merge + [& maps] + (apply merge-with merge maps)) + +(defn- create-default-filters + [] + {:period {:startTimestamp 0 + :endTimestamp 0} + :types [] + :statuses [] + :counterpartyAddresses [] + :assets [] + :collectibles [] + :filterOutAssets false + :filterOutCollectibles false}) + +(rf/reg-event-fx + :wallet/store-session-id-for-activity-filter-session + (fn [{:keys [db]} [request-id]] + {:db (assoc-in db [:wallet :ui :activity-tab :request :request-id] request-id)})) + +(rf/reg-event-fx + :wallet/fetch-activities-for-current-account + (fn [{:keys [db]}] + (let [address (get-in db [:wallet :current-viewing-account-address]) + chain-ids (networks.db/get-active-chain-ids db) + params [[address] chain-ids (create-default-filters) limit-per-request]] + {:db (-> db + (update-in [:wallet :ui :activity-tab :request] dissoc :request-id) + (update-in [:wallet :ui :activity-tab :request] + assoc + :address address + :loading? true + :remove-all-previous-activites? true)) + :fx [[:json-rpc/call + [{:method "wallet_startActivityFilterSessionV2" + :params params + :on-success [:wallet/store-session-id-for-activity-filter-session] + :on-error [:wallet/log-rpc-error + {:event :wallet/fetch-activities-for-current-account + :params params}]}]]]}))) + +(rf/reg-event-fx + :wallet/stop-activity-filter-session + (fn [{:keys [db]}] + (when-let [session-id (get-in db [:wallet :ui :activity-tab :request :request-id])] + {:db (update-in db [:wallet :ui :activity-tab] dissoc :request) + :fx [[:json-rpc/call + [{:method "wallet_stopActivityFilterSession" + :params [session-id] + :on-error [:wallet/log-rpc-error + {:event :wallet/stop-activity-filter-session + :params [session-id]}]}]]]}))) + +(rf/reg-event-fx + :wallet/get-more-for-activities-filter-session + (fn [{:keys [db]}] + (let [session-id (get-in db [:wallet :ui :activity-tab :request :request-id]) + has-more? (get-in db [:wallet :ui :activity-tab :request :has-more?]) + params [session-id limit-per-request]] + (when (and session-id has-more?) + {:db (assoc-in db [:wallet :ui :activity-tab :request :remove-all-existing-activities?] false) + :fx [[:json-rpc/call + [{:method "wallet_getMoreForActivityFilterSession" + :params params + :on-error [:wallet/log-rpc-error + {:event :wallet/get-more-for-activities-filter-session + :params params}]}]]]})))) + +(rf/reg-event-fx + :wallet/reset-activities-filter-session + (fn [{:keys [db]}] + (when-let [session-id (get-in db [:wallet :ui :activity-tab :request :request-id])] + {:db (assoc-in db [:wallet :ui :activity-tab :request :remove-all-existing-activities?] true) + :fx [[:json-rpc/call + [{:method "wallet_resetActivityFilterSession" + :params [session-id limit-per-request] + :on-error [:wallet/log-rpc-error + {:event :wallet/reset-activities-filter-session + :params [session-id limit-per-request]}]}]]]}))) + +(rf/reg-event-fx + :wallet/activity-filtering-for-current-account-done + (fn [{:keys [db]} [{:keys [message]}]] + (let [{:keys [address + remove-all-existing-activities?]} (get-in db [:wallet :ui :activity-tab :request]) + {:keys [activities offset hasMore]} (transforms/json->clj message) + new-activities (->> activities + (cske/transform-keys + transforms/->kebab-case-keyword) + (collection/index-by :key)) + existing-activities (get-in db [:wallet :activities address]) + updated-activities (if remove-all-existing-activities? + new-activities + (nested-merge existing-activities new-activities))] + {:db (-> db + (assoc-in [:wallet :activities address] updated-activities) + (assoc-in [:wallet :ui :activity-tab :request :offset] offset) + (assoc-in [:wallet :ui :activity-tab :request :has-more?] hasMore) + (assoc-in [:wallet :ui :activity-tab :request :loading?] false))}))) + +(rf/reg-event-fx + :wallet/activities-filtering-entries-updated + (fn [{:keys [db]} [{:keys [message requestId]}]] + (when (= requestId (get-in db [:wallet :ui :activity-tab :request :request-id])) + (let [address (get-in db [:wallet :ui :activity-tab :request :address]) + updated-activities (->> message + transforms/json->clj + (cske/transform-keys transforms/->kebab-case-keyword) + (collection/index-by :key))] + {:db (update-in db [:wallet :activities address] nested-merge updated-activities)})))) + +(rf/reg-event-fx + :wallet/activities-session-updated + (fn [_ [{:keys [message]}]] + (let [{:keys [hasNewOnTop]} (transforms/json->clj message)] + (when hasNewOnTop + {:fx [[:dispatch [:wallet/reset-activities-filter-session]]]})))) diff --git a/src/status_im/contexts/wallet/common/activity_tab/options/view.cljs b/src/status_im/contexts/wallet/common/activity_tab/options/view.cljs new file mode 100644 index 00000000000..97bf034a31e --- /dev/null +++ b/src/status_im/contexts/wallet/common/activity_tab/options/view.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.wallet.common.activity-tab.options.view + (:require [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [status-im.contexts.wallet.networks.core :as networks] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [chain-id] + tx-hash :hash}] + (let [{:keys [block-explorer-name] :as network} (rf/sub [:wallet/network-by-id chain-id]) + tx-details-link-on-block-explorer (networks/get-block-explorer-tx-url network tx-hash) + open-tx-on-block-explorer (rn/use-callback + #(rf/dispatch [:browser.ui/open-url + tx-details-link-on-block-explorer]) + [tx-details-link-on-block-explorer]) + copy-tx-hash-to-clipboard (rn/use-callback + (fn [] + (clipboard/set-string tx-hash) + (rf/dispatch + [:toasts/upsert + {:type :positive + :text + (i18n/label + :t/transaction-hash-copied-to-clipboard)}])) + [tx-hash]) + share-link-to-block-explorer (rn/use-callback + #(rf/dispatch [:open-share + {:options + {:message + tx-details-link-on-block-explorer}}]) + [tx-details-link-on-block-explorer])] + [quo/action-drawer + [[{:icon :i/link + :accessibility-label :view-on-block-explorer + :on-press open-tx-on-block-explorer + :label (i18n/label :t/view-on-block-explorer + {:block-explorer-name block-explorer-name}) + :right-icon :i/external}] + [{:icon :i/copy + :accessibility-label :copy-transaction-hash + :label (i18n/label :t/copy-transaction-hash) + :on-press copy-tx-hash-to-clipboard}] + [{:icon :i/share + :accessibility-label :share-link-to-block-explorer + :label (i18n/label :t/share-link-to-block-explorer + {:block-explorer-name block-explorer-name}) + :on-press share-link-to-block-explorer}]]])) diff --git a/src/status_im/contexts/wallet/common/activity_tab/view.cljs b/src/status_im/contexts/wallet/common/activity_tab/view.cljs new file mode 100644 index 00000000000..84d56420057 --- /dev/null +++ b/src/status_im/contexts/wallet/common/activity_tab/view.cljs @@ -0,0 +1,58 @@ +(ns status-im.contexts.wallet.common.activity-tab.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.shell.constants :as shell.constants] + [status-im.contexts.wallet.common.activity-tab.activity-types.view :as activity-type] + [status-im.contexts.wallet.common.empty-tab.view :as empty-tab] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- section-header + [{:keys [title]}] + [quo/divider-date title]) + +(defn- activity-item + [{:keys [tx-type] :as activity}] + (case tx-type + :send [activity-type/send-activity activity] + :bridge [activity-type/bridge-activity activity] + :swap [activity-type/swap-activity activity] + :approval [activity-type/approval-activity activity] + nil)) + +(defn view + [] + (let [theme (quo.context/use-theme) + loading? (rf/sub [:wallet/activity-tab-loading?]) + activity-list (rf/sub [:wallet/activities-for-current-viewing-account]) + on-end-reached (rn/use-callback + #(rf/dispatch + [:wallet/get-more-for-activities-filter-session]))] + (cond + (and (empty? activity-list) loading?) + [quo/skeleton-list + {:content :messages + :parent-height (:height (rn/get-window)) + :animated? false}] + + (and (empty? activity-list) (false? loading?)) + [empty-tab/view + {:title (i18n/label :t/no-activity) + :description (i18n/label :t/empty-tab-description) + :image (resources/get-themed-image :no-activity theme)}] + + :else + [rn/section-list + {:sections activity-list + :sticky-section-headers-enabled false + :style {:flex 1 + :padding-horizontal 8} + :content-container-style {:padding-bottom shell.constants/floating-shell-button-height} + :shows-vertical-scroll-indicator false + :render-fn activity-item + :render-section-header-fn section-header + :on-end-reached on-end-reached + :on-end-reached-threshold 2}]))) diff --git a/src/status_im/contexts/wallet/common/asset_list/view.cljs b/src/status_im/contexts/wallet/common/asset_list/view.cljs new file mode 100644 index 00000000000..dfe33826425 --- /dev/null +++ b/src/status_im/contexts/wallet/common/asset_list/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.wallet.common.asset-list.view + (:require + [quo.core :as quo] + [react-native.gesture :as gesture] + [status-im.contexts.wallet.common.utils :as utils] + [utils.re-frame :as rf])) + +(defn- asset-component + [{token-symbol :symbol + token-name :name + total-balance :total-balance + disabled? :disabled? + fiat-value :fiat-value + :as token} + _ _ + {:keys [currency-symbol on-token-press preselected-token-symbol prices-per-token]}] + (let [crypto-formatted (utils/get-standard-crypto-format token total-balance prices-per-token) + fiat-formatted (utils/fiat-formatted-for-ui currency-symbol fiat-value)] + [quo/token-network + {:token token-symbol + :label token-name + :token-value (str crypto-formatted " " token-symbol) + :fiat-value fiat-formatted + :networks (seq (:networks token)) + :on-press #(on-token-press token) + :state (cond + disabled? :disabled + (= preselected-token-symbol token-symbol) + :selected)}])) + +(defn view + [{:keys [content-container-style search-text on-token-press preselected-token-symbol chain-ids] + :or {content-container-style {:padding-horizontal 8}}}] + (let [filtered-tokens (rf/sub [:wallet/current-viewing-account-tokens-filtered + {:query search-text + :chain-ids chain-ids}]) + currency-symbol (rf/sub [:profile/currency-symbol]) + prices-per-token (rf/sub [:wallet/prices-per-token])] + [gesture/flat-list + {:data filtered-tokens + :render-data {:currency-symbol currency-symbol + :on-token-press on-token-press + :preselected-token-symbol preselected-token-symbol + :prices-per-token prices-per-token} + :style {:flex 1} + :content-container-style content-container-style + :keyboard-should-persist-taps :handled + :key-fn :symbol + :on-scroll-to-index-failed identity + :render-fn asset-component}])) diff --git a/src/status_im/contexts/wallet/common/collectibles_tab/style.cljs b/src/status_im/contexts/wallet/common/collectibles_tab/style.cljs new file mode 100644 index 00000000000..098d8b0d15d --- /dev/null +++ b/src/status_im/contexts/wallet/common/collectibles_tab/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.wallet.common.collectibles-tab.style + (:require [status-im.contexts.shell.constants :as constants])) + +(def list-container-style + {:margin-horizontal 12 + :padding-bottom constants/floating-shell-button-height}) + +(def collectible-container + {:padding 8 + :flex 0.5}) diff --git a/src/status_im/contexts/wallet/common/collectibles_tab/view.cljs b/src/status_im/contexts/wallet/common/collectibles_tab/view.cljs new file mode 100644 index 00000000000..f9f046ed63f --- /dev/null +++ b/src/status_im/contexts/wallet/common/collectibles_tab/view.cljs @@ -0,0 +1,106 @@ +(ns status-im.contexts.wallet.common.collectibles-tab.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.wallet.collectible.utils :as utils] + [status-im.contexts.wallet.common.collectibles-tab.style :as style] + [status-im.contexts.wallet.common.empty-tab.view :as empty-tab] + [utils.i18n :as i18n])) + +(defn- loading-collectible-item + [_ index] + (let [gradient-color (keyword (str "gradient-" (inc (mod index 5))))] + [quo/collectible-list-item + {:type :card + :gradient-color-index gradient-color + :supported-file? true + :loading? true + :container-style style/collectible-container}])) + +(defn- collectible-item + [{:keys [preview-url collection-data collectible-data total-owned on-press on-long-press] + :as collectible} + index] + (let [gradient-color (keyword (str "gradient-" (inc (mod index 5)))) + on-press-fn (rn/use-callback + (fn [aspect-ratio] + (when (fn? on-press) + (on-press {:collectible (dissoc collectible :on-press :on-long-press) + :aspect-ratio aspect-ratio + :gradient-color gradient-color})))) + on-long-press-fn (rn/use-callback + (fn [aspect-ratio] + (when on-long-press + (on-long-press + (dissoc collectible :on-press :on-long-press) + aspect-ratio))))] + [quo/collectible-list-item + {:type :card + :image-src (:uri preview-url) + :avatar-image-src (when (utils/supported-file? (:image-url collection-data)) + (:image-url collection-data)) + :collectible-name (:name collectible-data) + :supported-file? (utils/supported-file? (:animation-media-type collectible-data)) + :gradient-color-index gradient-color + :counter (utils/collectible-owned-counter total-owned) + :container-style style/collectible-container + :on-press on-press-fn + :on-long-press on-long-press-fn}])) + +(defn view + [{:keys [collectibles filtered? on-end-reached on-collectible-press + current-account-address on-collectible-long-press loading?]}] + (let [theme (quo.context/use-theme) + no-results-match-query? (and filtered? (empty? collectibles))] + (cond + loading? + [rn/flat-list + {:data (repeat 8 {}) + :style {:flex 1} + :content-container-style style/list-container-style + :num-columns 2 + :render-fn loading-collectible-item}] + + no-results-match-query? + [rn/view {:style {:flex 1 :justify-content :center}} + [quo/empty-state + {:title (i18n/label :t/nothing-found) + :description (i18n/label :t/try-to-search-something-else) + :image (resources/get-themed-image :no-collectibles theme)}]] + + (empty? collectibles) + [empty-tab/view + {:title (i18n/label :t/no-collectibles) + :description (i18n/label :t/no-collectibles-description) + :image (resources/get-themed-image :no-collectibles theme)}] + + :else + ;; TODO: https://github.com/status-im/status-mobile/issues/20137 + ;; 1. If possible, move `collectibles-data` calculation to a subscription + ;; 2. Optimization: do not recalculate all the collectibles, process only the new ones + (let [collectibles-data (map + (fn [collectible] + (let [total-owned (utils/collectible-balance collectible + current-account-address)] + (assoc collectible + :total-owned total-owned + :on-long-press on-collectible-long-press + :on-press on-collectible-press))) + collectibles)] + [rn/flat-list + {:data collectibles-data + :style {:flex 1} + :content-container-style style/list-container-style + :window-size 11 + :num-columns 2 + :render-fn collectible-item + :on-end-reached on-end-reached + :key-fn :unique-id + :header [quo/information-box + {:type :default + :icon :i/info + :style {:margin-horizontal 10}} + (i18n/label :t/bsc-collectibles-not-supported)] + :on-end-reached-threshold 4}])))) diff --git a/src/status_im/contexts/wallet/common/empty_tab/style.cljs b/src/status_im/contexts/wallet/common/empty_tab/style.cljs new file mode 100644 index 00000000000..e43773b853c --- /dev/null +++ b/src/status_im/contexts/wallet/common/empty_tab/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.wallet.common.empty-tab.style) + +(def empty-container-style + {:justify-content :center + :flex 1 + :margin-bottom 44}) diff --git a/src/status_im/contexts/wallet/common/empty_tab/view.cljs b/src/status_im/contexts/wallet/common/empty_tab/view.cljs new file mode 100644 index 00000000000..3ee724dbb47 --- /dev/null +++ b/src/status_im/contexts/wallet/common/empty_tab/view.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.wallet.common.empty-tab.view + (:require + [quo.core :as quo] + [status-im.contexts.wallet.common.empty-tab.style :as style])) + +(defn view + [props] + [quo/empty-state + (assoc props :container-style style/empty-container-style)]) diff --git a/src/status_im/contexts/wallet/common/fiat_text/view.cljs b/src/status_im/contexts/wallet/common/fiat_text/view.cljs new file mode 100644 index 00000000000..eed01c5fc0e --- /dev/null +++ b/src/status_im/contexts/wallet/common/fiat_text/view.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.wallet.common.fiat-text.view + (:require [quo.core :as quo] + [status-im.contexts.wallet.common.utils :as utils] + [utils.re-frame :as rf])) + +(defn view + [{:keys [amount] :as text-props}] + (let [currency-symbol (rf/sub [:profile/currency-symbol]) + pretty-amount (utils/prettify-balance currency-symbol amount)] + [quo/text text-props pretty-amount])) diff --git a/src/status_im/contexts/wallet/common/scan_account/view.cljs b/src/status_im/contexts/wallet/common/scan_account/view.cljs new file mode 100644 index 00000000000..4e122cdd2a7 --- /dev/null +++ b/src/status_im/contexts/wallet/common/scan_account/view.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.wallet.common.scan-account.view + (:require + [quo.context] + [status-im.common.scan-qr-code.view :as scan-qr-code] + [utils.address :as utils-address] + [utils.debounce :as debounce] + [utils.i18n :as i18n])) + +(defn view + [] + (let [{:keys [on-result]} (quo.context/use-screen-params)] + [scan-qr-code/view + {:title (i18n/label :t/scan-qr) + :subtitle (i18n/label :t/scan-an-address-qr-code) + :error-message (i18n/label :t/oops-this-qr-does-not-contain-an-address) + :validate-fn #(utils-address/supported-scan-address? %) + :on-success-scan (fn [result] + (let [address (utils-address/supported-address->eth-address result)] + (when on-result (on-result address)) + (debounce/debounce-and-dispatch + [:wallet/scan-address-success address] + 300)))}])) diff --git a/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/style.cljs b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/style.cljs new file mode 100644 index 00000000000..7c86a3539bb --- /dev/null +++ b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/style.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.wallet.common.screen-base.create-or-edit-account.style) + +(def account-avatar-container + {:padding-horizontal 20 + :padding-top 12}) + +(def reaction-button-container + {:position :absolute + :bottom 0 + :left 76}) + +(defn title-input-container + [error?] + {:padding-horizontal 20 + :padding-top 12 + :padding-bottom (if error? 8 16)}) + +(def divider-1 + {:margin-bottom 12}) + +(def section-container + {:padding-horizontal 20 + :padding-bottom 4}) + +(def color-picker-container + {:padding-vertical 8}) + +(def color-picker + {:padding-horizontal 20}) + +(def divider-2 + {:margin-top 4 + :margin-bottom 12}) diff --git a/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs new file mode 100644 index 00000000000..c70787e540f --- /dev/null +++ b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs @@ -0,0 +1,93 @@ +(ns status-im.contexts.wallet.common.screen-base.create-or-edit-account.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.screen-base.create-or-edit-account.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [page-nav-right-side placeholder account-name account-color account-emoji + on-change-name on-change-color on-change-emoji section-label bottom-action-label + bottom-action-props custom-bottom-action watch-only? error]} + & children] + (let [{window-width :width} (rn/get-window) + footer (if custom-bottom-action + custom-bottom-action + [quo/button + (merge {:size 40 + :type :primary} + bottom-action-props) + (i18n/label bottom-action-label)])] + [floating-button-page/view + {:header [quo/page-nav + {:type :no-title + :background :blur + :right-side page-nav-right-side + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + :footer footer + :gradient-cover? true + :footer-container-padding 0 + :header-container-style {:padding-top safe-area/top} + :customization-color account-color} + (into + [:<> + [rn/view {:style style/account-avatar-container} + [quo/account-avatar + {:customization-color account-color + :size 80 + :emoji account-emoji + :type (if watch-only? :watch-only :default)}] + [quo/button + {:size 32 + :type :grey + :background :photo + :icon-only? true + :on-press #(rf/dispatch [:emoji-picker/open {:on-select on-change-emoji}]) + :container-style style/reaction-button-container} + :i/edit]] + + [rn/view + [quo/title-input + {:placeholder placeholder + :max-length constants/wallet-account-name-max-length + :blur? true + :default-value account-name + :auto-focus true + :on-change-text on-change-name + :container-style (style/title-input-container error)}] + (when error + [quo/info-message + {:status :error + :size :default + :icon :i/info + :container-style {:margin-left 20 + :margin-bottom 16}} + (case error + :emoji (i18n/label :t/key-name-error-emoji) + :special-character (i18n/label :t/key-name-error-special-char) + :existing-name (i18n/label :t/name-must-differ-error) + :emoji-and-color (i18n/label :t/emoji-and-colors-unique-error) + nil)])] + + [quo/divider-line {:container-style style/divider-1}] + [quo/section-label + {:section (i18n/label :t/colour) + :container-style style/section-container}] + [rn/view + {:style style/color-picker-container} + [quo/color-picker + {:default-selected account-color + :on-change on-change-color + :container-style style/color-picker + :window-width window-width}]] + [quo/divider-line {:container-style style/divider-2}] + (when section-label + [quo/section-label + {:section (i18n/label section-label) + :container-style style/section-container}])] + children)])) diff --git a/src/status_im/contexts/wallet/common/token_value/view.cljs b/src/status_im/contexts/wallet/common/token_value/view.cljs new file mode 100644 index 00000000000..0f98ce7bfb6 --- /dev/null +++ b/src/status_im/contexts/wallet/common/token_value/view.cljs @@ -0,0 +1,139 @@ +(ns status-im.contexts.wallet.common.token-value.view + (:require [quo.core :as quo] + [status-im.contexts.wallet.send.utils :as send-utils] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) + +(defn- action-buy + [] + {:icon :i/buy + :accessibility-label :buy + :label (i18n/label :t/buy) + :on-press #(rf/dispatch [:show-bottom-sheet + {:content buy-token/view}]) + :right-icon :i/external}) + +(defn- action-send + [{:keys [disabled?] :as params} entry-point] + {:icon :i/send + :accessibility-label :send + :label (i18n/label :t/send) + :disabled? disabled? + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:wallet/clean-send-data]) + (rf/dispatch [:wallet/set-token-to-send params entry-point]))}) + +(defn- action-receive + [selected-account?] + {:icon :i/receive + :accessibility-label :receive + :label (i18n/label :t/receive) + :on-press (if selected-account? + #(rf/dispatch [:open-modal :screen/wallet.share-address {:status :receive}]) + #(rf/dispatch [:open-modal :screen/share-shell {:initial-tab :wallet}]))}) + +(defn- action-bridge + [{:keys [bridge-disabled? testnet-mode?] :as params}] + {:icon :i/bridge + :accessibility-label :bridge + :label (i18n/label :t/bridge) + :disabled? (or testnet-mode? bridge-disabled?) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:wallet/clean-send-data]) + (rf/dispatch [:wallet/bridge-select-token params]))}) + +(defn- action-swap + [{:keys [token token-symbol testnet-mode?]}] + {:icon :i/transaction + :accessibility-label :swap + :label (i18n/label :t/swap) + :disabled? testnet-mode? + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:wallet/clean-send-data]) + (rf/dispatch [:wallet/clean-swap]) + (rf/dispatch [:wallet.swap/start + {:asset-to-pay (or token {:symbol token-symbol}) + :open-new-screen? true}]))}) + +(defn- action-manage-tokens + [watch-only?] + {:icon :i/settings + :accessibility-label :settings + :label (i18n/label :t/manage-tokens) + :on-press #(js/alert "to be implemented") + :add-divider? (not watch-only?)}) + +(defn- action-hide + [] + {:icon :i/hide + :accessibility-label :hide + :label (i18n/label :t/hide) + :on-press #(js/alert "to be implemented")}) + +(defn token-value-drawer + [token watch-only? entry-point] + (let [token-symbol (:token token) + token-data (rf/sub [:wallet/token-by-symbol-from-first-available-account-with-balance + token-symbol]) + selected-account (rf/sub [:wallet/current-viewing-account-address]) + token-owners (rf/sub [:wallet/operable-addresses-with-token-symbol token-symbol]) + testnet-mode? (rf/sub [:profile/test-networks-enabled?]) + account-owns-token? (rf/sub [:wallet/current-account-owns-token token-symbol]) + networks (rf/sub [:wallet/active-networks]) + token-owned? (if selected-account account-owns-token? (seq token-owners)) + unique-owner? (= (count token-owners) 1) + params (cond-> {:start-flow? true + :owners token-owners + :testnet-mode? testnet-mode?} + selected-account + (assoc :token token-data + :stack-id :screen/wallet.accounts + :has-balance? (-> (get-in token [:values :fiat-unformatted-value]) + money/above-zero?)) + (and (not selected-account) unique-owner?) + (assoc :token-symbol token-symbol + :token token-data + :stack-id :screen/wallet-stack) + + (and (not selected-account) (not unique-owner?)) + (assoc :token-symbol token-symbol + :stack-id :screen/wallet-stack))] + [quo/action-drawer + [(cond->> [(when (ff/enabled? ::ff/wallet.assets-modal-manage-tokens) + (action-manage-tokens watch-only?)) + (when (ff/enabled? ::ff/wallet.assets-modal-hide) + (action-hide))] + (not watch-only?) + (concat [(action-buy) + (when token-owned? + (action-send params entry-point)) + (action-receive selected-account) + (when token-owned? + (action-swap params)) + (when token-owned? + (action-bridge (assoc params + :networks + networks + :bridge-disabled? + (send-utils/bridge-disabled? token-symbol))))]))]])) + +(defn view + [item _ _ {:keys [watch-only? entry-point]}] + [quo/token-value + (cond-> item + (or (not watch-only?) (ff/enabled? ::ff/wallet.long-press-watch-only-asset)) + (assoc :on-long-press + (fn [] + (when (= entry-point :screen/wallet-stack) + (rf/dispatch [:wallet/close-account-page]) + (rf/dispatch [:wallet/clean-current-viewing-account])) + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] [token-value-drawer item watch-only? entry-point]) + :selected-item (fn [] [quo/token-value item])}]))))]) diff --git a/src/status_im/contexts/wallet/common/transaction_settings/core.cljs b/src/status_im/contexts/wallet/common/transaction_settings/core.cljs new file mode 100644 index 00000000000..0f28dc8ee46 --- /dev/null +++ b/src/status_im/contexts/wallet/common/transaction_settings/core.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.wallet.common.transaction-settings.core + (:require + [status-im.constants :as constants])) + +(defn tx-fee-mode->gas-rate + [tx-fee-mode] + (case tx-fee-mode + :tx-fee-mode/normal constants/gas-rate-low + :tx-fee-mode/fast constants/gas-rate-medium + :tx-fee-mode/urgent constants/gas-rate-high + :tx-fee-mode/custom constants/gas-rate-custom + constants/gas-rate-medium)) + +(defn gas-rate->tx-fee-mode + [gas-rate] + (condp = gas-rate + constants/gas-rate-low :tx-fee-mode/normal + constants/gas-rate-medium :tx-fee-mode/fast + constants/gas-rate-high :tx-fee-mode/urgent + constants/gas-rate-custom :tx-fee-mode/custom + :tx-fee-mode/fast)) diff --git a/src/status_im/contexts/wallet/common/transaction_settings/gas_amount/view.cljs b/src/status_im/contexts/wallet/common/transaction_settings/gas_amount/view.cljs new file mode 100644 index 00000000000..02340a4da14 --- /dev/null +++ b/src/status_im/contexts/wallet/common/transaction_settings/gas_amount/view.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.wallet.common.transaction-settings.gas-amount.view + (:require + [quo.context] + [status-im.contexts.wallet.common.transaction-settings.view :as transaction-settings] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hint-and-status + [suggested-gas-amount {:keys [low high]} entered-value] + (cond + (> entered-value high) {:hint-text (i18n/label :t/gas-amount-higher + {:high high}) + :status :error} + (> entered-value (* 1.1 suggested-gas-amount)) {:hint-text (i18n/label :t/gas-amount-higher-than + {:current + suggested-gas-amount}) + :status :warning} + + (< entered-value low) {:hint-text (i18n/label :t/gas-amount-lower + {:low low}) + :status :error} + (< entered-value (* 0.9 suggested-gas-amount)) {:hint-text (i18n/label :t/gas-amount-lower-than + {:current + suggested-gas-amount}) + :status :warning} + :else {:hint-text (i18n/label :t/current-units + {:current + suggested-gas-amount}) + :status :default})) + +(defn view + [] + (let [spectrum {:low 21000 + :high 7920027} + gas-amount (rf/sub [:wallet/tx-settings-gas-amount]) + suggested-gas-amount (rf/sub [:wallet/tx-settings-suggested-tx-gas-amount]) + conditions (partial hint-and-status suggested-gas-amount spectrum)] + [transaction-settings/custom-setting-screen + {:screen-title (i18n/label :t/max-gas-amount) + :token-symbol :units + :conditions-fn conditions + :current gas-amount + :info-title (i18n/label :t/gas-amount) + :info-content (i18n/label :t/about-gas-amount) + :on-save (fn [new-val] + (rf/dispatch [:wallet/set-max-gas-amount new-val]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:show-bottom-sheet + {:content transaction-settings/custom-settings-sheet}])) + :with-decimals? false}])) diff --git a/src/status_im/contexts/wallet/common/transaction_settings/gas_price/view.cljs b/src/status_im/contexts/wallet/common/transaction_settings/gas_price/view.cljs new file mode 100644 index 00000000000..c42fce31edc --- /dev/null +++ b/src/status_im/contexts/wallet/common/transaction_settings/gas_price/view.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.wallet.common.transaction-settings.gas-price.view + (:require [status-im.contexts.wallet.common.transaction-settings.view :as transaction-settings] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hint-and-status + [suggested-gas-price current-gas-price {:keys [low high]} entered-value] + (cond + (> entered-value high) {:hint-text (i18n/label :t/gas-price-higher + {:high high}) + :status :error} + (> entered-value suggested-gas-price) {:hint-text (i18n/label :t/gas-price-higher-than + {:current + suggested-gas-price}) + :status :warning} + (< entered-value low) {:hint-text (i18n/label :t/gas-price-lower + {:low low}) + :status :error} + (< entered-value suggested-gas-price) {:hint-text (i18n/label :t/gas-price-lower-than + {:current + suggested-gas-price}) + :status :warning} + :else {:hint-text (i18n/label :t/gas-price-current + {:current + current-gas-price}) + :status :default})) + +(defn view + [] + (let [gas-price (rf/sub [:wallet/tx-settings-gas-price]) + current-gas-price (rf/sub [:wallet/tx-settings-gas-price-route]) + suggested-gas-price (rf/sub [:wallet/tx-settings-suggested-gas-price]) + ;; We allow the user to go low (-10%) or high (+20%) of the suggested gas price. This logic + ;; is aligned with the Desktop. + spectrum {:low (* 0.9 suggested-gas-price) + :high (* 1.2 suggested-gas-price)} + conditions (partial hint-and-status suggested-gas-price current-gas-price spectrum)] + [transaction-settings/custom-setting-screen + {:screen-title (i18n/label :t/gas-price) + :token-sybmol :gwei + :conditions-fn conditions + :current gas-price + :info-title (i18n/label :t/gas-price) + :info-content (i18n/label :t/about-gas-price) + :on-save (fn [new-val] + (rf/dispatch [:wallet/set-gas-price new-val]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:show-bottom-sheet + {:content transaction-settings/custom-settings-sheet}]))}])) diff --git a/src/status_im/contexts/wallet/common/transaction_settings/max_fee/view.cljs b/src/status_im/contexts/wallet/common/transaction_settings/max_fee/view.cljs new file mode 100644 index 00000000000..d39c2d90093 --- /dev/null +++ b/src/status_im/contexts/wallet/common/transaction_settings/max_fee/view.cljs @@ -0,0 +1,45 @@ +(ns status-im.contexts.wallet.common.transaction-settings.max-fee.view + (:require + [quo.context] + [status-im.contexts.wallet.common.transaction-settings.view :as transaction-settings] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hint-and-status + [network-base-fee priority-fee entered-value] + (cond + (> entered-value (* 1.1 network-base-fee)) {:hint-text (i18n/label :t/max-base-fee-higher + {:current + network-base-fee}) + :status :warning} + (< entered-value priority-fee) {:hint-text (i18n/label :t/max-base-fee-lower + {:priority-fee + priority-fee}) + :status :error} + (< entered-value (* 0.9 network-base-fee)) {:hint-text (i18n/label + :t/max-base-fee-lower-recommended + {:current network-base-fee}) + :status :warning} + :else {:hint-text (i18n/label + :t/max-base-fee-current + {:current network-base-fee}) + :status :default})) + +(defn view + [] + (let [network-base-fee (rf/sub [:wallet/tx-settings-network-base-fee-route]) + max-base-fee (rf/sub [:wallet/tx-settings-max-base-fee]) + priority-fee (rf/sub [:wallet/tx-settings-custom-priority-fee]) + conditions (partial hint-and-status network-base-fee priority-fee)] + [transaction-settings/custom-setting-screen + {:screen-title (i18n/label :t/max-base-fee) + :token-symbol :gwei + :conditions-fn conditions + :current max-base-fee + :info-title (i18n/label :t/max-base-fee) + :info-content (i18n/label :t/about-max-base-fee) + :on-save (fn [new-val] + (rf/dispatch [:wallet/set-max-base-fee new-val]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:show-bottom-sheet + {:content transaction-settings/custom-settings-sheet}]))}])) diff --git a/src/status_im/contexts/wallet/common/transaction_settings/nonce/view.cljs b/src/status_im/contexts/wallet/common/transaction_settings/nonce/view.cljs new file mode 100644 index 00000000000..726f3dcfcde --- /dev/null +++ b/src/status_im/contexts/wallet/common/transaction_settings/nonce/view.cljs @@ -0,0 +1,82 @@ +(ns status-im.contexts.wallet.common.transaction-settings.nonce.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.controlled-input.utils :as controlled-input] + [status-im.contexts.wallet.common.transaction-settings.view :as transaction-settings] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [current (rf/sub [:wallet/tx-settings-nonce]) + suggested-nonce (rf/sub [:wallet/tx-settings-suggested-nonce]) + last-tx-nonce (dec suggested-nonce) + [input-state set-input-state] (rn/use-state (controlled-input/set-value-numeric + controlled-input/init-state + current)) + input-value (controlled-input/input-value input-state) + warning? (> (controlled-input/value-numeric input-state) + suggested-nonce)] + [rn/view + {:style {:flex 1}} + [quo/page-nav + {:type :title + :title (i18n/label :t/nonce) + :text-align :center + :right-side [{:icon-name :i/info + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [transaction-settings/info-sheet + (i18n/label :t/nonce) + (i18n/label :t/about-nonce)])}])}] + :icon-name :i/arrow-left + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:show-bottom-sheet + {:content transaction-settings/custom-settings-sheet}]))}] + [rn/view + {:style {:padding-horizontal 20 + :gap 8}} + [quo/input + {:type :text + :label (i18n/label :t/type-nonce) + :label-right (i18n/label :t/last-transaction-is + {:number (if (< last-tx-nonce 0) + "-" + last-tx-nonce)}) + :editable false + :default-value input-value + :clearable? true + :error? warning? + :on-clear (fn [] + (set-input-state controlled-input/delete-all))}] + (when warning? + [quo/text + {:style {:color colors/warning-50} + :weight :regular + :size :paragraph-2} + (i18n/label :t/nonce-higher {:number suggested-nonce})])] + [rn/view {:style {:flex 1}}] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/save-changes) + :button-one-props {:disabled? (controlled-input/empty-value? input-state) + :on-press (fn [] + (rf/dispatch [:wallet/set-nonce + (controlled-input/value-numeric input-state)]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:show-bottom-sheet + {:content + transaction-settings/custom-settings-sheet}]))}}] + [quo/numbered-keyboard + {:container-style {:padding-bottom safe-area/bottom} + :left-action :none + :delete-key? true + :on-press (fn [c] + (set-input-state #(controlled-input/add-character % c))) + :on-delete (fn [] + (set-input-state controlled-input/delete-last)) + :on-long-press-delete (fn [] + (set-input-state controlled-input/delete-all))}]])) diff --git a/src/status_im/contexts/wallet/common/transaction_settings/priority_fee/view.cljs b/src/status_im/contexts/wallet/common/transaction_settings/priority_fee/view.cljs new file mode 100644 index 00000000000..a9919ddfcb2 --- /dev/null +++ b/src/status_im/contexts/wallet/common/transaction_settings/priority_fee/view.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.wallet.common.transaction-settings.priority-fee.view + (:require + [quo.context] + [status-im.contexts.wallet.common.transaction-settings.view :as transaction-settings] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn hint-and-status + [spectrum max-base-fee entered-value] + (let [upper-limit-exceeded? (> entered-value (:high spectrum)) + lower-limit-exceeded? (< entered-value (:low spectrum))] + (cond + (> entered-value max-base-fee) {:hint-text (i18n/label + :t/priority-fee-higher-max-base + {:max-base-fee max-base-fee}) + :status :error} + upper-limit-exceeded? {:hint-text (i18n/label :t/priority-fee-higher + spectrum) + :status :warning} + lower-limit-exceeded? {:hint-text (i18n/label :t/priority-fee-lower spectrum) + :status :warning} + :else {:hint-text (i18n/label :t/priority-fee-current spectrum) + :status :default}))) + +(defn view + [] + (let [priority-fee (rf/sub [:wallet/tx-settings-custom-priority-fee]) + max-base-fee (rf/sub [:wallet/tx-settings-max-base-fee]) + spectrum {:low (rf/sub [:wallet/tx-settings-suggested-min-priority-fee]) + :high (rf/sub [:wallet/tx-settings-suggested-max-priority-fee])} + conditions (partial hint-and-status spectrum max-base-fee)] + [transaction-settings/custom-setting-screen + {:screen-title (i18n/label :t/priority-fee) + :token-symbol :gwei + :conditions-fn conditions + :current priority-fee + :info-title (i18n/label :t/priority-fee) + :info-content (i18n/label :t/about-priority-fee) + :on-save (fn [new-val] + (rf/dispatch [:wallet/set-priority-fee new-val]) + (rf/dispatch [:navigate-back]) + (rf/dispatch [:show-bottom-sheet + {:content transaction-settings/custom-settings-sheet}]))}])) diff --git a/src/status_im/contexts/wallet/common/transaction_settings/view.cljs b/src/status_im/contexts/wallet/common/transaction_settings/view.cljs new file mode 100644 index 00000000000..5ac7ae436c8 --- /dev/null +++ b/src/status_im/contexts/wallet/common/transaction_settings/view.cljs @@ -0,0 +1,288 @@ +(ns status-im.contexts.wallet.common.transaction-settings.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.common.controlled-input.utils :as controlled-input] + [status-im.contexts.wallet.common.utils :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn custom-settings-sheet + [_] + (let [eip-1559-enabled? (rf/sub [:wallet/tx-eip-1559-enabled?]) + max-base-fee (rf/sub [:wallet/tx-settings-max-base-fee]) + priority-fee (rf/sub [:wallet/tx-settings-custom-priority-fee]) + max-gas-amount (rf/sub [:wallet/tx-settings-gas-amount]) + gas-price (rf/sub [:wallet/tx-settings-gas-price]) + nonce (rf/sub [:wallet/tx-settings-nonce]) + no-base-fee? (rf/sub [:wallet/tx-settings-no-base-fee?]) + no-priority-fee? (rf/sub [:wallet/tx-settings-no-priority-fee?]) + account-color (rf/sub [:wallet/current-viewing-account-color]) + current-screen (rf/sub [:view-id]) + fee-data-fields (if eip-1559-enabled? + [(when-not no-base-fee? + {:title (i18n/label :t/max-base-fee) + :description-props {:text (str max-base-fee " GWEI")} + + :image :none + :description :text + :action :arrow + :on-press #(rf/dispatch [:navigate-to-within-stack + [:screen/wallet.tx-settings-max-fee + current-screen]]) + :label :text + :preview-size :size-32}) + (when-not no-priority-fee? + {:title (i18n/label :t/priority-fee) + :description-props {:text (str priority-fee " GWEI")} + :image :none + :description :text + :action :arrow + :on-press #(rf/dispatch + [:navigate-to-within-stack + [:screen/wallet.tx-settings-priority-fee + current-screen]]) + :label :text + :preview-size :size-32})] + [{:title (i18n/label :t/gas-price) + :description-props {:text (str gas-price " GWEI")} + :image :none + :description :text + :action :arrow + :on-press #(rf/dispatch [:navigate-to-within-stack + [:screen/wallet.tx-settings-gas-price + current-screen]]) + :label :text + :preview-size :size-32}]) + extra-fields [{:title (i18n/label :t/max-gas-amount) + :description-props {:text (str max-gas-amount " UNITS")} + :image :none + :description :text + :action :arrow + :on-press #(rf/dispatch [:navigate-to-within-stack + [:screen/wallet.tx-settings-gas-amount + current-screen]]) + :label :text + :preview-size :size-32} + {:title (i18n/label :t/nonce) + :description-props {:text nonce} + :image :none + :description :text + :action :arrow + :on-press #(rf/dispatch [:navigate-to-within-stack + [:screen/wallet.tx-settings-nonce + current-screen]]) + :label :text + :preview-size :size-32}]] + [rn/view + [quo/drawer-top + {:title (i18n/label :t/custom)}] + [quo/category + {:list-type :settings + :data (concat fee-data-fields extra-fields)}] + [quo/bottom-actions + {:actions :one-action + :button-one-props {:on-press (fn [] + (rf/dispatch + [:wallet/custom-transaction-settings-confirmed]) + (rf/dispatch [:hide-bottom-sheet])) + :customization-color account-color} + :button-one-label (i18n/label :t/confirm)}]])) + +(defn- estimated-time + [fees-by-mode fee-mode] + (-> (get fees-by-mode fee-mode) + :estimated-time + utils/estimated-time-v2-format)) + +(defn- format-title + [estimated-time-s fee-mode] + (-> (condp = + fee-mode + :tx-fee-mode/normal + (i18n/label :t/normal) + + :tx-fee-mode/fast + (i18n/label :t/fast) + + :tx-fee-mode/urgent + (i18n/label :t/urgent) + + "") + (str " " estimated-time-s "s"))) + +(defn settings-sheet + [] + (let [current-transaction-setting (rf/sub [:wallet/tx-settings-fee-mode]) + fees-by-mode (rf/sub [:wallet/suggested-gas-fees-for-setting]) + account-color (rf/sub [:wallet/current-viewing-account-color]) + [transaction-setting set-transaction-setting] (rn/use-state current-transaction-setting) + set-normal #(set-transaction-setting :tx-fee-mode/normal) + set-fast #(set-transaction-setting :tx-fee-mode/fast) + set-urgent #(set-transaction-setting :tx-fee-mode/urgent) + title (fn [fee-mode] + (-> fees-by-mode + (estimated-time fee-mode) + (format-title fee-mode))) + set-custom (fn [] + (rf/dispatch + [:show-bottom-sheet + {:content custom-settings-sheet}]))] + [rn/view + [quo/drawer-top + {:title (i18n/label :t/transaction-settings)}] + [quo/category + {:list-type :settings + :data + [(when (contains? fees-by-mode :tx-fee-mode/normal) + {:title (title :tx-fee-mode/normal) + :image-props "🍿" + :description-props {:text (rf/sub [:wallet/wallet-transaction-setting-fiat-formatted + :tx-fee-mode/normal])} + :image :emoji + :description :text + :action :selector + :action-props {:type :radio + :checked? (= :tx-fee-mode/normal transaction-setting) + :customization-color account-color + ;; there is an UI issue in quo/category, it has :on-press event + ;; and child radio button has own :on-change. If they are not set + ;; to the same action then we are getting inconsistent behaviour + ;; when user can click on settings item but cant on radio itself. + ;; So duplication is to prevent that until general fix is applied + ;; to quo/category + :on-change set-normal} + :on-press set-normal + :label :text + :preview-size :size-32}) + (when (contains? fees-by-mode :tx-fee-mode/fast) + {:title (title :tx-fee-mode/fast) + :image-props "🚗" + :description-props {:text (rf/sub [:wallet/wallet-transaction-setting-fiat-formatted + :tx-fee-mode/fast])} + :image :emoji + :description :text + :action :selector + :action-props {:type :radio + :checked? (= :tx-fee-mode/fast transaction-setting) + :on-change set-fast + :customization-color account-color} + :on-press set-fast + :label :text + :preview-size :size-32}) + (when (contains? fees-by-mode :tx-fee-mode/urgent) + {:title (title :tx-fee-mode/urgent) + :image-props "🚀" + :description-props {:text (rf/sub [:wallet/wallet-transaction-setting-fiat-formatted + :tx-fee-mode/urgent])} + :image :emoji + :description :text + :action :selector + :action-props {:type :radio + :checked? (= :tx-fee-mode/urgent transaction-setting) + :customization-color account-color + :on-change set-urgent} + :on-press set-urgent + :label :text + :preview-size :size-32}) + {:title (i18n/label :t/custom) + :image-props :i/edit + :description-props {:text (i18n/label :t/set-your-own-fees-and-nonce)} + :image :icon + :description :text + :action :selector + :action-props {:type :radio + :checked? (= :tx-fee-mode/custom transaction-setting) + :customization-color account-color + :on-change set-custom} + :on-press set-custom + :label :text + :preview-size :size-32}]}] + [quo/bottom-actions + {:actions :one-action + :button-one-props {:on-press (fn [] + (rf/dispatch [:wallet/quick-fee-mode-confirmed + transaction-setting]) + (rf/dispatch [:hide-bottom-sheet])) + :customization-color account-color} + :button-one-label (i18n/label :t/confirm)}]])) + +(defn- hint + [{:keys [status text]}] + [quo/network-tags + {:title text + :status status}]) + +(defn info-sheet + [info-title info-content] + [quo/documentation-drawers + {:title info-title + :shell? true} + [rn/view + [quo/text {:size :paragraph-2} info-content] + [quo/button + {:type :outline + :size 24 + :icon-right :i/info + :container-style {:padding-top 21 + :padding-bottom (if platform/ios? 14 24) + :align-self :flex-start + :justify-content :center} + :on-press #(rn/open-url "https://status.app/help/wallet/set-transaction-fees-and-nonce")} + (i18n/label :t/read-more)]]]) + +(defn custom-setting-screen + "conditions-fn parameter should be a function that returns map with keys :status and :hint-text" + [{:keys [screen-title token-symbol conditions-fn current info-title info-content + on-save + with-decimals?] + :or {with-decimals? true}}] + (let [[input-state set-input-state] (rn/use-state (controlled-input/set-value-numeric + controlled-input/init-state + current)) + input-value (controlled-input/input-value input-state) + condition (when conditions-fn + (conditions-fn input-value))] + [rn/view + {:style {:flex 1}} + [quo/page-nav + {:type :title + :title screen-title + :text-align :center + :right-side [{:icon-name :i/info + :on-press #(rf/dispatch [:show-bottom-sheet + {:content (fn [] [info-sheet info-title + info-content])}])}] + :icon-name :i/arrow-left + :on-press (fn [] + (rf/dispatch [:navigate-back]) + (rf/dispatch [:show-bottom-sheet + {:content custom-settings-sheet}]))}] + [quo/token-input + {:token-symbol token-symbol + :swappable? false + :show-token-icon? false + :value input-value + :error? (= (:status condition) :error) + :currency-symbol token-symbol + :hint-component [hint + {:status (:status condition) + :text (:hint-text condition)}]}] + [rn/view {:style {:flex 1}}] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/save-changes) + :button-one-props {:disabled? (= (:status condition) :error) + :on-press #(on-save (controlled-input/value-numeric input-state))}}] + [quo/numbered-keyboard + {:container-style {:padding-bottom safe-area/bottom} + :left-action (if with-decimals? :dot :none) + :delete-key? true + :on-press (fn [c] + (set-input-state #(controlled-input/add-character % c))) + :on-delete (fn [] + (set-input-state controlled-input/delete-last)) + :on-long-press-delete (fn [] + (set-input-state controlled-input/delete-all))}]])) diff --git a/src/status_im/contexts/wallet/common/utils.cljs b/src/status_im/contexts/wallet/common/utils.cljs new file mode 100644 index 00000000000..f42dc8e880d --- /dev/null +++ b/src/status_im/contexts/wallet/common/utils.cljs @@ -0,0 +1,493 @@ +(ns status-im.contexts.wallet.common.utils + (:require [clojure.string :as string] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [utils.address] + [utils.money :as money] + [utils.number :as number] + [utils.string])) + +(def missing-price-decimals 6) ; if we don't have the monetary value of the token, we default to 6 decimals +(defn get-first-name + [full-name] + (first (string/split full-name #" "))) + +(defn cut-fiat-balance + [balance decimals] + (let [valid-balance? (and balance + (or (number? balance) (.-toFixed balance)))] + (as-> balance $ + (if valid-balance? $ 0) + (.toFixed $ decimals)))) + +(defn cut-fiat-balance-to-two-decimals + [balance] + (cut-fiat-balance balance 2)) + +(defn prepend-curency-symbol-to-fiat-balance + [fiat-balance currency-symbol] + (let [formatted-symbol (if (> (count currency-symbol) 1) + (str currency-symbol " ") + currency-symbol)] + (str formatted-symbol fiat-balance))) + +(defn prettify-balance + [currency-symbol fiat-balance] + (-> fiat-balance + cut-fiat-balance-to-two-decimals + (prepend-curency-symbol-to-fiat-balance currency-symbol))) + +(defn get-derivation-path + [number-of-accounts] + (str constants/path-wallet-root "/" number-of-accounts)) + +(defn format-derivation-path + [path] + (string/replace path "/" " / ")) + +(defn get-formatted-derivation-path + [number-of-accounts] + (let [path (get-derivation-path number-of-accounts)] + (format-derivation-path path))) + +(defn total-raw-balance-in-all-chains + [balances-per-chain] + (->> balances-per-chain + (map (comp :raw-balance val)) + (reduce money/add))) + +(defn extract-exponent + [s] + (if-let [index (string/index-of s "e")] + (subs s (+ index 2)) + nil)) + +(defn calc-max-crypto-decimals + [one-cent-value] + (let [str-representation (str one-cent-value) + decimal-part (second (clojure.string/split str-representation #"\.")) + exponent (extract-exponent str-representation) + zeroes-count (count (take-while #(= \0 %) decimal-part)) + max-decimals (or exponent zeroes-count) + first-non-zero-digit (first (filter #(not (= \0 %)) decimal-part))] + (if (= \1 first-non-zero-digit) + (inc max-decimals) + max-decimals))) + +(defn token-price-by-symbol + "Get token price for specific currency from prices-per-token structure" + [prices-per-token token-symbol currency] + (get-in prices-per-token [(keyword token-symbol) currency])) + +(defn token-usd-price + [token prices-per-token] + (token-price-by-symbol prices-per-token (:symbol token) :usd)) + +(defn one-cent-value + [token-price-in-usd] + (if (pos? token-price-in-usd) + (/ 0.01 token-price-in-usd) + 0)) + +(defn analyze-token-amount-for-price + "For full details: https://github.com/status-im/status-mobile/issues/18225" + [token-price-in-usd token-units] + (if (or (nil? token-units) + (not (money/bignumber? token-units)) + (money/equal-to token-units 0)) + {:zero-value? true} + (let [cent-value (one-cent-value token-price-in-usd)] + {:usd-cent-value cent-value + :standardized-decimals-count (if (nil? token-price-in-usd) + missing-price-decimals + (calc-max-crypto-decimals cent-value))}))) + +(defn cut-crypto-decimals-to-fit-usd-cents + [token-units token-price-in-usd] + (let [{:keys [zero-value? usd-cent-value standardized-decimals-count]} + (analyze-token-amount-for-price token-price-in-usd token-units)] + (cond + zero-value? "0" + (< token-units usd-cent-value) "0" + :else (number/remove-trailing-zeroes + (number/format-decimal-fixed token-units + standardized-decimals-count))))) + +(defn add-token-symbol-to-crypto-balance + [crypto-balance token-symbol] + (str crypto-balance " " (string/upper-case token-symbol))) + +(defn prettify-crypto-balance + [token-symbol crypto-balance conversion-rate] + (-> crypto-balance + (cut-crypto-decimals-to-fit-usd-cents conversion-rate) + (add-token-symbol-to-crypto-balance token-symbol))) + +(defn get-standard-crypto-format + "For full details: https://github.com/status-im/status-mobile/issues/18225" + [token token-units prices-per-token] + (let [token-price-in-usd (token-usd-price token prices-per-token) + {:keys [zero-value? usd-cent-value standardized-decimals-count]} + (analyze-token-amount-for-price token-price-in-usd token-units)] + (cond + zero-value? + "0" + + (< token-units usd-cent-value) + (str "<" (number/remove-trailing-zeroes (.toFixed usd-cent-value standardized-decimals-count))) + + :else + (number/remove-trailing-zeroes (.toFixed token-units standardized-decimals-count))))) + +(defn get-market-value + [currency {token-symbol :symbol} prices-per-token] + (or (token-price-by-symbol prices-per-token token-symbol currency) + (token-price-by-symbol prices-per-token token-symbol constants/profile-default-currency) + ;; NOTE: adding fallback value (zero) in case prices are + ;; unavailable and to prevent crash on calculating fiat value + 0)) + +(defn- filter-chains + [balances-per-chain chain-ids] + (if chain-ids + (select-keys balances-per-chain chain-ids) + balances-per-chain)) + +(defn calculate-total-token-balance + ([token] + (calculate-total-token-balance token nil)) + ([{:keys [balances-per-chain decimals]} chain-ids] + (-> balances-per-chain + (filter-chains chain-ids) + (total-raw-balance-in-all-chains) + (money/token->unit decimals)))) + +(defn get-account-by-address + [accounts address] + (some #(when (= (:address %) address) %) accounts)) + +(defn get-default-account + [accounts] + (some #(when (:default-account? %) %) accounts)) + +(defn calculate-token-fiat-value + "Returns the token fiat value for provided raw balance" + [{:keys [currency balance token prices-per-token]}] + (let [price (get-market-value currency token prices-per-token)] + (money/crypto->fiat balance price))) + +(defn calculate-token-fiat-change + "Returns the fiat value change for a token based on the percentage change" + [fiat-value change-pct-24h] + (let [fiat-change (-> (money/bignumber change-pct-24h) + (money/div (money/bignumber 100)) + (money/mul fiat-value) + (money/absolute-value))] + fiat-change)) + +(defn sanitized-token-amount-to-display + "Formats a token amount to a specified number of decimals. + Returns a threshold value if the formatted amount is less than the minimum value represented by the decimals." + [amount display-decimals] + (let [number (or (money/bignumber amount) + (money/bignumber 0)) + amount-fixed-decimals (-> number + (number/format-decimal-fixed display-decimals) + (number/remove-trailing-zeroes))] + (if (and (= amount-fixed-decimals "0") + (money/above-zero? amount)) + (number/small-number-threshold display-decimals) + (str amount-fixed-decimals)))) + +(defn token-balance-for-network + "Returns the token balance for a specific chain" + [token chain-id] + (let [token-decimals (:decimals token)] + (-> (get-in token [:balances-per-chain chain-id :raw-balance] 0) + (number/convert-to-whole-number token-decimals) + money/bignumber))) + +(defn token-balance-display-for-network + "Formats a token balance for a specific chain and rounds it to a specified number of decimals. + If the balance is less than the smallest representable value based on rounding decimals, + a threshold value is displayed instead." + [token chain-id rounding-decimals] + (let [token-decimals (:decimals token) + display-decimals (min token-decimals rounding-decimals)] + (-> (get-in token [:balances-per-chain chain-id :raw-balance] 0) + (number/convert-to-whole-number token-decimals) + money/bignumber + (sanitized-token-amount-to-display display-decimals)))) + +(defn calculate-balance-from-tokens + [{:keys [currency tokens chain-ids prices-per-token]}] + (->> tokens + (map #(calculate-token-fiat-value + {:currency currency + :balance (calculate-total-token-balance % chain-ids) + :token % + :prices-per-token prices-per-token})) + (reduce money/add))) + +(defn- add-balances-per-chain + [b1 b2] + {:raw-balance (money/add (:raw-balance b1) (:raw-balance b2)) + :chain-id (:chain-id b2)}) + +(defn- merge-token + [existing-token token] + (assoc token + :balances-per-chain + (merge-with add-balances-per-chain + (:balances-per-chain existing-token) + (:balances-per-chain token)))) + +(defn aggregate-tokens-for-all-accounts + "Receives accounts (seq) and returns aggregated tokens in all accounts + NOTE: We use double reduce for faster performance (faster than mapcat and flatten)" + [accounts] + (->> accounts + (map :tokens) + (reduce + (fn [result-map tokens-per-account] + (reduce + (fn [acc token] + (update acc (:symbol token) merge-token token)) + result-map + tokens-per-account)) + {}) + vals)) + +(defn fiat-formatted-for-ui + [currency-symbol fiat-value] + (cond + (money/equal-to fiat-value 0) (str currency-symbol "0.00") + (money/less-than fiat-value 0.01) (str "<" currency-symbol "0.01") + :else (prettify-balance currency-symbol fiat-value))) + +(defn prettify-percentage-change + "Returns unsigned precentage" + [percentage] + (-> (if (number? percentage) percentage 0) + money/bignumber + money/absolute-value + (money/to-fixed 2))) + +(defn formatted-token-fiat-value + "Converts a token balance into its equivalent fiat value, formatted with a currency symbol. + If the fiat value is below $0.01, it returns a <$0.01 string" + [{:keys [currency currency-symbol balance token prices-per-token]}] + (let [price (or (get-market-value currency token prices-per-token) 0) + price-zero? (zero? price) + balance (or balance 0) + balance-positive? (pos? balance) + fiat-value (money/crypto->fiat balance price) + fiat-value-zero? (money/equal-to fiat-value (money/bignumber 0))] + (if (and fiat-value-zero? (not price-zero?) balance-positive?) + (number/small-number-threshold 2 currency-symbol) + (fiat-formatted-for-ui currency-symbol fiat-value)))) + +(defn calculate-token-value + "This function returns token values in the props of token-value (quo) component" + [{:keys [token color currency currency-symbol prices-per-token market-values-per-token]}] + (let [balance (calculate-total-token-balance token) + fiat-unformatted-value (calculate-token-fiat-value + {:currency currency + :balance balance + :token token + :prices-per-token prices-per-token}) + market-values (get market-values-per-token (keyword (:symbol token))) + {:keys [change-pct-24h]} market-values + percentage-change (prettify-percentage-change change-pct-24h) + crypto-value (get-standard-crypto-format token balance prices-per-token) + fiat-value (fiat-formatted-for-ui currency-symbol + fiat-unformatted-value) + fiat-change (calculate-token-fiat-change fiat-unformatted-value change-pct-24h) + formatted-fiat-change (prettify-balance currency-symbol fiat-change)] + {:token (:symbol token) + :token-name (:name token) + :state :default + :metrics? (money/above-zero? balance) + :status (cond + (pos? change-pct-24h) :positive + (neg? change-pct-24h) :negative + :else :empty) + :customization-color color + :values {:crypto-value crypto-value + :fiat-value fiat-value + :fiat-unformatted-value fiat-unformatted-value + :fiat-change formatted-fiat-change + :percentage-change percentage-change}})) + +(defn filter-tokens-in-chains + [tokens chain-ids] + (map #(update % :balances-per-chain select-keys chain-ids) tokens)) + +(defn format-token-id + [token collectible] + (if token + (:symbol token) + (str (get-in collectible [:id :contract-id :address]) + ":" + (get-in collectible [:id :token-id])))) + +(defn get-token-from-account + [db token-symbol address] + (let [address-tokens (-> db :wallet :accounts (get address) :tokens)] + (some #(when (= token-symbol (:symbol %)) + %) + address-tokens))) + +(defn get-shortened-address + "Takes first and last 4 digits from address including leading 0x + and adds unicode ellipsis in between" + [address] + (when address + (let [counter (count address)] + (str (subs address 0 6) "\u2026" (subs address (- counter 3) counter))))) + +(defn get-account-name-error + [s existing-account-names] + (cond + (utils.string/contains-emoji? s) :emoji + (existing-account-names s) :existing-name + (utils.string/contains-special-character? s) :special-character)) + +(defn calculate-and-sort-tokens + [{:keys [tokens color currency currency-symbol prices-per-token market-values-per-token]}] + (let [calculate-token (fn [token] + (calculate-token-value {:token token + :color color + :currency currency + :currency-symbol currency-symbol + :prices-per-token prices-per-token + :market-values-per-token market-values-per-token})) + calculated-tokens (map calculate-token tokens)] + (sort-by (fn [token] + (let [fiat-value (get-in token [:values :fiat-unformatted-value]) + priority (get constants/token-sort-priority (:token token) ##Inf)] + [(- fiat-value) priority])) + calculated-tokens))) + +(defn sort-tokens + [tokens] + (sort-by (comp - :balance) tokens)) + +(defn sort-tokens-by-fiat-value + [tokens] + (sort-by (comp - :fiat-value) tokens)) + +(defn sort-tokens-by-name + [tokens] + (sort-by :symbol tokens)) + +(defn token-with-balance + ([token networks] + (token-with-balance token networks nil)) + ([token networks chain-ids] + (assoc token + :networks (network-utils/network-list-with-positive-balance token networks) + :supported-networks (network-utils/network-list token networks) + :available-balance (calculate-total-token-balance token) + :total-balance (calculate-total-token-balance token chain-ids)))) + +(defn tokens-with-balance + [tokens networks chain-ids] + (map (fn [token] + (token-with-balance token networks chain-ids)) + tokens)) + +(defn estimated-time-format + "Formats the estimated time in minute intervals for a transaction" + [estimated-time] + (condp = estimated-time + (:unknown constants/wallet-transaction-estimation) ">5" + (:less-than-one-minute constants/wallet-transaction-estimation) "<1" + (:less-than-three-minutes constants/wallet-transaction-estimation) "1-3" + (:less-than-five-minutes constants/wallet-transaction-estimation) "3-5" + ">5")) + +(defn estimated-time-v2-format + "Formats the estimated time (v2) in seconds for a transaction" + [estimated-time] + (cond + (or (nil? estimated-time) + (= estimated-time 0) + (> estimated-time 60)) ">60" + :else (str "~" estimated-time))) + +(defn transactions->hash-to-transaction-map + [transactions] + (reduce + (fn [acc {to-chain :toChain tx-hash :hash}] + (assoc acc + tx-hash + {:status :pending + :id tx-hash + :chain-id to-chain})) + {} + transactions)) + +(defn get-accounts-with-token-balance + [accounts token] + (let [operable-account (filter :operable? (vals accounts)) + positive-balance-in-any-chain? (fn [{:keys [balances-per-chain]}] + (->> balances-per-chain + (map (comp :raw-balance val)) + (some pos?))) + addresses-tokens-with-positive-balance (as-> operable-account $ + (group-by :address $) + (update-vals $ + #(filter positive-balance-in-any-chain? + (:tokens (first %)))))] + (if-let [asset-symbol (:symbol token)] + (let [addresses-with-asset (as-> addresses-tokens-with-positive-balance $ + (update-vals $ #(set (map :symbol %))) + (keep (fn [[address token-symbols]] + (when (token-symbols asset-symbol) address)) + $) + (set $))] + (filter #(addresses-with-asset (:address %)) operable-account)) + (filter (fn [{:keys [tokens]}] + (some positive-balance-in-any-chain? tokens)) + operable-account)))) + +(defn on-paste-address-or-ens + "Check if the clipboard has any valid address and extract the address without any chain info. + If it does not contain an valid address or it is ENS, return the clipboard text as it is" + [clipboard-text] + (if (utils.address/supported-address? clipboard-text) + (utils.address/extract-address-without-chains-info clipboard-text) + clipboard-text)) + +(defn token-exchange-rate + "Returns the exchange rate based on the token prices. + i.e. how many 'divisor' tokens is one 'divident' token." + ([divident-price divisor-price] + (token-exchange-rate divident-price divisor-price constants/min-token-decimals-to-display)) + ([divident-price divisor-price divisor-token-decimals] + (-> (money/bignumber divident-price) + (money/div (money/bignumber divisor-price)) + (number/to-fixed (min divisor-token-decimals constants/min-token-decimals-to-display))))) + +(defn calculate-max-safe-send-amount + "Calculates the max ETH that can be sent while reserving enough for gas fees. + + - Ensures a minimum of 0.0001 ETH and a max of 0.01 ETH for gas. + - Uses 20% of the value as an estimated fee, clamped within this range. + - In Desktop it's 10% but after some more test, we found 20% is better option. + - Prevents sending the full balance to avoid transaction failures. + + Aligned with the desktop logic for consistency. + https://github.com/status-im/status-desktop/blob/f320abb5c498ac260a1a4a9db046485b88af81e7/ui/app/AppLayouts/Wallet/WalletUtils.qml#L44" + [value] + (if (or (nil? value) (zero? value)) + "0" + (let [raw-fee (money/mul (money/bignumber value) 0.2) + clamped-fee (money/maximum (money/bignumber 0.0001) + (money/minimum (money/bignumber 0.01) raw-fee)) + result (money/sub (money/bignumber value) clamped-fee)] + (-> result + (money/maximum 0) + (number/format-decimal-fixed constants/eth-send-amount-decimal) + (number/remove-trailing-zeroes))))) diff --git a/src/status_im/contexts/wallet/common/utils/networks.cljs b/src/status_im/contexts/wallet/common/utils/networks.cljs new file mode 100644 index 00000000000..366819a46e3 --- /dev/null +++ b/src/status_im/contexts/wallet/common/utils/networks.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.wallet.common.utils.networks + (:require + [clojure.string :as string] + [status-im.contexts.wallet.networks.config :as networks.config] + [utils.money :as money] + [utils.number])) + +(defn network-list + [{:keys [balances-per-chain]} networks] + (->> balances-per-chain + keys + (map (fn [chain-id] + (first (filter #(or (= (:chain-id %) chain-id) + (= (:related-chain-id %) chain-id)) + networks)))) + set)) + +(defn balance-is-sufficient-to-use-chain + "Status network can operate with zero balance, for other networks we check if balance is greater than zero" + [[chain-id {:keys [raw-balance]}]] + (if (= chain-id networks.config/status-sepolia-chain-id) + true + (money/above-zero? raw-balance))) + +(defn filter-out-chains-with-insufficient-balance + [balances-per-chain] + (filter balance-is-sufficient-to-use-chain balances-per-chain)) + +(defn network-list-with-positive-balance + "Same as `network-list`, but only returns the networks that have a positive token balance" + [{:keys [balances-per-chain] :as token} networks] + (as-> balances-per-chain $ + (filter-out-chains-with-insufficient-balance $) + (assoc token :balances-per-chain $) + (network-list $ networks))) + +(defn split-network-full-address + [address] + (as-> address $ + (string/split $ ":") + [(butlast $) (last $)])) + +(defn network-summary + [network token-symbol amount] + (let [formatted-amount (if (money/equal-to amount 0) + "<0.01" + amount) + summary {:amount formatted-amount + :token-symbol token-symbol}] + (if (= :mainnet network) + {:mainnet summary} + {network summary}))) diff --git a/src/status_im/contexts/wallet/common/utils_test.cljs b/src/status_im/contexts/wallet/common/utils_test.cljs new file mode 100644 index 00000000000..5fd9c809ad7 --- /dev/null +++ b/src/status_im/contexts/wallet/common/utils_test.cljs @@ -0,0 +1,372 @@ +(ns status-im.contexts.wallet.common.utils-test + (:require + [cljs.test :refer [deftest is testing]] + [status-im.contexts.wallet.common.utils :as utils] + [utils.money :as money])) + +(deftest get-first-name-test + (testing "get-first-name function" + (is (= (utils/get-first-name "John Doe") "John")) + (is (= (utils/get-first-name "Jane Smith xyz") "Jane")))) + +(deftest prettify-balance-test + (testing "prettify-balance function" + (is (= (utils/prettify-balance "$" 100) "$100.00")) + (is (= (utils/prettify-balance "$" 0.5) "$0.50")) + (is (= (utils/prettify-balance "$" 0) "$0.00")) + (is (= (utils/prettify-balance "$" nil) "$0.00")) + (is (= (utils/prettify-balance "$" "invalid input") "$0.00")))) + +(deftest get-derivation-path-test + (testing "get-derivation-path function" + (is (= (utils/get-derivation-path 5) "m/44'/60'/0'/0/5")) + (is (= (utils/get-derivation-path 0) "m/44'/60'/0'/0/0")) + (is (= (utils/get-derivation-path 123) "m/44'/60'/0'/0/123")))) + +(deftest format-derivation-path-test + (testing "format-derivation-path function" + (is (= (utils/format-derivation-path "m/44'/60'/0'/0/5") "m / 44' / 60' / 0' / 0 / 5")) + (is (= (utils/format-derivation-path "m/44'/60'/0'/0/0") "m / 44' / 60' / 0' / 0 / 0")) + (is (= (utils/format-derivation-path "m/44'/60'/0'/0/123") "m / 44' / 60' / 0' / 0 / 123")))) + +(deftest get-formatted-derivation-path-test + (testing "get-formatted-derivation-path function" + (is (= (utils/get-formatted-derivation-path 5) "m / 44' / 60' / 0' / 0 / 5")) + (is (= (utils/get-formatted-derivation-path 0) "m / 44' / 60' / 0' / 0 / 0")) + (is (= (utils/get-formatted-derivation-path 123) "m / 44' / 60' / 0' / 0 / 123")))) + +(deftest total-raw-balance-in-all-chains-test + (testing "total-raw-balance-in-all-chains function" + (let [balances-per-chain {1 {:raw-balance (money/bignumber 100)} + 10 {:raw-balance (money/bignumber 200)} + 42161 {:raw-balance (money/bignumber 300)}}] + (is (money/equal-to (utils/total-raw-balance-in-all-chains balances-per-chain) + (money/bignumber 600)))))) + +(deftest extract-exponent-test + (testing "extract-exponent function" + (is (= (utils/extract-exponent "123.456") nil)) + (is (= (utils/extract-exponent "2.5e-2") "2")) + (is (= (utils/extract-exponent "4.567e-10") "10")))) + +(deftest calc-max-crypto-decimals-test + (testing "calc-max-crypto-decimals function" + (is (= (utils/calc-max-crypto-decimals 0.00323) 2)) + (is (= (utils/calc-max-crypto-decimals 0.00123) 3)) + (is (= (utils/calc-max-crypto-decimals 0.00000423) 5)) + (is (= (utils/calc-max-crypto-decimals 2.23e-6) 5)) + (is (= (utils/calc-max-crypto-decimals 1.13e-6) 6)))) + +(deftest get-standard-crypto-format-test + (testing "get-standard-crypto-format function" + (let [token {:symbol "ETH"} + prices-per-token {:ETH {:usd 100}} + token-units (money/bignumber 0.005)] + (is (= (utils/get-standard-crypto-format token + token-units + prices-per-token) + "0.005"))) + (let [token {:symbol "ETH"} + prices-per-token {:ETH {:usd nil}} + token-units (money/bignumber 0.0123456)] + (is (= (utils/get-standard-crypto-format token + token-units + prices-per-token) + "0.012346"))) + (let [token {:symbol "ETH"} + prices-per-token {:ETH {:usd 0.005}} + token-units (money/bignumber 0.01)] + (is (= (utils/get-standard-crypto-format token + token-units + prices-per-token) + "<2"))) + (let [token {:symbol "ETH"} + prices-per-token {:ETH {:usd 0.005}} + token-units "0.01"] + (is (= (utils/get-standard-crypto-format token + token-units + prices-per-token) + "0"))))) + +(deftest calculate-total-token-balance-test + (testing "calculate-total-token-balance function" + (let [token {:balances-per-chain {1 {:raw-balance (money/bignumber 100)} + 10 {:raw-balance (money/bignumber 200)} + 42161 {:raw-balance (money/bignumber 300)}} + :decimals 2}] + (is (money/equal-to (utils/calculate-total-token-balance token) 6.0))))) + +(deftest get-account-by-address-test + (testing "get-account-by-address function" + (let [accounts [{:address "0x123"} + {:address "0x456"} + {:address "0x789"}] + address-to-find "0x456"] + (is (= (utils/get-account-by-address accounts address-to-find) {:address "0x456"}))) + + (let [accounts [{:address "0x123"} + {:address "0x456"} + {:address "0x789"}] + address-to-find "0x999"] + (is (= (utils/get-account-by-address accounts address-to-find) nil))))) + +(deftest prettify-percentage-change-test + (testing "prettify-percentage-change function" + (is (= (utils/prettify-percentage-change nil) "0.00")) + (is (= (utils/prettify-percentage-change "") "0.00")) + (is (= (utils/prettify-percentage-change 0.5) "0.50")) + (is (= (utils/prettify-percentage-change 1.113454) "1.11")) + (is (= (utils/prettify-percentage-change -0.35) "0.35")) + (is (= (utils/prettify-percentage-change -0.78234) "0.78")))) + +(deftest calculate-and-sort-tokens-test + (testing "calculate-and-sort-tokens function" + (let [mock-color "blue" + mock-currency "USD" + mock-currency-symbol "$"] + + (with-redefs [utils/calculate-token-value + (fn [{:keys [token]}] + (case (:symbol token) + "ETH" {:token "ETH" :values {:fiat-unformatted-value 5}} + "DAI" {:token "DAI" :values {:fiat-unformatted-value 10}} + "SNT" {:token "SNT" :values {:fiat-unformatted-value 1}}))] + (testing "Standard case with different fiat-unformatted-values" + (let [mock-tokens [{:symbol "ETH" + :name "Ethereum" + :balances-per-chain {:mock-chain 5} + :decimals 18} + {:symbol "DAI" + :name "Dai" + :balances-per-chain {:mock-chain 10} + :decimals 18} + {:symbol "SNT" + :name "Status Network Token" + :balances-per-chain {:mock-chain 1} + :decimals 18}] + mock-input {:tokens mock-tokens + :color mock-color + :currency mock-currency + :currency-symbol mock-currency-symbol + :prices-per-token {} + :market-values-per-token {}} + sorted-tokens (map :token (utils/calculate-and-sort-tokens mock-input)) + expected-order ["DAI" "ETH" "SNT"]] + (is (= expected-order sorted-tokens)))))))) + +(deftest formatted-token-fiat-value-test + (testing "formatted-token-fiat-value function" + (let [default-params {:currency "USD" + :currency-symbol "$" + :balance 0.5 + :token {:symbol "ETH"} + :prices-per-token {:ETH {:usd 2000}}}] + (is (= (utils/formatted-token-fiat-value default-params) "$1000.00")) + (is (= (utils/formatted-token-fiat-value (assoc default-params :balance 0)) "$0.00")) + (is (= (utils/formatted-token-fiat-value (assoc default-params :balance 0.000001)) "<$0.01")) + (is (= (utils/formatted-token-fiat-value (assoc default-params :balance nil)) "$0.00")) + (is (= (utils/formatted-token-fiat-value + (assoc default-params :balance 1 :prices-per-token {:ETH {:usd nil}})) + "$0.00")) + (is (= (utils/formatted-token-fiat-value + (assoc default-params :balance 1 :prices-per-token {:ETH {:usd 0}})) + "$0.00"))))) + +(deftest sanitized-token-amount-to-display-test + (testing "sanitized-token-amount-to-display function" + (is (= (utils/sanitized-token-amount-to-display 1.2345 2) "1.23")) + (is (= (utils/sanitized-token-amount-to-display 0.0001 3) "<0.001")) + (is (= (utils/sanitized-token-amount-to-display 0.00001 3) "<0.001")) + (is (= (utils/sanitized-token-amount-to-display 0 2) "0")) + (is (= (utils/sanitized-token-amount-to-display 123.456789 4) "123.4567")) + (is (= (utils/sanitized-token-amount-to-display 0.00000123 6) "0.000001")) + (is (= (utils/sanitized-token-amount-to-display nil 2) "0")) + (is (= (utils/sanitized-token-amount-to-display "invalid" 2) "0")))) + +(deftest token-balance-display-for-network-test + (testing "Standard balance with rounding decimals" + (let [token {:decimals 18 + :balances-per-chain {1 {:raw-balance "1000000000000000000"}}} + chain-id 1 + rounding-decimals 2 + expected "1"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected)))) + + (testing "Balance with more decimals than specified rounding" + (let [token {:decimals 18 + :balances-per-chain {1 {:raw-balance "123456789000000000"}}} + chain-id 1 + rounding-decimals 3 + expected "0.123"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected)))) + + (testing "Very small balance displayed as threshold (<0.000001)" + (let [token {:decimals 18 + :balances-per-chain {1 {:raw-balance "1"}}} + chain-id 1 + rounding-decimals 6 + expected "<0.000001"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected)))) + + (testing + "Very small balance displayed without threshold when token decimals are equal than reounding decimals" + (let [token {:decimals 18 + :balances-per-chain {1 {:raw-balance "1"}}} + chain-id 1 + rounding-decimals 18 + expected "0.000000000000000001"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected)))) + + (testing + "Very small balance displayed without threshold when token decimals are lower than reounding decimals" + (let [token {:decimals 18 + :balances-per-chain {1 {:raw-balance "1"}}} + chain-id 1 + rounding-decimals 21 + expected "0.000000000000000001"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected)))) + + (testing "Zero balance displayed correctly" + (let [token {:decimals 18 + :balances-per-chain {1 {:raw-balance "0"}}} + chain-id 1 + rounding-decimals 2 + expected "0"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected)))) + + (testing "Balance with trailing zeroes removed" + (let [token {:decimals 8 + :balances-per-chain {1 {:raw-balance "123400000"}}} + chain-id 1 + rounding-decimals 6 + expected "1.234"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected)))) + + (testing "Balance when chain data is missing" + (let [token {:decimals 18} + chain-id 1 + rounding-decimals 2 + expected "0"] + (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) + expected))))) + +(def mock-accounts + {:0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5 + {:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}} + {:symbol "USDC" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]} + :0xbce36f66a8cd99f1d6489cb9585146e3f3b893be + {:address "0xbce36f66a8cd99f1d6489cb9585146e3f3b893be" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]}}) + +(deftest get-accounts-with-token-balance-test + (testing "Positive token balance for a specific token" + (let [accounts mock-accounts + token {:symbol "ETH"} + expected [{:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}} + {:symbol "USDC" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]}]] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected)))) + + (testing "No token symbol provided, return all tokens with positive balance" + (let [accounts mock-accounts + token {} + expected [{:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}} + {:symbol "USDC" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]}]] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected)))) + + (testing "No matching token found for a specific token" + (let [accounts mock-accounts + token {:symbol "DAI"} + expected []] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected)))) + + (testing "No operable accounts" + (let [accounts {:0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5 + {:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? false + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}}]}} + token {} + expected []] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected))))) + +(deftest sort-tokens-test + (testing "Sort tokens by balance in descending order" + (let [tokens [{:symbol "BTC" :balance 2} + {:symbol "ETH" :balance 5} + {:symbol "DAI" :balance 10}] + expected [{:symbol "DAI" :balance 10} + {:symbol "ETH" :balance 5} + {:symbol "BTC" :balance 2}]] + (is (= (utils/sort-tokens tokens) expected))))) + +(deftest sort-tokens-by-fiat-value-test + (testing "Sort tokens by fiat value in descending order" + (let [tokens [{:symbol "BTC" :fiat-value 50000} + {:symbol "ETH" :fiat-value 15000} + {:symbol "DAI" :fiat-value 1000}] + expected [{:symbol "BTC" :fiat-value 50000} + {:symbol "ETH" :fiat-value 15000} + {:symbol "DAI" :fiat-value 1000}]] + (is (= (utils/sort-tokens-by-fiat-value tokens) expected))))) + +(deftest sort-tokens-by-name-test + (testing "Sort tokens by symbol in ascending order" + (let [tokens [{:symbol "ETH"} + {:symbol "DAI"} + {:symbol "BTC"}] + expected [{:symbol "BTC"} + {:symbol "DAI"} + {:symbol "ETH"}]] + (is (= (utils/sort-tokens-by-name tokens) expected))))) + +(deftest calculate-token-fiat-change-test + (testing "Calculate token fiat change" + (is (= (utils/calculate-token-fiat-change 100 10) 10.0)) + (is (= (utils/calculate-token-fiat-change 200 -5) 10.0)) + (is (= (utils/calculate-token-fiat-change 50 0) 0.0)) + (is (= (utils/calculate-token-fiat-change 100 -100) 100.0)) + (is (= (utils/calculate-token-fiat-change 0.001 0.1) 0.000001)))) + +(deftest calculate-max-safe-send-amount-test + (testing "Calculates the max ETH sendable while reserving fees" + (is (= "0" (utils/calculate-max-safe-send-amount nil))) + (is (= "0" (utils/calculate-max-safe-send-amount 0))) + (is (= "0" (utils/calculate-max-safe-send-amount 0.00009))) + (is (= "0" (utils/calculate-max-safe-send-amount 0.0001))) + (is (= "0.008" (utils/calculate-max-safe-send-amount 0.01))) + (is (= "0.99" (utils/calculate-max-safe-send-amount 1.0))) + (is (= "9.99" (utils/calculate-max-safe-send-amount 10.0))) + (is (= "99.99" (utils/calculate-max-safe-send-amount 100.0))))) diff --git a/src/status_im/contexts/wallet/common/validation.cljs b/src/status_im/contexts/wallet/common/validation.cljs new file mode 100644 index 00000000000..924e3ef9e60 --- /dev/null +++ b/src/status_im/contexts/wallet/common/validation.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.wallet.common.validation + (:require [clojure.string :as string] + [status-im.constants :as constants])) + +(defn ens-name? + [s] + (and (not (string/blank? s)) + (boolean (re-find constants/regx-ens s)))) + +(defn private-key? + [s] + (or (re-find constants/regx-private-key-hex s) + (re-find constants/regx-private-key s))) diff --git a/src/status_im/contexts/wallet/common/wizard/events.cljs b/src/status_im/contexts/wallet/common/wizard/events.cljs new file mode 100644 index 00000000000..fcc9609a4ac --- /dev/null +++ b/src/status_im/contexts/wallet/common/wizard/events.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.wallet.common.wizard.events + (:require [status-im.contexts.wallet.common.wizard.utils :as wizard-utils] + [utils.re-frame :as rf])) + +(rf/reg-event-fx + :wallet/wizard-navigate-forward + (fn [{:keys [db]} [{:keys [current-screen flow-id start-flow?]}]] + (let [next-screen (wizard-utils/wizard-find-next-screen db flow-id current-screen)] + {:fx [[:dispatch + (if start-flow? + [:open-modal (:screen-id next-screen)] + [:navigate-to-within-stack [(:screen-id next-screen) current-screen]])]]}))) diff --git a/src/status_im/contexts/wallet/common/wizard/utils.cljs b/src/status_im/contexts/wallet/common/wizard/utils.cljs new file mode 100644 index 00000000000..4b17c26a07d --- /dev/null +++ b/src/status_im/contexts/wallet/common/wizard/utils.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.wallet.common.wizard.utils + (:require [status-im.contexts.wallet.bridge.flow-config :as wallet-bridge-flow] + [status-im.contexts.wallet.send.flow-config :as wallet-send-flow])) + +(defn wizard-find-next-screen + [db flow-id current-screen] + (let [flow-config (case flow-id + :wallet-send-flow wallet-send-flow/steps + :wallet-bridge-flow wallet-bridge-flow/steps + nil)] + (first (filter (fn [{:keys [skip-step? screen-id]}] + (and (not= screen-id current-screen) + (not (and (fn? skip-step?) (skip-step? db))))) + flow-config)))) diff --git a/src/status_im/contexts/wallet/connected_dapps/disconnect_dapp/style.cljs b/src/status_im/contexts/wallet/connected_dapps/disconnect_dapp/style.cljs new file mode 100644 index 00000000000..79fcc617ec3 --- /dev/null +++ b/src/status_im/contexts/wallet/connected_dapps/disconnect_dapp/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.wallet.connected-dapps.disconnect-dapp.style) + +(def content-wrapper + {:padding-vertical 8 + :padding-horizontal 20 + :gap 12}) diff --git a/src/status_im/contexts/wallet/connected_dapps/disconnect_dapp/view.cljs b/src/status_im/contexts/wallet/connected_dapps/disconnect_dapp/view.cljs new file mode 100644 index 00000000000..f7ca977abe5 --- /dev/null +++ b/src/status_im/contexts/wallet/connected_dapps/disconnect_dapp/view.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.wallet.connected-dapps.disconnect-dapp.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.connected-dapps.disconnect-dapp.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [customization-color dapp on-disconnect]}] + (let [{:keys [avatar name]} dapp] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :default + :full-name name + :profile-picture avatar + :title (i18n/label :t/disconnect-dapp) + :customization-color customization-color}] + [rn/view {:style style/content-wrapper} + [quo/text + {:size :paragraph-1} + (i18n/label :t/disconnect-dapp-confirmation {:dapp name})]] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/disconnect) + :button-one-props {:type :danger + :accessibility-label :block-contact + :on-press on-disconnect} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :accessibility-label :cancel + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) diff --git a/src/status_im/contexts/wallet/connected_dapps/scan_dapp/style.cljs b/src/status_im/contexts/wallet/connected_dapps/scan_dapp/style.cljs new file mode 100644 index 00000000000..05e325f8ced --- /dev/null +++ b/src/status_im/contexts/wallet/connected_dapps/scan_dapp/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.wallet.connected-dapps.scan-dapp.style + (:require [quo.foundations.colors :as colors])) + +(def subtitle-container + {:flex-direction :row + :align-items :center + :padding-horizontal 20 + :gap 4}) + +(def subtitle-text + {:color colors/white}) diff --git a/src/status_im/contexts/wallet/connected_dapps/scan_dapp/view.cljs b/src/status_im/contexts/wallet/connected_dapps/scan_dapp/view.cljs new file mode 100644 index 00000000000..e0e62b51e45 --- /dev/null +++ b/src/status_im/contexts/wallet/connected_dapps/scan_dapp/view.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.wallet.connected-dapps.scan-dapp.view + (:require [quo.context] + [quo.core :as quo] + [quo.foundations.resources :as quo.resources] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [status-im.common.scan-qr-code.view :as scan-qr-code] + [status-im.contexts.wallet.connected-dapps.scan-dapp.style :as style] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [{:keys [keyboard-shown]} (hooks/use-keyboard) + {:keys [name emoji color]} (rf/sub [:wallet/current-viewing-account])] + (rn/use-mount + (fn [] + (when keyboard-shown + (rn/dismiss-keyboard!)))) + [scan-qr-code/view + {:title (i18n/label :t/scan-qr) + :subtitle [rn/view style/subtitle-container + [quo/context-tag + {:theme :dark + :type :account + :size 24 + :account-name name + :emoji emoji + :customization-color color}] + [quo/text + {:style style/subtitle-text + :size :paragraph-1} + (i18n/label :t/wallet-connect-via)] + [quo/context-tag + {:theme :dark + :type :dapp + :size 24 + :dapp-name (i18n/label :t/wallet-connect-label) + :dapp-logo (quo.resources/get-dapp :wallet-connect)}]] + :on-success-scan (fn [scanned-text] + (debounce/debounce-and-dispatch + [:wallet-connect/on-scan-connection scanned-text] + 300))}])) diff --git a/src/status_im/contexts/wallet/connected_dapps/style.cljs b/src/status_im/contexts/wallet/connected_dapps/style.cljs new file mode 100644 index 00000000000..b011ac5defc --- /dev/null +++ b/src/status_im/contexts/wallet/connected_dapps/style.cljs @@ -0,0 +1,43 @@ +(ns status-im.contexts.wallet.connected-dapps.style + (:require [quo.foundations.colors :as colors])) + +(def screen-padding 20) + +(def header-container + {:flex-direction :row + :justify-content :space-between + :padding-horizontal screen-padding + :margin-vertical 12}) + +(def header-wrapper + {:flex-direction :column + :height 96 + :gap 8 + :padding-horizontal 20 + :padding-vertical 12}) + +(def account-details-wrapper + {:align-items :flex-start}) + +(def empty-container-style + {:justify-content :center + :flex 1 + :margin-bottom 44}) + +(defn dapps-container + [bottom] + {:padding-horizontal 20 + :padding-vertical 8 + :margin-bottom bottom + :flex 1}) + +(defn dapps-list + [theme] + {:border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) + +(defn separator + [theme] + {:height 1 + :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80 theme)}) diff --git a/src/status_im/contexts/wallet/connected_dapps/view.cljs b/src/status_im/contexts/wallet/connected_dapps/view.cljs new file mode 100644 index 00000000000..424ee458423 --- /dev/null +++ b/src/status_im/contexts/wallet/connected_dapps/view.cljs @@ -0,0 +1,139 @@ +(ns status-im.contexts.wallet.connected-dapps.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.plus-button.view :as plus-button] + [status-im.common.resources :as resources] + [status-im.contexts.wallet.connected-dapps.disconnect-dapp.view :as disconnect-dapp] + [status-im.contexts.wallet.connected-dapps.style :as style] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.string])) + +(defn- on-disconnect + [wallet-account {:keys [name topic]}] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:wallet-connect/disconnect-dapp + {:topic topic + :on-success (fn [] + (rf/dispatch [:toasts/upsert + {:id :dapp-disconnect-success + :type :positive + :text (i18n/label :t/disconnect-dapp-success + {:dapp name + :account (:name wallet-account)})}])) + :on-fail (fn [] + (rf/dispatch [:toasts/upsert + {:id :dapp-disconnect-failure + :type :negative + :text (i18n/label :t/disconnect-dapp-fail + {:dapp name + :account (:name wallet-account)})}]))}])) + +(defn- on-dapp-disconnect-press + [wallet-account dapp] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [disconnect-dapp/view + {:customization-color (:color wallet-account) + :dapp dapp + :on-disconnect #(on-disconnect wallet-account dapp)}])}])) + +(defn- account-details + [{:keys [name emoji color]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/account-details-wrapper} + [quo/context-tag + {:theme theme + :type :account + :size 24 + :account-name name + :emoji emoji + :customization-color color}]])) + +(defn- header + [{:keys [title wallet-account on-close on-add]}] + (let [{:keys [color]} wallet-account] + [:<> + [rn/view {:style style/header-container} + [quo/button + {:icon-only? true + :type :grey + :background :blur + :size 32 + :accessibility-label :connected-dapps-close + :on-press on-close} + :i/close]] + [rn/view {:style style/header-wrapper} + [quo/standard-title + {:title title + :accessibility-label :connected-dapps + :customization-color color + :right [plus-button/plus-button + {:on-press on-add + :accessibility-label :connected-dapps-add + :customization-color color}]}] + [rn/view {:flex 1} + [account-details wallet-account]]]])) + +(defn view + [] + (let [{:keys [bottom]} safe-area/insets + {:keys [color] :as wallet-account} (rf/sub [:wallet/current-viewing-account]) + customization-color (rf/sub [:profile/customization-color]) + sessions (rf/sub + [:wallet-connect/sessions-for-current-account-and-networks]) + theme (quo.context/use-theme)] + [rn/view {:flex 1} + [header + {:title (i18n/label :t/connected-dapps) + :wallet-account wallet-account + :on-close #(rf/dispatch [:navigate-back]) + :on-add #(rf/dispatch [:navigate-to :screen/wallet.scan-dapp])}] + (if (empty? sessions) + [quo/empty-state + {:title (i18n/label :t/no-dapps) + :description (i18n/label :t/no-dapps-description) + :image (resources/get-themed-image :no-dapps theme) + :container-style style/empty-container-style}] + [rn/view (style/dapps-container bottom) + [rn/flat-list + {:data sessions + :always-bounce-vertical false + :content-container-style (style/dapps-list theme) + :render-fn (fn [{:keys [topic pairingTopic name url iconUrl]}] + [quo/dapp + {:dapp {:avatar + (data-store/compute-dapp-icon-path + iconUrl + url) + :name + (data-store/compute-dapp-name + name + url) + :value url + :topic topic + :pairing-topic pairingTopic + :customization-color customization-color} + :accessibility-label (str "dapp-" topic) + :state :default + :action :icon + :blur? false + :customization-color color + :right-component (fn [dapp] + [rn/pressable + {:on-press (fn [] + (on-dapp-disconnect-press + wallet-account + dapp))} + [quo/icon :i/disconnect + {:color (colors/theme-colors + colors/neutral-50 + colors/neutral-40 + theme) + :accessibility-label :icon}]])}]) + :separator [rn/view {:style (style/separator theme)}]}]])])) diff --git a/src/status_im/contexts/wallet/data_store.cljs b/src/status_im/contexts/wallet/data_store.cljs new file mode 100644 index 00000000000..6e3f2b7585f --- /dev/null +++ b/src/status_im/contexts/wallet/data_store.cljs @@ -0,0 +1,337 @@ +(ns status-im.contexts.wallet.data-store + (:require + [camel-snake-kebab.extras :as cske] + [clojure.set :as set] + [clojure.string :as string] + [schema.core :as schema] + [status-im.constants :as constants] + [status-im.contexts.wallet.collectible.utils :as collectible-utils] + [status-im.contexts.wallet.router.core :as router] + [status-im.contexts.wallet.router.schema :as router.schema] + [utils.collection :as utils.collection] + [utils.money :as money] + [utils.number :as utils.number] + [utils.transforms :as transforms])) + +(defn chain-ids-string->set + [ids-string] + (into #{} + (map utils.number/parse-int) + (string/split ids-string constants/chain-id-separator))) + +(defn chain-ids-set->string + [ids] + (string/join constants/chain-id-separator ids)) + +(defn add-keys-to-account + [account] + (let [watch-only? (= (:type account) :watch)] + (-> account + (assoc :operable? (and (not= (:operable account) :no) (not watch-only?))) + (assoc :watch-only? watch-only?) + (assoc :default-account? (:wallet account))))) + +(defn- sanitize-emoji + "As Desktop uses Twemoji, the emoji received can be an img tag + with raw emoji in alt attribute. This function help us to extract + the emoji from it as mobile doesn't support HTML rendering and Twemoji" + [emoji] + (if (string/starts-with? emoji " (re-find #"alt=\"(.*?)\"" emoji) last) + emoji)) + +(defn rpc->account + [account] + (-> account + (set/rename-keys {:prodPreferredChainIds :prod-preferred-chain-ids + :testPreferredChainIds :test-preferred-chain-ids + :createdAt :created-at + :colorId :color}) + (update :prod-preferred-chain-ids chain-ids-string->set) + (update :test-preferred-chain-ids chain-ids-string->set) + (update :type keyword) + (update :operable keyword) + (update :color + #(if (and (not (keyword? %)) (string/blank? %)) + constants/account-default-customization-color + (keyword %))) + (update :emoji sanitize-emoji) + add-keys-to-account)) + +(defn rpc->accounts + [accounts] + (->> (filter #(not (:chat %)) accounts) + (sort-by :position) + (map rpc->account))) + +(defn <-account + [account] + (-> account + (set/rename-keys {:prod-preferred-chain-ids :prodPreferredChainIds + :test-preferred-chain-ids :testPreferredChainIds + :color :colorId}) + (update :prodPreferredChainIds chain-ids-set->string) + (update :testPreferredChainIds chain-ids-set->string) + (dissoc :watch-only? :default-account? :operable? :tokens :collectibles))) + +(defn add-missing-balance-per-chain-values + "Adds any missing chain balance (0) to the balance-per-chain map based on token supported chains as + status-go returns balance for that chain only if the balance is positive." + [balances-per-chain supported-chains] + (let [zero-balance-per-chain (fn [chain-id] + {:chain-id chain-id + :raw-balance (money/->bignumber 0) + :balance "0"})] + (reduce + (fn [result chain-id] + (if (contains? result chain-id) + result + (assoc result chain-id (zero-balance-per-chain chain-id)))) + balances-per-chain + supported-chains))) + +(defn- rpc->balances-per-chain + [token supported-chains] + (-> token + (update :balances-per-chain update-vals #(update % :raw-balance money/bignumber)) + (update :balances-per-chain update-keys (comp utils.number/parse-int name)) + (update :balances-per-chain #(add-missing-balance-per-chain-values % supported-chains)))) + +(defn- update-balances-per-chain + [tokens supported-chains-by-token-symbol] + (mapv #(rpc->balances-per-chain % (get supported-chains-by-token-symbol (:symbol %))) tokens)) + +(defn- remove-tokens-with-empty-values + [tokens] + (remove + #(or (string/blank? (:symbol %)) (string/blank? (:name %))) + tokens)) + +(defn- merge-token + [existing-token token] + (update existing-token + :balances-per-chain + #(merge-with merge % (:balances-per-chain token)))) + +(defn- merge-tokens-with-same-symbol + [tokens] + (->> tokens + (reduce + (fn [acc token] + (update acc + (:symbol token) + #(if % (merge-token % token) token))) + {}) + vals)) + +(defn rpc->tokens + [tokens supported-chains-by-token-symbol] + (-> tokens + (update-keys name) + (update-vals #(cske/transform-keys transforms/->kebab-case-keyword %)) + (update-vals remove-tokens-with-empty-values) + ;; merge tokens with the same symbol as if the same token has balance in different chains, + ;; it's returned separately + (update-vals merge-tokens-with-same-symbol) + (update-vals #(update-balances-per-chain % supported-chains-by-token-symbol)))) + +(defn partially-operable-accounts? + [accounts] + (boolean + (some (fn [[_ {:keys [operable]}]] + (= :partially operable)) + accounts))) + +(defn get-keypair-lowest-operability + [{:keys [accounts]}] + (cond + (some #(= (:operable %) :no) accounts) + :no + + (some #(= (:operable %) :partially) accounts) + :partially + + :else + :fully)) + +(defn- add-keys-to-keypair + [keypair] + (assoc keypair :lowest-operability (get-keypair-lowest-operability keypair))) + +(defn rpc->keypair + [keypair] + (-> keypair + (update :type keyword) + (update :accounts #(map rpc->account %)) + add-keys-to-keypair)) + +(defn rpc->keypairs + [keypairs] + (->> (map rpc->keypair keypairs) + (sort-by #(if (= (:type %) :profile) 0 1)))) + +(defn- add-keys-to-saved-address + [saved-address] + (assoc saved-address :ens? (not (string/blank? (:ens saved-address))))) + +(defn rpc->saved-address + [saved-address] + (-> saved-address + (set/rename-keys {:chainShortNames :chain-short-names + :isTest :test? + :createdAt :created-at + :colorId :customization-color + :mixedcaseAddress :mixedcase-address + :removed :removed?}) + (update :customization-color (comp keyword string/lower-case)) + add-keys-to-saved-address)) + +(defn rpc->saved-addresses + [saved-addresses] + (map rpc->saved-address saved-addresses)) + +(defn reconcile-keypairs + [keypairs] + (let [received-keypairs (rpc->keypairs keypairs) + keypair-label #(if % :removed-keypairs :updated-keypairs) + {:keys [removed-keypairs + updated-keypairs] + :or {updated-keypairs [] + removed-keypairs []}} (group-by (comp keypair-label :removed) received-keypairs) + updated-keypairs-by-id (utils.collection/index-by :key-uid updated-keypairs) + updated-accounts-by-address (transduce (comp (mapcat :accounts) + (filter (comp not :chat)) + (map #(vector (:address %) %))) + conj + {} + updated-keypairs) + removed-keypairs-ids (set (map :key-uid removed-keypairs)) + removed-account-addresses (transduce (comp (mapcat :accounts) + (map :address)) + conj + #{} + removed-keypairs)] + {:removed-keypair-ids removed-keypairs-ids + :removed-account-addresses removed-account-addresses + :updated-keypairs-by-id updated-keypairs-by-id + :updated-accounts-by-address updated-accounts-by-address})) + +(defn- rename-keys-to-kebab-case + [m] + (set/rename-keys m (zipmap (keys m) (map transforms/->kebab-case-keyword (keys m))))) + +(defn rpc->suggested-routes + [suggested-routes] + (cond + (map? suggested-routes) + (into {} + (for [[k v] (rename-keys-to-kebab-case suggested-routes)] + [k (rpc->suggested-routes v)])) + + (vector? suggested-routes) + (map rpc->suggested-routes suggested-routes) + + :else suggested-routes)) + +(defn new->old-route-path + [new-path] + (let [to-bignumber (fn [k] (-> new-path k money/bignumber))] + {:approval-fee (to-bignumber :approval-fee) + :approval-l-1-fee (to-bignumber :approval-l-1-fee) + :bonder-fees (to-bignumber :tx-bonder-fees) + :token-fees (to-bignumber :tx-token-fees) + :from (:from-chain new-path) + :amount-in (:amount-in new-path) + :bridge-name (:processor-name new-path) + :amount-out (:amount-out new-path) + :approval-contract-address (:approval-contract-address new-path) + :approval-required (:approval-required new-path) + :to (:to-chain new-path) + :approval-amount-required (:approval-amount-required new-path) + :gas-amount (:tx-gas-amount new-path) + :router-input-params-uuid (:router-input-params-uuid new-path) + :gas-fees (router/transaction-gas-fees new-path) + :estimated-time (router/transaction-estimated-time new-path) + :approval-estimated-time (router/approval-estimated-time new-path) + :fees-by-mode (router/transaction-fees-by-mode new-path) + :tx-fee-mode (router/transaction-fee-mode new-path) + :nonce (router/nonce-field new-path :tx-nonce) + :suggested-tx-nonce (router/nonce-field new-path :suggested-tx-nonce) + :suggested-tx-gas-amount (:suggested-tx-gas-amount new-path) + :suggested-tx-gas-price (router/suggested-gas-price new-path)})) + +(schema/=> new->old-route-path + [:=> + [:cat router.schema/?route] + :map]) + +(defn tokens-never-loaded? + [db] + (nil? (get-in db [:wallet :ui :tokens-loading]))) + +(defn rpc->collectibles + [collectibles] + (->> collectibles + (cske/transform-keys transforms/->kebab-case-keyword) + (map #(update % :ownership collectible-utils/remove-duplicates-in-ownership)) + (map #(assoc % :unique-id (collectible-utils/get-collectible-unique-id %))) + vec)) + +(defn selected-keypair-keycard? + [db] + (let [keypairs (get-in db [:wallet :keypairs]) + selected-keypair-uid (get-in db [:wallet :ui :create-account :selected-keypair-uid]) + keypair (get keypairs selected-keypair-uid)] + (boolean (seq (:keycards keypair))))) + +(defn- transform-collectible + [{:keys [added updated removed]}] + (let [entry (first (remove nil? (concat added updated removed)))] + (when entry + {:contract-id {:chain-id (:chainID (:contractID entry)) + :address (:address (:contractID entry))} + :token-id (str (:tokenID entry))}))) + +(defn rpc->collectible-id + [collectible] + (-> collectible + transforms/json->clj + transform-collectible)) + +(defn- bridge-amount-greater-than-bonder-fees? + [{{token-decimals :decimals} :from-token + bonder-fees :tx-bonder-fees + amount-in :amount-in}] + (let [bonder-fees (utils.money/token->unit bonder-fees token-decimals) + amount-to-bridge (utils.money/token->unit amount-in token-decimals)] + (>= amount-to-bridge bonder-fees))) + +(defn- remove-multichain-routes + [routes] + (if (> (count routes) 1) + [] ;; if route is multichain, we remove it + routes)) + +(defn- remove-invalid-bonder-fees-routes + [routes] + (filter bridge-amount-greater-than-bonder-fees? routes)) + +(defn- ->old-route-paths + [routes] + (map new->old-route-path routes)) + +(def ^:private best-routes-fix + (comp ->old-route-paths + remove-invalid-bonder-fees-routes + remove-multichain-routes)) + +(def ^:private candidates-fix + (comp ->old-route-paths remove-invalid-bonder-fees-routes)) + +(defn fix-routes + [{:keys [UpdatedPrices] :as data}] + (-> data + (rpc->suggested-routes) + (update :best best-routes-fix) + (update :candidates candidates-fix) + (assoc :updated-token-prices UpdatedPrices))) diff --git a/src/status_im/contexts/wallet/data_store_test.cljs b/src/status_im/contexts/wallet/data_store_test.cljs new file mode 100644 index 00000000000..09cbd6439d3 --- /dev/null +++ b/src/status_im/contexts/wallet/data_store_test.cljs @@ -0,0 +1,277 @@ +(ns status-im.contexts.wallet.data-store-test + (:require + [cljs.test :refer-macros [deftest is testing]] + matcher-combinators.test + [status-im.contexts.wallet.data-store :as sut])) + +(def raw-account + {:path "m/44'/60'/0'/0/0" + :name "Account name" + :wallet true + :chat false + :emoji "🍙" + :colorId "blue" + :type "generated" + :createdAt 1716548742000 + :prodPreferredChainIds "1:42161" + :testPreferredChainIds "11155111:421614" + :removed false + :operable "fully"}) + +(def account + {:path "m/44'/60'/0'/0/0" + :emoji "🍙" + :color :blue + :wallet true + :default-account? true + :name "Account name" + :type :generated + :chat false + :test-preferred-chain-ids #{11155111 421614} + :watch-only? false + :prod-preferred-chain-ids #{1 42161} + :created-at 1716548742000 + :operable? true + :operable :fully + :removed false}) + +(defn make-raw-account + [overrides] + (merge raw-account overrides)) + +(def raw-keypair-profile + {:type "profile" + :key-uid "0x000" + :accounts [(make-raw-account {:key-uid "0x000" + :address "1x000" + :wallet false + :chat true}) + (make-raw-account {:key-uid "0x000" + :address "2x000"})]}) + +(def raw-keypair-seed-phrase + {:type "seed" + :key-uid "0x123" + :accounts [(make-raw-account {:key-uid "0x123" + :address "1x123"})]}) + +(def raw-keypair-private-key + {:type "key" + :key-uid "0x456" + :accounts [(make-raw-account {:key-uid "0x456" + :address "1x456" + :operable "no"})]}) + +(deftest chain-ids-string->set-test + (testing "chaind-ids-string->set splits and parses chain-ids from string" + (is (match? #{1 42161} + (sut/chain-ids-string->set (:prodPreferredChainIds raw-account)))) + (is (match? #{11155111 421614} + (sut/chain-ids-string->set (:testPreferredChainIds raw-account)))))) + +(deftest rpc->keypair-test + (testing "rpc->keypair transforms a profile keypair" + (is + (match? {:type :profile + :key-uid "0x000" + :lowest-operability :fully + :accounts [{:key-uid "0x000" + :operable :fully + :chat true + :wallet false + :address "1x000" + :path (:path raw-account) + :name (:name raw-account) + :emoji (:emoji raw-account) + :removed (:removed raw-account) + :color (keyword (:colorId raw-account)) + :created-at (:createdAt raw-account) + :prod-preferred-chain-ids (sut/chain-ids-string->set + (:prodPreferredChainIds raw-account)) + :test-preferred-chain-ids (sut/chain-ids-string->set + (:testPreferredChainIds raw-account))} + {:key-uid "0x000" + :operable :fully + :chat false + :wallet true + :address "2x000" + :path (:path raw-account) + :name (:name raw-account) + :emoji (:emoji raw-account) + :removed (:removed raw-account) + :color (keyword (:colorId raw-account)) + :created-at (:createdAt raw-account) + :prod-preferred-chain-ids (sut/chain-ids-string->set + (:prodPreferredChainIds raw-account)) + :test-preferred-chain-ids (sut/chain-ids-string->set + (:testPreferredChainIds raw-account))}]} + (sut/rpc->keypair raw-keypair-profile)))) + (testing "rpc->keypair transforms a seed-phrase keypair" + (is + (match? {:type :seed + :key-uid "0x123" + :lowest-operability :fully + :accounts [{:key-uid "0x123" + :address "1x123" + :operable :fully + :path (:path raw-account) + :name (:name raw-account) + :emoji (:emoji raw-account) + :wallet (:wallet raw-account) + :removed (:removed raw-account) + :color (keyword (:colorId raw-account)) + :created-at (:createdAt raw-account) + :prod-preferred-chain-ids (sut/chain-ids-string->set + (:prodPreferredChainIds raw-account)) + :test-preferred-chain-ids (sut/chain-ids-string->set + (:testPreferredChainIds raw-account))}]} + (sut/rpc->keypair raw-keypair-seed-phrase)))) + (testing "rpc->keypair transforms a raw private-key keypair with inoperable accounts" + (is + (match? {:type :key + :key-uid "0x456" + :lowest-operability :no + :accounts [{:key-uid "0x456" + :address "1x456" + :operable :no + :path (:path raw-account) + :name (:name raw-account) + :emoji (:emoji raw-account) + :wallet (:wallet raw-account) + :removed (:removed raw-account) + :color (keyword (:colorId raw-account)) + :created-at (:createdAt raw-account) + :prod-preferred-chain-ids (sut/chain-ids-string->set + (:prodPreferredChainIds raw-account)) + :test-preferred-chain-ids (sut/chain-ids-string->set + (:testPreferredChainIds + raw-account))}]} + (sut/rpc->keypair raw-keypair-private-key))))) + +(deftest rpc->keypairs-test + (testing "rpc->keypairs transforms and sorts raw keypairs" + (is + (match? [(sut/rpc->keypair raw-keypair-profile) + (sut/rpc->keypair raw-keypair-seed-phrase) + (sut/rpc->keypair raw-keypair-private-key)] + (sut/rpc->keypairs [raw-keypair-seed-phrase + raw-keypair-private-key + raw-keypair-profile]))))) + +(deftest reconcile-keypairs-test + (testing "reconcile-keypairs represents updated key pairs and accounts" + (is + (match-strict? + {:removed-keypair-ids #{} + :removed-account-addresses #{} + :updated-accounts-by-address {"1x123" (merge account + {:key-uid "0x123" + :address "1x123"}) + "1x456" (merge account + {:key-uid "0x456" + :address "1x456" + :operable? false + :operable :no})} + :updated-keypairs-by-id {"0x123" {:key-uid "0x123" + :type :seed + :lowest-operability :fully + :accounts [(merge account + {:key-uid "0x123" + :address "1x123"})]} + "0x456" {:key-uid "0x456" + :type :key + :lowest-operability :no + :accounts [(merge account + {:key-uid "0x456" + :address "1x456" + :operable? false + :operable :no})]}}} + (sut/reconcile-keypairs [raw-keypair-seed-phrase + raw-keypair-private-key])))) + (testing "reconcile-keypairs represents removed key pairs and accounts" + (is + (match-strict? + {:removed-keypair-ids #{"0x456"} + :removed-account-addresses #{"1x456"} + :updated-accounts-by-address {"1x123" (merge account + {:key-uid "0x123" + :address "1x123"})} + :updated-keypairs-by-id {"0x123" {:key-uid "0x123" + :type :seed + :lowest-operability :fully + :accounts [(merge account + {:key-uid "0x123" + :address "1x123"})]}}} + (sut/reconcile-keypairs [raw-keypair-seed-phrase + (assoc raw-keypair-private-key :removed true)])))) + (testing "reconcile-keypairs ignores chat accounts inside updated accounts" + (is + (match-strict? + {:removed-keypair-ids #{} + :removed-account-addresses #{} + :updated-accounts-by-address {"2x000" (merge account + {:key-uid "0x000" + :address "2x000" + :chat false + :wallet true + :default-account? true})} + :updated-keypairs-by-id {"0x000" {:key-uid "0x000" + :type :profile + :lowest-operability :fully + :accounts [(merge account + {:key-uid "0x000" + :address "1x000" + :chat true + :wallet false + :default-account? false}) + (merge account + {:key-uid "0x000" + :address "2x000" + :chat false + :wallet true + :default-account? true})]}}} + (sut/reconcile-keypairs [raw-keypair-profile]))))) + +(def mock-db + {:wallet {:keypairs + {"0x123" {:type "key" + :key-uid "0x123" + :keycards [{:id "keycard1"}]} + "0x456" {:type "key" + :key-uid "0x456" + :keycards []}} + :ui {:create-account {:selected-keypair-uid "0x123"}}}}) + +(deftest selected-keypair-keycard?-test + (testing "returns true when the selected keypair has keycards" + (is (true? (sut/selected-keypair-keycard? mock-db)))) + + (testing "returns false when the selected keypair does not have keycards" + (let [db (assoc-in mock-db [:wallet :ui :create-account :selected-keypair-uid] "0x456")] + (is (false? (sut/selected-keypair-keycard? db))))) + + (testing "returns false when the selected keypair does not exist" + (let [db (assoc-in mock-db [:wallet :ui :create-account :selected-keypair-uid] "0x789")] + (is (false? (sut/selected-keypair-keycard? db))))) + + (testing "returns false when keypairs map is empty" + (let [db (assoc-in mock-db [:wallet :keypairs] {})] + (is (false? (sut/selected-keypair-keycard? db))))) + + (testing "returns false when no keypair is selected" + (let [db (assoc-in mock-db [:wallet :ui :create-account :selected-keypair-uid] nil)] + (is (false? (sut/selected-keypair-keycard? db))))) + + (testing "returns false when db does not contain wallet data" + (let [db {}] + (is (false? (sut/selected-keypair-keycard? db)))))) + +(deftest rpc->collectible-id-test + (testing "Transforms JSON with `updated` key into collectible ID" + (let + [json + "{\"updated\": [{\"contractID\": {\"chainID\": 222, \"address\": \"0x123456\"}, \"tokenID\": 77}]}" + expected {:contract-id {:chain-id 222 + :address "0x123456"} + :token-id "77"}] + (is (= expected (sut/rpc->collectible-id json)))))) diff --git a/src/status_im/contexts/wallet/db.cljs b/src/status_im/contexts/wallet/db.cljs new file mode 100644 index 00000000000..a2783db1e0f --- /dev/null +++ b/src/status_im/contexts/wallet/db.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.wallet.db + (:require + [status-im.contexts.wallet.db-path :as db-path])) + +(def defaults + {:ui {;; Note: we set it to nil by default to differentiate when the user logs + ;; in and the device is offline, versus re-fetching when offline and + ;; tokens already exist in the app-db. + :tokens-loading nil + :active-tab :assets}}) + +(defn send + [db] + (get-in db db-path/send)) diff --git a/src/status_im/contexts/wallet/db_path.cljs b/src/status_im/contexts/wallet/db_path.cljs new file mode 100644 index 00000000000..71c148efea4 --- /dev/null +++ b/src/status_im/contexts/wallet/db_path.cljs @@ -0,0 +1,4 @@ +(ns status-im.contexts.wallet.db-path) + +(def ^:const swap [:wallet :ui :swap]) +(def ^:const send [:wallet :ui :send]) diff --git a/src/status_im/contexts/wallet/effects.cljs b/src/status_im/contexts/wallet/effects.cljs new file mode 100644 index 00000000000..881ca4b9655 --- /dev/null +++ b/src/status_im/contexts/wallet/effects.cljs @@ -0,0 +1,134 @@ +(ns status-im.contexts.wallet.effects + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [promesa.core :as promesa] + [status-im.common.json-rpc.events :as json-rpc] + [status-im.contexts.profile.recover.effects :as profile.recover.effects] + [status-im.contexts.wallet.rpc :as wallet-rpc] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(defn- error-message + [kw] + (-> kw symbol str)) + +(rf/reg-fx + :effects.wallet/create-account-from-mnemonic + (fn [{:keys [mnemonic-phrase paths on-success] + :or {paths []}}] + (let [phrase (condp #(%1 %2) mnemonic-phrase + string? mnemonic-phrase + coll? (string/join " " mnemonic-phrase) + (log/error "Unexpected value " mnemonic-phrase))] + (-> phrase + (native-module/validate-mnemonic) + (promesa/then #(native-module/create-account-from-mnemonic + {:MnemonicPhrase phrase :paths paths} + on-success)))))) + +(defn create-account-from-private-key + [private-key] + (-> private-key + (security/safe-unmask-data) + (native-module/create-account-from-private-key) + (promesa/then (fn [result] + (let [{:keys [address emojiHash keyUid + publicKey privateKey]} (transforms/json->clj result)] + {:address address + :emoji-hash emojiHash + :key-uid keyUid + :public-key publicKey + :private-key (security/mask-data privateKey)}))))) + +(rf/reg-fx + :effects.wallet/create-account-from-private-key + (fn [{:keys [private-key on-success on-error]}] + (-> (create-account-from-private-key private-key) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(defn make-seed-phrase-fully-operable + [mnemonic password] + (promesa/create + (fn [resolver rejecter] + (json-rpc/call {:method "accounts_makeSeedPhraseKeypairFullyOperable" + :params [(security/safe-unmask-data mnemonic) + (security/safe-unmask-data password)] + :on-error (fn [error] + (rejecter (ex-info (str error) {:error error}))) + :on-success (fn [value] + (resolver {:value value}))})))) + +(defn import-missing-keypair-by-seed-phrase + [keypair-key-uid seed-phrase password] + (-> (profile.recover.effects/validate-mnemonic seed-phrase) + (promesa/then + (fn [{:keys [key-uid]}] + (if (not= keypair-key-uid key-uid) + (promesa/rejected + (ex-info + (error-message :import-missing-keypair-by-seed-phrase/import-error) + {:hint :incorrect-seed-phrase-for-keypair})) + (make-seed-phrase-fully-operable seed-phrase password)))) + (promesa/catch + (fn [error] + (promesa/rejected + (ex-info + (error-message :import-missing-keypair-by-seed-phrase/import-error) + (ex-data error))))))) + +(rf/reg-fx + :effects.wallet/import-missing-keypair-by-seed-phrase + (fn [{:keys [keypair-key-uid seed-phrase password on-success on-error]}] + (-> (import-missing-keypair-by-seed-phrase keypair-key-uid seed-phrase password) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(defn verify-private-key-for-keypair + [keypair-key-uid private-key] + (-> (create-account-from-private-key private-key) + (promesa/then + (fn [{:keys [key-uid] :as result}] + (if (= keypair-key-uid key-uid) + result + (promesa/rejected + (ex-info + (error-message :verify-private-key-for-keypair/verification-error) + {:hint :incorrect-private-key-for-keypair}))))))) + +(rf/reg-fx + :effects.wallet/verify-private-key-for-keypair + (fn [{:keys [keypair-key-uid private-key on-success on-error]}] + (-> (verify-private-key-for-keypair keypair-key-uid private-key) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(defn sign-payloads + [payloads password] + (-> (promesa/all + (for [{:keys [message address]} payloads] + (promesa/let [signature (wallet-rpc/sign-message message address password)] + {:message message + :address address + :signature signature}))) + (promesa/catch (fn [err] + (throw (ex-info "Failed to sign payloads" + {:error err + :code :error/sign-payloads})))))) + +(rf/reg-fx + :effects.wallet/sign-payloads + (fn [{:keys [payloads password on-success on-error]}] + (-> (sign-payloads payloads (security/safe-unmask-data password)) + (promesa/then on-success) + (promesa/catch on-error)))) + +(rf/reg-fx + :effects.wallet/sign-message + (fn [{:keys [message address password on-success on-error]}] + (-> (wallet-rpc/sign-message message address (security/safe-unmask-data password)) + (promesa/then on-success) + (promesa/catch on-error)))) diff --git a/src/status_im/contexts/wallet/events.cljs b/src/status_im/contexts/wallet/events.cljs new file mode 100644 index 00000000000..7c1c9cbefb9 --- /dev/null +++ b/src/status_im/contexts/wallet/events.cljs @@ -0,0 +1,892 @@ +(ns status-im.contexts.wallet.events + (:require + [camel-snake-kebab.extras :as cske] + [cljs-time.coerce :as time-coerce] + [clojure.set] + [clojure.string :as string] + [react-native.platform :as platform] + [status-im.constants :as constants] + [status-im.contexts.network.db :as network.db] + [status-im.contexts.profile.db :as profile.db] + [status-im.contexts.settings.wallet.effects] + [status-im.contexts.settings.wallet.events] + [status-im.contexts.wallet.account.db :as account.db] + [status-im.contexts.wallet.common.activity-tab.events] + [status-im.contexts.wallet.common.transaction-settings.core :as transaction-settings] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.data-store :as data-store] + [status-im.contexts.wallet.db :as db] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.contexts.wallet.item-types :as item-types] + [status-im.contexts.wallet.networks.core :as networks] + [status-im.contexts.wallet.networks.db :as networks.db] + status-im.contexts.wallet.networks.events + [status-im.contexts.wallet.send.utils :as send-utils] + [status-im.contexts.wallet.sheets.network-selection.view :as network-selection] + [status-im.contexts.wallet.tokens.events] + [status-im.feature-flags :as ff] + [taoensso.timbre :as log] + [utils.collection] + [utils.ethereum.eip.eip55 :as eip55] + [utils.i18n :as i18n] + [utils.number] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(rf/reg-event-fx :wallet/show-account-created-toast + (fn [{:keys [db]} [address]] + (let [account-name (get-in db [:wallet :accounts address :name])] + {:db (update db :wallet dissoc :navigate-to-account :new-account?) + :fx [[:dispatch + [:toasts/upsert + {:id :new-wallet-account-created + :type :positive + :text (i18n/label :t/account-created {:name account-name})}]]]}))) + +(rf/reg-event-fx :wallet/navigate-to-account + (fn [{:keys [db]} [address]] + {:db (assoc-in db [:wallet :current-viewing-account-address] address) + :fx [[:dispatch [:navigate-to :screen/wallet.accounts]]]})) + +(rf/reg-event-fx :wallet/navigate-to-account-within-stack + (fn [{:keys [db]} [address]] + {:db (assoc-in db [:wallet :current-viewing-account-address] address) + :fx [[:dispatch [:navigate-to-within-stack [:screen/wallet.accounts :screen/shell-stack]]]]})) + +(rf/reg-event-fx :wallet/navigate-to-new-account + (fn [{:keys [db]} [address]] + {:db (assoc-in db [:wallet :current-viewing-account-address] address) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch [:navigate-to :screen/wallet.accounts]] + [:dispatch [:wallet/show-account-created-toast address]]]})) + +(rf/reg-event-fx :wallet/select-account-tab + (fn [{:keys [db]} [tab]] + {:db (assoc-in db [:wallet :ui :account-page :active-tab] tab) + :fx [(if (= tab :activity) + [:dispatch [:wallet/fetch-activities-for-current-account]] + [:dispatch [:wallet/stop-activity-filter-session]])]})) + +(rf/reg-event-fx :wallet/select-home-tab + (fn [{:keys [db]} [tab]] + {:db (assoc-in db [:wallet :ui :active-tab] tab)})) + +(rf/reg-event-fx :wallet/clear-account-tab + (fn [{:keys [db]}] + {:db (assoc-in db [:wallet :ui :account-page :active-tab] nil) + :fx [[:dispatch [:wallet/stop-activity-filter-session]]]})) + +(rf/reg-event-fx :wallet/switch-current-viewing-account + (fn [{:keys [db]} [address]] + (let [{:keys [tx-type]} (db/send db) + bridge-tx? (= tx-type :tx/bridge)] + {:db (cond-> db + :always (assoc-in [:wallet :current-viewing-account-address] address) + bridge-tx? (update-in db-path/send assoc :to-address address))}))) + +(rf/reg-event-fx :wallet/clean-current-viewing-account + (fn [{:keys [db]} [ignore-just-completed-transaction?]] + (let [{:keys [entry-point just-completed-transaction? + collectible-multiple-owners?]} (db/send db) + entry-point-wallet-home? (= entry-point :screen/wallet-stack)] + {:db (cond-> db + (and (not entry-point) + (not ignore-just-completed-transaction?) + (not just-completed-transaction?)) + (update :wallet dissoc :current-viewing-account-address) + + (and entry-point-wallet-home? (not collectible-multiple-owners?)) + (update-in db-path/send dissoc :entry-point) + + (and entry-point-wallet-home? + (not just-completed-transaction?)) + (update :wallet dissoc :current-viewing-account-address))}))) + +(rf/reg-event-fx :wallet/close-account-page + (fn [{:keys [db]}] + (let [just-completed-transaction? (get-in db (conj db-path/send :just-completed-transaction?))] + {:db (update db :wallet dissoc :current-viewing-account-address) + :fx [(when-not just-completed-transaction? + [:dispatch [:wallet/clear-account-tab]]) + [:dispatch [:wallet/stop-activity-filter-session]]]}))) + +(defn log-rpc-error + [_ [{:keys [event params]} error]] + (log/warn (str "[wallet] Failed to " event) + {:params params + :error error})) + +(rf/reg-event-fx :wallet/log-rpc-error log-rpc-error) + +(def refresh-accounts-fx-dispatches + [[:dispatch [:wallet/get-wallet-token-for-all-accounts]] + [:dispatch [:wallet/request-collectibles-for-all-accounts {:new-request? true}]]]) + +(rf/reg-event-fx + :wallet/fetch-assets-for-address + (fn [_ [address]] + {:fx [[:dispatch [:wallet/get-wallet-token-for-accounts [address]]] + [:dispatch [:wallet/request-collectibles-for-account address]]]})) + +(defn- reconcile-accounts + [db-accounts-by-address new-accounts] + (reduce + (fn [res {:keys [address] :as account}] + ;; Because we add extra fields (tokens and collectibles) into the RPC + ;; response from accounts_getAccounts, if we are offline we want to keep + ;; the old balances in the accounts, thus we merge the up-to-date account + ;; from status-go into the cached accounts. We also merge when online + ;; because we will re-fetch balances anyway. + ;; + ;; Refactor improvement: don't augment entities from status-go, store + ;; tokens and collectibles in separate keys in the app-db indexed by + ;; account address. + (assoc res + address + (-> (get db-accounts-by-address address) + (merge account) + ;; These should not be cached, otherwise when going + ;; offline->online collectibles won't be fetched. + (dissoc :current-collectible-idx :has-more-collectibles?)))) + {} + new-accounts)) + +(rf/reg-event-fx :wallet/get-accounts-success + (fn [{:keys [db]} [accounts]] + (let [wallet-accounts (data-store/rpc->accounts accounts) + wallet-db (get db :wallet) + new-account? (:new-account? wallet-db) + navigate-to-account (:navigate-to-account wallet-db)] + {:db (update-in db [:wallet :accounts] reconcile-accounts wallet-accounts) + :fx (concat (when (or (data-store/tokens-never-loaded? db) + (network.db/online? db)) + refresh-accounts-fx-dispatches) + [(when new-account? + [:dispatch [:wallet/navigate-to-new-account navigate-to-account]])])}))) + +(rf/reg-event-fx :wallet/get-accounts + (fn [] + {:fx [[:json-rpc/call + [{:method "accounts_getAccounts" + :on-success [:wallet/get-accounts-success] + :on-error [:wallet/log-rpc-error {:event :wallet/get-accounts}]}]]]})) + +(rf/reg-event-fx + :wallet/save-account + (fn [_ [{:keys [account on-success]}]] + {:fx [[:json-rpc/call + [{:method "accounts_saveAccount" + :params [(data-store/<-account account)] + :on-success (fn [] + (rf/dispatch [:wallet/get-accounts]) + (when (fn? on-success) + (on-success))) + :on-error [:wallet/log-rpc-error {:event :wallet/save-account}]}]]]})) + +(rf/reg-event-fx + :wallet/show-account-deleted-toast + (fn [_ [toast-message]] + {:fx [[:dispatch [:toasts/upsert {:type :positive :text toast-message}]]]})) + +(rf/reg-event-fx + :wallet/remove-account-success + (fn [_ [toast-message _]] + {:fx [[:dispatch [:wallet/clean-current-viewing-account]] + [:dispatch [:wallet/get-accounts]] + [:dispatch [:wallet/get-keypairs]] + [:dispatch-later + {:ms 100 + :dispatch [:hide-bottom-sheet]}] + [:dispatch-later + {:ms 100 + :dispatch [:pop-to-root :screen/shell-stack]}] + [:dispatch-later + {:ms 100 + :dispatch [:wallet/show-account-deleted-toast toast-message]}]]})) + +(rf/reg-event-fx + :wallet/remove-account + (fn [_ [{:keys [address toast-message]}]] + {:fx [[:json-rpc/call + [{:method "accounts_deleteAccount" + :params [address] + :on-success [:wallet/remove-account-success toast-message] + :on-error [:wallet/log-rpc-error {:event :wallet/remove-account}]}]]]})) + +(rf/reg-event-fx :wallet/get-wallet-token-for-all-accounts + (fn [{:keys [db]}] + (let [addresses (->> (get-in db [:wallet :accounts]) + (vals) + (keep :address) + (vec))] + {:fx [[:dispatch [:wallet/get-wallet-token-for-accounts addresses]]]}))) + +(rf/reg-event-fx :wallet/get-wallet-token-for-accounts + (fn [{:keys [db]} [addresses]] + {:db (reduce + (fn [db address] + (assoc-in db [:wallet :ui :tokens-loading address] true)) + db + addresses) + :fx [[:json-rpc/call + [{:method "wallet_fetchOrGetCachedWalletBalances" + :params [addresses true] + :on-success [:wallet/store-wallet-token addresses] + :on-error [:wallet/get-wallet-token-for-accounts-failed addresses]}]]]})) + +(rf/reg-event-fx :wallet/reload-cached-balances + (fn [{:keys [db]}] + (let [addresses (account.db/get-accounts-addresses db)] + {:fx [[:json-rpc/call + [{:method "wallet_fetchOrGetCachedWalletBalances" + :params [addresses false] + :on-success [:wallet/store-wallet-token addresses] + :on-error [:wallet/get-wallet-token-for-accounts-failed addresses]}]]]}))) + +(rf/reg-event-fx + :wallet/get-wallet-token-for-accounts-failed + (fn [{:keys [db]} [addresses error]] + (log/info "failed to get wallet token " + {:error error + :event :wallet/get-wallet-token-for-accounts + :params addresses}) + {:fx [[:dispatch [:wallet/get-last-wallet-token-update-if-needed]]] + :db (reduce + (fn [db address] + (assoc-in db [:wallet :ui :tokens-loading address] false)) + db + addresses)})) + +(rf/reg-event-fx + :wallet/set-all-tokens-loading + (fn [{:keys [db]}] + {:db (reduce (fn [db address] + (assoc-in db [:wallet :ui :tokens-loading address] true)) + db + (account.db/get-accounts-addresses db))})) + +(rf/reg-event-fx + :wallet/reset-accounts-tokens + (fn [{:keys [db]}] + (let [reset-tokens (fn [stored-accounts] + (reduce-kv + (fn [accounts address _] + (update accounts address assoc :tokens [])) + stored-accounts + stored-accounts))] + {:db (update-in db [:wallet :accounts] reset-tokens) + :fx [[:dispatch [:wallet/set-all-tokens-loading]]]}))) + +(rf/reg-event-fx + :wallet/store-wallet-token + (fn [{:keys [db]} [addresses raw-tokens-data]] + (let [supported-chains-by-token-symbol (get-in db [:wallet :tokens :supported-chains-by-symbol]) + profile-currency (get-in db [:profile/profile :currency]) + tokens (data-store/rpc->tokens raw-tokens-data + supported-chains-by-token-symbol) + add-tokens (fn [stored-accounts tokens-per-account] + (reduce-kv + (fn [accounts address tokens-data] + (if (contains? accounts address) + (update accounts address assoc :tokens tokens-data) + accounts)) + stored-accounts + tokens-per-account)) + symbols (reduce-kv + (fn [acc _ tokens-data] + (into acc (map :symbol tokens-data))) + #{} + tokens)] + {:db (-> db + (update-in [:wallet :accounts] add-tokens tokens) + ((fn [db] + (reduce (fn [db address] + (assoc-in db [:wallet :ui :tokens-loading address] false)) + db + addresses)))) + :fx [[:dispatch [:wallet/get-last-wallet-token-update-if-needed]] + [:effects.wallet.tokens/fetch-market-values + {:symbols symbols + :currency profile-currency + :on-success [:wallet.tokens/store-market-values] + :on-error [:wallet.tokens/fetch-market-values-failed]}] + [:effects.wallet.tokens/fetch-prices + {:symbols symbols + :currencies [constants/profile-default-currency profile-currency] + :on-success [:wallet.tokens/store-prices] + :on-error [:wallet.tokens/fetch-prices-failed]}]]}))) + +(rf/reg-event-fx + :wallet/get-last-wallet-token-update-if-needed + (fn [{:keys [db]}] + (let [all-tokens-loaded? (->> (get-in db [:wallet :ui :tokens-loading]) + vals + (every? false?))] + (when all-tokens-loaded? + {:fx [[:json-rpc/call + [{:method "wallet_getLastWalletTokenUpdate" + :params [] + :on-success [:wallet/get-last-wallet-token-update-success] + :on-error [:wallet/log-rpc-error + {:event :wallet/get-last-wallet-token-update-if-needed}]}]]]})))) + +(rf/reg-event-fx + :wallet/get-last-wallet-token-update-success + (fn [{:keys [db]} [data]] + (let [last-updates (reduce (fn [acc [k v]] + (assoc acc k (time-coerce/from-long (* 1000 v)))) + {} + data)] + {:db (assoc-in db [:wallet :ui :last-updates-per-address] last-updates)}))) + +(rf/defn scan-address-success + {:events [:wallet/scan-address-success]} + [{:keys [db]} address] + {:db (assoc-in db [:wallet :ui :scanned-address] address)}) + +(rf/defn clean-scanned-address + {:events [:wallet/clean-scanned-address]} + [{:keys [db]}] + {:db (update-in db [:wallet :ui] dissoc :scanned-address)}) + +(rf/reg-event-fx :wallet/add-account-success + (fn [{:keys [db]} [address]] + {:db (-> db + (assoc-in [:wallet :navigate-to-account] address) + (assoc-in [:wallet :new-account?] true)) + :fx [[:dispatch [:wallet/get-accounts]] + [:dispatch [:wallet/get-keypairs]] + [:dispatch [:wallet/clear-create-account]]]})) + +(rf/reg-event-fx :wallet/add-account + (fn [_ + [{:keys [key-uid password account-name emoji color type] :or {type :generated}} + {:keys [public-key address path]}]] + (let [lowercase-address (some-> address + string/lower-case) + account-config {:key-uid (when (= type :generated) key-uid) + :wallet false + :chat false + :type type + :name account-name + :emoji emoji + :path path + :address lowercase-address + :public-key public-key + :colorID color}] + {:fx [[:json-rpc/call + [{:method "accounts_addAccount" + :params [(when (= type :generated) + (security/safe-unmask-data password)) + account-config] + :on-success [:wallet/add-account-success lowercase-address] + :on-error [:wallet/log-rpc-error + {:event :wallet/add-account + :params account-config}]}]]]}))) + +(defn get-keypairs + [_] + {:fx [[:json-rpc/call + [{:method "accounts_getKeypairs" + :params [] + :on-success [:wallet/get-keypairs-success] + :on-error [:wallet/log-rpc-error {:event :wallet/get-keypairs}]}]]]}) + +(rf/reg-event-fx :wallet/get-keypairs get-keypairs) + +(defn- remove-unsupported-bridge-networks-from-token + [token] + (-> token + (update :networks + (fn [networks] + (filter networks/bridge-supported-network? networks))) + (update :balances-per-chain + (fn [balances] + (into {} + (filter (fn [[_ balance]] + (networks/bridge-supported-network? balance)) + balances)))))) + +(rf/reg-event-fx :wallet/bridge-select-token + (fn [{:keys [db]} + [{:keys [token token-symbol stack-id network owners networks start-flow?] :as params}]] + (let [{:keys [wallet]} db + bridge-supported-networks (networks/filter-bridge-supported-networks networks) + unique-owner (when (= (count owners) 1) + (first owners)) + token (if (and unique-owner (nil? token)) + (let [token (utils/get-token-from-account db + token-symbol + unique-owner)] + (utils/token-with-balance token bridge-supported-networks)) + token) + token (remove-unsupported-bridge-networks-from-token token) + missing-recipient? (-> db db/send :to-address nil?) + to-address (or unique-owner + (-> db :wallet :current-viewing-account-address) + (utils/get-default-account (-> db :wallet :accounts))) + view-id (:view-id db) + root-screen? (or (= view-id :screen/wallet-stack) (nil? view-id)) + multi-account-balance? (-> (utils/get-accounts-with-token-balance (:accounts wallet) + token) + (count) + (> 1)) + account-not-defined? (and (not unique-owner) multi-account-balance?) + networks-with-balance (when (and token (:balances-per-chain token)) + (filter #(not= (:balance %) "0") + (vals (:balances-per-chain token)))) + balance-in-only-one-network? (when networks-with-balance (= (count networks-with-balance) 1)) + networks (networks.db/get-active-networks db) + network (if balance-in-only-one-network? + (first (filter #(= (:chain-id %) + (:chain-id (first networks-with-balance))) + networks)) + network)] + {:db (cond-> db + :always (update-in db-path/send assoc :tx-type :tx/bridge) + token (update-in db-path/send assoc :token token) + token-symbol (update-in db-path/send assoc :token-symbol token-symbol) + network (update-in db-path/send assoc :network network) + missing-recipient? (update-in db-path/send assoc :to-address to-address)) + :fx (cond + ;; If the token has a balance in more than one account and this was dispatched from the + ;; general wallet screen, open the account selection screen. + (and account-not-defined? root-screen?) + [[:dispatch [:open-modal :screen/wallet.select-from]]] + + ;; If the token has a balance in only one account (or this was dispatched from the + ;; account screen) and the network is already set, navigate forward in the bridge flow. + (some? network) + [(when to-address [:dispatch [:wallet/switch-current-viewing-account to-address]]) + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id :wallet-bridge-flow}]]] + + ;; If we know which account to bridge the token from but the network is not set yet, + ;; show the network selection drawer. + :else + [[:dispatch [:wallet/switch-current-viewing-account to-address]] + [:dispatch + [:show-bottom-sheet + {:content (fn [] + [network-selection/view + {:title (i18n/label :t/select-network-to-bridge-from) + :token-symbol (or token-symbol (:symbol token)) + :source :bridge + :on-select-network (fn [network] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:wallet/bridge-select-token + (assoc params :network network)]))}])}]]])}))) + +(rf/reg-event-fx + :wallet/start-bridge + (fn [{:keys [db]}] + {:db (update-in db db-path/send assoc :tx-type :tx/bridge) + :fx [[:dispatch + [:wallet/wizard-navigate-forward + {:start-flow? true + :flow-id :wallet-bridge-flow}]]]})) + +(rf/reg-event-fx + :wallet/set-send-tx-type + (fn [{:keys [db]} [type]] + {:db (update-in db db-path/send assoc :tx-type type)})) + +(rf/reg-event-fx :wallet/select-bridge-network + (fn [{:keys [db]} [{:keys [network-chain-id stack-id]}]] + {:db (update-in db db-path/send assoc :bridge-to-chain-id network-chain-id) + :fx [[:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :flow-id :wallet-bridge-flow}]]]})) + +(rf/reg-event-fx + :wallet/find-ens + (fn [{:keys [db]} [input contacts on-error-fn]] + (let [result (if (empty? input) + [] + (filter #(string/starts-with? (or (:ens-name %) "") input) contacts))] + (if (and input (empty? result)) + {:fx [[:dispatch [:wallet/search-ens input on-error-fn ".stateofus.eth"]]]} + {:db (-> db + (assoc-in [:wallet :ui :search-address :local-suggestions] + (map #(assoc % :type item-types/saved-address) result)) + (assoc-in [:wallet :ui :search-address :valid-ens-or-address?] + (not-empty result)))})))) + +(rf/reg-event-fx + :wallet/search-ens + (fn [{db :db} [input on-error-fn domain]] + (let [ens (if (string/includes? input ".") + input + (str input domain)) + chain-id (networks.db/get-chain-id db :mainnet)] + {:fx [[:json-rpc/call + [{:method "ens_addressOf" + :params [chain-id ens] + :on-success #(rf/dispatch [:wallet/set-ens-address % ens]) + :on-error (fn [] + (if (= domain ".stateofus.eth") + (rf/dispatch [:wallet/search-ens input on-error-fn ".eth"]) + (do + (rf/dispatch [:wallet/set-ens-address nil ens]) + (on-error-fn))))}]]]}))) + +(rf/reg-event-fx + :wallet/set-ens-address + (fn [{:keys [db]} [result ens]] + (let [suggestion (if result + [{:type item-types/address + :ens ens + :address (eip55/address->checksum result) + :full-address (eip55/address->checksum result)}] + [])] + {:db (-> db + (assoc-in [:wallet :ui :search-address :local-suggestions] suggestion) + (assoc-in [:wallet :ui :search-address :valid-ens-or-address?] (boolean result)))}))) + +(rf/reg-event-fx :wallet/address-validation-success + (fn [{:keys [db]}] + {:db (update-in db + [:wallet :ui :search-address] + assoc + :valid-ens-or-address? true + :loading? false)})) + +(rf/reg-event-fx :wallet/address-validation-failed + (fn [{:keys [db]}] + {:db (update-in db + [:wallet :ui :search-address] + assoc + :valid-ens-or-address? false + :loading? false)})) + +(rf/reg-event-fx :wallet/clean-local-suggestions + (fn [{:keys [db]}] + {:db (-> db + (assoc-in [:wallet :ui :search-address :local-suggestions] []) + (assoc-in [:wallet :ui :search-address :valid-ens-or-address?] false))})) + +(rf/reg-event-fx :wallet/searching-address + (fn [{:keys [db]}] + {:db (assoc-in db [:wallet :ui :search-address :loading?] true)})) + +(rf/reg-event-fx :wallet/reload + (fn [{:keys [db]}] + (let [supported-chains-by-symbol (get-in db [:wallet :tokens :supported-chains-by-symbol]) + symbols (mapv name (keys supported-chains-by-symbol)) + profile-currency (get-in db [:profile/profile :currency])] + {:fx [[:dispatch [:wallet/get-wallet-token-for-all-accounts]] + [:effects.wallet.tokens/fetch-prices + {:symbols symbols + :currencies [constants/profile-default-currency profile-currency] + :on-success [:wallet.tokens/store-prices] + :on-error [:wallet.tokens/fetch-prices-failed]}]] + :db (assoc-in db [:wallet :ui :loading :prices] true)}))) + +(rf/reg-event-fx :wallet/start-wallet + (fn [_] + {:fx [[:json-rpc/call + [{:method "wallet_startWallet" + :on-error [:wallet/log-rpc-error {:event :wallet/start-wallet}]}]]]})) + +(rf/reg-event-fx :wallet/initialize + (fn [] + {:fx [[:dispatch [:wallet/start-wallet]] + [:dispatch [:wallet/get-networks]] + [:dispatch [:wallet/get-accounts]] + [:dispatch [:wallet/get-keypairs]] + [:dispatch [:wallet/get-saved-addresses]] + (when (ff/enabled? ::ff/wallet.wallet-connect) + [:dispatch-later [{:ms 500 :dispatch [:wallet-connect/init]}]])]})) + +(rf/reg-event-fx :wallet/share-account + (fn [_ [{:keys [content title]}]] + {:fx [[:effects.share/open + {:options (if platform/ios? + {:activityItemSources + [{:placeholderItem {:type :text + :content content} + :item {:default {:type :text + :content content}} + :linkMetadata {:title title}}]} + {:title title + :subject title + :message content})}]]})) + +(rf/reg-event-fx + :wallet/blockchain-status-changed + (fn [{:keys [db]} [{:keys [message]}]] + (let [chains (-> (transforms/json->clj message) + (update-keys (comp utils.number/parse-int name))) + down-chain-ids (-> (select-keys chains + (for [[k v] chains :when (= v "down")] k)) + keys) + test-networks-enabled? (profile.db/testnet? db) + chain-ids (networks.db/get-active-chain-ids db) + chains-filtered (remove #(not + (contains? chain-ids %)) + down-chain-ids) + chains-down? (and (network.db/online? db) (seq chains-filtered)) + chain-names (when chains-down? + (->> (map (partial networks.db/get-network-details db) + chains-filtered) + (map :full-name) + distinct + (string/join ", ")))] + (when (seq down-chain-ids) + (log/info "[wallet] Chain(s) down: " down-chain-ids) + (log/info "[wallet] Chain name(s) down: " chain-names) + (log/info "[wallet] Test network enabled: " (boolean test-networks-enabled?))) + + ;; NOTE : We used to show an error toast, but disabled it because the down + ;; signal is sent randomly. Needs to be investigated and enabled again ! + ;; Context: https://github.com/status-im/status-mobile/issues/21054 + + {:db (assoc-in db [:wallet :statuses :blockchains] chains)}))) + +(rf/reg-event-fx + :wallet/get-crypto-on-ramps-success + (fn [{:keys [db]} [data]] + (let [crypto-on-ramps (cske/transform-keys transforms/->kebab-case-keyword data)] + {:db (assoc-in db + [:wallet :crypto-on-ramps] + {:one-time (remove #(string/blank? (:site-url %)) crypto-on-ramps) + :recurrent (remove #(string/blank? (:recurrent-site-url %)) crypto-on-ramps)})}))) + +(rf/reg-event-fx + :wallet/get-crypto-on-ramps + (fn [_] + {:fx [[:json-rpc/call + [{:method "wallet_getCryptoOnRamps" + :on-success [:wallet/get-crypto-on-ramps-success] + :on-error [:wallet/log-rpc-error {:event :wallet/get-crypto-on-ramps}]}]]]})) + +(rf/reg-event-fx + :wallet/resolve-ens + (fn [{db :db} [{:keys [ens on-success on-error]}]] + (let [chain-id (networks.db/get-chain-id db :mainnet)] + {:fx [[:json-rpc/call + [{:method "ens_addressOf" + :params [chain-id ens] + :on-success on-success + :on-error on-error}]]]}))) + +(rf/reg-event-fx + :wallet/process-keypair-from-backup + (fn [_ [{:keys [backedUpKeypair]}]] + {:fx [[:dispatch [:wallet/reconcile-keypairs [backedUpKeypair]]]]})) + +(rf/reg-event-fx + :wallet/process-watch-only-account-from-backup + (fn [_ [{:keys [backedUpWatchOnlyAccount]}]] + {:fx [[:dispatch [:wallet/reconcile-watch-only-accounts [backedUpWatchOnlyAccount]]]]})) + +(defn reconcile-watch-only-accounts + [{:keys [db]} [watch-only-accounts]] + (let [existing-accounts-by-address (get-in db [:wallet :accounts]) + group-label #(if % :removed-accounts :updated-accounts) + {:keys [removed-accounts + updated-accounts]} (->> watch-only-accounts + (map data-store/rpc->account) + (group-by (comp group-label :removed))) + existing-account-addresses (set (keys existing-accounts-by-address)) + removed-account-addresses (set (map :address removed-accounts)) + updated-account-addresses (set (map :address updated-accounts)) + new-account-addresses (clojure.set/difference updated-account-addresses + existing-account-addresses)] + (cond-> {:db (update-in db + [:wallet :accounts] + (fn [existing-accounts] + (merge-with merge + (apply dissoc existing-accounts removed-account-addresses) + (utils.collection/index-by :address updated-accounts))))} + + (seq new-account-addresses) + (assoc :fx + (mapv (fn [address] [:dispatch [:wallet/fetch-assets-for-address address]]) + new-account-addresses))))) + +(rf/reg-event-fx :wallet/reconcile-watch-only-accounts reconcile-watch-only-accounts) + +(defn reconcile-keypairs + [{:keys [db]} [keypairs]] + (let [existing-keypairs-by-id (get-in db [:wallet :keypairs]) + existing-accounts-by-address (get-in db [:wallet :accounts]) + {:keys [removed-keypair-ids + removed-account-addresses + updated-keypairs-by-id + updated-accounts-by-address]} (data-store/reconcile-keypairs keypairs) + updated-keypair-ids (set (keys updated-keypairs-by-id)) + updated-account-addresses (set (keys updated-accounts-by-address)) + existing-account-addresses (set (keys existing-accounts-by-address)) + new-account-addresses (clojure.set/difference updated-account-addresses + existing-account-addresses) + old-account-addresses (->> (vals existing-accounts-by-address) + (filter (fn [{:keys [address key-uid]}] + (and (contains? updated-keypair-ids key-uid) + (not (contains? + updated-accounts-by-address + address))))) + (map :address))] + (cond-> {:db (-> db + (assoc-in [:wallet :keypairs] + (-> (apply dissoc existing-keypairs-by-id removed-keypair-ids) + (merge updated-keypairs-by-id))) + (assoc-in [:wallet :accounts] + (merge-with merge + (apply dissoc + existing-accounts-by-address + (into removed-account-addresses + old-account-addresses)) + updated-accounts-by-address)))} + + (seq new-account-addresses) + (assoc :fx + (mapv (fn [address] [:dispatch [:wallet/fetch-assets-for-address address]]) + new-account-addresses))))) + +(rf/reg-event-fx :wallet/reconcile-keypairs reconcile-keypairs) + +(rf/reg-event-fx + :wallet/blockchain-health-changed + (fn [{:keys [db]} [{:keys [message]}]] + (let [full-status (cske/transform-keys message transforms/->kebab-case-keyword)] + {:db (assoc-in db [:wallet :blockchain] full-status)}))) + +(rf/reg-event-fx + :wallet/sign-transactions-signal-received + (fn [{:keys [db]} [{send-details :sendDetails :as data}]] + (let [type (if (or (= (:fromToken send-details) + (:toToken send-details)) + (string/blank? (:toToken send-details))) + :send + :swap) + callback-fx (get-in db [:wallet :ui type :sign-transactions-callback-fx]) + error-response (:errorResponse send-details)] + {:fx [(when (and callback-fx (not error-response)) + callback-fx)] + :db (-> db + (assoc-in [:wallet :ui type :sign-transactions-callback-fx] nil) + (assoc-in [:wallet :ui type :error-response] error-response) + (assoc-in [:wallet :ui type :transaction-for-signing] data))}))) + +(rf/reg-event-fx + :wallet/transactions-sent-signal-received + (fn [{:keys [db]} + [{sent-transactions :sentTransactions + send-details :sendDetails}]] + (let [swap? (-> db + (get-in db-path/swap) + seq)] + {:fx [[:dispatch + (if-let [error-response (:errorResponse send-details)] + [(if swap? + :wallet.swap/transaction-failure + :wallet/transaction-failure) + error-response] + [(if swap? + :wallet.swap/transaction-success + :wallet/transaction-success) + sent-transactions])]]}))) + +(rf/reg-event-fx + :wallet/set-max-base-fee + (fn [{db :db} [value]] + {:db (assoc-in db [:wallet :ui :user-tx-settings :max-base-fee] value)})) + +(rf/reg-event-fx + :wallet/set-priority-fee + (fn [{db :db} [value]] + {:db (assoc-in db [:wallet :ui :user-tx-settings :priority-fee] value)})) + +(rf/reg-event-fx + :wallet/set-max-gas-amount + (fn [{db :db} [value]] + {:db (assoc-in db [:wallet :ui :user-tx-settings :gas-amount] value)})) + +(rf/reg-event-fx + :wallet/set-gas-price + (fn [{db :db} [value]] + {:db (assoc-in db [:wallet :ui :user-tx-settings :gas-price] value)})) + +(rf/reg-event-fx + :wallet/set-nonce + (fn [{db :db} [value]] + {:db (assoc-in db [:wallet :ui :user-tx-settings :nonce] value)})) + +(defn set-fee-mode-effect + [path-tx-identity gas-rate] + (let [params [path-tx-identity gas-rate]] + [:json-rpc/call + [{:method "wallet_setFeeMode" + :params params + :on-error (fn [error] + (log/error "failed to set quick transaction settings" + {:event :wallet/quick-fee-mode-confirmed + :error (:message error) + :params params}))}]])) + +(rf/reg-event-fx :wallet/quick-fee-mode-confirmed + (fn [{db :db} [fee-mode]] + (let [gas-rate (transaction-settings/tx-fee-mode->gas-rate fee-mode) + tx-type (get-in db [:wallet :ui :send :tx-type]) + route (if tx-type + (first (get-in db [:wallet :ui :send :route])) + (get-in db [:wallet :ui :swap :swap-proposal])) + ;; bridge consist from 2 transactions - approval and send, so we need to apply + ;; setting to both of them by making 2 calls + set-fee-effects (if (= tx-type :tx/bridge) + [(set-fee-mode-effect (send-utils/path-identity route true) gas-rate) + (set-fee-mode-effect (send-utils/path-identity route false) gas-rate)] + [(set-fee-mode-effect (send-utils/path-identity route) gas-rate)])] + {:db (assoc-in db [:wallet :ui :user-fee-mode] fee-mode) + :fx (conj set-fee-effects + [:dispatch [:wallet/mark-user-tx-settings-for-deletion]])}))) + +(defn set-custom-tx-effect + [path-tx-identity custom-tx-params] + (let [params [path-tx-identity custom-tx-params]] + [:json-rpc/call + [{:method "wallet_setCustomTxDetails" + :params params + :on-error (fn [error] + (log/error "failed to set custom tx settings" + {:event :wallet/custom-transaction-settings-confirmed + :error (:message error) + :params params}))}]])) + +(rf/reg-event-fx :wallet/custom-transaction-settings-confirmed + (fn [{db :db}] + (let [tx-type (get-in db [:wallet :ui :send :tx-type]) + route (if tx-type + (first (get-in db [:wallet :ui :send :route])) + (get-in db [:wallet :ui :swap :swap-proposal])) + user-tx-settings (get-in db [:wallet :ui :user-tx-settings]) + custom-tx-params (send-utils/path-tx-custom-params user-tx-settings route) + ;; bridge consist from 2 transactions - approval and send, so we need to apply + ;; setting to both of them by making 2 calls + custom-tx-details-effects (if (= tx-type :tx/bridge) + [(set-custom-tx-effect (send-utils/path-identity route true) + custom-tx-params) + (set-custom-tx-effect (send-utils/path-identity route false) + custom-tx-params)] + [(set-custom-tx-effect (send-utils/path-identity route) + custom-tx-params)])] + {:db (assoc-in db [:wallet :ui :user-fee-mode] :tx-fee-mode/custom) + :fx (conj custom-tx-details-effects + [:dispatch [:wallet/mark-user-tx-settings-for-deletion]])}))) + +;; There is a delay between the moment when user selected +;; custom settings and the moment when new route arrived +;; with those settings applied. During this delay +;; we should keep user settings for ui. After new route +;; arrived we should clean the settings. +(rf/reg-event-fx :wallet/mark-user-tx-settings-for-deletion + (fn [{db :db}] + {:db (assoc-in db [:wallet :ui :user-tx-settings :delete-on-routes-update?] true)})) + diff --git a/src/status_im/contexts/wallet/events_test.cljs b/src/status_im/contexts/wallet/events_test.cljs new file mode 100644 index 00000000000..7c6bacc9af0 --- /dev/null +++ b/src/status_im/contexts/wallet/events_test.cljs @@ -0,0 +1,270 @@ +(ns status-im.contexts.wallet.events-test + (:require + [cljs.test :refer-macros [is testing]] + [matcher-combinators.matchers :as matchers] + matcher-combinators.test + [re-frame.db :as rf-db] + status-im.contexts.wallet.events + [test-helpers.unit :as h])) + +(def mainnet-name :mainnet) +(def arbitrum-name :arbitrum) +(def optimism-name :optimism) +(def default-networks #{mainnet-name arbitrum-name optimism-name}) + +(def address "0x2ee6138eb9344a8b76eca3cf7554a06c82a1e2d8") + +(def raw-account + {:path "m/44'/60'/0'/0/0" + :emoji "🛃" + :key-uid "0xf9b4dc40911638052ef9cbed6e8ac689198d8f11d2235c5d62e2457c1503dc4f" + :address address + :wallet true + :name "Ethereum account" + :createdAt 1716548742000 + :type "generated" + :chat false + :prodPreferredChainIds "1:42161" + :hidden false + :position 0 + :clock 1712315009484 + :testPreferredChainIds "11155111:421614" + :colorId "purple" + :operable "fully" + :mixedcase-address "0x2Ee6138eb9344a8b76Eca3cf7554A06C82A1e2D8" + :public-key + "0x04ee7c47e4b68cc05dcd3377cbd5cde6be3c89fcf20a981e55e0285ed63a50f51f8b423465eee134c51bb0255e6041e9e5b006054b0fa72a7c76942a5a1a3f4e7e" + :removed false}) + +(def account + {:path "m/44'/60'/0'/0/0" + :emoji "🛃" + :key-uid "0xf9b4dc40911638052ef9cbed6e8ac689198d8f11d2235c5d62e2457c1503dc4f" + :address address + :color :purple + :wallet true + :default-account? true + :operable? true + :name "Ethereum account" + :type :generated + :chat false + :test-preferred-chain-ids #{11155111 421614} + :watch-only? false + :hidden false + :prod-preferred-chain-ids #{1 42161} + :position 0 + :clock 1712315009484 + :created-at 1716548742000 + :operable :fully + :mixedcase-address "0x2Ee6138eb9344a8b76Eca3cf7554A06C82A1e2D8" + :public-key + "0x04ee7c47e4b68cc05dcd3377cbd5cde6be3c89fcf20a981e55e0285ed63a50f51f8b423465eee134c51bb0255e6041e9e5b006054b0fa72a7c76942a5a1a3f4e7e" + :removed false}) + +(h/deftest-event :wallet/scan-address-success + [event-id dispatch] + (is (match? {:wallet {:ui {:scanned-address address}}} + (:db (dispatch [event-id address]))))) + +(h/deftest-event :wallet/clean-scanned-address + [event-id dispatch] + (reset! rf-db/app-db {:wallet {:ui {:scanned-address address}}}) + (is (match? {:wallet {:ui {}}} + (:db (dispatch [event-id]))))) + +(h/deftest-event :wallet/get-wallet-token-for-all-accounts + [event-id dispatch] + (let [address-1 "0x1" + address-2 "0x2"] + (reset! rf-db/app-db {:wallet {:accounts {address-1 {:address address-1} + address-2 {:address address-2}}}}) + (is (match? [[:dispatch [:wallet/get-wallet-token-for-accounts [address-1 address-2]]]] + (:fx (dispatch [event-id])))))) + +(h/deftest-event :wallet/get-wallet-token-for-accounts + [event-id dispatch] + (let [expected-effects {:db {:wallet {:ui {:tokens-loading {address true}}}} + :fx [[:json-rpc/call + [{:method "wallet_fetchOrGetCachedWalletBalances" + :params [[address] true] + :on-success [:wallet/store-wallet-token [address]] + :on-error [:wallet/get-wallet-token-for-accounts-failed + [address]]}]]]}] + (is (match? expected-effects (dispatch [event-id [address]]))))) + +(h/deftest-event :wallet/reconcile-keypairs + [event-id dispatch] + (let [keypair-key-uid (:key-uid raw-account)] + (testing "event adds new key pairs" + (reset! rf-db/app-db {:wallet {:accounts {} + :keypairs {}}}) + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {(:address account) account} + :keypairs {keypair-key-uid {:key-uid keypair-key-uid + :type :seed + :lowest-operability :fully + :accounts [account]}}}} + :fx [[:dispatch [:wallet/fetch-assets-for-address address]]]}) + (dispatch [event-id + [{:key-uid keypair-key-uid + :type "seed" + :accounts [raw-account]}]])))) + (testing "event removes key pairs and accounts that are marked as removed" + (reset! rf-db/app-db {:wallet {:accounts {(:address account) account} + :keypairs {keypair-key-uid + {:key-uid keypair-key-uid + :type :seed + :lowest-operability :fully + :accounts [account]}}}}) + + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {} + :keypairs {}}}}) + (dispatch [event-id + [{:key-uid keypair-key-uid + :type "seed" + :removed true + :accounts [raw-account]}]])))) + (testing "event removes accounts not present with key pair" + (reset! rf-db/app-db {:wallet {:accounts {(:address account) account + "1x001" (assoc account + :address "1x001" + :key-uid "0x001")} + :keypairs {keypair-key-uid + {:key-uid keypair-key-uid + :type :seed + :lowest-operability :fully + :accounts [account]} + "0x001" + {:key-uid "0x001" + :type :seed + :lowest-operability :fully + :accounts [(assoc account + :address "1x001" + :key-uid "0x001")]}}}}) + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {(:address account) account} + :keypairs {keypair-key-uid + {:key-uid keypair-key-uid + :type :seed + :lowest-operability :fully + :accounts [account]} + "0x001" + {:key-uid "0x001" + :type :seed + :lowest-operability :fully + :accounts []}}}}}) + (dispatch [event-id + [{:key-uid "0x001" + :type "seed" + :accounts []}]])))) + (testing "event updates existing key pairs" + (reset! rf-db/app-db {:wallet + {:accounts {(:address account) + (assoc account :operable :no)} + :keypairs {keypair-key-uid + {:key-uid keypair-key-uid + :type :seed + :lowest-operability :no + :accounts [(assoc account :operable :no)]}}}}) + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {(:address account) account} + :keypairs {keypair-key-uid + {:key-uid keypair-key-uid + :type :seed + :lowest-operability :fully + :accounts [account]}}}}}) + (dispatch [event-id + [{:key-uid keypair-key-uid + :type "seed" + :accounts [raw-account]}]])))) + (testing "event ignores chat accounts for key pairs" + (reset! rf-db/app-db {:wallet {:accounts {(:address account) account} + :keypairs {keypair-key-uid + {:key-uid keypair-key-uid + :type :profile + :lowest-operability :fully + :accounts [account + (assoc account + :address "1x001" + :chat true)]}}}}) + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {(:address account) account} + :keypairs {keypair-key-uid + {:key-uid keypair-key-uid + :type :profile + :lowest-operability :fully + :accounts [account + (assoc account + :address "1x001" + :chat true)]}}}}}) + (dispatch [event-id + [{:key-uid keypair-key-uid + :type "profile" + :accounts [raw-account + (assoc raw-account + :address "1x001" + :chat true)]}]])))))) + +(h/deftest-event :wallet/reconcile-watch-only-accounts + [event-id dispatch] + (testing "event adds new watch-only accounts" + (reset! rf-db/app-db {:wallet {:accounts {}}}) + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {(:address account) account}}} + :fx [[:dispatch [:wallet/fetch-assets-for-address address]]]}) + (dispatch [event-id [raw-account]])))) + (testing "event removes watch-only accounts that are marked as removed" + (reset! rf-db/app-db {:wallet {:accounts {(:address account) account}}}) + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {}}}}) + (dispatch [event-id [(assoc raw-account :removed true)]])))) + (testing "event updates existing watch-only accounts" + (reset! rf-db/app-db {:wallet + {:accounts {address account}}}) + (is + (match? + (matchers/match-with + [set? matchers/set-equals + vector? matchers/equals + map? matchers/equals] + {:db {:wallet {:accounts {address (assoc account :name "Test")}}}}) + (dispatch [event-id + [(assoc raw-account + :address address + :name "Test")]]))))) diff --git a/src/status_im/contexts/wallet/home/style.cljs b/src/status_im/contexts/wallet/home/style.cljs new file mode 100644 index 00000000000..185c644494d --- /dev/null +++ b/src/status_im/contexts/wallet/home/style.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.wallet.home.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area] + [status-im.contexts.shell.constants :as constants])) + +(def tabs + {:padding-horizontal 20 + :padding-top 8 + :padding-bottom 12}) + +(def list-container + {:padding-bottom constants/floating-shell-button-height}) + +(def accounts-list + {:padding-top 8 + :padding-bottom 16 + :flex-grow 0}) + +(def accounts-list-container + {:padding-horizontal 20}) + +(def separator + {:width 12}) + +(defn home-container + [] + {:margin-top safe-area/top + :flex 1}) + +(defn header-container + [theme] + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(def cta-buttons + {:padding-horizontal 20 + :padding-bottom 13 + :flex-direction :row}) diff --git a/src/status_im/contexts/wallet/home/tabs/assets/style.cljs b/src/status_im/contexts/wallet/home/tabs/assets/style.cljs new file mode 100644 index 00000000000..7b6736af131 --- /dev/null +++ b/src/status_im/contexts/wallet/home/tabs/assets/style.cljs @@ -0,0 +1,14 @@ +(ns status-im.contexts.wallet.home.tabs.assets.style + (:require [status-im.contexts.shell.constants :as constants])) + +(def list-container + {:padding-horizontal 8 + :padding-bottom constants/floating-shell-button-height}) + +(def buy-and-receive-cta-container + {:flex-direction :row + :padding-horizontal 20 + :padding-vertical 8}) + +(def cta-card + {:flex 1}) diff --git a/src/status_im/contexts/wallet/home/tabs/assets/view.cljs b/src/status_im/contexts/wallet/home/tabs/assets/view.cljs new file mode 100644 index 00000000000..f1880210d15 --- /dev/null +++ b/src/status_im/contexts/wallet/home/tabs/assets/view.cljs @@ -0,0 +1,49 @@ +(ns status-im.contexts.wallet.home.tabs.assets.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.wallet.common.token-value.view :as token-value] + [status-im.contexts.wallet.home.tabs.assets.style :as style] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [tokens-loading? (rf/sub [:wallet/home-tokens-loading?]) + {:keys [tokens]} (rf/sub [:wallet/aggregated-token-values-and-balance]) + zero-balance? (rf/sub [:wallet/zero-balance-in-all-non-watched-accounts?]) + buy-assets (rn/use-callback + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content buy-token/view}]))) + receive-assets (rn/use-callback + (fn [] + (rf/dispatch [:open-modal :screen/share-shell {:initial-tab :wallet}])))] + [:<> + (when (and (some? tokens-loading?) (not tokens-loading?) zero-balance?) + [rn/view + {:style style/buy-and-receive-cta-container} + [quo/wallet-card + {:image (resources/get-image :buy) + :title (i18n/label :t/ways-to-buy) + :subtitle (i18n/label :t/via-card-or-bank) + :container-style (assoc style/cta-card :margin-right 12) + :on-press buy-assets}] + [quo/wallet-card + {:image (resources/get-image :receive) + :title (i18n/label :t/receive) + :subtitle (i18n/label :t/deposit-to-your-wallet) + :container-style style/cta-card + :on-press receive-assets}]]) + (if tokens-loading? + [quo/skeleton-list + {:content :assets + :parent-height 560 + :animated? false}] + [rn/flat-list + {:render-fn token-value/view + :data tokens + :render-data {:entry-point :screen/wallet-stack} + :content-container-style style/list-container}])])) diff --git a/src/status_im/contexts/wallet/home/tabs/style.cljs b/src/status_im/contexts/wallet/home/tabs/style.cljs new file mode 100644 index 00000000000..676f6d1fa2f --- /dev/null +++ b/src/status_im/contexts/wallet/home/tabs/style.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.wallet.home.tabs.style) + +(def container + {:padding-horizontal 8 + :flex 1}) diff --git a/src/status_im/contexts/wallet/home/tabs/view.cljs b/src/status_im/contexts/wallet/home/tabs/view.cljs new file mode 100644 index 00000000000..641283ca138 --- /dev/null +++ b/src/status_im/contexts/wallet/home/tabs/view.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.wallet.home.tabs.view + (:require + [react-native.core :as rn] + [status-im.contexts.wallet.collectible.options.view :as options-drawer] + [status-im.contexts.wallet.common.activity-tab.view :as activity] + [status-im.contexts.wallet.common.collectibles-tab.view :as collectibles] + [status-im.contexts.wallet.home.tabs.assets.view :as assets] + [status-im.contexts.wallet.home.tabs.style :as style] + [utils.re-frame :as rf])) + +(defn- on-collectible-long-press + [{:keys [preview-url :collectible-data id]}] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] + [options-drawer/view + {:id id + :name (:name collectible-data) + :image (:uri preview-url)}])}])) + +(def on-collectible-press #(rf/dispatch [:wallet/navigate-to-collectible-details %])) + +(def request-collectibles #(rf/dispatch [:wallet/request-collectibles-for-all-accounts {}])) + +(defn- collectibles-tab + [] + (let [updating? (rf/sub [:wallet/home-tab-collectibles-updating?]) + collectible-list (rf/sub [:wallet/owned-collectibles-list-in-filtered-networks])] + [collectibles/view + {:loading? updating? + :collectibles collectible-list + :on-collectible-long-press on-collectible-long-press + :on-end-reached request-collectibles + :on-collectible-press on-collectible-press}])) + +(defn view + [{:keys [selected-tab]}] + [rn/view {:style style/container} + (case selected-tab + :assets [assets/view] + :collectibles [collectibles-tab] + [activity/view {:activities []}])]) diff --git a/src/status_im/contexts/wallet/home/view.cljs b/src/status_im/contexts/wallet/home/view.cljs new file mode 100644 index 00000000000..1e871acaf08 --- /dev/null +++ b/src/status_im/contexts/wallet/home/view.cljs @@ -0,0 +1,206 @@ +(ns status-im.contexts.wallet.home.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.home.top-nav.view :as common.top-nav] + [status-im.common.refreshable-flat-list.view :as refreshable-flat-list] + [status-im.constants :as constants] + [status-im.contexts.wallet.home.style :as style] + [status-im.contexts.wallet.home.tabs.view :as tabs] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [status-im.contexts.wallet.sheets.network-filter.view :as network-filter] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn new-account + [] + (let [watched-accounts (rf/sub [:wallet/watch-only-accounts]) + reached-max-watched-account? (>= (count watched-accounts) + constants/max-allowed-watched-accounts) + on-add-address-press (rn/use-callback + (fn [] + (if reached-max-watched-account? + (rf/dispatch [:toasts/upsert + {:type :negative + :theme :dark + :text + (i18n/label + :t/saved-addresses-limit-reached-toast)}]) + (rf/dispatch [:navigate-to + :screen/wallet.add-address-to-watch]))) + [reached-max-watched-account?])] + [quo/action-drawer + [[{:icon :i/add + :accessibility-label :start-a-new-chat + :label (i18n/label :t/add-account) + :sub-label (i18n/label :t/add-account-description) + :on-press #(rf/dispatch [:navigate-to :screen/wallet.create-account])} + {:icon :i/reveal + :accessibility-label :add-a-contact + :label (i18n/label :t/add-address-to-watch) + :sub-label (i18n/label :t/add-address-to-watch-description) + :on-press on-add-address-press + :add-divider? true}]]])) + +(defn- new-account-card-data + [] + {:customization-color (rf/sub [:profile/customization-color]) + :on-press #(rf/dispatch [:show-bottom-sheet {:content new-account}]) + :type :add-account}) + +(def tabs-data + [{:id :assets :label (i18n/label :t/assets) :accessibility-label :assets-tab} + {:id :collectibles :label (i18n/label :t/collectibles) :accessibility-label :collectibles-tab} + (when (ff/enabled? ::ff/wallet.home-activity) + {:id :activity :label (i18n/label :t/activity) :accessibility-label :activity-tab})]) + +(defn- change-tab [id] (rf/dispatch [:wallet/select-home-tab id])) + +(defn- render-cards + [cards ref] + [rn/flat-list + {:ref #(reset! ref %) + :style style/accounts-list + :content-container-style style/accounts-list-container + :data cards + :horizontal true + :separator [rn/view {:style style/separator}] + :render-fn (fn [item] [quo/account-card item]) + :shows-horizontal-scroll-indicator false}]) + +(defn- render-tabs + [data on-change default-active] + [quo/tabs + {:style style/tabs + :size 32 + :default-active default-active + :data data + :on-change on-change}]) + +(defn- call-to-actions + [] + (let [operable-accounts (rf/sub [:wallet/operable-accounts]) + testnet-mode? (rf/sub [:profile/test-networks-enabled?]) + multiple-accounts? (> (count operable-accounts) 1) + first-account-address (:address (first operable-accounts)) + on-send-press (rn/use-callback + (fn [] + (rf/dispatch [:wallet/clean-send-data]) + (when-not multiple-accounts? + (rf/dispatch [:wallet/switch-current-viewing-account + first-account-address])) + (if multiple-accounts? + (rf/dispatch [:open-modal :screen/wallet.select-from]) + (rf/dispatch [:wallet/wizard-navigate-forward + {:start-flow? true + :flow-id :wallet-send-flow}]))) + [multiple-accounts? first-account-address]) + on-receive-press (rn/use-callback #(rf/dispatch [:open-modal :screen/share-shell + {:initial-tab :wallet + :status :receive + :hide-tab-selector? true}])) + on-buy-press (rn/use-callback #(rf/dispatch [:show-bottom-sheet + {:content buy-token/view}])) + on-bridge-press (rn/use-callback + (fn [] + ;; For a single account, it starts the bridge flow immediately. For + ;; multiple accounts, it sets the transaction type and starts the + ;; bridge flow after account selection. + (rf/dispatch [:wallet/clean-send-data]) + (when-not multiple-accounts? + (rf/dispatch [:wallet/switch-current-viewing-account + first-account-address]) + (rf/dispatch [:wallet/start-bridge])) + (when multiple-accounts? + (rf/dispatch [:wallet/set-send-tx-type :tx/bridge]) + (rf/dispatch [:open-modal :screen/wallet.select-from]))) + [multiple-accounts? first-account-address]) + on-swap-press #(rf/dispatch [:app.wallet/start-swap operable-accounts])] + [quo/wallet-ctas + {:container-style style/cta-buttons + :send-action on-send-press + :receive-action on-receive-press + :buy-action on-buy-press + :bridge-action on-bridge-press + :swap-action on-swap-press + :bridge-disabled? testnet-mode? + :swap-disabled? testnet-mode?}])) + +(defn view + [] + (let [selected-tab (rf/sub [:wallet/home-tab]) + privacy-mode-enabled? (rf/sub [:privacy-mode/privacy-mode-enabled?]) + account-list-ref (rn/use-ref-atom nil) + tokens-loading? (rf/sub [:wallet/home-tokens-loading?]) + networks (rf/sub [:wallet/filtered-networks]) + networks-filtered? (rf/sub [:wallet/network-filter?]) + account-cards-data (rf/sub [:wallet/account-cards-data]) + cards (conj account-cards-data (new-account-card-data)) + [init-loaded? set-init-loaded] (rn/use-state false) + {:keys [formatted-balance]} (rf/sub [:wallet/aggregated-token-values-and-balance]) + theme (quo.context/use-theme) + show-new-chain-indicator? (rf/sub [:wallet/show-new-chain-indicator?]) + show-privacy-mode-sheet (rn/use-callback + (fn [] + (rf/dispatch [:privacy-mode/show-bottom-sheet {:theme theme}])) + [theme]) + on-press-network-selector (rn/use-callback + (fn [] + (if networks-filtered? + (rf/dispatch [:wallet/reset-network-balances-filter]) + (do (rf/dispatch [:wallet/mark-new-networks-as-seen]) + (rf/dispatch [:show-bottom-sheet + {:content network-filter/view}])))) + [networks-filtered?])] + (rn/use-effect (fn [] + (when (and @account-list-ref (pos? (count cards))) + (.scrollToOffset ^js @account-list-ref + #js + {:animated true + :offset 0}))) + [(count cards)]) + (rn/use-effect + #(when (and (boolean? tokens-loading?) (not tokens-loading?) (not init-loaded?)) + (set-init-loaded true)) + [tokens-loading?]) + [rn/view {:style (style/home-container)} + [common.top-nav/view] + (when privacy-mode-enabled? + [quo/information-box + {:type :informative + :customization-color :purple + :icon :i/info + :on-button-press show-privacy-mode-sheet + :button-label (i18n/label :t/disable-privacy-mode) + :style {:margin-vertical 8 :margin-horizontal 20}} + (i18n/label :t/features-and-balances-disabled)]) + [refreshable-flat-list/view + {:refresh-control [rn/refresh-control + {:refreshing (and tokens-loading? init-loaded?) + :colors [colors/neutral-40] + :tint-color colors/neutral-40 + :on-refresh #(rf/dispatch [:wallet/get-accounts])}] + :header [rn/view {:style (style/header-container theme)} + [quo/wallet-overview + {:state (if tokens-loading? :loading :default) + :time-frame :none + :metrics :none + :balance formatted-balance + :networks networks + :networks-filtered? networks-filtered? + :dropdown-on-press on-press-network-selector + :show-new-chain-indicator? (when (not-empty networks) + show-new-chain-indicator?)}] + (when (ff/enabled? ::ff/wallet.graph) + [quo/wallet-graph {:time-frame :empty}]) + [render-cards cards account-list-ref] + [call-to-actions] + [render-tabs tabs-data change-tab selected-tab]] + :content-container-style style/list-container + :sticky-header-indices [0] + :data [] + :render-fn #() + :footer [tabs/view {:selected-tab selected-tab}]}]])) diff --git a/src/status_im/contexts/wallet/item_types.cljs b/src/status_im/contexts/wallet/item_types.cljs new file mode 100644 index 00000000000..a8424ec59d1 --- /dev/null +++ b/src/status_im/contexts/wallet/item_types.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.wallet.item-types) + +(def ^:const saved-address 2) +(def ^:const address 3) +(def ^:const saved-contact-address 4) diff --git a/src/status_im/contexts/wallet/networks/config.cljs b/src/status_im/contexts/wallet/networks/config.cljs new file mode 100644 index 00000000000..b8532933adc --- /dev/null +++ b/src/status_im/contexts/wallet/networks/config.cljs @@ -0,0 +1,117 @@ +(ns status-im.contexts.wallet.networks.config + (:require + [malli.core] + [malli.error] + [quo.foundations.resources :as resources] + [status-im.contexts.wallet.networks.validation :as validation])) + +;; NOTE: this value comes from status-go, as it supports up to 5 networks simultaneously. +(def ^:const max-active-networks 5) + +;; NOTE: Add chain-ids below + +(def ^:const ethereum-chain-id 1) +(def ^:const sepolia-chain-id 11155111) + +;; NOTE: Used for placing the ethereum chains (including testnet) in the top of the list during sorting +(def ^:const ethereum-chain-ids #{ethereum-chain-id sepolia-chain-id}) + +(def ^:const arbitrum-chain-id 42161) +(def ^:const arbitrum-sepolia-chain-id 421614) + +(def ^:const optimism-chain-id 10) +(def ^:const optimism-sepolia-chain-id 11155420) + +(def ^:const base-chain-id 8453) +(def ^:const base-sepolia-chain-id 84532) + +(def ^:const status-sepolia-chain-id 1660990954) + +(def ^:const bsc-chain-id 56) +(def ^:const bsc-testnet-chain-id 97) + +;; NOTE: Add a chain to `new-networks` to: +;; 1. highlight it as "new" in the UI +;; 2. add a "new feature" dot to places where we show networks + +(def ^:const new-networks + #{status-sepolia-chain-id + bsc-chain-id + bsc-testnet-chain-id}) + +(def ^:const chain-id-for-new-network-banner bsc-chain-id) + +;; NOTE: if the network should be supported in Bridge (Hop), add +;; the chain id to the following set. Otherwise, remove if no longer +;; supported. + +(def ^:const bridge-supported-networks + #{ethereum-chain-id + arbitrum-chain-id + optimism-chain-id + base-chain-id}) + +;; NOTE: add client-side chain details below for `mainnet` and `testnet` +;; respectively. + +(def ^:const mainnets + {ethereum-chain-id + {:network-name :mainnet + :source (resources/get-network :mainnet) + :abbreviated-name "Eth." + :block-explorer-name "Etherscan"} + + arbitrum-chain-id + {:network-name :arbitrum + :source (resources/get-network :arbitrum) + :abbreviated-name "Arb1." + :block-explorer-name "Arbiscan"} + + optimism-chain-id + {:network-name :optimism + :source (resources/get-network :optimism) + :abbreviated-name "Oeth." + :block-explorer-name "Optimistic"} + + base-chain-id + {:network-name :base + :source (resources/get-network :base) + :abbreviated-name "Base" + :block-explorer-name "Basescan"} + + bsc-chain-id + {:network-name :bsc + :source (resources/get-network :bsc) + :abbreviated-name "BSC" + :block-explorer-name "Bscscan"}}) + +(def ^:const testnets + {sepolia-chain-id + (get mainnets ethereum-chain-id) + + arbitrum-sepolia-chain-id + (get mainnets arbitrum-chain-id) + + optimism-sepolia-chain-id + (get mainnets optimism-chain-id) + + base-sepolia-chain-id + (get mainnets base-chain-id) + + bsc-testnet-chain-id + (get mainnets bsc-chain-id) + + status-sepolia-chain-id + {:network-name :status + :source (resources/get-network :status) + :abbreviated-name "Stat." + :block-explorer-name "Status Explorer"}}) + +(def ^:const networks + (merge mainnets testnets)) + +;; NOTE: runs schema validation over all the networks only in debug +;; mode to make sure the networks are defined correctly + +(when ^boolean js/goog.DEBUG + (map validation/validate-network networks)) diff --git a/src/status_im/contexts/wallet/networks/core.cljs b/src/status_im/contexts/wallet/networks/core.cljs new file mode 100644 index 00000000000..dd9b97aeca9 --- /dev/null +++ b/src/status_im/contexts/wallet/networks/core.cljs @@ -0,0 +1,97 @@ +(ns status-im.contexts.wallet.networks.core + (:require [status-im.contexts.wallet.networks.config :as networks.config] + [status-im.contexts.wallet.networks.filter :as networks.filter] + [utils.url :as url])) + +(defn get-chain-id + [networks network-name] + (some->> networks + (some #(when (= (keyword network-name) (:network-name %)) %)) + :chain-id)) + +(defn new-network? + "Checks if the network should be highlighted as `new` in the UI, based on the local + networks configuration" + [chain-id] + (contains? networks.config/new-networks chain-id)) + +(defn ethereum-network? + [network] + (contains? networks.config/ethereum-chain-ids (:chain-id network))) + +(defn eth-mainnet? + "Checks if the passed network is the Ethereum Mainnet chain" + [network] + (-> network + :chain-id + (= 1))) + +(defn get-bridge-supported-networks + "Returns the bridge (Hop) supported networks" + [] + networks.config/bridge-supported-networks) + +(defn bridge-supported-network? + "Checks if the network is supported in bridge (Hop)" + [{:keys [chain-id]}] + (contains? networks.config/bridge-supported-networks chain-id)) + +(defn filter-bridge-supported-networks + "Returns only the networks that are supported in bridge (Hop)" + [networks] + (filter #(bridge-supported-network? %) networks)) + +(defn get-block-explorer-tx-url + "Returns the block-explorer transaction url for a chain" + [network tx-hash] + (-> network + :block-explorer-url + (url/add-path :tx tx-hash))) + +(defn get-block-explorer-address-url + "Returns the block-explorer address url for a chain" + [network address] + (-> network + :block-explorer-url + (url/add-path :address address))) + +(defn get-networks-for-layer + "Returns networks for the layer (`1` or `2`)" + [networks layer] + (keep (fn [network] + (when (= layer (:layer network)) + network)) + networks)) + +(defn get-chain-ids + [networks] + (->> networks + (map :chain-id) + set)) + +(defn get-active-networks + "Returns only active networks" + [networks] + (keep #(when (:active? %) %) networks)) + +(defn get-active-chain-ids + "Returns only active network chain-ids" + [networks] + (->> networks + get-active-networks + get-chain-ids)) + +(defn get-max-active-networks + [] + networks.config/max-active-networks) + +(defn get-filtered-networks + [networks network-filter] + (-> networks + (networks.filter/by-id network-filter))) + +(defn get-filtered-chain-ids + [networks network-filter] + (->> network-filter + (get-filtered-networks networks) + get-chain-ids)) diff --git a/src/status_im/contexts/wallet/networks/core_test.cljs b/src/status_im/contexts/wallet/networks/core_test.cljs new file mode 100644 index 00000000000..9435251a7ef --- /dev/null +++ b/src/status_im/contexts/wallet/networks/core_test.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.wallet.networks.core-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.wallet.networks.core :as sut] + [tests.wallet-test-data :as test-data])) + +(deftest eth-mainnet?-test + (testing "returns true if network is mainnet ethereum" + (is (match? true (sut/eth-mainnet? test-data/mainnet)))) + (testing "returns false if network is not mainnet ethereum" + (is (match? false (sut/eth-mainnet? test-data/optimism))) + (is (match? false (sut/eth-mainnet? test-data/sepolia))) + (is (match? false (sut/eth-mainnet? test-data/optimism-sepolia))))) + +(deftest get-chain-id-test + (testing "returns the chain-id correctly" + (is (match? 10 (sut/get-chain-id [test-data/mainnet test-data/optimism] :optimism)))) + (testing "returns nil when the network is not present" + (is (match? nil (sut/get-chain-id [test-data/mainnet] :optimism))))) + +(deftest get-block-explorer-address-url-test + (testing "returns the block-explorer address url" + (is (match? "https://opt.block-explorer.com/address/0x123" + (sut/get-block-explorer-address-url test-data/optimism "0x123"))))) + +(deftest get-block-explorer-tx-url-test + (testing "returns the block-explorer transaction url" + (is (match? "https://eth.block-explorer.com/tx/0x123" + (sut/get-block-explorer-tx-url test-data/mainnet "0x123"))))) diff --git a/src/status_im/contexts/wallet/networks/db.cljs b/src/status_im/contexts/wallet/networks/db.cljs new file mode 100644 index 00000000000..82b27df0826 --- /dev/null +++ b/src/status_im/contexts/wallet/networks/db.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.wallet.networks.db + (:require [clojure.set :as set] + [status-im.contexts.profile.db :as profile.db] + [status-im.contexts.wallet.networks.config :as networks.config] + [status-im.contexts.wallet.networks.core :as networks])) + +(defn get-testnet-mode-key + "Returns either `:test` or `:prod` based on the testnet setting from db" + [db] + (if (profile.db/testnet? db) :test :prod)) + +(defn get-network-details + "returns the network details based on the `chain-id` from the db" + [db chain-id] + (get-in db [:wallet :networks/by-id chain-id])) + +(defn get-chain-ids + "Returns all chain-ids from db, based on the testnet setting" + [db] + (set (get-in db [:wallet :networks/chain-ids-by-mode (get-testnet-mode-key db)]))) + +(defn get-networks + "Returns all networks from db, based on the testnet setting" + [db] + (->> (get-in db [:wallet :networks/chain-ids-by-mode (get-testnet-mode-key db)]) + (map (partial get-network-details db)))) + +(defn get-active-chain-ids + "Returns all active chain-ids from db" + [db] + (-> db + get-networks + networks/get-active-chain-ids)) + +(defn get-active-networks + "Returns all active networks from db" + [db] + (->> db + get-networks + networks/get-active-networks)) + +(defn get-chain-id + "Returns the `chain-id` based on the `network-name`" + [db network-name] + (-> db + get-networks + (networks/get-chain-id network-name))) + +(defn get-network-name + "returns the network name based on the `chain-id` from the db" + [db chain-id] + (-> db (get-network-details chain-id) :full-name)) + +(defn get-block-explorer-address-url + "Returns the block-explorer address url for a chain" + [db chain-id address] + (-> db + (get-network-details chain-id) + (networks/get-block-explorer-address-url address))) + +(defn get-block-explorer-tx-url + "Returns the block-explorer transaction url for a chain" + [db chain-id tx-hash] + (-> db + (get-network-details chain-id) + (networks/get-block-explorer-tx-url tx-hash))) + +(defn max-active-networks-reached? + [db] + (-> db + get-active-chain-ids + count + (>= (networks/get-max-active-networks)))) + +(defn get-new-chain-ids + [db] + (-> db + get-chain-ids + (set/intersection networks.config/new-networks))) + +(defn network-active? + [db chain-id] + (-> db + get-active-chain-ids + (contains? chain-id))) diff --git a/src/status_im/contexts/wallet/networks/db_test.cljs b/src/status_im/contexts/wallet/networks/db_test.cljs new file mode 100644 index 00000000000..5cfc464e6af --- /dev/null +++ b/src/status_im/contexts/wallet/networks/db_test.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.wallet.networks.db-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.wallet.networks.db :as sut] + [tests.wallet-test-data :as test-data])) + +(deftest get-testnet-mode-key-test + (testing "returns the :test key correctly" + (let [db (-> {} + (test-data/add-networks-to-db) + (test-data/add-testnet-enabled-to-db))] + (is (match? :test (sut/get-testnet-mode-key db))))) + (testing "returns the :prod key correctly" + (let [db (test-data/add-networks-to-db {})] + (is (match? :prod (sut/get-testnet-mode-key db)))))) + +(deftest get-network-details-test + (testing "returns the network by chain-id corectly, regardless of testnet mode" + (let [db (test-data/add-networks-to-db {})] + (is (match? test-data/mainnet (sut/get-network-details db 1)))))) + +(deftest get-networks-test + (testing "returns testnet networks" + (let [db (-> {} + (test-data/add-networks-to-db) + (test-data/add-testnet-enabled-to-db))] + (is (match? [test-data/sepolia test-data/arbitrum-sepolia test-data/optimism-sepolia] + (sut/get-networks db))))) + (testing "returns mainnet networks" + (let [db (test-data/add-networks-to-db {})] + (is (match? [test-data/mainnet test-data/arbitrum test-data/optimism] (sut/get-networks db)))))) + +(deftest get-chain-ids-test + (testing "returns testnet chain-ids" + (let [db (-> {} + (test-data/add-networks-to-db) + (test-data/add-testnet-enabled-to-db))] + (is (match? #{test-data/sepolia-chain-id + test-data/arbitrum-sepolia-chain-id + test-data/optimism-sepolia-chain-id} + (sut/get-chain-ids db))))) + (testing "returns mainnet chain-ids" + (let [db (test-data/add-networks-to-db {})] + (is (match? #{test-data/mainnet-chain-id + test-data/arbitrum-chain-id + test-data/optimism-chain-id} + (sut/get-chain-ids db)))))) diff --git a/src/status_im/contexts/wallet/networks/effects.cljs b/src/status_im/contexts/wallet/networks/effects.cljs new file mode 100644 index 00000000000..fa7cdbc38ba --- /dev/null +++ b/src/status_im/contexts/wallet/networks/effects.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.wallet.networks.effects + (:require [promesa.core :as promesa] + [status-im.common.json-rpc.events :as rpc] + [status-im.contexts.wallet.networks.new-networks :as new-networks] + [utils.re-frame :as rf])) + +(rf/reg-fx + :effects.wallet/get-networks + (fn [{:keys [on-success on-error]}] + (-> (rpc/call-async "wallet_getFlatEthereumChains" false) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(rf/reg-fx + :effects.wallet/set-network-active + (fn [{:keys [chain-id active? on-success on-error]}] + (-> (rpc/call-async "wallet_setChainActive" + true + chain-id + active?) + (promesa/then on-success) + (promesa/catch on-error)))) + +(rf/reg-fx :effects.wallet/deactivate-and-activate-another-network + (fn [{:keys [deactivate-chain-id activate-chain-id on-success on-error]}] + (-> (promesa/do + (rpc/call-async "wallet_setChainActive" true deactivate-chain-id false) + (rpc/call-async "wallet_setChainActive" true activate-chain-id true)) + (promesa/then on-success) + (promesa/catch on-error)))) + +(rf/reg-fx + :effects.wallet/new-networks-seen? + (fn [{:keys [chain-ids on-success on-error]}] + (-> (new-networks/marked-as-seen? chain-ids) + (promesa/then on-success) + (promesa/catch on-error)))) + +(rf/reg-fx + :effects.wallet/store-new-networks-as-seen + (fn [{:keys [chain-ids on-success on-error]}] + (-> (new-networks/mark-as-seen chain-ids) + (promesa/then on-success) + (promesa/then on-error)))) diff --git a/src/status_im/contexts/wallet/networks/events.cljs b/src/status_im/contexts/wallet/networks/events.cljs new file mode 100644 index 00000000000..5b8ed0a2fbc --- /dev/null +++ b/src/status_im/contexts/wallet/networks/events.cljs @@ -0,0 +1,139 @@ +(ns status-im.contexts.wallet.networks.events + (:require [re-frame.core :as rf] + [status-im.contexts.profile.db :as profile.db] + [status-im.contexts.settings.wallet.network-settings.max-active-networks-sheet :as + max-active-networks-sheet] + [status-im.contexts.wallet.networks.db :as networks.db] + [status-im.contexts.wallet.networks.effects] + [status-im.contexts.wallet.networks.filter :as networks.filter] + [status-im.contexts.wallet.rpc-data-store.networks :as rpc-data-store.networks] + [utils.debounce :as debounce])) + +(rf/reg-event-fx + :wallet/get-networks + (fn [_] + {:fx [[:effects.wallet/get-networks + {:on-success [:wallet/get-networks-success] + :on-error [:wallet/log-rpc-error {:event :wallet/get-networks}]}]]})) + +(rf/reg-event-fx + :wallet/get-networks-success + (fn [{:keys [db]} [data]] + (let [networks (rpc-data-store.networks/rpc->networks data)] + {:db (-> db + (assoc-in [:wallet :networks/chain-ids-by-mode] + (rpc-data-store.networks/network-chain-ids networks)) + (assoc-in [:wallet :networks/by-id] + (rpc-data-store.networks/networks-by-id networks))) + :fx [[:dispatch [:wallet/on-networks-loaded]]]}))) + +(rf/reg-event-fx + :wallet/on-networks-loaded + (fn [_] + {:fx [[:dispatch [:wallet.tokens/get-token-list]] + [:dispatch [:wallet/check-new-networks-seen]]]})) + +(rf/reg-event-fx :wallet/toggle-network-active + (fn [{:keys [db]} [chain-id on-success]] + (let [{:keys [active? deactivatable?]} (networks.db/get-network-details db chain-id) + max-active-reached? (networks.db/max-active-networks-reached? db) + should-activate? (not active?)] + (if (and should-activate? max-active-reached?) + {:fx [[:dispatch + [:show-bottom-sheet + {:content max-active-networks-sheet/view + :theme :dark + :shell? true}]]]} + (when deactivatable? + {:fx [[:dispatch + [:wallet/update-network-active chain-id should-activate?]] + [:effects.wallet/set-network-active + {:chain-id chain-id + :active? should-activate? + :on-success #(do (debounce/debounce-and-dispatch + [:wallet/on-active-networks-change] + 500) + (when on-success (on-success))) + :on-error #(rf/dispatch + [:wallet/update-network-active chain-id active?])}]]}))))) + +(rf/reg-event-fx :wallet/deactivate-and-activate-another-network + (fn [_ [{:keys [activate-chain-id deactivate-chain-id on-success]}]] + {:fx [[:effects.wallet/deactivate-and-activate-another-network + {:activate-chain-id activate-chain-id + :deactivate-chain-id deactivate-chain-id + :on-success (fn [] + (rf/dispatch [:wallet/update-network-active deactivate-chain-id false]) + (rf/dispatch [:wallet/update-network-active activate-chain-id true]) + (rf/dispatch [:wallet/on-active-networks-change]) + (when on-success (on-success)))}]]})) + +(rf/reg-event-fx :wallet/on-active-networks-change + (fn [_] + {:fx [[:dispatch [:wallet/reset-accounts-tokens]] + [:dispatch [:wallet/reload-cached-balances]] + [:dispatch [:wallet/reload-collectibles]] + [:dispatch [:wallet.tokens/reset-tokens]] + [:dispatch [:wallet.tokens/get-token-list]] + [:dispatch [:wallet/check-new-networks-seen]]]})) + +(rf/reg-event-fx :wallet/update-network-active + (fn [{:keys [db]} [chain-id active?]] + {:db (assoc-in db + [:wallet :networks/by-id chain-id :active?] + active?)})) + +(rf/reg-event-fx + :wallet/navigate-to-chain-explorer + (fn [{:keys [db]} [chain-id address]] + (let [explorer-link (networks.db/get-block-explorer-address-url db chain-id address)] + {:fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch [:browser.ui/open-url explorer-link]]]}))) + +(rf/reg-event-fx + :wallet/toggle-testnet-mode + (fn [{:keys [db]}] + (let [testnet? (profile.db/testnet? db)] + {:fx [[:dispatch + [:profile.settings/profile-update :test-networks-enabled? (not testnet?) + {:on-success #(rf/dispatch + [:wallet/toggle-testnet-mode-success (not testnet?)])}]]]}))) + +(rf/reg-event-fx + :wallet/toggle-testnet-mode-success + (fn [{:keys [db]} [testnet?]] + {:db (assoc-in db [:profile/profile :test-networks-enabled?] testnet?) + :fx [[:dispatch [:profile/toggle-testnet-mode-banner]] + [:dispatch [:wallet/on-active-networks-change]]]})) + +(rf/reg-event-fx + :wallet/mark-new-networks-as-seen + (fn [{:keys [db]}] + {:db (assoc-in db [:wallet :ui :networks/new-marked-as-seen?] true) + :fx [[:effects.wallet/store-new-networks-as-seen + {:chain-ids (networks.db/get-new-chain-ids db)}]]})) + +(rf/reg-event-fx + :wallet/check-new-networks-seen + (fn [{:keys [db]}] + {:fx [[:effects.wallet/new-networks-seen? + {:chain-ids (networks.db/get-new-chain-ids db) + :on-success #(rf/dispatch [:wallet/update-new-networks-seen %])}]]})) + +(rf/reg-event-fx + :wallet/update-new-networks-seen + (fn [{:keys [db]} [new-networks-seen?]] + {:db (assoc-in db [:wallet :ui :networks/new-marked-as-seen?] new-networks-seen?)})) + +(rf/reg-event-fx + :wallet/filter-network-balances + (fn [{:keys [db]} [network-filter]] + {:db (update-in db + [:wallet :ui :networks/network-filter] + networks.filter/toggle + network-filter)})) + +(rf/reg-event-fx + :wallet/reset-network-balances-filter + (fn [{:keys [db]}] + {:db (update-in db [:wallet :ui] dissoc :networks/network-filter)})) diff --git a/src/status_im/contexts/wallet/networks/filter.cljs b/src/status_im/contexts/wallet/networks/filter.cljs new file mode 100644 index 00000000000..7fa0dff54d8 --- /dev/null +++ b/src/status_im/contexts/wallet/networks/filter.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.wallet.networks.filter) + +(defn by-id + [networks network-filter] + (if (:by-id network-filter) + (remove + (comp (partial contains? (:by-id network-filter)) :chain-id) + networks) + networks)) + +(defn- toggle-by-id + [filtered-chain-ids chain-id] + (let [already-filtered? (contains? filtered-chain-ids chain-id)] + (cond + (nil? filtered-chain-ids) #{chain-id} + already-filtered? (disj filtered-chain-ids chain-id) + (not already-filtered?) (conj filtered-chain-ids chain-id) + :else filtered-chain-ids))) + +(defn toggle + [filter-state filter-data] + (condp #(contains? %2 %1) filter-data + :by-id (update filter-state :by-id toggle-by-id (:by-id filter-data)) + filter-state)) + +(defn has-filters? + [network-filter] + (boolean + (and (seq network-filter) + (every? (complement empty?) (vals network-filter))))) diff --git a/src/status_im/contexts/wallet/networks/filter_test.cljs b/src/status_im/contexts/wallet/networks/filter_test.cljs new file mode 100644 index 00000000000..ab633319526 --- /dev/null +++ b/src/status_im/contexts/wallet/networks/filter_test.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.wallet.networks.filter-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.wallet.networks.filter :as sut] + [tests.wallet-test-data :as test-data])) + +(deftest by-id-test + (testing "networks filtered by id" + (is (match? [test-data/arbitrum] + (sut/by-id [test-data/mainnet test-data/arbitrum] + {:by-id #{test-data/mainnet-chain-id}})))) + + (testing "networks filtered by id, but the filter map is empty" + (is (match? [test-data/mainnet test-data/arbitrum] + (sut/by-id [test-data/mainnet test-data/arbitrum] + {})))) + + (testing "networks filtered by id, but the id filter is empty" + (is (match? [test-data/mainnet test-data/arbitrum] + (sut/by-id [test-data/mainnet test-data/arbitrum] + {:by-id #{}}))))) + +(deftest toggle-test + (testing "returns the new filter if the current one is nil" + (is (match? {:by-id #{test-data/mainnet-chain-id}} + (sut/toggle nil {:by-id test-data/mainnet-chain-id})))) + + (testing "removes the chain-id if already present in the filter" + (is (match? {:by-id #{test-data/mainnet-chain-id}} + (sut/toggle {:by-id #{test-data/mainnet-chain-id test-data/arbitrum-chain-id}} + {:by-id test-data/arbitrum-chain-id})))) + + (testing "adds the chain-id if not already filtered" + (is (match? {:by-id #{test-data/mainnet-chain-id test-data/arbitrum-chain-id}} + (sut/toggle {:by-id #{test-data/mainnet-chain-id}} + {:by-id test-data/arbitrum-chain-id}))))) + +(deftest has-filters?-test + (testing "returns true if there is a filter" + (is (match? true (sut/has-filters? {:by-id #{test-data/arbitrum-chain-id}})))) + + (testing "returns false if filter is not defined" + (is (match? false (sut/has-filters? {:by-id nil}))) + (is (match? false (sut/has-filters? {:by-id #{}}))) + (is (match? false (sut/has-filters? {}))) + (is (match? false (sut/has-filters? nil))))) diff --git a/src/status_im/contexts/wallet/networks/new_networks.cljs b/src/status_im/contexts/wallet/networks/new_networks.cljs new file mode 100644 index 00000000000..6208e6f6a37 --- /dev/null +++ b/src/status_im/contexts/wallet/networks/new_networks.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.wallet.networks.new-networks + (:require [clojure.set :as set] + [promesa.core :as promesa] + [react-native.async-storage :as async-storage])) + +(defn get-stored-seen + [] + (some-> + (async-storage/get-item :networks/seen-new-networks) + (promesa/then set))) + +(defn set-stored-seen + [chain-ids] + (async-storage/set-item! :networks/seen-new-networks chain-ids)) + +(defn marked-as-seen? + [chain-ids] + (some-> + (get-stored-seen) + (promesa/then #(set/subset? chain-ids %)))) + +(defn mark-as-seen + [chain-ids] + (promesa/-> (get-stored-seen) + (into chain-ids) + (set-stored-seen))) diff --git a/src/status_im/contexts/wallet/networks/validation.cljs b/src/status_im/contexts/wallet/networks/validation.cljs new file mode 100644 index 00000000000..158c42a3ab9 --- /dev/null +++ b/src/status_im/contexts/wallet/networks/validation.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.wallet.networks.validation + (:require [malli.core] + [malli.error])) + +(def ?network + [:map-of + int? + [:map {:closed true} + [:network-name keyword?] + [:source :schema.common/image-source] + [:abbreviated-name string?] + [:block-explorer-name string?]]]) + +(defn validate-network + [network] + (when-not (malli.core/validate ?network network) + (throw (ex-info "Invalid network data" + {:errors (-> (malli.core/explain ?network network) + malli.error/humanize)})))) diff --git a/src/status_im/contexts/wallet/router/core.cljs b/src/status_im/contexts/wallet/router/core.cljs new file mode 100644 index 00000000000..1cc317daa71 --- /dev/null +++ b/src/status_im/contexts/wallet/router/core.cljs @@ -0,0 +1,140 @@ +(ns status-im.contexts.wallet.router.core + (:require [schema.core :as schema] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.transaction-settings.core :as transaction-settings] + [status-im.contexts.wallet.router.schema :as router.schema] + [utils.money :as money])) + +(defn- to-gwei + [amount] + (-> amount + money/wei->gwei + (money/with-precision constants/min-token-decimals-to-display) + str)) + +(defn transaction-fee-mode + [route] + (-> route + :tx-gas-fee-mode + transaction-settings/gas-rate->tx-fee-mode)) + +(schema/=> transaction-fee-mode + [:=> + [:cat router.schema/?route] + router.schema/?fee-modes]) + +(defn transaction-fees-by-mode + [{:keys [from-chain suggested-levels-for-max-fees-per-gas suggested-non-eip-1559-fees]}] + (let [eip-1559-enabled? (:eip-1559-enabled from-chain) + {:keys [low medium high low-estimated-time + medium-estimated-time high-estimated-time]} + suggested-levels-for-max-fees-per-gas + {:keys [gas-price estimated-time]} suggested-non-eip-1559-fees] + (if eip-1559-enabled? + {:tx-fee-mode/normal + {:max-fees (to-gwei low) + :estimated-time low-estimated-time} + + :tx-fee-mode/fast + {:max-fees (to-gwei medium) + :estimated-time medium-estimated-time} + + :tx-fee-mode/urgent + {:max-fees (to-gwei high) + :estimated-time high-estimated-time}} + ;; For non EIP-1559 transactions, we show only the fast (medium) mode + {:tx-fee-mode/fast + {:max-fees (to-gwei gas-price) + :estimated-time estimated-time}}))) + +(schema/=> transaction-fees-by-mode + [:=> + [:cat router.schema/?route] + [:map-of + router.schema/?fee-modes + [:map {:closed? true} + [:max-fees :string] + [:estimated-time :int]]]]) + +(defn suggested-gas-price + [{:keys [suggested-non-eip-1559-fees]}] + (some-> suggested-non-eip-1559-fees + :gas-price + to-gwei)) + +(schema/=> suggested-gas-price + [:=> + [:cat router.schema/?route] + [:maybe :string]]) + +(defn transaction-estimated-time + "Get the transaction estimated time. If the time is unknown (0), + fallback to the current fee-mode's estimated time." + [route] + (let [estimated-time (:tx-estimated-time route)] + (if (= 0 estimated-time) + (-> route + transaction-fees-by-mode + (get (transaction-fee-mode route)) + :estimated-time) + estimated-time))) + +(schema/=> transaction-estimated-time + [:=> + [:cat router.schema/?route] + [:maybe :int]]) + +(defn approval-estimated-time + "Get the transaction approval estimated time." + [route] + (:approval-estimated-time route)) + +(schema/=> approval-estimated-time + [:=> + [:cat router.schema/?route] + [:maybe :int]]) + +(defn nonce-field + "Extract nonce field encoded in hex to integer representation" + [route field] + (-> route + field + money/from-hex + money/to-string + js/parseInt)) + +(schema/=> nonce-field + [:=> + [:cat router.schema/?route :keyword] + [:maybe :int]]) + +(defn transaction-gas-fees + [route] + (let [{:keys [tx-base-fee tx-priority-fee + tx-l-1-fee tx-max-fees-per-gas + suggested-min-priority-fee + suggested-max-priority-fee + current-base-fee tx-gas-price from-chain]} route] + {:gas-price (to-gwei tx-gas-price) + :eip-1559-enabled (:eip-1559-enabled from-chain) + :base-fee (to-gwei tx-base-fee) + :network-base-fee (to-gwei current-base-fee) + :tx-priority-fee (to-gwei tx-priority-fee) + :l-1-gas-fee (to-gwei tx-l-1-fee) + :tx-max-fees-per-gas (to-gwei tx-max-fees-per-gas) + :suggested-min-priority-fee (to-gwei suggested-min-priority-fee) + :suggested-max-priority-fee (to-gwei suggested-max-priority-fee)})) + +(schema/=> transaction-gas-fees + [:=> + [:cat router.schema/?route] + [:map {:closed? true} + [:gas-price :string] + [:eip-1559-enabled :boolean] + [:base-fee :string] + [:network-base-fee :string] + [:tx-priority-fee :string] + [:l-1-gas-fee :string] + [:tx-max-fees-per-gas :string] + [:suggested-min-priority-fee :string] + [:suggested-max-priority-fee :string]]]) diff --git a/src/status_im/contexts/wallet/router/router_test.cljs b/src/status_im/contexts/wallet/router/router_test.cljs new file mode 100644 index 00000000000..b23510bfba4 --- /dev/null +++ b/src/status_im/contexts/wallet/router/router_test.cljs @@ -0,0 +1,137 @@ +(ns status-im.contexts.wallet.router.router-test + (:require + [cljs.test :refer-macros [deftest testing is are]] + [malli.generator :as malli.generator] + [status-im.constants :as constants] + [status-im.contexts.wallet.router.core :as router] + [status-im.contexts.wallet.router.schema :as router.schema] + [utils.money :as money])) + +;; NOTE: instead of manually creating the map for the route, we can leverage the malli schema +;; to generate a "valid" route with random data +(def default-route + (malli.generator/generate router.schema/?route {:seed 1})) + +(deftest transaction-fees-by-mode-test + (testing "fails due to schema validation" + (let [route (update-in default-route + [:suggested-levels-for-max-fees-per-gas] + dissoc + :low :medium + :high :low-estimated-time + :medium-estimated-time :high-estimated-time)] + (is (thrown? js/Error (router/transaction-fees-by-mode route))))) + + (testing "fees and estimated time are correctly grouped by fee mode for EIP-1559" + (let [route (-> default-route + (assoc-in [:from-chain :eip-1559-enabled] true) + (update :suggested-levels-for-max-fees-per-gas + assoc + :low (money/to-hex 1500000000) + :medium (money/to-hex 2000000000) + :high (money/to-hex 3456789123) + :low-priority (money/to-hex 1500000000) + :medium-priority (money/to-hex 2000000000) + :high-priority (money/to-hex 3456789123) + :low-estimated-time 30 + :medium-estimated-time 10 + :high-estimated-time 5)) + expected {:tx-fee-mode/normal {:max-fees "1.5" + :estimated-time 30} + :tx-fee-mode/fast {:max-fees "2" + :estimated-time 10} + :tx-fee-mode/urgent {:max-fees "3.456789" + :estimated-time 5}}] + (is (match? expected (router/transaction-fees-by-mode route))))) + + (testing "fees and estimated time are correctly grouped by fee mode for non-EIP-1559" + (let [route (-> default-route + (assoc-in [:from-chain :eip-1559-enabled] false) + (update :suggested-non-eip-1559-fees + assoc + :gas-price (money/to-hex 500000000) + :estimated-time 5)) + expected {:tx-fee-mode/fast {:max-fees "0.5" + :estimated-time 5}}] + (is (match? expected (router/transaction-fees-by-mode route)))))) + +(deftest transaction-fee-mode-test + (testing "extracting the appropriate fee mode" + (are [expected-fee-mode gas-rate] + (match? expected-fee-mode + (-> default-route + (assoc :tx-gas-fee-mode gas-rate) + router/transaction-fee-mode)) + :tx-fee-mode/normal constants/gas-rate-low + :tx-fee-mode/fast constants/gas-rate-medium + :tx-fee-mode/urgent constants/gas-rate-high + :tx-fee-mode/custom constants/gas-rate-custom))) + +(deftest transaction-estimated-time-test + (testing "returns the estimated time correctly" + (is (match? 5 + (-> default-route + (assoc :tx-estimated-time 5) + router/transaction-estimated-time)))) + + (testing "if estimated time is 0, falls back to the time estimation based on fee mode" + (let [route (-> + default-route + (assoc-in [:from-chain :eip-1559-enabled] true) + (assoc :tx-estimated-time 0 + :tx-gas-fee-mode 1) + (update-in [:suggested-levels-for-max-fees-per-gas] + assoc + :low (money/to-hex 1500000000) + :medium (money/to-hex 2000000000) + :high (money/to-hex 3456789123) + :low-priority (money/to-hex 1500000000) + :medium-priority (money/to-hex 2000000000) + :high-priority (money/to-hex 3456789123) + :low-estimated-time 30 + :medium-estimated-time 5 + :high-estimated-time 5))] + (is (match? 5 + (router/transaction-estimated-time route)))))) + +(deftest approval-estimated-time-test + (testing "returns the estimated time correctly" + (is (match? 5 + (-> default-route + (assoc :approval-estimated-time 5) + router/approval-estimated-time))))) + +(deftest transaction-gas-fees-test + (testing "returns the gas fees for EIP-1559 correctly" + (let [expected {:gas-price "0" + :eip-1559-enabled true + :base-fee "0.0005" + :tx-priority-fee "0.0001" + :l-1-gas-fee "0.0002" + :tx-max-fees-per-gas "0.08"}] + (is (match? expected + (-> default-route + (assoc-in [:from-chain :eip-1559-enabled] true) + (assoc :tx-base-fee (money/to-hex 500000) + :tx-priority-fee (money/to-hex 100000) + :tx-l-1-fee (money/to-hex 200000) + :tx-max-fees-per-gas (money/to-hex 80000000) + :tx-gas-price (money/to-hex 0)) + router/transaction-gas-fees))))) + + (testing "returns the gas fees for non-EIP-1559 correctly" + (let [expected {:gas-price "0.5" + :eip-1559-enabled false + :base-fee "0" + :tx-priority-fee "0" + :l-1-gas-fee "0" + :tx-max-fees-per-gas "0"}] + (is (match? expected + (-> default-route + (assoc-in [:from-chain :eip-1559-enabled] false) + (assoc :tx-base-fee (money/to-hex 0) + :tx-priority-fee (money/to-hex 0) + :tx-l-1-fee (money/to-hex 0) + :tx-max-fees-per-gas (money/to-hex 0) + :tx-gas-price (money/to-hex 500000000)) + router/transaction-gas-fees)))))) diff --git a/src/status_im/contexts/wallet/router/schema.cljs b/src/status_im/contexts/wallet/router/schema.cljs new file mode 100644 index 00000000000..0093f51f834 --- /dev/null +++ b/src/status_im/contexts/wallet/router/schema.cljs @@ -0,0 +1,91 @@ +(ns status-im.contexts.wallet.router.schema) + +(def ?fee-modes + [:enum + :tx-fee-mode/custom + :tx-fee-mode/normal + :tx-fee-mode/fast + :tx-fee-mode/urgent]) + +(def ^:private ?suggested-levels-for-max-fees-per-gas + [:map + [:low :schema.common/hex] + [:medium :schema.common/hex] + [:high :schema.common/hex] + [:low-estimated-time nat-int?] + [:medium-estimated-time nat-int?] + [:high-estimated-time nat-int?] + [:low-priority :schema.common/hex] + [:medium-priority :schema.common/hex] + [:high-priority :schema.common/hex]]) + +(def ^:private ?suggested-non-eip-1559-fees + [:map + [:gas-price :schema.common/hex] + [:estimated-time nat-int?]]) + +(def ^:private ?chain + [:map + [:short-name :string] + [:chain-id nat-int?] + [:related-chain-id nat-int?] + [:chain-name :string] + [:native-currency-name :string] + [:eip-1559-enabled :boolean]]) + +(def ?route + [:map + ;; Route/Tx info + [:processor-name :string] + [:router-input-params-uuid :string] + [:tx-packed-data :string] + [:tx-nonce :schema.common/hex] + [:suggested-tx-nonce :schema.common/hex] + [:used-contract-address :schema.common/hex] + [:from-chain ?chain] + [:to-chain ?chain] + + ;; Amounts + [:amount-in :schema.common/hex] + [:amount-out :schema.common/hex] + [:required-native-balance nat-int?] + [:required-token-balance nat-int?] + + ;; Fees + [:tx-gas-amount nat-int?] + [:tx-token-fees :schema.common/hex] + [:tx-total-fee :schema.common/hex] + [:tx-priority-fee [:maybe :schema.common/hex]] + [:tx-base-fee [:maybe :schema.common/hex]] + [:tx-gas-price [:maybe :schema.common/hex]] + [:tx-fee [:maybe :schema.common/hex]] + [:tx-l-1-fee [:maybe :schema.common/hex]] + [:tx-bonder-fees [:maybe :schema.common/hex]] + [:tx-gas-fee-mode nat-int?] + [:tx-estimated-time nat-int?] + [:tx-max-fees-per-gas [:maybe :schema.common/hex]] + [:suggested-min-priority-fee [:maybe :schema.common/hex]] + [:suggested-max-priority-fee [:maybe :schema.common/hex]] + [:suggested-tx-gas-amount [:maybe nat-int?]] + [:suggested-levels-for-max-fees-per-gas [:maybe ?suggested-levels-for-max-fees-per-gas]] + [:suggested-non-eip-1559-fees [:maybe ?suggested-non-eip-1559-fees]] + [:current-base-fee [:maybe :schema.common/hex]] + [:subtract-fees :boolean] + + ;; Approval + [:approval-required :boolean] + [:approval-tx-nonce [:maybe :schema.common/hex]] + [:suggested-approval-tx-nonce [:maybe :schema.common/hex]] + [:suggested-approval-gas-amount nat-int?] + [:approval-contract-address [:maybe :schema.common/hex]] + [:approval-packed-data [:maybe :string]] + [:approval-amount-required [:maybe :schema.common/hex]] + [:approval-fee [:maybe :schema.common/hex]] + [:approval-l-1-fee [:maybe :schema.common/hex]] + [:approval-gas-amount [:maybe nat-int?]] + [:approval-priority-fee [:maybe :schema.common/hex]] + [:approval-base-fee [:maybe :schema.common/hex]] + [:approval-gas-price [:maybe :schema.common/hex]] + [:approval-max-fees-per-gas [:maybe :schema.common/hex]] + [:approval-estimated-time [:maybe nat-int?]] + [:approval-gas-fee-mode [:maybe nat-int?]]]) diff --git a/src/status_im/contexts/wallet/rpc.cljs b/src/status_im/contexts/wallet/rpc.cljs new file mode 100644 index 00000000000..f4b351842c6 --- /dev/null +++ b/src/status_im/contexts/wallet/rpc.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.wallet.rpc + (:require [oops.core :as oops] + [promesa.core :as promesa] + [status-im.common.json-rpc.events :as rpc-events] + [status-im.constants :as constants] + [utils.hex] + [utils.transforms :as transforms])) + +(defn build-transaction + [chain-id tx] + (promesa/let [res (rpc-events/call-async "wallet_buildTransaction" true chain-id tx)] + {:message-to-sign (oops/oget res :messageToSign) + :tx-args (oops/oget res :txArgs)})) + +(defn hash-message-eip-191 + [message] + (rpc-events/call-async "wallet_hashMessageEIP191" true message)) + +(defn safe-sign-typed-data + [data address password chain-id legacy?] + (rpc-events/call-async "wallet_safeSignTypedDataForDApps" + true + data + address + password + chain-id + legacy?)) + +(defn get-suggested-fees + [chain-id] + (-> (rpc-events/call-async "wallet_getSuggestedFees" true chain-id) + (promesa/then transforms/js->clj))) + +(defn send-transaction-with-signature + [chain-id tx-args signature] + (rpc-events/call-async "wallet_sendTransactionWithSignature" + true + chain-id + constants/transaction-pending-type-wallet-connect-transfer + (transforms/js-stringify tx-args 0) + (utils.hex/normalize-hex signature))) + +(defn sign-message + [message address password] + (-> (rpc-events/call-async "wallet_signMessage" + true + message + address + password) + (promesa/then utils.hex/normalize-hex))) diff --git a/src/status_im/contexts/wallet/rpc_data_store/networks.cljs b/src/status_im/contexts/wallet/rpc_data_store/networks.cljs new file mode 100644 index 00000000000..45ffaec178e --- /dev/null +++ b/src/status_im/contexts/wallet/rpc_data_store/networks.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.wallet.rpc-data-store.networks + (:require [schema.core :as schema] + [status-im.contexts.wallet.networks.config :as networks.config] + [status-im.contexts.wallet.networks.core :as networks] + [status-im.contexts.wallet.rpc-data-store.utils :as utils])) + +(defn- make-network + [network-data] + (-> (utils/extract-and-rename + network-data + {:isTest :testnet? + :chainId :chain-id + :isActive :active? + :isDeactivatable :deactivatable? + :relatedChainId :related-chain-id + :layer :layer + :shortName :short-name + :chainColor :chain-color + :chainName :full-name + :blockExplorerUrl :block-explorer-url + :nativeCurrencySymbol :native-currency-symbol + :nativeCurrencyName :native-currency-name + :nativeCurrencyDecimals :native-currency-decimals}) + (merge (get networks.config/networks (:chainId network-data))))) + +(schema/=> make-network + [:=> + [:cat + [:map + [:isTest boolean?] + [:chainId int?] + [:relatedChainId int?] + [:isActive boolean?] + [:isDeactivatable boolean?] + [:layer [:enum 1 2]] + [:shortName string?] + [:chainColor string?] + [:chainName string?] + [:blockExplorerUrl string?] + [:nativeCurrencySymbol string?] + [:nativeCurrencyName string?] + [:nativeCurrencyDecimals int?]]] + [:map {:closed true} + [:testnet? boolean?] + [:chain-id int?] + [:related-chain-id int?] + [:active? boolean?] + [:deactivatable? boolean?] + [:layer [:enum 1 2]] + [:short-name string?] + [:chain-color string?] + [:full-name string?] + [:block-explorer-url string?] + [:native-currency-symbol string?] + [:native-currency-name string?] + [:native-currency-decimals int?] + [:network-name keyword?] + [:source :schema.common/image-source] + [:abbreviated-name string?] + [:block-explorer-name string?]]]) + +(defn- sort-networks + [networks] + ;; Placing the ethereum chains (including testnet) in the top of the list during sorting + (sort-by (juxt (complement networks/ethereum-network?) :layer :short-name) networks)) + +(defn rpc->networks + [networks-data] + (->> (map make-network networks-data) + (filter (comp not nil? :source)))) + +(defn network-chain-ids + [networks] + (let [{:keys [testnet production]} + (group-by #(if (:testnet? %) :testnet :production) networks)] + {:prod (->> production + sort-networks + (map :chain-id)) + :test (->> testnet + sort-networks + (map :chain-id))})) + +(defn networks-by-id + [networks] + (into {} (map (juxt :chain-id identity)) networks)) diff --git a/src/status_im/contexts/wallet/rpc_data_store/utils.cljs b/src/status_im/contexts/wallet/rpc_data_store/utils.cljs new file mode 100644 index 00000000000..7538cb5d786 --- /dev/null +++ b/src/status_im/contexts/wallet/rpc_data_store/utils.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.wallet.rpc-data-store.utils) + +(defn extract-and-rename + "Extract keys from a map and rename them" + [m key-map] + (reduce (fn [acc [old-key new-key]] + (if (contains? m old-key) + (assoc acc new-key (get m old-key)) + acc)) + {} + key-map)) diff --git a/src/status_im/contexts/wallet/send/events.cljs b/src/status_im/contexts/wallet/send/events.cljs new file mode 100644 index 00000000000..de25c14f52d --- /dev/null +++ b/src/status_im/contexts/wallet/send/events.cljs @@ -0,0 +1,834 @@ +(ns status-im.contexts.wallet.send.events + (:require + [clojure.string :as string] + [status-im.constants :as constants] + [status-im.contexts.wallet.collectible.utils :as collectible.utils] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [status-im.contexts.wallet.data-store :as data-store] + [status-im.contexts.wallet.db :as db] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.contexts.wallet.networks.db :as networks.db] + [status-im.contexts.wallet.send.utils :as send-utils] + [status-im.contexts.wallet.sheets.network-selection.view :as network-selection] + [taoensso.timbre :as log] + [utils.address] + [utils.i18n :as i18n] + [utils.number] + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.signatures :as signatures])) + +(rf/reg-event-fx :wallet/clean-send-data + (fn [{:keys [db]}] + {:db (update-in db [:wallet :ui] dissoc :send)})) + +(rf/reg-event-fx :wallet/select-address-tab + (fn [{:keys [db]} [tab]] + {:db (update-in db db-path/send assoc :select-address-tab tab)})) + +(rf/reg-event-fx :wallet/stop-and-clean-suggested-routes + (fn [] + {:fx [[:dispatch [:wallet/stop-get-suggested-routes]] + [:dispatch [:wallet/clean-suggested-routes]]]})) + +(defn- add-not-enough-assets-data + [send-data chosen-route to-network-amounts-by-chain] + (-> send-data + (assoc :route chosen-route + :loading-suggested-routes? false + :suggested-routes {:best []} + :enough-assets? false + :to-values-by-chain to-network-amounts-by-chain) + (update :sender-network-values send-utils/reset-loading-network-amounts-to-zero) + (update :receiver-network-values send-utils/reset-loading-network-amounts-to-zero))) + +(rf/reg-event-fx + :wallet/suggested-routes-success + (fn [{:keys [db]} [suggested-routes-data enough-assets?]] + (let [chosen-route (:best suggested-routes-data) + {:keys [token collectible native-token? + receiver-network-values + sender-network-values tx-type]} (get-in db db-path/send) + token-decimals (if collectible 0 (:decimals token)) + to-network-amounts-by-chain (send-utils/network-amounts-by-chain + {:route chosen-route + :token-decimals token-decimals + :native-token? native-token? + :receiver? true})] + (if-not enough-assets? + {:db (update-in db + db-path/send + add-not-enough-assets-data + chosen-route + to-network-amounts-by-chain)} + (let [routes-available? (pos? (count chosen-route)) + from-network-amounts-by-chain (send-utils/network-amounts-by-chain + {:route chosen-route + :token-decimals token-decimals + :native-token? native-token? + :receiver? false}) + sender-possible-chain-ids (map :chain-id sender-network-values) + sender-network-values (if routes-available? + (send-utils/network-amounts + (if (= tx-type :tx/bridge) + from-network-amounts-by-chain + (send-utils/add-zero-values-to-network-values + from-network-amounts-by-chain + sender-possible-chain-ids))) + (send-utils/reset-loading-network-amounts-to-zero + sender-network-values)) + receiver-network-values (if routes-available? + (send-utils/network-amounts + to-network-amounts-by-chain) + (send-utils/reset-loading-network-amounts-to-zero + receiver-network-values)) + network-links (when routes-available? + (send-utils/network-links chosen-route + sender-network-values + receiver-network-values))] + {:db (update-in db + db-path/send + assoc + :suggested-routes suggested-routes-data + :route chosen-route + :from-values-by-chain from-network-amounts-by-chain + :to-values-by-chain to-network-amounts-by-chain + :sender-network-values sender-network-values + :receiver-network-values receiver-network-values + :network-links network-links + :loading-suggested-routes? false + :enough-assets? true)}))))) + +(rf/reg-event-fx + :wallet/suggested-routes-error + (fn [{:keys [db]} [error-message]] + (let [{:keys [sender-network-values receiver-network-values]} (get-in db db-path/send)] + {:db (-> db + (update-in db-path/send dissoc :route) + (update-in db-path/send + assoc + :sender-network-values + (send-utils/reset-loading-network-amounts-to-zero sender-network-values) + :receiver-network-values + (send-utils/reset-loading-network-amounts-to-zero receiver-network-values) + :loading-suggested-routes? false + :suggested-routes {:best []})) + :fx [[:dispatch + [:toasts/upsert + {:id :send-transaction-error + :type :negative + :text error-message}]]]}))) + +(rf/reg-event-fx :wallet/clean-send-address + (fn [{:keys [db]}] + {:db (update-in db db-path/send dissoc :recipient :to-address)})) + +(rf/reg-event-fx :wallet/clean-send-amount + (fn [{:keys [db]}] + {:db (update-in db db-path/send dissoc :amount)})) + +(rf/reg-event-fx + :wallet/init-send-flow-for-address + (fn [{:keys [db]} [{:keys [recipient address stack-id]}]] + (let [address (utils.address/extract-address-without-chains-info address) + wallet-accounts (vals (get-in db [:wallet :accounts])) + default-account-address (some #(when (:default-account? %) (:address %)) + wallet-accounts) + multiple-accounts? (-> (filter :operable? wallet-accounts) + count + (> 1))] + {:db (cond-> (update-in db [:wallet :ui] dissoc :send) + (not multiple-accounts?) + (assoc-in [:wallet :current-viewing-account-address] default-account-address) + + :always + (update-in + db-path/send + assoc + :general-flow? true + :recipient (or recipient address) + :to-address address)) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch [:shell/change-tab :screen/wallet-stack]] + [:dispatch [:pop-to-root :screen/shell-stack]] + [:dispatch-later + [{:ms 600 + :dispatch (if multiple-accounts? + [:open-modal :screen/wallet.select-from] + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? true + :flow-id :wallet-send-flow}])}]]]}))) + +(rf/reg-event-fx + :wallet/select-send-address + (fn [{:keys [db]} [{:keys [address recipient stack-id start-flow?]}]] + (let [address (utils.address/extract-address-without-chains-info address) + sender (get-in db [:wallet :current-viewing-account-address]) + collectible-tx? (send-utils/tx-type-collectible? (-> db db/send :tx-type)) + collectible (when collectible-tx? (-> db db/send :collectible)) + one-collectible? (when collectible-tx? + (= (collectible.utils/collectible-balance collectible sender) 1))] + {:db (update-in db + db-path/send + assoc + :recipient (or recipient address) + :to-address address) + :fx [(when (and collectible-tx? one-collectible?) + [:dispatch [:wallet/start-get-suggested-routes {:amount 1}]]) + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id :wallet-send-flow}]]]}))) + +(rf/reg-event-fx + :wallet/set-token-to-send + (fn [{:keys [db]} + [{:keys [token-symbol token network stack-id start-flow? owners] :as params} entry-point]] + ;; `token` is a map extracted from the sender, but in the wallet home page we don't know the + ;; sender yet, so we only provide the `token-symbol`, later in + ;; `:wallet/select-from-account` the `token` key will be set. + (let [{:keys [wallet]} db + unique-owner (when (= (count owners) 1) + (first owners)) + unique-owner-tokens (get-in db [:wallet :accounts unique-owner :tokens]) + token-data (or token + (when (and token-symbol unique-owner) + (some #(when (= (:symbol %) token-symbol) %) + unique-owner-tokens))) + view-id (:view-id db) + root-screen? (or (= view-id :screen/wallet-stack) (nil? view-id)) + multi-account-balance? (-> (utils/get-accounts-with-token-balance (:accounts wallet) + token) + (count) + (> 1)) + account-not-defined? (and (not unique-owner) multi-account-balance?) + networks-with-balance (when (and token-data (:balances-per-chain token)) + (filter #(not= (:balance %) "0") + (vals (:balances-per-chain token)))) + balance-in-only-one-network? (when networks-with-balance (= (count networks-with-balance) 1)) + network-details (networks.db/get-active-networks db) + network (if balance-in-only-one-network? + (first (filter #(= (:chain-id %) + (:chain-id (first networks-with-balance))) + network-details)) + network)] + (when (or token-data token-symbol) + {:db (cond-> db + network (update-in db-path/send + #(-> % + (dissoc :collectible) + (assoc :network network + :native-token? (= (or token-symbol + (:symbol token-data)) + (:native-currency-symbol network))))) + token-symbol (update-in db-path/send assoc :token-symbol token-symbol) + token-data (update-in db-path/send + #(assoc % + :token (assoc token-data + :supported-networks + (network-utils/network-list + token-data + network-details)) + :token-display-name (:symbol token-data) + :token-symbol (:symbol token-data))) + unique-owner (assoc-in [:wallet :current-viewing-account-address] unique-owner) + entry-point (update-in db-path/send assoc :entry-point entry-point) + :always (update-in db-path/send assoc :tx-type :tx/send)) + :fx (cond + ;; If the token has a balance in more than one account and this was dispatched from + ;; the general wallet screen, open the account selection screen. + (and account-not-defined? root-screen?) + [[:dispatch [:open-modal :screen/wallet.select-from]]] + + ;; If the token has a balance in only one account (or this was dispatched from the + ;; account screen) and the network is already set, stop and clean suggested routes, + ;; then navigate forward in the send flow. + (some? network) + [[:dispatch [:wallet/stop-and-clean-suggested-routes]] + [:dispatch + ;; ^:flush-dom allows us to make sure the re-frame DB state is always synced + ;; before the navigation occurs, so the new screen is always rendered with + ;; the DB state set by this event. By adding the metadata we are omitting + ;; a 1-frame blink when the screen is mounted. + ^:flush-dom + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id :wallet-send-flow}]]] + + ;; If we don't know the network but need to set it, show the network selection drawer. + :else + [[:dispatch + [:show-bottom-sheet + {:content (fn [] + [network-selection/view + {:token-symbol (or token-symbol (:symbol token-data)) + :source :send + :on-select-network (fn [network] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:wallet/set-token-to-send + (assoc params :network network)]))}])}]]])})))) + +(rf/reg-event-fx + :wallet/edit-token-to-send + (fn [{:keys [db]} [token]] + (let [{token-networks :supported-networks + token-symbol :symbol} token + receiver-networks (get-in db (conj db-path/send :receiver-networks)) + token-networks-ids (map :chain-id token-networks) + token-not-supported-in-receiver-networks? (not-any? (set receiver-networks) + token-networks-ids)] + {:db (update-in db + db-path/send + assoc + :token token + :token-display-name token-symbol + :token-not-supported-in-receiver-networks? + token-not-supported-in-receiver-networks?) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch [:wallet/stop-and-clean-suggested-routes]]]}))) + +(rf/reg-event-fx :wallet/clean-selected-token + (fn [{:keys [db]}] + {:db (update-in db + db-path/send + dissoc + :token + :token-symbol + :token-display-name + :network)})) + +(rf/reg-event-fx :wallet/clean-selected-collectible + (fn [{:keys [db]} [{:keys [ignore-entry-point?]}]] + (let [{:keys [entry-point collectible-multiple-owners? tx-type]} (get-in db db-path/send) + entry-point-wallet-home? (= entry-point :screen/wallet-stack) + multiple-owners? collectible-multiple-owners? + transaction-type tx-type] + (when (or ignore-entry-point? + (and entry-point-wallet-home? (not multiple-owners?)) + (not entry-point-wallet-home?)) + {:db (update-in db + db-path/send + dissoc + :collectible + :collectible-multiple-owners? + :token-display-name + (when (send-utils/tx-type-collectible? transaction-type) + :network) + :amount + (when (send-utils/tx-type-collectible? transaction-type) + :tx-type))})))) + +(rf/reg-event-fx + :wallet/set-collectible-to-send + (fn [{db :db} [{:keys [collectible current-screen start-flow? entry-point]}]] + (let [viewing-account? (some? (-> db :wallet :current-viewing-account-address)) + entry-point (cond + entry-point entry-point + viewing-account? :account-collectible-tab + :else :screen/wallet-stack) + collection-data (:collection-data collectible) + collectible-data (:collectible-data collectible) + contract-type (:contract-type collectible) + tx-type (if (= contract-type constants/wallet-contract-type-erc-1155) + :tx/collectible-erc-1155 + :tx/collectible-erc-721) + chain-id (get-in collectible [:id :contract-id :chain-id]) + network (networks.db/get-network-details db chain-id) + collectible-id (get-in collectible [:id :token-id]) + single-owner? (-> collectible :ownership count (= 1)) + owner-address (-> collectible :ownership first :address) + one-collectible? (when single-owner? + (= (collectible.utils/collectible-balance collectible owner-address) 1)) + token-display-name (cond + (and collectible + (not (string/blank? (:name collectible-data)))) + (:name collectible-data) + + collectible + (str (:name collection-data) " #" collectible-id)) + collectible-tx (-> db + (update-in db-path/send dissoc :token) + (update-in db-path/send + assoc + :entry-point + entry-point + :collectible + collectible + :collectible-multiple-owners? + (not single-owner?) + :token-display-name + token-display-name + :tx-type + tx-type + :network network)) + recipient-set? (-> db db/send :recipient)] + {:db (cond-> collectible-tx + + (and (not viewing-account?) single-owner?) + (assoc-in [:wallet :current-viewing-account-address] owner-address) + + one-collectible? + (update-in db-path/send assoc :amount 1)) + :fx (if + ;; If the collectible is present in multiple accounts, the user will be taken to select + ;; the address to send from + (and (not viewing-account?) (not single-owner?)) + [[:dispatch [:open-modal :screen/wallet.select-from]]] + + [(when (and one-collectible? recipient-set?) + [:dispatch [:wallet/start-get-suggested-routes {:amount 1}]]) + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen current-screen + :start-flow? start-flow? + :flow-id :wallet-send-flow}]]])}))) + +(rf/reg-event-fx + :wallet/set-collectible-amount-to-send + (fn [{db :db} [{:keys [stack-id amount]}]] + {:db (update-in db db-path/send assoc :amount amount) + :fx [[:dispatch [:wallet/start-get-suggested-routes {:amount amount}]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :flow-id :wallet-send-flow}]]]})) + +(rf/reg-event-fx + :wallet/set-token-amount-to-send + (fn [{:keys [db]} [{:keys [amount stack-id start-flow?]}]] + {:db (update-in db db-path/send assoc :amount amount) + :fx [[:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id :wallet-send-flow}]]]})) + +(rf/reg-event-fx + :wallet.send/auth-slider-completed + (fn [{:keys [db]}] + (let [last-request-uuid (get-in db (conj db-path/send :last-request-uuid))] + {:db (update-in db db-path/send dissoc :transaction-for-signing) + :fx [[:dispatch [:wallet/build-transactions-from-route {:request-uuid last-request-uuid}]] + [:dispatch + [:wallet.send/set-sign-transactions-callback-fx + [:dispatch + [:wallet/prepare-signatures-for-transactions :send]]]]]}))) + +(defn log-transaction-signature-error + [error] + (log/error + "failed to prepare signatures for transactions" + {:event :wallet/prepare-signatures-for-transactions + :error error}) + (rf/dispatch + [:toasts/upsert + {:id :prepare-signatures-for-transactions-error + :type :negative + :text (:message error)}])) + +(defn- send-transactions-with-signatures + [tx-type signatures] + (rf/dispatch + [:wallet/send-router-transactions-with-signatures tx-type + signatures])) + +(rf/reg-event-fx + :wallet/send-router-transactions-with-signatures + (fn [{:keys [db]} [tx-type signatures]] + (let [transaction-for-signing (get-in db [:wallet :ui tx-type :transaction-for-signing]) + signatures-map (reduce (fn [acc {:keys [message signature]}] + (assoc acc + message + (signatures/signature->rsv signature))) + {} + signatures)] + {:json-rpc/call [{:method "wallet_sendRouterTransactionsWithSignatures" + :params [{:uuid (get-in transaction-for-signing [:sendDetails :uuid]) + :signatures signatures-map}] + :on-success (fn [] + (rf/dispatch [:hide-bottom-sheet])) + :on-error (fn [error] + (log/error "failed to send router transactions with signatures" + {:event :wallet/send-router-transactions-with-signatures + :error error}) + (rf/dispatch [:toasts/upsert + {:id :send-router-transactions-with-signatures-error + :type :negative + :text (:message error)}]))}]}))) + +(defn- transaction-sign-payload + [transaction-for-signing] + (let [signing-details (:signingDetails transaction-for-signing) + address (:address signing-details) + messages (:hashes signing-details)] + (reduce (fn [acc message] + (conj acc + {:address address + :message message})) + [] + messages))) + +(rf/reg-event-fx + :wallet/prepare-signatures-for-transactions + (fn [{:keys [db]} [tx-type]] + (let [sign-payload (-> db + (get-in [:wallet :ui tx-type :transaction-for-signing]) + transaction-sign-payload) + send-tx-fn (partial send-transactions-with-signatures tx-type)] + {:fx [[:dispatch + [:standard-auth/authorize-and-sign + {:sign-payload sign-payload + :auth-button-label (i18n/label :t/confirm) + :on-sign-success (fn [signatures] + (send-tx-fn signatures)) + :on-sign-error log-transaction-signature-error}]]]}))) + +(rf/reg-event-fx + :wallet/standard-auth-autorization-success + (fn [_ [hashes address sha3-pwd send-tx-fn]] + {:fx [[:effects.wallet/sign-transaction-hashes + {:hashes hashes + :address address + :password (security/safe-unmask-data sha3-pwd) + :on-success send-tx-fn + :on-error log-transaction-signature-error}]]})) + +(rf/reg-event-fx + :wallet/build-transaction-for-collectible-route + (fn [{:keys [db]}] + (let [{:keys [last-request-uuid collectible]} (get-in db db-path/send) + collectible-unique-id (:unique-id collectible)] + {:db (-> + db + (update-in db-path/send dissoc :transaction-for-signing) + (assoc-in [:wallet :ui :collectibles :pending collectible-unique-id] true)) + :fx [[:dispatch [:wallet/build-transactions-from-route {:request-uuid last-request-uuid}]]]}))) + +(rf/reg-event-fx + :wallet/set-token-amount-to-bridge + (fn [{:keys [db]} [{:keys [amount stack-id start-flow?]}]] + {:db (-> db + (update-in db-path/send assoc :amount amount) + (update-in db-path/send dissoc :transaction-for-signing)) + :fx [[:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id :wallet-bridge-flow}]]]})) + +(rf/reg-event-fx + :wallet/clean-bridge-to-selection + (fn [{:keys [db]}] + {:db (update-in db db-path/send dissoc :bridge-to-chain-id)})) + +(rf/reg-event-fx + :wallet/clean-suggested-routes + (fn [{:keys [db]}] + (let [keys-to-remove [:to-values-by-chain :network-links :sender-network-values :route + :receiver-network-values :suggested-routes :from-values-by-chain + :loading-suggested-routes? :amount :enough-assets?]] + {:db (update-in db db-path/send #(apply dissoc % keys-to-remove))}))) + +(rf/reg-event-fx :wallet/reset-network-amounts-to-zero + (fn [{:keys [db]}] + (let [{:keys [sender-network-values + receiver-network-values]} (get-in db db-path/send) + sender-network-values (send-utils/reset-network-amounts-to-zero + sender-network-values) + receiver-network-values (send-utils/reset-network-amounts-to-zero + receiver-network-values)] + {:db (-> db + (update-in db-path/send + assoc + :sender-network-values sender-network-values + :receiver-network-values receiver-network-values) + (update-in db-path/send + dissoc + :network-links + (when (empty? sender-network-values) :sender-network-values) + (when (empty? receiver-network-values) :receiver-network-values)))}))) + +(rf/reg-event-fx :wallet/start-get-suggested-routes + (fn [{:keys [db]} [{:keys [amount amount-out updated-token] :as args :or {amount-out "0"}}]] + (let [wallet-address (get-in db [:wallet :current-viewing-account-address]) + {:keys [token tx-type collectible to-address + network bridge-to-chain-id max-slippage] + :or {token updated-token + max-slippage + constants/default-slippage}} (get-in db db-path/send) + ;; NOTE: for some reason, the router considers non-active networks as well when building + ;; routes, so we should use all the networks when disabling networks for the route + network-chain-ids (networks.db/get-chain-ids db) + token-decimal (when token (:decimals token)) + token-id (utils/format-token-id token collectible) + to-token-id "" + gas-rates constants/gas-rate-medium + to-hex (fn [v] + (send-utils/amount-in-hex v (if token token-decimal 0))) + amount-in (to-hex amount) + amount-out (to-hex amount-out) + from-address wallet-address + network-chain-id (if collectible + (get-in collectible [:id :contract-id :chain-id]) + (:chain-id network)) + disabled-from-chain-ids (filter #(not= % network-chain-id) network-chain-ids) + disabled-to-chain-ids (filter #(not= % + (if (= tx-type :tx/bridge) + bridge-to-chain-id + network-chain-id)) + network-chain-ids) + send-type (case tx-type + :tx/collectible-erc-721 + constants/send-type-erc-721-transfer + :tx/collectible-erc-1155 + constants/send-type-erc-1155-transfer + :tx/bridge constants/send-type-bridge + constants/send-type-transfer) + sender-network-values (when (= tx-type :tx/bridge) + (send-utils/loading-network-amounts + {:networks [network-chain-id] + :values {network-chain-id amount} + :receiver? false})) + receiver-network-values (when (= tx-type :tx/bridge) + (send-utils/loading-network-amounts + {:networks [bridge-to-chain-id] + :receiver? true})) + request-uuid (str (random-uuid)) + params [{:uuid request-uuid + :sendType send-type + :addrFrom from-address + :addrTo to-address + :amountIn amount-in + :amountOut amount-out + :tokenID token-id + :toTokenID to-token-id + :disabledFromChainIDs disabled-from-chain-ids + :disabledToChainIDs disabled-to-chain-ids + :gasFeeMode gas-rates + :fromLockedAmount {} + :username (:username args) + :publicKey (:publicKey args) + :packID (:packID args) + :slippagePercentage max-slippage}]] + (when (and to-address from-address amount-in token-id) + {:db (update-in db + db-path/send + #(-> % + (assoc :last-request-uuid request-uuid + :amount amount + :loading-suggested-routes? true + :sender-network-values sender-network-values + :receiver-network-values receiver-network-values) + (dissoc :network-links :skip-processing-suggested-routes?) + (cond-> token (assoc :token token)))) + :json-rpc/call [{:method "wallet_getSuggestedRoutesAsync" + :params params + :on-error (fn [error] + (rf/dispatch [:wallet/suggested-routes-error error]) + (log/error "failed to get suggested routes (async)" + {:event :wallet/start-get-suggested-routes + :error (:message error) + :params params}))}]})))) + +(rf/reg-event-fx :wallet/stop-get-suggested-routes + (fn [{:keys [db]}] + ;; Adding a key to prevent processing route signals in the client until the routes generation is + ;; stopped. This is to ensure no route signals are processed when we make the RPC call + {:db (update-in db db-path/send assoc :skip-processing-suggested-routes? true) + :fx [[:json-rpc/call + [{:method "wallet_stopSuggestedRoutesAsyncCalculation" + :params [] + :on-error (fn [error] + (log/error "failed to stop suggested routes calculation" + {:event :wallet/stop-get-suggested-routes + :error error}))}]]]})) + +(rf/reg-event-fx + :wallet/handle-suggested-routes + (fn [{:keys [db]} [data]] + (let [{send :send swap? :swap} (-> db :wallet :ui) + skip-processing-routes? (:skip-processing-suggested-routes? send) + clean-user-tx-settings? (get-in db + [:wallet :ui :user-tx-settings + :delete-on-routes-update?])] + (when (or swap? (not skip-processing-routes?)) + (let [{error-code :code + :as error} (:ErrorResponse data) + enough-assets? (not (and (:Best data) (= error-code "WR-002"))) + failure? (and error enough-assets? (not swap?)) + error-message (if (zero? error-code) "An error occurred" (:details error))] + (when failure? + (log/error "failed to get suggested routes (async)" + {:event :wallet/handle-suggested-routes + :error error-message})) + (merge + (when clean-user-tx-settings? + {:db (update-in db [:wallet :ui] dissoc :user-tx-settings)}) + {:fx [[:dispatch + (cond + (and failure? swap?) [:wallet/swap-proposal-error error] + failure? [:wallet/suggested-routes-error error-message] + swap? [:wallet/swap-proposal-success (data-store/fix-routes data)] + :else [:wallet/suggested-routes-success (data-store/fix-routes data) + enough-assets?])]]})))))) + +(rf/reg-event-fx + :wallet/transaction-success + (fn [{:keys [db]} [sent-transactions]] + (let [wallet-transactions (get-in db [:wallet :transactions] {}) + transactions (utils/transactions->hash-to-transaction-map sent-transactions) + transaction-ids (->> transactions + vals + (map :hash))] + {:db (-> db + (update-in db-path/send + assoc + :just-completed-transaction? true + :transaction-ids transaction-ids) + (assoc-in [:wallet :transactions] (merge wallet-transactions transactions))) + :fx [[:dispatch [:wallet/end-transaction-flow]] + [:dispatch-later + [{:ms 2000 + :dispatch [:wallet/stop-and-clean-suggested-routes]}]] + [:dispatch-later + [{:ms 2000 + :dispatch [:wallet/clean-just-completed-transaction]}]]]}))) + +(rf/reg-event-fx + :wallet/transaction-failure + (fn [_ [{:keys [details]}]] + {:fx [[:dispatch [:wallet/end-transaction-flow]] + [:dispatch-later + [{:ms 2000 + :dispatch [:wallet/stop-and-clean-suggested-routes]}]] + [:dispatch + [:toasts/upsert + {:id :send-transaction-failure + :type :negative + :text (or details "An error occured")}]]]})) + +(rf/reg-event-fx :wallet/clean-just-completed-transaction + (fn [{:keys [db]}] + {:db (update-in db db-path/send dissoc :just-completed-transaction?)})) + +(rf/reg-event-fx :wallet/clean-up-transaction-flow + (fn [_] + {:fx [[:dispatch [:dismiss-modal :screen/wallet.transaction-confirmation]] + [:dispatch [:dismiss-modal :screen/wallet.collectible]] + [:dispatch [:wallet/clean-scanned-address]] + [:dispatch [:wallet/clean-local-suggestions]] + [:dispatch [:wallet/clean-send-address]] + [:dispatch [:wallet/select-address-tab nil]]]})) + +(rf/reg-event-fx :wallet/end-transaction-flow + (fn [{:keys [db]}] + (let [address (get-in db [:wallet :current-viewing-account-address])] + {:fx [[:dispatch [:wallet/navigate-to-account-within-stack address]] + [:dispatch [:wallet/select-account-tab :activity]] + [:dispatch-later + [{:ms 20 + :dispatch [:wallet/clean-up-transaction-flow]}]]]}))) + +(rf/reg-event-fx + :wallet/build-transactions-from-route + (fn [_ [{:keys [request-uuid]}]] + {:json-rpc/call [{:method "wallet_buildTransactionsFromRoute" + :params [request-uuid] + :on-error (fn [error] + (log/error "failed to build transactions from route" + {:event :wallet/build-transactions-from-route + :error error}) + (rf/dispatch [:toasts/upsert + {:id :build-transactions-from-route-error + :type :negative + :text (:message error)}]))}]})) + +(rf/reg-event-fx + :wallet/select-from-account + (fn [{db :db} [{:keys [address stack-id network-details network start-flow?] :as params}]] + (let [{:keys [token-symbol + tx-type]} (db/send db) + no-tx-type? (nil? tx-type) + collectible-tx? (send-utils/tx-type-collectible? tx-type) + token (when token-symbol + ;; When this flow has started in the wallet home page, we + ;; know the token or collectible to send, but we don't know + ;; from which account, so we extract the token data from + ;; the picked account. + (let [token (utils/get-token-from-account db + token-symbol + address)] + (utils/token-with-balance token network-details))) + asset-selected? (or collectible-tx? (some? token)) + bridge-tx? (= tx-type :tx/bridge) + flow-id (if bridge-tx? + :wallet-bridge-flow + :wallet-send-flow) + networks-with-balance (when (and token (:balances-per-chain token)) + (filter #(not= (:balance %) "0") + (vals (:balances-per-chain token)))) + balance-in-only-one-network? (when networks-with-balance (= (count networks-with-balance) 1)) + networks (networks.db/get-active-networks db) + network (if balance-in-only-one-network? + (first (filter #(= (:chain-id %) + (:chain-id (first networks-with-balance))) + networks)) + network)] + {:db (cond-> db + network (update-in db-path/send assoc :network network) + token-symbol (update-in db-path/send assoc :token token) + bridge-tx? (update-in db-path/send assoc :to-address address)) + :fx (if (or no-tx-type? (some? network) collectible-tx? (not asset-selected?)) + [[:dispatch [:wallet/switch-current-viewing-account address]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? (and start-flow? (not balance-in-only-one-network?)) + :flow-id flow-id}]]] + [[:dispatch [:dismiss-modal :screen/wallet.select-from]] + [:dispatch [:wallet/switch-current-viewing-account address]] + [:dispatch + [:show-bottom-sheet + {:content (fn [] + [network-selection/view + {:token-symbol (or token-symbol (:symbol token)) + :source :send + :on-select-network (fn [network] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:wallet/select-from-account + (assoc params :network network)]))}])}]]])}))) + +(rf/reg-event-fx + :wallet/clean-route-data-for-collectible-tx + (fn [{db :db}] + (when (send-utils/tx-type-collectible? (-> db db/send :tx-type)) + {:db (update-in db + db-path/send + dissoc + :amount + :route + :suggested-routes + :last-request-uuid + :transaction-for-signing + :sign-transactions-callback-fx) + :fx [[:dispatch [:wallet/stop-and-clean-suggested-routes]]]}))) + +(rf/reg-event-fx + :wallet/collectible-amount-navigate-back + (fn [{db :db} [{:keys []}]] + (let [keep-tx-data? (#{:account-collectible-tab :screen/wallet-stack} + (-> db db/send :entry-point))] + {:db (cond-> db + :always (update-in db-path/send dissoc :amount :route) + (not keep-tx-data?) (update-in db-path/send dissoc :tx-type)) + :fx [[:dispatch [:navigate-back]]]}))) + +(rf/reg-event-fx :wallet.send/set-sign-transactions-callback-fx + (fn [{:keys [db]} [callback-fx]] + {:db (assoc-in db (conj db-path/send :sign-transactions-callback-fx) callback-fx)})) diff --git a/src/status_im/contexts/wallet/send/events_test.cljs b/src/status_im/contexts/wallet/send/events_test.cljs new file mode 100644 index 00000000000..c0e1033dced --- /dev/null +++ b/src/status_im/contexts/wallet/send/events_test.cljs @@ -0,0 +1,596 @@ +(ns status-im.contexts.wallet.send.events-test + (:require + [cljs.test :refer-macros [is testing]] + [matcher-combinators.matchers :as m] + matcher-combinators.test + [re-frame.db :as rf-db] + status-im.contexts.wallet.send.events + [status-im.contexts.wallet.send.utils :as send-utils] + [test-helpers.unit :as h] + [utils.money :as money])) + +(defn make-send-structure + [send-data] + {:wallet {:ui {:send send-data}}}) + +(defn extract-send-key + [result send-key] + (get-in result [:db :wallet :ui :send send-key])) + +(defn collectible-with-balance + [balance] + {:name "DOG #1" + :description + "dogs are cute and this one is the cutestdogs are cute and this one is the cutest" + :ownership [{:address "0x01" + :balance balance}] + :id {:contract-id {:address "0x11" + :chain-id 1} + :token-id "some-id"}}) + +(h/deftest-event :wallet/set-token-to-send + [event-id dispatch] + (let [token-symbol "ETH" + token {:symbol "ETH" + :name "Ether" + :networks #{{:chain-id 421614} + {:chain-id 11155420} + {:chain-id 11155111}}} + receiver-networks [421614 11155420]] + (testing "can be called with :token" + (let [initial-db (make-send-structure {:receiver-networks receiver-networks}) + expected-db (make-send-structure {:token-display-name token-symbol + :token-symbol token-symbol}) + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id {:token token}])] + (is (match? expected-db (:db result))))) + (testing "can be called with :token-symbol" + (let [initial-db (make-send-structure {:receiver-networks receiver-networks}) + expected-db (make-send-structure {:token-symbol token-symbol}) + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id {:token-symbol token-symbol}])] + (is (match? expected-db (:db result))))) + (testing "shouldn't have changes if called without :token or :token-symbol") + (let [initial-db (make-send-structure {:receiver-networks receiver-networks}) + expected-db nil + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id {}])] + (is (match? expected-db (:db result)))) + (testing "should clean :collectible set" + (let [initial-db (make-send-structure {:receiver-networks receiver-networks + :collectible "some-collectible"}) + expected-db (make-send-structure {:token-display-name token-symbol}) + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id + {:token token + :network "network"}])] + (is (match? expected-db (:db result))) + (is (match? nil (extract-send-key result :collectible))))) + (testing "should set :token-not-supported-in-receiver-networks?" + (let [initial-db (make-send-structure {:receiver-networks []}) + expected-db (make-send-structure {:token-display-name token-symbol}) + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id {:token token}])] + (is (match? expected-db (:db result))))))) + +(h/deftest-event :wallet/edit-token-to-send + [event-id dispatch] + (let [token-symbol "ETH" + token {:symbol "ETH" + :name "Ether" + :networks #{{:chain-id 421614} + {:chain-id 11155420} + {:chain-id 11155111}} + :supported-networks #{{:chain-id 421614} + {:chain-id 11155420} + {:chain-id 11155111}}} + receiver-networks [421614 11155420]] + (testing "can be called with :token" + (let [initial-db (make-send-structure {:receiver-networks receiver-networks + :token-display-name "DAI" + :token-not-supported-in-receiver-networks? true}) + expected-db (make-send-structure {:token-display-name token-symbol + :token-not-supported-in-receiver-networks? false}) + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id token])] + (is (match? expected-db (:db result))))) + (testing "should set :token-not-supported-in-receiver-networks?" + (let [initial-db (make-send-structure {:receiver-networks [] + :token-display-name "DAI" + :token-not-supported-in-receiver-networks? false}) + expected-db (make-send-structure {:token-display-name token-symbol + :token-not-supported-in-receiver-networks? true}) + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id token])] + (is (match? expected-db (:db result))))))) + +(h/deftest-event :wallet/set-collectible-to-send + [event-id dispatch] + (let + [collectible + {:data-type 2 + :id + {:contract-id + {:chain-id 11155111 + :address "0x1ed60fedff775d500dde21a974cd4e92e0047cc8"} + :token-id "15"} + :contract-type 3 + :collectible-data + {:name "DOG #1" + :description + "dogs are cute and this one is the cutestdogs are cute and this one is the cutest"} + :collection-data + {:name "ERC-1155 Faucet"} + :ownership + [{:address "0xf90014b2027e584fc96e6f6c8078998fe46c5ccb" + :balance "1" + :tx-timestamp 1710331776}] + :preview-url + {:uri + "https://ipfs.io/ipfs/bafybeie7b7g7iibpac4k6ydw4m5ivgqw5vov7oyzlf4v5zoor57wokmsxy/isolated-happy-smiling-dog-white-background-portrait-4_1562-693.avif"}} + initial-db (make-send-structure {:token {:symbol "ETH"}}) + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id {:collectible collectible}])] + (testing ":collectible field assigned" + (is (match? collectible (extract-send-key result :collectible)))) + (testing ":token should be removed" + (is (match? nil (extract-send-key result :token)))) + (testing ":token-display-name assigned" + (is (match? "DOG #1" (extract-send-key result :token-display-name)))) + (testing ":tx-type assigned" + (is (match? :tx/collectible-erc-1155 (extract-send-key result :tx-type)))) + (testing "amount set if collectible was single" + (is (match? 1 (extract-send-key result :amount)))))) + +(h/deftest-event :wallet/set-collectible-amount-to-send + [event-id dispatch] + (let [initial-db (make-send-structure nil) + expected-fx [[:dispatch [:wallet/start-get-suggested-routes {:amount 10}]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen nil :flow-id :wallet-send-flow}]]] + amount 10 + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id {:amount amount}])] + (testing "amount set" + (is (match? amount (extract-send-key result :amount)))) + (testing "effects match" + (is (match? expected-fx (:fx result)))))) + +(h/deftest-event :wallet/set-token-amount-to-send + [event-id dispatch] + (let [initial-db (make-send-structure {:token {:symbol "ETH"}}) + amount 10 + _ (reset! rf-db/app-db initial-db) + result (dispatch [event-id {:amount amount}])] + (testing "amount set" + (is (match? amount (extract-send-key result :amount)))))) + +(h/deftest-event :wallet/clean-send-data + [event-id dispatch] + (let [token-symbol "ETH" + token {:symbol "ETH" + :name "Ether" + :networks #{{:chain-id 421614} + {:chain-id 11155420} + {:chain-id 11155111}}} + receiver-networks [421614 11155420] + expected-db {:wallet {:ui {:other-props :value}}}] + (reset! rf-db/app-db + (-> (make-send-structure {:token-display-name token-symbol + :token token + :receiver-networks receiver-networks}) + (assoc-in [:wallet :ui :other-props] :value))) + (is (match-strict? expected-db (:db (dispatch [event-id])))))) + +(h/deftest-event :wallet/select-address-tab + [event-id dispatch] + (let [expected-db (make-send-structure {:select-address-tab "tab"})] + (reset! rf-db/app-db (make-send-structure nil)) + (is (match? expected-db (:db (dispatch [event-id "tab"])))))) + +(h/deftest-event :wallet/clean-send-address + [event-id dispatch] + (let [expected-db (make-send-structure {:other-props :value})] + (reset! rf-db/app-db + (make-send-structure {:to-address "0x01" + :recipient {:recipient-type :saved-address + :label "label"} + :other-props :value})) + (is (match-strict? expected-db (:db (dispatch [event-id])))))) + +(h/deftest-event :wallet/clean-send-amount + [event-id dispatch] + (let [expected-db (make-send-structure {:other-props :value})] + (reset! rf-db/app-db + (make-send-structure {:amount 10 + :other-props :value})) + (is (match-strict? expected-db (:db (dispatch [event-id])))))) + +(h/deftest-event :wallet/clean-selected-token + [event-id dispatch] + (let [expected-db (make-send-structure {:other-props :value})] + (reset! rf-db/app-db + (make-send-structure {:other-props :value + :token "ETH" + :token-display-name "ETH"})) + (is (match-strict? expected-db (:db (dispatch [event-id])))))) + +(h/deftest-event :wallet/clean-selected-collectible + [event-id dispatch] + (let [expected-db (make-send-structure {:other-props :value})] + (reset! rf-db/app-db + (make-send-structure {:other-props :value + :collectible "ETH" + :token-display-name "ETH" + :amount 10})) + (is (match-strict? expected-db (:db (dispatch [event-id])))))) + +(h/deftest-event :wallet/suggested-routes-error + [event-id dispatch] + (let [sender-network-amounts [{:chain-id 1 :total-amount (money/bignumber "100") :type :loading} + {:chain-id 10 :total-amount (money/bignumber "200") :type :default}] + receiver-network-amounts [{:chain-id 1 :total-amount (money/bignumber "100") :type :loading}] + expected-result {:db (make-send-structure + {:sender-network-values + (send-utils/reset-loading-network-amounts-to-zero + sender-network-amounts) + :receiver-network-values + (send-utils/reset-loading-network-amounts-to-zero + receiver-network-amounts) + :loading-suggested-routes? false + :suggested-routes {:best []}}) + :fx [[:dispatch + [:toasts/upsert + {:id :send-transaction-error + :type :negative + :text "error"}]]]}] + (reset! rf-db/app-db + (make-send-structure {:sender-network-values sender-network-amounts + :receiver-network-values receiver-network-amounts + :route :values + :loading-suggested-routes? true})) + (is (match? expected-result (dispatch [event-id "error"]))))) + +(h/deftest-event :wallet/reset-network-amounts-to-zero + [event-id dispatch] + (let [sender-network-values [{:chain-id 1 :total-amount (money/bignumber "100") :type :default} + {:chain-id 10 :total-amount (money/bignumber "200") :type :default}] + receiver-network-values [{:chain-id 1 :total-amount (money/bignumber "100") :type :loading}] + sender-network-zero (send-utils/reset-network-amounts-to-zero sender-network-values) + receiver-network-zero (send-utils/reset-network-amounts-to-zero receiver-network-values)] + (testing "if sender-network-value and receiver-network-value are not empty" + (let [expected-db (make-send-structure {:other-props :value + :sender-network-values sender-network-zero + :receiver-network-values receiver-network-zero})] + (reset! rf-db/app-db + (make-send-structure {:other-props :value + :sender-network-values sender-network-values + :receiver-network-values receiver-network-values + :network-links [{:from-chain-id 1 + :to-chain-id 10 + :position-diff 1}]})) + (is (match? expected-db (:db (dispatch [event-id])))))) + (testing "if only receiver-network-value is empty" + (let [expected-db (make-send-structure {:other-props :value + :sender-network-values sender-network-zero})] + (reset! rf-db/app-db + (make-send-structure {:other-props :value + :sender-network-values sender-network-values + :receiver-network-values [] + :network-links [{:from-chain-id 1 + :to-chain-id 10 + :position-diff 1}]})) + (is (match? expected-db (:db (dispatch [event-id])))))) + (testing "if receiver-network-value and sender-network-values are empty" + (let [expected-db (make-send-structure {:other-props :value})] + (reset! rf-db/app-db + (make-send-structure {:other-props :value + :sender-network-values [] + :receiver-network-values [] + :network-links [{:from-chain-id 1 + :to-chain-id 10 + :position-diff 1}]})) + (is (match? expected-db (:db (dispatch [event-id])))))))) + +(h/deftest-event :wallet/select-send-address + [event-id dispatch] + (let [address "eth:arb1:0x707f635951193ddafbb40971a0fcaab8a6415160" + to-address "0x707f635951193ddafbb40971a0fcaab8a6415160" + recipient {:type :saved-address + :label "0x70...160"} + stack-id :screen/wallet.select-address + start-flow? false + tx-type :tx/collectible-erc-721] + (testing "testing when collectible balance is more than 1" + (let [collectible (collectible-with-balance 2) + testnet-enabled? false + expected-result {:db (make-send-structure {:other-props :value + :recipient recipient + :to-address to-address + :tx-type tx-type + :collectible collectible}) + :fx [nil + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id :wallet-send-flow}]]]}] + (reset! rf-db/app-db + {:wallet {:current-viewing-account-address "0x01" + :ui {:send {:other-props :value + :tx-type tx-type + :collectible collectible}}} + :profile/profile {:test-networks-enabled? testnet-enabled?}}) + (is (match? expected-result + (dispatch [event-id + {:address address + :recipient recipient + :stack-id stack-id + :start-flow? start-flow?}]))))) + (testing "testing when collectible balance is 1" + (let [collectible (collectible-with-balance 1) + testnet-enabled? false + expected-result {:db (make-send-structure {:other-props :value + :recipient recipient + :to-address to-address + :tx-type tx-type + :collectible collectible}) + :fx [[:dispatch [:wallet/start-get-suggested-routes {:amount 1}]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id :wallet-send-flow}]]]}] + (reset! rf-db/app-db + {:wallet {:current-viewing-account-address "0x01" + :ui {:send {:other-props :value + :tx-type tx-type + :collectible collectible}}} + :profile/profile {:test-networks-enabled? testnet-enabled?}}) + (is (match? expected-result + (dispatch [event-id + {:address address + :recipient recipient + :stack-id stack-id + :start-flow? start-flow?}]))))))) + +(h/deftest-event :wallet/suggested-routes-success + [event-id dispatch] + (let [timestamp :timestamp + suggested-routes {:Best + [{:From {:isTest false + :chainName "Arbitrum" + :chainId 42161} + :AmountInLocked false + :AmountIn "0x5af3107a4000" + :MaxAmountIn "0x4f7920c6831d6" + :GasFees {:gasPrice "0.01" + :baseFee "0.008750001" + :eip1559Enabled true} + :BridgeName "Transfer" + :AmountOut "0x5af3107a4000" + :To {:isTest false + :chainName "Arbitrum" + :chainId 42161} + :Cost "0.006539438247064285301" + :GasAmount 108197}] + :Candidates + [{:From {:isTest false + :chainName "Arbitrum" + :chainId 42161} + :AmountInLocked false + :AmountIn "0x5af3107a4000" + :MaxAmountIn "0x4f7920c6831d6" + :GasFees {:gasPrice "0.01" + :baseFee "0.008750001" + :eip1559Enabled true} + :BridgeName "Transfer" + :AmountOut "0x5af3107a4000" + :To {:isTest false + :chainName "Arbitrum" + :chainId 42161} + :Cost "0.006539438247064285301" + :GasAmount 108197} + {:From {:isTest false + :chainName "Ethereum" + :chainId 1} + :AmountInLocked false + :AmountIn "0x0" + :MaxAmountIn "0x245aa392272e6" + :GasFees {:gasPrice "1.01" + :baseFee "1.008750001" + :eip1559Enabled true} + :BridgeName "Transfer" + :AmountOut "0x0" + :To {:isTest false + :chainName "Arbitrum" + :chainId 42161} + :Cost "1.906539438247064285301" + :GasAmount 23487}] + :NativeChainTokenPrice 123 + :TokenPrice 123} + suggested-routes-data suggested-routes + chosen-route (:best suggested-routes-data) + token-symbol "ETH" + token {:symbol "ETH" + :name "Ether" + :networks #{{:chain-id 421614} + {:chain-id 11155420} + {:chain-id 11155111}}} + routes-available? (pos? (count chosen-route)) + sender-network-values [1 10] + receiver-network-values [1 10] + receiver-networks [1 10 421614] + token-decimals (if (collectible-with-balance 1) 0 (:decimals token)) + native-token? (and token (= token-symbol "ETH")) + from-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route + :token-decimals + token-decimals + :native-token? + native-token? + :receiver? false}) + to-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route + :token-decimals + token-decimals + :native-token? + native-token? + :receiver? true}) + to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain) + tx-type :tx/collectible-erc-1155 + from-network-values-for-ui (send-utils/network-values-for-ui from-network-amounts-by-chain) + sender-possible-chain-ids (mapv :chain-id sender-network-values) + receiver-network-values (if routes-available? + (send-utils/network-amounts to-network-values-for-ui) + (send-utils/reset-loading-network-amounts-to-zero + receiver-network-values)) + sender-network-values (if routes-available? + (send-utils/network-amounts + (if (= tx-type :tx/bridge) + from-network-values-for-ui + (send-utils/add-zero-values-to-network-values + from-network-values-for-ui + sender-possible-chain-ids))) + (send-utils/reset-loading-network-amounts-to-zero + sender-network-values)) + expected-db (make-send-structure + {:other-props :value + :suggested-routes suggested-routes-data + :route chosen-route + :token token + :suggested-routes-call-timestamp timestamp + :collectible (collectible-with-balance 1) + :token-display-name token-symbol + :receiver-networks receiver-networks + :tx-type tx-type + :from-values-by-chain from-network-values-for-ui + :to-values-by-chain to-network-values-for-ui + :sender-network-values sender-network-values + :receiver-network-values receiver-network-values + :network-links (when routes-available? + (send-utils/network-links + chosen-route + sender-network-values + receiver-network-values)) + :loading-suggested-routes? false})] + (reset! rf-db/app-db + (make-send-structure {:other-props :value + :suggested-routes-call-timestamp timestamp + :token token + :collectible (collectible-with-balance 1) + :token-display-name token-symbol + :receiver-networks receiver-networks + :receiver-network-values [1 10] + :sender-network-values [1 10] + :tx-type tx-type})) + (is (match? expected-db (:db (dispatch [event-id suggested-routes timestamp])))))) + +(h/deftest-event :wallet/select-from-account + [event-id dispatch] + (let [stack-id :screen/stack + start-flow? false + address "0x01" + network {:chain-id 1}] + (testing "when tx-type is :tx/bridge and token-symbol is nil" + (let [flow-id :wallet-bridge-flow + tx-type :tx/bridge + expected-result {:db (make-send-structure {:to-address address + :tx-type tx-type}) + :fx [[:dispatch [:wallet/switch-current-viewing-account address]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id flow-id}]]]}] + (reset! rf-db/app-db (make-send-structure {:tx-type tx-type})) + (is (match? expected-result + (dispatch [event-id + {:address address + :stack-id stack-id + :start-flow? start-flow?}]))))) + (testing "when tx-type is :tx/bridge, network is selected and token-symbol is nil" + (let [flow-id :wallet-bridge-flow + tx-type :tx/bridge + expected-result {:db (make-send-structure {:to-address address + :tx-type tx-type}) + :fx [[:dispatch [:wallet/switch-current-viewing-account address]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id flow-id}]]]}] + (reset! rf-db/app-db (make-send-structure {:tx-type tx-type})) + (is (match? expected-result + (dispatch [event-id + {:address address + :stack-id stack-id + :network network + :start-flow? start-flow?}]))))) + (testing "when tx-type is not :tx/bridge and token-symbol is nil" + (let [flow-id :wallet-send-flow + tx-type :tx/collectible-erc-721 + expected-result {:db (make-send-structure {:tx-type tx-type}) + :fx [[:dispatch [:wallet/switch-current-viewing-account address]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id flow-id}]]]}] + (reset! rf-db/app-db (make-send-structure {:tx-type tx-type})) + (is (match? expected-result + (dispatch [event-id + {:address address + :stack-id stack-id + :network network + :start-flow? start-flow?}]))))) + (testing "when tx-type is :tx/bridge and token-symbol is not nil" + (let [tx-type :tx/bridge + expected-result {:db (make-send-structure {:to-address address + :tx-type tx-type}) + :fx [[:dispatch [:dismiss-modal :screen/wallet.select-from]] + [:dispatch [:wallet/switch-current-viewing-account address]] + [:dispatch [:show-bottom-sheet {:content (m/pred fn?)}]]]}] + (reset! rf-db/app-db (make-send-structure {:tx-type tx-type + :token-symbol "ETH"})) + (is (match? expected-result + (dispatch [event-id + {:address address + :stack-id stack-id + :start-flow? start-flow?}]))))) + (testing "when tx-type is not :tx/bridge and token-symbol is not nil" + (let [flow-id :wallet-send-flow + tx-type :tx/collectible-erc-721 + tokens [{:symbol "ETH" + :chain-id 1 + :balances-per-chain {1 {:raw-balance (money/bignumber 100)} + 10 {:raw-balance (money/bignumber 200)} + 42161 {:raw-balance (money/bignumber 300)}} + :decimals 2}] + network-details #{{:chain-id 1} + {:chain-id 10} + {:chain-id 42161}} + expected-result {:db (-> (make-send-structure {:tx-type tx-type + :token-symbol "ETH" + :token (assoc (first tokens) + :networks #{nil} + :total-balance + (money/bignumber + 6))}) + (assoc-in [:wallet :accounts] {address {:tokens tokens}})) + + :fx [[:dispatch [:wallet/switch-current-viewing-account address]] + [:dispatch + [:wallet/wizard-navigate-forward + {:current-screen stack-id + :start-flow? start-flow? + :flow-id flow-id}]]]}] + (reset! rf-db/app-db (-> (make-send-structure {:tx-type tx-type + :token-symbol "ETH"}) + (assoc-in [:wallet :accounts] {address {:tokens tokens}}))) + (is (match? expected-result + (dispatch [event-id + {:address address + :stack-id stack-id + :network network + :start-flow? start-flow? + :netork-details network-details}]))))))) diff --git a/src/status_im/contexts/wallet/send/flow_config.cljs b/src/status_im/contexts/wallet/send/flow_config.cljs new file mode 100644 index 00000000000..0a5b8a8becc --- /dev/null +++ b/src/status_im/contexts/wallet/send/flow_config.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.wallet.send.flow-config + (:require + [status-im.contexts.wallet.db :as db] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.contexts.wallet.send.utils :as send-utils])) + +(defn- collectible-selected? + [db] + (let [{collectible-stored :collectible + tx-type :tx-type} (db/send db)] + (and (some? collectible-stored) + (send-utils/tx-type-collectible? tx-type)))) + +(defn- token-selected? + [db] + (-> db db/send :token some?)) + +(def steps + [{:screen-id :screen/wallet.select-address + :skip-step? (fn [db] (some? (get-in db (conj db-path/send :recipient))))} + {:screen-id :screen/wallet.select-asset + :skip-step? (fn [db] (or (token-selected? db) (collectible-selected? db)))} + {:screen-id :screen/wallet.send-input-amount + :skip-step? (fn [db] + (-> db db/send :tx-type send-utils/tx-type-collectible?))} + {:screen-id :screen/wallet.select-collectible-amount + :skip-step? (fn [db] + (or (not (collectible-selected? db)) + (some? (get-in db (conj db-path/send :amount)))))} + {:screen-id :screen/wallet.transaction-confirmation}]) diff --git a/src/status_im/contexts/wallet/send/from/style.cljs b/src/status_im/contexts/wallet/send/from/style.cljs new file mode 100644 index 00000000000..94ea22b81c3 --- /dev/null +++ b/src/status_im/contexts/wallet/send/from/style.cljs @@ -0,0 +1,7 @@ +(ns status-im.contexts.wallet.send.from.style) + +(def accounts-list + {:padding-bottom 12}) + +(def accounts-list-container + {:padding-horizontal 8}) diff --git a/src/status_im/contexts/wallet/send/from/view.cljs b/src/status_im/contexts/wallet/send/from/view.cljs new file mode 100644 index 00000000000..972d0fb3b23 --- /dev/null +++ b/src/status_im/contexts/wallet/send/from/view.cljs @@ -0,0 +1,88 @@ +(ns status-im.contexts.wallet.send.from.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.collectible.utils :as collectible-utils] + [status-im.contexts.wallet.send.from.style :as style] + [status-im.setup.hot-reload :as hot-reload] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) + +(defn- on-account-press + [address general-flow? collectible-tx? token-selected?] + (when general-flow? + (rf/dispatch [:wallet/clean-selected-token]) + (rf/dispatch [:wallet/clean-selected-collectible])) + (rf/dispatch [:wallet/select-from-account + {:address address + :stack-id :screen/wallet.select-from + :start-flow? (not (or general-flow? collectible-tx? token-selected?))}])) + +(defn- on-close + [] + (rf/dispatch [:wallet/clean-selected-collectible {:ignore-entry-point? true}]) + (rf/dispatch [:wallet/clean-current-viewing-account])) + +(defn- render-fn + [item _ _ {:keys [general-flow? collectible-tx? collectible token]}] + (let [account-address (:address item) + balance (cond + general-flow? 0 + collectible-tx? (collectible-utils/collectible-balance collectible + account-address) + :else (string/replace-first (:asset-pay-balance item) "<" "")) + has-balance? (money/above-zero? balance) + asset-symbol (if collectible-tx? "" (:asset-pay-symbol item)) + asset-value (if collectible-tx? (str balance) (:asset-pay-balance item))] + [quo/account-item + {:type (if has-balance? :tag :default) + :on-press #(on-account-press account-address + general-flow? + collectible-tx? + (and (nil? collectible) (nil? token))) + :state (if (or has-balance? general-flow? (and (nil? collectible) (nil? token))) + :default + :disabled) + :token-props (when-not general-flow? + {:symbol asset-symbol + :value asset-value}) + :account-props item}])) + +(defn view + [] + (let [general-flow? (rf/sub [:wallet/send-general-flow?]) + collectible-tx? (rf/sub [:wallet/send-tx-type-collectible?]) + token-symbol (rf/sub [:wallet/send-token-symbol]) + token (rf/sub [:wallet/token-by-symbol-from-first-available-account-with-balance + token-symbol]) + collectible (rf/sub [:wallet/wallet-send-collectible]) + accounts (if (or general-flow? collectible-tx?) + (rf/sub [:wallet/operable-accounts]) + (rf/sub [:wallet/accounts-with-balances token]))] + (hot-reload/use-safe-unmount on-close) + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:type :no-title + :icon-name :i/close + :on-press events-helper/navigate-back + :margin-top safe-area/top + :background :blur}]} + [quo/page-top + {:title (i18n/label :t/from-label) + :title-accessibility-label :title-label}] + [rn/flat-list + {:style style/accounts-list + :content-container-style style/accounts-list-container + :data accounts + :render-data {:general-flow? general-flow? + :collectible-tx? collectible-tx? + :collectible collectible + :token token} + :render-fn render-fn + :shows-horizontal-scroll-indicator false}]])) diff --git a/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs b/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs new file mode 100644 index 00000000000..e27154a721e --- /dev/null +++ b/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs @@ -0,0 +1,168 @@ +(ns status-im.contexts.wallet.send.input-amount.component-spec + (:require + status-im.contexts.wallet.events + status-im.contexts.wallet.send.events + [status-im.contexts.wallet.send.input-amount.view :as input-amount] + [test-helpers.component :as h] + [utils.debounce :as debounce] + [utils.money :as money] + [utils.re-frame :as rf])) + +(set! rf/dispatch #()) +(set! debounce/debounce-and-dispatch #()) + +(def sub-mocks + {:profile/profile {:currency :usd} + :wallet/filtered-networks [{:source 525 + :short-name "eth" + :network-name :mainnet + :chain-id 1 + :related-chain-id 5}] + :wallet/network-filter? false + :wallet/show-new-chain-indicator? false + :wallet/current-viewing-account-address "0x1" + :wallet/current-viewing-account {:path "m/44'/60'/0'/0/1" + :emoji "💎" + :key-uid "0x2f5ea39" + :address "0x1" + :wallet false + :name "Account One" + :type :generated + :watch-only? false + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :purple + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :network-preferences-names #{:mainnet :arbitrum + :optimism} + :position 1 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false} + :wallet/current-viewing-account-color :purple + :wallet/wallet-send-enough-assets? true + :wallet/wallet-send-token {:symbol "ETH" + :networks [{:source 879 + :short-name "eth" + :network-name :mainnet + :abbreviated-name + "Eth." + :full-name "Ethereum" + :chain-id 1 + :related-chain-id 1 + :layer 1}] + :balances-per-chain {1 {:raw-balance + (money/bignumber + "2500") + :has-error false}} + :total-balance 100 + :available-balance 100} + :wallet/wallet-send-loading-suggested-routes? false + :wallet/wallet-send-route [{:from {:chainid 1 + :native-currency-symbol "ETH"} + :to {:chain-id 1 + :native-currency-symbol "ETH"} + :gas-amount "23487" + :gas-fees {:base-fee "32.325296406" + :eip1559-enabled true}}] + :wallet/wallet-send-suggested-routes nil + :wallet/wallet-send-receiver-networks [1] + :view-id :screen/wallet.send-input-amount + :wallet/wallet-send-to-address "0x04371e2d9d66b82f056bc128064" + :profile/currency-symbol "$" + :profile/currency :usd + :wallet/token-by-symbol {:symbol "ETH" + :total-balance 100 + :available-balance 100 + :balances-per-chain {1 {:raw-balance + (money/bignumber + "2500") + :has-error false}}} + :wallet/wallet-send-from-values-by-chain {1 (money/bignumber "250")} + :wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")} + :wallet/wallet-send-sender-network-values nil + :wallet/wallet-send-receiver-network-values nil + :wallet/wallet-send-network-links nil + :wallet/send-amount nil + :wallet/wallet-send-tx-type :tx/send + :wallet/wallet-send-fee-fiat-formatted "$5,00" + :wallet/sending-collectible? false + :wallet/selected-keypair-keycard? false + :wallet/send-total-amount-formatted "250 ETH" + :wallet/total-amount (money/bignumber "250") + :wallet/prices-per-token {:ETH {:usd 10}} + :wallet/bridge-to-network-details nil + :wallet/send-amount-fixed "" + :wallet/send-display-token-decimals 5 + :wallet/send-network {:chain-id 1} + :wallet/send-native-token {:symbol "ETH" + :decimals 18 + :name "Ethereum" + :total-balance 100 + :address "0x"}}) + +(h/describe "Send > input amount screen" + (h/setup-restorable-re-frame) + + (h/test "Default render" + (h/setup-subs (assoc sub-mocks :wallet/send-display-token-decimals 2)) + (h/render-with-theme-provider [input-amount/view + {:limit-crypto (money/bignumber 250) + :initial-crypto-currency? false}]) + (h/is-truthy (h/get-by-text "0")) + (h/is-truthy (h/get-by-text "USD")) + (h/is-truthy (h/get-by-text "0 ETH")) + (h/is-truthy (h/get-by-label-text :container)) + (h/is-disabled (h/get-by-label-text :button-one))) + + (h/test "Fill token input and confirm" + (h/setup-subs (assoc sub-mocks :wallet/send-display-token-decimals 10)) + + (let [on-confirm (h/mock-fn)] + (h/render-with-theme-provider [input-amount/view + {:limit-crypto (money/bignumber 1000) + :on-confirm on-confirm + :initial-crypto-currency? true}]) + + (h/fire-event :press (h/query-by-label-text :keyboard-key-1)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-2)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-3)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-.)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-4)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-5)) + + + (-> (h/wait-for #(h/get-by-text "$1234.50")) + (.then (fn [] + (let [btn (h/get-by-label-text :button-one)] + (h/is-truthy btn) + (h/fire-event :press btn) + (h/was-called on-confirm))))))) + + (h/test "Try to fill more than limit" + (h/setup-subs (assoc sub-mocks :wallet/send-display-token-decimals 1)) + (h/render-with-theme-provider [input-amount/view + {:limit-crypto (money/bignumber 1)}]) + + (h/fire-event :press (h/query-by-label-text :keyboard-key-2)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-9)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-5)) + + (h/is-truthy (h/get-by-label-text :container-error))) + + (h/test "Switch from crypto to fiat and check limit" + (h/setup-subs (assoc sub-mocks :wallet/send-display-token-decimals 1)) + (h/render-with-theme-provider [input-amount/view + {:limit-crypto (money/bignumber 10) + :on-confirm #()}]) + + (h/fire-event :press (h/query-by-label-text :keyboard-key-9)) + (h/fire-event :press (h/query-by-label-text :keyboard-key-9)) + (h/is-truthy (h/get-by-label-text :container-error)) + (h/fire-event :press (h/query-by-label-text :reorder)) + (h/is-truthy #(h/get-by-text "Max: $100.00")))) + diff --git a/src/status_im/contexts/wallet/send/input_amount/estimated_fees.cljs b/src/status_im/contexts/wallet/send/input_amount/estimated_fees.cljs new file mode 100644 index 00000000000..899ec49df05 --- /dev/null +++ b/src/status_im/contexts/wallet/send/input_amount/estimated_fees.cljs @@ -0,0 +1,70 @@ +(ns status-im.contexts.wallet.send.input-amount.estimated-fees + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.not-implemented :as not-implemented] + [status-im.contexts.wallet.send.input-amount.style :as style] + [status-im.feature-flags :as ff] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- bonder-fee-info-sheet + [] + [:<> + [quo/drawer-top + {:title (i18n/label :t/understanding-bonder-fees-title)}] + [rn/view {:style {:padding-horizontal 20}} + [quo/text + {:weight :regular + :size :paragraph-2} + (i18n/label :t/understanding-bonder-fees-description)]]]) + +(defn- show-bonder-fee-info + [] + (rf/dispatch + [:show-bottom-sheet {:content bonder-fee-info-sheet}])) + +(defn- received-amount + [{:keys [loading-routes?]}] + (let [amount (rf/sub [:wallet/send-total-amount-formatted]) + tx-type (rf/sub [:wallet/wallet-send-tx-type]) + {:keys [full-name]} (rf/sub [:wallet/bridge-to-network-details])] + (when (= tx-type :tx/bridge) + [quo/data-item + {:container-style style/amount-data-item + :status (if loading-routes? :loading :default) + :size :small + :title-icon :i/info + :title (i18n/label :t/bridged-to + {:network full-name}) + :subtitle amount + :on-press show-bonder-fee-info}]))) + +(defn view + [{:keys [not-enough-asset? loading-routes? fees]}] + (let [theme (quo.context/use-theme)] + [rn/view {:style style/estimated-fees-container} + (when (ff/enabled? ::ff/wallet.advanced-sending) + [rn/view {:style style/estimated-fees-content-container} + [quo/button + {:icon-only? true + :type :outline + :size 32 + :inner-style {:opacity 1} + :accessibility-label :advanced-button + :disabled? loading-routes? + :on-press not-implemented/alert} + :i/advanced]]) + [quo/data-item + (cond-> {:container-style style/fees-data-item + :status (if loading-routes? :loading :default) + :size :small + :title (i18n/label :t/max-fees) + :subtitle fees} + not-enough-asset? (assoc :subtitle-color + (colors/theme-colors colors/danger-50 + colors/danger-60 + theme)))] + [received-amount {:loading-routes? loading-routes?}]])) diff --git a/src/status_im/contexts/wallet/send/input_amount/style.cljs b/src/status_im/contexts/wallet/send/input_amount/style.cljs new file mode 100644 index 00000000000..3bc1c29bd87 --- /dev/null +++ b/src/status_im/contexts/wallet/send/input_amount/style.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.wallet.send.input-amount.style) + +(def screen + {:flex 1}) + +(def input-container + {:padding-top 12 + :padding-bottom 0}) + +(defn keyboard-container + [bottom] + {:padding-bottom bottom}) + +(def estimated-fees-container + {:height 48 + :width "100%" + :flex-direction :row + :align-items :center + :padding-horizontal 20 + :padding-top 8}) + +(def estimated-fees-content-container + {:align-items :center + :height 40}) + +(def fees-data-item + {:flex 1 + :height 40 + :background-color :transparent}) + +(def amount-data-item + {:flex 1 + :height 40 + :background-color :transparent}) + +(def no-routes-found-container + {:height 40 + :width "100%" + :align-items :center}) diff --git a/src/status_im/contexts/wallet/send/input_amount/view.cljs b/src/status_im/contexts/wallet/send/input_amount/view.cljs new file mode 100644 index 00000000000..51f25ee5919 --- /dev/null +++ b/src/status_im/contexts/wallet/send/input_amount/view.cljs @@ -0,0 +1,300 @@ +(ns status-im.contexts.wallet.send.input-amount.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.controlled-input.utils :as controlled-input] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.common.asset-list.view :as asset-list] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.send.input-amount.estimated-fees :as estimated-fees] + [status-im.contexts.wallet.send.input-amount.style :as style] + [status-im.contexts.wallet.send.routes.view :as routes] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [status-im.setup.hot-reload :as hot-reload] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) + +(defn select-asset-bottom-sheet + [clear-input!] + (let [{preselected-token-symbol :symbol} (rf/sub [:wallet/wallet-send-token]) + network (rf/sub [:wallet/send-network]) + chain-id (:chain-id network)] + [:<> ;; Need to be a `:<>` to keep `asset-list` scrollable. + [quo/drawer-top + {:title (i18n/label :t/select-token) + :container-style {:padding-bottom 8}}] + [asset-list/view + {:content-container-style {:padding-horizontal 8 + :padding-bottom 8} + :preselected-token-symbol preselected-token-symbol + :chain-ids [chain-id] + :on-token-press (fn [token] + (rf/dispatch [:wallet/edit-token-to-send token]) + (clear-input!))}]])) + +(defn- no-routes-found + [] + [rn/view {:style style/no-routes-found-container} + [quo/info-message + {:status :error + :icon :i/alert + :size :default + :container-style {:margin-top 15}} + (i18n/label :t/no-routes-found)]]) + +(defn- not-enough-asset + [native-token-symbol] + [quo/alert-banner + {:action? true + :text (i18n/label :t/not-enough-assets-to-pay-gas-fees) + :button-text (i18n/label :t/add-token {:token native-token-symbol}) + :on-button-press #(rf/dispatch [:show-bottom-sheet + {:content buy-token/view}])}]) + +(defn- fetch-routes + [{:keys [amount bounce-duration-ms token valid-input? reset-amounts-to-zero?]}] + (cond + valid-input? + (debounce/debounce-and-dispatch + [:wallet/start-get-suggested-routes + {:amount amount + :updated-token token}] + bounce-duration-ms) + + reset-amounts-to-zero? + (rf/dispatch [:wallet/reset-network-amounts-to-zero]) + + :else (rf/dispatch [:wallet/stop-and-clean-suggested-routes]))) + +(defn- insufficient-asset-amount? + [{:keys [limit-exceeded? enough-assets?]}] + (or limit-exceeded? + (false? enough-assets?))) + +(defn view + ;; crypto-decimals, limit-crypto and initial-crypto-currency? args are needed + ;; for component tests only + [{default-on-confirm :on-confirm + default-limit-crypto :limit-crypto + on-navigate-back :on-navigate-back + button-one-label :button-one-label + button-one-props :button-one-props + current-screen-id :current-screen-id + initial-crypto-currency? :initial-crypto-currency? + :or {initial-crypto-currency? true}}] + (let [view-id (rf/sub [:view-id]) + {fiat-currency :currency} (rf/sub [:profile/profile]) + currency (rf/sub [:profile/currency]) + {token-symbol :symbol + :as token} (rf/sub [:wallet/wallet-send-token]) + network (rf/sub [:wallet/send-network]) + {:keys [total-balance] + :as token-by-symbol} (rf/sub [:wallet/token-by-symbol + (str token-symbol) + [(:chain-id network)]]) + currency-symbol (rf/sub [:profile/currency-symbol]) + loading-routes? (rf/sub [:wallet/wallet-send-loading-suggested-routes?]) + route (rf/sub [:wallet/wallet-send-route]) + current-address (rf/sub [:wallet/current-viewing-account-address]) + current-color (rf/sub [:wallet/current-viewing-account-color]) + enough-assets? (rf/sub [:wallet/wallet-send-enough-assets?]) + native-token (rf/sub [:wallet/send-native-token]) + suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes]) + tx-type (rf/sub [:wallet/wallet-send-tx-type]) + token-decimals (rf/sub [:wallet/send-display-token-decimals]) + prices-per-token (rf/sub [:wallet/prices-per-token]) + [crypto-currency? + set-crypto-currency] (rn/use-state initial-crypto-currency?) + [input-state set-input-state] (rn/use-state controlled-input/init-state) + active-screen? (= view-id current-screen-id) + bottom safe-area/bottom + token-balance (or default-limit-crypto total-balance) + usd-conversion-rate (utils/token-usd-price token prices-per-token) + conversion-rate (utils/token-price-by-symbol prices-per-token + token-symbol + currency) + clear-input! #(set-input-state controlled-input/delete-all) + max-limit (if crypto-currency? + (utils/cut-crypto-decimals-to-fit-usd-cents + token-balance + usd-conversion-rate) + (utils/cut-fiat-balance-to-two-decimals + (money/crypto->fiat token-balance conversion-rate))) + input-value (controlled-input/input-value input-state) + valid-input? (not (or (controlled-input/empty-value? input-state) + (controlled-input/input-error input-state))) + amount-in-crypto (if crypto-currency? + input-value + (rf/sub [:wallet/send-amount-fixed + (/ input-value conversion-rate)])) + routes-request-uuid (when suggested-routes + (:uuid suggested-routes)) + no-routes-found? (and + routes-request-uuid + (empty? route) + (not loading-routes?)) + input-error (controlled-input/input-error input-state) + limit-exceeded? (controlled-input/upper-limit-exceeded? input-state) + not-enough-asset? (insufficient-asset-amount? + {:enough-assets? enough-assets? + :limit-exceeded? limit-exceeded?}) + should-try-again? (and (not limit-exceeded?) + no-routes-found? + (not not-enough-asset?)) + show-no-routes? (and (or no-routes-found? limit-exceeded?) + (not not-enough-asset?)) + confirm-disabled? (or (nil? route) + (empty? route) + (not valid-input?)) + fee-formatted (when (or (not confirm-disabled?) not-enough-asset?) + (rf/sub [:wallet/wallet-send-fee-fiat-formatted])) + handle-on-confirm (fn [amount] + (rf/dispatch [:wallet/set-token-amount-to-send + {:amount amount + :stack-id current-screen-id}])) + on-confirm (or default-on-confirm handle-on-confirm) + show-select-asset-sheet #(rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [select-asset-bottom-sheet + clear-input!])}]) + request-fetch-routes (fn [bounce-duration-ms] + (fetch-routes + {:amount amount-in-crypto + :valid-input? valid-input? + :bounce-duration-ms bounce-duration-ms + :token token + :reset-amounts-to-zero? (and limit-exceeded? + (some? route))})) + swap-between-fiat-and-crypto (fn [] + (if crypto-currency? + (set-input-state + #(controlled-input/->fiat % conversion-rate)) + (set-input-state + #(controlled-input/->crypto % conversion-rate))) + (set-crypto-currency (not crypto-currency?)))] + (rn/use-effect + (fn [] + (when active-screen? + (rn/dismiss-keyboard!))) + [active-screen?]) + (rn/use-mount + (fn [] + (let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!)) + app-keyboard-listener (.addEventListener rn/app-state "change" dismiss-keyboard-fn)] + #(.remove app-keyboard-listener)))) + (hot-reload/use-safe-unmount on-navigate-back) + (rn/use-effect + (fn [] + (set-input-state #(controlled-input/set-upper-limit % max-limit))) + [max-limit]) + (rn/use-effect + (fn [] + (when input-error + (rf/dispatch [:wallet/stop-get-suggested-routes]) + (debounce/clear-all))) + [input-error]) + (rn/use-effect + (fn [] + (clear-input!) + (rf/dispatch [:wallet/stop-and-clean-suggested-routes])) + [current-address]) + (rn/use-effect + (fn [] + (when active-screen? + (request-fetch-routes 2000))) + [amount-in-crypto valid-input?]) + [rn/view + {:style style/screen + :accessibility-label (str "container" + (when (controlled-input/input-error input-state) "-error"))} + [account-switcher/view + {:icon-name :i/arrow-left + :on-press #(rf/dispatch [:navigate-back]) + :switcher-type :select-account}] + [quo/token-input + {:container-style style/input-container + :token-symbol token-symbol + :value input-value + :on-swap swap-between-fiat-and-crypto + :on-token-press show-select-asset-sheet + :error? (controlled-input/input-error input-state) + :currency-symbol (if crypto-currency? token-symbol fiat-currency) + :converted-value (if crypto-currency? + (utils/prettify-balance + currency-symbol + (money/crypto->fiat input-value + conversion-rate)) + (utils/prettify-crypto-balance + (or (clj->js token-symbol) "") + (money/fiat->crypto input-value + conversion-rate) + conversion-rate)) + :hint-component [quo/network-tags + {:networks (seq [network]) + :title (i18n/label + :t/send-limit + {:limit (if crypto-currency? + (utils/prettify-crypto-balance + (or (clj->js token-symbol) "") + (controlled-input/upper-limit-bn input-state) + conversion-rate) + (utils/prettify-balance currency-symbol + (controlled-input/upper-limit-bn + input-state)))}) + :status (when (controlled-input/input-error input-state) :error)}]}] + (if (= tx-type :tx/bridge) + [routes/view] + [rn/view {:style {:flex 1}}]) + (when not-enough-asset? + [not-enough-asset (:symbol native-token)]) + (when (or loading-routes? route) + [estimated-fees/view + {:not-enough-asset? not-enough-asset? + :loading-routes? loading-routes? + :fees fee-formatted}]) + (when show-no-routes? + [no-routes-found]) + [quo/bottom-actions + {:actions :one-action + :button-one-label (if should-try-again? + (i18n/label :t/try-again) + button-one-label) + :button-one-props (merge (when-not should-try-again? + button-one-props) + {:disabled? (or not-enough-asset? + loading-routes? + (and (not should-try-again?) + confirm-disabled?)) + :on-press (if should-try-again? + #(rf/dispatch [:wallet/start-get-suggested-routes + {:amount amount-in-crypto + :updated-token token-by-symbol}]) + #(on-confirm amount-in-crypto)) + :customization-color current-color} + (when should-try-again? + {:type :grey}))}] + [quo/numbered-keyboard + {:container-style (style/keyboard-container bottom) + :left-action :dot + :delete-key? true + :on-press (fn [c] + (let [new-text (str input-value c) + max-decimals (if crypto-currency? token-decimals 2) + regex-pattern (str "^\\d*\\.?\\d{0," max-decimals "}$") + regex (re-pattern regex-pattern)] + (when (re-matches regex new-text) + (debounce/clear-all) + (set-input-state #(controlled-input/add-character % c))))) + :on-delete (fn [] + (debounce/clear-all) + (set-input-state controlled-input/delete-last) + (rf/dispatch-sync [:wallet/stop-and-clean-suggested-routes])) + :on-long-press-delete (fn [] + (debounce/clear-all) + (set-input-state controlled-input/delete-all) + (rf/dispatch-sync [:wallet/stop-and-clean-suggested-routes]))}]])) diff --git a/src/status_im/contexts/wallet/send/routes/style.cljs b/src/status_im/contexts/wallet/send/routes/style.cljs new file mode 100644 index 00000000000..56bd62b4588 --- /dev/null +++ b/src/status_im/contexts/wallet/send/routes/style.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.wallet.send.routes.style) + +(def routes-container + {:padding-horizontal 20 + :flex-grow 1 + :padding-vertical 16 + :width "100%"}) + +(def routes-header-container + {:flex-direction :row + :justify-content :space-between}) + +(def routes-inner-container + {:flex-direction :row + :justify-content :space-between}) + +(def section-label-right + {:width 135}) + +(def section-label-left + {:width 136}) + +(def network-links-container + {:margin-horizontal -1.5 + :margin-top 7.5 + :z-index 3 + :flex 1}) + +(defn network-link-container + [margin-top inverted?] + (cond-> {:position :absolute + :left 0 + :right 0 + :top margin-top} + inverted? + (assoc :transform [{:scaleY -1}]))) diff --git a/src/status_im/contexts/wallet/send/routes/view.cljs b/src/status_im/contexts/wallet/send/routes/view.cljs new file mode 100644 index 00000000000..9252af424b3 --- /dev/null +++ b/src/status_im/contexts/wallet/send/routes/view.cljs @@ -0,0 +1,107 @@ +(ns status-im.contexts.wallet.send.routes.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.common.utils :as common-utils] + [status-im.contexts.wallet.send.routes.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def row-height 44) +(def space-between-rows 11) +(def network-link-linear-height 10) +(def network-link-1x-height 56) +(def network-link-2x-height 111) + +(defn render-network-values + [{:keys [network-values token-symbol token-decimals receiver?]}] + [rn/view + (doall + (map-indexed (fn [index + {chain-id :chain-id + network-value-type :type + total-amount :total-amount}] + (let [network-name (rf/sub [:wallet/network-name-from-chain-id chain-id]) + amount-formatted (-> total-amount + (common-utils/sanitized-token-amount-to-display + token-decimals) + (str " " token-symbol))] + [rn/view + {:key (str (if receiver? "to" "from") "-" chain-id) + :style {:margin-top (if (pos? index) 11 7.5)}} + [quo/network-bridge + {:amount amount-formatted + :network network-name + :status network-value-type}]])) + network-values))]) + +(defn render-network-links + [{:keys [network-links sender-network-values]}] + [rn/view {:style style/network-links-container} + (map + (fn [{:keys [from-chain-id to-chain-id position-diff]}] + (let [position-diff-absolute (js/Math.abs position-diff) + shape (case position-diff-absolute + 0 :linear + 1 :1x + 2 :2x) + height (case position-diff-absolute + 0 network-link-linear-height + 1 network-link-1x-height + 2 network-link-2x-height) + inverted? (neg? position-diff) + source (rf/sub [:wallet/network-name-from-chain-id from-chain-id]) + destination (rf/sub [:wallet/network-name-from-chain-id to-chain-id]) + from-chain-id-index (first (keep-indexed #(when (= from-chain-id (:chain-id %2)) %1) + sender-network-values)) + base-margin-top (* (+ row-height space-between-rows) + from-chain-id-index) + margin-top (if (zero? position-diff) + (+ base-margin-top + (- (/ row-height 2) (/ height 2))) + (+ base-margin-top + (- (/ row-height 2) height) + (if inverted? height 0)))] + [rn/view + {:key (str "from-" from-chain-id "-to-" to-chain-id) + :style (style/network-link-container margin-top inverted?)} + [rn/view {:style {:flex 1}} + [quo/network-link + {:shape shape + :source source + :destination destination}]]])) + network-links)]) + +(defn view + [] + (let [token-symbol (rf/sub [:wallet/wallet-send-token-symbol]) + sender-network-values (rf/sub [:wallet/wallet-send-sender-network-values]) + receiver-network-values (rf/sub [:wallet/wallet-send-receiver-network-values]) + network-links (rf/sub [:wallet/wallet-send-network-links]) + token-decimals (rf/sub [:wallet/send-display-token-decimals]) + show-routes? (not-empty sender-network-values)] + [rn/scroll-view {:content-container-style style/routes-container} + (when show-routes? + [rn/view {:style style/routes-header-container} + [quo/section-label + {:section (i18n/label :t/from-label) + :container-style style/section-label-left}] + [quo/section-label + {:section (i18n/label :t/to-label) + :container-style style/section-label-right}]]) + [rn/view {:style style/routes-inner-container} + [render-network-values + {:token-symbol token-symbol + :network-values sender-network-values + :token-decimals token-decimals + :receiver? false}] + [render-network-links + {:network-links network-links + :sender-network-values sender-network-values}] + [render-network-values + {:token-symbol token-symbol + :network-values receiver-network-values + :token-decimals token-decimals + :receiver? true}]]])) + diff --git a/src/status_im/contexts/wallet/send/select_address/style.cljs b/src/status_im/contexts/wallet/send/select_address/style.cljs new file mode 100644 index 00000000000..03445e643f3 --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_address/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.wallet.send.select-address.style) + +(def tabs + {:padding-top 20 + :padding-bottom 12}) + +(def tabs-content + {:padding-left 20 + :padding-right 8}) + +(def network-text-container + {:padding-horizontal 12 + :padding-top 4}) diff --git a/src/status_im/contexts/wallet/send/select_address/tabs/style.cljs b/src/status_im/contexts/wallet/send/select_address/tabs/style.cljs new file mode 100644 index 00000000000..f0a879d2055 --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_address/tabs/style.cljs @@ -0,0 +1,10 @@ +(ns status-im.contexts.wallet.send.select-address.tabs.style) + +(def empty-container-style + {:justify-content :center + :flex 1 + :margin-bottom 44}) + +(def my-accounts-container + {:padding-left 8 + :padding-right 8}) diff --git a/src/status_im/contexts/wallet/send/select_address/tabs/view.cljs b/src/status_im/contexts/wallet/send/select_address/tabs/view.cljs new file mode 100644 index 00000000000..4898029f50a --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_address/tabs/view.cljs @@ -0,0 +1,120 @@ +(ns status-im.contexts.wallet.send.select-address.tabs.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.resources :as resources] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.send.select-address.tabs.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn other-account-item + [{:keys [address color emoji] + account-name :name + :as account}] + [quo/account-item + {:account-props (assoc account :customization-color color) + :on-press (fn [] + (rf/dispatch [:wallet/select-send-address + {:address address + :recipient {:recipient-type :account + :label account-name + :customization-color color + :emoji emoji} + :stack-id :screen/wallet.select-address}]))}]) + +(defn- my-accounts + [theme] + (let [other-accounts (rf/sub [:wallet/accounts-without-current-viewing-account])] + (if (zero? (count other-accounts)) + [quo/empty-state + {:title (i18n/label :t/no-other-accounts) + :description (i18n/label :t/here-is-a-cat-in-a-box-instead) + :image (resources/get-themed-image :cat-in-box theme) + :container-style style/empty-container-style}] + [rn/view {:style style/my-accounts-container} + (doall + (for [{:keys [address] :as account} other-accounts] + ^{:key (str address)} [other-account-item account]))]))) + +(defn- recent-transactions + [theme] + (let [recent-recipients (rf/sub [:wallet/recent-recipients])] + (if (zero? (count recent-recipients)) + [quo/empty-state + {:title (i18n/label :t/no-recent-transactions) + :description (i18n/label :t/make-one-it-is-easy-we-promise) + :image (resources/get-themed-image :angry-man theme) + :container-style style/empty-container-style}] + (into [rn/view {:style style/my-accounts-container}] + (map (fn [address] + [quo/address + {:address address + :on-press #(rf/dispatch [:wallet/select-send-address + {:address address + :recipient {:label (utils/get-shortened-address + address) + :recipient-type :address} + :stack-id :screen/wallet.select-address}])}])) + recent-recipients)))) + +(defn- saved-address + [{:keys [name address customization-color ens? ens]}] + (let [on-press-saved-address (rn/use-callback + #(rf/dispatch + [:wallet/select-send-address + {:address address + :recipient {:label name + :customization-color customization-color + :recipient-type :saved-address} + :stack-id :screen/wallet.select-address}]) + [address])] + [quo/saved-address + {:user-props {:name name + :address address + :ens (when ens? ens) + :customization-color customization-color} + :container-style {:margin-horizontal 8} + :on-press on-press-saved-address}])) + +(defn- saved-addresses + [theme] + (let [group-saved-addresses (rf/sub [:wallet/grouped-saved-addresses]) + section-header (rn/use-callback + (fn [{:keys [title index]}] + [quo/divider-label + {:tight? true + :container-style (when (pos? index) {:margin-top 8})} + title])) + empty-state-component (rn/use-memo + (fn [] + [quo/empty-state + {:title (i18n/label :t/no-saved-addresses) + :description (i18n/label + :t/you-like-to-type-43-characters) + :container-style style/empty-container-style + :image (resources/get-themed-image :sweating-man + theme)}]) + [theme])] + [rn/section-list + {:key-fn :title + :shows-vertical-scroll-indicator false + :render-section-header-fn section-header + :sections group-saved-addresses + :render-fn saved-address + :content-container-style {:flex 1} + :empty-component empty-state-component}])) + +(defn view + [{:keys [selected-tab]}] + (let [theme (quo.context/use-theme)] + (case selected-tab + :tab/recent [recent-transactions theme] + :tab/saved [saved-addresses theme] + :tab/contacts [quo/empty-state + {:title (i18n/label :t/no-contacts) + :description (i18n/label :t/no-contacts-description) + :image (resources/get-themed-image :no-contacts theme) + :container-style style/empty-container-style}] + :tab/my-accounts [my-accounts theme]))) diff --git a/src/status_im/contexts/wallet/send/select_address/view.cljs b/src/status_im/contexts/wallet/send/select_address/view.cljs new file mode 100644 index 00000000000..db1399b85d3 --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_address/view.cljs @@ -0,0 +1,233 @@ +(ns status-im.contexts.wallet.send.select-address.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.item-types :as types] + [status-im.contexts.wallet.send.select-address.style :as style] + [status-im.contexts.wallet.send.select-address.tabs.view :as tabs] + [status-im.feature-flags :as ff] + [status-im.setup.hot-reload :as hot-reload] + [utils.address :as utils-address] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:private tabs-data + [{:id :tab/recent :label (i18n/label :t/recent) :accessibility-label :recent-tab} + {:id :tab/saved :label (i18n/label :t/saved) :accessibility-label :saved-tab} + (when (ff/enabled? ::wallet.contacts) + {:id :tab/contacts :label (i18n/label :t/contacts) :accessibility-label :contacts-tab}) + {:id :tab/my-accounts :label (i18n/label :t/my-accounts) :accessibility-label :my-accounts-tab}]) + +(defn- validate-address + [address] + (debounce/debounce-and-dispatch + (cond + (<= (count address) 0) [:wallet/address-validation-failed address] + (utils-address/eip-3770-address? address) [:wallet/address-validation-success address] + :else [:wallet/address-validation-failed address]) + 300)) + +(defn- address-input + [input-value input-focused?] + (fn [] + (let [current-screen-id (rf/sub [:view-id]) + scanned-address (rf/sub [:wallet/scanned-address]) + send-address (rf/sub [:wallet/wallet-send-to-address]) + recipient (rf/sub [:wallet/send-recipient]) + recipient-plain-address? (= send-address recipient) + valid-ens-or-address? (rf/sub [:wallet/valid-ens-or-address?]) + contacts (rf/sub [:contacts/active])] + [quo/address-input + {:on-focus #(reset! input-focused? true) + :on-blur #(reset! input-focused? false) + :on-scan (fn [on-result] + (rn/dismiss-keyboard!) + (rf/dispatch [:wallet/clean-scanned-address]) + (rf/dispatch [:open-modal :screen/wallet.scan-address + {:on-result on-result}])) + :on-paste (fn [on-change] + (clipboard/get-string + (fn [clipboard] + (when-not (empty? clipboard) + (-> clipboard + utils/on-paste-address-or-ens + on-change))))) + :ens-regex constants/regx-ens + :scanned-value (or (when recipient-plain-address? send-address) scanned-address) + :address-regex utils-address/regx-eip-3770-address + :on-detect-address (fn [address] + (when (or (= current-screen-id :screen/wallet.select-address) + (= current-screen-id :screen/wallet.scan-address)) + ; ^ this check is to prevent effect being triggered when screen + ; is loaded but not being shown to the user (deep in the + ; navigation stack) and avoid undesired behaviors + (validate-address address))) + :on-detect-ens (fn [text cb] + (when (or (= current-screen-id :screen/wallet.select-address) + (= current-screen-id :screen/wallet.scan-address)) + ; ^ this check is to prevent effect being triggered when screen + ; is loaded but not being shown to the user (deep in the + ; navigation stack) and avoid undesired behaviors + (debounce/debounce-and-dispatch + [:wallet/find-ens text contacts cb] + 300))) + :on-change-text (fn [text] + (rf/dispatch [:wallet/clean-local-suggestions]) + (rf/dispatch [:wallet/searching-address]) + (validate-address text) + (reset! input-value text)) + :valid-ens-or-address? valid-ens-or-address?}]))) + +(defn- ens-linked-address + [{:keys [address networks]}] + (let [theme (quo.context/use-theme)] + [quo/text + {:size :paragraph-2 + :style style/network-text-container} + (map (fn [network] + ^{:key (str network)} + [quo/text + {:size :paragraph-2 + :style {:color (colors/resolve-color network theme)}} + (str (subs (name network) 0 3) ":")]) + networks) + [quo/text + {:size :paragraph-2 + :weight :monospace} + address]])) + +(defn- suggestion-component + [] + (fn [{:keys [type ens address accounts primary-name public-key ens-name color] + :as local-suggestion} + _ _ _] + (let [props {:on-press (fn [] + (let [address (if accounts (:address (first accounts)) address)] + (when-not ens + (rf/dispatch [:wallet/select-send-address + {:address address + :recipient local-suggestion + :stack-id :screen/wallet.select-address}])))) + :active-state? false}] + (cond + (= type types/saved-address) + [quo/saved-address + (assoc props + :user-props + {:name primary-name + :address public-key + :ens ens-name + :customization-color color})] + (= type types/saved-contact-address) + [quo/saved-contact-address (merge props local-suggestion)] + (and (not ens) (= type types/address)) + [quo/address (merge props local-suggestion)] + (and ens (= type types/address)) + [ens-linked-address local-suggestion] + :else nil)))) + +(defn- local-suggestions-list + [] + (let [local-suggestion (rf/sub [:wallet/local-suggestions])] + [rn/view {:style {:flex 1}} + [rn/flat-list + {:data local-suggestion + :content-container-style {:flex-grow 1} + :key-fn :id + :on-scroll-to-index-failed identity + :keyboard-should-persist-taps :handled + :render-fn suggestion-component}]])) + +(defn- footer + [input-value] + (let [local-suggestion-address (rf/sub [:wallet/local-suggestions->full-address]) + color (rf/sub [:wallet/current-viewing-account-color]) + valid-ens-or-address? (boolean (rf/sub [:wallet/valid-ens-or-address?]))] + [quo/button + {:accessibility-label :continue-button + :type :primary + :disabled? (not valid-ens-or-address?) + :on-press (fn [] + (let [address (utils-address/extract-address-without-chains-info + (or + local-suggestion-address + input-value))] + (rf/dispatch + [:wallet/select-send-address + {:address address + :recipient {:label + (utils/get-shortened-address + address) + :recipient-type :address} + :stack-id + :screen/wallet.select-address}]))) + :customization-color color} + (i18n/label :t/continue)])) + +(defn view + [] + (let [on-close (fn [] + (rf/dispatch [:wallet/clean-scanned-address]) + (rf/dispatch [:wallet/clean-local-suggestions]) + (rf/dispatch [:wallet/clean-selected-collectible]) + (rf/dispatch [:wallet/clean-send-address]) + (rf/dispatch [:wallet/select-address-tab nil]) + (rf/dispatch [:wallet/clean-current-viewing-account + :ignore-just-complete-transaction])) + on-change-tab #(rf/dispatch [:wallet/select-address-tab %]) + input-value (reagent/atom "") + input-focused? (reagent/atom false)] + (fn [] + (let [selected-tab (or (rf/sub [:wallet/send-tab]) (:id (first tabs-data))) + valid-ens-or-address? (boolean (rf/sub [:wallet/valid-ens-or-address?])) + searching-address? (rf/sub [:wallet/searching-address?])] + (hot-reload/use-safe-unmount on-close) + [floating-button-page/view + {:content-container-style {:flex 1} + :footer-container-padding 0 + :keyboard-should-persist-taps :always + :header [account-switcher/view + {:on-press #(rf/dispatch [:navigate-back]) + :margin-top safe-area/top + :switcher-type :select-account}] + :footer (when-not (string/blank? @input-value) + [footer @input-value])} + [quo/page-top + {:title (i18n/label :t/send-to) + :title-accessibility-label :title-label}] + [address-input input-value input-focused?] + [quo/divider-line] + (when (and (not valid-ens-or-address?) (> (count @input-value) 0) (not searching-address?)) + [rn/view {:style {:padding 20}} + [quo/info-message + {:status :error + :icon :i/info + :size :default} + (i18n/label :t/invalid-address)]]) + (if (or @input-focused? (> (count @input-value) 0)) + [rn/keyboard-avoiding-view + {:style {:flex 1} + :keyboard-vertical-offset 26} + [rn/view {:style {:flex 1 :padding 8}} + [local-suggestions-list]]] + [:<> + [quo/tabs + {:style style/tabs + :container-style style/tabs-content + :size 32 + :default-active selected-tab + :data tabs-data + :scrollable? true + :on-change on-change-tab}] + [tabs/view {:selected-tab selected-tab}]])])))) diff --git a/src/status_im/contexts/wallet/send/select_asset/style.cljs b/src/status_im/contexts/wallet/send/select_asset/style.cljs new file mode 100644 index 00000000000..49731a28caa --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_asset/style.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.wallet.send.select-asset.style) + +(def container + {:flex 1}) + +(def search-input-container + {:padding-horizontal 20 + :padding-vertical 8}) diff --git a/src/status_im/contexts/wallet/send/select_asset/view.cljs b/src/status_im/contexts/wallet/send/select_asset/view.cljs new file mode 100644 index 00000000000..9752ce17f54 --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_asset/view.cljs @@ -0,0 +1,92 @@ +(ns status-im.contexts.wallet.send.select-asset.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.common.asset-list.view :as asset-list] + [status-im.contexts.wallet.common.collectibles-tab.view :as collectibles-tab] + [status-im.contexts.wallet.send.select-asset.style :as style] + [status-im.setup.hot-reload :as hot-reload] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def tabs-data + [{:id :tab/assets :label (i18n/label :t/assets) :accessibility-label :assets-tab} + {:id :tab/collectibles :label (i18n/label :t/collectibles) :accessibility-label :collectibles-tab}]) + +(defn- search-input + [search-text on-change-text] + [rn/view {:style style/search-input-container} + [quo/input + {:small? true + :placeholder (i18n/label :t/search-assets) + :icon-name :i/search + :value search-text + :on-change-text on-change-text}]]) + +(defn collectibles-grid + [search-text] + (let [collectibles (rf/sub [:wallet/current-viewing-account-collectibles-filtered search-text]) + search-performed? (not (string/blank? search-text))] + [collectibles-tab/view + {:collectibles collectibles + :current-account-address (rf/sub [:wallet/current-viewing-account-address]) + :filtered? search-performed? + :on-end-reached #(rf/dispatch [:wallet/request-collectibles-for-current-viewing-account]) + :on-collectible-press (fn [{:keys [collectible]}] + (rf/dispatch [:wallet/set-collectible-to-send + {:collectible collectible + :current-screen :screen/wallet.select-asset + :entry-point :account-send-button}]))}])) + +(defn- tab-view + [search-text selected-tab on-change-text] + (let [unfiltered-collectibles (rf/sub [:wallet/current-viewing-account-collectibles]) + show-search-input? (or (= selected-tab :tab/assets) + (and (= selected-tab :tab/collectibles) + (seq unfiltered-collectibles))) + on-token-press (fn [token] + (rf/dispatch [:wallet/set-token-to-send + {:token token + :stack-id :screen/wallet.select-asset}]))] + [:<> + (when show-search-input? + [search-input search-text on-change-text]) + (case selected-tab + :tab/assets [asset-list/view + {:search-text search-text + :on-token-press on-token-press}] + :tab/collectibles [collectibles-grid search-text])])) + +(defn view + [] + (let [[selected-tab set-selected-tab] (rn/use-state (:id (first tabs-data))) + [search-text set-search-text] (rn/use-state "") + on-change-text #(set-search-text %) + on-change-tab #(set-selected-tab %) + on-close (fn [] + (rf/dispatch [:wallet/clean-selected-token]) + (rf/dispatch [:wallet/clean-selected-collectible]) + (rf/dispatch [:navigate-back]))] + (hot-reload/use-safe-unmount (fn [] + (rf/dispatch [:wallet/clean-selected-token]) + (rf/dispatch [:wallet/clean-selected-collectible]))) + [rn/safe-area-view {:style style/container} + [account-switcher/view + {:icon-name :i/arrow-left + :on-press on-close + :switcher-type :select-account}] + [quo/page-top + {:title (i18n/label :t/select-token) + :title-accessibility-label :title-label}] + [quo/segmented-control + {:size 32 + :blur? false + :symbol false + :default-active :tab/assets + :container-style {:margin-horizontal 20 + :margin-vertical 8} + :data tabs-data + :on-change on-change-tab}] + [tab-view search-text selected-tab on-change-text]])) diff --git a/src/status_im/contexts/wallet/send/select_collectible_amount/style.cljs b/src/status_im/contexts/wallet/send/select_collectible_amount/style.cljs new file mode 100644 index 00000000000..ebb5a58b730 --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_collectible_amount/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.wallet.send.select-collectible-amount.style) + +(def collectible-container + {:margin-vertical 12 + :margin-horizontal 56}) + +(def network-tags-container + {:align-self :center}) + +(def amount-input-container + {:padding-left 20 + :padding-right 20 + :margin-vertical 12}) diff --git a/src/status_im/contexts/wallet/send/select_collectible_amount/view.cljs b/src/status_im/contexts/wallet/send/select_collectible_amount/view.cljs new file mode 100644 index 00000000000..2027be67964 --- /dev/null +++ b/src/status_im/contexts/wallet/send/select_collectible_amount/view.cljs @@ -0,0 +1,72 @@ +(ns status-im.contexts.wallet.send.select-collectible-amount.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.controlled-input.utils :as controlled-input] + [status-im.contexts.wallet.collectible.utils :as utils] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.send.select-collectible-amount.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [on-close (rn/use-callback + #(rf/dispatch [:wallet/collectible-amount-navigate-back])) + send-transaction-data (rf/sub [:wallet/wallet-send]) + collectible (:collectible send-transaction-data) + sender-address (rf/sub [:wallet/current-viewing-account-address]) + balance (rf/sub [:wallet/total-owned-collectible (:ownership collectible) + sender-address]) + [input-state set-input-state] (rn/use-state (-> controlled-input/init-state + (controlled-input/set-value-numeric 1) + (controlled-input/set-lower-limit 1))) + preview-uri (get-in collectible [:preview-url :uri]) + incorrect-value? (controlled-input/input-error input-state) + increase-value (rn/use-callback #(set-input-state controlled-input/increase)) + decrease-value (rn/use-callback #(set-input-state controlled-input/decrease)) + delete-character (rn/use-callback + (fn [] (set-input-state #(controlled-input/delete-last % "1")))) + add-character (rn/use-callback + (fn [c] + (set-input-state #(controlled-input/add-character % c))))] + (rn/use-effect + (fn [] + (set-input-state #(controlled-input/set-upper-limit % balance))) + [balance]) + [rn/view + [account-switcher/view + {:icon-name :i/arrow-left + :on-press on-close + :switcher-type :select-account}] + [quo/expanded-collectible + {:image-src preview-uri + :square? true + :supported-file? (utils/supported-file? (get-in collectible + [:collectible-data :animation-media-type])) + :container-style style/collectible-container}] + [quo/network-tags + {:title (i18n/label :t/max {:number balance}) + :status (if incorrect-value? :error :default) + :container-style style/network-tags-container}] + [quo/amount-input + {:max-value (controlled-input/upper-limit input-state) + :min-value (controlled-input/lower-limit input-state) + :value (controlled-input/value-numeric input-state) + :on-inc-press increase-value + :on-dec-press decrease-value + :container-style style/amount-input-container + :status (if incorrect-value? :error :default)}] + [quo/bottom-actions + {:actions :one-action + :button-one-props {:on-press #(rf/dispatch + [:wallet/set-collectible-amount-to-send + {:stack-id :screen/wallet.select-collectible-amount + :amount (controlled-input/value-numeric input-state)}]) + :disabled? incorrect-value?} + :button-one-label (i18n/label :t/confirm)}] + [quo/numbered-keyboard + {:left-action :none + :delete-key? true + :on-press add-character + :on-delete delete-character}]])) diff --git a/src/status_im/contexts/wallet/send/send_amount/view.cljs b/src/status_im/contexts/wallet/send/send_amount/view.cljs new file mode 100644 index 00000000000..c333c5e5fe4 --- /dev/null +++ b/src/status_im/contexts/wallet/send/send_amount/view.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.wallet.send.send-amount.view + (:require + [quo.context] + [status-im.contexts.wallet.send.input-amount.view :as input-amount] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + [input-amount/view + {:current-screen-id :screen/wallet.send-input-amount + :button-one-label (i18n/label :t/review-send) + :on-navigate-back (fn [] + (rf/dispatch-sync [:wallet/stop-and-clean-suggested-routes]) + (rf/dispatch [:wallet/clean-send-amount]))}]) diff --git a/src/status_im/contexts/wallet/send/transaction_confirmation/style.cljs b/src/status_im/contexts/wallet/send/transaction_confirmation/style.cljs new file mode 100644 index 00000000000..ffc23474b2f --- /dev/null +++ b/src/status_im/contexts/wallet/send/transaction_confirmation/style.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.wallet.send.transaction-confirmation.style + (:require [quo.foundations.colors :as colors])) + +(def detail-item + {:flex 1 + :height 36 + :background-color :transparent}) + +(def content-container + {:padding-top 12 + :padding-horizontal 20 + :padding-bottom 32}) + +(def title-container + {:margin-right 4}) + +(defn details-container + [{:keys [loading-suggested-routes? route-loaded?]}] + {:flex-direction :row + :width "100%" + :justify-content (if route-loaded? :space-between :center) + :height (when (or loading-suggested-routes? route-loaded?) 52) + :margin-horizontal 5 + :padding-top 7 + :margin-bottom 8}) + +(defn section-label + [theme] + {:margin-bottom 8 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def summary-container + {:padding-horizontal 20 + :padding-bottom 16}) diff --git a/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs b/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs new file mode 100644 index 00000000000..6e646efe443 --- /dev/null +++ b/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs @@ -0,0 +1,350 @@ +(ns status-im.contexts.wallet.send.transaction-confirmation.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.common.transaction-settings.view :as transaction-settings] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.send.transaction-confirmation.style :as style] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [status-im.setup.hot-reload :as hot-reload] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- transaction-title + [{:keys [token-display-name amount account route to-network image-url transaction-type + collectible? recipient]}] + (let [to-network-name (:network-name to-network) + to-network-color (if (= to-network-name :mainnet) :ethereum to-network-name)] + [rn/view {:style style/content-container} + [rn/view {:style {:flex-direction :row}} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :send-label} + (if (= transaction-type :tx/bridge) + (i18n/label :t/bridge) + (i18n/label :t/send))] + [quo/summary-tag + (cond-> {:token (if collectible? "" token-display-name) + :label (str amount " " token-display-name) + :type (if collectible? :collectible :token)} + collectible? (assoc :image-source image-url))]] + (if (= transaction-type :tx/bridge) + (doall + (map-indexed + (fn [idx path] + (let [from-network (:from path) + chain-id (:chain-id from-network) + network (rf/sub [:wallet/network-by-id chain-id]) + network-name (:network-name network) + network-color (if (= network-name :mainnet) :ethereum network-name)] + (with-meta + [rn/view + {:style {:flex-direction :row + :margin-top 4}} + (if (zero? idx) + [:<> + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :send-label} + (i18n/label :t/from)] + [quo/summary-tag + {:label (:full-name network) + :type :network + :image-source (:source network) + :customization-color network-color}]] + [:<> + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :send-label} + (str (i18n/label :t/and) " ")] + [quo/summary-tag + {:label (:full-name network) + :type :network + :image-source (:source network) + :customization-color network-color}]])] + {:key (str "transaction-title" idx)}))) + route)) + [rn/view + {:style {:flex-direction :row + :margin-top 4}} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :send-label} + (i18n/label :t/from)] + [quo/summary-tag + {:label (:name account) + :type :account + :emoji (:emoji account) + :customization-color (:color account)}]]) + [rn/view + {:style {:flex-direction :row + :margin-top 4}} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :send-label} + (i18n/label :t/to)] + (if (= transaction-type :tx/bridge) + [quo/summary-tag + {:type :network + :image-source (:source to-network) + :label (:full-name to-network) + :customization-color to-network-color}] + [quo/summary-tag (assoc recipient :type (:recipient-type recipient))])] + (when (= transaction-type :tx/bridge) + [rn/view + {:style {:flex-direction :row + :margin-top 4}} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :send-label} + (i18n/label :t/in)] + [quo/summary-tag + {:label (:name account) + :type :account + :emoji (:emoji account) + :customization-color (:color account)}]])])) + +(defn- user-summary + [{:keys [account-props theme label accessibility-label summary-type recipient bridge-tx? account-to?]}] + (let [network-values (rf/sub [:wallet/send-network-values account-to?]) + summary-info-type (case (:recipient-type recipient) + :saved-address :saved-account + :account :status-account + summary-type)] + [rn/view {:style style/summary-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label accessibility-label} + label] + [quo/summary-info + {:type summary-info-type + :networks-to-show (when bridge-tx? + network-values) + :account-props (cond-> account-props + (and account-to? (not bridge-tx?)) + (assoc + :size 32 + :name (:label recipient) + :full-name (:label recipient) + :emoji (:emoji recipient) + :customization-color (:customization-color recipient)))}]])) + +(defn- network-summary + [{:keys [theme label accessibility-label]}] + (let [network (rf/sub [:wallet/send-selected-network])] + (when network + [rn/view {:style style/summary-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label accessibility-label} + label] + [quo/summary-info + {:type :network + :network-props network}]]))) + +(defn- data-item + [{:keys [title subtitle subtitle-color loading?]}] + [quo/data-item + {:container-style style/detail-item + :blur? false + :card? false + :status (if loading? :loading :default) + :size :small + :title title + :subtitle subtitle + :subtitle-color subtitle-color}]) + +(defn- error-banner + [] + (let [enough-assets? (rf/sub [:wallet/send-enough-assets?]) + no-routes-found? (rf/sub [:wallet/no-routes-found?]) + {native-token-symbol :symbol} (rf/sub [:wallet/send-native-token])] + (cond + no-routes-found? + [quo/alert-banner + {:text (i18n/label :t/no-routes-found-confirmation) + :text-number-of-lines 2}] + + (not enough-assets?) + [quo/alert-banner + {:action? true + :text (i18n/label :t/not-enough-assets-to-pay-gas-fees) + :button-text (i18n/label :t/add-token {:token native-token-symbol}) + :on-button-press #(rf/dispatch [:show-bottom-sheet + {:content buy-token/view}])}] + + :else nil))) + +(defn- transaction-details + [{:keys [max-fees to-network + transaction-type route-loaded?]}] + (let [theme (quo.context/use-theme) + loading-suggested-routes? (rf/sub [:wallet/wallet-send-loading-suggested-routes?]) + estimated-time (rf/sub [:wallet/send-estimated-time]) + enough-assets? (rf/sub [:wallet/send-enough-assets?]) + no-routes-found? (rf/sub [:wallet/no-routes-found?]) + loading? (or loading-suggested-routes? no-routes-found?) + amount (rf/sub [:wallet/send-total-amount-formatted])] + [rn/view + {:style (style/details-container + {:loading-suggested-routes? loading-suggested-routes? + :route-loaded? route-loaded?})} + [quo/button + {:icon-only? true + :type :outline + :size 32 + :inner-style {:opacity 1} + :accessibility-label :advanced-button + :container-style {:margin-right 8} + :disabled? no-routes-found? + :on-press #(rf/dispatch + [:show-bottom-sheet + {:content transaction-settings/settings-sheet}])} + :i/advanced] + [data-item + {:title (i18n/label :t/est-time) + :loading? loading? + :subtitle (i18n/label :t/time-in-sec + {:seconds estimated-time})}] + [data-item + {:title (i18n/label :t/max-fees) + :loading? loading? + :subtitle max-fees + :subtitle-color (when-not enough-assets? + (colors/theme-colors colors/danger-50 + colors/danger-60 + theme))}] + [data-item + {:title (if (= transaction-type :tx/bridge) + (i18n/label :t/bridged-to + {:network (:abbreviated-name to-network)}) + (i18n/label :t/recipient-gets)) + :loading? loading? + :subtitle amount}]])) + +(defn view + [_] + (let [theme (quo.context/use-theme) + send-transaction-data (rf/sub [:wallet/wallet-send]) + {:keys [token-display-name collectible amount + route + to-address bridge-to-chain-id type + recipient]} send-transaction-data + collectible? (some? collectible) + image-url (when collectible + (get-in collectible [:preview-url :uri])) + transaction-type (:tx-type send-transaction-data) + token-symbol (or token-display-name + (-> send-transaction-data :token :symbol)) + fee-formatted (rf/sub [:wallet/wallet-send-fee-fiat-formatted]) + account (rf/sub [:wallet/current-viewing-account]) + account-color (:color account) + bridge-to-network (when bridge-to-chain-id + (rf/sub [:wallet/network-by-id bridge-to-chain-id])) + loading-suggested-routes? (rf/sub + [:wallet/wallet-send-loading-suggested-routes?]) + from-account-props {:customization-color account-color + :size 32 + :emoji (:emoji account) + :type :default + :name (:name account) + :address (utils/get-shortened-address + (:address + account))} + user-props {:full-name to-address + :address (utils/get-shortened-address + to-address)} + auth-icon (rf/sub [:standard-auth/slider-icon]) + enough-assets? (rf/sub [:wallet/send-enough-assets?]) + no-routes-found? (rf/sub [:wallet/no-routes-found?])] + (hot-reload/use-safe-unmount #(rf/dispatch [:wallet/clean-route-data-for-collectible-tx])) + [rn/view {:style {:flex 1}} + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:icon-name :i/arrow-left + :on-press events-helper/navigate-back + :margin-top safe-area/top + :background :blur + :accessibility-label :top-bar}] + :blur-options {:padding-horizontal 0} + :footer [rn/view + [error-banner] + [rn/view {:style {:padding-horizontal 20}} + [transaction-details + {:max-fees fee-formatted + :to-network bridge-to-network + :transaction-type transaction-type + :route-loaded? (and route (seq route))}] + [quo/slide-button + {:size :size-48 + :track-text (if (= transaction-type :tx/bridge) + (i18n/label :t/slide-to-bridge) + (i18n/label :t/slide-to-send)) + :container-style {:z-index 2} + :disabled? (or (not enough-assets?) + loading-suggested-routes? + no-routes-found?) + :customization-color account-color + :track-icon auth-icon + :on-complete #(rf/dispatch + [:wallet.send/auth-slider-completed])}]]] + :gradient-cover? true + :customization-color (:color account)} + [rn/view + [transaction-title + {:token-display-name token-symbol + :amount amount + :account account + :type type + :recipient recipient + :route route + :to-network bridge-to-network + :image-url image-url + :transaction-type transaction-type + :collectible? collectible?}] + [user-summary + {:summary-type :status-account + :accessibility-label :summary-from-label + :label (i18n/label :t/from-capitalized) + :account-props from-account-props + :bridge-tx? (= transaction-type :tx/bridge) + :theme theme}] + [user-summary + {:summary-type (if (= transaction-type :tx/bridge) + :status-account + :account) + :accessibility-label :summary-to-label + :label (i18n/label :t/to-capitalized) + :account-props (if (= transaction-type :tx/bridge) + from-account-props + user-props) + :recipient recipient + :bridge-tx? (= transaction-type :tx/bridge) + :account-to? true + :theme theme}] + (when-not (= transaction-type :tx/bridge) + [network-summary + {:label (i18n/label :t/on-capitalized) + :theme theme}])]]])) diff --git a/src/status_im/contexts/wallet/send/transaction_progress/style.cljs b/src/status_im/contexts/wallet/send/transaction_progress/style.cljs new file mode 100644 index 00000000000..94f31ccae94 --- /dev/null +++ b/src/status_im/contexts/wallet/send/transaction_progress/style.cljs @@ -0,0 +1,5 @@ +(ns status-im.contexts.wallet.send.transaction-progress.style) + +(def content-container + {:flex 1 + :margin-horizontal 20}) diff --git a/src/status_im/contexts/wallet/send/transaction_progress/view.cljs b/src/status_im/contexts/wallet/send/transaction_progress/view.cljs new file mode 100644 index 00000000000..c09dd59e6fb --- /dev/null +++ b/src/status_im/contexts/wallet/send/transaction_progress/view.cljs @@ -0,0 +1,77 @@ +(ns status-im.contexts.wallet.send.transaction-progress.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.common.resources :as resources] + [status-im.contexts.wallet.send.transaction-progress.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn titles + [status] + (case status + :pending (i18n/label :t/sending-with-ellipsis) + :confirmed (i18n/label :t/transaction-confirmed) + :finalised (i18n/label :t/transacation-finalised) + "")) + +(defn combined-status-overview + [transaction-details] + (cond + (every? (fn [[_k v]] (= (:status v) :finalised)) transaction-details) :finalised + (some (fn [[_k v]] (= (:status v) :pending)) transaction-details) :pending + (some (fn [[_k v]] (= (:status v) :confirmed)) transaction-details) :confirmed + :else nil)) + +(defn- footer + [{:keys [color leave-page]}] + (let [send-to-address (rf/sub [:wallet/wallet-send-to-address]) + save-address-hidden? (rf/sub [:wallet/address-saved? send-to-address])] + [quo/bottom-actions + {:actions (if save-address-hidden? :one-action :two-actions) + :button-two-label (i18n/label :t/save-address) + :button-two-props {:type :grey + :icon-left :i/contact-book + :accessibility-label :save-address + :on-press (rn/use-callback + #(rf/dispatch [:open-modal + :screen/settings.save-address + {:address send-to-address}]))} + :button-one-label (i18n/label :t/done) + :button-one-props {:customization-color color + :type :primary + :accessibility-label :done + :on-press leave-page}}])) + +(defn view + [] + (let [leave-page #(rf/dispatch [:wallet/end-transaction-flow]) + {:keys [color]} (rf/sub [:wallet/current-viewing-account])] + (fn [] + (rn/use-effect + (fn [] + (rf/dispatch [:wallet/get-saved-addresses]))) + (let [transaction-details (rf/sub [:wallet/send-transaction-progress])] + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:type :no-title + :background :blur + :icon-name :i/close + :margin-top safe-area/top + :on-press leave-page + :accessibility-label :top-bar}] + :footer [footer + {:color color + :leave-page leave-page}] + :customization-color color + :gradient-cover? true} + [rn/view {:style style/content-container} + [rn/image + {:source (resources/get-image :transaction-progress) + :style {:margin-bottom 12}}] + [quo/standard-title + {:container-style {:flex 1} + :title (titles (combined-status-overview transaction-details))}]]])))) diff --git a/src/status_im/contexts/wallet/send/utils.cljs b/src/status_im/contexts/wallet/send/utils.cljs new file mode 100644 index 00000000000..0d1ca52b9b1 --- /dev/null +++ b/src/status_im/contexts/wallet/send/utils.cljs @@ -0,0 +1,248 @@ +(ns status-im.contexts.wallet.send.utils + (:require + [status-im.constants :as constants] + [utils.money :as money])) + +(defn amount-in-hex + [amount token-decimal] + (money/to-hex (money/mul (money/bignumber amount) (money/from-decimal token-decimal)))) + +(defn map-multitransaction-by-ids + [transaction-batch-id transaction-hashes] + (reduce-kv (fn [map1 chain-id value1] + (merge map1 + (reduce + (fn [map2 tx-id] + (assoc map2 + tx-id + {:status :pending + :id transaction-batch-id + :chain-id chain-id})) + {} + value1))) + {} + transaction-hashes)) + +(defn calculate-gas-fee + [{:keys [gas-amount gas-price l1-gas-fee]}] + (let [total-gas-fee-wei (money/mul (money/->wei :gwei gas-price) gas-amount) + l1-fee-wei (money/->wei :gwei l1-gas-fee)] + (money/add total-gas-fee-wei l1-fee-wei))) + +(defn path-gas-fee + [path] + (let [gas-amount (money/bignumber (get path :gas-amount)) + gas-fees (get path :gas-fees) + eip1559-enabled? (get gas-fees :eip-1559-enabled) + gas-price (money/bignumber (if eip1559-enabled? + (get gas-fees :tx-max-fees-per-gas) + (get gas-fees :gas-price))) + l1-gas-fee (get gas-fees :l-1-gas-fee)] + (calculate-gas-fee {:gas-amount gas-amount + :gas-price gas-price + :l1-gas-fee l1-gas-fee}))) + +(defn path-gas-fee-for-custom-gas-price + [route gas-price] + (let [gas-amount (money/bignumber (get route :gas-amount)) + gas-fees (get route :gas-fees) + l1-gas-fee (get gas-fees :l-1-gas-fee)] + (calculate-gas-fee {:gas-amount gas-amount + :gas-price (money/bignumber gas-price) + :l1-gas-fee l1-gas-fee}))) + +(defn full-route-gas-fee + "Sums all the routes fees in wei and then convert the total value to ether" + [route] + (money/wei->ether (reduce money/add (map path-gas-fee route)))) + +(defn full-route-gas-fee-for-custom-gas-price + "Calculates the gas fee for a route in ether using a custom gas price" + [route gas-price] + (money/wei->ether (path-gas-fee-for-custom-gas-price route gas-price))) + +(defn- path-amount-in + [path] + (-> path :amount-in money/from-hex)) + +(defn- path-amount-out + [path] + (if (= (:bridge-name path) constants/bridge-name-hop) + (let [{:keys [token-fees amount-in]} path] + (-> amount-in + money/from-hex + (money/sub token-fees))) + (-> path :amount-out money/from-hex))) + +(defn convert-wei-to-eth + [amount native-token? token-decimals] + (money/with-precision + (if native-token? + (money/wei->ether amount) + (money/token->unit amount token-decimals)) + constants/token-display-precision)) + +(defn network-amounts-by-chain + [{:keys [route token-decimals native-token? receiver?]}] + (reduce + (fn [acc path] + (let [amount (if receiver? + (path-amount-out path) + (path-amount-in path)) + chain-id (if receiver? + (get-in path [:to :chain-id]) + (get-in path [:from :chain-id]))] + (as-> amount $ + (convert-wei-to-eth $ native-token? token-decimals) + (update acc chain-id money/add $)))) + {} + route)) + +(defn path-estimated-received + "Calculates the (`bignumber`) estimated received token amount. For + bridge transactions, the amount is the difference between the + `amount-in` and the `token-fees`." + [path] + (if (-> path :bridge-name (= constants/bridge-name-hop)) + (-> path + :amount-in + money/from-hex + (money/sub (:token-fees path))) + (-> path + :amount-out + money/from-hex))) + +(defn estimated-received-by-chain + [route token-decimals native-token?] + (reduce + (fn [acc path] + (let [chain-id (get-in path [:to :chain-id]) + estimated-received (-> path + path-estimated-received + (convert-wei-to-eth native-token? token-decimals))] + (update acc chain-id money/add estimated-received))) + {} + route)) + +(defn network-values-for-ui + [amounts] + (reduce-kv (fn [acc k v] + (assoc acc k (if (money/equal-to v 0) "<0.01" v))) + {} + amounts)) + +(defn add-zero-values-to-network-values + [network-values all-possible-chain-ids] + (reduce + (fn [acc chain-id] + (let [route-value (get network-values chain-id)] + (assoc acc chain-id (or route-value (money/bignumber "0"))))) + {} + all-possible-chain-ids)) + +(defn token-available-networks-for-suggested-routes + [{:keys [balances-per-chain disabled-chain-ids only-with-balance?]}] + (let [disabled-set (set disabled-chain-ids)] + (->> balances-per-chain + (filter (fn [[_ {:keys [chain-id raw-balance]}]] + (and (not (contains? disabled-set chain-id)) + (or (not only-with-balance?) + (and only-with-balance? + (money/bignumber? raw-balance) + (money/above-zero? raw-balance)))))) + (map first)))) + +(defn reset-loading-network-amounts-to-zero + [network-amounts] + (mapv + (fn [network-amount] + (cond-> network-amount + (= (:type network-amount) :loading) + (assoc :total-amount (money/bignumber "0") + :type :default))) + network-amounts)) + +(defn reset-network-amounts-to-zero + [network-amounts] + (map + (fn [network-amount] + (assoc network-amount + :total-amount (money/bignumber "0") + :type :default)) + network-amounts)) + +(defn network-amounts + [network-values] + (->> network-values + (map + (fn [[chain-id amount]] + {:chain-id chain-id + :total-amount amount + :type :default})) + vec)) + +(defn loading-network-amounts + [{:keys [networks values receiver?]}] + (->> networks + (map + (fn [chain-id] + (let [network-value (when values (get values chain-id))] + (cond-> {:chain-id chain-id + :type (if network-value :default :loading)} + network-value (assoc :total-amount + (money/bignumber network-value)) + (and (not network-value) (not receiver?)) (assoc :total-amount (money/bignumber "0")))))) + (vec))) + +(defn network-links + [route from-values-by-chain to-values-by-chain] + (reduce (fn [acc path] + (let [from-chain-id (get-in path [:from :chain-id]) + to-chain-id (get-in path [:to :chain-id]) + from-chain-id-index (first (keep-indexed #(when (= from-chain-id (:chain-id %2)) %1) + from-values-by-chain)) + to-chain-id-index (first (keep-indexed #(when (= to-chain-id (:chain-id %2)) %1) + to-values-by-chain)) + position-diff (- from-chain-id-index to-chain-id-index)] + (conj acc + {:from-chain-id from-chain-id + :to-chain-id to-chain-id + :position-diff position-diff}))) + [] + route)) + +(def ^:private collectible-tx-set + #{:tx/collectible-erc-721 + :tx/collectible-erc-1155}) + +(defn tx-type-collectible? + [tx-type] + (contains? collectible-tx-set tx-type)) + +(defn bridge-disabled? + [token-symbol] + (not (constants/bridge-assets token-symbol))) + +(defn path-identity + ([path] + (path-identity path (:approval-required path))) + ([path is-approval-tx?] + {:routerInputParamsUuid (:router-input-params-uuid path) + :pathName (:bridge-name path) + :chainID (get-in path [:from :chain-id]) + :isApprovalTx is-approval-tx? + :communityID nil})) + +(defn path-tx-custom-params + [user-tx-settings route] + (let [nonce (or (:nonce user-tx-settings) (:nonce route)) + gas-amount (or (:gas-amount user-tx-settings) (:gas-amount route)) + priority-fee (or (:priority-fee user-tx-settings) (get-in route [:gas-fees :tx-priority-fee])) + max-base-fee (or (:max-base-fee user-tx-settings) (get-in route [:gas-fees :base-fee])) + gas-price (or (:gas-price user-tx-settings) (get-in route [:gas-fees :gas-price]))] + {:gasFeeMode constants/gas-rate-custom + :nonce nonce + :gasAmount gas-amount + :maxFeesPerGas (money/to-hex (money/->wei :gwei (money/add max-base-fee priority-fee))) + :priorityFee (money/to-hex (money/->wei :gwei priority-fee)) + :gasPrice (money/to-hex (money/->wei :gwei gas-price))})) diff --git a/src/status_im/contexts/wallet/send/utils_test.cljs b/src/status_im/contexts/wallet/send/utils_test.cljs new file mode 100644 index 00000000000..2bec0f17e5c --- /dev/null +++ b/src/status_im/contexts/wallet/send/utils_test.cljs @@ -0,0 +1,536 @@ +(ns status-im.contexts.wallet.send.utils-test + (:require [cljs.test :refer [deftest is testing]] + [status-im.contexts.wallet.send.utils :as utils] + [utils.map :as map] + [utils.money :as money])) + +(deftest amount-in-hex-test + (testing "Test amount-in-hex function" + (let [amount 1 + decimal 18] + (is (= (utils/amount-in-hex amount decimal) + "0xde0b6b3a7640000"))))) + +(def multichain-transacation + {:id 61 + :hashes {:5 ["0x5"] + :420 ["0x12" "0x11"]}}) + +(deftest map-multitransaction-by-ids-test + (testing "test map-multitransaction-by-ids formats to right data structure" + (let [{:keys [id hashes]} multichain-transacation] + (is (= (utils/map-multitransaction-by-ids id hashes) + {"0x5" {:status :pending + :id 61 + :chain-id :5} + "0x12" {:status :pending + :id 61 + :chain-id :420} + "0x11" {:status :pending + :id 61 + :chain-id :420}}))))) + +(deftest network-amounts-by-chain-test + (testing "Correctly calculates network amounts for transaction with native token" + (let [route [{:amount-out "0xde0b6b3a7640000" + :to {:chain-id 1}} + {:amount-out "0xde0b6b3a7640000" + :to {:chain-id 10}}] + token-decimals 18 + native-token? true + receiver? true + result (utils/network-amounts-by-chain {:route route + :token-decimals token-decimals + :native-token? native-token? + :receiver? receiver?}) + expected {1 (money/bignumber "1") + 10 (money/bignumber "1")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value))))) + + (testing + "Correctly calculates network amounts for transaction with native token and multiple routes to same chain-id" + (let [route [{:amount-out "0xde0b6b3a7640000" + :to {:chain-id 1}} + {:amount-out "0xde0b6b3a7640000" + :to {:chain-id 1}}] + token-decimals 18 + native-token? true + receiver? true + result (utils/network-amounts-by-chain {:route route + :token-decimals token-decimals + :native-token? native-token? + :receiver? receiver?}) + expected {1 (money/bignumber "2")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value))))) + + (testing "Correctly calculates network amounts for transaction with non-native token" + (let [route [{:amount-in "0x1e8480" + :from {:chain-id 1}} + {:amount-in "0x1e8480" + :from {:chain-id 10}}] + token-decimals 6 + native-token? false + receiver? false + result (utils/network-amounts-by-chain {:route route + :token-decimals token-decimals + :native-token? native-token? + :receiver? receiver?}) + expected {1 (money/bignumber "2") + 10 (money/bignumber "2")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value))))) + + (testing "Correctly calculates network (out) amounts for bridge transaction" + (let [route [{:bridge-name "Hop" + :amount-in "0xde0b6b3a7640000" + :token-fees (money/bignumber "230000000000000") + :to {:chain-id 1}} + {:bridge-name "Hop" + :token-fees (money/bignumber "410000000000000") + :amount-in "0xde0b6b3a7640000" + :to {:chain-id 10}}] + token-decimals 6 + native-token? true + receiver? true + result (utils/network-amounts-by-chain {:route route + :token-decimals token-decimals + :native-token? native-token? + :receiver? receiver?}) + expected {1 (money/bignumber "0.99977") + 10 (money/bignumber "0.99959")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value)))))) + +(deftest estimated-received-by-chain-test + (testing "Correctly calculates the bridge estimated received amount" + (let [chain-id 1 + route [{:bridge-name "Hop" + :amount-in "0xde0b6b3a7640000" + :token-fees (money/bignumber "230000000000000") + :to {:chain-id 1}}] + token-decimals 18 + native-token? true + result (utils/estimated-received-by-chain route token-decimals native-token?) + expected (money/bignumber "0.99977")] + (is (money/equal-to (get result chain-id) expected)))) + + (testing "Correctly calculates the estimated received amount with native token" + (let [chain-id 1 + route [{:amount-out "0x1bc16d674ec80000" + :to {:chain-id chain-id}}] + token-decimals 18 + native-token? true + result (utils/estimated-received-by-chain route token-decimals native-token?) + expected (money/bignumber "2")] + (is (money/equal-to (get result chain-id) expected)))) + + (testing "Correctly calculates the estimated received amount with non-native token" + (let [chain-id 10 + route [{:amount-out "0x1bc16d674ec80000" + :to {:chain-id chain-id}}] + token-decimals 18 + native-token? false + result (utils/estimated-received-by-chain route token-decimals native-token?) + expected (money/bignumber "2")] + (is (money/equal-to (get result chain-id) expected)))) + + (testing + "Correctly calculates the estimated received amount with multiple routes on different networks" + (let [route [{:amount-out "0x1bc16d674ec80000" + :to {:chain-id 1}} + {:amount-out "0xde0b6b3a7640000" + :to {:chain-id 10}}] + token-decimals 18 + native-token? false + result (utils/estimated-received-by-chain route token-decimals native-token?) + expected {10 (money/bignumber "1") + 1 (money/bignumber "2")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value))))) + + (testing "Correctly calculates the estimated received amount with multiple routes on the same network" + (let [chain-id 10 + route [{:amount-out "0x1bc16d674ec80000" + :to {:chain-id chain-id}} + {:amount-out "0x1bc16d674ec80000" + :to {:chain-id chain-id}}] + token-decimals 18 + native-token? false + result (utils/estimated-received-by-chain route token-decimals native-token?) + expected (money/bignumber "4")] + (is (money/equal-to (get result chain-id) expected))))) + +(deftest network-values-for-ui-test + (testing "Sanitizes values correctly for display" + (let [amounts {1 (money/bignumber "0") + 10 (money/bignumber "2.5") + 42161 (money/bignumber "0.005")} + result (utils/network-values-for-ui amounts) + expected {1 "<0.01" + 10 (money/bignumber "2.5") + 42161 (money/bignumber "0.005")}] + (doseq [[chain-id exp-value] expected] + (is #(or (= (get result chain-id) exp-value) + (money/equal-to (get result chain-id) exp-value))))))) + +(deftest calculate-gas-fee-test + (testing "EIP-1559 transaction without L1 fee" + (let [data {:gas-amount "23487" + :gas-fees {:tx-max-fees-per-gas "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}} + expected-result (money/bignumber "53063589834657")] ; This is in Wei + (is (money/equal-to (utils/path-gas-fee data) + expected-result)))) + + (testing "EIP-1559 transaction with L1 fee of 60,000 Gwei" + (let [data {:gas-amount "23487" + :gas-fees {:tx-max-fees-per-gas "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "60000"}} + expected-result (money/bignumber "113063589834657")] ; Added 60,000 Gwei in Wei to the + ; previous result + (is (money/equal-to (utils/path-gas-fee data) + expected-result)))) + + (testing "Non-EIP-1559 transaction with specified gas price" + (let [data {:gas-amount "23487" + :gas-fees {:gas-price "2.872721089" + :eip-1559-enabled false + :l-1-gas-fee "0"}} + expected-result (money/bignumber "67471600217343")] ; This is in Wei, for the specified + ; gas amount and price + (is (money/equal-to (utils/path-gas-fee data) + expected-result))))) + +(deftest calculate-full-route-gas-fee-test + (testing "Route with a single EIP-1559 transaction, no L1 fees" + (let [route [{:gas-amount "23487" + :gas-fees {:tx-max-fees-per-gas "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}}] + expected-result (money/bignumber "0.000053063589834657")] ; The Wei amount for the + ; transaction, converted to + ; Ether + (is (money/equal-to (utils/full-route-gas-fee route) + expected-result)))) + + (testing "Route with two EIP-1559 transactions, no L1 fees" + (let [route [{:gas-amount "23487" + :gas-fees {:tx-max-fees-per-gas "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}} + {:gas-amount "23487" + :gas-fees {:tx-max-fees-per-gas "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}}] + expected-result (money/bignumber "0.000106127179669314")] ; Sum of both transactions' Wei + ; amounts, converted to Ether + (is (money/equal-to (utils/full-route-gas-fee route) + expected-result)))) + + (testing "Route with two EIP-1559 transactions, one with L1 fee of 60,000 Gwei" + (let [route [{:gas-amount "23487" + :gas-fees {:tx-max-fees-per-gas "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}} + {:gas-amount "23487" + :gas-fees {:tx-max-fees-per-gas "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "60000"}}] + expected-result (money/bignumber "0.000166127179669314")] ; Added 60,000 Gwei in Wei to + ; the previous total and + ; converted to Ether + (is (money/equal-to (utils/full-route-gas-fee route) + expected-result))))) + +(deftest token-available-networks-for-suggested-routes-test + (testing "Excludes disabled chain-ids correctly" + (let [balances-per-chain {1 {:chain-id 1 :balance 100} + 10 {:chain-id 10 :balance 200} + 42161 {:chain-id 42161 :balance 300}} + disabled-chain-ids [10] + expected [1 42161]] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids}))))) + + (testing "Returns all chains when no disabled chains are specified" + (let [balances-per-chain {1 {:chain-id 1 :balance 100} + 10 {:chain-id 10 :balance 200} + 42161 {:chain-id 42161 :balance 300}} + disabled-chain-ids [] + expected [1 10 42161]] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids}))))) + + (testing "Returns empty list when all chains are disabled" + (let [balances-per-chain {1 {:chain-id 1 :balance 100} + 10 {:chain-id 10 :balance 200} + 42161 {:chain-id 42161 :balance 300}} + disabled-chain-ids [1 10 42161] + expected []] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids}))))) + + (testing "Handles non-existent chain-ids gracefully" + (let [balances-per-chain {59144 {:chain-id 59144 :balance 400}} + disabled-chain-ids [1 10 42161] + expected [59144]] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids})))))) + +(deftest reset-loading-network-amounts-to-zero-test + (testing "Correctly resets loading network amounts to zero and changes type to default" + (let [network-amounts [{:chain-id 1 :total-amount (money/bignumber "100") :type :loading} + {:chain-id 10 :total-amount (money/bignumber "200") :type :default}] + expected [{:chain-id 1 :total-amount (money/bignumber "0") :type :default} + {:chain-id 10 :total-amount (money/bignumber "200") :type :default}] + result (utils/reset-loading-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Leaves non-loading types unchanged" + (let [network-amounts [{:chain-id 1 :total-amount (money/bignumber "100") :type :default} + {:chain-id 10 :total-amount (money/bignumber "0") :type :disabled}] + expected [{:chain-id 1 :total-amount (money/bignumber "100") :type :default} + {:chain-id 10 :total-amount (money/bignumber "0") :type :disabled}] + result (utils/reset-loading-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Processes an empty list without error" + (let [network-amounts [] + expected [] + result (utils/reset-loading-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Applies transformations to multiple loading entries" + (let [network-amounts [{:chain-id 1 :total-amount (money/bignumber "100") :type :loading} + {:chain-id 10 :total-amount (money/bignumber "200") :type :loading}] + expected [{:chain-id 1 :total-amount (money/bignumber "0") :type :default} + {:chain-id 10 :total-amount (money/bignumber "0") :type :default}] + result (utils/reset-loading-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Mix of loading and non-loading types" + (let [network-amounts [{:chain-id 1 :total-amount (money/bignumber "100") :type :loading} + {:chain-id 10 :total-amount (money/bignumber "200") :type :default} + {:chain-id 42161 :total-amount (money/bignumber "300") :type :loading} + {:chain-id 59144 :total-amount (money/bignumber "0") :type :disabled}] + expected [{:chain-id 1 :total-amount (money/bignumber "0") :type :default} + {:chain-id 10 :total-amount (money/bignumber "200") :type :default} + {:chain-id 42161 :total-amount (money/bignumber "0") :type :default} + {:chain-id 59144 :total-amount (money/bignumber "0") :type :disabled}] + result (utils/reset-loading-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons))))) + +(deftest network-amounts-test + (testing "Handles network values with one chain" + (let [network-values {10 (money/bignumber "200")} + expected [{:chain-id 10 + :total-amount (money/bignumber "200") + :type :default}] + result (utils/network-amounts network-values)] + (is (every? identity (map #(map/deep-compare %1 %2) expected result)))))) + +(deftest loading-network-amounts-test + (testing "Assigns :loading type to valid networks except for disabled ones" + (let [valid-networks [1 10 42161] + disabled-chain-ids [42161] + receiver-networks [1 10] + token-networks-ids [1 10 42161] + receiver? true + expected [{:chain-id 1 :type :loading} + {:chain-id 10 :type :loading}] + tx-type :tx/send + result (utils/loading-network-amounts {:valid-networks valid-networks + :disabled-chain-ids disabled-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :tx-type tx-type + :receiver? receiver?}) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Filters out networks not in receiver networks when receiver? is true" + (let [valid-networks [1 10 42161 59144] + disabled-chain-ids [10] + receiver-networks [1 42161] + token-networks-ids [1 10 42161] + receiver? true + expected [{:chain-id 1 :type :loading} + {:chain-id 42161 :type :loading}] + tx-type :tx/send + result (utils/loading-network-amounts {:valid-networks valid-networks + :disabled-chain-ids disabled-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :tx-type tx-type + :receiver? receiver?}) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing + "Appends :edit type if receiver network count is less than available networks and receiver? is true" + (let [valid-networks [1 10 42161] + disabled-chain-ids [10] + receiver-networks [1] + token-networks-ids [1 10 42161] + receiver? true + expected [{:chain-id 1 :type :loading}] + tx-type :tx/send + result (utils/loading-network-amounts {:valid-networks valid-networks + :disabled-chain-ids disabled-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :tx-type tx-type + :receiver? receiver?}) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing + "Assigns :not-available type to networks not available in token-networks-ids when receiver? is false" + (let [valid-networks [42161] + disabled-chain-ids [] + receiver-networks [1] + token-networks-ids [42161] + receiver? false + expected [{:chain-id 42161 :type :loading}] + tx-type :tx/send + result (utils/loading-network-amounts {:valid-networks valid-networks + :disabled-chain-ids disabled-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :tx-type tx-type + :receiver? receiver?}) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing + "Assigns :not-available type to networks not available in token-networks-ids when receiver? is true" + (let [valid-networks [42161] + disabled-chain-ids [] + receiver-networks [1] + token-networks-ids [42161] + receiver? true + expected [{:chain-id 1 :type :not-available}] + tx-type :tx/send + result (utils/loading-network-amounts {:valid-networks valid-networks + :disabled-chain-ids disabled-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :tx-type tx-type + :receiver? receiver?}) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing + "Assigns :loading type to valid networks and :disabled for disabled ones when tx-type is :tx/bridge and receiver? false" + (let [valid-networks [1 10] + disabled-chain-ids [10] + receiver-networks [] + token-networks-ids [1 10] + tx-type :tx/bridge + receiver? false + expected [{:chain-id 1 :type :loading} + {:chain-id 10 :type :disabled :total-amount (money/bignumber "0")}] + result (utils/loading-network-amounts {:valid-networks valid-networks + :disabled-chain-ids disabled-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :tx-type tx-type + :receiver? receiver?}) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing + "Assigns :loading type to valid networks, ignore disabled ones and do not add {:type :edit} when tx-type is :tx/bridge and receiver? true" + (let [valid-networks [1] + disabled-chain-ids [10] + receiver-networks [] + token-networks-ids [1 10] + tx-type :tx/bridge + receiver? true + expected [{:chain-id 1 :type :loading}] + result (utils/loading-network-amounts {:valid-networks valid-networks + :disabled-chain-ids disabled-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :tx-type tx-type + :receiver? receiver?}) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons))))) + +(deftest network-links-test + (testing "Calculates position differences correctly" + (let [route [{:from {:chain-id 1} :to {:chain-id 42161}} + {:from {:chain-id 10} :to {:chain-id 1}} + {:from {:chain-id 42161} :to {:chain-id 10}}] + from-values-by-chain [{:chain-id 1} {:chain-id 10} {:chain-id 42161}] + to-values-by-chain [{:chain-id 42161} {:chain-id 1} {:chain-id 10}] + expected [{:from-chain-id 1 :to-chain-id 42161 :position-diff 0} + {:from-chain-id 10 :to-chain-id 1 :position-diff 0} + {:from-chain-id 42161 :to-chain-id 10 :position-diff 0}] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result)))) + + (testing "Handles cases with no position difference" + (let [route [{:from {:chain-id 1} :to {:chain-id 1}}] + from-values-by-chain [{:chain-id 1} {:chain-id 10} {:chain-id 42161}] + to-values-by-chain [{:chain-id 1} {:chain-id 10} {:chain-id 42161}] + expected [{:from-chain-id 1 :to-chain-id 1 :position-diff 0}] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result)))) + + (testing "Handles empty route" + (let [route [] + from-values-by-chain [] + to-values-by-chain [] + expected [] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result)))) + + (testing "Verifies negative position differences" + (let [route [{:from {:chain-id 1} :to {:chain-id 42161}}] + from-values-by-chain [{:chain-id 1} {:chain-id 10} {:chain-id 42161}] + to-values-by-chain [{:chain-id 1} {:chain-id 10} {:chain-id 42161}] + expected [{:from-chain-id 1 :to-chain-id 42161 :position-diff -2}] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result))))) diff --git a/src/status_im/contexts/wallet/sheets/account_options/style.cljs b/src/status_im/contexts/wallet/sheets/account_options/style.cljs new file mode 100644 index 00000000000..6b6cd276c5d --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/account_options/style.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.wallet.sheets.account-options.style) + +(def drawer-section-label + {:padding-horizontal 20 + :padding-top 12 + :padding-bottom 8}) + +(def options-container + {:position :absolute + :top 0 + :left 0 + :right 0 + :z-index 1 + :overflow :hidden}) + +(defn blur-container + [height] + {:height height + :position :absolute + :top 0 + :left 0 + :right 0}) + +(def divider-label + {:margin-top 8}) + +(defn list-container + [padding-top] + {:padding-top padding-top + :margin-horizontal 8}) diff --git a/src/status_im/contexts/wallet/sheets/account_options/view.cljs b/src/status_im/contexts/wallet/sheets/account_options/view.cljs new file mode 100644 index 00000000000..dc401e5740e --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/account_options/view.cljs @@ -0,0 +1,122 @@ +(ns status-im.contexts.wallet.sheets.account-options.view + (:require [clojure.string :as string] + [oops.core :as oops] + quo.context + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.platform :as platform] + [reagent.core :as reagent] + [status-im.contexts.wallet.sheets.account-options.style :as style] + [status-im.contexts.wallet.sheets.remove-account.view :as remove-account] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- render-account-item + [{:keys [name emoji color address watch-only?]}] + [quo/account-item + {:account-props (cond-> {:name name + :emoji emoji + :customization-color color + :address address} + + watch-only? + (assoc :type :watch-only)) + :title-icon (when watch-only? :i/reveal) + :on-press (fn [] + (rf/dispatch [:wallet/switch-current-viewing-account address]) + (rf/dispatch [:hide-bottom-sheet]))}]) + +(defn- options + [{:keys [theme show-account-selector? options-height]}] + (let [{:keys [name color emoji address watch-only? + default-account?]} (rf/sub [:wallet/current-viewing-account]) + {:keys [derived-from]} (rf/sub [:wallet/current-viewing-account-keypair]) + share-title (i18n/label :t/share-address-title {:address name}) + deletable? (or watch-only? + (not (or default-account? (string/blank? derived-from))))] + [rn/view + {:on-layout #(reset! options-height (oops/oget % "nativeEvent.layout.height")) + :style (when show-account-selector? style/options-container)} + (when show-account-selector? + [quo/blur + {:style (style/blur-container @options-height) + :blur-radius (if platform/android? 20 10) + :blur-amount (if platform/ios? 20 10) + :blur-type (if (= theme :light) (if platform/ios? :light :xlight) :dark) + :overlay-color (if (= theme :light) colors/white-70-blur colors/neutral-95-opa-70-blur)}]) + [quo/gradient-cover + {:customization-color color + :opacity 0.4}] + [quo/drawer-bar] + [quo/drawer-top + (cond-> {:title name + :type :account + :description address + :account-avatar-emoji emoji + :customization-color color} + + watch-only? + (assoc :title-icon :i/reveal + :account-avatar-type :watch-only))] + [quo/action-drawer + [[{:icon :i/edit + :accessibility-label :edit + :label (i18n/label :t/edit-account) + :on-press #(rf/dispatch [:navigate-to :screen/wallet.edit-account])} + {:icon :i/copy + :accessibility-label :copy-address + :label (i18n/label :t/copy-address) + :on-press (fn [] + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label :t/address-copied)}]) + (clipboard/set-string address))} + {:icon :i/qr-code + :accessibility-label :show-address-qr + :label (i18n/label :t/show-address-qr) + :on-press #(rf/dispatch [:open-modal :screen/wallet.share-address {:status :share}])} + {:icon :i/share + :accessibility-label :share-account + :label (i18n/label :t/share-address) + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (js/setTimeout + #(rf/dispatch [:wallet/share-account + {:title share-title :content address}]) + 600))} + (when deletable? + {:add-divider? (not show-account-selector?) + :icon :i/delete + :accessibility-label :remove-account + :label (i18n/label :t/remove-account) + :danger? true + :on-press #(rf/dispatch [:show-bottom-sheet + {:content remove-account/view}])})]]] + (when show-account-selector? + [:<> + [quo/divider-line {:container-style style/divider-label}] + [quo/section-label + {:section (i18n/label :t/select-another-account) + :container-style style/drawer-section-label}]])])) + +(defn view + [] + (let [options-height (reagent/atom 0)] + (fn [] + (let [theme (quo.context/use-theme) + accounts (rf/sub [:wallet/operable-accounts-without-current-viewing-account]) + show-account-selector? (pos? (count accounts))] + [:<> + (when show-account-selector? + [gesture/flat-list + {:data accounts + :render-fn render-account-item + :content-container-style (style/list-container @options-height) + :shows-vertical-scroll-indicator false}]) + [options + {:show-account-selector? show-account-selector? + :theme theme + :options-height options-height}]])))) diff --git a/src/status_im/contexts/wallet/sheets/account_origin/style.cljs b/src/status_im/contexts/wallet/sheets/account_origin/style.cljs new file mode 100644 index 00000000000..58253e83f36 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/account_origin/style.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.wallet.sheets.account-origin.style + (:require [react-native.platform :as platform])) + +(def header-container + {:gap 2 + :padding-horizontal 20 + :padding-top 12 + :padding-bottom 6}) + +(def desc-container + {:gap 2 + :padding-horizontal 20 + :padding-top 0 + :padding-bottom 8}) + +(def action-container + {:padding-horizontal 20 + :padding-top 21 + :padding-bottom (if platform/ios? 14 24) + :align-self :flex-start + :justify-content :center}) diff --git a/src/status_im/contexts/wallet/sheets/account_origin/view.cljs b/src/status_im/contexts/wallet/sheets/account_origin/view.cljs new file mode 100644 index 00000000000..42ecde462da --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/account_origin/view.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.wallet.sheets.account-origin.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as const] + [status-im.contexts.wallet.sheets.account-origin.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- header + [text] + [quo/text + {:weight :semi-bold + :style style/header-container} + text]) + +(defn- description + [text] + [quo/text + {:size :paragraph-2 + :style style/desc-container} + text]) + +(defn- section + [title desc] + [:<> + [header title] + [description desc]]) + +(defn view + [] + [rn/view + [quo/drawer-top {:title (i18n/label :t/account-origin-header)}] + [description (i18n/label :t/account-origin-desc)] + [section (i18n/label :t/origin-header) (i18n/label :t/origin-desc)] + [section + (i18n/label :t/derivation-path-header) + (i18n/label :t/derivation-path-desc)] + [quo/button + {:type :outline + :size 24 + :icon-left :i/info + :container-style style/action-container + :on-press #(rf/dispatch [:open-url const/create-account-link])} + (i18n/label :t/read-more)]]) diff --git a/src/status_im/contexts/wallet/sheets/buy_network_selection/style.cljs b/src/status_im/contexts/wallet/sheets/buy_network_selection/style.cljs new file mode 100644 index 00000000000..df40dc502d0 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/buy_network_selection/style.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.wallet.sheets.buy-network-selection.style) + +(defn network-list-container + [mainnet?] + {:margin-horizontal 8 + :padding-vertical (when mainnet? 8)}) + +(def header-container + {:height 62 + :padding-horizontal 20}) + +(def context-tag + {:margin-top 0}) + +(def divider-label + {:padding-top 0 + :padding-bottom 0}) + +(def subheader-container + {:flex-direction :row + :align-items :center + :margin-top 4}) + +(def on + {:margin-right 4}) + +(def to + {:margin-horizontal 4}) diff --git a/src/status_im/contexts/wallet/sheets/buy_network_selection/view.cljs b/src/status_im/contexts/wallet/sheets/buy_network_selection/view.cljs new file mode 100644 index 00000000000..65c87a4cb50 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/buy_network_selection/view.cljs @@ -0,0 +1,76 @@ +(ns status-im.contexts.wallet.sheets.buy-network-selection.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.networks.core :as networks] + [status-im.contexts.wallet.sheets.buy-network-selection.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- network-item + [{:keys [network on-select-network]}] + (let [{:keys [source full-name]} network + mainnet? (networks/eth-mainnet? network)] + [quo/network-list + {:label full-name + :network-image source + :on-press #(on-select-network network) + :container-style (style/network-list-container mainnet?)}])) + +(defn- subheader + [{:keys [provider account]}] + [rn/view {:style style/subheader-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/on} + (i18n/label :t/on-capitalized)] + [quo/context-tag + {:type :network + :network-logo (:logo-url provider) + :network-name (:name provider) + :size 24 + :container-style style/context-tag}] + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/to} + (i18n/label :t/to)] + [quo/context-tag + {:type :account + :account-name (:name account) + :emoji (:emoji account) + :size 24 + :container-style style/context-tag}]]) + +(defn view + [] + (let [provider (rf/sub [:wallet/wallet-buy-crypto-provider]) + account (rf/sub [:wallet/wallet-buy-crypto-account]) + mainnet-network (rf/sub [:wallet/eth-mainnet-network]) + layer-2-networks (rf/sub [:wallet/layer-2-networks]) + render-fn (rn/use-callback (fn [network] + [network-item + {:network network + :on-select-network + (fn [network] + (rf/dispatch [:wallet.buy-crypto/select-network + network]))}]))] + [:<> + [rn/view {:style style/header-container} + [quo/text + {:size :heading-2 + :weight :semi-bold} + (i18n/label :t/select-network-for-buying)] + [subheader + {:provider provider + :account account}]] + (when mainnet-network + [network-item + {:network mainnet-network + :on-select-network #(rf/dispatch [:wallet.buy-crypto/select-network mainnet-network])}]) + [quo/divider-label {:container-style style/divider-label} + (i18n/label :t/layer-2)] + [rn/flat-list + {:data (vec layer-2-networks) + :render-fn render-fn + :scroll-enabled false}]])) diff --git a/src/status_im/contexts/wallet/sheets/buy_token/style.cljs b/src/status_im/contexts/wallet/sheets/buy_token/style.cljs new file mode 100644 index 00000000000..966f59af0eb --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/buy_token/style.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.wallet.sheets.buy-token.style) + +(defn list-container + [min-height] + {:padding-horizontal 8 + :min-height min-height + :padding-bottom 8}) + +(def tabs + {:margin-horizontal 20 + :margin-top 8 + :margin-bottom 12}) diff --git a/src/status_im/contexts/wallet/sheets/buy_token/view.cljs b/src/status_im/contexts/wallet/sheets/buy_token/view.cljs new file mode 100644 index 00000000000..4be8f9ed267 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/buy_token/view.cljs @@ -0,0 +1,81 @@ +(ns status-im.contexts.wallet.sheets.buy-token.view + (:require [oops.core :as oops] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.sheets.buy-token.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- crypto-on-ramp-item + [{:keys [name fees logo-url site-url recurrent-site-url urls-need-parameters] + :as provider} + _ + _ + {:keys [account tab]}] + (let [open-url (rn/use-callback + (fn [] + (if urls-need-parameters + (rf/dispatch [:wallet.buy-crypto/select-provider + {:account account + :provider provider + :recurrent? (= tab :recurrent)}]) + (rn/open-url (if (= tab :recurrent) recurrent-site-url site-url)))) + [site-url recurrent-site-url tab])] + [quo/settings-item + {:title name + :tag :context + :tag-props {:icon :i/fees + :context fees} + :action :arrow + :action-props {:alignment :flex-start + :icon :i/external} + :image :icon-avatar + :image-props {:icon logo-url} + :on-press open-url}])) + +(def ^:private tabs + [{:id :one-time + :label (i18n/label :t/one-time)} + {:id :recurrent + :label (i18n/label :t/recurrent)}]) + +(def ^:private initial-tab (:id (first tabs))) + +(defn view + [{:keys [title]}] + (rn/use-mount (fn [] + (rf/dispatch [:wallet/get-crypto-on-ramps]))) + (let [crypto-on-ramps (rf/sub [:wallet/crypto-on-ramps]) + account (rf/sub [:wallet/current-viewing-account-or-default]) + has-recurrent? (seq (:recurrent crypto-on-ramps)) + [selected-tab set-selected-tab] (rn/use-state initial-tab) + [min-height set-min-height] (rn/use-state 0) + on-layout (rn/use-callback + #(set-min-height + (oops/oget % :nativeEvent :layout :height)))] + [:<> + [quo/drawer-top {:title (or title (i18n/label :t/ways-to-buy-assets))}] + (when has-recurrent? + [:<> + [quo/segmented-control + {:size 32 + :container-style style/tabs + :default-active initial-tab + :on-change set-selected-tab + :data tabs}] + [rn/flat-list + {:data (if (= selected-tab :recurrent) + (:recurrent crypto-on-ramps) + (:one-time crypto-on-ramps)) + :on-layout on-layout + :style (style/list-container min-height) + :render-data {:tab selected-tab + :account account} + :render-fn crypto-on-ramp-item}]]) + (when-not has-recurrent? + [rn/flat-list + {:data (:one-time crypto-on-ramps) + :on-layout on-layout + :style (style/list-container min-height) + :render-data {:account account} + :render-fn crypto-on-ramp-item}])])) diff --git a/src/status_im/contexts/wallet/sheets/missing_keypair/style.cljs b/src/status_im/contexts/wallet/sheets/missing_keypair/style.cljs new file mode 100644 index 00000000000..1fd58e94102 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/missing_keypair/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.wallet.sheets.missing-keypair.style) + +(def description-container + {:padding-horizontal 20 + :padding-top 4 + :padding-bottom 12}) diff --git a/src/status_im/contexts/wallet/sheets/missing_keypair/view.cljs b/src/status_im/contexts/wallet/sheets/missing_keypair/view.cljs new file mode 100644 index 00000000000..ce7f728dc08 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/missing_keypair/view.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.wallet.sheets.missing-keypair.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.sheets.missing-keypair.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [{:keys [name emoji color type] :as _account} keypair] + (let [customization-color (rf/sub [:profile/customization-color])] + [:<> + [quo/drawer-top + {:title (i18n/label :t/import-keypair-to-use-account) + :type :context-tag + :context-tag-type :account + :account-name name + :emoji emoji + :customization-color color}] + [rn/view {:style style/description-container} + [quo/text {:weight :medium} + (i18n/label :t/import-keypair-steps + {:account-name name + :keypair-name (:name keypair)})]] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/import-key-pair) + :button-one-props {:on-press + (fn [] + (case type + :seed + (rf/dispatch [:navigate-to + :screen/settings.missing-keypair.import-seed-phrase keypair]) + :key + (rf/dispatch [:navigate-to + :screen/settings.missing-keypair-import-private-key keypair]) + nil)) + :customization-color customization-color} + :button-two-label (i18n/label :t/not-now) + :button-two-props {:on-press #(rf/dispatch [:hide-bottom-sheet]) + :type :grey}}]])) diff --git a/src/status_im/contexts/wallet/sheets/network_filter/network_field.cljs b/src/status_im/contexts/wallet/sheets/network_filter/network_field.cljs new file mode 100644 index 00000000000..5c7445a7fae --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/network_filter/network_field.cljs @@ -0,0 +1,75 @@ +(ns status-im.contexts.wallet.sheets.network-filter.network-field + (:require [clojure.string :as string] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.contexts.wallet.common.fiat-text.view :as fiat-text] + [status-im.contexts.wallet.sheets.network-filter.style :as style] + [utils.i18n :as i18n])) + +(defn- icon + [{:keys [source]}] + [rn/view {:style style/item-icon} + [quo/icon-avatar + {:icon source + :size :size-20}]]) + +(defn- new-tag + [] + [quo/new-feature-gradient {:style style/new-gradient} + [quo/text + {:weight :semi-bold + :size :label + :style {:color colors/white}} + (string/upper-case (i18n/label :t/new))]]) + +(defn- collectibles-count + [{:keys [amount]}] + [rn/view + {:style {:flex-direction :row + :align-items :center}} + [quo/icon :i/nft + {:size 16 + :color colors/neutral-50}] + [quo/text + {:size :paragraph-2 + :style {:margin-left 4} + :color colors/neutral-80-opa-95} amount]]) + +(defn- network-balances + [{:keys [balance n-collectibles]}] + (let [collectibles? (not (zero? n-collectibles))] + [rn/view {:style style/item-balances-container} + [fiat-text/view + {:amount balance + :color colors/neutral-80-opa-95 + :size :paragraph-2}] + (when collectibles? + [:<> + [quo/dot-separator] + [collectibles-count {:amount n-collectibles}]])])) + +(defn view + [{:keys [image-source title balance n-collectibles on-press new? network-toggled? disabled? + customization-color]}] + [rn/pressable + {:style style/item-container + :on-press on-press + :accessibility-label :network-item} + [rn/view {:style style/item-left-side} + [icon {:source image-source}] + [rn/view {:style {:margin-left 12}} + [rn/view {:style style/item-title-container} + [quo/text {:weight :medium} title] + (when new? [new-tag])] + [network-balances + {:balance balance + :n-collectibles n-collectibles}]]] + [rn/view {:style style/action-icon-container} + [quo/selectors + {:type :checkbox + :customization-color customization-color + :on-change on-press + :default-checked? true + :checked? network-toggled? + :disabled? disabled?}]]]) diff --git a/src/status_im/contexts/wallet/sheets/network_filter/style.cljs b/src/status_im/contexts/wallet/sheets/network_filter/style.cljs new file mode 100644 index 00000000000..8b06064d7f1 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/network_filter/style.cljs @@ -0,0 +1,40 @@ +(ns status-im.contexts.wallet.sheets.network-filter.style) + +(def header-container + {:padding-bottom 4 + :flex-direction :row + :justify-content :space-between + :align-items :center}) + +(def item-container + {:padding-horizontal 12 + :padding-top 12 + :padding-bottom 14 + :flex-direction :row + :justify-content :space-between}) + +(def item-icon + {:padding-top 1}) + +(def item-left-side + {:flex 1 + :justify-content :flex-start + :flex-direction :row}) + +(def action-icon-container + {:flex-direction :row + :padding-right 0.5 + :align-items :center}) + +(def new-gradient + {:border-radius 12 + :height 16 + :padding-horizontal 4 + :margin-horizontal 4}) + +(def item-title-container + {:flex-direction :row :align-items :center}) + +(def item-balances-container + {:flex-direction :row + :height 18}) diff --git a/src/status_im/contexts/wallet/sheets/network_filter/view.cljs b/src/status_im/contexts/wallet/sheets/network_filter/view.cljs new file mode 100644 index 00000000000..7b67fb56753 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/network_filter/view.cljs @@ -0,0 +1,62 @@ +(ns status-im.contexts.wallet.sheets.network-filter.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.networks.config :as networks.config] + [status-im.contexts.wallet.networks.core :as networks] + [status-im.contexts.wallet.sheets.network-filter.network-field :as network-field] + [status-im.contexts.wallet.sheets.network-filter.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-network-press + [chain-id] + (rf/dispatch [:wallet/filter-network-balances {:by-id chain-id}])) + +(defn render-network + [chain-id] + (let [customization-color (rf/sub [:profile/customization-color]) + {:keys [chain-id full-name source]} (rf/sub [:wallet/network-by-id chain-id]) + network-toggled? (rf/sub [:wallet/network-filter-toggled? chain-id]) + disabled? (rf/sub [:wallet/disable-network-filter? chain-id]) + network-balance (rf/sub [:wallet/balance-for-network-filter chain-id]) + n-collectibles (rf/sub [:wallet/collectibles-count-for-network-filter + chain-id])] + [network-field/view + {:title full-name + :network-toggled? network-toggled? + :disabled? disabled? + :image-source source + :balance network-balance + :n-collectibles n-collectibles + :on-press (when-not disabled? #(on-network-press chain-id)) + :customization-color customization-color + :new? (networks/new-network? chain-id)}])) + +(defn view + [] + (let [active-chain-ids (rf/sub [:wallet/active-chain-ids]) + banner-network (rf/sub [:wallet/network-by-id + networks.config/chain-id-for-new-network-banner])] + [rn/view {:style {:padding-horizontal 20}} + [rn/view {:style style/header-container} + [quo/text + {:size :heading-2 + :weight :semi-bold} + (i18n/label :t/show-network-balances)]] + [quo/item-list + {:data active-chain-ids + :render-fn render-network + :container-style {:margin-top 12}}] + [quo/information-box + {:type :default + :icon :i/info + :style {:margin-top 12}} + (i18n/label :t/new-network-info {:network (:full-name banner-network)})] + [quo/button + {:type :outline + :container-style {:margin-vertical 12} + :size 40 + :icon-left :i/settings + :on-press #(rf/dispatch [:open-modal :screen/settings.network-settings])} + [quo/text {:weight :medium} (i18n/label :t/manage-networks)]]])) diff --git a/src/status_im/contexts/wallet/sheets/network_selection/style.cljs b/src/status_im/contexts/wallet/sheets/network_selection/style.cljs new file mode 100644 index 00000000000..0951d97251f --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/network_selection/style.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.wallet.sheets.network-selection.style) + +(def network-list-container + {:margin-horizontal 8}) + +(def header-container + {:height 62 + :padding-horizontal 20}) + +(def context-tag + {:margin-top 4}) + +(def divider-label + {:padding-top 0 + :padding-bottom 0}) diff --git a/src/status_im/contexts/wallet/sheets/network_selection/view.cljs b/src/status_im/contexts/wallet/sheets/network_selection/view.cljs new file mode 100644 index 00000000000..47547d8cdb7 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/network_selection/view.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.wallet.sheets.network-selection.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.sheets.network-selection.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- network-item + [{:keys [network on-select-network source]}] + (let [{:keys [full-name chain-id]} network + {balance-in-crypto :crypto + balance-in-fiat :fiat} (if (= source :swap) + (rf/sub [:wallet/swap-asset-to-pay-network-balance + chain-id]) + (rf/sub [:wallet/send-token-network-balance + chain-id]))] + [quo/network-list + {:label full-name + :network-image (:source network) + :token-value balance-in-crypto + :fiat-value balance-in-fiat + :on-press #(on-select-network network) + :container-style style/network-list-container}])) + +(defn view + [{:keys [token-symbol on-select-network source title] + :or {source :swap + title (i18n/label :t/select-network)}}] + (let [{token-networks :networks} (case source + :swap (rf/sub [:wallet/swap-asset-to-pay]) + :bridge (rf/sub [:wallet/bridge-token]) + (rf/sub [:wallet/wallet-send-token])) + render-fn (rn/use-callback (fn [network] + [network-item + {:network network + :on-select-network on-select-network + :source source}]))] + [:<> + [rn/view {:style style/header-container} + [quo/text + {:size :heading-2 + :weight :semi-bold} + title] + [quo/context-tag + {:type :token + :size 24 + :token token-symbol + :container-style style/context-tag}]] + [rn/flat-list + {:data (vec token-networks) + :render-fn render-fn + :scroll-enabled false}]])) diff --git a/src/status_im/contexts/wallet/sheets/remove_account/style.cljs b/src/status_im/contexts/wallet/sheets/remove_account/style.cljs new file mode 100644 index 00000000000..e0b9930ebd6 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/remove_account/style.cljs @@ -0,0 +1,17 @@ +(ns status-im.contexts.wallet.sheets.remove-account.style) + +(def desc-container + {:padding-horizontal 20 + :padding-top 4 + :padding-bottom 12}) + +(def copy-container + {:margin-horizontal 20 + :margin-top 4 + :margin-bottom 8}) + +(def checkbox-container + {:flex-direction :row + :padding-horizontal 20 + :padding-vertical 8 + :gap 10}) diff --git a/src/status_im/contexts/wallet/sheets/remove_account/view.cljs b/src/status_im/contexts/wallet/sheets/remove_account/view.cljs new file mode 100644 index 00000000000..1dfa8f44c7c --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/remove_account/view.cljs @@ -0,0 +1,116 @@ +(ns status-im.contexts.wallet.sheets.remove-account.view + (:require + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.sheets.remove-account.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- footer + [{:keys [address submit-disabled? toast-message]}] + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/remove) + :button-one-props {:on-press + (fn [] + (rf/dispatch [:wallet/remove-account + {:address address + :toast-message toast-message}])) + :type :danger + :disabled? submit-disabled?} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:on-press #(rf/dispatch [:hide-bottom-sheet]) + :type :grey}}]) + +(defn- recovery-phase-flow + [] + (let [confirmed? (reagent/atom false)] + (fn [{:keys [address name emoji path color] :as _account}] + (let [formatted-path (utils/format-derivation-path path)] + [:<> + [quo/drawer-top + {:title (i18n/label :t/remove-account-title) + :type :context-tag + :context-tag-type :account + :account-name name + :emoji emoji + :customization-color color}] + [rn/view {:style style/desc-container} + [quo/text {:weight :medium} + (i18n/label :t/remove-account-desc)]] + [quo/data-item + {:size :default + :status :default + :card? true + :title (i18n/label :t/derivation-path) + :custom-subtitle (fn [] + [quo/text {:weight :medium} + formatted-path]) + :right-icon :i/copy + :on-press (fn [] + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label :t/derivation-path-copied)}]) + (clipboard/set-string formatted-path)) + :container-style style/copy-container}] + [rn/pressable + {:style style/checkbox-container + :on-press #(swap! confirmed? not)} + [quo/selectors + {:type :checkbox + :customization-color color + :checked? @confirmed? + :on-change #(swap! confirmed? not)}] + [quo/text (i18n/label :t/remove-account-confirmation)]] + [footer + {:submit-disabled? (not @confirmed?) + :address address + :toast-message (i18n/label :t/account-removed)}]])))) + +(defn- watched-address-flow + [{:keys [address name emoji color] :as _account}] + [:<> + [quo/drawer-top + {:title (i18n/label :t/remove-watched-address-title) + :type :context-tag + :context-tag-type :account + :account-name name + :emoji emoji + :customization-color color}] + [rn/view {:style style/desc-container} + [quo/text {:weight :medium} + (i18n/label :t/remove-watched-address-desc)]] + [footer + {:submit-disabled? false + :address address + :toast-message (i18n/label :t/watched-account-removed)}]]) + +(defn- private-key-address-flow + [{:keys [address name emoji color] :as _account}] + [:<> + [quo/drawer-top + {:title (i18n/label :t/remove-account-title) + :type :context-tag + :context-tag-type :account + :account-name name + :emoji emoji + :customization-color color}] + [rn/view {:style style/desc-container} + [quo/text {:weight :medium} + (i18n/label :t/remove-private-key-address-desc)]] + [footer + {:submit-disabled? false + :address address + :toast-message (i18n/label :t/account-removed)}]]) + +(defn view + [] + (let [{account-type :type :as account} (rf/sub [:wallet/current-viewing-account])] + (case account-type + (:generated :seed) [recovery-phase-flow account] + :watch [watched-address-flow account] + :key [private-key-address-flow account] + nil))) diff --git a/src/status_im/contexts/wallet/sheets/select_account/style.cljs b/src/status_im/contexts/wallet/sheets/select_account/style.cljs new file mode 100644 index 00000000000..bca762dadd2 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/select_account/style.cljs @@ -0,0 +1,4 @@ +(ns status-im.contexts.wallet.sheets.select-account.style) + +(def list-container + {:margin-horizontal 8}) diff --git a/src/status_im/contexts/wallet/sheets/select_account/view.cljs b/src/status_im/contexts/wallet/sheets/select_account/view.cljs new file mode 100644 index 00000000000..de35be13da0 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/select_account/view.cljs @@ -0,0 +1,65 @@ +(ns status-im.contexts.wallet.sheets.select-account.view + (:require quo.context + [quo.core :as quo] + [react-native.gesture :as gesture] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.sheets.select-account.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def ^:private rounding-decimals 4) + +(defn list-item + [{:keys [account selected-account-address]}] + (let [{:keys [color address]} account] + [quo/account-item + {:type :default + :account-props (assoc account :customization-color color) + :customization-color color + :state (if (= address selected-account-address) :selected :default) + :on-press (fn [] + (rf/dispatch [:wallet/switch-current-viewing-account address]) + (rf/dispatch [:hide-bottom-sheet]))}])) + +(defn list-item-with-balance + [{:keys [account selected-account-address asset-symbol network]}] + (let [{:keys [color address tokens]} account + token (->> tokens + (filter #(= (:symbol %) asset-symbol)) + first) + chain-id (:chain-id network) + token-balance-display (utils/token-balance-display-for-network token + chain-id + rounding-decimals)] + [quo/account-item + {:type (if (= address selected-account-address) :default :tag) + :token-props {:symbol asset-symbol + :value token-balance-display} + :account-props (assoc account :customization-color color) + :customization-color color + :state (if (= address selected-account-address) :selected :default) + :on-press (fn [] + (rf/dispatch [:wallet/switch-current-viewing-account address]) + (rf/dispatch [:hide-bottom-sheet]))}])) + +(defn view + [{:keys [show-account-balances? asset-symbol network]}] + (let [selected-account-address (rf/sub [:wallet/current-viewing-account-address]) + accounts (rf/sub [:wallet/operable-accounts])] + [:<> + [quo/drawer-top {:title (i18n/label :t/select-account)}] + [gesture/flat-list + {:data accounts + :render-fn (fn [account _ _ {:keys [selected-account-address]}] + (if show-account-balances? + [list-item-with-balance + {:account account + :selected-account-address selected-account-address + :asset-symbol asset-symbol + :network network}] + [list-item + {:account account + :selected-account-address selected-account-address}])) + :render-data {:selected-account-address selected-account-address} + :content-container-style style/list-container + :shows-vertical-scroll-indicator false}]])) diff --git a/src/status_im/contexts/wallet/sheets/select_asset/asset_list/style.cljs b/src/status_im/contexts/wallet/sheets/select_asset/asset_list/style.cljs new file mode 100644 index 00000000000..cf0571e393b --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/select_asset/asset_list/style.cljs @@ -0,0 +1 @@ +(ns status-im.contexts.wallet.sheets.select-asset.asset-list.style) diff --git a/src/status_im/contexts/wallet/sheets/select_asset/asset_list/view.cljs b/src/status_im/contexts/wallet/sheets/select_asset/asset_list/view.cljs new file mode 100644 index 00000000000..7dfba0ee95f --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/select_asset/asset_list/view.cljs @@ -0,0 +1,115 @@ +(ns status-im.contexts.wallet.sheets.select-asset.asset-list.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.safe-area :as safe-area] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils :as utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- my-token-component + [{token-symbol :symbol + token-name :name + total-balance :total-balance + bridge-disabled? :bridge-disabled? + :as token} + {:keys [currency currency-symbol on-token-press disable-token-fn preselected-token-symbol]}] + (let [prices-per-token (rf/sub [:wallet/prices-per-token]) + fiat-value (utils/calculate-token-fiat-value + {:currency currency + :balance total-balance + :token token + :prices-per-token prices-per-token}) + crypto-formatted (utils/get-standard-crypto-format token total-balance prices-per-token) + fiat-formatted (utils/fiat-formatted-for-ui currency-symbol fiat-value)] + [rn/view {:style {:padding-horizontal 8}} + [quo/token-network + {:token token-symbol + :label token-name + :token-value (str crypto-formatted " " token-symbol) + :fiat-value fiat-formatted + :networks (seq (:networks token)) + :on-press #(on-token-press token) + :state (cond + (or bridge-disabled? + (when disable-token-fn + (disable-token-fn constants/swap-tokens-my token))) + :disabled + + (= preselected-token-symbol token-symbol) + :selected)}]])) + +(defn- popular-token-component + [{token-symbol :symbol + token-name :name + :as token} + {:keys [on-token-press disable-token-fn]}] + [rn/view {:style {:padding-horizontal 8}} + [quo/token-info + {:token token-symbol + :label token-name + :state (when (and disable-token-fn (disable-token-fn constants/swap-tokens-popular token)) + :disabled) + :on-press #(on-token-press token)}]]) + +(defn- list-component + [{:keys [type] :as token} _ _ render-data] + (if (= type constants/swap-tokens-popular) + [popular-token-component token render-data] + [my-token-component token render-data])) + +(defn section-header + [{:keys [title]}] + [quo/divider-label + {:container-style {:padding-horizontal 20 + :margin-top 12 + :margin-bottom 8} + :tight? false} + title]) + +(defn view + [{:keys [search-text on-token-press preselected-token-symbol network hide-token-fn + disable-token-fn]}] + (let [{:keys [bottom]} safe-area/insets + my-tokens (rf/sub [:wallet/current-viewing-account-tokens-filtered + {:query search-text + :chain-ids #{(:chain-id network)} + :hide-token-fn hide-token-fn}]) + popular-tokens (rf/sub [:wallet/tokens-filtered + {:query search-text + :chain-ids #{(:chain-id network)} + :hide-token-fn hide-token-fn}]) + currency (rf/sub [:profile/currency]) + currency-symbol (rf/sub [:profile/currency-symbol]) + sectioned-data (cond-> [] + (pos? (count my-tokens)) + (conj {:title (i18n/label :t/your-assets-on-network + {:network (:full-name network)}) + :data (map #(assoc % :type constants/swap-tokens-my) + my-tokens)}) + + (pos? (count popular-tokens)) + (conj {:title (i18n/label :t/popular-assets-on-network + {:network (:full-name network)}) + :data (map #(assoc % :type constants/swap-tokens-popular) + popular-tokens)})) + render-data {:currency currency + :currency-symbol currency-symbol + :on-token-press on-token-press + :preselected-token-symbol preselected-token-symbol + :disable-token-fn disable-token-fn}] + [gesture/section-list + {:data sectioned-data + :sections sectioned-data + :render-data render-data + :render-fn list-component + :sticky-section-headers-enabled false + :render-section-header-fn section-header + :style {:flex 1} + :content-container-style {:padding-bottom (+ bottom 20)} + :keyboard-should-persist-taps :handled + :key-fn (fn [{:keys [type title] :as token}] + (str (:symbol token) "-" type "-" title)) + :on-scroll-to-index-failed identity}])) diff --git a/src/status_im/contexts/wallet/sheets/select_asset/style.cljs b/src/status_im/contexts/wallet/sheets/select_asset/style.cljs new file mode 100644 index 00000000000..d021e010f56 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/select_asset/style.cljs @@ -0,0 +1,20 @@ +(ns status-im.contexts.wallet.sheets.select-asset.style) + +(defn container + [window-height] + {:flex 1 + :display :flex + :height window-height}) + +(def search-input-container + {:padding-horizontal 20 + :padding-vertical 8}) + +(def subheader-container + {:justify-content :flex-start + :align-items :center + :flex-direction :row + :flex-wrap :wrap + :gap 4 + :padding-horizontal 20 + :margin-bottom 8}) diff --git a/src/status_im/contexts/wallet/sheets/select_asset/view.cljs b/src/status_im/contexts/wallet/sheets/select_asset/view.cljs new file mode 100644 index 00000000000..9c26efdbf86 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/select_asset/view.cljs @@ -0,0 +1,109 @@ +(ns status-im.contexts.wallet.sheets.select-asset.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.sheets.select-asset.asset-list.view :as asset-list] + [status-im.contexts.wallet.sheets.select-asset.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- search-input + [search-text on-change-text] + [rn/view {:style style/search-input-container} + [quo/input + {:small? true + :placeholder (i18n/label :t/search-assets) + :icon-name :i/search + :value search-text + :on-change-text on-change-text}]]) + +(defn- assets-view + [{:keys [search-text on-change-text on-select network hide-token-fn disable-token-fn]}] + [:<> + [search-input search-text on-change-text] + [asset-list/view + {:search-text search-text + :on-token-press (fn [token] + (when on-select + (on-select token)) + (rf/dispatch [:hide-bottom-sheet])) + :network network + :hide-token-fn hide-token-fn + :disable-token-fn disable-token-fn}]]) + +(defn- account-view + [{:keys [emoji name color]}] + (let [theme (quo.context/use-theme)] + [quo/context-tag + {:theme theme + :type :account + :size 24 + :account-name name + :emoji emoji + :customization-color color}])) + +(defn- network-view + [{:keys [full-name source]}] + [quo/context-tag + {:size 24 + :network-logo source + :network-name full-name + :type :network}]) + +(defn- provider-view + [{:keys [name logo-url]}] + [quo/context-tag + {:size 24 + :network-logo logo-url + :network-name name + :type :network}]) + +(defn- subheader-view + [{:keys [provider account network]}] + [rn/view {:style style/subheader-container} + [quo/text + {:size :paragraph-2 + :weight :medium} + (i18n/label :t/select-asset-on)] + (if provider + [provider-view provider] + [account-view account]) + (when provider + [:<> + [quo/text + {:size :paragraph-2 + :weight :medium} + (i18n/label :t/to)] + [account-view account]]) + [quo/text + {:size :paragraph-2 + :weight :medium} + (i18n/label :t/select-asset-via)] + [network-view network]]) + +(defn view + [{:keys [title network provider on-select hide-token-fn disable-token-fn]}] + (let [account (rf/sub [:wallet/current-viewing-account-or-default]) + [search-text set-search-text] (rn/use-state "") + window-height (:height (rn/get-window))] + [rn/safe-area-view {:style (style/container window-height)} + [quo/page-nav + {:type :no-title + :icon-name :i/close + :on-press (fn [] + (rf/dispatch [:hide-bottom-sheet]))}] + [quo/page-top + {:title title + :title-accessibility-label :title-label}] + [subheader-view + {:account account + :network network + :provider provider}] + [assets-view + {:search-text search-text + :on-change-text #(set-search-text %) + :on-select on-select + :network network + :hide-token-fn hide-token-fn + :disable-token-fn disable-token-fn}]])) diff --git a/src/status_im/contexts/wallet/sheets/slippage_settings/style.cljs b/src/status_im/contexts/wallet/sheets/slippage_settings/style.cljs new file mode 100644 index 00000000000..b29ac5ca6ce --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/slippage_settings/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.wallet.sheets.slippage-settings.style) + +(def slippages + {:padding-horizontal 8}) + +(def info-message + {:margin-vertical 8 + :margin-horizontal 20}) + +(defn percentage-icon + [variant-colors] + {:size 20 + :color (:icon variant-colors)}) diff --git a/src/status_im/contexts/wallet/sheets/slippage_settings/view.cljs b/src/status_im/contexts/wallet/sheets/slippage_settings/view.cljs new file mode 100644 index 00000000000..3cd21210383 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/slippage_settings/view.cljs @@ -0,0 +1,154 @@ +(ns status-im.contexts.wallet.sheets.slippage-settings.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.sheets.slippage-settings.style :as style] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.number :as number] + [utils.re-frame :as rf])) + +(defn- validate-slippage + [slippage] + (let [slippage-value (number/parse-float slippage)] + (cond + (<= slippage-value 0) + {:message (i18n/label :t/slippage-should-be-more-than-0) + :type :error} + (> slippage-value constants/max-slippage) + {:message (i18n/label :t/slippage-cant-be-more-than-30) + :type :error} + (> (count (second (string/split slippage "."))) + constants/max-slippage-decimal-places) + {:message (i18n/label :t/max-2-decimals) + :type :error} + (> slippage-value constants/max-recommended-slippage) + {:message (i18n/label :t/slippage-may-be-higher-than-necessary) + :type :warning}))) + +(defn- on-cancel + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn- update-string-on-keypress + [k s] + (if (= :i/backspace k) + (subs s 0 (dec (count s))) + (str s k))) + +(defn- calculate-receive-amount + [asset-to-receive amount-out max-slippage] + (let [receive-token-decimals (:decimals asset-to-receive) + amount-out-whole-number (number/hex->whole amount-out receive-token-decimals) + amount-out-num (number/to-fixed amount-out-whole-number + receive-token-decimals) + slippage-num (-> (money/bignumber amount-out-num) + (money/mul (money/bignumber max-slippage)) + (money/div (money/bignumber 100))) + amount-out-minus-slippage (money/sub (money/bignumber amount-out-num) slippage-num) + display-decimals (min receive-token-decimals + constants/min-token-decimals-to-display) + receive-amount (if (money/above-zero? amount-out-minus-slippage) + (str (utils/sanitized-token-amount-to-display + amount-out-minus-slippage + display-decimals)) + 0)] + receive-amount)) + +(defn view + [] + (let [current-slippage (rf/sub [:wallet/swap-max-slippage]) + account-color (rf/sub [:wallet/current-viewing-account-color]) + asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) + amount-out (rf/sub [:wallet/swap-proposal-amount-out]) + [max-slippage set-max-slippage] (rn/use-state (str current-slippage)) + [error set-error] (rn/use-state nil) + [custom? set-custom?] (rn/use-state (not-any? #{current-slippage} + constants/slippages)) + receive-token-symbol (:symbol asset-to-receive) + receive-amount (calculate-receive-amount asset-to-receive + amount-out + max-slippage) + handle-slippage-change (rn/use-callback + (fn [value] + (let [new-slippage (update-string-on-keypress value + max-slippage)] + (set-max-slippage new-slippage) + (set-error (validate-slippage new-slippage)))) + [max-slippage set-max-slippage set-error]) + on-select-slippage (rn/use-callback (fn [slippage] + (set-max-slippage (str slippage)) + (set-custom? (nil? slippage)) + (when slippage (set-error nil))) + [set-max-slippage set-custom?]) + save-disabled? (rn/use-memo (fn [] + (or (= max-slippage (str current-slippage)) + (= (:type error) :error) + (and custom? + (empty? max-slippage)))) + [max-slippage current-slippage error custom?]) + on-save (rn/use-callback (fn [] + (rf/dispatch [:wallet.swap/set-max-slippage + max-slippage]) + (rf/dispatch [:hide-bottom-sheet])) + [max-slippage])] + [:<> + [quo/drawer-top + {:title (i18n/label :t/slippage-settings) + :description (i18n/label :t/slippage-settings-description)}] + [rn/view {:style style/slippages} + (map (fn [slippage] + ^{:key slippage} + [quo/drawer-action + (cond-> {:title (str slippage "%") + :on-press #(on-select-slippage slippage)} + (and (= (str slippage) max-slippage) (not custom?)) (assoc :state :selected))]) + constants/slippages) + [quo/drawer-action + (cond-> {:title (i18n/label :t/custom) + :action :input + :on-press #(on-select-slippage nil) + :input-props {:auto-focus true + :customization-color account-color + :placeholder (i18n/label :t/type-slippage) + :right-icon {:icon-name :i/percentage + :on-press identity + :style-fn style/percentage-icon} + :value max-slippage + :show-soft-input-on-focus false + :on-focus (fn [] + (when platform/android? + (rf/dispatch [:dismiss-keyboard])))}} + custom? (assoc :state :selected))]] + (when (and custom? error) + [quo/info-message + {:status (:type error) + :size :default + :container-style style/info-message + :icon :i/alert} + (:message error)]) + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/save-changes) + :button-one-props {:disabled? save-disabled? + :customization-color account-color + :on-press on-save} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:on-press on-cancel + :customization-color account-color + :type :grey} + :description :top + :context-tag-props {:size 24 + :type :token + :token receive-token-symbol + :amount receive-amount} + :description-top-text (i18n/label :t/receive-at-least)}] + (when custom? + [quo/numbered-keyboard + {:left-action :dot + :delete-key? true + :on-press handle-slippage-change + :on-delete handle-slippage-change}])])) diff --git a/src/status_im/contexts/wallet/signals.cljs b/src/status_im/contexts/wallet/signals.cljs new file mode 100644 index 00000000000..91eda36cf5f --- /dev/null +++ b/src/status_im/contexts/wallet/signals.cljs @@ -0,0 +1,125 @@ +(ns status-im.contexts.wallet.signals + (:require + [oops.core :as oops] + [status-im.constants :as constants] + [status-im.contexts.wallet.db-path :as db-path] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(rf/reg-event-fx + :wallet/pending-transaction-status-changed-received + (fn [{:keys [db]} [{:keys [message]}]] + (let [details (transforms/json->clj message) + tx-hash (:hash details) + tx-status (:status details) + status (cond + (= tx-status constants/transaction-status-success) + :confirmed + (= tx-status constants/transaction-status-pending) + :pending + (= tx-status constants/transaction-status-failed) + :failed) + {:keys [swap-approval-transaction-id]} (get-in db db-path/swap) + swap-approval-transaction? (= swap-approval-transaction-id tx-hash) + swap-transaction-ids (get-in db [:wallet :swap-transaction-ids]) + swap-transaction? (and swap-transaction-ids + (contains? swap-transaction-ids tx-hash))] + (cond-> {:db (update-in db [:wallet :transactions tx-hash] assoc :status status)} + swap-approval-transaction? + (assoc :fx + [[:dispatch + [:wallet.swap/approve-transaction-update + {:status status}]]]) + swap-transaction? + (assoc :fx + [[:dispatch + [:wallet.swap/swap-transaction-update + {:tx-hash tx-hash + :status status}]]]))))) + +(rf/reg-event-fx + :wallet/signal-received + (fn [_ [event-js]] + (let [event-type (oops/oget event-js "type") + blockNumber (oops/oget event-js "blockNumber") + accounts (oops/oget event-js "accounts")] + (log/debug "[wallet] Wallet signal received" + {:type event-type + :block-number blockNumber + :accounts accounts}) + (case event-type + "pending-transaction-status-changed" + {:fx + [[:dispatch + [:wallet/pending-transaction-status-changed-received + (transforms/js->clj event-js)]]]} + + "wallet-collectibles-ownership-update-finished" + {:fx [[:dispatch + [:wallet/collectible-ownership-update-finished + (transforms/js->clj event-js)]]]} + + "wallet-collectibles-ownership-update-finished-with-error" + {:fx [[:dispatch + [:wallet/collectible-ownership-update-finished-with-error + (transforms/js->clj event-js)]]]} + + "wallet-collectibles-data-updated" + {:fx [[:dispatch + [:wallet/collectibles-data-updated + (transforms/js->clj event-js)]]]} + + "wallet-owned-collectibles-filtering-done" + {:fx [[:dispatch + [:wallet/owned-collectibles-filtering-done + (transforms/js->clj event-js)]]]} + + "wallet-get-collectibles-details-done" + {:fx [[:dispatch + [:wallet/get-collectible-details-done + (transforms/js->clj event-js)]]]} + + "wallet-tick-reload" + {:fx [[:dispatch [:wallet/reload]]]} + + "wallet-blockchain-status-changed" + {:fx [[:dispatch-later + ;; Don't dispatch immediately because the signal may arrive as + ;; soon as the device goes offline. We need to give some time for + ;; RN to dispatch the network status update, otherwise when going + ;; offline the user will immediately see a toast saying "provider + ;; X is down". + [{:ms 500 + :dispatch [:wallet/blockchain-status-changed + (transforms/js->clj event-js)]}]]]} + + "wallet-blockchain-health-changed" + {:fx [[:dispatch [:wallet/blockchain-health-changed (transforms/js->clj event-js)]]]} + + "wallet-activity-filtering-done" + {:fx + [[:dispatch + [:wallet/activity-filtering-for-current-account-done + (transforms/js->clj event-js)]]]} + + "wallet-activity-filtering-entries-updated" + {:fx [[:dispatch + [:wallet/activities-filtering-entries-updated + (transforms/js->clj event-js)]]]} + + "wallet-activity-session-updated" + {:fx [[:dispatch + [:wallet/activities-session-updated + (transforms/js->clj event-js)]]]} + + "wallet-fetch-leaderboard-page-done" + {:dispatch [:gate.market/leaderboard-page-fetched event-js]} + + "wallet-leaderboard-page-data-updated" + {:dispatch [:gate.market/leaderboard-page-data-updated event-js]} + + "wallet-leaderboard-page-prices-updated" + {:dispatch [:gate.market/leaderboard-page-prices-updated event-js]} + + (log/debug ::unknown-wallet-event :type event-type))))) diff --git a/src/status_im/contexts/wallet/swap/events.cljs b/src/status_im/contexts/wallet/swap/events.cljs new file mode 100644 index 00000000000..b3437537c93 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/events.cljs @@ -0,0 +1,661 @@ +(ns status-im.contexts.wallet.swap.events + (:require [re-frame.core :as rf] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.data-store :as data-store] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.contexts.wallet.networks.db :as networks.db] + [status-im.contexts.wallet.send.utils :as send-utils] + [status-im.contexts.wallet.sheets.network-selection.view :as network-selection] + [status-im.contexts.wallet.swap.utils :as swap-utils] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.number :as number])) + +(rf/reg-event-fx :wallet.swap/start + (fn [{:keys [db]} [{:keys [network asset-to-receive open-new-screen? from-account] :as data}]] + (let [{:keys [wallet]} db + view-id (:view-id db) + root-screen? (or (= view-id :screen/wallet-stack) (nil? view-id)) + tokens-list (get-in db [:wallet :tokens :by-symbol]) + available-accounts (utils/get-accounts-with-token-balance (:accounts wallet) + (:asset-to-pay data)) + account (or from-account + (swap-utils/current-viewing-account wallet) + (first available-accounts)) + asset-to-pay (if (and (not from-account) (get-in data [:asset-to-pay :networks])) + (:asset-to-pay data) + (swap-utils/select-asset-to-pay-by-symbol + {:networks (networks.db/get-active-networks db) + :account account + :token-symbol (get-in data [:asset-to-pay :symbol])})) + network' (or network + (swap-utils/select-network asset-to-pay)) + default-asset-to-receive (swap-utils/get-default-asset-to-receive tokens-list + (:symbol asset-to-pay) + (:chain-id network')) + received-asset (if-not (nil? asset-to-receive) + asset-to-receive + default-asset-to-receive) + multi-account-balance? (-> available-accounts + (count) + (> 1)) + start-point (if open-new-screen? :action-menu :swap-button)] + {:db + (update-in db + db-path/swap + assoc + :asset-to-pay asset-to-pay + :asset-to-receive received-asset + :network network' + :launch-screen view-id + :start-point start-point) + :fx (if (and multi-account-balance? root-screen? (not from-account)) + [[:dispatch [:open-modal :screen/wallet.swap-select-account]]] + (if network' + [[:dispatch [:wallet/switch-current-viewing-account (:address account)]] + [:dispatch + (if open-new-screen? + [:open-modal :screen/wallet.setup-swap] + [:navigate-to-within-stack + [:screen/wallet.setup-swap :screen/wallet.swap-select-asset-to-pay]])] + [:dispatch + [:centralized-metrics/track :metric/swap-start + {:network (:chain-id network) + :pay_token (:symbol asset-to-pay) + :receive_token (:symbol received-asset) + :start_point start-point + :launch_screen view-id}]] + [:dispatch [:wallet.swap/set-default-slippage]]] + [[:dispatch + [:show-bottom-sheet + {:content (fn [] + [network-selection/view + {:token-symbol (:symbol asset-to-pay) + :on-select-network (fn [network] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:wallet.swap/start + {:asset-to-pay asset-to-pay + :asset-to-receive received-asset + :network network + :open-new-screen? + open-new-screen? + :from-account from-account}]))}])}]]]))}))) + +(rf/reg-event-fx :wallet.swap/select-asset-to-pay + (fn [{:keys [db]} [{:keys [token]}]] + (let [{:keys [asset-to-pay network]} (get-in db db-path/swap)] + {:db (update-in db + db-path/swap + #(-> % + (assoc :asset-to-pay token) + (dissoc :amount + :amount-hex + :last-request-uuid + :swap-proposal + :error-response + :loading-swap-proposal? + :approval-transaction-id + :approved-amount))) + :fx [[:dispatch + [:centralized-metrics/track :metric/swap-asset-to-pay-changed + {:network (:chain-id network) + :previous_token (:symbol asset-to-pay) + :new_token (:symbol token)}]]]}))) + +(rf/reg-event-fx :wallet.swap/select-asset-to-receive + (fn [{:keys [db]} [{:keys [token]}]] + (let [{:keys [asset-to-receive network]} (get-in db db-path/swap)] + {:db (update-in db db-path/swap assoc :asset-to-receive token) + :fx [[:dispatch + [:centralized-metrics/track :metric/swap-asset-to-receive-changed + {:network (:chain-id network) + :previous_token (:symbol asset-to-receive) + :new_token (:symbol token)}]]]}))) + +(rf/reg-event-fx :wallet.swap/set-default-slippage + (fn [{:keys [db]}] + {:db + (update-in db db-path/swap assoc :max-slippage constants/default-slippage)})) + +(rf/reg-event-fx :wallet.swap/set-max-slippage + (fn [{:keys [db]} [max-slippage]] + (let [amount (get-in db (conj db-path/swap :amount))] + {:db (update-in db db-path/swap assoc :max-slippage (number/parse-float max-slippage)) + :fx [[:dispatch [:wallet/stop-get-swap-proposal]] + [:dispatch [:wallet.swap/set-loading-swap-proposal]] + [:dispatch [:wallet/start-get-swap-proposal {:amount-in amount}]]]}))) + +(rf/reg-event-fx :wallet.swap/set-loading-swap-proposal + (fn [{:keys [db]}] + {:db (update-in db db-path/swap assoc :loading-swap-proposal? true)})) + +(defn- get-swap-proposal-params + [{:keys [db amount-in amount-out request-uuid]}] + (let [wallet-address (get-in db [:wallet :current-viewing-account-address]) + {:keys [asset-to-pay asset-to-receive + network max-slippage] + :or {max-slippage + constants/default-slippage}} (get-in db db-path/swap) + networks (networks.db/get-active-networks db) + network-chain-ids (map :chain-id networks) + pay-token-decimal (:decimals asset-to-pay) + pay-token-id (:symbol asset-to-pay) + receive-token-id (:symbol asset-to-receive) + gas-rates constants/gas-rate-medium + receive-token-decimals (:decimals asset-to-receive) + amount-in-hex (if amount-in + (send-utils/amount-in-hex amount-in pay-token-decimal) + 0) + amount-out-hex (when amount-out + (send-utils/amount-in-hex amount-out + receive-token-decimals)) + to-address wallet-address + from-address wallet-address + swap-chain-id (:chain-id network) + disabled-to-chain-ids (filter #(not= % swap-chain-id) network-chain-ids) + disabled-from-chain-ids (filter #(not= % swap-chain-id) network-chain-ids) + from-locked-amount {} + send-type constants/send-type-swap + params [(cond-> + {:uuid request-uuid + :sendType send-type + :addrFrom from-address + :addrTo to-address + :tokenID pay-token-id + :toTokenID receive-token-id + :disabledFromChainIDs disabled-from-chain-ids + :disabledToChainIDs disabled-to-chain-ids + :gasFeeMode gas-rates + :fromLockedAmount from-locked-amount + :amountOut (or amount-out-hex "0x0") + :slippagePercentage max-slippage} + amount-in (assoc :amountIn amount-in-hex))]] + params)) + +(rf/reg-event-fx :wallet/start-get-swap-proposal + (fn [{:keys [db]} [{:keys [amount-in amount-out clean-approval-transaction?]}]] + (let [{:keys [asset-to-pay asset-to-receive + network]} (get-in db db-path/swap) + pay-token-decimal (:decimals asset-to-pay) + pay-token-id (:symbol asset-to-pay) + receive-token-id (:symbol asset-to-receive) + amount-in-hex (if amount-in + (send-utils/amount-in-hex amount-in pay-token-decimal) + 0) + swap-chain-id (:chain-id network) + request-uuid (str (random-uuid)) + params (get-swap-proposal-params + {:db db + :amount-in amount-in + :amount-out amount-out + :request-uuid request-uuid})] + (when-let [amount (or amount-in amount-out)] + {:db (update-in db + db-path/swap + #(cond-> % + :always + (assoc + :last-request-uuid request-uuid + :amount amount + :amount-hex amount-in-hex + :initial-response? true) + clean-approval-transaction? + (dissoc :approval-transaction-id :approved-amount :swap-proposal))) + :fx [[:dispatch + [:centralized-metrics/track :metric/swap-proposal-start + {:network swap-chain-id + :pay_token pay-token-id + :receive_token receive-token-id}]]] + :json-rpc/call [{:method "wallet_getSuggestedRoutesAsync" + :params params + :on-error (fn [error] + (rf/dispatch [:wallet/swap-proposal-error error]) + (log/error "failed to get suggested routes (async)" + {:event :wallet/start-get-swap-proposal + :error (:message error) + :params params}))}]})))) + +(rf/reg-event-fx :wallet/swap-proposal-success + (fn [{:keys [db]} [swap-proposal]] + (let [{:keys [last-request-uuid + amount-hex + asset-to-pay + asset-to-receive + network + initial-response?]} (get-in db db-path/swap) + view-id (:view-id db) + request-uuid (:uuid swap-proposal) + best-routes (:best swap-proposal) + updated-token-prices (:updated-token-prices swap-proposal) + error-response (:error-response swap-proposal)] + (when (and (= request-uuid last-request-uuid) + (or (and (empty? best-routes) error-response) + (and + (pos? (count best-routes)) + (= (:amount-in (first best-routes)) amount-hex)))) + (cond-> {:db (update-in db + db-path/swap + assoc + :swap-proposal (when-not (empty? best-routes) + (assoc (first best-routes) :uuid request-uuid)) + :error-response error-response + :updated-token-prices updated-token-prices + :loading-swap-proposal? false + :initial-response? false)} + (and initial-response? (seq best-routes)) + (assoc :fx + [[:dispatch + [:centralized-metrics/track :metric/swap-proposal-received + {:network (:chain-id network) + :pay_token (:symbol asset-to-pay) + :receive_token (:symbol asset-to-receive)}]]]) + (and initial-response? (empty? best-routes)) + (assoc :fx + [[:dispatch + [:centralized-metrics/track :metric/swap-proposal-failed + {:error (:code error-response)}]]]) + ;; Router is unstable and it can return a swap proposal and after auto-refetching it can + ;; return an error. Ideally this shouldn't happen, but adding this behavior so if the + ;; user is in swap confirmation screen or in token approval confirmation screen, we + ;; navigate back to setup swap screen so proper error is displayed. + (and (empty? best-routes) (= view-id :screen/wallet.swap-set-spending-cap)) + (assoc :fx + [[:dispatch + [:centralized-metrics/track :metric/swap-proposal-failed + {:error (:code error-response)}]] + [:dismiss-modal :screen/wallet.swap-set-spending-cap]]) + (and (empty? best-routes) (= view-id :screen/wallet.swap-confirmation)) + (assoc :fx + [[:dispatch + [:centralized-metrics/track :metric/swap-proposal-failed + {:error (:code error-response)}]] + [:navigate-back]])))))) + +(rf/reg-event-fx :wallet/swap-proposal-error + (fn [{:keys [db]} [error-response]] + {:db (-> db + (update-in db-path/swap dissoc :route :swap-proposal) + (update-in db-path/swap + assoc + :loading-swap-proposal? false + :error-response error-response)) + :fx [[:dispatch + [:centralized-metrics/track :metric/swap-proposal-failed {:error (:code error-response)}]]]})) + +(rf/reg-event-fx :wallet/stop-get-swap-proposal + (fn [{:keys [db]}] + {:db (update-in db db-path/swap dissoc :last-request-uuid) + :json-rpc/call [{:method "wallet_stopSuggestedRoutesAsyncCalculation" + :params [] + :on-error (fn [error] + (log/error "failed to stop fetching swap proposals" + {:event :wallet/stop-get-swap-proposal + :error error}))}]})) + +(rf/reg-event-fx + :wallet/clean-swap-proposal + (fn [{:keys [db]} [{:keys [clean-amounts? clean-approval-transaction?]}]] + (let [keys-to-dissoc (cond-> [:last-request-uuid + :swap-proposal + :error-response + :loading-swap-proposal?] + clean-amounts? (conj :amount :amount-hex) + clean-approval-transaction? (conj :approval-transaction-id :approved-amount))] + {:db (apply update-in db db-path/swap dissoc keys-to-dissoc) + :fx [[:dispatch [:wallet/stop-get-swap-proposal]]]}))) + +(rf/reg-event-fx :wallet/clean-swap + (fn [{:keys [db]}] + {:db (update-in db [:wallet :ui] dissoc :swap)})) + +(rf/reg-event-fx :wallet.swap/add-authorized-transaction + (fn [{:keys [db]} [{:keys [sent-transactions swap-data approval-transaction?]}]] + (let [wallet-transactions (get-in db [:wallet :transactions] {}) + transactions (utils/transactions->hash-to-transaction-map sent-transactions) + transaction-ids (keys transactions) + transaction-id (first transaction-ids) + transaction-details (cond-> transactions + :always (assoc-in [transaction-id :tx-type] :swap) + swap-data (assoc-in [transaction-id :swap-data] swap-data)) + swap-transaction-ids (get-in db [:wallet :swap-transaction-ids])] + {:db (cond-> db + :always (assoc-in [:wallet :transactions] + (merge wallet-transactions transaction-details)) + :always (update-in db-path/swap assoc :transaction-ids transaction-ids) + approval-transaction? (update-in db-path/swap + assoc + :approval-transaction-id + transaction-id) + (not approval-transaction?) (assoc-in [:wallet :swap-transaction-ids] + (if swap-transaction-ids + (conj swap-transaction-ids transaction-id) + (hash-set transaction-id))))}))) + +(rf/reg-event-fx :wallet.swap/approve-transaction-update + (fn [{:keys [db]} [{:keys [status]}]] + (let [{:keys [amount asset-to-pay swap-proposal + network]} (get-in db db-path/swap) + provider-name (:bridge-name swap-proposal) + token-symbol (:symbol asset-to-pay) + swap-chain-id (:chain-id network) + current-viewing-account-address (get-in db + [:wallet :current-viewing-account-address]) + account-name (get-in db + [:wallet :accounts + current-viewing-account-address :name]) + transaction-confirmed-or-failed? (#{:confirmed :failed} status) + transaction-confirmed? (= status :confirmed)] + (when transaction-confirmed-or-failed? + (cond-> {:fx + [[:dispatch + [:centralized-metrics/track :metric/swap-approval-execution-finished + {:network swap-chain-id + :pay_token token-symbol + :succeeded transaction-confirmed?}]] + [:dispatch + [:toasts/upsert + {:id :approve-transaction-update + :type (if transaction-confirmed? :positive :negative) + :text (if transaction-confirmed? + (i18n/label :t/spending-cap-set + {:token-amount amount + :token-symbol token-symbol + :provider-name provider-name + :account-name account-name}) + (i18n/label :t/spending-cap-failed + {:token-amount amount + :token-symbol token-symbol + :provider-name provider-name + :account-name account-name}))}]]]} + transaction-confirmed? + (assoc :db (update-in db db-path/swap assoc :approved-amount amount)) + (not transaction-confirmed?) + (assoc :db (update-in db db-path/swap dissoc :approval-transaction-id))))))) + +(rf/reg-event-fx + :wallet.swap/swap-transaction-update + (fn [{:keys [db]} [{:keys [tx-hash status]}]] + (let [{:keys [pay-amount pay-token-symbol + receive-amount receive-token-symbol + swap-chain-id]} (get-in db + [:wallet :transactions tx-hash + :swap-data]) + transaction-confirmed-or-failed? (#{:confirmed :failed} status) + transaction-confirmed? (= status :confirmed)] + (when transaction-confirmed-or-failed? + {:db (-> db + (update-in [:wallet :swap-transaction-ids] disj tx-hash) + (update-in [:wallet :transactions] dissoc tx-hash)) + :fx [[:dispatch + [:centralized-metrics/track :metric/swap-transaction-execution-finished + {:network swap-chain-id + :pay_token pay-token-symbol + :receive_token receive-token-symbol + :succeeded transaction-confirmed?}]] + [:dispatch + [:toasts/upsert + {:id :swap-transaction-update + :type (if transaction-confirmed? :positive :negative) + :text (if transaction-confirmed? + (i18n/label :t/swapped-to + {:pay-amount pay-amount + :pay-token-symbol pay-token-symbol + :receive-token-symbol receive-token-symbol + :receive-amount receive-amount}) + (i18n/label :t/swap-failed))}]]]})))) + +(rf/reg-event-fx + :wallet.swap/flip-assets + (fn [{:keys [db]}] + (let [{:keys [asset-to-pay asset-to-receive + swap-proposal amount network]} (get-in db db-path/swap) + receive-token-decimals (:decimals asset-to-receive) + amount-out (when swap-proposal (:amount-out swap-proposal)) + receive-amount (when amount-out + (-> amount-out + (number/hex->whole receive-token-decimals) + (money/to-fixed receive-token-decimals)))] + {:db (update-in db + db-path/swap + #(-> % + (assoc + :asset-to-pay asset-to-receive + :asset-to-receive asset-to-pay + :amount (or receive-amount amount)) + (dissoc :swap-proposal + :error-response + :loading-swap-proposal? + :last-request-uuid + :approved-amount + :approval-transaction-id))) + :fx [[:dispatch + [:centralized-metrics/track :metric/swap-asset-to-pay-changed + {:network (:chain-id network) + :previous_token (:symbol asset-to-pay) + :new_token (:symbol asset-to-receive)}]] + [:dispatch + [:centralized-metrics/track :metric/swap-asset-to-receive-changed + {:network (:chain-id network) + :previous_token (:symbol asset-to-receive) + :new_token (:symbol asset-to-pay)}]]]}))) + +(rf/reg-event-fx + :wallet.swap/set-sign-transactions-callback-fx + (fn [{:keys [db]} [callback-fx]] + {:db (update-in db db-path/swap assoc :sign-transactions-callback-fx callback-fx)})) + +(rf/reg-event-fx + :wallet.swap/review-swap + (fn [{:keys [db]}] + {:db (-> db + (update-in db-path/swap dissoc :transaction-for-signing)) + :fx [[:dispatch + [:navigate-to-within-stack + [:screen/wallet.swap-confirmation + :screen/wallet.setup-swap]]]]})) + +(rf/reg-event-fx + :wallet/prepare-signatures-for-swap-transactions + (fn [{:keys [db]}] + (let [{:keys [last-request-uuid]} (get-in db db-path/swap)] + {:fx [[:dispatch + [:wallet/build-transactions-from-route + {:request-uuid last-request-uuid}]] + [:dispatch + [:wallet.swap/set-sign-transactions-callback-fx + [:dispatch [:wallet/prepare-signatures-for-transactions :swap]]]]]}))) + +(defn transaction-approval-required? + [transactions {:keys [swap-proposal approval-transaction-id]}] + (let [approval-transaction (when approval-transaction-id + (get transactions approval-transaction-id)) + already-approved? (and approval-transaction + (= (:status approval-transaction) + :confirmed))] + (and (:approval-required swap-proposal) + (not already-approved?)))) + +(rf/reg-event-fx + :wallet.swap/mark-as-pending + (fn [{:keys [db]} [transaction-id]] + {:db (-> db + (assoc-in [:wallet :transactions transaction-id :status] :pending) + (update-in db-path/swap assoc :approval-transaction-id transaction-id))})) + +(rf/reg-event-fx + :wallet.swap/transaction-success + (fn [{:keys [db]} [sent-transactions]] + (let [transactions (get-in db [:wallet :transactions]) + {:keys [swap-proposal + asset-to-pay + asset-to-receive + network + amount] + :as swap} (get-in db db-path/swap) + swap-chain-id (:chain-id network) + token-id-from (:symbol asset-to-pay) + token-id-to (:symbol asset-to-receive) + receive-token-decimals (:decimals asset-to-receive) + amount-out (:amount-out swap-proposal) + receive-amount (when amount-out + (-> amount-out + (number/hex->whole receive-token-decimals) + (money/to-fixed receive-token-decimals))) + approval-required? (transaction-approval-required? transactions swap)] + {:fx [[:dispatch + [:centralized-metrics/track + (if approval-required? + :metric/swap-approval-execution-start + :metric/swap-transaction-execution-start) + (cond-> {:network swap-chain-id + :pay_token token-id-from} + (not approval-required?) + (assoc :receive_token token-id-to))]] + [:dispatch + [:wallet.swap/add-authorized-transaction + (cond-> {:sent-transactions sent-transactions + :approval-transaction? approval-required?} + (not approval-required?) + (assoc :swap-data + {:pay-token-symbol token-id-from + :pay-amount amount + :receive-token-symbol token-id-to + :receive-amount receive-amount + :swap-chain-id swap-chain-id}))]] + (when approval-required? + ;; dismiss the spending cap dialog if the transaction needs to be approved + [:dispatch [:dismiss-modal :screen/wallet.swap-set-spending-cap]]) + (when approval-required? + [:dispatch [:wallet.swap/mark-as-pending (-> sent-transactions first :hash)]]) + (when-not approval-required? + ;; just end the whole transaction flow if no approval needed + [:dispatch [:wallet.swap/end-transaction-flow]]) + (when-not approval-required? + [:dispatch-later + {:ms 500 + :dispatch [:toasts/upsert + {:id :swap-transaction-pending + :icon :i/info + :type :neutral + :text (i18n/label :t/swapping-to + {:pay-amount amount + :pay-token-symbol token-id-from + :receive-token-symbol token-id-to + :receive-amount receive-amount})}]}])]}))) + +(rf/reg-event-fx + :wallet.swap/transaction-failure + (fn [{:keys [db]} [{:keys [details] :as error}]] + (let [transactions (get-in db [:wallet :transactions]) + {:keys [asset-to-pay + asset-to-receive + network] + :as swap} (get-in db db-path/swap) + swap-chain-id (:chain-id network) + token-id-from (:symbol asset-to-pay) + token-id-to (:symbol asset-to-receive) + approval-required? (transaction-approval-required? transactions swap)] + {:fx [[:centralized-metrics/track + (if approval-required? + :metric/swap-approval-execution-failed + :metric/swap-transaction-execution-failed) + (cond-> {:network swap-chain-id + :error error + :pay_token token-id-from} + (not approval-required?) + (assoc :receive_token token-id-to))] + [:dispatch [:wallet.swap/end-transaction-flow]] + [:dispatch + [:toasts/upsert + {:id :send-transaction-error + :type :negative + :text (or details "An error occured")}]]]}))) + +(rf/reg-event-fx + :wallet.swap/clean-up-transaction-flow + (fn [{:keys [db]}] + (let [transactions (get-in db [:wallet :transactions]) + swap (get-in db db-path/swap) + approval-required? (transaction-approval-required? transactions swap)] + {:db (update-in db [:wallet :ui] dissoc :swap) + :fx [[:dispatch + [:dismiss-modal + (if approval-required? + :screen/wallet.swap-set-spending-cap + :screen/wallet.swap-confirmation)]]]}))) + +(rf/reg-event-fx + :wallet.swap/end-transaction-flow + (fn [{:keys [db]}] + (let [address (get-in db [:wallet :current-viewing-account-address])] + {:fx [[:dispatch [:wallet/navigate-to-account-within-stack address]] + [:dispatch [:wallet/select-account-tab :activity]] + [:dispatch-later + [{:ms 20 + :dispatch [:wallet.swap/clean-up-transaction-flow]}]]]}))) + +(rf/reg-event-fx :wallet.swap/start-from-account + (fn [{:keys [db]} [account]] + (let [{:keys [asset-to-pay asset-to-receive]} (get-in db db-path/swap)] + {:fx (if asset-to-pay + [[:dispatch [:dismiss-modal :screen/wallet.swap-select-account]] + [:dispatch + [:wallet.swap/start + {:asset-to-pay asset-to-pay + :asset-to-receive asset-to-receive + :open-new-screen? true + :from-account account}]]] + [[:dispatch [:wallet/switch-current-viewing-account (:address account)]] + [:dispatch + [:navigate-to-within-stack + [:screen/wallet.swap-select-asset-to-pay :screen/wallet.swap-select-account]]]])}))) + +(rf/reg-event-fx :wallet/get-swap-proposal-fee + (fn [{:keys [db]} [{:keys [amount-in amount-out]}]] + (let [request-uuid (str (random-uuid)) + params (get-swap-proposal-params + {:db db + :amount-in amount-in + :amount-out amount-out + :request-uuid request-uuid})] + {:db (update-in db db-path/swap assoc :loading-swap-proposal-fee? true) + :json-rpc/call [{:method "wallet_getSuggestedRoutes" + :params params + :on-success (fn [data] + (let [swap-proposal (data-store/fix-routes data)] + (rf/dispatch [:wallet/swap-proposal-fee-success + swap-proposal]))) + :on-error (fn [error] + (rf/dispatch [:wallet/swap-proposal-fee-error]) + (log/error "failed to get suggested routes" + {:event :wallet/get-swap-proposal-fee + :error (:message error) + :params params}))}]}))) + +(rf/reg-event-fx + :wallet/swap-proposal-fee-success + (fn [{:keys [db]} [swap-proposal]] + (let [best-routes (:best swap-proposal) + selected-route (first best-routes) + relevant-fee-fields [:gas-amount :gas-fees :token-fees :approval-required + :approval-fee :approval-l-1-fee :bonder-fees] + fee-data (select-keys selected-route relevant-fee-fields)] + {:db (update-in db + db-path/swap + assoc + :loading-swap-proposal-fee? false + :swap-proposal + (when-not (empty? best-routes) + fee-data))}))) + +(rf/reg-event-fx + :wallet/swap-proposal-fee-error + (fn [{:keys [db]}] + {:db (update-in db + db-path/swap + assoc + :loading-swap-proposal-fee? + false)})) diff --git a/src/status_im/contexts/wallet/swap/select_account/style.cljs b/src/status_im/contexts/wallet/swap/select_account/style.cljs new file mode 100644 index 00000000000..e32271095b7 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/select_account/style.cljs @@ -0,0 +1,7 @@ +(ns status-im.contexts.wallet.swap.select-account.style) + +(def accounts-list + {:padding-bottom 12}) + +(def accounts-list-container + {:padding-horizontal 8}) diff --git a/src/status_im/contexts/wallet/swap/select_account/view.cljs b/src/status_im/contexts/wallet/swap/select_account/view.cljs new file mode 100644 index 00000000000..5e8f21738e9 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/select_account/view.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.wallet.swap.select-account.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.swap.select-account.style :as style] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) + +(defn- on-account-press + [account asset-to-pay] + (rf/dispatch [:wallet.swap/start-from-account account asset-to-pay])) + +(defn- render-fn + [item _ _ {:keys [asset-to-pay]}] + (let [has-balance (money/above-zero? (:asset-pay-balance item))] + [quo/account-item + {:type (if has-balance :tag :default) + :on-press #(on-account-press item asset-to-pay) + :state (if (or has-balance (nil? asset-to-pay)) :default :disabled) + :token-props {:symbol (:asset-pay-symbol item) + :value (:asset-pay-balance item)} + :account-props (assoc item + :address (:address item) + :full-address? true)}])) + +(defn- on-close + [] + (rf/dispatch [:wallet/clean-current-viewing-account]) + (events-helper/navigate-back)) + +(defn view + [] + (let [asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + accounts (rf/sub [:wallet/accounts-with-balances asset-to-pay])] + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:margin-top safe-area/top + :icon-name :i/close + :on-press on-close}]} + [quo/page-top + {:title (i18n/label :t/from-label) + :title-accessibility-label :title-label}] + [rn/flat-list + {:style style/accounts-list + :content-container-style style/accounts-list-container + :data accounts + :render-data {:asset-to-pay asset-to-pay} + :render-fn render-fn + :shows-horizontal-scroll-indicator false}]])) diff --git a/src/status_im/contexts/wallet/swap/select_asset_to_pay/style.cljs b/src/status_im/contexts/wallet/swap/select_asset_to_pay/style.cljs new file mode 100644 index 00000000000..3b49c33885c --- /dev/null +++ b/src/status_im/contexts/wallet/swap/select_asset_to_pay/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.wallet.swap.select-asset-to-pay.style + (:require [react-native.navigation :as navigation] + [react-native.platform :as platform])) + +(def container + {:flex 1 + :padding-top (when platform/android? (navigation/status-bar-height))}) + +(def search-input-container + {:padding-horizontal 20 + :padding-vertical 8}) diff --git a/src/status_im/contexts/wallet/swap/select_asset_to_pay/view.cljs b/src/status_im/contexts/wallet/swap/select_asset_to_pay/view.cljs new file mode 100644 index 00000000000..fe5c6b08625 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/select_asset_to_pay/view.cljs @@ -0,0 +1,48 @@ +(ns status-im.contexts.wallet.swap.select-asset-to-pay.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.common.asset-list.view :as asset-list] + [status-im.contexts.wallet.swap.select-asset-to-pay.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- search-input + [search-text on-change-text] + [rn/view {:style style/search-input-container} + [quo/input + {:small? true + :placeholder (i18n/label :t/search-assets) + :icon-name :i/search + :value search-text + :on-change-text on-change-text}]]) + +(defn- assets-view + [search-text on-change-text] + (let [on-token-press (fn [token] + (rf/dispatch [:wallet.swap/start + {:asset-to-pay {:symbol (:symbol token)} + :open-new-screen? false}]))] + [:<> + [search-input search-text on-change-text] + [asset-list/view + {:search-text search-text + :on-token-press on-token-press}]])) + +(defn view + [] + (let [[search-text set-search-text] (rn/use-state "") + on-change-text #(set-search-text %) + on-close (fn [] + (rf/dispatch [:centralized-metrics/track :metric/swap-closed]) + (rf/dispatch [:wallet/clean-swap]) + (rf/dispatch [:navigate-back]))] + [rn/safe-area-view {:style style/container} + [account-switcher/view + {:on-press on-close + :switcher-type :select-account}] + [quo/page-top + {:title (i18n/label :t/select-asset-to-pay) + :title-accessibility-label :title-label}] + [assets-view search-text on-change-text]])) diff --git a/src/status_im/contexts/wallet/swap/set_spending_cap/style.cljs b/src/status_im/contexts/wallet/swap/set_spending_cap/style.cljs new file mode 100644 index 00000000000..526289d671e --- /dev/null +++ b/src/status_im/contexts/wallet/swap/set_spending_cap/style.cljs @@ -0,0 +1,64 @@ +(ns status-im.contexts.wallet.swap.set-spending-cap.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:flex 1 + :margin-top -20}) + +(def detail-item + {:flex 1 + :min-height 36 + :background-color :transparent}) + +(def floating-button-page-content-container + {:margin-bottom 168}) + +(def scroll-view-container + {:flex 1 + :padding-bottom 20}) + +(def content-container + {:padding-top 12 + :padding-horizontal 20 + :padding-bottom 32}) + +(def title-container + {:margin-horizontal 4}) + +(def title-line-with-margin-top + {:flex-direction :row + :margin-top 4}) + +(def details-container + {:flex-direction :row + :justify-content :space-between + :min-height 52 + :padding-top 7 + :padding-horizontal 1 + :margin-bottom 8}) + +(def summary-section-container + {:padding-horizontal 20 + :padding-bottom 16}) + +(defn section-label + [theme] + {:margin-bottom 8 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def providers-container + {:align-items :center + :margin-top 12}) + +(defn swaps-powered-by + [theme] + {:color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-70 theme)}) + +(defn terms-and-conditions + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn footer-container + [theme] + {:margin-bottom -12 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) diff --git a/src/status_im/contexts/wallet/swap/set_spending_cap/view.cljs b/src/status_im/contexts/wallet/swap/set_spending_cap/view.cljs new file mode 100644 index 00000000000..7089c96b467 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/set_spending_cap/view.cljs @@ -0,0 +1,273 @@ +(ns status-im.contexts.wallet.swap.set-spending-cap.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.resources :as resources] + [react-native.core :as rn] + [status-im.common.events-helper :as events-helper] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.swap.set-spending-cap.style :as style] + [utils.address :as address-utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- swap-title + [] + (let [asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + pay-amount (rf/sub [:wallet/swap-pay-amount]) + account (rf/sub [:wallet/current-viewing-account]) + provider (rf/sub [:wallet/swap-proposal-provider]) + pay-token-symbol (:symbol asset-to-pay)] + [rn/view {:style style/content-container} + [rn/view {:style {:flex-direction :row}} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :set-spending-cap-of} + (i18n/label :t/set-spending-cap-of)]] + [rn/view {:style style/title-line-with-margin-top} + [quo/summary-tag + {:token pay-token-symbol + :label (str pay-amount " " pay-token-symbol) + :type :token}] + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :for} + (i18n/label :t/for)]] + [rn/view {:style style/title-line-with-margin-top} + [quo/summary-tag + {:label (:full-name provider) + :type :network + :image-source (resources/get-network (:name provider)) + :customization-color (:color provider)}] + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :on} + (i18n/label :t/on)]] + [rn/view {:style style/title-line-with-margin-top} + [quo/summary-tag + {:label (:name account) + :type :account + :emoji (:emoji account) + :customization-color (:color account)}]]])) + +(defn- spending-cap-section + [] + (let [theme (quo.context/use-theme) + asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + pay-amount (rf/sub [:wallet/swap-pay-amount-raw]) + pay-token-symbol (:symbol asset-to-pay)] + [rn/view {:style style/summary-section-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label :spending-cap-label} + (i18n/label :t/spending-cap)] + (when (and asset-to-pay pay-amount) + [quo/approval-info + {:type :spending-cap + :unlimited-icon? false + :label (str pay-amount " " pay-token-symbol) + :avatar-props {:token pay-token-symbol}}])])) + +(defn- account-section + [] + (let [theme (quo.context/use-theme) + asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + account (rf/sub [:wallet/current-viewing-account]) + pay-amount (rf/sub [:wallet/swap-pay-amount]) + pay-token-symbol (:symbol asset-to-pay)] + [rn/view {:style style/summary-section-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label :account-label} + (i18n/label :t/account)] + (when (and asset-to-pay pay-amount) + [quo/approval-info + {:type :account + :unlimited-icon? false + :label (:name account) + :description (address-utils/get-short-wallet-address (:address account)) + :tag-label (str pay-amount " " pay-token-symbol) + :avatar-props {:emoji (:emoji account) + :customization-color (:color account)}}])])) + +(defn- on-option-press + [{:keys [network contract-address]}] + (let [{:keys [chain-id block-explorer-name]} network] + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [quo/action-drawer + [[{:icon :i/link + :accessibility-label :view-on-block-explorer + :on-press (fn [] + (rf/dispatch + [:wallet/navigate-to-chain-explorer chain-id + contract-address])) + :label (i18n/label :t/view-address-on-block-explorer + {:block-explorer-name block-explorer-name}) + :right-icon :i/external}]]])}]))) + +(defn- token-section + [] + (let [theme (quo.context/use-theme) + asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + network (rf/sub [:wallet/swap-network]) + pay-token-symbol (:symbol asset-to-pay) + network-chain-id (:chain-id network) + pay-token-address (get-in asset-to-pay [:balances-per-chain network-chain-id :address])] + [rn/view {:style style/summary-section-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label :token-label} + (i18n/label :t/token)] + (when asset-to-pay + [quo/approval-info + {:type :token-contract + :option-icon :i/options + :on-option-press #(on-option-press {:network network + :contract-address pay-token-address}) + :unlimited-icon? false + :label pay-token-symbol + :description (address-utils/get-short-wallet-address pay-token-address) + :avatar-props {:token pay-token-symbol}}])])) + +(defn- spender-contract-section + [] + (let [theme (quo.context/use-theme) + network (rf/sub [:wallet/swap-network]) + provider (rf/sub [:wallet/swap-proposal-provider]) + spender-contract-address (or (rf/sub [:wallet/swap-proposal-approval-contract-address]) + (:contract-address provider))] + [rn/view {:style style/summary-section-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label :spender-contract-label} + (i18n/label :t/spender-contract)] + (when provider + [quo/approval-info + {:type :token-contract + :option-icon :i/options + :on-option-press #(on-option-press {:network network + :contract-address spender-contract-address}) + :unlimited-icon? false + :label (:full-name provider) + :description (address-utils/get-short-wallet-address spender-contract-address) + :avatar-props {:image (resources/get-network (:name provider))}}])])) + +(defn- data-item + [{:keys [network-image title subtitle size loading?]}] + [quo/data-item + {:container-style style/detail-item + :blur? false + :card? false + :network-image network-image + :subtitle-type (if network-image :network :default) + :status (if loading? :loading :default) + :title title + :subtitle subtitle + :subtitle-text-props {:style {:flex 1}} + :size size}]) + +(defn- transaction-details + [] + (let [network (rf/sub [:wallet/swap-network]) + approval-fees (rf/sub [:wallet/approval-gas-fees]) + currency-symbol (rf/sub [:profile/currency-symbol]) + approval-fees-formatted (utils/fiat-formatted-for-ui + currency-symbol + approval-fees) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + estimated-time (rf/sub [:wallet/swap-approval-estimated-time])] + [rn/view {:style style/details-container} + [:<> + [data-item + {:title (i18n/label :t/network) + :subtitle (:full-name network) + :network-image (:source network)}] + [data-item + {:title (i18n/label :t/max-fees) + :subtitle (if approval-fees-formatted + approval-fees-formatted + (i18n/label :t/unknown)) + :loading? loading-swap-proposal? + :size :small}] + [data-item + {:title (i18n/label :t/est-time) + :subtitle (i18n/label :t/time-in-sec {:seconds estimated-time}) + :loading? loading-swap-proposal? + :size :small}]]])) + +(defn- slide-button + [] + (let [loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) + account (rf/sub [:wallet/current-viewing-account]) + auth-icon (rf/sub [:standard-auth/slider-icon])] + [quo/slide-button + {:size :size-48 + :track-text (i18n/label :t/slide-to-sign) + :container-style {:z-index 2} + :customization-color (:color account) + :track-icon auth-icon + :disabled? (or loading-swap-proposal? (not swap-proposal)) + :on-complete #(rf/dispatch [:wallet/prepare-signatures-for-swap-transactions])}])) + +(defn- footer + [] + (let [provider (rf/sub [:wallet/swap-proposal-provider]) + theme (quo.context/use-theme) + on-press (rn/use-callback #(when provider + (rf/dispatch [:open-url (:terms-and-conditions-url provider)])) + [provider])] + [rn/view {:style (style/footer-container theme)} + [transaction-details] + [slide-button] + [rn/view {:style style/providers-container} + [quo/text + {:size :paragraph-2 + :style (style/swaps-powered-by theme)} + (i18n/label :t/swaps-powered-by + {:provider (if provider (:full-name provider) (i18n/label :t/unknown))})] + [quo/text + {:size :paragraph-2 + :style (style/terms-and-conditions theme) + :on-press on-press} + (i18n/label :t/terms-and-conditions)]]])) + +(defn view + [] + (let [account (rf/sub [:wallet/current-viewing-account])] + [rn/view {:style style/container} + [floating-button-page/view + {:footer-container-padding 0 + :content-container-style style/floating-button-page-content-container + :header [quo/page-nav + {:icon-name :i/close + :on-press events-helper/navigate-back + :margin-top 8 + :background :blur + :accessibility-label :top-bar}] + :footer [footer] + :gradient-cover? true + :customization-color (:color account)} + [rn/scroll-view {:style style/scroll-view-container} + [swap-title] + [spending-cap-section] + [account-section] + [token-section] + [spender-contract-section]]]])) diff --git a/src/status_im/contexts/wallet/swap/setup_swap/style.cljs b/src/status_im/contexts/wallet/swap/setup_swap/style.cljs new file mode 100644 index 00000000000..9c52c7ea405 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/setup_swap/style.cljs @@ -0,0 +1,64 @@ +(ns status-im.contexts.wallet.swap.setup-swap.style + (:require [quo.foundations.colors :as colors])) + +(def container + {:flex 1}) + +(def keyboard-container + {:align-self :flex-end + :width "100%"}) + +(def inputs-container + {:padding-top 12 + :padding-horizontal 20}) + +(defn details-container + [theme] + {:flex-direction :row + :justify-content :space-between + :padding-top 7 + :padding-horizontal 20 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) + +(def detail-item + {:flex 1 + :height 36 + :background-color :transparent}) + +(defn swap-order-button + [approval-required?] + {:margin-top (if approval-required? 3 -9) + :z-index 2 + :align-self :center}) + +(defn receive-token-swap-input-container + [approval-required?] + {:margin-top (if approval-required? 3 -9)}) + +(def footer-container + {:flex 1 + :justify-content :flex-end}) + +(def alert-banner + {:height :auto + :min-height 40 + :max-height 62}) + +(defn exchange-rate-loader + [theme] + {:margin-top 16 + :width 72 + :height 14 + :border-radius 6 + :background-color (colors/theme-colors colors/neutral-5 colors/neutral-90 theme)}) + +(def exchange-rate-container + {:margin-top 16 + :flex-direction :row}) + +(def exchange-rate-crypto-label + {:color colors/neutral-50}) + +(defn exchange-rate-fiat-label + [theme] + {:color (colors/theme-colors colors/neutral-40 colors/neutral-60 theme)}) diff --git a/src/status_im/contexts/wallet/swap/setup_swap/view.cljs b/src/status_im/contexts/wallet/swap/setup_swap/view.cljs new file mode 100644 index 00000000000..508f51bcc3d --- /dev/null +++ b/src/status_im/contexts/wallet/swap/setup_swap/view.cljs @@ -0,0 +1,544 @@ +(ns status-im.contexts.wallet.swap.setup-swap.view + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.context :as quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [status-im.common.controlled-input.utils :as controlled-input] + [status-im.common.events-helper :as events-helper] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [status-im.contexts.wallet.sheets.select-asset.view :as select-asset] + [status-im.contexts.wallet.sheets.slippage-settings.view :as slippage-settings] + [status-im.contexts.wallet.swap.setup-swap.style :as style] + [status-im.contexts.wallet.swap.utils :as swap-utils] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.number :as number] + [utils.re-frame :as rf] + [utils.string :as utils.string])) + +(def ^:private default-text-for-unfocused-input "0") +(def ^:private swap-proposal-debounce-time-ms 1000) + +(defn- on-close + [start-point] + (when (= start-point :action-menu) + (rf/dispatch [:centralized-metrics/track :metric/swap-closed])) + (rf/dispatch [:wallet/clean-swap-proposal + {:clean-amounts? true + :clean-approval-transaction? true}]) + (events-helper/navigate-back)) + +(defn- start-get-swap-proposal + [amount clean-approval-transaction?] + (rf/dispatch [:wallet/stop-get-swap-proposal]) + (rf/dispatch [:wallet.swap/set-loading-swap-proposal]) + (debounce/debounce-and-dispatch [:wallet/start-get-swap-proposal + {:amount-in amount + :clean-approval-transaction? clean-approval-transaction?}] + swap-proposal-debounce-time-ms)) + +(defn- fetch-swap-proposal + [{:keys [amount valid-input? clean-approval-transaction?]}] + (debounce/clear-all) + (if valid-input? + (start-get-swap-proposal amount clean-approval-transaction?) + (rf/dispatch [:wallet/clean-swap-proposal + {:clean-amounts? true + :clean-approval-transaction? clean-approval-transaction?}]))) + +(defn- data-item + [{:keys [title subtitle size subtitle-icon subtitle-color on-press loading?]}] + [quo/data-item + {:container-style style/detail-item + :blur? false + :card? false + :subtitle-type (if subtitle-icon :editable :default) + :status (if loading? :loading :default) + :title title + :subtitle subtitle + :size size + :icon subtitle-icon + :subtitle-color subtitle-color + :on-press on-press}]) + +(defn- transaction-details + [] + (let [theme (quo.context/use-theme) + swap-proposal-fee (rf/sub [:wallet/wallet-swap-proposal-fee-fiat]) + approval-gas-fee (rf/sub [:wallet/approval-gas-fees]) + max-slippage (rf/sub [:wallet/swap-max-slippage]) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + currency-symbol (rf/sub [:profile/currency-symbol]) + approval-required? (rf/sub [:wallet/swap-proposal-approval-required]) + approval-status (rf/sub [:wallet/swap-approval-transaction-status]) + max-fee (if (and approval-required? (not= approval-status :confirmed)) + (money/add approval-gas-fee swap-proposal-fee) + swap-proposal-fee) + max-fee-formatted (utils/fiat-formatted-for-ui + currency-symbol + max-fee) + error-response (rf/sub [:wallet/swap-error-response])] + [rn/view {:style (style/details-container theme)} + [data-item + (cond-> {:title (i18n/label :t/max-fees) + :subtitle max-fee-formatted + :loading? loading-swap-proposal? + :size :small} + error-response (assoc :subtitle-color + (colors/theme-colors colors/danger-50 + colors/danger-60 + theme)))] + [data-item + {:title (i18n/label :t/max-slippage) + :subtitle (str max-slippage "%") + :subtitle-icon :i/edit + :size :small + :on-press (fn [] + (when-not loading-swap-proposal? + (rf/dispatch [:show-bottom-sheet + {:content slippage-settings/view}])))}]])) + +(defn- pay-token-input + [{:keys [input-state on-max-press on-input-focus on-token-press on-approve-press input-focused?]}] + (let [account-color (rf/sub [:wallet/current-viewing-account-color]) + network (rf/sub [:wallet/swap-network]) + pay-token-symbol (rf/sub [:wallet/swap-asset-to-pay-symbol]) + pay-token-decimals (rf/sub [:wallet/swap-asset-to-pay-decimals]) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + loading-swap-proposal-fee? (rf/sub [:wallet/swap-loading-swap-proposal-fee?]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) + approval-required (rf/sub [:wallet/swap-proposal-approval-required]) + approval-amount-required (rf/sub [:wallet/swap-proposal-approval-amount-required]) + approval-transaction-status (rf/sub [:wallet/swap-approval-transaction-status]) + approval-transaction-id (rf/sub [:wallet/swap-approval-transaction-id]) + approved-amount (rf/sub [:wallet/swap-approved-amount]) + error-response (rf/sub [:wallet/swap-error-response]) + native-token-symbol (rf/sub [:wallet/swap-network-native-token-symbol]) + native-token-proposal? (= pay-token-symbol native-token-symbol) + overlay-shown? (boolean (:sheets (rf/sub [:bottom-sheet]))) + input-ref (rn/use-ref-atom nil) + set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref))) + pay-input-num-value (controlled-input/value-numeric input-state) + pay-input-amount (controlled-input/input-value input-state) + pay-token-balance-selected-chain (rf/sub [:wallet/swap-asset-to-pay-balance-for-chain + (:chain-id network)]) + pay-token-fiat-value (rf/sub [:wallet/swap-asset-to-pay-amount-in-fiat + pay-input-num-value]) + available-crypto-limit (rf/sub [:wallet/swap-available-crypto-limit]) + display-decimals (min pay-token-decimals + (if native-token-proposal? + constants/eth-send-amount-decimal + constants/min-token-decimals-to-display)) + total-crypto-limit (money/bignumber + pay-token-balance-selected-chain) + total-crypto-limit-display (utils/sanitized-token-amount-to-display + total-crypto-limit + display-decimals) + approval-amount-required-num (when approval-amount-required + (number/to-fixed (number/hex->whole + approval-amount-required + pay-token-decimals) + pay-token-decimals)) + approval-label-token-value (when (or approval-amount-required-num approved-amount) + (utils/sanitized-token-amount-to-display + (or approval-amount-required-num approved-amount) + display-decimals)) + pay-input-error? (or (and (not (string/blank? pay-input-amount)) + (money/greater-than + (money/bignumber pay-input-amount) + available-crypto-limit)) + (money/equal-to available-crypto-limit + (money/bignumber 0))) + valid-pay-input? (and + (not (string/blank? + pay-input-amount)) + (> pay-input-amount 0) + (not pay-input-error?)) + request-swap-proposal-fee (rn/use-callback + (fn [] + (let [safe-send-amount (utils/calculate-max-safe-send-amount + pay-token-balance-selected-chain)] + (rf/dispatch [:wallet/get-swap-proposal-fee + {:amount-in safe-send-amount}]))) + [pay-token-balance-selected-chain]) + request-fetch-swap-proposal (rn/use-callback + (fn [] + (fetch-swap-proposal + {:amount pay-input-amount + :valid-input? valid-pay-input? + :clean-approval-transaction? true})) + [pay-input-amount]) + on-max-balance-press (rn/use-callback + (fn [] + (let [max-value (if native-token-proposal? + (number/format-decimal-fixed + available-crypto-limit + constants/eth-send-amount-decimal) + (money/to-string available-crypto-limit))] + (when (money/greater-than max-value 0) + (on-max-press max-value)))) + [available-crypto-limit native-token-proposal?])] + (rn/use-unmount #(rf/dispatch [:wallet/clean-swap])) + (rn/use-effect + (fn [] + (when native-token-proposal? + (request-swap-proposal-fee))) + [native-token-proposal?]) + (rn/use-effect + (fn [] + (request-fetch-swap-proposal)) + [pay-input-amount]) + (rn/use-effect + (fn [] + ;; Restart swap proposal fetch after approval confirmation, as route building was paused. + (when (and approval-required (= approval-transaction-status :confirmed)) + (request-fetch-swap-proposal))) + [approval-required approval-transaction-status]) + (rn/use-effect + (fn [] + (when-not overlay-shown? + (some-> @input-ref + (oops/ocall "focus")))) + [overlay-shown?]) + [quo/swap-input + {:get-ref set-input-ref + :type :pay + :error? pay-input-error? + :token pay-token-symbol + :customization-color :blue + :show-approval-label? (or (and swap-proposal approval-required) + approval-transaction-id) + :auto-focus? true + :show-keyboard? false + :status (cond + (and loading-swap-proposal? (not input-focused?)) :loading + input-focused? :typing + :else :disabled) + :on-token-press on-token-press + :on-max-press on-max-balance-press + :max-loading? loading-swap-proposal-fee? + :on-input-focus on-input-focus + :value pay-input-amount + :fiat-value pay-token-fiat-value + :network-tag-props {:title (i18n/label :t/max-token + {:number total-crypto-limit-display + :token-symbol pay-token-symbol}) + :networks [{:source (:source network)}]} + :approval-label-props {:status (case approval-transaction-status + :pending :approving + :confirmed :approved + :finalised :approved + :approve) + :token-value approval-label-token-value + :button-props (merge {:on-press on-approve-press} + (when (or loading-swap-proposal? + error-response) + {:disabled? true})) + :customization-color account-color + :token-symbol pay-token-symbol}}])) + +(defn- swap-order-button + [{:keys [on-press]}] + (let [approval-required? (rf/sub [:wallet/swap-proposal-approval-required])] + [quo/swap-order-button + {:container-style (style/swap-order-button approval-required?) + :on-press on-press}])) + +(defn- receive-token-input + [{:keys [on-input-focus on-token-press input-focused?]}] + (let [account-color (rf/sub [:wallet/current-viewing-account-color]) + asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + currency (rf/sub [:profile/currency]) + currency-symbol (rf/sub [:profile/currency-symbol]) + amount-out (rf/sub [:wallet/swap-proposal-amount-out]) + approval-required? (rf/sub [:wallet/swap-proposal-approval-required]) + prices-per-token (rf/sub [:wallet/prices-per-token]) + receive-token-symbol (:symbol asset-to-receive) + receive-token-decimals (:decimals asset-to-receive) + amount-out-whole-number (when amount-out + (number/hex->whole amount-out receive-token-decimals)) + amount-out-num (if amount-out-whole-number + (number/to-fixed amount-out-whole-number receive-token-decimals) + default-text-for-unfocused-input) + receive-token-fiat-value (utils/formatted-token-fiat-value + {:currency currency + :currency-symbol currency-symbol + :balance (or amount-out-whole-number 0) + :token asset-to-receive + :prices-per-token prices-per-token})] + [quo/swap-input + {:type :receive + :error? false + :token receive-token-symbol + :customization-color account-color + :show-approval-label? false + :enable-swap? true + :input-disabled? true + :show-keyboard? false + :status (cond + (and loading-swap-proposal? (not input-focused?)) :loading + input-focused? :typing + :else :disabled) + :on-token-press on-token-press + :on-input-focus on-input-focus + :value amount-out-num + :fiat-value receive-token-fiat-value + :container-style (style/receive-token-swap-input-container approval-required?)}])) + +(defn- alert-banner + [{:keys [pay-input-error?]}] + (let [error-response (rf/sub [:wallet/swap-error-response]) + error-response-code (rf/sub [:wallet/swap-error-response-code]) + error-response-details (rf/sub [:wallet/swap-error-response-details]) + error-text (if pay-input-error? + (i18n/label :t/insufficient-funds-for-swaps) + (swap-utils/error-message-from-code error-response-code + error-response-details)) + native-token-symbol (rf/sub [:wallet/swap-network-native-token-symbol]) + props (cond-> {:container-style style/alert-banner + :text-number-of-lines 0 + :text error-text} + pay-input-error? + (merge {:action? true + :on-button-press (fn [] + (rf/dispatch [:centralized-metrics/track + :metric/swap-buy-assets]) + (rf/dispatch [:show-bottom-sheet + {:content buy-token/view}])) + :button-text (i18n/label :t/add-assets)}) + (= error-response-code + constants/router-error-code-not-enough-native-balance) + (merge {:action? true + :on-button-press (fn [] + (rf/dispatch [:centralized-metrics/track + :metric/swap-buy-eth]) + (rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [buy-token/view])}])) + :button-text (i18n/label :t/add-token + {:token native-token-symbol})}))] + (when (or pay-input-error? error-response) + [quo/alert-banner props]))) + +(defn- action-button + [{:keys [on-press]}] + (let [account-color (rf/sub [:wallet/current-viewing-account-color]) + swap-proposal-received-amount (rf/sub [:wallet/swap-proposal-amount-out]) + error-response (rf/sub [:wallet/swap-error-response]) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + approval-required? (rf/sub [:wallet/swap-proposal-approval-required]) + approval-transaction-status (rf/sub [:wallet/swap-approval-transaction-status])] + [quo/bottom-actions + {:actions :one-action + :button-one-label (i18n/label :t/review-swap) + :button-one-props {:disabled? (or (not swap-proposal-received-amount) + error-response + (and approval-required? + (not= approval-transaction-status :confirmed)) + loading-swap-proposal?) + :customization-color account-color + :on-press on-press}}])) + +(defn- pay-token-bottom-sheet + [] + (let [asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) + network (rf/sub [:wallet/swap-network])] + [select-asset/view + {:title (i18n/label :t/select-asset-to-pay) + :network network + :on-select (fn [token] + (rf/dispatch [:wallet.swap/select-asset-to-pay {:token token}])) + :hide-token-fn (fn [type {:keys [balances-per-chain]}] + (let [balance (get-in balances-per-chain [(:chain-id network) :balance] "0")] + (and (= type constants/swap-tokens-my) + (= balance "0")))) + :disable-token-fn (fn [_ token] + (= (:symbol token) + (:symbol asset-to-receive)))}])) + +(defn- receive-token-bottom-sheet + [] + (let [asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + network (rf/sub [:wallet/swap-network])] + [select-asset/view + {:title (i18n/label :t/select-asset-to-receive) + :network network + :on-select (fn [token] + (rf/dispatch [:wallet.swap/select-asset-to-receive {:token token}])) + :disable-token-fn (fn [_ token] + (= (:symbol token) + (:symbol asset-to-pay)))}])) + +(defn- swap-exchange-rate-view + [] + (let [theme (quo.context/use-theme) + asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) + swap-exchange-rate-crypto (rf/sub [:wallet/swap-exchange-rate-crypto]) + swap-exchange-rate-fiat (rf/sub [:wallet/swap-exchange-rate-fiat]) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])] + (cond + loading-swap-proposal? + [rn/view {:style (style/exchange-rate-loader theme)}] + swap-exchange-rate-crypto + [rn/view {:style style/exchange-rate-container} + [quo/text + {:weight :medium + :size :paragraph-2 + :style style/exchange-rate-crypto-label} + (i18n/label :t/swap-exchange-rate-in-crypto + {:receive-token-symbol (:symbol asset-to-receive) + :exchange-rate swap-exchange-rate-crypto + :pay-token-symbol (:symbol asset-to-pay)})] + [quo/text + {:weight :medium + :size :paragraph-2 + :style (style/exchange-rate-fiat-label theme)} + (str " (" swap-exchange-rate-fiat ")")]]))) + +(defn view + [] + (let [[pay-input-state set-pay-input-state] (rn/use-state controlled-input/init-state) + [pay-input-focused? set-pay-input-focused?] (rn/use-state true) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) + asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) + network (rf/sub [:wallet/swap-network]) + pay-token-balance-selected-chain (rf/sub [:wallet/swap-asset-to-pay-balance-for-chain + (:chain-id network)]) + start-point (rf/sub [:wallet/swap-start-point]) + current-account-address (rf/sub [:wallet/current-viewing-account-address]) + pay-input-amount (controlled-input/input-value pay-input-state) + pay-token-decimals (:decimals asset-to-pay) + pay-input-error? (and (not (string/blank? pay-input-amount)) + (money/greater-than + (money/bignumber pay-input-amount) + (money/bignumber + pay-token-balance-selected-chain))) + valid-pay-input? (and + (not (string/blank? + pay-input-amount)) + (> pay-input-amount 0) + (not pay-input-error?)) + on-review-swap-press (rn/use-callback + (fn [] + (rf/dispatch [:wallet.swap/review-swap]))) + on-press (rn/use-callback + (fn [c] + (let + [new-text (str pay-input-amount c) + valid-amount? + (utils.string/valid-amount-for-token-decimals? + pay-token-decimals + new-text)] + (when valid-amount? + (set-pay-input-state + #(controlled-input/add-character % + c + ##Inf))))) + [pay-input-amount pay-token-decimals]) + on-long-press (rn/use-callback + (fn [] + (set-pay-input-state controlled-input/delete-all) + (rf/dispatch [:wallet/clean-suggested-routes]))) + delete (rn/use-callback + (fn [] + (set-pay-input-state + controlled-input/delete-last) + (rf/dispatch [:wallet/clean-swap-proposal + {:clean-amounts? true + :clean-approval-transaction? + true}]))) + on-max-press (rn/use-callback + (fn [max-value] + (set-pay-input-state + (fn [input-state] + (controlled-input/set-input-value + input-state + max-value))))) + refetch-swap-proposal (fn [] + (when valid-pay-input? + (fetch-swap-proposal + {:amount pay-input-amount + :valid-input? valid-pay-input? + :clean-approval-transaction? true})))] + (rn/use-effect (fn [] + (rf/dispatch [:wallet/clean-swap-proposal + {:clean-amounts? false + :clean-approval-transaction? true}]) + (refetch-swap-proposal)) + [current-account-address]) + (rn/use-unmount (fn [] + (rf/dispatch [:wallet/clean-swap-proposal + {:clean-amounts? true + :clean-approval-transaction? true}]))) + (rn/use-effect + (fn [] + (when asset-to-pay + (let [swap-amount (rf/sub [:wallet/swap-amount])] + (cond (and swap-amount (not= swap-amount pay-input-amount)) + (set-pay-input-state + (fn [input-state] + (controlled-input/set-input-value + input-state + swap-amount))) + (and pay-input-amount + (not (number/valid-decimal-count? pay-input-amount (:decimals asset-to-pay)))) + (set-pay-input-state controlled-input/delete-all) + :else + (refetch-swap-proposal))))) + [asset-to-pay]) + (rn/use-effect + refetch-swap-proposal + [asset-to-receive]) + [rn/view {:style style/container} + [account-switcher/view + {:on-press #(on-close start-point) + :icon-name :i/arrow-left + :margin-top safe-area/top + :switcher-type :select-account + :params {:show-account-balances? true + :asset-symbol (:symbol asset-to-pay) + :network network}}] + [rn/scroll-view {:style style/inputs-container} + [pay-token-input + {:input-state pay-input-state + :on-max-press on-max-press + :input-focused? pay-input-focused? + :on-token-press #(rf/dispatch [:show-bottom-sheet {:content pay-token-bottom-sheet}]) + :on-approve-press #(rf/dispatch [:open-modal :screen/wallet.swap-set-spending-cap]) + :on-input-focus (fn [] + (when platform/android? (rf/dispatch [:dismiss-keyboard])) + (set-pay-input-focused? true))}] + [swap-order-button + {:on-press (fn [] + (rf/dispatch [:wallet.swap/flip-assets]))}] + [receive-token-input + {:input-focused? (not pay-input-focused?) + :on-token-press #(rf/dispatch [:show-bottom-sheet {:content receive-token-bottom-sheet}]) + :on-input-focus #(set-pay-input-focused? false)}] + [swap-exchange-rate-view]] + [rn/view {:style style/footer-container} + (when-not loading-swap-proposal? [alert-banner {:pay-input-error? pay-input-error?}]) + (when (or loading-swap-proposal? swap-proposal) + [transaction-details]) + [action-button {:on-press on-review-swap-press}]] + [quo/numbered-keyboard + {:container-style style/keyboard-container + :left-action :dot + :delete-key? true + :on-press on-press + :on-delete delete + :on-long-press on-long-press}]])) diff --git a/src/status_im/contexts/wallet/swap/swap_confirmation/style.cljs b/src/status_im/contexts/wallet/swap/swap_confirmation/style.cljs new file mode 100644 index 00000000000..cf89f60d5e5 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/swap_confirmation/style.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.wallet.swap.swap-confirmation.style + (:require [quo.foundations.colors :as colors])) + +(def detail-item + {:flex 1 + :height 36 + :background-color :transparent}) + +(def content-container + {:padding-top 12 + :padding-horizontal 20 + :padding-bottom 32}) + +(def title-container + {:margin-right 4}) + +(def title-line-with-margin-top + {:flex-direction :row + :margin-top 4}) + +(def details-container + {:flex-direction :row + :justify-content :space-between + :height 52 + :padding-top 7 + :margin-bottom 8}) + +(def summary-section-container + {:padding-horizontal 20 + :padding-bottom 16}) + +(defn section-label + [theme] + {:margin-bottom 8 + :color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(def providers-container + {:align-items :center + :margin-top 12}) + +(defn swaps-powered-by + [theme] + {:color (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-70 theme)}) + +(defn terms-and-conditions + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) diff --git a/src/status_im/contexts/wallet/swap/swap_confirmation/view.cljs b/src/status_im/contexts/wallet/swap/swap_confirmation/view.cljs new file mode 100644 index 00000000000..928964acefa --- /dev/null +++ b/src/status_im/contexts/wallet/swap/swap_confirmation/view.cljs @@ -0,0 +1,228 @@ +(ns status-im.contexts.wallet.swap.swap-confirmation.view + (:require + [quo.context :as quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.common.transaction-settings.view :as transaction-settings] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [status-im.contexts.wallet.swap.swap-confirmation.style :as style] + [utils.address :as address-utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- on-close-action + [] + (rf/dispatch [:navigate-back])) + +(defn- swap-title + [] + (let [asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) + account (rf/sub [:wallet/current-viewing-account]) + receive-amount (rf/sub [:wallet/swap-receive-amount]) + pay-amount (rf/sub [:wallet/swap-pay-amount]) + pay-token-symbol (:symbol asset-to-pay) + receive-token-symbol (:symbol asset-to-receive)] + [rn/view {:style style/content-container} + [rn/view {:style {:flex-direction :row}} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :title-label} + (i18n/label :t/swap)] + [quo/summary-tag + {:token pay-token-symbol + :label (str pay-amount " " pay-token-symbol) + :type :token}]] + [rn/view {:style style/title-line-with-margin-top} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :title-label} + (i18n/label :t/to)] + [quo/summary-tag + {:token receive-token-symbol + :label (str receive-amount " " receive-token-symbol) + :type :token}]] + [rn/view {:style style/title-line-with-margin-top} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style style/title-container + :accessibility-label :send-label} + (i18n/label :t/in)] + [quo/summary-tag + {:label (:name account) + :type :account + :emoji (:emoji account) + :customization-color (:color account)}]]])) + +(defn- summary-section + [{:keys [theme label title-accessibility-label amount token-symbol token-address network]}] + [rn/view {:style style/summary-section-container} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label title-accessibility-label} + label] + [quo/summary-info + {:type :token + :token-props {:token token-symbol + :label (str amount " " token-symbol) + :address (when token-address + (address-utils/get-shortened-compressed-key token-address)) + :size 32} + :networks-to-show (network-utils/network-summary network token-symbol amount)}]]) + +(defn- pay-section + [] + (let [theme (quo.context/use-theme) + asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) + network (rf/sub [:wallet/swap-network]) + pay-amount (rf/sub [:wallet/swap-pay-amount-raw]) + network-chain-id (:chain-id network) + network-name (:network-name network) + pay-token-symbol (:symbol asset-to-pay) + pay-token-address (get-in asset-to-pay [:balances-per-chain network-chain-id :address])] + [summary-section + {:title-accessibility-label :summary-section-pay + :label (i18n/label :t/pay) + :token-symbol pay-token-symbol + :amount pay-amount + :token-address (when-not (address-utils/zero-address? pay-token-address) + pay-token-address) + :network network-name + :theme theme}])) + +(defn- receive-section + [] + (let [theme (quo.context/use-theme) + asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) + network (rf/sub [:wallet/swap-network]) + receive-amount (rf/sub [:wallet/swap-receive-amount-raw]) + network-chain-id (:chain-id network) + network-name (:network-name network) + receive-token-symbol (:symbol asset-to-receive) + receive-token-address (get-in asset-to-receive [:balances-per-chain network-chain-id :address])] + [summary-section + {:title-accessibility-label :summary-section-receive + :label (i18n/label :t/receive) + :token-symbol receive-token-symbol + :amount receive-amount + :token-address (when-not (address-utils/zero-address? receive-token-address) + receive-token-address) + :network network-name + :theme theme}])) + +(defn- data-item + [{:keys [title subtitle loading?]}] + [quo/data-item + {:container-style style/detail-item + :blur? false + :card? false + :status (if loading? :loading :default) + :size :small + :title title + :subtitle subtitle}]) + +(defn- transaction-details + [] + (let [max-fees (rf/sub [:wallet/wallet-swap-proposal-fee-fiat]) + currency-symbol (rf/sub [:profile/currency-symbol]) + max-fees-formatted (utils/fiat-formatted-for-ui + currency-symbol + max-fees) + estimated-time (rf/sub [:wallet/swap-estimated-time]) + loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + max-slippage (rf/sub [:wallet/swap-max-slippage])] + [rn/view {:style style/details-container} + [:<> + [quo/button + {:icon-only? true + :type :outline + :size 32 + :inner-style {:opacity 1} + :accessibility-label :advanced-button + :container-style {:margin-right 8} + :on-press #(rf/dispatch + [:show-bottom-sheet + {:content transaction-settings/settings-sheet}])} + :i/advanced] + [data-item + {:title (i18n/label :t/est-time) + :subtitle (i18n/label :t/time-in-sec {:seconds estimated-time}) + :loading? loading-swap-proposal?}] + [data-item + {:title (i18n/label :t/max-fees) + :subtitle (if max-fees-formatted max-fees-formatted (i18n/label :t/unknown)) + :loading? loading-swap-proposal?}] + [data-item + {:title (i18n/label :t/max-slippage) + :subtitle (str max-slippage "%")}]]])) + +(defn- slide-button + [] + (let [loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) + account (rf/sub [:wallet/current-viewing-account]) + auth-icon (rf/sub [:standard-auth/slider-icon]) + account-color (:color account)] + [quo/slide-button + {:size :size-48 + :track-text (i18n/label :t/slide-to-swap) + :container-style {:z-index 2} + :customization-color account-color + :track-icon auth-icon + :disabled? (or loading-swap-proposal? + (not swap-proposal)) + :on-complete #(rf/dispatch [:wallet/prepare-signatures-for-swap-transactions])}])) + +(defn footer + [] + (let [provider (rf/sub [:wallet/swap-proposal-provider]) + theme (quo.context/use-theme) + on-press (rn/use-callback #(when provider + (rf/dispatch [:open-modal :screen/pdf-viewer + {:uri (:terms-and-conditions-url provider)}])) + [provider])] + [:<> + [transaction-details] + [slide-button] + [rn/view {:style style/providers-container} + [quo/text + {:size :paragraph-2 + :style (style/swaps-powered-by theme)} + (i18n/label :t/swaps-powered-by + {:provider (if provider (:full-name provider) (i18n/label :t/unknown))})] + [quo/text + {:size :paragraph-2 + :style (style/terms-and-conditions theme) + :on-press on-press} + (i18n/label :t/terms-and-conditions)]]])) + +(defn view + [] + (let [account (rf/sub [:wallet/current-viewing-account]) + account-color (:color account)] + [rn/view {:style {:flex 1}} + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:icon-name :i/arrow-left + :on-press on-close-action + :margin-top safe-area/top + :background :blur + :accessibility-label :top-bar}] + :footer [footer] + :gradient-cover? true + :customization-color account-color} + [rn/view + [swap-title] + [pay-section] + [receive-section]]]])) diff --git a/src/status_im/contexts/wallet/swap/utils.cljs b/src/status_im/contexts/wallet/swap/utils.cljs new file mode 100644 index 00000000000..fed9fea22df --- /dev/null +++ b/src/status_im/contexts/wallet/swap/utils.cljs @@ -0,0 +1,83 @@ +(ns status-im.contexts.wallet.swap.utils + (:require [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [status-im.contexts.wallet.networks.config :as networks.config] + [utils.i18n :as i18n])) + +(defn error-message-from-code + [error-code error-details] + (cond + (= error-code + constants/router-error-code-not-enough-liquidity) + (i18n/label :t/not-enough-liquidity) + (= error-code + constants/router-error-code-price-timeout) + (i18n/label :t/fetching-the-price-took-longer-than-expected) + (= error-code + constants/router-error-code-price-impact-too-high) + (i18n/label :t/price-impact-too-high) + (= error-code + constants/router-error-code-paraswap-custom-error) + (i18n/label :t/paraswap-error + {:paraswap-error error-details}) + (= error-code + constants/router-error-code-generic) + (i18n/label :t/generic-error + {:generic-error error-details}) + (= error-code + constants/router-error-code-not-enough-native-balance) + (i18n/label :t/not-enough-assets-to-pay-gas-fees) + :else + (i18n/label :t/something-went-wrong-please-try-again-later))) + +(defn current-viewing-account + [wallet] + (when-let [wallet-address (get wallet :current-viewing-account-address)] + (get-in wallet [:accounts wallet-address]))) + +(defn select-asset-to-pay-by-symbol + "Selects an asset to pay by token symbol. + It's used for cases when only token symbol is available and the information + about token needs to be extracted from the database. + That happens when token is being selected on the home screen and + it basically indicates that no account pre-selection was made." + [{:keys [networks account token-symbol]}] + (let [token (->> account + :tokens + (filter #(= token-symbol (:symbol %))) + first)] + (assoc token + :networks + (network-utils/network-list-with-positive-balance + token + networks)))) + +(defn select-network + "Chooses the network. + Usually user needs to do the selection first and if the selection was done + then the list of networks for the defined token will always contain + one entry. Otherwise `nil` will be returned from here which will serve + as an indicator that the network selector needs to be displayed." + [{:keys [networks]}] + (when (= (count networks) 1) + (first networks))) + +(defn updated-token-price + "Returns the updated token price (number) from the routes." + [token updated-prices] + (let [token-symbol (some-> token + :symbol + keyword)] + (get updated-prices token-symbol 0))) + +(defn get-default-asset-to-receive + [tokens pay-token-symbol chain-id] + (let [asset-to-receive + (cond + ;; Temporary workaround as USDC has 6 decimals on ETH/OPT/ARB/BASE and 18 decimals on BSC + (and (= pay-token-symbol "ETH") (not= chain-id networks.config/bsc-chain-id)) "USDC (EVM)" + (and (= pay-token-symbol "BNB") (= chain-id networks.config/bsc-chain-id)) "USDC (BSC)" + (and (not= pay-token-symbol "BNB") (= chain-id networks.config/bsc-chain-id)) "BNB" + (= pay-token-symbol "SNT") "ETH" + :else "SNT")] + (some #(when (and (= (:symbol %) asset-to-receive) (= (:chain-id %) chain-id)) %) tokens))) diff --git a/src/status_im/contexts/wallet/tokens/data.cljs b/src/status_im/contexts/wallet/tokens/data.cljs new file mode 100644 index 00000000000..d1146677263 --- /dev/null +++ b/src/status_im/contexts/wallet/tokens/data.cljs @@ -0,0 +1,41 @@ +(ns status-im.contexts.wallet.tokens.data) + +(defn- tokens-by-key + [{:keys [key-fn added-tokens source-name tokens chain-ids]}] + (reduce + (fn [acc {:keys [address chainId decimals name image verified] :as token}] + (if (some #{chainId} chain-ids) + (let [k (key-fn token)] + (assoc acc + k + {:key k + :name name + :symbol (:symbol token) + :sources (if-let [added-token (get added-tokens k)] + (conj (:sources added-token) source-name) + [source-name]) + :chain-id chainId + :address address + :decimals decimals + :image image + :type (if (= name "native") :native :erc20) + :community-id (get-in token [:communityData :id]) + :verified? verified})) + acc)) + {} + tokens)) + +(defn tokens-by-address + [props] + (tokens-by-key (assoc props + :key-fn + (fn [{:keys [chainId address]}] + (str chainId "-" address))))) + +(defn tokens-by-symbol + [props] + (tokens-by-key + (assoc props + :key-fn + (fn [{:keys [chainId address] :as token}] + (str chainId "-" (if (get-in token [:communityData :id]) address (:symbol token))))))) diff --git a/src/status_im/contexts/wallet/tokens/effects.cljs b/src/status_im/contexts/wallet/tokens/effects.cljs new file mode 100644 index 00000000000..69046f4c286 --- /dev/null +++ b/src/status_im/contexts/wallet/tokens/effects.cljs @@ -0,0 +1,25 @@ +(ns status-im.contexts.wallet.tokens.effects + (:require [promesa.core :as promesa] + [status-im.contexts.wallet.tokens.rpc :as rpc] + [utils.re-frame :as rf])) + +(rf/reg-fx + :effects.wallet.tokens/fetch-market-values + (fn [{:keys [symbols currency on-success on-error]}] + (-> (rpc/fetch-market-values symbols currency) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(rf/reg-fx + :effects.wallet.tokens/fetch-details + (fn [{:keys [symbols on-success on-error]}] + (-> (rpc/fetch-details symbols) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(rf/reg-fx + :effects.wallet.tokens/fetch-prices + (fn [{:keys [symbols currencies on-success on-error]}] + (-> (rpc/fetch-prices symbols currencies) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) diff --git a/src/status_im/contexts/wallet/tokens/events.cljs b/src/status_im/contexts/wallet/tokens/events.cljs new file mode 100644 index 00000000000..7c25af77740 --- /dev/null +++ b/src/status_im/contexts/wallet/tokens/events.cljs @@ -0,0 +1,161 @@ +(ns status-im.contexts.wallet.tokens.events + (:require [clojure.set :as set] + [re-frame.core :as rf] + [status-im.constants :as constants] + [status-im.contexts.wallet.networks.db :as networks.db] + [status-im.contexts.wallet.tokens.data :as tokens-data] + [status-im.contexts.wallet.tokens.effects] + [taoensso.timbre :as log] + [utils.address])) + +(rf/reg-event-fx + :wallet.tokens/reset-tokens + (fn [{:keys [db]}] + {:db (update db :wallet dissoc :tokens)})) + +(rf/reg-event-fx + :wallet.tokens/get-token-list + (fn [{:keys [db]}] + (when-not (get-in db [:wallet :tokens]) + {:db (assoc-in db [:wallet :ui :loading :token-list] true) + :fx [[:json-rpc/call + [{:method "wallet_getTokenList" + :params [] + :on-success [:wallet.tokens/store-token-list] + :on-error [:wallet.tokens/get-token-list-failed]}]]]}))) + +(defn store-token-list + [{:keys [db]} [{:keys [data]}]] + (let [chain-ids (networks.db/get-active-chain-ids db) + profile-currency (get-in db [:profile/profile :currency]) + tokens (reduce (fn [{:keys [by-address by-symbol] :as data} + {:keys [name source version tokens]}] + (-> data + (update :sources + conj + {:name name + :source source + :version version + :tokens-count (count tokens)}) + (update :by-address + merge + (tokens-data/tokens-by-address + {:added-tokens by-address + :source-name name + :tokens tokens + :chain-ids chain-ids})) + (update :by-symbol + merge + (tokens-data/tokens-by-symbol + {:added-tokens by-symbol + :source-name name + :tokens tokens + :chain-ids chain-ids})))) + {:sources [] + :by-address {} + :by-symbol {}} + data) + by-symbol-vals (-> tokens :by-symbol vals) + symbols (reduce (fn [acc token] + (conj acc (:symbol token))) + #{} + by-symbol-vals) + supported-chains-by-symbol (reduce + (fn [result token] + (update result (:symbol token) set/union #{(:chain-id token)})) + {} + by-symbol-vals)] + {:fx [[:effects.wallet.tokens/fetch-market-values + {:symbols symbols + :currency profile-currency + :on-success [:wallet.tokens/store-market-values] + :on-error [:wallet.tokens/fetch-market-values-failed]}] + [:effects.wallet.tokens/fetch-details + {:symbols symbols + :on-success [:wallet.tokens/store-details] + :on-error [:wallet.tokens/fetch-details-failed]}] + [:effects.wallet.tokens/fetch-prices + {:symbols symbols + :currencies [constants/profile-default-currency profile-currency] + :on-success [:wallet.tokens/store-prices] + :on-error [:wallet.tokens/fetch-prices-failed]}]] + :db (-> db + (assoc-in [:wallet :tokens] + {:sources (:sources tokens) + :supported-chains-by-symbol supported-chains-by-symbol + :by-address (-> tokens :by-address vals) + :by-symbol by-symbol-vals}) + (assoc-in [:wallet :ui :loading] + {:token-list false + :market-values true + :details true + :prices true}))})) + +(rf/reg-event-fx + :wallet.tokens/store-token-list + store-token-list) + +(rf/reg-event-fx + :wallet.tokens/get-token-list-failed + (fn [{:keys [db]} [error]] + (log/info "failed to get wallet tokens " + {:error error + :event :wallet.tokens/get-token-list}) + {:db (assoc-in db [:wallet :ui :loading :token-list] false)})) + +(rf/reg-event-fx + :wallet.tokens/store-market-values + (fn [{:keys [db]} [raw-data]] + (let [market-values (reduce (fn [acc [token data]] + (assoc acc + token + {:market-cap (:MKTCAP data) + :high-day (:HIGHDAY data) + :low-day (:LOWDAY data) + :change-pct-hour (:CHANGEPCTHOUR data) + :change-pct-day (:CHANGEPCTDAY data) + :change-pct-24h (:CHANGEPCT24HOUR data) + :change-24h (:CHANGE24HOUR data)})) + {} + raw-data)] + {:db (-> db + (update-in [:wallet :tokens :market-values-per-token] merge market-values) + (assoc-in [:wallet :ui :loading :market-values] false))}))) + +(rf/reg-event-fx + :wallet.tokens/fetch-market-values-failed + (fn [{:keys [db]} [error]] + (log/info "failed to get wallet market values " + {:error error + :event :wallet.tokens/fetch-market-values}) + {:db (assoc-in db [:wallet :ui :loading :market-values] false)})) + +(rf/reg-event-fx + :wallet.tokens/store-details + (fn [{:keys [db]} [details]] + {:db (-> db + (assoc-in [:wallet :tokens :details-per-token] details) + (assoc-in [:wallet :ui :loading :details] false))})) + +(rf/reg-event-fx + :wallet.tokens/fetch-details-failed + (fn [{:keys [db]} [error]] + (log/info "failed to get wallet details " + {:error error + :event :wallet.tokens/fetch-details}) + {:db (assoc-in db [:wallet :ui :loading :details] false)})) + +(rf/reg-event-fx + :wallet.tokens/store-prices + (fn [{:keys [db]} [prices]] + {:db (-> db + (update-in [:wallet :tokens :prices-per-token] merge prices) + (assoc-in [:wallet :ui :loading :prices] false))})) + +(rf/reg-event-fx + :wallet.tokens/fetch-prices-failed + (fn [{:keys [db]} [error]] + (log/info "failed to get wallet prices " + {:error error + :event :wallet.tokens/fetch-prices}) + {:db (assoc-in db [:wallet :ui :loading :prices] false)})) diff --git a/src/status_im/contexts/wallet/tokens/events_test.cljs b/src/status_im/contexts/wallet/tokens/events_test.cljs new file mode 100644 index 00000000000..84689329bbb --- /dev/null +++ b/src/status_im/contexts/wallet/tokens/events_test.cljs @@ -0,0 +1,263 @@ +(ns status-im.contexts.wallet.tokens.events-test + (:require + [cljs.test :refer [deftest is testing]] + [status-im.contexts.wallet.tokens.events :as tokens.events] + [tests.wallet-test-data :as test-data])) + +(def eth + {:address "0x0000000000000000000000000000000000000000" + :chainId 1 + :decimals 18 + :name "Ether" + :symbol "ETH" + :verified true}) + +(def snt + {:address "0x0000000000000000000000000000000000000001" + :chainId 1 + :decimals 18 + :name "Status Network Token" + :symbol "SNT" + :verified true}) + +(def another-token1 + {:address "0x0000000000000000000000000000000000000002" + :chainId 1 + :decimals 18 + :name "Another Token 1" + :symbol "ANOTHER1" + :verified false}) + +(def another-token2 + {:address "0x0000000000000000000000000000000000000000" ; uses the same address as `eth` + :chainId 1 + :decimals 18 + :name "Another Token 2" + :symbol "ANOTHER1" ;; uses the same symbol as `another-token1` + :verified false}) + + +(deftest store-token-list-test + (testing "response contains one list, and `chain-id` is equal to the one that's in the database" + (let [cofx {:db (test-data/add-active-networks-to-db {} [test-data/mainnet-chain-id])} + data [{:name "native" + :source "native" + :version "1.0.0" + :tokens [eth snt]}]] + (is + (match? + (-> cofx + (tokens.events/store-token-list [{:data data}]) + (get-in [:db :wallet :tokens])) + {:sources [{:name "native" + :source "native" + :version "1.0.0" + :tokens-count 2}] + :by-address '({:address "0x0000000000000000000000000000000000000000" + :decimals 18 + :key "1-0x0000000000000000000000000000000000000000" + :community-id nil + :symbol "ETH" + :sources ["native"] + :name "Ether" + :type :erc20 + :verified? true + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000001" + :decimals 18 + :key "1-0x0000000000000000000000000000000000000001" + :community-id nil + :symbol "SNT" + :sources ["native"] + :name "Status Network Token" + :type :erc20 + :verified? true + :chain-id 1 + :image nil}) + :by-symbol '({:address "0x0000000000000000000000000000000000000000" + :decimals 18 + :key "1-ETH" + :community-id nil + :symbol "ETH" + :sources ["native"] + :name "Ether" + :type :erc20 + :verified? true + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000001" + :decimals 18 + :key "1-SNT" + :community-id nil + :symbol "SNT" + :sources ["native"] + :name "Status Network Token" + :type :erc20 + :verified? true + :chain-id 1 + :image nil}) + :supported-chains-by-symbol {"ETH" #{test-data/mainnet-chain-id} + "SNT" #{test-data/mainnet-chain-id}}})))) + + (testing "response contains two lists" + (let [cofx {:db (test-data/add-active-networks-to-db {} [test-data/mainnet-chain-id])} + data [{:name "native" + :source "native" + :version "1.0.0" + :tokens [eth snt]} + {:name "second" + :source "second" + :version "1.0.0" + :tokens [snt another-token1]}]] + (is + (match? + (-> (tokens.events/store-token-list cofx [{:data data}]) + :db + :wallet + :tokens) + {:sources [{:name "native" + :source "native" + :version "1.0.0" + :tokens-count 2} + {:name "second" + :source "second" + :version "1.0.0" + :tokens-count 2}] + :by-address '({:address "0x0000000000000000000000000000000000000000" + :decimals 18 + :key "1-0x0000000000000000000000000000000000000000" + :community-id nil + :symbol "ETH" + :sources ["native"] + :name "Ether" + :type :erc20 + :verified? true + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000001" + :decimals 18 + :key "1-0x0000000000000000000000000000000000000001" + :community-id nil + :symbol "SNT" + :sources ["native" "second"] + :name "Status Network Token" + :type :erc20 + :verified? true + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000002" + :decimals 18 + :key "1-0x0000000000000000000000000000000000000002" + :community-id nil + :symbol "ANOTHER1" + :sources ["second"] + :name "Another Token 1" + :type :erc20 + :verified? false + :chain-id 1 + :image nil}) + :by-symbol '({:address "0x0000000000000000000000000000000000000000" + :decimals 18 + :key "1-ETH" + :community-id nil + :symbol "ETH" + :sources ["native"] + :name "Ether" + :type :erc20 + :verified? true + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000001" + :decimals 18 + :key "1-SNT" + :community-id nil + :symbol "SNT" + :sources ["native" "second"] + :name "Status Network Token" + :type :erc20 + :verified? true + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000002" + :decimals 18 + :key "1-ANOTHER1" + :community-id nil + :symbol "ANOTHER1" + :sources ["second"] + :name "Another Token 1" + :type :erc20 + :verified? false + :chain-id 1 + :image nil}) + :supported-chains-by-symbol {"ETH" #{1} "SNT" #{1} "ANOTHER1" #{1}}})))) + (testing "second list contains a token that replaces the one that was added before" + (let [cofx {:db (test-data/add-active-networks-to-db {} [test-data/mainnet-chain-id])} + data [{:name "native" + :source "native" + :version "1.0.0" + :tokens [eth another-token1]} + {:name "second" + :source "second" + :version "1.0.0" + :tokens [another-token2]}]] + (is + (match? + (-> (tokens.events/store-token-list cofx [{:data data}]) + :db + :wallet + :tokens) + {:sources [{:name "native" + :source "native" + :version "1.0.0" + :tokens-count 2} + {:name "second" + :source "second" + :version "1.0.0" + :tokens-count 1}] + :by-address '({:address "0x0000000000000000000000000000000000000000" + :decimals 18 + :key "1-0x0000000000000000000000000000000000000000" + :community-id nil + :symbol "ANOTHER1" + :sources ["native" "second"] + :name "Another Token 2" + :type :erc20 + :verified? false + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000002" + :decimals 18 + :key "1-0x0000000000000000000000000000000000000002" + :community-id nil + :symbol "ANOTHER1" + :sources ["native"] + :name "Another Token 1" + :type :erc20 + :verified? false + :chain-id 1 + :image nil}) + :by-symbol '({:address "0x0000000000000000000000000000000000000000" + :decimals 18 + :key "1-ETH" + :community-id nil + :symbol "ETH" + :sources ["native"] + :name "Ether" + :type :erc20 + :verified? true + :chain-id 1 + :image nil} + {:address "0x0000000000000000000000000000000000000000" + :decimals 18 + :key "1-ANOTHER1" + :community-id nil + :symbol "ANOTHER1" + :sources ["native" "second"] + :name "Another Token 2" + :type :erc20 + :verified? false + + :chain-id 1 + :image nil}) + :supported-chains-by-symbol {"ETH" #{1} "ANOTHER1" #{1}}}))))) diff --git a/src/status_im/contexts/wallet/tokens/rpc.cljs b/src/status_im/contexts/wallet/tokens/rpc.cljs new file mode 100644 index 00000000000..a49d52d938a --- /dev/null +++ b/src/status_im/contexts/wallet/tokens/rpc.cljs @@ -0,0 +1,19 @@ +(ns status-im.contexts.wallet.tokens.rpc + (:require [promesa.core :as promesa] + [status-im.common.json-rpc.events :as rpc-events] + [utils.transforms :as transforms])) + +(defn fetch-market-values + [symbols currency] + (-> (rpc-events/call-async "wallet_fetchMarketValues" true symbols currency) + (promesa/then transforms/js->clj))) + +(defn fetch-details + [symbols] + (-> (rpc-events/call-async "wallet_fetchTokenDetails" true symbols) + (promesa/then transforms/js->clj))) + +(defn fetch-prices + [symbols currencies] + (-> (rpc-events/call-async "wallet_fetchPrices" true symbols currencies) + (promesa/then transforms/js->clj))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/core.cljs b/src/status_im/contexts/wallet/wallet_connect/events/core.cljs new file mode 100644 index 00000000000..64ec7551332 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/core.cljs @@ -0,0 +1,124 @@ +(ns status-im.contexts.wallet.wallet-connect.events.core + (:require [re-frame.core :as rf] + [react-native.wallet-connect :as wallet-connect] + [status-im.constants :as constants] + status-im.contexts.wallet.wallet-connect.events.effects + status-im.contexts.wallet.wallet-connect.events.network + status-im.contexts.wallet.wallet-connect.events.session-proposals + status-im.contexts.wallet.wallet-connect.events.session-requests + status-im.contexts.wallet.wallet-connect.events.session-responses + status-im.contexts.wallet.wallet-connect.events.sessions + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [taoensso.timbre :as log] + [utils.i18n :as i18n])) + +(rf/reg-event-fx + :wallet-connect/init + (fn [{:keys [db]}] + (let [network-status (:network/status db) + web3-wallet-missing? (-> db :wallet-connect/web3-wallet boolean not)] + (if (and (= network-status :online) web3-wallet-missing?) + (do (log/info "Initialising WalletConnect SDK") + {:fx [[:effects.wallet-connect/init + {:on-success #(rf/dispatch [:wallet-connect/on-init-success %]) + :on-fail #(rf/dispatch [:wallet-connect/on-init-fail %])}]]}) + ;; NOTE: when offline, fetching persistent sessions without initializing WC + {:fx [[:dispatch [:wallet-connect/get-sessions]]]})))) + +(rf/reg-event-fx + :wallet-connect/on-init-success + (fn [{:keys [db]} [web3-wallet]] + (log/info "WalletConnect SDK initialisation successful") + {:db (assoc db :wallet-connect/web3-wallet web3-wallet) + :fx [[:dispatch [:wallet-connect/register-event-listeners]] + [:dispatch [:wallet-connect/get-sessions]] + [:dispatch [:wallet-connect/pair-with-pending-deeplink]]]})) + +(rf/reg-event-fx + :wallet-connect/reload-on-network-change + (fn [{:keys [db]} [is-connected?]] + (let [logged-in? (-> db :profile/profile boolean)] + (when (and is-connected? logged-in?) + (log/info "Re-Initialising WalletConnect SDK due to network change") + {:fx [[:dispatch [:wallet-connect/init]]]})))) + +(defn- on-session-proposal + [data] + (rf/dispatch [:wallet-connect/on-session-proposal data])) + +(defn- on-session-request + [data] + (rf/dispatch [:wallet-connect/on-session-request data])) + +(defn- on-session-delete + [data] + (rf/dispatch [:wallet-connect/on-session-delete data])) + + +(rf/reg-event-fx + :wallet-connect/register-event-listeners + (fn [{:keys [db]}] + (let [web3-wallet (get db :wallet-connect/web3-wallet)] + {:fx [[:effects.wallet-connect/register-event-listener + [web3-wallet + constants/wallet-connect-session-proposal-event + on-session-proposal]] + [:effects.wallet-connect/register-event-listener + [web3-wallet + constants/wallet-connect-session-request-event + on-session-request]] + [:effects.wallet-connect/register-event-listener + [web3-wallet + constants/wallet-connect-session-delete-event + on-session-delete]]]}))) + +(rf/reg-event-fx + :wallet-connect/unregister-event-listeners + (fn [{:keys [db]}] + (let [web3-wallet (get db :wallet-connect/web3-wallet)] + {:fx [[:effects.wallet-connect/unregister-event-listener + [web3-wallet + constants/wallet-connect-session-proposal-event + on-session-proposal]] + [:effects.wallet-connect/unregister-event-listener + [web3-wallet + constants/wallet-connect-session-request-event + on-session-request]] + [:effects.wallet-connect/unregister-event-listener + [web3-wallet + constants/wallet-connect-session-delete-event + on-session-delete]]]}))) + +(rf/reg-event-fx + :wallet-connect/on-init-fail + (fn [_ [error]] + (log/error "Failed to initialize Wallet Connect" + {:error error + :event :wallet-connect/on-init-fail}))) + +(rf/reg-event-fx + :wallet-connect/on-session-request + (fn [{:keys [db]} [event]] + (if (networks/event-should-be-handled? db event) + {:fx [[:dispatch [:wallet-connect/process-session-request event]]]} + {:fx [[:dispatch + [:wallet-connect/show-session-networks-unsupported-toast event]] + [:dispatch + [:wallet-connect/send-response + {:request event + :error (wallet-connect/get-sdk-error + constants/wallet-connect-user-rejected-chains-error-key)}]]]}))) + +(rf/reg-event-fx + :wallet-connect/reset-current-request + (fn [{:keys [db]}] + {:db (dissoc db :wallet-connect/current-request)})) + +(rf/reg-event-fx + :wallet-connect/no-internet-toast + (fn [{:keys [db]}] + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme (:theme db) + :text (i18n/label :t/wallet-connect-no-internet-warning)}]]]})) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/effects.cljs b/src/status_im/contexts/wallet/wallet_connect/events/effects.cljs new file mode 100644 index 00000000000..5570190f7f0 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/effects.cljs @@ -0,0 +1,136 @@ +(ns status-im.contexts.wallet.wallet-connect.events.effects + (:require + [native-module.core :as native-module] + [promesa.core :as promesa] + [react-native.wallet-connect :as wallet-connect] + [status-im.config :as config] + [status-im.constants :as constants] + [status-im.contexts.wallet.rpc :as wallet-rpc] + [status-im.contexts.wallet.wallet-connect.utils.sessions :as sessions] + [status-im.contexts.wallet.wallet-connect.utils.transactions :as transactions] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(rf/reg-fx + :effects.wallet-connect/init + (fn [{:keys [on-success on-fail]}] + (let + [project-id config/WALLET_CONNECT_PROJECT_ID + metadata {:name (i18n/label :t/status) + :description (i18n/label :t/status-is-a-secure-messaging-app) + :url constants/wallet-connect-metadata-url + :icons [constants/wallet-connect-metadata-icon] + :redirect {:native constants/wallet-connect-metadata-redirect-native}}] + (-> (wallet-connect/init project-id metadata) + (promesa/then on-success) + (promesa/catch on-fail))))) + +(rf/reg-fx + :effects.wallet-connect/register-event-listener + (fn [[web3-wallet wc-event handler]] + (wallet-connect/register-handler + {:web3-wallet web3-wallet + :event wc-event + :handler handler}))) + +(rf/reg-fx + :effects.wallet-connect/unregister-event-listener + (fn [[web3-wallet wc-event handler]] + (wallet-connect/unregister-handler + {:web3-wallet web3-wallet + :event wc-event + :handler handler}))) + +(rf/reg-fx + :effects.wallet-connect/pair + (fn [{:keys [web3-wallet url on-success on-fail]}] + (when web3-wallet + (-> (wallet-connect/core-pairing-pair web3-wallet url) + (promesa/then on-success) + (promesa/catch on-fail))))) + +(rf/reg-fx + :effects.wallet-connect/disconnect + (fn [{:keys [web3-wallet topic on-success on-fail]}] + (-> (sessions/disconnect web3-wallet topic) + (promesa/then on-success) + (promesa/catch on-fail)))) + +(rf/reg-fx + :effects.wallet-connect/approve-session + (fn [{:keys [web3-wallet proposal-request session-networks address on-success on-fail]}] + (-> (sessions/approve + {:web3-wallet web3-wallet + :proposal-request proposal-request + :address address + :session-networks session-networks}) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-fail))))) + +(rf/reg-fx + :effects.wallet-connect/hash-message + (fn [{:keys [message on-success on-fail]}] + (-> (wallet-rpc/hash-message-eip-191 message) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-fail))))) + +(rf/reg-fx + :effects.wallet-connect/hash-typed-data + (fn [{:keys [message legacy? on-success on-fail]}] + (-> (if legacy? + (native-module/hash-typed-data message) + (native-module/hash-typed-data-v4 message)) + (promesa/then (fn [response] (-> response transforms/json->clj :result))) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-fail))))) + +(rf/reg-fx + :effects.wallet-connect/prepare-transaction + (fn [{:keys [tx chain-id on-success on-error]}] + (-> (transactions/prepare-transaction tx + chain-id + transactions/default-tx-priority) + (promesa/then on-success) + (promesa/catch on-error)))) + +(rf/reg-fx + :effects.wallet-connect/send-transaction + (fn [{:keys [chain-id signature tx-args on-success on-error]}] + (-> (wallet-rpc/send-transaction-with-signature chain-id + tx-args + signature) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(rf/reg-fx + :effects.wallet-connect/respond-session-request + (fn [{:keys [web3-wallet topic id result error on-success on-error]}] + (-> (wallet-connect/respond-session-request + {:web3-wallet web3-wallet + :topic topic + :id id + :result result + :error error}) + (promesa/then on-success) + (promesa/catch on-error)))) + +(rf/reg-fx + :effects.wallet-connect/reject-session-proposal + (fn [{:keys [web3-wallet proposal on-success on-error]}] + (let [{:keys [id]} proposal + reason (wallet-connect/get-sdk-error + constants/wallet-connect-user-rejected-error-key)] + (-> (wallet-connect/reject-session + {:web3-wallet web3-wallet + :id id + :reason reason}) + (promesa/then on-success) + (promesa/catch on-error))))) + +(rf/reg-fx + :effects.wallet-connect/get-sessions + (fn [{:keys [web3-wallet addresses online? on-success on-error]}] + (-> (sessions/get-sessions web3-wallet addresses online?) + (promesa/then on-success) + (promesa/catch on-error)))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/network.cljs b/src/status_im/contexts/wallet/wallet_connect/events/network.cljs new file mode 100644 index 00000000000..ce96f003128 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/network.cljs @@ -0,0 +1,42 @@ +(ns status-im.contexts.wallet.wallet-connect.events.network + (:require [status-im.contexts.wallet.networks.db :as networks.db] + [status-im.contexts.wallet.wallet-connect.modals.change-network.view :as + change-network-modal] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :wallet-connect/show-activate-request-network-sheet + (fn [_ [{:keys [on-success]}]] + {:fx [[:dispatch + [:show-bottom-sheet + {:content (fn [] + [change-network-modal/view + {:on-success on-success}])}]]]})) + +(rf/reg-event-fx :wallet-connect/activate-request-network + (fn [{:keys [db]} [{:keys [deactivate-chain-id]}]] + (let [event (data-store/get-db-current-request-event db) + chain-id (data-store/get-request-chain-id event) + activate-network-name (networks.db/get-network-name db chain-id)] + {:fx [(if deactivate-chain-id + [:dispatch + [:wallet/deactivate-and-activate-another-network + {:activate-chain-id chain-id + :deactivate-chain-id deactivate-chain-id + :on-success #(rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label + :t/network-activated-and-deactivated + {:network-activated activate-network-name + :network-deactivated + (networks.db/get-network-name + db + deactivate-chain-id)})}])}]] + [:dispatch + [:wallet/toggle-network-active chain-id + #(rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label :t/network-activated + {:network activate-network-name})}])]])]}))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/session_proposals.cljs b/src/status_im/contexts/wallet/wallet_connect/events/session_proposals.cljs new file mode 100644 index 00000000000..2181fc3ecf7 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/session_proposals.cljs @@ -0,0 +1,216 @@ +(ns status-im.contexts.wallet.wallet-connect.events.session-proposals + (:require [clojure.string :as string] + [re-frame.core :as rf] + [react-native.wallet-connect :as wallet-connect] + [status-im.contexts.wallet.networks.db :as networks.db] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [status-im.contexts.wallet.wallet-connect.utils.sessions :as sessions] + [status-im.contexts.wallet.wallet-connect.utils.uri :as uri] + [taoensso.timbre :as log] + [utils.i18n :as i18n])) + +(rf/reg-event-fx + :wallet-connect/pair + (fn [{:keys [db]} [url]] + (let [web3-wallet (get db :wallet-connect/web3-wallet)] + {:fx [[:effects.wallet-connect/pair + {:web3-wallet web3-wallet + :url url + :on-fail #(log/error "Failed to pair with dApp" {:error %}) + :on-success #(log/info "dApp paired successfully")}]]}))) + +(rf/reg-event-fx + :wallet-connect/process-deeplink + (fn [{:keys [db]} [url]] + (let [web3-wallet (get db :wallet-connect/web3-wallet)] + (if web3-wallet + {:fx [[:dispatch [:wallet-connect/on-scan-connection url]]]} + {:db (assoc db :wallet-connect/pending-url url)})))) + +(rf/reg-event-fx + :wallet-connect/pair-with-pending-deeplink + (fn [{:keys [db]}] + (when-let [pending-url (get db :wallet-connect/pending-url)] + {:db (dissoc db :wallet-connect/pending-url) + :fx [[:dispatch [:wallet-connect/on-scan-connection pending-url]]]}))) + +(rf/reg-event-fx + :wallet-connect/on-scan-connection + (fn [{:keys [db]} [scanned-text]] + (let [network-status (:network/status db) + parsed-uri (wallet-connect/parse-uri scanned-text) + version (:version parsed-uri) + valid-wc-uri? (uri/valid-wc-uri? parsed-uri) + expired? (-> parsed-uri + :expiryTimestamp + uri/timestamp-expired?) + version-supported? (uri/version-supported? version)] + (cond + (or (not valid-wc-uri?) + (not version-supported?) + (= network-status :offline) + expired?) + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text (cond (= network-status :offline) + (i18n/label :t/wallet-connect-no-internet-warning) + + (not valid-wc-uri?) + (i18n/label :t/wallet-connect-wrong-qr) + + expired? + (i18n/label :t/wallet-connect-qr-expired) + + (not version-supported?) + (i18n/label :t/wallet-connect-version-not-supported + {:version version}) + + :else + (i18n/label :t/something-went-wrong))}]]]} + + :else + {:fx [[:dispatch [:wallet-connect/pair scanned-text]]]})))) + +(rf/reg-event-fx + :wallet-connect/on-session-proposal + (fn [{:keys [db]} [proposal]] + (log/info "Received Wallet Connect session proposal: " proposal) + (let [accounts (get-in db [:wallet :accounts]) + current-viewing-address (get-in db [:wallet :current-viewing-account-address]) + sessions (get db :wallet-connect/sessions) + available-accounts (sessions/filter-operable-accounts (vals accounts)) + latest-connected-account-address (sessions/latest-connected-account-address sessions) + networks (networks.db/get-chain-ids db) + session-networks (networks/proposal-networks-intersection proposal networks) + required-networks-supported? (networks/required-networks-supported? proposal networks)] + (if (and (not-empty session-networks) required-networks-supported?) + {:db (update db + :wallet-connect/current-proposal assoc + :request proposal + :session-networks session-networks + :address (cond + (not (string/blank? current-viewing-address)) + current-viewing-address + + (not (string/blank? + latest-connected-account-address)) + latest-connected-account-address + + :else (-> available-accounts + first + :address))) + :fx [[:dispatch [:open-modal :screen/wallet.wallet-connect-session-proposal]]]} + {:fx [[:dispatch [:wallet-connect/show-session-networks-unsupported-toast proposal]] + [:dispatch [:wallet-connect/reject-session-proposal proposal]]]})))) + +(rf/reg-event-fx + :wallet-connect/show-session-networks-unsupported-toast + (fn [{:keys [db]} [proposal]] + (let [{:keys [name url]} (data-store/get-session-dapp-metadata proposal)] + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme (:theme db) + :text (i18n/label :t/wallet-connect-networks-not-supported + {:dapp (data-store/compute-dapp-name name url)})}]]]}))) + +(rf/reg-event-fx + :wallet-connect/reset-current-session-proposal + (fn [{:keys [db]}] + {:db (dissoc db :wallet-connect/current-proposal)})) + +(rf/reg-event-fx + :wallet-connect/set-current-proposal-address + (fn [{:keys [db]} [address]] + {:db (assoc-in db [:wallet-connect/current-proposal :address] address)})) + +(rf/reg-event-fx + :wallet-connect/approve-session + (fn [{:keys [db]}] + (let [web3-wallet (get db :wallet-connect/web3-wallet) + current-proposal (get-in db [:wallet-connect/current-proposal :request]) + session-networks (->> (get-in db [:wallet-connect/current-proposal :session-networks]) + (map networks/chain-id->eip155) + vec) + current-address (get-in db [:wallet-connect/current-proposal :address]) + network-status (:network/status db) + expired? (-> current-proposal + (get-in [:params :expiryTimestamp]) + uri/timestamp-expired?)] + (if (= network-status :online) + {:db (assoc-in db [:wallet-connect/current-proposal :response-sent?] true) + :fx [(if expired? + [:dispatch + [:toasts/upsert + {:id :wallet-connect-proposal-expired + :type :negative + :text (i18n/label :t/wallet-connect-proposal-expired)}]] + [:effects.wallet-connect/approve-session + {:web3-wallet web3-wallet + :proposal-request current-proposal + :session-networks session-networks + :address current-address + :on-success [:wallet-connect/approve-session-success] + :on-fail [:wallet-connect/approve-session-error]}]) + [:dispatch [:dismiss-modal :screen/wallet.wallet-connect-session-proposal]]]} + {:fx [[:dispatch [:wallet-connect/no-internet-toast]]]})))) + +(rf/reg-event-fx :wallet-connect/approve-session-success + (fn [{:keys [db]} [session]] + (log/info "Successfully approved WalletConnect session" session) + (let [total-connected-dapps (data-store/get-total-connected-dapps db) + dapp-name (data-store/get-dapp-name session)] + {:fx [[:dispatch [:wallet-connect/on-new-session session]] + [:dispatch [:wallet-connect/reset-current-session-proposal]] + [:dispatch [:wallet-connect/redirect-to-dapp (data-store/get-dapp-redirect-url session)]] + [:dispatch + [:toasts/upsert + {:type :positive + :text (i18n/label :t/wallet-connect-proposal-approved-toast {:dapp dapp-name})}]] + [:dispatch + [:centralized-metrics/track :metric/dapp-session-proposal + {:action :approved + :total_connected_dapps total-connected-dapps}]]]}))) + +(rf/reg-event-fx :wallet-connect/approve-session-error + (fn [_ [error]] + (log/error "Wallet Connect session approval failed" + {:error error + :event :wallet-connect/approve-session}) + {:fx [[:dispatch [:wallet-connect/reset-current-session-proposal]] + [:dispatch + [:toasts/upsert + {:type :negative + :text (i18n/label :t/wallet-connect-something-went-wrong)}]]]})) + +(rf/reg-event-fx + :wallet-connect/reject-session-proposal + (fn [{:keys [db]} [proposal]] + (let [web3-wallet (get db :wallet-connect/web3-wallet) + {:keys [request response-sent?]} (:wallet-connect/current-proposal db) + networks (networks/get-proposal-networks (or proposal request)) + rejected? (nil? proposal) + dapp-name (-> (data-store/get-session-dapp-metadata (or proposal + request)) + :name)] + {:fx (concat + (when-not response-sent? + [[:effects.wallet-connect/reject-session-proposal + {:web3-wallet web3-wallet + :proposal (or proposal request) + :on-success (fn [] + (log/info "Wallet Connect session proposal rejected") + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label :t/wallet-connect-proposal-rejected-toast + {:dapp dapp-name})}])) + :on-error #(log/error "Wallet Connect unable to reject session proposal")}] + [:dispatch + [:centralized-metrics/track :metric/dapp-session-proposal + {:action (if rejected? :rejected :not_supported) + :networks networks}]]]) + [[:dispatch [:wallet-connect/reset-current-session-proposal]]])}))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/session_requests.cljs b/src/status_im/contexts/wallet/wallet_connect/events/session_requests.cljs new file mode 100644 index 00000000000..bff8d1b318e --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/session_requests.cljs @@ -0,0 +1,211 @@ +(ns status-im.contexts.wallet.wallet-connect.events.session-requests + (:require [cljs-bean.core :as bean] + [clojure.string :as string] + [native-module.core :as native-module] + [status-im.constants :as constants] + [status-im.contexts.wallet.networks.db :as networks.db] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks.utils] + [status-im.contexts.wallet.wallet-connect.utils.transactions :as transactions] + [status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data] + [taoensso.timbre :as log] + [utils.address :as utils-address] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(rf/reg-event-fx + :wallet-connect/show-request-modal + (fn [{:keys [db]}] + (let [event (get-in db [:wallet-connect/current-request :event]) + method (data-store/get-request-method event) + screen (data-store/method-to-screen method)] + (if screen + {:fx [[:dispatch [:open-modal screen]]]} + (log/error "Didn't find screen for Wallet Connect method" + {:method method + :event :wallet-connect/process-session-request}))))) +(rf/reg-event-fx + :wallet-connect/process-session-request + (fn [{:keys [db]} [event]] + (let [method (data-store/get-request-method event) + existing-event (get-in db [:wallet-connect/current-request :event])] + (log/info "Processing Wallet Connect session request" method) + ;; NOTE: make sure we don't show two requests at the same time + (when-not existing-event + {:db (-> db + (assoc-in [:wallet-connect/current-request :event] event) + (assoc-in [:wallet-connect/current-request :response-sent?] false)) + :fx [(condp = method + constants/wallet-connect-eth-send-transaction-method + [:dispatch + [:wallet-connect/process-eth-send-transaction + {:on-success (fn [] (rf/dispatch [:wallet-connect/show-request-modal]))}]] + + constants/wallet-connect-eth-sign-typed-method + [:dispatch [:wallet-connect/process-sign-typed]] + + constants/wallet-connect-eth-sign-typed-v4-method + [:dispatch [:wallet-connect/process-sign-typed]] + + constants/wallet-connect-personal-sign-method + [:dispatch [:wallet-connect/process-personal-sign]])]})))) + +(rf/reg-event-fx + :wallet-connect/store-prepared-hash + (fn [{:keys [db]} [prepared-hash]] + {:db (assoc-in db + [:wallet-connect/current-request :prepared-hash] + prepared-hash)})) + +(rf/reg-event-fx + :wallet-connect/process-personal-sign + (fn [{:keys [db]}] + (let [[raw-data address] (data-store/get-db-current-request-params db) + parsed-data (if (utils-address/has-hex-prefix? raw-data) + (native-module/hex-to-utf8 raw-data) + raw-data) + hex-message (if (utils-address/has-hex-prefix? raw-data) + raw-data + (native-module/utf8-to-hex raw-data))] + {:db (update-in db + [:wallet-connect/current-request] + assoc + :address (string/lower-case address) + :display-data (or parsed-data raw-data)) + :fx [[:effects.wallet-connect/hash-message + {:message hex-message + :on-success #(rf/dispatch [:wallet-connect/store-prepared-hash %]) + :on-fail #(rf/dispatch [:wallet-connect/on-processing-error %])}] + [:dispatch [:wallet-connect/show-request-modal]]]}))) + +(rf/reg-event-fx + :wallet-connect/prepare-transaction-success + (fn [{:keys [db]} [prepared-tx chain-id]] + (let [{:keys [tx-args tx-hash]} prepared-tx + tx (bean/->clj tx-args) + display-data (transactions/beautify-transaction tx)] + (log/info "WC transaction prepared") + {:db (update-in db + [:wallet-connect/current-request] + assoc + :raw-data prepared-tx + :transaction tx + :chain-id chain-id + :display-data display-data) + :fx [[:dispatch [:wallet-connect/store-prepared-hash tx-hash]]]}))) + +(rf/reg-event-fx :wallet-connect/prepare-transaction + (fn [{:keys [db]} [on-success]] + (let [event (data-store/get-db-current-request-event db) + tx (-> event data-store/get-request-params first) + chain-id (-> event + (get-in [:params :chainId]) + networks.utils/eip155->chain-id)] + (log/info "Preparing WC transaction") + {:fx [[:effects.wallet-connect/prepare-transaction + {:tx tx + :chain-id chain-id + :on-success (fn [data] + (rf/dispatch [:wallet-connect/prepare-transaction-success data + chain-id]) + (when on-success + (rf/call-continuation on-success))) + :on-error #(rf/dispatch [:wallet-connect/on-processing-error %])}]]}))) + +(rf/reg-event-fx + :wallet-connect/process-eth-send-transaction + (fn [{:keys [db]} [{:keys [on-success]}]] + (let [event (data-store/get-db-current-request-event db) + chain-id (data-store/get-request-chain-id event) + tx (-> event data-store/get-request-params first) + address (-> tx :from string/lower-case) + chain-active? (networks.db/network-active? db chain-id) + prepare-tx-effect [:wallet-connect/prepare-transaction on-success]] + (when tx + {:db (assoc-in db [:wallet-connect/current-request :address] address) + :fx [(if chain-active? + [:dispatch prepare-tx-effect] + [:dispatch + [:wallet-connect/show-activate-request-network-sheet + {:on-success #(rf/dispatch prepare-tx-effect)}]])]})))) + +(rf/reg-event-fx + :wallet-connect/process-sign-typed + (fn [{:keys [db]}] + (try + (let [[address raw-data] (data-store/get-db-current-request-params db) + method (-> db + data-store/get-db-current-request-event + data-store/get-request-method) + session-chain-id (-> (data-store/get-db-current-request-event db) + (get-in [:params :chainId]) + networks.utils/eip155->chain-id) + typed-data (-> raw-data + transforms/js-parse + transforms/js->clj) + data-chain-id (typed-data/get-chain-id typed-data)] + (if (and data-chain-id + (not= session-chain-id data-chain-id)) + {:fx [[:dispatch + [:wallet-connect/wrong-typed-data-chain-id + {:expected-chain-id session-chain-id + :wrong-chain-id data-chain-id}]]]} + {:db (update-in db + [:wallet-connect/current-request] + assoc + :address (string/lower-case address) + :display-data (typed-data/flatten-typed-data typed-data) + :raw-data raw-data) + :fx [[:effects.wallet-connect/hash-typed-data + {:message raw-data + :legacy? (not= constants/wallet-connect-eth-sign-typed-v4-method + method) + :on-success #(rf/dispatch [:wallet-connect/store-prepared-hash %]) + :on-fail #(rf/dispatch [:wallet-connect/on-processing-error %])}] + [:dispatch [:wallet-connect/show-request-modal]]]})) + (catch js/Error err + {:fx [[:dispatch + [:wallet-connect/on-processing-error + (ex-info "Failed to parse JSON typed data" + {:error err + :data (data-store/get-db-current-request-params db)})]]]})))) + +(rf/reg-event-fx + :wallet-connect/wrong-typed-data-chain-id + (fn [{:keys [db]} [{:keys [expected-chain-id wrong-chain-id]}]] + (let [wrong-network-name (->> wrong-chain-id + (networks.db/get-network-details db) + :full-name) + expected-network-name (->> expected-chain-id + (networks.db/get-network-details db) + :full-name) + toast-message (i18n/label :t/wallet-connect-typed-data-wrong-chain-id-warning + {:wrong-chain (or wrong-network-name + (networks.utils/chain-id->eip155 + wrong-chain-id)) + :expected-chain expected-network-name})] + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :text toast-message}]] + [:dispatch + [:wallet-connect/on-processing-error + (ex-info "Can't proceed signing typed data due to wrong chain-id included in the data" + {:expected-chain-id expected-chain-id + :wrong-chain-id wrong-chain-id})]]]}))) + +(rf/reg-event-fx + :wallet-connect/on-processing-error + (fn [{:keys [db]} [error]] + (let [{:keys [address event]} (get db :wallet-connect/current-request) + method (data-store/get-request-method event)] + (log/error "Failed to process Wallet Connect request" + {:error error + :address address + :method method + :wallet-connect-event event + :event :wallet-connect/on-processing-error}) + ;; FIXME(@clauxx/@alwx): rename this event eventually + {:fx [[:dispatch [:wallet-connect/on-request-modal-dismissed]]]}))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/session_responses.cljs b/src/status_im/contexts/wallet/wallet_connect/events/session_responses.cljs new file mode 100644 index 00000000000..36b372a730d --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/session_responses.cljs @@ -0,0 +1,163 @@ +(ns status-im.contexts.wallet.wallet-connect.events.session-responses + (:require [re-frame.core :as rf] + [react-native.wallet-connect :as wallet-connect] + [status-im.constants :as constants] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [status-im.contexts.wallet.wallet-connect.utils.uri :as uri] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.transforms :as transforms])) + +(rf/reg-event-fx + :wallet-connect/respond + (fn [{:keys [db]} [signature]] + (let [event (get-in db [:wallet-connect/current-request :event]) + method (data-store/get-request-method event) + screen (data-store/method-to-screen method) + expiry (get-in event [:params :request :expiryTimestamp])] + (if (uri/timestamp-expired? expiry) + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :text (i18n/label :t/wallet-connect-request-expired)}]] + [:dispatch [:dismiss-modal screen]]]} + {:fx [(condp contains? method + #{constants/wallet-connect-personal-sign-method + constants/wallet-connect-eth-sign-typed-method + constants/wallet-connect-eth-sign-typed-v4-method} + [:dispatch [:wallet-connect/finish-session-request signature]] + + #{constants/wallet-connect-eth-send-transaction-method} + [:dispatch [:wallet-connect/respond-send-transaction-data signature]])]})))) + +(rf/reg-event-fx + :wallet-connect/respond-send-transaction-data + (fn [{:keys [db]} [signature]] + (let [{:keys [chain-id raw-data]} (get db :wallet-connect/current-request) + {:keys [tx-args]} raw-data] + {:fx [[:effects.wallet-connect/send-transaction + {:signature signature + :chain-id chain-id + :tx-args tx-args + :on-error [:wallet-connect/on-sign-error] + :on-success [:wallet-connect/finish-session-request]}]]}))) + +(rf/reg-event-fx + :wallet-connect/on-sign-error + (fn [{:keys [db]} [error]] + (let [{:keys [raw-data address event]} (get db :wallet-connect/current-request) + method (data-store/get-request-method event)] + (log/error "Failed to sign Wallet Connect request" + {:error error + :address address + :sign-data raw-data + :method method + :wallet-connect-event event + :event :wallet-connect/on-sign-error}) + {:fx [[:dispatch [:wallet-connect/dismiss-request-modal]] + [:dispatch + [:toasts/upsert + {:type :negative + :text (i18n/label :t/wallet-connect-something-went-wrong)}]]]}))) + +(rf/reg-event-fx + :wallet-connect/send-response + (fn [{:keys [db]} [{:keys [request result error]}]] + (when-let [{:keys [id topic] :as event} (or request + (get-in db [:wallet-connect/current-request :event]))] + (let [method (data-store/get-request-method event) + web3-wallet (get db :wallet-connect/web3-wallet)] + {:db (assoc-in db [:wallet-connect/current-request :response-sent?] true) + :fx [[:effects.wallet-connect/respond-session-request + {:web3-wallet web3-wallet + :topic topic + :id id + :result result + :error error + :on-error (fn [error] + (log/error "Failed to send Wallet Connect response" + {:error error + :method method + :event :wallet-connect/send-response + :wallet-connect-event event})) + :on-success (fn [] + (rf/dispatch [:wallet-connect/redirect-to-dapp]) + (log/info "Successfully sent Wallet Connect response to dApp"))}]]})))) + +(rf/reg-event-fx + :wallet-connect/redirect-to-dapp + (fn [{:keys [db]} [url]] + (let [redirect-url (or url + (->> (get db :wallet-connect/current-request) + (data-store/get-current-request-dapp + (get db :wallet-connect/sessions)) + :sessionJson + transforms/json->clj + data-store/get-dapp-redirect-url))] + {:fx [[:effects/open-url redirect-url]]}))) + +(rf/reg-event-fx + :wallet-connect/dismiss-request-modal + (fn [{:keys [db]} _] + (let [screen (-> db + (get-in [:wallet-connect/current-request :event]) + data-store/get-request-method + data-store/method-to-screen)] + {:fx [[:dispatch [:dismiss-modal screen]]]}))) + +(rf/reg-event-fx + :wallet-connect/finish-session-request + (fn [{:keys [db]} [result]] + (let [event (get-in db [:wallet-connect/current-request :event]) + method (data-store/get-request-method event) + toast-text (condp contains? method + constants/wallet-connect-message-signing-methods + (i18n/label :t/wallet-connect-message-request-success-toast) + + constants/wallet-connect-transaction-methods + (i18n/label :t/wallet-connect-transaction-request-success-toast))] + {:fx [[:dispatch + [:centralized-metrics/track :metric/dapp-session-response + {:method method + :approved true}]] + [:dispatch [:wallet-connect/send-response {:result result}]] + [:dispatch [:wallet-connect/dismiss-request-modal]] + [:dispatch + [:toasts/upsert + {:type :positive + :text toast-text}]]]}))) + +;; NOTE: Currently we only reject a session if the user dismissed a modal +;; without accepting the session first. +;; But this needs to be solidified to ensure other cases: +;; - Unsupported WC version +;; - Invalid params from dapps +;; - Unsupported method +;; - Failed processing of request +;; - Failed "responding" (signing or sending message/transaction) +(rf/reg-event-fx + :wallet-connect/on-request-modal-dismissed + (fn [{:keys [db]}] + (let [{:keys [response-sent? event] :as request} (get db :wallet-connect/current-request) + method (data-store/get-request-method event) + dapp (->> (get db :wallet-connect/sessions) + (data-store/get-current-request-dapp request) + :name)] + {:fx (concat + (when-not response-sent? + [[:dispatch + [:centralized-metrics/track :metric/dapp-session-response + {:method method + :approved false}]] + [:dispatch + [:wallet-connect/send-response + {:request event + :error (wallet-connect/get-sdk-error + constants/wallet-connect-user-rejected-error-key)}]] + [:dispatch + [:toasts/upsert + {:type :positive + :text (i18n/label :t/wallet-connect-sign-request-rejected-toast + {:dapp dapp})}]]]) + [[:dispatch [:wallet-connect/reset-current-request]]])}))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/session_responses_test.cljs b/src/status_im/contexts/wallet/wallet_connect/events/session_responses_test.cljs new file mode 100644 index 00000000000..4c390ed5bc8 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/session_responses_test.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.wallet.wallet-connect.events.session-responses-test + (:require + [cljs.test :refer-macros [is]] + matcher-combinators.test + [re-frame.db :as rf-db] + status-im.contexts.wallet.wallet-connect.events.session-responses + [status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store] + [test-helpers.unit :as h] + [utils.transforms :as transforms])) + +(h/deftest-event :wallet-connect/redirect-to-dapp + [event-id dispatch] + (let [current-request {:event {:verifyContext {:verified {:origin "https://dapp.com"}}}} + session-json "{\"peer\":{\"metadata\":{\"redirect\":{\"native\":\"native://redirect-url\"}}}}" + sessions [{:url "https://dapp.com" + :sessionJson session-json}]] + (reset! rf-db/app-db {:wallet-connect {:current-request current-request + :sessions sessions}}) + (with-redefs [data-store/get-current-request-dapp + (fn [_ _] (first sessions)) + transforms/json->clj + (fn [json] (js/JSON.parse json)) + data-store/get-dapp-redirect-url + (fn [_] "native://redirect-url")] + + (is (match? {:fx [[:effects/open-url "native://redirect-url"]]} + (dispatch [event-id])))))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/sessions.cljs b/src/status_im/contexts/wallet/wallet_connect/events/sessions.cljs new file mode 100644 index 00000000000..ba64e8d604e --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/sessions.cljs @@ -0,0 +1,108 @@ +(ns status-im.contexts.wallet.wallet-connect.events.sessions + (:require [re-frame.core :as rf] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [status-im.contexts.wallet.wallet-connect.utils.sessions :as sessions] + [taoensso.timbre :as log] + [utils.i18n :as i18n])) + +(rf/reg-event-fx + :wallet-connect/on-session-delete + (fn [{:keys [db]} [{:keys [topic] :as event}]] + (let [session (data-store/get-session-by-topic db topic) + account-name (-> (data-store/get-account-by-session db session) + :name)] + (when (networks/event-should-be-handled? db event) + (log/info "Received Wallet Connect session delete from the SDK: " event) + {:fx [[:json-rpc/call + [{:method "wallet_disconnectWalletConnectSession" + :params [topic] + :on-success [:wallet-connect/delete-session topic] + :on-error #(log/info "Wallet Connect session persistence failed" %)}]] + [:dispatch + [:toasts/upsert + {:id :dapp-disconnect-success + :type :positive + :text (i18n/label :t/disconnect-dapp-success + {:dapp (:name session) + :account account-name})}]] + [:dispatch + [:centralized-metrics/track + :metric/dapp-session-disconnected]]]})))) + +(rf/reg-event-fx + :wallet-connect/disconnect-dapp + (fn [{:keys [db]} [{:keys [topic on-success on-fail]}]] + (let [web3-wallet (get db :wallet-connect/web3-wallet) + network-status (:network/status db)] + (log/info "Disconnecting dApp session" topic) + (if (= network-status :online) + {:fx [[:effects.wallet-connect/disconnect + {:web3-wallet web3-wallet + :topic topic + :on-fail (fn [] + (when on-fail + (on-fail))) + :on-success (fn [] + (log/info "Successfully disconnected dApp session" topic) + (rf/dispatch [:wallet-connect/delete-session topic]) + (rf/dispatch [:centralized-metrics/track + :metric/dapp-session-disconnected]) + (when on-success + (on-success)))}]]} + {:fx [[:dispatch [:wallet-connect/no-internet-toast]]]})))) + +(rf/reg-event-fx + :wallet-connect/get-sessions + (fn [{:keys [db]}] + (let [addresses (->> (get-in db [:wallet :accounts]) + vals + sessions/filter-operable-accounts + (map :address))] + (if (not (seq addresses)) + ;; NOTE: Re-trying to get active sessions if accounts weren't loaded yet during + ;; initialization + ((log/info "Re-trying to fetch active WalletConnect sessions") + {:fx [[:dispatch-later [{:ms 500 :dispatch [:wallet-connect/get-sessions]}]]]}) + {:fx [[:effects.wallet-connect/get-sessions + {:online? (-> db :network/status (= :online)) + :web3-wallet (get db :wallet-connect/web3-wallet) + :addresses addresses + :on-success #(rf/dispatch [:wallet-connect/get-sessions-success %]) + :on-error #(rf/dispatch [:wallet-connect/get-sessions-error %])}]]})))) + +(rf/reg-event-fx + :wallet-connect/get-sessions-success + (fn [{:keys [db]} [sessions]] + (log/info "WalletConnect sessions loaded successfully") + {:db (assoc db :wallet-connect/sessions sessions)})) + +(rf/reg-event-fx + :wallet-connect/get-sessions-error + (fn [_ [error]] + (log/error "WalletConnect sessions failed to load" error) + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :text (i18n/label :t/wallet-connect-connections-error)}]]]})) + +(rf/reg-event-fx + :wallet-connect/on-new-session + (fn [{:keys [db]} [new-session]] + {:db (update db + :wallet-connect/sessions + (fn [sessions] + (->> new-session + sessions/sdk-session->db-session + (conj sessions))))})) + +(rf/reg-event-fx + :wallet-connect/delete-session + (fn [{:keys [db]} [topic]] + {:db (update db + :wallet-connect/sessions + (fn [sessions] + (->> sessions + (remove #(= (:topic %) topic)) + (into []))))})) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/sessions_test.cljs b/src/status_im/contexts/wallet/wallet_connect/events/sessions_test.cljs new file mode 100644 index 00000000000..c5b5739060f --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/events/sessions_test.cljs @@ -0,0 +1,135 @@ +(ns status-im.contexts.wallet.wallet-connect.events.sessions-test + (:require + [cljs.test :refer-macros [is are testing]] + matcher-combinators.test + [re-frame.db :as rf-db] + status-im.contexts.wallet.wallet-connect.events.session-responses + status-im.contexts.wallet.wallet-connect.events.sessions + [test-helpers.unit :as h] + [tests.wallet-test-data :as test-data])) + +(defn- find-fx + [fx name] + (some #(when (= name (first %)) %) fx)) + +(defn- get-fx-arg + "Finds the arg value for an effect, if present in the fx vector" + [fx fx-name arg-fn] + (-> fx + (find-fx fx-name) + second + arg-fn)) + +(h/deftest-event :wallet-connect/on-session-delete + [event-id dispatch] + (testing "successfully deletes the session" + (swap! rf-db/app-db (fn [db] + (-> db + (test-data/add-networks-to-db) + (assoc :profile/profile {:test-networks-enabled? false}) + (assoc :wallet-connect/sessions + [{:topic "topic" + :chains ["eip155:1"]}])))) + (let [fx (:fx (dispatch [event-id + {:topic "topic" + :chains ["eip155:1"]}])) + get-rpc-fx-arg (partial get-fx-arg fx :json-rpc/call)] + (are [expected result] (match? expected result) + :json-rpc/call (-> fx (find-fx :json-rpc/call) first) + "wallet_disconnectWalletConnectSession" (get-rpc-fx-arg (comp :method first)) + ["topic"] (get-rpc-fx-arg (comp :params first)) + [:wallet-connect/delete-session "topic"] (get-rpc-fx-arg (comp :on-success first))))) + + (testing "ignore the deletion if session topic not found" + (let [topic-1 "topic-1" + topic-2 "topic-2"] + (swap! rf-db/app-db (fn [db] + (-> db + (test-data/add-networks-to-db) + (assoc :profile/profile {:test-networks-enabled? false}) + (assoc :wallet-connect/sessions + [{:topic topic-1 + :chains ["eip155:1"]}])))) + (is (match? nil + (dispatch [event-id + {:topic topic-2 + :chains ["eip155:1"]}]))))) + + (testing "ignore the deletion if the event is for the wrong network mode (testnet/mainnet)" + (let [topic "topic"] + (swap! rf-db/app-db (fn [db] + (-> db + (test-data/add-networks-to-db) + (assoc :profile/profile {:test-networks-enabled? true}) + (assoc :wallet-connect/sessions + [{:topic topic + :chains ["eip155:1"]}])))) + (is (match? nil + (dispatch [event-id + {:topic topic + :chains ["eip155:11155111"]}])))))) + +(h/deftest-event :wallet-connect/disconnect-dapp + [event-id dispatch] + (testing "disconnecting from dApp when online" + (reset! rf-db/app-db {:network/status :online + :wallet-connect/web3-wallet "mock"}) + (let [fx (:fx (dispatch [event-id {:topic "topic"}]))] + (are [expected result] (match? expected result) + :effects.wallet-connect/disconnect (ffirst fx) + "topic" (-> fx first second :topic) + "mock" (-> fx first second :web3-wallet)))) + + (testing "showing no-internet toast when offline" + (reset! rf-db/app-db {:network/status :offline}) + (is (match? :wallet-connect/no-internet-toast + (-> (dispatch [event-id {:topic "topic"}]) + :fx + first + second + first))))) + +(h/deftest-event :wallet-connect/get-sessions + [event-id dispatch] + (testing "the fx includes only for the available accounts" + (reset! rf-db/app-db {:wallet {:accounts {"available" {:address "0x123" + :operable? true} + "watch-only" {:address "0x456" + :operable? true + :watch-only? true} + "non-operable" {:address "0x789" + :operable? false}}} + :network/status :online + :wallet-connect/web3-wallet "mock"}) + (let [fx (:fx (dispatch [event-id])) + get-sessions-fx-arg (partial get-fx-arg fx :effects.wallet-connect/get-sessions)] + (are [expected result] (match? expected result) + :effects.wallet-connect/get-sessions (-> fx (find-fx :effects.wallet-connect/get-sessions) first) + true (get-sessions-fx-arg :online?) + '("0x123") (get-sessions-fx-arg :addresses) + "mock" (get-sessions-fx-arg :web3-wallet))))) + +(h/deftest-event :wallet-connect/get-sessions-success + [event-id dispatch] + (testing "sessions are stored in the db" + (let [sessions '({:topic "123"} {:topic "456"})] + (is (match? {:db {:wallet-connect/sessions sessions}} + (dispatch [event-id sessions])))))) + +(h/deftest-event :wallet-connect/on-new-session + [event-id dispatch] + (testing "new session is added to db" + (let [sessions '({:topic "123"} {:topic "456"}) + new-session {:topic "789"}] + (reset! rf-db/app-db {:wallet-connect/sessions sessions}) + (is (match? {:db {:wallet-connect/sessions (conj sessions new-session)}} + (dispatch [event-id new-session])))))) + +(h/deftest-event :wallet-connect/delete-session + [event-id dispatch] + (testing "session is deleted from db" + (let [sessions '({:topic "123"} {:topic "456"}) + expected '({:topic "123"})] + (reset! rf-db/app-db {:wallet-connect/sessions sessions}) + (is (match? {:db {:wallet-connect/sessions expected}} + (dispatch [event-id "456"])))))) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/change_network/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/change_network/view.cljs new file mode 100644 index 00000000000..8d7a1ab9668 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/change_network/view.cljs @@ -0,0 +1,98 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.change-network.view + (:require + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.wallet-connect.modals.common.header.components :as header] + [status-im.contexts.wallet.wallet-connect.modals.session-proposal.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.string])) + +(defn- on-dismiss + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn- make-list-item + [customization-color set-deactivate-chain-id deactivate-chain-id network] + (let [{:keys [full-name source chain-id]} network + on-select #(set-deactivate-chain-id chain-id) + selected? (= deactivate-chain-id chain-id)] + {:blur? true + :action :selector + :action-props {:type :radio + :blur? false + :customization-color customization-color + :checked? selected? + :on-change on-select} + :title full-name + :image :icon-avatar + :image-props {:icon source + :size :size-20}})) + +(defn view + [{:keys [on-success]}] + (let [max-active-networks? (rf/sub [:wallet/max-active-networks-reached?]) + dapp (rf/sub [:wallet-connect/current-request-dapp]) + dapp-icon (rf/sub [:wallet-connect/dapp-icon]) + activate-network (rf/sub [:wallet-connect/current-request-network]) + deactivatable-networks (rf/sub [:wallet/deactivatable-networks]) + {:keys [customization-color]} (rf/sub + [:wallet-connect/current-request-account-details]) + default-deactivate-chain-id (-> deactivatable-networks first :chain-id) + [deactivate-chain-id set-deactivate-chain-id] (rn/use-state (when max-active-networks? + default-deactivate-chain-id)) + success-pressed? (rn/use-ref-atom false) + on-press-success (fn [] + (reset! success-pressed? true) + (rf/dispatch + [:wallet-connect/activate-request-network + {:deactivate-chain-id deactivate-chain-id}]) + (rf/dispatch [:hide-bottom-sheet]) + ;; FIXME: not the most reliable way to make + ;; sure the balance was updated on the + ;; status-go side + (when on-success + (js/setTimeout on-success 1000)))] + (rn/use-unmount (fn [] + (when-not @success-pressed? + (rf/dispatch [:wallet-connect/on-request-modal-dismissed])))) + [rn/view {:style {:padding-horizontal 20}} + [rn/view {:style {:margin-bottom 20}} + [header/title-container + [header/title-text {:text (i18n/label :t/activate)}] + [header/title-summary + {:type :network + :label (:full-name activate-network) + :image-source (:source activate-network)}] + [header/title-text {:text (i18n/label :t/enable-network-to-sign-requests)}] + [header/title-summary + {:type :dapp + :label (:name dapp) + :image-source dapp-icon}]] + [quo/text + {:weight :medium + :size :paragraph-2} + (if max-active-networks? + (i18n/label :t/dapp-requests-network-access-with-limit) + (i18n/label :t/dapp-requests-network-access))]] + (when max-active-networks? + [quo/category + {:list-type :settings + :blur? true + :data (mapv (partial make-list-item + customization-color + set-deactivate-chain-id + deactivate-chain-id) + deactivatable-networks) + :container-style {:padding-horizontal 0}}]) + [quo/bottom-actions + {:actions :two-actions + :buttons-container-style style/footer-buttons-container + :button-two-label (i18n/label :t/decline) + :button-two-props {:type :grey + :on-press on-dismiss} + :button-one-label (i18n/label :t/confirm) + :button-one-props {:customization-color customization-color + :type :primary + :on-press on-press-success}}]])) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/data_block/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/data_block/view.cljs new file mode 100644 index 00000000000..eb2ce66499e --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/data_block/view.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.data-block.view + (:require [status-im.common.raw-data-block.view :as raw-data-block] + [utils.re-frame :as rf])) + +(defn view + [] + (let [display-data (rf/sub [:wallet-connect/current-request-display-data])] + [raw-data-block/view display-data])) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/fees_data_item/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/fees_data_item/view.cljs new file mode 100644 index 00000000000..3cd60a7a0e8 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/fees_data_item/view.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view + (:require [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [status-im.contexts.wallet.wallet-connect.modals.common.style :as style] + [utils.i18n :as i18n])) + +(defn- fees-subtitle + [{:keys [text error?]}] + (let [theme (quo.context/use-theme)] + [quo/text + {:weight :medium + :size :paragraph-2 + :style {:color (if error? + (colors/resolve-color :danger theme) + (colors/theme-colors colors/neutral-100 + colors/white + theme))}} + text])) + +(defn view + [{:keys [fees fees-error]}] + [quo/data-item + {:size :small + :status :default + :card? false + :container-style style/data-item + :title (i18n/label :t/max-fees) + :custom-subtitle (fn [] [fees-subtitle + {:text (or fees (i18n/label :t/no-fees)) + :error? (= fees-error :not-enough-assets-to-pay-gas-fees)}])}]) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/footer/style.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/footer/style.cljs new file mode 100644 index 00000000000..562df9ee807 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/footer/style.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.footer.style) + +(def content-container + {:padding-horizontal 20}) + +(def data-items-container + {:flex-direction :row + :padding-top 12 + :padding-bottom 4 + :gap 16}) + +(def auth-container + {:height 48 + :margin-vertical 12}) + +(def warning-container + {:align-items :center + :margin-bottom 12}) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/footer/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/footer/view.cljs new file mode 100644 index 00000000000..a8c3ff9ddc0 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/footer/view.cljs @@ -0,0 +1,80 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.footer.view + (:require [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.standard-authentication.core :as standard-authentication] + [status-im.contexts.wallet.sheets.buy-token.view :as buy-token] + [status-im.contexts.wallet.wallet-connect.modals.common.footer.style :as style] + [utils.hex :as hex] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- on-auth-success + [signatures] + (let [signature (-> signatures + first + :signature + hex/prefix-hex)] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:wallet-connect/respond signature]))) + +(defn- on-auth-fail + [error] + (rf/dispatch [:wallet-connect/on-sign-error error])) + +(defn- slide-button + [{:keys [disabled? slide-button-text]}] + (let [{:keys [customization-color]} (rf/sub [:wallet-connect/current-request-account-details]) + address (rf/sub [:wallet-connect/current-request-address]) + prepared-hash (rf/sub [:wallet-connect/prepared-hash])] + [rn/view {:style style/auth-container} + [standard-authentication/slide-sign + {:sign-payload [{:address address + :message prepared-hash}] + :size :size-48 + :track-text slide-button-text + :disabled? (or (not prepared-hash) disabled?) + :customization-color customization-color + :on-success on-auth-success + :on-fail on-auth-fail + :auth-button-label (i18n/label :t/confirm)}]])) + +(defn view + [{:keys [warning-label slide-button-text error-state]} & children] + (let [offline? (rf/sub [:network/offline?]) + native-token-symbol (rf/sub [:wallet-connect/current-request-network-native-token-symbol]) + theme (quo.context/use-theme)] + [:<> + (when (or offline? error-state) + [quo/alert-banner + {:action? (when error-state true) + :text (if offline? + (i18n/label :t/wallet-connect-no-internet-warning) + (i18n/label (condp = error-state + :not-enough-assets-to-pay-gas-fees + :t/not-enough-assets-to-pay-gas-fees + + :not-enough-assets + :t/not-enough-assets-for-transaction))) + :button-text (i18n/label :t/add-token {:token native-token-symbol}) + :on-button-press (rn/use-callback (fn [] + (rf/dispatch [:centralized-metrics/track + :metric/dapp-buy-eth]) + (rf/dispatch [:show-bottom-sheet + {:content buy-token/view}])))}]) + [rn/view {:style style/content-container} + (into [rn/view + {:style style/data-items-container}] + children) + [slide-button + {:disabled? (or offline? error-state) + :slide-button-text slide-button-text}] + [rn/view {:style style/warning-container} + [quo/text + {:size :paragraph-2 + :style {:color (if (= theme :dark) + colors/white-opa-70 + colors/neutral-80-opa-70)} + :weight :medium} + warning-label]]]])) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/header/components.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/header/components.cljs new file mode 100644 index 00000000000..a2d2707637a --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/header/components.cljs @@ -0,0 +1,28 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.header.components + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.wallet-connect.modals.common.header.style :as style])) + +(defn title-text + [{:keys [text]}] + [:<> + (->> (string/split text #" ") + (map + (fn [word] + ^{:key word} + [rn/view {:style style/word-container} + [quo/text + {:size :heading-1 + :weight :semi-bold} + (str " " word)]])) + (doall))]) + +(defn title-summary + [summary-tag-props] + [rn/view {:style style/dapp-container} + [quo/summary-tag summary-tag-props]]) + +(defn title-container + [& children] + (into [rn/view {:style style/header-container}] children)) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/header/style.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/header/style.cljs new file mode 100644 index 00000000000..a157843e871 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/header/style.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.header.style + (:require [quo.foundations.typography :as typography])) + +(def ^:private line-height (:line-height typography/heading-1)) + +(def header-container + {:padding-vertical 12 + :margin-left -4 + :justify-content :flex-start + :align-items :center + :flex-direction :row + :flex-wrap :wrap + :row-gap 2}) + +(def word-container + {:height line-height + :justify-content :center}) + +(def dapp-container + {:margin-top 0 + :height line-height + :margin-horizontal 4}) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/header/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/header/view.cljs new file mode 100644 index 00000000000..c24b561a911 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/header/view.cljs @@ -0,0 +1,22 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.header.view + (:require + [status-im.contexts.wallet.wallet-connect.modals.common.header.components :as header] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store])) + +(defn view + [{:keys [label dapp account]}] + [header/title-container + (let [{:keys [name iconUrl url]} dapp + image-source (data-store/compute-dapp-icon-path iconUrl url)] + [header/title-summary + {:type :dapp + :label name + :image-source image-source}]) + [header/title-text {:text label}] + (let [{:keys [emoji customization-color name]} account] + [header/title-summary + {:type :account + :emoji emoji + :label name + :customization-color customization-color}])]) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/list_info_box/style.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/list_info_box/style.cljs new file mode 100644 index 00000000000..20fb42683d2 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/list_info_box/style.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.list-info-box.style + (:require [quo.foundations.colors :as colors])) + +(defn container + [theme] + {:padding-horizontal 16 + :padding-vertical 12 + :border-radius 16 + :border-width 1 + :border-color (colors/theme-colors colors/neutral-10 colors/black-opa-30 theme) + :background-color (colors/theme-colors colors/neutral-2_5 colors/black-opa-30 theme)}) + +(def title + {:color colors/neutral-50 + :margin-bottom 8}) + +(def item + {:flex 1 + :flex-direction :row + :align-items :center + :gap 8}) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/list_info_box/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/list_info_box/view.cljs new file mode 100644 index 00000000000..2663034e864 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/list_info_box/view.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.list-info-box.view + (:require + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.contexts.wallet.wallet-connect.modals.common.list-info-box.style :as style] + [utils.i18n :as i18n] + [utils.string])) + +(defn view + [{:keys [dapp-name container-style]}] + (let [labels [(i18n/label :t/check-your-account-balance-and-activity) + (i18n/label :t/request-txns-and-message-signing)] + theme (quo.context/use-theme)] + [rn/view {:style (merge (style/container theme) container-style)} + [quo/text + {:style style/title + :weight :regular + :size :paragraph-2} + (i18n/label :t/dapp-will-be-able-to {:dapp-name dapp-name})] + (map-indexed + (fn [idx label] + ^{:key (str idx label)} + [rn/view {:style style/item} + [quo/icon :i/bullet + {:color colors/neutral-40}] + [quo/text + {:weight :regular + :size :paragraph-2 + :color colors/neutral-40} + label]]) + labels)])) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/page_nav/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/page_nav/view.cljs new file mode 100644 index 00000000000..13f2c2be9f9 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/page_nav/view.cljs @@ -0,0 +1,11 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view + (:require [quo.core :as quo] + [utils.re-frame :as rf])) + +(defn view + [{:keys [accessibility-label]}] + [quo/page-nav + {:icon-name :i/close + :background :blur + :on-press #(rf/dispatch [:wallet-connect/dismiss-request-modal]) + :accessibility-label accessibility-label}]) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/style.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/style.cljs new file mode 100644 index 00000000000..64dc38b3b0a --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/style.cljs @@ -0,0 +1,44 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.common.style + (:require [quo.foundations.colors :as colors] + [status-im.constants :as constants])) + +(defn container + [bottom] + {:position :absolute + :bottom bottom + :top 0 + :left 0 + :right 0 + :padding-top constants/sheet-screen-handle-height}) + +(def data-content-container + {:flex 1 + :padding-horizontal 20}) + +(def sign-message-content-container + (merge data-content-container + {:margin-bottom 10.5})) + +(def data-item + {:flex 1 + :background-color :transparent}) + +(def typed-data-item + (assoc data-item :margin-bottom 4)) + +(def data-border-container + {:padding-horizontal 12 + :padding-top 8 + :margin-top 10.5 + :margin-bottom 0 + :border-width 1 + :border-color colors/neutral-10 + :border-radius 16}) + +(def data-item-container + {:padding 10 + :margin-top 10.5 + :margin-bottom 0 + :border-width 1 + :border-color colors/neutral-10 + :border-radius 16}) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/send_transaction/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/send_transaction/view.cljs new file mode 100644 index 00000000000..84d60de829d --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/send_transaction/view.cljs @@ -0,0 +1,118 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.send-transaction.view + (:require [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.safe-area :as safe-area] + [status-im.constants :as constants] + [status-im.contexts.wallet.wallet-connect.modals.common.data-block.view :as data-block] + [status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as + fees-data-item] + [status-im.contexts.wallet.wallet-connect.modals.common.footer.view :as footer] + [status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header] + [status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav] + [status-im.contexts.wallet.wallet-connect.modals.common.style :as style] + [status-im.contexts.wallet.wallet-connect.utils.transactions :as transaction-utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- refetch-transaction + [] + (rf/dispatch [:wallet-connect/process-eth-send-transaction])) + +(def tabs-data + [{:id :tab/data :label (i18n/label :t/data)} + {:id :tab/hex :label (i18n/label :t/hex)}]) + +(defn- render-item + [props] + (let [[label value] props] + [quo/data-item + {:card? false + :container-style style/data-item + :title (str (name label) ":") + :subtitle value}])) + +(defn- tab-view + [selected-tab] + (let [{:keys [transaction]} (rf/sub [:wallet-connect/current-request]) + transaction-items (rn/use-memo #(-> transaction + (transaction-utils/transaction-hex-values->number) + (transaction-utils/transactions->display-array)) + [transaction])] + [:<> + (case selected-tab + :tab/data [gesture/flat-list + {:data transaction-items + :content-container-style style/data-item-container + :render-fn render-item + :shows-vertical-scroll-indicator false}] + :tab/hex [data-block/view])])) + +(defn view + [] + (let [bottom safe-area/bottom + {:keys [customization-color] + :as account} (rf/sub [:wallet-connect/current-request-account-details]) + dapp (rf/sub [:wallet-connect/current-request-dapp]) + network (rf/sub [:wallet-connect/current-request-network]) + {:keys [max-fees-fiat-formatted + error-state estimated-time]} (rf/sub + [:wallet-connect/current-request-transaction-information]) + [selected-tab set-selected-tab] (rn/use-state (:id (first tabs-data))) + on-change-tab #(set-selected-tab %) + refetch-interval-ref (rn/use-ref nil) + clear-interval (rn/use-callback (fn [] + (when (.-current refetch-interval-ref) + (js/clearInterval + (.-current refetch-interval-ref)))) + [refetch-interval-ref])] + (rn/use-mount + (fn [] + (clear-interval) + (set! (.-current refetch-interval-ref) + (js/setInterval refetch-transaction constants/wallet-connect-transaction-refresh-interval-ms)))) + + (rn/use-unmount (fn [] + (clear-interval) + (rf/dispatch [:wallet-connect/on-request-modal-dismissed]))) + + [rn/view {:style (style/container bottom)} + [quo/gradient-cover {:customization-color customization-color}] + [page-nav/view + {:accessibility-label :wallet-connect-sign-message-close}] + [rn/view {:flex 1} + [rn/view {:style style/data-content-container} + [header/view + {:label (i18n/label :t/wallet-connect-send-transaction-header) + :dapp dapp + :account account}] + [quo/segmented-control + {:size 32 + :blur? false + :default-active :tab/data + :data tabs-data + :on-change on-change-tab}] + [tab-view selected-tab]] + [footer/view + {:warning-label (i18n/label :t/wallet-connect-sign-warning) + :slide-button-text (i18n/label :t/slide-to-send) + :error-state error-state} + [quo/data-item + {:status :default + :card? false + :container-style style/data-item + :title (i18n/label :t/network) + :subtitle-type :network + :network-image (:source network) + :subtitle (:full-name network)}] + [fees-data-item/view + {:fees max-fees-fiat-formatted + :fees-error error-state}] + [quo/data-item + {:card? false + :container-style style/data-item + :title (i18n/label :t/est-time) + :subtitle (if estimated-time + (i18n/label :t/time-in-sec {:seconds (str estimated-time)}) + (i18n/label :t/unknown))}]]]])) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/style.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/style.cljs new file mode 100644 index 00000000000..be307fdc253 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/style.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.session-proposal.style) + +(def dapp-avatar + {:padding-horizontal 20 + :padding-top 12}) + +(def account-switcher-title + {:padding-horizontal 20}) + +(def account-switcher-list + {:margin-top 8 + :padding-horizontal 8}) + +(def footer-buttons-container + {:padding-horizontal 0}) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs new file mode 100644 index 00000000000..245066be89c --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs @@ -0,0 +1,163 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.session-proposal.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.wallet-connect.modals.common.list-info-box.view :as list-info-box] + [status-im.contexts.wallet.wallet-connect.modals.session-proposal.style :as style] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.string])) + +(defn- dapp-metadata + [] + (let [proposer (rf/sub [:wallet-connect/session-proposer]) + {:keys [icons name url]} (:metadata proposer) + first-icon (first icons) + dapp-name (data-store/compute-dapp-name name url) + profile-picture (data-store/compute-dapp-icon-path first-icon url)] + [:<> + [rn/view {:style style/dapp-avatar} + [quo/user-avatar + {:profile-picture profile-picture + :size :big + :full-name dapp-name}]] + [quo/page-top + {:title dapp-name + :description :context-tag + :context-tag {:type :icon + :size 32 + :icon :i/link + :context url}}]])) + +(defn- approval-note + [] + (let [dapp-name (rf/sub [:wallet-connect/session-proposer-name])] + [list-info-box/view + {:dapp-name dapp-name + :container-style {:margin-horizontal 20}}])) + +(defn- format-network-name + [network] + (-> network :network-name name string/capitalize)) + +(defn- set-current-proposal-address + [acc] + (fn [] + (rf/dispatch [:wallet-connect/set-current-proposal-address (:address acc)]) + (rf/dispatch [:hide-bottom-sheet]))) + +(defn- accounts-list + [] + (let [accounts (rf/sub [:wallet/operable-accounts]) + selected-address (rf/sub [:wallet-connect/current-proposal-address])] + [rn/view {:style style/account-switcher-list} + (for [{:keys [address] :as account} accounts] + ^{:key (str address)} + [quo/account-item + {:type :default + :state (if (= address selected-address) + :selected + :default) + :account-props account + :on-press (set-current-proposal-address account)}])])) + +(defn- account-switcher-sheet + [] + [:<> + [rn/view {:style style/account-switcher-title} + [quo/text + {:size :heading-2 + :weight :semi-bold + :accessibility-label "select-account-title"} + (i18n/label :t/select-account)]] + [accounts-list]]) + +(defn- show-account-switcher-bottom-sheet + [] + (rf/dispatch + [:show-bottom-sheet + {:content account-switcher-sheet}])) + +(defn- connection-category + [] + (let [{:keys [session-networks + all-networks-in-session?]} (rf/sub + [:wallet-connect/session-proposal-network-details]) + address (rf/sub [:wallet-connect/current-proposal-address]) + {:keys [name customization-color emoji]} (rf/sub [:wallet-connect/account-details-by-address + address]) + network-names (->> session-networks + (map format-network-name) + (string/join ", ")) + network-images (mapv :source session-networks) + data-item-common-props {:blur? false + :card? false + :status :default + :size :large} + account-data-item-props (assoc data-item-common-props + :right-content {:type :accounts + :size :size-32 + :data [{:emoji emoji + :customization-color + customization-color}]} + :on-press show-account-switcher-bottom-sheet + :title (i18n/label :t/account-title) + :subtitle name + :right-icon :i/chevron-right) + networks-data-item-props (assoc data-item-common-props + :right-content {:type :network + :data network-images} + :title (i18n/label :t/networks) + :subtitle (if all-networks-in-session? + (i18n/label :t/all-networks) + network-names))] + [quo/category + {:blur? false + :list-type :data-item + :data [account-data-item-props + networks-data-item-props]}])) + +(defn- footer + [] + (let [customization-color (rf/sub [:profile/customization-color])] + [quo/bottom-actions + {:actions :two-actions + :buttons-container-style style/footer-buttons-container + :button-two-label (i18n/label :t/decline) + :button-two-props {:type :grey + :accessibility-label :wc-deny-connection + :on-press #(rf/dispatch + [:dismiss-modal + :screen/wallet.wallet-connect-session-proposal])} + :button-one-label (i18n/label :t/connect) + :button-one-props {:customization-color customization-color + :type :primary + :accessibility-label :wc-connect + :on-press #(rf/dispatch + [:wallet-connect/approve-session])}}])) + +(defn- header + [] + [quo/page-nav + {:type :no-title + :background :blur + :icon-name :i/close + :on-press (rn/use-callback + #(rf/dispatch [:dismiss-modal :screen/wallet.wallet-connect-session-proposal])) + :accessibility-label :wc-session-proposal-top-bar}]) + +(defn view + [] + (rn/use-unmount #(rf/dispatch [:wallet-connect/reject-session-proposal])) + [floating-button-page/view + {:footer-container-padding 0 + :header [header] + :footer [footer]} + [rn/view + [dapp-metadata] + [connection-category] + [approval-note]]]) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/sign_message/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/sign_message/view.cljs new file mode 100644 index 00000000000..ccd1936f9a5 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/sign_message/view.cljs @@ -0,0 +1,68 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.sign-message.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.safe-area :as safe-area] + [status-im.common.raw-data-block.view :as data-block] + [status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as + fees-data-item] + [status-im.contexts.wallet.wallet-connect.modals.common.footer.view :as footer] + [status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header] + [status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav] + [status-im.contexts.wallet.wallet-connect.modals.common.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn typed-data-field + [{:keys [label value]}] + [quo/data-item + {:card? false + :container-style style/typed-data-item + :title label + :subtitle value}]) + +(defn typed-data-view + [] + (let [display-data (rf/sub [:wallet-connect/current-request-display-data])] + [gesture/flat-list + {:data display-data + :style style/data-border-container + :over-scroll-mode :never + :content-container-style {:padding-bottom 12} + :render-fn typed-data-field + :shows-vertical-scroll-indicator false}])) + +(defn message-data-view + [] + (let [display-data (rf/sub [:wallet-connect/current-request-display-data])] + [data-block/view display-data])) + +(defn display-data-view + [] + (let [typed-data? (rf/sub [:wallet-connect/typed-data-request?])] + (if typed-data? + [typed-data-view] + [message-data-view]))) + +(defn view + [] + (let [bottom safe-area/bottom + {:keys [customization-color] + :as account} (rf/sub [:wallet-connect/current-request-account-details]) + dapp (rf/sub [:wallet-connect/current-request-dapp])] + (rn/use-unmount #(rf/dispatch [:wallet-connect/on-request-modal-dismissed])) + [rn/view {:style (style/container bottom)} + [quo/gradient-cover {:customization-color customization-color}] + [page-nav/view + {:accessibility-label :wallet-connect-sign-message-close}] + [rn/view {:flex 1} + [rn/view {:style style/sign-message-content-container} + [header/view + {:label (i18n/label :t/wallet-connect-sign-message-header) + :dapp dapp + :account account}] + [display-data-view]] + [footer/view + {:warning-label (i18n/label :t/wallet-connect-sign-warning) + :slide-button-text (i18n/label :t/slide-to-sign)} + [fees-data-item/view]]]])) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/sign_transaction/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/sign_transaction/view.cljs new file mode 100644 index 00000000000..7f9bd10933a --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/sign_transaction/view.cljs @@ -0,0 +1,56 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.sign-transaction.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.contexts.wallet.wallet-connect.modals.common.data-block.view :as data-block] + [status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as + fees-data-item] + [status-im.contexts.wallet.wallet-connect.modals.common.footer.view :as footer] + [status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header] + [status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav] + [status-im.contexts.wallet.wallet-connect.modals.common.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn view + [] + (let [bottom safe-area/bottom + {:keys [customization-color] + :as account} (rf/sub [:wallet-connect/current-request-account-details]) + dapp (rf/sub [:wallet-connect/current-request-dapp]) + network (rf/sub [:wallet-connect/current-request-network]) + {:keys [max-fees-fiat-formatted + error-state]} (rf/sub [:wallet-connect/current-request-transaction-information])] + (rn/use-unmount #(rf/dispatch [:wallet-connect/on-request-modal-dismissed])) + [rn/view {:style (style/container bottom)} + [quo/gradient-cover {:customization-color customization-color}] + [page-nav/view + {:accessibility-label :wallet-connect-sign-message-close}] + [rn/view {:flex 1} + [rn/view {:style style/data-content-container} + [header/view + {:label (i18n/label :t/wallet-connect-sign-transaction-header) + :dapp dapp + :account account}] + [data-block/view]] + [footer/view + {:warning-label (i18n/label :t/wallet-connect-sign-warning) + :slide-button-text (i18n/label :t/slide-to-sign) + :error-text (when error-state + (i18n/label (condp = error-state + :not-enough-assets-to-pay-gas-fees + :t/not-enough-assets-to-pay-gas-fees + + :not-enough-assets + :t/not-enough-assets)))} + [quo/data-item + {:status :default + :card? false + :container-style style/data-item + :title (i18n/label :t/network) + :subtitle-type :network + :network-image (:source network) + :subtitle (:full-name network)}] + [fees-data-item/view + {:fees max-fees-fiat-formatted + :fees-error error-state}]]]])) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/data_store.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/data_store.cljs new file mode 100644 index 00000000000..9e3d3b76a34 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/data_store.cljs @@ -0,0 +1,102 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.data-store + (:require + [clojure.string :as string] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils :as wallet-utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks.utils] + utils.string)) + +(defn compute-dapp-name + "Sometimes dapps have no name or an empty name. Return url as name in that case" + [name url] + (if (seq name) + name + (when (seq url) + (-> url + utils.string/remove-trailing-slash + utils.string/remove-http-prefix + string/capitalize)))) + +(defn compute-dapp-icon-path + "Some dapps have icons with relative paths, make paths absolute in those cases, send nil if icon is missing" + [icon-path url] + (when (and (seq icon-path) + (seq url)) + (if (string/starts-with? icon-path "http") + icon-path + (str (utils.string/remove-trailing-slash url) icon-path)))) + +(def method-to-screen + {constants/wallet-connect-personal-sign-method :screen/wallet-connect.sign-message + constants/wallet-connect-eth-sign-typed-method :screen/wallet-connect.sign-message + constants/wallet-connect-eth-sign-typed-v4-method :screen/wallet-connect.sign-message + constants/wallet-connect-eth-send-transaction-method :screen/wallet-connect.send-transaction}) + +(defn get-request-method + [event] + (get-in event [:params :request :method])) + +(defn get-request-params + [event] + (get-in event [:params :request :params])) + +(defn get-request-chain-id + [event] + (-> event + (get-in [:params :chainId]) + networks.utils/eip155->chain-id)) + +(defn get-db-current-request-event + [db] + (get-in db [:wallet-connect/current-request :event])) + +(defn get-session-dapp-metadata + [proposal] + (let [metadata (get-in proposal [:params :proposer :metadata]) + origin (get-in proposal [:verifyContext :verified :origin])] + (or metadata {:url origin}))) + +(defn get-current-request-dapp + [request sessions] + (let [request-topic (get-in request [:event :topic])] + (->> sessions + (filter (fn [session] + (= (:topic session) + request-topic))) + first))) + +(defn get-dapp-redirect-url + [session] + (get-in session [:peer :metadata :redirect :native])) + +(defn get-dapp-name + [session] + (get-in session [:peer :metadata :name])) + +(defn get-db-current-request-params + [db] + (-> db + get-db-current-request-event + get-request-params)) + +(defn get-total-connected-dapps + [db] + (-> db + :wallet-connect/sessions + count + inc)) + +(defn get-session-by-topic + [db topic] + (->> db + :wallet-connect/sessions + (filter #(= (:topic %) topic)) + first)) + +(defn get-account-by-session + [db session] + (let [accounts (get-in db [:wallet :accounts]) + session-account-address (first (:accounts session)) + [_ address] (network-utils/split-network-full-address session-account-address)] + (wallet-utils/get-account-by-address (vals accounts) address))) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/data_store_test.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/data_store_test.cljs new file mode 100644 index 00000000000..c5ca79b2b28 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/data_store_test.cljs @@ -0,0 +1,85 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.data-store-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [clojure.string :as string] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as sut] + [utils.string])) + +(deftest get-current-request-dapp-test + (testing "returns the correct dapp based on the request's origin" + (let [request {:event {:topic "123"}} + sessions [{:topic "123"} + {:topic "456"}]] + (is (= {:topic "123"} + (sut/get-current-request-dapp request sessions))))) + + (testing "returns nil if no matching dapp is found" + (let [request {:event {:topic "123"}} + sessions [{:topic "456"}]] + (is (nil? (sut/get-current-request-dapp request sessions)))))) + +(deftest get-dapp-redirect-url-test + (testing "returns the native redirect URL if it exists" + (let [session {:peer {:metadata {:redirect {:native "native://redirect-url"}}}}] + (is (= "native://redirect-url" + (sut/get-dapp-redirect-url session))))) + + (testing "returns nil if no redirect URL is found" + (let [session {:peer {:metadata {}}}] + (is (nil? (sut/get-dapp-redirect-url session)))))) + +(deftest get-total-connected-dapps-test + (testing "returns the total number of connected dApps plus 1" + (let [db {:wallet-connect/sessions [{:url "https://dapp1.com"} + {:url "https://dapp2.com"}]}] + (is (= 3 (sut/get-total-connected-dapps db))))) + + (testing "returns 1 when there are no connected dApps" + (let [db {:wallet-connect/sessions []}] + (is (= 1 (sut/get-total-connected-dapps db))))) + + (testing "handles nil sessions correctly" + (let [db {:wallet-connect/sessions nil}] + (is (= 1 (sut/get-total-connected-dapps db)))))) + +(deftest get-session-by-topic-test + (testing "returns the correct session based on the topic" + (let [db {:wallet-connect/sessions [{:topic "topic1" :url "https://dapp1.com"} + {:topic "topic2" :url "https://dapp2.com"}]} + topic "topic1"] + (is (= {:topic "topic1" :url "https://dapp1.com"} + (sut/get-session-by-topic db topic))))) + + (testing "returns nil if no matching session is found" + (let [db {:wallet-connect/sessions [{:topic "topic1" :url "https://dapp1.com"}]} + topic "topic2"] + (is (nil? (sut/get-session-by-topic db topic))))) + + (testing "handles nil sessions correctly" + (let [db {:wallet-connect/sessions nil} + topic "topic1"] + (is (nil? (sut/get-session-by-topic db topic))))) + + (testing "handles empty sessions correctly" + (let [db {:wallet-connect/sessions []} + topic "topic1"] + (is (nil? (sut/get-session-by-topic db topic)))))); + +(deftest get-account-by-session-test + (testing "get-account-by-session function" + (let [db {:wallet {:accounts {"0x123" {:address "0x123"} + "0x456" {:address "0x456"} + "0x789" {:address "0x789"}}}} + session {:accounts ["network:0x456"]}] + (with-redefs [network-utils/split-network-full-address (fn [addr] + (string/split addr #":"))] + (is (= (sut/get-account-by-session db session) {:address "0x456"})))) + + (let [db {:wallet {:accounts {"0x123" {:address "0x123"} + "0x456" {:address "0x456"} + "0x789" {:address "0x789"}}}} + session {:accounts ["network:0x999"]}] + (with-redefs [network-utils/split-network-full-address (fn [addr] + (string/split addr #":"))] + (is (= (sut/get-account-by-session db session) nil)))))) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/networks.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/networks.cljs new file mode 100644 index 00000000000..96649f576a8 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/networks.cljs @@ -0,0 +1,66 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.networks + (:require [clojure.edn :as edn] + [clojure.set :as set] + [clojure.string :as string] + [status-im.contexts.wallet.networks.db :as networks.db] + [utils.string])) + +(defn chain-id->eip155 + [chain-id] + (str "eip155:" chain-id)) + +(defn eip155->chain-id + [chain-id-str] + (-> chain-id-str + (string/split #":") + last + edn/read-string)) + +(defn format-eip155-address + [address chain-id] + (str chain-id ":" address)) + +(defn session-networks-allowed? + [supported-chain-ids {:keys [chains]}] + (let [session-chain-ids (set (map (fn [chain] + (-> chain + (string/split ":") + second + js/parseInt)) + chains))] + (set/subset? session-chain-ids supported-chain-ids))) + +(defn get-proposal-networks + [proposal] + (let [required-namespaces (get-in proposal [:params :requiredNamespaces]) + optional-namespaces (get-in proposal [:params :optionalNamespaces])] + (->> [required-namespaces optional-namespaces] + (map #(get-in % [:eip155 :chains])) + (apply concat) + (into #{})))) + +(defn proposal-networks-intersection + [proposal supported-networks] + (let [proposed-networks (get-proposal-networks proposal)] + (->> supported-networks + (filter #(->> % + chain-id->eip155 + (contains? proposed-networks)))))) + +(defn required-networks-supported? + [proposal supported-networks] + (let [supported-namespaces #{:eip155} + required-namespaces (get-in proposal [:params :requiredNamespaces])] + (when (every? #(contains? supported-namespaces %) + (keys required-namespaces)) + (let [required-networks (get-in required-namespaces [:eip155 :chains]) + supported-eip155 (set (map chain-id->eip155 supported-networks))] + (every? #(contains? supported-eip155 %) + required-networks))))) + +(defn event-should-be-handled? + [db {:keys [topic]}] + (let [chain-ids (networks.db/get-chain-ids db)] + (some #(and (= (:topic %) topic) + (session-networks-allowed? chain-ids %)) + (:wallet-connect/sessions db)))) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/rpc.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/rpc.cljs new file mode 100644 index 00000000000..937fbe7cd4c --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/rpc.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.rpc + (:require [promesa.core :as promesa] + [status-im.common.json-rpc.events :as rpc-events] + [utils.transforms :as transforms])) + +(defn wallet-disconnect-persisted-session + [topic] + (rpc-events/call-async "wallet_disconnectWalletConnectSession" true topic)) + +(defn wallet-get-persisted-sessions + ([] + (let [now (-> (js/Date.) .getTime (quot 1000))] + (wallet-get-persisted-sessions now))) + ([expiry-timestamp] + (rpc-events/call-async "wallet_getWalletConnectActiveSessions" false expiry-timestamp))) + +(defn wallet-persist-session + [session] + (->> session + transforms/clj->json + (rpc-events/call-async "wallet_addWalletConnectSession" false))) + +(defn wallet-get-transaction-estimated-time + [chain-id max-fee-per-gas] + (-> (rpc-events/call-async "wallet_getTransactionEstimatedTime" true chain-id max-fee-per-gas) + (promesa/then transforms/js->clj))) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/sessions.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/sessions.cljs new file mode 100644 index 00000000000..07b99ca0aa5 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/sessions.cljs @@ -0,0 +1,154 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.sessions + (:require + [clojure.string :as string] + [promesa.core :as promesa] + [react-native.wallet-connect :as wallet-connect] + [status-im.constants :as constants] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [status-im.contexts.wallet.wallet-connect.utils.rpc :as rpc] + [taoensso.timbre :as log] + [utils.transforms :as transforms])) + +(defn sdk-session->db-session + [{:keys [topic expiry pairingTopic] :as session}] + {:topic topic + :expiry expiry + :sessionJson (transforms/clj->json session) + :pairingTopic pairingTopic + :name (get-in session [:peer :metadata :name]) + :iconUrl (get-in session [:peer :metadata :icons 0]) + :url (get-in session [:peer :metadata :url]) + :accounts (get-in session [:namespaces :eip155 :accounts]) + :chains (get-in session [:namespaces :eip155 :chains]) + :disconnected false}) + +(defn filter-operable-accounts + [accounts] + (filter #(and (:operable? %) + (not (:watch-only? %))) + accounts)) + +(defn filter-sessions-for-account-addresses + [account-addresses sessions] + (filter (fn [{:keys [accounts]}] + (some (fn [account] + (some (fn [account-address] + (string/includes? account account-address)) + account-addresses)) + accounts)) + sessions)) + +(defn latest-connected-account-address + [sessions] + (let [all-accounts (->> sessions + (sort-by :expiry >) + first + :accounts)] + (-> all-accounts + first + (string/split #":") + last))) + +(defn- parse-session-accounts + [{:keys [sessionJson] :as session}] + (assoc session + :accounts + (-> sessionJson + transforms/json->clj + :namespaces + :eip155 + :accounts))) + +(defn- find-inactive-sessions + [active-sessions persisted-sessions] + (->> persisted-sessions + (filter #(->> % + :topic + (contains? (->> active-sessions + (map :topic) + set)) + not)))) + +(defn get-persisted-sessions + [] + (-> (rpc/wallet-get-persisted-sessions) + (promesa/then #(map parse-session-accounts %)) + (promesa/catch (fn [err] + (throw (ex-info "Failed to get persisted WalletConnect sessions" + {:error err + :code :error/wc-get-persisted-sessions})))))) + +(defn get-active-sessions + [web3-wallet addresses] + (-> (wallet-connect/get-active-sessions web3-wallet) + (promesa/then #(->> + (transforms/js->clj %) + vals + (map sdk-session->db-session) + (filter-sessions-for-account-addresses addresses))) + (promesa/catch (fn [err] + (throw (ex-info "Failed to get active WalletConnect sessions" + {:error err + :code :error/wc-get-active-sessions})))))) + +(defn sync-persisted-sessions + [active-sessions persisted-sessions] + (-> (promesa/all + (for [session (find-inactive-sessions active-sessions + persisted-sessions)] + (do (log/info "Syncing disconnected session with persistance" session) + (rpc/wallet-disconnect-persisted-session (:topic session))))) + (promesa/catch (fn [err] + (throw (ex-info "Failed to synchronize persisted sessions" + {:error err + :code :error/wc-sync-persisted-sessions})))))) + +(defn get-sessions + [web3-wallet addresses online?] + (promesa/let [persisted-sessions (get-persisted-sessions)] + (if online? + (promesa/let [active-sessions (get-active-sessions web3-wallet addresses)] + (log/info "Got active Wallet Connect sessions" (map :topic active-sessions)) + ;; NOTE: handling the error here, so that if persistance fails, it doesn't affect the active + ;; sessions + (-> (sync-persisted-sessions active-sessions persisted-sessions) + (promesa/catch #(log/error %))) + active-sessions) + persisted-sessions))) + +(defn disconnect + [web3-wallet topic] + (let [reason (wallet-connect/get-sdk-error constants/wallet-connect-user-disconnected-reason-key)] + (-> + (promesa/do + (wallet-connect/disconnect-session {:web3-wallet web3-wallet + :topic topic + :reason reason}) + (rpc/wallet-disconnect-persisted-session topic)) + (promesa/catch (fn [err] + (throw (ex-info "Failed to disconnect dapp" + {:err err + :code :error/wc-disconnect-dapp}))))))) + +(defn approve + [{:keys [web3-wallet address session-networks proposal-request]}] + (let [{:keys [params id]} proposal-request + accounts (-> (partial networks/format-eip155-address address) + (map session-networks))] + (-> (promesa/let [session + (wallet-connect/approve-session + {:web3-wallet web3-wallet + :id id + :approved-namespaces (->> + {:eip155 + {:chains session-networks + :accounts accounts + :methods constants/wallet-connect-supported-methods + :events constants/wallet-connect-supported-events}} + (wallet-connect/build-approved-namespaces params))})] + (rpc/wallet-persist-session session) + (transforms/js->clj session)) + (promesa/catch (fn [err] + (throw (ex-info "Failed to approve session" + {:err err + :code :error/wc-approve}))))))) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/transactions.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/transactions.cljs new file mode 100644 index 00000000000..e906ed22aea --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/transactions.cljs @@ -0,0 +1,144 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.transactions + (:require [cljs-bean.core :as bean] + [clojure.string :as string] + [native-module.core :as native-module] + [promesa.core :as promesa] + [status-im.constants :as constants] + [status-im.contexts.wallet.rpc :as wallet-rpc] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [status-im.contexts.wallet.wallet-connect.utils.rpc :as wallet-connect-rpc] + [utils.money :as money] + [utils.transforms :as transforms])) + +(defn transaction-request? + [event] + (->> (data-store/get-request-method event) + (contains? constants/wallet-connect-transaction-methods))) + +;; NOTE: Currently we don't allow the user to configure the tx priority as we don't +;; show the estimated time, but when we implement it, we should allow to change it +(def ^:constant default-tx-priority :medium) + +(defn- strip-hex-prefix + "Strips the extra 0 in hex value if present" + [hex-value] + (let [formatted-hex (string/replace hex-value #"^0x0*" "0x")] + (if (= formatted-hex "0x") + "0x0" + formatted-hex))) + +(defn- format-tx-hex-values + "Due to how status-go expects hex values, we should remove the extra 0s in transaction hex values e.g. 0x0f -> 0xf" + [tx f] + (let [tx-keys [:gasLimit :gas :gasPrice :nonce :value :maxFeePerGas :maxPriorityFeePerGas]] + (reduce (fn [acc tx-key] + (if (and (contains? tx tx-key) + (not (nil? (get tx tx-key)))) + (update acc tx-key f) + acc)) + tx + tx-keys))) + +(defn- prepare-transaction-for-rpc + "Formats the transaction and transforms it into a stringified JS object, ready to be passed to an RPC call." + [tx] + (-> tx + ;; NOTE: removing `:nonce` to compute it when building the transaction on status-go + (dissoc :nonce) + (format-tx-hex-values strip-hex-prefix) + bean/->js + (transforms/js-stringify 0))) + +(defn beautify-transaction + [tx] + (-> tx + clj->js + (js/JSON.stringify nil 2))) + +(defn transaction-hex-values->number + [tx] + (let [hex->number #(-> % (subs 2) native-module/hex-to-number)] + (-> tx + (format-tx-hex-values hex->number)))) + +(defn- gwei->hex + [gwei] + (->> gwei + money/gwei->wei + native-module/number-to-hex + (str "0x"))) + +(defn- get-max-fee-per-gas-key + "Mapping transaction priority (which determines how quickly a tx is processed) + to the `suggested-routes` key that should be used for `:maxPriorityFeePerGas`. + + Returns `:high` | `:medium` | `:low`" + [tx-priority] + (get {:high :maxFeePerGasHigh + :medium :maxFeePerGasMedium + :low :maxFeePerGasLow} + tx-priority)) + +(defn- dynamic-fee-tx? + "Checks if a transaction has dynamic fees (EIP1559)" + [tx] + (every? tx [:maxFeePerGas :maxPriorityFeePerGas])) + +(defn- tx->eip1559-tx + "Adds `:maxFeePerGas` and `:maxPriorityFeePerGas` for dynamic fee support (EIP1559) and + removes `:gasPrice`, if the chain supports EIP1559 and the transaction doesn't already + have dynamic fees." + [tx suggested-fees tx-priority] + (if (and (:eip1559Enabled suggested-fees) + (not (dynamic-fee-tx? tx))) + (let [max-fee-per-gas-key (get-max-fee-per-gas-key tx-priority) + max-fee-per-gas (-> suggested-fees max-fee-per-gas-key gwei->hex) + max-priority-fee-per-gas (-> suggested-fees :maxPriorityFeePerGas gwei->hex)] + (-> tx + (assoc + :maxFeePerGas max-fee-per-gas + :maxPriorityFeePerGas max-priority-fee-per-gas) + ;; NOTE: `:gasPrice` is used only for legacy Tx, so we discard it in favor of dynamic fees + (dissoc :gasPrice))) + tx)) + +(defn- prepare-transaction-fees + "Makes sure the transaction has the correct gas and fees properties" + [tx tx-priority suggested-fees] + (-> (assoc tx + ;; NOTE: `gasLimit` is ignored on status-go when building a transaction + ;; (`wallet_buildTransaction`), so we're setting it as the `gas` property + :gas + (or (:gasLimit tx) + (:gas tx))) + (dissoc :gasLimit) + (tx->eip1559-tx suggested-fees tx-priority))) + +(defn prepare-transaction + "Formats and builds the incoming transaction, adding the missing properties and returning the final + transaction, along with the transaction hash and the suggested fees" + [tx chain-id tx-priority] + (promesa/let [suggested-fees (wallet-rpc/get-suggested-fees chain-id) + {:keys [tx-args message-to-sign]} (->> + (prepare-transaction-fees tx + tx-priority + suggested-fees) + prepare-transaction-for-rpc + (wallet-rpc/build-transaction + chain-id)) + estimated-time (wallet-connect-rpc/wallet-get-transaction-estimated-time + chain-id + (:maxPriorityFeePerGas suggested-fees))] + {:tx-args tx-args + :tx-hash message-to-sign + :suggested-fees suggested-fees + :estimated-time estimated-time})) + +(defn transactions->display-array + [data] + (remove (fn [[k v]] + (or (= v "0x") + (= k :MultiTransactionID) + (= k :Symbol))) + data)) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/typed_data.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data.cljs new file mode 100644 index 00000000000..4068336e901 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data.cljs @@ -0,0 +1,102 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.typed-data + (:require [clojure.string :as string] + [status-im.constants :as constants] + [status-im.contexts.wallet.rpc :as wallet-rpc] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [utils.number :as number])) + +(declare flatten-data) + +(defn- format-flattened-key + [k] + (cond + (keyword? k) (name k) + (number? k) (str k) + (string? k) k + :else "unsupported-key")) + +(defn- flatten-map + [data path] + (reduce-kv (fn [acc k v] + (->> (format-flattened-key k) + (conj path) + (flatten-data v acc))) + [] + data)) + +(defn- flatten-vec + [data path] + (->> data + (map-indexed vector) + (reduce (fn [acc [idx v]] + (->> (str idx) + (conj path) + (flatten-data v acc))) + []))) + +(defn flatten-data + "Recursively flatten a map or vector into a flat vector. + + e.g. `[[[\"person\" \"first-name\"] \"Rich\"] + [[[\"person\" \"last-name\"] \"Hickey\"]]]`" + ([value] + (flatten-data value [] [])) + ([value acc path] + (cond + (map? value) (into acc (flatten-map value path)) + (vector? value) (into acc (flatten-vec value path)) + :else (conj acc [path value])))) + +(defn format-fields + "Format the fields into maps with `:label` & `:value`, where the label + is the flattened keys joined with a separator + + e.g. `{:label \"person: first-name:\" :value \"Rich\"}`" + [data separator] + (mapv (fn [[kv v]] + {:label (-> separator + (string/join kv) + (str separator) + string/trim) + :value v}) + data)) + +(defn flatten-typed-data + "Flatten typed data and prepare it for UI" + [typed-data] + (-> typed-data + (select-keys [:domain :message]) + flatten-data + (format-fields ": "))) + +(defn get-chain-id + "Returns the `:chain-id` from typed data if it's present and if the EIP712 domain defines it. Without + the `:chain-id` in the domain type, it will not be signed as part of the typed-data." + [typed-data] + (let [chain-id-type? (->> typed-data + :types + :EIP712Domain + (some #(= "chainId" (:name %)))) + data-chain-id (-> typed-data + :domain + :chainId + number/parse-int)] + (when chain-id-type? + data-chain-id))) + +(defn sign + [password address data chain-id-eip155 version] + (let [legacy? (= version :v1) + chain-id (networks/eip155->chain-id chain-id-eip155)] + (wallet-rpc/safe-sign-typed-data data + address + password + chain-id + legacy?))) + +(defn typed-data-request? + [method] + (contains? + #{constants/wallet-connect-eth-sign-typed-v4-method + constants/wallet-connect-eth-sign-typed-method} + method)) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/typed_data_test.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data_test.cljs new file mode 100644 index 00000000000..c1f72bcfb4b --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data_test.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.typed-data-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.wallet.wallet-connect.utils.typed-data :as sut])) + +(deftest flatten-data-test + (testing "correctly returns the fallback when key is not string/keyword/number" + (is (match? [[["unsupported-key"] "value"]] + (sut/flatten-data {[1 2] "value"})))) + + (testing "correctly flattens a simple map" + (is (match? [[["zero"] 0] + [["one"] 1] + [["2"] 2]] + (sut/flatten-data {:zero 0 + "one" 1 + 2 2})))) + + (testing "correctly flattens a simple vec" + (is (match? [[["0"] "zero"] [["1"] "one"]] + (sut/flatten-data ["zero" "one"])))) + + (testing "correctly flattens a nested map" + (is (match? [[["nested" "child"] "child value"] + [["nested" "nested" "0"] "child one"] + [["nested" "nested" "1"] "child two"] + [["flat"] "child value"]] + (sut/flatten-data {:nested {:child "child value" + :nested ["child one" "child two"]} + :flat "child value"})))) + + (testing "correctly flattens a nested vector" + (is (match? [[["0" "flat"] 1] + [["0" "nested-vector" "0"] 1] + [["0" "nested-vector" "1"] 2] + [["0" "nested-map" "one"] 1]] + (sut/flatten-data [{:flat 1 + :nested-vector [1 2] + :nested-map {:one 1}}]))))) + +(deftest flatten-typed-data-test + (testing "successfully extracts, flattens and formats the typed data" + (is (match? [{:label "domain: chain-id:" :value 1} + {:label "message: to: address:" :value "0x"} + {:label "message: from: address:" :value "0x"} + {:label "message: amount:" :value "0x"}] + (sut/flatten-typed-data {:domain {:chain-id 1} + :types {:Tx [{:name "to" + :type "address"}]} + :message {:to {:address "0x"} + :from {:address "0x"} + :amount "0x"}}))))) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/uri.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/uri.cljs new file mode 100644 index 00000000000..1525eb72da7 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/uri.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.uri + (:require [react-native.wallet-connect :as wallet-connect])) + +(defn version-supported? + [version] + (= version 2)) + +(defn- current-timestamp + [] + (quot (.getTime (js/Date.)) 1000)) + +(defn timestamp-expired? + [expiry-timestamp] + (when expiry-timestamp + (> (current-timestamp) expiry-timestamp))) + +(defn valid-wc-uri? + [parsed-uri] + (let [{:keys [topic version]} parsed-uri] + (and (seq topic) + (number? version)))) + +(defn valid-uri? + "Check if the uri is in the wallet-connect format. + At this stage, the uri might be expired or from an unsupported version" + [s] + (-> s + wallet-connect/parse-uri + valid-wc-uri?)) diff --git a/src/status_im/core.cljs b/src/status_im/core.cljs index bb71a7f7d22..3b94ac2d86b 100644 --- a/src/status_im/core.cljs +++ b/src/status_im/core.cljs @@ -1,21 +1,88 @@ (ns status-im.core - (:require [re-frame.core :as re-frame] - [status-im.utils.error-handler :as error-handler] - [status-im.ui.components.react :as react] - [reagent.core :as reagent] - status-im.transport.impl.receive - [taoensso.timbre :as log] - [status-im.utils.config :as config] - [status-im.react-native.js-dependencies :as js-dependencies] - [goog.object :as object])) - -(when js/goog.DEBUG - (object/set js/console "ignoredYellowBox" #js ["re-frame: overwriting"])) - -(defn init [app-root] - (log/set-level! config/log-level) - (error-handler/register-exception-handler!) - (re-frame/dispatch [:initialize-keychain]) - (when config/testfairy-enabled? - (.begin js-dependencies/testfairy config/testfairy-token)) - (.registerComponent react/app-registry "StatusIm" #(reagent/reactify-component app-root))) + (:require + ;; NOTE: Do NOT sort i18n-resources because it MUST be loaded first. + [status-im.setup.i18n-resources :as i18n-resources] + ;; NOTE: reagent.config Must be before any component definition, so they take the config + ;; properly + #_{:clj-kondo/ignore [:unsorted-required-namespaces]} + reagent.config + ["@walletconnect/react-native-compat"] + legacy.status-im.events + legacy.status-im.subs.root + [native-module.core :as native-module] + [re-frame.core :as re-frame] + [re-frame.interop :as interop] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.shake :as react-native-shake] + [reagent.core] + [status-im.common.log :as logging] + [taoensso.timbre :as log] + [status-im.common.universal-links :as universal-links] + [status-im.config :as config] + [status-im.contexts.profile.push-notifications.events :as notifications] + [status-im.feature-flags :as ff] + [status-im.navigation.core :as navigation] + status-im.contexts.wallet.signals + status-im.events + [status-im.setup.dev :as dev] + [status-im.setup.global-error :as global-error] + [status-im.setup.interceptors :as interceptors] + status-im.subs.root + [utils.i18n :as i18n] + [status-im.setup.status-backend-client :as status-backend-client])) + +;;;; re-frame RN setup +(set! interop/next-tick js/setTimeout) + +;; Note: In the past we've configured reagent to run its batch rendering +;; faster by overriding the next-tick function. This technique could be useful +;; if we want to adjust the batch speed for different frame-rates since by +;; default reagent tunes its batch rendering for 60FPS. +;; +;; For example, this code would have batches rendering as fast as possible +;; (set! reagent.impl.batching/next-tick js/setImmediate) +;; +;; While this example would approximate 120FPS +;; (set! reagent.impl.batching/next-tick #(js/setTimeout % 8)) +;; +;; And under the hood this is what reagent will use for approximately 60FPS: +;; (set! reagent.impl.batching/next-tick #(js/setTimeout % 16)) + +(def adjust-resize 16) + +(defn is-hermes + [] + (boolean (.-HermesInternal js/global))) + +;; ignore warning: init used in shadow-cljs.edn +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn init + [] + (status-backend-client/init) + (navigation/init) + (native-module/init #(re-frame/dispatch [:signals/signal-received %])) + (when platform/android? + (native-module/set-soft-input-mode adjust-resize)) + (logging/setup (config/log-level)) + (global-error/register-handler) + (notifications/listen-notifications) + (.addEventListener rn/app-state "change" #(re-frame/dispatch [:app-state-change %])) + (i18n/set-language "en") + (i18n-resources/load-language "en") + (react-native-shake/add-shake-listener #(re-frame/dispatch [:shake-event])) + (universal-links/initialize) + (interceptors/register-global-interceptors) + + (when config/debug-or-pr-build? + (ff/load-flags)) + + (dev/setup) + (log/info "hermesEnabled ->" (is-hermes)) + + (re-frame/dispatch-sync [:app-started]) + + ;; Required for wallet-connect + ;; https://github.com/WalletConnect/walletconnect-monorepo/issues/3235#issuecomment-1645767800 + (when-not (.-BigInt js/global) + (set! js/BigInt (js/require "big-integer")))) diff --git a/src/status_im/core_spec.cljs b/src/status_im/core_spec.cljs new file mode 100644 index 00000000000..50231a246b4 --- /dev/null +++ b/src/status_im/core_spec.cljs @@ -0,0 +1,9 @@ +(ns status-im.core-spec + (:require + [status-im.common.floating-button-page.component-spec] + [status-im.contexts.chat.messenger.messages.content.audio.component-spec] + [status-im.contexts.communities.actions.community-options.component-spec] + [status-im.contexts.shell.share.wallet.component-spec] + [status-im.contexts.wallet.add-account.add-address-to-watch.component-spec] + [status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.component-spec] + [status-im.contexts.wallet.send.input-amount.component-spec])) diff --git a/src/status_im/data_store/accounts.cljs b/src/status_im/data_store/accounts.cljs deleted file mode 100644 index 4b17f38d8c4..00000000000 --- a/src/status_im/data_store/accounts.cljs +++ /dev/null @@ -1,59 +0,0 @@ -(ns status-im.data-store.accounts - (:require [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core] - [status-im.utils.types :as types])) - -;; TODO janherich: define as cofx once debug handlers are refactored -(defn get-by-address [address] - (-> @core/base-realm - (core/get-by-field :account :address address) - (core/single-clj :account) - (update :settings core/deserialize))) - -(defn- deserialize-bootnodes [bootnodes] - (reduce-kv - (fn [acc id {:keys [chain] :as bootnode}] - (assoc-in acc [chain id] bootnode)) - {} - bootnodes)) - -(defn- serialize-bootnodes [bootnodes] - (->> bootnodes - vals - (mapcat vals))) - -(defn- deserialize-account [account] - (-> account - (update :settings core/deserialize) - (update :bootnodes deserialize-bootnodes) - (update :networks (partial reduce-kv - (fn [acc network-id props] - (assoc acc network-id - (update props :config types/json->clj))) - {})))) - -(re-frame/reg-cofx - :data-store/get-all-accounts - (fn [coeffects _] - (assoc coeffects :all-accounts (-> @core/base-realm - (core/get-all :account) - (core/all-clj :account) - (as-> accounts - (map deserialize-account accounts)))))) - -(defn- serialize-account [account] - (-> account - (update :settings core/serialize) - (update :bootnodes serialize-bootnodes) - (update :networks (partial map (fn [[_ props]] - (update props :config types/clj->json)))))) - -(defn save-account-tx - "Returns tx function for saving account" - [{:keys [after-update-event] :as account}] - (fn [realm] - (let [account-to-save (-> (serialize-account account) - (dissoc :after-update-event))] - (core/create realm :account account-to-save true) - (when after-update-event - (re-frame/dispatch after-update-event))))) diff --git a/src/status_im/data_store/browser.cljs b/src/status_im/data_store/browser.cljs deleted file mode 100644 index d861c16add7..00000000000 --- a/src/status_im/data_store/browser.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.data-store.browser - (:require [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(re-frame/reg-cofx - :data-store/all-browsers - (fn [cofx _] - (assoc cofx :all-stored-browsers (-> @core/account-realm - (core/get-all :browser) - (core/sorted :timestamp :desc) - (core/all-clj :browser))))) - -(defn save-browser-tx - "Returns tx function for saving browser" - [{:keys [browser-id] :as browser}] - (fn [realm] - (core/create realm :browser browser true))) - -(defn remove-browser-tx - "Returns tx function for removing browser" - [browser-id] - (fn [realm] - (let [browser (core/single (core/get-by-field realm :browser :browser-id browser-id))] - (core/delete realm browser)))) diff --git a/src/status_im/data_store/chats.cljs b/src/status_im/data_store/chats.cljs deleted file mode 100644 index 563b63407d4..00000000000 --- a/src/status_im/data_store/chats.cljs +++ /dev/null @@ -1,75 +0,0 @@ -(ns status-im.data-store.chats - (:require [goog.object :as object] - [cljs.core.async :as async] - [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(defn- normalize-chat [{:keys [chat-id] :as chat}] - (let [last-clock-value (-> (core/get-by-field @core/account-realm - :message :chat-id chat-id) - (core/sorted :clock-value :desc) - (core/single-clj :message) - :clock-value)] - (assoc chat :last-clock-value (or last-clock-value 0)))) - -(re-frame/reg-cofx - :data-store/all-chats - (fn [cofx _] - (assoc cofx :all-stored-chats (map normalize-chat - (-> @core/account-realm - (core/get-all :chat) - (core/sorted :timestamp :desc) - (core/all-clj :chat)))))) - -(defn save-chat-tx - "Returns tx function for saving chat" - [{:keys [chat-id] :as chat}] - (fn [realm] - (core/create realm :chat chat true))) - -;; Only used in debug mode -(defn delete-chat-tx - "Returns tx function for hard deleting the chat" - [chat-id] - (fn [realm] - (core/delete realm (core/get-by-field realm :chat :chat chat-id)))) - -(defn- get-chat-by-id [chat-id realm] - (core/single (core/get-by-field realm :chat :chat-id chat-id))) - -(defn clear-history-tx - "Returns tx function for clearing the history of chat" - [chat-id deleted-at-clock-value] - (fn [realm] - (let [chat (get-chat-by-id chat-id realm)] - (doto chat - (aset "deleted-at-clock-value" deleted-at-clock-value))))) - -(defn deactivate-chat-tx - "Returns tx function for deactivating chat" - [chat-id now] - (fn [realm] - (let [chat (get-chat-by-id chat-id realm)] - (doto chat - (aset "is-active" false) - (aset "removed-at" now))))) - -(defn add-chat-contacts-tx - "Returns tx function for adding chat contacts" - [chat-id contacts] - (fn [realm] - (let [chat (get-chat-by-id chat-id realm) - existing-contacts (object/get chat "contacts")] - (aset chat "contacts" - (clj->js (into #{} (concat contacts - (core/list->clj existing-contacts)))))))) - -(defn remove-chat-contacts-tx - "Returns tx function for removing chat contacts" - [chat-id contacts] - (fn [realm] - (let [chat (get-chat-by-id chat-id realm) - existing-contacts (object/get chat "contacts")] - (aset chat "contacts" - (clj->js (remove (into #{} contacts) - (core/list->clj existing-contacts))))))) diff --git a/src/status_im/data_store/contacts.cljs b/src/status_im/data_store/contacts.cljs deleted file mode 100644 index fcdbe36ec66..00000000000 --- a/src/status_im/data_store/contacts.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.data-store.contacts - (:require [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(re-frame/reg-cofx - :data-store/get-all-contacts - (fn [coeffects _] - (assoc coeffects :all-contacts (-> @core/account-realm - (core/get-all :contact) - (core/all-clj :contact))))) - -(defn save-contact-tx - "Returns tx function for saving contact" - [{:keys [whisper-identity] :as contact}] - (fn [realm] - (core/create realm - :contact - (dissoc contact :command :response :subscriptions :jail-loaded-events) - true))) - -(defn save-contacts-tx - "Returns tx function for saving contacts" - [contacts] - (fn [realm] - (doseq [contact contacts] - ((save-contact-tx contact) realm)))) - -(defn delete-contact-tx - "Returns tx function for deleting contact" - [whisper-identity] - (fn [realm] - (core/delete realm (core/single (core/get-by-field realm :contact :whisper-identity whisper-identity))))) diff --git a/src/status_im/data_store/core.cljs b/src/status_im/data_store/core.cljs deleted file mode 100644 index e4cc8c2115e..00000000000 --- a/src/status_im/data_store/core.cljs +++ /dev/null @@ -1,55 +0,0 @@ -(ns status-im.data-store.core - (:require [cljs.core.async :as async] - [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.data-store.realm.core :as data-source] - status-im.data-store.chats - status-im.data-store.messages - status-im.data-store.contacts - status-im.data-store.transport - status-im.data-store.browser - status-im.data-store.accounts - status-im.data-store.local-storage - status-im.data-store.mailservers - status-im.data-store.requests)) - -(defn init [encryption-key] - (if @data-source/base-realm - (js/Promise.resolve) - (.. - (data-source/ensure-directories) - ;; This can be removed when we are confident all the users - ;; have migrated the data, introduced in 0.9.23 - (then #(data-source/move-realms)) - (catch (fn [error] - (log/error "Could not move realms" error))) - (then #(data-source/open-base-realm encryption-key))))) - -(defn change-account [address encryption-key] - (log/debug "changing account to: " address) - (data-source/change-account address encryption-key)) - -(defn- perform-transactions [raw-transactions realm] - (let [success-events (keep :success-event raw-transactions) - transactions (map (fn [{:keys [transaction] :as f}] - (or transaction f)) raw-transactions)] - (data-source/write realm #(doseq [transaction transactions] - (transaction realm))) - (doseq [event success-events] - (re-frame/dispatch event)))) - -(re-frame/reg-fx - :data-store/base-tx - (fn [transactions] - (async/go (async/>! data-source/realm-queue - (partial perform-transactions - transactions - @data-source/base-realm))))) - -(re-frame/reg-fx - :data-store/tx - (fn [transactions] - (async/go (async/>! data-source/realm-queue - (partial perform-transactions - transactions - @data-source/account-realm))))) diff --git a/src/status_im/data_store/dapp_permissions.cljs b/src/status_im/data_store/dapp_permissions.cljs deleted file mode 100644 index 2bb5270d95d..00000000000 --- a/src/status_im/data_store/dapp_permissions.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns status-im.data-store.dapp-permissions - (:require [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(re-frame/reg-cofx - :data-store/all-dapp-permissions - (fn [cofx _] - (assoc cofx :all-dapp-permissions (-> @core/account-realm - (core/get-all :dapp-permissions) - (core/all-clj :dapp-permissions))))) - -(defn save-dapp-permissions - "Returns tx function for saving dapp permissions" - [permissions] - (fn [realm] - (core/create realm :dapp-permissions permissions true))) \ No newline at end of file diff --git a/src/status_im/data_store/local_storage.cljs b/src/status_im/data_store/local_storage.cljs deleted file mode 100644 index c63f98d9309..00000000000 --- a/src/status_im/data_store/local_storage.cljs +++ /dev/null @@ -1,17 +0,0 @@ -(ns status-im.data-store.local-storage - (:require [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(re-frame/reg-cofx - :data-store/get-local-storage-data - (fn [cofx _] - (assoc cofx :get-local-storage-data #(-> @core/account-realm - (core/get-by-field :local-storage :chat-id %) - (core/single-clj :local-storage) - :data)))) - -(defn set-local-storage-data-tx - "Returns tx function setting local storage data" - [data] - (fn [realm] - (core/create realm :local-storage data true))) diff --git a/src/status_im/data_store/mailservers.cljs b/src/status_im/data_store/mailservers.cljs deleted file mode 100644 index 05b432d4f90..00000000000 --- a/src/status_im/data_store/mailservers.cljs +++ /dev/null @@ -1,26 +0,0 @@ -(ns status-im.data-store.mailservers - (:require [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(re-frame/reg-cofx - :data-store/get-all-mailservers - (fn [cofx _] - (assoc cofx :data-store/mailservers (-> @core/account-realm - (core/get-all :mailserver) - (core/all-clj :mailserver))))) - -(defn save-tx - "Returns tx function for saving a mailserver" - [{:keys [id] :as mailserver}] - (fn [realm] - (core/create realm - :mailserver - mailserver - true))) - -(defn delete-tx - "Returns tx function for deleting a mailserver" - [id] - (fn [realm] - (core/delete realm - (core/get-by-field realm :mailserver :id id)))) diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs deleted file mode 100644 index 0fed121f322..00000000000 --- a/src/status_im/data_store/messages.cljs +++ /dev/null @@ -1,117 +0,0 @@ -(ns status-im.data-store.messages - (:require [cljs.reader :as reader] - [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.data-store.realm.core :as core] - [status-im.utils.core :as utils])) - -(defn- command-type? - [type] - (contains? - #{constants/content-type-command constants/content-type-command-request} - type)) - -(defn- transform-message [{:keys [content-type] :as message}] - (cond-> (update message :message-type keyword) - (command-type? content-type) - (update :content reader/read-string))) - -(defn- get-by-chat-id - ([chat-id] - (get-by-chat-id chat-id 0)) - ([chat-id from] - (let [messages (-> (core/get-by-field @core/account-realm :message :chat-id chat-id) - (core/sorted :timestamp :desc) - (core/page from (+ from constants/default-number-of-messages)) - (core/all-clj :message))] - (map transform-message messages)))) - -;; TODO janherich: define as cofx once debug handlers are refactored -(defn get-log-messages - [chat-id] - (->> (get-by-chat-id chat-id 0) - (filter #(= (:content-type %) constants/content-type-log-message)) - (map #(select-keys % [:content :timestamp])))) - -(def default-values - {:to nil}) - -(re-frame/reg-cofx - :data-store/get-messages - (fn [cofx _] - (assoc cofx :get-stored-messages get-by-chat-id))) - -(re-frame/reg-cofx - :data-store/message-ids - (fn [cofx _] - (assoc cofx :stored-message-ids (let [chat-id->message-id (volatile! {})] - (-> @core/account-realm - (.objects "message") - (.map (fn [msg _ _] - (vswap! chat-id->message-id - #(update % - (aget msg "chat-id") - (fnil conj #{}) - (aget msg "message-id")))))) - @chat-id->message-id)))) - -(re-frame/reg-cofx - :data-store/unviewed-messages - (fn [{:keys [db] :as cofx} _] - (assoc cofx - :stored-unviewed-messages - (into {} - (map (fn [[chat-id user-statuses]] - [chat-id (into #{} (map :message-id) user-statuses)])) - (group-by :chat-id - (-> @core/account-realm - (core/get-by-fields - :user-status - :and {:whisper-identity (:current-public-key db) - :status "received"}) - (core/all-clj :user-status))))))) - -(defn- prepare-content [content] - (if (string? content) - content - (pr-str - ;; TODO janherich: this is ugly and not systematic, define something like `:not-persisent` - ;; option for command params instead - (update content :params dissoc :password :password-confirmation)))) - -(defn- prepare-message [message] - (utils/update-if-present message :content prepare-content)) - -(defn save-message-tx - "Returns tx function for saving message" - [{:keys [message-id from] :as message}] - (fn [realm] - (core/create realm - :message - (prepare-message (merge default-values - message - {:from (or from "anonymous")})) - true))) - -(defn delete-message-tx - "Returns tx function for deleting message" - [message-id] - (fn [realm] - (when-let [message (core/get-by-field realm :message :message-id message-id)] - (core/delete realm message) - (core/delete realm (core/get-by-field realm :user-status :message-id message-id))))) - -(defn delete-messages-tx - "Returns tx function for deleting messages with user statuses for given chat-id" - [chat-id] - (fn [realm] - (core/delete realm (core/get-by-field realm :message :chat-id chat-id)) - (core/delete realm (core/get-by-field realm :user-status :chat-id chat-id)))) - -(defn hide-messages-tx - "Returns tx function for hiding messages for given chat-id" - [chat-id] - (fn [realm] - (.map (core/get-by-field realm :message :chat-id chat-id) - (fn [msg _ _] - (aset msg "show?" false))))) diff --git a/src/status_im/data_store/realm/core.cljs b/src/status_im/data_store/realm/core.cljs deleted file mode 100644 index 9b67b8d017f..00000000000 --- a/src/status_im/data_store/realm/core.cljs +++ /dev/null @@ -1,266 +0,0 @@ -(ns status-im.data-store.realm.core - (:require [goog.object :as object] - [goog.string :as gstr] - [clojure.string :as string] - [status-im.data-store.realm.schemas.account.core :as account] - [status-im.data-store.realm.schemas.base.core :as base] - [taoensso.timbre :as log] - [status-im.utils.fs :as fs] - [status-im.utils.async :as utils.async] - [status-im.utils.platform :as utils.platform] - [status-im.utils.ethereum.core :as utils.ethereum] - [cognitect.transit :as transit] - [status-im.react-native.js-dependencies :as rn-dependencies] - [status-im.utils.utils :as utils])) - -(defn to-buffer [key] - (when key - (let [length (.-length key) - arr (js/Uint8Array. length)] - (dotimes [i length] - (aset arr i (aget key i))) - (.-buffer arr)))) - -(defn encrypted-realm-version - "Returns -1 if the file does not exists, the schema version if it successfully - decrypts it, error otherwise." - [file-name encryption-key] - (.schemaVersion rn-dependencies/realm file-name (to-buffer encryption-key))) - -(defn open-realm - [options file-name encryption-key] - (log/debug "Opening realm at " file-name "...") - (let [options-js (clj->js (assoc options :path file-name))] - (log/debug "Using encryption key...") - (set! (.-encryptionKey options-js) (to-buffer encryption-key)) - (when (exists? js/window) - (rn-dependencies/realm. options-js)))) - -(defn- is-account-file? [n] - (re-matches #".*/[0-9a-f]{40}$" n)) - -(defn- is-realm-file? [n] - (or (re-matches #".*/default\.realm(\.management|\.lock|\.note)?$" n) - (re-matches #".*/new-account(\.management|\.lock|\.note)?$" n) - (re-matches #".*/[0-9a-f]{40}(\.management|\.lock|\.note)?$" n))) - -(defn- realm-management-file? [n] - (re-matches #".*(\.management|\.lock|\.note)$" n)) - -(def old-base-realm-path - (.-defaultPath rn-dependencies/realm)) - -(def realm-dir - (cond - utils.platform/android? (str - (.-DocumentDirectoryPath rn-dependencies/fs) - "/../no_backup/realm/") - utils.platform/ios? (str - (.-LibraryDirectoryPath rn-dependencies/fs) - "/realm/") - :else (.-defaultPath rn-dependencies/realm))) - -(def old-realm-dir - (string/replace old-base-realm-path #"default\.realm$" "")) - -(def accounts-realm-dir - (str realm-dir "accounts/")) - -(def base-realm-path - (str realm-dir - "default.realm")) - -(defn- delete-realms [] - (log/warn "realm: deleting all realms") - (fs/unlink realm-dir)) - -(defn- ensure-directories [] - (.. - (fs/mkdir realm-dir) - (then #(fs/mkdir accounts-realm-dir)))) - -(defn- move-realm-to-library [path] - (let [filename (last (string/split path "/")) - new-path (if (is-account-file? path) - (str accounts-realm-dir (utils.ethereum/sha3 filename)) - (str realm-dir filename))] - (log/debug "realm: moving " path " to " new-path) - (if (realm-management-file? path) - (fs/unlink path) - (fs/move-file path new-path)))) - -(defn- move-realms [] - (log/info "realm: moving all realms") - (.. - (fs/read-dir old-realm-dir) - (then #(->> (js->clj % :keywordize-keys true) - (map :path) - (filter is-realm-file?))) - (then #(js/Promise.all (clj->js (map move-realm-to-library %)))))) - -(defn- close [realm] - (when realm - (.close realm))) - -(defn- migrate-schemas - "Apply migrations in sequence and open database with the last schema" - [file-name schemas encryption-key current-version] - (doseq [schema schemas - :when (> (:schemaVersion schema) current-version) - :let [migrated-realm (open-realm schema file-name encryption-key)]] - (close migrated-realm)) - (open-realm (last schemas) file-name encryption-key)) - -(defn migrate-realm - "Migrate realm if is a compatible version or reset the database" - [file-name schemas encryption-key] - (migrate-schemas file-name schemas encryption-key (encrypted-realm-version - file-name - encryption-key))) - -(defn open-migrated-realm - [file-name schemas encryption-key] - (migrate-realm file-name schemas encryption-key)) - -(defn- index-entity-schemas [all-schemas] - (into {} (map (juxt :name identity)) (-> all-schemas last :schema))) - -(def base-realm (atom nil)) -(def account-realm (atom nil)) - -(def entity->schemas (merge (index-entity-schemas base/schemas) - (index-entity-schemas account/schemas))) - -(def realm-queue (utils.async/task-queue 2000)) - -(defn close-account-realm [] - (close @account-realm) - (reset! account-realm nil)) - -(defn open-base-realm [encryption-key] - (log/debug "Opening base realm... (first run)") - (when @base-realm - (close @base-realm)) - (reset! base-realm (open-migrated-realm base-realm-path base/schemas encryption-key)) - (log/debug "Created @base-realm")) - -(defn change-account [address encryption-key] - (let [path (str accounts-realm-dir (utils.ethereum/sha3 address))] - (close-account-realm) - (reset! account-realm (open-migrated-realm path account/schemas encryption-key)))) - -(declare realm-obj->clj) - -;; realm functions - -(defn write [realm f] - (.write realm f)) - -(defn create - ([realm schema-name obj] - (create realm schema-name obj false)) - ([realm schema-name obj update?] - (let [obj-to-save (select-keys obj (keys (get-in entity->schemas - [schema-name :properties])))] - (.create realm (name schema-name) (clj->js obj-to-save) update?)))) - -(defn delete [realm obj] - (.delete realm obj)) - -(defn get-all [realm schema-name] - (.objects realm (name schema-name))) - -(defn sorted [results field-name order] - (.sorted results (name field-name) (if (= order :asc) - false - true))) - -(defn page [results from to] - (js/Array.prototype.slice.call results from to)) - -(defn filtered [results filter-query] - (.filtered results filter-query)) - -(def reader (transit/reader :json)) -(def writer (transit/writer :json)) - -(defn serialize [o] (transit/write writer o)) -(defn deserialize [o] (try (transit/read reader o) (catch :default e nil))) - -(defn- realm-list->clj-coll [realm-list coll map-fn] - (when realm-list - (into coll (map map-fn) (range 0 (.-length realm-list))))) - -(defn- list->clj [realm-list] - (realm-list->clj-coll realm-list [] #(object/get realm-list %))) - -(defn- object-list->clj [realm-object-list entity-name] - (let [primary-key (-> entity->schemas (get entity-name) :primaryKey name)] - (realm-list->clj-coll realm-object-list - {} - #(let [realm-obj (object/get realm-object-list %)] - [(object/get realm-obj primary-key) (realm-obj->clj realm-obj entity-name)])))) - -(defn- realm-obj->clj [realm-obj entity-name] - (when realm-obj - (let [{:keys [primaryKey properties]} (get entity->schemas entity-name)] - (into {} - (map (fn [[prop-name {:keys [type objectType]}]] - (let [prop-value (object/get realm-obj (name prop-name))] - [prop-name (case type - "string[]" (list->clj prop-value) - :list (object-list->clj prop-value objectType) - prop-value)]))) - properties)))) - -(defn single - "Takes realm results, returns the first one" - [result] - (object/get result 0)) - -(defn single-clj - "Takes realm results and schema name, returns the first result converted to cljs datastructure" - [results schema-name] - (-> results single (realm-obj->clj schema-name))) - -(defn all-clj - "Takes realm results and schema name, returns results as converted cljs datastructures in vector" - [results schema-name] - (realm-list->clj-coll results [] #(realm-obj->clj (object/get results %) schema-name))) - -(defn- field-type [schema-name field] - (let [field-def (get-in entity->schemas [schema-name :properties field])] - (or (:type field-def) field-def))) - -(defmulti to-query (fn [_ operator _ _] operator)) - -(defmethod to-query :eq [schema-name _ field value] - (let [field-type (field-type schema-name field) - query (str (name field) "=" (if (= "string" (name field-type)) - (str "\"" value "\"") - value))] - query)) - -(defn get-by-field - "Selects objects from realm identified by schema-name based on value of field" - [realm schema-name field value] - (let [q (to-query schema-name :eq field value)] - (.filtered (.objects realm (name schema-name)) q))) - -(defn- and-query [queries] - (string/join " and " queries)) - -(defn- or-query [queries] - (string/join " or " queries)) - -(defn get-by-fields - "Selects objects from realm identified by schema name based on field values - combined by `:and`/`:or` operator" - [realm schema-name op fields] - (let [queries (map (fn [[k v]] - (to-query schema-name :eq k v)) - fields)] - (.filtered (.objects realm (name schema-name)) - (case op - :and (and-query queries) - :or (or-query queries))))) diff --git a/src/status_im/data_store/realm/schemas/account/core.cljs b/src/status_im/data_store/realm/schemas/account/core.cljs deleted file mode 100644 index 45fedfbfa4f..00000000000 --- a/src/status_im/data_store/realm/schemas/account/core.cljs +++ /dev/null @@ -1,42 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.core - (:require - [status-im.data-store.realm.schemas.account.v1.core :as v1] - [status-im.data-store.realm.schemas.account.v2.core :as v2] - [status-im.data-store.realm.schemas.account.v3.core :as v3] - [status-im.data-store.realm.schemas.account.v4.core :as v4] - [status-im.data-store.realm.schemas.account.v5.core :as v5] - [status-im.data-store.realm.schemas.account.v6.core :as v6] - [status-im.data-store.realm.schemas.account.v7.core :as v7] - [status-im.data-store.realm.schemas.account.v8.core :as v8] - [status-im.data-store.realm.schemas.account.v9.core :as v9])) - -;; TODO(oskarth): Add failing test if directory vXX exists but isn't in schemas. - -;; put schemas ordered by version -(def schemas [{:schema v1/schema - :schemaVersion 1 - :migration v1/migration} - {:schema v2/schema - :schemaVersion 2 - :migration v2/migration} - {:schema v3/schema - :schemaVersion 3 - :migration v3/migration} - {:schema v4/schema - :schemaVersion 4 - :migration v4/migration} - {:schema v5/schema - :schemaVersion 5 - :migration v5/migration} - {:schema v6/schema - :schemaVersion 6 - :migration v6/migration} - {:schema v7/schema - :schemaVersion 7 - :migration v7/migration} - {:schema v8/schema - :schemaVersion 8 - :migration v8/migration} - {:schema v9/schema - :schemaVersion 9 - :migration v9/migration}]) diff --git a/src/status_im/data_store/realm/schemas/account/v1/browser.cljs b/src/status_im/data_store/realm/schemas/account/v1/browser.cljs deleted file mode 100644 index aa82ebaba4e..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/browser.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.browser) - -(def schema {:name :browser - :primaryKey :browser-id - :properties {:browser-id :string - :name :string - :timestamp :int - :dapp? {:type :bool - :default false} - :url {:type :string - :optional true} - :contact {:type :string - :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/chat.cljs b/src/status_im/data_store/realm/schemas/account/v1/chat.cljs deleted file mode 100644 index 4f93cb915b9..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/chat.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.chat - (:require [status-im.ui.components.styles :refer [default-chat-color]])) - -(def schema {:name :chat - :primaryKey :chat-id - :properties {:chat-id :string - :name :string - :color {:type :string - :default default-chat-color} - :group-chat {:type :bool - :indexed true} - :group-admin {:type :string - :optional true} - :is-active :bool - :timestamp :int - :contacts {:type "string[]"} - :removed-at {:type :int - :optional true} - :removed-from-at {:type :int - :optional true} - :added-to-at {:type :int - :optional true} - :updated-at {:type :int - :optional true} - :message-overhead {:type :int - :default 0} - :contact-info {:type :string - :optional true} - :debug? {:type :bool - :default false} - :public? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/chat_contact.cljs b/src/status_im/data_store/realm/schemas/account/v1/chat_contact.cljs deleted file mode 100644 index ff56e0d07b6..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/chat_contact.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.chat-contact - (:require [taoensso.timbre :as log])) - -(def schema {:name :chat-contact - :properties {:identity "string" - :is-in-chat {:type "bool" - :default true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/contact.cljs b/src/status_im/data_store/realm/schemas/account/v1/contact.cljs deleted file mode 100644 index c42f8e23030..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/contact.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.contact - (:require [taoensso.timbre :as log])) - -(def schema {:name :contact - :primaryKey :whisper-identity - :properties {:address {:type :string :optional true} - :whisper-identity :string - :name {:type :string :optional true} - :photo-path {:type :string :optional true} - :last-updated {:type :int :default 0} - :last-online {:type :int :default 0} - :pending? {:type :bool :default false} - :hide-contact? {:type :bool :default false} - :status {:type :string :optional true} - :fcm-token {:type :string :optional true} - :description {:type :string :optional true} - :public-key {:type :string - :optional true} - :dapp? {:type :bool - :default false} - :dapp-url {:type :string - :optional true} - :bot-url {:type :string - :optional true} - :dapp-hash {:type :int - :optional true} - :debug? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/core.cljs b/src/status_im/data_store/realm/schemas/account/v1/core.cljs deleted file mode 100644 index 24a85c51fd2..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/core.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.core - (:require [status-im.data-store.realm.schemas.account.v1.chat :as chat] - [status-im.data-store.realm.schemas.account.v1.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v1.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v1.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v1 account database: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v1/group_contact.cljs b/src/status_im/data_store/realm/schemas/account/v1/group_contact.cljs deleted file mode 100644 index 68df433a138..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/group_contact.cljs +++ /dev/null @@ -1,5 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.group-contact - (:require [taoensso.timbre :as log])) - -(def schema {:name :group-contact - :properties {:identity "string"}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/local_storage.cljs b/src/status_im/data_store/realm/schemas/account/v1/local_storage.cljs deleted file mode 100644 index 9c6c101e71a..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/local_storage.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.local-storage) - -(def schema {:name :local-storage - :primaryKey :chat-id - :properties {:chat-id :string - :data {:type :string - :default "{}"}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/message.cljs b/src/status_im/data_store/realm/schemas/account/v1/message.cljs deleted file mode 100644 index ca3ebadaa6b..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/message.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.message) - -(def schema {:name :message - :primaryKey :message-id - :properties {:message-id :string - :from :string - :to {:type :string - :optional true} - :content :string ; TODO make it ArrayBuffer - :content-type :string - :username {:type :string - :optional true} - :timestamp :int - :chat-id {:type :string - :indexed true} - :outgoing :bool - :retry-count {:type :int - :default 0} - :message-type {:type :string - :optional true} - :message-status {:type :string - :optional true} - :user-statuses {:type :list - :objectType :user-status} - :clock-value {:type :int - :default 0} - :show? {:type :bool - :default true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/request.cljs b/src/status_im/data_store/realm/schemas/account/v1/request.cljs deleted file mode 100644 index 8981ec30427..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/request.cljs +++ /dev/null @@ -1,8 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.request) - -(def schema {:name :request - :properties {:message-id :string - :chat-id :string - :response :string - :status {:type :string - :default "open"}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/transport.cljs b/src/status_im/data_store/realm/schemas/account/v1/transport.cljs deleted file mode 100644 index 8399164bd55..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/transport.cljs +++ /dev/null @@ -1,15 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.transport) - -(def schema {:name :transport - :primaryKey :chat-id - :properties {:chat-id :string - :ack :string - :seen :string - :pending-ack :string - :pending-send :string - :topic :string - :sym-key-id {:type :string - :optional true} - ;;TODO (yenda) remove once go implements persistence - :sym-key {:type :string - :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v1/user_status.cljs b/src/status_im/data_store/realm/schemas/account/v1/user_status.cljs deleted file mode 100644 index b94a1ff0c9b..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v1/user_status.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v1.user-status) - -(def schema {:name :user-status - :primaryKey :status-id - :properties {;; Unfortunately, realm doesn't support composite primary keys, - ;; so we have to keep separate `:status-id` property, which is just - ;; `:message-id`-`:whisper-identity` concatenated - :status-id :string - :message-id :string - :chat-id :string - :whisper-identity :string - :status :string}}) diff --git a/src/status_im/data_store/realm/schemas/account/v2/core.cljs b/src/status_im/data_store/realm/schemas/account/v2/core.cljs deleted file mode 100644 index dea2f508b46..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v2/core.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v2.core - (:require [status-im.data-store.realm.schemas.account.v1.chat :as chat] - [status-im.data-store.realm.schemas.account.v1.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v1.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v1.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v2 account database: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v2/mailserver.cljs b/src/status_im/data_store/realm/schemas/account/v2/mailserver.cljs deleted file mode 100644 index d4cab1452c6..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v2/mailserver.cljs +++ /dev/null @@ -1,11 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v2.mailserver - (:require [taoensso.timbre :as log])) - -(def schema {:name :mailserver - :primaryKey :id - :properties {:id :string - :name {:type :string} - :address {:type :string} - :password {:type :string - :optional true} - :chain {:type :string}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v3/chat.cljs b/src/status_im/data_store/realm/schemas/account/v3/chat.cljs deleted file mode 100644 index 5c34a79bdf8..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v3/chat.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v3.chat - (:require [status-im.ui.components.styles :refer [default-chat-color]])) - -(def schema {:name :chat - :primaryKey :chat-id - :properties {:chat-id :string - :name :string - :color {:type :string - :default default-chat-color} - :group-chat {:type :bool - :indexed true} - :group-admin {:type :string - :optional true} - :is-active :bool - :timestamp :int - :contacts {:type "string[]"} - :removed-at {:type :int - :optional true} - :removed-from-at {:type :int - :optional true} - :deleted-at-clock-value {:type :int - :optional true} - :added-to-at {:type :int - :optional true} - :updated-at {:type :int - :optional true} - :message-overhead {:type :int - :default 0} - :contact-info {:type :string - :optional true} - :debug? {:type :bool - :default false} - :public? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v3/core.cljs b/src/status_im/data_store/realm/schemas/account/v3/core.cljs deleted file mode 100644 index c9cb0723405..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v3/core.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v3.core - (:require [status-im.data-store.realm.schemas.account.v3.chat :as chat] - [status-im.data-store.realm.schemas.account.v1.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v1.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v1.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v3 account database: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v4/core.cljs b/src/status_im/data_store/realm/schemas/account/v4/core.cljs deleted file mode 100644 index ac7b9350f4b..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v4/core.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v4.core - (:require [status-im.data-store.realm.schemas.account.v3.chat :as chat] - [status-im.data-store.realm.schemas.account.v4.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v1.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v1.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v4 account database: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v4/transport.cljs b/src/status_im/data_store/realm/schemas/account/v4/transport.cljs deleted file mode 100644 index d74ffab0207..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v4/transport.cljs +++ /dev/null @@ -1,17 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v4.transport) - -(def schema {:name :transport - :primaryKey :chat-id - :properties {:chat-id :string - :ack :string - :seen :string - :pending-ack :string - :pending-send :string - :topic :string - :fetch-history? {:type :bool - :default false} - :sym-key-id {:type :string - :optional true} - ;;TODO (yenda) remove once go implements persistence - :sym-key {:type :string - :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v5/chat.cljs b/src/status_im/data_store/realm/schemas/account/v5/chat.cljs deleted file mode 100644 index 29cb43751d1..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v5/chat.cljs +++ /dev/null @@ -1,40 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v5.chat - (:require - [taoensso.timbre :as log] - [status-im.ui.components.styles :refer [default-chat-color]])) - -(def schema {:name :chat - :primaryKey :chat-id - :properties {:chat-id :string - :name :string - :color {:type :string - :default default-chat-color} - :group-chat {:type :bool - :indexed true} - :group-admin {:type :string - :optional true} - :is-active :bool - :timestamp :int - :contacts {:type "string[]"} - :removed-at {:type :int - :optional true} - :removed-from-at {:type :int - :optional true} - :deleted-at-clock-value {:type :int - :optional true} - :added-to-at {:type :int - :optional true} - :updated-at {:type :int - :optional true} - :message-overhead {:type :int - :default 0} - :debug? {:type :bool - :default false} - :public? {:type :bool - :default false}}}) - -(defn migration [old-realm new-realm] - (log/debug "migrating chats schema v5") - (let [chats (.objects new-realm "chat")] - (dotimes [i (.-length chats)] - (js-delete (aget chats i) "contact-info")))) diff --git a/src/status_im/data_store/realm/schemas/account/v5/core.cljs b/src/status_im/data_store/realm/schemas/account/v5/core.cljs deleted file mode 100644 index 8384e78d70a..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v5/core.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v5.core - (:require [status-im.data-store.realm.schemas.account.v5.chat :as chat] - [status-im.data-store.realm.schemas.account.v4.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v1.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v1.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v5 account database: " old-realm new-realm) - (chat/migration old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v6/core.cljs b/src/status_im/data_store/realm/schemas/account/v6/core.cljs deleted file mode 100644 index e612a33f122..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v6/core.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v6.core - (:require [status-im.data-store.realm.schemas.account.v5.chat :as chat] - [status-im.data-store.realm.schemas.account.v6.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v1.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v1.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v6 account database: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v6/transport.cljs b/src/status_im/data_store/realm/schemas/account/v6/transport.cljs deleted file mode 100644 index 1ff3309be37..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v6/transport.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v6.transport) - -(def schema {:name :transport - :primaryKey :chat-id - :properties {:chat-id :string - :ack :string - :seen :string - :pending-ack :string - :pending-send :string - :topic :string - :fetch-history? {:type :bool - :default false} - :resend? {:type :string - :optional true} - :sym-key-id {:type :string - :optional true} - ;;TODO (yenda) remove once go implements persistence - :sym-key {:type :string - :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v7/core.cljs b/src/status_im/data_store/realm/schemas/account/v7/core.cljs deleted file mode 100644 index 4ed30d58528..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v7/core.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v7.core - (:require [status-im.data-store.realm.schemas.account.v5.chat :as chat] - [status-im.data-store.realm.schemas.account.v6.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v7.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v1.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v7 account database: " old-realm new-realm) - (message/migration old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v7/message.cljs b/src/status_im/data_store/realm/schemas/account/v7/message.cljs deleted file mode 100644 index 0a44c9ddcdb..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v7/message.cljs +++ /dev/null @@ -1,33 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v7.message - (:require [taoensso.timbre :as log])) - -(def schema {:name :message - :primaryKey :message-id - :properties {:message-id :string - :from :string - :to {:type :string - :optional true} - :content :string ; TODO make it ArrayBuffer - :content-type :string - :username {:type :string - :optional true} - :timestamp :int - :chat-id {:type :string - :indexed true} - :outgoing :bool - :retry-count {:type :int - :default 0} - :message-type {:type :string - :optional true} - :message-status {:type :string - :optional true} - :clock-value {:type :int - :default 0} - :show? {:type :bool - :default true}}}) - -(defn migration [old-realm new-realm] - (log/debug "migrating messages schema v7") - (let [messages (.objects new-realm "message")] - (dotimes [i (.-length messages)] - (js-delete (aget messages i) "user-statuses")))) diff --git a/src/status_im/data_store/realm/schemas/account/v8/browser.cljs b/src/status_im/data_store/realm/schemas/account/v8/browser.cljs deleted file mode 100644 index 24e88108eb2..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v8/browser.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v8.browser - (:require [taoensso.timbre :as log])) - -(def schema {:name :browser - :primaryKey :browser-id - :properties {:browser-id :string - :name :string - :timestamp :int - :dapp? {:type :bool - :default false} - :history-index {:type :int - :optional true} - :history {:type "string[]" - :optional true}}}) - -(defn migration [old-realm new-realm] - (log/debug "migrating browser schema v8") - (let [browsers (.objects new-realm "browser") - old-browsers (.objects old-realm "browser")] - (dotimes [i (.-length browsers)] - (let [browser (aget browsers i) - old-browser (aget old-browsers i) - url (aget old-browser "url")] - (aset browser "history-index" 0) - (aset browser "history" (clj->js [url])))))) \ No newline at end of file diff --git a/src/status_im/data_store/realm/schemas/account/v8/core.cljs b/src/status_im/data_store/realm/schemas/account/v8/core.cljs deleted file mode 100644 index 3342bf8d139..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v8/core.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v8.core - (:require [status-im.data-store.realm.schemas.account.v5.chat :as chat] - [status-im.data-store.realm.schemas.account.v6.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v7.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v8.browser :as browser] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v8 account database: " old-realm new-realm) - (browser/migration old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v9/core.cljs b/src/status_im/data_store/realm/schemas/account/v9/core.cljs deleted file mode 100644 index 46f08a4331a..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v9/core.cljs +++ /dev/null @@ -1,26 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v9.core - (:require [status-im.data-store.realm.schemas.account.v5.chat :as chat] - [status-im.data-store.realm.schemas.account.v6.transport :as transport] - [status-im.data-store.realm.schemas.account.v1.contact :as contact] - [status-im.data-store.realm.schemas.account.v7.message :as message] - [status-im.data-store.realm.schemas.account.v1.request :as request] - [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] - [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] - [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] - [status-im.data-store.realm.schemas.account.v8.browser :as browser] - [status-im.data-store.realm.schemas.account.v9.dapp-permissions :as dapp-permissions] - [taoensso.timbre :as log])) - -(def schema [chat/schema - transport/schema - contact/schema - message/schema - request/schema - mailserver/schema - user-status/schema - local-storage/schema - browser/schema - dapp-permissions/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating v9 account database: " old-realm new-realm)) \ No newline at end of file diff --git a/src/status_im/data_store/realm/schemas/account/v9/dapp_permissions.cljs b/src/status_im/data_store/realm/schemas/account/v9/dapp_permissions.cljs deleted file mode 100644 index 9b3cf5c52f4..00000000000 --- a/src/status_im/data_store/realm/schemas/account/v9/dapp_permissions.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns status-im.data-store.realm.schemas.account.v9.dapp-permissions) - -(def schema {:name :dapp-permissions - :primaryKey :dapp - :properties {:dapp :string - :permissions {:type "string[]" - :optional true}}}) \ No newline at end of file diff --git a/src/status_im/data_store/realm/schemas/base/core.cljs b/src/status_im/data_store/realm/schemas/base/core.cljs deleted file mode 100644 index f140d9021a3..00000000000 --- a/src/status_im/data_store/realm/schemas/base/core.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.core - (:require [status-im.data-store.realm.schemas.base.v1.core :as v1] - [status-im.data-store.realm.schemas.base.v2.core :as v2] - [status-im.data-store.realm.schemas.base.v3.core :as v3] - [status-im.data-store.realm.schemas.base.v4.core :as v4] - [status-im.data-store.realm.schemas.base.v5.core :as v5] - [status-im.data-store.realm.schemas.base.v6.core :as v6] - [status-im.data-store.realm.schemas.base.v7.core :as v7])) - -;; put schemas ordered by version -(def schemas [{:schema v1/schema - :schemaVersion 1 - :migration v1/migration} - {:schema v2/schema - :schemaVersion 2 - :migration v2/migration} - {:schema v3/schema - :schemaVersion 3 - :migration v3/migration} - {:schema v4/schema - :schemaVersion 4 - :migration v4/migration} - {:schema v5/schema - :schemaVersion 5 - :migration v5/migration} - {:schema v6/schema - :schemaVersion 6 - :migration v6/migration} - {:schema v7/schema - :schemaVersion 7 - :migration v7/migration}]) diff --git a/src/status_im/data_store/realm/schemas/base/v1/account.cljs b/src/status_im/data_store/realm/schemas/base/v1/account.cljs deleted file mode 100644 index 29387dcf69d..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v1/account.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v1.account) - -(def schema {:name :account - :primaryKey :address - :properties {:address :string - :public-key :string - :name {:type :string :optional true} - :email {:type :string :optional true} - :status {:type :string :optional true} - :debug? {:type :bool :default false} - :photo-path :string - :signing-phrase {:type :string} - :mnemonic {:type :string} - :last-updated {:type :int :default 0} - :last-sign-in {:type :int :default 0} - :signed-up? {:type :bool - :default false} - :network :string - :networks {:type :list - :objectType :network} - :settings {:type :string} - :sharing-usage-data? {:type :bool :default false} - :dev-mode? {:type :bool :default false} - :seed-backed-up? {:type :bool :default false} - :wallet-set-up-passed? {:type :bool :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v1/core.cljs b/src/status_im/data_store/realm/schemas/base/v1/core.cljs deleted file mode 100644 index 4c162a81cda..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v1/core.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v1.core - (:require [status-im.data-store.realm.schemas.base.v1.network :as network] - [status-im.data-store.realm.schemas.base.v1.account :as account] - [taoensso.timbre :as log])) - -(def schema [network/schema - account/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating base database v1: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/base/v1/network.cljs b/src/status_im/data_store/realm/schemas/base/v1/network.cljs deleted file mode 100644 index 6c297f3b891..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v1/network.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v1.network - (:require [taoensso.timbre :as log])) - -(def schema {:name :network - :primaryKey :id - :properties {:id :string - :name {:type :string - :optional true} - :config {:type :string - :optional true} - :rpc-url {:type :string - :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v2/account.cljs b/src/status_im/data_store/realm/schemas/base/v2/account.cljs deleted file mode 100644 index c4434656244..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v2/account.cljs +++ /dev/null @@ -1,26 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v2.account) - -(def schema {:name :account - :primaryKey :address - :properties {:address :string - :public-key :string - :name {:type :string :optional true} - :email {:type :string :optional true} - :status {:type :string :optional true} - :debug? {:type :bool :default false} - :photo-path :string - :signing-phrase {:type :string} - :mnemonic {:type :string :optional true} - :last-updated {:type :int :default 0} - :last-sign-in {:type :int :default 0} - :signed-up? {:type :bool - :default false} - :network :string - :networks {:type :list - :objectType :network} - :settings {:type :string} - :sharing-usage-data? {:type :bool :default false} - :dev-mode? {:type :bool :default false} - :seed-backed-up? {:type :bool :default false} - :wallet-set-up-passed? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v2/core.cljs b/src/status_im/data_store/realm/schemas/base/v2/core.cljs deleted file mode 100644 index 742a73abc80..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v2/core.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v2.core - (:require [status-im.data-store.realm.schemas.base.v1.network :as network] - [status-im.data-store.realm.schemas.base.v2.account :as account] - [taoensso.timbre :as log])) - -(def schema [network/schema - account/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating base database v2: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/base/v3/account.cljs b/src/status_im/data_store/realm/schemas/base/v3/account.cljs deleted file mode 100644 index 532314ada02..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v3/account.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v3.account) - -(def schema {:name :account - :primaryKey :address - :properties {:address :string - :public-key :string - :name {:type :string :optional true} - :email {:type :string :optional true} - :status {:type :string :optional true} - :debug? {:type :bool :default false} - :photo-path :string - :signing-phrase {:type :string} - :mnemonic {:type :string :optional true} - :last-updated {:type :int :default 0} - :last-sign-in {:type :int :default 0} - :signed-up? {:type :bool - :default false} - :network :string - :networks {:type :list - :objectType :network} - :last-request {:type :int :optional true} - :settings {:type :string} - :sharing-usage-data? {:type :bool :default false} - :dev-mode? {:type :bool :default false} - :seed-backed-up? {:type :bool :default false} - :wallet-set-up-passed? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v3/core.cljs b/src/status_im/data_store/realm/schemas/base/v3/core.cljs deleted file mode 100644 index 3c7ad481d01..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v3/core.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v3.core - (:require [status-im.data-store.realm.schemas.base.v1.network :as network] - [status-im.data-store.realm.schemas.base.v3.account :as account] - [taoensso.timbre :as log])) - -(def schema [network/schema - account/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating base database v3: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/base/v4/account.cljs b/src/status_im/data_store/realm/schemas/base/v4/account.cljs deleted file mode 100644 index a62ff56132b..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v4/account.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v4.account) - -(def schema {:name :account - :primaryKey :address - :properties {:address :string - :public-key :string - :name {:type :string :optional true} - :email {:type :string :optional true} - :status {:type :string :optional true} - :debug? {:type :bool :default false} - :photo-path :string - :signing-phrase {:type :string} - :mnemonic {:type :string :optional true} - :last-updated {:type :int :default 0} - :last-sign-in {:type :int :default 0} - :signed-up? {:type :bool - :default false} - :network :string - :networks {:type :list - :objectType :network} - :bootnodes {:type :list - :objectType :bootnode} - :last-request {:type :int :optional true} - :settings {:type :string} - :sharing-usage-data? {:type :bool :default false} - :dev-mode? {:type :bool :default false} - :seed-backed-up? {:type :bool :default false} - :wallet-set-up-passed? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v4/bootnode.cljs b/src/status_im/data_store/realm/schemas/base/v4/bootnode.cljs deleted file mode 100644 index 7975d747355..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v4/bootnode.cljs +++ /dev/null @@ -1,8 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v4.bootnode) - -(def schema {:name :bootnode - :primaryKey :id - :properties {:id :string - :name {:type :string} - :chain {:type :string} - :address {:type :string}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v4/core.cljs b/src/status_im/data_store/realm/schemas/base/v4/core.cljs deleted file mode 100644 index 2202000d67c..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v4/core.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v4.core - (:require [status-im.data-store.realm.schemas.base.v1.network :as network] - [status-im.data-store.realm.schemas.base.v4.account :as account] - [status-im.data-store.realm.schemas.base.v4.bootnode :as bootnode] - [taoensso.timbre :as log])) - -(def schema [network/schema - bootnode/schema - account/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating base database v4: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/base/v5/account.cljs b/src/status_im/data_store/realm/schemas/base/v5/account.cljs deleted file mode 100644 index 76cb01b71d7..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v5/account.cljs +++ /dev/null @@ -1,33 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v5.account - (:require [taoensso.timbre :as log] - [cognitect.transit :as transit] - [clojure.set :as set] - [status-im.data-store.realm.schemas.base.v4.account :as v4])) - -(def schema v4/schema) - -(def removed-tokens - #{:ATMChain :Centra :ROL}) - -(def removed-fiat-currencies - #{:bmd :bzd :gmd :gyd :kyd :lak :lrd :ltl :mkd :mnt :nio :sos :srd :yer}) - -(def reader (transit/reader :json)) -(def writer (transit/writer :json)) - -(defn serialize [o] (transit/write writer o)) -(defn deserialize [o] (try (transit/read reader o) (catch :default e nil))) - -(defn migration [old-realm new-realm] - (log/debug "migrating accounts schema v4") - (let [accounts (.objects new-realm "account")] - (dotimes [i (.-length accounts)] - (let [account (aget accounts i) - old-settings (deserialize (aget account "settings")) - new-settings (-> old-settings - (update-in [:wallet :visible-tokens :mainnet] - #(set/difference % removed-tokens)) - (update-in [:wallet :currency] - #(if (removed-fiat-currencies %) :usd %))) - updated (serialize new-settings)] - (aset account "settings" updated))))) \ No newline at end of file diff --git a/src/status_im/data_store/realm/schemas/base/v5/core.cljs b/src/status_im/data_store/realm/schemas/base/v5/core.cljs deleted file mode 100644 index bac68ff5fb0..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v5/core.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v5.core - (:require [status-im.data-store.realm.schemas.base.v1.network :as network] - [status-im.data-store.realm.schemas.base.v4.bootnode :as bootnode] - [status-im.data-store.realm.schemas.base.v5.account :as account] - [taoensso.timbre :as log])) - -(def schema [network/schema - bootnode/schema - account/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating base database v5: " old-realm new-realm) - (account/migration old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/base/v6/account.cljs b/src/status_im/data_store/realm/schemas/base/v6/account.cljs deleted file mode 100644 index 4d942c75525..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v6/account.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v6.account) - -(def schema {:name :account - :primaryKey :address - :properties {:address :string - :public-key :string - :name {:type :string :optional true} - :email {:type :string :optional true} - :status {:type :string :optional true} - :debug? {:type :bool :default false} - :photo-path :string - :signing-phrase {:type :string} - :mnemonic {:type :string :optional true} - :last-updated {:type :int :default 0} - :last-sign-in {:type :int :default 0} - :signed-up? {:type :bool - :default false} - :network :string - :networks {:type :list - :objectType :network} - :bootnodes {:type :list - :objectType :bootnode} - :last-request {:type :int :optional true} - :settings {:type :string} - :sharing-usage-data? {:type :bool :default false} - :dev-mode? {:type :bool :default false} - :seed-backed-up? {:type :bool :default false} - :wallet-set-up-passed? {:type :bool - :default false} - :mainnet-warning-shown? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v6/core.cljs b/src/status_im/data_store/realm/schemas/base/v6/core.cljs deleted file mode 100644 index 4beb126af87..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v6/core.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v6.core - (:require [status-im.data-store.realm.schemas.base.v1.network :as network] - [status-im.data-store.realm.schemas.base.v4.bootnode :as bootnode] - [status-im.data-store.realm.schemas.base.v6.account :as account] - [taoensso.timbre :as log])) - -(def schema [network/schema - bootnode/schema - account/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating base database v6: " old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/base/v7/account.cljs b/src/status_im/data_store/realm/schemas/base/v7/account.cljs deleted file mode 100644 index fdcbaede477..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v7/account.cljs +++ /dev/null @@ -1,30 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v7.account) - -(def schema {:name :account - :primaryKey :address - :properties {:address :string - :public-key :string - :name {:type :string :optional true} - :email {:type :string :optional true} - :status {:type :string :optional true} - :debug? {:type :bool :default false} - :photo-path :string - :signing-phrase {:type :string} - :mnemonic {:type :string :optional true} - :last-updated {:type :int :default 0} - :last-sign-in {:type :int :default 0} - :signed-up? {:type :bool - :default false} - :network :string - :networks {:type :list - :objectType :network} - :bootnodes {:type :list - :objectType :bootnode} - :last-request {:type :int :optional true} - :settings {:type :string} - :dev-mode? {:type :bool :default false} - :seed-backed-up? {:type :bool :default false} - :wallet-set-up-passed? {:type :bool - :default false} - :mainnet-warning-shown? {:type :bool - :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v7/core.cljs b/src/status_im/data_store/realm/schemas/base/v7/core.cljs deleted file mode 100644 index f7b7a23a6b0..00000000000 --- a/src/status_im/data_store/realm/schemas/base/v7/core.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.data-store.realm.schemas.base.v7.core - (:require [status-im.data-store.realm.schemas.base.v1.network :as network] - [status-im.data-store.realm.schemas.base.v4.bootnode :as bootnode] - [status-im.data-store.realm.schemas.base.v7.account :as account] - [taoensso.timbre :as log])) - -(def schema [network/schema - bootnode/schema - account/schema]) - -(defn migration [old-realm new-realm] - (log/debug "migrating base database v7: " old-realm new-realm)) diff --git a/src/status_im/data_store/requests.cljs b/src/status_im/data_store/requests.cljs deleted file mode 100644 index 3c32ae0e27f..00000000000 --- a/src/status_im/data_store/requests.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.data-store.requests - (:require [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(re-frame/reg-cofx - :data-store/get-unanswered-requests - (fn [cofx _] - (assoc cofx :stored-unanswered-requests (-> @core/account-realm - (core/get-by-field :request :status "open") - (core/all-clj :request))))) - -(defn save-request-tx - "Returns tx function for saving request" - [request] - (fn [realm] - (core/create realm :request request true))) - -(defn mark-request-as-answered-tx - "Given chat-id and message-id, returns tx function for marking request as answered" - [chat-id message-id] - (fn [realm] - (some-> (core/get-by-fields realm :request :and [[:chat-id chat-id] - [:message-id message-id]]) - core/single - (aset "status" "answered")))) diff --git a/src/status_im/data_store/transport.cljs b/src/status_im/data_store/transport.cljs deleted file mode 100644 index eb7b62bf036..00000000000 --- a/src/status_im/data_store/transport.cljs +++ /dev/null @@ -1,46 +0,0 @@ -(ns status-im.data-store.transport - (:require [cljs.tools.reader.edn :as edn] - [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(defn deserialize-chat [serialized-chat] - (-> serialized-chat - (dissoc :chat-id) - (update :ack edn/read-string) - (update :seen edn/read-string) - (update :pending-ack edn/read-string) - (update :pending-send edn/read-string))) - -(re-frame/reg-cofx - :data-store/transport - (fn [cofx _] - (assoc cofx - :data-store/transport - (reduce (fn [acc {:keys [chat-id] :as chat}] - (assoc acc chat-id (deserialize-chat chat))) - {} - (-> @core/account-realm - (core/get-all :transport) - (core/all-clj :transport)))))) - -(defn save-transport-tx - "Returns tx function for saving transport" - [{:keys [chat-id chat]}] - (fn [realm] - (core/create realm - :transport - (-> chat - (assoc :chat-id chat-id) - (update :ack pr-str) - (update :seen pr-str) - (update :pending-ack pr-str) - (update :pending-send pr-str)) - true))) - -(defn delete-transport-tx - "Returns tx function for deleting transport" - [chat-id] - (fn [realm] - (let [transport (core/single - (core/get-by-field realm :transport :chat-id chat-id))] - (core/delete realm transport)))) diff --git a/src/status_im/data_store/user_statuses.cljs b/src/status_im/data_store/user_statuses.cljs deleted file mode 100644 index 6fec7a94432..00000000000 --- a/src/status_im/data_store/user_statuses.cljs +++ /dev/null @@ -1,50 +0,0 @@ -(ns status-im.data-store.user-statuses - (:require [clojure.string :as string] - [cljs.reader :as reader] - [re-frame.core :as re-frame] - [status-im.data-store.realm.core :as core])) - -(defn- in-query [message-ids] - (string/join " or " (map #(str "message-id=\"" % "\"") message-ids))) - -(defn- prepare-statuses [statuses] - (reduce (fn [acc {:keys [message-id whisper-identity] :as user-status}] - (assoc-in acc - [message-id whisper-identity] - (-> user-status - (update :status keyword) - (dissoc :status-id)))) - {} - statuses)) - -(defn- get-by-chat-and-messages-ids - [chat-id message-ids] - (-> @core/account-realm - (.objects "user-status") - (.filtered (str "chat-id=\"" chat-id "\"" - (when (seq message-ids) - (str " and (" (in-query message-ids) ")")))) - (core/all-clj :user-status) - prepare-statuses)) - -(re-frame/reg-cofx - :data-store/get-user-statuses - (fn [cofx _] - (assoc cofx :get-stored-user-statuses get-by-chat-and-messages-ids))) - -(defn- compute-status-id [{:keys [message-id whisper-identity]}] - (str message-id "-" whisper-identity)) - -(defn save-status-tx - "Returns tx function for saving message user status" - [user-status] - (fn [realm] - (let [status-id (compute-status-id user-status)] - (core/create realm :user-status (assoc user-status :status-id status-id) true)))) - -(defn save-statuses-tx - "Returns tx function for saving message user statuses" - [user-statuses] - (fn [realm] - (doseq [user-status user-statuses] - ((save-status-tx user-status) realm)))) diff --git a/src/status_im/db.cljs b/src/status_im/db.cljs new file mode 100644 index 00000000000..911347325a4 --- /dev/null +++ b/src/status_im/db.cljs @@ -0,0 +1,51 @@ +(ns status-im.db + (:require + [legacy.status-im.fleet.core :as fleet] + [react-native.core :as rn] + [status-im.config :as config] + [status-im.contexts.shell.activity-center.events :as activity-center] + [status-im.contexts.wallet.db :as wallet])) + +;; initial state of app-db +(def app-db + {:activity-center {:filter {:status (:filter-status activity-center/defaults) + :type (:filter-type activity-center/defaults)}} + :contacts/contacts {} + :pairing/installations {} + :group/selected-contacts #{} + :chats {} + :currencies {} + :current-chat-id nil + :group-chat/selected-participants #{} + :group-chat/deselected-members #{} + :sync-state :done + :link-previews-whitelist [] + :app-state "active" + :wallet wallet/defaults + :peer-stats/count 0 + :node-info {} + :peers-summary [] + :transport/message-envelopes {} + :mailserver/mailservers (fleet/default-mailservers {}) + :chat/cooldowns 0 + :chat/inputs {} + :chat/cooldown-enabled? false + :chat/last-outgoing-message-sent-at 0 + :chat/spam-messages-frequency 0 + :privacy-mode/privacy-mode-enabled? false + :chats-home-list #{} + :home-items-show-number 20 + :toasts {:ordered '() :toasts {}} + :tooltips {} + :dimensions/window (rn/get-window) + :registry {} + :visibility-status-updates {} + :stickers/packs-pending #{} + :settings/change-password {} + :keycard {} + :theme :dark + :log-level/pre-login-log-level (config/log-level) + :gate.market/last-fetched-page 0 + :domain.market/leaderboard {} + :domain.market/prices {} + :domain.market/tokens {}}) diff --git a/src/status_im/desktop/core.cljs b/src/status_im/desktop/core.cljs deleted file mode 100644 index 627a0cb250d..00000000000 --- a/src/status_im/desktop/core.cljs +++ /dev/null @@ -1,17 +0,0 @@ -(ns status-im.desktop.core - (:require [reagent.core :as reagent] - status-im.utils.db - status-im.ui.screens.db - status-im.ui.screens.events - status-im.ui.screens.subs - status-im.data-store.core - [status-im.ui.screens.desktop.views :as views] - [status-im.core :as core] - [status-im.ui.components.react :as react])) - -(defn app-root [] - (reagent/create-class - {:reagent-render views/main})) - -(defn init [] - (core/init app-root)) \ No newline at end of file diff --git a/src/status_im/desktop/platform.cljs b/src/status_im/desktop/platform.cljs deleted file mode 100644 index 4c06e187da1..00000000000 --- a/src/status_im/desktop/platform.cljs +++ /dev/null @@ -1,23 +0,0 @@ -(ns status-im.desktop.platform) - -(def fonts - {:light {:font-family "Roboto-Light"} - :default {:font-family "Roboto-Regular"} - :medium {:font-family "Roboto-Medium"} - - :toolbar-title {:font-family "Roboto-Regular"} - :roboto-mono {:font-family "RobotoMono-Medium"}}) - -;; Structure to be exported - -(def platform-specific - {:fonts fonts - :tabs {:tab-shadows? true} - :chats {:action-button? true - :new-chat-in-toolbar? false - :render-separator? false} - :contacts {:action-button? true - :new-contact-in-toolbar? false} - :group-block-shadows? true - :discover {:uppercase-subtitles? false} - :status-bar-default-height 25}) diff --git a/src/status_im/domain/README.md b/src/status_im/domain/README.md new file mode 100644 index 00000000000..f2df60ce2a3 --- /dev/null +++ b/src/status_im/domain/README.md @@ -0,0 +1,14 @@ +## Domain layer + +### Description +This folder contains "domain" layer. +Here belongs all the code responsible for storing "persistent" data which is data that remains useful after any user flow. +Data as `accounts`, `transactions`, `tokens` managed in this layer and stored normalized in rf-db under this layer. + +### Responsibilities +- storing data in a convenient structures and in a normalized form +- keeping functions to operate on domain data + +### Rules +- data on domain level modified only by domain events, never from other layers +- domain level knows nothing about UI and user flows diff --git a/src/status_im/domain/market/events.cljs b/src/status_im/domain/market/events.cljs new file mode 100644 index 00000000000..327fbf0f9e4 --- /dev/null +++ b/src/status_im/domain/market/events.cljs @@ -0,0 +1,24 @@ +(ns status-im.domain.market.events + (:require + [utils.re-frame :as rf])) + +(rf/reg-event-fx :domain.market/update-leaderboard + (fn [{:keys [db]} [leaderboard]] + {:db (update db :domain.market/leaderboard merge leaderboard)})) + +(rf/reg-event-fx :domain.market/update-prices + (fn [{:keys [db]} [new-prices]] + (let [old-prices (:domain.market/prices db) + merged-prices (merge-with merge old-prices new-prices)] + {:db (assoc db :domain.market/prices merged-prices)}))) + +(rf/reg-event-fx :domain.market/update-tokens + (fn [{:keys [db]} [tokens]] + {:db (update db :domain.market/tokens merge tokens)})) + +(rf/reg-event-fx :domain.market/clear-market-data + (fn [{:keys [db]}] + {:db (dissoc db + :domain.market/tokens + :domain.market/leaderboard + :domain.market/prices)})) diff --git a/src/status_im/domain/subs.cljs b/src/status_im/domain/subs.cljs new file mode 100644 index 00000000000..9c29e7ab1d8 --- /dev/null +++ b/src/status_im/domain/subs.cljs @@ -0,0 +1,7 @@ +(ns status-im.domain.subs + (:require + [utils.re-frame :as rf])) + +(rf/reg-root-key-sub :domain.market/leaderboard :domain.market/leaderboard) +(rf/reg-root-key-sub :domain.market/prices :domain.market/prices) +(rf/reg-root-key-sub :domain.market/tokens :domain.market/tokens) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs new file mode 100644 index 00000000000..85b9a05288c --- /dev/null +++ b/src/status_im/events.cljs @@ -0,0 +1,90 @@ +(ns status-im.events + (:require + status-im.app.market.events + status-im.app.wallet.events + status-im.common.alert-banner.events + status-im.common.alert.effects + status-im.common.app-monitoring.events + status-im.common.async-storage.effects + status-im.common.emoji-picker.events + status-im.common.enter-seed-phrase.events + status-im.common.font.events + status-im.common.image-crop-picker.events + [status-im.common.json-rpc.events] + status-im.common.kv-storage.effects + status-im.common.log + status-im.common.peer-stats.events + status-im.common.privacy-mode.events + status-im.common.shared-urls.events + status-im.common.signals.events + status-im.common.theme.effects + status-im.common.theme.events + [status-im.common.toasts.events] + status-im.common.universal-links + status-im.contexts.chat.contacts.events + status-im.contexts.chat.events + [status-im.contexts.chat.home.add-new-contact.events] + status-im.contexts.chat.messenger.composer.events + status-im.contexts.chat.messenger.photo-selector.events + status-im.contexts.communities.events + status-im.contexts.communities.overview.events + status-im.contexts.communities.sharing.events + status-im.contexts.contact.blocking.events + status-im.contexts.keycard.change-pin.events + status-im.contexts.keycard.effects + status-im.contexts.keycard.events + status-im.contexts.keycard.feature-unavailable.events + status-im.contexts.network.effects + status-im.contexts.network.events + status-im.contexts.onboarding.common.overlay.events + status-im.contexts.onboarding.events + status-im.contexts.profile.create.events + status-im.contexts.profile.events + status-im.contexts.profile.recover.events + status-im.contexts.profile.settings.events + status-im.contexts.settings.advanced.events + status-im.contexts.settings.language-and-currency.events + status-im.contexts.settings.wallet.saved-addresses.events + status-im.contexts.shell.effects + status-im.contexts.shell.events + status-im.contexts.shell.qr-reader.events + status-im.contexts.shell.share.events + status-im.contexts.syncing.events + status-im.contexts.wallet.add-account.add-address-to-watch.events + status-im.contexts.wallet.add-account.create-account.events + status-im.contexts.wallet.buy-crypto.events + status-im.contexts.wallet.collectible.events + status-im.contexts.wallet.common.wizard.events + status-im.contexts.wallet.effects + status-im.contexts.wallet.events + status-im.contexts.wallet.send.events + status-im.contexts.wallet.signals + status-im.contexts.wallet.swap.events + status-im.contexts.wallet.wallet-connect.events.core + [status-im.db :as db] + status-im.domain.market.events + status-im.gateway.events + status-im.gateway.market.events + status-im.navigation.effects + status-im.navigation.events + [utils.re-frame :as rf])) + +(rf/defn start-app + {:events [:app-started]} + [cofx] + (rf/merge + cofx + {:db db/app-db + :effects.async-storage/get {:keys [:selected-stack-id] + :cb (fn [{:keys [selected-stack-id]}] + (rf/dispatch [:shell/change-tab selected-stack-id]))} + :theme/init-theme nil + :effects.network/listen-to-network-info nil + :effects.biometric/get-supported-type nil + :effects.keycard/register-card-events nil + :effects.keycard/check-nfc-enabled nil + :effects.keycard/retrieve-pairings nil + ;;app starting flow continues in get-profiles-overview + :profile/get-profiles-overview #(rf/dispatch [:profile/get-profiles-overview-success %]) + :effects.font/get-font-file-for-initials-avatar + #(rf/dispatch [:font/init-font-file-for-initials-avatar %])})) diff --git a/src/status_im/feature_flags.cljs b/src/status_im/feature_flags.cljs new file mode 100644 index 00000000000..ec4501e05cb --- /dev/null +++ b/src/status_im/feature_flags.cljs @@ -0,0 +1,79 @@ +(ns status-im.feature-flags + (:require + [clojure.string :as string] + [react-native.async-storage :as async-storage] + [react-native.config :as config] + [reagent.core :as reagent])) + +(defn- enabled-in-env? + [k] + (= "1" (config/get-config k))) + +(def ^:private initial-flags + {::community.edit-account-selection (enabled-in-env? :FLAG_EDIT_ACCOUNT_SELECTION_ENABLED) + ::community.view-token-requirements (enabled-in-env? :FLAG_VIEW_TOKEN_REQUIREMENTS) + ::app-monitoring.intentional-crash (enabled-in-env? :FLAG_INTENTIONAL_CRASH_ENABLED) + ::analytics.copy-user-id (enabled-in-env? :FLAG_ANALYTICS_COPY_USER_ID) + + ;; Feature toggled (off by default) because the desktop app disabled this + ;; feature and we want both clients in sync. We keep the code because it + ;; works and we may re-enable it by default. + ::profile-pictures-visibility (enabled-in-env? :FLAG_PROFILE_PICTURES_VISIBILITY_ENABLED) + ::settings.import-all-keypairs (enabled-in-env? :FLAG_WALLET_SETTINGS_IMPORT_ALL_KEYPAIRS) + ::settings.news-notifications (enabled-in-env? + :FLAG_NOTIFICATION_SETTINGS_NEWS_NOTIFICATIONS) + ::wallet.advanced-sending (enabled-in-env? :FLAG_ADVANCED_SENDING) + ::wallet.assets-modal-hide (enabled-in-env? :FLAG_ASSETS_MODAL_HIDE) + ::wallet.assets-modal-manage-tokens (enabled-in-env? :FLAG_ASSETS_MODAL_MANAGE_TOKENS) + ::wallet.bridge-token (enabled-in-env? :FLAG_BRIDGE_TOKEN_ENABLED) + ::wallet.contacts (enabled-in-env? :FLAG_CONTACTS_ENABLED) + ::wallet.edit-derivation-path (enabled-in-env? :FLAG_EDIT_DERIVATION_PATH) + ::wallet.graph (enabled-in-env? :FLAG_GRAPH_ENABLED) + ::wallet.import-private-key (enabled-in-env? :FLAG_IMPORT_PRIVATE_KEY_ENABLED) + ::wallet.long-press-watch-only-asset (enabled-in-env? :FLAG_LONG_PRESS_WATCH_ONLY_ASSET_ENABLED) + ::wallet.saved-addresses (enabled-in-env? :WALLET_SAVED_ADDRESSES) + ::wallet.wallet-connect (enabled-in-env? :FLAG_WALLET_CONNECT_ENABLED) + ::wallet.custom-network-amounts (enabled-in-env? :FLAG_WALLET_CUSTOM_NETWORK_AMOUNTS_ENABLED) + ::wallet.transaction-params (enabled-in-env? :FLAG_WALLET_TRANSACTION_PARAMS_ENABLED) + ::market (enabled-in-env? :FLAG_MARKET_ENABLED) + ::privacy-mode-ui (enabled-in-env? :SHOW_PRIVACY_MODE_UI)}) + +(defonce ^:private feature-flags-config + (reagent/atom initial-flags)) + +(defn feature-flags [] @feature-flags-config) + +(def feature-flags-categories + (set (map + (fn [k] + (first (string/split (str (name k)) "."))) + (keys @feature-flags-config)))) + +(defn enabled? + [flag] + (get (feature-flags) flag)) + +(defn toggle + [flag] + (let [new-flags (update @feature-flags-config flag not)] + (async-storage/set-item! + :feature-flags + new-flags + (fn [] + (reset! feature-flags-config new-flags))))) + +(defn load-flags + [] + (async-storage/get-item + :feature-flags + (fn [flags] + (when flags + (reset! feature-flags-config flags))))) + +(defn reset-flags + [] + (async-storage/set-item! + :feature-flags + initial-flags + (fn [] + (reset! feature-flags-config initial-flags)))) diff --git a/src/status_im/gateway/README.md b/src/status_im/gateway/README.md new file mode 100644 index 00000000000..7f670cb49c3 --- /dev/null +++ b/src/status_im/gateway/README.md @@ -0,0 +1,13 @@ +## Gateway layer + +### Description +This folder contains "gateway" layer. +Here belongs all the code responsible for communication with external data sources - APIs. +It abstracts the particular data source from the data itself. + +### Responsibilities +- parsing network responses +- transforming backend data to internal formats + +### Rules +- never processes business logic. Just passes it further to **Domain** layer diff --git a/src/status_im/gateway/events.cljs b/src/status_im/gateway/events.cljs new file mode 100644 index 00000000000..69e0022631a --- /dev/null +++ b/src/status_im/gateway/events.cljs @@ -0,0 +1,25 @@ +(ns status-im.gateway.events + (:require + [utils.re-frame :as rf])) + +;; Log the error message and call the error handler event, if one exists. +(rf/reg-event-fx :gate.rpc/log-error + (fn [_ [{:keys [method params error-handler-event]} error]] + {:fx [[:effects.log/warn + [(str "Failed to call rpc: " method) + {:method method + :params params + :error error}]] + (when error-handler-event + [:dispatch [error-handler-event error]])]})) + +(rf/reg-event-fx :gate.rpc/call + (fn [_ [{:keys [method params error-handler-event]}]] + ;; TODO(volodymyr.kozieiev): :json-rpc/call should be moved to gateway layer + {:json-rpc/call [{:method method + :params params + :on-error [:gate.rpc/log-error + {:method method + :params params + :error-handler-event error-handler-event}]}]})) + diff --git a/src/status_im/gateway/market/core.cljs b/src/status_im/gateway/market/core.cljs new file mode 100644 index 00000000000..79e27a4d2ad --- /dev/null +++ b/src/status_im/gateway/market/core.cljs @@ -0,0 +1,36 @@ +(ns status-im.gateway.market.core) + +(defn page-size-to-ensure-tokens-updating + "Calculates the optimal page size to ensure that tokens are updated + based on the current visible index and the count of visible items. + + Takes a map with keys: + - :first-visible-index (int) - the index of the first visible item + - :visible-count (int) - the number of items currently visible + + Returns a map containing: + - :page-size (int) - the calculated size of the page + - :page-index (int) - the index of the computed page (1-based) + + If no valid page size is found, returns nil. + + Example usage: + (page-size-to-ensure-tokens-updating {:first-visible-index 14 + :visible-count 11})" + + [{:keys [first-visible-index visible-count]}] + (let [from first-visible-index + to (+ first-visible-index visible-count -1) + min-page-size visible-count + max-page-size (inc to)] + (when (pos? visible-count) + (some (fn [page-size] + (let [page-index (quot from page-size) + page-start (* page-index page-size) + page-end (+ page-start page-size -1)] + (when (and (<= page-start from) + (>= page-end to)) + {:page-size page-size + :page-index (inc page-index)}))) + (range min-page-size (inc max-page-size)))))) + diff --git a/src/status_im/gateway/market/core_test.cljs b/src/status_im/gateway/market/core_test.cljs new file mode 100644 index 00000000000..4040abf1599 --- /dev/null +++ b/src/status_im/gateway/market/core_test.cljs @@ -0,0 +1,15 @@ +(ns status-im.gateway.market.core-test + (:require [clojure.test :refer [are deftest]] + [status-im.gateway.market.core :refer [page-size-to-ensure-tokens-updating]])) + +(deftest page-size-to-ensure-tokens-updating-test + (are [input expected] + (= expected (page-size-to-ensure-tokens-updating input)) + {:first-visible-index 14 :visible-count 11} {:page-size 13 :page-index 2} + {:first-visible-index 10 :visible-count 6} {:page-size 8 :page-index 2} + {:first-visible-index 0 :visible-count 7} {:page-size 7 :page-index 1} + {:first-visible-index 15 :visible-count 0} nil + {:first-visible-index 0 :visible-count 1} {:page-size 1 :page-index 1} + {:first-visible-index 9 :visible-count 1} {:page-size 1 :page-index 10} + {:first-visible-index 5 :visible-count 2} {:page-size 4 :page-index 2} + {:first-visible-index 4 :visible-count 5} {:page-size 9 :page-index 1})) diff --git a/src/status_im/gateway/market/events.cljs b/src/status_im/gateway/market/events.cljs new file mode 100644 index 00000000000..2c60fca3592 --- /dev/null +++ b/src/status_im/gateway/market/events.cljs @@ -0,0 +1,76 @@ +(ns status-im.gateway.market.events + (:require + [status-im.gateway.market.core :as core] + [status-im.gateway.market.transform :as t] + [utils.number] + [utils.re-frame :as rf])) + +(def ^:const leaderboard-request-page-size 20) + +(rf/reg-event-fx :gate.market/start-fetching-tokens + (fn [{:keys [db]}] + (let [start-page 1] + {:db (assoc db :gate.market/last-fetched-page start-page) + :fx [[:dispatch [:gate.market/fetch-token-page start-page leaderboard-request-page-size]]]}))) + +(rf/reg-event-fx :gate.market/fetch-more-tokens + (fn [{:keys [db]}] + (let [last-page (:gate.market/last-fetched-page db) + next-page (inc last-page) + should-fetch-next-page? (> last-page 0)] + (when should-fetch-next-page? + {:db (assoc db :gate.market/last-fetched-page next-page) + :fx [[:dispatch [:gate.market/fetch-token-page next-page leaderboard-request-page-size]]]})))) + +(rf/reg-event-fx :gate.market/ensure-tokens-updating + (fn [_ [token-position tokens-amount]] + (let [{:keys [page-size page-index]} (core/page-size-to-ensure-tokens-updating + {:first-visible-index token-position + :visible-count tokens-amount})] + {:fx [[:dispatch [:gate.market/fetch-token-page page-index page-size]]]}))) + +(rf/reg-event-fx :gate.market/fetch-token-page + (fn [_ [page page-size]] + (let [page page + sort-order 0 + currency "" + params [page + page-size + sort-order + currency]] + {:fx [[:dispatch + [:gate.rpc/call + {:method "wallet_fetchMarketTokenPageAsync" + :params params}]]]}))) + +(rf/reg-event-fx :gate.market/stop-fetching-tokens + (fn [{:keys [db]}] + {:db (assoc db :gate.market/last-fetched-page 0) + :fx [[:dispatch + [:gate.rpc/call + {:method "wallet_unsubscribeFromLeaderboard"}]]]})) + +(rf/reg-event-fx :gate.market/leaderboard-page-fetched + (fn [_ [payload]] + (let [message (t/wallet-signal-message->clj payload)] + {:fx [[:dispatch [:gate.market/tranform-and-save-page-data message]]]}))) + +(rf/reg-event-fx :gate.market/leaderboard-page-data-updated + (fn [_ [payload]] + (let [message (t/wallet-signal-message->clj payload)] + {:fx [[:dispatch [:gate.market/tranform-and-save-page-data message]]]}))) + +(rf/reg-event-fx :gate.market/tranform-and-save-page-data + (fn [_ [message]] + (let [leaderboard (t/leaderboard-message->positions-by-token message) + prices (t/leaderboard-message->prices-by-token-id message) + tokens (t/leaderboard-message->token-data-by-token-id message)] + {:fx [[:dispatch [:domain.market/update-leaderboard leaderboard]] + [:dispatch [:domain.market/update-prices prices]] + [:dispatch [:domain.market/update-tokens tokens]]]}))) + +(rf/reg-event-fx :gate.market/leaderboard-page-prices-updated + (fn [_ [payload]] + (let [message (t/wallet-signal-message->clj payload) + prices-update (t/price-update-message->price-updates-by-token-id message)] + {:fx [[:dispatch [:domain.market/update-prices prices-update]]]}))) diff --git a/src/status_im/gateway/market/transform.cljs b/src/status_im/gateway/market/transform.cljs new file mode 100644 index 00000000000..9c5bf3205f9 --- /dev/null +++ b/src/status_im/gateway/market/transform.cljs @@ -0,0 +1,64 @@ +(ns status-im.gateway.market.transform + (:require + [oops.core :as oops] + [utils.transforms :as transforms])) + +(defn wallet-signal-message->clj + "Wallet signal comes as js object and has .-message field that is json-encoded. + This function extracts message field and converts it from json to clj" + [signal-payload] + (-> (oops/oget signal-payload "message") + (transforms/json->clj {:keywords-to-kebab-case? true}))) + +(defn leaderboard-message->positions-by-token + "Extract token ids and index them by position in a leaderboard" + [{:keys [page page-size cryptocurrencies] :as _leaderboard-message}] + (let [start-from (-> page + dec + (* page-size) + inc)] + (reduce (fn [acc cryptocurrency] + (let [position (+ start-from (count acc))] + (assoc acc (:id cryptocurrency) position))) + {} + cryptocurrencies))) + +(defn leaderboard-message->prices-by-token-id + "Extract price-related data from leaderboard message and index it by token id" + [{:keys [cryptocurrencies]}] + (reduce + (fn [acc token-data] + (assoc acc + (:id token-data) + (select-keys token-data + [:id + :current-price + :market-cap + :total-volume + :price-change-percentage-24h]))) + {} + cryptocurrencies)) + +(defn price-update-message->price-updates-by-token-id + "Extract prices and index them by token id" + [{:keys [prices]}] + (reduce + (fn [acc {:keys [id] :as price}] + (assoc acc id price)) + {} + prices)) + +(defn leaderboard-message->token-data-by-token-id + "Extract token information from leaderboard message and index it by token id" + [{:keys [cryptocurrencies]}] + (reduce + (fn [acc token-data] + (assoc acc + (:id token-data) + (select-keys token-data + [:id + :symbol + :name + :image]))) + {} + cryptocurrencies)) diff --git a/src/status_im/i18n.cljs b/src/status_im/i18n.cljs deleted file mode 100644 index 62c367f7c5f..00000000000 --- a/src/status_im/i18n.cljs +++ /dev/null @@ -1,462 +0,0 @@ -(ns status-im.i18n - (:require - [cljs.spec.alpha :as spec] - [status-im.react-native.js-dependencies :as rn-dependencies] - [status-im.translations.af :as af] - [status-im.translations.ar :as ar] - [status-im.translations.bel :as be] - [status-im.translations.cs :as cs] - [status-im.translations.da :as da] - [status-im.translations.de :as de] - [status-im.translations.de-ch :as de-ch] - [status-im.translations.el :as el] - [status-im.translations.en :as en] - [status-im.translations.es :as es] - [status-im.translations.es-ar :as es-ar] - [status-im.translations.es-mx :as es-mx] - [status-im.translations.fi :as fi] - [status-im.translations.fr :as fr] - [status-im.translations.fr-ch :as fr-ch] - [status-im.translations.fy :as fy] - [status-im.translations.he :as he] - [status-im.translations.hi :as hi] - [status-im.translations.hu :as hu] - [status-im.translations.id :as id] - [status-im.translations.it :as it] - [status-im.translations.it-ch :as it-ch] - [status-im.translations.ja :as ja] - [status-im.translations.ko :as ko] - [status-im.translations.la :as la] - [status-im.translations.lt :as lt] - [status-im.translations.lv :as lv] - [status-im.translations.ms :as ms] - [status-im.translations.nb :as nb] - [status-im.translations.ne :as ne] - [status-im.translations.nl :as nl] - [status-im.translations.pl :as pl] - [status-im.translations.pt-br :as pt-br] - [status-im.translations.pt-pt :as pt-pt] - [status-im.translations.ro :as ro] - [status-im.translations.ru :as ru] - [status-im.translations.sl :as sl] - [status-im.translations.sr-rs-cyrl :as sr-rs-cyrl] - [status-im.translations.sr-rs-latn :as sr-rs-latn] - [status-im.translations.sv :as sv] - [status-im.translations.sw :as sw] - [status-im.translations.th :as th] - [status-im.translations.tr :as tr] - [status-im.translations.uk :as uk] - [status-im.translations.ur :as ur] - [status-im.translations.vi :as vi] - [status-im.translations.zh-hans :as zh-hans] - [status-im.translations.zh-hant :as zh-hant] - [status-im.translations.zh-hant-tw :as zh-hant-tw] - [status-im.translations.zh-hant-sg :as zh-hant-sg] - [status-im.translations.zh-hant-hk :as zh-hant-hk] - [status-im.translations.zh-wuu :as zh-wuu] - [status-im.translations.zh-yue :as zh-yue] - [status-im.utils.js-resources :refer [default-contacts]] - [taoensso.timbre :as log] - [clojure.string :as string] - [clojure.set :as set])) - -(set! (.-fallbacks rn-dependencies/i18n) true) -(set! (.-defaultSeparator rn-dependencies/i18n) "/") - -;; translations -#_(def translations-by-locale {:af af/translations - :ar ar/translations - :be be/translations - :cs cs/translations - :da da/translations - :de de/translations - :de-ch de-ch/translations - :el el/translations - :en en/translations - :es es/translations - :es-ar es-ar/translations - :es-mx es-mx/translations - :fi fi/translations - :fr fr/translations - :fr-ch fr-ch/translations - :fy fy/translations - :he he/translations - :hi hi/translations - :hu hu/translations - :id id/translations - :it it/translations - :it-ch it-ch/translations - :ja ja/translations - :ko ko/translations - :la la/translations - :lt lt/translations - :lv lv/translations - :ms ms/translations - :nb nb/translations - :ne ne/translations - :nl nl/translations - :pl pl/translations - :pt-br pt-br/translations - :pt-pt pt-pt/translations - :ro ro/translations - :ru ru/translations - :sl sl/translations - :sr sr-rs-cyrl/translations - :sr-RS_#Latn sr-rs-latn/translations - :sr-RS_#Cyrl sr-rs-cyrl/translations - :sv sv/translations - :sw sw/translations - :th th/translations - :tr tr/translations - :uk uk/translations - :ur ur/translations - :vi vi/translations - :zh zh-hans/translations - :zh-hans zh-hans/translations - :zh-hans-cn zh-hans/translations - :zh-hans-mo zh-hans/translations - :zh-hant zh-hant/translations - :zh-hant-tw zh-hant-tw/translations - :zh-hant-sg zh-hant-sg/translations - :zh-hant-hk zh-hant-hk/translations - :zh-hant-cn zh-hant/translations - :zh-hant-mo zh-hant/translations - :zh-wuu zh-wuu/translations - :zh-yue zh-yue/translations}) -(def translations-by-locale {:en en/translations}) ; Temporarily disable all languages except English - -;; english as source of truth -(def labels (set (keys en/translations))) - -(spec/def ::label labels) -(spec/def ::labels (spec/coll-of ::label :kind set? :into #{})) - -(defn labels-for-all-locales [] - (->> translations-by-locale - (mapcat #(-> % val keys)) - set)) - -;; checkpoints - -;; Checkpoints specify milestones for locales. -;; -;; With milestones we can ensure that expected supported languages -;; are actually supported, and visualize the translation state for -;; the rest of locales according to these milestones. -;; -;; Checkpoints are defined by indicating the labels that need to be present -;; in a locale to achieve that checkpoint. -;; -;; We need to define the checkpoint that needs to be achieved for -;; a locale to be considered supported. This is why as we develop -;; we add translations, so we need to be defining a new target -;; for supported languages to achieve. -;; -;; Checkpoints are only used in dev and test. In dev when we want to -;; manually check the state of checkpoints for locales, and in test -;; to automatically check supported locales against the target checkpoint. - -(spec/def ::checkpoint.id keyword?) -(spec/def ::checkpoint-defs (spec/map-of ::checkpoint.id ::labels)) - -;; We define here the labels for the first specified checkpoint. -(def checkpoint-0-9-12-labels - #{:validation-amount-invalid-number :transaction-details :confirm :description - :phone-national :amount :open :close-app-title :members-active :chat-name - :phew-here-is-your-passphrase :public-group-topic :debug-enabled - :chat-settings :offline :update-status :invited :chat-send-eth :address - :new-public-group-chat :datetime-hour :wallet-settings - :datetime-ago-format :close-app-button :block :camera-access-error - :wallet-invalid-address :address-explication :remove - :transactions-delete-content :transactions-unsigned-empty - :transaction-moved-text :add-members :sign-later-title :sharing-cancel - :yes :dapps :popular-tags :network-settings :twelve-words-in-correct-order - :transaction-moved-title :photos-access-error :hash - :removed-from-chat :done :remove-from-contacts :delete-chat :new-group-chat - :edit-chats :wallet :wallet-exchange :wallet-request :sign-in - :datetime-yesterday :create-new-account :sign-in-to-status :dapp-profile - :sign-later-text :datetime-ago :no-hashtags-discovered-body :contacts - :search-chat :got-it :delete-group-confirmation :public-chats - :not-applicable :move-to-internal-failure-message :active-online - :password :status-seen-by-everyone :edit-group :not-specified - :delete-group :send-request :paste-json :browsing-title - :wallet-add-asset :reorder-groups :transactions-history-empty :discover - :browsing-cancel :faucet-success :intro-status :name :gas-price - :view-transaction-details :wallet-error - :validation-amount-is-too-precise :copy-transaction-hash :unknown-address - :received-invitation :show-qr :edit-network-config :connect - :choose-from-contacts :edit :wallet-address-from-clipboard - :account-generation-message :remove-network :no-messages :passphrase - :recipient :members-title :new-group :suggestions-requests - :connected :rpc-url :settings :remove-from-group :specify-rpc-url - :transactions-sign-all :gas-limit :wallet-browse-photos - :add-new-contact :no-statuses-discovered-body :add-json-file :delete - :search-contacts :chats :transaction-sent :transaction :public-group-status - :leave-chat :transactions-delete :mainnet-text :image-source-make-photo - :chat :start-conversation :topic-format :add-new-network :save - :enter-valid-public-key :faucet-error :all - :confirmations-helper-text :search-for :sharing-copy-to-clipboard - :your-wallets :sync-in-progress :enter-password - :enter-address :switch-users :send-transaction :confirmations - :recover-access :image-source-gallery :sync-synced - :currency :status-pending :delete-contact :connecting-requires-login - :no-hashtags-discovered-title :datetime-day :request-transaction - :wallet-send :mute-notifications :scan-qr :contact-s - :unsigned-transaction-expired :status-sending :gas-used - :transactions-filter-type :next :recent - :open-on-etherscan :share :status :from - :wrong-password :search-chats :transactions-sign-later :in-contacts - :transactions-sign :sharing-share :type-a-message - :usd-currency :existing-networks :node-unavailable :url :shake-your-phone - :add-network :unknown-status-go-error :contacts-group-new-chat :and-you - :wallets :clear-history :wallet-choose-from-contacts - :signing-phrase-description :no-contacts :here-is-your-signing-phrase - :soon :close-app-content :status-sent :status-prompt :testfairy-title - :delete-contact-confirmation :datetime-today :add-a-status - :web-view-error :notifications-title :error :transactions-sign-transaction - :edit-contacts :more :cancel :no-statuses-found :can-not-add-yourself - :transaction-description :add-to-contacts :available - :paste-json-as-text :You :main-wallet :process-json :testnet-text - :transactions :transactions-unsigned :members :intro-message1 - :public-chat-user-count :eth :transactions-history :not-implemented - :new-contact :datetime-second :status-failed :is-typing :recover - :suggestions-commands :nonce :new-network :contact-already-added :datetime-minute - :browsing-open-in-web-browser :delete-group-prompt :wallet-total-value - :wallet-insufficient-funds :edit-profile :active-unknown :testfairy-message - :search-tags :transaction-failed :public-key :error-processing-json - :status-seen :transactions-filter-tokens :status-delivered :profile - :wallet-choose-recipient :no-statuses-discovered :none :removed :empty-topic - :no :transactions-filter-select-all :transactions-filter-title :message - :here-is-your-passphrase :wallet-assets :image-source-title :current-network - :left :edit-network-warning :to :data :cost-fee}) - -;; NOTE: the rest checkpoints are based on the previous one, defined -;; like this: -;; (def checkpoint-2-labels (set/union checkpoint-1-labels #{:foo :bar}) -;; (def checkpoint-3-labels (set/union checkpoint-2-labels #{:baz}) - -;; NOTE: This defines the scope of each checkpoint. To support a checkpoint, -;; change the var `checkpoint-to-consider-locale-supported` a few lines -;; below. -(def checkpoints-def (spec/assert ::checkpoint-defs - {::checkpoint-0-9-12 checkpoint-0-9-12-labels})) -(def checkpoints (set (keys checkpoints-def))) - -(spec/def ::checkpoint checkpoints) - -(def checkpoint-to-consider-locale-supported ::checkpoint-0-9-12) - -(defn checkpoint->labels [checkpoint] - (get checkpoints-def checkpoint)) - -(defn checkpoint-val-to-compare [c] - (-> c name (string/replace #"^.*\|" "") int)) - -(defn >checkpoints [& cs] - (apply > (map checkpoint-val-to-compare cs))) - -(defn labels-that-are-not-in-current-checkpoint [] - (set/difference labels (checkpoint->labels checkpoint-to-consider-locale-supported))) - -;; locales - -(def locales (set (keys translations-by-locale))) - -(spec/def ::locale locales) -(spec/def ::locales (spec/coll-of ::locale :kind set? :into #{})) - -;; NOTE: Add new locale keywords here to indicate support for them. -#_(def supported-locales (spec/assert ::locales #{:fr - :zh - :zh-hans - :zh-hans-cn - :zh-hans-mo - :zh-hant - :zh-hant-sg - :zh-hant-hk - :zh-hant-tw - :zh-hant-mo - :zh-hant-cn - :sr-RS_#Cyrl - :el - :en - :de - :lt - :sr-RS_#Latn - :sr - :sv - :ja - :uk})) -(def supported-locales (spec/assert ::locales #{:en})) - -(spec/def ::supported-locale supported-locales) -(spec/def ::supported-locales (spec/coll-of ::supported-locale :kind set? :into #{})) - -(defn locale->labels [locale] - (-> translations-by-locale (get locale) keys set)) - -(defn locale->checkpoint [locale] - (let [locale-labels (locale->labels locale) - checkpoint (->> checkpoints-def - (filter (fn [[checkpoint checkpoint-labels]] - (set/subset? checkpoint-labels locale-labels))) - ffirst)] - checkpoint)) - -(defn locales-with-checkpoint [] - (->> locales - (map (fn [locale] - [locale (locale->checkpoint locale)])) - (into {}))) - -(defn locale-is-supported-based-on-translations? [locale] - (let [c (locale->checkpoint locale)] - (and c (or (= c checkpoint-to-consider-locale-supported) - (>checkpoints checkpoint-to-consider-locale-supported c))))) - -(defn actual-supported-locales [] - (->> locales - (filter locale-is-supported-based-on-translations?) - set)) - -(defn locales-with-full-support [] - (->> locales - (filter (fn [locale] - (set/subset? labels (locale->labels locale)))) - set)) - -(defn supported-locales-that-are-not-considered-supported [] - (set/difference (actual-supported-locales) supported-locales)) - -(set! (.-translations rn-dependencies/i18n) - (clj->js translations-by-locale)) - -;;:zh, :zh-hans-xx, :zh-hant-xx have been added until this bug will be fixed https://github.com/fnando/i18n-js/issues/460 - -(def delimeters - "This function is a hack: mobile Safari doesn't support toLocaleString(), so we need to pass - this map to WKWebView to make number formatting work." - (let [n (.toLocaleString (js/Number 1000.1)) - delimiter? (= (count n) 7)] - (if delimiter? - {:delimiter (subs n 1 2) - :separator (subs n 5 6)} - {:delimiter "" - :separator (subs n 4 5)}))) - -(defn label-number [number] - (when number - (let [{:keys [delimiter separator]} delimeters] - (.toNumber rn-dependencies/i18n - (string/replace number #"," ".") - (clj->js {:precision 10 - :strip_insignificant_zeros true - :delimiter delimiter - :separator separator}))))) - -(def default-option-value "") - -(defn label-options [options] - ;; i18n ignores nil value, leading to misleading messages - (into {} (for [[k v] options] [k (or v default-option-value)]))) - -(defn label - ([path] (label path {})) - ([path options] - (if (exists? rn-dependencies/i18n.t) - (let [options (update options :amount label-number)] - (.t rn-dependencies/i18n (name path) (clj->js (label-options options)))) - (name path)))) - -(defn label-pluralize [count path & options] - (if (exists? rn-dependencies/i18n.t) - (.p rn-dependencies/i18n count (name path) (clj->js options)) - (name path))) - -(defn message-status-label [status] - (->> status - (name) - (str "t/status-") - (keyword) - (label))) - -(def locale - (.-locale rn-dependencies/i18n)) - -(defn get-contact-translated [contact-id key fallback] - (let [translation #(get-in default-contacts [(keyword contact-id) key (keyword %)])] - (or (translation locale) - (translation (subs locale 0 2)) - fallback))) - -(defn format-currency [value currency-code] - (.addTier2Support goog.i18n.currency) - - (def currency-code-to-nfs-map - {"ZAR" (.-NumberFormatSymbols_af goog.i18n) - "ETB" (.-NumberFormatSymbols_am goog.i18n) - "EGP" (.-NumberFormatSymbols_ar goog.i18n) - "DZD" (.-NumberFormatSymbols_ar_DZ goog.i18n) - "AZN" (.-NumberFormatSymbols_az goog.i18n) - "BYN" (.-NumberFormatSymbols_be goog.i18n) - "BGN" (.-NumberFormatSymbols_bg goog.i18n) - "BDT" (.-NumberFormatSymbols_bn goog.i18n) - "EUR" (.-NumberFormatSymbols_br goog.i18n) - "BAM" (.-NumberFormatSymbols_bs goog.i18n) - "USD" (.-NumberFormatSymbols_en goog.i18n) - "CZK" (.-NumberFormatSymbols_cs goog.i18n) - "GBP" (.-NumberFormatSymbols_cy goog.i18n) - "DKK" (.-NumberFormatSymbols_da goog.i18n) - "CHF" (.-NumberFormatSymbols_de_CH goog.i18n) - "AUD" (.-NumberFormatSymbols_en_AU goog.i18n) - "CAD" (.-NumberFormatSymbols_en_CA goog.i18n) - "INR" (.-NumberFormatSymbols_en_IN goog.i18n) - "SGD" (.-NumberFormatSymbols_en_SG goog.i18n) - "MXN" (.-NumberFormatSymbols_es_419 goog.i18n) - "IRR" (.-NumberFormatSymbols_fa goog.i18n) - "PHP" (.-NumberFormatSymbols_fil goog.i18n) - "ILS" (.-NumberFormatSymbols_he goog.i18n) - "HRK" (.-NumberFormatSymbols_hr goog.i18n) - "HUF" (.-NumberFormatSymbols_hu goog.i18n) - "AMD" (.-NumberFormatSymbols_hy goog.i18n) - "IDR" (.-NumberFormatSymbols_id goog.i18n) - "ISK" (.-NumberFormatSymbols_is goog.i18n) - "JPY" (.-NumberFormatSymbols_ja goog.i18n) - "GEL" (.-NumberFormatSymbols_ka goog.i18n) - "KZT" (.-NumberFormatSymbols_kk goog.i18n) - "KHR" (.-NumberFormatSymbols_km goog.i18n) - "KRW" (.-NumberFormatSymbols_ko goog.i18n) - "KGS" (.-NumberFormatSymbols_ky goog.i18n) - "CDF" (.-NumberFormatSymbols_ln goog.i18n) - "LAK" (.-NumberFormatSymbols_lo goog.i18n) - "MKD" (.-NumberFormatSymbols_mk goog.i18n) - "MNT" (.-NumberFormatSymbols_mn goog.i18n) - "MDL" (.-NumberFormatSymbols_mo goog.i18n) - "MYR" (.-NumberFormatSymbols_ms goog.i18n) - "MMK" (.-NumberFormatSymbols_my goog.i18n) - "NOK" (.-NumberFormatSymbols_nb goog.i18n) - "NPR" (.-NumberFormatSymbols_ne goog.i18n) - "PLN" (.-NumberFormatSymbols_pl goog.i18n) - "BRL" (.-NumberFormatSymbols_pt goog.i18n) - "RON" (.-NumberFormatSymbols_ro goog.i18n) - "RUB" (.-NumberFormatSymbols_ru goog.i18n) - "RSD" (.-NumberFormatSymbols_sh goog.i18n) - "LKR" (.-NumberFormatSymbols_si goog.i18n) - "ALL" (.-NumberFormatSymbols_sq goog.i18n) - "SEK" (.-NumberFormatSymbols_sv goog.i18n) - "TZS" (.-NumberFormatSymbols_sw goog.i18n) - "THB" (.-NumberFormatSymbols_th goog.i18n) - "TRY" (.-NumberFormatSymbols_tr goog.i18n) - "UAH" (.-NumberFormatSymbols_uk goog.i18n) - "PKR" (.-NumberFormatSymbols_ur goog.i18n) - "UZS" (.-NumberFormatSymbols_uz goog.i18n) - "VND" (.-NumberFormatSymbols_vi goog.i18n) - "CNY" (.-NumberFormatSymbols_zh goog.i18n) - "HKD" (.-NumberFormatSymbols_zh_HK goog.i18n) - "TWD" (.-NumberFormatSymbols_zh_TW goog.i18n)}) - - (def custom-nfs (currency-code-to-nfs-map currency-code)) - (set! (.-NumberFormatSymbols goog.i18n) (if custom-nfs custom-nfs (.-NumberFormatSymbols_en goog.i18n))) - (.format - (new goog.i18n.NumberFormat (.-CURRENCY goog.i18n.NumberFormat.Format) currency-code) - value)) diff --git a/src/status_im/ios/core.cljs b/src/status_im/ios/core.cljs deleted file mode 100644 index 6a96ba08622..00000000000 --- a/src/status_im/ios/core.cljs +++ /dev/null @@ -1,51 +0,0 @@ -(ns status-im.ios.core - (:require [reagent.core :as reagent] - [re-frame.core :refer [subscribe dispatch dispatch-sync]] - status-im.utils.db - status-im.ui.screens.db - status-im.ui.screens.events - status-im.ui.screens.subs - status-im.data-store.core - [status-im.ui.screens.views :as views] - [status-im.ui.components.react :as react] - [status-im.utils.notifications :as notifications] - [status-im.core :as core] - [status-im.utils.instabug :as instabug] - [status-im.utils.snoopy :as snoopy])) - -(defn app-state-change-handler [state] - (dispatch [:app-state-change state])) - -(defn app-root [] - (let [keyboard-height (subscribe [:get :keyboard-height])] - (reagent/create-class - {:component-will-mount - (fn [] - (.addListener react/keyboard - "keyboardWillShow" - (fn [e] - (let [h (.. e -endCoordinates -height)] - (when-not (= h @keyboard-height) - (dispatch [:set :keyboard-height h]) - (dispatch [:set :keyboard-max-height h]))))) - (.addListener react/keyboard - "keyboardWillHide" - #(when-not (= 0 @keyboard-height) - (dispatch [:set :keyboard-height 0]))) - (.hide react/splash-screen) - (.addEventListener react/app-state "change" app-state-change-handler)) - :component-did-mount - (fn [] - (notifications/on-refresh-fcm-token) - (notifications/on-notification)) - :component-will-unmount - (fn [] - (.stop react/http-bridge) - (.removeEventListener react/app-state "change" app-state-change-handler)) - :display-name "root" - :reagent-render views/main}))) - -(defn init [] - (core/init app-root) - (snoopy/subscribe!) - (instabug/init)) diff --git a/src/status_im/ios/platform.cljs b/src/status_im/ios/platform.cljs deleted file mode 100644 index 1109d5e886c..00000000000 --- a/src/status_im/ios/platform.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.ios.platform - (:require [status-im.react-native.js-dependencies :as rn-dependencies])) - -(def fonts - {:light {:font-family "SFUIText-Light"} - :default {:font-family "SFUIText-Regular"} - :medium {:font-family "SFUIText-Medium"} - :bold {:font-family "SFUIText-Bold"} - - :toolbar-title {:font-family "SFUIText-Semibold"} - :toolbar-subtitle {:font-family "SFUIText-Regular"} - :roboto-mono {:font-family "RobotoMono-Medium"}}) - -;; iPhone X dimensions -(def x-width 375) -(def x-height 812) - -(defn iphone-x-dimensions? [] - (let [{:keys [width height]} (-> (.-Dimensions rn-dependencies/react-native) - (.get "window") - (js->clj :keywordize-keys true))] - (and (= width x-width) (= height x-height)))) - -(def platform-specific - {:fonts fonts - :tabs {:tab-shadows? false} - :chats {:action-button? false - :new-chat-in-toolbar? true - :render-separator? true} - :contacts {:action-button? false - :new-contact-in-toolbar? true} - :group-block-shadows? false - :discover {:uppercase-subtitles? true} - :status-bar-default-height (if (iphone-x-dimensions?) 0 20)}) diff --git a/src/status_im/js_dependencies.cljs b/src/status_im/js_dependencies.cljs deleted file mode 100644 index b2eeb0ab0c3..00000000000 --- a/src/status_im/js_dependencies.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns status-im.js-dependencies) - -(def Chance (js/require "chance")) -(def emojis (js/require "emojilib")) -(def homoglyph-finder (js/require "homoglyph-finder")) -(def identicon-js (js/require "identicon.js")) -(def Web3 (js/require "web3")) diff --git a/src/status_im/models/account.cljs b/src/status_im/models/account.cljs deleted file mode 100644 index df3c3b1db4d..00000000000 --- a/src/status_im/models/account.cljs +++ /dev/null @@ -1,5 +0,0 @@ -(ns status-im.models.account) - -(defn logged-in? [cofx] - (boolean - (get-in cofx [:db :account/account]))) diff --git a/src/status_im/models/bootnode.cljs b/src/status_im/models/bootnode.cljs deleted file mode 100644 index 85e42d1e9eb..00000000000 --- a/src/status_im/models/bootnode.cljs +++ /dev/null @@ -1,81 +0,0 @@ -(ns status-im.models.bootnode - (:require - [clojure.string :as string] - [status-im.ui.screens.accounts.utils :as accounts.utils] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.ethereum.core :as ethereum])) - -(def address-regex #"enode://[a-zA-Z0-9]+:?(.*)\@\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b:(\d{1,5})") - -(defn valid-address? [address] - (re-matches address-regex address)) - -(defn- build [id bootnode-name address chain] - {:address address - :chain chain - :id (string/replace id "-" "") - :name bootnode-name}) - -(defn fetch [id cofx] - (let [network (get-in cofx [:db :network])] - (get-in cofx [:db :account/account :bootnodes network id]))) - -(defn set-input [input-key value {:keys [db]}] - {:db (update - db - :bootnodes/manage - assoc - input-key - {:value value - :error (case input-key - :id false - :name (string/blank? value) - :url (not (valid-address? value)))})}) - -(defn edit [id {:keys [db] :as cofx}] - (let [{:keys [id - address - name]} (fetch id cofx) - fxs (handlers-macro/merge-fx - cofx - (set-input :id id) - (set-input :url (str address)) - (set-input :name (str name)))] - (assoc fxs :dispatch [:navigate-to :edit-bootnode]))) - -(defn custom-bootnodes-in-use? [{:keys [db] :as cofx}] - (let [network (:network db)] - (get-in db [:account/account :settings :bootnodes network]))) - -(defn delete [id {{:account/keys [account] :as db} :db :as cofx}] - (let [network (:network db) - new-account (update-in account [:bootnodes network] dissoc id)] - (handlers-macro/merge-fx {:db (assoc db :account/account new-account)} - (accounts.utils/account-update - (select-keys new-account [:bootnodes]) - (when (custom-bootnodes-in-use? cofx) - [:logout]))))) - -(defn upsert [{{:bootnodes/keys [manage] :account/keys [account] :as db} :db :as cofx}] - (let [{:keys [name - id - url]} manage - network (:network db) - bootnode (build - (or (:value id) (:random-id cofx)) - (:value name) - (:value url) - network) - new-bootnodes (assoc-in - (:bootnodes account) - [network (:id bootnode)] - bootnode)] - - (handlers-macro/merge-fx - cofx - {:db (dissoc db :bootnodes/manage) - :dispatch [:navigate-back]} - (accounts.utils/account-update - {:bootnodes new-bootnodes} - (when (custom-bootnodes-in-use? cofx) - [:logout]))))) diff --git a/src/status_im/models/browser.cljs b/src/status_im/models/browser.cljs deleted file mode 100644 index a39b5b40424..00000000000 --- a/src/status_im/models/browser.cljs +++ /dev/null @@ -1,85 +0,0 @@ -(ns status-im.models.browser - (:require [status-im.data-store.browser :as browser-store] - [status-im.data-store.dapp-permissions :as dapp-permissions] - [status-im.i18n :as i18n] - [status-im.constants :as constants] - [status-im.ui.screens.browser.default-dapps :as default-dapps] - [status-im.utils.http :as http])) - -(defn get-current-url [{:keys [history history-index]}] - (when (and history-index history) - (nth history history-index))) - -(defn can-go-back? [{:keys [history-index]}] - (pos? history-index)) - -(defn can-go-forward? [{:keys [history-index history]}] - (< history-index (dec (count history)))) - -(defn check-if-dapp-in-list [{:keys [history history-index] :as browser}] - (let [history-host (http/url-host (try (nth history history-index) (catch js/Error _))) - dapp (first (filter #(= history-host (http/url-host (:dapp-url %))) (apply concat (mapv :data default-dapps/all))))] - (if dapp - (assoc browser :dapp? true :name (:name dapp)) - (assoc browser :dapp? false :name (i18n/label :t/browser))))) - -(defn update-browser-fx [{:keys [db now]} browser] - (let [updated-browser (check-if-dapp-in-list (assoc browser :timestamp now))] - {:db (update-in db [:browser/browsers (:browser-id updated-browser)] - merge updated-browser) - :data-store/tx [(browser-store/save-browser-tx updated-browser)]})) - -(defn update-browser-history-fx [cofx browser url loading?] - (when-not loading? - (let [history-index (:history-index browser) - history (:history browser) - history-url (try (nth history history-index) (catch js/Error _))] - (when (not= history-url url) - (let [slash? (= url (str history-url "/")) - new-history (if slash? - (assoc history history-index url) - (conj (subvec history 0 (inc history-index)) url)) - new-index (if slash? - history-index - (dec (count new-history)))] - (update-browser-fx cofx - (assoc browser :history new-history :history-index new-index))))))) - -(defn update-browser-and-navigate [cofx browser] - (merge (update-browser-fx cofx browser) - {:dispatch [:navigate-to :browser (:browser-id browser)]})) - -(def permissions {constants/dapp-permission-contact-code {:label (i18n/label :t/your-contact-code)}}) - -(defn update-dapp-permissions-fx [{:keys [db]} permissions] - {:db (assoc-in db [:dapps/permissions (:dapp permissions)] permissions) - :data-store/tx [(dapp-permissions/save-dapp-permissions permissions)]}) - -(defn request-permission [cofx - {:keys [dapp-name index requested-permissions permissions-allowed user-permissions - permissions-data webview] - :as params}] - ;; iterate all requested permissions - (if (< index (count requested-permissions)) - (let [requested-permission (get requested-permissions index)] - ;; if requested permission exists and valid continue if not decline permission - (if (and requested-permission (get permissions requested-permission)) - ;; if permission already allowed go to next, if not, show confirmation dialog - (if ((set user-permissions) requested-permission) - {:dispatch [:next-dapp-permission params requested-permission permissions-data]} - {:show-dapp-permission-confirmation-fx [requested-permission params]}) - {:dispatch [:next-dapp-permission params]})) - (assoc (update-dapp-permissions-fx cofx {:dapp dapp-name - :permissions (vec (set (concat (keys permissions-allowed) - user-permissions)))}) - :send-to-bridge-fx [permissions-allowed webview]))) - -(defn next-permission [cofx params & [permission permissions-data]] - (request-permission - cofx - (cond-> params - true - (update :index inc) - - (and permission permissions-data) - (assoc-in [:permissions-allowed permission] (get permissions-data permission))))) \ No newline at end of file diff --git a/src/status_im/models/contact.cljs b/src/status_im/models/contact.cljs deleted file mode 100644 index 99ffe697e11..00000000000 --- a/src/status_im/models/contact.cljs +++ /dev/null @@ -1,94 +0,0 @@ -(ns status-im.models.contact - (:require - [status-im.utils.contacts :as utils.contacts] - [status-im.data-store.contacts :as contacts-store] - [status-im.transport.message.core :as transport] - [status-im.transport.message.v1.contact :as message.v1.contact] - [status-im.utils.handlers-macro :as handlers-macro])) - -(defn can-add-to-contacts? [{:keys [pending? dapp?]}] - (and (not dapp?) - (or pending? - ;; it's not in the contact list at all - (nil? pending?)))) - -(defn- build-contact [whisper-id {{:keys [chats] :contacts/keys [contacts]} :db}] - (assoc (or (get contacts whisper-id) - (utils.contacts/whisper-id->new-contact whisper-id)) - :address (utils.contacts/public-key->address whisper-id))) - -(defn- own-info [db] - (let [{:keys [name photo-path address]} (:account/account db) - fcm-token (get-in db [:notifications :fcm-token])] - {:name name - :profile-image photo-path - :address address - :fcm-token fcm-token})) - -(defn- add-new-contact [{:keys [whisper-identity] :as contact} {:keys [db]}] - (let [new-contact (assoc contact - :pending? false - :public-key whisper-identity)] - {:db (-> db - (update-in [:contacts/contacts whisper-identity] - merge new-contact) - (assoc-in [:contacts/new-identity] "")) - :data-store/tx [(contacts-store/save-contact-tx new-contact)]})) - -(defn send-contact-request [{:keys [whisper-identity pending? dapp?] :as contact} {:keys [db] :as cofx}] - (when-not dapp? - (if pending? - (transport/send (message.v1.contact/map->ContactRequestConfirmed (own-info db)) whisper-identity cofx) - (transport/send (message.v1.contact/map->ContactRequest (own-info db)) whisper-identity cofx)))) - -(defn add-contact [whisper-id {:keys [db] :as cofx}] - (let [contact (build-contact whisper-id cofx)] - (handlers-macro/merge-fx cofx - (add-new-contact contact) - (send-contact-request contact)))) - -(defn handle-contact-update - [public-key - timestamp - {:keys [name profile-image address fcm-token] :as m} - {{:contacts/keys [contacts] :keys [current-public-key] :as db} :db :as cofx}] - ;; We need to convert to timestamp ms as before we were using now in ms to - ;; set last updated - ;; Using whisper timestamp mostly works but breaks in a few scenarios: - ;; 2 updates sent in the same second - ;; when using multi-device & clocks are out of sync - ;; Using logical clocks is probably the correct way to handle it, but an overkill - ;; for now - (let [timestamp-ms (* timestamp 1000) - prev-last-updated (get-in db [:contacts/contacts public-key :last-updated])] - (when (and (not= current-public-key public-key) - (< prev-last-updated timestamp-ms)) - (let [contact (get contacts public-key) - - ;; Backward compatibility with <= 0.9.21, as they don't send - ;; fcm-token & address in contact updates - contact-props (cond-> - {:whisper-identity public-key - :public-key public-key - :photo-path profile-image - :name name - :address (or address - (:address contact) - (utils.contacts/public-key->address public-key)) - :last-updated timestamp-ms - ;;NOTE (yenda) in case of concurrent contact request - :pending? (get contact :pending? true)} - fcm-token (assoc :fcm-token fcm-token))] - ;;NOTE (yenda) only update if there is changes to the contact - (when-not (= contact-props - (select-keys contact [:whisper-identity :public-key :address - :photo-path :name :fcm-token :pending?])) - (handlers-macro/merge-fx cofx - {:db (update-in db [:contacts/contacts public-key] - merge contact-props) - :data-store/tx [(contacts-store/save-contact-tx - contact-props)]})))))) - -(def receive-contact-request handle-contact-update) -(def receive-contact-request-confirmation handle-contact-update) -(def receive-contact-update handle-contact-update) diff --git a/src/status_im/models/mailserver.cljs b/src/status_im/models/mailserver.cljs deleted file mode 100644 index 08266ecd9f6..00000000000 --- a/src/status_im/models/mailserver.cljs +++ /dev/null @@ -1,153 +0,0 @@ -(ns status-im.models.mailserver - (:require - [clojure.string :as string] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.ethereum.core :as ethereum] - [status-im.models.network :as models.network] - [status-im.data-store.mailservers :as data-store.mailservers])) - -(def enode-address-regex #"enode://[a-zA-Z0-9]+\@\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b:(\d{1,5})") -(def enode-url-regex #"enode://[a-zA-Z0-9]+:(.+)\@\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b:(\d{1,5})") - -(defn- extract-address-components [address] - (rest (re-matches #"enode://(.*)@(.*)" address))) - -(defn- extract-url-components [address] - (rest (re-matches #"enode://(.*?):(.*)@(.*)" address))) - -(defn valid-enode-url? [address] - (re-matches enode-url-regex address)) - -(defn valid-enode-address? [address] - (re-matches enode-address-regex address)) - -(defn build-url [address password] - (let [[initial host] (extract-address-components address)] - (str "enode://" initial ":" password "@" host))) - -(defn set-input [input-key value {:keys [db]}] - {:db (update - db - :mailservers/manage - assoc - input-key - {:value value - :error (case input-key - :id false - :name (string/blank? value) - :url (not (valid-enode-url? value)))})}) - -(defn- address->mailserver [address] - (let [[enode password url :as response] (extract-url-components address)] - (cond-> {:address (if (seq response) - (str "enode://" enode "@" url) - address) - :user-defined true} - password (assoc :password password)))) - -(defn- build [id mailserver-name address] - (assoc (address->mailserver address) - :id (string/replace id "-" "") - :name mailserver-name)) - -(defn connected? [id {:keys [db]}] - (= (:inbox/current-id db) id)) - -(defn fetch [id {:keys [db] :as cofx}] - (get-in db [:inbox/wnodes (models.network/get-chain cofx) id])) - -(defn fetch-current [{:keys [db] :as cofx}] - (fetch (:inbox/current-id db) cofx)) - -(defn preferred-mailserver-id [{:keys [db] :as cofx}] - (let [chain (models.network/get-chain cofx)] - (get-in db [:account/account :settings :wnode chain]))) - -(defn- round-robin - "Find the choice and pick the next one, default to first if not found" - [choices current-id] - (let [next-index (reduce - (fn [index choice] - (if (= current-id choice) - (reduced (inc index)) - (inc index))) - 0 - choices)] - (nth choices - (mod - next-index - (count choices))))) - -(defn selected-or-random-id - "Use the preferred mailserver if set & exists, otherwise picks one randomly - if current-id is not set, else round-robin" - [{:keys [db] :as cofx}] - (let [chain (models.network/get-chain cofx) - current-id (:inbox/current-id db) - preference (preferred-mailserver-id cofx) - choices (-> db :inbox/wnodes chain keys)] - (if (and preference - (fetch preference cofx)) - preference - (if current-id - (round-robin choices current-id) - (rand-nth choices))))) - -(def default? (comp not :user-defined fetch)) - -(defn delete [id {:keys [db] :as cofx}] - (when-not (or - (default? id cofx) - (connected? id cofx)) - {:db (update-in db [:inbox/wnodes (models.network/get-chain cofx)] dissoc id) - :data-store/tx [(data-store.mailservers/delete-tx id)]})) - -(defn set-current-mailserver [{:keys [db] :as cofx}] - {:db (assoc db :inbox/current-id (selected-or-random-id cofx))}) - -(defn set-initial-last-request [{:keys [db now] :as cofx}] - {:db (update-in db [:account/account :last-request] (fnil identity (quot now 1000)))}) - -(defn add-custom-mailservers [mailservers {:keys [db]}] - {:db (reduce (fn [db {:keys [id chain] :as mailserver}] - (assoc-in db [:inbox/wnodes (keyword chain) id] - (-> mailserver - (dissoc :chain) - (assoc :user-defined true)))) - db - mailservers)}) - -(defn edit [id {:keys [db] :as cofx}] - (let [{:keys [id - address - password - name]} (fetch id cofx) - url (when address (build-url address password)) - fxs (handlers-macro/merge-fx - cofx - (set-input :id id) - (set-input :url (str url)) - (set-input :name (str name)))] - (assoc fxs :dispatch [:navigate-to :edit-mailserver]))) - -(defn upsert [{{:mailservers/keys [manage] :account/keys [account] :as db} :db :as cofx}] - (let [{:keys [name url id]} manage - network (get (:networks (:account/account db)) (:network db)) - chain (ethereum/network->chain-keyword network) - mailserver (build - (or (:value id) - (string/replace (:random-id cofx) "-" "")) - (:value name) - (:value url)) - current (connected? (:id mailserver) cofx)] - {:db (-> db - (dissoc :mailservers/manage) - (assoc-in [:inbox/wnodes chain (:id mailserver)] mailserver)) - :data-store/tx [{:transaction - (data-store.mailservers/save-tx (assoc - mailserver - :chain - chain)) - ;; we naively logout if the user is connected to the edited mailserver - :success-event (when current [:logout])}] - :dispatch [:navigate-back]})) diff --git a/src/status_im/models/network.cljs b/src/status_im/models/network.cljs deleted file mode 100644 index 0a3045c1774..00000000000 --- a/src/status_im/models/network.cljs +++ /dev/null @@ -1,71 +0,0 @@ -(ns status-im.models.network - (:require [clojure.string :as string] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.ui.screens.accounts.utils :as accounts.utils])) - -(def url-regex - #"https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}(\.[a-z]{2,6})?\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)") - -(defn valid-rpc-url? [url] - (boolean (re-matches url-regex (str url)))) - -(def default-manage - {:name {:value ""} - :url {:value ""} - :chain {:value :mainnet}}) - -(defn validate-string [{:keys [value]}] - {:value value - :error (string/blank? value)}) - -(defn validate-url [{:keys [value]}] - {:value value - :error (not (valid-rpc-url? value))}) - -(defn validate-manage [manage] - (-> manage - (update :url validate-url) - (update :name validate-string) - (update :chain validate-string))) - -(defn valid-manage? [manage] - (->> (validate-manage manage) - vals - (map :error) - (not-any? identity))) - -(defn- new-network [{:keys [random-id]} network-name upstream-url type network-id] - (let [data-dir (str "/ethereum/" (name type) "_rpc") - config {:NetworkId (or (when network-id (int network-id)) - (ethereum/chain-keyword->chain-id type)) - :DataDir data-dir - :UpstreamConfig {:Enabled true - :URL upstream-url}}] - {:id (string/replace random-id "-" "") - :name network-name - :config config})) - -(defn get-chain [{:keys [db]}] - (let [network (get (:networks (:account/account db)) (:network db))] - (ethereum/network->chain-keyword network))) - -(defn set-input [input-key value {:keys [db]}] - {:db (-> db - (update-in [:networks/manage input-key] assoc :value value) - (update-in [:networks/manage] validate-manage))}) - -(defn save [{{:networks/keys [manage] :account/keys [account] :as db} :db :as cofx}] - (when (valid-manage? manage) - (let [{:keys [name url chain network-id]} manage - network (new-network cofx (:value name) (:value url) (:value chain) (:value network-id)) - new-networks (merge {(:id network) network} (:networks account))] - (handlers-macro/merge-fx cofx - {:db (dissoc db :networks/manage) - :dispatch [:navigate-back]} - (accounts.utils/account-update {:networks new-networks}))))) - -;; No edit functionality actually implemented -(defn edit [{db :db}] - {:db (assoc db :networks/manage (validate-manage default-manage)) - :dispatch [:navigate-to :edit-network]}) diff --git a/src/status_im/models/wallet.cljs b/src/status_im/models/wallet.cljs deleted file mode 100644 index 34724c7ebc4..00000000000 --- a/src/status_im/models/wallet.cljs +++ /dev/null @@ -1,53 +0,0 @@ -(ns status-im.models.wallet - (:require [status-im.utils.money :as money])) - -(def min-gas-price-wei (money/bignumber 1)) - -(defmulti invalid-send-parameter? (fn [type _] type)) - -(defmethod invalid-send-parameter? :gas-price [_ value] - (cond - (not value) :invalid-number - (< (money/->wei :gwei value) min-gas-price-wei) :not-enough-wei)) - -(defmethod invalid-send-parameter? :default [_ value] - (when (or (not value) - (<= value 0)) - :invalid-number)) - -(defn- calculate-max-fee - [gas gas-price] - (if (and gas gas-price) - (money/to-fixed (money/wei->ether (.times gas gas-price))) - "0")) - -(defn- edit-max-fee [edit] - (let [gas (get-in edit [:gas-price :value-number]) - gas-price (get-in edit [:gas :value-number])] - (assoc edit :max-fee (calculate-max-fee gas gas-price)))) - -(defn add-max-fee [{:keys [gas gas-price] :as transaction}] - (assoc transaction :max-fee (calculate-max-fee gas gas-price))) - -(defn build-edit [edit-value key value] - "Takes the previous edit, either :gas or :gas-price and a value as string. - Wei for gas, and gwei for gas price. - Validates them and sets max fee" - (let [bn-value (money/bignumber value) - invalid? (invalid-send-parameter? key bn-value) - data (if invalid? - {:value value - :max-fee 0 - :invalid? invalid?} - {:value value - :value-number (if (= :gas-price key) - (money/->wei :gwei bn-value) - bn-value) - :invalid? false})] - (-> edit-value - (assoc key data) - edit-max-fee))) - -(defn edit-value - [key value {:keys [db]}] - {:db (update-in db [:wallet :edit] build-edit key value)}) diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs deleted file mode 100644 index 3f4f5cc7690..00000000000 --- a/src/status_im/native_module/core.cljs +++ /dev/null @@ -1,67 +0,0 @@ -(ns status-im.native-module.core - (:require [status-im.native-module.impl.module :as native-module])) - -(def adjust-resize 16) - -(defn move-to-internal-storage [callback] - (native-module/move-to-internal-storage callback)) - -(defn start-node [config] - (native-module/start-node config)) - -(defn stop-node [] - (native-module/stop-node)) - -(defn create-account [password callback] - (native-module/create-account password callback)) - -(defn recover-account [passphrase password callback] - (native-module/recover-account passphrase password callback)) - -(defn login [address password callback] - (native-module/login address password callback)) - -(defn approve-sign-request [id password callback] - (native-module/approve-sign-request id password callback)) - -(defn approve-sign-request-with-args [id password gas gas-price callback] - (native-module/approve-sign-request-with-args id password gas gas-price callback)) - -(defn discard-sign-request [id] - (native-module/discard-sign-request id)) - -(defn set-soft-input-mode [mode] - (native-module/set-soft-input-mode mode)) - -(defn clear-web-data [] - (native-module/clear-web-data)) - -(defn call-web3 [payload callback] - (native-module/call-web3 payload callback)) - -(defn call-web3-private [payload callback] - (native-module/call-web3-private payload callback)) - -(defn module-initialized! [] - (native-module/module-initialized!)) - -(defn should-move-to-internal-storage? [callback] - (native-module/should-move-to-internal-storage? callback)) - -(defn notify-users [m callback] - (native-module/notify-users m callback)) - -(defn add-peer [enode callback] - (native-module/add-peer enode callback)) - -(defn close-application [] - (native-module/close-application)) - -(defn connection-change [data] - (native-module/connection-change data)) - -(defn app-state-change [state] - (native-module/app-state-change state)) - -(defn get-device-UUID [callback] - (native-module/get-device-UUID callback)) diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs deleted file mode 100644 index 68b52f2b276..00000000000 --- a/src/status_im/native_module/impl/module.cljs +++ /dev/null @@ -1,156 +0,0 @@ -(ns status-im.native-module.impl.module - (:require-macros - [cljs.core.async.macros :refer [go-loop go]]) - (:require [status-im.ui.components.react :as r] - [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [cljs.core.async :as async] - [status-im.utils.platform :as p] - [status-im.utils.async :as async-util] - [status-im.react-native.js-dependencies :as rn-dependencies] - [clojure.string :as string])) - -;; if StatusModule is not initialized better to store -;; calls and make them only when StatusModule is ready -;; this flag helps to handle this -(defonce module-initialized? (atom (or p/ios? js/goog.DEBUG p/desktop?))) - -;; array of calls to StatusModule -(defonce calls (atom [])) - -(defn module-initialized! [] - (reset! module-initialized? true)) - -(defn store-call [args] - (log/debug :store-call args) - (swap! calls conj args)) - -(defn call-module [f] - ;;(log/debug :call-module f) - (if @module-initialized? - (f) - (store-call f))) - -(defonce loop-started (atom false)) - -(when-not @loop-started - (go-loop [_ nil] - (reset! loop-started true) - (if (and (seq @calls) @module-initialized?) - (do (swap! calls (fn [calls] - (doseq [call calls] - (call)))) - (reset! loop-started false)) - (recur (async/ (count @state/modals) 1) + (reset! state/modals (vec (butlast @state/modals))) + (reset! state/modals [])) + + (let [dissmissing @state/dissmissing] + (reset! state/dissmissing false) + (when (vector? dissmissing) + (effects/open-modal dissmissing))))) + + ;;;; Toast + + (navigation/register-component + "toasts" + ; `:flex 0` is the same as `flex: 0 0 auto` in CSS. + ; We need this to override the HOC default layout which is + ; flex 1. If we don't override this property, this HOC + ; will catch all touches/gestures while the toast is shown, + ; preventing the user doing any action in the app + #(gesture/gesture-handler-root-hoc views/toasts + #js {:flex 0}) + (fn [] views/toasts)) + + ;;;; Bottom sheet + + (navigation/register-component + "bottom-sheet" + (fn [] (gesture/gesture-handler-root-hoc views/bottom-sheet)) + (fn [] views/bottom-sheet)) + + ;;;; Alert Banner + + (navigation/register-component + "alert-banner" + (fn [] (gesture/gesture-handler-root-hoc views/alert-banner #js {:flex 0})) + (fn [] views/alert-banner)) + + ;;;; NFC sheet + + (navigation/register-component + "nfc-sheet" + (fn [] (gesture/gesture-handler-root-hoc views/nfc-sheet-comp)) + (fn [] views/nfc-sheet-comp))) + +;;;; LEGACY (should be removed in status 2.0) + +(navigation/register-component + "popover" + (fn [] (gesture/gesture-handler-root-hoc views/popover-comp)) + (fn [] views/popover-comp)) diff --git a/src/status_im/navigation/effects.cljs b/src/status_im/navigation/effects.cljs new file mode 100644 index 00000000000..2f6ff99fc26 --- /dev/null +++ b/src/status_im/navigation/effects.cljs @@ -0,0 +1,256 @@ +(ns status-im.navigation.effects + (:require + [oops.core :as oops] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.navigation :as navigation] + [react-native.platform :as platform] + [react-native.share :as share] + [status-im.contexts.shell.constants :as shell.constants] + [status-im.navigation.options :as options] + [status-im.navigation.roots :as roots] + [status-im.navigation.state :as state] + [status-im.navigation.view :as views] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn get-status-nav-color + [view-id theme] + (let [theme (or (get-in views/screens [view-id :options :theme]) + theme) + status-bar-theme (if (or (= theme :dark) + @state/alert-banner-shown?) + :light + :dark) + home-stack? (shell.constants/stacks-ids view-id) + ;; Home screen nav bar always dark due to bottom tabs + nav-bar-color (if (or home-stack? + (= view-id :screen/shell-stack) + (= theme :dark)) + colors/neutral-100 + colors/white) + comp-id (if (shell.constants/stacks-ids view-id) + :screen/shell-stack + view-id)] + [status-bar-theme nav-bar-color comp-id])) + +(defn reload-status-nav-color-fx + [[view-id theme]] + (when (and (= @state/root-id :screen/shell-stack) view-id) + (let [[status-bar-theme nav-bar-color comp-id] (get-status-nav-color view-id theme)] + (when platform/android? + (navigation/merge-options + (name comp-id) + {:statusBar {:style status-bar-theme} + :navigationBar {:backgroundColor nav-bar-color}}))))) + +(rf/reg-fx :reload-status-nav-color-fx reload-status-nav-color-fx) + +(rf/reg-fx :set-view-id-fx + (fn [[view-id theme]] + (reload-status-nav-color-fx [view-id theme]) + (rf/dispatch [:screens/on-will-focus view-id]) + (when-let [{:keys [on-focus]} (get views/screens view-id)] + (when on-focus + (rf/dispatch on-focus))))) + +(defn- dismiss-all-modals + [] + (when (seq @state/modals) + (reset! state/dissmissing true) + (doseq [modal @state/modals] + (navigation/dismiss-modal (name modal))) + (state/navigation-pop-from (first @state/modals)) + (reset! state/modals []))) + +;;;; Root + +(rf/reg-fx :set-root + (fn [[root-id theme]] + (let [[status-bar-theme] (get-status-nav-color root-id theme) + root (get (roots/roots status-bar-theme) root-id)] + (dismiss-all-modals) + (reset! state/root-id (or (get-in root [:root :stack :id]) root-id)) + (navigation/set-root root) + (state/navigation-state-reset [{:id root-id + :type :root}])))) + +;;;; Navigate to + +(defn- navigate + [[component theme animations]] + (let [{:keys [options]} (get views/screens component) + options (if (map? animations) (assoc options :animations animations) options)] + (dismiss-all-modals) + (navigation/push + (name @state/root-id) + {:component {:id component + :name component + :options (merge (options/root-options {:theme (or (:theme options) theme)}) + options)}}) + (state/navigation-state-push {:id component + :type :stack + :parent @state/root-id}))) + +(rf/reg-fx :navigate-to navigate) + +;;;; Navigate to within stack + +(defn- navigate-to-within-stack + [[component comp-id theme]] + (let [{:keys [options]} (get views/screens component)] + (navigation/push + (name comp-id) + {:component {:id component + :name component + :options (merge + (options/root-options {:theme theme}) + options)}}) + (state/navigation-state-push {:id component + :type :stack + :parent comp-id}))) + +(rf/reg-fx :navigate-to-within-stack navigate-to-within-stack) + +(defn dismiss-modal + ([] (dismiss-modal nil)) + ([comp-id] + (reset! state/dissmissing true) + (-> (navigation/dismiss-modal (name (or comp-id (last @state/modals)))) + (oops/ocall! :then #(rf/dispatch-sync [:reload-status-nav-color]))) + (state/navigation-pop-from comp-id))) + +(defn navigate-back + [] + (when-let [{:keys [type parent id]} (last (state/get-navigation-state))] + (cond + (and (= type :modal) id) + (dismiss-modal id) + (and (= type :stack) parent) + (do + (navigation/pop (name parent)) + (state/navigation-state-pop))))) + +(rf/reg-fx :navigate-back navigate-back) + +(rf/reg-fx :navigate-back-to + (fn [comp-id] + (navigation/pop-to (name comp-id)) + (state/navigation-pop-after comp-id))) + +(rf/reg-fx :dismiss-modal + (fn [comp-id] + (dismiss-modal comp-id))) + +(defn- pop-to-root + [root-id] + (dismiss-all-modals) + (navigation/pop-to-root root-id) + (state/navigation-pop-after root-id)) + +(rf/reg-fx :pop-to-root-fx pop-to-root) + +;;;; Modal + +(defn open-modal + [[component theme]] + (let [{:keys [options]} (get views/screens component) + sheet? (:sheet? options)] + (if @state/dissmissing + (reset! state/dissmissing [component theme]) + (do + (swap! state/modals conj component) + (navigation/show-modal + {:stack {:children [{:component + {:name component + :id component + :options (merge (options/root-options {:theme (or (:theme options) + theme)}) + options + (when sheet? + options/sheet-options))}}]}}))) + (state/navigation-state-push {:id component + :type :modal}))) + +(rf/reg-fx :open-modal-fx open-modal) + +;;;; Share + +(rf/reg-fx :effects.share/open + (fn [{:keys [options on-success on-error]}] + (cond-> (share/open options) + (fn? on-success) (.then on-success) + :always (.catch (fn [error] + (log/error "Failed to share content" + {:error error + :effect :effects.share/open}) + (when (fn? on-error) + (on-error error))))))) + +;;;; Overlay + +(defn show-overlay + ([component] (show-overlay component {})) + ([component opts] + (navigation/dissmiss-overlay component) + (navigation/show-overlay + {:component {:name component + :id component + :options (merge (options/statusbar-and-navbar-options (:theme opts) nil nil) + {:layout {:componentBackgroundColor :transparent + :orientation ["portrait"]} + :overlay {:interceptTouchOutside true + :handleKeyboardEvents true}} + opts)}}))) + +(rf/reg-fx :show-toasts + (fn [[view-id theme]] + (let [[status-bar-theme nav-bar-color] (get-status-nav-color view-id theme)] + (show-overlay "toasts" + (assoc (options/statusbar-and-navbar-options nil status-bar-theme nav-bar-color) + :overlay + {:interceptTouchOutside false}))))) + +(rf/reg-fx :hide-toasts + (fn [] (navigation/dissmiss-overlay "toasts"))) + +;;;; Bottom sheet + +(rf/reg-fx :show-bottom-sheet + (fn [options] + (show-overlay "bottom-sheet" options))) + +(rf/reg-fx :hide-bottom-sheet + (fn [] (navigation/dissmiss-overlay "bottom-sheet"))) + +;;;; Alert Banner + +(rf/reg-fx :show-alert-banner + (fn [[view-id theme]] + (show-overlay "alert-banner" + (assoc (options/statusbar-and-navbar-options nil :light nil) + :overlay + {:interceptTouchOutside false})) + (reset! state/alert-banner-shown? true) + (reload-status-nav-color-fx [view-id theme]))) + +(rf/reg-fx :hide-alert-banner + (fn [[view-id theme]] + (navigation/dissmiss-overlay "alert-banner") + (reset! state/alert-banner-shown? false) + (reload-status-nav-color-fx [view-id theme]))) + +;;;; NFC sheet + +(rf/reg-fx :show-nfc-sheet + (fn [] (show-overlay "nfc-sheet"))) + +(rf/reg-fx :hide-nfc-sheet + (fn [] (navigation/dissmiss-overlay "nfc-sheet"))) + +;;;; Legacy (should be removed in status 2.0) +(rf/reg-fx :show-popover + (fn [] (show-overlay "popover"))) + +(rf/reg-fx :hide-popover + (fn [] (navigation/dissmiss-overlay "popover"))) diff --git a/src/status_im/navigation/events.cljs b/src/status_im/navigation/events.cljs new file mode 100644 index 00000000000..721338b842c --- /dev/null +++ b/src/status_im/navigation/events.cljs @@ -0,0 +1,153 @@ +(ns status-im.navigation.events + (:require + [clojure.string :as string] + [re-frame.core :as re-frame] + [react-native.core :as react] + [react-native.platform :as platform] + [utils.re-frame :as rf] + [utils.url :as url])) + +(defn- all-screens-params + [db view screen-params] + (cond-> db + (and (seq screen-params) (:screen screen-params) (:params screen-params)) + (all-screens-params (:screen screen-params) (:params screen-params)) + + (seq screen-params) + (assoc-in [:navigation/screen-params view] screen-params))) + +(rf/defn navigate-to + {:events [:navigate-to]} + [{:keys [db] :as cofx} go-to-view-id screen-params] + (rf/merge + cofx + {:db (-> db + (assoc :view-id go-to-view-id) + (all-screens-params go-to-view-id screen-params)) + :dispatch-n [[:hide-bottom-sheet]] + :navigate-to [go-to-view-id (:theme db) (:animation screen-params)]})) + +(rf/defn navigate-to-within-stack + {:events [:navigate-to-within-stack]} + [{:keys [db]} comp-id screen-params] + {:db (all-screens-params db (first comp-id) screen-params) + :fx [[:dispatch [:hide-bottom-sheet]] + [:navigate-to-within-stack (conj comp-id (:theme db))]]}) + +(re-frame/reg-event-fx :open-modal + (fn [{:keys [db]} [component screen-params]] + {:db (-> db + (assoc :view-id component) + (all-screens-params component screen-params)) + :fx [[:dispatch [:hide-bottom-sheet]] + [:dispatch [:dismiss-keyboard]] + [:open-modal-fx [component (:theme db)]]]})) + +(rf/defn dismiss-modal + {:events [:dismiss-modal]} + [_ comp-id] + {:dismiss-modal comp-id}) + +(rf/defn navigate-back + {:events [:navigate-back]} + [{:keys [db]} screen-id] + {:fx [[:navigate-back nil] + (when (or + ;; Closing chat using UI buttons in chat screen (screen-id is known) + (= screen-id :screen/chat) + ;; Support chat closing using android hardware back button + (and platform/android? (= (:view-id db) :screen/chat))) + [:dispatch [:chat/close]])]}) + +(rf/defn navigate-back-to + {:events [:navigate-back-to]} + [_ comp-id] + {:navigate-back-to comp-id}) + +(rf/defn pop-to-root + {:events [:pop-to-root]} + [{:keys [db]} tab] + (cond-> + {:pop-to-root-fx tab + :db (assoc db :view-id (:shell/selected-stack-id db))} + + (:current-chat-id db) + (assoc :dispatch [:chat/close (:current-chat-id db)]))) + +(rf/defn init-root + "WARNING: Use `:update-theme-and-init-root` instead. `:init-root` should not be used directly." + {:events [:init-root]} + [{:keys [db]} root-id screen-params] + {:db (all-screens-params db root-id screen-params) + :set-root [root-id (:theme db)]}) + +(rf/defn update-theme-and-init-root + {:events [:update-theme-and-init-root]} + [_ root-id screen-params] + {:fx [[:dispatch [:theme/switch {:view-id root-id}]] + [:dispatch [:init-root root-id screen-params]]]}) + +(rf/defn hide-bottom-sheet + {:events [:hide-bottom-sheet]} + [{:keys [db]}] + (let [{:keys [hide? sheets]} (:bottom-sheet db)] + (when (and (not hide?) (seq sheets)) + {:db (assoc-in db [:bottom-sheet :hide?] true)}))) + +(rf/defn bottom-sheet-hidden + {:events [:bottom-sheet-hidden]} + [{:keys [db]}] + (let [{:keys [sheets]} (:bottom-sheet db) + rest-sheets (butlast sheets)] + (merge + {:db (assoc db :bottom-sheet {:sheets rest-sheets :hide? false}) + :hide-bottom-sheet nil + :reload-status-nav-color-fx [(:view-id db) (:theme db)]} + (when (seq rest-sheets) + {:dispatch [:show-next-bottom-sheet]})))) + +(rf/defn show-next-bottom-sheet + {:events [:show-next-bottom-sheet]} + [{:keys [db]}] + {:show-bottom-sheet {:theme (:theme db)}}) + +(rf/defn show-bottom-sheet + {:events [:show-bottom-sheet]} + [{:keys [db] :as cofx} content] + (let [theme (or (:theme content) (:theme db)) + {:keys [sheets hide?]} (:bottom-sheet db)] + (rf/merge cofx + {:db (update-in db [:bottom-sheet :sheets] conj content) + :dismiss-keyboard nil} + (fn [new-cofx] + (when-not hide? + (if (seq sheets) + (hide-bottom-sheet new-cofx) + {:show-bottom-sheet {:theme theme}})))))) + +(rf/defn set-view-id + {:events [:set-view-id]} + [{:keys [db]} view-id] + (let [view-id (if (= view-id :screen/shell-stack) (:shell/selected-stack-id db) view-id)] + {:db (assoc db :view-id view-id) + :set-view-id-fx [view-id (:theme db)]})) + +(rf/reg-event-fx :reload-status-nav-color + (fn [{:keys [db]} [view-id]] + {:fx [[:reload-status-nav-color-fx [(or view-id (:view-id db)) (:theme db)]]]})) + +(defn open-share + [_ [config]] + {:fx [[:effects.share/open config]]}) + +(rf/reg-event-fx :open-share open-share) + +(rf/reg-event-fx :open-url + (fn [_ [url]] + {:fx [[:effects/open-url url]]})) + +(rf/reg-fx + :effects/open-url + (fn [url] + (when (not (string/blank? url)) + (.openURL ^js react/linking (url/normalize-url url))))) diff --git a/src/status_im/navigation/options.cljs b/src/status_im/navigation/options.cljs new file mode 100644 index 00000000000..229548975d2 --- /dev/null +++ b/src/status_im/navigation/options.cljs @@ -0,0 +1,110 @@ +(ns status-im.navigation.options + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform] + [status-im.navigation.transitions :as transitions])) + +(defn default-options + [] + {:layout {:orientation ["portrait"]} + :topBar {:visible false}}) + +(defn statusbar-and-navbar-options + [theme status-bar-theme nav-bar-color] + (let [[status-bar-theme nav-bar-color] + (if (= :dark theme) + [(or status-bar-theme :light) (or nav-bar-color colors/neutral-100)] + [(or status-bar-theme :dark) (or nav-bar-color colors/white)])] + (if platform/android? + {:navigationBar {:backgroundColor nav-bar-color} + :statusBar {:translucent true + :backgroundColor :transparent + :style (or status-bar-theme :light) + :drawBehind true}} + {:statusBar {:style (or status-bar-theme :light)}}))) + +(defn root-options + [{:keys [background-color theme status-bar-theme nav-bar-color]}] + (let [layout-background (or background-color + (colors/theme-colors colors/white colors/neutral-100 theme)) + component-background (or background-color + (colors/theme-colors colors/white colors/neutral-100 theme))] + (assoc (statusbar-and-navbar-options theme status-bar-theme nav-bar-color) + :topBar {:visible false} + :layout {:componentBackgroundColor component-background + :orientation ["portrait"] + :backgroundColor layout-background}))) + +(defn dark-root-options + [] + (root-options + {:background-color colors/neutral-100 + :theme :dark + :status-bar-theme :light + :nav-bar-color colors/neutral-100})) + +;;;; Screen Specific Options +(def onboarding-layout + {:componentBackgroundColor colors/neutral-80-opa-80-blur + :orientation ["portrait"] + :backgroundColor colors/neutral-80-opa-80-blur}) + +(def onboarding-transparent-layout + {:componentBackgroundColor :transparent + :orientation ["portrait"] + :backgroundColor :transparent}) + +(def transparent-screen-options + {:modalPresentationStyle :overCurrentContext + :theme :dark + :layout {:componentBackgroundColor :transparent + :orientation ["portrait"] + :backgroundColor :transparent}}) + +(def transparent-modal-screen-options + (merge + transparent-screen-options + {:animations (merge + transitions/new-to-status-modal-animations + transitions/push-animations-for-transparent-background)})) + +(def sheet-options + {:layout {:componentBackgroundColor :transparent + :orientation ["portrait"] + :backgroundColor :transparent} + :modalPresentationStyle :overCurrentContext + ;; disabled on iOS in debug mode: + ;; https://github.com/status-im/status-mobile/pull/16053#issuecomment-1568349702 + :animations (if (or platform/android? (not js/goog.DEBUG)) + {:showModal {:alpha {:from 1 :to 1 :duration 300}} + :dismissModal {:alpha {:from 1 :to 1 :duration 300}}} + {})}) + +(def dark-screen + {:theme :dark + :modalPresentationStyle :overCurrentContext + :layout {:componentBackgroundColor colors/neutral-95 + :orientation ["portrait"] + :backgroundColor colors/neutral-95}}) + +(def lightbox + {:topBar {:visible false} + :statusBar {:backgroundColor :transparent + :style :light + :animate true + :drawBehind true + :translucent true} + :navigationBar {:backgroundColor colors/neutral-100} + :layout {:componentBackgroundColor :transparent + :backgroundColor :transparent + ;; issue: https://github.com/wix/react-native-navigation/issues/7726 + :orientation (if platform/ios? ["portrait" "landscape"] ["portrait"])} + :animations {:push {:sharedElementTransitions [{:fromId :shared-element + :toId :shared-element + :interpolation {:type :decelerate + :factor 1.5}}]} + :pop {:sharedElementTransitions [{:fromId :shared-element + :toId :shared-element + :interpolation {:type + :decelerate + :factor 1.5}}]}}}) diff --git a/src/status_im/navigation/roots.cljs b/src/status_im/navigation/roots.cljs new file mode 100644 index 00000000000..ee33f8e6ce4 --- /dev/null +++ b/src/status_im/navigation/roots.cljs @@ -0,0 +1,50 @@ +(ns status-im.navigation.roots + (:require + [quo.foundations.colors :as colors] + [status-im.navigation.options :as options])) + +(defn roots-internal + [status-bar-theme] + {:screen/onboarding.intro + {:root + {:stack {:id :screen/onboarding.intro + :children [{:component {:name :screen/onboarding.intro + :id :screen/onboarding.intro + :options (options/dark-root-options)}}]}}} + :screen/shell-stack + {:root + {:stack {:id :screen/shell-stack + :children [{:component {:name :screen/shell-stack + :id :screen/shell-stack + :options (options/root-options + {:nav-bar-color colors/neutral-100 + :status-bar-theme status-bar-theme})}}]}}} + :screen/profile.profiles + {:root + {:stack {:id :screen/profile.profiles + :children [{:component {:name :screen/profile.profiles + :id :screen/profile.profiles + :options (options/dark-root-options)}}]}}} + + :screen/onboarding.syncing-biometric + {:root {:stack {:id :screen/onboarding.enable-biometrics + :children [{:component {:name :screen/onboarding.enable-biometrics + :id :screen/onboarding.enable-biometrics + :options (options/dark-root-options)}}]}}}}) + +(defn old-roots + [status-bar-theme] + {:screen/progress + {:root {:stack {:children [{:component {:name :screen/progress + :id :screen/progress + :options (options/root-options {:status-bar-theme + status-bar-theme})}}] + :options (assoc (options/root-options nil) + :topBar + {:visible false})}}}}) + +(defn roots + [status-bar-theme] + (merge + (old-roots status-bar-theme) + (roots-internal status-bar-theme))) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs new file mode 100644 index 00000000000..976738bfa02 --- /dev/null +++ b/src/status_im/navigation/screens.cljs @@ -0,0 +1,1318 @@ +(ns status-im.navigation.screens + (:require + [legacy.status-im.ui.screens.screens :as old-screens] + [quo.foundations.colors :as colors] + [status-im.common.emoji-picker.view :as emoji-picker] + [status-im.common.enter-seed-phrase.view :as enter-seed-phrase] + [status-im.common.lightbox.view :as lightbox] + [status-im.common.pdf-viewer.view :as pdf-viewer] + [status-im.common.privacy.view :as privacy-policy] + [status-im.common.terms.view :as terms-of-use] + [status-im.config :as config] + [status-im.contexts.chat.group.create.view :as group.create] + [status-im.contexts.chat.group.details.view :as group.details] + [status-im.contexts.chat.group.update.view :as group.update] + [status-im.contexts.chat.home.add-new-contact.scan.scan-profile-qr-page :as scan-profile-qr-page] + [status-im.contexts.chat.home.add-new-contact.views :as add-new-contact] + [status-im.contexts.chat.home.new-chat.view :as new-chat] + [status-im.contexts.chat.messenger.camera.view :as camera-screen] + [status-im.contexts.chat.messenger.messages.view :as chat] + [status-im.contexts.chat.messenger.photo-selector.view :as photo-selector] + [status-im.contexts.communities.actions.accounts-selection.view :as communities.accounts-selection] + [status-im.contexts.communities.actions.addresses-for-permissions.view :as + addresses-for-permissions] + [status-im.contexts.communities.actions.airdrop-addresses.view :as airdrop-addresses] + [status-im.contexts.communities.actions.channel-view-details.view :as + channel-view-channel-members-and-details] + [status-im.contexts.communities.actions.invite-contacts.view :as communities.invite] + [status-im.contexts.communities.actions.share-community-channel.view :as share-community-channel] + [status-im.contexts.communities.actions.share-community.view :as share-community] + [status-im.contexts.communities.discover.view :as communities.discover] + [status-im.contexts.communities.overview.view :as communities.overview] + [status-im.contexts.keycard.authorise.view :as keycard.authorise] + [status-im.contexts.keycard.backup.view :as keycard.backup] + [status-im.contexts.keycard.change-pin.view :as keycard.change-pin] + [status-im.contexts.keycard.check.view :as keycard.check] + [status-im.contexts.keycard.create.view :as keycard.create] + [status-im.contexts.keycard.different-card.view :as keycard.different-card] + [status-im.contexts.keycard.error.view :as keycard.error] + [status-im.contexts.keycard.factory-reset.view :as keycard.factory-reset] + [status-im.contexts.keycard.login.view :as keycard.login] + [status-im.contexts.keycard.manage.profile-keys.view :as keycard.manage.profile-keys] + [status-im.contexts.keycard.manage.view :as keycard.manage] + [status-im.contexts.keycard.migrate.fail.view :as keycard.migrate.fail] + [status-im.contexts.keycard.migrate.profile-keys.view :as keycard.migrate.profile-keys] + [status-im.contexts.keycard.migrate.re-encrypting.view :as keycard.re-encrypting] + [status-im.contexts.keycard.migrate.success.view :as keycard.migrate.success] + [status-im.contexts.keycard.migrate.view :as keycard.migrate] + [status-im.contexts.keycard.not-keycard.view :as keycard.not-keycard] + [status-im.contexts.keycard.pin.create.view :as keycard.pin.create] + [status-im.contexts.keycard.pin.enter.view :as keycard.pin.enter] + [status-im.contexts.keycard.unblock.view :as keycard.unblock] + [status-im.contexts.market.token.view :as market.token] + [status-im.contexts.onboarding.create-password.view :as create-password] + [status-im.contexts.onboarding.create-profile.view :as create-profile] + [status-im.contexts.onboarding.enable-biometrics.view :as enable-biometrics] + [status-im.contexts.onboarding.enable-notifications.view :as enable-notifications] + [status-im.contexts.onboarding.intro.view :as intro] + [status-im.contexts.onboarding.log-in.view :as log-in] + [status-im.contexts.onboarding.preparing-status.view :as preparing-status] + [status-im.contexts.onboarding.share-usage.view :as onboarding.share-usage] + [status-im.contexts.onboarding.sign-in.view :as sign-in] + [status-im.contexts.onboarding.syncing.progress.view :as syncing-devices] + [status-im.contexts.preview.feature-flags.view :as feature-flags] + [status-im.contexts.preview.quo.component-preview.view :as component-preview] + [status-im.contexts.preview.quo.main :as quo.preview] + [status-im.contexts.preview.status-im.main :as status-im-preview] + [status-im.contexts.profile.backup-recovery-phrase.view :as backup-recovery-phrase] + [status-im.contexts.profile.contact.share.view :as share-contact] + [status-im.contexts.profile.contact.view :as contact-profile] + [status-im.contexts.profile.edit.accent-colour.view :as edit-accent-colour] + [status-im.contexts.profile.edit.bio.view :as edit-bio] + [status-im.contexts.profile.edit.modal.view :as edit-profile-modal] + [status-im.contexts.profile.edit.name.view :as edit-name] + [status-im.contexts.profile.edit.view :as edit-profile] + [status-im.contexts.profile.profiles.view :as profiles] + [status-im.contexts.profile.settings.screens.messages.blocked-users.view :as + settings.blocked-users] + [status-im.contexts.profile.settings.screens.messages.view :as settings.messages] + [status-im.contexts.profile.settings.screens.notifications.view :as settings.notifications] + [status-im.contexts.profile.settings.screens.password.change-password.loading :as + change-password-loading] + [status-im.contexts.profile.settings.screens.password.change-password.view :as change-password] + [status-im.contexts.profile.settings.screens.password.view :as settings-password] + [status-im.contexts.profile.settings.screens.syncing.view :as settings.syncing] + [status-im.contexts.profile.settings.view :as settings] + [status-im.contexts.settings.about.view :as settings.about] + [status-im.contexts.settings.advanced.view :as settings.advanced] + [status-im.contexts.settings.keycard.view :as settings.keycard] + [status-im.contexts.settings.language-and-currency.currency.view :as settings.currency-selection] + [status-im.contexts.settings.language-and-currency.view :as settings.language-and-currency] + [status-im.contexts.settings.privacy-and-security.share-usage.view :as settings.share-usage] + [status-im.contexts.settings.privacy-and-security.view :as settings.privacy-and-security] + [status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.encrypted-qr.view + :as encrypted-keypair-qr] + [status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.import-private-key.view + :as missing-keypairs.import-private-key] + [status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.import-seed-phrase.view + :as missing-keypairs.import-seed-phrase] + [status-im.contexts.settings.wallet.keypairs-and-accounts.missing-keypairs.scan-qr.view + :as scan-keypair-qr] + [status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view :as keypair-rename] + [status-im.contexts.settings.wallet.keypairs-and-accounts.view :as keypairs-and-accounts] + [status-im.contexts.settings.wallet.network-settings.view :as network-settings] + [status-im.contexts.settings.wallet.saved-addresses.add-address-to-save.view :as + wallet-add-address-to-save] + [status-im.contexts.settings.wallet.saved-addresses.save-address.view :as wallet-save-address] + [status-im.contexts.settings.wallet.saved-addresses.share-address.view :as + share-saved-address] + [status-im.contexts.settings.wallet.saved-addresses.view :as saved-addresses-settings] + [status-im.contexts.settings.wallet.wallet-options.view :as wallet-options] + [status-im.contexts.shell.activity-center.view :as activity-center] + [status-im.contexts.shell.qr-reader.view :as shell-qr-reader] + [status-im.contexts.shell.share.view :as share] + [status-im.contexts.shell.view :as shell] + [status-im.contexts.syncing.find-sync-code.view :as find-sync-code] + [status-im.contexts.syncing.how-to-pair.view :as how-to-pair] + [status-im.contexts.syncing.scan-sync-code-page.view :as scan-sync-code-page] + [status-im.contexts.syncing.setup-syncing.view :as settings-setup-syncing] + [status-im.contexts.syncing.syncing-devices-list.view :as syncing-devices-list] + [status-im.contexts.wallet.account.edit-account.view :as wallet-edit-account] + [status-im.contexts.wallet.account.share-address.view :as wallet-share-address] + [status-im.contexts.wallet.account.view :as wallet-accounts] + [status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.view :as + wallet-confirm-address-to-watch] + [status-im.contexts.wallet.add-account.add-address-to-watch.view :as wallet-add-address-to-watch] + [status-im.contexts.wallet.add-account.create-account.edit-derivation-path.view :as + wallet-edit-derivation-path] + [status-im.contexts.wallet.add-account.create-account.import-private-key.view :as + wallet-import-private-key] + [status-im.contexts.wallet.add-account.create-account.key-pair-name.view :as + wallet-key-pair-name] + [status-im.contexts.wallet.add-account.create-account.new-keypair.confirm-backup.view :as + wallet-confirm-backup] + [status-im.contexts.wallet.add-account.create-account.select-keypair.view :as wallet-select-keypair] + [status-im.contexts.wallet.add-account.create-account.view :as wallet-create-account] + [status-im.contexts.wallet.bridge.bridge-to.view :as wallet-bridge-to] + [status-im.contexts.wallet.bridge.input-amount.view :as wallet-bridge-input-amount] + [status-im.contexts.wallet.bridge.select-asset.view :as wallet-bridge-select-asset] + [status-im.contexts.wallet.collectible.view :as wallet-collectible] + [status-im.contexts.wallet.common.scan-account.view :as wallet-scan-address] + [status-im.contexts.wallet.common.transaction-settings.gas-amount.view :as + wallet-tx-settings-gas-amount] + [status-im.contexts.wallet.common.transaction-settings.gas-price.view :as + wallet-tx-settings-gas-price] + [status-im.contexts.wallet.common.transaction-settings.max-fee.view :as wallet-tx-settings-max-fee] + [status-im.contexts.wallet.common.transaction-settings.nonce.view :as wallet-tx-settings-nonce] + [status-im.contexts.wallet.common.transaction-settings.priority-fee.view :as + wallet-tx-settings-priority-fee] + [status-im.contexts.wallet.connected-dapps.scan-dapp.view :as wallet-scan-dapp] + [status-im.contexts.wallet.connected-dapps.view :as wallet-connected-dapps] + [status-im.contexts.wallet.send.from.view :as wallet-select-from] + [status-im.contexts.wallet.send.select-address.view :as wallet-select-address] + [status-im.contexts.wallet.send.select-asset.view :as wallet-select-asset] + [status-im.contexts.wallet.send.select-collectible-amount.view :as wallet-select-collectible-amount] + [status-im.contexts.wallet.send.send-amount.view :as wallet-send-input-amount] + [status-im.contexts.wallet.send.transaction-confirmation.view :as wallet-transaction-confirmation] + [status-im.contexts.wallet.send.transaction-progress.view :as wallet-transaction-progress] + [status-im.contexts.wallet.swap.select-account.view :as wallet-swap-select-account] + [status-im.contexts.wallet.swap.select-asset-to-pay.view :as wallet-swap-select-asset-to-pay] + [status-im.contexts.wallet.swap.set-spending-cap.view :as wallet-swap-set-spending-cap] + [status-im.contexts.wallet.swap.setup-swap.view :as wallet-swap-setup-swap] + [status-im.contexts.wallet.swap.swap-confirmation.view :as wallet-swap-confirmation] + [status-im.contexts.wallet.wallet-connect.modals.send-transaction.view :as + wallet-connect-send-transaction] + [status-im.contexts.wallet.wallet-connect.modals.session-proposal.view :as + wallet-connect-session-proposal] + [status-im.contexts.wallet.wallet-connect.modals.sign-message.view :as wallet-connect-sign-message] + [status-im.contexts.wallet.wallet-connect.modals.sign-transaction.view :as + wallet-connect-sign-transaction] + [status-im.navigation.options :as options] + [status-im.navigation.transitions :as transitions] + [utils.collection])) + +(def chat-screens + [{:name :screen/start-a-new-chat + :metrics {:track? true + :alias-id :messenger.new-chat} + :options {:sheet? true} + :component new-chat/view} + + {:name :screen/chat + :metrics {:track? true + :alias-id :messenger.chat} + :options {:popGesture false + :animations transitions/stack-transition-from-bottom} + :component chat/chat} + + {:name :screen/group-create + :metrics {:track? true + :alias-id :messenger.new-group} + :options {:sheet? true + :skip-background? true} + :component group.create/view} + + {:name :screen/group-details + :metrics {:track? true + :alias-id :messenger.group-details} + :component group.details/view} + + {:name :screen/group-update + :metrics {:track? true + :alias-id :messenger.group-update} + :options {:sheet? true + :skip-background? true} + :component group.update/view} + + {:name :screen/group-add-manage-members + :metrics {:track? true + :flow :messenger + :alias-id :messenger.group-manage-members} + :options {:sheet? true} + :component group.details/add-manage-members}]) + +(def community-screens + [{:name :screen/discover-communities + :metrics {:track? true + :alias-id :community.discover} + :component communities.discover/view} + + {:name :screen/community-overview + :metrics {:track? true + :alias-id :community.overview} + :options {:animations transitions/stack-transition-from-bottom} + :component communities.overview/view} + + ;; Note: the sheet screen is used when selecting addresses to share when + ;; joining a community. The non-sheet screen is used when editing shared + ;; addresses after the join request was sent. + {:name :screen/community-account-selection-sheet + :metrics {:track? true + :alias-id :community.select-addresses-for-joining-community} + :options {:sheet? true} + :component communities.accounts-selection/view} + {:name :screen/community-account-selection + :metrics {:track? true + :alias-id :community.select-addresses-for-community} + :options {:insets {:top? true}} + :component communities.accounts-selection/view} + + {:name :screen/share-community + :metrics {:track? true + :alias-id :community.share-community} + :options options/transparent-screen-options + :component share-community/view} + + {:name :screen/invite-people-community + :metrics {:track? true + :alias-id :community.invite-people} + :options {:sheet? true} + :component communities.invite/view} + + {:name :screen/share-community-channel + :metrics {:track? true + :alias-id :community.share-channel} + :options options/transparent-screen-options + :component share-community-channel/view} + + {:name :screen/chat.view-channel-members-and-details + :metrics {:track? true + :alias-id :community.view-channel-members-and-details} + :options {:insets {:top? true}} + :component channel-view-channel-members-and-details/view} + + {:name :screen/addresses-for-permissions + :metrics {:track? true + :alias-id :community.choose-addresses-for-permissions} + :options {:insets {:top? true}} + :component addresses-for-permissions/view} + + {:name :screen/address-for-airdrop + :metrics {:track? true + :alias-id :community.choose-addresses-for-airdrop} + :options {:insets {:top? true}} + :component airdrop-addresses/view}]) + +(def contact-screens + [{:name :screen/new-contact + :metrics {:track? true + :alias-id :contact.new-contact} + :options {:sheet? true} + :component add-new-contact/new-contact} + + {:name :screen/scan-profile-qr-code + :metrics {:track? true + :alias-id :contact.scan-profile-qr-code} + :options options/dark-screen + :component scan-profile-qr-page/view} + + {:name :screen/contact-profile + :metrics {:track? true + :alias-id :contact.contact-profile} + :options {:modalPresentationStyle :overCurrentContext} + :component contact-profile/view} + + {:name :screen/share-contact + :metrics {:track? true + :alias-id :contact.share-profile} + :options options/transparent-screen-options + :component share-contact/view}]) + +(def device-syncing-screens + [{:name :screen/how-to-pair + :metrics {:track? true + :alias-id :syncing.how-to-pair} + :options (assoc options/dark-screen :sheet? true) + :component how-to-pair/view} + + {:name :screen/find-sync-code + :metrics {:track? true + :alias-id :syncing.find-sync-code} + :options (assoc options/dark-screen :sheet? true) + :component find-sync-code/view} + + {:name :screen/settings-setup-syncing + :metrics {:track? true + :alias-id :syncing.setup-syncing} + :options options/transparent-screen-options + :component settings-setup-syncing/view} + + {:name :screen/scan-sync-code-page + :metrics {:track? true + :alias-id :syncing.scan-sync-code} + :options options/transparent-modal-screen-options + :component scan-sync-code-page/view}]) + +(def settings-screens + [{:name :screen/settings + :metrics {:track? true + :alias-id :settings.profile-settings} + :options options/transparent-screen-options + :component settings/view} + + {:name :screen/settings.keycard + :metrics {:track? true + :alias-id :settings.keycard} + :options {:theme :dark + :insets {:top? true :bottom? true}} + :component settings.keycard/view} + + {:name :screen/edit-profile + :metrics {:track? true + :alias-id :settings.edit-profile} + :options options/transparent-modal-screen-options + :component edit-profile/view} + + {:name :screen/edit-accent-colour + :metrics {:track? true + :alias-id :settings.edit-profile-accent-colour} + :options options/transparent-modal-screen-options + :component edit-accent-colour/view} + + {:name :screen/edit-name + :metrics {:track? true + :alias-id :settings.edit-profile-name} + :options options/transparent-modal-screen-options + :component edit-name/view} + + {:name :screen/edit-bio + :metrics {:track? true + :alias-id :settings.edit-profile-bio} + :options options/transparent-modal-screen-options + :component edit-bio/view} + + {:name :screen/settings-password + :metrics {:track? true + :alias-id :settings.password} + :options options/transparent-modal-screen-options + :component settings-password/view} + + {:name :screen/change-password + :metrics {:track? true + :alias-id :settings.change-password} + :options (assoc options/transparent-modal-screen-options :theme :dark) + :component change-password/view} + + {:name :screen/change-password-loading + :metrics {:track? true + :alias-id :settings.change-password-loading} + :options (assoc + options/transparent-modal-screen-options + :theme :dark + :popGesture false + :hardwareBackButton {:dismissModalOnPress false + :popStackOnPress false}) + :component change-password-loading/view} + + {:name :screen/settings-messages + :metrics {:track? true + :alias-id :settings.messages} + :options options/transparent-modal-screen-options + :component settings.messages/view} + + {:name :screen/settings-blocked-users + :metrics {:track? true + :alias-id :settings.blocked-users} + :options options/transparent-modal-screen-options + :component settings.blocked-users/view} + + {:name :screen/settings-privacy-and-security + :metrics {:track? true + :alias-id :settings.private-and-security} + :options options/transparent-modal-screen-options + :component settings.privacy-and-security/view} + + {:name :screen/settings.share-usage-data + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component settings.share-usage/view} + + {:name :screen/settings.syncing + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component settings.syncing/view} + + {:name :screen/settings.notifications + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component settings.notifications/view} + + {:name :screen/paired-devices + :metrics {:track? true + :alias-id :settings.paired-devices} + :options options/transparent-modal-screen-options + :component syncing-devices-list/view} + + {:name :screen/settings.language-and-currency + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component settings.language-and-currency/view} + + {:name :screen/settings.currency-selection + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component settings.currency-selection/view} + + {:name :screen/settings.about + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component settings.about/view} + + {:name :screen/settings.privacy-policy + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component privacy-policy/view} + + {:name :screen/settings.terms-of-use + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component terms-of-use/view} + + {:name :screen/settings.advanced + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component settings.advanced/view}]) + +(def wallet-settings-screens + [{:name :screen/settings.wallet + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component wallet-options/view} + + {:name :screen/settings.rename-keypair + :metrics {:track? true + :alias-id :settings.wallet-rename-keypair} + :options options/transparent-screen-options + :component keypair-rename/view} + + {:name :screen/settings.encrypted-keypair-qr + :metrics {:track? true + :alias-id :settings.wallet-encrypted-keypair-qr} + :options options/transparent-screen-options + :component encrypted-keypair-qr/view} + + {:name :screen/settings.saved-addresses + :metrics {:track? true + :alias-id :settings.wallet-saved-addresses} + :options options/transparent-modal-screen-options + :component saved-addresses-settings/view} + + {:name :screen/settings.keypairs-and-accounts + :metrics {:track? true + :alias-id :settings.wallet-keypairs-and-accounts} + :options options/transparent-modal-screen-options + :component keypairs-and-accounts/view} + + {:name :screen/settings.scan-keypair-qr + :metrics {:track? true + :alias-id :settings.wallet-scan-keypair-qr} + :options options/transparent-screen-options + :component scan-keypair-qr/view} + + {:name :screen/settings.missing-keypair.import-seed-phrase + :metrics {:track? true + :alias-id :settings.wallet-missing-keypair-import-seed-phrase} + :options options/transparent-screen-options + :component missing-keypairs.import-seed-phrase/view} + + {:name :screen/settings.missing-keypair-import-private-key + :metrics {:track? true + :alias-id :settings.wallet-missing-keypair-import-private-key} + :options options/transparent-screen-options + :component missing-keypairs.import-private-key/view} + + {:name :screen/settings.network-settings + :metrics {:track? true + :alias-id :settings.wallet-network-settings} + :options options/transparent-modal-screen-options + :component network-settings/view} + + {:name :screen/settings.save-address + :metrics {:track? true + :alias-id :settings.wallet-saved-addresses} + :options options/transparent-modal-screen-options + :component wallet-save-address/view} + + {:name :screen/settings.edit-saved-address + :metrics {:track? true + :alias-id :settings.wallet-edit-saved-addresses} + :options (assoc options/dark-screen :sheet? true) + :component wallet-save-address/view} + + {:name :screen/settings.add-address-to-save + :metrics {:track? true + :alias-id :settings.wallet-add-saved-address} + :options options/transparent-modal-screen-options + :component wallet-add-address-to-save/view} + + {:name :screen/settings.share-saved-address + :metrics {:track? true + :alias-id :settings.wallet-share-saved-address} + :options options/transparent-screen-options + :component share-saved-address/view}]) + +(def wallet-screens + [{:name :screen/wallet.accounts + :metrics {:track? true + :alias-id :wallet.account} + :options {:insets {:top? true}} + :component wallet-accounts/view} + + {:name :screen/wallet.collectible + :metrics {:track? true} + :options {:modalPresentationStyle :overCurrentContext} + :component wallet-collectible/view} + + {:name :screen/wallet.edit-account + :metrics {:track? true} + :component wallet-edit-account/view} + + {:name :screen/wallet.create-account + :metrics {:track? true + :alias-id :wallet.create-account} + :options {:insets {:top? true}} + :component wallet-create-account/view} + + {:name :screen/wallet.select-keypair + :metrics {:track? true + :alias-id :wallet.create-account-select-keypair} + :options {:insets {:top? true :bottom? true}} + :component wallet-select-keypair/view} + + {:name :screen/wallet.edit-derivation-path + :metrics {:track? true + :alias-id :wallet.create-account-edit-derivation-path} + :component wallet-edit-derivation-path/view} + + {:name :screen/confirm-backup + :metrics {:track? true + :alias-id :wallet.create-account-backup-new-keypair-confirm} + :options {:insets {:top? true :bottom? true}} + :component wallet-confirm-backup/view} + + {:name :screen/wallet.keypair-name + :metrics {:track? true + :alias-id :wallet.create-account-add-new-keypair-name} + :options {:insets {:top? true}} + :component wallet-key-pair-name/view} + + {:name :screen/wallet.import-private-key + :metrics {:track? true + :alias-id :wallet.create-account-import-keypair-by-private-key} + :options {:insets {:top? true}} + :component wallet-import-private-key/view} + + {:name :screen/wallet.share-address + :metrics {:track? true} + :options options/transparent-screen-options + :component wallet-share-address/view} + + {:name :screen/wallet.add-address-to-watch + :metrics {:track? true + :alias-id :wallet.add-watched-address} + :options {:insets {:top? true}} + :component wallet-add-address-to-watch/view} + + {:name :screen/wallet.confirm-address-to-watch + :metrics {:track? true + :alias-id :wallet.add-watched-address-profile} + :component wallet-confirm-address-to-watch/view} + + {:name :screen/wallet.transaction-confirmation + :metrics {:track? true} + :component wallet-transaction-confirmation/view} + + {:name :screen/wallet.transaction-progress + :metrics {:track? true} + :component wallet-transaction-progress/view}]) + +(def wallet-send-screens + [{:name :screen/wallet.scan-address + :metrics {:track? true + :alias-id :wallet-send.scan-address} + :options options/dark-screen + :component wallet-scan-address/view} + + {:name :screen/wallet.select-address + :metrics {:track? true + :alias-id :wallet-send.select-destination-address} + :options {:modalPresentationStyle :overCurrentContext + :insets {:top? true}} + :component wallet-select-address/view} + + {:name :screen/wallet.select-from + :metrics {:track? true + :alias-id :wallet-send.select-source-address} + :options {:modalPresentationStyle :overCurrentContext + :insets {:top? true}} + :component wallet-select-from/view} + + {:name :screen/wallet.select-asset + :metrics {:track? true + :alias-id :wallet-send.select-asset} + :options {:modalPresentationStyle :overCurrentContext + :insets {:top? true}} + :component wallet-select-asset/view} + + {:name :screen/wallet.send-input-amount + :metrics {:track? true + :alias-id :wallet-send.input-amount} + :options {:modalPresentationStyle :overCurrentContext + :insets {:top? true + :bottom? true}} + :component wallet-send-input-amount/view} + + {:name :screen/wallet.select-collectible-amount + :metrics {:track? true + :alias-id :wallet-send.select-collectible-amount} + :options {:insets {:top? true}} + :component wallet-select-collectible-amount/view} + + {:name :screen/wallet.tx-settings-max-fee + :metrics {:track? true + :alias-id :wallet.tx-settings-max-fee} + :options {:insets {:top? true}} + :component wallet-tx-settings-max-fee/view} + + {:name :screen/wallet.tx-settings-priority-fee + :metrics {:track? true + :alias-id :wallet.tx-settings-priority-fee} + :options {:insets {:top? true}} + :component wallet-tx-settings-priority-fee/view} + + {:name :screen/wallet.tx-settings-gas-amount + :metrics {:track? true + :alias-id :wallet.tx-settings-gas-amount} + :options {:insets {:top? true}} + :component wallet-tx-settings-gas-amount/view} + + {:name :screen/wallet.tx-settings-gas-price + :metrics {:track? true + :alias-id :wallet.tx-settings-gas-price} + :options {:insets {:top? true}} + :component wallet-tx-settings-gas-price/view} + + {:name :screen/wallet.tx-settings-nonce + :metrics {:track? true + :alias-id :wallet.tx-settings-nonce} + :options {:insets {:top? true}} + :component wallet-tx-settings-nonce/view}]) + +(def wallet-bridge-screens + [{:name :screen/wallet.bridge-select-asset + :metrics {:track? true + :alias-id :wallet-bridge.select-asset} + :options {:insets {:top? true} + :modalPresentationStyle :overCurrentContext} + :component wallet-bridge-select-asset/view} + + {:name :screen/wallet.bridge-to + :metrics {:track? true + :alias-id :wallet-bridge.select-network} + :options {:insets {:top? true} + :modalPresentationStyle :overCurrentContext} + :component wallet-bridge-to/view} + + {:name :screen/wallet.bridge-input-amount + :metrics {:track? true + :alias-id :wallet-bridge.input-amount-to-bridge} + :options {:insets {:top? true}} + :component wallet-bridge-input-amount/view}]) + +(def wallet-swap-screens + [{:name :screen/wallet.swap-select-asset-to-pay + :metrics {:track? true + :alias-id :wallet-swap.select-asset-to-pay} + :options {:modalPresentationStyle :overCurrentContext + :insets {:top? true}} + :component wallet-swap-select-asset-to-pay/view} + + {:name :screen/wallet.swap-select-account + :metrics {:track? true} + :options {:modalPresentationStyle :overCurrentContext + :insets {:bottom? true}} + :component wallet-swap-select-account/view} + + {:name :screen/wallet.setup-swap + :metrics {:track? true + :alias-id :wallet-swap.input-amount-to-swap} + :options {:modalPresentationStyle :overCurrentContext + :insets {:bottom? true}} + :component wallet-swap-setup-swap/view} + + {:name :screen/wallet.swap-set-spending-cap + :metrics {:track? true + :alias-id :wallet-swap.set-spending-cap} + :options {:sheet? true} + :component wallet-swap-set-spending-cap/view} + + {:name :screen/wallet.swap-confirmation + :metrics {:track? true + :alias-id :wallet-swap.swap-confirmation} + :options {:modalPresentationStyle :overCurrentContext} + :component wallet-swap-confirmation/view}]) + +(def wallet-connect-screens + [{:name :screen/wallet.wallet-connect-session-proposal + :metrics {:track? true + :alias-id :wallet-connect.session-proposal} + :options {:sheet? true} + :component wallet-connect-session-proposal/view} + + {:name :screen/wallet-connect.sign-message + :metrics {:track? true} + :options {:sheet? true} + :component wallet-connect-sign-message/view} + + {:name :screen/wallet-connect.sign-transaction + :metrics {:track? true} + :options {:sheet? true} + :component wallet-connect-sign-transaction/view} + + {:name :screen/wallet-connect.send-transaction + :metrics {:track? true} + :options {:sheet? true} + :component wallet-connect-send-transaction/view} + + {:name :screen/wallet.connected-dapps + :metrics {:track? true + :alias-id :wallet-connect.connected-dapps} + :options {:insets {:top? true}} + :component wallet-connected-dapps/view} + + {:name :screen/wallet.scan-dapp + :metrics {:track? true + :alias-id :wallet-connect.scan-dapp} + :options options/dark-screen + :component wallet-scan-dapp/view}]) + +(def market-screens + [{:name :screen/market.token + :metrics {:track? true + :alias-id :wallet-market.select-token} + :options {:modalPresentationStyle :overCurrentContext + :insets {:top? true}} + :component market.token/view}]) + +(def onboarding-intro + {:name :screen/onboarding.intro + :metrics {:track? true} + :options {:theme :dark} + :on-focus [:onboarding/overlay-dismiss] + :component intro/view}) + +(def onboarding-create-profile + {:name :screen/onboarding.create-profile + :metrics {:track? true + :alias-id :onboarding.create-profile-intro} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations (merge + transitions/new-to-status-modal-animations + transitions/push-animations-for-transparent-background) + :popGesture false + :modalPresentationStyle :overCurrentContext} + :component create-profile/view}) + +(def onboarding-log-in + {:name :screen/onboarding.log-in + :metrics {:track? true} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations (merge + transitions/new-to-status-modal-animations + transitions/push-animations-for-transparent-background) + :popGesture false + :modalPresentationStyle :overCurrentContext} + :component log-in/view}) + +(def onboarding-create-profile-password + {:name :screen/onboarding.create-profile-password + :metrics {:track? true + :alias-id :onboarding.create-profile-password} + :options {:theme :dark + :insets {:top false} + :layout options/onboarding-transparent-layout + :animations transitions/push-animations-for-transparent-background + :popGesture false} + :component create-password/create-password}) + +(def onboarding-enable-biometrics + {:name :screen/onboarding.enable-biometrics + :metrics {:track? true} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations (merge + transitions/new-to-status-modal-animations + transitions/push-animations-for-transparent-background) + :popGesture false + :modalPresentationStyle :overCurrentContext + :hardwareBackButton {:dismissModalOnPress false + :popStackOnPress false}} + :component enable-biometrics/view}) + +(def onboarding-share-usage + {:name :screen/onboarding.share-usage + :metrics {:track? true} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations transitions/modal-animations-vertical + :popGesture false + :modalPresentationStyle :overCurrentContext} + :component onboarding.share-usage/view}) + +(def onboarding-preparing-status + {:name :screen/onboarding.preparing-status + :metrics {:track? true} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations transitions/push-animations-for-transparent-background + :popGesture false + :hardwareBackButton {:dismissModalOnPress false + :popStackOnPress false}} + :component preparing-status/view}) + +(def onboarding-entering-seed-phrase + {:name :screen/onboarding.enter-seed-phrase + :metrics {:track? true + :alias-id :onboarding.sign-in-by-seed-phrase} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations transitions/push-animations-for-transparent-background + :popGesture false} + :component enter-seed-phrase/view}) + +(def onboarding-enable-notifications + {:name :screen/onboarding.enable-notifications + :metrics {:track? true} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations transitions/push-animations-for-transparent-background + :popGesture false + :modalPresentationStyle :overCurrentContext + :hardwareBackButton {:dismissModalOnPress false + :popStackOnPress false}} + :component enable-notifications/view}) + +(def onboarding-sign-in-intro + {:name :screen/onboarding.sign-in-intro + :metrics {:track? true + :alias-id :onboarding.sign-in-by-syncing} + :options {:layout options/onboarding-transparent-layout + :animations (merge + transitions/sign-in-modal-animations + transitions/push-animations-for-transparent-background) + :modalPresentationStyle :overCurrentContext} + :component sign-in/animated-view}) + +;; TODO(@seanstrom): Remove this definition if it is no longer needed +(def onboarding-sign-in + {:name :screen/onboarding.sign-in + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :overCurrentContext + :layout options/onboarding-layout} + :component sign-in/view}) + +;; NOTE(@seanstrom): +;; This screen seems to be accessible from settings when syncing. +;; Should we consider this an onboarding screen? +(def onboarding-syncing-progress + {:name :screen/onboarding.syncing-progress + :metrics {:track? true + :alias-id :onboarding.syncing-devices} + :options (assoc options/dark-screen + :popGesture + false) + :component syncing-devices/view}) + +(def onboarding-syncing-progress-intro + {:name :screen/onboarding.syncing-progress-intro + :metrics {:track? true + :alias-id :onboarding.sign-in-by-syncing-started} + :options {:theme :dark + :layout options/onboarding-transparent-layout + :animations transitions/push-animations-for-transparent-background + :popGesture false} + :component syncing-devices/view-onboarding}) + +(def onboarding-screens + [onboarding-intro + onboarding-create-profile + onboarding-log-in + onboarding-create-profile-password + onboarding-enable-biometrics + onboarding-preparing-status + onboarding-entering-seed-phrase + onboarding-enable-notifications + onboarding-share-usage + onboarding-sign-in-intro + onboarding-sign-in + onboarding-syncing-progress + onboarding-syncing-progress-intro]) + +(def keycard-screens + [{:name :screen/keycard.check + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.check/view} + + {:name :screen/keycard.manage.empty-import + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.manage/empty-import-view} + + {:name :screen/keycard.manage.empty-backup + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.manage/empty-backup-view} + + {:name :screen/keycard.manage.not-empty-logout + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.manage/not-empty-logout-view} + + {:name :screen/keycard.login.empty + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.login/empty-view} + + {:name :screen/keycard.login.already-added + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.login/already-added-view} + + {:name :screen/keycard.create.empty + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.create/empty-view} + + {:name :screen/keycard.create.not-empty + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.create/not-empty-view} + + {:name :screen/keycard.error + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.error/view} + + {:name :screen/keycard.different-card + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.different-card/view} + + {:name :screen/keycard.not-keycard + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.not-keycard/view} + + {:name :screen/keycard.authorise + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.authorise/view} + + {:name :screen/keycard.migrate + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.migrate/view} + + {:name :screen/keycard.re-encrypting + :metrics {:track? true} + :options {:theme :dark + :insets {:top? true :bottom? true} + :popGesture false + :modalPresentationStyle :fullScreen + :hardwareBackButton {:dismissModalOnPress false + :popStackOnPress false}} + :component keycard.re-encrypting/view} + + {:name :screen/keycard.migrate.success + :metrics {:track? true} + :options {:theme :dark + :insets {:top? true :bottom? true} + :popGesture false + :modalPresentationStyle :fullScreen + :hardwareBackButton {:dismissModalOnPress false + :popStackOnPress false}} + :component keycard.migrate.success/view} + + {:name :screen/keycard.migrate.fail + :metrics {:track? true} + :options {:theme :dark + :insets {:top? true :bottom? true} + :popGesture false + :modalPresentationStyle :fullScreen + :hardwareBackButton {:dismissModalOnPress false + :popStackOnPress false}} + :component keycard.migrate.fail/view} + + {:name :screen/keycard.pin.create + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.pin.create/view} + + {:name :screen/keycard.pin.enter + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.pin.enter/view} + + {:name :screen/keycard.migrate.profile-keys + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.migrate.profile-keys/view} + + {:name :screen/keycard.manage.profile-keys + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.manage.profile-keys/view} + + {:name :screen/keycard.create-profile + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.create/view} + + {:name :screen/keycard.create.ready-to-add + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.create/ready-to-add} + + {:name :screen/keycard.create.ready-to-generate + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.create/ready-to-generate} + + {:name :screen/keycard.ready-to-change-pin + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.change-pin/ready-to-change-pin} + + {:name :screen/keycard.pin-change-success + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.change-pin/pin-change-success} + + {:name :screen/keycard.pin-change-failed + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark + :modalPresentationStyle :fullScreen} + :component keycard.change-pin/pin-change-failed} + + {:name :screen/keycard.factory-reset.success + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.factory-reset/success-view} + + {:name :screen/keycard.factory-reset.fail + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.factory-reset/failed-view} + + {:name :screen/keycard.backup.scan-empty + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.backup/scan-empty} + + {:name :screen/keycard.backup.not-empty + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.backup/not-empty-view} + + {:name :screen/keycard.backup.ready-to-add + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.backup/ready-to-add} + + {:name :screen/keycard.backup.success + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen + :insets {:top? true :bottom? true}} + :component keycard.backup/success-view} + + {:name :screen/keycard.unblock.ready-to-unblock + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :modalPresentationStyle :fullScreen} + :component keycard.unblock/ready-to-unblock} + + {:name :screen/keycard.unblock.success + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :modalPresentationStyle :fullScreen} + :component keycard.unblock/success-view}]) + +(defn screens + [] + (concat + (old-screens/screens) + chat-screens + community-screens + contact-screens + device-syncing-screens + settings-screens + wallet-settings-screens + wallet-screens + wallet-send-screens + wallet-bridge-screens + wallet-swap-screens + wallet-connect-screens + market-screens + onboarding-screens + keycard-screens + + [{:name :screen/activity-center + :metrics {:track? true} + :options options/transparent-screen-options + :component activity-center/view} + + {:name :screen/share-shell + :metrics {:track? true} + :options options/transparent-screen-options + :component share/view} + + {:name :screen/shell-stack + :metrics {:track? true} + :component shell/shell-stack} + + {:name :screen/shell-qr-reader + :metrics {:track? true} + :options options/dark-screen + :component shell-qr-reader/view} + + {:name :screen/lightbox + :metrics {:track? true} + :options options/lightbox + :component lightbox/lightbox} + + {:name :screen/photo-selector + :metrics {:track? true} + :options {:sheet? true} + :component photo-selector/photo-selector} + + {:name :screen/camera-screen + :metrics {:track? true} + :options {:navigationBar {:backgroundColor colors/black} + :theme :dark} + :component camera-screen/camera-screen} + + {:name :screen/emoji-picker + :metrics {:track? true} + :options {:sheet? true} + :component emoji-picker/view} + + {:name :screen/pdf-viewer + :metrics {:track? true} + :options {:insets {:top? true} + :modalPresentationStyle :overCurrentContext} + :component pdf-viewer/view} + + {:name :screen/backup-recovery-phrase + :metrics {:track? true} + :options {:insets {:top? true :bottom? true}} + :component backup-recovery-phrase/view} + + {:name :screen/backup-recovery-phrase-on-shell + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component backup-recovery-phrase/view} + + {:name :screen/backup-recovery-phrase-dark + :metrics {:track? true} + :options {:theme :dark + :insets {:top? true :bottom? true}} + :component backup-recovery-phrase/view} + + {:name :screen/confirm-backup-dark + :metrics {:track? true} + :options {:insets {:top? true :bottom? true} + :theme :dark} + :component wallet-confirm-backup/view} + + {:name :screen/confirm-backup-on-shell + :metrics {:track? true} + :options options/transparent-modal-screen-options + :component wallet-confirm-backup/view} + + {:name :screen/use-recovery-phrase + :metrics {:track? true} + :component enter-seed-phrase/view} + + {:name :screen/use-recovery-phrase-dark + :metrics {:track? true} + :options {:theme :dark + :modalPresentationStyle :fullScreen} + :component enter-seed-phrase/view} + + {:name :screen/profile.profiles + :metrics {:track? true + :alias-id :app.profiles} + :options {:theme :dark + :layout options/onboarding-layout} + :on-focus [:onboarding/overlay-dismiss] + :component profiles/view} + + {:name :screen/profile.edit-profile-modal + :metrics {:track? true} + :options options/transparent-screen-options + :component edit-profile-modal/view}] + + (when js/goog.DEBUG + [{:name :screen/dev-component-preview + :options {:sheet? true} + :component component-preview/view}]) + + (when config/debug-or-pr-build? + quo.preview/screens) + + (when config/debug-or-pr-build? + quo.preview/main-screens) + + (when config/debug-or-pr-build? + status-im-preview/screens) + + (when config/debug-or-pr-build? + status-im-preview/main-screens) + + (when config/debug-or-pr-build? + [{:name :screen/feature-flags + :options {:insets {:top? true}} + :component feature-flags/view}]))) + +(def screens-by-name + (utils.collection/index-by :name (screens))) diff --git a/src/status_im/navigation/state.cljs b/src/status_im/navigation/state.cljs new file mode 100644 index 00000000000..e80bbc79df4 --- /dev/null +++ b/src/status_im/navigation/state.cljs @@ -0,0 +1,51 @@ +(ns status-im.navigation.state + (:require [utils.re-frame :as rf])) + +(defonce root-id (atom nil)) +(defonce modals (atom [])) +(defonce dissmissing (atom false)) +(defonce alert-banner-shown? (atom false)) + +(defonce ^:private navigation-state (atom [])) + +(defn get-navigation-state + [] + @navigation-state) + +(defn- update-view-id + [] + (when-let [view-id (:id (last (get-navigation-state)))] + (rf/dispatch [:set-view-id view-id]))) + +(defn navigation-state-push + [component] + (when-let [view-id (:id (last (get-navigation-state)))] + (when-not (= view-id (:id component)) + (swap! navigation-state conj component) + (update-view-id)))) + +(defn navigation-state-pop + [] + (reset! navigation-state (vec (butlast @navigation-state))) + (update-view-id)) + +(defn navigation-state-reset + [state] + (reset! navigation-state state) + (update-view-id)) + +(defn- indices-of-predicate-match + [pred coll] + (keep-indexed #(when (pred %2) %1) coll)) + +(defn navigation-pop-from + "Pops all items after match (including match)" + [comp-id] + (when-let [index (first (indices-of-predicate-match #(= comp-id (:id %)) @navigation-state))] + (navigation-state-reset (vec (take index @navigation-state))))) + +(defn navigation-pop-after + "Pops all items after match (excluding match)" + [comp-id] + (when-let [index (first (indices-of-predicate-match #(= comp-id (:id %)) @navigation-state))] + (navigation-state-reset (vec (take (inc index) @navigation-state))))) diff --git a/src/status_im/navigation/transitions.cljs b/src/status_im/navigation/transitions.cljs new file mode 100644 index 00000000000..d802a2832ba --- /dev/null +++ b/src/status_im/navigation/transitions.cljs @@ -0,0 +1,66 @@ +(ns status-im.navigation.transitions + (:require + [react-native.core :as rn] + [status-im.constants :as constants])) + +;;;; Modal Transitions +(def sign-in-modal-animations + {:showModal {:translationY {:from (:height (rn/get-window)) + :to 0 + :duration constants/onboarding-modal-animation-duration} + :alpha {:from 1 + :to 1 + :duration 0}} + :dismissModal {:translationY {:from 0 + :to (:height (rn/get-window)) + :duration constants/onboarding-modal-animation-duration} + :alpha {:from 1 + :to 0 + :duration constants/onboarding-modal-animation-duration}}}) + +(def push-animations-for-transparent-background + {:push {:content {:enter {:translationX {:from (:width (rn/get-window)) + :to 0 + :duration constants/onboarding-modal-animation-duration}} + :exit {:translationX {:from 0 + :to (- (:width (rn/get-window))) + :duration constants/onboarding-modal-animation-duration}}}} + :pop {:content {:exit {:translationX {:from 0 + :to (:width (rn/get-window)) + :duration constants/onboarding-modal-animation-duration}} + :enter {:translationX {:from (- (:width (rn/get-window))) + :to 0 + :duration constants/onboarding-modal-animation-duration}}}}}) + +(def new-to-status-modal-animations + {:showModal {:translationX {:from (:width (rn/get-window)) + :to 0 + :duration constants/onboarding-modal-animation-duration}} + :dismissModal {:translationX {:from 0 + :to (:width (rn/get-window)) + :duration constants/onboarding-modal-animation-duration}}}) + +(def modal-animations-vertical + {:showModal {:translationY {:from (:height (rn/get-window)) + :to 0 + :duration constants/onboarding-modal-animation-duration}} + :dismissModal {:translationY {:from 0 + :to (:height (rn/get-window)) + :duration constants/onboarding-modal-animation-duration}}}) + +;;;; Stack Transitions +(def stack-slide-transition + {:push {:content {:translationX {:from (:width (rn/get-window)) + :to 0 + :duration 250}}} + :pop {:content {:translationX {:from 0 + :to (:width (rn/get-window)) + :duration 250}}}}) + +(def stack-transition-from-bottom + {:push {:content {:translationY {:from (:height (rn/get-window)) + :to 0 + :duration 250}}} + :pop {:content {:translationY {:from 0 + :to (:height (rn/get-window)) + :duration 250}}}}) diff --git a/src/status_im/navigation/view.cljs b/src/status_im/navigation/view.cljs new file mode 100644 index 00000000000..3bf0767c221 --- /dev/null +++ b/src/status_im/navigation/view.cljs @@ -0,0 +1,147 @@ +(ns status-im.navigation.view + (:require + [legacy.status-im.ui.screens.popover.views :as popover] + [quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [react-native.safe-area-context :as safe-area-context] + [reagent.core :as reagent] + schema.view + [status-im.common.alert-banner.view :as alert-banner] + [status-im.common.bottom-sheet-screen.view :as bottom-sheet-screen] + [status-im.common.bottom-sheet.view :as bottom-sheet] + [status-im.common.toasts.view :as toasts] + [status-im.contexts.keycard.nfc.sheets.view :as keycard.sheet] + [status-im.navigation.screens :as screens] + [status-im.setup.hot-reload :as reloader] + [utils.re-frame :as rf])) + +(defn get-screens + [] + (reduce + (fn [acc screen] + (assoc acc (:name screen) screen)) + {} + (screens/screens))) + +(def screens (get-screens)) + +(defn inactive + [] + (let [theme (rf/sub [:theme])] + (when (rf/sub [:hide-screen?]) + [rn/view + {:position :absolute + :flex 1 + :top 0 + :bottom 0 + :left 0 + :right 0 + :background-color (colors/theme-colors colors/white colors/neutral-100 theme) + :z-index 999999999999999999}]))) + +(defn wrapped-screen-style + [{:keys [top? bottom? background-color alert-banners-top-margin theme]}] + (merge + {:flex 1 + :margin-top alert-banners-top-margin + :background-color (or background-color (colors/theme-colors colors/white colors/neutral-95 theme))} + (when bottom? + {:padding-bottom safe-area/bottom}) + (when top? + {:padding-top safe-area/top}))) + +(defn screen + [screen-key] + (reagent.core/reactify-component + (fn [] + (let [screen-details (get (if js/goog.DEBUG + (get-screens) + screens) + (keyword "screen" screen-key)) + {:keys [component options name]} screen-details + screen-params (rf/sub [:get-screen-params name]) + {:keys [insets sheet? theme + skip-background?]} options + alert-banners-top-margin (rf/sub [:alert-banners/top-margin]) + background-color (or (get-in options [:layout :backgroundColor]) + (when sheet? :transparent)) + app-theme (rf/sub [:theme]) + theme (or theme app-theme)] + ^{:key (str "root" screen-key @reloader/cnt)} + [quo.context/provider {:theme theme :screen-id name :screen-params screen-params} + [safe-area-context/safe-area-provider + [rn/view + {:style (wrapped-screen-style (assoc + insets + :theme theme + :background-color background-color + :alert-banners-top-margin alert-banners-top-margin))} + [inactive] + (if sheet? + [bottom-sheet-screen/view {:content component :skip-background? skip-background?}] + [component])]] + (when js/goog.DEBUG + [:<> + [reloader/reload-view] + [schema.view/view]])])))) + +(def bottom-sheet + (reagent/reactify-component + (fn [] + (let [app-theme (rf/sub [:theme]) + view-id (rf/sub [:view-id]) + {:keys [sheets hide?]} (rf/sub [:bottom-sheet]) + sheet (last sheets) + {:keys [theme screen-id]} sheet + screen-id-value (or screen-id view-id) + screen-params (rf/sub [:get-screen-params screen-id-value]) + insets safe-area/insets + keyboard-vertical-offset (- (max 20 (:bottom insets)))] + ^{:key (str "sheet" @reloader/cnt)} + [quo.context/provider + {:theme (or theme app-theme) + :screen-id screen-id-value + :screen-params screen-params} + [safe-area-context/safe-area-provider + [:<> + [inactive] + [rn/keyboard-avoiding-view + {:style {:flex 1} + :keyboard-vertical-offset keyboard-vertical-offset} + (when sheet + [bottom-sheet/view + {:insets insets :hide? hide?} + sheet])]]]])))) + +(def toasts (reagent/reactify-component toasts/toasts)) + +(def alert-banner + (reagent/reactify-component + (fn [] + ^{:key (str "alert-banner" @reloader/cnt)} + [quo.context/provider {:theme :dark} + [alert-banner/view]]))) + +(def nfc-sheet-comp + (reagent/reactify-component + (fn [] + (let [app-theme (rf/sub [:theme])] + ^{:key (str "nfc-sheet-" @reloader/cnt)} + [quo.context/provider {:theme app-theme} + [rn/keyboard-avoiding-view + {:style {:position :relative :flex 1}} + [keycard.sheet/android-view]]])))) + +;; LEGACY (should be removed in status 2.0) + +(def popover-comp + (reagent/reactify-component + (fn [] + ^{:key (str "popover" @reloader/cnt)} + [:<> + [inactive] + [popover/popover] + (when js/goog.DEBUG + [reloader/reload-view])]))) diff --git a/src/status_im/network/events.cljs b/src/status_im/network/events.cljs deleted file mode 100644 index db18b3aba3e..00000000000 --- a/src/status_im/network/events.cljs +++ /dev/null @@ -1,41 +0,0 @@ -(ns status-im.network.events - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.network.net-info :as net-info] - [status-im.native-module.core :as status] - [status-im.transport.inbox :as inbox])) - -(re-frame/reg-fx - ::listen-to-network-status - (fn [[connection-listener net-info-listener]] - (net-info/is-connected? connection-listener) - (net-info/net-info net-info-listener) - (net-info/add-connection-listener connection-listener) - (net-info/add-net-info-listener net-info-listener))) - -(re-frame/reg-fx - ::notify-status-go - (fn [data] - (status/connection-change data))) - -(handlers/register-handler-fx - :listen-to-network-status - (fn [] - {::listen-to-network-status [#(re-frame/dispatch [::update-connection-status %]) - #(re-frame/dispatch [::update-network-status %])]})) - -(handlers/register-handler-fx - ::update-connection-status - [re-frame/trim-v] - (fn [{db :db :as cofx} [is-connected?]] - (handlers-macro/merge-fx - cofx - {:db (assoc db :network-status (if is-connected? :online :offline))} - (inbox/request-messages)))) - -(handlers/register-handler-fx - ::update-network-status - [re-frame/trim-v] - (fn [_ [data]] - {::notify-status-go data})) diff --git a/src/status_im/network/net_info.cljs b/src/status_im/network/net_info.cljs deleted file mode 100644 index 72fe5e35aa8..00000000000 --- a/src/status_im/network/net_info.cljs +++ /dev/null @@ -1,36 +0,0 @@ -(ns status-im.network.net-info - (:require [taoensso.timbre :as log] - [status-im.ui.components.react :as react-components] - [status-im.utils.platform :as platform])) - -(defn is-connected? [callback] - (when react-components/net-info - (.then (.fetch (.-isConnected react-components/net-info)) - (fn [is-connected?] - (log/debug "Is connected?" is-connected?) - (callback is-connected?))))) - -(defn- wrap-net-info [callback] - (fn [info-js] - (let [info (js->clj info-js :keywordize-keys true) - on-success #(callback {:type (:type info) :expensive? %})] - (if platform/ios? - (on-success false) - (.. react-components/net-info - isConnectionExpensive - (then on-success) - (catch (fn [error] (log/warn "isConnectionExpensive: " error)))))))) - -(defn net-info [callback] - (when react-components/net-info - (.then (.getConnectionInfo react-components/net-info) - (wrap-net-info callback)))) - -(defn add-connection-listener [listener] - (when react-components/net-info - (.addEventListener (.-isConnected react-components/net-info) "connectionChange" listener))) - -(defn add-net-info-listener [listener] - (when react-components/net-info - (.addEventListener react-components/net-info "connectionChange" - (wrap-net-info listener)))) diff --git a/src/status_im/protocol/handlers.cljs b/src/status_im/protocol/handlers.cljs deleted file mode 100644 index e59ea45a8dd..00000000000 --- a/src/status_im/protocol/handlers.cljs +++ /dev/null @@ -1,105 +0,0 @@ -(ns status-im.protocol.handlers - (:require [cljs.core.async :as async] - [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.native-module.core :as status] - [status-im.utils.utils :as utils] - [status-im.utils.datetime :as datetime] - [status-im.utils.ethereum.core :as ethereum-utils] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.web3-provider :as web3-provider] - [status-im.transport.core :as transport] - [status-im.transport.inbox :as transport.inbox] - [status-im.utils.ethereum.core :as ethereum])) - -;;;; COFX -(re-frame/reg-cofx - ::get-web3 - (fn [coeffects _] - (let [web3 (web3-provider/make-web3) - address (get-in coeffects [:db :account/account :address])] - (set! (.-defaultAccount (.-eth web3)) - (ethereum/normalized-address address)) - (assoc coeffects :web3 web3)))) - -(re-frame/reg-fx - ::web3-get-syncing - (fn [web3] - (when web3 - (.getSyncing - (.-eth web3) - (fn [error sync] - (re-frame/dispatch [:update-sync-state error sync])))))) - -(defn- assert-correct-network - [{:keys [db]}] - ;; Assure that node was started correctly - (let [{:keys [web3]} db] - (let [network (get-in db [:account/account :network]) - network-id (str (get-in db [:account/account :networks network :config :NetworkId]))] - (when (and network-id web3) ; necessary because of the unit tests - (.getNetwork (.-version web3) - (fn [error fetched-network-id] - (when (and (not error) ; error most probably means we are offline - (not= network-id fetched-network-id)) - (utils/show-popup - "Ethereum node started incorrectly" - "Ethereum node was started with incorrect configuration, application will be stopped to recover from that condition." - #(re-frame/dispatch [:close-application]))))))))) - -(defn initialize-protocol - [{:data-store/keys [transport mailservers] :keys [db web3] :as cofx} [current-account-id ethereum-rpc-url]] - (handlers-macro/merge-fx cofx - {:db (assoc db - :web3 web3 - :rpc-url (or ethereum-rpc-url constants/ethereum-rpc-url) - :transport/chats transport)} - (assert-correct-network) - (transport.inbox/initialize-offline-inbox mailservers) - (transport/init-whisper current-account-id))) -;;; INITIALIZE PROTOCOL -(handlers/register-handler-fx - :initialize-protocol - [re-frame/trim-v - (re-frame/inject-cofx ::get-web3) - (re-frame/inject-cofx :data-store/get-all-mailservers) - (re-frame/inject-cofx :data-store/transport)] - initialize-protocol) - -;;; NODE SYNC STATE - -(handlers/register-handler-db - :update-sync-state - (fn [{:keys [sync-state sync-data] :as db} [_ error sync]] - (let [{:keys [highestBlock currentBlock] :as state} - (js->clj sync :keywordize-keys true) - syncing? (> (- highestBlock currentBlock) constants/blocks-per-hour) - new-state (cond - error :offline - syncing? (if (= sync-state :done) - :pending - :in-progress) - :else (if (or (= sync-state :done) - (= sync-state :pending)) - :done - :synced))] - (cond-> db - (when (and (not= sync-data state) (= :in-progress new-state))) - (assoc :sync-data state) - (when (not= sync-state new-state)) - (assoc :sync-state new-state))))) - -(handlers/register-handler-fx - :check-sync - (fn [{{:keys [web3]} :db} _] - {::web3-get-syncing web3 - :dispatch-later [{:ms 10000 :dispatch [:check-sync]}]})) - -(handlers/register-handler-fx - :initialize-sync-listener - (fn [{{:keys [sync-listening-started network account/account] :as db} :db} _] - (when (and (not sync-listening-started) - (not (ethereum-utils/network-with-upstream-rpc? (get-in account [:networks network])))) - {:db (assoc db :sync-listening-started true) - :dispatch [:check-sync]}))) diff --git a/src/status_im/react_native/resources.cljs b/src/status_im/react_native/resources.cljs deleted file mode 100644 index 5690e9e16fb..00000000000 --- a/src/status_im/react_native/resources.cljs +++ /dev/null @@ -1,61 +0,0 @@ -(ns status-im.react-native.resources) - -(def contacts - {:mkr-market (js/require "./resources/images/contacts/mkr-market.png") - :oaken-water-meter (js/require "./resources/images/contacts/oaken-water-meter.png") - :gnosis (js/require "./resources/images/contacts/gnosis.png") - :melonport (js/require "./resources/images/contacts/melonport.png") - :bchat (js/require "./resources/images/contacts/bchat.png") - :dentacoin (js/require "./resources/images/contacts/Dentacoin.png") - :augur (js/require "./resources/images/contacts/augur.png") - :ethlance (js/require "./resources/images/contacts/ethlance.png") - :commiteth (js/require "./resources/images/contacts/commiteth.png") - :etherplay (js/require "./resources/images/contacts/etherplay.png") - :ethcro (js/require "./resources/images/contacts/ethcro.png") - :easytrade (js/require "./resources/images/contacts/easytrade.png") - - :name-bazaar (js/require "./resources/images/contacts/name-bazaar.png") - :cryptopunks (js/require "./resources/images/contacts/cryptopunks.png") - :cryptokitties (js/require "./resources/images/contacts/cryptokitties.png") - :etheremon (js/require "./resources/images/contacts/etheremon.png") - :cryptostrikers (js/require "./resources/images/contacts/cryptostrikers.png") - :oasis-direct (js/require "./resources/images/contacts/oasis-direct.png") - :erc-dex (js/require "./resources/images/contacts/erc-dex.png") - :cent (js/require "./resources/images/contacts/cent.png") - :hexel (js/require "./resources/images/contacts/hexel.png") - :peepeth (js/require "./resources/images/contacts/peepeth.png") - :etherbots (js/require "./resources/images/contacts/etherbots.png") - :opensea (js/require "./resources/images/contacts/opensea.png") - :kyber (js/require "./resources/images/contacts/kyber.png") - :bancor (js/require "./resources/images/contacts/bancor.png") - :cryptocribs (js/require "./resources/images/contacts/cryptocribs.png") - :cryptofighters (js/require "./resources/images/contacts/cryptofighters.png") - :airswap (js/require "./resources/images/contacts/airswap.png") - :civitas (js/require "./resources/images/contacts/civitas.png") - :dai (js/require "./resources/images/contacts/dai.png") - :local-ethereum (js/require "./resources/images/contacts/local-ethereum.png") - :eth2phone (js/require "./resources/images/contacts/eth2phone.png") - :bounties-network (js/require "./resources/images/contacts/bounties-network.png") - :cryptopurr (js/require "./resources/images/contacts/cryptopurr.png") - :ethlend (js/require "./resources/images/contacts/ethlend.png") - :smartz (js/require "./resources/images/contacts/smartz.png") - - :ddex (js/require "./resources/images/contacts/ddex.png") - :emoon (js/require "./resources/images/contacts/emoon.png") - :livepeer (js/require "./resources/images/contacts/livepeer.png") - - :console (js/require "./resources/images/contacts/console.png")}) - -(def assets - {:ethereum (js/require "./resources/images/assets/ethereum.png")}) - -(def ui - {:empty-hashtags (js/require "./resources/images/ui/empty-hashtags.png") - :empty-recent (js/require "./resources/images/ui/empty-recent.png") - :analytics-image (js/require "./resources/images/ui/analytics-image.png") - :welcome-image (js/require "./resources/images/ui/welcome-image.png") - :lock {:image (js/require "./resources/images/ui/lock.png") - :width 993 - :height 933} - :wallet-welcome (js/require "./resources/images/ui/wallet-welcome.png") - :wallet-setup (js/require "./resources/images/ui/wallet-setup.png")}) diff --git a/src/status_im/setup/dev.cljs b/src/status_im/setup/dev.cljs new file mode 100644 index 00000000000..1f89f62bf5b --- /dev/null +++ b/src/status_im/setup/dev.cljs @@ -0,0 +1,64 @@ +(ns status-im.setup.dev + (:require + ["react-native" :refer (DevSettings LogBox NativeModules)] + [react-native.platform :as platform] + [status-im.setup.oops :as setup.oops] + [status-im.setup.schema :as schema] + [utils.re-frame :as rf])) + +;; Ignore all logs, because there are lots of temporary warnings when developing and hot reloading +(.ignoreAllLogs LogBox) + +;; Only ignore warnings/errors that cannot be fixed for the time being. +;; When you add a warning to be ignored explain below why it is ignored and how it can be fixed. +;; When a warning is fixed make sure to remove it from here. +#_(.ignoreLogs ^js LogBox + (clj->js ["undefined is not an object (evaluating 'e.message')" + "Cannot read property 'message' of undefined" + "InternalError Metro has encountered an error" + "undefined Unable to resolve module `parse-svg-path`" + "group12" + "Setting a timer for a long period of time"])) + +;; List of ignored warnings/errors: +;; 1. "evaluating 'e.message'": Not sure why this error is happening, but it is coming from here +;; @walletconnect/jsonrpc-utils/dist/esm/error.js parseConnectionError() method +;; 2. "Cannot read property 'message' of undefined": same as 1, but for Android +;; 3. "InternalError Metro has encountered an error": an error that happens when losing connection to +;; metro, can be safely ignored +;; 4. "undefined Unable to resolve module `parse-svg-path`": an error that happens when losing +;; connection +;; to metro, can be safely ignored +;; 5. "group12": referring to the group-icon size 12x12. Currently, it is not available. When the design +;; team adds it to the +;; icon set it will be added to project. +;; 6. Setting a timer for a long period of time. Not sure why this error is happening + +(defn setup + [] + (rf/set-mergeable-keys #{:filters/load-filters + :pairing/set-installation-metadata + :fx + :dispatch-n + :legacy.status-im.ens.core/verify-names + :shh/send-direct-message + :shh/remove-filter + :transport/confirm-messages-processed + :group-chats/extract-membership-signature + :utils/dispatch-later + :json-rpc/call}) + (when ^:boolean js/goog.DEBUG + (setup.oops/setup!) + (schema/setup!) + (when platform/ios? + ;; on Android this method doesn't work + (when-let [setHotLoadingEnabled (or + (and NativeModules + (.-DevSettings NativeModules) + (.-setHotLoadingEnabled (.-DevSettings NativeModules))) + (and DevSettings + (.-_nativeModule DevSettings) + (.-setHotLoadingEnabled (.-_nativeModule DevSettings))))] + ;;there is a bug in RN, so we have to enable it first and then disable + (setHotLoadingEnabled true) + (js/setTimeout #(setHotLoadingEnabled false) 1000))))) diff --git a/src/status_im/setup/global_error.cljs b/src/status_im/setup/global_error.cljs new file mode 100644 index 00000000000..eece4cbc80a --- /dev/null +++ b/src/status_im/setup/global_error.cljs @@ -0,0 +1,70 @@ +(ns status-im.setup.global-error + (:require + [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.common.alert.effects :as alert.effects] + [utils.i18n :as i18n])) + +;; Error handling code based on https://gist.github.com/pesterhazy/e6846be1b6712a9038537022d131ce46 + +(defonce !handler-set (atom false)) + +(defn downgrade-reagent-errors! + "Downgrade reagent error to warning + Reagent uses console.error to notify the user that an exception occurs while + rendering. Unfortunately, react-native can only show one RedBox at a time and + discards all subsequent ones. This obscures the actual exception. By + downgrading the first screen to a warning, the second screen is actually shown + to the user." + [] + (when-not @!handler-set + (reset! !handler-set true) + (let [original-error (.-error js/console)] + (set! (.-error js/console) + (fn [& [head :as args]] + (if (and (string? head) (string/starts-with? head "Error rendering component")) + (apply (.-warn js/console) "Additional exception info:" args) + (apply original-error args))))))) + +(defn format-error + [e] + (if (instance? js/Error e) + {:name (.-name ^js e) :message (.-message ^js e) :stack (.-stack ^js e)} + {:message (pr-str e)})) + +(defn handle-error + [e _] + (let [f (format-error e)] + (js/console.log (str "PRETTY PRINTED EXCEPTION" + "\n\n***\nNAME: " + (pr-str (:name f)) + "\nMESSAGE: " + (:message f) + "\n\n" + (:stack f) + "\n\n***")))) + +(defonce !error-handler-set? (atom false)) + +(defn register-handler + "Improve error messages printed to console. + When js/goog.DEBUG is false, show a popup with an error summary; else rely on default `red` screen." + [] + (downgrade-reagent-errors!) + (when-not @!error-handler-set? + (reset! !error-handler-set? true) + (let [^js orig-handler (some-> js/ErrorUtils + ^js .-getGlobalHandler + (.call))] + (js/ErrorUtils.setGlobalHandler + (fn [^js e isFatal] + (handle-error e isFatal) + (if js/goog.DEBUG + (some-> orig-handler + (.call nil e isFatal)) + (alert.effects/show-confirmation + {:title "Error" + :content (.-message e) + :confirm-button-text (i18n/label :t/send-logs) + :on-accept #(re-frame/dispatch [:logging/send-logs-on-error + (.-message e)])}))))))) diff --git a/src/status_im/setup/hot_reload.cljs b/src/status_im/setup/hot_reload.cljs new file mode 100644 index 00000000000..3c66b276493 --- /dev/null +++ b/src/status_im/setup/hot_reload.cljs @@ -0,0 +1,110 @@ +(ns status-im.setup.hot-reload + (:require + [re-frame.core :as re-frame] + [react-native.core :as rn] + [reagent.core :as reagent] + schema.state + [status-im.setup.schema :as schema])) + +(defonce cnt (reagent/atom 0)) +(defonce reload-locked? (atom false)) +(defonce reload-interval (atom nil)) +(defonce warning? (reagent/atom false)) +(defonce visible (reagent/atom false)) +(defonce label (reagent/atom "")) + +;; ignore warning: reload used in shadow-cljs.edn +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn reload + [] + (js/setTimeout #(reset! visible false) 500) + (js/setTimeout #(reset! reload-locked? false) 3000) + (reset! warning? false) + (reset! visible true) + (reset! label "reloading UI") + (re-frame/clear-subscription-cache!) + (schema/setup!) + (schema.state/clear-errors) + (swap! cnt inc)) + +;; ignore warning: before-reload used in shadow-cljs.edn +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn before-reload + [done] + (when @reload-interval (js/clearInterval @reload-interval)) + (if @reload-locked? + (reset! + reload-interval + (js/setInterval + (fn [] + (when-not @reload-locked? + (js/clearInterval @reload-interval) + (reset! reload-locked? true) + (done))) + 500)) + (do + (reset! reload-locked? true) + (done)))) + +(defn build-completed + [] + (reset! label "reloading code") + (reset! warning? false) + (reset! visible true)) + +(defn build-failed + [warnings] + (reset! warning? true) + (reset! label (str "building failed" + (when (seq warnings) + (str "\n" (count warnings) " warnings")))) + (reset! visible true)) + +(defn build-start + [] + (reset! warning? false) + (reset! label "building") + (reset! visible true)) + +;; ignore warning: build-notify used in shadow-cljs.edn +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn build-notify + [{:keys [type info]}] + (cond (= :build-start type) + (build-start) + (or (= :build-failure type) + (and (= :build-complete type) (seq (:warnings info)))) + (build-failed (:warnings info)) + (= :build-complete type) + (build-completed))) + +(defn reload-view + [_] + (fn [] + (when @visible + [rn/view + {:pointerEvents :none + :style {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :justify-content :center + :align-items :center}} + [rn/view + {:width 64 + :height 64 + :background-color "#0000FF30" + :border-radius 32 + :justify-content :center + :align-items :center} + [rn/activity-indicator {:animating true}]] + [rn/text {:style {:margin-top 10 :color (if @warning? :red :black)}} + @label]]))) + +(defn use-safe-unmount + [handler] + (let [prev-cnt @cnt] + (if js/goog.DEBUG + (rn/use-unmount #(when (= prev-cnt @cnt) (handler))) + (rn/use-unmount handler)))) diff --git a/src/status_im/setup/i18n_resources.cljs b/src/status_im/setup/i18n_resources.cljs new file mode 100644 index 00000000000..cd931194bc0 --- /dev/null +++ b/src/status_im/setup/i18n_resources.cljs @@ -0,0 +1,81 @@ +(ns status-im.setup.i18n-resources + (:require + [clojure.string :as string] + [utils.i18n :as i18n])) + +;; FIXME: that should be replaced with `(react-native-languages/get-lang-keyword)` +;; in order for languages/translations to work +;; see https://github.com/status-im/status-mobile/issues/16058 for details +(def default-device-language :en) + +(def languages + #{:ar :bn :de :el :en :es :es_419 :es_AR :fil :fr :hi :id :in :it :ja :ko :ms :nl :pl :pt :pt_BR :ru + :tr :vi :zh :zh_Hant :zh_TW}) + +(defonce loaded-languages + (atom + (conj #{:en} default-device-language))) + +(defn valid-language + [lang] + (if (contains? languages lang) + (keyword lang) + (let [parts (string/split (name lang) #"[\-\_]") + short-lang (keyword (str (first parts) "_" (second parts))) + shortest-lang (keyword (first parts))] + (if (and (> (count parts) 2) (contains? languages short-lang)) + short-lang + (when (contains? languages shortest-lang) + shortest-lang))))) + +(defn require-translation + [lang-key] + (when-let [lang (valid-language (keyword lang-key))] + (case lang + :ar (js/require "../translations/ar.json") + :bn (js/require "../translations/bn.json") + :de (js/require "../translations/de.json") + :el (js/require "../translations/el.json") + :en (js/require "../translations/en.json") + :es (js/require "../translations/es.json") + :es_419 (js/require "../translations/es_419.json") + :es_AR (js/require "../translations/es_AR.json") + :fil (js/require "../translations/fil.json") + :fr (js/require "../translations/fr.json") + :hi (js/require "../translations/hi.json") + :id (js/require "../translations/id.json") + :in (js/require "../translations/id.json") + :it (js/require "../translations/it.json") + :ja (js/require "../translations/ja.json") + :ko (js/require "../translations/ko.json") + :ms (js/require "../translations/ms.json") + :nl (js/require "../translations/nl.json") + :pl (js/require "../translations/pl.json") + :pt (js/require "../translations/pt.json") + :pt_BR (js/require "../translations/pt_BR.json") + :ru (js/require "../translations/ru.json") + :tr (js/require "../translations/tr.json") + :vi (js/require "../translations/vi.json") + :zh (js/require "../translations/zh.json") + :zh_Hant (js/require "../translations/zh_hant.json") + :zh_TW (js/require "../translations/zh_TW.json")))) + +;; translations +(def translations-by-locale + (cond-> {:en (require-translation :en)} + (not= :en default-device-language) + (assoc default-device-language + (require-translation (-> (name default-device-language) + (string/replace "-" "_") + keyword))))) + +(i18n/setup (name default-device-language) (clj->js translations-by-locale)) + +(defn load-language + [lang] + (when-let [lang-key (valid-language (keyword lang))] + (when-not (contains? @loaded-languages lang-key) + (aset (i18n/get-translations) + lang + (require-translation lang-key)) + (swap! loaded-languages conj lang-key)))) diff --git a/src/status_im/setup/interceptor_metrics.cljc b/src/status_im/setup/interceptor_metrics.cljc new file mode 100644 index 00000000000..8f3604d855b --- /dev/null +++ b/src/status_im/setup/interceptor_metrics.cljc @@ -0,0 +1,10 @@ +(ns status-im.setup.interceptor-metrics + (:require + #?@(:mobile + [[re-frame.core :as re-frame] + [status-im.contexts.centralized-metrics.events :as centralized-metrics]]))) + +(defn setup-centralized-metrics-interceptor + [] + #?(:mobile + (re-frame/reg-global-interceptor centralized-metrics/interceptor))) diff --git a/src/status_im/setup/interceptors.cljs b/src/status_im/setup/interceptors.cljs new file mode 100644 index 00000000000..7575d52ce20 --- /dev/null +++ b/src/status_im/setup/interceptors.cljs @@ -0,0 +1,32 @@ +(ns status-im.setup.interceptors + (:require + [re-frame.core :as re-frame] + [re-frame.std-interceptors :as std-interceptors] + [status-im.setup.interceptor-metrics :as interceptor-metrics] + [utils.re-frame :as rf])) + +(defn register-global-interceptors + [] + (re-frame/reg-global-interceptor rf/debug-handlers-names) + (interceptor-metrics/setup-centralized-metrics-interceptor) + (re-frame/reg-global-interceptor (re-frame/inject-cofx :now)) + + ;; Interceptor `trim-v` removes the first element of the event vector. + ;; + ;; Without the interceptor: + ;; + ;; (re-frame/reg-event-fx + ;; :some-event-id + ;; (fn [{:keys [db]} [_event-id arg1 arg2]] + ;; {:db ... + ;; :fx [...]})) + ;; + ;; With the interceptor: + ;; + ;; (re-frame/reg-event-fx + ;; :some-event-id + ;; (fn [{:keys [db]} [arg1 arg2]] + ;; {:db ... + ;; :fx [...]})) + ;; + (re-frame/reg-global-interceptor std-interceptors/trim-v)) diff --git a/src/status_im/setup/oops.cljs b/src/status_im/setup/oops.cljs new file mode 100644 index 00000000000..f52062e6640 --- /dev/null +++ b/src/status_im/setup/oops.cljs @@ -0,0 +1,20 @@ +(ns status-im.setup.oops + (:require [oops.config])) + +(defn setup! + "Change oops defaults to warn and print instead of throwing exceptions during + development." + [] + (oops.config/update-current-runtime-config! + merge + {:error-reporting :console + :expected-function-value :warn + :invalid-selector :warn + :missing-object-key :warn + :object-is-frozen :warn + :object-is-sealed :warn + :object-key-not-writable :warn + :unexpected-empty-selector :warn + :unexpected-object-value :warn + :unexpected-punching-selector :warn + :unexpected-soft-selector :warn})) diff --git a/src/status_im/setup/schema.cljs b/src/status_im/setup/schema.cljs new file mode 100644 index 00000000000..f1f483d0b21 --- /dev/null +++ b/src/status_im/setup/schema.cljs @@ -0,0 +1,101 @@ +(ns status-im.setup.schema + (:require + [malli.core :as malli] + [malli.dev.cljs :as malli.dev] + [malli.dev.pretty :as malli.pretty] + [malli.dev.virhe :as malli.virhe] + malli.error + malli.instrument + malli.util + schema.common + [schema.core :as schema] + schema.quo + schema.re-frame + schema.registry + [taoensso.timbre :as log])) + +;;;; Formatters +;; These formatters replace the original ones provided by Malli. They are more +;; compact (less line breaks) and don't show the "More Information" section. + +(defn block + "Same as `malli.dev.pretty/-block`, but adds only one line break between `text` + and `body`." + [text body printer] + [:group (malli.virhe/-text text printer) :break [:align 2 body]]) + +(defmethod malli.virhe/-format ::malli/explain + [_ _ {:keys [schema] :as explanation} printer] + {:body + [:group + (block "Value:" (malli.virhe/-visit (malli.error/error-value explanation printer) printer) printer) + :break :break + (block "Errors:" (malli.virhe/-visit (malli.error/humanize explanation) printer) printer) + :break :break + (block "Schema:" (malli.virhe/-visit schema printer) printer)]}) + +(defmethod malli.virhe/-format ::malli/invalid-input + [_ _ {:keys [args input fn-name]} printer] + {:body + (cond-> [:group + (block "Invalid function arguments:" (malli.virhe/-visit args printer) printer) + :break :break] + fn-name + (conj (block "Function Var:" (malli.virhe/-visit fn-name printer) printer) + :break + :break) + :else + (conj (block "Input Schema:" (malli.virhe/-visit input printer) printer) + :break + :break + (block "Errors:" (malli.pretty/-explain input args printer) printer)))}) + +(defmethod malli.virhe/-format ::malli/invalid-output + [_ _ {:keys [value args output fn-name]} printer] + {:body + (cond-> [:group + (block "Invalid function return value:" (malli.virhe/-visit value printer) printer) + :break :break] + fn-name + (conj (block "Function Var:" (malli.virhe/-visit fn-name printer) printer) + :break + :break) + :else + (conj (block "Function arguments:" (malli.virhe/-visit args printer) printer) + :break + :break + (block "Output Schema:" (malli.virhe/-visit output printer) printer) + :break + :break + (block "Errors:" (malli.pretty/-explain output value printer) printer)))}) + +(defn register-schemas + "Register all global schemas in `schema.registry/registry`. + + Since keys in a map are unique, remember to qualify keywords. Prefer to add to + the global registry schemas for domain entities (e.g. message, chat, + notification, etc) or unambiguously useful schemas, like + `:schema.common/theme`." + [] + (schema.registry/merge (malli.util/schemas)) + (schema.common/register-schemas) + (schema.re-frame/register-schemas) + (schema.quo/register-schemas)) + +(defn setup! + "Configure Malli and initializes instrumentation. + + After evaluating an s-exp in the REPL that changes a function schema you'll + need to either save the file where the schema is defined and hot reload or + manually call `setup!`, otherwise you won't see any changes. It is safe and + even expected you will call `setup!` multiple times in REPLs." + [] + (schema.registry/init-global-registry) + (register-schemas) + + ;; In theory not necessary, but sometimes in a REPL session the dev needs to + ;; call unstrument! manually. + (malli.instrument/unstrument!) + + (malli.dev/start! {:report (schema/reporter)}) + (log/debug "Schemas initialized.")) diff --git a/src/status_im/setup/schema_preload.cljs b/src/status_im/setup/schema_preload.cljs new file mode 100644 index 00000000000..48724d30ac6 --- /dev/null +++ b/src/status_im/setup/schema_preload.cljs @@ -0,0 +1,6 @@ +(ns status-im.setup.schema-preload + ":dev/always is needed so that the compiler doesn't cache this file." + {:dev/always true} + (:require [status-im.setup.schema :as schema])) + +(schema/setup!) diff --git a/src/status_im/setup/status_backend_client.cljs b/src/status_im/setup/status_backend_client.cljs new file mode 100644 index 00000000000..e901b78cab7 --- /dev/null +++ b/src/status_im/setup/status_backend_client.cljs @@ -0,0 +1,22 @@ +(ns status-im.setup.status-backend-client + (:require ["react-native" :as react-native] + [status-im.config :as config])) + +(def default-config + {:server-enabled? (config/enabled? config/STATUS_BACKEND_SERVER_ENABLED) + :status-go-endpoint (str "http://" config/STATUS_BACKEND_SERVER_HOST "/statusgo/") + :signal-endpoint (str "ws://" config/STATUS_BACKEND_SERVER_HOST "/signals") + :root-data-dir config/STATUS_BACKEND_SERVER_ROOT_DATA_DIR}) + +(defn set-config! + [{:keys [server-enabled? status-go-endpoint signal-endpoint root-data-dir]}] + (when-let [client (.-StatusBackendClient (.-NativeModules react-native))] + (.configStatusBackendServer client + server-enabled? + status-go-endpoint + signal-endpoint + root-data-dir))) + +(defn init + [] + (set-config! default-config)) diff --git a/src/status_im/setup/test_preload.cljs b/src/status_im/setup/test_preload.cljs new file mode 100644 index 00000000000..31e02ae121b --- /dev/null +++ b/src/status_im/setup/test_preload.cljs @@ -0,0 +1,21 @@ +(ns status-im.setup.test-preload + (:require + ["bignumber.js" :as BigNumber] + [matcher-combinators.core :as matcher-combinators] + [matcher-combinators.model :as matcher.model] + [status-im.setup.oops :as setup.oops])) + +;; We must implement Matcher in order for tests to work with the `match?` +;; directive. +(extend-type BigNumber + matcher-combinators/Matcher + (-matcher-for [this] this) + (-base-name [_] 'bignumber) + (-match [this actual] + (if (-equiv this actual) + {:matcher-combinators.result/type :match + :matcher-combinators.result/value actual} + {:matcher-combinators.result/type :mismatch + :matcher-combinators.result/value (matcher.model/->Mismatch this actual)}))) + +(setup.oops/setup!) diff --git a/src/status_im/subs/activity_center.cljs b/src/status_im/subs/activity_center.cljs new file mode 100644 index 00000000000..e72dc7414cb --- /dev/null +++ b/src/status_im/subs/activity_center.cljs @@ -0,0 +1,90 @@ +(ns status-im.subs.activity-center + (:require + [re-frame.core :as re-frame] + [status-im.contexts.shell.activity-center.notification-types :as types])) + +(re-frame/reg-sub + :activity-center/notifications + :<- [:activity-center] + (fn [activity-center] + (:notifications activity-center))) + +(re-frame/reg-sub + :activity-center/unread-counts-by-type + :<- [:activity-center] + (fn [activity-center] + (:unread-counts-by-type activity-center))) + +(re-frame/reg-sub + :activity-center/notification-types-with-unread + :<- [:activity-center/unread-counts-by-type] + (fn [unread-counts] + (reduce-kv + (fn [acc notification-type unread-count] + (if (pos? unread-count) + (conj acc notification-type) + acc)) + #{} + unread-counts))) + +(re-frame/reg-sub + :activity-center/unread-count + :<- [:activity-center/unread-counts-by-type] + (fn [unread-counts] + (->> unread-counts + vals + (reduce + 0)))) + +(re-frame/reg-sub + :activity-center/seen? + :<- [:activity-center] + (fn [activity-center] + (:seen? activity-center))) + +(re-frame/reg-sub + :activity-center/unread-indicator + :<- [:activity-center/seen?] + :<- [:activity-center/unread-count] + (fn [[seen? unread-count]] + (cond + (zero? unread-count) :unread-indicator/none + seen? :unread-indicator/seen + :else :unread-indicator/new))) + +(re-frame/reg-sub + :activity-center/mark-all-as-read-undoable-till + :<- [:activity-center] + (fn [activity-center] + (:mark-all-as-read-undoable-till activity-center))) + +(re-frame/reg-sub + :activity-center/filter-status + :<- [:activity-center] + (fn [activity-center] + (get-in activity-center [:filter :status]))) + +(re-frame/reg-sub + :activity-center/filter-type + :<- [:activity-center] + (fn [activity-center] + (get-in activity-center [:filter :type] types/no-type))) + +(re-frame/reg-sub + :activity-center/filter-status-unread-enabled? + :<- [:activity-center/filter-status] + (fn [filter-status] + (= :unread filter-status))) + +(re-frame/reg-sub + :activity-center/pending-contact-requests + :<- [:activity-center] + (fn [activity-center] + (:contact-requests activity-center))) + +(re-frame/reg-sub + :activity-center/pending-contact-request-from-contact-id + :<- [:activity-center/pending-contact-requests] + (fn [pending-contact-requests [_ contact-id]] + (->> pending-contact-requests + (filter #(= contact-id (:author %))) + (first)))) diff --git a/src/status_im/subs/activity_center_test.cljs b/src/status_im/subs/activity_center_test.cljs new file mode 100644 index 00000000000..3a2bc74c3b2 --- /dev/null +++ b/src/status_im/subs/activity_center_test.cljs @@ -0,0 +1,113 @@ +(ns status-im.subs.activity-center-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + [status-im.contexts.shell.activity-center.notification-types :as types] + status-im.subs.activity-center + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(h/deftest-sub :activity-center/filter-status-unread-enabled? + [sub-name] + (testing "returns true when filter status is unread" + (swap! rf-db/app-db assoc-in [:activity-center :filter :status] :unread) + (is (true? (rf/sub [sub-name])))) + + (testing "returns false when filter status is not unread" + (swap! rf-db/app-db assoc-in [:activity-center :filter :status] :all) + (is (false? (rf/sub [sub-name]))))) + +(h/deftest-sub :activity-center/notification-types-with-unread + [sub-name] + (testing "returns an empty set when no types have unread notifications" + (swap! rf-db/app-db assoc-in + [:activity-center :unread-counts-by-type] + {types/one-to-one-chat 0 + types/private-group-chat 0 + types/contact-verification 0 + types/contact-request 0 + types/mention 0 + types/reply 0 + types/admin 0 + types/new-installation-received 0 + types/new-installation-created 0}) + + (is (= #{} (rf/sub [sub-name])))) + + (testing "returns a set with all types containing positive unread counts" + (swap! rf-db/app-db assoc-in + [:activity-center :unread-counts-by-type] + {types/one-to-one-chat 1 + types/private-group-chat 0 + types/contact-verification 1 + types/contact-request 0 + types/mention 3 + types/reply 0 + types/admin 2 + types/new-installation-received 1 + types/new-installation-created 0}) + + (let [actual (rf/sub [sub-name])] + (is (= #{types/one-to-one-chat + types/contact-verification + types/mention + types/admin + types/new-installation-received} + actual)) + (is (set? actual))))) + +(h/deftest-sub :activity-center/unread-count + [sub-name] + (swap! rf-db/app-db assoc-in + [:activity-center :unread-counts-by-type] + {types/one-to-one-chat 1 + types/private-group-chat 2 + types/contact-verification 3 + types/contact-request 4 + types/mention 5 + types/reply 6 + types/admin 7 + types/new-installation-received 8 + types/new-installation-created 9}) + + (is (= 45 (rf/sub [sub-name])))) + +(h/deftest-sub :activity-center/unread-indicator + [sub-name] + (testing "not seen and no unread notifications" + (swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 0}) + (swap! rf-db/app-db assoc-in [:activity-center :seen?] false) + (is (= :unread-indicator/none (rf/sub [sub-name])))) + + (testing "not seen and one or more unread notifications" + (swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 1}) + (swap! rf-db/app-db assoc-in [:activity-center :seen?] false) + (is (= :unread-indicator/new (rf/sub [sub-name])))) + + (testing "seen and no unread notifications" + (swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 0}) + (swap! rf-db/app-db assoc-in [:activity-center :seen?] true) + (is (= :unread-indicator/none (rf/sub [sub-name])))) + + (testing "seen and one or more unread notifications" + (swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 1}) + (swap! rf-db/app-db assoc-in [:activity-center :seen?] true) + (is (= :unread-indicator/seen (rf/sub [sub-name]))))) + +(h/deftest-sub :activity-center/pending-contact-request-from-contact-id + [sub-name] + (testing "returns contact request data if it finds a matching contact-id" + (let [contact-id "0x01" + contact-request {:author contact-id + :message {:content {:text "Hey there"}}}] + (swap! rf-db/app-db assoc-in [:activity-center :contact-requests] [contact-request]) + (is (match? contact-request + (rf/sub [sub-name contact-id]))))) + + (testing "returns nil if it does not find a matching contact-id" + (let [contact-id "0x01" + contact-request {:author "0x02" + :message {:content {:text "Hey there"}}}] + (swap! rf-db/app-db assoc-in [:activity-center :contact-requests] [contact-request]) + (is (match? nil + (rf/sub [sub-name contact-id])))))) diff --git a/src/status_im/subs/alert_banner.cljs b/src/status_im/subs/alert_banner.cljs new file mode 100644 index 00000000000..0dfe27eb2dd --- /dev/null +++ b/src/status_im/subs/alert_banner.cljs @@ -0,0 +1,14 @@ +(ns status-im.subs.alert-banner + (:require + [re-frame.core :as re-frame] + [status-im.constants :as constants])) + +(re-frame/reg-sub + :alert-banners/top-margin + :<- [:alert-banners] + :<- [:alert-banners/hide?] + (fn [[banners hide-banners?]] + (let [banners-count (count banners)] + (if (and (pos? banners-count) (not hide-banners?)) + (+ (* constants/alert-banner-height banners-count) 10) + 0)))) diff --git a/src/status_im/subs/alert_banner_test.cljs b/src/status_im/subs/alert_banner_test.cljs new file mode 100644 index 00000000000..a7f3f4f77f5 --- /dev/null +++ b/src/status_im/subs/alert_banner_test.cljs @@ -0,0 +1,32 @@ +(ns status-im.subs.alert-banner-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + status-im.subs.activity-center + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(h/deftest-sub :alert-banners/top-margin + [sub-name] + (testing "returns 50 when only alert banner" + (swap! rf-db/app-db assoc + :alert-banners + {:alert {:text "Alert" + :type :alert}}) + (is (= (rf/sub [sub-name]) 50))) + + (testing "returns 50 when only error banner" + (swap! rf-db/app-db assoc + :alert-banners + {:error {:text "Error" + :type :error}}) + (is (= (rf/sub [sub-name]) 50))) + + (testing "returns 90 when both alert and error banner" + (swap! rf-db/app-db assoc + :alert-banners + {:alert {:text "Alert" + :type :alert} + :error {:text "Error" + :type :error}}) + (is (= (rf/sub [sub-name]) 90)))) diff --git a/src/status_im/subs/biometrics.cljs b/src/status_im/subs/biometrics.cljs new file mode 100644 index 00000000000..1c86f988d8b --- /dev/null +++ b/src/status_im/subs/biometrics.cljs @@ -0,0 +1,8 @@ +(ns status-im.subs.biometrics + (:require [re-frame.core :as rf])) + +(rf/reg-sub + :biometrics/supported-type + :<- [:biometrics] + (fn [biometrics] + (get biometrics :supported-type))) diff --git a/src/status_im/subs/chat/utils.cljs b/src/status_im/subs/chat/utils.cljs new file mode 100644 index 00000000000..199e1adaeb6 --- /dev/null +++ b/src/status_im/subs/chat/utils.cljs @@ -0,0 +1,7 @@ +(ns status-im.subs.chat.utils + (:require [status-im.constants :as constants])) + +(defn online? + [visibility-status-type] + (or (= visibility-status-type constants/visibility-status-automatic) + (= visibility-status-type constants/visibility-status-always-online))) diff --git a/src/status_im/subs/chats.cljs b/src/status_im/subs/chats.cljs new file mode 100644 index 00000000000..cef7b64b039 --- /dev/null +++ b/src/status_im/subs/chats.cljs @@ -0,0 +1,394 @@ +(ns status-im.subs.chats + (:require + [clojure.string :as string] + [legacy.status-im.data-store.chats :as data-store.chats] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [status-im.contexts.chat.events :as chat.events] + [status-im.contexts.profile.utils :as profile.utils] + [status-im.subs.contact.utils :as contact.utils])) + +(def memo-chats-stack-items (atom nil)) + +(re-frame/reg-sub + :chats/chats-stack-items + :<- [:chats/home-list-chats] + :<- [:view-id] + :<- [:home-items-show-number] + (fn [[chats view-id home-items-show-number]] + (if (or (empty? @memo-chats-stack-items) (= view-id :screen/chats-stack)) + (let [res (take home-items-show-number chats)] + (reset! memo-chats-stack-items res) + res) + ;;we want to keep data unchanged so react doesn't change component when we leave screen + @memo-chats-stack-items))) + +(re-frame/reg-sub + :chats/chat + :<- [:chats/chats] + (fn [chats [_ chat-id]] + (get chats chat-id))) + +(re-frame/reg-sub + :community-id-by-chat-id + (fn [[_ chat-id]] + [(re-frame/subscribe [:chats/chat chat-id])]) + (fn [[chat]] + (:community-id chat))) + +(re-frame/reg-sub + :chats/by-community-id + :<- [:chats/chats] + (fn [chats [_ community-id]] + (->> chats + (keep (fn [[_ chat]] + (when (= (:community-id chat) community-id) + chat))) + (sort-by :timestamp >)))) + +(re-frame/reg-sub + :chats/community-chat-by-id + (fn [[_ community-id _]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [chats]}] [_ community-id chat-id]] + (get chats (string/replace chat-id community-id "")))) + +(defn member? + [public-key {:keys [members contacts users]}] + (let [members-list (into #{} (concat (keys users) contacts (map #(:id %) members)))] + (contains? members-list public-key))) + +(re-frame/reg-sub + :chats/home-list-chats + :<- [:chats/chats] + :<- [:chats-home-list] + :<- [:multiaccount/public-key] + (fn [[chats active-chats my-public-key]] + (reduce #(if-let [item (get chats %2)] + (let [group-chat-member? (and (chat.events/group-chat? item) + (member? my-public-key item))] + (conj %1 + (assoc item + :group-chat-member? + group-chat-member?))) + %1) + [] + active-chats))) + +(re-frame/reg-sub + :chats/chat-by-id + :<- [:chats/chats] + (fn [chats [_ chat-id]] + (get chats chat-id))) + +(re-frame/reg-sub + :chats/muted + (fn [[_ chat-id] _] + (re-frame/subscribe [:chats/chat-by-id chat-id])) + (fn [{:keys [muted]}] + muted)) + +(re-frame/reg-sub + :chats/current-raw-chat + :<- [:chats/chats] + :<- [:chats/current-chat-id] + (fn [[chats current-chat-id]] + (get chats current-chat-id))) + +(re-frame/reg-sub + :chats/current-chat-input + :<- [:chats/current-chat-id] + :<- [:chat/inputs] + (fn [[chat-id inputs]] + (get inputs chat-id))) + +(re-frame/reg-sub + :chats/sending-image + :<- [:chats/current-chat-id] + :<- [:chat/inputs] + (fn [[chat-id inputs]] + (get-in inputs [chat-id :metadata :sending-image]))) + +(re-frame/reg-sub + :chats/current-chat-membership + :<- [:chats/current-chat-id] + :<- [:chat/memberships] + (fn [[chat-id memberships]] + (get memberships chat-id))) + +(re-frame/reg-sub + :chats/current-chat + :<- [:chats/current-raw-chat] + :<- [:multiaccount/public-key] + :<- [:communities/current-community] + :<- [:contacts/blocked-set] + :<- [:contacts/contacts-raw] + :<- [:chat/inputs] + (fn [[{:keys [group-chat chat-id chat-name name] :as current-chat} + my-public-key + community + blocked-users-set contacts + inputs]] + (when current-chat + ;; 68 Is the length of the community-id prefix in the `chat-id` + (let [relative-chat-id (some-> current-chat + :chat-id + data-store.chats/community-chat-id->channel-id)] + (cond-> current-chat + + (and (chat.events/community-chat? current-chat) + (get-in community [:chats relative-chat-id :can-post?])) + (assoc :able-to-send-message? true) + + (and (chat.events/group-chat? current-chat) + (member? my-public-key current-chat)) + (assoc :able-to-send-message? true + :member? true) + + (not chat-name) + (assoc :chat-name name) + + (not group-chat) + (assoc + :contact-request-state (get-in contacts [chat-id :contact-request-state]) + :able-to-send-message? + (and + (or (get-in inputs [chat-id :metadata :sending-contact-request]) + (= constants/contact-request-state-mutual + (get-in contacts [chat-id :contact-request-state]))) + (not (contains? blocked-users-set chat-id))))))))) + +(re-frame/reg-sub + :chats/able-to-send-message? + :<- [:chats/current-chat] + (fn [current-chat] + (get current-chat :able-to-send-message?))) + +(re-frame/reg-sub + :chats/chat-type + :<- [:chats/current-chat] + (fn [current-chat] + (condp apply [current-chat] + chat.events/community-chat? :community-chat + chat.events/group-chat? :group-chat + chat.events/public-chat? :public-chat + :chat))) + +(re-frame/reg-sub :chats/current-chat-chat-view + :<- [:chats/current-chat] + (fn [chat] + {:able-to-send-message? (:able-to-send-message? chat) + :admins (:admins chat) + :chat-id (:chat-id chat) + :chat-name (:chat-name chat) + :chat-type (:chat-type chat) + :color (:color chat) + :community-id (:community-id chat) + :contact-request-state (:contact-request-state chat) + :description (:description chat) + :emoji (:emoji chat) + :empty-chat? (not (:last-message chat)) + :group-chat (:group-chat chat) + :invitation-admin (:invitation-admin chat) + :name (:name chat) + :public? (:public? chat) + :synced-from (:synced-from chat) + :synced-to (:synced-to chat)})) + +(re-frame/reg-sub + :chats/current-chat-exist? + :<- [:chats/current-chat-chat-view] + (fn [current-chat] + (boolean (:chat-id current-chat)))) + +(re-frame/reg-sub + :chats/current-chat-color + :<- [:chats/current-raw-chat] + (fn [current-chat] + (:color current-chat))) + +(re-frame/reg-sub + :chats/community-channel-ui-details-by-id + :<- [:chats/chats] + (fn [chats [_ chat-id]] + (select-keys + (get chats chat-id) + [:chat-name + :color + :emoji]))) + +(re-frame/reg-sub + :chats/current-chat-message-list-view-context + :<- [:chats/current-chat-chat-view] + :<- [:communities/current-community] + :<- [:multiaccount/public-key] + :<- [:log-level/debug?] + (fn [[current-chat current-community current-public-key debug-log-level?] [_ in-pinned-view?]] + (let [{:keys [group-chat chat-id public? admins space-keeper able-to-send-message?]} + current-chat + + {:keys [can-delete-message-for-everyone? admin-settings]} + current-community + + {:keys [pin-message-all-members-enabled?]} admin-settings + community? (some? current-community) + group-admin? (contains? admins current-public-key) + community-admin? (get current-community :admin false) + community-member? (get current-community :is-member? false) + + message-pin-enabled + (cond public? false + (not group-chat) true ; one to one chat + ;; in public group or community + group-chat (or group-admin? + pin-message-all-members-enabled? + community-admin?) + :else false)] + {:group-chat group-chat + :group-admin? group-admin? + :public? public? + :community? community? + :community-admin? community-admin? + :current-public-key current-public-key + :debug-log-level? debug-log-level? + :space-keeper space-keeper + :chat-id chat-id + :in-pinned-view? (boolean in-pinned-view?) + :able-to-send-message? able-to-send-message? + :community-member? community-member? + :message-pin-enabled message-pin-enabled + :can-delete-message-for-everyone? can-delete-message-for-everyone?}))) + +(re-frame/reg-sub + :chats/current-chat-one-to-one? + :<- [:chats/current-raw-chat] + (fn [{:keys [chat-type]}] + (= chat-type constants/one-to-one-chat-type))) + +(re-frame/reg-sub + :chats/photo-path + :<- [:contacts/contacts] + :<- [:profile/profile-with-image] + :<- [:mediaserver/port] + :<- [:initials-avatar-font-file] + :<- [:theme] + (fn [[contacts {:keys [public-key] :as multiaccount} port font-file theme] [_ id]] + (let [contact (or (when (= id public-key) multiaccount) + (get contacts id) + (contact.utils/replace-contact-image-uri + {:contact {:public-key id + :customization-color constants/profile-default-color} + :port port + :public-key id + :font-file font-file + :theme theme}))] + (profile.utils/photo contact)))) + +(re-frame/reg-sub + :chats/reply-message + :<- [:chats/current-chat-input] + (fn [{:keys [metadata]}] + (:responding-to-message metadata))) + +(re-frame/reg-sub + :chats/edit-message + :<- [:chats/current-chat-input] + (fn [{:keys [metadata]}] + (:editing-message metadata))) + +(defn filter-selected-contacts + [selected-contacts contacts] + (filter #(:added? (contacts %)) selected-contacts)) + +(re-frame/reg-sub + :selected-contacts-count + :<- [:group/selected-contacts] + :<- [:contacts/contacts] + (fn [[selected-contacts contacts]] + (count (filter-selected-contacts selected-contacts contacts)))) + +(defn filter-contacts + [selected-contacts active-contacts] + (filter #(selected-contacts (:public-key %)) active-contacts)) + +(re-frame/reg-sub + :selected-group-contacts + :<- [:group/selected-contacts] + :<- [:contacts/active] + (fn [[selected-contacts active-contacts]] + (filter-contacts selected-contacts active-contacts))) + +(re-frame/reg-sub + :group-chat/invitations-by-chat-id + :<- [:group-chat/invitations] + (fn [invitations [_ chat-id]] + (filter #(= (:chat-id %) chat-id) (vals invitations)))) + +(re-frame/reg-sub + :chat/mention-suggestions + :<- [:chats/current-chat-id] + :<- [:chats/mention-suggestions] + (fn [[chat-id mentions]] + (take 15 (get mentions chat-id)))) + +(re-frame/reg-sub + :chats/link-previews-unfurled + :<- [:chat/link-previews] + (fn [previews] + (get previews :unfurled))) + +(re-frame/reg-sub + :camera-roll/total-photos-count-android + (fn [{:keys [camera-roll/albums]}] + (->> albums + :my-albums + (reduce + (fn [total-album-count current-album] + (+ total-album-count (:count current-album))) + 0)))) + +(re-frame/reg-sub + :camera-roll/total-photos-count-ios + (fn [{:keys [camera-roll/ios-images-count]}] + ios-images-count)) + +(re-frame/reg-sub + :chats/group-chat-image + (fn [[_ chat-id]] + (re-frame/subscribe [:chats/chat chat-id])) + :-> + :image) + +(re-frame/reg-sub + :group-chat/selected-participants-count + :<- [:group-chat/selected-participants] + (fn [selected-participants] + (count selected-participants))) + +(re-frame/reg-sub + :group-chat/deselected-members-count + :<- [:group-chat/deselected-members] + (fn [deselected-members] + (count deselected-members))) + +(re-frame/reg-sub + :group-chat/member-count + (fn [[_ chat-id]] + (re-frame/subscribe [:chats/chat-by-id chat-id])) + (fn [{:keys [contacts]}] + (count contacts))) + +(re-frame/reg-sub + :group-chat/manage-members-count + (fn [[_ chat-id]] + [(re-frame/subscribe [:group-chat/member-count chat-id]) + (re-frame/subscribe [:group-chat/selected-participants-count]) + (re-frame/subscribe [:group-chat/deselected-members-count])]) + (fn [[member-count selected-count deselected-count]] + (+ member-count selected-count (- deselected-count)))) + +(re-frame/reg-sub + :group-chat/manage-members-error-by-chat-id + :<- [:group-chat/manage-members-error] + (fn [errors [_ chat-id]] + (get errors chat-id))) diff --git a/src/status_im/subs/chats_test.cljs b/src/status_im/subs/chats_test.cljs new file mode 100644 index 00000000000..6f6cf7cc060 --- /dev/null +++ b/src/status_im/subs/chats_test.cljs @@ -0,0 +1,221 @@ +(ns status-im.subs.chats-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + [status-im.constants :as constants] + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def public-key "0xpk") +(def multiaccount {:public-key public-key}) +(def chat-id "1") +(def chat {:chat-id chat-id}) +(def community-id "community-1") +(def community {:community-id community-id}) + +(def private-group-chat + (assoc + chat + :members #{{:id public-key}} + :group-chat true + :chat-type constants/private-group-chat-type)) + +(def community-chat + (assoc + chat + :group-chat true + :community-id community-id + :chat-type constants/community-chat-type)) + +(def one-to-one-chat + (assoc + chat + :chat-type + constants/one-to-one-chat-type)) + +(h/deftest-sub :chats/current-chat + [sub-name] + (testing "private group chat, user is a member" + (let [chats {chat-id private-group-chat}] + (swap! rf-db/app-db assoc + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats) + (is (true? (:able-to-send-message? (rf/sub [sub-name])))))) + (testing "private group chat, user is not member" + (let [chats {chat-id (dissoc private-group-chat :members)}] + (swap! rf-db/app-db assoc + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats) + (is (not (:able-to-send-message? (rf/sub [sub-name])))))) + (testing "one to one chat, mutual contacts" + (let [chats {chat-id one-to-one-chat}] + (swap! rf-db/app-db assoc + :contacts/contacts {chat-id {:contact-request-state constants/contact-request-state-mutual}} + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats) + (is (:able-to-send-message? (rf/sub [sub-name]))))) + (testing "one to one chat, not a contact" + (let [chats {chat-id one-to-one-chat}] + (swap! rf-db/app-db assoc + :contacts/contacts {chat-id {:contact-request-state constants/contact-request-state-sent}} + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats) + (is (not (:able-to-send-message? (rf/sub [sub-name]))))))) + +(h/deftest-sub :chats/current-chat-message-list-view-context + [sub-name] + (testing "reflect :in-pinned-view? in the result" + (reset! rf-db/app-db {}) + (is (true? (:in-pinned-view? (rf/sub [sub-name :in-pinned-view])))) + (is (false? (:in-pinned-view? (rf/sub [sub-name]))))) + (testing "reflect current community in community?" + (let [chats {chat-id community-chat} + communities {community-id community}] + (is (false? (:community? (rf/sub [sub-name])))) + (swap! rf-db/app-db assoc + :communities/enabled? true + :current-chat-id chat-id + :chats chats + :communities communities) + (is (true? (:community? (rf/sub [sub-name])))))) + (testing "community admin" + (let [chats {chat-id community-chat} + communities {community-id (assoc community + :admin true + :can-delete-message-for-everyone? true + :admin-settings {:pin-message-all-members-enabled? false})}] + (is (false? (:community? (rf/sub [sub-name])))) + (swap! rf-db/app-db assoc + :communities/enabled? true + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats + :communities communities) + (is (= chat-id (:chat-id (rf/sub [sub-name])))) + (is (= public-key (:current-public-key (rf/sub [sub-name])))) + (is (true? (:community? (rf/sub [sub-name])))) + (is (true? (:group-chat (rf/sub [sub-name])))) + (is (true? (:community-admin? (rf/sub [sub-name])))) + (is (true? (:can-delete-message-for-everyone? (rf/sub [sub-name])))) + (is (not (:group-admin? (rf/sub [sub-name])))) + (is (true? (:message-pin-enabled (rf/sub [sub-name])))))) + (testing "community member" + (let [chats {chat-id community-chat} + communities {community-id (assoc community + :admin false + :can-delete-message-for-everyone? false + :admin-settings {:pin-message-all-members-enabled? + false})}] + (is (false? (:community? (rf/sub [sub-name])))) + (swap! rf-db/app-db assoc + :communities/enabled? true + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats + :communities communities) + (is (= chat-id (:chat-id (rf/sub [sub-name])))) + (is (= public-key (:current-public-key (rf/sub [sub-name])))) + (is (true? (:community? (rf/sub [sub-name])))) + (is (true? (:group-chat (rf/sub [sub-name])))) + (is (not (:community-admin? (rf/sub [sub-name])))) + (is (not (:can-delete-message-for-everyone? (rf/sub [sub-name])))) + (is (not (:group-admin? (rf/sub [sub-name])))) + (is (not (:message-pin-enabled (rf/sub [sub-name])))))) + (testing "group admin" + (let [chats {chat-id (assoc private-group-chat + :admins + #{public-key})}] + (swap! rf-db/app-db assoc + :communities/enabled? true + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats) + (is (= chat-id (:chat-id (rf/sub [sub-name])))) + (is (= public-key (:current-public-key (rf/sub [sub-name])))) + (is (not (:public? (rf/sub [sub-name])))) + (is (not (:community? (rf/sub [sub-name])))) + (is (true? (:group-chat (rf/sub [sub-name])))) + (is (not (:community-admin? (rf/sub [sub-name])))) + (is (not (:can-delete-message-for-everyone? (rf/sub [sub-name])))) + (is (true? (:group-admin? (rf/sub [sub-name])))) + (is (true? (:message-pin-enabled (rf/sub [sub-name])))))) + (testing "group member" + (let [chats {chat-id (assoc private-group-chat + :admins + #{})}] + (swap! rf-db/app-db assoc + :communities/enabled? true + :profile/profile multiaccount + :current-chat-id chat-id + :chats chats) + (is (= chat-id (:chat-id (rf/sub [sub-name])))) + (is (= public-key (:current-public-key (rf/sub [sub-name])))) + (is (not (:public? (rf/sub [sub-name])))) + (is (not (:community? (rf/sub [sub-name])))) + (is (true? (:group-chat (rf/sub [sub-name])))) + (is (not (:community-admin? (rf/sub [sub-name])))) + (is (not (:can-delete-message-for-everyone? (rf/sub [sub-name])))) + (is (not (:group-admin? (rf/sub [sub-name])))) + (is (not (:message-pin-enabled (rf/sub [sub-name]))))))) + +(h/deftest-sub :chats/community-channel-ui-details-by-id + [sub-name] + (testing "returns specific ui details of a given community channel chat id" + (let [chats {chat-id (assoc community-chat + :color :army + :emoji "🍑" + :chat-name "test")}] + (swap! rf-db/app-db assoc + :chats + chats) + (let [result (rf/sub [sub-name chat-id])] + (is (= 3 (count (keys result)))) + (is (= :army (:color result))) + (is (= "test" (:chat-name result))) + (is (= "🍑" (:emoji result))))))) + +(h/deftest-sub :chats/group-chat-image + [sub-name] + (testing "returns picture for group" + (let [image-data {:uri "data:image/png1234"} + chats {chat-id (assoc community-chat + :color :army + :emoji "🍑" + :chat-name "test" + :image image-data)}] + (swap! rf-db/app-db assoc + :chats + chats) + (let [result (rf/sub [sub-name chat-id])] + (= image-data result))))) + +(h/deftest-sub :chats/current-chat-exist? + [sub-name] + (testing "current chat without chat-id" + (let [chats {chat-id (dissoc one-to-one-chat :chat-id)}] + (swap! rf-db/app-db assoc + :chats chats + :current-chat-id chat-id) + (is (false? (rf/sub [sub-name]))))) + (testing "current chat with chat-id" + (let [chats {chat-id one-to-one-chat}] + (swap! rf-db/app-db assoc + :chats chats + :current-chat-id chat-id) + (is (true? (rf/sub [sub-name])))))) + +(h/deftest-sub :group-chat/manage-members-count + [sub-name] + (testing "calculates the correct count of members in a group chat" + (let [contacts [{:id "0x777"} {:id "0x789"}] + selected-participants ["0x123" "0x456"] + deselected-members [{:id "Ox789"}]] + (swap! rf-db/app-db assoc + :chats {chat-id {:contacts contacts}} + :group-chat/selected-participants selected-participants + :group-chat/deselected-members deselected-members) + (is (= 3 (rf/sub [sub-name chat-id])))))) diff --git a/src/status_im/subs/communities.cljs b/src/status_im/subs/communities.cljs new file mode 100644 index 00000000000..8abbd43599b --- /dev/null +++ b/src/status_im/subs/communities.cljs @@ -0,0 +1,530 @@ +(ns status-im.subs.communities + (:require + [clojure.string :as string] + [legacy.status-im.data-store.chats :as data-store.chats] + [legacy.status-im.ui.screens.profile.visibility-status.utils :as visibility-status-utils] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [status-im.contexts.communities.utils :as utils] + [status-im.contexts.profile.utils :as profile.utils] + [status-im.subs.chat.utils :as subs.utils] + [status-im.subs.contact.utils :as contact.utils] + [utils.i18n :as i18n] + [utils.money :as money])) + +(re-frame/reg-sub + :communities/fetching-community + :<- [:communities/fetching-communities] + (fn [info [_ id]] + (get info id))) + +;; Do not use this subscription directly in views. There is a significant risk +;; of re-rendering views too frequently because an active community can change +;; for numerous reasons. +(re-frame/reg-sub + :communities/community + :<- [:communities] + (fn [communities [_ id]] + (get communities id))) + +(re-frame/reg-sub :communities/logo + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[community]] + (get-in community [:images :thumbnail :uri]))) + +(re-frame/reg-sub :communities/name + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [name]}]] + name)) + +(re-frame/reg-sub :communities/permissions + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [permissions]}]] + permissions)) + +(re-frame/reg-sub + :communities/community-color + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [color]}] _] + color)) + +(re-frame/reg-sub + :communities/community-outro-message + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [outro-message]}] _] + outro-message)) + +(re-frame/reg-sub + :communities/community-joined + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [joined]}] _] + joined)) + +(re-frame/reg-sub + :communities/community-members + (fn [[_ community-id]] + (re-frame/subscribe [:communities/community community-id])) + (fn [community _] + (js-keys (:members community)))) + +(re-frame/reg-sub + :communities/community-chat-members + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [members chats]}] [_ _ chat-id]] + (let [channel-id (data-store.chats/community-chat-id->channel-id chat-id)] + (if (get-in chats [channel-id :token-gated?]) + (get-in chats [channel-id :members]) + members)))) + +(defn- keys->names + [public-keys profile] + (reduce (fn [acc contact-identity] + (assoc acc + contact-identity + (when (= (:public-key profile) contact-identity) + (:primary-name profile) + contact-identity))) + {} + public-keys)) + +(defn- sort-members-by-name-old + [names descending? members] + (if descending? + (sort-by #(get names %) #(compare %2 %1) members) + (sort-by #(get names %) members))) + +(defn- sort-members-by-name + [names members-keys] + (let [forced-last-key "zzzzzz" + sort-keyfn (fn [k] + (if-let [[primary-name secondary-name] (get names k)] + (or (some-> primary-name + string/lower-case) + (some-> secondary-name + string/lower-case)) + ;; Sort unknown keys at the end. + forced-last-key))] + (sort-by sort-keyfn members-keys))) + +;; This implementation is wrong, but since it's only used in a legacy view, we +;; can ignore it for now. +(re-frame/reg-sub :communities/sorted-community-members + (fn [[_ community-id]] + [(re-frame/subscribe [:profile/profile]) + (re-frame/subscribe [:communities/community-members community-id])]) + (fn [[profile members] _] + (let [names (keys->names members profile)] + (->> members + (sort-members-by-name-old names false) + (sort-by #(visibility-status-utils/visibility-status-order (get % 0))))))) + +(re-frame/reg-sub :communities/chat-members + (fn [[_ community-id chat-id]] + [(re-frame/subscribe [:profile/public-key]) + (re-frame/subscribe [:communities/community-chat-members community-id chat-id]) + (re-frame/subscribe [:visibility-status-updates]) + (re-frame/subscribe [:multiaccount/current-user-visibility-status])]) + (fn [[profile-pub-key members-js visibility-status-updates my-status-update] [_ _ _ visibility-status]] + (let [members-keys (js-keys members-js) + online? (fn [public-key] + (let [{visibility-status-type :status-type} + (if (or (string/blank? profile-pub-key) + (= profile-pub-key public-key)) + my-status-update + (get visibility-status-updates public-key))] + (subs.utils/online? visibility-status-type)))] + (filter (if (= :online visibility-status) + online? + (complement online?)) + members-keys)))) + +(defn- names-by-key + [contacts profile public-keys] + (let [names (reduce (fn [acc k] + (if-let [contact (get contacts k)] + (assoc acc k (contact.utils/contact-two-names contact profile)) + acc)) + {} + public-keys)] + (assoc names + (:public-key profile) + [(profile.utils/displayed-name profile) nil]))) + +;; This is a potentially expensive subscription because we don't control how +;; many members and contacts exist in the app-db. Future improvements include +;; removing members from the payload and paginating them from status-go. +(re-frame/reg-sub :communities/chat-members-sorted + (fn [[_ community-id chat-id visibility-status]] + [(re-frame/subscribe [:profile/profile]) + (re-frame/subscribe [:contacts/contacts-raw]) + (re-frame/subscribe [:communities/chat-members community-id chat-id visibility-status])]) + (fn [[profile contacts ^js members-keys]] + (sort-members-by-name (names-by-key contacts profile members-keys) + members-keys))) + +(re-frame/reg-sub + :communities/featured-contract-communities + :<- [:contract-communities] + (fn [contract-communities] + (sort-by :name (vals (:featured contract-communities))))) + +(re-frame/reg-sub + :communities/other-contract-communities + :<- [:contract-communities] + (fn [contract-communities] + (sort-by :name (vals (:other contract-communities))))) + +(def memo-communities-stack-items (atom nil)) + +(defn- group-communities-by-status + [requests + {:keys [id] + :as community}] + (cond + (:joined community) :joined + (boolean (get requests id)) :pending + (:spectated community) :opened + :else :other)) + +(re-frame/reg-sub + :communities/grouped-by-status + :<- [:view-id] + :<- [:communities] + :<- [:communities/my-pending-requests-to-join] + ;; Return communities splitted by level of user participation. Some communities user already + ;; joined, to some of them join request sent and others were opened one day and their data remained + ;; in app-db. Result map has form: {:joined [id1, id2] :pending [id3, id5] :opened [id4]}" + (fn [[view-id communities requests]] + (if (or (empty? @memo-communities-stack-items) (= view-id :screen/communities-stack)) + (let [grouped-communities (->> communities + vals + ;; Remove data that can grow fast or is + ;; reliably not needed to list communities. + ;; We could use an allowlist of keys for + ;; optimal performance of this sub, but + ;; that's harder to maintain in case we miss + ;; any key. + (map #(dissoc % :members :chats :token-permissions :tokens-metadata)) + (group-by #(group-communities-by-status requests %)) + (map (fn [[k v]] + {k (sort-by (fn [{:keys [requested-to-join-at last-opened-at + joined-at]}] + (condp = k + :joined joined-at + :pending requested-to-join-at + :opened last-opened-at + last-opened-at)) + #(compare %2 %1) + v)})) + (into {}))] + (reset! memo-communities-stack-items grouped-communities) + grouped-communities) + @memo-communities-stack-items))) + +(defn community->home-item + [community counts] + {:name (:name community) + :muted? (:muted community) + :unread-messages? (pos? (:unviewed-messages-count counts)) + :unread-mentions-count (:unviewed-mentions-count counts) + :community-icon (:images community)}) + +(re-frame/reg-sub + :communities/my-pending-request-to-join + :<- [:communities/my-pending-requests-to-join] + (fn [requests [_ community-id]] + (:id (get requests community-id)))) + +(re-frame/reg-sub + :communities/current-community + :<- [:communities] + :<- [:chats/current-raw-chat] + (fn [[communities {:keys [community-id]}]] + (get communities community-id))) + +(defn calculate-unviewed-counts + [chats] + (reduce (fn [acc {:keys [unviewed-mentions-count unviewed-messages-count muted]}] + (if-not muted + (-> acc + (update :unviewed-messages-count + unviewed-messages-count) + (update :unviewed-mentions-count + unviewed-mentions-count)) + acc)) + {:unviewed-messages-count 0 + :unviewed-mentions-count 0} + chats)) + +(re-frame/reg-sub + :communities/unviewed-counts + (fn [[_ community-id]] + [(re-frame/subscribe [:chats/by-community-id community-id])]) + (fn [[chats]] + (calculate-unviewed-counts chats))) + +(re-frame/reg-sub + :communities/requests-to-join-for-community + :<- [:communities/requests-to-join] + (fn [requests [_ community-id]] + (->> + (get requests community-id {}) + vals + (filter (fn [{:keys [state]}] + (= state constants/request-to-join-pending-state)))))) + +(defn- reduce-over-categories + [community-id + categories + collapsed-categories + full-chats-data] + (fn [acc + [_ + {:keys [name + categoryID + position + id + emoji + can-view? + can-post? + token-gated? + hide-if-permissions-not-met?]}]] + (let [category-id (if (seq categoryID) categoryID constants/empty-category-id) + {:keys [unviewed-messages-count + unviewed-mentions-count + muted + color]} (get full-chats-data + (str community-id id)) + acc-with-category (if (get acc category-id) + acc + (assoc acc + category-id + (assoc + (or (get categories category-id) + {:name (i18n/label :t/none)}) + :collapsed? (get collapsed-categories + category-id) + :chats []))) + locked? (when token-gated? + (and (not can-view?) + (not can-post?))) + categorized-chat {:name name + :emoji emoji + :muted? muted + :unread-messages? (pos? unviewed-messages-count) + :position position + :mentions-count (or unviewed-mentions-count 0) + :can-post? can-post? + ;; NOTE: this is a troolean nil->no permissions, true->no access, + ;; false -> has access + :locked? locked? + :hide-if-permissions-not-met? (and hide-if-permissions-not-met? locked?) + :id id + :color color}] + (update-in acc-with-category [category-id :chats] conj categorized-chat)))) + +(re-frame/reg-sub + :communities/categorized-channels + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id]) + (re-frame/subscribe [:chats/chats]) + (re-frame/subscribe [:communities/collapsed-categories-for-community community-id])]) + (fn [[{:keys [categories chats]} full-chats-data collapsed-categories] + [_ community-id]] + (let [reduce-fn (reduce-over-categories + community-id + categories + collapsed-categories + full-chats-data) + categories-and-chats + (->> chats + (reduce reduce-fn {}) + (sort-by (comp :position second)) + (map (fn [[k v]] + [k + (-> v + (update :chats #(sort-by :position %)) + (update :chats + #(filter (comp not :hide-if-permissions-not-met?) + %)))])))] + categories-and-chats))) + +(def ^:private channel-separator-item [{:render-as :separator}]) + +(defn- get-category-item + ([category] + (get-category-item category nil)) + ([category category-id] + (let [render-as (if (= category-id :communities/not-categorized) :nothing :category)] + (-> category + (dissoc :chats) + (assoc :render-as render-as) + vector)))) + +(defn- mark-as-channel + [channel] + (assoc channel :render-as :channel)) + +(defn- category-with-channels + [category-id {:keys [chats] :as category}] + (let [category-item (get-category-item category category-id) + channels (map mark-as-channel chats)] + (concat category-item channels channel-separator-item))) + +(re-frame/reg-sub + :communities/flatten-channels-and-categories + (fn [[_ community-id]] + (re-frame/subscribe [:communities/categorized-channels community-id])) + (fn [categorized-channels [_ _community-id]] + (mapcat (fn [[category-id {:keys [collapsed? chats] :as category}]] + (when (seq chats) + (if collapsed? + (get-category-item category) + (category-with-channels category-id category)))) + categorized-channels))) + +(re-frame/reg-sub + :communities/community-overview + (fn [[_ community-id]] + (re-frame/subscribe [:communities/community community-id])) + (fn [{:keys [joined spectated images description color activeMembersCount tags + permissions role-permissions?] + :as community}] + (when community + {:joined? joined + :spectated? spectated + :cover-image (-> images :banner :uri) + :logo (-> images :large :uri) + :community-name (:name community) + :description description + :color color + :active-members-count activeMembersCount + :tags tags + :permissions permissions + :role-permissions? role-permissions?}))) + +(re-frame/reg-sub + :communities/collapsed-categories-for-community + :<- [:communities/collapsed-categories] + (fn [collapsed-categories [_ community-id]] + (get collapsed-categories community-id))) + +(defn token-requirement->token + [checking-permissions? + token-images + {:keys [satisfied criteria]}] + (let [sym (:symbol criteria) + amount-in-wei (:amountInWei criteria) + decimals (:decimals criteria)] + {:symbol sym + :sufficient? satisfied + :collectible? (= (:type criteria) constants/community-token-type-erc721) + :loading? checking-permissions? + :amount (money/to-fixed (money/token->unit amount-in-wei decimals)) + :img-src (get token-images sym)})) + +(re-frame/reg-sub + :communities/checking-permissions-by-id + :<- [:communities/permissions-check] + (fn [permissions [_ id]] + (get permissions id))) + + +(re-frame/reg-sub + :communities/checking-permissions-all-by-id + :<- [:communities/permissions-check-all] + (fn [permissions [_ id]] + (get permissions id))) + +(re-frame/reg-sub + :community/token-gated-overview + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id]) + (re-frame/subscribe [:communities/checking-permissions-by-id community-id])]) + (fn [[{:keys [token-images]} + {:keys [checking? check]}] _] + (let [highest-role (:highestRole check) + networks-not-supported? (:networksNotSupported check) + lowest-role (last (:roles check)) + highest-permission-role (:type highest-role) + can-request-access? (and (boolean highest-permission-role) (not networks-not-supported?))] + {:can-request-access? can-request-access? + :checking? checking? + :highest-permission-role highest-permission-role + :networks-not-supported? networks-not-supported? + :no-member-permission? (and highest-permission-role + (not (-> check :highestRole :criteria))) + :tokens (map (fn [{:keys [tokenRequirement]}] + (map + (partial token-requirement->token + checking? + token-images) + tokenRequirement)) + (or (:criteria highest-role) + (:criteria lowest-role)))}))) + +(re-frame/reg-sub + :community/images + :<- [:communities] + (fn [communities [_ id]] + (get-in communities [id :images]))) + +(re-frame/reg-sub + :communities/rules + :<- [:communities] + (fn [communities [_ community-id]] + (get-in communities [community-id :intro-message]))) + +(re-frame/reg-sub + :communities/token-images-by-symbol + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [tokens-metadata]}] _] + (->> tokens-metadata + (map (fn [{sym :symbol image :image}] + {sym image})) + (into {})))) + +(re-frame/reg-sub + :community/token-permissions + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id]) + (re-frame/subscribe [:communities/checking-permissions-all-by-id community-id])]) + (fn [[{:keys [token-images]} + {:keys [checking? check]}] _] + (let [roles (:roles check) + member-and-satisifed-roles (filter #(or (= (:type %) + constants/community-token-permission-become-member) + (:satisfied %)) + roles)] + (mapv (fn [role] + {:role (:type role) + :role-text (i18n/label (utils/role->translation-key (:type role))) + :satisfied? (:satisfied role) + :tokens (map (fn [{:keys [tokenRequirement]}] + (map + (partial token-requirement->token + checking? + token-images) + tokenRequirement)) + (:criteria role))}) + member-and-satisifed-roles)))) + +(re-frame/reg-sub + :communities/has-permissions? + (fn [[_ community-id]] + [(re-frame/subscribe [:community/token-permissions community-id])]) + (fn [[permissions] _] + (let [all-tokens (apply concat (map :tokens permissions))] + (boolean (some seq all-tokens))))) + +(re-frame/reg-sub + :communities/join-requests-for-signing-by-id + :<- [:communities/join-requests-for-signing] + (fn [sign-requests [_ id]] + (get sign-requests id))) diff --git a/src/status_im/subs/communities_test.cljs b/src/status_im/subs/communities_test.cljs new file mode 100644 index 00000000000..6acb183e1a6 --- /dev/null +++ b/src/status_im/subs/communities_test.cljs @@ -0,0 +1,754 @@ +(ns status-im.subs.communities-test + (:require + [cljs.test :refer [is testing]] + matcher-combinators.test + [re-frame.db :as rf-db] + [status-im.constants :as constants] + status-im.subs.communities + [status-im.subs.profile-test :as profile-test] + [test-helpers.unit :as h] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(def community-id "0x02b5bdaf5a25fcfe2ee14c501fab1836b8de57f61621080c3d52073d16de0d98d6") +(def channel-id "0x1-channel-id") +(def chat-id (str community-id channel-id)) + +(h/deftest-sub :communities + [sub-name] + (testing "returns raw communities" + (let [raw-communities {"0x1" {:id "0x1"}}] + (swap! rf-db/app-db assoc + :communities + raw-communities) + (is (match? raw-communities (rf/sub [sub-name])))))) + +(h/deftest-sub :communities/unviewed-counts + [sub-name] + (testing "sums counts for a particular community" + (swap! rf-db/app-db assoc + :chats + {"0x100" {:community-id community-id + :unviewed-mentions-count 3 + :unviewed-messages-count 2} + "0x101" {:community-id "0x2" + :unviewed-mentions-count 7 + :unviewed-messages-count 9} + "0x102" {:community-id community-id + :unviewed-mentions-count 5 + :unviewed-messages-count 1}}) + (is (match? {:unviewed-messages-count 3 + :unviewed-mentions-count 8} + (rf/sub [sub-name community-id])))) + + (testing "defaults to zero when count keys are not present" + (swap! rf-db/app-db assoc + :chats + {"0x100" {:community-id community-id}}) + (is (match? {:unviewed-messages-count 0 + :unviewed-mentions-count 0} + (rf/sub [sub-name community-id]))))) + +(h/deftest-sub :communities/categorized-channels + [sub-name] + (testing "Channels with categories" + (swap! rf-db/app-db assoc + :communities + {"0x1" {:id "0x1" + :chats {"0x1" + {:id "0x1" + :position 1 + :name "chat1" + :muted? nil + :categoryID "1" + :token-gated? false + :can-post? true} + "0x2" + {:id "0x2" + :position 2 + :name "chat2" + :muted? nil + :categoryID "1" + :token-gated? true + :can-post? false} + "0x3" + {:id "0x3" + :position 3 + :name "chat3" + :muted? nil + :categoryID "2" + :token-gated? true + :can-post? true}} + :categories {"1" {:id "1" + :position 2 + :name "category1"} + "2" {:id "2" + :position 1 + :name "category2"}} + :joined true}}) + (is + (match? [["2" + {:id "2" + :name "category2" + :collapsed? nil + :position 1 + :chats [{:name "chat3" + :position 3 + :emoji nil + :muted? nil + :locked? false + :id "0x3" + :unread-messages? false + :mentions-count 0}]}] + ["1" + {:id "1" + :name "category1" + :collapsed? nil + :position 2 + :chats [{:name "chat1" + :emoji nil + :position 1 + :muted? nil + :locked? nil + :id "0x1" + :unread-messages? false + :mentions-count 0} + {:name "chat2" + :emoji nil + :position 2 + :muted? nil + :locked? true + :id "0x2" + :unread-messages? false + :mentions-count 0}]}]] + (rf/sub [sub-name "0x1"])))) + (testing "Channels without categories" + (swap! rf-db/app-db assoc + :communities + {"0x1" {:id "0x1" + :chats {"0x1" + {:id "0x1" + :position 1 + :name "chat1" + :categoryID "1" + :can-post? true + :muted? nil} + "0x2" + {:id "0x2" + :position 2 + :name "chat2" + :categoryID "1" + :can-post? false + :muted? nil} + "0x3" {:id "0x3" :position 3 :name "chat3" :can-post? true :muted? nil}} + :categories {"1" {:id "1" + :position 1 + :name "category1"} + "2" {:id "2" + :position 2 + :name "category2"}} + :joined true}}) + (is + (match? + [[constants/empty-category-id + {:name (i18n/label :t/none) + :collapsed? nil + :chats [{:name "chat3" + :emoji nil + :position 3 + :locked? nil + :muted? nil + :id "0x3" + :unread-messages? false + :mentions-count 0}]}] + ["1" + {:name "category1" + :id "1" + :position 1 + :collapsed? nil + :chats [{:name "chat1" + :emoji nil + :position 1 + :locked? nil + :muted? nil + :id "0x1" + :unread-messages? false + :mentions-count 0} + {:name "chat2" + :emoji nil + :position 2 + :locked? nil + :id "0x2" + :muted? nil + :unread-messages? false + :mentions-count 0}]}]] + (rf/sub [sub-name "0x1"])))) + + (testing "Unread messages" + (swap! rf-db/app-db assoc + :communities + {"0x1" {:id "0x1" + :chats {"0x1" + {:id "0x1" :position 1 :name "chat1" :categoryID "1" :can-post? true} + "0x2" + {:id "0x2" :position 2 :name "chat2" :categoryID "1" :can-post? false}} + :categories {"1" {:id "1" :name "category1"}} + :joined true}} + :chats + {"0x10x1" {:unviewed-messages-count 1 :unviewed-mentions-count 2} + "0x10x2" {:unviewed-messages-count 0 :unviewed-mentions-count 0}}) + (is + (match? [["1" + {:name "category1" + :id "1" + :collapsed? nil + :chats [{:name "chat1" + :emoji nil + :position 1 + :locked? nil + :id "0x1" + :muted? nil + :unread-messages? true + :mentions-count 2} + {:name "chat2" + :emoji nil + :position 2 + :locked? nil + :muted? nil + :id "0x2" + :unread-messages? false + :mentions-count 0}]}]] + (rf/sub [sub-name "0x1"]))))) + +(h/deftest-sub :communities/my-pending-requests-to-join + [sub-name] + (testing "no requests" + (swap! rf-db/app-db assoc + :communities/my-pending-requests-to-join + {}) + (is (match? {} + (rf/sub [sub-name])))) + (testing "users requests to join different communities" + (swap! rf-db/app-db assoc + :communities/my-pending-requests-to-join + {:community-id-1 {:id :request-id-1} + :community-id-2 {:id :request-id-2}}) + (is (match? {:community-id-1 {:id :request-id-1} + :community-id-2 {:id :request-id-2}} + (rf/sub [sub-name]))))) + +(h/deftest-sub :communities/my-pending-request-to-join + [sub-name] + (testing "no request for community" + (swap! rf-db/app-db assoc + :communities/my-pending-requests-to-join + {}) + (is (match? nil + (rf/sub [sub-name :community-id-1])))) + (testing "users request to join a specific communities" + (swap! rf-db/app-db assoc + :communities/my-pending-requests-to-join + {:community-id-1 {:id :request-id-1} + :community-id-2 {:id :request-id-2}}) + (is (match? :request-id-1 + (rf/sub [sub-name :community-id-1]))))) + +(h/deftest-sub :community/token-gated-overview + [sub-name] + (let + [checking-permissions? true + token-image-eth "data:image/jpeg;base64,/9j/2w" + checks {:checking? checking-permissions? + :check + {:satisfied true + :highestRole + {:type constants/community-token-permission-become-admin + :criteria [{:tokenRequirement [{:satisfied true + :criteria {:contract_addresses + {:5 "0x0"} + :type 1 + :symbol "DAI" + :amountInWei + "5000000000000000000" + :amount "5.0" + :decimals 18}}]} + {:tokenRequirement [{:satisfied false + :criteria {:type 1 + :symbol "ETH" + :amountInWei "2000000000000000" + :amount "0.002" + :decimals 18}}]}]} + + :permissions + {:a3dd5b6b-d93b-452c-b22a-09a8f42ec566 {:criteria [true false + true]}} + :validCombinations + [{:address "0xd722eaa60dc73e334b588d34ba66a3b27e537783" + :chainIds nil} + {:address "0x738d3146831c5871fa15872b409e8f360e341784" + :chainIds [5 420]}]}} + community {:id community-id + :checking-permissions? checking-permissions? + :permissions {:access 3} + :highest-permission-role constants/community-token-permission-become-admin + :token-images {"ETH" token-image-eth} + :name "Community super name" + :chats {"89f98a1e-6776-4e5f-8626-8ab9f855253f" + {:description "x" + :emoji "🎲" + :permissions {:access 1} + :color "#88B0FF" + :name "random" + :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" + :id "89f98a1e-6776-4e5f-8626-8ab9f855253f" + :position 4 + :can-post? false + :members {"0x04" {"roles" [1]}}} + "a076358e-4638-470e-a3fb-584d0a542ce6" + {:description "General channel for the community" + :emoji "🐷 " + :permissions {:access 1} + :color "#4360DF" + :name "general" + :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" + :id "a076358e-4638-470e-a3fb-584d0a542ce6" + :position 0 + :can-post? false + :members {"0x04" {"roles" [1]}}}} + :members {"0x04" {"roles" [1]}} + :can-request-access? false + :outro-message "bla" + :verified false}] + (swap! rf-db/app-db assoc-in [:communities community-id] community) + (swap! rf-db/app-db assoc-in [:communities/permissions-check community-id] checks) + (is (match? {:can-request-access? true + :networks-not-supported? nil + :tokens [[{:symbol "DAI" + :amount "5" + :collectible? false + :sufficient? true + :loading? checking-permissions?}] + [{:symbol "ETH" + :amount "0.002" + :collectible? false + :sufficient? false + :loading? checking-permissions? + :img-src token-image-eth}]]} + (rf/sub [sub-name community-id]))))) + +(h/deftest-sub :communities/community-color + [sub-name] + (testing "returns the community color" + (let [community-color "#FEFEFE"] + (swap! rf-db/app-db assoc + :communities + {community-id {:color community-color}}) + (is (match? community-color (rf/sub [sub-name community-id])))))) + +(h/deftest-sub :communities/token-images-by-symbol + [sub-name] + (testing + "returns a map keyed by the images of tokens/collectibles + And has data-uri as it's values" + (swap! rf-db/app-db assoc-in + [:communities community-id :tokens-metadata] + [{:contract-addresses {:420 "0x1"} + :image "data:image/jpeg;base64,/9j/2wCEAAYEBQYFBAYGBQYH" + :tokenType 2 + :symbol "DOGE" + :name "Doge Coin coll"} + {:contract-addresses {:420 "0x1"} + :image "data:image/jpeg;base64,/9j/2wCEAAYEBQYFBAYGBQYH" + :tokenType 2 + :symbol "BTC" + :name "Bitcoin coll"}]) + (is (match? {"DOGE" "data:image/jpeg;base64,/9j/2wCEAAYEBQYFBAYGBQYH" + "BTC" "data:image/jpeg;base64,/9j/2wCEAAYEBQYFBAYGBQYH"} + (rf/sub [sub-name community-id]))))) + +(h/deftest-sub :community/token-permissions + [sub-name] + (testing + "with visible permissions" + (let + [checking? false + token-image-eth "data:image/jpeg;base64,/9j/2w" + checks + {:checking? checking? + :check + {:roles + [{:type 1 + :satisfied true + :criteria [{:roles 1 + :tokenRequirement [{:satisfied true + :criteria {:contract_addresses + {:421614 + 0x0000000000000000000000000000000000000000 + :11155111 + 0x0000000000000000000000000000000000000000 + :11155420 + 0x0000000000000000000000000000000000000000} + :type 1 + :symbol "ETH" + :amount 1 + :decimals 18 + :amountInWei 1000000000000000000}}] + :criteria [true]}]} + {:type 2 + :satisfied false + :criteria [{:roles 2 + :tokenRequirement [{:satisfied false + :criteria {:contract_addresses + {:11155111 + 0x3e622317f8c93f7328350cf0b56d9ed4c620c5d6} + :type 1 + :symbol "DAI" + :amount 10 + :decimals 18 + :amountInWei 10000000000000000000}}] + :criteria [false]}]}]} + } + community {:id community-id + :checking-permissions? checking? + :token-images {"ETH" token-image-eth} + :name "Community super name" + :can-request-access? false + :outro-message "bla" + :verified false}] + (swap! rf-db/app-db assoc-in [:communities community-id] community) + (swap! rf-db/app-db assoc-in [:communities/permissions-check-all community-id] checks) + (is + (match? [{:role 1 + :satisfied? true + :tokens + [[{:symbol "ETH" + :sufficient? true + :loading? false + :collectible? false + :amount "1" + :img-src token-image-eth}]]} + {:role 2 + :satisfied? false + :tokens [[{:symbol "DAI" + :sufficient? false + :collectible? false + :loading? false + :amount "10" + :img-src nil}]]}] + (rf/sub [sub-name community-id]))))) + (testing + "without any visible permissions" + (let + [checking? false + token-image-eth "data:image/jpeg;base64,/9j/2w" + checks + {:checking? checking? + :check + {:roles []} + } + community {:id community-id + :checking-permissions? checking? + :token-images {"ETH" token-image-eth} + :name "Community super name" + :can-request-access? false + :outro-message "bla" + :verified false}] + (swap! rf-db/app-db assoc-in [:communities community-id] community) + (swap! rf-db/app-db assoc-in [:communities/permissions-check-all community-id] checks) + (is + (match? [] + (rf/sub [sub-name community-id])))))) + +(h/deftest-sub :communities/chat-members-sorted + [sub-name] + (let [token-image-eth "data:image/jpeg;base64,/9j/2w" + channel-id-1 "89f98a1e-6776-4e5f-8626-8ab9f855253f" + channel-id-2 "a076358e-4638-470e-a3fb-584d0a542ce6" + chat-id-2 (str community-id channel-id-2) + + member-id-1 "0x01" + member-id-2 "0x02" + + visibility-status-updates + {member-id-1 {:status-type constants/visibility-status-always-online} + member-id-2 {:status-type constants/visibility-status-always-online}} + + contacts + {member-id-1 {:display-name "John Marston"} + member-id-2 {:display-name "Arthur Morgan"}} + + community {:id community-id + :permissions {:access 3} + :token-images {"ETH" token-image-eth} + :name "Community super name" + :chats {channel-id-1 + {:description "x" + :emoji "🎲" + :permissions {:access 1} + :color "#88B0FF" + :name "random" + :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" + :id channel-id-1 + :position 4 + :can-post? false + :members nil} + channel-id-2 + {:description "General channel for the community" + :emoji "🥔" + :permissions {:access 1} + :color "#4360DF" + :name "general" + :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" + :id channel-id-2 + :position 0 + :token-gated? true + :can-post? false + :members (clj->js {member-id-1 {"roles" [1]} + member-id-2 {"roles" [1]} + "0x05" {"roles" [1]}})}} + :members (js->clj {member-id-1 {"roles" [1]} + member-id-2 {"roles" [1]} + "0x03" {"roles" [1]} + "0x04" {"roles" [1]}})}] + (testing "returns sorted community members who are online" + (swap! rf-db/app-db assoc :contacts/contacts contacts) + (swap! rf-db/app-db assoc-in [:communities community-id] community) + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (is (= [member-id-2 member-id-1] + (rf/sub [sub-name community-id chat-id-2 :online])))) + + (testing "returns sorted community members per offline status" + (swap! rf-db/app-db assoc-in [:communities community-id] community) + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (is (= ["0x05"] (rf/sub [sub-name community-id chat-id-2 :offline])))))) + +(h/deftest-sub :communities/chat-members + [sub-name] + (let [member-1-id "0x1-member" + member-2-id "0x2-member" + + visibility-status-updates + {member-1-id {:status-type constants/visibility-status-always-online} + member-2-id {:status-type constants/visibility-status-always-online}} + + communities + {community-id {:id community-id + :chats {channel-id {:token-gated? false + :members (clj->js {member-2-id {}})}} + :members (clj->js {member-1-id {} + member-2-id {}})}}] + (testing "members from non token-gated channels and online" + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :communities communities) + + ;; When channel is not token-gated, all community members are considered. + (is (= [member-1-id member-2-id] + (rf/sub [sub-name community-id chat-id :online])))) + + (testing "members from token-gated channels and online" + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc + :communities + (assoc-in communities [community-id :chats channel-id :token-gated?] true)) + + ;; When channel is token-gated, only its members are considered. + (is (= [member-2-id] + (rf/sub [sub-name community-id chat-id :online])))) + + (testing "members from token-gated channels and offline" + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc + :communities + (assoc-in communities [community-id :chats channel-id :token-gated?] true)) + + (is (= [member-2-id] (rf/sub [sub-name community-id chat-id :offline])))) + + (testing "members from non token-gated channels and offline" + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :communities communities) + + (is (= [member-1-id member-2-id] + (rf/sub [sub-name community-id chat-id :offline])))))) + +(h/deftest-sub :communities/flatten-channels-and-categories + [sub-name] + (let [none-category {:name "None" :collapsed? nil :render-as :nothing} + separator {:render-as :separator} + chat (fn [{:keys [id chat-name position]}] + {:emoji nil + :muted? nil + :color nil + :name chat-name + :mentions-count 0 + :unread-messages? false + :hide-if-permissions-not-met? nil + :render-as :channel + :id id + :locked? false + :position position + :can-post? true}) + category (fn [{:keys [id category-name position collapsed?]}] + {:id id + :position position + :name category-name + :collapsed? collapsed? + :render-as :category})] + (testing "A list with the categories and channels" + (swap! rf-db/app-db assoc + :communities + {"0x1" {:id "0x1" + :chats {"0x1" {:id "0x1" + :position 1 + :name "chat1" + :muted? nil + :categoryID "1" + :token-gated? true + :can-post? true} + "0x2" {:id "0x2" + :position 2 + :name "chat2" + :muted? nil + :categoryID "1" + :token-gated? true + :can-post? true} + "0x3" {:id "0x3" + :position 3 + :name "chat3" + :muted? nil + :categoryID "2" + :token-gated? true + :can-post? true} + "0x4" {:id "0x4" + :position 4 + :name "chat4" + :muted? nil + :categoryID "" + :token-gated? true + :can-post? true}} + :categories {"1" {:id "1" + :position 2 + :name "category1"} + "2" {:id "2" + :position 1 + :name "category2"}} + :joined true}}) + (is + (= [none-category + (chat {:id "0x4" + :chat-name "chat4" + :position 4}) + separator + (category {:id "2" + :category-name "category2" + :position 1}) + (chat {:id "0x3" + :chat-name "chat3" + :position 3}) + separator + (category {:id "1" + :category-name "category1" + :position 2}) + (chat {:id "0x1" + :chat-name "chat1" + :position 1}) + (chat {:id "0x2" + :chat-name "chat2" + :position 2}) + separator] + (rf/sub [sub-name "0x1"])))) + + (testing "All chats are uncategorized" + (swap! rf-db/app-db assoc + :communities + {"0x1" {:id "0x1" + :chats {"0x1" {:id "0x1" + :position 1 + :name "chat1" + :muted? nil + :categoryID "" + :token-gated? true + :can-post? true} + "0x2" {:id "0x2" + :position 2 + :name "chat2" + :muted? nil + :categoryID "" + :token-gated? true + :can-post? true} + "0x3" {:id "0x3" + :position 3 + :name "chat3" + :muted? nil + :categoryID "" + :token-gated? true + :can-post? true}} + :joined true}}) + (is (= [none-category + (chat {:id "0x1" + :chat-name "chat1" + :position 1}) + (chat {:id "0x2" + :chat-name "chat2" + :position 2}) + (chat {:id "0x3" + :chat-name "chat3" + :position 3}) + separator] + (rf/sub [sub-name "0x1"])))) + + (testing "Collapsed categories don't include their channels" + (swap! rf-db/app-db assoc + :communities/collapsed-categories {"0x1" {"1" true "2" true}} + :communities + {"0x1" {:id "0x1" + :chats {"0x1" {:id "0x1" + :position 1 + :name "chat1" + :muted? nil + :categoryID "1" + :token-gated? false + :can-post? true} + "0x2" {:id "0x2" + :position 2 + :name "chat2" + :muted? nil + :categoryID "2" + :token-gated? true + :can-post? false} + "0x3" {:id "0x3" + :position 3 + :name "chat3" + :muted? nil + :categoryID "3" + :token-gated? true + :can-post? true}} + :categories {"1" {:id "1" + :position 1 + :name "category1"} + "2" {:id "2" + :position 2 + :name "category2"} + "3" {:id "3" + :position 3 + :name "category3"}} + :joined true}}) + (is (= [(category {:id "1" + :category-name "category1" + :position 1 + :collapsed? true}) + (category {:id "2" + :category-name "category2" + :position 2 + :collapsed? true}) + (category {:id "3" + :category-name "category3" + :position 3}) + (chat {:id "0x3" + :chat-name "chat3" + :position 3}) + separator] + (rf/sub [sub-name "0x1"])))))) diff --git a/src/status_im/subs/community/account_selection.cljs b/src/status_im/subs/community/account_selection.cljs new file mode 100644 index 00000000000..8bc3bbb5ae0 --- /dev/null +++ b/src/status_im/subs/community/account_selection.cljs @@ -0,0 +1,74 @@ +(ns status-im.subs.community.account-selection + (:require + [re-frame.core :as re-frame])) + +;; This sub is particularly useful in context tags in page-nav or drawer-top +;; components. It should be preferred instead of subscribing to the whole +;; community. +(re-frame/reg-sub :communities/for-context-tag + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [name images color]}]] + {:name name + :logo (get-in images [:thumbnail :uri]) + :color color})) + +(re-frame/reg-sub :communities/addresses-to-reveal + :<- [:communities/all-addresses-to-reveal] + (fn [addresses-by-id [_ community-id]] + (get addresses-by-id community-id))) + +(re-frame/reg-sub :communities/airdrop-address + :<- [:communities/all-airdrop-addresses] + (fn [addresses-by-id [_ community-id]] + (get addresses-by-id community-id))) + +(re-frame/reg-sub :communities/share-all-addresses? + :<- [:communities/selected-share-all-addresses] + (fn [flags-by-id [_ community-id]] + (get flags-by-id community-id))) + +(re-frame/reg-sub :communities/can-edit-shared-addresses? + (fn [[_ community-id]] + (re-frame/subscribe [:communities/community community-id])) + (fn [{:keys [joined]}] + joined)) + +(re-frame/reg-sub :communities/permissions-check-for-selection + :<- [:communities/permissions-checks-for-selection] + (fn [permissions [_ id]] + (get permissions id))) + +(re-frame/reg-sub :communities/permissions-check-for-selection-checking? + (fn [[_ community-id]] + (re-frame/subscribe [:communities/permissions-check-for-selection community-id])) + (fn [check] + (:checking? check))) + +(re-frame/reg-sub :communities/highest-role-for-selection + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/permissions-check-for-selection community-id])]) + (fn [[permissions-check] _] + (get-in permissions-check [:check :highestRole :type]))) + +(re-frame/reg-sub :communities/accounts-to-reveal + (fn [[_ community-id]] + [(re-frame/subscribe [:wallet/operable-accounts]) + (re-frame/subscribe [:communities/addresses-to-reveal community-id])]) + (fn [[accounts addresses] _] + (filter #(contains? addresses (:address %)) + accounts))) + +(re-frame/reg-sub :communities/airdrop-account + (fn [[_ community-id]] + [(re-frame/subscribe [:wallet/operable-accounts]) + (re-frame/subscribe [:communities/airdrop-address community-id])]) + (fn [[accounts airdrop-address] _] + (->> accounts + (filter #(= (:address %) airdrop-address)) + first))) + +(re-frame/reg-sub :communities/permissioned-balances-by-address + :<- [:communities/permissioned-balances] + (fn [balances [_ community-id account-address]] + (get-in balances [community-id (keyword account-address)]))) diff --git a/src/status_im/subs/community/account_selection_test.cljs b/src/status_im/subs/community/account_selection_test.cljs new file mode 100644 index 00000000000..461215f63ed --- /dev/null +++ b/src/status_im/subs/community/account_selection_test.cljs @@ -0,0 +1,47 @@ +(ns status-im.subs.community.account-selection-test + (:require + [cljs.test :refer [is]] + matcher-combinators.test + [re-frame.db :as rf-db] + status-im.subs.communities + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def community-id "0x1") + +(h/deftest-sub :communities/for-context-tag + [sub-name] + (reset! rf-db/app-db + {:communities + {community-id {:name "foo" + :images {:thumbnail {:uri "data:image/png;"}} + :color :orange}}}) + + (is (match? {:name "foo" + :logo "data:image/png;" + :color :orange} + (rf/sub [sub-name community-id])))) + +(h/deftest-sub :communities/airdrop-account + [sub-name] + (let [airdrop-account {:address "0xA" :operable? true :position 1}] + (reset! rf-db/app-db + {:communities/all-airdrop-addresses {community-id "0xA"} + :wallet {:accounts {"0xB" {:address "0xB" :operable? true :position 0} + "0xA" airdrop-account}}}) + + (is (match? airdrop-account (rf/sub [sub-name community-id]))))) + +(h/deftest-sub :communities/accounts-to-reveal + [sub-name] + (reset! rf-db/app-db + {:communities/all-addresses-to-reveal {community-id #{"0xC" "0xB"}} + :wallet {:accounts {"0xB" {:address "0xB" :operable? true :position 0} + "0xA" {:address "0xA" :operable? true :position 1} + "0xC" {:address "0xC" + :operable? true + :position 2}}}}) + + (is (match? [{:address "0xB" :position 0} + {:address "0xC" :position 2}] + (rf/sub [sub-name community-id])))) diff --git a/src/status_im/subs/contact.cljs b/src/status_im/subs/contact.cljs new file mode 100644 index 00000000000..093a4630674 --- /dev/null +++ b/src/status_im/subs/contact.cljs @@ -0,0 +1,304 @@ +(ns status-im.subs.contact + (:require + [clojure.set :as set] + [clojure.string :as string] + [quo.context] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [status-im.subs.chat.utils :as chat.utils] + [status-im.subs.contact.utils :as contact.utils] + [utils.address :as address] + [utils.collection] + [utils.i18n :as i18n])) + +(re-frame/reg-sub + :multiaccount/profile-pictures-show-to + :<- [:profile/profile] + (fn [multiaccount] + (get multiaccount :profile-pictures-show-to))) + +(re-frame/reg-sub + ::profile-pictures-visibility + :<- [:profile/profile] + (fn [multiaccount] + (get multiaccount :profile-pictures-visibility))) + +(defn- enrich-contact + ([contact] (enrich-contact contact nil nil)) + ([{:keys [public-key] :as contact} setting own-public-key] + (cond-> contact + (and setting + (not= public-key own-public-key) + (or (= setting constants/profile-pictures-visibility-none) + (and (= setting constants/profile-pictures-visibility-contacts-only) + (not (:added? contact))))) + (dissoc :images)))) + +(defn- enrich-contacts + [contacts profile-pictures-visibility own-public-key] + (reduce-kv + (fn [acc public-key contact] + (assoc acc public-key (enrich-contact contact profile-pictures-visibility own-public-key))) + {} + contacts)) + +(defn- reduce-contacts-image-uri + [contacts port font-file theme] + (reduce-kv (fn [acc public-key contact] + (let [contact (contact.utils/replace-contact-image-uri + {:contact contact + :port port + :public-key public-key + :font-file font-file + :theme theme})] + (assoc acc public-key contact))) + {} + contacts)) + +(re-frame/reg-sub + :contacts/contacts + :<- [:contacts/contacts-raw] + :<- [::profile-pictures-visibility] + :<- [:multiaccount/public-key] + :<- [:mediaserver/port] + :<- [:initials-avatar-font-file] + :<- [:theme] + (fn [[contacts profile-pictures-visibility public-key port font-file theme]] + (let [contacts (enrich-contacts contacts profile-pictures-visibility public-key)] + (reduce-contacts-image-uri contacts port font-file theme)))) + +(defn sort-contacts + [contacts] + (sort (fn [c1 c2] + (let [name1 (:primary-name c1) + name2 (:primary-name c2)] + (when (and name1 name2) + (compare (string/lower-case name1) + (string/lower-case name2))))) + (vals contacts))) + +(re-frame/reg-sub + :contacts/active + :<- [:contacts/contacts] + (fn [contacts] + (->> contacts + (filter (fn [[_ contact]] (:active? contact))) + sort-contacts))) + +(re-frame/reg-sub + :contacts/active-sections + :<- [:contacts/active] + (fn [contacts] + (->> contacts + (group-by #(string/upper-case (first (:primary-name %)))) + sort + (mapv (fn [[title items]] {:title title :data items}))))) + +(re-frame/reg-sub + :contacts/grouped-by-first-letter + :<- [:contacts/current-chat-contacts] + :<- [:contacts/active] + (fn [[members contacts]] + (-> (reduce + (fn [acc contact] + (let [first-char (first (:primary-name contact))] + (if (get acc first-char) + (update-in acc [first-char :data] #(conj % contact)) + (assoc acc first-char {:title first-char :data [contact]})))) + {} + (utils.collection/distinct-by :public-key (concat members contacts))) + sort + vals))) + +(re-frame/reg-sub + :contacts/sorted-and-grouped-by-first-letter + :<- [:contacts/active] + :<- [:selected-contacts-count] + (fn [[contacts selected-contacts-count]] + (->> contacts + (filter :mutual?) + (map #(assoc % + :allow-new-users? + (< selected-contacts-count + (dec constants/max-group-chat-participants)))) + (group-by (comp (fnil string/upper-case "") first :primary-name)) + (sort-by first) + (map (fn [[title data]] + {:title title + :data data}))))) + +(re-frame/reg-sub + :contacts/blocked + :<- [:contacts/contacts] + (fn [contacts] + (->> contacts + (filter (fn [[_ contact]] + (:blocked? contact))) + sort-contacts))) + +(re-frame/reg-sub + :contacts/blocked-set + :<- [:contacts/blocked] + (fn [contacts] + (into #{} (map :public-key contacts)))) + +(defn public-key-and-ens-name->new-contact + [public-key ens-name] + (let [contact {:public-key public-key}] + (if ens-name + (-> contact + (assoc :ens-name ens-name) + (assoc :ens-verified true) + (assoc :name ens-name)) + contact))) + +(defn- prepare-contact + [_ contact-identity ens-name port font-file theme] + (let [contact (enrich-contact + (public-key-and-ens-name->new-contact contact-identity ens-name))] + (contact.utils/replace-contact-image-uri + {:contact contact + :port port + :public-key contact-identity + :font-file font-file + :theme theme}))) + +(re-frame/reg-sub + :contacts/current-contact + :<- [:contacts/contacts] + :<- [:contacts/current-contact-identity] + :<- [:contacts/current-contact-ens-name] + :<- [:mediaserver/port] + :<- [:initials-avatar-font-file] + :<- [:theme] + (fn [[contacts contact-identity ens-name port font-file theme]] + (let [contact (get contacts contact-identity)] + (cond-> contact + (nil? contact) + (prepare-contact contact-identity ens-name port font-file theme))))) + +(re-frame/reg-sub + :contacts/contact-by-identity + :<- [:contacts/contacts] + (fn [contacts [_ contact-identity]] + (get + contacts + contact-identity + (contact.utils/build-contact-from-public-key contact-identity)))) + +(re-frame/reg-sub + :contacts/contact-two-names-by-identity + (fn [[_ contact-identity] _] + [(re-frame/subscribe [:contacts/contact-by-identity contact-identity]) + (re-frame/subscribe [:profile/profile])]) + (fn [[contact profile] [_ _]] + (contact.utils/contact-two-names contact profile))) + +(defn get-all-contacts-in-group-chat + [members admins contacts {:keys [public-key preferred-name name display-name] :as current-account}] + (let [current-contact (some-> + current-account + (select-keys [:name :preferred-name :public-key :images :compressed-key]) + (set/rename-keys {:name :alias :preferred-name :name}) + (assoc :primary-name (or display-name preferred-name name))) + all-contacts (cond-> contacts + current-contact + (assoc public-key current-contact))] + (->> members + (map #(or (get all-contacts %) + (contact.utils/build-contact-from-public-key %))) + (sort-by (comp string/lower-case + (fn [{:keys [primary-name name alias public-key]}] + (or primary-name + name + alias + public-key)))) + (map #(if (get admins (:public-key %)) + (assoc % :admin? true) + %))))) + +(re-frame/reg-sub + :contacts/current-chat-contacts + :<- [:chats/current-chat] + :<- [:contacts/contacts] + :<- [:profile/profile] + (fn [[{:keys [contacts admins]} all-contacts current-multiaccount]] + (get-all-contacts-in-group-chat contacts admins all-contacts current-multiaccount))) + +(re-frame/reg-sub + :contacts/contacts-by-chat + (fn [[_ chat-id]] + [(re-frame/subscribe [:chats/chat chat-id]) + (re-frame/subscribe [:contacts/contacts]) + (re-frame/subscribe [:profile/profile])]) + (fn [[{:keys [contacts admins]} all-contacts current-multiaccount]] + (get-all-contacts-in-group-chat contacts admins all-contacts current-multiaccount))) + +(defn- contact-by-address + [[addr contact] address] + (when (address/address= addr address) + contact)) + +(defn find-contact-by-address + [contacts address] + (some #(contact-by-address % address) contacts)) + +(re-frame/reg-sub + :contacts/contact-by-address + :<- [:contacts/contacts] + :<- [:multiaccount/contact] + (fn [[contacts multiaccount] [_ address]] + (if (address/address= address (:public-key multiaccount)) + (merge (contact.utils/build-contact-from-public-key address) + multiaccount) + (or (find-contact-by-address contacts address) + (contact.utils/build-contact-from-public-key address))))) + +(re-frame/reg-sub + :contacts/contact-customization-color-by-address + (fn [[_ address]] + [(re-frame/subscribe [:contacts/contact-by-address address])]) + (fn [[contact]] + (:customization-color contact))) + +(re-frame/reg-sub + :contacts/filtered-active-sections + :<- [:contacts/active-sections] + :<- [:contacts/search-query] + (fn [[contacts query]] + (if (empty? query) + contacts + (->> contacts + (map (fn [item] + (update item + :data + (fn [data] + (filter #(string/includes? + (string/lower-case (:alias %)) + (string/lower-case query)) + data))))) + (remove #(empty? (:data %))))))) + +(re-frame/reg-sub + :contacts/group-members-sections + (fn [[_ chat-id]] + [(re-frame/subscribe [:contacts/contacts-by-chat chat-id]) + (re-frame/subscribe [:visibility-status-updates]) + (re-frame/subscribe [:multiaccount/public-key]) + (re-frame/subscribe [:multiaccount/current-user-visibility-status])]) + (fn [[members status-updates current-user-public-key current-user-visibility-status]] + (let [members (map (fn [{:keys [public-key] :as member}] + (assoc member + :online? + (chat.utils/online? + (if (= public-key current-user-public-key) + (:status-type current-user-visibility-status) + (get-in status-updates [public-key :status-type]))))) + members) + admins (filter :admin? members) + online (filter #(and (not (:admin? %)) (:online? %)) members) + offline (filter #(and (not (:admin? %)) (not (:online? %))) members)] + (vals (cond-> {} + (seq admins) (assoc :owner {:title (i18n/label :t/owner) :data admins}) + (seq online) (assoc :online {:title (i18n/label :t/online) :data online}) + (seq offline) (assoc :offline {:title (i18n/label :t/offline) :data offline})))))) diff --git a/src/status_im/subs/contact/utils.cljs b/src/status_im/subs/contact/utils.cljs new file mode 100644 index 00000000000..c4b2de93656 --- /dev/null +++ b/src/status_im/subs/contact/utils.cljs @@ -0,0 +1,72 @@ +(ns status-im.subs.contact.utils + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [status-im.common.pixel-ratio :as pixel-ratio] + [status-im.constants :as constants] + [status-im.contexts.profile.utils :as profile.utils] + [utils.address :as address])) + +(defn replace-contact-image-uri + [{:keys [contact port public-key font-file theme]}] + (let [{:keys [images customization-color]} contact + images + (reduce (fn [acc image] + (let [image-name (:type image) + clock (:clock image) + options {:port port + :ratio pixel-ratio/ratio + :public-key public-key + :image-name image-name + ; We pass the clock so that we reload the + ; image if the image is updated + :clock clock + :theme theme}] + (assoc-in acc + [(keyword image-name) :config] + {:type :contact + :options options}))) + images + (vals images)) + + images (if (seq images) + images + {:thumbnail + {:config {:type :initials + :options {:port port + :ratio pixel-ratio/ratio + :public-key public-key + :uppercase-ratio (:uppercase-ratio + constants/initials-avatar-font-conf) + :customization-color customization-color + :theme theme + :font-file font-file}}}})] + + (assoc contact :images images))) + +(defn- build-contact-from-public-key* + [public-key] + (when public-key + (let [compressed-key (native-module/serialize-legacy-key public-key)] + {:public-key public-key + :compressed-key compressed-key + :primary-name (address/get-shortened-compressed-key (or compressed-key public-key))}))) + +(def build-contact-from-public-key + "The result of this function is stable because it relies exclusively on the + public key, but it's not cheap to be performed hundreds of times in a row, + such as when displaying a long list of channel members." + (memoize build-contact-from-public-key*)) + +(defn contact-two-names + [{:keys [primary-name] :as contact} + {:keys [public-key preferred-name display-name name]}] + [(if (= public-key (:public-key contact)) + (cond + (not (string/blank? preferred-name)) preferred-name + (not (string/blank? display-name)) display-name + (not (string/blank? primary-name)) primary-name + (not (string/blank? name)) name + :else public-key) + (profile.utils/displayed-name contact)) + (:secondary-name contact)]) diff --git a/src/status_im/subs/contact_test.cljs b/src/status_im/subs/contact_test.cljs new file mode 100644 index 00000000000..59e30907043 --- /dev/null +++ b/src/status_im/subs/contact_test.cljs @@ -0,0 +1,210 @@ +(ns status-im.subs.contact-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + status-im.subs.contact + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def contacts-sample-data + {:selected-contacts-count 1 + :contacts/contacts {"0xtest" {:last-updated 1672582629695 + :mutual? true + :contactRequestClock 0 + :images {} + :added? true + :name "slim.shady" + :primary-name "rslim.shady" + :Removed false + :trustStatus 0 + :alias "Real Slim Shady" + :bio "" + :IsSyncing false + :display-name "" + :ens-verified true + :socialLinks nil + :blocked? false + :active? true + :verificationStatus 0 + :lastUpdatedLocally 1672582563204 + :public-key "0xtest" + :has-added-us? true + :contact-request-state 1} + "0xtest2" {:last-updated 1672582629695 + :mutual? true + :contactRequestClock 0 + :images {} + :added? true + :name "slim.shady" + :primary-name "fslim.shady" + :Removed false + :trustStatus 0 + :alias "Fake Slim Shady" + :bio "" + :IsSyncing false + :display-name "" + :ens-verified true + :socialLinks nil + :blocked? false + :active? true + :verificationStatus 0 + :lastUpdatedLocally 1672582563204 + :public-key "0xtest" + :has-added-us? true + :contact-request-state 1} + "0xtest3" {:last-updated 1672582629695 + :mutual? true + :contactRequestClock 0 + :images {} + :added? true + :name "slim.shady" + :primary-name "islim.shady" + :Removed false + :trustStatus 0 + :alias "Instant noodles" + :bio "" + :IsSyncing false + :display-name "" + :ens-verified true + :socialLinks nil + :blocked? false + :active? true + :verificationStatus 0 + :lastUpdatedLocally 1672582563204 + :public-key "0xtest" + :has-added-us? true + :contact-request-state 1}}}) + +(def expected-sorted-contacts-without-images + [{:title "F" + :data [{:active? true + :last-updated 1672582629695 + :mutual? true + :blocked? false + :contactRequestClock 0 + :added? true + :name "slim.shady" + :primary-name "fslim.shady" + :Removed false + :trustStatus 0 + :alias "Fake Slim Shady" + :bio "" + :IsSyncing false + :display-name "" + :ens-verified true + :socialLinks nil + :allow-new-users? true + :verificationStatus 0 + :lastUpdatedLocally 1672582563204 + :public-key "0xtest" + :has-added-us? true + :contact-request-state 1}]} + {:title "I" + :data [{:active? true + :last-updated 1672582629695 + :mutual? true + :blocked? false + :contactRequestClock 0 + :added? true + :name "slim.shady" + :primary-name "islim.shady" + :Removed false + :trustStatus 0 + :alias "Instant noodles" + :bio "" + :IsSyncing false + :display-name "" + :ens-verified true + :socialLinks nil + :allow-new-users? true + :verificationStatus 0 + :lastUpdatedLocally 1672582563204 + :public-key "0xtest" + :has-added-us? true + :contact-request-state 1}]} + {:title "R" + :data [{:active? true + :last-updated 1672582629695 + :mutual? true + :blocked? false + :contactRequestClock 0 + :added? true + :name "slim.shady" + :primary-name "rslim.shady" + :Removed false + :trustStatus 0 + :alias "Real Slim Shady" + :bio "" + :IsSyncing false + :display-name "" + :ens-verified true + :socialLinks nil + :allow-new-users? true + :verificationStatus 0 + :lastUpdatedLocally 1672582563204 + :public-key "0xtest" + :has-added-us? true + :contact-request-state 1}]}]) + +(defn remove-contact-images + [{:keys [data] :as contact}] + (assoc contact :data (mapv #(dissoc % :images) data))) + +(h/deftest-sub :contacts/sorted-and-grouped-by-first-letter + [sub-name] + (testing "Returning empty sequence when no contacts" + (swap! rf-db/app-db merge (assoc contacts-sample-data :contacts/contacts {})) + (is (empty? (rf/sub [sub-name])))) + (testing "Returning empty sequence when no mutual contacts" + (let [remove-contact-as-mutual #(-> % + (assoc-in ["0xtest" :mutual?] false) + (assoc-in ["0xtest2" :mutual?] false) + (assoc-in ["0xtest3" :mutual?] false))] + (swap! rf-db/app-db merge + (update contacts-sample-data :contacts/contacts remove-contact-as-mutual)) + (is (empty? (rf/sub [sub-name]))))) + (testing "Returning sorted contacts" + (swap! rf-db/app-db merge contacts-sample-data) + (let [contact-list-without-identicons (mapv (fn [contact-group] + (update contact-group + :data + #(mapv (fn [contact] + (dissoc contact :identicon)) + %))) + (rf/sub [sub-name]))] + (is (= expected-sorted-contacts-without-images + (mapv remove-contact-images contact-list-without-identicons)))))) + +(h/deftest-sub :contacts/contact-two-names-by-identity + [sub-name] + (testing "contact is current profile" + (let [profile-key "profile-key"] + (swap! rf-db/app-db assoc + :profile/profile + {:public-key profile-key + :display-name "Display Name" + :name "Name" + :preferred-name "Preferred Name"} + + :contacts/contacts + {profile-key {:primary-name "Primary Name" :public-key profile-key} + "contact-key" {:secondary-name "Secondary Name"}}) + + (is (= ["Preferred Name" nil] (rf/sub [sub-name profile-key]))) + + (swap! rf-db/app-db update :profile/profile dissoc :preferred-name) + (is (= ["Display Name" nil] (rf/sub [sub-name profile-key]))) + + (swap! rf-db/app-db update :profile/profile dissoc :display-name) + (is (= ["Primary Name" nil] (rf/sub [sub-name profile-key]))) + + (swap! rf-db/app-db update-in [:contacts/contacts profile-key] dissoc :primary-name) + (is (= ["Name" nil] (rf/sub [sub-name profile-key]))))) + + (testing "contact is not current profile" + (swap! rf-db/app-db assoc + :profile/profile {:public-key "profile-key"} + :contacts/contacts {"contact-key" {:preferred-name "Preferred Name" + :secondary-name "Secondary Name"}}) + + (is (= ["Preferred Name" "Secondary Name"] (rf/sub [sub-name "contact-key"]))))) diff --git a/src/status_im/subs/general.cljs b/src/status_im/subs/general.cljs new file mode 100644 index 00000000000..a8a224853ae --- /dev/null +++ b/src/status_im/subs/general.cljs @@ -0,0 +1,169 @@ +(ns status-im.subs.general + (:require + [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.subs.chat.utils :as chat.utils] + [utils.ethereum.chain :as chain])) + +(re-frame/reg-sub + :visibility-status-updates/visibility-status-update + :<- [:multiaccount/public-key] + :<- [:multiaccount/current-user-visibility-status] + :<- [:visibility-status-updates] + (fn [[my-public-key my-status-update status-updates] [_ public-key]] + (if (or (string/blank? public-key) (= public-key my-public-key)) + my-status-update + (get status-updates public-key)))) + +(re-frame/reg-sub + :visibility-status-updates/online? + (fn [[_ public-key]] + [(re-frame/subscribe [:visibility-status-updates/visibility-status-update public-key])]) + (fn [[status-update]] + (let [visibility-status-type (:status-type status-update)] + (chat.utils/online? visibility-status-type)))) + +(re-frame/reg-sub + :multiaccount/logged-in? + :<- [:profile/profile] + (fn [profile] + (not (nil? profile)))) + +(re-frame/reg-sub + :hide-screen? + :<- [:app-state] + :<- [:profile/profile] + (fn [[state multiaccount]] + (and (= state "inactive") (:preview-privacy? multiaccount)))) + +(re-frame/reg-sub + :current-network + :<- [:networks/networks] + :<- [:networks/current-network] + (fn [[networks current-network]] + (when-let [network (get networks current-network)] + (assoc network :rpc-network? (get-in network [:config :UpstreamConfig :Enabled]))))) + +(re-frame/reg-sub + :chain-name + :<- [:current-network] + (fn [network] + (chain/network->chain-name network))) + +(re-frame/reg-sub + :chain-id + :<- [:current-network] + (fn [network] + (chain/network->chain-id network))) + +(re-frame/reg-sub + :network-name + :<- [:current-network] + (fn [network] + (:name network))) + +(re-frame/reg-sub + :syncing? + :<- [:sync-state] + (fn [sync-state] + (#{:pending :in-progress} sync-state))) + +(re-frame/reg-sub + :dimensions/window-width + :<- [:dimensions/window] + :-> :width) + +(re-frame/reg-sub + :dimensions/window-height + :<- [:dimensions/window] + :-> :height) + +;; WARNING: Do not use :get-screen-params directly, use quo.context/use-screen-params instead +(re-frame/reg-sub + :get-screen-params + :<- [:screen-params] + (fn [params [_ screen-id]] + (get params screen-id))) + +(defn- node-version + [web3-node-version] + (or web3-node-version "N/A")) + +(re-frame/reg-sub + :get-app-node-version + :<- [:web3-node-version] + node-version) + +(re-frame/reg-sub + :my-profile/recovery + :<- [:my-profile/seed] + (fn [seed] + (or seed {:step :intro}))) + +(re-frame/reg-sub + :is-contact-selected? + :<- [:group/selected-contacts] + (fn [selected-contacts [_ element]] + (-> selected-contacts + (contains? element)))) + +(re-frame/reg-sub + :mnemonic + :<- [:profile/profile] + (fn [{:keys [mnemonic]}] + mnemonic)) + +(re-frame/reg-sub + :toasts/toast + :<- [:toasts] + (fn [toasts [_ toast-id]] + (get-in toasts [:toasts toast-id]))) + +(re-frame/reg-sub + :toasts/toast-cursor + :<- [:toasts] + (fn [toasts [_ toast-id & cursor]] + (get-in toasts (into [:toasts toast-id] cursor)))) + +(re-frame/reg-sub :network/offline? + :<- [:network/status] + (fn [status] + (= status :offline))) + +(re-frame/reg-sub :network/online? + :<- [:network/status] + (fn [status] + (= status :online))) + +(re-frame/reg-sub :currencies/categorized + :<- [:currencies] + (fn [currencies [_ query]] + (let [search-lc (string/lower-case query)] + (reduce + (fn [acc currency] + (let [{:keys [popular? token? name short-name]} currency + matches-query? (or (string/includes? (string/lower-case + name) + search-lc) + (string/includes? (string/lower-case + short-name) + search-lc))] + (cond-> acc + matches-query? (update :total inc) + (and popular? matches-query?) (update :popular conj currency) + (and token? matches-query?) (update :crypto conj currency) + (and matches-query? (not popular?) (not token?)) (update :other conj currency)))) + {:total 0 + :popular [] + :crypto [] + :other []} + (vals currencies))))) + +(re-frame/reg-sub + :wakuv2-nodes/validation-errors + :<- [:wakuv2-nodes/manage] + (fn [manage] + (set (keep + (fn [[k {:keys [error]}]] + (when error k)) + manage)))) diff --git a/src/status_im/subs/general_test.cljs b/src/status_im/subs/general_test.cljs new file mode 100644 index 00000000000..f04529de795 --- /dev/null +++ b/src/status_im/subs/general_test.cljs @@ -0,0 +1,187 @@ +(ns status-im.subs.general-test + (:require + [cljs.test :refer [is testing use-fixtures]] + [re-frame.db :as rf-db] + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(use-fixtures :each + {:before #(reset! rf-db/app-db {}) + :after #(reset! rf-db/app-db {})}) + +(def currencies + {:usd + {:id :usd :short-name "USD" :symbol "$" :emoji "🇺🇸" :name "US Dollar" :popular? true :token? false} + :eur {:id :eur :short-name "EUR" :symbol "€" :emoji "🇪🇺" :name "Euro" :popular? true :token? false} + :btc + {:id :btc :short-name "BTC" :symbol "₿" :emoji "🇧🇹" :name "Bitcoin" :popular? false :token? true} + :eth + {:id :eth :short-name "ETH" :symbol "Ξ" :emoji "🇪🇹" :name "Ethereum" :popular? false :token? true} + :gbp {:id :gbp + :short-name "GBP" + :symbol "£" + :emoji "🇬🇧" + :name "British Pound" + :popular? false + :token? false} + :jpy {:id :jpy + :short-name "JPY" + :symbol "¥" + :emoji "🇯🇵" + :name "Japanese Yen" + :popular? false + :token? false}}) + +(h/deftest-sub :currencies/categorized + [sub-name] + (swap! rf-db/app-db assoc :currencies currencies) + + (testing "all currencies categorized correctly" + (is + (= {:total 6 + :popular [{:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false} + {:id :eur + :short-name "EUR" + :symbol "€" + :emoji "🇪🇺" + :name "Euro" + :popular? true + :token? false}] + :crypto [{:id :btc + :short-name "BTC" + :symbol "₿" + :emoji "🇧🇹" + :name "Bitcoin" + :popular? false + :token? true} + {:id :eth + :short-name "ETH" + :symbol "Ξ" + :emoji "🇪🇹" + :name "Ethereum" + :popular? false + :token? true}] + :other [{:id :gbp + :short-name "GBP" + :symbol "£" + :emoji "🇬🇧" + :name "British Pound" + :popular? false + :token? false} + {:id :jpy + :short-name "JPY" + :symbol "¥" + :emoji "🇯🇵" + :name "Japanese Yen" + :popular? false + :token? false}]} + (rf/sub [sub-name ""])))) + + (testing "search query filters correctly" + (is (= {:total 1 + :popular [{:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false}] + :crypto [] + :other []} + (rf/sub [sub-name "usd"]))) + (is + (= + {:total 1 + :popular + [{:id :eur :short-name "EUR" :symbol "€" :emoji "🇪🇺" :name "Euro" :popular? true :token? false}] + :crypto [] + :other []} + (rf/sub [sub-name "eur"]))) + (is + (= {:total 3 + :popular [] + :crypto [{:id :btc + :short-name "BTC" + :symbol "₿" + :emoji "🇧🇹" + :name "Bitcoin" + :popular? false + :token? true} + {:id :eth + :short-name "ETH" + :symbol "Ξ" + :emoji "🇪🇹" + :name "Ethereum" + :popular? false + :token? true}] + :other [{:id :gbp + :short-name "GBP" + :symbol "£" + :emoji "🇬🇧" + :name "British Pound" + :popular? false + :token? false}]} + (rf/sub [sub-name "t"])))) + + (testing "case insensitive search query" + (is (= {:total 1 + :popular [{:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false}] + :crypto [] + :other []} + (rf/sub [sub-name "USD"]))) + (is + (= + {:total 1 + :popular + [{:id :eur :short-name "EUR" :symbol "€" :emoji "🇪🇺" :name "Euro" :popular? true :token? false}] + :crypto [] + :other []} + (rf/sub [sub-name "eUr"]))) + (is + (= {:total 3 + :popular [] + :crypto [{:id :btc + :short-name "BTC" + :symbol "₿" + :emoji "🇧🇹" + :name "Bitcoin" + :popular? false + :token? true} + {:id :eth + :short-name "ETH" + :symbol "Ξ" + :emoji "🇪🇹" + :name "Ethereum" + :popular? false + :token? true}] + :other [{:id :gbp + :short-name "GBP" + :symbol "£" + :emoji "🇬🇧" + :name "British Pound" + :popular? false + :token? false}]} + (rf/sub [sub-name "T"])))) + + (testing "search with no matching results" + (is (= {:total 0 + :popular [] + :crypto [] + :other []} + (rf/sub [sub-name "xyz"])))) + + (testing "all categories are included" + (is (= [:total :popular :crypto :other] + (keys (rf/sub [sub-name ""])))))) diff --git a/src/status_im/subs/keycard.cljs b/src/status_im/subs/keycard.cljs new file mode 100644 index 00000000000..2c057b4cb71 --- /dev/null +++ b/src/status_im/subs/keycard.cljs @@ -0,0 +1,102 @@ +(ns status-im.subs.keycard + (:require [utils.re-frame :as rf])) + +(rf/reg-sub + :keycard/keycard-profile? + (fn [db] + (not (nil? (get-in db [:profile/profile :keycard-pairing]))))) + +(rf/reg-sub + :keycard/keycard-profile + :<- [:profile/name] + :<- [:profile/image] + :<- [:profile/customization-color] + (fn [[profile-name profile-image customization-color]] + {:profile-name profile-name + :profile-image profile-image + :customization-color customization-color})) + +(defn profile-keypair-keycards? + [{:keys [type keycards]}] + (and (= :profile type) keycards)) + +(rf/reg-sub + :keycard/keypairs-keycards + :<- [:wallet/keypairs-list] + (fn [keypairs] + (transduce (comp (filter profile-keypair-keycards?) + (mapcat :keycards)) + conj + keypairs))) + +(rf/reg-sub + :keycard/registered-keycards + :<- [:keycard/keycard-profile] + :<- [:keycard/keypairs-keycards] + (fn [[keycard-profile keycards]] + (map (fn [keycard] + (assoc keycard + :profile-name (:profile-name keycard-profile) + :profile-image (:profile-image keycard-profile) + :customization-color (:customization-color keycard-profile))) + keycards))) + +(rf/reg-sub + :keycard/nfc-enabled? + :<- [:keycard] + (fn [keycard] + (:nfc-enabled? keycard))) + +(rf/reg-sub + :keycard/connected? + :<- [:keycard] + (fn [keycard] + (:card-connected? keycard))) + +(rf/reg-sub + :keycard/steps + :<- [:keycard] + (fn [keycard] + (:steps keycard))) + +(rf/reg-sub + :keycard/current-step + :<- [:keycard] + (fn [keycard] + (:current-step keycard))) + +(rf/reg-sub + :keycard/pin + :<- [:keycard] + (fn [keycard] + (:pin keycard))) + +(rf/reg-sub + :keycard/pin-retry-counter + :<- [:keycard] + (fn [keycard] + (get-in keycard [:application-info :pin-retry-counter]))) + +(rf/reg-sub + :keycard/connection-sheet-opts + :<- [:keycard] + (fn [keycard] + (:connection-sheet-opts keycard))) + +(rf/reg-sub + :keycard/application-info-error + :<- [:keycard] + (fn [keycard] + (:application-info-error keycard))) + +(rf/reg-sub + :keycard/initialized? + :<- [:keycard] + (fn [keycard] + (get-in keycard [:application-info :initialized?]))) + +(rf/reg-sub + :keycard/key-uid + :<- [:keycard] + (fn [keycard] + (get-in keycard [:application-info :key-uid]))) diff --git a/src/status_im/subs/messages.cljs b/src/status_im/subs/messages.cljs new file mode 100644 index 00000000000..f2e2b4f7594 --- /dev/null +++ b/src/status_im/subs/messages.cljs @@ -0,0 +1,242 @@ +(ns status-im.subs.messages + (:require + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [status-im.contexts.chat.messenger.messages.list.events :as models.message-list] + [status-im.contexts.chat.messenger.messages.resolver.message-resolver :as resolver] + [utils.i18n :as i18n])) + +(defn intersperse-datemark + "Reduce step which expects the input list of messages to be sorted by clock value. + It makes best effort to group them by day. + We cannot sort them by :timestamp, as that represents the clock of the sender + and we have no guarantees on the order. + We naively and arbitrarly group them assuming that out-of-order timestamps + fall in the previous bucket. + A sends M1 to B with timestamp 2000-01-01T00:00:00 + B replies M2 with timestamp 1999-12-31-23:59:59 + M1 needs to be displayed before M2 + so we bucket both in 1999-12-31" + [{:keys [acc last-timestamp last-datemark]} {:keys [whisper-timestamp datemark] :as msg}] + (cond + (empty? acc) ; initial element + {:last-timestamp whisper-timestamp + :last-datemark datemark + :acc (conj acc msg)} + + (and (not= last-datemark datemark) ; not the same day + (< whisper-timestamp last-timestamp)) ; not out-of-order + {:last-timestamp whisper-timestamp + :last-datemark datemark + :acc (conj acc + {:value last-datemark ; intersperse datemark message + :type :datemark} + msg)} + :else + {:last-timestamp (min whisper-timestamp last-timestamp) ; use last datemark + :last-datemark last-datemark + :acc (conj acc (assoc msg :datemark last-datemark))})) + +(defn add-datemarks + "Add a datemark in between an ordered seq of messages when two datemarks are not + the same. Ignore messages with out-of-order timestamps" + [messages] + (when (seq messages) + (let [messages-with-datemarks (:acc (reduce intersperse-datemark {:acc []} messages))] + ; Append last datemark + (conj messages-with-datemarks + {:value (:datemark (peek messages-with-datemarks)) + :type :datemark})))) + +(defn hydrate-messages + "Pull data from messages and add it to the sorted list" + ([message-list messages] (hydrate-messages message-list messages {})) + ([message-list messages pinned-messages] + (keep #(if (= :message (% :type)) + (when-let [message (messages (% :message-id))] + (let [pinned-message (get pinned-messages (% :message-id)) + pinned (if pinned-message true (some? (message :pinned-by))) + pinned-by (when pinned (or (message :pinned-by) (pinned-message :pinned-by))) + message (assoc message :pinned pinned :pinned-by pinned-by)] + (merge message %))) + %) + message-list))) + +(defn albumize-messages + [messages] + (->> messages + (reduce + (fn [{:keys [messages albums]} message] + (let [{:keys [album-id content quoted-message]} message + {:keys [response-to]} content + albums (cond-> albums + album-id + (update album-id conj message)) + messages (if album-id + (conj (filterv #(not= album-id (:album-id %)) + messages) + (merge message + {:album (get albums album-id) + :album-id album-id + :content {:response-to + response-to} + :quoted-message quoted-message + :content-type + constants/content-type-album})) + (conj messages message))] + {:messages messages + :albums albums})) + {:messages [] + :albums {}}) + :messages)) + +(re-frame/reg-sub + :chats/chat-messages + :<- [:messages/messages] + (fn [messages [_ chat-id]] + (get messages chat-id {}))) + +(re-frame/reg-sub + :chats/message-link-previews + :<- [:messages/messages] + (fn [messages [_ chat-id message-id]] + (get-in messages [chat-id message-id :link-previews]))) + +(re-frame/reg-sub + :chats/message-link-previews? + :<- [:messages/messages] + (fn [messages [_ chat-id message-id]] + (-> messages + (get-in [chat-id message-id :link-previews]) + count + pos?))) + +(re-frame/reg-sub + :chats/message-status-link-previews + :<- [:messages/messages] + (fn [messages [_ chat-id message-id]] + (get-in messages [chat-id message-id :status-link-previews]))) + +(re-frame/reg-sub + :chats/pinned + :<- [:messages/pin-messages] + (fn [pinned-messages [_ chat-id] _] + (get pinned-messages chat-id))) + +;; local messages will not have a :pinned-at key until user navigates away and to +;; chat screen. For this reason we want to retain order of local messages with :pinned-at nil +;; as these will be a stack on top of the messages, however we do want to sort previous messages +;; from backend that have a :pinned-at value. +(defn sort-pinned + [a b] + (cond + (and a b) (- a b) + (or a b) (if b false true) + :else a)) + +(re-frame/reg-sub + :chats/pinned-sorted-list + (fn [[_ chat-id] _] + (re-frame/subscribe [:chats/pinned chat-id])) + (fn [pin-messages _] + (let [pin-messages-vals (vals pin-messages)] + (sort-by :pinned-at sort-pinned pin-messages-vals)))) + +(re-frame/reg-sub + :chats/last-pinned-message + (fn [[_ chat-id]] + (re-frame/subscribe [:chats/pinned-sorted-list chat-id])) + (fn [pin-messages _] + (last pin-messages))) + +(defn message-text + [{:keys [content-type] :as message}] + (cond (= content-type constants/content-type-audio) + (i18n/label :t/audio-message) + :else + (get-in message [:content :parsed-text]))) + +(re-frame/reg-sub + :chats/last-pinned-message-text + (fn [[_ chat-id]] + (re-frame/subscribe [:chats/last-pinned-message chat-id])) + (fn [pinned-message _] + (let [latest-pin-text (message-text pinned-message) + {:keys [deleted? deleted-for-me?]} pinned-message] + (cond deleted? (i18n/label :t/message-deleted-for-everyone) + deleted-for-me? (i18n/label :t/message-deleted-for-you) + (string? latest-pin-text) latest-pin-text + :else (resolver/resolve-message latest-pin-text))))) + +(re-frame/reg-sub + :chats/pin-messages-count + (fn [[_ chat-id]] + (re-frame/subscribe [:chats/pinned chat-id])) + (fn [pinned-messages _] + (count pinned-messages))) + +(re-frame/reg-sub + :chats/message-reactions + :<- [:multiaccount/public-key] + :<- [:messages/reactions] + (fn [[current-public-key reactions] [_ message-id chat-id]] + (let [reactions (get-in reactions [chat-id message-id])] + (reduce + (fn [acc [emoji-id reactions]] + (if (pos? (count reactions)) + (let [own (first (filter (fn [[_ {:keys [from]}]] + (= from current-public-key)) + reactions))] + (conj acc + {:emoji-id emoji-id + :own (boolean (seq own)) + :emoji-reaction-id (:emoji-reaction-id (second own)) + :quantity (count reactions)})) + acc)) + [] + reactions)))) + +(re-frame/reg-sub + :chats/all-loaded? + :<- [:messages/pagination-info] + (fn [pagination-info [_ chat-id]] + (get-in pagination-info [chat-id :all-loaded?]))) + +(re-frame/reg-sub + :chats/loading-messages? + :<- [:messages/pagination-info] + (fn [pagination-info [_ chat-id]] + (get-in pagination-info [chat-id :loading-messages?]))) + +(re-frame/reg-sub + :chats/message-list + :<- [:messages/message-lists] + (fn [message-lists [_ chat-id]] + (get message-lists chat-id))) + +(re-frame/reg-sub + :chats/raw-chat-messages-stream + (fn [[_ chat-id] _] + [(re-frame/subscribe [:chats/message-list chat-id]) + (re-frame/subscribe [:chats/chat-messages chat-id]) + (re-frame/subscribe [:chats/pinned chat-id]) + (re-frame/subscribe [:chats/loading-messages? chat-id])]) + (fn [[message-list messages pin-messages loading-messages?] _] + ;;TODO (perf) + (let [message-list-seq (models.message-list/->seq message-list)] + ; Don't show gaps if that's the case as we are still loading messages + (if (and (empty? message-list-seq) loading-messages?) + [] + (-> message-list-seq + (add-datemarks) + (hydrate-messages messages pin-messages) + (albumize-messages)))))) + +(re-frame/reg-sub + :messages/resolve-mention + (fn [[_ mention] _] + [(re-frame/subscribe [:contacts/contact-two-names-by-identity mention])]) + (fn [[contact-names] [_ mention]] + (if (= mention constants/everyone-mention-id) + (i18n/label :t/everyone-mention) + (first contact-names)))) diff --git a/src/status_im/subs/messages_test.cljs b/src/status_im/subs/messages_test.cljs new file mode 100644 index 00000000000..5ed81f39690 --- /dev/null +++ b/src/status_im/subs/messages_test.cljs @@ -0,0 +1,177 @@ +(ns status-im.subs.messages-test + (:require + [cljs.test :refer [deftest is testing]] + [re-frame.db :as rf-db] + [status-im.constants :as constants] + [status-im.subs.messages :as messages] + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def messages-state + [{:message-id "0x111" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"} + {:message-id "0x222" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"} + {:message-id "0x333" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"} + {:message-id "0x444" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"}]) + +(def messages-albumized-state + [{:album [{:message-id "0x444" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"} + {:message-id "0x333" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"} + {:message-id "0x222" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"} + {:message-id "0x111" :album-id "abc" :albumize? true :from :xyz :timestamp-str "14:00"}] + :content {:response-to nil} + :quoted-message nil + :album-id "abc" + :albumize? true + :message-id "0x444" + :from :xyz + :timestamp-str "14:00" + :content-type constants/content-type-album}]) + +(deftest albumize-messages-test + (testing "Finding albums in the messages list" + (is (= (messages/albumize-messages messages-state) messages-albumized-state)))) + +(deftest intersperse-datemarks-test + (testing "it mantains the order even when timestamps are across days" + (let [message-1 {:datemark "Dec 31, 1999" + :whisper-timestamp 946641600000} ; 1999} + message-2 {:datemark "Jan 1, 2000" + :whisper-timestamp 946728000000} ; 2000 this will displayed in 1999 + message-3 {:datemark "Dec 31, 1999" + :whisper-timestamp 946641600000} ; 1999 + message-4 {:datemark "Jan 1, 2000" + :whisper-timestamp 946728000000} ; 2000 + ordered-messages [message-4 + message-3 + message-2 + message-1] + [m1 d1 m2 m3 m4 d2] (messages/add-datemarks ordered-messages)] + (is (= "Jan 1, 2000" + (:datemark m1))) + (is (= {:type :datemark + :value "Jan 1, 2000"} + d1)) + (is (= "Dec 31, 1999" + (:datemark m2) + (:datemark m3) + (:datemark m4))) + (is (= {:type :datemark + :value "Dec 31, 1999"} + d2))))) + +(def pinned-messages-state + {:0xChat {:0x2 {:chat-id :0xChat + :message-id :0x2 + :pinned-at 2000 + :pinned-by :test-user} + :0x1 {:chat-id :0xChat + :message-id :0x1 + :pinned-at 1000 + :pinned-by :test-user} + :0x3 {:chat-id :0xChat + :message-id :0x3 + :pinned-at 3000 + :pinned-by :test-user}}}) + +(def pinned-messages-state-with-1-new-local-message + {:0xChat {:0x3 {:chat-id :0xChat + :message-id :0x3 + :pinned-at nil + :pinned-by :test-user} + :0x2 {:chat-id :0xChat + :message-id :0x2 + :pinned-at 3000 + :pinned-by :test-user} + :0x1 {:chat-id :0xChat + :message-id :0x1 + :pinned-at 2000 + :pinned-by :test-user}}}) + +(def pinned-messages-state-with-2-new-local-messages + {:0xChat {:0x3 {:chat-id :0xChat + :message-id :0x3 + :pinned-at nil + :pinned-by :test-user} + :0x4 {:chat-id :0xChat + :message-id :0x4 + :pinned-at nil + :pinned-by :test-user} + :0x2 {:chat-id :0xChat + :message-id :0x2 + :pinned-at 3000 + :pinned-by :test-user} + :0x1 {:chat-id :0xChat + :message-id :0x1 + :pinned-at 2000 + :pinned-by :test-user}}}) + +(h/deftest-sub :chats/pinned-sorted-list + [sub-name] + (testing "It sorts three messages with pinned-at property" + (swap! rf-db/app-db assoc :pin-messages pinned-messages-state) + (is + (= [{:chat-id :0xChat + :message-id :0x1 + :pinned-at 1000 + :pinned-by :test-user} + {:chat-id :0xChat + :message-id :0x2 + :pinned-at 2000 + :pinned-by :test-user} + {:chat-id :0xChat + :message-id :0x3 + :pinned-at 3000 + :pinned-by :test-user}] + (rf/sub [sub-name :0xChat])))) + (testing "It sorts messages from backend with pinned-at property and 1 new local pinned message" + (swap! rf-db/app-db assoc :pin-messages pinned-messages-state-with-1-new-local-message) + (is + (= [{:chat-id :0xChat + :message-id :0x1 + :pinned-at 2000 + :pinned-by :test-user} + {:chat-id :0xChat + :message-id :0x2 + :pinned-at 3000 + :pinned-by :test-user} + {:chat-id :0xChat + :message-id :0x3 + :pinned-at nil + :pinned-by :test-user}] + (rf/sub [sub-name :0xChat])))) + (testing "It sorts messages from backend with pinned-at property and 2 new local pinned messages" + (swap! rf-db/app-db assoc :pin-messages pinned-messages-state-with-2-new-local-messages) + (is + (= [{:chat-id :0xChat + :message-id :0x1 + :pinned-at 2000 + :pinned-by :test-user} + {:chat-id :0xChat + :message-id :0x2 + :pinned-at 3000 + :pinned-by :test-user} + {:chat-id :0xChat + :message-id :0x3 + :pinned-at nil + :pinned-by :test-user} + {:chat-id :0xChat + :message-id :0x4 + :pinned-at nil + :pinned-by :test-user}] + (rf/sub [sub-name :0xChat]))))) + +(def message-with-link-previews + {:0xChat {:0x01 {:content {:response-to nil} + :quoted-message nil + :message-id "0x1" + :from :xyz + :timestamp-str "14:00" + :content-type constants/content-type-text + :chat-id :0xChat + :link-previews [{:dummy-link-preview 1}]}}}) + +(h/deftest-sub :chats/message-link-previews? + [sub-name] + (testing "It returns true if link previews exists in a message" + (swap! rf-db/app-db assoc :messages message-with-link-previews) + (is (rf/sub [sub-name :0xChat :0x01])))) diff --git a/src/status_im/subs/onboarding.cljs b/src/status_im/subs/onboarding.cljs new file mode 100644 index 00000000000..f3bc3ec471c --- /dev/null +++ b/src/status_im/subs/onboarding.cljs @@ -0,0 +1,13 @@ +(ns status-im.subs.onboarding + (:require + [re-frame.core :as re-frame])) + +(re-frame/reg-sub + :onboarding/customization-color + :<- [:onboarding/profile] + :<- [:profile/customization-color] + (fn [[{:keys [color]} customization-color] + [_sub-id {:keys [onboarding?]}]] + (if (and onboarding? (some? color)) + color + customization-color))) diff --git a/src/status_im/subs/pairing.cljs b/src/status_im/subs/pairing.cljs new file mode 100644 index 00000000000..ce9072d315f --- /dev/null +++ b/src/status_im/subs/pairing.cljs @@ -0,0 +1,42 @@ +(ns status-im.subs.pairing + (:require + [legacy.status-im.pairing.core :as pairing] + [re-frame.core :as re-frame])) + +(re-frame/reg-sub + :pairing/installations + :<- [:get-pairing-installations] + :<- [:pairing/installation-id] + (fn [[installations installation-id]] + (->> installations + vals + (pairing/sort-installations installation-id)))) + +(re-frame/reg-sub + :pairing/paired-devices-count + :<- [:pairing/installations] + (fn [installations] + (count (filter :enabled? (rest installations))))) + +(re-frame/reg-sub + :pairing/enabled-installations + :<- [:pairing/installations] + (fn [installations] + (filter :enabled? installations))) + +(re-frame/reg-sub + :pairing/installation-id + :<- [:profile/profile] + (fn [multiaccount] (:installation-id multiaccount))) + +(re-frame/reg-sub + :pairing/pairing-status + :<- [:syncing] + (fn [syncing] + (:pairing-status syncing))) + +(re-frame/reg-sub + :pairing/has-paired-devices + :<- [:pairing/enabled-installations] + (fn [installations] + (> (count installations) 1))) diff --git a/src/status_im/subs/profile.cljs b/src/status_im/subs/profile.cljs new file mode 100644 index 00000000000..179e1bf0733 --- /dev/null +++ b/src/status_im/subs/profile.cljs @@ -0,0 +1,332 @@ +(ns status-im.subs.profile + (:require + [cljs.spec.alpha :as spec] + [clojure.string :as string] + [legacy.status-im.fleet.core :as fleet] + [legacy.status-im.multiaccounts.db :as multiaccounts.db] + [quo.context] + [re-frame.core :as re-frame] + [status-im.common.pixel-ratio :as pixel-ratio] + [status-im.constants :as constants] + [status-im.contexts.profile.data-store :as profile.data-store] + [status-im.contexts.profile.utils :as profile.utils] + [utils.security.core :as security])) + +(re-frame/reg-sub + :profile/customization-color + :<- [:profile/profile] + (fn [{:keys [customization-color]}] + (or customization-color constants/profile-default-color))) + +;; A profile can only be created without accepting terms in Status v1. In Status +;; v2, the terms may be unaccepted because we intentionally created a migration +;; resetting the flag in case the privacy policy changed. +(re-frame/reg-sub :profile/has-profiles-and-unaccepted-terms? + :<- [:profile/profiles-overview] + (fn [profiles-overview] + (and (seq profiles-overview) + (not (profile.data-store/accepted-terms? (vals profiles-overview)))))) + +(re-frame/reg-sub + :profile/currency + :<- [:profile/profile] + (fn [{:keys [currency]}] + (or currency constants/profile-default-currency))) + +(re-frame/reg-sub + :profile/syncing-on-mobile-network? + :<- [:profile/profile] + (fn [{:keys [syncing-on-mobile-network?]}] + (boolean syncing-on-mobile-network?))) + +(re-frame/reg-sub + :profile/currency-symbol + :<- [:currencies] + :<- [:profile/currency] + (fn [[currencies currency-id]] + (let [currency (get currencies currency-id)] + (if (:token? currency) + (:short-name currency) + (:symbol currency))))) + +(re-frame/reg-sub + :profile/currency-info + :<- [:currencies] + :<- [:profile/currency] + (fn [[currencies currency-id]] + (get currencies currency-id))) + +(re-frame/reg-sub + :profile/login-profiles-picture + :<- [:profile/profiles-overview] + :<- [:mediaserver/port] + :<- [:initials-avatar-font-file] + :<- [:theme] + (fn [[profiles port font-file theme] [_ target-key-uid]] + (let [{:keys [images customization-color] :as profile} (get profiles target-key-uid) + image-name (-> images first :type) + image-clock (-> images first :clock)] + (when (and profile port) + {:config + (if image-name + {:type :account + :options {:port port + :ratio pixel-ratio/ratio + :image-name image-name + :key-uid target-key-uid + :theme theme + :clock image-clock}} + {:type :initials + :options {:port port + :ratio pixel-ratio/ratio + :key-uid target-key-uid + :theme theme + :uppercase-ratio (:uppercase-ratio constants/initials-avatar-font-conf) + :customization-color customization-color + :font-file font-file}})})))) + +;; DEPRECATED +;; use `:profile/public-key` instead +(re-frame/reg-sub + :multiaccount/public-key + :<- [:profile/profile] + (fn [{:keys [public-key]}] + public-key)) + +(re-frame/reg-sub + :profile/public-key + :<- [:profile/profile] + (fn [{:keys [public-key]}] + public-key)) + +(re-frame/reg-sub + :profile/light-client-enabled? + :<- [:profile/profile] + (fn [profile] + (get-in profile [:wakuv2-config :LightClient]))) + +(re-frame/reg-sub + :profile/store-confirmations-enabled? + :<- [:profile/profile] + (fn [profile] + (get-in profile [:wakuv2-config :EnableStoreConfirmationForMessagesSent]))) + +(re-frame/reg-sub + :profile/peer-syncing-enabled? + :<- [:profile/profile] + (fn [profile] + (:peer-syncing-enabled? profile))) + +(re-frame/reg-sub + :profile/telemetry-enabled? + :<- [:profile/profile] + (fn [profile] + (not (string/blank? (:telemetry-server-url profile))))) + +(re-frame/reg-sub + :profile/test-networks-enabled? + :<- [:profile/profile] + (fn [profile] + (:test-networks-enabled? profile))) + +(re-frame/reg-sub + :profile/compressed-key + :<- [:profile/profile] + (fn [{:keys [compressed-key]}] + compressed-key)) + +(re-frame/reg-sub + :multiaccount/contact + :<- [:profile/profile] + (fn [current-account] + (select-keys current-account [:name :preferred-name :public-key :image :images]))) + +(re-frame/reg-sub + :sign-in-enabled? + :<- [:profile/login] + (fn [{:keys [password]}] + (spec/valid? ::multiaccounts.db/password + (security/safe-unmask-data password)))) + +(re-frame/reg-sub + :fleets/current-fleet + :<- [:profile/profile] + (fn [multiaccount] + (fleet/current-fleet-sub multiaccount))) + +(re-frame/reg-sub + :opensea-enabled? + :<- [:profile/profile] + (fn [{:keys [opensea-enabled?]}] + (boolean opensea-enabled?))) + +(re-frame/reg-sub + :log-level/current-profile-log-level + :<- [:profile/profile] + (fn [multiaccount] + (get multiaccount :log-level))) + +(re-frame/reg-sub :log-level/debug? + :<- [:log-level/current-profile-log-level] + (fn [level] + (= "DEBUG" level))) + +(re-frame/reg-sub + :dapps-address + :<- [:profile/profile] + (fn [acc] + (get acc :dapps-address))) + +(re-frame/reg-sub + :dapps-account + :<- [:profile/wallet-accounts] + :<- [:dapps-address] + (fn [[accounts address]] + (some #(when (= (:address %) address) %) accounts))) + +(re-frame/reg-sub + :visible-accounts-without-watch-only + :<- [:profile/wallet-accounts] + (fn [accounts] + (remove :hidden (filter #(not= (:type %) :watch) accounts)))) + +(re-frame/reg-sub + :multiaccount/current-user-visibility-status + :<- [:profile/profile] + (fn [{:keys [current-user-visibility-status]}] + current-user-visibility-status)) + +(re-frame/reg-sub + :multiaccount/reset-password-form-vals-and-errors + :<- [:multiaccount/reset-password-form-vals] + :<- [:multiaccount/reset-password-errors] + :<- [:multiaccount/resetting-password?] + (fn [[form-vals errors resetting?]] + (let [{:keys [current-password new-password confirm-new-password]} form-vals] + {:form-vals form-vals + :errors errors + :resetting? resetting? + :next-enabled? + (and (pos? (count current-password)) + (pos? (count new-password)) + (pos? (count confirm-new-password)) + (>= (count new-password) 6) + (>= (count current-password) 6) + (= new-password confirm-new-password))}))) + +(re-frame/reg-sub + :profile/has-picture + :<- [:profile/profile] + (fn [multiaccount] + (pos? (count (get multiaccount :images))))) + +(re-frame/reg-sub :profile/pictures-visibility + :<- [:profile/profile] + :-> :profile-pictures-visibility) + +(re-frame/reg-sub :profile/allow-new-contact-requests? + :<- [:profile/profile] + (fn [{:keys [messages-from-contacts-only]}] + (not messages-from-contacts-only))) + +(re-frame/reg-sub :profile/preview-privacy? + :<- [:profile/profile] + (fn [{:keys [preview-privacy?]}] + (boolean preview-privacy?))) + +(re-frame/reg-sub :profile/news-feed-enabled? + :<- [:profile/profile] + (fn [{:keys [news-feed-enabled?]}] + (boolean news-feed-enabled?))) + +(defn- replace-multiaccount-image-uri + [profile port font-file avatar-opts theme] + (let [{:keys [key-uid images + customization-color]} profile + images-with-uri (mapv (fn [{key-uid :keyUid + image-name :type + image-clock :clock + :as image}] + (assoc image + :config + {:type :account + :options (merge + {:port port + :ratio pixel-ratio/ratio + :image-name image-name + :clock image-clock + :key-uid key-uid + :theme theme} + avatar-opts)})) + images) + new-images (if (seq images-with-uri) + images-with-uri + [{:config {:type :initials + :options (merge + {:port port + :ratio pixel-ratio/ratio + :uppercase-ratio + (:uppercase-ratio + constants/initials-avatar-font-conf) + :key-uid key-uid + :customization-color customization-color + :theme theme + :font-file font-file} + avatar-opts)}}])] + (assoc profile :images new-images))) + +(re-frame/reg-sub + :profile/profile-with-image + :<- [:profile/profile] + :<- [:mediaserver/port] + :<- [:initials-avatar-font-file] + :<- [:theme] + (fn [[profile port font-file theme] [_ avatar-opts]] + ;; Right after logout, this subscription is recomputed, but the sub + ;; `:profile/profile` output will always be nil. We skip any further + ;; processing because it's wasteful and because it will trigger a schema + ;; error. + (when profile + (replace-multiaccount-image-uri profile port font-file avatar-opts theme)))) + +(re-frame/reg-sub + :profile/image + :<- [:profile/profile-with-image] + (fn [profile] + (profile.utils/photo profile))) + +(re-frame/reg-sub + :profile/name + :<- [:profile/profile] + (fn [profile] + (profile.utils/displayed-name profile))) + +(re-frame/reg-sub + :profile/login-profile + :<- [:profile/login] + :<- [:profile/profiles-overview] + (fn [[{:keys [key-uid]} profiles]] + (get profiles key-uid))) + +(re-frame/reg-sub + :profile/mnemonic + :<- [:profile/profile] + :-> :mnemonic) + +(re-frame/reg-sub + :profile/notifications-settings + :<- [:profile/profile] + (fn [{:keys [notifications-blocked? + notifications-enabled? + news-notifications-enabled? + messenger-notifications-enabled? + push-notifications-block-mentions? + push-notifications-from-contacts-only?]}] + (let [blocked? (boolean notifications-blocked?)] + {:notifications-blocked? blocked? + :notifications-enabled? (and (boolean notifications-enabled?) + (not blocked?)) + :news-notifications-enabled? (boolean news-notifications-enabled?) + :messenger-notifications-enabled? (boolean messenger-notifications-enabled?) + :non-contact-notifications-enabled? (not (boolean push-notifications-from-contacts-only?)) + :community-mentions-notifications-enabled? (not (boolean push-notifications-block-mentions?))}))) diff --git a/src/status_im/subs/profile_test.cljs b/src/status_im/subs/profile_test.cljs new file mode 100644 index 00000000000..55406b13981 --- /dev/null +++ b/src/status_im/subs/profile_test.cljs @@ -0,0 +1,130 @@ +(ns status-im.subs.profile-test + (:require [cljs.test :refer [is testing use-fixtures]] + [re-frame.db :as rf-db] + status-im.subs.root + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(use-fixtures :each + {:before #(reset! rf-db/app-db {})}) + +(def sample-profile + {:keycard-pairing nil + :send-push-notifications? true + :send-status-updates? true + :key-uid "0x2285f5c1ffd94ade0aa3568bff85f6c06f2860391ba65ccf56276cbc6829a22a" + :backup-enabled? true + :address "0x70F8913fbE0Ca5687F1Fb73068944d6e99B27804" + :mnemonic "lucky veteran business source debris large priority color endless answer strong pave" + :preview-privacy? true + :identicon "" + :use-mailservers? true + :signing-phrase "polo rush vest" + :url-unfurling-mode 1 + :custom-bootnodes-enabled? {} + :log-level "INFO" + :profile-pictures-visibility 2 + :messages-from-contacts-only false + :pinned-mailservers {} + :eip1581-address "0x15636c0aa4036b9f984e8998db085328795b26d8" + :images [{:keyUid "0x2285f5c1ffd94ade0aa3568bff85f6c06f2860391ba65ccf56276cbc6829a22a" + :type "large" + :uri "data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJC=" + :width 240 + :height 240 + :fileSize 15973 + :resizeTarget 240 + :clock 0} + {:keyUid "0x2285f5c1ffd94ade0aa3568bff85f6c06f2860391ba65ccf56276cbc6829a22a" + :type "thumbnail" + :uri "data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJC=" + :width 80 + :height 240 + :fileSize 2558 + :resizeTarget 80 + :clock 0}] + :name "Plush Shiny Songbird" + :latest-derived-path 0 + :compressed-key "zQ3shS6tp3NsQT4RSUFtnTqnBQzC5kt2SZzxZmnPEiNkHetwj" + :wallet-legacy/visible-tokens {:mainnet #{:SNT}} + :kdfIterations 3200 + :ens-name? false + :emoji-hash ["👮" "🧑🏿‍🏭" "📬" "👰‍♀️" "🦚" "💳" "👨🏿‍🍳" "☝️" "🤰🏾" "🍊" "☁️" "☔" "👷🏽" "🤹🏾"] + :wallet-root-address "0x704c9a261b918cb8e522f7fc2bc477c12d0c74ac" + :last-backup 1701832050 + :link-previews-enabled-sites #{} + :wakuv2-config {:Port 0 + :DataDir "" + :LightClient true + :AutoUpdate true + :MaxMessageSize 0 + :KeepAliveInterval 0 + :Nameserver "" + :UseShardAsDefaultTopic false + :PeerExchange true + :StoreCapacity 0 + :UDPPort 0 + :EnableStore false + :EnableFilterFullNode false + :Enabled true + :EnableConfirmations false + :Host "0.0.0.0" + :CustomNodes {} + :FullNode false + :EnableDiscV5 true + :DiscoveryLimit 20 + :StoreSeconds 0} + :current-user-visibility-status {:clock 1701798568 + :text "" + :status-type 1} + :gifs/api-key "" + :currency :usd + :gifs/favorite-gifs nil + :customization-color :magenta + :default-sync-period 777600 + :photo-path "" + :dapps-address "0x52fB56556A039244CED121AFB9ec829788Db78c8" + :custom-bootnodes {} + :display-name "Alisher Y" + :gifs/recent-gifs nil + :appearance 0 + :link-preview-request-enabled true + :profile-pictures-show-to 2 + :timestamp 1701798892 + :device-name "" + :colorId 2 + :networks/current-network "mainnet_rpc" + :mutual-contact-enabled? false + :public-key + "0x0445b4d3a20f9fcf95b9e669857f83a073e7fdb7b79d0ac03ffb601d6889c413fa86282a2b2bed46ecf7d499807c1567549367a4eaa2b7b925067d44562d93cfa6" + :colorHash [[3 25] [4 3] [5 4] [2 0] [1 10] [5 2] [2 4] [1 17] [3 23] [2 19] [4 1]] + :installation-id "cee7e269-1ca7-4468-a1dd-e60e5cfb0894"}) + +(def sample-currency + {:usd {:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false}}) + +(h/deftest-sub :profile/currency + [sub-name] + (testing "returns the selected currency of user" + (swap! rf-db/app-db #(assoc % :profile/profile sample-profile)) + (is (match? :usd (rf/sub [sub-name]))))) + +(h/deftest-sub :profile/currency-symbol + [sub-name] + (testing "returns the symbol of the user's selected currency" + (swap! rf-db/app-db assoc + :profile/profile sample-profile + :currencies sample-currency) + (is (match? "$" (rf/sub [sub-name]))))) + +(h/deftest-sub :profile/public-key + [sub-name] + (testing "returns the user's public key" + (swap! rf-db/app-db #(assoc % :profile/profile sample-profile)) + (is (match? (:public-key sample-profile) (rf/sub [sub-name]))))) diff --git a/src/status_im/subs/root.cljs b/src/status_im/subs/root.cljs new file mode 100644 index 00000000000..464f0b1898f --- /dev/null +++ b/src/status_im/subs/root.cljs @@ -0,0 +1,200 @@ +(ns status-im.subs.root + (:require + status-im.domain.subs + status-im.subs.activity-center + status-im.subs.alert-banner + status-im.subs.biometrics + status-im.subs.chats + status-im.subs.communities + status-im.subs.community.account-selection + status-im.subs.contact + status-im.subs.general + status-im.subs.keycard + status-im.subs.messages + status-im.subs.onboarding + status-im.subs.pairing + status-im.subs.profile + status-im.subs.settings + status-im.subs.shell + status-im.subs.standard-authentication + status-im.subs.wallet.activities + status-im.subs.wallet.buy + status-im.subs.wallet.collectibles + status-im.subs.wallet.dapps.core + status-im.subs.wallet.networks + status-im.subs.wallet.saved-addresses + status-im.subs.wallet.send + status-im.subs.wallet.swap + status-im.subs.wallet.wallet + status-im.ui.market.subs + [utils.re-frame :as rf])) + +;;view +(rf/reg-root-key-sub :view-id :view-id) +(rf/reg-root-key-sub :screen-params :navigation/screen-params) +(rf/reg-root-key-sub :animation-shared-element-id :animation-shared-element-id) + +;;bottom sheet +(rf/reg-root-key-sub :bottom-sheet :bottom-sheet) + +;;media-server +(rf/reg-root-key-sub :mediaserver/port :mediaserver/port) + +;;push notifications +(rf/reg-root-key-sub :push-notifications/preferences :push-notifications/preferences) + +;;device +(rf/reg-root-key-sub :network/status :network/status) +(rf/reg-root-key-sub :network/type :network/type) + +;;general +(rf/reg-root-key-sub :messenger/started? :messenger/started?) +(rf/reg-root-key-sub :animations :animations) +(rf/reg-root-key-sub :toasts :toasts) +(rf/reg-root-key-sub :popover/popover :popover/popover) +(rf/reg-root-key-sub :auth-method :auth-method) +(rf/reg-root-key-sub :syncing :syncing) +(rf/reg-root-key-sub :sync-state :sync-state) +(rf/reg-root-key-sub :dimensions/window :dimensions/window) +(rf/reg-root-key-sub :sync-data :sync-data) +(rf/reg-root-key-sub :mobile-network/remember-choice? :mobile-network/remember-choice?) +(rf/reg-root-key-sub :qr-modal :qr-modal) +(rf/reg-root-key-sub :bootnodes/manage :bootnodes/manage) +(rf/reg-root-key-sub :wakuv2-nodes/manage :wakuv2-nodes/manage) +(rf/reg-root-key-sub :wakuv2-nodes/list :wakuv2-nodes/list) +(rf/reg-root-key-sub :networks/current-network :networks/current-network) +(rf/reg-root-key-sub :networks/networks :networks/networks) +(rf/reg-root-key-sub :networks/manage :networks/manage) +(rf/reg-root-key-sub :get-pairing-installations :pairing/installations) +(rf/reg-root-key-sub :tooltips :tooltips) +(rf/reg-root-key-sub :app-state :app-state) +(rf/reg-root-key-sub :home-items-show-number :home-items-show-number) +(rf/reg-root-key-sub :password-authentication :password-authentication) +(rf/reg-root-key-sub :initials-avatar-font-file :initials-avatar-font-file) +(rf/reg-root-key-sub :alert-banners :alert-banners) +(rf/reg-root-key-sub :alert-banners/hide? :alert-banners/hide?) +(rf/reg-root-key-sub :currencies :currencies) +(rf/reg-root-key-sub :enter-seed-phrase/error :enter-seed-phrase/error) +(rf/reg-root-key-sub :privacy-mode/privacy-mode-enabled? :privacy-mode/privacy-mode-enabled?) + +;;onboarding +(rf/reg-root-key-sub :onboarding/generated-keys? :onboarding/generated-keys?) +(rf/reg-root-key-sub :onboarding/new-account? :onboarding/new-account?) +(rf/reg-root-key-sub :onboarding/profile :onboarding/profile) + +;;my profile +(rf/reg-root-key-sub :my-profile/seed :my-profile/seed) +;;profiles +(rf/reg-root-key-sub :profile/profiles-overview :profile/profiles-overview) +(rf/reg-root-key-sub :profile/login :profile/login) +(rf/reg-root-key-sub :profile/profile :profile/profile) +(rf/reg-root-key-sub :profile/logging-out? :profile/logging-out?) +(rf/reg-root-key-sub :profile/wallet-accounts :profile/wallet-accounts) + +(rf/reg-root-key-sub :multiaccount/reset-password-form-vals :multiaccount/reset-password-form-vals) +(rf/reg-root-key-sub :multiaccount/reset-password-errors :multiaccount/reset-password-errors) +(rf/reg-root-key-sub :multiaccount/resetting-password? :multiaccount/resetting-password?) + +;;chat +(rf/reg-root-key-sub :chats/cooldown-enabled? :chat/cooldown-enabled?) +(rf/reg-root-key-sub :chats/chats :chats) +(rf/reg-root-key-sub :chats/current-chat-id :current-chat-id) +(rf/reg-root-key-sub :public-group-topic :public-group-topic) +(rf/reg-root-key-sub :new-chat-name :new-chat-name) +(rf/reg-root-key-sub :chat/inputs :chat/inputs) +(rf/reg-root-key-sub :chat/memberships :chat/memberships) +(rf/reg-root-key-sub :group-chat/invitations :group-chat/invitations) +(rf/reg-root-key-sub :chats/mention-suggestions :chats/mention-suggestions) + +(rf/reg-root-key-sub :chats-home-list :chats-home-list) +(rf/reg-root-key-sub :chats/recording? :chats/recording?) +(rf/reg-root-key-sub :reactions/authors :reactions/authors) + +;;chat images lightbox +(rf/reg-root-key-sub :lightbox/exit-signal :lightbox/exit-signal) +(rf/reg-root-key-sub :lightbox/zoom-out-signal :lightbox/zoom-out-signal) +(rf/reg-root-key-sub :lightbox/orientation :lightbox/orientation) +(rf/reg-root-key-sub :lightbox/scale :lightbox/scale) + +;;chat images camera roll +(rf/reg-root-key-sub :camera-roll/photos :camera-roll/photos) +(rf/reg-root-key-sub :camera-roll/end-cursor :camera-roll/end-cursor) +(rf/reg-root-key-sub :camera-roll/has-next-page :camera-roll/has-next-page) +(rf/reg-root-key-sub :camera-roll/loading-more :camera-roll/loading-more) +(rf/reg-root-key-sub :camera-roll/albums :camera-roll/albums) +(rf/reg-root-key-sub :camera-roll/selected-album :camera-roll/selected-album) + +;;group chat +(rf/reg-root-key-sub :group-chat/selected-participants :group-chat/selected-participants) +(rf/reg-root-key-sub :group-chat/deselected-members :group-chat/deselected-members) +(rf/reg-root-key-sub :group-chat/manage-members-error :group-chat/manage-members-error) + +;;messages +(rf/reg-root-key-sub :messages/messages :messages) +(rf/reg-root-key-sub :messages/reactions :reactions) +(rf/reg-root-key-sub :messages/message-lists :message-lists) +(rf/reg-root-key-sub :messages/pagination-info :pagination-info) +(rf/reg-root-key-sub :messages/pin-message-lists :pin-message-lists) +(rf/reg-root-key-sub :messages/pin-messages :pin-messages) +(rf/reg-root-key-sub :messages/pin-modal :pin-modal) + +(rf/reg-root-key-sub :messages-home/selected-tab :messages-home/selected-tab) + +;;communities +(rf/reg-root-key-sub :communities :communities) +(rf/reg-root-key-sub :communities/create :communities/create) +(rf/reg-root-key-sub :communities/create-channel :communities/create-channel) +(rf/reg-root-key-sub :communities/requests-to-join :communities/requests-to-join) +(rf/reg-root-key-sub :communities/community-id-input :communities/community-id-input) +(rf/reg-root-key-sub :communities/fetching-communities :communities/fetching-communities) +(rf/reg-root-key-sub :communities/my-pending-requests-to-join :communities/my-pending-requests-to-join) +(rf/reg-root-key-sub :communities/collapsed-categories :communities/collapsed-categories) +(rf/reg-root-key-sub :communities/selected-tab :communities/selected-tab) +(rf/reg-root-key-sub :contract-communities :contract-communities) +(rf/reg-root-key-sub :communities/permissioned-balances :communities/permissioned-balances) +(rf/reg-root-key-sub :communities/permissions-check :communities/permissions-check) +(rf/reg-root-key-sub :communities/permissions-check-all :communities/permissions-check-all) +(rf/reg-root-key-sub :communities/all-addresses-to-reveal :communities/all-addresses-to-reveal) +(rf/reg-root-key-sub :communities/all-airdrop-addresses :communities/all-airdrop-addresses) +(rf/reg-root-key-sub :communities/selected-share-all-addresses :communities/selected-share-all-addresses) +(rf/reg-root-key-sub :communities/permissions-checks-for-selection + :communities/permissions-checks-for-selection) +(rf/reg-root-key-sub :communities/channel-permissions-check :communities/channel-permissions-check) +(rf/reg-root-key-sub :communities/join-requests-for-signing :communities/join-requests-for-signing) + +;;activity center +(rf/reg-root-key-sub :activity-center :activity-center) + +;;wallet +(rf/reg-root-key-sub :wallet :wallet) + +;;wallet-connect +(rf/reg-root-key-sub :wallet-connect/web3-wallet :wallet-connect/web3-wallet) +(rf/reg-root-key-sub :wallet-connect/current-proposal :wallet-connect/current-proposal) +(rf/reg-root-key-sub :wallet-connect/current-request :wallet-connect/current-request) +(rf/reg-root-key-sub :wallet-connect/sessions :wallet-connect/sessions) + +;;biometrics +(rf/reg-root-key-sub :biometrics :biometrics) + +;;settings +(rf/reg-root-key-sub :settings/change-password :settings/change-password) + +;;debug +(when js/goog.DEBUG + (rf/reg-root-key-sub :dev/previewed-component :dev/previewed-component)) + +;;theme +(rf/reg-root-key-sub :theme :theme) + +;; centralized-metrics +(rf/reg-root-key-sub :centralized-metrics/enabled? :centralized-metrics/enabled?) +(rf/reg-root-key-sub :centralized-metrics/user-confirmed? :centralized-metrics/user-confirmed?) +(rf/reg-root-key-sub :centralized-metrics/user-id :centralized-metrics/user-id) + +;;keycard +(rf/reg-root-key-sub :keycard :keycard) + +(rf/reg-root-key-sub :log-level/pre-login-log-level :log-level/pre-login-log-level) + +;; domain.market diff --git a/src/status_im/subs/settings.cljs b/src/status_im/subs/settings.cljs new file mode 100644 index 00000000000..42ceb6898f5 --- /dev/null +++ b/src/status_im/subs/settings.cljs @@ -0,0 +1,17 @@ +(ns status-im.subs.settings + (:require [re-frame.core :as rf])) + +(rf/reg-sub :settings/change-password-current-step + :<- [:settings/change-password] + (fn [change-password] + (or (get change-password :current-step) :old-password))) + +(rf/reg-sub :settings/change-password-loading + :<- [:settings/change-password] + (fn [change-password] + (get change-password :loading?))) + +(rf/reg-sub :settings/change-password-error + :<- [:settings/change-password] + (fn [change-password] + (get change-password :verify-error))) diff --git a/src/status_im/subs/shell.cljs b/src/status_im/subs/shell.cljs new file mode 100644 index 00000000000..bf78aaa22a5 --- /dev/null +++ b/src/status_im/subs/shell.cljs @@ -0,0 +1,51 @@ +(ns status-im.subs.shell + (:require + [re-frame.core :as re-frame] + [status-im.constants :as constants])) + +;; Bottom tabs +(re-frame/reg-sub + :shell/bottom-tabs-notifications-data + :<- [:chats/chats] + (fn [chats] + (let [{:keys [chats-stack community-stack]} + (reduce + (fn [acc [_ {:keys [unviewed-messages-count unviewed-mentions-count chat-type muted]}]] + (cond + (and (not muted) + (= chat-type constants/community-chat-type)) + (-> acc + (update-in [:community-stack :unviewed-messages-count] + unviewed-messages-count) + (update-in [:community-stack :unviewed-mentions-count] + unviewed-mentions-count)) + + (and (not muted) + (= chat-type constants/private-group-chat-type)) + (-> acc + (update-in [:chats-stack :unviewed-messages-count] + unviewed-messages-count) + (update-in [:chats-stack :unviewed-mentions-count] + unviewed-mentions-count)) + + (and (not muted) + (= chat-type constants/one-to-one-chat-type)) + ;; Note - for 1-1 chats, all unread messages are counted as mentions and shown with + ;; counter + (-> acc + (update-in [:chats-stack :unviewed-messages-count] + unviewed-messages-count) + (update-in [:chats-stack :unviewed-mentions-count] + unviewed-messages-count)) + + :else + acc)) + {:chats-stack {:unviewed-messages-count 0 :unviewed-mentions-count 0} + :community-stack {:unviewed-messages-count 0 :unviewed-mentions-count 0}} + chats)] + {:screen/communities-stack + {:new-notifications? (pos? (:unviewed-messages-count community-stack)) + :notification-indicator (if (pos? (:unviewed-mentions-count community-stack)) + :counter + :unread-dot) + :counter-label (:unviewed-mentions-count community-stack)} + :screen/chats-stack + {:new-notifications? (pos? (:unviewed-messages-count chats-stack)) + :notification-indicator (if (pos? (:unviewed-mentions-count chats-stack)) + :counter + :unread-dot) + :counter-label (:unviewed-mentions-count chats-stack)}}))) diff --git a/src/status_im/subs/shell_test.cljs b/src/status_im/subs/shell_test.cljs new file mode 100644 index 00000000000..9f11392987e --- /dev/null +++ b/src/status_im/subs/shell_test.cljs @@ -0,0 +1,59 @@ +(ns status-im.subs.shell-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + [status-im.constants :as constants] + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +;; Note - for 1-1 chats, all unread messages are counted as mentions and shown with counter +(def one-to-one-group-community-chats1 + {"0xone-to-one-chat1" {:chat-type constants/one-to-one-chat-type + :unviewed-messages-count 0 + :unviewed-mentions-count 0} + "0xgroup-chat1" {:chat-type constants/private-group-chat-type + :unviewed-messages-count 2 + :unviewed-mentions-count 0} + "0xcommunity-chat1" {:chat-type constants/community-chat-type + :unviewed-messages-count 3 + :unviewed-mentions-count 0}}) + +(def expected-notification-data-for-one-to-one-group-community-chats1 + {:screen/communities-stack {:new-notifications? true + :notification-indicator :unread-dot + :counter-label 0} + :screen/chats-stack {:new-notifications? true + :notification-indicator :unread-dot + :counter-label 0}}) + +(def one-to-one-group-community-chats2 + (merge + one-to-one-group-community-chats1 + {"0xone-to-one-chat2" {:chat-type constants/one-to-one-chat-type + :unviewed-messages-count 8 + :unviewed-mentions-count 6} + "0xgroup-chat2" {:chat-type constants/private-group-chat-type + :unviewed-messages-count 4 + :unviewed-mentions-count 3} + "0xcommunity-chat2" {:chat-type constants/community-chat-type + :unviewed-messages-count 9 + :unviewed-mentions-count 7}})) + +(def expected-notification-data-for-one-to-one-group-community-chats2 + {:screen/communities-stack {:new-notifications? true + :notification-indicator :counter + :counter-label 7} + :screen/chats-stack {:new-notifications? true + :notification-indicator :counter + :counter-label 11}}) + +(h/deftest-sub :shell/bottom-tabs-notifications-data + [sub-name] + (testing "chats with only unviewed-messages, without unviewed-mentions count should use unread-dot" + (swap! rf-db/app-db assoc :chats one-to-one-group-community-chats1) + (is (= (rf/sub [sub-name]) expected-notification-data-for-one-to-one-group-community-chats1))) + + (testing + "chats with both unviewed-messages and unviewed-mentions count should use counter with mentions count" + (swap! rf-db/app-db assoc :chats one-to-one-group-community-chats2) + (is (= (rf/sub [sub-name]) expected-notification-data-for-one-to-one-group-community-chats2)))) diff --git a/src/status_im/subs/standard_authentication.cljs b/src/status_im/subs/standard_authentication.cljs new file mode 100644 index 00000000000..9edbc489381 --- /dev/null +++ b/src/status_im/subs/standard_authentication.cljs @@ -0,0 +1,19 @@ +(ns status-im.subs.standard-authentication + (:require [status-im.common.biometric.utils :as biometric] + [status-im.constants :as constants] + [utils.re-frame :as rf])) + +(rf/reg-sub + :standard-auth/slider-icon + :<- [:auth-method] + :<- [:biometrics/supported-type] + :<- [:keycard/keycard-profile?] + (fn [[auth-method biometrics-type keycard-pairing?]] + (let [icons {:biometrics (biometric/get-icon-by-type biometrics-type) + :password :password + :keycard :i/keycard} + auth-with-biometrics? (= auth-method constants/auth-method-biometric)] + (cond + keycard-pairing? (:keycard icons) + auth-with-biometrics? (:biometrics icons) + :else (:password icons))))) diff --git a/src/status_im/subs/wallet/activities.cljs b/src/status_im/subs/wallet/activities.cljs new file mode 100644 index 00000000000..c7c46a3fec4 --- /dev/null +++ b/src/status_im/subs/wallet/activities.cljs @@ -0,0 +1,176 @@ +(ns status-im.subs.wallet.activities + (:require + [quo.foundations.resources :as resources] + [re-frame.core :as rf] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.activity-tab.constants :as activity-constants] + [status-im.contexts.wallet.send.utils :as send-utils] + [utils.collection :as collection] + [utils.datetime :as datetime] + [utils.hex :as hex] + [utils.money :as money])) + +(defn- hex->number + [hex-str] + (-> hex-str + hex/normalize-hex + money/from-hex + str)) + +(defn- hex->amount + [hex-str native-token? token-decimals] + (-> hex-str + hex->number + (send-utils/convert-wei-to-eth native-token? token-decimals) + str)) + +(defn- get-token-amount + [{:keys [token-type]} naitve-token? amount decimals] + (if (#{activity-constants/wallet-activity-token-type-erc-721 + activity-constants/wallet-activity-token-type-erc-1155} + token-type) + (hex->number amount) + (hex->amount amount naitve-token? decimals))) + +(defn- get-address-context-tag + [accounts-and-saved-addresses address] + (let [{:keys [context-type name emoji color customization-color]} + (get accounts-and-saved-addresses address)] + (case context-type + :account + {:type :account + :account-name name + :emoji emoji + :customization-color color} + + :saved-address + {:type :default + :full-name name + :customization-color customization-color} + + {:type :address :address address}))) + +(defn- get-spender-context-tag + [address] + (let [swap-providers (->> constants/swap-providers + vals + (collection/index-by :contract-address)) + {:keys [full-name name] :as spender} (get swap-providers address)] + (if spender + {:type :network + :network-name full-name + :network-logo (resources/get-network name)} + {:type :address :address address}))) + +(defn- process-base-activity + [{:keys [timestamp sender recipient token-in token-out amount-in amount-out symbol-in symbol-out + chain-id-in chain-id-out activity-status] + :as activity} + {:keys [chain-id->network-details accounts-and-saved-addresses tokens-by-symbol]}] + (let [token-id (some-> (or token-in token-out) + :token-id + hex->number) + network-in (chain-id->network-details chain-id-in) + network-out (chain-id->network-details chain-id-out) + native-token-in? (= (:native-currency-symbol network-in) symbol-in) + native-token-out? (= (:native-currency-symbol network-out) symbol-out) + amount-out (when amount-out + (get-token-amount token-out + native-token-out? + amount-out + (get-in tokens-by-symbol + [symbol-out :decimals]))) + amount-in (when amount-in + (get-token-amount token-in + native-token-in? + amount-in + (get-in tokens-by-symbol + [symbol-in :decimals])))] + (assoc activity + :relative-date (datetime/timestamp->relative (* timestamp 1000)) + :sender-tag (get-address-context-tag accounts-and-saved-addresses sender) + :recipient-tag (get-address-context-tag accounts-and-saved-addresses recipient) + :network-name-in (:full-name network-in) + :network-logo-in (resources/get-network (:network-name network-in)) + :network-name-out (:full-name network-out) + :network-logo-out (resources/get-network (:network-name network-out)) + :status (activity-constants/wallet-activity-status->name activity-status) + :token-id token-id + :amount-out amount-out + :amount-in amount-in))) + +(defn- process-activity + [{:keys [activity-type approval-spender] + :as activity} + context] + (let [base-activity (process-base-activity activity context)] + (condp = activity-type + activity-constants/wallet-activity-type-send + (assoc base-activity :tx-type :send) + + activity-constants/wallet-activity-type-bridge + (assoc base-activity :tx-type :bridge) + + activity-constants/wallet-activity-type-swap + (assoc base-activity :tx-type :swap) + + activity-constants/wallet-activity-type-approval + (assoc base-activity + :tx-type :approval + :spender-tag (get-spender-context-tag approval-spender)) + + nil))) + +(rf/reg-sub + :wallet/all-activities + :<- [:wallet] + :-> :activities) + +(rf/reg-sub + :wallet/activity-tab + :<- [:wallet/ui] + :-> :activity-tab) + +(rf/reg-sub + :wallet/activity-tab-request + :<- [:wallet/activity-tab] + :-> :request) + +(rf/reg-sub + :wallet/activity-tab-loading? + :<- [:wallet/activity-tab-request] + :-> :loading?) + +(rf/reg-sub + :wallet/accounts-and-saved-addresses + :<- [:wallet/accounts-without-assets] + :<- [:wallet/saved-addresses-by-network-mode] + (fn [[accounts saved-addresses]] + (merge + (collection/index-by :address (map #(assoc % :context-type :account) accounts)) + (collection/index-by :address + (map #(assoc % :context-type :saved-address) (vals saved-addresses)))))) + +(rf/reg-sub + :wallet/activities-for-current-viewing-account + :<- [:wallet/all-activities] + :<- [:wallet/tokens-by-symbol] + :<- [:wallet/current-viewing-account-address] + :<- [:wallet/networks-by-id] + :<- [:wallet/accounts-and-saved-addresses] + (fn [[activities tokens-by-symbol current-address networks-by-id accounts-and-saved-addresses]] + (let [context {:tokens-by-symbol tokens-by-symbol + :chain-id->network-details networks-by-id + :accounts-and-saved-addresses accounts-and-saved-addresses} + activities (->> (get activities current-address) + vals + (keep #(process-activity % context)) + (sort-by :timestamp >) + (group-by #(datetime/timestamp->relative-short-date + (* (:timestamp %) 1000))))] + (->> activities + (map (fn [[date activities]] + {:title date + :data activities + :timestamp (:timestamp (first activities))})) + (sort-by :timestamp >))))) diff --git a/src/status_im/subs/wallet/activities_test.cljs b/src/status_im/subs/wallet/activities_test.cljs new file mode 100644 index 00000000000..39394fa6a8c --- /dev/null +++ b/src/status_im/subs/wallet/activities_test.cljs @@ -0,0 +1,131 @@ +(ns status-im.subs.wallet.activities-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + [status-im.contexts.wallet.common.activity-tab.constants :as constants] + [status-im.subs.root] + [status-im.subs.wallet.collectibles] + [test-helpers.unit :as h] + [tests.wallet-test-data :as test-data] + [utils.re-frame :as rf])) + +(h/deftest-sub :wallet/all-activities + [sub-name] + (testing "Return the activities list from wallet data" + (swap! rf-db/app-db assoc-in + [:wallet :activities] + [{:id 1 :name "Transaction1"} + {:id 2 :name "Transaction2"}]) + (is (match? [{:id 1 :name "Transaction1"} {:id 2 :name "Transaction2"}] (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/activities-for-current-viewing-account + [sub-name] + (testing "Return activities filtered and grouped by account and dates" + (swap! rf-db/app-db + (fn [db] + (-> db + (test-data/add-networks-to-db) + (assoc-in [:wallet :tokens :by-symbols] + (list {:symbol "ETH" :decimals 18} + {:symbol "DAI" :decimals 18} + {:symbol "SNT" :decimals 18} + {:symbol "USDT" :decimals 6})) + (assoc-in [:wallet :activities] + {"0x1" {1 {:activity-type constants/wallet-activity-type-send + :amount-out "0x1" + :symbol-out "ETH" + :sender "0x1" + :recipient "0x2" + :chain-id-out 1 + :timestamp 1588291200} + 3 {:activity-type constants/wallet-activity-type-bridge + :amount-out "0x1" + :symbol-out "ETH" + :sender "0x1" + :recipient "0x1" + :chain-id-out 1 + :timestamp 1588464000} + 4 {:activity-type constants/wallet-activity-type-swap + :amount-out "0x1" + :symbol-out "ETH" + :amount-in "0x1" + :symbol-in "SNT" + :sender "0x1" + :recipient "0x1" + :chain-id-out 1 + :timestamp 1588464100} + 5 {:activity-type constants/wallet-activity-type-send + :amount-out "0x1" + :symbol-out "ETH" + :sender "0x1" + :recipient "0x4" + :chain-id-out 1 + :timestamp 1588464050}} + "0x3" {6 {:activity-type constants/wallet-activity-type-receive + :amount-in "0x1" + :symbol-out "ETH" + :sender "0x4" + :recipient "0x3" + :chain-id-in 1 + :timestamp 1588464000}}}) + (assoc-in [:wallet :current-viewing-account-address] "0x1")))) + (is + (match? + [{:title "May 3, 2020" + :timestamp 1588464100 + :data [{:relative-date "May 3, 2020" + :amount-out "0" + :recipient "0x1" + :token-id nil + :amount-in "" + :tx-type :swap + :activity-type 3 + :network-name-in nil + :network-name-out "Mainnet" + :symbol-in "SNT" + :symbol-out "ETH" + :status nil + :sender "0x1" + :timestamp 1588464100} + {:relative-date "May 3, 2020" + :amount-out "0" + :recipient "0x4" + :token-id nil + :amount-in nil + :tx-type :send + :activity-type 0 + :network-name-in nil + :network-name-out "Mainnet" + :symbol-out "ETH" + :status nil + :sender "0x1" + :timestamp 1588464050} + {:relative-date "May 3, 2020" + :amount-out "0" + :recipient "0x1" + :token-id nil + :amount-in nil + :tx-type :bridge + :activity-type 4 + :network-name-in nil + :network-name-out "Mainnet" + :symbol-out "ETH" + :status nil + :sender "0x1" + :timestamp 1588464000}]} + {:title "May 1, 2020" + :timestamp 1588291200 + :data [{:relative-date "May 1, 2020" + :amount-out "0" + :recipient "0x2" + :token-id nil + :amount-in nil + :tx-type :send + :activity-type 0 + :network-name-in nil + :network-name-out "Mainnet" + :symbol-out "ETH" + :status nil + :sender "0x1" + :timestamp 1588291200}]}] + (rf/sub [sub-name]))))) diff --git a/src/status_im/subs/wallet/add_account/address_to_watch.cljs b/src/status_im/subs/wallet/add_account/address_to_watch.cljs new file mode 100644 index 00000000000..7c4b7477412 --- /dev/null +++ b/src/status_im/subs/wallet/add_account/address_to_watch.cljs @@ -0,0 +1,17 @@ +(ns status-im.subs.wallet.add-account.address-to-watch + (:require [re-frame.core :as rf])) + +(rf/reg-sub + :wallet/add-address-to-watch + :<- [:wallet/ui] + :-> :add-address-to-watch) + +(rf/reg-sub + :wallet/watch-address-activity-state + :<- [:wallet/add-address-to-watch] + :-> :activity-state) + +(rf/reg-sub + :wallet/watch-address-validated-address + :<- [:wallet/add-address-to-watch] + :-> :validated-address) diff --git a/src/status_im/subs/wallet/buy.cljs b/src/status_im/subs/wallet/buy.cljs new file mode 100644 index 00000000000..93b794ced32 --- /dev/null +++ b/src/status_im/subs/wallet/buy.cljs @@ -0,0 +1,22 @@ +(ns status-im.subs.wallet.buy + (:require [re-frame.core :as rf])) + +(rf/reg-sub + :wallet/crypto-on-ramps + :<- [:wallet] + :-> :crypto-on-ramps) + +(rf/reg-sub + :wallet/wallet-buy-crypto + :<- [:wallet/ui] + :-> :buy-crypto) + +(rf/reg-sub + :wallet/wallet-buy-crypto-account + :<- [:wallet/wallet-buy-crypto] + :-> :account) + +(rf/reg-sub + :wallet/wallet-buy-crypto-provider + :<- [:wallet/wallet-buy-crypto] + :-> :provider) diff --git a/src/status_im/subs/wallet/collectibles.cljs b/src/status_im/subs/wallet/collectibles.cljs new file mode 100644 index 00000000000..bfbf3ace67c --- /dev/null +++ b/src/status_im/subs/wallet/collectibles.cljs @@ -0,0 +1,188 @@ +(ns status-im.subs.wallet.collectibles + (:require + [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.contexts.wallet.collectible.utils :as collectible-utils] + [utils.collection])) + +(defn- filter-collectibles-in-chains + [collectibles chain-ids] + (filter #(contains? chain-ids (get-in % [:id :contract-id :chain-id])) collectibles)) + +(defn- svg-animation? + [url media-type] + (and (not (string/blank? url)) + (string/includes? media-type "svg"))) + +(defn- animation? + [url media-type] + (and (not (string/blank? url)) + (not (string/blank? media-type)))) + +(defn- preview-url + [{{collectible-image-url :image-url + animation-url :animation-url + animation-media-type :animation-media-type} :collectible-data}] + (cond + (svg-animation? animation-url animation-media-type) + {:uri animation-url + :svg? true} + + (animation? animation-url animation-media-type) + {:uri animation-url} + + (not (string/blank? collectible-image-url)) + {:uri collectible-image-url} + + :else + {:uri nil})) + +(defn add-collectibles-preview-url + [collectibles] + (map (fn [collectible] + (assoc collectible :preview-url (preview-url collectible))) + collectibles)) + +(re-frame/reg-sub + :wallet/current-viewing-account-collectibles + :<- [:wallet/current-viewing-account] + (fn [current-account] + (-> current-account + :collectibles + add-collectibles-preview-url + collectible-utils/sort-collectibles-by-name))) + +(re-frame/reg-sub + :wallet/current-viewing-account-collectibles-in-active-networks + :<- [:wallet/current-viewing-account-collectibles] + :<- [:wallet/active-chain-ids] + (fn [[collectibles chain-ids]] + (filter-collectibles-in-chains collectibles chain-ids))) + +(re-frame/reg-sub + :wallet/current-viewing-account-collectibles-in-filtered-networks + :<- [:wallet/current-viewing-account-collectibles] + :<- [:wallet/filtered-chain-ids] + (fn [[collectibles chain-ids]] + (filter-collectibles-in-chains collectibles chain-ids))) + +(re-frame/reg-sub + :wallet/owned-collectibles-list + :<- [:wallet/accounts-without-watched-accounts] + (fn [accounts] + (let [max-collectibles (->> accounts + (map (comp count :collectibles)) + (apply max)) + all-collectibles (map (fn [{:keys [collectibles]}] + (let [amount-to-add (- max-collectibles (count collectibles)) + empty-collectibles (repeat amount-to-add nil)] + (reduce conj collectibles empty-collectibles))) + accounts)] + (->> all-collectibles + (apply interleave) + (remove nil?) + (utils.collection/distinct-by :id) + (add-collectibles-preview-url) + (collectible-utils/sort-collectibles-by-name))))) + +(re-frame/reg-sub + :wallet/owned-collectibles-list-in-filtered-networks + :<- [:wallet/owned-collectibles-list] + :<- [:wallet/filtered-chain-ids] + (fn [[all-collectibles chain-ids]] + (filter-collectibles-in-chains all-collectibles chain-ids))) + +(re-frame/reg-sub + :wallet/owned-collectibles-list-in-active-networks + :<- [:wallet/owned-collectibles-list] + :<- [:wallet/active-chain-ids] + (fn [[all-collectibles chain-ids]] + (filter-collectibles-in-chains all-collectibles chain-ids))) + +(re-frame/reg-sub + :wallet/collectibles-by-network + :<- [:wallet/owned-collectibles-list-in-active-networks] + (fn [collectibles] + (group-by (comp :chain-id :contract-id :id) collectibles))) + +(re-frame/reg-sub + :wallet/current-account-collectibles-by-network + :<- [:wallet/current-viewing-account-collectibles-in-active-networks] + (fn [collectibles] + (group-by (comp :chain-id :contract-id :id) collectibles))) + +(re-frame/reg-sub + :wallet/current-viewing-account-collectibles-filtered + :<- [:wallet/current-viewing-account-collectibles] + (fn [current-account-collectibles [_ search-text]] + (let [search-text-lower-case (string/lower-case search-text)] + (filter (fn [{{collection-name :name} :collection-data + {collectible-name :name} :collectible-data}] + (or (string/includes? (string/lower-case collection-name) search-text-lower-case) + (string/includes? (string/lower-case collectible-name) search-text-lower-case))) + current-account-collectibles)))) + +(re-frame/reg-sub + :wallet/collectible + :<- [:wallet/ui] + :-> :collectible) + +(re-frame/reg-sub + :wallet/collectible-details + :<- [:wallet/collectible] + (fn [collectible] + (as-> collectible $ + (:details $) + (assoc $ :preview-url (preview-url $))))) + +(re-frame/reg-sub + :wallet/collectible-aspect-ratio + :<- [:wallet/collectible] + (fn [collectible] + (:aspect-ratio collectible 1))) + +(re-frame/reg-sub + :wallet/collectible-gradient-color + :<- [:wallet/collectible] + (fn [collectible] + (:gradient-color collectible :gradient-1))) + +(re-frame/reg-sub + :wallet/total-owned-collectible + :<- [:wallet/accounts-without-watched-accounts] + (fn [accounts [_ ownership address]] + (let [addresses (if address + #{address} + (set (map :address accounts)))] + (reduce (fn [acc item] + (if (contains? addresses (:address item)) + (+ acc (js/parseInt (:balance item))) + acc)) + 0 + ownership)))) + +(re-frame/reg-sub + :wallet/collectibles + :<- [:wallet/ui] + :-> :collectibles) + +(re-frame/reg-sub + :wallet/collectibles-updating + :<- [:wallet/collectibles] + :-> :updating) + +(re-frame/reg-sub + :wallet/current-viewing-account-collectibles-updating? + :<- [:wallet/collectibles-updating] + :<- [:wallet/current-viewing-account-address] + (fn [[updating-addresses address]] + (contains? updating-addresses address))) + +(re-frame/reg-sub + :wallet/home-tab-collectibles-updating? + :<- [:wallet/collectibles-updating] + :<- [:wallet/accounts-without-watched-accounts] + (fn [[updating-addresses accounts]] + (->> accounts + (map :address) + (every? #(contains? updating-addresses %))))) diff --git a/src/status_im/subs/wallet/dapps/core.cljs b/src/status_im/subs/wallet/dapps/core.cljs new file mode 100644 index 00000000000..252a3393bdf --- /dev/null +++ b/src/status_im/subs/wallet/dapps/core.cljs @@ -0,0 +1,17 @@ +(ns status-im.subs.wallet.dapps.core + (:require [re-frame.core :as rf] + [status-im.contexts.wallet.common.utils :as wallet-utils] + status-im.subs.wallet.dapps.proposals + status-im.subs.wallet.dapps.requests + status-im.subs.wallet.dapps.sessions + status-im.subs.wallet.dapps.transactions + [utils.string])) + +(rf/reg-sub + :wallet-connect/account-details-by-address + :<- [:wallet/accounts-without-watched-accounts] + (fn [accounts [_ address]] + (let [{:keys [customization-color name emoji]} (wallet-utils/get-account-by-address accounts address)] + {:customization-color customization-color + :name name + :emoji emoji}))) diff --git a/src/status_im/subs/wallet/dapps/proposals.cljs b/src/status_im/subs/wallet/dapps/proposals.cljs new file mode 100644 index 00000000000..42f81ff4c10 --- /dev/null +++ b/src/status_im/subs/wallet/dapps/proposals.cljs @@ -0,0 +1,45 @@ +(ns status-im.subs.wallet.dapps.proposals + (:require [re-frame.core :as rf] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [utils.string])) + +(rf/reg-sub + :wallet-connect/current-proposal-request + :<- [:wallet-connect/current-proposal] + :-> :request) + +(rf/reg-sub + :wallet-connect/session-proposal-networks + :<- [:wallet-connect/current-proposal] + :-> :session-networks) + +(rf/reg-sub + :wallet-connect/session-proposer + :<- [:wallet-connect/current-proposal-request] + (fn [proposal] + (-> proposal :params :proposer))) + +(rf/reg-sub + :wallet-connect/session-proposer-name + :<- [:wallet-connect/session-proposer] + (fn [proposer] + (let [{:keys [name url]} (-> proposer :metadata)] + (data-store/compute-dapp-name name url)))) + +(rf/reg-sub + :wallet-connect/session-proposal-network-details + :<- [:wallet-connect/session-proposal-networks] + :<- [:wallet/networks] + (fn [[session-networks networks]] + (let [supported-networks (map :chain-id networks) + session-networks (filterv #(contains? (set session-networks) (:chain-id %)) + networks) + all-networks-in-session? (= (count supported-networks) (count session-networks))] + {:session-networks session-networks + :all-networks-in-session? all-networks-in-session?}))) + +(rf/reg-sub + :wallet-connect/current-proposal-address + (fn [db] + (get-in db [:wallet-connect/current-proposal :address]))) diff --git a/src/status_im/subs/wallet/dapps/proposals_test.cljs b/src/status_im/subs/wallet/dapps/proposals_test.cljs new file mode 100644 index 00000000000..51c9301e2c5 --- /dev/null +++ b/src/status_im/subs/wallet/dapps/proposals_test.cljs @@ -0,0 +1,84 @@ +(ns status-im.subs.wallet.dapps.proposals-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + status-im.subs.root + status-im.subs.wallet.dapps.core + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def sample-session + {:session-proposal + {:id 1716798889093634 + :params + {:id 1716798889093634 + :pairingTopic "9b18e1348817a548bbc97f9b4a09278f4fdf7c984e4a61ddf461bd1f57710d33" + :expiryTimestamp 1716799189 + :requiredNamespaces {} + :optionalNamespaces {:eip155 + {:chains ["eip155:1" "eip155:42161" "eip155:137" "eip155:43114" "eip155:56" + "eip155:10" "eip155:100" + "eip155:324" "eip155:7777777" "eip155:8453" "eip155:42220" + "eip155:1313161554" "eip155:11155111" "eip155:11155420"] + :methods ["personal_sign" "eth_accounts" "eth_requestAccounts" + "eth_sendRawTransaction" "eth_sendTransaction" + "eth_sign" "eth_signTransaction" "eth_signTypedData" + "eth_signTypedData_v3" "eth_signTypedData_v4" + "wallet_addEthereumChain" "wallet_getCallsStatus" + "wallet_getCapabilities" "wallet_getPermissions" + "wallet_registerOnboarding" "wallet_requestPermissions" + "wallet_scanQRCode" "wallet_sendCalls" + "wallet_showCallsStatus" "wallet_switchEthereumChain" + "wallet_watchAsset"] + :events ["chainChanged" "accountsChanged"]}} + :relays [{:protocol "irn"}] + :proposer {:publicKey "cddea055b8974d93380e6c7e72110145506c06524047866f8034f3db0990137a" + :metadata {:name "Web3Modal" + :description "Web3Modal Laboratory" + :url "https://lab.web3modal.com" + :icons ["https://avatars.githubusercontent.com/u/37784886"]}}} + :verifyContext {:verified {:verifyUrl "https://verify.walletconnect.com" + :validation "VALID" + :origin "https://lab.web3modal.com" + :isScam false}}}}) + +(h/deftest-sub :wallet-connect/session-proposer + [sub-name] + (testing "Return the session proposer public key and metadata" + (swap! rf-db/app-db + assoc-in + [:wallet-connect/current-proposal :request] + sample-session) + + (let [proposer (rf/sub [sub-name])] + (is (= (-> proposer :publicKey) + (-> sample-session :params :proposer :publicKey))) + + (is (= (-> proposer :metadata :url) + + (-> sample-session :params :proposer :metadata :url)))))) + +(h/deftest-sub :wallet-connect/session-proposer-name + [sub-name] + (testing "Return only the name of the session proposer" + (swap! rf-db/app-db + assoc-in + [:wallet-connect/current-proposal :request] + sample-session) + + (is (= (-> sample-session :params :proposer :metadata :name) + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet-connect/current-proposal-address + [sub-name] + (testing "Return the current proposal account address" + (let [address "0x1234567890abcdef"] + + (swap! rf-db/app-db assoc-in [:wallet-connect/current-proposal :address] address) + + (is (= address (rf/sub [sub-name])))) + + (testing "Return nil when there is no current proposal account" + (swap! rf-db/app-db assoc-in [:wallet-connect/current-proposal :address] nil) + + (is (nil? (rf/sub [sub-name])))))) diff --git a/src/status_im/subs/wallet/dapps/requests.cljs b/src/status_im/subs/wallet/dapps/requests.cljs new file mode 100644 index 00000000000..5edb1d0316f --- /dev/null +++ b/src/status_im/subs/wallet/dapps/requests.cljs @@ -0,0 +1,81 @@ +(ns status-im.subs.wallet.dapps.requests + (:require [re-frame.core :as rf] + [status-im.contexts.wallet.common.utils :as wallet-utils] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data] + [utils.string])) + +(rf/reg-sub + :wallet-connect/current-request-address + :<- [:wallet-connect/current-request] + :-> :address) + +(rf/reg-sub + :wallet-connect/prepared-hash + :<- [:wallet-connect/current-request] + (fn [request] + (:prepared-hash request))) + +(rf/reg-sub + :wallet-connect/current-request-display-data + :<- [:wallet-connect/current-request] + :-> :display-data) + +(rf/reg-sub + :wallet-connect/current-request-method + :<- [:wallet-connect/current-request] + (fn [request] + (-> request + :event + data-store/get-request-method))) + +(rf/reg-sub + :wallet-connect/current-request-account-details + :<- [:wallet-connect/current-request-address] + :<- [:wallet/accounts-without-watched-accounts] + (fn [[address accounts]] + (let [{:keys [customization-color name emoji]} (wallet-utils/get-account-by-address accounts address)] + {:customization-color customization-color + :name name + :emoji emoji}))) + +(rf/reg-sub + :wallet-connect/current-request-dapp + :<- [:wallet-connect/current-request] + :<- [:wallet-connect/sessions] + (fn [[request sessions]] + (data-store/get-current-request-dapp request sessions))) + +(rf/reg-sub + :wallet-connect/chain-id + :<- [:wallet-connect/current-request] + (fn [request] + (-> request + (get-in [:event :params :chainId]) + (networks/eip155->chain-id)))) + +(rf/reg-sub + :wallet-connect/current-request-network + :<- [:wallet/networks-by-id] + :<- [:wallet-connect/chain-id] + (fn [[networks chain-id]] + (get networks chain-id))) + +(rf/reg-sub + :wallet-connect/current-request-network-native-token-symbol + :<- [:wallet-connect/current-request-network] + :-> :native-currency-symbol) + +(rf/reg-sub + :wallet-connect/typed-data-request? + :<- [:wallet-connect/current-request-method] + typed-data/typed-data-request?) + + +(rf/reg-sub + :wallet-connect/dapp-icon + :<- [:wallet-connect/current-request-dapp] + (fn [{:keys [url iconUrl]}] + (data-store/compute-dapp-icon-path iconUrl url))) diff --git a/src/status_im/subs/wallet/dapps/sessions.cljs b/src/status_im/subs/wallet/dapps/sessions.cljs new file mode 100644 index 00000000000..c50ed995c82 --- /dev/null +++ b/src/status_im/subs/wallet/dapps/sessions.cljs @@ -0,0 +1,27 @@ +(ns status-im.subs.wallet.dapps.sessions + (:require [clojure.string :as string] + [re-frame.core :as rf] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [utils.string])) + +(rf/reg-sub + :wallet-connect/sessions-for-current-account + :<- [:wallet-connect/sessions] + :<- [:wallet/current-viewing-account-address] + (fn [[sessions address]] + (filter + (fn [{:keys [accounts]}] + (some #(string/includes? % address) accounts)) + sessions))) + +(rf/reg-sub + :wallet-connect/sessions-for-current-account-and-networks + ;; NOTE: using all chain-ids (as opposed to only the active ones) for showing sessions, + ;; so that we don't end up hiding sessions if the session network(s) are deactivated + :<- [:wallet/chain-ids] + :<- [:wallet-connect/sessions-for-current-account] + (fn [[chain-ids sessions]] + (filter + (partial networks/session-networks-allowed? chain-ids) + sessions))) + diff --git a/src/status_im/subs/wallet/dapps/transactions.cljs b/src/status_im/subs/wallet/dapps/transactions.cljs new file mode 100644 index 00000000000..861eb8b4688 --- /dev/null +++ b/src/status_im/subs/wallet/dapps/transactions.cljs @@ -0,0 +1,84 @@ +(ns status-im.subs.wallet.dapps.transactions + (:require [re-frame.core :as rf] + [status-im.contexts.wallet.common.utils :as wallet-utils] + [status-im.contexts.wallet.wallet-connect.utils.transactions :as transactions] + [utils.money :as money] + [utils.string])) + +(rf/reg-sub + :wallet-connect/transaction-args + :<- [:wallet-connect/current-request] + (fn [{:keys [event transaction]}] + (when (transactions/transaction-request? event) + transaction))) + +(rf/reg-sub + :wallet-connect/transaction-suggested-fees + :<- [:wallet-connect/current-request] + (fn [{:keys [event raw-data]}] + (when (transactions/transaction-request? event) + (:suggested-fees raw-data)))) + +(rf/reg-sub + :wallet-connect/transaction-max-fees-wei + :<- [:wallet-connect/transaction-args] + :<- [:wallet-connect/transaction-suggested-fees] + (fn [[transaction suggested-fees]] + (when transaction + (let [{:keys [gasPrice gas gasLimit maxFeePerGas]} transaction + eip-1559-chain? (:eip1559Enabled suggested-fees) + gas-limit (or gasLimit gas) + max-gas-fee (if eip-1559-chain? maxFeePerGas gasPrice)] + (money/bignumber (* max-gas-fee gas-limit)))))) + +(rf/reg-sub + :wallet-connect/account-native-token + :<- [:wallet-connect/current-request-address] + :<- [:wallet/accounts] + :<- [:wallet-connect/current-request-network-native-token-symbol] + (fn [[address accounts native-token-symbol]] + (let [find-account #(when (= (:address %) address) %) + find-token #(when (= (:symbol %) native-token-symbol) %)] + (->> accounts + (some find-account) + :tokens + (some find-token))))) + +(rf/reg-sub + :wallet-connect/current-request-transaction-information + :<- [:wallet-connect/chain-id] + :<- [:wallet-connect/transaction-max-fees-wei] + :<- [:wallet-connect/transaction-args] + :<- [:wallet-connect/account-native-token] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + :<- [:wallet/prices-per-token] + (fn [[chain-id max-fees-wei transaction native-token currency currency-symbol prices-per-token]] + (when transaction + (let [max-fees-ether (money/wei->ether max-fees-wei) + max-fees-fiat (wallet-utils/calculate-token-fiat-value {:currency currency + :balance max-fees-ether + :token native-token + :prices-per-token + prices-per-token}) + max-fees-fiat-formatted (wallet-utils/fiat-formatted-for-ui currency-symbol + max-fees-fiat) + balance (-> native-token + (get-in [:balances-per-chain chain-id :raw-balance]) + money/bignumber) + tx-value (money/bignumber (:value transaction)) + total-transaction-value (money/add max-fees-wei tx-value) + estimated-time-formatted (wallet-utils/estimated-time-format (:estimated-time transaction))] + {:total-transaction-value total-transaction-value + :balance balance + :estimated-time estimated-time-formatted + :max-fees max-fees-wei + :max-fees-fiat-value max-fees-fiat + :max-fees-fiat-formatted max-fees-fiat-formatted + :error-state (cond + (not (money/sufficient-funds? tx-value balance)) + :not-enough-assets + + (not (money/sufficient-funds? total-transaction-value + balance)) + :not-enough-assets-to-pay-gas-fees)})))) diff --git a/src/status_im/subs/wallet/networks.cljs b/src/status_im/subs/wallet/networks.cljs new file mode 100644 index 00000000000..0089eb01dff --- /dev/null +++ b/src/status_im/subs/wallet/networks.cljs @@ -0,0 +1,195 @@ +(ns status-im.subs.wallet.networks + (:require [re-frame.core :as re-frame] + [status-im.contexts.wallet.networks.core :as networks] + [status-im.contexts.wallet.networks.filter :as networks.filter])) + +#_" + Since our wallet can handle a limited number of networks simultaneously, + we can only have N number of networks `active` at a time, so we should be + careful about which networks subscriptions we use and where. + + - The following subscriptions will return only *active* networks, which should + be used in most cases within the wallet. + + `:wallet/active-chain-ids` (returns a set of active chain ids) + `:wallet/active-networks` (returns a vector of active networks) + + - The following subscriptions will return filtered active networks, based + on the filter state, and should be used in the wallet when showing + balances and collectibles. + + `:wallet/filtered-chain-ids` (returns a set of chain ids) + `:wallet/filtered-networks` (returns a vector of networks) + + - The following subscriptions will return *all* the networks we support, + regardless if active or not. If unsure which to use, it's likely you + need the active networks instead. + + `:wallet/chain-ids` (returns a set of chain ids) + `:wallet/networks` (returns a vector of networks) + + - We can also get the details of *any* network by its `chain-id` with: + + `:wallet/networks-by-id` (returns a map of networks indexed by chain-id) + `:wallet/network-by-id` (returns the network for the passed chain-id) +" + +(re-frame/reg-sub + :wallet/chain-ids-by-mode + :<- [:wallet] + :-> :networks/chain-ids-by-mode) + +(re-frame/reg-sub + :wallet/networks-by-id + :<- [:wallet] + :-> :networks/by-id) + +(re-frame/reg-sub + :wallet/network-filter + :<- [:wallet/ui] + :-> :networks/network-filter) + +(re-frame/reg-sub + :wallet/network-filter? + :<- [:wallet/network-filter] + (fn [network-filter] + (networks.filter/has-filters? network-filter))) + +(re-frame/reg-sub + :wallet/chain-ids + :<- [:wallet/chain-ids-by-mode] + :<- [:profile/test-networks-enabled?] + (fn [[chain-ids-by-mode testnet?]] + (set (get chain-ids-by-mode (if testnet? :test :prod))))) + +(re-frame/reg-sub + :wallet/networks + :<- [:wallet/chain-ids] + :<- [:wallet/networks-by-id] + (fn [[chain-ids networks-by-id]] + (->> chain-ids + (map #(get networks-by-id %)) + vec))) + +(re-frame/reg-sub + :wallet/network-by-id + :<- [:wallet/networks-by-id] + (fn [networks [_ chain-id]] + (get networks chain-id))) + +(re-frame/reg-sub + :wallet/network-name-from-chain-id + :<- [:wallet/networks-by-id] + (fn [networks [_ chain-id]] + (-> networks (get chain-id) :network-name))) + +(re-frame/reg-sub + :wallet/active-chain-ids + :<- [:wallet/networks] + (fn [networks] + (networks/get-active-chain-ids networks))) + +(re-frame/reg-sub + :wallet/active-networks + :<- [:wallet/networks] + (fn [networks] + (networks/get-active-networks networks))) + +(re-frame/reg-sub + :wallet/max-available-active-networks + :<- [:wallet/chain-ids] + (fn [chain-ids] + (min (count chain-ids) + (networks/get-max-active-networks)))) + +(re-frame/reg-sub + :wallet/active-networks-count + :<- [:wallet/active-chain-ids] + (fn [chain-ids] + (count chain-ids))) + +(re-frame/reg-sub + :wallet/filtered-networks + :<- [:wallet/active-networks] + :<- [:wallet/network-filter] + (fn [[active-networks network-filter]] + (networks/get-filtered-networks active-networks network-filter))) + +(re-frame/reg-sub + :wallet/filtered-chain-ids + :<- [:wallet/active-networks] + :<- [:wallet/network-filter] + (fn [[active-networks network-filter]] + (networks/get-filtered-chain-ids active-networks network-filter))) + +(re-frame/reg-sub + :wallet/network-filter-toggled? + :<- [:wallet/filtered-chain-ids] + (fn [filtered-chain-ids [_ chain-id]] + (contains? filtered-chain-ids chain-id))) + +(re-frame/reg-sub + :wallet/disable-network-filter? + :<- [:wallet/filtered-chain-ids] + (fn [filtered-chain-ids [_ chain-id]] + (and (= 1 (count filtered-chain-ids)) + (contains? filtered-chain-ids chain-id)))) + +(re-frame/reg-sub + :wallet/layer-1-networks + :<- [:wallet/active-networks] + (fn [networks] + (networks/get-networks-for-layer networks 1))) + +(re-frame/reg-sub + :wallet/eth-mainnet-network + :<- [:wallet/layer-1-networks] + (fn [networks] + (first networks))) + +(re-frame/reg-sub + :wallet/layer-2-networks + :<- [:wallet/active-networks] + (fn [networks] + (networks/get-networks-for-layer networks 2))) + +(re-frame/reg-sub + :wallet/balance-for-network-filter + :<- [:wallet/current-viewing-account-address] + :<- [:wallet/current-account-balances-by-network] + :<- [:wallet/balances-by-network] + (fn [[current-address current-account-balances balances] [_ chain-id]] + (-> (if (seq current-address) current-account-balances balances) + (get chain-id 0)))) + +(re-frame/reg-sub + :wallet/collectibles-count-for-network-filter + :<- [:wallet/current-viewing-account-address] + :<- [:wallet/current-account-collectibles-by-network] + :<- [:wallet/collectibles-by-network] + (fn [[current-address current-account-collectibles collectibles] [_ chain-id]] + (-> (if (seq current-address) current-account-collectibles collectibles) + (get chain-id) + count))) + +(re-frame/reg-sub + :wallet/show-new-chain-indicator? + :<- [:wallet/ui] + (fn [ui] + (-> ui + (get :networks/new-marked-as-seen? true) + not))) + +(re-frame/reg-sub + :wallet/max-active-networks-reached? + :<- [:wallet/active-chain-ids] + (fn [active-chain-ids] + (-> active-chain-ids + count + (>= (networks/get-max-active-networks))))) + +(re-frame/reg-sub + :wallet/deactivatable-networks + :<- [:wallet/active-networks] + (fn [active-networks] + (filter :deactivatable? active-networks))) diff --git a/src/status_im/subs/wallet/networks_test.cljs b/src/status_im/subs/wallet/networks_test.cljs new file mode 100644 index 00000000000..69ca8a35e0e --- /dev/null +++ b/src/status_im/subs/wallet/networks_test.cljs @@ -0,0 +1,54 @@ +(ns status-im.subs.wallet.networks-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + status-im.subs.root + status-im.subs.wallet.networks + [test-helpers.unit :as h] + [tests.wallet-test-data :as test-data] + [utils.re-frame :as rf])) + +(h/deftest-sub :wallet/chain-ids + [sub-name] + (testing "returns chain-ids for prod" + (swap! rf-db/app-db #(test-data/add-networks-to-db %)) + (is + (match? #{test-data/mainnet-chain-id test-data/arbitrum-chain-id test-data/optimism-chain-id} + (rf/sub [sub-name])))) + + (testing "returns chain-ids for test" + (swap! rf-db/app-db #(-> % + (test-data/add-testnet-enabled-to-db) + (test-data/add-networks-to-db))) + (is + (match? #{test-data/sepolia-chain-id test-data/arbitrum-sepolia-chain-id + test-data/optimism-sepolia-chain-id} + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/networks + [sub-name] + (testing "returns networks collection correctly" + (swap! rf-db/app-db #(test-data/add-networks-to-db %)) + (is + (match? [test-data/mainnet test-data/arbitrum test-data/optimism] + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/active-chain-ids + [sub-name] + (testing "returns active chain-ids only" + (swap! rf-db/app-db #(test-data/add-active-networks-to-db + % + [test-data/mainnet-chain-id])) + (is + (match? #{test-data/mainnet-chain-id} + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/active-networks + [sub-name] + (testing "returns active networks only" + (swap! rf-db/app-db #(test-data/add-active-networks-to-db + % + [test-data/optimism-chain-id])) + (is + (match? [test-data/optimism] + (rf/sub [sub-name]))))) diff --git a/src/status_im/subs/wallet/saved_addresses.cljs b/src/status_im/subs/wallet/saved_addresses.cljs new file mode 100644 index 00000000000..7591205d433 --- /dev/null +++ b/src/status_im/subs/wallet/saved_addresses.cljs @@ -0,0 +1,68 @@ +(ns status-im.subs.wallet.saved-addresses + (:require + [clojure.string :as string] + [re-frame.core :as rf])) + +(rf/reg-sub + :wallet/saved-addresses + :<- [:wallet] + :-> :saved-addresses) + +(rf/reg-sub + :wallet/saved-addresses-by-network-mode + :<- [:wallet/saved-addresses] + :<- [:profile/test-networks-enabled?] + (fn [[saved-addresses test-networks-enabled?]] + (get saved-addresses (if test-networks-enabled? :test :prod)))) + +(rf/reg-sub + :wallet/address-saved? + :<- [:wallet/saved-addresses-by-network-mode] + (fn [saved-addresses [_ address]] + (contains? saved-addresses address))) + +(rf/reg-sub + :wallet/has-saved-addresses? + :<- [:wallet/saved-addresses-by-network-mode] + (fn [saved-addresses] + (-> saved-addresses vals boolean))) + +(rf/reg-sub + :wallet/grouped-saved-addresses + :<- [:wallet/saved-addresses-by-network-mode] + (fn [saved-addresses] + (->> saved-addresses + vals + (group-by (comp string/upper-case first :name)) + (map (fn [[k v]] + {:title k + :data (sort-by (comp string/lower-case :name) v)})) + (sort-by :title)))) + +(rf/reg-sub + :wallet/saved-addresses-addresses + :<- [:wallet/saved-addresses-by-network-mode] + (fn [saved-addresses] + (-> saved-addresses keys set))) + +(rf/reg-sub + :wallet/saved-address-by-address + :<- [:wallet/saved-addresses-by-network-mode] + (fn [saved-addresses [_ address]] + (get saved-addresses (string/lower-case address)))) + +(rf/reg-sub + :wallet/filtered-saved-addresses + :<- [:wallet/saved-addresses-by-network-mode] + (fn [saved-addresses [_ query]] + (->> saved-addresses + vals + (sort-by (comp string/lower-case :name)) + (filter + (fn [{:keys [name address ens chain-short-names]}] + (let [lowercase-query (string/lower-case (string/trim query))] + (or + (string/includes? (string/lower-case name) lowercase-query) + (string/includes? address lowercase-query) + (string/includes? ens lowercase-query) + (string/includes? chain-short-names lowercase-query)))))))) diff --git a/src/status_im/subs/wallet/saved_addresses_test.cljs b/src/status_im/subs/wallet/saved_addresses_test.cljs new file mode 100644 index 00000000000..fa95c31e174 --- /dev/null +++ b/src/status_im/subs/wallet/saved_addresses_test.cljs @@ -0,0 +1,132 @@ +(ns status-im.subs.wallet.saved-addresses-test + (:require [clojure.test :refer [is testing]] + [re-frame.db :as rf-db] + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def saved-addresses-db + {:test {"0x1" {:test? true + :address "0x1" + :mixedcase-address "0x1" + :chain-short-names "eth:arb1:oeth:" + :ens? false + :network-preferences-names #{:arbitrum + :optimism + :mainnet} + :name "Alice" + :created-at 1716826714 + :ens "" + :customization-color :blue + :removed? false} + "0x2" {:test? true + :address "0x2" + :mixedcase-address "0x2" + :chain-short-names "eth:" + :ens? false + :network-preferences-names #{:mainnet} + :name "Bob" + :created-at 1716828825 + :ens "" + :customization-color :purple + :removed? false}} + :prod {"0x1" {:test? false + :address "0x1" + :mixedcase-address "0x1" + :chain-short-names "eth:" + :ens? false + :network-preferences-names #{:mainnet} + :name "Alice" + :created-at 1716826745 + :ens "" + :customization-color :blue + :removed? false} + "0x2" {:test? false + :address "0x2" + :mixedcase-address "0x2" + :chain-short-names "eth:arb1:oeth:" + :ens? false + :network-preferences-names #{:arbitrum + :optimism + :mainnet} + :name "Bob" + :created-at 1716828561 + :ens "" + :customization-color :purple + :removed? false}}}) + +(def grouped-saved-addresses-data + [{:title "A" + :data + [{:test? false + :address "0x1" + :mixedcase-address "0x1" + :chain-short-names "eth:" + :ens? false + :network-preferences-names #{:mainnet} + :name "Alice" + :created-at 1716826745 + :ens "" + :customization-color :blue + :removed? false}]} + {:title "B" + :data + [{:test? false + :address "0x2" + :mixedcase-address "0x2" + :chain-short-names "eth:arb1:oeth:" + :ens? false + :network-preferences-names #{:arbitrum + :optimism + :mainnet} + :name "Bob" + :created-at 1716828561 + :ens "" + :customization-color :purple + :removed? false}]}]) + +(h/deftest-sub :wallet/saved-addresses-by-network-mode + [sub-name] + (testing "returns saved addresses by network mode" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :saved-addresses] saved-addresses-db) + (assoc-in [:profile/profile :test-networks-enabled?] true))) + (is (= (:test saved-addresses-db) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/address-saved? + [sub-name] + (testing "returns boolean if given address is saved" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :saved-addresses] saved-addresses-db) + (assoc-in [:profile/profile :test-networks-enabled?] false))) + (is (true? (rf/sub [sub-name "0x1"]))) + (is (false? (rf/sub [sub-name "0x3"]))))) + +(h/deftest-sub :wallet/grouped-saved-addresses + [sub-name] + (testing "returns saved addresses grouped by name" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :saved-addresses] saved-addresses-db) + (assoc-in [:profile/profile :test-networks-enabled?] false))) + (is (= grouped-saved-addresses-data (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/saved-addresses-addresses + [sub-name] + (testing "returns saved addresses addresses set" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :saved-addresses] saved-addresses-db) + (assoc-in [:profile/profile :test-networks-enabled?] true))) + (is (= (-> saved-addresses-db :test keys set) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/saved-address-by-address + [sub-name] + (testing "returns a saved address" + (let [address "0x1"] + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :saved-addresses] saved-addresses-db) + (assoc-in [:profile/profile :test-networks-enabled?] false))) + (is (= (get-in saved-addresses-db [:prod address]) (rf/sub [sub-name address])))))) diff --git a/src/status_im/subs/wallet/send.cljs b/src/status_im/subs/wallet/send.cljs new file mode 100644 index 00000000000..7102bdc5e10 --- /dev/null +++ b/src/status_im/subs/wallet/send.cljs @@ -0,0 +1,260 @@ +(ns status-im.subs.wallet.send + (:require + [clojure.string :as string] + [re-frame.core :as rf] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.activity-tab.constants :as activity-tab-constants] + [status-im.contexts.wallet.common.utils :as common-utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [status-im.contexts.wallet.networks.core :as networks] + [status-im.contexts.wallet.send.utils :as send-utils] + [utils.money :as money] + [utils.number :as number])) + +(rf/reg-sub + :wallet/send-tab + :<- [:wallet/ui] + (fn [ui] + (get-in ui [:send :select-address-tab]))) + +(rf/reg-sub + :wallet/wallet-send + :<- [:wallet/ui] + :-> :send) + +(rf/reg-sub + :wallet/send-recipient + :<- [:wallet/wallet-send] + :-> :recipient) + +(rf/reg-sub + :wallet/send-route + :<- [:wallet/wallet-send] + :-> :route) + +(rf/reg-sub + :wallet/send-token-symbol + :<- [:wallet/wallet-send] + :-> :token-symbol) + +(rf/reg-sub + :wallet/send-transaction-ids + :<- [:wallet/wallet-send] + :-> :transaction-ids) + +(rf/reg-sub + :wallet/send-amount + :<- [:wallet/wallet-send] + :-> :amount) + +(rf/reg-sub + :wallet/send-tx-type + :<- [:wallet/wallet-send] + :-> :tx-type) + +(rf/reg-sub + :wallet/send-network + :<- [:wallet/wallet-send] + :-> :network) + +(rf/reg-sub + :wallet/send-native-token + :<- [:wallet/current-viewing-account-or-default] + :<- [:wallet/active-networks] + :<- [:wallet/send-network] + (fn [[{:keys [tokens]} networks send-network]] + (->> (common-utils/tokens-with-balance tokens networks [(:chain-id send-network)]) + (filter #(= (string/lower-case (:symbol %)) + (string/lower-case (:native-currency-symbol send-network)))) + first))) + +(rf/reg-sub + :wallet/sending-collectible? + :<- [:wallet/send-tx-type] + #(send-utils/tx-type-collectible? %)) + +(rf/reg-sub + :wallet/send-transaction-progress + :<- [:wallet/send-transaction-ids] + :<- [:wallet/transactions] + (fn [[tx-ids transactions]] + (let [send-tx-ids (set (keys transactions))] + (select-keys transactions + (filter send-tx-ids tx-ids))))) + +(rf/reg-sub + :wallet/recent-recipients + :<- [:wallet/all-activities] + :<- [:wallet/current-viewing-account-address] + (fn [[all-activities current-viewing-account-address]] + (let [address-activity (vals (get all-activities current-viewing-account-address))] + (->> address-activity + (sort :timestamp) + (keep (fn [{:keys [activity-type recipient]}] + (when (= activity-tab-constants/wallet-activity-type-send activity-type) + recipient))) + (distinct))))) + +(rf/reg-sub + :wallet/bridge-from-networks + :<- [:wallet/wallet-send] + :<- [:wallet/active-networks] + (fn [[{:keys [bridge-to-chain-id]} networks]] + (set (filter (fn [network] + (not= (:chain-id network) bridge-to-chain-id)) + networks)))) + +(rf/reg-sub + :wallet/bridge-from-chain-ids + :<- [:wallet/wallet-send] + :<- [:wallet/active-networks] + (fn [[{:keys [bridge-to-chain-id]} networks]] + (keep (fn [network] + (when (not= (:chain-id network) bridge-to-chain-id) + (:chain-id network))) + networks))) + +(rf/reg-sub + :wallet/send-token-decimals + :<- [:wallet/wallet-send] + (fn [{:keys [token collectible]}] + (if collectible 0 (:decimals token)))) + +(rf/reg-sub + :wallet/send-display-token-decimals + :<- [:wallet/wallet-send] + :<- [:wallet/prices-per-token] + (fn [[{:keys [token collectible]} prices-per-token]] + (if collectible + 0 + (-> token + (common-utils/token-usd-price prices-per-token) + common-utils/one-cent-value + common-utils/calc-max-crypto-decimals + (min constants/min-token-decimals-to-display))))) + +(rf/reg-sub + :wallet/send-native-token? + :<- [:wallet/wallet-send] + (fn [{:keys [token token-symbol network]}] + (and token (= token-symbol (:native-currency-symbol network))))) + +(rf/reg-sub + :wallet/total-amount + :<- [:wallet/send-route] + :<- [:wallet/send-token-decimals] + :<- [:wallet/send-native-token?] + (fn [[route token-decimals native-token?]] + (let [default-amount (money/bignumber 0)] + (if route + (->> (send-utils/estimated-received-by-chain + route + token-decimals + native-token?) + vals + (reduce money/add default-amount)) + default-amount)))) + +(rf/reg-sub + :wallet/send-total-amount-formatted + :<- [:wallet/total-amount] + :<- [:wallet/send-display-token-decimals] + :<- [:wallet/wallet-send-token-symbol] + (fn [[amount token-decimals token-symbol]] + (-> amount + (number/to-fixed token-decimals) + (str " " token-symbol)))) + +(rf/reg-sub + :wallet/send-amount-fixed + :<- [:wallet/send-display-token-decimals] + (fn [token-decimals [_ amount]] + (number/to-fixed (money/->bignumber amount) token-decimals))) + +(rf/reg-sub + :wallet/bridge-to-network-details + :<- [:wallet/wallet-send] + :<- [:wallet/networks-by-id] + (fn [[{:keys [bridge-to-chain-id]} networks-by-id]] + (get networks-by-id bridge-to-chain-id))) + +(rf/reg-sub + :wallet/send-token-network-balance + :<- [:wallet/wallet-send-token] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + :<- [:wallet/prices-per-token] + (fn [[token currency currency-symbol prices-per-token] [_ chain-id]] + (let [{:keys [balances-per-chain + decimals]} token + balance-for-chain (get balances-per-chain chain-id) + total-balance (money/token->unit (:raw-balance balance-for-chain) decimals) + fiat-value (common-utils/calculate-token-fiat-value + {:currency currency + :balance total-balance + :token token + :prices-per-token prices-per-token}) + crypto-formatted (common-utils/get-standard-crypto-format token + total-balance + prices-per-token) + fiat-formatted (common-utils/fiat-formatted-for-ui currency-symbol + fiat-value)] + {:crypto (str crypto-formatted " " (:symbol token)) + :fiat fiat-formatted}))) + +(rf/reg-sub + :wallet/send-estimated-time + :<- [:wallet/send-best-route] + (fn [route] + (-> route + :estimated-time + common-utils/estimated-time-v2-format))) + +(rf/reg-sub + :wallet/send-enough-assets? + :<- [:wallet/wallet-send] + (fn [{:keys [enough-assets?]}] + (if (nil? enough-assets?) true enough-assets?))) + +(rf/reg-sub + :wallet/no-routes-found? + :<- [:wallet/wallet-send-loading-suggested-routes?] + :<- [:wallet/send-route] + (fn [[loading? route]] + (and (empty? route) (not loading?)))) + +(rf/reg-sub :wallet/send-selected-network + :<- [:wallet/networks-by-id] + :<- [:wallet/wallet-send] + (fn [[networks {:keys [to-values-by-chain]}]] + (->> to-values-by-chain + keys + first + (get networks)))) + +(rf/reg-sub + :wallet/send-network-values + :<- [:wallet/networks-by-id] + :<- [:wallet/wallet-send] + (fn [[networks {:keys [from-values-by-chain to-values-by-chain token-display-name token] :as send-data}] + [_ to-values?]] + (let [network-values (if to-values? to-values-by-chain from-values-by-chain) + token-symbol (or token-display-name + (-> send-data :token :symbol)) + token-decimals (:decimals token)] + (reduce-kv + (fn [acc chain-id amount] + (let [network-name (-> networks (get chain-id) :network-name) + amount-fixed (number/to-fixed (money/->bignumber amount) token-decimals)] + (merge acc (network-utils/network-summary network-name token-symbol amount-fixed)))) + {} + network-values)))) + +(rf/reg-sub + :wallet/bridge-to-networks + :<- [:wallet/active-networks] + :<- [:wallet/send-network] + (fn [[networks send-network]] + (filter #(and (networks/bridge-supported-network? %) + (not= (:chain-id %) (:chain-id send-network))) + networks))) diff --git a/src/status_im/subs/wallet/send_test.cljs b/src/status_im/subs/wallet/send_test.cljs new file mode 100644 index 00000000000..9db8b1ccb8c --- /dev/null +++ b/src/status_im/subs/wallet/send_test.cljs @@ -0,0 +1,254 @@ +(ns status-im.subs.wallet.send-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + [status-im.contexts.wallet.common.activity-tab.constants :as constants] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.subs.root] + [status-im.subs.wallet.send] + [test-helpers.unit :as h] + [tests.wallet-test-data :as test-data] + [utils.money :as money] + [utils.re-frame :as rf])) + +(def ^:private token-mock + {:decimals 8 + :symbol "ETH" + :balances-per-chain {1 {:raw-balance "100"}}}) + +(h/deftest-sub :wallet/send-tab + [sub-name] + (testing "returns active tab for selecting address" + (swap! rf-db/app-db update-in db-path/send assoc :select-address-tab :tabs/recent) + (is (= :tabs/recent (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-transaction-ids + [sub-name] + (testing "returns the transaction ids attached the last send flow" + (swap! rf-db/app-db update-in db-path/send assoc :transaction-ids ["0x123" "0x321"]) + (is (= ["0x123" "0x321"] (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-transaction-progress + [sub-name] + (testing "returns transaction data for a transaction with multiple transactions" + (swap! rf-db/app-db assoc-in + [:wallet :transactions] + {"0x123" {:status :pending + :id 240 + :chain-id 5} + "0x321" {:status :pending + :id 240 + :chain-id 1}}) + (swap! rf-db/app-db update-in db-path/send assoc :transaction-ids ["0x123" "0x321"]) + (is (= {"0x123" {:status :pending + :id 240 + :chain-id 5} + "0x321" {:status :pending + :id 240 + :chain-id 1}} + (rf/sub [sub-name])))) + + (testing "returns transaction data for a transaction with a single transaction" + (swap! rf-db/app-db assoc-in + [:wallet :transactions] + {"0x123" {:status :pending + :id 100 + :chain-id 5} + "0x321" {:status :pending + :id 240 + :chain-id 1}}) + (swap! rf-db/app-db update-in db-path/send assoc :transaction-ids ["0x123"]) + (is (= {"0x123" {:status :pending + :id 100 + :chain-id 5}} + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/recent-recipients + [sub-name] + (testing "returns recent tab for selecting address" + (swap! rf-db/app-db + (fn [db] + (-> db + (assoc-in [:wallet :activities] + {"acc1" {1 {:activity-type constants/wallet-activity-type-send + :amount-out "0x1" + :sender "acc1" + :recipient "acc2" + :timestamp 1588291200} + 2 {:activity-type constants/wallet-activity-type-receive + :amount-in "0x1" + :sender "acc2" + :recipient "acc1" + :timestamp 1588377600} + 3 {:activity-type constants/wallet-activity-type-send + :amount-out "0x1" + :sender "acc1" + :recipient "acc4" + :timestamp 1588464000}} + "acc3" {4 {:activity-type constants/wallet-activity-type-receive + :amount-in "0x1" + :sender "acc4" + :recipient "acc3" + :timestamp 1588464000}}}) + (assoc-in [:wallet :current-viewing-account-address] "acc1")))) + (is (match? ["acc2" "acc4"] (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-token-decimals + [sub-name] + (testing "returns the decimals for the chosen token" + (swap! rf-db/app-db update-in db-path/send assoc :token token-mock) + (is (= 8 (rf/sub [sub-name])))) + + (testing "returns 0 if sending collectibles" + (swap! rf-db/app-db update-in db-path/send assoc :collectible {}) + (is (match? 0 (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-display-token-decimals + [sub-name] + (testing "returns the decimals based on the token price for the chosen token" + (swap! rf-db/app-db + (fn [db] + (-> db + (update-in db-path/send + assoc + :token + {:symbol "ETH" + :balances-per-chain {1 {:raw-balance "100"}}}) + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 10000}})))) + (is (match? 6 (rf/sub [sub-name])))) + + (testing "returns 0 if sending collectibles" + (swap! rf-db/app-db update-in db-path/send assoc :collectible {}) + (is (match? 0 (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-native-token? + [sub-name] + (testing "returns true if the chosen token is native (ETH)" + (swap! rf-db/app-db + (fn [db] + (update-in db db-path/send assoc :token {} :token-display-name "ETH"))) + (is (rf/sub [sub-name]))) + + (testing "returns false if the chosen token is not native" + (swap! rf-db/app-db + (swap! rf-db/app-db + (fn [db] + (update-in db db-path/send assoc :token {} :token-display-name "SNT")))) + (is (not (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/total-amount + [sub-name] + (testing "returns the total SEND amount when the route is present" + (swap! rf-db/app-db + (fn [db] + (update-in db + db-path/send + assoc + :token token-mock + :token-display-name "ETH" + :route + [{:amount-out "0x1bc16d674ec80000" + :to {:chain-id 1}}]))) + (is (match? (money/bignumber 2) (rf/sub [sub-name])))) + + (testing "returns the total BRIDGE amount when the route is present" + (swap! rf-db/app-db + (fn [db] + (update-in db + db-path/send + assoc + :token token-mock + :token-display-name "ETH" + :route + [{:bridge-name "Hop" + :amount-in "0xde0b6b3a7640000" + :token-fees (money/bignumber "230000000000000") + :to {:chain-id 1}}]))) + (is (match? (money/bignumber 0.99977) (rf/sub [sub-name])))) + + (testing "returns the default total amount (0) when the route is not present" + (swap! rf-db/app-db + (fn [db] + (update-in db db-path/send assoc :token token-mock :token-display-name "ETH"))) + (is (match? (money/bignumber 0) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-total-amount-formatted + [sub-name] + (testing "returns the formatted total amount" + (swap! rf-db/app-db + (fn [db] + (-> db + (update-in db-path/send + assoc + :token token-mock + :token-display-name "ETH" + :route + [{:amount-out "0x2b139f68a611c00" ;; 193999990000000000 + :to {:chain-id 1}}]) + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 10000}})))) + (is (match? "0.194 ETH" (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-amount-fixed + [sub-name] + (testing "returns the fixed value when the amount is a string" + (swap! rf-db/app-db + (fn [db] + (update-in db db-path/send assoc :token token-mock))) + (is (match? "2" (rf/sub [sub-name "1.9999999"])))) + + (testing "returns the fixed value when the amount is a number" + (swap! rf-db/app-db + (fn [db] + (update-in db db-path/send assoc :token token-mock))) + (is (match? "2" (rf/sub [sub-name 1.9999999])))) + + (testing "returns the fixed value when the amount is a bignumber" + (swap! rf-db/app-db + (fn [db] + (update-in db db-path/send assoc :token token-mock))) + (is (match? "2" (rf/sub [sub-name (money/bignumber "1.9999999")]))))) + +(h/deftest-sub :wallet/bridge-to-network-details + [sub-name] + (testing "returns the network details for bridge transactions" + (swap! rf-db/app-db + (fn [db] + (-> db + (test-data/add-networks-to-db) + (assoc-in [:profile/profile :test-networks-enabled?] false) + (assoc-in db-path/send {:bridge-to-chain-id test-data/mainnet-chain-id})))) + (is (match? test-data/mainnet (rf/sub [sub-name])))) + + (testing "returns nil if not on the bridge flow" + (swap! rf-db/app-db + (fn [db] + (-> db + (test-data/add-networks-to-db) + (assoc-in [:profile/profile :test-networks-enabled?] false)))) + (is (match? nil (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/send-network-values + [sub-name] + (testing "network values for the from account are returned correctly" + (swap! rf-db/app-db #(-> % + (test-data/add-networks-to-db) + (assoc-in + [:wallet :ui :send] + {:from-values-by-chain {test-data/mainnet-chain-id 100} + :to-values-by-chain {test-data/arbitrum-chain-id 100} + :token-display-name "ETH"}))) + (is + (match? {:mainnet {:amount "100" :token-symbol "ETH"}} (rf/sub [sub-name false])))) + + (testing "network values for the to account are returned correctly" + (swap! rf-db/app-db #(-> % + (test-data/add-networks-to-db) + (assoc-in + [:wallet :ui :send] + {:from-values-by-chain {test-data/mainnet-chain-id 100} + :to-values-by-chain {test-data/arbitrum-chain-id 100} + :token-display-name "ARB1"}))) + (is + (match? {:arbitrum {:amount "100" :token-symbol "ARB1"}} (rf/sub [sub-name true]))))) diff --git a/src/status_im/subs/wallet/swap.cljs b/src/status_im/subs/wallet/swap.cljs new file mode 100644 index 00000000000..5863afbfad0 --- /dev/null +++ b/src/status_im/subs/wallet/swap.cljs @@ -0,0 +1,411 @@ +(ns status-im.subs.wallet.swap + (:require [clojure.string :as string] + [re-frame.core :as rf] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.send.utils :as send-utils] + [status-im.contexts.wallet.swap.utils :as swap-utils] + [utils.money :as money] + [utils.number :as number])) + +(rf/reg-sub + :wallet/swap + :<- [:wallet/ui] + :-> :swap) + +(rf/reg-sub + :wallet/swap-asset-to-pay + :<- [:wallet/swap] + :-> :asset-to-pay) + +(rf/reg-sub + :wallet/swap-asset-to-pay-decimals + :<- [:wallet/swap-asset-to-pay] + :-> :decimals) + +(rf/reg-sub + :wallet/swap-asset-to-pay-symbol + :<- [:wallet/swap-asset-to-pay] + :-> :symbol) + +(rf/reg-sub + :wallet/swap-asset-to-receive + :<- [:wallet/swap] + :-> :asset-to-receive) + +(rf/reg-sub + :wallet/swap-network + :<- [:wallet/swap] + :-> :network) + +(rf/reg-sub + :wallet/swap-network-native-token-symbol + :<- [:wallet/swap-network] + :-> :native-currency-symbol) + +(rf/reg-sub + :wallet/swap-start-point + :<- [:wallet/swap] + :-> :start-point) + +(rf/reg-sub + :wallet/swap-error-response + :<- [:wallet/swap] + :-> :error-response) + +(rf/reg-sub + :wallet/swap-error-response-code + :<- [:wallet/swap-error-response] + :-> :code) + +(rf/reg-sub + :wallet/swap-error-response-details + :<- [:wallet/swap-error-response] + :-> :details) + +(rf/reg-sub + :wallet/swap-asset-to-pay-token-symbol + :<- [:wallet/swap-asset-to-pay] + :-> :symbol) + +(rf/reg-sub + :wallet/swap-updated-token-prices-usd + :<- [:wallet/swap] + :-> :updated-token-prices) + +(rf/reg-sub + :wallet/swap-asset-to-pay-network-balance + :<- [:wallet/swap-asset-to-pay] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + :<- [:wallet/swap-asset-to-pay-token-symbol] + :<- [:wallet/prices-per-token] + (fn [[token currency currency-symbol token-symbol prices-per-token] [_ chain-id]] + (let [{:keys [balances-per-chain + decimals]} token + balance-for-chain (get balances-per-chain chain-id) + total-balance (money/token->unit (:raw-balance balance-for-chain) decimals) + fiat-value (utils/calculate-token-fiat-value + {:currency currency + :balance total-balance + :token token + :prices-per-token prices-per-token}) + crypto-formatted (utils/get-standard-crypto-format token total-balance prices-per-token) + fiat-formatted (utils/fiat-formatted-for-ui currency-symbol fiat-value)] + {:crypto (str crypto-formatted " " token-symbol) + :fiat fiat-formatted}))) + +(rf/reg-sub + :wallet/swap-max-slippage + :<- [:wallet/swap] + :-> :max-slippage) + +(rf/reg-sub + :wallet/swap-approval-transaction-id + :<- [:wallet/swap] + :-> :approval-transaction-id) + +(rf/reg-sub + :wallet/swap-approval-transaction-status + :<- [:wallet/transactions] + :<- [:wallet/swap-approval-transaction-id] + (fn [[transactions approval-transaction-id]] + (get-in transactions [approval-transaction-id :status]))) + +(rf/reg-sub + :wallet/swap-proposal + :<- [:wallet/swap] + :-> :swap-proposal) + +(rf/reg-sub + :wallet/swap-proposal-without-fees + :<- [:wallet/swap] + (fn [swap] + (let [swap-proposal (:swap-proposal swap)] + (reduce dissoc + swap-proposal + [:gas-fees :gas-amount :token-fees :bonder-fees :approval-fee :approval-l-1-fee])))) + +(rf/reg-sub + :wallet/swap-amount + :<- [:wallet/swap] + :-> :amount) + +(rf/reg-sub + :wallet/swap-approved-amount + :<- [:wallet/swap] + :-> :approved-amount) + +(rf/reg-sub + :wallet/swap-loading-swap-proposal? + :<- [:wallet/swap] + :-> :loading-swap-proposal?) + +(rf/reg-sub + :wallet/swap-loading-swap-proposal-fee? + :<- [:wallet/swap] + :-> :loading-swap-proposal-fee?) + +(rf/reg-sub + :wallet/swap-proposal-amount-out + :<- [:wallet/swap-proposal] + :-> :amount-out) + +(rf/reg-sub + :wallet/swap-proposal-amount-in + :<- [:wallet/swap-proposal] + :-> :amount-in) + +(rf/reg-sub + :wallet/swap-receive-amount + :<- [:wallet/swap-proposal-amount-out] + :<- [:wallet/swap-asset-to-receive] + (fn [[amount-out asset-to-receive]] + (let [receive-token-decimals (:decimals asset-to-receive) + amount-out-whole (when amount-out + (number/hex->whole amount-out receive-token-decimals)) + amount-out-num (when amount-out-whole + (number/to-fixed amount-out-whole + receive-token-decimals)) + display-decimals (min receive-token-decimals + constants/min-token-decimals-to-display) + receive-amount (when amount-out-num + (utils/sanitized-token-amount-to-display amount-out-num + display-decimals))] + (or receive-amount 0)))) + +(rf/reg-sub + :wallet/swap-receive-amount-raw + :<- [:wallet/swap-proposal-amount-out] + :<- [:wallet/swap-asset-to-receive] + (fn [[amount-out asset-to-receive]] + (let [receive-token-decimals (:decimals asset-to-receive) + amount-out-whole (when amount-out + (number/hex->whole amount-out receive-token-decimals)) + amount-out-num (when amount-out-whole + (number/to-fixed amount-out-whole + receive-token-decimals))] + (or amount-out-num 0)))) + +(rf/reg-sub + :wallet/swap-pay-amount + :<- [:wallet/swap-proposal-amount-in] + :<- [:wallet/swap-asset-to-pay] + (fn [[amount-in asset-to-pay]] + (let [pay-token-decimals (:decimals asset-to-pay) + amount-in-whole (when amount-in + (number/hex->whole amount-in pay-token-decimals)) + amount-in-num (when amount-in-whole + (number/to-fixed amount-in-whole + pay-token-decimals)) + display-decimals (min pay-token-decimals + constants/min-token-decimals-to-display) + pay-amount (when amount-in-num + (utils/sanitized-token-amount-to-display amount-in-num + display-decimals))] + (or pay-amount 0)))) + +(rf/reg-sub + :wallet/swap-pay-amount-raw + :<- [:wallet/swap-proposal-amount-in] + :<- [:wallet/swap-asset-to-pay] + (fn [[amount-in asset-to-pay]] + (let [pay-token-decimals (:decimals asset-to-pay) + amount-in-whole (when amount-in + (number/hex->whole amount-in pay-token-decimals)) + amount-in-num (when amount-in-whole + (number/to-fixed amount-in-whole + pay-token-decimals))] + (or amount-in-num 0)))) + +(rf/reg-sub + :wallet/swap-proposal-provider + :<- [:wallet/swap-proposal] + (fn [swap-proposal] + (when swap-proposal + (let [bridge-name (:bridge-name swap-proposal) + provider-key (keyword (string/lower-case bridge-name))] + (get constants/swap-providers provider-key))))) + +(rf/reg-sub + :wallet/swap-proposal-approval-required + :<- [:wallet/swap-proposal] + :-> :approval-required) + +(rf/reg-sub + :wallet/swap-proposal-approval-contract-address + :<- [:wallet/swap-proposal] + :-> :approval-contract-address) + +(rf/reg-sub + :wallet/swap-proposal-approval-amount-required + :<- [:wallet/swap-proposal] + :-> :approval-amount-required) + +(rf/reg-sub + :wallet/wallet-swap-proposal-fee-fiat + :<- [:wallet/current-viewing-account] + :<- [:wallet/swap-proposal] + :<- [:profile/currency] + :<- [:wallet/prices-per-token] + :<- [:wallet/swap-network-native-token-symbol] + (fn [[account swap-proposal currency prices-per-token native-token-symbol]] + (when native-token-symbol + (let [tokens (:tokens account) + token-for-fees (first (filter #(= (string/lower-case (:symbol %)) + (string/lower-case native-token-symbol)) + tokens)) + fee-in-native-token (send-utils/full-route-gas-fee [swap-proposal]) + fee-in-fiat (utils/calculate-token-fiat-value + {:currency currency + :balance fee-in-native-token + :token token-for-fees + :prices-per-token prices-per-token})] + fee-in-fiat)))) + +(rf/reg-sub + :wallet/swap-asset-to-pay-balance-for-chain-data + :<- [:wallet/swap-asset-to-pay] + :<- [:wallet/swap-network-native-token-symbol] + (fn [[asset-to-pay native-token-symbol]] + (when-let [token-symbol (or (:symbol asset-to-pay) native-token-symbol)] + @(rf/subscribe [:wallet/token-by-symbol token-symbol])))) + +(rf/reg-sub + :wallet/swap-asset-to-pay-balance-for-chain + :<- [:wallet/swap-asset-to-pay-balance-for-chain-data] + (fn [asset-to-pay-with-current-account-balance [_ chain-id]] + (let [pay-token-decimals (:decimals asset-to-pay-with-current-account-balance) + pay-token-balance-selected-chain (-> (get-in asset-to-pay-with-current-account-balance + [:balances-per-chain chain-id :raw-balance] + 0) + (number/convert-to-whole-number pay-token-decimals))] + pay-token-balance-selected-chain))) + +(rf/reg-sub + :wallet/swap-asset-to-pay-amount-in-fiat + :<- [:wallet/swap-asset-to-pay-balance-for-chain-data] + :<- [:wallet/prices-per-token] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + (fn [[asset-to-pay-with-current-account-balance prices-per-token currency currency-symbol] [_ amount]] + (utils/formatted-token-fiat-value + {:currency currency + :currency-symbol currency-symbol + :balance (or amount 0) + :token asset-to-pay-with-current-account-balance + :prices-per-token prices-per-token}))) + +(rf/reg-sub + :wallet/swap-approval-fee + :<- [:wallet/swap-proposal] + (fn [{:keys [approval-fee approval-l-1-fee]}] + (money/add (money/wei->ether approval-fee) + (money/wei->ether approval-l-1-fee)))) + +(rf/reg-sub + :wallet/approval-gas-fees + :<- [:wallet/current-viewing-account] + :<- [:wallet/swap-approval-fee] + :<- [:profile/currency] + :<- [:wallet/prices-per-token] + :<- [:wallet/swap-network-native-token-symbol] + (fn [[account approval-fee currency prices-per-token native-token-symbol]] + (when native-token-symbol + (let [tokens (:tokens account) + token-for-fees (first (filter #(= (string/lower-case (:symbol %)) + (string/lower-case native-token-symbol)) + tokens)) + fee-in-fiat (utils/calculate-token-fiat-value + {:currency currency + :balance approval-fee + :token token-for-fees + :prices-per-token prices-per-token})] + fee-in-fiat)))) + +;; NOTE: updated route prices come only in USD (for now). When the router will +;; allow to define the currency, we should use only the updated prices, but till +;; then we fallback to `prices-per-token` +(rf/reg-sub :wallet/swap-pay-token-price + :<- [:wallet/swap-asset-to-pay] + :<- [:profile/currency] + :<- [:wallet/swap-updated-token-prices-usd] + :<- [:wallet/prices-per-token] + (fn [[asset-to-pay currency updated-token-prices prices-per-token]] + (if (= currency constants/profile-default-currency) + (swap-utils/updated-token-price asset-to-pay updated-token-prices) + (utils/token-price-by-symbol prices-per-token (:symbol asset-to-pay) currency)))) + +(rf/reg-sub :wallet/swap-receive-token-price + :<- [:wallet/swap-asset-to-receive] + :<- [:profile/currency] + :<- [:wallet/swap-updated-token-prices-usd] + :<- [:wallet/prices-per-token] + (fn [[asset-to-receive currency updated-token-prices prices-per-token]] + (if (= currency constants/profile-default-currency) + (swap-utils/updated-token-price asset-to-receive updated-token-prices) + (utils/token-price-by-symbol prices-per-token (:symbol asset-to-receive) currency)))) + +(rf/reg-sub + :wallet/swap-exchange-rate-crypto + :<- [:wallet/swap-pay-token-price] + :<- [:wallet/swap-receive-token-price] + :<- [:wallet/swap-asset-to-pay] + :<- [:wallet/swap-updated-token-prices-usd] + (fn [[pay-token-price receive-token-price asset-to-pay token-prices]] + (when (and token-prices asset-to-pay) + (let [exchange-rate (some->> asset-to-pay + :decimals + (utils/token-exchange-rate pay-token-price receive-token-price))] + (or exchange-rate 0))))) + +(rf/reg-sub + :wallet/swap-exchange-rate-fiat + :<- [:wallet/swap-pay-token-price] + :<- [:profile/currency-symbol] + (fn [[pay-token-price currency-symbol]] + (when pay-token-price + (utils/fiat-formatted-for-ui currency-symbol pay-token-price)))) + +(rf/reg-sub :wallet/max-swap-fee + :<- [:wallet/swap-proposal-approval-required] + :<- [:wallet/swap-approval-fee] + :<- [:wallet/swap-proposal] + (fn [[approval-required swap-approval-fee swap-proposal]] + (let [wallet-swap-proposal-fee (send-utils/full-route-gas-fee [swap-proposal])] + (if approval-required + (money/add swap-approval-fee wallet-swap-proposal-fee) + wallet-swap-proposal-fee)))) + +(rf/reg-sub :wallet/swap-available-crypto-limit + :<- [:wallet/swap-network] + :<- [:wallet/swap-asset-to-pay-balance-for-chain-data] + :<- [:wallet/max-swap-fee] + :<- [:wallet/swap-asset-to-pay-symbol] + :<- [:wallet/swap-network-native-token-symbol] + (fn [[network asset-to-pay-with-current-account-balance max-swap-fee pay-token-symbol + native-token-symbol]] + (let [pay-token-balance (utils/token-balance-for-network + asset-to-pay-with-current-account-balance + (:chain-id network))] + (if (= pay-token-symbol native-token-symbol) + (let [buffered-fee (money/mul max-swap-fee + (inc (/ constants/eth-max-fee-buffer-percent 100)))] + (money/sub pay-token-balance buffered-fee)) + pay-token-balance)))) + +(rf/reg-sub + :wallet/swap-estimated-time + :<- [:wallet/swap-proposal] + (fn [route] + (-> route + :estimated-time + utils/estimated-time-v2-format))) + +(rf/reg-sub + :wallet/swap-approval-estimated-time + :<- [:wallet/swap-proposal] + (fn [route] + (some-> route + :approval-estimated-time + utils/estimated-time-v2-format))) diff --git a/src/status_im/subs/wallet/swap_test.cljs b/src/status_im/subs/wallet/swap_test.cljs new file mode 100644 index 00000000000..87592d96930 --- /dev/null +++ b/src/status_im/subs/wallet/swap_test.cljs @@ -0,0 +1,259 @@ +(ns status-im.subs.wallet.swap-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.subs.root] + [status-im.subs.wallet.collectibles] + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def ^:private currencies + {:usd {:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false}}) + +(def ^:private prices-per-token-high + {:ETH {:usd 10000} + :SNT {:usd 10000}}) + +(def ^:private prices-per-token-low + {:DAI {:usd 0.0251} + :SNT {:usd 0.0251}}) + +(def ^:private accounts-with-tokens + {:0x1 {:tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance "100"}}} + {:symbol "SNT" + :balances-per-chain {1 {:raw-balance "100"}}}] + :network-preferences-names #{} + :customization-color nil + :operable? true + :operable :fully + :address "0x1"} + :0x2 {:tokens [{:symbol "SNT" + :balances-per-chain {1 {:raw-balance "200"}}}] + :network-preferences-names #{} + :customization-color nil + :operable? true + :operable :partially + :address "0x2"}}) + +(def networks + {:mainnet-network + {:full-name "Ethereum" + :network-name :mainnet + :chain-id 1 + :related-chain-id 5 + :layer 1 + :native-currency-symbol "ETH" + :native-currency-name "Ether" + :native-currency-decimals 18} + :layer-2-networks + [{:full-name "Optimism" + :network-name :optimism + :chain-id 10 + :related-chain-id 420 + :layer 2 + :native-currency-symbol "ETH" + :native-currency-name "Ether" + :native-currency-decimals 18} + {:full-name "Arbitrum" + :network-name :arbitrum + :chain-id 42161 + :related-chain-id 421613 + :layer 2 + :native-currency-symbol "ETH" + :native-currency-name "Ether" + :native-currency-decimals 18}]}) + +(def swap-data + {:asset-to-pay + {:description "Status Network Token (SNT)" + :decimals 18 + :symbol "SNT" + :name "Status" + :total-balance 1 + :balances-per-chain + {1 + {:raw-balance 1000000000000000000 + :balance "1" + :chain-id 1} + 10 + {:raw-balance 0 + :balance "0" + :chain-id 10} + 42161 + {:raw-balance 0 + :balance "0" + :chain-id 42161}} + :networks (concat [(networks :mainnet-network)] (networks :layer-2-networks)) + :chain-id 0 + :asset-website-url "https://status.im/" + :available-balance 1 + :token-list-id "" + :built-on "ETH" + :verified true} + :asset-to-receive + {:description "Dai Stablecoin" + :decimals 18 + :symbol "DAI" + :name "Dai Stablecoin" + :total-balance 1 + :balances-per-chain + {1 + {:raw-balance 1000000000000000000 + :balance "1" + :chain-id 1} + 10 + {:raw-balance 0 + :balance "0" + :chain-id 10} + 42161 + {:raw-balance 0 + :balance "0" + :chain-id 42161}} + :networks (concat [(networks :mainnet-network)] (networks :layer-2-networks)) + :chain-id 0 + :asset-website-url "https://status.im/" + :available-balance 1 + :token-list-id "" + :built-on "ETH" + :verified true} + :network (networks :mainnet-network) + :swap-proposal {:amount-out "0x10000" + :amount-in "0x10000" + :approval-required true + :approval-amount-required "0x10000" + :gas-amount "25000" + :gas-fees {:tx-max-fees-per-gas "4" + :eip-1559-enabled true + :l-1-gas-fee "0"}} + :error-response "Error" + :loading-swap-proposal? false + :max-slippage 0.5}) + +(h/deftest-sub :wallet/swap + [sub-name] + (testing "Return the wallet/ui/swap node" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? swap-data (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-asset-to-pay + [sub-name] + (testing "Return swap asset-to-pay" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? (swap-data :asset-to-pay) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-asset-to-receive + [sub-name] + (testing "Return swap asset-to-receive" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? (swap-data :asset-to-receive) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-asset-to-pay-token-symbol + [sub-name] + (testing "Return asset-to-pay token symbol" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? "SNT" (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-asset-to-pay-network-balance + [sub-name] + (testing "Return swap asset-to-pay" + (swap! rf-db/app-db + #(-> % + (assoc :currencies currencies) + (assoc-in db-path/swap swap-data) + (assoc-in [:wallet :tokens :prices-per-token] prices-per-token-low))) + (is (match? {:crypto "1 SNT" :fiat "$0.03"} (rf/sub [sub-name 1]))))) + +(h/deftest-sub :wallet/swap-network + [sub-name] + (testing "Return the current swap network" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? (swap-data :network) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-error-response + [sub-name] + (testing "Return the swap error response" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? (swap-data :error-response) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-max-slippage + [sub-name] + (testing "Return the max slippage for the swap" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? 0.5 (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-loading-swap-proposal? + [sub-name] + (testing "Return if swap is loading the swap proposal" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (false? (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-proposal + [sub-name] + (testing "Return the swap proposal" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? (swap-data :swap-proposal) (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-proposal-amount-out + [sub-name] + (testing "Return the amount out in the swap proposal" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? "0x10000" (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-proposal-approval-required + [sub-name] + (testing "Return if approval is required in the swap proposal" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (true? (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/swap-proposal-approval-amount-required + [sub-name] + (testing "Return the approval amount required in the swap proposal" + (swap! rf-db/app-db assoc-in + db-path/swap + swap-data) + (is (match? "0x10000" (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/wallet-swap-proposal-fee-fiat + [sub-name] + (testing "wallet send fee calculated and formatted in fiat" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts-with-tokens) + (assoc-in [:wallet :current-viewing-account-address] "0x1") + (assoc-in db-path/swap swap-data) + (assoc-in [:currencies] currencies) + (assoc-in [:profile/profile :currency] :usd) + (assoc-in [:profile/profile :currency-symbol] "$") + (assoc-in [:wallet :tokens :prices-per-token] prices-per-token-high))) + (let [result (rf/sub [sub-name])] + (is (match? result 1))))) diff --git a/src/status_im/subs/wallet/wallet.cljs b/src/status_im/subs/wallet/wallet.cljs new file mode 100644 index 00000000000..9f9d635ff9b --- /dev/null +++ b/src/status_im/subs/wallet/wallet.cljs @@ -0,0 +1,1061 @@ +(ns status-im.subs.wallet.wallet + (:require [cljs-time.core :as t] + [clojure.string :as string] + [re-frame.core :as rf] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] + [status-im.contexts.wallet.networks.core :as networks] + [status-im.contexts.wallet.send.utils :as send-utils] + [status-im.contexts.wallet.sheets.missing-keypair.view :as missing-keypair] + [status-im.subs.wallet.add-account.address-to-watch] + [utils.collection] + [utils.money :as money] + [utils.number] + [utils.security.core :as security])) + +(rf/reg-sub + :wallet/ui + :<- [:wallet] + :-> :ui) + +(rf/reg-sub + :wallet/scanned-address + :<- [:wallet/ui] + :-> :scanned-address) + +(rf/reg-sub + :wallet/last-updates-per-address + :<- [:wallet/ui] + :-> :last-updates-per-address) + +(rf/reg-sub + :wallet/latest-update + :<- [:wallet/last-updates-per-address] + (fn [last-updates-per-address] + (->> last-updates-per-address + vals + (reduce (fn [earliest-time last-update-time] + (if (or (nil? earliest-time) + (t/before? last-update-time earliest-time)) + last-update-time + earliest-time)))))) + +(rf/reg-sub + :wallet/blockchain + :<- [:wallet] + :-> :blockchain) + +(rf/reg-sub + :wallet/blockchain-status + :<- [:wallet/blockchain] + :-> :status) + +(rf/reg-sub + :wallet/tokens + :<- [:wallet] + :-> :tokens) + +(rf/reg-sub + :wallet/tokens-by-symbol + :<- [:wallet/tokens] + (fn [{:keys [by-symbol]}] + (utils.collection/index-by :symbol by-symbol))) + +(rf/reg-sub + :wallet/prices-per-token + :<- [:wallet/tokens] + :-> :prices-per-token) + +(rf/reg-sub + :wallet/market-values-per-token + :<- [:wallet/tokens] + :-> :market-values-per-token) + +(rf/reg-sub + :wallet/tokens-loading + :<- [:wallet/ui] + :-> :tokens-loading) + +(rf/reg-sub + :wallet/home-tokens-loading? + :<- [:wallet/tokens-loading] + (fn [tokens-loading] + (->> tokens-loading + vals + (some true?) + boolean))) + +(rf/reg-sub + :wallet/current-viewing-account-tokens-loading? + :<- [:wallet/tokens-loading] + :<- [:wallet/current-viewing-account-address] + (fn [[tokens-loading current-viewing-account-address]] + (get tokens-loading current-viewing-account-address))) + +(rf/reg-sub + :wallet/create-account + :<- [:wallet/ui] + :-> :create-account) + +(rf/reg-sub + :wallet/create-account-new-keypair + :<- [:wallet/create-account] + :-> :new-keypair) + +(rf/reg-sub + :wallet/current-viewing-account-address + :<- [:wallet] + :-> :current-viewing-account-address) + +(rf/reg-sub + :wallet/wallet-send-to-address + :<- [:wallet/wallet-send] + :-> :to-address) + +(rf/reg-sub + :wallet/wallet-send-receiver-networks + :<- [:wallet/wallet-send] + :-> :receiver-networks) + +(rf/reg-sub + :wallet/wallet-send-route + :<- [:wallet/wallet-send] + :-> :route) + +(rf/reg-sub + :wallet/send-best-route + :<- [:wallet/wallet-send-route] + (fn [routes] + (first routes))) + +(rf/reg-sub + :wallet/suggested-gas-fees-for-setting + :<- [:wallet/route] + (fn [route] + (:fees-by-mode route))) + +(rf/reg-sub + :wallet/wallet-send-enough-assets? + :<- [:wallet/wallet-send] + :-> :enough-assets?) + +(rf/reg-sub + :wallet/wallet-send-collectible + :<- [:wallet/wallet-send] + :-> :collectible) + +(rf/reg-sub + :wallet/wallet-send-token + :<- [:wallet/wallet-send] + :<- [:wallet/active-networks] + (fn [[wallet-send networks]] + (let [token (:token wallet-send) + enabled-from-chain-ids (->> networks + (map :chain-id) + set)] + (some-> token + (assoc :networks (network-utils/network-list-with-positive-balance token networks) + :supported-networks (network-utils/network-list token networks) + :available-balance (utils/calculate-total-token-balance token) + :total-balance (utils/calculate-total-token-balance + token + enabled-from-chain-ids)))))) + +(rf/reg-sub + :wallet/bridge-token + :<- [:wallet/wallet-send-token] + (fn [token] + (update token :networks #(filter networks/bridge-supported-network? %)))) + +(rf/reg-sub + :wallet/wallet-send-token-symbol + :<- [:wallet/wallet-send] + (fn [{:keys [token-symbol token]}] + (or token-symbol (:symbol token)))) + +(rf/reg-sub + :wallet/wallet-send-from-values-by-chain + :<- [:wallet/wallet-send] + :-> :from-values-by-chain) + +(rf/reg-sub + :wallet/wallet-send-to-values-by-chain + :<- [:wallet/wallet-send] + :-> :to-values-by-chain) + +(rf/reg-sub + :wallet/wallet-send-loading-suggested-routes? + :<- [:wallet/wallet-send] + :-> :loading-suggested-routes?) + +(rf/reg-sub + :wallet/wallet-send-suggested-routes + :<- [:wallet/wallet-send] + :-> :suggested-routes) + +(rf/reg-sub + :wallet/wallet-send-sender-network-values + :<- [:wallet/wallet-send] + :-> :sender-network-values) + +(rf/reg-sub + :wallet/wallet-send-receiver-network-values + :<- [:wallet/wallet-send] + :-> :receiver-network-values) + +(rf/reg-sub + :wallet/wallet-send-network-links + :<- [:wallet/wallet-send] + :-> :network-links) + +(rf/reg-sub + :wallet/wallet-send-tx-type + :<- [:wallet/wallet-send] + :-> :tx-type) + +(rf/reg-sub + :wallet/send-tx-type-collectible? + :<- [:wallet/wallet-send-tx-type] + (fn [tx-type] + (send-utils/tx-type-collectible? tx-type))) + +(rf/reg-sub + :wallet/send-general-flow? + :<- [:wallet/wallet-send] + :-> :general-flow?) + +(rf/reg-sub + :wallet/keypairs + :<- [:wallet] + :-> :keypairs) + +(rf/reg-sub + :wallet/keypairs-list + :<- [:wallet] + (fn [{:keys [keypairs]}] + (vals keypairs))) + +(rf/reg-sub + :wallet/fully-operable-keypairs-list + :<- [:wallet/keypairs-list] + (fn [keypairs] + (filter #(and (= :fully (:lowest-operability %)) (not-empty (:derived-from %))) keypairs))) + +(rf/reg-sub + :wallet/keypair-names + :<- [:wallet/keypairs-list] + (fn [keypairs] + (set (map :name keypairs)))) + +(rf/reg-sub + :wallet/selected-keypair-uid + :<- [:wallet/create-account] + :-> :selected-keypair-uid) + +(rf/reg-sub + :wallet/selected-keypair-keycard? + :<- [:wallet/selected-keypair] + (fn [{:keys [keycards]}] + (boolean (seq keycards)))) + +(rf/reg-sub + :wallet/selected-keypair + :<- [:wallet/keypairs] + :<- [:wallet/selected-keypair-uid] + (fn [[keypairs selected-keypair-uid]] + (get keypairs selected-keypair-uid))) + +(rf/reg-sub + :wallet/selected-primary-keypair? + :<- [:wallet/keypairs] + :<- [:wallet/selected-keypair-uid] + (fn [[keypairs selected-keypair-uid]] + (= (get-in keypairs [selected-keypair-uid :type]) + :profile))) + +(defn- format-settings-keypair-accounts + [accounts + {:keys [networks size] + :or {networks [] + size 32}}] + (->> accounts + (keep (fn [{:keys [path color emoji name address]}] + (when-not (string/starts-with? (str path) constants/path-eip1581) + {:account-props {:customization-color color + :size size + :emoji emoji + :type :default + :name name + :address address} + :networks networks + :state :default + :blur? true + :action :none}))))) + +(defn- format-settings-missing-keypair-accounts + [accounts] + (->> accounts + (map (fn [{:keys [color emoji]}] + {:customization-color color + :emoji emoji + :type :default})))) + +(rf/reg-sub + :wallet/settings-keypairs-accounts + :<- [:wallet/keypairs-list] + (fn [keypairs [_ format-options]] + (reduce + (fn [acc {:keys [accounts name type key-uid lowest-operability keycards]}] + (if (= lowest-operability :no) + (update acc + :missing + conj + {:type type + :keycard? (boolean (seq keycards)) + :name name + :key-uid key-uid + :accounts (format-settings-missing-keypair-accounts accounts)}) + (update acc + :operable + conj + {:type type + :keycard? (boolean (seq keycards)) + :name name + :key-uid key-uid + :accounts (format-settings-keypair-accounts accounts format-options)}))) + {:missing [] + :operable []} + keypairs))) + +(rf/reg-sub + :wallet/derivation-path-state + :<- [:wallet/create-account] + :-> :derivation-path-state) + +(rf/reg-sub + :wallet/accounts-by-address + :<- [:wallet] + :-> :accounts) + +(rf/reg-sub + :wallet/accounts + :<- [:wallet/accounts-by-address] + (fn [accounts-by-address] + (->> accounts-by-address + vals + (sort-by :position)))) + +(rf/reg-sub + :wallet/accounts-without-assets + :<- [:wallet/accounts] + (fn [accounts] + (map #(dissoc % :tokens :collectibles) accounts))) + +(rf/reg-sub + :wallet/watch-only-accounts + :<- [:wallet/accounts] + (fn [accounts] + (filter :watch-only? accounts))) + +(rf/reg-sub + :wallet/addresses + :<- [:wallet] + :-> #(->> % + :accounts + keys + set)) + +(rf/reg-sub + :wallet/balances-in-active-networks + :<- [:wallet/accounts] + :<- [:profile/currency] + :<- [:wallet/filtered-chain-ids] + :<- [:wallet/prices-per-token] + (fn [[accounts currency chain-ids prices-per-token]] + (zipmap (map :address accounts) + (map (fn [account] + (utils/calculate-balance-from-tokens {:currency currency + :tokens (:tokens account) + :chain-ids chain-ids + :prices-per-token prices-per-token})) + accounts)))) + +(rf/reg-sub + :wallet/balances-by-network + :<- [:wallet/operable-accounts] + :<- [:profile/currency] + :<- [:wallet/active-chain-ids] + :<- [:wallet/prices-per-token] + (fn [[accounts currency chain-ids prices-per-token]] + (zipmap chain-ids + (map (fn [chain-id] + (->> accounts + (reduce (fn [acc {:keys [tokens]}] + (-> (utils/calculate-balance-from-tokens {:currency currency + :tokens tokens + :chain-ids [chain-id] + :prices-per-token + prices-per-token}) + (money/add acc))) + (money/bignumber 0)))) + chain-ids)))) + +(rf/reg-sub + :wallet/current-account-balances-by-network + :<- [:wallet/current-viewing-account] + :<- [:profile/currency] + :<- [:wallet/active-chain-ids] + :<- [:wallet/prices-per-token] + (fn [[current-account currency chain-ids prices-per-token]] + (zipmap chain-ids + (map (fn [chain-id] + (utils/calculate-balance-from-tokens {:currency currency + :tokens (:tokens current-account) + :chain-ids [chain-id] + :prices-per-token prices-per-token})) + chain-ids)))) + +(rf/reg-sub + :wallet/account-cards-data + :<- [:wallet/accounts] + :<- [:wallet/balances-in-active-networks] + :<- [:wallet/tokens-loading] + :<- [:network/online?] + :<- [:profile/currency-symbol] + :<- [:wallet/keypairs] + (fn [[accounts balances tokens-loading online? currency-symbol keypairs]] + (mapv (fn [{:keys [color address watch-only? key-uid operable] :as account}] + (let [account-type (cond + (= operable :no) :missing-keypair + watch-only? :watch-only + :else :empty) + keypair (get keypairs key-uid)] + (assoc account + :customization-color color + :type (cond + (= operable :no) :missing-keypair + watch-only? :watch-only + :else :empty) + :on-press (if (= account-type :missing-keypair) + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content #(missing-keypair/view + account + keypair)}])) + #(rf/dispatch [:wallet/navigate-to-account address])) + :loading? (and online? + (or (get tokens-loading address) + (not (contains? tokens-loading address)))) + :balance (utils/prettify-balance currency-symbol + (get balances address))))) + accounts))) + +(rf/reg-sub + :wallet/current-viewing-account + :<- [:wallet/accounts] + :<- [:wallet/current-viewing-account-address] + :<- [:wallet/balances-in-active-networks] + :<- [:profile/currency-symbol] + (fn [[accounts current-viewing-account-address balances currency-symbol]] + (let [balance (get balances current-viewing-account-address) + formatted-balance (utils/prettify-balance currency-symbol balance)] + (-> accounts + (utils/get-account-by-address current-viewing-account-address) + (assoc :balance balance + :formatted-balance formatted-balance))))) + +(rf/reg-sub + :wallet/current-viewing-account-or-default + :<- [:wallet/accounts] + :<- [:wallet/current-viewing-account-address] + :<- [:wallet/balances-in-active-networks] + :<- [:profile/currency-symbol] + (fn [[accounts current-viewing-account-address balances currency-symbol]] + (let [account (or (utils/get-account-by-address accounts current-viewing-account-address) + (utils/get-default-account accounts)) + address (:address account) + balance (get balances address) + formatted-balance (utils/prettify-balance currency-symbol balance)] + (assoc account + :balance balance + :formatted-balance formatted-balance)))) + +(rf/reg-sub + :wallet/current-viewing-account-color + :<- [:wallet/current-viewing-account] + :-> :color) + +(rf/reg-sub + :wallet/current-viewing-account-keypair + :<- [:wallet/current-viewing-account] + :<- [:wallet/keypairs] + (fn [[{:keys [key-uid]} keypairs]] + (get keypairs key-uid))) + +(rf/reg-sub + :wallet/current-viewing-account-tokens-in-active-networks + :<- [:wallet/current-viewing-account] + :<- [:wallet/filtered-chain-ids] + (fn [[{:keys [tokens]} chain-ids]] + (utils/filter-tokens-in-chains tokens chain-ids))) + +(rf/reg-sub + :wallet/current-viewing-account-tokens-filtered + :<- [:wallet/current-viewing-account-or-default] + :<- [:wallet/active-networks] + :<- [:wallet/wallet-send] + :<- [:profile/currency] + :<- [:wallet/prices-per-token] + (fn [[account networks send-data currency price-per-token] [_ {:keys [query chain-ids hide-token-fn]}]] + (let [tx-type (:tx-type send-data) + tokens (->> (:tokens account) + (map + (fn [token] + (let [total-balance (utils/calculate-total-token-balance + token + chain-ids)] + (assoc token + :disabled? (and (= tx-type :tx/bridge) + (send-utils/bridge-disabled? (:symbol + token))) + :networks (cond->> + (network-utils/network-list-with-positive-balance + token + networks) + chain-ids + (filter #(some #{(:chain-id %)} chain-ids))) + :supported-networks (network-utils/network-list token networks) + :available-balance (utils/calculate-total-token-balance token) + :total-balance total-balance + :fiat-value (utils/calculate-token-fiat-value + {:currency currency + :balance total-balance + :token token + :prices-per-token price-per-token}))))) + (filter (fn [{:keys [networks]}] + (pos? (count networks)))) + (remove #(when hide-token-fn (hide-token-fn constants/swap-tokens-my %)))) + sorted-tokens (utils/sort-tokens-by-fiat-value tokens)] + (if query + (let [query-string (string/lower-case query)] + (filter #(or (string/starts-with? (string/lower-case (:name %)) query-string) + (string/starts-with? (string/lower-case (:symbol %)) query-string)) + sorted-tokens)) + sorted-tokens)))) + + +(rf/reg-sub + :wallet/tokens-filtered + :<- [:wallet/tokens] + (fn [{:keys [by-symbol market-values-per-token details-per-token]} + [_ {:keys [query chain-ids hide-token-fn]}]] + (let [tokens (->> by-symbol + (map (fn [token] + (let [token-symbol (keyword (:symbol token))] + (-> token + (assoc :market-values + (get market-values-per-token token-symbol)) + (assoc :details (get details-per-token token-symbol)))))) + (filter (fn [{:keys [chain-id]}] + (some #{chain-id} chain-ids))) + (remove #(when hide-token-fn + (hide-token-fn constants/swap-tokens-popular %)))) + sorted-tokens (utils/sort-tokens-by-name tokens)] + (if query + (let [query-string (string/lower-case query)] + (filter #(or (string/starts-with? (string/lower-case (:name %)) query-string) + (string/starts-with? (string/lower-case (:symbol %)) query-string)) + sorted-tokens)) + sorted-tokens)))) + +(rf/reg-sub + :wallet/token-by-symbol + :<- [:wallet/current-viewing-account-or-default] + :<- [:wallet/networks] + (fn [[{:keys [tokens]} networks] [_ token-symbol chain-ids]] + (->> (utils/tokens-with-balance tokens networks chain-ids) + (filter #(= (string/lower-case (:symbol %)) + (string/lower-case token-symbol))) + first))) + +(rf/reg-sub + :wallet/token-by-symbol-from-first-available-account-with-balance + :<- [:wallet/operable-accounts] + :<- [:wallet/current-viewing-account-or-default] + :<- [:wallet/networks] + (fn [[accounts {:keys [tokens]} networks] [_ token-symbol chain-ids]] + (when token-symbol + (or + (->> (utils/tokens-with-balance tokens networks chain-ids) + (filter #(and (= (string/lower-case (:symbol %)) + (string/lower-case token-symbol)) + (money/greater-than (:total-balance %) 0))) + first) + (some + (fn [{:keys [tokens]}] + (->> (utils/tokens-with-balance tokens networks chain-ids) + (filter #(and (= (string/lower-case (:symbol %)) + (string/lower-case token-symbol)) + (money/greater-than (:total-balance %) 0))) + first)) + accounts))))) + +(rf/reg-sub + :wallet/accounts-without-current-viewing-account + :<- [:wallet/accounts] + :<- [:wallet/current-viewing-account-address] + (fn [[accounts current-viewing-account-address]] + (remove #(= (:address %) current-viewing-account-address) accounts))) + +(rf/reg-sub + :wallet/accounts-without-watched-accounts + :<- [:wallet/accounts-with-customization-color] + (fn [accounts] + (remove :watch-only? accounts))) + +(defn- keep-operable-accounts + [accounts] + (filter :operable? accounts)) + +(rf/reg-sub + :wallet/operable-accounts-without-current-viewing-account + :<- [:wallet/accounts-without-current-viewing-account] + keep-operable-accounts) + +(rf/reg-sub + :wallet/operable-accounts + :<- [:wallet/accounts-without-watched-accounts] + keep-operable-accounts) + +(rf/reg-sub + :wallet/operable-addresses-tokens-with-positive-balance + :<- [:wallet/operable-accounts] + (fn [accounts] + (let [positive-balance-in-any-chain? (fn [{:keys [balances-per-chain]}] + (->> balances-per-chain + (map (comp :raw-balance val)) + (some pos?)))] + (as-> accounts $ + (group-by :address $) + (update-vals $ #(filter positive-balance-in-any-chain? (:tokens (first %)))))))) + +(rf/reg-sub + :wallet/operable-addresses-with-token-symbol + :<- [:wallet/operable-addresses-tokens-with-positive-balance] + (fn [addresses-tokens [_ token-symbol]] + (keep (fn [[address tokens]] + (some #(when (= (:symbol %) token-symbol) address) + tokens)) + addresses-tokens))) + +(rf/reg-sub + :wallet/current-account-owns-token + (fn [[_ token-symbol]] + [(rf/subscribe [:wallet/current-viewing-account-address]) + (rf/subscribe [:wallet/operable-addresses-with-token-symbol token-symbol])]) + (fn [[address addresses-with-token]] + (-> addresses-with-token set (contains? address)))) + +(rf/reg-sub + :wallet/account-tab + :<- [:wallet/ui] + (fn [ui] + (get-in ui [:account-page :active-tab]))) + +(rf/reg-sub + :wallet/home-tab + :<- [:wallet/ui] + (fn [ui] + (:active-tab ui))) + +(rf/reg-sub + :wallet/aggregated-tokens + :<- [:wallet/accounts-without-watched-accounts] + (fn [accounts] + (utils/aggregate-tokens-for-all-accounts accounts))) + +(rf/reg-sub + :wallet/aggregated-tokens-in-active-networks + :<- [:wallet/aggregated-tokens] + :<- [:wallet/filtered-chain-ids] + (fn [[aggregated-tokens chain-ids]] + (utils/filter-tokens-in-chains aggregated-tokens chain-ids))) + +(rf/reg-sub + :wallet/current-viewing-account-token-values + :<- [:wallet/current-viewing-account] + :<- [:wallet/current-viewing-account-tokens-in-active-networks] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + :<- [:wallet/prices-per-token] + :<- [:wallet/market-values-per-token] + (fn [[{:keys [color]} tokens currency currency-symbol prices-per-token market-values-per-token]] + (utils/calculate-and-sort-tokens {:tokens tokens + :color color + :currency currency + :currency-symbol currency-symbol + :prices-per-token prices-per-token + :market-values-per-token market-values-per-token}))) + +(rf/reg-sub + :wallet/aggregated-token-values-and-balance + :<- [:wallet/aggregated-tokens-in-active-networks] + :<- [:profile/customization-color] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + :<- [:wallet/prices-per-token] + :<- [:wallet/market-values-per-token] + (fn [[aggregated-tokens color currency currency-symbol prices-per-token market-values-per-token]] + (let [balance (utils/calculate-balance-from-tokens {:currency currency + :tokens aggregated-tokens + :prices-per-token prices-per-token}) + formatted-balance (utils/prettify-balance currency-symbol balance) + sorted-token-values (utils/calculate-and-sort-tokens {:tokens aggregated-tokens + :color color + :currency currency + :currency-symbol currency-symbol + :prices-per-token prices-per-token + :market-values-per-token + market-values-per-token})] + {:balance balance + :formatted-balance formatted-balance + :tokens sorted-token-values}))) + +(rf/reg-sub + :wallet/zero-balance-in-all-non-watched-accounts? + :<- [:wallet/aggregated-tokens] + (fn [aggregated-tokens] + (let [zero-balance? (->> aggregated-tokens + (mapcat (comp vals :balances-per-chain)) + (every? #(money/equal-to (:raw-balance %) 0)))] + (and (not-empty aggregated-tokens) zero-balance?)))) + +(rf/reg-sub + :wallet/accounts-with-customization-color + :<- [:wallet/accounts] + (fn [accounts] + (map (fn [{:keys [color] :as account}] + (assoc account :customization-color color)) + accounts))) + +(rf/reg-sub + :wallet/transactions + :<- [:wallet] + :-> :transactions) + +(rf/reg-sub + :wallet/search-address + :<- [:wallet/ui] + :-> :search-address) + +(rf/reg-sub + :wallet/local-suggestions + :<- [:wallet/search-address] + :-> :local-suggestions) + +(rf/reg-sub + :wallet/local-suggestions->full-address + :<- [:wallet/local-suggestions] + (fn [local-suggestions] + (:full-address (first local-suggestions)))) + +(rf/reg-sub + :wallet/valid-ens-or-address? + :<- [:wallet/search-address] + :-> :valid-ens-or-address?) + +(rf/reg-sub + :wallet/searching-address? + :<- [:wallet/search-address] + :-> :loading?) + +(rf/reg-sub + :wallet/import-private-key + :<- [:wallet/create-account] + (fn [create-account] + (some-> create-account + :private-key + security/unmask))) + +(rf/reg-sub + :wallet/public-address + :<- [:wallet/create-account] + :-> :public-address) + +(rf/reg-sub + :wallet/wallet-send-fee-fiat-formatted + :<- [:wallet/current-viewing-account] + :<- [:wallet/wallet-send-route] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + :<- [:wallet/prices-per-token] + (fn [[account route currency currency-symbol prices-per-token]] + (let [token-symbol-for-fees (get-in (first route) [:from :native-currency-symbol])] + (when token-symbol-for-fees + (let [tokens (:tokens account) + token-for-fees (first (filter #(= (string/lower-case (:symbol %)) + (string/lower-case token-symbol-for-fees)) + tokens)) + fee-in-native-token (send-utils/full-route-gas-fee route) + fee-in-fiat (utils/calculate-token-fiat-value + {:currency currency + :balance fee-in-native-token + :token token-for-fees + :prices-per-token prices-per-token}) + fee-formatted (utils/fiat-formatted-for-ui + currency-symbol + fee-in-fiat)] + fee-formatted))))) + +(rf/reg-sub + :wallet/wallet-transaction-setting-fiat-formatted + :<- [:wallet/current-viewing-account] + :<- [:wallet/route] + :<- [:profile/currency] + :<- [:profile/currency-symbol] + :<- [:wallet/prices-per-token] + :<- [:wallet/suggested-gas-fees-for-setting] + (fn [[account route currency currency-symbol prices-per-token fees-for-settings] + [_ transaction-setting]] + (let [token-symbol-for-fees (get-in route [:from :native-currency-symbol])] + (when token-symbol-for-fees + (let [tokens (:tokens account) + token-for-fees (first (filter #(= (string/lower-case (:symbol %)) + (string/lower-case token-symbol-for-fees)) + tokens)) + gas-price (-> fees-for-settings + (get transaction-setting) + :max-fees) + fee-in-native-token (send-utils/full-route-gas-fee-for-custom-gas-price route gas-price) + fee-in-fiat (utils/calculate-token-fiat-value + {:currency currency + :balance fee-in-native-token + :token token-for-fees + :prices-per-token prices-per-token}) + fee-formatted (utils/fiat-formatted-for-ui + currency-symbol + fee-in-fiat)] + fee-formatted))))) + +(rf/reg-sub + :wallet/accounts-names + :<- [:wallet/accounts] + (fn [accounts] + (set (map :name accounts)))) + +(rf/reg-sub + :wallet/accounts-names-without-current-account + :<- [:wallet/accounts-names] + :<- [:wallet/current-viewing-account] + (fn [[account-names current-viewing-account]] + (disj account-names (:name current-viewing-account)))) + +(defn- get-emoji-and-colors-from-accounts + [accounts] + (->> accounts + (map (fn [{:keys [emoji color]}] [emoji color])) + (set))) + +(rf/reg-sub + :wallet/accounts-emojis-and-colors + :<- [:wallet/accounts] + (fn [accounts] + (get-emoji-and-colors-from-accounts accounts))) + +(rf/reg-sub + :wallet/accounts-emojis-and-colors-without-current-account + :<- [:wallet/accounts-without-current-viewing-account] + (fn [accounts] + (get-emoji-and-colors-from-accounts accounts))) + +(rf/reg-sub + :wallet/accounts-with-balances + :<- [:wallet/operable-accounts] + (fn [accounts [_ token]] + (let [token-symbol (:symbol token)] + (map + (fn [account] + (let [tokens (:tokens account) + filtered-tokens (filter #(= (:symbol %) token-symbol) tokens) + asset-pay-balance (utils/calculate-total-token-balance filtered-tokens)] + (assoc account + :asset-pay-balance (utils/sanitized-token-amount-to-display + asset-pay-balance + constants/min-token-decimals-to-display) + :asset-pay-symbol token-symbol))) + accounts)))) + +(rf/reg-sub + :wallet/route + :<- [:wallet/send-tx-type] + :<- [:wallet/send-route] + :<- [:wallet/swap-proposal] + (fn [[tx-type send-route swap-proposal]] + (if tx-type + (first send-route) + swap-proposal))) + +(rf/reg-sub + :wallet/user-fee-mode-settings + :<- [:wallet/ui] + :-> :user-fee-mode) + +(rf/reg-sub + :wallet/user-tx-settings + :<- [:wallet/ui] + :-> :user-tx-settings) + +(rf/reg-sub + :wallet/tx-settings-max-base-fee-user + :<- [:wallet/user-tx-settings] + :-> :max-base-fee) + +(rf/reg-sub + :wallet/tx-settings-priority-fee-user + :<- [:wallet/user-tx-settings] + :-> :priority-fee) + +(rf/reg-sub + :wallet/tx-settings-nonce-user + :<- [:wallet/user-tx-settings] + :-> :nonce) + +(rf/reg-sub + :wallet/tx-settings-gas-amount-user + :<- [:wallet/user-tx-settings] + :-> :gas-amount) + +(rf/reg-sub + :wallet/tx-settings-gas-price-user + :<- [:wallet/user-tx-settings] + :-> :gas-price) + +(rf/reg-sub + :wallet/tx-settings-gas-fees + :<- [:wallet/route] + (fn [route] + (:gas-fees route))) + +(rf/reg-sub + :wallet/tx-eip-1559-enabled? + :<- [:wallet/tx-settings-gas-fees] + :-> :eip-1559-enabled) + +(rf/reg-sub + :wallet/tx-settings-max-base-fee-route + :<- [:wallet/tx-settings-gas-fees] + (fn [gas-fees] + (:base-fee gas-fees))) + +(rf/reg-sub + :wallet/tx-settings-network-base-fee-route + :<- [:wallet/tx-settings-gas-fees] + (fn [gas-fees] + (:network-base-fee gas-fees))) + +(rf/reg-sub + :wallet/tx-settings-gas-amount-route + :<- [:wallet/route] + (fn [route] + (:gas-amount route))) + +(rf/reg-sub + :wallet/tx-settings-suggested-tx-gas-amount + :<- [:wallet/route] + (fn [route] + (:suggested-tx-gas-amount route))) + +(rf/reg-sub + :wallet/tx-settings-gas-price-route + :<- [:wallet/tx-settings-gas-fees] + :-> :gas-price) + +(rf/reg-sub + :wallet/tx-settings-fee-mode + :<- [:wallet/route] + :<- [:wallet/user-fee-mode-settings] + (fn [[route value-set-by-user]] + (or (:tx-fee-mode route) value-set-by-user))) + +(rf/reg-sub + :wallet/tx-settings-no-base-fee? + :<- [:wallet/route] + (fn [route] + (-> route :from :no-base-fee))) + +(rf/reg-sub + :wallet/tx-settings-no-priority-fee? + :<- [:wallet/route] + (fn [route] + (-> route :from :no-priority-fee))) + +(rf/reg-sub + :wallet/tx-settings-max-base-fee + :<- [:wallet/tx-settings-max-base-fee-route] + :<- [:wallet/tx-settings-max-base-fee-user] + (fn [[value-from-routes value-set-by-user]] + (or value-set-by-user value-from-routes))) + +(rf/reg-sub + :wallet/tx-settings-priority-fee + :<- [:wallet/tx-settings-gas-fees] + :<- [:wallet/tx-settings-priority-fee-user] + (fn [[gas-fees value-set-by-user]] + (or value-set-by-user (:tx-priority-fee gas-fees)))) + +(rf/reg-sub + :wallet/tx-settings-custom-priority-fee + :<- [:wallet/tx-settings-fee-mode] + :<- [:wallet/tx-settings-gas-fees] + :<- [:wallet/tx-settings-priority-fee-user] + :<- [:wallet/tx-settings-suggested-min-priority-fee] + (fn [[fee-mode gas-fees value-set-by-user min-priority-fee]] + (cond + value-set-by-user value-set-by-user + (= :tx-fee-mode/custom fee-mode) (:tx-priority-fee gas-fees) + :else min-priority-fee))) + +(rf/reg-sub + :wallet/tx-settings-gas-amount + :<- [:wallet/tx-settings-gas-amount-route] + :<- [:wallet/tx-settings-gas-amount-user] + (fn [[value-from-routes value-set-by-user]] + (or value-set-by-user value-from-routes))) + +(rf/reg-sub + :wallet/tx-settings-gas-price + :<- [:wallet/tx-settings-gas-price-route] + :<- [:wallet/tx-settings-gas-price-user] + (fn [[value-from-routes value-set-by-user]] + (or value-set-by-user value-from-routes))) + +(rf/reg-sub + :wallet/tx-settings-nonce + :<- [:wallet/route] + :<- [:wallet/tx-settings-nonce-user] + (fn [[route value-set-by-user]] + (or value-set-by-user (:nonce route)))) + +(rf/reg-sub + :wallet/tx-settings-suggested-nonce + :<- [:wallet/route] + (fn [route] + (:suggested-tx-nonce route))) + +(rf/reg-sub + :wallet/tx-settings-suggested-max-priority-fee + :<- [:wallet/tx-settings-gas-fees] + (fn [gas-fees] + (:suggested-max-priority-fee gas-fees))) + +(rf/reg-sub + :wallet/tx-settings-suggested-min-priority-fee + :<- [:wallet/tx-settings-gas-fees] + (fn [gas-fees] + (:suggested-min-priority-fee gas-fees))) + +(rf/reg-sub + :wallet/tx-settings-suggested-gas-price + :<- [:wallet/route] + :-> :suggested-tx-gas-price) diff --git a/src/status_im/subs/wallet/wallet_test.cljs b/src/status_im/subs/wallet/wallet_test.cljs new file mode 100644 index 00000000000..3693ab06818 --- /dev/null +++ b/src/status_im/subs/wallet/wallet_test.cljs @@ -0,0 +1,960 @@ +(ns status-im.subs.wallet.wallet-test + (:require + [cljs.test :refer [is testing use-fixtures]] + [re-frame.db :as rf-db] + [status-im.contexts.wallet.db-path :as db-path] + [status-im.subs.root] + [test-helpers.unit :as h] + [tests.wallet-test-data :as test-data] + [utils.money :as money] + [utils.re-frame :as rf])) + +(use-fixtures :each + {:before #(reset! rf-db/app-db {})}) + +(def ^:private currencies + {:usd {:id :usd + :short-name "USD" + :symbol "$" + :emoji "🇺🇸" + :name "US Dollar" + :popular? true + :token? false}}) + +(def ^:private accounts-with-tokens + {:0x1 {:tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance "100"}}} + {:symbol "SNT" + :balances-per-chain {1 {:raw-balance "100"}}}] + :customization-color nil + :operable? true + :operable :fully + :address "0x1"} + :0x2 {:tokens [{:symbol "SNT" + :balances-per-chain {1 {:raw-balance "200"}}}] + :customization-color nil + :operable? true + :operable :partially + :address "0x2"}}) + +(def tokens-0x1 + [{:decimals 0 + :symbol "ETH" + :name "Ether" + :balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "2") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "1") + :has-error false}}} + {:decimals 0 + :symbol "DAI" + :name "Dai Stablecoin" + :balances-per-chain {test-data/mainnet-chain-id + {:raw-balance (money/bignumber + "1") + :has-error false} + test-data/optimism-chain-id + {:raw-balance (money/bignumber + "1.5") + :has-error false} + test-data/arbitrum-chain-id + {:raw-balance nil :has-error false}}}]) + +(def tokens-0x2 + [{:decimals 0 + :symbol "ETH" + :name "Ether" + :balances-per-chain {test-data/mainnet-chain-id + {:raw-balance (money/bignumber + "2.5") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "3") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "") + :has-error false}}} + {:decimals 0 + :symbol "DAI" + :name "Dai Stablecoin" + :balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "1") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber "0") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "") + :has-error false}}}]) + +(def tokens-0x3 + [{:decimals 0 + :symbol "ETH" + :name "Ether" + :balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "5") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "2") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "") + :has-error false}}} + {:decimals 0 + :symbol "DAI" + :name "Dai Stablecoin" + :balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "1") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber "0") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "") + :has-error false}}}]) + +(def accounts + {"0x1" {:path "m/44'/60'/0'/0/0" + :emoji "😃" + :key-uid "0x2f5ea39" + :address "0x1" + :wallet false + :name "Account One" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :blue + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 0 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x1} + "0x2" {:path "m/44'/60'/0'/0/1" + :emoji "💎" + :key-uid "0x2f5ea39" + :address "0x2" + :wallet false + :name "Account Two" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :purple + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 1 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x2} + "0x3" {:path "" + :emoji "🎉" + :key-uid "0x2f5ea39" + :address "0x3" + :wallet false + :name "Watched Account 1" + :type :watch + :watch-only? true + :operable? true + :chat false + :test-preferred-chain-ids #{0} + :color :magenta + :hidden false + :prod-preferred-chain-ids #{0} + :position 2 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x" + :removed false + :tokens tokens-0x3}}) + +(def route-data + [{:gas-amount "25000" + :gas-fees {:tx-max-fees-per-gas "4" + :eip-1559-enabled true + :l-1-gas-fee "0"} + :from {:native-currency-symbol "ETH"}}]) + +(h/deftest-sub :wallet/balances-in-active-networks + [sub-name] + (testing "a map: address->balance" + (swap! rf-db/app-db #(-> % + (test-data/add-networks-to-db) + (assoc-in [:wallet :accounts] accounts) + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 2000} :DAI {:usd 1}}))) + (let [result (rf/sub [sub-name]) + balance-0x1 (money/bignumber 6002.5) + balance-0x2 (money/bignumber 11001) + balance-0x3 (money/bignumber 14001)] + + (is (money/equal-to balance-0x1 (get result "0x1"))) + (is (money/equal-to balance-0x2 (get result "0x2"))) + (is (money/equal-to balance-0x3 (get result "0x3")))))) + +(h/deftest-sub :wallet/accounts + [sub-name] + (testing "returns all accounts without balance" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (test-data/add-networks-to-db))) + (is + (match? + (list {:path "m/44'/60'/0'/0/0" + :emoji "😃" + :key-uid "0x2f5ea39" + :address "0x1" + :wallet false + :name "Account One" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :blue + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 0 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x1} + {:path "m/44'/60'/0'/0/1" + :emoji "💎" + :key-uid "0x2f5ea39" + :address "0x2" + :wallet false + :name "Account Two" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :purple + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 1 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x2} + {:path "" + :emoji "🎉" + :key-uid "0x2f5ea39" + :address "0x3" + :wallet false + :name "Watched Account 1" + :type :watch + :watch-only? true + :operable? true + :chat false + :test-preferred-chain-ids #{0} + :color :magenta + :hidden false + :prod-preferred-chain-ids #{0} + :position 2 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x" + :removed false + :tokens tokens-0x3}) + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/current-viewing-account-address + [sub-name] + (testing "returns the address of the current viewing account" + (let [viewing-address "0x1"] + (swap! rf-db/app-db #(assoc-in % [:wallet :current-viewing-account-address] viewing-address)) + (is (match? viewing-address (rf/sub [sub-name])))))) + +(h/deftest-sub :wallet/current-viewing-account + [sub-name] + (testing "returns current account with balance base" + (swap! rf-db/app-db + #(-> % + (assoc :currencies currencies) + (assoc-in [:wallet :accounts] accounts) + (assoc-in [:wallet :current-viewing-account-address] "0x1") + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 2000} :DAI {:usd 1}}) + (test-data/add-networks-to-db))) + + (let [result (rf/sub [sub-name])] + (is + (match? {:path "m/44'/60'/0'/0/0" + :emoji "😃" + :key-uid "0x2f5ea39" + :address "0x1" + :wallet false + :name "Account One" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :blue + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 0 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x1} + (dissoc result :balance :formatted-balance))) + + (is (money/equal-to (:balance result) (money/bignumber 6002.5))) + (is (match? (:formatted-balance result) "$6002.50"))))) + +(h/deftest-sub :wallet/addresses + [sub-name] + (testing "returns all addresses" + (swap! rf-db/app-db #(assoc-in % [:wallet :accounts] accounts)) + (is (match? #{"0x1" "0x2" "0x3"} + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/watch-address-activity-state + [sub-name] + (testing "watch address activity state with nil value" + (is (nil? (rf/sub [sub-name])))) + + (testing "watch address activity state with no-activity value" + (swap! rf-db/app-db #(assoc-in % [:wallet :ui :add-address-to-watch :activity-state] :no-activity)) + (is (match? :no-activity (rf/sub [sub-name])))) + + (testing "watch address activity state with has-activity value" + (swap! rf-db/app-db #(assoc-in % [:wallet :ui :add-address-to-watch :activity-state] :has-activity)) + (is (match? :has-activity (rf/sub [sub-name])))) + + (testing "watch address activity state with invalid-ens value" + (swap! rf-db/app-db #(assoc-in % [:wallet :ui :add-address-to-watch :activity-state] :invalid-ens)) + (is (match? :invalid-ens (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/accounts-without-current-viewing-account + [sub-name] + (testing "returns the accounts list without the current viewing account in it" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (assoc-in [:wallet :current-viewing-account-address] "0x2") + (test-data/add-networks-to-db))) + (is + (match? + (list + {:path "m/44'/60'/0'/0/0" + :emoji "😃" + :key-uid "0x2f5ea39" + :address "0x1" + :wallet false + :name "Account One" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :blue + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 0 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x1} + {:path "" + :emoji "🎉" + :key-uid "0x2f5ea39" + :address "0x3" + :wallet false + :name "Watched Account 1" + :type :watch + :watch-only? true + :operable? true + :chat false + :test-preferred-chain-ids #{0} + :color :magenta + :hidden false + :prod-preferred-chain-ids #{0} + :position 2 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x" + :removed false + :tokens tokens-0x3}) + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/accounts-without-watched-accounts + [sub-name] + (testing "returns the accounts list without the watched accounts in it" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (test-data/add-networks-to-db))) + (is + (match? + (list + {:path "m/44'/60'/0'/0/0" + :emoji "😃" + :key-uid "0x2f5ea39" + :address "0x1" + :wallet false + :name "Account One" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :blue + :customization-color :blue + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 0 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x1} + {:path "m/44'/60'/0'/0/1" + :emoji "💎" + :key-uid "0x2f5ea39" + :address "0x2" + :wallet false + :name "Account Two" + :type :generated + :watch-only? false + :operable? true + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :purple + :customization-color :purple + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :position 1 + :clock 1698945829328 + :created-at 1698928839000 + :operable :fully + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false + :tokens tokens-0x2}) + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/aggregated-tokens + [sub-name] + (testing "returns aggregated tokens from all accounts" + (swap! rf-db/app-db #(assoc-in % [:wallet :accounts] accounts)) + (let [result (rf/sub [sub-name]) + eth-token (some #(when (= (:symbol %) "ETH") %) result) + eth-mainnet-raw-balance (get-in eth-token [:balances-per-chain 1 :raw-balance])] + (is (match? 2 (count result))) + (is (money/equal-to (money/bignumber 4.5) eth-mainnet-raw-balance))))) + +(h/deftest-sub :wallet/aggregated-token-values-and-balance + [sub-name] + (testing "returns aggregated tokens (in quo/token-value props) and balances from all accounts" + (swap! rf-db/app-db #(-> % + (assoc :currencies currencies) + (assoc-in [:wallet :accounts] accounts) + (test-data/add-networks-to-db) + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 2000} :DAI {:usd 1}}))) + (let [{:keys [formatted-balance tokens]} (rf/sub [sub-name])] + (is (match? 2 (count tokens))) + (is (match? "$17003.50" formatted-balance))))) + +(h/deftest-sub :wallet/accounts-with-customization-color + [sub-name] + (testing "returns all accounts with customization color" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (test-data/add-networks-to-db))) + (is + (match? [(-> accounts + (get "0x1") + (assoc :customization-color :blue)) + (-> accounts + (get "0x2") + (assoc :customization-color :purple)) + (-> accounts + (get "0x3") + (assoc :customization-color :magenta))] + (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/watch-only-accounts + [sub-name] + (testing "returns only active (not watch-only?) accounts" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (test-data/add-networks-to-db))) + (is + (match? [(get accounts "0x3")] + (rf/sub [sub-name]))))) + +(def chat-account + {:path "m/43'/60'/1581'/0'/0" + :emoji "" + :key-uid "abc" + :address "address-1" + :color-id "" + :wallet false + :name "My Profile" + :type "generated" + :chat true + :color :blue + :hidden false + :removed false}) + +(def operable-wallet-account + {:path "m/44'/60'/0'/0/0" + :emoji "🤡" + :key-uid "abc" + :address "address-2" + :wallet true + :name "My Account" + :type "generated" + :chat false + :color :primary + :hidden false + :operable :fully + :removed false}) + +(def inoperable-wallet-account + {:path "m/44'/60'/0'/0/0" + :emoji "🧠" + :key-uid "def" + :address "address-3" + :wallet true + :name "My Other Account" + :type "generated" + :chat false + :color :primary + :hidden false + :operable :no + :removed false}) + +(def profile-key-pair-key-uid "abc") +(def profile-key-pair-name "My Profile") +(def seed-phrase-key-pair-key-uid "def") +(def seed-phrase-key-pair-name "My Key Pair") + +(def profile-keypair + {:key-uid profile-key-pair-key-uid + :name profile-key-pair-name + :type :profile + :lowest-operability :fully + :accounts []}) + +(def seed-phrase-keypair + {:key-uid seed-phrase-key-pair-key-uid + :name seed-phrase-key-pair-name + :type :seed + :lowest-operability :no + :accounts []}) + +(h/deftest-sub :wallet/keypairs + [sub-name] + (testing "returns keypairs map" + (swap! rf-db/app-db assoc-in [:wallet :keypairs] {profile-key-pair-key-uid profile-keypair}) + (is (match? {profile-key-pair-key-uid profile-keypair} (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/keypairs-list + [sub-name] + (swap! rf-db/app-db assoc-in + [:wallet :keypairs] + {profile-key-pair-key-uid profile-keypair + seed-phrase-key-pair-key-uid seed-phrase-keypair}) + (let [result (rf/sub [sub-name]) + expected (list profile-keypair seed-phrase-keypair)] + (is (= 2 (count result))) + (is (match? expected result)))) + +(h/deftest-sub :wallet/keypair-names + [sub-name] + (swap! rf-db/app-db assoc-in + [:wallet :keypairs] + {profile-key-pair-key-uid profile-keypair + seed-phrase-key-pair-key-uid seed-phrase-keypair}) + (is (match? #{seed-phrase-key-pair-name profile-key-pair-name} (rf/sub [sub-name])))) + +(h/deftest-sub :wallet/settings-keypairs-accounts + [sub-name] + (testing "returns formatted key-pairs and accounts" + (swap! rf-db/app-db + (fn [db] + (-> db + (assoc-in + [:wallet :keypairs] + {profile-key-pair-key-uid (update profile-keypair + :accounts + conj + operable-wallet-account) + seed-phrase-key-pair-key-uid (update seed-phrase-keypair + :accounts + conj + inoperable-wallet-account)}) + (assoc-in + [:wallet :accounts] + {(:address operable-wallet-account) operable-wallet-account + (:address inoperable-wallet-account) inoperable-wallet-account})))) + + (is + (match? + {:missing [{:name (:name seed-phrase-keypair) + :key-uid (:key-uid seed-phrase-keypair) + :type (:type seed-phrase-keypair) + :accounts [{:customization-color (:color inoperable-wallet-account) + :emoji (:emoji inoperable-wallet-account) + :type :default}]}] + :operable [{:name (:name profile-keypair) + :key-uid (:key-uid profile-keypair) + :type (:type profile-keypair) + :accounts [{:account-props {:customization-color (:color operable-wallet-account) + :size 32 + :emoji (:emoji operable-wallet-account) + :type :default + :name (:name operable-wallet-account) + :address (:address operable-wallet-account)} + :networks [] + :state :default + :action :none}]}]} + (rf/sub [sub-name])))) + + (testing "allows for passing account format options" + (swap! rf-db/app-db + (fn [db] + (-> db + (assoc-in + [:wallet :keypairs] + {profile-key-pair-key-uid (update profile-keypair + :accounts + conj + operable-wallet-account)}) + (assoc-in + [:wallet :accounts] + {(:address operable-wallet-account) operable-wallet-account})))) + + (let [{:keys [color + name + address + emoji]} operable-wallet-account + network-options [{:network-name :mainnet :short-name "eth"} + {:network-name :optimism :short-name "oeth"} + {:network-name :arbitrum :short-name "arb1"}] + size-option 20] + (is + (match? {:missing [] + :operable [{:name (:name profile-keypair) + :key-uid (:key-uid profile-keypair) + :type (:type profile-keypair) + :accounts [{:account-props {:customization-color color + :size size-option + :emoji emoji + :type :default + :name name + :address address} + :networks network-options + :state :default + :action :none}]}]} + (rf/sub [sub-name + {:networks network-options + :size size-option}]))))) + + (testing "filters non-wallet accounts" + (swap! rf-db/app-db + (fn [db] + (-> db + (assoc-in + [:wallet :keypairs] + {profile-key-pair-key-uid (update profile-keypair + :accounts + conj + operable-wallet-account + chat-account)}) + (assoc-in + [:wallet :accounts] + {(:address operable-wallet-account) operable-wallet-account + (:address chat-account) chat-account})))) + (is + (match? + {:missing [] + :operable [{:name (:name profile-keypair) + :key-uid (:key-uid profile-keypair) + :type (:type profile-keypair) + :accounts [{:account-props {:customization-color (:color operable-wallet-account) + :size 32 + :emoji (:emoji operable-wallet-account) + :type :default + :name (:name operable-wallet-account) + :address (:address operable-wallet-account)} + :networks [] + :state :default + :action :none}]}]} + (rf/sub [sub-name]))))) + +(def local-suggestions ["a" "b"]) + +(h/deftest-sub :wallet/local-suggestions + [sub-name] + (testing "returns local suggestions:" + (swap! rf-db/app-db + #(assoc-in % [:wallet :ui :search-address :local-suggestions] local-suggestions)) + (is (match? local-suggestions (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/valid-ens-or-address? + [sub-name] + (testing "returns local suggestions:" + (swap! rf-db/app-db + #(assoc-in % [:wallet :ui :search-address :valid-ens-or-address?] true)) + (is + (rf/sub [sub-name])))) + +(h/deftest-sub :wallet/selected-keypair-uid + [sub-name] + (testing "returns selected keypair uid" + (swap! rf-db/app-db + #(assoc-in % [:wallet :ui :create-account :selected-keypair-uid] "key-uid")) + (is (= "key-uid" (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/current-viewing-account-tokens-filtered + [sub-name] + (testing "current viewing tokens filtered" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (test-data/add-networks-to-db) + (assoc-in [:wallet :current-viewing-account-address] "0x2") + (assoc :currencies currencies) + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 2000} :DAI {:usd 1}}) + (assoc-in [:profile/profile :currency] :usd))) + (is (match? (count (rf/sub [sub-name ""])) 2)) + (is (match? (count (rf/sub [sub-name "et"])) 2)))) + +(h/deftest-sub :wallet/current-viewing-account-tokens-in-active-networks + [sub-name] + (testing "current account tokens in active networks" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (assoc-in [:wallet :current-viewing-account-address] "0x1") + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 2000} :DAI {:usd 1}}) + (test-data/add-active-networks-to-db [test-data/arbitrum-chain-id]))) + (let [result (rf/sub [sub-name]) + token (nth result 1) + chains (-> token + :balances-per-chain + keys)] + (is (match? (count chains) 1)) + (is (match? (first chains) test-data/arbitrum-chain-id))))) + +(h/deftest-sub :wallet/aggregated-tokens-in-active-networks + [sub-name] + (testing "aggregated tokens in selected networks" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (test-data/add-active-networks-to-db [test-data/arbitrum-chain-id]))) + + (let [result (rf/sub [sub-name]) + token (first result) + chains (-> token + :balances-per-chain + keys)] + (is (match? (count chains) 1)) + (is (match? (first chains) test-data/arbitrum-chain-id))))) + +(h/deftest-sub :wallet/wallet-send-fee-fiat-formatted + [sub-name] + (testing "wallet send fee calculated and formatted in fiat" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts-with-tokens) + (assoc-in [:wallet :current-viewing-account-address] "0x1") + (update-in db-path/send assoc :route route-data) + (assoc :currencies currencies) + (assoc-in [:wallet :tokens :prices-per-token] + {:ETH {:usd 2000} :DAI {:usd 1}}) + (assoc-in [:profile/profile :currency] :usd) + (assoc-in [:profile/profile :currency-symbol] "$"))) + + (is (match? (rf/sub [sub-name]) "$0.20")))) + +(h/deftest-sub :wallet/zero-balance-in-all-non-watched-accounts? + [sub-name] + (testing "returns true if the balance is zero in all non-watched accounts" + (swap! rf-db/app-db + #(assoc-in % + [:wallet :accounts] + {"0x1" {:address "0x1" + :watch-only? false + :tokens + [{:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}} + {:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}}]} + "0x2" {:address "0x2" + :watch-only? false + :tokens + [{:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}} + {:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}}]} + + "0x3" + {:address "0x3" + :watch-only? true + :tokens [{:balances-per-chain {test-data/mainnet-chain-id {:raw-balance + (money/bignumber + "2") + :has-error false} + test-data/optimism-chain-id {:raw-balance + (money/bignumber + "1") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance + (money/bignumber + "0") + :has-error false}}} + {:balances-per-chain {test-data/mainnet-chain-id {:raw-balance + (money/bignumber + "0") + :has-error false} + test-data/optimism-chain-id {:raw-balance + (money/bignumber + "2") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance + (money/bignumber + "0") + :has-error + false}}}]}})) + (is (true? (rf/sub [sub-name])))) + (testing "returns false if the balance is not zero in all non-watched accounts" + (swap! rf-db/app-db + #(assoc-in % + [:wallet :accounts] + {"0x1" {:address "0x1" + :watch-only? false + :tokens + [{:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "2") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "1") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}} + {:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "2") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}}]} + "0x2" {:address "0x2" + :watch-only? true + :tokens + [{:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "2") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "1") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}} + {:balances-per-chain {test-data/mainnet-chain-id {:raw-balance (money/bignumber + "0") + :has-error false} + test-data/optimism-chain-id {:raw-balance (money/bignumber + "2") + :has-error false} + test-data/arbitrum-chain-id {:raw-balance (money/bignumber + "0") + :has-error false}}}]}})) + (is (false? (rf/sub [sub-name]))))) + +(h/deftest-sub :wallet/selected-keypair-keycard? + [sub-name] + (testing "returns true if the selected keypair has keycards" + (swap! rf-db/app-db + #(assoc-in % + [:wallet :keypairs] + {:keypair-1 {:id :keypair-1 + :keycards [:keycard-1 :keycard-2]}})) + (swap! rf-db/app-db + #(assoc-in % [:wallet :ui :create-account :selected-keypair-uid] :keypair-1)) + (is (true? (rf/sub [sub-name])))) + + (testing "returns false if the selected keypair has no keycards" + (swap! rf-db/app-db + #(assoc-in % + [:wallet :keypairs] + {:keypair-2 {:id :keypair-2 + :keycards []}})) + (swap! rf-db/app-db + #(assoc-in % [:wallet :ui :create-account :selected-keypair-uid] :keypair-2)) + (is (false? (rf/sub [sub-name]))))) diff --git a/src/status_im/translations/af.cljs b/src/status_im/translations/af.cljs deleted file mode 100644 index 97a5cceb7a2..00000000000 --- a/src/status_im/translations/af.cljs +++ /dev/null @@ -1,227 +0,0 @@ -(ns status-im.translations.af) - -(def translations - {;;common - :members-title "lede" - :not-implemented "!nie geimplimenteer nie" - :chat-name "Bynaam" - :notifications-title "Kennisgewings en klanke" - :offline "Aflyn" - - ;;drawer - :switch-users "Verander gebruikers" - - ;;chat - :is-typing "is besig om te tik" - :and-you "en jy" - :search-chat "Deursoek geselsies" - :members {:one "1 lid" - :other "{{count}} lede" - :zero "geen lede"} - :members-active {:one "1 lid, 1 aktief" - :other "{{count}} lede, {{count}} aktief" - :zero "geen lede"} - :active-online "Aanlyn" - :active-unknown "Onbekend" - :available "Beskikbaar" - :no-messages "Geen boodskappe" - :suggestions-requests "Versoeke" - :suggestions-commands "Opdragte" - - ;;sync - :sync-in-progress "Besig om te sinchroniseer..." - :sync-synced "gesinchroniseer" - - ;;messages - :status-sending "Besig om te stuur" - :status-pending "Hangende" - :status-sent "Gestuur" - :status-seen-by-everyone "Deur almal gesien" - :status-seen "Gesien" - :status-delivered "Afgelewer" - :status-failed "Gefaal" - - ;;datetime - :datetime-second {:one "sekonde" - :other "sekondes"} - :datetime-minute {:one "minuut" - :other "minute"} - :datetime-hour {:one "uur" - :other "ure"} - :datetime-day {:one "dag" - :other "dae"} - :datetime-ago "gelede" - :datetime-yesterday "gister" - :datetime-today "vandag" - - ;;profile - :profile "Profiel" - :message "Boodskap" - :not-specified "Nie gespesifiseer nie" - :public-key "Openbare sleutel" - :phone-number "Telefoonnommer" - :add-to-contacts "Voeg by kontakte" - - ;;make_photo - :image-source-title "Profielbeeld" - :image-source-make-photo "Opname" - :image-source-gallery "Kies uit galery" - - ;;sign-up - :contacts-syncronized "U kontakte is gesinchroniseer" - :confirmation-code (str "Dankie! Ons het vir u 'n SMS gestuur met 'n bevestigings " - "kode. Verskaf asseblief daardie kode om u telefoonnommer te bevestig") - :incorrect-code (str "Jammer, die kode was nie korrek nie, voer asseblief weer in") - :phew-here-is-your-passphrase "*Sjoe* dit was moeilik, hier is jou tydelike wagwoord, *skryf dit neer en hou dit veilig!* Jy sal dit nodig hê om jou rekening te herwin." - :here-is-your-passphrase "Hier is jou tydelike wagwoord, *skryf dit neer en hou dit veilig!* Jy sal dit nodig hê om jou rekening te herwin." - :phone-number-required "Tik hier om jou telefoonnommer in te voer & ek sal jou vriende opspoor" - :intro-status "Gesels met my om jou rekening op te stel en jou stellings te verander!" - :intro-message1 "Welkom by Status\nTik tik hierdie boodskap om jou wagwoord te stel & laat ons begin!" - :account-generation-message "Net 'n oomblik, ek moet mal somme doen om jou rekening te skep!" - - ;;chats - :chats "Geselsies" - :new-group-chat "Nuwe groepgeselsie" - - ;;discover - :discover "Ontdekking" - :none "Geen" - :search-tags "Tik jou soek-oortjies hier in" - :popular-tags "Gewilde oortjies" - :recent "Onlangs" - :no-statuses-discovered "Geen statusse gevind nie" - - ;;settings - :settings "Stellings" - - ;;contacts - :contacts "Kontakte" - :new-contact "Nuwe kontak" - :contacts-group-new-chat "Begin nuwe geselsie" - :no-contacts "Nog geen kontakte nie" - :show-qr "Wys QR" - - ;;group-settings - :remove "Verwyder" - :save "Stoor" - :clear-history "Maak geskiedenis skoon" - :chat-settings "Geselsie-stellings" - :edit "Wysig" - :add-members "Voeg lede by" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "geselsie-uitnodiging ontvang" - :removed-from-chat "het jou van groepsgeselsie verwyder" - :left "uitgegaan" - :invited "uitgenooi" - :removed "verwyder" - :You "Jou" - - ;;new-contact - :add-new-contact "Voeg nuwe kontak by" - :scan-qr "Skandeer QR" - :name "Naam" - :address-explication "Miskien moet hier 'n bietjie teks wees wat verduidelik wat 'n adres is en waar om daarvoor te soek." - :contact-already-added "Die kontak is alreeds bygevoeg" - :can-not-add-yourself "Jy kan nie jouself byvoeg nie" - :unknown-address "Onbekende adres" ;;login - :connect "Konnekteer" - :address "Adres" - :password "Wagwoord" - :wrong-password "Verkeerde wagwoord" - - ;;recover - :passphrase "Tydelike wagwoord" - :recover "Herstel" - - ;;accounts - :recover-access "Herwin toegang" - - ;;wallet-qr-code - :done "Gedoen" - :main-wallet "Hoofbeursie" - - ;;validation - :invalid-phone "Ongeldige telefoonnommer" - :amount "Bedrag" - ;;transactions - :status "Status" - :recipient "Ontvanger" - - ;;:webview - :web-view-error "oepsie, fout" - - :confirm "Bevestig" - :phone-national "Nasionaal" - :public-group-topic "Onderwerp" - :debug-enabled "Ontfout-bediener is bekendgestel! U kan nou u DApp byvoeg deur *status-dev-cli scan* te hardloop vanaf u rekenaar" - :new-public-group-chat "Sluit aan by openbare klets" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Kanselleer" - :twelve-words-in-correct-order "12 woorde in korrekte volgorde" - :remove-from-contacts "Verwyder van kontakte" - :delete-chat "Skrap klets" - :edit-chats "Redigeer kletse" - :sign-in "Teken aan" - :create-new-account "Skep nuwe rekening" - :sign-in-to-status "Teken aan op Status" - :got-it "Het dit" - :move-to-internal-failure-message "Ons moet ‘n paar belangrike lêers skuif van eksterne na interne berging. Om dit te doen, het ons u toestemming nodig. Ons sal nie eksterne berging gebruik in toekomstige weergawes nie." - :edit-group "Redigeer groep" - :delete-group "Skrap groep" - :browsing-title "Blaai" - :reorder-groups "Herrangskik groepe" - :browsing-cancel "Kanselleer" - :faucet-success "Kraan versoek is ontvang" - :choose-from-contacts "Kies uit kontakte" - :new-group "Nuwe groep" - :phone-e164 "Internasionaal 1" - :remove-from-group "Verwyder van groep" - :search-contacts "Soek kontakte" - :transaction "Transaksie" - :public-group-status "Openbaar" - :leave-chat "Verlaat klets" - :start-conversation "Begin gesprek" - :topic-format "Verkeerde formaat [a-z0-9\\-]+" - :enter-valid-public-key "Voer asseblief ‘n geldige openbare sleutel in of skandeer ‘n QR-kode in" - :faucet-error "Kraan versoek fout" - :phone-significant "Aansienlik" - :search-for "Soek vir..." - :sharing-copy-to-clipboard "Kopieer na knipbord" - :phone-international "Internasionaal 2" - :enter-address "Voeg ‘n adres in" - :send-transaction "Stuur transaksie" - :delete-contact "Skrap kontak" - :mute-notifications "Demp kennisgewings" - - :contact-s {:one "kontak" - :other "kontakte"} - :next "Volgende" - :from "Van" - :search-chats "Soek kletse" - :in-contacts "In kontakte" - - :sharing-share "Deel..." - :type-a-message "Tik ‘n boodskap..." - :type-a-command "Begin ‘n opdrag tik..." - :shake-your-phone "Vind ‘n fout of het ‘n voorstel? Skud ~net~ u foon!" - :status-prompt "Skep ‘n status om te help dat mense weet oor die goed wat u aanbied. U kan #hutsmerke ook gebruik." - :add-a-status "Voeg ‘n status by..." - :error "Fout" - :edit-contacts "Redigeer kontakte" - :more "meer" - :cancel "Kanselleer" - :no-statuses-found "Geen statusse gevind nie" - :browsing-open-in-web-browser "Maak oop in webblaaier" - :delete-group-prompt "Dit sal nie kontakte affekteer nie" - :edit-profile "Redigeer profiel" - :empty-topic "Leë onderwerp" - :to "Aan" - :data "Data"}) diff --git a/src/status_im/translations/ar.cljs b/src/status_im/translations/ar.cljs deleted file mode 100644 index 16f98a6c28a..00000000000 --- a/src/status_im/translations/ar.cljs +++ /dev/null @@ -1,161 +0,0 @@ -(ns status-im.translations.ar) - -(def translations - {;;common - :members-title "الأعضاء" - :not-implemented "!غير مُطَبّق" - :chat-name "اسم الدردشة" - :notifications-title "الإخطارات والأصوات" - :offline "غير متصل" - - ;;drawer - :switch-users "التبديل بين المستخدمين" - - ;;chat - :is-typing "يكتب" - :and-you "وأنت أيضاً" - :search-chat "البحث في الدردشة" - :members {:one "1 عضو" - :other "{{count}} أعضاء" - :zero "لا يوجد أعضاء"} - :members-active {:one "1 عضو، 1 نشط" - :other "{{count}} أعضاء، {{count}} نشط" - :zero "لا يوجد أعضاء"} - :active-online "متصل بالانترنت" - :active-unknown "غير معروف" - :available "متاح" - :no-messages "لا توجد رسائل" - :suggestions-requests "الطلبات" - :suggestions-commands "الأوامر" - - ;;sync - :sync-in-progress "قيد المزامنة..." - :sync-synced "بالتزامن" - - ;;messages - :status-sending "إرسال" - :status-pending "ريثما" - :status-sent "تم الإرسال" - :status-seen-by-everyone "شوهد من قبل الجميع" - :status-seen "شوهد" - :status-delivered "تم الاستلام" - :status-failed "فشل" - - ;;datetime - :datetime-second {:one "ثانية" - :other "ثوان"} - :datetime-minute {:one "دقيقة" - :other "دقائق"} - :datetime-hour {:one "ساعة" - :other "ساعات"} - :datetime-day {:one "يوم" - :other "أيام"} - :datetime-ago "منذ" - :datetime-yesterday "الأمس" - :datetime-today "اليوم" - - ;;profile - :profile "الملف الشخصي" - :message "الرسالة" - :not-specified "غير محدد" - :public-key "المفتاح العمومي" - :phone-number "رقم الهاتف" - :add-to-contacts "أضف إلى جهات الاتصال" - - ;;make_photo - :image-source-title "الصورة الشخصية" - :image-source-make-photo "التقاط صورة" - :image-source-gallery "الاختيار من معرض الصور" - - ;;sign-up - :contacts-syncronized "تمت مزامنة جهات الاتصال الخاصة بك" - :confirmation-code (str "شكراً! لقد أرسلنا لك رسالة نصية تتضمن رمز تأكيد" - "يرجى إدخال هذا الرمز لتأكيد رقم هاتفك") - :incorrect-code (str "عذراً الرمز غير صحيح، يرجى إدخاله مرة أخرى") - :phew-here-is-your-passphrase "*أف* لقد كان أمراً شاقاً، إليك عبارة المرور الخاصة بك، * قم بتسجيلها واحتفظ بها في مكان آمن!* سوف تحتاج إليها لاسترداد حسابك." - :here-is-your-passphrase "إليك عبارة المرور الخاصة بك، * قم بتسجيلها واحتفظ بها في مكان آمن! *سوف تحتاج إليها لاسترداد حسابك" - :phone-number-required "اضغط هنا لإدخال رقم الهاتف الخاص بك وسوف تعثر على أصدقائك" - :intro-status "دردش معي لتثبيت حسابك وقم بتغيير الإعدادات الخاصة بك!" - :intro-message1 "مرحبا بك في Status \n اضغط على هذه الرسالة لتعيين كلمة المرور الخاصة بك وابدأ!" - :account-generation-message "امنحني ثانية واحدة، سوف أحتاج إلى إجراء بعض الحسابات الرياضية المجنونة لإنتاج الحساب الخاص بك!" - - ;;chats - :chats "الدردشات" - :new-group-chat "مجموعة دردشة جديدة" - - ;;discover - :discover "اكتشاف" - :none "لا شيء" - :search-tags "اكتب بيانات بحثك هنا" - :popular-tags "العلامات المشهورة" - :recent "حديثة" - :no-statuses-discovered "لم يتم الكشف عن حالات" - - ;;settings - :settings "الإعدادات" - - ;;contacts - :contacts "جهات الاتصال" - :new-contact "جهة اتصال جديدة" - :contacts-group-new-chat "ابدأ دردشة جديدة" - :no-contacts "لا توجد جهات اتصال بعد" - :show-qr "عرض شفرة التعريف" - - ;;group-settings - :remove "إزالة" - :save "حفظ" - :clear-history "حذف التاريخ" - :chat-settings "إعدادات الدردشة" - :edit "تحرير" - :add-members "إضافة أعضاء" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "تسلم دعوة الدردشة" - :removed-from-chat "قام بإزالتك من مجموعة الدردشة" - :left "غادر" - :invited "مَدْعُوّ" - :removed "مُسْتَبْعَد" - :You "أنت" - - ;;new-contact - :add-new-contact "إضافة جهة اتصال جديدة" - :scan-qr "مسح شفرة التعريف" - :name "اسم" - :address-explication "ربما يتعين أن تتوافر هنا بعض النصوص التي تشرح ما هو العنوان وأين تبحث عنه" - :contact-already-added "تمت إضافة جهة الاتصال" - :can-not-add-yourself "لا يمكنك إضافة نفسك" - :unknown-address "عنوان غير معروف" - - ;;login - :connect "اتصال" - :address "عنوان" - :password "كلمة مرور" - :wrong-password "كلمة مرور خاطئة" - - ;;recover - :passphrase "عبارة المرور" - :recover "استعادة" - - ;;accounts - :recover-access "استعادة إمكانية الوصول" - - ;;wallet-qr-code - :done "تم التنفيذ" - :main-wallet "المحفظة الرئيسية" - - ;;validation - :invalid-phone "رقم هاتف غير صحيح" - :amount "الكمية" - ;;transactions - :status "الحالة" - :recipient "المستلم" - - ;;:webview - :web-view-error "عفواً، حدث خطأ ما"}) diff --git a/src/status_im/translations/bel.cljs b/src/status_im/translations/bel.cljs deleted file mode 100644 index 88e5d46d97b..00000000000 --- a/src/status_im/translations/bel.cljs +++ /dev/null @@ -1,175 +0,0 @@ -(ns status-im.translations.bel) - -(def translations - {;;common - :members-title "Удзельнікі" - :not-implemented "!не рэалізавана" - :chat-name "Імя чата" - :notifications-title "Апавяшчэнні і гукі" - :offline "Оффлайн" - :cancel "Адмена" - :next "Працягнуць" - - ;;drawer - :switch-users "Пераключыць карыстальнікау" - - ;;chat - :is-typing "друкуе" - :and-you "і вы" - :search-chat "Пошук па чату" - :members {:one "1 член" - :other "{{count}} члена(ау)" - :zero "няма членов"} - :members-active {:one "1 член, 1 актыўны" - :other "{{count}} члена(ау), {{count}} актыўных" - :zero "няма членау"} - :active-online "У сетцы" - :active-unknown "Невядома" - :available "Даступна" - :no-messages "Няма паведамленняў" - :suggestions-requests "Запыты" - :suggestions-commands "Каманды" - - ;;sync - :sync-in-progress "Сінхранізацыя..." - :sync-synced "Сінхранізуецца" - - ;;messages - :status-sending "Адпраўляецца" - :status-pending "у чаканні" - :status-sent "Адправіць" - :status-seen-by-everyone "Прагледжана усімі" - :status-seen "Прагледжана" - :status-delivered "Дастаўлена" - :status-failed "Памылка" - - ;;datetime - :datetime-second {:one "секунда" - :other "секунды"} - :datetime-minute {:one "хвіліна" - :other "хвіліны"} - :datetime-hour {:one "гадзіна" - :other "гадзін"} - :datetime-day {:one "дзень" - :other "дні"} - :datetime-ago "назад" - :datetime-yesterday "учора" - :datetime-today "сёння" - - ;;profile - :profile "Профіль" - :message "Паведамленне" - :not-specified "Не пазначана" - :public-key "Публічны ключ" - :phone-number "Нумар тэлефона" - :add-to-contacts "Дадаць у кантакты" - - ;;make_photo - :image-source-title "Mалюнак профілю" - :image-source-make-photo "Сфатаграфаваць" - :image-source-gallery "Выбраць з галерэі" - - ;;sharing - :sharing-copy-to-clipboard "Скапіяваць" - :sharing-share "Падзяліцца..." - :sharing-cancel "Адмена" - - ;;sign-up - :contacts-syncronized "Вашы кантакты сінхранізаваныя" - :confirmation-code (str "Дзякуй! Мы адправілі вам СМС з кодам пацверджання." - "Калі ласка, увядзіце гэты код для пацвярджэння свайго нумара тэлефона") - :incorrect-code (str "Выбачайце, код няправільны, увядзіце яшчэ раз") - :phew-here-is-your-passphrase (str "*Уф*, эгэта было няпроста, вось ваша парольная фраза, *запішыце яе і захавайце ў надзейным месцы!* " - "Яна будзе патрэбна вам для аднаўлення акаўнта.") - :here-is-your-passphrase (str "Вось ваша парольная фраза, *запішыце яе і захавайце ў надзейным месцы!* " - "Яна будзе патрэбна вам для аднаўлення акаўнта.") - :phone-number-required "Націсніце сюды для ўводу свайго нумара тэлефона і пошуку сваіх сяброў" - :intro-status "Пагутарыце са мной у чаце, каб наладзіць свой рахунак і змяніць свае налады!" - :intro-message1 "Сардэчна запрашаем у Статус\nНацісніце на гэтае паведамленне, каб усталяваць пароль і пачаць!" - :account-generation-message "Секундачку, мне трэба выканаць вар'яцка складаныя разлікі для стварэння вашага акаўнта!" - - ;;chats - :chats "Чаты" - :new-group-chat "Новы групавы чат" - - ;;discover - :discover "Пошук" - :none "Няма" - :search-tags "Увядзіце тэгі для пошуку сюды" - :popular-tags "Папулярныя тэгі" - :recent "Апошнія" - :no-statuses-discovered "Статусы не выяўлены" - - ;;settings - :settings "Налады" - - ;;contacts - :contacts "Кантакты" - :new-contact "Новы кантакт" - :edit-contacts "Рэдагаванне кантактаў" - :contacts-group-new-chat "Пачаць новы чат" - :no-contacts "Пакуль няма кантактаў" - :show-qr "Паказаць QR" - :enter-address "Ўвесці адрас" - - ;;group-settings - :remove "Выдаліць" - :save "Захаваць" - :clear-history "Ачысціць гісторыю" - :chat-settings "Налады чата" - :edit "Змяніць" - :add-members "Дадаць членаў" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Новая група" - :reorder-groups "Упарадкаваць групы" - - ;;participants - - ;;protocol - :received-invitation "атрымаў(ла) запрашэнне ў чат" - :removed-from-chat "выдаліў(ла) вас з групавога чата" - :left "засталося" - :invited "запрошаны(на)" - :removed "выдалены(на)" - :You "Вы" - - ;;new-contact - :add-new-contact "Дадаць новы кантакт" - :scan-qr "Сканаваць QR" - :name "Імя" - :address-explication "Можа быць, тут павінен быць нейкі тэкст, які тлумачыць адрас і тое, дзе яго шукаць" - :enter-valid-public-key "Калі ласка, увядзіце сапраўдны публічны ключ ці скануйце QR-код" - :contact-already-added "Кантакт ўжо дададзены" - :can-not-add-yourself "Вы не можаце дадаць сябе" - :unknown-address "Невядомы адрас" - - ;;login - :connect "Падлучыцца" - :address "Адрас" - :password "Пароль" - :wrong-password "Няправільны пароль" - - ;;recover - :passphrase "Парольная фраза" - :recover "Аднавіць" - - ;;accounts - :recover-access "Аднавіць доступ" - - ;;wallet-qr-code - :done "Гатова" - :main-wallet "Асноўны кашалёк" - - ;;validation - :invalid-phone "Няправільны нумар тэлефона" - :amount "Сума" - ;;transactions - :status "Статус" - :recipient "Атрымальнік" - - ;;:webview - :web-view-error "ой, памылка"}) diff --git a/src/status_im/translations/cs.cljs b/src/status_im/translations/cs.cljs deleted file mode 100644 index 558a1184b4c..00000000000 --- a/src/status_im/translations/cs.cljs +++ /dev/null @@ -1,262 +0,0 @@ -(ns status-im.translations.cs) - -(def translations - {;;common - :members-title "Členové" - :not-implemented "!není implementováno" - :chat-name "Název chatu" - :notifications-title "Oznámení a zvuky" - :offline "Offline" - :search-for "Hledat..." - :cancel "Storno" - :next "Další" - :type-a-message "Napište zprávu..." - :type-a-command "Začněte psát příkaz..." - :error "Chyba" - - :camera-access-error "Pro udělení potřebných oprávnění ke kameře přejděte do nastavení systému a ujistěte se, že je vybráno Status > Kamera." - :photos-access-error "Pro udělení potřebných oprávnění k fotoaparátu přejděte do nastavení systému a ujistěte se, že je vybráno Status > Fotoaparát." - - ;;drawer - :switch-users "Přepnout uživatele" - :current-network "Aktuální síť" - - ;;chat - :is-typing "píše" - :and-you "a ty" - :search-chat "Hledat v chatu" - :members {:one "1 účastník" - :other "{{count}} účastníci" - :zero "žádní účastníci"} - :members-active {:one "1 účastník" - :other "{{count}} účastníci" - :zero "žádní účastníci"} - :public-group-status "Veřejné" - :active-online "Online" - :active-unknown "Neznámý" - :available "Dostupný" - :no-messages "Žádné zprávy" - :suggestions-requests "Požadavky" - :suggestions-commands "Příkazy" - :faucet-success "Požadavek na faucet byl přijat" - :faucet-error "Chyba požadavku na faucet" - - ;;sync - :sync-in-progress "Synchronizuji..." - :sync-synced "Synchronizováno" - - ;;messages - :status-sending "Odesílám" - :status-pending "Čekám" - :status-sent "Odesláno" - :status-seen-by-everyone "Přečteno všemi" - :status-seen "Přečteno" - :status-delivered "Doručeno" - :status-failed "Selhalo" - - ;;datetime - :datetime-ago-format "{{ago}} {{number}} {{time-intervals}}" - :datetime-second {:one "sekunda" - :other "sekund(y)"} - :datetime-minute {:one "minuta" - :other "minut(y)"} - :datetime-hour {:one "hodina" - :other "hodin(y)"} - :datetime-day {:one "den" - :other "dny(ů)"} - :datetime-ago "uplynulo:" - :datetime-yesterday "včera" - :datetime-today "dnes" - - ;;profile - :profile "Profil" - :edit-profile "Upravit profil" - :message "Zpráva" - :not-specified "Není zadáno" - :public-key "Veřejný klíč" - :phone-number "Telefonní číslo" - :update-status "Aktualizovat svůj status..." - :add-a-status "Přidat status..." - :status-prompt "Můžete vytvořit status, aby ostatní věděli, co nabízíte. Můžete používat i #hashtagy." - :add-to-contacts "Přidat do kontaktů" - :in-contacts "V kontaktech" - :remove-from-contacts "Odstranit z kontaktů" - :start-conversation "Zahájit konverzaci" - :send-transaction "Odeslat transakci" - - ;;make_photo - :image-source-title "Profilový obrázek" - :image-source-make-photo "Vyfotit" - :image-source-gallery "Vybrat z galerie" - - ;;sharing - :sharing-copy-to-clipboard "Kopírovat do schránky" - :sharing-share "Sdílet..." - :sharing-cancel "Storno" - - :browsing-title "Prohlížet" - :browsing-open-in-web-browser "Otevřít ve webovém prohlížeči" - :browsing-cancel "Storno" - - ;;sign-up - :contacts-syncronized "Vaše kontakty byly synchronizovány" - :confirmation-code (str "Díky! Poslali jsme Ti textovou zprávu s kódem pro potvrzení. " - "Prosím potvrď své telefonní číslo zadáním tohoto kódu.") - :incorrect-code (str "Tento kód není správný, prosím zkus to znovu.") - :phew-here-is-your-passphrase "*Uf*, to byla fuška, tady jsou bezpečnostní slova, *zapiš je a ulož na bezpečném místě!* Budou potřeba pro obnovení přístupu k účtu." - :here-is-your-passphrase "Tady jsou bezpečnostní slova, *zapiš je a ulož na bezpečném místě!* Budou potřeba pro obnovení přístupu k účtu." - :phone-number-required "Klepnutím zde můžeš zadat své telefonní číslo a hledat přátele." - :shake-your-phone "Našel jsi chybu nebo máš návrh? Prostě --zatřes-- telefonem!" - :intro-status "Chat se mnou ti může pomoci nastavit ůčet a změnit další nastavení!" - :intro-message1 (str "Vítá vás Status\nKlepněte na tuto zprávu pro nastavení hesla a zahájení!") - :account-generation-message "Vteřinku, musím použít docela těžkou matiku na vygenerování účtu!" - :move-to-internal-failure-message "Potřebujeme přesunout některé důležité soubory z externího na interní úložiště. K tomu potřebujeme tvoje povolení. V dalších verzích už nebudeme externí úložiště používat." - :debug-enabled "Server pro ladění byl spuštěn! Nyní můžeš spustit *status-dev-cli scan* pro nalezení serveru z počítače ve stejné síti." - - ;;phone types - :phone-e164 "Mezinárodní 1" - :phone-international "Mezinárodní 2" - :phone-national "Národní" - :phone-significant "Významný" - - ;;chats - :chats "Chaty" - :delete-chat "Smazat chat" - :new-group-chat "Nový skupinový chat" - :new-public-group-chat "Přidat se k veřejnému chatu" - :edit-chats "Upravit chaty" - :search-chats "Hledat v chatech" - :empty-topic "Prázdné téma" - :topic-format "Špatný formát [a-z0-9\\-]+" - :public-group-topic "Téma" - - ;;discover - :discover "Objevit" - :none "Žádný" - :search-tags "Napište tagy pro hledání" - :popular-tags "Populární tagy" - :recent "Poslední" - :no-statuses-discovered "Žádné statusy nebyly objeveny" - :no-statuses-found "Žádné statusy nebyly nalezeny" - - ;;settings - :settings "Nastavení" - - ;;contacts - :contacts "Kontakty" - :new-contact "Nový kontakt" - :delete-contact "Smazat kontakt" - :delete-contact-confirmation "Tento kontakt bude smazán z tvých kontaktů" - :remove-from-group "Odstranit ze skupiny" - :edit-contacts "Upravit kontakty" - :search-contacts "Hledat kontakty" - :contacts-group-new-chat "Zahájit nový chat" - :choose-from-contacts "Vybrat z kontaktů" - :no-contacts "Ještě tu nejsou žádné kontakty" - :show-qr "Zobrazit QR kǒd" - :enter-address "Zadejte adresu" - :more "více" - - ;;group-settings - :remove "Odstranit" - :save "Uložit" - :delete "Smazat" - :clear-history "Vymazat historii" - :mute-notifications "Vypnout oznámení" - :leave-chat "Opustit chat" - :chat-settings "Nastavení chatu" - :edit "Upravit" - :add-members "Přidat členy" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nová skupina" - :reorder-groups "Uspořádat skupiny" - :edit-group "Upravit skupinu" - :delete-group "Smazat skupinu" - :delete-group-confirmation "Tato skupina bude odstraněna z tvých skupin. Kontaktů se to nedotkne." - :delete-group-prompt "Kontaktů se to nedotkne" - :contact-s {:one "kontakt" - :other "kontakty(ů)"} - ;;participants - - ;;protocol - :received-invitation "přijata pozvánka k chatu" - :removed-from-chat "přijata pozvánka k skupinovému chatu" - :left "opuštěno" - :invited "pozván" - :removed "odstraněn" - :You "Ty" - - ;;new-contact - :add-new-contact "Přidat nový kontakt" - :scan-qr "Načíst QR kód" - :name "Jméno" - :address-explication "Identita na Whisperu je adresa blockchainové sítě Ethereum" - :enter-valid-public-key "Prosím zadejte platný veřejný klíč nebo načtěte QR kód" - :contact-already-added "Tento kontakt již byl přidán" - :can-not-add-yourself "Nemůžeš přidat sebe" - :unknown-address "Neznámá adresa" - - ;;login - :connect "Připojit" - :address "Adresa" - :password "Heslo" - :sign-in-to-status "Přihlásit se do Statusu" - :sign-in "Přihlásit se" - :wrong-password "Špatné heslo" - - ;;recover - :passphrase "Bezpečnostní slova" - :recover "Obnovit" - :twelve-words-in-correct-order "12 anglických slov ve správném pořadí" - - ;;accounts - :recover-access "Obnovit přístup" - :create-new-account "Vytvořit nový účet" - - ;;wallet-qr-code - :done "Hotovo" - - ;;validation - :invalid-phone "Neplatné telefonní číslo" - :amount "Množství" - ;;transactions - :confirm "Potvrdit" - :transaction "Transakce" - :status "Status" - :recipient "Příjemce" - :to "Komu" - :from "Od" - :data "Data" - :got-it "Mám to" - - ;;:webview - :web-view-error "ups, chyba" - ;;testfairy warning - :testfairy-title "Varování!" - :testfairy-message "Používáte aplikaci z nestabilního sestavení. Kvůli testování se v případě, že používáte Wi-Fi připojení, veškerá interakce nahrává (jako video a záznamy) a tyto informace může využít tým vývojářů pro řešení problémů. Uložená videa ani záznamy neobsahují Vaše hesla. Toto naahrávání se provádí jen v případě, že je aplikace nainstalována z nestabilního sestavení. Pokud je aplikace instalována z Obchodu Play nebo TestFlightu, nic se nenahrává." - - ;; wallet - :wallet "Peněženka" - :wallets "Peněženky" - :your-wallets "Tvé peněženky" - :main-wallet "Hlavní peněženka" - :wallet-send "Poslat" - :wallet-request "Požadavek" - :wallet-exchange "Směnárna" - :wallet-assets "Aktiva" - :transactions "Transakce" - :transactions-sign "Podepsat" - :transactions-sign-all "Podepsat vše" - :transactions-delete "Smazat" - :transactions-history "Historie" - :transactions-unsigned "Nepodepsáno" - :transactions-history-empty "V historii nemáš žádné transakce" - :transactions-unsigned-empty "Nemáš žádné nepodepsané transakce" - :transactions-filter-title "Filtrovat historii" - :transactions-filter-tokens "Tokeny" - :transactions-filter-type "Typ" - :transactions-filter-select-all "Označit vše"}) diff --git a/src/status_im/translations/da.cljs b/src/status_im/translations/da.cljs deleted file mode 100644 index e853462907e..00000000000 --- a/src/status_im/translations/da.cljs +++ /dev/null @@ -1,237 +0,0 @@ -(ns status-im.translations.da) - -(def translations - {;;common - :members-title "Medlemmere" - :not-implemented "Ikke implementeret!" - :chat-name "Chatnavn" - :notifications-title "Notifikationer" - :offline "Offline" - :search-for "Søg efter..." - :cancel "Annullér" - :next "Næste" - :type-a-message "Skriv en besked..." - :type-a-command "Skriv en kommando..." - :error "Fejl" - - :camera-access-error "Gå venligst til dine systemindstillinger og sørg for at du Status > Kamera er tilladt." - :photos-access-error "Gå venligst til dine systemindstillinger og sørg for at du Status > Billeder er tilladt." - - ;;drawer - :switch-users "Skift bruger" - :current-network "Nuværende netværk" - - ;;chat - :is-typing "skriver" - :and-you "og du" - :search-chat "Søg i chat" - :members {:one "1 medlem" - :other "{{count}} medlemmer" - :zero "ingen medlemmer"} - :members-active {:one "1 medlem" - :other "{{count}} medlemmer" - :zero "ingen medlemmer"} - :public-group-status "Offentlig" - :active-online "Online" - :active-unknown "Ukendt" - :available "Aktiv" - :no-messages "Ingen beskeder" - :suggestions-requests "Anmodninger" - :suggestions-commands "Kommandoer" - :faucet-success "Faucet request er modtaget" - :faucet-error "Faucet request fejl" - - ;;sync - :sync-in-progress "Synkroniserer..." - :sync-synced "Synkroniseret" - - ;;messages - :status-sending "Sender" - :status-pending "Under behandling" - :status-sent "Sendt" - :status-seen-by-everyone "Set af alle" - :status-seen "Set" - :status-delivered "Leveret" - :status-failed "Mislykket" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "sekund" - :other "sekunder"} - :datetime-minute {:one "minut" - :other "minutter"} - :datetime-hour {:one "time" - :other "timer"} - :datetime-day {:one "dag" - :other "dage"} - :datetime-ago "siden" - :datetime-yesterday "i går" - :datetime-today "i dag" - - ;;profile - :profile "Profil" - :edit-profile "Rediger profil" - :message "Meddelelser" - :not-specified "Ikke angivet" - :public-key "Offentlig nøgle" - :phone-number "Telefonnummer" - :update-status "Opdater din status..." - :add-a-status "Tilføj en status..." - :status-prompt "Lav en status for at lade folk vide hvad du tilbyder. Du kan også bruge #hashtags." - :add-to-contacts "Tilføj til kontakter" - :in-contacts "I kontakter" - :remove-from-contacts "Fjern fra kontakter" - :start-conversation "Begynd samtale" - :send-transaction "Send transaktion" - - ;;make_photo - :image-source-title "Profilbillede" - :image-source-make-photo "Tag billede" - :image-source-gallery "Vælg fra galleri" - ;;sharing - :sharing-copy-to-clipboard "Kopier til udklipsholderen" - :sharing-share "Del..." - :sharing-cancel "Annullér" - - :browsing-title "Browse" - :browsing-open-in-web-browser "Åben i web browser" - :browsing-cancel "Annullér" - - ;;sign-up - :contacts-syncronized "Dine kontakter er synkroniseret" - :confirmation-code (str "Tak! Vi har sendt dig en sms med en bekræftelseskode. " - "Vær venlig at indtaste koden for at bekræfte dit telefonnummer") - :incorrect-code (str "Koden var desværre forkert. Prøv venligst igen") - :phew-here-is-your-passphrase "*Puha* det var hårdt, her er din kodesætning (passphrase), *skriv den ned og gem den sikkert!* Du skal bruge den for at kunne gendanne din konto." - :here-is-your-passphrase "Her er din kodesætning (passphrase), *skriv den ned og gem den sikkert!* Du behøver den for at gendanne din konto." - :phone-number-required "Klik her for at indtaste dit telefonnummer, så vil jeg finde dine venner" - :shake-your-phone "Fundet en bug eller har du et foreslag. Blot ryst din telefon!" - :intro-status "Chat med mig for at opsætte din konto og ændre dine indstillinger!!" - :intro-message1 "Velkommen til Status\n Tryk på denne meddelelse for at angive din adgangskode og komme i gang!" - :account-generation-message "Vent et øjeblik, jeg er nødt til at gøre nogle skøre beregninger for at generere din konto!" - :move-to-internal-failure-message "Vi skal flyyte nogle vigtige filer fra din eksterne til din interne hukommelse. For at gøre dette har vi brug for din tilladelse. Vi vil ikke bruge din eksterne hukkomelse i kommende versioner." - :debug-enabled "Debug server has been launched! You can now execute *status-dev-cli scan* to find the server from your computer on the same network." - - ;;phone types - :phone-e164 "International 1" - :phone-international "International 2" - :phone-national "National" - :phone-significant "Significant" - - ;;chats - :chats "Samtaler" - :delete-chat "Fjern samtale" - :new-group-chat "Ny gruppsamtale" - :new-public-group-chat "Deltag i offentlig samtale" - :edit-chats "Rediger samtaler" - :search-chats "Søg samtaler" - :empty-topic "Tomt emne" - :topic-format "Forkert format [a-z0-9\\-]+" - :public-group-topic "Emne" - - ;;discover - :discover "Opdag" - :none "Ingen" - :search-tags "Skrive de tags du vil søge efter her" - :popular-tags "Populære tags" - :recent "Seneste" - :no-statuses-discovered "Ingen statusser opdaget" - :no-statuses-found "Inger statusser fundet" - - ;;settings - :settings "Indstillinger" - - ;;contacts - :contacts "Kontakter" - :new-contact "Ny kontakt" - :delete-contact "Fjern kontakt" - :delete-contact-confirmation "Denne kontakt vil blive fjernet fra dine kontakter" - :remove-from-group "Fjern fra gruppe" - :edit-contacts "Rediger kontakter" - :search-contacts "Søg efter kontakter" - :contacts-group-new-chat "Start en ny samtale" - :choose-from-contacts "Vælg fra kontakter" - :no-contacts "Ingen kontakter endnu" - :show-qr "Vis QR-kode" - :enter-address "Indtast adresse" - :more "mere" - - ;;group-settings - :remove "Fjern" - :save "Gem" - :delete "Slet" - :clear-history "Fjern historik" - :mute-notifications "Sluk for notifikationer" - :leave-chat "Forlad samtale" - :chat-settings "Chatindstillinger" - :edit "Rediger" - :add-members "Tilføj medlemmere" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Ny gruppe" - :reorder-groups "Arranger grupper" - :edit-group "Rediger gruppe" - :delete-group "Slet gruppe" - :delete-group-confirmation "Denne gruppe vil blivve fjernet fra dine grupper. Dette vil ikke påvirke dine kontakter." - :delete-group-prompt "Dette vil ikke påvirke dine kontakter" - :contact-s {:one "kontakt" - :other "kontakter"} - ;;participants - - ;;protocol - :received-invitation "Accepterede chatinvitaion" - :removed-from-chat "Fjernede dig fra gruppechatten" - :left "Forlod" - :invited "Inviteret" - :removed "Fjernet" - :You "Du" - - ;;new-contact - :add-new-contact "Tilføj ny kontakt" - :scan-qr "Skan QR-kode" - :name "Navn" - :address-explication "Maybe here should be some text explaining what an address is and where to look for it" - :enter-valid-public-key "Indtast venligst en gyldig offentlig nøgle eller scan en QR-kode" - :contact-already-added "Kontakten er allerede tilføjet" - :can-not-add-yourself "Du kan ikke tilføje dig selv fjolle" - :unknown-address "Ukendt adresse" - - ;;login - :connect "Tilslut" - :address "Adresse" - :password "Kodeord" - :sign-in-to-status "Log på Status" - :sign-in "Log på" - :wrong-password "Forkert kodeord" - - ;;recover - :passphrase "Kodesætning (passphrase)" - :recover "Gendan" - :twelve-words-in-correct-order "12 ord i den korrekte rækkefølge" - - ;;accounts - :recover-access "Gendan adgang" - :create-new-account "Opret en ny konto" - - ;;wallet-qr-code - :done "Klar" - :main-wallet "Hovedpung" - - ;;validation - :invalid-phone "Ugyldigt telefonnummer" - :amount "Beløb" - ;;transactions - :confirm "Bekræft" - :transaction "Transaktion" - :status "Status" - :recipient "Modtager" - :to "Til" - :from "Fra" - :data "Data" - :got-it "Forstået" - - ;;:webview - :web-view-error "hovsa, fejl"}) diff --git a/src/status_im/translations/de.cljs b/src/status_im/translations/de.cljs deleted file mode 100644 index 65b7df8b764..00000000000 --- a/src/status_im/translations/de.cljs +++ /dev/null @@ -1,353 +0,0 @@ -(ns status-im.translations.de) - -(def translations - {;;common - :members-title "Mitglieder" - :not-implemented "!nicht implementiert" - :chat-name "Chatname" - :notifications-title "Benachrichtigungen und Töne" - :offline "Offline" - :search-for "Suche nach..." - :cancel "Abbrechen" - :next "Weiter" - :open "Öffnen" - :description "Beschreibung" - :url "URL" - :type-a-message "Tippe eine Nachricht..." - :type-a-command "Tippe einen Befehl..." - :error "Fehler" - :unknown-status-go-error "Unbekannter status-go Fehler" - :node-unavailable "Kein laufender Ethereum-Knoten" - :yes "Ja" - :no "Nein" - - :camera-access-error "Um den benötigten Kamera-Zugriff zu erlauben, gehe bitte in deine Systemeinstellungen und stelle sicher, dass Status > Kamera ausgewählt ist" - :photos-access-error "Um den benötigten Foto-Zugriff zu erlauben, gehe bitte in deine Systemeinstellungen und stelle sicher, dass Status > Fotos ausgewählt ist" - - ;;drawer - :switch-users "Benutzer wechseln" - :current-network "Aktuelles Netzwerk" - - ;;chat - :is-typing "schreibt" - :and-you "und du" - :search-chat "Chat durchsuchen" - :members {:one "1 Mitglied" - :other "{{count}} Mitglieder" - :zero "Keine Mitglieder"} - :members-active {:one "1 Mitglied" - :other "{{count}} Mitglieder" - :zero "Keine Mitglieder"} - :public-group-status "Öffentlich" - :active-online "Online" - :active-unknown "Unbekannt" - :available "Verfügbar" - :no-messages "Keine Nachrichten" - :suggestions-requests "Anfragen" - :suggestions-commands "Befehle" - :faucet-success "Faucet-Anfrage wurde empfangen" - :faucet-error "Faucet-Anfragefehler" - - ;;sync - :sync-in-progress "Synchronisiere..." - :sync-synced "Synchronisiert" - - ;;messages - :status-sending "Sende..." - :status-pending "Ausstehend" - :status-sent "Gesendet" - :status-seen-by-everyone "Von allen gesehen" - :status-seen "Gesehen" - :status-delivered "Zugestellt" - :status-failed "Fehlgeschlagen" - - ;;datetime - :datetime-ago-format "{{ago}} {{number}} {{time-intervals}}" - :datetime-second {:one "Sekunde" - :other "Sekunden"} - :datetime-minute {:one "Minute" - :other "Minuten"} - :datetime-hour {:one "Stunde" - :other "Stunden"} - :datetime-day {:one "Tag" - :other "Tage"} - :datetime-ago "vor" - :datetime-yesterday "Gestern" - :datetime-today "Heute" - - ;;profile - :profile "Profil" - :edit-profile "Profil bearbeiten" - :message "Nachricht" - :not-specified "Nicht spezifiziert" - :public-key "Öffentlicher Schlüssel" - :phone-number "Telefonnummer" - :update-status "Aktualisiere deinen Status..." - :add-a-status "Einen Status hinzufügen..." - :status-prompt "Erstelle deinen Status. Die Verwendung von #hashtags kann anderen helfen, dich zu finden und mit dir über das zu reden, was dir auf dem Herzen liegt" - :add-to-contacts "Zu Kontakten hinzufügen" - :in-contacts "In Kontakten" - :remove-from-contacts "Aus Kontakten entfernen" - :start-conversation "Beginne ein Gespräch" - :send-transaction "Sende Transaktion" - :testnet-text "Du befindest dich auf dem {{testnet}} Testnetzwerk - . Sende keine echten ETH oder SNT an deine Adresse" - :mainnet-text "Du befindest dich auf dem Mainnet. Echte ETH werden gesendet" - - ;;make_photo - :image-source-title "Profilbild" - :image-source-make-photo "Aufnehmen" - :image-source-gallery "Aus Gallerie auswählen" - - ;;sharing - :sharing-copy-to-clipboard "In die Zwischenablage kopieren" - :sharing-share "Teilen..." - :sharing-cancel "Abbrechen" - - :browsing-title "Browse" - :browsing-open-in-web-browser "In Webbrowser öffnen" - :browsing-cancel "Abbrechen" - - ;;sign-up - :contacts-syncronized "Deine Kontakte wurden synchronisiert" - :confirmation-code (str "Danke! Wir haben dir eine Textnachricht mit einem Bestätigungscode " - "gesendet. Bitte gib diesen Code an, um deine Telefonnummer zu verifizieren.") - :incorrect-code (str "Es tut uns leid, der Code war nicht korrekt, bitte gib ihn erneut ein") - :phew-here-is-your-passphrase "Puh, das war anstrengend. Hier ist deine Passphrase. *Notiere sie dir und bewahre sie sicher auf!* Du wirst sie brauchen, um deinen Account wiederherzustellen." - :here-is-your-passphrase "Hier ist deine Passphrase. *Notiere sie dir und bewahre sie sicher auf!* Du wirst sie brauchen, um deinen Account wiederherzustellen." - :here-is-your-signing-phrase "Hier ist deine Signing Phrase. Du wirst sie benutzen, um deine Transaktionen zu verifizieren. *Notiere sie dir und bewahre sie sicher auf!*" - :phone-number-required "Tippe hier, um deine Telefonnummer zu validieren und ich werde deine Freunde finden." - :shake-your-phone "Du hast einen Bug gefunden oder hast einen Verbesserungsvorschlag? ~Schüttle~ einfach dein Handy!" - :intro-status "Chatte mit mir, um deinen Account einzurichten und deine Einstellungen zu ändern." - :intro-message1 "Willkommen bei Status!\nKlicke auf diese Nachricht, um dein Passwort festzulegen und loszulegen." - :account-generation-message "Gib mir eine Sekunde, ich muss wahnsinnig komplizierte Berechnungen tätigen, um deinen Account zu generieren!" - :move-to-internal-failure-message "Wir müssen ein paar wichtige Dateien vom externen zum internen Speicher verschieben. Dafür brauchen wir deine Erlaubnis. In zukünftigen Versionen werden wir keinen externen Speicher mehr verwenden." - :debug-enabled "Der Debug-Server wurde gestartet! Du kannst jetzt *status-dev-cli scan* ausführen, um den Server von deinem Computer im selben Netzwerk zu finden." - - ;;phone types - :phone-e164 "International 1" - :phone-international "International 2" - :phone-national "National" - :phone-significant "Signifikant" - - ;;chats - :chats "Chats" - :delete-chat "Chat löschen" - :new-group-chat "Neuer Gruppenchat" - :new-public-group-chat "Öffentlichem Chat beitreten" - :edit-chats "Chats bearbeiten" - :search-chats "Chats durchsuchen" - :empty-topic "Leeres Thema" - :topic-format "Falsches Format [a-z0-9\\-]+" - :public-group-topic "Thema" - - ;;discover - :discover "Entdecken" - :none "Nichts" - :search-tags "Tippe deine Suchbegriffe hier" - :popular-tags "Beliebte #hashtags" - :recent "Kürzliche Status" - :no-statuses-found "Keine Status gefunden" - :chat "Chat" - :all "Alle" - :public-chats "Öffentliche Chats" - :soon "Bald" - :public-chat-user-count "{{count}} Personen" - :dapps "ÐApps" - :dapp-profile "ÐApp Profil" - :no-statuses-discovered "Keine Status gefunden" - :no-statuses-discovered-body "Wenn jemand einen\n Status erstellt, wirst du es hier sehen." - :no-hashtags-discovered-title "Keine #hashtags gefunden" - :no-hashtags-discovered-body "Wenn ein #hashtag beliebt\nwird, wirst du es hier sehen." - - ;;settings - :settings "Einstellungen" - - ;;contacts - :contacts "Kontakte" - :new-contact "Neuer Kontakt" - :delete-contact "Kontakt löschen" - :delete-contact-confirmation "Dieser Kontakt wird aus deinen Kontakten gelöscht" - :remove-from-group "Aus Gruppe entfernen" - :edit-contacts "Kontakte bearbeiten" - :search-contacts "Kontakte durchsuchen" - :contacts-group-new-chat "Neuen Chat starten" - :choose-from-contacts "Aus Kontakten wählen" - :no-contacts "Noch keine Kontakte" - :show-qr "QR-Code anzeigen" - :enter-address "Andresse eingeben" - :more "mehr" - - ;;group-settings - :remove "Entfernen" - :save "Speichern" - :delete "Löschen" - :clear-history "Verlauf leeren" - :mute-notifications "Benachrichtigungen stummschalten" - :leave-chat "Chat verlassen" - :chat-settings "Chat-Einstellungen" - :edit "Bearbeiten" - :add-members "Mitglieder hinzufügen" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Neue Gruppe" - :reorder-groups "Gruppen neu ordnen" - :edit-group "Gruppe bearbeiten" - :delete-group "Gruppe löschen" - :delete-group-confirmation "Diese Gruppe wird aus deinen Gruppen gelöscht. Deine Kontakte bleiben hiervon unbeeinflusst" - :delete-group-prompt "Deine Kontakte bleiben hiervon unbeeinflusst" - :contact-s {:one "Kontakt" - :other "Kontakte"} - - ;;protocol - :received-invitation "Chat-Einladung erhalten" - :removed-from-chat "hat dich aus dem Gruppenchat entfernt" - :left "hat die Gruppe verlassen" - :invited "eingeladen" - :removed "entfernt" - :You "Du" - - ;;new-contact - :add-new-contact "Neuen Kontakt hinzufügen" - :scan-qr "Scanne QR-Code" - :name "Name" - :address-explication "Dein öffentlicher Schlüssel wird verwendet, um deine Adresse auf Ethereum zu generieren und besteht aus einer Folge von Zahlen und Buchstaben. Du findest ihn leicht in deinem Profil" - :enter-valid-public-key "Bitte gib einen validen öffentlichen Schlüssen ein oder scanne einen QR-Code" - :contact-already-added "Dieser Kontakt wurde bereits hinzugefügt" - :can-not-add-yourself "Du kannst dich nicht selbst hinzufügen" - :unknown-address "Unbekannte Adresse" - - ;;login - :connect "Verbinden" - :address "Adresse" - :password "Passwort" - :sign-in-to-status "Bei Status einloggen" - :sign-in "Einloggen" - :wrong-password "Falsches Passwort" - :enter-password "Passwort eingeben" - - ;;recover - :passphrase "Passphrase" - :recover "Wiederherstellen" - :twelve-words-in-correct-order "12 Wörter in der richtigen Reihenfolge" - - ;;accounts - :recover-access "Zugriff Wiederherstellen" - :create-new-account "Neues Konto erstellen" - - ;;wallet-qr-code - :done "Erledigt" - - ;;validation - :invalid-phone "Ungültige Telefonnummer" - :amount "Betrag" - - ;;transactions - :confirm "Bestätigen" - :transaction "Transaktion" - :unsigned-transaction-expired "Unsignierte Transaktion ist ausgelaufen" - :status "Status" - :recipient "Empfänger" - :to "An" - :from "Von" - :data "Daten" - :got-it "Erhalten" - :block "Block" - :hash "Hash" - :gas-limit "Gaslimit" - :gas-price "Gaspreis" - :gas-used "Gas verbraucht" - :cost-fee "Kosten/Gebühren" - :nonce "Nonce" - :confirmations "Bestätigungen" - :confirmations-helper-text "Bitte warte mindestens 12 Bestätigungen ab, um sicherzustellen, dass deine Transaktion sicher bearbeitet wurde" - :copy-transaction-hash "Transaktions-Hash kopieren" - :open-on-etherscan "Auf Etherscan.io öffnen" - - ;;webview - :web-view-error "Ups, Fehler" - - ;;testfairy warning - :testfairy-title "Warnung!" - :testfairy-message "Du verwendest eine App Version, die aus einem Nightly Build stammt. Zu Testzwecken zeichnet diese Versions Sitzungsdaten auf, solange eine Wlan-Verbindung genutzt wird. Das bedeutet, dass all deine Interaktionen mit der App gespeichert werden (als Video und Logs) und von unseren Entwicklungsteam verwendet werden könnten, um mögliche Probleme zu untersuchen. Gespeicherte Videos/Logs enthalten keine Passwörter. Aufnahmen werden nur erstellt, wenn die App aus einem Nightly Build installiert wurde. Es wird nichts aufgezeichnet, wenn die App aus dem PlayStore oder TestFlight installiert wurde." - - ;; wallet - :wallet "Wallet" - :wallets "Wallets" - :your-wallets "Deine Wallets" - :main-wallet "Haupt-Wallet" - :wallet-error "Fehler beim Laden" - :wallet-send "Senden" - :wallet-request "Anfragen" - :wallet-exchange "Tauschen" - :wallet-assets "Assets" - :wallet-add-asset "Asset hinzufügen" - :wallet-total-value "Gesamter Wert" - :wallet-settings "Wallet-Einstellungen" - :signing-phrase-description "Signiere die Transaktion, indem du dein Passwort eingibst. Stelle sicher, dass die Wörter oben deiner geheimen Signing Phrase entsprechen" - :wallet-insufficient-funds "Unzureichendes Kapital" - :request-transaction "Transaktion anfordern" - :send-request "Anfrage senden" - :share "Teilen" - :eth "ETH" - :currency "Währung" - :usd-currency "USD" - :transactions "Transaktionen" - :transaction-details "Transaktionsdetails" - :transaction-failed "Transaktion fehlgeschlagen" - :transactions-sign "Signiere" - :transactions-sign-all "Signiere alle" - :transactions-sign-transaction "Signiere Transaktion" - :transactions-sign-later "Später signieren" - :transactions-delete "Transaktion löschen" - :transactions-delete-content "Transaktion wird von der 'Unsigniert' Liste entfernt" - :transactions-history "Verlauf" - :transactions-unsigned "Unsigniert" - :transactions-history-empty "Noch keine Transaktionen in deinem Verlauf" - :transactions-unsigned-empty "Du hast keine unsignierten Transaktionen" - :transactions-filter-title "Filterverlauf" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Typ" - :transactions-filter-select-all "Alle auswählen" - :view-transaction-details "Transaktionsdetails anzeigen" - :transaction-description "Bitte warte mindestens 12 Bestätigungen ab, um sicherzustellen, dass deine Transaktion sicher bearbeitet wurde" - :transaction-sent "Transaktion gesendet" - :transaction-moved-text "Die Transaktion wird für die nächsten 5 Minuten in der 'Unsigniert' Liste bleiben" - :transaction-moved-title "Transaktion verschoben" - :sign-later-title "Transaktion später signieren?" - :sign-later-text "Gehe in den Transaktionsverlauf, um diese Transaktion zu signieren." - :not-applicable "Nicht anwendbar für unsignierte Transaktionen" - - ;; Wallet Send - :wallet-choose-recipient "Empfänger auswählen" - :wallet-choose-from-contacts "Aus Kontakten auswählen" - :wallet-address-from-clipboard "Adresse aus Zwischenablage verwenden" - :wallet-invalid-address "Ungültige Adresse: \n {{data}}" - :wallet-browse-photos "Fotos durchsuchen" - :validation-amount-invalid-number "Menge ist keine gültige Zahl" - :validation-amount-is-too-precise "Menge ist zu präzise. Die kleinste Einheit, die du senden kannst, ist 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Neues Netzwerk" - :add-network "Netzwerk hinzufügen" - :add-new-network "Neues Netzwerk hinzufügen" - :existing-networks "Existierende Netzwerke" - :add-json-file "JSON Datei hinzufügen" - :paste-json-as-text "JSON als Text einfügen" - :paste-json "JSON einfügen" - :specify-rpc-url "Spezifiziere eine RPC URL" - :edit-network-config "Netzwerkeinstellungen bearbeiten" - :connected "Verbunden" - :process-json "JSON verarbeiten" - :error-processing-json "Fehler beim Verarbeiten von JSON" - :rpc-url "RPC URL" - :remove-network "Netzwerk entfernen" - :network-settings "Netzwerkeinstellungen" - :edit-network-warning "Sei vorsichtig, das Bearbeiten der Netzwerkinformationen kann das Netzwerk für dich deaktivieren" - :connecting-requires-login "Das Verbinden mit einem anderen Netzwerk erfordert eine Anmeldung" - :close-app-title "Warnung!" - :close-app-content "Die App wird gestoppt und geschlossen. Wenn du sie wieder öffnest, wird das ausgewählte Netzwerk verwendet" - :close-app-button "Bestätigen"}) diff --git a/src/status_im/translations/de_ch.cljs b/src/status_im/translations/de_ch.cljs deleted file mode 100644 index 9885948e57c..00000000000 --- a/src/status_im/translations/de_ch.cljs +++ /dev/null @@ -1,161 +0,0 @@ -(ns status-im.translations.de-ch) - -(def translations - {;;common - :members-title "Mitglieder" - :not-implemented "!nicht implementiert" - :chat-name "Chat Name" - :notifications-title "Notifikationen and Klänge" - :offline "Offline" - - ;;drawer - :switch-users "Benutzer wechseln" - - ;;chat - :is-typing "tippt" - :and-you "und du" - :search-chat "Suche Chat" - :members {:one "1 Mitglied" - :other "{{count}} Mitglieder" - :zero "Keine Mitglieder"} - :members-active {:one "1 Mitglied, 1 aktiv" - :other "{{count}} Mitglieder, {{count}} aktiv" - :zero "Keine Mitglieder"} - :active-online "Online" - :active-unknown "Unbekannt" - :available "Verfügbar" - :no-messages "Keine Nachrichten" - :suggestions-requests "Fragt an" - :suggestions-commands "Kommandiert" - - ;;sync - :sync-in-progress "Synchronisierung..." - :sync-synced "Synchron" - - ;;messages - :status-sending "Senden" - :status-pending "Anhängig" - :status-sent "Gesendet" - :status-seen-by-everyone "Von allen gesehen" - :status-seen "Gesehen" - :status-delivered "Geliefert" - :status-failed "Fehlgeschlagen" - - ;;datetime - :datetime-second {:one "Sekunde" - :other "Sekunden"} - :datetime-minute {:one "Minute" - :other "Minuten"} - :datetime-hour {:one "Stunde" - :other "Stunden"} - :datetime-day {:one "Tag" - :other "Tage"} - :datetime-ago "vor" - :datetime-yesterday "gestern" - :datetime-today "heute" - - ;;profile - :profile "Profil" - :message "Nachricht" - :not-specified "Nicht spezifiziert" - :public-key "Öffentlicher Schlüssel" - :phone-number "Telefonnummer" - :add-to-contacts "Zu Kontakten hinzufügen" - - ;;make_photo - :image-source-title "Profilbild" - :image-source-make-photo "Erfassen" - :image-source-gallery "Aus der Galerie auswählen" - - ;;sign-up - :contacts-syncronized "Deine Kontakte wurden synchronisiert" - :confirmation-code (str "Vielen Dank! Wir haben Dir eine SMS mit einem Bestätigungcode" - "zugesandt. Bitte gebe diesen Code zur Bestätigung deiner Telefonnummer ein") - :incorrect-code (str "Leider ist der Code nicht korrekt. Bitte versuche es nochmals") - :phew-here-is-your-passphrase "*Phew* das war hart, hier ist deine Passphrase, *Schreibe diese auf und bewahre diese sicher auf!* Du benötigst diese, um dein Konto wiederherzustellen." - :here-is-your-passphrase "Hier ist deine Passphrase, *Schreibe diese auf und bewahre diese sicher auf!* Du benötigst diese, um dein Konto wiederherzustellen." - :phone-number-required "Klicke hier, um deine Telefonnummer einzugeben & Ich finde deine Freunde für dich" - :intro-status "Chat mit mir, damit ich dein Konto einrichten und deine Einstellungen ändern kann!" - :intro-message1 "Willkommen zu Status\nTap diese Nachricht um dein Passwort einzurichten & loszulegen!" - :account-generation-message "Gib mir eine Sekunde, Ich muss wie verrückt etwas berechnen um dein Konto zu generieren!" - - ;;chats - :chats "Chats" - :new-group-chat "Neuer Gruppenchat" - - ;;discover - :discover "Entdecken" - :none "Keine" - :search-tags "Gebe hier deine Suchbegriffe ein" - :popular-tags "Beliebte Suchbegriffe" - :recent "Kürzlich" - :no-statuses-discovered "Es wurden keine Status gefunden" - - ;;settings - :settings "Einstellungen" - - ;;contacts - :contacts "Kontakte" - :new-contact "Neuer Kontakt" - :contacts-group-new-chat "Starte neuen Gruppenchat" - :no-contacts "Noch keine Kontakte" - :show-qr "QR-Code anzeigen" - - ;;group-settings - :remove "Entfernen" - :save "Speichern" - :clear-history "Verlauf löschen" - :chat-settings "Chat Einstellungen" - :edit "Bearbeiten" - :add-members "Mitglieder hinzufügen" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "Chat Einladung erhalten" - :removed-from-chat "du wurdest vom Gruppenchat entfernt" - :left "verlassen" - :invited "eingeladen" - :removed "entfernt" - :You "Du" - - ;;new-contact - :add-new-contact "Teilnehmer hinzufügen" - :scan-qr "QR scannen" - :name "Name" - :address-explication "Vielleicht sollte hier ein Text stehen der erklärt, was eine Adresse ist und wo man suchen soll" - :contact-already-added "Der Kontakt wurde bereits hinzugefügt" - :can-not-add-yourself "Du kannst dich nicht selbst hinzufügen" - :unknown-address "Unbekannte Adresse" - - ;;login - :connect "Verbinden" - :address "Adresse" - :password "Passwort" - :wrong-password "Falsches Passwort" - - ;;recover - :passphrase "Passphrase" - :recover "Wiederherstellen" - - ;;accounts - :recover-access "Zugriff wiederherstellen" - - ;;wallet-qr-code - :done "Erledigt" - :main-wallet "Haupt-Portemonnaie" - - ;;validation - :invalid-phone "Ungültige Telefonnummer" - :amount "Betrag" - ;;transactions - :status "Status" - :recipient "Empfänger" - - ;;:webview - :web-view-error "oops, Fehler"}) diff --git a/src/status_im/translations/el.cljs b/src/status_im/translations/el.cljs deleted file mode 100644 index ebf783e0fcc..00000000000 --- a/src/status_im/translations/el.cljs +++ /dev/null @@ -1,375 +0,0 @@ -(ns status-im.translations.el) - -(def translations - {;;common - :members-title "Μέλη" - :not-implemented "!μη υλοποιημένο" - :chat-name "Όνομα συνομιλίας" - :notifications-title "Ειδοποιήσεις και ήχοι" - :offline "Εκτός σύνδεσης" - :search-for "Αναζήτηση για..." - :cancel "Ακύρωση" - :next "Επόμενο" - :open "Άνοιγμα" - :description "Περιγραφή" - :url "URL" - :type-a-message "Πληκτρολογήστε ένα μήνυμα..." - :type-a-command "Αρχίστε να πληκτρολογείτε μια εντολή..." - :error "Σφάλμα" - :unknown-status-go-error "Άγνωστο status-go σφάλμα" - :node-unavailable "Κανένας κόμβος ethereum σε λειτουργία" - :yes "Ναι" - :no "Όχι" - - :camera-access-error "Για να χορηγήσετε την απαιτούμενη άδεια κάμερας, παρακαλώ μεταβείτε στις ρυθμίσεις του συστήματός σας και σιγουρευτείτε ότι το Status > Κάμερα είναι επιλεγμένο." - :photos-access-error "Για να χορηγήσετε την απαιτούμενη άδεια φωτογραφιών, παρακαλώ μεταβείτε στις ρυθμίσεις του συστήματός σας και σιγουρευτείτε ότι το Status > Φωτογραφίες είναι επιλεγμένο." - - ;;drawer - :switch-users "Αλλαγή χρηστών" - :logout "Αποσύνδεση" - :current-network "Τρέχον δίκτυο" - - ;;home - :home "Αρχική" - - ;;chat - :is-typing "πληκτρολογεί" - :and-you "και εσείς" - :search-chat "Αναζήτηση συνομιλίας" - :members {:one "1 μέλος" - :other "{{count}} μέλη" - :zero "κανένα μέλος"} - :members-active {:one "1 μέλος" - :other "{{count}} μέλη" - :zero "κανένα μέλος"} - :public-group-status "Δημόσιο" - :active-online "Σε σύνδεση" - :active-unknown "Άγνωστος" - :available "Διαθέσιμος" - :no-messages "Κανένα μήνυμα" - :suggestions-requests "Αιτήσεις" - :suggestions-commands "Εντολές" - :faucet-success "Το αίτημα faucet έχει παραληφθεί" - :faucet-error "Το αίτημα faucet έχει σφάλμα" - - ;;sync - :sync-in-progress "Συγχρονισμός..." - :sync-synced "Σε συγχρονισμό" - - ;;messages - :status-sending "Αποστολή..." - :status-pending "Εκκρεμεί" - :status-sent "Απεσταλμένα" - :status-seen-by-everyone "Το έχουν δει όλοι" - :status-seen "Το είδε" - :status-delivered "Παραδόθηκε" - :status-failed "Απέτυχε" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "δευτερόλεπτο" - :other "δευτερόλεπτα"} - :datetime-minute {:one "λεπτό" - :other "λεπτά"} - :datetime-hour {:one "ώρα" - :other "ώρες"} - :datetime-day {:one "μέρα" - :other " μέρες"} - :datetime-ago "πριν" - :datetime-yesterday "χτες" - :datetime-today "σήμερα" - - ;;profile - :profile "Προφίλ" - :edit-profile "Επεξεργασία προφίλ" - :message "Μήνυμα" - :not-specified "Δεν διευκρινίζεται" - :public-key "Δημόσιο κλειδί" - :phone-number "Αριθμός τηλεφώνου" - :update-status "Ενημέρωση της κατάστασής σας..." - :add-a-status "Προσθέστε μια κατάσταση..." - :status-prompt "Ορίστε την κατάστασή σας. Χρησιμοποιώντας #hastags θα βοηθήσετε τους άλλους να σας ανακαλύψουν και να μιλήσετε σχετικά με το τι έχετε στο μυαλό σας" - :add-to-contacts "Προσθήκη στις επαφές" - :in-contacts "Στις επαφές" - :remove-from-contacts "Κατάργηση από τις επαφές" - :start-conversation "Έναρξη συνομιλίας" - :testnet-text "Βρίσκεστε στο {{testnet}} Testnet. Μην στέλνετε πραγματικό ETH ή SNT στη διεύθυνσή σας" - :mainnet-text "Βρίσκεστε στο Mainnet. Θα στείλετε πραγματικό ETH" - - ;;make_photo - :image-source-title "Εικόνα προφίλ" - :image-source-make-photo "Λήψη" - :image-source-gallery "Επιλέξτε από τη συλλογή" - - ;;sharing - :sharing-copy-to-clipboard "Αντιγραφή στο πρόχειρο" - :sharing-share "Κοινοποίηση..." - :sharing-cancel "Ακύρωση" - - :browsing-title "Περιηγηθείτε" - :browsing-open-in-web-browser "Ανοίξτε στο πρόγραμμα περιήγησης ιστού" - :browsing-cancel "Ακύρωση" - - ;;sign-up - :contacts-syncronized "Οι επαφές σας έχουν συγχρονιστεί" - :confirmation-code (str "Ευχαριστούμε! Σας έχουμε στείλει ένα μήνυμα κειμένου με έναν κωδικό επιβεβαίωσης. " - "Παρακαλούμε καταχωρήστε αυτόν τον κωδικό για να επιβεβαιώσετε τον αριθμό τηλεφώνου σας") - :incorrect-code (str "Δυστυχώς ο κωδικός ήταν λανθασμένος, παρακαλούμε εισάγετέ τον ξανά") - :phew-here-is-your-passphrase "Πωπω, αυτό ήταν δύσκολο. Αυτό είναι το συνθηματικό σας, *γράψτε το και κρατήστε το ασφαλές!* Θα χρειαστεί για ανάκτηση του λογαριασμού σας." - :here-is-your-passphrase "Αυτό είναι το συνθηματικό σας, *γράψτε το και κρατήστε το ασφαλές!* Θα χρειαστεί για ανάκτηση του λογαριασμού σας." - :here-is-your-signing-phrase "Εδώ είναι η φράση επικύρωσής σας. Θα τη χρησιμοποιήσετε για να επαληθεύσετε τις συναλλαγές σας. *Γράψτε τη και κρατήστε την ασφαλές!*" - :phone-number-required "Αγγίξτε εδώ για να επικυρώσετε τον αριθμό τηλεφώνου σας & θα βρω τους φίλους σας." - :shake-your-phone "Βρήκατε ένα σφάλμα ή έχετε μια πρόταση; Απλά ~κουνήστε~ το τηλέφωνό σας!" - :intro-status "Συζητήστε μαζί μου για να ρυθμίσετε το λογαριασμό σας και να αλλάξετε τις ρυθμίσεις σας." - :intro-message1 "Καλωσήρθες στο Status!\nΑγγίξτε αυτό το μήνυμα για να ορίσετε τον κωδικό πρόσβασής σας και να ξεκινήσετε." - :account-generation-message "Δώστε μου ένα δευτερόλεπτο, πρέπει να κάνω κάποιους τρελούς μαθηματικούς υπολογισμούς για να δημιουργήσω το λογαριασμό σας!" - :move-to-internal-failure-message "Πρέπει να μεταφέρουμε κάποια σημαντικά αρχεία από την εξωτερική μνήμη στην εσωτερική μνήμη. Για να γίνει αυτό, χρειαζόμαστε την άδειά σας. Δεν θα χρησιμοποιούμε την εξωτερική μνήμη σε μελλοντικές εκδόσεις." - :debug-enabled "Ο debug server έχει ξεκινήσει! Τώρα μπορείτε να εκτελέσετε *status-dev-cli scan* για να βρείτε τον server από τον υπολογιστή σας στο ίδιο δίκτυο." - - ;;phone types - :phone-e164 "Διεθνές 1" - :phone-international "Διεθνές 2" - :phone-national "Εθνικό" - :phone-significant "Σημαντικό" - - ;;chats - :chats "Συνομιλίες" - :delete-chat "Διαγραφή συνομιλίας" - :new-group-chat "Νέα ομαδική συνομιλία" - :new-public-group-chat "Συμμετοχή σε δημόσια συνομιλία" - :edit-chats "Επεξεργασία συνομιλίας" - :search-chats "Αναζήτηση συνομιλιών" - :empty-topic "Κενό θέμα" - :topic-format "Λάθος μορφή [a-z0-9\\-]+" - :public-group-topic "Θέμα" - - ;;discover - :discover "Ανακαλύψτε" - :none "Κανένα" - :search-tags "Πληκτρολογήστε εδώ τις ετικέτες αναζήτησης" - :popular-tags "Δημοφιλή #hashtags" - :recent "Πρόσφατες καταστάσεις" - :no-statuses-found "Δεν βρέθηκαν καταστάσεις" - :chat "Συνομιλία" - :all "Όλα" - :public-chats "Δημόσιες συνομιλίες" - :soon "Σύντομα" - :public-chat-user-count "{{count}} άνθρωποι" - :dapps "ÐApps" - :dapp-profile "ÐApp προφίλ" - :no-statuses-discovered "Δεν βρέθηκαν καταστάσεις" - :no-statuses-discovered-body "Όταν κάποιος δημοσιεύει\nμια κατάσταση θα την δείτε εδώ." - :no-hashtags-discovered-title "Δε βρέθηκαν #hashtags" - :no-hashtags-discovered-body "Όταν κάποιο #hashtag γίνεται\nδημοφιλές θα το δείτε εδώ." - - ;;settings - :settings "Ρυθμίσεις" - - ;;contacts - :contacts "Επαφές" - :new-contact "Νέα επαφή" - :delete-contact "Διαγραφή επαφής" - :delete-contact-confirmation "Αυτή η επαφή θα καταργηθεί από τις επαφές σας" - :remove-from-group "Κατάργηση από την ομάδα" - :edit-contacts "Επεξεργασία επαφών" - :search-contacts "Αναζήτηση επαφών" - :contacts-group-new-chat "Ξεκινήστε νέα συνομιλία" - :choose-from-contacts "Επιλέξτε από τις επαφές" - :no-contacts "Δεν υπάρχουν επαφές" - :show-qr "Εμφάνιση κωδικού QR" - :enter-address "Εισάγετε τη διεύθυνση" - :more "περισσότερα" - - ;;group-settings - :remove "Αφαιρέστε" - :save "Αποθηκεύστε" - :delete "Διαγράψτε" - :clear-history "Καθαρισμός ιστορικού" - :mute-notifications "Σίγαση ειδοποιήσεων" - :leave-chat "Φύγετε από τη συνομιλία" - :chat-settings "Ρυθμίσεις συνομιλίας" - :edit "Επεξεργασία" - :add-members "Προσθήκη μελών" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Νέα ομάδα" - :reorder-groups "Επαναδιαρθρώστε τις ομάδες" - :edit-group "Επεξεργασία ομάδας" - :delete-group "Διαγραφή ομάδας" - :delete-group-confirmation "Αυτή η ομάδα θα καταργηθεί από τις ομάδες σας. Αυτό δε θα επηρεάσει τις επαφές σας" - :delete-group-prompt "Αυτό δε θα επηρεάσει τις επαφές σας" - :contact-s {:one "επαφή" - :other "επαφές"} - - ;;protocol - :received-invitation "έλαβες πρόσκληση συνομιλίας" - :removed-from-chat "αφαιρέθηκες από την ομαδική συζήτηση" - :left "έφυγες" - :invited "προσκλήθηκες" - :removed "αφαιρέθηκες" - :You "Εσύ" - - ;;new-contact - :add-new-contact "Προσθήκη νέας επαφής" - :scan-qr "Σάρωση κωδικού QR" - :name "Όνομα" - :address-explication "Το δημόσιο κλειδί σας χρησιμοποιείται για τη δημιουργία της διεύθυνσής σας στο Ethereum και είναι μια σειρά αριθμών και γραμμάτων. Μπορείτε να το βρείτε εύκολα στο προφίλ σας" - :enter-valid-public-key "Παρακαλώ εισάγετε ένα έγκυρο δημόσιο κλειδί ή σαρώσετε έναν κωδικό QR" - :contact-already-added "Η επαφή έχει ήδη προστεθεί" - :can-not-add-yourself "Δεν μπορείτε να προσθέσετε τον εαυτό σας" - :unknown-address "Άγνωστη διεύθυνση" - - ;;login - :connect "Σύνδεση" - :address "Διεύθυνση" - :password "Κωδικός πρόσβασης" - :sign-in-to-status "Συνδεθείτε στο Status" - :sign-in "Συνδεθείτε" - :wrong-password "Λάθος κωδικός πρόσβασης" - :enter-password "Εισάγετε κωδικός πρόσβασης" - - ;;recover - :passphrase "Συνθηματικό" - :recover "Ανάκτηση" - :twelve-words-in-correct-order "12 λέξεις σε σωστή σειρά" - - ;;accounts - :recover-access "Ανάκτηση πρόσβασης" - :create-new-account "Δημιουργία νέου λογαριασμού" - - ;;wallet-qr-code - :done "Ολοκληρώθηκε" - - ;;validation - :invalid-phone "Μη έγκυρος αριθμός τηλεφώνου" - :amount "Ποσό" - - ;;transactions - :confirm "Επιβεβαιώνω" - :transaction "Συναλλαγή" - :unsigned-transaction-expired "Η μη επικυρωμένη συναλλαγή έληξε" - :status "Κατάσταση" - :recipient "Παραλήπτης" - :to "Προς" - :from "Από" - :data "Δεδομένα" - :got-it "Το κατάλαβα" - :block "Block" - :hash "Hash" - :gas-limit "όριο Gas" - :gas-price "τιμή Gas" - :gas-used "χρησιμοποιημένο Gas" - :cost-fee "Κόστος/Χρέωση" - :nonce "Nonce" - :confirmations "Επιβεβαιώσεις" - :confirmations-helper-text "Παρακαλώ περιμένετε τουλάχιστον 12 επιβεβαιώσεις για να βεβαιωθείτε ότι η συναλλαγή σας γίνεται με ασφάλεια" - :copy-transaction-hash "Αντιγραφή συναλλαγής hash" - :open-on-etherscan "Άνοιγμα στο Etherscan.io" - :incoming "Εισερχόμενη" - :outgoing "Εξερχόμενη" - :pending "Εκκρεμής" - :postponed "Αναβλήθηκε" - - ;;webview - :web-view-error "ωπ, σφάλμα" - - ;;testfairy warning - :testfairy-title "Προειδοποίηση!" - :testfairy-message "Χρησιμοποιείτε μια εφαρμογή εγκατεστημένη από ένα νυχτερινό build. Για πειραματικούς σκοπούς αυτό το build περιλαμβάνει καταγραφή της συνεδρίας σας, εφόσον χρησιμοποιείτε wifi. Έτσι αποθηκεύονται όλες οι αλληλεπιδράσεις σας με αυτήν την εφαρμογή (όπως βίντεο και logs) και θα μπορούσαν να χρησιμοποιηθούν από την ομάδα ανάπτυξής μας για τη διερεύνηση πιθανών προβλημάτων. Τα αποθηκευμένα βίντεο/logs δεν συμπεριλαμβάνουν τους κωδικούς πρόσβασής σας. Η καταγραφή γίνεται μόνο αν η εφαρμογή είναι εγκατεστημένη από ένα νυχτερινό build. Δεν καταγράφεται τίποτα εφόσον η εφαρμογή είναι εγκατεστημένη μέσω του PlayStore ή του TestFlight." - - ;; wallet - :wallet "Πορτοφόλι" - :wallets "Πορτοφόλια" - :your-wallets "Τα πορτοφόλια σας" - :main-wallet "Κύριο Πορτοφόλι" - :wallet-error "Σφάλμα κατά τη φόρτωση δεδομένων" - :wallet-send "Στείλτε" - :wallet-send-token "Στείλτε {{symbol}}" - :wallet-request "Αίτημα" - :wallet-exchange "Αντάλλαγμα" - :wallet-asset "Ενεργητικό" - :wallet-assets "Ενεργητικό" - :wallet-add-asset "Προσθήκη ενεργητικού" - :wallet-total-value "Συνολική αξία" - :wallet-settings "Ρυθμίσεις πορτοφολιού" - :wallet-manage-assets "Διαχείριση ενεργητικού" - :signing-phrase-description "Επικυρώστε τη συναλλαγή εισάγοντας το συνθηματικό σας. Βεβαιωθείτε ότι οι παραπάνω λέξεις ταιριάζουν με τη μυστική φράση επικύρωσης που έχετε" - :wallet-insufficient-funds "Ανεπαρκείς πόροι" - :request-transaction "Αίτηση συναλλαγής" - :send-request "Στείλε αίτημα" - :share "Κοινοποίηση" - :eth "ETH" - :currency "Νόμισμα" - :usd-currency "USD" - :amount-placeholder "Καθορίστε το ποσό" - :transactions "Συναλλαγές" - :transaction-details "Λεπτομέρειες συναλλαγής" - :transaction-failed "Αποτυχία συναλλαγής" - :transactions-sign "Επικύρωση" - :transactions-sign-all "Επικύρωση όλων" - :transactions-sign-transaction "Επικύρωση συναλλαγής" - :transactions-sign-later "Επικύρωση αργότερα" - :transactions-delete "Διαγραφή συναλλαγής" - :transactions-delete-content "Η συναλλαγή θα αφαιρεθεί από 'Ανυπόγραφη' λίστα" - :transactions-history "Ιστορικό" - :transactions-unsigned "Μη επικυρωμένες" - :transactions-history-empty "Δεν υπάρχουν ακόμη συναλλαγές στο ιστορικό σας" - :transactions-unsigned-empty "Δεν έχετε καμία μη επικυρωμένη συναλλαγή" - :transactions-filter-title "Φίλτρο ιστορικού" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Τύπος" - :transactions-filter-select-all "Επιλογή όλων" - :view-transaction-details "Προβολή λεπτομερειών συναλλαγής" - :transaction-description "Περιμένετε τουλάχιστον 12 επιβεβαιώσεις για να βεβαιωθείτε ότι η συναλλαγή σας γίνεται με ασφάλεια" - :transaction-sent "Η συναλλαγή έχει σταλεί" - :transaction-moved-text "Η συναλλαγή θα παραμείνει στη λίστα των 'Μη επικυρωμένων' για τα επόμενα 5 λεπτά" - :transaction-moved-title "Η συναλλαγή μετακινήθηκε" - :sign-later-title "Επικυρώστε τη συναλλαγή αργότερα;" - :sign-later-text "Ελέγξτε το ιστορικό συναλλαγών για να επικυρώσετε αυτήν τη συναλλαγή" - :not-applicable "Δεν ισχύει για συναλλαγές που δεν έχουν υπογραφεί" - :send-transaction "Αποστολή συναλλαγής" - :receive-transaction "Λήψη συναλλαγής" - :transaction-history "Ιστορικό συναλλαγών" - - ;; Wallet Send - :wallet-choose-recipient "Επιλογή παραλήπτη" - :wallet-choose-from-contacts "Επιλογή από Επαφές" - :wallet-address-from-clipboard "Χρησιμοποιήστε τη διεύθυνση από το πρόχειρο" - :wallet-invalid-address "Μη έγκυρη διεύθυνση: \n {{data}}" - :wallet-invalid-chain-id "Το δίκτυο δεν ταιριάζει: \n {{data}} αλλά η τρέχουσα αλυσίδα είναι {{chain}}" - :wallet-browse-photos "Περιηγηθείτε στις φωτογραφίες" - :wallet-advanced "Προχωρημένη" - :wallet-transaction-fee "Χρέωση Συναλλαγής" - :wallet-transaction-fee-details "Το όριο gas είναι το ποσό του gas που θα αποσταλεί με τη συναλλαγή σας. Η αύξηση αυτού του αριθμού δε θα οδηγήσει σε ταχύτερη επεξεργασία της συναλλαγής σας" - :wallet-transaction-total-fee "Σύνολική Χρέωση" - :validation-amount-invalid-number "Το ποσό δεν είναι έγκυρος αριθμός" - :validation-amount-is-too-precise "Το ποσό είναι πολύ ακριβές. Η μικρότερη μονάδα που μπορείτε να στείλετε είναι 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Νέο δίκτυο" - :add-network "Προσθήκη δικτύου" - :add-new-network "Προσθήκη νέου δικτύου" - :add-wnode "Προσθήκη mailserver" - :existing-networks "Υπάρχοντα δίκτυα" - ;; TODO(dmitryn): come up with better description/naming. Suggested namings: Mailbox and Master Node - :existing-wnodes "Υπάρχοντες mailservers" - :add-json-file "Προσθήκη ενός JSON αρχείου" - :paste-json-as-text "Επικόλληση JSON ως κείμενο" - :paste-json "Επικόλληση JSON" - :specify-rpc-url "Καθορίστε ένα RPC URL" - :edit-network-config "Επεξεργασία ρυθμίσεων δικτύου" - :connected "Συνδεδεμένος" - :process-json "Επεξεργασία JSON" - :error-processing-json "Σφάλμα επεξεργασίας JSON" - :rpc-url "RPC URL" - :remove-network "Αφαίρεση δικτύου" - :network-settings "Ρυθμίσεις δικτύου" - :offline-messaging-settings "Ρυθμίσεις μηνυμάτων εκτός σύνδεσης" - :edit-network-warning "Προσοχή, η επεξεργασία των δεδομένων δικτύου ενδέχεται να απενεργοποιήσει αυτό το δίκτυο για εσάς" - :connecting-requires-login "Η σύνδεση σε άλλο δίκτυο απαιτεί είσοδο" - :close-app-title "Προειδοποίηση!" - :close-app-content "Η εφαρμογή θα κλείσει. Όταν την ξανανοίξετε, το επιλεγμένο δίκτυο θα χρησιμοποιηθεί" - :close-app-button "Επιβεβαιώστε"}) diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs deleted file mode 100644 index e7e473bacfc..00000000000 --- a/src/status_im/translations/en.cljs +++ /dev/null @@ -1,707 +0,0 @@ -(ns status-im.translations.en) - -(def translations - {;;common - :members-title "Members" - :not-implemented "!not implemented" - :chat-name "Chat name" - :notifications-title "Notifications and sounds" - :offline "Offline" - :disconnected "Connecting to peers..." - :connection-problem "Messages connection problem" - :mailserver-reconnect "Could not connect to mailserver. Tap to reconnect" - :fetching-messages "Fetching messages..." - :search-for "Search for..." - :cancel "Cancel" - :next "Next" - :open "Open" - :description "Description" - :enter-url "Enter URL" - :open-dapp "Open ÐApp" - :url "URL" - :type-a-message "Type a message..." - :type-a-command "Start typing a command..." - :error "Error" - :unknown-status-go-error "Unknown status-go error" - :node-unavailable "No ethereum node running" - :add "Add" - :yes "Yes" - :no "No" - :on "On" - :off "Off" - :mailserver-connection-error "Could not connect to mailserver" - :dont-allow "Don't Allow" - :custom "Custom" - - :camera-access-error "To grant the required camera permission, please go to your system settings and make sure that Status > Camera is selected." - :photos-access-error "To grant the required photos permission, please go to your system settings and make sure that Status > Photos is selected." - - :status-console "Status Console" - - ;;sign in - :intro-text "Status is your gateway to the decentralized web" - :intro-text-description "A community-powered platform to securely chat and transact on the Ethereum blockchain" - :create-account "Create account" - :already-have-account "I already have an account" - :creating-your-account "Creating your account..." - :password-placeholder "At least 6 characters" - :password-placeholder2 "Confirm your password" - :name-placeholder "Display name" - :password_error1 "Passwords don't match." - :password-description "You'll need this password to open the app and confirm transactions." - :name-description "Change this anytime in your Profile." - :other-accounts "Other accounts" - :sign-you-in "Signing you in…" - - ;;drawer - :switch-users "Switch users" - :logout-title "Log out?" - :logout-are-you-sure "Are you sure you want\nto log out?" - :logout "Log out" - :current-network "Current network" - - ;;home - :home "Home" - :no-recent-chats "There are no recent Chats or DApps here yet. Tap the “Plus” button to see the list of Dapps or discover people to chat with." - :welcome-to-status "Welcome to Status" - :welcome-to-status-description "Tap the plus (+) button to get started" - - ;;chat - :is-typing "is typing" - :and-you "and you" - :search-chat "Search chat" - :members {:one "1 member" - :other "{{count}} members" - :zero "no members"} - :members-active {:one "1 member" - :other "{{count}} members" - :zero "no members"} - :public-group-status "Public" - :active-online "Online" - :active-unknown "Unknown" - :available "Available" - :no-messages "No messages" - :no-messages-yet "No messages yet" - :suggestions-requests "Requests" - :suggestions-commands "Commands" - :faucet-success "Faucet request has been received" - :faucet-error "Faucet request error" - :network-mismatch "Network mismatch" - - ;;sync - :sync-in-progress "Syncing..." - :sync-synced "In sync" - - ;;messages - :status-sending "Sending..." - :status-pending "Pending" - :status-confirmed "Confirmed" - :status-sent "Sent" - :status-tx-not-found "TX not found" - :status-seen-by-everyone "Seen by everyone" - :status-seen "Seen" - :status-delivered "Delivered" - :status-failed "Failed" - :status-not-sent "Not sent. Tap for options" - :message-not-sent "Message not sent" - :resend-message "Resend" - :delete-message "Delete message" - :sent-at "Sent at" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "second" - :other "seconds"} - :datetime-minute {:one "minute" - :other "minutes"} - :datetime-hour {:one "hour" - :other "hours"} - :datetime-day {:one "day" - :other "days"} - :datetime-ago "ago" - :datetime-yesterday "yesterday" - :datetime-today "today" - - ;;profile - :profile "Profile" - :view-profile "View profile" - :edit-profile "Edit profile" - :main-currency "Main currency" - :message "Message" - :notifications "Notifications" - :need-help "Need help?" - :help-center "Help Center" - :faq "Frequently asked questions" - :submit-bug "Submit a bug" - :request-feature "Request a feature" - :not-specified "Not specified" - :public-key "Public key" - :phone-number "Phone number" - :share-contact-code "Share my contact code" - :update-status "Update your status..." - :add-a-status "Add a status..." - :status-prompt "Set your status. Using #hastags will help others discover you and talk about what's on your mind" - :contact-code "Contact code" - :add-to-contacts "Add to contacts" - :in-contacts "In contacts" - :remove-from-contacts "Remove from contacts" - :start-conversation "Start conversation" - :send-message "Send message" - :testnet-text "You’re on the {{testnet}} Testnet. Do not send real ETH or SNT to your address" - :mainnet-text "You’re on the Mainnet. Real ETH will be sent" - :dev-mode "Development mode" - :backup-your-recovery-phrase "Backup your recovery phrase" - - ;;recovery phrase - :your-data-belongs-to-you "If you lose your recovery phrase you lose your data and funds" - :your-data-belongs-to-you-description "Status can’t help you recover your account if you lose your recovery phrase. You are in charge of the security of your data, and backing up your recovery phrase is the best safeguard." - :ok-continue "Ok, continue" - :your-recovery-phrase "Your recovery phrase" - :your-recovery-phrase-description "This is your recovery phrase. You use it to prove that this is your wallet. You only get to see it once! Write it on paper and keep it in a secure place. You will need it if you lose or reinstall your wallet." - :enter-word "Enter word" - :check-your-recovery-phrase "Check your recovery phrase" - :wrong-word "Wrong word" - :are-you-sure? "Are you sure?" - :are-you-sure-description "You will not be able to see the whole recovery phrase again" - :you-are-all-set "You’re all set!" - :you-are-all-set-description "Now if you lose your phone you can restore your account and wallet using the recovery phrase." - :ok-got-it "Ok, got it" - :backup-recovery-phrase "Backup recovery phrase" - :step-i-of-n "Step {{step}} of {{number}}" - :word-n-description "In order to check if you have backed up your recovery phrase correctly, enter the word #{{number}} above." - :word-n "Word #{{number}}" - - :wallet-onboarding-title "Simple and secure cryptocurrency wallet" - :wallet-onboarding-description "Send, receive, and store your cryptocurrency with the Status Wallet" - :wallet-onboarding-set-up "Let’s get set up" - :wallet-set-up-title "Set up your wallet" - :wallet-set-up-signing-phrase "This is your personal transaction phrase that you’ll use everytime you make a transaction. Make sure to write it down on a piece of paper, store it somewhere, and only confirm transactions when you see these three words." - :wallet-set-up-confirm-title "Wrote it down?" - :wallet-set-up-confirm-description "You won’t be able to see your 3-word transaction phrase again after this." - :wallet-backup-recovery-title "Backup your recovery phrase" - :wallet-backup-recovery-description "This will help you to keep your assets safe" - - ;;make_photo - :image-source-title "Edit picture" - :image-source-make-photo "Capture" - :image-source-gallery "Select from gallery" - - ;;sharing - :sharing-copy-to-clipboard "Copy to clipboard" - :sharing-copied-to-clipboard "Copied to clipboard" - :sharing-share "Share..." - :sharing-cancel "Cancel" - - :browsing-title "Browse" - :browsing-open-in-web-browser "Open in web browser" - :browsing-open-in-status "Open in Status" - :browsing-cancel "Cancel" - - ;;sign-up - :contacts-syncronized "Your contacts have been synchronized" - :confirmation-code (str "Thanks! We've sent you a text message with a confirmation " - "code. Please provide that code to confirm your phone number") - :incorrect-code (str "Sorry the code was incorrect, please enter it again") - :phew-here-is-your-passphrase "Phew, that was hard. Here is your passphrase, *write this down and keep it safe!* You will need it to recover your account." - :here-is-your-passphrase "Here is your passphrase, *write this down and keep this safe!* You will need it to recover your account." - :here-is-your-signing-phrase "Here is your signing phrase. You will use it to verify your transactions. *Write it down and keep it safe!*" - :phone-number-required "Tap here to validate your phone number & I'll find your friends." - :shake-your-phone "Found a bug or have a suggestion? Just ~shake~ your phone!" - :intro-status "Chat with me to setup your account and change your settings." - :intro-message1 "Welcome to Status!\nTap this message to set your password and get started." - :account-generation-message "Gimmie a sec, I gotta do some crazy math to generate your account!" - :move-to-internal-failure-message "We need to move some important files from external to internal storage. To do this, we need your permission. We won't be using external storage in future versions." - :debug-enabled "Debug server has been launched! You can now execute *status-dev-cli scan* to find the server from your computer on the same network." - - ;;phone types - :phone-e164 "International 1" - :phone-international "International 2" - :phone-national "National" - :phone-significant "Significant" - - ;;chats - :new "New" - :new-chat "New chat" - :start-chat "Start chat" - :start-new-chat "Start new chat" - :start-group-chat "Start group chat" - :invite-friends "Invite friends" - :get-status-at "Get Status at http://status.im" - :chats "Chats" - :delete-chat "Delete chat" - :group-chat "Group chat" - :group-info "Group info" - :delete-chat-confirmation "Are you sure you want to delete this chat?" - :delete-group-chat-confirmation "Are you sure you want to delete this group chat?" - :new-group-chat "New group chat" - :new-public-group-chat "Join public chat" - :selected-for-you "Selected for you" - :public-chat "Public chat" - :edit-chats "Edit chats" - :search-chats "Search chats" - :empty-topic "Empty topic" - :topic-format "Wrong format [a-z0-9\\-]+" - :topic-name-error "A topic name can contain only following characters: a-z -" - :public-group-topic "Topic" - :set-a-topic "Set a topic" - :empty-chat-description "There are no messages \nin this chat yet" - :empty-chat-description-console "Look under the hood! Console is a javascript runtime environment that exposes the whole web3 API. Type \"web3.\" to get started." - :empty-chat-description-one-to-one "Any messages you send here are encrypted and can only be read by you and " - :counter-9-plus "9+" - :show-more "Show more" - :show-less "Show less" - :cooldown/warning-message "Sorry, we limit sending several messages in quick succession to prevent spam. Please try again in a moment" - :cooldown/text-input-disabled "Please wait a moment..." - - ;;discover - :discover "Discover" - :none "None" - :search-tags "Type your search tags here" - :popular-tags "Popular #hashtags" - :recent "Recent statuses" - :no-statuses-found "No statuses found" - :chat "Chat" - :all "All" - :public-chats "Public chats" - :soon "Soon" - :public-chat-user-count "{{count}} people" - :dapps "ÐApps" - :dapp-profile "ÐApp profile" - :no-statuses-discovered "No statuses discovered" - :no-statuses-discovered-body "When somebody posts\na status you will see it here." - :no-hashtags-discovered-title "No #hashtags discovered" - :no-hashtags-discovered-body "When a #hashtag becomes\npopular you will see it here." - - ;;settings - :settings "Settings" - - ;;contacts - :contacts "Contacts" - :new-contact "New contact" - :delete-contact "Delete contact" - :delete-contact-confirmation "This contact will be removed from your contacts" - :remove-from-group "Remove from group" - :edit-contacts "Edit contacts" - :search-contacts "Search contacts" - :contacts-group-new-chat "Start new chat" - :choose-from-contacts "Choose from contacts" - :or-choose-a-contact "Or choose a contact" - :no-contacts "No contacts yet" - :show-qr "Show QR code" - :copy-qr "Copy code" - :qr-code-public-key-hint "Share this code to \nstart chatting" - :enter-address "Enter address" - :enter-contact-code "Enter contact code" - :more "more" - - ;;group-settings - :remove "Remove" - :save "Save" - :create "Create" - :delete "Delete" - :delete-confirmation "Delete?" - :leave "Leave" - :leave-confirmation "Leave?" - :clear "Clear" - :clear-history "Clear history" - :clear-history-title "Clear history?" - :clear-history-confirmation-content "Are you sure you want to clear this chat history?" - :clear-history-confirmation "Clear history?" - :clear-history-action "Clear" - :mute-notifications "Mute notifications" - :leave-group-action "Leave" - :leave-group-title "Leave?" - :leave-group-confirmation "Are you sure you want to leave this group?" - :leave-chat "Leave chat" - :leave-group-chat-confirmation "Are you sure you want to leave this group?" - :leave-group-chat "Leave group chat" - :leave-group "Leave group" - :remove-from-chat "Remove from chat" - :delete-chat-title "Delete chat?" - :leave-public-chat "Leave public chat" - :chat-settings "Chat settings" - :edit "Edit" - :add-members "Add members" - - ;;commands - :chat-send-eth "{{amount}} ETH" - :transaction-request "Transaction Request" - :command-button-send "Send" - :command-button-sent "Sent" - :command-requesting "Requesting " - :command-sending "Send transaction " - :request-requesting-from "from " - :send-sending-to "to " - - ;;new-group - :new-group "New group" - :reorder-groups "Reorder groups" - :edit-group "Edit group" - :delete-group "Delete group" - :delete-group-confirmation "This group will be removed from your groups. This will not affect your contacts" - :delete-group-prompt "This will not affect your contacts" - :contact-s {:one "contact" - :other "contacts"} - - ;;protocol - :received-invitation "received chat invitation" - :removed-from-chat "removed you from group chat" - :left "left" - :invited "invited" - :removed "removed" - :You "You" - - ;;new-contact - :add-new-contact "Add new contact" - :scan-qr "Scan QR code" - :name "Name" - :specify-name "Specify a name" - :address-explication "Your public key is used to generate your address on Ethereum and is a series of numbers and letters. You can find it easily in your profile" - :unable-to-read-this-code "Unable to read this code" - :use-valid-contact-code "Please scan a valid contact code" - :enter-valid-public-key "Please enter a valid public key or scan a QR code" - :contact-already-added "The contact has already been added" - :can-not-add-yourself "You can't add yourself" - :unknown-address "Unknown address" - - ;;login - :connect "Connect" - :address "Address" - :password "Password" - :sign-in-to-status "Sign in to Status" - :sign-in "Sign in" - :sign-in-to-another "Sign in to another account" - :wrong-password "Wrong password" - :enter-password "Enter password" - - ;;recover - :passphrase "Passphrase" - :recovery-phrase "Recovery phrase" - :recover "Recover" - :twelve-words-in-correct-order "12 words in correct order" - :enter-12-words "Enter the 12 words of your recovery phrase, separated by single spaces" - - ;;accounts - :recover-access "Recover access" - :create-new-account "Create new account" - :add-existing-account "Add existing account" - - ;;wallet-qr-code - :done "Done" - - ;;validation - :invalid-phone "Invalid phone number" - :amount "Amount" - :invalid-number "Invalid number" - - ;;transactions - :confirm "Confirm" - :transaction "Transaction" - :unsigned-transaction-expired "Unsigned transaction expired" - :status "Status" - :recipient "Recipient" - :specify-recipient "Specify recipient..." - :recipient-code "Enter recipient address" - :recent-recipients "Contacts" - :to "To" - :from "From" - :at "at" - :data "Data" - :got-it "Got it" - :block "Block" - :hash "Hash" - :gas-limit "Gas limit" - :gas-price "Gas price" - :gas-used "Gas used" - :cost-fee "Cost/Fee" - :nonce "Nonce" - :confirmations "Confirmations" - :confirmations-helper-text "Consider transactions complete after 12 confirmations" - :copy-transaction-hash "Copy transaction ID" - :open-on-etherscan "Open on Etherscan.io" - :incoming "Incoming" - :outgoing "Outgoing" - :pending "Pending" - :failed "Failed" - :postponed "Postponed" - - ;;webview - :web-view-error "Unable to load page" - - ;;testfairy warning - :testfairy-title "Warning!" - :testfairy-message "You are using an app installed from a nightly build. If you're connected to WiFi, your interactions with the app will be saved as video and logs. These recordings do not save your passwords. They are used by our development team to investigate possible issues and only occur if the app is install from a nightly build. Nothing is recorded if the app is installed from PlayStore or TestFlight." - - ;; wallet - :wallet "Wallet" - :wallets "Wallets" - :your-wallets "Your wallets" - :main-wallet "Main Wallet" - :wallet-error "Error loading data" - :wallet-send "Send" - :wallet-send-token "Send {{symbol}}" - :wallet-request "Request" - :wallet-exchange "Exchange" - :wallet-asset "Asset" - :wallet-assets "Assets" - :wallet-collectibles "Collectibles" - :wallet-add-asset "Add asset" - :wallet-total-value "Total value" - :wallet-settings "Wallet settings" - :wallet-manage-assets "Manage Assets" - :sign-message "Sign Message" - :signing-message-phrase-description "Sign the message by entering your password. Make sure that the words above match your secret signing phrase" - :signing-phrase-description "Sign the transaction by entering your password. Make sure that the words above match your secret signing phrase" - :wallet-insufficient-funds "Insufficient funds" - :wallet-insufficient-gas "Not enough ETH for gas" - :receive "Receive" - :request-qr-legend "Share this code to receive assets" - :send-request "Send request" - :send-transaction-request "Send a transaction request" - :share "Share" - :eth "ETH" - :gwei "Gwei" - :wallet-send-min-wei "Min 1 wei" - :currency "Currency" - :usd-currency "USD" - :currency-display-name-aed "Emirati Dirham" - :currency-display-name-afn "Afghanistan Afghani" - :currency-display-name-ars "Argentine Peso" - :currency-display-name-aud "Australian Dollar" - :currency-display-name-bbd "Barbados Dollar" - :currency-display-name-bdt "Bangladeshi Taka" - :currency-display-name-bgn "Bulgarian Lev" - :currency-display-name-bhd "Bahraini Dinar" - :currency-display-name-bmd "Bermuda Dollar" - :currency-display-name-bnd "Brunei Darussalam Dollar" - :currency-display-name-bob "Bolivia Bolíviano" - :currency-display-name-brl "Brazil Real" - :currency-display-name-btn "Bhutanese Ngultrum" - :currency-display-name-bzd "Belize Dollar" - :currency-display-name-cad "Canada Dollar" - :currency-display-name-chf "Switzerland Franc" - :currency-display-name-clp "Chile Peso" - :currency-display-name-cny "China Yuan Renminbi" - :currency-display-name-cop "Colombia Peso" - :currency-display-name-crc "Costa Rica Colon" - :currency-display-name-czk "Czech Koruna" - :currency-display-name-dkk "Denmark Krone" - :currency-display-name-dop "Dominican Republic Peso" - :currency-display-name-egp "Egypt Pound" - :currency-display-name-etb "Ethiopian Birr" - :currency-display-name-eur "Euro" - :currency-display-name-gbp "British Pound" - :currency-display-name-gel "Georgian Lari" - :currency-display-name-ghs "Ghana Cedi" - :currency-display-name-gmd "Gambian dalasi" - :currency-display-name-gyd "Guyana Dollar" - :currency-display-name-hkd "Hong Kong Dollar" - :currency-display-name-hrk "Croatia Kuna" - :currency-display-name-huf "Hungary Forint" - :currency-display-name-idr "Indonesia Rupiah" - :currency-display-name-ils "Israel Shekel" - :currency-display-name-inr "India Rupee" - :currency-display-name-isk "Iceland Krona" - :currency-display-name-jmd "Jamaica Dollar" - :currency-display-name-jpy "Japanese Yen" - :currency-display-name-kes "Kenyan Shilling" - :currency-display-name-krw "Korea (South) Won" - :currency-display-name-kwd "Kuwaiti Dinar" - :currency-display-name-kyd "Cayman Islands Dollar" - :currency-display-name-kzt "Kazakhstan Tenge" - :currency-display-name-lak "Laos Kip" - :currency-display-name-lkr "Sri Lanka Rupee" - :currency-display-name-lrd "Liberia Dollar" - :currency-display-name-ltl "Lithuanian Litas" - :currency-display-name-mad "Moroccan Dirham" - :currency-display-name-mdl "Moldovan Leu" - :currency-display-name-mkd "Macedonia Denar" - :currency-display-name-mnt "Mongolia Tughrik" - :currency-display-name-mur "Mauritius Rupee" - :currency-display-name-mwk "Malawian Kwacha" - :currency-display-name-mxn "Mexico Peso" - :currency-display-name-myr "Malaysia Ringgit" - :currency-display-name-mzn "Mozambique Metical" - :currency-display-name-nad "Namibia Dollar" - :currency-display-name-ngn "Nigeria Naira" - :currency-display-name-nio "Nicaragua Cordoba" - :currency-display-name-nok "Norway Krone" - :currency-display-name-npr "Nepal Rupee" - :currency-display-name-nzd "New Zealand Dollar" - :currency-display-name-omr "Oman Rial" - :currency-display-name-pen "Peru Sol" - :currency-display-name-pgk "Papua New Guinean Kina" - :currency-display-name-php "Philippines Peso" - :currency-display-name-pkr "Pakistan Rupee" - :currency-display-name-pln "Poland Zloty" - :currency-display-name-pyg "Paraguay Guarani" - :currency-display-name-qar "Qatar Riyal" - :currency-display-name-ron "Romania Leu" - :currency-display-name-rsd "Serbia Dinar" - :currency-display-name-rub "Russia Ruble" - :currency-display-name-sar "Saudi Arabia Riyal" - :currency-display-name-sek "Sweden Krona" - :currency-display-name-sgd "Singapore Dollar" - :currency-display-name-sos "Somalia Shilling" - :currency-display-name-srd "Suriname Dollar" - :currency-display-name-thb "Thailand Baht" - :currency-display-name-ttd "Trinidad and Tobago Dollar" - :currency-display-name-twd "Taiwan New Dollar" - :currency-display-name-tzs "Tanzanian Shilling" - :currency-display-name-try "Turkish Lira" - :currency-display-name-uah "Ukraine Hryvnia" - :currency-display-name-ugx "Ugandan Shilling" - :currency-display-name-uyu "Uruguay Peso" - :currency-display-name-usd "United States Dollar" - :currency-display-name-vef "Venezuela Bolívar" - :currency-display-name-vnd "Viet Nam Dong" - :currency-display-name-yer "Yemen Rial" - :currency-display-name-zar "South Africa Rand" - :amount-placeholder "Specify amount..." - :transactions "Transactions" - :transaction-details "Transaction details" - :transaction-failed "Transaction failed" - :transactions-sign "Sign" - :transactions-sign-all "Sign all" - :transactions-sign-transaction "Sign transaction" - :transactions-sign-later "Sign later" - :transactions-delete "Delete transaction" - :transactions-delete-content "Transaction will be removed from 'Unsigned' list" - :transactions-history "Transaction history" - :transactions-unsigned "Unsigned" - :transactions-history-empty "No transactions in your history yet" - :transactions-unsigned-empty "You don't have any unsigned transactions" - :transactions-filter-title "Filter history" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Type" - :transactions-filter-select-all "Select all" - :view-transaction-details "View transaction details" - :transaction-description "Please wait for at least 12 confirmations to make sure your transaction is processed securely" - :transaction-sent "Transaction sent" - :transaction-moved-text "The transaction will remain in the 'Unsigned' list for the next 5 mins" - :transaction-moved-title "Transaction moved" - :sign-later-title "Sign transaction later?" - :sign-later-text "Check the transaction history to sign this transaction" - :not-applicable "Not applicable for unsigned transactions" - :send-transaction "Send transaction" - :new-request "New request" - :request-transaction "Request transaction" - :receive-transaction "Receive transaction" - :new-transaction "New Transaction" - :transaction-history "Transaction History" - :new-contract "New Contract" - - ;; Wallet Send - :wallet-choose-recipient "Choose Recipient" - :wallet-choose-from-contacts "Choose from Contacts" - :wallet-address-from-clipboard "Use Address From Clipboard" - :wallet-invalid-address "Invalid address: \n {{data}}" - :wallet-invalid-chain-id "Network does not match: \n {{data}} but current chain is {{chain}}" - :changed-asset-warning "Asset was changed from {{old}} to {{new}}" - :changed-amount-warning "Amount was changed from {{old}} to {{new}}" - :wallet-browse-photos "Browse Photos" - :wallet-advanced "Advanced" - :wallet-transaction-fee "Transaction Fee" - :wallet-transaction-fee-details "Gas limit caps the units of gas spent on the transaction. Gas price sets the price per unit of gas. Increasing gas price can make your transaction faster." - :wallet-transaction-total-fee "Total Fee" - :validation-amount-invalid-number "Amount is not a valid number" - :validation-amount-is-too-precise "Amount is too precise. Max number of decimals is {{decimals}}." - :scan-qr-code "Scan a QR code with a wallet address" - :reset-default "Reset to default" - - :view-cryptokitties "View in CryptoKitties" - :cryptokitty-name "CryptoKitty #{{id}}" - - :view-etheremon "View in Etheremon" - - :view-cryptostrikers "View in CryptoStrikers" - - ;; network settings - :new-network "New network" - :add-network "Add network" - :add-new-network "Add new network" - :add-wnode "Add mailserver" - :existing-networks "Existing networks" - :delete-network-title "Delete network?" - :delete-network-confirmation "Are you sure you want to delete this network?" - ;; TODO(dmitryn): come up with better description/naming. Suggested namings: Mailbox and Master Node - :existing-wnodes "Existing mailservers" - :add-mailserver "Add Mailserver" - :mailserver-details "Mailserver details" - :delete-mailserver-title "Delete mailserver" - :delete-mailserver-are-you-sure "Are you sure you want to delete this mailserver?" - :delete-mailserver "Delete mailserver" - :mailserver-address "Mailserver address" - :specify-mailserver-address "Specify a mailserver address" - :add-json-file "Add a JSON file" - :paste-json-as-text "Paste JSON as text" - :paste-json "Paste JSON" - :specify-rpc-url "Specify a RPC URL" - :edit-network-config "Edit network config" - :connected "Connected" - :process-json "Process JSON" - :error-processing-json "Error processing JSON" - :rpc-url "RPC URL" - :network-chain "Network chain" - :network "Network" - :network-details "Network details" - :remove-network "Remove network" - :network-settings "Network settings" - :offline-messaging "Mailserver" - :offline-messaging-settings "Mailserver settings" - :edit-network-warning "Be careful! Editing the network data may disable this network for you" - :delete-network-error "Please connect to a different network before deleting this one" - :connecting-requires-login "Connecting to another network requires login" - :close-app-title "Warning!" - :close-app-content "The app will stop and close. When you reopen it, the selected network will be used" - :close-app-button "Confirm" - :connect-wnode-content "Connect to {{name}}?" - - ;; Bootnodes - :bootnodes "Bootnodes" - :bootnodes-settings "Bootnodes settings" - :bootnodes-enabled "Bootnodes enabled" - :bootnode-address "Bootnode address" - :add-bootnode "Add bootnode" - :bootnode-details "Bootnode details" - :specify-bootnode-address "Specify bootnode address" - :delete-bootnode-title "Delete bootnode" - :delete-bootnode-are-you-sure "Are you sure you want to delete this bootnode?" - :delete-bootnode "Delete bootnode" - - :mainnet-is-default-alert-title "Hello" - :mainnet-is-default-alert-text "Thanks for choosing Status! We are now in Beta, which means new installs are automatically connected to the Mainnet. If you upgraded, you may still need to switch networks, which you can do easily by navigating to your Profile -> Advanced Mode -> Switch Developer Mode on -> Choose your network. Now just have fun! Browse DApps, exchange assets, start your own #chats, and message your friends, all from the world's first decentralised, mobile client for Ethereum.\n\nPlease note that this is a beta release and we advise you that using this app should be done for testing purposes only and you assume the full responsibility for all risks concerning your data and funds." - - :main-networks "Main networks" - :test-networks "Test networks" - :custom-networks "Custom networks" - - :mainnet-network "Main network" - :ropsten-network "Ropsten test network" - :rinkeby-network "Rinkeby test network" - :network-id "Network ID" - :specify-network-id "Specify network id" - - ;; invalid-key - - :invalid-key-title "We detected a problem with the encryption key" - :invalid-key-content "To protect yourself, you need to create new account and erase your old data by tapping “Apply”. If you have an existing account and would like to save your recovery phrase then choose “Cancel”, back it up, and restart the app. We strongly recommend creating new account because the old one is stored unencrypted." - :invalid-key-confirm "Apply" - - ;; decryption-failed - - :decryption-failed-title "We were not able to decrypt your data" - :decryption-failed-content "We were not able to decrypt your data, you might need to create new account and erase your old data by tapping “Apply”. Clicking on “Cancel”, will try again" - :decryption-failed-confirm "Apply" - - ;; browser - :browser "Browser" - :enter-dapp-url "Enter a ÐApp URL" - :dapp "ÐApp" - :selected "Selected" - :selected-dapps "Selected ÐApps" - :browser-secure "Connection is secure. Make sure you really trust this site before signing transactions or entering personal data." - :browser-not-secure "Connection is not secure! Do not sign transactions or send personal data on this site." - :make-sure-you-trust-dapp "Make sure that you trust this DApp" - :would-like-to-access "Would like to Access" - :your-contact-code "Your Contact Code"}) diff --git a/src/status_im/translations/es.cljs b/src/status_im/translations/es.cljs deleted file mode 100644 index 098d51c8de9..00000000000 --- a/src/status_im/translations/es.cljs +++ /dev/null @@ -1,351 +0,0 @@ -(ns status-im.translations.es) - -(def translations - {;;common - :members-title "Miembros" - :not-implemented "¡No se ha implementado!" - :chat-name "Nombre del chat" - :notifications-title "Notificaciones y sonidos" - :offline "Desconectado" - :search-for "Buscar..." - :cancel "Cancelar" - :next "Siguiente" - :open "Abrir" - :description "Descripción" - :url "URL" - :type-a-message "Escribe un mensaje..." - :type-a-command "Comienza escribiendo un comando..." - :error "Error" - :unknown-status-go-error "Error de status-go desconocido" - :node-unavailable "No existe un nodo en funcionamiento" - :yes "Sí" - :no "No" - - :camera-access-error "Para conceder el permiso requerido de la cámara, por favor dirígete a los ajustes del sistema y asegúrate que Status > Cámara está seleccionado." - :photos-access-error "Para conceder el permiso requerido de fotos, por favor dirígete a los ajustes del sistema y asegúrate de que Status > Fotos está seleccionado." - - ;;drawer - :switch-users "Cambio de usuario" - :current-network "Red actual" - - ;;chat - :is-typing "está escribiendo" - :and-you "y tú" - :search-chat "Buscar chat" - :members {:one "1 miembro" - :other "{{count}} miembros" - :zero "sin miembros"} - :members-active {:one "1 miembro" - :other "{{count}} miembros" - :zero "sin miembros"} - :public-group-status "Público" - :active-online "En línea" - :active-unknown "Desconocido" - :available "Disponible" - :no-messages "Sin mensajes" - :suggestions-requests "Peticiones" - :suggestions-commands "Comandos" - :faucet-success "La petición de la fuente ha sido recibida" - :faucet-error "La petición de la fuente falló" - - ;;sync - :sync-in-progress "Sincronizando..." - :sync-synced "Sincronizado" - - ;;messages - :status-pending "Pendiente" - :status-sent "Enviado" - :status-seen-by-everyone "Visto por todos" - :status-seen "Visto" - :status-delivered "Entregado" - :status-failed "Fallído" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "segundo" - :other "segundos"} - :datetime-minute {:one "minuto" - :other "minutos"} - :datetime-hour {:one "hora" - :other "horas"} - :datetime-day {:one "día" - :other "días"} - :datetime-ago "hace" - :datetime-yesterday "ayer" - :datetime-today "hoy" - - ;;profile - :profile "Perfil" - :edit-profile "Editar perfil" - :message "Mensaje" - :not-specified "No especificado" - :public-key "Clave pública" - :phone-number "Número de teléfono" - :update-status "Actualiza tu estado..." - :add-a-status "Añade un estado..." - :status-prompt "Establece tu estado. Usando #hastags permitirás que los demás te encuentren y podrás hablar sobre lo que tengas en mente" - :add-to-contacts "Añadir a contactos" - :in-contacts "En contactos" - :remove-from-contacts "Eliminar de contactos" - :start-conversation "Empezar conversación" - :send-transaction "Enviar transacción" - :testnet-text "Estás en la red de prueba {{testnet}}. No envíes ETH o SNT real a tu dirección" - :mainnet-text "Estás en la red principal. Se enviará ETH real" - - ;;make_photo - :image-source-title "Imágen de perfil" - :image-source-make-photo "Captura" - :image-source-gallery "Seleccionar de la galería" - - ;;sharing - :sharing-copy-to-clipboard "Copiar al portapapeles" - :sharing-share "Compartir..." - :sharing-cancel "Cancelar" - - :browsing-title "Navegar" - :browsing-open-in-web-browser "Abrir en el navegador" - :browsing-cancel "Cancelar" - - ;;sign-up - :contacts-syncronized "Tus contactos se han sincronizado" - :confirmation-code (str "¡Gracias! Has enviado un mensaje de texto con un código de confirmación. " - "Por favor danos ese código para confirmar tu número de teléfono") - :incorrect-code (str "Lo sentimos, el código es incorrecto, por favor inténtalo nuevamente") - :phew-here-is-your-passphrase "*Uff*, ha sido difícil. Aquí esta tu frase de respaldo, *¡anótala y mantenla a salvo!* La vas a necesitar para recuperar tu cuenta." - :here-is-your-passphrase "Aquí está tu frase de respaldo, *escríbela y ¡guárdala en un sitio seguro! *La necesitarás para recuperar tu cuenta." - :here-is-your-signing-phrase "Aquí está tu frase para firmar. La vas a usar para firmar tus transacciones. *¡Anótala y mantenla a salvo!*" - :phone-number-required "Toca aquí para validar tu número de teléfono y encontraré a tus amigos." - :shake-your-phone "¿Has encontrado un error o tienes alguna sugerencia? ¡Simplemente ~agíta~ tu teléfono!" - :intro-status "Habla conmigo para configurar tu cuenta y cambiar tus ajustes." - :intro-message1 "¡Bienvenido a Status!\nToca este mensaje para establecer tu clave y empezar." - :account-generation-message "Dame un segundo, ¡tengo que hacer unas cálculos para generar tu cuenta!" - :move-to-internal-failure-message "Tenemos que mover algunos archivos importantes del almacenamiento externo al interno. Para ello necesitamos tu permiso. No usaremos el almacenamiento externo en versiones futuras." - :debug-enabled "¡El servidor de depuración ha sido activado! Ahora puedes ejecutar *status-dev-cli scan* para encontrar el servidor en tu ordenador desde la misma red." - - ;;phone types - :phone-e164 "Internacional 1" - :phone-international "Internacional 2" - :phone-national "Nacional" - :phone-significant "Importante" - - ;;chats - :chats "Chats" - :delete-chat "Eliminar chat" - :new-group-chat "Nuevo chat grupal" - :new-public-group-chat "Unirse a nuevo chat público" - :edit-chats "Editar chats" - :search-chats "Buscar chats" - :empty-topic "Limpiar tema" - :topic-format "Formato incorrecto [a-z0-9\\-]+" - :public-group-topic "Tema" - - ;;discover - :discover "Descubrir" - :none "Nada" - :search-tags "Escribe tus etiquetas de búsqueda aquí" - :popular-tags "#hashtags populares" - :recent "Estados recientes" - :no-statuses-found "No se han encontrado estados" - :chat "Chat" - :all "Todo" - :public-chats "Chats públicos" - :soon "Pronto" - :public-chat-user-count "{{count}} personas" - :dapps "ÐApps" - :dapp-profile "Perfil ÐApp" - :no-statuses-discovered "No se han descubierto estados" - :no-statuses-discovered-body "Cuando alguien publique\nun estado lo verás aquí." - :no-hashtags-discovered-title "No se han descubierto #hashtags" - :no-hashtags-discovered-body "Cuando un #hashtag se vuelva\npopular lo verás aquí." - - ;;settings - :settings "Ajustes" - - ;;contacts - :contacts "Contactos" - :new-contact "Contacto nuevo" - :delete-contact "Eliminar contacto" - :delete-contact-confirmation "Este contacto será eliminado de tus contactos" - :remove-from-group "Eliminar del grupo" - :edit-contacts "Editar contactos" - :search-contacts "Buscar contactos" - :contacts-group-new-chat "Empezar un nuevo chat" - :choose-from-contacts "Elegir de los contactos" - :no-contacts "No hay contactos todavía" - :show-qr "Mostrar código QR" - :enter-address "Introducir dirección" - :more "más" - - ;;group-settings - :remove "Eliminar" - :save "Guardar" - :delete "Eliminar" - :clear-history "Limpiar historial" - :mute-notifications "Silenciar notificaciones" - :leave-chat "Abandonar chat" - :chat-settings "Ajustes de chat" - :edit "Editar" - :add-members "Añadir miembros" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nuevo grupo" - :reorder-groups "Reordenar grupos" - :edit-group "Editar grupo" - :delete-group "Eliminar grupo" - :delete-group-confirmation "Este grupo será eliminado de tus grupos. Esto no afectará a tus contactos" - :delete-group-prompt "Esto no afectará a tus contactos" - :contact-s {:one "contacto" - :other "contactos"} - - ;;protocol - :received-invitation "recibió invitación de chat" - :removed-from-chat "fue eliminado del chat" - :left "Se salió" - :invited "fue invitado" - :removed "fue eliminado" - :You "Tú" - - ;;new-contact - :add-new-contact "Agregar nuevo contacto" - :scan-qr "Escanear código QR" - :name "Nombre" - :address-explication "Tu clave pública se usa para generar tu dirección en Ethereum y es una serie de números y letras. La puedes encontrar fácilmente en tu perfil" - :enter-valid-public-key "Por favor introduce una clave pública válida o escanea un código QR" - :contact-already-added "El contacto se ha agregado" - :can-not-add-yourself "No te puedes agregar a tí mismo" - :unknown-address "Dirección desconocida" - - ;;login - :connect "Conectar" - :address "Dirección" - :password "Contraseña" - :sign-in-to-status "Iniciar sesión en Status" - :sign-in "Iniciar sesión" - :wrong-password "Contraseña incorrecta" - :enter-password "Ingresar contraseña" - - ;;recover - :passphrase "Frase clave" - :recover "Recuperar" - :twelve-words-in-correct-order "12 palabras en el orden correcto" - - ;;accounts - :recover-access "Recuperar acceso" - :create-new-account "Crear una nueva cuenta" - - ;;wallet-qr-code - :done "Hecho" - - ;;validation - :invalid-phone "Número de teléfono inválido" - :amount "Cantidad" - - ;;transactions - :confirm "Confirmar" - :transaction "Transacción" - :unsigned-transaction-expired "Transación sin firmar expirada" - :status "Estado" - :recipient "Destinatario" - :to "Para" - :from "De" - :data "Datos" - :got-it "Entendido" - :block "Bloquear" - :hash "Hash" - :gas-limit "Límite de gas" - :gas-price "Precio del gas" - :gas-used "Gas usado" - :cost-fee "Costo/Tarifa" - :nonce "Mientras tanto" - :confirmations "Confirmaciones" - :confirmations-helper-text "Por favor espera al menos a que haya 12 confirmaciones para asegurarte de que tu transacción es procesada con seguridad" - :copy-transaction-hash "Copiar hash de transacción" - :open-on-etherscan "Abrir en Etherscan.io" - - ;;webview - :web-view-error "oops, error" - - ;;testfairy warning - :testfairy-title "¡Advertencia!" - :testfairy-message "Estás usando una aplicación instalada desde una versión nocturna. Por motivos de prueba esta versión incluye grabación de la sesión si se usa conexión wifi, así que todas las interacciones con esta aplicación se guardan (así como vídeo y registros) y pueden ser usadas por nuestro equipo de desarrollo para investigar posibles problemas. Los video/registros no incluyen tus contraseñas. La grabación se realiza solo si la app está instalada desde una versión nocturna. Nada se graba si la app está instalada desde la PlayStore o desde TestFlight." - - ;; wallet - :wallet "Cartera" - :wallets "Carteras" - :your-wallets "Tus carteras" - :main-wallet "Cartera principal" - :wallet-error "Error cargando datos" - :wallet-send "Enviar" - :wallet-request "Solicitar" - :wallet-exchange "Intercambio" - :wallet-assets "Activos" - :wallet-add-asset "Añadir activo" - :wallet-total-value "Valor total" - :wallet-settings "Ajustes de cartera" - :signing-phrase-description "Firma la transación poniendo tu contraseña. Asegúrate de que las palabras de arriba coinciden con tu frase de firmas secreta" - :wallet-insufficient-funds "Fondos insuficientes" - :request-transaction "Solicitar transacción" - :send-request "Enviar solicitud" - :share "Compartir" - :eth "ETH" - :currency "Divisa" - :usd-currency "USD" - :transactions "Transacciones" - :transaction-details "Detalles de la transación" - :transaction-failed "Transacción fallida" - :transactions-sign "Firmar" - :transactions-sign-all "Firmar todo" - :transactions-sign-transaction "Firmar transacción" - :transactions-sign-later "Firmar después" - :transactions-delete "Eliminar transación" - :transactions-delete-content "La transacción será eliminada de la lista 'Sin firmar'" - :transactions-history "Historial" - :transactions-unsigned "Sin firmar" - :transactions-history-empty "No hay transacciones en tu historial todavía" - :transactions-unsigned-empty "No tienes ninguna transacción sin firmar" - :transactions-filter-title "Filtrar historial" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Tipo" - :transactions-filter-select-all "Seleccionar todo" - :view-transaction-details "Ver detalles de la transacción" - :transaction-description "Espera hasta al menos 12 confirmaciones para asegurarte de que la transacción es procesada con seguridad" - :transaction-sent "Transacción enviada" - :transaction-moved-text "La transacción permanecerá en la lista 'Sin firmar' durante los siguientes 5 minutos" - :transaction-moved-title "Transacción movida" - :sign-later-title "¿Firmar transacción mas tarde?" - :sign-later-text "Comprueba el historial de transacciones para firmar esta transacción" - :not-applicable "No aplicable para transacciones sin firmar" - - ;; Wallet Send - :wallet-choose-recipient "Eligir destinatario" - :wallet-choose-from-contacts "Elegir de los Contactos" - :wallet-address-from-clipboard "Usar Dirección del Portapapeles" - :wallet-invalid-address "Dirección inválida: \n {{data}}" - :wallet-browse-photos "Navegar Imágenes" - :validation-amount-invalid-number "La cantidad no es un número válido" - :validation-amount-is-too-precise "La cantidad es demasiado precisa. La mínima unidad que puedes enviar es 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Nueva red" - :add-network "Añadir red" - :add-new-network "Añadir nueva red" - :existing-networks "Redes existentes" - :add-json-file "Añadir un archivo JSON" - :paste-json-as-text "Pegar JSON como texto" - :paste-json "Pegar JSON" - :specify-rpc-url "Especificar una URL RPC" - :edit-network-config "Editar configuración de red" - :connected "Conectado" - :process-json "Procesar JSON" - :error-processing-json "Error procesando JSON" - :rpc-url "RPC URL" - :remove-network "Eliminar red" - :network-settings "Ajustes de red" - :edit-network-warning "Cuidado, editar los datos de red puede deshabilitar esta red para tí" - :connecting-requires-login "Conectarse a otra red requiere inicio de sesión" - :close-app-title "¡Advertencia!" - :close-app-content "La app se detendrá y cerrará. Cuando la vuelvas a abrir, la red seleccionada será usada" - :close-app-button "Confirmar"}) diff --git a/src/status_im/translations/es_ar.cljs b/src/status_im/translations/es_ar.cljs deleted file mode 100644 index 85ad37f0cc9..00000000000 --- a/src/status_im/translations/es_ar.cljs +++ /dev/null @@ -1,230 +0,0 @@ -(ns status-im.translations.es-ar) - -(def translations - {;;common - :members-title "Miembros" - :not-implemented "!no implementado" - :chat-name "Nombre del chat" - :notifications-title "Notificaciones y sonidos" - :offline "Fuera de línea" - - ;;drawer - :switch-users "Cambiar de usuario" - - ;;chat - :is-typing "está escribiendo" - :and-you "y tú" - :search-chat "Buscar chat" - :members {:one "1 miembro" - :other "{{count}} miembros" - :zero "no hay miembros"} - :members-active {:one "1 miembro, 1 activo" - :other "{{count}} miembros, {{count}} activo(s)" - :zero "no hay miembros"} - :active-online "En línea" - :active-unknown "Desconocido" - :available "Disponible" - :no-messages "No hay mensajes" - :suggestions-requests "Solicitudes" - :suggestions-commands "Comandos" - - ;;sync - :sync-in-progress "Sincronizando..." - :sync-synced "Sincronizado" - - ;;messages - :status-sending "Enviando" - :status-pending "Pendiente" - :status-sent "Enviado" - :status-seen-by-everyone "Visto por todos" - :status-seen "Visto" - :status-delivered "Enviado" - :status-failed "Fallido" - - ;;datetime - :datetime-second {:one "segundo" - :other "segundos"} - :datetime-minute {:one "minuto" - :other "minutos"} - :datetime-hour {:one "hora" - :other "horas"} - :datetime-day {:one "día" - :other "días"} - :datetime-ago "atrás" - :datetime-yesterday "ayer" - :datetime-today "hoy" - - ;;profile - :profile "Perfil" - :message "Mensaje" - :not-specified "No especificado" - :public-key "Clave pública" - :phone-number "Número telefónico" - :add-to-contacts "Agregar a contactos" - - ;;make_photo - :image-source-title "Imagen de perfil" - :image-source-make-photo "Capturar" - :image-source-gallery "Seleccionar de la galería" - - ;;sharing - :sharing-copy-to-clipboard "Copiar" - :sharing-share "Compartir..." - :sharing-cancel "Cancelar" - - ;;sign-up - :contacts-syncronized "Tus contactos se han sincronizado" - :confirmation-code (str "¡Gracias! Te hemos enviado un código de confirmación por mensaje de texto. " - "Ingresa este código para confirmar tu número telefónico") - :incorrect-code (str "Lo siento, el código no era correcto;; ingrésalo de nuevo") - :phew-here-is-your-passphrase "*Wow* eso estuvo difícil, aquí tienes tu contraseña, *¡anótala y mantenla segura!* La necesitarás para recuperar tu cuenta." - :here-is-your-passphrase "Aquí tienes tu frase de contraseña, *¡Anótala y mantenga segura!* La necesitarás para recuperar tu cuenta." - :phone-number-required "Pulsa aquí para ingresar tu número telefónico y yo encontraré a tus amigos" - :intro-status "Chatea conmigo para establecer tu cuenta y cambiar tu configuración!" - :intro-message1 "Bienvenido(a) a Status\n¡Pulsa en este mensaje para establecer tu contraseña y comenzar!" - :account-generation-message "¡Dame un segundo, necesito realizar un súper cálculo para generar tu cuenta!" - - ;;chats - :chats "Chats" - :new-group-chat "Nuevo chat de grupo" - - ;;discover - :discover "Descubrimiento" - :none "Ninguno" - :search-tags "Ingresa aquí tus etiquetas de búsqueda" - :popular-tags "Etiquetas populares" - :recent "Reciente" - :no-statuses-discovered "No se encontró ningún estatus" - - ;;settings - :settings "Configuración" - - ;;contacts - :contacts "Contactos" - :new-contact "Nuevo contacto" - :contacts-group-new-chat "Iniciar nuevo chat" - :no-contacts "No hay contactos todavía" - :show-qr "Mostrar QR" - - ;;group-settings - :remove "Eliminar" - :save "Guardar" - :clear-history "Borrar historial" - :chat-settings "Configuración del chat" - :edit "Editar" - :add-members "Agregar miembros" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "recibió invitación a chat" - :removed-from-chat "te retiró del grupo de chat" - :left "restantes" - :invited "invitado" - :removed "retirado" - :You "Tú" - - ;;new-contact - :add-new-contact "Agregar nuevo contacto" - :scan-qr "Escanear QR" - :name "Nombre" - :address-explication "Tal vez, aquí debería haber alguna indicación explicando qué es una dirección y dónde buscarla" - :contact-already-added "El contacto ya ha sido agregado" - :can-not-add-yourself "No puedes agregarte a ti mismo(a)" - :unknown-address "Dirección desconocida" - - ;;login - :connect "Conectar" - :address "Dirección" - :password "Contraseña" - :wrong-password "Contraseña incorrecta" - - ;;recover - :passphrase "Frase de contraseña" - :recover "Recuperar" - - ;;accounts - :recover-access "Recuperar acceso" - - ;;wallet-qr-code - :done "Completado" - :main-wallet "Cartera principal" - - ;;validation - :invalid-phone "Número telefónico inválido" - :amount "Monto" - ;;transactions - :status "Estatus" - :recipient "Destinatario" - - ;;:webview - :web-view-error "oops, error" - - :confirm "Confirmar" - :phone-national "Nacional" - :public-group-topic "Tema" - :debug-enabled "¡Se inició el servidor de depuración! Ahora puede agregar su DApp ejecutando *status-dev-cli scan* desde su computadora" - :new-public-group-chat "Unirse al chat público" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :twelve-words-in-correct-order "12 palabras en el orden correcto" - :remove-from-contacts "Eliminar de los contactos" - :delete-chat "Eliminar chat" - :edit-chats "Editar chats" - :sign-in "Iniciar sesión" - :create-new-account "Crear cuenta nueva" - :sign-in-to-status "Iniciar sesión en Status" - :got-it "Entendido" - :move-to-internal-failure-message "Necesitamos mover algunos archivos importantes del almacenamiento externo al interno. Para esto, necesitamos su permiso. No usaremos almacenamiento externo en futuras versiones." - :edit-group "Editar grupo" - :delete-group "Eliminar grupo" - :browsing-title "Explorar" - :reorder-groups "Reorganizar grupos" - :browsing-cancel "Cancelar" - :faucet-success "Se recibió la solicitud de Faucet" - :choose-from-contacts "Elegir de los contactos" - :new-group "Nuevo grupo" - :phone-e164 "Internacional 1" - :remove-from-group "Eliminar del grupo" - :search-contacts "Buscar contactos" - :transaction "Transacción" - :public-group-status "Público" - :leave-chat "Abandonar chat" - :start-conversation "Iniciar conversación" - :topic-format "Formato incorrecto [a-z0-9\\-]+" - :enter-valid-public-key "Ingrese una clave pública válida o escanee un código QR" - :faucet-error "Error de solicitud de Faucet" - :phone-significant "Significativo" - :search-for "Buscar..." - :phone-international "Internacional 2" - :enter-address "Ingresar dirección" - :send-transaction "Enviar transacción" - :delete-contact "Eliminar contacto" - :mute-notifications "Silenciar notificaciones" - :contact-s {:one "contacto" - :other "contactos"} - :next "Siguiente" - :from "De" - :search-chats "Buscar chats" - :in-contacts "En contactos" - - :type-a-message "Escribir un mensaje..." - :type-a-command "Comience a escribir un comando..." - :shake-your-phone "¿Encontró un error o tiene una sugerencia? ¡~Sacuda~ su teléfono!" - :status-prompt "Cree un estado para que la gente vea lo que ofrece. También puede usar #hashtags." - :add-a-status "Agregar un estado..." - :error "Error" - :edit-contacts "Editar contactos" - :more "más" - :cancel "Cancelar" - :no-statuses-found "No se encontraron estados" - :browsing-open-in-web-browser "Abrir en navegador web" - :delete-group-prompt "Esto no afectará a los contactos" - :edit-profile "Editar perfil" - :empty-topic "Tema vacío" - :to "Para" - :data "Datos"}) diff --git a/src/status_im/translations/es_mx.cljs b/src/status_im/translations/es_mx.cljs deleted file mode 100644 index e73734bf9ad..00000000000 --- a/src/status_im/translations/es_mx.cljs +++ /dev/null @@ -1,238 +0,0 @@ -(ns status-im.translations.es-mx) - -(def translations - {;;common - :members-title "Miembros" - :not-implemented "!No implementado!" - :chat-name "Nombre del chat" - :notifications-title "Notificaciones y sonidos" - :offline "Desconectado" - :search-for "Buscar..." - :cancel "Cancelar" - :next "Siguiente" - :type-a-message "Escribe un mensaje..." - :type-a-command "Escribe un comando..." - :error "Error" - - :camera-access-error "Para permitir el acceso a la cámara, porfavor, ve a configuración de sistema y asegúrate que la opción 'Status > Cámara' está seleccionada." - :photos-access-error "Para permitir el acceso a fotos, porfavor, ve a configuración de sistema y asegúrate que la opción 'Status > Fotos' está seleccionada." - - ;;drawer - :switch-users "Cambiar usuarios" - :current-network "Red actual" - - ;;chat - :is-typing "está escribiendo" - :and-you "y tú" - :search-chat "Buscar chat" - :members {:one "1 miembro" - :other "{{count}} miembros" - :zero "no hay miembros"} - :members-active {:one "1 miembro" - :other "{{count}} miembros" - :zero "no hay miembros"} - :public-group-status "Público" - :active-online "En línea" - :active-unknown "Desconocido" - :available "Disponible" - :no-messages "No hay mensajes" - :suggestions-requests "Peticiones" - :suggestions-commands "Comandos" - :faucet-success "Se recibió la solicitud de Faucet" - :faucet-error "Error de solicitud de Faucet" - - ;;sync - :sync-in-progress "Sincronizando..." - :sync-synced "Sincronizado" - - ;;messages - :status-sending "Enviando" - :status-pending "Pendiente" - :status-sent "Enviado" - :status-seen-by-everyone "Visto por todos" - :status-seen "Visto" - :status-delivered "Entregado" - :status-failed "Fallido" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "segundo" - :other "segundos"} - :datetime-minute {:one "minuto" - :other "minutos"} - :datetime-hour {:one "hora" - :other "horas"} - :datetime-day {:one "día" - :other "días"} - :datetime-ago "hace" - :datetime-yesterday "ayer" - :datetime-today "hoy" - - ;;profile - :profile "Perfil" - :edit-profile "Editar perfil" - :message "Mensaje" - :not-specified "No especificado" - :public-key "Llave pública" - :phone-number "Número de teléfono" - :update-status "Actualizar tu estado..." - :add-a-status "Agregar un estado..." - :status-prompt "Crea un estado para informar a los demás lo que ofreces. También puedes usar #hashtags." - :add-to-contacts "Agregar a contactos" - :in-contacts "En contactos" - :remove-from-contacts "Eliminar de contactos" - :start-conversation "Empezar conversación" - :send-transaction "Enviar transacción" - - ;;make_photo - :image-source-title "Imagen de perfil" - :image-source-make-photo "Tomar foto" - :image-source-gallery "Seleccionar de la galería" - - ;;sharing - :sharing-copy-to-clipboard "Copiar" - :sharing-share "Compartir..." - :sharing-cancel "Cancelar" - - :browsing-title "Navegar" - :browsing-open-in-web-browser "Abrir en navegador" - :browsing-cancel "Cancelar" - - ;;sign-up - :contacts-syncronized "Tus contactos se han sincronizado" - :confirmation-code (str "¡Gracias! Te hemos enviado un mensaje de texto con un código de confirmación " - ". Por favor, usa el código para confirmar tu número de teléfono") - :incorrect-code (str "Lo sentimos, el código es incorrecto, escríbelo de nuevo") - :phew-here-is-your-passphrase "*Uff* eso fue difícil, esta es tu frase de contraseña, *¡escríbela y guárdala en un lugar seguro!* La necesitarás para recuperar tu cuenta." - :here-is-your-passphrase "Esta es tu frase de contraseña, *¡escríbela y guárdala en un lugar seguro!* La necesitarás para recuperar tu cuenta." - :phone-number-required "Toca aquí para ingresar tu número de teléfono y encontraré a tus amigos" - :shake-your-phone "¿Encontraste un bug o tienes una sugerencia? ¡~Agíta tu teléfono~!" - :intro-status "¡Chatea conmigo para establecer tu cuenta y modificar tu configuración!" - :intro-message1 "¡Bienvenid@ a Status\nToca este mensaje para configurar tu contraseña y empezar!" - :account-generation-message "Dame un segundo, ¡haré algunos cálculos para generar tu cuenta!" - :move-to-internal-failure-message "Necesitamos mover algunos datos importantes de almacenamiento externo a interno. Necesitamos tu permiso para hacerlo. No usaremos almacenamiento externo en futuras versiones." - :debug-enabled "¡El servidor de depuración ha sido lanzado! Ahora puedes ejecutar *status-dev-cli scan* para encontrar el servidor desde tu computadora en la misma red." - - ;;phone types - :phone-e164 "Internacional 1" - :phone-international "Internacional 2" - :phone-national "Nacional" - :phone-significant "Importante" - - ;;chats - :chats "Chats" - :delete-chat "Eliminar chat" - :new-group-chat "Nuevo grupo de chat" - :new-public-group-chat "Unirte a chat público" - :edit-chats "Editar chats" - :search-chats "Buscar chats" - :empty-topic "Tema vacío" - :topic-format "Formato incorrecto [a-z0-9\\-]+" - :public-group-topic "Tema" - - ;;discover - :discover "Descubrir" - :none "Ninguno" - :search-tags "Escribe tus etiquetas de búsqueda aquí" - :popular-tags "Etiquetas populares" - :recent "Recientes" - :no-statuses-discovered "No se descubrieron estados" - :no-statuses-found "No se encontraron estados" - - ;;settings - :settings "Ajustes" - - ;;contacts - :contacts "Nuevo contacto" - :delete-contact "Borrar contacto" - :delete-contact-confirmation "Este contacto será eliminado de tus contactos" - :remove-from-group "Eliminar del grupo" - :edit-contacts "Editar contactos" - :search-contacts "Buscar contactos" - :contacts-group-new-chat "Iniciar chat nuevo" - :choose-from-contacts "Seleccionar de contactos" - :no-contacts "Aún no hay contactos" - :show-qr "Mostrar QR" - :enter-address "Ingresar dirección" - :more "más" - - ;;group-settings - :remove "Remover" - :save "Guardar" - :delete "Eliminar" - :clear-history "Borrar historial" - :mute-notifications "Silenciar notificaciones" - :leave-chat "Abandonar chat" - :chat-settings "Ajustes de chat" - :edit "Editar" - :add-members "Agregar miembros" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nuevo grupo" - :reorder-groups "Reordenar grupos" - :edit-group "Editar grupo" - :delete-group "Eliminar grupo" - :delete-group-confirmation "Este grupo será eliminado de tus grupos. Esto no afectará tus contactos" - :delete-group-prompt "Esto no afectará tus contactos" - :contact-s {:one "contacto" - :other "contactos"} - ;;participants - - ;;protocol - :received-invitation "recibió invitación de chat" - :removed-from-chat "te removió del chat" - :left "salió" - :invited "invitado" - :removed "eliminado" - :You "Tú" - - ;;new-contact - :add-new-contact "Agregar nuevo contacto" - :scan-qr "Escanear QR" - :name "Nombre" - :address-explication "Tal vez aquí debería haber algún texto explicando qué es una dirección y dónde encontrarla" - :enter-valid-public-key "Por favor ingresa una llave pública o escanea un código QR" - :contact-already-added "El contacto ya ha sido agregado" - :can-not-add-yourself "No puedes agregarte a ti mismo" - :unknown-address "Dirección desconocida" - - ;;login - :connect "Conectar" - :address "Dirección" - :password "Contraseña" - :sign-in-to-status "Regístrate a Status" - :sign-in "Regístrate" - :wrong-password "Contraseña incorrecta" - - ;;recover - :passphrase "Frase de contraseña" - :recover "Recuperar" - :twelve-words-in-correct-order "12 palabras en orden correcto" - - ;;accounts - :recover-access "Recuperar acceso" - :create-new-account "Crear nueva cuenta" - - ;;wallet-qr-code - :done "Listo" - :main-wallet "Wallet principal" - - ;;validation - :invalid-phone "Número de teléfono incorrecto" - :amount "Cantidad" - - ;;transactions - :confirm "Confirmar" - :transaction "Transacción" - :status "Estado" - :recipient "Receptor" - :to "Para" - :from "De" - :data "Datos" - :got-it "Entendido" - - ;;:webview - :web-view-error "oops, error"}) diff --git a/src/status_im/translations/fi.cljs b/src/status_im/translations/fi.cljs deleted file mode 100644 index ed4914c5841..00000000000 --- a/src/status_im/translations/fi.cljs +++ /dev/null @@ -1,238 +0,0 @@ -(ns status-im.translations.fi) - -(def translations - {;;common - :members-title "Jäsenet" - :not-implemented "!ei toteutettu" - :chat-name "Keskustelun nimi" - :notifications-title "Ilmoitukset ja äänet" - :offline "Poissa" - :search-for "Etsi..." - :cancel "Peruuta" - :next "Seuraava" - :type-a-message "Kirjoita viesti..." - :type-a-command "Aloita komennon kirjoittaminen..." - :error "Virhe" - - :camera-access-error "Myöntääksesi vaadittu kameran käyttölupa, siirry järjestelmäasetuksiin ja varmista, että Status > Kamera on valittu." - :photos-access-error "Myöntääksesi vaadittu kuvien käyttölupa, siirry järjestelmäasetuksiin ja varmista, että Status > Kuvat on valittu." - - ;;drawer - :switch-users "Vaihda käyttäjää" - :current-network "Nykyinen verkko" - - ;;chat - :is-typing "kirjoittaa" - :and-you "ja sinä" - :search-chat "Etsi keskustelusta" - :members {:one "1 käyttäjä" - :other "{{count}} käyttäjää" - :zero "ei käyttäjiä"} - :members-active {:one "1 käyttäjä" - :other "{{count}} käyttäjää" - :zero "ei käyttäjiä"} - :public-group-status "Julkinen" - :active-online "Linjalla" - :active-unknown "Ei tiedossa" - :available "Saatavilla" - :no-messages "Ei viestejä" - :suggestions-requests "Pyynnöt" - :suggestions-commands "Komennot" - :faucet-success "Faucet pyyntö vastaanotettu" - :faucet-error "Faucet pyyntö virhe" - - ;;sync - :sync-in-progress "Synkronoidaan..." - :sync-synced "Synkronoitu" - - ;;messages - :status-sending "Lähetetään" - :status-pending "Odottaa" - :status-sent "Lähetetty" - :status-seen-by-everyone "Kaikki nähneet" - :status-seen "Nähty" - :status-delivered "Toimitettu" - :status-failed "Epäonnistui" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "sekunti" - :other "sekuntia"} - :datetime-minute {:one "minuutti" - :other "minuuttia"} - :datetime-hour {:one "tunti" - :other "tuntia"} - :datetime-day {:one "päivä" - :other "päivää"} - :datetime-ago "sitten" - :datetime-yesterday "eilen" - :datetime-today "tänään" - - ;;profile - :profile "Profiili" - :edit-profile "Muokkaa profiilia" - :message "Viesti" - :not-specified "Ei määritelty" - :public-key "Julkinen avain" - :phone-number "Puhelinnumero" - :update-status "Päivitä tila..." - :add-a-status "Lisää tila..." - :status-prompt "Luo tila, jonka avulla ihmiset tietävät mitä asioita tarjoat. Voit myös käyttää #hashtag-merkintöjä." - :add-to-contacts "Lisää kontakteihin" - :in-contacts "Kontakteissa" - :remove-from-contacts "Poista kontakteista" - :start-conversation "Aloita keskustelu" - :send-transaction "Lähetä tapahtuma" - - ;;make_photo - :image-source-title "Profiilikuva" - :image-source-make-photo "Kuvakaappaus" - :image-source-gallery "Valitse galleriasta" - - ;;sharing - :sharing-copy-to-clipboard "Kopioi leikepöydälle" - :sharing-share "Jaa..." - :sharing-cancel "Peruuta" - - :browsing-title "Selaa" - :browsing-open-in-web-browser "Avaa selaimessa" - :browsing-cancel "Peruuta" - - ;;sign-up - :contacts-syncronized "Yhteystietosi ovat synkronoitu" - :confirmation-code (str "Kiitos! Olemme lähettäneet sinulle tekstiviestin, jossa on vahvistus " - "koodi. Ole hyvä ja anna koodi vahvistaaksesi puhelinnumerosi") - :incorrect-code (str "Virheellinen koodi, ole hyvä ja yritä uudelleen") - :phew-here-is-your-passphrase "*Phew* se oli vaikeaa, tässä on tunnuslauseesi, *kirjoita tämä ylös ja pidä tallessa!* Tarvitset sitä palauttaaksesi tilisi." - :here-is-your-passphrase "Tässä on tunnuslauseesi, *kirjoita tämä ylös ja pidä tallessa!* Tarvitset sitä palauttaaksesi tilisi." - :phone-number-required "Napauta tähän syöttääksesi puhelinnumerosi niin löydän ystäväsi" - :shake-your-phone "Löysitkö vian tai onko sinulla ehdotus? Ravista puhelintasi!" - :intro-status "Keskustele kanssani määrittääksesi tai muuttaaksesi tilisi asetuksia!" - :intro-message1 "Tervetuloa Statukseen\nNapauta tätä viestiä asettaaksesi salasanasi ja alkuun pääsemiseksi!" - :account-generation-message "Ole hyvä ja odota hetki, käynnistän kvanttitietokoneeni tilisi luomiseksi!" - :move-to-internal-failure-message "Meidän on siirrettävä joitain tärkeitä tiedostoja ulkoisesta sisäiseen tallennustilaan. Tätä varten tarvitsemme luvan. Emme tule käyttämään ulkoista tallennustilaa tulevissa versioissa." - :debug-enabled "Debug-palvelin käynnistetty! Löytääksesi palvelimen suorita samaan verkkoon kuuluvalta tietokoneeltasi *status-dev-cli scan*." - - ;;phone types - :phone-e164 "Kansainvälinen 1" - :phone-international "Kansainvälinen 2" - :phone-national "Kansallinen" - :phone-significant "Merkittävä" - - ;;chats - :chats "Keskustelut" - :delete-chat "Poista keskustelu" - :new-group-chat "Uusi ryhmäkeskustelu" - :new-public-group-chat "Liity julkiseen keskusteluun" - :edit-chats "Muokkaa keskusteluja" - :search-chats "Etsi keskusteluja" - :empty-topic "Tyhjä otsikko" - :topic-format "Virheellinen muoto [a-z0-9\\-]+" - :public-group-topic "Otsikko" - - ;;discover - :discover "Löydä" - :none "Tyhjä" - :search-tags "Kirjoita hakutunnisteesi tähän" - :popular-tags "Suositut tunnisteet" - :recent "Uusimmat" - :no-statuses-discovered "Ei hakutuloksia" - :no-statuses-found "Ei hakutuloksia" - - ;;settings - :settings "Asetukset" - - ;;contacts - :contacts "Yhteystiedot" - :new-contact "Uusi yhteystieto" - :delete-contact "Poista yhteystieto" - :delete-contact-confirmation "Tämä yhteystieto poistetaan yhteystiedoista" - :remove-from-group "Poista ryhmästä" - :edit-contacts "Muokkaa yhteystietoja" - :search-contacts "Etsi yhteystietoja" - :contacts-group-new-chat "Aloita uusi keskustelu" - :choose-from-contacts "Valitse yhteystiedoista" - :no-contacts "No contacts yet" - :show-qr "Näytä QR" - :enter-address "Syötä osoite" - :more "lisää" - - ;;group-settings - :remove "Poista" - :save "Tallenna" - :delete "Poista" - :clear-history "Tyhjennä historia" - :mute-notifications "Mykistä ilmoitukset" - :leave-chat "Poistu keskustelusta" - :chat-settings "Keskustelun asetukset" - :edit "Muokkaa" - :add-members "Lisää käyttäjiä" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Uusi ryhmä" - :reorder-groups "Järjestele ryhmät uudelleen" - :edit-group "Muokkaa ryhmää" - :delete-group "Poista ryhmä" - :delete-group-confirmation "Tämä ryhmä poistetaan ryhmistäsi. Tämä ei vaikuta yhteystietoihin" - :delete-group-prompt "Tämä ei vaikuta yhteystietoihin" - :contact-s {:one "yhteystieto" - :other "yhteystietoa"} - ;;participants - - ;;protocol - :received-invitation "poista keskustelukutsu" - :removed-from-chat "poista itsesi keskustelusta" - :left "poistui" - :invited "kutsuttu" - :removed "poistettu" - :You "Sinä" - - ;;new-contact - :add-new-contact "Lisää uusi yhteystieto" - :scan-qr "Skannaa QR" - :name "Nimi" - :address-explication "Ehkä tässä pitäisi olla jokin teksti, jossa selitetään, mikä osoite on ja mistä etsiä sitä" - :enter-valid-public-key "Anna voimassaoleva julkinen osoite tai skannaa QR koodi" - :contact-already-added "Tämä yhteystieto on jo lisätty" - :can-not-add-yourself "Et voi lisätä itseäsi" - :unknown-address "Tuntematon osoite" - - ;;login - :connect "Yhdistä" - :address "Osoite" - :password "Salasana" - :sign-in-to-status "Luo Status -tili" - :sign-in "Luo tili" - :wrong-password "Väärä salasana" - - ;;recover - :passphrase "Tunnuslause" - :recover "Palauta" - :twelve-words-in-correct-order "12 sanaa oikeassa järjestyksessä" - - ;;accounts - :recover-access "Palauta käyttöoikeus" - :create-new-account "Luo uusi tili" - - ;;wallet-qr-code - :done "Valmis" - :main-wallet "Ensisijainen Lompakko" - - ;;validation - :invalid-phone "Virheellinen puhelinnumero" - :amount "Määrä" - ;;transactions - :confirm "Vahvista" - :transaction "Tapahtuma" - :status "Tila" - :recipient "Vastaanottaja" - :to "Vastaanottajalle" - :from "Lähettäjältä" - :data "Tieto" - :got-it "Vastaanotettu" - - ;;:webview - :web-view-error "oops, virhe"}) diff --git a/src/status_im/translations/fr.cljs b/src/status_im/translations/fr.cljs deleted file mode 100644 index ab83798b3af..00000000000 --- a/src/status_im/translations/fr.cljs +++ /dev/null @@ -1,378 +0,0 @@ -(ns status-im.translations.fr) - -(def translations - {;;common - :members-title "Membres" - :not-implemented "! Non disponible" - :chat-name "Pseudonyme" - :notifications-title "Notifications et sons" - :offline "Hors ligne" - :search-for "Rechercher..." - :cancel "Annuler" - :next "Suivant" - :open "Ouvrir" - :description "Description" - :enter-url "Entrer l'URL" - :open-dapp "Ouvrir une ÐApp" - :url "URL" - :type-a-message "Saisissez un message..." - :type-a-command "Saisissez une commande..." - :error "Erreur" - :unknown-status-go-error "Erreur status-go inconnue" - :node-unavailable "Aucun noeud ethereum n'est disponible" - :yes "Oui" - :no "Non" - - :camera-access-error "Merci d'aller dans vos paramètres système et de vous assurer que Status > Caméra est sélectionné afin d'accorder la permission d'utiliser votre caméra." - :photos-access-error "Merci d'aller dans vos paramètres système et de vous assurer que Status > Photos est sélectionné afin d'accorder la permission d'utiliser vos photos." - - ;;drawer - :switch-users "Changer d'utilisateur" - :current-network "Réseau actuel" - :logout "Déconnexion" - - ;;chat - :is-typing "écrit..." - :and-you "et vous" - :search-chat "Rechercher un chat" - :members {:one "1 membre" - :other "{{count}} membres" - :zero "Aucun membre"} - :members-active {:one "1 membre, 1 actif" - :other "{{count}} membres, {{count}} actifs" - :zero "Aucun membre"} - :public-group-status "Public" - :active-online "En ligne" - :active-unknown "Inconnu" - :available "Disponible" - :no-messages "Pas de messages" - :suggestions-requests "Demandes" - :suggestions-commands "Commandes" - :faucet-success "La demande de Faucet a été reçue" - :faucet-error "Erreur avec la demande de Faucet" - - ;;sync - :sync-in-progress "Synchronisation en cours..." - :sync-synced "Synchronisé" - - ;;messages - :status-sending "Envoi en cours..." - :status-pending "En attente..." - :status-sent "Envoyé" - :status-seen-by-everyone "Vu par tout le monde" - :status-seen "Vu" - :status-delivered "Délivré" - :status-failed "Échec" - - ;;datetime - :datetime-ago-format "{{ago}} {{number}} {{time-intervals}}" - :datetime-second {:one "seconde" - :other "secondes"} - :datetime-minute {:one "minute" - :other "minutes"} - :datetime-hour {:one "heure" - :other "heures"} - :datetime-day {:one "jour" - :other "jours"} - :datetime-ago "il y a" - :datetime-yesterday "hier" - :datetime-today "aujourd'hui" - - ;;profile - :profile "Profil" - :view-profile "Voir profil" - :edit-profile "Modifier profil" - :main-currency "Devise principale" - :message "Message" - :not-specified "Non spécifié" - :public-key "Clé publique" - :phone-number "Numéro de téléphone" - :update-status "Actualisez votre statut..." - :share-contact-code "Partager mon code contact" - :add-a-status "Ajoutez un statut..." - :status-prompt "Créez un nouveau statut pour partager vos intérêts avec tout le monde. Vous pouvez aussi utiliser les #hashtags." - :add-to-contacts "Ajouter aux contacts" - :in-contacts "Dans les contacts" - :remove-from-contacts "Supprimer des contacts" - :start-conversation "Débuter une conversation" - :send-transaction "Envoyer une transaction" - :testnet-text "Vous êtes connecté au réseau de test {{testnet}}. Surtout n'envoyez jamais de vrai ETH ou SNT vers vos adresses" - :mainnet-text "Vous êtes sur le réseau principal. Vous pouvez envoyer de vrai ETH" - - ;;make_photo - :image-source-title "Photo de profil" - :image-source-make-photo "Capturer" - :image-source-gallery "Sélectionner dans la galerie" - - ;;sharing - :sharing-copy-to-clipboard "Copier vers le presse-papiers" - :sharing-share "Partagez..." - :sharing-cancel "Annuler" - - :browsing-title "Naviguer" - :browsing-open-in-web-browser "Ouvrir dans le navigateur" - :browsing-cancel "Annuler" - - ;;sign-up - :contacts-syncronized "Vos contacts ont été synchronisés" - :confirmation-code (str "Merci ! Nous vous avons envoyé un message texte contenant le code " - "de confirmation. Veuillez fournir ce code pour confirmer votre numéro de téléphone") - :incorrect-code (str "Désolé, le code est incorrect. Veuillez le saisir à nouveau.") - :phew-here-is-your-passphrase "*Ouf* c'était dur, voici votre phrase secrète, *écrivez-la et gardez-la en lieu sûr !* Vous en aurez besoin pour restaurer votre compte." - :here-is-your-passphrase "Voici votre phrase secrète, *écrivez-la et gardez-la en lieu sûr !* Vous en aurez besoin pour restaurer votre compte." - :here-is-your-signing-phrase "Voici votre phrase de signature. Vous l'utiliserez pour vérifier vos signatures. *Ecrivez la et conservez la soigneusement!*" - :phone-number-required "Appuyez ici pour entrer votre numéro de téléphone et je vais trouver vos amis" - :shake-your-phone "Vous avez trouvé un bug ou alors avez une suggestion? ~Secouez~ simplement votre téléphone!" - :intro-status "Chattez avec moi pour configurer votre compte et modifier vos paramètres !" - :intro-message1 "Bienvenue dans Status\nTapez sur ce message pour définir votre mot de passe et commencer !" - :account-generation-message "Donnez-moi une seconde, je dois faire quelques calculs de ouf pour générer votre compte !" - :move-to-internal-failure-message "Nous devons déplacer des fichiers importants depuis un stockage extérieur vers un stockage local. Afin de faire cela, nous avons besoin de votre permission. Nous n'utiliserons plus de stockage extérieur dans nos futures versions." - :debug-enabled "Le serveur Debug a été lancé! Vous pouvez désormais utiliser *status-dev-cli scan* pour trouver le serveur depuis votre ordinateur sur le même réseau." - - ;;phone types - :phone-e164 "International 1" - :phone-international "International 2" - :phone-national "National" - :phone-significant "Significatif" - - ;;chats - :new "Nouveau" - :new-chat "Nouveau chat" - :start-new-chat "Démarrer un chat" - :start-group-chat "Démarrer un groupe" - :invite-friends "Inviter des amis" - :chats "Chats" - :delete-chat "Supprimer chat" - :group-chat "Groupe" - :new-group-chat "Nouveau chat de groupe" - :new-public-group-chat "Rejoindre chat public" - :edit-chats "Modifier chats" - :search-chats "Rechercher chats" - :empty-topic "Sujet indéfini" - :topic-format "Mauvais format [a-z0-9\\-]+" - :public-group-topic "Sujet" - - ;;discover - :discover "Découvrir" - :none "Aucun" - :search-tags "Entrez vos mots clés ici" - :popular-tags "Mots-clés populaires" - :recent "Récent" - :no-statuses-found "Aucun statut trouvé" - :chat "Chat" - :all "Tout" - :public-chats "Chats publics" - :soon "Prochainement" - :public-chat-user-count "{{count}} personnes" - :dapps "ÐApps" - :dapp-profile "profile ÐApp" - :no-statuses-discovered "Aucun statut découvert" - :no-statuses-discovered-body "Quand quelqu'un écrira un post\nvous le verrez ici." - :no-hashtags-discovered-title "Aucun #hashtag trouvé" - :no-hashtags-discovered-body "Quand un #hashtag devient\npopulaire vous le verrez ici." - - ;;settings - :settings "Paramètres" - - ;;contacts - :contacts "Contacts" - :new-contact "Nouveau contact" - :delete-contact "Supprimer contact" - :delete-contact-confirmation "Ce contact sera supprimé de vos contacts" - :remove-from-group "Supprimer ce groupe" - :edit-contacts "Modifier contacts" - :search-contacts "Rechercher contacts" - :contacts-group-new-chat "Démarrer un nouveau chat" - :choose-from-contacts "Ajouter depuis mes contacts" - :no-contacts "Pas encore de contacts" - :show-qr "Afficher le QR" - :enter-address "Entrer Adresse" - :more "plus" - - ;;group-settings - :remove "Retirer" - :save "Sauvegarder" - :delete "Supprimer" - :clear-history "Effacer l'historique" - :mute-notifications "Couper le son des notifications" - :leave-chat "Quitter le chat" - :leave-group-chat "Quitter le groupe" - :chat-settings "Paramètres de chat" - :edit "Modifier" - :add-members "Ajouter des membres" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nouveau groupe" - :reorder-groups "Réarranger les groupes" - :edit-group "Modifier groupe" - :delete-group "Supprimer le groupe" - :delete-group-confirmation "Ce groupe sera enlevé de vos groupes. Cela n'affectera pas vos contacts" - :delete-group-prompt "Cela n'affectera pas vos contacts" - :contact-s {:one "contact" - :other "contacts"} - - ;;protocol - :received-invitation "invitation de chat reçue" - :removed-from-chat "vous avez été retiré du chat de groupe" - :left "est parti(e)" - :invited "invité" - :removed "supprimé" - :You "Vous" - - ;;new-contact - :add-new-contact "Ajouter un nouveau contact" - :scan-qr "Scanner le QR" - :name "Nom" - :address-explication "Peut-être qu'ici devrait se trouver un texte expliquant ce qu'est une adresse et où la chercher" - :enter-valid-public-key "Veuillez saisir une clé publique valide ou scanner un code QR" - :contact-already-added "Le contact a déjà été ajouté" - :can-not-add-yourself "Vous ne pouvez pas vous ajouter vous-même" - :unknown-address "Adresse inconnue" - - ;;login - :connect "Connecter" - :address "Adresse" - :password "Mot de passe" - :sign-in-to-status "Se connecter à Status" - :sign-in "Se connecter" - :wrong-password "Mot de passe incorrect" - :enter-password "Saisir le mot de passe" - - ;;recover - :passphrase "Phrase secrète" - :recover "Restaurer" - :twelve-words-in-correct-order "12 mots dans l'ordre" - - ;;accounts - :recover-access "Restaurer l'accès" - :create-new-account "Créer un nouveau compte" - - ;;wallet-qr-code - :done "Terminé" - - ;;validation - :invalid-phone "Numéro de téléphone invalide" - :amount "Montant" - - ;;transactions - :confirm "Confirmer" - :transaction "Transaction" - :unsigned-transaction-expired "La transaction non signée a expiré" - :status "Status" - :recipient "Destinataire" - :to "à" - :from "de" - :data "Données" - :got-it "Compris" - :block "Bloc" - :hash "Hash" - :gas-limit "limite Gas" - :gas-price "prix Gas" - :gas-used "Gas utilisé" - :cost-fee "Coût/Frais" - :nonce "Nonce" - :confirmations "Confirmations" - :confirmations-helper-text "Veuillez attendre au moins 12 confirmations pour être sûr que votre transaction a bien été définitivement validée" - :copy-transaction-hash "Copier le hash de la transaction" - :open-on-etherscan "Ouvrir sur Etherscan.io" - - ;;webview - :web-view-error "Oops, erreur" - - ;;testfairy warning - :testfairy-title "Avertissement!" - :testfairy-message "Vous utilisez une application installée à partir d'une version de développement. À des fins de test, cette version inclut l'enregistrement de session si la connexion Wi-Fi est utilisée. Toutes vos interactions avec cette application sont sauvegardées (vidéo et logs) et pourraient être utilisées par notre équipe de développement pour enquêter sur d'éventuels problèmes. Les vidéos / logs enregistrés n'incluent pas vos mots de passe. L'enregistrement est effectué uniquement si l'application est installée à partir d'une version instable (nightly builds). Rien n'est enregistré si l'application est installée depuis PlayStore ou TestFlight." - - ;; wallet - :wallet "Portefeuille" - :wallets "Portefeuilles" - :your-wallets "Vos portefeuilles" - :main-wallet "Portefeuille principal" - :wallet-error "Erreur survenue lors du chargement de données" - :wallet-send "Envoyé" - :wallet-request "Requête" - :wallet-exchange "Echange" - :wallet-assets "Actifs" - :wallet-add-asset "Ajouter un actif" - :wallet-total-value "Valeur totale" - :wallet-settings "Paramètres du portefeuille" - :wallet-manage-assets "Gérer les actifs" - :signing-phrase-description "Signez la transaction en saisissant votre mot de passe. Assurez-vous que les mots ci-dessus correspondent à votre phrase de signature secrète" - :wallet-insufficient-funds "Fonds insuffisants" - :request-transaction "Effectuer une transaction" - :receive-transaction "Recevoir une transaction" - :new-transaction "Nouvelle Transaction" - :transaction-history "Historique des transactions" - - :send-request "Envoyer la requête" - :share "Partager" - :eth "ETH" - :currency "Devise" - :usd-currency "USD" - :transactions "Transactions" - :transaction-details "Détails de la transaction" - :transaction-failed "Transaction échouée" - :transactions-sign "Signer" - :transactions-sign-all "Tout signer" - :transactions-sign-transaction "Signer la transaction" - :transactions-sign-later "Signer ultérieurement" - :transactions-delete "Supprimer la transaction" - :transactions-delete-content "La transaction va être enlevée de la liste 'Non-Signées'" - :transactions-history "Historique" - :transactions-unsigned "Non-Signées" - :transactions-history-empty "Aucune transaction dans votre historique pour le moment" - :transactions-unsigned-empty "Vous n'avez aucune transaction non signée" - :transactions-filter-title "Filtrer l'historique" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Type" - :transactions-filter-select-all "Tout sélectionner" - :view-transaction-details "Voir le détail de la transaction" - :transaction-description "Veuillez attendre au moins 12 confirmations pour être sûr que votre transaction a bien été définitivement validée" - :transaction-sent "Transaction envoyée" - :transaction-moved-text "La transaction va rester dans la liste 'Non-signées' pendant 5 mins" - :transaction-moved-title "Transaction déplacée" - :sign-later-title "Voulez vous signer la transaction ultérieurement?" - :sign-later-text "Vous pourrez signer la transaction dans l'historique des transactions" - :not-applicable "Ne s'applique pas aux transactions non signées" - - ;; Wallet Send - :wallet-choose-recipient "Choisir le destinataire" - :wallet-choose-from-contacts "Choisir à partir des Contacts" - :wallet-address-from-clipboard "Utiliser l'adresse du presse-papiers" - :wallet-invalid-address "Adresse invalide: \n {{data}}" - :wallet-browse-photos "Parcourir les Photos" - :validation-amount-invalid-number "Le montant n'est pas un nombre valide" - :validation-amount-is-too-precise "Le montant est trop précis. La plus petite unité que vous pouvez saisir est 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Nouveau réseau" - :add-network "Ajouter un réseau" - :add-new-network "Ajouter un nouveau réseau" - :existing-networks "Réseaux existants" - :existing-wnodes "Serveurs de mail existants" - :add-json-file "Ajouter un fichier JSON" - :paste-json-as-text "Coller un JSON comme texte" - :paste-json "Coller un JSON" - :specify-rpc-url "Préciser une URL RPC" - :edit-network-config "Modifier la configuration réseau" - :connected "Connecté" - :process-json "Interpréter le JSON" - :error-processing-json "Erreur survenue lors de l'interprétation du JSON" - :rpc-url "URL RPC" - :network "Réseau" - :remove-network "Supprimer le réseau" - :offline-messaging-settings "Configuration de la messagerie hors-ligne" - :network-settings "Paramètres réseau" - :edit-network-warning "Soyez prudent, la modification des données réseau pourrait rendre ce réseau inutilisable pour vous" - :connecting-requires-login "Il faut s'identifier pour se connecter à un nouveau réseau" - :close-app-title "Avertissement!" - :close-app-content "L'application va s'arrêter et se fermer. Lors de la prochaine ouverture, le réseau sélectionné sera utilisé" - :close-app-button "Confirmer" - - ;; browser - :browser "Navigateur" - :enter-dapp-url "Entrer l'URL d'une ÐApp" - :selected-dapps "ÐApps sélectionnées"}) diff --git a/src/status_im/translations/fr_ch.cljs b/src/status_im/translations/fr_ch.cljs deleted file mode 100644 index f7a0fbfd4b6..00000000000 --- a/src/status_im/translations/fr_ch.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.fr-ch) - -(def translations - {;;common - :members-title "Membres" - :not-implemented "!pas mis en place" - :chat-name "Nom de chat" - :notifications-title "Notifications et sons" - :offline "Hors ligne" - - ;;drawer - :switch-users "Changer d'utilisateur" - - ;;chat - :is-typing "est en train de taper" - :and-you "et vous" - :search-chat "Chercher dans une conversation" - :members {:one "1 membre" - :other "{{count}} membres" - :zero "pas de membres"} - :members-active {:one "1 membre, 1 actif" - :other "{{count}} membres, {{count}} actif(s)" - :zero "pas de membres"} - :active-online "En ligne" - :active-unknown "Inconnu" - :available "Disponible" - :no-messages "Pas de messages" - :suggestions-requests "Demandes" - :suggestions-commands "Commandes" - - ;;sync - :sync-in-progress "En cours de synchronisation..." - :sync-synced "Synchronisé" - - ;;messages - :status-sending "Envoi en cours" - :status-pending "En attendant" - :status-sent "Envoyé" - :status-seen-by-everyone "Vu par tous" - :status-seen "Vu" - :status-delivered "Livré" - :status-failed "Echoué" - - ;;datetime - :datetime-second {:one "seconde" - :other "secondes"} - :datetime-minute {:one "minute" - :other "minutes"} - :datetime-hour {:one "heure" - :other "heures"} - :datetime-day {:one "jour" - :other "jours"} - :datetime-ago "avant" - :datetime-yesterday "hier" - :datetime-today "aujourd'hui" - - ;;profile - :profile "Profil" - :message "Message" - :not-specified "Non spécifié" - :public-key "Clé publique" - :phone-number "Numéro de téléphone" - :add-to-contacts "Ajouter aux contacts" - - ;;make_photo - :image-source-title "Image du profil" - :image-source-make-photo "Capture" - :image-source-gallery "Sélectionner dans la galerie" - - ;;sign-up - :contacts-syncronized "Vos contacts ont été synchronisés" - :confirmation-code (str "Merci! Nous vous avons envoyé un message de texte avec un code " - "de confirmation. Veuillez fournir ce code pour confirmer votre numéro de téléphone") - :incorrect-code (str "Désolé, le code est incorrect, veuillez réessayer") - :phew-here-is-your-passphrase "*Pffff* c'était dur, voici votre phrase de passe, *notez-la et gardez-la en sécurité!* Vous en aurez besoin pour récupérer votre compte." - :here-is-your-passphrase "Voici votre phrase de passe, *notez-la et gardez-la en sécurité!* Vous en aurez besoin pour récupérer votre compte." - :phone-number-required "Tapez ici pour saisir votre numéro de téléphone et je trouverai vos amis" - :intro-status "Chattez avec moi pour configurer votre compte et changer vos paramètres !" - :intro-message1 "Bienvenue dans le statut\nTapez ce message pour établir votre mot de passe et vous lancer !" - :account-generation-message "Une seconde svp, je dois lancer quelques formules magiques pour générer votre compte !" - - ;;chats - :chats "Chats" - :new-group-chat "Nouveau chat de groupe" - - ;;discover - :discover "Découverte" - :none "Aucun" - :search-tags "Tapez vos clés de recherche ici" - :popular-tags "Clés populaires" - :recent "Récent" - :no-statuses-discovered "Aucun statut trouvé" - - ;;settings - :settings "Paramètres" - - ;;contacts - :contacts "Contacts" - :new-contact "Nouveau Contact" - :contacts-group-new-chat "Lancer un nouveau chat" - :no-contacts "Pas encore de contacts" - :show-qr "Montrer QR" - - ;;group-settings - :remove "Supprimer" - :save "Sauvegarder" - :clear-history "Effacer l'historique" - :chat-settings "Paramètres de chat" - :edit "Modifier" - :add-members "Ajouter des membres" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "reçu une invitation à un chat" - :removed-from-chat "vous a supprimé du chat de groupe" - :left "restant" - :invited "invité" - :removed "supprimé" - :You "Vous" - - ;;new-contact - :add-new-contact "Ajouter un nouveau contact" - :scan-qr "Scannner QR" - :name "Nom" - :address-explication "On pourrait mettre ici un texte qui explique ce qu'est une adresse et comment la rechercher" - :contact-already-added "Le contact a déjà été ajouté" - :can-not-add-yourself "Vous ne pouvez pas vous ajouter vous-même" - :unknown-address "Adresse inconnue" - - ;;login - :connect "Se connecter" - :address "Adresse" - :password "Mot de passe" - :wrong-password "Mauvais mot de passe" - - ;;recover - :passphrase "Phrase de passe" - :recover "Restaurer" - - ;;accounts - :recover-access "Récupérer l'accès" - - ;;wallet-qr-code - :done "Terminé" - :main-wallet "Portefeuille principal" - - ;;validation - :invalid-phone "Numéro de téléphone non valable" - :amount "Montant" - ;;transactions - :status "Statut" - :recipient "Destinataire" - - ;;:webview - :web-view-error "oups, erreur" - - :confirm "Confirmer" - :phone-national "National" - :public-group-topic "Sujet" - :debug-enabled "Le serveur de débogage a été lancé ! Vous pouvez maintenant ajouter votre Dapp en exécutant *status-dev-cli scan* depuis votre ordinateur" - :new-public-group-chat "Rejoindre le chat public" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Annuler" - :twelve-words-in-correct-order "12 mots dans le bon ordre" - :remove-from-contacts "Retirer des contacts" - :delete-chat "Supprimer la discussion" - :edit-chats "Modifier les discussions" - :sign-in "S'identifier" - :create-new-account "Créer un nouveau compte" - :sign-in-to-status "S'identifier vers le statut" - :got-it "J'ai compris" - :move-to-internal-failure-message "Nous devons déplacer des fichiers importants depuis le stockage externe vers le stockage interne. Pour cela, nous avons besoin de votre permission. Nous n'utiliserons pas le stockage externe dans les versions futures." - :edit-group "Modifier le groupe" - :delete-group "Supprimer le groupe" - :browsing-title "Parcourir" - :reorder-groups "Réorganiser les groupes" - :browsing-cancel "Annuler" - :faucet-success "La requête du site faucet a été reçue" - :choose-from-contacts "Choisir parmi les contacts" - :new-group "Nouveau groupe" - :phone-e164 "International 1" - :remove-from-group "Retirer du groupe" - :search-contacts "Rechercher des contacts" - :transaction "Transaction" - :public-group-status "Public" - :leave-chat "Quitter le chat" - :start-conversation "Débuter une conversation" - :topic-format "Mauvais format [a-z0-9\\-]+" - :enter-valid-public-key "Merci de renseigner une clé publique valide ou de scanner un code QR" - :faucet-error "Erreur de la requête du site faucet" - :phone-significant "Important" - :search-for "Rechercher…" - :sharing-copy-to-clipboard "Copier vers le presse-papiers" - :phone-international "International 2" - :enter-address "Renseigner l'adresse" - :send-transaction "Envoyer la transaction" - :delete-contact "Supprimer le contact" - :mute-notifications "Désactiver le son des notifications" - :contact-s {:one "contact" - :other "contacts"} - :next "Suivant" - :from "De" - :search-chats "Rechercher dans les discussions" - :in-contacts "Dans les contacts" - - :sharing-share "Partagez…" - :type-a-message "Tapez un message…" - :type-a-command "Commencez à taper une commande…" - :shake-your-phone "Vous trouvez un bug ou vous avez une suggestion ? ~Secouez~ simplement votre natel !" - :status-prompt "Créez un statut afin d'aider les gens à savoir ce que vous offrez. Vous pouvez également utiliser les #hashtags." - :add-a-status "Ajoutez un statut…" - :error "Erreur" - :edit-contacts "Modifier les contacts" - :more "plus" - :cancel "Annuler" - :no-statuses-found "Aucun statut trouvé" - :browsing-open-in-web-browser "Ouvrir dans le navigateur" - :delete-group-prompt "Cela n'affectera pas les contacts" - :edit-profile "Modifier le profil" - :empty-topic "Sujet vide" - :to "À" - :data "Données"}) diff --git a/src/status_im/translations/fy.cljs b/src/status_im/translations/fy.cljs deleted file mode 100644 index 8b0d41784c3..00000000000 --- a/src/status_im/translations/fy.cljs +++ /dev/null @@ -1,238 +0,0 @@ -(ns status-im.translations.fy) - -(def translations - {;;common - :members-title "Leden" - :not-implemented "!net ymplemintearre" - :chat-name "Chatnamme" - :notifications-title "Notifikaasjes en gelûden " - :offline "Offline" - :search-for "Sykje om..." - :cancel "Ôfbrekke" - :next "Folgjende" - :type-a-message "Typ in berjocht..." - :type-a-command "Begjin mei in kommando te typen..." - :error "Flater" - - :camera-access-error "Om de kamera tastimming te jaan, gea nei dyn systeem ynstellings en wêz wis dat Status > Camera selektearre is." - :photos-access-error "Om ús tastimming te jaan oan dyn foto's, gea nei dyn systeem ynstellings en wêz wis dat Status > Foto's selektearre is." - - ;;drawer - :switch-users "Skeakel tusken brûkers " - :current-network "Aktuele netwurk" - - ;;chat - :is-typing "typt" - :and-you "en do" - :search-chat "Sykje yn de chat" - :members {:one "1 lid" - :other "{{count}} leden" - :zero "gjin leden"} - :members-active {:one "1 lid, 1 aktive" - :other "{{count}} leden, {{count}} aktive" - :zero "gjin leden"} - :public-group-status "Iepenbier" - :active-online "Online" - :active-unknown "Ûnbekend" - :available "Beskikber" - :no-messages "Gjin berjochten" - :suggestions-requests "Oanfreegje" - :suggestions-commands "Kommando’s" - :faucet-success "Faucet fersyk is ûntfongen" - :faucet-error "Faucet fersyk flater" - - ;;sync - :sync-in-progress "Wurdt syngronisearre ..." - :sync-synced "Syngronisearre" - - ;;messages - :status-sending "Wurdt ferstjoerd " - :status-pending "Yn behanneling" - :status-sent "Ferstjoerd" - :status-seen-by-everyone "Troch eltsenien sjoen" - :status-seen "Sjoen" - :status-delivered "Besoarge" - :status-failed "Mislearre" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "sekonde " - :other " sekonden"} - :datetime-minute {:one "minút" - :other "minuten"} - :datetime-hour {:one "oere" - :other "oeren"} - :datetime-day {:one "dei" - :other "dagen"} - :datetime-ago "lyn" - :datetime-yesterday "juster" - :datetime-today "hjoed" - - ;;profile - :profile "Profyl" - :edit-profile "Profyl oanpasse" - :message "Berjocht" - :not-specified "Net opjûn" - :public-key "Iepenbiere kaai" - :phone-number "Telefoannûmer" - :update-status "Fernij dyn status..." - :add-a-status "Foegje in status ta..." - :status-prompt "Foeg in status ta om oare minsken te litte witte wast do oanbiedest, do kinst ek #hashtags gebroeke." - :add-to-contacts "Oan kontaktpersoanen tafoegje" - :in-contacts "Yn kontakten" - :remove-from-contacts "Fuortsmite út kontakten" - :start-conversation "Start konversaasje" - :send-transaction "Stjoer transaksje" - - ;;make_photo - :image-source-title "Profylfoto" - :image-source-make-photo "Foto nimme" - :image-source-gallery "Kies út galerij" - - ;;sharing - :sharing-copy-to-clipboard "Nei it klamboerd ta kopiearje" - :sharing-share "Diele..." - :sharing-cancel "Ôfbrekke" - - :browsing-title "Blêdzje" - :browsing-open-in-web-browser "Iepenje yn web blêder" - :browsing-cancel "Ôfbrekke" - - ;;sign-up - :contacts-syncronized "Dyn kontaktpersoanen binne syngroniseard" - :confirmation-code (str "Tiige tank! Wy ha dy in sms stjoerd mei in befêstigingskoade" - ". Jou dyn koade op om dyn telefoannûmer te befêstigje") - :incorrect-code (str "Sorry, de koade wie ferkeard, fier hem opnei yn") - :phew-here-is-your-passphrase "*Poah* dat wie dreech, hjir is dyn wachtsin, *skriuw dizze op en bewarje him goed!* Do silst him nedich ha om dyn account te herstelle." - :here-is-your-passphrase "Dat wie dreech, hjir is dyn wachtsin, *skriuw dizze op en bewarje him goed!* Do silst him nedich ha om dyn account te herstelle." - :phone-number-required "Tik hjir om dyn telefoannûmer yn te fieren, dan sykje ik dyn freonen" - :shake-your-phone "Hast in bug fûn of hast in suggestje? ~Skodzje~ dyn telefoan!" - :intro-status "Chat mei my om dyn account yn te stellen en dyn ynstellingen te wizigjen!" - :intro-message1 "Wolkom by Status\nTik op dit berjocht om dyn wachtwurd yn te stellen en oan de slach te gean!" - :account-generation-message "Jou my in momintsje, ik moat wat yngewikkelde berekkeningen dwaan om dyn account oan te meitsjen !" - :move-to-internal-failure-message "We need to move some important files from external to internal storage. To do this, we need your permission. We won't be using external storage in future versions." - :debug-enabled "Debug server has been launched! You can now execute *status-dev-cli scan* to find the server from your computer on the same network." - - ;;phone types - :phone-e164 "Ynternasjonaal 1" - :phone-international "Ynternasjonaal 2" - :phone-national "Lanlik" - :phone-significant "Significant" - - ;;chats - :chats "Chats" - :delete-chat "Chat fuortsmite" - :new-group-chat "Nije groepchat" - :new-public-group-chat "Doch mei yn iepenbare chat" - :edit-chats "Bewurkje chats" - :search-chats "Sykje chats" - :empty-topic "Leech ûnderwerp" - :topic-format "Ferkearde yndieling [a-z0-9\\-]+" - :public-group-topic "Ûnderwerp" - - ;;discover - :discover "Ûntdekking" - :none "Gjin" - :search-tags "Typ hjir dyn syktags" - :popular-tags "Populêre tags" - :recent "Koartlyn" - :no-statuses-discovered "Gjin statussen ûntdutsen" - :no-statuses-found "Gjin statussen fûn" - - ;;settings - :settings "Ynstellingen" - - ;;contacts - :contacts "Kontaktpersoanen" - :new-contact "Nije kontaktpersoanen" - :delete-contact "Wiskje kontakt" - :delete-contact-confirmation "Dit kontakt sil fuortsmiten wurde fan jo kontakten" - :remove-from-group "Fuortsmite út groep" - :edit-contacts "Bewurkje kontakten" - :search-contacts "Sykje kontakten" - :contacts-group-new-chat "Start neie chat" - :choose-from-contacts "Kies út de kontakten" - :no-contacts "Noch gjin kontaktpersoanen" - :show-qr "Toan QR" - :enter-address "Fier adres yn" - :more "mear" - - ;;group-settings - :remove "Fuortsmite" - :save "Opslaan" - :delete "Wiskje" - :clear-history "Wis skiednis" - :mute-notifications "Mute notifikaasjes" - :leave-chat "Ferlit chat" - :chat-settings "Chatynstellingen" - :edit "Bewurkje" - :add-members "Foegje leden ta" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nije groep" - :reorder-groups "Reorder groepen" - :edit-group "Bewurkje group" - :delete-group "Wiskje groep" - :delete-group-confirmation "Dizze groep sille fuortsmiten wurde fan dyn groepen. Dit sil gjin ynfloed ha mei jo kontakten" - :delete-group-prompt "Dit sil gjin ynfloed ha mei jo kontakten" - :contact-s {:one "kontakt" - :other "kontakten"} - ;;participants - - ;;protocol - :received-invitation "Chatútnoeging ûntfange" - :removed-from-chat "ferwidere dy út de chat" - :left "gie fuort" - :invited "útnoege" - :removed "ferwidere" - :You "Do" - - ;;new-contact - :add-new-contact "Foegje nij kontaktpersoan ta " - :scan-qr "QR scanne" - :name "Namme" - :address-explication "Maybe here should be some text explaining what an address is and where to look for it" - :enter-valid-public-key "Fear in jildich adres public kaai yn of scan in QR koade" - :contact-already-added "De kontaktpersoan is ol tafoege" - :can-not-add-yourself "Do kinst dysels net tafoegje" - :unknown-address "Ûnbekend adres" - - ;;login - :connect "Ferbine" - :address "Adres" - :password "Wachtwurd" - :sign-in-to-status "Oanmelde by Status" - :sign-in "Ynlogge" - :wrong-password "Ferkeard wachtwurd" - - ;;recover - :passphrase "Wachtsin" - :recover "Herstelle" - :twelve-words-in-correct-order "12 wurden yn de goeie folchoarder" - - ;;accounts - :recover-access "Tagong herstelle" - :create-new-account "Nij akkount oanmeitsje" - - ;;wallet-qr-code - :done "Klear" - :main-wallet "Haadbeurs" - - ;;validation - :invalid-phone "Ûnjildich telefoannûmer" - :amount "Bedrach" - ;;transactions - :confirm "Befestigje" - :transaction "Transaksje" - :status "Status" - :recipient "Ûntfanger" - :to "Ta" - :from "Fan" - :data "Data" - :got-it "Befetsje ik" - - ;;:webview - :web-view-error "oeps, flater"}) diff --git a/src/status_im/translations/he.cljs b/src/status_im/translations/he.cljs deleted file mode 100644 index 2a52f5df9de..00000000000 --- a/src/status_im/translations/he.cljs +++ /dev/null @@ -1,239 +0,0 @@ -(ns status-im.translations.he) - -(def translations - {;;common - :members-title "חבר" - :not-implemented "!עדיין לא יושם" - :chat-name "שם הצ'אט" - :notifications-title "התראות וצלילים" - :offline "לא מחובר" - :search-for "...חפש" - :cancel "בטל" - :next "הבא" - :type-a-message "...הקלד הודעה" - :type-a-command "...הקלד פעולה" - :error "שגיאה" - - :camera-access-error ".כדי להעניק את הרשאת המצלמה הנדרשת, בבקשה, תלכו להגדרות המערכת ותוודאו שהאופציה שנבחרה היא סטטוס > מצלמה" - :photos-access-error ".כדי להעניק את הרשאת התמונות הנדרשת, בבקשה, תלכו להגדרות המערכת ותוודאו שהאופציה שנבחרה היא סטטוס > תמונות" - - ;;drawer - :switch-users "שנה משתמש" - :current-network "Current network" - - ;;chat - :is-typing "מקליד" - :and-you "גם אתה" - :search-chat "חפש בצ'אט" - :members {:one "חבר אחד" - :other "{{count}} חברים" - :zero "אין חברים"} - :members-active {:one "חבר אחד" - :other "{{count}} חברים" - :zero "אין חברים"} - :public-group-status "פומבי" - :active-online "מחובר" - :active-unknown "לא ידוע" - :available "זמין" - :no-messages "אין הודעות" - :suggestions-requests "בקשות" - :suggestions-commands "פקודות" - :faucet-success "בקשת הברז התקבלה" - :faucet-error "שגיאה בבקשת הברז" - - ;;sync - :sync-in-progress "...מסנכרן" - :sync-synced "מסונכרן" - - ;;messages - :status-sending "שולח" - :status-pending "ממתין" - :status-sent "נשלחה" - :status-seen-by-everyone "פומבי" - :status-seen "נראה" - :status-delivered "התקבל" - :status-failed "נכשל" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "שניה" - :other "שניות"} - :datetime-minute {:one "דקה" - :other "דקות"} - :datetime-hour {:one "שעה" - :other "שעות"} - :datetime-day {:one "יום" - :other "ימים"} - :datetime-ago "לפני" - :datetime-yesterday "אתמול" - :datetime-today "היום" - - ;;profile - :profile "פרופיל" - :edit-profile "ערוך פרופיל" - :message "הודעה" - :not-specified "לא מוגדר" - :public-key "מפתח פומבי" - :phone-number "מספר טלפון" - :update-status "...עדכן סטטוס" - :add-a-status "...הוסף סטטוס" - :status-prompt ".#hashtags צור סטטוס כדי ליידע אחרים בקשר לדברים אותם אתה מציע. אתה יכול גם להשתמש ב" - - :add-to-contacts "הוסף לאנשי קשר" - :in-contacts "באנשי קשר" - :remove-from-contacts "הסר מאנשי קשר" - :start-conversation "התחל שיחה" - :send-transaction "שלח עיסקה" - - ;;make_photo - :image-source-title "תמונת פרופיל" - :image-source-make-photo "צלם" - :image-source-gallery "בחר מהגלריה" - - ;;sharing - :sharing-copy-to-clipboard "העתק לשולחן העבודה" - :sharing-share "...שתף" - :sharing-cancel "בטל" - - :browsing-title "דפדף" - :browsing-open-in-web-browser "פתח בדפדפן" - :browsing-cancel "בטל" - - ;;sign-up - :contacts-syncronized "אנשי הקשר שלך סונכרנו" - :confirmation-code (str "תודה! שלחנו לך הודעה עם קוד" - "אישור. אנא ספק את הקוד כדי לאשר את מספר הטלפון שלך") - :incorrect-code (str "סליחה הקוד שגוי, אנא נסה שנית") - :phew-here-is-your-passphrase "פוו זה היה קשה, הנה משפט הקוד שלך, *תרשום אותו ותשמור עליו!* אתה תצטרך אותו כדי לשחזר את המשתמש שלך" - :here-is-your-passphrase "הנה משפט הקוד שלך, *תרשום אותו ותשמור עליו!* אתה תצטרך אותו כדי לשחזר את המשתמש שלך." - :phone-number-required "הקש כאן כדי להזין את מספר הטלפון שלך ואני אמצא את החברים שלך" - :shake-your-phone "!מצאת באג או שיש לך הצעה? פשוט טלטל את הפלאפון" - :intro-status "דבר איתי כדי להסדיר את המשתמש שלך ולשנות את ההגדרות" - :intro-message1 "!ברוך הבא לסטטוס הקש על הודעה זו כדי לייצר סיסמא ולהתחיל" - :account-generation-message "!שניה, אני הולך להשתמש בקצת קסם מתמטי ולייצר את חשבונך" - :move-to-internal-failure-message "אנו זקוקים לאישורך כדי להזיז קבצים חשובים מהזיכרון החיצוני לפנימי. אנחנו לא נשתמש בזיכרון חיצוני בגרסאות עתידיות" - :debug-enabled ".שרת הניפוי הותחל! אתה יכול כעת לבצע *status-dev-cli scan* כדי למצוא את השרת מהמחשב שלך על אותה הרשת" - - ;;phone types - :phone-e164 "בין לאומי 1" - :phone-international "בין לאומי 2" - :phone-national "לאומי" - :phone-significant "משמעותי" - - ;;chats - :chats "צ'אטים" - :delete-chat "מחק צ'אט" - :new-group-chat "צ'אט קבוצתי חדש" - :new-public-group-chat "הצטרף לצ'אט פומבי" - :edit-chats "ערוך צ'אטים" - :search-chats "חפש בצ'אטים" - :empty-topic "נושא ריק" - :topic-format "פורמט שגוי [a-z0-9\\-]+" - :public-group-topic "נושא" - - ;;discover - :discover "גילוי" - :none "אף אחד" - :search-tags "תקליד את תג החיפוש שלך כאן" - :popular-tags "תגים פופולאריים" - :recent "חדש" - :no-statuses-discovered "לא התגלה שום סטטוס" - :no-statuses-found "לא נמצא שום סטטוס" - - ;;settings - :settings "הגדרות" - - ;;contacts - :contacts "אנשי קשר" - :new-contact "איש קשר חדש" - :delete-contact "מחק איש קשר" - :delete-contact-confirmation "המשתמש הזה ימחק מרשימת אנשי הקשר שלך" - :remove-from-group "הסר מהקבוצה" - :edit-contacts "ערוך אנשי קשר" - :search-contacts "חפש באנשי הקשר" - :contacts-group-new-chat "התחל צ'אט חדש" - :choose-from-contacts "בחר מאנשי הקשר" - :no-contacts "אין שום אנשי קשר" - :show-qr "הראה ברקוד" - :enter-address "הזן כתובת" - :more "עוד" - - ;;group-settings - :remove "הסר" - :save "שמור" - :delete "מחק" - :clear-history "נקה היסטוריה" - :mute-notifications "השתק התראות" - :leave-chat "צא מהצ'אט" - :chat-settings "הגדרות צ'אט" - :edit "ערוך" - :add-members "הוסף מספרים" - - ;;commands - :chat-send-eth "{{amount}} אתר" - - ;;new-group - :new-group "קבוצה חדשה" - :reorder-groups "סדר מחדש את הקבוצה" - :edit-group "ערוך את הקבוצה" - :delete-group "מחר את הקבוצה" - :delete-group-confirmation "הקבוצה הזאת תמחק מרשימת הקבוצות. פעולה זאת לא תשפיע על אנשי הקשר" - :delete-group-prompt "זה לא ישפיע על אנשי הקשר" - :contact-s {:one "איש קשר" - :other "אנשי קשר"} - ;;participants - - ;;protocol - :received-invitation "התקבלה הזמנה לצ'אט" - :removed-from-chat "הוסרת מהצ'אט הקבוצתי" - :left "עזב" - :invited "הוזמן" - :removed "הוסר" - :You "אתה" - - ;;new-contact - :add-new-contact "הוסף איש קשר חדש" - :scan-qr "סרוק ברקוד" - :name "שם" - :address-explication "Maybe here should be some text explaining what an address is and where to look for it" - :enter-valid-public-key "אנא הזן קוד פומבי תקף או סרוק ברקוד" - :contact-already-added "איש הקשר כבר קיים" - :can-not-add-yourself "אתה לא יכול להוסיף את עצמך" - :unknown-address "כתובת לא ידועה" - - ;;login - :connect "התחבר" - :address "כתובת" - :password "סיסמא" - :sign-in-to-status "התחבר לסטטוס" - :sign-in "התחבר" - :wrong-password "סיסמא שגויה" - - ;;recover - :passphrase "משפט קוד" - :recover "שחזר" - :twelve-words-in-correct-order "12 מילים בסדר הנכון" - - ;;accounts - :recover-access "שחזר גישה" - :create-new-account "יצר איש קשר חדש" - - ;;wallet-qr-code - :done "הושלם" - :main-wallet "ארנק ראשי" - - ;;validation - :invalid-phone "מספר טלפון שגוי" - :amount "כמות" - ;;transactions - :confirm "אשר" - :transaction "העברות" - :status "סטטוס" - :recipient "מקבל" - :to "ל" - :from "מ" - :data "נתונים" - :got-it "קיבלתי" - - ;;:webview - :web-view-error "אופס,טעות"}) diff --git a/src/status_im/translations/hi.cljs b/src/status_im/translations/hi.cljs deleted file mode 100644 index 0cbc5968226..00000000000 --- a/src/status_im/translations/hi.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.hi) - -(def translations - {;;common - :members-title "सदस्य" - :not-implemented "!कार्यान्वित नहीं" - :chat-name "चैट नाम" - :notifications-title "सूचनाएं और आवाज" - :offline "ऑफ़लाइन" - - ;;drawer - :switch-users "उपयोगकर्ताओं को स्विच करें" - - ;;chat - :is-typing "टाइप कर रहा है" - :and-you "और आप" - :search-chat "चैट खोजें" - :members {:one "1 सदस्य" - :other "{{count}} सदस्य" - :zero "कोई सदस्य नहीं"} - :members-active {:one "1 सदस्य, 1 सक्रिय" - :other "{{count}} सदस्य, {{count}} सक्रिय" - :zero "कोई सदस्य नहीं"} - :active-online "ऑनलाइन" - :active-unknown "अनजान" - :available "उपलब्ध" - :no-messages "कोई संदेश नहीं" - :suggestions-requests "अनुरोध" - :suggestions-commands "कमांड" - - ;;sync - :sync-in-progress "सिंक किया जा रहा है..." - :sync-synced "सिंक में" - - ;;messages - :status-sending "भेज रहे हैं" - :status-pending "विचाराधीन" - :status-sent "भेज दिया" - :status-seen-by-everyone "हर किसी ने देख लिया" - :status-seen "देखा गया" - :status-delivered "सौंप दिया" - :status-failed "विफल रहा" - - ;;datetime - :datetime-second {:one "सेकंड" - :other "सेकंड"} - :datetime-minute {:one "मिनट" - :other "मिनट"} - :datetime-hour {:one "घंटा" - :other "घंटे"} - :datetime-day {:one "दिन" - :other "दिन"} - :datetime-ago "पहले" - :datetime-yesterday "बीता हुआ कल" - :datetime-today "आज" - - ;;profile - :profile "प्रोफाइल" - :message "संदेश" - :not-specified "निर्दिष्ट नहीं" - :public-key "सार्वजनिक कुंजी" - :phone-number "फ़ोन नंबर" - :add-to-contacts "संपर्क में जोड़ें" - - ;;make_photo - :image-source-title "प्रोफ़ाइल छवि" - :image-source-make-photo "प्राप्त करें" - :image-source-gallery "गैलरी से चयन करें" - - ;;sign-up - :contacts-syncronized "आपके संपर्कों को सिंक्रनाइज़ किया गया है" - :confirmation-code (str "धन्यवाद! हमने आपको \"पुष्टि कोड\" के साथ एक टेक्स्ट संदेश भेजा है " - "कृपया अपने फोन नंबर की पुष्टि करने के लिए वह कोड डालें।") - :incorrect-code (str "क्षमा करें, कोड गलत था, कृपया फिर से डालें") - :phew-here-is-your-passphrase "*उफ़्फ़* यह बहुत मुश्किल था, यह रहा आपका पासफ्रेज, *इसे लिख लें और सुरक्षित रखें! * अपने खाते को ठीक करने के लिए आपको इसकी आवश्यकता होगी।" - :here-is-your-passphrase "यह रहा आपका पासफ्रेज, *इसे लिख लें और सुरक्षित रखें! * अपने खाते को ठीक करने के लिए आपको इसकी आवश्यकता होगी।" - :phone-number-required "अपना फोन नंबर दर्ज करने के लिए यहां टैप करें और मैं आपके दोस्तों को ढूंढ निकालूंगा" - :intro-status "अपना खाता सेट करने और अपनी सेटिंग्स बदलने के लिए मेरे साथ चैट करें!" - :intro-message1 "स्टेटस में आपका स्वागत है\n अपना पासवर्ड सेट करने और शुरुआत करने के लिए इस संदेश पर करें!" - :account-generation-message "मुझे एक सेकंड का समय दें, आपका खाता जनरेट करने के लिए मुझे कुछ क्रेजी गणित का प्रयोग करना होगा!" - - ;;chats - :chats "चैट" - :new-group-chat "नई ग्रुप चैट" - - ;;discover - :discover "खोज" - :none "कोई नहीं" - :search-tags "अपने खोज टैग यहां टाइप करें" - :popular-tags "लोकप्रिय टैग" - :recent "नया" - :no-statuses-discovered "कोई स्टेटस नहीं मिला" - - ;;settings - :settings "सेटिंग्स" - - ;;contacts - :contacts "संपर्क" - :new-contact "नया संपर्क" - :contacts-group-new-chat "नई चैट शुरू करें" - :no-contacts "अभी तक कोई संपर्क नहीं" - :show-qr "QR दिखाएं" - - ;;group-settings - :remove "निकालें" - :save "सहेजें" - :clear-history "इतिहास साफ़ करें" - :chat-settings "चैट सेटिंग्स" - :edit "संपादित करें" - :add-members "सदस्य जोड़ें" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "चैट आमंत्रण प्राप्त हुआ" - :removed-from-chat "आपको ग्रुप चैट से निकाल दिया" - :left "बाएं" - :invited "आमंत्रित" - :removed "निकाल दिया" - :You "आप" - - ;;new-contact - :add-new-contact "नया संपर्क जोड़ें" - :scan-qr "QR स्कैन करें" - :name "नाम" - :address-explication "संभवतः यहाँ कुछ टेक्स्ट विवरण होना चाहिए कि पता क्या है और इसे कहाँ खोजा जाए" - :contact-already-added "संपर्क पहले से जोड़ लिया गया है" - :can-not-add-yourself "आप अपने आपको नहीं जोड़ सकते" - :unknown-address "अज्ञात पता" - - ;;login - :connect "कनेक्ट करें" - :address "पता" - :password "पासवर्ड" - :wrong-password "गलत पासवर्ड" - - ;;recover - :passphrase "पासफ्रेज" - :recover "ठीक करें" - - ;;accounts - :recover-access "पहुंच को ठीक करें" - - ;;wallet-qr-code - :done "पूरा हो गया" - :main-wallet "मुख्य वॉलेट" - - ;;validation - :invalid-phone "अमान्य फोन नंबर" - :amount "राशि" - ;;transactions - :status "स्टेटस" - :recipient "प्राप्तकर्ता" - - ;;:webview - :web-view-error "ओह, त्रुटि" - - :confirm "पुष्टि करें" - :phone-national "राष्ट्रीय" - :public-group-topic "विषय" - :debug-enabled "डीबग सर्वर लाँच कर दिया गया है! है। अब आप अपने कम्प्यूटर से *status-dev-cli scan* चलाकर अपने DApp को शामिल कर सकते हैं" - :new-public-group-chat "सार्वजनिक चैट में शामिल हों" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "रद्द करें" - :twelve-words-in-correct-order "सही क्रम में 12 शब्द" - :remove-from-contacts "संपर्कों से हटाएं" - :delete-chat "चैट मिटाएं" - :edit-chats "चैट्स का संपादन करें" - :sign-in "साइन इन" - :create-new-account "नया खाता बनाएं" - :sign-in-to-status "स्टैटस में साइन इन करें" - :got-it "समझ गया" - :move-to-internal-failure-message "हमें बाह्य से आंतरिक स्टोरेज में कुछ जरूरी फाइलें ले जानी हैं। ऐसा करने के लिए हमें आपकी अनुमति चाहिए। हम भावी संस्करणों में बाह्य स्टोरेज का उपयोग नहीं करने वाले हैं।" - :edit-group "समूह का संपादन करें" - :delete-group "समूह मिटाएं" - :browsing-title "ब्राउज़ करें" - :reorder-groups "समूहों को फिर से क्रम में रखें" - :browsing-cancel "रद्द करें" - :faucet-success "फॉसेट का अनुरोध प्राप्त हुआ है" - :choose-from-contacts "संपर्कों में से चुनें" - :new-group "नया समूह" - :phone-e164 "अंतरराष्ट्रीय 1" - :remove-from-group "समूह से हटाएं" - :search-contacts "संपर्क खोजें" - :transaction "लेनदेन" - :public-group-status "सार्वजनिक" - :leave-chat "चैट छोड़ें" - :start-conversation "बातचीत शुरू करें" - :topic-format "गलत फॉर्मैट [a-z0-9\\-]+" - :enter-valid-public-key "कृपया वैध सार्वजनिक कुंजी प्रविष्ट करें या QR कोड स्कैन करें" - :faucet-error "फॉसेट के अनुरोध की त्रुटि" - :phone-significant "उल्लेखनीय" - :search-for "... की खोज करें" - :sharing-copy-to-clipboard "क्लिपबोर्ड में कॉपी करें" - :phone-international "अंतरराष्ट्रीय 2" - :enter-address "पता प्रविष्ट करें" - :send-transaction "लेनदेन भेजें" - :delete-contact "संपर्क मिटाएं" - :mute-notifications "अधिसूचनाएं म्यूट करें" - :contact-s {:one "संपर्क" - :other "संपर्क"} - :next "अगला" - :from "द्वारा" - :search-chats "चैट्स खोजें" - :in-contacts "संपर्कों में" - - :sharing-share "साझा करें..." - :type-a-message "संदेश टाइप करें..." - :type-a-command "आदेश टाइप करना शुरू करें..." - :shake-your-phone "क्या कोई समस्या मिली या सुझाव देना है? बस अपने फोन को ~हिलाएं~!" - :status-prompt "आप जिन चीजों की पेशकश कर रहे हैं उनके बारे में जानने में लोगों की मदद करने के लिए एक स्टैटस बनाएं। आप #हैशटैग्स का उपयोग भी कर सकते हैं।" - :add-a-status "स्टैटस जोड़ें..." - :error "त्रुटि" - :edit-contacts "संपर्कों का संपादन करें" - :more "अधिक" - :cancel "रद्द करें" - :no-statuses-found "कोई स्टैटस नहीं मिले" - :browsing-open-in-web-browser "वेब ब्राउज़र में खोलें" - :delete-group-prompt "इससे संपर्क प्रभावित नहीं होंगे" - :edit-profile "प्रोफाइल का संपादन करें" - :empty-topic "रिक्त विषय" - :to "प्रति" - :data "डेटा"}) diff --git a/src/status_im/translations/hu.cljs b/src/status_im/translations/hu.cljs deleted file mode 100644 index e066e14ecf3..00000000000 --- a/src/status_im/translations/hu.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.hu) - -(def translations - {;;common - :members-title "Tagok" - :not-implemented "!nem végrehajtott" - :chat-name "Csevegés neve" - :notifications-title "Értesítések és hangok" - :offline "Offline" - - ;;drawer - :switch-users "Felhasználók váltása" - - ;;chat - :is-typing "gépel" - :and-you "és te" - :search-chat "Csevegés keresése" - :members {:one "1 tag" - :other "{{count}} tag" - :zero "nincsenek tagok"} - :members-active {:one "1 tag, 1 aktív" - :other "{{count}} tag, {{count}} aktív" - :zero "nincsenek tagok"} - :active-online "Online" - :active-unknown "Ismeretlen" - :available "Elérhető" - :no-messages "Nincsenek üzenetek" - :suggestions-requests "Kérések" - :suggestions-commands "Parancsok" - - ;;sync - :sync-in-progress "Szinkronizálás..." - :sync-synced "Szinkronizálás folyamatban" - - ;;messages - :status-sending "Küldés" - :status-pending "Függőben levő" - :status-sent "Elküldve" - :status-seen-by-everyone "Látva mindenki által" - :status-seen "Látva" - :status-delivered "Kézbesítve" - :status-failed "Nem sikerült" - - ;;datetime - :datetime-second {:one "másodperc" - :other "másodperc"} - :datetime-minute {:one "perc" - :other "perc"} - :datetime-hour {:one "óra" - :other "óra"} - :datetime-day {:one "nap" - :other "nap"} - :datetime-ago "ezelőtt" - :datetime-yesterday "tegnap" - :datetime-today "ma" - - ;;profile - :profile "Profil" - :message "Üzenet" - :not-specified "Nincs megadva" - :public-key "Nyilvános kulcs" - :phone-number "Telefonszám" - :add-to-contacts "Hozzáadás a kapcsolatokhoz" - - ;;make_photo - :image-source-title "Profilkép" - :image-source-make-photo "Rögzítés" - :image-source-gallery "Kiválasztás a galériából" - - ;;sign-up - :contacts-syncronized "Kapcsolataid szinkronizálásra kerültek" - :confirmation-code (str "Köszönjük! Küldtünk neked egy szöveges üzenetet megerősítési " - "kóddal. Kérjük, add meg a kódot telefonszámod megerősítése érdekében") - :incorrect-code (str "Sajnáljuk, hibás kód, kérjük, add meg újból") - :phew-here-is-your-passphrase "*Hűha* ez nehéz volt, de kész a jelmondatod, *írd fel valahova és vigyázz rá!* Szükséged lesz hozzá felhasználói fiókod helyreállításához." - :here-is-your-passphrase "Kész a jelmondatod, *írd fel valahová és őrizd meg!* Szükséged lesz hozzá felhasználói fiókod helyreállításához." - :phone-number-required "Érints ide telefonszámod megadásához és megtalálom a barátaidat" - :intro-status "Csevegj velem felhasználói fiókod létrehozásáról és beállításaid megváltoztatásáról!" - :intro-message1 "Üdv az Állapotnál\nÉrints erre a üzenetre, állítsd be a jelszavad és fogj hozzá!" - :account-generation-message "Adj egy percet, varázsolok egy kicsit és létre is hozom a felhasználói fiókodat!" - - ;;chats - :chats "Csevegések" - :new-group-chat "Új csoportos csevegés" - - ;;discover - :discover "Felfedezés" - :none "Semmi" - :search-tags "Add meg keresési címkéidet itt" - :popular-tags "Népszerű címkék" - :recent "Legutóbbi" - :no-statuses-discovered "Nincsenek felfedezett állapotok" - - ;;settings - :settings "Beállítások" - - ;;contacts - :contacts "Kapcsolatok" - :new-contact "Új kapcsolat" - :contacts-group-new-chat "Új csevegés indítása" - :no-contacts "Még nincsenek kapcsolatok" - :show-qr "QR mutatása" - - ;;group-settings - :remove "Eltávolítás" - :save "Mentés" - :clear-history "Előzmények törlése" - :chat-settings "Csevegés beállítások" - :edit "Szerkesztés" - :add-members "Tagok hozzáadása" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "csevegési meghívásban részesült" - :removed-from-chat "eltávolítva a csoportos csevegésből" - :left "maradt" - :invited "meghívott" - :removed "eltávolított" - :You "Te" - - ;;new-contact - :add-new-contact "Új kapcsolat hozzáadása" - :scan-qr "QR beolvasása" - :name "Név" - :address-explication "Itt talán szükség lenne egy kis szövegre, ami elmagyarázná, mi is az a cím és hol lehet megtalálni" - :contact-already-added "A kapcsolat már hozzáadásra került" - :can-not-add-yourself "Magadat nem adhatod hozzá" - :unknown-address "Ismeretlen cím" - - ;;login - :connect "Kapcsolódás" - :address "Cím" - :password "Jelszó" - :wrong-password "Hibás jelszó" - - ;;recover - :passphrase "Jelmondat" - :recover "Visszaállítás" - - ;;accounts - :recover-access "Hozzáférés helyreállítása" - - ;;wallet-qr-code - :done "Kész" - :main-wallet "Fő zseb" - - ;;validation - :invalid-phone "Hibás telefonszám" - :amount "Összeg" - ;;transactions - :status "Állapot" - :recipient "Címzett" - - ;;:webview - :web-view-error "hoppá, hiba" - - :confirm "Megerősít" - :phone-national "Nemzeti" - :public-group-topic "Téma" - :debug-enabled "A hibakereső szerver elindításra került! Mostantól hozzáadhatod a DAppod, ha futtatod a számítógépeden a következőt: *status-dev-cli scan*" - :new-public-group-chat "Csatlakozás nyilvános csevegéshez" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Mégse" - :twelve-words-in-correct-order "12 szó helyes sorrendben" - :remove-from-contacts "Eltávolítás a névjegyekből" - :delete-chat "Csevegés törlése" - :edit-chats "Csevegések szerkesztése" - :sign-in "Bejelentkezés" - :create-new-account "Új fiók létrehozása" - :sign-in-to-status "Bejelentkezés Állapot megosztásához" - :got-it "Értem" - :move-to-internal-failure-message "Át kell helyeznünk néhány fontos fájlt a külső tárhelyről a belső tárhelyre. Ehhez szükségünk van az engedélyedre. A jövőbeni verziókban nem fogjuk használni a külső tárhelyet." - :edit-group "Csoport szerkesztése" - :delete-group "Csoport törlése" - :browsing-title "Böngészés" - :reorder-groups "Csoport átrendezése" - :browsing-cancel "Mégse" - :faucet-success "A Faucet kérés beérkezett" - :choose-from-contacts "Választás a névjegyekből" - :new-group "Új csoport" - :phone-e164 "1. nemzetközi" - :remove-from-group "Eltávolítás a csoportból" - :search-contacts "Névjegyek keresése" - :transaction "Tranzakció" - :public-group-status "Nyilvános" - :leave-chat "Kilépés a csevegésből" - :start-conversation "Beszélgetés indítása" - :topic-format "Rossz formátum [a-z0-9\\-]+" - :enter-valid-public-key "Kérjük, adj meg egy érvényes nyilvános billentyűkombinációt vagy olvasd be a QR-kódot" - :faucet-error "Hiba a Faucet kérés során" - :phone-significant "Jelentős" - :search-for "Keresés a következőre: ..." - :sharing-copy-to-clipboard "Másolás vágólapra" - :phone-international "2. nemzetközi" - :enter-address "Cím megadása" - :send-transaction "Tranzakció küldése" - :delete-contact "Névjegy törlése" - :mute-notifications "Értesítések elnémítása" - :contact-s {:one "névjegy" - :other "névjegyek"} - :next "Következő" - :from "Feladó" - :search-chats "Csevegések keresése" - :in-contacts "Névjegyekben" - - :sharing-share "Megosztás..." - :type-a-message "Írj egy üzenetet..." - :type-a-command "Kezdj el begépelni egy parancsot..." - :shake-your-phone "Találtál egy hibát vagy javaslatod van? Csak ~rázd~ meg a telefonod!" - :status-prompt "Hozz létre egy állapotot és segíts az embereknek, hogy megismerjék az általad kínált dolgokat. Használhatsz #hashtageket is." - :add-a-status "Állapot hozzáadása..." - :error "Hiba" - :edit-contacts "Névjegyek szerkesztése" - :more "több" - :cancel "Mégse" - :no-statuses-found "Nem található állapot" - :browsing-open-in-web-browser "Megnyitás új böngészőben" - :delete-group-prompt "Nem lesz hatással a névjegyekre" - :edit-profile "Profil szerkesztése" - :empty-topic "Üres téma" - :to "Címzett" - :data "Adatok"}) diff --git a/src/status_im/translations/id.cljs b/src/status_im/translations/id.cljs deleted file mode 100644 index c46ba3db549..00000000000 --- a/src/status_im/translations/id.cljs +++ /dev/null @@ -1,161 +0,0 @@ -(ns status-im.translations.id) - -(def translations - {;;common - :members-title "anggota" - :not-implemented "tidak diimplementasikan !" - :chat-name "nama obrolan" - :notifications-title "judul notifikasi" - :offline "offline" - - ;;drawer - :switch-users "beralih pengguna" - - ;;chat - :is-typing "sedang mengetik" - :and-you "dan kamu" - :search-chat "cari obrolan" - :members {:one "1 orang" - :other "anggota {{count}}orang" - :zero "Tidak ada anggota"} - :members-active {:one "1 anggota, 1 aktivitas" - :other "anggota {{count}}orang, aktivitas {{count}}orang" - :zero "Tidak ada anggota"} - :active-online "online" - :active-unknown "tidak diketahui" - :available "tersedia" - :no-messages "tidak ada pesan" - :suggestions-requests "permintaan" - :suggestions-commands "perintah" - - ;;sync - :sync-in-progress "sinkronisasi..." - :sync-synced "disinkronisasi" - - ;;messages - :status-sending "mengirim" - :status-pending "pending" - :status-sent "dikirim" - :status-seen-by-everyone "terlihat oleh semua" - :status-seen "terlihat" - :status-delivered "terkirim" - :status-failed "gagal" - - ;;datetime - :datetime-second {:one "detik" - :other "detik"} - :datetime-minute {:one "menit" - :other "menit"} - :datetime-hour {:one "jam" - :other "jam"} - :datetime-day {:one "hari" - :other "hari"} - :datetime-ago "dari" - :datetime-yesterday "kemarin" - :datetime-today "hari ini" - - ;;profile - :profile "profil" - :message "pesan" - :not-specified "tidak ditentukan" - :public-key "public key" - :phone-number "nomor telepon" - :add-to-contacts "tambahkan ke Kontak" - - ;;make_photo - :image-source-title "foto profil" - :image-source-make-photo "capture" - :image-source-gallery "pilih dari Galeri" - - ;;sign-up - :contacts-syncronized "kontak telah disinkronkan" - :confirmation-code (str "terima kasih! Kami telah mengirim pesan teks untuk mengkonfirmasi akun anda " - "kode. untuk mengkonfirmasi nomor telepon Anda, masukkan kode ini") - :incorrect-code (str "maaf, kode yang anda masukan salah. Silakan coba lagi") - :phew-here-is-your-passphrase "* wah * hal yang rumit, inilah passphrase anda, * simpan dan jaga baik - baik! * anda akan membutuhkannya untuk mengembalikan akun Anda." - :here-is-your-passphrase "berikut adalah passphrase anda. * jaga agar tetap aman dan menyimpannya! * bila anda perlu untuk memulihkan akun Anda." - :phone-number-required "ketuk di sini untuk memasukkan nomor telepon Anda, anda akan menemukan teman-teman anda" - :intro-status "berbicara dengan saya untuk mengatur akun Anda dan mengubah pengaturan Anda!" - :intro-message1 "selamat datang di Status \n klik pesan ini untuk mengatur sandi Anda & memulai!" - :account-generation-message "beri kami waktu, kami harus membuat perhitungan super untuk menghasilkan akun anda sendiri" - - ;;chats - :chats "obrolan" - :new-group-chat "obrolan grup baru" - - ;;discover - :discover "mendeteksi" - :none "apa saja" - :search-tags "silakan memasukkan pencarian di tag" - :popular-tags "tag populer" - :recent "terbaru" - :no-statuses-discovered "informasi status tidak ditemukan" - - ;;settings - :settings "pengaturan" - - ;;contacts - :contacts "kontak" - :new-contact "kontak baru" - :contacts-group-new-chat "untuk memulai obrolan baru" - :no-contacts "tidak ada kontak" - :show-qr "show QR kode" - - ;;group-settings - :remove "menghapus" - :save "menyimpan" - :clear-history "bersihkan riwayat" - :chat-settings "pengaturan obrolan" - :edit "edit" - :add-members "tambah anggota" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "telah menerima undangan untuk obrolan" - :removed-from-chat "anda dihapus dari grup obrolan" - :left "tinggalkan" - :invited "undang" - :removed "hapus" - :You "kamu" - - ;;new-contact - :add-new-contact "menambahkan kontak baru" - :scan-qr "memindai kode QR ini" - :name "nama" - :address-explication "mungkin harus ada beberapa konten untuk menjelaskan alamat ini dan di mana menemukannya" - :contact-already-added "Kontak yang telah ditambahkan" - :can-not-add-yourself "tidak dapat menambahkan anda sendiri" - :unknown-address "alamat tidak diketahui" - - ;;login - :connect "terhubung" - :address "alamat" - :password "kata sandi" - :wrong-password "kata sandi salah" - - ;;recover - :passphrase "passphrase" - :recover "pemulihan" - - ;;accounts - :recover-access "akses Pemulihan" - - ;;wallet-qr-code - :done "lengkap" - :main-wallet "wallet utama" - - ;;validation - :invalid-phone "nomor telepon salah" - :amount "jumlah dana" - ;;transactions - :status "kondisi" - :recipient "penerima" - - ;;:webview - :web-view-error "oopss, error"}) diff --git a/src/status_im/translations/it.cljs b/src/status_im/translations/it.cljs deleted file mode 100644 index d103763e582..00000000000 --- a/src/status_im/translations/it.cljs +++ /dev/null @@ -1,351 +0,0 @@ -(ns status-im.translations.it) - -(def translations - {;;common - :members-title "Membri" - :not-implemented "!non implementato" - :chat-name "Nome della chat" - :notifications-title "Notifiche e suoni" - :offline "Offline" - :search-for "Cercando..." - :cancel "Annulla" - :next "Avanti" - :open "Apri" - :description "Descrizione" - :url "URL" - :type-a-message "Scrivi un messaggio..." - :type-a-command "Inizia a digitare un comando..." - :error "Errore" - :unknown-status-go-error "Errore status-go sconosciuto" - :node-unavailable "Nessun nodo ethereum disponibile" - :yes "Sì" - :no "No" - - :camera-access-error "Per concedere i permessi necessari per la fotocamera, per favore vai nelle impostazioni di sistema e assicurati che la voce Status > Fotocamera sia selezionata." - :photos-access-error "Per concedere i permessi necessari per le foto, per favore vai nelle impostazioni di sistema e assicurati che la voce Status > Foto sia selezionata." - - ;;drawer - :switch-users "Cambia utente" - :current-network "Rete corrente" - - ;;chat - :is-typing "sta scrivendo" - :and-you "e tu" - :search-chat "Cerca chat" - :members {:one "1 membro" - :other "{{count}} membri" - :zero "nessun membro"} - :members-active {:one "1 membro" - :other "{{count}} membri" - :zero "nessun membro"} - :public-group-status "Pubblico" - :active-online "Online" - :active-unknown "Sconosciuto" - :available "Disponibile" - :no-messages "Nessun messaggio" - :suggestions-requests "Richieste" - :suggestions-commands "Comandi" - :faucet-success "La richiesta al faucet è stata ricevuta" - :faucet-error "Errore durante la richiesta al faucet" - - ;;sync - :sync-in-progress "Sincronizzazione in corso..." - :sync-synced "Sincronizzato" - - ;;messages - :status-pending "In attesa" - :status-sent "Inviato" - :status-seen-by-everyone "Visto da tutti" - :status-seen "Visto" - :status-delivered "Consegnato" - :status-failed "Fallito" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "secondo" - :other "secondi"} - :datetime-minute {:one "minuto" - :other "minuti"} - :datetime-hour {:one "ora" - :other "ore"} - :datetime-day {:one "giorno" - :other "giorni"} - :datetime-ago "fa" - :datetime-yesterday "ieri" - :datetime-today "oggi" - - ;;profile - :profile "Profilo" - :edit-profile "Modifica profilo" - :message "Messaggio" - :not-specified "Non specificato" - :public-key "Chiave pubblica" - :phone-number "Numero di telefono" - :update-status "Aggiorna il tuo stato..." - :add-a-status "Aggiungi uno stato..." - :status-prompt "Imposta il tuo stato. L'utilizzo di #hastags aiuterà gli altri a scoprirti e a parlare di ciò che pensi" - :add-to-contacts "Aggiungi ai contatti" - :in-contacts "Nei contatti" - :remove-from-contacts "Rimuovi dai contatti" - :start-conversation "Inizia la conversazione" - :send-transaction "Invia una transazione" - :testnet-text "Sei in {{testnet}} Testnet. Non mandare ETH o SNT reali al tuo indirizzo" - :mainnet-text "Sei nella Mainnet. Verranno inviati ETH reali" - - ;;make_photo - :image-source-title "Immagine del profilo" - :image-source-make-photo "Cattura" - :image-source-gallery "Seleziona dalla galleria" - - ;;sharing - :sharing-copy-to-clipboard "Copia negli appunti" - :sharing-share "Condividi..." - :sharing-cancel "Annulla" - - :browsing-title "Naviga" - :browsing-open-in-web-browser "Apri nel web browser" - :browsing-cancel "Annulla" - - ;;sign-up - :contacts-syncronized "I tuoi contatti sono stati sincronizzati" - :confirmation-code (str "Grazie! Ti abbiamo inviato un sms con un codice " - "di conferma. Per favore inserisci quel codice per confermare il tuo numero di telefono") - :incorrect-code (str "Il codice era errato, riprova") - :phew-here-is-your-passphrase "Fiù, è stato difficile. Ecco la tua passphrase, *scrivila e conservala al sicuro!* Ne avrai bisogno per recuperare il tuo account." - :here-is-your-passphrase "Ecco la tua passphrase, *scrivila e conservala al sicuro!* Ne avrai bisogno per recuperare il tuo account." - :here-is-your-signing-phrase "Ecco la tua signing phrase. La userai per verificare le tue transazioni. *Scrivila e conservala al sicuro!*" - :phone-number-required "Tocca qui per convalidare il tuo numero di telefono e troverò i tuoi amici." - :shake-your-phone "Hai trovato un bug o hai un suggerimento? Devi solo ~scuotere~ il tuo telefono!" - :intro-status "Chatta con me per inizializzare il tuo account e cambiare le tue impostazioni." - :intro-message1 "Benvenuto in Status!\nTocca questo messaggio per impostare la tua password e cominciare." - :account-generation-message "Dammi un secondo, devo fare dei calcoli complicati per generare il tuo account!" - :move-to-internal-failure-message "Dobbiamo spostare alcuni file importanti dalla periferica di archiviazione esterna a quella interna. Per farlo, abbiamo bisogno del tuo permesso. Nelle versioni future non utilizzeremo periferiche di archiviazione esterne." - :debug-enabled "Il server di debug è stato lanciato! Adesso puoi eseguire *status-dev-cli scan* per trovare il server dal tuo computer nella tua stessa rete." - - ;;phone types - :phone-e164 "Internazionale 1" - :phone-international "Internazionale 2" - :phone-national "Nazionale" - :phone-significant "Rilevante" - - ;;chats - :chats "Chat" - :delete-chat "Cancella chat" - :new-group-chat "Nuova chat di gruppo" - :new-public-group-chat "Entra nella chat pubblica" - :edit-chats "Modifica chat" - :search-chats "Cerca chat" - :empty-topic "Argomento vuoto" - :topic-format "Formato errato [a-z0-9\\-]+" - :public-group-topic "Argomento" - - ;;discover - :discover "Scopri" - :none "Nessuno" - :search-tags "Digita qui i tuoi tag di ricerca" - :popular-tags "#hashtags popolari" - :recent "Stati recenti" - :no-statuses-found "Nessuno stato trovato" - :chat "Chat" - :all "Tutte" - :public-chats "Chat pubbliche" - :soon "Presto" - :public-chat-user-count "{{count}} persone" - :dapps "ÐApps" - :dapp-profile "Profilo ÐApp" - :no-statuses-discovered "Nessuno stato trovato" - :no-statuses-discovered-body "Quando qualcuno posta\nuno stato lo vedrai qui." - :no-hashtags-discovered-title "Nessun #hashtags trovato" - :no-hashtags-discovered-body "Quando un #hashtag diventa\npopolare lo vedrai qui." - - ;;settings - :settings "Impostazioni" - - ;;contacts - :contacts "Contatti" - :new-contact "Nuovo contatto" - :delete-contact "Elimina contatto" - :delete-contact-confirmation "Questo contatto sarà eliminato dai tuoi contatti" - :remove-from-group "Rimuovi dal gruppo" - :edit-contacts "Modifica contatto" - :search-contacts "Cerca contatti" - :contacts-group-new-chat "Inizia una nuova chat" - :choose-from-contacts "Scegli dai contatti" - :no-contacts "Ancora nessun contatto" - :show-qr "Mostra QR" - :enter-address "Inserisci indirizzo" - :more "ancora" - - ;;group-settings - :remove "Rimuovi" - :save "Salva" - :delete "Cancella" - :clear-history "Cancella cronologia" - :mute-notifications "Silenzia notifiche" - :leave-chat "Lascia la chat" - :chat-settings "Impostazioni della chat" - :edit "Modifica" - :add-members "Aggiungi membri" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nuovo gruppo" - :reorder-groups "Riordina gruppi" - :edit-group "Modifica gruppo" - :delete-group "Elimina gruppo" - :delete-group-confirmation "Questo gruppo sarà cancellato dai tuoi gruppi. Questo non intaccherà i tuoi contatti" - :delete-group-prompt "Questo non intaccherà i tuoi contatti" - :contact-s {:one "contatto" - :other "contatti"} - - ;;protocol - :received-invitation "invito alla chat ricevuto" - :removed-from-chat "sei stato rimosso dalla chat di gruppo" - :left "lasciato" - :invited "invitato" - :removed "rimosso" - :You "Tu" - - ;;new-contact - :add-new-contact "Aggiungi nuovo contatto" - :scan-qr "Scansiona QR" - :name "Nome" - :address-explication "La tua chiave privata è usata per generare il tuo indirizzo su Ethereum ed è una serie di numeri e lettere. Puoi trovarla facilmente nel tuo profilo" - :enter-valid-public-key "Per favore inserisci una chiave pubblica valida o scansiona un QR" - :contact-already-added "Il contatto è già stato aggiunto" - :can-not-add-yourself "Non puoi aggiungere te stesso" - :unknown-address "Indirizzo sconosciuto" - - ;;login - :connect "Connetti" - :address "Indirizzo" - :password "Password" - :sign-in-to-status "Accedi a Status" - :sign-in "Accedi" - :wrong-password "Password sbagliata" - :enter-password "Inserisci la password" - - ;;recover - :passphrase "Passphrase" - :recover "Recupera" - :twelve-words-in-correct-order "12 parole nell'ordine corretto" - - ;;accounts - :recover-access "Recupera accesso" - :create-new-account "Crea un nuovo account" - - ;;wallet-qr-code - :done "Fatto" - - ;;validation - :invalid-phone "Numero di telefono invalido" - :amount "Quantità" - - ;;transactions - :confirm "Conferma" - :transaction "Transazione" - :unsigned-transaction-expired "Transazione non firmata scaduta" - :status "Stato" - :recipient "Ricevente" - :to "A" - :from "Da" - :data "Dati" - :got-it "Ho capito" - :block "Blocco" - :hash "Hash" - :gas-limit "Gas limit" - :gas-price "Gas price" - :gas-used "Gas usato" - :cost-fee "Costo/Fee" - :nonce "Nonce" - :confirmations "Conferme" - :confirmations-helper-text "Per favore aspetta almeno 12 conferme per essere sicuro che la tua transazione sia stata processata in sicurezza" - :copy-transaction-hash "Copia l'hash della transazione" - :open-on-etherscan "Apri su Etherscan.io" - - ;;webview - :web-view-error "oops, errore" - - ;;testfairy warning - :testfairy-title "Attenzione!" - :testfairy-message "Stai usando un app installata da una build nightly. Per scopi di testing questa build include sessioni di registrazione se è usata la connessione wifi, così tutte le tue interazioni con l'app sono salvate (sia video che log) e possono essere usate dal nostro team di sviluppo per investigare possibili problemi. I video/log salvati non includono le tue password. La registrazione è fatta solo se l'app è installata da una build nightly. Niente viene registrato se l'app è installata dal PlayStore o da TestFlight." - - ;; wallet - :wallet "Portafoglio" - :wallets "Portafogli" - :your-wallets "I tuoi Portafogli" - :main-wallet "Portafoglio principale" - :wallet-error "Errore durante il caricamento dei dati" - :wallet-send "Invia" - :wallet-request "Richiedi" - :wallet-exchange "Scambia" - :wallet-assets "Assets" - :wallet-add-asset "Aggiungi asset" - :wallet-total-value "Valore totale" - :wallet-settings "Impostazioni del portafoglio" - :signing-phrase-description "Firma la transazione inserendo la tua password. Assicurati che le parole sopra corrispondano alla tua signing phrase segreta" - :wallet-insufficient-funds "Fondi insufficienti" - :request-transaction "Richiesta di transazione" - :send-request "Invia richiesta" - :share "Condividi" - :eth "ETH" - :currency "Valuta" - :usd-currency "USD" - :transactions "Transazioni" - :transaction-details "Dettagli della transazione" - :transaction-failed "Transazione fallita" - :transactions-sign "Firma" - :transactions-sign-all "Firma tutto" - :transactions-sign-transaction "Firma transazione" - :transactions-sign-later "Firma dopo" - :transactions-delete "Elimina transazione" - :transactions-delete-content "La transazione sarà rimossa dalla lista delle tranzazioni 'Non Firmate'" - :transactions-history "Cronologia" - :transactions-unsigned "Non Firmate" - :transactions-history-empty "Non hai ancora nessuna transazione nella cronologia" - :transactions-unsigned-empty "Non hai nessuna transazione non firmata" - :transactions-filter-title "Filtra cronologia" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Tipo" - :transactions-filter-select-all "Seleziona tutte" - :view-transaction-details "Vedi i dettagli della transazione" - :transaction-description "Per favore aspetta almeno 12 conferme per essere sicuro che la tua transazione sia stata processata in sicurezza" - :transaction-sent "Transazione inviata" - :transaction-moved-text "La transazione rimarrà nella lista delle transazioni 'Non Firmate' per i prossimi 5 minuti" - :transaction-moved-title "Transazione spostata" - :sign-later-title "Firmare la transazione dopo?" - :sign-later-text "Controlla la cronologia delle transazioni per firmare questa transazione" - :not-applicable "Non applicabile alle transazioni non firmate" - - ;; Wallet Send - :wallet-choose-recipient "Scegli destinatario" - :wallet-choose-from-contacts "Scegli Dai Contatti" - :wallet-address-from-clipboard "Usa Indirizzo Dagli Appunti" - :wallet-invalid-address "Indirizzo non valido: \n {{data}}" - :wallet-browse-photos "Sfoglia Foto" - :validation-amount-invalid-number "La Quantità è un numero invalido" - :validation-amount-is-too-precise "L'ammontare è troppo preciso. La più piccola unità che puoi inviare è 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Nuova rete" - :add-network "Aggiungi rete" - :add-new-network "Aggiungi nuova rete" - :existing-networks "Reti esistenti" - :add-json-file "Aggiugni un file JSON" - :paste-json-as-text "Incolla JSON come testo" - :paste-json "Incolla JSON" - :specify-rpc-url "Specifica un URL RPC" - :edit-network-config "Modifica configurazioni di rete" - :connected "Connesso" - :process-json "Processa JSON" - :error-processing-json "Errore nel processamento del JSON" - :rpc-url "URL RPC" - :remove-network "Rimuovi rete" - :network-settings "Impostazioni di rete" - :edit-network-warning "Attenzione, modificando i dettagli di questa rete potresti disabilitarla per te" - :connecting-requires-login "La connessione ad un'altra rete richiede il login" - :close-app-title "Attenzione!" - :close-app-content "L'app si fermerà e si chiuderà. Quando la riaprirai, verrà utilizzata la rete selezionata" - :close-app-button "Conferma"}) diff --git a/src/status_im/translations/it_ch.cljs b/src/status_im/translations/it_ch.cljs deleted file mode 100644 index bdd127c4596..00000000000 --- a/src/status_im/translations/it_ch.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.it-ch) - -(def translations - {;;common - :members-title "Membri" - :not-implemented "!non implementato" - :chat-name "Nome chat" - :notifications-title "Notifiche e suoni" - :offline "Offline" - - ;;drawer - :switch-users "Cambia utenti" - - ;;chat - :is-typing "sta scrivendo" - :and-you "e tu" - :search-chat "Cerca chat" - :members {:one "1 membro" - :other "{{count}} membri" - :zero "nessun membro"} - :members-active {:one "1 membro, 1 attivo" - :other "{{count}} membri, {{count}} attivi" - :zero "nessun membro"} - :active-online "Online" - :active-unknown "Sconosciuto" - :available "Disponibile" - :no-messages "Nessun messaggio" - :suggestions-requests "Richieste" - :suggestions-commands "Comandi" - - ;;sync - :sync-in-progress "Sincronizzazione in corso..." - :sync-synced "Sincronizzato" - - ;;messages - :status-sending "Invio in corso" - :status-pending "In attesa di" - :status-sent "Inviato" - :status-seen-by-everyone "Visto da tutti" - :status-seen "Visto" - :status-delivered "Consegnato" - :status-failed "Invio fallito" - - ;;datetime - :datetime-second {:one "secondo" - :other "secondi"} - :datetime-minute {:one "minuto" - :other "minuti"} - :datetime-hour {:one "ora" - :other "ore"} - :datetime-day {:one "giorno" - :other "giorni"} - :datetime-ago "fa" - :datetime-yesterday "ieri" - :datetime-today "oggi" - - ;;profile - :profile "Profilo" - :message "Messaggio" - :not-specified "Non specificato" - :public-key "Chiave pubblica" - :phone-number "Numero di telefono" - :add-to-contacts "Aggiungi ai contatti" - - ;;make_photo - :image-source-title "Immagine profilo" - :image-source-make-photo "Scatta" - :image-source-gallery "Seleziona dalla galleria immagini" - - ;;sign-up - :contacts-syncronized "I tuoi contatti sono stati sincronizzati" - :confirmation-code (str "Grazie! Ti abbiamo inviato un messaggio con un codice di " - "conferma. Utilizza tale codice per confermare il tuo numero di telefono") - :incorrect-code (str "Il codice inserito è errato, riprova") - :phew-here-is-your-passphrase "*Wow* È stato difficile, ecco qui la tua passphrase, *prendi nota e conservala in un luogo sicuro!* Ti servirà per ripristinare il tuo conto." - :here-is-your-passphrase "Ecco qui la tua passphrase, *prendi nota e conservala in un luogo sicuro!* Ti servirà per ripristinare il tuo conto." - :phone-number-required "Clicca qui per inserire il tuo numero di telefono e trovare i tuoi amici" - :intro-status "Avvia una conversazione con me per impostare il tuo conto e modificare le tue impostazioni!" - :intro-message1 "Benvenuto su Status\nTocca questo messaggio per impostare la tua password e iniziare!" - :account-generation-message "Dammi un secondo, devo eseguire dei calcoli matematici complessi per generare il tuo conto!" - - ;;chats - :chats "Conversazioni" - :new-group-chat "Nuova conversazione di gruppo" - - ;;discover - :discover "Scoperta" - :none "Nessuna" - :search-tags "Inserisci qui i tag di ricerca" - :popular-tags "Tag popolari" - :recent "Recente" - :no-statuses-discovered "Nessuno stato identificato" - - ;;settings - :settings "Impostazioni" - - ;;contacts - :contacts "Contatti" - :new-contact "Nuovo contatto" - :contacts-group-new-chat "Inizia una nuova conversazione" - :no-contacts "Nessun contatto registrato" - :show-qr "Mostra QR" - - ;;group-settings - :remove "Rimuovi" - :save "Salva" - :clear-history "Cancella cronologia" - :chat-settings "Impostazioni conversazioni" - :edit "Modifica" - :add-members "Aggiungi membri" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "ha ricevuto un invito di conversazione" - :removed-from-chat "ti ha rimosso dalla conversazione di gruppo" - :left "È uscito" - :invited "È stato invitato" - :removed "È stato rimosso" - :You "Tu" - - ;;new-contact - :add-new-contact "Aggiungi nuovo contatto" - :scan-qr "Scansiona QR" - :name "Nome" - :address-explication "Forse qui dovremmo spiegare cos'è un indirizzo e dove cercarlo" - :contact-already-added "Il contatto è già stato aggiunto" - :can-not-add-yourself "Non puoi aggiungere te stesso" - :unknown-address "Indirizzo sconosciuto" - - ;;login - :connect "Effettua connessione" - :address "Indirizzo" - :password "Password" - :wrong-password "Password errata" - - ;;recover - :passphrase "Passphrase" - :recover "Ripristina" - - ;;accounts - :recover-access "Ripristina l'accesso" - - ;;wallet-qr-code - :done "OK" - :main-wallet "Wallet principale" - - ;;validation - :invalid-phone "Numero di telefono non valido" - :amount "Saldo" - ;;transactions - :status "Stato" - :recipient "Beneficiario" - - ;;:webview - :web-view-error "Ops, si è verificato un errore" - - :confirm "Conferma" - :phone-national "Nazionale" - :public-group-topic "Argomento" - :debug-enabled "Il server di debug è stato avviato! Ora puoi aggiungere la tua DApp eseguendo *status-dev-cli scan* dal tuo computer" - :new-public-group-chat "Entra in chat pubblica" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Annulla" - :twelve-words-in-correct-order "12 parole in ordine corretto" - :remove-from-contacts "Rimuovi dai contatti" - :delete-chat "Elimina chat" - :edit-chats "Modifica chat" - :sign-in "Accedi" - :create-new-account "Crea nuovo conto" - :sign-in-to-status "Accedi a Stato" - :got-it "OK" - :move-to-internal-failure-message "Dobbiamo spostare alcuni file importanti dal dispositivo di archiviazione esterno a quello interno. Per poter completare l'operazione abbiamo bisogno della tua autorizzazione. Non utilizzeremo lo spazio di archiviazione esterno nelle versioni future." - :edit-group "Modifica gruppo" - :delete-group "Elimina gruppo" - :browsing-title "Sfoglia" - :reorder-groups "Riordina gruppi" - :browsing-cancel "Annulla" - :faucet-success "Richiesta faucet ricevuta" - :choose-from-contacts "Scegli dai contatti" - :new-group "Nuovo gruppo" - :phone-e164 "Internazionale 1" - :remove-from-group "Rimuovi dal gruppo" - :search-contacts "Cerca tra i contatti" - :transaction "Transazione" - :public-group-status "Pubblico" - :leave-chat "Abbandona chat" - :start-conversation "Inizia conversazione" - :topic-format "Formato errato [a-z0-9\\-]+" - :enter-valid-public-key "Inserisci una chiave pubblica valida o scansione un codice QR" - :faucet-error "Errore richiesta faucet" - :phone-significant "Rilevante" - :search-for "Cerca…" - :sharing-copy-to-clipboard "Copia negli appunti" - :phone-international "Internazionale 2" - :enter-address "Inserisci indirizzo" - :send-transaction "Invia transazione" - :delete-contact "Elimina contatto" - :mute-notifications "Silenzia le notifiche" - :contact-s {:one "contatto" - :other "contatti"} - :next "Avanti" - :from "Da" - :search-chats "Cerca nelle chat" - :in-contacts "Nei contatti" - - :sharing-share "Condividi…" - :type-a-message "Digita un messaggio…" - :type-a-command "Inizia a digitare un comando…" - :shake-your-phone "Hai trovato un bug o hai un suggerimento? ~Scuoti~ il telefono!" - :status-prompt "Crea uno stato per consentire le persone di conoscere le cose che offri. Puoi usare anche gli #hashtag" - :add-a-status "Aggiungi uno stato…" - :error "Errore" - :edit-contacts "Modifica contatti" - :more "altro" - :cancel "Annulla" - :no-statuses-found "Nessuno stato trovato" - :browsing-open-in-web-browser "Apri nel browser" - :delete-group-prompt "Questo non intaccherà i contatti" - :edit-profile "Modifica profilo" - :empty-topic "Argomento non definito" - :to "A" - :data "Dati"}) diff --git a/src/status_im/translations/ja.cljs b/src/status_im/translations/ja.cljs deleted file mode 100644 index b4ad8ffdef1..00000000000 --- a/src/status_im/translations/ja.cljs +++ /dev/null @@ -1,366 +0,0 @@ -(ns status-im.translations.ja) - -(def translations - {;;common - :members-title "メンバー" - :not-implemented "実装されていません" - :chat-name "チャット名" - :notifications-title "通知と音声" - :offline "オフライン" - :search-for "検索..." - :cancel "キャンセル" - :next "次へ" - :open "開く" - :description "説明" - :url "URL" - :type-a-message "メッセージを入力する..." - :type-a-command "コマンドを入力する..." - :error "エラー" - :unknown-status-go-error "不明なstatus-goがエラー" - :node-unavailable "実行中のエーテル ノードがありません" - :yes "はい" - :no "いいえ" - - :camera-access-error "必要なカメラの許可を与えるには、システム設定に行き、ステータス>カメラが選択されていることを確認してください。" - :photos-access-error "必要な写真の許可を与えるには、あなたのシステム設定に行き、ステータス> 写真が選択されていることを確認してください。" - - ;;drawer - :switch-users "ユーザーの切り替え" - :current-network "現在のネットワーク" - - ;;chat - :is-typing "が入力中" - :and-you "とあなた" - :search-chat "チャットを検索" - :members {:one "1人のメンバー" - :other "{{count}}人のメンバー" - :zero "メンバーがいません"} - :members-active {:one "1人がアクティブ" - :other "{{count}}人がアクティブ" - :zero "アクティブなメンバーはいません"} - :public-group-status "公開" - :active-online "オンライン" - :active-unknown "不明" - :available "利用可能" - :no-messages "メッセージがありません" - :suggestions-requests "リクエスト" - :suggestions-commands "コマンド" - :faucet-success "フォーセットリクエストを受け取りました" - :faucet-error "フォーセットリクエストエラー" - - ;;sync - :sync-in-progress "同期中..." - :sync-synced "同期しました" - - ;;messages - :status-sending "送信中" - :status-pending "保留中" - :status-sent "送信済み" - :status-seen-by-everyone "全員が閲覧" - :status-seen "閲覧" - :status-delivered "配達済み" - :status-failed "失敗しました" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分" - :other "分"} - :datetime-hour {:one "時間" - :other "時間"} - :datetime-day {:one "日" - :other "日"} - :datetime-ago "前" - :datetime-yesterday "昨日" - :datetime-today "今日" - - ;;profile - :profile "プロフィール" - :edit-profile "プロフィールを編集" - :message "メッセージ" - :not-specified "特定されていません" - :public-key "公開鍵" - :phone-number "電話番号" - :update-status "ステータスを更新..." - :add-a-status "ステータスを追加..." - :status-prompt "ステータスを設定します。#hastagsを使用すると、他の人があなたを見つけて、あなたのことをより知るのに役立ちます" - :add-to-contacts "連絡先に追加" - :in-contacts "連絡先" - :remove-from-contacts "連絡先から削除" - :start-conversation "会話を始める" - :send-transaction "トランザクションを送信" - :testnet-text "あなたはテストネットにいる. 実際のETHまたはSNTをあなたの住所に送信しないでください" - :mainnet-text "あなたはメインネットにいる. 実際のETHが送信されます" - - ;;make_photo - :image-source-title "プロフィール画像" - :image-source-make-photo "カメラを起動" - :image-source-gallery "ギャラリーから選択" - - ;;sharing - :sharing-copy-to-clipboard "クリップボードにコピー" - :sharing-share "共有..." - :sharing-cancel "キャンセル" - - :browsing-title "閲覧" - :browsing-open-in-web-browser "ブラウザーで閲覧" - :browsing-cancel "キャンセル" - - ;;sign-up - :contacts-syncronized "連絡先が同期されました" - :confirmation-code (str "ありがとうございます! 確認コードが記載されたメッセージが" - "送信されました。電話番号を確認するにはそのコードを入力してください") - :incorrect-code (str "申し訳ありません。コードが間違っています。もう一度入力してください") - :phew-here-is-your-passphrase "*お疲れ様でした。*これがパスフレーズです。*書き留めて安全な場所に保管してください!*アカウントの復元に必要になります。" - :here-is-your-passphrase "これがパスフレーズです。*書き留めて安全な場所に保管してください!*アカウントの復元に必要になります。" - :here-is-your-signing-phrase "あなたの署名フレーズがあります。 あなたはそれを使って取引を確認します。 *書き留めて安全に保管してください!*" - :phone-number-required "ここをタップして電話番号を入力するとお友達を検索します。" - :shake-your-phone "問題点や改善点を発見? スマホを振って下さい!" - :intro-status "チャットしてアカウントを設定し、設定を変更してください!" - :intro-message1 "ステータスにようこそ\nこのメッセージをタップして、パスワードを設定し開始しましょう!" - :account-generation-message "少々お待ちください。アカウントの生成するにはお時間がかかります。" - :move-to-internal-failure-message "いくつかの重要なファイルを外部メディアに保存します。続行するには、許可が必要です。(将来的には外部メディアを使用しないようにします。)" - :debug-enabled "デバッグサーバーを起動しました! *status-dev-cli scan*を実行すると、同じネットワーク上のコンピュータからサーバーを見つけることができます。" - - ;;phone types - :phone-e164 "国際 1" - :phone-international "国際 2" - :phone-national "国内" - :phone-significant "重要" - - ;;chats - :chats "チャット" - :delete-chat "チャットを削除" - :new-group-chat "新規グループチャット" - :new-public-group-chat "公開チャットに参加" - :edit-chats "チャットを編集" - :search-chats "チャットを検索" - :empty-topic "空のトピック" - :topic-format "間違った形式 [a-z0-9\\-]+" - :public-group-topic "トピック" - - ;;discover - :discover "発見" - :none "なし" - :search-tags "ここに検索タグを入力してください" - :popular-tags "人気のタグ" - :recent "最近のステータス" - :no-statuses-found "ステータスが見つかりませんでした" - :chat "チャット" - :all "全部" - :public-chats "公開チャット" - :soon "すぐに" - :public-chat-user-count "{{count}} 人" - :dapps "ÐApps" - :dapp-profile "ÐApp プロフィール" - :no-statuses-discovered "ステータスは見つかりませんでした" - :no-statuses-discovered-body "誰かがステータスを投稿すると、ここに表示されます." - :no-hashtags-discovered-title "#hashtags が見つかりませんでした" - :no-hashtags-discovered-body "#hashtag ハッシュタグが普及すると、ここに表示されます。" - ;;settings - :settings "設定" - - ;;contacts - :contacts "連絡先" - :new-contact "新規連絡先" - :delete-contact "連絡先を削除" - :delete-contact-confirmation "連絡帳よりこの連絡先を削除します" - :remove-from-group "グループから削除" - :edit-contacts "連絡先を編集" - :search-contacts "連絡先を検索" - :contacts-group-new-chat "新規チャットを開始" - :choose-from-contacts "連絡先から選択" - :no-contacts "まだ連絡先がありません" - :show-qr "QRを表示" - :enter-address "アドレスを入力してください" - :more "もっと" - - ;;group-settings - :remove "削除" - :save "保存" - :delete "削除" - :clear-history "履歴の消去" - :mute-notifications "お知らせをミュート" - :leave-chat "チャットから退出" - :chat-settings "チャット設定" - :edit "編集" - :add-members "メンバーを追加" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新しいグループ" - :reorder-groups "グループを並び替え" - :edit-group "グループを編集" - :delete-group "グループを削除" - :delete-group-confirmation "このグループを削除します。連絡先には影響を与えません。" - :delete-group-prompt "連絡先には影響を与えません。" - :contact-s {:one "連絡先" - :other "連絡先"} - - ;;protocol - :received-invitation "チャット招待状を受信しました" - :removed-from-chat "グループチャットから削除されました" - :left "離れた" - :invited "招待済み" - :removed "削除済み" - :You "あなた" - - ;;new-contact - :add-new-contact "新規連絡先を追加" - :scan-qr "QRコードをスキャン" - :name "名前" - :address-explication "あなたの公開鍵は、Ethereum上にあなたのアドレスを生成するために使用され、一連の数字と文字です。 あなたのプロフィールで簡単に見つけることができます" - :enter-valid-public-key "有効な公開鍵を入力するかQRコードをスキャンしてください" - :contact-already-added "連絡先は既に追加されています" - :can-not-add-yourself "自分を追加することはできません" - :unknown-address "不明な住所" - - ;;login - :connect "接続" - :address "アドレス" - :password "パスワード" - :sign-in-to-status "ステータスにサインイン" - :sign-in "サインイン" - :wrong-password "パスワードが間違っています" - :enter-password "パスワードを入力する" - - ;;recover - :passphrase "パスフレーズ" - :recover "復元" - :twelve-words-in-correct-order "正しい順序で12単語を並ぶ" - - ;;accounts - :recover-access "アクセスを復元" - :create-new-account "新規アカウントを作成" - - ;;wallet-qr-code - :done "完了" - - ;;validation - :invalid-phone "無効な電話番号" - :amount "金額" - - ;;transactions - :confirm "確認" - :transaction "取引" - :unsigned-transaction-expired "署名されていない取引は期限切れです" - :status "ステータス" - :recipient "受信者" - :to "宛先" - :from "送信元" - :data "データ" - :got-it "理解しました" - :block "ブロック" - :hash "ハッシュ" - :gas-limit "gas制限" - :gas-price "gas価格" - :gas-used "使用Gas" - :cost-fee "費用/料金" - :nonce "ノンス" - :confirmations "確認" - :confirmations-helper-text "あなたの取引が安全に処理されることを確認するために少なくとも12回の確認を待ってください。" - :copy-transaction-hash "トランザクションハッシュをコピーする" - :open-on-etherscan "Etherscan.ioを開く" - :incoming "着信" - :outgoing "送信" - :pending "未定" - :postponed "延期された" - - ;;webview - :web-view-error "エラーが発生しました" - - ;;testfairy warning - :testfairy-title "警告!" - :testfairy-message "あなたはインストールされたアプリは夜間のバージョンです。 テスト目的のために、このバージョンにWi-Fi接続が使用されている場合は会話記録が含まれています。このアプリとのすべてのやり取りは(動画やログとして)保存され、開発チームが問題の調査に使用する可能性があります。 保存されたビデオ/ログにはパスワードは含まれません。 会話記録は、アプリが夜間のバージョンからインストールされている場合にのみ実行されます。 アプリがPlayStoreまたはTestFlightからインストールされている場合は記録されません。" - - ;; wallet - :wallet "ウォレット" - :wallets "全てのウォレット" - :your-wallets "あなたの全てのウォレット" - :main-wallet "メインウォレット" - :wallet-error "データの読み込みエラー" - :wallet-send "送信" - :wallet-send-token "送信 {{symbol}}" - :wallet-request "要求" - :wallet-exchange "交換" - :wallet-assets "資産" - :wallet-add-asset "資産を追加" - :wallet-total-value "総価値" - :wallet-settings "ウォレット設定" - :wallet-manage-assets "資産の管理" - :signing-phrase-description "パスワードを入力して取引に署名します。 上記の単語があなたの秘密の署名句に一致することを確認してください" - :wallet-insufficient-funds "残高不足" - :request-transaction "取引をリクエストする" - :send-request "リクエストを送信する" - :share "シェア" - :eth "ETH" - :currency "通貨" - :usd-currency "USD" - :transactions "取引" - :transaction-details "取引の詳細" - :transaction-failed "取引が失敗しました" - :transactions-sign "署名" - :transactions-sign-all "全てに署名する" - :transactions-sign-transaction "取引に署名する" - :transactions-sign-later "後で署名する" - :transactions-delete "取引を削除する" - :transactions-delete-content "取引は「未署名」リストから削除されます。" - :transactions-history "取引履歴" - :transactions-unsigned "署名なし" - :transactions-history-empty "あなたの履歴にまだ取引はありません" - :transactions-unsigned-empty "署名のない取引はありません" - :transactions-filter-title "フィルタ履歴" - :transactions-filter-tokens "トークン" - :transactions-filter-type "タイプ" - :transactions-filter-select-all "全て選択" - :view-transaction-details "取引の詳細を表示する" - :transaction-description "あなたの取引が安全に処理されることを確認するために少なくとも12回の確認を待ってください。" - :transaction-sent "送信された取引" - :transaction-moved-text "取引は'未署名'リストに5分間を残ります" - :transaction-moved-title "取引の移動タイトル" - :sign-later-title "後で取引に署名する?" - :sign-later-text "この取引に署名するために取引履歴を確認する" - :not-applicable "署名のない取引には適用されません" - - ;; Wallet Send - :wallet-choose-recipient "受信者を選択" - :wallet-choose-from-contacts "連絡先から選択" - :wallet-address-from-clipboard "アドレスをクリップボードから使用する" - :wallet-invalid-address "無効なアドレス: \n {{data}}" - :wallet-invalid-chain-id "ネットワークが一致しません: \n {{data}} でも、現在のチェーン は {{chain}}" - :wallet-browse-photos "写真を閲覧" - :wallet-advanced "高度な" - :wallet-transaction-fee "取引手数料" - :wallet-transaction-fee-details "Gas リミットは、取引で送付するgasの量です。 この数を増やしても、取引はより速く処理されません" - :wallet-transaction-total-fee "合計料金" - :validation-amount-invalid-number "金額は有効な数字ではありません" - :validation-amount-is-too-precise "金額が高すぎます。送れる最小単位は1 Wei(1x10 ^ -18 ETH)です。" - - ;; network settings - :new-network "新しいネットワーク" - :add-network "ネットワークを追加する" - :add-new-network "新しいネットワークを追加" - :add-wnode "メールサーバーを追加する" - :existing-networks "既存のネットワーク" - ;; TODO(dmitryn): come up with better description/naming. Suggested namings: Mailbox and Master Node - :existing-wnodes "既存のメールサーバー" - :add-json-file "JSONファイルを追加する" - :paste-json-as-text "JSONをテキストとして貼り付ける" - :paste-json "JSONを貼り付け" - :specify-rpc-url "RPC URLを指定する" - :edit-network-config "ネットワーク設定を編集する" - :connected "接続済み" - :process-json "JSONを処理" - :error-processing-json "JSONの処理エラー" - :rpc-url "RPC URL" - :remove-network "ネットワークを削除" - :network-settings "ネットワーク設定" - :offline-messaging-settings "オフラインメッセージの設定" - :edit-network-warning "注意してください。ネットワークデータを編集すると、このネットワークが無効になることがあります" - :connecting-requires-login "別のネットワークに接続するにはログインが必要です" - :close-app-title "警告!" - :close-app-content "アプリは停止して終了します。 これを再び開くと、選択したネットワークが使用されます" - :close-app-button "確認"}) diff --git a/src/status_im/translations/ko.cljs b/src/status_im/translations/ko.cljs deleted file mode 100644 index 54dad8ab068..00000000000 --- a/src/status_im/translations/ko.cljs +++ /dev/null @@ -1,351 +0,0 @@ -(ns status-im.translations.ko) - -(def translations - {;;common - :members-title "멤버" - :not-implemented "!구현되지 않음" - :chat-name "채팅 이름" - :notifications-title "알림 및 소리" - :offline "오프라인" - :search-for "검색하기..." - :cancel "취소" - :next "다음" - :open "열기" - :description "설명" - :url "URL" - :type-a-message "메시지 입력..." - :type-a-command "명령 입력..." - :error "오류" - :unknown-status-go-error "알 수 없는 status-go 오류" - :node-unavailable "사용 가능한 Ethereum 노드가 없습니다." - :yes "예" - :no "아니오" - - :camera-access-error "카메라 권한을 승인하려면 시스템 앱 설정에서 Status > 권한 > 카메라가 선택되어 있는지 확인해 주세요." - :photos-access-error "사진 접근 권한을 승인하려면 시스템 앱 설정에서 Status > 권한 > 사진이 선택되어 있는지 확인해 주세요." - - ;;drawer - :switch-users "사용자 전환" - :current-network "현재 네트워크" - - ;;chat - :is-typing "이(가) 입력 중입니다" - :and-you "그리고 당신" - :search-chat "채팅 검색" - :members {:one "멤버 1명" - :other "멤버 {{count}}명" - :zero "멤버 없음"} - :members-active {:one "멤버 1명" - :other "멤버 {{count}}명" - :zero "멤버 없음"} - :public-group-status "공개" - :active-online "온라인" - :active-unknown "상태 알 수 없음" - :available "이용 가능" - :no-messages "메시지 없음" - :suggestions-requests "요청" - :suggestions-commands "명령" - :faucet-success "Faucet 요청이 도착했습니다" - :faucet-error "Faucet 요청 오류" - - ;;sync - :sync-in-progress "동기화 중..." - :sync-synced "동기화됨" - - ;;messages - :status-pending "보류중" - :status-sent "전송됨" - :status-seen-by-everyone "모두 읽음" - :status-seen "읽음" - :status-delivered "전송됨" - :status-failed "실패" - - ;;datetime - :datetime-ago-format "{{number}}{{time-intervals}} {{ago}}" - :datetime-second {:one "초" - :other "초"} - :datetime-minute {:one "분" - :other "분"} - :datetime-hour {:one "시간" - :other "시간"} - :datetime-day {:one "일" - :other "일"} - :datetime-ago "전" - :datetime-yesterday "어제" - :datetime-today "오늘" - - ;;profile - :profile "프로필" - :edit-profile "프로필 편집" - :message "메시지" - :not-specified "지정되지 않음" - :public-key "공개 키" - :phone-number "휴대전화 번호" - :update-status "상태 메시지 업데이트..." - :add-a-status "상태 메시지 추가..." - :status-prompt "상태 메시지를 설정하세요. #해시태그를 사용하여 다른 사람이 날 찾는 것을 도울 수 있습니다." - :add-to-contacts "연락처에 추가" - :in-contacts "연락처에 있음" - :remove-from-contacts "연락처에서 제거" - :start-conversation "대화하기" - :send-transaction "송금하기" - :testnet-text "현재 {{testnet}} 테스트 네트워크에 있습니다. 이 주소로 진짜 ETH나 SNT를 보내지 마세요!" - :mainnet-text "메인 네트워크에 있습니다. 진짜 ETH가 전송될 것입니다." - - ;;make_photo - :image-source-title "프로필 이미지" - :image-source-make-photo "캡처" - :image-source-gallery "갤러리에서 선택하기" - - ;;sharing - :sharing-copy-to-clipboard "클립보드로 복사" - :sharing-share "공유하기" - :sharing-cancel "취소" - - :browsing-title "브라우저" - :browsing-open-in-web-browser "웹 브라우저로 열기" - :browsing-cancel "취소" - - ;;sign-up - :contacts-syncronized "연락처가 동기화되었습니다" - :confirmation-code (str "감사합니다! 확인 코드를 문자 메시지로 보내 드렸습니다. " - "전화번호 검증을 위해 확인 코드를 입력해 주세요") - :incorrect-code (str "잘못된 코드입니다. 다시 입력해 주세요") - :phew-here-is-your-passphrase "휴~ 꽤 어려웠어요. 이것이 당신의 암호문입니다. *이걸 적어두신 다음 안전한 곳에 보관하세요!* 계정을 복구할 때 필요합니다." - :here-is-your-passphrase "이것이 당신의 암호문입니다. *이걸 적어두신 다음 안전한 곳에 보관하세요!* 계정을 복구할 때 필요합니다." - :here-is-your-signing-phrase "이건 당신의 서명 문구입니다. 거래를 승인할 때 필요합니다. *이걸 적어두신 다음 안전한 곳에 보관하세요!*" - :phone-number-required "여기를 눌러 전화번호를 검증하시면 친구들을 찾아드리겠습니다" - :shake-your-phone "버그나 건의사항이 있나요? 폰을 ~흔들어~ 보세요!" - :intro-status "계정을 설정하고 설정을 변경하려면 저와 채팅하세요!" - :intro-message1 "Status에 오신 것을 환영합니다!\n이 메시지를 탭하여 비밀번호를 설정하고 시작하세요!" - :account-generation-message "잠시만요, 계정을 만들려면 엄청 복잡한 수학 문제를 풀어야 합니다. 기다려 주세요!" - :move-to-internal-failure-message "몇몇 중요한 파일을 외부 저장소에서 내부 저장소로 옮기기 위해 권한이 필요합니다. 추후 버전에서는 외부 저장소를 사용하지 않을 것입니다." - :debug-enabled "디버그 서버가 시작되었습니다! 같은 네트워크 상의 컴퓨터에서 *status-dev-cli scan*를 실행하여 디버그 서버를 확인하세요." - - ;;phone types - :phone-e164 "국제전화 1" - :phone-international "국제전화 2" - :phone-national "국내" - :phone-significant "전국 전화번호" - - ;;chats - :chats "채팅" - :delete-chat "채팅 삭제" - :new-group-chat "새 그룹 채팅" - :new-public-group-chat "공개 채팅에 참여하기" - :edit-chats "채팅방 편집" - :search-chats "채팅 검색하기" - :empty-topic "토픽 없음" - :topic-format "잘못된 포맷 [a-z0-9\\-]+" - :public-group-topic "토픽" - - ;;discover - :discover "찾아보기" - :none "없음" - :search-tags "검색할 태그를 입력하세요" - :popular-tags "인기 급상승 태그" - :recent "최근 상태 메시지" - :no-statuses-found "발견된 메시지 없음" - :chat "채팅" - :all "전체" - :public-chats "공개 채팅방" - :soon "곧" - :public-chat-user-count "{{count}}명" - :dapps "ÐApps" - :dapp-profile "ÐApp 프로필" - :no-statuses-discovered "발견된 상태 메시지 없음" - :no-statuses-discovered-body "누군가가 상태 메시지를 올리면\n여기 나타납니다." - :no-hashtags-discovered-title "발견된 해시태그 없음" - :no-hashtags-discovered-body "유명세를 탄 해시태그가\n여기 나타납니다." - - ;;settings - :settings "설정" - - ;;contacts - :contacts "연락처" - :new-contact "새 연락처" - :delete-contact "연락처 삭제" - :delete-contact-confirmation "이 연락처가 삭제됩니다" - :remove-from-group "그룹에서 삭제" - :edit-contacts "연락처 편집" - :search-contacts "연락처 검색" - :contacts-group-new-chat "새 채팅 시작" - :choose-from-contacts "연락처에서 선택하기" - :no-contacts "아직 연락처가 없습니다" - :show-qr "QR코드 표시" - :enter-address "주소 입력" - :more "더보기" - - ;;group-settings - :remove "제거" - :save "저장" - :delete "삭제" - :clear-history "대화내역 삭제" - :mute-notifications "알림 비활성화" - :leave-chat "채팅방 나가기" - :chat-settings "채팅방 설정" - :edit "편집" - :add-members "멤버 추가" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "새 그룹" - :reorder-groups "그룹 재정렬" - :edit-group "그룹 편집" - :delete-group "그룹 삭제" - :delete-group-confirmation "이 그룹을 그룹 목록에서 삭제합니다. 연락처에는 영향을 미치지 않습니다." - :delete-group-prompt "연락처에는 영향이 없습니다" - :contact-s {:one "연락처" - :other "연락처"} - - ;;protocol - :received-invitation "채팅방 초대" - :removed-from-chat "그룹 채팅방에서 쫓겨났습니다" - :left "떠남" - :invited "초대됨" - :removed "쫓겨남" - :You "당신" - - ;;new-contact - :add-new-contact "새 연락처 추가" - :scan-qr "QR코드 스캔" - :name "이름" - :address-explication "공개 키는 Ethereum 주소를 생성하기 위해 사용되며 숫자와 알파벳으로 이루어져 있습니다. 당신의 프로필에서 쉽게 찾을 수 있습니다." - :enter-valid-public-key "올바른 공개 키를 입력하거나 QR 코드를 스캔하세요" - :contact-already-added "이미 추가되어 있는 연락처입니다." - :can-not-add-yourself "자기 자신을 추가할 수 없습니다" - :unknown-address "알 수 없는 주소" - - ;;login - :connect "접속" - :address "주소" - :password "비밀번호" - :sign-in-to-status "Status에 로그인" - :sign-in "로그인" - :wrong-password "잘못된 비밀번호" - :enter-password "비밀번호 입력" - - ;;recover - :passphrase "암호문" - :recover "복구" - :twelve-words-in-correct-order "12개 단어를 올바른 순서로 입력하세요" - - ;;accounts - :recover-access "계정 복구" - :create-new-account "새 계정 생성" - - ;;wallet-qr-code - :done "완료" - - ;;validation - :invalid-phone "잘못된 전화번호" - :amount "액수" - - ;;transactions - :confirm "승인" - :transaction "거래" - :unsigned-transaction-expired "승인되지 않은 거래 만료됨" - :status "상태" - :recipient "수신인" - :to "수신자" - :from "송금자" - :data "데이터" - :got-it "알겠습니다" - :block "블록" - :hash "해시값" - :gas-limit "최대 가스 사용량" - :gas-price "가스 가격" - :gas-used "가스 사용량" - :cost-fee "비용" - :nonce "Nonce" - :confirmations "이체확인(컨펌)" - :confirmations-helper-text "거래가 안전하게 처리된 것을 확실히 하기 위해 이체확인이 12개를 넘어갈 때 까지 기다려주세요" - :copy-transaction-hash "거래 해시값 복사" - :open-on-etherscan "Etherscan.io에서 보기" - - ;;webview - :web-view-error "이런, 오류입니다" - - ;;testfairy warning - :testfairy-title "경고!" - :testfairy-message "나이틀리 빌드에서 설치된 앱을 사용중입니다. 이 빌드에는 테스트를 위해 세션 녹화 기능이 포함되어 있으며, Wi-Fi가 연결되어 있을 시 앱과의 모든 상호작용이 (비디오와 로그로) 저장됩니다. 이 데이터는 개발자들이 문제를 확인하기 위해 사용할 수 있습니다. 녹화된 정보에 비밀번호는 포함되어 있지 않습니다. 세션 녹화는 앱이 나이틀리 빌드에서 설치되었을 때만 진행됩니다. Play 스토어 또는 TestFlight를 통해 설치했을 시에는 녹화가 진행되지 않습니다." - - ;; wallet - :wallet "지갑" - :wallets "지갑들" - :your-wallets "내 지갑" - :main-wallet "주 지갑" - :wallet-error "데이터 로딩 중 오류 발생" - :wallet-send "송금" - :wallet-request "요청" - :wallet-exchange "거래소" - :wallet-assets "자산" - :wallet-add-asset "자산 추가" - :wallet-total-value "총자산" - :wallet-settings "지갑 설정" - :signing-phrase-description "거래를 승인하려면 비밀번호를 입력하세요. 위에 있는 단어들이 당신의 서명 문구와 일치하는지 확인하세요." - :wallet-insufficient-funds "자금 부족" - :request-transaction "거래 요청" - :send-request "요청 전송" - :share "공유" - :eth "ETH" - :currency "통화" - :usd-currency "USD" - :transactions "거래" - :transaction-details "거래 세부정보" - :transaction-failed "거래 실패" - :transactions-sign "승인" - :transactions-sign-all "모두 승인" - :transactions-sign-transaction "거래 승인" - :transactions-sign-later "나중에 승인" - :transactions-delete "거래 삭제" - :transactions-delete-content "거래가 '승인되지 않음' 목록에서 삭제됩니다" - :transactions-history "거래 내역" - :transactions-unsigned "승인되지 않음" - :transactions-history-empty "아직 거래 내역이 없습니다" - :transactions-unsigned-empty "승인되지 않은 거래가 없습니다" - :transactions-filter-title "거래 내역 필터" - :transactions-filter-tokens "토큰" - :transactions-filter-type "종류" - :transactions-filter-select-all "모두 선택" - :view-transaction-details "거래 세부정보 보기" - :transaction-description "거래가 안전하게 처리된 것을 확실히 하기 위해 이체확인이 12개를 넘어갈 때 까지 기다려주세요" - :transaction-sent "거래 전송됨" - :transaction-moved-text "다음 5분간 거래가 '승인되지 않음' 목록에 남아있을 것입니다" - :transaction-moved-title "거래 옮겨짐" - :sign-later-title "거래를 나중에 승인할까요?" - :sign-later-text "이 거래를 승인하려면 거래 내역을 확인하세요" - :not-applicable "승인되지 않은 거래에 사용할 수 없음" - - ;; Wallet Send - :wallet-choose-recipient "수신인 선택" - :wallet-choose-from-contacts "연락처에서 선택" - :wallet-address-from-clipboard "클립보드에서 가져오기" - :wallet-invalid-address "잘못된 주소: \n {{data}}" - :wallet-browse-photos "사진 보기" - :validation-amount-invalid-number "잘못된 액수입니다" - :validation-amount-is-too-precise "액수가 너무 정확합니다. 최소 송금 단위는 1 Wei (1x10^-18 ETH) 입니다." - - ;; network settings - :new-network "새 네트워크" - :add-network "네트워크 추가" - :add-new-network "새 네트워크 추가" - :existing-networks "기존 네트워크" - :add-json-file "JSON 파일 추가" - :paste-json-as-text "JSON 텍스트로 붙여넣기" - :paste-json "JSON 붙여넣기" - :specify-rpc-url "RPC URL을 지정하세요" - :edit-network-config "네트워크 설정 편집" - :connected "연결됨" - :process-json "JSON 처리" - :error-processing-json "JSON 처리중 오류 발생" - :rpc-url "RPC URL" - :remove-network "네트워크 제거" - :network-settings "네트워크 설정" - :edit-network-warning "주의하십시오. 네트워크 설정을 편집하면 이 네트워크를 사용하지 못할 수 있습니다." - :connecting-requires-login "다른 네트워크에 연결하기 위해서는 로그인이 필요합니다" - :close-app-title "경고!" - :close-app-content "앱이 닫힐 것입니다. 앱을 다시 열었을 때 설정된 네트워크가 사용될 것입니다" - :close-app-button "확인"}) diff --git a/src/status_im/translations/la.cljs b/src/status_im/translations/la.cljs deleted file mode 100644 index 0522b15bc03..00000000000 --- a/src/status_im/translations/la.cljs +++ /dev/null @@ -1,153 +0,0 @@ -(ns status-im.translations.la) - -(def translations - {;;common - :members-title "sodalis" - :not-implemented "ne attexit" - :chat-name "nomen colloquii" - :notifications-title "nuntiationes" - :offline "abesse" - - ;;drawer - :switch-users "utens ex cambit" - - ;;chat - :is-typing "conscribit" - :and-you "et tu" - :search-chat "perscruta colloquii" - :members {:one "I sodalis" - :other "{{count}} sodalis" - :zero "nullus sodalis"} - :members-active {:one "I soladis, I aktive" - :other "{{count}} sodalis, {{count}} aktivi" - :zero "nullus sodalis"} - :active-online "adesse" - :active-unknown "incognitus" - :available "relicus" - :no-messages "ne nuntii" - :suggestions-requests "precatus" - :suggestions-commands "jussum" - - ;;sync - :sync-in-progress "synchronizari..." - :sync-synced "synchronizatum esse" - - ;;messages - :status-sending "allegere" - :status-pending "tractare" - :status-sent "deditum iri" - :status-seen-by-everyone "adspectatum esse ab omnibus" - :status-seen "adspectatum esse" - :status-delivered "deditum esse" - :status-failed "vitiosus" - - ;;datetime - :datetime-ago-format "{{ago}} {{number}} {{time-intervals}}" - :datetime-second {:one "pars minuta secunda horae" - :other "pars minuta secunda horae"} - :datetime-minute {:one "pars minuta horae" - :other "pars minuta horae"} - :datetime-hour {:one "hora" - :other "horae"} - :datetime-day {:one "dies" - :other "dies"} - :datetime-ago "anto" - :datetime-yesterday "heri" - :datetime-today "hodie" - - ;;profile - :profile "biographia" - :message "nuntium" - :not-specified "ne adnotans" - :public-key "publica clavis" - :phone-number "nummerus telephonicus" - :add-to-contacts "adicere ad contagio" - ;;make_photo - :image-source-title "photographia bigraphiae" - :image-source-make-photo "photographere" - :image-source-gallery "eligere ad pinacothecae" - ;;sign-up - :contacts-syncronized "contagium sui synchronizatum esse." - :confirmation-code (str "gratias tibi! misimus nuntium conscriptionis cum notae. " - "transcribe notam, ut nummerum telefonicum possimus verificare.") - :incorrect-code (str "nos paenite, nota falsa, perscribe denuo") - :phew-here-is-your-passphrase "*uph* erat gravor. hic phrasis arcana sui est, *exscribe et adserva!* necesse est, ut benificium sui recreare." - :here-is-your-passphrase "hic phrasis arcana sui est, *exscribe et adserva!* necesse est, ut benificium sui recreare." - :phone-number-required "attige ipse, et transcribe numerus telephonicus sui. deinde inveniar amici sui." - :intro-status "habere colloquium cum me, ut syngraphus sui congerit et moderationes sui mutare!" - :intro-message1 "salve! hic Status est!.\n attige id nuntium, ut signum arcanum sui congere et incipere!" - :account-generation-message "mane! cumputo benificium sui." - - ;;chats - :chats "colloquia" - :new-group-chat "novum colloquium circli" - - ;;discover - :discover "invenite" - :none "nihil" - :search-tags "perscribe hic verbum querentis" - :popular-tags "favorabilis opiones" - :recent "novae" - :no-statuses-discovered "nullus status repertum esse" - - ;;settings - :settings "moderationes" - - ;;contacts - :contacts "contagiis" - :new-contact "novum contagium" - :contacts-group-new-chat "novum colloquium circli" - :no-contacts "Nulla contagio" - :show-qr "QR-nota elucere" - - ;;group-settings - :remove "amovere" - :save "apothecare" - :clear-history "processus amovere" - :chat-settings "moderationes colloquiorum" - :edit "commutare" - :add-members "sodalis adjungere" - ;;commands - :chat-send-eth "{{amount}} ETH" - ;;new-group - ;;participants - ;;protocol - :received-invitation "nancisci colloquium circli" - :removed-from-chat "ex colloquium circli relinquere" - :left "desse" - :invited "invitavisse" - :removed "reliquisse" - :You "tu" - - ;;new-contact - :add-new-contact "nova contagio adjungere" - :scan-qr "QR-nota photographere" - :name "nomen" - :address-explication "explain what adresses are" ;; TODO - :contact-already-added "iste contagio jam adjunctum esse" - :can-not-add-yourself "ne potest adjungere ipse!" - :unknown-address "ignotum adloquium" - - ;;login - :connect "iungere" - :address "adloquium" - :password "signum arcanum" - :wrong-password "falsum signum acranum" - - ;;recover - :passphrase "phrasis arcana" - :recover "recreare" - ;;accounts - :recover-access "restituere possessio" - ;;wallet-qr-code - :done "paratus" - :main-wallet "crumera caput" - - ;;validation - :invalid-phone "nummerus telephonicus" - :amount "summa" - ;;transactions - :status "status" - :recipient "acceptor" - ;;:webview - :web-view-error "Erratum ad adspectus"}) diff --git a/src/status_im/translations/lt.cljs b/src/status_im/translations/lt.cljs deleted file mode 100644 index 294a1c17f57..00000000000 --- a/src/status_im/translations/lt.cljs +++ /dev/null @@ -1,353 +0,0 @@ -(ns status-im.translations.lt) - -(def translations - {;;common - :members-title "Nariams" - :not-implemented "neįgyvendinta" - :chat-name "Pokalbio pavadinimas" - :notifications-title "Perspėjimai ir garsai" - :offline "Neprisijungęs" - :search-for "Ieškoti..." - :cancel "Atšaukti" - :next "Sekantis" - :open "Atidaryti" - :description "Aprašymas" - :url "URL" - :type-a-message "Įveskite žinutę..." - :type-a-command "Pradėkite rašyti komandą..." - :error "Klaida" - :unknown-status-go-error "Nežinoma status-go klaida" - :node-unavailable "Joks ethereum mazgas neveikia" - :yes "Taip" - :no "Ne" - - :camera-access-error "Norėdami suteikti reikalingą leidimą filmuoti, eikite į savo sistemos nustatymus ir įsitikinkite, kad pasirinktas Status > Camera." - :photos-access-error "Norėdami suteikti reikalingą leidimą fotografuoti, eikite į savo sistemos nustatymus ir įsitikinkite, kad pasirinktas Status > Photo." - - ;;drawer - :switch-users "Pasikeisti naudotoją" - :current-network "Dabartinis tinklas" - - ;;chat - :is-typing "rašo" - :and-you "ir tu" - :search-chat "Ieškoti pokalbyje" - :members {:one "1 narys" - :other "{{count}} nariai" - :zero "jokių narių"} - :members-active {:one "1 narys" - :other "{{count}} nariai" - :zero "jokių narių"} - :public-group-status "Viešas" - :active-online "Prisijungęs" - :active-unknown "Nežinomas" - :available "Prieinama" - :no-messages "Jokių žinučių" - :suggestions-requests "Užklausimai" - :suggestions-commands "Komandos" - :faucet-success "Dalintuvo užklausa gauta" - :faucet-error "Dalintuvo užklausos klaida" - - ;;sync - :sync-in-progress "Sinchronizuojama..." - :sync-synced "Sinchronizuota" - - ;;messages - :status-sending "Siunčiama..." - :status-pending "Laukiama" - :status-sent "Nusiųsta" - :status-seen-by-everyone "Visų matyta" - :status-seen "Matyta" - :status-delivered "Pristatyta" - :status-failed "Nepavykęs" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "sekundė" - :other "sekundės"} - :datetime-minute {:one "minutė" - :other "minutės"} - :datetime-hour {:one "valanda" - :other "valandos"} - :datetime-day {:one "diena" - :other "dienos"} - :datetime-ago "prieš" - :datetime-yesterday "vakar" - :datetime-today "šiandien" - - ;;profile - :profile "Profilis" - :edit-profile "Redaguoti profilį" - :message "Žinutė" - :not-specified "Nenurodyta" - :public-key "Viešasis raktas" - :phone-number "Telefono numeris" - :update-status "Atsinaujinkite būseną..." - :add-a-status "Pridėkite būseną..." - :status-prompt "Sukurkite būseną, kad žmonės žinotų, ką jūs siūlote. Taip pat galite naudoti #hashtagus." - :add-to-contacts "Pridėti prie kontaktų" - :in-contacts "Kontaktuose" - :remove-from-contacts "Ištrinti iš kontaktų" - :start-conversation "Pradėti pokalbį" - :send-transaction "Siųsti sandėrį" - :testnet-text "Jūs esate testiniame tinkle {{testnet}}. Nesiųskite tikru ETH arba SNT į jūsų adresą" - :mainnet-text "Jūs esate pagrindiniame tinkle. Tikri ETH bus siunčiami" - - ;;make_photo - :image-source-title "Profilio atvaizdas" - :image-source-make-photo "Fotografuoti" - :image-source-gallery "Pasirinkti iš galerijos" - - ;;sharing - :sharing-copy-to-clipboard "Kopijuoti į iškarpinę" - :sharing-share "Dalintis..." - :sharing-cancel "Atšaukti" - - :browsing-title "Naršyti" - :browsing-open-in-web-browser "Atverti naršyklėje" - :browsing-cancel "Naršyti" - - ;;sign-up - :contacts-syncronized "Jūsų kontaktai buvo sinchronizuoti" - :confirmation-code (str "Dėkui! Mes išsiuntėme jums teksto pranešimą su patvirtinimo kodu. " - "Prašau nurodyti šį kodą, kad patvirtintumėte savo telefono numerį") - :incorrect-code (str "Atsiprašome, kodas neteisingas, prašome įvesti jį dar kartą") - :phew-here-is-your-passphrase "Vajė, buvo sunkoka, čia yra jūsų slaptažodis, *išsaugokite jį!* jeigu reikės atkurti jūsų paskyrą." - :here-is-your-passphrase "Čia yra jūsų slaptafrazė, *išsaugokite ją!* jeigu reikės atkurti jūsų paskyrą." - :here-is-your-signing-phrase "Čia yra jūsų pasirašymo frazė. Ją naudosite, kad patvirtintumėte savo sandorius. *Užsirašykite ir saugokite!*" - :phone-number-required "Palieskite čia, jei norite įvesti savo telefono numerį, tam kad rasti jūsų draugus" - :shake-your-phone "Radote klaidą arba turite pasiūlymų? Tiesiog ~pakratykite~ savo telefoną!" - :intro-status "Kalbėkite su manimi, kad nustatytumėte savo paskyrą ir pakeistumėte savo nustatymus!" - :intro-message1 "Sveiki atvykę į Status!\nPalieskite šį pranešimą, kad nustatytumėte slaptažodį ir pradėtumėte!" - :account-generation-message "Sekundėlę, vyksta nemenki skaičiavimai! Generuojama jūsų paskyra!" - :move-to-internal-failure-message "Mums reikia perkelti keletą svarbių failų iš išorinės į vidinę atmintį. Norėdami tai padaryti, mums reikia jūsų leidimo. Būsimose versijose mes nenaudosime išorinės atminties." - :debug-enabled "Debug serveris buvo paleistas! Dabar galite paleisti *status-dev-cli scan*, kad surastumėte serverį iš savo kompiuterio tame pačiame tinkle." - - ;;phone types - :phone-e164 "Tarptautinis 1" - :phone-international "Tarptautinis 2" - :phone-national "Vietinis" - :phone-significant "Reikšmingas" - - ;;chats - :chats "Pokalbiai" - :delete-chat "Trinti pokalbį" - :new-group-chat "Naujas grupinis pokalbis" - :new-public-group-chat "Jungtis prie grupinio pokalbio" - :edit-chats "Redaguoti grupinius pokalbius" - :search-chats "Ieškoti pokalbių" - :empty-topic "Tuščia tema" - :topic-format "Netinkamas formatas [a-z0-9\\-]+" - :public-group-topic "Tema" - - ;;discover - :discover "Atrasti" - :none "Jokio" - :search-tags "Įveskite paieškos kriterijus" - :popular-tags "Populiarūs hashtagai" - :recent "Paskiausios būsenos" - :no-statuses-found "Jokių būsenų nerasta" - :chat "Pokalbis" - :all "Viskas" - :public-chats "Vieši pokalbiai" - :soon "Greitai" - :public-chat-user-count "{{count}} asmenys" - :dapps "DApp'ai" - :dapp-profile "ÐApp'ų profilis" - :no-statuses-discovered "Jokių būsenų nerasta" - :no-statuses-discovered-body "Kai kas nors paskelbia būseną,\nmatysite ją čia." - :no-hashtags-discovered-title "Jokių hashtagų nerasta" - :no-hashtags-discovered-body "Kai hashtagas tampa populiarus,\nmatysite jį čia." - - ;;settings - :settings "Nustatymai" - - ;;contacts - :contacts "Kontaktai" - :new-contact "Nauji kontaktai" - :delete-contact "Trinti kontaktą" - :delete-contact-confirmation "Šis įrašas bus pašalintas iš kontaktų" - :remove-from-group "Pašalinti iš grupės" - :edit-contacts "Redaguoti kontaktus" - :search-contacts "Ieškoti kontaktų" - :contacts-group-new-chat "Pradėti naują pokalbį" - :choose-from-contacts "Pasirinkti iš kontaktų" - :no-contacts "Jokių kontaktų nerasta" - :show-qr "Rodyti QR" - :enter-address "Įveskite adresą" - :more "daugiau" - - ;;group-settings - :remove "Pašalinti" - :save "išsaugoti" - :delete "Ištrinti" - :clear-history "Naikinti istoriją" - :mute-notifications "Nutildyti pranešimus" - :leave-chat "Palikti pokalbį" - :chat-settings "Pokalbio nustatymai" - :edit "Redaguoti" - :add-members "Pridėti narių" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nauja grupė" - :reorder-groups "Rūšiuoti grupes" - :edit-group "Redaguoti grupę" - :delete-group "Trinti grupę" - :delete-group-confirmation "Ši grupė bus pašalinta iš jūsų grupių sarašo. Jūsų kontaktai nepakis." - :delete-group-prompt "Tai neįtakos jūsų kontaktų" - :contact-s {:one "kontaktas" - :other "kontaktai"} - - ;;protocol - :received-invitation "gautas pakvietimas į pokalbį" - :removed-from-chat "pašalino tave iš pokalbio" - :left "išėjo" - :invited "pakviestas" - :removed "perkeltas" - :You "Tu" - - ;;new-contact - :add-new-contact "Pridėti naują kontaktą" - :scan-qr "Skanuoti QR" - :name "Vardas" - :address-explication "Gal čia turėtų būti tekstas, kuriame būtų paaiškinta, kas yra adresas ir kur jo ieškoti" - :enter-valid-public-key "Įveskite teisingą viešąjį raktą arba skanuokite QR kodą" - :contact-already-added "Šis kontaktas jau pridėtas" - :can-not-add-yourself "Negalima pridėti savęs" - :unknown-address "Nežinomas adresas" - - ;;login - :connect "Jungtis" - :address "Adresas" - :password "Slaptažodis" - :sign-in-to-status "Jungtis į Status" - :sign-in "Prisijungti" - :wrong-password "Neteisingas slaptažodis" - :enter-password "Įveskite slaptažodį" - - ;;recover - :passphrase "Slaptafrazė" - :recover "Atkurti" - :twelve-words-in-correct-order "12 žodžių teisinga tvarka" - - ;;accounts - :recover-access "Atkurti prieigą" - :create-new-account "Sukurti naują paskyrą" - - ;;wallet-qr-code - :done "Baigta" - - ;;validation - :invalid-phone "Neteisingas telefono numeris" - :amount "Paskyra" - - ;;transactions - :confirm "Patvirtinti" - :transaction "Sandėris" - :unsigned-transaction-expired "Nepasirašytas sandėris baigėsi" - :status "Status" - :recipient "Gavėjas" - :to "Į" - :from "Iš" - :data "Duomenys" - :got-it "Gavo" - :block "Blokas" - :hash "Hash" - :gas-limit "Gas limitas" - :gas-price "Gas kaina" - :gas-used "Gas sunaudota" - :cost-fee "Kaina/Mokestis" - :nonce "Nonce" - :confirmations "Patvirtinimas" - :confirmations-helper-text "Jei norite būti tikri, kad jūsų sandoris nebus kompromituotas, palaukite, kol jis gaus mažiausiai 10 blokų patvirtinimą" - :copy-transaction-hash "Kopijuoti sandėrio hash" - :open-on-etherscan "Atidaryti Etherscan" - - ;;webview - :web-view-error "vajė, klaida" - - ;;testfairy warning - :testfairy-title "Įspėjimas!" - :testfairy-message "Naudojate programą, įdiegtą iš naktinio kūrimo. Testavimo tikslais šiame kūrime įrašomas seansas, jei naudojamas \"Wi-Fi\" ryšys, taigi visa sąveika su programa išsaugoma (kaip vaizdo įrašas ir žurnalas) ir gali būti naudojama kūrėjų komandos, kad ištirtų galimus klausimus. Išsaugotas vaizdo įrašas/žurnalas neįtraukia slaptažodžių. Įrašymas atliekamas tik tada, jei programa yra įdiegta iš naktinio kūrimo. Nieko nėra įrašoma, jei programa įdiegta iš \"PlayStore\" arba \"TestFlight\"" - - ;; wallet - :wallet "Piniginė" - :wallets "Piniginės" - :your-wallets "Jūsų piniginės" - :main-wallet "Pagrindinė piniginė" - :wallet-error "Klaida užkraunant duomenis" - :wallet-send "Siųsti" - :wallet-request "Užklausa" - :wallet-exchange "Keitykla" - :wallet-assets "Aktyvai" - :wallet-add-asset "Pridėti aktyvą" - :wallet-total-value "Bendra vertė" - :wallet-settings "Piniginės nustatymai" - :signing-phrase-description "Pasirašykite sandorį įvesdami savo slaptažodį. Įsitikinkite, kad aukščiau esantys žodžiai atitinka jūsų slaptą pasirašymo frazę" - :wallet-insufficient-funds "Nepakankamos lėšos" - :request-transaction "Reikalauti sandėrio" - :send-request "Siųsti užklausą" - :share "Dalintis" - :eth "ETH" - :currency "Valiuta" - :usd-currency "USD" - :transactions "Sandėriai" - :transaction-details "Sandėrių detalės" - :transaction-failed "Sandoris nepavyko" - :transactions-sign "Pasirašyti" - :transactions-sign-all "Pasirašyti viską" - :transactions-sign-transaction "Pasirašyti sandorį" - :transactions-sign-later "Pasirašyti veliau" - :transactions-delete "Trinti sandorį" - :transactions-delete-content "Sandoris bus pašalintas iš nepasirašytų sarašo" - :transactions-history "Istorija" - :transactions-unsigned "Nepasirašyta" - :transactions-history-empty "Tu neturi sandorių istorijos" - :transactions-unsigned-empty "Jūs neturite nepasirašytų sandorių" - :transactions-filter-title "Filtruoti istoriją" - :transactions-filter-tokens "Žėtonai" - :transactions-filter-type "Tipas" - :transactions-filter-select-all "Rinktis viską" - :view-transaction-details "Žiūrėti sandėrių detales" - :transaction-description "Jei norite būti tikri, kad jūsų sandoris nebus kompromituotas, palaukite, kol jis gaus mažiausiai 10 blokų patvirtinimą" - :transaction-sent "Sandėris išsiųstas" - :transaction-moved-text "Sandėris sėkmingai perkeltas į “Nepasirašytos”" - :transaction-moved-title "Sandėris perkeltas" - :sign-later-title "Pasirašyti sandėrį vėliau?" - :sign-later-text "Galėsite prisijungti prie operacijų istorijos" - :not-applicable "Netaikoma nepasirašytiems sandėriams" - - ;; Wallet Send - :wallet-choose-recipient "Pasirinkti gavėją" - :wallet-choose-from-contacts "Pasirinkti iš kontaktų" - :wallet-address-from-clipboard "Įklijuoti adresą" - :wallet-invalid-address "Neteisingas adresas: \n {{data}}" - :wallet-invalid-chain-id "Tinklas nesutampa su: \n {{data}}" - :wallet-browse-photos "Naršyti žinutes" - :validation-amount-invalid-number "Kiekio skaičius netinkamas" - :validation-amount-is-too-precise "Suma yra per daug tiksli. Mažiausias vienetas, kurį galite siųsti, yra 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Naujas tinklas" - :add-network "Pridėti tinklą" - :add-new-network "Pridėti naują tinklą" - :existing-networks "Egzistuojantys tinklai" - :add-json-file "Pridėti JSON failą" - :paste-json-as-text "Įklijuoti JSON kaip tekstą" - :paste-json "Įklijuoti JSON" - :specify-rpc-url "Nurodykite RPC URL" - :edit-network-config "Redaguoti tinklo konfiguraciją" - :connected "Jungtis" - :process-json "Apdoroti JSON" - :error-processing-json "Klaida apdorojant JSON" - :rpc-url "RPC URL" - :remove-network "Nutolęs tinklas" - :network-settings "Tinklo nustatymai" - :edit-network-warning "Būkite atsargūs, redaguodami šio tinklo duomenis, galite padaryti jį sau neprieinamą" - :connecting-requires-login "Prisijungimas prie kito tinklo reikalauja prisijungimo" - :close-app-title "Įspėjimas!" - :close-app-content "Programa bus uždaryta. Kai iš naujo paleisite ją, pasirinktas tinklas bus naudojamas." - :close-app-button "Patvirtinti"}) diff --git a/src/status_im/translations/lv.cljs b/src/status_im/translations/lv.cljs deleted file mode 100644 index a2675cfacba..00000000000 --- a/src/status_im/translations/lv.cljs +++ /dev/null @@ -1,237 +0,0 @@ -(ns status-im.translations.lv) - -(def translations - {;;common - :members-title "Dalībnieki" - :not-implemented "!tādas funkcijas pagaidām nav" - :chat-name "Čata nosaukums" - :notifications-title "Paziņojumi un Skaņas" - :offline "Offline" - :search-for "Meklēt..." - :cancel "Atcelt" - :next "Tālāk" - :type-a-message "Raksti īsziņu..." - :type-a-command "Ievadi komandu..." - :error "Kļūda" - - :camera-access-error "Kļūda, nav atļaujas piekļūt kamerai. Lūdzu, iestatījumos pārliecinies, ka Status > Camera ir izvēlēts." - :photos-access-error "Kļūda, nav atļaujas piekļūt fotogrāfijām. Lūdzu, iestatījumos pārliecinies, ka Status > Photos ir izvēlēts." - - ;;drawer - :switch-users "Mainīt lietotāju" - :current-network "Tīkls" - - ;;chat - :is-typing "raksta" - :and-you "un tu" - :search-chat "Meklēt čatu" - :members {:one "1 dalībnieks" - :other "{{count}} dalībnieki" - :zero "nav dalībnieku"} - :members-active {:one "1 dalībnieks" - :other "{{count}} dalībnieki" - :zero "nav dalībnieku"} - :public-group-status "Publisks" - :active-online "Online" - :active-unknown "Nezināms" - :available "Pieejams" - :no-messages "Nav īsziņu" - :suggestions-requests "Pieprasījums" - :suggestions-commands "Komandas" - :faucet-success "Faucet pieprasījums saņemts" - :faucet-error "Faucet pieprasījuma kļūda" - - ;;sync - :sync-in-progress "Sinhronizē..." - :sync-synced "Sinhronizēts" - - ;;messages - :status-sending "Sūta" - :status-pending "Neizlemts" - :status-sent "Aizsūtīts" - :status-seen-by-everyone "Apskatīts" - :status-seen "Apskatīts" - :status-delivered "Piegādāts" - :status-failed "Neizdevās" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "sekunde" - :other "sekundes"} - :datetime-minute {:one "minūte" - :other "minūtes"} - :datetime-hour {:one "stunda" - :other "stundas"} - :datetime-day {:one "diena" - :other "dienas"} - :datetime-ago "pirms" - :datetime-yesterday "vakar" - :datetime-today "šodien" - - ;;profile - :profile "Profils" - :edit-profile "Rediģēt profilu" - :message "Īsziņa" - :not-specified "Nav norādīts" - :public-key "Public key" - :phone-number "Telefona numurs" - :update-status "Rediģēt aprakstu..." - :add-a-status "Pievienot aprakstu..." - :status-prompt "Pievieno aprakstu lai cilvēki zinātu ko tu piedāvā. Tu vari arī izlietot #hashtagus." - :add-to-contacts "Pievieno kontaktu" - :in-contacts "Kontaktos" - :remove-from-contacts "Izdzēst kontaktu" - :start-conversation "Sākt sarunu" - :send-transaction "Sūtīt transakciju" - ;;make_photo - :image-source-title "Profila bilde" - :image-source-make-photo "Fotografēt" - :image-source-gallery "Izvēlēties no galerijas" - - ;;sharing - :sharing-copy-to-clipboard "Copy to clipboard" - :sharing-share "Dalīties..." - :sharing-cancel "Atcelt" - - :browsing-title "Browse" - :browsing-open-in-web-browser "Atvert pārlūkprogrammā" - :browsing-cancel "Atcelt" - - ;;sign-up - :contacts-syncronized "Jūsu kontakti ir sinhronizēti" - :confirmation-code (str "Paldies! Mēs nosūtijām tev īsziņu ar apstiprinājuma kodu" - "code. Lūdzu ievadi apstiprinājuma kodu lai verificētu savu telefona numuru") - :incorrect-code (str "Nepareizs kods, mēģiniet vēlreiz") - :phew-here-is-your-passphrase "*Fuh* tas bija grūti, re, te ir tavs jauns passphrase, *pieraksti un sargā to!* Tev viņš būs vajadzīgs lai atgūtu pieju." - :here-is-your-passphrase "Te ir tavs jauns passphrase, *pieraksti un sargā to!* Tev viņš būs vajadzīgs lai atgūtu pieju savam kontam." - :phone-number-required "Ievadi savu telefonu un es atradīšu tavus draugus." - :shake-your-phone "Atradi kļūdu? Pastāsti mums par to! Krati telefonu!" - :intro-status "Čato ar mani, ja gribi mainīt ustatījumus!" - :intro-message1 "Sveicināti! Uzspied lai sāktu!" - :account-generation-message "Lūdzu uzgaidi..." - :move-to-internal-failure-message "Mums vajag pārvietot dažus failus no eksternās uz interno glabātuvi. Lai mēs to varētu izdarīt mums vajag jūsu atļauju." - :debug-enabled "Debug serveris startēja! Tu vari izlietot *status-dev-cli scan* lai atrastu serveri." - - ;;phone types - :phone-e164 "International 1" - :phone-international "International 2" - :phone-national "National" - :phone-significant "Significant" - - ;;chats - :chats "Čati" - :delete-chat "Dzēst čatu" - :new-group-chat "Jauna grupa" - :new-public-group-chat "Pievienoties publiskajam čatam" - :edit-chats "Rediģēt čatu" - :search-chats "Meklēt čatu" - :empty-topic "Nav temata" - :topic-format "Nepareiz formāts [a-z0-9\\-]+" - :public-group-topic "Temats" - - ;;discover - :discover "Discover" - :none "None" - :search-tags "Type your search tags here" - :popular-tags "Popular tags" - :recent "Recent" - :no-statuses-discovered "No statuses discovered" - :no-statuses-found "No statuses found" - - ;;settings - :settings "Iestatījumi" - - ;;contacts - :contacts "Kontakti" - :new-contact "Jauns kontakts" - :delete-contact "Dzēst kontaktu" - :delete-contact-confirmation "Kontakts tiks dzēsts" - :remove-from-group "Dzēst no grupas" - :edit-contacts "Rediģēt kontakts" - :search-contacts "Meklēt kontaktus" - :contacts-group-new-chat "Sākt jaunu čatu" - :choose-from-contacts "Izvēlēties no kontaktiem" - :no-contacts "Kontaktu nav" - :show-qr "Parādīt QR" - :enter-address "Ieraksti adresi" - :more "vairāk" - - ;;group-settings - :remove "Noņemt" - :save "Saglabāt" - :delete "Dzēst" - :clear-history "Dzēst vēsturi" - :mute-notifications "Mute" - :leave-chat "Iziet no čata" - :chat-settings "Čata iestatījumi" - :edit "Rediģēt" - :add-members "Pievienot biedrus" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Jauna grupa" - :reorder-groups "Reorder groups" - :edit-group "Rediģēt grupu" - :delete-group "Dzēst grupu" - :delete-group-confirmation "Grupa tiks noņēmta. Tas neietikmēs tavus kontaktus" - :delete-group-prompt "Tas neietikmēs kontaktus" - :contact-s {:one "kontakts" - :other "kontakti"} - ;;participants - - ;;protocol - :received-invitation "čata uzaicinājums" - :removed-from-chat "noņema jūs no grupas čata" - :left "atstāja grupu" - :invited "uzaicināts" - :removed "noņemts" - :You "Tu" - - ;;new-contact - :add-new-contact "Pievienot jaunu kontaktu" - :scan-qr "Skanēt QR" - :name "Vārds" - :address-explication "Maybe here should be some text explaining what an address is and where to look for it" - :enter-valid-public-key "Lūdzu ievadi publisko atslēgu, jeb skanē QR kodu" - :contact-already-added "Kontakts jau bija pievienots" - :can-not-add-yourself "Tu nevari pievienot sevi" - :unknown-address "Nezināmā adrese" - - ;;login - :connect "Savienoties" - :address "Adrese" - :password "Parole" - :sign-in-to-status "Ieiet Status" - :sign-in "Ieiet" - :wrong-password "Parole ievadīta nepareizi" - - ;;recover - :passphrase "Passphrase" - :recover "Atgūt" - :twelve-words-in-correct-order "12 vārdi" - - ;;accounts - :recover-access "Atgūt pieeju" - :create-new-account "Izveidot jaunu kontu" - - ;;wallet-qr-code - :done "Darīts" - :main-wallet "Galvenais maks" - - ;;validation - :invalid-phone "Nepareizs telefona numurs" - :amount "Summa" - ;;transactions - :confirm "Apstiprināt" - :transaction "Transakcija" - :status "Status" - :recipient "Saņēmējs" - :to "Kam" - :from "No" - :data "Dati" - :got-it "Got it" - - ;;:webview - :web-view-error "ups, kļūda"}) diff --git a/src/status_im/translations/ms.cljs b/src/status_im/translations/ms.cljs deleted file mode 100644 index c78de96a423..00000000000 --- a/src/status_im/translations/ms.cljs +++ /dev/null @@ -1,238 +0,0 @@ -(ns status-im.translations.ms) - -(def translations - {;;common - :members-title "Ahli" - :not-implemented "!tidak diimplementasikan" - :chat-name "Nama perbualan" - :notifications-title "Pemberitahuan dan bunyi" - :offline "Luar talian" - :search-for "Cari..." - :cancel "Batal" - :next "Seterusnya" - :type-a-message "Taip mesej..." - :type-a-command "Mulakan menaip arahan..." - :error "Gagal" - - :camera-access-error "Untuk memberi keizinan akses kamera yang diperlukan, sila pergi ke aturan sistem anda dan pastikan bahawa Status > Kamera telah dipilih." - :photos-access-error "Untuk memberi keizinan akses gambar yang diperlukan, sila pergi ke aturan sistem anda dan pastikan bahawa Status > Gambar telah dipilih." - - ;;drawer - :switch-users "Tukar pengguna" - :current-network "Rangkaian sekarang" - - ;;chat - :is-typing "sedang menaip" - :and-you "dan anda" - :search-chat "Cari perbualan" - :members {:one "1 ahli" - :other "{{count}} ahli" - :zero "tiada ahli"} - :members-active {:one "1 ahli" - :other "{{count}} ahli" - :zero "tiada ahli"} - :public-group-status "Umum" - :active-online "Dalam talian" - :active-unknown "Tidak diketahui" - :available "Ada" - :no-messages "Tiada mesej" - :suggestions-requests "Permintaan" - :suggestions-commands "Arahan" - :faucet-success "Permintaan Faucet telah diterima" - :faucet-error "Permintaan Faucet gagal" - - ;;sync - :sync-in-progress "sedang disegerakan..." - :sync-synced "Dalam penyegeraan" - - ;;messages - :status-sending "Menghantar" - :status-pending "Belum selesai" - :status-sent "Dihantar" - :status-seen-by-everyone "Dilihat oleh semua" - :status-seen "Dilihat" - :status-delivered "Dihantar" - :status-failed "Gagal" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "saat" - :other "saat"} - :datetime-minute {:one "minit" - :other "minit"} - :datetime-hour {:one "jam" - :other "jam"} - :datetime-day {:one "hari" - :other "hari"} - :datetime-ago "lalu" - :datetime-yesterday "semalam" - :datetime-today "hari ini" - - ;;profile - :profile "Profail" - :edit-profile "Ubah profail" - :message "Mesej" - :not-specified "Tidak diberikan" - :public-key "Public key" - :phone-number "Nombor telefon" - :update-status "Kemaskini status anda..." - :add-a-status "Tambah pada status..." - :status-prompt "Ciptakan satu status untuk membantu pengguna lain mengetahui tentang perkara yang anda tawarkan. Anda juga boleh menggunakan #hashtag. " - :add-to-contacts "Tambah pada senarai kenalan" - :in-contacts "Dalam senarai kenalan" - :remove-from-contacts "Buang dari senarai kenalan" - :start-conversation "Mulakan perbualan" - :send-transaction "Hantar transaksi" - - ;;make_photo - :image-source-title "Gambar profail" - :image-source-make-photo "Rakam" - :image-source-gallery "Pilih dari galeri" - - ;;sharing - :sharing-copy-to-clipboard "Salin ke clipboard" - :sharing-share "Kongsi..." - :sharing-cancel "Batal" - - :browsing-title "Pelayar" - :browsing-open-in-web-browser "Buka dalam pelayar web" - :browsing-cancel "Batal" - - ;;sign-up - :contacts-syncronized "Senarai kenalan anda telah disegerakan" - :confirmation-code (str "Terima kasih! Kami telah menghantar kepada anda satu mesej mengandungi kod" - "pengesahan. Sila berikan kod tersebut untuk mengesahkan nombor telefon anda") - :incorrect-code (str "Maaf kod salah, sila masukkan sekali lagi") - :phew-here-is-your-passphrase "*Wah* itu sangatlah susah, ini adalah ayat pengesahan anda, *sila tulis dan pastikan ini selamat!* Anda akan memerlukan ia untuk memulihkan akses ke akaun anda." - :here-is-your-passphrase "Ini adalah ayat pengesahan anda, *sila tulis dan pastikan ini selamat!* Anda akan memerlukan ia untuk memulihkan akses ke akaun anda." - :phone-number-required "Sentuh disini untuk memasukkan nombor telefon anda & saya akan mecari rakan anda" - :shake-your-phone "Jumpa kerentanan atau terdapat cadangan? Hanya ~goncang~ telefon anda!" - :intro-status "Mulakan perbualan dengan saya untuk mencipta akaun anda dan mengubah aturan akaun anda!" - :intro-message1 "Selamat datang ke Status\nSila sentuh mesej ini untuk menetapkan kata lalaun & memulakan!" - :account-generation-message "Berikan saya sedikit masa, saya perlu menyelesaikan masalah matematik yang rumit untuk menciptakan akaun anda!" - :move-to-internal-failure-message "Kita perlu mengubah lokasi sedikit fail penting dari memori dalaman ke memori luaran. Untuk itu, kami perlukan kebenaran anda. Kami tidak akan menggunakan memori luaran pada versi akan datang." - :debug-enabled "Debug server telah dijalankan! Sekarang anda boleh menjalankan *status-dev-cli scan* untuk mecari sambungan server dari komputer anda dalam jaringan yang sama." - - ;;phone types - :phone-e164 "Internasional 1" - :phone-international "Internasional 2" - :phone-national "Nasional" - :phone-significant "Penting" - - ;;chats - :chats "Perbualan" - :delete-chat "Padam perbualan" - :new-group-chat "Perbualan kumpulan baru" - :new-public-group-chat "Sertai perbualan umum" - :edit-chats "Ubah perbualan" - :search-chats "Cari perbualan" - :empty-topic "Tiada topik" - :topic-format "Format salah [a-z0-9\\-]+" - :public-group-topic "Topik" - - ;;discover - :discover "Jelajah" - :none "Tiada" - :search-tags "Taip carian anda disini" - :popular-tags "Popular" - :recent "Terbaru" - :no-statuses-discovered "Tiada status ditemui" - :no-statuses-found "Tiada status ditemui" - - ;;settings - :settings "Aturan" - - ;;contacts - :contacts "Kenalan" - :new-contact "Kenalan baru" - :delete-contact "Padam kenalan" - :delete-contact-confirmation "Kenalan ini akan dipadam dari senarai kenalan anda" - :remove-from-group "Buang dari kumpulan" - :edit-contacts "Ubah kenalan" - :search-contacts "Cari kenalan" - :contacts-group-new-chat "Mulakan perbualan baru" - :choose-from-contacts "Pilih dari kenalan" - :no-contacts "Masih tiada kenalan" - :show-qr "Tunjuk kod QR" - :enter-address "Masukkan address" - :more "seterusnya" - - ;;group-settings - :remove "Buang" - :save "Simpan" - :delete "Padam" - :clear-history "Padam sejarah perbualan" - :mute-notifications "Pemberitahuan senyap" - :leave-chat "Tinggalkan perbualan" - :chat-settings "Aturan perbualan" - :edit "Ubah" - :add-members "Tambah ahli" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Kumpulan baru" - :reorder-groups "Susun kumpulan" - :edit-group "Ubah kumpulan" - :delete-group "Padam kumpulan" - :delete-group-confirmation "Kumpulan ini akan dipadam dari senarai kumpulan anda. Ini tidak akan menjejaskan senarai kenalan anda" - :delete-group-prompt "Ini tidak akan menjejaskan senarai kenalan anda" - :contact-s {:one "kenalan" - :other "kenalan"} - ;;participants - - ;;protocol - :received-invitation "menerima permintaan bual" - :removed-from-chat "membuang anda dari perbualan kumpulan" - :left "pergi" - :invited "dijemput" - :removed "dibuang" - :You "Anda" - - ;;new-contact - :add-new-contact "Tambah kenalan baru" - :scan-qr "Imbas kod QR" - :name "Nama" - :address-explication "Mungkin disini sepatutnya terdapat sedikit teks menjelaskan apa itu address dan dimana untuk melihatnya" - :enter-valid-public-key "Sila masukkan public key yang sah atau imbas kod QR" - :contact-already-added "Kenalan telah ditambah" - :can-not-add-yourself "Anda tidak boleh menambah diri anda sendiri" - :unknown-address "Address tidak diketahui" - - ;;login - :connect "Sambung" - :address "Address" - :password "Kata laluan" - :sign-in-to-status "Daftar masuk ke Status" - :sign-in "Daftar masuk" - :wrong-password "Kata laluan salah" - - ;;recover - :passphrase "Ayat pengesahan" - :recover "Pulihkan" - :twelve-words-in-correct-order "12 perkataan dalam susunan yang betul" - - ;;accounts - :recover-access "Pulihkan akses" - :create-new-account "Cipta akaun baru" - - ;;wallet-qr-code - :done "Selesai" - :main-wallet "Dompet utama" - - ;;validation - :invalid-phone "Nombor telefon salah" - :amount "Jumlah" - ;;transactions - :confirm "Sahkan" - :transaction "Transaksi" - :status "Status" - :recipient "Penerima" - :to "Ke" - :from "Dari" - :data "Maklumat" - :got-it "Ya" - - ;;:webview - :web-view-error "oops, gagal"}) diff --git a/src/status_im/translations/nb.cljs b/src/status_im/translations/nb.cljs deleted file mode 100644 index e5ae840c2e0..00000000000 --- a/src/status_im/translations/nb.cljs +++ /dev/null @@ -1,274 +0,0 @@ -(ns status-im.translations.nb) - -(def translations - {;;common - :members-title "Medlemmer" - :not-implemented "!ikke implementert" - :chat-name "Kallenavn" - :notifications-title "Notifikasjoner og lyd" - :offline "Avlogget" - :search-for "Søk etter..." - :cancel "Avbryt" - :next "Neste" - :type-a-message "Skriv en melding..." - :type-a-command "Start å skrive en kommando..." - :error "Feil" - :unknown-status-go-error "Ukjent status-go feil" - :node-unavailable "Ingen Ethereum node kjørende" - - :camera-access-error "For å tillate bruk av kamera, gå til systeminstillinger og sørg for at Status > Kamera er aktivert." - :photos-access-error "For å tillate bruk av bilder, gå til systeminstillinger og sørg for at Status > Bilder er aktivert." - - ;;drawer - :switch-users "Bytt brukere" - :current-network "Nåværende nettverk" - - ;;chat - :is-typing "skriver" - :and-you "og du" - :search-chat "Søk i meldinger" - :members {:one "1 medlem" - :other "{{count}} medlemmer" - :zero "ingen medlemmer"} - :members-active {:one "1 medlem" - :other "{{count}} medlemmer" - :zero "ingen medlemmer"} - :public-group-status "Offentlig" - :active-online "Pålogget" - :active-unknown "Ukjent" - :available "Tilgjengelig" - :no-messages "Ingen meldinger" - :suggestions-requests "Forespørsler" - :suggestions-commands "Kommandoer" - :faucet-success "Du har mottatt en faucet forespørsel" - :faucet-error "Faucet forespørselen feilet" - - ;;sync - :sync-in-progress "Synkroniserer..." - :sync-synced "Synkronisert" - - ;;messages - :status-sending "Sender" - :status-pending "Venter" - :status-sent "Sendt" - :status-seen-by-everyone "Sett av alle" - :status-seen "Sett" - :status-delivered "Levert" - :status-failed "Feilet" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "sekund" - :other "sekunder"} - :datetime-minute {:one "minutt" - :other "minutter"} - :datetime-hour {:one "time" - :other "timer"} - :datetime-day {:one "dag" - :other "dager"} - :datetime-ago "siden" - :datetime-yesterday "i går" - :datetime-today "i dag" - - ;;profile - :profile "Profil" - :edit-profile "Rediger profil" - :message "Melding" - :not-specified "Ikke spesifisert" - :public-key "Offentlig nøkkel" - :phone-number "Telefonnummer" - :update-status "Oppdater din status..." - :add-a-status "Legg til en status..." - :status-prompt "Legg til en status slik at andre forstår hva du tilbyr. Bruk gjerne #hashtag." - :add-to-contacts "Legg til i kontakter" - :in-contacts "I kontaker" - :remove-from-contacts "Fjern fra kontakter" - :start-conversation "Start en samtale" - :send-transaction "Send overføring" - - ;;make_photo - :image-source-title "Profilbilde" - :image-source-make-photo "Opptak" - :image-source-gallery "Velg fra bilder" - - ;;sharing - :sharing-copy-to-clipboard "Kopier til utklippstavle" - :sharing-share "Del..." - :sharing-cancel "Avbryt" - - :browsing-title "Utforsk" - :browsing-open-in-web-browser "Åpne i nettleser" - :browsing-cancel "Avbryt" - - ;;sign-up - :contacts-syncronized "Dine kontaker har blitt synkronisert" - :confirmation-code (str "Topp! Vi har sendt deg en tekstmelding med bekreftelseskode." - "Vennligst send oss tilbake koden for å verifisere at dette er din telefon") - :incorrect-code (str "Beklager, prøv på ny") - - :phew-here-is-your-passphrase "*Phew* det var vrient, her er din nye passordfrase, *skriv den ned og hold den for deg selv!* Du vil trenge den for å kunne gjenopprette kontoen." - :here-is-your-passphrase "Her er din nye passordfrase, *skriv den ned og hold den for deg selv!* Du vil trenge den for å kunne gjenopprette kontoen." - :here-is-your-signing-phrase "Her er din signeringsfrase. Du vil bruke den til å verifisere dine transaksjoner. *skriv den ned og hold den for deg selv!*" - :phone-number-required "Trykk her og skriv ditt telefonnummer, så skal jeg finne vennene dine" - :shake-your-phone "Har du funnet en feil eller har du tilbakemelding til oss? Bare ~rist~ på telefonen!" - :intro-status "Chat med meg for å sette opp kontoen din og endre instillinger!" - :intro-message1 "Velkommen til Status\nTrykk på denne beskjeden for å sette et passord og komme i gang!" - :account-generation-message "Gi meg et øyeblikk! Jeg må granske universet for å generere en konto til deg!" - :move-to-internal-failure-message "Vi må flytte noen viktige filer fra ekstern til intern lagring. For å kunne gjøre dette, trenger vi din godkjenning. Vi kommer ikke til å bruke ekstern lagring i fremtidige versjoner." - :debug-enabled "Debug server har blitt satt i gang! Du kan nå kjøre *status-dev-cli scan* for å finne serveren på PC'n din fra samme nettverk." - - ;;phone types - :phone-e164 "Internasjonal 1" - :phone-international "Internasjonal 2" - :phone-national "Nasjonal" - :phone-significant "Betydningsfull" - - ;;chats - :chats "Chats" - :delete-chat "Fjern melding" - :new-group-chat "Ny gruppemelding" - :new-public-group-chat "Bli med i offentlig chat" - :edit-chats "Rediger melding" - :search-chats "Søk meldinger" - :empty-topic "Tomt emne" - :topic-format "Feil format [a-z0-9\\-]+" - :public-group-topic "Emne" - - ;;discover - :discover "Oppdag" - :none "Ingen" - :search-tags "Søk etter etikett her" - :popular-tags "Populære etiketter" - :recent "Nylig" - :no-statuses-discovered "Ingen status oppdaget" - :no-statuses-found "Ingen status funnet" - - ;;settings - :settings "Instillinger" - - ;;contacts - :contacts "Kontaker" - :new-contact "Ny kontakt" - :delete-contact "Slett kontakt" - :delete-contact-confirmation "Kontakten vil bli fjernet fra kontaktlisten din" - :remove-from-group "Fjern fra gruppen" - :edit-contacts "Rediger kontakter" - :search-contacts "Søk kontakter" - :contacts-group-new-chat "Start ny chat" - :choose-from-contacts "Velg fra kontaker" - :no-contacts "Ingen kontaker enda" - :show-qr "Vis QR-kode" - :enter-address "Adresse" - :more "mer" - - ;;group-settings - :remove "Fjern" - :save "Lagre" - :delete "Slett" - :clear-history "Fjern historikk" - - :mute-notifications "Demp notifikasjoner" - - :leave-chat "Forlat chat" - :chat-settings "Chat instillinger" - :edit "Rediger" - :add-members "Legg til medlemmer" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Ny gruppe" - :reorder-groups "Organiser grupper" - :edit-group "Rediger gruppe" - :delete-group "Slett gruppe" - :delete-group-confirmation "Denne slettes fra gruppen, men vil fortsatt være tilgjengelig fra kontakter" - :delete-group-prompt "Dette vil ikke affektere kontakter" - :contact-s {:one "kontakt" - :other "kontaker"} - - ;;participants - - ;;protocol - :received-invitation "Motatt chat-invitasjon" - :removed-from-chat "fjernet deg fra gruppechatten" - :left "venstre" - :invited "invitert" - :removed "fjernet" - :You "Du" - - ;;new-contact - :add-new-contact "Legg til ny kontakt" - :scan-qr "Skan QR-kode" - :name "Navn" - :address-explication "Her bør det kanskje være tekst som forklarer hva en adresse er og hvor du finner den" - :enter-valid-public-key "Skriv en offentlig nøkkel eller skan en QR-kode" - :contact-already-added "Kontakten er allerede i kontaktlisten" - :can-not-add-yourself "Du kan ikke legge til deg selv" - :unknown-address "Ukjent adresse" - - ;;login - :connect "Koble til" - :address "Adresse" - :password "Passord" - :sign-in-to-status "Logg på Status" - :sign-in "Logg på" - :wrong-password "Feil passord" - - ;;recover - :passphrase "Passordfrase" - :recover "Gjenopprett" - :twelve-words-in-correct-order "12 ord i riktig rekkefølge" - - ;;accounts - :recover-access "Få tilbake tilgang" - :create-new-account "Opprett konto" - - ;;wallet-qr-code - :done "Ferdig" - - ;;validation - :invalid-phone "Feil telefonnummer" - :amount "Beløp" - - ;;transactions - :confirm "Bekreft" - :transaction "Transaksjon" - :status "Status" - :recipient "Mottager" - :to "Til" - :from "Fra" - :data "Data" - :got-it "Mottatt" - - ;;:webview - :web-view-error "oops, feil" - - ;;testfairy warning - :testfairy-title "Advarsel!" - :testfairy-message "Du bruker en nattlig versjon av applikasjonen. I den nattlige versjonen tar vi opp video og kjører en del logging som kan bli tatt i bruk av våre utviklere for å etterforske potensielle problemer. Dette gjelder kun den nattlige versjonen, versjoner som er installert fra PlayStore eller TestFlight blir ikke logget." - - ;; wallet - :wallet "Lommebok" - :wallets "Lommebøker" - :your-wallets "Dine lommebøker" - :main-wallet "Hovedlommebok" - :wallet-error "Feil ved lasting av data" - :wallet-send "Send" - :wallet-request "Forespørsel" - :wallet-exchange "Exchange" - :wallet-assets "Assets" - :wallet-add-asset "Add asset" - :wallet-total-value "Total verdi" - :wallet-settings "Lommeboksinstillinger" - :transactions "Transaksjoner" - :transactions-sign "Signer" - :transactions-sign-all "Signer alle" - :transactions-history "Historikk" - :transactions-unsigned "Usignert" - :transactions-history-empty "Du har ingen tidligere transaksjoner" - :transactions-unsigned-empty "Du har ingen usignerte transaksjoner" - :transactions-filter-title "Filtrer historikk" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Type" - :transactions-filter-select-all "Velg alle"}) diff --git a/src/status_im/translations/ne.cljs b/src/status_im/translations/ne.cljs deleted file mode 100644 index a4282925641..00000000000 --- a/src/status_im/translations/ne.cljs +++ /dev/null @@ -1,240 +0,0 @@ -(ns status-im.translations.ne) - -(def translations - {;;common - :members-title "सदस्यहरु" - :not-implemented "कार्यान्वयन भएको छैन" - :chat-name "च्याट नाम" - :notifications-title "सूचना र ध्वनि" - :offline "अफलाइन" - :search-for "॰॰॰ को लागि खोज" - :cancel "रद्द गर्नुहोस्" - :next "अर्को" - :type-a-message "सन्देश टाइप गरौं ॰॰॰" - :type-a-command "आदेश टाइप गरौं ॰॰॰" - :error "त्रुटि" - - :camera-access-error "कृपया क्यामेरा उपयोग गर्न आवश्यक अनुमति प्रदान गर्न, सिस्टम सेटिङ्ग भित्र गएर स्टेटस > क्यमेरा चयन भएको स्थिति निश्चित गर्नुहोस् ।" - :photos-access-error "कृपया फोटो उपयोग गर्न आवश्यक अनुमति प्रदान गर्न, सिस्टम सेटिङ्ग भित्र गएर स्टेटस > फोटो चयन भएको स्थिति निश्चित गर्नुहोस् ।" - - ;;drawer - :switch-users "उपयोगकर्ता बदलौं" - :current-network "चल्ती नेटवर्क" - - ;;chat - :is-typing "ले टाइप गर्दै हुनुहुन्छ" - :and-you "अनि तपाईं" - :search-chat "च्याटमा खोजौं" - :members {:one "१ सदस्य" - :other "{{count}} सदस्यहरु" - :zero "सदस्यहीन"} - :members-active {:one "१ सदस्य" - :other "{{count}} सदस्यहरु" - :zero "सदस्यहीन"} - :public-group-status "सार्वजनिक" - :active-online "अनलाइन" - :active-unknown "अज्ञात" - :available "उपलब्ध" - :no-messages "सन्देशहरु खाली" - :suggestions-requests "अनुरोधहरु" - :suggestions-commands "आदेशहरु" - :faucet-success "धारा अनुरोध प्राप्त भएको छ" - :faucet-error "धारा अनुरोध त्रुटि" - - ;;sync - :sync-in-progress "एकनास बनाइंदै छ ॰॰॰" - :sync-synced "एकनास छ" - - ;;messages - :status-sending "पठाउंदै" - :status-pending "बाँकी छ" - :status-sent "पठाइसकेको" - :status-seen-by-everyone "सबैले हेरिसकेको" - :status-seen "देखिसकेको" - :status-delivered "बुझाइएको" - :status-failed "असफल" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "सेकेन्ड" - :other "सेकेन्ड"} - :datetime-minute {:one "मिनेट" - :other "मिनेट"} - :datetime-hour {:one "घण्टा" - :other "घण्टा"} - :datetime-day {:one "दिन" - :other "दिन"} - :datetime-ago "पहिले" - :datetime-yesterday "हिजो" - :datetime-today "आज" - - ;;profile - :profile "प्रोफाइल" - :edit-profile "प्रोफाइल सम्पादन" - :message "सन्देश" - :not-specified "नतोकिएको" - :public-key "सार्वजनिक सांचो" - :phone-number "फोन नम्बर" - :update-status "तपाईंको स्टेटस अद्यतन गर्नुहोस् ॰॰॰" - :add-a-status "नया स्टेटस जोड्नुहोस् ॰॰॰" - :status-prompt "एउटा स्टेटस बनाउनुहोस् जसद्वारा तपांईले प्रस्ताव गर्नुभएको कुराहरु अरुहरुले थाहापाउन मद्दत पुगोस । तपांईले #ह्यास्ट्याग पनि प्रयोग गर्न सक्नुहुन्छ ।" - :add-to-contacts "सम्पर्कहरुमा जोड्नुहोस्" - :in-contacts "सम्पर्कहरुमा समावेस छ" - :remove-from-contacts "सम्पर्कहरुबाट हटाउनुहोस्" - :start-conversation "कुराकानी सुरु गर्नुहोस्" - :send-transaction "कारोबार पठाउनुहोस्" - - ;;make_photo - :image-source-title "प्रोफाइल तस्विर" - :image-source-make-photo "खिच्नुहोस" - :image-source-gallery "ग्यालेरीबाट चयन गर्नुहोस्" - - ;;sharing - :sharing-copy-to-clipboard "क्लिप्बोर्डमा प्रतिलिपि राख्नुहोस्" - :sharing-share "बांड्नुहोस् ॰॰॰" - :sharing-cancel "रद्द गर्नुहोस्" - - :browsing-title "ब्राउज" - :browsing-open-in-web-browser "वेब ब्राउजरमा खोल्ने" - :browsing-cancel "रद्द गर्नुहोस्" - - ;;sign-up - :contacts-syncronized "तपाईंको सम्पर्कहरु एकनास भयो" - :confirmation-code (str "धन्यवाद! हामीले तपाईंलाई पुष्टि कोड सहितको सन्देश पठाएका छौं । " - "कृपया आफ्नो फोन नम्बर पुष्टि गर्न त्यो कोड प्रस्तुत गर्नुहोस् ।") - :incorrect-code (str "माफ गर्नुहोस्, त्यो कोड गलत रहेछ, कृपया पुनः प्रविष्ट गर्नुहोस्") - :phew-here-is-your-passphrase "*उफऽऽऽ* त्यो कठिन थियो, तपाईंको गुप्तवाक्यांश प्रस्तुत छ, *यसलाई लेखेर सुरक्षित राख्नुहोस्* आफ्नो खाता पुनर्स्थापना गर्न यो अति आवश्यक छ ।" - :here-is-your-passphrase "तपाईंको गुप्तवाक्यांश प्रस्तुत छ, *यसलाई लेखेर सुरक्षित राख्नुहोस्* आफ्नो खाता पुनर्स्थापना गर्न यो अति आवश्यक छ ।" - :phone-number-required "यहाँ ट्याप गरेर तपाईंको फोन नम्बर प्रविष्ट गर्नुहोस् अनि म तपाईंको साथीहरु भेट्टाइ दिन्छु ।" - :shake-your-phone "त्रुटि फेलापार्नुभयो, अथवा सुझाव दिनु छ? बस फोन ~हल्लाउनुहोस्~!" - :intro-status "तपाईंको खाता सेटअप गर्न र सेटिङ्ग बदल्नको लागि म सँग च्याट गर्नुहोस् ।" - :intro-message1 "स्टेटसमा तपाईंलाई स्वागत छ\nतपाईंको पास्वर्ड सेटअप र सुरुवात गर्नको लागि यो सन्देसमा ट्याप गर्नुहोस् ।" - :account-generation-message "एकै छिन पर्खिनुहोस्, तपाईंको खाता उत्पन्न गर्न केही कठिन हिसाब गर्दै छु!" - :move-to-internal-failure-message "हामीले केही महत्वपूर्ण फाइलहरु बाह्य बाट आन्तरिक भण्डारणमा सार्न आवश्यक छ । यस्को लागि हामीलाई तपाईंको अनुमति चाहिन्छ । भावी संस्करणहरुमा हामी बाह्य भण्डारण प्रयोगमा ल्याउदैनौं ।" - :debug-enabled "त्रुटिसुधार सर्भर प्रवर्तित भयो! अब तपाईंले *status-dev-cli scan* कार्यान्वयन गरेर एकै नेटवर्कमा रहेको तपाईंको कम्प्युटरबाट सो सर्भर भेट्टाउन सक्नुहुन्छ ।" - - ;;phone types - :phone-e164 "अन्तराष्ट्रिय १" - :phone-international "अन्तराष्ट्रिय २" - :phone-national "राष्ट्रिय" - :phone-significant "महत्वपूर्ण" - - ;;chats - :chats "च्याटहरु" - :delete-chat "च्याट मेटाउने" - :new-group-chat "नया सामूहिक च्याट" - :new-public-group-chat "सार्वजनिक च्याटमा भाग लिने" - :edit-chats "च्याटहरुको सम्पादन" - :search-chats "च्याटहरुमा खोज" - :empty-topic "खाली विषय" - :topic-format "गलत ढाँचा [a-z0-9\\-]+" - :public-group-topic "विषय" - - ;;discover - :discover "भेट्टाउनुहोस्" - :none "बिना कुनै" - :search-tags "खोज्ने ट्यागहरु यहाँ टाइप गर्नुहोस्" - :popular-tags "लोकप्रिय ट्यागहरु" - :recent "भर्खरको" - :no-statuses-discovered "कुनैपनि स्टेटस भेट्टाइएन" - :no-statuses-found "कुनैपनि स्टेटस फेलापारिएन" - - ;;settings - :settings "सेटिङ्गहरु" - - ;;contacts - :contacts "सम्पर्कहरु" - :new-contact "नया सम्पर्क" - :delete-contact "सम्पर्क मेटाउने" - :delete-contact-confirmation "यो सम्पर्क तपाईंको सम्पर्कहरुबाट हटाइने छ" - :remove-from-group "समूह बाट हटाउने" - :edit-contacts "सम्पर्क सम्पादन" - :search-contacts "सम्पर्कहरुमा खोज" - :contacts-group-new-chat "नया च्याट सुरु" - :choose-from-contacts "सम्पर्कहरुबाट चयन गर्नुहोस्" - :no-contacts "हाल कुनै सम्पर्कहरु छैनन्" - :show-qr "QR देखाउने" - :enter-address "ठेगान प्रविष्ट गर्ने" - :more "अरु" - - ;;group-settings - :remove "हटाउनुहोस्" - :save "साँच्नुहोस्" - :delete "मेटाउनुहोस्" - :clear-history "इतिहास खाली गर्नुहोस्" - :mute-notifications "सूचनाहरु मौन बनाउनुहोस्" - :leave-chat "च्याटबाट बाहिरिनुहोस्" - :chat-settings "च्याट सेटिङ्गहरु" - :edit "सम्पादन" - :add-members "सदस्यहरु जोड्नुहोस्" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "नया समूह" - :reorder-groups "समूह पुन: क्रमबद्ध गर्नुहोस्" - :edit-group "समूह सम्पादन" - :delete-group "समूह मेटाउनुहोस्" - :delete-group-confirmation "यो समूह तपाईंको समूहहरुबाट हटाइने छ । यसले सम्पर्कहरुमा असर गर्दैन ।" - :delete-group-prompt "यसले सम्पर्कहरुमा असर गर्दैन ।" - :contact-s {:one "सम्पर्क" - :other "सम्पर्कहरु"} - - ;;participants - - ;;protocol - :received-invitation "च्याट निमन्त्रणा पाइयो" - :removed-from-chat "तपाईंलाई सामूहिक च्याटबाट हटाइयो" - :left "बाहिरियो" - :invited "आमन्त्रित" - :removed "हटाइयो" - :You "तपाईं" - - ;;new-contact - :add-new-contact "नया सम्पर्क जोड्नुहोस्" - :scan-qr "QR स्क्यान गर्नुहोस्" - :name "नाम" - :address-explication "सायद यहां ठेगाना बारे बयान गर्ने अनि त्यो कहाँ खोज्ने भन्ने कुराको कुनै पाठ हुनुपर्छ ।" - :enter-valid-public-key "कृपया एक मान्य सार्वजनिक सांचो प्रविष्ट गर्नुहोस् वा QR कोड स्क्यान गर्नुहोस् " - :contact-already-added "यो सम्पर्क पहिल्यै जोडीसकिएको छ" - :can-not-add-yourself "आफुले आफुलाइनै जोड्न मिल्दैन" - :unknown-address "अज्ञात ठेगाना" - - ;;login - :connect "कनेक्ट" - :address "ठेगाना" - :password "पासवर्ड" - :sign-in-to-status "स्टेटसमा साइन-इन गर्नुहोस्" - :sign-in "साइन-इन" - :wrong-password "गलत पासवर्ड" - - ;;recover - :passphrase "गुप्तवाक्यांश" - :recover "पुनर्स्थापना" - :twelve-words-in-correct-order "१२ सब्दहरु सही क्रममा" - - ;;accounts - :recover-access "पहुँच पुनर्स्थापना गर्नुहोस्" - :create-new-account "नया खाता सिर्जना गर्नुहोस्" - - ;;wallet-qr-code - :done "सम्पन्न" - :main-wallet "मुख्य वालेट" - - ;;validation - :invalid-phone "नमिल्ने फोन नम्बर" - :amount "रकम" - - ;;transactions - :confirm "पुष्टि गर्नुहोस्" - :transaction "कारोबार" - :status "स्टेटस" - :recipient "प्रापक" - :to "तर्फ" - :from "बाट" - :data "डाटा" - :got-it "पाइयो" - - ;;webview - :web-view-error "ओहोऽऽऽ, त्रुटि"}) diff --git a/src/status_im/translations/nl.cljs b/src/status_im/translations/nl.cljs deleted file mode 100644 index 903e083abeb..00000000000 --- a/src/status_im/translations/nl.cljs +++ /dev/null @@ -1,240 +0,0 @@ -(ns status-im.translations.nl) - -(def translations - {;;common - :members-title "Leden" - :not-implemented "!niet geïmplementeerd" - :chat-name "Chatnaam" - :notifications-title "Meldingen en geluiden" - :offline "Offline" - :search-for "Zoek naar..." - :cancel "Annuleren" - :next "Volgende" - :type-a-message "Typ een bericht..." - :type-a-command "typ een commando..." - :error "Error" - - :camera-access-error "Om cameratoegang te geven, ga je naar systeem instellingen en zorg je dat Status > Camera geselecteerd is." - :photos-access-error "Om fototoegang te geven, ga je naar systeem instellingen en zorg je dat Status > Foto's geselecteerd is." - - ;;drawer - :switch-users "Schakel tussen gebruikers" - :current-network "Huidige netwerk" - - ;;chat - :is-typing "is aan het typen" - :and-you "en jij" - :search-chat "Zoek in chat" - :members {:one "1 lid" - :other "{{count}} leden" - :zero "geen leden"} - :members-active {:one "1 lid, 1 actieve" - :other "{{count}} leden, {{count}} actieve" - :zero "geen leden"} - :active-online "Online" - :active-unknown "Onbekend" - :available "Beschikbaar" - :no-messages "Geen berichten" - :suggestions-requests "Aanvragen" - :suggestions-commands "Commando’s" - :faucet-success "Faucet aanvraag is ontvangen" - :faucet-error "Error bij Faucet aanvraag " - - ;;sync - :sync-in-progress "Wordt gesynchroniseerd..." - :sync-synced "Gesynchroniseerd" - - ;;messages - :status-sending "Wordt verstuurd" - :status-pending "Hangende" - :status-sent "Verstuurd" - :status-seen-by-everyone "Door iedereen gezien" - :status-seen "Gezien" - :status-delivered "Bezorgd" - :status-failed "Mislukt" - - ;;datetime - :datetime-second {:one "seconde" - :other "seconden"} - :datetime-minute {:one "minuut" - :other "minuten"} - :datetime-hour {:one "uur" - :other "uren"} - :datetime-day {:one "dag" - :other "dagen"} - :datetime-ago "geleden" - :datetime-yesterday "gisteren" - :datetime-today "vandaag" - - ;;profile - :profile "Profiel" - :edit-profile "Bewerk profiel" - :message "Bericht" - :not-specified "Niet opgegeven" - :public-key "Openbare sleutel" - :phone-number "Telefoonnummer" - :update-status "Wijzig je status..." - :add-a-status "Voeg een status toe..." - :status-prompt "Maak een status aan om mensen te laten weten wat je te bieden hebt. Je kan ook #hashtags gebruiken." - :add-to-contacts "Aan contactpersonen toevoegen" - :in-contacts "In contacts" - :remove-from-contacts "Remove from contacts" - :start-conversation "Begin een gesprek" - :send-transaction "Verstuur een transactie" - - ;;make_photo - :image-source-title "Profielfoto" - :image-source-make-photo "Foto nemen" - :image-source-gallery "Kies uit galerij" - - ;;sharing - :sharing-copy-to-clipboard "Kopieer naar klembord" - :sharing-share "Deel..." - :sharing-cancel "Annuleren" - - :browsing-title "Browse" - :browsing-open-in-web-browser "Open in een web browser" - :browsing-cancel "Annuleren" - - ;;sign-up - :contacts-syncronized "Jouw contactpersonen zijn gesynchroniseerd" - :confirmation-code (str "Bedankt! We hebben je een sms gestuurd met een bevestigingscode" - ". Geef die code op om jouw telefoonnummer te bevestigen") - :incorrect-code (str "Sorry, de code was onjuist, voer hem opnieuw in") - :phew-here-is-your-passphrase "*Foei* dat was moeilijk, hier is jouw wachtzin, *schrijf deze op en bewaar hem goed!* Je zult hem nodig hebben om jouw account te herstellen." - :here-is-your-passphrase "Hier is jouw wachtzin, *schrijf deze op en bewaar hem goed!* Je zult hem nodig hebben om jouw account te herstellen." - :phone-number-required "Tik hier om je telefoonnummer in te voeren, dan zoek ik jouw vrienden" - :shake-your-phone "Bug gevonden, of heb je een suggestie? ~Schud~ je telefoon!" - :intro-status "Chat met me om jouw account in te stellen en jouw instellingen te wijzigen!" - :intro-message1 "Welkom bij Status\nTik op dit bericht om jouw wachtwoord in te stellen en aan de slag te gaan!" - :account-generation-message "Geef me een moment, ik moet wat ingewikkelde berekeningen doen om jouw account aan te maken!" - :move-to-internal-failure-message "We moeten wat belangrijke bestanden van externe naar interne opslag verplaatsen. Om dit te doen hebben we je toestemming interne opslag niet meer gebruiken in volgende versies." - :debug-enabled "Debug-server is gelanceerd! Je kan nu *status-dev-cli scan* uitvoeren om de server te vinden vanaf je computer op het zelfde netwerk." - - ;;phone types - :phone-e164 "International 1" - :phone-international "International 2" - :phone-national "Nationaal" - :phone-significant "Significant" - - ;;chats - :chats "Chats" - :delete-chat "Verwijder chat" - :new-group-chat "Nieuwe groepchat" - :new-public-group-chat "Neem deel in publieke chat" - :edit-chats "Bewerk chats" - :search-chats "Zoek chats" - :empty-topic "Leeg onderwerp" - :topic-format "Verkeert formaat [a-z0-9\\-]+" - :public-group-topic "Onderwerp" - - ;;discover - :discover "Ontdekking" - :none "Geen" - :search-tags "Typ hier jouw zoektags" - :popular-tags "Populaire tags" - :recent "Recent" - :no-statuses-discovered "Geen statussen ontdekt" - :no-statuses-found "Geen statussen gevonden" - - ;;settings - :settings "Instellingen" - - ;;contacts - :contacts "Contactpersonen" - :new-contact "Nieuwe contactpersonen" - :delete-contact "Delete contact" - :delete-contact-confirmation "This contact will be removed from your contacts" - :remove-from-group "Remove from group" - :edit-contacts "Edit contacts" - :search-contacts "Search contacts" - :contacts-group-new-chat "Start nieuwe chat" - :choose-from-contacts "Choose from contacts" - :no-contacts "Nog geen contactpersonen" - :show-qr "Toon QR" - :enter-address "Enter address" - :more "more" - - ;;group-settings - :remove "Verwijderen" - :save "Opslaan" - :delete "Delete" - :clear-history "Wis geschiedenis" - :mute-notifications "Stille notificaties" - :leave-chat "Verlaat chat" - :chat-settings "Chatinstellingen" - :edit "Bewerken" - :add-members "Voeg leden toe" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Nieuwe groep" - :reorder-groups "Hergroepeer groep" - :edit-group "Bewerk groep" - :delete-group "Verwijder groep" - :delete-group-confirmation "Deze groep zal worden verwijderd van jouw groepen. Dit heeft geen effect op je contacten." - :delete-group-prompt "This will not affect contacts" - :contact-s {:one "contact" - :other "contacten"} - - ;;participants - - ;;protocol - :received-invitation "ontving chatuitnodiging" - :removed-from-chat "verwijderde jou van groepchat" - :left "ging weg" - :invited "uitgenodigd" - :removed "verwijderd" - :You "Jou" - - ;;new-contact - :add-new-contact "Voeg nieuwe contactpersoon toe" - :scan-qr "QR scannen" - :name "Naam" - :address-explication "Misschien zou hier wat tekst moeten staan waarin wordt uitgelegd wat een adres is en waar je deze kunt vinden" - :enter-valid-public-key "Voer een geldig publieke sleutel in of scan een QR code" - :contact-already-added "De contactpersoon is al toegevoegd" - :can-not-add-yourself "Je kunt niet zelf toevoegen" - :unknown-address "Onbekend adres" - - ;;login - :connect "Verbinden" - :address "Adres" - :password "Wachtwoord" - :sign-in-to-status "Meld u aan bij Status" - :sign-in "Meld aan" - :wrong-password "Verkeerd wachtwoord" - - ;;recover - :passphrase "Wachtzin" - :recover "Herstellen" - :twelve-words-in-correct-order "12 woorden in goede volgorde" - - ;;accounts - :recover-access "Toegang herstellen" - :create-new-account "Maake een nieuwe account aan" - - ;;wallet-qr-code - :done "Klaar" - :main-wallet "Hoofdportemonnee" - - ;;validation - :invalid-phone "Ongeldig telefoonnummer" - :amount "Bedrag" - - ;;transactions - :confirm "Bevestigen" - :transaction "Transactie" - :status "Status" - :recipient "Ontvanger" - :to "Naar" - :from "Van" - :data "Data" - :got-it "Got it" - - ;;:webview - :web-view-error "oeps, fout" - - :public-group-status "Publiek"}) diff --git a/src/status_im/translations/pl.cljs b/src/status_im/translations/pl.cljs deleted file mode 100644 index 6695a852c1b..00000000000 --- a/src/status_im/translations/pl.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.pl) - -(def translations - {;;common - :members-title "Użytkownicy" - :not-implemented "nie wprowadzono" - :chat-name "Nazwa czatu" - :notifications-title "Powiadomienia i dźwięki" - :offline "Offline" - - ;;drawer - :switch-users "Przełącz użytkowników" - - ;;chat - :is-typing "wpisuje tekst" - :and-you "i ty" - :search-chat "Przeszukaj czat" - :members {:one "1 użytkownik" - :other "Użytkownicy: {{count}}" - :zero "brak użytkowników "} - :members-active {:one "1 użytkownik, 1 aktywny" - :other "Użytkownicy:{{count}}, aktywni:{{count}}" - :zero "brak użytkowników"} - :active-online "Online" - :active-unknown "Nieznany" - :available "Dostępny" - :no-messages "Brak wiadomości" - :suggestions-requests "Żądania" - :suggestions-commands "Polecenia" - - ;;sync - :sync-in-progress "Synchronizacja..." - :sync-synced "W synchronizacji" - - ;;messages - :status-sending "Wysyłanie" - :status-pending "W oczekiwaniu" - :status-sent "Wysłano" - :status-seen-by-everyone "Przeczytane przez wszystkich" - :status-seen "Przeczytane" - :status-delivered "Dostarczono" - :status-failed "Niepowodzenie" - - ;;datetime - :datetime-second {:one "sekunda" - :other "sekund(y)"} - :datetime-minute {:one "minuta" - :other "minut(y)"} - :datetime-hour {:one "godzina" - :other "godziny(y)"} - :datetime-day {:one "dzień" - :other "dni"} - :datetime-ago "temu" - :datetime-yesterday "wczoraj" - :datetime-today "dzisiaj" - - ;;profile - :profile "Profil" - :message "Wiadomość" - :not-specified "Nie określono" - :public-key "Klucz publiczny" - :phone-number "Numer telefonu " - :add-to-contacts "Dodaj do kontaktów" - - ;;make_photo - :image-source-title "Zdjęcie profilowe " - :image-source-make-photo "Przechwyć" - :image-source-gallery "Wybierz z galerii " - - ;;sign-up - :contacts-syncronized "Twoje kontakty zostały zsynchronizowane" - :confirmation-code (str "Dziękujemy! Wysłaliśmy ci SMS-a z kodem" - "potwierdzającym. Prosimy o podanie kodu w celu zweryfikowania swojego numeru telefonu") - :incorrect-code (str "Przepraszamy, kod jest nieprawidłowy. Prosimy wprowadzić kod ponownie") - :phew-here-is-your-passphrase "*Uff*, to nie było łatwe. Oto twoje specjalne hasło, *zapisz je i przechowuj w bezpiecznym miejscu!* Będzie ci potrzebne podczas procedury odzyskiwania konta." - :here-is-your-passphrase "Oto twoje specjalne hasło, *zapisz je i przechowuj w bezpiecznym miejscu!* Będzie ci potrzebne podczas procedury odzyskiwania konta." - :phone-number-required "Dotknij tutaj, aby wprowadzić swój numer telefonu, a my znajdziemy twoich znajomych " - :intro-status "Porozmawiaj ze mną na czacie, aby skonfigurować swoje konto i zmienić ustawienia!" - :intro-message1 "Witamy w sekcji Status\nWybierz tę wiadomość, aby ustawić hasło i rozpocząć!" - :account-generation-message "Daj mi chwilkę, muszę wykonać szalone obliczenia, żeby utworzyć dla Ciebie konto!" - - ;;chats - :chats "Czaty" - :new-group-chat "Nowy czat grupowy" - - ;;discover - :discover "Odkryte" - :none "Brak" - :search-tags "Tutaj wpisz swoje tagi wyszukiwania" - :popular-tags "Popularne tagi" - :recent "Najnowsze" - :no-statuses-discovered "Nie odkryto statusów" - - ;;settings - :settings "Ustawienia" - - ;;contacts - :contacts "Kontakty" - :new-contact "Nowy kontakt" - :contacts-group-new-chat "Rozpocznij nowy czat" - :no-contacts "W tej chwili brak kontaktów" - :show-qr "Pokaż QR" - - ;;group-settings - :remove "Usuń" - :save "Zapisz" - :clear-history "Wyczyść historię" - :chat-settings "Ustawienia czatu" - :edit "Edytuj" - :add-members "Dodaj użytkowników" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "otrzymano zaproszenie na czat" - :removed-from-chat "usunięto z czatu grupowego" - :left "pozostało" - :invited "zaproszeni" - :removed "usunięci" - :You "Ty" - - ;;new-contact - :add-new-contact "Dodaj nowy kontakt" - :scan-qr "Skanuj QR" - :name "Nazwa" - :address-explication "Być może tutaj powinien znajdować się tekst wyjaśniający, czym jest adres i gdzie go szukać" - :contact-already-added "Kontakt został już dodany" - :can-not-add-yourself "Nie możesz dodać samego siebie" - :unknown-address "Nieznany adres" - - ;;login - :connect "Połącz" - :address "Adres" - :password "Hasło" - :wrong-password "Nieprawidłowe hasło" - - ;;recover - :passphrase "Hasło specjalne" - :recover "Odzyskaj" - - ;;accounts - :recover-access "Odzyskaj dostęp" - - ;;wallet-qr-code - :done "Zrobione" - :main-wallet "Portfel główny" - - ;;validation - :invalid-phone "Nieprawidłowy numer telefonu" - :amount "Kwota" - ;;transactions - :status "Status" - :recipient "Odbiorca" - - ;;:webview - :web-view-error "oj, mamy błąd" - - :confirm "Potwierdź" - :phone-national "Krajowy" - :public-group-topic "Temat" - :debug-enabled "Uruchomiono serwer debugowania! Możesz teraz dodać program DApp, uruchamiając *status-dev-cli scan* na swoim komputerze" - :new-public-group-chat "Dołącz do publicznego czatu" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Anuluj" - :twelve-words-in-correct-order "12 słów w prawidłowym porządku" - :remove-from-contacts "Usuń z kontaktów" - :delete-chat "Usuń czat" - :edit-chats "Edytuj czaty" - :sign-in "Zaloguj się" - :create-new-account "Utwórz nowe konto" - :sign-in-to-status "Zaloguj się do Statusu" - :got-it "Rozumiem" - :move-to-internal-failure-message "Musimy przenieść ważne pliki z zewnętrznego na wewnętrzny nośnik danych. W tym celu potrzebujemy Twojego pozwolenia. W przyszłych wersjach zewnętrzne nośniki danych nie będą stosowane." - :edit-group "Edytuj grupę" - :delete-group "Usuń grupę" - :browsing-title "Przeglądaj" - :reorder-groups "Przestaw grupy" - :browsing-cancel "Anuluj" - :faucet-success "Odebrano prośbę dotyczącą kranu" - :choose-from-contacts "Wybierz spośród kontaktów" - :new-group "Nowa grupa" - :phone-e164 "Międzynarodowy 1" - :remove-from-group "Usuń z grupy" - :search-contacts "Przeszukaj kontakty" - :transaction "Transakcja" - :public-group-status "Publiczny" - :leave-chat "Opuść czat" - :start-conversation "Rozpocznij rozmowę" - :topic-format "Nieprawidłowy format [a-z0-9\\-]+" - :enter-valid-public-key "Wprowadź prawidłowy klucz publiczny lub zeskanuj kod QR" - :faucet-error "Błąd wysyłania prośby dotyczącej kranu" - :phone-significant "Ważne" - :search-for "Wyszukaj..." - :sharing-copy-to-clipboard "Skopiuj do schowka" - :phone-international "Międzynarodowy 2" - :enter-address "Wprowadź adres" - :send-transaction "Wyślij transakcję" - :delete-contact "Usuń kontakt" - :mute-notifications "Wycisz powiadomienia" - :contact-s {:one "kontakt" - :other "kontakty"} - :next "Dalej" - :from "Z" - :search-chats "Przeszukaj czaty" - :in-contacts "W kontaktach" - - :sharing-share "Udostępnij..." - :type-a-message "Wpisz wiadomość..." - :type-a-command "Zacznij wpisywać komendę..." - :shake-your-phone "Znalazłeś błąd lub masz pytanie? Wystarczy ~potrząsnąć~ telefonem!" - :status-prompt "Utwórz status, aby inni mogli dowiedzieć się, co oferujesz. Możesz również wykorzystać #hashtagi." - :add-a-status "Dodaj status..." - :error "Błąd" - :edit-contacts "Edytuj kontakty" - :more "więcej" - :cancel "Anuluj" - :no-statuses-found "Nie znaleziono statusów" - :browsing-open-in-web-browser "Otwórz w przeglądarce" - :delete-group-prompt "To nie będzie miało wpływu na kontakty" - :edit-profile "Edytuj profil" - :empty-topic "Pusty temat" - :to "Do" - :data "Dane"}) diff --git a/src/status_im/translations/pt_br.cljs b/src/status_im/translations/pt_br.cljs deleted file mode 100644 index e3a86709e81..00000000000 --- a/src/status_im/translations/pt_br.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.pt-br) - -(def translations - {;;common - :members-title "Membros" - :not-implemented "não implementado" - :chat-name "Nome do chat" - :notifications-title "Notificações e sons" - :offline "Offline" - - ;;drawer - :switch-users "Trocar usuário" - - ;;chat - :is-typing "está digitando" - :and-you "e você" - :search-chat "Pesquisar chat" - :members {:one "1 membro" - :other "{{count}} membros" - :zero "nenhum membro"} - :members-active {:one "1 membro, 1 ativo" - :other "{{count}} membros, {{count}} ativo" - :zero "nenhum membro"} - :active-online "Online" - :active-unknown "Desconhecido" - :available "Disponível" - :no-messages "Nenhuma mensagem" - :suggestions-requests "Solicitações" - :suggestions-commands "Comandos" - - ;;sync - :sync-in-progress "Sincronizando..." - :sync-synced "Sincronizado" - - ;;messages - :status-sending "Enviando..." - :status-pending "Pendente" - :status-sent "Enviado" - :status-seen-by-everyone "Visto por todos" - :status-seen "Visto" - :status-delivered "Entregue" - :status-failed "Malsucedido" - - ;;datetime - :datetime-second {:one "segundo" - :other "segundos"} - :datetime-minute {:one "minuto" - :other "minutos"} - :datetime-hour {:one "hora" - :other "horas"} - :datetime-day {:one "dia" - :other "dias"} - :datetime-ago "atrás" - :datetime-yesterday "ontem" - :datetime-today "hoje" - - ;;profile - :profile "Perfil" - :message "Mensagem" - :not-specified "Não especificado" - :public-key "Chave pública" - :phone-number "Número de telefone" - :add-to-contacts "Adicionar aos contatos" - - ;;make_photo - :image-source-title "Imagem do perfil" - :image-source-make-photo "Tirar foto" - :image-source-gallery "Escolher na galeria" - - ;;sign-up - :contacts-syncronized "Seus contatos foram sincronizados" - :confirmation-code (str "Obrigado! Nós lhe enviamos uma mensagem de texto com um código de " - "confirmação. Por favor, informe esse código para confirmar seu número de telefone") - :incorrect-code (str "Desculpe, o código estava incorreto. Por favor, digite novamente") - :phew-here-is-your-passphrase "*Ufa* isso foi difícil. Aqui está a sua frase secreta. *Anote-a e guarde-a em segurança!* Você precisará dela para recuperar a sua conta." - :here-is-your-passphrase "Aqui está a sua frase secreta. *Anote-a e guarde-a em segurança!* Você precisará dela para recuperar a sua conta." - :phone-number-required "Toque aqui para inserir seu número de telefone e eu vou encontrar seus amigos" - :intro-status "Converse comigo para configurar a sua conta e alterar suas definições!" - :intro-message1 "Bem-vindo ao Status\nToque nesta mensagem para definir sua senha e começar!" - :account-generation-message "Mê dê um segundinho. Tenho de fazer uns cálculos malucos para gerar a sua conta!" - - ;;chats - :chats "Chats" - :new-group-chat "Novo grupo de chat" - - ;;discover - :discover "Descobrir" - :none "Nenhum(a)" - :search-tags "Digite suas tags de pesquisa aqui" - :popular-tags "Tags populares" - :recent "Recentes" - :no-statuses-discovered "Nenhum status descoberto" - - ;;settings - :settings "Configurações" - - ;;contacts - :contacts "Contatos" - :new-contact "Novo contato" - :contacts-group-new-chat "Iniciar novo chat" - :no-contacts "Você ainda não tem contatos" - :show-qr "Mostrar QR" - - ;;group-settings - :remove "Remover" - :save "Salvar" - :clear-history "Apagar histórico" - :chat-settings "Configurações do chat" - :edit "Editar" - :add-members "Adicionar membros" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "recebeu o convite para o chat" - :removed-from-chat "removeu você do grupo" - :left "saiu" - :invited "convidou" - :removed "removeu" - :You "Você" - - ;;new-contact - :add-new-contact "Adicionar novo contato" - :scan-qr "Escanear QR" - :name "Nome" - :address-explication "Talvez aqui deveria haver algum texto explicando o que é um endereço e onde procurá-lo" - :contact-already-added "O contato já foi adicionado" - :can-not-add-yourself "Não é possível adicionar a si mesmo" - :unknown-address "E-mail desconhecido" - - ;;login - :connect "Conectar" - :address "Endereço" - :password "Senha" - :wrong-password "Senha incorreta" - - ;;recover - :passphrase "Frase secreta" - :recover "Recuperar" - - ;;accounts - :recover-access "Recuperar o acesso" - - ;;wallet-qr-code - :done "Concluído" - :main-wallet "Carteira principal" - - ;;validation - :invalid-phone "Número de telefone inválido" - :amount "Quantia" - ;;transactions - :status "Status" - :recipient "Destinatário" - - ;;:webview - :web-view-error "Ops, erro" - - :confirm "Confirmar" - :phone-national "Nacional" - :public-group-topic "Assunto" - :debug-enabled "O servidor de debug foi inicializado! Você agora pode adicionar seu DApp ao executar *status-dev-cli scan* a partir de seu computador." - :new-public-group-chat "Juntar-se a bate-papo público." - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Cancelar" - :twelve-words-in-correct-order "12 palavras na ordem correta" - :remove-from-contacts "Remover dos contatos" - :delete-chat "Excluir bate-papo" - :edit-chats "Editar bate-papos" - :sign-in "Entrar" - :create-new-account "Criar nova conta" - :sign-in-to-status "Entrar para Status" - :got-it "Entendido" - :move-to-internal-failure-message "Precisamos mover alguns arquivos importantes do armazenamento externo para o interno. Para fazer isso, precisamos de sua permissão. Não utilizaremos armazenamento externo em versões futuras." - :edit-group "Editar grupo" - :delete-group "Excluir grupo" - :browsing-title "Buscar" - :reorder-groups "Reorganizar grupos" - :browsing-cancel "Cancelar" - :faucet-success "Requisição faucet foi recebida" - :choose-from-contacts "Selecionar a partir dos contatos" - :new-group "Novo grupo" - :phone-e164 "Internacional 1" - :remove-from-group "Remover do grupo" - :search-contacts "Buscar contatos" - :transaction "Transação" - :public-group-status "Público" - :leave-chat "Sair do bate-papo" - :start-conversation "Iniciar conversa" - :topic-format "Formato errado [a-z0-9\\-]+" - :enter-valid-public-key "Por favor, insira uma chave pública válida ou capture um código QR" - :faucet-error "Erro na requisição faucet" - :phone-significant "Significante" - :search-for "Buscar por..." - :sharing-copy-to-clipboard "Copiar para área de transferência" - :phone-international "Internacional 2" - :enter-address "Inserir endereço" - :send-transaction "Enviar transação" - :delete-contact "Excluir contato" - :mute-notifications "Silenciar notificações" - :contact-s {:one "contato" - :other "contatos"} - :next "Próximo" - :from "De" - :search-chats "Buscar bate-papos" - :in-contacts "Em contatos" - - :sharing-share "Compartilhar..." - :type-a-message "Digite uma mensagem..." - :type-a-command "Comece a digitar um comando..." - :shake-your-phone "Encontrou um erro ou tem uma sugestão? Basta ~agitar~ seu telefone!" - :status-prompt "Crie um status para ajudar as pessoas a saberem sobre as coisas que você está oferecendo. Você também pode utilizar #hashtags." - :add-a-status "Adicionar um status..." - :error "Erro" - :edit-contacts "Editar contatos" - :more "mais" - :cancel "Cancelar" - :no-statuses-found "Nenhum status encontrado" - :browsing-open-in-web-browser "Abrir no navegador de internet" - :delete-group-prompt "Isso não afetará os contatos" - :edit-profile "Editar perfil" - :empty-topic "Assunto vazio" - :to "Para" - :data "Dados"}) diff --git a/src/status_im/translations/pt_pt.cljs b/src/status_im/translations/pt_pt.cljs deleted file mode 100644 index 85d578db927..00000000000 --- a/src/status_im/translations/pt_pt.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.pt-pt) - -(def translations - {;;common - :members-title "Membros" - :not-implemented "!não implementado" - :chat-name "Nome no chat" - :notifications-title "Notificações e sons" - :offline "Offline" - - ;;drawer - :switch-users "Mudar de utilizadores" - - ;;chat - :is-typing "está a digitar" - :and-you "e você" - :search-chat "Pesquisar chat" - :members {:one "1 membro" - :other "{{count}} membros" - :zero "sem membros"} - :members-active {:one "1 membro, 1 ativo" - :other "{{count}} membros, {{count}} ativos" - :zero "sem membros"} - :active-online "Online" - :active-unknown "Desconhecido" - :available "Disponível" - :no-messages "Sem mensagens" - :suggestions-requests "Pedidos" - :suggestions-commands "Comandos" - - ;;sync - :sync-in-progress "A sincronizar..." - :sync-synced "Em sincronização" - - ;;messages - :status-sending "A enviar" - :status-pending "Pendente" - :status-sent "Enviado" - :status-seen-by-everyone "Visto por todos" - :status-seen "Visto" - :status-delivered "Entregue" - :status-failed "Falhou" - - ;;datetime - :datetime-second {:one "segundo" - :other "segundos"} - :datetime-minute {:one "minuto" - :other "minutos"} - :datetime-hour {:one "hora" - :other "horas"} - :datetime-day {:one "dia" - :other "dias"} - :datetime-ago "atrás" - :datetime-yesterday "ontem" - :datetime-today "hoje" - - ;;profile - :profile "Perfil" - :message "Mensagem" - :not-specified "Não especificado" - :public-key "Chave Pública" - :phone-number "Número de telefone" - :add-to-contacts "Adicionar aos contactos" - - ;;make_photo - :image-source-title "Imagem de perfil" - :image-source-make-photo "Capturar" - :image-source-gallery "Selecionar a partir da galeria" - - ;;sign-up - :contacts-syncronized "Os seus contactos foram sincronizados" - :confirmation-code (str "Obrigado! Enviámos-lhe uma mensagem de texto com um código de " - "confirmação. Por favor, forneça esse código para confirmar o seu número de telefone") - :incorrect-code (str "Desculpe, o código estava incorreto. Por favor, volte a digitar") - :phew-here-is-your-passphrase "*Ufa* foi complicado, aqui está a sua frase-chave, *anote-a e mantenha-a em segurança!* Vai precisar dela para recuperar a sua conta." - :here-is-your-passphrase "Aqui está a sua frase-chave, *anote-a e mantenha-a em segurança!* Vai precisar dela para recuperar a sua conta." - :phone-number-required "Toque aqui para digitar o seu número de telefone e vou encontrar os seus amigos" - :intro-status "Fale comigo para configurar a sua conta e alterar as suas definições!" - :intro-message1 "Bem-vindo ao Status\nToque nesta mensagem para definir a sua palavra-passe e começar!" - :account-generation-message "Dê-me um segundo, tenho de fazer alguns cálculos doidos para gerar a sua conta!" - - ;;chats - :chats "Chats" - :new-group-chat "Novo chat em grupo" - - ;;discover - :discover "Descoberta" - :none "Nenhum" - :search-tags "Digite aqui os seus tags de pesquisa" - :popular-tags "Tags populares" - :recent "Recente" - :no-statuses-discovered "Não foi descoberto nenhum estado" - - ;;settings - :settings "Definições" - - ;;contacts - :contacts "Contactos" - :new-contact "Novo Contacto" - :contacts-group-new-chat "Iniciar um novo chat" - :no-contacts "Ainda sem contactos" - :show-qr "Mostrar QR" - - ;;group-settings - :remove "Remover" - :save "Guardar" - :clear-history "Limpar o histórico" - :chat-settings "Definições de chat" - :edit "Editar" - :add-members "Adicionar Membros" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "recebeu um convite de chat" - :removed-from-chat "removeu-o do chat em grupo" - :left "saiu" - :invited "convidado" - :removed "removido" - :You "Você" - - ;;new-contact - :add-new-contact "Adicionar novo contacto" - :scan-qr "Digitalizar QR" - :name "Nome" - :address-explication "Talvez aqui devesse aparecer algum texto a explicar o que é um endereço e onde procurá-lo" - :contact-already-added "O contacto já foi adicionado" - :can-not-add-yourself "Não pode adicionar-se a si mesmo" - :unknown-address "Endereço desconhecido" - - ;;login - :connect "Ligar" - :address "Endereço" - :password "Palavra-passe" - :wrong-password "Palavra-passe errada" - - ;;recover - :passphrase "Frase-chave" - :recover "Recuperar" - - ;;accounts - :recover-access "Recuperar o acesso" - - ;;wallet-qr-code - :done "Concluído" - :main-wallet "Carteira Principal" - - ;;validation - :invalid-phone "Número de telefone inválido" - :amount "Montante" - ;;transactions - :status "Estado" - :recipient "Destinatário" - - ;;:webview - :web-view-error "ups, erro" - - :confirm "Confirmar" - :phone-national "Nacional" - :public-group-topic "Tema" - :debug-enabled "Foi lançado o servidor de depuração! Agora, pode adicionar a sua DApp executando *status-dev-cli scan* do seu computador" - :new-public-group-chat "Juntar-se ao chat público" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Cancelar" - :twelve-words-in-correct-order "12 palavras na ordem correta" - :remove-from-contacts "Remover dos contactos" - :delete-chat "Eliminar o chat" - :edit-chats "Editar chats" - :sign-in "Entrar" - :create-new-account "Criar nova conta" - :sign-in-to-status "Entrar com o Estado" - :got-it "Entendido" - :move-to-internal-failure-message "Precisamos de mover alguns ficheiros importantes do armazenamento externo para o interno. Para fazê-lo, precisamos da sua permissão. Não utilizaremos o armazenamento externo em versões futuras." - :edit-group "Editar grupo" - :delete-group "Eliminar grupo" - :browsing-title "Procurar" - :reorder-groups "Reordenar grupos" - :browsing-cancel "Cancelar" - :faucet-success "O pedido de torneira foi recebido" - :choose-from-contacts "Escolher entre os contactos" - :new-group "Novo grupo" - :phone-e164 "Internacional 1" - :remove-from-group "Remover do grupo" - :search-contacts "Pesquisar contactos" - :transaction "Transação" - :public-group-status "Público" - :leave-chat "Sair do chat" - :start-conversation "Iniciar a conversa" - :topic-format "Formato errado [a-z0-9\\-]+" - :enter-valid-public-key "Por favor, introduza uma chave pública válida ou analise um código QR" - :faucet-error "Erro de pedido de torneira" - :phone-significant "Significativo" - :search-for "Procurar por..." - :sharing-copy-to-clipboard "Copiar para a área de transferência" - :phone-international "Internacional 2" - :enter-address "Digitar o endereço" - :send-transaction "Enviar a transação" - :delete-contact "Apagar contacto" - :mute-notifications "Silenciar as notificações" - :contact-s {:one "contacto" - :other "Contactos"} - :next "Próximo" - :from "De" - :search-chats "Pesquisar chats" - :in-contacts "Nos contactos" - - :sharing-share "Partilhar..." - :type-a-message "Digitar uma mensagem..." - :type-a-command "Começar a digitar um comando..." - :shake-your-phone "Encontrar um bug ou ter uma sugestão? Basta ~ abanar ~ o seu telefone!" - :status-prompt "Crie um estado para ajudar as pessoas a saberem sobre as coisas que está a oferecer. Também pode utilizar #hashtags." - :add-a-status "Adicionar um estado..." - :error "Erro" - :edit-contacts "Editar contactos" - :more "mais" - :cancel "Cancelar" - :no-statuses-found "Nenhum estado encontrado" - :browsing-open-in-web-browser "Abrir no navegador Web" - :delete-group-prompt "Isto não afetará os contactos" - :edit-profile "Editar perfil" - :empty-topic "Tema em branco" - :to "Para" - :data "Dados"}) diff --git a/src/status_im/translations/ro.cljs b/src/status_im/translations/ro.cljs deleted file mode 100644 index 5799a834bcd..00000000000 --- a/src/status_im/translations/ro.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.ro) - -(def translations - {;;common - :members-title "Membri" - :not-implemented "!nu a fost implementat" - :chat-name "Nume chat" - :notifications-title "Notificări și sunete" - :offline "Offline" - - ;;drawer - :switch-users "Schimbă utilizatori" - - ;;chat - :is-typing "tastează" - :and-you "Și cu tine" - :search-chat "Caută în chat" - :members {:one "1 membru" - :other "{{count}} membri" - :zero "nici un membru"} - :members-active {:one "1 membru, 1 activ" - :other "{{count}} membri, {{count}} activi" - :zero "nici un membru"} - :active-online "Online" - :active-unknown "Necunoscut" - :available "Disponibil" - :no-messages "Nici un mesaj" - :suggestions-requests "Solicitări" - :suggestions-commands "Comenzi" - - ;;sync - :sync-in-progress "Se sincronizează…" - :sync-synced "Sincronizat" - - ;;messages - :status-sending "Se trimite" - :status-pending "În așteptare" - :status-sent "Trimis" - :status-seen-by-everyone "Văzut de toată lumea" - :status-seen "Văzut" - :status-delivered "Livrat" - :status-failed "Eșuat" - - ;;datetime - :datetime-second {:one "secundă" - :other "secunde"} - :datetime-minute {:one "minut" - :other "minute"} - :datetime-hour {:one "oră" - :other "ore"} - :datetime-day {:one "zi" - :other "zile"} - :datetime-ago "în urmă" - :datetime-yesterday "ieri" - :datetime-today "azi" - - ;;profile - :profile "Profil" - :message "Mesaj" - :not-specified "Nu este specificat" - :public-key "Cheie publică" - :phone-number "Număr de telefon" - :add-to-contacts "Adaugă la contacte" - - ;;make_photo - :image-source-title "Imagine de profil" - :image-source-make-photo "Captează" - :image-source-gallery "Selectează din galerie" - - ;;sign-up - :contacts-syncronized "Contactele tale au fost sincronizate" - :confirmation-code (str "Mulțumim! Ți-am trims un mesaj text cu un cod " - "de confirmare. Te rugăm să ne transmiți acel cod pentru a-ți confirma numărul de telefon") - :incorrect-code (str "Ne pare rău, dar codul este greșit, te rugăm să-l introduci încă o dată") - :phew-here-is-your-passphrase "*Pfui* a fost greu, iată fraza ta de acces, *noteaz-o și păstreaz-o în siguranță!* Vei avea nevoie de ea pentru a-ți redobândi accesul la cont." - :here-is-your-passphrase "Iată fraza ta de acces, *noteaz-o și păstreaz-o în siguranță!* Vei avea nevoie de ea pentru a-ți redobândi accesul la cont." - :phone-number-required "Apasă aici ca să-ți introduci numărul de telefon și eu îți voi invita prietenii" - :intro-status "Hai să vorbim pe chat pentru a-ți seta contul și modifica setările!" - :intro-message1 "Bine ai vent în Status\nApasă pe acest mesaj pentru a-ți seta parola și a începe!" - :account-generation-message "O secundă, trebuie să fac niște calcule complicate ca să-ți generez contul!" - - ;;chats - :chats "Discuții pe chat" - :new-group-chat "Grup nou de chat" - - ;;discover - :discover "Descoperire" - :none "Niciuna" - :search-tags "Tastează aici etichetele de căutat" - :popular-tags "Etichete populare" - :recent "Recente" - :no-statuses-discovered "Nici un status găsit" - - ;;settings - :settings "Setări" - - ;;contacts - :contacts "Contacte" - :new-contact "Contact nou" - :contacts-group-new-chat "Începe discuție nouă" - :no-contacts "Nici un contact deocamdată" - :show-qr "Afișează QR" - - ;;group-settings - :remove "Elimină" - :save "Salvează" - :clear-history "Șterge istoricul" - :chat-settings "Setări chat" - :edit "Editare" - :add-members "Adăugare membri" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "A primit invitația de chat" - :removed-from-chat "te-a elimination din grupul de chat" - :left "a plecat" - :invited "invitat" - :removed "eliminat" - :You "Tu" - - ;;new-contact - :add-new-contact "Adaugă contact nou" - :scan-qr "Scanează QR" - :name "Nume" - :address-explication "Poate că aici ar trebui să fie un text care să explice ce este o adresă și unde să o cauți" - :contact-already-added "Contactul a fost adăugat deja" - :can-not-add-yourself "Nu te poți adăuga pe tine" - :unknown-address "Adresă necunoscută" - - ;;login - :connect "Conectare" - :address "Adresă" - :password "Parolă" - :wrong-password "Parola greșită" - - ;;recover - :passphrase "Fraza de acces" - :recover "Recuperează" - - ;;accounts - :recover-access "Recuperează acces" - - ;;wallet-qr-code - :done "Gata" - :main-wallet "Portofelul principal" - - ;;validation - :invalid-phone "Număr de telefon nevalid" - :amount "Sumă" - ;;transactions - :status "Status" - :recipient "Beneficiar" - - ;;:webview - :web-view-error "ups, eroare" - - :confirm "Confirmare" - :phone-national "Național" - :public-group-topic "Subiect" - :debug-enabled "Serverul de curățare a fost lansat! Adresa dvs. Acum vă puteți adăuga DApp rulând *status-dev-cli scan* de pe computerul dvs." - :new-public-group-chat "Alăturați-vă chat-ului public" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Anulare" - :twelve-words-in-correct-order "12 de cuvinte în ordinea corectă" - :remove-from-contacts "Eliminare din contacte" - :delete-chat "Ștergere chat" - :edit-chats "Editare discuții" - :sign-in "Conectare" - :create-new-account "Creați un cont nou" - :sign-in-to-status "Conectați-vă cu statusul" - :got-it "Am înțeles" - :move-to-internal-failure-message "Trebuie să mutăm anumite fișiere importante din mediul de stocare extern în mediul de stocare intern. Pentru a face asta, avem nevoie de permisiunea dvs. În versiunile viitoare, nu vom folosi stocarea externă." - :edit-group "Editare grup" - :delete-group "Ștergere grup" - :browsing-title "Navigare" - :reorder-groups "Reorganizare grupuri" - :browsing-cancel "Anulare" - :faucet-success "Solicitarea a fost primită" - :choose-from-contacts "Alegeți dintre contacte" - :new-group "Grup nou" - :phone-e164 "Internațional 1" - :remove-from-group "Eliminare din grup" - :search-contacts "Căutare contacte" - :transaction "Tranzacție" - :public-group-status "Public" - :leave-chat "Părăsire chat" - :start-conversation "Începeți o conversație" - :topic-format "Format greșit [a-z0-9\\-]+" - :enter-valid-public-key "Vă rugăm să introduceți o cheie publică validă sau să scanați un cod QR" - :faucet-error "Eroare solicitare" - :phone-significant "Important" - :search-for "Căutare după..." - :sharing-copy-to-clipboard "Copiere în clipboard" - :phone-international "Internațional 2" - :enter-address "Introduceți adresa" - :send-transaction "Trimitere tranzacție" - :delete-contact "Ștergere contact" - :mute-notifications "Notificări silențioase" - :contact-s {:one "contact" - :other "contacte"} - :next "Mai departe" - :from "De la" - :search-chats "Căutare discuții" - :in-contacts "În contacte" - - :sharing-share "Partajare..." - :type-a-message "Tastați un mesaj..." - :type-a-command "Începeți să tastați o comandă..." - :shake-your-phone "Ați găsit o eroare sau aveți o sugestie? E suficient ~să scuturați~ telefonul!" - :status-prompt "Creați un status pentru a-i ajuta pe ceilalți să afle ce anume oferiți. Puteți folosi și #hashtag-uri." - :add-a-status "Adăugați un status..." - :error "Eroare" - :edit-contacts "Editare contacte" - :more "mai mult" - :cancel "Anulare" - :no-statuses-found "Niciun status găsit" - :browsing-open-in-web-browser "Deschide în browser web" - :delete-group-prompt "Aceasta nu va afecta contactele" - :edit-profile "Editare profil" - :empty-topic "Subiect gol" - :to "Către" - :data "Date"}) diff --git a/src/status_im/translations/ru.cljs b/src/status_im/translations/ru.cljs deleted file mode 100644 index e50ff784ba6..00000000000 --- a/src/status_im/translations/ru.cljs +++ /dev/null @@ -1,351 +0,0 @@ -(ns status-im.translations.ru) - -(def translations - {;;common - :members-title "Участники" - :not-implemented "!не реализовано" - :chat-name "Имя чата" - :notifications-title "Уведомления и звуки" - :offline "Оффлайн" - :search-for "Искать..." - :cancel "Отмена" - :next "Продолжить" - :open "Открыть" - :description "Описание" - :url "URL-адрес" - :type-a-message "Напишите сообщение..." - :type-a-command "Начинайте вводить команду..." - :error "Ошибка" - :unknown-status-go-error "Неизвестная status-go ошибка" - :node-unavailable "Нет запущенного ethereum узла" - :yes "Да" - :no "Нет" - - :camera-access-error "Чтобы предоставить необходимое разрешение для камеры, пожалуйста, перейдите в настройки системы и убедитесь, что выбрано Status > Камера." - :photos-access-error "Чтобы предоставить необходимое разрешение для фото, пожалуйста, перейдите в настройки системы и убедитесь, что выбрано Status > Фотографии." - - ;;drawer - :switch-users "Переключить пользователей" - :current-network "Текущая сеть" - - ;;chat - :is-typing "печатает" - :and-you "и вы" - :search-chat "Поиск по чату" - :members {:one "1 член" - :other "{{count}} члена(ов)" - :zero "нет членов"} - :members-active {:one "1 член" - :other "{{count}} члена(ов)" - :zero "нет членов"} - :public-group-status "Публичный" - :active-online "В сети" - :active-unknown "Неизвестно" - :available "Доступно" - :no-messages "Нет сообщений" - :suggestions-requests "Запросы" - :suggestions-commands "Команды" - :faucet-success "Запрос Faucet был получен" - :faucet-error "Ошибка запроса Faucet" - - ;;sync - :sync-in-progress "Синхронизация..." - :sync-synced "Синхронизируется" - - ;;messages - :status-sending "Отправляется..." - :status-pending "В ожидании" - :status-sent "Отправлено" - :status-seen-by-everyone "Просмотрено всеми" - :status-seen "Просмотрено" - :status-delivered "Доставлено" - :status-failed "Ошибка" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "секунда" - :other "секунды"} - :datetime-minute {:one "минута" - :other "минуты"} - :datetime-hour {:one "час" - :other "часы"} - :datetime-day {:one "день" - :other "дни"} - :datetime-ago "назад" - :datetime-yesterday "вчера" - :datetime-today "сегодня" - - ;;profile - :profile "Профиль" - :edit-profile "Редактировать профиль" - :message "Сообщение" - :not-specified "Не указано" - :public-key "Публичный ключ" - :phone-number "Номер телефона" - :update-status "Обновите свой статус..." - :add-a-status "Добавить статус..." - :status-prompt "Укажите свой статус. Использование #хэштегов поможет другим узнать о вас и рассказать о том, что у вас на уме" - :add-to-contacts "Добавить в контакты" - :in-contacts "В контактах" - :remove-from-contacts "Удалить из контактов" - :start-conversation "Начать разговор" - :send-transaction "Отправить транзакцию" - :testnet-text "Вы находитесь в {{testnet}} Testnet. Не отправляйте реальный ETH или SNT на свой адрес" - :mainnet-text "Вы в Mainnet. Реальный ETH будет отправлен" - - ;;make_photo - :image-source-title "Изображение профиля" - :image-source-make-photo "Сфотографировать" - :image-source-gallery "Выбрать из галереи" - - ;;sharing - :sharing-copy-to-clipboard "Скопировать" - :sharing-share "Поделиться..." - :sharing-cancel "Отмена" - - :browsing-title "Просматривать" - :browsing-open-in-web-browser "Открыть в веб-браузере" - :browsing-cancel "Отмена" - - ;;sign-up - :contacts-syncronized "Ваши контакты синхронизированы" - :confirmation-code (str "Спасибо! Мы отправили вам СМС с кодом подтверждения." - "Введите этот код для подтверждения своего номера телефона") - :incorrect-code (str "Извините, код неправильный, введите еще раз") - :phew-here-is-your-passphrase "*Уф*, это было непросто, вот ваша парольная фраза, *запишите ее и храните в надежном месте!* Она будет нужна вам для восстановления аккаунта." - :here-is-your-passphrase "Вот ваша парольная фраза, *запишите ее и храните в надежном месте!* Она будет нужна вам для восстановления аккаунта." - :here-is-your-signing-phrase "Вот ваша фраза подписи. Вы будете использовать ее для проверки достоверности своих транзакций. *Запишите ее и храните в безопасности!*" - :phone-number-required "Нажмите сюда для ввода своего номера телефона и я найду ваших друзей." - :shake-your-phone "Найдена ошибка или есть предложение? Просто ~потрясите~ свой телефон!" - :intro-status "Пообщайтесь со мной в чате, чтобы настроить свой аккаунт и изменить свои настройки!" - :intro-message1 "Добро пожаловать в Status\nНажмите на это сообщение, чтобы установить пароль и начать!" - :account-generation-message "Секундочку, мне нужно выполнить безумно сложные расчеты для создания вашего аккаунта!" - :move-to-internal-failure-message "Нам нужно переместить некоторые важные файлы из внешнего хранилища во внутреннее. Для этого нам нужно ваше разрешение. В следующих версиях мы не будем использовать внешнее хранилища." - :debug-enabled "Запущен сервер отладки! Теперь вы можете выполнить * status-dev-cli scan *, чтобы найти сервер с вашего компьютера в той же сети." - - ;;phone types - :phone-e164 "Международный 1" - :phone-international "Международный 2" - :phone-national "Государственный" - :phone-significant "Простой" - - ;;chats - :chats "Чаты" - :delete-chat "Удалить чат" - :new-group-chat "Новый групповой чат" - :new-public-group-chat "Присоединиться к публичному чату" - :edit-chats "Редактировать чаты" - :search-chats "Поиск чатов" - :empty-topic "Пустая тема" - :topic-format "Неверный формат [a-z0-9\\-]+" - :public-group-topic "Тема" - - ;;discover - :discover "Поиск" - :none "Нет" - :search-tags "Введите теги для поиска сюда" - :popular-tags "Популярные теги" - :recent "Последние статусы" - :no-statuses-found "Статусы не обнаружены" - :chat "Чат" - :all "Все" - :public-chats "Публичные чаты" - :soon "Скоро" - :public-chat-user-count "{{count}} людей" - :dapps "ÐApps" - :dapp-profile "ÐApp профиль" - :no-statuses-discovered "Статусы не обнаружены" - :no-statuses-discovered-body "Когда кто-то создаст\nстатус, вы увидите его здесь." - :no-hashtags-discovered-title "#хэштеги не обнаружены" - :no-hashtags-discovered-body "Когда #хэштег станет\nпопулярным, вы увидите его здесь." - - ;;settings - :settings "Настройки" - - ;;contacts - :contacts "Контакты" - :new-contact "Новый контакт" - :delete-contact "Удалить контакт" - :delete-contact-confirmation "Этот контакт будет удален из ваших контактов" - :remove-from-group "Удалить из группы" - :edit-contacts "Редактирование контактов" - :search-contacts "Поиск контактов" - :contacts-group-new-chat "Новый групповой чат" - :choose-from-contacts "Выбрать из контактов" - :no-contacts "Пока нет контактов" - :show-qr "Показать QR" - :enter-address "Ввести адрес" - :more "Больше" - - ;;group-settings - :remove "Удалить" - :save "Сохранить" - :delete "Удалить" - :clear-history "Очистить историю" - :mute-notifications "Отключить звук в оповещениях" - :leave-chat "Выйти из чата" - :chat-settings "Настройки чата" - :edit "Изменить" - :add-members "Добавить членов" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Новая группа" - :reorder-groups "Упорядочить группы" - :edit-group "Изменить группу" - :delete-group "Удалить группу" - :delete-group-confirmation "Эта группа будет удалена из ваших групп. Это не повлияет на ваши контакты" - :delete-group-prompt "Это не повлияет на ваши контакты" - :contact-s {:one "контакт" - :other "контакты"} - - ;;protocol - :received-invitation "получил(а) приглашение в чат" - :removed-from-chat "удалил(а) вас из группового чата" - :left "осталось" - :invited "приглашен(а)" - :removed "удален(а)" - :You "Вы" - - ;;new-contact - :add-new-contact "Добавить новый контакт" - :scan-qr "Сканировать QR" - :name "Имя" - :address-explication "Ваш публичный ключ используется для создания вашего адреса в Ethereum и представляет собой ряд цифр и букв. Вы можете легко найти его в своем профиле" - :enter-valid-public-key "Введите действительный публичный ключ или сканируйте QR-код" - :contact-already-added "Контакт уже добавлен" - :can-not-add-yourself "Вы не можете добавить себя" - :unknown-address "Неизвестный адрес" - - ;;login - :connect "Подключиться" - :address "Адрес" - :password "Пароль" - :sign-in-to-status "Войти в Status" - :sign-in "Войти" - :wrong-password "Неверный пароль" - :enter-password "Введите пароль" - - ;;recover - :passphrase "Парольная фраза" - :recover "Восстановить" - :twelve-words-in-correct-order "12 слов в правильном порядке" - - ;;accounts - :recover-access "Восстановление доступа" - :create-new-account "Создать новый аккаунт" - - ;;wallet-qr-code - :done "Готово" - - ;;validation - :invalid-phone "Неправильный номер телефона" - :amount "Сумма" - - ;;transactions - :confirm "Подтвердить" - :transaction "Транзакция" - :unsigned-transaction-expired "Неподписанная транзакция истекла" - :status "Status" - :recipient "Получатель" - :to "Кому" - :from "От" - :data "Данные" - :got-it "Понятно" - :block "Блок" - :hash "Хеш" - :gas-limit "Лимит Гaзa" - :gas-price "Цена на Газ" - :gas-used "Использованный Газ" - :cost-fee "Стоимость/Комиссия" - :nonce "Nonce" - :confirmations "Подтверждения" - :confirmations-helper-text "Пожалуйста, дождитесь как минимум 12-ти подтверждений, чтобы убедиться, что транзакция обработана безопасно" - :copy-transaction-hash "Копировать транзакционный хеш" - :open-on-etherscan "Открыть ссылку на Etherscan.io" - - ;;webview - :web-view-error "ой, ошибка" - - ;;testfairy warning - :testfairy-title "Предупреждение!" - :testfairy-message "Вы используете приложение, установленное из ночной сборки. Для целей тестирования эта сборка включает в себя запись сеанса, если используется соединение Wi-Fi, так что все ваши взаимодействия с этим приложением сохраняются (как видео и логи) и могут использоваться нашей командой разработчиков для изучения возможных проблем. Сохраненные видео / логи не включают в себя ваши пароли. Запись выполняется только в том случае, если приложение установлено из ночной сборки. Ничего не записывается, если приложение установлено из PlayStore или TestFlight." - ;; wallet - :wallet "Кошелек" - :wallets "Кошельки" - :your-wallets "Ваши кошельки" - :main-wallet "Основной Кошелек" - :wallet-error "Ошибка при загрузке данных" - :wallet-send "Отправить" - :wallet-request "Запрос" - :wallet-exchange "Обмен" - :wallet-assets "Активы" - :wallet-add-asset "Добавить актив" - :wallet-total-value "Общая стоимость" - :wallet-settings "Настройки кошелька" - :signing-phrase-description "Подпишите транзакцию, введя свой пароль. Убедитесь, что слова выше соответствуют вашей секретной фразе подписи" - :wallet-insufficient-funds "Недостаточно средств" - :request-transaction "Запрос транзакции" - :send-request "Послать запрос" - :share "Поделиться" - :eth "ETH" - :currency "Валюта" - :usd-currency "USD" - :transactions "Транзакции" - :transaction-details "Детали транзакции" - :transaction-failed "Не удалось выполнить транзакцию" - :transactions-sign "Подписать" - :transactions-sign-all "Подписать все" - :transactions-sign-transaction "Подписать транзакцию" - :transactions-sign-later "Подписать позже" - :transactions-delete "Удалить транзакцию" - :transactions-delete-content "Транзакция будет удалена из списка 'Неподписанные'" - :transactions-history "История" - :transactions-unsigned "Неподписанные" - :transactions-history-empty "В вашей истории еще нет транзакций" - :transactions-unsigned-empty "У вас нет никаких неподписанных транзакций" - :transactions-filter-title "Фильтры Истории" - :transactions-filter-tokens "Токены" - :transactions-filter-type "Тип" - :transactions-filter-select-all "Выбрать все" - :view-transaction-details "Посмотреть детали транзакции" - :transaction-description "Пожалуйста, подождите как минимум 12 подтверждений, чтобы убедиться, что транзакция обработана безопасно" - :transaction-sent "Транзакция отправлена" - :transaction-moved-text "Транзакция будет находиться в списке 'Неподписанные' в течение 5 минут" - :transaction-moved-title "Транзакция перемещена" - :sign-later-title "Подписать транзакцию позже?" - :sign-later-text "Откройте историю транзакций, чтобы подписать эту транзакцию" - :not-applicable "Неприменимо для неподписанных транзакций" - - ;; Wallet Send - :wallet-choose-recipient "Выберите Получателя" - :wallet-choose-from-contacts "Выберите Из Контактов" - :wallet-address-from-clipboard "Использовать Адрес Из Буфера Обмена" - :wallet-invalid-address "Недействительный адрес: \n {{data}}" - :wallet-browse-photos "Просмотреть Фотографии" - :validation-amount-invalid-number "Сумма недействительна" - :validation-amount-is-too-precise "Слишком много чисел после запятой. Наименьшая сумма, которую вы можете отправить это 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Новая сеть" - :add-network "Добавить сеть" - :add-new-network "Добавить новую сеть" - :existing-networks "Существующие сети" - :add-json-file "Добавить JSON файл" - :paste-json-as-text "Вставить JSON как текст" - :paste-json "Вставить JSON" - :specify-rpc-url "Укажите RPC URL-адрес" - :edit-network-config "Редактировать конфигурацию сети" - :connected "Подключен" - :process-json "Обработать JSON" - :error-processing-json "Ошибка обработки JSON" - :rpc-url "RPC URL-адрес" - :remove-network "Удалить сеть" - :network-settings "Настройки сети" - :edit-network-warning "Будьте осторожны, изменение настроек сети может сделать её недоступной для вас" - :connecting-requires-login "Подключение к другой сети требует входа в систему" - :close-app-title "Предупреждение!" - :close-app-content "Приложение остановится и закроется. При повторном открытии, будет использоваться выбранная сеть" - :close-app-button "Подтвердить"}) diff --git a/src/status_im/translations/sl.cljs b/src/status_im/translations/sl.cljs deleted file mode 100644 index 78ed7a3148f..00000000000 --- a/src/status_im/translations/sl.cljs +++ /dev/null @@ -1,228 +0,0 @@ -(ns status-im.translations.sl) - -(def translations - {;;common - :members-title "Člani" - :not-implemented "!ni implementirano" - :chat-name "Ime za klepet" - :notifications-title "Obvestila in zvoki" - :offline "Nedosegljiv/-a" - - ;;drawer - :switch-users "Preklopi med uporabniki" - - ;;chat - :is-typing "piše" - :and-you "in ti" - :search-chat "Iskanje po klepetu" - :members {:one "1 član" - :other "{{count}} članov" - :zero "ni članov"} - :members-active {:one "1 član, 1 aktiven" - :other "{{count}} članov, {{count}} aktivnih" - :zero "ni članov"} - :active-online "Dosegljiv/-a" - :active-unknown "Neznano" - :available "Na voljo" - :no-messages "Ni sporočil" - :suggestions-requests "Prošnje" - :suggestions-commands "Ukazi" - - ;;sync - :sync-in-progress "Sinhronizacija..." - :sync-synced "Sinhronizirano" - - ;;messages - :status-sending "Pošiljanje" - :status-pending "V teku" - :status-sent "Poslano" - :status-seen-by-everyone "Vsi so videli" - :status-seen "Videno" - :status-delivered "Dostavljeno" - :status-failed "Ni uspelo" - - ;;datetime - :datetime-second {:one "sekunda" - :other "sekund"} - :datetime-minute {:one "minuta" - :other "minut"} - :datetime-hour {:one "ura" - :other "ur"} - :datetime-day {:one "dan" - :other "dni"} - :datetime-ago "nazaj" - :datetime-yesterday "včeraj" - :datetime-today "danes" - - ;;profile - :profile "Profil" - :message "Sporočilo" - :not-specified "Ni navedeno" - :public-key "Javni ključ" - :phone-number "Telefonska številka" - :add-to-contacts "Dodaj med stike" - - ;;make_photo - :image-source-title "Fotografija profila" - :image-source-make-photo "Zajemi" - :image-source-gallery "Izberi iz galerije" - - ;;sign-up - :contacts-syncronized "Tvoji stiki so bili sinhronizirani" - :confirmation-code (str "Hvala! Poslali smo ti sporočilo s potrditveno " - "kodo. Prosimo, vnesi to kodo in potrdi svojo telefonsko številko") - :incorrect-code (str "Koda na žalost ni bila pravilna, prosimo, da jo ponovno vneseš") - :phew-here-is-your-passphrase "*Pfuu* to pa je bilo težko, tukaj je tvoje šifrirno geslo, *zapiši si ga in shrani na varno mesto!* Potreboval/-a ga boš, da obnoviš svoj račun." - :here-is-your-passphrase "Tukaj je tvoje geslo, *zapiši si ga in shrani na varno mesto!* Potreboval/-a ga boš, da obnoviš svoj račun." - :phone-number-required "Pritisni tukaj in vnesi svojo telefonsko številko ter poiskal bom tvoje prijatelje" - :intro-status "Klepetaj z mano in nastavi svoj račun ter spremeni svoje nastavitve!" - :intro-message1 "Dobrodošel/-la v status\nPritisni to sporočilo in nastavi svoje geslo ter začni!" - :account-generation-message "Počakaj sekundo, opraviti moram noro računico, da ustvarim tvoj račun!" - - ;;chats - :chats "Klepeti" - :new-group-chat "Nov skupinski klepet" - - ;;discover - :discover "Odkrivanje" - :none "Brez" - :search-tags "Sem vnesi svoje priljubljene oznake" - :popular-tags "Priljubljene oznake" - :recent "Nedavno" - :no-statuses-discovered "Ni odkritih statusov" - - ;;settings - :settings "Nastavitve" - - ;;contacts - :contacts "Stiki" - :new-contact "Nov stik" - :contacts-group-new-chat "Začni nov klepet" - :no-contacts "Zaenkrat še ni stikov" - :show-qr "Prikaži QR" - - ;;group-settings - :remove "Odstrani" - :save "Shrani" - :clear-history "Počisti zgodovino" - :chat-settings "Nastavitve klepeta" - :edit "Uredi" - :add-members "Dodaj člane" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "je prejel/-a povabilo za klepet" - :removed-from-chat "te je odstranil/-a iz skupinskega klepeta" - :left "preostane" - :invited "povabil/-a" - :removed "odstranil/-a" - :You "Ti" - - ;;new-contact - :add-new-contact "Dodaj nov stik" - :scan-qr "Skeniraj QR" - :name "Ime" - :address-explication "Sem morda sodi besedilo, ki razlaga, kaj je naslov ter kje ga najti" - :contact-already-added "Stik je bil že dodan" - :can-not-add-yourself "Sebe ni mogoče dodati" - :unknown-address "Neznan naslov" - - ;;login - :connect "Poveži" - :address "Naslov" - :password "Geslo" - :wrong-password "Napačno geslo" - - ;;recover - :passphrase "Šifrirno geslo" - :recover "Povrni" - - ;;accounts - :recover-access "Povrni dostop" - - ;;wallet-qr-code - :done "Končano" - :main-wallet "Glavna denarnica" - - ;;validation - :invalid-phone "Neveljavna telefonska številka" - :amount "Vsota" - ;;transactions - :status "Status" - :recipient "Prejemnik" - - ;;:webview - :web-view-error "ups, napaka" - - :confirm "Potrdi" - :phone-national "Državno" - :public-group-topic "Tema" - :debug-enabled "Strežnik za odpravljanje napak je bil zagnan! Sedaj lahko aplikacijo DApp dodate tako, da zaženete *status-dev-cli scan* na vašem računalniku" - :new-public-group-chat "Pridruži se javnemu klepetu" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Prekliči" - :twelve-words-in-correct-order "12 besed v pravilnem vrstnem redu" - :remove-from-contacts "Odstrani iz stikov" - :delete-chat "Izbriši klepet" - :edit-chats "Uredi klepete" - :sign-in "Prijava" - :create-new-account "Ustvari nov račun" - :sign-in-to-status "Prijava v Status" - :got-it "Razumem" - :move-to-internal-failure-message "Nekatere pomembne datoteke moramo premakniti iz zunanjega na notranji pomnilnik. Za to potrebujemo vaše dovoljenje. V prihajajočih različicah ne bomo uporabljali zunanjega pomnilnika." - :edit-group "Uredi skupino" - :delete-group "Izbriši skupino" - :browsing-title "Brskaj" - :reorder-groups "Preuredi skupine" - :browsing-cancel "Prekliči" - :faucet-success "Zahteva za dovod je bila prejeta" - :choose-from-contacts "Izberi iz stikov" - :new-group "Nova skupina" - :phone-e164 "Mednarodno 1" - :remove-from-group "Odstrani iz skupine" - :search-contacts "Iskanje stikov" - :transaction "Transakcija" - :public-group-status "Javno" - :leave-chat "Zapusti klepet" - :start-conversation "Začni pogovor" - :topic-format "Napačna oblika [a-z0-9\\-]+" - :enter-valid-public-key "Prosimo, vnesite veljavno javno kodo, ali skenirajte QR kodo" - :faucet-error "Napaka v zahtevi za dovod" - :phone-significant "Pomembno" - :search-for "Išči..." - :sharing-copy-to-clipboard "Kopiraj v odložišče" - :phone-international "Mednarodno 2" - :enter-address "Vnesi naslov" - :send-transaction "Pošlji transakcijo" - :delete-contact "Izbriši stik" - :mute-notifications "Utišaj obvestila" - :contact-s {:one "stik" - :other "stiki"} - :next "Naprej" - :from "Od" - :search-chats "Išči klepete" - :in-contacts "Med stiki" - - :sharing-share "Deli..." - :type-a-message "Napiši sporočilo..." - :type-a-command "Začni pisati ukaz..." - :shake-your-phone "Ste naleteli na napako ali imate predlog? Preprosto ~stresite~ vaš telefon!" - :status-prompt "Ustvarite status, ki bo drugim nudil več informacij o stvareh, ki jih nudite. Uporabljate lahko tudi #hashtage." - :add-a-status "Dodaj status..." - :error "Napaka" - :edit-contacts "Urejanje stikov" - :more "več" - :cancel "Prekliči" - :no-statuses-found "Ni statusov" - :browsing-open-in-web-browser "Odpri v spletnem brskalniku" - :delete-group-prompt "To ne bo vplivalo na stike" - :edit-profile "Uredi profil" - :empty-topic "Prazna tema" - :to "Za" - :data "Podatki"}) diff --git a/src/status_im/translations/sr_rs_cyrl.cljs b/src/status_im/translations/sr_rs_cyrl.cljs deleted file mode 100644 index 992efecb2be..00000000000 --- a/src/status_im/translations/sr_rs_cyrl.cljs +++ /dev/null @@ -1,352 +0,0 @@ -(ns status-im.translations.sr-rs-cyrl) - -(def translations - {;;common - :members-title "Чланови" - :not-implemented "!није имплементирано" - :chat-name "Назив разговора" - :notifications-title "Нотификације и звукови" - :offline "Ван мреже" - :search-for "Тражи..." - :cancel "Откажи" - :next "Следећи" - :open "Отвори" - :description "Опис" - :url "УРЛ" - :type-a-message "Укуцајте поруку..." - :type-a-command "Укуцајте наредбу..." - :error "Грешка" - :unknown-status-go-error "Непозната status-go грешка" - :node-unavailable "Етереум сервис није покренут" - :yes "Да" - :no "Нe" - - :camera-access-error "Да би сте дозволили приступ камери, молимо вас да у системским подешавањима селектујете Статус > Камера." - :photos-access-error "Да би сте дозволили приступ фотографијама, молимо вас у системским подешаванјима селектујете Статус > Албум." - - ;;drawer - :switch-users "Промените корисника" - :current-network "Тренутна мрежа" - - ;;chat - :is-typing "куца" - :and-you "и ви" - :search-chat "Претражи разговор" - :members {:one "1 члан" - :other "{{count}} чланова" - :zero "нема чланова"} - :members-active {:one "1 члан" - :other "{{count}} чланови" - :zero "нема чланова"} - :public-group-status "Јавно" - :active-online "На мрежи" - :active-unknown "Непознат" - :available "Доступан" - :no-messages "Нема порука" - :suggestions-requests "Захтеви" - :suggestions-commands "Наредбе" - :faucet-success "Захтев за славину је примљен" - :faucet-error "Грешка у захтеву за славину" - - ;;sync - :sync-in-progress "Синхронизација..." - :sync-synced "Синхронизовано" - - ;;messages - :status-sending "Слање у току" - :status-pending "Обрађује се" - :status-sent "Послато" - :status-seen-by-everyone "Сви су видели" - :status-seen "Прегледано" - :status-delivered "Достављено" - :status-failed "Неуспешно" - - ;;datetime - :datetime-ago-format "{{ago}} {{number}} {{time-intervals}}" - :datetime-second {:one "секунда" - :other "секунди"} - :datetime-minute {:one "минут" - :other "минута"} - :datetime-hour {:one "сат" - :other "сати"} - :datetime-day {:one "дан" - :other "дана"} - :datetime-ago "пре" - :datetime-yesterday "јуче" - :datetime-today "данас" - - ;;profile - :profile "Профил" - :edit-profile "Ажурирајте профил" - :message "Порука" - :not-specified "Није специфицирано" - :public-key "Јавни кључ" - :phone-number "Број телефона" - :update-status "Ажурирајте свој статус..." - :add-a-status "Додајте статус..." - :status-prompt "Поставите свој статус. Коришћење #тагова ће омогућити другима да те упознају и причају о ономе о чему размишљаш" - :add-to-contacts "Додај у контакте" - :in-contacts "У контактима" - :remove-from-contacts "Уклони из контаката" - :start-conversation "Започни разговор" - :send-transaction "Пошаљи трансакцију" - :testnet-text "Налазите се на {{testnet}} тест мрежи. Не шаљите прави ETH или SNT на своју адресу" - :mainnet-text "Налазите се на главној мрежи. Прави ETH ће бити послат" - - ;;make_photo - :image-source-title "Профилна слика" - :image-source-make-photo "Сликај" - :image-source-gallery "Изабери из галерије" - - ;;sharing - :sharing-copy-to-clipboard "Копирај" - :sharing-share "Подели..." - :sharing-cancel "Одустани" - - :browsing-title "Претражи" - :browsing-open-in-web-browser "Отвори у интернет претраживачу" - :browsing-cancel "Одустани" - - ;;sign-up - :contacts-syncronized "Ваши контакти су синхронизовани" - :confirmation-code (str "Хвала! Послали смо вам СМС поруку са кодом " - "Молимо вас да унесете код да би сте потврдили свој број телефона") - :incorrect-code (str "Жао нам је код је неисправан, молимо вас покушајте поново.") - :phew-here-is-your-passphrase "Уф, то је било тешко. Ово је ваша лозинка, *запишите је и чувајте на сигурном!* Требаће вам за обнову налога." - :here-is-your-passphrase "Ово је ваша лозинка, *Запишите је и чувајте на сигурном!* Требаће вам за обнову налога." - :here-is-your-signing-phrase "Ово је ваша фраза за потписивање. Користићете је за потврду својих трансакција. *Запишите је и чувајте на сигурном!*" - :phone-number-required "Кликните овде да бисте потврдили свој број телефона и пронаћи ћу ваше пријатеље." - :shake-your-phone "Пронашли сте грешку или имате предлог? Само ~протресите~ свој мобилни телефон!" - :intro-status "Разговарајте са мном да бисте подесили свој налог." - :intro-message1 "Добродошли у Статус!\nЗа почетак кликните на ову поруку да поставите своју шифру." - :account-generation-message "Само тренутак, одрађујем неку жешћу математику око креирања вашег налога!" - :move-to-internal-failure-message "Потребно је да неке важне фајлове пребацимо из екстерног у интерно складиште. Да бисмо то урадили, потребна нам је ваша дозвола. Нећемо користити екстерно складиште у наредним верзијама." - :debug-enabled "Дебаговање сервера је покренуто! Сад можете да извршиш *status-dev-cli scan* да бисте нашли сервер са свог компјутера на истој мрежи." - - ;;phone types - :phone-e164 "Међународни 1" - :phone-international "Међународни 2" - :phone-national "Домаћи" - :phone-significant "Значајан" - - ;;chats - :chats "Разговори" - :delete-chat "Обришите разговор" - :new-group-chat "Нова група за разговор" - :new-public-group-chat "Придружите се јавном разговору" - :edit-chats "Уредите разговор" - :search-chats "Претражите разговор" - :empty-topic "Празна тема" - :topic-format "Погрешан формат [a-z0-9\\-]+" - :public-group-topic "Тема" - - ;;discover - :discover "Открића" - :none "Ништа" - :search-tags "Укуцајте кључне речи за претрагу" - :popular-tags "Популарни #тагови" - :recent "Нови статуси" - :no-statuses-found "Ниједан статус није пронађен" - :chat "Разговор" - :all "Све" - :public-chats "Јавни разговори" - :soon "Ускоро" - :public-chat-user-count "{{count}} људи" - :dapps "ÐApps" - :dapp-profile "ÐApp профил" - :no-statuses-discovered "Нема пронађених статуса" - :no-statuses-discovered-body "Кад неко објави статус\nвидећете га овде." - :no-hashtags-discovered-title "Нема пронађених #тагова" - :no-hashtags-discovered-body "Кад #таг постане популаран\nвидећете га овде." - - ;;settings - :settings "Подешавања" - - ;;contacts - :contacts "Контакти" - :new-contact "Нови контакт" - :delete-contact "Обриши контакт" - :delete-contact-confirmation "Овај контакт ће бити уклоњен из вашег адресара" - :remove-from-group "Уклони из групе" - :edit-contacts "Уреди контакте" - :search-contacts "Претражи контакте" - :contacts-group-new-chat "Започни нов разговор" - :choose-from-contacts "Изабери из контаката" - :no-contacts "Још увек без контаката" - :show-qr "Прикажи QR code" - :enter-address "Унесите адресу" - :more "још" - - ;;group-settings - :remove "Уклони" - :save "Сачувај" - :delete "Обриши" - :clear-history "Очисти историју" - :mute-notifications "Утишај обавештења" - :leave-chat "Напусти разговор" - :chat-settings "Подешавања разговора" - :edit "Уреди" - :add-members "Додај чланове" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Нова група" - :reorder-groups "Промени распоред група" - :edit-group "Уреди групу" - :delete-group "Обриши групу" - :delete-group-confirmation "Ова група ће бити уклоњена са ваше листе група. Ово неће утицати на ваше контакте" - :delete-group-prompt "Ово неће утицати на ваше контакте" - :contact-s {:one "контакт" - :other "контаката"} - - ;;protocol - :received-invitation "примили сте позивницу за разговор" - :removed-from-chat "напусти групу за разговор" - :left "остати" - :invited "позвани" - :removed "уклоњени" - :You "Ви" - - ;;new-contact - :add-new-contact "Додај нови контакт" - :scan-qr "Скенирај QR код" - :name "Име" - :address-explication "Ваш јавни кључ је ваша адреса на Етереуму и представља низ бројева и слова. Можете га лако пронаћи у свом профилу" - :enter-valid-public-key "Молимо унесите исправан јавни кључ или скенирајте QR код" - :contact-already-added "Контакт је већ додат" - :can-not-add-yourself "Не можете да додате самог себе" - :unknown-address "Непозната адреса" - - ;;login - :connect "Повежи" - :address "Адреса" - :password "Лозинка" - :sign-in-to-status "Улогујте се на Статус" - :sign-in "Улогујте се" - :wrong-password "Погрешна лозинка" - :enter-password "Унесите лозинку" - - ;;recover - :passphrase "Тајна фраза" - :recover "Обнова налога" - :twelve-words-in-correct-order "12 речи у тачном редоследу" - - ;;accounts - :recover-access "Обнови налог" - :create-new-account "Направи нови налог" - - ;;wallet-qr-code - :done "Урађено" - - ;;validation - :invalid-phone "Неисправан број телефона" - :amount "Износ" - - ;;transactions - :confirm "Потврди" - :transaction "Трансакција" - :unsigned-transaction-expired "Непотписана трансакција је истекла" - :status "Статус" - :recipient "Прималац" - :to "Коме" - :from "Од" - :data "Подаци" - :got-it "ОК" - :block "Блокирај" - :hash "Hash" - :gas-limit "Лимит гаса" - :gas-price "Цена гаса" - :gas-used "Искоришћени гас" - :cost-fee "Цена" - :nonce "Nonce" - :confirmations "Потврде" - :confirmations-helper-text "Молимо вас сачекајте најмање 12 потврда како бисте били сигурни да је ваша трансакција безбедно обрађена" - :copy-transaction-hash "Копирај hash трансакције" - :open-on-etherscan "Отвори Etherscan.io" - - ;;webview - :web-view-error "упс, грешка" - - ;;testfairy warning - :testfairy-title "Упозорење!" - :testfairy-message "Користите експерименталну верзију апликације. За потребе тестирања ова верзија снима сесије уколико користите Wi-Fi конекцију. Свака ваша интеракција са овом апликацијом сачувана као видео запис и може бити коришћена од стране нашег развојног тима приликом истраживању могућих проблема. Сачувани видео не садржи ваше лозинке. Снимање се врши искључиво на екперименталним верзијама. Ништа се не снима уколико је апликација инсталирана преко PlayStore-а или TestFlight-а." - - ;; wallet - :wallet "Новчаник" - :wallets "Новчаници" - :your-wallets "Ваш новчаник" - :main-wallet "Главни новчаник" - :wallet-error "Грешка при учитавању података" - :wallet-send "Пошаљи" - :wallet-request "Захтев" - :wallet-exchange "Размена" - :wallet-assets "Средства" - :wallet-add-asset "Додај средства" - :wallet-total-value "Укупна вредност" - :wallet-settings "Подешавања новчаника" - :signing-phrase-description "Доделите трансакцију укуцавањем своје лозинке. Побрините се да се речи изнад поклапају са вашом тајном фразом за потписивање" - :wallet-insufficient-funds "Недовољно средстава" - :request-transaction "Захтевајте трансакцију" - :send-request "Пошаљите захтев" - :share "Подели" - :eth "ETH" - :currency "Валута" - :usd-currency "USD" - :transactions "Трансакције" - :transaction-details "Детаљи трансакције" - :transaction-failed "Неуспела трансакција" - :transactions-sign "Додели" - :transactions-sign-all "Додели све" - :transactions-sign-transaction "Додели трансакцију" - :transactions-sign-later "Додели касније" - :transactions-delete "Обриши трансакцију" - :transactions-delete-content "Трансакција ће бити уклоњена из листе непотписаних" - :transactions-history "Историја" - :transactions-unsigned "Непотписане" - :transactions-history-empty "Још увек немате трансакције у историји" - :transactions-unsigned-empty "Немате недодељене трансакције" - :transactions-filter-title "Филтрирај историју" - :transactions-filter-tokens "Токени" - :transactions-filter-type "Тип" - :transactions-filter-select-all "Изабери све" - :view-transaction-details "Прикажи детаље трансакција" - :transaction-description "Молимо вас сачекајте најмање 12 потврда да бисте потврдили да је ваша трансакција успешно обрађена" - :transaction-sent "Трансакција послата" - :transaction-moved-text "Ова трансакција ће остати у листи непотписаних током наредних 5 мин" - :transaction-moved-title "Трансакција је померена" - :sign-later-title "Потпишите трансакцију касније?" - :sign-later-text "Проверите историју трансакција да бисте потписали ову трансакцију" - :not-applicable "Није примењиво за непотписане трансакције" - - ;; Wallet Send - :wallet-choose-recipient "Изабери примаоца" - :wallet-choose-from-contacts "Изабери из контаката" - :wallet-address-from-clipboard "Користи адресу из Клипборда" - :wallet-invalid-address "Неисправна адреса: \n {{data}}" - :wallet-browse-photos "Претражи слике" - :validation-amount-invalid-number "Износ није исправан број" - :validation-amount-is-too-precise "Износ има превише децимала. Најмања јединица коју можете послати је 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Нова мрежа" - :add-network "Додај мрежу" - :add-new-network "Додај нову мрежу" - :existing-networks "Постојеће мреже" - :add-json-file "Додај JSON фајл" - :paste-json-as-text "Налепи JSON као текст" - :paste-json "Налепи JSON" - :specify-rpc-url "Дефиниши RPC URL" - :edit-network-config "Уреди конфигурацију мреже" - :connected "Повезано" - :process-json "Обради JSON" - :error-processing-json "Грешка у обради JSON" - :rpc-url "RPC URL" - :remove-network "Уклони мрежу" - :network-settings "Подешавања мреже" - :edit-network-warning "Будите пажљиви, подешавање података мреже може да онеспособи ову мрежу" - :connecting-requires-login "Повезивање на другу мрежу захтева да се поново улогујете" - :close-app-title "Упозорење!" - :close-app-content "Ова апликација ће да буде рестартована. Када је поново отворите, биће коришћена изабрана мрежа" - :close-app-button "Потврди"}) diff --git a/src/status_im/translations/sr_rs_latn.cljs b/src/status_im/translations/sr_rs_latn.cljs deleted file mode 100644 index 5185d934227..00000000000 --- a/src/status_im/translations/sr_rs_latn.cljs +++ /dev/null @@ -1,77 +0,0 @@ -(ns status-im.translations.sr-rs-latn - (:require [status-im.translations.sr-rs-cyrl :as sr-rs-cyrl] - [clojure.string :as string])) - -(def letter-pairs - {"А" "A" - "Б" "B" - "В" "V" - "Г" "G" - "Д" "D" - "Ђ" "Đ" - "Е" "E" - "Ж" "Ž" - "З" "Z" - "И" "I" - "Ј" "J" - "К" "K" - "Л" "L" - "Љ" "Lj" - "М" "M" - "Н" "N" - "Њ" "Nj" - "О" "O" - "П" "P" - "Р" "R" - "С" "S" - "Т" "T" - "Ћ" "Ć" - "У" "U" - "Ф" "F" - "Х" "H" - "Ц" "C" - "Ч" "Č" - "Џ" "Dž" - "Ш" "Š" - - "а" "a" - "б" "b" - "в" "v" - "г" "g" - "д" "d" - "ђ" "đ" - "е" "e" - "ж" "ž" - "з" "z" - "и" "i" - "ј" "j" - "к" "k" - "л" "l" - "љ" "lj" - "м" "m" - "н" "n" - "њ" "nj" - "о" "o" - "п" "p" - "р" "r" - "с" "s" - "т" "t" - "ћ" "ć" - "у" "u" - "ф" "f" - "х" "h" - "ц" "c" - "ч" "č" - "џ" "dž" - "ш" "š"}) - -(defn cyr->lat [cyr] - (cond - (string? cyr) (string/join - (map #(get letter-pairs (str %) (str %)) cyr)) - (map? cyr) (into {} - (map (fn [[k v]] [k (cyr->lat v)]) cyr)) - :else nil)) - -(def translations - (cyr->lat sr-rs-cyrl/translations)) diff --git a/src/status_im/translations/sv.cljs b/src/status_im/translations/sv.cljs deleted file mode 100644 index adea9f8f0f8..00000000000 --- a/src/status_im/translations/sv.cljs +++ /dev/null @@ -1,359 +0,0 @@ -(ns status-im.translations.sv) - -(def translations - {;;common - :members-title "Medlemmar" - :not-implemented "!inte implementerad" - :chat-name "Chattnamn" - :notifications-title "Aviseringar och ljud" - :offline "Offline" - :search-for "Sök efter..." - :cancel "Avbryt" - :next "Nästa" - :open "Öppna" - :description "Beskrivning" - :url "URL" - :type-a-message "Skriv ett meddelande..." - :type-a-command "Skriv ett kommando..." - :error "Ett fel uppstod" - :unknown-status-go-error "Okänt status-go fel" - :node-unavailable "Ingen ethereum-nod är igång" - :yes "Ja" - :no "Nej" - - :camera-access-error "För att bevilja nödvändinga kamerarättigheter, gå till systeminställningarna och kontrollera att Status > Kamera är vald." - :photos-access-error "För att bevilja nödvändinga fotorättigheter, gå till systeminställiningarna och kontrollera att Status > Foton är vald." - - ;;drawer - :switch-users "Byt användare" - :current-network "Nuvarande nätverk" - - ;;chat - :is-typing "skriver" - :and-you "och du" - :search-chat "Sök chatt" - :members {:one "1 medlem" - :other "{{count}} medlemmar" - :zero "inga medlemmar"} - :members-active {:one "1 medlem" - :other "{{count}} medlemmar" - :zero "inga medlemmar"} - :public-group-status "Offentlig" - :active-online "Online" - :active-unknown "Okänd" - :available "Tillgänglig" - :no-messages "Inga meddelanden" - :suggestions-requests "Begäranden" - :suggestions-commands "Kommandon" - :faucet-success "Faucet-begäran mottagen" - :faucet-error "Felaktig faucet-egäran" - - ;;sync - :sync-in-progress "Synkroniserar..." - :sync-synced "Synkroniserad" - - ;;messages - :status-sending "Skickar" - :status-pending "Avvaktan" - :status-sent "Skickad" - :status-seen-by-everyone "Sedd av alla" - :status-seen "Sedd" - :status-delivered "Levererad" - :status-failed "Misslyckad" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "sekund" - :other "sekunder"} - :datetime-minute {:one "minut" - :other "minuter"} - :datetime-hour {:one "timma" - :other "timmar"} - :datetime-day {:one "dag" - :other "dagar"} - :datetime-ago "sedan" - :datetime-yesterday "igår" - :datetime-today "idag" - - ;;profile - :profile "Profil" - :edit-profile "Redigera profil" - :message "Meddelande" - :not-specified "Inte angivet" - :public-key "Offentlig nyckel" - :phone-number "Telefonnummer" - :update-status "Uppdatera din status..." - :add-a-status "Lägg till status..." - :status-prompt "Lägg till din status. Använd #hashtags för att göra det lättare för andra att hitta dig och tala om det som intresserar dig" - :add-to-contacts "Lägg till i kontakter" - :in-contacts "I kontakterna" - :remove-from-contacts "Ta bort från kontakter" - :start-conversation "Börja en konversation" - :send-transaction "Sänd en transaktion" - :testnet-text "Du är på {{testnet}} Testnet. Sänd inte riktiga ETH eller SNT till din address" - :mainnet-text "Du är på Mainnet. Äkta ETH kommer att sändas" - - ;;make_photo - :image-source-title "Profilbild" - :image-source-make-photo "Fånga" - :image-source-gallery "Välj från galleri" - - ;;sharing - :sharing-copy-to-clipboard "Kopiera till urklipp" - :sharing-share "Dela..." - :sharing-cancel "Avbryt" - - :browsing-title "Webbläsning" - :browsing-open-in-web-browser "Öppna in webbläsare" - :browsing-cancel "Avbryt" - - ;;sign-up - :contacts-syncronized "Dina kontakter har synkroniserats" - :confirmation-code (str "Tack! Vi har skickat dig ett textmeddelande med en bekräftelse " - "kod. Var god ange den koden för att bekräfta ditt telefonnummer") - :incorrect-code (str "Tyvärr var koden felaktig, var god ange den igen") - :phew-here-is-your-passphrase "*Pust* det var svårt, här är din lösenordsfras, *skriv ner den och förvara den säkert!* Du kommer att behöva den för att återställa ditt konto." - :here-is-your-passphrase "Här är din lösenordsfras, *skriv ner den och förvara den säkert!* Du kommer att behöva den för att återställa ditt konto." - :here-is-your-signing-phrase "Här är din underskriftsfras. Du kommer att använde den för att verifiera dina transaktioner. *Skriv ner den och förvara den säkert!*" - :phone-number-required "Tryck här för att ange ditt telefonnummer så jag kommer att hitta dina vänner" - :shake-your-phone "Hittade du ett fel eller har du ett förbättringsförslag? ~Skaka~ bara din telefon!" - :intro-status "Chatta med mig för att konfigurera ditt konto och ändra dina inställningar!" - :intro-message1 "Välkommen till Status\nTryck på detta meddelande för att ange ett lösenord och komma igång!" - :account-generation-message "Vänta ett ögonblick, jag måste göra lite galna beräkningar för att generera ditt konto!" - :move-to-internal-failure-message "Vi måste flytta vissa viktiga filer från extern lagring till intern lagring. För att göra detta behöver vi ditt tillstånd. Vi kommer inte längre att använda extern lagring i framtida versioner." - :debug-enabled "Debugservern har startats! Du kan nu köra *status-dev-cli scan* för att hitta servern på samma nätverk som din dator." - - ;;phone types - :phone-e164 "Internationell 1" - :phone-international "Internationell 2" - :phone-national "Nationell" - :phone-significant "Viktig" - - ;;chats - :chats "Chattar" - :delete-chat "Ta bort chatt" - :new-group-chat "Ny gruppchatt" - :new-public-group-chat "Gå med i offentlig chatt" - :edit-chats "Redigera chattar" - :search-chats "Sök chattar" - :empty-topic "Tom rubrik" - :topic-format "Felaktigt format [a-z0-9\\-]+" - :public-group-topic "Rubrik" - - ;;discover - :discover "Upptäck" - :none "Inga" - :search-tags "Skriv dina söktaggar här" - :popular-tags "Populära taggar" - :recent "Senaste" - :no-statuses-found "Inga statusar hittade" - :chat "Chatta" - :all "Alla" - :public-chats "Offentliga chattar" - :soon "Snart" - :public-chat-user-count "{{count}} deltagare" - :dapps "ÐApps" - :dapp-profile "ÐApp profil" - :no-statuses-discovered "Inga statusar upptäckta" - :no-statuses-discovered-body "När någon har publicerat\nen status ser du den här." - :no-hashtags-discovered-title "Inga #hashtags upptäcktes" - :no-hashtags-discovered-body "När en #hashtag blir\npopulär ser du den här." - - ;;settings - :settings "Inställningar" - - ;;contacts - :contacts "Kontakter" - :new-contact "Ny kontakt" - :delete-contact "Ta bort kontakt" - :delete-contact-confirmation "Denna kontakt tas bort från dina kontakter" - :remove-from-group "Ta bort från grupp" - :edit-contacts "Redigera kontakter" - :search-contacts "Sök kontakter" - :contacts-group-new-chat "Starta ny chatt" - :choose-from-contacts "Välj från kontakter" - :no-contacts "Inga kontakter ännu" - :show-qr "Visa QR kod" - :enter-address "Ge address" - :more "mer" - - ;;group-settings - :remove "Ta bort" - :save "Spara" - :delete "Ta bort" - :clear-history "Rensa historik" - :mute-notifications "Tysta aviseringar" - :leave-chat "Lämna chatt" - :chat-settings "Chattinställningar" - :edit "Redigera" - :add-members "Lägg till medlemmar" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Ny grupp" - :reorder-groups "Ordna grupper" - :edit-group "Redigera grupp" - :delete-group "Ta bort grupp" - :delete-group-confirmation "Denna grupp kommer att tas bort från dina grupper. Detta påverkar inte dina kontakter" - :delete-group-prompt "Detta påverkar inte dina kontakter" - :contact-s {:one "kontakt" - :other "kontakter"} - - ;;protocol - :received-invitation "tog emot chattinbjudan" - :removed-from-chat "tog bort dig från gruppchatten" - :left "lämnade" - :invited "inbjudna" - :removed "borttagna" - :You "Du" - - ;;new-contact - :add-new-contact "Lägg till ny kontakt" - :scan-qr "Skanna QR kod" - :name "Namn" - :address-explication "Din offentliga nykel används för att generera din address på Ethereum och är en serie siffror och bokstäver. Du kan lätt hitta den i din profil" - :enter-valid-public-key "Var vänlig och ange en giltig offentlig nyckel eller skanna en QR kod" - :contact-already-added "Kontakten har redan lagts till" - :can-not-add-yourself "Du kan inte lägga till dig själv" - :unknown-address "Okänd adress" - - ;;login - :connect "Anslut" - :address "Adress" - :password "Lösenord" - :sign-in-to-status "Logga in in Status" - :sign-in "Logga in" - :wrong-password "Fel lösenord" - :enter-password "Ge lösenord" - - ;;recover - :passphrase "Lösenordsfras" - :recover "Återställ" - :twelve-words-in-correct-order "12 ord i korrekt ordning" - - ;;accounts - :recover-access "Återställ åtkomst" - :create-new-account "Skapa nytt konto" - - ;;wallet-qr-code - :done "Klar" - - ;;validation - :invalid-phone "Ogiltigt telefonnummer" - :amount "Belopp" - - ;;transactions - :confirm "Bekräfta" - :transaction "Transaktion" - :unsigned-transaction-expired "Ej underskriven transaktion förföll" - :status "Status" - :recipient "Mottagare" - :to "Till" - :from "Från" - :data "Data" - :got-it "OK" - :block "Block" - :hash "Hash" - :gas-limit "Gas begränsning" - :gas-price "Gas pris" - :gas-used "Gas använt" - :cost-fee "Kostnad/Avgift" - :nonce "Nonce" - :confirmations "Bekräftelser" - :confirmations-helper-text "Var god och vänta på minst 12 bekräftelser för att försäkra dig om att transaktionen gått igenom" - :copy-transaction-hash "Kopiera transaktionshash" - :open-on-etherscan "Öppna på Etherscan.io" - :incoming "Inkommande" - :outgoing "Utgående" - :pending "Pågående" - :postponed "Uppskjutna" - - ;;webview - :web-view-error "hoppsan, fel" - - ;;testfairy warning - :testfairy-title "Varning!" - :testfairy-message "Du använder en app som installerats från en nattlig version. För att underlätta testning spelar denna version in sessionen om WiFi används. All interaktion med appen sparas (som video och loggfiler) och kan användas av utvecklarna för att undersöka möjliga fel. Sparade video/loggfiler innehåller inte lösenord. Inspelningen sker endast om appen installeras från en nattlig version. Inget spelas in om appen installeras från PlayStore eller TestFlight." - - ;; wallet - :wallet "Plånbok" - :wallets "Plånböcker" - :your-wallets "Dina plånböcker" - :main-wallet "Huvudplånbok" - :wallet-error "Fel vid laddning av data" - :wallet-send "Sänd" - :wallet-send-token "Sänd {{symbol}}" - :wallet-request "Begär" - :wallet-exchange "Växla" - :wallet-assets "Tillgångar" - :wallet-add-asset "Lägg till tillgång" - :wallet-total-value "Totalt värde" - :wallet-settings "Plånboksinställningar" - :wallet-manage-assets "Hantera tillgångar" - :signing-phrase-description "Skriv under transaktionen genom att ange ditt lösenord. Kontrollera att orden ovan motsvarar din hemliga underskriftsfras" - :wallet-insufficient-funds "Otillräckligt kapital" - :request-transaction "Begär transaktion" - :send-request "Skicka begäran" - :share "Dela" - :eth "ETH" - :currency "Valuta" - :usd-currency "USD" - :transactions "Transaktioner" - :transaction-details "Transaktiondetaljer" - :transaction-failed "Transaktionen misslyckades" - :transactions-sign "Skriv under" - :transactions-sign-all "Skriv under alla" - :transactions-sign-transaction "Skriv under transaktionen" - :transactions-sign-later "Skriv under senare" - :transactions-delete "Ta bort transaktion" - :transactions-delete-content "Transaktionen tas bort från listan 'Ej underskrivna'" - :transactions-history "Historik" - :transactions-unsigned "Ej underskrivna" - :transactions-history-empty "Inga transaktioner i din historik ännu" - :transactions-unsigned-empty "Du har inga ej underskrivna transaktioner" - :transactions-filter-title "Filtrera historik" - :transactions-filter-tokens "Tokens" - :transactions-filter-type "Typ" - :transactions-filter-select-all "Välj alla" - :view-transaction-details "Visa transaktionsdetaljer" - :transaction-description "Var god och vänta på minst 12 bekräftelser för att försäkra dig om att transaktionen gått igenom" - :transaction-sent "Transaktionen skickad" - :transaction-moved-text "Transaktionen står kvar på listan 'Ej underskrivna' i 5 minuter" - :transaction-moved-title "Transaktionen flyttades" - :sign-later-title "Skriv under transaktionen senare?" - :sign-later-text "Kontrollera transaktionshistoriken för att skriva under denna transaktion" - :not-applicable "Otillämplig på ej underskrivna transaktioner" - - ;; Wallet Send - :wallet-choose-recipient "Välj mottagare" - :wallet-choose-from-contacts "Välj från kontakter" - :wallet-address-from-clipboard "Använd address i urklipp" - :wallet-invalid-address "Ogiltig address: \n {{data}}" - :wallet-invalid-chain-id "Felaktigt nätverk: \n {{data}}" - :wallet-browse-photos "Bläddra foton" - :validation-amount-invalid-number "Beloppet är inte ett giltigt tal" - :validation-amount-is-too-precise "Summan är för precis Den minsta summan du kan skicka är 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Nytt nätverk" - :add-network "Lägg till nätverk" - :add-new-network "Lägg till nytt nätverk" - :existing-networks "Nuvarande nätverk" - :add-json-file "Lägg till JSON fil" - :paste-json-as-text "Klistra in JSON som text" - :paste-json "Klistra in JSON" - :specify-rpc-url "Ange RPC URL" - :edit-network-config "Redigera nätverksinställningar" - :connected "Ansluten" - :process-json "Bearbeta JSON" - :error-processing-json "Bearbetning av JSON misslyckades" - :rpc-url "RPC URL" - :remove-network "Ta bort nätverk" - :network-settings "Nätverksinställningar" - :edit-network-warning "Var försiktig. Om du redigerar nätverksinställningarna kan nätverket tas ur bruk för dig" - :connecting-requires-login "Du måste logga in för att kunna ansluta till ett annat nätverk" - :close-app-title "Varning!" - :close-app-content "Denna app kommer att avslutas. När du öppnar den på nytt kommer det valda nätverket att användas" - :close-app-button "Bekräfta"}) diff --git a/src/status_im/translations/sw.cljs b/src/status_im/translations/sw.cljs deleted file mode 100644 index 476641591a3..00000000000 --- a/src/status_im/translations/sw.cljs +++ /dev/null @@ -1,166 +0,0 @@ -(ns status-im.translations.sw) - -(def translations - {;;common - :members-title "Wanachama" - :not-implemented "!haijatekelezwa" - :chat-name "Jina la gumzo" - :notifications-title "Notisi na sauti" - :offline "Nje ya mtandao" - - ;;drawer - :switch-users "Badili kwa watumiaji" - - ;;chat - :is-typing "anaandika" - :and-you "na wewe" - :search-chat "Tafuta gumzo" - :members {:one "Mwanachama 1" - :other "{{count}} wanachama" - :zero "hakuna wanachama"} - :members-active {:one "Mwanachama 1, Aliyewajibika 1" - :other "{{count}} wanachama, {{count}} waliowajibika" - :zero "hakuna wanachama"} - :active-online "Mtandaoni" - :active-unknown "Hawajulikani" - :available "Wanapatikana" - :no-messages "Hakuna ujumbe" - :suggestions-requests "Maombi" - :suggestions-commands "Amri" - - ;;sync - :sync-in-progress "Kulandanisha..." - :sync-synced "Katika ulandanishaji" - - ;;messages - :status-sending "Kutuma" - :status-pending "Inasubiri" - :status-sent "Tuma" - :status-seen-by-everyone "Imeonekana na kila mtu" - :status-seen "Imeonekana" - :status-delivered "Imefikishwa" - :status-failed "Imeshindwa" - - ;;datetime - :datetime-second {:one "sekunde" - :other "sekunde"} - :datetime-minute {:one "dakika" - :other "dakika"} - :datetime-hour {:one "saa" - :other "masaa"} - :datetime-day {:one "siku" - :other "siku"} - :datetime-ago "iliyopita" - :datetime-yesterday "jana" - :datetime-today "leo" - - ;;profile - :profile "Profaili" - :message "Ujumbe" - :not-specified "Haijafafanuliwa" - :public-key "Ufunguo wa Umma" - :phone-number "Namba ya Simu" - :add-to-contacts "Ongeza kwa mawasiliano" - - ;;make_photo - :image-source-title "Picha ya profaili" - :image-source-make-photo "Chukua picha" - :image-source-gallery "Chagua kutoka nyumba ya sanaa" - - ;;sharing - :sharing-copy-to-clipboard "Kopiera" - :sharing-share "Dela..." - :sharing-cancel "Ghairi" - - ;;sign-up - :contacts-syncronized "Mawasiliano yako yamelandanishwa" - :confirmation-code (str "Asante! Tumekutumia ujumbe mfupi na uthibitisho " - "kificho. Tafadhali peana hicho kificho kuthibitisha namba yako ya simu") - :incorrect-code (str "Samahani kificho hakikuwa sahihi, tafadhali ingiza tena") - :phew-here-is-your-passphrase "*Phew* hiyo ilikuwa ngumu, hapa ni kaulisiri yako, *iandike na uiweke salama!* Utaihitaji kwa ajili ya kufufua akaunti yako." - :here-is-your-passphrase "Hapa ni kaulisiri yako, *iandike na uiweke salama!* Utaihitaji kwa ajili ya kufufua akaunti yako." - :phone-number-required "Bofya hapa kuingiza namba yako ya simu na nitapata marafiki zako" - :intro-status "Ongea nami kuanzisha akaunti yako na kubadilisha mipangilio yako!" - :intro-message1 "Karibu kwa Hali na Ubofye ujumbe huu ili kuweka nenosiri lako na uanze!" - :account-generation-message "Nipe sekunde, naenda kufanya baadhi ya hisabati kutengeneza akaunti yako!" - - ;;chats - :chats "Gumzo" - :new-group-chat "Gumzo mpya ya kikundi" - - ;;discover - :discover "Ugunduzi" - :none "Hakuna" - :search-tags "Andika vitambulisho vyako vya kutafuta hapa" - :popular-tags "Vitambulisho maarufu" - :recent "Hivi karibuni" - :no-statuses-discovered "Hakuna hali zimegundulika" - - ;;settings - :settings "Mipangilio" - - ;;contacts - :contacts "Mawasiliano" - :new-contact "Mawasiliano mapya" - :contacts-group-new-chat "Anza gumzo mpya" - :no-contacts "Bado hakuna mawasiliano" - :show-qr "Onyesha QR" - - ;;group-settings - :remove "Ondoa" - :save "Hifadhi" - :clear-history "Futa historia" - :chat-settings "Mipangilio ya gumzo" - :edit "Hariri" - :add-members "Ongeza wanachama" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "mwaliko wa gumzo ulipokelewa" - :removed-from-chat "uliondolewa kwenye kikundi cha gumzo" - :left "uliondoka" - :invited "ulialikwa" - :removed "uliondolewa" - :You "Wewe" - - ;;new-contact - :add-new-contact "Ongeza mawasiliano mapya" - :scan-qr "Piga picha QR" - :name "Jina" - :address-explication "Labda hapa kunapaswa kuwa na baadhi ya maandishi kueleza anwani ni nini na ni wapi pa kuitafuta" - :contact-already-added "Tayari mawasiliano yameongezwa" - :can-not-add-yourself "Huwezi kujiongeza mwenyewe" - :unknown-address "Anwani Haijulikani" - - ;;login - :connect "Unganisha" - :address "Anwani" - :password "Nenosiri" - :wrong-password "Nenosiri sio halali" - - ;;recover - :passphrase "Kaulisiri" - :recover "Okoa" - - ;;accounts - :recover-access "Okoa ufikiaji/Upatikanaji" - - ;;wallet-qr-code - :done "Imefanyika" - :main-wallet "Mkoba Mkuu" - - ;;validation - :invalid-phone "Namba ya simu ni batili" - :amount "Kiasi" - ;;transactions - :status "Hali" - :recipient "Mpokeaji" - - ;;:webview - :web-view-error "nadhani, hitilafu"}) diff --git a/src/status_im/translations/th.cljs b/src/status_im/translations/th.cljs deleted file mode 100644 index 26daa3b5bb9..00000000000 --- a/src/status_im/translations/th.cljs +++ /dev/null @@ -1,230 +0,0 @@ -(ns status-im.translations.th) - -(def translations - {;;common - :members-title "สมาชิก" - :not-implemented "!ยังไม่ได้ดำเนินการ" - :chat-name "ชื่อแชท" - :notifications-title "การแจ้งเตือนและเสียง" - :offline "ออฟไลน์" - - ;;drawer - :switch-users "สลับผู้ใช้" - - ;;chat - :is-typing "กำลังพิมพ์" - :and-you "และคุณ" - :search-chat "ค้นหาแชท" - :members {:one "1 สมาชิก" - :other "{{count}} สมาชิก" - :zero "ไม่มีสมาชิก"} - :members-active {:one "1 สมาชิก, 1 ใช้งานอยู่" - :other "{{count}} สมาชิก, {{count}} ใช้งานอยู่" - :zero "ไม่มีสมาชิก"} - :active-online "ออนไลน์" - :active-unknown "ไม่ทราบ" - :available "มีให้ใช้ได้" - :no-messages "ไม่มีข้อความ" - :suggestions-requests "คำร้องขอ" - :suggestions-commands "คำสั่ง" - - ;;sync - :sync-in-progress "กำลังซิงค์..." - :sync-synced "ระหว่างการซิงค์" - - ;;messages - :status-sending "กำลังส่ง" - :status-pending "อยู่ระหว่างดำเนินการ" - :status-sent "ส่งแล้ว" - :status-seen-by-everyone "อ่านแล้วโดยทุกคน" - :status-seen "อ่านแล้ว" - :status-delivered "ส่งแล้ว" - :status-failed "ล้มเหลว" - - ;;datetime - :datetime-second {:one "วินาที" - :other "วินาที"} - :datetime-minute {:one "นาที" - :other "นาที"} - :datetime-hour {:one "ชั่วโมง" - :other "ชั่วโมง"} - :datetime-day {:one "วัน" - :other "วัน"} - :datetime-ago "ที่ผ่านมา" - :datetime-yesterday "เมื่อวาน" - :datetime-today "วันนี้" - - ;;profile - :profile "โปรไฟล์" - :message "ข้อความ" - :not-specified "ไม่ระบุ" - :public-key "คีย์สาธารณะ" - :phone-number "หมายเลขโทรศัพท์" - :add-to-contacts "เพิ่มไปยังผู้ติดต่อ" - - ;;make_photo - :image-source-title "รูปโปรไฟล์" - :image-source-make-photo "ถ่ายภาพ" - :image-source-gallery "เลือกจากแกลเลอรี" - - ;;sign-up - :contacts-syncronized "ไซิงค์รายชื่อผู้ติดต่อของคุณแล้ว" - :confirmation-code (str "ขอขอบคุณ! เราได้ส่งข้อความตัวอักษรพร้อมรหัสยืนยันแล้ว" - "โปรดมอบรหัสนั้นเพื่อยืนยันหมายเลขโทรศัพท์ของคุณ") - :incorrect-code (str "ขออภัย รหัสไม่ถูกต้อง โปรดกรอกอีกครั้ง") - :phew-here-is-your-passphrase "*โล่งอกไปที* มันไม่ง่ายเลย นี่คือวลีรหัสผ่านของคุณ *จดมันไว้และรักษามันให้ปลอดภัย!* คุณจะจำเป็นต้องใช้มันเพื่อกู้คืนบัญชีของคุณ" - :here-is-your-passphrase "นี่คือวลีรหัสผ่านของคุณ *จดมันไว้และรักษามันให้ปลอดภัย!* คุณจะจำเป็นต้องใช้มันเพื่อกู้คืนบัญชีของคุณ" - :phone-number-required "แตะที่นี่เพื่อกรอกหมายเลขโทรศัพท์ของคุณ & ฉันจะค้นหาเพื่อนของคุณ" - :intro-status "แชทกับฉันเพื่อตั้งค่าบัญชีของคุณและเปลี่ยนการตั้งค่าของคุณ!" - :intro-message1 "ยินดีต้อนรับสู่สถานะ \n แตะข้อความนี้เพื่อตั้งรหัสผ่านของคุณ & เริ่มต้น!" - :account-generation-message "ให้เวลาฉันหนึ่งวินาที ฉันจะต้องคำนวณอย่างหนักเพื่อสร้างบัญชีของคุณ!" - - ;;chats - :chats "แชท" - :new-group-chat "แชทกลุ่มใหม่" - - ;;discover - :discover "การค้นพบ" - :none "ไม่มี" - :search-tags "พิมพ์แท็กการค้นหาของคุณที่นี่" - :popular-tags "แท็กยอดนิยม" - :recent "เมื่อเร็ว ๆ นี้" - :no-statuses-discovered "ไม่พบสถานะใด ๆ" - - ;;settings - :settings "การตั้งค่า" - - ;;contacts - :contacts "ผู้ติดต่อ" - :new-contact "ผู้ติดต่อใหม่" - :contacts-group-new-chat "เริ่มแชทใหม่" - :no-contacts "ยังไม่มีผู้ติดต่อ" - :show-qr "แสดง QR" - - ;;group-settings - :remove "ลบ" - :save "บันทึก" - :clear-history "ลบประวัติ" - :chat-settings "การตั้งค่าแชท" - :edit "แก้ไข" - :add-members "เพิ่มสมาชิก" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "คำเชิญแชทที่ได้รับ" - :removed-from-chat "ลบคุณออกจากแชทกลุ่ม" - :left "ออกไปแล้ว" - :invited "เชิญแล้ว" - :removed "ลบแล้ว" - :You "คุณ" - - ;;new-contact - :add-new-contact "เพิ่มผู้ติดต่อใหม่" - :scan-qr "สแกน QR" - :name "ชื่อ" - :address-explication "บางที ในที่นี้คุณควรกรอกข้อความสักเล็กน้อยเพื่อแสดงที่อยู่หรือสถานที่ที่จะมองหามันได้" - :contact-already-added "ได้มีการเพิ่มผู้ติดต่อนี้แล้ว" - :can-not-add-yourself "คุณไม่สามารถเพิ่มตัวคุณเอง" - :unknown-address "ที่อยู่ที่ไม่ทราบ" - - ;;login - :connect "เชื่อมต่อ" - :address "ที่อยู่" - :password "รหัสผ่าน" - :wrong-password "รหัสผ่านไม่ถูกต้อง" - - ;;recover - :passphrase "วลีรหัสผ่าน" - :recover "กู้คืน" - - ;;accounts - :recover-access "กู้คืนการเข้าถึง" - - ;;wallet-qr-code - :done "เสร็จสิ้น" - :main-wallet "กระเป๋าเงินหลัก" - - ;;validation - :invalid-phone "หมายเลขโทรศัพท์ไม่ถูกต้อง" - :amount "จำนวน" - ;;transactions - :status "สถานะ" - :recipient "ผู้รับ" - - ;;:webview - :web-view-error "อุ๊ย มีข้อผิดพลาด" - - :confirm "ยืนยัน" - :phone-national "ระดับชาติ" - :public-group-topic "หัวข้อ" - :debug-enabled "มีการเปิดการดำเนินงานของเซิร์ฟเวอร์ที่ได้แก้ไขข้อบกพร่องแล้ว! ตอนนี้คุณสามารถเพิ่ม DApp ของคุณโดยการเปิดดำเนินการ *status-dev-cli scan* จากคอมพิวเตอร์ของคุณ" - :new-public-group-chat "เข้าร่วมแชทสาธารณะ" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "ยกเลิก" - :twelve-words-in-correct-order "12 คำตามลำดับที่ถูกต้อง" - :remove-from-contacts "ลบออกจากรายชื่อติดต่อ" - :delete-chat "ลบแชท" - :edit-chats "แก้ไขแชท" - :sign-in "ลงชื่อเข้าใช้" - :create-new-account "สร้างบัญชีใหม่" - :sign-in-to-status "ลงชื่อเข้าใช้ในสถานะ" - :got-it "เข้าใจแล้ว" - :move-to-internal-failure-message "เราจำเป็นต้องย้ายไฟล์สำคัญบางไฟล์จากที่เก็บข้อมูลภายนอกมาเก็บไว้ในที่เก็บข้อมูลภายใน ในการดำเนินการนี้ เราต้องการการอนุญาตจากคุณ เราจะไม่ใช้ที่เก็บข้อมูลภายนอกสำหรับในเวอร์ชันในอนาคต" - :edit-group "แก้ไขกลุ่ม" - :delete-group "ลบกลุ่ม" - :browsing-title "เบราว์" - :reorder-groups "จัดกลุ่มใหม่" - :browsing-cancel "ยกเลิก" - :faucet-success "ได้รับคำขอไขก๊อกแล้ว" - :choose-from-contacts "เลือกจากรายชื่อติดต่อ" - :new-group "กลุ่มใหม่" - :phone-e164 "ระหว่างประเทศ 1" - :remove-from-group "ลบออกจากกลุ่ม" - :search-contacts "ค้นหารายชื่อติดต่อ" - :transaction "การทำธุรกรรม" - :public-group-status "สาธารณะ" - :leave-chat "ออกจากแชท" - :start-conversation "เริ่มการสนทนา" - :topic-format "รูปแบบไม่ถูกต้อง [a-z0-9\\-]+" - :enter-valid-public-key "โปรดใส่คีย์สาธารณะที่ถูกต้องหรือสแกนรหัส QR" - :faucet-error "คำขอไขก๊อกผิดพลาด" - :phone-significant "มีความสำคัญ" - :search-for "ค้นหา..." - :sharing-copy-to-clipboard "คัดลอกไปยังคลิปบอร์ด" - :phone-international "ระหว่างประเทศ 2" - :enter-address "ใส่ที่อยู่" - :send-transaction "ส่งการทำธุรกรรม" - :delete-contact "ลบรายชื่อติดต่อ" - :mute-notifications "ปิดเสียงการแจ้งเตือน" - - :contact-s {:one "รายชื่อติดต่อ" - :other "รายชื่อติดต่อ"} - :next "ถัดไป" - :from "จาก" - :search-chats "ค้นหาแชท" - :in-contacts "ในรายชื่อติดต่อ" - - :sharing-share "แชร์..." - :type-a-message "พิมพ์ข้อความ..." - :type-a-command "เริ่มพิมพ์คำสั่ง..." - :shake-your-phone "พบข้อบกพร่อง หรือมีข้อเสนอแนะ? เพียง ~เขย่า~ โทรศัพท์ของคุณ!" - :status-prompt "สร้างสถานะเพื่อช่วยให้ผู้คนได้รู้ถึงสิ่งที่คุณกำลังเสนอ คุณสามารถใช้ #hashtags ได้ด้วย" - :add-a-status "เพิ่มสถานะ..." - :error "ข้อผิดพลาด" - :edit-contacts "แก้ไขรายชื่อติดต่อ" - :more "เพิ่มเติม" - :cancel "ยกเลิก" - :no-statuses-found "ไม่พบสถานะ" - :browsing-open-in-web-browser "เปิดในเว็บเบราว์เซอร์" - :delete-group-prompt "การดำเนินการนี้จะไม่ส่งผลต่อรายชื่อติดต่อ" - :edit-profile "แก้ไขโปรไฟล์" - - :empty-topic "หัวข้อว่างเปล่า" - :to "ถึง" - :data "ข้อมูล"}) diff --git a/src/status_im/translations/tr.cljs b/src/status_im/translations/tr.cljs deleted file mode 100644 index fab67a819f8..00000000000 --- a/src/status_im/translations/tr.cljs +++ /dev/null @@ -1,230 +0,0 @@ -(ns status-im.translations.tr) - -(def translations - {;;common - :members-title "Üyeler" - :not-implemented "Uygulanmadı!" - :chat-name "Sohbet adı" - :notifications-title "Bildirimler ve sesler" - :offline "Çevrimdışı" - - ;;drawer - :switch-users "Kullanıcıları değiştir" - - ;;chat - :is-typing "yazıyor" - :and-you "ve siz" - :search-chat "Sohbette ara" - :members {:one "1 üye" - :other "{{count}} üye" - :zero "üye yok"} - :members-active {:one "1 üye, 1 aktif" - :other "{{count}} üye, {{count}} aktif" - :zero "üye yok"} - :active-online "Çevrimiçi" - :active-unknown "Bilinmiyor" - :available "Uygun" - :no-messages "Mesaj yok" - :suggestions-requests "İstekler" - :suggestions-commands "Komutlar" - - ;;sync - :sync-in-progress "Eşitleniyor..." - :sync-synced "Eşitlendi" - - ;;messages - :status-sending "Gönderiliyor..." - :status-pending "Beklemede" - :status-sent "Gönderildi" - :status-seen-by-everyone "Herkes tarafından görüldü" - :status-seen "Görüldü" - :status-delivered "Teslim edildi" - :status-failed "Başarısız" - - ;;datetime - :datetime-second {:one "saniye" - :other "saniye"} - :datetime-minute {:one "dakika" - :other "dakika"} - :datetime-hour {:one "saat" - :other "saat"} - :datetime-day {:one "gün" - :other "gün"} - :datetime-ago "önce" - :datetime-yesterday "dün" - :datetime-today "bugün" - - ;;profile - :profile "Profil" - :message "Mesaj" - :not-specified "Belirtilmemiş" - :public-key "Ortak Anahtar" - :phone-number "Telefon Numarası'" - :add-to-contacts "Kişi listesine ekle" - - ;;make_photo - :image-source-title "Profil Fotoğrafı" - :image-source-make-photo "Çek" - :image-source-gallery "Galeriden seç" - - ;;sign-up - :contacts-syncronized "Kişi listeniz eşitlendi" - :confirmation-code (str "Teşekkürler! Size onay kodunu içeren bir kısa mesaj " - "gönderdim. Telefon numaranızı onaylamak için lütfen bu kodu girin") - :incorrect-code (str "Üzgünüm kod hatalıydı, lütfen yeniden girin") - :phew-here-is-your-passphrase "*Oh* bu oldukça zor oldu işte parolanız, *bu parolayı bir yere not ederek saklayın!* Hesabınızı kurtarmak için bu parolaya ihtiyacınız olacaktır." - :here-is-your-passphrase "İşte parolanız, *bu parolayı bir yere not ederek saklayın!* Hesabınızı kurtarmak için bu parolaya ihtiyacınız olacaktır." - :phone-number-required "Telefon numaranızı girmek için buraya dokunun, arkadaşlarınızı ben bulacağım" - :intro-status "Hesabınızı kurmak ve ayarlarınızı değiştirmek için benimle sohbet edin!" - :intro-message1 "Status'e hoş geldiniz\nŞifrenizi oluşturmak ve hemen başlamak için bu mesaja dokunun!" - :account-generation-message "Bana birkaç saniye ayırın, hesabınızı oluşturmak için biraz matematik hesabı yapmam gerekecek!" - - ;;chats - :chats "Sohbetler" - :new-group-chat "Yeni grup sohbeti" - - ;;discover - :discover "Keşfet" - :none "Hiçbiri" - :search-tags "Aramak istediğiniz etiketleri buraya girin" - :popular-tags "Popüler etiketler" - :recent "Güncel" - :no-statuses-discovered "Herhangi bir durum keşfedilmedi" - - ;;settings - :settings "Ayarlar" - - ;;contacts - :contacts "Kişiler" - :new-contact "Yeni Kişi" - :contacts-group-new-chat "Yeni sohbet başlat" - :no-contacts "Henüz herhangi bir kişi yok" - :show-qr "Kare Kodu göster" - - ;;group-settings - :remove "Kaldır" - :save "Kaydet" - :clear-history "Geçmişi temizle" - :chat-settings "Sohbet ayarları" - :edit "Düzenle" - :add-members "Üye ekle" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "sohbet daveti alındı" - :removed-from-chat "sizi grup sohbetinden sildi" - :left "ayrıldı" - :invited "davet edildi" - :removed "silindi" - :You "Siz" - - ;;new-contact - :add-new-contact "Yeni kişi ekle" - :scan-qr "Kare Kod tara" - :name "İsim" - :address-explication "Burada belki de bir adresin ne olduğu ve bunu bulmak için nereye bakılmasıyla ilgili bazı metinler yer alabilir" - :contact-already-added "Kişi zaten eklendi" - :can-not-add-yourself "Kendinizi ekleyemezsiniz" - :unknown-address "Bilinmeyen adres" - - ;;login - :connect "Bağlan" - :address "Adres" - :password "Şifre" - :wrong-password "Hatalı Şifre" - - ;;recover - :passphrase "Parola" - :recover "Kurtar" - - ;;accounts - :recover-access "Erişimi kurtar" - - ;;wallet-qr-code - :done "Yapıldı" - :main-wallet "Ana Cüzdan" - - ;;validation - :invalid-phone "Geçersiz telefon numarası" - :amount "Miktar" - ;;transactions - :status "Durum" - :recipient "Alıcı" - - ;;:webview - :web-view-error "hoppala, hata" - - :confirm "Onayla" - :phone-national "Ulusal" - :public-group-topic "Başlık" - :debug-enabled "Hata ayıklama sunucusu başlatıldı! Artık şimdi bilgisayarınızdan *status-dev-cli scan* çalıştırarak DApp'inizi ekleyebilirsiniz" - :new-public-group-chat "Herkese açık sohbete katılın" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "İptal Et" - :twelve-words-in-correct-order "Doğru sırayla 12 kelime" - :remove-from-contacts "Kişi listesinden çıkarın" - :delete-chat "Sohbeti silin" - :edit-chats "Sohbetleri düzenleyin" - :sign-in "Oturum açın" - :create-new-account "Yeni hesap açın" - :sign-in-to-status "Durumlar için oturum açın" - :got-it "Anladım" - :move-to-internal-failure-message "Bazı önemli dosyaları harici depolamadan dahili depolamaya taşımamız gerekiyor. Bu nedenle, izniniz gerekli. Gelecek sürümlerde harici depolamayı kullanmayacağız." - :edit-group "Grubu düzenleyin" - :delete-group "Grubu silin" - :browsing-title "Tarayın" - :reorder-groups "Grupları yeniden sıralayın" - :browsing-cancel "İptal Et" - :faucet-success "Vana talebi alındı" - :choose-from-contacts "Kişi listesinden seçin" - :new-group "Yeni grup" - :phone-e164 "Uluslararası 1" - :remove-from-group "Gruptan çıkarın" - :search-contacts "Kişiler listesinde arayın" - :transaction "İşlem" - :public-group-status "Herkese açık" - :leave-chat "Sohbetten çıkın" - :start-conversation "Sohbete başlayın" - :topic-format "Yanlış format [a-z0-9\\-]+" - :enter-valid-public-key "Lütfen geçerli bir ortak anahtar girin veya QR kodu tarayın" - :faucet-error "Musluk talebi hatası" - :phone-significant "Önemli" - :search-for "Şunu ara..." - :sharing-copy-to-clipboard "Panoya kopyalayın" - :phone-international "Uluslararası 2" - :enter-address "Adresi girin" - :send-transaction "İşlem gönderin" - :delete-contact "Kişiyi silin" - :mute-notifications "Bildirimleri kapat" - - :contact-s {:one "kişi" - :other "kişiler"} - :next "Sonraki" - :from "Şuradan:" - :search-chats "Sohbetlerde arayın" - :in-contacts "kişiler listesinde" - - :sharing-share "Paylaşın..." - :type-a-message "Mesaj yazın..." - :type-a-command "Komut girmeye başlayın..." - :shake-your-phone "Hata mı buldunuz ya da öneriniz mi var? Sadece telefonunuzu ~sallamanız~ yeterli!" - :status-prompt "Sunduğunuz şeyler hakkında insanlara yardımcı olması için bir durum oluşturun. #hashtag de kullanabilirsiniz." - :add-a-status "Durum ekleyin..." - :error "Hata" - :edit-contacts "Kişileri düzenleyin" - :more "dahası" - :cancel "İptal Et" - :no-statuses-found "Durum bulunamadı" - :browsing-open-in-web-browser "Web tarayıcısında açın" - :delete-group-prompt "Bu, kişi listesini etkilemeyecektir" - :edit-profile "Profili düzenleyin" - - :empty-topic "Boş başlık" - :to "Gönderilen:" - :data "Veri"}) diff --git a/src/status_im/translations/uk.cljs b/src/status_im/translations/uk.cljs deleted file mode 100644 index 311be84e346..00000000000 --- a/src/status_im/translations/uk.cljs +++ /dev/null @@ -1,363 +0,0 @@ -(ns status-im.translations.uk) - -(def translations - {;;common - :members-title "Учасники" - :not-implemented "!не реалізовано" - :chat-name "Назва чату" - :notifications-title "Сповіщення та звуки" - :offline "Оффлайн" - :search-for "Шукати..." - :cancel "Скасувати" - :next "Далі" - :open "Відкрити" - :description "Опис" - :url "URL-адреса" - :type-a-message "Написати повідомлення..." - :type-a-command "Почніть вводити команду..." - :error "Помилка" - :unknown-status-go-error "Невідома status-go помилка" - :node-unavailable "Немає запущеного ethereum вузла" - :yes "Так" - :no "Ні" - - :camera-access-error "Щоб надати необхідний дозвіл для камери, будь ласка, перейдіть в налаштування системи і переконайтеся, що вибрано Status > Камера." - :photos-access-error "Щоб надати необхідний дозвіл для фото, будь ласка, перейдіть в налаштування системи і переконайтеся, що вибрано Status > Фотографії." - - ;;drawer - :switch-users "Змінити користувачів" - :current-network "Поточна мережа" - - ;;chat - :is-typing "друкує" - :and-you "і ви" - :search-chat "Шукати чат" - :members {:one "1 контакт" - :other "{{count}} контакти" - :zero "немає контактів"} - :members-active {:one "1 контакт" - :other "{{count}} контакти" - :zero "немає контактів"} - :public-group-status "Публічний" - :active-online "Онлайн" - :active-unknown "Невідомо" - :available "Доступно" - :no-messages "Немає повідомлень" - :suggestions-requests "Запити" - :suggestions-commands "Команди" - :faucet-success "Запит Faucet був отриманий" - :faucet-error "Помилка запиту Faucet" - - ;;sync - :sync-in-progress "Синхронізація..." - :sync-synced "Синхронізовано" - - ;;messages - :status-sending "Відправляється..." - :status-pending "В очікуванні" - :status-sent "Відправлено" - :status-seen-by-everyone "Переглянуто усіма" - :status-seen "Переглянуто" - :status-delivered "Доставлено" - :status-failed "Помилка" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "секунда" - :other "секунди"} - :datetime-minute {:one "хвилина" - :other "хвилин"} - :datetime-hour {:one "година" - :other "години"} - :datetime-day {:one "день" - :other "дні"} - :datetime-ago "назад" - :datetime-yesterday "вчора" - :datetime-today "сьогодні" - - ;;profile - :profile "Профіль" - :edit-profile "Редагувати профіль" - :message "Повідомлення" - :not-specified "Не вказано" - :public-key "Публічний ключ" - :phone-number "Номер телефону" - :update-status "Оновіть свій статус..." - :add-a-status "Додати статус..." - :status-prompt "Вкажіть свій статус. Використання #хештегів допоможе іншим дізнатися про вас і розповісти про те, що у вас на думці" - :add-to-contacts "Додати до контактів" - :in-contacts "В контактах" - :remove-from-contacts "Відалити за контактів" - :start-conversation "Почати розмову" - :send-transaction "Відправити транзакцію" - :testnet-text "Ви знаходитесь в {{testnet}} Testnet. Не відправляйте реальний ETH або SNT на свою адресу" - :mainnet-text "Ви в Mainnet. Реальний ETH буде відправлений" - - ;;make_photo - :image-source-title "Зображення профілю" - :image-source-make-photo "Сфотографувати" - :image-source-gallery "Вибрати з галереї" - - ;;sharing - :sharing-copy-to-clipboard "Скопіювати" - :sharing-share "Поділитися..." - :sharing-cancel "Скасувати" - - :browsing-title "Переглядати" - :browsing-open-in-web-browser "Відкрити у веб-браузері" - :browsing-cancel "Скасувати" - - ;;sign-up - :contacts-syncronized "Ваші контакти синхронізовано" - :confirmation-code (str "Дякуємо! Ми вам надіслали СМС з кодом підтвердження." - "Введіть цей код для підтвердження свого номера телефону") - :incorrect-code (str "Вибачте, код неправильний, введіть ще раз") - :phew-here-is-your-passphrase "*Уф*, це було непросто, ось ваша парольна фраза *запишіть її і зберігайте в надійному місці!* Вона буде потрібна вам для відновлення облікового запису." - :here-is-your-passphrase "Ось ваша парольна фраза *запишіть її і зберігайте в надійному місці!* Вона буде потрібна вам для відновлення облікового запису." - :here-is-your-signing-phrase "Ось ваша фраза підпису. Ви будете використовувати її для перевірки достовірності своїх транзакцій. *Запишіть її і зберігайте в безпеці!*" - :phone-number-required "Натисніть сюди для введення номера телефону і я знайду ваших друзів." - :shake-your-phone "Знайдена помилка або є пропозиція? Просто ~потрясіть~ свій телефон!" - :intro-status "Поспілкуйтеся зі мною в чаті, щоб налаштувати свій аккаунт і змінити свої налаштування!" - :intro-message1 "Ласкаво просимо в Status\nнатисніть на це повідомлення, щоб встановити пароль і почати!" - :account-generation-message "Секундочку, мені потрібно виконати шалено складні розрахунки для створення вашого аккаунта!" - :move-to-internal-failure-message "Нам потрібно перемістити деякі важливі файли з зовнішнього сховища у внутрішнє. Для цього нам потрібен ваш дозвіл. В наступних версіях ми не будемо використовувати зовнішнє сховища." - :debug-enabled "Запущений сервер налагоджування! Тепер ви можете виконати * status-dev-cli scan *, щоб знайти сервер з вашого комп'ютера в мережі." - - ;;phone types - :phone-e164 "Міжнародний 1" - :phone-international "Міжнародний 2" - :phone-national "Державний" - :phone-significant "Значний" - - ;;chats - :chats "Чати" - :delete-chat "Видалити чат" - :new-group-chat "Новий груповий чат" - :new-public-group-chat "Приєднатися до публічного чату" - :edit-chats "Редагувати чати" - :search-chats "Пошук чатів" - :empty-topic "Порожня тема" - :topic-format "Неправильний формат [a-z0-9\\-]+" - :public-group-topic "Тема" - - ;;discover - :discover "Пошук" - :none "Жоден" - :search-tags "Введіть теги для пошуку сюди" - :popular-tags "Популярні #хештеги" - :recent "Останні статуси" - :no-statuses-found "Статуси не виявлені" - :chat "Чат" - :all "Всі" - :public-chats "Публічні чати" - :soon "Незабаром" - :public-chat-user-count "{{count}} людей" - :dapps "ÐApps" - :dapp-profile "ÐApp профіль" - :no-statuses-discovered "Статуси не виявлені" - :no-statuses-discovered-body "Коли хтось створить\nстатус, ви побачите його тут." - :no-hashtags-discovered-title "#хештеги не виявлені" - :no-hashtags-discovered-body "Коли #хештег стане\nпопулярним, ви побачите його тут." - - ;;settings - :settings "Налаштування" - - ;;contacts - :contacts "Контакти" - :new-contact "Новий контакт" - :delete-contact "Видалити контакт" - :delete-contact-confirmation "Цей контакт буде видалено з ваших контактів" - :remove-from-group "Видалити з групи" - :edit-contacts "Редагування контактів" - :search-contacts "Пошук контактів" - :contacts-group-new-chat "Новий груповий чат" - :choose-from-contacts "Вибрати з контактів" - :no-contacts "Поки що немає контактів" - :show-qr "Показати QR код" - :enter-address "Ввести адресу" - :more "більше" - - ;;group-settings - :remove "Видалити" - :save "Зберегти" - :delete "Видалити" - :clear-history "Видалити історію" - :mute-notifications "Вимкнути звук в сповіщеннях" - :leave-chat "Вийти з чату" - :chat-settings "Налаштування чату" - :edit "Редагувати" - :add-members "Додати учасників" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "Нова група" - :reorder-groups "Впорядкувати групи" - :edit-group "Редагувати групу" - :delete-group "Видалити групу" - :delete-group-confirmation "Ця група буде видалена з ваших груп. Це не вплине на ваші контакти" - :delete-group-prompt "Це не вплине на ваші контакти" - :contact-s {:one "контакт" - :other "контакти"} - - ;;protocol - :received-invitation "отримав запрошення у чат" - :removed-from-chat "видалив вас з групового чату" - :left "вийшов" - :invited "запрошений" - :removed "видалений" - :You "Ви" - - ;;new-contact - :add-new-contact "Додати новий контакт" - :scan-qr "Сканувати QR код" - :name "Ім'я" - :address-explication "Ваш публічний ключ використовується для створення вашої адреси в Ethereum і являє собою ряд цифр і букв. Ви можете легко знайти його в своєму профілі" - :enter-valid-public-key "Введіть дійсний публічний ключ або скануйте QR код" - :contact-already-added "Контакт вже додано" - :can-not-add-yourself "Ви не можете додати себе" - :unknown-address "Невідома адреса" - - ;;login - :connect "Підключитися" - :address "Адреса" - :password "Пароль" - :sign-in-to-status "Увійти в Status" - :sign-in "Увійти" - :wrong-password "Невірний пароль" - :enter-password "Введіть пароль" - - ;;recover - :passphrase "Парольна фраза" - :recover "Відновити" - :twelve-words-in-correct-order "12 слів в правильному порядку" - - ;;accounts - :recover-access "Відновлення доступу" - :create-new-account "Створити новий обліковий запис" - - ;;wallet-qr-code - :done "Готово" - - ;;validation - :invalid-phone "Неправильний номер телефону" - :amount "Сума" - - ;;transactions - :confirm "Підтвердити" - :transaction "Транзакція" - :unsigned-transaction-expired "Термін дії непідписаної транзакції завершився" - :status "Status" - :recipient "Одержувач" - :to "Кому" - :from "Від" - :data "Дані" - :got-it "Зрозуміло" - :block "Блок" - :hash "Хеш" - :gas-limit "Ліміт Газу" - :gas-price "Ціна Газу" - :gas-used "Використаний Газ" - :cost-fee "Вартість/Комісія" - :nonce "Nonce" - :confirmations "Підтвердження" - :confirmations-helper-text "Будь ласка, зачекайте як мінімум 12-ти підтверджень, щоб переконатися, що транзакція оброблена безпечно" - :copy-transaction-hash "Копіювати хеш транзакції" - :open-on-etherscan "Відкрити посилання на Etherscan.io" - :incoming "Вхідні" - :outgoing "Вихідні" - :pending "В очікуванні" - :postponed "Відкладені" - - ;;webview - :web-view-error "ой, помилка" - - ;;testfairy warning - :testfairy-title "Попередження!" - :testfairy-message "Ви використовуєте додаток, встановлений з нічної збірки. Для цілей тестування ця збірка включає в себе запис сеансу, якщо використовується з'єднання Wi-Fi, так що всі ваші взаємодії з цим додатком зберігаються (як відео і логи) і можуть використовуватися нашою командою розробників для вивчення можливих проблем. Збережені відео / логи не включають в себе ваші паролі. Запис виконується тільки в тому випадку, якщо додаток встановлено з нічного збирання. Нічого не записується, якщо додаток встановлено з PlayStore або TestFlight." - - ;; wallet - :wallet "Гаманець" - :wallets "Гаманці" - :your-wallets "Ваші гаманці" - :main-wallet "Основний Гаманець" - :wallet-error "Помилка при завантаженні даних" - :wallet-send "Відправити" - :wallet-send-token "Відправити {{symbol}}" - :wallet-request "Запросити" - :wallet-exchange "Обміняти" - :wallet-assets "Активи" - :wallet-add-asset "Додати актив" - :wallet-total-value "Загальна вартість" - :wallet-settings "Налаштування гаманця" - :wallet-manage-assets "Управління Активами" - :signing-phrase-description "Підпишіть транзакцію, ввівши свій пароль. Переконайтеся, що слова вище відповідають вашій секретній фразі підпису" - :wallet-insufficient-funds "Недостатньо коштів" - :request-transaction "Запит транзакції" - :send-request "Надіслати запит" - :share "Поділитися" - :eth "ETH" - :currency "Валюта" - :usd-currency "USD" - :transactions "Транзакції" - :transaction-details "Деталі транзакції" - :transaction-failed "Не вдалося виконати транзакцію" - :transactions-sign "Підписати" - :transactions-sign-all "Підписати все" - :transactions-sign-transaction "Підписати транзакцію" - :transactions-sign-later "Підписати пізніше" - :transactions-delete "Видалити транзакцію" - :transactions-delete-content "Транзакція буде видалена зі списку 'Непідписані'" - :transactions-history "Історія" - :transactions-unsigned "Непідписані" - :transactions-history-empty "У вашій історії ще немає транзакцій" - :transactions-unsigned-empty "У вас немає жодних непідписаних транзакцій" - :transactions-filter-title "Фільтри історії" - :transactions-filter-tokens "Токени" - :transactions-filter-type "Тип" - :transactions-filter-select-all "Вибрати все" - :view-transaction-details "Подивитися деталі транзакції" - :transaction-description "Будь ласка, почекайте як мінімум 12 підтверджень, щоб переконатися, що транзакція оброблена безпечно" - :transaction-sent "Транзакція відправлена" - :transaction-moved-text "Транзакція буде знаходитися в списку 'Непідписані' протягом 5 хвилин" - :transaction-moved-title "Транзакція переміщена" - :sign-later-title "Підписати транзакцію пізніше?" - :sign-later-text "Відкрийте історію транзакцій, щоб підписати цю транзакцію" - :not-applicable "Не можна застосувати для непідписаних транзакцій" - - ;; Wallet Send - :wallet-choose-recipient "Виберіть Одержувача" - :wallet-choose-from-contacts "Вибрати з Контактів" - :wallet-address-from-clipboard "Використати Адресу Із Буфера Обміну" - :wallet-invalid-address "Недійсна адреса: \n {{data}}" - :wallet-invalid-chain-id "Мережа не відповідає: \n {{data}}" - :wallet-browse-photos "Переглянути Фотографії" - :validation-amount-invalid-number "Сума недійсна" - :validation-amount-is-too-precise "Занадто багато цифр після десяткової коми. Найменша сума, яку ви можете надіслати це 1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "Нова мережа" - :add-network "Додати мережу" - :add-new-network "Додати нову мережу" - :add-wnode "Додати поштовий сервер" - :existing-networks "Існуючі мережі" - ;; TODO(dmitryn): come up with better description/naming. Suggested namings: Mailbox and Master Node - :existing-wnodes "Існуючі поштові сервери" - :add-json-file "Додати JSON файл" - :paste-json-as-text "Вставити JSON як текст" - :paste-json "Вставити JSON" - :specify-rpc-url "Вкажіть RPC URL-адресу" - :edit-network-config "Редагувати конфігурацію мережі" - :connected "Підключений" - :process-json "Обробити JSON" - :error-processing-json "Помилка обробки JSON" - :rpc-url "RPC URL-адреса" - :remove-network "Видалити мережу" - :network-settings "Налаштування мережі" - :offline-messaging-settings "Параметри оффлайн повідомленнь" - :edit-network-warning "Будьте обережні, зміна налаштувань мережі може зробити її недоступною для вас" - :connecting-requires-login "Підключення до мережі вимагає входу в систему" - :close-app-title "Попередження!" - :close-app-content "Додаток зупиниться і закриється. При повторному відкритті, буде використовуватися обрана мережа" - :close-app-button "Підтвердити"}) diff --git a/src/status_im/translations/ur.cljs b/src/status_im/translations/ur.cljs deleted file mode 100644 index 05737606765..00000000000 --- a/src/status_im/translations/ur.cljs +++ /dev/null @@ -1,230 +0,0 @@ -(ns status-im.translations.ur) - -(def translations - {;;common - :members-title "ممبران" - :not-implemented "نافذ نہیں ہوا!" - :chat-name "چیٹ کا نام" - :notifications-title "نوٹیفیکیشن اور آوازیں" - :offline "آف لائن" - - ;;drawer - :switch-users "صارف تبدیل کریں" - - ;;chat - :is-typing "اندراج کر رہا ہے" - :and-you "اور آپ" - :search-chat "چیٹ تلاش کریں" - :members {:one "1 ممبر" - :other "{{count}} ممبران" - :zero "کوئی ممبر نہیں"} - :members-active {:one "1 ممبر, 1 فعال" - :other "{{count}} ممبران, {count}} فعال" - :zero "کوئی ممبر نہیں"} - :active-online "آن لائن" - :active-unknown "نامعلوم" - :available "دستیاب" - :no-messages "کوئی پیغام نہیں" - :suggestions-requests "درخواستیں" - :suggestions-commands "کمانڈ" - - ;;sync - :sync-in-progress "سنکرونائز ہو رہا ہے" - :sync-synced "دوران سنکرونائز" - - ;;messages - :status-sending "بھیجا جا رہا ہے" - :status-pending "زیر غور" - :status-sent "بھیج دیا گیا" - :status-seen-by-everyone "ہر ایک نے دیکھ لیا ہے" - :status-seen "دیکھ لیا گیا" - :status-delivered "پہنچا دیا گیا" - :status-failed "ناکام ہو گیا" - - ;;datetime - :datetime-second {:one "سیکنڈ" - :other "سیکنڈ"} - :datetime-minute {:one "منٹ" - :other "منٹ"} - :datetime-hour {:one "گھنٹہ" - :other "گھنٹے"} - :datetime-day {:one "دن" - :other "دن"} - :datetime-ago "قبل" - :datetime-yesterday "کل" - :datetime-today "آج" - - ;;profile - :profile "پروفائل" - :message "پیغام" - :not-specified "واضح نہیں کیا گیا" - :public-key "Public Key" - :phone-number "فون نمبر" - :add-to-contacts "اپنے رابطوں میں درج کریں" - - ;;make_photo - :image-source-title "پروفائل تصویر" - :image-source-make-photo "کھینچیں" - :image-source-gallery "گیلری سے چنیں" - - ;;sign-up - :contacts-syncronized "آپ کے کانٹیکٹس سنکرونائز ہو گئے ہیں" - :confirmation-code (str "شکریہ! ہم نے آپ کو تصدیق کا پیغام بھیج دیا ہے " - "کوڈ کے ساتھ۔ برائے مہربانی اپنا نمبر کنفرم کرنے کے لیے وہ کوڈ فراہم کریں۔") - :incorrect-code (str "معذرت، کوڈ غلط تھا، برائے مہربانی دوبارہ درج کریں") - :phew-here-is-your-passphrase "*اف* یہ مشکل تھا, یہ آپ کی passphrase ہے۔, *اسے لکھ لیں اور اپنے پاس محفوظ کر لیں۔* آپ کو اپنا اکاؤنٹ بحال کرنے کے لیے اس کی ضرورت ہو گی۔" - :here-is-your-passphrase "یہ آپ کی passphrase ہے۔, *ا اکاؤنٹ بحال کرنے کے لیے اس کی ضرورت * ہے۔, *اسے لکھ لیں اور اپنے پاس محفوظ کر لیں۔* آپ کو ا" - :phone-number-required "اپنا فون نمبر درج کرنے کے لیے یہاں ٹیپ کریں اور میں آپ کے دوستوں کو تلاش کروں گا" - :intro-status "اپنا اکاؤنٹ سیٹ اپ کرنے اور سیٹنگز تبدیل کرنے کے لیے مجھ سے چیٹ کریں!" - :intro-message1 "سٹیٹس میں خوش آمدید اور اپنا پاسورڈ سیٹ کرنے کے لیے اس کو ٹیپ کریں اور شروعات کریں۔" - :account-generation-message "مجھے ایک سیکنڈ دیں، اپ کا اکاؤنٹ تشکیل دینے کے لیے مجھے کچھ ریاضی سے کام کرنا ہے۔!" - - ;;chats - :chats "چیٹ" - :new-group-chat "نئی گروپ چیٹ" - - ;;discover - :discover "دریافت" - :none "کوئی نہیں" - :search-tags "اپنا تلاش کا ٹیگ یہاں درج کریں" - :popular-tags "مشہور ٹیگ" - :recent "حالیہ" - :no-statuses-discovered "کوئی سٹیٹس نہیں پایا گیا" - - ;;settings - :settings "سیٹنگز" - - ;;contacts - :contacts "رابطے" - :new-contact "نئے رابطے" - :contacts-group-new-chat "نئی چیٹ شروع کریں" - :no-contacts "ابھی یہاں کوئی کنٹیکٹ نہیں" - :show-qr "دکھائیں QR" - - ;;group-settings - :remove "نکالیں" - :save "محفوظ کریں" - :clear-history "تاریخ مٹا دیں" - :chat-settings "چیٹ کی سیٹنگز" - :edit "تصحیح" - :add-members "ممبران شامل کریں" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "چیٹ کا دعوت نامہ وصول ہوا ہے" - :removed-from-chat "گروپ سے نکال دیا ہے" - :left "چھوڑ دیا" - :invited "مدعو" - :removed "نکال دیا گیا" - :You "آپ" - - ;;new-contact - :add-new-contact "نیا کانٹیکٹ شامل کریں" - :scan-qr "سکین کریں QR" - :name "نام" - :address-explication "شائد یہاں آپ کو کچھ لکھنا چاہیے دیکھنے کے لیے کہ پتہ کیا ہے اور اسے کہاں ہونا چاہیے" - :contact-already-added "کانٹیکٹ پہلے سے شامل ہے" - :can-not-add-yourself "آپ خود کو شامل نہیں کر سکتے" - :unknown-address "نامعلوم پتہ" - - ;;login - :connect "ملیں" - :address "پتہ" - :password "پاسورڈ" - :wrong-password "غلط پاسورڈ" - - ;;recover - :passphrase "Passphrase" - :recover "بحال کریں" - - ;;accounts - :recover-access "رسائی بحال کریں" - - ;;wallet-qr-code - :done "ہو گیا" - :main-wallet "مرکزی والٹ" - - ;;validation - :invalid-phone "غلط فون نمبر" - :amount "رقم" - ;;transactions - :status "سٹیٹس" - :recipient "وصول کنندہ" - - ;;:webview - :web-view-error "اوہ، غلطی ہو گئی" - - :confirm "توثیق کریں" - :phone-national "قومی" - :public-group-topic "موضوع" - :debug-enabled "ڈی بگ سرور کو لانچ کیا گیا ہے! آپ کا آئی پتہ ہے۔ اب آپ اپنے کمپیوٹر سے *status-dev-cli scan* کو چلاتے ہوئے اپنا DApp شامل کر سکتے ہیں" - :new-public-group-chat "عوامی چیٹ میں شامل ہوں" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "منسوخ کریں" - :twelve-words-in-correct-order "درست ترتیب میں 12 الفاظ" - :remove-from-contacts "روابط سے خارج کریں" - :delete-chat "چیٹ کو حذف کریں" - :edit-chats "چیٹس میں ترمیم کریں" - :sign-in "سائن اِن کریں" - :create-new-account "نیا اکاؤنٹ تخلیق کریں" - :sign-in-to-status "اسٹیٹس میں سائن اِن کریں" - :got-it "سمجھ گیا" - :move-to-internal-failure-message "ہمیں خارجی اسٹوریج سے داخلی اسٹوریج کی طرف کچھ فائلیں منتقل کرنی ہوگی۔ ایسا کرنے کے لیے، ہمیں آپ کی اجازت درکار ہے۔ ہم مستقبل کے ورژن میں بیرونی اسٹوریج کا استعمال نہیں کریں گے۔" - :edit-group "گروپ میں ترمیم کریں" - :delete-group "گروپ کو حذف کریں" - :browsing-title "براؤز" - :reorder-groups "ریکارڈر کے گروپ" - :browsing-cancel "منسوخ کریں" - :faucet-success "فوسیٹ درخواست موصول ہوئی ہے" - :choose-from-contacts "روابط سے منتخب کریں" - :new-group "نیا گروپ" - :phone-e164 "بین الاقوامی 1" - :remove-from-group "گروپ سے خارج کریں" - :search-contacts "روابط کی تلاش کریں" - :transaction "لین دین" - :public-group-status "عوامی" - :leave-chat "چیٹ سے باہر نکلیں" - :start-conversation "بات چیت شروع کریں" - :topic-format "غلط فارمیٹ [a-z0-9\\-]+" - :enter-valid-public-key "برائے مہربانی ایک درست عوامی کلید داخل کریں یا کوئی کیو آر کوڈ اسکین کریں" - :faucet-error "فاسیٹ درخواست کی خرابی" - :phone-significant "اہم" - :search-for ".... کو تلاش کریں" - :sharing-copy-to-clipboard "کلپ بورڈ پر نقل کیا گیا" - :phone-international "بین الاقوامی 2" - :enter-address "پتہ داخل کریں" - :send-transaction "لین دین بھیجیں" - :delete-contact "رابطہ حذف کریں" - :mute-notifications "اطلاع نامے خاموش کریں" - - :contact-s {:one "رابطہ" - :other "روابط"} - :next "اگلا" - :from "سے" - :search-chats "چیٹس تلاش کریں" - :in-contacts "روابط میں" - - :sharing-share "اشتراک کریں....." - :type-a-message "کوئی پیغام ٹائپ کریں...." - :type-a-command "کمانڈ ٹائپ کرنا شروع کریں..." - :shake-your-phone "کسی بگ کا پتہ لگا یا ذہن میں کوئی مشورہ ہے؟ بس اپنا فون ~ہلائیں~!" - :status-prompt "آپ کے ذریعہ پیشکش کی جانے والی چیزوں کے متعلق لوگوں کو جاننے میں مدد دینے کے لیے کوئی اسٹیٹس تخلیق کریں۔ آپ #ہیشٹیگ بھی استعمال کرسکتے ہیں۔" - :add-a-status "کوئی اسٹیٹس شامل کریں..." - :error "خرابی" - :edit-contacts "روابط کو حذف کریں" - :more "مزید" - :cancel "منسوخ کریں" - :no-statuses-found "کسی اسٹیٹس کا پتہ نہیں چلا" - :browsing-open-in-web-browser "ویب براؤزر میں کھولیں" - :delete-group-prompt "اس سے روابط متاثر نہیں ہونگے" - :edit-profile "پروفائل میں ترمیم کریں" - - :empty-topic "خالی موضوع" - :to "بجانب" - :data "ڈیٹا"}) diff --git a/src/status_im/translations/vi.cljs b/src/status_im/translations/vi.cljs deleted file mode 100644 index a5506b02e25..00000000000 --- a/src/status_im/translations/vi.cljs +++ /dev/null @@ -1,230 +0,0 @@ -(ns status-im.translations.vi) - -(def translations - {;;common - :members-title "Các thành viên" - :not-implemented "!không được thực hiện" - :chat-name "Tên trò chuyện" - :notifications-title "Thông báo và âm thanh" - :offline "Không trực tuyến" - - ;;drawer - :switch-users "Đổi người dùng" - - ;;chat - :is-typing "đang gõ" - :and-you "và bạn" - :search-chat "Tìm kiếm trò chuyện" - :members {:one "1 thành viên" - :other "{{count}} thành viên" - :zero "không thành viên"} - :members-active {:one "1 thành viên, 1 đang hoạt động" - :other "{{count}} thành viên, {{count}} đang hoạt động" - :zero "không thành viên"} - :active-online "Trực tuyến" - :active-unknown "Không rõ" - :available "Sẵn sàng" - :no-messages "Không có tin nhắn" - :suggestions-requests "Các yêu cầu" - :suggestions-commands "Các lệnh" - - ;;sync - :sync-in-progress "Đang đồng bộ..." - :sync-synced "Đồng bộ" - - ;;messages - :status-sending "Đang gửi" - :status-pending "Chưa giải quyết" - :status-sent "Đã gửi" - :status-seen-by-everyone "Được nhìn thấy bởi mọi người" - :status-seen "Đã nhìn thấy " - :status-delivered "Đã gửi" - :status-failed "Đã thất bại" - - ;;datetime - :datetime-second {:one "giây" - :other "giây"} - :datetime-minute {:one "phút" - :other "phút"} - :datetime-hour {:one "giờ" - :other "giờ"} - :datetime-day {:one "ngày" - :other "ngày"} - :datetime-ago "trước đây" - :datetime-yesterday "hôm qua" - :datetime-today "hôm nay" - - ;;profile - :profile "Hồ sơ" - :message "Tin nhắn" - :not-specified "Không được xác định" - :public-key "Khóa công khai" - :phone-number "Số điện thoại" - :add-to-contacts "Thêm vào liên hệ" - - ;;make_photo - :image-source-title "Ảnh đại diện" - :image-source-make-photo "Chụp" - :image-source-gallery "Chọn từ gallery" - - ;;sign-up - :contacts-syncronized "Các liên hệ của bạn đã được đồng bộ hóa" - :confirmation-code (str "Cảm ơn! Chúng tôi đã gửi cho bạn một tin nhắn văn bản với một xác nhận " - "Vui lòng cung cấp mã đó để xác nhận số điện thoại của bạn") - :incorrect-code (str "Xin lỗi mã không chính xác, vui lòng nhập lại") - :phew-here-is-your-passphrase "*Phù* điều đó thật khó khăn, đây là cụm mật khẩu của bạn, *hãy viết cụm này ra và giữ nó an toàn!* Bạn sẽ cần nó để khôi phục tài khoản của bạn." - :here-is-your-passphrase "Đây là cụm mật khẩu của bạn, *hãy viết cụm này ra và giữ nó an toàn!* Bạn sẽ cần nó để khôi phục tài khoản của bạn." - :phone-number-required "Nhấp vào đây để nhập số điện thoại của bạn & tôi sẽ tìm kiếm bạn bè của bạn" - :intro-status "Trò chuyện với tôi để thiết lập tài khoản của bạn và thay đổi các thiết lập của bạn!" - :intro-message1 "Chào mừng đến với trang Trạng thái\nNhấp vào tin nhắn này để thiết lập mật khẩu của bạn & bắt đầu!" - :account-generation-message "Cho tôi một giây, tôi phải làm vài thuật toán điên khùng để khởi tạo tài khoản của bạn!" - - ;;chats - :chats "Trò chuyện" - :new-group-chat "Cuộc trò chuyện theo nhóm mới" - - ;;discover - :discover "Khám phá" - :none "Không" - :search-tags "Gõ các thẻ tìm kiếm của bạn tại đây" - :popular-tags "Các thẻ phổ biến" - :recent "Gần đây" - :no-statuses-discovered "Không có trạng thái được tìm thấy" - - ;;settings - :settings "Các thiết lập" - - ;;contacts - :contacts "Các liên hệ" - :new-contact "Liên hệ mới" - :contacts-group-new-chat "Bắt đầu cuộc trò chuyện mới" - :no-contacts "Chưa có liên hệ" - :show-qr "Hiển thị QR" - - ;;group-settings - :remove "Xóa" - :save "Lưu" - :clear-history "Xóa lịch sử" - :chat-settings "Thiết lập trò chuyện" - :edit "Điều chỉnh" - :add-members "Thêm Thành viên" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - - ;;participants - - ;;protocol - :received-invitation "đã nhận lời mời trò chuyện" - :removed-from-chat "đã xóa bạn khỏi cuộc trò chuyện theo nhóm" - :left "đã rời đi" - :invited "đã mời" - :removed "đã xóa" - :You "Bạn" - - ;;new-contact - :add-new-contact "Thêm liên hệ mới" - :scan-qr "Quét QR" - :name "Tên" - :address-explication "Có lẽ ở đây nên có một vài nội dung để giải thích địa chỉ này là gì và phải tìm nó ở đâu" - :contact-already-added "Liên hệ đã được thêm vào" - :can-not-add-yourself "Bạn không thể tự thêm mình" - :unknown-address "Địa chỉ không xác định" - - ;;login - :connect "Kết nối" - :address "Địa chỉ" - :password "Mật khẩu" - :wrong-password "Mật khẩu sai" - - ;;recover - :passphrase "Cụm mật khẩu" - :recover "Khôi phục" - - ;;accounts - :recover-access "Khôi phục quyền truy cập" - - ;;wallet-qr-code - :done "Đã hoàn thành" - :main-wallet "Ví chính" - - ;;validation - :invalid-phone "Số điện thoại không hợp lệ" - :amount "Số tiền" - ;;transactions - :status "Trạng thái" - :recipient "Người nhận" - - ;;:webview - :web-view-error "Ối, lỗi" - - :confirm "Xác nhận" - :phone-national "Quốc gia" - :public-group-topic "Chủ đề" - :debug-enabled "Máy chủ gỡ lỗi đã được khởi chạy! Bạn có thể thêm DApp bằng cách chạy *status-dev-cli scan* từ máy tính của mình" - :new-public-group-chat "Tham gia cuộc trò chuyện công khai" - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :sharing-cancel "Hủy" - :twelve-words-in-correct-order "12 từ theo đúng thứ tự" - :remove-from-contacts "Xóa khỏi danh bạ" - :delete-chat "Xóa cuộc trò chuyện" - :edit-chats "Chỉnh sửa cuộc trò chuyện" - :sign-in "Đăng nhập" - :create-new-account "Tạo tài khoản mới" - :sign-in-to-status "Đăng nhập vào Trạng thái" - :got-it "Đã hiểu" - :move-to-internal-failure-message "Chúng tôi cần di chuyển một số tập tin quan trọng từ bộ nhớ ngoài sang bộ nhớ trong. Để làm điều này, chúng tôi cần sự cho phép của bạn. Chúng tôi sẽ không sử dụng bộ nhớ ngoài trong các phiên bản trong tương lai." - :edit-group "Chỉnh sửa nhóm" - :delete-group "Xóa nhóm" - :browsing-title "Duyệt" - :reorder-groups "Sắp xếp lại các nhóm" - :browsing-cancel "Hủy" - :faucet-success "Đã nhận được yêu cầu Faucet" - :choose-from-contacts "Chọn từ danh bạ" - :new-group "Nhóm mới" - :phone-e164 "Quốc tế 1" - :remove-from-group "Xóa khỏi nhóm" - :search-contacts "Tìm kiếm danh bạ" - :transaction "Giao dịch" - :public-group-status "Công cộng" - :leave-chat "Rời cuộc trò chuyện" - :start-conversation "Bắt đầu cuộc trò chuyện" - :topic-format "Định dạng sai [a-z0-9 \\ -] +" - :enter-valid-public-key "Vui lòng nhập khoá công khai hợp lệ hoặc quét mã QR" - :faucet-error "Lỗi yêu cầu Faucet" - :phone-significant "Đáng kể" - :search-for "Tìm kiếm..." - :sharing-copy-to-clipboard "Sao chép vào bộ nhớ đệm" - :phone-international "Quốc tế 2" - :enter-address "Nhập địa chỉ" - :send-transaction "Gửi giao dịch" - :delete-contact "Xóa liên lạc" - :mute-notifications "Tắt tiếng thông báo" - - :contact-s {:one "liên hệ" - :other "Danh bạ"} - :next "Tiếp theo" - :from "Từ" - :search-chats "Tìm kiếm các cuộc trò chuyện" - :in-contacts "Trong danh bạ" - - :sharing-share "Chia sẻ..." - :type-a-message "Nhập một tin nhắn..." - :type-a-command "Bắt đầu nhập một lệnh ..." - :shake-your-phone "Tìm thấy lỗi hoặc có đề xuất? Chỉ cần ~lắc~ điện thoại của bạn!" - :status-prompt "Tạo trạng thái để giúp mọi người biết về những điều bạn đang cung cấp. Bạn cũng có thể sử dụng #hashtags." - :add-a-status "Thêm trạng thái ..." - :error "Lỗi" - :edit-contacts "Chỉnh sửa danh bạ" - :more "thêm" - :cancel "Hủy" - :no-statuses-found "Không tìm thấy trạng thái nào" - :browsing-open-in-web-browser "Mở trong trình duyệt web" - :delete-group-prompt "Điều này sẽ không ảnh hưởng đến danh bạ" - :edit-profile "Chỉnh sửa hồ sơ" - - :empty-topic "Chủ đề trống" - :to "Đến" - :data "Dữ liệu"}) diff --git a/src/status_im/translations/zh_hans.cljs b/src/status_im/translations/zh_hans.cljs deleted file mode 100644 index 08441769296..00000000000 --- a/src/status_im/translations/zh_hans.cljs +++ /dev/null @@ -1,352 +0,0 @@ -(ns status-im.translations.zh-hans) - -(def translations - {;;common - :members-title "成员" - :not-implemented "!未实现" - :chat-name "聊天名称" - :notifications-title "通知和声音" - :offline "离线" - :search-for "搜索..." - :cancel "取消" - :next "下一个" - :open "打开" - :description "简介" - :url "链接" - :type-a-message "输入讯息..." - :type-a-command "开始输入指令..." - :error "错误" - :unknown-status-go-error "未知的 status-go 错误" - :node-unavailable "没有运行的以太坊节点" - :yes "是" - :no "否" - - :camera-access-error "要授予所需的摄像机许可,请转到系统设置,并确定选中了 “Status” > “摄像机”。" - :photos-access-error "要授予所需的照片许可,请转到系统设置,并确保选中了 “Status” > “照片”。" - - ;;drawer - :switch-users "切换用户" - :current-network "当前网络" - - ;;chat - :is-typing "正在输入" - :and-you "你" - :search-chat "搜索聊天" - :members {:one "1 个成员" - :other "{{count}} 个成员" - :zero "无成员"} - :members-active {:one "1 个成员" - :other "{{count}} 个成员" - :zero "无成员"} - :public-group-status "公共" - :active-online "在线" - :active-unknown "未知" - :available "可用" - :no-messages "无消息" - :suggestions-requests "请求" - :suggestions-commands "命令" - :faucet-success "水龙头请求已经收到" - :faucet-error "水龙头请求错误" - - ;;sync - :sync-in-progress "正在同步..." - :sync-synced "已同步" - - ;;messages - :status-sending "正在发送..." - :status-pending "等待中" - :status-sent "已发送" - :status-seen-by-everyone "所有人可见" - :status-seen "可见" - :status-delivered "已发送" - :status-failed "失败" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分钟" - :other "分钟"} - :datetime-hour {:one "小时" - :other "小时"} - :datetime-day {:one "天" - :other "天"} - :datetime-ago "前" - :datetime-yesterday "昨天" - :datetime-today "今天" - - ;;profile - :profile "个人资料" - :edit-profile "编辑个人资料" - :message "信息" - :not-specified "未指定" - :public-key "公钥" - :phone-number "电话号码" - :update-status "更新你的状态..." - :add-a-status "添加状态..." - :status-prompt "设置你的 Status。使用 #hashtags 可以帮助其他人了解你,并且谈论你正在想些什么。" - :add-to-contacts "添加到联系人" - :in-contacts "已添加联系人" - :remove-from-contacts "从联系人中删除" - :start-conversation "开始对话" - :send-transaction "发送交易" - :testnet-text "你正在使用{{testnet}}测试网络,请不要向你的地址发送真实的 ETH 或者 SNT。" - :mainnet-text "你正在使用主干网络,可以发送真正的 ETH。" - - ;;make_photo - :image-source-title "个人资料图片" - :image-source-make-photo "拍摄" - :image-source-gallery "从图库中选择" - - ;;sharing - :sharing-copy-to-clipboard "复制到剪贴板" - :sharing-share "分享..." - :sharing-cancel "取消" - - :browsing-title "浏览" - :browsing-open-in-web-browser "在网络浏览器中打开" - :browsing-cancel "取消" - - ;;sign-up - :contacts-syncronized "已同步你的联系人" - :confirmation-code (str "谢谢!我们已经向你发送了一条包含确认代码的短信," - "请提供该代码以确认你的电话号码。") - :incorrect-code (str "抱歉,代码不正确,请重新输入") - :phew-here-is-your-passphrase "嗨,真不容易。这是你的密码,*写下来并好好保管它!* 你需要用它来恢复你的帐户。" - :here-is-your-passphrase "这是你的密码,*写下来并好好保管它!* 你需要用它来恢复你的帐户。" - :here-is-your-signing-phrase "这是你的签名密码,你需要用它来验证你的交易,*写下来并好好保管它!*" - :phone-number-required "点击此处输入你的电话号码,我会找到你的朋友" - :shake-your-phone "发现错误或有建议?只要 ~摇一摇~ 你的手机!" - :intro-status "跟我聊天,以设置你的帐户并更改你的设置!" - :intro-message1 "欢迎来到Status\n点击该消息来设置你的密码并开始!" - :account-generation-message "给我一点时间,我得疯狂地计算一下,才能生成你的帐户!" - :move-to-internal-failure-message "我们需要将一些重要的文件从外部存储移动到内部存储。为此,我们需要你的许可。在之后的版本中,我们将不会使用外部存储。" - :debug-enabled "调试服务器已经推出!现在你可以在自己的计算机上执行 *status-dev-cli scan* 来查找在同一网络上的服务器。" - - ;;phone types - :phone-e164 "国际1" - :phone-international "国际2" - :phone-national "国内" - :phone-significant "重要" - - ;;chats - :chats "聊天" - :delete-chat "刪除聊天" - :new-group-chat "新的群聊" - :new-public-group-chat "加入公共聊天" - :edit-chats "编辑对话" - :search-chats "搜索对话" - :empty-topic "清空话题" - :topic-format "格式错误 [a-z0-9\\-]+" - :public-group-topic "话题" - - ;;discover - :discover "发现" - :none "无" - :search-tags "在此处输入搜索标签" - :popular-tags "热门标签" - :recent "最近" - :no-statuses-found "未发现状态" - :chat "聊天" - :all "全部" - :public-chats "公共聊天" - :soon "不久" - :public-chat-user-count "{{count}}人" - :dapps "ÐApps" - :dapp-profile "ÐApp简介" - :no-statuses-discovered "未发现状态" - :no-statuses-discovered-body "当某个人发布\n一条状态时,你可以在这里看到它。" - :no-hashtags-discovered-title "未发现 #hashtags" - :no-hashtags-discovered-body "当一个 #hashtag 变为流行标签,你可以在这里看到它。" - - ;;settings - :settings "设置" - - ;;contacts - :contacts "联系人" - :new-contact "新的联系人" - :delete-contact "删除联系人" - :delete-contact-confirmation "此联系人将从你的联系人中删除" - :remove-from-group "从群组中移除" - :edit-contacts "编辑联系人" - :search-contacts "搜索联系人" - :contacts-group-new-chat "开始新的聊天" - :choose-from-contacts "从联系人中选择" - :no-contacts "还没有联系人" - :show-qr "显示二维码" - :enter-address "输入地址" - :more "更多" - - ;;group-settings - :remove "删除" - :save "保存" - :delete "刪除" - :clear-history "清空历史记录" - :mute-notifications "静音通知" - :leave-chat "离开对话" - :chat-settings "对话设置" - :edit "编辑" - :add-members "添加成员" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新增群组" - :reorder-groups "重新排序群组" - :edit-group "编辑群组" - :delete-group "删除群组" - :delete-group-confirmation "此群组将从你的群组中移除,这不会影响你的联系人。" - :delete-group-prompt "这不会影响你的联系人" - :contact-s {:one "联系人" - :other "联系人"} - - ;;protocol - :received-invitation "收到的聊天邀请" - :removed-from-chat "已将你从群聊中删除" - :left "剩余" - :invited "已邀请" - :removed "已删除" - :You "你" - - ;;new-contact - :add-new-contact "添加新的联系人" - :scan-qr "扫描二维码" - :name "名称" - :address-explication "你的公钥可以用来生成以太坊地址,由一系列数字和字母组成,可以在个人简介中轻松地找到。" - :enter-valid-public-key "请输入一个有效的公钥或扫描二维码" - :contact-already-added "已添加该联系人" - :can-not-add-yourself "不能添加自己" - :unknown-address "未知地址" - - ;;login - :connect "连接" - :address "地址" - :password "密码" - :sign-in-to-status "登录 Status" - :sign-in "登录" - :wrong-password "密码错误" - :enter-password "输入密码" - - ;;recover - :passphrase "密码" - :recover "恢复" - :twelve-words-in-correct-order "正确排序的12个单词" - - ;;accounts - :recover-access "恢复访问" - :create-new-account "创建新账户" - - ;;wallet-qr-code - :done "完成" - - ;;validation - :invalid-phone "无效的电话号码" - :amount "金额" - - ;;transactions - :confirm "确认" - :transaction "交易" - :unsigned-transaction-expired "未签名的交易过期" - :status "状态" - :recipient "接收方" - :to "到" - :from "从" - :data "数据" - :got-it "知道了" - :block "块" - :hash "哈希" - :gas-limit "Gas 限制" - :gas-price "Gas 价格" - :gas-used "使用的 Gas" - :cost-fee "成本/费用" - :nonce "随机数" - :confirmations "确认" - :confirmations-helper-text "请等待达到至少12个确认,来确保交易已被安全地处理。" - :copy-transaction-hash "复制交易哈希" - :open-on-etherscan "打开 Etherscan.io" - - ;;webview - :web-view-error "糟糕,出错了" - - ;;testfairy warning - :testfairy-title "警告!" - :testfairy-message "你正在使用一个从 nightly build 上安装的应用。为了测试目的,在使用 WIFI 连接情况下,本构建包含了会话记录,即和本应用交互的所有记录(如视频和日志)将被保存,并且可能被我们的开发团队用于调查可能存在的问题。保存的视频、日志不包含你的密码。只有从 nightly build 下载安装本应用时,才会进行记录。如果本应用是从 PlayStore 或者 TestFlight 上下载安装,则不会记录。" - - ;; wallet - :wallet "钱包" - :wallets "钱包" - :your-wallets "你的钱包" - :main-wallet "主钱包" - :wallet-error "加载数据错误" - :wallet-send "发送" - :wallet-request "请求" - :wallet-exchange "交易" - :wallet-assets "资产" - :wallet-add-asset "添加资产" - :wallet-total-value "总值" - :wallet-settings "钱包设置" - :signing-phrase-description "通过输入密码对交易进行签名,请确保输入的词语和你的签名密码相匹配" - :wallet-insufficient-funds "资金不足" - :request-transaction "请求交易" - :send-request "发送请求" - :share "分享" - :eth "ETH" - :currency "货币" - :usd-currency "USD" - :transactions "交易" - :transaction-details "交易明细" - :transaction-failed "交易失败" - :transactions-sign "签名" - :transactions-sign-all "签名所有" - :transactions-sign-transaction "签名交易" - :transactions-sign-later "稍后签名" - :transactions-delete "删除交易" - :transactions-delete-content "交易将从“未签名”列表中移除" - :transactions-history "历史记录" - :transactions-unsigned "未签名" - :transactions-history-empty "暂无交易历史记录" - :transactions-unsigned-empty "你没有任何未签名的交易" - :transactions-filter-title "过滤历史记录" - :transactions-filter-tokens "代币" - :transactions-filter-type "类型" - :transactions-filter-select-all "全部选中" - :view-transaction-details "查看交易明细" - :transaction-description "请等待达到至少12个确认,来确保交易已被安全地处理。" - :transaction-sent "发送的交易" - :transaction-moved-text "本交易将在接下来的5分钟保持在“未签名”列表中" - :transaction-moved-title "移动的交易" - :sign-later-title "稍后对交易进行签名?" - :sign-later-text "检查交易历史记录,可以对本次交易进行签名" - :not-applicable "对未签名交易不可用" - - ;; Wallet Send - :wallet-choose-recipient "选择接收方" - :wallet-choose-from-contacts "从联系人中选择" - :wallet-address-from-clipboard "使用剪贴板中的地址" - :wallet-invalid-address "无效的地址: \n {{data}}" - :wallet-browse-photos "浏览相册" - :validation-amount-invalid-number "金额不是一个有效的数字" - :validation-amount-is-too-precise "金额过于精确,所能发送的最小单位是1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "新建网络" - :add-network "添加网络" - :add-new-network "添加新网络" - :existing-networks "已有的网络" - :add-json-file "添加一个 JSON 文件" - :paste-json-as-text "粘贴 JSON 为文本" - :paste-json "粘贴 JSON" - :specify-rpc-url "指定一个 RPC 链接" - :edit-network-config "编辑网络配置" - :connected "已连接" - :process-json "处理 JSON" - :error-processing-json "处理 JSON 错误" - :rpc-url "RPC 链接" - :remove-network "移除网络" - :network-settings "网络设置" - :edit-network-warning "请小心,编辑网络数据可能导致当前网络不可用" - :connecting-requires-login "连接其它网络需要登录" - :close-app-title "警告!" - :close-app-content "应用将会停止并关闭。当你重新打开应用,选中的网络将会被使用" - :close-app-button "确认"}) diff --git a/src/status_im/translations/zh_hant.cljs b/src/status_im/translations/zh_hant.cljs deleted file mode 100644 index 7ec4bf2b4ac..00000000000 --- a/src/status_im/translations/zh_hant.cljs +++ /dev/null @@ -1,352 +0,0 @@ -(ns status-im.translations.zh-hant) - -(def translations - {;;common - :members-title "成員" - :not-implemented "!未實現" - :chat-name "聊天名稱" - :notifications-title "通知和聲音" - :offline "離線" - :search-for "搜索..." - :cancel "取消" - :next "下一個" - :open "打開" - :description "簡介" - :url "鏈接" - :type-a-message "輸入訊息..." - :type-a-command "開始輸入指令..." - :error "錯誤" - :unknown-status-go-error "未知的 status-go 錯誤" - :node-unavailable "沒有運行的以太坊節點" - :yes "是" - :no "否" - - :camera-access-error "要授予所需的攝像機許可,請轉到系統設置,並確定選中了 “Status” > “攝像機”。" - :photos-access-error "要授予所需的照片許可,請轉到系統設置,並確保選中了 “Status” > “照片”。" - - ;;drawer - :switch-users "切換用户" - :current-network "當前網絡" - - ;;chat - :is-typing "正在輸入" - :and-you "你" - :search-chat "搜索聊天" - :members {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :members-active {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :public-group-status "公共" - :active-online "在線" - :active-unknown "未知" - :available "可用" - :no-messages "無消息" - :suggestions-requests "請求" - :suggestions-commands "命令" - :faucet-success "水龍頭請求已經收到" - :faucet-error "水龍頭請求錯誤" - - ;;sync - :sync-in-progress "正在同步..." - :sync-synced "已同步" - - ;;messages - :status-sending "正在發送..." - :status-pending "等待中" - :status-sent "已發送" - :status-seen-by-everyone "所有人可見" - :status-seen "可見" - :status-delivered "已發送" - :status-failed "失敗" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分鐘" - :other "分鐘"} - :datetime-hour {:one "小時" - :other "小時"} - :datetime-day {:one "天" - :other "天"} - :datetime-ago "前" - :datetime-yesterday "昨天" - :datetime-today "今天" - - ;;profile - :profile "個人資料" - :edit-profile "編輯個人資料" - :message "信息" - :not-specified "未指定" - :public-key "公鑰" - :phone-number "電話號碼" - :update-status "更新你的狀態..." - :add-a-status "添加狀態..." - :status-prompt "設置你的 Status。使用 #hastags 可以幫助其他人瞭解你,並且談論你正在想些什麼。" - :add-to-contacts "添加到聯繫人" - :in-contacts "已添加聯繫人" - :remove-from-contacts "從聯繫人中刪除" - :start-conversation "開始對話" - :send-transaction "發送交易" - :testnet-text "你正在使用 {{testnet}} 測試網絡,請不要向你的地址發送真實的 ETH 或者 SNT。" - :mainnet-text "你正在使用主幹網絡,可以發送真正的 ETH。" - - ;;make_photo - :image-source-title "個人資料圖片" - :image-source-make-photo "拍攝" - :image-source-gallery "從圖庫中選擇" - - ;;sharing - :sharing-copy-to-clipboard "複製到剪貼板" - :sharing-share "分享..." - :sharing-cancel "取消" - - :browsing-title "瀏覽" - :browsing-open-in-web-browser "在網絡瀏覽器中打開" - :browsing-cancel "取消" - - ;;sign-up - :contacts-syncronized "已同步你的聯繫人" - :confirmation-code (str "謝謝!我們已經向你發送了一條包含確認代碼的短信," - "請提供該代碼以確認你的電話號碼。") - :incorrect-code (str "抱歉,代碼不正確,請重新輸入") - :phew-here-is-your-passphrase "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。" - :here-is-your-passphrase "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。" - :here-is-your-signing-phrase "這是你的簽名密碼,你需要用它來驗證你的交易,*寫下來並好好保管它!*" - :phone-number-required "點擊此處輸入你的電話號碼,我會找到你的朋友" - :shake-your-phone "發現錯誤或有建議?只要 ~搖一搖~ 你的手機!" - :intro-status "跟我聊天,以設置你的帳户並更改你的設置!" - :intro-message1 "歡迎來到 Status\n點擊該消息來設置你的密碼並開始!" - :account-generation-message "給我一點時間,我得瘋狂地計算一下,才能生成你的帳户!" - :move-to-internal-failure-message "我們需要將一些重要的文件從外部存儲移動到內部存儲。為此,我們需要你的許可。在之後的版本中,我們將不會使用外部存儲。" - :debug-enabled "調試服務器已經推出!現在你可以在自己的計算機上執行 *status-dev-cli scan* 來查找在同一網絡上的服務器。" - - ;;phone types - :phone-e164 "國際1" - :phone-international "國際2" - :phone-national "國內" - :phone-significant "重要" - - ;;chats - :chats "聊天" - :delete-chat "刪除聊天" - :new-group-chat "新的羣聊" - :new-public-group-chat "加入公共聊天" - :edit-chats "編輯對話" - :search-chats "搜索對話" - :empty-topic "清空話題" - :topic-format "格式錯誤 [a-z0-9\\-]+" - :public-group-topic "話題" - - ;;discover - :discover "發現" - :none "無" - :search-tags "在此處輸入搜索標籤" - :popular-tags "熱門標籤" - :recent "最近" - :no-statuses-found "未發現狀態" - :chat "聊天" - :all "全部" - :public-chats "公共聊天" - :soon "不久" - :public-chat-user-count "{{count}} 人" - :dapps "ÐApps" - :dapp-profile "ÐApp 簡介" - :no-statuses-discovered "未發現狀態" - :no-statuses-discovered-body "當某個人發佈\n一條狀態時,你可以在這裏看到它。" - :no-hashtags-discovered-title "未發現 #hashtags" - :no-hashtags-discovered-body "當一個 #hashtag 變為流行標籤,你可以在這裏看到它。" - - ;;settings - :settings "設置" - - ;;contacts - :contacts "聯繫人" - :new-contact "新的聯繫人" - :delete-contact "刪除聯繫人" - :delete-contact-confirmation "此聯繫人將從你的聯繫人中刪除" - :remove-from-group "從羣組中移除" - :edit-contacts "編輯聯繫人" - :search-contacts "搜索聯繫人" - :contacts-group-new-chat "開始新的聊天" - :choose-from-contacts "從聯繫人中選擇" - :no-contacts "還沒有聯繫人" - :show-qr "顯示二維碼" - :enter-address "輸入地址" - :more "更多" - - ;;group-settings - :remove "刪除" - :save "保存" - :delete "刪除" - :clear-history "清空歷史記錄" - :mute-notifications "靜音通知" - :leave-chat "離開對話" - :chat-settings "對話設置" - :edit "編輯" - :add-members "添加成員" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新增羣組" - :reorder-groups "重新排序羣組" - :edit-group "編輯羣組" - :delete-group "刪除羣組" - :delete-group-confirmation "此羣組將從你的羣組中移除,這不會影響你的聯繫人。" - :delete-group-prompt "這不會影響你的聯繫人" - :contact-s {:one "聯繫人" - :other "聯繫人"} - - ;;protocol - :received-invitation "收到的聊天邀請" - :removed-from-chat "已將你從羣聊中刪除" - :left "剩餘" - :invited "已邀請" - :removed "已刪除" - :You "你" - - ;;new-contact - :add-new-contact "添加新的聯繫人" - :scan-qr "掃描二維碼" - :name "名稱" - :address-explication "你的公鑰可以用來生成以太坊地址,由一系列數字和字母組成,可以在個人簡介中輕鬆地找到。" - :enter-valid-public-key "請輸入一個有效的公鑰或掃描二維碼" - :contact-already-added "已添加該聯繫人" - :can-not-add-yourself "不能添加自己" - :unknown-address "未知地址" - - ;;login - :connect "連接" - :address "地址" - :password "密碼" - :sign-in-to-status "登錄 Status" - :sign-in "登錄" - :wrong-password "密碼錯誤" - :enter-password "輸入密碼" - - ;;recover - :passphrase "密碼" - :recover "恢復" - :twelve-words-in-correct-order "正確排序的12個單詞" - - ;;accounts - :recover-access "恢復訪問" - :create-new-account "創建新賬户" - - ;;wallet-qr-code - :done "完成" - - ;;validation - :invalid-phone "無效的電話號碼" - :amount "金額" - - ;;transactions - :confirm "確認" - :transaction "交易" - :unsigned-transaction-expired "未簽名的交易過期" - :status "狀態" - :recipient "接收方" - :to "到" - :from "從" - :data "數據" - :got-it "知道了" - :block "塊" - :hash "哈希" - :gas-limit "Gas限制" - :gas-price "Gas價格" - :gas-used "使用的Gas" - :cost-fee "成本/費用" - :nonce "隨機數" - :confirmations "確認" - :confirmations-helper-text "請等待達到至少12個確認,來確保交易已被安全地處理。" - :copy-transaction-hash "複製交易哈希" - :open-on-etherscan "打開 Etherscan.io" - - ;;webview - :web-view-error "糟糕,出錯了" - - ;;testfairy warning - :testfairy-title "警告!" - :testfairy-message "你正在使用一個從 nightly build 上安裝的應用。為了測試目的,在使用 WIFI 連接情況下,本構建包含了會話記錄,即和本應用交互的所有記錄(如視頻和日誌)將被保存,並且可能被我們的開發團隊用於調查可能存在的問題。保存的視頻、日誌不包含你的密碼。只有從 nightly build 下載安裝本應用時,才會進行記錄。如果本應用是從 PlayStore 或者 TestFlight 上下載安裝,則不會記錄。" - - ;; wallet - :wallet "錢包" - :wallets "錢包" - :your-wallets "你的錢包" - :main-wallet "主錢包" - :wallet-error "加載數據錯誤" - :wallet-send "發送" - :wallet-request "請求" - :wallet-exchange "交易" - :wallet-assets "資產" - :wallet-add-asset "添加資產" - :wallet-total-value "總值" - :wallet-settings "錢包設置" - :signing-phrase-description "通過輸入密碼對交易進行簽名,請確保輸入的詞語和你的簽名密碼相匹配" - :wallet-insufficient-funds "資金不足" - :request-transaction "請求交易" - :send-request "發送請求" - :share "分享" - :eth "ETH" - :currency "貨幣" - :usd-currency "USD" - :transactions "交易" - :transaction-details "交易明細" - :transaction-failed "交易失敗" - :transactions-sign "簽名" - :transactions-sign-all "簽名所有" - :transactions-sign-transaction "簽名交易" - :transactions-sign-later "稍後簽名" - :transactions-delete "刪除交易" - :transactions-delete-content "交易將從“未簽名”列表中移除" - :transactions-history "歷史記錄" - :transactions-unsigned "未簽名" - :transactions-history-empty "暫無交易歷史記錄" - :transactions-unsigned-empty "你沒有任何未簽名的交易" - :transactions-filter-title "過濾歷史記錄" - :transactions-filter-tokens "代幣" - :transactions-filter-type "類型" - :transactions-filter-select-all "全部選中" - :view-transaction-details "查看交易明細" - :transaction-description "請等待達到至少12個確認,來確保交易已被安全地處理。" - :transaction-sent "發送的交易" - :transaction-moved-text "本交易將在接下來的5分鐘保持在“未簽名”列表中" - :transaction-moved-title "移動的交易" - :sign-later-title "稍後對交易進行簽名?" - :sign-later-text "檢查交易歷史記錄,可以對本次交易進行簽名" - :not-applicable "對未簽名交易不可用" - - ;; Wallet Send - :wallet-choose-recipient "選擇接收方" - :wallet-choose-from-contacts "從聯繫人中選擇" - :wallet-address-from-clipboard "使用剪貼板中的地址" - :wallet-invalid-address "無效的地址: \n {{data}}" - :wallet-browse-photos "瀏覽相冊" - :validation-amount-invalid-number "金額不是一個有效的數字" - :validation-amount-is-too-precise "金額過於精確,所能發送的最小單位是1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "新建網絡" - :add-network "添加網絡" - :add-new-network "添加新網絡" - :existing-networks "已有的網絡" - :add-json-file "添加一個 JSON 文件" - :paste-json-as-text "粘貼 JSON 為文本" - :paste-json "粘貼 JSON" - :specify-rpc-url "指定一個 RPC 鏈接" - :edit-network-config "編輯網絡配置" - :connected "已連接" - :process-json "處理 JSON" - :error-processing-json "處理 JSON 錯誤" - :rpc-url "RPC 鏈接" - :remove-network "移除網絡" - :network-settings "網絡設置" - :edit-network-warning "請小心,編輯網絡數據可能導致當前網絡不可用" - :connecting-requires-login "連接其它網絡需要登錄" - :close-app-title "警告!" - :close-app-content "應用將會停止並關閉。當你重新打開應用,選中的網絡將會被使用" - :close-app-button "確認"}) diff --git a/src/status_im/translations/zh_hant_hk.cljs b/src/status_im/translations/zh_hant_hk.cljs deleted file mode 100644 index c0156fcafd0..00000000000 --- a/src/status_im/translations/zh_hant_hk.cljs +++ /dev/null @@ -1,352 +0,0 @@ -(ns status-im.translations.zh-hant-hk) - -(def translations - {;;common - :members-title "成員" - :not-implemented "!未實現" - :chat-name "聊天名稱" - :notifications-title "通知和聲音" - :offline "離線" - :search-for "搜索..." - :cancel "取消" - :next "下一個" - :open "打開" - :description "簡介" - :url "鏈接" - :type-a-message "輸入訊息..." - :type-a-command "開始輸入指令..." - :error "錯誤" - :unknown-status-go-error "未知的 status-go 錯誤" - :node-unavailable "沒有運行的以太坊節點" - :yes "是" - :no "否" - - :camera-access-error "要授予所需的攝像機許可,請轉到系統設置,並確定選中了 “Status” > “攝像機”。" - :photos-access-error "要授予所需的照片許可,請轉到系統設置,並確保選中了 “Status” > “照片”。" - - ;;drawer - :switch-users "切換用户" - :current-network "當前網絡" - - ;;chat - :is-typing "正在輸入" - :and-you "你" - :search-chat "搜索聊天" - :members {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :members-active {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :public-group-status "公共" - :active-online "在線" - :active-unknown "未知" - :available "可用" - :no-messages "無消息" - :suggestions-requests "請求" - :suggestions-commands "命令" - :faucet-success "水龍頭請求已經收到" - :faucet-error "水龍頭請求錯誤" - - ;;sync - :sync-in-progress "正在同步..." - :sync-synced "已同步" - - ;;messages - :status-sending "正在發送..." - :status-pending "等待中" - :status-sent "已發送" - :status-seen-by-everyone "所有人可見" - :status-seen "可見" - :status-delivered "已發送" - :status-failed "失敗" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分鐘" - :other "分鐘"} - :datetime-hour {:one "小時" - :other "小時"} - :datetime-day {:one "天" - :other "天"} - :datetime-ago "前" - :datetime-yesterday "昨天" - :datetime-today "今天" - - ;;profile - :profile "個人資料" - :edit-profile "編輯個人資料" - :message "信息" - :not-specified "未指定" - :public-key "公鑰" - :phone-number "電話號碼" - :update-status "更新你的狀態..." - :add-a-status "添加狀態..." - :status-prompt "設置你的 Status。使用 #hastags 可以幫助其他人瞭解你,並且談論你正在想些什麼。" - :add-to-contacts "添加到聯繫人" - :in-contacts "已添加聯繫人" - :remove-from-contacts "從聯繫人中刪除" - :start-conversation "開始對話" - :send-transaction "發送交易" - :testnet-text "你正在使用 {{testnet}} 測試網絡,請不要向你的地址發送真實的 ETH 或者 SNT。" - :mainnet-text "你正在使用主幹網絡,可以發送真正的 ETH。" - - ;;make_photo - :image-source-title "個人資料圖片" - :image-source-make-photo "拍攝" - :image-source-gallery "從圖庫中選擇" - - ;;sharing - :sharing-copy-to-clipboard "複製到剪貼板" - :sharing-share "分享..." - :sharing-cancel "取消" - - :browsing-title "瀏覽" - :browsing-open-in-web-browser "在網絡瀏覽器中打開" - :browsing-cancel "取消" - - ;;sign-up - :contacts-syncronized "已同步你的聯繫人" - :confirmation-code (str "謝謝!我們已經向你發送了一條包含確認代碼的短信," - "請提供該代碼以確認你的電話號碼。") - :incorrect-code (str "抱歉,代碼不正確,請重新輸入") - :phew-here-is-your-passphrase "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。" - :here-is-your-passphrase "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。" - :here-is-your-signing-phrase "這是你的簽名密碼,你需要用它來驗證你的交易,*寫下來並好好保管它!*" - :phone-number-required "點擊此處輸入你的電話號碼,我會找到你的朋友" - :shake-your-phone "發現錯誤或有建議?只要 ~搖一搖~ 你的手機!" - :intro-status "跟我聊天,以設置你的帳户並更改你的設置!" - :intro-message1 "歡迎來到 Status\n點擊該消息來設置你的密碼並開始!" - :account-generation-message "給我一點時間,我得瘋狂地計算一下,才能生成你的帳户!" - :move-to-internal-failure-message "我們需要將一些重要的文件從外部存儲移動到內部存儲。為此,我們需要你的許可。在之後的版本中,我們將不會使用外部存儲。" - :debug-enabled "調試服務器已經推出!現在你可以在自己的計算機上執行 *status-dev-cli scan* 來查找在同一網絡上的服務器。" - - ;;phone types - :phone-e164 "國際1" - :phone-international "國際2" - :phone-national "國內" - :phone-significant "重要" - - ;;chats - :chats "聊天" - :delete-chat "刪除聊天" - :new-group-chat "新的羣聊" - :new-public-group-chat "加入公共聊天" - :edit-chats "編輯對話" - :search-chats "搜索對話" - :empty-topic "清空話題" - :topic-format "格式錯誤 [a-z0-9\\-]+" - :public-group-topic "話題" - - ;;discover - :discover "發現" - :none "無" - :search-tags "在此處輸入搜索標籤" - :popular-tags "熱門標籤" - :recent "最近" - :no-statuses-found "未發現狀態" - :chat "聊天" - :all "全部" - :public-chats "公共聊天" - :soon "不久" - :public-chat-user-count "{{count}} 人" - :dapps "ÐApps" - :dapp-profile "ÐApp 簡介" - :no-statuses-discovered "未發現狀態" - :no-statuses-discovered-body "當某個人發佈\n一條狀態時,你可以在這裏看到它。" - :no-hashtags-discovered-title "未發現 #hashtags" - :no-hashtags-discovered-body "當一個 #hashtag 變為流行標籤,你可以在這裏看到它。" - - ;;settings - :settings "設置" - - ;;contacts - :contacts "聯繫人" - :new-contact "新的聯繫人" - :delete-contact "刪除聯繫人" - :delete-contact-confirmation "此聯繫人將從你的聯繫人中刪除" - :remove-from-group "從羣組中移除" - :edit-contacts "編輯聯繫人" - :search-contacts "搜索聯繫人" - :contacts-group-new-chat "開始新的聊天" - :choose-from-contacts "從聯繫人中選擇" - :no-contacts "還沒有聯繫人" - :show-qr "顯示二維碼" - :enter-address "輸入地址" - :more "更多" - - ;;group-settings - :remove "刪除" - :save "保存" - :delete "刪除" - :clear-history "清空歷史記錄" - :mute-notifications "靜音通知" - :leave-chat "離開對話" - :chat-settings "對話設置" - :edit "編輯" - :add-members "添加成員" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新增羣組" - :reorder-groups "重新排序羣組" - :edit-group "編輯羣組" - :delete-group "刪除羣組" - :delete-group-confirmation "此羣組將從你的羣組中移除,這不會影響你的聯繫人。" - :delete-group-prompt "這不會影響你的聯繫人" - :contact-s {:one "聯繫人" - :other "聯繫人"} - - ;;protocol - :received-invitation "收到的聊天邀請" - :removed-from-chat "已將你從羣聊中刪除" - :left "剩餘" - :invited "已邀請" - :removed "已刪除" - :You "你" - - ;;new-contact - :add-new-contact "添加新的聯繫人" - :scan-qr "掃描二維碼" - :name "名稱" - :address-explication "你的公鑰可以用來生成以太坊地址,由一系列數字和字母組成,可以在個人簡介中輕鬆地找到。" - :enter-valid-public-key "請輸入一個有效的公鑰或掃描二維碼" - :contact-already-added "已添加該聯繫人" - :can-not-add-yourself "不能添加自己" - :unknown-address "未知地址" - - ;;login - :connect "連接" - :address "地址" - :password "密碼" - :sign-in-to-status "登錄 Status" - :sign-in "登錄" - :wrong-password "密碼錯誤" - :enter-password "輸入密碼" - - ;;recover - :passphrase "密碼" - :recover "恢復" - :twelve-words-in-correct-order "正確排序的12個單詞" - - ;;accounts - :recover-access "恢復訪問" - :create-new-account "創建新賬户" - - ;;wallet-qr-code - :done "完成" - - ;;validation - :invalid-phone "無效的電話號碼" - :amount "金額" - - ;;transactions - :confirm "確認" - :transaction "交易" - :unsigned-transaction-expired "未簽名的交易過期" - :status "狀態" - :recipient "接收方" - :to "到" - :from "從" - :data "數據" - :got-it "知道了" - :block "塊" - :hash "哈希" - :gas-limit "Gas限制" - :gas-price "Gas價格" - :gas-used "使用的Gas" - :cost-fee "成本/費用" - :nonce "隨機數" - :confirmations "確認" - :confirmations-helper-text "請等待達到至少12個確認,來確保交易已被安全地處理。" - :copy-transaction-hash "複製交易哈希" - :open-on-etherscan "打開 Etherscan.io" - - ;;webview - :web-view-error "糟糕,出錯了" - - ;;testfairy warning - :testfairy-title "警告!" - :testfairy-message "你正在使用一個從 nightly build 上安裝的應用。為了測試目的,在使用 WIFI 連接情況下,本構建包含了會話記錄,即和本應用交互的所有記錄(如視頻和日誌)將被保存,並且可能被我們的開發團隊用於調查可能存在的問題。保存的視頻、日誌不包含你的密碼。只有從 nightly build 下載安裝本應用時,才會進行記錄。如果本應用是從 PlayStore 或者 TestFlight 上下載安裝,則不會記錄。" - - ;; wallet - :wallet "錢包" - :wallets "錢包" - :your-wallets "你的錢包" - :main-wallet "主錢包" - :wallet-error "加載數據錯誤" - :wallet-send "發送" - :wallet-request "請求" - :wallet-exchange "交易" - :wallet-assets "資產" - :wallet-add-asset "添加資產" - :wallet-total-value "總值" - :wallet-settings "錢包設置" - :signing-phrase-description "通過輸入密碼對交易進行簽名,請確保輸入的詞語和你的簽名密碼相匹配" - :wallet-insufficient-funds "資金不足" - :request-transaction "請求交易" - :send-request "發送請求" - :share "分享" - :eth "ETH" - :currency "貨幣" - :usd-currency "USD" - :transactions "交易" - :transaction-details "交易明細" - :transaction-failed "交易失敗" - :transactions-sign "簽名" - :transactions-sign-all "簽名所有" - :transactions-sign-transaction "簽名交易" - :transactions-sign-later "稍後簽名" - :transactions-delete "刪除交易" - :transactions-delete-content "交易將從“未簽名”列表中移除" - :transactions-history "歷史記錄" - :transactions-unsigned "未簽名" - :transactions-history-empty "暫無交易歷史記錄" - :transactions-unsigned-empty "你沒有任何未簽名的交易" - :transactions-filter-title "過濾歷史記錄" - :transactions-filter-tokens "代幣" - :transactions-filter-type "類型" - :transactions-filter-select-all "全部選中" - :view-transaction-details "查看交易明細" - :transaction-description "請等待達到至少12個確認,來確保交易已被安全地處理。" - :transaction-sent "發送的交易" - :transaction-moved-text "本交易將在接下來的5分鐘保持在“未簽名”列表中" - :transaction-moved-title "移動的交易" - :sign-later-title "稍後對交易進行簽名?" - :sign-later-text "檢查交易歷史記錄,可以對本次交易進行簽名" - :not-applicable "對未簽名交易不可用" - - ;; Wallet Send - :wallet-choose-recipient "選擇接收方" - :wallet-choose-from-contacts "從聯繫人中選擇" - :wallet-address-from-clipboard "使用剪貼板中的地址" - :wallet-invalid-address "無效的地址: \n {{data}}" - :wallet-browse-photos "瀏覽相冊" - :validation-amount-invalid-number "金額不是一個有效的數字" - :validation-amount-is-too-precise "金額過於精確,所能發送的最小單位是1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "新建網絡" - :add-network "添加網絡" - :add-new-network "添加新網絡" - :existing-networks "已有的網絡" - :add-json-file "添加一個 JSON 文件" - :paste-json-as-text "粘貼 JSON 為文本" - :paste-json "粘貼 JSON" - :specify-rpc-url "指定一個 RPC 鏈接" - :edit-network-config "編輯網絡配置" - :connected "已連接" - :process-json "處理 JSON" - :error-processing-json "處理 JSON 錯誤" - :rpc-url "RPC 鏈接" - :remove-network "移除網絡" - :network-settings "網絡設置" - :edit-network-warning "請小心,編輯網絡數據可能導致當前網絡不可用" - :connecting-requires-login "連接其它網絡需要登錄" - :close-app-title "警告!" - :close-app-content "應用將會停止並關閉。當你重新打開應用,選中的網絡將會被使用" - :close-app-button "確認"}) diff --git a/src/status_im/translations/zh_hant_sg.cljs b/src/status_im/translations/zh_hant_sg.cljs deleted file mode 100644 index 1ab9af97ee4..00000000000 --- a/src/status_im/translations/zh_hant_sg.cljs +++ /dev/null @@ -1,352 +0,0 @@ -(ns status-im.translations.zh-hant-sg) - -(def translations - {;;common - :members-title "成員" - :not-implemented "!未實現" - :chat-name "聊天名稱" - :notifications-title "通知和聲音" - :offline "離線" - :search-for "搜尋..." - :cancel "取消" - :next "下一個" - :open "開啟" - :description "簡介" - :url "連結" - :type-a-message "輸入訊息..." - :type-a-command "開始輸入指令..." - :error "錯誤" - :unknown-status-go-error "未知的 status-go 錯誤" - :node-unavailable "沒有可用的以太坊節點" - :yes "是" - :no "否" - - :camera-access-error "要授予所需的攝像機許可,請轉到系統設定,並確定選中了 “Status” > “攝像機”。" - :photos-access-error "要授予所需的照片許可,請轉到系統設定,並確保選中了 “Status” > “照片”。" - - ;;drawer - :switch-users "切換使用者" - :current-network "當前網路" - - ;;chat - :is-typing "正在輸入" - :and-you "你" - :search-chat "搜尋聊天" - :members {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :members-active {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :public-group-status "公共" - :active-online "線上" - :active-unknown "未知" - :available "可用" - :no-messages "無訊息" - :suggestions-requests "請求" - :suggestions-commands "命令" - :faucet-success "水龍頭請求已經收到" - :faucet-error "水龍頭請求錯誤" - - ;;sync - :sync-in-progress "正在同步..." - :sync-synced "已同步" - - ;;messages - :status-sending "正在傳送..." - :status-pending "等待中" - :status-sent "已傳送" - :status-seen-by-everyone "所有人可見" - :status-seen "可見" - :status-delivered "已傳送" - :status-failed "失敗" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分鐘" - :other "分鐘"} - :datetime-hour {:one "小時" - :other "小時"} - :datetime-day {:one "天" - :other "天"} - :datetime-ago "前" - :datetime-yesterday "昨天" - :datetime-today "今天" - - ;;profile - :profile "個人資料" - :edit-profile "編輯個人資料" - :message "資訊" - :not-specified "未指定" - :public-key "公鑰" - :phone-number "電話號碼" - :update-status "更新你的狀態..." - :add-a-status "新增狀態..." - :status-prompt "設定你的 Status。使用 #hastags 可以幫助其他人瞭解你,並且談論你正在想些什麼。" - :add-to-contacts "新增到聯絡人" - :in-contacts "已新增聯絡人" - :remove-from-contacts "從聯絡人中刪除" - :start-conversation "開始對話" - :send-transaction "傳送交易" - :testnet-text "你正在使用 {{testnet}} 測試網路,請不要向你的地址傳送真實的 ETH 或者 SNT。" - :mainnet-text "你正在使用主幹網路,可以傳送真正的 ETH。" - - ;;make_photo - :image-source-title "個人資料圖片" - :image-source-make-photo "拍攝" - :image-source-gallery "從相簿中選擇" - - ;;sharing - :sharing-copy-to-clipboard "複製到剪貼簿" - :sharing-share "分享..." - :sharing-cancel "取消" - - :browsing-title "瀏覽" - :browsing-open-in-web-browser "在網路瀏覽器中開啟" - :browsing-cancel "取消" - - ;;sign-up - :contacts-syncronized "已同步你的聯絡人" - :confirmation-code (str "謝謝!我們已經向你傳送了一條包含確認程式碼的簡訊," - "請提供該程式碼以確認你的電話號碼。") - :incorrect-code (str "抱歉,程式碼不正確,請重新輸入") - :phew-here-is-your-passphrase "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。" - :here-is-your-passphrase "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。" - :here-is-your-signing-phrase "這是你的簽名密碼,你需要用它來驗證你的交易,*寫下來並好好保管它!*" - :phone-number-required "點選此處輸入你的電話號碼,我會找到你的朋友" - :shake-your-phone "發現錯誤或有建議?僅需 ~搖一搖~ 你的手機!" - :intro-status "跟我聊天,以設定你的帳戶並更改你的設定!" - :intro-message1 "歡迎來到 Status\n點選該訊息來設定你的密碼並開始!" - :account-generation-message "給我一點時間,我得瘋狂地計算一下,才能生成你的帳戶!" - :move-to-internal-failure-message "我們需要將一些重要的檔案從外部儲存移動到內部儲存。為此,我們需要你的許可。在之後的版本中,我們將不會使用外部儲存。" - :debug-enabled "除錯伺服器已經推出!現在你可以在自己的計算機上執行 *status-dev-cli scan* 來查詢在同一網路上的伺服器。" - - ;;phone types - :phone-e164 "國際1" - :phone-international "國際2" - :phone-national "國內" - :phone-significant "重要" - - ;;chats - :chats "聊天" - :delete-chat "刪除聊天" - :new-group-chat "新的群聊" - :new-public-group-chat "加入公共聊天" - :edit-chats "編輯對話" - :search-chats "搜尋對話" - :empty-topic "清空話題" - :topic-format "格式錯誤 [a-z0-9\\-]+" - :public-group-topic "話題" - - ;;discover - :discover "發現" - :none "無" - :search-tags "在此處輸入搜尋標籤" - :popular-tags "熱門標籤" - :recent "最近" - :no-statuses-found "未發現狀態" - :chat "聊天" - :all "全部" - :public-chats "公共聊天" - :soon "不久" - :public-chat-user-count "{{count}} 人" - :dapps "ÐApps" - :dapp-profile "ÐApp 簡介" - :no-statuses-discovered "未發現狀態" - :no-statuses-discovered-body "當某個人釋出\n一條狀態時,你可以在這裡看到它。" - :no-hashtags-discovered-title "未發現 #hashtags" - :no-hashtags-discovered-body "當一個 #hashtag 變為流行標籤,你可以在這裡看到它。" - - ;;settings - :settings "設定" - - ;;contacts - :contacts "聯絡人" - :new-contact "新的聯絡人" - :delete-contact "刪除聯絡人" - :delete-contact-confirmation "此聯絡人將從你的聯絡人中刪除" - :remove-from-group "從群組中移除" - :edit-contacts "編輯聯絡人" - :search-contacts "搜尋聯絡人" - :contacts-group-new-chat "開始新的聊天" - :choose-from-contacts "從聯絡人中選擇" - :no-contacts "還沒有聯絡人" - :show-qr "顯示二維碼" - :enter-address "輸入地址" - :more "更多" - - ;;group-settings - :remove "刪除" - :save "儲存" - :delete "刪除" - :clear-history "清空歷史記錄" - :mute-notifications "靜音通知" - :leave-chat "離開對話" - :chat-settings "對話設定" - :edit "編輯" - :add-members "新增成員" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新增群組" - :reorder-groups "重新排序群組" - :edit-group "編輯群組" - :delete-group "刪除群組" - :delete-group-confirmation "此群組將從你的群組中移除,這不會影響你的聯絡人。" - :delete-group-prompt "這不會影響你的聯絡人" - :contact-s {:one "聯絡人" - :other "聯絡人"} - - ;;protocol - :received-invitation "收到的聊天邀請" - :removed-from-chat "已將你從群聊中刪除" - :left "剩餘" - :invited "已邀請" - :removed "已刪除" - :You "你" - - ;;new-contact - :add-new-contact "新增新的聯絡人" - :scan-qr "掃描二維碼" - :name "名稱" - :address-explication "你的公鑰可以用來生成以太坊地址,由一系列數字和字母組成,可以在個人簡介中輕鬆地找到。" - :enter-valid-public-key "請輸入一個有效的公鑰或掃描二維碼" - :contact-already-added "已新增該聯絡人" - :can-not-add-yourself "不能新增自己" - :unknown-address "未知地址" - - ;;login - :connect "連線" - :address "地址" - :password "密碼" - :sign-in-to-status "登入 Status" - :sign-in "登入" - :wrong-password "密碼錯誤" - :enter-password "輸入密碼" - - ;;recover - :passphrase "密碼" - :recover "恢復" - :twelve-words-in-correct-order "正確排序的12個單詞" - - ;;accounts - :recover-access "恢復訪問" - :create-new-account "建立新賬戶" - - ;;wallet-qr-code - :done "完成" - - ;;validation - :invalid-phone "無效的電話號碼" - :amount "金額" - - ;;transactions - :confirm "確認" - :transaction "交易" - :unsigned-transaction-expired "未簽名的交易過期" - :status "狀態" - :recipient "接收方" - :to "到" - :from "從" - :data "資料" - :got-it "知道了" - :block "塊" - :hash "雜湊" - :gas-limit "Gas 限制" - :gas-price "Gas 價格" - :gas-used "使用的 Gas" - :cost-fee "成本/費用" - :nonce "隨機數" - :confirmations "確認" - :confirmations-helper-text "請等待達到至少12個確認,來確保交易已被安全地處理。" - :copy-transaction-hash "複製交易雜湊" - :open-on-etherscan "開啟 Etherscan.io" - - ;;webview - :web-view-error "糟糕,出錯了" - - ;;testfairy warning - :testfairy-title "警告!" - :testfairy-message "你正在使用一個從 nightly build 上安裝的應用。為了測試目的,在使用 WIFI 連線情況下,本構建包含了會話記錄,即和本應用互動的所有記錄(如視訊和日誌)將被儲存,並且可能被我們的開發團隊用於調查可能存在的問題。儲存的視訊、日誌不包含你的密碼。只有從 nightly build 下載安裝本應用時,才會進行記錄。如果本應用是從 PlayStore 或者 TestFlight 上下載安裝,則不會記錄。" - - ;; wallet - :wallet "錢包" - :wallets "錢包" - :your-wallets "你的錢包" - :main-wallet "主錢包" - :wallet-error "載入資料錯誤" - :wallet-send "傳送" - :wallet-request "請求" - :wallet-exchange "交易" - :wallet-assets "資產" - :wallet-add-asset "新增資產" - :wallet-total-value "總值" - :wallet-settings "錢包設定" - :signing-phrase-description "通過輸入密碼對交易進行簽名,請確保輸入的詞語和你的簽名密碼相匹配" - :wallet-insufficient-funds "資金不足" - :request-transaction "請求交易" - :send-request "傳送請求" - :share "分享" - :eth "ETH" - :currency "貨幣" - :usd-currency "USD" - :transactions "交易" - :transaction-details "交易明細" - :transaction-failed "交易失敗" - :transactions-sign "簽名" - :transactions-sign-all "簽名所有" - :transactions-sign-transaction "簽名交易" - :transactions-sign-later "稍後簽名" - :transactions-delete "刪除交易" - :transactions-delete-content "交易將從“未簽名”列表中移除" - :transactions-history "歷史記錄" - :transactions-unsigned "未簽名" - :transactions-history-empty "暫無交易歷史記錄" - :transactions-unsigned-empty "你沒有任何未簽名的交易" - :transactions-filter-title "過濾歷史記錄" - :transactions-filter-tokens "代幣" - :transactions-filter-type "型別" - :transactions-filter-select-all "全部選中" - :view-transaction-details "檢視交易明細" - :transaction-description "請等待達到至少12個確認,來確保交易已被安全地處理。" - :transaction-sent "傳送的交易" - :transaction-moved-text "本交易將在接下來的5分鐘保持在“未簽名”列表中" - :transaction-moved-title "移動的交易" - :sign-later-title "稍後對交易進行簽名?" - :sign-later-text "檢查交易歷史記錄,可以對本次交易進行簽名" - :not-applicable "對未簽名交易不可用" - - ;; Wallet Send - :wallet-choose-recipient "選擇接收方" - :wallet-choose-from-contacts "從聯絡人中選擇" - :wallet-address-from-clipboard "使用剪貼簿中的地址" - :wallet-invalid-address "無效的地址: \n {{data}}" - :wallet-browse-photos "瀏覽相簿" - :validation-amount-invalid-number "金額不是一個有效的數字" - :validation-amount-is-too-precise "金額過於精確,所能傳送的最小單位是1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "新建網路" - :add-network "新增網路" - :add-new-network "新增新網路" - :existing-networks "已有的網路" - :add-json-file "新增一個 JSON 檔案" - :paste-json-as-text "貼上 JSON 為文字" - :paste-json "貼上 JSON" - :specify-rpc-url "指定一個 RPC 連結" - :edit-network-config "編輯網路配置" - :connected "已連線" - :process-json "處理 JSON" - :error-processing-json "處理 JSON 錯誤" - :rpc-url "RPC 連結" - :remove-network "移除網路" - :network-settings "網路設定" - :edit-network-warning "請小心,編輯網路資料可能導致當前網路不可用" - :connecting-requires-login "連線其它網路需要登入" - :close-app-title "警告!" - :close-app-content "應用將會停止並關閉。當你重新開啟應用,選中的網路將會被使用" - :close-app-button "確認"}) diff --git a/src/status_im/translations/zh_hant_tw.cljs b/src/status_im/translations/zh_hant_tw.cljs deleted file mode 100644 index a0b13faeb99..00000000000 --- a/src/status_im/translations/zh_hant_tw.cljs +++ /dev/null @@ -1,352 +0,0 @@ -(ns status-im.translations.zh-hant-tw) - -(def translations - {;;common - :members-title "成員" - :not-implemented "!未實現" - :chat-name "聊天名稱" - :notifications-title "通知和聲音" - :offline "離線" - :search-for "搜尋..." - :cancel "取消" - :next "下一個" - :open "開啟" - :description "簡介" - :url "連結" - :type-a-message "輸入訊息..." - :type-a-command "開始輸入指令..." - :error "錯誤" - :unknown-status-go-error "未知的 status-go 錯誤" - :node-unavailable "沒有可用的以太坊節點" - :yes "是" - :no "否" - - :camera-access-error "要授予所需的攝像機許可,請轉到系統設定,並確定選中了 “Status” > “攝像機”。" - :photos-access-error "要授予所需的照片許可,請轉到系統設定,並確保選中了 “Status” > “照片”。" - - ;;drawer - :switch-users "切換使用者" - :current-network "當前網路" - - ;;chat - :is-typing "正在輸入" - :and-you "你" - :search-chat "搜尋聊天" - :members {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :members-active {:one "1 個成員" - :other "{{count}} 個成員" - :zero "無成員"} - :public-group-status "公共" - :active-online "線上" - :active-unknown "未知" - :available "可用" - :no-messages "無訊息" - :suggestions-requests "請求" - :suggestions-commands "命令" - :faucet-success "水龍頭請求已經收到" - :faucet-error "水龍頭請求錯誤" - - ;;sync - :sync-in-progress "正在同步..." - :sync-synced "已同步" - - ;;messages - :status-sending "正在傳送..." - :status-pending "等待中" - :status-sent "已傳送" - :status-seen-by-everyone "所有人可見" - :status-seen "可見" - :status-delivered "已傳送" - :status-failed "失敗" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分鐘" - :other "分鐘"} - :datetime-hour {:one "小時" - :other "小時"} - :datetime-day {:one "天" - :other "天"} - :datetime-ago "前" - :datetime-yesterday "昨天" - :datetime-today "今天" - - ;;profile - :profile "個人資料" - :edit-profile "編輯個人資料" - :message "資訊" - :not-specified "未指定" - :public-key "公鑰" - :phone-number "電話號碼" - :update-status "更新你的狀態..." - :add-a-status "新增狀態..." - :status-prompt "設定你的 Status。使用 #hastags 可以幫助其他人瞭解你,並且談論你正在想些什麼。" - :add-to-contacts "新增到聯絡人" - :in-contacts "已新增聯絡人" - :remove-from-contacts "從聯絡人中刪除" - :start-conversation "開始對話" - :send-transaction "傳送交易" - :testnet-text "你正在使用 {{testnet}} 測試網路,請不要向你的地址傳送真實的 ETH 或者 SNT。" - :mainnet-text "你正在使用主幹網路,可以傳送真正的 ETH。" - - ;;make_photo - :image-source-title "個人資料圖片" - :image-source-make-photo "拍攝" - :image-source-gallery "從相簿中選擇" - - ;;sharing - :sharing-copy-to-clipboard "複製到剪貼簿" - :sharing-share "分享..." - :sharing-cancel "取消" - - :browsing-title "瀏覽" - :browsing-open-in-web-browser "在網路瀏覽器中開啟" - :browsing-cancel "取消" - - ;;sign-up - :contacts-syncronized "已同步你的聯絡人" - :confirmation-code (str "謝謝!我們已經向你傳送了一條包含確認程式碼的簡訊," - "請提供該程式碼以確認你的電話號碼。") - :incorrect-code (str "抱歉,程式碼不正確,請重新輸入") - :phew-here-is-your-passphrase "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。" - :here-is-your-passphrase "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。" - :here-is-your-signing-phrase "這是你的簽名密碼,你需要用它來驗證你的交易,*寫下來並好好保管它!*" - :phone-number-required "點選此處輸入你的電話號碼,我會找到你的朋友" - :shake-your-phone "發現錯誤或有建議?僅需 ~搖一搖~ 你的手機!" - :intro-status "跟我聊天,以設定你的帳戶並更改你的設定!" - :intro-message1 "歡迎來到 Status\n點選該訊息來設定你的密碼並開始!" - :account-generation-message "給我一點時間,我得瘋狂地計算一下,才能生成你的帳戶!" - :move-to-internal-failure-message "我們需要將一些重要的檔案從外部儲存移動到內部儲存。為此,我們需要你的許可。在之後的版本中,我們將不會使用外部儲存。" - :debug-enabled "除錯伺服器已經推出!現在你可以在自己的計算機上執行 *status-dev-cli scan* 來查詢在同一網路上的伺服器。" - - ;;phone types - :phone-e164 "國際1" - :phone-international "國際2" - :phone-national "國內" - :phone-significant "重要" - - ;;chats - :chats "聊天" - :delete-chat "刪除聊天" - :new-group-chat "新的群聊" - :new-public-group-chat "加入公共聊天" - :edit-chats "編輯對話" - :search-chats "搜尋對話" - :empty-topic "清空話題" - :topic-format "格式錯誤 [a-z0-9\\-]+" - :public-group-topic "話題" - - ;;discover - :discover "發現" - :none "無" - :search-tags "在此處輸入搜尋標籤" - :popular-tags "熱門標籤" - :recent "最近" - :no-statuses-found "未發現狀態" - :chat "聊天" - :all "全部" - :public-chats "公共聊天" - :soon "不久" - :public-chat-user-count "{{count}} 人" - :dapps "ÐApps" - :dapp-profile "ÐApp 簡介" - :no-statuses-discovered "未發現狀態" - :no-statuses-discovered-body "當某個人釋出\n一條狀態時,你可以在這裡看到它。" - :no-hashtags-discovered-title "未發現 #hashtags" - :no-hashtags-discovered-body "當一個 #hashtag 變為流行標籤,你可以在這裡看到它。" - - ;;settings - :settings "設定" - - ;;contacts - :contacts "聯絡人" - :new-contact "新的聯絡人" - :delete-contact "刪除聯絡人" - :delete-contact-confirmation "此聯絡人將從你的聯絡人中刪除" - :remove-from-group "從群組中移除" - :edit-contacts "編輯聯絡人" - :search-contacts "搜尋聯絡人" - :contacts-group-new-chat "開始新的聊天" - :choose-from-contacts "從聯絡人中選擇" - :no-contacts "還沒有聯絡人" - :show-qr "顯示二維碼" - :enter-address "輸入地址" - :more "更多" - - ;;group-settings - :remove "刪除" - :save "儲存" - :delete "刪除" - :clear-history "清空歷史記錄" - :mute-notifications "靜音通知" - :leave-chat "離開對話" - :chat-settings "對話設定" - :edit "編輯" - :add-members "新增成員" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新增群組" - :reorder-groups "重新排序群組" - :edit-group "編輯群組" - :delete-group "刪除群組" - :delete-group-confirmation "此群組將從你的群組中移除,這不會影響你的聯絡人。" - :delete-group-prompt "這不會影響你的聯絡人" - :contact-s {:one "聯絡人" - :other "聯絡人"} - - ;;protocol - :received-invitation "收到的聊天邀請" - :removed-from-chat "已將你從群聊中刪除" - :left "剩餘" - :invited "已邀請" - :removed "已刪除" - :You "你" - - ;;new-contact - :add-new-contact "新增新的聯絡人" - :scan-qr "掃描二維碼" - :name "名稱" - :address-explication "你的公鑰可以用來生成以太坊地址,由一系列數字和字母組成,可以在個人簡介中輕鬆地找到。" - :enter-valid-public-key "請輸入一個有效的公鑰或掃描二維碼" - :contact-already-added "已新增該聯絡人" - :can-not-add-yourself "不能新增自己" - :unknown-address "未知地址" - - ;;login - :connect "連線" - :address "地址" - :password "密碼" - :sign-in-to-status "登入 Status" - :sign-in "登入" - :wrong-password "密碼錯誤" - :enter-password "輸入密碼" - - ;;recover - :passphrase "密碼" - :recover "恢復" - :twelve-words-in-correct-order "正確排序的12個單詞" - - ;;accounts - :recover-access "恢復訪問" - :create-new-account "建立新賬戶" - - ;;wallet-qr-code - :done "完成" - - ;;validation - :invalid-phone "無效的電話號碼" - :amount "金額" - - ;;transactions - :confirm "確認" - :transaction "交易" - :unsigned-transaction-expired "未簽名的交易過期" - :status "狀態" - :recipient "接收方" - :to "到" - :from "從" - :data "資料" - :got-it "知道了" - :block "塊" - :hash "雜湊" - :gas-limit "Gas 限制" - :gas-price "Gas 價格" - :gas-used "使用的 Gas" - :cost-fee "成本/費用" - :nonce "隨機數" - :confirmations "確認" - :confirmations-helper-text "請等待達到至少12個確認,來確保交易已被安全地處理。" - :copy-transaction-hash "複製交易雜湊" - :open-on-etherscan "開啟 Etherscan.io" - - ;;webview - :web-view-error "糟糕,出錯了" - - ;;testfairy warning - :testfairy-title "警告!" - :testfairy-message "你正在使用一個從 nightly build 上安裝的應用。為了測試目的,在使用 WIFI 連線情況下,本構建包含了會話記錄,即和本應用互動的所有記錄(如視訊和日誌)將被儲存,並且可能被我們的開發團隊用於調查可能存在的問題。儲存的視訊、日誌不包含你的密碼。只有從 nightly build 下載安裝本應用時,才會進行記錄。如果本應用是從 PlayStore 或者 TestFlight 上下載安裝,則不會記錄。" - - ;; wallet - :wallet "錢包" - :wallets "錢包" - :your-wallets "你的錢包" - :main-wallet "主錢包" - :wallet-error "載入資料錯誤" - :wallet-send "傳送" - :wallet-request "請求" - :wallet-exchange "交易" - :wallet-assets "資產" - :wallet-add-asset "新增資產" - :wallet-total-value "總值" - :wallet-settings "錢包設定" - :signing-phrase-description "通過輸入密碼對交易進行簽名,請確保輸入的詞語和你的簽名密碼相匹配" - :wallet-insufficient-funds "資金不足" - :request-transaction "請求交易" - :send-request "傳送請求" - :share "分享" - :eth "ETH" - :currency "貨幣" - :usd-currency "USD" - :transactions "交易" - :transaction-details "交易明細" - :transaction-failed "交易失敗" - :transactions-sign "簽名" - :transactions-sign-all "簽名所有" - :transactions-sign-transaction "簽名交易" - :transactions-sign-later "稍後簽名" - :transactions-delete "刪除交易" - :transactions-delete-content "交易將從“未簽名”列表中移除" - :transactions-history "歷史記錄" - :transactions-unsigned "未簽名" - :transactions-history-empty "暫無交易歷史記錄" - :transactions-unsigned-empty "你沒有任何未簽名的交易" - :transactions-filter-title "過濾歷史記錄" - :transactions-filter-tokens "代幣" - :transactions-filter-type "型別" - :transactions-filter-select-all "全部選中" - :view-transaction-details "檢視交易明細" - :transaction-description "請等待達到至少12個確認,來確保交易已被安全地處理。" - :transaction-sent "傳送的交易" - :transaction-moved-text "本交易將在接下來的5分鐘保持在“未簽名”列表中" - :transaction-moved-title "移動的交易" - :sign-later-title "稍後對交易進行簽名?" - :sign-later-text "檢查交易歷史記錄,可以對本次交易進行簽名" - :not-applicable "對未簽名交易不可用" - - ;; Wallet Send - :wallet-choose-recipient "選擇接收方" - :wallet-choose-from-contacts "從聯絡人中選擇" - :wallet-address-from-clipboard "使用剪貼簿中的地址" - :wallet-invalid-address "無效的地址: \n {{data}}" - :wallet-browse-photos "瀏覽相簿" - :validation-amount-invalid-number "金額不是一個有效的數字" - :validation-amount-is-too-precise "金額過於精確,所能傳送的最小單位是1 Wei (1x10^-18 ETH)" - - ;; network settings - :new-network "新建網路" - :add-network "新增網路" - :add-new-network "新增新網路" - :existing-networks "已有的網路" - :add-json-file "新增一個 JSON 檔案" - :paste-json-as-text "貼上 JSON 為文字" - :paste-json "貼上 JSON" - :specify-rpc-url "指定一個 RPC 連結" - :edit-network-config "編輯網路配置" - :connected "已連線" - :process-json "處理 JSON" - :error-processing-json "處理 JSON 錯誤" - :rpc-url "RPC 連結" - :remove-network "移除網路" - :network-settings "網路設定" - :edit-network-warning "請小心,編輯網路資料可能導致當前網路不可用" - :connecting-requires-login "連線其它網路需要登入" - :close-app-title "警告!" - :close-app-content "應用將會停止並關閉。當你重新開啟應用,選中的網路將會被使用" - :close-app-button "確認"}) diff --git a/src/status_im/translations/zh_wuu.cljs b/src/status_im/translations/zh_wuu.cljs deleted file mode 100644 index a4ae5d67a2c..00000000000 --- a/src/status_im/translations/zh_wuu.cljs +++ /dev/null @@ -1,238 +0,0 @@ -(ns status-im.translations.zh-wuu) - -(def translations - {;;common - :members-title "会员" - :not-implemented "!未实现" - :chat-name "聊天名称" - :notifications-title "通知和声音" - :offline "离线" - :search-for "搜索..." - :cancel "取消" - :next "下一个" - :type-a-message "输入讯息..." - :type-a-command "开始输入指令..." - :error "错误" - - :camera-access-error "要授予所需的摄像机许可,请转到系统设置,并确定选择了“状态”>“摄像机”。" - :photos-access-error "要授予所需的照片许可,请转到系统设置,并确保选择“状态“>“照片“。" - - ;;drawer - :switch-users "切换用户" - :current-network "当前网络" - - ;;chat - :is-typing "正在输入" - :and-you "和您" - :search-chat "搜索聊天" - :members {:one "1人" - :other "{{count}}会员" - :zero "无会员"} - :members-active {:one "1个会员,1个活跃成员" - :other "{{count}} 会员, {{count}} 活跃成员" - :zero "无会员"} - :public-group-status "公共" - :active-online "在线" - :active-unknown "未知" - :available "可用" - :no-messages "无信息" - :suggestions-requests "请求" - :suggestions-commands "命令" - :faucet-success "水龙头请求已经收到" - :faucet-error "水龙头请求错误" - - ;;sync - :sync-in-progress "同步中..." - :sync-synced "同步" - - ;;messages - :status-sending "发送中" - :status-pending "待定" - :status-sent "已发送" - :status-seen-by-everyone "每个人已阅" - :status-seen "已阅" - :status-delivered "已发送" - :status-failed "发送失败" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分钟" - :other "分钟"} - :datetime-hour {:one "小时" - :other "小时"} - :datetime-day {:one "日" - :other "日"} - :datetime-ago "之前" - :datetime-yesterday "昨天" - :datetime-today "今天" - - ;;profile - :profile "个人资料" - :edit-profile "编辑个人资料" - :message "消息" - :not-specified "未指定" - :public-key "公共密钥" - :phone-number "电话号码" - :update-status "更新你的状态..." - :add-a-status "添加状态..." - :status-prompt "建立一个状态以帮助人们了解您提供的内容。 你也可以使用#hashtags。" - :add-to-contacts "添加到联系人" - :in-contacts "在联系人" - :remove-from-contacts "从联系人中删除" - :start-conversation "开始对话" - :send-transaction "发送交易" - - ;;make_photo - :image-source-title "个人资料照片" - :image-source-make-photo "截图" - :image-source-gallery "从相册中选择" - - ;;sharing - :sharing-copy-to-clipboard "复制到剪贴板" - :sharing-share "分享..." - :sharing-cancel "取消" - - :browsing-title "浏览" - :browsing-open-in-web-browser "在网络浏览器中打开" - :browsing-cancel "取消" - - ;;sign-up - :contacts-syncronized "您的联系人已同步" - :confirmation-code (str "谢谢!我们已经给您发了一个确认短信" - "代码。请提供该代码以确认您的电话号码") - :incorrect-code (str "对不起,代码不正确,请再输入一次") - :phew-here-is-your-passphrase "*唷* 是很难的,这是您的口令短语,*写下来并保证安全!* 您将需要它来恢复您的帐户。" - :here-is-your-passphrase "这是您的口令短语,*写下来并保证安全!* 您将需要它来恢复您的帐户。" - :phone-number-required "点击这里进入您的电话号码,我会找到您的朋友" - :shake-your-phone "发现错误或有建议? 只要〜摇一摇〜你的手机!" - :intro-status "与我聊天设置您的帐户,并更改您的设置!" - :intro-message1 "欢迎来到\n点击这一消息设置您的密码并开始!" - :account-generation-message "给我一秒,我要做一些疯狂的计算生成您的帐号!" - :move-to-internal-failure-message "我们需要将一些重要的文件从外部存储移动到内部存储。 为此,我们需要你的许可。 我们在将来的版本不会使用外部存储。" - :debug-enabled "调试服务器已经推出! 现在你可以执行* status-dev-cli scan *在同一网络上的电脑上查找服务器。" - - ;;phone types - :phone-e164 "国际1" - :phone-international "国际2" - :phone-national "国内" - :phone-significant "显着" - - ;;chats - :chats "聊天" - :delete-chat "刪除聊天" - :new-group-chat "新的群聊" - :new-public-group-chat "加入公开聊天" - :edit-chats "编辑对话" - :search-chats "搜索对话" - :empty-topic "主题空白" - :topic-format "格式错误 [a-z0-9\\-]+" - :public-group-topic "主题" - - ;;discover - :discover "发现" - :none "无" - :search-tags "在这里键入您的搜索标签" - :popular-tags "热门标签" - :recent "最近" - :no-statuses-discovered "未发现状态" - :no-statuses-found "找不到状态" - - ;;settings - :settings "设置" - - ;;contacts - :contacts "联系人" - :new-contact "新的联系人" - :delete-contact "删除联络人" - :delete-contact-confirmation "此联络人将从你的联络人中删除" - :remove-from-group "从群组中移除" - :edit-contacts "编辑联络人" - :search-contacts "搜寻联络人" - :contacts-group-new-chat "开始新的聊天" - :choose-from-contacts "从联络人中选择" - :no-contacts "暂无联系人" - :show-qr "显示QR" - :enter-address "輸入地址" - :more "更多" - - ;;group-settings - :remove "移动" - :save "保存" - :clear-history "清除历史" - :delete "刪除" - :mute-notifications "静音通知" - :leave-chat "离开对话" - :chat-settings "聊天设置" - :edit "编辑" - :add-members "添加会员" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新增群組" - :reorder-groups "重新排序群組" - :edit-group "編輯群組" - :delete-group "刪除群組" - :delete-group-confirmation "此群組將從你的群組中移除。 這不會影響聯絡人" - :delete-group-prompt "這不會影響聯絡人" - :contact-s {:one "contact" - :other "contacts"} - ;;participants - - ;;protocol - :received-invitation "接受聊天邀请" - :removed-from-chat "将您从群聊中移除" - :left "离开" - :invited "已邀请" - :removed "已移除" - :You "您" - - ;;new-contact - :add-new-contact "增加新的联系人" - :scan-qr "扫描QR" - :name "名称" - :address-explication "也许这应该有一个文本,解释地址是什么以及在哪里寻找它" - :enter-valid-public-key "请输入有效公钥或扫描QR码" - :contact-already-added "联系人已被添加" - :can-not-add-yourself "您不能添加您自己" - :unknown-address "未知地址" - - ;;login - :connect "连接" - :address "地址" - :password "密码" - :wrong-password "错误的密码" - :sign-in-to-status "登录Status" - :sign-in "登录" - - ;;recover - :passphrase "口令短语" - :recover "恢复" - :twelve-words-in-correct-order "正确排序12个字" - - ;;accounts - :recover-access "恢复访问" - :create-new-account "建立新帐户" - - ;;wallet-qr-code - :done "完成" - :main-wallet "主要钱包" - - ;;validation - :invalid-phone "无效的电话号码" - :amount "金额" - ;;transactions - :confirm "确认" - :transaction "交易" - :status "状态" - :recipient "收件人" - :to "至" - :from "从" - :data "数据" - :got-it "得到了" - - ;;:webview - :web-view-error "啊哦,错误"}) diff --git a/src/status_im/translations/zh_yue.cljs b/src/status_im/translations/zh_yue.cljs deleted file mode 100644 index 4e4dd08b7c5..00000000000 --- a/src/status_im/translations/zh_yue.cljs +++ /dev/null @@ -1,238 +0,0 @@ -(ns status-im.translations.zh-yue) - -(def translations - {;;common - :members-title "會員" - :not-implemented "!未能實現" - :chat-name "用戶名稱" - :notifications-title "通知及聲音設定" - :offline "離線" - :search-for "搜索..." - :cancel "取消" - :next "下一個" - :type-a-message "輸入訊息..." - :type-a-command "開始輸入指令..." - :error "錯誤" - - :camera-access-error "要授予所需的攝像機許可,請轉到系統設置,並確定選擇了“狀態”>“攝像機”。" - :photos-access-error "要授予所需的照片許可,請轉到系統設置,並確保選擇“狀態“>“照片“。" - - ;;drawer - :switch-users "切換用戶" - :current-network "當前網絡" - - ;;chat - :is-typing "正在輸入" - :and-you "與你" - :search-chat "搜索聊天記錄" - :members {:one "1位成員" - :other "{{count}} 位成員" - :zero "暫無成員"} - :members-active {:one "1位成員, 1位活躍" - :other "{{count}}位成員, {{count}}位活躍" - :zero "暫無成員"} - :public-group-status "公共" - :active-online "在線" - :active-unknown "未知" - :available "有空" - :no-messages "沒有新訊息" - :suggestions-requests "徵求" - :suggestions-commands "指令" - :faucet-success "水龍頭請求已經收到" - :faucet-error "水龍頭請求錯誤" - - ;;sync - :sync-in-progress "正在同步..." - :sync-synced "已同步" - - ;;messages - :status-sending "發送中" - :status-pending "待定" - :status-sent "發送成功" - :status-seen-by-everyone "所有人已讀" - :status-seen "已讀" - :status-delivered "已發送" - :status-failed "失敗" - - ;;datetime - :datetime-ago-format "{{number}} {{time-intervals}} {{ago}}" - :datetime-second {:one "秒" - :other "秒"} - :datetime-minute {:one "分鐘" - :other "分鐘"} - :datetime-hour {:one "小時" - :other "小時"} - :datetime-day {:one "天" - :other "天"} - :datetime-ago "之前" - :datetime-yesterday "昨天" - :datetime-today "今天" - - ;;profile - :profile "用戶簡介" - :edit-profile "編輯個人資料" - :message "短訊" - :not-specified "未標明" - :public-key "公共鑰匙" - :phone-number "電話號碼" - :update-status "更新你的狀態..." - :add-a-status "添加狀態..." - :status-prompt "建立一個狀態以幫助人們了解您提供的內容。 你也可以使用#hashtags。" - :add-to-contacts "添加到通訊錄" - :in-contacts "在聯絡資訊" - :remove-from-contacts "從聯絡資訊中刪除" - :start-conversation "開始對話" - :send-transaction "發送交易" - - ;;make_photo - :image-source-title "封面照片" - :image-source-make-photo "捕獲照片" - :image-source-gallery "從圖庫中選取" - - ;;sharing - :sharing-copy-to-clipboard "複製到剪貼板" - :sharing-share "分享..." - :sharing-cancel "取消" - - :browsing-title "瀏覽" - :browsing-open-in-web-browser "在網絡瀏覽器中打開" - :browsing-cancel "取消" - - ;;sign-up - :contacts-syncronized "你的聯繫人已同步" - :confirmation-code (str "謝謝!我們已以短訊形式將確認信息發送給你" - "代碼。請提供該代碼,以確認你的電話號碼") - :incorrect-code (str "對不起,代碼不正確,請重新輸入") - :phew-here-is-your-passphrase "*啊* 那可真不簡單。 這是你的臨時登入碼。 *請記錄並保存於安全地方!* 你將會需要它,以恢復你的帳戶。" - :here-is-your-passphrase "這是你的臨時登入碼。 *請記錄並保存於安全地方!* 你將會需要它,以恢復你的帳戶。" - :phone-number-required "請點擊這裡輸入您的電話號碼,我們將為你尋覓你的好友" - :shake-your-phone "發現錯誤或有建議? 只要〜搖一搖〜你的手機!" - :intro-status "如需設置新帳戶或更改現有設置,請與我聊天!" - :intro-message1 "歡迎來到Status\n請點擊此短訊,以設置你的密碼及開始!" - :account-generation-message "請給我一秒鐘。我正在瘋狂地運算,以啟動你的帳戶!" - :move-to-internal-failure-message "我們需要將一些重要的文件從外部存儲移動到內部存儲。 為此,我們需要你的許可。 我們在將來的版本不會使用外部存儲。" - :debug-enabled "調試服務器已經推出! 現在你可以執行* status-dev-cli scan *在同一網絡上的電腦上查找服務器。" - - ;;phone types - :phone-e164 "國際1" - :phone-international "國際2" - :phone-national "國內" - :phone-significant "顯著" - - ;;chats - :chats "聊天史" - :delete-chat "刪除對話" - :new-group-chat "新增群聊" - :new-public-group-chat "加入公開聊天" - :edit-chats "編輯對話" - :search-chats "搜索對話" - :empty-topic "主題空白" - :topic-format "格式錯誤 [a-z0-9\\-]+" - :public-group-topic "主題" - - ;;discover - :discover "新發現" - :none "不存在" - :search-tags "請輸入你的搜索標籤" - :popular-tags "熱門標籤" - :recent "最近發現" - :no-statuses-discovered "沒有發現任何狀態" - :no-statuses-found "找不到狀態" - - ;;settings - :settings "設置" - - ;;contacts - :contacts "聯絡人" - :new-contact "新增聯絡" - :delete-contact "刪除聯絡人" - :delete-contact-confirmation "此聯絡人將從你的聯絡人中刪除" - :remove-from-group "從群組中移除" - :edit-contacts "編輯聯絡人" - :search-contacts "搜尋聯絡人" - :contacts-group-new-chat "開啟新聊天" - :choose-from-contacts "從聯絡人中選擇" - :no-contacts "No contacts yet" - :show-qr "暫無聯絡人" - :enter-address "輸入地址" - :more "更多" - - ;;group-settings - :remove "刪除" - :save "儲存" - :delete "刪除" - :clear-history "清除歷史記錄" - :mute-notifications "靜音通知" - :leave-chat "離開對話" - :chat-settings "聊天設置" - :edit "編輯" - :add-members "增添成員" - - ;;commands - :chat-send-eth "{{amount}} ETH" - - ;;new-group - :new-group "新增群組" - :reorder-groups "重新排序群組" - :edit-group "編輯群組" - :delete-group "刪除群組" - :delete-group-confirmation "此群組將從你的群組中移除。 這不會影響聯絡人" - :delete-group-prompt "這不會影響聯絡人" - :contact-s {:one "contact" - :other "contacts"} - ;;participants - - ;;protocol - :received-invitation "收到聊天邀請" - :removed-from-chat "已將你從群組聊天中刪除" - :left "離開" - :invited "邀請" - :removed "刪除" - :You "你" - - ;;new-contact - :add-new-contact "添加新聯繫人" - :scan-qr "掃描 QR" - :name "名稱" - :address-explication "請注意: 此文本解釋地址是什麼,以及在何處可找到它。" - :enter-valid-public-key "請輸入有效公鑰或掃描QR碼" - :contact-already-added "此聯繫人已添加" - :can-not-add-yourself "你不能添加自己" - :unknown-address "未知地址" - - ;;login - :connect "連接" - :address "地址" - :password "密碼" - :sign-in-to-status "登入Status" - :sign-in "登入" - :wrong-password "密碼錯誤" - - ;;recover - :passphrase "臨時登入碼" - :recover "還原" - :twelve-words-in-correct-order "正確排序12個字" - - ;;accounts - :recover-access "恢復訪問" - :create-new-account "建立新帳號" - - ;;wallet-qr-code - :done "完成" - :main-wallet "主錢包" - - ;;validation - :invalid-phone "電話號碼無效" - :amount "金額" - ;;transactions - :confirm "確認" - :transaction "交易" - :status "狀態" - :recipient "收件人" - :to "至" - :from "從" - :data "數據" - :got-it "得到了" - - ;;:webview - :web-view-error "抱歉,錯誤"}) diff --git a/src/status_im/transport/core.cljs b/src/status_im/transport/core.cljs deleted file mode 100644 index 4c7e342f831..00000000000 --- a/src/status_im/transport/core.cljs +++ /dev/null @@ -1,73 +0,0 @@ -(ns ^{:doc "API to init and stop whisper messaging"} - status-im.transport.core - (:require [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.data-store.transport :as transport-store] - [status-im.transport.handlers :as transport.handlers] - [status-im.transport.inbox :as inbox] - status-im.transport.filters - [status-im.transport.utils :as transport.utils] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [taoensso.timbre :as log])) - -(defn init-whisper - "Initialises whisper protocol by: - - adding fixed shh discovery filter - - restoring existing symetric keys along with their unique filters - - (optionally) initializing offline inboxing" - [current-account-id {:keys [db web3] :as cofx}] - (log/debug :init-whisper) - (when-let [public-key (get-in db [:account/account :public-key])] - (let [sym-key-added-callback (fn [chat-id sym-key sym-key-id] - (re-frame/dispatch [::sym-key-added {:chat-id chat-id - :sym-key sym-key - :sym-key-id sym-key-id}])) - topic (transport.utils/get-topic constants/contact-discovery)] - (handlers-macro/merge-fx cofx - {:shh/add-discovery-filter {:web3 web3 - :private-key-id public-key - :topic topic} - :shh/restore-sym-keys {:web3 web3 - :transport (:transport/chats db) - :on-success sym-key-added-callback}} - (inbox/connect-to-mailserver) - (transport.handlers/resend-contact-messages))))) - -;;TODO (yenda) remove once go implements persistence -;;Since symkeys are not persisted, we restore them via add sym-keys, -;;this is the callback that is called when a key has been restored for a particular chat. -;;it saves the sym-key-id in app-db to send messages later -;;and starts a filter to receive messages -(handlers/register-handler-fx - ::sym-key-added - (fn [{:keys [db]} [_ {:keys [chat-id sym-key sym-key-id]}]] - (let [web3 (:web3 db) - {:keys [topic] :as chat} (get-in db [:transport/chats chat-id])] - {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) - :data-store/tx [(transport-store/save-transport-tx {:chat-id chat-id - :chat (assoc chat :sym-key-id sym-key-id)})] - :shh/add-filter {:web3 web3 - :sym-key-id sym-key-id - :topic topic - :chat-id chat-id}}))) - -;;TODO (yenda) uncomment and rework once go implements persistence -#_(doseq [[chat-id {:keys [sym-key-id topic] :as chat}] transport] - (when sym-key-id - (filters/add-filter! web3 - {:symKeyID sym-key-id - :topics [topic]} - (fn [js-error js-message] - (re-frame/dispatch [:protocol/receive-whisper-message js-error js-message chat-id]))))) - -(defn stop-whisper - "Stops whisper protocol by removing all existing shh filters - It is necessary to remove the filters because status-go there isn't currently a logout feature in status-go - to clean-up after logout. When logging out of account A and logging in account B, account B would receive - account A messages without this." - [{:keys [db]}] - (let [{:transport/keys [chats discovery-filter]} db - chat-filters (mapv :filter (vals chats)) - all-filters (conj chat-filters discovery-filter)] - {:shh/remove-filters all-filters})) diff --git a/src/status_im/transport/db.cljs b/src/status_im/transport/db.cljs deleted file mode 100644 index d7903e8e425..00000000000 --- a/src/status_im/transport/db.cljs +++ /dev/null @@ -1,37 +0,0 @@ -(ns ^{:doc "DB spec and utils for the transport layer"} - status-im.transport.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec])) - -;; required -(spec/def ::ack (spec/coll-of string? :kind vector?)) -(spec/def ::seen (spec/coll-of string? :kind vector?)) -(spec/def ::pending-ack (spec/coll-of string? :kind vector?)) -(spec/def ::pending-send (spec/coll-of string? :kind vector?)) -(spec/def ::topic string?) -(spec/def ::fetch-history? boolean?) -(spec/def ::resend? (spec/nilable #{"contact-request" "contact-request-confirmation" "contact-update"})) - -;; optional -(spec/def ::sym-key-id string?) -;;TODO (yenda) remove once go implements persistence -(spec/def ::sym-key string?) -(spec/def ::filter any?) - -(spec/def :transport/chat (allowed-keys :req-un [::ack ::seen ::pending-ack ::pending-send ::topic ::fetch-history?] - :opt-un [::sym-key-id ::sym-key ::filter ::resend?])) - -(spec/def :transport/chats (spec/map-of :global/not-empty-string :transport/chat)) -(spec/def :transport/discovery-filter (spec/nilable any?)) - -(defn create-chat - "Initialize datastructure for chat representation at the transport level - Currently only :topic is actually used" - [{:keys [topic resend?]}] - {:ack [] - :seen [] - :pending-ack [] - :pending-send [] - :fetch-history? true - :resend? resend? - :topic topic}) diff --git a/src/status_im/transport/filters.cljs b/src/status_im/transport/filters.cljs deleted file mode 100644 index c674f456c76..00000000000 --- a/src/status_im/transport/filters.cljs +++ /dev/null @@ -1,72 +0,0 @@ -(ns ^{:doc "API for whisper filters"} - status-im.transport.filters - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.transport.utils :as utils] - [status-im.utils.config :as config] - [taoensso.timbre :as log])) - -(defn remove-filter! [filter] - (.stopWatching filter - (fn [error _] - (if error - (log/warn :remove-filter-error filter error) - (log/debug :removed-filter filter)))) - (log/debug :stop-watching filter)) - -(defn add-shh-filter! - [web3 options callback] - (.newMessageFilter (utils/shh web3) (clj->js options) - callback - #(log/warn :add-filter-error (.stringify js/JSON (clj->js options)) %))) - -(defn add-filter! - [web3 {:keys [topics to] :as options} callback] - (let [options (if config/offline-inbox-enabled? - (assoc options :allowP2P true) - options)] - (log/debug :add-filter options) - (add-shh-filter! web3 options callback))) - -(re-frame/reg-fx - :shh/add-filter - (fn [{:keys [web3 sym-key-id topic chat-id]}] - (when-let [filter (add-filter! web3 - {:topics [topic] - :symKeyID sym-key-id} - (fn [js-error js-message] - (re-frame/dispatch [:protocol/receive-whisper-message js-error js-message chat-id])))] - (re-frame/dispatch [::filter-added chat-id filter])))) - -(handlers/register-handler-db - ::filter-added - [re-frame/trim-v] - (fn [db [chat-id filter]] - (assoc-in db [:transport/chats chat-id :filter] filter))) - -(re-frame/reg-fx - :shh/add-discovery-filter - (fn [{:keys [web3 private-key-id topic]}] - (when-let [filter (add-filter! web3 - {:topics [topic] - :privateKeyID private-key-id} - (fn [js-error js-message] - (re-frame/dispatch [:protocol/receive-whisper-message js-error js-message])))] - (re-frame/dispatch [::discovery-filter-added filter])))) - -(handlers/register-handler-db - ::discovery-filter-added - [re-frame/trim-v] - (fn [db [filter]] - (assoc db :transport/discovery-filter filter))) - -(re-frame/reg-fx - :shh/remove-filter - (fn [filter] - (when filter (remove-filter! filter)))) - -(re-frame/reg-fx - :shh/remove-filters - (fn [filters] - (doseq [filter filters] - (remove-filter! filter)))) diff --git a/src/status_im/transport/handlers.cljs b/src/status_im/transport/handlers.cljs deleted file mode 100644 index 37c3e7822c7..00000000000 --- a/src/status_im/transport/handlers.cljs +++ /dev/null @@ -1,279 +0,0 @@ -(ns ^{:doc "Events for message handling"} - status-im.transport.handlers - (:require [re-frame.core :as re-frame] - [status-im.chat.models.message :as models.message] - [status-im.data-store.transport :as transport-store] - [status-im.transport.message.core :as message] - [status-im.transport.message.transit :as transit] - [status-im.transport.message.v1.contact :as v1.contact] - [status-im.transport.message.v1.group-chat :as v1.group-chat] - [status-im.transport.shh :as shh] - [status-im.transport.utils :as transport.utils] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [taoensso.timbre :as log] - [status-im.transport.message.v1.protocol :as protocol])) - -(defn update-last-received-from-inbox - "Distinguishes messages that are expired from those that are not - Expired messages are coming from offline inboxing" - [now-in-s timestamp ttl {:keys [db now] :as cofx}] - (when (> (- now-in-s timestamp) ttl) - {:db (assoc db :inbox/last-received now)})) - -(defn receive-message [cofx now-in-s chat-id js-message] - (let [{:keys [payload sig timestamp ttl]} (js->clj js-message :keywordize-keys true) - status-message (-> payload - transport.utils/to-utf8 - transit/deserialize)] - (when (and sig status-message) - (try - (handlers-macro/merge-fx - (assoc cofx :js-obj js-message) - (message/receive status-message (or chat-id sig) sig timestamp) - (update-last-received-from-inbox now-in-s timestamp ttl)) - (catch :default e nil))))) ; ignore unknown message types - -(defn- js-array->seq [array] - (for [i (range (.-length array))] - (aget array i))) - -(defn receive-whisper-messages [{:keys [now] :as cofx} [js-error js-messages chat-id]] - (if (and (not js-error) - js-messages) - (let [now-in-s (quot now 1000)] - (handlers-macro/merge-effects - cofx - (fn [message temp-cofx] - (receive-message temp-cofx now-in-s chat-id message)) - (js-array->seq js-messages))) - (log/error "Something went wrong" js-error js-messages))) - -(handlers/register-handler-fx - :protocol/receive-whisper-message - [re-frame/trim-v - handlers/logged-in - (re-frame/inject-cofx :random-id)] - receive-whisper-messages) - -(handlers/register-handler-fx - :protocol/send-status-message-success - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [_ resp]] - (log/debug :send-status-message-success resp))) - -(handlers/register-handler-fx - :protocol/send-status-message-error - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [err]] - (log/error :send-status-message-error err))) - -(handlers/register-handler-fx - :contact/send-new-sym-key - (fn [{:keys [db random-id] :as cofx} [_ {:keys [chat-id topic message sym-key sym-key-id]}]] - (let [{:keys [web3 current-public-key]} db - chat-transport-info (-> (get-in db [:transport/chats chat-id]) - (assoc :sym-key-id sym-key-id - :sym-key sym-key - :topic topic))] - (handlers-macro/merge-fx cofx - {:db (assoc-in db [:transport/chats chat-id] chat-transport-info) - :shh/add-filter {:web3 web3 - :sym-key-id sym-key-id - :topic topic - :chat-id chat-id} - :data-store/tx [(transport-store/save-transport-tx {:chat-id chat-id - :chat chat-transport-info})]} - (message/send (v1.contact/NewContactKey. sym-key topic message) - chat-id))))) - -(handlers/register-handler-fx - :contact/add-new-sym-key - (fn [{:keys [db] :as cofx} [_ {:keys [sym-key-id sym-key chat-id topic timestamp message]}]] - (let [{:keys [web3 current-public-key]} db - chat-transport-info (-> (get-in db [:transport/chats chat-id]) - (assoc :sym-key-id sym-key-id - :sym-key sym-key - :topic topic))] - (handlers-macro/merge-fx cofx - {:db (assoc-in db - [:transport/chats chat-id] - chat-transport-info) - :dispatch [:inbox/request-chat-history chat-id] - :shh/add-filter {:web3 web3 - :sym-key-id sym-key-id - :topic topic - :chat-id chat-id} - :data-store/tx [(transport-store/save-transport-tx {:chat-id chat-id - :chat chat-transport-info})]} - (message/receive message chat-id chat-id timestamp))))) - -#_(handlers/register-handler-fx - :send-test-message - (fn [cofx [this timer chat-id n]] - (if (zero? n) - (println "Time: " (str (- (inst-ms (js/Date.)) @timer))) - (handlers-macro/merge-fx cofx - {:dispatch [this timer chat-id (dec n)]} - (message/send (protocol/map->Message {:content (str n) - :content-type "text/plain" - :message-type :user-message - :clock-value n - :timestamp (str (inst-ms (js/Date.)))}) - chat-id))))) - -(handlers/register-handler-fx - :group/unsubscribe-from-chat - [re-frame/trim-v] - (fn [cofx [chat-id]] - (transport.utils/unsubscribe-from-chat chat-id cofx))) - -(handlers/register-handler-fx - :group/send-new-sym-key - [re-frame/trim-v] - ;; this is the event that is called when we want to send a message that required first - ;; some async operations - (fn [{:keys [db] :as cofx} [{:keys [chat-id message sym-key sym-key-id]}]] - (let [{:keys [web3]} db] - (handlers-macro/merge-fx cofx - {:db (update-in db [:transport/chats chat-id] - assoc - :sym-key-id sym-key-id - :sym-key sym-key) - :shh/add-filter {:web3 web3 - :sym-key-id sym-key-id - :topic (transport.utils/get-topic chat-id) - :chat-id chat-id} - :data-store/tx [(transport-store/save-transport-tx - {:chat-id chat-id - :chat (-> (get-in db [:transport/chats chat-id]) - (assoc :sym-key-id sym-key-id) - ;;TODO (yenda) remove once go implements persistence - (assoc :sym-key sym-key))})]} - (message/send (v1.group-chat/NewGroupKey. chat-id sym-key message) chat-id))))) - -(handlers/register-handler-fx - :group/add-new-sym-key - [re-frame/trim-v (re-frame/inject-cofx :random-id)] - (fn [{:keys [db] :as cofx} [{:keys [sym-key-id sym-key chat-id signature timestamp message]}]] - (let [{:keys [web3 current-public-key]} db - topic (transport.utils/get-topic chat-id) - fx {:db (assoc-in db - [:transport/chats chat-id :sym-key-id] - sym-key-id) - :dispatch [:inbox/request-chat-history chat-id] - :shh/add-filter {:web3 web3 - :sym-key-id sym-key-id - :topic topic - :chat-id chat-id} - :data-store/tx [(transport-store/save-transport-tx - {:chat-id chat-id - :chat (-> (get-in db [:transport/chats chat-id]) - (assoc :sym-key-id sym-key-id) - ;;TODO (yenda) remove once go implements persistence - (assoc :sym-key sym-key))})]}] - ;; if new sym-key is wrapping some message, call receive on it as well, if not just update the transport layer - (if message - (handlers-macro/merge-fx cofx fx (message/receive message chat-id signature timestamp)) - fx)))) - -(re-frame/reg-fx - ;; TODO(janherich): this should be called after `:data-store/tx` actually - :confirm-messages-processed - (fn [messages] - (let [{:keys [web3]} (first messages) - js-messages (->> messages - (keep :js-obj) - (apply array))] - (when (pos? (.-length js-messages)) - (.confirmMessagesProcessed (transport.utils/shh web3) - js-messages - (fn [err resp] - (when err - (log/info "Confirming messages processed failed")))))))) - -(handlers/register-handler-fx - :transport/set-message-envelope-hash - [re-frame/trim-v] - ;; message-type is used for tracking - (fn [{:keys [db]} [chat-id message-id message-type envelope-hash]] - {:db (assoc-in db [:transport/message-envelopes envelope-hash] - {:chat-id chat-id - :message-id message-id - :message-type message-type})})) - -(handlers/register-handler-fx - :transport/set-contact-message-envelope-hash - [re-frame/trim-v] - (fn [{:keys [db]} [chat-id envelope-hash]] - {:db (assoc-in db [:transport/message-envelopes envelope-hash] - {:chat-id chat-id - :message-type :contact-message})})) - -(defn remove-hash [envelope-hash {:keys [db] :as cofx}] - {:db (update db :transport/message-envelopes dissoc envelope-hash)}) - -(defn update-resend-contact-message [chat-id {:keys [db] :as cofx}] - (let [chat (get-in db [:transport/chats chat-id]) - updated-chat (assoc chat :resend? nil)] - {:db (assoc-in db [:transport/chats chat-id :resend?] nil) - :data-store/tx [(transport-store/save-transport-tx {:chat-id chat-id - :chat updated-chat})]})) - -(handlers/register-handler-fx - :signals/envelope-status - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [envelope-hash status]] - (let [{:keys [chat-id message-type message-id]} - (get-in db [:transport/message-envelopes envelope-hash])] - (case message-type - :contact-message - (when (= :sent status) - (handlers-macro/merge-fx cofx - (remove-hash envelope-hash) - (update-resend-contact-message chat-id))) - - (when-let [message (get-in db [:chats chat-id :messages message-id])] - (let [{:keys [fcm-token]} (get-in db [:contacts/contacts chat-id])] - (handlers-macro/merge-fx cofx - (remove-hash envelope-hash) - (models.message/update-message-status message status) - (models.message/send-push-notification fcm-token status)))))))) - -(defn- own-info [db] - (let [{:keys [name photo-path address]} (:account/account db) - fcm-token (get-in db [:notifications :fcm-token])] - {:name name - :profile-image photo-path - :address address - :fcm-token fcm-token})) - -(defn resend-contact-request [own-info chat-id {:keys [sym-key topic]} cofx] - (message/send (v1.contact/NewContactKey. sym-key - topic - (v1.contact/map->ContactRequest own-info)) - chat-id cofx)) - -(defn resend-contact-messages - ([cofx] - (resend-contact-messages [] cofx)) - ([previous-summary {:keys [db] :as cofx}] - (when (and (zero? (count previous-summary)) - (= :online (:network-status db)) - (pos? (count (:peers-summary db)))) - (let [own-info (own-info db)] - (handlers-macro/merge-effects - cofx - (fn [[chat-id {:keys [resend?] :as chat}] temp-cofx] - (case resend? - "contact-request" - (resend-contact-request own-info chat-id chat temp-cofx) - "contact-request-confirmation" - (message/send (v1.contact/map->ContactRequestConfirmed own-info) - chat-id temp-cofx) - "contact-update" - (protocol/send {:chat-id chat-id - :payload (v1.contact/map->ContactUpdate own-info)} - temp-cofx) - nil)) - (:transport/chats db)))))) diff --git a/src/status_im/transport/impl/receive.cljs b/src/status_im/transport/impl/receive.cljs deleted file mode 100644 index cd425d00107..00000000000 --- a/src/status_im/transport/impl/receive.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.transport.impl.receive - (:require - [status-im.chat.models.group-chat :as models.group-chat] - [status-im.models.contact :as models.contact] - [status-im.transport.message.core :as message] - [status-im.transport.message.v1.contact :as transport.contact] - [status-im.transport.message.v1.group-chat :as transport.group-chat])) - -(extend-type transport.group-chat/GroupAdminUpdate - message/StatusMessage - (receive [this chat-id signature _ cofx] - (models.group-chat/handle-group-admin-update this chat-id signature cofx))) - -(extend-type transport.group-chat/GroupLeave - message/StatusMessage - (receive [this chat-id signature _ cofx] - (models.group-chat/handle-group-leave chat-id signature cofx))) - -(extend-type transport.contact/ContactRequest - message/StatusMessage - (receive [this _ signature timestamp cofx] - (models.contact/receive-contact-request signature timestamp this cofx))) - -(extend-type transport.contact/ContactRequestConfirmed - message/StatusMessage - (receive [this _ signature timestamp cofx] - (models.contact/receive-contact-request-confirmation signature timestamp this cofx))) - -(extend-type transport.contact/ContactUpdate - message/StatusMessage - (receive [this _ signature timestamp cofx] - (models.contact/receive-contact-update signature timestamp this cofx))) diff --git a/src/status_im/transport/inbox.cljs b/src/status_im/transport/inbox.cljs deleted file mode 100644 index d1e3fe9844b..00000000000 --- a/src/status_im/transport/inbox.cljs +++ /dev/null @@ -1,390 +0,0 @@ -(ns ^{:doc "Offline inboxing events and API"} - status-im.transport.inbox - (:require [re-frame.core :as re-frame] - [status-im.native-module.core :as status] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.transport.utils :as transport.utils] - [status-im.utils.config :as config] - [taoensso.timbre :as log] - [status-im.constants :as constants] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.utils :as utils] - [status-im.i18n :as i18n] - [status-im.constants :as constants] - [status-im.data-store.accounts :as accounts-store] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.data-store.core :as data-store] - [status-im.models.mailserver :as models.mailserver] - [status-im.ui.screens.accounts.events :as accounts] - [status-im.data-store.transport :as transport-store])) - -;; How does offline inboxing work ? -;; -;; - We send a request to the mailserver, we are only interested in the -;; messages since `last-request`, the time of the last successful request, -;; and the last 24 hours for topics that were just joined -;; - The mailserver doesn't directly respond to the request and -;; instead we start receiving messages in the filters for the requested -;; topics. -;; - These messages are expired that is how we differentiate them from -;; normal whisper messages to update last-received -;; - After fetching-timeout is reached since the last mailserver message -;; was received without a connection incident, we consider the request -;; successfull and update `last-request` and `fetch-history?` fields of each -;; topic to false -;; - If the mailserver was not ready when we tried for instance to request -;; the history of a topic after joining a chat, the request will be done -;; as soon as the mailserver becomes available - -(def connection-timeout - "Time after which mailserver connection is considered to have failed" - 15000) - -(def fetching-timeout - "Time we should wait after last message was fetch from mailserver before we - consider it done - Needs to be at least 10 seconds because that is the time it takes for the app - to realize it was disconnected" - 10000) - -(defn- parse-json - ;; NOTE(dmitryn) Expects JSON response like: - ;; {"error": "msg"} or {"result": true} - [s] - (try - (let [res (-> s - js/JSON.parse - (js->clj :keywordize-keys true))] - ;; NOTE(dmitryn): AddPeer() may return {"error": ""} - ;; assuming empty error is a success response - ;; by transforming {"error": ""} to {:result true} - (if (and (:error res) - (= (:error res) "")) - {:result true} - res)) - (catch :default e - {:error (.-message e)}))) - -(defn- response-handler [error-fn success-fn] - (fn handle-response - ([response] - (let [{:keys [error result]} (parse-json response)] - (handle-response error result))) - ([error result] - (if error - (error-fn error) - (success-fn result))))) - -(defn add-sym-key-id-to-wnode [{:keys [id]} sym-key-id {:keys [db]}] - (let [network (get (:networks (:account/account db)) (:network db)) - chain (ethereum/network->chain-keyword network)] - {:db (assoc-in db [:inbox/wnodes chain id :sym-key-id] sym-key-id)})) - -(defn registered-peer? [peers enode] - (let [peer-ids (into #{} (map :id) peers) - enode-id (transport.utils/extract-enode-id enode)] - (contains? peer-ids enode-id))) - -(defn add-peer [enode success-fn error-fn] - (status/add-peer enode (response-handler error-fn success-fn))) - -(defn mark-trusted-peer [web3 enode success-fn error-fn] - (.markTrustedPeer (transport.utils/shh web3) - enode - (fn [err resp] - (if-not err - (success-fn resp) - (error-fn err))))) - -(def one-day (* 24 3600)) -(def seven-days (* 7 one-day)) - -(defn request-inbox-messages-params [mailserver from to topics] - (let [days (conj - (into [] (range from to one-day)) - to) - day-ranges (map vector days (drop 1 days))] - (for [topic topics - [current-from current-to] day-ranges] - {:topic topic - :mailServerPeer (:address mailserver) - :symKeyID (:sym-key-id mailserver) - :from current-from - :to current-to}))) - -(defn request-inbox-messages - [web3 mailserver topics start-from end-to success-fn error-fn] - (log/info "offline inbox: request-messages request for topics " topics " from " start-from " to " end-to) - (doseq [{:keys [topic] :as params} (request-inbox-messages-params - mailserver - start-from - end-to - topics)] - (log/info "offline inbox: request-messages for: " - " topic " topic - " from " (:from params) - " to " (:to params)) - (.requestMessages (transport.utils/shh web3) - (clj->js params) - (fn [err resp] - (if-not err - (success-fn resp topic) - (error-fn err topic)))))) - -(re-frame/reg-fx - ::add-peer - (fn [{:keys [wnode]}] - (add-peer wnode - #(log/debug "offline inbox: add-peer success" %) - #(log/error "offline inbox: add-peer error" %)))) - -(re-frame/reg-fx - ::mark-trusted-peer - (fn [{:keys [wnode web3]}] - (mark-trusted-peer web3 - wnode - #(re-frame/dispatch [:inbox/mailserver-trusted %]) - #(re-frame/dispatch [:inbox/check-connection])))) - -(re-frame/reg-fx - ::request-messages - (fn [params] - (doseq [{:keys [wnode topics to from web3]} params] - (request-inbox-messages web3 - wnode - topics - from - to - #(log/info "offline inbox: request-messages response" %1 %2 from to) - #(log/error "offline inbox: request-messages error" %1 %2 from to))))) - -(defn update-mailserver-status [transition {:keys [db]}] - (let [state transition] - {:db (assoc db - :mailserver-status state - :inbox/fetching? false)})) - -(defn generate-mailserver-symkey [wnode {:keys [db] :as cofx}] - (when-not (:sym-key-id wnode) - {:shh/generate-sym-key-from-password - {:password (:password wnode) - :web3 (:web3 db) - :on-success (fn [_ sym-key-id] - (re-frame/dispatch [:inbox/get-sym-key-success wnode sym-key-id])) - :on-error #(log/error "offline inbox: get-sym-key error" %)}})) - -(defn connect-to-mailserver - "Add mailserver as a peer using ::add-peer cofx and generate sym-key when - it doesn't exists - Peer summary will change and we will receive a signal from status go when - this is successful - A connection-check is made after `connection timeout` is reached and - mailserver-status is changed to error if it is not connected by then" - [{:keys [db] :as cofx}] - (let [web3 (:web3 db) - {:keys [address] :as wnode} (models.mailserver/fetch-current cofx) - peers-summary (:peers-summary db) - connected? (registered-peer? peers-summary address)] - (when config/offline-inbox-enabled? - (if connected? - (handlers-macro/merge-fx cofx - (update-mailserver-status :connected) - (generate-mailserver-symkey wnode)) - (handlers-macro/merge-fx cofx - {::add-peer {:wnode address} - :utils/dispatch-later [{:ms connection-timeout - :dispatch [:inbox/check-connection]}]} - (update-mailserver-status :connecting) - (generate-mailserver-symkey wnode)))))) - -(defn peers-summary-change-fx - "There is only 2 summary changes that require offline inboxing action: - - mailserver disconnected: we try to reconnect - - mailserver connected: we mark the mailserver as trusted peer" - [previous-summary {:keys [db] :as cofx}] - (when (and (:account/account db) - config/offline-inbox-enabled?) - (let [{:keys [peers-summary peers-count]} db - wnode (:address (models.mailserver/fetch-current cofx)) - mailserver-was-registered? (registered-peer? previous-summary - wnode) - mailserver-is-registered? (registered-peer? peers-summary - wnode) - ;; the mailserver just connected - mailserver-connected? (and mailserver-is-registered? - (not mailserver-was-registered?)) - ;; the mailserver just disconnected - mailserver-disconnected? (and mailserver-was-registered? - (not mailserver-is-registered?))] - (cond - mailserver-disconnected? - (connect-to-mailserver cofx) - - mailserver-connected? - {::mark-trusted-peer {:web3 (:web3 db) - :wnode wnode}})))) - -(defn inbox-ready? [{:keys [sym-key-id]} {:keys [db]}] - (let [mailserver-status (:mailserver-status db)] - (and (= :connected mailserver-status) - sym-key-id))) - -(defn get-request-messages-topics - "Returns topics for which full history has already been recovered" - [db] - (conj (map :topic - (remove :fetch-history? - (vals (:transport/chats db)))) - (transport.utils/get-topic constants/contact-discovery))) - -(defn get-request-history-topics - "Returns topics for which full history has not been recovered" - [db] - (map :topic - (filter :fetch-history? - (vals (:transport/chats db))))) - -(defn request-history-span [now-in-s] - (- now-in-s one-day)) - -(defn request-messages - ([{:keys [db now] :as cofx}] - (let [wnode (models.mailserver/fetch-current cofx) - web3 (:web3 db) - now-in-s (quot now 1000) - last-request (max - (get-in db [:account/account :last-request]) - (- now-in-s seven-days)) - request-messages-topics (get-request-messages-topics db) - request-history-topics (get-request-history-topics db)] - (when (inbox-ready? wnode cofx) - {::request-messages [{:wnode wnode - :topics request-messages-topics - :from last-request - :to now-in-s - :web3 web3} - {:wnode wnode - :from (request-history-span now-in-s) - :to now-in-s - :topics request-history-topics - :web3 web3}] - :db (assoc db :inbox/fetching? true) - :dispatch-later [{:ms fetching-timeout - :dispatch [:inbox/check-fetching now-in-s]}]}))) - ([should-recover? {:keys [db] :as cofx}] - (when should-recover? - (request-messages cofx)))) - -(defn request-chat-history [chat-id {:keys [db now] :as cofx}] - (let [wnode (models.mailserver/fetch-current cofx) - web3 (:web3 db) - topic (get-in db [:transport/chats chat-id :topic]) - now-in-s (quot now 1000)] - (when (inbox-ready? wnode cofx) - {::request-messages [{:wnode wnode - :topics [topic] - :from (request-history-span now-in-s) - :to now-in-s - :web3 web3}] - :db (assoc db :inbox/fetching? true) - :dispatch-later [{:ms fetching-timeout - :dispatch [:inbox/check-fetching now-in-s chat-id]}]}))) - -;;;; Handlers - -(handlers/register-handler-fx - :inbox/mailserver-trusted - (fn [{:keys [db] :as cofx} _] - (handlers-macro/merge-fx cofx - (update-mailserver-status :connected) - (request-messages)))) - -(handlers/register-handler-fx - :inbox/get-sym-key-success - (fn [{:keys [db] :as cofx} [_ wnode sym-key-id]] - (handlers-macro/merge-fx cofx - (add-sym-key-id-to-wnode wnode sym-key-id) - (request-messages)))) - -(handlers/register-handler-fx - :inbox/request-chat-history - (fn [{:keys [db] :as cofx} [_ chat-id]] - (request-chat-history chat-id cofx))) - -(handlers/register-handler-fx - :inbox/check-connection - (fn [{:keys [db] :as cofx} _] - (when (= :connecting (:mailserver-status db)) - (if (models.mailserver/preferred-mailserver-id cofx) - (update-mailserver-status :error cofx) - (handlers-macro/merge-fx cofx - (models.mailserver/set-current-mailserver) - (connect-to-mailserver)))))) - -(defn update-last-request [last-request {:keys [db]}] - (let [chats (:transport/chats db) - transport-txs (reduce (fn [txs [chat-id chat]] - (if (:fetch-history? chat) - (conj txs - (transport-store/save-transport-tx - {:chat-id chat-id - :chat (assoc chat - :fetch-history? false)})) - txs)) - [] - chats) - chats-update (reduce (fn [acc [chat-id chat]] - (if (:fetch-history? chat) - (assoc acc chat-id (assoc chat :fetch-history? false)) - (assoc acc chat-id chat))) - {} - chats)] - {:db (-> db - (assoc :transport/chats chats-update) - (assoc-in [:account/account :last-request] - last-request)) - :data-store/base-tx [(accounts-store/save-account-tx - (assoc (:account/account db) - :last-request last-request))] - :data-store/tx transport-txs})) - -(defn update-fetch-history [chat-id {:keys [db]}] - {:db (assoc-in db - [:transport/chats chat-id :fetch-history?] - false) - :data-store/tx [(transport-store/save-transport-tx - {:chat-id chat-id - :chat (assoc (get-in db [:transport/chats chat-id]) - :fetch-history? false)})]}) - -(defn initialize-offline-inbox [custom-mailservers cofx] - (handlers-macro/merge-fx cofx - (models.mailserver/add-custom-mailservers custom-mailservers) - (models.mailserver/set-initial-last-request) - (models.mailserver/set-current-mailserver))) - -(handlers/register-handler-fx - :inbox/check-fetching - (fn [{:keys [db now] :as cofx} [_ last-request chat-id]] - (when (and (:inbox/fetching? db) - ;; if chat was removed before messages were fetched no need - ;; to proceed with further actions - (or (not chat-id) (contains? (:transport/chats db) chat-id))) - (let [time-since-last-received (- now (:inbox/last-received db))] - (if (> time-since-last-received fetching-timeout) - (if chat-id - (handlers-macro/merge-fx cofx - {:db (assoc db :inbox/fetching? false)} - (update-fetch-history chat-id)) - (handlers-macro/merge-fx cofx - {:db (assoc db :inbox/fetching? false)} - (update-last-request last-request))) - {:dispatch-later [{:ms (- fetching-timeout - time-since-last-received) - :dispatch [:inbox/check-fetching last-request chat-id]}]}))))) - -(handlers/register-handler-fx - :inbox/reconnect - (fn [cofx [_ args]] - (connect-to-mailserver cofx))) diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs deleted file mode 100644 index 3fa2ae8033e..00000000000 --- a/src/status_im/transport/message/core.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns ^{:doc "Definition of the StatusMessage protocol"} - status-im.transport.message.core) - -(defprotocol StatusMessage - "Protocol for the messages that are sent through the transport layer" - (send [this chat-id cofx] "Method producing all effects necessary for sending the message record") - (receive [this chat-id signature timestamp cofx] "Method producing all effects necessary for receiving the message record")) diff --git a/src/status_im/transport/message/transit.cljs b/src/status_im/transport/message/transit.cljs deleted file mode 100644 index 2900f79dd0f..00000000000 --- a/src/status_im/transport/message/transit.cljs +++ /dev/null @@ -1,116 +0,0 @@ -(ns ^{:doc "Transit custom readers and writers, required when adding a new record implementing StatusMessage protocol"} - status-im.transport.message.transit - (:require [status-im.transport.message.v1.contact :as v1.contact] - [status-im.transport.message.v1.protocol :as v1.protocol] - [status-im.transport.message.v1.group-chat :as v1.group-chat] - [cognitect.transit :as transit])) - -;; When adding a new reccord implenting the StatusMessage protocol it is required to implement: -;; - a handler that will turn the clojure record into a javascript datastructure. -;; - a reader that will turn the javascript datastructure back into a clojure record. - -;; Use the existing types as exemples of how this is done - -;; -;; Writer handlers -;; - -;; Each writer defines a tag and a representation -;; The tag will determine which reader is used to recreate the clojure record -;; When migrating a particular record, it is important to use a different type and still handle the previous -;; gracefully for compatibility -(deftype NewContactKeyHandler [] - Object - (tag [this v] "c1") - (rep [this {:keys [sym-key topic message]}] - #js [sym-key topic message])) - -(deftype ContactRequestHandler [] - Object - (tag [this v] "c2") - (rep [this {:keys [name profile-image address fcm-token]}] - #js [name profile-image address fcm-token])) - -(deftype ContactRequestConfirmedHandler [] - Object - (tag [this v] "c3") - (rep [this {:keys [name profile-image address fcm-token]}] - #js [name profile-image address fcm-token])) - -(deftype ContactUpdateHandler [] - Object - (tag [this v] "c6") - (rep [this {:keys [name profile-image address fcm-token]}] - #js [name profile-image address fcm-token])) - -(deftype MessageHandler [] - Object - (tag [this v] "c4") - (rep [this {:keys [content content-type message-type clock-value timestamp]}] - #js [content content-type message-type clock-value timestamp])) - -(deftype MessagesSeenHandler [] - Object - (tag [this v] "c5") - (rep [this {:keys [message-ids]}] - (clj->js message-ids))) - -(deftype NewGroupKeyHandler [] - Object - (tag [this v] "g1") - (rep [this {:keys [chat-id sym-key message]}] - #js [chat-id sym-key message])) - -(deftype GroupAdminUpdateHandler [] - Object - (tag [this v] "g2") - (rep [this {:keys [chat-name participants]}] - #js [chat-name participants])) - -(deftype GroupLeaveHandler [] - Object - (tag [this v] "g3") - (rep [this _] - (clj->js nil))) - -(def writer (transit/writer :json - {:handlers - {v1.contact/NewContactKey (NewContactKeyHandler.) - v1.contact/ContactRequest (ContactRequestHandler.) - v1.contact/ContactRequestConfirmed (ContactRequestConfirmedHandler.) - v1.contact/ContactUpdate (ContactUpdateHandler.) - v1.protocol/Message (MessageHandler.) - v1.protocol/MessagesSeen (MessagesSeenHandler.) - v1.group-chat/NewGroupKey (NewGroupKeyHandler.) - v1.group-chat/GroupAdminUpdate (GroupAdminUpdateHandler.) - v1.group-chat/GroupLeave (GroupLeaveHandler.)}})) - -;; -;; Reader handlers -;; - -;; Here we only need to call the record with the arguments parsed from the clojure datastructures -(def reader (transit/reader :json - {:handlers - {"c1" (fn [[sym-key topic message]] - (v1.contact/NewContactKey. sym-key topic message)) - "c2" (fn [[name profile-image address fcm-token]] - (v1.contact/ContactRequest. name profile-image address fcm-token)) - "c3" (fn [[name profile-image address fcm-token]] - (v1.contact/ContactRequestConfirmed. name profile-image address fcm-token)) - "c4" (fn [[content content-type message-type clock-value timestamp]] - (v1.protocol/Message. content content-type message-type clock-value timestamp)) - "c5" (fn [message-ids] - (v1.protocol/MessagesSeen. message-ids)) - "c6" (fn [[name profile-image address fcm-token]] - (v1.contact/ContactUpdate. name profile-image address fcm-token))}})) ; removed group chat handlers for https://github.com/status-im/status-react/issues/4506 - -(defn serialize - "Serializes a record implementing the StatusMessage protocol using the custom writers" - [o] - (transit/write writer o)) - -(defn deserialize - "Deserializes a record implementing the StatusMessage protocol using the custom readers" - [o] - (try (transit/read reader o) (catch :default e nil))) diff --git a/src/status_im/transport/message/v1/contact.cljs b/src/status_im/transport/message/v1/contact.cljs deleted file mode 100644 index 984778f594a..00000000000 --- a/src/status_im/transport/message/v1/contact.cljs +++ /dev/null @@ -1,123 +0,0 @@ -(ns ^{:doc "Contact request and update API"} - status-im.transport.message.v1.contact - (:require [re-frame.core :as re-frame] - [status-im.transport.message.core :as message] - [status-im.transport.message.v1.protocol :as protocol] - [status-im.transport.utils :as transport.utils] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.handlers :as handlers] - [status-im.data-store.transport :as transport-store])) - -(defrecord ContactRequest [name profile-image address fcm-token] - message/StatusMessage - (send [this chat-id {:keys [db random-id] :as cofx}] - (let [topic (transport.utils/get-topic random-id) - on-success (fn [sym-key sym-key-id] - (re-frame/dispatch [:contact/send-new-sym-key - {:sym-key-id sym-key-id - :sym-key sym-key - :chat-id chat-id - :topic topic - :message this}]))] - (handlers-macro/merge-fx cofx - {:shh/get-new-sym-keys [{:web3 (:web3 db) - :on-success on-success}]} - (protocol/init-chat {:chat-id chat-id - :topic topic - :resend? "contact-request"}))))) - -(defrecord ContactRequestConfirmed [name profile-image address fcm-token] - message/StatusMessage - (send [this chat-id {:keys [db] :as cofx}] - (let [success-event [:transport/set-contact-message-envelope-hash chat-id] - chat (get-in db [:transport/chats chat-id]) - updated-chat (assoc chat :resend? "contact-request-confirmation")] - (handlers-macro/merge-fx cofx - {:db (assoc-in db - [:transport/chats chat-id :resend?] - "contact-request-confirmation") - :data-store/tx [(transport-store/save-transport-tx {:chat-id chat-id - :chat updated-chat})]} - (protocol/send-with-pubkey {:chat-id chat-id - :payload this - :success-event success-event}))))) - -(defrecord ContactUpdate [name profile-image address fcm-token] - message/StatusMessage - (send [this _ {:keys [db] :as cofx}] - (let [public-keys (reduce (fn [acc [_ {:keys [public-key pending?]}]] - (if (and public-key - (not pending?)) - (conj acc public-key) - acc)) - #{} - (:contacts/contacts db)) - recipients (filter #(public-keys (first %)) (:transport/chats db))] - (handlers-macro/merge-effects - cofx - (fn [[chat-id chat] temp-cofx] - (let [updated-chat (assoc chat :resend? "contact-update") - tx [(transport-store/save-transport-tx {:chat-id chat-id - :chat updated-chat})] - success-event [:transport/set-contact-message-envelope-hash chat-id]] - (handlers-macro/merge-fx temp-cofx - {:db (assoc-in db - [:transport/chats chat-id :resend?] - "contact-update") - :data-store/tx tx} - (protocol/send-with-pubkey {:chat-id chat-id - :payload this - :success-event success-event})))) - recipients)))) - -(defn remove-chat-filter - "Stops the filter for the given chat-id" - [chat-id {:keys [db]}] - (when-let [filter (get-in db [:transport/chats chat-id :filter])] - {:shh/remove-filter filter})) - -(defn init-chat - [chat-id topic cofx] - (when-not (get-in cofx [:db :transport/chats chat-id]) - (protocol/init-chat {:chat-id chat-id - :topic topic} cofx))) - -(defrecord NewContactKey [sym-key topic message] - message/StatusMessage - (send [this chat-id cofx] - (let [success-event [:transport/set-contact-message-envelope-hash chat-id]] - (protocol/send-with-pubkey {:chat-id chat-id - :payload this - :success-event success-event} - cofx))) - (receive [this chat-id _ timestamp {:keys [db] :as cofx}] - (let [current-sym-key (get-in db [:transport/chats chat-id :sym-key]) - ;; NOTE(yenda) to support concurrent contact request without additional - ;; interactions we don't save the new key if these conditions are met: - ;; - the message is a contact request - ;; - we already have a sym-key - ;; - this sym-key is first in alphabetical order compared to the new one - save-key? (not (and (= ContactRequest (type message)) - current-sym-key - (= current-sym-key - (first (sort [current-sym-key sym-key])))))] - (if save-key? - (let [on-success (fn [sym-key sym-key-id] - (re-frame/dispatch [:contact/add-new-sym-key - {:sym-key-id sym-key-id - :timestamp timestamp - :sym-key sym-key - :chat-id chat-id - :topic topic - :message message}]))] - (handlers-macro/merge-fx cofx - {:shh/add-new-sym-keys [{:web3 (:web3 db) - :sym-key sym-key - :on-success on-success}]} - (init-chat chat-id topic) - ;; in case of concurrent contact request we want - ;; to stop the filter for the previous key before - ;; dereferrencing it - (remove-chat-filter chat-id))) - ;; if we don't save the key, we read the message directly - (message/receive message chat-id chat-id timestamp cofx))))) diff --git a/src/status_im/transport/message/v1/group_chat.cljs b/src/status_im/transport/message/v1/group_chat.cljs deleted file mode 100644 index 8cfbb145817..00000000000 --- a/src/status_im/transport/message/v1/group_chat.cljs +++ /dev/null @@ -1,71 +0,0 @@ -(ns ^{:doc "Group chat API"} - status-im.transport.message.v1.group-chat - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.transport.message.core :as message] - [status-im.transport.message.v1.protocol :as protocol] - [status-im.transport.utils :as transport.utils])) - -;; NOTE: We ignore the chat-id from the send and receive method. -;; The chat-id is usually deduced from the filter the message comes from but not in that case because it is sent -;; individually to each participant of the group. -;; In order to be able to determine which group the message belongs to the chat-id is therefore -;; passed in the message itself -(defrecord NewGroupKey [chat-id sym-key message] - message/StatusMessage - (send [this _ cofx] - (let [public-keys (get-in cofx [:db :chats chat-id :contacts])] - (protocol/multi-send-by-pubkey {:public-keys public-keys - :chat-id chat-id - :payload this} - cofx))) - (receive [this _ signature timestamp {:keys [db] :as cofx}] - (handlers-macro/merge-fx - cofx - {:shh/add-new-sym-keys - [{:web3 (:web3 db) - :sym-key sym-key - :on-success (fn [sym-key sym-key-id] - (re-frame/dispatch - [:group/add-new-sym-key - {:chat-id chat-id - :signature signature - :timestamp timestamp - :sym-key sym-key - :sym-key-id sym-key-id - :message message}]))}]} - (protocol/init-chat {:chat-id chat-id})))) - -(defn- user-is-group-admin? [chat-id cofx] - (= (get-in cofx [:db :chats chat-id :group-admin]) - (get-in cofx [:db :current-public-key]))) - -(defn send-new-group-key [message chat-id cofx] - (when (user-is-group-admin? chat-id cofx) - {:shh/get-new-sym-keys [{:web3 (get-in cofx [:db :web3]) - :on-success (fn [sym-key sym-key-id] - (re-frame/dispatch - [:group/send-new-sym-key - {:chat-id chat-id - :sym-key sym-key - :sym-key-id sym-key-id - :message message}]))}]})) - -(defn- init-chat-if-new [chat-id cofx] - (if (nil? (get-in cofx [:db :transport/chats chat-id])) - (protocol/init-chat {:chat-id chat-id} cofx))) - -(defrecord GroupAdminUpdate [chat-name participants] - message/StatusMessage - (send [this chat-id cofx] - (handlers-macro/merge-fx cofx - (init-chat-if-new chat-id) - (send-new-group-key this chat-id)))) - -(defrecord GroupLeave [] - message/StatusMessage - (send [this chat-id cofx] - (protocol/send {:chat-id chat-id - :payload this - :success-event [:group/unsubscribe-from-chat chat-id]} - cofx))) diff --git a/src/status_im/transport/message/v1/protocol.cljs b/src/status_im/transport/message/v1/protocol.cljs deleted file mode 100644 index f9ab6ba555b..00000000000 --- a/src/status_im/transport/message/v1/protocol.cljs +++ /dev/null @@ -1,116 +0,0 @@ -(ns ^{:doc "Protocol API and protocol utils"} - status-im.transport.message.v1.protocol - (:require [status-im.utils.config :as config] - [status-im.constants :as constants] - [status-im.chat.core :as chat] - [status-im.transport.db :as transport.db] - [status-im.transport.message.core :as message] - [status-im.transport.utils :as transport.utils])) - -(def ^:private whisper-opts - {:ttl 10 ;; ttl of 10 sec - :powTarget config/pow-target - :powTime config/pow-time}) - -(defn init-chat - "Initialises chat on protocol layer. - If topic is not passed as argument it is derived from `chat-id`" - [{:keys [chat-id topic resend?] - :or {topic (transport.utils/get-topic chat-id)}} - {:keys [db]}] - {:db (assoc-in db - [:transport/chats chat-id] - (transport.db/create-chat {:topic topic - :resend? resend?}))}) - -#_(defn requires-ack [message-id chat-id {:keys [db] :as cofx}] - {:db (update-in db [:transport/chats chat-id :pending-ack] conj message-id)}) - -#_(defn ack [message-id chat-id {:keys [db] :as cofx}] - {:db (update-in db [:transport/chats chat-id :ack] conj message-id)}) - -(defn send - "Sends the payload using symetric key and topic from db (looked up by `chat-id`)" - [{:keys [payload chat-id success-event]} {:keys [db] :as cofx}] - ;; we assume that the chat contains the contact public-key - (let [{:keys [current-public-key web3]} db - {:keys [sym-key-id topic]} (get-in db [:transport/chats chat-id])] - {:shh/post [{:web3 web3 - :success-event success-event - :message (merge {:sig current-public-key - :symKeyID sym-key-id - :payload payload - :topic topic} - whisper-opts)}]})) - -(defn send-with-pubkey - "Sends the payload using asymetric key (`:current-public-key` in db) and fixed discovery topic" - [{:keys [payload chat-id success-event]} {:keys [db] :as cofx}] - (let [{:keys [current-public-key web3]} db] - {:shh/post [{:web3 web3 - :success-event success-event - :message (merge {:sig current-public-key - :pubKey chat-id - :payload payload - :topic (transport.utils/get-topic constants/contact-discovery)} - whisper-opts)}]})) - -(defn- prepare-recipients [public-keys db] - (map (fn [public-key] - (select-keys (get-in db [:transport/chats public-key]) [:topic :sym-key-id])) - public-keys)) - -(defn multi-send-by-pubkey - "Sends payload to multiple participants selected by `:public-keys` key. " - [{:keys [payload public-keys success-event]} {:keys [db] :as cofx}] - (let [{:keys [current-public-key web3]} db - recipients (prepare-recipients public-keys db)] - {:shh/multi-post {:web3 web3 - :success-event success-event - :recipients recipients - :message (merge {:sig current-public-key - :payload payload} - whisper-opts)}})) - -;; TODO currently not used -(defrecord Ack [message-ids] - message/StatusMessage - (send [this cofx chat-id]) - (receive [this chat-id sig timestamp cofx])) - -(defrecord Seen [message-ids] - message/StatusMessage - (send [this cofx chat-id]) - (receive [this chat-id sig timestamp cofx])) - -(defrecord Message [content content-type message-type clock-value timestamp] - message/StatusMessage - (send [this chat-id cofx] - (let [params {:chat-id chat-id - :payload this - :success-event [:transport/set-message-envelope-hash - chat-id - (transport.utils/message-id this) - message-type]} - group-chat (get-in cofx [:db :chats chat-id :group-chat])] - (if (or group-chat - config/use-sym-key) - (send params cofx) - (send-with-pubkey params cofx)))) - (receive [this chat-id signature _ cofx] - {:chat-received-message/add-fx - [(assoc (into {} this) - :message-id (transport.utils/message-id this) - :show? true - :chat-id chat-id - :from signature - :js-obj (:js-obj cofx))]})) - -(defrecord MessagesSeen [message-ids] - message/StatusMessage - (send [this chat-id cofx] - (send {:chat-id chat-id - :payload this} - cofx)) - (receive [this chat-id signature _ cofx] - (chat/receive-seen chat-id signature this cofx))) diff --git a/src/status_im/transport/message/v1/public_chat.cljs b/src/status_im/transport/message/v1/public_chat.cljs deleted file mode 100644 index e417bd0c7a3..00000000000 --- a/src/status_im/transport/message/v1/public_chat.cljs +++ /dev/null @@ -1,44 +0,0 @@ -(ns ^{:doc "Public chat API"} - status-im.transport.message.v1.public-chat - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.transport.message.core :as message] - [status-im.transport.message.v1.protocol :as protocol] - [status-im.transport.utils :as transport.utils] - [status-im.data-store.transport :as transport-store])) - -(defn- has-already-joined? [chat-id {:keys [db]}] - (get-in db [:transport/chats chat-id])) - -(defn join-public-chat - "Function producing all protocol level effects necessary for joining public chat identified by chat-id" - [chat-id {:keys [db] :as cofx}] - (when-not (has-already-joined? chat-id cofx) - (let [on-success (fn [sym-key sym-key-id] - (re-frame/dispatch [::add-new-sym-key {:chat-id chat-id - :sym-key sym-key - :sym-key-id sym-key-id}]))] - (handlers-macro/merge-fx cofx - {:shh/generate-sym-key-from-password {:web3 (:web3 db) - :password chat-id - :on-success on-success}} - (protocol/init-chat {:chat-id chat-id}))))) - -(handlers/register-handler-fx - ::add-new-sym-key - (fn [{:keys [db] :as cofx} [_ {:keys [sym-key-id sym-key chat-id]}]] - (let [{:keys [web3]} db - topic (transport.utils/get-topic chat-id)] - {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) - :shh/add-filter {:web3 web3 - :sym-key-id sym-key-id - :topic topic - :chat-id chat-id} - :data-store/tx [(transport-store/save-transport-tx - {:chat-id chat-id - :chat (-> (get-in db [:transport/chats chat-id]) - (assoc :sym-key-id sym-key-id) - ;;TODO (yenda) remove once go implements persistence - (assoc :sym-key sym-key))})] - :dispatch [:inbox/request-chat-history chat-id]}))) diff --git a/src/status_im/transport/shh.cljs b/src/status_im/transport/shh.cljs deleted file mode 100644 index 12f14a5ba93..00000000000 --- a/src/status_im/transport/shh.cljs +++ /dev/null @@ -1,174 +0,0 @@ -(ns ^{:doc "Whisper API and events for managing keys and posting messages"} - status-im.transport.shh - (:require [taoensso.timbre :as log] - [re-frame.core :as re-frame] - [status-im.transport.utils :as transport.utils] - [status-im.transport.message.transit :as transit])) - -(defn get-new-key-pair [{:keys [web3 on-success on-error]}] - (if web3 - (.. web3 - -shh - (newKeyPair (fn [err resp] - (if-not err - (on-success resp) - (on-error err))))) - (on-error "web3 not available."))) - -(re-frame/reg-fx - :shh/get-new-key-pair - (fn [{:keys [web3 success-event error-event]}] - (get-new-key-pair {:web3 web3 - :on-success #(re-frame/dispatch [success-event %]) - :on-error #(re-frame/dispatch [error-event %])}))) - -(defn get-public-key [{:keys [web3 key-pair-id on-success on-error]}] - (if (and web3 key-pair-id) - (.. web3 - -shh - (getPublicKey key-pair-id (fn [err resp] - (if-not err - (on-success resp) - (on-error err))))) - (on-error "web3 or key-pair id not available."))) - -(re-frame/reg-fx - :shh/get-public-key - (fn [{:keys [web3 key-pair-id success-event error-event]}] - (get-public-key {:web3 web3 - :key-pair-id key-pair-id - :on-success #(re-frame/dispatch [success-event %]) - :on-error #(re-frame/dispatch [error-event %])}))) - -(defn generate-sym-key-from-password - [{:keys [web3 password on-success on-error]}] - (.. web3 - -shh - (generateSymKeyFromPassword password (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))))) - -(defn post-message - [{:keys [web3 whisper-message on-success on-error]}] - (.. web3 - -shh - (extPost (clj->js whisper-message) (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))))) - -(re-frame/reg-fx - :shh/post - (fn [post-calls] - (doseq [{:keys [web3 message success-event error-event] - :or {error-event :protocol/send-status-message-error}} post-calls] - (post-message {:web3 web3 - :whisper-message (update message :payload (comp transport.utils/from-utf8 - transit/serialize)) - :on-success (if success-event - #(re-frame/dispatch (conj success-event %)) - #(log/debug :shh/post-success)) - :on-error #(re-frame/dispatch [error-event %])})))) - -;; This event params contain a recipients key because it's a vector of map with public-key and topic keys. -;; the :shh/post event has public-key and topic keys at the top level of the args map. -;; This event is used to send messages to multiple recipients when you can't send it on a topic. -;; It is used for renewing keys in a private group chat, because if someone leaves/join. -;; We want to change the symmetric key but we can't do that in the group topic with the old key -;; otherwise leavers can still eavesdrop / joiners can read past history." -(re-frame/reg-fx - :shh/multi-post - (fn [{:keys [web3 message recipients success-event error-event] - :or {error-event :protocol/send-status-message-error}}] - (let [whisper-message (update message :payload (comp transport.utils/from-utf8 - transit/serialize))] - (doseq [{:keys [sym-key-id topic]} recipients] - (post-message {:web3 web3 - :whisper-message (assoc whisper-message - :topic topic - :symKeyID sym-key-id) - :on-success (if success-event - #(re-frame/dispatch success-event) - #(log/debug :shh/post-success)) - :on-error #(re-frame/dispatch [error-event %])}))))) - -(defn add-sym-key - [{:keys [web3 sym-key on-success on-error]}] - (.. web3 - -shh - (addSymKey sym-key (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))))) - -(defn get-sym-key - [{:keys [web3 sym-key-id on-success on-error]}] - (.. web3 - -shh - (getSymKey sym-key-id (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))))) - -(defn new-sym-key - [{:keys [web3 on-success on-error]}] - (.. web3 - -shh - (newSymKey (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))))) - -(defn log-error [error] - (log/error :shh/get-new-sym-key-error error)) - -;;TODO (yenda) remove once go implements persistence -(re-frame/reg-fx - :shh/restore-sym-keys - (fn [{:keys [web3 transport on-success]}] - (doseq [[chat-id {:keys [sym-key]}] transport] - (add-sym-key {:web3 web3 - :sym-key sym-key - :on-success (fn [sym-key-id] - (on-success chat-id sym-key sym-key-id)) - :on-error log-error})))) - -(defn add-new-sym-key [{:keys [web3 sym-key on-success]}] - (add-sym-key {:web3 web3 - :sym-key sym-key - :on-success (fn [sym-key-id] - (on-success sym-key sym-key-id)) - :on-error log-error})) - -(re-frame/reg-fx - :shh/add-new-sym-keys - (fn [args] - (doseq [add-new-sym-key-params args] - (add-new-sym-key add-new-sym-key-params)))) - -(re-frame/reg-fx - :shh/get-new-sym-keys - (fn [args] - (doseq [{:keys [web3 on-success]} args] - (new-sym-key {:web3 web3 - :on-success (fn [sym-key-id] - (get-sym-key {:web3 web3 - :sym-key-id sym-key-id - :on-success (fn [sym-key] - (on-success sym-key sym-key-id)) - :on-error log-error})) - :on-error log-error})))) - -(re-frame/reg-fx - :shh/generate-sym-key-from-password - (fn [{:keys [web3 password on-success]}] - (generate-sym-key-from-password {:web3 web3 - :password password - :on-success (fn [sym-key-id] - (get-sym-key {:web3 web3 - :sym-key-id sym-key-id - :on-success (fn [sym-key] - (on-success sym-key sym-key-id)) - :on-error log-error})) - :on-error log-error}))) diff --git a/src/status_im/transport/utils.cljs b/src/status_im/transport/utils.cljs deleted file mode 100644 index 3513872505c..00000000000 --- a/src/status_im/transport/utils.cljs +++ /dev/null @@ -1,51 +0,0 @@ -(ns ^{:doc "Utils for transport layer"} - status-im.transport.utils - (:require [cljs-time.coerce :refer [to-long]] - [cljs-time.core :refer [now]] - [clojure.string :as string] - [status-im.js-dependencies :as dependencies] - [status-im.data-store.transport :as transport-store])) - -(defn unsubscribe-from-chat - "Unsubscribe from chat on transport layer" - [chat-id {:keys [db]}] - (let [filter (get-in db [:transport/chats chat-id :filter])] - {:db (update db :transport/chats dissoc chat-id) - :data-store/tx [(transport-store/delete-transport-tx chat-id)] - :shh/remove-filter filter})) - -(defn from-utf8 [s] - (.fromUtf8 dependencies/Web3.prototype s)) - -(defn to-ascii [s] - (.toAscii dependencies/Web3.prototype s)) - -(defn to-utf8 [s] - (try - (.toUtf8 dependencies/Web3.prototype (str s)) - (catch :default err nil))) - -(defn sha3 [s] - (.sha3 dependencies/Web3.prototype s)) - -(defn message-id - "Get a message-id" - [message] - (sha3 (pr-str message))) - -(defn get-topic - "Get the topic of a group chat or public chat from the chat-id" - [chat-id] - (subs (sha3 chat-id) 0 10)) - -(defn shh [web3] - (.-shh web3)) - -(defn extract-enode-id [enode] - (-> enode - (string/split #"/") - (get 2 "") - (string/split #":") - (get 0 "") - (string/split "@") - (get 0))) diff --git a/src/status_im/ui/README.md b/src/status_im/ui/README.md new file mode 100644 index 00000000000..9ef6b0f9b5b --- /dev/null +++ b/src/status_im/ui/README.md @@ -0,0 +1,15 @@ +## UI layer + +### Description +This folder contains "ui" layer. +Here belongs all the code responsible for displaying UI and handling user input. +Only stores state that of active UI. + + +### Responsibilities +- displaying UI +- providing convenient subscriptions for UI elements + +### Rules +- UI layer does not contain logic beyond rendering. +( diff --git a/src/status_im/ui/components/action_button/action_button.cljs b/src/status_im/ui/components/action_button/action_button.cljs deleted file mode 100644 index 4b095bcb5e4..00000000000 --- a/src/status_im/ui/components/action_button/action_button.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.ui.components.action-button.action-button - (:require [status-im.ui.components.action-button.styles :as st] - [status-im.ui.components.common.common :refer [list-separator]] - [status-im.ui.components.icons.vector-icons :as vi] - [status-im.ui.components.react :as rn])) - -(defn action-button [{:keys [label accessibility-label icon icon-opts on-press label-style cyrcle-color]}] - [rn/touchable-highlight (merge {:on-press on-press} - (when accessibility-label - {:accessibility-label accessibility-label})) - [rn/view st/action-button - [rn/view (st/action-button-icon-container cyrcle-color) - [vi/icon icon icon-opts]] - [rn/view st/action-button-label-container - [rn/text {:style (merge st/action-button-label label-style)} - label]]]]) - -(defn action-button-disabled [{:keys [label icon]}] - [rn/view st/action-button - [rn/view st/action-button-icon-container-disabled - [rn/view {:opacity 0.4} - [vi/icon icon]]] - [rn/view st/action-button-label-container - [rn/text {:style st/action-button-label-disabled} - label]]]) - -(defn action-separator [] - [list-separator]) diff --git a/src/status_im/ui/components/action_button/styles.cljs b/src/status_im/ui/components/action_button/styles.cljs deleted file mode 100644 index a4699b46d3d..00000000000 --- a/src/status_im/ui/components/action_button/styles.cljs +++ /dev/null @@ -1,45 +0,0 @@ -(ns status-im.ui.components.action-button.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.ui.components.colors :as colors])) - -(defstyle action-button - {:padding-left 16 - :flex-direction :row - :align-items :center - :ios {:height 63} - :android {:height 56}}) - -(defnstyle action-button-icon-container [circle-color] - {:border-radius 50 - :width 40 - :height 40 - :align-items :center - :justify-content :center - :background-color (or circle-color styles/color-light-blue-transparent)}) - -(def action-button-label-container - {:padding-left 16}) - -(def action-button-label - {:color colors/blue - :letter-spacing -0.2 - :font-size 16}) - -(defstyle actions-list - {:background-color styles/color-white - :android {:padding-top 8 - :padding-bottom 8}}) - -(def action-button-label-disabled - (merge action-button-label - {:color styles/color-gray4})) - -(defstyle action-button-icon-container-disabled - {:border-radius 50 - :width 40 - :height 40 - :align-items :center - :justify-content :center - :background-color styles/color-light-gray}) - diff --git a/src/status_im/ui/components/action_sheet.cljs b/src/status_im/ui/components/action_sheet.cljs deleted file mode 100644 index 1a1dd31d257..00000000000 --- a/src/status_im/ui/components/action_sheet.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.ui.components.action-sheet - (:require [status-im.i18n :as i18n] - [status-im.utils.core :as utils] - [status-im.react-native.js-dependencies :as js-dependencies])) - -(defn- callback [options] - (fn [index] - (when (< index (count options)) - (when-let [handler (:action (nth options index))] - (handler))))) - -(defn- prepare-options [title message options] - (let [destructive-opt-index (utils/first-index :destructive? options)] ;; TODO Can only be a single destructive? - (clj->js (merge {:options (conj (mapv :label options) (i18n/label :t/cancel)) - :cancelButtonIndex (count options)} - (when destructive-opt-index - {:destructiveButtonIndex destructive-opt-index}) - (when title {:title title}) - (when message {:message message}))))) - -(defn show [{:keys [title message options]}] - (.showActionSheetWithOptions (.-ActionSheetIOS js-dependencies/react-native) - (prepare-options title message options) - (callback options))) diff --git a/src/status_im/ui/components/animation.cljs b/src/status_im/ui/components/animation.cljs deleted file mode 100644 index 270e7e21f14..00000000000 --- a/src/status_im/ui/components/animation.cljs +++ /dev/null @@ -1,53 +0,0 @@ -(ns status-im.ui.components.animation - (:require [status-im.ui.components.react :as react])) - -(defn start - ([anim] (.start anim)) - ([anim callback] (.start anim callback))) - -(defn timing [anim-value config] - (.timing react/animated anim-value (clj->js config))) - -(defn spring [anim-value config] - (.spring react/animated anim-value (clj->js config))) - -(defn decay [anim-value config] - (.decay react/animated anim-value (clj->js config))) - -(defn anim-sequence [animations] - (.sequence react/animated (clj->js animations))) - -(defn parallel [animations] - (.parallel react/animated (clj->js animations))) - -(defn anim-delay [duration] - (.delay react/animated duration)) - -(defn event [config] - (.event react/animated (clj->js [nil, config]))) - -(defn add-listener [anim-value listener] - (.addListener anim-value listener)) - -(defn remove-all-listeners [anim-value] - (.removeAllListeners anim-value)) - -(defn stop-animation [anim-value] - (.stopAnimation anim-value)) - -(defn set-value [anim-value value] - (.setValue anim-value value)) - -(defn create-value [value] - (js/ReactNative.Animated.Value. value)) - -(defn x [value-xy] - (.-x value-xy)) - -(defn y [value-xy] - (.-y value-xy)) - -(defn get-layout [value-xy] - (js->clj (.getLayout value-xy))) - -(defn easing [] js/ReactNative.Easing) \ No newline at end of file diff --git a/src/status_im/ui/components/bottom_buttons/styles.cljs b/src/status_im/ui/components/bottom_buttons/styles.cljs deleted file mode 100644 index 34a258aea2c..00000000000 --- a/src/status_im/ui/components/bottom_buttons/styles.cljs +++ /dev/null @@ -1,23 +0,0 @@ -(ns status-im.ui.components.bottom-buttons.styles - (:require [status-im.ui.components.colors :as colors])) - -(def wrapper - {:position :absolute - :bottom 0 - :left 0 - :right 0}) - -(def border - {:margin-horizontal 16 - :border-top-width 1 - :border-color colors/white-light-transparent}) - -(def container - {:flex-direction :row - :align-items :center - :justify-content :space-between}) - -(def container-single - {:flex-direction :row - :align-items :center - :justify-content :center}) diff --git a/src/status_im/ui/components/bottom_buttons/view.cljs b/src/status_im/ui/components/bottom_buttons/view.cljs deleted file mode 100644 index 62d7a27d383..00000000000 --- a/src/status_im/ui/components/bottom_buttons/view.cljs +++ /dev/null @@ -1,21 +0,0 @@ -(ns status-im.ui.components.bottom-buttons.view - (:require [status-im.ui.components.bottom-buttons.styles :as styles] - [status-im.ui.components.react :as react])) - -(defn bottom-button - ([button] (bottom-button nil button)) - ([style button] - [react/view styles/wrapper - [react/view styles/border] - [react/view (merge styles/container-single style) - button]])) - -(defn bottom-buttons - ([left right] (bottom-buttons nil left right)) - ([style left right] - [react/view styles/wrapper - [react/view styles/border] - [react/view (merge styles/container style) - left - [react/view {:flex 1}] - right]])) diff --git a/src/status_im/ui/components/button/styles.cljs b/src/status_im/ui/components/button/styles.cljs deleted file mode 100644 index 64c63cf5478..00000000000 --- a/src/status_im/ui/components/button/styles.cljs +++ /dev/null @@ -1,73 +0,0 @@ -(ns status-im.ui.components.button.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.utils.platform :as platform])) - -(def border-color styles/color-white-transparent-3) -(def border-color-high styles/color-white-transparent-4) - -(def buttons-container {:flex-direction :row}) - -(def button-container styles/flex) - -(def button - {:flex-direction :row - :justify-content :center - :align-items :center - :padding-horizontal 12}) - -(defn- border [position] - (let [radius (if platform/ios? 8 4)] - (case position - :first {:border-bottom-left-radius radius - :border-top-left-radius radius - :ios {:border-top-width 1 - :border-left-width 1 - :border-bottom-width 1}} - :last {:border-bottom-right-radius radius - :border-top-right-radius radius - :ios {:border-top-width 1 - :border-right-width 1 - :border-bottom-width 1}} - {:android {:border-left-width 1 - :border-right-width 1} - :ios {:border-width 1}}))) - -(defnstyle button-bar [position] - (merge {:border-color border-color} - (border position))) - -(defstyle button-text - {:font-weight :normal - :color styles/color-white - :padding-horizontal 16 - :android {:font-size 14 - :padding-vertical 10 - :letter-spacing 0.5} - :ios {:font-size 15 - :padding-vertical 9 - :letter-spacing -0.2}}) - -(defstyle button-text-disabled - {:android {:opacity 0.4} - :ios {:opacity 0.6}}) - -(defstyle button-borders - {:border-radius 8 - :ios {;; Border radius is ignored with transparent background unless overflow "hidden" is used - ;; See https://github.com/facebook/react-native/issues/13760 - :overflow :hidden}}) - -(def primary-button - (merge - button-borders - {:background-color styles/color-blue4})) - -(def primary-button-text {:color styles/color-white}) - -(def secondary-button - (merge - button-borders - {:background-color styles/color-blue4-transparent})) - -(def secondary-button-text {:color styles/color-blue4}) diff --git a/src/status_im/ui/components/button/view.cljs b/src/status_im/ui/components/button/view.cljs deleted file mode 100644 index a357d19622c..00000000000 --- a/src/status_im/ui/components/button/view.cljs +++ /dev/null @@ -1,35 +0,0 @@ -(ns status-im.ui.components.button.view - (:require [status-im.ui.components.button.styles :as styles] - [status-im.ui.components.react :as react] - [status-im.utils.platform :as platform])) - -(defn button [{:keys [on-press style disabled? fit-to-text? text-style accessibility-label] :or {fit-to-text? true}} label icon] - [react/touchable-highlight (merge {:underlay-color styles/border-color-high} - (when-not fit-to-text? - {:style styles/button-container}) - (when (and on-press (not disabled?)) - {:on-press on-press}) - (when accessibility-label - {:accessibility-label accessibility-label})) - [react/view {:style (merge styles/button - style)} - [react/text {:style (merge styles/button-text - text-style - (when disabled? - {:opacity 0.4})) - :font (if platform/android? :medium :default) - :uppercase? true} - label] - icon]]) - -(defn primary-button [{:keys [style text-style] :as m} label] - [button (assoc m - :style (merge styles/primary-button style) - :text-style (merge styles/primary-button-text text-style)) - label]) - -(defn secondary-button [{:keys [style text-style] :as m} label] - [button (assoc m - :style (merge styles/secondary-button style) - :text-style (merge styles/secondary-button-text text-style)) - label]) diff --git a/src/status_im/ui/components/camera.cljs b/src/status_im/ui/components/camera.cljs deleted file mode 100644 index 9dffd936eb3..00000000000 --- a/src/status_im/ui/components/camera.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.ui.components.camera - (:require [goog.object :as object] - [reagent.core :as reagent] - [clojure.walk :as walk] - [status-im.react-native.js-dependencies :as js-dependecies])) - -(def default-camera (.-default js-dependecies/camera)) - -(defn constants [t] - (-> js-dependecies/camera - (object/get "constants") - (object/get t) - (js->clj) - (walk/keywordize-keys))) - -(def aspects (constants "Aspect")) -(def capture-targets (constants "CaptureTarget")) -(def torch-modes (constants "TorchMode")) - -(defn set-torch [state] - (set! (.-torchMode default-camera) (get torch-modes state))) - -(defn request-access-ios [then else] - (-> (.checkVideoAuthorizationStatus default-camera) - (.then (fn [allowed?] (if allowed? (then) (else)))) - (.catch else))) - -(defn camera [props] - (reagent/create-element default-camera (clj->js (merge {:inverted true} props)))) - -(defn get-qr-code-data [code] - (.-data code)) diff --git a/src/status_im/ui/components/chat_icon/screen.cljs b/src/status_im/ui/components/chat_icon/screen.cljs deleted file mode 100644 index 99c7c433d03..00000000000 --- a/src/status_im/ui/components/chat_icon/screen.cljs +++ /dev/null @@ -1,175 +0,0 @@ -(ns status-im.ui.components.chat-icon.screen - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.chat-icon.styles :as styles] - [status-im.ui.components.styles :as components.styles] - [status-im.chat.views.photos :as photos] - [status-im.react-native.resources :as resources])) - -(defn default-chat-icon [name styles] - [react/view (:default-chat-icon styles) - [react/text {:style (:default-chat-icon-text styles)} - (string/capitalize (first name))]]) - -(defn dapp-badge [{:keys [online-view-wrapper online-view online-dot-left online-dot-right]}] - [react/view online-view-wrapper - [react/view online-view - [react/view - [react/view online-dot-left] - [react/view online-dot-right]]]]) - -(defview pending-contact-badge - [chat-id {:keys [pending-wrapper pending-outer-circle pending-inner-circle]}] - (letsubs [pending-contact? [:get-in [:contacts/contacts chat-id :pending?]]] - (when pending-contact? - [react/view pending-wrapper - [react/view pending-outer-circle - [react/view pending-inner-circle]]]))) - -(defview chat-icon-view [chat-id _group-chat name _online styles & [hide-dapp?]] - (letsubs [photo-path [:get-chat-photo chat-id] - dapp? [:get-in [:contacts/contacts chat-id :dapp?]]] - [react/view (:container styles) - (if-not (string/blank? photo-path) - [photos/photo photo-path styles] - [default-chat-icon name styles]) - (when (and dapp? (not hide-dapp?)) - [dapp-badge styles]) - [pending-contact-badge chat-id styles]])) - -(defn chat-icon-view-toolbar [chat-id group-chat name color online] - [chat-icon-view chat-id group-chat name online - {:container styles/container-chat-list - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right - :pending-wrapper styles/pending-wrapper - :pending-outer-circle styles/pending-outer-circle - :pending-inner-circle styles/pending-inner-circle - :size 40 - :chat-icon styles/chat-icon-chat-list - :default-chat-icon (styles/default-chat-icon-chat-list color) - :default-chat-icon-text styles/default-chat-icon-text}]) - -(defn chat-icon-view-chat-list [chat-id group-chat name color online & [hide-dapp?]] - [chat-icon-view chat-id group-chat name online - {:container styles/container-chat-list - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right - :pending-wrapper styles/pending-wrapper - :pending-outer-circle styles/pending-outer-circle - :pending-inner-circle styles/pending-inner-circle - :size 40 - :chat-icon styles/chat-icon-chat-list - :default-chat-icon (styles/default-chat-icon-chat-list color) - :default-chat-icon-text styles/default-chat-icon-text} - hide-dapp?]) - -(defn chat-icon-view-action [chat-id group-chat name online] - ^{:key chat-id} - [chat-icon-view chat-id group-chat name online - {:container styles/container-chat-list - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right - :size 40 - :chat-icon styles/chat-icon-chat-list - :default-chat-icon (styles/default-chat-icon-chat-list components.styles/default-chat-color) - :default-chat-icon-text styles/default-chat-icon-text}]) - -(defn chat-icon-view-menu-item [chat-id group-chat name color online] - ^{:key chat-id} - [chat-icon-view chat-id group-chat name online - {:container styles/container-menu-item - :online-view-wrapper styles/online-view-menu-wrapper - :online-view styles/online-view-menu-item - :online-dot-left styles/online-dot-left-menu-item - :online-dot-right styles/online-dot-right-menu-item - :pending-wrapper styles/pending-view-menu-wrapper - :pending-outer-circle styles/pending-outer-circle - :pending-inner-circle styles/pending-inner-circle - :size 24 - :chat-icon styles/chat-icon-menu-item - :default-chat-icon (styles/default-chat-icon-view-action color) - :default-chat-icon-text styles/default-chat-icon-text} - true]) - -(defn chat-icon-message-status [chat-id group-chat name color online] - ^{:key chat-id} - [chat-icon-view chat-id group-chat name online - {:container styles/container-message-status - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right - :pending-wrapper styles/pending-wrapper - :pending-outer-circle styles/pending-outer-circle - :pending-inner-circle styles/pending-inner-circle - :size 64 - :chat-icon styles/chat-icon-message-status - :default-chat-icon (styles/default-chat-icon-message-status color) - :default-chat-icon-text styles/message-status-icon-text}]) - -(defn contact-icon-view [{:keys [photo-path name dapp?]} {:keys [container] :as styles}] - (let [photo-path photo-path] - [react/view container - (if-not (string/blank? photo-path) - [photos/photo photo-path styles] - [default-chat-icon name styles]) - (when dapp? - [dapp-badge styles])])) - -(defn contact-icon-contacts-tab [contact] - [contact-icon-view contact - {:container styles/container-chat-list - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right - :size 40 - :chat-icon styles/chat-icon-chat-list - :default-chat-icon (styles/default-chat-icon-chat-list components.styles/default-chat-color) - :default-chat-icon-text styles/default-chat-icon-text}]) - -(defn dapp-icon-browser [contact size] - [contact-icon-view contact - {:container {:width size :height size :top 3 :margin-left 2} - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right - :size size - :chat-icon (styles/custom-size-icon size) - :default-chat-icon (styles/default-chat-icon-chat-list components.styles/default-chat-color) - :default-chat-icon-text styles/default-chat-icon-text}]) - -(defn profile-icon-view [photo-path name color edit? size] - (let [styles {:container {:width size :height size} - :online-view styles/online-view-profile - :online-dot-left styles/online-dot-left-profile - :online-dot-right styles/online-dot-right-profile - :size size - :chat-icon styles/chat-icon-profile - :default-chat-icon (styles/default-chat-icon-profile color size) - :default-chat-icon-text styles/default-chat-icon-text}] - [react/view (:container styles) - (when edit? - [react/view (styles/profile-icon-mask size)]) - (when edit? - [react/view (styles/profile-icon-edit-text-containter size) - [react/i18n-text {:style styles/profile-icon-edit-text :key :edit}]]) - (if (and photo-path (seq photo-path)) - [photos/photo photo-path styles] - [default-chat-icon name styles])])) - -(defn my-profile-icon [{{:keys [photo-path name]} :account - edit? :edit?}] - (let [color components.styles/default-chat-color - size 56] - [profile-icon-view photo-path name color edit? size])) diff --git a/src/status_im/ui/components/chat_icon/styles.cljs b/src/status_im/ui/components/chat_icon/styles.cljs deleted file mode 100644 index 1fd735f8937..00000000000 --- a/src/status_im/ui/components/chat_icon/styles.cljs +++ /dev/null @@ -1,253 +0,0 @@ -(ns status-im.ui.components.chat-icon.styles - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :refer [online-color]])) - -(defn default-chat-icon [color] - {:margin 0 - :width 40 - :height 40 - :align-items :center - :justify-content :center - :border-radius 20 - :background-color color}) - -(defn default-chat-icon-chat-list [color] - (merge (default-chat-icon color) - {:width 40 - :height 40 - :border-radius 20})) - -(defn default-chat-icon-profile [color size] - (merge (default-chat-icon color) - {:width size - :height size - :border-radius (/ size 2)})) - -(defn default-chat-icon-view-action [color] - (merge (default-chat-icon color) - {:width 36 - :height 36 - :border-radius 18})) - -(defn default-chat-icon-message-status [color] - (merge (default-chat-icon color) - {:width 64 - :height 64 - :border-radius 32})) - -(def default-chat-icon-text - {:color colors/white - :font-size 20 - :opacity 0.8 - :line-height 24}) - -(def message-status-icon-text - {:margin-top -2 - :color colors/white - :font-size 24}) - -(def chat-icon - {:margin 4 - :border-radius 20 - :width 40 - :height 40}) - -(def chat-icon-chat-list - (merge chat-icon - {:width 40 - :height 40 - :margin 0})) - -(defn custom-size-icon [size] - (merge chat-icon - {:width size - :height size - :margin 0})) - -(def chat-icon-menu-item - {:width 24 - :height 24 - :border-radius 12}) - -(def chat-icon-profile - (merge chat-icon - {:width 64 - :height 64 - :border-radius 32})) - -(def chat-icon-view-action - (merge chat-icon - {:width 36 - :height 36 - :border-radius 18 - :margin 0})) - -(def chat-icon-message-status - {:border-radius 32 - :width 64 - :height 64}) - -(def online-view-wrapper - {:position :absolute - :bottom -2 - :right -2 - :width 17 - :height 17 - :border-radius 11 - :background-color :white}) - -(def online-view-menu-wrapper - {:position :absolute - :bottom 0 - :right -1 - :width 16 - :height 16 - :border-radius 8 - :background-color :white}) - -(def online-view - {:position :absolute - :bottom 2 - :right 2 - :width 13 - :height 13 - :border-radius 9 - :background-color online-color}) - -(def online-view-menu-item - (merge online-view - {:width 14 - :height 14 - :border-radius 7 - :bottom 1 - :right 1})) - -(def online-view-profile - (merge online-view - {:width 24 - :height 24 - :border-radius 12})) - -(def online-dot - {:position :absolute - :top 5 - :width 3 - :height 3 - :border-radius 2 - :background-color colors/white}) -(def online-dot-left (merge online-dot {:left 2.8})) -(def online-dot-right (merge online-dot {:left 7.2})) - -(def online-dot-menu-item - (merge online-dot - {:top 4 - :width 3 - :height 3})) -(def online-dot-left-menu-item - (merge online-dot-menu-item {:left 2.5 - :top 5})) -(def online-dot-right-menu-item - (merge online-dot-menu-item {:left 8 - :top 5})) - -(def online-dot-profile - (merge online-dot - {:top 8 - :width 4 - :height 4})) -(def online-dot-left-profile - (merge online-dot-profile {:left 5})) -(def online-dot-right-profile - (merge online-dot-profile {:left 11})) - -(def container - {:width 44 - :height 44}) - -(def container-chat-list - {:width 40 - :height 40}) - -(def container-menu-item - {:width 24 - :height 24}) - -(def container-profile - {:width 72 - :height 72}) - -(def container-message-status - {:margin-top 20}) - -(def default-image-style - {:margin 4}) - -(defn border-style [size] - {:width size - :height size - :border-radius (/ size 2) - :background-color :#b9c8d6 - :padding 0.5}) - -(defn image-style [size] - (let [image-size (dec size)] - {:width image-size - :height image-size - :border-radius (/ image-size 2)})) - -(def pending-wrapper - {:position :absolute - :bottom 0 - :right 0 - :width 12 - :height 12 - :border-radius 6 - :background-color :white - :align-items :center - :justify-content :center}) - -(def pending-view-menu-wrapper - {:position :absolute - :bottom 2 - :right 2 - :width 12 - :height 12 - :border-radius 6 - :background-color :white - :align-items :center - :justify-content :center}) - -(def pending-outer-circle - {:background-color online-color - :width 8 - :height 8 - :border-radius 4 - :align-items :center - :justify-content :center}) - -(def pending-inner-circle - {:background-color :white - :width 4 - :height 4 - :border-radius 2}) - -(defn profile-icon-mask [size] - {:height size - :width size - :position :absolute - :z-index 1 - :background-color :black - :opacity 0.4 - :border-radius 50}) - -(defn profile-icon-edit-text-containter [size] - {:height size - :width size - :position :absolute - :z-index 2 - :align-items :center - :justify-content :center}) - -(def profile-icon-edit-text - {:color :white - :background-color :transparent}) diff --git a/src/status_im/ui/components/chat_preview.cljs b/src/status_im/ui/components/chat_preview.cljs deleted file mode 100644 index 94a180638d1..00000000000 --- a/src/status_im/ui/components/chat_preview.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns status-im.ui.components.chat-preview - (:require [status-im.ui.components.react :as components] - [status-im.ui.screens.home.styles :as home.styles] - [status-im.utils.core :as utils])) - -(def default-attributes - {:style home.styles/last-message-text - :number-of-lines 1}) - -(defn text [attributes s] - (-> default-attributes - (utils/deep-merge attributes) - (components/text s))) diff --git a/src/status_im/ui/components/checkbox/styles.cljs b/src/status_im/ui/components/checkbox/styles.cljs deleted file mode 100644 index e2dbe427ee9..00000000000 --- a/src/status_im/ui/components/checkbox/styles.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.ui.components.checkbox.styles - (:require [status-im.ui.components.colors :as colors])) - -(def wrapper - {:padding 16}) - -(defn icon-check-container [checked?] - {:background-color (if checked? colors/blue colors/gray-lighter) - :align-items :center - :justify-content :center - :border-radius 2 - :width 24 - :height 24}) - -(defn icon-radio-container [checked?] - (merge (icon-check-container checked?) - {:border-radius 100 - :width 26 - :height 26})) - -(def check-icon - {:width 12 - :height 12}) - -(def plain-check-icon - (merge check-icon - {:tint-color colors/blue})) diff --git a/src/status_im/ui/components/checkbox/view.cljs b/src/status_im/ui/components/checkbox/view.cljs deleted file mode 100644 index 1f3eafeb09b..00000000000 --- a/src/status_im/ui/components/checkbox/view.cljs +++ /dev/null @@ -1,26 +0,0 @@ -(ns status-im.ui.components.checkbox.view - (:require [status-im.ui.components.checkbox.styles :as styles] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.utils.platform :as platform])) - -(defn- checkbox-generic [{:keys [on-value-change checked? accessibility-label] :or {accessibility-label :checkbox}} plain?] - (let [icon-check-container (if plain? #() styles/icon-check-container) - check-icon (if plain? styles/plain-check-icon styles/check-icon)] - (if platform/android? - [react/view styles/wrapper - [react/check-box {:on-value-change on-value-change - :value checked? - :accessibility-label accessibility-label}]] - [react/touchable-highlight (merge {:style styles/wrapper - :accessibility-label accessibility-label} - (when on-value-change {:on-press #(on-value-change (not checked?))})) - [react/view (icon-check-container checked?) - (when checked? - [react/icon :check_on check-icon])]]))) - -(defn checkbox [props] - [checkbox-generic props false]) - -(defn plain-checkbox [props] - [checkbox-generic props true]) diff --git a/src/status_im/ui/components/colors.cljs b/src/status_im/ui/components/colors.cljs deleted file mode 100644 index 3b75fc0802e..00000000000 --- a/src/status_im/ui/components/colors.cljs +++ /dev/null @@ -1,49 +0,0 @@ -(ns status-im.ui.components.colors - (:require [clojure.string :as string])) - -(def white "#ffffff") -(def transparent "transparent") -(def white-light-transparent "rgba(255, 255, 255, 0.1)") ;; Used as icon background color for a dark foreground -(def white-transparent "rgba(255, 255, 255, 0.2)") ;; Used as icon color on dark background -(def white-lighter-transparent "rgba(255, 255, 255, 0.6)") ;; Used for input placeholder color -(def black "#000000") ;; Used as the default text color -(def black-transparent "#00000020") ;; Used as background color for rounded button on dark background -(def black-darker-transparent "#00000033") ;; Used as background color for containers like "Backup seed phrase" -(def gray "#939ba1") ;; Used as a background for a light foreground and as section header and secondary text color -(def gray-icon "#6e777e") ;; Used for forward icon in accounts -(def gray-light "#e8ebec") ;; Used as divider color -(def gray-lighter "#eef2f5") ;; Used as a background or shadow -(def gray-transparent "rgba(184, 193, 199, 0.5)") ;; Used for tabs -(def gray-notifications "#4A5054cc") ;; Used for notifications background -(def gray-border "#ececf0") -(def blue "#4360df") ;; Used as main wallet color, and ios home add button -(def blue-transparent "rgba(67, 96, 223, 0.2)") -(def blue-transparent-40 "rgba(67, 96, 223, 0.4)") -(def blue-darker "#c4cced") -(def blue-dark "#3147ac") ;; Used as secondary wallet color (icon background) -(def hawkes-blue "#dce2fb") ;; Outgoing chat messages background -(def wild-blue-yonder "#707caf") ;; Text color for outgoing messages timestamp -(def red "#ff2d55") ;; Used to highlight errors or "dangerous" actions -(def red-light "#ffe5ea") ;; error tooltip -(def text-light-gray "#212121") ;; Used for labels (home items) -(def cyan "#7adcfb") ;; Used by wallet transaction filtering icon -(def photo-border-color "#ccd3d6") -(def green "#44d058") ;; icon for successful inboud transaction -(def tooltip-green-text "#66bf50") ;; fading tooltip text color -(def tooltip-green "#e9f6e6") ;; fading tooltip background color - -(def chat-colors ["#fa6565" - "#7cda00" - "#887af9" - "#51d0f0" - "#fe8f59" - "#d37ef4"]) - -(defn alpha [hex opacity] - (let [hex (string/replace hex #"#" "") - r (js/parseInt (subs hex 0 2) 16) - g (js/parseInt (subs hex 2 4) 16) - b (js/parseInt (subs hex 4 6) 16)] - (str "rgba(" r "," g "," b "," opacity ")"))) - -(def text black) diff --git a/src/status_im/ui/components/common/common.cljs b/src/status_im/ui/components/common/common.cljs deleted file mode 100644 index 589b4b254b7..00000000000 --- a/src/status_im/ui/components/common/common.cljs +++ /dev/null @@ -1,112 +0,0 @@ -(ns status-im.ui.components.common.common - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [reagent.core :as reagent] - [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.common.styles :as styles] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.react :as react] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.platform :as platform] - [status-im.ui.components.icons.vector-icons :as icons] - [status-im.ui.components.colors :as colors])) - -(defn top-shadow [] - (when platform/android? - [react/view])) - -(defn bottom-shadow [] - (when platform/android? - [react/view])) - -(defn separator [style & [wrapper-style]] - [react/view (merge styles/separator-wrapper wrapper-style) - [react/view (merge styles/separator style)]]) - -(defn list-separator [] - [separator styles/list-separator]) - -(defn list-footer [] - [react/view styles/list-header-footer-spacing]) - -(defn list-header [] - [react/view styles/list-header-footer-spacing]) - -(defn form-title [label & [{:keys [count-value]}]] - [react/view - [react/view styles/form-title-container - [react/view styles/form-title-inner-container - [react/text {:style styles/form-title - :font :medium} - label] - (when-not (nil? count-value) - [react/text {:style styles/form-title-count - :font :medium} - count-value])]] - [top-shadow]]) - -(defview network-info [{:keys [text-color]}] - (letsubs [network-id [:get-network-id]] - [react/view - [react/view styles/network-container - [react/view styles/network-icon - [vector-icons/icon :icons/network {:color :white}]] - [react/text {:style (styles/network-text text-color)} - (if (ethereum/testnet? network-id) - (i18n/label :t/testnet-text {:testnet (get-in ethereum/chains [(ethereum/chain-id->chain-keyword network-id) :name] "Unknown")}) - (i18n/label :t/mainnet-text))]]])) - -(defn logo - ([] (logo nil)) - ([{:keys [size icon-size shadow?] :or {shadow? true}}] - [react/view {:style (styles/logo-container size shadow?)} - [icons/icon :icons/logo (styles/logo icon-size)]])) - -(defn bottom-button [{:keys [label disabled? on-press forward?]}] - [react/touchable-highlight {:on-press on-press :disabled disabled?} - [react/view (styles/bottom-button disabled?) - [react/text {:style styles/bottom-button-label - :uppercase? true} - (or label (i18n/label :t/next))] - (when forward? - [icons/icon :icons/forward {:color colors/blue}])]]) - -(defn button [{:keys [on-press label background? uppercase? button-style label-style] :or {background? true uppercase? true}}] - [react/touchable-highlight {:on-press on-press} - [react/view {:style (styles/button button-style background?)} - [react/text {:uppercase? uppercase? - :style (merge styles/button-label label-style)} - label]]]) - -(defn counter - ([value] (counter nil value)) - ([{:keys [size accessibility-label] :or {size 18}} value] - (let [more-than-9 (> value 9)] - [react/view {:style (styles/counter-container size more-than-9)} - [react/text (cond-> {:style (styles/counter-label size) - :font :toolbar-title} - accessibility-label - (assoc :accessibility-label accessibility-label)) - (if more-than-9 (i18n/label :t/counter-9-plus) value)]]))) - -(defview image-contain [{:keys [container-style style]} {:keys [image width height]}] - (letsubs [content-width (reagent/atom 0) - {window-width :width window-height :height} [:dimensions/window]] - [react/view {:style (merge styles/image-contain container-style) - :on-layout #(reset! content-width (-> % .-nativeEvent .-layout .-width))} - [react/image {:source image - :resize-mode :contain - :style (merge style - (if (> window-height window-width) - {:width (* @content-width - (if (< window-height 600) - 0.6 - 1)) - :height (/ (* @content-width height - (if (< window-height 600) - 0.6 - 1)) - width)} - {:width @content-width - :height (* window-height 0.6)}))}]])) diff --git a/src/status_im/ui/components/common/styles.cljs b/src/status_im/ui/components/common/styles.cljs deleted file mode 100644 index da716571ba9..00000000000 --- a/src/status_im/ui/components/common/styles.cljs +++ /dev/null @@ -1,167 +0,0 @@ -(ns status-im.ui.components.common.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.ui.components.colors :as colors])) - -(def gradient-top - {:flex-direction :row - :height 3 - :background-color colors/gray-lighter}) - -(def gradient-top-colors - ["rgba(25, 53, 76, 0.01)" - "rgba(25, 53, 76, 0.1)"]) - -(def gradient-bottom - {:flex-direction :row - :height 2 - :background-color colors/gray-lighter}) - -(def gradient-bottom-colors - ["rgba(25, 53, 76, 0.1)" - "rgba(25, 53, 76, 0.01)"]) - -(def separator-wrapper - {:background-color colors/white}) - -(defstyle separator - {:android {:height 0} - :ios {:height 1 - :background-color colors/gray-light - :opacity 0.5}}) - -(def list-separator - {:margin-left 72}) - -(defstyle form-title-container - {:flex-direction :row}) - -(defstyle form-title-inner-container - {:padding-left 16 - :padding-right 16 - :flex 1 - :flex-direction :row - :ios {:background-color colors/white - :padding-top 19 - :padding-bottom 15 - :margin-top 16} - :android {:background-color colors/gray-lighter - :padding-top 20 - :padding-bottom 17 - :margin-top 8}}) - -(defstyle form-title - {:flex-shrink 1 - :ios {:color styles/text1-color - :letter-spacing -0.2 - :font-size 16} - :android {:color styles/text4-color - :font-size 14}}) - -(def form-title-count - (merge form-title - {:flex-shrink 0 - :opacity 0.6 - :padding-left 8 - :padding-right 5 - :color styles/text4-color})) - -(defstyle form-spacer - {:ios {:height 16} - :android {:height 11}}) - -(defstyle list-header-footer-spacing - {:android {:background-color colors/white - :height 8}}) - -(def network-container - {:flex-direction :row - :padding-horizontal 13 - :padding-vertical 11 - :align-items :center}) - -(defn network-text [text-color] - {:flex 1 - :color (or text-color colors/black) - :letter-spacing -0.2 - :font-size 14 - :margin-left 16}) - -(def network-icon - {:width 40 - :height 40 - :border-radius (/ 40 2) - :background-color styles/color-green-4 - :align-items :center - :justify-content :center}) - -(defstyle label-action-text - {:padding-right 16 - :color colors/blue - :ios {:font-size 15} - :android {:font-size 14}}) - -(defstyle logo-shaddow - {:ios {:shadowColor colors/black - :shadowOffset {:height 5} - :shadowRadius 10 - :shadowOpacity 0.14} - :android {:elevation 2}}) - -(defn logo-container [size shadow?] - (merge - {:width size - :height size - :border-radius size - :background-color colors/blue - :align-items :center - :justify-content :center} - (when shadow? - logo-shaddow))) - -(defn logo [icon-size] - {:color :white - :width icon-size - :height icon-size}) - -(defn bottom-button [disabled?] - {:flex-direction :row - :align-items :center - :opacity (if disabled? 0.4 1)}) - -(def bottom-button-label - {:font-size 15 - :letter-spacing -0.2 - :color colors/blue}) - -(defn button [style background?] - (merge - {:padding-vertical 12 - :padding-horizontal 42 - :border-radius 8 - :background-color (when background? - (colors/alpha colors/blue 0.1))} - style)) - -(def button-label - {:font-size 15 - :letter-spacing -0.2 - :text-align :center - :color colors/blue}) - -(defn counter-container [size more-than-nine] - {:width (if more-than-nine (+ 2 size) size) - :height size - :border-radius (/ size 2) - :background-color colors/blue - :align-items :center - :justify-content :center}) - -(defnstyle counter-label [size] - {:font-size (/ size 2) - :color colors/white - :android {:line-height (+ (/ size 2) 2)} - :text-align :center}) - -(def image-contain - {:align-self :stretch}) diff --git a/src/status_im/ui/components/connectivity/styles.cljs b/src/status_im/ui/components/connectivity/styles.cljs deleted file mode 100644 index 2bab1359ca5..00000000000 --- a/src/status_im/ui/components/connectivity/styles.cljs +++ /dev/null @@ -1,18 +0,0 @@ -(ns status-im.ui.components.connectivity.styles - (:require-macros [status-im.utils.styles :refer [defnstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(defnstyle text-wrapper [top opacity window-width pending?] - {:ios {:z-index 0} - :opacity opacity - :width window-width - :top (+ (+ 56 top) (if pending? 35 0)) - :position :absolute - :background-color colors/gray-notifications - :height 35}) - -(def text - {:text-align :center - :color :white - :font-size 14 - :top 8}) diff --git a/src/status_im/ui/components/connectivity/view.cljs b/src/status_im/ui/components/connectivity/view.cljs deleted file mode 100644 index 079c8fcf45b..00000000000 --- a/src/status_im/ui/components/connectivity/view.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.ui.components.connectivity.view - (:require-macros [status-im.utils.views :refer [defview letsubs] :as views]) - (:require [re-frame.core :as re-frame] - [reagent.core :as reagent] - [status-im.ui.components.react :as react] - [status-im.ui.components.connectivity.styles :as styles] - [status-im.i18n :as i18n])) - -(defview error-view [{:keys [top]}] - (letsubs [offline? [:offline?] - disconnected? [:disconnected?] - mailserver-error? [:mailserver-error?] - fetching? [:fetching?] - current-chat-contact [:get-current-chat-contact] - view-id [:get :view-id] - window-width [:dimensions/window-width]] - (when-let [label (cond - offline? :t/offline - disconnected? :t/disconnected - mailserver-error? :t/mailserver-reconnect - fetching? :t/fetching-messages - :else nil)] - (let [pending? (and (:pending current-chat-contact) (= :chat view-id))] - [react/view {:style (styles/text-wrapper top 1.0 window-width pending?) - :accessibility-label :connection-status-text} - [react/text {:style styles/text - :on-press (when mailserver-error? - #(re-frame/dispatch [:inbox/reconnect]))} - (i18n/label label)]])))) diff --git a/src/status_im/ui/components/contact/contact.cljs b/src/status_im/ui/components/contact/contact.cljs deleted file mode 100644 index 1d0ad036c3e..00000000000 --- a/src/status_im/ui/components/contact/contact.cljs +++ /dev/null @@ -1,57 +0,0 @@ -(ns status-im.ui.components.contact.contact - (:require-macros [status-im.utils.views :as views]) - (:require [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.chat-icon.screen :as chat-icon] - [status-im.ui.components.contact.styles :as styles] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.list.views :as list] - [status-im.utils.gfycat.core :as gfycat])) - -(defn- contact-inner-view - ([{:keys [info style props] {:keys [whisper-identity name dapp?] :as contact} :contact}] - [react/view (merge styles/contact-inner-container style) - [react/view - [chat-icon/contact-icon-contacts-tab contact]] - [react/view styles/info-container - [react/text (merge {:style styles/name-text - :number-of-lines 1} - (when dapp? {:accessibility-label :dapp-name}) - props) - (if (pos? (count name)) - (i18n/get-contact-translated whisper-identity :name name) - ;;TODO is this correct behaviour? - (gfycat/generate-gfy whisper-identity))] - (when info - [react/text {:style styles/info-text} - info])]])) - -(defn contact-view [{:keys [style contact extended? on-press extend-options extend-title info show-forward? - accessibility-label inner-props] - :or {accessibility-label :contact-item}}] - [react/touchable-highlight (merge {:accessibility-label accessibility-label} - (when-not extended? - {:on-press (when on-press #(on-press contact))})) - [react/view styles/contact-container - [contact-inner-view {:contact contact :info info :style style :props inner-props}] - (when show-forward? - [react/view styles/forward-btn - [vector-icons/icon :icons/forward]]) - (when (and extended? (not (empty? extend-options))) - [react/view styles/more-btn-container - [react/touchable-highlight {:on-press #(list-selection/show {:options extend-options - :title extend-title}) - :accessibility-label :menu-option} - [react/view styles/more-btn - [vector-icons/icon :icons/options {:accessibility-label :options}]]]])]]) - -(views/defview toogle-contact-view [{:keys [whisper-identity] :as contact} selected-key on-toggle-handler] - (views/letsubs [checked [selected-key whisper-identity]] - [react/view {:accessibility-label :contact-item} - [list/list-item-with-checkbox - {:checked? checked - :on-value-change #(on-toggle-handler checked whisper-identity) - :plain-checkbox? true} - [react/view styles/contact-container - [contact-inner-view {:contact contact}]]]])) diff --git a/src/status_im/ui/components/contact/styles.cljs b/src/status_im/ui/components/contact/styles.cljs deleted file mode 100644 index a3d24f9f7a3..00000000000 --- a/src/status_im/ui/components/contact/styles.cljs +++ /dev/null @@ -1,60 +0,0 @@ -(ns status-im.ui.components.contact.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as common] - [status-im.utils.platform] - [status-im.ui.components.colors :as colors])) - -(defstyle contact-inner-container - {:flex 1 - :flex-direction :row - :align-items :center - :padding-left 16 - :background-color colors/white - :android {:height 56} - :ios {:height 63}}) - -(def info-container - {:flex 1 - :flex-direction :column - :margin-left 16 - :margin-right 5 - :justify-content :center}) - -(defstyle name-text - {:color common/text1-color - :android {:font-size 16} - :ios {:font-size 17 - :letter-spacing -0.2}}) - -(def info-text - {:margin-top 1 - :font-size 12 - :color common/text2-color}) - -(def contact-container - {:flex-direction :row - :align-items :center - :background-color colors/white}) - -(def forward-btn - {:opacity 0.4 - :padding 12 - :align-items :center - :justify-content :center}) - -(def more-btn-container - {:align-items :center - :justify-content :center}) - -(def more-btn - {:padding 16}) - -(def toggle-container - {:width 56 - :height 56 - :alignItems :center - :justifyContent :center}) - -(def check-icon - {:width 16 - :height 16}) diff --git a/src/status_im/ui/components/desktop/events.cljs b/src/status_im/ui/components/desktop/events.cljs deleted file mode 100644 index 2161732871d..00000000000 --- a/src/status_im/ui/components/desktop/events.cljs +++ /dev/null @@ -1,8 +0,0 @@ -(ns status-im.ui.components.desktop.events - (:require [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro])) - -(handlers/register-handler-fx - :show-desktop-tab - (fn [{:keys [db] :as cofx} [_ tab-name]] - {:db (assoc-in db [:desktop/desktop :tab-view-id] tab-name)})) diff --git a/src/status_im/ui/components/desktop/tabs.cljs b/src/status_im/ui/components/desktop/tabs.cljs deleted file mode 100644 index 4d29b9d5241..00000000000 --- a/src/status_im/ui/components/desktop/tabs.cljs +++ /dev/null @@ -1,53 +0,0 @@ -(ns status-im.ui.components.desktop.tabs - (:require [re-frame.core :as re-frame] - status-im.ui.components.desktop.events - [status-im.ui.components.icons.vector-icons :as icons] - [taoensso.timbre :as log] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.react :as react] - [status-im.ui.screens.main-tabs.styles :as tabs.styles]) - (:require-macros [status-im.utils.views :as views])) - -;;TODO copy-pate with minimum modifications of status-react tabs - -(def tabs-list-data - [{:view-id :home - :content {:title "Home" - :icon-inactive :icons/home - :icon-active :icons/home-active}} - #_{:view-id :wallet - :content {:title "Wallet" - :icon-inactive :icons/wallet - :icon-active :icons/wallet-active}} - {:view-id :profile - :content {:title "Profile" - :icon-inactive :icons/profile - :icon-active :icons/profile-active}}]) - -(defn- tab-content [{:keys [title icon-active icon-inactive]}] - (fn [active?] - [react/view {:style tabs.styles/tab-container} - (let [icon (if active? icon-active icon-inactive)] - [react/view - [icons/icon icon {:style {:tint-color (if active? colors/blue colors/gray-icon)}}]]) - [react/view - [react/text {:style (tabs.styles/tab-title active?)} - title]]])) - -(def tabs-list-indexed (map-indexed vector (map #(update % :content tab-content) tabs-list-data))) - -(defn tab [index content view-id active?] - [react/touchable-highlight {:style (merge tabs.styles/tab-container {:flex 1}) - :disabled active? - :on-press #(do - (re-frame/dispatch [:navigate-to :home]) - (re-frame/dispatch [:show-desktop-tab view-id]))} - [react/view - [content active?]]]) - -(views/defview main-tabs [] - (views/letsubs [current-tab [:get-in [:desktop/desktop :tab-view-id]]] - [react/view - [react/view {:style tabs.styles/tabs-container} - (for [[index {:keys [content view-id]}] tabs-list-indexed] - ^{:key index} [tab index content view-id (= current-tab view-id)])]])) diff --git a/src/status_im/ui/components/desktop/views.cljs b/src/status_im/ui/components/desktop/views.cljs deleted file mode 100644 index a420b483d45..00000000000 --- a/src/status_im/ui/components/desktop/views.cljs +++ /dev/null @@ -1,85 +0,0 @@ -(ns status-im.ui.components.desktop.views - (:require - [status-im.ui.components.icons.vector-icons :as icons] - [status-im.ui.components.common.styles :as styles] - [status-im.ui.components.action-button.styles :as st] - [status-im.ui.components.styles :as common] - [status-im.ui.components.checkbox.styles :as checkbox.styles] - [status-im.ui.components.react :as react])) - -(defn checkbox [{:keys [on-value-change checked?]}] - [react/touchable-highlight {:style checkbox.styles/wrapper :on-press #(do (when on-value-change (on-value-change (not checked?))))} - [react/view {:style (checkbox.styles/icon-check-container checked?)} - (when checked? - [icons/icon :icons/ok {:style checkbox.styles/check-icon}])]]) - -;; TODO copy-pate with minimum modifications of status-react components - -(defn action-button [{:keys [label icon icon-opts on-press label-style cyrcle-color]}] - [react/touchable-highlight {:on-press on-press} - [react/view {:style st/action-button} - [react/view {:style (st/action-button-icon-container cyrcle-color)} - [icons/icon icon icon-opts]] - [react/view {:style st/action-button-label-container} - [react/text {:style (merge st/action-button-label label-style)} - label]]]]) - -(defn separator [style & [wrapper-style]] - [react/view {:style (merge styles/separator-wrapper wrapper-style)} - [react/view {:style (merge styles/separator style)}]]) - -(def sticky-button-style - {:flex-direction :row - :height 52 - :justify-content :center - :align-items :center - :background-color common/color-light-blue}) - -(def sticky-button-label-style - {:color common/color-white - :font-size 17 - :line-height 20 - :letter-spacing -0.2}) - -(defn sticky-button [label on-press] - [react/touchable-highlight {:on-press on-press} - [react/view {:style sticky-button-style} - [react/text {:style sticky-button-label-style} - label]]]) - -(defn button-label-style [enabled?] - {:color "#4360df" - :font-size 15 - :opacity (if enabled? 1 0.3) - :letter-spacing -0.2}) - -(defn button [label enabled? on-press] - [react/touchable-highlight {:on-press on-press :disabled (not enabled?)} - [react/view {:style {:width 290 - :height 52 - :align-items :center - :justify-content :center - :border-radius 8 - :background-color :white}} - ;:box-shadow "0 2px 6px 0 rgba(0, 0, 0, 0.25)"}} - [react/text {:style (button-label-style enabled?)} - label]]]) - -(def text-button-label-style - {:color :white - :height 23 - :font-size 15 - :letter-spacing -0.2}) - -(defn text-button [label on-press] - [react/touchable-highlight {:on-press on-press} - [react/view - [react/text {:style text-button-label-style} - label]]]) - -(defn back-button [on-press] - [react/view {:style {:position :absolute :left 32 :top 32 :z-index 1000}} - [react/touchable-highlight {:on-press on-press} - [react/view {:style {:flex-direction :row :align-items :center}} - [icons/icon :icons/back {:color :white}] - [react/text {:style {:margin-left 16 :font-size 15 :color :white}} "Back"]]]]) \ No newline at end of file diff --git a/src/status_im/ui/components/dialog.cljs b/src/status_im/ui/components/dialog.cljs deleted file mode 100644 index 3a851c9c172..00000000000 --- a/src/status_im/ui/components/dialog.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns status-im.ui.components.dialog - (:require [status-im.react-native.js-dependencies :as rn-dependencies])) - -(defn- callback [options] - (fn [index] - (when (< index (count options)) - (when-let [handler (:action (nth options index))] - (handler))))) - -(defn- show [{:keys [title options cancel-text]}] - (let [dialog (new rn-dependencies/dialogs)] - (.set dialog (clj->js {:title title - :negativeText cancel-text - :items (mapv :label options) - :itemsCallback (callback options)})) - (.show dialog))) diff --git a/src/status_im/ui/components/icons/vector_icons.cljs b/src/status_im/ui/components/icons/vector_icons.cljs deleted file mode 100644 index 53ad9d3e5bf..00000000000 --- a/src/status_im/ui/components/icons/vector_icons.cljs +++ /dev/null @@ -1,204 +0,0 @@ -(ns status-im.ui.components.icons.vector-icons - (:require-macros [status-im.ui.components.svg :as components.svg]) - (:require [goog.object :as object] - [reagent.core :as reagent] - [status-im.ui.components.react :as react] - [status-im.utils.platform :as platform] - [status-im.ui.components.styles :as styles] - [status-im.react-native.js-dependencies :as js-dependencies]) - (:refer-clojure :exclude [use])) - -(when-not platform/desktop? - (defn get-property [name] - (object/get js-dependencies/svg name)) - - (defn adapt-class [class] - (when class - (reagent/adapt-react-class class))) - - (defn get-class [name] - (adapt-class (get-property name))) - - (def svg (get-class "Svg")) - (def g (get-class "G")) - (def rect (get-class "Rect")) - (def path (get-class "Path")) - (def use (get-class "Use")) - (def defs (get-class "Defs")) - (def circle (get-class "Circle"))) - -(def icons - (if platform/desktop? - {:icons/discover (js/require "./resources/icons/bottom/discover_gray.svg") - :icons/contacts (js/require "./resources/icons/bottom/contacts_gray.svg") - :icons/home (js/require "./resources/icons/bottom/home_gray.svg") - :icons/home-active (js/require "./resources/icons/bottom/home_blue.svg") - :icons/profile (js/require "./resources/icons/bottom/profile_gray.svg") - :icons/profile-active (js/require "./resources/icons/bottom/profile_blue.svg") - :icons/wallet (js/require "./resources/icons/bottom/wallet_gray.svg") - :icons/wallet-active (js/require "./resources/icons/bottom/wallet_active.svg") - :icons/speaker (js/require "./resources/icons/speaker.svg") - :icons/speaker-off (js/require "./resources/icons/speaker_off.svg") - :icons/transaction-history (js/require "./resources/icons/transaction_history.svg") - :icons/add (js/require "./resources/icons/add.svg") - :icons/add-contact (js/require "./resources/icons/add_contact.svg") - :icons/add-wallet (js/require "./resources/icons/add_wallet.svg") - :icons/address (js/require "./resources/icons/address.svg") - :icons/arrow-left (js/require "./resources/icons/arrow_left.svg") - :icons/arrow-right (js/require "./resources/icons/arrow_right.svg") - :icons/flash-active (js/require "./resources/icons/flash_active.svg") - :icons/flash-inactive (js/require "./resources/icons/flash_inactive.svg") - :icons/attach (js/require "./resources/icons/attach.svg") - :icons/browse (js/require "./resources/icons/browse.svg") - :icons/close (js/require "./resources/icons/close.svg") - :icons/copy-from (js/require "./resources/icons/copy_from.svg") - :icons/delete (js/require "./resources/icons/delete.svg") - :icons/dots-horizontal (js/require "./resources/icons/dots_horizontal.svg") - :icons/dots-vertical (js/require "./resources/icons/dots_vertical.svg") - :icons/exclamation-mark (js/require "./resources/icons/exclamation_mark.svg") - :icons/filter (js/require "./resources/icons/filter.svg") - :icons/fullscreen (js/require "./resources/icons/fullscreen.svg") - :icons/group-big (js/require "./resources/icons/group_big.svg") - :icons/group-chat (js/require "./resources/icons/group_chat.svg") - :icons/chats (js/require "./resources/icons/chats.svg") - :icons/hamburger (js/require "./resources/icons/hamburger.svg") - :icons/hidden (js/require "./resources/icons/hidden.svg") - :icons/in-contacts (js/require "./resources/icons/in_contacts.svg") - :icons/lock (js/require "./resources/icons/lock.svg") - :icons/mic (js/require "./resources/icons/mic.svg") - :icons/ok (js/require "./resources/icons/ok.svg") - :icons/public (js/require "./resources/icons/public.svg") - :icons/public-chat (js/require "./resources/icons/public_chat.svg") - :icons/qr (js/require "./resources/icons/QR.svg") - :icons/input-commands (js/require "./resources/icons/input_commands.svg") - :icons/input-send (js/require "./resources/icons/input_send.svg") - :icons/back (js/require "./resources/icons/back.svg") - :icons/forward (js/require "./resources/icons/forward.svg") - :icons/dropdown-up (js/require "./resources/icons/dropdown_up.svg") - :icons/up (js/require "./resources/icons/up.svg") - :icons/down (js/require "./resources/icons/down.svg") - :icons/grab (js/require "./resources/icons/grab.svg") - :icons/share (js/require "./resources/icons/share.svg") - :icons/tooltip-triangle (js/require "./resources/icons/tooltip-triangle.svg") - :icons/open (js/require "./resources/icons/open.svg") - :icons/network (js/require "./resources/icons/network.svg") - :icons/wnode (js/require "./resources/icons/wnode.svg") - :icons/refresh (js/require "./resources/icons/refresh.svg") - :icons/newchat (js/require "./resources/icons/newchat.svg") - :icons/logo (js/require "./resources/icons/logo.svg") - :icons/camera (js/require "./resources/icons/camera.svg") - :icons/check (js/require "./resources/icons/check.svg") - :icons/dots (js/require "./resources/icons/dots.svg") - :icons/warning (js/require "./resources/icons/warning.svg")} - {:icons/discover (components.svg/slurp-svg "./resources/icons/bottom/discover_gray.svg") - :icons/contacts (components.svg/slurp-svg "./resources/icons/bottom/contacts_gray.svg") - :icons/home (components.svg/slurp-svg "./resources/icons/bottom/home_gray.svg") - :icons/home-active (components.svg/slurp-svg "./resources/icons/bottom/home_blue.svg") - :icons/profile (components.svg/slurp-svg "./resources/icons/bottom/profile_gray.svg") - :icons/profile-active (components.svg/slurp-svg "./resources/icons/bottom/profile_blue.svg") - :icons/wallet (components.svg/slurp-svg "./resources/icons/bottom/wallet_gray.svg") - :icons/wallet-active (components.svg/slurp-svg "./resources/icons/bottom/wallet_active.svg") - :icons/speaker (components.svg/slurp-svg "./resources/icons/speaker.svg") - :icons/speaker-off (components.svg/slurp-svg "./resources/icons/speaker_off.svg") - :icons/transaction-history (components.svg/slurp-svg "./resources/icons/transaction_history.svg") - :icons/add (components.svg/slurp-svg "./resources/icons/add.svg") - :icons/add-contact (components.svg/slurp-svg "./resources/icons/add_contact.svg") - :icons/add-wallet (components.svg/slurp-svg "./resources/icons/add_wallet.svg") - :icons/address (components.svg/slurp-svg "./resources/icons/address.svg") - :icons/arrow-left (components.svg/slurp-svg "./resources/icons/arrow_left.svg") - :icons/arrow-right (components.svg/slurp-svg "./resources/icons/arrow_right.svg") - :icons/flash-active (components.svg/slurp-svg "./resources/icons/flash_active.svg") - :icons/flash-inactive (components.svg/slurp-svg "./resources/icons/flash_inactive.svg") - :icons/attach (components.svg/slurp-svg "./resources/icons/attach.svg") - :icons/browse (components.svg/slurp-svg "./resources/icons/browse.svg") - :icons/close (components.svg/slurp-svg "./resources/icons/close.svg") - :icons/copy-from (components.svg/slurp-svg "./resources/icons/copy_from.svg") - :icons/delete (components.svg/slurp-svg "./resources/icons/delete.svg") - :icons/dots-horizontal (components.svg/slurp-svg "./resources/icons/dots_horizontal.svg") - :icons/dots-vertical (components.svg/slurp-svg "./resources/icons/dots_vertical.svg") - :icons/exclamation-mark (components.svg/slurp-svg "./resources/icons/exclamation_mark.svg") - :icons/filter (components.svg/slurp-svg "./resources/icons/filter.svg") - :icons/fullscreen (components.svg/slurp-svg "./resources/icons/fullscreen.svg") - :icons/group-big (components.svg/slurp-svg "./resources/icons/group_big.svg") - :icons/group-chat (components.svg/slurp-svg "./resources/icons/group_chat.svg") - :icons/chats (components.svg/slurp-svg "./resources/icons/chats.svg") - :icons/hamburger (components.svg/slurp-svg "./resources/icons/hamburger.svg") - :icons/hidden (components.svg/slurp-svg "./resources/icons/hidden.svg") - :icons/in-contacts (components.svg/slurp-svg "./resources/icons/in_contacts.svg") - :icons/lock (components.svg/slurp-svg "./resources/icons/lock.svg") - :icons/lock-opened (components.svg/slurp-svg "./resources/icons/lock_opened.svg") - :icons/mic (components.svg/slurp-svg "./resources/icons/mic.svg") - :icons/ok (components.svg/slurp-svg "./resources/icons/ok.svg") - :icons/public (components.svg/slurp-svg "./resources/icons/public.svg") - :icons/public-chat (components.svg/slurp-svg "./resources/icons/public_chat.svg") - :icons/qr (components.svg/slurp-svg "./resources/icons/QR.svg") - :icons/input-commands (components.svg/slurp-svg "./resources/icons/input_commands.svg") - :icons/input-send (components.svg/slurp-svg "./resources/icons/input_send.svg") - :icons/back (components.svg/slurp-svg "./resources/icons/back.svg") - :icons/forward (components.svg/slurp-svg "./resources/icons/forward.svg") - :icons/dropdown-up (components.svg/slurp-svg "./resources/icons/dropdown_up.svg") - :icons/up (components.svg/slurp-svg "./resources/icons/up.svg") - :icons/down (components.svg/slurp-svg "./resources/icons/down.svg") - :icons/grab (components.svg/slurp-svg "./resources/icons/grab.svg") - :icons/share (components.svg/slurp-svg "./resources/icons/share.svg") - :icons/tooltip-triangle (components.svg/slurp-svg "./resources/icons/tooltip-triangle.svg") - :icons/open (components.svg/slurp-svg "./resources/icons/open.svg") - :icons/network (components.svg/slurp-svg "./resources/icons/network.svg") - :icons/wnode (components.svg/slurp-svg "./resources/icons/wnode.svg") - :icons/refresh (components.svg/slurp-svg "./resources/icons/refresh.svg") - :icons/newchat (components.svg/slurp-svg "./resources/icons/newchat.svg") - :icons/logo (components.svg/slurp-svg "./resources/icons/logo.svg") - :icons/camera (components.svg/slurp-svg "./resources/icons/camera.svg") - :icons/check (components.svg/slurp-svg "./resources/icons/check.svg") - :icons/dots (components.svg/slurp-svg "./resources/icons/dots.svg") - :icons/warning (components.svg/slurp-svg "./resources/icons/warning.svg")})) - -(defn normalize-property-name [n] - (if (= n :icons/options) - (if platform/ios? :icons/dots-horizontal :icons/dots-vertical) - n)) - -(defn desktop-icon - [name {:keys [color container-style style accessibility-label width height] - :or {accessibility-label :icon}}] - (let [default-viewbox {:width 24 :height 24} - icon-style (if width - (assoc default-viewbox :width width :height height) - default-viewbox)] - [react/view {:style container-style - :accessibility-label accessibility-label} - [react/image {:source (get icons (normalize-property-name name)) - :style (merge icon-style style)}]])) - -(defn mobile-icon - [name {:keys [color container-style style accessibility-label width height] - :or {accessibility-label :icon}}] - ^{:key name} - [react/view {:style container-style - :accessibility-label accessibility-label} - (if-let [icon-fn (get icons (normalize-property-name name))] - (let [icon-vec (icon-fn - (cond - (keyword? color) - (case color - :dark styles/icon-dark-color - :gray styles/icon-gray-color - :blue styles/color-light-blue - :active styles/color-blue4 - :white styles/color-white - :red styles/icon-red-color - styles/icon-dark-color) - (string? color) - color - :else - styles/icon-dark-color))] - (if width - (update icon-vec 1 assoc :width width :height height) - icon-vec)) - (throw (js/Error. (str "Unknown icon: " name))))]) - -(defn icon - ([name] (icon name nil)) - ([name options] - (let [icon-fn (if platform/desktop? desktop-icon mobile-icon)] - [icon-fn name options]))) \ No newline at end of file diff --git a/src/status_im/ui/components/list/styles.cljs b/src/status_im/ui/components/list/styles.cljs deleted file mode 100644 index a1347f0a316..00000000000 --- a/src/status_im/ui/components/list/styles.cljs +++ /dev/null @@ -1,123 +0,0 @@ -(ns status-im.ui.components.list.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def item - {:flex-direction :row - :justify-content :center}) - -(def item-content-view - {:flex 1 - :flex-direction :column - :justify-content :center}) - -(def item-checkbox - {:flex 1 - :flex-direction :column - :align-items :center - :justify-content :center}) - -(def primary-text-base - {:font-size 16 - :color colors/black}) - -(def primary-text - (merge primary-text-base - {:padding-top 12})) - -(def primary-text-only - (merge primary-text-base - {:padding-vertical 16})) - -(def secondary-text - {:font-size 14 - :color colors/gray - :padding-top 4}) - -(def image-size 40) - -(def item-image - {:width image-size - :height image-size}) - -(def icon-size 24) -(def icon-wrapper-size (+ icon-size (* 2 8))) - -(def item-icon-wrapper - {:width icon-wrapper-size - :height icon-wrapper-size - :align-items :center - :justify-content :center}) - -(def item-icon - {:width icon-size - :height icon-size}) - -(def horizontal-margin 12) -(def vertical-margin 12) - -(def left-item-wrapper - {:justify-content :center - :margin-left horizontal-margin - :margin-vertical vertical-margin}) - -(def content-item-wrapper - {:flex 1 - :justify-content :center - :margin-horizontal horizontal-margin}) - -(def right-item-wrapper - {:justify-content :center - :margin-right 12}) - -(def base-separator - {:height 1 - :background-color colors/gray-light}) - -(def separator - (merge - base-separator - {:margin-left 70})) - -(defstyle list-header-footer-spacing - {:android {:background-color colors/white - :height 8}}) - -(defstyle section-header - {:font-size 14 - :color colors/gray - :margin-left 16 - :margin-top 16 - :android {:margin-bottom 3} - :ios {:margin-bottom 10}}) - -(def section-header-container - {:background-color colors/white}) - -(def action-list - {:background-color colors/blue}) - -(def action - {:background-color colors/white-transparent - :border-radius 50}) - -(def action-disabled - {:background-color colors/gray-lighter}) - -(def action-label - {:color colors/white}) - -(def action-label-disabled - {:color colors/gray}) - -(def action-separator - {:height 1 - :background-color colors/white-light-transparent - :margin-left 70}) - -(def list-with-label-wrapper - {:margin-top 26 - :margin-horizontal 16}) - -(def label - {:color colors/gray}) diff --git a/src/status_im/ui/components/list/views.cljs b/src/status_im/ui/components/list/views.cljs deleted file mode 100644 index edfcb2649f7..00000000000 --- a/src/status_im/ui/components/list/views.cljs +++ /dev/null @@ -1,221 +0,0 @@ -(ns status-im.ui.components.list.views - " - Wrapper for react-native list components. - - (defn render [{:keys [title subtitle]}] - [item - [item-icon {:icon :dots_vertical_white}] - [item-content - [item-primary title] - [item-secondary subtitle]] - [item-icon {:icon :arrow_right_gray}]]) - - [flat-list {:data [{:title \"\" :subtitle \"\"}] :render-fn render}] - - [section-list {:sections [{:title :key :unik :data {:title \"\" :subtitle \"\"}}] :render-fn render}] - - or with a per-section `render-fn` - - [section-list {:sections [{:title \"\" :key :unik :render-fn render :data {:title \"\" :subtitle \"\"}}]}] - " - (:require [reagent.core :as reagent] - [status-im.ui.components.checkbox.view :as checkbox] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list.styles :as styles] - [status-im.ui.components.react :as react] - [status-im.utils.platform :as platform])) - -(def flat-list-class (react/get-class "FlatList")) -(def section-list-class (react/get-class "SectionList")) - -(defn item - ([content] (item nil content)) - ([left content] (item left content nil)) - ([left content right] - [react/view {:style styles/item} - (when left - [react/view {:style styles/left-item-wrapper} - left]) - [react/view {:style styles/content-item-wrapper} - content] - (when right - [react/view {:style styles/right-item-wrapper} - right])])) - -(defn touchable-item [handler item] - [react/touchable-highlight {:on-press handler} - [react/view - item]]) - -(defn item-icon - [{:keys [icon style icon-opts]}] - {:pre [(not (nil? icon))]} - [react/view {:style (merge styles/item-icon-wrapper style)} - [vector-icons/icon icon (merge icon-opts {:style styles/item-icon})]]) - -(defn item-image - [{:keys [source style image-style]}] - [react/view {:style style} - [react/image {:source source - :style (merge styles/item-image image-style)}]]) - -(defn item-primary - ([s] (item-primary nil s)) - ([{:keys [style] :as props} s] - [react/text (merge {:style styles/primary-text} - (dissoc props :style)) - s])) - -(defn item-primary-only - ([s] (item-primary-only nil s)) - ([{:keys [style] :as props} s] - [react/text (merge {:style (merge styles/primary-text-only style)} - (dissoc props :style)) - s])) - -(defn item-secondary - [secondary] - [react/text {:style styles/secondary-text :ellipsize-mode :middle :number-of-lines 1} secondary]) - -(defn item-content - [& children] - (into [react/view {:style styles/item-content-view}] (keep identity children))) - -(defn item-checkbox - [{:keys [style] :as props}] - [react/view {:style (merge style styles/item-checkbox)} - [checkbox/checkbox props]]) - -(defn list-item-with-checkbox [{:keys [on-value-change checked? plain-checkbox?] :as props} item] - (let [handler #(on-value-change (not checked?)) - checkbox [(if plain-checkbox? checkbox/plain-checkbox item-checkbox) props] - item (conj item checkbox)] - [touchable-item handler item])) - -(def item-icon-forward - [item-icon {:icon :icons/forward - :icon-opts {:color colors/white-light-transparent}}]) - -(defn- wrap-render-fn [f] - (fn [data] - (reagent/as-element (f (.-item data) (.-index data) (.-separators data))))) - -(defn- wrap-key-fn [f] - (fn [data index] - {:post [(some? %)]} - (f data index))) - -(def base-separator [react/view styles/base-separator]) - -(def default-separator [react/view styles/separator]) - -(def default-header [react/view styles/list-header-footer-spacing]) - -(def default-footer [react/view styles/list-header-footer-spacing]) - -(defn- base-list-props - [{:keys [key-fn render-fn empty-component header separator default-separator?]}] - (let [separator (or separator (when (and platform/ios? default-separator?) default-separator))] - (merge (when key-fn {:keyExtractor (wrap-key-fn key-fn)}) - (when render-fn {:renderItem (wrap-render-fn render-fn)}) - (when separator {:ItemSeparatorComponent (fn [] (reagent/as-element separator))}) - (when empty-component {:ListEmptyComponent (fn [] (reagent/as-element empty-component))}) - (when header {:ListHeaderComponent (fn [] (reagent/as-element header))})))) - -;; Workaround an issue in reagent that does not consider JS array as JS value -;; This forces clj <-> js serialization and breaks clj semantic -;; See https://github.com/reagent-project/reagent/issues/335 - -(deftype Item [value] - IEncodeJS - (-clj->js [x] (.-value x)) - (-key->js [x] (.-value x)) - IEncodeClojure - (-js->clj [x _] (.-value x))) - -(defn- to-js-array - "Converts a collection to a JS array (but leave content as is)" - [coll] - (let [arr (array)] - (doseq [x coll] - (.push arr x)) - arr)) - -(defn- wrap-data [o] - (Item. (to-js-array o))) - -(defn flat-list - "A wrapper for FlatList. - See https://facebook.github.io/react-native/docs/flatlist.html" - [{:keys [data] :as props}] - {:pre [(or (nil? data) - (sequential? data))]} - [flat-list-class - (merge (base-list-props props) - props - {:data (wrap-data data)})]) - -(defn- wrap-render-section-header-fn [f] - (fn [data] - (let [section (.-section data)] - (reagent/as-element (f {:title (.-title section) - :data (.-data section)}))))) - -(defn- default-render-section-header [{:keys [title data]}] - (when (seq data) - [react/view styles/section-header-container - [react/text {:style styles/section-header} - title]])) - -(defn- wrap-per-section-render-fn [props] - (update - (if-let [f (:render-fn props)] - (assoc (dissoc props :render-fn) :renderItem (wrap-render-fn f)) - props) - :data wrap-data)) - -(defn section-list - "A wrapper for SectionList. - See https://facebook.github.io/react-native/docs/sectionlist.html" - [{:keys [sections render-section-header-fn] :as props - :or {render-section-header-fn default-render-section-header}}] - [section-list-class - (merge (base-list-props props) - props - {:sections (clj->js (map wrap-per-section-render-fn sections)) - :renderSectionHeader (wrap-render-section-header-fn render-section-header-fn)})]) - -(defn- render-action [{:keys [label accessibility-label icon action disabled?]} - {:keys [action-style action-label-style icon-opts]}] - [react/touchable-highlight {:on-press action} - [react/view {:accessibility-label accessibility-label} - [item - [item-icon {:icon icon - :style (merge styles/action - action-style - (when disabled? styles/action-disabled)) - :icon-opts (merge {:color :white} - icon-opts - (when disabled? {:color colors/gray}))}] - [item-primary-only {:style (merge styles/action-label - action-label-style - (when disabled? styles/action-label-disabled))} - label] - item-icon-forward]]]) - -(defn action-list [actions {:keys [container-style action-separator-style] :as styles}] - [react/view (merge styles/action-list container-style) - [flat-list - {:separator (when platform/ios? - [react/view (merge styles/action-separator - action-separator-style)]) - :data actions - :key-fn (fn [_ i] (str i)) - :render-fn #(render-action % styles)}]]) - -(defn list-with-label [{:keys [style]} label list] - [react/view (merge styles/list-with-label-wrapper style) - [react/text {:style styles/label} - label] - list]) diff --git a/src/status_im/ui/components/list_selection.cljs b/src/status_im/ui/components/list_selection.cljs deleted file mode 100644 index 0aa832e7b79..00000000000 --- a/src/status_im/ui/components/list_selection.cljs +++ /dev/null @@ -1,43 +0,0 @@ -(ns status-im.ui.components.list-selection - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.action-sheet :as action-sheet] - [status-im.ui.components.dialog :as dialog] - [status-im.ui.components.react :as react] - [status-im.utils.platform :as platform] - [status-im.utils.http :as http])) - -(defn- open-share [content] - (when (or (:message content) - (:url content)) - (.share react/sharing (clj->js content)))) - -(defn share-options [text] - [{:label (i18n/label :t/sharing-copy-to-clipboard) - :action #(react/copy-to-clipboard text)} - {:label (i18n/label :t/sharing-share) - :action #(open-share {:message text})}]) - -(defn show [options] - (if platform/ios? - (action-sheet/show options) - (dialog/show options))) - -(defn share [text dialog-title] - (show {:title dialog-title - :options (share-options text) - :cancel-text (i18n/label :t/sharing-cancel)})) - -(defn browse [link] - (show {:title (i18n/label :t/browsing-title) - :options [{:label (i18n/label :t/browsing-open-in-status) - :action #(re-frame/dispatch [:open-url-in-browser link])} - {:label (i18n/label :t/browsing-open-in-web-browser) - :action #(.openURL react/linking (http/normalize-url link))}] - :cancel-text (i18n/label :t/browsing-cancel)})) - -(defn browse-dapp [link] - (show {:title (i18n/label :t/browsing-title) - :options [{:label (i18n/label :t/browsing-open-in-status) - :action #(re-frame/dispatch [:open-url-in-browser link])}] - :cancel-text (i18n/label :t/browsing-cancel)})) diff --git a/src/status_im/ui/components/permissions.cljs b/src/status_im/ui/components/permissions.cljs deleted file mode 100644 index 18b9fb5bea9..00000000000 --- a/src/status_im/ui/components/permissions.cljs +++ /dev/null @@ -1,33 +0,0 @@ -(ns status-im.ui.components.permissions - (:require [status-im.utils.platform :as platform] - [taoensso.timbre :as log] - [status-im.ui.components.camera :as camera] - [status-im.react-native.js-dependencies :as js-dependencies])) - -(def permissions-class (.-PermissionsAndroid js-dependencies/react-native)) - -(def permissions-map - {:read-external-storage "android.permission.READ_EXTERNAL_STORAGE" - :write-external-storage "android.permission.WRITE_EXTERNAL_STORAGE" - :camera "android.permission.CAMERA"}) - -(defn all-granted? [permissions] - (let [permission-vals (distinct (vals permissions))] - (and (= (count permission-vals) 1) - (not (#{"denied" "never_ask_again"} (first permission-vals)))))) - -(defn request-permissions [{:keys [permissions on-allowed on-denied] - :or {on-allowed #() - on-denied #()} - :as options}] - (if platform/android? - (let [permissions (mapv #(get permissions-map %) permissions)] - (-> (.requestMultiple permissions-class (clj->js permissions)) - (.then #(if (all-granted? (js->clj %)) - (on-allowed) - (on-denied))) - (.catch on-denied))) - - (if ((set permissions) :camera) - (camera/request-access-ios on-allowed on-denied) - (on-allowed)))) diff --git a/src/status_im/ui/components/qr_code_viewer/styles.cljs b/src/status_im/ui/components/qr_code_viewer/styles.cljs deleted file mode 100644 index 73acf586123..00000000000 --- a/src/status_im/ui/components/qr_code_viewer/styles.cljs +++ /dev/null @@ -1,80 +0,0 @@ -(ns status-im.ui.components.qr-code-viewer.styles - (:require [status-im.ui.components.colors :as colors]) - (:require-macros [status-im.utils.styles :refer [defstyle]])) - -(def qr-code-hint - {:color colors/gray - :padding-top 24 - :padding-bottom 22 - :text-align :center}) - -(def qr-code-padding - 15) - -(defn qr-code-container [width] - {:background-color colors/white - :width width - :align-items :center - :justify-content :center - :padding qr-code-padding - :border-radius 8}) - -(defstyle name-container - {:flex 0.6 - :flex-direction :column - :ios {:align-items :center} - :android {:margin-left 15}}) - -(defstyle name-text - {:color colors/black - :font-size 17 - :ios {:letter-spacing -0.2}}) - -(def address-text - {:color colors/white - :font-size 12}) - -(def wallet-qr-code - {:flex-grow 1 - :flex-direction :column}) - -(def account-toolbar - {:background-color colors/white}) - -(def toolbar-contents - {:flex-grow 1 - :flex-direction :row - :height 55 - :align-items :center - :justify-content :center}) - -(def qr-code - {:flex-grow 1 - :align-items :center - :justify-content :center}) - -(def footer - {:flex-direction :row - :justify-content :center - :padding-top 17}) - -(def wallet-info - {:flex-grow 1 - :align-items :center - :padding-bottom 20}) - -(def hash-value-type - {:color colors/black - :padding-bottom 5}) - -(def hash-value-text - {:color colors/black - :align-self :stretch - :margin-horizontal 60 - :text-align :center - :font-size 15 - :letter-spacing -0.2 - :line-height 20}) - -(def done-button-text - {:color colors/white}) diff --git a/src/status_im/ui/components/qr_code_viewer/views.cljs b/src/status_im/ui/components/qr_code_viewer/views.cljs deleted file mode 100644 index 136863b56ba..00000000000 --- a/src/status_im/ui/components/qr_code_viewer/views.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.ui.components.qr-code-viewer.views - (:require [reagent.core :as reagent] - [re-frame.core :as re-frame] - [status-im.react-native.js-dependencies :as rn-dependencies] - [status-im.ui.components.qr-code-viewer.styles :as styles] - [status-im.ui.components.react :as react] - [status-im.ui.components.text :as text])) - -(defn qr-code [props] - (reagent/create-element - rn-dependencies/qr-code - (clj->js (merge {:inverted true} props)))) - -(defn- footer [style value] - [react/view styles/footer - [react/view styles/wallet-info - [text/selectable-text {:value value - :style (merge styles/hash-value-text style)}]]]) - -(defn qr-code-viewer [{:keys [style hint-style footer-style]} value hint legend] - {:pre [(not (nil? value))]} - (let [{:keys [width height]} @(re-frame/subscribe [:dimensions/window])] - [react/view {:style (merge styles/qr-code style)} - [react/text {:style (merge styles/qr-code-hint hint-style)} - hint] - (when width - (let [size (int (* 0.7 (min width height)))] - [react/view {:style (styles/qr-code-container size) - :accessibility-label :qr-code-image} - [qr-code {:value value - :size (- size (* 2 styles/qr-code-padding))}]])) - [footer footer-style legend]])) diff --git a/src/status_im/ui/components/status_bar/styles.cljs b/src/status_im/ui/components/status_bar/styles.cljs deleted file mode 100644 index 4be05fb0b9e..00000000000 --- a/src/status_im/ui/components/status_bar/styles.cljs +++ /dev/null @@ -1,109 +0,0 @@ -(ns status-im.ui.components.status-bar.styles - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :as styles] - [status-im.utils.platform :as platform]) - (:require-macros [status-im.utils.styles :refer [defstyle]])) - -(def elevation 2) - -(defn- create-status-bar-style [{:keys [background-color bar-style translucent?] - :or {bar-style "light-content"}}] - {:background-color (if translucent? "transparent" background-color) - :translucent translucent? - :bar-style bar-style}) - -(defn- create-view-style [{:keys [background-color height elevation] - :or {height (get platform/platform-specific :status-bar-default-height)}}] - {:background-color background-color - :elevation elevation - :height height}) - -;; :main -(defstyle status-bar-main - {:ios (create-status-bar-style {:background-color colors/white - :bar-style "default"}) - :android (create-status-bar-style {:translucent? true - :bar-style "dark-content"})}) - -(def view-main - (create-view-style {:background-color colors/white})) - -;; :transparent -(defstyle status-bar-transparent - {:ios (create-status-bar-style {:background-color styles/color-transparent}) - :android (create-status-bar-style {:translucent? true})}) - -(def view-transparent - (create-view-style {:background-color styles/color-transparent})) - -;; :modal -(defstyle status-bar-modal - {:ios (create-status-bar-style {:background-color "#2f3031"}) - :android (create-status-bar-style {:background-color colors/black})}) - -(defstyle view-modal - {:ios (create-view-style {:background-color "#2f3031"}) - :android (create-view-style {:background-color colors/black - :height 0})}) - -;; :modal-white -(defstyle status-bar-modal-white - {:ios (create-status-bar-style {:background-color colors/white - :bar-style "default"}) - :android (create-status-bar-style {:background-color colors/black - :bar-style "light-content"})}) - -(defstyle view-modal-white - {:ios (create-view-style {:background-color colors/white}) - :android (create-view-style {:background-color colors/black - :height 0})}) - -;; :modal-wallet -(def status-bar-modal-wallet - (create-status-bar-style {:background-color colors/blue})) - -(defstyle view-modal-wallet - {:ios (create-view-style {:background-color colors/blue}) - :android (create-view-style {:background-color colors/blue - :height 0 - :elevation elevation})}) - -;; :transaction -(defstyle status-bar-transaction - {:ios (create-status-bar-style {:background-color styles/color-transparent}) - :android (create-status-bar-style {:background-color styles/color-dark-blue-2})}) - -(defstyle view-transaction - {:ios (create-view-style {:background-color styles/color-transparent}) - :android (create-view-style {:background-color styles/color-dark-blue-2 - :height 0})}) - -;; TODO(jeluard) Fix status-bar mess by removing useless view and introducing 2dn level tab-bar - -;; :wallet HOME -(defstyle status-bar-wallet-tab - {:ios (create-status-bar-style {:background-color colors/blue}) - :android (create-status-bar-style {:translucent? true})}) - -(def view-wallet-tab - (create-view-style {:background-color styles/color-blue4})) - -;; :wallet -(defstyle status-bar-wallet - {:ios (create-status-bar-style {:background-color colors/blue}) - :android (create-status-bar-style {:translucent? true})}) - -(def view-wallet - (create-view-style {:background-color styles/color-blue4 - :elevation elevation})) - -;; :default -(defstyle status-bar-default - {:ios (create-status-bar-style {:background-color colors/white - :bar-style "default"}) - :android (create-status-bar-style {:translucent? true - :bar-style "dark-content"})}) - -(defstyle view-default - (create-view-style {:background-color colors/white - :elevation elevation})) diff --git a/src/status_im/ui/components/status_bar/view.cljs b/src/status_im/ui/components/status_bar/view.cljs deleted file mode 100644 index 33d86b26f25..00000000000 --- a/src/status_im/ui/components/status_bar/view.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.ui.components.status-bar.view - (:require [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.styles :as styles])) - -(defn status-bar [{:keys [type flat?]}] - (let [[status-bar-style view-style] - (case type - :main [styles/status-bar-main styles/view-main] - :transparent [styles/status-bar-transparent styles/view-transparent] - :modal [styles/status-bar-modal styles/view-modal] - :modal-white [styles/status-bar-modal-white styles/view-modal-white] - :modal-wallet [styles/status-bar-modal-wallet styles/view-modal-wallet] - :transaction [styles/status-bar-transaction styles/view-transaction] - :wallet [styles/status-bar-wallet styles/view-wallet] - :wallet-tab [styles/status-bar-wallet-tab styles/view-wallet-tab] - [styles/status-bar-default styles/view-default])] - [react/view - [react/status-bar (cond-> status-bar-style flat? (assoc :elevation 0))] - [react/view {:style (cond-> view-style flat? (assoc :elevation 0))}]])) diff --git a/src/status_im/ui/components/styles.cljs b/src/status_im/ui/components/styles.cljs deleted file mode 100644 index cb859ea4833..00000000000 --- a/src/status_im/ui/components/styles.cljs +++ /dev/null @@ -1,121 +0,0 @@ -(ns status-im.ui.components.styles - (:require [status-im.ui.components.colors :as colors])) - -(def color-transparent "transparent") -(def color-blue "#7099e6") -(def color-blue4 "#4360df") ; colors/blue -(def color-blue4-transparent "rgba(67, 96, 223, 0.10)") -(def color-blue6 "#3745AF") -(def color-blue-transparent "#7099e632") -(def color-black "#000000") -(def color-purple "#a187d5") -(def color-gray-transparent "rgba(0, 0, 0, 0.4)") -(def color-gray4-transparent "rgba(147, 155, 161, 0.2)") -(def color-gray "#838c93de") -(def color-gray2 "#8f838c93") -(def color-gray3 "#00000040") -(def color-gray4 "#939ba1") -(def color-gray5 "#d9dae1") -(def color-gray6 "#212121") -(def color-gray7 "#9fa3b4") -(def color-gray9 "#E9EBEC") -(def color-dark "#49545d") -(def color-white "white") -(def color-white-transparent "#ffffff66") -(def color-white-transparent-1 "#f1f1f11a") -(def color-white-transparent-3 "#FFFFFF1A") -(def color-white-transparent-4 "#FFFFFF33") -(def color-white-transparent-5 "#FFFFFF8C") -(def color-light-blue "#628fe3") -(def color-light-blue-transparent "#628fe333") -(def color-dark-blue-2 "#1f253f") -(def color-light-gray "#EEF2F5") -(def color-light-gray3 "#e8ebec") -(def color-light-gray6 "#BAC1C6") -(def color-red "red") -(def color-red-2 "#d84b4b") -(def color-light-red "#e86363") -(def color-green-3 "#44d058") -(def color-green-3-light "rgba(68, 208, 88, 0.2)") -(def color-green-4 "#0dcd8d") - -(def color-separator "#D6D6D6") - -(def text1-color color-black) -(def text2-color color-gray) -(def text4-color color-gray4) -(def icon-dark-color color-dark) -(def icon-gray-color color-gray7) -(def icon-red-color color-red-2) -(def online-color color-light-blue) -(def new-messages-count-color color-blue-transparent) -(def chat-background color-light-gray) -(def separator-color "#0000001f") -(def default-chat-color color-purple) - -(defn random-chat-color [] - (rand-nth colors/chat-colors)) - -;;rgb 237 241 243 - -(def flex - {:flex 1}) - -(def create-icon - {:font-size 20 - :height 22 - :color :white}) - -(def icon-back - {:width 8 - :height 14}) - -(def icon-default - {:width 24 - :height 24}) - -(def icon-add - {:width 24 - :height 24 - :color colors/blue}) - -(def icon-add-illuminated - {:width 24 - :height 24 - :color colors/blue - :container-style {:background-color (colors/alpha colors/blue 0.12) - :border-radius 32 - :width 32 - :height 32 - :display :flex - :justify-content :center - :align-items :center}}) - -(def icon-ok - {:width 18 - :height 14}) - -(def icon-qr - {:width 23 - :height 22}) - -(def button-input-container - {:flex 1 - :flex-direction :row}) - -(def button-input - {:flex 1 - :flex-direction :column}) - -(def modal - {:position :absolute - :left 0 - :top 0 - :right 0 - :bottom 0}) - -(def main-container - {:background-color color-white - :flex 1}) - -(def border-radius 8) diff --git a/src/status_im/ui/components/svgimage.cljs b/src/status_im/ui/components/svgimage.cljs deleted file mode 100644 index 4f1eb0e6005..00000000000 --- a/src/status_im/ui/components/svgimage.cljs +++ /dev/null @@ -1,53 +0,0 @@ -(ns status-im.ui.components.svgimage - (:require [status-im.ui.components.react :as react] - [reagent.core :as reagent] - [status-im.utils.platform :as platform] - [status-im.utils.http :as http])) - -(defn html [uri width height] - (str - "\n - - - - - -
- -
- - ")) - -(defn svgimage [{:keys [style source]}] - (let [width (reagent/atom nil) - {:keys [uri k] :or {k 1}} source] - (when (http/url-sanitized? uri) - (fn [] - [react/view {:style style - :on-layout #(reset! width (-> % .-nativeEvent .-layout .-width))} - [react/web-view - {:java-script-enabled false - :third-party-cookies-enabled false - :scroll-enabled false - :bounces false - :scales-page-to-fit platform/android? - :style {:width @width :height (* @width k) :background-color :transparent} - :source {:html (html uri @width (* @width k))}}]])))) \ No newline at end of file diff --git a/src/status_im/ui/components/text.cljs b/src/status_im/ui/components/text.cljs deleted file mode 100644 index 7a9eb016110..00000000000 --- a/src/status_im/ui/components/text.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.ui.components.text - (:require [status-im.ui.components.react :as react] - [status-im.utils.platform :as platform])) - -(defn- selectable-text [{:keys [value style]}] - (if platform/ios? - [react/text-input {:value value - :editable false - :multiline true - :style style}] - [react/text {:style style - :accessibility-label :address-text - :selectable true} - value])) diff --git a/src/status_im/ui/components/text_input/styles.cljs b/src/status_im/ui/components/text_input/styles.cljs deleted file mode 100644 index 7e15038e1fc..00000000000 --- a/src/status_im/ui/components/text_input/styles.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.ui.components.text-input.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.utils.platform :as p])) - -(def label - {:font-size 14 - :letter-spacing -0.2 - :color colors/black}) - -(defn input-container [height] - {:padding 16 - :justify-content :center - :margin-vertical 8 - :height (or height 52) - :border-radius 8 - :background-color colors/gray-lighter}) - -(defstyle input - {:font-size 15 - :letter-spacing -0.2 - :color colors/black - :padding 0 - :desktop {:height 52}}) - -(defn error [label?] - {:bottom-value (if label? -20 0) - :color colors/red-light - :font-size 12}) \ No newline at end of file diff --git a/src/status_im/ui/components/text_input/view.cljs b/src/status_im/ui/components/text_input/view.cljs deleted file mode 100644 index 68901d4b2ae..00000000000 --- a/src/status_im/ui/components/text_input/view.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.ui.components.text-input.view - (:require [status-im.ui.components.react :as react] - [status-im.ui.components.text-input.styles :as styles] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.tooltip.views :as tooltip])) - -(defn text-input-with-label [{:keys [label content error style height container] :as props}] - [react/view - (when label - [react/text {:style styles/label} - label]) - [react/view {:style (merge (styles/input-container height) container)} - [react/text-input - (merge - {:style (merge styles/input style) - :placeholder-text-color colors/gray - :auto-focus true - :auto-capitalize :none} - (dissoc props :style :height))] - (when content content)] - (when error - [tooltip/tooltip error (styles/error label)])]) diff --git a/src/status_im/ui/components/toolbar/actions.cljs b/src/status_im/ui/components/toolbar/actions.cljs deleted file mode 100644 index 3a2236679bf..00000000000 --- a/src/status_im/ui/components/toolbar/actions.cljs +++ /dev/null @@ -1,54 +0,0 @@ -(ns status-im.ui.components.toolbar.actions - (:require [re-frame.core :as re-frame] - [status-im.ui.components.styles :as styles])) - -(defn add [illuminated? handler] - {:icon :icons/add - :icon-opts (if illuminated? styles/icon-add-illuminated styles/icon-add) - :handler handler}) - -(defn opts [options] - {:icon :icons/options - :options options}) - -(defn back [handler] - {:icon :icons/back - :handler handler - :accessibility-label :back-button}) - -(def default-handler #(re-frame/dispatch [:navigate-back])) - -(def home-handler #(re-frame/dispatch [:navigate-to-clean :home])) - -(def default-back - (back default-handler)) - -(def home-back - (back home-handler)) - -(defn back-white [handler] - {:icon :icons/back - :icon-opts {:color :white} - :handler handler - :accessibility-label :back-button}) - -(defn close [handler] - {:icon :icons/close - :handler handler}) - -(def default-close - (close default-handler)) - -(defn close-white [handler] - {:icon :icons/close - :icon-opts {:color :white} - :handler handler}) - -(defn list-white [handler] - {:icon :icons/transaction-history - :icon-opts {:color :white} - :handler handler}) - -(defn add-wallet [handler] - {:icon :icons/add-wallet - :handler handler}) diff --git a/src/status_im/ui/components/toolbar/styles.cljs b/src/status_im/ui/components/toolbar/styles.cljs deleted file mode 100644 index 458d30a04c3..00000000000 --- a/src/status_im/ui/components/toolbar/styles.cljs +++ /dev/null @@ -1,91 +0,0 @@ -(ns status-im.ui.components.toolbar.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def toolbar-background colors/white) - -(def toolbar-height 56) -(def toolbar-icon-width 24) -(def toolbar-icon-height 24) -(def toolbar-icon-spacing 24) - -(defnstyle toolbar [background-color flat?] - {:flex 0 - :flex-direction :row - :align-items :center - :justify-content :space-between - :background-color (or background-color toolbar-background) - :elevation (if flat? 0 2) - :android {:height 55} - :ios {:height 56}}) - -(def toolbar-nav-actions-container - {:flex-direction :row - :margin-left 4}) - -(defstyle toolbar-container - {:flex 1 - :align-items :center}) - -(def toolbar-title-container - {:flex 1 - :flex-direction :column - :margin-left 6}) - -(defstyle toolbar-title-text - {:color colors/black - :letter-spacing -0.2 - :font-size 17 - :ios {:text-align :center} - :android {:text-align :left - :margin-left 22}}) - -(def toolbar-actions - {:flex 0 - :flex-direction :row}) - -(defn toolbar-actions-container [actions-count custom] - (merge {:flex-direction :row} - (when-not custom {:margin-right 4}) - (when (and (zero? actions-count) (not custom)) - {:width (+ toolbar-icon-width toolbar-icon-spacing)}))) - -(def toolbar-action - {:flex 1 - :align-items :center - :justify-content :center}) - -(def action-default - {:width 24 - :height 24}) - -(defn nav-item-button [unread-messages?] - {:margin-left 13 - :margin-right (if unread-messages? -5 13)}) - -(defstyle item - {:ios {:padding-horizontal 12 - :padding-vertical 16} - :android {:padding 16}}) - -(def item-text - {:color colors/blue - :font-size 17}) - -(defstyle item-text-action - {:color colors/blue - :ios {:font-size 15 - :letter-spacing -0.2} - :android {:font-size 14 - :letter-spacing 0.5}}) - -(def toolbar-text-action-disabled {:color colors/gray}) - -(def item-text-white-background {:color colors/blue}) - -;;TODO(goranjovic) - Breaks the toolbar title into new line on smaller screens -;;e.g. see Discover > Popular hashtags on iPhone 5s -(def ios-content-item {:position :absolute :right 40 :left 40}) - -(def counter-container - {:top 3}) diff --git a/src/status_im/ui/components/toolbar/view.cljs b/src/status_im/ui/components/toolbar/view.cljs deleted file mode 100644 index 86e63cf674d..00000000000 --- a/src/status_im/ui/components/toolbar/view.cljs +++ /dev/null @@ -1,161 +0,0 @@ -(ns status-im.ui.components.toolbar.view - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.react :as react] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.toolbar.styles :as styles] - [status-im.utils.platform :as platform] - [status-im.utils.core :as utils] - [status-im.ui.components.common.common :as components.common])) - -;; Navigation item - -(defn nav-item - [{:keys [handler accessibility-label style] :or {handler #(re-frame/dispatch [:navigate-back])}} item] - [react/touchable-highlight - (merge {:on-press handler} - (when accessibility-label - {:accessibility-label accessibility-label})) - [react/view {:style style} - item]]) - -(defn nav-button - [{:keys [icon icon-opts unread-messages?] :as props}] - [nav-item (merge {:style (styles/nav-item-button unread-messages?)} props) - [vector-icons/icon icon (if unread-messages? - (assoc icon-opts :color :active) - icon-opts)]]) - -(defview nav-button-with-count [props] - (letsubs [unread-messages-number [:get-chats-unread-messages-number]] - (let [unread-messages? (pos? unread-messages-number)] - [react/view {:flex-direction :row} - [nav-button (assoc props :unread-messages? unread-messages?)] - (when unread-messages? - [nav-item (merge {:style styles/counter-container} props) - [components.common/counter unread-messages-number]])]))) - -(defn nav-text - ([text] (nav-text nil text)) - ([{:keys [handler] :as props} text] - [react/text (utils/deep-merge {:style (merge styles/item styles/item-text) - :on-press (or handler #(re-frame/dispatch [:navigate-back]))} - props) - text])) - -(defn nav-clear-text - ([text] (nav-clear-text nil text)) - ([props text] - (nav-text (merge props styles/item-text-white-background) text))) - -(def default-nav-back [nav-button actions/default-back]) - -(defn nav-back-count - ([] - [nav-button-with-count actions/default-back]) - ([{:keys [home?]}] - [nav-button-with-count (if home? actions/home-back actions/default-back)])) - -(defn default-done - "Renders a touchable icon on Android or a label or iOS." - [{:keys [icon] :as props}] - (if platform/ios? - [react/view - [nav-text props - (i18n/label :t/done)]] - [react/view - [nav-button (merge props {:icon (or icon :icons/close)})]])) - -;; Content - -(defn content-wrapper [content] - [react/view {:style styles/toolbar-container} - content]) - -(defn content-title - ([title] (content-title nil title)) - ([title-style title] - (content-title title-style title nil nil)) - ([title-style title subtitle-style subtitle] - [react/view {:style styles/toolbar-title-container} - [react/text {:style (merge styles/toolbar-title-text title-style) - :font :toolbar-title} - title] - (when subtitle - [react/text {:style subtitle-style} - subtitle])])) - -;; Actions - -(defn text-action [{:keys [style handler disabled? accessibility-label]} title] - [react/text (cond-> {:style (merge styles/item styles/item-text style - (when disabled? styles/toolbar-text-action-disabled)) - :on-press (when-not disabled? handler) - :uppercase? true} - accessibility-label - (assoc :accessibility-label accessibility-label)) - title]) - -(def blank-action [react/view {:style (merge styles/item styles/toolbar-action)}]) - -(defn- icon-action [icon {:keys [overlay-style] :as icon-opts} handler] - [react/touchable-highlight {:on-press handler} - [react/view {:style (merge styles/item styles/toolbar-action)} - (when overlay-style - [react/view overlay-style]) - [vector-icons/icon icon (merge {:container-style styles/action-default} icon-opts)]]]) - -(defn- option-actions [icon icon-opts options] - [icon-action icon icon-opts - #(list-selection/show {:options options})]) - -(defn actions [v] - [react/view {:style styles/toolbar-actions} - (for [{:keys [image icon icon-opts options handler]} v] - (with-meta - (cond (= image :blank) - blank-action - - options - [option-actions icon icon-opts options] - - :else - [icon-action icon icon-opts handler]) - {:key (str "action-" (or image icon))}))]) - -(defn toolbar - ([props nav-item content-item] (toolbar props nav-item content-item nil)) - ([{:keys [background-color style flat?]} - nav-item - content-item - action-items] - [react/view {:style (merge (styles/toolbar background-color flat?) style)} - [react/view styles/ios-content-item - content-item] - (when nav-item - [react/view {:style styles/toolbar-nav-actions-container} - nav-item]) - [react/view components.styles/flex] - action-items])) - -(defn platform-agnostic-toolbar - ([props nav-item content-item] (platform-agnostic-toolbar props nav-item content-item [actions [{:image :blank}]])) - ([{:keys [background-color style flat?]} - nav-item - content-item - action-items] - [react/view {:style (merge (styles/toolbar background-color flat?) style)} - (when nav-item - [react/view {:style (styles/toolbar-nav-actions-container 0)} - nav-item]) - content-item - action-items])) - -(defn simple-toolbar - "A simple toolbar composed of a nav-back item and a single line title." - ([] (simple-toolbar nil)) - ([title] (toolbar nil default-nav-back [content-title title]))) diff --git a/src/status_im/ui/components/tooltip/animations.cljs b/src/status_im/ui/components/tooltip/animations.cljs deleted file mode 100644 index 0308b2ddc4e..00000000000 --- a/src/status_im/ui/components/tooltip/animations.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.ui.components.tooltip.animations - (:require [status-im.ui.components.animation :as animation])) - -(defn animate-tooltip [bottom-value bottom-anim-value opacity-value delta] - (fn [] - (animation/start - (animation/parallel - [(animation/timing opacity-value {:toValue 1 - :duration 500}) - (animation/timing bottom-anim-value {:toValue (- bottom-value delta) - :easing (.bezier (animation/easing) 0.685, 0.000, 0.025, 1.185) - :duration 500})])))) \ No newline at end of file diff --git a/src/status_im/ui/components/tooltip/styles.cljs b/src/status_im/ui/components/tooltip/styles.cljs deleted file mode 100644 index 82e45726d3a..00000000000 --- a/src/status_im/ui/components/tooltip/styles.cljs +++ /dev/null @@ -1,58 +0,0 @@ -(ns status-im.ui.components.tooltip.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.ui.components.colors :as colors])) - -(def tooltip-container - {:position :absolute - :align-items :center - :left 0 - :right 0 - :top 0}) - -(defstyle bottom-tooltip-container - {:position :absolute - :align-items :center - :left 12 - :right 12 - :ios {:top 0} - :android {:top 30}}) - -(defn tooltip-animated [bottom-value opacity-value] - {:position :absolute - :align-items :center - :left 0 - :right 0 - :bottom bottom-value - :opacity opacity-value}) - -(defn tooltip-text-container [color] - {:padding-horizontal 16 - :padding-vertical 9 - :background-color color - :border-radius 8}) - -(def bottom-tooltip-text-container - {:flex-direction :row - :align-items :center - :margin-horizontal 12 - :padding-horizontal 16 - :padding-vertical 9 - :background-color colors/gray-notifications - :border-radius 8}) - -(defn tooltip-text [font-size] - {:color styles/color-red-2 - :font-size font-size}) - -(def bottom-tooltip-text - {:color colors/white - :font-size 15}) - -(def tooltip-triangle - {:width 16 - :height 8}) - -(def close-icon - {:margin-right 4 - :margin-left 10}) \ No newline at end of file diff --git a/src/status_im/ui/components/tooltip/views.cljs b/src/status_im/ui/components/tooltip/views.cljs deleted file mode 100644 index 960036ef4e7..00000000000 --- a/src/status_im/ui/components/tooltip/views.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.ui.components.tooltip.views - (:require-macros [status-im.utils.views :as views]) - (:require [status-im.ui.components.animation :as animation] - [status-im.ui.components.tooltip.animations :as animations] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.tooltip.styles :as styles] - [status-im.ui.components.colors :as colors] - [reagent.core :as reagent])) - -(views/defview tooltip [label & [{:keys [bottom-value color font-size] :or {bottom-value -30 color :white font-size 15}}]] - (views/letsubs [bottom-anim-value (animation/create-value bottom-value) - opacity-value (animation/create-value 0)] - {:component-did-mount (animations/animate-tooltip bottom-value bottom-anim-value opacity-value 10)} - [react/view styles/tooltip-container - [react/animated-view {:style (styles/tooltip-animated bottom-anim-value opacity-value)} - [react/view (styles/tooltip-text-container color) - [react/text {:style (styles/tooltip-text font-size)} label]] - [vector-icons/icon :icons/tooltip-triangle {:color color :style styles/tooltip-triangle}]]])) - -(views/defview bottom-tooltip-info [label on-close] - (views/letsubs [bottom-anim-value (animation/create-value -150) - opacity-value (animation/create-value 0)] - {:component-did-mount (animations/animate-tooltip -150 bottom-anim-value opacity-value -10)} - [react/view styles/bottom-tooltip-container - [react/animated-view {:style (styles/tooltip-animated bottom-anim-value opacity-value)} - [vector-icons/icon :icons/tooltip-triangle {:color colors/gray-notifications - :style styles/tooltip-triangle - :container-style {:transform [{:rotate "180deg"}]}}] - [react/view styles/bottom-tooltip-text-container - [react/text {:style styles/bottom-tooltip-text} label] - [react/touchable-highlight {:on-press on-close - :style styles/close-icon} - [vector-icons/icon :icons/close {:color colors/white}]]]]])) diff --git a/src/status_im/ui/components/webview_bridge.cljs b/src/status_im/ui/components/webview_bridge.cljs deleted file mode 100644 index e474880f82b..00000000000 --- a/src/status_im/ui/components/webview_bridge.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.ui.components.webview-bridge - (:require [reagent.core :as reagent] - [status-im.react-native.js-dependencies :as js-dependencies])) - -(def webview-bridge-class - (reagent/adapt-react-class (.-default js-dependencies/webview-bridge))) - -(defn webview-bridge [opts] - [webview-bridge-class opts]) diff --git a/src/status_im/ui/market/style.cljs b/src/status_im/ui/market/style.cljs new file mode 100644 index 00000000000..b10ca56b69d --- /dev/null +++ b/src/status_im/ui/market/style.cljs @@ -0,0 +1,41 @@ +(ns status-im.ui.market.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area] + [status-im.contexts.shell.constants :as constants])) + +(defn header-background + [theme] + (colors/theme-colors colors/white colors/neutral-95 theme)) + +(defn swap-header-container + [theme] + {:background-color (header-background theme) + :padding-horizontal 20 + :padding-vertical 12 + :flex-direction :row + :justify-content :space-between}) + +(defn sort-header-container + [theme] + {:background-color (header-background theme) + :padding-horizontal 20 + :padding-vertical 12 + :align-items :center + :flex-direction :row}) + +(defn market-header-text + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) + +(defn sort-text + [theme] + {:color (colors/theme-colors colors/neutral-40 colors/neutral-40 theme)}) + +(def list-container + {:padding-bottom constants/floating-shell-button-height}) + +(defn home-container + [] + {:margin-top safe-area/top + :flex 1}) diff --git a/src/status_im/ui/market/subs.cljs b/src/status_im/ui/market/subs.cljs new file mode 100644 index 00000000000..4af0aee261c --- /dev/null +++ b/src/status_im/ui/market/subs.cljs @@ -0,0 +1,38 @@ +(ns status-im.ui.market.subs + (:require + [clojure.string :as string] + [re-frame.core :as rf] + [status-im.contexts.wallet.common.utils :as wallet.utils] + [utils.number] + [utils.string])) + +(rf/reg-sub + :ui.market/leaderboard + :<- [:domain.market/leaderboard] + (fn [leaderboard] + (->> leaderboard + (sort-by val) + (map key)))) + +(rf/reg-sub + :ui.market/leaderboard-loading? + :<- [:ui.market/leaderboard] + (fn [leaderboard] + (empty? leaderboard))) + +(rf/reg-sub + :ui.market/token + :<- [:domain.market/prices] + :<- [:domain.market/tokens] + :<- [:domain.market/leaderboard] + (fn [[token-prices token-details leaderboard] [_ token-id]] + (let [{:keys [current-price price-change-percentage-24h market-cap]} (get token-prices token-id) + {short-name :symbol name :name image :image} (get token-details token-id) + position (get leaderboard token-id)] + {:price (wallet.utils/prettify-balance "$" current-price) + :percentage-change (utils.number/naive-round price-change-percentage-24h 2) + :market-cap (utils.number/format-number-with-si-suffix market-cap) + :short-name (when short-name (string/upper-case short-name)) + :token-rank position + :token-name (utils.string/truncate name 18) + :image image}))) diff --git a/src/status_im/ui/market/view.cljs b/src/status_im/ui/market/view.cljs new file mode 100644 index 00000000000..788e3d4b6d7 --- /dev/null +++ b/src/status_im/ui/market/view.cljs @@ -0,0 +1,96 @@ +(ns status-im.ui.market.view + (:require + [oops.core :as oops] + [quo.context] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.home.top-nav.view :as common.top-nav] + [status-im.common.refreshable-flat-list.view :as refreshable-flat-list] + [status-im.ui.market.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn swap-header + [] + (let [theme (quo.context/use-theme) + operable-accounts (rf/sub [:wallet/operable-accounts])] + [rn/view {:style (style/swap-header-container theme)} + [quo/text + {:weight :semi-bold + :size :heading-1 + :style (style/market-header-text theme)} + (i18n/label :t/market)] + [quo/button + {:size 32 + :icon-left :i/swap + :on-press #(rf/dispatch [:app.wallet/start-swap operable-accounts])} + (i18n/label :t/swap)]])) + +(defn sort-header + [] + (let [theme (quo.context/use-theme)] + [rn/view {:style (style/sort-header-container theme)} + [quo/text + {:size :paragraph-2 + :style (style/sort-text theme)} + (i18n/label :t/market-cap)] + [quo/icon :i/arrow-down + {:size 12 + :color colors/neutral-50 + :container-style {:margin-left 4}}]])) + +(defn token + [token-id] + (let [{:keys [price percentage-change market-cap short-name token-rank token-name image]} + (rf/sub [:ui.market/token token-id])] + [quo/market-token + {:token-short-name short-name + :token-rank token-rank + :token-name token-name + :market-cap market-cap + :price price + :token-image image + :percentage-change percentage-change + ;; TODO: fix if needed + ;; :on-press (fn [] + ;; (rf/dispatch [:open-modal :screen/market.token])) + }])) + +(defn ensure-tokens-updating + [event] + (when-let [first-token-name (oops/oget event "viewableItems.?0.?item")] + (rf/dispatch [:app.market/ensure-tokens-updating first-token-name + (oops/oget event "viewableItems.length")]))) + +(defn view + [] + (let [token-ids (rf/sub [:ui.market/leaderboard]) + tokens-loading? (rf/sub [:ui.market/leaderboard-loading?]) + reload-leaderboard (rn/use-callback + (fn [] (rf/dispatch [:app.market/reload-leaderboard]))) + load-more-tokens (rn/use-callback + (fn [] (rf/dispatch [:app.market/load-more-leaderboard-tokens])))] + (rn/use-mount + #(rf/dispatch [:app.market/load-leaderboard])) + (rn/use-unmount + #(rf/dispatch [:app.market/close-leaderboard])) + [rn/view {:style (style/home-container)} + [common.top-nav/view] + [refreshable-flat-list/view + {:refresh-control [rn/refresh-control + {:refreshing tokens-loading? + :colors [colors/neutral-40] + :tint-color colors/neutral-40 + :on-refresh reload-leaderboard}] + :header [rn/view + [swap-header] + [sort-header]] + :content-container-style style/list-container + :sticky-header-indices [0] + :data token-ids + :render-fn token + :on-end-reached load-more-tokens + :viewability-config {:item-visible-percent-threshold 100 + :minimum-view-time 200} + :on-viewable-items-changed ensure-tokens-updating}]])) diff --git a/src/status_im/ui/screens/accounts/create/navigation.cljs b/src/status_im/ui/screens/accounts/create/navigation.cljs deleted file mode 100644 index 57ec659a56d..00000000000 --- a/src/status_im/ui/screens/accounts/create/navigation.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.ui.screens.accounts.create.navigation - (:require [status-im.ui.screens.navigation :as nav])) - -(defmethod nav/preload-data! :create-account - [db] - (update db :accounts/create - #(-> % - (assoc :step :enter-password) - (dissoc :password :password-confirm :name :error)))) \ No newline at end of file diff --git a/src/status_im/ui/screens/accounts/create/styles.cljs b/src/status_im/ui/screens/accounts/create/styles.cljs deleted file mode 100644 index bb157844fa4..00000000000 --- a/src/status_im/ui/screens/accounts/create/styles.cljs +++ /dev/null @@ -1,58 +0,0 @@ -(ns status-im.ui.screens.accounts.create.styles - (:require [status-im.ui.components.colors :as colors])) - -(def create-account-view - {:flex 1 - :background-color colors/white}) - -(def account-creating-view - {:flex 1 - :padding-horizontal 14}) - -(def account-creating-logo-container - {:margin-top 37 - :align-items :center}) - -(def account-creating-logo - {:size 82 - :icon-size 34}) - -(def account-creating-indicatior - {:flex 1 - :align-items :center - :justify-content :center - :margin-bottom 100}) - -(def account-creating-text - {:font-size 14 - :line-height 21 - :letter-spacing -0.2 - :text-align :center - :color colors/black - :margin-top 16}) - -(def logo-container - {:position :absolute - :top 37 - :left 0 - :right 0 - :align-items :center}) - -(def logo - {:size 82 - :icon-size 34}) - -(def input-container - {:margin-horizontal 16 - :margin-top 105}) - -(def input-description - {:font-size 14 - :letter-spacing -0.2 - :color colors/gray - :line-height 21}) - -(def bottom-container - {:flex-direction :row - :margin-horizontal 12 - :margin-vertical 15}) \ No newline at end of file diff --git a/src/status_im/ui/screens/accounts/create/views.cljs b/src/status_im/ui/screens/accounts/create/views.cljs deleted file mode 100644 index 6d0a71285fa..00000000000 --- a/src/status_im/ui/screens/accounts/create/views.cljs +++ /dev/null @@ -1,85 +0,0 @@ -(ns status-im.ui.screens.accounts.create.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.components.react :as react] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.i18n :as i18n] - [re-frame.core :as re-frame] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.react :as components] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.text-input.view :as text-input] - [status-im.ui.screens.accounts.create.styles :as styles])) - -(def steps - {:enter-password {:input-key :password - :input-label (i18n/label :t/password) - :input-placeholder (i18n/label :t/password-placeholder) - :input-description (i18n/label :t/password-description)} - :confirm-password {:input-key :password-confirm - :input-label (i18n/label :t/confirm) - :input-placeholder (i18n/label :t/password-placeholder2) - :input-description (i18n/label :t/password-description)} - :account-creating nil - :enter-name {:input-key :name - :input-label (i18n/label :t/name) - :input-placeholder (i18n/label :t/name-placeholder) - :input-description (i18n/label :t/name-description)}}) - -(defn next-step [step password password-confirm] - (case step - :enter-password (re-frame/dispatch [:set-in [:accounts/create :step] :confirm-password]) - :confirm-password (if (= password password-confirm) - (re-frame/dispatch [:create-account]) - (re-frame/dispatch [:set-in [:accounts/create :error] (i18n/label :t/password_error1)])) - :enter-name (re-frame/dispatch [:account-set-name]))) - -(defn step-back [step] - (case step - :enter-password (re-frame/dispatch [:navigate-back]) - :confirm-password (re-frame/dispatch [:reset-account-creation]))) - -(defview input [step error] - [text-input/text-input-with-label - {:label (get-in steps [step :input-label]) - :placeholder (get-in steps [step :input-placeholder]) - :on-change-text #(re-frame/dispatch [:account-set-input-text (get-in steps [step :input-key]) %]) - :secure-text-entry (boolean (#{:enter-password :confirm-password} step)) - :error error}]) - -(defview create-account [] - (letsubs [step [:get-in [:accounts/create :step]] - next-enabled? [:get-account-creation-next-enabled?] - error [:get-in [:accounts/create :error]] - password [:get-in [:accounts/create :password]] - password-confirm [:get-in [:accounts/create :password-confirm]]] - [react/keyboard-avoiding-view {:style styles/create-account-view} - [status-bar/status-bar {:flat? true}] - (when (= :account-creating step) - [react/view styles/account-creating-view - [react/view styles/account-creating-logo-container - [components.common/logo styles/account-creating-logo]] - [react/view {:style styles/account-creating-indicatior} - [components/activity-indicator {:animating true}] - [react/text {:style styles/account-creating-text} - (i18n/label :t/creating-your-account)]]]) - (when (#{:enter-password :confirm-password :enter-name} step) - [react/view components.styles/flex - [toolbar/toolbar {:flat? true} (when (#{:enter-password :confirm-password} step) - (toolbar/nav-button (actions/back #(step-back step)))) nil] - [react/view {:style styles/logo-container} - [components.common/logo styles/logo]] - ^{:key (str "step" step)} - [react/view components.styles/flex - [react/view {:style styles/input-container} - [input step error] - [react/text {:style styles/input-description} - (get-in steps [step :input-description])]] - [react/view {:style components.styles/flex}] - [react/view {:style styles/bottom-container} - [react/view {:style components.styles/flex}] - [components.common/bottom-button - {:forward? true - :disabled? (not next-enabled?) - :on-press #(next-step step password password-confirm)}]]]])])) diff --git a/src/status_im/ui/screens/accounts/db.cljs b/src/status_im/ui/screens/accounts/db.cljs deleted file mode 100644 index 9cfea154531..00000000000 --- a/src/status_im/ui/screens/accounts/db.cljs +++ /dev/null @@ -1,59 +0,0 @@ -(ns status-im.ui.screens.accounts.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec] - status-im.utils.db - status-im.ui.screens.network-settings.db - status-im.ui.screens.bootnodes-settings.db - [status-im.constants :as const])) - -(defn valid-length? [password] - (>= (count password) const/min-password-length)) - -(spec/def ::password (spec/and :global/not-empty-string valid-length?)) - -(spec/def :account/address :global/address) -(spec/def :account/name :global/not-empty-string) -(spec/def :account/public-key :global/public-key) -;;not used -(spec/def :account/email nil?) -(spec/def :account/signed-up? (spec/nilable boolean?)) -(spec/def :account/last-updated (spec/nilable int?)) -(spec/def :account/last-sign-in (spec/nilable int?)) -(spec/def :account/last-request (spec/nilable int?)) -(spec/def :account/photo-path (spec/nilable string?)) -(spec/def :account/debug? (spec/nilable boolean?)) -(spec/def :account/status (spec/nilable string?)) -(spec/def :account/network (spec/nilable string?)) -(spec/def :account/networks (spec/nilable :networks/networks)) -(spec/def :account/bootnodes (spec/nilable :bootnodes/bootnodes)) -(spec/def :account/wnode (spec/nilable string?)) -(spec/def :account/settings (spec/nilable (spec/map-of keyword? any?))) -(spec/def :account/signing-phrase :global/not-empty-string) -(spec/def :account/mnemonic (spec/nilable string?)) -(spec/def :account/sharing-usage-data? (spec/nilable boolean?)) -(spec/def :account/dev-mode? (spec/nilable boolean?)) -(spec/def :account/seed-backed-up? (spec/nilable boolean?)) -(spec/def :account/wallet-set-up-passed? (spec/nilable boolean?)) -(spec/def :account/mainnet-warning-shown? (spec/nilable boolean?)) - -(spec/def :accounts/account (allowed-keys - :req-un [:account/name :account/address :account/public-key - :account/photo-path :account/signing-phrase] - :opt-un [:account/debug? :account/status :account/last-updated - :account/email :account/signed-up? :account/network - :account/networks :account/settings :account/wnode - :account/last-sign-in :account/sharing-usage-data? :account/dev-mode? - :account/seed-backed-up? :account/mnemonic - :account/wallet-set-up-passed? :account/last-request - :account/bootnodes :account/mainnet-warning-shown?])) - -(spec/def :accounts/accounts (spec/nilable (spec/map-of :account/address :accounts/account))) - -;;used during creating account -(spec/def :accounts/create (spec/nilable map?)) -;;used during recovering account -(spec/def :accounts/recover (spec/nilable map?)) -;;used during logging -(spec/def :accounts/login (spec/nilable map?)) -;;logged in account -(spec/def :account/account (spec/nilable :accounts/account)) diff --git a/src/status_im/ui/screens/accounts/events.cljs b/src/status_im/ui/screens/accounts/events.cljs deleted file mode 100644 index c7527f858d8..00000000000 --- a/src/status_im/ui/screens/accounts/events.cljs +++ /dev/null @@ -1,82 +0,0 @@ -(ns status-im.ui.screens.accounts.events - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - status-im.ui.screens.accounts.create.navigation - [status-im.ui.screens.accounts.utils :as accounts.utils] - [status-im.ui.screens.accounts.models :as models])) - -;;;; COFX - -(re-frame/reg-cofx - ::get-signing-phrase - (fn [cofx _] - (models/get-signing-phrase cofx))) - -(re-frame/reg-cofx - ::get-status - (fn [cofx _] - (models/get-status cofx))) - -;;;; FX - -(re-frame/reg-fx - :create-account - models/create-account!) - -;;;; Handlers - -(handlers/register-handler-fx - :create-account - (fn [cofx _] - (models/create-account cofx))) - -(handlers/register-handler-fx - :account-created - [re-frame/trim-v (re-frame/inject-cofx ::get-signing-phrase) (re-frame/inject-cofx ::get-status)] - (fn [cofx [result password]] - (models/on-account-created result password cofx))) - -(handlers/register-handler-fx - :send-account-update-if-needed - (fn [cofx _] - (models/send-account-update-if-needed cofx))) - -(handlers/register-handler-fx - :account-set-name - (fn [cofx _] - (models/account-set-name cofx))) - -(handlers/register-handler-fx - :account-set-input-text - (fn [cofx [_ input-key text]] - (models/account-set-input-text input-key text cofx))) - -(handlers/register-handler-fx - :update-sign-in-time - (fn [{db :db now :now :as cofx} _] - (accounts.utils/account-update {:last-sign-in now} cofx))) - -(handlers/register-handler-fx - :update-mainnet-warning-shown - (fn [cofx _] - (accounts.utils/account-update {:mainnet-warning-shown? true} cofx))) - -(handlers/register-handler-fx - :show-mainnet-is-default-alert - (fn [cofx] - (models/show-mainnet-is-default-alert cofx))) - -(handlers/register-handler-fx - :reset-account-creation - (fn [cofx _] - (models/reset-account-creation cofx))) - -(handlers/register-handler-fx - :switch-dev-mode - (fn [cofx [_ dev-mode]] - (accounts.utils/account-update {:dev-mode? dev-mode} cofx))) - -(handlers/register-handler-fx - :wallet-set-up-passed - (fn [cofx [_ modal?]] - (models/wallet-set-up-passed modal? cofx))) diff --git a/src/status_im/ui/screens/accounts/login/events.cljs b/src/status_im/ui/screens/accounts/login/events.cljs deleted file mode 100644 index e07947a4166..00000000000 --- a/src/status_im/ui/screens/accounts/login/events.cljs +++ /dev/null @@ -1,55 +0,0 @@ -(ns status-im.ui.screens.accounts.login.events - (:require status-im.ui.screens.accounts.login.navigation - [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.ui.screens.accounts.login.models :as models])) - -;;;; FX - -(re-frame/reg-fx :stop-node models/stop-node!) - -(re-frame/reg-fx - :login - (fn [[address password]] - (models/login! address password))) - -(re-frame/reg-fx - :clear-web-data - models/clear-web-data!) - -(re-frame/reg-fx - :change-account - (fn [[address]] - (models/handle-change-account! address))) - -;;;; Handlers - -(handlers/register-handler-fx - :open-login - (fn [cofx [_ address photo-path name]] - (models/open-login address photo-path name cofx))) - -(handlers/register-handler-fx - :login-account-internal - (fn [cofx [_ address password]] - (models/login-account-internal address password cofx))) - -(handlers/register-handler-fx - :start-node - (fn [cofx [_ address password]] - (models/start-node address password cofx))) - -(handlers/register-handler-fx - :login-account - (fn [cofx [_ address password]] - (models/login-account address password cofx))) - -(handlers/register-handler-fx - :login-handler - (fn [cofx [_ login-result address]] - (models/login-handler login-result address cofx))) - -(handlers/register-handler-fx - :change-account-handler - (fn [cofx [_ address]] - (models/change-account-handler address cofx))) diff --git a/src/status_im/ui/screens/accounts/login/models.cljs b/src/status_im/ui/screens/accounts/login/models.cljs deleted file mode 100644 index 49718918ffd..00000000000 --- a/src/status_im/ui/screens/accounts/login/models.cljs +++ /dev/null @@ -1,134 +0,0 @@ -(ns status-im.ui.screens.accounts.login.models - (:require status-im.ui.screens.accounts.login.navigation - [re-frame.core :as re-frame] - [status-im.utils.types :as types] - [status-im.data-store.core :as data-store] - [status-im.native-module.core :as status] - [status-im.utils.config :as config] - [status-im.utils.keychain.core :as keychain] - [status-im.utils.universal-links.core :as universal-links])) - -;;;; FX - -(defn stop-node! [] (status/stop-node)) - -(defn login! [address password] - (status/login address password #(re-frame/dispatch [:login-handler % address]))) - -(defn clear-web-data! [] - (status/clear-web-data)) - -(defn- change-account! [address encryption-key] - (data-store/change-account address encryption-key) - (re-frame/dispatch [:change-account-handler address])) - -(defn handle-change-account! [address] - ;; No matter what is the keychain we use, as checks are done on decrypting base - (.. (keychain/safe-get-encryption-key) - (then (partial change-account! address)) - (catch (fn [error] - ;; If all else fails we fallback to showing initial error - (re-frame/dispatch [:initialize-app "" :decryption-failed]))))) - -;;;; Handlers - -(defn open-login [address photo-path name {db :db}] - {:db (update db - :accounts/login assoc - :address address - :photo-path photo-path - :name name) - :dispatch [:navigate-to :login]}) - -(defn wrap-with-login-account-fx [db address password] - {:db db - :login [address password]}) - -(defn login-account-internal [address password {db :db}] - (wrap-with-login-account-fx - (assoc db :node/after-start nil) - address password)) - -(defn- add-custom-bootnodes [config network all-bootnodes] - (let [bootnodes (as-> all-bootnodes $ - (get $ network) - (vals $) - (map :address $))] - (if (seq bootnodes) - (assoc config :ClusterConfig {:Enabled true - :BootNodes bootnodes}) - config))) - -(defn- get-network-by-address [db address] - (let [accounts (get db :accounts/accounts) - {:keys [network - settings - bootnodes - networks]} (get accounts address) - use-custom-bootnodes (get-in settings [:bootnodes network]) - config (cond-> (get-in networks [network :config]) - (and - config/bootnodes-settings-enabled? - use-custom-bootnodes) - (add-custom-bootnodes network bootnodes))] - {:use-custom-bootnodes use-custom-bootnodes - :network network - :config config})) - -(defn- wrap-with-initialize-geth-fx [db address password] - (let [{:keys [network config]} (get-network-by-address db address)] - {:initialize-geth-fx config - :db (assoc db - :network network - :node/after-start [:login-account-internal address password])})) - -(defn start-node [address password {db :db}] - (wrap-with-initialize-geth-fx - (assoc db :node/after-stop nil) - address password)) - -(defn- wrap-with-stop-node-fx [db address password] - {:db (assoc db :node/after-stop [:start-node address password]) - :stop-node nil}) - -(defn- restart-node? [account-network network use-custom-bootnodes] - (or (not= account-network network) - (and config/bootnodes-settings-enabled? - use-custom-bootnodes))) - -(defn login-account [address password {{:keys [network status-node-started?] :as db} :db}] - (let [{use-custom-bootnodes :use-custom-bootnodes - account-network :network} (get-network-by-address db address) - db' (-> db - (assoc-in [:accounts/login :processing] true)) - wrap-fn (cond (not status-node-started?) - wrap-with-initialize-geth-fx - - (not (restart-node? account-network - network - use-custom-bootnodes)) - wrap-with-login-account-fx - - :else - wrap-with-stop-node-fx)] - (wrap-fn db' address password))) - -(defn login-handler [login-result address {db :db}] - (let [data (types/json->clj login-result) - error (:error data) - success (zero? (count error)) - db' (assoc-in db [:accounts/login :processing] false)] - (if success - {:db db - :clear-web-data nil - :change-account [address]} - {:db (assoc-in db' [:accounts/login :error] error)}))) - -(defn change-account-handler [address {{:keys [view-id] :as db} :db :as cofx}] - {:db (cond-> (dissoc db :accounts/login) - (= view-id :create-account) - (assoc-in [:accounts/create :step] :enter-name)) - :dispatch [:initialize-account address - (when (not= view-id :create-account) - [[:navigate-to-clean :home] - (universal-links/stored-url-event cofx)])]}) diff --git a/src/status_im/ui/screens/accounts/login/navigation.cljs b/src/status_im/ui/screens/accounts/login/navigation.cljs deleted file mode 100644 index 141620806cb..00000000000 --- a/src/status_im/ui/screens/accounts/login/navigation.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns status-im.ui.screens.accounts.login.navigation - (:require [status-im.ui.screens.navigation :as nav])) - -(defmethod nav/preload-data! :login - [db] - (update db :accounts/login dissoc :error :password :processing)) \ No newline at end of file diff --git a/src/status_im/ui/screens/accounts/login/styles.cljs b/src/status_im/ui/screens/accounts/login/styles.cljs deleted file mode 100644 index 9f3f3e1d670..00000000000 --- a/src/status_im/ui/screens/accounts/login/styles.cljs +++ /dev/null @@ -1,40 +0,0 @@ -(ns status-im.ui.screens.accounts.login.styles - (:require-macros [status-im.utils.styles :refer [defnstyle defstyle]]) - (:require [status-im.ui.components.styles :as st] - [status-im.ui.components.colors :as colors])) - -(defstyle login-view - {:flex 1 - :margin-horizontal 16}) - -(defstyle login-badge-container - {:margin-top 24}) - -(def processing-view - {:flex 1 - :align-items :center - :justify-content :center}) - -(def sign-you-in - {:margin-top 16 - :font-size 13 - :color colors/text-light-gray}) - -(def bottom-button-container - {:flex-direction :row - :margin-horizontal 12 - :margin-vertical 15 - :align-items :center}) - -(def login-badge - {:align-items :center}) - -(def login-badge-image-size 56) - -(def login-badge-name - {:font-size 15 - :color colors/text-light-gray - :margin-top 8}) - -(def password-container - {:margin-top 24}) diff --git a/src/status_im/ui/screens/accounts/login/views.cljs b/src/status_im/ui/screens/accounts/login/views.cljs deleted file mode 100644 index 78624e33344..00000000000 --- a/src/status_im/ui/screens/accounts/login/views.cljs +++ /dev/null @@ -1,89 +0,0 @@ -(ns status-im.ui.screens.accounts.login.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [status-im.ui.screens.accounts.styles :as ast] - [status-im.ui.components.text-input.view :as text-input] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.toolbar.actions :as act] - [status-im.ui.screens.accounts.login.styles :as styles] - [status-im.ui.components.react :as react] - [status-im.i18n :as i18n] - [status-im.ui.components.react :as components] - [status-im.ui.components.common.common :as components.common] - [status-im.chat.views.photos :as photos] - [re-frame.core :as re-frame] - [cljs.spec.alpha :as spec] - [status-im.ui.screens.accounts.db :as db])) - -(defn login-toolbar [can-navigate-back?] - [toolbar/toolbar - nil - (when can-navigate-back? - [toolbar/nav-button act/default-back]) - [toolbar/content-title (i18n/label :t/sign-in-to-status)]]) - -(defn login-account [password-text-input address password] - (.blur password-text-input) - (re-frame/dispatch [:login-account address password])) - -(defn- error-key [error] - ;; TODO Improve selection logic when status-go provide an error code - ;; see https://github.com/status-im/status-go/issues/278 - (cond - (string/starts-with? error "there is no running node") - :t/node-unavailable - - (string/starts-with? error "cannot retrieve a valid key") - :t/wrong-password - - :else - :t/unknown-status-go-error)) - -(defn account-login-badge [photo-path name] - [react/view styles/login-badge - [photos/photo photo-path {:size styles/login-badge-image-size}] - [react/view - [react/text {:style styles/login-badge-name - :numberOfLines 1} - name]]]) - -(defview login [] - (letsubs [{:keys [address photo-path name password error processing]} [:get :accounts/login] - can-navigate-back? [:can-navigate-back?] - password-text-input (atom nil)] - [react/keyboard-avoiding-view {:style ast/accounts-view} - [status-bar/status-bar] - [login-toolbar can-navigate-back?] - [components.common/separator] - [react/view styles/login-view - [react/view styles/login-badge-container - [account-login-badge photo-path name] - [react/view styles/password-container - [text-input/text-input-with-label - {:label (i18n/label :t/password) - :placeholder (i18n/label :t/password) - :ref #(reset! password-text-input %) - :auto-focus can-navigate-back? ;;this needed because keyboard overlays testfairy alert - :on-submit-editing #(login-account @password-text-input address password) - :on-change-text #(do - (re-frame/dispatch [:set-in [:accounts/login :password] %]) - (re-frame/dispatch [:set-in [:accounts/login :error] ""])) - :secure-text-entry true - :error (when (pos? (count error)) (i18n/label (error-key error)))}]]]] - (when processing - [react/view styles/processing-view - [components/activity-indicator {:animating true}] - [react/i18n-text {:style styles/sign-you-in :key :sign-you-in}]]) - (when-not processing - [react/view {:style styles/bottom-button-container} - (when-not can-navigate-back? - [components.common/bottom-button - {:label (i18n/label :t/other-accounts) - :on-press #(re-frame/dispatch [:navigate-to-clean :accounts])}]) - [react/view {:style {:flex 1}}] - [components.common/bottom-button - {:forward? true - :label (i18n/label :t/sign-in) - :disabled? (not (spec/valid? ::db/password password)) - :on-press #(login-account @password-text-input address password)}]])])) diff --git a/src/status_im/ui/screens/accounts/models.cljs b/src/status_im/ui/screens/accounts/models.cljs deleted file mode 100644 index f6fc8878c06..00000000000 --- a/src/status_im/ui/screens/accounts/models.cljs +++ /dev/null @@ -1,136 +0,0 @@ -(ns status-im.ui.screens.accounts.models - (:require [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.native-module.core :as status] - [status-im.utils.types :as types] - [status-im.utils.identicon :as identicon] - [clojure.string :as str] - [status-im.i18n :as i18n] - [status-im.utils.config :as config] - [status-im.utils.utils :as utils] - [status-im.utils.datetime :as time] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.ui.screens.accounts.statuses :as statuses] - [status-im.utils.signing-phrase.core :as signing-phrase] - [status-im.utils.gfycat.core :as gfycat] - [status-im.utils.hex :as utils.hex] - [status-im.constants :as constants] - status-im.ui.screens.accounts.create.navigation - [status-im.ui.screens.accounts.utils :as accounts.utils] - [status-im.data-store.accounts :as accounts-store] - [status-im.ui.screens.navigation :as navigation] - [status-im.ui.screens.wallet.settings.models :as wallet.settings.models])) - -;;;; COFX - -(defn get-signing-phrase [cofx] - (assoc cofx :signing-phrase (signing-phrase/generate))) - -(defn get-status [cofx] - (assoc cofx :status (rand-nth statuses/data))) - -;;;; FX - -(defn create-account! [password] - (status/create-account - password - #(re-frame/dispatch [:account-created (types/json->clj %) password]))) - -;;;; Handlers - -(defn create-account [{{:accounts/keys [create] :as db} :db}] - {:db (update db :accounts/create assoc :step :account-creating :error nil) - :create-account (:password create)}) - -(defn- add-account - "Takes db and new account, creates map of effects describing adding account to database and realm" - [{:networks/keys [networks] :as db} {:keys [address] :as account}] - (let [enriched-account (assoc account - :network config/default-network - :networks networks - :address address)] - {:db (assoc-in db [:accounts/accounts address] enriched-account) - :data-store/base-tx [(accounts-store/save-account-tx enriched-account)]})) - -(defn on-account-created [{:keys [pubkey address mnemonic]} password {:keys [signing-phrase status db]}] - (let [normalized-address (utils.hex/normalize-hex address) - account {:public-key pubkey - :address normalized-address - :name (gfycat/generate-gfy pubkey) - :status status - :signed-up? true - :photo-path (identicon/identicon pubkey) - :signing-phrase signing-phrase - :mnemonic mnemonic - :settings (constants/default-account-settings)}] - (log/debug "account-created") - (when-not (str/blank? pubkey) - (-> (add-account db account) - (assoc :dispatch [:login-account normalized-address password]))))) - -(defn load-accounts [{:keys [db all-accounts]}] - (let [accounts (->> all-accounts - (map (fn [{:keys [address] :as account}] - [address account])) - (into {}))] - {:db (assoc db :accounts/accounts accounts)})) - -(defn update-settings - ([settings cofx] (update-settings settings nil cofx)) - ([settings success-event {{:keys [account/account] :as db} :db :as cofx}] - (let [new-account (assoc account :settings settings)] - {:db (assoc db :account/account new-account) - :data-store/base-tx [{:transaction (accounts-store/save-account-tx new-account) - :success-event success-event}]}))) - -(defn send-account-update-if-needed [{:keys [db now] :as cofx}] - (let [{:keys [last-updated]} (:account/account db) - needs-update? (> (- now last-updated) time/week)] - (log/info "Need to send account-update: " needs-update?) - (when needs-update? - ;; TODO(janherich): this is very strange and misleading, need to figure out why it'd necessary to update - ;; account with network update when last update was more then week ago - (accounts.utils/account-update nil cofx)))) - -(defn account-set-name [{{:accounts/keys [create] :as db} :db :as cofx}] - (handlers-macro/merge-fx cofx - {:db (assoc db :accounts/create {:show-welcome? true}) - :dispatch-n [[:navigate-to-clean :home] - [:request-notifications]]} - (accounts.utils/account-update {:name (:name create)}))) - -(defn account-set-input-text [input-key text {db :db}] - {:db (update db :accounts/create merge {input-key text :error nil})}) - -(defn show-mainnet-is-default-alert [{:keys [db]}] - (let [enter-name-screen? (= :enter-name (get-in db [:accounts/create :step])) - shown? (get-in db [:account/account :mainnet-warning-shown?])] - (when (and config/mainnet-warning-enabled? - (not shown?) - (not enter-name-screen?)) - (utils/show-popup - (i18n/label :mainnet-is-default-alert-title) - (i18n/label :mainnet-is-default-alert-text) - #(re-frame/dispatch [:update-mainnet-warning-shown]))))) - -(defn reset-account-creation [{db :db}] - {:db (update db :accounts/create assoc :step :enter-password :password nil :password-confirm nil :error nil)}) - -(defn- chat-send? [transaction] - (and (seq transaction) - (not (:in-progress? transaction)) - (:from-chat? transaction))) - -(defn continue-after-wallet-onboarding [db modal? cofx] - (let [transaction (get-in db [:wallet :send-transaction])] - (cond modal? {:dispatch [:navigate-to-modal :wallet-send-transaction-modal]} - (chat-send? transaction) {:db (navigation/navigate-back db) - :dispatch [:navigate-to :wallet-send-transaction-chat]} - :else {:db (navigation/navigate-back db)}))) - -(defn wallet-set-up-passed [modal? {:keys [db] :as cofx}] - (handlers-macro/merge-fx - cofx - (continue-after-wallet-onboarding db modal?) - (wallet.settings.models/wallet-autoconfig-tokens) - (accounts.utils/account-update {:wallet-set-up-passed? true}))) diff --git a/src/status_im/ui/screens/accounts/recover/events.cljs b/src/status_im/ui/screens/accounts/recover/events.cljs deleted file mode 100644 index e1fab1ce735..00000000000 --- a/src/status_im/ui/screens/accounts/recover/events.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.ui.screens.accounts.recover.events - (:require status-im.ui.screens.accounts.recover.navigation - [re-frame.core :as re-frame] - [status-im.ui.screens.accounts.recover.models :as models] - [status-im.utils.handlers :as handlers])) - -;;;; FX - -(re-frame/reg-fx - :recover-account-fx - (fn [[masked-passphrase password]] - (models/recover-account-fx! masked-passphrase password))) - -;;;; Handlers - -(handlers/register-handler-fx - :account-recovered - (fn [cofx [_ result password]] - (models/on-account-recovered result password cofx))) - -(handlers/register-handler-fx - :account-recovered-navigate - (fn [cofx] - (models/account-recovered-navigate cofx))) - -(handlers/register-handler-fx - :recover-account - (fn [cofx [_ masked-passphrase password]] - (models/recover-account masked-passphrase password cofx))) diff --git a/src/status_im/ui/screens/accounts/recover/models.cljs b/src/status_im/ui/screens/accounts/recover/models.cljs deleted file mode 100644 index 1941c40f0eb..00000000000 --- a/src/status_im/ui/screens/accounts/recover/models.cljs +++ /dev/null @@ -1,60 +0,0 @@ -(ns status-im.ui.screens.accounts.recover.models - (:require status-im.ui.screens.accounts.recover.navigation - [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.native-module.core :as status] - [status-im.ui.screens.accounts.models :as accounts.models] - [status-im.utils.types :as types] - [status-im.utils.identicon :as identicon] - [status-im.utils.gfycat.core :as gfycat] - [status-im.utils.security :as security] - [status-im.utils.signing-phrase.core :as signing-phrase] - [status-im.utils.hex :as utils.hex] - [status-im.constants :as constants])) - -;;;; FX - -(defn recover-account-fx! [masked-passphrase password] - (status/recover-account - (security/unmask masked-passphrase) - password - (fn [result] - ;; here we deserialize result, dissoc mnemonic and serialize the result again - ;; because we want to have information about the result printed in logs, but - ;; don't want secure data to be printed - (let [data (-> (types/json->clj result) - (dissoc :mnemonic) - (types/clj->json))] - (re-frame/dispatch [:account-recovered data password]))))) - -;;;; Handlers - -(defn on-account-recovered [result password {:keys [db]}] - (let [data (types/json->clj result) - public-key (:pubkey data) - address (-> data :address utils.hex/normalize-hex) - phrase (signing-phrase/generate) - account {:public-key public-key - :address address - :name (gfycat/generate-gfy public-key) - :photo-path (identicon/identicon public-key) - :mnemonic "" - :signed-up? true - :signing-phrase phrase - :settings (constants/default-account-settings) - :wallet-set-up-passed? false - :seed-backed-up? true}] - (when-not (string/blank? public-key) - (-> db - (accounts.models/add-account account) - (assoc :dispatch [:login-account address password]) - (assoc :dispatch-later [{:ms 2000 :dispatch [:account-recovered-navigate]}]))))) - -(defn account-recovered-navigate [{:keys [db]}] - {:db (assoc-in db [:accounts/recover :processing] false) - :dispatch-n [[:navigate-to-clean :home] - [:request-notifications]]}) - -(defn recover-account [masked-passphrase password {:keys [db]}] - {:db (assoc-in db [:accounts/recover :processing] true) - :recover-account-fx [masked-passphrase password]}) diff --git a/src/status_im/ui/screens/accounts/recover/navigation.cljs b/src/status_im/ui/screens/accounts/recover/navigation.cljs deleted file mode 100644 index b3ecb2acb4c..00000000000 --- a/src/status_im/ui/screens/accounts/recover/navigation.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns status-im.ui.screens.accounts.recover.navigation - (:require [status-im.ui.screens.navigation :as nav])) - -(defmethod nav/preload-data! :recover - [db] - (update db :accounts/recover dissoc :password :passphrase :processing)) \ No newline at end of file diff --git a/src/status_im/ui/screens/accounts/recover/styles.cljs b/src/status_im/ui/screens/accounts/recover/styles.cljs deleted file mode 100644 index e449baa730d..00000000000 --- a/src/status_im/ui/screens/accounts/recover/styles.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.ui.screens.accounts.recover.styles - (:require [status-im.ui.components.colors :as colors])) - -(def screen-container - {:flex 1 - :background-color colors/white}) - -(def bottom-button-container - {:flex-direction :row - :margin-horizontal 12 - :margin-vertical 15}) - -(def processing-view - {:flex 1 - :align-items :center - :justify-content :center}) - -(def sign-you-in - {:margin-top 16 - :font-size 13 - :color colors/text-light-gray}) - -(def recovery-phrase-input - {:flex 1 - :text-align-vertical :top}) diff --git a/src/status_im/ui/screens/accounts/recover/views.cljs b/src/status_im/ui/screens/accounts/recover/views.cljs deleted file mode 100644 index c7c5405b4ab..00000000000 --- a/src/status_im/ui/screens/accounts/recover/views.cljs +++ /dev/null @@ -1,77 +0,0 @@ -(ns status-im.ui.screens.accounts.recover.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [reagent.core :as reagent] - [status-im.ui.components.text-input.view :as text-input] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.i18n :as i18n] - [status-im.ui.screens.accounts.recover.styles :as styles] - [status-im.ui.screens.accounts.db :as db] - [status-im.utils.config :as config] - [status-im.utils.ethereum.core :as ethereum] - [status-im.react-native.js-dependencies :as js-dependencies] - [cljs.spec.alpha :as spec] - [status-im.ui.components.common.common :as components.common] - [status-im.utils.security :as security])) - -(defview passphrase-input [passphrase] - (letsubs [error [:get-in [:accounts/recover :passphrase-error]] - input-ref (reagent/atom nil)] - {:component-did-mount (fn [_] (when config/testfairy-enabled? - (.hideView js-dependencies/testfairy @input-ref)))} - [text-input/text-input-with-label - {:style styles/recovery-phrase-input - :height 92 - :ref (partial reset! input-ref) - :label (i18n/label :t/recovery-phrase) - :placeholder (i18n/label :t/enter-12-words) - :multiline true - :default-value passphrase - :auto-correct false - :on-change-text #(re-frame/dispatch [:set-in [:accounts/recover :passphrase] (string/lower-case %)]) - :error error}])) - -(defview password-input [password] - (letsubs [error [:get-in [:accounts/recover :password-error]]] - [react/view {:margin-top 10} - [text-input/text-input-with-label - {:label (i18n/label :t/password) - :placeholder (i18n/label :t/enter-password) - :default-value password - :auto-focus false - :on-change-text #(re-frame/dispatch [:set-in [:accounts/recover :password] %]) - :secure-text-entry true - :error error}]])) - -(defview recover [] - (letsubs [{:keys [passphrase password processing]} [:get :accounts/recover]] - (let [words (ethereum/passphrase->words passphrase) - valid-form? (and - (ethereum/valid-words? words) - (spec/valid? ::db/password password))] - [react/keyboard-avoiding-view {:style styles/screen-container} - [status-bar/status-bar] - [toolbar/toolbar nil toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/sign-in-to-another)]] - [components.common/separator] - [react/view {:margin 16} - [passphrase-input (or passphrase "")] - [password-input (or password "")]] - [react/view {:flex 1}] - (if processing - [react/view styles/processing-view - [react/activity-indicator {:animating true}] - [react/i18n-text {:style styles/sign-you-in :key :sign-you-in}]] - [react/view {:style styles/bottom-button-container} - [react/view {:style {:flex 1}}] - [components.common/bottom-button - {:forward? true - :label (i18n/label :t/sign-in) - :disabled? (not valid-form?) - :on-press (fn [_] - (let [masked-passphrase (security/mask-data (ethereum/words->passphrase words))] - (re-frame/dispatch [:recover-account masked-passphrase password])))}]])]))) diff --git a/src/status_im/ui/screens/accounts/statuses.cljs b/src/status_im/ui/screens/accounts/statuses.cljs deleted file mode 100644 index 7a6cb3f62c8..00000000000 --- a/src/status_im/ui/screens/accounts/statuses.cljs +++ /dev/null @@ -1,89 +0,0 @@ -(ns status-im.ui.screens.accounts.statuses) - -(def data - ["be the hero of your own journey" - "nature is pleased with simplicity" - "accept yourself and keep moving forward" - "only wise men look for new wisdom" - "do it or not, but do" - "any human-defined truth is ultimately relative" - "success is not an activity but a process" - "the future is forever a projection of the present" - "open source can propagate to fill all the corners of the universe" - "wisdom is unlearning what you have learnt" - "people who are afraid of the truth never find it" - "a single thought can shift your entire world" - "the man who is not permitted to own is owned" - "the wisest are the most annoyed at the loss of time" - "freedom flourishes upon the bedrock of ethics and integrity" - "speaking less and doing more says more" - "the future belongs to those who never forget to think" - "begin, be bold, and venture to be wise" - "wisdom is the reward for surviving our own stupidity" - "history is the insight to foresight" - "being surrounded by great people can get you really really far" - "cheapen words and they'll cheapen you" - "there is always exception to every rule" - "the future is certain. it is just not known" - "the future depends on what we do in the present" - "waiting leads to wondering" - "imagination is an endless possibility" - "if we don't make our own future, it will be made for us" - "i would rather risk failure than achieve it without risk" - "life would be much easier if i had the source code" - "relax and recreate yourself!" - "your history is not your future" - "wise men speak only of what they know" - "make time to read" - "we learn from failure, not from success" - "science is a way humans try to tell themselves the truths" - "success is simply the wisdom born out of so called failures" - "little decisions shape big consequences" - "it's possible to go on, no matter how impossible it seems" - "the intelligent have plans; the wise have principles" - "embrace the future and keep your back up against the past" - "knowledge is beautiful; science is amazing" - "realize that everything connects to everything else" - "wonder is the seed of knowledge" - "i want something that is nothing like the past" - "the future belongs to those who innovate" - "for the newborn and wise, everything begins small" - "your future is only as bright as your mind is open" - "all too often, the rabbit hole is as deep as you have dug it" - "ego is recessive in wisdom" - "without courage, wisdom bears no fruit" - "explore, experiment and experience" - "we are star stuff harvesting sunlight" - "he who knows all the answers has not been asked all the questions" - "the future will be different from how they have told us" - "read. you can always talk with another reader" - "the best way to predict your future is to create it" - "our thoughts create our future" - "today is the beginning of new history" - "reading & thinking breeds limitless progress" - "stuff your brain with knowledge" - "look for patterns, and then ask why those patterns exist" - "he who doesn't understand history is doomed to repeat it" - "asking why, is the root of a scientific mindset" - "thinking breeds inventions and innovations" - "every past used to be a future once upon a time" - "the future begins with hope" - "censorship exists to protect corruption" - "devote yourself to reading, learning and writing" - "the highest form of wisdom is often hidden in simplicity" - "today's science is tomorrow's technology" - "prediction is very difficult, especially about the future" - "wishes are memories coming from our future" - "fight for greater course than for greater loss" - "the life we're given is on a thread, so wear it well" - "a free thinker walks on shortcuts among wisdoms" - "free your mind from routine, keep your brain somewhere else" - "let discernment be your trustee, and mistakes your teacher" - "he who forgets the past is doomed to repeat it" - "everybody's gotta learn, nobody's born knowing" - "in a broader sense, systems thinking is a path to greater awareness" - "your choices are where your future begins" - "value your freedom or you will lose it" - "the future starts today, not tomorrow" - "our future starts in our decisions" - "there's no greater show on earth than observing human nature"]) \ No newline at end of file diff --git a/src/status_im/ui/screens/accounts/styles.cljs b/src/status_im/ui/screens/accounts/styles.cljs deleted file mode 100644 index 4031903b19d..00000000000 --- a/src/status_im/ui/screens/accounts/styles.cljs +++ /dev/null @@ -1,56 +0,0 @@ -(ns status-im.ui.screens.accounts.styles - (:require-macros [status-im.utils.styles :refer [defnstyle defstyle]]) - (:require [status-im.ui.components.styles :as common] - [status-im.ui.components.colors :as colors])) - -(def accounts-view - {:flex 1 - :background-color colors/white}) - -(def accounts-container - {:flex 1 - :padding-horizontal 16 - :background-color colors/gray-lighter}) - -(def bottom-actions-container - {:margin-bottom 16}) - -(def account-image-size 40) - -(defstyle account-title-text - {:color :black - :font-size 17 - :ios {:letter-spacing -0.2}}) - -(defstyle accounts-list-container - {:flex 1 - :margin-top 16 - :margin-bottom 16}) - -(defstyle account-view - {:background-color :white - :flex-direction :row - :align-items :center - :padding-horizontal 16 - :height 64 - :border-radius 8}) - -(def account-badge-text-view - {:margin-left 16 - :margin-right 21 - :flex-shrink 1}) - -(def account-badge-text - {:font-size 17 - :letter-spacing -0.2 - :color common/color-black}) - -(defstyle account-badge-pub-key-text - {:font-size 14 - :ios {:letter-spacing -0.2} - :color colors/gray - :margin-top 4}) - -(def bottom-button-container - {:margin-top 14 - :margin-bottom 6}) diff --git a/src/status_im/ui/screens/accounts/subs.cljs b/src/status_im/ui/screens/accounts/subs.cljs deleted file mode 100644 index b0fdf0fb36e..00000000000 --- a/src/status_im/ui/screens/accounts/subs.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.ui.screens.accounts.subs - (:require [re-frame.core :refer [reg-sub subscribe]] - [clojure.string :as string] - [status-im.ui.screens.accounts.db :as db] - [status-im.utils.ethereum.core :as ethereum] - [cljs.spec.alpha :as spec])) - -(reg-sub :get-current-public-key - (fn [db] - (:current-public-key db))) - -(reg-sub :get-accounts - (fn [db] - (:accounts/accounts db))) - -(reg-sub :get-current-account - (fn [db] - (:account/account db))) - -(reg-sub :get-current-account-hex - :<- [:get-current-account] - (fn [{:keys [address]}] - (ethereum/normalized-address address))) - -(reg-sub - :get-account-creation-next-enabled? - (fn [{:accounts/keys [create]}] - (let [{:keys [step password password-confirm name]} create] - (or (and password (= :enter-password step) (spec/valid? ::db/password password)) - (and password-confirm (= :confirm-password step) (spec/valid? ::db/password password-confirm)) - (and name (= :enter-name step) (not (string/blank? name))))))) diff --git a/src/status_im/ui/screens/accounts/utils.cljs b/src/status_im/ui/screens/accounts/utils.cljs deleted file mode 100644 index bd1fce35fe4..00000000000 --- a/src/status_im/ui/screens/accounts/utils.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.ui.screens.accounts.utils - (:require [status-im.transport.message.core :as transport] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.transport.message.v1.contact :as message.contact] - [status-im.data-store.accounts :as accounts-store])) - -(defn account-update - "Takes effects (containing :db) + new account fields, adds all effects necessary for account update. - Optionally, one can specify event to be dispatched after fields are persisted." - ([new-account-fields cofx] - (account-update new-account-fields nil cofx)) - ([new-account-fields after-update-event {:keys [db] :as cofx}] - (let [current-account (:account/account db) - new-account (merge current-account new-account-fields) - fcm-token (get-in db [:notifications :fcm-token]) - fx {:db (assoc db :account/account new-account) - :data-store/base-tx [(accounts-store/save-account-tx - (assoc new-account - :after-update-event - after-update-event))]} - {:keys [name photo-path address]} new-account] - (if (or (:name new-account-fields) (:photo-path new-account-fields)) - (handlers-macro/merge-fx - cofx - fx - (transport/send (message.contact/ContactUpdate. name photo-path address fcm-token) nil)) - fx)))) - -(defn clean-seed-phrase - "A helper function that removes seed phrase from storage." - [cofx] - (account-update {:seed-backed-up? true - :mnemonic nil} - cofx)) diff --git a/src/status_im/ui/screens/accounts/views.cljs b/src/status_im/ui/screens/accounts/views.cljs deleted file mode 100644 index c80ea16cde6..00000000000 --- a/src/status_im/ui/screens/accounts/views.cljs +++ /dev/null @@ -1,49 +0,0 @@ -(ns status-im.ui.screens.accounts.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.chat.views.photos :as photos] - [status-im.ui.screens.accounts.styles :as styles] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.react :as react] - [status-im.i18n :as i18n] - [status-im.ui.components.icons.vector-icons :as icons] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.toolbar.view :as toolbar])) - -(defn account-view [{:keys [address photo-path name public-key]}] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:open-login address photo-path name])} - [react/view styles/account-view - [photos/photo photo-path {:size styles/account-image-size}] - [react/view styles/account-badge-text-view - [react/text {:style styles/account-badge-text - :numberOfLines 1} - name] - [react/text {:style styles/account-badge-pub-key-text - :ellipsize-mode :middle - :numberOfLines 1} - public-key]] - [react/view {:flex 1}] - [icons/icon :icons/forward {:color (colors/alpha colors/gray-icon 0.4)}]]]) - -(defview accounts [] - (letsubs [accounts [:get-accounts]] - [react/view styles/accounts-view - [status-bar/status-bar] - [toolbar/toolbar nil nil - [toolbar/content-title (i18n/label :t/sign-in-to-status)]] - [react/view styles/accounts-container - [react/view styles/accounts-list-container - [list/flat-list {:data (vals accounts) - :key-fn :address - :render-fn (fn [account] [account-view account]) - :separator [react/view {:height 12}]}]] - [react/view - [components.common/button {:on-press #(re-frame/dispatch [:navigate-to :create-account]) - :label (i18n/label :t/create-new-account)}] - [react/view styles/bottom-button-container - [components.common/button {:on-press #(re-frame/dispatch [:navigate-to :recover]) - :label (i18n/label :t/add-existing-account) - :background? false}]]]]])) diff --git a/src/status_im/ui/screens/add_new/db.cljs b/src/status_im/ui/screens/add_new/db.cljs deleted file mode 100644 index fa0076f8ed4..00000000000 --- a/src/status_im/ui/screens/add_new/db.cljs +++ /dev/null @@ -1,5 +0,0 @@ -(ns status-im.ui.screens.add-new.db - (:require [cljs.spec.alpha :as spec] - status-im.ui.screens.contacts.db)) - -(spec/def :new/open-dapp (spec/nilable :contact/contact)) \ No newline at end of file diff --git a/src/status_im/ui/screens/add_new/new_chat/db.cljs b/src/status_im/ui/screens/add_new/new_chat/db.cljs deleted file mode 100644 index 9fcfe2341e3..00000000000 --- a/src/status_im/ui/screens/add_new/new_chat/db.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns status-im.ui.screens.add-new.new-chat.db - (:require [status-im.utils.hex :as hex] - [status-im.i18n :as i18n] - [cljs.spec.alpha :as spec] - [clojure.string :as string])) - -(defn- validate-pub-key [whisper-identity {:keys [address public-key]}] - (cond - (string/blank? whisper-identity) - (i18n/label :t/use-valid-contact-code) - (#{(hex/normalize-hex address) (hex/normalize-hex public-key)} - (hex/normalize-hex whisper-identity)) - (i18n/label :t/can-not-add-yourself) - - (not (spec/valid? :global/public-key whisper-identity)) - (i18n/label :t/use-valid-contact-code))) diff --git a/src/status_im/ui/screens/add_new/new_chat/navigation.cljs b/src/status_im/ui/screens/add_new/new_chat/navigation.cljs deleted file mode 100644 index 39d5175b250..00000000000 --- a/src/status_im/ui/screens/add_new/new_chat/navigation.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns status-im.ui.screens.add-new.new-chat.navigation - (:require [status-im.ui.screens.navigation :as navigation])) - -(defmethod navigation/preload-data! :new-chat - [db _] - (assoc db :contacts/new-identity nil)) diff --git a/src/status_im/ui/screens/add_new/new_chat/styles.cljs b/src/status_im/ui/screens/add_new/new_chat/styles.cljs deleted file mode 100644 index 629d89d3eff..00000000000 --- a/src/status_im/ui/screens/add_new/new_chat/styles.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.ui.screens.add-new.new-chat.styles - (:require [status-im.ui.components.colors :as colors])) - -(def error-message - {:margin-horizontal 14 - :margin-top 4 - :font-size 12 - :letter-spacing -0.2 - :color colors/red}) \ No newline at end of file diff --git a/src/status_im/ui/screens/add_new/new_chat/subs.cljs b/src/status_im/ui/screens/add_new/new_chat/subs.cljs deleted file mode 100644 index e153627650c..00000000000 --- a/src/status_im/ui/screens/add_new/new_chat/subs.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns status-im.ui.screens.add-new.new-chat.subs - (:require [re-frame.core :as re-frame] - [status-im.ui.screens.add-new.new-chat.db :as db])) - -(re-frame/reg-sub - :new-contact-error-message - :<- [:get :contacts/new-identity] - :<- [:get-current-account] - (fn [[new-identity account]] - (db/validate-pub-key new-identity account))) diff --git a/src/status_im/ui/screens/add_new/new_chat/views.cljs b/src/status_im/ui/screens/add_new/new_chat/views.cljs deleted file mode 100644 index 57740a1656b..00000000000 --- a/src/status_im/ui/screens/add_new/new_chat/views.cljs +++ /dev/null @@ -1,53 +0,0 @@ -(ns status-im.ui.screens.add-new.new-chat.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.contact.contact :as contact-view] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar.view] - [status-im.ui.screens.add-new.styles :as add-new.styles] - [status-im.ui.screens.add-new.new-chat.styles :as styles] - [status-im.ui.screens.add-new.open-dapp.styles :as open-dapp.styles])) - -(defn- render-row [row _ _] - [contact-view/contact-view {:contact row - :on-press #(re-frame/dispatch [:start-chat (:whisper-identity %) {:navigation-replace? true}]) - :show-forward? true}]) - -(views/defview new-chat [] - (views/letsubs [contacts [:all-added-people-contacts] - error-message [:new-contact-error-message]] - [react/keyboard-avoiding-view open-dapp.styles/main-container - [status-bar/status-bar] - [toolbar.view/simple-toolbar (i18n/label :t/new-chat)] - [react/view add-new.styles/new-chat-container - [react/view add-new.styles/new-chat-input-container - [react/text-input {:on-change-text #(re-frame/dispatch [:set :contacts/new-identity %]) - :on-submit-editing #(when-not error-message - (re-frame/dispatch [:add-contact-handler])) - :placeholder (i18n/label :t/enter-contact-code) - :style add-new.styles/input - :accessibility-label :enter-contact-code-input - :return-key-type :go}]] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:scan-qr-code - {:toolbar-title (i18n/label :t/new-contact)} - :set-contact-identity-from-qr]) - :style add-new.styles/button-container - :accessibility-label :scan-contact-code-button} - [react/view - [vector-icons/icon :icons/qr {:color colors/blue}]]]] - [react/text {:style styles/error-message} - error-message] - (when (seq contacts) - [react/text {:style open-dapp.styles/list-title} - (i18n/label :t/contacts)]) - [list/flat-list {:data contacts - :key-fn :address - :render-fn render-row - :default-separator? true - :enableEmptySections true - :keyboardShouldPersistTaps :always}]])) diff --git a/src/status_im/ui/screens/add_new/new_public_chat/db.cljs b/src/status_im/ui/screens/add_new/new_public_chat/db.cljs deleted file mode 100644 index f3fc10c17e6..00000000000 --- a/src/status_im/ui/screens/add_new/new_public_chat/db.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.ui.screens.add-new.new-public-chat.db - (:require [clojure.string :as string] - [cljs.spec.alpha :as spec] - [status-im.constants :as constants] - [status-im.utils.homoglyph :as utils] - status-im.utils.db)) - -(defn- legal-name? [username] - (let [username (some-> username string/trim)] - (not (utils/matches username constants/console-chat-id)))) - -(spec/def ::legal-name legal-name?) - -(spec/def ::name (spec/and :global/not-empty-string - ::legal-name)) - -(spec/def ::topic (spec/and :global/not-empty-string - ::legal-name - (partial re-matches #"[a-z\-]+"))) diff --git a/src/status_im/ui/screens/add_new/new_public_chat/styles.cljs b/src/status_im/ui/screens/add_new/new_public_chat/styles.cljs deleted file mode 100644 index 977e3579291..00000000000 --- a/src/status_im/ui/screens/add_new/new_public_chat/styles.cljs +++ /dev/null @@ -1,52 +0,0 @@ -(ns status-im.ui.screens.add-new.new-public-chat.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def group-chat-name-input - {:font-size 17 - :padding-bottom 0 - :letter-spacing -0.2 - :color colors/text}) - -(def topic-hash - (merge group-chat-name-input - {:margin-left 14})) - -(def group-container - {:flex 1 - :flex-direction :column}) - -(def chat-name-container - {:margin-top 10}) - -(defstyle members-text - {:color colors/gray - :ios {:letter-spacing -0.2 - :font-size 16} - :android {:font-size 14}}) - -(def section-title - (merge members-text - {:padding-horizontal 16})) - -(def public-chat-icon - {:background-color colors/blue - :border-radius 50 - :width 40 - :height 40 - :align-items :center - :justify-content :center}) - -(def public-chat-icon-symbol - {:font-size 20 - :color colors/white}) - -(def input-container - {:padding 0 - :padding-right 16 - :background-color nil}) - -(def tooltip - {:bottom-value -15 - :color colors/red-light - :font-size 12}) diff --git a/src/status_im/ui/screens/add_new/new_public_chat/subs.cljs b/src/status_im/ui/screens/add_new/new_public_chat/subs.cljs deleted file mode 100644 index 10b59719c61..00000000000 --- a/src/status_im/ui/screens/add_new/new_public_chat/subs.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.ui.screens.add-new.new-public-chat.subs - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.screens.add-new.new-public-chat.db :as db] - [cljs.spec.alpha :as spec])) - -(re-frame/reg-sub - :new-topic-error-message - :<- [:get :public-group-topic] - (fn [topic] - (when-not (spec/valid? ::db/topic topic) - (i18n/label :topic-name-error)))) diff --git a/src/status_im/ui/screens/add_new/new_public_chat/view.cljs b/src/status_im/ui/screens/add_new/new_public_chat/view.cljs deleted file mode 100644 index bc691172c76..00000000000 --- a/src/status_im/ui/screens/add_new/new_public_chat/view.cljs +++ /dev/null @@ -1,79 +0,0 @@ -(ns status-im.ui.screens.add-new.new-public-chat.view - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.add-new.styles :as add-new.styles] - [status-im.ui.screens.add-new.new-public-chat.styles :as styles] - [status-im.ui.screens.add-new.new-public-chat.db :as v] - [status-im.ui.components.text-input.view :as text-input.view] - status-im.utils.db - [status-im.ui.components.styles :as common.styles] - [cljs.spec.alpha :as spec] - [status-im.ui.components.tooltip.views :as tooltip])) - -(defn- chat-name-input [topic error] - [react/view - [react/view (merge add-new.styles/input-container {:margin-top 8}) - [react/text {:style styles/topic-hash} "#"] - [react/view common.styles/flex - [text-input.view/text-input-with-label - {:container styles/input-container - :on-change-text #(do - (re-frame/dispatch [:set :public-group-topic-error (when-not (spec/valid? ::v/topic %) - (i18n/label :t/topic-name-error))]) - (re-frame/dispatch [:set :public-group-topic %])) - :on-submit-editing #(when (and topic (spec/valid? ::v/topic topic)) - (re-frame/dispatch [:create-new-public-chat topic])) - :auto-capitalize :none - :auto-focus false - :accessibility-label :chat-name-input - :placeholder nil - :return-key-type :go - :auto-correct false}]]] - (when error - [tooltip/tooltip error styles/tooltip])]) - -(defn- public-chat-icon [topic] - [react/view styles/public-chat-icon - [react/text {:uppercase? true - :style styles/public-chat-icon-symbol} - (first topic)]]) - -(defn- render-topic [topic] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:create-new-public-chat topic]) - :accessibility-label :chat-item} - [react/view - [list/item - [public-chat-icon topic] - [list/item-primary-only - topic] - [list/item-icon {:icon :icons/forward - :icon-opts {:color :gray}}]]]]) - -(def default-public-chats ["status" "status 中文" "status 日本語" "status 한국어" "status по-русски" "status español" "status فارسی" "cryptostrikers" "dapps" "ethereum" "openbounty"]) - -(views/defview new-public-chat [] - (views/letsubs [topic [:get :public-group-topic] - error [:get :public-group-topic-error]] - [react/keyboard-avoiding-view styles/group-container - [status-bar/status-bar] - [toolbar/simple-toolbar - (i18n/label :t/public-chat)] - [react/view styles/chat-name-container - [react/text {:style styles/section-title - :font :medium} - (i18n/label :t/public-group-topic)] - [chat-name-input topic error]] - [react/view styles/chat-name-container - [react/text {:style styles/section-title - :font :medium} - (i18n/label :t/selected)]] - [list/flat-list {:data default-public-chats - :key-fn identity - :render-fn render-topic - :keyboard-should-persist-taps :always - :default-separator? true}]])) diff --git a/src/status_im/ui/screens/add_new/open_dapp/styles.cljs b/src/status_im/ui/screens/add_new/open_dapp/styles.cljs deleted file mode 100644 index 5b258137bd4..00000000000 --- a/src/status_im/ui/screens/add_new/open_dapp/styles.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.ui.screens.add-new.open-dapp.styles - (:require [status-im.ui.components.colors :as colors])) - -(def main-container - {:flex 1 - :background-color colors/white}) - -(def gray-label - {:color colors/gray}) - -(def black-label - {:font-size 15 - :letter-spacing -0.2}) - -(def dapp - (merge gray-label - {:margin-top 4})) - -(def dapp-name - (merge black-label - {:margin-top 8})) - -(def list-title - {:margin-top 24 - :margin-left 16 - :font-size 14 - :letter-spacing -0.2 - :color colors/gray}) - -(def description-container - {:margin-top 26 - :margin-horizontal 16}) diff --git a/src/status_im/ui/screens/add_new/open_dapp/views.cljs b/src/status_im/ui/screens/add_new/open_dapp/views.cljs deleted file mode 100644 index 446b16cd696..00000000000 --- a/src/status_im/ui/screens/add_new/open_dapp/views.cljs +++ /dev/null @@ -1,77 +0,0 @@ -(ns status-im.ui.screens.add-new.open-dapp.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.action-button.action-button :as action-button] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.common.common :as components] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar.view] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.contact.contact :as contact-view] - [status-im.ui.components.chat-icon.screen :as chat-icon.screen] - [status-im.ui.screens.add-new.styles :as add-new.styles] - [status-im.ui.screens.add-new.open-dapp.styles :as styles])) - -(defn- render-dapp [dapp] - [contact-view/contact-view {:contact dapp - :on-press #(re-frame/dispatch [:navigate-to :dapp-description dapp]) - :show-forward? true - :accessibility-label :dapp-item}]) - -(views/defview open-dapp [] - (views/letsubs [dapps [:all-dapps] - url-text (atom nil)] - [react/keyboard-avoiding-view styles/main-container - [status-bar/status-bar] - [toolbar.view/simple-toolbar (i18n/label :t/open-dapp)] - [components/separator] - [react/view add-new.styles/input-container - [react/text-input {:on-change-text #(reset! url-text %) - :on-submit-editing #(do - (re-frame/dispatch [:navigate-to-clean :home]) - (re-frame/dispatch [:open-url-in-browser @url-text])) - :placeholder (i18n/label :t/enter-url) - :auto-capitalize :none - :auto-correct false - :style add-new.styles/input - :accessibility-label :dapp-url-input - :return-key-type :go}]] - [list/section-list {:sections dapps - :key-fn :dapp-url - :render-fn render-dapp - :default-separator? true - :enableEmptySections true - :keyboardShouldPersistTaps :always}]])) - -(views/defview dapp-description [] - (views/letsubs [{:keys [name dapp-url description] :as dapp} [:get-screen-params]] - [react/keyboard-avoiding-view styles/main-container - [status-bar/status-bar] - [toolbar.view/simple-toolbar] - [react/view {:margin-top 24 :align-items :center} - [chat-icon.screen/dapp-icon-browser dapp 56] - [react/text {:style styles/dapp-name - :accessibility-label :dapp-name-text} - name] - [react/text {:style styles/dapp} - (i18n/label :t/dapp)]] - [react/view {:margin-top 24} - [action-button/action-button {:label (i18n/label :t/open) - :icon :icons/address - :icon-opts {:color colors/blue} - :accessibility-label :open-dapp-button - :on-press #(do - (re-frame/dispatch [:navigate-to-clean :home]) - (re-frame/dispatch [:open-url-in-browser dapp-url]))}] - [components/separator {:margin-left 72}]] - [react/view styles/description-container - [react/i18n-text {:style styles/gray-label :key :description}] - [react/text {:style (merge styles/black-label {:padding-top 18})} - description] - [components/separator {:margin-top 15}] - [react/i18n-text {:style (merge styles/gray-label {:padding-top 18}) :key :url}] - [react/text {:style (merge styles/black-label {:padding-top 14})} - dapp-url] - [components/separator {:margin-top 6}]]])) diff --git a/src/status_im/ui/screens/add_new/styles.cljs b/src/status_im/ui/screens/add_new/styles.cljs deleted file mode 100644 index 79370ea22b1..00000000000 --- a/src/status_im/ui/screens/add_new/styles.cljs +++ /dev/null @@ -1,41 +0,0 @@ -(ns status-im.ui.screens.add-new.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :as styles])) - -(def new-chat-container - {:flex-direction :row - :align-items :center}) - -(def input-container - {:flex-direction :row - :align-items :center - :border-radius styles/border-radius - :height 52 - :background-color colors/gray-lighter - :margin-horizontal 14 - :margin-top 24}) - -(def new-chat-input-container - (merge - input-container - {:flex 1 - :margin-horizontal 0 - :margin-left 14})) - -(def button-container - {:justify-content :center - :border-radius styles/border-radius - :height 52 - :background-color colors/gray-lighter - :padding-horizontal 15 - :margin-right 14 - :margin-left 3 - :margin-top 24}) - -(defstyle input - {:flex 1 - :font-size 15 - :letter-spacing -0.2 - :padding-horizontal 14 - :android {:padding 0}}) diff --git a/src/status_im/ui/screens/add_new/views.cljs b/src/status_im/ui/screens/add_new/views.cljs deleted file mode 100644 index db803576e70..00000000000 --- a/src/status_im/ui/screens/add_new/views.cljs +++ /dev/null @@ -1,62 +0,0 @@ -(ns status-im.ui.screens.add-new.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.action-button.action-button :as action-button] - [status-im.ui.components.action-button.styles :as action-button.styles] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.common.common :as common] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.react :as react] - [status-im.ui.components.styles :as styles] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.utils.config :as config])) - -(defn- options-list [{:keys [anon-id]}] - [react/view action-button.styles/actions-list - [action-button/action-button - {:label (i18n/label :t/start-new-chat) - :accessibility-label :start-1-1-chat-button - :icon :icons/newchat - :icon-opts {:color colors/blue} - :on-press #(re-frame/dispatch [:navigate-to :new-chat])}] - [action-button/action-separator] - ;; Hide behind flag (false by default), till everything is fixed in group chats - (when config/group-chats-enabled? - [action-button/action-button - {:label (i18n/label :t/start-group-chat) - :accessibility-label :start-group-chat-button - :icon :icons/contacts - :icon-opts {:color colors/blue} - :on-press #(re-frame/dispatch [:open-contact-toggle-list])}]) - [action-button/action-separator] - [action-button/action-button - {:label (i18n/label :t/new-public-group-chat) - :accessibility-label :join-public-chat-button - :icon :icons/public - :icon-opts {:color colors/blue} - :on-press #(re-frame/dispatch [:navigate-to :new-public-chat])}] - [action-button/action-separator] - [action-button/action-button - {:label (i18n/label :t/open-dapp) - :accessibility-label :open-dapp-button - :icon :icons/address - :icon-opts {:color colors/blue} - :on-press #(re-frame/dispatch [:navigate-to :open-dapp])}] - [action-button/action-separator] - [action-button/action-button - {:label (i18n/label :t/invite-friends) - :accessibility-label :invite-friends-button - :icon :icons/share - :icon-opts {:color colors/blue} - :on-press #(list-selection/open-share {:message (i18n/label :t/get-status-at)})}]]) - -(views/defview add-new [] - (views/letsubs [account [:get-current-account] - device-UUID [:get-device-UUID]] - [react/view {:flex 1 :background-color :white} - [status-bar/status-bar] - [toolbar/simple-toolbar (i18n/label :t/new)] - [common/separator] - [options-list (assoc account :anon-id device-UUID)]])) diff --git a/src/status_im/ui/screens/bootnodes_settings/db.cljs b/src/status_im/ui/screens/bootnodes_settings/db.cljs deleted file mode 100644 index 42e723359d2..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/db.cljs +++ /dev/null @@ -1,18 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require - [clojure.string :as string] - [cljs.spec.alpha :as spec])) - -(spec/def ::not-blank-string (complement string/blank?)) - -(spec/def :bootnode/address ::not-blank-string) -(spec/def :bootnode/name ::not-blank-string) -(spec/def :bootnode/id ::not-blank-string) -(spec/def :bootnode/chain ::not-blank-string) -(spec/def :bootnode/bootnode (allowed-keys :req-un [:bootnode/chain - :bootnode/address - :bootnode/name - :bootnode/id])) - -(spec/def :bootnodes/bootnodes (spec/nilable (spec/map-of :bootnode/id (spec/map-of :bootnode/id :bootnode/bootnode)))) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/events.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/events.cljs deleted file mode 100644 index 8fa6bf45d8b..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/events.cljs +++ /dev/null @@ -1,33 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.events - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.utils.handlers :refer [register-handler] :as handlers] - [status-im.models.bootnode :as models.bootnode])) - -(handlers/register-handler-fx - :save-new-bootnode - [(re-frame/inject-cofx :random-id)] - (fn [cofx _] - (models.bootnode/upsert cofx))) - -(handlers/register-handler-fx - :bootnode-set-input - (fn [cofx [_ input-key value]] - (models.bootnode/set-input input-key value cofx))) - -(handlers/register-handler-fx - :edit-bootnode - (fn [cofx [_ bootnode-id]] - (models.bootnode/edit bootnode-id cofx))) - -(handlers/register-handler-fx - :delete-bootnode - (fn [cofx [_ bootnode-id]] - (assoc (models.bootnode/delete bootnode-id cofx) - :dispatch [:navigate-back]))) - -(handlers/register-handler-fx - :set-bootnode-from-qr - (fn [cofx [_ _ url]] - (assoc (models.bootnode/set-input :url url cofx) - :dispatch [:navigate-back]))) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/styles.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/styles.cljs deleted file mode 100644 index 14ae4cb1761..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/styles.cljs +++ /dev/null @@ -1,53 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.ui.components.colors :as colors])) - -(def input-container - {:flex-direction :row - :align-items :center - :justify-content :space-between - :border-radius styles/border-radius - :height 52 - :margin-top 15}) - -(defstyle input - {:flex 1 - :font-size 15 - :letter-spacing -0.2 - :android {:padding 0}}) - -(def qr-code - {:margin-right 14}) - -(def edit-bootnode-view - {:flex 1 - :margin-horizontal 16 - :margin-vertical 15}) - -(def bottom-container - {:flex-direction :row - :margin-horizontal 12 - :margin-vertical 15}) - -(def button-container - {:margin-top 8 - :margin-bottom 16 - :margin-horizontal 16}) - -(def button - {:height 52 - :align-items :center - :justify-content :center - :border-radius 8 - :ios {:opacity 0.9}}) - -(defstyle button-label - {:color colors/white - :ios {:font-size 17 - :letter-spacing -0.2} - :android {:font-size 14}}) - -(defstyle delete-button - (assoc button - :background-color colors/red)) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/subs.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/subs.cljs deleted file mode 100644 index 49e41fdc820..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/subs.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.subs - (:require [re-frame.core :refer [reg-sub]])) - -(reg-sub - :get-manage-bootnode - :<- [:get :bootnodes/manage] - (fn [manage] - manage)) - -(reg-sub - :manage-bootnode-valid? - :<- [:get-manage-bootnode] - (fn [manage] - (not-any? :error (vals manage)))) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/views.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/views.cljs deleted file mode 100644 index 41fb647040d..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/views.cljs +++ /dev/null @@ -1,76 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.views - (:require-macros [status-im.utils.views :as views]) - (:require - [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.i18n :as i18n] - [status-im.utils.utils :as utils] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.text-input.view :as text-input] - [status-im.ui.screens.bootnodes-settings.edit-bootnode.styles :as styles])) - -(defn handle-delete [id] - (utils/show-confirmation (i18n/label :t/delete-bootnode-title) - (i18n/label :t/delete-bootnode-are-you-sure) - (i18n/label :t/delete-bootnode) - #(re-frame/dispatch [:delete-bootnode id]))) - -(defn delete-button [id] - [react/touchable-highlight {:on-press #(handle-delete id)} - [react/view styles/button-container - [react/view {:style styles/delete-button - :accessibility-label :bootnode-delete-button} - [react/text {:style styles/button-label - :uppercase? true} - (i18n/label :t/delete)]]]]) - -(def qr-code - [react/touchable-highlight {:on-press #(re-frame/dispatch [:scan-qr-code - {:toolbar-title (i18n/label :t/add-bootnode)} - :set-bootnode-from-qr]) - :style styles/qr-code} - [react/view - [vector-icons/icon :icons/qr {:color colors/blue}]]]) - -(views/defview edit-bootnode [] - (views/letsubs [manage-bootnode [:get-manage-bootnode] - is-valid? [:manage-bootnode-valid?]] - (let [url (get-in manage-bootnode [:url :value]) - id (get-in manage-bootnode [:id :value]) - name (get-in manage-bootnode [:name :value])] - [react/view components.styles/flex - [status-bar/status-bar] - [react/keyboard-avoiding-view components.styles/flex - [toolbar/simple-toolbar (i18n/label (if id :t/bootnode-details :t/add-bootnode))] - [react/scroll-view {:keyboard-should-persist-taps :handled} - [react/view styles/edit-bootnode-view - [text-input/text-input-with-label - {:label (i18n/label :t/name) - :placeholder (i18n/label :t/specify-name) - :style styles/input - :container styles/input-container - :default-value name - :on-change-text #(re-frame/dispatch [:bootnode-set-input :name %]) - :auto-focus true}] - [text-input/text-input-with-label - {:label (i18n/label :t/bootnode-address) - :placeholder (i18n/label :t/specify-bootnode-address) - :content qr-code - :style styles/input - :container styles/input-container - :default-value url - :on-change-text #(re-frame/dispatch [:bootnode-set-input :url %])}] - (when id - [delete-button id])]] - [react/view styles/bottom-container - [react/view components.styles/flex] - [components.common/bottom-button - {:forward? true - :label (i18n/label :t/save) - :disabled? (not is-valid?) - :on-press #(re-frame/dispatch [:save-new-bootnode])}]]]]))) diff --git a/src/status_im/ui/screens/bootnodes_settings/events.cljs b/src/status_im/ui/screens/bootnodes_settings/events.cljs deleted file mode 100644 index 57c90840c3f..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/events.cljs +++ /dev/null @@ -1,18 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.events - (:require [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.ui.screens.accounts.models :as accounts.models] - status-im.ui.screens.bootnodes-settings.edit-bootnode.events)) - -(defn toggle-custom-bootnodes [value {:keys [db] :as cofx}] - (let [network (get-in db [:account/account :network]) - settings (get-in db [:account/account :settings])] - (handlers-macro/merge-fx cofx - (accounts.models/update-settings - (assoc-in settings [:bootnodes network] value) - [:logout])))) - -(handlers/register-handler-fx - :toggle-custom-bootnodes - (fn [cofx [_ value]] - (toggle-custom-bootnodes value cofx))) diff --git a/src/status_im/ui/screens/bootnodes_settings/styles.cljs b/src/status_im/ui/screens/bootnodes_settings/styles.cljs deleted file mode 100644 index fd69219d6f7..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/styles.cljs +++ /dev/null @@ -1,30 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.styles - (:require [status-im.ui.components.colors :as colors]) - (:require-macros [status-im.utils.styles :refer [defstyle]])) - -(def wrapper - {:flex 1 - :background-color :white}) - -(def bootnode-item-inner - {:padding-horizontal 16}) - -(defstyle bootnode-item - {:flex-direction :row - :background-color :white - :align-items :center - :padding-horizontal 16 - :ios {:height 64} - :android {:height 56}}) - -(defstyle bootnode-item-name-text - {:color colors/black - :ios {:font-size 17 - :letter-spacing -0.2 - :line-height 20} - :android {:font-size 16}}) - -(defstyle switch-container - {:height 50 - :background-color colors/white - :padding-left 15}) diff --git a/src/status_im/ui/screens/bootnodes_settings/subs.cljs b/src/status_im/ui/screens/bootnodes_settings/subs.cljs deleted file mode 100644 index 5daa5c8500b..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/subs.cljs +++ /dev/null @@ -1,15 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.subs - (:require [re-frame.core :as re-frame] - status-im.ui.screens.bootnodes-settings.edit-bootnode.subs - [status-im.utils.ethereum.core :as ethereum])) - -(re-frame/reg-sub :settings/bootnodes-enabled - :<- [:get :account/account] - (fn [account] - (let [{:keys [network settings]} account] - (get-in settings [:bootnodes network])))) - -(re-frame/reg-sub :settings/network-bootnodes - :<- [:get :account/account] - (fn [account] - (get-in account [:bootnodes (:network account)]))) diff --git a/src/status_im/ui/screens/bootnodes_settings/views.cljs b/src/status_im/ui/screens/bootnodes_settings/views.cljs deleted file mode 100644 index 5f32510d54c..00000000000 --- a/src/status_im/ui/screens/bootnodes_settings/views.cljs +++ /dev/null @@ -1,47 +0,0 @@ -(ns status-im.ui.screens.bootnodes-settings.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.utils.config :as config] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.toolbar.actions :as toolbar.actions] - [status-im.ui.screens.profile.components.views :as profile.components] - [status-im.ui.screens.bootnodes-settings.styles :as styles])) - -(defn navigate-to-add-bootnode [id] - (re-frame/dispatch [:edit-bootnode id])) - -(defn render-row [{:keys [name id]}] - [react/touchable-highlight - {:on-press #(navigate-to-add-bootnode id) - :accessibility-label :bootnode-item} - [react/view styles/bootnode-item - [react/view styles/bootnode-item-inner - [react/text {:style styles/bootnode-item-name-text} - name]]]]) - -(views/defview bootnodes-settings [] - (views/letsubs [bootnodes-enabled [:settings/bootnodes-enabled] - bootnodes [:settings/network-bootnodes]] - [react/view {:flex 1} - [status-bar/status-bar] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/bootnodes-settings)] - [toolbar/actions - [(toolbar.actions/add false #(navigate-to-add-bootnode nil))]]] - [react/view styles/switch-container - [profile.components/settings-switch-item - {:label-kw :t/bootnodes-enabled - :value bootnodes-enabled - :action-fn #(re-frame/dispatch [:toggle-custom-bootnodes %])}]] - [react/view styles/wrapper - [list/flat-list {:data (vals bootnodes) - :default-separator? false - :key-fn :id - :render-fn render-row}]]])) diff --git a/src/status_im/ui/screens/browser/db.cljs b/src/status_im/ui/screens/browser/db.cljs deleted file mode 100644 index d425b5ecebf..00000000000 --- a/src/status_im/ui/screens/browser/db.cljs +++ /dev/null @@ -1,43 +0,0 @@ -(ns status-im.ui.screens.browser.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec])) - -(spec/def :browser/browser-id (spec/nilable string?)) -(spec/def :browser/timestamp (spec/nilable int?)) -(spec/def :browser/name (spec/nilable string?)) -(spec/def :browser/dapp? (spec/nilable boolean?)) -(spec/def :browser/error? (spec/nilable boolean?)) -(spec/def :browser/history (spec/nilable vector?)) -(spec/def :browser/history-index (spec/nilable int?)) -(spec/def :browser/loading? (spec/nilable boolean?)) -(spec/def :browser/url-editing? (spec/nilable boolean?)) -(spec/def :browser/show-tooltip (spec/nilable keyword?)) - -(spec/def :browser/options - (allowed-keys - :opt-un [:browser/browser-id - :browser/loading? - :browser/url-editing? - :browser/show-tooltip - :browser/error?])) - -(spec/def :browser/browser - (allowed-keys - :req-un [:browser/browser-id - :browser/timestamp] - :opt-un [:browser/name - :browser/dapp? - :browser/history - :browser/history-index])) - -(spec/def :browser/browsers (spec/nilable (spec/map-of :global/not-empty-string :browser/browser))) - -(spec/def :dapp/dapp (spec/nilable string?)) -(spec/def :dapp/permissions (spec/nilable vector?)) - -(spec/def :dapp/permission-map - (allowed-keys - :req-un [:dapp/dapp] - :opt-un [:dapp/permissions])) - -(spec/def :dapps/permissions (spec/nilable (spec/map-of :global/not-empty-string :dapp/permission-map))) \ No newline at end of file diff --git a/src/status_im/ui/screens/browser/default_dapps.cljs b/src/status_im/ui/screens/browser/default_dapps.cljs deleted file mode 100644 index 327840e1ab5..00000000000 --- a/src/status_im/ui/screens/browser/default_dapps.cljs +++ /dev/null @@ -1,133 +0,0 @@ -(ns status-im.ui.screens.browser.default-dapps) - -(def all - [{:title "Exchanges" - :data [{:name "Airswap" - :dapp-url "https://www.airswap.io/trade" - :photo-path "contacts://airswap" - :description "Meet the future of trading."} - {:name "Bancor" - :dapp-url "https://www.bancor.network/" - :photo-path "contacts://bancor" - :description "Bancor is a decentralized liquidity network"} - {:name "ERC dEX" - :dapp-url "https://app.ercdex.com/" - :photo-path "contacts://erc-dex" - :description "Trustless trading has arrived on Ethereum"} - {:name "Kyber" - :dapp-url "https://web3.kyber.network" - :photo-path "contacts://kyber" - :description "On-chain, instant and liquid platform for exchange and payment service"} - {:name "Oasis Direct" - :dapp-url "https://oasis.direct/" - :photo-path "contacts://oasis-direct" - :description "The first decentralized instant exchange"} - {:name "DAI by MakerDao" - :dapp-url "https://dai.makerdao.com" - :photo-path "contacts://dai" - :description "Stability for the blockchain"} - {:name "LocalEthereum" - :dapp-url "https://localethereum.com/" - :photo-path "contacts://local-ethereum" - :description "The smartest way to buy and sell Ether"} - {:name "Eth2phone" - :dapp-url "https://eth2.io" - :photo-path "contacts://eth2phone" - :description "Send Ether by phone number"} - {:name "DDEX" - :dapp-url "https://ddex.io/" - :photo-path "contacts://ddex" - :description "Instant, real-time order matching with secure on-chain settlement"} - {:name "EasyTrade" - :dapp-url "https://easytrade.io" - :photo-path "contacts://easytrade" - :description "One exchange for every token"}]} - {:title "Marketplaces" - :data [{:name "CryptoCribs" - :dapp-url "https://cryptocribs.com" - :photo-path "contacts://cryptocribs" - :description "Travel the globe. Pay in crypto."} - {:name "Ethlance" - :dapp-url "https://ethlance.com" - :photo-path "contacts://ethlance" - :description "The future of work is now. Hire people or work yourself in return for ETH."} - {:name "OpenSea" - :dapp-url "https://opensea.io" - :photo-path "contacts://opensea" - :description "The largest decentralized marketplace for cryptogoods"} - {:name "Name Bazaar" - :dapp-url "https://namebazaar.io" - :photo-path "contacts://name-bazaar" - :description "ENS name marketplace"} - {:name "The Bounties Network" - :dapp-url "https://moksha.bounties.network/" - :photo-path "contacts://bounties-network" - :description "Bounties on any task, paid in any token"} - {:name "Emoon" - :dapp-url "https://www.emoon.io/" - :photo-path "contacts://emoon" - :description "A decentralized marketplace for buying & selling crypto assets"}]} - {:title "Fun & Games" - :data [{:name "CryptoKitties" - :dapp-url "https://www.cryptokitties.co" - :photo-path "contacts://cryptokitties" - :description "Collect and breed adorable digital cats."} - {:name "CryptoFighters" - :dapp-url "https://cryptofighters.io" - :photo-path "contacts://cryptofighters" - :description "Collect train and fight digital fighters."} - {:name "CryptoPunks" - :dapp-url "https://www.larvalabs.com/cryptopunks" - :photo-path "contacts://cryptopunks" - :description "10,000 unique collectible punks"} - {:name "Etherbots" - :dapp-url "https://etherbots.io/" - :photo-path "contacts://etherbots" - :description "Robot wars on the Ethereum Platform"} - {:name "Etheremon" - :dapp-url "https://www.etheremon.com/" - :photo-path "contacts://etheremon" - :description "Decentralized World of Ether Monsters"} - {:name "CryptoStrikers" - :dapp-url "https://www.cryptostrikers.com/" - :photo-path "contacts://cryptostrikers" - :description "The Beautiful (card) Game"}]} - {:title "Social Networks" - :data [{:name "Cent" - :dapp-url "https://beta.cent.co/" - :photo-path "contacts://cent" - :description "Get wisdom, get money"} - {:name "Peepeth" - :dapp-url "https://peepeth.com/" - :photo-path "contacts://peepeth" - :description "Blockchain-powered microblogging"} - {:name "Purrbook" - :dapp-url "https://cryptopurr.co/" - :photo-path "contacts://cryptopurr" - :description "A social network for CryptoKitties"}]} - {:title "Utilities" - :data [{:name "Civitas" - :dapp-url "https://communities.colu.com/" - :photo-path "contacts://civitas" - :description "Blockchain-powered local communities"} - {:name "Hexel" - :dapp-url "https://www.onhexel.com/" - :photo-path "contacts://hexel" - :description "Create your own cryptocurrency"} - {:name "ETHLend" - :dapp-url "https://app.ethlend.io" - :photo-path "contacts://ethlend" - :description "Decentralized lending on Ethereum"} - {:name "Smartz" - :dapp-url "https://smartz.io" - :photo-path "contacts://smartz" - :description "Easy smart contract management"} - {:name "Status Test DApp" - :dapp-url "https://status-im.github.io/dapp/" - :description "Request test assets and test basic web3 functionality." - :developer? true}]} - {:title "Other: Media" - :data [{:name "LivePeer" - :dapp-url "https://media.livepeer.org/" - :photo-path "contacts://livepeer" - :description "Decentralized video broadcasting"}]}]) diff --git a/src/status_im/ui/screens/browser/events.cljs b/src/status_im/ui/screens/browser/events.cljs deleted file mode 100644 index 5419073235e..00000000000 --- a/src/status_im/ui/screens/browser/events.cljs +++ /dev/null @@ -1,138 +0,0 @@ -(ns status-im.ui.screens.browser.events - (:require status-im.ui.screens.browser.navigation - [status-im.utils.handlers :as handlers] - [re-frame.core :as re-frame] - [status-im.utils.random :as random] - [status-im.i18n :as i18n] - [status-im.ui.components.list-selection :as list-selection] - [status-im.utils.universal-links.core :as utils.universal-links] - [status-im.data-store.browser :as browser-store] - [status-im.utils.http :as http] - [status-im.models.browser :as model] - [status-im.utils.platform :as platform] - [status-im.utils.utils :as utils] - [status-im.constants :as constants])) - -(re-frame/reg-fx - :browse - (fn [link] - (if (utils.universal-links/universal-link? link) - (utils.universal-links/open! link) - (list-selection/browse link)))) - -(re-frame/reg-fx - :send-to-bridge-fx - (fn [[permissions-allowed webview]] - (.sendToBridge @webview (.stringify js/JSON (clj->js {:type constants/status-api-success - :data permissions-allowed - :keys (keys permissions-allowed)}))))) - -(re-frame/reg-fx - :show-dapp-permission-confirmation-fx - (fn [[permission {:keys [dapp-name permissions-data] :as params}]] - (utils/show-confirmation - {:ios-confirm-style "default"} - (str "\"" dapp-name "\" " (i18n/label :t/would-like-to-access) " " (:label (get model/permissions permission))) - (i18n/label :t/make-sure-you-trust-dapp) - nil - #(re-frame/dispatch [:next-dapp-permission params permission permissions-data]) - #(re-frame/dispatch [:next-dapp-permission params]) - (i18n/label :t/dont-allow)))) - -(handlers/register-handler-fx - :initialize-browsers - [(re-frame/inject-cofx :data-store/all-browsers)] - (fn [{:keys [db all-stored-browsers]} _] - (let [browsers (into {} (map #(vector (:browser-id %) %) all-stored-browsers))] - {:db (assoc db :browser/browsers browsers)}))) - -(handlers/register-handler-fx - :initialize-dapp-permissions - [(re-frame/inject-cofx :data-store/all-dapp-permissions)] - (fn [{:keys [db all-dapp-permissions]} _] - (let [dapp-permissions (into {} (map #(vector (:dapp %) %) all-dapp-permissions))] - {:db (assoc db :dapps/permissions dapp-permissions)}))) - -(handlers/register-handler-fx - :browse-link-from-message - (fn [_ [_ link]] - {:browse link})) - -(handlers/register-handler-fx - :open-url-in-browser - [re-frame/trim-v] - (fn [cofx [url]] - (let [normalized-url (http/normalize-and-decode-url url)] - (model/update-browser-and-navigate cofx {:browser-id (or (http/url-host normalized-url) (random/id)) - :history-index 0 - :history [normalized-url]})))) - -(handlers/register-handler-fx - :open-browser - [re-frame/trim-v] - (fn [cofx [browser]] - (model/update-browser-and-navigate cofx browser))) - -(handlers/register-handler-fx - :update-browser-on-nav-change - [re-frame/trim-v] - (fn [cofx [browser url loading]] - (model/update-browser-history-fx cofx browser url loading))) - -(handlers/register-handler-fx - :update-browser-options - [re-frame/trim-v] - (fn [{:keys [db]} [options]] - {:db (update db :browser/options merge options)})) - -(handlers/register-handler-fx - :remove-browser - [re-frame/trim-v] - (fn [{:keys [db]} [browser-id]] - {:db (update-in db [:browser/browsers] dissoc browser-id) - :data-store/tx [(browser-store/remove-browser-tx browser-id)]})) - -(defn nav-update-browser [cofx browser history-index] - (model/update-browser-fx cofx (assoc browser :history-index history-index))) - -(handlers/register-handler-fx - :browser-nav-back - [re-frame/trim-v] - (fn [cofx [{:keys [history-index] :as browser}]] - (when (pos? history-index) - (nav-update-browser cofx browser (dec history-index))))) - -(handlers/register-handler-fx - :browser-nav-forward - [re-frame/trim-v] - (fn [cofx [{:keys [history-index] :as browser}]] - (when (< history-index (dec (count (:history browser)))) - (nav-update-browser cofx browser (inc history-index))))) - -(handlers/register-handler-fx - :on-bridge-message - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [{{:keys [url]} :navState :keys [type host permissions]} browser webview]] - (cond - - (and (= type constants/history-state-changed) platform/ios? (not= "about:blank" url)) - (model/update-browser-history-fx cofx browser url false) - - (= type constants/status-api-request) - (let [{:account/keys [account]} db - {:keys [dapp? name]} browser - dapp-name (if dapp? name host)] - (model/request-permission - cofx - {:dapp-name dapp-name - :webview webview - :index 0 - :user-permissions (get-in db [:dapps/permissions dapp-name :permissions]) - :requested-permissions permissions - :permissions-data {constants/dapp-permission-contact-code (:public-key account)}}))))) - -(handlers/register-handler-fx - :next-dapp-permission - [re-frame/trim-v] - (fn [cofx [params permission permissions-data]] - (model/next-permission cofx params permission permissions-data))) \ No newline at end of file diff --git a/src/status_im/ui/screens/browser/navigation.cljs b/src/status_im/ui/screens/browser/navigation.cljs deleted file mode 100644 index 2105b794cb1..00000000000 --- a/src/status_im/ui/screens/browser/navigation.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns status-im.ui.screens.browser.navigation - (:require [status-im.ui.screens.navigation :as navigation])) - -(defmethod navigation/preload-data! :browser - [db [_ _ browser-id]] - (assoc db :browser/options {:browser-id browser-id})) diff --git a/src/status_im/ui/screens/browser/styles.cljs b/src/status_im/ui/screens/browser/styles.cljs deleted file mode 100644 index f1de41f12e0..00000000000 --- a/src/status_im/ui/screens/browser/styles.cljs +++ /dev/null @@ -1,84 +0,0 @@ -(ns status-im.ui.screens.browser.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def browser {:flex 1}) - -(defstyle dapp-name - {:flex 1 - :justify-content :center - :margin-left 12 - :android {:padding-bottom 6}}) - -(def dapp-name-text - {:color colors/text-light-gray - :font-size 16}) - -(defstyle dapp-text - {:color colors/gray - :ios {:margin-top 4} - :android {:font-size 13}}) - -(def toolbar - {:background-color :white - :height 48 - :flex-direction :row - :align-items :center - :padding-horizontal 32}) - -(def disabled-button - {:opacity 0.4}) - -(def forward-button - {:margin-left 72}) - -(def background - {:flex 1 - :background-color colors/gray-lighter - :align-items :center - :justify-content :center}) - -(def web-view-loading - {:flex 1 - :background-color colors/gray-transparent - :align-items :center - :justify-content :center - :position :absolute - :top 0 - :bottom 0 - :left 0 - :right 0}) - -(def web-view-error - {:flex 1 - :justify-content :center - :align-items :center - :background-color colors/gray-lighter}) - -(def web-view-error-text - {:color colors/gray}) - -(defnstyle toolbar-content [show-actions] - {:flex-direction :row - :flex 1 - :border-radius 4 - :height 36 - :background-color colors/gray-lighter - :padding-horizontal 12 - :margin-right 5 - :align-items :center - :android {:margin-left (if show-actions 66 20)} - :ios {:margin-left 20}}) - -(defstyle url-input - {:flex 1 - :font-size 14 - :letter-spacing -0.2 - :margin-horizontal 5 - :android {:padding 0}}) - -(def url-text - {:font-size 14 - :letter-spacing -0.2 - :margin-horizontal 5}) - diff --git a/src/status_im/ui/screens/browser/subs.cljs b/src/status_im/ui/screens/browser/subs.cljs deleted file mode 100644 index 7781606f5ae..00000000000 --- a/src/status_im/ui/screens/browser/subs.cljs +++ /dev/null @@ -1,11 +0,0 @@ -(ns status-im.ui.screens.browser.subs - (:require [re-frame.core :as re-frame])) - -(re-frame/reg-sub :browsers :browser/browsers) - -(re-frame/reg-sub - :get-current-browser - :<- [:get :browser/options] - :<- [:browsers] - (fn [[options browsers]] - (get browsers (:browser-id options)))) \ No newline at end of file diff --git a/src/status_im/ui/screens/browser/views.cljs b/src/status_im/ui/screens/browser/views.cljs deleted file mode 100644 index 61197c9913d..00000000000 --- a/src/status_im/ui/screens/browser/views.cljs +++ /dev/null @@ -1,151 +0,0 @@ -(ns status-im.ui.screens.browser.views - (:require-macros [status-im.utils.slurp :refer [slurp]] - [status-im.utils.views :as views]) - (:require [cljs.reader :as reader] - [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.ui.screens.browser.styles :as styles] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar.view] - [status-im.ui.components.webview-bridge :as components.webview-bridge] - [status-im.utils.js-resources :as js-res] - [status-im.ui.components.react :as components] - [reagent.core :as reagent] - [status-im.ui.components.icons.vector-icons :as icons] - [status-im.i18n :as i18n] - [status-im.utils.ethereum.core :as ethereum] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.tooltip.views :as tooltip] - [status-im.models.browser :as model] - [status-im.utils.http :as http] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.colors :as colors] - [clojure.string :as string])) - -(def browser-config - (reader/read-string (slurp "./src/status_im/utils/browser_config.edn"))) - -(defn toolbar-content [url {:keys [dapp? history history-index] :as browser} error? url-editing?] - (let [url-text (atom url) - history-url (try (nth history history-index) (catch js/Error _)) - secure? (or dapp? (and (not error?) (string/starts-with? history-url "https://")))] - [react/view - [react/view (styles/toolbar-content false) - [react/touchable-highlight {:on-press #(re-frame/dispatch [:update-browser-options - {:show-tooltip (if secure? :secure :not-secure)}])} - (if secure? - [icons/icon :icons/lock {:color colors/green}] - [icons/icon :icons/lock-opened])] - (if url-editing? - [react/text-input {:on-change-text #(reset! url-text %) - :on-blur #(re-frame/dispatch [:update-browser-options {:url-editing? false}]) - :on-submit-editing #(do - (re-frame/dispatch [:update-browser-options {:url-editing? false}]) - (re-frame/dispatch [:update-browser-on-nav-change - browser - (http/normalize-and-decode-url @url-text) - false])) - :placeholder (i18n/label :t/enter-url) - :auto-capitalize :none - :auto-correct false - :auto-focus true - :default-value url - :ellipsize :end - :style styles/url-input}] - [react/touchable-highlight {:style {:flex 1} :on-press #(re-frame/dispatch [:update-browser-options {:url-editing? true}])} - [react/text {:style styles/url-text} (http/url-host url)]])]])) - -(defn- web-view-error [_ code desc] - (reagent/as-element - [react/view styles/web-view-error - [react/i18n-text {:style styles/web-view-error-text :key :web-view-error}] - [react/text {:style styles/web-view-error-text} - (str code)] - [react/text {:style styles/web-view-error-text} - (str desc)]])) - -(defn on-navigation-change [event browser] - (let [{:strs [url loading]} (js->clj event)] - (re-frame/dispatch [:update-browser-options {:loading? loading}]) - (when (not= "about:blank" url) - (re-frame/dispatch [:update-browser-on-nav-change browser url loading])))) - -(defn get-inject-js [url] - (let [domain-name (nth (re-find #"^\w+://(www\.)?([^/:]+)" url) 2)] - (get (:inject-js browser-config) domain-name))) - -(views/defview browser [] - (views/letsubs [webview (atom nil) - {:keys [address]} [:get-current-account] - {:keys [browser-id] :as browser} [:get-current-browser] - {:keys [error? loading? url-editing? show-tooltip]} [:get :browser/options] - rpc-url [:get :rpc-url] - network-id [:get-network-id]] - (let [can-go-back? (model/can-go-back? browser) - can-go-forward? (model/can-go-forward? browser) - url (model/get-current-url browser)] - [react/view styles/browser - [status-bar/status-bar] - [toolbar.view/toolbar {} - [toolbar.view/nav-button-with-count - (actions/close (fn [] - (when @webview - (.sendToBridge @webview "navigate-to-blank")) - (re-frame/dispatch [:navigate-back]) - (when error? - (re-frame/dispatch [:remove-browser browser-id]))))] - [toolbar-content url browser error? url-editing?] - [toolbar.view/actions [{:icon :icons/wallet - :icon-opts {:color :black - :accessibility-label :wallet-modal-button} - :handler #(re-frame/dispatch [:navigate-to-modal :wallet-modal])}]]] - [react/view components.styles/flex - [components.webview-bridge/webview-bridge - {:ref #(reset! webview %) - :source {:uri url} - :java-script-enabled true - :bounces false - :local-storage-enabled true - :render-error web-view-error - :on-navigation-state-change #(on-navigation-change % browser) - :on-bridge-message #(re-frame/dispatch [:on-bridge-message - (js->clj (.parse js/JSON %) - :keywordize-keys true) - browser - webview]) - :on-load #(re-frame/dispatch [:update-browser-options {:error? false}]) - :on-error #(re-frame/dispatch [:update-browser-options {:error? true - :loading? false}]) - :injected-on-start-loading-java-script (str js-res/web3 - (get-inject-js url) - (js-res/web3-init - rpc-url - (ethereum/normalized-address address) - (str network-id))) - :injected-java-script js-res/webview-js}] - (when loading? - [react/view styles/web-view-loading - [components/activity-indicator {:animating true}]])] - [react/view styles/toolbar - [react/touchable-highlight {:on-press #(re-frame/dispatch [:browser-nav-back browser]) - :disabled (not can-go-back?) - :style (when-not can-go-back? styles/disabled-button) - :accessibility-label :previou-page-button} - [react/view - [icons/icon :icons/arrow-left]]] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:browser-nav-forward browser]) - :disabled (not can-go-forward?) - :style (merge styles/forward-button - (when-not can-go-forward? styles/disabled-button)) - :accessibility-label :next-page-button} - [react/view - [icons/icon :icons/arrow-right]]] - [react/view {:flex 1}] - [react/touchable-highlight {:on-press #(.reload @webview)} - [icons/icon :icons/refresh]]] - (when show-tooltip - [tooltip/bottom-tooltip-info - (if (= show-tooltip :secure) - (i18n/label :t/browser-secure) - (i18n/label :t/browser-not-secure)) - #(re-frame/dispatch [:update-browser-options {:show-tooltip nil}])])]))) \ No newline at end of file diff --git a/src/status_im/ui/screens/contacts/db.cljs b/src/status_im/ui/screens/contacts/db.cljs deleted file mode 100644 index 870c8ad0270..00000000000 --- a/src/status_im/ui/screens/contacts/db.cljs +++ /dev/null @@ -1,83 +0,0 @@ -(ns status-im.ui.screens.contacts.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec] - status-im.utils.db)) - -;;;; DB - -;;Contact - -;we can't validate public key, because for dapps whisper-identity is just string -(spec/def :contact/whisper-identity :global/not-empty-string) -(spec/def :contact/name :global/not-empty-string) -(spec/def :contact/address (spec/nilable :global/address)) -(spec/def :contact/public-key (spec/nilable string?)) -(spec/def :contact/photo-path (spec/nilable string?)) -(spec/def :contact/status (spec/nilable string?)) -(spec/def :contact/fcm-token (spec/nilable string?)) -(spec/def :contact/description (spec/nilable string?)) - -(spec/def :contact/last-updated (spec/nilable int?)) -(spec/def :contact/last-online (spec/nilable int?)) -(spec/def :contact/pending? boolean?) -(spec/def :contact/unremovable? boolean?) -(spec/def :contact/hide-contact? boolean?) - -(spec/def :contact/dapp? boolean?) -(spec/def :contact/dapp-url (spec/nilable string?)) -(spec/def :contact/dapp-hash (spec/nilable int?)) -(spec/def :contact/bot-url (spec/nilable string?)) -(spec/def :contact/command (spec/nilable (spec/map-of int? map?))) -(spec/def :contact/response (spec/nilable (spec/map-of int? map?))) -(spec/def :contact/jail-loaded? (spec/nilable boolean?)) -(spec/def :contact/jail-loaded-events (spec/nilable seq?)) -(spec/def :contact/subscriptions (spec/nilable map?)) -;true when contact added using status-dev-cli -(spec/def :contact/debug? boolean?) - -(spec/def :contact/contact - (allowed-keys - :req-un [:contact/name] - :opt-un [:contact/whisper-identity - :contact/address - :contact/public-key - :contact/photo-path - :contact/status - :contact/last-updated - :contact/last-online - :contact/pending? - :contact/hide-contact? - :contact/unremovable? - :contact/dapp? - :contact/dapp-url - :contact/dapp-hash - :contact/bot-url - :contact/jail-loaded? - :contact/jail-loaded-events - :contact/command - :contact/response - :contact/debug? - :contact/subscriptions - :contact/fcm-token - :contact/description])) - -;;Contact list ui props -(spec/def :contact-list-ui/edit? boolean?) - -;;Contacts ui props -(spec/def :contacts-ui/edit? boolean?) - -(spec/def :contacts/contacts (spec/nilable (spec/map-of :global/not-empty-string :contact/contact))) -;public key of new contact during adding this new contact -(spec/def :contacts/new-identity (spec/nilable string?)) -(spec/def :contacts/new-public-key-error (spec/nilable string?)) -;on showing this contact's profile (andrey: better to move into profile ns) -(spec/def :contacts/identity (spec/nilable :global/not-empty-string)) -(spec/def :contacts/list-ui-props (spec/nilable (allowed-keys :opt-un [:contact-list-ui/edit?]))) -(spec/def :contacts/ui-props (spec/nilable (allowed-keys :opt-un [:contacts-ui/edit?]))) -;used in modal list (for example for wallet) -(spec/def :contacts/click-handler (spec/nilable fn?)) -;used in modal list (for example for wallet) -(spec/def :contacts/click-action (spec/nilable #{:send :request})) -;used in modal list (for example for wallet) -(spec/def :contacts/click-params (spec/nilable map?)) diff --git a/src/status_im/ui/screens/contacts/events.cljs b/src/status_im/ui/screens/contacts/events.cljs deleted file mode 100644 index 51e46ee2897..00000000000 --- a/src/status_im/ui/screens/contacts/events.cljs +++ /dev/null @@ -1,78 +0,0 @@ -(ns status-im.ui.screens.contacts.events - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.screens.add-new.new-chat.db :as new-chat.db] - [status-im.ui.screens.browser.default-dapps :as default-dapps] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.js-resources :as js-res] - [status-im.chat.events :as chat.events] - [status-im.ui.screens.navigation :as navigation] - [status-im.utils.utils :as utils] - [status-im.models.contact :as models.contact])) - -(defn add-contact-and-open-chat [whisper-id cofx] - (handlers-macro/merge-fx cofx - (navigation/navigate-to-clean :home) - (models.contact/add-contact whisper-id) - (chat.events/start-chat whisper-id {}))) - -(re-frame/reg-cofx - :get-default-contacts - (fn [coeffects _] - (assoc coeffects :default-contacts js-res/default-contacts))) - -(re-frame/reg-cofx - :get-default-dapps - (fn [coeffects _] - (assoc coeffects :default-dapps default-dapps/all))) - -;;;; Handlers - -(handlers/register-handler-fx - :load-contacts - [(re-frame/inject-cofx :data-store/get-all-contacts)] - (fn [{:keys [db all-contacts]} _] - (let [contacts-list (map #(vector (:whisper-identity %) %) all-contacts) - contacts (into {} contacts-list)] - {:db (update db :contacts/contacts #(merge contacts %))}))) - -(handlers/register-handler-fx - :add-contact - [(re-frame/inject-cofx :random-id)] - (fn [cofx [_ whisper-id]] - (models.contact/add-contact whisper-id cofx))) - -(handlers/register-handler-fx - :set-contact-identity-from-qr - [(re-frame/inject-cofx :random-id)] - (fn [{:keys [db] :as cofx} [_ _ contact-identity]] - (let [current-account (:account/account db) - fx {:db (assoc db :contacts/new-identity contact-identity)} - validation-result (new-chat.db/validate-pub-key contact-identity current-account)] - (if (some? validation-result) - (utils/show-popup (i18n/label :t/unable-to-read-this-code) validation-result #(re-frame/dispatch [:navigate-to-clean :home])) - (handlers-macro/merge-fx cofx - fx - (add-contact-and-open-chat contact-identity)))))) - -(handlers/register-handler-db - :open-contact-toggle-list - (fn [db _] - (-> (assoc db - :group/selected-contacts #{} - :new-chat-name "") - (navigation/navigate-to :contact-toggle-list)))) - -(handlers/register-handler-fx - :open-chat-with-contact - [(re-frame/inject-cofx :random-id)] - (fn [cofx [_ {:keys [whisper-identity]}]] - (add-contact-and-open-chat whisper-identity cofx))) - -(handlers/register-handler-fx - :add-contact-handler - [(re-frame/inject-cofx :random-id)] - (fn [{{:contacts/keys [new-identity]} :db :as cofx} _] - (when (seq new-identity) - (add-contact-and-open-chat new-identity cofx)))) diff --git a/src/status_im/ui/screens/contacts/subs.cljs b/src/status_im/ui/screens/contacts/subs.cljs deleted file mode 100644 index f276f578f88..00000000000 --- a/src/status_im/ui/screens/contacts/subs.cljs +++ /dev/null @@ -1,159 +0,0 @@ -(ns status-im.ui.screens.contacts.subs - (:require [re-frame.core :refer [reg-sub subscribe]] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.identicon :as identicon] - [status-im.utils.contacts :as utils.contacts])) - -(reg-sub :get-current-contact-identity :contacts/identity) - -(reg-sub :get-contacts :contacts/contacts) - -(reg-sub :get-dapps :contacts/dapps) - -(reg-sub :get-current-contact - :<- [:get-contacts] - :<- [:get-current-contact-identity] - (fn [[contacts identity]] - (contacts identity))) - -(reg-sub :get-current-chat-contact - :<- [:get-contacts] - :<- [:get-current-chat-id] - (fn [[contacts chat-id]] - (get contacts chat-id))) - -(defn sort-contacts [contacts] - (sort (fn [c1 c2] - (let [name1 (or (:name c1) (:address c1) (:whisper-identity c1)) - name2 (or (:name c2) (:address c2) (:whisper-identity c2))] - (compare (clojure.string/lower-case name1) - (clojure.string/lower-case name2)))) - (vals contacts))) - -(reg-sub :all-added-contacts - :<- [:get-contacts] - (fn [contacts] - (->> contacts - (remove (fn [[_ {:keys [pending? hide-contact?]}]] - (or pending? hide-contact?))) - (sort-contacts)))) - -(reg-sub :all-added-people-contacts - :<- [:all-added-contacts] - (fn [contacts] - (remove :dapp? contacts))) - -(defn- filter-dapps [v dev-mode?] - (remove #(when-not dev-mode? (true? (:developer? %))) v)) - -(reg-sub :all-dapps - :<- [:get-dapps] - :<- [:get-current-account] - (fn [[dapps {:keys [dev-mode?]}]] - (map (fn [m] (update m :data #(filter-dapps % dev-mode?))) dapps))) - -(reg-sub :get-people-in-current-chat - :<- [:get-current-chat-contacts] - (fn [contacts] - (remove #(true? (:dapp? %)) contacts))) - -(defn filter-group-contacts [group-contacts contacts] - (let [group-contacts' (into #{} group-contacts)] - (filter #(group-contacts' (:whisper-identity %)) contacts))) - -(reg-sub :get-contact-by-identity - :<- [:get-contacts] - (fn [contacts [_ identity]] - (or - (get contacts identity) - (utils.contacts/whisper-id->new-contact identity)))) - -(reg-sub :get-dapp-by-name - :<- [:get-dapps] - (fn [dapps [_ name]] - (first (filter #(= (:name %) name) (apply concat (map :data dapps)))))) - -(reg-sub :get-contact-name-by-identity - :<- [:get-contacts] - :<- [:get-current-account] - (fn [[contacts current-account] [_ identity]] - (let [me? (= (:public-key current-account) identity)] - (if me? - (:name current-account) - (:name (contacts identity)))))) - -(defn query-chat-contacts [[{:keys [contacts group-admin]} all-contacts] [_ query-fn]] - (let [participant-set (into #{} (filter identity) (conj contacts group-admin))] - (query-fn (comp participant-set :whisper-identity) (vals all-contacts)))) - -(reg-sub :query-current-chat-contacts - :<- [:get-current-chat] - :<- [:get-contacts] - query-chat-contacts) - -(reg-sub :get-all-contacts-not-in-current-chat - :<- [:query-current-chat-contacts remove] - identity) - -(defn get-all-contacts-in-group-chat [chat-contact-ids group-admin-id contacts current-account] - (let [participant-set (into #{} (filter identity) (conj chat-contact-ids group-admin-id)) - current-account-contact (-> current-account - (select-keys [:name :photo-path :public-key]) - (clojure.set/rename-keys {:public-key :whisper-identity})) - all-contacts (assoc contacts (:whisper-identity current-account-contact) current-account-contact)] - (map #(or (get all-contacts %) - (utils.contacts/whisper-id->new-contact %)) - participant-set))) - -(reg-sub :get-current-chat-contacts - :<- [:get-current-chat] - :<- [:get-contacts] - :<- [:get-current-account] - (fn [[{:keys [contacts group-admin]} all-contacts current-account]] - (get-all-contacts-in-group-chat contacts group-admin all-contacts current-account))) - -(reg-sub :get-contacts-by-chat - (fn [[_ _ chat-id] _] - [(subscribe [:get-chat chat-id]) - (subscribe [:get-contacts])]) - query-chat-contacts) - -(reg-sub :get-chat-photo - (fn [[_ chat-id] _] - [(subscribe [:get-chat chat-id]) - (subscribe [:get-contacts-by-chat filter chat-id])]) - (fn [[chat contacts] [_ chat-id]] - (when (and chat (not (:group-chat chat))) - (cond - (:photo-path chat) - (:photo-path chat) - - (pos? (count contacts)) - (:photo-path (first contacts)) - - :else - (identicon/identicon chat-id))))) - -(defn- address= [{:keys [address] :as contact} s] - (when (and address (= (ethereum/normalized-address s) - (ethereum/normalized-address address))) - contact)) - -(defn- contact-by-address [[_ contact] s] - (when (address= contact s) - contact)) - -(reg-sub :get-contact-by-address - :<- [:get-contacts] - (fn [contacts [_ address]] - (some #(contact-by-address % address) contacts))) - -(reg-sub :get-contacts-by-address - :<- [:get-contacts] - (fn [contacts] - (reduce (fn [acc [_ {:keys [address] :as contact}]] - (if address - (assoc acc address contact) - acc)) - {} - contacts))) diff --git a/src/status_im/ui/screens/currency_settings/events.cljs b/src/status_im/ui/screens/currency_settings/events.cljs deleted file mode 100644 index 2d5cbe1617f..00000000000 --- a/src/status_im/ui/screens/currency_settings/events.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.ui.screens.currency-settings.events - (:require [status-im.ui.screens.accounts.models :as accounts.models] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.ui.screens.wallet.events :as wallet.events])) - -(handlers/register-handler-fx - :wallet.settings/set-currency - (fn [{:keys [db] :as cofx} [_ currency]] - (let [settings (get-in db [:account/account :settings]) - new-settings (assoc-in settings [:wallet :currency] currency)] - (handlers-macro/merge-fx cofx - (accounts.models/update-settings new-settings) - (wallet.events/update-wallet))))) diff --git a/src/status_im/ui/screens/currency_settings/styles.cljs b/src/status_im/ui/screens/currency_settings/styles.cljs deleted file mode 100644 index b83cdc89677..00000000000 --- a/src/status_im/ui/screens/currency_settings/styles.cljs +++ /dev/null @@ -1,23 +0,0 @@ -(ns status-im.ui.screens.currency-settings.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def wrapper - {:flex 1 - :background-color :white}) - -(defstyle currency-item - {:flex-direction :row - :justify-content :space-between - :background-color :white - :align-items :center - :padding-horizontal 16 - :ios {:height 64} - :android {:height 56}}) - -(defstyle currency-name-text - {:color colors/black - :ios {:font-size 17 - :letter-spacing -0.2 - :line-height 20} - :android {:font-size 16}}) diff --git a/src/status_im/ui/screens/currency_settings/subs.cljs b/src/status_im/ui/screens/currency_settings/subs.cljs deleted file mode 100644 index 6340677a34d..00000000000 --- a/src/status_im/ui/screens/currency_settings/subs.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns status-im.ui.screens.currency-settings.subs - (:require [re-frame.core :as re-frame])) - -(defn get-user-currency [db] - (get-in db [:account/account :settings :wallet :currency] :usd)) - -(re-frame/reg-sub - :wallet.settings/currency - (fn [db] - (get-user-currency db))) diff --git a/src/status_im/ui/screens/currency_settings/views.cljs b/src/status_im/ui/screens/currency_settings/views.cljs deleted file mode 100644 index 7ead3caa0a5..00000000000 --- a/src/status_im/ui/screens/currency_settings/views.cljs +++ /dev/null @@ -1,38 +0,0 @@ -(ns status-im.ui.screens.currency-settings.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.profile.components.views :as profile.components] - [status-im.ui.screens.currency-settings.styles :as styles] - [status-im.constants :as constants])) - -(defn render-currency [current-currency-id] - (fn [{:keys [id code display-name] :as currency}] - (let [selected? (= id current-currency-id)] - [react/touchable-highlight - {:on-press #(re-frame/dispatch [:wallet.settings/set-currency id]) - :accessibility-label :currency-item} - [react/view styles/currency-item - [react/text {:style styles/currency-name-text} - (str display-name " (" code ")")] - (when selected? - [vector-icons/icon :icons/ok {:color :active}])]]))) - -(views/defview currency-settings [] - (views/letsubs [currency-id [:wallet.settings/currency]] - [react/view {:flex 1} - [status-bar/status-bar] - [toolbar/simple-toolbar - (i18n/label :t/main-currency)] - [react/view styles/wrapper - [list/flat-list {:data (->> constants/currencies - vals - (sort #(compare (:code %1) (:code %2)))) - :key-fn :code - :separator (profile.components/settings-item-separator) - :render-fn (render-currency currency-id)}]]])) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs deleted file mode 100644 index 53d8d413af3..00000000000 --- a/src/status_im/ui/screens/db.cljs +++ /dev/null @@ -1,285 +0,0 @@ -(ns status-im.ui.screens.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec] - [status-im.constants :as constants] - [status-im.utils.platform :as platform] - [status-im.utils.dimensions :as dimensions] - status-im.transport.db - status-im.ui.screens.accounts.db - status-im.ui.screens.contacts.db - status-im.ui.screens.qr-scanner.db - status-im.ui.screens.group.db - status-im.chat.specs - status-im.ui.screens.profile.db - status-im.ui.screens.network-settings.db - status-im.ui.screens.offline-messaging-settings.db - status-im.ui.screens.browser.db - status-im.ui.screens.add-new.db - status-im.ui.screens.add-new.new-public-chat.db)) - -;; initial state of app-db -(def app-db {:current-public-key nil - :status-module-initialized? (or platform/ios? js/goog.DEBUG platform/desktop?) - :keyboard-height 0 - :tab-bar-visible? true - :navigation-stack '() - :contacts/contacts {} - :qr-codes {} - :group/selected-contacts #{} - :chats {} - :current-chat-id nil - :selected-participants #{} - :discoveries {} - :discover-search-tags #{} - :discover-current-dapp {} - :tags [] - :sync-state :done - :app-state "active" - :wallet.transactions constants/default-wallet-transactions - :wallet-selected-asset {} - :prices {} - :peers-count 0 - :peers-summary [] - :notifications {} - :network constants/default-network - :networks/networks constants/default-networks - :inbox/wnodes constants/default-wnodes - :my-profile/editing? false - :transport/chats {} - :transport/message-envelopes {} - :chat/cooldowns 0 - :chat/cooldown-enabled? false - :chat/last-outgoing-message-sent-at 0 - :chat/spam-messages-frequency 0 - :tooltips {} - :desktop/desktop {:tab-view-id :home} - :dimensions/window (dimensions/window)}) - -;;;;GLOBAL - -;;public key of current logged in account -(spec/def ::current-public-key (spec/nilable string?)) -(spec/def ::was-modal? (spec/nilable boolean?)) -;;"http://localhost:8545" -(spec/def ::rpc-url (spec/nilable string?)) -;;object? doesn't work -(spec/def ::web3 (spec/nilable any?)) -(spec/def ::web3-node-version (spec/nilable string?)) -;;object? -(spec/def ::webview-bridge (spec/nilable any?)) -(spec/def ::status-module-initialized? (spec/nilable boolean?)) -(spec/def ::status-node-started? (spec/nilable boolean?)) -;;height of native keyboard if shown -(spec/def ::keyboard-height (spec/nilable number?)) -(spec/def ::keyboard-max-height (spec/nilable number?)) -(spec/def ::tab-bar-visible? (spec/nilable boolean?)) -;;:online - presence of internet connection in the phone -(spec/def ::network-status (spec/nilable keyword?)) - -(spec/def ::mailserver-status (spec/nilable keyword?)) - -(spec/def ::app-state string?) - -;;;;NODE - -(spec/def ::sync-listening-started (spec/nilable boolean?)) -(spec/def ::sync-state (spec/nilable #{:pending :in-progress :synced :done :offline})) -(spec/def ::sync-data (spec/nilable map?)) - -;;;;NAVIGATION - -;;current view -(spec/def :navigation/view-id (spec/nilable keyword?)) -;;modal view id -(spec/def :navigation/modal (spec/nilable keyword?)) -;;stack of view's ids (keywords) -(spec/def :navigation/navigation-stack (spec/nilable seq?)) -(spec/def :navigation/prev-tab-view-id (spec/nilable keyword?)) -(spec/def :navigation/prev-view-id (spec/nilable keyword?)) -;; navigation screen params -(spec/def :navigation.screen-params/network-details (allowed-keys :req [:networks/selected-network])) -(spec/def :navigation.screen-params/browser (spec/nilable string?)) -(spec/def :navigation.screen-params.profile-qr-viewer/contact (spec/nilable map?)) -(spec/def :navigation.screen-params.profile-qr-viewer/source (spec/nilable keyword?)) -(spec/def :navigation.screen-params.profile-qr-viewer/value (spec/nilable string?)) -(spec/def :navigation.screen-params/profile-qr-viewer (allowed-keys :opt-un [:navigation.screen-params.profile-qr-viewer/contact - :navigation.screen-params.profile-qr-viewer/source - :navigation.screen-params.profile-qr-viewer/value])) -(spec/def :navigation.screen-params.qr-scanner/current-qr-context (spec/nilable any?)) -(spec/def :navigation.screen-params/qr-scanner (allowed-keys :opt-un [:navigation.screen-params.qr-scanner/current-qr-context])) -(spec/def :navigation.screen-params.group-contacts/show-search? (spec/nilable any?)) -(spec/def :navigation.screen-params/group-contacts (allowed-keys :opt [:group/contact-group-id] - :opt-un [:navigation.screen-params.group-contacts/show-search?])) -(spec/def :navigation.screen-params.edit-contact-group/group (spec/nilable any?)) -(spec/def :navigation.screen-params.edit-contact-group/group-type (spec/nilable any?)) -(spec/def :navigation.screen-params/edit-contact-group (allowed-keys :opt-un [:navigation.screen-params.edit-contact-group/group - :navigation.screen-params.edit-contact-group/group-type])) -(spec/def :navigation.screen-params.dapp-description/dapp :new/open-dapp) -(spec/def :navigation.screen-params/dapp-description map?) - -(spec/def :navigation.screen-params/collectibles-list map?) - -(spec/def :navigation/screen-params (spec/nilable (allowed-keys :opt-un [:navigation.screen-params/network-details - :navigation.screen-params/browser - :navigation.screen-params/profile-qr-viewer - :navigation.screen-params/qr-scanner - :navigation.screen-params/group-contacts - :navigation.screen-params/edit-contact-group - :navigation.screen-params/dapp-description - :navigation.screen-params/collectibles-list]))) - -(spec/def :desktop/desktop (spec/nilable any?)) -(spec/def ::tooltips (spec/nilable any?)) - -;;;;NETWORK - -(spec/def ::network (spec/nilable string?)) -(spec/def ::chain (spec/nilable string?)) -(spec/def ::peers-count (spec/nilable integer?)) -(spec/def ::peers-summary (spec/nilable vector?)) -(spec/def :inbox/fetching? (spec/nilable boolean?)) -(spec/def :inbox/current-id (spec/nilable string?)) - -(spec/def ::collectible (spec/nilable map?)) -(spec/def ::collectibles (spec/nilable map?)) - -;;;;NODE - -(spec/def :node/after-start (spec/nilable vector?)) -(spec/def :node/after-stop (spec/nilable vector?)) - -(spec/def ::message-envelopes (spec/nilable map?)) - -;;;;UUID - -(spec/def ::device-UUID (spec/nilable string?)) - -;;;;UNIVERSAL LINKS - -(spec/def :universal-links/url (spec/nilable string?)) - -;; DIMENSIONS -(spec/def :dimensions/window map?) - -(spec/def ::db (allowed-keys - :opt - [:contacts/contacts - :contacts/dapps - :contacts/new-identity - :contacts/new-public-key-error - :contacts/identity - :contacts/ui-props - :contacts/list-ui-props - :contacts/click-handler - :contacts/click-action - :contacts/click-params - :commands/stored-command - :group/selected-contacts - :accounts/accounts - :accounts/create - :accounts/recover - :accounts/login - :account/account - :my-profile/profile - :my-profile/default-name - :my-profile/editing? - :my-profile/advanced? - :my-profile/seed - :group-chat-profile/profile - :group-chat-profile/editing? - :networks/selected-network - :networks/networks - :networks/manage - :mailservers/manage - :bootnodes/manage - :node/after-start - :node/after-stop - :inbox/wnodes - :inbox/last-received - :inbox/current-id - :inbox/fetching? - :universal-links/url - :browser/browsers - :browser/options - :new/open-dapp - :navigation/screen-params - :chat/cooldowns - :chat/cooldown-enabled? - :chat/last-outgoing-message-sent-at - :chat/spam-messages-frequency - :transport/message-envelopes - :transport/chats - :transport/discovery-filter - :desktop/desktop - :dimensions/window - :dapps/permissions] - :opt-un - [::current-public-key - ::modal - ::was-modal? - ::rpc-url - ::tooltips - ::web3 - ::web3-node-version - ::webview-bridge - ::status-module-initialized? - ::status-node-started? - ::keyboard-height - ::keyboard-max-height - ::tab-bar-visible? - ::network-status - ::mailserver-status - ::peers-count - ::peers-summary - ::sync-listening-started - ::sync-state - ::sync-data - ::network - ::chain - ::app-state - :navigation/view-id - :navigation/navigation-stack - :navigation/prev-tab-view-id - :navigation/prev-view-id - :qr/qr-codes - :qr/qr-modal - :qr/current-qr-context - :chat/chats - :chat/current-chat-id - :chat/chat-id - :chat/new-chat - :chat/new-chat-name - :chat/chat-animations - :chat/chat-ui-props - :chat/chat-list-ui-props - :chat/layout-height - :chat/message-data - :chat/message-status - :chat/selected-participants - :chat/chat-loaded-callbacks - :chat/public-group-topic - :chat/public-group-topic-error - :chat/messages - :chat/message-groups - :chat/message-statuses - :chat/not-loaded-message-ids - :chat/last-clock-value - :chat/loaded-chats - :chat/bot-db - :chat/id->command - :chat/access-scope->command-id - :discoveries/discoveries - :discoveries/discover-search-tags - :discoveries/discover-current-dapp - :discoveries/tags - :discoveries/current-tag - :discoveries/request-discoveries-timer - :wallet/wallet - :wallet/wallet.transactions - :wallet/wallet-selected-asset - :prices/prices - :prices/prices-loading? - :notifications/notifications - ::device-UUID - ::collectible - ::collectibles])) diff --git a/src/status_im/ui/screens/desktop/main/add_new/styles.cljs b/src/status_im/ui/screens/desktop/main/add_new/styles.cljs deleted file mode 100644 index 0e46522aaf4..00000000000 --- a/src/status_im/ui/screens/desktop/main/add_new/styles.cljs +++ /dev/null @@ -1,75 +0,0 @@ -(ns status-im.ui.screens.desktop.main.add-new.styles - (:require [status-im.ui.components.colors :as colors])) - -(def new-contact-view - {:flex 1 - :background-color colors/white - :margin-left 24 - :margin-right 37}) - -(def new-contact-title - {:height 64 - :align-items :flex-start - :justify-content :center}) - -(def new-contact-title-text - {:font-size 20 - :color :black - :font-weight "600"}) - -(def new-contact-subtitle - {:font-size 14}) - -(def new-contact-separator - {:height 1 - :background-color colors/gray-light}) - -(def add-contact-edit-view - {:height 45 - :margin-bottom 32 - :background-color colors/white - :border-radius 12 - :flex-direction :row - :margin-top 16}) - -(def add-contact-input - {:font-size 14 - :background-color colors/gray-lighter - :margin-right 12 - :border-radius 8}) - -(defn add-contact-button [error?] - {:width 140 - :height 45 - :border-radius 8 - :background-color (if error? colors/gray-lighter colors/blue) - :align-items :center - :justify-content :center}) - -(defn add-contact-button-text [error?] - {:font-size 16 - :color (if error? colors/gray colors/white)}) - -(def suggested-contact-view - {:flex-direction "row" - :align-items :center - :margin-bottom 16}) - -(def suggested-contacts - {:margin-top 12}) - -(def suggested-contact-image - {:width 46 - :height 46 - :border-radius 46 - :margin-right 16}) - -(def suggested-topic-image - (merge suggested-contact-image - {:background-color colors/blue - :align-items :center - :justify-content :center})) - -(def suggested-topic-text - {:font-size 25.6 - :color colors/white}) diff --git a/src/status_im/ui/screens/desktop/main/add_new/views.cljs b/src/status_im/ui/screens/desktop/main/add_new/views.cljs deleted file mode 100644 index 6cdb7266305..00000000000 --- a/src/status_im/ui/screens/desktop/main/add_new/views.cljs +++ /dev/null @@ -1,97 +0,0 @@ -(ns status-im.ui.screens.desktop.main.add-new.views - (:require-macros [status-im.utils.views :as views]) - (:require [status-im.ui.components.icons.vector-icons :as icons] - [status-im.ui.screens.add-new.new-public-chat.view :as public-chat] - [status-im.ui.components.list.views :as list] - [clojure.string :as string] - [status-im.i18n :as i18n] - [re-frame.core :as re-frame] - [status-im.ui.screens.desktop.main.add-new.styles :as styles] - [status-im.ui.screens.add-new.new-public-chat.view :refer [default-public-chats]] - [status-im.ui.screens.add-new.new-public-chat.db :as public-chat-db] - [taoensso.timbre :as log] - [status-im.ui.components.react :as react])) - -(views/defview new-contact [] - (views/letsubs [new-contact-identity [:get :contacts/new-identity] - contacts [:all-added-people-contacts] - chat-error [:new-contact-error-message] - topic [:get :public-group-topic] - topic-error [:new-topic-error-message] - account [:get-current-account] - topic-input-ref (atom nil)] - [react/scroll-view - [react/view {:style styles/new-contact-view} - ^{:key "newcontact"} - [react/view {:style styles/new-contact-title} - [react/text {:style styles/new-contact-title-text} - (i18n/label :new-chat)]] - [react/text {:style styles/new-contact-subtitle} (i18n/label :contact-code)] - [react/view {:style styles/new-contact-separator}] - [react/view {:style styles/add-contact-edit-view} - [react/text-input {:placeholder "0x..." - :flex 1 - :style styles/add-contact-input - :on-change (fn [e] - (let [native-event (.-nativeEvent e) - text (.-text native-event)] - (re-frame/dispatch [:set :contacts/new-identity text])))}] - [react/touchable-highlight {:disabled chat-error :on-press #(when-not chat-error (re-frame/dispatch [:add-contact-handler new-contact-identity]))} - [react/view - {:style (styles/add-contact-button chat-error)} - [react/text - {:style (styles/add-contact-button-text chat-error)} - (i18n/label :start-chat)]]]] - ^{:key "choosecontact"} - [react/view - (when (seq contacts) [react/text {:style styles/new-contact-subtitle} (i18n/label :or-choose-a-contact)]) - [react/view {:style styles/suggested-contacts} - (doall - (for [c contacts] - ^{:key (:whisper-identity c)} - [react/touchable-highlight {:on-press #(do - (re-frame/dispatch [:set :contacts/new-identity (:whisper-identity c)]) - (re-frame/dispatch [:add-contact-handler (:whisper-identity c)]))} - [react/view {:style styles/suggested-contact-view} - [react/image {:style styles/suggested-contact-image - :source {:uri (:photo-path c)}}] - [react/text {:style styles/new-contact-subtitle} (:name c)]]]))]] - ^{:key "publicchat"} - [react/view {:style styles/new-contact-title} - [react/text {:style styles/new-contact-title-text} - (i18n/label :new-public-group-chat)]] - [react/text {:style styles/new-contact-subtitle} (i18n/label :public-group-topic)] - [react/view {:style styles/new-contact-separator}] - [react/view {:style styles/add-contact-edit-view} - [react/view {:style {:flex 1}} - [react/text-input {:flex 1 - :ref #(when (and (nil? @topic-input-ref) %) - (.setNativeProps % (js-obj "text" "#")) - (reset! topic-input-ref %)) - :style styles/add-contact-input - :on-change (fn [e] - (let [native-event (.-nativeEvent e) - text (.-text native-event) - [_ before after] (first (re-seq #"(.*)\#(.*)" text))] - (.setNativeProps @topic-input-ref (js-obj "text" (str "#" before after))) - (re-frame/dispatch [:set :public-group-topic (subs text 1)])))}]] - [react/touchable-highlight {:disabled topic-error - :on-press #(when-not topic-error - (do - (re-frame/dispatch [:set :public-group-topic nil]) - (re-frame/dispatch [:create-new-public-chat topic])))} - [react/view {:style (styles/add-contact-button topic-error)} - [react/text {:style (styles/add-contact-button-text topic-error)} - (i18n/label :new-public-group-chat)]]]] - [react/text {:style styles/new-contact-subtitle} (i18n/label :selected-for-you)] - [react/view {:style styles/suggested-contacts} - (doall - (for [topic public-chat/default-public-chats] - ^{:key topic} - [react/touchable-highlight {:on-press #(do - (re-frame/dispatch [:set :public-group-topic nil]) - (re-frame/dispatch [:create-new-public-chat topic]))} - [react/view {:style styles/suggested-contact-view} - [react/view {:style styles/suggested-topic-image} - [react/text {:style styles/suggested-topic-text} (string/capitalize (first topic))]] - [react/text {:style styles/new-contact-subtitle} topic]]]))]]])) diff --git a/src/status_im/ui/screens/desktop/main/chat/styles.cljs b/src/status_im/ui/screens/desktop/main/chat/styles.cljs deleted file mode 100644 index 2f4ca138cef..00000000000 --- a/src/status_im/ui/screens/desktop/main/chat/styles.cljs +++ /dev/null @@ -1,172 +0,0 @@ -(ns status-im.ui.screens.desktop.main.chat.styles - (:require [status-im.ui.components.colors :as colors])) - -(defn message-box [{:keys [outgoing] :as message}] - (let [align (if outgoing :flex-end :flex-start) - color (if outgoing colors/hawkes-blue colors/white)] - {:align-self align - :background-color color - :border-radius 8 - :padding-left 12 - :padding-right 12 - :padding-top 10 - :padding-bottom 10 - :max-width 340})) - -(defn message-row [{:keys [outgoing first-in-group?] :as message}] - (let [padding-horizontal (if outgoing :padding-right :padding-left) - padding-top-value (if first-in-group? 16 8)] - {padding-horizontal 24 - :padding-top padding-top-value})) - -(def message-row-column - {:flex-direction :column}) - -(defn message-timestamp-placeholder [] - {:color colors/transparent - :font-size 10 - :align-self :flex-end - :opacity 0.5 - :text-align :right - :text-align-vertical :center - :width 60}) - -(defn message-timestamp [] - (merge (message-timestamp-placeholder) - {:color colors/gray - :position :absolute - :bottom 0 - :right -5})) - -(def author - {:color colors/gray - :font-size 12 - :margin-left 48 - :margin-bottom 4}) - -(def chat-box - {:height 68 - :background-color :white - :border-radius 12 - :margin-horizontal 24 - :padding-vertical 15}) - -(def chat-box-inner - {:flex-direction :row - :flex 1}) - -(def chat-text-input - {:flex 1}) - -(def messages-view - {:flex 1 - :background-color colors/gray-lighter}) - -(def img-container - {:height 56 - :justify-content :center}) - -(def messages-scrollview-inner - {:padding-vertical 46}) - -(def photo-style - {:border-radius 20 - :width 40 - :height 40 - :margin-right 8}) - -(def photo-style-toolbar - {:border-radius 32 - :width 32 - :height 32 - :margin-right 8}) - -(defn topic-image [color] - (merge photo-style-toolbar - {:background-color color - :align-items :center - :justify-content :center})) - -(def topic-text - {:font-size 18 - :color colors/white}) - -(def toolbar-chat-view - {:margin-left 11 - :justify-content :center}) - -(def toolbar-chat-name - {:font-size 16 - :color :black - :font-weight "300"}) - -(def add-contact - {:background-color :white - :border-radius 6 - :margin-top 3 - :padding-top 1}) - -(def add-contact-text - {:font-size 14 - :color colors/blue}) - -(def message-text - {:font-size 14}) - -(def message-wrapper - {:flex-direction :row - :flex-wrap :wrap}) - -(def not-first-in-group-wrapper - {:flex-direction :row}) - -(def send-icon - {:margin-left 16 - :width 30 - :height 30 - :border-radius 15 - :background-color colors/gray-lighter - :align-items :center - :justify-content :center - :transform [{:rotate "90deg"}]}) - -(def chat-view - {:flex 1 - :background-color :white}) - -(defn contact-card-text [color] - {:color color - :font-size 14}) - -(def contact-card-gray-text - {:color colors/gray - :font-size 14}) - -(def chat-profile-body - {:margin 32}) - -(def chat-profile-row - {:flex-direction :row - :align-items :center - :margin-bottom 10}) - -(def chat-profile-icon-container - {:background-color (colors/alpha colors/blue 0.1) - :justify-content :center - :align-items :center - :border-radius 15 - ;; not sure idiomatic reagent for multiple style values - ;; for margin - :width 22 - :height 22 - :margin-right 10}) - -(defn chat-profile-icon [color] - {:tint-color color - :width 15 - :height 15}) - -(def chat-profile-contact-code - {:color colors/gray - :margin-top 10 - :margin-bottom 5}) diff --git a/src/status_im/ui/screens/desktop/main/chat/views.cljs b/src/status_im/ui/screens/desktop/main/chat/views.cljs deleted file mode 100644 index 0f23af32024..00000000000 --- a/src/status_im/ui/screens/desktop/main/chat/views.cljs +++ /dev/null @@ -1,218 +0,0 @@ -(ns status-im.ui.screens.desktop.main.chat.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.ui.components.icons.vector-icons :as icons] - [clojure.string :as string] - [status-im.chat.styles.message.message :as message.style] - [status-im.utils.gfycat.core :as gfycat.core] - [taoensso.timbre :as log] - [status-im.utils.gfycat.core :as gfycat] - [status-im.constants :as constants] - [status-im.utils.identicon :as identicon] - [status-im.utils.datetime :as time] - [status-im.ui.components.react :as react] - [status-im.ui.components.colors :as colors] - [status-im.chat.views.message.datemark :as message.datemark] - [status-im.ui.screens.desktop.main.tabs.profile.views :as profile.views] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.screens.desktop.main.chat.styles :as styles] - [status-im.i18n :as i18n])) - -(views/defview toolbar-chat-view [] - (views/letsubs [{:keys [chat-id public-key public? group-chat color]} [:get-current-chat] - {:keys [pending? whisper-identity photo-path]} [:get-current-chat-contact] - current-chat-name [:get-current-chat-name]] - [react/view {:style styles/toolbar-chat-view} - [react/view {:style {:flex-direction :row - :align-items :center}} - - [react/view {:style styles/img-container} - (if public? - [react/view {:style (styles/topic-image color)} - [react/text {:style styles/topic-text} - (string/capitalize (first current-chat-name))]] - [react/image {:style styles/photo-style-toolbar - :source {:uri photo-path}}])] - - [react/view - [react/text {:style styles/toolbar-chat-name} current-chat-name] - (when pending? - [react/touchable-highlight - {:on-press #(re-frame/dispatch [:add-contact whisper-identity])} - [react/view {:style styles/add-contact} - [react/text {:style styles/add-contact-text} - (i18n/label :t/add-to-contacts)]]])] - (when (and (not group-chat) (not public?)) - [react/text {:style {:position :absolute - :right 20} - :on-press #(re-frame/dispatch [:navigate-to :chat-profile])} - (i18n/label :t/view-profile)])]])) - -(views/defview message-author-name [{:keys [outgoing from] :as message}] - (views/letsubs [current-account [:get-current-account] - incoming-name [:get-contact-name-by-identity from]] - (let [name (or incoming-name (gfycat/generate-gfy from))] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:show-contact-dialog from name (boolean incoming-name)])} - [react/text {:style styles/author} name]]))) - -(views/defview member-photo [from] - [react/view - [react/image {:source {:uri (identicon/identicon from)} - :style styles/photo-style}]]) - -(views/defview my-photo [from] - (views/letsubs [account [:get-current-account]] - (let [{:keys [photo-path]} account] - [react/view - [react/image {:source {:uri (if (string/blank? photo-path) - (identicon/identicon from) - photo-path)} - :style styles/photo-style}]]))) - -(views/defview message-with-timestamp [text {:keys [timestamp] :as message} style] - [react/view {:style style} - [react/view {:style styles/message-wrapper} - [react/text {:style styles/message-text - :selectable true} - text] - [react/text {:style (styles/message-timestamp-placeholder)} - (time/timestamp->time timestamp)] - [react/text {:style (styles/message-timestamp)} - (time/timestamp->time timestamp)]]]) - -(views/defview text-only-message [text message] - [react/view {:style (styles/message-row message)} - [message-with-timestamp text message (styles/message-box message)]]) - -(views/defview photo-placeholder [] - [react/view {:style styles/photo-style}]) - -(views/defview message-with-name-and-avatar [text {:keys [from first-in-group? last-in-group?] :as message}] - [react/view {:style (styles/message-row message)} - [react/view {:style styles/message-row-column} - (when first-in-group? - [message-author-name message]) - [react/view {:style styles/not-first-in-group-wrapper} - (if last-in-group? - [member-photo from] - [photo-placeholder]) - [message-with-timestamp text message (styles/message-box message)]]]]) - -(defn message [text me? {:keys [message-id chat-id message-status user-statuses from - current-public-key content-type group-chat outgoing type value] :as message}] - (when (nil? message-id) - (log/debug "nil?" message)) - (if (= type :datemark) - ^{:key (str "datemark" message-id)} - [message.datemark/chat-datemark value] - (when (= content-type constants/text-content-type) - (reagent.core/create-class - {:component-did-mount - #(when (and message-id - chat-id - (not outgoing) - (not= :seen message-status) - (not= :seen (keyword (get-in user-statuses [current-public-key :status])))) - (re-frame/dispatch [:send-seen! {:chat-id chat-id - :from from - :message-id message-id}])) - :reagent-render - (fn [] - ^{:key (str "message" message-id)} - (if (and group-chat (not outgoing)) - [message-with-name-and-avatar text message] - [text-only-message text message]))})))) - -(views/defview messages-view [{:keys [chat-id group-chat]}] - (views/letsubs [chat-id* (atom nil) - scroll-ref (atom nil) - scroll-timer (atom nil) - scroll-height (atom nil)] - (let [_ (when (or (not @chat-id*) (not= @chat-id* chat-id)) - (reset! chat-id* chat-id) - (js/setTimeout #(when scroll-ref (.scrollToEnd @scroll-ref)) 400)) - messages (re-frame/subscribe [:get-current-chat-messages-stream]) - current-public-key (re-frame/subscribe [:get-current-public-key])] - [react/view {:style styles/messages-view} - [react/scroll-view {:scrollEventThrottle 16 - :on-scroll (fn [e] - (let [ne (.-nativeEvent e) - y (.-y (.-contentOffset ne))] - (when (zero? y) - (when @scroll-timer (js/clearTimeout @scroll-timer)) - (reset! scroll-timer (js/setTimeout #(re-frame/dispatch [:load-more-messages]) 300))) - (reset! scroll-height (+ y (.-height (.-layoutMeasurement ne)))))) - :on-content-size-change #(.scrollToEnd @scroll-ref) - :ref #(reset! scroll-ref %)} - [react/view {:style styles/messages-scrollview-inner} - (doall - (for [[index {:keys [from content message-id type value] :as message-obj}] (map-indexed vector (reverse @messages))] - ^{:key (or message-id (str type value))} - [message content (= from @current-public-key) (assoc message-obj :group-chat group-chat)]))]]]))) - -(views/defview chat-text-input [] - (views/letsubs [inp-ref (atom nil)] - [react/view {:style styles/chat-box} - [react/view {:style styles/chat-box-inner} - [react/view {:style {:flex 1}} - [react/text-input {:placeholder (i18n/label :t/type-a-message) - :auto-focus true - :multiline true - :blur-on-submit true - :style styles/chat-text-input - :ref #(reset! inp-ref %) - :on-key-press (fn [e] - (let [native-event (.-nativeEvent e) - key (.-key native-event) - modifiers (js->clj (.-modifiers native-event)) - should-send (and (= key "Enter") (not (contains? (set modifiers) "shift")))] - (when should-send - (.clear @inp-ref) - (.focus @inp-ref) - (re-frame/dispatch [:send-current-message])))) - :on-change (fn [e] - (let [native-event (.-nativeEvent e) - text (.-text native-event)] - (re-frame/dispatch [:set-chat-input-text text])))}]] - [react/touchable-highlight {:on-press (fn [] - (.clear @inp-ref) - (.focus @inp-ref) - (re-frame/dispatch [:send-current-message]))} - [react/view {:style styles/send-icon} - [icons/icon :icons/arrow-left]]]]])) - -(views/defview chat-view [] - (views/letsubs [current-chat [:get-current-chat]] - [react/view {:style styles/chat-view} - [toolbar-chat-view] - [messages-view current-chat] - [chat-text-input]])) - -(views/defview chat-profile [] - (views/letsubs [{:keys [pending? whisper-identity public-key] :as contact} [:get-current-chat-contact]] - [react/view {:style styles/chat-profile-body} - [profile.views/profile-badge contact] - ;; for private chat, public key will be chat-id - [react/view - (if pending? - [react/touchable-highlight {:on-press #(re-frame/dispatch [:add-contact whisper-identity])} - [react/view {:style styles/chat-profile-row} - [react/view {:style styles/chat-profile-icon-container - :accessibility-label :add-contact-link} - [vector-icons/icon :icons/add {:style (styles/chat-profile-icon colors/blue)}]] - [react/text {:style (styles/contact-card-text colors/blue)} (i18n/label :t/add-to-contacts)]]] - [react/view {:style styles/chat-profile-row} - [react/view {:style styles/chat-profile-icon-container - :accessibility-label :add-contact-link} - [vector-icons/icon :icons/add {:style (styles/chat-profile-icon colors/gray)}]] - [react/text {:style (styles/contact-card-text colors/gray)} (i18n/label :t/in-contacts)]]) - [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to-chat public-key])} - [react/view {:style styles/chat-profile-row} - [react/view {:style styles/chat-profile-icon-container - :accessibility-label :send-message-link} - [vector-icons/icon :icons/chats {:style (styles/chat-profile-icon colors/blue)}]] - [react/text {:style (styles/contact-card-text colors/blue)} - (i18n/label :t/send-message)]]] - [react/text {:style styles/chat-profile-contact-code} (i18n/label :t/contact-code)] - [react/text {:style {:font-size 14} - :selectable true} public-key]]])) diff --git a/src/status_im/ui/screens/desktop/main/styles.cljs b/src/status_im/ui/screens/desktop/main/styles.cljs deleted file mode 100644 index c37c33dc9e9..00000000000 --- a/src/status_im/ui/screens/desktop/main/styles.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.ui.screens.desktop.main.styles - (:require [status-im.ui.components.colors :as colors])) - -(def main-views - {:flex 1 - :flex-direction :row}) - -(def left-sidebar - {:width 340 - :background-color colors/white}) - -(def pane-separator - {:width 1 - :background-color colors/gray-light}) diff --git a/src/status_im/ui/screens/desktop/main/tabs/home/styles.cljs b/src/status_im/ui/screens/desktop/main/tabs/home/styles.cljs deleted file mode 100644 index 0eb5e1f8f9a..00000000000 --- a/src/status_im/ui/screens/desktop/main/tabs/home/styles.cljs +++ /dev/null @@ -1,87 +0,0 @@ -(ns status-im.ui.screens.desktop.main.tabs.home.styles - (:require [status-im.ui.components.colors :as colors])) - -(def chat-list-view - {:flex 1 - :background-color colors/white}) - -(defn chat-list-item [current?] - {:padding-horizontal 16 - :flex-direction :row - :flex 1 - :justify-content :space-between - :background-color (if current? colors/gray-lighter colors/white) - :align-items :center}) - -(def chat-list-header - {:flex-direction :row - :align-items :center - :padding 11}) - -(def img-container - {:height 78 - :justify-content :center}) - -(def chat-icon - {:width 46 - :height 46 - :border-radius 46 - :margin-right 16}) - -(def unread-messages-icon - {:position :absolute - :width 22 - :height 22 - :border-radius 22 - :left 28 - :top 10 - :justify-content :center - :align-items :center - :background-color colors/blue}) - -(defn unread-messages-text [large?] - {:color colors/white - :font-size (if large? 10 12)}) - -(def chat-list-separator - {:height 1 - :background-color colors/gray-light}) - -(def chat-name-box - {:flex-direction :row - :align-items :center}) - -(def chat-name-last-msg-box - {:flex 1 - :padding-vertical 16}) - -(defn chat-name [current?] - {:font-size 14 - :font-weight (if current? "600" :normal)}) - -(def chat-last-message - {:color colors/gray - :font-size 14}) - -(def timestamp - {:justify-content :flex-start - :align-items :flex-end - :padding-vertical 16}) - -(def add-new - {:background-color colors/blue - :width 34 - :height 34 - :border-radius 34 - :justify-content :center - :align-items :center}) - -(defn topic-image [color] - (merge chat-icon - {:background-color color - :align-items :center - :justify-content :center})) - -(def topic-text - {:font-size 25.6 - :color colors/white}) diff --git a/src/status_im/ui/screens/desktop/main/tabs/home/views.cljs b/src/status_im/ui/screens/desktop/main/tabs/home/views.cljs deleted file mode 100644 index eaf170c33e3..00000000000 --- a/src/status_im/ui/screens/desktop/main/tabs/home/views.cljs +++ /dev/null @@ -1,77 +0,0 @@ -(ns status-im.ui.screens.desktop.main.tabs.home.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.utils.gfycat.core :as gfycat] - [status-im.i18n :as i18n] - - [status-im.ui.screens.desktop.main.tabs.home.styles :as styles] - [clojure.string :as string] - [status-im.ui.screens.home.views.inner-item :as chat-item] - [taoensso.timbre :as log] - [status-im.ui.components.icons.vector-icons :as icons] - [status-im.ui.components.react :as react])) - -(views/defview unviewed-indicator [chat-id] - (let [unviewed-messages-count (re-frame/subscribe [:unviewed-messages-count chat-id])] - (when (pos? @unviewed-messages-count) - [react/view - [react/text {:font :medium} - @unviewed-messages-count]]))) - -(views/defview chat-list-item-inner-view [{:keys [chat-id name group-chat color public? public-key] :as chat-item}] - (letsubs [photo-path [:get-chat-photo chat-id] - unviewed-messages-count [:unviewed-messages-count chat-id] - chat-name [:get-chat-name chat-id] - current-chat-id [:get-current-chat-id] - last-message [:get-last-message chat-id]] - (let [name (or chat-name - (gfycat/generate-gfy public-key)) - [unviewed-messages-label large?] (if (< 99 unviewed-messages-count) - ["99+" true] - [unviewed-messages-count false])] - [react/view {:style (styles/chat-list-item (= current-chat-id chat-id))} - [react/view {:style styles/img-container} - (if public? - [react/view {:style (styles/topic-image color)} - [react/text {:style styles/topic-text} - (string/capitalize (second name))]] - [react/image {:style styles/chat-icon - :source {:uri photo-path}}]) - (when (pos? unviewed-messages-count) - [react/view {:style styles/unread-messages-icon} - [react/text {:style (styles/unread-messages-text large?)} unviewed-messages-label]])] - [react/view {:style styles/chat-name-last-msg-box} - [react/view {:style styles/chat-name-box} - (when (and group-chat (not public?)) - [icons/icon :icons/group-chat]) - (when public? - [icons/icon :icons/public-chat]) - [react/text {:ellipsize-mode :tail - :number-of-lines 1 - :style (styles/chat-name (= current-chat-id chat-id))} - name]] - [react/text {:ellipsize-mode :tail - :number-of-lines 1 - :style styles/chat-last-message} - (or (:content last-message) (i18n/label :no-messages-yet))]] - [react/view {:style styles/timestamp} - [chat-item/message-timestamp (:timestamp last-message)]]]))) - -(defn chat-list-item [[chat-id chat]] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to-chat chat-id])} - [chat-list-item-inner-view (assoc chat :chat-id chat-id)]]) - -(views/defview chat-list-view [] - (views/letsubs [home-items [:home-items]] - [react/view {:style styles/chat-list-view} - [react/view {:style styles/chat-list-header} - [react/view {:style {:flex 1}}] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to :new-contact])} - [react/view {:style styles/add-new} - [icons/icon :icons/add {:style {:tint-color :white}}]]]] - [react/view {:style styles/chat-list-separator}] - [react/scroll-view - [react/view - (for [[index chat] (map-indexed vector home-items)] - ^{:key (first chat)} - [chat-list-item chat])]]])) diff --git a/src/status_im/ui/screens/desktop/main/tabs/profile/styles.cljs b/src/status_im/ui/screens/desktop/main/tabs/profile/styles.cljs deleted file mode 100644 index 7bfffad2c81..00000000000 --- a/src/status_im/ui/screens/desktop/main/tabs/profile/styles.cljs +++ /dev/null @@ -1,135 +0,0 @@ -(ns status-im.ui.screens.desktop.main.tabs.profile.styles - (:require [status-im.ui.components.colors :as colors])) - -(def profile-view - {:align-items :center}) - -(def profile-badge - {:margin-top 34 - :align-items :center - :margin-bottom 16}) - -(def logout-row - {:justify-content :space-between - :flex-direction :row - :margin-horizontal 24 - :align-self :stretch - :margin-top 60}) - -(defn logout-row-text [color] - {:color color - :font-size 16}) - -(def profile-photo - {:border-radius 100 - :width 100 - :height 100}) - -(def profile-user-name - {:font-weight :bold - :font-size 18}) - -(def share-contact-code - {:flex-direction :row - :justify-content :space-between - :align-items :center - :height 45 - :width 240 - :margin-horizontal 50 - :border-radius 8 - :background-color (colors/alpha colors/blue 0.1)}) - -(def share-contact-code-text-container - {:margin-left 32 - :justify-content :center - :align-items :center}) - -(def share-contact-code-text - {:color colors/blue - :font-size 14}) - -(def share-contact-icon-container - {:margin-right 12 - :width 22 - :height 22 - :align-items :center - :justify-content :center}) - -(def qr-code-container - {:align-items :center - :padding-top 16 - :padding-bottom 46 - :padding-left 58 - :padding-right 58}) - -(def close-icon-container - {:flex 1 - :margin-top 22 - :margin-right 22 - :margin-bottom 16 - :flex-direction :row - :justify-content :flex-end}) - -(def close-icon - {:height 24 - :width 24 - :tint-color colors/gray-icon}) - -(def check-icon - {:height 16 - :width 16 - :margin-right 8 - :tint-color colors/tooltip-green-text}) - -(def qr-code-title - {:font-size 20 - :font-weight "600" - :margin-bottom 32}) - -(def qr-code - {:width 130 - :height 130 - :margin-bottom 16}) - -(def qr-code-text - {:font-size 16 - :text-align :center - :margin-bottom 16}) - -(def qr-code-copy - {:width 185 - :height 45 - :border-radius 8 - :background-color colors/blue - :justify-content :center - :align-items :center}) - -(def qr-code-copy-text - {:font-size 16 - :color colors/white}) - -(defn tooltip-container [opacity] - {:position :absolute - :align-items :center - :opacity opacity - :top -34}) - -(def tooltip-icon-text - {:flex-direction :row - :justify-content :space-between - :align-items :center - :height 24 - :border-radius 8 - :padding-left 10 - :padding-right 10 - :background-color colors/tooltip-green}) - -(def tooltip-triangle - {:width 0 - :height 0 - :border-top-width 9.1 - :border-left-width 9.1 - :border-right-width 9.1 - :border-left-color :transparent - :border-right-color :transparent - :border-top-color colors/tooltip-green}) diff --git a/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs b/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs deleted file mode 100644 index 822944053b9..00000000000 --- a/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs +++ /dev/null @@ -1,78 +0,0 @@ -(ns status-im.ui.screens.desktop.main.tabs.profile.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.ui.screens.profile.user.views :as profile] - [status-im.utils.build :as build] - [status-im.utils.utils :as utils] - [status-im.ui.components.colors :as colors] - [status-im.i18n :as i18n] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [taoensso.timbre :as log] - [clojure.string :as string] - [status-im.ui.components.qr-code-viewer.views :as qr-code-viewer] - [status-im.ui.screens.desktop.main.tabs.profile.styles :as styles] - [status-im.ui.screens.profile.user.views :as profile])) - -(defn profile-badge [{:keys [name photo-path]}] - [react/view styles/profile-badge - [react/image {:source {:uri photo-path} - :style styles/profile-photo}] - [react/text {:style styles/profile-user-name - :number-of-lines 1} - name]]) - -(views/defview copied-tooltip [opacity] - (views/letsubs [] - [react/view {:style (styles/tooltip-container opacity)} - [react/view {:style styles/tooltip-icon-text} - [vector-icons/icon :icons/check - {:style styles/check-icon}] - [react/text {:style {:font-size 14 :color colors/tooltip-green-text}} - (i18n/label :sharing-copied-to-clipboard)]] - [react/view {:style styles/tooltip-triangle}]])) - -(views/defview qr-code [] - (views/letsubs [{:keys [public-key]} [:get-current-account] - tooltip-opacity [:get-in [:tooltips :qr-copied]]] - [react/view - [react/view {:style styles/qr-code-container} - [react/text {:style styles/qr-code-title} - (string/replace (i18n/label :qr-code-public-key-hint) "\n" "")] - [react/view {:style styles/qr-code} - [qr-code-viewer/qr-code {:value public-key :size 130}]] - [react/view {:style {:align-items :center}} - [react/text {:style styles/qr-code-text} - public-key] - (when tooltip-opacity - [copied-tooltip tooltip-opacity])] - [react/touchable-highlight {:on-press #(do - (re-frame/dispatch [:copy-to-clipboard public-key]) - (re-frame/dispatch [:show-tooltip :qr-copied]))} - [react/view {:style styles/qr-code-copy} - [react/text {:style styles/qr-code-copy-text} - (i18n/label :copy-qr)]]]]])) - -(defn share-contact-code [] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to :qr-code])} - [react/view {:style styles/share-contact-code} - [react/view {:style styles/share-contact-code-text-container} - [react/text {:style styles/share-contact-code-text} - (i18n/label :share-contact-code)]] - [react/view {:style styles/share-contact-icon-container - :accessibility-label :share-my-contact-code-button} - [vector-icons/icon :icons/qr {:style {:tint-color colors/blue}}]]]]) - -(views/defview profile [user] - [react/view styles/profile-view - [profile-badge user] - [share-contact-code] - [react/view {:style styles/logout-row} - [react/touchable-highlight {:on-press #(re-frame/dispatch [:logout])} - [react/text {:style (styles/logout-row-text colors/red)} (i18n/label :t/logout)]] - [react/view [react/text {:style (styles/logout-row-text colors/gray)} "V" build/version " (" build/commit-sha ")"]]]]) - -(views/defview profile-data [] - (views/letsubs - [user [:get-current-account]] - [profile user])) diff --git a/src/status_im/ui/screens/desktop/main/views.cljs b/src/status_im/ui/screens/desktop/main/views.cljs deleted file mode 100644 index 629529229d0..00000000000 --- a/src/status_im/ui/screens/desktop/main/views.cljs +++ /dev/null @@ -1,43 +0,0 @@ -(ns status-im.ui.screens.desktop.main.views - (:require-macros [status-im.utils.views :as views]) - (:require [status-im.ui.screens.desktop.main.tabs.profile.views :as profile.views] - [status-im.ui.screens.desktop.main.tabs.home.views :as home.views] - [status-im.ui.screens.desktop.main.styles :as styles] - [status-im.ui.screens.desktop.main.chat.views :as chat.views] - [status-im.ui.screens.desktop.main.add-new.views :as add-new.views] - [status-im.ui.components.desktop.tabs :as tabs] - [status-im.ui.components.react :as react])) - -(views/defview status-view [] - [react/view {:style {:flex 1 :background-color "#eef2f5" :align-items :center :justify-content :center}} - [react/text {:style {:font-size 18 :color "#939ba1"}} - "Status.im"]]) - -(views/defview tab-views [] - (views/letsubs [tab [:get-in [:desktop/desktop :tab-view-id]]] - (let [component (case tab - :profile profile.views/profile-data - :home home.views/chat-list-view - react/view)] - [react/view {:style {:flex 1}} - [component]]))) - -(views/defview main-view [] - (views/letsubs [view-id [:get :view-id]] - (let [component (case view-id - :chat chat.views/chat-view - :new-contact add-new.views/new-contact - :qr-code profile.views/qr-code - :chat-profile chat.views/chat-profile - status-view)] - [react/view {:style {:flex 1}} - [component]]))) - -(views/defview main-views [] - [react/view {:style styles/main-views} - [react/view {:style styles/left-sidebar} - [react/view {:style {:flex 1}} - [tab-views]] - [tabs/main-tabs]] - [react/view {:style styles/pane-separator}] - [main-view]]) diff --git a/src/status_im/ui/screens/desktop/views.cljs b/src/status_im/ui/screens/desktop/views.cljs deleted file mode 100644 index eee7098263b..00000000000 --- a/src/status_im/ui/screens/desktop/views.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.ui.screens.desktop.views - (:require-macros [status-im.utils.views :as views]) - (:require [status-im.ui.screens.desktop.main.views :as main.views] - [status-im.ui.components.react :as react] - [status-im.ui.screens.intro.views :as intro.views] - [status-im.ui.screens.accounts.create.views :as create.views] - [status-im.ui.screens.accounts.login.views :as login.views] - [status-im.ui.screens.accounts.recover.views :as recover.views] - [status-im.ui.screens.accounts.views :as accounts.views])) - -(enable-console-print!) - -(views/defview main [] - (views/letsubs [view-id [:get :view-id]] - (let [component (case view-id - :intro intro.views/intro - :accounts accounts.views/accounts - :recover recover.views/recover - :create-account create.views/create-account - (:new-contact :chat :home :qr-code :chat-profile) main.views/main-views - :login login.views/login - react/view)] - [react/view {:style {:flex 1}} - [component]]))) diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs deleted file mode 100644 index 34f0c132ecb..00000000000 --- a/src/status_im/ui/screens/events.cljs +++ /dev/null @@ -1,457 +0,0 @@ -(ns status-im.ui.screens.events - (:require status-im.chat.events - status-im.network.events - [status-im.transport.handlers :as transport.handlers] - status-im.protocol.handlers - [status-im.ui.screens.accounts.models :as accounts.models] - status-im.ui.screens.accounts.login.events - status-im.ui.screens.accounts.recover.events - [status-im.ui.screens.contacts.events :as contacts] - status-im.ui.screens.group.chat-settings.events - status-im.ui.screens.group.events - [status-im.ui.screens.navigation :as navigation] - [status-im.utils.universal-links.core :as universal-links] - [status-im.utils.dimensions :as dimensions] - status-im.utils.universal-links.events - status-im.ui.screens.add-new.new-chat.navigation - status-im.ui.screens.network-settings.events - status-im.ui.screens.profile.events - status-im.ui.screens.qr-scanner.events - status-im.ui.screens.wallet.events - status-im.ui.screens.wallet.collectibles.events - status-im.ui.screens.wallet.send.events - status-im.ui.screens.wallet.settings.events - status-im.ui.screens.wallet.transactions.events - status-im.ui.screens.wallet.choose-recipient.events - status-im.ui.screens.wallet.collectibles.cryptokitties.events - status-im.ui.screens.wallet.collectibles.cryptostrikers.events - status-im.ui.screens.wallet.collectibles.etheremon.events - status-im.ui.screens.browser.events - status-im.ui.screens.offline-messaging-settings.events - status-im.ui.screens.bootnodes-settings.events - status-im.ui.screens.currency-settings.events - status-im.utils.keychain.events - [re-frame.core :as re-frame] - [status-im.native-module.core :as status] - [status-im.ui.components.permissions :as permissions] - [status-im.constants :as constants] - [status-im.data-store.core :as data-store] - [status-im.data-store.realm.core :as realm] - [status-im.utils.keychain.core :as keychain] - [status-im.i18n :as i18n] - [status-im.js-dependencies :as dependencies] - [status-im.ui.components.react :as react] - [status-im.transport.core :as transport] - [status-im.transport.inbox :as inbox] - [status-im.ui.screens.db :refer [app-db]] - [status-im.utils.datetime :as time] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.random :as random] - [status-im.utils.config :as config] - [status-im.utils.notifications :as notifications] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.http :as http] - [status-im.utils.instabug :as instabug] - [status-im.utils.platform :as platform] - [status-im.utils.types :as types] - [status-im.utils.utils :as utils] - [taoensso.timbre :as log])) - -;;;; COFX - -(re-frame/reg-cofx - :now - (fn [coeffects _] - (assoc coeffects :now (time/timestamp)))) - -(re-frame/reg-cofx - :random-id - (fn [coeffects _] - (assoc coeffects :random-id (random/id)))) - -(re-frame/reg-cofx - :random-id-seq - (fn [coeffects _] - (assoc coeffects :random-id-seq (repeatedly random/id)))) - -;;;; FX - -(defn- http-get [{:keys [url response-validator success-event-creator failure-event-creator timeout-ms]}] - (let [on-success #(re-frame/dispatch (success-event-creator %)) - on-error #(re-frame/dispatch (failure-event-creator %)) - opts {:valid-response? response-validator - :timeout-ms timeout-ms}] - (http/get url on-success on-error opts))) - -(re-frame/reg-fx - :http-get - http-get) - -(re-frame/reg-fx - :http-get-n - (fn [calls] - (doseq [call calls] - (http-get call)))) - -;; Try to decrypt the database, move on if successful otherwise go back to -;; initial state -(re-frame/reg-fx - ::init-store - (fn [encryption-key] - (.. (data-store/init encryption-key) - (then #(re-frame/dispatch [:after-decryption])) - (catch (fn [error] - (log/warn "Could not decrypt database" error) - (re-frame/dispatch [:initialize-app encryption-key :decryption-failed])))))) - -(re-frame/reg-fx - :initialize-geth-fx - (fn [config] - (status/start-node (types/clj->json config)))) - -(re-frame/reg-fx - ::status-module-initialized-fx - (fn [_] - (status/module-initialized!))) - -(re-frame/reg-fx - :request-permissions-fx - (fn [options] - (permissions/request-permissions options))) - -(re-frame/reg-fx - ::request-notifications-fx - (fn [_] - (notifications/request-permissions))) - -(re-frame/reg-fx - ::testfairy-alert - (fn [_] - (when config/testfairy-enabled? - (utils/show-popup - (i18n/label :testfairy-title) - (i18n/label :testfairy-message))))) - -(re-frame/reg-fx - ::init-device-UUID - (fn [] - (status/get-device-UUID #(re-frame/dispatch [:set :device-UUID %])))) - -(re-frame/reg-fx - ::listen-to-window-dimensions-change - (fn [] - (dimensions/add-event-listener))) - -(re-frame/reg-fx - ::get-fcm-token-fx - (fn [_] - (notifications/get-fcm-token))) - -(re-frame/reg-fx - :show-error - (fn [content] - (utils/show-popup "Error" content))) - -(re-frame/reg-fx - :show-confirmation - (fn [{:keys [title content confirm-button-text on-accept on-cancel]}] - (utils/show-confirmation title content confirm-button-text on-accept on-cancel))) - -(re-frame/reg-fx - :close-application - (fn [_] - (status/close-application))) - -(re-frame/reg-fx - ::app-state-change-fx - (fn [state] - (status/app-state-change state))) - -;;;; Handlers - -(handlers/register-handler-db - :set - (fn [db [_ k v]] - (assoc db k v))) - -(handlers/register-handler-db - :set-in - (fn [db [_ path v]] - (assoc-in db path v))) - -(defn- reset-keychain [] - (.. (keychain/reset) - (then - #(re-frame/dispatch [:initialize-keychain])))) - -(defn- handle-reset-data [] - (.. (realm/delete-realms) - (then reset-keychain) - (catch reset-keychain))) - -(defn handle-invalid-key-parameters [encryption-key] - {:title (i18n/label :invalid-key-title) - :content (i18n/label :invalid-key-content) - :confirm-button-text (i18n/label :invalid-key-confirm) - ;; On cancel we initialize the app with the invalid key, to allow the user - ;; to recover the seed phrase - :on-cancel #(do - (log/warn "initializing app with invalid key") - (re-frame/dispatch [:initialize-app encryption-key])) - :on-accept handle-reset-data}) - -(defn handle-decryption-failed-parameters [encryption-key] - {:title (i18n/label :decryption-failed-title) - :content (i18n/label :decryption-failed-content) - :confirm-button-text (i18n/label :decryption-failed-confirm) - ;; On cancel we initialize the app with the same key, in case the error was - ;; not related/fs error - :on-cancel #(do - (log/warn "initializing app with same key after decryption failed") - (re-frame/dispatch [:initialize-app encryption-key])) - :on-accept handle-reset-data}) - -(defn initialize-views [{{:accounts/keys [accounts] :as db} :db}] - {:db (if (empty? accounts) - (assoc db :view-id :intro :navigation-stack (list :intro)) - (let [{:keys [address photo-path name]} (first (sort-by :last-sign-in > (vals accounts)))] - (-> db - (assoc :view-id :login - :navigation-stack (list :login)) - (update :accounts/login assoc - :address address - :photo-path photo-path - :name name))))}) - -(defn initialize-db - "Initialize db to the initial state" - [{{:universal-links/keys [url] - :keys [status-module-initialized? status-node-started? - network-status network peers-count peers-summary device-UUID] - :or {network (get app-db :network)}} :db}] - {:db (assoc app-db - :contacts/contacts {} - :network-status network-status - :peers-count (or peers-count 0) - :peers-summary (or peers-summary []) - :status-module-initialized? (or platform/ios? js/goog.DEBUG status-module-initialized?) - :status-node-started? status-node-started? - :network network - :universal-links/url url - :device-UUID device-UUID)}) - -;; Entrypoint, fetches the key from the keychain and initialize the app -(handlers/register-handler-fx - :initialize-keychain - (fn [_ _] - {:get-encryption-key [:initialize-app]})) - -;; Check the key is valid, shows options if not, otherwise continues loading -;; the database -(handlers/register-handler-fx - :initialize-app - (fn [cofx [_ encryption-key error]] - (cond - (= :invalid-key error) - {:show-confirmation (handle-invalid-key-parameters encryption-key)} - - (= :decryption-failed error) - {:show-confirmation (handle-decryption-failed-parameters encryption-key)} - - :else - (handlers-macro/merge-fx cofx - {::init-device-UUID nil - ::init-store encryption-key - ::listen-to-window-dimensions-change nil - ::testfairy-alert nil} - (initialize-db))))) - -;; DB has been decrypted, load accounts, initialize geth, etc -(handlers/register-handler-fx - :after-decryption - [(re-frame/inject-cofx :data-store/get-all-accounts)] - (fn [cofx _] - (handlers-macro/merge-fx cofx - {:dispatch-n - [[:listen-to-network-status] - [:initialize-geth]]} - (accounts.models/load-accounts) - (initialize-views)))) - -(handlers/register-handler-fx - :logout - (fn [{:keys [db] :as cofx} _] - (let [{:transport/keys [chats]} db] - (handlers-macro/merge-fx cofx - {:dispatch [:initialize-keychain]} - (navigation/navigate-to-clean nil) - (transport/stop-whisper))))) - -(handlers/register-handler-db - :initialize-account-db - (fn [{:keys [accounts/accounts accounts/create contacts/contacts networks/networks - network network-status peers-count peers-summary view-id navigation-stack - status-module-initialized? status-node-started? device-UUID] - :or [network (get app-db :network)]} [_ address]] - (let [console-contact (get contacts constants/console-chat-id) - current-account (accounts address) - account-network-id (get current-account :network network) - account-network (get-in current-account [:networks account-network-id])] - (cond-> (assoc app-db - :current-public-key (:public-key current-account) - :view-id view-id - :navigation-stack navigation-stack - :status-module-initialized? (or platform/ios? js/goog.DEBUG status-module-initialized?) - :status-node-started? status-node-started? - :accounts/create create - :networks/networks networks - :account/account current-account - :network-status network-status - :network network - :chain (ethereum/network->chain-name account-network) - :peers-summary peers-summary - :peers-count peers-count - :device-UUID device-UUID) - console-contact - (assoc :contacts/contacts {constants/console-chat-id console-contact}))))) - -(handlers/register-handler-fx - :initialize-account - (fn [cofx [_ address events-after]] - {:dispatch-n (cond-> [[:initialize-account-db address] - [:initialize-protocol address] - [:fetch-web3-node-version] - [:initialize-sync-listener] - [:load-contacts] - [:initialize-chats] - [:initialize-browsers] - [:initialize-dapp-permissions] - [:send-account-update-if-needed] - [:process-pending-messages] - [:update-wallet] - [:update-transactions] - (when platform/mobile? [:get-fcm-token]) - [:start-wallet-transactions-sync] - [:update-sign-in-time]] - (seq events-after) (into events-after))})) - -(handlers/register-handler-fx - :initialize-geth - (fn [{db :db} _] - (when-not (:status-node-started? db) - (let [default-networks (:networks/networks db) - default-network (:network db)] - {:initialize-geth-fx (get-in default-networks [default-network :config])})))) - -(handlers/register-handler-fx - :fetch-web3-node-version-callback - (fn [{:keys [db]} [_ resp]] - (when-let [git-commit (nth (re-find #"-([0-9a-f]{7,})/" resp) 1)] - {:db (assoc db :web3-node-version git-commit)}))) - -(handlers/register-handler-fx - :fetch-web3-node-version - (fn [{{:keys [web3] :as db} :db} _] - (.. web3 -version (getNode (fn [err resp] - (when-not err - (re-frame/dispatch [:fetch-web3-node-version-callback resp]))))) - nil)) - -(handlers/register-handler-fx - :webview-geo-permissions-granted - (fn [{{:keys [webview-bridge]} :db} _] - (.geoPermissionsGranted webview-bridge))) - -(handlers/register-handler-fx - :get-fcm-token - (fn [_ _] - {::get-fcm-token-fx nil})) - -(handlers/register-handler-fx - :discovery/summary - (fn [{:keys [db] :as cofx} [_ peers-summary]] - (let [previous-summary (:peers-summary db) - peers-count (count peers-summary)] - (handlers-macro/merge-fx cofx - {:db (assoc db - :peers-summary peers-summary - :peers-count peers-count)} - (transport.handlers/resend-contact-messages previous-summary) - (inbox/peers-summary-change-fx previous-summary))))) - -(handlers/register-handler-fx - :signal-event - (fn [_ [_ event-str]] - (log/debug :event-str event-str) - (instabug/log (str "Signal event: " event-str)) - (let [{:keys [type event]} (types/json->clj event-str) - to-dispatch (case type - "sign-request.queued" [:sign-request-queued event] - "sign-request.failed" [:sign-request-failed event] - "node.started" [:status-node-started] - "node.stopped" [:status-node-stopped] - "module.initialized" [:status-module-initialized] - "envelope.sent" [:signals/envelope-status (:hash event) :sent] - "envelope.expired" [:signals/envelope-status (:hash event) :not-sent] - "discovery.summary" [:discovery/summary event] - (log/debug "Event " type " not handled"))] - (when to-dispatch - {:dispatch to-dispatch})))) - -(handlers/register-handler-fx - :status-module-initialized - (fn [{:keys [db]} _] - {:db (assoc db :status-module-initialized? true) - ::status-module-initialized-fx nil})) - -(handlers/register-handler-fx - :status-node-started - (fn [{{:node/keys [after-start] :as db} :db} _] - (merge {:db (assoc db :status-node-started? true)} - (when after-start {:dispatch-n [after-start]})))) - -(handlers/register-handler-fx - :status-node-stopped - (fn [{{:node/keys [after-stop]} :db} _] - (when after-stop {:dispatch-n [after-stop]}))) - -(handlers/register-handler-fx - :app-state-change - (fn [{:keys [db] :as cofx} [_ state]] - (let [app-coming-from-background? (= state "active")] - (handlers-macro/merge-fx cofx - {::app-state-change-fx state - :db (assoc db :app-state state)} - (inbox/request-messages app-coming-from-background?))))) - -(handlers/register-handler-fx - :request-permissions - (fn [_ [_ options]] - {:request-permissions-fx options})) - -(handlers/register-handler-fx - :request-notifications - (fn [_ _] - {::request-notifications-fx {}})) - -(handlers/register-handler-db - :set-swipe-position - [re-frame/trim-v] - (fn [db [item-id value]] - (assoc-in db [:chat-animations item-id :delete-swiped] value))) - -(handlers/register-handler-db - :show-tab-bar - (fn [db _] - (assoc db :tab-bar-visible? true))) - -(handlers/register-handler-db - :hide-tab-bar - (fn [db _] - (assoc db :tab-bar-visible? false))) - -(handlers/register-handler-db - :update-window-dimensions - (fn [db [_ dimensions]] - (assoc db :dimensions/window (dimensions/window dimensions)))) diff --git a/src/status_im/ui/screens/group/add_contacts/views.cljs b/src/status_im/ui/screens/group/add_contacts/views.cljs deleted file mode 100644 index 549562eb172..00000000000 --- a/src/status_im/ui/screens/group/add_contacts/views.cljs +++ /dev/null @@ -1,68 +0,0 @@ -(ns status-im.ui.screens.group.add-contacts.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.contact.contact :refer [toogle-contact-view]] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :refer [status-bar]] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.group.styles :as styles])) - -(defn- on-toggle [checked? whisper-identity] - (let [action (if checked? :deselect-contact :select-contact)] - (re-frame/dispatch [action whisper-identity]))) - -(defn- on-toggle-participant [checked? whisper-identity] - (let [action (if checked? :deselect-participant :select-participant)] - (re-frame/dispatch [action whisper-identity]))) - -(defn- group-toggle-contact [contact] - [toogle-contact-view contact :is-contact-selected? on-toggle]) - -(defn- group-toggle-participant [contact] - [toogle-contact-view contact :is-participant-selected? on-toggle-participant]) - -(defn- toggle-list-toolbar [{:keys [handler count label]} title] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title title] - (when (pos? count) - [toolbar/text-action {:handler handler - :accessibility-label :next-button} - label])]) - -(defn toggle-list [contacts render-function] - [react/view {:flex 1} - [list/flat-list {:style styles/contacts-list - :data contacts - :key-fn :address - :render-fn render-function - :keyboardShouldPersistTaps :always}]]) - -;; Start group chat -(defview contact-toggle-list [] - (letsubs [contacts [:all-added-people-contacts] - selected-contacts-count [:selected-contacts-count]] - [react/keyboard-avoiding-view {:style styles/group-container} - [status-bar] - [toggle-list-toolbar {:handler #(re-frame/dispatch [:navigate-to :new-group]) - :label (i18n/label :t/next) - :count (pos? selected-contacts-count)} - (i18n/label :t/group-chat)] - [toggle-list contacts group-toggle-contact]])) - -;; Add participants to existing group chat -(defview add-participants-toggle-list [] - (letsubs [contacts [:get-all-contacts-not-in-current-chat] - {:keys [name]} [:get-current-chat] - selected-contacts-count [:selected-participants-count]] - [react/keyboard-avoiding-view {:style styles/group-container} - [status-bar] - [toggle-list-toolbar {:count selected-contacts-count - :handler #(do - (re-frame/dispatch [:add-new-group-chat-participants]) - (re-frame/dispatch [:navigate-back])) - :label (i18n/label :t/add)} - name] - [toggle-list contacts group-toggle-participant]])) diff --git a/src/status_im/ui/screens/group/chat_settings/events.cljs b/src/status_im/ui/screens/group/chat_settings/events.cljs deleted file mode 100644 index 221669999cf..00000000000 --- a/src/status_im/ui/screens/group/chat_settings/events.cljs +++ /dev/null @@ -1,60 +0,0 @@ -(ns status-im.ui.screens.group.chat-settings.events - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.chat.models.message :as models.message] - [status-im.chat.models :as models.chat] - [status-im.ui.screens.navigation :as navigation] - [status-im.transport.message.v1.group-chat :as group-chat] - [status-im.transport.message.core :as transport] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.data-store.chats :as chats-store] - [status-im.data-store.messages :as messages-store])) - -(handlers/register-handler-fx - :show-group-chat-profile - [re-frame/trim-v] - (fn [{:keys [db]} [chat-id]] - {:db (-> db - (assoc :new-chat-name (get-in db [:chats chat-id :name]) - :group/group-type :chat-group) - (navigation/navigate-to :group-chat-profile))})) - -(handlers/register-handler-fx - :add-new-group-chat-participants - [(re-frame/inject-cofx :random-id)] - (fn [{{:keys [current-chat-id selected-participants] :as db} :db now :now message-id :random-id :as cofx} _] - (let [participants (concat (get-in db [:chats current-chat-id :contacts]) selected-participants) - contacts (:contacts/contacts db) - added-participants-names (map #(get-in contacts [% :name]) selected-participants)] - (handlers-macro/merge-fx cofx - {:db (-> db - (assoc-in [:chats current-chat-id :contacts] participants) - (assoc :selected-participants #{})) - :data-store/tx [(chats-store/add-chat-contacts-tx current-chat-id selected-participants)]} - (models.message/receive - (models.message/system-message current-chat-id message-id now - (str "You've added " (apply str (interpose ", " added-participants-names))))) - (transport/send (group-chat/GroupAdminUpdate. nil participants) current-chat-id))))) - -(handlers/register-handler-fx - :remove-group-chat-participants - [re-frame/trim-v (re-frame/inject-cofx :random-id)] - (fn [{{:keys [current-chat-id] :as db} :db now :now message-id :random-id :as cofx} [removed-participants]] - (let [participants (remove removed-participants (get-in db [:chats current-chat-id :contacts])) - contacts (:contacts/contacts db) - removed-participants-names (map #(get-in contacts [% :name]) removed-participants)] - (handlers-macro/merge-fx cofx - {:db (assoc-in db [:chats current-chat-id :contacts] participants) - :data-store/tx [(chats-store/remove-chat-contacts-tx current-chat-id removed-participants)]} - (models.message/receive - (models.message/system-message current-chat-id message-id now - (str "You've removed " (apply str (interpose ", " removed-participants-names))))) - (transport/send (group-chat/GroupAdminUpdate. nil participants) current-chat-id))))) - -(handlers/register-handler-fx - :set-group-chat-name - (fn [{{:keys [current-chat-id] :as db} :db} [_ new-chat-name]] - {:db (assoc-in db [:chats current-chat-id :name] new-chat-name) - :data-store/tx [(chats-store/save-chat-tx {:chat-id current-chat-id - :name new-chat-name})]})) diff --git a/src/status_im/ui/screens/group/core.cljs b/src/status_im/ui/screens/group/core.cljs deleted file mode 100644 index cb2bd2863dd..00000000000 --- a/src/status_im/ui/screens/group/core.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.ui.screens.group.core - (:require [status-im.data-store.chats :as chats-store])) - -(defn participants-added [chat-id added-participants-set {:keys [db] :as cofx}] - (when (seq added-participants-set) - {:db (update-in db [:chats chat-id :contacts] - concat added-participants-set) - :data-store/tx [(chats-store/add-chat-contacts-tx - chat-id added-participants-set)]})) - -(defn participants-removed [chat-id removed-participants-set {:keys [now db] :as cofx}] - (when (seq removed-participants-set) - (let [{:keys [is-active timestamp]} (get-in db [:chats chat-id])] - ;;TODO: not sure what this condition is for - (when (and is-active (>= now timestamp)) - {:db (update-in db [:chats chat-id :contacts] - (partial remove removed-participants-set)) - :data-store/tx [(chats-store/remove-chat-contacts-tx - chat-id removed-participants-set)]})))) diff --git a/src/status_im/ui/screens/group/db.cljs b/src/status_im/ui/screens/group/db.cljs deleted file mode 100644 index bdf748e31a0..00000000000 --- a/src/status_im/ui/screens/group/db.cljs +++ /dev/null @@ -1,5 +0,0 @@ -(ns status-im.ui.screens.group.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec])) - -(spec/def :group/selected-contacts (spec/nilable (spec/* string?))) diff --git a/src/status_im/ui/screens/group/events.cljs b/src/status_im/ui/screens/group/events.cljs deleted file mode 100644 index fee0e56b7fc..00000000000 --- a/src/status_im/ui/screens/group/events.cljs +++ /dev/null @@ -1,23 +0,0 @@ -(ns status-im.ui.screens.group.events - (:require [status-im.utils.handlers :as handlers] - [status-im.ui.screens.group.navigation])) - -(handlers/register-handler-db - :deselect-contact - (fn [db [_ id]] - (update db :group/selected-contacts disj id))) - -(handlers/register-handler-db - :select-contact - (fn [db [_ id]] - (update db :group/selected-contacts conj id))) - -(handlers/register-handler-db - :deselect-participant - (fn [db [_ id]] - (update db :selected-participants disj id))) - -(handlers/register-handler-db - :select-participant - (fn [db [_ id]] - (update db :selected-participants conj id))) diff --git a/src/status_im/ui/screens/group/navigation.cljs b/src/status_im/ui/screens/group/navigation.cljs deleted file mode 100644 index 43ab255582a..00000000000 --- a/src/status_im/ui/screens/group/navigation.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns status-im.ui.screens.group.navigation - (:require [status-im.ui.screens.navigation :as nav])) - -(defmethod nav/preload-data! :add-participants-toggle-list - [db _] - (assoc db :selected-participants #{})) - -(defmethod nav/preload-data! :new-public-chat - [db] - (dissoc db :public-group-topic :public-group-topic-error)) diff --git a/src/status_im/ui/screens/group/styles.cljs b/src/status_im/ui/screens/group/styles.cljs deleted file mode 100644 index 1b2fba42ee5..00000000000 --- a/src/status_im/ui/screens/group/styles.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.ui.screens.group.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(defn toolbar-icon [enabled?] - {:width 20 - :height 18 - :opacity (if enabled? 1 0.3)}) - -(def group-container - {:flex 1 - :flex-direction :column - :background-color colors/white}) - -(def contact - {:padding-left 0}) - -(def contacts-list - {:background-color colors/white}) diff --git a/src/status_im/ui/screens/group/subs.cljs b/src/status_im/ui/screens/group/subs.cljs deleted file mode 100644 index 590e67b3cfe..00000000000 --- a/src/status_im/ui/screens/group/subs.cljs +++ /dev/null @@ -1,37 +0,0 @@ -(ns status-im.ui.screens.group.subs - (:require [re-frame.core :refer [reg-sub]] - [status-im.utils.subs :as utils])) - -(reg-sub - :is-contact-selected? - (utils/contains-sub :group/selected-contacts)) - -(reg-sub - :is-participant-selected? - (utils/contains-sub :selected-participants)) - -(defn filter-selected-contacts [selected-contacts contacts] - (remove #(true? (:pending? (contacts %))) selected-contacts)) - -(reg-sub - :selected-contacts-count - :<- [:get :group/selected-contacts] - :<- [:get-contacts] - (fn [[selected-contacts contacts]] - (count (filter-selected-contacts selected-contacts contacts)))) - -(reg-sub - :selected-participants-count - :<- [:get :selected-participants] - (fn [selected-participants] - (count selected-participants))) - -(defn filter-contacts [selected-contacts added-contacts] - (filter #(selected-contacts (:whisper-identity %)) added-contacts)) - -(reg-sub - :selected-group-contacts - :<- [:get :group/selected-contacts] - :<- [:all-added-contacts] - (fn [[selected-contacts added-contacts]] - (filter-contacts selected-contacts added-contacts))) diff --git a/src/status_im/ui/screens/group/views.cljs b/src/status_im/ui/screens/group/views.cljs deleted file mode 100644 index 3fffa166bb4..00000000000 --- a/src/status_im/ui/screens/group/views.cljs +++ /dev/null @@ -1,63 +0,0 @@ -(ns status-im.ui.screens.group.views - (:require-macros [status-im.utils.views :as views]) - (:require [cljs.spec.alpha :as spec] - [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.utils.platform :as platform] - [status-im.ui.components.contact.contact :as contact] - [status-im.ui.screens.add-new.styles :as add-new.styles] - [status-im.ui.screens.group.styles :as styles])) - -(views/defview group-name-view [] - (views/letsubs [new-group-name [:get :new-chat-name]] - [react/view add-new.styles/input-container - [react/text-input - {:auto-focus true - :on-change-text #(re-frame/dispatch [:set :new-chat-name %]) - :default-value new-group-name - :placeholder (i18n/label :t/set-a-topic) - :style add-new.styles/input - :accessibility-label :chat-name-input}]])) - -(defn- render-contact [contact] - [contact/contact-view {:contact contact - :style styles/contact - :accessibility-label :chat-member-item}]) - -(defn- toolbar [group-name save-btn-enabled?] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/group-chat)] - (when save-btn-enabled? - (let [handler #(re-frame/dispatch [:create-new-group-chat-and-open group-name])] - (if platform/android? - [toolbar/actions [{:icon :icons/ok - :icon-opts {:color :blue - :accessibility-label :create-button} - :handler handler}]] - [toolbar/text-action {:handler handler - :accessibility-label :create-button} - (i18n/label :t/create)])))]) - -;; New Group Chat -(views/defview new-group [] - (views/letsubs [contacts [:selected-group-contacts] - group-name [:get :new-chat-name]] - (let [save-btn-enabled? (and (spec/valid? :global/not-empty-string group-name) (pos? (count contacts)))] - [react/keyboard-avoiding-view (merge {:behavior :padding} - styles/group-container) - [status-bar/status-bar] - [toolbar group-name save-btn-enabled?] - [group-name-view] - [list/list-with-label {:flex 1} - (i18n/label :t/members-title) - [list/flat-list {:data contacts - :key-fn :address - :render-fn render-contact - :bounces false - :keyboard-should-persist-taps :always - :enable-empty-sections true}]]]))) diff --git a/src/status_im/ui/screens/help_center/styles.cljs b/src/status_im/ui/screens/help_center/styles.cljs deleted file mode 100644 index 61f736a8641..00000000000 --- a/src/status_im/ui/screens/help_center/styles.cljs +++ /dev/null @@ -1,4 +0,0 @@ -(ns status-im.ui.screens.help-center.styles) - -(def wrapper - {:flex 1}) diff --git a/src/status_im/ui/screens/help_center/views.cljs b/src/status_im/ui/screens/help_center/views.cljs deleted file mode 100644 index 302da9bee61..00000000000 --- a/src/status_im/ui/screens/help_center/views.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.ui.screens.help-center.views - (:require-macros [status-im.utils.views :as views]) - (:require [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.help-center.styles :as styles] - [status-im.ui.screens.profile.components.views :as profile.components] - [status-im.utils.instabug :as instabug])) - -(views/defview help-center [] - [react/view styles/wrapper - [status-bar/status-bar] - [toolbar/simple-toolbar - (i18n/label :t/help-center)] - [react/scroll-view - [react/view - [profile.components/settings-item-separator] - [profile.components/settings-item {:label-kw :t/faq - :accessibility-label :faq-button - :action-fn #(.openURL react/linking "https://wiki.status.im/Questions_around_beta#firstHeading")}] - [profile.components/settings-item-separator] - [profile.components/settings-item {:label-kw :t/submit-bug - :accessibility-label :submit-bug-button - :action-fn #(instabug/submit-bug)}] - [profile.components/settings-item-separator] - [profile.components/settings-item {:label-kw :t/request-feature - :accessibility-label :request-feature-button - :action-fn #(instabug/request-feature)}]]]]) diff --git a/src/status_im/ui/screens/home/animations/responder.cljs b/src/status_im/ui/screens/home/animations/responder.cljs deleted file mode 100644 index e1b45fcade3..00000000000 --- a/src/status_im/ui/screens/home/animations/responder.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.ui.screens.home.animations.responder - (:require [status-im.ui.components.react :as react] - [re-frame.core :as re-frame] - [status-im.ui.components.animation :as animation])) - -(defn get-updated-value [gesture end-offset-x swiped?] - (let [base-value (if swiped? end-offset-x 0)] - (- base-value (.-dx gesture)))) - -(defn on-start [_ gesture] - (> (js/Math.abs (.-dx gesture)) 10)) - -(defn on-move [animated-offset-x end-offset-x swiped?] - (fn [_ gesture] - (let [to-value (get-updated-value gesture end-offset-x swiped?)] - (animation/start (animation/spring animated-offset-x {:toValue to-value}))))) - -(defn on-release [animated-offset-x end-offset-x chat-id swiped?] - (fn [_ gesture] - (let [updated-value (get-updated-value gesture end-offset-x swiped?) - should-open? (> updated-value (/ end-offset-x 2)) - to-value (if should-open? end-offset-x 0)] - (re-frame/dispatch [:set-swipe-position chat-id should-open?]) - (animation/start (animation/spring animated-offset-x {:toValue to-value}))))) - -(defn swipe-pan-responder [animated-offset-x end-offset-x chat-id swiped?] - (.create react/pan-responder - (clj->js {:onMoveShouldSetPanResponder on-start - :onPanResponderMove (on-move animated-offset-x end-offset-x swiped?) - :onPanResponderRelease (on-release animated-offset-x end-offset-x chat-id swiped?) - :onPanResponderTerminate (on-release animated-offset-x end-offset-x chat-id swiped?)}))) - -(defn pan-handlers [pan-responder] - (js->clj (.-panHandlers pan-responder))) \ No newline at end of file diff --git a/src/status_im/ui/screens/home/styles.cljs b/src/status_im/ui/screens/home/styles.cljs deleted file mode 100644 index 9fc07ec5dde..00000000000 --- a/src/status_im/ui/screens/home/styles.cljs +++ /dev/null @@ -1,217 +0,0 @@ -(ns status-im.ui.screens.home.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :as component.styles] - [status-im.ui.components.colors :as colors])) - -(def delete-button-width 100) - -(defn toolbar [] - {:background-color colors/white}) - -(def chat-separator-item - {:border-bottom-width 1 - :border-bottom-color component.styles/color-gray5}) - -(defstyle chat-container - {:flex-direction :row - :background-color colors/white - :android {:height 76} - :ios {:height 74} - :overflow :hidden}) - -(defstyle chat-icon-container - {:padding-top 18 - :padding-bottom 18 - :padding-left 12 - :padding-right 20 - :width 72 - :android {:height 76} - :ios {:height 74}}) - -(def browser-icon-container - {:width 40 - :height 40 - :border-radius 20 - :background-color colors/gray-lighter - :align-items :center - :justify-content :center}) - -(defstyle chat-info-container - {:margin-bottom 13 - :justify-content :space-between - :flex 1 - :flex-direction :column - :android {:margin-top 16} - :ios {:margin-top 14}}) - -(defstyle chat-options-container - {:padding-top 10}) - -(defstyle item-upper-container - {:flex 1 - :flex-direction :row - :padding-right 16}) - -(defstyle item-lower-container - {:flex 1 - :flex-direction :row - :justify-content :space-between - :padding-right 16 - :android {:margin-top 4} - :ios {:margin-top 6}}) - -(def message-status-container - {:flex-direction :row - :align-items :center}) - -(def name-view - {:flex-direction :row - :flex 1 - :margin-right 4}) - -(def name-text - {:color component.styles/text1-color - :font-size 16}) - -(defstyle private-group-icon-container - {:width 20 - :height 12 - :margin-right 6}) - -(defstyle public-group-icon-container - {:width 20 - :height 12 - :margin-right 6}) - -(def last-message-container - {:flex-shrink 1}) - -(defstyle last-message-text - {:color component.styles/text4-color - :android {:font-size 14 - :height 24} - :ios {:font-size 15 - :height 24}}) - -(def status-container - {:flex-direction :row - :top 16 - :right 16}) - -(def status-image - {:opacity 0.6 - :margin-right 4 - :width 16 - :height 16}) - -(defstyle datetime-text - {:color component.styles/text4-color - :android {:font-size 14} - :desktop {:font-size 14} - :ios {:font-size 15}}) - -(def new-messages-container - {:min-width 22 - :height 22 - :padding-horizontal 8 - :margin-left 15 - :background-color component.styles/new-messages-count-color - :border-radius 50}) - -(defstyle new-messages-text - {:left 0 - :font-size 12 - :color colors/blue - :text-align :center - :android {:top 2} - :ios {:top 3}}) - -(def container - {:flex 1}) - -(def native-button-offset 16) - -(def create-icon - {:font-size 20 - :height 22 - :color colors/white}) - -(def group-icon - {:margin-top 8 - :margin-right 6 - :width 14 - :height 9 - :tint-color :white}) - -(def no-chats - {:flex 1 - :align-items :center - :justify-content :center - :margin-horizontal 34}) - -(def no-chats-text - {:line-height 21 - :text-align :center - :color colors/gray}) - -(def welcome-view - {:flex 1}) - -(defstyle welcome-image-container - {:align-items :center - :android {:margin-top 38} - :ios {:margin-top 42}}) - -(def welcome-image - {:width 320 - :height 278}) - -(defstyle welcome-text - {:line-height 28 - :font-size 22 - :font-weight :bold - :letter-spacing -0.3 - :android {:margin-top 22} - :ios {:margin-top 96} - :text-align :center - :color colors/black}) - -(defstyle welcome-text-description - {:line-height 21 - :margin-top 8 - :android {:margin-bottom 82} - :ios {:margin-bottom 32} - :text-align :center - :color colors/gray}) - -(def toolbar-logo - {:size 42 - :icon-size 17 - :shadow? false}) - -(def delete-icon-highlight - {:position :absolute - :top 0 - :bottom 0 - :right -800 - :width 800 - :background-color colors/red-light}) - -(def delete-icon-container - {:flex 1 - :width delete-button-width - :justify-content :center - :align-items :center}) - -(def action-button-container - {:position :absolute - :bottom 16 - :right 16}) - -(def action-button - {:width 56 - :height 56 - :background-color colors/blue - :border-radius 28 - :align-items :center - :justify-content :center}) diff --git a/src/status_im/ui/screens/home/subs.cljs b/src/status_im/ui/screens/home/subs.cljs deleted file mode 100644 index 209a9387d6f..00000000000 --- a/src/status_im/ui/screens/home/subs.cljs +++ /dev/null @@ -1,8 +0,0 @@ -(ns status-im.ui.screens.home.subs - (:require [re-frame.core :as re-frame])) - -(re-frame/reg-sub :home-items - :<- [:get-active-chats] - :<- [:browsers] - (fn [[chats browsers]] - (sort-by #(-> % second :timestamp) > (merge chats browsers)))) diff --git a/src/status_im/ui/screens/home/views.cljs b/src/status_im/ui/screens/home/views.cljs deleted file mode 100644 index 08cfa23f853..00000000000 --- a/src/status_im/ui/screens/home/views.cljs +++ /dev/null @@ -1,95 +0,0 @@ -(ns status-im.ui.screens.home.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.toolbar.actions :as toolbar.actions] - [status-im.ui.components.connectivity.view :as connectivity] - [status-im.ui.screens.home.views.inner-item :as inner-item] - [status-im.ui.screens.home.styles :as styles] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.animation :as animation] - [status-im.ui.screens.home.animations.responder :as responder] - [status-im.utils.platform :as platform] - [status-im.react-native.resources :as resources] - [status-im.ui.components.common.common :as components.common] - [status-im.i18n :as i18n] - [status-im.ui.components.icons.vector-icons :as icons])) - -(defn- toolbar [show-welcome?] - (when-not (and show-welcome? - platform/android?) - [toolbar/toolbar nil nil - (when-not show-welcome? - [toolbar/content-wrapper - [components.common/logo styles/toolbar-logo]]) - [toolbar/actions - (when platform/ios? - [(-> (toolbar.actions/add true #(re-frame/dispatch [:navigate-to :new])) - (assoc-in [:icon-opts :accessibility-label] :new-chat-button))])]])) - -(defn- home-action-button [] - [react/view styles/action-button-container - [react/touchable-highlight {:accessibility-label :new-chat-button - :on-press #(re-frame/dispatch [:navigate-to :new])} - [react/view styles/action-button - [icons/icon :icons/add {:color :white}]]]]) - -(views/defview home-list-item [[home-item-id home-item]] - (views/letsubs [swiped? [:delete-swipe-position home-item-id]] - (let [delete-action (if (:chat-id home-item) :remove-chat-and-navigate-home :remove-browser) - inner-item-view (if (:chat-id home-item) - inner-item/home-list-chat-item-inner-view - inner-item/home-list-browser-item-inner-view) - is-deletable? (not= (:chat-id home-item) constants/console-chat-id) - offset-x (animation/create-value (if (and is-deletable? swiped?) styles/delete-button-width 0)) - swipe-pan-responder (responder/swipe-pan-responder offset-x styles/delete-button-width home-item-id swiped?) - swipe-pan-handler (if is-deletable? (responder/pan-handlers swipe-pan-responder) {})] - [react/view (assoc swipe-pan-handler :accessibility-label :chat-item) - [react/animated-view {:style {:flex 1 :right offset-x}} - [inner-item-view home-item] - (when is-deletable? - [react/touchable-highlight {:style styles/delete-icon-highlight - :on-press #(do - (re-frame/dispatch [:set-swipe-position home-item-id false]) - (re-frame/dispatch [delete-action home-item-id]))} - [react/view {:style styles/delete-icon-container} - [vector-icons/icon :icons/delete {:color colors/red}]]])]]))) - -;;do not remove view-id and will-update or will-unmount handlers, this is how it works -(views/defview welcome [view-id] - (views/letsubs [handler #(re-frame/dispatch [:set-in [:accounts/create :show-welcome?] false])] - {:component-will-update handler - :component-will-unmount handler} - [react/view {:style styles/welcome-view} - [react/view {:style styles/welcome-image-container} - [react/image {:source (:welcome-image resources/ui) - :style styles/welcome-image}]] - [react/i18n-text {:style styles/welcome-text :key :welcome-to-status}] - [react/view - [react/i18n-text {:style styles/welcome-text-description - :key :welcome-to-status-description}]]])) - -(views/defview home [] - (views/letsubs [home-items [:home-items] - show-welcome? [:get-in [:accounts/create :show-welcome?]] - view-id [:get :view-id]] - [react/view styles/container - [toolbar show-welcome?] - (cond show-welcome? - [welcome view-id] - (empty? home-items) - [react/view styles/no-chats - [react/i18n-text {:style styles/no-chats-text :key :no-recent-chats}]] - :else - [list/flat-list {:data home-items - :key-fn first - :render-fn (fn [home-item] - [home-list-item home-item])}]) - (when platform/android? - [home-action-button]) - (when-not show-welcome? - [connectivity/error-view])])) diff --git a/src/status_im/ui/screens/home/views/inner_item.cljs b/src/status_im/ui/screens/home/views/inner_item.cljs deleted file mode 100644 index 78476ce9965..00000000000 --- a/src/status_im/ui/screens/home/views/inner_item.cljs +++ /dev/null @@ -1,132 +0,0 @@ -(ns status-im.ui.screens.home.views.inner-item - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [clojure.string :as str] - [status-im.constants :as constants] - [status-im.chat.commands.core :as commands] - [status-im.chat.commands.receiving :as commands-receiving] - [status-im.ui.components.react :as react] - [status-im.ui.screens.home.styles :as styles] - [status-im.ui.components.styles :as component.styles] - [status-im.utils.core :as utils] - [status-im.i18n :as i18n] - [status-im.utils.datetime :as time] - [status-im.utils.gfycat.core :as gfycat] - [status-im.constants :as const] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.chat-preview :as chat-preview] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.chat-icon.screen :as chat-icon.screen] - [status-im.ui.components.common.common :as components.common] - [status-im.models.browser :as model])) - -(defview command-short-preview [message] - (letsubs [id->command [:get-id->command]] - (when-let [command (commands-receiving/lookup-command-by-ref message id->command)] - (commands/generate-short-preview command message)))) - -(defn message-content-text [{:keys [content content-type] :as message}] - [react/view styles/last-message-container - (cond - - (not message) - [react/text {:style styles/last-message-text - :accessibility-label :no-messages-text} - (i18n/label :t/no-messages)] - - (str/blank? content) - [react/text {:style styles/last-message-text} - ""] - - (:content content) - [react/text {:style styles/last-message-text - :number-of-lines 1 - :accessibility-label :chat-message-text} - (:content content)] - - (contains? #{constants/content-type-command - constants/content-type-command-request} - content-type) - [command-short-preview message] - - :else - [react/text {:style styles/last-message-text - :number-of-lines 1 - :accessibility-label :chat-message-text} - content])]) - -(defn message-timestamp [timestamp] - (when timestamp - [react/text {:style styles/datetime-text - :accessibility-label :last-message-time-text} - (time/to-short-str timestamp)])) - -(defview unviewed-indicator [chat-id] - (letsubs [unviewed-messages-count [:unviewed-messages-count chat-id]] - (when (pos? unviewed-messages-count) - [components.common/counter {:size 22 - :accessibility-label :unread-messages-count-text} - unviewed-messages-count]))) - -(defn chat-list-item-name [chat-name group-chat? public? public-key] - (let [private-group? (and group-chat? (not public?)) - public-group? (and group-chat? public?)] - [react/view styles/name-view - (when public-group? - [react/view styles/public-group-icon-container - [vector-icons/icon :icons/public-chat {:color colors/gray}]]) - (when private-group? - [react/view styles/private-group-icon-container - [vector-icons/icon :icons/group-chat {:color colors/gray}]]) - [react/view {:flex-shrink 1} - [react/text {:style styles/name-text - :number-of-lines 1 - :accessibility-label :chat-name-text} - chat-name]]])) - -(defview home-list-chat-item-inner-view [{:keys [chat-id name color online - group-chat public? - public-key - timestamp]}] - (letsubs [last-message [:get-last-message chat-id] - chat-name [:get-chat-name chat-id]] - (let [hide-dapp? (= chat-id const/console-chat-id) - truncated-chat-name (utils/truncate-str chat-name 30)] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to-chat chat-id])} - [react/view styles/chat-container - [react/view styles/chat-icon-container - [chat-icon.screen/chat-icon-view-chat-list chat-id group-chat truncated-chat-name color online hide-dapp?]] - [react/view styles/chat-info-container - [react/view styles/item-upper-container - [chat-list-item-name truncated-chat-name group-chat public? public-key] - [react/view styles/message-status-container - [message-timestamp (or (:timestamp last-message) - timestamp)]]] - [react/view styles/item-lower-container - [message-content-text last-message] - [unviewed-indicator chat-id]]]]]))) - -(defview home-list-browser-item-inner-view [{:keys [name] :as browser}] - (letsubs [dapp [:get-dapp-by-name name] - url (model/get-current-url browser)] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:open-browser browser])} - [react/view styles/chat-container - [react/view styles/chat-icon-container - (if dapp - [chat-icon.screen/dapp-icon-browser dapp 36] - [react/view styles/browser-icon-container - [vector-icons/icon :icons/discover {:color component.styles/color-light-gray6}]])] - [react/view styles/chat-info-container - [react/view styles/item-upper-container - [react/view styles/name-view - [react/view {:flex-shrink 1} - [react/text {:style styles/name-text - :accessibility-label :chat-name-text - :number-of-lines 1} - name]]]] - [react/view styles/item-lower-container - [react/view styles/last-message-container - [react/text {:style styles/last-message-text - :accessibility-label :chat-url-text - :number-of-lines 1} - (or url (i18n/label :t/dapp))]]]]]])) diff --git a/src/status_im/ui/screens/intro/styles.cljs b/src/status_im/ui/screens/intro/styles.cljs deleted file mode 100644 index 2bce93a8e7e..00000000000 --- a/src/status_im/ui/screens/intro/styles.cljs +++ /dev/null @@ -1,41 +0,0 @@ -(ns status-im.ui.screens.intro.styles - (:require-macros [status-im.utils.styles :refer [defnstyle defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def intro-view - {:flex 1 - :padding-horizontal 30 - :background-color colors/white}) - -(def intro-logo-container - {:flex 1 - :align-items :center - :justify-content :center}) - -(def intro-logo - {:size 111 - :icon-size 46}) - -(defstyle intro-text - {:text-align :center - :color colors/black - :ios {:line-height 28 - :font-size 22 - :font-weight :bold - :letter-spacing -0.3} - :android {:font-size 24 - :line-height 30}}) - -(def intro-text-description - {:line-height 21 - :margin-top 8 - :margin-bottom 16 - :text-align :center - :color colors/gray}) - -(def buttons-container - {:align-items :center}) - -(def bottom-button-container - {:margin-bottom 6 - :margin-top 38}) diff --git a/src/status_im/ui/screens/intro/views.cljs b/src/status_im/ui/screens/intro/views.cljs deleted file mode 100644 index 142e602d808..00000000000 --- a/src/status_im/ui/screens/intro/views.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.ui.screens.intro.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.components.react :as react] - [re-frame.core :as re-frame] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.screens.intro.styles :as styles] - [status-im.i18n :as i18n] - [status-im.ui.components.status-bar.view :as status-bar])) - -(defview intro [] - [react/view {:style styles/intro-view} - [status-bar/status-bar {:flat? true}] - [react/view {:style styles/intro-logo-container} - [components.common/logo styles/intro-logo]] - [react/i18n-text {:style styles/intro-text - :key :intro-text}] - [react/view - [react/i18n-text {:style styles/intro-text-description - :key :intro-text-description}]] - [react/view styles/buttons-container - [components.common/button {:button-style {:flex-direction :row} - :on-press #(re-frame/dispatch [:navigate-to :create-account]) - :label (i18n/label :t/create-account)}] - [react/view styles/bottom-button-container - [components.common/button {:on-press #(re-frame/dispatch [:navigate-to :recover]) - :label (i18n/label :t/already-have-account) - :background? false}]]]]) diff --git a/src/status_im/ui/screens/main_tabs/styles.cljs b/src/status_im/ui/screens/main_tabs/styles.cljs deleted file mode 100644 index ac76855d935..00000000000 --- a/src/status_im/ui/screens/main_tabs/styles.cljs +++ /dev/null @@ -1,42 +0,0 @@ -(ns status-im.ui.screens.main-tabs.styles - (:require [status-im.ui.components.styles :as styles] - [status-im.utils.platform :as platform]) - (:require-macros [status-im.utils.styles :refer [defnstyle]])) - -(def tabs-height - (cond - platform/android? 56 - platform/ios? 52 - platform/desktop? 68)) - -(def tab-height (dec tabs-height)) - -(def tabs-container - {:flex-direction :row - :height tabs-height - :background-color styles/color-white - :border-top-width 1 - :border-top-color styles/color-light-gray3}) - -(def tab-container - {:height tabs-height - :justify-content :center - :align-items :center}) - -(defnstyle tab-title [active?] - {:ios {:font-size 11} - :android {:font-size 12} - :text-align :center - :color (if active? - styles/color-blue4 - styles/color-gray4)}) - -(defn tab-icon [active?] - {:color (if active? styles/color-blue4 styles/color-gray4)}) - -(def counter-container - {:position :absolute - :top 4}) - -(def counter - {:margin-left 18}) \ No newline at end of file diff --git a/src/status_im/ui/screens/main_tabs/views.cljs b/src/status_im/ui/screens/main_tabs/views.cljs deleted file mode 100644 index b5649a35b0e..00000000000 --- a/src/status_im/ui/screens/main_tabs/views.cljs +++ /dev/null @@ -1,80 +0,0 @@ -(ns status-im.ui.screens.main-tabs.views - (:require-macros [status-im.utils.views :as views]) - (:require [status-im.i18n :as i18n] - [re-frame.core :as re-frame] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar.view] - [status-im.ui.components.styles :as common.styles] - [status-im.ui.screens.home.views :as home] - [status-im.ui.screens.wallet.main.views :as wallet.main] - [status-im.ui.screens.main-tabs.styles :as styles] - [status-im.ui.screens.profile.user.views :as profile.user] - [status-im.ui.components.common.common :as components.common])) - -(def tabs-list-data - [{:view-id :home - :content {:title (i18n/label :t/home) - :icon-inactive :icons/home - :icon-active :icons/home-active} - :count-subscription :get-chats-unread-messages-number - :accessibility-label :home-tab-button} - {:view-id :wallet - :content {:title (i18n/label :t/wallet) - :icon-inactive :icons/wallet - :icon-active :icons/wallet-active} - :count-subscription :get-wallet-unread-messages-number - :accessibility-label :wallet-tab-button} - {:view-id :my-profile - :content {:title (i18n/label :t/profile) - :icon-inactive :icons/profile - :icon-active :icons/profile-active} - :count-subscription :get-profile-unread-messages-number - :accessibility-label :profile-tab-button}]) - -(defn- tab-content [{:keys [title icon-active icon-inactive]}] - (fn [active? count] - [react/view {:style styles/tab-container} - (let [icon (if active? icon-active icon-inactive)] - [react/view - [vector-icons/icon icon (styles/tab-icon active?)]]) - [react/view - [react/text {:style (styles/tab-title active?)} - title]] - (when (pos? count) - [react/view styles/counter-container - [react/view styles/counter - [components.common/counter count]]])])) - -(def tabs-list (map #(update % :content tab-content) tabs-list-data)) - -(views/defview tab [view-id content active? accessibility-label count-subscription] - (views/letsubs [count [count-subscription]] - [react/touchable-highlight - (cond-> {:style common.styles/flex - :disabled active? - :on-press #(re-frame/dispatch [:navigate-to-tab view-id])} - accessibility-label - (assoc :accessibility-label accessibility-label)) - [react/view - [content active? count]]])) - -(defn tabs [current-view-id] - [react/view {:style styles/tabs-container} - (for [{:keys [content view-id accessibility-label count-subscription]} tabs-list] - ^{:key view-id} [tab view-id content (= view-id current-view-id) accessibility-label count-subscription])]) - -(views/defview main-tabs [] - (views/letsubs [view-id [:get :view-id] - tab-bar-visible? [:tab-bar-visible?]] - [react/view common.styles/flex - [status-bar.view/status-bar {:type (if (= view-id :wallet) :wallet-tab :main)}] - [react/view common.styles/main-container - - (case view-id - :home [home/home] - :wallet [wallet.main/wallet] - :my-profile [profile.user/my-profile]) - - (when tab-bar-visible? - [tabs view-id])]])) diff --git a/src/status_im/ui/screens/navigation.cljs b/src/status_im/ui/screens/navigation.cljs deleted file mode 100644 index ca197287ae1..00000000000 --- a/src/status_im/ui/screens/navigation.cljs +++ /dev/null @@ -1,135 +0,0 @@ -(ns status-im.ui.screens.navigation - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro])) - -;; private helper fns - -(defn- push-view [db view-id] - (-> db - (update :navigation-stack conj view-id) - (assoc :view-id view-id))) - -(defn- replace-top-element [stack view-id] - (let [stack' (if (> 2 (count stack)) - (list :home) - (pop stack))] - (conj stack' view-id))) - -;; public fns - -(defn navigate-to-clean - ([view-id cofx] (navigate-to-clean view-id cofx nil)) - ([view-id {:keys [db]} screen-params] - ;; TODO (jeluard) Unify all :navigate-to flavours. Maybe accept a map of parameters? - - (let [db (cond-> (assoc db :navigation-stack (list)) - (seq screen-params) - (assoc-in [:navigation/screen-params view-id] screen-params))] - {:db (push-view db view-id)}))) - -(defn replace-view [view-id {:keys [db]}] - {:db (-> (update db :navigation-stack replace-top-element view-id) - (assoc :view-id view-id))}) - -(defn navigate-forget [view-id {:keys [db]}] - {:db (assoc db :view-id view-id)}) - -(defmulti unload-data! - (fn [db] (:view-id db))) - -(defmethod unload-data! :default [db] db) - -(defmulti preload-data! - (fn [db [_ view-id]] (or view-id (:view-id db)))) - -(defmethod preload-data! :default [db _] db) - -(defn- -preload-data! [{:keys [was-modal?] :as db} & args] - (if was-modal? - (dissoc db :was-modal?) ;;TODO check how it worked with this bug - (apply preload-data! db args))) - -(defn navigate-to-cofx [go-to-view-id screen-params {:keys [db]}] - (let [view-id (:view-id db) - db (cond-> db - (seq screen-params) - (assoc-in [:navigation/screen-params go-to-view-id] screen-params))] - {:db (if (= view-id go-to-view-id) - db - (push-view db go-to-view-id))})) - -(defn navigate-to - "DEPRECATED, use navigate-to-cofx above. - Navigates to particular view" - ([db go-to-view-id] - (navigate-to db go-to-view-id nil)) - ([{:keys [view-id] :as db} go-to-view-id screen-params] - (:db (navigate-to-cofx go-to-view-id screen-params {:db db})))) - -(def unload-data-interceptor - (re-frame/->interceptor - :id unload-data-interceptor - :before (fn unload-data-interceptor-before - [context] - (let [db (re-frame/get-coeffect context :db)] - (re-frame/assoc-coeffect context :db (unload-data! db)))))) - -(def navigation-interceptors - [unload-data-interceptor (re-frame/enrich preload-data!)]) - -;; event handlers - -(handlers/register-handler-db - :navigate-to - navigation-interceptors - (fn [db [_ & params]] - (apply navigate-to db params))) - -(handlers/register-handler-db - :navigate-to-modal - navigation-interceptors - (fn [db [_ modal-view]] - (assoc db :modal modal-view))) - -(handlers/register-handler-fx - :navigation-replace - navigation-interceptors - (fn [cofx [_ view-id]] - (replace-view view-id cofx))) - -(defn navigate-back [{:keys [navigation-stack view-id modal] :as db}] - (cond - modal (assoc db :modal nil - :was-modal? true) - (>= 1 (count navigation-stack)) db - - :else - (let [[previous-view-id :as navigation-stack'] (pop navigation-stack) - first-in-stack (first navigation-stack)] - (if (= view-id first-in-stack) - (-> db - (assoc :view-id previous-view-id) - (assoc :navigation-stack navigation-stack')) - (assoc db :view-id first-in-stack))))) - -(handlers/register-handler-db - :navigate-back - (re-frame/enrich -preload-data!) - (fn [db _] - (navigate-back db))) - -(handlers/register-handler-fx - :navigate-to-clean - (fn [cofx [_ view-id params]] - (navigate-to-clean view-id cofx params))) - -(handlers/register-handler-fx - :navigate-to-tab - navigation-interceptors - (fn [{:keys [db] :as cofx} [_ view-id]] - (handlers-macro/merge-fx cofx - {:db (-> db - (assoc :prev-tab-view-id (:view-id db)) - (assoc :prev-view-id (:view-id db)))} - (navigate-to-clean view-id)))) diff --git a/src/status_im/ui/screens/network_settings/db.cljs b/src/status_im/ui/screens/network_settings/db.cljs deleted file mode 100644 index d29c6316df7..00000000000 --- a/src/status_im/ui/screens/network_settings/db.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns status-im.ui.screens.network-settings.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec])) - -(spec/def :networks/id string?) -(spec/def :networks/name string?) -(spec/def :networks/config map?) - -(spec/def :networks/network - (spec/keys :req-un [:networks/id :networks/name :networks/config])) - -(spec/def :networks/selected-network :networks/network) - -(spec/def :networks/networks (spec/nilable (spec/map-of :networks/id :networks/network))) - -(spec/def :networks/manage (spec/nilable map?)) diff --git a/src/status_im/ui/screens/network_settings/edit_network/events.cljs b/src/status_im/ui/screens/network_settings/edit_network/events.cljs deleted file mode 100644 index dab316e4da8..00000000000 --- a/src/status_im/ui/screens/network_settings/edit_network/events.cljs +++ /dev/null @@ -1,20 +0,0 @@ -(ns status-im.ui.screens.network-settings.edit-network.events - (:require [re-frame.core :as re-frame] - [status-im.models.network :as models.network] - [status-im.utils.handlers :refer [register-handler] :as handlers])) - -(handlers/register-handler-fx - :save-new-network - [(re-frame/inject-cofx :random-id)] - (fn [cofx _] - (models.network/save cofx))) - -(handlers/register-handler-fx - :network-set-input - (fn [cofx [_ input-key value]] - (models.network/set-input input-key value cofx))) - -(handlers/register-handler-fx - :edit-network - (fn [cofx _] - (models.network/edit cofx))) diff --git a/src/status_im/ui/screens/network_settings/edit_network/styles.cljs b/src/status_im/ui/screens/network_settings/edit_network/styles.cljs deleted file mode 100644 index 3eea619bc82..00000000000 --- a/src/status_im/ui/screens/network_settings/edit_network/styles.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.ui.screens.network-settings.edit-network.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :as styles])) - -(def edit-network-view - {:flex 1 - :margin-horizontal 16 - :margin-vertical 15}) - -(def input-container - {:margin-bottom 15}) - -(def network-type - {:flex-direction :row - :align-items :center}) - -(defstyle network-type-text - {:color colors/black - :ios {:font-size 17 - :letter-spacing -0.2} - :android {:font-size 16}}) - -(def bottom-container - {:flex-direction :row - :margin-horizontal 12 - :margin-vertical 15}) diff --git a/src/status_im/ui/screens/network_settings/edit_network/subs.cljs b/src/status_im/ui/screens/network_settings/edit_network/subs.cljs deleted file mode 100644 index 00f88edf528..00000000000 --- a/src/status_im/ui/screens/network_settings/edit_network/subs.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.ui.screens.network-settings.edit-network.subs - (:require [re-frame.core :refer [reg-sub]])) - -(reg-sub - :get-manage-network - :<- [:get :networks/manage] - (fn [manage] - manage)) - -(reg-sub - :manage-network-valid? - :<- [:get-manage-network] - (fn [manage] - (not-any? :error (vals manage)))) diff --git a/src/status_im/ui/screens/network_settings/edit_network/views.cljs b/src/status_im/ui/screens/network_settings/edit_network/views.cljs deleted file mode 100644 index f2b03775150..00000000000 --- a/src/status_im/ui/screens/network_settings/edit_network/views.cljs +++ /dev/null @@ -1,70 +0,0 @@ -(ns status-im.ui.screens.network-settings.edit-network.views - (:require-macros [status-im.utils.views :as views]) - (:require - [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.i18n :as i18n] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.text-input.view :as text-input] - [status-im.ui.screens.network-settings.edit-network.styles :as styles] - [clojure.string :as string])) - -(defn- render-network-type [manage-network type] - (let [name (case type - :mainnet (i18n/label :t/mainnet-network) - :testnet (i18n/label :t/ropsten-network) - :rinkeby (i18n/label :t/rinkeby-network) - :custom (i18n/label :t/custom))] - [list/list-item-with-checkbox - {:checked? (= (get-in manage-network [:chain :value]) type) - :on-value-change #(re-frame/dispatch [:network-set-input :chain type]) - :plain-checkbox? true} - [list/item - nil [list/item-primary-only name]]])) - -(views/defview edit-network [] - (views/letsubs [manage-network [:get-manage-network] - is-valid? [:manage-network-valid?]] - (let [custom? (= (get-in manage-network [:chain :value]) :custom)] - [react/view components.styles/flex - [status-bar/status-bar] - [react/keyboard-avoiding-view components.styles/flex - [toolbar/simple-toolbar (i18n/label :t/add-network)] - [react/scroll-view - [react/view styles/edit-network-view - [text-input/text-input-with-label - {:label (i18n/label :t/name) - :placeholder (i18n/label :t/specify-name) - :container styles/input-container - :default-value (get-in manage-network [:name :value]) - :on-change-text #(re-frame/dispatch [:network-set-input :name %]) - :auto-focus true}] - [text-input/text-input-with-label - {:label (i18n/label :t/rpc-url) - :placeholder (i18n/label :t/specify-rpc-url) - :container styles/input-container - :default-value (get-in manage-network [:url :value]) - :on-change-text #(re-frame/dispatch [:network-set-input :url (string/lower-case %)])}] - [react/i18n-text {:key :network-chain}] - [react/view styles/network-type - [list/flat-list {:data [:mainnet :testnet :rinkeby :custom] - :key-fn (fn [_ i] (str i)) - :separator list/base-separator - :render-fn #(render-network-type manage-network %)}]] - (when custom? - [text-input/text-input-with-label - {:label (i18n/label :t/network-id) - :container styles/input-container - :placeholder (i18n/label :t/specify-network-id) - :on-change-text #(re-frame/dispatch [:network-set-input :network-id %])}])]] - [react/view styles/bottom-container - [react/view components.styles/flex] - [components.common/bottom-button - {:forward? true - :label (i18n/label :t/save) - :disabled? (not is-valid?) - :on-press #(re-frame/dispatch [:save-new-network])}]]]]))) diff --git a/src/status_im/ui/screens/network_settings/events.cljs b/src/status_im/ui/screens/network_settings/events.cljs deleted file mode 100644 index 2e918e9b4f8..00000000000 --- a/src/status_im/ui/screens/network_settings/events.cljs +++ /dev/null @@ -1,64 +0,0 @@ -(ns status-im.ui.screens.network-settings.events - (:require [re-frame.core :refer [dispatch dispatch-sync after] :as re-frame] - [status-im.utils.handlers :refer [register-handler] :as handlers] - status-im.ui.screens.network-settings.edit-network.events - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.ui.screens.accounts.utils :as accounts.utils] - [status-im.i18n :as i18n] - [status-im.utils.ethereum.core :as utils] - [status-im.transport.core :as transport] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.types :as types] - [clojure.string :as string])) - -;; handlers - -(handlers/register-handler-fx - :close-application - (fn [_ _] - {:close-application nil})) - -(handlers/register-handler-fx - ::save-network - (fn [{:keys [db now] :as cofx} [_ network]] - (handlers-macro/merge-fx cofx - (accounts.utils/account-update {:network network - :last-updated now} - [::close-application])))) - -(handlers/register-handler-fx - ::remove-network - (fn [{:keys [db now] :as cofx} [_ network]] - (let [networks (dissoc (get-in db [:account/account :networks]) network)] - (handlers-macro/merge-fx cofx - {:dispatch [:navigate-back]} - (accounts.utils/account-update {:networks networks - :last-updated now}))))) - -(handlers/register-handler-fx - :connect-network - (fn [{:keys [db now] :as cofx} [_ network]] - (let [current-network (get-in db [:account/account :networks (:network db)]) - chats (:transport/chats db)] - (if (utils/network-with-upstream-rpc? current-network) - (handlers-macro/merge-fx cofx - (accounts.utils/account-update {:network network - :last-updated now} - [:logout])) - {:show-confirmation {:title (i18n/label :t/close-app-title) - :content (i18n/label :t/close-app-content) - :confirm-button-text (i18n/label :t/close-app-button) - :on-accept #(dispatch [::save-network network]) - :on-cancel nil}})))) - -(handlers/register-handler-fx - :delete-network - (fn [{{:account/keys [account] :as db} :db :as cofx} [_ network]] - (let [current-network? (= (:network account) network)] - (if current-network? - {:show-error (i18n/label :t/delete-network-error)} - {:show-confirmation {:title (i18n/label :t/delete-network-title) - :content (i18n/label :t/delete-network-confirmation) - :confirm-button-text (i18n/label :t/delete) - :on-accept #(dispatch [::remove-network network]) - :on-cancel nil}})))) diff --git a/src/status_im/ui/screens/network_settings/network_details/views.cljs b/src/status_im/ui/screens/network_settings/network_details/views.cljs deleted file mode 100644 index 642c9a2ebfa..00000000000 --- a/src/status_im/ui/screens/network_settings/network_details/views.cljs +++ /dev/null @@ -1,50 +0,0 @@ -(ns status-im.ui.screens.network-settings.network-details.views - (:require-macros [status-im.utils.views :as views]) - (:require - [re-frame.core :as rf] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.react :as react] - [status-im.i18n :as i18n] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.screens.network-settings.styles :as st] - [status-im.ui.screens.network-settings.views :as network-settings] - [status-im.ui.components.colors :as colors])) - -(views/defview network-details [] - (views/letsubs [{:keys [networks/selected-network]} [:get-screen-params] - {:keys [network]} [:get-current-account] - networks [:get-networks]] - (let [{:keys [id name config]} selected-network - connected? (= id network) - custom? (seq (filter #(= (:id %) id) (:custom networks)))] - [react/view components.styles/flex - [status-bar/status-bar] - [react/view components.styles/flex - [toolbar/simple-toolbar (i18n/label :t/network-details)] - [react/view components.styles/flex - [network-settings/network-badge - {:name name - :connected? connected?}] - (when-not connected? - [react/touchable-highlight {:on-press #(rf/dispatch [:connect-network id])} - [react/view st/connect-button-container - [react/view {:style st/connect-button - :accessibility-label :network-connect-button} - [react/text {:style st/connect-button-label - :uppercase? true} - (i18n/label :t/connect)]] - [react/i18n-text {:style st/connect-button-description - :key :connecting-requires-login}]]]) - [react/view st/network-config-container - [react/text {:style st/network-config-text - :accessibility-label :network-details-text} - config]]] - (when custom? - [react/view st/bottom-container - [react/view components.styles/flex - [components.common/button {:label (i18n/label :t/delete) - :button-style st/delete-button - :label-style st/delete-button-text - :on-press #(rf/dispatch [:delete-network id])}]]])]]))) diff --git a/src/status_im/ui/screens/network_settings/styles.cljs b/src/status_im/ui/screens/network_settings/styles.cljs deleted file mode 100644 index 2cd18d0076f..00000000000 --- a/src/status_im/ui/screens/network_settings/styles.cljs +++ /dev/null @@ -1,148 +0,0 @@ -(ns status-im.ui.screens.network-settings.styles - (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def wrapper - {:flex 1 - :background-color :white}) - -(defstyle badge-name-text - {:color colors/black - :ios {:font-size 17 - :letter-spacing -0.2} - :android {:font-size 16}}) - -(defstyle badge-connected-text - {:color colors/gray - :ios {:margin-top 5} - :android {:font-size 13}}) - -(defstyle paste-json-text-input - {:ios {:font-size 17 - :line-height 24 - :letter-spacing -0.2}}) - -(def connect-button-container - {:margin-top 8 - :margin-bottom 16 - :margin-horizontal 16}) - -(defstyle connect-button - {:height 52 - :align-items :center - :justify-content :center - :background-color colors/blue - :border-radius 8 - :ios {:opacity 0.9}}) - -(defstyle connect-button-label - {:color colors/white - :ios {:font-size 17 - :letter-spacing -0.2} - :android {:font-size 14}}) - -(defstyle connect-button-description - {:color colors/gray - :ios {:margin-top 8 - :height 20} - :android {:margin-top 12 - :font-size 12}}) - -(defstyle network-config-container - {:height 160 - :margin-top 8 - :padding-top 16 - :padding-left 16 - :margin-horizontal 16 - :background-color colors/gray-lighter - :ios {:border-radius 9 - :opacity 0.9} - :android {:border-radius 4}}) - -(defstyle network-config-text - {:color colors/black - :ios {:opacity 0.8 - :font-size 17 - :line-height 24 - :letter-spacing -0.2} - :android {:opacity 0.4 - :font-size 16 - :line-height 24}}) - -(def edit-button-container - {:margin-top 16 - :align-items :center - :margin-bottom 16 - :margin-horizontal 16}) - -(defstyle edit-button - {:height 52 - :align-items :center - :justify-content :center - :background-color colors/blue - :border-radius 8 - :ios {:width 343} - - :android {:width 328}}) - -(defstyle edit-button-label - {:color colors/blue - :ios {:font-size 17 - :letter-spacing -0.2} - :android {:font-size 14}}) - -(defstyle edit-button-description - {:text-align :center - :color colors/gray - :ios {:margin-top 8 - :font-size 14 - :letter-spacing -0.2} - :android {:margin-top 12 - :font-size 12}}) - -(defn network-icon [connected? size] - {:width size - :height size - :border-radius (/ size 2) - :background-color (if connected? colors/blue colors/gray-lighter) - :align-items :center :justify-content :center}) - -(def network-badge - {:height 88 - :padding-left 16 - :flex-direction :row - :align-items :center}) - -(defstyle network-item - {:flex-direction :row - :background-color :white - :align-items :center - :padding-horizontal 16 - :ios {:height 64} - :android {:height 56}}) - -(defstyle network-item-name-text - {:color colors/black - :ios {:font-size 17 - :letter-spacing -0.2 - :line-height 20} - :android {:font-size 16}}) - -(defstyle network-item-connected-text - {:color colors/gray - :ios {:font-size 14 - :margin-top 6 - :letter-spacing -0.2} - :android {:font-size 12 - :margin-top 2}}) - -(def bottom-container - {:flex-direction :row - :margin-horizontal 12 - :margin-vertical 15}) - -(def delete-button - {:background-color colors/white}) - -(def delete-button-text - {:color colors/red}) diff --git a/src/status_im/ui/screens/network_settings/subs.cljs b/src/status_im/ui/screens/network_settings/subs.cljs deleted file mode 100644 index f69ad4c2a65..00000000000 --- a/src/status_im/ui/screens/network_settings/subs.cljs +++ /dev/null @@ -1,36 +0,0 @@ -(ns status-im.ui.screens.network-settings.subs - (:require [re-frame.core :refer [reg-sub subscribe]] - [status-im.utils.ethereum.core :as ethereum] - status-im.ui.screens.network-settings.edit-network.subs)) - -(reg-sub - :get-network-id - :<- [:network] - (fn [network] - (ethereum/network->chain-id network))) - -(defn- filter-networks [network-type] - (fn [network] - (let [chain-id (ethereum/network->chain-id network) - testnet? (ethereum/testnet? chain-id) - custom? (:custom? network)] - (case network-type - :custom custom? - :mainnet (and (not custom?) (not testnet?)) - :testnet (and (not custom?) testnet?))))) - -(defn- label-networks [default-networks] - (fn [network] - (let [custom? (not (contains? default-networks (:id network)))] - (assoc network :custom? custom?)))) - -(reg-sub - :get-networks - :<- [:get :account/account] - :<- [:get :networks/networks] - (fn [[{:keys [networks] :as account} default-networks]] - (let [networks (map (label-networks default-networks) (sort-by :name (vals networks))) - types [:mainnet :testnet :custom]] - (zipmap - types - (map #(filter (filter-networks %) networks) types))))) diff --git a/src/status_im/ui/screens/network_settings/views.cljs b/src/status_im/ui/screens/network_settings/views.cljs deleted file mode 100644 index b8f69c9e8e8..00000000000 --- a/src/status_im/ui/screens/network_settings/views.cljs +++ /dev/null @@ -1,74 +0,0 @@ -(ns status-im.ui.screens.network-settings.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [reagent.core :as reagent] - [status-im.i18n :as i18n] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.toolbar.actions :as toolbar.actions] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.screens.network-settings.styles :as styles])) - -(defn- network-icon [connected? size] - [react/view (styles/network-icon connected? size) - [vector-icons/icon :icons/network {:color (if connected? :white :gray)}]]) - -(defn network-badge [& [{:keys [name connected?]}]] - [react/view styles/network-badge - [network-icon connected? 56] - [react/view {:padding-left 16} - [react/text {:style styles/badge-name-text} - (or name (i18n/label :t/new-network))] - (when connected? - [react/i18n-text {:style styles/badge-connected-text - :key :connected}])]]) - -(def mainnet? - #{"mainnet" "mainnet_rpc"}) - -(defn navigate-to-network [network] - (re-frame/dispatch [:navigate-to :network-details {:networks/selected-network network}])) - -(defn navigate-to-add-network [] - (re-frame/dispatch [:edit-network])) - -(defn render-network [current-network] - (fn [{:keys [id name] :as network}] - (let [connected? (= id current-network)] - [list/touchable-item #(navigate-to-network network) - [react/view styles/network-item - [network-icon connected? 40] - [react/view {:padding-horizontal 16} - [react/text {:style styles/network-item-name-text} - name] - (when connected? - [react/text {:style styles/network-item-connected-text - :accessibility-label :connected-text} - (i18n/label :t/connected)])]]]))) - -(views/defview network-settings [] - (views/letsubs [{:keys [network]} [:get-current-account] - networks [:get-networks]] - [react/view components.styles/flex - [status-bar/status-bar] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/network-settings)] - [toolbar/actions - [(toolbar.actions/add false navigate-to-add-network)]]] - [react/view styles/wrapper - [list/section-list {:sections [{:title (i18n/label :t/main-networks) - :key :mainnet - :data (:mainnet networks)} - {:title (i18n/label :t/test-networks) - :key :testnet - :data (:testnet networks)} - {:title (i18n/label :t/custom-networks) - :key :custom - :data (:custom networks)}] - :key-fn :id - :default-separator? true - :render-fn (render-network network)}]]])) diff --git a/src/status_im/ui/screens/offline_messaging_settings/db.cljs b/src/status_im/ui/screens/offline_messaging_settings/db.cljs deleted file mode 100644 index 201234e706d..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/db.cljs +++ /dev/null @@ -1,21 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require - [status-im.models.mailserver :as models.mailserver] - [cljs.spec.alpha :as spec])) - -(spec/def ::not-blank-string (spec/and string? seq)) - -(spec/def :wnode/address (spec/and string? models.mailserver/valid-enode-address?)) -(spec/def :wnode/name ::not-blank-string) -(spec/def :wnode/id ::not-blank-string) -(spec/def :wnode/user-defined boolean?) -(spec/def :wnode/password ::not-blank-string) -(spec/def :wnode/sym-key-id string?) -(spec/def :wnode/wnode (allowed-keys :req-un [:wnode/address :wnode/name :wnode/id] - :opt-un [:wnode/sym-key-id - :wnode/user-defined - :wnode/password])) - -(spec/def :inbox/wnodes (spec/nilable (spec/map-of keyword? (spec/map-of :wnode/id :wnode/wnode)))) -(spec/def :inbox/last-received integer?) diff --git a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/events.cljs b/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/events.cljs deleted file mode 100644 index dfd04e0aca7..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/events.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.edit-mailserver.events - (:require [re-frame.core :as re-frame] - [status-im.models.mailserver :as models.mailserver] - [status-im.utils.handlers :refer [register-handler] :as handlers])) - -(handlers/register-handler-fx - :upsert-mailserver - [(re-frame/inject-cofx :random-id)] - (fn [cofx _] - (models.mailserver/upsert cofx))) - -(handlers/register-handler-fx - :mailserver-set-input - (fn [cofx [_ input-key value]] - (models.mailserver/set-input input-key value cofx))) - -(handlers/register-handler-fx - :edit-mailserver - (fn [cofx [_ mailserver-id]] - (models.mailserver/edit mailserver-id cofx))) - -(handlers/register-handler-fx - :delete-mailserver - (fn [cofx [_ mailserver-id]] - (assoc (models.mailserver/delete mailserver-id cofx) - :dispatch [:navigate-back]))) - -(handlers/register-handler-fx - :set-mailserver-from-qr - (fn [cofx [_ _ url]] - (assoc (models.mailserver/set-input :url url cofx) - :dispatch [:navigate-back]))) diff --git a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/styles.cljs b/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/styles.cljs deleted file mode 100644 index 843037b526a..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/styles.cljs +++ /dev/null @@ -1,57 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.edit-mailserver.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.ui.components.colors :as colors])) - -(def edit-mailserver-view - {:flex 1 - :margin-horizontal 16 - :margin-vertical 15}) - -(def input-container - {:flex-direction :row - :align-items :center - :justify-content :space-between - :border-radius styles/border-radius - :height 52 - :margin-top 15}) - -(defstyle input - {:flex 1 - :font-size 15 - :letter-spacing -0.2 - :android {:padding 0}}) - -(def qr-code - {:margin-right 14}) - -(def bottom-container - {:flex-direction :row - :margin-horizontal 12 - :margin-vertical 15}) - -(def button-container - {:margin-top 8 - :margin-bottom 16 - :margin-horizontal 16}) - -(def button - {:height 52 - :align-items :center - :justify-content :center - :border-radius 8 - :ios {:opacity 0.9}}) - -(defstyle connect-button - (assoc button - :background-color colors/blue)) - -(defstyle delete-button - (assoc button - :background-color colors/red)) - -(defstyle button-label - {:color colors/white - :ios {:font-size 17 - :letter-spacing -0.2} - :android {:font-size 14}}) diff --git a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/subs.cljs b/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/subs.cljs deleted file mode 100644 index f9dfa42a4bb..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/subs.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.edit-mailserver.subs - (:require - [re-frame.core :refer [reg-sub]] - [status-im.models.mailserver :as models.mailserver])) - -(reg-sub - :get-manage-mailserver - :<- [:get :mailservers/manage] - (fn [manage] - manage)) - -(reg-sub - :get-connected-mailserver - (fn [db] - (models.mailserver/connected? (get-in db [:mailservers/manage :id :value]) - {:db db}))) - -(reg-sub - :manage-mailserver-valid? - :<- [:get-manage-mailserver] - (fn [manage] - (not-any? :error (vals manage)))) diff --git a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/views.cljs b/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/views.cljs deleted file mode 100644 index 1332cd7ecff..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/edit_mailserver/views.cljs +++ /dev/null @@ -1,90 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.edit-mailserver.views - (:require-macros [status-im.utils.views :as views]) - (:require - [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.i18n :as i18n] - [status-im.utils.utils :as utils] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.text-input.view :as text-input] - [status-im.ui.screens.offline-messaging-settings.edit-mailserver.styles :as styles])) - -(defn handle-delete [id] - (utils/show-confirmation (i18n/label :t/delete-mailserver-title) - (i18n/label :t/delete-mailserver-are-you-sure) - (i18n/label :t/delete-mailserver) - #(re-frame/dispatch [:delete-mailserver id]))) - -(defn connect-button [id] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:connect-wnode id])} - [react/view styles/button-container - [react/view {:style styles/connect-button - :accessibility-label :mailserver-connect-button} - [react/text {:style styles/button-label - :uppercase? true} - (i18n/label :t/connect)]]]]) - -(defn delete-button [id] - [react/touchable-highlight {:on-press #(handle-delete id)} - [react/view styles/button-container - [react/view {:style styles/delete-button - :accessibility-label :mailserver-delete-button} - [react/text {:style styles/button-label - :uppercase? true} - (i18n/label :t/delete)]]]]) - -(def qr-code - [react/touchable-highlight {:on-press #(re-frame/dispatch [:scan-qr-code - {:toolbar-title (i18n/label :t/add-mailserver)} - :set-mailserver-from-qr]) - :style styles/qr-code} - [react/view - [vector-icons/icon :icons/qr {:color colors/blue}]]]) - -(views/defview edit-mailserver [] - (views/letsubs [manage-mailserver [:get-manage-mailserver] - connected? [:get-connected-mailserver] - is-valid? [:manage-mailserver-valid?]] - (let [url (get-in manage-mailserver [:url :value]) - id (get-in manage-mailserver [:id :value]) - name (get-in manage-mailserver [:name :value])] - [react/view components.styles/flex - [status-bar/status-bar] - [react/keyboard-avoiding-view components.styles/flex - [toolbar/simple-toolbar (i18n/label (if id :t/mailserver-details :t/add-mailserver))] - [react/scroll-view {:keyboard-should-persist-taps :handled} - [react/view styles/edit-mailserver-view - [text-input/text-input-with-label - {:label (i18n/label :t/name) - :placeholder (i18n/label :t/specify-name) - :style styles/input - :container styles/input-container - :default-value name - :on-change-text #(re-frame/dispatch [:mailserver-set-input :name %]) - :auto-focus true}] - [text-input/text-input-with-label - {:label (i18n/label :t/mailserver-address) - :placeholder (i18n/label :t/specify-mailserver-address) - :content qr-code - :style styles/input - :container styles/input-container - :default-value url - :on-change-text #(re-frame/dispatch [:mailserver-set-input :url %])}] - (when (and id - (not connected?)) - [react/view - [connect-button id] - [delete-button id]])]] - [react/view styles/bottom-container - [react/view components.styles/flex] - [components.common/bottom-button - {:forward? true - :label (i18n/label :t/save) - :disabled? (not is-valid?) - :on-press #(re-frame/dispatch [:upsert-mailserver])}]]]]))) diff --git a/src/status_im/ui/screens/offline_messaging_settings/events.cljs b/src/status_im/ui/screens/offline_messaging_settings/events.cljs deleted file mode 100644 index 84fb25621c0..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/events.cljs +++ /dev/null @@ -1,30 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.events - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.ui.screens.accounts.models :as accounts.models] - [status-im.i18n :as i18n] - status-im.ui.screens.offline-messaging-settings.edit-mailserver.events - [status-im.utils.ethereum.core :as ethereum])) - -(handlers/register-handler-fx - ::save-wnode - (fn [{:keys [db now] :as cofx} [_ chain wnode]] - (let [settings (get-in db [:account/account :settings])] - (handlers-macro/merge-fx cofx - (accounts.models/update-settings - (assoc-in settings [:wnode chain] wnode) - [:logout]))))) - -(handlers/register-handler-fx - :connect-wnode - (fn [{:keys [db]} [_ wnode]] - (let [network (get (:networks (:account/account db)) (:network db)) - chain (ethereum/network->chain-keyword network)] - {:show-confirmation {:title (i18n/label :t/close-app-title) - :content (i18n/label :t/connect-wnode-content - {:name (get-in db [:inbox/wnodes chain wnode :name])}) - :confirm-button-text (i18n/label :t/close-app-button) - :on-accept #(re-frame/dispatch [::save-wnode chain wnode]) - :on-cancel nil}}))) - diff --git a/src/status_im/ui/screens/offline_messaging_settings/styles.cljs b/src/status_im/ui/screens/offline_messaging_settings/styles.cljs deleted file mode 100644 index 9c543a35699..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/styles.cljs +++ /dev/null @@ -1,43 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.styles - (:require [status-im.ui.components.colors :as colors]) - (:require-macros [status-im.utils.styles :refer [defstyle]])) - -(def wrapper - {:flex 1 - :background-color :white}) - -(def wnode-item-inner - {:padding-horizontal 16}) - -(defstyle wnode-item - {:flex-direction :row - :background-color :white - :align-items :center - :padding-horizontal 16 - :ios {:height 64} - :android {:height 56}}) - -(defstyle wnode-item-name-text - {:color colors/black - :ios {:font-size 17 - :letter-spacing -0.2 - :line-height 20} - :android {:font-size 16}}) - -(defstyle wnode-item-connected-text - {:color colors/gray - :ios {:font-size 14 - :margin-top 6 - :letter-spacing -0.2} - :android {:font-size 12 - :margin-top 2}}) - -(defn wnode-icon [connected?] - {:width 40 - :height 40 - :border-radius 20 - :background-color (if connected? - colors/blue - colors/gray-light) - :align-items :center - :justify-content :center}) diff --git a/src/status_im/ui/screens/offline_messaging_settings/subs.cljs b/src/status_im/ui/screens/offline_messaging_settings/subs.cljs deleted file mode 100644 index a10be1d26fb..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/subs.cljs +++ /dev/null @@ -1,15 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.subs - (:require [re-frame.core :as re-frame] - status-im.ui.screens.offline-messaging-settings.edit-mailserver.subs - [status-im.utils.ethereum.core :as ethereum])) - -(re-frame/reg-sub :settings/current-wnode - (fn [db _] - (:inbox/current-id db))) - -(re-frame/reg-sub :settings/network-wnodes - :<- [:network] - :<- [:get :inbox/wnodes] - (fn [[network wnodes]] - (let [chain (ethereum/network->chain-keyword network)] - (chain wnodes)))) diff --git a/src/status_im/ui/screens/offline_messaging_settings/views.cljs b/src/status_im/ui/screens/offline_messaging_settings/views.cljs deleted file mode 100644 index 8fda592cf3b..00000000000 --- a/src/status_im/ui/screens/offline_messaging_settings/views.cljs +++ /dev/null @@ -1,53 +0,0 @@ -(ns status-im.ui.screens.offline-messaging-settings.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.utils.config :as config] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.toolbar.actions :as toolbar.actions] - [status-im.ui.screens.offline-messaging-settings.styles :as styles])) - -(defn- wnode-icon [connected?] - [react/view (styles/wnode-icon connected?) - [vector-icons/icon :icons/wnode {:color (if connected? :white :gray)}]]) - -(defn connect-to-mailserver [id] - (re-frame/dispatch [:connect-wnode id])) - -(defn navigate-to-add-mailserver [wnode-id] - (re-frame/dispatch [:edit-mailserver wnode-id])) - -(defn- render-row [current-wnode-id] - (fn [{:keys [name id user-defined]}] - (let [connected? (= id current-wnode-id)] - [react/touchable-highlight - {:on-press #(if user-defined - (navigate-to-add-mailserver id) - (connect-to-mailserver id)) - :accessibility-label :mailserver-item} - [react/view styles/wnode-item - [wnode-icon connected?] - [react/view styles/wnode-item-inner - [react/text {:style styles/wnode-item-name-text} - name]]]]))) - -(views/defview offline-messaging-settings [] - (views/letsubs [current-wnode-id [:settings/current-wnode] - wnodes [:settings/network-wnodes]] - [react/view {:flex 1} - [status-bar/status-bar] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/offline-messaging-settings)] - (when config/add-custom-mailservers-enabled? - [toolbar/actions - [(toolbar.actions/add false (partial navigate-to-add-mailserver nil))]])] - [react/view styles/wrapper - [list/flat-list {:data (vals wnodes) - :default-separator? false - :key-fn :id - :render-fn (render-row current-wnode-id)}]]])) diff --git a/src/status_im/ui/screens/profile/components/styles.cljs b/src/status_im/ui/screens/profile/components/styles.cljs deleted file mode 100644 index c159a4c1ab5..00000000000 --- a/src/status_im/ui/screens/profile/components/styles.cljs +++ /dev/null @@ -1,93 +0,0 @@ -(ns status-im.ui.screens.profile.components.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.styles :as styles] - [status-im.ui.components.colors :as colors])) - -;; profile header elements - -(def profile-header-display - {:flex-direction :column - :justify-content :center - :align-items :center}) - -(def profile-header-edit - {:flex-direction :column - :justify-content :center}) - -(defstyle profile-name-text - {:padding-vertical 14 - :font-size 15 - :text-align :center - :ios {:letter-spacing -0.2} - :android {:color colors/black}}) - -(defstyle profile-name-input-text - {:font-size 15 - :text-align :center - :flex 1 - :ios {:letter-spacing -0.2 - :height 46 - :border-bottom-width 1 - :border-bottom-color colors/gray-light} - :android {:color colors/black - :border-bottom-width 2 - :border-bottom-color colors/blue}}) - -(def profile-header-name-container - {:flex 1 - :justify-content :center}) - -;; settings items elements - -(def settings-item-separator - {:margin-left 16}) - -(def settings-item - {:padding-horizontal 16 - :flex 1 - :flex-direction :row - :align-items :center - :background-color colors/white - :height 52}) - -(def settings-item-text-wrapper - {:flex 1 - :flex-direction :row - :justify-content :space-between}) - -(defstyle settings-item-text - {:flex-wrap :nowrap - :font-size 15 - :android {:color colors/black}}) - -(def settings-item-destructive - {:color colors/red}) - -(def settings-item-value - {:flex 1 - :flex-wrap :nowrap - :text-align :right - :padding-right 10 - :font-size 15 - :color colors/gray}) - -(defstyle settings-title - {:color colors/gray - :margin-left 16 - :margin-top 18 - :font-size 14 - :ios {:letter-spacing -0.2}}) - -;; shared profile styles - -(def modal-menu - {:align-items :center}) - -(def profile - {:flex 1 - :background-color colors/white - :flex-direction :column}) - -(def profile-form - {:background-color colors/white - :padding 16}) diff --git a/src/status_im/ui/screens/profile/components/views.cljs b/src/status_im/ui/screens/profile/components/views.cljs deleted file mode 100644 index 22798fed149..00000000000 --- a/src/status_im/ui/screens/profile/components/views.cljs +++ /dev/null @@ -1,98 +0,0 @@ -(ns status-im.ui.screens.profile.components.views - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.chat-icon.screen :as chat-icon.screen] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.common.common :as common] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.react :as react] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.screens.profile.components.styles :as styles])) - -;; profile header elements - -(defn- profile-name-input [name on-change-text-event & [props]] - [react/view - [react/text-input - (merge {:style styles/profile-name-input-text - :placeholder "" - :default-value name - :auto-focus true - :on-change-text #(when on-change-text-event - (re-frame/dispatch [on-change-text-event %])) - :accessibility-label :username-input} - props)]]) - -(defn- show-profile-icon-actions [options] - (when (seq options) - (list-selection/show {:title (i18n/label :t/image-source-title) - :options options}))) - -(defn- profile-header-display [{:keys [name] :as contact}] - [react/view styles/profile-header-display - [chat-icon.screen/my-profile-icon {:account contact - :edit? false}] - [react/view styles/profile-header-name-container - [react/text {:style styles/profile-name-text - :number-of-lines 1} - name]]]) - -(defn- profile-header-edit [{:keys [name group-chat] :as contact} - icon-options on-change-text-event allow-icon-change?] - [react/view styles/profile-header-edit - [react/touchable-highlight {:on-press #(show-profile-icon-actions icon-options) - :accessibility-label :edit-profile-photo-button} - [react/view styles/modal-menu - [chat-icon.screen/my-profile-icon {:account contact - :edit? allow-icon-change?}]]] - [react/view styles/profile-header-name-container - [profile-name-input name on-change-text-event - (when group-chat {:accessibility-label :chat-name-input})]]]) - -(defn profile-header - [{:keys [contact edited-contact editing? allow-icon-change? options on-change-text-event]}] - (if editing? - [profile-header-edit (or edited-contact contact) options on-change-text-event allow-icon-change?] - [profile-header-display contact])) - -;; settings items elements - -(defn settings-item-separator [] - [common/separator styles/settings-item-separator]) - -(defn settings-title [title] - [react/text {:style styles/settings-title} - title]) - -(defn settings-item [{:keys [label-kw value action-fn active? destructive? hide-arrow? accessibility-label icon-content] - :or {value "" active? true}}] - [react/touchable-highlight - (cond-> {:on-press action-fn - :disabled (not active?)} - accessibility-label - (assoc :accessibility-label accessibility-label)) - [react/view styles/settings-item - [react/view styles/settings-item-text-wrapper - [react/text {:style (merge styles/settings-item-text - (when destructive? styles/settings-item-destructive)) - :number-of-lines 1} - (i18n/label label-kw)] - (when-not (string/blank? value) - [react/text {:style styles/settings-item-value - :number-of-lines 1 - :uppercase? true} - value])] - (if icon-content - icon-content - (when (and active? (not hide-arrow?)) - [vector-icons/icon :icons/forward {:color colors/gray}]))]]) - -(defn settings-switch-item [{:keys [label-kw value action-fn active?] :or {active? true}}] - [react/view styles/settings-item - [react/view styles/settings-item-text-wrapper - [react/i18n-text {:style styles/settings-item-text :key label-kw}]] - [react/switch {:on-tint-color colors/blue - :value value - :on-value-change action-fn - :disabled (not active?)}]]) diff --git a/src/status_im/ui/screens/profile/contact/styles.cljs b/src/status_im/ui/screens/profile/contact/styles.cljs deleted file mode 100644 index 13242bce1d4..00000000000 --- a/src/status_im/ui/screens/profile/contact/styles.cljs +++ /dev/null @@ -1,56 +0,0 @@ -(ns status-im.ui.screens.profile.contact.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def network-info {:background-color :white}) - -(defstyle profile-info-item - {:flex-direction :row - :align-items :center - :padding-left 16}) - -(defn profile-info-text-container [options] - {:flex 1 - :padding-right (if options 16 40)}) - -(defstyle profile-info-title - {:color colors/gray - :font-size 14 - :ios {:letter-spacing -0.2}}) - -(defstyle profile-setting-spacing - {:ios {:height 10} - :android {:height 7}}) - -(defstyle profile-setting-text - {:ios {:font-size 17 - :letter-spacing -0.2} - :android {:font-size 16 - :color colors/black}}) - -(def action-container - {:background-color colors/white - :padding-top 24}) - -(def action - {:background-color (colors/alpha colors/blue 0.1) - :border-radius 50}) - -(def action-label - {:color colors/blue}) - -(def action-separator - {:height 1 - :background-color colors/gray-light - :margin-left 50}) - -(def action-icon-opts - {:color colors/blue}) - -(def profile-setting-text-empty - (merge profile-setting-text - {:color colors/gray})) - -(defstyle contact-profile-info-container - {:padding-top 26 - :background-color colors/white}) diff --git a/src/status_im/ui/screens/profile/contact/views.cljs b/src/status_im/ui/screens/profile/contact/views.cljs deleted file mode 100644 index 5fe65142996..00000000000 --- a/src/status_im/ui/screens/profile/contact/views.cljs +++ /dev/null @@ -1,79 +0,0 @@ -(ns status-im.ui.screens.profile.contact.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.screens.profile.contact.styles :as styles] - [status-im.ui.components.react :as react] - [status-im.ui.screens.profile.components.styles :as profile.components.styles] - [status-im.ui.screens.profile.components.views :as profile.components] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.i18n :as i18n] - [re-frame.core :as re-frame] - [status-im.utils.contacts :as utils.contacts] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.text :as text])) - -(defn profile-contact-toolbar [] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title ""]]) - -(defn actions [{:keys [pending? whisper-identity dapp?]}] - (concat (if (or (nil? pending?) pending?) - [{:label (i18n/label :t/add-to-contacts) - :icon :icons/add-contact - :action #(re-frame/dispatch [:add-contact whisper-identity]) - :accessibility-label :add-to-contacts-button}] - [{:label (i18n/label :t/in-contacts) - :icon :icons/in-contacts - :disabled? true - :accessibility-label :in-contacts-button}]) - [{:label (i18n/label :t/send-message) - :icon :icons/chats - :action #(re-frame/dispatch [:open-chat-with-contact {:whisper-identity whisper-identity}]) - :accessibility-label :start-conversation-button}] - (when-not dapp? - [{:label (i18n/label :t/send-transaction) - :icon :icons/arrow-right - :action #(re-frame/dispatch [:profile/send-transaction whisper-identity]) - :accessibility-label :send-transaction-button}]))) - -(defn profile-info-item [{:keys [label value options accessibility-label]}] - [react/view styles/profile-info-item - [react/view (styles/profile-info-text-container options) - [react/text {:style styles/profile-info-title} - label] - [react/view styles/profile-setting-spacing] - [text/selectable-text {:value value - :style styles/profile-setting-text}]]]) - -(defn profile-info-contact-code-item [whisper-identity] - [profile-info-item - {:label (i18n/label :t/contact-code) - :accessibility-label :profile-public-key - :value whisper-identity}]) - -(defn profile-info [{:keys [whisper-identity]}] - [react/view - [profile-info-contact-code-item whisper-identity]]) - -(defview profile [] - (letsubs [identity [:get-current-contact-identity] - maybe-contact [:get-current-contact]] - (let [contact (or maybe-contact (utils.contacts/whisper-id->new-contact identity))] - [react/view profile.components.styles/profile - [status-bar/status-bar] - [profile-contact-toolbar] - [react/scroll-view - [react/view profile.components.styles/profile-form - [profile.components/profile-header - {:contact contact - :editing? false - :allow-icon-change? false}]] - [list/action-list (actions contact) - {:container-style styles/action-container - :action-style styles/action - :action-label-style styles/action-label - :action-separator-style styles/action-separator - :icon-opts styles/action-icon-opts}] - [react/view styles/contact-profile-info-container - [profile-info contact]]]]))) diff --git a/src/status_im/ui/screens/profile/db.cljs b/src/status_im/ui/screens/profile/db.cljs deleted file mode 100644 index 57c5e865963..00000000000 --- a/src/status_im/ui/screens/profile/db.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.ui.screens.profile.db - (:require [cljs.spec.alpha :as spec] - [clojure.string :as string] - [status-im.chat.constants :as chat.constants] - [status-im.constants :as constants] - [status-im.utils.homoglyph :as homoglyph])) - -(defn correct-name? [username] - (when-let [username (some-> username (string/trim))] - (every? false? - [(string/blank? username) - (homoglyph/matches username constants/console-chat-id) - (string/includes? username chat.constants/command-char)]))) - -(defn base64-encoded-image-path? [photo-path] - (or (string/starts-with? photo-path "data:image/jpeg;base64,") - (string/starts-with? photo-path "data:image/png;base64,"))) - -(spec/def :profile/name correct-name?) -(spec/def :profile/status (spec/nilable string?)) -(spec/def :profile/photo-path (spec/nilable base64-encoded-image-path?)) - -(spec/def :my-profile/default-name (spec/nilable string?)) -(spec/def :my-profile/editing? (spec/nilable boolean?)) -(spec/def :my-profile/advanced? (spec/nilable boolean?)) -(spec/def :my-profile/seed (spec/nilable map?)) -(spec/def :my-profile/profile (spec/keys :opt-un [::name :profile/status :profile/photo-path - ::edit-status? ::valid-name?])) diff --git a/src/status_im/ui/screens/profile/events.cljs b/src/status_im/ui/screens/profile/events.cljs deleted file mode 100644 index 0760f9e48f2..00000000000 --- a/src/status_im/ui/screens/profile/events.cljs +++ /dev/null @@ -1,80 +0,0 @@ -(ns status-im.ui.screens.profile.events - (:require [re-frame.core :as re-frame] - [status-im.ui.screens.profile.navigation] - [status-im.utils.handlers :as handlers] - [status-im.ui.screens.profile.models :as profile.models])) - -(re-frame/reg-fx - :open-image-picker - ;; the image picker is only used here for now, this effect can be use in other scenarios as well - (fn [callback-event] - (profile.models/open-image-picker! callback-event))) - -(handlers/register-handler-fx - :profile/send-transaction - (fn [cofx [_ chat-id]] - (profile.models/send-transaction chat-id cofx))) - -(handlers/register-handler-fx - :my-profile/update-name - (fn [cofx [_ name]] - (profile.models/update-name name cofx))) - -(handlers/register-handler-fx - :my-profile/update-picture - (fn [cofx [this-event base64-image]] - (profile.models/update-picture this-event base64-image cofx))) - -(handlers/register-handler-fx - :my-profile/start-editing-profile - (fn [cofx _] - (profile.models/start-editing cofx))) - -(handlers/register-handler-fx - :my-profile/save-profile - (fn [cofx _] - (profile.models/save cofx))) - -(handlers/register-handler-fx - :group-chat-profile/start-editing - (fn [cofx _] - (profile.models/start-editing-group-chat-profile cofx))) - -(handlers/register-handler-fx - :group-chat-profile/save-profile - (fn [cofx _] - (profile.models/save-group-chat-profile cofx))) - -(handlers/register-handler-fx - :my-profile/enter-two-random-words - (fn [cofx _] - (profile.models/enter-two-random-words cofx))) - -(handlers/register-handler-fx - :my-profile/set-step - (fn [cofx [_ step]] - (profile.models/set-step step cofx))) - -(handlers/register-handler-fx - :my-profile/finish - (fn [cofx _] - (profile.models/finish cofx))) - -(re-frame/reg-fx - :copy-to-clipboard - (fn [value] - (profile.models/copy-to-clipboard! value))) - -(handlers/register-handler-fx - :copy-to-clipboard - (fn [_ [_ value]] - {:copy-to-clipboard value})) - -(re-frame/reg-fx - :show-tooltip - profile.models/show-tooltip!) - -(handlers/register-handler-fx - :show-tooltip - (fn [_ [_ tooltip-id]] - {:show-tooltip tooltip-id})) diff --git a/src/status_im/ui/screens/profile/group_chat/styles.cljs b/src/status_im/ui/screens/profile/group_chat/styles.cljs deleted file mode 100644 index c459c3d3023..00000000000 --- a/src/status_im/ui/screens/profile/group_chat/styles.cljs +++ /dev/null @@ -1,21 +0,0 @@ -(ns status-im.ui.screens.profile.group-chat.styles - (:require [status-im.ui.components.colors :as colors])) - -(def action-container - {:background-color colors/white - :padding-top 24}) - -(def action - {:background-color (colors/alpha colors/blue 0.1) - :border-radius 50}) - -(def action-label - {:color colors/blue}) - -(def action-separator - {:height 1 - :background-color colors/gray-light - :margin-left 50}) - -(def action-icon-opts - {:color colors/blue}) \ No newline at end of file diff --git a/src/status_im/ui/screens/profile/group_chat/views.cljs b/src/status_im/ui/screens/profile/group_chat/views.cljs deleted file mode 100644 index 8d70f7d918c..00000000000 --- a/src/status_im/ui/screens/profile/group_chat/views.cljs +++ /dev/null @@ -1,115 +0,0 @@ -(ns status-im.ui.screens.profile.group-chat.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.screens.profile.group-chat.styles :as styles] - [status-im.ui.components.react :as react] - [status-im.ui.screens.profile.components.styles :as profile.components.styles] - [status-im.ui.screens.profile.components.views :as profile.components] - [status-im.ui.components.contact.contact :as contact] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [re-frame.core :as re-frame] - [status-im.ui.components.common.styles :as common.styles] - [status-im.i18n :as i18n] - [status-im.utils.utils :as utils])) - -(defn group-chat-profile-toolbar [] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title ""] - [react/touchable-highlight - {:on-press #(re-frame/dispatch [:group-chat-profile/start-editing])} - [react/view - [react/text {:style common.styles/label-action-text - :uppercase? true - :accessibility-label :edit-button} - (i18n/label :t/edit)]]]]) - -(defn group-chat-profile-edit-toolbar [] - [toolbar/toolbar {} - nil - [toolbar/content-title ""] - [toolbar/default-done {:handler #(re-frame/dispatch [:group-chat-profile/save-profile]) - :icon :icons/ok - :icon-opts {:color colors/blue - :accessibility-label :done-button}}]]) - -(defn actions [admin? chat-id] - (concat - (when admin? - [{:label (i18n/label :add-members) - :icon :icons/add - :action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}]) - [{:label (i18n/label :t/clear-history) - :icon :icons/close - :action #(utils/show-confirmation (i18n/label :t/clear-history-title) - (i18n/label :t/clear-history-confirmation-content) - (i18n/label :t/clear-history-action) - (fn [] (re-frame/dispatch [:clear-history]))) - :accessibility-label :clear-history-button} - {:label (i18n/label :t/delete-chat) - :icon :icons/arrow-left - :action #(utils/show-confirmation (i18n/label :t/leave-group-title) - (i18n/label :t/leave-group-confirmation) - (i18n/label :t/leave-group-action) - (fn [] (re-frame/dispatch [:remove-chat-and-navigate-home chat-id]))) - :accessibility-label :delete-chat-button}])) - -(defn contact-actions [contact] - [{:action #(re-frame/dispatch [:show-profile (:whisper-identity contact)]) - :label (i18n/label :t/view-profile)} - {:action #(re-frame/dispatch [:remove-group-chat-participants #{(:whisper-identity contact)}]) - :label (i18n/label :t/remove-from-chat)}]) - -(defn render-contact [{:keys [name whisper-identity] :as contact} admin? group-admin-identity current-user-identity] - [react/view - [contact/contact-view - {:contact contact - :extend-options (contact-actions contact) - :extend-title name - :extended? (and admin? (not= whisper-identity group-admin-identity)) - :accessibility-label :member-item - :inner-props {:accessibility-label :member-name-text} - :on-press (when (not= whisper-identity current-user-identity) - #(re-frame/dispatch [:show-profile whisper-identity]))}]]) - -(defview chat-group-contacts-view [admin? group-admin-identity current-user-identity] - (letsubs [contacts [:get-current-chat-contacts]] - [react/view - [list/flat-list {:data contacts - :separator list/default-separator - :key-fn :address - :render-fn #(render-contact % admin? group-admin-identity current-user-identity)}]])) - -(defn members-list [admin? group-admin-identity current-user-identity] - [react/view - [profile.components/settings-title (i18n/label :t/members-title)] - [chat-group-contacts-view admin? group-admin-identity current-user-identity]]) - -(defview group-chat-profile [] - (letsubs [{:keys [group-admin] :as current-chat} [:get-current-chat] - editing? [:get :group-chat-profile/editing?] - changed-chat [:get :group-chat-profile/profile] - current-pk [:get :current-public-key]] - (let [shown-chat (merge current-chat changed-chat) - admin? (= current-pk group-admin)] - [react/view profile.components.styles/profile - [status-bar/status-bar] - (if editing? - [group-chat-profile-edit-toolbar] - [group-chat-profile-toolbar]) - [react/scroll-view - [react/view profile.components.styles/profile-form - [profile.components/profile-header - {:contact shown-chat - :editing? editing? - :allow-icon-change? false - :on-change-text-event :set-group-chat-name}] - [list/action-list (actions admin? (:chat-id current-chat)) - {:container-style styles/action-container - :action-style styles/action - :action-label-style styles/action-label - :action-separator-style styles/action-separator - :icon-opts styles/action-icon-opts}] - [members-list admin? group-admin current-pk]]]]))) diff --git a/src/status_im/ui/screens/profile/models.cljs b/src/status_im/ui/screens/profile/models.cljs deleted file mode 100644 index 9b9d7c051f3..00000000000 --- a/src/status_im/ui/screens/profile/models.cljs +++ /dev/null @@ -1,116 +0,0 @@ -(ns status-im.ui.screens.profile.models - (:require [clojure.spec.alpha :as spec] - [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.ui.screens.profile.navigation] - [status-im.ui.screens.accounts.utils :as accounts.utils] - [status-im.chat.events :as chat-events] - [status-im.chat.commands.core :as commands] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.image-processing :as image-processing] - [taoensso.timbre :as log])) - -(defn open-image-picker! [callback-event] - (react/show-image-picker - (fn [image] - (let [path (get (js->clj image) "path") - _ (log/debug path) - on-success (fn [base64] - (re-frame/dispatch [callback-event base64])) - on-error (fn [type error] - (.log js/console type error))] - (image-processing/img->base64 path on-success on-error))))) - -(defn send-transaction [chat-id {:keys [db] :as cofx}] - (let [send-command (get-in db [:id->command ["send" #{:personal-chats}]])] - (handlers-macro/merge-fx cofx - (chat-events/start-chat chat-id {:navigation-replace? true}) - (commands/select-chat-input-command send-command nil)))) - -(defn- valid-name? [name] - (spec/valid? :profile/name name)) - -(defn update-name [name {:keys [db]}] - {:db (-> db - (assoc-in [:my-profile/profile :valid-name?] (valid-name? name)) - (assoc-in [:my-profile/profile :name] name))}) - -(defn update-picture [this-event base64-image {:keys [db]}] - (if base64-image - {:db (-> db - (assoc-in [:my-profile/profile :photo-path] - (str "data:image/jpeg;base64," base64-image)) - (assoc :my-profile/editing? true))} - {:open-image-picker this-event})) - -(defn- clean-name [db edit-view] - (let [name (get-in db [edit-view :name])] - (if (valid-name? name) - name - (get-in db [:account/account :name])))) - -(defn clear-profile [{:keys [db]}] - {:db (dissoc db :my-profile/profile :my-profile/default-name :my-profile/editing?)}) - -(defn start-editing [{:keys [db]}] - (let [profile (select-keys (:account/account db) [:name :photo-path])] - {:db (assoc db - :my-profile/editing? true - :my-profile/profile profile)})) - -(defn save [{:keys [db now] :as cofx}] - (let [{:keys [photo-path]} (:my-profile/profile db) - cleaned-name (clean-name db :my-profile/profile) - cleaned-edit (merge {:name cleaned-name - :last-updated now} - (if photo-path - {:photo-path photo-path}))] - (handlers-macro/merge-fx cofx - (clear-profile) - (accounts.utils/account-update cleaned-edit)))) - -(defn start-editing-group-chat-profile [{:keys [db]}] - {:db (assoc db :group-chat-profile/editing? true)}) - -(defn save-group-chat-profile [{:keys [db]}] - (-> {:db db} - (update :db dissoc :group-chat-profile/editing?))) - -(defn enter-two-random-words [{:keys [db]}] - (let [{:keys [mnemonic]} (:account/account db) - shuffled-mnemonic (shuffle (map-indexed vector (clojure.string/split mnemonic #" ")))] - {:db (assoc db :my-profile/seed {:step :first-word - :first-word (first shuffled-mnemonic) - :second-word (second shuffled-mnemonic)})})) - -(defn set-step [step {:keys [db]}] - {:db (update db :my-profile/seed assoc :step step :error nil :word nil)}) - -(defn finish [{:keys [db] :as cofx}] - (handlers-macro/merge-fx cofx - {:db (update db :my-profile/seed assoc :step :finish :error nil :word nil)} - (accounts.utils/clean-seed-phrase))) - -(defn copy-to-clipboard! [value] - (react/copy-to-clipboard value)) - -(def show-tooltip! - (let [tooltips (atom {})] - (fn [tooltip-id] - (when-let [{:keys [interval-id]} (@tooltips tooltip-id)] - (js/clearInterval interval-id)) - (let [interval-id (js/setInterval - #(let [{:keys [opacity interval-id cnt]} (@tooltips tooltip-id)] - (when opacity - (swap! tooltips assoc-in [tooltip-id :cnt] (inc cnt)) - (if (and opacity (>= 0.0 opacity)) - (do - (log/debug "remove interval:" interval-id) - (js/clearInterval interval-id) - (re-frame/dispatch [:set-in [:tooltips tooltip-id] nil]) - (swap! tooltips dissoc interval-id)) - (do (re-frame/dispatch [:set-in [:tooltips tooltip-id] opacity]) - (when (< 10 cnt) - (swap! tooltips assoc-in [tooltip-id :opacity] (- opacity 0.05))))))) - 100)] - (swap! tooltips assoc tooltip-id {:opacity 1.0 :interval-id interval-id :cnt 0}))))) diff --git a/src/status_im/ui/screens/profile/navigation.cljs b/src/status_im/ui/screens/profile/navigation.cljs deleted file mode 100644 index 710fae988cf..00000000000 --- a/src/status_im/ui/screens/profile/navigation.cljs +++ /dev/null @@ -1,18 +0,0 @@ -(ns status-im.ui.screens.profile.navigation - (:require [status-im.ui.screens.navigation :as navigation])) - -;;TODO(goranjovic) - replace this with an atomic navigation event that calls functions, not dispatch -;; possibly use the generic event, see https://github.com/status-im/status-react/issues/2987 -(defmethod navigation/preload-data! :profile-qr-viewer - [db [_ _ {:keys [contact source value]}]] - (update db :qr-modal #(merge % {:contact (or contact (get db :account/account)) - :source source - :value value}))) - -(defmethod navigation/preload-data! :backup-seed - [db] - (assoc db :my-profile/seed {:step :intro})) - -(defmethod navigation/unload-data! :my-profile - [db] - (dissoc db :my-profile/editing?)) diff --git a/src/status_im/ui/screens/profile/photo_capture/styles.cljs b/src/status_im/ui/screens/profile/photo_capture/styles.cljs deleted file mode 100644 index ab8f473a177..00000000000 --- a/src/status_im/ui/screens/profile/photo_capture/styles.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.ui.screens.profile.photo-capture.styles) - -(def container - {:flex 1 - :background-color :white}) - -(def button-container - {:align-items :center}) - -(def button - {:position :absolute - :bottom 10}) \ No newline at end of file diff --git a/src/status_im/ui/screens/profile/photo_capture/views.cljs b/src/status_im/ui/screens/profile/photo_capture/views.cljs deleted file mode 100644 index 18b35deb812..00000000000 --- a/src/status_im/ui/screens/profile/photo_capture/views.cljs +++ /dev/null @@ -1,46 +0,0 @@ -(ns status-im.ui.screens.profile.photo-capture.views - (:require [re-frame.core :as re-frame] - [reagent.core :as reagent] - [status-im.ui.components.camera :as camera] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.i18n :as i18n] - [status-im.ui.screens.profile.photo-capture.styles :as styles] - [status-im.utils.image-processing :as image-processing] - [taoensso.timbre :as log] - [status-im.ui.components.icons.vector-icons :as icons])) - -(defn image-captured [data] - (let [path (.-path data) - _ (log/debug "Captured image: " path) - on-success (fn [base64] - (log/debug "Captured success: " base64) - (re-frame/dispatch [:my-profile/update-picture base64]) - (re-frame/dispatch [:navigate-back])) - on-error (fn [type error] - (log/debug type error))] - (image-processing/img->base64 path on-success on-error))) - -(defn profile-photo-capture [] - (let [camera-ref (reagent/atom nil)] - [react/view styles/container - [status-bar/status-bar] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/image-source-title)]] - [camera/camera {:style {:flex 1} - :aspect (:fill camera/aspects) - :captureQuality "480p" - :captureTarget (:disk camera/capture-targets) - :type "front" - :ref #(reset! camera-ref %)}] - [react/view styles/button-container - [react/view styles/button - [react/touchable-highlight {:on-press (fn [] - (let [camera @camera-ref] - (-> (.capture camera) - (.then image-captured) - (.catch #(log/debug "Error capturing image: " %)))))} - [react/view - [icons/icon :icons/camera {:color :white}]]]]]])) diff --git a/src/status_im/ui/screens/profile/seed/styles.cljs b/src/status_im/ui/screens/profile/seed/styles.cljs deleted file mode 100644 index d260ba561ce..00000000000 --- a/src/status_im/ui/screens/profile/seed/styles.cljs +++ /dev/null @@ -1,153 +0,0 @@ -(ns status-im.ui.screens.profile.seed.styles - (:require [status-im.ui.components.colors :as colors])) - -(def intro-container - {:flex 1 - :align-items :center - :justify-content :center - :margin-horizontal 26}) - -(def intro-image - {:padding-bottom 10 - :align-items :center}) - -(def intro-text - {:text-align :center - :font-size 22 - :font-weight :bold - :line-height 28 - :letter-spacing -0.3}) - -(def intro-description - {:margin-top 8 - :line-height 21 - :text-align :center - :color colors/gray}) - -(def intro-button - {:flex-direction :row - :margin-top 16}) - -(def six-words-container - {:flex 1 - :padding 15}) - -(def six-word-row - {:flex-direction :row}) - -(def six-word-num - {:opacity 0.4 - :font-size 15 - :letter-spacing -0.2}) - -(def six-words-word - {:margin-left 16 - :font-size 15 - :letter-spacing -0.2}) - -(def six-words-separator - {:height 12}) - -(def twelve-words-container - {:flex 1 - :padding 16}) - -(def twelve-words-label - {:font-size 14 - :line-height 21 - :letter-spacing -0.2}) - -(def twelve-words-description - {:font-size 14 - :line-height 21 - :letter-spacing -0.2}) - -(def twelve-words-spacer - {:flex 1}) - -(def twelve-words-button-container - {:align-items :flex-end}) - -(def twelve-words-columns - {:margin-top 8 - :margin-bottom 16 - :flex-direction :row - :border-radius 8 - :background-color colors/white - :border-width 1 - :border-color colors/gray-border}) - -(def twelve-words-columns-separator - {:width 1 - :background-color colors/gray-border}) - -(def enter-word-container - {:flex 1 - :padding 16}) - -(def enter-word-row - {:flex-direction :row}) - -(def enter-word-label - {:font-size 14 - :line-height 21 - :letter-spacing -0.2}) - -(def enter-word-n - {:margin-left 8 - :font-size 14 - :line-height 21 - :letter-spacing -0.2 - :color colors/gray}) - -(def enter-word-n-description - {:font-size 14 - :line-height 21 - :letter-spacing -0.2 - :color colors/gray}) - -(def finish-container - {:flex 1 - :padding-horizontal 24 - :align-items :center}) - -(def finish-logo-container - {:flex 1 - :align-items :center - :justify-content :center}) - -(def ok-icon - {:color :white - :width 41 - :height 41}) - -(def finish-label - {:font-size 22 - :font-weight :bold - :line-height 30 - :letter-spacing -0.3 - :text-align :center}) - -(def finish-description - {:margin-top 8 - :font-size 14 - :line-height 20 - :text-align :center - :color colors/gray}) - -(def finish-button - {:flex-direction :row - :margin-top 16 - :margin-bottom 32}) - -(def backup-seed - {:font-weight :bold - :font-size 15 - :letter-spacing -0.2 - :text-align :center}) - -(def step-n - {:margin-top 5 - :font-size 14 - :text-align :center - :color colors/gray}) diff --git a/src/status_im/ui/screens/profile/seed/views.cljs b/src/status_im/ui/screens/profile/seed/views.cljs deleted file mode 100644 index 08de43516d3..00000000000 --- a/src/status_im/ui/screens/profile/seed/views.cljs +++ /dev/null @@ -1,157 +0,0 @@ -(ns status-im.ui.screens.profile.seed.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.colors :as colors] - [status-im.react-native.js-dependencies :as js-dependencies] - [status-im.react-native.resources :as resources] - [status-im.ui.components.common.common :as components.common] - [re-frame.core :as re-frame] - [reagent.core :as reagent] - [status-im.ui.components.text-input.view :as text-input] - [status-im.ui.components.icons.vector-icons :as icons] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.common.styles :as components.common.styles] - [clojure.string :as string] - [status-im.utils.config :as config] - [status-im.utils.utils :as utils] - [status-im.ui.screens.profile.seed.styles :as styles] - [status-im.i18n :as i18n] - [status-im.ui.components.styles :as common.styles])) - -(def steps-numbers - {:intro 1 - :12-words 1 - :first-word 2 - :second-word 3 - :finish 3}) - -(defn step-back [step] - (case step - (:intro :12-words) (re-frame/dispatch [:navigate-back]) - :first-word (re-frame/dispatch [:my-profile/set-step :12-words]) - :second-word (re-frame/dispatch [:my-profile/set-step :first-word]))) - -(defn intro [] - [react/view {:style styles/intro-container} - [components.common/image-contain {:container-style styles/intro-image} - (:lock resources/ui)] - [react/i18n-text {:style styles/intro-text - :key :your-data-belongs-to-you}] - [react/i18n-text {:style styles/intro-description - :key :your-data-belongs-to-you-description}] - [components.common/button {:button-style styles/intro-button - :on-press #(re-frame/dispatch [:set-in [:my-profile/seed :step] :12-words]) - :label (i18n/label :t/ok-continue)}]]) - -(defn six-words [words] - [react/view {:style styles/six-words-container} - (for [[i word] words] - ^{:key (str "word" i)} - [react/view - [react/view {:style styles/six-word-row} - [react/text {:style styles/six-word-num} - (inc i)] - [react/text {:style styles/six-words-word} - word]] - (when (not= i (first (last words))) - [react/view {:style styles/six-words-separator}])])]) - -(defview twelve-words [{:keys [mnemonic]}] - (letsubs [mnemonic-vec (vec (map-indexed vector (clojure.string/split mnemonic #" "))) - ref (reagent/atom nil)] - {:component-did-mount (fn [_] (when config/testfairy-enabled? - (.hideView js-dependencies/testfairy @ref)))} - [react/view {:style styles/twelve-words-container} - [react/text {:style styles/twelve-words-label} - (i18n/label :t/your-recovery-phrase)] - [react/view {:style styles/twelve-words-columns - :ref (partial reset! ref)} - [six-words (subvec mnemonic-vec 0 6)] - [react/view {:style styles/twelve-words-columns-separator}] - [six-words (subvec mnemonic-vec 6 12)]] - [react/text {:style styles/twelve-words-description} - (i18n/label :t/your-recovery-phrase-description)] - [react/view styles/twelve-words-spacer] - [react/view styles/twelve-words-button-container - [components.common/bottom-button - {:forward? true - :on-press #(re-frame/dispatch [:my-profile/enter-two-random-words])}]]])) - -(defview input [error] - (letsubs [ref (reagent/atom nil)] - {:component-did-mount (fn [_] (when config/testfairy-enabled? - (.hideView js-dependencies/testfairy @ref)))} - [text-input/text-input-with-label - {:placeholder (i18n/label :t/enter-word) - :ref (partial reset! ref) - :auto-focus true - :on-change-text #(re-frame/dispatch [:set-in [:my-profile/seed :word] %]) - :error error}])) - -(defn enter-word [step [idx word] error entered-word] - ^{:key word} - [react/view {:style styles/enter-word-container} - [react/view {:style styles/enter-word-row} - [react/text {:style styles/enter-word-label} - (i18n/label :t/check-your-recovery-phrase)] - [react/text {:style styles/enter-word-n} - (i18n/label :t/word-n {:number (inc idx)})]] - [input error] - [react/text {:style styles/enter-word-n-description} - (i18n/label :t/word-n-description {:number (inc idx)})] - [react/view styles/twelve-words-spacer] - [react/view styles/twelve-words-button-container - [components.common/bottom-button - {:forward? (not= :second-word step) - :label (when (= :second-word step) (i18n/label :t/done)) - :disabled? (string/blank? entered-word) - :on-press (fn [_] - (cond (not= word entered-word) - (re-frame/dispatch [:set-in [:my-profile/seed :error] (i18n/label :t/wrong-word)]) - - (= :first-word step) - (re-frame/dispatch [:my-profile/set-step :second-word]) - - :else - (utils/show-question - (i18n/label :t/are-you-sure?) - (i18n/label :t/are-you-sure-description) - #(re-frame/dispatch [:my-profile/finish]))))}]]]) - -(defn finish [] - [react/view {:style styles/finish-container} - [react/view {:style styles/finish-logo-container} - [react/view {:style (components.common.styles/logo-container 80 true)} - [icons/icon :icons/ok styles/ok-icon]]] - [react/text {:style styles/finish-label} - (i18n/label :t/you-are-all-set)] - [react/text {:style styles/finish-description} - (i18n/label :t/you-are-all-set-description)] - [components.common/button {:button-style styles/finish-button - :on-press #(re-frame/dispatch [:navigate-back]) - :label (i18n/label :t/ok-got-it)}]]) - -(defview backup-seed [] - (letsubs [current-account [:get-current-account] - {:keys [step first-word second-word error word]} [:get :my-profile/seed]] - [react/keyboard-avoiding-view {:style common.styles/flex} - [status-bar/status-bar] - [toolbar/toolbar - nil - (when-not (#{:finish} step) - (toolbar/nav-button (actions/back #(step-back step)))) - [react/view - [react/text {:style styles/backup-seed} - (i18n/label :t/backup-recovery-phrase)] - [react/text {:style styles/step-n} - (i18n/label :t/step-i-of-n {:step (steps-numbers step) :number 3})]]] - [components.common/separator] - (case step - :intro [intro] - :12-words [twelve-words current-account] - :first-word [enter-word step first-word error word] - :second-word [enter-word step second-word error word] - :finish [finish])])) diff --git a/src/status_im/ui/screens/profile/subs.cljs b/src/status_im/ui/screens/profile/subs.cljs deleted file mode 100644 index dacaa750e35..00000000000 --- a/src/status_im/ui/screens/profile/subs.cljs +++ /dev/null @@ -1,21 +0,0 @@ -(ns status-im.ui.screens.profile.subs - (:require [re-frame.core :refer [reg-sub]] - [clojure.string :as string] - [status-im.utils.build :as build] - [status-im.utils.platform :as platform])) - -(reg-sub - :get-profile-unread-messages-number - :<- [:get-current-account] - (fn [{:keys [seed-backed-up? mnemonic]}] - (if (or seed-backed-up? (string/blank? mnemonic)) 0 1))) - -(reg-sub - :get-app-version - (fn [{:keys [web3-node-version]}] - (let [version (if platform/desktop? build/version build/build-no)] - (str build/version " (" version ")\nnode " (or web3-node-version "N/A") "")))) - -(reg-sub :get-device-UUID - (fn [db] - (:device-UUID db))) diff --git a/src/status_im/ui/screens/profile/user/styles.cljs b/src/status_im/ui/screens/profile/user/styles.cljs deleted file mode 100644 index 56b34de8dbe..00000000000 --- a/src/status_im/ui/screens/profile/user/styles.cljs +++ /dev/null @@ -1,78 +0,0 @@ -(ns status-im.ui.screens.profile.user.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.screens.profile.components.styles :as profile.components.styles] - [status-im.ui.components.styles :as components.styles])) - -(def share-contact-code - {:margin-horizontal 16 - :flex-direction :row - :justify-content :space-between - :align-items :center - :height 42 - :border-radius components.styles/border-radius - :background-color (colors/alpha colors/blue 0.1)}) - -(def share-contact-code-text-container - {:padding-left 16 - :padding-bottom 1 - :flex 0.9 - :flex-direction :row - :justify-content :center - :align-items :center}) - -(def share-contact-code-text - {:color colors/blue - :font-size 15}) - -(def share-contact-icon-container - {:border-radius 50 - :flex 0.1 - :padding-right 5 - :align-items :center - :justify-content :center}) - -(def qr-code - {:background-color colors/gray-lighter}) - -(def qr-code-viewer - {:flex-grow 1 - :flex-direction :column}) - -(defstyle my-profile-info-container - {:background-color colors/white}) - -(defstyle my-profile-settings-logout-wrapper - {:flex-direction :row - :justify-content :space-between - :align-items :center}) - -(defstyle my-profile-settings-logout - {:min-width "50%"}) - -(defstyle my-profile-settings-logout-version - {:padding-horizontal 16}) - -(def advanced-button - {:margin-top 16 - :margin-bottom 12}) - -(def advanced-button-container - {:align-items :center - :justify-content :center}) - -(def advanced-button-container-background - {:padding-left 16 - :padding-right 12 - :padding-vertical 6 - :border-radius 18 - :background-color (colors/alpha colors/blue 0.1)}) - -(def advanced-button-row - {:flex-direction :row - :align-items :center}) - -(def advanced-button-label - {:font-size 15 - :letter-spacing -0.2 - :color colors/blue}) diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs deleted file mode 100644 index c6549e891c1..00000000000 --- a/src/status_im/ui/screens/profile/user/views.cljs +++ /dev/null @@ -1,215 +0,0 @@ -(ns status-im.ui.screens.profile.user.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [reagent.core :as reagent] - [status-im.i18n :as i18n] - [status-im.ui.components.action-button.styles :as action-button.styles] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.common.styles :as common.styles] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.qr-code-viewer.views :as qr-code-viewer] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.profile.components.views :as profile.components] - [status-im.ui.screens.profile.components.styles :as profile.components.styles] - [status-im.ui.screens.profile.user.styles :as styles] - [status-im.utils.build :as build] - [status-im.utils.config :as config] - [status-im.utils.platform :as platform] - [status-im.utils.utils :as utils] - [status-im.ui.components.icons.vector-icons :as icons] - [status-im.ui.components.common.common :as components.common] - [clojure.string :as string])) - -(defn my-profile-toolbar [] - [toolbar/toolbar {} - nil - [toolbar/content-title ""] - [react/touchable-highlight - {:on-press #(re-frame/dispatch [:my-profile/start-editing-profile]) - :accessibility-label :edit-button} - [react/view - [react/text {:style common.styles/label-action-text - :uppercase? true} - (i18n/label :t/edit)]]]]) - -(defn my-profile-edit-toolbar [on-show] - (reagent/create-class - {:component-did-mount on-show - :reagent-render (fn [] [toolbar/toolbar {} - nil - [toolbar/content-title ""] - [toolbar/default-done {:handler #(re-frame/dispatch [:my-profile/save-profile]) - :icon :icons/ok - :icon-opts {:color colors/blue} - :accessibility-label :done-button}]])})) - -(def profile-icon-options - [{:label (i18n/label :t/image-source-gallery) - :action #(re-frame/dispatch [:my-profile/update-picture])} - {:label (i18n/label :t/image-source-make-photo) - :action (fn [] - (re-frame/dispatch [:request-permissions {:permissions [:camera :write-external-storage] - :on-allowed #(re-frame/dispatch [:navigate-to :profile-photo-capture]) - :on-denied (fn [] - (utils/set-timeout - #(utils/show-popup (i18n/label :t/error) - (i18n/label :t/camera-access-error)) - 50))}]))}]) - -(defn qr-viewer-toolbar [label value] - [toolbar/toolbar {} - [toolbar/default-done {:icon-opts {:color colors/black} - :accessibility-label :done-button}] - [toolbar/content-title label] - [toolbar/actions [{:icon :icons/share - :icon-opts {:color :black - :accessibility-label :share-code-button} - :handler #(list-selection/open-share {:message value})}]]]) - -(defview qr-viewer [] - (letsubs [{:keys [value contact]} [:get :qr-modal]] - [react/view styles/qr-code-viewer - [status-bar/status-bar {:type :modal-white}] - [qr-viewer-toolbar (:name contact) value] - [qr-code-viewer/qr-code-viewer {:style styles/qr-code} - value (i18n/label :t/qr-code-public-key-hint) (str value)]])) - -(defn- show-qr [contact source value] - #(re-frame/dispatch [:navigate-to-modal :profile-qr-viewer {:contact contact - :source source - :value value}])) - -(defn share-contact-code [current-account public-key] - [react/touchable-highlight {:on-press (show-qr current-account :public-key public-key)} - [react/view styles/share-contact-code - [react/view styles/share-contact-code-text-container - [react/text {:style styles/share-contact-code-text - :uppercase? true} - (i18n/label :t/share-contact-code)]] - [react/view {:style styles/share-contact-icon-container - :accessibility-label :share-my-contact-code-button} - [vector-icons/icon :icons/qr {:color colors/blue}]]]]) - -(defn- handle-logout [] - (utils/show-confirmation (i18n/label :t/logout-title) - (i18n/label :t/logout-are-you-sure) - (i18n/label :t/logout) #(re-frame/dispatch [:logout]))) - -(defn- my-profile-settings [{:keys [seed-backed-up? mnemonic]} currency] - (let [show-backup-seed? (and (not seed-backed-up?) (not (string/blank? mnemonic)))] - [react/view - [profile.components/settings-title (i18n/label :t/settings)] - [profile.components/settings-item {:label-kw :t/main-currency - :value (:code currency) - :action-fn #(re-frame/dispatch [:navigate-to :currency-settings]) - :accessibility-label :currency-button}] - [profile.components/settings-item-separator] - [profile.components/settings-item {:label-kw :t/notifications - :accessibility-label :notifications-button - :action-fn #(.openURL react/linking "app-settings://notification/status-im")}] - (when show-backup-seed? - [profile.components/settings-item-separator]) - (when show-backup-seed? - [profile.components/settings-item - {:label-kw :t/backup-your-recovery-phrase - :action-fn #(re-frame/dispatch [:navigate-to :backup-seed]) - :icon-content [components.common/counter {:size 22} 1]}]) - [profile.components/settings-item-separator] - [profile.components/settings-item {:label-kw :t/need-help - :accessibility-label :help-button - :action-fn #(re-frame/dispatch [:navigate-to :help-center])}] - [profile.components/settings-item-separator] - [react/view styles/my-profile-settings-logout-wrapper - [react/view styles/my-profile-settings-logout - [profile.components/settings-item {:label-kw :t/logout - :accessibility-label :log-out-button - :destructive? true - :hide-arrow? true - :action-fn #(handle-logout)}]] - [react/view styles/my-profile-settings-logout-version - [react/text @(re-frame/subscribe [:get-app-version])]]]])) - -(defview advanced-settings [{:keys [network networks dev-mode?]} on-show] - (letsubs [{:keys [sharing-usage-data?]} [:get-current-account]] - {:component-did-mount on-show} - [react/view - (when dev-mode? - [profile.components/settings-item - {:label-kw :t/network - :value (get-in networks [network :name]) - :action-fn #(re-frame/dispatch [:navigate-to :network-settings]) - :accessibility-label :network-button}]) - (when config/offline-inbox-enabled? - [profile.components/settings-item-separator]) - (when config/offline-inbox-enabled? - [profile.components/settings-item - {:label-kw :t/offline-messaging - :action-fn #(re-frame/dispatch [:navigate-to :offline-messaging-settings]) - :accessibility-label :offline-messages-settings-button}]) - (when config/bootnodes-settings-enabled? - [profile.components/settings-item-separator]) - (when config/bootnodes-settings-enabled? - [profile.components/settings-item - {:label-kw :t/bootnodes - :action-fn #(re-frame/dispatch [:navigate-to :bootnodes-settings]) - :accessibility-label :bootnodes-settings-button}]) - [profile.components/settings-item-separator] - [profile.components/settings-switch-item - {:label-kw :t/dev-mode - :value dev-mode? - :action-fn #(re-frame/dispatch [:switch-dev-mode %])}]])) - -(defview advanced [params on-show] - (letsubs [advanced? [:get :my-profile/advanced?]] - {:component-will-unmount #(re-frame/dispatch [:set :my-profile/advanced? false])} - [react/view - [react/touchable-highlight {:on-press #(re-frame/dispatch [:set :my-profile/advanced? (not advanced?)]) - :style styles/advanced-button} - [react/view {:style styles/advanced-button-container} - [react/view {:style styles/advanced-button-container-background} - [react/view {:style styles/advanced-button-row} - [react/text {:style styles/advanced-button-label} - (i18n/label :t/wallet-advanced)] - [icons/icon (if advanced? :icons/up :icons/down) {:color colors/blue}]]]]] - (when advanced? - [advanced-settings params on-show])])) - -(defview my-profile [] - (letsubs [{:keys [public-key] :as current-account} [:get-current-account] - editing? [:get :my-profile/editing?] - changed-account [:get :my-profile/profile] - currency [:wallet/currency] - scroll (reagent/atom nil)] - (let [shown-account (merge current-account changed-account) - ;; We scroll on the component once rendered. setTimeout is necessary, - ;; likely to allow the animation to finish. - on-show-edit (fn [] - (js/setTimeout - #(.scrollTo @scroll {:x 0 :y 0 :animated false}) - 300)) - on-show-advanced (fn [] - (js/setTimeout - #(.scrollToEnd @scroll {:animated false}) - 300))] - [react/view profile.components.styles/profile - (if editing? - [my-profile-edit-toolbar on-show-edit] - [my-profile-toolbar]) - [react/scroll-view {:ref #(reset! scroll %) - :keyboard-should-persist-taps :handled} - [react/view profile.components.styles/profile-form - [profile.components/profile-header - {:contact current-account - :edited-contact changed-account - :editing? editing? - :allow-icon-change? true - :options profile-icon-options - :on-change-text-event :my-profile/update-name}]] - [react/view action-button.styles/actions-list - [share-contact-code current-account public-key]] - [react/view styles/my-profile-info-container - [my-profile-settings current-account currency]] - [advanced shown-account on-show-advanced]]]))) diff --git a/src/status_im/ui/screens/qr_scanner/db.cljs b/src/status_im/ui/screens/qr_scanner/db.cljs deleted file mode 100644 index 868e3935ae9..00000000000 --- a/src/status_im/ui/screens/qr_scanner/db.cljs +++ /dev/null @@ -1,8 +0,0 @@ -(ns status-im.ui.screens.qr-scanner.db - (:require [cljs.spec.alpha :as s])) - -;;on scan qr -(s/def :qr/qr-codes (s/nilable map?)) -;;used in qr modal screen -(s/def :qr/qr-modal (s/nilable map?)) -(s/def :qr/current-qr-context (s/nilable map?)) \ No newline at end of file diff --git a/src/status_im/ui/screens/qr_scanner/events.cljs b/src/status_im/ui/screens/qr_scanner/events.cljs deleted file mode 100644 index ac97ac1fdc6..00000000000 --- a/src/status_im/ui/screens/qr_scanner/events.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.ui.screens.qr-scanner.events - (:require [re-frame.core :as re-frame] - [status-im.ui.components.camera :as camera] - [status-im.utils.handlers :as handlers] - [status-im.utils.utils :as utils] - [status-im.i18n :as i18n])) - -(handlers/register-handler-fx - :scan-qr-code - (fn [{:keys [db]} [_ identifier handler]] - {:db (assoc-in db [:qr-codes identifier] handler) - :request-permissions-fx {:permissions [:camera] - :on-allowed #(re-frame/dispatch [:navigate-to :qr-scanner {:current-qr-context identifier}]) - :on-denied (fn [] - (utils/set-timeout - #(utils/show-popup (i18n/label :t/error) - (i18n/label :t/camera-access-error)) - 50))}})) - -(handlers/register-handler-fx - :clear-qr-code - (fn [{:keys [db]} [_ identifier]] - {:db (update db :qr-codes dissoc identifier)})) - -(handlers/register-handler-fx - :set-qr-code - (fn [{:keys [db]} [_ context data]] - (merge {:db (-> db - (update :qr-codes dissoc context) - (dissoc :current-qr-context))} - (when-let [handler (get-in db [:qr-codes context])] - {:dispatch [handler context data]})))) diff --git a/src/status_im/ui/screens/qr_scanner/styles.cljs b/src/status_im/ui/screens/qr_scanner/styles.cljs deleted file mode 100644 index fcb37754f77..00000000000 --- a/src/status_im/ui/screens/qr_scanner/styles.cljs +++ /dev/null @@ -1,74 +0,0 @@ -(ns status-im.ui.screens.qr-scanner.styles - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.toolbar.styles :as toolbar.styles])) - -(def barcode-scanner-container - {:flex 1 - :background-color :white}) - -(def barcode-scanner - {:flex 1}) - -(def rectangle-container - {:position :absolute - :left 0 - :top toolbar.styles/toolbar-height - :bottom 0 - :right 0 - :flex 1 - :align-items :center - :justify-content :center - :background-color :transparent}) - -(def rectangle - {:height 250 - :width 250 - :background-color :transparent}) - -(def corner-left-top - {:position :absolute - :left 0 - :top 0 - :width 56 - :height 56}) - -(def corner-right-top - {:position :absolute - :right 0 - :top 0 - :width 56 - :height 56}) - -(def corner-right-bottom - {:position :absolute - :right 0 - :bottom 0 - :width 56 - :height 56}) - -(def corner-left-bottom - {:position :absolute - :left 0 - :bottom 0 - :width 56 - :height 56}) - -(def import-button - {:position :absolute - :right 16 - :flex 1 - :height 50 - :align-items :center}) - -(def import-button-content - {:flex 1 - :flex-direction :row - :height 50 - :align-items :center - :align-self :center}) - -(def import-text - {:flex 1 - :flex-direction :column - :color colors/white - :margin-left 8}) diff --git a/src/status_im/ui/screens/qr_scanner/views.cljs b/src/status_im/ui/screens/qr_scanner/views.cljs deleted file mode 100644 index b714379c568..00000000000 --- a/src/status_im/ui/screens/qr_scanner/views.cljs +++ /dev/null @@ -1,36 +0,0 @@ -(ns status-im.ui.screens.qr-scanner.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [reagent.core :as reagent] - [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] - [status-im.ui.components.camera :as camera] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.qr-scanner.styles :as styles])) - -(defview qr-scanner-toolbar [title hide-nav?] - (letsubs [modal [:get :modal]] - [react/view - [status-bar/status-bar] - [toolbar/simple-toolbar title]])) - -(defview qr-scanner [] - (letsubs [{identifier :current-qr-context} [:get-screen-params] - camera-initialized? (reagent/atom false)] - - [react/view styles/barcode-scanner-container - [qr-scanner-toolbar (:toolbar-title identifier) (not @camera-initialized?)] - [camera/camera {:onBarCodeRead #(re-frame/dispatch [:set-qr-code identifier (camera/get-qr-code-data %)]) - :ref #(reset! camera-initialized? true) - :captureAudio false - :style styles/barcode-scanner}] - [react/view styles/rectangle-container - [react/view styles/rectangle - [react/image {:source {:uri :corner_left_top} - :style styles/corner-left-top}] - [react/image {:source {:uri :corner_right_top} - :style styles/corner-right-top}] - [react/image {:source {:uri :corner_right_bottom} - :style styles/corner-right-bottom}] - [react/image {:source {:uri :corner_left_bottom} - :style styles/corner-left-bottom}]]]])) diff --git a/src/status_im/ui/screens/subs.cljs b/src/status_im/ui/screens/subs.cljs deleted file mode 100644 index 32f0d6a04f3..00000000000 --- a/src/status_im/ui/screens/subs.cljs +++ /dev/null @@ -1,101 +0,0 @@ -(ns status-im.ui.screens.subs - (:require [re-frame.core :refer [reg-sub subscribe]] - [status-im.utils.ethereum.core :as ethereum] - status-im.chat.subs - status-im.ui.screens.accounts.subs - status-im.ui.screens.home.subs - status-im.ui.screens.contacts.subs - status-im.ui.screens.group.subs - status-im.ui.screens.wallet.subs - status-im.ui.screens.wallet.collectibles.subs - status-im.ui.screens.wallet.request.subs - status-im.ui.screens.wallet.send.subs - status-im.ui.screens.wallet.transactions.subs - status-im.ui.screens.network-settings.subs - status-im.ui.screens.offline-messaging-settings.subs - status-im.ui.screens.bootnodes-settings.subs - status-im.ui.screens.currency-settings.subs - status-im.ui.screens.browser.subs - status-im.ui.screens.add-new.new-chat.subs - status-im.ui.screens.add-new.new-public-chat.subs - status-im.ui.screens.profile.subs)) - -(reg-sub :get - (fn [db [_ k]] - (get db k))) - -(reg-sub :get-in - (fn [db [_ path]] - (get-in db path))) - -(reg-sub :signed-up? - :<- [:get-current-account] - (fn [current-account] - (:signed-up? current-account))) - -(reg-sub :network - :<- [:get-current-account] - (fn [current-account] - (get (:networks current-account) (:network current-account)))) - -(reg-sub :network-name :chain) - -(reg-sub :sync-state :sync-state) -(reg-sub :network-status :network-status) -(reg-sub :peers-count :peers-count) -(reg-sub :mailserver-status :mailserver-status) - -(reg-sub :fetching? - (fn [db] - (get db :inbox/fetching?))) - -(reg-sub :offline? - :<- [:network-status] - :<- [:sync-state] - (fn [[network-status sync-state]] - (or (= network-status :offline) - (= sync-state :offline)))) - -(reg-sub :disconnected? - :<- [:peers-count] - (fn [peers-count] - (zero? peers-count))) - -(reg-sub :mailserver-error? - :<- [:mailserver-status] - (fn [mailserver-status] - (#{:error :disconnected} mailserver-status))) - -(reg-sub :syncing? - :<- [:sync-state] - (fn [sync-state] - (#{:pending :in-progress} sync-state))) - -(reg-sub :tab-bar-visible? - (fn [db _] - (get db :tab-bar-visible?))) - -(reg-sub :get-screen-params - (fn [db [_ view-id]] - (get-in db [:navigation/screen-params (or view-id (:view-id db))]))) - -(reg-sub :can-navigate-back? - (fn [db] - (> (count (:navigation-stack db)) 1))) - -(reg-sub :delete-swipe-position - (fn [db [_ item-id]] - (let [item-animation (get-in db [:chat-animations item-id])] - (if (some? item-animation) (:delete-swiped item-animation) nil)))) - -(reg-sub :get-current-account-network - (fn [{:keys [network] :as db} [_]] - (get-in db [:account/account :networks network]))) - -(reg-sub :dimensions/window - (fn [db _] - (get db :dimensions/window))) - -(reg-sub :dimensions/window-width - :<- [:dimensions/window] - :width) diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs deleted file mode 100644 index b5bef3cf8d7..00000000000 --- a/src/status_im/ui/screens/views.cljs +++ /dev/null @@ -1,148 +0,0 @@ -(ns status-im.ui.screens.views - (:require-macros [status-im.utils.views :refer [defview letsubs] :as views]) - (:require [re-frame.core :refer [dispatch]] - [status-im.utils.platform :refer [android?]] - [status-im.utils.universal-links.core :as utils.universal-links] - [status-im.ui.components.react :refer [view modal create-main-screen-view] :as react] - [status-im.ui.components.styles :as common-styles] - [status-im.ui.screens.main-tabs.views :refer [main-tabs]] - - [status-im.ui.screens.accounts.login.views :refer [login]] - [status-im.ui.screens.accounts.recover.views :refer [recover]] - [status-im.ui.screens.accounts.views :refer [accounts]] - - [status-im.chat.screen :refer [chat]] - [status-im.ui.screens.add-new.views :refer [add-new]] - [status-im.ui.screens.add-new.new-chat.views :refer [new-chat]] - [status-im.ui.screens.add-new.new-public-chat.view :refer [new-public-chat]] - - [status-im.ui.screens.qr-scanner.views :refer [qr-scanner]] - - [status-im.ui.screens.group.views :refer [new-group]] - [status-im.ui.screens.group.add-contacts.views :refer [contact-toggle-list - add-participants-toggle-list]] - [status-im.ui.screens.profile.user.views :as profile.user] - [status-im.ui.screens.profile.contact.views :as profile.contact] - [status-im.ui.screens.profile.group-chat.views :as profile.group-chat] - [status-im.ui.screens.profile.photo-capture.views :refer [profile-photo-capture]] - [status-im.ui.screens.wallet.main.views :as wallet.main] - [status-im.ui.screens.wallet.collectibles.views :refer [collectibles-list]] - [status-im.ui.screens.wallet.send.views :refer [send-transaction send-transaction-modal sign-message-modal]] - [status-im.ui.screens.wallet.choose-recipient.views :refer [choose-recipient]] - [status-im.ui.screens.wallet.request.views :refer [request-transaction send-transaction-request]] - [status-im.ui.screens.wallet.components.views :as wallet.components] - [status-im.ui.screens.wallet.onboarding.setup.views :as wallet.onboarding.setup] - [status-im.ui.screens.wallet.send.views :as wallet.send] - [status-im.ui.screens.wallet.settings.views :as wallet-settings] - [status-im.ui.screens.wallet.transactions.views :as wallet-transactions] - [status-im.ui.screens.wallet.send.transaction-sent.views :refer [transaction-sent transaction-sent-modal]] - [status-im.ui.screens.wallet.components.views :refer [contact-code recent-recipients recipient-qr-code]] - [status-im.ui.screens.network-settings.views :refer [network-settings]] - [status-im.ui.screens.network-settings.network-details.views :refer [network-details]] - [status-im.ui.screens.network-settings.edit-network.views :refer [edit-network]] - [status-im.ui.screens.offline-messaging-settings.views :refer [offline-messaging-settings]] - [status-im.ui.screens.offline-messaging-settings.edit-mailserver.views :refer [edit-mailserver]] - [status-im.ui.screens.bootnodes-settings.views :refer [bootnodes-settings]] - [status-im.ui.screens.bootnodes-settings.edit-bootnode.views :refer [edit-bootnode]] - [status-im.ui.screens.currency-settings.views :refer [currency-settings]] - [status-im.ui.screens.help-center.views :refer [help-center]] - [status-im.ui.screens.browser.views :refer [browser]] - [status-im.ui.screens.add-new.open-dapp.views :refer [open-dapp dapp-description]] - [status-im.ui.screens.intro.views :refer [intro]] - [status-im.ui.screens.accounts.create.views :refer [create-account]] - [status-im.ui.screens.profile.seed.views :refer [backup-seed]])) - -(defn get-main-component [view-id] - (case view-id - :collectibles-list collectibles-list - :intro intro - :create-account create-account - (:home :wallet :my-profile) main-tabs - :browser browser - :open-dapp open-dapp - :dapp-description dapp-description - :wallet-onboarding-setup wallet.onboarding.setup/screen - :wallet-send-transaction send-transaction - :wallet-send-transaction-chat send-transaction - :wallet-transaction-sent transaction-sent - :wallet-request-transaction request-transaction - :wallet-send-transaction-request send-transaction-request - (:transactions-history :unsigned-transactions) wallet-transactions/transactions - :wallet-transaction-details wallet-transactions/transaction-details - :wallet-send-assets wallet.components/send-assets - :wallet-request-assets wallet.components/request-assets - :new add-new - :new-group new-group - :add-participants-toggle-list add-participants-toggle-list - :new-public-chat new-public-chat - :contact-toggle-list contact-toggle-list - :new-chat new-chat - :qr-scanner qr-scanner - :chat chat - :profile profile.contact/profile - :group-chat-profile profile.group-chat/group-chat-profile - :profile-photo-capture profile-photo-capture - :accounts accounts - :login login - :recover recover - :network-settings network-settings - :network-details network-details - :edit-network edit-network - :offline-messaging-settings offline-messaging-settings - :edit-mailserver edit-mailserver - :bootnodes-settings bootnodes-settings - :edit-bootnode edit-bootnode - :currency-settings currency-settings - :help-center help-center - :recent-recipients recent-recipients - :recipient-qr-code recipient-qr-code - :contact-code contact-code - :backup-seed backup-seed - [react/view [react/text (str "Unknown view: " view-id)]])) - -(defn get-modal-component [modal-view] - (case modal-view - :qr-scanner qr-scanner - :profile-qr-viewer profile.user/qr-viewer - :wallet-modal wallet.main/wallet-modal - :wallet-transactions-filter wallet-transactions/filter-history - :wallet-settings-assets wallet-settings/manage-assets - :wallet-send-transaction-modal send-transaction-modal - :wallet-transaction-sent-modal transaction-sent-modal - :wallet-sign-message-modal sign-message-modal - :wallet-transaction-fee wallet.send/transaction-fee - :wallet-onboarding-setup-modal wallet.onboarding.setup/modal - [react/view [react/text (str "Unknown modal view: " modal-view)]])) - -(defview main-modal [] - (letsubs [modal-view [:get :modal]] - (when modal-view - [view common-styles/modal - [modal {:animation-type :slide - :transparent true - :on-request-close (fn [] - (cond - (#{:wallet-send-transaction-modal - :wallet-sign-message-modal - :wallet-transaction-fee} - modal-view) - (dispatch [:wallet/discard-transaction-navigate-back]) - - :else - (dispatch [:navigate-back])))} - (let [component (get-modal-component modal-view)] - [react/main-screen-modal-view modal-view - [component]])]]))) - -(defview main [] - (letsubs [signed-up? [:signed-up?] - view-id [:get :view-id]] - {:component-did-mount utils.universal-links/initialize - :component-will-unmount utils.universal-links/finalize - :component-will-update (fn [] (react/dismiss-keyboard!))} - (when view-id - (let [component (get-main-component view-id) - main-screen-view (create-main-screen-view view-id)] - [main-screen-view common-styles/flex - [component] - [main-modal]])))) diff --git a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs b/src/status_im/ui/screens/wallet/choose_recipient/events.cljs deleted file mode 100644 index 5cb73c328b8..00000000000 --- a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs +++ /dev/null @@ -1,91 +0,0 @@ -(ns status-im.ui.screens.wallet.choose-recipient.events - (:require [status-im.constants :as constants] - [status-im.i18n :as i18n] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.eip681 :as eip681] - [status-im.utils.handlers :as handlers] - [status-im.utils.money :as money])) - -(handlers/register-handler-db - :wallet/toggle-flashlight - (fn [db] - (let [flashlight-state (get-in db [:wallet :send-transaction :camera-flashlight]) - toggled-state (if (= :on flashlight-state) :off :on)] - (assoc-in db [:wallet :send-transaction :camera-flashlight] toggled-state)))) - -(defn- fill-request-details [db {:keys [address name value symbol gas gasPrice whisper-identity from-chat?]}] - {:pre [(not (nil? address))]} - (update-in - db [:wallet :send-transaction] - (fn [{old-symbol :symbol :as old-transaction}] - (let [symbol-changed? (not= old-symbol symbol)] - (cond-> (assoc old-transaction :to address :to-name name :whisper-identity whisper-identity) - value (assoc :amount value) - symbol (assoc :symbol symbol) - (and gas symbol-changed?) (assoc :gas (money/bignumber gas)) - from-chat? (assoc :from-chat? from-chat?) - (and gasPrice symbol-changed?) - (assoc :gas-price (money/bignumber gasPrice)) - (and symbol (not gasPrice) symbol-changed?) - (assoc :gas-price (ethereum/estimate-gas symbol))))))) - -(defn- extract-details - "First try to parse as EIP681 URI, if not assume this is an address directly. - Returns a map containing at least the `address` and `chain-id` keys" - [s chain-id] - (or (let [m (eip681/parse-uri s)] - (merge m (eip681/extract-request-details m))) - (when (ethereum/address? s) - {:address s :chain-id chain-id}))) - -;; NOTE(janherich) - whenever changing assets, we want to clear the previusly set amount/amount-text -(defn changed-asset [{:keys [db] :as fx} old-symbol new-symbol] - (-> fx - (merge {:update-gas-price {:web3 (:web3 db) - :success-event :wallet/update-gas-price-success - :edit? false}}) - (assoc-in [:db :wallet :send-transaction :amount] nil) - (assoc-in [:db :wallet :send-transaction :amount-text] nil) - (assoc-in [:db :wallet :send-transaction :asset-error] - (i18n/label :t/changed-asset-warning {:old old-symbol :new new-symbol})))) - -(defn changed-amount-warning [fx old-amount new-amount] - (assoc-in fx [:db :wallet :send-transaction :amount-error] - (i18n/label :t/changed-amount-warning {:old old-amount :new new-amount}))) - -(defn use-default-eth-gas [fx] - (assoc-in fx [:db :wallet :send-transaction :gas] - ethereum/default-transaction-gas)) - -(handlers/register-handler-fx - :wallet/fill-request-from-url - (fn [{{:keys [network] :as db} :db} [_ data origin]] - (let [current-chain-id (get-in constants/default-networks [network :config :NetworkId]) - {:keys [address chain-id] :as details} (extract-details data current-chain-id) - valid-network? (boolean (= current-chain-id chain-id)) - previous-state (get-in db [:wallet :send-transaction]) - old-symbol (:symbol previous-state) - new-symbol (:symbol details) - old-amount (:amount previous-state) - new-amount (:value details) - new-gas (:gas details) - symbol-changed? (and old-symbol new-symbol (not= old-symbol new-symbol))] - (cond-> {:db db - :dispatch [:navigate-back]} - (and address valid-network?) (update :db #(fill-request-details % details)) - symbol-changed? (changed-asset old-symbol new-symbol) - (and old-amount new-amount (not= old-amount new-amount)) (changed-amount-warning old-amount new-amount) - ;; NOTE(goranjovic) - the next line is there is because QR code scanning switches the amount to ETH - ;; automatically, so we need to update the gas limit accordingly. The check for origin screen is there - ;; so that we wouldn't also switch gas limit to ETH specific if the user pastes address as text. - ;; We need to check if address is defined so that we wouldn't trigger this behavior when invalid QR is scanned - ;; (e.g. whisper-id) - (and address (= origin :qr) (not new-gas) symbol-changed?) (use-default-eth-gas) - (not address) (assoc :show-error (i18n/label :t/wallet-invalid-address {:data data})) - (and address (not valid-network?)) (assoc :show-error (i18n/label :t/wallet-invalid-chain-id {:data data :chain current-chain-id})))))) - -(handlers/register-handler-fx - :wallet/fill-request-from-contact - (fn [{db :db} [_ {:keys [address name whisper-identity]}]] - {:db (fill-request-details db {:address address :name name :whisper-identity whisper-identity}) - :dispatch [:navigate-back]})) diff --git a/src/status_im/ui/screens/wallet/choose_recipient/styles.cljs b/src/status_im/ui/screens/wallet/choose_recipient/styles.cljs deleted file mode 100644 index e11f4c8eee0..00000000000 --- a/src/status_im/ui/screens/wallet/choose_recipient/styles.cljs +++ /dev/null @@ -1,95 +0,0 @@ -(ns status-im.ui.screens.wallet.choose-recipient.styles - (:require [status-im.ui.components.colors :as colors])) - -(def toolbar - {:background-color :transparent}) - -(def wallet-container - {:flex 1 - :background-color colors/blue}) - -(def recipient-button - {:flex-direction :row - :justify-content :space-between - :margin-vertical 10 - :margin-left 20}) - -(def recipient-button-text - {:color :white - :align-self :center - :font-size 14}) - -(def qr-container - {:position :absolute - :top 0 - :left 0 - :right 0 - :bottom 0}) - -(def preview - {:flex 1 - :justify-content :flex-end - :align-items :center}) - -(def corner-dimensions - {:position :absolute - :width 40 - :height 40}) - -(defn corner-left-bottom [height width size] - (merge corner-dimensions {:bottom (int (/ (- height size) 2)) - :left (int (/ (- width size) 2))})) - -(defn corner-right-bottom [height width size] - (merge corner-dimensions {:right (int (/ (- width size) 2)) - :bottom (int (/ (- height size) 2))})) - -(defn corner-left-top [height width size] - (merge corner-dimensions {:top (int (/ (- height size) 2)) - :left (int (/ (- width size) 2))})) - -(defn corner-right-top [height width size] - (merge corner-dimensions {:top (int (/ (- height size) 2)) - :right (int (/ (- width size) 2))})) - -(def viewfinder-port {:position :absolute - :left 0 - :top 0 - :bottom 0 - :right 0 - :flex 1}) - -(defn viewfinder-translucent [height width size side] - (let [top-bottom-width width - top-bottom-height (int (/ (- height size) 2)) - left-right-width (int (/ (- width size) 2)) - left-right-height (- height (* 2 top-bottom-height))] - (cond-> {:position :absolute - :background-color :black - :opacity 0.7} - (= :top side) (assoc :height top-bottom-height - :width top-bottom-width) - (= :right side) (assoc :height left-right-height - :width left-right-width - :top top-bottom-height - :right 0) - (= :bottom side) (assoc :height top-bottom-height - :width top-bottom-width - :bottom 0 - :left 0) - (= :left side) (assoc :height left-right-height - :width left-right-width - :top top-bottom-height - :left 0)))) - -(def qr-code - {:flex 1 - :background-color colors/white-lighter-transparent - :align-items :center}) - -(defn qr-code-text [dimensions] - {:zIndex 1 - :padding-top 20 - :color :white - :text-align :center - :width (int (/ (:width dimensions) 2))}) diff --git a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs deleted file mode 100644 index 5c21aef3eda..00000000000 --- a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs +++ /dev/null @@ -1,72 +0,0 @@ -(ns status-im.ui.screens.wallet.choose-recipient.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.bottom-buttons.view :as bottom-buttons] - [status-im.ui.components.button.view :as button] - [status-im.ui.components.camera :as camera] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.wallet.choose-recipient.styles :as styles] - [status-im.utils.platform :as platform])) - -(defn- toolbar-view [camera-flashlight] - [toolbar/toolbar styles/toolbar - [toolbar/nav-button (actions/back-white actions/default-handler)] - [toolbar/content-title {:color :white} - (i18n/label :t/wallet-choose-recipient)] - [toolbar/actions [{:icon (if (= :on camera-flashlight) - :icons/flash-active - :icons/flash-inactive) - :icon-opts {:color :white} - :handler #(re-frame/dispatch [:wallet/toggle-flashlight])}]]]) - -(defn- viewfinder [{:keys [height width]} size] - (let [height (cond-> height - platform/iphone-x? (- 78))] - [react/view {:style styles/viewfinder-port} - [react/view {:style (styles/viewfinder-translucent height width size :top)}] - [react/view {:style (styles/viewfinder-translucent height width size :right)}] - [react/view {:style (styles/viewfinder-translucent height width size :bottom)}] - [react/view {:style (styles/viewfinder-translucent height width size :left)}] - [react/image {:source {:uri :corner_left_top} - :style (styles/corner-left-top height width size)}] - [react/image {:source {:uri :corner_right_top} - :style (styles/corner-right-top height width size)}] - [react/image {:source {:uri :corner_left_bottom} - :style (styles/corner-left-bottom height width size)}] - [react/image {:source {:uri :corner_right_bottom} - :style (styles/corner-right-bottom height width size)}]])) - -(defn- size [{:keys [height width]}] - (int (* 2 (/ (min height width) 3)))) - -(defview choose-recipient [] - (letsubs [read-once? (atom false) - dimensions [:dimensions/window] - camera-flashlight [:wallet.send/camera-flashlight]] - [react/view {:style styles/qr-code} - [status-bar/status-bar {:type :transparent}] - [toolbar-view camera-flashlight] - [react/text {:style (styles/qr-code-text dimensions) - :accessibility-label :scan-qr-code-with-wallet-address-text} - (i18n/label :t/scan-qr-code)] - [react/view {:style styles/qr-container - :pointer-events :none} - [react/with-activity-indicator - {} - [camera/camera {:style styles/preview - :aspect :fill - :captureAudio false - :torchMode (camera/set-torch camera-flashlight) - :onBarCodeRead #(when-not @read-once? - (reset! read-once? true) - (re-frame/dispatch [:wallet/fill-request-from-url (camera/get-qr-code-data %) :qr]))}]] - [viewfinder dimensions (size dimensions)]] - [bottom-buttons/bottom-button - [button/button {:disabled? false - :on-press #(re-frame/dispatch [:navigate-back]) - :accessibility-label :cancel-button} - (i18n/label :t/cancel)]]])) diff --git a/src/status_im/ui/screens/wallet/collectibles/cryptokitties/events.cljs b/src/status_im/ui/screens/wallet/collectibles/cryptokitties/events.cljs deleted file mode 100644 index 6325389839f..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/cryptokitties/events.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.cryptokitties.events - (:require [status-im.utils.handlers :as handlers] - [status-im.ui.screens.wallet.collectibles.events :as collectibles] - [status-im.utils.http :as http])) - -(def ck :CK) - -(handlers/register-handler-fx - :load-kitties - (fn [{db :db} [_ ids]] - {:db db - :http-get-n (mapv (fn [id] - {:url (str "https://api.cryptokitties.co/kitties/" id) - :success-event-creator (fn [o] - [:load-collectible-success ck {id (http/parse-payload o)}]) - :failure-event-creator (fn [o] - [:load-collectible-failure ck {id (http/parse-payload o)}])}) - ids)})) - -;; TODO(andrey) Each HTTP call will return up to 100 kitties. Maybe we need to implement some kind of paging later -(defmethod collectibles/load-collectibles-fx ck [_ _ items-number address] - {:http-get {:url (str "https://api.cryptokitties.co/kitties?offset=0&limit=" - items-number - "&owner_wallet_address=" - address - "&parents=false") - :success-event-creator (fn [o] - [:load-kitties (map :id (:kitties (http/parse-payload o)))]) - :failure-event-creator (fn [o] - [:load-collectibles-failure (http/parse-payload o)]) - :timeout-ms 10000}}) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/collectibles/cryptokitties/views.cljs b/src/status_im/ui/screens/wallet/collectibles/cryptokitties/views.cljs deleted file mode 100644 index 638d941fa94..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/cryptokitties/views.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.cryptokitties.views - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.action-button.action-button :as action-button] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.react :as react] - [status-im.ui.screens.wallet.collectibles.styles :as styles] - [status-im.ui.screens.wallet.collectibles.views :as collectibles] - [status-im.ui.components.svgimage :as svgimage])) - -(defmethod collectibles/render-collectible :CK [_ {:keys [id name bio image_url]}] - [react/view {:style styles/details} - [react/view {:style styles/details-text} - [svgimage/svgimage {:style styles/details-image - :source {:uri image_url}}] - [react/view {:flex 1} - [react/text {:style styles/details-name} - (or name (i18n/label :t/cryptokitty-name {:id id}))] - [react/text {:number-of-lines 3 - :ellipsize-mode :tail} - bio]]] - [action-button/action-button - {:label (i18n/label :t/view-cryptokitties) - :icon :icons/address - :icon-opts {:color colors/blue} - :accessibility-label :open-collectible-button - :on-press #(re-frame/dispatch [:open-collectible-in-browser - (str "https://www.cryptokitties.co/kitty/" id)])}]]) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/collectibles/cryptostrikers/events.cljs b/src/status_im/ui/screens/wallet/collectibles/cryptostrikers/events.cljs deleted file mode 100644 index 90f910544b9..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/cryptostrikers/events.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.cryptostrikers.events - (:require [status-im.ui.screens.wallet.collectibles.events :as collectibles] - [status-im.utils.http :as http])) - -(def strikers :STRK) - -(defmethod collectibles/load-collectible-fx strikers [_ id] - {:http-get {:url (str "https://us-central1-cryptostrikers-prod.cloudfunctions.net/cards/" id) - :success-event-creator (fn [o] - [:load-collectible-success strikers {id (http/parse-payload o)}]) - :failure-event-creator (fn [o] - [:load-collectible-failure strikers {id (http/parse-payload o)}])}}) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/collectibles/cryptostrikers/views.cljs b/src/status_im/ui/screens/wallet/collectibles/cryptostrikers/views.cljs deleted file mode 100644 index 158b4048282..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/cryptostrikers/views.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.cryptostrikers.views - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.action-button.action-button :as action-button] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.react :as react] - [status-im.ui.screens.wallet.collectibles.styles :as styles] - [status-im.ui.components.svgimage :as svgimage] - [status-im.ui.screens.wallet.collectibles.views :as collectibles])) - -(defmethod collectibles/render-collectible :STRK [_ {:keys [external_url description name image]}] - [react/view {:style styles/details} - [react/view {:style styles/details-text} - [svgimage/svgimage {:style styles/details-image - :source {:uri image - :k 1.4}}] - [react/view {:flex 1 :justify-content :center} - [react/text {:style styles/details-name} - name] - [react/text - description]]] - [action-button/action-button - {:label (i18n/label :t/view-cryptostrikers) - :icon :icons/address - :icon-opts {:color colors/blue} - :accessibility-label :open-collectible-button - :on-press #(re-frame/dispatch [:open-collectible-in-browser external_url])}]]) diff --git a/src/status_im/ui/screens/wallet/collectibles/etheremon/events.cljs b/src/status_im/ui/screens/wallet/collectibles/etheremon/events.cljs deleted file mode 100644 index 396bd56ba98..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/etheremon/events.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.etheremon.events - (:require [status-im.ui.screens.wallet.collectibles.events :as collectibles] - [status-im.utils.http :as http])) - -(def emona :EMONA) - -(defmethod collectibles/load-collectible-fx emona [_ id] - {:http-get {:url (str "https://www.etheremon.com/api/monster/get_data?monster_ids=" id) - :success-event-creator (fn [o] - [:load-collectible-success emona (:data (http/parse-payload o))]) - :failure-event-creator (fn [o] - [:load-collectible-failure emona {id (http/parse-payload o)}])}}) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/collectibles/etheremon/views.cljs b/src/status_im/ui/screens/wallet/collectibles/etheremon/views.cljs deleted file mode 100644 index 3742b626bc7..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/etheremon/views.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.etheremon.views - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.action-button.action-button :as action-button] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.react :as react] - [status-im.ui.screens.wallet.collectibles.styles :as styles] - [status-im.ui.components.svgimage :as svgimage] - [status-im.ui.screens.wallet.collectibles.views :as collectibles])) - -(defmethod collectibles/render-collectible :EMONA [_ {:keys [user_defined_name image class_id]}] - [react/view {:style styles/details} - [react/view {:style styles/details-text} - [react/view {:flex 1} - [svgimage/svgimage {:style styles/details-image - :source {:uri image - :k 2}}]] - [react/view {:flex 1 :justify-content :center} - [react/text {:style styles/details-name} - user_defined_name]]] - [action-button/action-button - {:label (i18n/label :t/view-etheremon) - :icon :icons/address - :icon-opts {:color colors/blue} - :accessibility-label :open-collectible-button - :on-press #(re-frame/dispatch [:open-collectible-in-browser - (str "https://www.etheremon.com/#/mons/" class_id)])}]]) diff --git a/src/status_im/ui/screens/wallet/collectibles/events.cljs b/src/status_im/ui/screens/wallet/collectibles/events.cljs deleted file mode 100644 index a702bfdb391..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/events.cljs +++ /dev/null @@ -1,66 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.events - (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.utils.ethereum.erc721 :as erc721] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.money :as money])) - -(defmulti load-collectible-fx (fn [symbol _] symbol)) - -(defmethod load-collectible-fx :default [_ _] nil) - -(defmulti load-collectibles-fx (fn [_ symbol _ _] symbol)) - -(defmethod load-collectibles-fx :default [web3 symbol items-number address] - {:load-collectibles-fx [web3 symbol items-number address]}) - -(handlers/register-handler-fx - :show-collectibles-list - (fn [{:keys [db]} [_ address {:keys [symbol amount] :as collectible}]] - (let [items-number (money/to-number amount) - loaded-items-number (count (get-in db [:collectibles symbol]))] - (merge (when (not= items-number loaded-items-number) - (load-collectibles-fx (:web3 db) symbol items-number address)) - {:dispatch [:navigate-to :collectibles-list collectible]})))) - -(defn load-token [web3 i items-number contract address symbol] - (when (< i items-number) - (erc721/token-of-owner-by-index web3 contract address i - (fn [v1 v2] - (load-token web3 (inc i) items-number contract address symbol) - (re-frame/dispatch [:load-collectible symbol (.toNumber v2)]))))) - -(re-frame/reg-fx - :load-collectibles-fx - (fn [[web3 symbol items-number address]] - (let [contract (:address (tokens/symbol->token :mainnet symbol))] - (load-token web3 0 items-number contract address symbol)))) - -(handlers/register-handler-fx - :load-collectible - (fn [_ [_ symbol token-id]] - (load-collectible-fx symbol token-id))) - -(handlers/register-handler-fx - :load-collectible-success - [re-frame/trim-v] - (fn [{db :db} [symbol collectibles]] - {:db (update-in db [:collectibles symbol] merge collectibles)})) - -(handlers/register-handler-fx - :load-collectibles-failure - [re-frame/trim-v] - (fn [{db :db} [{:keys [message]}]] - {:db (assoc db :collectibles-failure message)})) - -(handlers/register-handler-fx - :load-collectible-failure - [re-frame/trim-v] - (fn [{db :db} [_]] - {:db db})) - -(handlers/register-handler-fx - :open-collectible-in-browser - [re-frame/trim-v] - (fn [_ [data]] - {:dispatch [:open-url-in-browser data]})) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/collectibles/styles.cljs b/src/status_im/ui/screens/wallet/collectibles/styles.cljs deleted file mode 100644 index 3c7df70d25d..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/styles.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.styles - (:require [status-im.ui.components.colors :as colors])) - -(def default-collectible - {:padding-left 10 - :padding-vertical 20}) - -(def loading-indicator - {:flex 1 - :align-items :center - :justify-content :center}) - -(def details - {:padding-vertical 10}) - -(def details-text - {:flex 1 - :flex-direction :row - :align-items :center - :padding-horizontal 16}) - -(def details-name - {:color colors/black - :text-align-vertical :center - :margin-bottom 10}) - -(def details-image - {:flex 1 - :margin 10}) diff --git a/src/status_im/ui/screens/wallet/collectibles/subs.cljs b/src/status_im/ui/screens/wallet/collectibles/subs.cljs deleted file mode 100644 index d9a74289460..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/subs.cljs +++ /dev/null @@ -1,11 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.subs - (:require [re-frame.core :as re-frame])) - -(re-frame/reg-sub :collectibles :collectibles) - -(re-frame/reg-sub - :screen-collectibles - :<- [:collectibles] - :<- [:get-screen-params] - (fn [[collectibles {:keys [symbol]}]] - (mapv #(assoc (second %) :id (first %)) (get collectibles symbol)))) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/collectibles/views.cljs b/src/status_im/ui/screens/wallet/collectibles/views.cljs deleted file mode 100644 index f978ff7c3e1..00000000000 --- a/src/status_im/ui/screens/wallet/collectibles/views.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.ui.screens.wallet.collectibles.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.styles :as component.styles] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.wallet.collectibles.styles :as styles])) - -(defmulti render-collectible (fn [symbol _] symbol)) - -(defmethod render-collectible :default [symbol {:keys [id name]}] - [react/view {:style styles/default-collectible} - [react/text (str (clojure.core/name symbol) " #" (or id name))]]) - -(defview collectibles-list [] - (letsubs [{:keys [name symbol]} [:get-screen-params] - collectibles [:screen-collectibles]] - [react/view {:style component.styles/flex} - [react/view {:style component.styles/flex} - [status-bar/status-bar] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title name]] - (if (seq collectibles) - [list/flat-list {:data collectibles - :key-fn (comp str :id) - :render-fn #(render-collectible symbol %)}] - [react/view {:style styles/loading-indicator} - [react/activity-indicator {:animating true :size :large :color colors/blue}]])]])) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/components/styles.cljs b/src/status_im/ui/screens/wallet/components/styles.cljs deleted file mode 100644 index 86b753c0519..00000000000 --- a/src/status_im/ui/screens/wallet/components/styles.cljs +++ /dev/null @@ -1,225 +0,0 @@ -(ns status-im.ui.screens.wallet.components.styles - (:require-macros [status-im.utils.styles :refer [defnstyle defstyle]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :as styles])) - -;; Components - -(def cartouche-container - {:flex 1 - :margin-top 16 - :margin-horizontal 16}) - -(def cartouche-header - {:color colors/white}) - -(defn cartouche-content-wrapper [disabled?] - (merge - {:flex-direction :row - :margin-top 8 - :border-radius styles/border-radius - :padding-left 14 - :padding-right 8} - (if disabled? - {:border-color colors/white-light-transparent - :border-width 1} - {:background-color colors/white-transparent}))) - -(def cartouche-icon-wrapper - {:flex 1 - :flex-direction :row - :justify-content :space-between - :align-items :center}) - -(def cartouche-text-wrapper - {:flex-direction :row - :justify-content :space-between - :padding-horizontal 15 - :padding-vertical 15}) - -(def cartouche-primary-text - {:color colors/white}) - -(def cartouche-secondary-text - {:color colors/white-transparent}) - -(def text-content - {:color colors/white}) - -(def text-secondary-content - {:color colors/white-lighter-transparent}) - -(def text - {:margin-right 10}) - -(def text-list-primary-content - (merge text {:color colors/black})) - -(def text-input - (merge text-content - {:font-size 15 - :flex 1 - :padding-bottom 0 - :padding-top 0 - :height 52 - :letter-spacing -0.2})) - -(def contact-code-text-input - {:text-align-vertical :top - :padding-top 16 - :padding-left 2 - :padding-right 8 - :height 72}) - -(defstyle label - {:color :white - :ios {:line-height 16} - :android {:font-size 12}}) - -(def amount-text-input-container - {:margin-top 8}) - -(def label-transparent - (merge label - {:color colors/white-lighter-transparent})) - -(defn amount-container [active?] - {:height 52 - :background-color (if active? - colors/white-light-transparent - styles/color-white-transparent-3) - :border-radius 8}) - -(def network - {:color :white - :font-size 13 - :letter-spacing -0.2}) - -(def network-container - {:padding-horizontal 10 - :height 27 - :border-radius 100 - :border-width 1 - :border-color colors/white-light-transparent - :align-items :center - :justify-content :center}) - -(def asset-container - {:margin-top 8 - :height 52 - :background-color styles/color-white-transparent-3 - :justify-content :center - :padding-left 14 - :padding-vertical 14 - :padding-right 8 - :border-radius 8}) - -(def asset-container-read-only - {:margin-top 8 - :height 52 - :border-color styles/color-white-transparent-3 - :border-width 1 - :justify-content :center - :padding-left 14 - :padding-vertical 14 - :padding-right 8 - :border-radius 8}) - -(def asset-content-container - {:flex-direction :row - :align-items :center - :margin-vertical 11}) - -(def asset-icon - {:background-color styles/color-gray9 - :border-radius 50}) - -(def asset-text-content - {:flex 1 - :flex-direction :row - :justify-content :space-between - :align-items :center}) - -(def asset-label-content - {:flex-direction :row - :margin-horizontal 10}) - -(def asset-label - {:margin-right 10}) - -(def asset-text - {:color styles/color-white}) - -(defstyle container-disabled - {:border-width 1 - :border-color colors/white-light-transparent - :background-color nil - :border-radius 8}) - -(def wallet-container - {:flex-direction :row - :margin-top 8 - :height 52 - :border-width 1 - :border-color colors/white-light-transparent - :align-items :center - :padding 14 - :border-radius 8}) - -(def wallet-name - {:color :white - :font-size 15 - :letter-spacing -0.2}) - -(defn participant [address?] - {:color (if address? :white colors/white-lighter-transparent) - :flex-shrink 1 - :font-size 15 - :letter-spacing -0.2}) - -(def recipient-container - {:flex-direction :row}) - -(def recipient-icon - {:margin-top 11}) - -(def recipient-name - {:flex 1 - :flex-direction :column - :margin-horizontal 12 - :margin-vertical 16}) - -(def recipient-address - {:margin-vertical 17 - :color colors/white}) - -(def recipient-no-address - {:color colors/white-lighter-transparent}) - -(def recent-recipients - {:flex 1 - :background-color colors/white}) - -(def wallet-value-container - {:flex 1 - :flex-direction :row}) - -(def wallet-value - {:padding-left 6 - :color styles/color-white-transparent-5 - :font-size 15 - :letter-spacing -0.2}) - -(def wallet-value-amount - {:flex -1}) - -(def separator - {:height 1 - :margin-horizontal 15 - :background-color styles/color-white-transparent-1}) - -(def button-text - {:color :white - :font-size 15 - :letter-spacing -0.2}) - diff --git a/src/status_im/ui/screens/wallet/components/views.cljs b/src/status_im/ui/screens/wallet/components/views.cljs deleted file mode 100644 index 3b9e94b5871..00000000000 --- a/src/status_im/ui/screens/wallet/components/views.cljs +++ /dev/null @@ -1,311 +0,0 @@ -(ns status-im.ui.screens.wallet.components.views - (:require-macros [status-im.utils.views :as views]) - (:require [clojure.string :as string] - [reagent.core :as reagent] - [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.bottom-buttons.view :as bottom-buttons] - [status-im.ui.components.button.view :as button] - [status-im.chat.views.photos :as photos] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.list.styles :as list.styles] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.react :as react] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.screens.wallet.components.styles :as styles] - [status-im.ui.screens.wallet.choose-recipient.views :as choose-recipient] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.ui.screens.wallet.utils :as wallet.utils] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.money :as money] - [status-im.utils.platform :as platform] - [status-im.ui.components.tooltip.views :as tooltip] - [status-im.ui.components.colors :as colors] - [status-im.utils.core :as utils.core] - [status-im.utils.utils :as utils.utils] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.icons.vector-icons :as vector-icons])) - -;; Wallet tab has a different coloring scheme (dark) that forces color changes (background, text) -;; It might be replaced by some theme mechanism - -(defn text-input [props text] - [react/text-input (utils.core/deep-merge {:placeholder-text-color colors/white-lighter-transparent - :selection-color colors/white - :style {:color colors/white - :font-size 15 - :height 52 - :letter-spacing -0.2}} - props) - text]) - -(def default-action (actions/back-white actions/default-handler)) - -(defn- toolbar - ([title] (toolbar {} title)) - ([props title] (toolbar props default-action title)) - ([props action title] (toolbar props action title nil)) - ([props action title options] - [toolbar/toolbar (utils.core/deep-merge {:style wallet.styles/toolbar} - props) - [toolbar/nav-button action] - [toolbar/content-title {:color :white} - title] - options])) - -(defn- top-view [avoid-keyboard?] - (if avoid-keyboard? - react/keyboard-avoiding-view - react/view)) - -(defn simple-screen - ([toolbar content] (simple-screen nil toolbar content)) - ([{:keys [avoid-keyboard? status-bar-type]} toolbar content] - [(top-view avoid-keyboard?) {:flex 1 :background-color colors/blue} - [status-bar/status-bar {:type (or status-bar-type :wallet)}] - toolbar - content])) - -(defn- cartouche-content [{:keys [disabled?]} content] - [react/view {:style (styles/cartouche-content-wrapper disabled?)} - [react/view {:flex 1} - content]]) - -(defn cartouche [{:keys [disabled? on-press icon icon-opts] :or {icon :icons/forward} :as m} header content] - [react/view {:style styles/cartouche-container} - [react/text {:style styles/cartouche-header} - header] - (if (or disabled? (nil? on-press)) - [cartouche-content m content] - [react/touchable-highlight {:on-press on-press} - [react/view - [cartouche-content m - (if-not (true? disabled?) - [react/view styles/cartouche-icon-wrapper - [react/view {:flex 1} ;; Let content shrink if needed - content] - [vector-icons/icon icon (merge {:color :white} icon-opts)]] - content)]]])]) - -(defn- cartouche-primary-text [s] - [react/text {:style styles/cartouche-primary-text} - s]) - -(defn- cartouche-secondary-text [s] - [react/text {:style styles/cartouche-secondary-text} - s]) - -(defn cartouche-text-content [primary secondary] - [react/view styles/cartouche-text-wrapper - [cartouche-primary-text primary] - [cartouche-secondary-text secondary]]) - -(defn view-asset [symbol] - [react/view - [react/i18n-text {:style styles/label :key :wallet-asset}] - [react/view styles/asset-container-read-only - [react/text {:style styles/asset-text} - (name symbol)]]]) - -(defn- type->handler [k] - (case k - :send :wallet.send/set-symbol - :request :wallet.request/set-symbol - (throw (str "Unknown type: " k)))) - -(defn- render-token [{:keys [symbol name icon decimals amount]} type] - [list/touchable-item #(do (re-frame/dispatch [(type->handler type) symbol]) - (re-frame/dispatch [:navigate-back])) - [react/view - [list/item - [list/item-image icon] - [list/item-content - [react/view {:flex-direction :row} - [react/text {:style styles/text-list-primary-content} - name] - [react/text {:force-uppercase? true} - (clojure.core/name symbol)]] - [list/item-secondary (wallet.utils/format-amount amount decimals)]]]]]) - -(views/defview assets [type] - (views/letsubs [assets [:wallet/transferrable-assets-with-amount]] - [simple-screen - [toolbar (i18n/label :t/wallet-assets)] - [react/view {:style (assoc components.styles/flex :background-color :white)} - [list/flat-list {:default-separator? true - :data assets - :key-fn (comp str :symbol) - :render-fn #(render-token % type)}]]])) - -(defn send-assets [] - [assets :send]) - -(defn request-assets [] - [assets :request]) - -(defn- type->view [k] - (case k - :send :wallet-send-assets - :request :wallet-request-assets - (throw (str "Unknown type: " k)))) - -(views/defview asset-selector [{:keys [disabled? type symbol error]}] - (views/letsubs [balance [:balance] - network [:network]] - (let [{:keys [name icon decimals]} (tokens/asset-for (ethereum/network->chain-keyword network) symbol)] - [react/view - [cartouche {:disabled? disabled? :on-press #(re-frame/dispatch [:navigate-to (type->view type)])} - (i18n/label :t/wallet-asset) - [react/view {:style styles/asset-content-container - :accessibility-label :choose-asset-button} - [list/item-image (assoc icon :style styles/asset-icon :image-style {:width 32 :height 32})] - [react/view styles/asset-text-content - [react/view styles/asset-label-content - [react/text {:style (merge styles/text-content styles/asset-label)} - name] - [react/text {:style styles/text-secondary-content} - (clojure.core/name symbol)]] - [react/text {:style (merge styles/text-secondary-content styles/asset-label)} - (str (wallet.utils/format-amount (get balance symbol) decimals))]]]] - (when error - [tooltip/tooltip error {}])]))) - -(defn- recipient-address [address modal?] - [react/text {:style (merge styles/recipient-address (when-not address styles/recipient-no-address)) - :accessibility-label :recipient-address-text} - (or (ethereum/normalized-address address) - (if modal? - (i18n/label :t/new-contract) - (i18n/label :t/specify-recipient)))]) - -(views/defview recipient-contact [address name request?] - (views/letsubs [contact [:get-contact-by-address address]] - (let [address? (and (not (nil? address)) (not= address ""))] - [react/view styles/recipient-container - [react/view styles/recipient-icon - (when contact - [photos/photo (:photo-path contact) {:size list.styles/image-size}])] - [react/view {:style styles/recipient-name} - [react/text {:style (styles/participant true) - :accessibility-label (if request? :contact-name-text :recipient-name-text) - :number-of-lines 1} - name] - [react/text {:style (styles/participant (and (not name) address?)) - :accessibility-label (if request? :contact-address-text :recipient-address-text)} - (ethereum/normalized-address address)]]]))) - -(defn render-contact [contact] - [list/touchable-item #(re-frame/dispatch [:wallet/fill-request-from-contact contact]) - [list/item - [photos/photo (:photo-path contact) {:size list.styles/image-size}] - [list/item-content - [list/item-primary {:accessibility-label :contact-name-text} - (:name contact)] - [react/text {:style list.styles/secondary-text - :accessibility-label :contact-address-text} - (ethereum/normalized-address (:address contact))]]]]) - -(views/defview recent-recipients [] - (views/letsubs [contacts [:all-added-people-contacts]] - [simple-screen - [toolbar (i18n/label :t/recipient)] - [react/view styles/recent-recipients - [list/flat-list {:data contacts - :key-fn :address - :render-fn render-contact}]]])) - -(defn contact-code [] - (let [content (reagent/atom nil)] - (fn [] - [simple-screen {:avoid-keyboard? true} - [toolbar {:style wallet.styles/toolbar-bottom-line} - default-action - (i18n/label :t/recipient)] - [react/view components.styles/flex - [cartouche {} - (i18n/label :t/recipient) - [text-input {:multiline true - :style styles/contact-code-text-input - :placeholder (i18n/label :t/recipient-code) - :on-change-text #(reset! content %) - :accessibility-label :recipient-address-input}]] - [bottom-buttons/bottom-button - [button/button {:disabled? (string/blank? @content) - :on-press #(re-frame/dispatch [:wallet/fill-request-from-url @content :code]) - :fit-to-text? false} - (i18n/label :t/done)]]]]))) - -(defn recipient-qr-code [] - [choose-recipient/choose-recipient]) - -(defn- request-camera-permissions [] - (re-frame/dispatch [:request-permissions {:permissions [:camera] - :on-allowed #(re-frame/dispatch [:navigate-to :recipient-qr-code]) - :on-denied #(utils.utils/set-timeout - (fn [] - (utils.utils/show-popup (i18n/label :t/error) - (i18n/label :t/camera-access-error))) - 50)}])) - -(defn- on-choose-recipient [contact-only?] - (list-selection/show {:title (i18n/label :t/wallet-choose-recipient) - :options (concat - [{:label (i18n/label :t/recent-recipients) - :action #(re-frame/dispatch [:navigate-to :recent-recipients])}] - (when-not contact-only? - [{:label (i18n/label :t/scan-qr) - :action request-camera-permissions} - {:label (i18n/label :t/recipient-code) - :action #(re-frame/dispatch [:navigate-to :contact-code])}]))})) - -(defn recipient-selector [{:keys [name address disabled? contact-only? request? modal?]}] - [cartouche {:on-press #(on-choose-recipient contact-only?) - :disabled? disabled? - :icon :icons/dots-horizontal - :icon-opts {:accessibility-label :choose-contact-button}} - (i18n/label :t/wallet-choose-recipient) - [react/view {:accessibility-label :choose-recipient-button} - (if name - [recipient-contact address name request?] - [recipient-address address modal?])]]) - -(defn- amount-input [{:keys [input-options amount amount-text disabled?]} - {:keys [symbol decimals]}] - [react/view {:style components.styles/flex - :accessibility-label :specify-amount-button} - [text-input - (merge - input-options - ;; We only auto-correct and prettify user's input when it is valid and positive. - ;; Otherwise, user might want to fix his input and autocorrection will give more harm than good. - ;; Positive check is because we don't want to replace unfinished 0.000 with just plain 0, that is annoying and - ;; potentially dangerous on this screen (e.g. sending 7 ETH instead of 0.0007) - {:default-value (if (empty? amount-text) - (str (money/to-fixed (money/internal->formatted amount symbol decimals))) - amount-text)} - (if disabled? - {:editable false} - {:keyboard-type :numeric - :placeholder (i18n/label :t/amount-placeholder) - :style components.styles/flex - :accessibility-label :amount-input}))]]) - -(defn amount-selector [{:keys [error disabled?] :as m} token] - [react/view - [cartouche {:disabled? disabled?} - (i18n/label :t/amount) - [amount-input m token]] - (when error - [tooltip/tooltip error])]) - -(defn separator [] - [react/view styles/separator]) - -(defn button-text [label] - [react/text {:style styles/button-text - :font (if platform/android? :medium :default) - :uppercase? true} - label]) diff --git a/src/status_im/ui/screens/wallet/db.cljs b/src/status_im/ui/screens/wallet/db.cljs deleted file mode 100644 index 8c64f859a2c..00000000000 --- a/src/status_im/ui/screens/wallet/db.cljs +++ /dev/null @@ -1,39 +0,0 @@ -(ns status-im.ui.screens.wallet.db - (:require [cljs.spec.alpha :as spec] - [status-im.i18n :as i18n] - status-im.ui.screens.wallet.request.db - status-im.ui.screens.wallet.send.db - [status-im.utils.money :as money])) - -(spec/def :wallet.send/recipient string?) - -(spec/def :wallet/send (spec/keys :req-un [:wallet.send/recipient])) - -(spec/def :wallet/wallet (spec/keys :opt-un [:wallet/send-transaction :wallet/request-transaction - :wallet/transactions-queue])) - -;; Placeholder namespace for wallet specs, which are a WIP depending on data -;; model we decide on for balances, prices, etc. - - -(defn- too-precise-amount? - "Checks if number has any extra digit beyond the allowed number of decimals. - It does so by checking the number against its rounded value." - [amount decimals] - (let [bn (money/bignumber amount)] - (not (.eq bn - (.round bn decimals))))) - -(defn parse-amount [amount decimals] - (when-not (empty? amount) - (let [normalized-amount (money/normalize amount) - value (money/bignumber normalized-amount)] - (cond - (not (money/valid? value)) - {:error (i18n/label :t/validation-amount-invalid-number) :value value} - - (too-precise-amount? normalized-amount decimals) - {:error (i18n/label :t/validation-amount-is-too-precise {:decimals decimals}) :value value} - - :else - {:value value})))) diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs deleted file mode 100644 index e23d461d130..00000000000 --- a/src/status_im/ui/screens/wallet/events.cljs +++ /dev/null @@ -1,292 +0,0 @@ -(ns status-im.ui.screens.wallet.events - (:require [clojure.set :as set] - [re-frame.core :as re-frame :refer [dispatch reg-fx]] - [status-im.i18n :as i18n] - [status-im.ui.screens.wallet.navigation] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.erc20 :as erc20] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.handlers :as handlers] - [status-im.utils.prices :as prices] - [status-im.utils.transactions :as transactions] - [status-im.models.wallet :as models.wallet] - [taoensso.timbre :as log] - status-im.ui.screens.wallet.request.events - [status-im.constants :as constants] - [status-im.ui.screens.navigation :as navigation] - [status-im.utils.money :as money] - [status-im.wallet.transactions :as wallet.transactions])) - -(defn get-balance [{:keys [web3 account-id on-success on-error]}] - (if (and web3 account-id) - (.getBalance - (.-eth web3) - account-id - (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))) - (on-error "web3 or account-id not available"))) - -(defn get-token-balance [{:keys [web3 contract account-id on-success on-error]}] - (if (and web3 contract account-id) - (erc20/balance-of - web3 - contract - (ethereum/normalized-address account-id) - (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))) - (on-error "web3, contract or account-id not available"))) - -(defn assoc-error-message [db error-type err] - (assoc-in db [:wallet :errors error-type] (or (when err (str err)) - :unknown-error))) - -(defn clear-error-message [db error-type] - (update-in db [:wallet :errors] dissoc error-type)) - -;; FX - -(reg-fx - :get-balance - (fn [{:keys [web3 account-id success-event error-event]}] - (get-balance {:web3 web3 - :account-id account-id - :on-success #(re-frame/dispatch [success-event %]) - :on-error #(re-frame/dispatch [error-event %])}))) - -(reg-fx - :get-tokens-balance - (fn [{:keys [web3 symbols chain account-id success-event error-event]}] - (doseq [symbol symbols] - (let [contract (:address (tokens/symbol->token chain symbol))] - (get-token-balance {:web3 web3 - :contract contract - :account-id account-id - :on-success #(re-frame/dispatch [success-event symbol %]) - :on-error #(re-frame/dispatch [error-event symbol %])}))))) - -(reg-fx - :get-transactions - (fn [{:keys [web3 chain account-id token-addresses success-event error-event]}] - (transactions/get-transactions chain - account-id - #(re-frame/dispatch [success-event % account-id]) - #(re-frame/dispatch [error-event %])) - (doseq [direction [:inbound :outbound]] - (erc20/get-token-transactions web3 - chain - token-addresses - direction - account-id - #(re-frame/dispatch [success-event % account-id]))))) - -;; TODO(oskarth): At some point we want to get list of relevant assets to get prices for -(reg-fx - :get-prices - (fn [{:keys [from to success-event error-event]}] - (prices/get-prices from - to - #(re-frame/dispatch [success-event %]) - #(re-frame/dispatch [error-event %])))) - -(reg-fx - :update-gas-price - (fn [{:keys [web3 success-event edit?]}] - (ethereum/gas-price web3 #(re-frame/dispatch [success-event %2 edit?])))) - -(reg-fx - :update-estimated-gas - (fn [{:keys [web3 obj success-event]}] - (ethereum/estimate-gas-web3 web3 (clj->js obj) #(re-frame/dispatch [success-event %2])))) - -(defn tokens-symbols [v chain] - (set/difference (set v) (set (map :symbol (tokens/nfts-for chain))))) - -(defn update-wallet [{{:keys [web3 network network-status] {:keys [address settings]} :account/account :as db} :db}] - (let [network (get-in db [:account/account :networks network]) - chain (ethereum/network->chain-keyword network) - mainnet? (= :mainnet chain) - assets (get-in settings [:wallet :visible-tokens chain]) - tokens (tokens-symbols (get-in settings [:wallet :visible-tokens chain]) chain) - currency-id (or (get-in settings [:wallet :currency]) :usd) - currency (get constants/currencies currency-id)] - (when (not= network-status :offline) - {:get-balance {:web3 web3 - :account-id address - :success-event :update-balance-success - :error-event :update-balance-fail} - :get-tokens-balance {:web3 web3 - :account-id address - :symbols assets - :chain chain - :success-event :update-token-balance-success - :error-event :update-token-balance-fail} - :get-prices {:from (if mainnet? (conj tokens "ETH") ["ETH"]) - :to [(:code currency)] - :success-event :update-prices-success - :error-event :update-prices-fail} - :db (-> db - (clear-error-message :prices-update) - (clear-error-message :balance-update) - (assoc-in [:wallet :balance-loading?] true) - (assoc :prices-loading? true))}))) - -;; Handlers -(handlers/register-handler-fx - :update-wallet - (fn [cofx _] - (update-wallet cofx))) - -(handlers/register-handler-fx - :update-transactions - (fn [cofx _] - (wallet.transactions/run-update cofx))) - -(defn combine-entries [transaction token-transfer] - (merge transaction (select-keys token-transfer [:symbol :from :to :value :type :token :transfer]))) - -(defn update-confirmations [tx1 tx2] - (assoc tx1 :confirmations (max (:confirmations tx1) - (:confirmations tx2)))) - -(defn- tx-and-transfer? - "A helper function that checks if first argument is a transaction and the second argument a token transfer object." - [tx1 tx2] - (and (not (:transfer tx1)) (:transfer tx2))) - -(defn- both-transfer? - [tx1 tx2] - (and (:transfer tx1) (:transfer tx2))) - -(defn dedupe-transactions [tx1 tx2] - (cond (tx-and-transfer? tx1 tx2) (combine-entries tx1 tx2) - (tx-and-transfer? tx2 tx1) (combine-entries tx2 tx1) - (both-transfer? tx1 tx2) (update-confirmations tx1 tx2) - :else tx2)) - -(defn own-transaction? [address [_ {:keys [type to from]}]] - (let [normalized (ethereum/normalized-address address)] - (or (and (= :inbound type) (= normalized (ethereum/normalized-address to))) - (and (= :outbound type) (= normalized (ethereum/normalized-address from))) - (and (= :failed type) (= normalized (ethereum/normalized-address from)))))) - -(handlers/register-handler-db - :update-transactions-success - (fn [db [_ transactions address]] - ;; NOTE(goranjovic): we want to only show transactions that belong to the current account - ;; this filter is to prevent any late transaction updates initated from another account on the same - ;; device from being applied in the current account. - (let [own-transactions (into {} (filter #(own-transaction? address %) transactions))] - (-> db - (update-in [:wallet :transactions] #(merge-with dedupe-transactions % own-transactions)) - (assoc-in [:wallet :transactions-loading?] false))))) - -(handlers/register-handler-db - :update-transactions-fail - (fn [db [_ err]] - (log/debug "Unable to get transactions: " err) - (-> db - (assoc-error-message :transactions-update err) - (assoc-in [:wallet :transactions-loading?] false)))) - -(handlers/register-handler-db - :update-balance-success - (fn [db [_ balance]] - (-> db - (assoc-in [:wallet :balance :ETH] balance) - (assoc-in [:wallet :balance-loading?] false)))) - -(handlers/register-handler-db - :update-balance-fail - (fn [db [_ err]] - (log/debug "Unable to get balance: " err) - (-> db - (assoc-error-message :balance-update err) - (assoc-in [:wallet :balance-loading?] false)))) - -(defn update-token-balance-success [symbol balance {:keys [db]}] - {:db (-> db - (assoc-in [:wallet :balance symbol] balance) - (assoc-in [:wallet :balance-loading?] false))}) - -(handlers/register-handler-fx - :update-token-balance-success - (fn [cofx [_ symbol balance]] - (update-token-balance-success symbol balance cofx))) - -(handlers/register-handler-db - :update-token-balance-fail - (fn [db [_ symbol err]] - (log/debug "Unable to get token " symbol "balance: " err) - (-> db - (assoc-error-message :balance-update err) - (assoc-in [:wallet :balance-loading?] false)))) - -(handlers/register-handler-db - :update-prices-success - (fn [db [_ prices]] - (assoc db - :prices prices - :prices-loading? false))) - -(handlers/register-handler-db - :update-prices-fail - (fn [db [_ err]] - (log/debug "Unable to get prices: " err) - (-> db - (assoc-error-message :prices-update err) - (assoc :prices-loading? false)))) - -(handlers/register-handler-fx - :show-transaction-details - (fn [{:keys [db]} [_ hash]] - {:db (assoc-in db [:wallet :current-transaction] hash) - :dispatch [:navigate-to :wallet-transaction-details]})) - -(handlers/register-handler-fx - :wallet/show-sign-transaction - (fn [{:keys [db]} [_ {:keys [id method]} from-chat?]] - {:db (assoc-in db [:wallet :send-transaction] {:id id - :method method - :from-chat? from-chat?}) - :dispatch [:navigate-to-modal :wallet-send-transaction-modal]})) - -(handlers/register-handler-db - :wallet/update-gas-price-success - (fn [db [_ price edit?]] - (if edit? - (:db (models.wallet/edit-value - :gas-price - (money/to-fixed - (money/wei-> :gwei price)) - {:db db})) - (assoc-in db [:wallet :send-transaction :gas-price] price)))) - -(handlers/register-handler-fx - :wallet/update-estimated-gas - (fn [{:keys [db]} [_ obj]] - {:update-estimated-gas {:web3 (:web3 db) - :obj obj - :success-event :wallet/update-estimated-gas-success}})) - -(handlers/register-handler-db - :wallet/update-estimated-gas-success - (fn [db [_ gas]] - (if gas - (assoc-in db [:wallet :send-transaction :gas] (money/bignumber (int (* gas 1.2)))) - db))) - -(handlers/register-handler-fx - :wallet/show-error - (fn [] - {:show-error (i18n/label :t/wallet-error)})) - -(handlers/register-handler-fx - :wallet-setup-navigate-back - (fn [{:keys [db]}] - {:db (-> db - (assoc-in [:wallet :send-transaction] {}) - (navigation/navigate-back))})) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/main/styles.cljs b/src/status_im/ui/screens/wallet/main/styles.cljs deleted file mode 100644 index a64532ed353..00000000000 --- a/src/status_im/ui/screens/wallet/main/styles.cljs +++ /dev/null @@ -1,136 +0,0 @@ -(ns status-im.ui.screens.wallet.main.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -;; Main section - -(defstyle main-section - {:flex 1 - :android {:background-color colors/white} - :ios {:background-color colors/blue}}) - -(defstyle scroll-bottom - {:background-color colors/white - :zIndex -1 - :position :absolute - :left 0 - :right 0 - :android {:height 0} - :ios {:height 9999}}) - -(def section - {:background-color colors/blue}) - -(def backup-seed-phrase-container - {:flex-direction :row - :align-items :center - :border-radius 8 - :margin 16 - :background-color colors/black-darker-transparent - :padding-top 10 - :padding-bottom 10 - :padding-left 14 - :padding-right 12}) - -(def backup-seed-phrase-text-container - {:flex 1}) - -(def backup-seed-phrase-title - {:font-size 15 - :line-height 20 - :color colors/white}) - -(def backup-seed-phrase-description - {:line-height 20 - :color colors/white-lighter-transparent}) - -(def total-balance-container - {:align-items :center - :justify-content :center}) - -(def total-balance - {:flex-direction :row}) - -(def total-balance-value - {:font-size 37 - :color colors/white}) - -(def total-value - {:color colors/white-transparent}) - -(defstyle total-balance-currency - {:font-size 37 - :margin-left 9 - :color colors/white-lighter-transparent - :android {:letter-spacing 1.5} - :ios {:letter-spacing 1.16}}) - -;; Actions section - -(def action-section - {:background-color colors/blue}) - -(def action - {:background-color colors/white-transparent - :width 40 - :height 40 - :border-radius 50}) - -(def action-label - {:color :white}) - -(def action-separator - {:height 1 - :background-color colors/white-light-transparent - :margin-left 70}) - -;; Assets section - -(def asset-section - {:flex 1 - :padding-top 16 - :background-color colors/white}) - -(def asset-item-container - {:flex 1 - :flex-direction :row - :align-items :center - :justify-content :space-between}) - -(def asset-item-value-container - {:flex 1 - :flex-direction :row - :align-items :center}) - -(def asset-item-value - {:flex -1 - :font-size 16 - :color colors/black}) - -(def asset-item-currency - {:font-size 16 - :color colors/gray - :margin-left 6}) - -(def asset-item-price - {:font-size 16 - :color colors/gray - :margin-left 6}) - -(def wallet-address - {:color :white - :text-align :center - :font-size 15 - :letter-spacing -0.2 - :line-height 20}) - -(def address-section - (merge - section - {:flex-grow 1 - :align-items :center - :padding 20})) - -(def modal-history - {:flex 1 - :background-color :white}) diff --git a/src/status_im/ui/screens/wallet/main/views.cljs b/src/status_im/ui/screens/wallet/main/views.cljs deleted file mode 100644 index eb6929a76ec..00000000000 --- a/src/status_im/ui/screens/wallet/main/views.cljs +++ /dev/null @@ -1,192 +0,0 @@ -(ns status-im.ui.screens.wallet.main.views - (:require-macros [status-im.utils.views :as views]) - (:require [reagent.core :as reagent] - [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.screens.wallet.onboarding.views :as onboarding.views] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.ui.screens.wallet.main.styles :as styles] - [status-im.ui.screens.wallet.utils :as wallet.utils] - [status-im.utils.money :as money] - [status-im.ui.components.toolbar.actions :as action] - status-im.ui.screens.wallet.collectibles.etheremon.views - status-im.ui.screens.wallet.collectibles.cryptostrikers.views - status-im.ui.screens.wallet.collectibles.cryptokitties.views - [status-im.ui.components.status-bar.view :as status-bar.view] - [status-im.ui.components.text :as text] - [status-im.ui.screens.wallet.transactions.views :as transactions.views])) - -(defn toolbar-view [] - [toolbar/toolbar {:style wallet.styles/toolbar :flat? true} - nil - [toolbar/content-wrapper] - [toolbar/actions - [{:icon :icons/options - :icon-opts {:color :white - :accessibility-label :options-menu-button} - :options [{:label (i18n/label :t/wallet-manage-assets) - :action #(re-frame/dispatch [:navigate-to-modal :wallet-settings-assets])}]}]]]) - -(defn toolbar-modal [modal-history?] - [react/view - [status-bar.view/status-bar {:type :modal-wallet}] - [toolbar/toolbar {:style wallet.styles/toolbar :flat? true} - [toolbar/nav-button (action/close-white action/default-handler)] - [toolbar/content-wrapper] - [toolbar/actions - [{:icon (if modal-history? :icons/wallet :icons/transaction-history) - :icon-opts {:color :white - :accessibility-label (if modal-history? :wallet-modal-button :transaction-history-button)} - :handler #(re-frame/dispatch [:set-in [:wallet :modal-history?] (not modal-history?)])}]]]]) - -(defn- total-section [value currency] - [react/view styles/section - [react/view {:style styles/total-balance-container} - [react/view {:style styles/total-balance} - [react/text {:style styles/total-balance-value - :accessibility-label :total-amount-value-text} - value] - [react/text {:style styles/total-balance-currency - :accessibility-label :total-amount-currency-text} - (:code currency)]] - [react/i18n-text {:style styles/total-value - :key :wallet-total-value}]]]) - -(defn- backup-seed-phrase [] - [react/view styles/section - [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to :backup-seed])} - [react/view styles/backup-seed-phrase-container - [react/view styles/backup-seed-phrase-text-container - [react/i18n-text {:style styles/backup-seed-phrase-title - :key :wallet-backup-recovery-title}] - [react/i18n-text {:style styles/backup-seed-phrase-description - :key :wallet-backup-recovery-description}]] - [vector-icons/icon :icons/forward {:color :white}]]]]) - -(def actions - [{:label (i18n/label :t/send-transaction) - :accessibility-label :send-transaction-button - :icon :icons/arrow-right - :action #(re-frame/dispatch [:navigate-to :wallet-send-transaction])} - {:label (i18n/label :t/receive-transaction) - :accessibility-label :receive-transaction-button - :icon :icons/arrow-left - :action #(re-frame/dispatch [:navigate-to :wallet-request-transaction])} - {:label (i18n/label :t/transaction-history) - :accessibility-label :transaction-history-button - :icon :icons/transaction-history - :action #(re-frame/dispatch [:navigate-to :transactions-history])}]) - -(defn- render-asset [currency] - (fn [{:keys [symbol icon decimals amount]}] - (let [asset-value (re-frame/subscribe [:asset-value symbol decimals (-> currency :code keyword)])] - [react/view {:style styles/asset-item-container} - [list/item - [list/item-image icon] - [react/view {:style styles/asset-item-value-container} - [react/text {:style styles/asset-item-value - :number-of-lines 1 - :ellipsize-mode :tail - :accessibility-label (str (-> symbol name clojure.string/lower-case) "-asset-value-text")} - (wallet.utils/format-amount amount decimals)] - [react/text {:style styles/asset-item-currency - :uppercase? true - :number-of-lines 1} - (clojure.core/name symbol)]] - [react/text {:style styles/asset-item-price - :uppercase? true - :number-of-lines 1} - (or @asset-value "...")]]]))) - -(def item-icon-forward - [list/item-icon {:icon :icons/forward - :icon-opts {:color :gray}}]) - -(defn- render-collectible [address-hex {:keys [symbol name icon amount] :as collectible} modal?] - (let [items-number (money/to-fixed amount) - details? (and (pos? items-number) (not modal?))] - [react/touchable-highlight - (when details? - {:on-press #(re-frame/dispatch [:show-collectibles-list address-hex collectible])}) - [react/view {:style styles/asset-item-container} - [list/item - [list/item-image icon] - [react/view {:style styles/asset-item-value-container} - [react/text {:style styles/asset-item-value - :number-of-lines 1 - :ellipsize-mode :tail - :accessibility-label (str (-> symbol clojure.core/name clojure.string/lower-case) - "-collectible-value-text")} - (or items-number "...")] - [react/text {:style styles/asset-item-currency - :number-of-lines 1} - name]] - (when details? - item-icon-forward)]]])) - -(defn group-assets [v] - (group-by #(if (:nft? %) :nfts :tokens) v)) - -(defn- asset-section [assets currency address-hex modal?] - (let [{:keys [tokens nfts]} (group-assets assets)] - [react/view styles/asset-section - [list/section-list - {:default-separator? true - :scroll-enabled false - :key-fn (comp str :symbol) - :sections [{:title (i18n/label :t/wallet-assets) - :key :assets - :data tokens - :render-fn (render-asset currency)} - {:title (i18n/label :t/wallet-collectibles) - :key :collectibles - :data nfts - :render-fn #(render-collectible address-hex % modal?)}]}]])) - -(views/defview wallet-root [modal?] - (views/letsubs [assets [:wallet/visible-assets-with-amount] - currency [:wallet/currency] - portfolio-value [:portfolio-value] - {:keys [modal-history?]} [:get :wallet] - {:keys [seed-backed-up?]} [:get-current-account] - address-hex [:get-current-account-hex]] - [react/view styles/main-section - (if modal? - [toolbar-modal modal-history?] - [toolbar-view]) - (if (and modal? modal-history?) - [react/view styles/modal-history - [transactions.views/history-list true]] - [react/scroll-view {:refresh-control - (reagent/as-element - [react/refresh-control {:on-refresh #(re-frame/dispatch [:update-wallet]) - :tint-color :white - :refreshing false}])} - [total-section portfolio-value currency] - (when (and (not modal?) - (not seed-backed-up?) - (some (fn [{:keys [amount]}] - (and amount (not (.isZero amount)))) - assets)) - [backup-seed-phrase]) - (if modal? - [react/view styles/address-section - [text/selectable-text {:value address-hex :style styles/wallet-address}]] - [list/action-list actions - {:container-style styles/action-section}]) - [asset-section assets currency address-hex modal?] - ;; Hack to allow different colors for bottom scroll view (iOS only) - [react/view {:style styles/scroll-bottom}]])])) - -(views/defview wallet-modal [] - [wallet-root true]) - -(views/defview wallet [] - (views/letsubs [{:keys [wallet-set-up-passed?]} [:get-current-account]] - (if (not wallet-set-up-passed?) - [onboarding.views/onboarding] - [wallet-root false]))) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/navigation.cljs b/src/status_im/ui/screens/wallet/navigation.cljs deleted file mode 100644 index aa560a3fc34..00000000000 --- a/src/status_im/ui/screens/wallet/navigation.cljs +++ /dev/null @@ -1,44 +0,0 @@ -(ns status-im.ui.screens.wallet.navigation - (:require [re-frame.core :as re-frame] - [status-im.ui.screens.navigation :as navigation] - [status-im.utils.ethereum.core :as ethereum])) - -(defmethod navigation/preload-data! :wallet - [db _] - (re-frame/dispatch [:update-wallet]) - (assoc-in db [:wallet :current-tab] 0)) - -(defmethod navigation/preload-data! :wallet-modal - [db _] - (re-frame/dispatch [:update-wallet]) - (re-frame/dispatch [:update-transactions]) - (assoc-in db [:wallet :modal-history?] false)) - -(defmethod navigation/preload-data! :transactions-history - [db _] - (re-frame/dispatch [:update-transactions]) - db) - -(def transaction-send-default - (let [symbol :ETH] - {:gas (ethereum/estimate-gas symbol) - :symbol symbol})) - -(def transaction-request-default - {:symbol :ETH}) - -(defmethod navigation/preload-data! :wallet-request-transaction - [db [event]] - (if (= event :navigate-back) - db - (-> db - (assoc-in [:wallet :request-transaction] transaction-request-default) - (assoc-in [:wallet :send-transaction] transaction-send-default)))) - -(defmethod navigation/preload-data! :wallet-send-transaction - [db [event]] - (if (= event :navigate-back) - db - (do - (re-frame/dispatch [:wallet/update-gas-price]) - (assoc-in db [:wallet :send-transaction] transaction-send-default)))) diff --git a/src/status_im/ui/screens/wallet/onboarding/setup/styles.cljs b/src/status_im/ui/screens/wallet/onboarding/setup/styles.cljs deleted file mode 100644 index fed2111e364..00000000000 --- a/src/status_im/ui/screens/wallet/onboarding/setup/styles.cljs +++ /dev/null @@ -1,47 +0,0 @@ -(ns status-im.ui.screens.wallet.onboarding.setup.styles - (:require [status-im.ui.components.colors :as colors])) - -(def setup-image-container - {:align-items :center - :margin 41}) - -(def setup-image - {:width 151 - :height 77}) - -(def signing-phrase - {:background-color colors/white - :border-radius 8 - :margin-left 16 - :margin-right 16 - :flex-direction :row}) - -(def signing-word - {:flex 1 - :height 52 - :align-items :center - :justify-content :center}) - -(def signing-word-text - {:font-size 15 - :letter-spacing -0.2}) - -(def description - {:font-size 14 - :letter-spacing -0.2 - :color colors/white - :margin-left 24 - :margin-right 24 - :margin-top 16 - :text-align :center}) - -(def bottom-buttons - {:background-color colors/blue - :padding-vertical 8}) - -(def got-it-button-text - {:padding-horizontal 0}) - -(def modal - {:flex 1 - :background-color colors/blue}) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/onboarding/setup/views.cljs b/src/status_im/ui/screens/wallet/onboarding/setup/views.cljs deleted file mode 100644 index 0fba97cba05..00000000000 --- a/src/status_im/ui/screens/wallet/onboarding/setup/views.cljs +++ /dev/null @@ -1,64 +0,0 @@ -(ns status-im.ui.screens.wallet.onboarding.setup.views - (:require-macros [status-im.utils.views :as views]) - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.react-native.resources :as resources] - [status-im.ui.components.react :as react] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.screens.wallet.components.views :as wallet.components] - [status-im.ui.screens.wallet.onboarding.setup.styles :as styles] - [status-im.ui.components.bottom-buttons.view :as bottom-buttons] - [status-im.ui.components.button.view :as button] - [status-im.utils.utils :as utils] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.status-bar.view :as status-bar])) - -(defn signing-word [word] - [react/view styles/signing-word - [react/text {:style styles/signing-word-text - :font :roboto-mono - :number-of-lines 1} - word]]) - -(defn display-confirmation [modal?] - (utils/show-question - (i18n/label :t/wallet-set-up-confirm-title) - (i18n/label :t/wallet-set-up-confirm-description) - #(re-frame/dispatch [:wallet-set-up-passed modal?]))) - -(views/defview onboarding-panel [modal?] - (views/letsubs [{:keys [signing-phrase]} [:get-current-account]] - (let [signing-words (string/split signing-phrase #" ") - container (if modal? react/view wallet.components/simple-screen) - container-opts (if modal? components.styles/flex {:avoid-keyboard? true})] - [container container-opts - [wallet.components/toolbar - {} - (actions/back-white #(re-frame/dispatch [:wallet-setup-navigate-back])) - (i18n/label :t/wallet-set-up-title)] - [react/view components.styles/flex - [react/view {:style styles/setup-image-container} - [react/image {:source (:wallet-setup resources/ui) - :style styles/setup-image}]] - [react/view {:style styles/signing-phrase} - (for [word signing-words] - ^{:key (str "signing-word-" word)} - [signing-word word])] - [react/text {:style styles/description} - (i18n/label :t/wallet-set-up-signing-phrase)] - [bottom-buttons/bottom-buttons styles/bottom-buttons - nil - [button/button {:on-press (partial display-confirmation modal?) - :text-style styles/got-it-button-text - :accessibility-label :done-button} - (i18n/label :t/got-it) - nil]]]]))) - -(views/defview screen [] - [onboarding-panel false]) - -(views/defview modal [] - [react/view styles/modal - [status-bar/status-bar {:type :modal-wallet}] - [onboarding-panel true]]) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/onboarding/styles.cljs b/src/status_im/ui/screens/wallet/onboarding/styles.cljs deleted file mode 100644 index 5585cf0ad8a..00000000000 --- a/src/status_im/ui/screens/wallet/onboarding/styles.cljs +++ /dev/null @@ -1,43 +0,0 @@ -(ns status-im.ui.screens.wallet.onboarding.styles - (:require [status-im.ui.components.colors :as colors])) - -(def root - {:flex 1 - :background-color colors/blue - :align-items :center - :justify-content :center - :padding-horizontal 30}) - -(def onboarding-image-container - {:flex 1 - :align-items :center - :justify-content :center}) - -(def onboarding-image - {:width 285 - :height 312}) - -(def onboarding-title - {:line-height 28 - :font-size 22 - :font-weight :bold - :letter-spacing -0.3 - :text-align :center - :color colors/white}) - -(def onboarding-text - {:line-height 21 - :margin-top 8 - :margin-bottom 32 - :font-size 14 - :letter-spacing -0.2 - :text-align :center - :color colors/white-lighter-transparent}) - -(def set-up-button - {:flex-direction :row - :background-color (colors/alpha colors/black 0.1) - :margin-bottom 32}) - -(def set-up-button-label - {:color "white"}) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/onboarding/views.cljs b/src/status_im/ui/screens/wallet/onboarding/views.cljs deleted file mode 100644 index 74c4c90a66a..00000000000 --- a/src/status_im/ui/screens/wallet/onboarding/views.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.ui.screens.wallet.onboarding.views - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.common.common :as components.common] - [status-im.ui.components.react :as react] - [status-im.ui.screens.wallet.onboarding.styles :as styles] - [status-im.react-native.resources :as resources])) - -(defn onboarding [] - [react/view styles/root - [react/view {:style styles/onboarding-image-container} - [react/image {:source (:wallet-welcome resources/ui) - :style styles/onboarding-image}]] - [react/text {:style styles/onboarding-title} - (i18n/label :t/wallet-onboarding-title)] - [react/text {:style styles/onboarding-text} - (i18n/label :t/wallet-onboarding-description)] - - [components.common/button - {:button-style styles/set-up-button - :label-style styles/set-up-button-label - :on-press #(re-frame/dispatch [:navigate-to :wallet-onboarding-setup]) - :label (i18n/label :t/wallet-onboarding-set-up)}]]) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/request/db.cljs b/src/status_im/ui/screens/wallet/request/db.cljs deleted file mode 100644 index 0608c7b2458..00000000000 --- a/src/status_im/ui/screens/wallet/request/db.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.ui.screens.wallet.request.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec] - [status-im.utils.money :as money])) - -(spec/def ::amount (spec/nilable money/valid?)) -(spec/def ::amount-error (spec/nilable string?)) -(spec/def ::amount-text (spec/nilable string?)) -(spec/def ::symbol (spec/nilable keyword?)) - -(spec/def :wallet/request-transaction (allowed-keys - :opt-un [::amount ::amount-error ::amount-text ::symbol])) diff --git a/src/status_im/ui/screens/wallet/request/events.cljs b/src/status_im/ui/screens/wallet/request/events.cljs deleted file mode 100644 index 2a0495ffb66..00000000000 --- a/src/status_im/ui/screens/wallet/request/events.cljs +++ /dev/null @@ -1,48 +0,0 @@ -(ns status-im.ui.screens.wallet.request.events - (:require [re-frame.core :as re-frame] - [status-im.ui.screens.wallet.db :as wallet-db] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.chat.commands.core :as commands] - [status-im.utils.money :as money])) - -(handlers/register-handler-fx - ::wallet-send-chat-request - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [asset amount]] - (handlers-macro/merge-fx cofx - {:dispatch [:send-current-message]} - (commands/select-chat-input-command - (get-in db [:id->command ["request" #{:personal-chats}]]) [asset amount])))) - -(handlers/register-handler-fx - :wallet-send-request - [re-frame/trim-v] - (fn [_ [whisper-identity amount symbol decimals]] - (assert whisper-identity) - ;; TODO(janherich) remove this dispatch sequence, there is absolutely no need for that :/ - {:dispatch-n [[:navigate-back] - [:navigate-to-clean :home] - [:add-chat-loaded-event whisper-identity - [::wallet-send-chat-request (name symbol) (str (money/internal->formatted amount symbol decimals))]] - [:start-chat whisper-identity]]})) - -(handlers/register-handler-fx - :wallet.request/set-recipient - (fn [{:keys [db]} [_ s]] - {:db (assoc-in db [:wallet :request-transaction :to] s)})) - -(handlers/register-handler-fx - :wallet.request/set-and-validate-amount - (fn [{:keys [db]} [_ amount symbol decimals]] - (let [{:keys [value error]} (wallet-db/parse-amount amount decimals)] - {:db (-> db - (assoc-in [:wallet :request-transaction :amount] (money/formatted->internal value symbol decimals)) - (assoc-in [:wallet :request-transaction :amount-text] amount) - (assoc-in [:wallet :request-transaction :amount-error] error))}))) - -(handlers/register-handler-fx - :wallet.request/set-symbol - (fn [{:keys [db]} [_ symbol]] - {:db (-> db - (assoc-in [:wallet :request-transaction :symbol] symbol))})) diff --git a/src/status_im/ui/screens/wallet/request/styles.cljs b/src/status_im/ui/screens/wallet/request/styles.cljs deleted file mode 100644 index ebbd16c43b9..00000000000 --- a/src/status_im/ui/screens/wallet/request/styles.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.ui.screens.wallet.request.styles - (:require [status-im.ui.components.colors :as colors])) - -(def hint - {:color colors/white-lighter-transparent}) - -(def footer - {:color colors/white}) - -(def bottom-buttons - {:background-color colors/blue}) - -(def request-wrapper - {:flex 1 - :flex-direction :column - :margin-horizontal 16}) - -;; Request panel - -(def request-details-wrapper - {:padding-bottom 60}) - -(def send-request - {:background-color colors/black-transparent - :margin-bottom 12}) diff --git a/src/status_im/ui/screens/wallet/request/subs.cljs b/src/status_im/ui/screens/wallet/request/subs.cljs deleted file mode 100644 index 16d6735b6a6..00000000000 --- a/src/status_im/ui/screens/wallet/request/subs.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns status-im.ui.screens.wallet.request.subs - (:require [re-frame.core :as re-frame])) - -(re-frame/reg-sub :wallet.request/transaction - :<- [:wallet] - :request-transaction) diff --git a/src/status_im/ui/screens/wallet/request/views.cljs b/src/status_im/ui/screens/wallet/request/views.cljs deleted file mode 100644 index c1c1f6952ee..00000000000 --- a/src/status_im/ui/screens/wallet/request/views.cljs +++ /dev/null @@ -1,89 +0,0 @@ -(ns status-im.ui.screens.wallet.request.views - (:require-macros [status-im.utils.views :as views]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.bottom-buttons.view :as bottom-buttons] - [status-im.ui.components.button.view :as button] - [status-im.ui.components.common.common :as common] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.qr-code-viewer.views :as qr-code-viewer] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.screens.wallet.request.styles :as styles] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.screens.wallet.components.views :as wallet.components] - [status-im.ui.screens.wallet.components.views :as components] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.eip681 :as eip681] - [status-im.utils.utils :as utils] - [status-im.utils.ethereum.tokens :as tokens])) - -;; Request screen - -(views/defview send-transaction-request [] - ;; TODO(jeluard) both send and request flows should be merged - (views/letsubs [network [:get-current-account-network] - {:keys [to to-name whisper-identity]} [:wallet.send/transaction] - {:keys [amount amount-error amount-text symbol]} [:wallet.request/transaction] - scroll (atom nil)] - (let [{:keys [decimals] :as token} (tokens/asset-for (ethereum/network->chain-keyword network) symbol)] - [wallet.components/simple-screen {:avoid-keyboard? true} - [wallet.components/toolbar (i18n/label :t/new-request)] - [react/view components.styles/flex - [common/network-info {:text-color :white}] - [react/scroll-view {:ref #(reset! scroll %) :keyboardShouldPersistTaps :always} - [react/view styles/request-details-wrapper - [components/recipient-selector {:contact-only? true - :address to - :name to-name - :request? true - :modal? false}] - [components/asset-selector {:disabled? false - :type :request - :symbol symbol}] - [components/amount-selector {:error amount-error - :amount amount - :amount-text amount-text - :input-options {:on-focus (fn [] (when @scroll (utils/set-timeout #(.scrollToEnd @scroll) 100))) - :on-change-text #(re-frame/dispatch [:wallet.request/set-and-validate-amount % symbol decimals])}} - token]]] - [bottom-buttons/bottom-buttons styles/bottom-buttons - nil ;; Force a phantom button to ensure consistency with other transaction screens which define 2 buttons - [button/button {:disabled? (or amount-error (not (and to amount))) - :on-press #(re-frame/dispatch [:wallet-send-request whisper-identity amount symbol decimals]) - :text-style {:padding-horizontal 0} - :accessibility-label :sent-request-button} - (i18n/label :t/send-request) - [vector-icons/icon :icons/forward {:color :white}]]]]]))) - -;; Main screen - -(defn- qr-code [address chain-id] - [qr-code-viewer/qr-code-viewer {:hint-style styles/hint :footer-style styles/footer} - (eip681/generate-uri address {:chain-id chain-id}) - (i18n/label :t/request-qr-legend) - address]) - -(views/defview request-transaction [] - (views/letsubs [address-hex [:get-current-account-hex] - chain-id [:get-network-id]] - [wallet.components/simple-screen - [wallet.components/toolbar {} - wallet.components/default-action - (i18n/label :t/receive) - [toolbar/actions [{:icon :icons/share - :icon-opts {:color :white - :accessibility-label :share-button} - :handler #(list-selection/open-share {:message address-hex})}]]] - [react/view {:flex 1} - [common/network-info {:text-color :white}] - [react/scroll-view styles/request-wrapper - [qr-code address-hex chain-id] - [button/primary-button {:on-press #(re-frame/dispatch [:navigate-to :wallet-send-transaction-request]) - :style styles/send-request - :accessibility-label :sent-transaction-request-button} - (i18n/label :t/send-transaction-request)]]]])) diff --git a/src/status_im/ui/screens/wallet/send/animations.cljs b/src/status_im/ui/screens/wallet/send/animations.cljs deleted file mode 100644 index ef73a5fe28f..00000000000 --- a/src/status_im/ui/screens/wallet/send/animations.cljs +++ /dev/null @@ -1,11 +0,0 @@ -(ns status-im.ui.screens.wallet.send.animations - (:require [status-im.ui.components.animation :as animation])) - -(defn animate-sign-panel [opacity-value bottom-value] - (animation/start - (animation/parallel - [(animation/timing opacity-value {:toValue 1 - :duration 500}) - (animation/timing bottom-value {:toValue 53 - :easing (.bezier (animation/easing) 0.685, 0.000, 0.025, 1.185) - :duration 500})]))) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/send/db.cljs b/src/status_im/ui/screens/wallet/send/db.cljs deleted file mode 100644 index 9ea7b01656e..00000000000 --- a/src/status_im/ui/screens/wallet/send/db.cljs +++ /dev/null @@ -1,37 +0,0 @@ -(ns status-im.ui.screens.wallet.send.db - (:require-macros [status-im.utils.db :refer [allowed-keys]]) - (:require [cljs.spec.alpha :as spec] - [status-im.utils.money :as money] - [status-im.utils.security :as security])) - -(spec/def ::amount (spec/nilable money/valid?)) -(spec/def ::to (spec/nilable string?)) -(spec/def ::to-name (spec/nilable string?)) -(spec/def ::amount-error (spec/nilable string?)) -(spec/def ::asset-error (spec/nilable string?)) -(spec/def ::amount-text (spec/nilable string?)) -(spec/def ::password (spec/nilable #(instance? security/MaskedData %))) -(spec/def ::wrong-password? (spec/nilable boolean?)) -(spec/def ::id (spec/nilable string?)) -(spec/def ::waiting-signal? (spec/nilable boolean?)) -(spec/def ::signing? (spec/nilable boolean?)) -(spec/def ::later? (spec/nilable boolean?)) -(spec/def ::height double?) -(spec/def ::width double?) -(spec/def ::camera-flashlight #{:on :off}) -(spec/def ::in-progress? boolean?) -(spec/def ::from-chat? (spec/nilable boolean?)) -(spec/def ::symbol (spec/nilable keyword?)) -(spec/def ::gas (spec/nilable money/valid?)) -(spec/def ::gas-price (spec/nilable money/valid?)) -(spec/def ::advanced? boolean?) -(spec/def ::whisper-identity (spec/nilable string?)) -(spec/def ::method (spec/nilable string?)) -(spec/def ::tx-hash (spec/nilable string?)) - -(spec/def :wallet/send-transaction (allowed-keys - :opt-un [::amount ::to ::to-name ::amount-error ::asset-error ::amount-text ::password - ::waiting-signal? ::signing? ::id ::later? - ::camera-flashlight ::in-progress? - ::wrong-password? ::from-chat? ::symbol ::advanced? - ::gas ::gas-price ::whisper-identity ::method ::tx-hash])) diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs deleted file mode 100644 index 3edb6b53c4d..00000000000 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ /dev/null @@ -1,492 +0,0 @@ -(ns status-im.ui.screens.wallet.send.events - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.native-module.core :as status] - [status-im.ui.screens.db :as db] - [status-im.ui.screens.wallet.db :as wallet.db] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.erc20 :as erc20] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.hex :as utils.hex] - [status-im.utils.money :as money] - [status-im.utils.security :as security] - [status-im.utils.types :as types] - [status-im.utils.utils :as utils] - [status-im.models.wallet :as models.wallet] - [status-im.chat.models.message :as models.message] - [status-im.chat.commands.sending :as commands-sending] - [status-im.constants :as constants] - [status-im.transport.utils :as transport.utils] - [taoensso.timbre :as log] - [status-im.ui.screens.navigation :as navigation] - [status-im.wallet.transactions :as wallet.transactions])) - -;;;; FX - -(re-frame/reg-fx - ::accept-transaction - (fn [{:keys [masked-password id on-completed]}] - ;; unmasking the password as late as possible to avoid being exposed from app-db - (status/approve-sign-request id - (security/unmask masked-password) - on-completed))) - -(re-frame/reg-fx - ::accept-transaction-with-changed-gas - (fn [{:keys [masked-password id on-completed gas gas-price default-gas-price]}] - ;; unmasking the password as late as possible to avoid being exposed from app-db - (if gas - (status/approve-sign-request-with-args id - (security/unmask masked-password) - (money/to-fixed gas) - (money/to-fixed (or gas-price default-gas-price)) - on-completed) - (status/approve-sign-request id - (security/unmask masked-password) - on-completed)))) - -(defn- send-ethers [{:keys [web3 from to value gas gas-price]}] - (.sendTransaction (.-eth web3) - (clj->js {:from from :to to :value value :gas gas :gasPrice gas-price}) - #())) - -(defn- send-tokens [{:keys [web3 from to value gas gas-price symbol chain]}] - (let [contract (:address (tokens/symbol->token (keyword chain) symbol))] - (erc20/transfer web3 contract from to value {:gas gas :gasPrice gas-price} #()))) - -(re-frame/reg-fx - ::send-transaction - (fn [{:keys [symbol] :as params}] - (case symbol - :ETH (send-ethers params) - (send-tokens params)))) - -(re-frame/reg-fx - ::show-transaction-error - (fn [message] - ;; (andrey) we need this timeout because modal window conflicts with alert - (utils/set-timeout #(utils/show-popup (i18n/label :t/transaction-failed) message) 1000))) - -(re-frame/reg-fx - :discard-transaction - (fn [id] - (status/discard-sign-request id))) - -;;Helper functions - -(defn transaction-valid? [{{:keys [to data]} :args}] - (or (and to (utils.hex/valid-hex? to)) (and data (not= data "0x")))) - -(defn dispatch-transaction-completed [result & [modal?]] - (re-frame/dispatch [::transaction-completed {:id (:id result) :response result} modal?])) -;;;; Handlers - -(defn set-and-validate-amount-db [db amount symbol decimals] - (let [{:keys [value error]} (wallet.db/parse-amount amount decimals)] - (-> db - (assoc-in [:wallet :send-transaction :amount] (money/formatted->internal value symbol decimals)) - (assoc-in [:wallet :send-transaction :amount-text] amount) - (assoc-in [:wallet :send-transaction :amount-error] error)))) - -(handlers/register-handler-fx - :wallet.send/set-and-validate-amount - (fn [{:keys [db]} [_ amount symbol decimals]] - {:db (set-and-validate-amount-db db amount symbol decimals)})) - -(defn update-gas-price - ([db edit? success-event] - {:update-gas-price {:web3 (:web3 db) - :success-event (or success-event :wallet/update-gas-price-success) - :edit? edit?}}) - ([db edit?] (update-gas-price db edit? :wallet/update-gas-price-success)) - ([db] (update-gas-price db false :wallet/update-gas-price-success))) - -(defn recalculate-gas [{:keys [db] :as fx} symbol] - (-> fx - (assoc-in [:db :wallet :send-transaction :gas] (ethereum/estimate-gas symbol)) - (merge (update-gas-price db)))) - -(handlers/register-handler-fx - :wallet/update-gas-price - (fn [{:keys [db]} [_ edit?]] - (update-gas-price db edit?))) - -(handlers/register-handler-fx - :wallet.send/set-symbol - (fn [{:keys [db]} [_ symbol]] - (let [old-symbol (get-in db [:wallet :send-transaction :symbol])] - (cond-> {:db (-> db - (assoc-in [:wallet :send-transaction :symbol] symbol) - (assoc-in [:wallet :send-transaction :amount] nil) - (assoc-in [:wallet :send-transaction :amount-text] nil) - (assoc-in [:wallet :send-transaction :asset-error] nil))} - (not= old-symbol symbol) (recalculate-gas symbol))))) - -(handlers/register-handler-fx - :wallet.send/toggle-advanced - (fn [{:keys [db]} [_ advanced?]] - {:db (assoc-in db [:wallet :send-transaction :advanced?] advanced?)})) - -(def ^:private clear-send-properties {:id nil - :signing? false - :wrong-password? false - :waiting-signal? false - :from-chat? false - :in-progress? false - :password nil}) - -;; TODO(goranjovic) - this is a temporary workaround because in regular `clear-send-properties` we need `:from-chat?` -;; to be false to avoid the wallet onboarding crash if the user accessed transactions from dapps first. -;; On the other hand, if we reset the same flag to false every time we clear the current transaction that would also -;; happen when user clicks Cancel in chat initiated transaction and then all fields would become editable, which is -;; another bug. -;; This temporary workaround resets all fields except the flag if we are in a chat /send transaction. -;; A permanent solution would be to add onboarding check to dapp transaction and/or review the workflow. -(def ^:private partial-clear-send-properties {:id nil - :signing? false - :wrong-password? false - :waiting-signal? false - :in-progress? false - :password nil}) - -(defn on-transactions-completed [raw-results] - (let [result (types/json->clj raw-results)] - (dispatch-transaction-completed result))) - -(defn prepare-transaction [{:keys [id message_id method args]} now] - ;;NOTE(goranjovic): the transactions started from chat using /send command - ;; are only in ether, so this parameter defaults to ETH - (let [{:keys [from to value symbol data gas gasPrice] :or {symbol :ETH}} args] - {:id id - :from from - :to to - :to-name (when (nil? to) - (i18n/label :t/new-contract)) - :symbol symbol - :method method - :value (money/bignumber (or value 0)) - :data data - :gas (when (seq gas) - (money/bignumber (money/to-decimal gas))) - :gas-price (when (seq gasPrice) - (money/bignumber (money/to-decimal gasPrice))) - :timestamp now - :message-id message_id})) - -;;TRANSACTION QUEUED signal from status-go -(handlers/register-handler-fx - :sign-request-queued - (fn [{:keys [db]} [_ transaction]] - {:db (update-in db [:wallet :transactions-queue] conj transaction) - :dispatch [:check-transactions-queue]})) - -(handlers/register-handler-fx - :check-transactions-queue - (fn [{:keys [db now]} _] - (let [{:keys [send-transaction transactions-queue]} (:wallet db) - {:keys [id method args] :as queued-transaction} (last transactions-queue) - db' (update-in db [:wallet :transactions-queue] drop-last)] - (when (and (not (:id send-transaction)) queued-transaction) - (cond - ;;SEND TRANSACTION - (= method constants/web3-send-transaction) - - (let [{:keys [gas gasPrice]} args - transaction (prepare-transaction queued-transaction now) - sending-from-dapp? (not (get-in db [:wallet :send-transaction :waiting-signal?])) - new-db (assoc-in db' [:wallet :transactions-unsigned id] transaction) - sender-account (:account/account db) - sending-db {:id id - :method method - :from-chat? (or sending-from-dapp? ;;TODO(goranjovic): figure out why we need to - ;; have from-chat? flag for dapp txs and get rid of this - (get-in db [:wallet :send-transaction :from-chat?]))}] - (if sending-from-dapp? - ;;SENDING FROM DAPP - {:db (assoc-in new-db [:wallet :send-transaction] sending-db) ; we need to completely reset sending state here - :dispatch-n [[:update-wallet] - [:navigate-to-modal (if (:wallet-set-up-passed? sender-account) - :wallet-send-transaction-modal - :wallet-onboarding-setup-modal)] - (when-not (seq gas) - [:wallet/update-estimated-gas transaction]) - (when-not (seq gasPrice) - [:wallet/update-gas-price])]} - ;;WALLET SEND SCREEN WAITING SIGNAL - (let [{:keys [password]} (get-in db [:wallet :send-transaction]) - new-db' (update-in new-db [:wallet :send-transaction] merge sending-db)] ; just update sending state as we are in wallet flow - {:db new-db' - ::accept-transaction {:id id - :masked-password password - :on-completed on-transactions-completed}}))) - ;;SIGN MESSAGE - (= method constants/web3-personal-sign) - - (let [{:keys [data]} args - data' (transport.utils/to-utf8 data)] - (if data' - {:db (-> db' - (assoc-in [:wallet :transactions-unsigned id] {:data data' :id id}) - (assoc-in [:wallet :send-transaction] {:id id :method method})) - :dispatch [:navigate-to-modal :wallet-sign-message-modal]} - {:db db'}))))))) - -(defn this-transaction-signing? [id signing-id view-id modal] - (and (= signing-id id) - (or (= view-id :wallet-send-transaction) - (= view-id :wallet-send-transaction-chat) - (= modal :wallet-send-transaction-modal) - (= modal :wallet-sign-message-modal)))) - -(defn handle-failed-tx [cofx error_message] - (-> cofx - (assoc ::show-transaction-error error_message) - (update-in [:db :wallet] dissoc :send-transaction))) - -;;TRANSACTION FAILED signal from status-go -(handlers/register-handler-fx - :sign-request-failed - (fn [{{:keys [view-id modal] :as db} :db} [_ {:keys [id method error_code error_message]}]] - (let [send-transaction (get-in db [:wallet :send-transaction])] - (case error_code - - ;;WRONG PASSWORD - constants/send-transaction-password-error-code - {:db (-> db - (assoc-in [:wallet :send-transaction :wrong-password?] true) - (assoc-in [:wallet :send-transaction :waiting-signal?] false))} - - ;;NO ERROR, DISCARDED, TIMEOUT or DEFAULT ERROR - (if (this-transaction-signing? id (:id send-transaction) view-id modal) - (cond-> {:db (-> db - navigation/navigate-back - (assoc-in [:wallet :transactions-queue] nil) - (update-in [:wallet :transactions-unsigned] dissoc id) - (update-in [:wallet :send-transaction] merge clear-send-properties))} - (= method constants/web3-send-transaction) - (handle-failed-tx error_message)) - {:db (update-in db [:wallet :transactions-unsigned] dissoc id)}))))) - -(defn prepare-unconfirmed-dapp-transaction [now hash transaction] - (-> transaction - (assoc :confirmations "0" - :timestamp (str now) - :type :outbound - :hash hash) - (update :gas-price str) - (update :value str) - (update :gas str) - (dissoc :message-id :id))) - -(defn prepare-unconfirmed-status-transaction [db now hash transaction] - (let [chain (:chain db) - token (tokens/symbol->token (keyword chain) (:symbol transaction))] - (-> transaction - (assoc :confirmations "0" - :timestamp (str now) - :type :outbound - :hash hash) - (update :gas-price str) - (assoc :value (:amount transaction)) - (assoc :token token) - (update :gas str) - (dissoc :message-id :id)))) - -(defn prepare-unconfirmed-transaction [db now hash id] - (let [unsigned-transaction (get-in db [:wallet :transactions-unsigned id]) - send-transaction (get-in db [:wallet :send-transaction])] - ;;TODO(goranjovic) - unify `send-transaction` with transactions-unsigned` - ;; currently the latter is only used for transactions initiated from dapps - (if-not (:symbol send-transaction) - (prepare-unconfirmed-dapp-transaction now hash unsigned-transaction) - (prepare-unconfirmed-status-transaction db now hash send-transaction)))) - -(handlers/register-handler-fx - :send-transaction-message - (concat models.message/send-interceptors - navigation/navigation-interceptors) - (fn [{:keys [db] :as cofx} [chat-id params]] - ;;NOTE(goranjovic): we want to send the payment message only when we have a whisper id - ;; for the recipient, we always redirect to `:wallet-transaction-sent` even when we don't - (if-let [send-command (and chat-id (get-in db [:id->command ["send" #{:personal-chats}]]))] - (handlers-macro/merge-fx cofx - (commands-sending/send chat-id send-command params) - (navigation/replace-view :wallet-transaction-sent)) - (handlers-macro/merge-fx cofx - (navigation/replace-view :wallet-transaction-sent))))) - -(handlers/register-handler-fx - ::transaction-completed - (fn [{db :db now :now} [_ {:keys [id response] :as params} modal?]] - (let [{:keys [hash error]} response - {:keys [method whisper-identity to symbol amount-text]} (get-in db [:wallet :send-transaction]) - db' (assoc-in db [:wallet :send-transaction :in-progress?] false)] - (if (and error (string? error) (not (string/blank? error))) ;; ignore error here, error will be handled in :transaction-failed - {:db db'} - (merge - {:db (cond-> db' - (= method constants/web3-send-transaction) - (assoc-in [:wallet :transactions hash] (prepare-unconfirmed-transaction db now hash id)) - true - (update-in [:wallet :transactions-unsigned] dissoc id) - true - (update-in [:wallet :send-transaction] merge clear-send-properties {:tx-hash hash}))} - (if modal? - - (cond-> {:dispatch [:navigate-back]} - (= method constants/web3-send-transaction) - (assoc :dispatch-later [{:ms 400 :dispatch [:navigate-to-modal :wallet-transaction-sent-modal]}])) - - {:dispatch [:send-transaction-message whisper-identity {:address to - :asset (name symbol) - :amount amount-text - :tx-hash hash}]})))))) - -(defn on-transactions-modal-completed [raw-results] - (let [result (types/json->clj raw-results)] - (dispatch-transaction-completed result true))) - -(handlers/register-handler-fx - :wallet/sign-transaction - (fn [{{:keys [web3 chain] :as db} :db} [_ later?]] - (let [db' (assoc-in db [:wallet :send-transaction :wrong-password?] false) - {:keys [amount id password to symbol method gas gas-price]} (get-in db [:wallet :send-transaction])] - (if id - {::accept-transaction {:id id - :masked-password password - :on-completed on-transactions-completed} - :db (assoc-in db' [:wallet :send-transaction :in-progress?] true)} - {:db (update-in db' [:wallet :send-transaction] assoc - :waiting-signal? true - :later? later? - :in-progress? true) - ::send-transaction {:web3 web3 - :from (get-in db [:account/account :address]) - :to to - :value amount - :gas gas - :gas-price gas-price - :symbol symbol - :method method - :chain chain}})))) - -(handlers/register-handler-fx - :wallet/sign-message-modal - (fn [{db :db} _] - (let [{:keys [id password]} (get-in db [:wallet :send-transaction])] - {:db (assoc-in db [:wallet :send-transaction :in-progress?] true) - ::accept-transaction {:id id - :masked-password password - :on-completed on-transactions-modal-completed}}))) - -(defn sign-transaction-modal [{:keys [db]} default-gas-price] - ;;TODO(goranjovic) - unify send-transaction and unsigned-transaction - (let [{:keys [id password] :as send-transaction} (get-in db [:wallet :send-transaction]) - {:keys [gas gas-price]} [:wallet.send/unsigned-transaction]] - {:db (assoc-in db [:wallet :send-transaction :in-progress?] true) - ::accept-transaction-with-changed-gas {:id id - :masked-password password - :gas (or gas (:gas send-transaction)) - :gas-price (or gas-price (:gas-price send-transaction)) - :default-gas-price default-gas-price - :on-completed on-transactions-modal-completed}})) - -(handlers/register-handler-fx - :wallet/sign-transaction-modal-update-gas-success - (fn [cofx [_ default-gas-price]] - (sign-transaction-modal cofx default-gas-price))) - -(handlers/register-handler-fx - :wallet/sign-transaction-modal - (fn [{:keys [db]} _] - (update-gas-price db false :wallet/sign-transaction-modal-update-gas-success))) - -(defn discard-transaction - [{:keys [db]}] - (let [{:keys [id from-chat?]} (get-in db [:wallet :send-transaction]) - clear-fields (if from-chat? partial-clear-send-properties clear-send-properties)] - (merge {:db (update-in db [:wallet :send-transaction] merge clear-fields)} - (when id - {:discard-transaction id})))) - -(handlers/register-handler-fx - :wallet/discard-transaction - (fn [cofx _] - (discard-transaction cofx))) - -(handlers/register-handler-fx - :wallet/discard-transaction-navigate-back - (fn [cofx _] - (-> cofx - discard-transaction - (assoc :dispatch [:navigate-back])))) - -(handlers/register-handler-fx - :wallet/cancel-signing-modal - (fn [{:keys [db]} _] - {:db (update-in db [:wallet :send-transaction] assoc - :signing? false - :wrong-password? false - :password nil)})) - -(handlers/register-handler-fx - :wallet.send/set-password - (fn [{:keys [db]} [_ masked-password]] - {:db (assoc-in db [:wallet :send-transaction :password] masked-password)})) - -(handlers/register-handler-fx - :wallet.send/set-signing? - (fn [{:keys [db]} [_ signing?]] - {:db (assoc-in db [:wallet :send-transaction :signing?] signing?)})) - -(handlers/register-handler-fx - :wallet.send/edit-value - (fn [cofx [_ key value]] - (models.wallet/edit-value key value cofx))) - -(handlers/register-handler-fx - :wallet.send/set-gas-details - (fn [{:keys [db]} [_ gas gas-price]] - {:db (-> db - (assoc-in [:wallet :send-transaction :gas] gas) - (assoc-in [:wallet :send-transaction :gas-price] gas-price))})) - -(handlers/register-handler-fx - :wallet.send/clear-gas - (fn [{:keys [db]}] - {:db (update db :wallet dissoc :edit)})) - -(handlers/register-handler-fx - :wallet.send/reset-gas-default - (fn [{:keys [db] :as cofx}] - (let [gas-estimate (money/to-fixed - (ethereum/estimate-gas - (-> db :wallet :send-transaction :symbol)))] - (assoc (models.wallet/edit-value - :gas - gas-estimate - cofx) - :dispatch [:wallet/update-gas-price true])))) - -(handlers/register-handler-fx - :close-transaction-sent-screen - (fn [{:keys [db]} [_ chat-id]] - {:dispatch [:navigate-back] - :dispatch-later [{:ms 400 :dispatch [:check-transactions-queue]}]})) - -(handlers/register-handler-fx - :sync-wallet-transactions - (fn [cofx _] - (wallet.transactions/sync cofx))) - -(handlers/register-handler-fx - :start-wallet-transactions-sync - (fn [cofx _] - (when-not (get-in cofx [:db :wallet :transactions-sync-started?]) - (handlers-macro/merge-fx cofx - (wallet.transactions/load-missing-chat-transactions) - (wallet.transactions/sync) - (wallet.transactions/set-sync-started))))) diff --git a/src/status_im/ui/screens/wallet/send/styles.cljs b/src/status_im/ui/screens/wallet/send/styles.cljs deleted file mode 100644 index a0cc04b473d..00000000000 --- a/src/status_im/ui/screens/wallet/send/styles.cljs +++ /dev/null @@ -1,150 +0,0 @@ -(ns status-im.ui.screens.wallet.send.styles - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :as styles] - [status-im.ui.screens.wallet.components.styles :as wallet.components.styles])) - -(def send-transaction-form - {:flex 1 - :padding-bottom 60}) - -(defn animated-sign-panel [bottom-value] - {:position :absolute - :left 12 - :right 12 - :bottom bottom-value}) - -(defn sign-panel [opacity-value] - {:opacity opacity-value - :border-radius 8 - :background-color colors/white - :padding-top 12 - :padding-horizontal 12}) - -(def spinner-container - {:position :absolute - :left 0 - :top 0 - :right 0 - :bottom 0 - :justify-content :center}) - -(def signing-phrase-container - {:border-radius 8 - :height 36 - :align-items :center - :justify-content :center - :background-color colors/gray-lighter}) - -(def signing-phrase - {:font-size 15 - :letter-spacing -0.2 - :color colors/black}) - -(def signing-phrase-description - {:padding-top 8}) - -(def password-container - {:flex 1 - :padding-vertical 20}) - -(def password - {:padding 0 - :font-size 15 - :letter-spacing -0.2 - :height 20}) - -(def processing-view - {:position :absolute - :top 0 - :bottom 0 - :right 0 - :left 0 - :align-items :center - :justify-content :center - :background-color (str colors/black "1A")}) - -(def empty-text - {:text-align :center - :margin-top 22 - :margin-horizontal 92}) - -(def advanced-button - {:flex-direction :row - :background-color styles/color-blue6 - :border-radius 50 - :padding 8 - :align-items :center}) - -(def advanced-button-wrapper - {:align-items :center}) - -(def advanced-wrapper - {:margin-top 24 - :margin-bottom 16}) - -(def gas-container-wrapper - {:flex 1 - :flex-direction :row}) - -(def gas-input-wrapper - {:align-items :center - :justify-content :space-between - :flex-direction :row}) - -(def advanced-options-text-wrapper - {:flex 1 - :flex-direction :row - :justify-content :space-between - :margin-vertical 15}) - -(def advanced-label - {:text-align-vertical :center - :margin-left 4}) - -(def advanced-fees-text - {:color colors/white}) - -(def advanced-fees-details-text - {:color colors/white-lighter-transparent}) - -(def transaction-fee-block-wrapper - {:flex-direction :row}) - -(def transaction-fee-info - {:flex-direction :row - :margin 15}) - -(def transaction-fee-info-text-wrapper - {:flex-shrink 1}) - -(def transaction-fee-info-icon - {:border-radius 25 - :width 25 - :height 25 - :margin-right 15 - :align-items :center - :justify-content :center - :background-color colors/blue-dark}) - -(def transaction-fee-info-icon-text - {:color colors/white - :font-size 14}) - -(def transaction-fee-input - {:keyboard-type :numeric - :auto-capitalize "none" - :placeholder "0.000" - :placeholder-text-color colors/white-lighter-transparent - :selection-color colors/white - :style wallet.components.styles/text-input}) - -(def sign-buttons - {:background-color colors/blue - :padding-vertical 8}) - -(def fee-buttons - {:background-color colors/blue}) - -(def password-error-tooltip - {:bottom-value 15 - :color colors/red-light}) diff --git a/src/status_im/ui/screens/wallet/send/subs.cljs b/src/status_im/ui/screens/wallet/send/subs.cljs deleted file mode 100644 index 1bb7f9b9faf..00000000000 --- a/src/status_im/ui/screens/wallet/send/subs.cljs +++ /dev/null @@ -1,117 +0,0 @@ -(ns status-im.ui.screens.wallet.send.subs - (:require [re-frame.core :as re-frame] - [status-im.utils.money :as money] - [status-im.models.wallet :as models.wallet] - [status-im.utils.hex :as utils.hex])) - -(re-frame/reg-sub ::send-transaction - :<- [:wallet] - (fn [wallet] - (:send-transaction wallet))) - -(re-frame/reg-sub :wallet.send/symbol - :<- [::send-transaction] - (fn [send-transaction] - (:symbol send-transaction))) - -(re-frame/reg-sub :wallet.send/advanced? - :<- [::send-transaction] - (fn [send-transaction] - (:advanced? send-transaction))) - -(re-frame/reg-sub :wallet.send/camera-flashlight - :<- [::send-transaction] - (fn [send-transaction] - (:camera-flashlight send-transaction))) - -(re-frame/reg-sub :wallet.send/wrong-password? - :<- [::send-transaction] - (fn [send-transaction] - (:wrong-password? send-transaction))) - -(re-frame/reg-sub :wallet.send/sign-password-enabled? - :<- [::send-transaction] - (fn [{:keys [password]}] - (and (not (nil? password)) (not= password "")))) - -(re-frame/reg-sub ::unsigned-transactions - :<- [:wallet] - (fn [wallet] - (:transactions-unsigned wallet))) - -(re-frame/reg-sub ::unsigned-transaction - :<- [::send-transaction] - :<- [::unsigned-transactions] - (fn [[send-transaction unsigned-transactions]] - (when-let [unsigned-transaction (get unsigned-transactions - (:id send-transaction))] - (merge send-transaction - unsigned-transaction - {:gas (or (:gas send-transaction) (:gas unsigned-transaction)) - :gas-price (or (:gas-price send-transaction) (:gas-price unsigned-transaction))})))) - -(defn edit-or-transaction-data - "Set up edit data structure, defaulting to transaction when not available" - [transaction edit] - (cond-> edit - (not (get-in edit [:gas-price :value])) - (models.wallet/build-edit - :gas-price - (money/to-fixed (money/wei-> :gwei (:gas-price transaction)))) - - (not (get-in edit [:gas :value])) - (models.wallet/build-edit - :gas - (money/to-fixed (:gas transaction))))) - -(re-frame/reg-sub :wallet/edit - :<- [::send-transaction] - :<- [::unsigned-transaction] - :<- [:wallet] - (fn [[send-transaction unsigned-transaction {:keys [edit]}]] - (edit-or-transaction-data - (if (:id send-transaction) - unsigned-transaction - send-transaction) - edit))) - -(defn check-sufficient-funds [transaction balance symbol amount] - (assoc transaction :sufficient-funds? - (or (nil? amount) - (money/sufficient-funds? amount (get balance symbol))))) - -(defn check-sufficient-gas [transaction balance symbol amount] - (assoc transaction :sufficient-gas? - (or (nil? amount) - (let [available-ether (get balance :ETH) - available-for-gas (if (= :ETH symbol) - (.minus available-ether (money/bignumber amount)) - available-ether)] - (money/sufficient-funds? (-> transaction - :max-fee - money/bignumber - (money/formatted->internal :ETH 18)) - (money/bignumber available-for-gas)))))) - -(re-frame/reg-sub :wallet.send/transaction - :<- [::send-transaction] - :<- [:balance] - (fn [[{:keys [amount symbol] :as transaction} balance]] - (-> transaction - (models.wallet/add-max-fee) - (check-sufficient-funds balance symbol amount) - (check-sufficient-gas balance symbol amount)))) - -(re-frame/reg-sub :wallet.send/unsigned-transaction - :<- [::unsigned-transaction] - :<- [:get-contacts-by-address] - :<- [:balance] - (fn [[{:keys [value to symbol] :as transaction} contacts balance]] - (when transaction - (let [contact (contacts (utils.hex/normalize-hex to))] - (-> transaction - (assoc :amount value - :to-name (:name contact)) - (models.wallet/add-max-fee) - (check-sufficient-funds balance symbol value) - (check-sufficient-gas balance symbol value)))))) diff --git a/src/status_im/ui/screens/wallet/send/transaction_sent/styles.cljs b/src/status_im/ui/screens/wallet/send/transaction_sent/styles.cljs deleted file mode 100644 index 99afde00158..00000000000 --- a/src/status_im/ui/screens/wallet/send/transaction_sent/styles.cljs +++ /dev/null @@ -1,50 +0,0 @@ -(ns status-im.ui.screens.wallet.send.transaction-sent.styles - (:require-macros [status-im.utils.styles :refer [defnstyle defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -(def transaction-sent-container - {:align-items :center}) - -(def ok-icon-container - {:width 56 - :height 56 - :border-radius 28 - :background-color :white - :align-items :center - :justify-content :center - :margin-top 57 - :margin-bottom 16}) - -(def transaction-sent - {:color :white - :font-size 17}) - -(def gap - {:height 8}) - -(def transaction-sent-description - {:color :white - :opacity 0.6 - :text-align :center - :padding-horizontal 16}) - -(def transaction-details-container - {:height 42 - :background-color colors/black-darker-transparent - :margin-horizontal 16 - :opacity 0.2 - :align-items :center - :justify-content :center - :border-radius 8}) - -(def transaction-details - {:color :white - :font-size 15}) - -(def got-it-container - {:align-items :center - :padding-vertical 18}) - -(def got-it - {:color :white - :font-size 15}) diff --git a/src/status_im/ui/screens/wallet/send/transaction_sent/views.cljs b/src/status_im/ui/screens/wallet/send/transaction_sent/views.cljs deleted file mode 100644 index 809bbb6a519..00000000000 --- a/src/status_im/ui/screens/wallet/send/transaction_sent/views.cljs +++ /dev/null @@ -1,39 +0,0 @@ -(ns status-im.ui.screens.wallet.send.transaction-sent.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.icons.vector-icons :as vi] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.ui.screens.wallet.send.transaction-sent.styles :as styles] - [status-im.ui.components.styles :as components.styles] - [re-frame.core :as re-frame] - [status-im.ui.screens.wallet.components.views :as components] - [status-im.i18n :as i18n] - [status-im.utils.platform :as platform])) - -(defview transaction-sent [& [modal?]] - (letsubs [chat-id [:get-current-chat-id]] - [react/view wallet.styles/wallet-modal-container - [status-bar/status-bar {:type (if modal? :modal-wallet :transparent)}] - [react/view styles/transaction-sent-container - [react/view styles/ok-icon-container - [vi/icon :icons/ok {:color components.styles/color-blue4}]] - [react/text {:style styles/transaction-sent - :font (if platform/android? :medium :default) - :accessibility-label :transaction-sent-text} - (i18n/label :t/transaction-sent)] - [react/view styles/gap] - [react/i18n-text {:style styles/transaction-sent-description - :key :transaction-description}]] - [react/view components.styles/flex] - [components/separator] - [react/touchable-highlight {:on-press #(re-frame/dispatch [:close-transaction-sent-screen chat-id]) - :accessibility-label :got-it-button} - [react/view styles/got-it-container - [react/text {:style styles/got-it - :font (if platform/android? :medium :default) - :uppercase? true} - (i18n/label :t/got-it)]]]])) - -(defview transaction-sent-modal [] - [transaction-sent true]) diff --git a/src/status_im/ui/screens/wallet/send/views.cljs b/src/status_im/ui/screens/wallet/send/views.cljs deleted file mode 100644 index e7b95784c93..00000000000 --- a/src/status_im/ui/screens/wallet/send/views.cljs +++ /dev/null @@ -1,306 +0,0 @@ -(ns status-im.ui.screens.wallet.send.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.bottom-buttons.view :as bottom-buttons] - [status-im.ui.components.button.view :as button] - [status-im.ui.components.common.common :as common] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.toolbar.actions :as act] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.tooltip.views :as tooltip] - [status-im.ui.screens.wallet.components.styles :as wallet.components.styles] - [status-im.ui.screens.wallet.components.views :as components] - [status-im.ui.screens.wallet.components.views :as wallet.components] - [status-im.ui.screens.wallet.send.animations :as send.animations] - [status-im.ui.screens.wallet.send.styles :as styles] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.utils.money :as money] - [status-im.utils.security :as security] - [status-im.utils.utils :as utils] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.ethereum.core :as ethereum])) - -(defview sign-panel [message-label spinning?] - (letsubs [account [:get-current-account] - wrong-password? [:wallet.send/wrong-password?] - signing-phrase (:signing-phrase @account) - bottom-value (animation/create-value -250) - opacity-value (animation/create-value 0)] - {:component-did-mount #(send.animations/animate-sign-panel opacity-value bottom-value)} - [react/animated-view {:style (styles/animated-sign-panel bottom-value)} - (when wrong-password? - [tooltip/tooltip (i18n/label :t/wrong-password) styles/password-error-tooltip]) - [react/animated-view {:style (styles/sign-panel opacity-value)} - [react/view styles/spinner-container - ;;NOTE(goranjovic) - android build doesn't seem to react on change in `:animating` property, so - ;;we have this workaround of just using `when` around the whole element. - (when spinning? - [react/activity-indicator {:animating true - :size :large}])] - [react/view styles/signing-phrase-container - [react/text {:style styles/signing-phrase - :accessibility-label :signing-phrase-text} - signing-phrase]] - [react/i18n-text {:style styles/signing-phrase-description :key message-label}] - [react/view styles/password-container - [react/text-input - {:auto-focus true - :secure-text-entry true - :placeholder (i18n/label :t/enter-password) - :placeholder-text-color components.styles/color-gray4 - :on-change-text #(re-frame/dispatch [:wallet.send/set-password (security/mask-data %)]) - :style styles/password - :accessibility-label :enter-password-input - :auto-capitalize :none}]]]])) - -;; "Cancel" and "Sign Transaction >" buttons, signing with password -(defview signing-buttons [spinning? cancel-handler sign-handler sign-label] - (letsubs [sign-enabled? [:wallet.send/sign-password-enabled?]] - [bottom-buttons/bottom-buttons - styles/sign-buttons - [button/button {:style components.styles/flex - :on-press cancel-handler - :accessibility-label :cancel-button} - (i18n/label :t/cancel)] - [button/button {:style (wallet.styles/button-container sign-enabled?) - :on-press sign-handler - :disabled? (or spinning? (not sign-enabled?)) - :accessibility-label :sign-transaction-button} - (i18n/label sign-label) - [vector-icons/icon :icons/forward {:color :white}]]])) - -(defn- sign-enabled? [amount-error to amount modal?] - (and - (nil? amount-error) - (or modal? (not (empty? to))) ;;NOTE(goranjovic) - contract creation will have empty `to` - (not (nil? amount)))) - -;; "Sign Later" and "Sign Transaction >" buttons -(defn- sign-button [amount-error to amount sufficient-funds? sufficient-gas? modal?] - (let [sign-enabled? (sign-enabled? amount-error to amount modal?) - immediate-sign-enabled? (and sign-enabled? sufficient-funds? sufficient-gas?)] - [bottom-buttons/bottom-buttons - styles/sign-buttons - [react/view] - [button/button {:style components.styles/flex - :disabled? (not immediate-sign-enabled?) - :on-press #(re-frame/dispatch [:wallet.send/set-signing? true]) - :text-style {:color :white} - :accessibility-label :sign-transaction-button} - (i18n/label :t/transactions-sign-transaction) - [vector-icons/icon :icons/forward {:color (if immediate-sign-enabled? :white :gray)}]]])) - -(defn return-to-transaction [dapp-transaction?] - (if dapp-transaction? - (re-frame/dispatch [:navigate-to-modal :wallet-send-transaction-modal]) - (act/default-handler))) - -(defn handler [discard? dapp-transaction?] - (if discard? - #(re-frame/dispatch [:wallet/discard-transaction-navigate-back]) - #(return-to-transaction dapp-transaction?))) - -(defn- toolbar [discard? dapp-transaction? action title] - [toolbar/toolbar {:style wallet.styles/toolbar} - [toolbar/nav-button (action (handler discard? dapp-transaction?))] - [toolbar/content-title {:color :white} title]]) - -(defview transaction-fee [] - (letsubs [send-transaction [:wallet.send/transaction] - unsigned-transaction [:wallet.send/unsigned-transaction] - network [:get-current-account-network] - {gas-edit :gas - max-fee :max-fee - gas-price-edit :gas-price} [:wallet/edit]] - (let [modal? (:id send-transaction) - ;;TODO(goranjovic) - unify unsigned and regular transaction subs - {:keys [amount symbol] :as transaction} (if modal? unsigned-transaction send-transaction) - gas (:value gas-edit) - gas-price (:value gas-price-edit) - {:keys [decimals]} (tokens/asset-for (ethereum/network->chain-keyword network) symbol)] - [wallet.components/simple-screen {:status-bar-type :modal-wallet} - [toolbar false modal? act/close-white - (i18n/label :t/wallet-transaction-fee)] - [react/view components.styles/flex - [react/view {:flex-direction :row} - - [react/view styles/gas-container-wrapper - [wallet.components/cartouche {} - (i18n/label :t/gas-limit) - [react/view styles/gas-input-wrapper - [react/text-input (merge styles/transaction-fee-input - {:on-change-text #(re-frame/dispatch [:wallet.send/edit-value :gas %]) - :default-value gas - :accessibility-label :gas-limit-input})]]] - (when (:invalid? gas-edit) - [tooltip/tooltip (i18n/label :t/invalid-number)])] - - [react/view styles/gas-container-wrapper - [wallet.components/cartouche {} - (i18n/label :t/gas-price) - [react/view styles/gas-input-wrapper - [react/text-input (merge styles/transaction-fee-input - {:on-change-text #(re-frame/dispatch [:wallet.send/edit-value :gas-price %]) - :default-value gas-price - :accessibility-label :gas-price-input})] - [wallet.components/cartouche-secondary-text - (i18n/label :t/gwei)]]] - (when (:invalid? gas-price-edit) - [tooltip/tooltip (i18n/label (if (= :invalid-number (:invalid? gas-price-edit)) - :t/invalid-number - :t/wallet-send-min-wei))])]] - - [react/view styles/transaction-fee-info - [react/view styles/transaction-fee-info-icon - [react/text {:style styles/transaction-fee-info-icon-text} "?"]] - [react/view styles/transaction-fee-info-text-wrapper - [react/i18n-text {:style styles/advanced-fees-text - :key :wallet-transaction-fee-details}]]] - [components/separator] - [react/view styles/transaction-fee-block-wrapper - [wallet.components/cartouche {:disabled? true} - (i18n/label :t/amount) - [react/view {:accessibility-label :amount-input} - [wallet.components/cartouche-text-content - (str (money/to-fixed (money/internal->formatted amount symbol decimals))) - (name symbol)]]] - [wallet.components/cartouche {:disabled? true} - (i18n/label :t/wallet-transaction-total-fee) - [react/view {:accessibility-label :total-fee-input} - [wallet.components/cartouche-text-content - (str max-fee " " (i18n/label :t/eth))]]]] - - [bottom-buttons/bottom-buttons styles/fee-buttons - [button/button {:on-press #(re-frame/dispatch [:wallet.send/reset-gas-default]) - :accessibility-label :reset-to-default-button} - (i18n/label :t/reset-default)] - [button/button {:on-press #(do (re-frame/dispatch [:wallet.send/set-gas-details - (:value-number gas-edit) - (:value-number gas-price-edit)]) - (return-to-transaction modal?)) - :accessibility-label :done-button - :disabled? (or (:invalid? gas-edit) - (:invalid? gas-price-edit))} - (i18n/label :t/done)]]]]))) - -(defn- advanced-cartouche [{:keys [max-fee gas gas-price]}] - [react/view - [wallet.components/cartouche {:on-press #(do (re-frame/dispatch [:wallet.send/clear-gas]) - (re-frame/dispatch [:navigate-to-modal :wallet-transaction-fee]))} - (i18n/label :t/wallet-transaction-fee) - [react/view {:style styles/advanced-options-text-wrapper - :accessibility-label :transaction-fee-button} - [react/text {:style styles/advanced-fees-text} - (str max-fee " " (i18n/label :t/eth))] - [react/text {:style styles/advanced-fees-details-text} - (str (money/to-fixed gas) " * " (money/to-fixed (money/wei-> :gwei gas-price)) (i18n/label :t/gwei))]]]]) - -(defn- advanced-options [advanced? transaction modal? scroll] - [react/view {:style styles/advanced-wrapper} - [react/touchable-highlight {:on-press (fn [] - (re-frame/dispatch [:wallet.send/toggle-advanced (not advanced?)]) - (when (and scroll @scroll) (utils/set-timeout #(.scrollToEnd @scroll) 350)))} - [react/view {:style styles/advanced-button-wrapper} - [react/view {:style styles/advanced-button - :accessibility-label :advanced-button} - [react/i18n-text {:style (merge wallet.components.styles/label - styles/advanced-label) - :key :wallet-advanced}] - [vector-icons/icon (if advanced? :icons/up :icons/down) {:color :white}]]]] - (when advanced? - [advanced-cartouche transaction])]) - -(defn- send-transaction-panel [{:keys [modal? transaction scroll advanced? network]}] - (let [{:keys [amount amount-text amount-error asset-error signing? to to-name sufficient-funds? sufficient-gas? - in-progress? from-chat? symbol]} transaction - {:keys [decimals] :as token} (tokens/asset-for (ethereum/network->chain-keyword network) symbol) - timeout (atom nil)] - [wallet.components/simple-screen {:avoid-keyboard? (not modal?) - :status-bar-type (if modal? :modal-wallet :wallet)} - [toolbar from-chat? false (if modal? act/close-white act/back-white) - (i18n/label :t/send-transaction)] - [react/view components.styles/flex - [common/network-info {:text-color :white}] - [react/scroll-view {:keyboard-should-persist-taps :always - :ref #(reset! scroll %) - :on-content-size-change #(when (and (not modal?) scroll @scroll) - (.scrollToEnd @scroll))} - [react/view styles/send-transaction-form - [components/recipient-selector {:disabled? (or from-chat? modal?) - :address to - :name to-name - :modal? modal?}] - [components/asset-selector {:disabled? (or from-chat? modal?) - :error asset-error - :type :send - :symbol symbol}] - [components/amount-selector {:disabled? (or from-chat? modal?) - :error (or amount-error - (when-not sufficient-funds? (i18n/label :t/wallet-insufficient-funds)) - (when-not sufficient-gas? (i18n/label :t/wallet-insufficient-gas))) - :amount amount - :amount-text amount-text - :input-options {:on-focus (fn [] (when (and scroll @scroll) (utils/set-timeout #(.scrollToEnd @scroll) 100))) - :on-change-text #(re-frame/dispatch [:wallet.send/set-and-validate-amount % symbol decimals])}} token] - [advanced-options advanced? transaction modal? scroll]]] - (if signing? - [signing-buttons in-progress? - #(re-frame/dispatch (if modal? [:wallet/cancel-signing-modal] [:wallet/discard-transaction])) - #(re-frame/dispatch (if modal? [:wallet/sign-transaction-modal] [:wallet/sign-transaction])) - :t/transactions-sign-transaction] - [sign-button amount-error to amount sufficient-funds? sufficient-gas? modal?]) - (when signing? - [sign-panel :t/signing-phrase-description in-progress?]) - (when in-progress? [react/view styles/processing-view])]])) - -(defview send-transaction [] - (letsubs [transaction [:wallet.send/transaction] - advanced? [:wallet.send/advanced?] - network [:get-current-account-network] - scroll (atom nil)] - [send-transaction-panel {:modal? false :transaction transaction :scroll scroll :advanced? advanced? - :network network}])) - -(defview send-transaction-modal [] - (letsubs [transaction [:wallet.send/unsigned-transaction] - advanced? [:wallet.send/advanced?] - network [:get-current-account-network] - scroll (atom nil)] - (if transaction - [send-transaction-panel {:modal? true :transaction transaction :scroll scroll :advanced? advanced? - :network network}] - [react/view wallet.styles/wallet-modal-container - [react/view components.styles/flex - [status-bar/status-bar {:type :modal-wallet}] - [toolbar false false act/close-white - (i18n/label :t/send-transaction)] - [react/i18n-text {:style styles/empty-text - :key :unsigned-transaction-expired}]]]))) - -(defview sign-message-modal [] - (letsubs [{:keys [data in-progress?]} [:wallet.send/unsigned-transaction]] - [wallet.components/simple-screen {:status-bar-type :modal-wallet} - [toolbar true false act/close-white - (i18n/label :t/sign-message)] - [react/view components.styles/flex - [react/scroll-view - [react/view styles/send-transaction-form - [wallet.components/cartouche {:disabled? true} - (i18n/label :t/message) - [components/amount-input - {:disabled? true - :input-options {:multiline true} - :amount-text data} - nil]]]] - [signing-buttons false - #(re-frame/dispatch [:wallet/discard-transaction-navigate-back]) - #(re-frame/dispatch [:wallet/sign-message-modal]) - :t/transactions-sign] - [sign-panel :t/signing-message-phrase-description false] - (when in-progress? - [react/view styles/processing-view])]])) diff --git a/src/status_im/ui/screens/wallet/settings/events.cljs b/src/status_im/ui/screens/wallet/settings/events.cljs deleted file mode 100644 index 51fb444ebcb..00000000000 --- a/src/status_im/ui/screens/wallet/settings/events.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.ui.screens.wallet.settings.events - (:require [status-im.ui.screens.wallet.settings.models :as models] - [status-im.ui.screens.accounts.models :as accounts.models] - [status-im.utils.handlers :as handlers])) - -(handlers/register-handler-fx - :wallet.settings/toggle-visible-token - (fn [cofx [_ symbol checked?]] - (models/toggle-visible-token symbol checked? accounts.models/update-settings cofx))) - -(handlers/register-handler-fx - :configure-token-balance-and-visibility - (fn [cofx [_ symbol balance]] - (models/configure-token-balance-and-visibility symbol balance accounts.models/update-settings cofx))) diff --git a/src/status_im/ui/screens/wallet/settings/models.cljs b/src/status_im/ui/screens/wallet/settings/models.cljs deleted file mode 100644 index 000676e6f39..00000000000 --- a/src/status_im/ui/screens/wallet/settings/models.cljs +++ /dev/null @@ -1,38 +0,0 @@ -(ns status-im.ui.screens.wallet.settings.models - (:require [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.ui.screens.wallet.events :as wallet.events] - [status-im.utils.ethereum.tokens :as tokens] - [re-frame.core :as re-frame])) - -(defn- set-checked [ids id checked?] - (if checked? - (conj (or ids #{}) id) - (disj ids id))) - -(defn toggle-visible-token [symbol checked? update-settings-fx {{:keys [account/account]} :db :as cofx}] - (let [network (get (:networks account) (:network account)) - chain (ethereum/network->chain-keyword network) - settings (get account :settings) - new-settings (update-in settings [:wallet :visible-tokens chain] #(set-checked % symbol checked?))] - (update-settings-fx new-settings cofx))) - -(defn configure-token-balance-and-visibility [symbol balance update-settings-fx cofx] - (handlers-macro/merge-fx cofx - (toggle-visible-token symbol true update-settings-fx) - ;;TODO(goranjovic): move `update-token-balance-success` function to wallet models - (wallet.events/update-token-balance-success symbol balance))) - -(defn wallet-autoconfig-tokens [{:keys [db]}] - (let [{:keys [account/account web3]} db - network (get (:networks account) (:network account)) - chain (ethereum/network->chain-keyword network) - contracts (->> (tokens/tokens-for chain) - (remove :hidden?))] - (doseq [{:keys [address symbol]} contracts] - ;;TODO(goranjovic): move `get-token-balance` function to wallet models - (wallet.events/get-token-balance {:web3 web3 - :contract address - :account-id (:address account) - :on-success #(when (> % 0) - (re-frame/dispatch [:configure-token-balance-and-visibility symbol %]))})))) diff --git a/src/status_im/ui/screens/wallet/settings/views.cljs b/src/status_im/ui/screens/wallet/settings/views.cljs deleted file mode 100644 index 265fdab9325..00000000000 --- a/src/status_im/ui/screens/wallet/settings/views.cljs +++ /dev/null @@ -1,40 +0,0 @@ -(ns status-im.ui.screens.wallet.settings.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.tokens :as tokens])) - -(defn- render-token [{:keys [symbol name icon]} visible-tokens] - [list/list-item-with-checkbox - {:checked? (contains? visible-tokens (keyword symbol)) - :on-value-change #(re-frame/dispatch [:wallet.settings/toggle-visible-token (keyword symbol) %])} - [list/item - [list/item-image icon] - [list/item-content - [list/item-primary name] - [list/item-secondary symbol]]]]) - -(defview manage-assets [] - (letsubs [network [:network] - visible-tokens [:wallet/visible-tokens-symbols]] - [react/view (merge components.styles/flex {:background-color :white}) - [status-bar/status-bar {:type :modal-wallet}] - [toolbar/toolbar {:style wallet.styles/toolbar} - [toolbar/nav-text {:handler #(do (re-frame/dispatch [:update-wallet]) - (re-frame/dispatch [:navigate-back])) - :style {:color :white} - :accessibility-label :done-button} - (i18n/label :t/done)] - [toolbar/content-title {:color :white} - (i18n/label :t/wallet-assets)]] - [react/view {:style components.styles/flex} - [list/flat-list {:data (tokens/sorted-tokens-for (ethereum/network->chain-keyword network)) - :key-fn (comp str :symbol) - :render-fn #(render-token % visible-tokens)}]]])) diff --git a/src/status_im/ui/screens/wallet/styles.cljs b/src/status_im/ui/screens/wallet/styles.cljs deleted file mode 100644 index fc2ff1e1778..00000000000 --- a/src/status_im/ui/screens/wallet/styles.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.ui.screens.wallet.styles - (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) - -;; wallet - -(def toolbar - {:background-color colors/blue}) - -(defstyle toolbar-bottom-line - {:ios {:border-bottom-width 1 - :border-bottom-color colors/white-transparent}}) - -(defn button-container [enabled?] - (merge {:flex-direction :row - :align-items :center} - (when-not enabled? - {:opacity 0.4}))) - -(def wallet-modal-container - {:flex 1 - :background-color colors/blue}) diff --git a/src/status_im/ui/screens/wallet/subs.cljs b/src/status_im/ui/screens/wallet/subs.cljs deleted file mode 100644 index 84640874f17..00000000000 --- a/src/status_im/ui/screens/wallet/subs.cljs +++ /dev/null @@ -1,122 +0,0 @@ -(ns status-im.ui.screens.wallet.subs - (:require [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.i18n :as i18n] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.money :as money])) - -(re-frame/reg-sub :wallet - (fn [db] - (:wallet db))) - -(re-frame/reg-sub :balance - :<- [:wallet] - (fn [wallet] - (:balance wallet))) - -(re-frame/reg-sub :prices - (fn [db] - (:prices db))) - -(re-frame/reg-sub :price - :<- [:prices] - (fn [prices [_ fsym tsym]] - (get-in prices [fsym tsym :price]))) - -(re-frame/reg-sub :last-day - :<- [:prices] - (fn [prices [_ fsym tsym]] - (get-in prices [fsym tsym :last-day]))) - -(re-frame/reg-sub :asset-value - (fn [[_ fsym decimals tsym]] - [(re-frame/subscribe [:balance]) - (re-frame/subscribe [:price fsym tsym]) - (re-frame/subscribe [:wallet/currency])]) - (fn [[balance price currency] [_ fsym decimals tsym]] - (when (and balance price) - (-> (money/internal->formatted (get balance fsym) fsym decimals) - (money/crypto->fiat price) - (money/with-precision 2) - str - (i18n/format-currency (:code currency)))))) - -(defn- get-balance-total-value [balance prices currency token->decimals] - (reduce-kv (fn [acc symbol value] - (if-let [price (get-in prices [symbol currency :price])] - (+ acc (-> (money/internal->formatted value symbol (token->decimals symbol)) - (money/crypto->fiat price) - .toNumber)) - acc)) 0 balance)) - -(re-frame/reg-sub :portfolio-value - :<- [:balance] - :<- [:prices] - :<- [:wallet/currency] - :<- [:network] - (fn [[balance prices currency network] [_ currency-code]] - (if (and balance prices) - (let [assets (tokens/tokens-for (ethereum/network->chain-keyword network)) - token->decimals (into {} (map #(vector (:symbol %) (:decimals %)) assets)) - balance-total-value - (get-balance-total-value balance - prices - (or currency-code - (-> currency :code keyword)) - token->decimals)] - (-> balance-total-value - (money/with-precision 2) - str - (i18n/format-currency (:code currency)))) - "..."))) - -(re-frame/reg-sub :prices-loading? - (fn [db] - (:prices-loading? db))) - -(re-frame/reg-sub :wallet/balance-loading? - :<- [:wallet] - (fn [wallet] - (:balance-loading? wallet))) - -(re-frame/reg-sub :wallet/error-message? - :<- [:wallet] - (fn [wallet] - (or (get-in wallet [:errors :balance-update]) - (get-in wallet [:errors :prices-update])))) - -(re-frame/reg-sub :get-wallet-unread-messages-number - (fn [db] - 0)) - -(re-frame/reg-sub :wallet/visible-tokens-symbols - :<- [:network] - :<- [:get-current-account] - (fn [[network current-account]] - (let [chain (ethereum/network->chain-keyword network)] - (get-in current-account [:settings :wallet :visible-tokens chain])))) - -(re-frame/reg-sub :wallet/visible-assets - :<- [:network] - :<- [:wallet/visible-tokens-symbols] - (fn [[network visible-tokens-symbols]] - (conj (filter #(contains? visible-tokens-symbols (:symbol %)) - (tokens/sorted-tokens-for (ethereum/network->chain-keyword network))) - tokens/ethereum))) - -(re-frame/reg-sub :wallet/visible-assets-with-amount - :<- [:balance] - :<- [:wallet/visible-assets] - (fn [[balance visible-assets]] - (map #(assoc % :amount (get balance (:symbol %))) visible-assets))) - -(re-frame/reg-sub :wallet/transferrable-assets-with-amount - :<- [:wallet/visible-assets-with-amount] - (fn [all-assets] - (filter #(not (:nft? %)) all-assets))) - -(re-frame/reg-sub :wallet/currency - :<- [:wallet.settings/currency] - (fn [currency-id] - (get constants/currencies currency-id))) diff --git a/src/status_im/ui/screens/wallet/transactions/events.cljs b/src/status_im/ui/screens/wallet/transactions/events.cljs deleted file mode 100644 index 1691526d0a1..00000000000 --- a/src/status_im/ui/screens/wallet/transactions/events.cljs +++ /dev/null @@ -1,21 +0,0 @@ -(ns status-im.ui.screens.wallet.transactions.events - (:require [status-im.utils.handlers :as handlers])) - -(defn- mark-all-checked [filters] - (update filters :type #(map (fn [m] (assoc m :checked? true)) %))) - -(defn- mark-checked [filters {:keys [type]} checked?] - (update filters :type #(map (fn [{:keys [id] :as m}] (if (= type id) (assoc m :checked? checked?) m)) %))) - -(defn- update-filters [db f] - (update-in db [:wallet.transactions :filters] f)) - -(handlers/register-handler-db - :wallet.transactions/filter - (fn [db [_ path checked?]] - (update-filters db #(mark-checked % path checked?)))) - -(handlers/register-handler-db - :wallet.transactions/filter-all - (fn [db] - (update-filters db mark-all-checked))) diff --git a/src/status_im/ui/screens/wallet/transactions/styles.cljs b/src/status_im/ui/screens/wallet/transactions/styles.cljs deleted file mode 100644 index 7fd4e6dcd6b..00000000000 --- a/src/status_im/ui/screens/wallet/transactions/styles.cljs +++ /dev/null @@ -1,236 +0,0 @@ -(ns status-im.ui.screens.wallet.transactions.styles - (:require-macros [status-im.utils.styles :refer [defnstyle defstyle]]) - (:require [status-im.ui.components.colors :as colors] - [status-im.ui.screens.main-tabs.styles :as tabs.styles])) - -(defnstyle tab [active?] - {:flex 1 - :height tabs.styles/tab-height - :justify-content :center - :align-items :center - :padding-bottom (if active? 0 1) - :border-bottom-width (if active? 2 1) - :border-bottom-color (if active? - colors/blue - colors/gray-transparent)}) - -(def tabs-container - {:flex-direction :row - :height tabs.styles/tab-height}) - -(defnstyle tab-title [active?] - {:ios {:font-size 15} - :android {:font-size 14} - :text-align :center - :color (if active? - colors/blue - colors/black)}) - -(def transactions - {:flex 1 - :background-color :white}) - -(def tab-unsigned-transactions-count - (merge (tab-title false) - {:color colors/gray})) - -(def forward - {:color colors/gray}) - -(def empty-text - {:text-align :center - :margin-top 22 - :margin-horizontal 92}) - -(defstyle amount-time - {:flex-direction :row - :justify-content :space-between - :padding-right 22 - :padding-left 17 - :ios {:padding-top 13} - :android {:padding-top 14}}) - -(def amount-text - {:color colors/text}) - -(def tx-amount - {:flex-grow 1 - :flex-shrink 1 - :margin-right 10 - :font-size 17}) - -(def tx-time - {:flex-grow 1 - :font-size 14 - :text-align :right - :color colors/blue}) - -(def address-row - {:flex-direction :row - :padding-right 22 - :padding-left 17 - :padding-top 4}) - -(def address-item - {:font-size 16 - :color colors/gray}) - -(def address-label - (merge address-item - {:margin-right 5})) - -(def address-contact - (merge address-item - {:color colors/black - :margin-right 5})) - -(def address-hash - (merge address-item - {:flex-shrink 1})) - -(def action-buttons - {:flex 1 - :flex-direction :row - :padding-vertical 12}) - -(def sign-all-view - {:flex 1 - :flex-direction :column - :justify-content :center - :background-color colors/gray-transparent}) - -(def sign-all-popup - {:align-self :flex-start - :background-color colors/white - :margin-horizontal 12 - :border-radius 8}) - -(def sign-all-popup-text - {:margin-top 8 - :margin-horizontal 12}) - -(def sign-all-actions - {:flex-direction :row - :justify-content :space-between - :margin-horizontal 12 - :margin-vertical 20}) - -(def sign-all-input - {:width 150 - :height 38}) - -(def sign-all-done - {:position :absolute - :right 0 - :top 0}) - -(def sign-all-done-button - {:background-color :transparent}) - -(defn transaction-icon-background [color] - {:justify-content :center - :align-items :center - :width 40 - :height 40 - :border-radius 32 - :background-color color}) - -;; transaction details - -(def details-row - {:flex-direction :row - :margin-vertical 5}) - -(def details-item-label - {:flex 1 - :margin-right 10 - :color colors/gray}) - -(def details-item-value-wrapper - {:flex 5}) - -(def details-item-value - {:font-size 14 - :color colors/black}) - -(def details-item-extra-value - {:font-size 14 - :color colors/gray}) - -(def details-header - {:margin-horizontal 16 - :margin-top 10 - :flex-direction :row}) - -(def details-header-icon - {:margin-vertical 7}) - -(def details-header-infos - {:flex 1 - :flex-direction :column - :margin-left 12 - :margin-vertical 7}) - -(def details-header-value - {:font-size 16 - :color colors/black}) - -(def details-header-date - {:font-size 14 - :color colors/gray}) - -(def details-block - {:margin-horizontal 16}) - -(def progress-bar - {:flex-direction :row - :margin-vertical 10 - :height 2}) - -(defn progress-bar-done [done failed?] - {:flex done - :background-color (if failed? - colors/red - colors/blue)}) - -(defn progress-bar-todo [todo failed?] - {:flex todo - :background-color (if failed? - colors/red - colors/blue) - :opacity 0.30}) - -(def details-confirmations-count - {:color colors/black - :font-size 15 - :margin-vertical 2}) - -(def details-failed - {:color colors/red - :font-size 15 - :margin-vertical 2}) - -(def details-confirmations-helper-text - {:color colors/gray - :margin-vertical 2}) - -(def details-separator - {:background-color colors/gray-light - :height 1 - :margin-vertical 10}) - -(def corner-dot - {:position :absolute - :top 0 - :right 0 - :width 4 - :height 4 - :border-radius 2 - :background-color colors/cyan}) - -(def filter-container - {:flex 1}) - -(def transactions-view - {:flex 1 - :background-color :white}) diff --git a/src/status_im/ui/screens/wallet/transactions/subs.cljs b/src/status_im/ui/screens/wallet/transactions/subs.cljs deleted file mode 100644 index 8cbd71b223b..00000000000 --- a/src/status_im/ui/screens/wallet/transactions/subs.cljs +++ /dev/null @@ -1,176 +0,0 @@ -(ns status-im.ui.screens.wallet.transactions.subs - (:require [re-frame.core :refer [reg-sub subscribe]] - [status-im.i18n :as i18n] - [status-im.utils.datetime :as datetime] - [status-im.utils.hex :as utils.hex] - [status-im.utils.money :as money] - [status-im.utils.transactions :as transactions] - [status-im.utils.ethereum.core :as ethereum])) - -(reg-sub :wallet.transactions/transactions-loading? - :<- [:wallet] - (fn [wallet] - (:transactions-loading? wallet))) - -(reg-sub :wallet.transactions/current-tab - :<- [:wallet] - (fn [wallet] - (get wallet :current-tab 0))) - -(defn enrich-transaction [{:keys [type to from timestamp] :as transaction} contacts] - (let [[contact-address key-contact key-wallet] (if (= type :inbound) - [from :from-contact :to-wallet] - [to :to-contact :from-wallet]) - wallet (i18n/label :main-wallet) - contact (get contacts (utils.hex/normalize-hex contact-address))] - (cond-> transaction - contact (assoc key-contact (:name contact)) - :always (assoc key-wallet wallet - :time-formatted (datetime/timestamp->time timestamp))))) - -(reg-sub :wallet.transactions/transactions - :<- [:wallet] - :<- [:get-contacts-by-address] - (fn [[wallet contacts]] - (reduce (fn [acc [hash transaction]] - (assoc acc hash (enrich-transaction transaction contacts))) - {} - (:transactions wallet)))) - -(reg-sub :wallet.transactions/grouped-transactions - :<- [:wallet.transactions/transactions] - (fn [transactions] - (group-by :type (vals transactions)))) - -(defn- format-unsigned-transaction [{:keys [id] :as transaction}] - (assoc transaction - :type :unsigned - :confirmations 0 - ;; TODO (andrey) revisit this, we shouldn't set not hash value to the hash field - :hash id)) - -(reg-sub :wallet/unsigned-transactions - :<- [:wallet] - :<- [:get-contacts-by-address] - (fn [[wallet contacts]] - (map #(enrich-transaction % contacts) (vals (:transactions-unsigned wallet))))) - -(reg-sub :wallet.transactions/unsigned-transactions - :<- [:wallet/unsigned-transactions] - (fn [transactions] - (reduce (fn [acc {:keys [id] :as transaction}] - (assoc acc id (format-unsigned-transaction transaction))) - {} - transactions))) - -(reg-sub :wallet.transactions/unsigned-transactions-count - :<- [:wallet.transactions/unsigned-transactions] - (fn [unsigned-transactions] - (count unsigned-transactions))) - -(reg-sub :wallet.transactions/unsigned-transactions-list - :<- [:wallet.transactions/unsigned-transactions] - (fn [unsigned-transactions] - (vals unsigned-transactions))) - -(reg-sub :wallet.transactions/postponed-transactions-list - :<- [:wallet.transactions/grouped-transactions] - (fn [{:keys [postponed]}] - (when postponed - {:title "Postponed" - :key :postponed - :data postponed}))) - -(reg-sub :wallet.transactions/pending-transactions-list - :<- [:wallet.transactions/grouped-transactions] - (fn [{:keys [pending]}] - (when pending - {:title "Pending" - :key :pending - :data pending}))) - -(reg-sub :wallet.transactions/failed-transactions-list - :<- [:wallet.transactions/grouped-transactions] - (fn [{:keys [failed]}] - (when failed - {:title "Failed" - :key :failed - :data failed}))) - -(defn group-transactions-by-date [transactions] - (->> transactions - (group-by #(datetime/timestamp->date-key (:timestamp %))) - (sort-by key) - reverse - (map (fn [[date-key transactions]] - {:title (datetime/timestamp->mini-date (:timestamp (first transactions))) - :key date-key - :data (sort-by :timestamp > transactions)})))) - -(reg-sub :wallet.transactions/completed-transactions-list - :<- [:wallet.transactions/grouped-transactions] - (fn [{:keys [inbound outbound failed]}] - (group-transactions-by-date (concat inbound outbound failed)))) - -(reg-sub :wallet.transactions/transactions-history-list - :<- [:wallet.transactions/postponed-transactions-list] - :<- [:wallet.transactions/pending-transactions-list] - :<- [:wallet.transactions/completed-transactions-list] - (fn [[postponed pending completed]] - (cond-> [] - postponed (into postponed) - pending (into pending) - completed (into completed)))) - -(reg-sub :wallet.transactions/current-transaction - :<- [:wallet] - (fn [wallet] - (:current-transaction wallet))) - -(reg-sub :wallet.transactions/transaction-details - :<- [:wallet.transactions/unsigned-transactions] - :<- [:wallet.transactions/transactions] - :<- [:wallet.transactions/current-transaction] - :<- [:network] - (fn [[unsigned-transactions transactions current-transaction network]] - (let [transactions (merge transactions unsigned-transactions) - {:keys [gas-used gas-price hash timestamp type] :as transaction} (get transactions current-transaction) - chain (ethereum/network->chain-keyword network)] - (when transaction - (merge transaction - {:gas-price-eth (if gas-price (money/wei->str :eth gas-price) "-") - :gas-price-gwei (if gas-price (money/wei->str :gwei gas-price) "-") - :date (datetime/timestamp->long-date timestamp)} - (if (= type :unsigned) - {:block (i18n/label :not-applicable) - :cost (i18n/label :not-applicable) - :gas-limit (i18n/label :not-applicable) - :gas-used (i18n/label :not-applicable) - :nonce (i18n/label :not-applicable) - :hash (i18n/label :not-applicable)} - {:cost (when gas-used - (money/wei->str :eth (money/fee-value gas-used gas-price))) - :url (transactions/get-transaction-details-url chain hash)})))))) - -(reg-sub :wallet.transactions.details/confirmations - :<- [:wallet.transactions/transaction-details] - (fn [transaction-details] - ;;TODO (yenda) this field should be calculated based on the current-block and the block of the transaction - (:confirmations transaction-details))) - -(reg-sub :wallet.transactions.details/confirmations-progress - :<- [:wallet.transactions.details/confirmations] - (fn [confirmations] - (let [max-confirmations 10] - (if (>= confirmations max-confirmations) - 100 - (* 100 (/ confirmations max-confirmations)))))) - -(reg-sub :wallet.transactions/filters - (fn [db] - (get-in db [:wallet.transactions :filters]))) - -(reg-sub :wallet.transactions/error-message? - :<- [:wallet] - (fn [wallet] - (get-in wallet [:errors :transactions-update]))) diff --git a/src/status_im/ui/screens/wallet/transactions/views.cljs b/src/status_im/ui/screens/wallet/transactions/views.cljs deleted file mode 100644 index 1d239cf4cb5..00000000000 --- a/src/status_im/ui/screens/wallet/transactions/views.cljs +++ /dev/null @@ -1,256 +0,0 @@ -(ns status-im.ui.screens.wallet.transactions.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.screens.wallet.transactions.styles :as styles] - [status-im.utils.money :as money] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.ethereum.core :as ethereum])) - -(defn history-action [filter?] - (cond-> - {:icon :icons/filter - :icon-opts {:accessibility-label :filters-button} - :handler #(re-frame/dispatch [:navigate-to-modal :wallet-transactions-filter])} - filter? (assoc-in [:icon-opts :overlay-style] styles/corner-dot))) - -(defn- all-checked? [filter-data] - (and (every? :checked? (:type filter-data)) - (every? :checked? (:tokens filter-data)))) - -(defn- toolbar-view [filter-data] - [toolbar/toolbar nil - toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/transactions-history)] - [toolbar/actions - [(history-action (not (all-checked? filter-data)))]]]) - -(defn- inbound? [type] (= :inbound type)) -(defn- failed? [type] (= :failed type)) - -(defn- transaction-icon [k background-color color] - {:icon k - :icon-opts {:color color} - :style (styles/transaction-icon-background background-color)}) - -(defn- transaction-type->icon [k] - (case k - :inbound (transaction-icon :icons/arrow-left (colors/alpha colors/green 0.2) colors/green) - :outbound (transaction-icon :icons/arrow-right (colors/alpha colors/blue 0.1) colors/blue) - :failed (transaction-icon :icons/exclamation-mark colors/gray-light colors/red) - (:postponed :pending) (transaction-icon :icons/arrow-right colors/gray-light colors/gray) - (throw (str "Unknown transaction type: " k)))) - -(defn render-transaction [{:keys [hash from-contact to-contact to from type value time-formatted symbol]} network hide-details?] - (let [[label contact address - contact-accessibility-label - address-accessibility-label] (if (inbound? type) - [(i18n/label :t/from) from-contact from :sender-text :sender-address-text] - [(i18n/label :t/to) to-contact to :recipient-name-text :recipient-address-text]) - {:keys [decimals]} (tokens/asset-for (ethereum/network->chain-keyword network) symbol)] - [list/touchable-item #(when-not hide-details? (re-frame/dispatch [:show-transaction-details hash])) - [react/view {:accessibility-label :transaction-item} - [list/item - [list/item-icon (transaction-type->icon (keyword type))] - [list/item-content - [react/view {:style styles/amount-time} - [react/text {:style styles/tx-amount - :ellipsize-mode "tail" - :number-of-lines 1} - [react/text {:accessibility-label :amount-text - :style styles/amount-text} - (-> value (money/internal->formatted symbol decimals) money/to-fixed str)] - " " - [react/text {:accessibility-label :currency-text} - (clojure.string/upper-case (name symbol))]] - [react/text {:style styles/tx-time} - time-formatted]] - [react/view {:style styles/address-row} - [react/text {:style styles/address-label} - label] - (when contact - [react/text {:style styles/address-contact - :accessibility-label contact-accessibility-label} - contact]) - [react/text {:style styles/address-hash - :ellipsize-mode "middle" - :number-of-lines 1 - :accessibility-label address-accessibility-label} - address]]] - (when-not hide-details? - [list/item-icon {:icon :icons/forward - :style {:margin-top 10} - :icon-opts (merge styles/forward - {:accessibility-label :show-transaction-button})}])]]])) - -(defn filtered-transaction? [transaction filter-data] - (:checked? (some #(when (= (:type transaction) (:id %)) %) (:type filter-data)))) - -(defn update-transactions [m filter-data] - (update m :data (fn [v] (filter #(filtered-transaction? % filter-data) v)))) - -(defview history-list [& [hide-details?]] - (letsubs [transactions-history-list [:wallet.transactions/transactions-history-list] - filter-data [:wallet.transactions/filters] - network [:get-current-account-network]] - [react/view components.styles/flex - [list/section-list {:sections (map #(update-transactions % filter-data) transactions-history-list) - :key-fn :hash - :render-fn #(render-transaction % network hide-details?) - :empty-component [react/i18n-text {:style styles/empty-text - :key :transactions-history-empty}] - :on-refresh #(re-frame/dispatch [:update-transactions]) - :refreshing false}]])) - -;; Filter history - -(defn- item-filter [{:keys [icon checked? path]} content] - [react/view {:accessibility-label :filter-item} - [list/list-item-with-checkbox - {:checked? checked? - :on-value-change #(re-frame/dispatch [:wallet.transactions/filter path %])} - [list/item - [list/item-icon icon] - content]]]) - -(defn- render-item-filter [{:keys [id label checked?]}] - [item-filter {:icon (transaction-type->icon id) :checked? checked? :path {:type id}} - [list/item-content - [list/item-primary-only {:accessibility-label :filter-name-text} - label]]]) - -(defn- wrap-filter-data [m] - [{:title (i18n/label :t/transactions-filter-type) - :key :type - :render-fn render-item-filter - :data (:type m)}]) - -(defview filter-history [] - (letsubs [filter-data [:wallet.transactions/filters]] - [react/view styles/filter-container - [status-bar/status-bar {:type :modal-white}] - [toolbar/toolbar {} - [toolbar/nav-clear-text {:accessibility-label :done-button} (i18n/label :t/done)] - [toolbar/content-title (i18n/label :t/transactions-filter-title)] - [toolbar/text-action {:handler #(re-frame/dispatch [:wallet.transactions/filter-all]) - :disabled? (all-checked? filter-data) - :accessibility-label :select-all-button} - (i18n/label :t/transactions-filter-select-all)]] - [react/view {:style (merge {:background-color :white} components.styles/flex)} - [list/section-list {:sections (wrap-filter-data filter-data) - :key-fn (comp str :id)}]]])) - -(defview transactions [] - (letsubs [filter-data [:wallet.transactions/filters]] - [react/view styles/transactions-view - [status-bar/status-bar] - [toolbar-view filter-data] - [history-list]])) - -(defn- pretty-print-asset [symbol amount token] - (if amount - (if (= :ETH symbol) - (->> amount (money/wei-> :eth) money/to-fixed str) - (-> amount (money/token->unit (:decimals token)) money/to-fixed str)) - "...")) - -(defn details-header [{:keys [value date type symbol token]}] - [react/view {:style styles/details-header} - [react/view {:style styles/details-header-icon} - [list/item-icon (transaction-type->icon type)]] - [react/view {:style styles/details-header-infos} - [react/text {:style styles/details-header-value} - [react/text {:accessibility-label :amount-text} - (pretty-print-asset symbol value token)] - " " - [react/text {:accessibility-label :currency-text} - (clojure.string/upper-case (name symbol))]] - [react/text {:style styles/details-header-date} date]]]) - -(defn progress-bar [progress failed?] - [react/view {:style styles/progress-bar} - [react/view {:style (styles/progress-bar-done progress failed?)}] - [react/view {:style (styles/progress-bar-todo (- 100 progress) failed?)}]]) - -(defn details-confirmations [confirmations confirmations-progress type] - [react/view {:style styles/details-block} - [progress-bar confirmations-progress (failed? type)] - (if (failed? type) - [react/i18n-text {:style styles/details-failed - :key :failed}] - [react/text {:style styles/details-confirmations-count} - (str confirmations " " (i18n/label :t/confirmations))]) - [react/i18n-text {:style styles/details-confirmations-helper-text - :key :confirmations-helper-text}]]) - -(defn details-list-row - ([label props-value] - (details-list-row label props-value nil)) - ([label props-value extra-props-value] - (let [[props value] (if (string? props-value) - [nil props-value] - props-value) - [extra-props extra-value] (if (string? extra-props-value) - [nil extra-props-value] - extra-props-value)] - [react/view {:style styles/details-row} - [react/i18n-text {:style styles/details-item-label :key label}] - [react/view {:style styles/details-item-value-wrapper} - [react/text (merge {:style styles/details-item-value} props) - (str (or value "-"))] - [react/text (merge {:style styles/details-item-extra-value} extra-props) - (str extra-value)]]]))) - -(defn details-list [{:keys [block hash - from from-wallet from-contact - to to-wallet to-contact - gas-limit gas-price-gwei gas-price-eth gas-used cost nonce data]}] - [react/view {:style styles/details-block} - [details-list-row :t/block block] - [details-list-row :t/hash hash] - [details-list-row :t/from - [{:accessibility-label (if from-wallet :sender-name-text :sender-address-text)} - (or from-wallet from-contact from)] - (when (or from-wallet from-contact) - [{:accessibility-label :sender-address-text} - from])] - [details-list-row :t/to - [{:accessibility-label (if to-wallet :recipient-name-text :recipient-address-text)} - (or to-wallet to-contact to)] - (when (or to-wallet to-contact) - [{:accessibility-label :recipient-address-text} - to])] - [details-list-row :t/gas-limit gas-limit] - [details-list-row :t/gas-price gas-price-gwei gas-price-eth] - [details-list-row :t/gas-used gas-used] - [details-list-row :t/cost-fee cost] - [details-list-row :t/nonce nonce] - [details-list-row :t/data data]]) - -(defn details-action [hash url] - [(actions/opts [{:label (i18n/label :t/copy-transaction-hash) :action #(react/copy-to-clipboard hash)} - {:label (i18n/label :t/open-on-etherscan) :action #(.openURL react/linking url)}])]) - -(defview transaction-details [] - (letsubs [{:keys [hash url type] :as transaction} [:wallet.transactions/transaction-details] - confirmations [:wallet.transactions.details/confirmations] - confirmations-progress [:wallet.transactions.details/confirmations-progress]] - [react/view {:style components.styles/flex} - [status-bar/status-bar] - [toolbar/toolbar {} - toolbar/default-nav-back - [toolbar/content-title (i18n/label :t/transaction-details)] - (when transaction [toolbar/actions (details-action hash url)])] - [react/scroll-view {:style components.styles/main-container} - [details-header transaction] - [details-confirmations confirmations confirmations-progress type] - [react/view {:style styles/details-separator}] - [details-list transaction]]])) diff --git a/src/status_im/ui/screens/wallet/utils.cljs b/src/status_im/ui/screens/wallet/utils.cljs deleted file mode 100644 index e6c0bf8efbf..00000000000 --- a/src/status_im/ui/screens/wallet/utils.cljs +++ /dev/null @@ -1,8 +0,0 @@ -(ns status-im.ui.screens.wallet.utils - (:require [status-im.utils.money :as money])) - -(defn format-amount [amount decimals] - (-> amount - (or (money/bignumber 0)) - (money/token->unit decimals) - money/to-fixed)) \ No newline at end of file diff --git a/src/status_im/utils/async.cljs b/src/status_im/utils/async.cljs deleted file mode 100644 index 248f52faa4d..00000000000 --- a/src/status_im/utils/async.cljs +++ /dev/null @@ -1,42 +0,0 @@ -(ns status-im.utils.async - "Utility namespace containing `core.async` helper constructs" - (:require [cljs.core.async :as async] - [status-im.utils.utils :as utils])) - -(defn timeout [ms] - (let [c (async/chan)] - (utils/set-timeout (fn [] (async/close! c)) ms) - c)) - -(defn chunked-pipe! - "Connects input channel to the output channel with time-based chunking. - `flush-time` parameter decides for how long we are waiting to accumulate - value from input channel in a vector before it's put on the output channel. - When `flush-time` interval elapses and there are no values accumulated, nothing - is put on the output channel till the next input arrives, which is then put on - the output channel immediately (wrapped in a vector). - When input channel is closed, output channel is closed as well and go-loop exits." - [input-ch output-ch flush-time] - (async/go-loop [acc [] - flush? false] - (if flush? - (do (async/put! output-ch acc) - (recur [] false)) - (let [[v ch] (async/alts! [input-ch (timeout flush-time)])] - (if (= ch input-ch) - (if v - (recur (conj acc v) (and (seq acc) flush?)) - (async/close! output-ch)) - (recur acc (seq acc))))))) - -(defn task-queue - "Creates `core.async` channel which will process 0 arg functions put there in serial fashon. - Takes the same argument/s as `core.async/chan`, those arguments will be delegated to the - channel constructor. - Returns task-queue where tasks represented by 0 arg task functions can be put for processing." - [& args] - (let [task-queue (apply async/chan args)] - (async/go-loop [task-fn (async/ }, JS code will be injected if the domain name matches (www. is stripped away) - -{:inject-js {"cryptokitties.co" "; if (!window.chrome) { window.chrome = { webstore: true }; }"}} diff --git a/src/status_im/utils/build.clj b/src/status_im/utils/build.clj deleted file mode 100644 index d843e6d7f0f..00000000000 --- a/src/status_im/utils/build.clj +++ /dev/null @@ -1,42 +0,0 @@ -(ns status-im.utils.build - (:require [cljs.analyzer :as analyzer] - [clojure.string :as string] - [clojure.java.shell :as shell] - [clojure.java.io :as io])) - -;; Some warnings are unavoidable due to dependencies. For example, reagent 0.6.0 -;; has a warning in its util.cljs namespace. Adjust this as is necessary and -;; unavoidable warnings arise. -(def acceptable-warning? - #{"Protocol IFn implements method -invoke with variadic signature (&)"}) ;; reagent 0.6.0 reagent/impl/util.cljs:61 - -(defn nil-acceptable-warning [s] - (when-not (acceptable-warning? s) - s)) - -(defn warning-handler [warning-type env extra] - (when (warning-type analyzer/*cljs-warnings*) - (when-let [s (nil-acceptable-warning (analyzer/error-message warning-type extra))] - (binding [*out* *err*] - (println (analyzer/message env (str "\u001B[31mWARNING\u001B[0m: " s)))) - (System/exit 1)))) - -(defmacro get-build-no [] - (-> (shell/sh "bash" "-c" "sh ./scripts/build_no.sh") - :out - (string/replace "\n" ""))) - -(defmacro get-current-sha [] - "fetches the latest commit sha from the current branch" - (-> (shell/sh "bash" "-c" "git describe --always") - :out - (string/replace "\n" ""))) - -(defmacro git-short-version [] - (let [version-file-path "VERSION" - version-file (io/file version-file-path)] - (if (.exists version-file) - (string/trim (slurp version-file-path)) - (-> (shell/sh "bash" "-c" "git describe --always") - :out - (string/replace "\n" ""))))) diff --git a/src/status_im/utils/build.cljs b/src/status_im/utils/build.cljs deleted file mode 100644 index 2ce3c1d966b..00000000000 --- a/src/status_im/utils/build.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns status-im.utils.build - (:require-macros [status-im.utils.build :as build])) - -(def version (build/git-short-version)) -(def commit-sha (build/get-current-sha)) -(def build-no (build/get-build-no)) diff --git a/src/status_im/utils/clocks.cljs b/src/status_im/utils/clocks.cljs deleted file mode 100644 index 7f13642d8f1..00000000000 --- a/src/status_im/utils/clocks.cljs +++ /dev/null @@ -1,94 +0,0 @@ -(ns status-im.utils.clocks - (:require [status-im.utils.datetime :as utils.datetime])) - -;; We use Lamport clocks to ensure correct ordering of events in chats. This is -;; necessary because we operate in a distributed system and there is no central -;; coordinator for what happened before what. -;; -;; We can't rely uniquely on timestamps as clocks might be different on each device. -;; -;; Received time cannot be used as it does not work with out-of-order messages. -;; If we used received time also each client could potentially have a different -;; ordering of messages, which would lead to some difficult misunderstanding -;; among participants. -;; -;; Lamport timestamps offer a consistent view across client, at the expenses of -;; understanding exactly at what time something has happened. -;; They satisfy the property: if a caused b then T(a) < T(b) -;; -;; In chat terms: -;; -;; Any message I send will always be displayed after any message I have seen, -;; including the messages I have sent. -;; This is a necessary condition to have a meaningful conversation with someone -;; and ought to be always true. -;; -;; We need to address another issue here: -;; -;; Even if I don't see all the messages, if I post a message I want that message -;; to be displayed last in a chat. -;; -;; That's were the basic algorithm of Lamport timestamp would fall short, as -;; it's only meant to order causally related events. -;; -;; If I join a public chat and I have not received any messages or I have missed -;; many messages because I was offline, when I post a new message it would be -;; displayed back in the history ( I would have to wait to receive a message -;; to bring my timestamp up-to-date). -;; -;; We cannot completely solve this as there's no way to know what the chat -;; current timestamp is without having to contact other peers ( which might all be offline) -;; -;; But what we can do, is to use our time to make a "bid", hoping that it will -;; beat the current chat-timestamp. So our Lamport timestamp format is: -;; {unix-timestamp-ms}{2-digits-post-id} -;; -;; We always need to make sure we take the max value between the last-clock-value -;; for the chat and the bid-timestamp. -;; -;; This will still satisfy Lamport requirement, namely: a -> b then T(a) < T(b) -;; -;; One way to think of this is as as Lamport timestamps where at every ms -;; an internal event is generated. -;; -;; In whisper v6 any message with a timestamp older than 20 seconds will be discarded. -;; -;; So worst case scenario is: -;; Your clock is 20 seconds behind, you join a public chat where everyone's clock -;; is 20 seconds ahead, you have not received 40s of inflight messages, you -;; publish. drama. -;; Your post will be displayed before any non-received inflight message. -;; -;; Once received the posts you will be able to communicate effectively, much rejoicing. -;; If there are no inflight messages then your post will be last. -;; -;; Posts sent when offline are more troublesome, as they would carry an old -;; timestamp, so the timestamp should be refreshed before retrying. -;; -;; Details: -;; https://en.wikipedia.org/wiki/Lamport_timestamps -;; http://amturing.acm.org/p558-lamport.pdf - -(def one-month-in-ms (* 60 60 24 31 1000)) -(def post-id-digits 100) - -(defn- ->timestamp-bid [] - (* (utils.datetime/timestamp) post-id-digits)) - -; The timestamp has an upper limit of Number.MAX_SAFE_INTEGER -; A malicious client could send a crafted message with timestamp = Number.MAX_SAFE_INTEGER -; which effectively would DoS the chat, as any new message would get -; a timestamp of Number.MAX_SAFE_INTEGER (inc becomes a noop). -; We should never receive messages from untrusted peers with a timestamp greater -; then now + 20s. -; We cap the timestamp to time now + 1 month to give some room for trusted peers -(defn- safe-timestamp [t] - (min t (* (+ one-month-in-ms (utils.datetime/timestamp)) post-id-digits))) - -(defn send [local-clock] - (inc (max local-clock (->timestamp-bid)))) - -(defn receive [message-clock local-clock] - (-> (+ 1000 (max (or message-clock 0) (or local-clock 0))) - safe-timestamp - inc)) diff --git a/src/status_im/utils/config.cljs b/src/status_im/utils/config.cljs deleted file mode 100644 index dff557e2003..00000000000 --- a/src/status_im/utils/config.cljs +++ /dev/null @@ -1,45 +0,0 @@ -(ns status-im.utils.config - (:require [status-im.react-native.js-dependencies :as rn-dependencies] - [clojure.string :as string])) - -(def config (js->clj (.-default rn-dependencies/config) :keywordize-keys true)) - -(defn get-config - ([k] (get config k)) - ([k not-found] (get config k not-found))) - -;; TODO(oskarth): Extend this to deal with true/false for Jenkins parameter builds -(defn enabled? [v] (= "1" v)) - -;; NOTE(oskarth): Feature flag deprecation lifecycles. We want to make sure -;; flags stay up to date and are removed once behavior introduced is stable. -;; -;; TESTFAIRY_ENABLED - indefinite -;; OFFLINE_INBOX_ENABLED - TBD, tenatively until #idea 1 is merged - -(def testfairy-enabled? (enabled? (get-config :TESTFAIRY_ENABLED))) -(def offline-inbox-enabled? (enabled? (get-config :OFFLINE_INBOX_ENABLED "1"))) -(def bootnodes-settings-enabled? (enabled? (get-config :BOOTNODES_SETTINGS_ENABLED "1"))) -(def universal-links-enabled? (enabled? (get-config :UNIVERSAL_LINK_ENABLED "1"))) -(def log-level - (-> (get-config :LOG_LEVEL "error") - string/lower-case - keyword)) - -(def add-custom-mailservers-enabled? (enabled? (get-config :ADD_CUSTOM_MAILSERVERS_ENABLED "1"))) -(def rn-bridge-threshold-warnings-enabled? (enabled? (get-config :RN_BRIDGE_THRESHOLD_WARNINGS 0))) -(def default-network (get-config :DEFAULT_NETWORK)) -;; the default value should be a string for `enabled?` to work correctly. -(def rpc-networks-only? (enabled? (get-config :RPC_NETWORKS_ONLY "1"))) -(def testfairy-token (get-config :TESTFAIRY_TOKEN)) -(def instabug-token (get-config :INSTABUG_TOKEN)) -(def instabug-surveys-enabled? (get-config :INSTABUG_SURVEYS)) -(def force-sr-ratio-tracking (get-config :FORCE_SENT_RECEIVED_TRACKING)) - -(def pow-target (js/parseFloat (get-config :POW_TARGET "0.002"))) -(def pow-time (js/parseInt (get-config :POW_TIME "1"))) -(def use-sym-key (enabled? (get-config :USE_SYM_KEY 0))) - -(def group-chats-enabled? (enabled? (get-config :GROUP_CHATS_ENABLED))) -(def spam-button-detection-enabled? (enabled? (get-config :SPAM_BUTTON_DETECTION_ENABLED "0"))) -(def mainnet-warning-enabled? (enabled? (get-config :MAINNET_WARNING_ENABLED 0))) diff --git a/src/status_im/utils/contacts.cljs b/src/status_im/utils/contacts.cljs deleted file mode 100644 index 9039e4e21e0..00000000000 --- a/src/status_im/utils/contacts.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.utils.contacts - (:require [status-im.js-dependencies :as js-dependencies] - [status-im.utils.identicon :as identicon] - [status-im.utils.gfycat.core :as gfycat])) - -(defn whisper-id->new-contact [whisper-id] - {:name (gfycat/generate-gfy whisper-id) - :photo-path (identicon/identicon whisper-id) - :whisper-identity whisper-id}) - -(defn public-key->address [public-key] - (let [length (count public-key) - normalized-key (case length - 132 (subs public-key 4) - 130 (subs public-key 2) - 128 public-key - nil)] - (when normalized-key - (subs (.sha3 js-dependencies/Web3.prototype normalized-key #js {:encoding "hex"}) 26)))) diff --git a/src/status_im/utils/core.cljc b/src/status_im/utils/core.cljc deleted file mode 100644 index f13ca6065fc..00000000000 --- a/src/status_im/utils/core.cljc +++ /dev/null @@ -1,69 +0,0 @@ -(ns status-im.utils.core - (:require [clojure.string :as str])) - -(defn truncate-str - "Given string and max threshold, trims the string to threshold length with `...` - appended to end or in the middle if length of the string exceeds max threshold, - returns the same string if threshold is not exceeded" - [s threshold & [middle?]] - (if (and s (< threshold (count s))) - (if middle? - (let [str-len (count s) - max-len (- threshold 3) - start-len (Math/ceil (/ max-len 2)) - end-len (Math/floor (/ max-len 2)) - start (subs s 0 start-len) - end (subs s (- str-len end-len) str-len)] - (str start "..." end)) - (str (subs s 0 (- threshold 3)) "...")) - s)) - -(defn clean-text [s] - (-> s - (str/replace #"\n" "") - (str/replace #"\r" "") - (str/trim))) - -(defn first-index - "Returns first index in coll where predicate on coll element is truthy" - [pred coll] - (->> coll - (keep-indexed (fn [idx e] - (when (pred e) - idx))) - first)) - -(defn hash-tag? [s] - (= \# (first s))) - -(defn wrap-call-once! - "Returns a version of provided function that will be called only the first time wrapping function is called. Returns nil." - [f] - (let [called? (volatile! false)] - (fn [& args] - (when-not @called? - (vreset! called? true) - (apply f args) - nil)))) - -(defn update-if-present - "Like regular `clojure.core/update` but returns original map if update key is not present" - [m k f & args] - (if (contains? m k) - (apply update m k f args) - m)) - -(defn map-values - "Efficiently apply function to all map values" - [m f] - (into {} - (map (fn [[k v]] - [k (f v)])) - m)) - -(defn deep-merge - "Recursively merge maps" - [& maps] - (if (every? map? maps) - (apply merge-with deep-merge maps) - (last maps))) diff --git a/src/status_im/utils/datetime.cljs b/src/status_im/utils/datetime.cljs deleted file mode 100644 index 9420cf6fe29..00000000000 --- a/src/status_im/utils/datetime.cljs +++ /dev/null @@ -1,163 +0,0 @@ -(ns status-im.utils.datetime - (:require [cljs-time.core :as t :refer [date-time plus days hours before?]] - [cljs-time.coerce :refer [from-long to-long from-date]] - [cljs-time.format :refer [formatters - formatter - unparse]] - [status-im.i18n :refer [label label-pluralize]] - [goog.string :as gstring] - goog.string.format - goog.i18n.DateTimeFormat - [clojure.string :as s] - [goog.object :refer [get]])) - -(defn now [] - (t/now)) - -(def hour (* 1000 60 60)) -(def day (* hour 24)) -(def week (* 7 day)) -(def units [{:name :t/datetime-second :limit 60 :in-second 1} - {:name :t/datetime-minute :limit 3600 :in-second 60} - {:name :t/datetime-hour :limit 86400 :in-second 3600} - {:name :t/datetime-day :limit nil :in-second 86400}]) - -(def time-zone-offset (hours (- (/ (.getTimezoneOffset (js/Date.)) 60)))) - -;; xx-YY locale, xx locale or en fallback -(defn- locale-symbols [locale-name] - (if-let [loc (get goog.i18n (str "DateTimeSymbols_" locale-name))] - loc - (let [name-first (s/replace (or locale-name "") #"-.*$" "") - loc (get goog.i18n (str "DateTimeSymbols_" name-first))] - (or loc goog.i18n.DateTimeSymbols_en)))) - -;; detects if given locale symbols timeformat generates AM/PM ("a") -(defn- is24Hour-locsym [locsym] - (not (s/includes? - (nth (get locsym 'TIMEFORMATS) 2) - "a"))) - -;; returns is24Hour from device or from given locale symbols -;; when device-info module is not available (ie. desktop) returns from the given locale -; -; TODO integrate with native module. example: -; (defn- is24Hour [locsym] -; (if rn/device-info -; (.is24Hour rn/device-info) -; (is24Hour-locsym locsym))) - -(defn- is24Hour [locsym] - (is24Hour-locsym locsym)) - -;; time formats -(defn- short-time-format [locsym] (if (is24Hour locsym) "HH:mm" "h:mm a")) -(defn- time-format [locsym] (if (is24Hour locsym) "HH:mm:ss" "h:mm:ss a")) - -;; date formats -(defn- short-date-format [locsym] "dd MMM") -(defn- medium-date-format [locsym] (nth (get locsym 'DATEFORMATS) 2)) ; get medium format from current locale symbols - -;; date-time formats -(defn- medium-date-time-format [locsym] (str (medium-date-format locsym) ", " (time-format locsym))) - -;; get formatter for current locale symbols and format function -(defn- mk-fmt [locale format-fn] - (let [locsym (locale-symbols locale)] - (goog.i18n.DateTimeFormat. (format-fn locsym) locsym))) - -;; generate formatters for different formats -(def date-time-fmt - (mk-fmt status-im.i18n/locale medium-date-time-format)) -(def date-fmt - (mk-fmt status-im.i18n/locale medium-date-format)) -(def time-fmt - (mk-fmt status-im.i18n/locale short-time-format)) -(def short-date-fmt - (mk-fmt status-im.i18n/locale short-date-format)) - -;; -;; functions which apply formats for the given timestamp -;; - -(defn- to-str [ms old-fmt-fn yesterday-fmt-fn today-fmt-fn] - (let [date (from-long ms) - local (plus date time-zone-offset) ; this is wrong, it uses the current timezone offset, regardless of DST - today (t/today-at-midnight) - yesterday (plus today (days -1))] - (cond - (before? date yesterday) (old-fmt-fn local) - (before? date today) (yesterday-fmt-fn local) - :else (today-fmt-fn local)))) - -(defn to-short-str [ms] - (to-str ms - #(.format date-fmt %) - #(label :t/datetime-yesterday) - #(.format time-fmt %))) - -(defn day-relative [ms] - (to-str ms - #(.format date-fmt %) - #(label :t/datetime-yesterday) - #(label :t/datetime-today))) - -(defn timestamp->mini-date [ms] - (.format short-date-fmt (-> ms - from-long - (plus time-zone-offset)))) - -(defn timestamp->time [ms] - (.format time-fmt (-> ms - from-long - (plus time-zone-offset)))) - -(defn timestamp->date-key [ms] - (keyword (unparse (formatter "YYYYMMDD") (-> ms - from-long - (plus time-zone-offset))))) - -(defn timestamp->long-date [ms] - (.format date-time-fmt (-> ms - from-long - (plus time-zone-offset)))) - -(defn format-time-ago [diff unit] - (let [name (label-pluralize diff (:name unit))] - (label :t/datetime-ago-format {:ago (label :t/datetime-ago) - :number diff - :time-intervals name}))) - -(defn time-ago [time] - (let [diff (t/in-seconds (t/interval time (t/now)))] - (if (< diff 60) - (label :t/active-online) - (let [unit (first (drop-while #(and (>= diff (:limit %)) - (:limit %)) - units))] - (-> (/ diff (:in-second unit)) - Math/floor - int - (format-time-ago unit)))))) - -(defn to-date [ms] - (from-long ms)) - -(defn timestamp [] - (inst-ms (js/Date.))) - -(defn format-date [format date] - (let [local (plus (from-date date) time-zone-offset)] - (unparse (formatter format) local))) - -(defn get-ordinal-date [date] - (let [local (plus (from-date date) time-zone-offset) - day (js/parseInt (unparse (formatter "d") local)) - s {0 "th" - 1 "st" - 2 "nd" - 3 "rd"} - m (mod day 100)] - (str day (or (s (mod (- m 20) 10)) - (s m) - (s 0))))) diff --git a/src/status_im/utils/db.clj b/src/status_im/utils/db.clj deleted file mode 100644 index 7ef66ccb12c..00000000000 --- a/src/status_im/utils/db.clj +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.utils.db - (:require [cljs.spec.alpha :as s])) - -(defmacro allowed-keys - [& {:keys [req req-un opt opt-un] :as args}] - `(s/merge (s/keys ~@(apply concat (vec args))) - - (s/map-of ~(set (concat req - (map (comp keyword name) req-un) - opt - (map (comp keyword name) opt-un))) - any?))) \ No newline at end of file diff --git a/src/status_im/utils/db.cljs b/src/status_im/utils/db.cljs deleted file mode 100644 index 744a1c60d86..00000000000 --- a/src/status_im/utils/db.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.utils.db - (:require [clojure.string :as string] - [cljs.spec.alpha :as spec] - [status-im.js-dependencies :as dependencies] - [status-im.utils.ethereum.core :as ethereum])) - -(defn hex-string? [s] - (let [s' (if (string/starts-with? s "0x") - (subs s 2) - s)] - (boolean (re-matches #"(?i)[0-9a-f]+" s')))) - -(defn valid-length? [identity] - (let [length (count identity)] - (and - (hex-string? identity) - (or - (and (= 128 length) (not (string/includes? identity "0x"))) - (and (= 130 length) (string/starts-with? identity "0x")) - (and (= 132 length) (string/starts-with? identity "0x04")) - (ethereum/address? identity))))) - -(spec/def :global/not-empty-string (spec/and string? not-empty)) -(spec/def :global/public-key (spec/and :global/not-empty-string valid-length?)) -(spec/def :global/address ethereum/address?) \ No newline at end of file diff --git a/src/status_im/utils/dimensions.cljs b/src/status_im/utils/dimensions.cljs deleted file mode 100644 index d7f185da283..00000000000 --- a/src/status_im/utils/dimensions.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns status-im.utils.dimensions - (:require [re-frame.core :as re-frame] - [status-im.ui.components.react :as react])) - -(defn add-event-listener [] - (.addEventListener react/dimensions - "change" - #(re-frame/dispatch [:update-window-dimensions %]))) - -(defn window - ([] - (react/get-dimensions "window")) - ([m] - (-> m - (js->clj :keywordize-keys true) - :window))) diff --git a/src/status_im/utils/error_handler.cljs b/src/status_im/utils/error_handler.cljs deleted file mode 100644 index bcdabe19174..00000000000 --- a/src/status_im/utils/error_handler.cljs +++ /dev/null @@ -1,57 +0,0 @@ -(ns status-im.utils.error-handler - (:require [clojure.string :as string] - [status-im.utils.utils :as utils])) - -;; Error handling code based on https://gist.github.com/pesterhazy/e6846be1b6712a9038537022d131ce46 - -(defonce !handler-set (atom false)) - -(defn downgrade-reagent-errors! - "Downgrade reagent error to warning - Reagent uses console.error to notify the user that an exception occurs while - rendering. Unfortunately, react-native can only show one RedBox at a time and - discards all subsequent ones. This obscures the actual exception. By - downgrading the first screen to a warning, the second screen is actually shown - to the user." - [] - (when-not @!handler-set - (reset! !handler-set true) - (let [original-error (.-error js/console)] - (set! (.-error js/console) - (fn [& [head :as args]] - (if (and (string? head) (string/starts-with? head "Error rendering component")) - (apply (.-warn js/console) "Additional exception info:" args) - (apply original-error args))))))) - -(defn format-error [e] - (if (instance? js/Error e) - {:name (.-name e) :message (.-message e) :stack (.-stack e)} - {:message (pr-str e)})) - -(defn handle-error [e is-fatal] - (let [f (format-error e)] - (js/console.log (str "PRETTY PRINTED EXCEPTION" - "\n\n***\nNAME: " - (pr-str (:name f)) - "\nMESSAGE: " - (:message f) - "\n\n" - (:stack f) - "\n\n***")))) - -(defonce !error-handler-set? (atom false)) - -(defn register-exception-handler! - "Improve error messages printed to console. - When js/goog.DEBUG is false, show a popup with an error summary; else rely on default `red` screen." - [] - (downgrade-reagent-errors!) - (when-not @!error-handler-set? - (reset! !error-handler-set? true) - (let [orig-handler (some-> js/ErrorUtils .-getGlobalHandler (.call))] - (js/ErrorUtils.setGlobalHandler - (fn [e isFatal] - (handle-error e isFatal) - (if js/goog.DEBUG - (some-> orig-handler (.call nil e isFatal)) - (utils/show-popup "Error" (.-message e)))))))) \ No newline at end of file diff --git a/src/status_im/utils/ethereum/core.cljs b/src/status_im/utils/ethereum/core.cljs deleted file mode 100644 index 508bd7bcdf8..00000000000 --- a/src/status_im/utils/ethereum/core.cljs +++ /dev/null @@ -1,156 +0,0 @@ -(ns status-im.utils.ethereum.core - (:require [clojure.string :as string] - [status-im.js-dependencies :as dependencies] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.money :as money] - [taoensso.timbre :as log])) - -;; IDs standardized in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids - -(def chains - {:mainnet {:id 1 :name "Mainnet"} - :testnet {:id 3 :name "Ropsten"} - :rinkeby {:id 4 :name "Rinkeby"}}) - -(defn chain-id->chain-keyword [i] - (or (some #(when (= i (:id (val %))) (key %)) chains) - :custom)) - -(defn chain-keyword->chain-id [k] - (get-in chains [k :id])) - -(defn testnet? [id] - (contains? #{(chain-keyword->chain-id :testnet) (chain-keyword->chain-id :rinkeby)} id)) - -(defn network-with-upstream-rpc? [network] - (get-in network [:config :UpstreamConfig :Enabled])) - -(defn passphrase->words [s] - (when s - (-> (string/trim s) - (string/replace-all #"\s+" " ") - (string/split #" ")))) - -(defn words->passphrase [v] - (string/join " " v)) - -(def valid-word-counts #{12 15 18 21 24}) - -(defn valid-word-counts? [v] - (boolean (valid-word-counts (count v)))) - -(defn- valid-word? [s] - (re-matches #"^[A-z]+$" s)) - -(defn valid-words? [v] - (and - (valid-word-counts? v) - (every? valid-word? v))) - -(def hex-prefix "0x") - -(defn normalized-address [address] - (when address - (if (string/starts-with? address hex-prefix) - address - (str hex-prefix address)))) - -(defn address? [s] - (when s - (.isAddress dependencies/Web3.prototype s))) - -(defn network->chain-id [network] - (get-in network [:config :NetworkId])) - -(defn network->chain-keyword [network] - (chain-id->chain-keyword (network->chain-id network))) - -(defn network->chain-name [network] - (-> network - network->chain-keyword - name)) - -(defn sha3 [s] - (.sha3 dependencies/Web3.prototype (str s))) - -(defn hex->boolean [s] - (= s "0x0")) - -(defn boolean->hex [b] - (if b "0x0" "0x1")) - -(defn hex->int [s] - (js/parseInt s 16)) - -(defn int->hex [i] - (.toHex dependencies/Web3.prototype i)) - -(defn hex->bignumber [s] - (money/bignumber (if (= s hex-prefix) 0 s))) - -(defn zero-pad-64 [s] - (str (apply str (drop (count s) (repeat 64 "0"))) s)) - -(defn format-param [param] - (if (number? param) - (zero-pad-64 (str (hex->int param))) - (zero-pad-64 (subs param 2)))) - -(defn format-call-params [method-id & params] - (let [params (string/join (map format-param params))] - (str method-id params))) - -(defn- sig->method-id [signature] - (apply str (take 10 (sha3 signature)))) - -(defn call [web3 params cb] - (.call (.-eth web3) (clj->js params) cb)) - -(defn call-params [contract method-sig & params] - (let [data (apply format-call-params (sig->method-id method-sig) params)] - {:to contract :data data})) - -(defn send-transaction [web3 params cb] - (.sendTransaction (.-eth web3) (clj->js params) cb)) - -(def default-transaction-gas (money/bignumber 21000)) - -(defn gas-price [web3 cb] - (.getGasPrice (.-eth web3) cb)) - -(defn estimate-gas-web3 [web3 obj cb] - (.estimateGas (.-eth web3) obj cb)) - -(defn estimate-gas [symbol] - (if (tokens/ethereum? symbol) - default-transaction-gas - ;; TODO(jeluard) Rely on estimateGas call - (.times default-transaction-gas 5))) - -(defn handle-error [error] - (log/info (.stringify js/JSON error))) - -(defn get-block-number [web3 cb] - (.getBlockNumber (.-eth web3) - (fn [error result] - (if-not error - (cb result) - (handle-error error))))) - -(defn get-block-info [web3 number cb] - (.getBlock (.-eth web3) number (fn [error result] - (if-not error - (cb (js->clj result :keywordize-keys true)) - (handle-error error))))) - -(defn get-transaction [web3 number cb] - (.getTransaction (.-eth web3) number (fn [error result] - (if-not error - (cb (js->clj result :keywordize-keys true)) - (handle-error error))))) - -(defn get-transaction-receipt [web3 number cb] - (.getTransactionReceipt (.-eth web3) number (fn [error result] - (if-not error - (cb (js->clj result :keywordize-keys true)) - (handle-error error))))) diff --git a/src/status_im/utils/ethereum/eip681.cljs b/src/status_im/utils/ethereum/eip681.cljs deleted file mode 100644 index a597b0d0d6a..00000000000 --- a/src/status_im/utils/ethereum/eip681.cljs +++ /dev/null @@ -1,111 +0,0 @@ -(ns status-im.utils.ethereum.eip681 - "Utility function related to [EIP681](https://github.com/ethereum/EIPs/issues/681) - - This EIP standardize how ethereum payment request can be represented as URI (say to embed them in a QR code). - - e.g. ethereum:0x1234@1/transfer?to=0x5678&value=1e18&gas=5000" - (:require [clojure.set :as set] - [clojure.string :as string] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.money :as money])) - -(def scheme "ethereum") -(def scheme-separator ":") -(def chain-id-separator "@") -(def function-name-separator "/") -(def query-separator "?") -(def parameter-separator "&") -(def key-value-separator "=") - -(def uri-pattern (re-pattern (str scheme scheme-separator "([^" query-separator "]*)(?:\\" query-separator "(.*))?"))) -(def authority-path-pattern (re-pattern (str "^([^" chain-id-separator function-name-separator "]*)(?:" chain-id-separator "(\\d))?(?:" function-name-separator "(\\w*))?"))) -(def key-value-format (str "([^" parameter-separator key-value-separator "]+)")) -(def query-pattern (re-pattern (str key-value-format key-value-separator key-value-format))) - -(def valid-native-arguments #{:value :gas :gasPrice}) - -(defn- parse-query [s] - (into {} (for [[_ k v] (re-seq query-pattern (or s ""))] - [(keyword k) v]))) - -(defn- parse-native-arguments [m] - (select-keys m valid-native-arguments)) - -(defn- parse-arguments [function-name s] - (let [m (parse-query s) - arguments (parse-native-arguments m)] - (if function-name - (merge arguments {:function-name function-name} - (when (seq m) - {:function-arguments (apply dissoc m valid-native-arguments)})) - arguments))) - -;; TODO add ENS support - -(defn parse-uri - "Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address`. - Note that values are not decoded and you might need to rely on specific methods for some fields (parse-value, parse-number). - Invalid URI will be parsed as `nil`." - [s] - (when (string? s) - (let [[_ authority-path query] (re-find uri-pattern s)] - (when authority-path - (let [[_ address chain-id function-name] (re-find authority-path-pattern authority-path)] - (when (ethereum/address? address) - (when-let [arguments (parse-arguments function-name query)] - (merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-keyword->chain-id :mainnet))} - arguments)))))))) - -(defn parse-eth-value [s] - "Takes a map as returned by `parse-uri` and returns value as BigNumber" - (when (string? s) - (let [eth? (string/ends-with? s "ETH") - n (money/bignumber (string/replace s "ETH" ""))] - (if eth? (.times n 1e18) n)))) - -(defn extract-request-details [{:keys [value address chain-id function-name function-arguments]}] - "Return a map encapsulating request details (with keys `value`, `address` and `symbol`) from a parsed URI. - Supports ethereum and erc20 token." - (when address - (case function-name - nil - {:value (parse-eth-value value) - :symbol :ETH - :address address} - "transfer" - {:value (money/bignumber (:uint256 function-arguments)) - :symbol (:symbol (tokens/address->token (ethereum/chain-id->chain-keyword chain-id) address)) - :address (:address function-arguments)} - nil))) - -(defn- generate-query-string [m] - (string/join parameter-separator - (for [[k v] m] - (str (name k) key-value-separator v)))) - -(defn generate-uri - "Generate a EIP 681 URI based on `address` and a map (keywords / {bignumbers/strings} ) of extra properties. - No validation of address format is performed." - [address {:keys [chain-id function-name function-arguments] :as m}] - (when (ethereum/address? address) - (let [parameters (dissoc (into {} (filter second m)) :chain-id)] ;; filter nil values - (str scheme scheme-separator address - (when (and chain-id (not= chain-id (ethereum/chain-keyword->chain-id :mainnet))) - ;; Add chain-id if specified and is not main-net - (str chain-id-separator chain-id)) - (when-not (empty? parameters) - (if function-name - (str function-name-separator function-name query-separator - (let [native-parameters (dissoc parameters :function-name :function-arguments)] - (generate-query-string (merge function-arguments native-parameters)))) - (str query-separator (generate-query-string parameters)))))))) - -(defn generate-erc20-uri - "Generate a EIP 681 URI encapsulating ERC20 token transfer" - [address {:keys [symbol value chain-id] :as m}] - (when-let [token (tokens/symbol->token (if chain-id (ethereum/chain-id->chain-keyword chain-id) :mainnet) symbol)] - (generate-uri (:address token) - (merge (dissoc m :value :symbol) - {:function-name "transfer" - :function-arguments {:uint256 value :address address}})))) diff --git a/src/status_im/utils/ethereum/erc20.cljs b/src/status_im/utils/ethereum/erc20.cljs deleted file mode 100644 index 2a8bf579cbc..00000000000 --- a/src/status_im/utils/ethereum/erc20.cljs +++ /dev/null @@ -1,177 +0,0 @@ -(ns status-im.utils.ethereum.erc20 - " - Helper functions to interact with [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md) smart contract - - Example - - Contract: https://ropsten.etherscan.io/address/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5#readContract - Owner: https://ropsten.etherscan.io/token/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5?a=0xa7cfd581060ec66414790691681732db249502bd - - With a running node on Ropsten: - (let [web3 (:web3 @re-frame.db/app-db) - contract \"0x29b5f6efad2ad701952dfde9f29c960b5d6199c5\" - address \"0xa7cfd581060ec66414790691681732db249502bd\"] - (erc20/balance-of web3 contract address println)) - - => 29166666 - " - (:require [status-im.utils.ethereum.core :as ethereum] - [status-im.native-module.core :as status] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.constants :as constants] - [status-im.utils.datetime :as datetime] - [clojure.string :as string]) - (:refer-clojure :exclude [name symbol])) - -(defn name [web3 contract cb] - (ethereum/call web3 (ethereum/call-params contract "name()") cb)) - -(defn symbol [web3 contract cb] - (ethereum/call web3 (ethereum/call-params contract "symbol()") cb)) - -(defn decimals [web3 contract cb] - (ethereum/call web3 (ethereum/call-params contract "decimals()") cb)) - -(defn total-supply [web3 contract cb] - (ethereum/call web3 - (ethereum/call-params contract "totalSupply()") - #(cb %1 (ethereum/hex->bignumber %2)))) - -(defn balance-of [web3 contract address cb] - (ethereum/call web3 - (ethereum/call-params contract "balanceOf(address)" (ethereum/normalized-address address)) - #(cb %1 (ethereum/hex->bignumber %2)))) - -(defn transfer [web3 contract from address value params cb] - (ethereum/send-transaction web3 - (merge (ethereum/call-params contract "transfer(address,uint256)" (ethereum/normalized-address address) (ethereum/int->hex value)) - {:from from} - params) - #(cb %1 (ethereum/hex->boolean %2)))) - -(defn transfer-from [web3 contract from-address to-address value cb] - (ethereum/call web3 - (ethereum/call-params contract "transferFrom(address,address,uint256)" (ethereum/normalized-address from-address) (ethereum/normalized-address to-address) (ethereum/int->hex value)) - #(cb %1 (ethereum/hex->boolean %2)))) - -(defn approve [web3 contract address value cb] - (ethereum/call web3 - (ethereum/call-params contract "approve(address,uint256)" (ethereum/normalized-address address) (ethereum/int->hex value)) - #(cb %1 (ethereum/hex->boolean %2)))) - -(defn allowance [web3 contract owner-address spender-address cb] - (ethereum/call web3 - (ethereum/call-params contract "allowance(address,address)" (ethereum/normalized-address owner-address) (ethereum/normalized-address spender-address)) - #(cb %1 (ethereum/hex->bignumber %2)))) - -(defn- parse-json [s] - (try - (let [res (-> s - js/JSON.parse - (js->clj :keywordize-keys true))] - (if (= (:error res) "") - {:result true} - res)) - (catch :default e - {:error (.-message e)}))) - -(defn- add-padding [address] - (when address - (str "0x000000000000000000000000" (subs address 2)))) - -(defn- remove-padding [topic] - (if topic - (str "0x" (subs topic 26)))) - -(defn- parse-transaction-entries [current-block-number block-info chain direction transfers] - (into {} - (keep identity - (for [transfer transfers] - (if-let [token (->> transfer :address (tokens/address->token chain))] - (when-not (:nft? token) - [(:transactionHash transfer) - {:block (-> block-info :number str) - :hash (:transactionHash transfer) - :symbol (:symbol token) - :from (-> transfer :topics second remove-padding) - :to (-> transfer :topics last remove-padding) - :value (-> transfer :data ethereum/hex->bignumber) - :type direction - - :confirmations (str (- current-block-number (-> transfer :blockNumber ethereum/hex->int))) - - :gas-price nil - :nonce nil - :data nil - - :gas-limit nil - :timestamp (-> block-info :timestamp (* 1000) str) - - :gas-used nil - - ;; NOTE(goranjovic) - metadata on the type of token: contains name, symbol, decimas, address. - :token token - - ;; NOTE(goranjovic) - if an event has been emitted, we can say there was no error - :error? false - - ;; NOTE(goranjovic) - just a flag we need when we merge this entry with the existing entry in - ;; the app, e.g. transaction info with gas details, or a previous transfer entry with old - ;; confirmations count. - :transfer true}])))))) - -(defn add-block-info [web3 current-block-number chain direction result success-fn] - (let [transfers-by-block (group-by :blockNumber result)] - (doseq [[block-number transfers] transfers-by-block] - (ethereum/get-block-info web3 (ethereum/hex->int block-number) - (fn [block-info] - (success-fn (parse-transaction-entries current-block-number - block-info - chain - direction - transfers))))))) - -(defn- response-handler [web3 current-block-number chain direction error-fn success-fn] - (fn handle-response - ([response] - (let [{:keys [error result]} (parse-json response)] - (handle-response error result))) - ([error result] - (if error - (error-fn error) - (add-block-info web3 current-block-number chain direction result success-fn))))) - -;; -;; Here we are querying event logs for Transfer events. -;; -;; The parameters are as follows: -;; - address - token smart contract address -;; - fromBlock - we need to specify it, since default is latest -;; - topics[0] - hash code of the Transfer event signature -;; - topics[1] - address of token sender with leading zeroes padding up to 32 bytes -;; - topics[2] - address of token sender with leading zeroes padding up to 32 bytes -;; - -(defn get-token-transfer-logs - ;; NOTE(goranjovic): here we use direct JSON-RPC calls to get event logs because of web3 event issues with infura - ;; we still use web3 to get other data, such as block info - [web3 current-block-number chain contracts direction address cb] - (let [[from to] (if (= :inbound direction) - [nil (ethereum/normalized-address address)] - [(ethereum/normalized-address address) nil]) - args {:jsonrpc "2.0" - :id 2 - :method constants/web3-get-logs - :params [{:address (map string/lower-case contracts) - :fromBlock "0x0" - :topics [constants/event-transfer-hash - (add-padding from) - (add-padding to)]}]} - payload (.stringify js/JSON (clj->js args))] - (status/call-web3-private payload - (response-handler web3 current-block-number chain direction ethereum/handle-error cb)))) - -(defn get-token-transactions - [web3 chain contracts direction address cb] - (ethereum/get-block-number web3 - #(get-token-transfer-logs web3 % chain contracts direction address cb))) diff --git a/src/status_im/utils/ethereum/erc721.cljs b/src/status_im/utils/ethereum/erc721.cljs deleted file mode 100644 index bd8a1ef1e85..00000000000 --- a/src/status_im/utils/ethereum/erc721.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.utils.ethereum.erc721 - " - Helper functions to interact with [ERC721](https://eips.ethereum.org/EIPS/eip-721) smart contract - " - (:require [status-im.utils.ethereum.core :as ethereum])) - -(defn token-of-owner-by-index [web3 contract address index cb] - (ethereum/call web3 - (ethereum/call-params - contract - "tokenOfOwnerByIndex(address,uint256)" - (ethereum/normalized-address address) - (ethereum/int->hex index)) - #(cb %1 (ethereum/hex->bignumber %2)))) diff --git a/src/status_im/utils/ethereum/macros.clj b/src/status_im/utils/ethereum/macros.clj deleted file mode 100644 index 24cebaf5e75..00000000000 --- a/src/status_im/utils/ethereum/macros.clj +++ /dev/null @@ -1,21 +0,0 @@ -(ns status-im.utils.ethereum.macros - (:require [clojure.string :as string] - [clojure.java.io :as io])) - -(defn icon-path - [network symbol] - (let [s (str "./resources/images/tokens/" (name network) "/" (name symbol) ".png")] - (if (.exists (io/file s)) - `(js/require ~s) - `(js/require "./resources/images/tokens/default.png")))) - -(defn- token->icon [network {:keys [icon symbol]}] - ;; Tokens can define their own icons. - ;; If not try to make one using a local image as resource, if it does not exist fallback to default. - (or icon (icon-path network symbol))) - -(defmacro resolve-icons - "In react-native arguments to require must be static strings. - Resolve all icons at compilation time so no variable is used." - [network tokens] - (mapv #(assoc-in % [:icon :source] (token->icon network %)) tokens)) \ No newline at end of file diff --git a/src/status_im/utils/ethereum/tokens.cljs b/src/status_im/utils/ethereum/tokens.cljs deleted file mode 100644 index 2d291a89c6c..00000000000 --- a/src/status_im/utils/ethereum/tokens.cljs +++ /dev/null @@ -1,465 +0,0 @@ -(ns status-im.utils.ethereum.tokens - (:require-macros [status-im.utils.ethereum.macros :refer [resolve-icons]]) - (:require [clojure.string :as string] - [status-im.utils.config :as config])) - -(defn- asset-border [color] - {:border-color color :border-width 1 :border-radius 32}) - -(def ethereum {:name "Ether" - :symbol :ETH - :decimals 18 - :icon {:source (js/require "./resources/images/assets/ethereum.png") - ;; TODO(goranjovic) find a better place to set UI info - ;; like colors. Removed the reference to component.styles to - ;; avoid circular dependency between namespaces. - :style (asset-border "#628fe333")}}) - -(defn ethereum? [k] - (= k (:symbol ethereum))) - -;; symbol are used as global identifier (per network) so they must be unique - -(def all - {:mainnet - (resolve-icons :mainnet - [{:symbol :DAI - :name "DAI" - :address "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" - :decimals 18} - {:symbol :EOS - :name "EOS" - :address "0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0" - :decimals 18} - {:symbol :OMG - :name "OMGToken" - :address "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" - :decimals 18} - {:symbol :PPT - :name "Populous Platform" - :address "0xd4fa1460f537bb9085d22c7bccb5dd450ef28e3a" - :decimals 8} - {:symbol :REP - :name "Reputation" - :address "0x1985365e9f78359a9B6AD760e32412f4a445E862" - :decimals 18} - {:symbol :POWR - :name "PowerLedger" - :address "0x595832f8fc6bf59c85c527fec3740a1b7a361269" - :decimals 6} - {:symbol :PAY - :name "TenX Pay Token" - :address "0xB97048628DB6B661D4C2aA833e95Dbe1A905B280" - :decimals 18} - {:symbol :VRS - :name "VEROS" - :address "0xedbaf3c5100302dcdda53269322f3730b1f0416d" - :decimals 5} - {:symbol :GNT - :name "Golem Network Token" - :address "0xa74476443119A942dE498590Fe1f2454d7D4aC0d" - :decimals 18} - {:symbol :SALT - :name "Salt" - :address "0x4156D3342D5c385a87D264F90653733592000581" - :decimals 8} - {:symbol :BNB - :name "BNB" - :address "0xB8c77482e45F1F44dE1745F52C74426C631bDD52" - :decimals 18} - {:symbol :BAT - :name "Basic Attention Token" - :address "0x0d8775f648430679a709e98d2b0cb6250d2887ef" - :decimals 18} - {:symbol :KNC - :name "Kyber Network Crystal" - :address "0xdd974d5c2e2928dea5f71b9825b8b646686bd200" - :decimals 18} - {:symbol :DGD - :name "Digix DAO" - :address "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a" - :decimals 9} - {:symbol :AE - :name "Aeternity" - :address "0x5ca9a71b1d01849c0a95490cc00559717fcf0d1d" - :decimals 18} - {:symbol :TRX - :name "Tronix" - :address "0xf230b790e05390fc8295f4d3f60332c93bed42e2" - :decimals 6} - {:symbol :BQX - :name "Bitquence" - :address "0x5af2be193a6abca9c8817001f45744777db30756" - :decimals 8} - {:symbol :RDN - :name "Raiden Token" - :address "0x255aa6df07540cb5d3d297f0d0d4d84cb52bc8e6" - :decimals 18} - {:symbol :SNT - :name "Status Network" - :address "0x744d70fdbe2ba4cf95131626614a1763df805b9e" - :decimals 18} - {:symbol :SNGLS - :name "SingularDTV" - :address "0xaec2e87e0a235266d9c5adc9deb4b2e29b54d009" - :decimals 0} - {:symbol :GNO - :name "Gnosis Token" - :address "0x6810e776880c02933d47db1b9fc05908e5386b96" - :decimals 18} - {:symbol :STORJ - :name "StorjToken" - :address "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac" - :decimals 8} - {:symbol :ADX - :name "AdEx" - :address "0x4470bb87d77b963a013db939be332f927f2b992e" - :decimals 4} - {:symbol :FUN - :name "FunFair" - :address "0x419d0d8bdd9af5e606ae2232ed285aff190e711b" - :decimals 8} - {:symbol :CVC - :name "Civic" - :address "0x41e5560054824ea6b0732e656e3ad64e20e94e45" - :decimals 8} - {:symbol :ICN - :name "ICONOMI" - :address "0x888666CA69E0f178DED6D75b5726Cee99A87D698" - :decimals 18} - {:symbol :WTC - :name "Walton Token" - :address "0xb7cb1c96db6b22b0d3d9536e0108d062bd488f74" - :decimals 18} - {:symbol :BTM - :name "Bytom" - :address "0xcb97e65f07da24d46bcdd078ebebd7c6e6e3d750" - :decimals 8} - {:symbol :ZRX - :name "0x Protocol Token" - :address "0xe41d2489571d322189246dafa5ebde1f4699f498" - :decimals 18} - {:symbol :BNT - :name "Bancor Network Token" - :address "0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c" - :decimals 18} - {:symbol :MTL - :name "Metal" - :address "0xF433089366899D83a9f26A773D59ec7eCF30355e" - :decimals 8} - {:symbol :PPP - :name "PayPie" - :address "0xc42209aCcC14029c1012fB5680D95fBd6036E2a0" - :decimals 18} - {:symbol :LINK - :name "ChainLink Token" - :address "0x514910771af9ca656af840dff83e8264ecf986ca" - :decimals 18} - {:symbol :KIN - :name "Kin" - :address "0x818fc6c2ec5986bc6e2cbf00939d90556ab12ce5" - :decimals 18} - {:symbol :ANT - :name "Aragon Network Token" - :address "0x960b236A07cf122663c4303350609A66A7B288C0" - :decimals 18} - {:symbol :MGO - :name "MobileGo Token" - :address "0x40395044Ac3c0C57051906dA938B54BD6557F212" - :decimals 8} - {:symbol :MCO - :name "Monaco" - :address "0xb63b606ac810a52cca15e44bb630fd42d8d1d83d" - :decimals 8} - {:symbol :LRC - :name "loopring" - :address "0xEF68e7C694F40c8202821eDF525dE3782458639f" - :decimals 18} - {:symbol :ZSC - :name "Zeus Shield Coin" - :address "0x7A41e0517a5ecA4FdbC7FbebA4D4c47B9fF6DC63" - :decimals 18} - {:symbol :DATA - :name "DATAcoin" - :address "0x0cf0ee63788a0849fe5297f3407f701e122cc023" - :decimals 18} - {:symbol :RCN - :name "Ripio Credit Network Token" - :address "0xf970b8e36e23f7fc3fd752eea86f8be8d83375a6" - :decimals 18} - {:symbol :WINGS - :name "WINGS" - :address "0x667088b212ce3d06a1b553a7221E1fD19000d9aF" - :decimals 18} - {:symbol :EDG - :name "Edgeless" - :address "0x08711d3b02c8758f2fb3ab4e80228418a7f8e39c" - :decimals 0} - {:symbol :MLN - :name "Melon Token" - :address "0xBEB9eF514a379B997e0798FDcC901Ee474B6D9A1" - :decimals 18} - {:symbol :MDA - :name "Moeda Loyalty Points" - :address "0x51db5ad35c671a87207d88fc11d593ac0c8415bd" - :decimals 18} - {:symbol :PLR - :name "PILLAR" - :address "0xe3818504c1b32bf1557b16c238b2e01fd3149c17" - :decimals 18} - {:symbol :QRL - :name "QRL" - :address "0x697beac28b09e122c4332d163985e8a73121b97f" - :decimals 8} - {:symbol :MOD - :name "Modum Token" - :address "0x957c30ab0426e0c93cd8241e2c60392d08c6ac8e" - :decimals 0} - {:symbol :TAAS - :name "Token-as-a-Service" - :address "0xe7775a6e9bcf904eb39da2b68c5efb4f9360e08c" - :decimals 6} - {:symbol :GRID - :name "GRID Token" - :address "0x12b19d3e2ccc14da04fae33e63652ce469b3f2fd" - :decimals 12} - {:symbol :SAN - :name "SANtiment network token" - :address "0x7c5a0ce9267ed19b22f8cae653f198e3e8daf098" - :decimals 18} - {:symbol :SNM - :name "SONM Token" - :address "0x983f6d60db79ea8ca4eb9968c6aff8cfa04b3c63" - :decimals 18} - {:symbol :REQ - :name "Request Token" - :address "0x8f8221afbb33998d8584a2b05749ba73c37a938a" - :decimals 18} - {:symbol :SUB - :name "Substratum" - :address "0x12480e24eb5bec1a9d4369cab6a80cad3c0a377a" - :decimals 2} - {:symbol :MANA - :name "Decentraland" - :address "0x0f5d2fb29fb7d3cfee444a200298f468908cc942" - :decimals 18} - {:symbol :AST - :name "AirSwap Token" - :address "0x27054b13b1b798b345b591a4d22e6562d47ea75a" - :decimals 4} - {:symbol :R - :name "R token" - :address "0x48f775efbe4f5ece6e0df2f7b5932df56823b990" - :decimals 0} - {:symbol :1ST - :name "Firstblood" - :address "0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7" - :decimals 18} - {:symbol :CFI - :name "Cofoundit" - :address "0x12fef5e57bf45873cd9b62e9dbd7bfb99e32d73e" - :decimals 18} - {:symbol :ENG - :name "Enigma" - :address "0xf0ee6b27b759c9893ce4f094b49ad28fd15a23e4" - :decimals 8} - {:symbol :AMB - :name "Amber Token" - :address "0x4dc3643dbc642b72c158e7f3d2ff232df61cb6ce" - :decimals 18} - {:symbol :XPA - :name "XPlay Token" - :address "0x90528aeb3a2b736b780fd1b6c478bb7e1d643170" - :decimals 18} - {:symbol :OTN - :name "Open Trading Network" - :address "0x881ef48211982d01e2cb7092c915e647cd40d85c" - :decimals 18} - {:symbol :TRST - :name "Trustcoin" - :address "0xcb94be6f13a1182e4a4b6140cb7bf2025d28e41b" - :decimals 6} - {:symbol :TKN - :name "Monolith TKN" - :address "0xaaaf91d9b90df800df4f55c205fd6989c977e73a" - :decimals 8} - {:symbol :RHOC - :name "RHOC" - :address "0x168296bb09e24a88805cb9c33356536b980d3fc5" - :decimals 8} - {:symbol :TGT - :name "Target Coin" - :address "0xac3da587eac229c9896d919abc235ca4fd7f72c1" - :decimals 1} - {:symbol :EVX - :name "Everex" - :address "0xf3db5fa2c66b7af3eb0c0b782510816cbe4813b8" - :decimals 4} - {:symbol :ICOS - :name "ICOS" - :address "0x014b50466590340d41307cc54dcee990c8d58aa8" - :decimals 6} - {:symbol :DNT - :name "district0x Network Token" - :address "0x0abdace70d3790235af448c88547603b945604ea" - :decimals 18} - {:symbol :٨ - :name "Dentacoin" - :address "0x08d32b0da63e2C3bcF8019c9c5d849d7a9d791e6" - :decimals 0} - {:symbol :EDO - :name "Eidoo Token" - :address "0xced4e93198734ddaff8492d525bd258d49eb388e" - :decimals 18} - {:symbol :CSNO - :name "BitDice CSNO" - :address "0x29d75277ac7f0335b2165d0895e8725cbf658d73" - :decimals 8} - {:symbol :COB - :name "Cobinhood Token" - :address "0xb2f7eb1f2c37645be61d73953035360e768d81e6" - :decimals 18} - {:symbol :ENJ - :name "Enjin Coin" - :address "0xf629cbd94d3791c9250152bd8dfbdf380e2a3b9c" - :decimals 18} - {:symbol :AVT - :name "AVENTUS" - :address "0x0d88ed6e74bbfd96b831231638b66c05571e824f" - :decimals 18} - {:symbol :TIME - :name "Chronobank TIME" - :address "0x6531f133e6deebe7f2dce5a0441aa7ef330b4e53" - :decimals 8} - {:symbol :CND - :name "Cindicator Token" - :address "0xd4c435f5b09f855c3317c8524cb1f586e42795fa" - :decimals 18} - {:symbol :STX - :name "Stox" - :address "0x006BeA43Baa3f7A6f765F14f10A1a1b08334EF45" - :decimals 18} - {:symbol :XAUR - :name "Xaurum" - :address "0x4DF812F6064def1e5e029f1ca858777CC98D2D81" - :decimals 8} - {:symbol :VIB - :name "VIB" - :address "0x2c974b2d0ba1716e644c1fc59982a89ddd2ff724" - :decimals 18} - {:symbol :PRG - :name "ParagonCoin" - :address "0x7728dFEF5aBd468669EB7f9b48A7f70a501eD29D" - :decimals 6} - {:symbol :DPY - :name "Delphy Token" - :address "0x6c2adc2073994fb2ccc5032cc2906fa221e9b391" - :decimals 18} - {:symbol :CDT - :name "CoinDash Token" - :address "0x2fe6ab85ebbf7776fee46d191ee4cea322cecf51" - :decimals 18} - {:symbol :TNT - :name "Tierion Network Token" - :address "0x08f5a9235b08173b7569f83645d2c7fb55e8ccd8" - :decimals 8} - {:symbol :DRT - :name "Domraider" - :address "0x9af4f26941677c706cfecf6d3379ff01bb85d5ab" - :decimals 8} - {:symbol :SPANK - :name "SPANK" - :address "0x42d6622deCe394b54999Fbd73D108123806f6a18" - :decimals 18} - ;; NOTE(goranjovic) : the following three tokens are removed from the Manage Assets list - ;; and automatically removed from user's selection by a migration. However, we still need - ;; them listed here in order to correctly display any previous transactions the user had - ;; in their history prior to the upgrade. So, we're just hiding them, not actually deleting from the - ;; app. - {:symbol :CTR - :name "Centra" - :address "0x96A65609a7B84E8842732DEB08f56C3E21aC6f8a" - :decimals 18 - :hidden? true} - {:symbol :ROL - :name "DICE" - :address "0x2e071D2966Aa7D8dECB1005885bA1977D6038A65" - :decimals 16 - :hidden? true} - {:symbol :ATM - :name "Attention Token of Media" - :address "0x9B11EFcAAA1890f6eE52C6bB7CF8153aC5d74139" - :decimals 8 - :hidden? true} - {:symbol :CK - :nft? true - :name "CryptoKitties" - :address "0x06012c8cf97bead5deae237070f9587f8e7a266d"} - {:symbol :EMONA - :nft? true - :name "Etheremon" - :address "0xB2c0782ae4A299f7358758B2D15dA9bF29E1DD99"} - {:symbol :STRK - :nft? true - :name "CryptoStrikers" - :address "0xdcaad9fd9a74144d226dbf94ce6162ca9f09ed7e"}]) - :testnet - (resolve-icons :testnet - [{:name "Status Test Token" - :symbol :STT - :decimals 18 - ;;NOTE(goranjovic): intentionally checksummed for purposes of testing - :address "0xc55cF4B03948D7EBc8b9E8BAD92643703811d162"} - {:name "Handy Test Token" - :symbol :HND - :decimals 0 - :address "0xdee43a267e8726efd60c2e7d5b81552dcd4fa35c"} - {:name "Lucky XS Test" - :symbol :LXS - :decimals 2 - :address "0x703d7dc0bc8e314d65436adf985dda51e09ad43b"} - {:name "Adi Test Token" - :symbol :ADI - :decimals 7 - :address "0xe639e24346d646e927f323558e6e0031bfc93581"} - {:name "Wagner Test Token" - :symbol :WGN - :decimals 10 - :address "0x2e7cd05f437eb256f363417fd8f920e2efa77540"} - {:name "Modest Test Token" - :symbol :MDS - :decimals 18 - :address "0x57cc9b83730e6d22b224e9dc3e370967b44a2de0"}]) - - :rinkeby - (resolve-icons :rinkeby - [{:name "Moksha Coin" - :symbol :MOKSHA - :decimals 18 - :address "0x6ba7dc8dd10880ab83041e60c4ede52bb607864b"}]) - - :custom []}) - -(defn tokens-for [chain] - (get all chain)) - -(defn nfts-for [chain] - (filter :nft? (tokens-for chain))) - -(defn sorted-tokens-for [chain] - (->> (tokens-for chain) - (filter #(not (:hidden? %))) - (sort #(compare (string/lower-case (:name %1)) - (string/lower-case (:name %2)))))) - -(defn symbol->token [chain symbol] - (some #(when (= symbol (:symbol %)) %) (tokens-for chain))) - -(defn address->token [chain address] - (some #(when (= (string/lower-case address) - (string/lower-case (:address %))) %) (tokens-for chain))) - -(defn asset-for [chain symbol] - (if (= (:symbol ethereum) symbol) - ethereum - (symbol->token chain symbol))) diff --git a/src/status_im/utils/fs.cljs b/src/status_im/utils/fs.cljs deleted file mode 100644 index 10e17a57ec7..00000000000 --- a/src/status_im/utils/fs.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.utils.fs - (:require [status-im.react-native.js-dependencies :as rn-dependencies])) - -(defn move-file [src dst] - (.moveFile rn-dependencies/fs src dst)) - -(defn read-file [path encoding on-read on-error] - (-> (.readFile rn-dependencies/fs path encoding) - (.then on-read) - (.catch on-error))) - -(defn read-dir [path] - (.readDir rn-dependencies/fs path)) - -(defn mkdir [path] - (.mkdir rn-dependencies/fs path)) - -(defn unlink [path] - (.unlink rn-dependencies/fs path)) diff --git a/src/status_im/utils/gfycat/adjectives.cljs b/src/status_im/utils/gfycat/adjectives.cljs deleted file mode 100644 index 13185d8f0dd..00000000000 --- a/src/status_im/utils/gfycat/adjectives.cljs +++ /dev/null @@ -1,1398 +0,0 @@ -(ns status-im.utils.gfycat.adjectives) - -;Names are only deterministic as long as word lists do not change! -;If you change this list, please also update the tests at: -; -; - cljs/status_im/test/utils/gfycat/core.cljs - - -(def data - ["able" - "absolute" - "academic" - "acceptable" - "acclaimed" - "accomplished" - "accurate" - "aching" - "acidic" - "acrobatic" - "adorable" - "adventurous" - "babyish" - "back" - "baggy" - "bare" - "basic" - "beautiful" - "belated" - "beloved" - "bitter" - "calculating" - "calm" - "candid" - "canine" - "capital" - "carefree" - "careful" - "careless" - "caring" - "cautious" - "cavernous" - "celebrated" - "charming" - "damp" - "dangerous" - "dapper" - "daring" - "dark" - "darling" - "dazzling" - "deadly" - "deafening" - "dear" - "dearest" - "each" - "eager" - "early" - "earnest" - "easy" - "easygoing" - "ecstatic" - "edible" - "educated" - "elderly" - "fabulous" - "failing" - "faint" - "fair" - "faithful" - "familiar" - "famous" - "fancy" - "fantastic" - "far" - "faraway" - "farflung" - "faroff" - "gargantuan" - "gaseous" - "general" - "generous" - "gentle" - "genuine" - "giant" - "giddy" - "gigantic" - "hairy" - "half" - "handmade" - "handsome" - "handy" - "happy" - "happygolucky" - "hard" - "icky" - "icy" - "ideal" - "idealistic" - "identical" - "idle" - "idolized" - "ill" - "jaded" - "jagged" - "jampacked" - "kaleidoscopic" - "keen" - "lanky" - "large" - "last" - "lasting" - "lavish" - "lawful" - "madeup" - "magnificent" - "majestic" - "major" - "mammoth" - "marvelous" - "married" - "naive" - "narrow" - "nasty" - "natural" - "oblong" - "obvious" - "occasional" - "oily" - "palatable" - "pale" - "paltry" - "parallel" - "parched" - "partial" - "passionate" - "past" - "pastel" - "peaceful" - "peppery" - "perfect" - "perfumed" - "quaint" - "qualified" - "radiant" - "ragged" - "rapid" - "rare" - "rash" - "raw" - "recent" - "reckless" - "rectangular" - "safe" - "salty" - "same" - "sandy" - "sane" - "sarcastic" - "sardonic" - "satisfied" - "scaly" - "scarce" - "scary" - "scented" - "scholarly" - "scientific" - "scornful" - "scratchy" - "scrawny" - "second" - "secondary" - "secret" - "selfassured" - "selfreliant" - "sentimental" - "single" - "talkative" - "tall" - "tame" - "tan" - "tangible" - "tart" - "tasty" - "tattered" - "taut" - "tedious" - "teeming" - "ultimate" - "unaware" - "uncommon" - "unconscious" - "understated" - "unequaled" - "vacant" - "vague" - "vain" - "valid" - "wan" - "warlike" - "warm" - "warmhearted" - "warped" - "wary" - "wasteful" - "watchful" - "waterlogged" - "watery" - "wavy" - "weak" - "weird" - "yawning" - "yearly" - "zany" - "active" - "actual" - "adept" - "admirable" - "admired" - "adolescent" - "adorable" - "adored" - "advanced" - "affectionate" - "afraid" - "aged" - "aggravating" - "beneficial" - "best" - "better" - "bewitched" - "big" - "bighearted" - "biodegradable" - "bitesized" - "cheerful" - "cheery" - "chief" - "chilly" - "chubby" - "circular" - "classic" - "clean" - "clear" - "clearcut" - "clever" - "close" - "closed" - "decent" - "decimal" - "decisive" - "deep" - "defenseless" - "defensive" - "defiant" - "deficient" - "definite" - "definitive" - "delayed" - "delectable" - "delicious" - "elaborate" - "elastic" - "elated" - "electric" - "elegant" - "elementary" - "elliptical" - "fast" - "fatal" - "favorable" - "favorite" - "fearless" - "feisty" - "feline" - "few" - "fickle" - "gifted" - "giving" - "glamorous" - "glaring" - "glass" - "gleaming" - "gleeful" - "glistening" - "glittering" - "gross" - "hardtofind" - "harmful" - "harmless" - "harmonious" - "harsh" - "hasty" - "haunting" - "illustrious" - "imaginary" - "imaginative" - "immaculate" - "immaterial" - "immediate" - "immense" - "impassioned" - "jaunty" - "jealous" - "jittery" - "key" - "kind" - "leading" - "leafy" - "lean" - "left" - "legal" - "legitimate" - "light" - "massive" - "mature" - "meager" - "mealy" - "mean" - "measly" - "meaty" - "medical" - "mediocre" - "nautical" - "near" - "neat" - "necessary" - "needy" - "odd" - "oddball" - "offbeat" - "official" - "old" - "periodic" - "perky" - "personal" - "pertinent" - "pesky" - "pessimistic" - "petty" - "physical" - "piercing" - "pink" - "pitiful" - "plain" - "quarrelsome" - "quarterly" - "ready" - "real" - "realistic" - "reasonable" - "red" - "reflecting" - "regal" - "regular" - "separate" - "serene" - "serious" - "serpentine" - "several" - "severe" - "shabby" - "shadowy" - "shady" - "shallow" - "sharp" - "shimmering" - "shiny" - "shocked" - "shocking" - "shoddy" - "short" - "shortterm" - "showy" - "shrill" - "shy" - "silent" - "silky" - "tempting" - "tender" - "tense" - "tepid" - "terrific" - "testy" - "thankful" - "that" - "these" - "tremendous" - "uneven" - "unfinished" - "unfolded" - "uniform" - "unique" - "valuable" - "vapid" - "variable" - "vast" - "velvety" - "wealthy" - "weary" - "webbed" - "wee" - "weekly" - "weepy" - "weighty" - "welcome" - "welldocumented" - "yellow" - "zealous" - "aggressive" - "agile" - "agitated" - "agonizing" - "agreeable" - "ajar" - "alarmed" - "alarming" - "alert" - "alienated" - "alive" - "all" - "altruistic" - "bland" - "blank" - "blaring" - "bleak" - "blind" - "blissful" - "blond" - "blue" - "blushing" - "cloudy" - "clueless" - "clumsy" - "cluttered" - "coarse" - "cold" - "colorful" - "colorless" - "colossal" - "comfortable" - "common" - "compassionate" - "competent" - "complete" - "delightful" - "demanding" - "dense" - "dental" - "dependable" - "dependent" - "descriptive" - "deserted" - "detailed" - "determined" - "devoted" - "different" - "embellished" - "eminent" - "emotional" - "empty" - "enchanted" - "enchanting" - "energetic" - "enlightened" - "enormous" - "fine" - "finished" - "firm" - "first" - "firsthand" - "fitting" - "fixed" - "flaky" - "flamboyant" - "flashy" - "flat" - "flawless" - "flickering" - "gloomy" - "glorious" - "glossy" - "glum" - "golden" - "good" - "goodnatured" - "gorgeous" - "graceful" - "healthy" - "heartfelt" - "hearty" - "heavenly" - "heavy" - "hefty" - "helpful" - "humongous" - "impartial" - "impeccable" - "imperfect" - "imperturbable" - "important" - "impossible" - "impractical" - "impressionable" - "impressive" - "improbable" - "joint" - "jolly" - "jovial" - "juvenile" - "kindhearted" - "kindly" - "lighthearted" - "likable" - "likely" - "limited" - "limp" - "limping" - "linear" - "lined" - "liquid" - "medium" - "meek" - "mellow" - "melodic" - "memorable" - "menacing" - "merry" - "messy" - "metallic" - "mild" - "negative" - "neglected" - "negligible" - "neighboring" - "nervous" - "new" - "oldfashioned" - "only" - "open" - "optimal" - "optimistic" - "opulent" - "plaintive" - "plastic" - "playful" - "pleasant" - "pleased" - "pleasing" - "plump" - "plush" - "pointed" - "pointless" - "poised" - "polished" - "polite" - "political" - "pungent" - "queasy" - "querulous" - "reliable" - "relieved" - "remarkable" - "remorseful" - "remote" - "repentant" - "required" - "respectful" - "responsible" - "silly" - "silver" - "similar" - "simple" - "simplistic" - "sizzling" - "skeletal" - "skinny" - "sleepy" - "slight" - "slim" - "slimy" - "slippery" - "slow" - "slushy" - "small" - "smart" - "smoggy" - "smooth" - "smug" - "snappy" - "snarling" - "sneaky" - "sniveling" - "snoopy" - "thick" - "thin" - "third" - "thirsty" - "this" - "thorny" - "thorough" - "those" - "thoughtful" - "threadbare" - "united" - "unkempt" - "unknown" - "unlined" - "unnatural" - "unrealistic" - "venerated" - "vengeful" - "verifiable" - "vibrant" - "vicious" - "wellgroomed" - "wellinformed" - "welllit" - "wellmade" - "welloff" - "welltodo" - "wellworn" - "wet" - "which" - "whimsical" - "whirlwind" - "whispered" - "worse" - "writhing" - "yellowish" - "zesty" - "amazing" - "ambitious" - "ample" - "amused" - "amusing" - "anchored" - "ancient" - "angelic" - "angry" - "anguished" - "animated" - "annual" - "another" - "antique" - "bogus" - "boiling" - "bold" - "bony" - "boring" - "bossy" - "both" - "bouncy" - "bountiful" - "bowed" - "complex" - "complicated" - "composed" - "concerned" - "concrete" - "confused" - "conscious" - "considerate" - "constant" - "content" - "conventional" - "cooked" - "cool" - "cooperative" - "difficult" - "digital" - "diligent" - "dim" - "dimpled" - "dimwitted" - "direct" - "discrete" - "entire" - "envious" - "equal" - "equatorial" - "essential" - "esteemed" - "ethical" - "euphoric" - "flimsy" - "flippant" - "flowery" - "fluffy" - "fluid" - "flustered" - "focused" - "fond" - "foolhardy" - "foolish" - "forceful" - "forked" - "formal" - "forsaken" - "gracious" - "grand" - "grandiose" - "granular" - "grateful" - "grave" - "gray" - "great" - "greedy" - "green" - "hidden" - "high" - "highlevel" - "hilarious" - "hoarse" - "hollow" - "homely" - "inborn" - "incomparable" - "incompatible" - "incomplete" - "inconsequential" - "incredible" - "indelible" - "indolent" - "inexperienced" - "infamous" - "infantile" - "joyful" - "joyous" - "jubilant" - "klutzy" - "knobby" - "little" - "live" - "lively" - "livid" - "lone" - "long" - "milky" - "mindless" - "miniature" - "minor" - "minty" - "misguided" - "misty" - "mixed" - "next" - "nice" - "nifty" - "nimble" - "nippy" - "orange" - "orderly" - "ordinary" - "organic" - "ornate" - "ornery" - "poor" - "popular" - "portly" - "posh" - "positive" - "possible" - "potable" - "powerful" - "powerless" - "practical" - "precious" - "present" - "prestigious" - "questionable" - "quick" - "revolving" - "rewarding" - "rich" - "right" - "rigid" - "ringed" - "ripe" - "sociable" - "soft" - "soggy" - "solid" - "somber" - "some" - "sophisticated" - "sore" - "sorrowful" - "soulful" - "soupy" - "sour" - "spanish" - "sparkling" - "sparse" - "specific" - "spectacular" - "speedy" - "spherical" - "spicy" - "spiffy" - "spirited" - "spiteful" - "splendid" - "spotless" - "spotted" - "spry" - "thrifty" - "thunderous" - "tidy" - "tight" - "timely" - "tinted" - "tiny" - "tired" - "torn" - "total" - "unripe" - "unruly" - "unselfish" - "unsightly" - "unsteady" - "unsung" - "untidy" - "untimely" - "untried" - "victorious" - "vigilant" - "vigorous" - "villainous" - "violet" - "whole" - "whopping" - "wicked" - "wide" - "wideeyed" - "wiggly" - "wild" - "willing" - "wilted" - "winding" - "windy" - "young" - "zigzag" - "anxious" - "any" - "apprehensive" - "appropriate" - "apt" - "arctic" - "arid" - "aromatic" - "artistic" - "assured" - "astonishing" - "athletic" - "brave" - "breakable" - "brief" - "bright" - "brilliant" - "brisk" - "broken" - "bronze" - "brown" - "bruised" - "coordinated" - "corny" - "corrupt" - "costly" - "courageous" - "courteous" - "crafty" - "crazy" - "creamy" - "creative" - "creepy" - "crisp" - "dirty" - "disguised" - "distant" - "distant" - "distinct" - "distorted" - "dizzy" - "dopey" - "downright" - "dreary" - "even" - "evergreen" - "everlasting" - "every" - "evil" - "exalted" - "excellent" - "excitable" - "exemplary" - "exhausted" - "forthright" - "fortunate" - "fragrant" - "frail" - "frank" - "frayed" - "free" - "french" - "frequent" - "fresh" - "friendly" - "frightened" - "frightening" - "gregarious" - "grim" - "grimy" - "gripping" - "grizzled" - "grouchy" - "grounded" - "honest" - "honorable" - "honored" - "hopeful" - "hospitable" - "hot" - "huge" - "infatuated" - "inferior" - "infinite" - "informal" - "innocent" - "insecure" - "insignificant" - "insistent" - "instructive" - "insubstantial" - "judicious" - "juicy" - "jumbo" - "knotty" - "knowing" - "knowledgeable" - "longterm" - "loose" - "lopsided" - "lost" - "loud" - "lovable" - "lovely" - "loving" - "modern" - "modest" - "moist" - "monthly" - "monumental" - "moral" - "mortified" - "motionless" - "nocturnal" - "noisy" - "nonstop" - "normal" - "notable" - "noted" - "original" - "other" - "our" - "outgoing" - "outlandish" - "outlying" - "precious" - "pretty" - "previous" - "pricey" - "prickly" - "primary" - "prime" - "pristine" - "private" - "prize" - "probable" - "productive" - "profitable" - "quickwitted" - "quiet" - "quintessential" - "roasted" - "robust" - "rosy" - "rotating" - "rotten" - "rough" - "round" - "rowdy" - "square" - "squeaky" - "squiggly" - "stable" - "staid" - "stained" - "stale" - "standard" - "starchy" - "stark" - "starry" - "steel" - "steep" - "sticky" - "stiff" - "stimulating" - "stingy" - "stormy" - "strange" - "strict" - "strident" - "striking" - "striped" - "strong" - "studious" - "stunning" - "tough" - "tragic" - "trained" - "treasured" - "tremendous" - "triangular" - "tricky" - "trifling" - "trim" - "untrue" - "unused" - "unusual" - "unwelcome" - "unwieldy" - "unwilling" - "unwitting" - "unwritten" - "upbeat" - "virtual" - "virtuous" - "visible" - "winged" - "wiry" - "wise" - "witty" - "wobbly" - "woeful" - "wonderful" - "wooden" - "woozy" - "wordy" - "worldly" - "worn" - "youthful" - "attached" - "attentive" - "attractive" - "austere" - "authentic" - "authorized" - "automatic" - "avaricious" - "average" - "aware" - "awesome" - "awful" - "awkward" - "bubbly" - "bulky" - "bumpy" - "buoyant" - "burdensome" - "burly" - "bustling" - "busy" - "buttery" - "buzzing" - "critical" - "crooked" - "crowded" - "crushing" - "cuddly" - "cultivated" - "cultured" - "cumbersome" - "curly" - "curvy" - "cute" - "cylindrical" - "doting" - "double" - "downright" - "drab" - "drafty" - "dramatic" - "dry" - "dual" - "dutiful" - "excited" - "exciting" - "exotic" - "expensive" - "experienced" - "expert" - "extralarge" - "extraneous" - "extrasmall" - "extroverted" - "frilly" - "frivolous" - "frizzy" - "front" - "frosty" - "frozen" - "frugal" - "fruitful" - "full" - "fumbling" - "functional" - "funny" - "fussy" - "fuzzy" - "growing" - "growling" - "grown" - "grubby" - "grumpy" - "guilty" - "gullible" - "gummy" - "humble" - "humming" - "hungry" - "husky" - "intelligent" - "intent" - "intentional" - "interesting" - "internal" - "international" - "intrepid" - "ironclad" - "irresponsible" - "itchy" - "jumpy" - "junior" - "known" - "kooky" - "low" - "loyal" - "lucky" - "lumbering" - "luminous" - "lumpy" - "lustrous" - "luxurious" - "mountainous" - "muddy" - "muffled" - "mundane" - "murky" - "mushy" - "musty" - "mysterious" - "noteworthy" - "novel" - "noxious" - "numb" - "nutritious" - "nutty" - "outrageous" - "outstanding" - "oval" - "overcooked" - "overdue" - "overjoyed" - "profuse" - "proper" - "proud" - "prudent" - "punctual" - "puny" - "pure" - "purple" - "pushy" - "puzzled" - "puzzling" - "quirky" - "quixotic" - "quizzical" - "royal" - "rubbery" - "ruddy" - "rundown" - "runny" - "rural" - "rusty" - "stupendous" - "sturdy" - "stylish" - "subdued" - "substantial" - "subtle" - "suburban" - "sudden" - "sugary" - "sunny" - "super" - "superb" - "superficial" - "superior" - "supportive" - "surefooted" - "surprised" - "suspicious" - "svelte" - "sweaty" - "sweet" - "sweltering" - "swift" - "sympathetic" - "trivial" - "troubled" - "trusting" - "trustworthy" - "trusty" - "truthful" - "tubby" - "turbulent" - "twin" - "upright" - "upset" - "urban" - "usable" - "used" - "useful" - "useless" - "utilized" - "utter" - "vital" - "vivacious" - "vivid" - "voluminous" - "worst" - "worthwhile" - "worthy" - "wrathful" - "wry" - "yummy" - "true" - "aliceblue" - "aqua" - "aquamarine" - "azure" - "beige" - "bisque" - "blanchedalmond" - "blue" - "blueviolet" - "brown" - "burlywood" - "cadetblue" - "chartreuse" - "chocolate" - "coral" - "cornflowerblue" - "cornsilk" - "crimson" - "cyan" - "darkblue" - "darkcyan" - "darkgoldenrod" - "darkgray" - "darkgreen" - "darkgrey" - "darkkhaki" - "darkmagenta" - "darkolivegreen" - "darkorange" - "darkorchid" - "darkred" - "darksalmon" - "darkseagreen" - "darkslateblue" - "darkslategray" - "darkslategrey" - "darkturquoise" - "darkviolet" - "deeppink" - "deepskyblue" - "dimgray" - "dimgrey" - "dodgerblue" - "firebrick" - "floralwhite" - "forestgreen" - "fractal" - "fuchsia" - "gainsboro" - "ghostwhite" - "gold" - "goldenrod" - "gray" - "green" - "greenyellow" - "honeydew" - "hotpink" - "indianred" - "indigo" - "ivory" - "khaki" - "lavender" - "lavenderblush" - "lawngreen" - "lemonchiffon" - "lightblue" - "lightcoral" - "lightcyan" - "lightgoldenrod" - "lightgoldenrodyellow" - "lightgray" - "lightgreen" - "lightgrey" - "lightpink" - "lightsalmon" - "lightseagreen" - "lightskyblue" - "lightslateblue" - "lightslategray" - "lightsteelblue" - "lightyellow" - "lime" - "limegreen" - "linen" - "magenta" - "maroon" - "mediumaquamarine" - "mediumblue" - "mediumforestgreen" - "mediumgoldenrod" - "mediumorchid" - "mediumpurple" - "mediumseagreen" - "mediumslateblue" - "mediumspringgreen" - "mediumturquoise" - "mediumvioletred" - "midnightblue" - "mintcream" - "mistyrose" - "moccasin" - "navajowhite" - "navy" - "navyblue" - "oldlace" - "olive" - "olivedrab" - "opaque" - "orange" - "orangered" - "orchid" - "palegoldenrod" - "palegreen" - "paleturquoise" - "palevioletred" - "papayawhip" - "peachpuff" - "peru" - "pink" - "plum" - "powderblue" - "purple" - "red" - "rosybrown" - "royalblue" - "saddlebrown" - "salmon" - "sandybrown" - "seagreen" - "seashell" - "sienna" - "silver" - "skyblue" - "slateblue" - "slategray" - "slategrey" - "snow" - "springgreen" - "steelblue" - "tan" - "teal" - "thistle" - "tomato" - "transparent" - "turquoise" - "violet" - "violetred" - "wheat" - "whitesmoke" - "yellow" - "yellowgreen"]) \ No newline at end of file diff --git a/src/status_im/utils/gfycat/animals.cljs b/src/status_im/utils/gfycat/animals.cljs deleted file mode 100644 index 0550c4f749b..00000000000 --- a/src/status_im/utils/gfycat/animals.cljs +++ /dev/null @@ -1,1713 +0,0 @@ -(ns status-im.utils.gfycat.animals) - -;Names are only deterministic as long as word lists do not change! -;If you change this list, please also update the tests at: -; -; - cljs/status_im/test/utils/gfycat/core.cljs - - -(def data - ["aardvark" - "aardwolf" - "abalone" - "abyssiniancat" - "abyssiniangroundhornbill" - "acaciarat" - "achillestang" - "acornbarnacle" - "acornweevil" - "acornwoodpecker" - "acouchi" - "adamsstaghornedbeetle" - "addax" - "adder" - "adeliepenguin" - "admiralbutterfly" - "adouri" - "aegeancat" - "affenpinscher" - "hound" - "augurbuzzard" - "bushviper" - "civet" - "clawedfrog" - "elephant" - "fisheagle" - "goldencat" - "groundhornbill" - "harrierhawk" - "hornbill" - "jacana" - "molesnake" - "paradiseflycatcher" - "piedkingfisher" - "porcupine" - "rockpython" - "wildcat" - "wilddog" - "agama" - "agouti" - "aidi" - "airedale" - "airedaleterrier" - "akitainu" - "mapturtle" - "jingle" - "husky" - "kleekai" - "malamute" - "albacoretuna" - "albatross" - "albertosaurus" - "albino" - "aldabratortoise" - "allensbigearedbat" - "alleycat" - "alligator" - "alligatorgar" - "alligatorsnappingturtle" - "allosaurus" - "alpaca" - "alpinegoat" - "alpineroadguidetigerbeetle" - "altiplanochinchillamouse" - "amazondolphin" - "amazonparrot" - "amazontreeboa" - "amberpenshell" - "ambushbug" - "alligator" - "avocet" - "badger" - "bittern" - "vulture" - "bobtail" - "bulldog" - "cicada" - "crayfish" - "creamdraft" - "crocodile" - "crow" - "curl" - "goldfinch" - "indianhorse" - "kestrel" - "lobster" - "marten" - "painthorse" - "quarterhorse" - "ratsnake" - "redsquirrel" - "riverotter" - "robin" - "saddlebred" - "shorthair" - "toad" - "warmblood" - "wigeon" - "wirehair" - "amethystgemclam" - "amethystinepython" - "amethystsunbird" - "ammonite" - "amoeba" - "amphibian" - "amphiuma" - "amurminnow" - "amurratsnake" - "amurstarfish" - "anaconda" - "anchovy" - "andalusianhorse" - "andeancat" - "andeancondor" - "anemone" - "anemonecrab" - "anemoneshrimp" - "angelfish" - "angelwingmussel" - "anglerfish" - "angora" - "angwantibo" - "anhinga" - "ankole" - "ankolewatusi" - "annashummingbird" - "annelid" - "annelida" - "anole" - "anophelesmosquito" - "ant" - "antarcticfurseal" - "antarcticgiantpetrel" - "antbear" - "anteater" - "antelope" - "antelopegroundsquirrel" - "antipodesgreenparakeet" - "antlion" - "anura" - "aoudad" - "apatosaur" - "ape" - "aphid" - "apisdorsatalaboriosa" - "aplomadofalcon" - "appaloosa" - "aquaticleech" - "arabianhorse" - "arabianoryx" - "arabianwildcat" - "aracari" - "arachnid" - "arawana" - "archaeocete" - "archaeopteryx" - "archerfish" - "arcticduck" - "arcticfox" - "arctichare" - "arcticseal" - "arcticwolf" - "argali" - "argentinehornedfrog" - "argentineruddyduck" - "argusfish" - "arieltoucan" - "arizonaalligatorlizard" - "arkshell" - "armadillo" - "armedcrab" - "armednylonshrimp" - "armyant" - "armyworm" - "arrowana" - "arrowcrab" - "arrowworm" - "arthropods" - "aruanas" - "constablebutterfly" - "damselfly" - "elephant" - "lion" - "piedstarling" - "porcupine" - "smallclawedotter" - "trumpetfish" - "waterbuffalo" - "greaterfreshwaterclam" - "lesserfreshwaterclam" - "mouflon" - "asp" - "assassinbug" - "astarte" - "astrangiacoral" - "atlanticblackgoby" - "atlanticbluetang" - "atlanticridleyturtle" - "atlanticsharpnosepuffer" - "atlanticspadefish" - "atlasmoth" - "attwatersprairiechicken" - "auk" - "auklet" - "aurochs" - "australiancattledog" - "australiancurlew" - "australianfreshwatercrocodile" - "australianfurseal" - "australiankelpie" - "australiankestrel" - "australianshelduck" - "australiansilkyterrier" - "austrianpinscher" - "avians" - "avocet" - "axisdeer" - "axolotl" - "ayeaye" - "aztecant" - "azurevase" - "azurevasesponge" - "azurewingedmagpie" - "babirusa" - "baboon" - "backswimmer" - "bactrian" - "badger" - "bagworm" - "baiji" - "baldeagle" - "baleenwhale" - "balloonfish" - "ballpython" - "bandicoot" - "bangeltiger" - "bantamrooster" - "banteng" - "barasinga" - "barasingha" - "barb" - "barbet" - "barebirdbat" - "barnacle" - "barnowl" - "barnswallow" - "barracuda" - "basenji" - "basil" - "basilisk" - "bass" - "bassethound" - "bat" - "bats" - "beagle" - "bear" - "beardedcollie" - "beardeddragon" - "beauceron" - "beaver" - "bedbug" - "bedlingtonterrier" - "bee" - "beetle" - "bellfrog" - "bellsnake" - "belugawhale" - "bengaltiger" - "bergerpicard" - "bernesemountaindog" - "betafish" - "bettong" - "bichonfrise" - "bighorn" - "bighornedsheep" - "bighornsheep" - "bigmouthbass" - "bilby" - "binturong" - "bird" - "birdofparadise" - "bison" - "bittern" - "blackbear" - "blackbird" - "blackbuck" - "blackfish" - "blackfly" - "blackfootedferret" - "blacklab" - "blacklemur" - "blackmamba" - "blacknorwegianelkhound" - "blackpanther" - "blackrhino" - "blackrussianterrier" - "blackwidowspider" - "blesbok" - "blobfish" - "blowfish" - "blueandgoldmackaw" - "bluebird" - "bluebottle" - "bluebottlejellyfish" - "bluefintuna" - "bluefish" - "bluegill" - "bluejay" - "blueshark" - "bluet" - "bluetickcoonhound" - "bluetonguelizard" - "bluewhale" - "boa" - "boaconstrictor" - "boar" - "bobcat" - "bobolink" - "bobwhite" - "boilweevil" - "bongo" - "bonobo" - "booby" - "bordercollie" - "borderterrier" - "borer" - "borzoi" - "boto" - "boubou" - "boutu" - "bovine" - "brahmanbull" - "brahmancow" - "brant" - "bream" - "brocketdeer" - "bronco" - "brontosaurus" - "brownbear" - "brownbutterfly" - "bubblefish" - "buck" - "buckeyebutterfly" - "budgie" - "bufeo" - "buffalo" - "bufflehead" - "bug" - "bull" - "bullfrog" - "bullmastiff" - "bumblebee" - "bunny" - "bunting" - "burro" - "bushbaby" - "bushsqueaker" - "bustard" - "butterfly" - "buzzard" - "caecilian" - "caiman" - "caimanlizard" - "calf" - "camel" - "canadagoose" - "canary" - "canine" - "canvasback" - "capeghostfrog" - "capybara" - "caracal" - "cardinal" - "caribou" - "carp" - "carpenterant" - "cassowary" - "cat" - "catbird" - "caterpillar" - "catfish" - "cats" - "cattle" - "caudata" - "cavy" - "centipede" - "cero" - "chafer" - "chameleon" - "chamois" - "chanticleer" - "cheetah" - "chevrotain" - "chick" - "chickadee" - "chicken" - "chihuahua" - "chimneyswift" - "chimpanzee" - "chinchilla" - "chinesecrocodilelizard" - "chipmunk" - "chital" - "chrysalis" - "chrysomelid" - "chuckwalla" - "chupacabra" - "cicada" - "cirriped" - "civet" - "clam" - "cleanerwrasse" - "clingfish" - "clownanemonefish" - "clumber" - "coati" - "cob" - "cobra" - "cockerspaniel" - "cod" - "coelacanth" - "collardlizard" - "collie" - "colt" - "comet" - "commabutterfly" - "commongonolek" - "conch" - "condor" - "coney" - "conure" - "cony" - "coot" - "cooter" - "copepod" - "copperbutterfly" - "copperhead" - "coqui" - "coral" - "cormorant" - "cornsnake" - "corydorascatfish" - "cottonmouth" - "cottontail" - "cow" - "cowbird" - "cowrie" - "coyote" - "coypu" - "crab" - "crane" - "cranefly" - "crayfish" - "creature" - "cricket" - "crocodile" - "crocodileskink" - "crossbill" - "crow" - "crownofthornsstarfish" - "crustacean" - "cub" - "cuckoo" - "cur" - "curassow" - "curlew" - "cuscus" - "cusimanse" - "cuttlefish" - "cutworm" - "cygnet" - "dachshund" - "dalmatian" - "damselfly" - "danishswedishfarmdog" - "darklingbeetle" - "dartfrog" - "darwinsfox" - "dassie" - "dassierat" - "davidstiger" - "deer" - "deermouse" - "degu" - "degus" - "deinonychus" - "desertpupfish" - "devilfish" - "deviltasmanian" - "diamondbackrattlesnake" - "dikdik" - "dikkops" - "dingo" - "dinosaur" - "diplodocus" - "dipper" - "discus" - "dobermanpinscher" - "doctorfish" - "dodo" - "dodobird" - "doe" - "dog" - "dogfish" - "dolphin" - "donkey" - "dorado" - "dore" - "dorking" - "dormouse" - "dotterel" - "dove" - "dowitcher" - "drafthorse" - "dragon" - "dragonfly" - "drake" - "drever" - "dromaeosaur" - "dromedary" - "drongo" - "duck" - "duckbillcat" - "duckbillplatypus" - "duckling" - "dugong" - "duiker" - "dunlin" - "dunnart" - "dutchshepherddog" - "dutchsmoushond" - "eagle" - "earthworm" - "earwig" - "easternglasslizard" - "easternnewt" - "echidna" - "eel" - "eelelephant" - "eeve" - "eft" - "egg" - "egret" - "eider" - "eidolonhelvum" - "ekaltadeta" - "eland" - "electriceel" - "elephant" - "elephantbeetle" - "elephantseal" - "elk" - "elkhound" - "elver" - "emeraldtreeskink" - "emperorpenguin" - "emperorshrimp" - "emu" - "englishpointer" - "englishsetter" - "equestrian" - "equine" - "erin" - "ermine" - "erne" - "eskimodog" - "esok" - "estuarinecrocodile" - "ethiopianwolf" - "europeanfiresalamander" - "europeanpolecat" - "ewe" - "eyas" - "eyelashpitviper" - "eyra" - "fairybluebird" - "fairyfly" - "falcon" - "fallowdeer" - "fantail" - "fanworms" - "fattaileddunnart" - "fawn" - "feline" - "fennecfox" - "ferret" - "fiddlercrab" - "fieldmouse" - "fieldspaniel" - "finch" - "finnishspitz" - "finwhale" - "fireant" - "firebelliedtoad" - "firecrest" - "firefly" - "fish" - "fishingcat" - "flamingo" - "flatcoatretriever" - "flatfish" - "flea" - "flee" - "flicker" - "flickertailsquirrel" - "flies" - "flounder" - "fluke" - "fly" - "flycatcher" - "flyingfish" - "flyingfox" - "flyinglemur" - "flyingsquirrel" - "foal" - "fossa" - "fowl" - "fox" - "foxhound" - "foxterrier" - "frenchbulldog" - "freshwatereel" - "frigatebird" - "frilledlizard" - "frillneckedlizard" - "fritillarybutterfly" - "frog" - "frogmouth" - "fruitbat" - "fruitfly" - "fugu" - "fulmar" - "funnelweaverspider" - "furseal" - "gadwall" - "galago" - "galah" - "galapagosalbatross" - "galapagosdove" - "galapagoshawk" - "galapagosmockingbird" - "galapagospenguin" - "galapagossealion" - "galapagostortoise" - "gallinule" - "gallowaycow" - "gander" - "gangesdolphin" - "gannet" - "gar" - "gardensnake" - "garpike" - "gartersnake" - "gaur" - "gavial" - "gazelle" - "gecko" - "geese" - "gelada" - "gelding" - "gemsbok" - "gemsbuck" - "genet" - "gentoopenguin" - "gerbil" - "gerenuk" - "germanpinscher" - "germanshepherd" - "germanshorthairedpointer" - "germanspaniel" - "germanspitz" - "germanwirehairedpointer" - "gharial" - "ghostshrimp" - "giantschnauzer" - "gibbon" - "gilamonster" - "giraffe" - "glassfrog" - "globefish" - "glowworm" - "gnat" - "gnatcatcher" - "gnu" - "goa" - "goat" - "godwit" - "goitered" - "goldeneye" - "goldenmantledgroundsquirrel" - "goldenretriever" - "goldfinch" - "goldfish" - "gonolek" - "goose" - "goosefish" - "gopher" - "goral" - "gordonsetter" - "gorilla" - "goshawk" - "gosling" - "gossamerwingedbutterfly" - "gourami" - "grackle" - "grasshopper" - "grassspider" - "grayfox" - "grayling" - "grayreefshark" - "graysquirrel" - "graywolf" - "greatargus" - "greatdane" - "greathornedowl" - "greatwhiteshark" - "grebe" - "greendarnerdragonfly" - "greyhounddog" - "grison" - "grizzlybear" - "grosbeak" - "groundbeetle" - "groundhog" - "grouper" - "grouse" - "grub" - "grunion" - "guanaco" - "guernseycow" - "guillemot" - "guineafowl" - "guineapig" - "gull" - "guppy" - "gyrfalcon" - "hackee" - "haddock" - "hadrosaurus" - "hagfish" - "hairstreak" - "hairstreakbutterfly" - "hake" - "halcyon" - "halibut" - "halicore" - "hamadryad" - "hamadryas" - "hammerheadbird" - "hammerheadshark" - "hammerkop" - "hamster" - "hanumanmonkey" - "hapuka" - "hapuku" - "harborporpoise" - "harborseal" - "hare" - "harlequinbug" - "harpseal" - "harpyeagle" - "harrier" - "harrierhawk" - "hart" - "hartebeest" - "harvestmen" - "harvestmouse" - "hatchetfish" - "hawaiianmonkseal" - "hawk" - "hectorsdolphin" - "hedgehog" - "heifer" - "hellbender" - "hen" - "herald" - "herculesbeetle" - "hermitcrab" - "heron" - "herring" - "hind" - "hippopotamus" - "hoatzin" - "hochstettersfrog" - "hog" - "hogget" - "hoiho" - "hoki" - "homalocephale" - "honeybadger" - "honeybee" - "honeycreeper" - "honeyeater" - "hookersealion" - "hoopoe" - "hornbill" - "hornedtoad" - "hornedviper" - "hornet" - "hornshark" - "horse" - "horsechestnutleafminer" - "horsefly" - "horsemouse" - "horseshoebat" - "horseshoecrab" - "hound" - "housefly" - "hoverfly" - "howlermonkey" - "huemul" - "huia" - "human" - "hummingbird" - "humpbackwhale" - "husky" - "hydatidtapeworm" - "hydra" - "hyena" - "hylaeosaurus" - "hypacrosaurus" - "hypsilophodon" - "hyracotherium" - "hyrax" - "iaerismetalmark" - "ibadanmalimbe" - "iberianbarbel" - "iberianchiffchaff" - "iberianemeraldlizard" - "iberianlynx" - "iberianmidwifetoad" - "iberianmole" - "iberiannase" - "ibex" - "ibis" - "ibisbill" - "ibizanhound" - "iceblueredtopzebra" - "icefish" - "icelandgull" - "icelandichorse" - "icelandicsheepdog" - "ichidna" - "ichneumonfly" - "ichthyosaurs" - "ichthyostega" - "icterinewarbler" - "iggypops" - "iguana" - "iguanodon" - "illadopsis" - "ilsamochadegu" - "imago" - "impala" - "imperatorangel" - "imperialeagle" - "incatern" - "inchworm" - "indianabat" - "indiancow" - "indianelephant" - "indianglassfish" - "indianhare" - "indianjackal" - "indianpalmsquirrel" - "indianpangolin" - "indianrhinoceros" - "indianringneckparakeet" - "indianrockpython" - "indianskimmer" - "indianspinyloach" - "indigobunting" - "indigowingedparrot" - "indochinahogdeer" - "indochinesetiger" - "indri" - "indusriverdolphin" - "inexpectatumpleco" - "inganue" - "insect" - "intermediateegret" - "invisiblerail" - "iraniangroundjay" - "iridescentshark" - "iriomotecat" - "irishdraughthorse" - "irishredandwhitesetter" - "irishsetter" - "irishterrier" - "irishwaterspaniel" - "irishwolfhound" - "irrawaddydolphin" - "irukandjijellyfish" - "isabellineshrike" - "isabellinewheatear" - "islandcanary" - "islandwhistler" - "isopod" - "italianbrownbear" - "italiangreyhound" - "ivorybackedwoodswallow" - "ivorybilledwoodpecker" - "ivorygull" - "izuthrush" - "jabiru" - "jackal" - "jackrabbit" - "jaeger" - "jaguar" - "jaguarundi" - "janenschia" - "japanesebeetle" - "javalina" - "jay" - "jellyfish" - "jenny" - "jerboa" - "joey" - "johndory" - "juliabutterfly" - "jumpingbean" - "junco" - "junebug" - "kagu" - "kakapo" - "kakarikis" - "kangaroo" - "karakul" - "katydid" - "kawala" - "kentrosaurus" - "kestrel" - "kid" - "killdeer" - "killerwhale" - "killifish" - "kingbird" - "kingfisher" - "kinglet" - "kingsnake" - "kinkajou" - "kiskadee" - "kissingbug" - "kite" - "kitfox" - "kitten" - "kittiwake" - "kitty" - "kiwi" - "koala" - "koalabear" - "kob" - "kodiakbear" - "koi" - "komododragon" - "koodoo" - "kookaburra" - "kouprey" - "krill" - "kronosaurus" - "kudu" - "kusimanse" - "labradorretriever" - "lacewing" - "ladybird" - "ladybug" - "lamb" - "lamprey" - "langur" - "lark" - "larva" - "laughingthrush" - "lcont" - "leafbird" - "leafcutterant" - "leafhopper" - "leafwing" - "leech" - "lemming" - "lemur" - "leonberger" - "leopard" - "leopardseal" - "leveret" - "lhasaapso" - "liger" - "lightningbug" - "limpet" - "limpkin" - "ling" - "lion" - "lionfish" - "littlenightmonkeys" - "lizard" - "llama" - "lobo" - "lobster" - "locust" - "loggerheadturtle" - "longhorn" - "longhornbeetle" - "longspur" - "loon" - "lorikeet" - "loris" - "louse" - "lovebird" - "lowchen" - "lunamoth" - "lungfish" - "lynx" - "macaque" - "macaw" - "macropod" - "magpie" - "maiasaura" - "majungatholus" - "malamute" - "mallard" - "maltesedog" - "mamba" - "mamenchisaurus" - "mammal" - "mammoth" - "manatee" - "mandrill" - "mangabey" - "manta" - "mantaray" - "mantid" - "mantis" - "mantisray" - "manxcat" - "mara" - "marabou" - "marbledmurrelet" - "mare" - "marlin" - "marmoset" - "marmot" - "marten" - "martin" - "massasauga" - "massospondylus" - "mastiff" - "mastodon" - "mayfly" - "meadowhawk" - "meadowlark" - "mealworm" - "meerkat" - "megalosaurus" - "megaraptor" - "merganser" - "merlin" - "metalmarkbutterfly" - "metamorphosis" - "microvenator" - "midge" - "milksnake" - "milkweedbug" - "millipede" - "minibeast" - "mink" - "minnow" - "mite" - "moa" - "mockingbird" - "mole" - "mollies" - "mollusk" - "molly" - "monarch" - "mongoose" - "monkey" - "monkfish" - "monoclonius" - "montanoceratops" - "moorhen" - "moose" - "moray" - "morayeel" - "morpho" - "mosasaur" - "mosquito" - "moth" - "motmot" - "mouflon" - "mountaincat" - "mountainlion" - "mouse" - "mousebird" - "mudpuppy" - "mule" - "mullet" - "muntjac" - "murrelet" - "muskox" - "muskrat" - "mussaurus" - "mussel" - "mustang" - "mutt" - "myna" - "mynah" - "myotis" - "nabarlek" - "nag" - "naga" - "nagapies" - "nandine" - "nandoo" - "nandu" - "narwhal" - "narwhale" - "natterjacktoad" - "nauplius" - "nautilus" - "needlefish" - "needletail" - "nematode" - "nene" - "neonblueguppy" - "neonbluehermitcrab" - "neondwarfgourami" - "neonrainbowfish" - "neonredguppy" - "neontetra" - "nerka" - "nettlefish" - "newfoundlanddog" - "newt" - "newtnutria" - "nightcrawler" - "nighthawk" - "nightheron" - "nightingale" - "nightjar" - "nilgai" - "armadillo" - "noctilio" - "noctule" - "noddy" - "noolbenger" - "northerncardinals" - "northernelephantseal" - "northernflyingsquirrel" - "northernfurseal" - "northernpike" - "northernseahorse" - "northernspottedowl" - "norwaylobster" - "norwayrat" - "nubiangoat" - "nudibranch" - "numbat" - "nurseshark" - "nutcracker" - "nuthatch" - "nutria" - "nyala" - "ocelot" - "octopus" - "okapi" - "olingo" - "olm" - "opossum" - "orangutan" - "orca" - "oregonsilverspotbutterfly" - "oriole" - "oropendola" - "oropendula" - "oryx" - "osprey" - "ostracod" - "ostrich" - "otter" - "ovenbird" - "owl" - "owlbutterfly" - "ox" - "oxen" - "oxpecker" - "oyster" - "ozarkbigearedbat" - "paca" - "pachyderm" - "pacificparrotlet" - "paddlefish" - "paintedladybutterfly" - "panda" - "pangolin" - "panther" - "paperwasp" - "papillon" - "parakeet" - "parrot" - "partridge" - "peacock" - "peafowl" - "peccary" - "pekingese" - "pelican" - "pelicinuspetrel" - "penguin" - "perch" - "peregrinefalcon" - "pewee" - "phalarope" - "pharaohhound" - "pheasant" - "phoebe" - "phoenix" - "pigeon" - "piglet" - "pika" - "pike" - "pikeperch" - "pilchard" - "pinemarten" - "pinkriverdolphin" - "pinniped" - "pintail" - "pipistrelle" - "pipit" - "piranha" - "pitbull" - "pittabird" - "plainsqueaker" - "plankton" - "planthopper" - "platypus" - "plover" - "polarbear" - "polecat" - "polyp" - "polyturator" - "pomeranian" - "pondskater" - "pony" - "pooch" - "poodle" - "porcupine" - "porpoise" - "portuguesemanofwar" - "possum" - "prairiedog" - "prawn" - "prayingmantid" - "prayingmantis" - "primate" - "pronghorn" - "pseudodynerusquadrisectus" - "ptarmigan" - "pterodactyls" - "pterosaurs" - "puffer" - "pufferfish" - "puffin" - "pug" - "pullet" - "puma" - "pupa" - "pupfish" - "puppy" - "purplemarten" - "pygmy" - "python" - "quadrisectus" - "quagga" - "quahog" - "quail" - "queenalexandrasbirdwing" - "queenalexandrasbirdwingbutterfly" - "queenant" - "queenbee" - "queenconch" - "queenslandgrouper" - "queenslandheeler" - "queensnake" - "quelea" - "quetzal" - "quetzalcoatlus" - "quillback" - "quinquespinosus" - "quokka" - "quoll" - "rabbit" - "rabidsquirrel" - "raccoon" - "racer" - "racerunner" - "ragfish" - "rail" - "rainbowfish" - "rainbowlorikeet" - "rainbowtrout" - "ram" - "raptors" - "rasbora" - "rat" - "ratfish" - "rattail" - "rattlesnake" - "raven" - "ray" - "redhead" - "redheadedwoodpecker" - "redpoll" - "redstart" - "redtailedhawk" - "reindeer" - "reptile" - "reynard" - "rhea" - "rhesusmonkey" - "rhino" - "rhinoceros" - "rhinocerosbeetle" - "rhodesianridgeback" - "ringtailedlemur" - "ringworm" - "riograndeescuerzo" - "roach" - "roadrunner" - "roan" - "robberfly" - "robin" - "rockrat" - "rodent" - "roebuck" - "roller" - "rook" - "rooster" - "rottweiler" - "sable" - "sableantelope" - "sablefish" - "saiga" - "sakimonkey" - "salamander" - "salmon" - "saltwatercrocodile" - "sambar" - "samoyeddog" - "sandbarshark" - "sanddollar" - "sanderling" - "sandpiper" - "sapsucker" - "sardine" - "sawfish" - "scallop" - "scarab" - "scarletibis" - "scaup" - "schapendoes" - "schipperke" - "schnauzer" - "scorpion" - "scoter" - "screamer" - "seabird" - "seagull" - "seahog" - "seahorse" - "seal" - "sealion" - "seamonkey" - "seaslug" - "seaurchin" - "senegalpython" - "seriema" - "serpent" - "serval" - "shark" - "shearwater" - "sheep" - "sheldrake" - "shelduck" - "shibainu" - "shihtzu" - "shorebird" - "shoveler" - "shrew" - "shrike" - "shrimp" - "siamang" - "siamesecat" - "siberiantiger" - "sidewinder" - "sifaka" - "silkworm" - "silverfish" - "silverfox" - "silversidefish" - "siskin" - "skimmer" - "skipper" - "skua" - "skylark" - "sloth" - "slothbear" - "slug" - "smelts" - "smew" - "snail" - "snake" - "snipe" - "snoutbutterfly" - "snowdog" - "snowgeese" - "snowleopard" - "snowmonkey" - "snowyowl" - "sockeyesalmon" - "solenodon" - "solitaire" - "songbird" - "sora" - "southernhairnosedwombat" - "sow" - "spadefoot" - "sparrow" - "sphinx" - "spider" - "spidermonkey" - "spiketail" - "spittlebug" - "sponge" - "spoonbill" - "spotteddolphin" - "spreadwing" - "springbok" - "springpeeper" - "springtail" - "squab" - "squamata" - "squeaker" - "squid" - "squirrel" - "stag" - "stagbeetle" - "stallion" - "starfish" - "starling" - "steed" - "steer" - "stegosaurus" - "stickinsect" - "stickleback" - "stilt" - "stingray" - "stinkbug" - "stinkpot" - "stoat" - "stonefly" - "stork" - "stud" - "sturgeon" - "sugarglider" - "sulphurbutterfly" - "sunbear" - "sunbittern" - "sunfish" - "swallow" - "swallowtail" - "swallowtailbutterfly" - "swan" - "swellfish" - "swift" - "swordfish" - "tadpole" - "tahr" - "takin" - "tamarin" - "tanager" - "tapaculo" - "tapeworm" - "tapir" - "tarantula" - "tarpan" - "tarsier" - "taruca" - "tasmaniandevil" - "tasmaniantiger" - "tattler" - "tayra" - "teal" - "tegus" - "teledu" - "tench" - "tenrec" - "termite" - "tern" - "terrapin" - "terrier" - "thoroughbred" - "thrasher" - "thrip" - "thrush" - "thunderbird" - "thylacine" - "tick" - "tiger" - "tigerbeetle" - "tigermoth" - "tigershark" - "tilefish" - "tinamou" - "titi" - "titmouse" - "toad" - "toadfish" - "tomtit" - "topi" - "tortoise" - "toucan" - "towhee" - "tragopan" - "treecreeper" - "trex" - "triceratops" - "trogon" - "trout" - "trumpeterbird" - "trumpeterswan" - "tsetsefly" - "tuatara" - "tuna" - "turaco" - "turkey" - "turnstone" - "turtle" - "turtledove" - "uakari" - "ugandakob" - "uintagroundsquirrel" - "ulyssesbutterfly" - "umbrellabird" - "umbrette" - "unau" - "ungulate" - "unicorn" - "upupa" - "urchin" - "urial" - "uromastyxmaliensis" - "uromastyxspinipes" - "urson" - "urubu" - "urus" - "urutu" - "urva" - "utahprairiedog" - "vampirebat" - "vaquita" - "veery" - "velociraptor" - "velvetcrab" - "velvetworm" - "venomoussnake" - "verdin" - "vervet" - "viceroybutterfly" - "vicuna" - "viper" - "viperfish" - "vipersquid" - "vireo" - "virginiaopossum" - "vixen" - "vole" - "volvox" - "vulpesvelox" - "vulpesvulpes" - "vulture" - "walkingstick" - "wallaby" - "wallaroo" - "walleye" - "walrus" - "warbler" - "warthog" - "wasp" - "waterboatman" - "waterbuck" - "waterbuffalo" - "waterbug" - "waterdogs" - "waterdragons" - "watermoccasin" - "waterstrider" - "waterthrush" - "wattlebird" - "watussi" - "waxwing" - "weasel" - "weaverbird" - "weevil" - "westafricanantelope" - "whale" - "whapuku" - "whelp" - "whimbrel" - "whippet" - "whippoorwill" - "whitebeakeddolphin" - "whiteeye" - "whitepelican" - "whiterhino" - "whitetaileddeer" - "whitetippedreefshark" - "whooper" - "whoopingcrane" - "widgeon" - "widowspider" - "wildcat" - "wildebeast" - "wildebeest" - "willet" - "wireworm" - "wisent" - "wobbegongshark" - "wolf" - "wolfspider" - "wolverine" - "wombat" - "woodborer" - "woodchuck" - "woodnymphbutterfly" - "woodpecker" - "woodstorks" - "woollybearcaterpillar" - "worm" - "wrasse" - "wreckfish" - "wren" - "wrenchbird" - "wryneck" - "wuerhosaurus" - "wyvern" - "xanclomys" - "xanthareel" - "xantus" - "xantusmurrelet" - "xeme" - "xenarthra" - "xenoposeidon" - "xenops" - "xenopterygii" - "xenopus" - "xenotarsosaurus" - "xenurusunicinctus" - "xerus" - "xiaosaurus" - "xinjiangovenator" - "xiphias" - "xiphiasgladius" - "xiphosuran" - "xoloitzcuintli" - "xoni" - "xrayfish" - "xraytetra" - "xuanhanosaurus" - "xuanhuaceratops" - "xuanhuasaurus" - "yaffle" - "yak" - "yapok" - "yardant" - "yearling" - "yellowbelliedmarmot" - "yellowbellylizard" - "yellowhammer" - "yellowjacket" - "yellowlegs" - "yellowthroat" - "yellowwhitebutterfly" - "yeti" - "ynambu" - "yorkshireterrier" - "yosemitetoad" - "yucker" - "zander" - "zanzibardaygecko" - "zebra" - "zebradove" - "zebrafinch" - "zebrafish" - "zebralongwingbutterfly" - "zebraswallowtailbutterfly" - "zebratailedlizard" - "zebu" - "zenaida" - "zeren" - "zethusspinipes" - "zethuswasp" - "zigzagsalamander" - "zonetailedpigeon" - "zooplankton" - "zopilote" - "zorilla"]) \ No newline at end of file diff --git a/src/status_im/utils/gfycat/core.cljs b/src/status_im/utils/gfycat/core.cljs deleted file mode 100644 index 8443ca42386..00000000000 --- a/src/status_im/utils/gfycat/core.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.utils.gfycat.core - (:require [status-im.utils.gfycat.animals :as animals] - [status-im.utils.gfycat.adjectives :as adjectives] - [clojure.string :as str] - [status-im.utils.random :as rnd] - [status-im.utils.datetime :as datetime])) - -(defn- pick-random - [gen vector] - (str/capitalize (rnd/seeded-rand-nth gen vector))) - -(defn- build-gfy - [public-key] - (let [gen (rnd/rand-gen public-key) - first-adjective (pick-random gen adjectives/data) - second-adjective (pick-random gen adjectives/data) - animal (pick-random gen animals/data)] - (str first-adjective " " second-adjective " " animal))) - -(def unknown-gfy "Unknown") - -(defn generate-gfy - ([public-key] - (case public-key - nil unknown-gfy - "0" unknown-gfy - (build-gfy public-key))) - ([] (generate-gfy (datetime/timestamp)))) diff --git a/src/status_im/utils/handlers.cljs b/src/status_im/utils/handlers.cljs deleted file mode 100644 index 27d910f2a81..00000000000 --- a/src/status_im/utils/handlers.cljs +++ /dev/null @@ -1,126 +0,0 @@ -(ns status-im.utils.handlers - (:require [cljs.spec.alpha :as spec] - [clojure.string :as string] - [re-frame.core :refer [reg-event-db reg-event-fx] :as re-frame] - [re-frame.interceptor :refer [->interceptor get-coeffect get-effect]] - [status-im.utils.instabug :as instabug] - [status-im.models.account :as models.account] - [cljs.core.async :as async] - [taoensso.timbre :as log])) - -(def pre-event-callback (atom nil)) - -(defn add-pre-event-callback [callback] - (reset! pre-event-callback callback)) - -(defn side-effect! - "Middleware for handlers that will not affect db." - [handler] - (fn [db params] - (handler db params) - db)) - -(defn- pretty-print-event [ctx] - (let [[first second] (get-coeffect ctx :event)] - (if (or (string? second) (keyword? second) (boolean? second)) - (str first " " second) - first))) - -(def debug-handlers-names - "Interceptor which logs debug information to js/console for each event." - (->interceptor - :id :debug-handlers-names - :before (fn debug-handlers-names-before - [context] - (when @pre-event-callback - (@pre-event-callback (get-coeffect context :event))) - (log/debug "Handling re-frame event: " (pretty-print-event context)) - context))) - -(def logged-in - "Interceptor which stops the event chain if the user is logged out" - (->interceptor - :id :logged-in - :before (fn logged-in-before - [context] - (when (models.account/logged-in? (:coeffects context)) - context)))) - -(defn- check-spec-msg-path-problem [problem] - (str "Spec: " (-> problem :via last) "\n" - "Predicate: " (subs (str (:pred problem)) 0 50))) - -(defn- check-spec-msg-path-problems [path path-problems] - (str "Key path: " path "\n" - "Val: " (pr-str (-> path-problems first :val)) "\n\n" - "Number of problems: " (count path-problems) "\n\n" - (->> path-problems - (map check-spec-msg-path-problem) - (interpose "\n\n") - (apply str)))) - -(defn- check-spec-msg [event-id db] - (let [explain-data (spec/explain-data :status-im.ui.screens.db/db db) - problems (::spec/problems explain-data) - db-root-keys (->> problems (map (comp first :in)) set) - heading #(str "\n\n------\n" % "\n------\n\n") - msg (str "re-frame db spec check failed." - (heading "SUMMARY") - "After event id: " event-id "\n" - "Number of problems: " (count problems) "\n\n" - "Failing root db keys:\n" - (->> db-root-keys (interpose "\n") (apply str)) - (heading "PROBLEMS") - (->> problems - (group-by :in) - (map (fn [[path path-problems]] - (check-spec-msg-path-problems path path-problems))) - (interpose "\n\n-----\n\n") - (apply str)))] - msg)) - -(def check-spec - "throw an exception if db doesn't match the spec" - (->interceptor - :id check-spec - :after - (fn check-handler - [context] - (let [new-db (get-effect context :db) - event-id (-> (get-coeffect context :event) first)] - (when (and new-db (not (spec/valid? :status-im.ui.screens.db/db new-db))) - (throw (ex-info (check-spec-msg event-id new-db) {}))) - context)))) - -(defn register-handler - ([name handler] (register-handler name nil handler)) - ([name middleware handler] - (reg-event-db name [debug-handlers-names (when js/goog.DEBUG check-spec) middleware] handler))) - -(def default-interceptors - [debug-handlers-names - (when js/goog.DEBUG check-spec) - (re-frame/inject-cofx :now)]) - -(defn register-handler-db - ([name handler] (register-handler-db name nil handler)) - ([name interceptors handler] - (reg-event-db name [default-interceptors interceptors] handler))) - -(defn register-handler-fx - ([name handler] (register-handler-fx name nil handler)) - ([name interceptors handler] - (reg-event-fx name [default-interceptors interceptors] handler))) - -(defn get-hashtags [status] - (if status - (let [hashtags (map #(keyword (string/lower-case (subs % 1))) - (re-seq #"#[^ !?,;:.]+" status))] - (set (or hashtags []))) - #{})) - -(defn identities [contacts] - (->> (map second contacts) - (remove (fn [{:keys [dapp? pending?]}] - (or pending? dapp?))) - (map :whisper-identity))) diff --git a/src/status_im/utils/handlers_macro.clj b/src/status_im/utils/handlers_macro.clj deleted file mode 100644 index 7946a5dced5..00000000000 --- a/src/status_im/utils/handlers_macro.clj +++ /dev/null @@ -1,55 +0,0 @@ -(ns status-im.utils.handlers-macro) - -(defmacro merge-fx* - "This macro is called recursively from merge-fx - It wraps each form in a let binding that captures - - the new co-effect after updating the db key if a db effect was in the map returned - by the previous form - - the new fx map after merging previous fx map with the map returned by the previous form - We use safe-merge for that which is returning a map with a :merging-fx-with-common-keys effect - in case at least one effect that is not a :db effect was overwritten by merge" - {:added "1.0"} - [fx cofx & forms] - (if forms - (let [form (first forms) - temp-cofx (gensym 'temp-cofx)] - `(let [~temp-cofx (update-db ~cofx ~fx) - fx# (safe-merge ~fx ~(with-meta `(~(first form) ~@(next form) ~temp-cofx) (meta form)))] - (merge-fx* fx# ~temp-cofx ~@(next forms)))) - fx)) - -(defmacro merge-fx - "Takes a map of co-effects and forms as argument. - The first optional form can be map of effects/ - The next forms are functions applying effects and returning a map of effects. - The macro ensures that updates to db are passed from function to function within the cofx :db key and - that only a :merging-fx-with-common-keys effect is returned if some functions are trying - to produce the same effects (excepted :db, :data-source/tx and :data-source/base-tx effects). - :data-source/tx and :data-source/base-tx effects are handled specially and their results - (list of transactions) are compacted to one transactions list (for each effect). " - {:added "1.0"} - [cofx & forms] - (let [form (first forms)] - (if (or (symbol? form) - (map? form)) - `(merge-fx* ~form ~cofx ~@(next forms)) - `(merge-fx* {} ~cofx ~@forms)))) - -(comment (defn fn1 [{:keys [db]}] - {:db (assoc db :a 0) - :a "1"}) - - (defn fn2 [a {:keys [db]}] - {:db (update db :a + a)}) - - (defn fn3 [a {:keys [db u]}] - {:db (update db :a + u)}) - - (let [a 1 - b 2 - cofx {:db {} :u 1}] - (merge-fx cofx - {:db {:hello 2}} - (fn1) - (fn2 a) - (fn3 b)))) diff --git a/src/status_im/utils/handlers_macro.cljs b/src/status_im/utils/handlers_macro.cljs deleted file mode 100644 index 84d3d3e3ee0..00000000000 --- a/src/status_im/utils/handlers_macro.cljs +++ /dev/null @@ -1,39 +0,0 @@ -(ns status-im.utils.handlers-macro - (:require-macros status-im.utils.handlers-macro) - (:require [clojure.set :as set])) - -(defn update-db [cofx fx] - (if-let [db (:db fx)] - (assoc cofx :db db) - cofx)) - -(def ^:private mergable-keys - #{:data-store/tx :data-store/base-tx :chat-received-message/add-fx - :shh/add-new-sym-keys :shh/get-new-sym-keys :shh/post - :confirm-messages-processed}) - -(defn safe-merge [fx new-fx] - (if (:merging-fx-with-common-keys fx) - fx - (let [common-keys (set/intersection (into #{} (keys fx)) - (into #{} (keys new-fx)))] - (if (empty? (set/difference common-keys (conj mergable-keys :db))) - (merge (apply dissoc fx mergable-keys) - (apply dissoc new-fx mergable-keys) - (merge-with into - (select-keys fx mergable-keys) - (select-keys new-fx mergable-keys))) - {:merging-fx-with-common-keys common-keys})))) - -(defn merge-effects - ([{:keys [db] :as cofx} handler args] - (merge-effects {:db db} cofx handler args)) - ([initial-fx {:keys [db] :as cofx} handler args] - (reduce (fn [fx arg] - (let [temp-cofx (update-db cofx fx)] - (safe-merge - fx - (handler arg temp-cofx)))) - (or initial-fx - {:db db}) - args))) diff --git a/src/status_im/utils/hex.cljs b/src/status_im/utils/hex.cljs deleted file mode 100644 index eb37d89d08f..00000000000 --- a/src/status_im/utils/hex.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns status-im.utils.hex - (:require [clojure.string :as string])) - -(defn normalize-hex [hex] - (when hex - (string/lower-case (if (string/starts-with? hex "0x") - (subs hex 2) - hex)))) - -(defn valid-hex? [hex] - (let [hex (normalize-hex hex)] - (and (re-matches #"^[0-9a-fA-F]+$" hex) - (not= (js/parseInt hex 16) 0)))) diff --git a/src/status_im/utils/homoglyph.cljs b/src/status_im/utils/homoglyph.cljs deleted file mode 100644 index ad2dffe5550..00000000000 --- a/src/status_im/utils/homoglyph.cljs +++ /dev/null @@ -1,5 +0,0 @@ -(ns status-im.utils.homoglyph - (:require [status-im.js-dependencies :as dependencies])) - -(defn matches [s1 s2] - (.isMatches dependencies/homoglyph-finder s1 s2)) diff --git a/src/status_im/utils/http.cljs b/src/status_im/utils/http.cljs deleted file mode 100644 index 77273ae5094..00000000000 --- a/src/status_im/utils/http.cljs +++ /dev/null @@ -1,86 +0,0 @@ -(ns status-im.utils.http - (:require [status-im.utils.utils :as utils] - [status-im.react-native.js-dependencies :as rn-dependencies] - [taoensso.timbre :as log] - [goog.Uri :as uri] - [clojure.string :as string]) - (:refer-clojure :exclude [get])) - -;; Default HTTP request timeout ms -(def http-request-default-timeout-ms 3000) - -(defn post - "Performs an HTTP POST request" - ([url data on-success] - (post url data on-success nil)) - ([url data on-success on-error] - (post url data on-success on-error nil)) - ([url data on-success on-error {:keys [timeout-ms headers]}] - (-> (rn-dependencies/fetch - url - (clj->js (merge {:method "POST" - :body data - :timeout (or timeout-ms http-request-default-timeout-ms)} - (when headers - {:headers headers})))) - (.then (fn [response] - (on-success response))) - (.catch (or on-error - (fn [error] - (utils/show-popup "Error" (str error)))))))) - -(defn get - "Performs an HTTP GET request" - ([url] (get url nil)) - ([url on-success] (get url on-success nil)) - ([url on-success on-error] - (get url on-success on-error nil)) - ([url on-success on-error {:keys [valid-response? timeout-ms]}] - (-> (rn-dependencies/fetch url - (clj->js {:method "GET" - :headers {"Cache-Control" "no-cache"} - :timeout (or timeout-ms http-request-default-timeout-ms)})) - (.then (fn [response] - (-> - (.text response) - (.then (fn [response-body] - (let [ok? (.-ok response) - ok?' (if valid-response? - (and ok? (valid-response? response)) - ok?)] - [response-body ok?'])))))) - (.then (fn [[response ok?]] - (cond - (and on-success ok?) - (on-success response) - - (and on-error (not ok?)) - (on-error response) - - :else false))) - (.catch (or on-error - (fn [error] - (utils/show-popup "Error" (str error)))))))) - -(defn normalize-url [url] - (str (when (and (string? url) (not (re-find #"^[a-zA-Z-_]+:/" url))) "http://") url)) - -(def normalize-and-decode-url (comp js/decodeURI normalize-url)) - -(defn url-host [url] - (try - (when-let [host (.getDomain (goog.Uri. url))] - (when-not (string/blank? host) - (string/replace host #"www." ""))) - (catch :default _ nil))) - -(defn parse-payload [o] - (when o - (try - (js->clj (js/JSON.parse o) - :keywordize-keys true) - (catch :default _ - (log/debug (str "Failed to parse " o)))))) - -(defn url-sanitized? [uri] - (not (nil? (re-find #"^(https:)([/|.|\w|\s|-])*\.(?:jpg|svg|png)$" uri)))) diff --git a/src/status_im/utils/identicon.cljs b/src/status_im/utils/identicon.cljs deleted file mode 100644 index 401388fdcd2..00000000000 --- a/src/status_im/utils/identicon.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.utils.identicon - (:require [status-im.js-dependencies :as dependencies])) - -(def default-size 40) - -(defn identicon - ([hash] (identicon hash (clj->js {:background [255 255 255 255] - :margin 0.24 - :size default-size}))) - ([hash options] - (str "data:image/png;base64," - (str (new dependencies/identicon-js hash options))))) diff --git a/src/status_im/utils/image_processing.cljs b/src/status_im/utils/image_processing.cljs deleted file mode 100644 index aa60b4b923e..00000000000 --- a/src/status_im/utils/image_processing.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.utils.image-processing - (:require [goog.object :as object] - [status-im.utils.fs :refer [read-file]] - [taoensso.timbre :as log] - [clojure.string :as str] - [status-im.react-native.js-dependencies :as rn-dependencies])) - -(defn- resize [path max-width max-height on-resize on-error] - (let [resize-fn (-> rn-dependencies/image-resizer - (object/get "default") - (object/get "createResizedImage"))] - (-> (resize-fn path max-width max-height "JPEG" 75 0 nil) - (.then on-resize) - (.catch on-error)))) - -(defn- image-base64-encode [path on-success on-error] - (let [on-error (fn [error] - (on-error :base64 error))] - (read-file path "base64" on-success on-error))) - -(defn img->base64 [path on-success on-error] - (let [on-resized (fn [image] - (let [path (object/get image "path")] - (log/debug "Resized: " path) - (image-base64-encode path on-success on-error))) - on-error (fn [error] - (log/debug "Resized error: " error) - (on-error :resize error))] - (resize path 150 150 on-resized on-error))) diff --git a/src/status_im/utils/instabug.cljs b/src/status_im/utils/instabug.cljs deleted file mode 100644 index 03fcb2d53ea..00000000000 --- a/src/status_im/utils/instabug.cljs +++ /dev/null @@ -1,45 +0,0 @@ -(ns status-im.utils.instabug - (:require [taoensso.timbre :as log] - [status-im.utils.config :as config] - [status-im.react-native.js-dependencies :as rn-dependencies])) - -(def instabug rn-dependencies/instabug) - -(defn submit-bug [] - (.invokeWithInvocationMode - instabug - (.. instabug - -invocationMode - -newBug))) - -(defn request-feature [] - (.showFeatureRequests - instabug)) - -(defn- prepare-event-name [event {:keys [target]}] - (str event " " target)) - -(defn log [str] - (if js/goog.DEBUG - (log/debug str) - (.IBGLog rn-dependencies/instabug str))) - -(defn instabug-appender [] - {:enabled? true - :async? false - :min-level nil - :rate-limit nil - :output-fn :inherit - - :fn (fn [data] - (let [{:keys [level ?ns-str ?err output_]} data] - (log (force output_))))}) - -(when-not js/goog.DEBUG - (log/merge-config! {:appenders {:instabug (instabug-appender)}})) - -(defn init [] - (.startWithToken rn-dependencies/instabug - config/instabug-token - (.. rn-dependencies/instabug -invocationEvent -shake)) - (.setIntroMessageEnabled rn-dependencies/instabug false)) diff --git a/src/status_im/utils/js_resources.cljs b/src/status_im/utils/js_resources.cljs deleted file mode 100644 index 7f9c390a165..00000000000 --- a/src/status_im/utils/js_resources.cljs +++ /dev/null @@ -1,27 +0,0 @@ -(ns status-im.utils.js-resources - (:require-macros [status-im.utils.slurp :refer [slurp slurp-bot]]) - (:require [status-im.utils.types :refer [json->clj]] - [clojure.string :as s])) - -(def local-protocol "local://") - -(defn local-resource? [url] - (and (string? url) (s/starts-with? url local-protocol))) - -(def default-contacts (json->clj (slurp "resources/default_contacts.json"))) - -(def webview-js (slurp "resources/js/webview.js")) -(def web3 (str "; if (typeof Web3 == 'undefined') {" - (slurp "node_modules/web3/dist/web3.min.js") - "}")) -(defn web3-init [provider-address current-account-address network-id] - (str "var providerAddress = \"" provider-address "\";" - "var currentAccountAddress = \"" current-account-address "\";" - "var networkId = \"" network-id "\";" - (slurp "resources/js/web3_init.js"))) - -(defn local-storage-data [data] - (str "var localStorageData = " (or data "{}") ";")) - -(defn network-id [id] - (str "status.ethereumNetworkId = " (or id "null") "; ")) diff --git a/src/status_im/utils/keychain/core.cljs b/src/status_im/utils/keychain/core.cljs deleted file mode 100644 index 87062f07423..00000000000 --- a/src/status_im/utils/keychain/core.cljs +++ /dev/null @@ -1,79 +0,0 @@ -(ns status-im.utils.keychain.core - (:require [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.react-native.js-dependencies :as rn] - [status-im.utils.platform :as platform])) - -(def key-bytes 64) -(def username "status-im.encryptionkey") - -(defn- bytes->js-array [b] - (.from js/Array b)) - -(defn- string->js-array [s] - (.parse js/JSON (.-password s))) - -;; Smoke test key to make sure is ok, we noticed some non-random keys on -;; some IOS devices. We check naively that there are no more than key-bytes/2 -;; identical characters. -(defn validate - [encryption-key] - (cond - (or (not encryption-key) - (not= (.-length encryption-key) key-bytes)) - (.reject js/Promise {:error :invalid-key - :key encryption-key}) - - (>= (/ key-bytes 2) - (count (keys (group-by identity encryption-key)))) - (.reject js/Promise {:error :weak-key - :key encryption-key}) - - :else encryption-key)) - -(defn store [encryption-key] - (log/debug "storing encryption key") - (-> (.setGenericPassword - rn/keychain - username - (.stringify js/JSON encryption-key)) - (.then (constantly encryption-key)))) - -(defn create [] - (log/debug "no key exists, creating...") - (.. (rn/secure-random key-bytes) - (then bytes->js-array))) - -(defn handle-not-found [] - (.. (create) - (then validate) - (then store))) - -(def handle-found - (comp validate - string->js-array)) - -(defn get-encryption-key [] - (log/debug "initializing realm encryption key...") - (.. (.getGenericPassword rn/keychain) - (then - (fn [res] - (if res - (handle-found res) - (handle-not-found)))))) - -(defn safe-get-encryption-key - "Return encryption key or empty string in case invalid/empty" - [] - (log/debug "initializing realm encryption key...") - (.. (get-encryption-key) - (catch (fn [{:keys [_ key]}] - (log/warn "key is invalid, continuing") - (or key ""))))) - -(defn reset [] - (log/debug "resetting key...") - (.resetGenericPassword rn/keychain)) - -(defn set-username [] - (when platform/desktop? (.setUsername rn/keychain username))) diff --git a/src/status_im/utils/keychain/events.cljs b/src/status_im/utils/keychain/events.cljs deleted file mode 100644 index 78b67a0478a..00000000000 --- a/src/status_im/utils/keychain/events.cljs +++ /dev/null @@ -1,20 +0,0 @@ -(ns status-im.utils.keychain.events - (:require [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.utils.keychain.core :as keychain] - [status-im.utils.platform :as platform])) - -(defn handle-key-error [event {:keys [error key]}] - (if (= :weak-key error) - (log/warn "weak key used, database might not be encrypted properly") - (log/warn "invalid key detected")) - (re-frame/dispatch (into [] (concat event [(or key "") - (or error :invalid-key)])))) - -(re-frame/reg-fx - :get-encryption-key - (fn [event] - (when platform/desktop? (keychain/set-username)) - (.. (keychain/get-encryption-key) - (then #(re-frame/dispatch (conj event %))) - (catch (partial handle-key-error event))))) diff --git a/src/status_im/utils/money.cljs b/src/status_im/utils/money.cljs deleted file mode 100644 index e912d884f73..00000000000 --- a/src/status_im/utils/money.cljs +++ /dev/null @@ -1,152 +0,0 @@ -(ns status-im.utils.money - (:require [clojure.string :as string] - [status-im.js-dependencies :as dependencies])) - -;; The BigNumber version included in web3 sometimes hangs when dividing large -;; numbers Hence we want to use these functions instead of fromWei etc, which -;; come bundled with web3. See -;; https://github.com/MikeMcl/bignumber.js/issues/120 for this regression being -;; introduced in some JS environments. It is fixed in the MikeMcl/bignumber.js -;; repo, but not in the web3 BigNumber fork: -;; https://github.com/ethereum/web3.js/issues/877 -;; -;; Additionally, while it is possible to use the BigNumber constructor without -;; stringifying the number, this only works up to some 15 significant digits: -;; https://github.com/MikeMcl/bignumber.js/issues/120 -;; -;; Lastly, notice the bad rounding for native Javascript numbers above 17 digits -;; that may result in errors earlier up the call chain. Ideally all money-related -;; sensitive functions should be moved into this namespace to check for such -;; matters: -;; (str 111122223333441239) => "111122223333441230" - -(defn normalize - "A normalized string representation of an amount" - [s] - {:pre [(or (nil? s) (string? s))]} - (when s - (string/replace (string/trim s) #"," "."))) - -(defn bignumber [n] - (when n - (try - (dependencies/Web3.prototype.toBigNumber (normalize (str n))) - (catch :default err nil)))) - -(defn valid? [bn] - (when bn - (.greaterThanOrEqualTo bn 0))) - -(defn str->wei [s] - (when-let [ns (normalize s)] - (try - (dependencies/Web3.prototype.toWei ns "ether") - (catch :default err nil)))) - -(defn to-decimal [s] - (when s - (try - (dependencies/Web3.prototype.toDecimal (normalize s)) - (catch :default err nil)))) - -(defn from-decimal [n] (when n (str "1" (string/join (repeat n "0"))))) - -(def eth-units - {:wei (bignumber "1") - :kwei (bignumber (from-decimal 3)) - :mwei (bignumber (from-decimal 6)) - :gwei (bignumber (from-decimal 9)) - :szabo (bignumber (from-decimal 12)) - :finney (bignumber (from-decimal 15)) - :eth (bignumber (from-decimal 18)) - :keth (bignumber (from-decimal 21)) - :meth (bignumber (from-decimal 24)) - :geth (bignumber (from-decimal 27)) - :teth (bignumber (from-decimal 30))}) - -(defn wei-> [unit n] - (when-let [bn (bignumber n)] - (.dividedBy bn (eth-units unit)))) - -(defn ->wei [unit n] - (when-let [bn (bignumber n)] - (.times bn (eth-units unit)))) - -(defn to-fixed [bn] - (when bn - (.toFixed bn))) - -(defn to-number [bn] - (when bn - (.toNumber bn))) - -(defn wei->str [unit n] - (str (to-fixed (wei-> unit n)) " " (string/upper-case (name unit)))) - -(defn wei->ether [n] - (wei-> :eth n)) - -(defn ether->wei [bn] - (when bn - (.times bn (bignumber 1e18)))) - -(defn token->unit [n decimals] - (when-let [bn (bignumber n)] - (.dividedBy bn (bignumber (from-decimal decimals))))) - -(defn unit->token [n decimals] - (when-let [bn (bignumber n)] - (.times bn (bignumber (from-decimal decimals))))) - -;;NOTE(goranjovic) - We have two basic representations of values that refer to cryptocurrency amounts: formatted and -;; internal. Formatted representation is the one we show on screens and include in reports, whereas internal -;; representation is the one that we pass on to ethereum network for execution, transfer, etc. -;; The difference between the two depends on the number of decimals, i.e. internal representation is expressed in terms -;; of a whole number of smallest divisible parts of the formatted value. -;; -;; E.g. for Ether, it's smallest part is wei or 10^(-18) of 1 ether -;; for arbitrary ERC20 token the smallest part is 10^(-decimals) of 1 token -;; -;; Different tokens can have different number of allowed decimals, so it's neccessary to include the decimals parameter -;; to get the amount scale right. - -(defn formatted->internal [n symbol decimals] - (if (= :ETH symbol) - (ether->wei n) - (unit->token n decimals))) - -(defn internal->formatted [n symbol decimals] - (if (= :ETH symbol) - (wei->ether n) - (token->unit n decimals))) - -(defn fee-value [gas gas-price] - (.times (bignumber gas) (bignumber gas-price))) - -(defn crypto->fiat [crypto fiat-price] - (when-let [bn (bignumber crypto)] - (.times bn (bignumber fiat-price)))) - -(defn percent-change [from to] - (let [bnf (bignumber from) - bnt (bignumber to)] - (when (and bnf bnt) - (-> (.dividedBy bnf bnt) - (.minus 1) - (.times 100))))) - -(defn with-precision [n decimals] - (when-let [bn (bignumber n)] - (.round bn decimals))) - -(defn sufficient-funds? [amount balance] - (when (and amount balance) - (.greaterThanOrEqualTo balance amount))) - -(defn fiat-amount-value [amount-str from to prices] - (-> amount-str - (js/parseFloat) - bignumber - (crypto->fiat (get-in prices [from to :price] (bignumber 0))) - (with-precision 2) - str)) diff --git a/src/status_im/utils/name.cljs b/src/status_im/utils/name.cljs deleted file mode 100644 index fc75e98e90e..00000000000 --- a/src/status_im/utils/name.cljs +++ /dev/null @@ -1,30 +0,0 @@ -(ns status-im.utils.name - (:require [clojure.string :as str])) - -(defn too-long? [name max-len] - (> (count name) max-len)) - -(defn max-name - [name max-len] - (let [names (str/split name " ")] - (first - (reduce (fn [[name done] next-name] - (if done - name - (let [new-name (str/join " " [name next-name])] - (if (too-long? new-name max-len) - (let [new-name' (str name " " (first next-name) ".")] - (if (too-long? new-name' max-len) - [name true] - [new-name' true])) - [new-name])))) - [(first names)] - (rest names))))) - -(defn shortened-name [name max-len] - (if (> (count name) max-len) - (let [name' (max-name name max-len)] - (if (too-long? name' max-len) - (str (str/trim (subs name 0 max-len)) "...") - name')) - name)) diff --git a/src/status_im/utils/notifications.cljs b/src/status_im/utils/notifications.cljs deleted file mode 100644 index 24139ed77f1..00000000000 --- a/src/status_im/utils/notifications.cljs +++ /dev/null @@ -1,67 +0,0 @@ -(ns status-im.utils.notifications - (:require [goog.object :as object] - [re-frame.core :as re-frame :refer [dispatch reg-fx]] - [status-im.utils.handlers :as handlers] - [status-im.react-native.js-dependencies :as rn] - [status-im.utils.config :as config] - [status-im.utils.utils :as utils] - [status-im.ui.components.react :refer [copy-to-clipboard]] - [taoensso.timbre :as log] - [status-im.utils.platform :as platform])) - -;; Work in progress namespace responsible for push notifications and interacting -;; with Firebase Cloud Messaging. - -(handlers/register-handler-db - :update-fcm-token - (fn [db [_ fcm-token]] - (assoc-in db [:notifications :fcm-token] fcm-token))) - -(handlers/register-handler-fx - :request-notifications-granted - (fn [_ _] - (re-frame.core/dispatch [:show-mainnet-is-default-alert]))) - -(handlers/register-handler-fx - :request-notifications-denied - (fn [_ _] - (re-frame.core/dispatch [:show-mainnet-is-default-alert]))) - -;; NOTE: Only need to explicitly request permissions on iOS. -(defn request-permissions [] - (if platform/desktop? - (dispatch [:request-notifications-granted {}]) - (-> (.requestPermissions (.-default rn/react-native-fcm)) - (.then - (fn [_] - (log/debug "notifications-granted") - (dispatch [:request-notifications-granted {}])) - (fn [_] - (log/debug "notifications-denied") - (dispatch [:request-notifications-denied {}])))))) - -(defn get-fcm-token [] - (-> (.getFCMToken (object/get rn/react-native-fcm "default")) - (.then (fn [x] - (log/debug "get-fcm-token: " x) - (dispatch [:update-fcm-token x]))))) - -(defn on-refresh-fcm-token [] - (.on (.-default rn/react-native-fcm) - (.-RefreshToken (.-FCMEvent rn/react-native-fcm)) - (fn [x] - (log/debug "on-refresh-fcm-token: " x) - (dispatch [:update-fcm-token x])))) - -;; TODO(oskarth): Only called in background on iOS right now. -;; NOTE(oskarth): Hardcoded data keys :sum and :msg in status-go right now. -(defn on-notification [] - (.on (.-default rn/react-native-fcm) - (.-Notification (.-FCMEvent rn/react-native-fcm)) - (fn [event-js] - (let [event (js->clj event-js :keywordize-keys true) - data (select-keys event [:sum :msg]) - aps (:aps event)] - (log/debug "on-notification event: " (pr-str event)) - (log/debug "on-notification aps: " (pr-str aps)) - (log/debug "on-notification data: " (pr-str data)))))) diff --git a/src/status_im/utils/platform.cljs b/src/status_im/utils/platform.cljs deleted file mode 100644 index 117d31a2f65..00000000000 --- a/src/status_im/utils/platform.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.utils.platform - (:require [status-im.android.platform :as android] - [status-im.ios.platform :as ios] - [status-im.desktop.platform :as desktop] - [status-im.react-native.js-dependencies :as rn-dependencies])) - -(def platform - (.-Platform rn-dependencies/react-native)) - -(def os - (when platform - (.-OS platform))) - -(def version - (when platform - (.-Version platform))) - -(def android? (= os "android")) -(def ios? (= os "ios")) -(def desktop? (= os "desktop")) -(def mobile? (not= os "desktop")) -(def iphone-x? (and ios? (ios/iphone-x-dimensions?))) - -(def platform-specific - (cond - android? android/platform-specific - ios? ios/platform-specific - :else desktop/platform-specific)) diff --git a/src/status_im/utils/prices.cljs b/src/status_im/utils/prices.cljs deleted file mode 100644 index d3e50e8c027..00000000000 --- a/src/status_im/utils/prices.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.utils.prices - (:require [status-im.utils.http :as http] - [status-im.utils.types :as types])) - -;; Responsible for interacting with Cryptocompare API to get current prices for -;; currencies and tokens. -;; -;; No tests since fetch API (via http-get) relies on `window` being available. -;; -;; Example usage: -;; (get-prices "ETH" "USD" println print) - -(def api-url "https://min-api.cryptocompare.com/data") -(def status-identifier "extraParams=Status.im") - -(defn- ->url-param-syms [syms] - ((comp (partial clojure.string/join ",") (partial map name)) syms)) - -(defn- gen-price-url [fsyms tsyms] - (str api-url "/pricemultifull?fsyms=" (->url-param-syms fsyms) "&tsyms=" (->url-param-syms tsyms) "&" status-identifier)) - -(defn- format-price-resp [resp] - (into {} (for [[from entries] (:RAW (types/json->clj resp))] - {from (into {} (for [[to entry] entries] - {to {:from (name from) - :to (name to) - :price (:PRICE entry) - :last-day (:OPEN24HOUR entry)}}))}))) - -(defn get-prices [from to on-success on-error] - (http/get - (gen-price-url from to) - (fn [resp] (on-success (format-price-resp resp))) - on-error)) diff --git a/src/status_im/utils/random.cljs b/src/status_im/utils/random.cljs deleted file mode 100644 index 0182dcfe8f8..00000000000 --- a/src/status_im/utils/random.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.utils.random - (:require [status-im.js-dependencies :as dependencies] - [status-im.utils.datetime :as datetime])) - -(def chance (dependencies/Chance.)) - -(defn id [] - (str (datetime/timestamp) "-" (.guid chance))) - -(defn rand-gen - [seed] - (dependencies/Chance. seed)) - -(defn seeded-rand-int - [gen n] (.integer gen #js {:min 0 :max (dec n)})) - -(defn seeded-rand-nth - [gen coll] - (nth coll (seeded-rand-int gen (count coll)))) diff --git a/src/status_im/utils/security.cljs b/src/status_im/utils/security.cljs deleted file mode 100644 index 25c08019ade..00000000000 --- a/src/status_im/utils/security.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.utils.security) - -(defprotocol Unmaskable - ;; Retrieve the stored value. - (unmask [this])) - -;; MaskedData ensures that the object passed to it won't be occasionally printed -;; via println or log functions. Useful for keeping sensitive data, such as passwords -;; to avoid accidentally exposing them. -(deftype MaskedData [data] - Object - (toString [_] "******") - Unmaskable - (unmask [this] - (.-data this))) - -;; Returns a MaskedData instance that stores the piece of data. -(defn mask-data [data] - (MaskedData. data)) diff --git a/src/status_im/utils/signing_phrase/core.cljs b/src/status_im/utils/signing_phrase/core.cljs deleted file mode 100644 index b90541f8f2e..00000000000 --- a/src/status_im/utils/signing_phrase/core.cljs +++ /dev/null @@ -1,26 +0,0 @@ -(ns status-im.utils.signing-phrase.core - (:require [status-im.utils.signing-phrase.dictionaries.en :as en] - [clojure.string :as string])) - -; In order to reduce phishing threat for Status.im users we want to have them -; recognize 3 predefined words when they sign transactions or make other sensitive operations. -; -; Onboarding flow needs to generate the 3 words for the user and store it in the profile. -; In order to reduce phishing threat for users we want to have them recognize 3 -; predefined words when they sign transactions or make other sensitive operations. -; Onboarding flow needs to generate the 3 words for the user and store it in the profile. -; -; As a user, I want to accept 3 generated words when creating my account so that I can recognize these words when signing transactions -; and thus make it hard to create a phishing page and protect myself from phishing attack. -; -; See more info at: https://github.com/status-im/status-react/issues/1585 -; -; Currently only English is supported (as the default language), -; for more details see #https://github.com/status-im/status-react/issues/1679 - -(defn pick-words [dictionary] - (repeatedly 3 #(rand-nth dictionary))) - -(defn generate [] - (string/join " " (pick-words en/dictionary))) - diff --git a/src/status_im/utils/signing_phrase/dictionaries/en.cljs b/src/status_im/utils/signing_phrase/dictionaries/en.cljs deleted file mode 100644 index f630e410cfd..00000000000 --- a/src/status_im/utils/signing_phrase/dictionaries/en.cljs +++ /dev/null @@ -1,625 +0,0 @@ -(ns status-im.utils.signing-phrase.dictionaries.en) - -(def dictionary - ["acid" - "alto" - "apse" - "arch" - "area" - "army" - "atom" - "aunt" - "babe" - "baby" - "back" - "bail" - "bait" - "bake" - "ball" - "band" - "bank" - "barn" - "base" - "bass" - "bath" - "bead" - "beak" - "beam" - "bean" - "bear" - "beat" - "beef" - "beer" - "beet" - "bell" - "belt" - "bend" - "bike" - "bill" - "bird" - "bite" - "blow" - "blue" - "boar" - "boat" - "body" - "bolt" - "bomb" - "bone" - "book" - "boot" - "bore" - "boss" - "bowl" - "brow" - "bulb" - "bull" - "burn" - "bush" - "bust" - "cafe" - "cake" - "calf" - "call" - "calm" - "camp" - "cane" - "cape" - "card" - "care" - "carp" - "cart" - "case" - "cash" - "cast" - "cave" - "cell" - "cent" - "chap" - "chef" - "chin" - "chip" - "chop" - "chub" - "chug" - "city" - "clam" - "clef" - "clip" - "club" - "clue" - "coal" - "coat" - "code" - "coil" - "coin" - "coke" - "cold" - "colt" - "comb" - "cone" - "cook" - "cope" - "copy" - "cord" - "cork" - "corn" - "cost" - "crab" - "craw" - "crew" - "crib" - "crop" - "crow" - "curl" - "cyst" - "dame" - "dare" - "dark" - "dart" - "dash" - "data" - "date" - "dead" - "deal" - "dear" - "debt" - "deck" - "deep" - "deer" - "desk" - "dhow" - "diet" - "dill" - "dime" - "dirt" - "dish" - "disk" - "dock" - "doll" - "door" - "dory" - "drag" - "draw" - "drop" - "drug" - "drum" - "duck" - "dump" - "dust" - "duty" - "ease" - "east" - "eave" - "eddy" - "edge" - "envy" - "epee" - "exam" - "exit" - "face" - "fact" - "fail" - "fall" - "fame" - "fang" - "farm" - "fawn" - "fear" - "feed" - "feel" - "feet" - "file" - "fill" - "film" - "find" - "fine" - "fire" - "fish" - "flag" - "flat" - "flax" - "flow" - "foam" - "fold" - "font" - "food" - "foot" - "fork" - "form" - "fort" - "fowl" - "frog" - "fuel" - "full" - "gain" - "gale" - "galn" - "game" - "garb" - "gate" - "gear" - "gene" - "gift" - "girl" - "give" - "glad" - "glen" - "glue" - "glut" - "goal" - "goat" - "gold" - "golf" - "gong" - "good" - "gown" - "grab" - "gram" - "gray" - "grey" - "grip" - "grit" - "gyro" - "hail" - "hair" - "half" - "hall" - "hand" - "hang" - "harm" - "harp" - "hate" - "hawk" - "head" - "heat" - "heel" - "hell" - "helo" - "help" - "hemp" - "herb" - "hide" - "high" - "hill" - "hire" - "hive" - "hold" - "hole" - "home" - "hood" - "hoof" - "hook" - "hope" - "hops" - "horn" - "hose" - "host" - "hour" - "hunt" - "hurt" - "icon" - "idea" - "inch" - "iris" - "iron" - "item" - "jail" - "jeep" - "jeff" - "joey" - "join" - "joke" - "judo" - "jump" - "junk" - "jury" - "jute" - "kale" - "keep" - "kick" - "kill" - "kilt" - "kind" - "king" - "kiss" - "kite" - "knee" - "knot" - "lace" - "lack" - "lady" - "lake" - "lamb" - "lamp" - "land" - "lark" - "lava" - "lawn" - "lead" - "leaf" - "leek" - "lier" - "life" - "lift" - "lily" - "limo" - "line" - "link" - "lion" - "lisa" - "list" - "load" - "loaf" - "loan" - "lock" - "loft" - "long" - "look" - "loss" - "lout" - "love" - "luck" - "lung" - "lute" - "lynx" - "lyre" - "maid" - "mail" - "main" - "make" - "male" - "mall" - "manx" - "many" - "mare" - "mark" - "mask" - "mass" - "mate" - "math" - "meal" - "meat" - "meet" - "menu" - "mess" - "mice" - "midi" - "mile" - "milk" - "mime" - "mind" - "mine" - "mini" - "mint" - "miss" - "mist" - "moat" - "mode" - "mole" - "mood" - "moon" - "most" - "moth" - "move" - "mule" - "mutt" - "nail" - "name" - "neat" - "neck" - "need" - "neon" - "nest" - "news" - "node" - "nose" - "note" - "oboe" - "okra" - "open" - "oval" - "oven" - "oxen" - "pace" - "pack" - "page" - "pail" - "pain" - "pair" - "palm" - "pard" - "park" - "part" - "pass" - "past" - "path" - "peak" - "pear" - "peen" - "peer" - "pelt" - "perp" - "pest" - "pick" - "pier" - "pike" - "pile" - "pimp" - "pine" - "ping" - "pink" - "pint" - "pipe" - "piss" - "pith" - "plan" - "play" - "plot" - "plow" - "poem" - "poet" - "pole" - "polo" - "pond" - "pony" - "poof" - "pool" - "port" - "post" - "prow" - "pull" - "puma" - "pump" - "pupa" - "push" - "quit" - "race" - "rack" - "raft" - "rage" - "rail" - "rain" - "rake" - "rank" - "rate" - "read" - "rear" - "reef" - "rent" - "rest" - "rice" - "rich" - "ride" - "ring" - "rise" - "risk" - "road" - "robe" - "rock" - "role" - "roll" - "roof" - "room" - "root" - "rope" - "rose" - "ruin" - "rule" - "rush" - "ruth" - "sack" - "safe" - "sage" - "sail" - "sale" - "salt" - "sand" - "sari" - "sash" - "save" - "scow" - "seal" - "seat" - "seed" - "self" - "sell" - "shed" - "shin" - "ship" - "shoe" - "shop" - "shot" - "show" - "sick" - "side" - "sign" - "silk" - "sill" - "silo" - "sing" - "sink" - "site" - "size" - "skin" - "sled" - "slip" - "smog" - "snob" - "snow" - "soap" - "sock" - "soda" - "sofa" - "soft" - "soil" - "song" - "soot" - "sort" - "soup" - "spot" - "spur" - "stag" - "star" - "stay" - "stem" - "step" - "stew" - "stop" - "stud" - "suck" - "suit" - "swan" - "swim" - "tail" - "tale" - "talk" - "tank" - "tard" - "task" - "taxi" - "team" - "tear" - "teen" - "tell" - "temp" - "tent" - "term" - "test" - "text" - "thaw" - "tile" - "till" - "time" - "tire" - "toad" - "toga" - "togs" - "tone" - "tool" - "toot" - "tote" - "tour" - "town" - "tram" - "tray" - "tree" - "trim" - "trip" - "tuba" - "tube" - "tuna" - "tune" - "turn" - "tutu" - "twig" - "type" - "unit" - "user" - "vane" - "vase" - "vast" - "veal" - "veil" - "vein" - "vest" - "vibe" - "view" - "vise" - "wait" - "wake" - "walk" - "wall" - "wash" - "wasp" - "wave" - "wear" - "weed" - "week" - "well" - "west" - "whip" - "wife" - "will" - "wind" - "wine" - "wing" - "wire" - "wish" - "wolf" - "wood" - "wool" - "word" - "work" - "worm" - "wrap" - "wren" - "yard" - "yarn" - "yawl" - "year" - "yoga" - "yoke" - "yurt" - "zinc" - "zone"]) \ No newline at end of file diff --git a/src/status_im/utils/slurp.clj b/src/status_im/utils/slurp.clj deleted file mode 100644 index 0b1516b31d2..00000000000 --- a/src/status_im/utils/slurp.clj +++ /dev/null @@ -1,15 +0,0 @@ -(ns status-im.utils.slurp - (:refer-clojure :exclude [slurp]) - (:require [clojure.string :as string])) - -(defmacro slurp [file] - (clojure.core/slurp file)) - -(defmacro slurp-bot [bot-name & files] - (->> (concat files ["translations.js" "bot.js"]) - (map (fn [file-name] - (try - (clojure.core/slurp - (string/join "/" ["resources/js/bots" (name bot-name) file-name])) - (catch Exception _ "")))) - (apply str))) \ No newline at end of file diff --git a/src/status_im/utils/snoopy.cljs b/src/status_im/utils/snoopy.cljs deleted file mode 100644 index 8b615fe4e3a..00000000000 --- a/src/status_im/utils/snoopy.cljs +++ /dev/null @@ -1,101 +0,0 @@ -(ns status-im.utils.snoopy - (:require [status-im.react-native.js-dependencies :as js-dependencies] - [status-im.utils.config :as config])) - -(def snoopy (.-default js-dependencies/snoopy)) -(def sn-filter (.-default js-dependencies/snoopy-filter)) -(def bars (.-default js-dependencies/snoopy-bars)) -(def buffer (.-default js-dependencies/snoopy-buffer)) - -(defn create-filter [f] - (fn [message] - (let [method (.-method message) - module (.-module message) - args (.-args message) - first-arg (when (pos? (.-length args)) - (aget args 0))] - (f {:method method - :module module - :first-arg first-arg})))) - -(defn status-module-filter [{:keys [method module first-arg]}] - (or (= module "Status") - (and (= module "RCTNativeAppEventEmitter") - (= method "emit") - (= first-arg "gethEvent")) - (and - (string? method) - (clojure.string/starts-with? method " (collection db) - (contains? element)))) diff --git a/src/status_im/utils/transactions.cljs b/src/status_im/utils/transactions.cljs deleted file mode 100644 index ba2946a1eb7..00000000000 --- a/src/status_im/utils/transactions.cljs +++ /dev/null @@ -1,63 +0,0 @@ -(ns status-im.utils.transactions - (:require [status-im.utils.http :as http] - [status-im.utils.types :as types] - [taoensso.timbre :as log])) - -(defn- get-network-subdomain [chain] - (case chain - (:testnet) "ropsten" - (:mainnet) nil - (:rinkeby) "rinkeby")) - -(defn get-transaction-details-url [chain hash] - (let [network-subdomain (get-network-subdomain chain)] - (str "https://" (when network-subdomain (str network-subdomain ".")) "etherscan.io/tx/" hash))) - -(def etherscan-api-key "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI") - -(defn- get-api-network-subdomain [chain] - (case chain - (:testnet) "api-ropsten" - (:mainnet) "api" - (:rinkeby) "api-rinkeby")) - -(defn get-transaction-url [chain account] - (let [network-subdomain (get-api-network-subdomain chain)] - (str "https://" network-subdomain ".etherscan.io/api?module=account&action=txlist&address=0x" - account "&startblock=0&endblock=99999999&sort=desc&apikey=" etherscan-api-key "?q=json"))) - -(defn- format-transaction [account {:keys [value timeStamp blockNumber hash from to gas gasPrice gasUsed nonce confirmations input isError]}] - (let [inbound? (= (str "0x" account) to) - error? (= "1" isError)] - {:value value - ;; timestamp is in seconds, we convert it in ms - :timestamp (str timeStamp "000") - :symbol :ETH - :type (cond error? :failed - inbound? :inbound - :else :outbound) - :block blockNumber - :hash hash - :from from - :to to - :gas-limit gas - :gas-price gasPrice - :gas-used gasUsed - :nonce nonce - :confirmations confirmations - :data input})) - -(defn- format-transactions-response [response account] - (->> response - types/json->clj - :result - (reduce (fn [transactions {:keys [hash] :as transaction}] - (assoc transactions hash (format-transaction account transaction))) - {}))) - -(defn get-transactions [chain account on-success on-error] - (let [url (get-transaction-url chain account)] - (log/debug "HTTP GET" url) - (http/get url - #(on-success (format-transactions-response % account)) - on-error))) diff --git a/src/status_im/utils/transducers.cljs b/src/status_im/utils/transducers.cljs deleted file mode 100644 index af6e367072e..00000000000 --- a/src/status_im/utils/transducers.cljs +++ /dev/null @@ -1,26 +0,0 @@ -(ns status-im.utils.transducers - "Utility namespace containing various usefull transducers") - -(defn last-distinct-by - "Just like regular `distinct`, but you provide function - computing the distinctness of input elements and when - duplicate elements are removed, the last, not the first - one is removed." - [compare-fn] - (fn [rf] - (let [accumulated-input (volatile! {:seen {} - :input []})] - (fn - ([] (rf)) - ([result] - (reduce rf result (:input @accumulated-input))) - ([result input] - (let [compare-value (compare-fn input)] - (if-let [previous-duplicate-index (get-in @accumulated-input [:seen compare-value])] - (do (vswap! accumulated-input assoc-in [:input previous-duplicate-index] input) - result) - (do (vswap! accumulated-input (fn [{previous-input :input :as accumulated-input}] - (-> accumulated-input - (update :seen assoc compare-value (count previous-input)) - (update :input conj input)))) - result)))))))) diff --git a/src/status_im/utils/types.cljs b/src/status_im/utils/types.cljs deleted file mode 100644 index 25c075ae86e..00000000000 --- a/src/status_im/utils/types.cljs +++ /dev/null @@ -1,15 +0,0 @@ -(ns status-im.utils.types) - -(defn to-string [s] - (if (keyword? s) - (name s) - s)) - -(defn clj->json [data] - (.stringify js/JSON (clj->js data))) - -(defn json->clj [json] - (when-not (= json "undefined") - (try - (js->clj (.parse js/JSON json) :keywordize-keys true) - (catch js/Error _ (when (string? json) json))))) diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs deleted file mode 100644 index c6dddcbb972..00000000000 --- a/src/status_im/utils/universal_links/core.cljs +++ /dev/null @@ -1,121 +0,0 @@ -(ns status-im.utils.universal-links.core - (:require - [taoensso.timbre :as log] - [re-frame.core :as re-frame] - [status-im.utils.config :as config] - [status-im.chat.events :as chat.events] - [status-im.models.account :as models.account] - [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.react :as react])) - -(def public-chat-regex #".*/chat/public/(.*)$") -(def profile-regex #".*/user/(.*)$") -(def browse-regex #".*/browse/(.*)$") - -(defn match-url [url regex] - (some->> url - (re-matches regex) - peek)) - -(defn universal-link? [url] - (boolean - (re-matches #"((^https?://get.status.im/)|(^status-im://))[\x00-\x7F]+$" url))) - -(defn open! [url] - (log/info "universal-links: opening " url) - (if-let [dapp-url (match-url url browse-regex)] - (list-selection/browse-dapp dapp-url) - ;; We need to dispatch here, we can't openURL directly - ;; as it is opened in safari on iOS - (re-frame/dispatch [:handle-universal-link url]))) - -(defn handle-browse [url cofx] - (log/info "universal-links: handling browse " url) - {:browse url}) - -(defn handle-public-chat [public-chat cofx] - (log/info "universal-links: handling public chat " public-chat) - (chat.events/create-new-public-chat public-chat cofx)) - -(defn handle-view-profile [profile-id cofx] - (log/info "universal-links: handling view profile" profile-id) - (chat.events/show-profile profile-id true cofx)) - -(defn handle-not-found [full-url] - (log/info "universal-links: no handler for " full-url)) - -(defn stored-url-event - "Return an event description for processing a url if in the database" - [{:keys [db]}] - (when-let [url (:universal-links/url db)] - [:handle-universal-link url])) - -(defn dispatch-url - "Dispatch url so we can get access to re-frame/db" - [url] - (if-not (nil? url) - (re-frame/dispatch [:handle-universal-link url]) - (log/debug "universal-links: no url"))) - -(defn store-url-for-later - "Store the url in the db to be processed on login" - [url {:keys [db]}] - (assoc-in {:db db} [:db :universal-links/url] url)) - -(defn clear-url - "Remove a url from the db" - [{:keys [db]}] - (update {:db db} :db dissoc :universal-links/url)) - -(defn route-url - "Match a url against a list of routes and handle accordingly" - [url cofx] - (cond - (match-url url public-chat-regex) - (handle-public-chat (match-url url public-chat-regex) cofx) - - (match-url url profile-regex) - (handle-view-profile (match-url url profile-regex) cofx) - - (match-url url browse-regex) - (handle-browse url cofx) - - :else (handle-not-found url))) - -(defn handle-url - "Store url in the database if the user is not logged in, to be processed - on login, otherwise just handle it" - [url cofx] - (if (models.account/logged-in? cofx) - (do - (clear-url cofx) - (route-url url cofx)) - (store-url-for-later url cofx))) - -(defn unwrap-js-url [e] - (-> e - (js->clj :keywordize-keys true) - :url)) - -(def url-event-listener - (comp dispatch-url unwrap-js-url)) - -(defn initialize - "Add an event listener for handling background->foreground transition - and handles incoming url if the app has been started by clicking on a link" - [] - (when config/universal-links-enabled? - (log/debug "universal-links: initializing") - (.. react/linking - (getInitialURL) - (then dispatch-url)) - (.. react/linking - (addEventListener "url" url-event-listener)))) - -(defn finalize - "Remove event listener for url" - [] - (when config/universal-links-enabled? - (log/debug "universal-links: finalizing") - (.. react/linking - (removeEventListener "url" url-event-listener)))) diff --git a/src/status_im/utils/universal_links/events.cljs b/src/status_im/utils/universal_links/events.cljs deleted file mode 100644 index b34b72fcb40..00000000000 --- a/src/status_im/utils/universal_links/events.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.utils.universal-links.events - (:require [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.utils.config :as config] - [status-im.utils.handlers :as handlers] - [status-im.utils.handlers-macro :as handlers-macro] - [status-im.utils.universal-links.core :as universal-links])) - -(handlers/register-handler-fx - :handle-universal-link - (fn [cofx [_ url]] - (log/debug "universal links: event received for " url) - (when config/universal-links-enabled? - (universal-links/handle-url url cofx)))) diff --git a/src/status_im/utils/utils.cljs b/src/status_im/utils/utils.cljs deleted file mode 100644 index 115fc8a1f99..00000000000 --- a/src/status_im/utils/utils.cljs +++ /dev/null @@ -1,81 +0,0 @@ -(ns status-im.utils.utils - (:require [status-im.i18n :as i18n] - [status-im.react-native.js-dependencies :as rn-dependencies] - [re-frame.core :as re-frame])) - -(defn show-popup - ([title content] - (show-popup title content nil)) - ([title content on-dismiss] - (.alert (.-Alert rn-dependencies/react-native) - title - content - (clj->js - (vector (merge {:text "OK" - :style "cancel" - :accessibility-label :cancel-button} - (when on-dismiss {:onPress on-dismiss})))) - (when on-dismiss - (clj->js {:cancelable false}))))) - -(defn show-confirmation - ([title content on-accept] - (show-confirmation title content nil on-accept)) - ([title content confirm-button-text on-accept] - (show-confirmation title content confirm-button-text on-accept nil)) - ([title content confirm-button-text on-accept on-cancel] - (show-confirmation nil title content confirm-button-text on-accept on-cancel)) - ([ios-style title content confirm-button-text on-accept on-cancel] - (show-confirmation ios-style title content confirm-button-text on-accept on-cancel nil)) - ([{:keys [ios-confirm-style] :or {ios-confirm-style "destructive"}} - title content confirm-button-text on-accept on-cancel cancel-button-text] - (.alert (.-Alert rn-dependencies/react-native) - title - content - ;; Styles are only relevant on iOS. On Android first button is 'neutral' and second is 'positive' - (clj->js - (vector (merge {:text (or cancel-button-text (i18n/label :t/cancel)) - :style "cancel" - :accessibility-label :cancel-button} - (when on-cancel {:onPress on-cancel})) - {:text (or confirm-button-text "OK") - :onPress on-accept - :style ios-confirm-style - :accessibility-label :confirm-button}))))) - -(defn show-question - ([title content on-accept] - (show-question title content on-accept nil)) - ([title content on-accept on-cancel] - (.alert (.-Alert rn-dependencies/react-native) - title - content - (clj->js - (vector (merge {:text (i18n/label :t/no) - :accessibility-label :no-button} - (when on-cancel {:onPress on-cancel})) - {:text (i18n/label :t/yes) - :onPress on-accept - :accessibility-label :yes-button}))))) - -;; background-timer - -(defn set-timeout [cb ms] - (.setTimeout rn-dependencies/background-timer cb ms)) - -;; same as re-frame dispatch-later but using background timer for long -;; running timeouts -(re-frame/reg-fx - :utils/dispatch-later - (fn [params] - (doseq [{:keys [ms dispatch]} params] - (set-timeout #(re-frame/dispatch dispatch) ms)))) - -(defn clear-timeout [id] - (.clearTimeout rn-dependencies/background-timer id)) - -(defn set-interval [cb ms] - (.setInterval rn-dependencies/background-timer cb ms)) - -(defn clear-interval [id] - (.clearInterval rn-dependencies/background-timer id)) diff --git a/src/status_im/utils/views.clj b/src/status_im/utils/views.clj deleted file mode 100644 index b8a523c4546..00000000000 --- a/src/status_im/utils/views.clj +++ /dev/null @@ -1,68 +0,0 @@ -(ns status-im.utils.views - (:require [clojure.walk :as w])) - -(defn atom? [sub] - (or (vector? sub) - (and (seq sub) - (#{`reagent.core/atom} (first sub))))) - -(defn walk-sub [sub form->sym] - (if (coll? sub) - (w/postwalk (fn [f] - (or (form->sym f) f)) sub) - (or (form->sym sub) sub))) - -(defn prepare-subs [subs] - (let [pairs (map (fn [[form sub]] - {:form form - :sub sub - :sym (if (atom? sub) - (gensym (str (if (map? form) "keys" form))) - form)}) - (partition 2 subs)) - form->sym (->> pairs - (map (fn [{:keys [form sym]}] - [form sym])) - (into {}))] - [(mapcat (fn [{:keys [form sym sub]}] - (if (vector? sub) - [sym `(re-frame.core/subscribe ~(walk-sub sub form->sym))] - [form (walk-sub sub form->sym)])) - pairs) - (apply concat (keep (fn [{:keys [sym form sub]}] - (when (atom? sub) - [form `(deref ~sym)])) - pairs))])) - -(defmacro letsubs [args & body]) - -(defmacro defview - [n params & rest-body] - (let [first-symbol (ffirst rest-body) - rest-body' (if (and (symbol? first-symbol) - (= (name first-symbol) "letsubs")) - (rest (first rest-body)) - rest-body) - [subs component-map body] (case (count rest-body') - 1 [nil {} (first rest-body')] - 2 [(first rest-body') {} (second rest-body')] - 3 rest-body') - [subs-bindings vars-bindings] (prepare-subs subs)] - `(do - (when-not (find-ns 're-frame.core) - (require 're-frame.core)) - (defn ~n ~params - (let [~@subs-bindings] - (reagent.core/create-class - (merge ~(->> component-map - (map (fn [[k f]] - (let [args (gensym "args")] - [k `(fn [& ~args] - (let [~@vars-bindings] - (apply ~f ~args)))]))) - (into {})) - {:display-name (name '~n) - :reagent-render - (fn ~params - (let [~@vars-bindings] - ~body))}))))))) diff --git a/src/status_im/utils/web3_provider.cljs b/src/status_im/utils/web3_provider.cljs deleted file mode 100644 index 6116b90a6da..00000000000 --- a/src/status_im/utils/web3_provider.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.utils.web3-provider - (:require [taoensso.timbre :as log] - [status-im.native-module.core :as status] - [status-im.js-dependencies :as dependencies])) - -(defn make-web3 [] - (dependencies/Web3. - #js {:sendAsync (fn [payload callback] - (status/call-web3 - (.stringify js/JSON payload) - (fn [response] - (if (= "" response) - (log/warn :web3-response-error) - (callback nil (.parse js/JSON response))))))})) diff --git a/src/status_im/wallet/transactions.cljs b/src/status_im/wallet/transactions.cljs deleted file mode 100644 index 0ba7a12c97a..00000000000 --- a/src/status_im/wallet/transactions.cljs +++ /dev/null @@ -1,108 +0,0 @@ -(ns status-im.wallet.transactions - (:require [clojure.set :as set] - [status-im.utils.datetime :as time] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.handlers-macro :as handlers-macro] - [taoensso.timbre :as log])) - -(def sync-interval-ms 15000) -(def confirmations-count-threshold 12) - -;; Detects if some of the transactions have less than 12 confirmations -(defn- have-unconfirmed-transactions? [cofx] - (->> (get-in cofx [:db :wallet :transactions]) - vals - (map :confirmations) - (map int) - (some #(< % confirmations-count-threshold)))) - -(defn- wallet-transactions-set [db] - (-> db - (get-in [:wallet :transactions]) - keys - set)) - -;; Detects if some of missing chat transactions are missing from wallet -(defn- have-missing-chat-transactions? [{:keys [db]}] - (let [chat-transactions (get-in db [:wallet :chat-transactions])] - (not= (count chat-transactions) - (count (set/intersection - chat-transactions - (wallet-transactions-set db)))))) - -(defn- schedule-sync [cofx] - {:utils/dispatch-later [{:ms sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}) - -(defn store-chat-transaction-hash [tx-hash {:keys [db]}] - {:db (update-in db [:wallet :chat-transactions] conj tx-hash)}) - -(defn- missing-chat-transactions [{:keys [db] :as cofx}] - (let [chat-transactions (->> db - :chats - vals - (remove :public?) - (mapcat :messages) - vals - flatten - (filter #(= "command" (:content-type %))) - (map #(get-in % [:content :params :tx-hash])) - (filter identity) - set)] - (set/difference - chat-transactions - (wallet-transactions-set db)))) - -; Find missing chat transactions -; and store them at [:wallet :chat-transactions] -; to be used later by have-missing-chat-transactions? on every sync request -(defn load-missing-chat-transactions [{:keys [db] :as cofx}] - (when (nil? (get-in db [:wallet :chat-transactions])) - {:db (assoc-in db - [:wallet :chat-transactions] - (missing-chat-transactions cofx))})) - -(defn run-update [{{:keys [network network-status web3] :as db} :db}] - (when (not= network-status :offline) - (let [network (get-in db [:account/account :networks network]) - chain (ethereum/network->chain-keyword network)] - (when-not (= :custom chain) - (let [all-tokens (tokens/tokens-for chain) - token-addresses (map :address all-tokens)] - (log/debug "Syncing transactions data..") - {:get-transactions {:account-id (get-in db [:account/account :address]) - :token-addresses token-addresses - :chain chain - :web3 web3 - :success-event :update-transactions-success - :error-event :update-transactions-fail} - :db (-> db - (update-in [:wallet :errors] dissoc :transactions-update) - (assoc-in [:wallet :transactions-loading?] true) - (assoc-in [:wallet :transactions-last-updated-at] (time/timestamp)))}))))) - -(defn- time-to-sync? [cofx] - (let [last-updated-at (get-in cofx [:db :wallet :transactions-last-updated-at])] - (or (nil? last-updated-at) - (< sync-interval-ms - (- (time/timestamp) last-updated-at))))) - -(defn set-sync-started [{:keys [db]}] - {:db (assoc-in db [:wallet :transactions-sync-started?] true)}) - -; Fetch updated data for any unconfirmed transactions or incoming chat transactions missing in wallet -; and schedule new recurring sync request -(defn sync [{:keys [db] :as cofx}] - (let [in-progress? (get-in db [:wallet :transactions-loading?]) - {:keys [app-state network-status]} db] - (if (and (not= network-status :offline) - (= app-state "active") - (not in-progress?) - (time-to-sync? cofx) - (or (have-unconfirmed-transactions? cofx) - (have-missing-chat-transactions? cofx))) - (handlers-macro/merge-fx cofx - (run-update) - (schedule-sync)) - (schedule-sync cofx)))) diff --git a/src/test_helpers/component.clj b/src/test_helpers/component.clj new file mode 100644 index 00000000000..f5223b60e53 --- /dev/null +++ b/src/test_helpers/component.clj @@ -0,0 +1,57 @@ +(ns test-helpers.component (:refer-clojure :exclude [test])) + +(defmacro describe + [description & body] + `(js/global.describe + ~description + (fn [] + ~@body + ;; We need to return 'undefined', otherwise Jest gives a + ;; warning: "Describe callback must not return a value". + js/undefined))) + +(defmacro describe-skip + [description & body] + `(js/global.describe.skip + ~description + (fn [] + ~@body + js/undefined))) + +(defmacro describe-only + [description & body] + `(js/global.describe.only + ~description + (fn [] + ~@body + js/undefined))) + +(defmacro test + [description & body] + `(js/global.test + ~description + (fn [] ~@body))) + +(defmacro test-skip + [description & body] + `(js/global.test.skip + ~description + (fn [] ~@body))) + +(defmacro test-only + [description & body] + `(js/global.test.only + ~description + (fn [] ~@body))) + +(defmacro before-each + [description & body] + `(js/beforeEach + ~description + (fn [] ~@body))) + +(defmacro after-each + [description & body] + `(js/afterEach + ~description + (fn [] ~@body))) diff --git a/src/test_helpers/component.cljs b/src/test_helpers/component.cljs new file mode 100644 index 00000000000..f075f5d655c --- /dev/null +++ b/src/test_helpers/component.cljs @@ -0,0 +1,247 @@ +(ns test-helpers.component + "Helpers for writing component tests using React Native Testing Library." + (:require-macros [test-helpers.component]) + (:require ["@testing-library/react-native" :as rtl] + [camel-snake-kebab.core :as camel-snake-kebab] + [oops.core :as oops] + [quo.context] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [utils.i18n :as i18n])) + +;;;; React Native Testing Library + +(defn- with-node-or-screen + "Wrap RN Testing Library `method-name` and call it either on a ReactTestInstance + or directly on the screen object. + + `method-name` can be either the name of the native method, or a kebab case + keyword. + + It is often necessary in REPL sessions to call methods on the returned + instance of RNTL `render` method, otherwise you can get weird errors, like + 'Unable to find node on an unmounted component'. This happens because RNTL was + mainly conceptualized to run inside a test runner that automatically cleans up + everything after each test. + + Usage: + (def get-by-text (wrap-screen-or-node :get-by-text)) + + In another file, and with the REPL running with the Shadow-CLJS `:mobile` + target: + + (comment + ;; Consider using a shorter var name when playing in a REPL. + (def component (h/render [quo/counter {} 50])) + (h/get-by-text component \"50\") + + ;; Or without the node it works too, but it is only reliable inside + ;; a test runner. + (h/get-by-text \"50\")) + " + [method-name] + (let [method-name (camel-snake-kebab/->camelCaseString method-name)] + (fn [& args] + (if (= js/Object (type (first args))) ; Check if it's a node instance. + (let [method (aget (first args) method-name)] + (apply method (clj->js (rest args)))) + (let [method (aget rtl/screen method-name)] + (apply method (clj->js args))))))) + +(defn render + [component] + (rtl/render (reagent/as-element component))) + +(defn render-with-theme-provider + ([component] + (render-with-theme-provider component :light)) + ([component theme] + (rtl/render (reagent/as-element [quo.context/provider {:theme theme} component])))) + +(defn render-with-context-provider + ([component] + (render-with-theme-provider component :light)) + ([component context] + (rtl/render (reagent/as-element [quo.context/provider context component])))) + +(defn wait-for + ([condition] (wait-for condition {})) + ([condition options] + (rtl/waitFor condition (clj->js options)))) + +(defn fire-event + ([event-name node] + (fire-event event-name node nil)) + ([event-name node data] + (rtl/fireEvent + node + (camel-snake-kebab/->camelCaseString event-name) + (clj->js data)))) + +;;; Queries: find-* +;; +;; find-* functions don't work in the REPL because the returned promise is +;; always rejected with ReferenceError: Can't find variable: MessageChannel +;; +;; For this reason, find-* functions only work within the Jest runtime, hence +;; using the wrapper function `with-node-or-screen` is unnecessary. + +;;; Queries that work with a REPL and with Jest + +(def get-by-text (with-node-or-screen :get-by-text)) +(def query-all-by-text (with-node-or-screen :query-all-by-text)) +(def query-by-text (with-node-or-screen :query-by-text)) +(def query-first-by-text (comp first query-all-by-text)) + +(def get-all-by-label-text (with-node-or-screen :get-all-by-label-text)) +(def get-by-label-text (with-node-or-screen :get-by-label-text)) +(def query-all-by-label-text (with-node-or-screen :query-all-by-label-text)) +(def query-by-label-text (with-node-or-screen :query-by-label-text)) + +(def get-by-test-id (with-node-or-screen :get-by-test-id)) +(defn get-by-translation-text + ([translation] + (get-by-translation-text rtl/screen translation nil)) + ([translation translation-opts] + (get-by-translation-text rtl/screen translation translation-opts)) + ([^js node translation translation-opts & args] + (apply (with-node-or-screen :get-by-text) + node + (i18n/label translation translation-opts) + args))) + +(defn query-by-translation-text + ([translation] + (query-by-translation-text rtl/screen translation nil)) + ([translation translation-opts] + (query-by-translation-text rtl/screen translation translation-opts)) + ([^js node translation translation-opts & args] + (apply (with-node-or-screen :query-by-text) + node + (i18n/label translation translation-opts) + args))) + +;;; Jest utilities + +(def ^:private jest? + (exists? js/jest)) + +(defn expect + [match] + (js/expect match)) + +(defn use-fake-timers + [] + (when jest? + (js/jest.useFakeTimers))) + +(defn clear-all-timers + [] + (when jest? + (js/jest.clearAllTimers))) + +(defn use-real-timers + [] + (when jest? + (js/jest.useRealTimers))) + +(defn advance-timers-by-time + [time-ms] + (when jest? + (js/jest.advanceTimersByTime time-ms))) + +(def mock-fn + (when jest? + js/jest.fn)) + +(defn is-truthy + [element] + (.toBeTruthy (js/expect element))) + +(defn is-falsy + [element] + (.toBeFalsy (js/expect element))) + +(defn is-null + [element] + (.toBeNull (js/expect element))) + +(defn is-disabled + [element] + (.toBeDisabled (js/expect element))) + +(defn is-equal + [element-1 element-2] + (.toBe (js/expect element-1) element-2)) + +(defn was-called + [mock] + (.toHaveBeenCalled (js/expect mock))) + +(defn was-called-with + [mock expected-arg] + (.toHaveBeenCalledWith (js/expect mock) expected-arg)) + +(defn was-called-times + [^js mock number-of-times] + (.toHaveBeenCalledTimes (js/expect mock) number-of-times)) + +(defn was-not-called + [mock] + (was-called-times mock 0)) + +(defn has-style + [mock styles] + (.toHaveStyle (js/expect mock) (clj->js styles))) + +(defn has-prop + ([element prop] (has-prop element prop js/undefined)) + ([element prop value] + (.toHaveProp (js/expect element) (camel-snake-kebab/->camelCaseString prop) value))) + +(defn get-rerender-fn + "Returns a rerender function from React Native Testing Library. + Takes a JS Object representing a component and returns a function that accepts hiccup + representing the component to rerender with the new props to rerender it. + e.g. + (let [rerender-fn (h/get-rerender-fn + (h/render [my-component {:prop-1 :A + :prop-2 :B}]))] + ;; Rerenders `my-component` with the new props + (rerender-fn [my-component {:prop-1 :new-A + :prop-2 :new-B}])) + " + [component] + (fn [component-updated] + (let [rerender-fn (oops/oget component "rerender") + react-element (reagent/as-element component-updated)] + (rerender-fn react-element)))) + +(defn setup-subs + "Registers `subscriptions`, a map of key (sub ID) to value (sub computation)." + [subscriptions] + (doseq [[sub-id v] subscriptions] + (re-frame/reg-sub sub-id + (fn [_] v)))) + +(defn setup-restorable-re-frame + [] + (let [restorer (atom nil)] + (js/beforeEach + (fn [] + (reset! restorer (re-frame/make-restore-fn)))) + + (js/afterEach + (fn [] + (@restorer))))) + +(defn setup-fake-timers + [] + (js/beforeEach + (fn [] + (use-fake-timers))) + + (js/afterEach + (fn [] + (clear-all-timers) + (use-real-timers)))) diff --git a/src/test_helpers/component_tests_preload.cljs b/src/test_helpers/component_tests_preload.cljs new file mode 100644 index 00000000000..72702c8d573 --- /dev/null +++ b/src/test_helpers/component_tests_preload.cljs @@ -0,0 +1,25 @@ +(ns test-helpers.component-tests-preload + {:dev/always true} + (:require + ;; NOTE: Do NOT sort i18n-resources because it MUST be loaded first. + [status-im.setup.i18n-resources :as i18n-resources] + ;; NOTE: reagent.config Must be before any component definition, so they take the config + ;; properly + #_{:clj-kondo/ignore [:unsorted-required-namespaces]} + reagent.config + [status-im.setup.interceptors :as interceptors] + [utils.i18n :as i18n])) + +(defn- setup + "Prerequisites to run some component tests, for example, the ones in + `status-im.contexts.wallet.send.input-amount.component-spec`. + + Because of the way Jest and ShadowCLJS are set up, this is a preload file that + should never be directly required. However, it will be loaded automatically + before any component test runs." + [] + (interceptors/register-global-interceptors) + (i18n/set-language "en") + (i18n-resources/load-language "en")) + +(setup) diff --git a/src/test_helpers/integration.cljs b/src/test_helpers/integration.cljs new file mode 100644 index 00000000000..b1ab275e897 --- /dev/null +++ b/src/test_helpers/integration.cljs @@ -0,0 +1,274 @@ +(ns test-helpers.integration + (:require + [cljs.test :refer [is] :as test] + legacy.status-im.events + legacy.status-im.subs.root + [native-module.core :as native-module] + [promesa.core :as promesa] + [re-frame.core :as rf] + status-im.events + status-im.navigation.core + status-im.subs.root + [taoensso.timbre :as log] + [tests.integration-test.constants :as constants] + [tests.test-utils :as test-utils] + [utils.collection :as collection] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(defn validate-mnemonic + [mnemonic] + (promesa/create + (fn [p-resolve p-reject] + (native-module/validate-mnemonic + (security/safe-unmask-data mnemonic) + (fn [result] + (let [{:keys [error keyUID]} (transforms/json->clj result)] + (if (seq error) + (p-reject error) + (p-resolve [mnemonic keyUID])))))))) + +(def default-re-frame-wait-for-timeout-ms + "Controls the maximum time allowed to wait for all events to be processed by + re-frame on every call to `wait-for`. + + Take into consideration that some endpoints/signals may take significantly + more time to finish/arrive." + (* 10 1000)) + +(def default-integration-test-timeout-ms + "Use a high-enough value in milliseconds to timeout integration tests. Not too + small, which would cause sporadic failures, and not too high as to make you + sleepy." + (* 60 1000)) + +(defn create-multiaccount! + [] + (rf/dispatch [:profile.create/create-and-login + {:display-name constants/account-name :password constants/password :color "blue"}])) + +(defn app-initialized + [] + (let [app-state @(rf/subscribe [:app-state])] + (= "active" app-state))) + +(defn assert-app-initialized + [] + (is (app-initialized))) + +(defn messenger-started + [] + @(rf/subscribe [:messenger/started?])) + +(defn wallet-loaded? + [] + (not @(rf/subscribe [:wallet/home-tokens-loading?]))) + +(defn assert-messenger-started + [] + (is (messenger-started))) + +(defn assert-wallet-loaded + [] + (is (wallet-loaded?))) + +(defn logout + [] + (log/info "==== before dispatch logout ====") + (rf/dispatch [:profile/logout])) + +(defn log-headline + [test-name] + (log/info (str "==== " test-name " ===="))) + +(defn wait-for + "Returns a promise that resolves when all `event-ids` are processed by re-frame, + otherwise rejects after `timeout-ms`. + + If an event ID that is expected in `event-ids` occurs in a different order, + the promise will be rejected." + ([event-ids] + (wait-for event-ids default-re-frame-wait-for-timeout-ms)) + ([event-ids timeout-ms] + (let [waiting-ids (atom event-ids)] + (promesa/create + (fn [promise-resolve promise-reject] + (let [cb-id (gensym "post-event-callback") + timer-id (js/setTimeout (fn [] + (rf/remove-post-event-callback cb-id) + (promise-reject (ex-info + "timed out waiting for all event-ids to run" + {:event-ids event-ids + :waiting-ids @waiting-ids + :timeout-ms timeout-ms} + ::timeout))) + timeout-ms)] + (log/info "==== inside wait-for after let block ====") + (rf/add-post-event-callback + cb-id + (fn [[event-id & _]] + (when-let [idx (collection/first-index #(= % event-id) @waiting-ids)] + ;; All `event-ids` should be processed in their original order. + (if (zero? idx) + (do + (swap! waiting-ids rest) + ;; When there's nothing else to wait for, clean up resources. + (when (empty? @waiting-ids) + (js/clearTimeout timer-id) + (rf/remove-post-event-callback cb-id) + (promise-resolve))) + (do + (js/clearTimeout timer-id) + (rf/remove-post-event-callback cb-id) + (promise-reject (ex-info "event happened in unexpected order" + {:event-ids event-ids + :waiting-for @waiting-ids} + ::out-of-order-event-id))))))))))))) + +(defn setup-app + [] + (test-utils/init!) + (if (app-initialized) + (promesa/resolved ::app-initialized) + (promesa/do! + (rf/dispatch [:app-started]) + (wait-for [:profile/get-profiles-overview-success])))) + +(defn setup-account + [] + (if (messenger-started) + (promesa/resolved ::messenger-started) + (promesa/do! + (create-multiaccount!) + (promesa/then (wait-for [:profile.login/messenger-started]) #(assert-messenger-started))))) + +(defn- recover-and-login + [seed-phrase] + (rf/dispatch [:profile.recover/recover-and-login + {:display-name (:name constants/recovery-account) + :seed-phrase seed-phrase + :password constants/password + :color "blue"}])) + +(defn enable-testnet! + [] + (rf/dispatch [:profile.settings/profile-update :test-networks-enabled? true {}]) + (rf/dispatch [:wallet/initialize])) + +(defn- recover-multiaccount! + [] + (promesa/let [masked-seed-phrase (security/mask-data (:seed-phrase constants/recovery-account)) + [mnemonic key-uid] (validate-mnemonic masked-seed-phrase)] + (rf/dispatch [:onboarding/seed-phrase-validated (security/mask-data mnemonic) key-uid]) + (rf/dispatch [:pop-to-root :screen/profile.profiles]) + (rf/dispatch [:profile/profile-selected key-uid]) + (recover-and-login mnemonic))) + +(defn setup-recovered-account + [] + (if (messenger-started) + (promesa/resolved ::messenger-started) + (promesa/do! + (recover-multiaccount!) + (promesa/then (wait-for [:profile.login/messenger-started]) #(assert-messenger-started)) + (enable-testnet!) + (promesa/then (wait-for [:wallet/store-wallet-token]) #(assert-wallet-loaded))))) + +(defn test-async + "Runs `f` inside `cljs.test/async` macro in a restorable re-frame checkpoint. + + `f` will be called with one argument, the `done` function exposed by the + `cljs.test/async` macro. Normally, you don't need to use `done`, but you can + call it if you want to early-terminate the current test, so that the test + runner can execute the next one. + + Option `fail-fast?`, when truthy (defaults to true), will force the test + runner to terminate on any test failure. Setting it to false can be useful + during development when you want the rest of the test suite to run due to a + flaky test. Prefer to fail fast in the CI to save on time & resources. + + When `fail-fast?` is falsey, re-frame's state is automatically restored after + a test failure, so that the next integration test can run from a pristine + state. + + Option `timeout-ms` controls the total time allowed to run `f`. The value + should be high enough to account for some variability, otherwise the test may + fail more often. + " + ([test-name f] + (test-async + test-name + {:fail-fast? true + :timeout-ms default-integration-test-timeout-ms} + f)) + ([test-name {:keys [fail-fast? timeout-ms]} f] + (test/async + done + (let [restore-fn (rf/make-restore-fn)] + (log-headline test-name) + (-> (promesa/do (f done)) + (promesa/timeout timeout-ms) + (promesa/catch (fn [error] + (is (nil? error)) + (when fail-fast? + (js/process.exit 1)))) + (promesa/finally (fn [] + (restore-fn) + (done)))))))) + +(defn test-app-initialization + [] + (test-async ::initialize-app + (fn [] + (promesa/do + (test-utils/init!) + (rf/dispatch [:app-started]) + ;; Use initialize-view because it has the longest avg. time and is + ;; dispatched by initialize-multiaccounts (last non-view event). + (wait-for [:profile/get-profiles-overview-success + :font/init-font-file-for-initials-avatar]) + (assert-app-initialized))))) + +(defn test-account-creation + [] + (test-async ::create-account + (fn [] + (promesa/do + (setup-app) + (setup-account) + (logout) + (log/info "==== before wait-for logout ====") + (wait-for [:profile.logout/reset-state]) + (log/info "==== after wait-for logout ===="))))) + +;;;; Fixtures + +(defn fixture-session + "Fixture to set up the application and a logged account before the test runs. + Log out after the test is done. + + Usage: + + (use-fixtures :each (h/fixture-logged))" + ([type] + {:before (if (= :recovered-account type) + (fn [] + (test/async done + (promesa/do (setup-app) + (setup-recovered-account) + (done)))) + (fn [] + (test/async done + (promesa/do (setup-app) + (setup-account) + (done))))) + :after (fn [] + (test/async done + (promesa/do (logout) + (log/info "==== before wait-for logout ====") + (wait-for [:profile.logout/reset-state]) + (log/info "==== after wait-for logout ====") + (done))))}) + ([] (fixture-session [:new-account]))) + + diff --git a/src/test_helpers/matchers.clj b/src/test_helpers/matchers.clj new file mode 100644 index 00000000000..24693f486e3 --- /dev/null +++ b/src/test_helpers/matchers.clj @@ -0,0 +1,47 @@ +(ns test-helpers.matchers + "Internal use. Don't require it directly." + (:require + [cljs.test :as test] + [matcher-combinators.core :as core] + [matcher-combinators.matchers :as matchers] + [matcher-combinators.parser] + [matcher-combinators.result :as result])) + +;; This implementation is identical to `match?`, but wraps the expected value +;; with `nested-equals`. This differs from the default `embeds` matcher on maps, +;; where extra map keys are considered valid. +(defmethod test/assert-expr 'match-strict? + [_ msg form] + `(let [args# (list ~@(rest form)) + [matcher# actual#] args#] + (cond + (not (= 2 (count args#))) + (test/do-report + {:type :fail + :message ~msg + :expected (symbol "`match-strict?` expects 2 arguments: a `matcher` and the `actual`") + :actual (symbol (str (count args#) " were provided: " '~form))}) + + (core/matcher? matcher#) + (let [result# (core/match (matchers/nested-equals matcher#) actual#)] + (test/do-report + (if (core/indicates-match? result#) + {:type :pass + :message ~msg + :expected '~form + :actual (list 'match? matcher# actual#)} + (with-file+line-info + {:type :fail + :message ~msg + :expected '~form + :actual (tagged-for-pretty-printing (list '~'not (list 'match? matcher# actual#)) + result#) + :markup (::result/value result#)})))) + + :else + (test/do-report + {:type :fail + :message ~msg + :expected (str "The first argument of `match-strict?` " + "needs to be a matcher (implement the match protocol)") + :actual '~form})))) diff --git a/src/test_helpers/matchers.cljs b/src/test_helpers/matchers.cljs new file mode 100644 index 00000000000..f294052f816 --- /dev/null +++ b/src/test_helpers/matchers.cljs @@ -0,0 +1,28 @@ +(ns test-helpers.matchers + "Some vars in this namespace solely exist to support the matchers.clj file." + (:require-macros test-helpers.matchers) + (:require + [cljs.test :as t] + [matcher-combinators.parser] + [matcher-combinators.printer :as printer] + [matcher-combinators.result :as result])) + +(defrecord Mismatch [summary match-result]) + +;; used in matchers.clj +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn tagged-for-pretty-printing + [actual-summary result] + (->Mismatch actual-summary result)) + +(extend-protocol IPrintWithWriter + Mismatch + (-pr-writer [this writer _] + (-write writer (printer/as-string (-> this :match-result ::result/value))))) + +;; used in matchers.clj +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn with-file+line-info + [report] + (merge (t/file-and-line (js/Error.) 4) + report)) diff --git a/src/test_helpers/unit.clj b/src/test_helpers/unit.clj new file mode 100644 index 00000000000..2fe0438c5fe --- /dev/null +++ b/src/test_helpers/unit.clj @@ -0,0 +1,153 @@ +(ns test-helpers.unit + (:require + [clojure.spec.alpha :as s] + [clojure.string :as string] + [clojure.walk])) + +(defn- keyword->test-name + [sub-name] + (->> [(namespace sub-name) + (name sub-name) + "test"] + (remove nil?) + (map #(string/replace % #"\." "-")) + (string/join "-"))) + +(defmacro ^:private testing-restorable-app-db + [description & body] + `(cljs.test/testing ~description + (restore-app-db (fn [] ~@body)))) + +(s/fdef deftest-sub + :args (s/cat :sub-name keyword? + :args (s/coll-of symbol? :count 1) + :body (s/* any?))) + +(defmacro deftest-sub + "Defines a test based on `sub-name`, executes `body` and restores the app db. + + Any usage of the `cljs.test/testing` macro inside `body` will be modified to + also make sure the app db is restored and the subscription cache is reset. + + Expressions in `body` will have access to `sub-name`, which should be used to + avoid needlessly repeating the subscription name. + + Example: + + ```clojure + (require '[test-helpers.unit :as h]) + + (h/deftest-sub :wallet-legacy/sorted-tokens + [sub-name] + (testing \"sorts tokens by name, lowercased\" + ;; Arrange + (swap! rf-db/app-db assoc-in [] ) + + ;; Act and Assert + (is (= (rf/sub [sub-name]))))) + ```" + [sub-name bindings & body] + `(let [sub-name# ~sub-name] + (cljs.test/deftest ~(symbol (keyword->test-name sub-name)) + (let [~bindings [sub-name#]] + (restore-app-db + (fn [] + ;; Do not warn about subscriptions being used in non-reactive contexts. + (with-redefs [re-frame.interop/debug-enabled? false] + ~@(clojure.walk/postwalk-replace + {'cljs.test/testing `testing-restorable-app-db + 'testing `testing-restorable-app-db} + body)))))))) + +(defmacro ^:private event-dispatcher + "Returns an s-exp that can build an event dispatcher given an event vector." + [] + `(fn [event-v#] + (let [event-id# (first event-v#) + all-interceptors# (re-frame.registrar/get-handler :event event-id#) + interceptors-without-fx# (remove #(= :do-fx (:id %)) all-interceptors#)] + (assert (seq all-interceptors#) + (str "Event does not exist '" event-id# "'")) + (:effects (re-frame.interceptor/execute event-v# interceptors-without-fx#))))) + +(defmacro deftest-event + "Defines a test var for an event `event-id`. + + This macro primarily exists to facilitate testing anonymous event handlers + directly, without the need to extract them to vars. + + Similar to `deftest-sub`, this macro uses the re-frame machinery. + Consequently, the test body is no longer guaranteed to be pure, as all + interceptors will run (except for the standard `:do-fx`). + + Within the test, we can directly mutate `re-frame.db/app-db` atom to set up + test data. Upon test completion, it is guaranteed that the app-db will be + restored to its original state. + + Any usage of the `cljs.test/testing` macro within `body` will be modified to + ensure the app-db is restored. + + The macro offers two bindings, namely `event-id` and `dispatch`. The + `dispatch` function is similar to `re-frame.core/dispatch`, but without + executing effects. It returns the map of effects we can assert on and it's + synchronous. + + Example: + + ```clojure + (ns events-test + (:require [test-helpers.unit :as h])) + + (h/deftest-event :wallet/dummy-event + [event-id dispatch] + (let [expected {:db {:a false}}] + ;; Arrange + (swap! rf-db/app-db {:a true}) + + ;; Act and Assert + (is (match? expected (dispatch [event-id arg1 arg2]))))) + ``` + " + [event-id bindings & body] + `(let [event-id# ~event-id] + (cljs.test/deftest ~(symbol (keyword->test-name event-id)) + (let [dispatcher# (event-dispatcher) + ~bindings [event-id# dispatcher#]] + (restore-app-db + (fn [] + (with-redefs [re-frame.interop/debug-enabled? false] + ~@(clojure.walk/postwalk-replace + {'cljs.test/testing `testing-restorable-app-db + 'testing `testing-restorable-app-db} + body)))))))) + +(defmacro use-log-fixture + "Register log fixture which allows inspecting all calls to `taoensso.timbre/log`. + + Usage: Simply call this macro once per test namespace, and use the + `test-helpers.unit/logs` atom to deref the collection of all logs for the + test under execution. + + In Clojure(Script), we can rely on fixtures for each `cljs.deftest`, but not + for individual `cljs.testing` macro calls. So keep that in mind when testing + for log messages." + [] + `(cljs.test/use-fixtures + :each + {:before test-helpers.unit/log-fixture-before + :after test-helpers.unit/log-fixture-after})) + +(defmacro run-test-sync + "Wrap around `re-frame.test/run-test-sync` to make it work with our aliased + function `utils.re-frame/dispatch`. + + This macro is a must, because the original implementation uses `with-redefs` + pointing to the original re-frame `dispatch` function, but our event handlers + are dispatching using our aliased function. + + If tests run using the original `run-test-sync`, then all bets are off, and + tests start to behave erratically." + [& body] + `(day8.re-frame.test/run-test-sync + (with-redefs [utils.re-frame/dispatch re-frame.core/dispatch] + ~@body))) diff --git a/src/test_helpers/unit.cljs b/src/test_helpers/unit.cljs new file mode 100644 index 00000000000..b8e27228c67 --- /dev/null +++ b/src/test_helpers/unit.cljs @@ -0,0 +1,67 @@ +(ns test-helpers.unit + "Utilities for simplifying the process of writing tests and improving test + readability. + + Avoid coupling this namespace with particularities of the Status' domain, thus + prefer to use it for more general purpose concepts, such as the re-frame event + layer." + (:require-macros test-helpers.unit) + ;; We must require test-helpers.matchers namespace to register the custom cljs.test directive + ;; `match-strict?` + (:require + [re-frame.db :as rf-db] + [re-frame.subs :as rf-subs] + [taoensso.timbre :as log] + test-helpers.matchers)) +;; We must require `test-helpers.matchers` this namespace to register the custom cljs.test +;; directive `match-strict?`. + + +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} ;; used in unit.clj +(defn restore-app-db + "Saves current app DB, calls `f` and restores the original app DB. + + Always clears the subscription cache after calling `f`." + [f] + (rf-subs/clear-subscription-cache!) + (let [original-db @rf-db/app-db] + (try + (f) + (finally + (reset! rf-db/app-db original-db))))) + +;;;; Log fixture + +(def ^:private original-log-config + (atom nil)) + +(def logs + "The collection of all logs registered by `test-log-appender`. Tests can + de-reference it and verify log messages and their respective levels." + (atom [])) + +(defn- test-log-appender + "Custom log appender that persists all `taoensso.timbre/log` call arguments." + [{:keys [vargs level]}] + (swap! logs conj {:args vargs :level level})) + +#_{:clj-kondo/ignore [:unused-private-var]} +(defn- log-fixture-before + [] + #_{:clj-kondo/ignore [:unresolved-var]} + (reset! original-log-config log/*config*) + + ;; We reset the logs *before* running tests instead of *after* because: 1. + ;; It's just as reliable; 2. It helps when using the REPL, because we can + ;; easily inspect `logs` after a test has finished. + (reset! logs []) + + (log/swap-config! assoc-in + [:appenders :test] + {:enabled? true + :fn test-log-appender})) + +#_{:clj-kondo/ignore [:unused-private-var]} +(defn- log-fixture-after + [] + (log/set-config! @original-log-config)) diff --git a/src/tests/contract_test/core_test.cljs b/src/tests/contract_test/core_test.cljs new file mode 100644 index 00000000000..0a73795a785 --- /dev/null +++ b/src/tests/contract_test/core_test.cljs @@ -0,0 +1,15 @@ +(ns tests.contract-test.core-test + (:require + [cljs.test :refer [deftest]] + legacy.status-im.events + legacy.status-im.subs.root + status-im.events + status-im.navigation.core + status-im.subs.root + [test-helpers.integration :as h])) + +(deftest initialize-app-test + (h/test-app-initialization)) + +(deftest create-account-test + (h/test-account-creation)) diff --git a/src/tests/contract_test/profile_test.cljs b/src/tests/contract_test/profile_test.cljs new file mode 100644 index 00000000000..46616d78e43 --- /dev/null +++ b/src/tests/contract_test/profile_test.cljs @@ -0,0 +1,19 @@ +(ns tests.contract-test.profile-test + (:require + [cljs.test :refer [deftest is use-fixtures]] + legacy.status-im.events + legacy.status-im.subs.root + [promesa.core :as promesa] + [status-im.common.json-rpc.events :as rpc-events] + status-im.events + status-im.navigation.core + status-im.subs.root + [test-helpers.integration :as h])) + +(use-fixtures :each (h/fixture-session)) + +(deftest profile-set-bio-contract-test + (h/test-async :contract/wakuext_setBio + (fn [] + (-> (rpc-events/call-async "wakuext_setBio" false "new bio") + (promesa/catch #(is (nil? %) "Set bio RPC call should have succeeded")))))) diff --git a/src/tests/contract_test/utils.cljs b/src/tests/contract_test/utils.cljs new file mode 100644 index 00000000000..56d2a8367c3 --- /dev/null +++ b/src/tests/contract_test/utils.cljs @@ -0,0 +1,11 @@ +(ns tests.contract-test.utils + (:require + [utils.number])) + +(defn get-default-account + [accounts] + (first (filter :wallet accounts))) + +(defn get-default-address + [accounts] + (:address (get-default-account accounts))) diff --git a/src/tests/contract_test/wallet_recovered_account_test.cljs b/src/tests/contract_test/wallet_recovered_account_test.cljs new file mode 100644 index 00000000000..e4e6e7b948a --- /dev/null +++ b/src/tests/contract_test/wallet_recovered_account_test.cljs @@ -0,0 +1,38 @@ +(ns tests.contract-test.wallet-recovered-account-test + (:require + [cljs.test :refer [deftest is use-fixtures]] + legacy.status-im.events + legacy.status-im.subs.root + [native-module.core :as native-module] + [promesa.core :as promesa] + [status-im.common.json-rpc.events :as rpc-events] + status-im.events + status-im.navigation.core + status-im.subs.root + [test-helpers.integration :as h] + [tests.contract-test.utils :as contract-utils] + [tests.integration-test.constants :as integration-constants])) + +(use-fixtures :each (h/fixture-session :recovered-account)) + +(defn assert-derived-account + [response] + (let [{:keys [address public-key]} (first response)] + (is (= integration-constants/derived-address address)) + (is (= (:public-key integration-constants/recovery-account) public-key)) + (is (= integration-constants/derivation-path (:path (first response)))))) + +(deftest wallet-get-derived-addressess-contract-test + (h/test-async :wallet/create-derived-addresses + (fn [] + (promesa/let [sha3-pwd-hash (native-module/sha3 integration-constants/password) + derivation-path [integration-constants/derivation-path] + accounts (rpc-events/call-async "accounts_getAccounts" false) + default-address (contract-utils/get-default-address accounts) + response (rpc-events/call-async + "wallet_getDerivedAddresses" + false + sha3-pwd-hash + default-address + derivation-path)] + (assert-derived-account response))))) diff --git a/src/tests/contract_test/wallet_test.cljs b/src/tests/contract_test/wallet_test.cljs new file mode 100644 index 00000000000..7bb981bb89b --- /dev/null +++ b/src/tests/contract_test/wallet_test.cljs @@ -0,0 +1,111 @@ +(ns tests.contract-test.wallet-test + (:require + [cljs.test :refer [deftest is use-fixtures]] + legacy.status-im.events + legacy.status-im.subs.root + [promesa.core :as promesa] + [status-im.common.emoji-picker.utils :as emoji-picker.utils] + [status-im.common.json-rpc.events :as rpc-events] + [status-im.contexts.wallet.data-store :as data-store] + [status-im.contexts.wallet.networks.config :as networks.config] + status-im.events + status-im.navigation.core + status-im.subs.root + [test-helpers.integration :as h] + [tests.contract-test.utils :as contract-utils])) + +(use-fixtures :each (h/fixture-session)) + +(def ethereum-chain-id networks.config/ethereum-chain-id) +(def mainnet-chain-ids (keys networks.config/mainnets)) +(def testnet-chain-ids (keys networks.config/testnets)) +(def all-chain-ids (keys networks.config/networks)) + +(defn assert-accounts-get-accounts + [result] + (is (true? (some :wallet result))) + (is (true? (some :chat result))) + (is (= 2 (count result)))) + +(deftest accounts-get-accounts-contract-test + (h/test-async :contract/accounts-get-accounts + (fn [] + (promesa/let [result (rpc-events/call-async "accounts_getAccounts" false)] + (assert-accounts-get-accounts result))))) + +(defn check-emoji-is-updated + [test-emoji accounts] + (let [default-account (contract-utils/get-default-account accounts)] + (is (= (:emoji default-account) test-emoji)))) + +(deftest accounts-save-accounts-contract-test + (h/test-async :contract/accounts-save-account + (fn [] + (promesa/let [test-emoji (emoji-picker.utils/random-emoji) + account (rpc-events/call-async "accounts_getAccounts" false) + default-account (contract-utils/get-default-account account) + _ (rpc-events/call-async + "accounts_saveAccount" + false + (data-store/<-account (merge default-account {:emoji test-emoji}))) + accounts (rpc-events/call-async "accounts_getAccounts" false)] + (check-emoji-is-updated test-emoji accounts))))) + +(defn assert-ethereum-chains + [response] + (is (= (count all-chain-ids) (count response))) + (doseq [chain-id mainnet-chain-ids] + (is (some #(= chain-id (get-in % [:Prod :chainId])) response))) + (doseq [chain-id testnet-chain-ids] + (is (some #(= chain-id (get-in % [:Test :chainId])) response)))) + +(deftest accounts-get-chains-contract-test + (h/test-async :contract/wallet_get-ethereum-chains + (fn [] + (promesa/let [response (rpc-events/call-async "wallet_getEthereumChains" false)] + (assert-ethereum-chains response))))) + +(defn assert-wallet-tokens + [tokens] + (let [flattened-tokens (mapcat val tokens)] + (doseq [token flattened-tokens] + (is (not-empty (:symbol token))) + (is (:decimals token)) + (is (contains? token :balancesPerChain)) + (let [balances-per-chain (:balancesPerChain token)] + (doseq [[_ balance] balances-per-chain] + (is (contains? balance :rawBalance)) + (let [raw-balance (:rawBalance balance)] + (is (not-empty raw-balance)) + (is (re-matches #"\d+" raw-balance)))))))) + +(deftest wallet-get-wallet-token-test + (h/test-async :wallet/get-wallet-token + (fn [] + (promesa/let [accounts (rpc-events/call-async "accounts_getAccounts" false) + default-address (contract-utils/get-default-address accounts) + response (rpc-events/call-async + "wallet_fetchOrGetCachedWalletBalances" + false + [default-address] + true)] + (assert-wallet-tokens response))))) + +(defn assert-address-details + [result] + (is (contains? result :address)) + (is (contains? result :path)) + (is (boolean? (:hasActivity result))) + (is (false? (:alreadyCreated result)))) + +(deftest wallet-get-address-details-contract-test + (h/test-async :wallet/get-address-details + (fn [] + (promesa/let [input "test.eth" + chain-id ethereum-chain-id + ens-address (rpc-events/call-async "ens_addressOf" false chain-id input) + response (rpc-events/call-async "wallet_getAddressDetails" + false + chain-id + ens-address)] + (assert-address-details response))))) diff --git a/src/tests/integration_test/chat_test.cljs b/src/tests/integration_test/chat_test.cljs new file mode 100644 index 00000000000..f203f3204aa --- /dev/null +++ b/src/tests/integration_test/chat_test.cljs @@ -0,0 +1,95 @@ +(ns tests.integration-test.chat-test + (:require + [cljs.test :refer [deftest is use-fixtures]] + legacy.status-im.events + legacy.status-im.subs.root + [promesa.core :as promesa] + [re-frame.core :as rf] + [status-im.constants :as constants] + status-im.events + status-im.navigation.core + status-im.subs.root + [test-helpers.integration :as h])) + +(use-fixtures :each (h/fixture-session)) + +(def chat-id + "0x0402905bed83f0bbf993cee8239012ccb1a8bc86907ead834c1e38476a0eda71414eed0e25f525f270592a2eebb01c9119a4ed6429ba114e51f5cb0a28dae1adfd") + +(deftest one-to-one-chat-test + (h/test-async ::one-to-one-chat + (fn [] + (promesa/do + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) + (h/wait-for [:chat/one-to-one-chat-created]) + (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))))))) + +(deftest delete-chat-test + (h/test-async ::delete-chat + (fn [] + (promesa/do + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) + (h/wait-for [:chat/one-to-one-chat-created]) + (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) + (is @(rf/subscribe [:chats/chat chat-id])) + (rf/dispatch-sync [:chat.ui/show-remove-confirmation chat-id]) + (rf/dispatch-sync [:chat.ui/remove-chat chat-id]))))) + +(deftest mute-chat-test + (h/test-async ::mute-chat + (fn [] + (promesa/do + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) + (h/wait-for [:chat/one-to-one-chat-created]) + + (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) + (is @(rf/subscribe [:chats/chat chat-id])) + + (rf/dispatch-sync [:chat.ui/mute chat-id true constants/mute-till-unmuted]) + (h/wait-for [:chat/mute-successfully]) + (is @(rf/subscribe [:chats/muted chat-id])) + + (rf/dispatch-sync [:chat.ui/mute chat-id false]) + (h/wait-for [:chat/mute-successfully]) + (is (not @(rf/subscribe [:chats/muted chat-id]))))))) + +(deftest undo-mute-chat-test + (h/test-async ::undo-mute-chat + (fn [] + (promesa/do + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) + (h/wait-for [:chat/one-to-one-chat-created]) + (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) + + (rf/dispatch-sync [:chat.ui/mute chat-id true constants/mute-till-unmuted]) + (h/wait-for [:chat/mute-successfully :toasts/upsert]) + (is @(rf/subscribe [:chats/muted chat-id])) + + (rf/dispatch-sync [:chat.ui/undo-mute chat-id]) + (h/wait-for [:chat/undo-mute-success :toasts/close]) + (is (not @(rf/subscribe [:chats/muted chat-id]))))))) + +(deftest add-contact-test + (h/test-async ::add-contact + (fn [] + (let [compressed-key "zQ3shMwgSMKHVznoowceZMxWde9HUnkQEVSGvvex8UFpFNErL" + public-key (str "0x0407e9dc435fe366cb0b4c4f35cbd925438c0f46fe0" + "ed2a86050325bc8856e26898c17e31dee2602b9429c91" + "ecf65a41d62ac1f2f0823c0710dcb536e79af2763c") + primary-name "zQ3...pFNErL"] + (promesa/do + ;; Search for contact using compressed key + (rf/dispatch [:contacts/set-new-identity {:input compressed-key}]) + (h/wait-for [:contacts/set-new-identity-success]) + (let [new-identity @(rf/subscribe [:contacts/new-identity])] + (is (= public-key (:public-key new-identity))) + (is (= :valid (:state new-identity)))) + + ;; Click 'view profile' button + (rf/dispatch [:chat.ui/show-profile public-key]) + (h/wait-for [:contacts/build-contact :contacts/build-contact-success]) + (let [contact @(rf/subscribe [:contacts/current-contact])] + (is (= primary-name (:primary-name contact))))))))) diff --git a/src/tests/integration_test/constants.cljs b/src/tests/integration_test/constants.cljs new file mode 100644 index 00000000000..7142f8760c4 --- /dev/null +++ b/src/tests/integration_test/constants.cljs @@ -0,0 +1,16 @@ +(ns tests.integration-test.constants) + +(def password "testabc") + +(def account-name "account-abc") + +(def derivation-path "m/43'/60'/1581'/0'/0") + +(def derived-address "0x64829fa755b9fda0c7ece27434970807aecc8c79") + +(def recovery-account + {:name "wallet-abc" + :seed-phrase "destroy end torch puzzle develop note wise island disease chaos kind bus" + :key-uid "0x6827f3d1e21ae723a24e0dcbac1853b5ed4d651c821a2326492ce8f2367ec905" + :public-key + "0x04ca1446090fa458f42a54307fb191daf132d295bd364a691a5434e1b5717e4cf3116e66b1f5188dd18f1b74fe70fe77cff02e5d6c279f3834f47176563393be1c"}) diff --git a/src/tests/integration_test/core_test.cljs b/src/tests/integration_test/core_test.cljs new file mode 100644 index 00000000000..fb43cd9e641 --- /dev/null +++ b/src/tests/integration_test/core_test.cljs @@ -0,0 +1,15 @@ +(ns tests.integration-test.core-test + (:require + [cljs.test :refer [deftest]] + legacy.status-im.events + legacy.status-im.subs.root + status-im.events + status-im.navigation.core + status-im.subs.root + [test-helpers.integration :as h])) + +(deftest initialize-app-test + (h/test-app-initialization)) + +(deftest create-account-test + (h/test-account-creation)) diff --git a/src/tests/integration_test/profile_test.cljs b/src/tests/integration_test/profile_test.cljs new file mode 100644 index 00000000000..05a9cf11d91 --- /dev/null +++ b/src/tests/integration_test/profile_test.cljs @@ -0,0 +1,55 @@ +(ns tests.integration-test.profile-test + (:require + [cljs.test :refer [deftest is use-fixtures]] + [promesa.core :as promesa] + [status-im.contexts.profile.utils :as profile.utils] + [test-helpers.integration :as h] + [tests.test-utils :as test-utils] + [utils.re-frame :as rf])) + +(use-fixtures :each (h/fixture-session)) + +(deftest edit-profile-name-test + (h/test-async ::edit-profile-name + (fn [] + (let [new-name "John Doe"] + (promesa/do + (rf/dispatch [:profile/edit-name {:display-name new-name}]) + (h/wait-for [:navigate-back :toasts/upsert]) + (let [profile (rf/sub [:profile/profile]) + display-name (profile.utils/displayed-name profile)] + (is (= new-name display-name)))))))) + +(deftest edit-profile-picture-test + (h/test-async ::edit-profile-picture + (fn [] + (let [mock-image "resources/images/mock2/monkey.png" + absolute-path (.resolve test-utils/path mock-image)] + (promesa/do + (rf/dispatch [:profile/edit-picture + {:picture absolute-path + :crop-width 80 + :crop-height 80}]) + (h/wait-for [:profile/update-local-picture :toasts/upsert]) + (let [profile (rf/sub [:profile/profile])] + (is (not (nil? (:images profile)))))))))) + +(deftest delete-profile-picture-test + (h/test-async ::delete-profile-picture + (fn [] + (promesa/do + (rf/dispatch [:profile/delete-picture]) + (h/wait-for [:profile/update-local-picture :toasts/upsert]) + (let [profile (rf/sub [:profile/profile])] + (is (nil? (:image profile)))))))) + +(deftest edit-profile-bio-test + (h/test-async ::edit-profile-bio + (fn [] + (let [new-bio "New bio text"] + (promesa/do + (rf/dispatch [:profile/edit-bio new-bio]) + (h/wait-for [:navigate-back :toasts/upsert]) + (let [profile (rf/sub [:profile/profile]) + bio (:bio profile)] + (is (= new-bio bio)))))))) diff --git a/src/tests/integration_test/standard_auth_test.cljs b/src/tests/integration_test/standard_auth_test.cljs new file mode 100644 index 00000000000..ae5ab647a83 --- /dev/null +++ b/src/tests/integration_test/standard_auth_test.cljs @@ -0,0 +1,123 @@ +(ns tests.integration-test.standard-auth-test + (:require + [cljs.test :refer [deftest is testing]] + [day8.re-frame.test :as rf-test] + re-frame.core + [test-helpers.integration :as h] + [utils.re-frame :as rf])) + +(def default-args + {:on-auth-success identity + :on-auth-fail identity + :on-close identity + :auth-button-label "test" + :auth-button-icon-left :test-icon + :blur? false + :theme :light}) + +(defn wait-for-biometric-login + "Sets the biometric :auth-method in the db during login" + [on-done] + (rf/reg-fx :effects.biometric/check-if-available + (fn [{:keys [on-success]}] (on-success "biometric"))) + (rf/reg-fx :keychain/password-hash-migration + (fn [{:keys [callback]}] (callback))) + (rf/reg-event-fx :biometric/authenticate + (fn [_ [{:keys [on-success]}]] (on-success))) + (rf/reg-event-fx :profile.login/biometric-success (fn [_])) + (rf/dispatch [:profile.login/select-profile-and-login-with-biometric-if-available :key-uid-1]) + (rf-test/wait-for [:profile.login/biometric-success] + (on-done))) + +(defn auth-success-fixtures + [] + (rf/reg-fx :effects.standard-auth/on-auth-success + (fn [on-auth-success] (on-auth-success))) + (rf/reg-fx :effects.biometric/check-if-available + (fn [{:keys [on-success]}] (on-success "biometric"))) + (rf/reg-fx :keychain/password-hash-migration + (fn [{:keys [callback]}] (callback))) + (rf/reg-event-fx :biometric/authenticate + (fn [_ [{:keys [on-success]}]] (on-success))) + (rf/reg-fx :keychain/get-user-password + (fn [[_ on-success]] (on-success)))) + +(deftest standard-auth-biometric-authorize-success-test + (testing "calling success callback when completing biometric authentication" + (h/log-headline :standard-auth-authorize-success) + (rf-test/run-test-async + (wait-for-biometric-login + (fn [] + (auth-success-fixtures) + (let [on-success-called? (atom false) + args (assoc default-args :on-auth-success #(reset! on-success-called? true))] + (rf/dispatch [:standard-auth/authorize args]) + (rf-test/wait-for [:standard-auth/on-biometric-success] + (rf-test/wait-for [:standard-auth/finish-auth] + (is @on-success-called?))))))))) + +(defn auth-cancel-fixtures + [] + (rf/reg-fx :effects.biometric/check-if-available + (fn [{:keys [on-success]}] (on-success "biometric"))) + (rf/reg-fx :keychain/password-hash-migration + (fn [{:keys [callback]}] (callback))) + (rf/reg-event-fx :biometric/authenticate + (fn [_ [{:keys [on-cancel]}]] (on-cancel))) + (rf/reg-event-fx :show-bottom-sheet identity)) + +(deftest standard-auth-biometric-authorize-cancel-test + (testing "falling back to password authorization when biometrics canceled" + (h/log-headline :standard-auth-authorize-cancel) + (rf-test/run-test-async + (wait-for-biometric-login + (fn [] + (auth-cancel-fixtures) + (rf/dispatch [:standard-auth/authorize default-args]) + (rf-test/wait-for [:show-bottom-sheet] + (is true))))))) + +(defn auth-fail-fixtures + [expected-error-cause] + (rf/reg-fx :effects.biometric/check-if-available + (fn [{:keys [on-success]}] (on-success "biometric"))) + (rf/reg-fx :keychain/password-hash-migration + (fn [{:keys [callback]}] (callback))) + (rf/reg-event-fx :biometric/authenticate + (fn [_ [{:keys [on-fail]}]] (on-fail (ex-info "error" {} expected-error-cause)))) + (rf/reg-event-fx :biometric/show-message identity)) + +(deftest standard-auth-biometric-authorize-fail-test + (testing "showing biometric error message when authorization failed" + (h/log-headline :standard-auth-authorize-fail) + (rf-test/run-test-async + (let [on-fail-called? (atom false) + expected-error-cause :bad-error + error (atom nil) + args (assoc default-args + :on-auth-fail + (fn [err] + (reset! on-fail-called? true) + (reset! error err)))] + (wait-for-biometric-login + (fn [] + (auth-fail-fixtures expected-error-cause) + (rf/dispatch [:standard-auth/authorize args]) + (rf-test/wait-for [:biometric/show-message] + (is @on-fail-called?) + (is (= expected-error-cause (ex-cause @error)))))))))) + +(defn auth-password-fallback-fixtures + [] + (rf/reg-fx :effects.biometric/check-if-available + (fn [{:keys [on-fail]}] (on-fail))) + (rf/reg-event-fx :show-bottom-sheet identity)) + +(deftest standard-auth-password-authorize-fallback-test + (testing "falling back to password when biometrics is not available" + (h/log-headline :standard-auth-password-authorize-fallback) + (rf-test/run-test-async + (auth-password-fallback-fixtures) + (rf/dispatch [:standard-auth/authorize default-args]) + (rf-test/wait-for [:standard-auth/authorize-with-password] + (is true))))) diff --git a/src/tests/test_utils.cljs b/src/tests/test_utils.cljs new file mode 100644 index 00000000000..f583bd1a1b8 --- /dev/null +++ b/src/tests/test_utils.cljs @@ -0,0 +1,133 @@ +(ns tests.test-utils + (:require + [legacy.status-im.utils.deprecated-types :as types] + [re-frame.core :as re-frame])) + +(def native-status (js/require "../../modules/react-native-status/nodejs/bindings")) + +(def fs (js/require "fs")) +(def path (js/require "path")) +(def os (js/require "os")) + +(def tmpdir (.tmpdir os)) + +(def test-dir-prefix (.join path tmpdir "status-mobile-tests")) + +(def test-dir (.mkdtempSync fs test-dir-prefix)) + +(def initialized? (atom false)) + +(defn signal-received-callback + [a] + (re-frame/dispatch [:signals/signal-received a])) + +;; We poll for signals, could not get callback working +(defn init! + [] + (when-not @initialized? + (.setSignalEventCallback native-status) + (reset! initialized? true) + (js/setInterval (fn [] + (.pollSignal native-status signal-received-callback) + 100)))) + +(def ui-helper + (clj->js + {:clearCookies identity + :clearStorageAPIs identity})) + +(def encryption-utils + (clj->js + {:sha3 + (fn [s] (.sha3 native-status s)) + :serializeLegacyKey + (fn [s] (.serializeLegacyKey native-status s)) + :setBlankPreviewFlag + identity + :hexToNumber + (fn [hex] (.hexToNumber native-status hex)) + :decodeParameters + (fn [decode-param-json] + (.decodeParameters native-status decode-param-json)) + :numberToHex + (fn [num-str] (.numberToHex native-status num-str)) + :initKeystore + (fn [key-uid callback] + (callback (.initKeystore native-status + (str test-dir "/keystore/" key-uid)))) + :multiformatDeserializePublicKey + (fn [public-key deserialization-key callback] + (callback (.multiformatDeserializePublicKey + native-status + public-key + deserialization-key)))})) + +(def account-manager + (clj->js + {:initializeApplication + (fn [request callback] + (callback (.initializeApplication native-status request))) + :acceptTerms + (fn [callback] + (callback (.acceptTerms native-status))) + :createAccountAndLogin + (fn [request] (.createAccountAndLogin native-status request)) + :restoreAccountAndLogin + (fn [request] + (prn native-status) + (.restoreAccountAndLogin native-status request)) + :loginAccount + (fn [request] (.loginAccount native-status request)) + :logout + (fn [] (.logout native-status)) + :multiAccountImportMnemonic + (fn [json callback] + (callback (.multiAccountImportMnemonic native-status json))) + :multiAccountLoadAccount + (fn [json callback] + (callback (.multiAccountLoadAccount native-status json))) + :multiAccountDeriveAddresses + (fn [json callback] + (callback (.multiAccountDeriveAddresses native-status json))) + :multiAccountGenerateAndDeriveAddresses + (fn [json callback] + (callback (.multiAccountGenerateAndDeriveAddresses native-status json))) + :multiAccountStoreDerived + (fn [json callback] + (callback (.multiAccountStoreDerivedAccounts native-status json)))})) + +(def utils + (clj->js + {:backupDisabledDataDir + (fn [] (str test-dir "/backup")) + :keystoreDir (fn [] "") + :toChecksumAddress + (fn [address] (.toChecksumAddress native-status address)) + :checkAddressChecksum + (fn [address] (.checkAddressChecksum native-status address)) + :validateMnemonic + (fn [json callback] (callback (.validateMnemonic native-status json))) + :isAddress + (fn [address] (.isAddress native-status address)) + :getMMKVStoragePath + (fn [] (str test-dir "/mmkv"))})) + +(def log-manager + (clj->js + {:logFileDirectory + (fn [] (str test-dir "/log"))})) + +(def network + (clj->js + {:callPrivateRPC + (fn [payload callback] + (callback (.callPrivateRPC native-status payload)))})) + +(def status + (clj->js + {:intendedPanic identity + :getNodeConfig (fn [] (types/clj->json {:WakuV2Config ""})) + :addCentralizedMetric (fn [_ callback] + (callback)) + :fleets (fn [] (.fleets native-status)) + :startLocalNotifications identity})) diff --git a/src/tests/wallet_test_data.cljs b/src/tests/wallet_test_data.cljs new file mode 100644 index 00000000000..47026d1bb95 --- /dev/null +++ b/src/tests/wallet_test_data.cljs @@ -0,0 +1,114 @@ +(ns tests.wallet-test-data + (:require [status-im.contexts.wallet.networks.config :as networks.config])) + +(def mainnet-chain-id networks.config/ethereum-chain-id) +(def optimism-chain-id networks.config/optimism-chain-id) +(def arbitrum-chain-id networks.config/arbitrum-chain-id) +(def sepolia-chain-id networks.config/sepolia-chain-id) +(def arbitrum-sepolia-chain-id networks.config/arbitrum-sepolia-chain-id) +(def optimism-sepolia-chain-id networks.config/optimism-sepolia-chain-id) +(def mainnet-name :mainnet) +(def optimism-name :optimism) +(def arbitrum-name :arbitrum) + +(def mainnet + {:test? false + :short-name "eth" + :network-name mainnet-name + :chain-id mainnet-chain-id + :related-chain-id mainnet-chain-id + :abbreviated-name "Eth." + :full-name "Mainnet" + :block-explorer-url "https://eth.block-explorer.com/" + :active? true + :layer 1 + :native-currency-symbol "ETH" + :native-currency-name "Ether" + :native-currency-decimals 18}) + +(def arbitrum + {:test? false + :short-name "arb1" + :network-name arbitrum-name + :chain-id arbitrum-chain-id + :related-chain-id arbitrum-chain-id + :abbreviated-name "Arb1." + :block-explorer-url "https://arb.block-explorer.com/" + :full-name "Arbitrum" + :active? true + :layer 2 + :native-currency-symbol "ETH" + :native-currency-name "Ether" + :native-currency-decimals 18}) + +(def optimism + {:test? false + :short-name "oeth" + :network-name optimism-name + :chain-id optimism-chain-id + :related-chain-id optimism-chain-id + :abbreviated-name "Opt." + :full-name "Optimism" + :block-explorer-url "https://opt.block-explorer.com/" + :active? true + :layer 2 + :native-currency-symbol "ETH" + :native-currency-name "Ether" + :native-currency-decimals 18}) + +(def sepolia + (assoc mainnet + :test? true + :related-chain-id mainnet-chain-id + :chain-id sepolia-chain-id)) + +(def arbitrum-sepolia + (assoc mainnet + :test? true + :related-chain-id arbitrum-chain-id + :chain-id arbitrum-sepolia-chain-id)) + +(def optimism-sepolia + (assoc mainnet + :test? true + :related-chain-id optimism-chain-id + :chain-id optimism-sepolia-chain-id)) + +(def chain-ids-by-mode + {:prod [mainnet-chain-id + arbitrum-chain-id + optimism-chain-id] + :test [sepolia-chain-id + arbitrum-sepolia-chain-id + optimism-sepolia-chain-id]}) + +(def networks-by-id + {mainnet-chain-id mainnet + optimism-chain-id optimism + arbitrum-chain-id arbitrum + sepolia-chain-id sepolia + arbitrum-sepolia-chain-id arbitrum-sepolia + optimism-sepolia-chain-id optimism-sepolia}) + +(defn add-networks-to-db + ([db] + (add-networks-to-db db identity)) + ([db update-fn] + (update-in db + [:wallet] + #(-> % + (assoc :networks/chain-ids-by-mode chain-ids-by-mode) + (assoc :networks/by-id (update-vals networks-by-id update-fn)))))) + +(defn add-active-networks-to-db + [db active-chain-ids] + (add-networks-to-db + db + (fn [network] + (if (not (contains? (set active-chain-ids) (:chain-id network))) + (assoc network :active? false) + network)))) + +(defn add-testnet-enabled-to-db + [db] + (assoc-in db [:profile/profile :test-networks-enabled?] true)) diff --git a/src/utils/address.cljs b/src/utils/address.cljs new file mode 100644 index 00000000000..b08ed8fc417 --- /dev/null +++ b/src/utils/address.cljs @@ -0,0 +1,170 @@ +(ns utils.address + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [utils.ethereum.eip.eip681 :as eip681])) + + +(def hex-prefix "0x") +;; EIP-3770 is a format used by Status until v2.32 and described here: +;; https://eips.ethereum.org/EIPS/eip-3770 +(def regx-eip-3770-address #"^(?:(?:eth:|arb1:|oeth:)(?=:|))*0x[0-9a-fA-F]{40}$") +(def regx-metamask-address #"^ethereum:(0x[0-9a-fA-F]{40})(?:@(0x1|0xa|0xa4b1))?$") +(def regx-address-contains #"(?i)0x[a-fA-F0-9]{40}") +(def zero-address "0x0000000000000000000000000000000000000000") + +(defn has-hex-prefix? + [message] + (string/starts-with? message hex-prefix)) + +(defn normalized-hex + [hex] + (when hex + (if (string/starts-with? hex hex-prefix) + hex + (str hex-prefix hex)))) + +(defn address? + [address] + (native-module/address? address)) + +(defn address= + [address1 address2] + (and address1 + address2 + (= (string/lower-case (normalized-hex address1)) + (string/lower-case (normalized-hex address2))))) + +(defn get-shortened-key + "Takes first and last 4 digits from address including leading 0x + and adds unicode ellipsis in between" + [value] + (when value + (str (subs value 0 6) "\u2026" (subs value (- (count value) 3) (count value))))) + +(defn get-abbreviated-profile-url + "The goal here is to generate a string that begins with status.app/u/ joined + with the 1st 5 characters of the encoded data followed by an ellipsis + followed by the last 10 characters of the compressed public key" + [universal-profile-url] + (when-let [re-find-result (re-find #"^https://(status.app/u/)(.*)#(.*)$" (str universal-profile-url))] + (let [[_whole-url base-url encoded-data public-key] re-find-result] + (when (> (count public-key) 9) + (let [first-part-of-encoded-data (subs encoded-data 0 5) + ellipsis "..." + public-key-size (count public-key) + last-part-of-public-key (subs public-key (- public-key-size 10) public-key-size) + abbreviated-url (str base-url + first-part-of-encoded-data + ellipsis + last-part-of-public-key)] + abbreviated-url))))) + +(defn get-shortened-compressed-key + "The goal here is to generate a string that begins with 1st 3 + characters of the compressed public key followed by an ellipsis followed by + the last 6 characters of the compressed public key" + [public-key] + (if (and public-key (> (count public-key) 9)) + (let [first-part-of-public-key (subs public-key 0 3) + ellipsis "..." + public-key-size (count public-key) + last-part-of-public-key (subs public-key (- public-key-size 5) public-key-size) + abbreviated-public-key (str first-part-of-public-key ellipsis last-part-of-public-key)] + abbreviated-public-key) + nil)) + +(defn get-short-wallet-address + [value] + (when value + (str (subs value 0 5) "..." (subs value (- (count value) 3) (count value))))) + +(defn eip-155-suffix->eip-3770-prefix + [eip-155-suffix] + (case eip-155-suffix + "0x1" "eth:" + "0xa4b1" "arb1:" + "0xa" "oeth:" + nil)) + +(defn split-metamask-address + [address] + (re-find regx-metamask-address address)) + +(defn metamask-address? + [address] + (boolean (split-metamask-address address))) + +(defn eip-3770-address? + "Checks if address follows EIP-3770 format which is default for Status" + [address] + (re-find regx-eip-3770-address address)) + +(defn eip-681-address? + [scanned-text] + (-> scanned-text + eip681/parse-uri + :address + boolean)) + +(defn supported-address? + [s] + (boolean (or (eip-3770-address? s) + (metamask-address? s)))) + +(defn supported-scan-address? + [s] + (boolean (or (eip-681-address? s) + (supported-address? s)))) + +(defn metamask-address->eip-3770-address + [metamask-address] + (when-let [[_ address metamask-network-suffix] (split-metamask-address metamask-address)] + (if-let [status-network-prefix (eip-155-suffix->eip-3770-prefix metamask-network-suffix)] + (str status-network-prefix address) + address))) + +(defn extract-address-without-chains-info + [address] + (re-find regx-address-contains address)) + +(defn metamask-address->eth-address + [eip-3770-address] + (extract-address-without-chains-info eip-3770-address)) + +(defn eip-3770-address->eth-address + [eip-3770-address] + (extract-address-without-chains-info eip-3770-address)) + +(defn eip-681-address->eth-address + [eip-681-address] + (-> eip-681-address + eip681/parse-uri + :address)) + +(defn supported-address->eth-address + [address] + (cond + (eip-681-address? address) + (eip-681-address->eth-address address) + + (eip-3770-address? address) + (eip-3770-address->eth-address address) + + (metamask-address? address) + (metamask-address->eth-address address))) + +(defn zero-address? + [address] + (= address zero-address)) + +(defn public-key->address + [public-key] + (let [length (count public-key) + normalized-key (case length + 132 (str "0x" (subs public-key 4)) + 130 public-key + 128 (str "0x" public-key) + nil)] + (when normalized-key + (subs (native-module/sha3 normalized-key) 26)))) diff --git a/src/utils/address_test.cljs b/src/utils/address_test.cljs new file mode 100644 index 00000000000..c2856abd4fe --- /dev/null +++ b/src/utils/address_test.cljs @@ -0,0 +1,161 @@ +(ns utils.address-test + (:require + [cljs.test :refer [deftest is testing]] + [utils.address])) + +(deftest get-shortened-compressed-key-test + (testing "Ensure the function correctly abbreviates a valid public key" + (is (= "zQ3...sgt5N" + (utils.address/get-shortened-compressed-key + "zQ3ssgRy5TtB47MMiMKMKaGyaawkCgMqqbrnAUYrZJ1sgt5N")))) + + (testing "Ensure the function returns nil when given an empty string" + (is (nil? (utils.address/get-shortened-compressed-key "")))) + + (testing "Ensure the function returns nil when given a nil input" + (is (nil? (utils.address/get-shortened-compressed-key nil)))) + + (testing "Ensure the function returns nil when given a public key shorter than 9 characters" + (is (nil? (utils.address/get-shortened-compressed-key "abc"))) + (is (nil? (utils.address/get-shortened-compressed-key "1234"))))) + + +(deftest get-abbreviated-profile-url-test + (testing "Ensure the function correctly generates an abbreviated profile URL for a valid public key" + (is (= "status.app/u/abcde...mrdYpzeFUa" + (utils.address/get-abbreviated-profile-url + "https://status.app/u/abcdefg#zQ3shPrnUhhR42JJn3QdhodGest8w8MjiH8hPaimrdYpzeFUa")))) + + (testing "Ensure the function returns nil when given an empty public key" + (is (nil? (utils.address/get-abbreviated-profile-url "status.app/u/abcdefg"))) + (is (nil? (utils.address/get-abbreviated-profile-url "status.app/u/abcdefg#")))) + + (testing "Ensure the function returns nil when given an incorrect base URL" + (is (nil? (utils.address/get-abbreviated-profile-url + "https://status.app/uwu/abcdefg#zQ3shPrnUhhR42JJn3QdhodGest8w8MjiH8hPaimrdYpzeFUa")))) + + (testing "Ensure the function returns nil when given a public key shorter than 10 characters" + (is (nil? (utils.address/get-abbreviated-profile-url "https://status.app/u/abcdefg#012345678")))) + + (testing "Ensure the function returns nil when given nil" + (is (nil? (utils.address/get-abbreviated-profile-url nil))))) + +(def valid-metamask-addresses + ["ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1" + "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1" + "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa" + "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"]) + +(def valid-eip-3770-addresses + ["0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + "eth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + "eth:arb1:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + "eth:arb1:oeth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"]) + +(def invalid-eip-3770-addresses + ["0x+38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + "eth3:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + ":eth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2:eth" + "eth:arb10x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"]) + +(def invalid-metamask-addresses + ["ethe:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1" + ":0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1" + "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa" + "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1d" + "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd20xa4b1" + "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2:0xa"]) + +(def metamask-to-eip-3770 + [{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1" + :eip-3770 "eth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1" + :eip-3770 "arb1:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa" + :eip-3770 "oeth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + :eip-3770 "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethe:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1" :eip-3770 nil} + {:metamask ":0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1" :eip-3770 nil} + {:metamask "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa" :eip-3770 nil} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1d" :eip-3770 nil} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd20xa4b1" :eip-3770 nil} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2:0xa" :eip-3770 nil}]) + +(def metamask-to-status + [{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:metamask "ethe:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1" :status nil} + {:metamask ":0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1" :status nil} + {:metamask "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa" :status nil} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1d" :status nil} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd20xa4b1" :status nil} + {:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2:0xa" :status nil}]) + +(def eip-3770-to-status + [{:eip-3770 "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:eip-3770 "eth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:eip-3770 "eth:arb1:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:eip-3770 "eth:arb1:oeth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2" + :status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:status nil + :eip-3770 "0x+38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:status nil + :eip-3770 "eth3:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:status nil + :eip-3770 ":eth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"} + {:status nil + :eip-3770 "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2:eth"} + {:status nil + :eip-3770 "eth:arb10x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"}]) + +(deftest metamask-address?-test + (testing "Check valid metamask addresses" + (dorun + (for [address valid-metamask-addresses] + (is (utils.address/metamask-address? address))))) + (testing "Check invalid metamask addresses" + (dorun + (for [address invalid-metamask-addresses] + (is (not (utils.address/metamask-address? address))))))) + +(deftest eip-3770-address?-test + (testing "Check valid eip-3770 addresses" + (dorun + (for [address valid-eip-3770-addresses] + (is (utils.address/eip-3770-address? address))))) + (testing "Check invalid eip-3770 addresses" + (dorun + (for [address invalid-eip-3770-addresses] + (is (not (utils.address/metamask-address? address))))))) + +(deftest metamask-address->eip-3770-address-test + (testing "Check metamask to status address conversion is valid" + (dorun + (for [{metamask-address :metamask + eip-3770-address :eip-3770} metamask-to-eip-3770] + (is (= eip-3770-address (utils.address/metamask-address->eip-3770-address metamask-address))))))) + + +(deftest supported-address->status-address-test + (testing "Check metamask to status address conversion is valid" + (dorun + (for [{metamask-address :metamask + status-address :status} metamask-to-status] + (is (= status-address (utils.address/supported-address->eth-address metamask-address)))))) + (testing "Check eip-3770 to status address conversion is valid" + (dorun + (for [{eip-3770-address :eip-3770 + status-address :status} eip-3770-to-status] + (is (= status-address (utils.address/supported-address->eth-address eip-3770-address))))))) + diff --git a/src/utils/collection.cljs b/src/utils/collection.cljs new file mode 100644 index 00000000000..4e556d0d251 --- /dev/null +++ b/src/utils/collection.cljs @@ -0,0 +1,22 @@ +(ns utils.collection) + +(defn first-index + "Returns first index in coll where predicate on coll element is truthy" + [pred coll] + (->> coll + (keep-indexed (fn [idx e] + (when (pred e) + idx))) + first)) + +(defn index-by + "Given a collection and a unique key function, returns a map that indexes the collection. + Similar to group-by except that the map values are single objects (depends on key uniqueness)." + [k coll] + (into {} (map #(vector (k %) %) coll))) + +(defn distinct-by + "Given a key and a collection returns a unique collection by that key" + [k coll] + (let [groups (group-by k coll)] + (map #(first (groups %)) (distinct (map k coll))))) diff --git a/src/utils/collection_test.cljs b/src/utils/collection_test.cljs new file mode 100644 index 00000000000..8d7c60b6a41 --- /dev/null +++ b/src/utils/collection_test.cljs @@ -0,0 +1,12 @@ +(ns utils.collection-test + (:require + [cljs.test :refer-macros [deftest is]] + [utils.collection :as c])) + +(deftest first-index-test + (is (= 2 + (c/first-index (partial = :test) + '(:a :b :test :c :test)))) + (is (= nil + (c/first-index (partial = :test) + '(:a :b :c))))) diff --git a/src/utils/datetime.cljs b/src/utils/datetime.cljs new file mode 100644 index 00000000000..e82976d270e --- /dev/null +++ b/src/utils/datetime.cljs @@ -0,0 +1,291 @@ +(ns utils.datetime + (:require + [cljs-time.coerce :as t.coerce] + [cljs-time.core :as t] + [cljs-time.format :as t.format] + [clojure.string :as string] + [goog.string :as gstring] + [utils.i18n :as i18n] + [utils.i18n-goog :as i18n-goog])) + +(defn now [] (t/now)) + +(def weekday-names ["su" "mo" "tu" "we" "th" "fr" "sa"]) + +(def ^:const int->weekday + "Maps the corresponding string representation of a weekday + By it's numeric index as in cljs-time" + {1 "mon" + 2 "tue" + 3 "wed" + 4 "thu" + 5 "fri" + 6 "sat" + 7 "sun"}) + +(def ^:const months + "Maps the corresponding string representation of a weekday + By it's numeric index as in cljs-time" + {1 "jan" + 2 "feb" + 3 "mar" + 4 "apr" + 5 "may" + 6 "jun" + 7 "jul" + 8 "aug" + 9 "sep" + 10 "oct" + 11 "nov" + 12 "dec"}) + +(def one-second 1000) +(def minute (* 60 one-second)) +(defn minutes [m] (* m minute)) +(def hour (* 60 minute)) +(def day (* 24 hour)) +(def week (* 7 day)) +(defn weeks [w] (* w week)) +(def default-units + [{:name :t/datetime-second-short :limit 60 :in-second 1} + {:name :t/datetime-minute-short :limit 3600 :in-second 60} + {:name :t/datetime-hour-short :limit 86400 :in-second 3600} + {:name :t/datetime-day-short :limit nil :in-second 86400}]) + +(def time-zone-offset (t/hours (- (/ (.getTimezoneOffset ^js (js/Date.)) 60)))) + +;;;; Utilities +(defn- is-24-hour-locsym + "Detects if given locale symbols timeformat generates AM/PM ('a')." + [^js locsym] + (not (string/includes? + (nth (.-TIMEFORMATS locsym) 2) + "a"))) + +(defn- is-24-hour + "Returns is24Hour from device or from given locale symbols. Whenever we get + non-nil value use it, else calculate it from the given locale symbol." + [^js locsym] + (is-24-hour-locsym locsym)) + +;;;; Time formats +(defn- short-time-format + [^js locsym] + (if (is-24-hour locsym) + "HH:mm" + "h:mm a")) + +(defn- time-format + [^js locsym] + (if (is-24-hour locsym) + "HH:mm:ss" + "h:mm:ss a")) + +;;;; Date formats +(defn- short-date-format-with-time [_] "dd MMM h:mm a") + +(defn- datetime-within-one-week-format + [^js locsym] + (if (is-24-hour locsym) + "E HH:mm" + "E h:mm a")) + +(defn- medium-date-format + "Get medium format from current locale symbols." + [^js locsym] + (nth (.-DATEFORMATS locsym) 2)) + +(defn format-date + [date] + (if date + (t.format/unparse (t.format/formatter "dd/MM/yyyy") date) + "")) + +;;;; Datetime formats +(defn- medium-date-time-format + [locsym] + (str (medium-date-format locsym) ", " (time-format locsym))) + +(defn get-formatter-fn + [format] + (let [formatter (atom nil)] + (fn [] + (or @formatter + (reset! formatter + (i18n-goog/mk-fmt i18n/locale format)))))) + +(def date-time-fmt (get-formatter-fn medium-date-time-format)) +(def date-fmt (get-formatter-fn medium-date-format)) +(def time-fmt (get-formatter-fn short-time-format)) +(def short-date-with-time-fmt (get-formatter-fn short-date-format-with-time)) +(def datetime-within-one-week-fmt (get-formatter-fn datetime-within-one-week-format)) + +(def format-long-month + (memoize (fn [month] + (.format ^js ((get-formatter-fn (constantly "MMMM"))) + (t/date-time 1970 month))))) +;;;; Utilities +(defn previous-years? + [datetime] + (< (t/year datetime) (t/year (t/now)))) + +(defn current-year? + [datetime] + (= (t/year datetime) (t/year (t/now)))) + +(defn today? + [datetime] + (let [time-now (t/now)] + (and (= (t/year time-now) (t/year datetime)) + (= (t/month time-now) (t/month datetime)) + (= (t/day time-now) (t/day datetime))))) + +(defn tomorrow? + [datetime] + (= (-> (t/now) + (t/plus (t/days 1)) + t/day) + (t/day datetime))) + +(defn within-last-n-days? + "Returns true if `datetime` is within last `n` days (inclusive on both ends)." + [datetime n] + (let [time-now (t/now) + start (t/at-midnight (t/minus time-now (t/days n))) + end (t/plus time-now (t/millis 1))] + (t/within? start end datetime))) + +;;;; Timestamp formatters +(defn- to-str + [ms old-fmt-fn yesterday-fmt-fn today-fmt-fn] + (let [date (t.coerce/from-long ms) + ;; NOTE(edge-case): this is wrong, it uses the current timezone offset, regardless of DST. + local (t/plus date time-zone-offset) + today (t/minus (t/today-at-midnight) time-zone-offset) + yesterday (t/plus today (t/days -1))] + (cond + (t/before? date yesterday) (old-fmt-fn local) + (t/before? date today) (yesterday-fmt-fn local) + :else (today-fmt-fn local)))) + +(defn to-short-str + [ms] + (to-str ms + #(.format ^js (date-fmt) %) + #(i18n/label :t/datetime-yesterday) + #(.format ^js (time-fmt) %))) + +(defn day-relative + [ms] + (to-str ms + #(.format ^js (date-fmt) %) + #(i18n/label :t/datetime-yesterday) + #(i18n/label :t/datetime-today))) + +(defn timestamp->relative + [ms] + (let [datetime (-> ms + t.coerce/from-long + (t/plus time-zone-offset))] + (cond + (today? datetime) + (str (string/capitalize (i18n/label :t/datetime-today)) + " " + (.format ^js (time-fmt) datetime)) + + (within-last-n-days? datetime 1) + (str (string/capitalize (i18n/label :t/datetime-yesterday)) + " " + (.format ^js (time-fmt) datetime)) + + (within-last-n-days? datetime 6) + (.format ^js (datetime-within-one-week-fmt) datetime) + + (current-year? datetime) + (.format ^js (short-date-with-time-fmt) datetime) + + (previous-years? datetime) + (.format ^js (date-fmt) datetime)))) + +(defn full-date->short-date + [s] + (let [words (string/split s #"\s+") + last-word (last words)] + (if (or (= "AM" last-word) (= "PM" last-word)) + (string/join " " (take (- (count words) 2) words)) + s))) + +(defn timestamp->relative-short-date + [ms] + (->> ms + timestamp->relative + full-date->short-date)) + +(defn timestamp->time + [ms] + (.format ^js (time-fmt) + (-> ms + t.coerce/from-long + (t/plus time-zone-offset)))) + +(defn timestamp->long-date + [ms] + (.format ^js (date-time-fmt) + (-> ms + t.coerce/from-long + (t/plus time-zone-offset)))) + +(defn seconds-ago + [date-time] + (let [time-now (t/now)] + (if (<= (.getTime ^js date-time) (.getTime ^js time-now)) + (t/in-seconds (t/interval date-time time-now)) + 0))) + +(defn time-ago-long + ([date-time units] + (let [time-ago-seconds (seconds-ago date-time) + unit (first (drop-while #(and (>= time-ago-seconds (:limit %)) + (:limit %)) + units)) + diff (-> (/ time-ago-seconds (:in-second unit)) + Math/floor + int) + + name (i18n/label-pluralize diff (:name unit))] + (i18n/label :t/datetime-ago-format + {:ago (i18n/label :t/datetime-ago) + :number diff + :time-intervals name}))) + ([date-time] + (time-ago-long date-time default-units))) + +(defn to-date + [ms] + (t.coerce/from-long ms)) + +(defn timestamp + [] + (inst-ms (js/Date.))) + +(defn timestamp-sec + [] + (int (/ (timestamp) 1000))) + +(defn timestamp->year-month-day-date + [ms] + (t.format/unparse (:year-month-day t.format/formatters) (to-date ms))) + +(defn to-ms + [sec] + (* 1000 sec)) + +(defn ms-to-duration + "miliseconds to mm:ss format" + [ms] + (let [sec (quot ms 1000)] + (gstring/format "%02d:%02d" (quot sec 60) (mod sec 60)))) + +(def ^:const go-default-time + "Zero value for golang's time var" + "0001-01-01T00:00:00Z") diff --git a/src/utils/datetime_test.cljs b/src/utils/datetime_test.cljs new file mode 100644 index 00000000000..78021bf1032 --- /dev/null +++ b/src/utils/datetime_test.cljs @@ -0,0 +1,228 @@ +(ns utils.datetime-test + (:require + [cljs-time.coerce :as time-coerce] + [cljs-time.core :as t] + [cljs.test :refer-macros [deftest testing is]] + [utils.datetime :as datetime] + [utils.i18n-goog :as i18n-goog])) + +(defn match + [name symbols] + (is (identical? (.-dateTimeSymbols_ (i18n-goog/mk-fmt name #'utils.datetime/medium-date-format)) + symbols))) + +(deftest date-time-formatter-test + (match "en-US" goog.i18n.DateTimeSymbols_en_US) + (match "en-ZZ" goog.i18n.DateTimeSymbols_en) + (match "en" goog.i18n.DateTimeSymbols_en) + (match "nb-NO" goog.i18n.DateTimeSymbols_nb) + (match "nb" goog.i18n.DateTimeSymbols_nb) + (match "whoa-WHOA" goog.i18n.DateTimeSymbols_en) + (match "whoa" goog.i18n.DateTimeSymbols_en)) + +;; 1970-01-01 00:00:00 UTC +(def epoch 0) +;; 1970-01-03 00:00:00 UTC +(def epoch-plus-3d 172800000) +(deftest is-24-hour-locale-en-test + (is (false? (#'utils.datetime/is-24-hour-locsym (i18n-goog/locale-symbols "en"))))) + +(deftest is-24-hour-locale-it-test + (is (true? (#'utils.datetime/is-24-hour-locsym (i18n-goog/locale-symbols "it"))))) + +(deftest is-24-hour-locale-nb-test + (is (true? (#'utils.datetime/is-24-hour-locsym (i18n-goog/locale-symbols "nb-NO"))))) + +(deftest to-short-str-today-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/time-fmt (fn [] + (i18n-goog/mk-fmt "us" #'utils.datetime/short-time-format)) + datetime/time-zone-offset (t/period :hours 0)] + (is (= (datetime/to-short-str epoch-plus-3d) "12:00 AM")))) + +#_((deftest to-short-str-today-force-24H-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/is-24-hour (constantly true) + datetime/time-fmt (i18n-module/mk-fmt "us" datetime/short-time-format) + datetime/time-zone-offset (t/period :hours 0)] + (is (= (datetime/to-short-str epoch-plus-3d) "00:00")))) + + (deftest to-short-str-today-force-AMPM-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/is-24-hour (constantly false) + datetime/time-fmt (i18n-module/mk-fmt "it" datetime/short-time-format) + datetime/time-zone-offset (t/period :hours 0)] + (is (= (datetime/to-short-str epoch-plus-3d) "12:00 AM"))))) + +(deftest to-short-str-before-yesterday-us-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/time-zone-offset (t/period :hours 0) + datetime/date-fmt (fn [] + (i18n-goog/mk-fmt "us" #'utils.datetime/medium-date-format))] + (is (= (datetime/to-short-str epoch) "Jan 1, 1970")))) + +(deftest to-short-str-before-yesterday-nb-test + (with-redefs [datetime/time-zone-offset (t/period :hours 0) + datetime/date-fmt (fn [] + (i18n-goog/mk-fmt "nb-NO" + #'utils.datetime/medium-date-format)) + t/*ms-fn* (constantly epoch-plus-3d)] + (is (= (datetime/to-short-str epoch) "1. jan. 1970")))) + +(deftest day-relative-before-yesterday-us-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/time-zone-offset (t/period :hours 0) + datetime/date-fmt (fn [] + (i18n-goog/mk-fmt "us" + #'utils.datetime/medium-date-time-format))] + (is (= (datetime/day-relative epoch) "Jan 1, 1970, 12:00:00 AM")))) + +(deftest day-relative-before-yesterday-nb-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/time-zone-offset (t/period :hours 0) + datetime/date-fmt (fn [] + (i18n-goog/mk-fmt "nb-NO" + #'utils.datetime/medium-date-time-format))] + (is (= (datetime/day-relative epoch) "1. jan. 1970, 00:00:00")))) + +(deftest current-year?-test + ;; Today is Monday, 1975-03-10 15:15:45Z + (with-redefs [t/*ms-fn* (constantly 163696545000) + datetime/time-zone-offset (t/period :hours 0)] + (is (datetime/current-year? (t/now))) + + (testing "returns false for future years" + (is (not (datetime/current-year? (t/plus (t/now) (t/years 1)))))) + + (testing "returns true at 1975-01-01 00:00:00" + (is (datetime/current-year? (time-coerce/from-long 157766400000)))) + + (testing "returns false at 1974-12-31 23:59:59" + (is (not (datetime/current-year? (time-coerce/from-long 157766399000))))))) + +(deftest previous-years?-test + ;; Today is Monday, 1975-03-10 15:15:45Z + (with-redefs [t/*ms-fn* (constantly 163696545000) + datetime/time-zone-offset (t/period :hours 0)] + (is (not (datetime/previous-years? (t/now)))) + + (testing "returns false for future years" + (is (not (datetime/current-year? (t/plus (t/now) (t/years 1)))))) + + (testing "returns false at 1975-01-01 00:00:00" + (is (not (datetime/previous-years? (time-coerce/from-long 1640995200000))))) + + (testing "returns true at 1974-12-31 23:59:59" + (is (not (datetime/previous-years? (time-coerce/from-long 1640995199000))))))) + +(deftest within-last-n-days?-test + ;; Today is Monday, 1975-03-10 15:15:45Z + (let [now 163696545000] + (with-redefs [t/*ms-fn* (constantly now) + datetime/time-zone-offset (t/period :hours 0)] + (testing "start of the period, 6 days ago (inclusive)" + ;; Tuesday, 1975-03-03 23:59:59Z + (is (not (datetime/within-last-n-days? (time-coerce/from-long 163123199000) 6))) + + ;; Tuesday, 1975-03-04 00:00:00Z + (is (datetime/within-last-n-days? (time-coerce/from-long 163123200000) 6)) + + ;; Tuesday, 1975-03-04 00:00:01Z + (is (datetime/within-last-n-days? (time-coerce/from-long 163123201000) 6))) + + (testing "end of the period (inclusive)" + ;; Monday, 1975-03-10 15:15:44Z + (is (datetime/within-last-n-days? (time-coerce/from-long 163696544000) 6)) + + ;; Monday, 1975-03-10 15:15:45Z + (is (datetime/within-last-n-days? (time-coerce/from-long now) 6)) + + ;; Monday, 1975-03-10 15:15:46Z + (is (not (datetime/within-last-n-days? (time-coerce/from-long 163696546000) 6))))))) + +(deftest timestamp->relative-test + ;; Today is Monday, 1975-03-10 15:15:45Z + (with-redefs [t/*ms-fn* (constantly 163696545000) + datetime/time-zone-offset (t/period :hours 0) + datetime/is-24-hour (constantly false)] + (testing "formats previous years" + ;; 1974-12-31 23:59:59Z + (is (= "Dec 31, 1974" (datetime/timestamp->relative 157766399000))) + ;; 1973-01-01 00:00:00Z + (is (= "Jan 1, 1973" (datetime/timestamp->relative 94694400000)))) + + (testing "formats 7 days ago or older, but in the current year" + (is (= "03 Mar 3:15 PM" (datetime/timestamp->relative 163091745000))) + (is (= "02 Mar 3:00 PM" (datetime/timestamp->relative 163004400000))) + (is (= "01 Jan 3:00 PM" (datetime/timestamp->relative 157820400000)))) + + (testing "formats dates within the last 6 days" + (is (= "Sat 3:15 PM" (datetime/timestamp->relative 163523745000))) + (is (= "Fri 3:15 PM" (datetime/timestamp->relative 163437345000))) + (is (= "Thu 3:15 PM" (datetime/timestamp->relative 163350945000))) + (is (= "Wed 3:15 PM" (datetime/timestamp->relative 163264545000))) + (is (= "Tue 3:15 PM" (datetime/timestamp->relative 163178145000)))) + + (testing "formats within yesterday window" + (is (= "Yesterday 3:15 PM" (datetime/timestamp->relative 163610145000))) + (is (= "Yesterday 11:59 PM" (datetime/timestamp->relative 163641599000)))) + + (testing "formats today, at various timestamps" + (is (= "Today 3:15 PM" (datetime/timestamp->relative 163696545000))) + (is (= "Today 12:00 PM" (datetime/timestamp->relative 163684800000))) + (is (= "Today 12:00 AM" (datetime/timestamp->relative 163641600000)))))) + +(deftest timestamp->relative-short-date-test + (with-redefs [t/*ms-fn* (constantly 163696545000) + datetime/time-zone-offset (t/period :hours 0) + datetime/is-24-hour (constantly false)] + (testing "short date format for previous years" + (is (= "Dec 31, 1974" (datetime/timestamp->relative-short-date 157766399000))) + (is (= "Jan 1, 1973" (datetime/timestamp->relative-short-date 94694400000)))) + + (testing "short date format for dates 7 days ago or older, but in the current year" + (is (= "03 Mar" (datetime/timestamp->relative-short-date 163091745000))) + (is (= "02 Mar" (datetime/timestamp->relative-short-date 163004400000))) + (is (= "01 Jan" (datetime/timestamp->relative-short-date 157820400000)))) + + (testing "short date format for dates within the last 6 days" + (is (= "Sat" (datetime/timestamp->relative-short-date 163523745000))) + (is (= "Fri" (datetime/timestamp->relative-short-date 163437345000))) + (is (= "Thu" (datetime/timestamp->relative-short-date 163350945000))) + (is (= "Wed" (datetime/timestamp->relative-short-date 163264545000))) + (is (= "Tue" (datetime/timestamp->relative-short-date 163178145000)))) + + (testing "short date format for yesterday" + (is (= "Yesterday" (datetime/timestamp->relative-short-date 163610145000))) + (is (= "Yesterday" (datetime/timestamp->relative-short-date 163641599000)))) + + (testing "short date format for today, at various timestamps" + (is (= "Today" (datetime/timestamp->relative-short-date 163696545000))) + (is (= "Today" (datetime/timestamp->relative-short-date 163684800000))) + (is (= "Today" (datetime/timestamp->relative-short-date 163641600000)))))) + +#_((deftest day-relative-before-yesterday-force-24H-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/is-24-hour (constantly true) + datetime/time-zone-offset (t/period :hours 0) + datetime/date-fmt (i18n-module/mk-fmt + "us" + #'utils.datetime/medium-date-time-format)] + (is (= (datetime/day-relative epoch) "Jan 1, 1970, 00:00:00")))) + + (deftest day-relative-before-yesterday-force-AMPM-test + (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) + datetime/is-24-hour (constantly false) + datetime/time-zone-offset (t/period :hours 0) + datetime/date-fmt (i18n-module/mk-fmt + "it" + #'utils.datetime/medium-date-time-format)] + (is (= (datetime/day-relative epoch) "01 gen 1970, 12:00:00 AM"))))) + +(deftest format-long-month-test + (testing "returns correct month string" + (is (= "January" (datetime/format-long-month 1))) + (is (= "February" (datetime/format-long-month 2))) + (is (= "March" (datetime/format-long-month 3))) + (is (= "April" (datetime/format-long-month 4))) + (is (= "December" (datetime/format-long-month 12))))) diff --git a/src/utils/debounce.cljs b/src/utils/debounce.cljs new file mode 100644 index 00000000000..610255533d6 --- /dev/null +++ b/src/utils/debounce.cljs @@ -0,0 +1,38 @@ +(ns utils.debounce + (:require + [goog.functions] + [re-frame.core :as re-frame])) + +(def timeout (atom {})) + +(defn clear + [event-key] + (when-let [event-timeout (get @timeout event-key)] + (js/clearTimeout event-timeout))) + +(defn clear-all + [] + (doseq [[_ v] @timeout] + (js/clearTimeout v))) + +(defn debounce-and-dispatch + "Dispatches `event` iff it was not dispatched for the duration of `duration-ms`." + [event duration-ms] + (let [event-key (first event)] + (clear event-key) + (swap! timeout assoc event-key (js/setTimeout #(re-frame/dispatch event) duration-ms)))) + +(def throttle (atom {})) + +(defn throttle-and-dispatch + "Dispatches event and ignores subsequent calls for the duration of `duration-ms`." + [event duration-ms] + (let [event-key (first event)] + (when-not (get @throttle event-key) + (swap! throttle assoc event-key true) + (js/setTimeout #(swap! throttle dissoc event-key) duration-ms) + (re-frame/dispatch event)))) + +(defn debounce + [f duration-ms] + (goog.functions/debounce f duration-ms)) diff --git a/src/utils/emojilib.cljs b/src/utils/emojilib.cljs new file mode 100644 index 00000000000..c5f2e9ee2bc --- /dev/null +++ b/src/utils/emojilib.cljs @@ -0,0 +1,22 @@ +(ns utils.emojilib + (:require ["emojilib" :as emojis] + [goog.object :as object] + utils.string)) + +(def emoji-regex + #"(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])") + +(def lib (.-lib emojis)) + +(defn get-char + [emoji-id] + (when-let [emoji-map (object/get lib emoji-id)] + (.-char ^js emoji-map))) + +(defn text->emoji + "Replaces emojis in a specified `text`" + [text] + (utils.string/safe-replace text + #":([a-z_\-+0-9]*):" + (fn [[original emoji-id]] + (or (get-char emoji-id) original)))) diff --git a/src/utils/ens/core.cljs b/src/utils/ens/core.cljs new file mode 100644 index 00000000000..a97d2889fe2 --- /dev/null +++ b/src/utils/ens/core.cljs @@ -0,0 +1,16 @@ +(ns utils.ens.core + (:require [clojure.string :as string])) + +;; this is the addresses of ens registries for the different networks +(def ens-registries + {:mainnet "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" + :sepolia "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"}) + +(def default-address "0x0000000000000000000000000000000000000000") +(def default-key + "0x0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +(defn is-valid-eth-name? + [ens-name] + (and ens-name + (string? ens-name) + (string/ends-with? ens-name ".eth"))) diff --git a/src/utils/ens/stateofus.cljs b/src/utils/ens/stateofus.cljs new file mode 100644 index 00000000000..d27e5299ffe --- /dev/null +++ b/src/utils/ens/stateofus.cljs @@ -0,0 +1,45 @@ +(ns utils.ens.stateofus + (:require + [clojure.string :as string] + [utils.ens.core :as utils.ens])) + +(def domain "stateofus.eth") + +(defn subdomain + [username] + (str username "." domain)) + +(defn username-with-domain + "checks if the username is a status username or a ens name + for that we check if there is a dot in the username, which + would indicate that there is already a domain name so we don't + concatenated stateofus domain to it" + [username] + (when (and (string? username) + (seq username)) + (if (string/includes? username ".") + username + (subdomain username)))) + +(defn username + [name] + (when (and name (string/ends-with? name domain)) + (first (string/split name ".")))) + +(defn lower-case? + [s] + (when s + (= s (string/lower-case s)))) + +(defn valid-username? + [s] + (boolean (and (lower-case? s) + (re-find #"^[a-z0-9]+$" s)))) + +(defn ens-name-parse + [contact-identity] + (when (string? contact-identity) + (string/lower-case + (if (utils.ens/is-valid-eth-name? contact-identity) + contact-identity + (subdomain contact-identity))))) diff --git a/src/utils/ens/stateofus_test.cljs b/src/utils/ens/stateofus_test.cljs new file mode 100644 index 00000000000..bb2a959c0a5 --- /dev/null +++ b/src/utils/ens/stateofus_test.cljs @@ -0,0 +1,16 @@ +(ns utils.ens.stateofus-test + (:require + [cljs.test :refer-macros [deftest is]] + [utils.ens.stateofus :as stateofus])) + +(deftest valid-username?-test + (is (false? (stateofus/valid-username? nil))) + (is (true? (stateofus/valid-username? "andrey"))) + (is (false? (stateofus/valid-username? "Andrey"))) + (is (true? (stateofus/valid-username? "andrey12")))) + +(deftest username-with-domain-test + (is (nil? (stateofus/username-with-domain nil))) + (is (= "andrey.stateofus.eth" (stateofus/username-with-domain "andrey"))) + (is (= "andrey.eth" (stateofus/username-with-domain "andrey.eth"))) + (is (= "andrey.stateofus.eth" (stateofus/username-with-domain "andrey.stateofus.eth")))) diff --git a/src/utils/ethereum/chain.cljs b/src/utils/ethereum/chain.cljs new file mode 100644 index 00000000000..8d45b3bea3c --- /dev/null +++ b/src/utils/ethereum/chain.cljs @@ -0,0 +1,53 @@ +(ns utils.ethereum.chain + {:deprecated true}) + +(def BSC-mainnet-chain-id 56) +(def BSC-testnet-chain-id 97) + +;; IDs standardized in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids +(def chains + {:mainnet {:id 1 :name "Ethereum"} + :xdai {:id 100 :name "xDai"} + :sepolia {:id 11155111 :name "Sepolia"} + :bsc {:id BSC-mainnet-chain-id + :name "BSC"} + :bsc-testnet {:id BSC-testnet-chain-id + :name "BSC testnet"} + :arbitrum {:id 42161 :name "Arbitrum"} + :optimism {:id 10 :name "Optimism"} + :base {:id 8453 :name "Base"}}) + +(defn chain-id->chain-keyword + [i] + (or (some #(when (= i (:id (val %))) (key %)) chains) + :custom)) + +(defn chain-keyword->chain-id + [k] + (get-in chains [k :id])) + +(defn network->chain-id + [network] + (get-in network [:config :NetworkId])) + +(defn network->chain-keyword + [network] + (chain-id->chain-keyword (network->chain-id network))) + +(defn network->chain-name + [network] + (-> network + network->chain-keyword + name)) + +(defn get-current-network + [_] + nil) + +(defn chain-keyword + [db] + (network->chain-keyword (get-current-network db))) + +(defn chain-id + [db] + (network->chain-id (get-current-network db))) diff --git a/src/utils/ethereum/chain_test.cljs b/src/utils/ethereum/chain_test.cljs new file mode 100644 index 00000000000..3c8fc94f070 --- /dev/null +++ b/src/utils/ethereum/chain_test.cljs @@ -0,0 +1,9 @@ +(ns utils.ethereum.chain-test + (:require + [cljs.test :refer-macros [deftest is]] + [utils.ethereum.chain :as chain])) + +(deftest chain-id->chain-keyword-test + (is (= (chain/chain-id->chain-keyword 1) :mainnet)) + (is (= (chain/chain-id->chain-keyword 11155111) :sepolia)) + (is (= (chain/chain-id->chain-keyword 5777) :custom))) diff --git a/src/utils/ethereum/eip/eip55.cljs b/src/utils/ethereum/eip/eip55.cljs new file mode 100644 index 00000000000..a008a9ffff7 --- /dev/null +++ b/src/utils/ethereum/eip/eip55.cljs @@ -0,0 +1,25 @@ +(ns utils.ethereum.eip.eip55 + "Utility function related to [EIP55](https://eips.ethereum.org/EIPS/eip-55) + + This EIP standardize how ethereum addresses should be printed as strings to validate checksum. + + e.g. 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + (:require + [clojure.string :as string] + [native-module.core :as native-module])) + +(def hex-prefix "0x") + +(defn address->checksum + "Converts an arbitrary case address to one with correct checksum case." + [address] + (when address + (native-module/to-checksum-address + (if (string/starts-with? address hex-prefix) + address + (str hex-prefix address))))) + +(defn valid-address-checksum? + "Checks address checksum validity." + [address] + (native-module/check-address-checksum address)) diff --git a/src/utils/ethereum/eip/eip55_test.cljs b/src/utils/ethereum/eip/eip55_test.cljs new file mode 100644 index 00000000000..6479a002c66 --- /dev/null +++ b/src/utils/ethereum/eip/eip55_test.cljs @@ -0,0 +1,17 @@ +(ns utils.ethereum.eip.eip55-test + (:require + [cljs.test :refer-macros [deftest is]] + [utils.ethereum.eip.eip55 :as eip55])) + +(deftest valid-address-checksum?-test + (is (true? (eip55/valid-address-checksum? "0x52908400098527886E0F7030069857D2E4169EE7"))) + (is (true? (eip55/valid-address-checksum? "0x8617E340B3D01FA5F11F306F4090FD50E238070D"))) + (is (true? (eip55/valid-address-checksum? "0xde709f2102306220921060314715629080e2fb77"))) + (is (true? (eip55/valid-address-checksum? "0x27b1fdb04752bbc536007a920d24acb045561c26"))) + (is (true? (eip55/valid-address-checksum? "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"))) + (is (true? (eip55/valid-address-checksum? "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"))) + (is (true? (eip55/valid-address-checksum? "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"))) + (is (true? (eip55/valid-address-checksum? "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"))) + (is (false? (eip55/valid-address-checksum? "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9adB"))) + (is (false? (eip55/valid-address-checksum? "0x8617e340b3d01fa5f11f306f4090fd50e238070d"))) + (is (false? (eip55/valid-address-checksum? "0xDE709F2102306220921060314715629080E2fB77")))) diff --git a/src/utils/ethereum/eip/eip681.cljs b/src/utils/ethereum/eip/eip681.cljs new file mode 100644 index 00000000000..5e2898c3ab0 --- /dev/null +++ b/src/utils/ethereum/eip/eip681.cljs @@ -0,0 +1,121 @@ +(ns utils.ethereum.eip.eip681 + "Utility function related to [EIP681](https://eips.ethereum.org/EIPS/eip-681) + + This EIP standardize how ethereum payment request can be represented as URI (say to embed them in a QR code). + + e.g. ethereum:0x1234@1/transfer?to=0x5678&value=1e18&gas=5000" + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [utils.ens.core :as utils.ens] + [utils.ethereum.chain :as chain])) + +(def scheme "ethereum") +(def scheme-separator ":") +(def chain-id-separator "@") +(def function-name-separator "/") +(def query-separator "?") +(def parameter-separator "&") +(def key-value-separator "=") + +(defn- address? + [address] + (native-module/address? address)) + +(def uri-pattern + (re-pattern (str scheme scheme-separator "([^" query-separator "]*)(?:\\" query-separator "(.*))?"))) +(def authority-path-pattern + (re-pattern (str "^([^" + chain-id-separator + function-name-separator + "]*)(?:" + chain-id-separator + "(\\d+))?(?:" + function-name-separator + "(\\w*))?"))) +(def key-value-format (str "([^" parameter-separator key-value-separator "]+)")) +(def query-pattern (re-pattern (str key-value-format key-value-separator key-value-format))) + +(def valid-native-arguments #{:value :gas :gasPrice :gasLimit}) + +(defn- parse-query + [s] + (into {} + (for [[_ k v] (re-seq query-pattern (or s ""))] + [(keyword k) v]))) + +(defn- parse-native-arguments + [m] + (select-keys m valid-native-arguments)) + +(defn- parse-arguments + [function-name s] + (let [m (parse-query s) + arguments (parse-native-arguments m)] + (if function-name + (merge arguments + {:function-name function-name} + (when (seq m) + {:function-arguments (apply dissoc m valid-native-arguments)})) + arguments))) + +(defn parse-uri + "Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address` + which will be either a valid ENS or Ethereum address. + Note that values are not decoded and you might need to rely on specific methods for some fields + (parse-value, parse-number). + Invalid URI will be parsed as `nil`." + [s] + (when (string? s) + (if (address? s) + {:address s} + (let [[_ authority-path query] (re-find uri-pattern s)] + (when authority-path + (let [[_ raw-address chain-id function-name] (re-find authority-path-pattern authority-path)] + (when (or (utils.ens/is-valid-eth-name? raw-address) + (address? raw-address) + (when (string/starts-with? raw-address "pay-") + (let [pay-address (string/replace-first raw-address "pay-" "")] + (or (utils.ens/is-valid-eth-name? pay-address) + (address? pay-address))))) + (let [address (if (string/starts-with? raw-address "pay-") + (string/replace-first raw-address "pay-" "") + raw-address)] + (when-let [arguments (parse-arguments function-name query)] + (let [contract-address (get-in arguments [:function-arguments :address])] + (if-not (or (not contract-address) + (utils.ens/is-valid-eth-name? contract-address) + (address? contract-address)) + nil + (merge {:address address + :chain-id (if chain-id + (js/parseInt chain-id) + (chain/chain-keyword->chain-id :mainnet))} + arguments)))))))))))) + +(defn- generate-query-string + [m] + (string/join parameter-separator + (for [[k v] m] + (str (name k) key-value-separator v)))) + +(defn generate-uri + "Generate a EIP 681 URI based on `address` and a map (keywords / {bignumbers/strings} ) of extra properties. + No validation of address format is performed." + [address {:keys [chain-id function-name function-arguments] :as m}] + (when (address? address) + (let [parameters (dissoc (into {} (filter second m)) :chain-id)] ;; filter nil values + (str scheme + scheme-separator + address + (when (and chain-id (not= chain-id (chain/chain-keyword->chain-id :mainnet))) + ;; Add chain-id if specified and is not main-net + (str chain-id-separator chain-id)) + (when-not (empty? parameters) + (if function-name + (str function-name-separator + function-name + query-separator + (let [native-parameters (dissoc parameters :function-name :function-arguments)] + (generate-query-string (merge function-arguments native-parameters)))) + (str query-separator (generate-query-string parameters)))))))) diff --git a/src/utils/ethereum/eip/eip681_test.cljs b/src/utils/ethereum/eip/eip681_test.cljs new file mode 100644 index 00000000000..6b53a00b011 --- /dev/null +++ b/src/utils/ethereum/eip/eip681_test.cljs @@ -0,0 +1,146 @@ +(ns utils.ethereum.eip.eip681-test + (:require + [cljs.test :refer-macros [deftest is]] + [utils.ethereum.eip.eip681 :as eip681] + [utils.money :as money])) + +(deftest parse-uri-test + (is (= nil (eip681/parse-uri nil))) + (is (= nil (eip681/parse-uri 5))) + (is (= nil (eip681/parse-uri "random"))) + (is (= nil (eip681/parse-uri "ethereum:"))) + (is (= nil (eip681/parse-uri "ethereum:?value=1"))) + (is (= nil (eip681/parse-uri "bitcoin:0x1234"))) + (is (= nil (eip681/parse-uri "ethereum:0x1234"))) + (is (= nil (eip681/parse-uri "ethereum:gimme.ether?value=1e18"))) + (is (= nil (eip681/parse-uri "ethereum:pay-gimme.ether?value=1e18"))) + (is (= nil + (eip681/parse-uri + "ethereum:pay-snt.thetoken.ether/transfer?address=gimme.eth&uint256=1&gas=100"))) + (is (= nil + (eip681/parse-uri + "ethereum:pay-snt.thetoken.eth/transfer?address=gimme.ether&uint256=1&gas=100"))) + + (is (= (eip681/parse-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"})) + + (is (= (eip681/parse-uri "ethereum:pay-gimme.eth?value=1e18") + {:address "gimme.eth" :value "1e18" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:pay-0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1e18") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?unknown=1") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1})) + + (is + (= + (eip681/parse-uri + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=2.014e18") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "2.014e18" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=-1e18") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "-1e18" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=+1E18") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "+1E18" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1E18&gas=100") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1E18" :gas "100" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=NOT_NUMBER") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "NOT_NUMBER" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1ETH") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1ETH" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@1?value=1e18&gas=5000") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 1})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1e18&gas=5000") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 3})) + + (is (= (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer"})) + + (is + (= + (eip681/parse-uri + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + :chain-id 1 + :function-name "transfer" + :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}})) + + (is + (= + (eip681/parse-uri + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100") + {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + :chain-id 1 + :gas "100" + :function-name "transfer" + :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}})) + + (is (= (eip681/parse-uri "ethereum:pay-snt.thetoken.eth/transfer?address=gimme.eth&uint256=1&gas=100") + {:address "snt.thetoken.eth" + :chain-id 1 + :gas "100" + :function-name "transfer" + :function-arguments {:address "gimme.eth" :uint256 "1"}})) + + (is (= (eip681/parse-uri "ethereum:snt.thetoken.eth/transfer?address=gimme.eth&uint256=1&gas=100") + {:address "snt.thetoken.eth" + :chain-id 1 + :gas "100" + :function-name "transfer" + :function-arguments {:address "gimme.eth" :uint256 "1"}}))) + +(deftest generate-uri-test + (is (= nil (eip681/generate-uri nil nil))) + (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" nil))) + (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {}))) + (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value nil}))) + (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + {:value (money/bignumber 1)}))) + (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1000000000000000000" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + {:value (money/bignumber 1e18)}))) + (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1&gas=100" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1}))) + (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1&gas=100" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 3}))) + (is + (= + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100" + (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" + {:gas (money/bignumber 100) + :chain-id 1 + :function-name "transfer" + :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" + :uint256 1}})))) + +(deftest round-trip-test + (let [uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1&gas=100" + {:keys [address] :as params} (eip681/parse-uri uri)] + (is (= uri (eip681/generate-uri address (dissoc params :address))))) + (let + [uri + "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3/transfer?uint256=5&address=0xc55cF4B03948D7EBc8b9E8BAD92643703811d162" + {:keys [address] :as params} (eip681/parse-uri uri)] + (is (= uri (eip681/generate-uri address (dissoc params :address)))))) diff --git a/src/utils/hex.cljs b/src/utils/hex.cljs new file mode 100644 index 00000000000..649ca121e9f --- /dev/null +++ b/src/utils/hex.cljs @@ -0,0 +1,39 @@ +(ns utils.hex + (:require + [clojure.string :as string] + [native-module.core :as native-module] + [schema.core :as schema])) + +(defn normalize-hex + [hex] + (when hex + (string/lower-case (if (string/starts-with? hex "0x") + (subs hex 2) + hex)))) + +(schema/=> normalize-hex + [:=> + [:cat [:maybe :string]] + [:maybe :string]]) + +(defn prefix-hex + [hex] + (if (string/starts-with? hex "0x") + hex + (str "0x" hex))) + +(schema/=> prefix-hex + [:=> + [:cat :string] + :string]) + +(defn number-to-hex + [value] + (->> value + native-module/number-to-hex + prefix-hex)) + +(schema/=> number-to-hex + [:=> + [:cat [:or :string :int]] + :string]) diff --git a/src/utils/i18n.cljs b/src/utils/i18n.cljs new file mode 100644 index 00000000000..f2e11643392 --- /dev/null +++ b/src/utils/i18n.cljs @@ -0,0 +1,70 @@ +(ns utils.i18n + (:require + ["i18n-js" :as i18n] + [clojure.string :as string])) + +(defn setup + [default-device-language translations-by-locale] + (set! (.-fallbacks i18n) true) + (set! (.-defaultSeparator i18n) "/") + (set! (.-locale i18n) default-device-language) + (set! (.-translations i18n) translations-by-locale)) + +(defn get-translations + [] + (.-translations i18n)) + +(defn set-language + [lang] + (set! (.-locale i18n) lang)) + +;;:zh, :zh-hans-xx, :zh-hant-xx have been added until this bug will be fixed +;;https://github.com/fnando/i18n-js/issues/460 + +(def delimeters + "This function is a hack: mobile Safari doesn't support toLocaleString(), so we need to pass + this map to WKWebView to make number formatting work." + (let [n (.toLocaleString ^js (js/Number 1000.1)) + delimiter? (= (count n) 7)] + (if delimiter? + {:delimiter (subs n 1 2) + :separator (subs n 5 6)} + {:delimiter "" + :separator (subs n 4 5)}))) + +(defn label-number + [number] + (when number + (let [{:keys [delimiter separator]} delimeters] + (.toNumber i18n + (string/replace number #"," ".") + (clj->js {:precision 10 + :strip_insignificant_zeros true + :delimiter delimiter + :separator separator}))))) + +(def default-option-value "") + +(defn label-options + [options] + ;; i18n ignores nil value, leading to misleading messages + (into {} (for [[k v] options] [k (or v default-option-value)]))) + +(defn label-fn + ([path] (label-fn path {})) + ([path options] + (if (exists? (.t i18n)) + (let [options (update options :amount label-number)] + (.t i18n (name path) (clj->js (label-options options)))) + (name path)))) + +(def label (memoize label-fn)) + +(defn label-pluralize + [amount path & options] + (if (exists? (.t i18n)) + (.p i18n amount (name path) (clj->js options)) + (name path))) + +(def locale + (.-locale i18n)) diff --git a/src/utils/i18n_goog.cljs b/src/utils/i18n_goog.cljs new file mode 100644 index 00000000000..0df05a8025a --- /dev/null +++ b/src/utils/i18n_goog.cljs @@ -0,0 +1,368 @@ +(ns utils.i18n-goog + (:require + [clojure.string :as string] + goog.i18n.CompactNumberFormatSymbols + goog.i18n.CompactNumberFormatSymbols_af + goog.i18n.CompactNumberFormatSymbols_am + goog.i18n.CompactNumberFormatSymbols_ar + goog.i18n.CompactNumberFormatSymbols_ar_DZ + goog.i18n.CompactNumberFormatSymbols_ar_EG + goog.i18n.CompactNumberFormatSymbols_az + goog.i18n.CompactNumberFormatSymbols_be + goog.i18n.CompactNumberFormatSymbols_bg + goog.i18n.CompactNumberFormatSymbols_bn + goog.i18n.CompactNumberFormatSymbols_br + goog.i18n.CompactNumberFormatSymbols_bs + goog.i18n.CompactNumberFormatSymbols_ca + goog.i18n.CompactNumberFormatSymbols_chr + goog.i18n.CompactNumberFormatSymbols_cs + goog.i18n.CompactNumberFormatSymbols_cy + goog.i18n.CompactNumberFormatSymbols_da + goog.i18n.CompactNumberFormatSymbols_de + goog.i18n.CompactNumberFormatSymbols_de_AT + goog.i18n.CompactNumberFormatSymbols_de_CH + goog.i18n.CompactNumberFormatSymbols_el + goog.i18n.CompactNumberFormatSymbols_en + goog.i18n.CompactNumberFormatSymbols_en_AU + goog.i18n.CompactNumberFormatSymbols_en_CA + goog.i18n.CompactNumberFormatSymbols_en_GB + goog.i18n.CompactNumberFormatSymbols_en_IE + goog.i18n.CompactNumberFormatSymbols_en_IN + goog.i18n.CompactNumberFormatSymbols_en_SG + goog.i18n.CompactNumberFormatSymbols_en_US + goog.i18n.CompactNumberFormatSymbols_en_ZA + goog.i18n.CompactNumberFormatSymbols_es + goog.i18n.CompactNumberFormatSymbols_es_419 + goog.i18n.CompactNumberFormatSymbols_es_ES + goog.i18n.CompactNumberFormatSymbols_es_MX + goog.i18n.CompactNumberFormatSymbols_es_US + goog.i18n.CompactNumberFormatSymbols_et + goog.i18n.CompactNumberFormatSymbols_eu + goog.i18n.CompactNumberFormatSymbols_fa + goog.i18n.CompactNumberFormatSymbols_fi + goog.i18n.CompactNumberFormatSymbols_fil + goog.i18n.CompactNumberFormatSymbols_fr + goog.i18n.CompactNumberFormatSymbols_fr_CA + goog.i18n.CompactNumberFormatSymbols_ga + goog.i18n.CompactNumberFormatSymbols_gl + goog.i18n.CompactNumberFormatSymbols_gsw + goog.i18n.CompactNumberFormatSymbols_gu + goog.i18n.CompactNumberFormatSymbols_haw + goog.i18n.CompactNumberFormatSymbols_he + goog.i18n.CompactNumberFormatSymbols_hi + goog.i18n.CompactNumberFormatSymbols_hr + goog.i18n.CompactNumberFormatSymbols_hu + goog.i18n.CompactNumberFormatSymbols_hy + goog.i18n.CompactNumberFormatSymbols_id + goog.i18n.CompactNumberFormatSymbols_in + goog.i18n.CompactNumberFormatSymbols_is + goog.i18n.CompactNumberFormatSymbols_it + goog.i18n.CompactNumberFormatSymbols_iw + goog.i18n.CompactNumberFormatSymbols_ja + goog.i18n.CompactNumberFormatSymbols_ka + goog.i18n.CompactNumberFormatSymbols_kk + goog.i18n.CompactNumberFormatSymbols_km + goog.i18n.CompactNumberFormatSymbols_kn + goog.i18n.CompactNumberFormatSymbols_ko + goog.i18n.CompactNumberFormatSymbols_ky + goog.i18n.CompactNumberFormatSymbols_ln + goog.i18n.CompactNumberFormatSymbols_lo + goog.i18n.CompactNumberFormatSymbols_lt + goog.i18n.CompactNumberFormatSymbols_lv + goog.i18n.CompactNumberFormatSymbols_mk + goog.i18n.CompactNumberFormatSymbols_ml + goog.i18n.CompactNumberFormatSymbols_mn + goog.i18n.CompactNumberFormatSymbols_mo + goog.i18n.CompactNumberFormatSymbols_mr + goog.i18n.CompactNumberFormatSymbols_ms + goog.i18n.CompactNumberFormatSymbols_mt + goog.i18n.CompactNumberFormatSymbols_my + goog.i18n.CompactNumberFormatSymbols_nb + goog.i18n.CompactNumberFormatSymbols_ne + goog.i18n.CompactNumberFormatSymbols_nl + goog.i18n.CompactNumberFormatSymbols_no + goog.i18n.CompactNumberFormatSymbols_no_NO + goog.i18n.CompactNumberFormatSymbols_or + goog.i18n.CompactNumberFormatSymbols_pa + goog.i18n.CompactNumberFormatSymbols_pl + goog.i18n.CompactNumberFormatSymbols_pt + goog.i18n.CompactNumberFormatSymbols_pt_BR + goog.i18n.CompactNumberFormatSymbols_pt_PT + goog.i18n.CompactNumberFormatSymbols_ro + goog.i18n.CompactNumberFormatSymbols_ru + goog.i18n.CompactNumberFormatSymbols_sh + goog.i18n.CompactNumberFormatSymbols_si + goog.i18n.CompactNumberFormatSymbols_sk + goog.i18n.CompactNumberFormatSymbols_sl + goog.i18n.CompactNumberFormatSymbols_sq + goog.i18n.CompactNumberFormatSymbols_sr + goog.i18n.CompactNumberFormatSymbols_sr_Latn + goog.i18n.CompactNumberFormatSymbols_sv + goog.i18n.CompactNumberFormatSymbols_sw + goog.i18n.CompactNumberFormatSymbols_ta + goog.i18n.CompactNumberFormatSymbols_te + goog.i18n.CompactNumberFormatSymbols_th + goog.i18n.CompactNumberFormatSymbols_tl + goog.i18n.CompactNumberFormatSymbols_tr + goog.i18n.CompactNumberFormatSymbols_uk + goog.i18n.CompactNumberFormatSymbols_ur + goog.i18n.CompactNumberFormatSymbols_uz + goog.i18n.CompactNumberFormatSymbols_vi + goog.i18n.CompactNumberFormatSymbols_zh + goog.i18n.CompactNumberFormatSymbols_zh_CN + goog.i18n.CompactNumberFormatSymbols_zh_HK + goog.i18n.CompactNumberFormatSymbols_zh_TW + goog.i18n.CompactNumberFormatSymbols_zu + goog.i18n.currency + goog.i18n.DateTimeFormat + goog.i18n.DateTimeFormat.Format + goog.i18n.DateTimeSymbols + goog.i18n.DateTimeSymbols_af + goog.i18n.DateTimeSymbols_am + goog.i18n.DateTimeSymbols_ar + goog.i18n.DateTimeSymbols_ar_DZ + goog.i18n.DateTimeSymbols_ar_EG + goog.i18n.DateTimeSymbols_az + goog.i18n.DateTimeSymbols_be + goog.i18n.DateTimeSymbols_bg + goog.i18n.DateTimeSymbols_bn + goog.i18n.DateTimeSymbols_br + goog.i18n.DateTimeSymbols_bs + goog.i18n.DateTimeSymbols_ca + goog.i18n.DateTimeSymbols_chr + goog.i18n.DateTimeSymbols_cs + goog.i18n.DateTimeSymbols_cy + goog.i18n.DateTimeSymbols_da + goog.i18n.DateTimeSymbols_de + goog.i18n.DateTimeSymbols_de_AT + goog.i18n.DateTimeSymbols_de_CH + goog.i18n.DateTimeSymbols_el + goog.i18n.DateTimeSymbols_en + goog.i18n.DateTimeSymbols_en_AU + goog.i18n.DateTimeSymbols_en_CA + goog.i18n.DateTimeSymbols_en_GB + goog.i18n.DateTimeSymbols_en_IE + goog.i18n.DateTimeSymbols_en_IN + goog.i18n.DateTimeSymbols_en_ISO + goog.i18n.DateTimeSymbols_en_SG + goog.i18n.DateTimeSymbols_en_US + goog.i18n.DateTimeSymbols_en_ZA + goog.i18n.DateTimeSymbols_es + goog.i18n.DateTimeSymbols_es_419 + goog.i18n.DateTimeSymbols_es_ES + goog.i18n.DateTimeSymbols_es_MX + goog.i18n.DateTimeSymbols_es_US + goog.i18n.DateTimeSymbols_et + goog.i18n.DateTimeSymbols_eu + goog.i18n.DateTimeSymbols_fa + goog.i18n.DateTimeSymbols_fi + goog.i18n.DateTimeSymbols_fil + goog.i18n.DateTimeSymbols_fr + goog.i18n.DateTimeSymbols_fr_CA + goog.i18n.DateTimeSymbols_ga + goog.i18n.DateTimeSymbols_gl + goog.i18n.DateTimeSymbols_gsw + goog.i18n.DateTimeSymbols_gu + goog.i18n.DateTimeSymbols_haw + goog.i18n.DateTimeSymbols_he + goog.i18n.DateTimeSymbols_hi + goog.i18n.DateTimeSymbols_hr + goog.i18n.DateTimeSymbols_hu + goog.i18n.DateTimeSymbols_hy + goog.i18n.DateTimeSymbols_id + goog.i18n.DateTimeSymbols_in + goog.i18n.DateTimeSymbols_is + goog.i18n.DateTimeSymbols_it + goog.i18n.DateTimeSymbols_iw + goog.i18n.DateTimeSymbols_ja + goog.i18n.DateTimeSymbols_ka + goog.i18n.DateTimeSymbols_kk + goog.i18n.DateTimeSymbols_km + goog.i18n.DateTimeSymbols_kn + goog.i18n.DateTimeSymbols_ko + goog.i18n.DateTimeSymbols_ky + goog.i18n.DateTimeSymbols_ln + goog.i18n.DateTimeSymbols_lo + goog.i18n.DateTimeSymbols_lt + goog.i18n.DateTimeSymbols_lv + goog.i18n.DateTimeSymbols_mk + goog.i18n.DateTimeSymbols_ml + goog.i18n.DateTimeSymbols_mn + goog.i18n.DateTimeSymbols_mo + goog.i18n.DateTimeSymbols_mr + goog.i18n.DateTimeSymbols_ms + goog.i18n.DateTimeSymbols_mt + goog.i18n.DateTimeSymbols_my + goog.i18n.DateTimeSymbols_nb + goog.i18n.DateTimeSymbols_ne + goog.i18n.DateTimeSymbols_nl + goog.i18n.DateTimeSymbols_no + goog.i18n.DateTimeSymbols_no_NO + goog.i18n.DateTimeSymbols_or + goog.i18n.DateTimeSymbols_pa + goog.i18n.DateTimeSymbols_pl + goog.i18n.DateTimeSymbols_pt + goog.i18n.DateTimeSymbols_pt_BR + goog.i18n.DateTimeSymbols_pt_PT + goog.i18n.DateTimeSymbols_ro + goog.i18n.DateTimeSymbols_ru + goog.i18n.DateTimeSymbols_sh + goog.i18n.DateTimeSymbols_si + goog.i18n.DateTimeSymbols_sk + goog.i18n.DateTimeSymbols_sl + goog.i18n.DateTimeSymbols_sq + goog.i18n.DateTimeSymbols_sr + goog.i18n.DateTimeSymbols_sr_Latn + goog.i18n.DateTimeSymbols_sv + goog.i18n.DateTimeSymbols_sw + goog.i18n.DateTimeSymbols_ta + goog.i18n.DateTimeSymbols_te + goog.i18n.DateTimeSymbols_th + goog.i18n.DateTimeSymbols_tl + goog.i18n.DateTimeSymbols_tr + goog.i18n.DateTimeSymbols_uk + goog.i18n.DateTimeSymbols_ur + goog.i18n.DateTimeSymbols_uz + goog.i18n.DateTimeSymbols_vi + goog.i18n.DateTimeSymbols_zh + goog.i18n.DateTimeSymbols_zh_CN + goog.i18n.DateTimeSymbols_zh_HK + goog.i18n.DateTimeSymbols_zh_TW + goog.i18n.DateTimeSymbols_zu + goog.i18n.DateTimeSymbolsType + goog.i18n.MessageFormat + goog.i18n.NumberFormat + goog.i18n.NumberFormat.CurrencyStyle + goog.i18n.NumberFormat.Format + goog.i18n.ordinalRules + goog.i18n.pluralRules + goog.i18n.TimeZone)) + +(def locales + {"af" ^js goog.i18n.DateTimeSymbols_af + "am" ^js goog.i18n.DateTimeSymbols_am + "ar" ^js goog.i18n.DateTimeSymbols_ar + "ar_DZ" ^js goog.i18n.DateTimeSymbols_ar_DZ + "ar_EG" ^js goog.i18n.DateTimeSymbols_ar_EG + "az" ^js goog.i18n.DateTimeSymbols_az + "be" ^js goog.i18n.DateTimeSymbols_be + "bg" ^js goog.i18n.DateTimeSymbols_bg + "bn" ^js goog.i18n.DateTimeSymbols_bn + "br" ^js goog.i18n.DateTimeSymbols_br + "bs" ^js goog.i18n.DateTimeSymbols_bs + "ca" ^js goog.i18n.DateTimeSymbols_ca + "chr" ^js goog.i18n.DateTimeSymbols_chr + "cs" ^js goog.i18n.DateTimeSymbols_cs + "cy" ^js goog.i18n.DateTimeSymbols_cy + "da" ^js goog.i18n.DateTimeSymbols_da + "de" ^js goog.i18n.DateTimeSymbols_de + "de_AT" ^js goog.i18n.DateTimeSymbols_de_AT + "de_CH" ^js goog.i18n.DateTimeSymbols_de_CH + "el" ^js goog.i18n.DateTimeSymbols_el + "en" ^js goog.i18n.DateTimeSymbols_en + "en_AU" ^js goog.i18n.DateTimeSymbols_en_AU + "en_CA" ^js goog.i18n.DateTimeSymbols_en_CA + "en_GB" ^js goog.i18n.DateTimeSymbols_en_GB + "en_IE" ^js goog.i18n.DateTimeSymbols_en_IE + "en_IN" ^js goog.i18n.DateTimeSymbols_en_IN + "en_ISO" ^js goog.i18n.DateTimeSymbols_en_ISO + "en_SG" ^js goog.i18n.DateTimeSymbols_en_SG + "en_US" ^js goog.i18n.DateTimeSymbols_en_US + "en_ZA" ^js goog.i18n.DateTimeSymbols_en_ZA + "es" ^js goog.i18n.DateTimeSymbols_es + "es_419" ^js goog.i18n.DateTimeSymbols_es_419 + "es_ES" ^js goog.i18n.DateTimeSymbols_es_ES + "es_MX" ^js goog.i18n.DateTimeSymbols_es_MX + "es_US" ^js goog.i18n.DateTimeSymbols_es_US + "et" ^js goog.i18n.DateTimeSymbols_et + "eu" ^js goog.i18n.DateTimeSymbols_eu + "fa" ^js goog.i18n.DateTimeSymbols_fa + "fi" ^js goog.i18n.DateTimeSymbols_fi + "fil" ^js goog.i18n.DateTimeSymbols_fil + "fr" ^js goog.i18n.DateTimeSymbols_fr + "fr_CA" ^js goog.i18n.DateTimeSymbols_fr_CA + "ga" ^js goog.i18n.DateTimeSymbols_ga + "gl" ^js goog.i18n.DateTimeSymbols_gl + "gsw" ^js goog.i18n.DateTimeSymbols_gsw + "gu" ^js goog.i18n.DateTimeSymbols_gu + "haw" ^js goog.i18n.DateTimeSymbols_haw + "he" ^js goog.i18n.DateTimeSymbols_he + "hi" ^js goog.i18n.DateTimeSymbols_hi + "hr" ^js goog.i18n.DateTimeSymbols_hr + "hu" ^js goog.i18n.DateTimeSymbols_hu + "hy" ^js goog.i18n.DateTimeSymbols_hy + "id" ^js goog.i18n.DateTimeSymbols_id + "in" ^js goog.i18n.DateTimeSymbols_in + "is" ^js goog.i18n.DateTimeSymbols_is + "it" ^js goog.i18n.DateTimeSymbols_it + "iw" ^js goog.i18n.DateTimeSymbols_iw + "ja" ^js goog.i18n.DateTimeSymbols_ja + "ka" ^js goog.i18n.DateTimeSymbols_ka + "kk" ^js goog.i18n.DateTimeSymbols_kk + "km" ^js goog.i18n.DateTimeSymbols_km + "kn" ^js goog.i18n.DateTimeSymbols_kn + "ko" ^js goog.i18n.DateTimeSymbols_ko + "ky" ^js goog.i18n.DateTimeSymbols_ky + "ln" ^js goog.i18n.DateTimeSymbols_ln + "lo" ^js goog.i18n.DateTimeSymbols_lo + "lt" ^js goog.i18n.DateTimeSymbols_lt + "lv" ^js goog.i18n.DateTimeSymbols_lv + "mk" ^js goog.i18n.DateTimeSymbols_mk + "ml" ^js goog.i18n.DateTimeSymbols_ml + "mn" ^js goog.i18n.DateTimeSymbols_mn + "mo" ^js goog.i18n.DateTimeSymbols_mo + "mr" ^js goog.i18n.DateTimeSymbols_mr + "ms" ^js goog.i18n.DateTimeSymbols_ms + "mt" ^js goog.i18n.DateTimeSymbols_mt + "my" ^js goog.i18n.DateTimeSymbols_my + "nb" ^js goog.i18n.DateTimeSymbols_nb + "ne" ^js goog.i18n.DateTimeSymbols_ne + "nl" ^js goog.i18n.DateTimeSymbols_nl + "no" ^js goog.i18n.DateTimeSymbols_no + "no_NO" ^js goog.i18n.DateTimeSymbols_no_NO + "or" ^js goog.i18n.DateTimeSymbols_or + "pa" ^js goog.i18n.DateTimeSymbols_pa + "pl" ^js goog.i18n.DateTimeSymbols_pl + "pt" ^js goog.i18n.DateTimeSymbols_pt + "pt_BR" ^js goog.i18n.DateTimeSymbols_pt_BR + "pt_PT" ^js goog.i18n.DateTimeSymbols_pt_PT + "ro" ^js goog.i18n.DateTimeSymbols_ro + "ru" ^js goog.i18n.DateTimeSymbols_ru + "sh" ^js goog.i18n.DateTimeSymbols_sh + "si" ^js goog.i18n.DateTimeSymbols_si + "sk" ^js goog.i18n.DateTimeSymbols_sk + "sl" ^js goog.i18n.DateTimeSymbols_sl + "sq" ^js goog.i18n.DateTimeSymbols_sq + "sr" ^js goog.i18n.DateTimeSymbols_sr + "sr_Latn" ^js goog.i18n.DateTimeSymbols_sr_Latn + "sv" ^js goog.i18n.DateTimeSymbols_sv + "sw" ^js goog.i18n.DateTimeSymbols_sw + "ta" ^js goog.i18n.DateTimeSymbols_ta + "te" ^js goog.i18n.DateTimeSymbols_te + "th" ^js goog.i18n.DateTimeSymbols_th + "tl" ^js goog.i18n.DateTimeSymbols_tl + "tr" ^js goog.i18n.DateTimeSymbols_tr + "uk" ^js goog.i18n.DateTimeSymbols_uk + "ur" ^js goog.i18n.DateTimeSymbols_ur + "uz" ^js goog.i18n.DateTimeSymbols_uz + "vi" ^js goog.i18n.DateTimeSymbols_vi + "zh" ^js goog.i18n.DateTimeSymbols_zh + "zh_CN" ^js goog.i18n.DateTimeSymbols_zh_CN + "zh_HK" ^js goog.i18n.DateTimeSymbols_zh_HK + "zh_TW" ^js goog.i18n.DateTimeSymbols_zh_TW + "zu" ^js goog.i18n.DateTimeSymbols_zu}) + +;; xx-YY locale, xx locale or en fallback +(defn locale-symbols + [locale-name] + (if-let [loc (get locales locale-name)] + loc + (let [name-first (string/replace (or locale-name "") #"-.*$" "") + loc (get locales name-first)] + (or loc goog.i18n.DateTimeSymbols_en)))) + +;; get formatter for current locale symbols and format function +(defn mk-fmt + [locale format-fn] + (let [locsym (locale-symbols locale)] + (goog.i18n.DateTimeFormat. (format-fn locsym) locsym))) diff --git a/src/utils/image_server.cljs b/src/utils/image_server.cljs new file mode 100644 index 00000000000..b6658199372 --- /dev/null +++ b/src/utils/image_server.cljs @@ -0,0 +1,217 @@ +;; Exception here as referrenced status-im.config, the implementation related to the image +;; server and the qr components leave something to be desired. Added an exception to the lint +;; rule as quick fix. Feel free to improve it if you're a brave soul refactor +(ns utils.image-server + (:require + [quo.foundations.colors :as colors] + [react-native.fs :as utils.fs] + [react-native.platform :as platform] + [schema.core :as schema] + [status-im.config :as config])) + +(def ^:const image-server-uri-prefix config/STATUS_BACKEND_SERVER_IMAGE_SERVER_URI_PREFIX) +(def ^:const account-images-action "/accountImages") +(def ^:const account-initials-action "/accountInitials") +(def ^:const contact-images-action "/contactImages") +(def ^:const generate-qr-action "/GenerateQRCode") + +(defn get-font-file-ready + "setup font file and get the absolute path to it + this font file is passed to status-go later to render the initials avatar + + for ios, it's located at main-bundle-path + for android, it's located in the assets dir which can not be accessed by status-go + so we copy one to the cache directory" + [font-file-name callback] + (if platform/android? + (let [cache-dir (utils.fs/cache-dir) + font-file-name font-file-name + src (str "fonts/" font-file-name) + dest (str cache-dir "/" font-file-name) + copy #(utils.fs/copy-assets src dest) + cb #(callback dest)] + (.then (utils.fs/file-exists? dest) + (fn [file?] + (if file? + (cb) + (.then (copy) cb))))) + (callback (str (utils.fs/main-bundle-path) + "/" + font-file-name)))) + +(defn current-theme-index + [theme] + (case theme + :light 1 + :dark 2)) + +(defn correction-level->index + [level] + (case (keyword level) + :low 1 + :medium 2 + :quart 3 + :highest 4 + 4)) + +(defn get-account-image-uri + "fn to get the avatar uri when multiaccount has custom image set. + + color formats (for all color options): + #RRGGBB + #RRGGBBAA + rgb(255,255,255) + rgba(255,255,255,0.1) note alpha is 0-1 + + non-placeholder-avatar: requires at least one of `public-key` or `key-uid` + placeholder-avatar: pass image file path as `image-name` + + `indicator-size` is outer indicator radius + `indicator-size` - `indicator-border` is inner indicator radius" + [{:keys [port public-key image-name key-uid size theme indicator-size + indicator-border indicator-center-to-edge indicator-color + ratio clock]}] + (str + image-server-uri-prefix + port + account-images-action + "?publicKey=" + public-key + "&keyUid=" + key-uid + "&imageName=" + image-name + "&size=" + (Math/round (* size ratio)) + "&theme=" + (current-theme-index theme) + "&clock=" + clock + "&indicatorColor=" + (js/encodeURIComponent indicator-color) + "&indicatorSize=" + (* indicator-size ratio) + "&indicatorBorder=" + (* indicator-border ratio) + "&indicatorCenterToEdge=" + (* indicator-center-to-edge ratio) + "&addRing=0")) + +(defn get-initials-avatar-uri + "fn to get the avatar uri when account/contact/placeholder has no custom pic set + + multiaccount: at least one of `key-uid`, `public-key` is required to render the ring + contact: `public-key` is required to render the ring + + check `get-account-image-uri` for color formats + check `get-font-file-ready` for `font-file` + + `uppercase-ratio` is the uppercase-height/line-height for `font-file`" + [{:keys [port public-key key-uid theme length size customization-color + color font-size font-file uppercase-ratio indicator-size + indicator-border indicator-center-to-edge indicator-color full-name + ratio clock]}] + (str + image-server-uri-prefix + port + account-initials-action + "?publicKey=" + public-key + "&keyUid=" + key-uid + "&length=" + length + "&size=" + (Math/round (* size ratio)) + "&bgColor=" + (js/encodeURIComponent (colors/resolve-color customization-color theme)) + "&color=" + (js/encodeURIComponent color) + "&fontSize=" + (* font-size ratio) + "&fontFile=" + (js/encodeURIComponent font-file) + "&uppercaseRatio=" + uppercase-ratio + "&theme=" + (current-theme-index theme) + "&clock=" + clock + "&name=" + (js/encodeURIComponent full-name) + "&indicatorColor=" + (js/encodeURIComponent indicator-color) + "&indicatorSize=" + (* indicator-size ratio) + "&indicatorBorder=" + (* indicator-border ratio) + "&indicatorCenterToEdge=" + (* indicator-center-to-edge ratio) + "&addRing=0")) + +(defn get-contact-image-uri + "check `get-account-image-uri` for color formats + check `get-font-file-ready` for `font-file` + + `public-key` is required to render the ring + `uppercase-ratio` is the uppercase-height/line-height for `font-file`" + [{:keys [port public-key image-name clock theme indicator-size indicator-border + indicator-center-to-edge indicator-color size ratio]}] + (str + image-server-uri-prefix + port + contact-images-action + "?publicKey=" + public-key + "&imageName=" + image-name + "&size=" + (Math/round (* size ratio)) + "&theme=" + (current-theme-index theme) + "&clock=" + clock + "&indicatorColor=" + (js/encodeURIComponent indicator-color) + "&indicatorSize=" + (* indicator-size ratio) + "&indicatorBorder=" + (* indicator-border ratio) + "&indicatorCenterToEdge=" + (* indicator-center-to-edge ratio) + "&addRing=0")) + +(defn get-qr-image-uri-for-any-url + [{:keys [url port qr-size error-level]}] + (let [qr-url-base64 (js/btoa url) + error-correction-level (correction-level->index error-level) + superimpose-profile? false + media-server-url (str image-server-uri-prefix + port + generate-qr-action + "?level=" + error-correction-level + "&url=" + qr-url-base64 + "&allowProfileImage=" + superimpose-profile? + "&size=" + qr-size)] + media-server-url)) + +(defn get-image-uri + [{:keys [type options]} + profile-picture-options] + ((case type + :account get-account-image-uri + :contact get-contact-image-uri + :initials get-initials-avatar-uri + str) + (merge options profile-picture-options))) + +(schema/=> get-image-uri + [:=> + [:cat + :schema.quo/image-uri-config + :schema.quo/profile-picture-options] + :string]) diff --git a/src/utils/image_server_test.cljs b/src/utils/image_server_test.cljs new file mode 100644 index 00000000000..8ba103ea112 --- /dev/null +++ b/src/utils/image_server_test.cljs @@ -0,0 +1,48 @@ +(ns utils.image-server-test + (:require + [cljs.test :as t] + [quo.foundations.colors :as colors] + [utils.image-server :as sut])) + +(t/deftest get-account-image-uri-test + (with-redefs + [sut/current-theme-index identity] + (t/is + (= + (sut/get-account-image-uri {:port "port" + :public-key "public-key" + :ratio 2 + :image-name "image-name" + :key-uid "key-uid" + :clock "timestamp" + :theme :dark + :indicator-size 2 + :indicator-color "rgba(9,16,28,0.08)" + :indicator-center-to-edge 6}) + "https://localhost:port/accountImages?publicKey=public-key&keyUid=key-uid&imageName=image-name&size=0&theme=:dark&clock=timestamp&indicatorColor=rgba(9%2C16%2C28%2C0.08)&indicatorSize=4&indicatorBorder=0&indicatorCenterToEdge=12&addRing=0")))) + +(t/deftest get-account-initials-uri-test + (with-redefs + [sut/current-theme-index identity + colors/resolve-color str] + (t/is + (= + (sut/get-initials-avatar-uri + {:port "port" + :public-key "public-key" + :ratio 2 + :key-uid "key-uid" + :clock "timestamp" + :full-name "full-name" + :length "length" + :size 48 + :theme :light + :customization-color :blue + :color "#0E162000" + :font-size 12 + :font-file "/font/Inter Medium.otf" + :uppercase-ratio 0.6 + :indicator-size 2 + :indicator-center-to-edge 6 + :indicator-color "#0E1620"}) + "https://localhost:port/accountInitials?publicKey=public-key&keyUid=key-uid&length=length&size=96&bgColor=%3Ablue%3Alight&color=%230E162000&fontSize=24&fontFile=%2Ffont%2FInter%20Medium.otf&uppercaseRatio=0.6&theme=:light&clock=timestamp&name=full-name&indicatorColor=%230E1620&indicatorSize=4&indicatorBorder=0&indicatorCenterToEdge=12&addRing=0")))) diff --git a/src/utils/map.cljs b/src/utils/map.cljs new file mode 100644 index 00000000000..a476b6bea2a --- /dev/null +++ b/src/utils/map.cljs @@ -0,0 +1,17 @@ +(ns utils.map + (:require [utils.money :as money])) + +(defn compare-values + "Compares two values, using special handling for BigNumbers and regular equality for others." + [v1 v2] + (cond + (and (money/bignumber? v1) (money/bignumber? v2)) (money/equal-to v1 v2) + :else (= v1 v2))) + +(defn deep-compare + "Recursively compare two maps, specially handling BigNumber values within the maps." + [map1 map2] + (and + (= (set (keys map1)) (set (keys map2))) + (every? (fn [k] (compare-values (get map1 k) (get map2 k))) + (keys map1)))) diff --git a/src/utils/money.cljs b/src/utils/money.cljs new file mode 100644 index 00000000000..bf97f44a0bb --- /dev/null +++ b/src/utils/money.cljs @@ -0,0 +1,260 @@ +(ns utils.money + (:require + ["bignumber.js" :as BigNumber] + [clojure.string :as string] + [schema.core :as schema] + [utils.i18n :as i18n])) + +;; The BigNumber version included in web3 sometimes hangs when dividing large +;; numbers Hence we want to use these functions instead of fromWei etc, which +;; come bundled with web3. See +;; https://github.com/MikeMcl/bignumber.js/issues/120 for this regression being +;; introduced in some JS environments. It is fixed in the MikeMcl/bignumber.js +;; repo, but not in the web3 BigNumber fork: +;; https://github.com/ethereum/web3.js/issues/877 +;; +;; Additionally, while it is possible to use the BigNumber constructor without +;; stringifying the number, this only works up to some 15 significant digits: +;; https://github.com/MikeMcl/bignumber.js/issues/120 +;; +;; Lastly, notice the bad rounding for native Javascript numbers above 17 digits +;; that may result in errors earlier up the call chain. Ideally all money-related +;; sensitive functions should be moved into this namespace to check for such +;; matters: +;; (str 111122223333441239) => "111122223333441230" + +(defn normalize + "A normalized string representation of an amount" + [s] + {:pre [(or (nil? s) (string? s))]} + (when s + (string/replace (string/trim s) #"," "."))) + +(defn bignumber + [n] + (when n + (try + (new BigNumber (normalize (str n))) + (catch :default _ nil)))) + +(defn bignumber? + "Check if the value is a bignumber." + [x] + (instance? BigNumber x)) + +(defn ->bignumber + [n] + (if (bignumber? n) n (bignumber n))) + +(defn ->bignumbers + [& numbers] + (let [transformed-numbers (map ->bignumber numbers)] + (when (every? bignumber? transformed-numbers) + transformed-numbers))) + +(defn greater-than-or-equals + [^js n1 ^js n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (.greaterThanOrEqualTo bn1 bn2))) + +(defn greater-than + [n1 n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (.greaterThan ^js bn1 bn2))) + +(defn less-than + [n1 n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (.lessThan ^js bn1 bn2))) + +(defn equal-to + [n1 n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (.eq ^js bn1 bn2))) + +(extend-type BigNumber + IEquiv + (-equiv [this other] + (if (or (number? other) + (string? other) + (instance? BigNumber other)) + (equal-to this other) + false)) + + IComparable + (-compare [this other] + (cond + (less-than this other) -1 + (greater-than this other) 1 + :else 0))) + +(defn sub + [n1 n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (.sub ^js bn1 bn2))) + +(defn valid? + [^js bn] + (when bn + (greater-than-or-equals bn 0))) + +(defn from-decimal + [n] + (when n + (str "1" (string/join (repeat n "0"))))) + +(def eth-units + {:wei (bignumber "1") + :kwei (bignumber (from-decimal 3)) + :mwei (bignumber (from-decimal 6)) + :gwei (bignumber (from-decimal 9)) + :szabo (bignumber (from-decimal 12)) + :finney (bignumber (from-decimal 15)) + :eth (bignumber (from-decimal 18)) + :keth (bignumber (from-decimal 21)) + :meth (bignumber (from-decimal 24)) + :geth (bignumber (from-decimal 27)) + :teth (bignumber (from-decimal 30))}) + +(defn wei-> + [unit n] + (when-let [^js bn (bignumber n)] + (.dividedBy bn (eth-units unit)))) + +(defn ->wei + [unit n] + (when-let [^js bn (bignumber n)] + (.times bn (eth-units unit)))) + +(defn to-fixed + ([^js bn] + (when bn + (.toFixed bn))) + ([^js bn b] + (when bn + (.toFixed bn b)))) + +(defn to-string + ([^js bn] + (to-string bn 10)) + ([^js bn base] + (when bn + (.toString bn base)))) + +(defn to-hex + [^js bn] + (when bn + (str "0x" (to-string bn 16)))) + +(defn from-hex + [hex-str] + (try + (new BigNumber hex-str 16) + (catch :default _ nil))) + +(defn wei->ether + [n] + (wei-> :eth n)) + +(defn wei->gwei + [n] + (wei-> :gwei n)) + +(defn gwei->wei + [n] + (->wei :gwei n)) + +(defn token->unit + [n decimals] + (when-let [^js bn (bignumber n)] + (when-let [d (from-decimal decimals)] + (.dividedBy bn ^js (bignumber d))))) + +(defn with-precision + [n decimals] + (when-let [^js bn (bignumber n)] + (.round bn decimals))) + +(defn crypto->fiat + [crypto fiat-price] + (let [^js crypto-bn (bignumber crypto) + ^js fiat-price-bn (bignumber fiat-price)] + (when (and crypto-bn fiat-price-bn) + (-> crypto-bn + (.times fiat-price-bn))))) + +(defn above-zero? + [^js balance] + (when balance + (->> 0 + bignumber + (greater-than balance)))) + +(defn sufficient-funds? + [^js amount ^js balance] + (when (and amount balance) + (greater-than-or-equals balance amount))) + +(defn- add* + [n1 n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (.add ^js bn1 bn2))) + +(def add + "Add with defaults, this version is able to receive `nil` and takes them as 0." + (fnil add* (bignumber 0) (bignumber 0))) + +(defn- mul* + [bn1 bn2] + (.mul ^js bn1 bn2)) + +(def mul + "Multiply with defaults, this version is able to receive `nil` and takes them as 0." + (fnil mul* (bignumber 0) (bignumber 0))) + +(defn- div* + [bn1 bn2] + (.dividedBy ^js bn1 bn2)) + +(def div + "Divides with defaults, this version is able to receive `nil` and takes them as 0." + (fnil div* (bignumber 0) (bignumber 1))) + +(defn fiat->crypto + [crypto fiat-price] + (when-let [crypto-bn (bignumber crypto)] + (div crypto-bn + (bignumber fiat-price)))) + +(defn absolute-value + [bn] + (when (bignumber? bn) + (.absoluteValue ^js bn))) + +(defn format-amount + "Format `amount` to thousands or millions. Return nil if `amount` is not truthy." + [amount] + (when amount + (cond + (> amount 999999) + (str (with-precision (/ amount 1000000) 1) (i18n/label :t/M)) + + (< 999 amount 1000000) + (str (with-precision (/ amount 1000) 1) (i18n/label :t/K)) + + :else + (str amount)))) + +(schema/=> format-amount + [:=> [:cat [:maybe :int]] + [:maybe :string]]) + +(defn maximum + [n1 n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (if (greater-than-or-equals bn1 bn2) bn1 bn2))) + +(defn minimum + [n1 n2] + (when-let [[^js bn1 ^js bn2] (->bignumbers n1 n2)] + (if (less-than bn1 bn2) bn1 bn2))) diff --git a/src/utils/money_test.cljs b/src/utils/money_test.cljs new file mode 100644 index 00000000000..556db66ce36 --- /dev/null +++ b/src/utils/money_test.cljs @@ -0,0 +1,92 @@ +(ns utils.money-test + (:require + [cljs.test :refer-macros [deftest testing is are]] + [utils.money :as money])) + +(deftest comparable-test + (testing "sorts bignumbers (and nils)" + (is (= [nil + nil + (money/bignumber -4) + (money/bignumber 0) + (money/bignumber 1) + (money/bignumber 1.1) + (money/bignumber 2.1)] + (sort [(money/bignumber 0) + nil + (money/bignumber 2.1) + (money/bignumber -4) + (money/bignumber 1.1) + (money/bignumber nil) + (money/bignumber 1)])))) + + (testing "throws when comparing non-bignumbers with bignumbers" + (is (thrown? js/Error (sort [(money/bignumber 42) ""]))))) + +(deftest equivalence-test + (testing "equivalence with numbers and strings" + (is (= (money/bignumber 42) 42)) + (is (= (money/bignumber 42) 42.0)) + (is (= (money/bignumber 42) "42")) + (is (= (money/bignumber 42) "42.0")) + (is (not= (money/bignumber 42) :42)) + (is (not= (money/bignumber 42) {:x 42}))) + + (testing "bignumbers are never equivalent to nil" + ;; The native `.eq` function throws with nil values, but we gracefully handle this. + (is (false? (= (money/bignumber 0) nil))) + (is (false? (= (money/bignumber 10) (money/bignumber nil))))) + + (testing "equivalence of actual bignumbers" + (is (= (money/bignumber 0) (money/bignumber 0))) + (is (= (money/bignumber -1) (money/bignumber -1))) + (is (not= (money/bignumber 10) (money/bignumber -10))) + (is (match? {:a {:b {:c (money/bignumber 42)}}} + {:a {:b {:c (money/bignumber 42)}}})))) + +(deftest wei->ether-test + (testing "Numeric input, 15 significant digits" + (is (= (str (money/wei->ether 111122223333444000)) + "0.111122223333444"))) + (testing "String input, 18 significant digits" + (is (= (str (money/wei->ether "111122223333441239")) + "0.111122223333441239")))) + +(deftest valid?-test + (is (not (true? (money/valid? nil)))) + (is (true? (money/valid? (money/bignumber 0)))) + (is (true? (money/valid? (money/bignumber 1)))) + (is (not (true? (money/valid? (money/bignumber -1)))))) + +(deftest normalize-test + (is (= nil (money/normalize nil))) + (is (= "1" (money/normalize " 1 "))) + (is (= "1.1" (money/normalize "1.1"))) + (is (= "1.1" (money/normalize "1,1")))) + +(deftest format-amount-test + (are [amount expected] + (= expected (money/format-amount amount)) + nil nil + 0 "0" + 1 "1" + 10 "10" + 100 "100" + 999 "999" + 1000 "1K" + 1001 "1K" + 10000 "10K" + 100000 "100K" + 999999 "1000K" + 1000000 "1M" + 1000000 "1M" + 1000001 "1M" + 10000000 "10M" + 100000000 "100M")) + +(deftest from-hex-test + (is (= (money/bignumber "2840425681744351250") (money/from-hex "276b381bbb44d012"))) + (is (= (money/bignumber "0") (money/from-hex "0"))) + (is (= (money/bignumber "255") (money/from-hex "ff"))) + (is (= (money/bignumber "4294967295") (money/from-hex "ffffffff"))) + (is (= (money/bignumber "12345678901234567890") (money/from-hex "ab54a98ceb1f0ad2")))) diff --git a/src/utils/number.cljs b/src/utils/number.cljs new file mode 100644 index 00000000000..f154fe92a63 --- /dev/null +++ b/src/utils/number.cljs @@ -0,0 +1,142 @@ +(ns utils.number + (:require [clojure.string :as string] + [native-module.core :as native-module] + [utils.hex :as utils.hex] + [utils.money :as money])) + +(defn naive-round + "Quickly and naively round number `n` up to `decimal-places`. + + Example usage: use it to avoid re-renders caused by floating-point number + changes in Reagent atoms. Such numbers can be rounded up to a certain number + of `decimal-places` in order to avoid re-rendering due to tiny fractional + changes. + + Don't use this function for arbitrary-precision arithmetic." + [n decimal-places] + (let [scale (Math/pow 10 decimal-places)] + (/ (Math/round (* n scale)) + scale))) + +(defn parse-int + "Parses `n` as an integer. Defaults to zero or `default` instead of NaN." + ([n] + (parse-int n 0)) + ([n default] + (let [maybe-int (js/parseInt n 10)] + (if (integer? maybe-int) + maybe-int + default)))) + +(defn parse-float + "Parses `n` as a float. Defaults to zero or `default` instead of NaN." + ([n] + (parse-float n 0)) + ([n default] + (let [maybe-float (js/parseFloat n 10)] + (if (js/Number.isNaN maybe-float) + default + maybe-float)))) + +(defn value-in-range + "Returns `num` if is in the range [`lower-bound` `upper-bound`] + if `num` exceeds a given bound, then returns the bound exceeded." + [number lower-bound upper-bound] + (max lower-bound (min number upper-bound))) + +(defn remove-trailing-zeroes + [num] + (let [parts (string/split (str num) #"\.")] + (str (first parts) + (if-let [decimals (second parts)] + (if (seq (string/replace decimals #"0+$" "")) + (str "." (string/replace decimals #"0+$" "")) + "") + "")))) + +(defn convert-to-whole-number + "Converts a fractional `amount` to its corresponding whole number representation + by dividing it by 10 raised to the power of `decimals`. This is often used in financial + calculations where amounts are stored in their smallest units (e.g., cents) and need + to be converted to their whole number equivalents (e.g., dollars). + + Example usage: + (convert-to-whole-number 12345 2) ; => 123.45" + [amount decimals] + (when-let [[amount-bn divisor-bn] (money/->bignumbers amount (money/from-decimal decimals))] + (-> (money/div amount-bn divisor-bn) + (money/to-fixed decimals) + remove-trailing-zeroes))) + +(defn hex->whole + [num decimals] + (-> num + utils.hex/normalize-hex + native-module/hex-to-number + (convert-to-whole-number decimals) + money/bignumber)) + +(defn to-fixed + [num decimals] + (-> num + (money/to-fixed decimals) + remove-trailing-zeroes)) + +(defn small-number-threshold + "Receives a decimal count and returns a string like '<0.001' if the decimal count is 3, + '<0.000001' if the decimal count is 6, etc." + ([decimal-count] + (small-number-threshold decimal-count "")) + ([decimal-count currency-symbol] + (if (> decimal-count 0) + (str "<" currency-symbol "0." (apply str (repeat (dec decimal-count) "0")) "1") + (str currency-symbol "0")))) + +(defn valid-decimal-count? + "Returns false if the number has more decimals than the decimal count, otherwise true." + [num decimal-count] + (let [decimal-part (second (string/split (str num) #"\."))] + (or (nil? decimal-part) (<= (count decimal-part) decimal-count)))) + +(defn format-decimal-fixed + "Formats a number `n` to `decimal-places` without rounding. + - Ensures the exact number of decimal places (including trailing zeros). + - Does not round up or down, just trims excess decimals." + [n decimal-places] + (let [bn (money/bignumber n) + scale (money/bignumber (money/from-decimal decimal-places)) + truncated (if (money/less-than bn 0) + (Math/ceil (money/mul bn scale)) + (Math/floor (money/mul bn scale)))] + (-> truncated + (money/bignumber) + (money/div scale) + (money/to-fixed decimal-places)))) + +(defn format-as-percentage + "Formats a number to be shown as a percentage with two decimal places." + [change] + (-> change + (* 100) + (naive-round 2))) + +(defn format-number-with-si-suffix + "Formats a large number using SI suffixes for readability. + Given a number n, this function converts it to a string representation + with appropriate SI suffixes: + - 'K' for thousands (10^3) + - 'M' for millions (10^6) + - 'B' for billions (10^9) + - 'T' for trillions (10^12) + If the number is less than one million, it is returned as-is without + any suffix." + [n] + (let [letters '("K" "M" "B" "T")] + (loop [l letters + value n] + (if (< value 1000) + (str value) + (let [divided (naive-round (/ value 1000) 1)] + (if (or (< divided 1000) (empty? l)) + (str divided (first l)) + (recur (rest l) divided))))))) diff --git a/src/utils/number_test.cljs b/src/utils/number_test.cljs new file mode 100644 index 00000000000..bd7b17e1faa --- /dev/null +++ b/src/utils/number_test.cljs @@ -0,0 +1,124 @@ +(ns utils.number-test + (:require + [cljs.test :refer [are deftest is testing]] + [utils.money :as money] + [utils.number])) + +(deftest convert-to-whole-number-test + (testing "correctly converts fractional amounts to whole numbers" + (is (= "123.45" (utils.number/convert-to-whole-number 12345 2))) + (is (= "1.2345" (utils.number/convert-to-whole-number 12345 4))) + (is (= "12345" (utils.number/convert-to-whole-number 1234500 2))) + (is (= "0.123" (utils.number/convert-to-whole-number 123 3))) + (is (= "1000" (utils.number/convert-to-whole-number 1000000 3)))) + + (testing "handles zero decimals" + (is (= "12345" (utils.number/convert-to-whole-number 12345 0)))) + + (testing "handles negative amounts" + (is (= "-123.45" (utils.number/convert-to-whole-number -12345 2))) + (is (= "-1.2345" (utils.number/convert-to-whole-number -12345 4))) + (is (= "-0.123" (utils.number/convert-to-whole-number -123 3)))) + + (testing "handles zero amount" + (is (= "0" (utils.number/convert-to-whole-number 0 2))) + (is (= "0" (utils.number/convert-to-whole-number 0 0)))) + + (testing "handles BigNumber amounts with 18 decimals" + (let [amount (money/bignumber "3141969777175276657")] + (is (= "3.141969777175276657" (utils.number/convert-to-whole-number amount 18)))))) + +(deftest parse-int-test + (testing "defaults to zero" + (is (= 0 (utils.number/parse-int nil)))) + + (testing "accepts any other default value" + (is (= 3 (utils.number/parse-int "" 3))) + (is (= :invalid-int (utils.number/parse-int "" :invalid-int)))) + + (testing "valid numbers" + (is (= -6 (utils.number/parse-int "-6a" 0))) + (is (= 6 (utils.number/parse-int "6" 0))) + (is (= 6 (utils.number/parse-int "6.99" 0))) + (is (= -6 (utils.number/parse-int "-6" 0))))) + +(deftest parse-float-test + (testing "defaults to zero" + (is (= 0 (utils.number/parse-float nil)))) + + (testing "accepts any other default value" + (is (= 3 (utils.number/parse-float "" 3))) + (is (= :invalid-float (utils.number/parse-float "" :invalid-float)))) + + (testing "valid numbers" + (is (= -6 (utils.number/parse-float "-6a"))) + (is (= 6 (utils.number/parse-float "6"))) + (is (= 6.99 (utils.number/parse-float "6.99" 0))) + (is (= -6.9 (utils.number/parse-float "-6.9" 0))))) + +(deftest small-number-threshold-test + (testing "correctly generates threshold strings based on decimal count" + (is (= "<0.1" (utils.number/small-number-threshold 1))) + (is (= "<0.01" (utils.number/small-number-threshold 2))) + (is (= "<0.001" (utils.number/small-number-threshold 3))) + (is (= "<0.000001" (utils.number/small-number-threshold 6))) + (is (= "<0.0000000001" (utils.number/small-number-threshold 10))) + (is (= "<0.000000000000000001" (utils.number/small-number-threshold 18))) + (is (= "<0.000000000000000000001" (utils.number/small-number-threshold 21)))) + + (testing "handles edge cases for decimal count" + (is (= "0" (utils.number/small-number-threshold 0))) + (is (= "0" (utils.number/small-number-threshold -1))))) + +(deftest valid-decimal-count?-test + (testing "valid decimal count check for numbers with varying decimals" + (is (true? (utils.number/valid-decimal-count? 123 2))) + (is (true? (utils.number/valid-decimal-count? 123 0))) + + (is (true? (utils.number/valid-decimal-count? 123.45 2))) + (is (true? (utils.number/valid-decimal-count? 123.4 2))) + (is (true? (utils.number/valid-decimal-count? 123.456 3))) + + (is (false? (utils.number/valid-decimal-count? 123.456 2))) + (is (false? (utils.number/valid-decimal-count? 123.456789 4))) + + (is (true? (utils.number/valid-decimal-count? 123.0 1))) + (is (true? (utils.number/valid-decimal-count? -123.45 2))) + (is (false? (utils.number/valid-decimal-count? -123.4567 3))) + + (is (true? (utils.number/valid-decimal-count? 1234567890.12 2))) + (is (false? (utils.number/valid-decimal-count? 1234567890.12345 3))))) + +(deftest format-decimal-fixed-test + (testing "Format decimal numbers correctly without rounding" + (is (= "123.45" (utils.number/format-decimal-fixed "123.456" 2))) + (is (= "123.456" (utils.number/format-decimal-fixed "123.456" 3))) + + (is (= "123.45600" (utils.number/format-decimal-fixed "123.456" 5))) + (is (= "123.000" (utils.number/format-decimal-fixed "123.000" 3))) + + (is (= "123" (utils.number/format-decimal-fixed "123.999" 0))) + + (is (= "-123.4" (utils.number/format-decimal-fixed "-123.456" 1))) + (is (= "-123.45" (utils.number/format-decimal-fixed "-123.456" 2))) + + (is (= "0.0006" (utils.number/format-decimal-fixed "0.000650462672354754" 4))) + (is (= "999999999.999" (utils.number/format-decimal-fixed "999999999.999999" 3))))) + +(deftest format-number-with-si-suffix-test + (testing "formats larger numbers with SI suffixes correctly" + (are [n expected] (= expected (utils.number/format-number-with-si-suffix n)) + 1e6 "1M" + 12345678 "12.3M" + 1200000000 "1.2B" + 3500000000000 "3.5T" + 999999 "1M" + 1e12 "1T" + 999900000000 "999.9B" + 1.0e12 "1T" + 0 "0" + 10 "10" + 14199 "14.2K" + 500000 "500K" + 999990000 "1B" + 0.000999 "0.000999"))) diff --git a/src/utils/promesa.clj b/src/utils/promesa.clj new file mode 100644 index 00000000000..58c042922e0 --- /dev/null +++ b/src/utils/promesa.clj @@ -0,0 +1,15 @@ +(ns utils.promesa + (:require [promesa.core :as promesa])) + +(defmacro all-seq + "A drop-in replacement for promesa/all which runs the promises in + sequence, instead of running them in parallel." + [bindings] + `(promesa/loop [results# [] + remaining# ~bindings] + (if (empty? remaining#) + (promesa/resolved results#) + (promesa/let [result# (first remaining#) + value# (promesa/resolved result#)] + (promesa/recur (conj results# value#) (rest remaining#)))))) + diff --git a/src/utils/re_frame.clj b/src/utils/re_frame.clj new file mode 100644 index 00000000000..4d50b634fe5 --- /dev/null +++ b/src/utils/re_frame.clj @@ -0,0 +1,91 @@ +(ns utils.re-frame (:refer-clojure :exclude [defn])) + +(defn- register-events + [events interceptors name argsyms] + (mapv (fn [event] + `(re-frame.core/reg-event-fx + ~event + ~interceptors + (fn [cofx# [~@argsyms]] (~name cofx# ~@argsyms)))) + events)) + +(defn- fully-qualified-name + [sym] + (str *ns* "/" (name sym))) + +(defmacro defn + "Defines an fx producing function + Takes the same arguments as the defn macro + Produces a 2 arity function: + - first arity takes the declared parameters and returns a function that takes cofx as + single argument, for use in composition of effects + - second arity takes cofx as first arguments and declared parameters as next arguments, + for use in repl or direct call + Notes: + - destructuring of cofx is possible + - supports docstring + - supports attr-map with optional :events key which needs to be a vector of + event keywords under which the function will be registered + - TODO: add suport for `prepost-map?` (don't forget to add it to arglist) + - TODO: add validation of macro parameters" + {:arglists '([name doc-string? attr-map? [params*] body]) + :superseded-by "utils.re-frame/reg-event-fx"} + [name & fdecl] + (let [m (if (string? (first fdecl)) + {:doc (first fdecl)} + {}) + fdecl (if (string? (first fdecl)) + (next fdecl) + fdecl) + m (if (map? (first fdecl)) + (conj m (first fdecl)) + m) + events (get m :events []) + interceptors (get m :interceptors []) + fdecl (if (map? (first fdecl)) + (next fdecl) + fdecl) + [cofx & args] (first fdecl) + fdecl (next fdecl) + argsyms (take (count args) (repeatedly #(gensym "arg")))] + (if (and (sequential? events) + (every? keyword? events)) + `(do + (clojure.core/defn ~(with-meta name m) + ([~@argsyms] (fn [cofx#] (~(with-meta name m) cofx# ~@argsyms))) + ([cofx# ~@args] + (when js/goog.DEBUG + (when (taoensso.timbre/level>= + :trace + (:min-level taoensso.timbre/*config*)) + (println + (clojure.string/join + (concat + (repeat + (deref utils.re-frame/handler-nesting-level) + "│ ") + ["├─"])) + ~(str (clojure.core/name name) " " *ns*)))) + (if (and (map? cofx#) + (not (nil? (:db cofx#)))) + (let [res# (let [~cofx cofx#] ~@fdecl)] + (when-not (nil? res#) + (aset res# + "cljs$core$ILookup$_lookup$arity$2" + (fn [foo# k#] + (clojure.core/this-as + m# + (when (and (map? k#) + (contains? k# :db)) + (throw (js/Error. (str + "fx/defn's result is used as fx producing function in " + ~(fully-qualified-name name))))) + (get m# k# nil))))) + res#) + (throw (js/Error. (str "fx/defn expects a map of cofx as first argument got " cofx# + " in function " ~(fully-qualified-name name))))))) + ~@(register-events events interceptors (with-meta name m) argsyms)) + (throw + (Exception. + (str "fx/defn expects a vector of keyword as value for :events key in attr-map in function " + name)))))) diff --git a/src/utils/re_frame.cljs b/src/utils/re_frame.cljs new file mode 100644 index 00000000000..f8ba54dcc04 --- /dev/null +++ b/src/utils/re_frame.cljs @@ -0,0 +1,138 @@ +(ns utils.re-frame + (:require-macros utils.re-frame) + (:require + [re-frame.core :as re-frame] + [re-frame.interceptor :as interceptor] + [re-frame.interop :as rf.interop] + [taoensso.timbre :as log] + [utils.datetime :as datetime]) + (:refer-clojure :exclude [merge reduce])) + +(def handler-nesting-level (atom 0)) + +(re-frame/reg-cofx :now (fn [coeffects _] (assoc coeffects :now (datetime/timestamp)))) + +(def debug-handlers-names + "Interceptor which logs debug information to js/console for each event." + (interceptor/->interceptor + :id :debug-handlers-names + :before (fn debug-handlers-names-before + [context] + (when js/goog.DEBUG + (reset! handler-nesting-level 0)) + (when rf.interop/debug-enabled? + (log/debug "Handling re-frame event: " + (first (interceptor/get-coeffect context :event)))) + context))) + +(defn- update-db + [cofx fx] + (if-let [db (:db fx)] + (assoc cofx :db db) + cofx)) + +(def ^:private mergeable-keys (atom nil)) + +(defn set-mergeable-keys + [v] + (reset! mergeable-keys v)) + +(defn- safe-merge + [fx new-fx] + (if (:merging-fx-with-common-keys fx) + fx + (clojure.core/reduce (fn [merged-fx [k v]] + (if (= :db k) + (assoc merged-fx :db v) + (if (get merged-fx k) + (if (get @mergeable-keys k) + (update merged-fx k into v) + (do (log/error "Merging fx with common-key: " k v (get merged-fx k)) + (reduced {:merging-fx-with-common-keys k}))) + (assoc merged-fx k v)))) + fx + new-fx))) + +(defn merge + "Takes a map of co-effects and forms as argument. + The first optional form can be map of effects + The next forms are functions applying effects and returning a map of effects. + The fn ensures that updates to db are passed from function to function within the cofx :db key and + that only a :merging-fx-with-common-keys effect is returned if some functions are trying + to produce the same effects (excepted :db, :data-source/tx effects). + :data-source/tx and effects are handled specially and their results + (list of transactions) are compacted to one transactions list (for each effect). " + [{:keys [db] :as cofx} & args] + (when js/goog.DEBUG + (swap! handler-nesting-level inc)) + (let [[first-arg & rest-args] args + initial-fxs? (map? first-arg) + fx-fns (if initial-fxs? rest-args args) + res + (clojure.core/reduce (fn [fxs fx-fn] + (let [updated-cofx (update-db cofx fxs)] + (if fx-fn + (safe-merge fxs (fx-fn updated-cofx)) + fxs))) + (if initial-fxs? first-arg {:db db}) + fx-fns)] + (swap! handler-nesting-level dec) + res)) + +(def sub (comp deref re-frame/subscribe)) + +(def reg-sub re-frame/reg-sub) + +(def dispatch re-frame/dispatch) + +(def reg-fx re-frame/reg-fx) + +(def dispatch-sync re-frame/dispatch-sync) + +(def reg-event-fx re-frame/reg-event-fx) + +(defn reg-root-key-sub + [sub-name db-key] + (re-frame/reg-sub sub-name (fn [db] (get db db-key)))) + +(defn call-continuation + "Choose how to call a continuation for a Re-Frame event or effect. + + When defining an event or effect, we can receive `on-success` and `on-error` + parameters for continuing the logic depending on if it succeeded or failed. + When we attempt to continue the logic, we can choose to either dispatch a Re-Frame event, + or we can call a callback function. + + Code example: + + (rf/reg-event-fx :my-event + (fn [_ [arg on-success on-error]] + {:fx [[:my-effect [arg on-success on-error]]]})) + + (rf/reg-event-fx :my-event-success + (fn [db [result]] + {:db (assoc db :my-event-result result)})) + + (rf/reg-event-fx :my-event-error + (fn [db [error]] + {:db (assoc db :my-event-error error)})) + + (rf/reg-fx :my-effect + (fn [[arg on-success on-error]] + (-> (my-effect-impl arg) + (promesa/then (partial call-continuation on-success)) + (promesa/catch (partial call-continuation on-error))))) + + (rf/dispatch [:my-event + :arg + [:my-event-success] + (fn [error] + (log/error error) + (rf/dispatch [:my-event-error error]))])" + [continuation & args] + (cond + (vector? continuation) (dispatch (into continuation args)) + (fn? continuation) (apply continuation args) + :else (throw (ex-info + (str "Unsupported continuation: " (type->str continuation)) + {:hint "Make sure to pass a vector or function"})))) diff --git a/src/utils/re_frame_test.cljs b/src/utils/re_frame_test.cljs new file mode 100644 index 00000000000..df9d8fedd0d --- /dev/null +++ b/src/utils/re_frame_test.cljs @@ -0,0 +1,87 @@ +(ns utils.re-frame-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [utils.re-frame :as rf])) + +(rf/defn hello + "this is a very nice useless function" + [{:keys [db]} a] + {:db (assoc db :a a)}) + +(rf/defn hello2 + {:doc "this function is useless as well"} + [{:keys [db]} b] + {:db (assoc db :a b) :b (:a db)}) + +(rf/defn hello3 + {:doc "lol lazy function does nothing"} + [_ _] + (identity nil)) + +(rf/defn hello4 + {:doc "this function is useless as well" + :events [:test/valid1 :test/valid2]} + [{:keys [db]} b] + {:db (assoc db :a b) :b (:a db)}) + +(rf/defn hello5 + "lol lazy function does nothing" + {:events [:test]} + [_ _] + (identity nil)) + +(rf/defn hello6 + "lol lazy function does nothing" + {:events [:test2]} + [_ {:keys [a b]}] + {:db {:a (identity a) + :b b}}) + +(deftest merge-fxs-test + (testing "merge function for fxs" + (let [cofx {:db {:c 2}}] + (is (= (rf/merge cofx + (hello "a") + (hello2 "b")) + {:db {:c 2 + :a "b"} + :b "a"})) + (testing "with initial fxs map" + (is (= (rf/merge cofx + {:potatoe :potating} + (hello "a") + (hello2 "b")) + {:db {:c 2 + :a "b"} + :b "a" + :potatoe :potating}) + "initial fxs map should be merged in the result")) + (testing "with a nil producing function" + (is (= (rf/merge cofx + (hello "a") + (hello3 "c") + (hello2 "b")) + {:db {:c 2 + :a "b"} + :b "a"}))) + (testing "with condition statement" + (testing "false" + (is (= (let [do-hello? false] + (rf/merge cofx + (when do-hello? + (hello "a")) + (hello2 "b"))) + {:db {:c 2 + :a "b"} + :b nil}) + "the conditional statement should not apply")) + (testing "true" + (is (= (let [do-hello? true] + (rf/merge cofx + (when do-hello? + (hello "a")) + (hello2 "b"))) + {:db {:c 2 + :a "b"} + :b "a"}) + "the conditional statement should apply")))))) diff --git a/src/utils/red_black_tree.cljs b/src/utils/red_black_tree.cljs new file mode 100644 index 00000000000..111f30eb1bb --- /dev/null +++ b/src/utils/red_black_tree.cljs @@ -0,0 +1,52 @@ +(ns utils.red-black-tree + (:refer-clojure :exclude [remove update find]) + (:require + ["functional-red-black-tree" :as red-black-tree])) + +(def tree ^js red-black-tree) + +(defn find + [^js t item] + (.find t item)) + +(defn insert + [^js t item] + (.insert t item)) + +(defn update + [^js iterator item] + (.update iterator item)) + +(defn remove + [^js iterator] + (.remove iterator)) + +(defn get-values + [^js t] + (.-values ^js t)) + +(defn get-prev-element + "Get previous item in the iterator, and wind it back to the initial state" + [^js iterator] + (.prev iterator) + (let [e (.-value iterator)] + (.next iterator) + e)) + +(defn get-prev + [^js iterator] + (when (.-hasPrev iterator) + (get-prev-element iterator))) + +(defn get-next-element + "Get next item in the iterator, and wind it back to the initial state" + [^js iterator] + (.next iterator) + (let [e (.-value iterator)] + (.prev iterator) + e)) + +(defn get-next + [^js iterator] + (when (.-hasNext iterator) + (get-next-element iterator))) diff --git a/src/utils/responsiveness.cljs b/src/utils/responsiveness.cljs new file mode 100644 index 00000000000..671cbf465cd --- /dev/null +++ b/src/utils/responsiveness.cljs @@ -0,0 +1,10 @@ +(ns utils.responsiveness) + +(def ^:const IPHONE_11_PRO_VIEWPORT_WIDTH 375) + +(defn iphone-11-Pro-20-pixel-from-width + [window-width] + ;; Divide iPhone 11 Pro VW by the desired value + (let [calculate-ratio (/ IPHONE_11_PRO_VIEWPORT_WIDTH 20)] + ;; Divide window width by the ratio to get a dynamic value. Based on the VW width + (int (/ window-width calculate-ratio)))) diff --git a/src/utils/security/core.cljs b/src/utils/security/core.cljs new file mode 100644 index 00000000000..029d723ceb2 --- /dev/null +++ b/src/utils/security/core.cljs @@ -0,0 +1,82 @@ +(ns utils.security.core + (:require + [native-module.core :as native-module] + [schema.core :as schema] + [utils.security.security-html :as h])) + +(defprotocol Unmaskable + ;; Retrieve the stored value. + (unmask [this])) + +;; MaskedData ensures that the object passed to it won't be occasionally printed +;; via println or log functions. Useful for keeping sensitive data, such as passwords +;; to avoid accidentally exposing them. +(deftype MaskedData [data] + Object + (toString [_] "******") + + ICounted + (-count [^js this] + (count (.-data this))) + + IEquiv + (-equiv [this other] + (if (instance? MaskedData other) + (= (unmask this) + (unmask other)) + false)) + + Unmaskable + (unmask [^js this] + (.-data this))) + +;; Returns a MaskedData instance that stores the piece of data. +(defn mask-data + [data] + (MaskedData. data)) + +(defn safe-unmask-data + [data] + (if (instance? MaskedData data) + (unmask data) + data)) + +;; Links starting with javascript:// should not be handled at all +(def javascript-link-regex #"(?i)javascript://.*") +;; Anything with rtlo character we don't handle as it might be a spoofed url +(def rtlo-link-regex #".*\u202e.*") + +(defn safe-link? + "Check the link is safe to be handled, it is not a javavascript link or contains + an rtlo character, which might mean is a spoofed url" + [link] + (let [decoded-link (js/decodeURIComponent link)] + (not (or (re-matches javascript-link-regex decoded-link) + (re-matches rtlo-link-regex decoded-link) + (h/is-html? decoded-link))))) + +(defn safe-link-text? + "Check the text of the message containing a link is safe to be handled + and does not contain an rtlo character, which might mean that the url is spoofed" + [text] + (not (re-matches rtlo-link-regex text))) + +(defn hash-masked-password + [masked-password] + (-> masked-password + safe-unmask-data + native-module/sha3 + mask-data)) + +(defn masked-data-instance? + [value] + (instance? MaskedData value)) + +(def ?masked-password + [:fn {:error/message "should be an instance of utils.security.core/MaskedData"} + masked-data-instance?]) + +(schema/=> hash-masked-password + [:=> + [:cat ?masked-password] + ?masked-password]) diff --git a/src/utils/security/security_html.cljs b/src/utils/security/security_html.cljs new file mode 100644 index 00000000000..bee7d34621a --- /dev/null +++ b/src/utils/security/security_html.cljs @@ -0,0 +1,136 @@ +(ns utils.security.security-html + (:require + [clojure.string :as string])) + +; Taken from https://github.com/sindresorhus/is-html + +(def html-tags + ["a" + "abbr" + "address" + "area" + "article" + "aside" + "audio" + "b" + "base" + "bdi" + "bdo" + "blockquote" + "body" + "br" + "button" + "canvas" + "caption" + "cite" + "code" + "col" + "colgroup" + "data" + "datalist" + "dd" + "del" + "details" + "dfn" + "dialog" + "div" + "dl" + "dt" + "em" + "embed" + "fieldset" + "figcaption" + "figure" + "footer" + "form" + "h1" + "h2" + "h3" + "h4" + "h5" + "h6" + "head" + "header" + "hgroup" + "hr" + "html" + "i" + "iframe" + "img" + "input" + "ins" + "kbd" + "label" + "legend" + "li" + "link" + "main" + "map" + "mark" + "math" + "menu" + "menuitem" + "meta" + "meter" + "nav" + "noscript" + "object" + "ol" + "optgroup" + "option" + "output" + "p" + "param" + "picture" + "pre" + "progress" + "q" + "rb" + "rp" + "rt" + "rtc" + "ruby" + "s" + "samp" + "script" + "section" + "select" + "slot" + "small" + "source" + "span" + "strong" + "style" + "sub" + "summary" + "sup" + "svg" + "table" + "tbody" + "td" + "template" + "textarea" + "tfoot" + "th" + "thead" + "time" + "title" + "tr" + "track" + "u" + "ul" + "var" + "video" + "wbr"]) + +(def basic-regex #"(?i)\s?|(]*>|]*>|]+>)+") +(def tags-regex + (->> html-tags + (map #(str "<" % "\\b[^>]*>")) + (string/join "|"))) +(def full-regex (new js/RegExp tags-regex "i")) + +(defn is-html? + [text] + (or (re-find basic-regex text) + (re-find full-regex text))) diff --git a/src/utils/security/security_html_test.cljs b/src/utils/security/security_html_test.cljs new file mode 100644 index 00000000000..4f7fdd7565b --- /dev/null +++ b/src/utils/security/security_html_test.cljs @@ -0,0 +1,28 @@ +(ns utils.security.security-html-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [utils.security.security-html :as s])) + +(deftest with-doctype-test + (is (s/is-html? "")) + (is (s/is-html? "\n\n"))) + +(deftest body-html-tags-test + (testing "detect HTML if it has , or " + (is (s/is-html? "")) + (is (s/is-html? "")) + (is (s/is-html? "")) + (is (s/is-html? "")) + (is (s/is-html? "")) + (is (s/is-html? "")))) + +(deftest html-standard-tags-test + (testing "detect HTML if it contains any of the standard HTML tags" + (is (s/is-html? "

foo

")) + (is (s/is-html? "foo")))) + +(deftest not-matching-xml-test + (is (not (s/is-html? "foo"))) + (is (not (s/is-html? "rocks"))) + (is (not (s/is-html? "not"))) + (is (not (s/is-html? "not")))) diff --git a/src/utils/security/security_test.cljs b/src/utils/security/security_test.cljs new file mode 100644 index 00000000000..5d3d9dedf97 --- /dev/null +++ b/src/utils/security/security_test.cljs @@ -0,0 +1,55 @@ +(ns utils.security.security-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [native-module.core :as native-module] + [utils.security.core :as security])) + +(def rtlo-link "‮http://google.com") +(def rtlo-link-text "blah blah ‮ some other blah blah http://google.com blah bash") + +(deftest safe-link-test-happy-path-test + (testing "an http link" + (is (security/safe-link? "http://test.com"))) + (testing "an https link" + (is (security/safe-link? "https://test.com"))) + (testing "a link without a a protocol" + (is (security/safe-link? "test.com")))) + +(deftest safe-link-test-exceptions-test + (testing "a javascript link" + (is (not (security/safe-link? "javascript://anything")))) + (testing "a javascript link mixed cases" + (is (not (security/safe-link? "JaVasCrIpt://anything")))) + (testing "a javascript link upper cases" + (is (not (security/safe-link? "JAVASCRIPT://anything")))) + (testing "an url-encoded javascript link" + (is (not (security/safe-link? "javascript:/%2F%250dalert(document.domain)")))) + (testing "rtlo links" + (is (not (security/safe-link? rtlo-link))))) + +(deftest safe-link-text-test-exceptions-test + (testing "rtlo links" + (is (not (security/safe-link-text? rtlo-link-text))))) + +(deftest mask-data-test + (testing "returns an instance of MaskedData" + (is (instance? security/MaskedData (security/mask-data "test")))) + (testing "hides the original value" + (is (= "******" (str (security/mask-data "test"))))) + (testing "succeeds the equality check between same MaskedData instances" + (is (= (security/mask-data "value") (security/mask-data "value")))) + (testing "fails the equality check between different MaskedData instances" + (is (not (= (security/mask-data "value-A") (security/mask-data "value-B"))))) + (testing "fails the equality check with non-MaskedData instances" + (is (not (= (security/mask-data "value") "value")))) + (testing "counts the masked data correctly" + (is (= (count "test") (count (security/mask-data "test"))))) + (testing "unmasks the data correctly" + (is (= "test" (-> "test" security/mask-data security/safe-unmask-data))))) + +(deftest hash-masked-password-test + (testing "returns an instance of MaskedData with the hashed content" + (is (= (-> "test" native-module/sha3 security/mask-data) + (-> "test" security/mask-data security/hash-masked-password)))) + (testing "throws a schema exception if the argument is not an instance of MaskedData" + (is (thrown? js/Error (security/hash-masked-password "test"))))) diff --git a/src/utils/signatures.cljs b/src/utils/signatures.cljs new file mode 100644 index 00000000000..6c5aec2736e --- /dev/null +++ b/src/utils/signatures.cljs @@ -0,0 +1,38 @@ +(ns utils.signatures + (:require [clojure.string :as string] + [native-module.core :as native-module] + [utils.hex :as hex])) + +(defn signature->rsv + [hex-signature] + (let [signature (hex/normalize-hex hex-signature)] + {:r (subs signature 0 64) + :s (subs signature 64 128) + :v (subs signature 128 130)})) + +(defn rsv->signature + [rsv] + (->> rsv + vals + (string/join "") + hex/prefix-hex)) + +(defn adjust-v-value + [rsv] + (let [v-int (js/parseInt (:v rsv) 16)] + (if (or (= v-int 0) (= v-int 1)) + (assoc rsv + :v + (-> (+ v-int 27) + native-module/number-to-hex)) + rsv))) + +(defn adjust-legacy-ecdsa-signature + "Due to legacy reasons, ensures the `v` value in an Ethereum personal_sign + signature should be set to 27 or 28, which comes from legacy ECDSA signature + recovery mechanisms." + [signature] + (-> signature + signature->rsv + adjust-v-value + rsv->signature)) diff --git a/src/utils/string.cljs b/src/utils/string.cljs new file mode 100644 index 00000000000..1ac964bcb24 --- /dev/null +++ b/src/utils/string.cljs @@ -0,0 +1,100 @@ +(ns utils.string + (:require + [clojure.string :as string] + [utils.transforms :as transforms])) + +(defn has-lower-case? + [s] + (some? (re-find #"[a-z]" s))) + +(defn has-upper-case? + [s] + (some? (re-find #"[A-Z]" s))) + +(defn has-numbers? + [s] + (some? (re-find #"\d" s))) + +(defn has-symbols? + [s] + (some? (re-find #"[^a-zA-Z0-9\s]" s))) + +(defn at-least-n-chars? + [s n] + (>= (count s) n)) + +(defn at-most-n-chars? + [s n] + (<= (count s) n)) + +(defn safe-trim + [s] + (when (string? s) + (string/trim s))) + +(defn safe-replace + [s m r] + (when (string? s) + (string/replace s m r))) + +(defn get-initials + "Returns `n` number of initial letters from `s`, all uppercased." + [s n] + (let [words (-> s str string/trim (string/split #"\s+"))] + (->> words + (take n) + (map (comp string/upper-case str first)) + string/join))) + +(def emoji-data (transforms/js->clj (js/require "../resources/data/emojis/en.json"))) +(def emoji-unicode-values (map :unicode emoji-data)) + +(defn contains-emoji? + [s] + (some (fn [emoji] + (string/includes? s emoji)) + emoji-unicode-values)) + +(defn contains-special-character? + [s] + (re-find #"[^a-zA-Z0-9\s]" s)) + +(defn remove-trailing-slash + "Given a URL, checks if it has a trailing slash and removes it. + Returns the URL as-is if there is no trailing slash." + [url] + (if (and (string? url) (string/ends-with? url "/")) + (subs url 0 (dec (count url))) + url)) + +(defn remove-http-prefix + "Given a URL, removes the 'http://' or 'https://' prefix if present. + Returns the URL without the prefix." + [url] + (when (string? url) + (string/replace url #"^https?://" ""))) + +(defn valid-amount-for-token-decimals? + [token-decimals amount-text] + (let [regex-pattern (str "^\\d*\\.?\\d{0," token-decimals "}$") + regex (re-pattern regex-pattern)] + (boolean (re-matches regex amount-text)))) + +(defn- truncate-str-memo + "Given string and max threshold, trims the string to threshold length with `...` + appended to end or in the middle if length of the string exceeds max threshold, + returns the same string if threshold is not exceeded" + [s threshold & [middle?]] + (if (and s (< threshold (count s))) + (if middle? + (let [str-len (count s) + max-len (- threshold 3) + start-len (Math/ceil (/ max-len 2)) + end-len (Math/floor (/ max-len 2)) + start (subs s 0 start-len) + end (subs s (- str-len end-len) str-len)] + (str start "..." end)) + (str (subs s 0 (- threshold 3)) "...")) + s)) + +(def truncate (memoize truncate-str-memo)) diff --git a/src/utils/string_test.cljs b/src/utils/string_test.cljs new file mode 100644 index 00000000000..f806f2a81b5 --- /dev/null +++ b/src/utils/string_test.cljs @@ -0,0 +1,52 @@ +(ns utils.string-test + (:require + [cljs.test :refer [are deftest is]] + utils.string)) + +(deftest get-initials-test + (are [expected input amount-initials] + (= expected (utils.string/get-initials input amount-initials)) + "" nil 0 + "" nil 1 + "" "" 0 + "" "ab" 0 + "" "" 1 + "A" "ab" 1 + "A" " ab " 1 + "A" "a b" 1 + "A" "ab" 2 + "AB" "a b" 2 + "ABC" "a b c d" 3 + "ABC" " a b c d" 3)) + +(deftest remove-trailing-slash-test + (are [expected input] + (= expected (utils.string/remove-trailing-slash input)) + "http://example.com" "http://example.com/" + "http://example.com" "http://example.com" + "http://example.com/path" "http://example.com/path/" + "http://example.com/path" "http://example.com/path" + "" "" + nil nil)) + +(deftest remove-http-prefix-test + (are [expected input] + (= expected (utils.string/remove-http-prefix input)) + "example.com" "http://example.com" + "example.com" "https://example.com" + "example.com" "example.com" + "example.com/path" "http://example.com/path" + "example.com/path" "https://example.com/path" + "example.com/path" "example.com/path" + "" "" + nil nil)) + +(deftest truncate-str-test + ; threshold is less then string length + (is (= (utils.string/truncate "Long string" 7) "Long...")) + ; string length (truncate middle) + (is (= (utils.string/truncate "Long string" 7 true) "Lo...ng")) ; threshold is less then + + ; threshold is the same as string length + (is (= (utils.string/truncate "Long string" 11) "Long string")) + (is (= (utils.string/truncate "Long string" 20) "Long string"))) diff --git a/src/utils/transforms.cljs b/src/utils/transforms.cljs new file mode 100644 index 00000000000..6bd82eecc2e --- /dev/null +++ b/src/utils/transforms.cljs @@ -0,0 +1,82 @@ +(ns utils.transforms + (:refer-clojure :exclude [js->clj]) + (:require + [camel-snake-kebab.core :as csk] + [camel-snake-kebab.extras :as cske] + [cljs-bean.core :as clj-bean] + [oops.core :as oops])) + +(defn js->clj [data] (cljs.core/js->clj data :keywordize-keys true)) + +(defn clj->pretty-json + [data spaces] + (.stringify js/JSON (clj-bean/->js data) nil spaces)) + +(defn clj->json [data] (clj->pretty-json data 0)) + +(defn <-js-map + "Shallowly transforms JS Object keys/values with `key-fn`/`val-fn`. + + Returns m if `m` is not an instance of `js/Object`. + + Implementation taken from `js->clj`, but with the ability to customize how + keys and/or values are transformed in one loop. + + This function is useful when you don't want to recursively apply the same + transformation to keys/values. For example, many maps in the app-db are + indexed by ID, like `community.members`. If we convert the entire community + with (js->clj m :keywordize-keys true), then IDs will be converted to + keywords, but we want them as strings. Instead of transforming to keywords and + then transforming back to strings, it's better to not transform them at all. + " + ([^js m] + (<-js-map m nil)) + ([^js m {:keys [key-fn val-fn]}] + (assert (or (nil? m) (identical? (type m) js/Object) (map? m))) + (if (identical? (type m) js/Object) + (persistent! + (reduce (fn [r k] + (let [v (oops/oget+ m k) + new-key (if key-fn (key-fn k v) k) + new-val (if val-fn (val-fn k v) v)] + (assoc! r new-key new-val))) + (transient {}) + (js-keys m))) + m))) + +(defn js-stringify + [js-object spaces] + (.stringify js/JSON js-object nil spaces)) + +(defn js-parse + [data] + (.parse js/JSON data)) + +(def ->kebab-case-keyword (memoize csk/->kebab-case-keyword)) +(def ->PascalCaseKeyword (memoize csk/->PascalCaseKeyword)) +(def ->camelCaseString (memoize csk/->camelCaseString)) + +(defn json->clj + ([json] + (json->clj json nil)) + ([json {:keys [keywords-to-kebab-case?]}] + (when-not (= json "undefined") + (try + (let [parsed (js->clj (.parse js/JSON json))] + (if keywords-to-kebab-case? + (cske/transform-keys ->kebab-case-keyword parsed) + parsed)) + (catch js/Error _ (when (string? json) json)))))) + +(defn json->js + [json] + (when-not (= json "undefined") + (try (.parse js/JSON json) (catch js/Error _ (when (string? json) json))))) + +(defn map-array + "Performs an efficient `map` operation on `coll` but returns a JS array" + [f coll] + (let [js-array ^js (array)] + (doseq [e coll] + (.push js-array (f e))) + js-array)) diff --git a/src/utils/transforms_test.cljs b/src/utils/transforms_test.cljs new file mode 100644 index 00000000000..f74441b5f3c --- /dev/null +++ b/src/utils/transforms_test.cljs @@ -0,0 +1,37 @@ +(ns utils.transforms-test + (:require + [cljs.test :refer [are deftest is testing]] + [utils.transforms :as sut])) + +(defn equals-as-json + [m1 m2] + (= (js/JSON.stringify (clj->js m1)) + (js/JSON.stringify (clj->js m2)))) + +(deftest <-js-map-test + (testing "without transforming keys/values" + (are [expected m] + (is (equals-as-json expected (sut/<-js-map m))) + nil nil + {:a 1} {:a 1} + #js {} #js {} + #js {"a" 1 "b" 2} #js {"a" 1 "b" 2})) + + (testing "with key/value transformation" + (is (equals-as-json {"aa" [1] "bb" [2]} + (sut/<-js-map #js {"a" 1 "b" 2} + {:key-fn (fn [k] (str k k)) + :val-fn (fn [_ v] (vector v))})))) + + (testing "it is non-recursive" + (is (equals-as-json {:a 1 :b #js {"c" 3}} + (sut/<-js-map #js {"a" 1 "b" #js {"c" 3}} + {:key-fn (fn [k] (keyword k))})))) + + (testing "value transformation based on the key" + (is (equals-as-json {"a" 1 "b" "banana"} + (sut/<-js-map #js {"a" 1 "b" 2} + {:val-fn (fn [k v] + (if (= "b" k) + "banana" + v))}))))) diff --git a/src/utils/url.cljs b/src/utils/url.cljs new file mode 100644 index 00000000000..99283a592e9 --- /dev/null +++ b/src/utils/url.cljs @@ -0,0 +1,76 @@ +(ns utils.url + (:require + [clojure.string :as string])) + +(defn normalize-url + [url] + (str (when (and (string? url) + (not (re-find #"^[a-zA-Z-_]+:/" url))) + "https://") + ((fnil string/trim "") url))) + +(def normalize-and-decode-url (comp js/decodeURI normalize-url)) + +(defn url-host + [url] + (try + (when-let [host (.getDomain ^js (goog.Uri. url))] + (when-not (string/blank? host) + (string/replace host #"www." ""))) + (catch :default _ nil))) + +(defn url? + [s] + (try + (when-let [host (.getDomain ^js (goog.Uri. s))] + (not (string/blank? host))) + (catch :default _ nil))) + +(defn url-sanitized? + [uri] + (not (nil? (re-find #"^(https:)([/|.|\w|\s|-])*\.(?:jpg|svg|png)$" uri)))) + +(defn- split-param + [param] + (-> + (string/split param #"=") + (concat (repeat "")) + (->> + (take 2)))) + +(defn- url-decode + [string] + (some-> string + str + (string/replace #"\+" "%20") + (js/decodeURIComponent))) + +(defn query->map + [qstr] + (when-not (string/blank? qstr) + (some->> (string/split qstr #"&") + seq + (mapcat split-param) + (map url-decode) + (apply hash-map)))) + +(defn replace-port + [url new-port] + (when url + (string/replace url #"(:\d+)" (str ":" new-port)))) + +(defn add-slash-separator + [url] + (if (not= (last url) "/") + (str url "/") + url)) + +(defn add-path + ([url path value] + (-> url + (add-path path) + (add-path value))) + ([url path] + (-> url + add-slash-separator + (str (name path))))) diff --git a/src/utils/url_test.cljs b/src/utils/url_test.cljs new file mode 100644 index 00000000000..275f3fa1473 --- /dev/null +++ b/src/utils/url_test.cljs @@ -0,0 +1,74 @@ +(ns utils.url-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [utils.url :as url])) + +(deftest url-sanitize-check-test + (testing + "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/818934.svg" + (testing "it returns true" + (is + (url/url-sanitized? + "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/818934.svg")))) + + (testing "https://www.cryptostrikers.com/assets/images/cards/017.svg" + (testing "it returns true" + (is (url/url-sanitized? "https://www.cryptostrikers.com/assets/images/cards/017.svg")))) + + (testing "https://www.etheremon.com/assets/images/mons_origin/025.png" + (testing "it returns true" + (is (url/url-sanitized? "https://www.etheremon.com/assets/images/mons_origin/025.png")))) + + (testing "http://www.etheremon.com/assets/images/mons_origin/025.png" + (testing "it returns false" + (is (not (url/url-sanitized? "http://www.etheremon.com/assets/images/mons_origin/025.png"))))) + + (testing "xxx:x \\\\x0Aonerror=javascript:alert(1)" + (testing "it returns false" + (is (not (url/url-sanitized? "xxx:x \\\\x0Aonerror=javascript:alert(1)"))))) + + (testing + "https://www.etheremon.com/assets/images/mons_origin/025.png'<script>alert('123');</script>" + (testing "it returns false" + (is + (not + (url/url-sanitized? + "https://www.etheremon.com/assets/images/mons_origin/025.png'<script>alert('123');</script>"))))) + + (testing + "https://www.etheremon.com/assets/images/mons'<script>alert('123');</script>origin/025.png" + (testing "it returns false" + (is + (not + (url/url-sanitized? + "https://www.etheremon.com/assets/images/mons'<script>alert('123');</script>origin/025.png"))))) + + (testing + "https://www.etheremon.com/assets/images/mons_origin/025.png'>" + (testing "it returns false" + (is + (not + (url/url-sanitized? + "https://www.etheremon.com/assets/images/mons_origin/025.png'>"))))) + + (testing + "https://www.etheremon.com/assets/images/mons'>origin/025.png" + (testing "it returns false" + (is + (not + (url/url-sanitized? + "https://www.etheremon.com/assets/images/mons'>origin/025.png")))))) + +(deftest url-host-check-test + (testing "Extract host/domain from URL" + (testing "Valid URL with endpoint" + (is (= "status.im" (url/url-host "https://status.im/testing")))) + (testing "Valid URL" + (is (= "status.im" (url/url-host "http://status.im")))) + (testing "Blank domainlocalhost" + (is (nil? (url/url-host "localhost:3000/testing"))))) + (testing "Return nil for Invalid URL" + (testing "Bad scheme" + (is (nil? (url/url-host "invalid//status.im/testing")))) + (testing "No scheme" + (is (nil? (url/url-host "status.im/testing")))))) diff --git a/src/utils/vector.cljs b/src/utils/vector.cljs new file mode 100644 index 00000000000..cedbb977e9e --- /dev/null +++ b/src/utils/vector.cljs @@ -0,0 +1,7 @@ +(ns utils.vector) + +(defn insert-element-at + [data element index] + (let [before (take index data) + after (drop index data)] + (vec (concat before [element] after)))) diff --git a/src/utils/vector_test.cljs b/src/utils/vector_test.cljs new file mode 100644 index 00000000000..f9613d64336 --- /dev/null +++ b/src/utils/vector_test.cljs @@ -0,0 +1,17 @@ +(ns utils.vector-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [utils.vector :as vector])) + +(deftest insert-element-at-test + (testing "Inserting into an empty vector" + (is (= [42] (vector/insert-element-at [] 42 0)))) + + (testing "Inserting at the beginning of a vector" + (is (= [42 1 2 3] (vector/insert-element-at [1 2 3] 42 0)))) + + (testing "Inserting in the middle of a vector" + (is (= [1 42 2 3] (vector/insert-element-at [1 2 3] 42 1)))) + + (testing "Inserting at the end of a vector" + (is (= [1 2 3 42] (vector/insert-element-at [1 2 3] 42 3))))) diff --git a/src/utils/worklets/chat/messenger/composer.cljs b/src/utils/worklets/chat/messenger/composer.cljs new file mode 100644 index 00000000000..865bbcd2a0a --- /dev/null +++ b/src/utils/worklets/chat/messenger/composer.cljs @@ -0,0 +1,7 @@ +(ns utils.worklets.chat.messenger.composer) + +(def ^:private worklets (js/require "../src/js/worklets/chat/messenger/composer.js")) + +(defn scroll-down-button-opacity + [chat-list-scroll-y composer-focused? window-height] + (.scrollDownButtonOpacity ^js worklets chat-list-scroll-y composer-focused? window-height)) diff --git a/src/utils/worklets/chat/messenger/lightbox.cljs b/src/utils/worklets/chat/messenger/lightbox.cljs new file mode 100644 index 00000000000..b3c4a1f7557 --- /dev/null +++ b/src/utils/worklets/chat/messenger/lightbox.cljs @@ -0,0 +1,7 @@ +(ns utils.worklets.chat.messenger.lightbox) + +(def ^:private layout-worklets (js/require "../src/js/worklets/chat/messenger/lightbox.js")) + +(defn info-layout + [input top?] + (.infoLayout ^js layout-worklets input top?)) diff --git a/src/utils/worklets/chat/messenger/messages.cljs b/src/utils/worklets/chat/messenger/messages.cljs new file mode 100644 index 00000000000..adfaf17a8e5 --- /dev/null +++ b/src/utils/worklets/chat/messenger/messages.cljs @@ -0,0 +1,8 @@ +(ns utils.worklets.chat.messenger.messages) + +(def ^:private worklets (js/require "../src/js/worklets/chat/messenger/messages.js")) + +(defn messages-list-on-scroll + [distance-from-list-top chat-list-scroll-y callback] + (.messagesListOnScroll ^js worklets distance-from-list-top chat-list-scroll-y callback)) + diff --git a/src/utils/worklets/communities.cljs b/src/utils/worklets/communities.cljs new file mode 100644 index 00000000000..5476f8c3269 --- /dev/null +++ b/src/utils/worklets/communities.cljs @@ -0,0 +1,33 @@ +(ns utils.worklets.communities + (:require [goog.object :as gobj] + [react-native.utils :as utils])) + +(def ^:private worklets (js/require "../src/js/worklets/communities.js")) + +(defn- transform-args + [f] + (fn [& args] + (apply f (map utils/kebab-case-map->camelCase-obj args)))) + +(defn- worklet-wrapper + [worklet-name] + (if-let [worklet-fn (gobj/get worklets worklet-name)] + (transform-args worklet-fn) + (throw (js/Error. + (ex-info "Non-existing worklet!" + {:name worklet-name + :file "../src/js/worklets/communities.js"}))))) + +(def use-logo-styles (worklet-wrapper "useLogoStyles")) +(def use-sheet-styles (worklet-wrapper "useSheetStyles")) +(def use-name-styles (worklet-wrapper "useNameStyles")) +(def use-info-styles (worklet-wrapper "useInfoStyles")) +(def use-channels-styles (worklet-wrapper "useChannelsStyles")) +(def use-scroll-to (worklet-wrapper "useScrollTo")) +(def use-header-opacity (worklet-wrapper "useHeaderOpacity")) +(def use-opposite-header-opacity (worklet-wrapper "useOppositeHeaderOpacity")) +(def use-nav-content-opacity (worklet-wrapper "useNavContentOpacity")) + +(def on-pan-start (worklet-wrapper "onPanStart")) +(def on-pan-update (worklet-wrapper "onPanUpdate")) +(def on-pan-end (worklet-wrapper "onPanEnd")) diff --git a/src/utils/worklets/core.cljs b/src/utils/worklets/core.cljs new file mode 100644 index 00000000000..dcb75edd35c --- /dev/null +++ b/src/utils/worklets/core.cljs @@ -0,0 +1,18 @@ +(ns utils.worklets.core) + +(def core-js (js/require "../src/js/worklets/core.js")) + +(defn interpolate-value + [shared-value + input-range + output-range + extrapolation] + (.interpolateValue ^js core-js + shared-value + (clj->js input-range) + (clj->js output-range) + (clj->js extrapolation))) + +(defn apply-animations-to-style + [animations style] + (.applyAnimationsToStyle ^js core-js (clj->js animations) (clj->js style))) diff --git a/src/utils/worklets/header_animations.cljs b/src/utils/worklets/header_animations.cljs new file mode 100644 index 00000000000..d0dd15f7f04 --- /dev/null +++ b/src/utils/worklets/header_animations.cljs @@ -0,0 +1,7 @@ +(ns utils.worklets.header-animations) + +(def worklets (js/require "../src/js/worklets/header_animations.js")) + +(def use-blur-amount (.-useBlurAmount worklets)) + +(def use-layer-opacity (.-useLayerOpacity worklets)) diff --git a/src/utils/worklets/onboarding_carousel.cljs b/src/utils/worklets/onboarding_carousel.cljs new file mode 100644 index 00000000000..dea06f83450 --- /dev/null +++ b/src/utils/worklets/onboarding_carousel.cljs @@ -0,0 +1,11 @@ +(ns utils.worklets.onboarding-carousel) + +(def worklets (js/require "../src/js/worklets/onboarding_carousel.js")) + +(defn dynamic-progress-bar-width + [static-progress-bar-width progress] + (.dynamicProgressBarWidth ^js worklets static-progress-bar-width progress)) + +(defn carousel-left-position + [window-width progress is-dragging? drag-amount] + (.carouselLeftPosition ^js worklets window-width progress is-dragging? drag-amount)) diff --git a/src/utils/worklets/profile_header.cljs b/src/utils/worklets/profile_header.cljs new file mode 100644 index 00000000000..a5d1fc0a45b --- /dev/null +++ b/src/utils/worklets/profile_header.cljs @@ -0,0 +1,10 @@ +(ns utils.worklets.profile-header) + +(def ^:private worklets (js/require "../src/js/worklets/profile_header.js")) + +(defn profile-header-animation + [scroll-y threshold top-bar-height] + (.profileHeaderAnimation ^js worklets + scroll-y + threshold + top-bar-height)) diff --git a/src/utils/worklets/record_audio.cljs b/src/utils/worklets/record_audio.cljs new file mode 100644 index 00000000000..f83ae8e1f62 --- /dev/null +++ b/src/utils/worklets/record_audio.cljs @@ -0,0 +1,7 @@ +(ns utils.worklets.record-audio) + +(def ^:private record-audio-worklets (js/require "../src/js/worklets/record_audio.js")) + +(defn ring-scale + [scale substract] + (.ringScale ^js record-audio-worklets scale substract)) diff --git a/src/utils/worklets/shell.cljs b/src/utils/worklets/shell.cljs new file mode 100644 index 00000000000..747b64832b4 --- /dev/null +++ b/src/utils/worklets/shell.cljs @@ -0,0 +1,18 @@ +(ns utils.worklets.shell) + +(def bottom-tabs-worklets (js/require "../src/js/worklets/shell/bottom_tabs.js")) +(def home-stack-worklets (js/require "../src/js/worklets/shell/home_stack.js")) + +;; Derived values for Bottom tabs +(defn bottom-tab-icon-color + [id selected-stack-id selected-tab-color default-color] + (.bottomTabIconColor ^js bottom-tabs-worklets id selected-stack-id selected-tab-color default-color)) + +;; Derived values for stacks (communities, chat, wallet, browser) +(defn stack-opacity + [id selected-stack-id] + (.stackOpacity ^js home-stack-worklets id selected-stack-id)) + +(defn stack-z-index + [id selected-stack-id] + (.stackZIndex ^js home-stack-worklets id selected-stack-id)) diff --git a/status-go-version.json b/status-go-version.json new file mode 100644 index 00000000000..7763ddf481b --- /dev/null +++ b/status-go-version.json @@ -0,0 +1,9 @@ +{ + "_comment": "THIS SHOULD NOT BE EDITED BY HAND.", + "_comment": "Instead use: scripts/update-status-go.sh ", + "owner": "status-im", + "repo": "status-go", + "version": "v10.29.4", + "commit-sha1": "658d142d1664c87b9610e7ee8babf79827bc3a20", + "src-sha256": "1zcgrcfmy9qr5k558r66lh8wg4ma36zcgjgcmf1vzz7wablk9k2d" +} diff --git a/test-resources/override.js b/test-resources/override.js new file mode 100644 index 00000000000..224f9d7d973 --- /dev/null +++ b/test-resources/override.js @@ -0,0 +1,28 @@ +var Mocks = require('../target/mocks/mocks.js'); +var Module = require('module'); +process.env.TZ = 'Etc/UTC'; + +const originalLoader = Module._load; + +/* + Hook `require` so that RN abuse of require does not break when running tests in nodejs. +*/ + +Module._load = function hookedLoader(request, parent, isMain) { + if (request.match(/.jpeg|.jpg|.png|.mp4$/)) { + return { uri: request }; + } + + return originalLoader(request, parent, isMain); +}; + +var originalRequire = Module.prototype.require; + +Module.prototype.require = function (req) { + module = Mocks.mocks(req); + if (module == null) { + return originalRequire.apply(this, arguments); + } else { + return module; + } +}; diff --git a/test/appium/Jenkinsfile b/test/appium/Jenkinsfile deleted file mode 100644 index 185cd6c7abb..00000000000 --- a/test/appium/Jenkinsfile +++ /dev/null @@ -1,12 +0,0 @@ -node ('linux1'){sauce('12e007ad-48cf-4c20-92f3-b923bb5641bd'){ - checkout([$class: 'GitSCM', branches: [[name: 'tests/align-with-start-chat-redesign-#3195']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], submoduleCfg: [], userRemoteConfigs: [[url: 'https://github.com/status-im/status-react.git']]]) - currentBuild.displayName = "PR-$pr_id" - try {withCredentials([string(credentialsId: 'GIT_HUB_TOKEN', variable: 'GIT_HUB_TOKEN'), string(credentialsId: 'SAUCE_ACCESS_KEY', variable: 'SAUCE_ACCESS_KEY'), string(credentialsId: 'SAUCE_USERNAME', variable: 'SAUCE_USERNAME')]){ - sh 'cd test/appium/tests && python3 -m pytest -m all --pr_number=$pr_id --build=PR-$pr_id -n12 $apk' - } - } - finally { - saucePublisher() - junit testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], testResults: 'test/appium/tests/*.xml' } - } -} diff --git a/test/appium/Jenkinsfile.message_reliability b/test/appium/Jenkinsfile.message_reliability deleted file mode 100644 index 78bb50f0ca2..00000000000 --- a/test/appium/Jenkinsfile.message_reliability +++ /dev/null @@ -1,42 +0,0 @@ -import net.sf.json.JSONArray; -import net.sf.json.JSONObject; -node ('linux1'){sauce('1be1b688-e0e7-4314-92a0-db11f52d3c00'){ - def testPassed = true - try { - - stage('Tests & Report'){ - try { - withCredentials([string(credentialsId: 'GIT_HUB_TOKEN', variable: 'GIT_HUB_TOKEN'), - string(credentialsId: 'SOB_SAUCE_ACCESS_KEY', variable: 'SOB_SAUCE_ACCESS_KEY'), - string(credentialsId: 'SOB_SAUCE_USERNAME', variable: 'SOB_SAUCE_USERNAME')]){ - sh 'cd test/appium/tests && python3 -m pytest test_message_reliability.py::TestMessageReliability::$test_to_run --messages_number=$messages_number --message_wait_time=$message_wait_time $apk' } - } - finally { - saucePublisher() - junit testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], - testResults: 'test/appium/tests/*.xml' - archiveArtifacts 'test/appium/tests/*.png' - } - } - } catch(e) { - testPassed = false - } - - stage('Slack notification'){ - def c = (testPassed ? 'good' : 'warning') - def resultText = (testPassed ? '' : 'FAILED') - def fileData = readFile('test/appium/tests/messages.txt') - def map = evaluate(fileData) - def text = 'Sent messages: ' + map.sent + '\nReceived messages: ' + map.received + '\n<' + env.BUILD_URL + '/artifact/test/appium/tests/chart.png' + '|Test run chart>' - JSONArray attachments = new JSONArray(); - JSONObject attachment = new JSONObject(); - - attachment.put('title','Message reliability job run, ' + test_to_run + ' ' + resultText); - attachment.put('title_link', env.BUILD_URL); - attachment.put('text', text); - - attachments.add(attachment); - slackSend(color: c, channel: '#test-notifications', attachments: attachments.toString()) - } -} -} \ No newline at end of file diff --git a/test/appium/README.md b/test/appium/README.md new file mode 100644 index 00000000000..d8623705845 --- /dev/null +++ b/test/appium/README.md @@ -0,0 +1,23 @@ +# E2E tests requirements + +### Environment variables +- WEB3_INFURA_PROJECT_ID +- ETHERSCAN_API_KEY + +1) If running on LambdaTest emulators: +- LAMBDA_TEST_USERNAME +- LAMBDA_TEST_ACCESS_KEY +2) If running with Testrail (`--testrail_report=True` param): +- TESTRAIL_PASS +- TESTRAIL_USER + +## Other +Userdata is stored in `tests.users` module (`test/appium/tests/users.py`, which is not committed to repo). + +So to launch e2e you need to copy `users.py` to `test/appium/tests/` to prevent `ImportError` + +If you are external contributor, you can contact us at [error-reports@status.im](mailto:error-reports@status.im) to get `users.py` + +More info on local setup for e2e can be found [here](https://notes.status.im/setup-e2e) + + diff --git a/test/appium/docker/Dockerfile b/test/appium/docker/Dockerfile new file mode 100644 index 00000000000..fdb80592666 --- /dev/null +++ b/test/appium/docker/Dockerfile @@ -0,0 +1,110 @@ +FROM ubuntu:16.04 + +LABEL maintainer "Lukasz Fryc " + +WORKDIR /root + +#================== +# General Packages +#------------------ +# openjdk-8-jdk +# Java +# ca-certificates +# SSL client +# tzdata +# Timezone +# zip +# Make a zip file +# unzip +# Unzip zip file +# curl +# Transfer data from or to a server +# wget +# Network downloader +# libqt5webkit5 +# Web content engine (Fix issue in Android) +# libgconf-2-4 +# Required package for chrome and chromedriver to run on Linux +# xvfb +# X virtual framebuffer +#================== +RUN apt-get -qqy update && \ + apt-get -qqy --no-install-recommends install \ + openjdk-8-jdk \ + ca-certificates \ + tzdata \ + zip \ + unzip \ + curl \ + wget \ + libqt5webkit5 \ + libgconf-2-4 \ + xvfb \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +#=============== +# Set JAVA_HOME +#=============== +ENV JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64/jre" \ + PATH=$PATH:$JAVA_HOME/bin + +#===================== +# Install Android SDK +#===================== +ARG SDK_VERSION=sdk-tools-linux-4333796 +ARG ANDROID_BUILD_TOOLS_VERSION=28.0.1 +ARG ANDROID_PLATFORM_VERSION="android-27" + +ENV SDK_VERSION=$SDK_VERSION \ + ANDROID_BUILD_TOOLS_VERSION=$ANDROID_BUILD_TOOLS_VERSION \ + ANDROID_HOME=/root + +RUN wget -O tools.zip https://dl.google.com/android/repository/${SDK_VERSION}.zip && \ + unzip tools.zip && rm tools.zip && \ + chmod a+x -R $ANDROID_HOME && \ + chown -R root:root $ANDROID_HOME + +ENV PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin + +# https://askubuntu.com/questions/885658/android-sdk-repositories-cfg-could-not-be-loaded +RUN mkdir -p ~/.android && \ + touch ~/.android/repositories.cfg && \ + echo y | sdkmanager "platform-tools" && \ + echo y | sdkmanager "build-tools;$ANDROID_BUILD_TOOLS_VERSION" && \ + echo y | sdkmanager "platforms;$ANDROID_PLATFORM_VERSION" + +ENV PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools + +#==================================== +# Install latest nodejs, npm, appiuma +#==================================== +# ARG APPIUM_VERSION=1.7.2 +# ENV APPIUM_VERSION=$APPIUM_VERSION + +RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - +RUN apt-get -qqy install nodejs +RUN npm set maxsockets 3 && \ + npm install -g appium@1.7.2 --unsafe-perm=true --allow-root +RUN apt-get remove --purge -y npm && \ + apt-get autoremove --purge -y && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ + apt-get clean + +#================================== +# Fix Issue with timezone mismatch +#================================== +# ENV TZ="US/Pacific" +# RUN echo "${TZ}" > /etc/timezone + +#=============== +# Expose Ports +#--------------- +# 4723 +# Appium port +#=============== +EXPOSE 4723 + +COPY entry_point.sh /root/ +RUN chmod +x /root/entry_point.sh +ENTRYPOINT /root/entry_point.sh \ No newline at end of file diff --git a/test/appium/docker/HOWTO.md b/test/appium/docker/HOWTO.md new file mode 100644 index 00000000000..94d4e7425b4 --- /dev/null +++ b/test/appium/docker/HOWTO.md @@ -0,0 +1,66 @@ +## Instructions + +Use device with Android 7.1 or newer. + +1. Install needed tools + + - Install Docker https://docs.docker.com/install/ + - Install Python 3 + + Go to https://realpython.com/installing-python and follow the instructions. For macOS with Homebrew, you can run `brew install python3` to install it. + +2. Build Appium docker image + + - Make sure Docker app is running + - Build Appium image to have environment in which tests will be executed + + ``` + $ cd appium/docker + $ docker build -t "appium/appium:local" . + ``` + + Make sure all build steps were successful. You should see: + ``` + Successfully tagged appium/appium:local + ``` + +3. Find out IP address of the device + + On device, open wifi settings and click on the current network. Write down the IP address as it will be needed for running the battery test wirelessly. + +4. Run the battery test + + - Make sure the device is sufficiently charged + - Disconnect the device USB cable + - Create a directory that will be shared with the docker container. E.g. `mkdir /Users/lukas/Desktop/shared` + - Put .apk to test into the shared directory. E.g. `mv StatusIm-181109-092655-009d97-e2e.apk /Users/lukas/Desktop/shared` + - Run a test with using appium container + - `--device_ip` - IP address of the device (see 3.) + - `--apk=StatusIm-181109-092655-009d97-e2e.apk` - name of the apk file + - `--docker=True` - run the tests using appium docker container + - `--docker_shared_volume=/Users/lukas/Desktop/shared` - path to the shared directory + - `--bugreport=True` - generate bugreport file + + ``` + $ cd status-mobile/test/appium + $ python3 -m pytest --apk=StatusIm-181109-092655-009d97-e2e.apk --device_ip=192.168.0.104 --bugreport=True --docker=true --docker_shared_volume=/Users/lukas/Desktop/shared tests/atomic/transactions/test_wallet.py::TestTransactionWalletSingleDevice::test_send_eth_from_wallet_to_contact + ``` + - Follow the instructions shown in the command line + + **In case of issues:** + + Find out the Appium container id: `docker ps` and get the latest logs: `docker logs ebdf1761f51f` where `ebdf1761f51f` is the container id. + +5. Analyse test results + + The test generates Android bugreport that is saved in shared directory. It contains data about battery, cpu, memory, network usage, and more. See [energy-efficient-bok](https://github.com/status-im/energy-efficient-bok/blob/master/QA_Android.md) or [Battery Historian](https://developer.android.com/studio/profile/battery-historian) to learn how to analyse it. + + +## Issues with adb connect + +To run the battery test without charging the device, adb needs to connect to it wirelessly. To avoid wifi connection issues, you can connect to the device via bluetooth. + +macOS instructions: +- On device, enable bluetooth on your device in Android settings -> connections -> bluetooth +- On device, go to mobile hotspots and tethering and enable bluetooth tethering +- On macOS, go to the bluetooth settings and connect with the device (click "Connect to Network") diff --git a/test/appium/docker/entry_point.sh b/test/appium/docker/entry_point.sh new file mode 100644 index 00000000000..f0907e95e01 --- /dev/null +++ b/test/appium/docker/entry_point.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +APPIUM_LOG="/var/log/appium.log" +CMD="appium --log $APPIUM_LOG" + +$CMD diff --git a/test/appium/pytest.ini b/test/appium/pytest.ini index d168b6a10e9..10fff662117 100644 --- a/test/appium/pytest.ini +++ b/test/appium/pytest.ini @@ -1,3 +1,10 @@ [pytest] norecursedirs = .git views -addopts = -s -v --junitxml=result.xml --tb=line +addopts = -s -v --tb=short --junitxml=result.xml --dist loadgroup -n 9 +junit_family=legacy +markers = + testrail_id: case ID in testrail + flaky: flaky tests for re-run if necessary + smoke: PR tests + nightly: nightly tests + secured: tests where SauceLabs session link is not posted in PR comments \ No newline at end of file diff --git a/test/appium/requirements.txt b/test/appium/requirements.txt index 7b57c2d4ca3..702b7673201 100644 --- a/test/appium/requirements.txt +++ b/test/appium/requirements.txt @@ -1,49 +1,20 @@ -aiohttp==2.2.3 -allpairspy==2.3.0 -apipkg==1.4 -Appium-Python-Client==0.26 -async-timeout==1.2.1 -asyncio==3.4.3 -certifi==2017.7.27.1 -chardet==3.0.4 -cycler==0.10.0 -cytoolz==0.9.0.1 -emoji==0.5.0 -eth-hash==0.1.4 -eth-keys==0.2.0b3 -eth-utils==1.0.3 -ethereum==2.3.0 -execnet==1.4.1 -future==0.16.0 -idna==2.5 -kiwisolver==1.0.1 -lxml==3.8.0 -matplotlib==2.2.2 -multidict==3.1.3 -namedlist==1.7 -numpy==1.14.4 -pbkdf2==1.3 -Pillow==5.1.0 -py==1.4.34 -py-ecc==1.4.2 -pycryptodome==3.6.1 -pyethash==0.1.27 -pyparsing==2.2.0 -pysha3==1.0.2 -pytest==3.2.1 -pytest-forked==0.2 -pytest-xdist==1.22.2 -python-dateutil==2.7.3 -pytz==2018.4 -PyYAML==3.12 -repoze.lru==0.7 -requests==2.18.3 -rlp==1.0.1 -sauceclient==1.0.0 -scrypt==0.8.6 -selenium==3.8.1 -six==1.10.0 -toolz==0.9.0 -urllib3==1.22 -yarl==0.12.0 -zbarlight==1.2 +pytest~=7.4.0 +Appium-Python-Client~=2.11.1 +selenium~=4.11.2 +emoji~=0.5.0 +urllib3~=1.26.12 +requests~=2.31.0 +filelock~=3.16.1 +python-dateutil~=2.8.1 +ImageHash~=4.3.1 +Pillow~=10.0.0 +pytest-xdist==3.2.0 +pytz~=2020.4 +PyGithub==1.55 +typing-extensions==4.12.2 +mnemonic==0.21 +bip-utils==2.9.3 +testrail==0.3.15 +webcolors==24.8.0 +CurrencyConverter==0.17.34 +bs4==0.0.2 \ No newline at end of file diff --git a/test/appium/support/api/lightweight_browser_api.py b/test/appium/support/api/lightweight_browser_api.py new file mode 100644 index 00000000000..83e99b710ab --- /dev/null +++ b/test/appium/support/api/lightweight_browser_api.py @@ -0,0 +1,54 @@ +import requests +from bs4 import BeautifulSoup +import re + + +class LightweightBrowserHandler: + + def __init__(self, base_url: str): + self.base_url = base_url + + + def load_tx_etherscan_page(self, tx_hash: str) -> BeautifulSoup: + headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "Accept-Language": "en-US,en;q=0.9", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "DNT": "1", + } + tx_url = '%s/tx/%s' % (self.base_url, tx_hash) + response = requests.get(tx_url, headers=headers, timeout=10) + response.raise_for_status() + return BeautifulSoup(response.text, "html.parser") + + def extract_tx_details(self, soup: BeautifulSoup) -> dict: + tx_fee_elem = soup.select_one("#txfeebutton") + tx_fee = tx_fee_elem.text.strip() if tx_fee_elem else "Not Found" + + confirmation_time_elem = soup.find(string=re.compile('Confirmed within')) + confirmation_time = ( + confirmation_time_elem.text.replace("Confirmed within", "").strip() + if confirmation_time_elem + else "Not Found" + ) + + return {'fee': tx_fee, 'confirmation_time': confirmation_time} + + def find_text(self, soup: BeautifulSoup, text: str) -> bool: + return soup.find(string=re.compile(re.escape(text))) is not None + + def find_success_badge(self, soup: BeautifulSoup): + for span in soup.find_all("span", class_=lambda x: x and "text-green-600" in x): + if "Success" in span.get_text(): + return span + return None + + def get_fee_and_confirmation_time(self, tx_url: str) -> dict: + try: + soup = self.load_tx_etherscan_page(tx_url) + return self.extract_tx_details(soup) + except requests.RequestException as e: + print(f"An error occurred while fetching transaction details: {e}") + return {'fee': None, 'confirmation_time': None} + \ No newline at end of file diff --git a/test/appium/support/api/network_api.py b/test/appium/support/api/network_api.py index ce66d916000..d3ea2cbbbbb 100644 --- a/test/appium/support/api/network_api.py +++ b/test/appium/support/api/network_api.py @@ -1,117 +1,103 @@ +import logging +import time +from datetime import datetime +from json import JSONDecodeError +from os import environ +from typing import List +import json + import pytest +import pytz import requests -import time -from tests import info +from selenium.common import TimeoutException +import tests -class NetworkApi: +class NetworkApi: def __init__(self): - self.network_url = 'http://api-%s.etherscan.io/api?' % pytest.config.getoption('network') - self.faucet_url = 'http://51.15.45.169:3001/donate' - self.chat_bot_url = 'http://offsite.chat:8099' - - def get_transactions(self, address: str) -> dict: - method = self.network_url + 'module=account&action=txlist&address=0x%s&sort=desc' % address - return requests.request('GET', url=method).json()['result'] - - def get_token_transaction(self, address: str) -> dict: - method = self.network_url + 'module=account&action=tokentx&address=0x%s&sort=desc' % address - return requests.request('GET', url=method).json()['result'] - - def is_transaction_successful(self, transaction_hash: str) -> int: - method = self.network_url + 'module=transaction&action=getstatus&txhash=%s' % transaction_hash - return not int(requests.request('GET', url=method).json()['result']['isError']) - - def get_balance(self, address): - method = self.network_url + 'module=account&action=balance&address=0x%s&tag=latest' % address - for i in range(5): - try: - return int(requests.request('GET', method).json()["result"]) - except ValueError: - pass - - def get_latest_block_number(self) -> int: - method = self.network_url + 'module=proxy&action=eth_blockNumber' - return int(requests.request('GET', url=method).json()['result'], 0) - - def find_transaction_by_hash(self, address: str, transaction_hash: str): - transactions = self.get_transactions(address=address) - for transaction in transactions: - if transaction['hash'] == transaction_hash: - info('Transaction is found in Ropsten network') - return - pytest.fail('Transaction is not found in Ropsten network') - - def find_transaction_by_unique_amount(self, address, amount, token=False, decimals=18, wait_time=600): - counter = 0 - while True: - if counter >= wait_time: - pytest.fail( - 'Transaction with amount %s is not found in list of transactions, address is %s' % - (amount, address)) - else: - counter += 10 - time.sleep(10) - if token: - transactions = self.get_token_transaction(address=address) - else: - transactions = self.get_transactions(address=address) - info('Looking for a transaction with unique amount %s in list of transactions, address is %s' % - (amount, address)) - for transaction in transactions: - if float(int(transaction['value']) / 10 ** decimals) == float(amount): - info('Transaction with unique amount %s is found in list of transactions, address is %s' % - (amount, address)) - return transaction - - def wait_for_confirmation_of_transaction(self, address, amount): + self.network_url = 'http://api-sepolia.arbiscan.io/api' + self.api_key = environ.get('ETHERSCAN_API_KEY') + + def log(self, text: str): + tests.test_suite_data.current_test.testruns[-1].steps.append(text) + logging.info(text) + + def send_etherscan_request(self, params): + params['apikey'] = self.api_key + try: + response = requests.get(url=self.network_url, params=params).json() + if response: + return response['result'] + except TypeError as e: + self.log("Check response from etherscan API. Returned values do not match expected. %s" % str(e)) + except JSONDecodeError as e: + self.log("No valid JSON response from Etherscan: %s " % str(e)) + pass + + def get_token_transactions(self, address: str) -> List[dict]: + params = {'module': 'account', 'action': 'tokentx', 'address': address, 'sort': 'desc'} + return self.send_etherscan_request(params) + + def get_transactions(self, address: str) -> List[dict]: + params = {'module': 'account', 'action': 'txlist', 'address': address, 'page': 1, 'offset': 10, 'sort': 'desc'} + return self.send_etherscan_request(params) + + def get_balance(self, address: str): + params = {'module': 'account', 'action': 'balance', 'address': address, 'tag': 'latest'} + balance = self.send_etherscan_request(params) + if balance: + self.log('Balance is %s Gwei' % balance) + return int(balance) / 1000000000000000000 + else: + self.log('Cannot extract balance!') + + def wait_for_confirmation_of_transaction(self, address, tx_time, confirmations=7, token=False): + expected_tx_timestamp = datetime.strptime(tx_time, "%Y-%m-%dT%H:%M:%S%z").replace(tzinfo=pytz.UTC) start_time = time.time() - while round(time.time() - start_time, ndigits=2) < 900: # should be < idleTimeout capability - transaction = self.find_transaction_by_unique_amount(address, amount) - if int(transaction['confirmations']) > 1: - return - time.sleep(10) - pytest.fail('Transaction with amount %s was not confirmed, address is %s' % (amount, address)) - - def verify_balance_is_updated(self, initial_balance, recipient_address, wait_time=360): - counter = 0 - while True: - if counter >= wait_time: - pytest.fail('Balance is not changed during %s seconds, funds were not received!' % wait_time) - elif initial_balance == self.get_balance(recipient_address): - counter += 10 - time.sleep(10) - info('Waiting %s seconds for funds' % counter) + if token: + token_info = "token transaction" + else: + token_info = "ETH transaction" + self.log('Waiting for %s of %s to have %s confirmations' % (token_info, address, confirmations)) + while round(time.time() - start_time, ndigits=2) < 600: # should be < idleTimeout capability + if token: + transaction = self.get_token_transactions(address)[0] else: - info('Transaction is received') + transaction = self.get_transactions(address)[0] + tx_timestamp = datetime.fromtimestamp(int(transaction['timeStamp'])).replace(tzinfo=pytz.UTC) + if tx_timestamp > expected_tx_timestamp: + if int(transaction['confirmations']) >= confirmations: + return + time.sleep(20) + self.log( + 'Expected amount of confirmations is %s, in fact %s' % (confirmations, transaction['confirmations'])) + pytest.fail('The last transaction was not confirmed, address is %s, still has %s confirmations' % ( + address, int(transaction['confirmations']))) + + def wait_for_balance_to_be(self, address: str, expected_balance: int): + expected_balance = round(expected_balance, 4) + for _ in range(5): + balance = round(self.get_balance(address), 4) + if balance == expected_balance: return + time.sleep(20) + raise TimeoutException( + 'balance is not updated on Etherscan, it is %s but expected to be %s' % (balance, expected_balance)) - def verify_balance_is(self, expected_balance: int, recipient_address: str, errors: list): - balance = self.get_balance(recipient_address) - if balance / 1000000000000000000 != expected_balance: - errors.append('Recipients balance is not updated on etherscan') - - def faucet(self, address): - return requests.request('GET', '%s/0x%s' % (self.faucet_url, address)).json() - def get_donate(self, address, wait_time=300): - initial_balance = self.get_balance(address) - counter = 0 - if initial_balance < 1000000000000000000: - response = self.faucet(address) - while True: - if counter >= wait_time: - pytest.fail("Donation was not received during %s seconds!" % wait_time) - elif self.get_balance(address) == initial_balance: - counter += 10 - time.sleep(10) - info('Waiting %s seconds for donation' % counter) - else: - info('Got %s for %s' % (response["amount_eth"], address)) - return - - def start_chat_bot(self, chat_name: str, messages_number: int, interval: int = 1) -> list: - url = '%s/ping/%s?count=%s&interval=%s' % (self.chat_bot_url, chat_name, messages_number, interval) - text = requests.request('GET', url).text - return [i.split(maxsplit=5)[-1].strip('*') for i in text.splitlines()] +class SepoliaNetworkApi: + def __init__(self): + self.rpc_url = 'https://ethereum-sepolia.rpc.subquery.network/public' + + def is_tx_successful(self, tx_hash): + response = requests.post(headers={"Content-Type": "application/json"}, + url=self.rpc_url, + data=json.dumps( + {"jsonrpc": "2.0", "method": "eth_getTransactionReceipt", "params": [tx_hash], + "id": 1})).json() + result = response.get('result') + if result is not None: + return result.get('status') == '0x1' + return None + \ No newline at end of file diff --git a/test/appium/support/api/third_parties_api.py b/test/appium/support/api/third_parties_api.py deleted file mode 100644 index e69e3a8c729..00000000000 --- a/test/appium/support/api/third_parties_api.py +++ /dev/null @@ -1,11 +0,0 @@ -import requests - - -def get_ethereum_price_in_usd() -> float: - url = 'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD' - return float(requests.request('GET', url).json()['USD']) - - -def get_token_info(address: str): - url = 'http://api.ethplorer.io/getTokenInfo/%s?apiKey=freekey' % address - return requests.request('GET', url).json() diff --git a/test/appium/support/base_test_report.py b/test/appium/support/base_test_report.py index 0d58fb444c4..aebd581300f 100644 --- a/test/appium/support/base_test_report.py +++ b/test/appium/support/base_test_report.py @@ -1,17 +1,16 @@ import json -import hmac import os -from hashlib import md5 +import re +import urllib + +from support.lambda_test import get_session_info from support.test_data import SingleTestData class BaseTestReport: - TEST_REPORT_DIR = "%s/../report" % os.path.dirname(os.path.abspath(__file__)) - def __init__(self, sauce_username, sauce_access_key): - self.sauce_username = sauce_username - self.sauce_access_key = sauce_access_key + def __init__(self): self.init_report() def init_report(self): @@ -26,12 +25,33 @@ def get_test_report_file_path(self, test_name): file_name = "%s.json" % test_name return os.path.join(self.TEST_REPORT_DIR, file_name) - def save_test(self, test): + def save_logs(self, logs: dict): + logs_paths = {} + for log_name, log_content in logs.items(): + log_path = os.path.join(self.TEST_REPORT_DIR, log_name) + with open(log_path, "wb") as archive_file: + archive_file.write(log_content) + logs_paths[log_name] = log_path + return logs_paths + + def save_test(self, test, geth: dict = None, requests_log: dict = None): + for log in geth, requests_log: + if log: + logs_paths = self.save_logs(log) + else: + if hasattr(test, 'logs_paths'): + logs_paths = test.logs_paths + else: + logs_paths = '' + file_path = self.get_test_report_file_path(test.name) test_dict = { 'testrail_case_id': test.testrail_case_id, 'name': test.name, - 'testruns': list() + 'logs_paths': logs_paths, + 'testruns': list(), + 'group_name': test.group_name, + 'secured': test.secured } for testrun in test.testruns: test_dict['testruns'].append(testrun.__dict__) @@ -39,40 +59,65 @@ def save_test(self, test): def get_all_tests(self): tests = list() - file_list = [f for f in os.listdir(self.TEST_REPORT_DIR)] + file_list = [f for f in os.listdir(self.TEST_REPORT_DIR) if f.endswith('json')] for file_name in file_list: file_path = os.path.join(self.TEST_REPORT_DIR, file_name) test_data = json.load(open(file_path)) testruns = list() for testrun_data in test_data['testruns']: testruns.append(SingleTestData.TestRunData( - steps=testrun_data['steps'], jobs=testrun_data['jobs'], error=testrun_data['error'])) - tests.append(SingleTestData(name=test_data['name'], testruns=testruns, - testrail_case_id=test_data['testrail_case_id'])) + steps=testrun_data['steps'], + jobs=testrun_data['jobs'], + error=testrun_data['error'], + first_commands=testrun_data['first_commands'], + xfail=testrun_data['xfail'], + run=testrun_data['run'])) + tests.append(SingleTestData(name=test_data['name'], + logs_paths=test_data['logs_paths'], + testruns=testruns, + testrail_case_id=test_data['testrail_case_id'], + grop_name=test_data['group_name'], + secured=test_data['secured'])) return tests - def get_failed_tests(self): - tests = self.get_all_tests() - failed = list() - for test in tests: - if not self.is_test_successful(test): - failed.append(test) - return failed - - def get_passed_tests(self): + def get_tests_by_status(self): tests = self.get_all_tests() - passed = list() + passed, failed, xfailed = list(), list(), list() for test in tests: if self.is_test_successful(test): passed.append(test) - return passed + else: + if test.testruns[-1].xfail: + xfailed.append(test) + else: + failed.append(test) + return passed, failed, xfailed + + def get_lambda_test_job_url(self, job_id, first_command=0): + return "https://appautomation.lambdatest.com/test?testID=" + get_session_info(job_id)['test_id'] - def get_sauce_job_url(self, job_id): - token = hmac.new(bytes(self.sauce_username + ":" + self.sauce_access_key, 'latin-1'), - bytes(job_id, 'latin-1'), md5).hexdigest() - return "https://saucelabs.com/jobs/%s?auth=%s" % (job_id, token) + @staticmethod + def get_jenkins_link_to_rerun_e2e(branch_name="develop", pr_id="", tr_case_ids=""): + branch_name = urllib.parse.quote(branch_name) + return 'https://ci.status.im/job/status-mobile/job/e2e/job/status-app-prs-rerun/parambuild/' \ + '?BRANCH_NAME=%s&PR_ID=%s&APK_NAME=%s.apk&TR_CASE_IDS=%s' % (branch_name, pr_id, pr_id, tr_case_ids) @staticmethod def is_test_successful(test): # Test passed if last testrun has passed return test.testruns[-1].error is None + + @staticmethod + def separate_xfail_error(error): + issue_id_list = re.findall(r'#\d+', error) + issue_id = issue_id_list[0] if issue_id_list else '' + + xfail_error = re.findall(r'\[\[.*\]\]', error) + if xfail_error: + no_code_error_str = xfail_error[0] + main_error = error.replace(no_code_error_str, '') + else: + no_code_error_str = '' + main_error = error + + return main_error, no_code_error_str, issue_id diff --git a/test/appium/support/device_apps.py b/test/appium/support/device_apps.py deleted file mode 100644 index 5afbca1f581..00000000000 --- a/test/appium/support/device_apps.py +++ /dev/null @@ -1,11 +0,0 @@ -from selenium.common.exceptions import WebDriverException - -from tests import info - - -def start_web_browser(driver): - info('Start web browser') - try: - driver.start_activity('org.chromium.webview_shell', 'WebViewBrowserActivity') - except WebDriverException: - pass diff --git a/test/appium/support/github_report.py b/test/appium/support/github_report.py index e685e62bb50..5c8de23a3cb 100644 --- a/test/appium/support/github_report.py +++ b/test/appium/support/github_report.py @@ -1,65 +1,138 @@ import os + from support.base_test_report import BaseTestReport +from support.testrail_report import TestrailReport class GithubHtmlReport(BaseTestReport): - TEST_REPORT_DIR = "%s/../report" % os.path.dirname(os.path.abspath(__file__)) - def __init__(self, sauce_username, sauce_access_key): - super(GithubHtmlReport, self).__init__(sauce_username, sauce_access_key) + def __init__(self): + super(GithubHtmlReport, self).__init__() - def build_html_report(self): + def list_of_failed_testrail_ids(self, tests_data): + ids_failed_test = [] + for i, test in enumerate(tests_data): + if test.testrail_case_id: + ids_failed_test.append(test.testrail_case_id) + return ','.join(map(str, ids_failed_test)) + + def build_html_report(self, run_id): tests = self.get_all_tests() - passed_tests = self.get_passed_tests() - failed_tests = self.get_failed_tests() + passed, failed, xfailed = self.get_tests_by_status() + not_executed_tests = TestrailReport().get_not_executed_tests(run_id) if len(tests) > 0: - title_html = "## %.0f%% of end-end tests have passed\n" % (len(passed_tests) / len(tests) * 100) + title_html = "## %.0f%% of end-end tests have passed\n" % (len(passed) / len(tests) * 100) summary_html = "```\n" summary_html += "Total executed tests: %d\n" % len(tests) - summary_html += "Failed tests: %d\n" % len(failed_tests) - summary_html += "Passed tests: %d\n" % len(passed_tests) + summary_html += "Failed tests: %d\n" % len(failed) + summary_html += "Expected to fail tests: %d\n" % len(xfailed) + summary_html += "Passed tests: %d\n" % len(passed) + if not_executed_tests: + summary_html += "Not executed tests: %d\n" % len(not_executed_tests) summary_html += "```\n" + not_executed_tests_html = str() failed_tests_html = str() + xfailed_tests_html = str() passed_tests_html = str() - if failed_tests: - failed_tests_html = self.build_tests_table_html(failed_tests, failed_tests=True) - if passed_tests: - passed_tests_html = self.build_tests_table_html(passed_tests, failed_tests=False) - return title_html + summary_html + failed_tests_html + passed_tests_html + if not_executed_tests: + not_executed_tests_html = self.build_tests_table_html(not_executed_tests, run_id, + not_executed_tests=True) + summary_html += "```\n" + summary_html += 'IDs of not executed tests: %s \n' % ','.join([str(i) for i in not_executed_tests]) + summary_html += "```\n" + if failed: + failed_tests_html = self.build_tests_table_html(failed, run_id, failed_tests=True) + summary_html += "```\n" + summary_html += 'IDs of failed tests: %s \n' % self.list_of_failed_testrail_ids(failed) + summary_html += "```\n" + if xfailed: + xfailed_tests_html = self.build_tests_table_html(xfailed, run_id, xfailed_tests=True) + summary_html += "```\n" + summary_html += 'IDs of expected to fail tests: %s \n' % self.list_of_failed_testrail_ids(xfailed) + summary_html += "```\n" + if passed: + passed_tests_html = self.build_tests_table_html(passed, run_id, failed_tests=False) + return title_html + summary_html + not_executed_tests_html + failed_tests_html + xfailed_tests_html \ + + passed_tests_html else: return None - def build_tests_table_html(self, tests, failed_tests=False): - tests_type = "Failed tests" if failed_tests else "Passed tests" + def build_tests_table_html(self, tests, run_id, failed_tests=False, xfailed_tests=False, not_executed_tests=False): + if failed_tests: + tests_type = "Failed tests" + elif not_executed_tests: + tests_type = "Not executed tests" + elif xfailed_tests: + tests_type = "Expected to fail tests" + else: + tests_type = "Passed tests" html = "

%s (%d)

" % (tests_type, len(tests)) html += "
" html += "Click to expand" html += "
" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - for i, test in enumerate(tests): - html += self.build_test_row_html(i, test) - html += "" - html += "
" + + from tests import pytest_config_global + pr_id = pytest_config_global['pr_number'] + + from github import Github + from conftest import github_token + branch_name = Github(github_token).get_user('status-im').get_repo('status-mobile').get_pull(int(pr_id)).head.ref + + if not_executed_tests: + html += "
  • Rerun not executed tests
  • " % self.get_jenkins_link_to_rerun_e2e( + branch_name=branch_name, + pr_id=pr_id, + tr_case_ids=','.join([str(i) for i in tests])) + + if failed_tests: + html += "
  • Rerun failed tests
  • " % self.get_jenkins_link_to_rerun_e2e( + branch_name=branch_name, + pr_id=pr_id, + tr_case_ids=','.join([str(test.testrail_case_id) for test in tests])) + + if not not_executed_tests: + groups = {i: list() for i in set([test.group_name for test in tests])} + for i in tests: + groups[i.group_name].append(i) + + html += "
    " + + for class_name, tests_list in groups.items(): + if class_name: + html += "

    Class %s:

    " % class_name + else: + html += "

    Single device tests:

    " + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + for i, test in enumerate(tests_list): + html += self.build_test_row_html(i, test, run_id) + html += "" + html += "
    " html += "
    " return html - def build_test_row_html(self, index, test): - html = "%d. %s" % (index+1, test.name) + def build_test_row_html(self, index, test, run_id): + test_rail_link = TestrailReport().get_test_result_link(run_id, test.testrail_case_id) + if test_rail_link: + html = "%s. %s, id: %s " % ( + index + 1, test_rail_link, test.name, test.testrail_case_id) + else: + html = "%d. %s (TestRail link is not found)" % (index + 1, test.name) html += "" test_steps_html = list() last_testrun = test.testruns[-1] for step in last_testrun.steps: test_steps_html.append("
    %s
    " % step) if last_testrun.error: + error = last_testrun.error if test_steps_html: html += "

    " html += "

    " @@ -67,18 +140,35 @@ def build_test_row_html(self, index, test): html += "%s" % ''.join(test_steps_html[-2:]) html += "
    " html += "

    " - html += "%s" % last_testrun.error + code_error, no_code_error_str, _ = self.separate_xfail_error(error) + if no_code_error_str: + html += "\n\n```\n%s\n```\n\n" % code_error + html += "%s" % no_code_error_str + else: + html += "\n\n```\n%s\n```\n\n" % error.replace("[[", "[[").replace("]]", "]]") html += "

    " - if last_testrun.jobs: - html += self.build_device_sessions_html(last_testrun.jobs) + if last_testrun.jobs and not test.secured: + html += self.build_device_sessions_html(last_testrun) html += "" return html - def build_device_sessions_html(self, jobs): - html = "Device sessions:" + def build_device_sessions_html(self, test_run): + html = "Device sessions" html += "

      " - for i, job_id in enumerate(jobs): - html += "
    • Device %d
    • " % (self.get_sauce_job_url(job_id), i+1) + for job_id, i in test_run.jobs.items(): + html += "

      " + html += "Device %d:" % i + html += "

        " + if test_run.first_commands: + try: + first_command = test_run.first_commands[job_id] + except KeyError: + first_command = 0 + else: + first_command = 0 + html += "
      • Steps, video, logs
      • " % self.get_lambda_test_job_url(job_id, first_command) + # if test_run.error: + # html += "
      • Failure screenshot
      • " % self.get_sauce_final_screenshot_url(job_id) + html += "

      " html += "

    " return html - diff --git a/test/appium/support/helpers.py b/test/appium/support/helpers.py new file mode 100644 index 00000000000..86d1bcd3bb8 --- /dev/null +++ b/test/appium/support/helpers.py @@ -0,0 +1,15 @@ +from bip_utils import Bip39SeedGenerator, Bip44, Bip44Coins, Bip44Changes + + +def generate_wallet_address(passphrase: str, number: int = 1): + # Derive accounts using the standard MetaMask path + seed_bytes = Bip39SeedGenerator(passphrase).Generate() + bip44_mst_ctx = Bip44.FromSeed(seed_bytes, Bip44Coins.ETHEREUM) + + # Generate first n addresses + expected_addresses = list() + for i in range(number): + bip44_acc_ctx = bip44_mst_ctx.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT).AddressIndex(i) + expected_addresses.append(bip44_acc_ctx.PublicKey().ToAddress().lower()) + + return expected_addresses diff --git a/test/appium/support/lambda_test.py b/test/appium/support/lambda_test.py new file mode 100644 index 00000000000..ff5c11b2438 --- /dev/null +++ b/test/appium/support/lambda_test.py @@ -0,0 +1,38 @@ +import requests + +from conftest import lambda_test_username, lambda_test_access_key +from tests import test_suite_data + +session = requests.Session() +session.auth = (lambda_test_username, lambda_test_access_key) + + +def upload_apk(apk_file_path): + resp = session.post( + url="https://manual-api.lambdatest.com/app/upload/virtualDevice", + files={'appFile': open(apk_file_path, 'rb')}, + data={'name': test_suite_data.apk_name} + ) + assert resp.status_code == 200 + return resp.json()['app_url'] + + +def update_session(session_id, session_name, status): + resp = session.get( + url="https://mobile-api.lambdatest.com/mobile-automation/api/v1/sessions/%s" % session_id, + data={ + "name": session_name # , "status_ind": status + } + ) + assert resp.status_code == 200 + + +def get_session_info(session_id): + resp = session.get(url="https://mobile-api.lambdatest.com/mobile-automation/api/v1/sessions/%s" % session_id) + assert resp.status_code == 200 + return resp.json()['data'] + + +def upload_image(): + 'curl -u "yevheniia:fZaXHEAFEWOVCZLnSVrwHY11eJGsWAknibtG572PiZsvT1h57V" -X POST https://mobile-mgm.lambdatest.com/mfs/v1.0/media/upload -F media_file=@/Users/yberdnyk/Downloads/aaa.png -F type=image -F custom_id=SampleImage' + pass diff --git a/test/appium/support/message_reliability_report.py b/test/appium/support/message_reliability_report.py deleted file mode 100644 index ce7e3009479..00000000000 --- a/test/appium/support/message_reliability_report.py +++ /dev/null @@ -1,61 +0,0 @@ -def write_data_to_file(sent: int, received: int): - with open('messages.txt', 'a') as f: - f.write("['sent': %s, 'received': %s]" % (sent, received)) - - -def create_one_to_one_chat_report(one_to_one_chat_data: dict): - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - - user_a = one_to_one_chat_data['user_a'] - user_b = one_to_one_chat_data['user_b'] - - fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(15, 7)) - time_1 = sorted(user_a['message_time']) - ax.plot([i / 60 for i in time_1], [user_a['message_time'][i] for i in time_1], - 'o-', color='#0c0fea', label='user_a') - time_2 = sorted(user_b['message_time']) - ax.plot([i / 60 for i in time_2], [user_b['message_time'][i] for i in time_2], - 'o-', color='#f61e06', label='user_b') - - sent_messages = user_a['sent_messages'] + user_b['sent_messages'] - received_messages = len(user_a['message_time']) + len(user_b['message_time']) - - title = "User A: sent messages: {}, received messages: {}" \ - "\nUser B: sent messages: {}, received messages: {}".format(user_a['sent_messages'], - len(user_a['message_time']), - user_b['sent_messages'], - len(user_b['message_time'])) - if sent_messages: - title += "\nReceived messages: {}%".format( - round((len(user_a['message_time']) + len(user_b['message_time'])) / sent_messages * 100, ndigits=2)) - - plt.title(title) - plt.xlabel('chat session duration, minutes') - plt.ylabel('time to receive a message, seconds') - plt.legend() - fig.savefig('chart.png') - - write_data_to_file(sent=sent_messages, received=received_messages) - - -def create_public_chat_report(public_chat_data: dict): - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - - sent_messages = public_chat_data['sent_messages'] - message_time = public_chat_data['message_time'] - - fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(15, 7)) - sorted_time = sorted(message_time) - ax.plot([i / 60 for i in sorted_time], [message_time[i] for i in sorted_time], 'o-', color='#0c0fea') - title = "Sent messages: {}\nReceived messages: {}".format(sent_messages, len(message_time)) - plt.title(title) - plt.xlabel('chat session duration, minutes') - plt.ylabel('time to receive a message, seconds') - plt.legend() - fig.savefig('chart.png') - - write_data_to_file(sent=sent_messages, received=len(message_time)) diff --git a/test/appium/support/test_data.py b/test/appium/support/test_data.py index b09ce88420f..80aefd38a4c 100644 --- a/test/appium/support/test_data.py +++ b/test/appium/support/test_data.py @@ -1,17 +1,28 @@ +from typing import Dict + + class SingleTestData(object): - def __init__(self, name, testruns, testrail_case_id): + def __init__(self, name, testruns, testrail_case_id, logs_paths, grop_name, secured): self.testrail_case_id = testrail_case_id self.name = name self.testruns = testruns + self.logs_paths = logs_paths + self.group_name = grop_name + self.secured = secured class TestRunData(object): - def __init__(self, steps, jobs, error): + def __init__(self, steps, jobs, error, first_commands: Dict[str, int], xfail, run): self.steps = steps self.jobs = jobs self.error = error + self.first_commands = first_commands + self.xfail = xfail + self.run = run def create_new_testrun(self): - self.testruns.append(SingleTestData.TestRunData(list(), list(), None)) + self.testruns.append(SingleTestData.TestRunData( + steps=list(), jobs=dict(), error=None, first_commands=dict(), xfail='', run=True + )) class TestSuiteData(object): @@ -20,11 +31,11 @@ def __init__(self): self.current_test = None self.tests = list() - def set_current_test(self, test_name, testrail_case_id): + def set_current_test(self, test_name, testrail_case_id, secured): existing_test = next((test for test in self.tests if test.name == test_name), None) if existing_test: self.current_test = existing_test else: - test = SingleTestData(test_name, list(), testrail_case_id) + test = SingleTestData(test_name, list(), testrail_case_id, list(), None, secured) self.tests.append(test) self.current_test = test diff --git a/test/appium/support/test_rerun.py b/test/appium/support/test_rerun.py deleted file mode 100644 index 9ed9a9f6b4f..00000000000 --- a/test/appium/support/test_rerun.py +++ /dev/null @@ -1,28 +0,0 @@ -RERUN_ERRORS = [ - "can't receive further commands", - 'Original error: Error: ESOCKETTIMEDOUT', - "The server didn't respond in time.", - 'An unknown server-side error occurred while processing the command.', - 'Could not proxy command to remote server. Original error: Error: socket hang up', - 'The server returned an invalid or incomplete response.', - '502 Bad Gateway', - 'Unexpected server error', - '504 Gateway Time-out', - 'Internal Server Error', - 'Invalid message: ERROR Internal Server Error', - 'ERROR The test with session id' - "Message: 'CreateAccountButton' is not found on screen", - "503 Service Unavailable", - "object has no attribute", - "[Errno 104] Connection reset by peer", - "Sauce could not start your job", - "HTTP Error 303", - "http.client.RemoteDisconnected: Remote end closed connection without response" -] - - -def should_rerun_test(test_error): - for rerun_error in RERUN_ERRORS: - if rerun_error in test_error: - return True - return False diff --git a/test/appium/support/testrail_report.py b/test/appium/support/testrail_report.py index 11c98b585b5..9bc0a49c676 100644 --- a/test/appium/support/testrail_report.py +++ b/test/appium/support/testrail_report.py @@ -1,54 +1,76 @@ -import json -import requests -import emoji import base64 +import json +import logging +from json import JSONDecodeError from os import environ +from sys import argv + +import emoji +import requests +from testrail.client import TestRail + from support.base_test_report import BaseTestReport class TestrailReport(BaseTestReport): - def __init__(self, sauce_username, sauce_access_key): - super(TestrailReport, self).__init__(sauce_username, sauce_access_key) + def __init__(self): + super(TestrailReport, self).__init__() self.password = environ.get('TESTRAIL_PASS') self.user = environ.get('TESTRAIL_USER') self.run_id = None - self.suite_id = 15 - self.project_id = 9 + self.suite_id = 5274 + self.project_id = 14 self.outcomes = { 'passed': 1, - 'undefined_fail': 10} + 'failed': 5, + 'undefined_fail': 10, + 'skipped': 11} self.headers = dict() self.headers['Authorization'] = 'Basic %s' % str( base64.b64encode(bytes('%s:%s' % (self.user, self.password), 'utf-8')), 'ascii').strip() self.headers['Content-Type'] = 'application/json' + self.headers['x-api-ident'] = 'beta' - self.url = 'https://ethstatus.testrail.net/index.php?/api/v2/' + self.url = 'https://ethstatus.testrail.net' + self.api_url = self.url + '/index.php?/api/v2/' + self.client = TestRail(email=self.user, key=self.password, url=self.url, project_id=self.project_id) def get(self, method): - raw_response = requests.get(self.url + method, headers=self.headers).text - return json.loads(raw_response) + rval = self.client.api._get(method) + if 'error' in rval: + logging.error("Failed TestRail request: %s" % rval['error']) + return rval def post(self, method, data): data = bytes(json.dumps(data), 'utf-8') - raw_response = requests.post(self.url + method, data=data, headers=self.headers).text - return json.loads(raw_response) + return self.client.api._post(method, data=json.loads(data)) + + def add_attachment(self, method, path): + files = {'attachment': (open(path, 'rb'))} + result = requests.post(self.api_url + method, headers={'Authorization': self.headers['Authorization']}, + files=files) + files['attachment'].close() + try: + return result.json() + except JSONDecodeError: + pass def get_suites(self): return self.get('get_suites/%s' % self.project_id) def get_tests(self): - return self.get('get_tests/%s' % self.run_id) + return self.get('get_tests/%s' % self.run_id)['tests'] def get_milestones(self): - return self.get('get_milestones/%s' % self.project_id) + return self.get('get_milestones/%s' % self.project_id)['milestones'] def get_runs(self, pr_number): - return [i for i in self.get('get_runs/%s' % self.project_id) if 'PR-%s ' % pr_number in i['name']] + return [i for i in self.get('get_runs/%s' % self.project_id)['runs'] if 'PR-%s ' % pr_number in i['name']] def get_run(self, run_id: int): return self.get('get_run/%s' % run_id) @@ -65,41 +87,213 @@ def add_run(self, run_name): request_body = {'suite_id': self.suite_id, 'name': run_name, 'milestone_id': self.actual_milestone_id, - 'case_ids': self.get_regression_cases(is_pr='PR-' in run_name), + 'case_ids': self.get_regression_cases(), 'include_all': False} run = self.post('add_run/%s' % self.project_id, request_body) - self.run_id = run['id'] + try: + self.run_id = run['id'] + except KeyError: + print("TestRail error when creating a run: %s" % run) + print("Testrun: %sruns/view/%s" % (self.url, self.run_id)) def get_cases(self, section_id): - return self.get('get_cases/%s&suite_id=%s§ion_id=%s' % (self.project_id, self.suite_id, section_id)) - - def get_regression_cases(self, is_pr=False): - test_cases = dict() - test_cases['smoke_phase_1'] = 157 - test_cases['smoke_phase_2'] = 308 - test_cases['upgrade'] = 309 - test_cases['error_handling'] = 458 - test_cases['logcat_verifications'] = 718 + return self.get('get_cases/%s&suite_id=%s§ion_id=%s' % (self.project_id, self.suite_id, section_id))[ + 'cases'] + + def get_regression_cases(self): + test_categories = dict() + test_categories['all'] = 50955 + test_categories['one_to_one_chat'] = 50956 + test_categories['group_chat'] = 50964 + test_categories['community_multiple'] = 50982 + test_categories['community_single'] = 50983 + test_categories['activity_centre_contact_request'] = 50984 + test_categories['activity_centre_other'] = 51005 + test_categories['deep_links'] = 51535 + test_categories['wallet'] = 59443 + test_categories['fallback'] = 63472 + test_categories['android_versions'] = 63809 + test_categories['profile'] = 63854 + test_categories['real_tx'] = 63867 + test_categories['wallet_connect'] = 64122 + case_ids = list() - if is_pr: - case_ids = [case['id'] for case in self.get_cases(test_cases['smoke_phase_1'])] - else: - for phase in test_cases: - for case in self.get_cases(test_cases[phase]): - case_ids.append(case['id']) + for arg in argv: + if "run_testrail_ids" in arg: + key, value = arg.split('=') + case_ids = value.split(',') + if len(case_ids) == 0: + if 'smoke' in argv: + for category in test_categories.values(): + case_ids.extend([case['id'] for case in self.get_cases(category) if case['type_id'] == 11]) + else: + for category in test_categories.values(): + case_ids.extend([case['id'] for case in self.get_cases(category)]) return case_ids def add_results(self): - for test in self.get_all_tests(): + data = list() + all_tests = self.get_all_tests() + for test in all_tests: test_steps = "# Steps: \n" devices = str() - method = 'add_result_for_case/%s/%s' % (self.run_id, test.testrail_case_id) last_testrun = test.testruns[-1] for step in last_testrun.steps: test_steps += step + "\n" for i, device in enumerate(last_testrun.jobs): - devices += "# [Device %d](%s) \n" % (i + 1, self.get_sauce_job_url(device)) - data = {'status_id': self.outcomes['undefined_fail'] if last_testrun.error else self.outcomes['passed'], - 'comment': '%s' % ('# Error: \n %s \n' % emoji.demojize(last_testrun.error)) + devices + test_steps if last_testrun.error - else devices + test_steps} - self.post(method, data=data) + if last_testrun.first_commands: + try: + first_command = last_testrun.first_commands[device] + except KeyError: + first_command = 0 + else: + first_command = 0 + try: + devices += "# [Device %d](%s) \n" % (i + 1, self.get_lambda_test_job_url(job_id=device, + first_command=first_command)) + except KeyError: + devices += "# Device %s: LambdaTest session was not found \n" % (i + 1) + comment = str() + if test.group_name: + comment += "# Class: %s \n" % test.group_name + if last_testrun.error: + full_error = last_testrun.error + (code_error, no_code_error_str, issue_id) = self.separate_xfail_error(full_error) + if issue_id: + test_rail_xfail = self.make_error_with_gh_issue_link(no_code_error_str, issue_id) + error = "%s %s" % (code_error, test_rail_xfail) + else: + error = full_error + error = error.replace("[[", "**").replace("]]", "**") + comment += '%s' % ('# Error: \n %s \n' % emoji.demojize(error)) + devices + test_steps + else: + comment += devices + test_steps + + if last_testrun.xfail and not last_testrun.run: + status_id = self.outcomes['skipped'] + elif last_testrun.error: + if last_testrun.xfail: + status_id = self.outcomes['failed'] + else: + status_id = self.outcomes['undefined_fail'] + else: + status_id = self.outcomes['passed'] + data.append( + {'case_id': test.testrail_case_id, + 'status_id': status_id, + 'comment': comment}) + + results = self.post('add_results_for_cases/%s' % self.run_id, data={"results": data}) + try: + results[0] + except (IndexError, KeyError): + print("Got TestRail error when adding results: \n%s" % results) + + for test in all_tests: + last_testrun = test.testruns[-1] + if last_testrun.error: + try: + device = list(last_testrun.jobs.keys())[0] + except IndexError: + continue + for res in results: + if ('setup failed' in last_testrun.error and self.get_lambda_test_job_url(job_id=device) in res[ + 'comment']) or test.name in res['comment']: + try: + for log in test.logs_paths.keys(): + self.add_attachment(method='add_attachment_to_result/%s' % str(res['id']), + path=test.logs_paths[log]) + except (AttributeError, FileNotFoundError): + pass + break + + self.change_test_run_description() + + def change_test_run_description(self): + tests = self.get_all_tests() + passed, failed, xfailed = self.get_tests_by_status() + not_executed_tests = self.get_not_executed_tests(self.run_id) + final_description = "Nothing to report this time..." + if len(tests) > 0: + description_title = "# %.0f%% of end-end tests have passed\n" % (len(passed) / len(tests) * 100) + description_title += "\n" + description_title += "Total executed tests: %d\n" % len(tests) + description_title += "Failed tests: %d\n" % len(failed) + description_title += "Expected to fail tests: %d\n" % len(xfailed) + description_title += "Passed tests: %d\n" % len(passed) + if not_executed_tests: + description_title += "Not executed tests: %d\n" % len(not_executed_tests) + description_title += "\n" + ids_failed_test = [] + single_devices_block, group_blocks, case_info = str(), dict(), str() + if failed: + for test in failed: + if test.group_name: + group_blocks[test.group_name] = "\n-------\n## Class: %s:\n" % test.group_name + for test in failed: + last_testrun = test.testruns[-1] + test_rail_link = self.get_test_result_link(self.run_id, test.testrail_case_id) + ids_failed_test.append(test.testrail_case_id) + case_title = '\n' + case_title += '-------\n' + case_title += "### ID %s: [%s](%s) \n" % (test.testrail_case_id, test.name, test_rail_link) + full_error = last_testrun.error[-255:] + (code_error, no_code_error_str, issue_id) = self.separate_xfail_error(full_error) + if issue_id: + test_rail_xfail = self.make_error_with_gh_issue_link(no_code_error_str, issue_id) + error = "```%s```\n **%s** \n" % (code_error, test_rail_xfail) + else: + error = "```%s```\n **%s** \n" % (code_error, no_code_error_str) + for job_id, f in last_testrun.jobs.items(): + if last_testrun.first_commands: + try: + first_command = last_testrun.first_commands[job_id] + except KeyError: + first_command = 0 + else: + first_command = 0 + job_url = self.get_lambda_test_job_url(job_id=job_id, first_command=first_command) + case_info = "Logs for device %d: [steps](%s)" % (f, job_url) + + if test.group_name: + group_blocks[test.group_name] += case_title + error + case_info + else: + single_devices_block += case_title + error + case_info + description_title += '## Failed tests: %s \n' % ','.join(map(str, ids_failed_test)) + if not_executed_tests: + description_title += "## Not executed tests: %s\n" % ','.join([str(i) for i in not_executed_tests]) + final_description = description_title + single_devices_block + ''.join([i for i in group_blocks.values()]) + + request_body = {'description': final_description} + return self.post('update_run/%s' % self.run_id, request_body) + + def get_run_results(self, test_run_id=None): + return self.get('get_results_for_run/%s' % (test_run_id if test_run_id else self.run_id))['results'] + + def is_run_successful(self): + for test in self.get_run_results(): + if test['status_id'] != 1: + return False + else: + return True + + def get_test_result_link(self, test_run_id, test_case_id): + try: + test_id = self.get('get_results_for_case/%s/%s' % (test_run_id, test_case_id))['results'][0]['test_id'] + return '%s/index.php?/tests/view/%s' % (self.url, test_id) + except (KeyError, JSONDecodeError): + print('Cannot extract result for %s e2e' % test_case_id) + return None + + def get_not_executed_tests(self, test_run_id): + try: + results = self.get("get_tests/%s&status_id=3" % test_run_id) + return [result['case_id'] for result in results["tests"]] + except KeyError: + print('Cannot extract result for %s' % test_run_id) + pass + + @staticmethod + def make_error_with_gh_issue_link(error, issue_id): + return error.replace(issue_id, + '[%s](https://github.com/status-im/status-mobile/issues/%s)' % (issue_id, issue_id[1:])) diff --git a/test/appium/tests/__init__.py b/test/appium/tests/__init__.py index da29420ed90..45ae68fe61a 100644 --- a/test/appium/tests/__init__.py +++ b/test/appium/tests/__init__.py @@ -1,176 +1,54 @@ import asyncio -import logging +import functools +import json +import os +import time from datetime import datetime +from urllib3.exceptions import MaxRetryError + from support.test_data import TestSuiteData -@asyncio.coroutine -def start_threads(quantity: int, func: type, returns: dict, *args): +async def start_threads(test_name: str, quantity: int, func: type, returns: dict, **kwargs): loop = asyncio.get_event_loop() for i in range(quantity): - returns[i] = loop.run_in_executor(None, func, *args) + returns[i] = loop.run_in_executor(None, functools.partial(func, **kwargs)) for k in returns: - returns[k] = yield from returns[k] + for _ in range(3): + try: + returns[k] = await returns[k] + break + except MaxRetryError: + print("MaxRetryError when creating a driver for %s" % test_name) + time.sleep(10) return returns -def get_current_time(): - return datetime.now().strftime('%-m%-d%-H%-M%-S') - - -def info(text: str): - if "Base" not in text: - logging.info(text) - test_suite_data.current_test.testruns[-1].steps.append(text) +async def run_in_parallel(funcs): + loop = asyncio.get_event_loop() + res = [] + returns = [] + for func in funcs: + try: + res.append(loop.run_in_executor(None, functools.partial(func[0], **func[1]))) + except IndexError: + res.append(loop.run_in_executor(None, func[0])) + for k in res: + returns.append(await k) + return returns -def debug(text: str): - logging.debug(text) +def get_current_time(): + return datetime.now().strftime('%-m%-d%-H%-M%-S') +pytest_config_global = dict() test_suite_data = TestSuiteData() +common_password = 'qwerty1234' -basic_user = dict() -basic_user['password'] = "newuniquepassword12" -basic_user['passphrase'] = "tree weekend ceiling awkward universe pyramid glimpse raven pair lounge grant grief" -basic_user['username'] = "Little Weighty Iberianmole" -basic_user['public_key'] = "0x040d3400f0ba80b2f6017a9021a66e042abc33cf7051ddf98a24a815c93d6c052ce2b7873d799f096325" \ - "9f41c5a1bf08133dd4f3fe63ea1cceaa1e86ebc4bc42c9" - -transaction_users = dict() -transaction_users['A_USER'] = dict() -transaction_users['A_USER']['password'] = "qwerty" -transaction_users['A_USER']['passphrase'] = "heavy earn fence pool drift balcony act coast stairs basic juice hip" -transaction_users['A_USER']['username'] = "Simplistic Personal Isabellinewheatear" -transaction_users['A_USER']['address'] = "c8f8b3376814d0dfe61e4b9eaf7970697c24f6b2" -transaction_users['A_USER']['public_key'] = "0x042bb1943a6516510ecb57356f1dbeeaf928e3260b4d0b8e81e1e139ab9e4be75" \ - "185086bedb27992d554d6e7782515e4c00d022b89d46ea5c208f0e5003c7286a4" - -transaction_users['B_USER'] = dict() -transaction_users['B_USER']['password'] = "qwerty" -transaction_users['B_USER']['passphrase'] = "subway size dial screen stomach route skin kitchen radar true coffee six" -transaction_users['B_USER']['username'] = "Unselfish Thrifty Racer" -transaction_users['B_USER']['address'] = "85b159bad0d500483b307ec6892c7fa3be591cbc" -transaction_users['B_USER']['public_key'] = "0x04d25784aec35405a870dba2e44e02103bc6a258fe9fa8580bb1da4deeedf8827" \ - "baa3c44def9a4685a7db7a1d30c46d3ca9e7ef46a6ca9037a59791e2b0eca04fd" - -transaction_users['C_USER'] = dict() -transaction_users['C_USER']['password'] = "qwerty" -transaction_users['C_USER']['passphrase'] = "potato labor object reward minor casino dismiss size flame task winter report" -transaction_users['C_USER']['username'] = "Magnificent Earnest Bandicoot" -transaction_users['C_USER']['address'] = "549b9bcd069d720390a6819ff8b3d9dd252fa3be" -transaction_users['C_USER']['public_key'] = "0x04099019246c7a54e581dcb68d0d78969a76a8753c588a62f2e7f0e1c28f88e574d" \ - "ae7d9cd6db9cdc6318714b2aa2945e1be8e1a5cf0f1f876b0c0627f8c6bfdb4" - -transaction_users['D_USER'] = dict() -transaction_users['D_USER']['password'] = "qwerty" -transaction_users['D_USER']['passphrase'] = "weekend unable empty crime blind rhythm crunch answer travel coast silly like" -transaction_users['D_USER']['username'] = "Warped Murky Avocet" -transaction_users['D_USER']['address'] = "616874dc6cc2810cdc930dea26496fcf217d58ca" -transaction_users['D_USER']['public_key'] = "0x049191be3d0b0258732a4c3bd66d2e68cf7e536a0a975fd501a3265cba9ff5f7c78" \ - "f51345417e42f56e16926b685f99448ac05b9608b0d42cf65ee58853f104696" - -transaction_users['E_USER'] = dict() -transaction_users['E_USER']['password'] = "newuniquepassword12" -transaction_users['E_USER']['passphrase'] = "sea ill guard bounce gesture tomato walnut fitness plastic affair oven transfer" -transaction_users['E_USER']['username'] = "Chartreuse Comfortable Spadefoot" -transaction_users['E_USER']['address'] = "f7cb60839c0de25e37be0391c33bb34a8f0f8414" -transaction_users['E_USER']['public_key'] = "0x04d27bda317be9b3d943db5e098b3ab1c19ac8156d706f1d237c000e2ea0f553eec" \ - "09499522623dc686f5d0a3233c7f1f9724849127cedb5511945e12ca3b4d297" - -transaction_users['F_USER'] = dict() -transaction_users['F_USER']['password'] = "newuniquepassword12" -transaction_users['F_USER']['passphrase'] = "style case lazy pole general section dawn royal slice evoke crowd boat" -transaction_users['F_USER']['username'] = "Happygolucky Zigzag Stoat" -transaction_users['F_USER']['address'] = "a94a2a7584620677d290549a96046e59a18f09ef" -transaction_users['F_USER']['public_key'] = "0x042edc34542022b2017a8e9bbdbd7535fb4bdf98a5474edbe7179c4782dbe902704" \ - "380f4d7146a2f8e432dfb6fead596057782ae96fba82c3541cc67884e3a9224" - -transaction_users['G_USER'] = dict() -transaction_users['G_USER']['password'] = "qwerty" -transaction_users['G_USER']['passphrase'] = "load other time gadget fury sudden gossip hurry entry curtain jaguar inflict" -transaction_users['G_USER']['username'] = "Unsung Yearly Amethystgemclam" -transaction_users['G_USER']['address'] = "876c331d60aedd175ca78cb6a50eaeb9681a1868" -transaction_users['G_USER']['public_key'] = "0x04e70078c562b4a65f08e57cc336e8cbd66d27f411324158807654ae82ef16d8f0f" \ - "583454ba2c98a2451a9a990ec3efc874dd845d8ba72a49fb9fb0454d0bd6bab" - -transaction_users['H_USER'] = dict() -transaction_users['H_USER']['password'] = 'qwerty' -transaction_users['H_USER']['passphrase'] = "age three camp tip jump radio copper merry mention top panther motor" -transaction_users['H_USER']['username'] = "Lawngreen Rich Silverfox" -transaction_users['H_USER']['address'] = "762d87f77c88048281ddad78501fcfe0700f08ce" -transaction_users['H_USER']['public_key'] = "0x040ff9ff7c1fd9d325a0762bf36fdd6efce0190a02bba4367afc99f0313404a5a3ed" \ - "f74aa54ebf7221561e0d83728b4f7934b28809d69ba9e7434fc2fe9d4b9bd4" - -transaction_users_wallet = dict() -transaction_users_wallet['A_USER'] = dict() -transaction_users_wallet['A_USER']['password'] = "new_unique_password" -transaction_users_wallet['A_USER']['passphrase'] = "six runway asthma blur secret rebuild parent logic horror decline rib buyer" -transaction_users_wallet['A_USER']['username'] = "Leading Practical Paradiseflycatcher" -transaction_users_wallet['A_USER']['address'] = "0887afe0ee3e1b195f596350bb04bba034514af9" -transaction_users_wallet['A_USER']['public_key'] = "0x04026c389530328076016ebcdc3f8e558e2e9e6e6c015adb1a668783ca1b11" \ - "34260afe392967434cdbdf616138cbadcd009642edd62a5275ad0bc2ff460849fb74" - -transaction_users_wallet['B_USER'] = dict() -transaction_users_wallet['B_USER']['password'] = "new_unique_password" -transaction_users_wallet['B_USER']['passphrase'] = "shrug dring breeze marcy install net reopen uniform atom guilt sadness elite" -transaction_users_wallet['B_USER']['username'] = "Blank Bowed Chrysomelid" -transaction_users_wallet['B_USER']['address'] = "13b36abe0be8fa607ab5ec755f2719e545705490" -transaction_users_wallet['B_USER']['public_key'] = "0x04e9299758f894d45d2515b254799cdfbe823d387f46896a52ec12e8f37f771" \ - "fba9fb49a44f93198e581651be71132f68d6d7cca6d1d9c801edbbaab58641b5dc4" - -transaction_users_wallet['C_USER'] = dict() -transaction_users_wallet['C_USER']['password'] = "new_unique_password" -transaction_users_wallet['C_USER']['passphrase'] = "purchase ensure mistake crystal person similar shaft family shield clog risk market" -transaction_users_wallet['C_USER']['username'] = "Official Scratchy Bernesemountaindog" -transaction_users_wallet['C_USER']['address'] = "8dce052ccda2f6f6b555759cee6957e04a6ddf5b" -transaction_users_wallet['C_USER']['public_key'] = "0x04fd6db8ae245ca351cb6e24bea29df6d81cb35b7f7a91c68a8b5a0c49da444" \ - "8cdb356cc9c24b9d813094a86e1fb2dc2bf5252a3f117194bbb3bb29f4befbe898c" - -transaction_users_wallet['D_USER'] = dict() -transaction_users_wallet['D_USER']['password'] = "new_unique_password" -transaction_users_wallet['D_USER']['passphrase'] = "night grit town donate length zoo meat collect vapor brush topic check" -transaction_users_wallet['D_USER']['username'] = "Joyful Complete Crow" -transaction_users_wallet['D_USER']['address'] = "1fc705a336a0a2e48eb3c953ce577e591f2767ad" -transaction_users_wallet['D_USER']['public_key'] = "0x04b785a16763a8f9a139ea53c2da4dc976a40e3ca288aaff52e3e9527d19150" \ - "21ea6dda3083f37bc5c6a160028faf16dc9d7f684ed59473830fe422700abf826ea" - - -group_chat_users = dict() -group_chat_users['A_USER'] = dict() -group_chat_users['A_USER']['password'] = "qwerty" -group_chat_users['A_USER']['passphrase'] = "thank fruit brisk pond opera census grid husband claw sight chunk arena" -group_chat_users['A_USER']['username'] = "Astonishing Low Johndory" -group_chat_users['A_USER']['public_key'] = "0x047c67e698c27527e4cac58a3e67103f4fe59849287171464088b321f1c323ac4bfbb54" \ - "c87a3f27179be7404cb840f90f92edddb36bcf1bc3d462c79a2d73566f7" - -group_chat_users['B_USER'] = dict() -group_chat_users['B_USER']['password'] = "qwerty" -group_chat_users['B_USER']['passphrase'] = "label ill slender audit atom love vote snap edit program climb beyond" -group_chat_users['B_USER']['username'] = "Milky Major Albatross" -group_chat_users['B_USER']['public_key'] = "0x04b5457d527c337083a2d0c0295492239ff98f577875049d4d09cefbe5af3b76c18c48d" \ - "312aafb788b2b9922249819932bbc570a738e5c3b49d5e84ebcdec247cb" - -group_chat_users['C_USER'] = dict() -group_chat_users['C_USER']['password'] = "qwerty" -group_chat_users['C_USER']['passphrase'] = "you salmon ticket antique spray panther flee neck scale mad trial exile" -group_chat_users['C_USER']['username'] = "Optimistic Gigantic Bagworm" -group_chat_users['C_USER']['public_key'] = "0x04dcdb5cac266328c41bdb0e33a266544a4ac1f2655a68170e5fe4452baff1a413a1d40" \ - "3dba7e295445505ee55ea03ee99cb7d26bee05e6b486a9bdaaf6be73a0b" - -common_password = 'qwerty' - -bootnode_address = "enode://a8a97f126f5e3a340cb4db28a1187c325290ec08b2c9a6b1f19845ac86c46f9fac2ba13328822590" \ - "fd3de3acb09cc38b5a05272e583a2365ad1fa67f66c55b34@167.99.210.203:30404" -mailserver_address = "enode://531e252ec966b7e83f5538c19bf1cde7381cc7949026a6e499b6e998e695751aadf26d4c98d5a4eab" \ - "fb7cefd31c3c88d600a775f14ed5781520a88ecd25da3c6:status-offline-inbox@35.225.227.79:30504" - -camera_access_error_text = "To grant the required camera permission, please go to your system settings " \ - "and make sure that Status > Camera is selected." +emojis = {'thumbs-up': 2, 'thumbs-down': 3, 'love': 1, 'laugh': 4, 'angry': 6, 'sad': 5} -connection_not_secure_text = "Connection is not secure! " \ - "Do not sign transactions or send personal data on this site." -connection_is_secure_text = "Connection is secure. Make sure you really trust this site " \ - "before signing transactions or entering personal data." +with open(os.sep.join(__file__.split(os.sep)[:-1]) + '/../../../translations/en.json') as json_file: + transl = json.load(json_file) diff --git a/test/appium/tests/activity_center/test_activity_center.py b/test/appium/tests/activity_center/test_activity_center.py new file mode 100644 index 00000000000..32706489550 --- /dev/null +++ b/test/appium/tests/activity_center/test_activity_center.py @@ -0,0 +1,507 @@ +import pytest +from appium.webdriver.connectiontype import ConnectionType +from selenium.common.exceptions import TimeoutException + +from tests import marks, run_in_parallel +from tests.base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from tests.users import transaction_senders +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="three_2") +@marks.nightly +class TestActivityCenterContactRequestMultipleDevicePR(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + self.loop.run_until_complete(run_in_parallel(((self.device_1.create_user, {'enable_notifications': True}), + (self.device_2.create_user,)))) + self.homes = self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.profile_1, self.profile_2 = self.home_1.get_profile_view(), self.home_2.get_profile_view() + self.public_key_1 = self.home_1.get_public_key() + self.profile_link_2 = self.home_2.get_link_to_profile() + self.home_2.close_share_tab_button.click_until_absense_of_element(self.home_2.link_to_profile_button) + [home.navigate_to_chats_view() for home in self.homes] + + @marks.testrail_id(702850) + def test_activity_center_contact_request_decline(self): + app_package = self.device_1.driver.current_package + self.device_1.put_app_to_background() + self.device_2.just_fyi('Device2 sends a contact request to Device1 via Paste button and check user details') + self.home_2.new_chat_button.click_until_presence_of_element(self.home_2.add_a_contact_chat_bottom_sheet_button) + self.home_2.add_a_contact_chat_bottom_sheet_button.click() + self.home_2.driver.set_clipboard_text(self.public_key_1) + self.home_2.element_by_translation_id("paste").click() + self.home_2.element_by_translation_id("user-found").wait_for_visibility_of_element(10) + if self.home_2.user_name_text.is_element_displayed(30): + text_name = self.home_2.user_name_text.text + if text_name != self.username_1 and text_name != "%s...%s" % ( + self.public_key_1[:3], self.public_key_1[-6:]): + self.errors.append( + self.home_2, + "Neither username nor public key is shown on 'Add contact' page after entering valid public key") + else: + self.errors.append(self.home_2, "User is not found on 'Add contact' page after entering valid public key") + chat = self.home_2.get_chat_view() + + chat.view_profile_new_contact_button.click_until_presence_of_element(chat.profile_send_contact_request_button) + chat.profile_send_contact_request_button.click() + chat.contact_request_message_input.send_keys("hi") + chat.confirm_send_contact_request_button.click() + chat.close_button.click_until_absense_of_element(chat.close_button) + self.home_2.navigate_back_to_home_view() + + self.device_1.just_fyi("Device 1: check there is no PN when receiving new message to activity centre") + self.device_1.open_notification_bar() + if self.home_1.element_by_text_part("Please add me to your contacts").is_element_displayed(): + self.errors.append(self.home_1, + "Push notification with text was received for new message in activity centre") + self.device_1.click_system_back_button() + self.device_1.driver.activate_app(app_package) + self.device_1.wait_for_application_to_be_running(app_package) + + self.device_1.just_fyi('Device1 verifies pending contact request') + self.home_1.contacts_tab.click_until_presence_of_element(self.home_1.pending_contact_request_text) + for indicator in (self.home_1.notifications_unread_badge, self.home_1.contact_new_badge): + if not indicator.is_element_displayed(): + self.errors.append( + self.home_1, "Unread indicator on contacts tab or on activity center is not shown for incoming CR!") + if self.home_1.pending_contact_request_text.text != '1': + self.errors.append(self.home_1, "The amount of contact requests is not shown for incoming CR!") + + self.device_1.just_fyi('Device1 declines pending contact request') + self.home_1.handle_contact_request(username=self.username_2, action='decline') + for indicator in (self.home_1.notifications_unread_badge, self.home_1.contact_new_badge, + self.home_1.pending_contact_request_text): + if indicator.is_element_displayed(): + self.errors.append( + self.home_1, + "Unread indicator on contacts tab or on activity center is shown after declining contact request!") + + self.device_1.just_fyi("Check that it is still pending contact after declining on sender device") + self.home_2.navigate_back_to_home_view() + self.home_2.open_activity_center_button.click() + self.home_2.activity_unread_filter_button.click() + element = self.home_2.get_activity_center_element_by_text('connect') + if element.title.text != 'Contact request' or element.context_tag_text != self.username_1 \ + or not element.pending_status_tag.is_element_displayed(): + self.errors.append( + self.home_2, "Pending contact request is not shown on unread notification element on Activity center!") + self.home_2.close_activity_centre.click() + + self.errors.verify_no_errors() + + @marks.testrail_id(702851) + def test_activity_center_contact_request_accept_swipe_mark_all_as_read(self): + self.device_2.just_fyi('Creating a new user on Device2') + self.home_2.reopen_app(sign_in=False) + self.device_2.create_user(first_user=False) + new_username = self.home_2.get_username() + + self.device_2.just_fyi('Device2 sends a contact request to Device1 via Paste button and check user details') + self.home_2.chats_tab.click() + self.home_2.new_chat_button.click_until_presence_of_element(self.home_2.add_a_contact_chat_bottom_sheet_button) + self.home_2.add_a_contact_chat_bottom_sheet_button.click() + self.home_2.driver.set_clipboard_text(self.public_key_1) + self.home_2.element_by_translation_id("paste").click() + self.home_2.element_by_translation_id("user-found").wait_for_visibility_of_element(10) + chat = self.home_2.get_chat_view() + chat.view_profile_new_contact_button.click_until_presence_of_element(chat.profile_send_contact_request_button) + chat.profile_send_contact_request_button.click() + chat.contact_request_message_input.send_keys("hi") + chat.confirm_send_contact_request_button.click() + chat.close_button.click_until_absense_of_element(chat.close_button) + + self.device_1.just_fyi('Device1 accepts pending contact request by swiping') + self.home_1.chats_tab.click() + self.home_1.notifications_unread_badge.wait_for_visibility_of_element(30) + if self.home_1.toast_content_element.is_element_displayed(10): + self.home_1.toast_content_element.wait_for_invisibility_of_element() + self.home_1.open_activity_center_button.click() + + self.home_1.just_fyi("Mark all as read") + cr_element = self.home_1.get_element_from_activity_center_view(new_username) + self.home_1.more_options_activity_button.click() + self.home_1.mark_all_read_activity_button.click() + if cr_element.is_element_displayed(): + self.errors.append( + self.home_1, "Contact request is still shown in activity centre after marking all messages as read!") + + self.home_1.just_fyi("Check that can accept contact request from read notifications") + self.home_1.activity_unread_filter_button.click() + cr_element.message_body.swipe_right_on_element() + self.home_1.activity_notification_swipe_button.click_inside_element_by_coordinate(rel_x=0.5, rel_y=0.5) + self.home_1.close_activity_centre.click() + self.home_1.contacts_tab.click() + if not self.home_1.contact_details_row(username=new_username).is_element_displayed(20): + self.errors.append(self.home_1, + "Contact was not added to contact list after accepting contact request (as receiver)") + + self.device_2.just_fyi('Device1 check that contact appeared in contact list mutually') + self.home_2.navigate_to_chats_view() + self.home_2.contacts_tab.click() + if not self.home_2.contact_details_row(username=self.username_1).is_element_displayed(20): + self.errors.append(self.home_2, + "Contact was not added to contact list after accepting contact request (as sender)") + + self.errors.verify_no_errors() + + @marks.testrail_id(702777) + def test_add_contact_field_validation(self): + public_key_2 = self.profile_link_2.split("#")[-1] + + def _device_1_creates_user(): + self.home_1.just_fyi("Device 1 creates a new user") + self.home_1.reopen_app(sign_in=False) + self.device_1.create_user(first_user=False) + return self.home_1.get_username() + + def _device_2_sign_in(): + self.home_2.just_fyi("Device 2 sign in, user name is " + self.username_2) + self.home_2.reopen_app(sign_in=False) + self.device_2.sign_in(user_name=self.username_2) + + new_username_1, _ = self.loop.run_until_complete(run_in_parallel(((_device_1_creates_user, {}), + (_device_2_sign_in, {})))) + + self.device_1.just_fyi('Device1 sends a contact request to Device2 using his profile link') + self.home_1.chats_tab.click() + self.home_1.new_chat_button.click_until_presence_of_element(self.home_1.add_a_contact_chat_bottom_sheet_button) + self.home_1.add_a_contact_chat_bottom_sheet_button.click() + self.home_1.driver.set_clipboard_text(self.profile_link_2) + self.home_1.element_by_translation_id("paste").click() + self.home_1.element_by_translation_id("user-found").wait_for_visibility_of_element(10) + if self.home_1.user_name_text.is_element_displayed(30): + text_name = self.home_1.user_name_text.text + if text_name != self.username_2 and text_name != "%s...%s" % (public_key_2[:3], public_key_2[-6:]): + self.errors.append( + self.home_1, + "Neither username nor public key is shown on 'Add contact' page after entering valid profile link") + else: + self.errors.append(self.home_1, "User is not found on 'Add contact' page after entering valid public key") + chat_1 = self.home_1.get_chat_view() + chat_1.view_profile_new_contact_button.click_until_presence_of_element( + chat_1.profile_send_contact_request_button) + chat_1.profile_send_contact_request_button.click() + chat_1.contact_request_message_input.send_keys("hi") + chat_1.confirm_send_contact_request_button.click() + chat_1.close_button.click_until_absense_of_element(chat_1.close_button) + + self.home_2.just_fyi("Device 2 accepts contact request") + self.home_2.handle_contact_request(new_username_1) + + self.home_1.just_fyi("Device 1 checks that can find already added contact using public key") + self.home_1.navigate_back_to_home_view() + self.home_1.new_chat_button.click_until_presence_of_element(self.home_1.add_a_contact_chat_bottom_sheet_button) + self.home_1.add_a_contact_chat_bottom_sheet_button.click() + self.home_1.driver.set_clipboard_text(public_key_2) + self.home_1.element_by_translation_id("paste").click() + self.home_1.element_by_translation_id("user-found").wait_for_visibility_of_element(10) + if self.home_1.user_name_text.is_element_displayed(30): + text_name = self.home_1.user_name_text.text + if text_name != self.username_2 and text_name != "%s...%s" % (public_key_2[:3], public_key_2[-6:]): + self.errors.append( + self.home_1, + "Neither username nor public key is shown on 'Add contact' page after entering valid public key") + else: + self.errors.append(self.home_1, "User is not found on 'Add contact' page after entering valid public key") + + self.home_2.just_fyi("Device 2 gets sync code") + self.home_2.navigate_back_to_home_view() + self.home_2.profile_button.click_until_presence_of_element(self.profile_2.default_username_text) + sync_code = self.profile_2.get_sync_code() + + invalid_values = [self.public_key_1[:-1], "random string 123", + '0x' + transaction_senders['ETH_ADI_STT_3']['address'], sync_code] + for value in invalid_values: + self.home_1.just_fyi("Device 1 checks adding a contact with invalid value \"%s\"" % value) + chat_1.public_key_edit_box.clear() + self.home_1.element_by_translation_id("invalid-ens-or-key").wait_for_invisibility_of_element() + self.home_1.driver.set_clipboard_text(value) + self.home_1.element_by_translation_id("paste").click() + try: + self.home_1.element_by_translation_id("invalid-ens-or-key").wait_for_visibility_of_element() + except TimeoutException: + self.errors.append(self.home_1, "Error message is not shown for value \"%s\"" % value) + + self.errors.verify_no_errors() + + +@pytest.mark.xdist_group(name="four_2") +@marks.nightly +class TestActivityMultipleDevicePR(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + self.loop.run_until_complete( + run_in_parallel(((self.device_1.create_user,), + (self.device_2.create_user,)))) + self.homes = self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.profile_1, self.profile_2 = self.home_1.get_profile_view(), self.home_2.get_profile_view() + self.public_key_2 = self.home_2.get_public_key() + + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(self.public_key_2) + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + self.text_message = 'hello' + + self.home_1.just_fyi("Open community to message") + self.home_1.communities_tab.click() + self.community_name = "open community" + self.channel_name = 'general' + self.home_1.create_community(community_type="open") + self.channel_1 = self.home_1.get_to_community_channel_from_home(self.community_name) + self.channel_1.send_message(self.text_message) + + self.community_1, self.community_2 = self.home_1.get_community_view(), self.home_2.get_community_view() + self.community_1.invite_to_community(self.community_name, self.username_2) + self.home_1.get_to_community_channel_from_home(self.community_name) + + self.chat_2 = self.home_2.get_chat(self.username_1).click() + self.community_2.join_community() + self.channel_2 = self.community_2.get_channel(self.channel_name).click() + + @marks.testrail_id(702936) + @marks.skip("The feature is disabled") + def test_navigation_jump_to(self): + self.community_1.just_fyi("Check Jump to screen and redirect on tap") + self.community_1.click_on_floating_jump_to() + for card in (self.community_name, self.username_2): + if not self.community_1.element_by_text_part(card).is_element_displayed(20): + self.errors.append(self.community_1, "Card %s is not shown on Jump to screen!" % card) + self.community_1.element_by_translation_id("community-channel").click() + if not self.channel_1.chat_element_by_text(self.text_message).is_element_displayed(20): + self.errors.append(self.channel_1, + "User was not redirected to community channel after tapping on community channel card!") + self.channel_1.click_system_back_button() + self.community_1.click_on_floating_jump_to() + self.community_1.element_by_text_part(self.username_2).click() + if not self.chat_1.chat_element_by_text(self.one_to_one_message).is_element_displayed(20): + self.errors.append(self.chat_1, "User was not redirected to 1-1 chat after tapping card!") + + self.errors.verify_no_errors() + + @marks.testrail_id(702947) + def test_activity_center_reply_read_unread_delete_filter_swipe(self): + message_to_reply, reply_to_message_from_sender = 'something to reply to', 'this is a reply' + self.home_1.navigate_to_communities_view() + self.home_1.get_chat(self.community_name, community=True).click() + self.community_1.get_channel(self.channel_name).click() + self.channel_1.send_message(message_to_reply) + + self.home_1.navigate_to_communities_view() + self.channel_2.chat_element_by_text(message_to_reply).wait_for_visibility_of_element(120) + self.channel_2.quote_message(message_to_reply) + self.channel_2.send_message(reply_to_message_from_sender) + + self.home_1.just_fyi("Checking unread indicators") + self.home_1.notifications_unread_badge.wait_for_visibility_of_element(120) + community_element_1 = self.home_1.get_chat(self.community_name, community=True) + for unread_counter in community_element_1.new_messages_counter, self.home_1.communities_tab.counter: + if not unread_counter.is_element_displayed(60): + self.errors.append(self.home_1, 'New message counter badge is not shown!') + if int(unread_counter.text) != 1: + self.errors.append(self.home_1, 'New message counter badge is not 1, it is %s!' % unread_counter.text) + + self.home_1.just_fyi("Checking reply attributes in activity center") + self.home_1.open_activity_center_button.click() + reply_element = self.home_1.get_element_from_activity_center_view(reply_to_message_from_sender) + if reply_element.title.text != 'Reply': + self.errors.append(self.home_1, "Expected title is not shown, '%s' is instead!" % reply_element.title) + if not reply_element.unread_indicator.is_element_displayed(): + self.errors.append(self.home_1, "No unread dot is shown on activity center element!") + + self.home_2.chats_tab.is_element_displayed() # just saving device 2 session from expiration + + self.home_1.just_fyi("Swiping to 'Replies' on activity center and check unread there") + self.home_1.mention_activity_tab_button.click() + if reply_element.is_element_displayed(2): + self.errors.append(self.home_1, "Filter on mentions is not working in Activity centre!") + self.home_1.reply_activity_tab_button.click() + if not self.home_1.reply_activity_tab_button.counter.is_element_displayed(2): + self.errors.append(self.home_1, "No unread dot is shown on activity center tab element!") + if not reply_element.is_element_displayed(): + self.errors.append(self.home_1, "Filter on replies tab is not working in Activity centre!") + + self.home_2.chats_tab.is_element_displayed() # just saving device 2 session from expiration + + self.home_1.just_fyi("Mark it as read and check filter") + reply_element.title.swipe_right_on_element(width_percentage=6) + self.home_1.activity_notification_swipe_button.click() + if reply_element.is_element_displayed(2): + self.errors.append(self.home_1, "Message is not marked as read!") + self.home_1.activity_unread_filter_button.click() + if not reply_element.is_element_displayed(2): + self.errors.append(self.home_1, "Read filter is not displayed read message!") + + self.home_2.chats_tab.is_element_displayed() # just saving device 2 session from expiration + + self.home_1.just_fyi("Mark it as unread and check filter via right swipe") + reply_element.title.swipe_right_on_element(width_percentage=6) + self.home_1.activity_notification_swipe_button.click() + if not reply_element.unread_indicator.is_element_displayed(): + self.errors.append(self.home_1, + "No unread dot is shown on activity center element after marking it as unread!") + + self.home_1.just_fyi("Tap on it and check it marked as read") + reply_element.click() + if not self.channel_1.chat_element_by_text(reply_to_message_from_sender).is_element_displayed(): + self.errors.append(self.home_1, "Was not redirected to chat after tapping on reply!") + self.home_1.navigate_to_communities_view() + if self.home_1.notifications_unread_badge.is_element_displayed(): + self.errors.append(self.home_1, + "Notification was not marked as read after opening it in community channel!") + + self.home_2.chats_tab.is_element_displayed() # just saving device 2 session from expiration + + self.home_1.just_fyi("Delete it from unread via left swipe") + self.home_1.open_activity_center_button.click() + reply_element.swipe_left_on_element() + self.home_1.activity_notification_swipe_button.click() + if reply_element.is_element_displayed(): + self.errors.append(self.home_1, "Reply is still shown after removing from activity centre!") + + self.errors.verify_no_errors() + + +@pytest.mark.xdist_group(name="six_2") +@marks.nightly +class TestActivityMultipleDevicePRTwo(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + self.loop.run_until_complete( + run_in_parallel(((self.device_1.create_user,), + (self.device_2.create_user,)))) + self.homes = self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.profile_1, self.profile_2 = self.home_1.get_profile_view(), self.home_2.get_profile_view() + self.public_key_2 = self.home_2.get_public_key() + + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(self.public_key_2) + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + self.text_message = 'hello' + + self.home_1.just_fyi("Open community to message") + self.home_1.communities_tab.click() + self.community_name = "open community" + self.channel_name = 'general' + self.home_1.create_community(community_type="open") + self.channel_1 = self.home_1.get_to_community_channel_from_home(self.community_name) + self.channel_1.send_message(self.text_message) + + self.community_1, self.community_2 = self.home_1.get_community_view(), self.home_2.get_community_view() + self.community_1.invite_to_community(self.community_name, self.username_2) + self.home_1.get_to_community_channel_from_home(self.community_name) + + self.chat_2 = self.home_2.get_chat(self.username_1).click() + self.community_2.join_community() + self.channel_2 = self.community_2.get_channel(self.channel_name).click() + self.channel_2.chat_message_input.wait_for_visibility_of_element(20) + + @marks.testrail_id(702957) + def test_activity_center_mentions(self): + self.home_1.navigate_to_communities_view() + + self.device_2.just_fyi("Invited member sends a message with a mention") + self.channel_2.mention_user(self.username_1) + self.channel_2.send_message_button.click() + + self.home_1.just_fyi("Checking unread indicators") + self.home_1.notifications_unread_badge.wait_for_visibility_of_element(120) + community_element_1 = self.home_1.get_chat(self.community_name, community=True) + for unread_counter in community_element_1.new_messages_counter, self.home_1.communities_tab.counter: + if not unread_counter.is_element_displayed(60): + self.errors.append(self.home_1, 'New message counter badge is not shown while mentioned!') + if int(unread_counter.text) != 1: + self.errors.append(self.home_1, 'New message counter badge is not 1, it is %s!' % unread_counter.text) + + self.home_1.just_fyi("Checking mention attributes in activity center") + self.home_1.open_activity_center_button.click() + mention_element = self.home_1.get_element_from_activity_center_view('@%s' % self.username_1) + if mention_element.title.text != 'Mention': + self.errors.append(self.home_1, "Expected title is not shown, '%s' is instead!" % mention_element.title) + if not mention_element.unread_indicator.is_element_displayed(): + self.errors.append(self.home_1, "No unread dot is shown on activity center element (mention)!") + if mention_element.message_body.text != '@%s' % self.username_1: + self.errors.append( + self.home_1, "Mention body in activity center does not match expected, it is %s!" + % mention_element.message_body.text) + + self.home_1.just_fyi("Tap on it and check redirect to channel") + mention_element.click() + if not self.channel_1.chat_element_by_text(self.username_1).is_element_displayed(): + self.errors.append(self.home_1, "Was not redirected to chat after tapping on mention!") + self.errors.verify_no_errors() + + @marks.testrail_id(702958) + def test_activity_center_admin_notification_accept_swipe(self): + self.home_2.just_fyi("Clearing history") + self.home_2.navigate_to_chats_view() + self.home_2.clear_chat_long_press(self.username_1) + + [home.navigate_back_to_home_view() for home in (self.home_1, self.home_2)] + + self.home_1.just_fyi("Open community to message") + self.home_1.communities_tab.click() + community_name = 'closed community' + self.channel_name = "dogs" + self.home_1.create_community(community_type="closed") + self.home_1.reopen_app(user_name=self.username_1) + self.community_1.invite_to_community(community_name, self.username_2) + + self.home_2.just_fyi("Request access to community") + self.home_2.navigate_to_chats_view() + self.chat_2 = self.home_2.get_chat(self.username_1).click() + self.community_2.join_community(open_community=False) + for home in self.home_1, self.home_2: + home.navigate_to_communities_view() + + self.home_1.just_fyi("Checking unread indicators") + try: + self.home_1.notifications_unread_badge.wait_for_visibility_of_element(120) + except TimeoutException: + self.errors.append(self.home_1, "Unread indicator is not shown in notifications") + self.home_1.open_activity_center_button.click() + reply_element = self.home_1.get_element_from_activity_center_view(self.username_2) + if reply_element.title.text != 'Join request': + self.errors.append(self.home_1, "Expected title is not shown, '%s' is instead!" % reply_element.title) + if not reply_element.unread_indicator.is_element_displayed(): + self.errors.append(self.home_1, "No unread dot is shown on activity center element!") + reply_element.title.swipe_right_on_element(width_percentage=2.5) + self.home_1.activity_notification_swipe_button.click() + self.home_1.close_activity_centre.click() + + self.home_2.just_fyi("Checking that community appeared on the list") + if not self.home_2.element_by_text_part(community_name).is_element_displayed(30): + self.errors.append( + self.home_2, "Community is not appeared in the list after accepting admin request from activity centre") + self.errors.verify_no_errors() + + @marks.testrail_id(741809) + def test_activity_centre_contact_request_receiver_offline(self): + self.device_2.navigate_back_to_home_view() + + self.device_1.just_fyi("Device 1: Create a new user") + self.device_1.reopen_app(sign_in=False) + self.device_1.create_user(first_user=False) + new_username_1 = self.home_1.get_username() + self.device_1.chats_tab.click() + + self.device_2.just_fyi("Device 2: Turn off network connection") + self.device_2.driver.set_network_connection(ConnectionType.AIRPLANE_MODE) + + self.device_1.just_fyi("Device 1: Send a contact request") + self.home_1.add_contact(self.public_key_2) + + self.device_2.just_fyi("Device 2: Turn on network connection and check that the contact request is received") + self.device_2.driver.set_network_connection(ConnectionType.ALL_NETWORK_ON) + self.home_2.handle_contact_request(username=new_username_1, notifications_count=3) diff --git a/test/appium/tests/atomic/account_management/test_create_account.py b/test/appium/tests/atomic/account_management/test_create_account.py deleted file mode 100644 index 3c1031412f5..00000000000 --- a/test/appium/tests/atomic/account_management/test_create_account.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest - -from tests import marks, common_password, get_current_time -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.all -@marks.account -class TestCreateAccount(SingleDeviceTestCase): - - @marks.testrail_id(758) - @marks.smoke_1 - def test_create_account(self): - if not self.test_fairy_warning_is_shown: - self.errors.append('TestFairy warning is not shown') - sign_in = SignInView(self.driver) - if not sign_in.i_have_account_button.is_element_displayed(): - self.errors.append("'I have an account' button is not displayed") - sign_in.create_account_button.click() - sign_in.password_input.set_value(common_password) - sign_in.next_button.click() - sign_in.confirm_password_input.set_value(common_password) - sign_in.next_button.click() - - sign_in.element_by_text_part('Display name').wait_for_element(30) - sign_in.name_input.send_keys('user_%s' % get_current_time()) - - sign_in.next_button.click() - - @marks.testrail_id(1433) - def test_switch_users_and_add_new_account(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - public_key = sign_in.get_public_key() - profile = sign_in.get_profile_view() - profile.logout() - if sign_in.ok_button.is_element_displayed(): - sign_in.ok_button.click() - sign_in.other_accounts_button.click() - sign_in.create_user() - if sign_in.get_public_key() == public_key: - pytest.fail('New account was not created') - - @marks.testrail_id(3787) - @marks.smoke_1 - def test_home_view(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - if not home.welcome_image.is_element_displayed(): - self.errors.append('Welcome image is not shown') - for text in ['Welcome to Status', - 'Tap the plus (+) button to get started']: - if not home.element_by_text(text).is_element_displayed(): - self.errors.append("'%s' text is not shown" % text) - home.profile_button.click() - home.home_button.click() - text = ('There are no recent Chats or DApps here yet. ' - 'Tap the “Plus” button to see the list of Dapps or discover people to chat with.') - if not home.element_by_text(text).is_element_displayed(): - self.errors.append("'%s' text is not shown" % text) - self.verify_no_errors() - - @marks.testrail_id(844) - def test_create_account_short_and_mismatch_password(self): - sign_in = SignInView(self.driver) - sign_in.create_account_button.click() - sign_in.password_input.set_value('12345') - - mismatch_error = "Passwords don't match" - - sign_in.next_button.click() - if sign_in.confirm_password_input.is_element_displayed(): - self.errors.append('Next button is clickable when password is less then 6 symbols') - - sign_in.password_input.set_value('123456') - sign_in.next_button.click() - sign_in.confirm_password_input.set_value('1234567') - sign_in.next_button.click() - - if not sign_in.find_text_part(mismatch_error): - self.errors.append("'%s' is not shown") - self.verify_no_errors() - - @marks.testrail_id(3767) - def test_password_in_logcat_creating_account(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - sign_in.check_no_values_in_logcat(password=common_password) diff --git a/test/appium/tests/atomic/account_management/test_profile.py b/test/appium/tests/atomic/account_management/test_profile.py deleted file mode 100644 index 30bf4117f40..00000000000 --- a/test/appium/tests/atomic/account_management/test_profile.py +++ /dev/null @@ -1,302 +0,0 @@ -import pytest - -from tests import marks, group_chat_users, basic_user, bootnode_address, mailserver_address, camera_access_error_text -from tests.base_test_case import SingleDeviceTestCase, MultipleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.all -@marks.account -class TestProfileSingleDevice(SingleDeviceTestCase): - - @marks.testrail_id(760) - @marks.smoke_1 - def test_set_profile_picture(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.edit_profile_picture(file_name='sauce_logo.png') - profile_view.home_button.click() - sign_in_view.profile_button.click() - profile_view.swipe_down() - if not profile_view.profile_picture.is_element_image_equals_template(): - pytest.fail('Profile picture was not updated') - - @marks.testrail_id(1403) - @marks.smoke_1 - def test_share_contact_code_and_wallet_address(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.share_my_contact_key_button.click() - public_key = profile_view.public_key_text.text - profile_view.share_button.click() - profile_view.share_via_messenger() - if not profile_view.element_by_text_part(public_key).is_element_present(): - self.errors.append("Can't share public key") - profile_view.click_system_back_button() - profile_view.cross_icon.click() - wallet = profile_view.wallet_button.click() - wallet.set_up_wallet() - request = wallet.receive_transaction_button.click() - address = wallet.address_text.text - request.share_button.click() - wallet.share_via_messenger() - if not wallet.element_by_text_part(address).is_element_present(): - self.errors.append("Can't share address") - self.verify_no_errors() - - @marks.testrail_id(3704) - @marks.smoke_1 - def test_copy_contact_code_and_wallet_address(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.share_my_contact_key_button.click() - public_key = profile_view.public_key_text.text - profile_view.public_key_text.long_press_element() - profile_view.copy_text() - profile_view.cross_icon.click() - home = profile_view.home_button.click() - chat = home.add_contact(group_chat_users['A_USER']['public_key']) - chat.chat_message_input.click() - chat.paste_text() - input_text = chat.chat_message_input.text - if input_text not in public_key or len(input_text) < 1: - self.errors.append('Public key was not copied') - chat.chat_message_input.clear() - chat.get_back_to_home_view() - - wallet = home.wallet_button.click() - wallet.set_up_wallet() - wallet.receive_transaction_button.click() - address = wallet.address_text.text - wallet.address_text.long_press_element() - wallet.copy_text() - wallet.get_back_to_home_view() - wallet.home_button.click() - home.get_chat_with_user(group_chat_users['A_USER']['username']).click() - chat.chat_message_input.click() - chat.paste_text() - if chat.chat_message_input.text != address: - self.errors.append('Wallet address was not copied') - self.verify_no_errors() - - @marks.testrail_id(1407) - def test_change_profile_picture_several_times(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - for file_name in ['sauce_logo.png', 'sauce_logo_red.png', 'saucelabs_sauce.png']: - profile_view.edit_profile_picture(file_name=file_name) - profile_view.swipe_down() - if not profile_view.profile_picture.is_element_image_equals_template(): - pytest.fail('Profile picture was not updated') - - @marks.testrail_id(2374) - @marks.smoke_1 - def test_backup_recovery_phrase(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - if sign_in_view.profile_button.counter.text != '1': - self.errors.append('Profile button counter is not shown') - profile_view = sign_in_view.profile_button.click() - profile_view.logout() - sign_in_view.sign_in() - if sign_in_view.profile_button.counter.text != '1': - self.errors.append('Profile button counter is not shown after relogin') - sign_in_view.profile_button.click() - profile_view.backup_recovery_phrase() - if sign_in_view.profile_button.counter.is_element_displayed(): - self.errors.append('Profile button counter is shown after seed phrase backup') - self.verify_no_errors() - - @marks.testrail_id(3721) - def test_invite_friends(self): - sign_in_view = SignInView(self.driver) - home = sign_in_view.create_user() - start_new_chat = home.plus_button.click() - start_new_chat.invite_friends_button.click() - start_new_chat.share_via_messenger() - start_new_chat.find_text_part("Get Status at http://status.im") - - @marks.testrail_id(3450) - def test_set_currency(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.set_currency('Euro (EUR)') - profile_view.get_back_to_home_view() - wallet_view = profile_view.wallet_button.click() - wallet_view.set_up_wallet() - assert '€' in wallet_view.total_amount_text.text - assert 'EUR' == wallet_view.currency_text.text - - @marks.testrail_id(3707) - def test_add_custom_network(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.add_custom_network() - sign_in_view.sign_in() - profile_view = sign_in_view.profile_button.click() - profile_view.advanced_button.click() - profile_view.find_text_part('CUSTOM_ROPSTEN') - - @marks.logcat - @marks.testrail_id(3774) - def test_logcat_backup_recovery_phrase(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.backup_recovery_phrase_button.click() - profile_view.ok_continue_button.click() - recovery_phrase = profile_view.get_recovery_phrase() - profile_view.next_button.click() - word_number = profile_view.recovery_phrase_word_number.number - profile_view.recovery_phrase_word_input.set_value(recovery_phrase[word_number]) - profile_view.next_button.click() - word_number_1 = profile_view.recovery_phrase_word_number.number - profile_view.recovery_phrase_word_input.set_value(recovery_phrase[word_number_1]) - profile_view.done_button.click() - profile_view.yes_button.click() - profile_view.check_no_values_in_logcat(passphrase1=recovery_phrase[word_number], - passphrase2=recovery_phrase[word_number_1]) - - @marks.testrail_id(3751) - def test_need_help_section(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.help_button.click() - profile_view.request_feature_button.click() - profile_view.find_full_text('Feature Requests') - profile_view.click_system_back_button() - profile_view.submit_bug_button.click() - profile_view.find_full_text('Report a problem') - profile_view.click_system_back_button() - profile_view.discard_button.click() - base_web_view = profile_view.faq_button.click() - base_web_view.open_in_webview() - profile_view.find_text_part('Questions around beta') - - @marks.testrail_id(1416) - def test_contact_profile_view(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - home_view = sign_in_view.get_home_view() - home_view.add_contact(basic_user['public_key']) - chat_view = home_view.get_chat_view() - chat_view.chat_options.click_until_presence_of_element(chat_view.view_profile_button) - chat_view.view_profile_button.click() - for text in basic_user['username'], 'In contacts', 'Send transaction', 'Send message', 'Contact code': - chat_view.find_full_text(text) - - @marks.testrail_id(2177) - def test_deny_camera_access_changing_profile_photo(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - profile = sign_in.profile_button.click() - profile.edit_button.click() - profile.edit_picture_button.click() - profile.capture_button.click() - for _ in range(2): - profile.deny_button.click() - profile.element_by_text(camera_access_error_text).wait_for_visibility_of_element(3) - profile.ok_button.click() - profile.edit_picture_button.click() - profile.capture_button.click() - profile.deny_button.wait_for_visibility_of_element(2) - - @marks.testrail_id(2178) - def test_deny_device_storage_access_changing_profile_photo(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - profile = sign_in.profile_button.click() - profile.edit_button.click() - profile.edit_picture_button.click() - profile.select_from_gallery_button.click() - profile.deny_button.click() - profile.element_by_text(camera_access_error_text, element_type='text').wait_for_visibility_of_element(3) - profile.ok_button.click() - profile.edit_picture_button.click() - profile.select_from_gallery_button.click() - profile.deny_button.wait_for_visibility_of_element(2) - - -@marks.all -@marks.account -class TestProfileMultipleDevice(MultipleDeviceTestCase): - - @marks.testrail_id(3708) - def test_custom_bootnodes(self): - self.create_drivers(2) - sign_in_1, sign_in_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1, username_2 = 'user_1', 'user_2' - home_1, home_2 = sign_in_1.create_user(username=username_1), sign_in_2.create_user(username=username_2) - public_key = home_2.get_public_key() - home_2.home_button.click() - - profile_1 = home_1.profile_button.click() - profile_1.advanced_button.click() - profile_1.bootnodes_button.click() - profile_1.plus_button.click() - profile_1.specify_name_input.set_value('test') - profile_1.bootnode_address_input.set_value(bootnode_address) - profile_1.save_button.click() - profile_1.enable_bootnodes.click() - sign_in_1.sign_in() - - chat_1 = home_1.add_contact(public_key) - message = 'test message' - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - chat_2 = home_2.get_chat_with_user(username_1).click() - chat_2.chat_element_by_text(message).wait_for_visibility_of_element() - chat_2.add_to_contacts.click() - - chat_1.get_back_to_home_view() - home_1.profile_button.click() - profile_1.advanced_button.click() - profile_1.bootnodes_button.click() - profile_1.enable_bootnodes.click() - sign_in_1.sign_in() - - home_1.get_chat_with_user(username_2).click() - message_1 = 'new message' - chat_1.chat_message_input.send_keys(message_1) - chat_1.send_message_button.click() - chat_2.chat_element_by_text(message_1).wait_for_visibility_of_element() - - @marks.testrail_id(3737) - def test_switch_mailserver(self): - self.create_drivers(2) - sign_in_1, sign_in_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1, username_2 = 'user_1', 'user_2' - home_1, home_2 = sign_in_1.create_user(username=username_1), sign_in_2.create_user(username=username_2) - public_key = home_2.get_public_key() - home_2.home_button.click() - - profile_1 = home_1.profile_button.click() - profile_1.advanced_button.click() - profile_1.mail_server_button.click() - profile_1.plus_button.click() - server_name = 'test' - profile_1.specify_name_input.set_value(server_name) - profile_1.mail_server_address_input.set_value(mailserver_address) - profile_1.save_button.click() - profile_1.mail_server_by_name(server_name).click() - profile_1.mail_server_connect_button.click() - profile_1.confirm_button.click() - sign_in_1.sign_in() - - chat_1 = home_1.add_contact(public_key) - message = 'test message' - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - chat_2 = home_2.get_chat_with_user(username_1).click() - chat_2.chat_element_by_text(message).wait_for_visibility_of_element() - message_1 = 'new message' - chat_2.chat_message_input.send_keys(message_1) - chat_2.send_message_button.click() - chat_1.chat_element_by_text(message_1).wait_for_visibility_of_element() diff --git a/test/appium/tests/atomic/account_management/test_recover.py b/test/appium/tests/atomic/account_management/test_recover.py deleted file mode 100644 index a2e5741d298..00000000000 --- a/test/appium/tests/atomic/account_management/test_recover.py +++ /dev/null @@ -1,62 +0,0 @@ -import pytest - -from tests import marks, common_password, basic_user -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.all -@marks.account -class TestRecoverAccountSingleDevice(SingleDeviceTestCase): - - @marks.testrail_id(759) - @marks.smoke_1 - def test_recover_account(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - public_key = home.get_public_key() - profile = home.get_profile_view() - profile.backup_recovery_phrase_button.click() - profile.ok_continue_button.click() - recovery_phrase = profile.get_recovery_phrase() - profile.back_button.click() - wallet = profile.wallet_button.click() - wallet.set_up_wallet() - address = wallet.get_wallet_address() - self.driver.reset() - sign_in.accept_agreements() - sign_in.recover_access(passphrase=' '.join(recovery_phrase.values()), password=common_password) - home.connection_status.wait_for_invisibility_of_element(30) - home.wallet_button.click() - wallet.set_up_wallet() - address2 = wallet.get_wallet_address() - if address2 != address: - self.errors.append('Wallet address is %s after recovery, but %s is expected' % (address2, address)) - public_key2 = wallet.get_public_key() - if public_key2 != public_key: - self.errors.append('Public key is %s after recovery, but %s is expected' % (public_key2, public_key)) - self.verify_no_errors() - - @marks.skip - @marks.testrail_id(845) - def test_recover_account_with_incorrect_passphrase(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - public_key = sign_in.get_public_key() - profile = sign_in.get_profile_view() - profile.backup_recovery_phrase_button.click() - profile.ok_continue_button.click() - recovery_phrase = profile.get_recovery_phrase() - - self.driver.reset() - sign_in.accept_agreements() - sign_in.recover_access(passphrase=' '.join(list(recovery_phrase.values())[::-1]), password=common_password) - if sign_in.get_public_key() == public_key: - pytest.fail('The same account is recovered with reversed passphrase') - - @marks.logcat - @marks.testrail_id(3769) - def test_logcat_recovering_account(self): - sign_in = SignInView(self.driver) - sign_in.recover_access(basic_user['passphrase'], basic_user['password']) - sign_in.check_no_values_in_logcat(passphrase=basic_user['passphrase'], password=basic_user['password']) diff --git a/test/appium/tests/atomic/account_management/test_sign_in.py b/test/appium/tests/atomic/account_management/test_sign_in.py deleted file mode 100644 index 9a1cbd81e3f..00000000000 --- a/test/appium/tests/atomic/account_management/test_sign_in.py +++ /dev/null @@ -1,72 +0,0 @@ -from tests import marks, common_password -from tests.base_test_case import MultipleDeviceTestCase, SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.all -@marks.sign_in -class TestSignIn(SingleDeviceTestCase): - - @marks.testrail_id(1381) - @marks.smoke_1 - def test_login_with_new_account(self): - sign_in = SignInView(self.driver) - username = 'test_user' - sign_in.create_user(username=username) - - self.driver.close_app() - self.driver.launch_app() - - sign_in.accept_agreements() - if not sign_in.test_fairy_warning.is_shown: - self.errors.append('TestFairy warning is not shown') - if not sign_in.element_by_text(username).is_element_displayed(): - self.errors.append('Username is not shown while login') - sign_in.sign_in() - if not sign_in.home_button.is_element_displayed(): - self.errors.append('User is not logged in') - self.verify_no_errors() - - @marks.testrail_id(2169) - def test_login_with_incorrect_password(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - profile = sign_in.profile_button.click() - profile.logout() - if sign_in.ok_button.is_element_displayed(): - sign_in.ok_button.click() - sign_in.password_input.set_value(common_password + '1') - sign_in.sign_in_button.click() - sign_in.find_full_text('Wrong password') - - @marks.logcat - @marks.testrail_id(3768) - def test_password_in_logcat_sign_in(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - profile = sign_in.profile_button.click() - profile.logout() - sign_in.sign_in() - sign_in.check_no_values_in_logcat(password=common_password) - - -@marks.all -@marks.sign_in -class TestSignInOffline(MultipleDeviceTestCase): - - @marks.testrail_case_id(3740) - @marks.testrail_id(1432) - def test_offline_login(self): - self.create_drivers(1, offline_mode=True) - driver = self.drivers[0] - sign_in = SignInView(driver) - sign_in.create_user() - - driver.close_app() - driver.set_network_connection(1) # airplane mode - - driver.launch_app() - sign_in.accept_agreements() - home = sign_in.sign_in() - home.home_button.wait_for_visibility_of_element() - assert home.connection_status.text == 'Offline' diff --git a/test/appium/tests/atomic/account_management/test_wallet_management.py b/test/appium/tests/atomic/account_management/test_wallet_management.py deleted file mode 100644 index 5f23cbe814c..00000000000 --- a/test/appium/tests/atomic/account_management/test_wallet_management.py +++ /dev/null @@ -1,190 +0,0 @@ -import pytest - -from tests import marks, transaction_users, camera_access_error_text, common_password, transaction_users_wallet -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.all -@marks.account -class TestWalletManagement(SingleDeviceTestCase): - - @marks.testrail_id(3698) - @marks.smoke_1 - def test_wallet_set_up(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - wallet = sign_in.wallet_button.click() - text = 'Simple and secure cryptocurrency wallet' - if not wallet.element_by_text(text).is_element_displayed(): - self.errors.append("'%s' is not displayed" % text) - wallet.set_up_button.click() - text = ('This is your personal transaction phrase that you’ll use everytime you make a transaction. ' - 'Make sure to write it down on a piece of paper, store it somewhere, ' - 'and only confirm transactions when you see these three words.') - if not wallet.element_by_text(text).is_element_displayed(): - self.errors.append("'%s' text is not displayed" % text) - phrase_length = len(wallet.sign_in_phrase.list) - if phrase_length != 3: - self.errors.append('Transaction phrase length is %s' % phrase_length) - wallet.done_button.click() - for text in ['Wrote it down?', 'You won’t be able to see your 3-word transaction phrase again after this.']: - if not wallet.element_by_text(text).is_element_displayed(): - self.errors.append("'%s' text is not displayed" % text) - wallet.yes_button.click() - for element in [wallet.send_transaction_button, wallet.receive_transaction_button, - wallet.transaction_history_button]: - if not element.is_element_displayed(): - self.errors.append('%s button is not shown after wallet setup' % element.name) - self.verify_no_errors() - - @marks.testrail_id(1449) - def test_open_transaction_on_etherscan(self): - user = transaction_users['A_USER'] - sign_in_view = SignInView(self.driver) - home_view = sign_in_view.recover_access(user['passphrase'], user['password']) - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - transactions_view = wallet_view.transaction_history_button.click() - transaction_details = transactions_view.transactions_table.transaction_by_index(0).click() - transaction_hash = transaction_details.get_transaction_hash() - transaction_details.options_button.click() - transaction_details.open_transaction_on_etherscan_button.click() - base_web_view = wallet_view.get_base_web_view() - base_web_view.open_in_webview() - base_web_view.find_text_part(transaction_hash) - - @marks.testrail_id(1450) - def test_copy_transaction_hash(self): - user = transaction_users['A_USER'] - sign_in_view = SignInView(self.driver) - home_view = sign_in_view.recover_access(user['passphrase'], user['password']) - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - transactions_view = wallet_view.transaction_history_button.click() - transaction_details = transactions_view.transactions_table.transaction_by_index(0).click() - transaction_hash = transaction_details.get_transaction_hash() - transaction_details.options_button.click() - transaction_details.copy_transaction_hash_button.click() - transaction_details.get_back_to_home_view() - wallet_view.home_button.click() - public_chat = home_view.join_public_chat('testchat') - public_chat.chat_message_input.paste_text_from_clipboard() - if public_chat.chat_message_input.text != transaction_hash: - pytest.fail('Transaction hash was not copied') - - @marks.testrail_id(3713) - @marks.smoke_1 - def test_manage_assets(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - wallet = sign_in.wallet_button.click() - wallet.set_up_wallet() - wallet.options_button.click() - wallet.manage_assets_button.click() - select_asset = 'MDS' - deselect_asset = 'STT' - wallet.asset_checkbox_by_name(select_asset).click() - wallet.asset_checkbox_by_name(deselect_asset).click() - wallet.done_button.click() - if not wallet.asset_by_name(select_asset).is_element_displayed(): - self.errors.append('%s asset is not shown in wallet' % select_asset) - if wallet.asset_by_name(deselect_asset).is_element_displayed(): - self.errors.append('%s asset is shown in wallet but was deselected' % deselect_asset) - self.verify_no_errors() - - @marks.testrail_id(3725) - def test_backup_recovery_phrase_warning_from_wallet(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - wallet = sign_in.wallet_button.click() - wallet.set_up_wallet() - if wallet.backup_recovery_phrase.is_element_present(): - pytest.fail("'Backup your Recovery phrase' option is shown on Wallet for an account with no funds") - wallet.receive_transaction_button.click() - address = wallet.address_text.text[2:] - wallet.get_back_to_home_view() - home = wallet.home_button.click() - self.network_api.get_donate(address) - home.wallet_button.click() - if not wallet.backup_recovery_phrase.is_element_present(): - pytest.fail("'Backup your Recovery phrase' option is not shown on Wallet for an account with funds") - profile = wallet.get_profile_view() - profile.backup_recovery_phrase() - - @marks.testrail_id(3777) - def test_no_collectibles_to_send_from_wallet(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - profile = sign_in.profile_button.click() - profile.switch_network('Mainnet with upstream RPC') - sign_in.sign_in() - wallet = sign_in.wallet_button.click() - wallet.set_up_wallet() - wallet.options_button.click() - wallet.manage_assets_button.click() - asset_name = 'CryptoKitties' - wallet.asset_checkbox_by_name(asset_name).click() - wallet.done_button.click() - send_transaction = wallet.send_transaction_button.click() - send_transaction.select_asset_button.click() - if send_transaction.asset_by_name(asset_name).is_element_displayed(): - pytest.fail('Collectibles can be sent from wallet') - - @marks.testrail_id(2176) - def test_deny_camera_access_scanning_wallet_adders(self): - sign_in = SignInView(self.driver) - sign_in.create_user() - wallet = sign_in.wallet_button.click() - wallet.set_up_wallet() - send_transaction = wallet.send_transaction_button.click() - send_transaction.chose_recipient_button.click() - send_transaction.scan_qr_code_button.click() - send_transaction.deny_button.click() - send_transaction.element_by_text(camera_access_error_text).wait_for_visibility_of_element(3) - send_transaction.ok_button.click() - send_transaction.chose_recipient_button.click() - send_transaction.scan_qr_code_button.click() - send_transaction.deny_button.wait_for_visibility_of_element(2) - - @marks.testrail_id(3730) - def test_filter_transactions_history(self): - user = transaction_users_wallet['C_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(passphrase=user['passphrase'], password=user['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - - transaction_history = wallet_view.transaction_history_button.click() - transaction_history.filters_button.click() - for filter_name in 'Outgoing', 'Pending', 'Failed': - transaction_history.filter_checkbox(filter_name).click() - wallet_view.done_button.click() - for i in range(transaction_history.transactions_table.get_transactions_number()): - details = transaction_history.transactions_table.transaction_by_index(i).click() - if details.get_recipient_address() != '0x' + user['address'] \ - or details.element_by_text('Failed').is_element_displayed(): - pytest.fail('Incoming transactions are not filtered') - details.back_button.click() - - transaction_history.filters_button.click() - for filter_name in 'Outgoing', 'Incoming': - transaction_history.filter_checkbox(filter_name).click() - wallet_view.done_button.click() - for i in range(transaction_history.transactions_table.get_transactions_number()): - details = transaction_history.transactions_table.transaction_by_index(i).click() - if details.get_sender_address() != '0x' + user['address'] \ - or details.element_by_text('Failed').is_element_displayed(): - pytest.fail('Outgoing transactions are not filtered') - details.back_button.click() - - transaction_history.filters_button.click() - for filter_name in 'Outgoing', 'Failed': - transaction_history.filter_checkbox(filter_name).click() - wallet_view.done_button.click() - for i in range(transaction_history.transactions_table.get_transactions_number()): - details = transaction_history.transactions_table.transaction_by_index(i).click() - if not details.element_by_text('Failed').is_element_displayed(): - pytest.fail('Failed transactions are not filtered') - details.back_button.click() - self.verify_no_errors() diff --git a/test/appium/tests/atomic/account_management/test_wallet_modal.py b/test/appium/tests/atomic/account_management/test_wallet_modal.py deleted file mode 100644 index c5c6ddab743..00000000000 --- a/test/appium/tests/atomic/account_management/test_wallet_modal.py +++ /dev/null @@ -1,103 +0,0 @@ -import pytest - -from tests import marks, transaction_users -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.wallet_modal -class TestWalletModal(SingleDeviceTestCase): - - @marks.testrail_id(3794) - def test_wallet_modal_public_chat(self): - user = transaction_users['A_USER'] - sign_in = SignInView(self.driver) - sign_in.recover_access(user['passphrase'], user['password']) - wallet = sign_in.wallet_button.click() - wallet.set_up_wallet() - usd_value = wallet.get_usd_total_value() - eth_value = wallet.get_eth_value() - stt_value = wallet.get_stt_value() - home = wallet.home_button.click() - chat = home.join_public_chat(home.get_public_chat_name()) - wallet_modal = chat.wallet_modal_button.click() - if wallet_modal.address_text.text != '0x' + user['address']: - self.errors.append('Wallet address is not shown in wallet modal') - modal_usd_value = wallet_modal.get_usd_total_value() - if modal_usd_value > usd_value * 1.001 or modal_usd_value < usd_value * 0.999: - self.errors.append('Total value in USD is not correct in wallet modal') - if wallet_modal.get_eth_value() != eth_value: - self.errors.append('ETH value is not correct in wallet modal') - if wallet_modal.get_stt_value() != stt_value: - self.errors.append('STT value is not correct in wallet modal') - if not wallet_modal.transaction_history_button.is_element_displayed(): - self.errors.append('Transaction history button is not visible in wallet modal') - self.verify_no_errors() - - @marks.testrail_id(3795) - def test_wallet_modal_dapp(self): - user = transaction_users['B_USER'] - sign_in = SignInView(self.driver) - sign_in.recover_access(user['passphrase'], user['password']) - wallet = sign_in.wallet_button.click() - wallet.set_up_wallet() - usd_value = wallet.get_usd_total_value() - eth_value = wallet.get_eth_value() - stt_value = wallet.get_stt_value() - home = wallet.home_button.click() - start_new_chat = home.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.element_by_text('Airswap').click() - start_new_chat.open_button.click() - wallet_modal = start_new_chat.wallet_modal_button.click() - if wallet_modal.address_text.text != '0x' + user['address']: - self.errors.append('Wallet address is not shown in wallet modal') - modal_usd_value = wallet_modal.get_usd_total_value() - if modal_usd_value > usd_value * 1.001 or modal_usd_value < usd_value * 0.999: - self.errors.append('Total value in USD is not correct in wallet modal') - if wallet_modal.get_eth_value() != eth_value: - self.errors.append('ETH value is not correct in wallet modal') - if wallet_modal.get_stt_value() != stt_value: - self.errors.append('STT value is not correct in wallet modal') - if not wallet_modal.transaction_history_button.is_element_displayed(): - self.errors.append('Transaction history button is not visible in wallet modal') - self.verify_no_errors() - - @marks.testrail_id(3797) - def test_wallet_modal_transaction_history(self): - user = transaction_users['B_USER'] - sign_in = SignInView(self.driver) - home = sign_in.recover_access(user['passphrase'], user['password']) - start_new_chat = home.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.element_by_text('Airswap').click() - start_new_chat.open_button.click() - wallet_modal = start_new_chat.wallet_modal_button.click() - transaction_history = wallet_modal.transaction_history_button.click() - if transaction_history.transactions_table.get_transactions_number() < 1: - pytest.fail('Transactions history is not shown') - - @marks.testrail_id(3800) - def test_close_and_open_wallet_modal(self): - user = transaction_users['B_USER'] - sign_in = SignInView(self.driver) - home = sign_in.recover_access(user['passphrase'], user['password']) - start_new_chat = home.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.element_by_text('Airswap').click() - start_new_chat.open_button.click() - web_view = start_new_chat.get_base_web_view() - wallet_modal = start_new_chat.wallet_modal_button.click() - if not wallet_modal.usd_total_value.is_element_displayed(): - pytest.fail("Wallet modal doesn't contain balance") - transaction_history = wallet_modal.transaction_history_button.click() - if transaction_history.transactions_table.get_transactions_number() < 1: - pytest.fail('Transactions history is not shown') - transaction_history.cross_icon.click() - if not web_view.browser_previous_page_button.is_element_displayed(): - pytest.fail('Modal wallet was not closed') - web_view.cross_icon.click() - chat = home.join_public_chat(home.get_public_chat_name()) - chat.wallet_modal_button.click() - if not wallet_modal.usd_total_value.is_element_displayed(): - pytest.fail("Wallet modal was not opened") diff --git a/test/appium/tests/atomic/chats/test_chats_management.py b/test/appium/tests/atomic/chats/test_chats_management.py deleted file mode 100644 index f967c15d168..00000000000 --- a/test/appium/tests/atomic/chats/test_chats_management.py +++ /dev/null @@ -1,185 +0,0 @@ -import pytest - -from tests import marks, group_chat_users, basic_user, camera_access_error_text -from tests.base_test_case import SingleDeviceTestCase, MultipleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.chat -class TestChatManagement(SingleDeviceTestCase): - - @marks.testrail_id(1428) - def test_clear_history_one_to_one_chat(self): - sign_in_view = SignInView(self.driver) - home_view = sign_in_view.create_user() - chat_view = home_view.add_contact(basic_user['public_key']) - for _ in range(2): - chat_view.chat_message_input.send_keys('test message') - chat_view.send_message_button.click() - chat_view.clear_history() - if not chat_view.no_messages_in_chat.is_element_present(): - pytest.fail('Message history is shown') - home_view.relogin() - home_view.get_chat_with_user(basic_user['username']).click() - if not chat_view.no_messages_in_chat.is_element_present(): - pytest.fail('Message history is shown after re-login') - - @marks.testrail_id(1395) - @marks.smoke_1 - def test_swipe_to_delete_1_1_chat(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - chat = home.add_contact(group_chat_users['A_USER']['public_key']) - chat.chat_message_input.send_keys('test message') - chat.send_message_button.click() - chat.get_back_to_home_view() - home.get_chat_with_user(group_chat_users['A_USER']['username']).swipe_and_delete() - self.driver.close_app() - self.driver.launch_app() - sign_in.accept_agreements() - sign_in.sign_in() - if home.get_chat_with_user(group_chat_users['A_USER']['username']).is_element_displayed(): - pytest.fail('Deleted 1-1 chat is present after relaunch app') - - @marks.testrail_id(3718) - @marks.smoke_1 - def test_swipe_to_delete_public_chat(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - chat_name = home.get_public_chat_name() - chat = home.join_public_chat(chat_name) - message = 'test message' - chat.chat_message_input.send_keys(message) - chat.send_message_button.click() - chat.get_back_to_home_view() - home.get_chat_with_user('#' + chat_name).swipe_and_delete() - profile = home.profile_button.click() - profile.logout() - sign_in.sign_in() - if home.get_chat_with_user('#' + chat_name).is_element_displayed(): - self.errors.append('Deleted public chat is present after relogin') - home.join_public_chat(chat_name) - if chat.chat_element_by_text(message).is_element_displayed(): - self.errors.append('Chat history is shown') - self.verify_no_errors() - - @marks.testrail_id(763) - @marks.smoke_1 - def test_add_contact_by_pasting_public_key(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - public_key = group_chat_users['A_USER']['public_key'] - - chat = home.join_public_chat(home.get_public_chat_name()) - chat.chat_message_input.send_keys(public_key) - chat.send_message_button.click() - chat.chat_element_by_text(public_key).long_press_element() - chat.element_by_text('Copy to clipboard').click() - chat.get_back_to_home_view() - - start_new_chat = home.plus_button.click() - start_new_chat.start_new_chat_button.click() - start_new_chat.public_key_edit_box.paste_text_from_clipboard() - if start_new_chat.public_key_edit_box.text != public_key: - pytest.fail('Public key is not pasted from clipboard') - start_new_chat.confirm() - start_new_chat.get_back_to_home_view() - home.plus_button.click() - start_new_chat.start_new_chat_button.click() - if not start_new_chat.element_by_text(group_chat_users['A_USER']['username']).is_element_displayed(): - pytest.fail("List of contacts doesn't contain added user") - - @marks.testrail_id(3719) - def test_delete_one_to_one_chat_via_delete_button(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - chat_view = home.add_contact(basic_user['public_key']) - for _ in range(2): - chat_view.chat_message_input.send_keys('test message') - chat_view.send_message_button.click() - chat_view.delete_chat() - if home.get_chat_with_user(basic_user['username']).is_element_present(10): - self.errors.append("One-to-one' chat is shown, but the chat has been deleted") - home.relogin() - if home.get_chat_with_user(basic_user['username']).is_element_present(10): - self.errors.append("One-to-one' chat is shown after re-login, but the chat has been deleted") - self.verify_no_errors() - - @marks.testrail_id(3720) - def test_delete_public_chat_via_delete_button(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - chat_name = home.get_public_chat_name() - public_chat = home.join_public_chat(chat_name) - public_chat.chat_message_input.send_keys('test message') - public_chat.send_message_button.click() - public_chat.delete_chat() - if home.element_by_text(chat_name).is_element_present(5): - self.errors.append("Public chat '%s' is shown, but the chat has been deleted" % chat_name) - home.relogin() - if home.element_by_text(chat_name).is_element_present(5): - self.errors.append("Public chat '%s' is shown after re-login, but the chat has been deleted" % chat_name) - self.verify_no_errors() - - @marks.testrail_id(2172) - def test_incorrect_contact_code_start_new_chat(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - start_new_chat = home.plus_button.click() - start_new_chat.start_new_chat_button.click() - start_new_chat.public_key_edit_box.set_value(group_chat_users['B_USER']['public_key'][:-1]) - start_new_chat.confirm() - if not start_new_chat.element_by_text('Please scan a valid contact code').is_element_displayed(): - pytest.fail('Error is not shown for invalid public key') - - @marks.testrail_id(2175) - def test_deny_camera_access_scanning_contact_code(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - start_new_chat = home.plus_button.click() - start_new_chat.start_new_chat_button.click() - start_new_chat.scan_contact_code_button.click() - start_new_chat.deny_button.click() - start_new_chat.element_by_text(camera_access_error_text).wait_for_visibility_of_element(3) - start_new_chat.ok_button.click() - start_new_chat.scan_contact_code_button.click() - start_new_chat.deny_button.wait_for_visibility_of_element(2) - - -@marks.chat -class TestChatManagementMultipleDevice(MultipleDeviceTestCase): - - @marks.testrail_id(3694) - @marks.smoke_1 - def test_add_contact_from_public_chat(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1, home_2 = device_1.create_user(), device_2.create_user() - chat_name = 'testaddcontact' - chat_1, chat_2 = home_1.join_public_chat(chat_name), home_2.join_public_chat(chat_name) - message = 'test message' - - chat_2.chat_message_input.send_keys(message) - chat_2.send_message_button.click() - self.drivers[1].quit() - - chat_element = chat_1.chat_element_by_text(message) - chat_element.find_element() - username = chat_element.username.text - chat_element.member_photo.click() - for element in [chat_1.contact_profile_picture, chat_1.add_to_contacts, chat_1.profile_send_message, - chat_1.profile_send_transaction, chat_1.public_key_text, - chat_1.element_by_text(username, 'text')]: - if not element.is_element_displayed(): - self.errors.append('%s is not visible' % 'user name' if 'Base' in element.name else element.name) - chat_1.add_to_contacts.click() - if not chat_1.element_by_text('In contacts').is_element_displayed(): - self.errors.append("'Add to contacts' is not changed to 'In contacts'") - - chat_1.get_back_to_home_view() - start_new_chat = home_1.plus_button.click() - start_new_chat.start_new_chat_button.click() - if not start_new_chat.element_by_text(username).is_element_displayed(): - self.errors.append("List of contacts doesn't contain added user") - - self.verify_no_errors() diff --git a/test/appium/tests/atomic/chats/test_commands.py b/test/appium/tests/atomic/chats/test_commands.py deleted file mode 100644 index 9ef2acbcf02..00000000000 --- a/test/appium/tests/atomic/chats/test_commands.py +++ /dev/null @@ -1,476 +0,0 @@ -import pytest -from _pytest.outcomes import Failed -from decimal import Decimal as d -from selenium.common.exceptions import TimeoutException - -from tests import marks, transaction_users, common_password, group_chat_users, transaction_users_wallet -from tests.base_test_case import MultipleDeviceTestCase, SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.chat -@marks.transaction -class TestCommandsMultipleDevices(MultipleDeviceTestCase): - - @marks.smoke_1 - @marks.testrail_id(3697) - def test_network_mismatch_for_send_request_commands(self): - sender = self.senders['d_user'] = transaction_users['D_USER'] - self.create_drivers(2) - device_1_sign_in, device_2_sign_in = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - device_1_sign_in.recover_access(passphrase=sender['passphrase'], password=sender['password']) - device_2_sign_in.create_user() - device_1_home, device_2_home = device_1_sign_in.get_home_view(), device_2_sign_in.get_home_view() - - public_key = device_2_home.get_public_key() - device_2_profile = device_2_home.get_profile_view() - device_2_profile.switch_network('Mainnet with upstream RPC') - device_2_sign_in.sign_in() - - device_1_chat = device_1_home.add_contact(public_key) - amount_1 = device_1_chat.get_unique_amount() - device_1_chat.send_transaction_in_1_1_chat('ETH', amount_1, common_password, wallet_set_up=True) - device_1_chat.chat_element_by_text(amount_1).progress_bar.wait_for_invisibility_of_element() - status_text_1 = device_1_chat.chat_element_by_text(amount_1).status.text - if status_text_1 != 'Sent': - self.errors.append("Message about sent funds has status '%s' instead of 'Sent'" % status_text_1) - - device_2_chat = device_2_home.get_chat_with_user(sender['username']).click() - chat_element_1 = device_2_chat.chat_element_by_text(amount_1) - try: - chat_element_1.wait_for_visibility_of_element(120) - chat_element_1.progress_bar.wait_for_invisibility_of_element() - if chat_element_1.status.text != 'Network mismatch': - self.errors.append("'Network mismatch' warning is not shown for send transaction message") - if not chat_element_1.contains_text('testnet'): - self.errors.append("Sent transaction message doesn't contain text 'testnet'") - except TimeoutException: - self.errors.append('Sent transaction message was not received') - device_2_chat.get_back_to_home_view() - - amount_2 = device_1_chat.get_unique_amount() - device_1_chat.request_transaction_in_1_1_chat('ETH', amount_2) - status_text_2 = device_1_chat.chat_element_by_text(amount_2).status.text - if status_text_2 != 'Sent': - self.errors.append("Request funds message has status '%s' instead of 'Sent'" % status_text_2) - - device_2_home.get_chat_with_user(sender['username']).click() - chat_element_2 = device_2_chat.chat_element_by_text(amount_2) - try: - chat_element_2.wait_for_visibility_of_element(120) - chat_element_2.progress_bar.wait_for_invisibility_of_element() - if chat_element_2.status.text != 'Network mismatch': - self.errors.append("'Network mismatch' warning is not shown for request funds message") - if not chat_element_2.contains_text('On testnet'): - self.errors.append("Request funds message doesn't contain text 'testnet'") - if not chat_element_2.contains_text('Transaction Request'): - self.errors.append("Request funds message doesn't contain text 'Transaction Request'") - except TimeoutException: - self.errors.append('Request funds message was not received') - self.verify_no_errors() - - @marks.testrail_id(765) - @marks.smoke_1 - def test_send_eth_in_1_1_chat(self): - recipient = transaction_users['D_USER'] - sender = self.senders['c_user'] = transaction_users['C_USER'] - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1 = device_1.recover_access(passphrase=sender['passphrase'], password=sender['password']) - home_2 = device_2.recover_access(passphrase=recipient['passphrase'], password=recipient['password']) - wallet_1, wallet_2 = home_1.wallet_button.click(), home_2.wallet_button.click() - wallet_1.set_up_wallet() - wallet_1.home_button.click() - wallet_2.set_up_wallet() - init_balance = wallet_2.get_eth_value() - wallet_2.home_button.click() - - chat_1 = home_1.add_contact(recipient['public_key']) - amount = chat_1.get_unique_amount() - chat_1.commands_button.click() - chat_1.send_command.click() - chat_1.asset_by_name('ETH').click() - chat_1.send_as_keyevent(amount) - send_transaction_view = chat_1.get_send_transaction_view() - chat_1.send_message_button.click_until_presence_of_element(send_transaction_view.sign_transaction_button) - - send_transaction_view.chose_recipient_button.find_element().click() - if send_transaction_view.recent_recipients_button.is_element_displayed(): - self.errors.append('Recipient field is editable') - send_transaction_view.click_system_back_button() - - send_transaction_view.select_asset_button.click() - if not send_transaction_view.chose_recipient_button.is_element_displayed(): - self.errors.append('Asset field is editable') - send_transaction_view.back_button.click() - - if send_transaction_view.amount_edit_box.is_element_displayed(): - self.errors.append('Amount field is editable') - - send_transaction_view.advanced_button.click() - send_transaction_view.transaction_fee_button.click() - gas_limit = '25000' - send_transaction_view.gas_limit_input.clear() - send_transaction_view.gas_limit_input.set_value(gas_limit) - gas_price = '1' - send_transaction_view.gas_price_input.clear() - send_transaction_view.gas_price_input.set_value(gas_price) - send_transaction_view.total_fee_input.click() - if send_transaction_view.total_fee_input.text != '%s ETH' % (d(gas_limit) * d(gas_price) / d(1000000000)): - self.errors.append('Gas limit and/or gas price fields were not edited') - send_transaction_view.done_button.click() - send_transaction_view.sign_transaction(sender['password']) - - if not chat_1.chat_element_by_text(amount).is_element_displayed(): - self.errors.append('Message with the sent amount is not shown for the sender') - chat_2 = home_2.get_chat_with_user(sender['username']).click() - if not chat_2.chat_element_by_text(amount).is_element_displayed(): - self.errors.append('Message with the sent amount is not shown for the recipient') - - chat_2.get_back_to_home_view() - home_2.wallet_button.click() - try: - wallet_2.wait_balance_changed_on_wallet_screen(expected_balance=init_balance + float(amount)) - self.network_api.find_transaction_by_unique_amount(recipient['address'], amount) - except Failed as e: - self.errors.append(e.msg) - self.verify_no_errors() - - @marks.testrail_id(1391) - @marks.smoke_1 - def test_request_and_receive_eth_in_1_1_chat(self): - recipient = transaction_users['C_USER'] - sender = self.senders['d_user'] = transaction_users['D_USER'] - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1 = device_1.recover_access(passphrase=sender['passphrase'], password=sender['password']) - home_2 = device_2.recover_access(passphrase=recipient['passphrase'], password=recipient['password']) - wallet_1, wallet_2 = home_1.wallet_button.click(), home_2.wallet_button.click() - wallet_1.set_up_wallet() - wallet_1.home_button.click() - wallet_2.set_up_wallet() - init_balance = wallet_2.get_eth_value() - wallet_2.home_button.click() - - chat_2 = home_2.add_contact(sender['public_key']) - amount = chat_2.get_unique_amount() - chat_2.request_transaction_in_1_1_chat('ETH', amount) - - chat_1 = home_1.get_chat_with_user(recipient['username']).click() - chat_1.send_funds_to_request(amount=amount, sender_password=sender['password']) - - if not chat_1.chat_element_by_text(amount).is_element_displayed(): - self.errors.append('Message with the sent amount is not shown for the sender') - if not chat_2.chat_element_by_text(amount).is_element_displayed(): - self.errors.append('Message with the sent amount is not shown for the recipient') - - chat_2.get_back_to_home_view() - home_2.wallet_button.click() - try: - wallet_2.wait_balance_changed_on_wallet_screen(expected_balance=init_balance + float(amount)) - self.network_api.find_transaction_by_unique_amount(recipient['address'], amount) - except Failed as e: - self.errors.append(e.msg) - self.verify_no_errors() - - @marks.testrail_id(1429) - @marks.smoke_1 - def test_request_eth_in_wallet(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1 = 'user_1' - recipient = group_chat_users['C_USER'] - - home_1 = device_1.create_user(username=username_1) - home_2 = device_2.recover_access(passphrase=recipient['passphrase'], password=recipient['password']) - - home_1.add_contact(recipient['public_key']) - home_1.get_back_to_home_view() - wallet_1 = home_1.wallet_button.click() - wallet_1.set_up_wallet() - - send_transaction_device_1 = wallet_1.receive_transaction_button.click_until_presence_of_element( - wallet_1.send_transaction_request) - wallet_1.send_transaction_request.click() - send_transaction_device_1.amount_edit_box.scroll_to_element() - amount = home_1.get_unique_amount() - send_transaction_device_1.amount_edit_box.set_value(amount) - send_transaction_device_1.confirm() - send_transaction_device_1.chose_recipient_button.click() - sender_button = send_transaction_device_1.element_by_text(recipient['username']) - send_transaction_device_1.recent_recipients_button.click_until_presence_of_element(sender_button) - sender_button.click() - wallet_1.send_request_button.click() - - chat_2 = home_2.get_chat_with_user(username_1).click() - chat_element = chat_2.chat_element_by_text(amount) - try: - chat_element.wait_for_visibility_of_element(120) - if not chat_element.contains_text('Transaction Request'): - self.errors.append("Request funds message doesn't contain text 'Transaction Request'") - if not chat_element.send_request_button.is_element_displayed(): - self.errors.append("Request funds message doesn't contain 'Send' button") - except TimeoutException: - self.errors.append('Request funds message was not received') - self.verify_no_errors() - - @marks.testrail_id(1417) - def test_contact_profile_send_transaction(self): - self.create_drivers(1) - recipient = transaction_users['B_USER'] - sign_in_view = SignInView(self.drivers[0]) - sign_in_view.create_user() - home_view = sign_in_view.get_home_view() - sender_public_key = home_view.get_public_key() - sender_address = home_view.public_key_to_address(sender_public_key) - home_view.home_button.click() - self.network_api.get_donate(sender_address) - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - home_view.home_button.click() - home_view.add_contact(recipient['public_key']) - chat_view = home_view.get_chat_view() - chat_view.chat_options.click_until_presence_of_element(chat_view.view_profile_button) - chat_view.view_profile_button.click() - chat_view.profile_send_transaction.click() - chat_view.chat_message_input.click() - chat_view.asset_by_name('ETH').click() - amount = chat_view.get_unique_amount() - chat_view.send_as_keyevent(amount) - chat_view.send_message_button.click() - send_transaction_view = chat_view.get_send_transaction_view() - send_transaction_view.sign_transaction(common_password) - self.network_api.find_transaction_by_unique_amount(recipient['address'], amount) - - @marks.testrail_id(3744) - @marks.smoke_1 - def test_send_tokens_in_1_1_chat(self): - recipient = transaction_users['D_USER'] - sender = transaction_users['C_USER'] - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1 = device_1.recover_access(passphrase=sender['passphrase'], password=sender['password']) - home_2 = device_2.recover_access(passphrase=recipient['passphrase'], password=recipient['password']) - wallet_1 = home_1.wallet_button.click() - wallet_1.set_up_wallet() - wallet_1.home_button.click() - - chat_1 = home_1.add_contact(recipient['public_key']) - amount = chat_1.get_unique_amount() - chat_1.send_transaction_in_1_1_chat('STT', amount, sender['password']) - - message_1 = chat_1.chat_element_by_text(amount) - if not message_1.is_element_displayed() or not message_1.contains_text('STT'): - self.errors.append('Message with the sent amount is not shown for the sender') - chat_2 = home_2.get_chat_with_user(sender['username']).click() - message_2 = chat_2.chat_element_by_text(amount) - if not message_2.is_element_displayed() or not message_2.contains_text('STT'): - self.errors.append('Message with the sent amount is not shown for the recipient') - - try: - self.network_api.find_transaction_by_unique_amount(recipient['address'], amount, token=True) - except Failed as e: - self.errors.append(e.msg) - self.verify_no_errors() - - @marks.testrail_id(3748) - @marks.smoke_1 - def test_request_and_receive_tokens_in_1_1_chat(self): - recipient = transaction_users['C_USER'] - sender = transaction_users['D_USER'] - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1 = device_1.recover_access(passphrase=sender['passphrase'], password=sender['password']) - home_2 = device_2.recover_access(passphrase=recipient['passphrase'], password=recipient['password']) - wallet_1, wallet_2 = home_1.wallet_button.click(), home_2.wallet_button.click() - wallet_1.set_up_wallet() - wallet_1.home_button.click() - wallet_2.set_up_wallet() - wallet_2.home_button.click() - - chat_2 = home_2.add_contact(sender['public_key']) - amount = chat_2.get_unique_amount() - chat_2.request_transaction_in_1_1_chat('STT', amount) - - chat_1 = home_1.get_chat_with_user(recipient['username']).click() - chat_1.send_funds_to_request(amount=amount, sender_password=sender['password']) - - message_1 = chat_1.chat_element_by_text(amount) - if not message_1.is_element_displayed() or not message_1.contains_text('STT'): - self.errors.append('Message with the sent amount is not shown for the sender') - message_2 = chat_2.chat_element_by_text(amount) - if not message_2.is_element_displayed() or not message_2.contains_text('STT'): - self.errors.append('Message with the sent amount is not shown for the recipient') - - try: - self.network_api.find_transaction_by_unique_amount(recipient['address'], amount, token=True) - except Failed as e: - self.errors.append(e.msg) - self.verify_no_errors() - - @marks.testrail_id(3749) - @marks.smoke_1 - def test_transaction_confirmed_on_recipient_side(self): - recipient = transaction_users['D_USER'] - sender = transaction_users['C_USER'] - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1 = device_1.recover_access(passphrase=sender['passphrase'], password=sender['password']) - home_2 = device_2.recover_access(passphrase=recipient['passphrase'], password=recipient['password']) - wallet_1 = home_1.wallet_button.click() - wallet_1.set_up_wallet() - wallet_1.home_button.click() - - chat_1 = home_1.add_contact(recipient['public_key']) - amount = chat_1.get_unique_amount() - chat_1.send_transaction_in_1_1_chat('ETH', amount, sender['password']) - - chat_2 = home_2.get_chat_with_user(sender['username']).click() - self.network_api.wait_for_confirmation_of_transaction(recipient['address'], amount) - if not chat_2.chat_element_by_text(amount).contains_text('Confirmed', 60): - pytest.fail('Transaction state is not updated on the recipient side') - - -@marks.chat -@marks.transaction -class TestCommandsSingleDevices(SingleDeviceTestCase): - - @marks.testrail_id(3745) - @marks.smoke_1 - def test_send_request_not_enabled_tokens(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - chat = home.add_contact(transaction_users['D_USER']['public_key']) - chat.commands_button.click() - chat.send_command.click() - if chat.asset_by_name('MDS').is_element_displayed(): - self.errors.append('Token which is not enabled in wallet can be sent in 1-1 chat') - chat.chat_message_input.clear() - chat.commands_button.click() - chat.request_command.click() - if chat.asset_by_name('MDS').is_element_displayed(): - self.errors.append('Token which is not enabled in wallet can be requested in 1-1 chat') - self.verify_no_errors() - - @marks.logcat - @marks.testrail_id(3771) - def test_logcat_send_transaction_in_1_1_chat(self): - sender = transaction_users['C_USER'] - sign_in = SignInView(self.driver) - home = sign_in.recover_access(passphrase=sender['passphrase'], password=sender['password']) - wallet = home.wallet_button.click() - wallet.set_up_wallet() - wallet.home_button.click() - chat = home.add_contact(transaction_users['D_USER']['public_key']) - amount = chat.get_unique_amount() - chat.send_transaction_in_1_1_chat('ETH', amount, sender['password']) - chat.check_no_values_in_logcat(password=sender['password']) - - @marks.testrail_id(3736) - @marks.smoke_1 - def test_send_transaction_details_in_1_1_chat(self): - recipient = transaction_users['D_USER'] - sender = transaction_users['C_USER'] - sign_in = SignInView(self.driver) - home = sign_in.recover_access(passphrase=sender['passphrase'], password=sender['password']) - wallet = home.wallet_button.click() - wallet.set_up_wallet() - wallet.home_button.click() - - chat = home.add_contact(recipient['public_key']) - amount = chat.get_unique_amount() - chat.commands_button.click() - chat.send_command.click() - chat.asset_by_name('ETH').click() - chat.send_as_keyevent(amount) - send_transaction_view = chat.get_send_transaction_view() - chat.send_message_button.click_until_presence_of_element(send_transaction_view.sign_transaction_button) - - if not send_transaction_view.element_by_text(recipient['username']).is_element_displayed(): - self.errors.append('Recipient name is not shown') - if not send_transaction_view.element_by_text('0x' + recipient['address']).is_element_displayed(): - self.errors.append('Recipient address is not shown') - if not send_transaction_view.element_by_text('ETH').is_element_displayed(): - self.errors.append("Asset field doesn't contain 'ETH' text") - if not send_transaction_view.element_by_text(amount).is_element_displayed(): - self.errors.append('Amount is not visible') - self.verify_no_errors() - - @marks.testrail_id(3750) - @marks.smoke_1 - def test_transaction_confirmed_on_sender_side(self): - sender = transaction_users['D_USER'] - sign_in = SignInView(self.driver) - home = sign_in.recover_access(passphrase=sender['passphrase'], password=sender['password']) - wallet = home.wallet_button.click() - wallet.set_up_wallet() - wallet.home_button.click() - chat = home.add_contact(transaction_users['C_USER']['public_key']) - amount = chat.get_unique_amount() - chat.send_transaction_in_1_1_chat('ETH', amount, sender['password']) - self.network_api.wait_for_confirmation_of_transaction(sender['address'], amount) - if not chat.chat_element_by_text(amount).contains_text('Confirmed', wait_time=90): - pytest.fail('Transaction state is not updated on the sender side') - - @marks.testrail_id(3790) - def test_insufficient_funds_1_1_chat_0_balance(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - home_view = wallet_view.home_button.click() - chat_view = home_view.add_contact(transaction_users['H_USER']['public_key']) - chat_view.commands_button.click() - chat_view.send_command.click() - chat_view.asset_by_name('ETH').click() - chat_view.send_as_keyevent('1') - chat_view.send_message_button.click() - send_transaction = chat_view.get_send_transaction_view() - error_text = send_transaction.element_by_text('Insufficient funds') - if not error_text.is_element_displayed(): - self.errors.append("'Insufficient funds' error is now shown when sending 1 ETH from chat with balance 0") - send_transaction.back_button.click() - chat_view.commands_button.click() - chat_view.send_command.click() - chat_view.asset_by_name('STT').click() - chat_view.send_as_keyevent('1') - chat_view.send_message_button.click() - if not error_text.is_element_displayed(): - self.errors.append("'Insufficient funds' error is now shown when sending 1 STT from chat with balance 0") - self.verify_no_errors() - - @marks.testrail_id(3793) - def test_insufficient_funds_1_1_chat_positive_balance(self): - sender = transaction_users_wallet['A_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - eth_value = wallet_view.get_eth_value() - stt_value = wallet_view.get_stt_value() - if eth_value == 0 or stt_value == 0: - pytest.fail('No funds!') - home_view = wallet_view.home_button.click() - chat_view = home_view.add_contact(transaction_users['H_USER']['public_key']) - chat_view.commands_button.click() - chat_view.send_command.click() - chat_view.asset_by_name('ETH').click() - chat_view.send_as_keyevent(str(round(eth_value + 1))) - chat_view.send_message_button.click() - send_transaction = chat_view.get_send_transaction_view() - error_text = send_transaction.element_by_text('Insufficient funds') - if not error_text.is_element_displayed(): - self.errors.append( - "'Insufficient funds' error is now shown when sending %s ETH from chat with balance %s" % ( - round(eth_value + 1), eth_value)) - send_transaction.back_button.click() - chat_view.commands_button.click() - chat_view.send_command.click() - chat_view.asset_by_name('STT').click() - chat_view.send_as_keyevent(str(round(stt_value + 1))) - chat_view.send_message_button.click() - if not error_text.is_element_displayed(): - self.errors.append( - "'Insufficient funds' error is now shown when sending %s STT from chat with balance %s" % ( - round(stt_value + 1), stt_value)) - self.verify_no_errors() diff --git a/test/appium/tests/atomic/chats/test_one_to_one.py b/test/appium/tests/atomic/chats/test_one_to_one.py deleted file mode 100644 index 4134321da90..00000000000 --- a/test/appium/tests/atomic/chats/test_one_to_one.py +++ /dev/null @@ -1,534 +0,0 @@ -import random -import string -import emoji -from datetime import datetime -from selenium.common.exceptions import TimeoutException -from tests import marks, get_current_time, group_chat_users -from tests.base_test_case import MultipleDeviceTestCase, SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.chat -class TestMessagesOneToOneChatMultiple(MultipleDeviceTestCase): - - @marks.testrail_id(764) - @marks.smoke_1 - def test_text_message_1_1_chat(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1 = 'user_%s' % get_current_time() - device_1_home, device_2_home = device_1.create_user(username=username_1), device_2.create_user() - device_2_public_key = device_2_home.get_public_key() - device_2_home.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key) - - message = 'hello' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - - device_2_chat = device_2_home.get_chat_with_user(username_1).click() - device_2_chat.chat_element_by_text(message).wait_for_visibility_of_element() - - @marks.testrail_id(772) - @marks.smoke_1 - def test_offline_messaging_1_1_chat(self): - self.create_drivers(2, offline_mode=True) - device_1, device_2 = self.drivers[0], self.drivers[1] - sign_in_1, sign_in_2 = SignInView(device_1), SignInView(device_2) - username_2 = 'user_2' - home_1, home_2 = sign_in_1.create_user(), sign_in_2.create_user(username=username_2) - public_key_1 = home_1.get_public_key() - home_1.home_button.click() - - device_1.set_network_connection(1) # airplane mode on primary device - - chat_2 = home_2.add_contact(public_key_1) - message_1 = 'test message' - chat_2.chat_message_input.send_keys(message_1) - chat_2.send_message_button.click() - device_2.set_network_connection(1) # airplane mode on secondary device - - device_1.set_network_connection(2) # turning on WiFi connection on primary device - - home_1.connection_status.wait_for_invisibility_of_element(20) - chat_element = home_1.get_chat_with_user(username_2) - chat_element.wait_for_visibility_of_element(20) - chat_1 = chat_element.click() - chat_1.chat_element_by_text(message_1).wait_for_visibility_of_element(2) - - device_2.set_network_connection(2) # turning on WiFi connection on secondary device - device_1.set_network_connection(1) # airplane mode on primary device - - chat_2.element_by_text('Connecting to peers...').wait_for_invisibility_of_element(60) - message_2 = 'one more message' - chat_2.chat_message_input.send_keys(message_2) - chat_2.send_message_button.click() - - device_1.set_network_connection(2) # turning on WiFi connection on primary device - - chat_1 = chat_element.click() - chat_1.chat_element_by_text(message_2).wait_for_visibility_of_element(180) - - @marks.smoke_1 - @marks.testrail_id(3701) - def test_resend_message_offline(self): - self.create_drivers(2, offline_mode=True) - device_1, device_2 = self.drivers[0], self.drivers[1] - sign_in_1, sign_in_2 = SignInView(device_1), SignInView(device_2) - username_1 = 'user_%s' % get_current_time() - home_1, home_2 = sign_in_1.create_user(username_1), sign_in_2.create_user() - public_key_2 = home_2.get_public_key() - home_2.home_button.click() - - device_1.set_network_connection(1) # airplane mode on primary device - - chat_1 = home_1.add_contact(public_key_2) - message = 'test message' - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - progress_time = chat_1.chat_element_by_text(message).progress_bar.measure_time_while_element_is_shown() - if not 5 < progress_time < 30: - self.errors.append('Progress indicator is shown during %s seconds' % progress_time) - - device_1.set_network_connection(2) # turning on WiFi connection - chat_1.element_by_text('Connecting to peers...').wait_for_invisibility_of_element(30) - chat_1.element_by_text('Not sent. Tap for options').click() - if not chat_1.element_by_text('Delete message').is_element_displayed(): - self.errors.append("'Delete message' button is not shown for not sent message") - - chat_element = chat_1.chat_element_by_text(message) - chat_1.element_by_text('Resend').click() - chat_element.status.wait_for_visibility_of_element() - if chat_element.status.text != 'Sent': - self.errors.append("Message status is not 'Sent' after resending the message") - - chat_2 = home_2.get_chat_with_user(username_1).click() - if not chat_2.chat_element_by_text(message).is_element_displayed(10): - self.errors.append("Message with text '%s' is not received" % message) - - self.verify_no_errors() - - @marks.testrail_id(3710) - @marks.smoke_1 - def test_messaging_in_different_networks(self): - self.create_drivers(2) - device_1, device_2 = self.drivers[0], self.drivers[1] - sign_in_1, sign_in_2 = SignInView(device_1), SignInView(device_2) - username_1 = 'user_%s' % get_current_time() - sign_in_1.create_user(username_1) - sign_in_2.create_user() - home_1, home_2 = sign_in_1.get_home_view(), sign_in_2.get_home_view() - public_key_2 = home_2.get_public_key() - profile_2 = home_2.get_profile_view() - profile_2.switch_network('Mainnet with upstream RPC') - sign_in_2.sign_in() - - chat_1 = home_1.add_contact(public_key_2) - message = 'test message' - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - - chat_2 = home_2.get_chat_with_user(username_1).click() - chat_2.chat_element_by_text(message).wait_for_visibility_of_element() - - public_chat_name = home_1.get_public_chat_name() - chat_1.get_back_to_home_view() - home_1.join_public_chat(public_chat_name) - chat_2.get_back_to_home_view() - home_2.join_public_chat(public_chat_name) - - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - chat_2.chat_element_by_text(message).wait_for_visibility_of_element() - - @marks.testrail_id(1386) - @marks.smoke_1 - def test_send_message_to_newly_added_contact(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1 = 'user_%s' % get_current_time() - - device_1_home, device_2_home = device_1.create_user(username=username_1), device_2.create_user() - - profile_1 = device_1_home.profile_button.click() - file_name = 'sauce_logo.png' - profile_1.edit_profile_picture(file_name) - profile_1.home_button.click() - - device_2_public_key = device_2_home.get_public_key() - device_2_home.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key) - message = 'hello' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - - chat_element = device_2_home.get_chat_with_user(username_1) - chat_element.wait_for_visibility_of_element() - device_2_chat = chat_element.click() - if not device_2_chat.chat_element_by_text(message).is_element_displayed(): - self.errors.append("Message with test '%s' was not received" % message) - if not device_2_chat.add_to_contacts.is_element_displayed(): - self.errors.append('Add to contacts button is not shown') - if device_2_chat.user_name_text.text != username_1: - self.errors.append("Real username '%s' is not shown in one-to-one chat" % username_1) - device_2_chat.chat_options.click() - device_2_chat.view_profile_button.click() - if not device_2_chat.contact_profile_picture.is_element_image_equals_template(file_name): - self.errors.append("Updated profile picture is not shown in one-to-one chat") - self.verify_no_errors() - - @marks.testrail_id(1387) - @marks.smoke_1 - def test_add_to_contacts(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1, username_2 = 'user_1', 'user_2' - - device_1_home, device_2_home = device_1.create_user(username=username_1), device_2.create_user( - username=username_2) - - device_2_public_key = device_2_home.get_public_key() - profile_2 = device_2_home.get_profile_view() - file_name = 'sauce_logo.png' - profile_2.edit_profile_picture(file_name) - profile_2.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key) - message = 'hello' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - - chat_element = device_2_home.get_chat_with_user(username_1) - chat_element.wait_for_visibility_of_element() - device_2_chat = chat_element.click() - if not device_2_chat.chat_element_by_text(message).is_element_displayed(): - self.errors.append("Message with text '%s' was not received" % message) - device_2_chat.reconnect() - device_2_chat.add_to_contacts.click() - - device_2_chat.get_back_to_home_view() - start_new_chat = device_2_home.plus_button.click() - start_new_chat.start_new_chat_button.click() - if not start_new_chat.element_by_text(username_1).is_element_displayed(): - self.errors.append('%s is not added to contacts' % username_1) - - if device_1_chat.user_name_text.text != username_2: - self.errors.append("Real username '%s' is not shown in one-to-one chat" % username_2) - device_1_chat.chat_options.click() - device_1_chat.view_profile_button.click() - if not device_1_chat.contact_profile_picture.is_element_image_equals_template(file_name): - self.errors.append("Updated profile picture is not shown in one-to-one chat") - self.verify_no_errors() - - @marks.testrail_id(1413) - @marks.smoke_1 - def test_send_and_open_links(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1, username_2 = 'user_1', 'user_2' - - home_1, home_2 = device_1.create_user(username=username_1), device_2.create_user(username=username_2) - public_key_2 = home_2.get_public_key() - home_2.home_button.click() - - chat_1 = home_1.add_contact(public_key_2) - url_message = 'status.im' - chat_1.chat_message_input.send_keys(url_message) - chat_1.send_message_button.click() - chat_1.get_back_to_home_view() - chat_2 = home_2.get_chat_with_user(username_1).click() - chat_2.element_starts_with_text(url_message, 'button').click() - web_view = chat_2.open_in_status_button.click() - try: - web_view.find_full_text('Status, the Ethereum discovery tool.') - except TimeoutException: - self.errors.append('URL was not opened from 1-1 chat') - web_view.back_to_home_button.click() - chat_2.get_back_to_home_view() - - chat_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(7)) - home_1.join_public_chat(chat_name) - home_2.join_public_chat(chat_name) - chat_2.chat_message_input.send_keys(url_message) - chat_2.send_message_button.click() - chat_1.element_starts_with_text(url_message, 'button').click() - web_view = chat_1.open_in_status_button.click() - try: - web_view.find_full_text('Status, the Ethereum discovery tool.') - except TimeoutException: - self.errors.append('URL was not opened from 1-1 chat') - self.verify_no_errors() - - @marks.testrail_id(1431) - @marks.smoke_1 - def test_offline_status(self): - self.create_drivers(1, offline_mode=True) - driver = self.drivers[0] - sign_in = SignInView(driver) - home_view = sign_in.create_user() - - # Dismiss "Welcome to Status" placeholder. - # When the placeholder is visible, the offline status bar does not appear - wallet_view = home_view.wallet_button.click() - wallet_view.home_button.click() - - driver.set_network_connection(1) # airplane mode - - if home_view.connection_status.text != 'Offline': - self.errors.append('Offline status is not shown in home screen') - - chat = home_view.add_contact(group_chat_users['C_USER']['public_key']) - if chat.connection_status.text != 'Offline': - self.errors.append('Offline status is not shown in 1-1 chat') - chat.get_back_to_home_view() - - public_chat = home_view.join_public_chat(home_view.get_public_chat_name()) - if public_chat.connection_status.text != 'Offline': - self.errors.append('Offline status is not shown in a public chat') - self.verify_no_errors() - - @marks.testrail_id(3695) - @marks.smoke_1 - def test_message_marked_as_sent_and_seen_1_1_chat(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1 = 'user_%s' % get_current_time() - device_1_home, device_2_home = device_1.create_user(username=username_1), device_2.create_user() - device_2_public_key = device_2_home.get_public_key() - device_2_home.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key) - - message = 'test message' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - if device_1_chat.chat_element_by_text(message).status.text != 'Sent': - self.errors.append("'Sent' status is not shown under the sent text message") - - device_2_chat = device_2_home.get_chat_with_user(username_1).click() - device_2_chat.chat_element_by_text(message).wait_for_visibility_of_element() - - if device_1_chat.chat_element_by_text(message).status.text != 'Seen': - self.errors.append("'Seen' status is not shown under the text message which was read by a receiver") - self.verify_no_errors() - - @marks.testrail_id(3784) - def test_unread_messages_counter_1_1_chat(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_2 = 'user_%s' % get_current_time() - device_1_home, device_2_home = device_1.create_user(), device_2.create_user(username=username_2) - device_1_public_key = device_1_home.get_public_key() - device_1_home.home_button.click() - - device_2_chat = device_2_home.add_contact(device_1_public_key) - - message = 'test message' - device_2_chat.chat_message_input.send_keys(message) - device_2_chat.send_message_button.click() - - if device_1_home.home_button.counter.text != '1': - self.errors.append('New messages counter is not shown on Home button') - - chat_element = device_1_home.get_chat_with_user(username_2) - if chat_element.new_messages_counter.text != '1': - self.errors.append('New messages counter is not shown on chat element') - - chat_element.click() - device_1_home.get_back_to_home_view() - - if device_1_home.home_button.counter.is_element_displayed(): - self.errors.append('New messages counter is shown on Home button for already seen message') - - if chat_element.new_messages_counter.is_element_displayed(): - self.errors.append('New messages counter is shown on chat element for already seen message') - self.verify_no_errors() - - @marks.testrail_id(1414) - def test_bold_and_italic_text_in_messages(self): - self.create_drivers(2) - sign_in_1, sign_in_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_2 = 'user_%s' % get_current_time() - device_1_home, device_2_home = sign_in_1.create_user(), sign_in_2.create_user(username=username_2) - device_1_public_key = device_1_home.get_public_key() - device_1_home.home_button.click() - - device_2_chat = device_2_home.add_contact(device_1_public_key) - - bold_text = 'bold text' - device_2_chat.chat_message_input.send_keys('*%s*' % bold_text) - device_2_chat.send_message_button.click() - if not device_2_chat.chat_element_by_text(bold_text).is_element_displayed(): - self.errors.append('Bold text is not displayed in 1-1 chat for the sender') - - device_1_chat = device_1_home.get_chat_with_user(username_2).click() - if not device_1_chat.chat_element_by_text(bold_text).is_element_displayed(): - self.errors.append('Bold text is not displayed in 1-1 chat for the recipient') - - italic_text = 'italic text' - device_2_chat.chat_message_input.send_keys('~%s~' % italic_text) - device_2_chat.send_message_button.click() - if not device_2_chat.chat_element_by_text(italic_text).is_element_displayed(): - self.errors.append('Italic text is not displayed in 1-1 chat for the sender') - - if not device_1_chat.chat_element_by_text(italic_text).is_element_displayed(): - self.errors.append('Italic text is not displayed in 1-1 chat for the recipient') - - device_1_chat.get_back_to_home_view() - device_2_chat.get_back_to_home_view() - chat_name = device_1_home.get_public_chat_name() - device_1_home.join_public_chat(chat_name) - device_2_home.join_public_chat(chat_name) - - device_2_chat.chat_message_input.send_keys('*%s*' % bold_text) - device_2_chat.send_message_button.click() - if not device_2_chat.chat_element_by_text(bold_text).is_element_displayed(): - self.errors.append('Bold text is not displayed in public chat for the sender') - - if not device_1_chat.chat_element_by_text(bold_text).is_element_displayed(): - self.errors.append('Bold text is not displayed in public chat for the recipient') - - device_2_chat.chat_message_input.send_keys('~%s~' % italic_text) - device_2_chat.send_message_button.click() - if not device_2_chat.chat_element_by_text(italic_text).is_element_displayed(): - self.errors.append('Italic text is not displayed in public chat for the sender') - - if not device_1_chat.chat_element_by_text(italic_text).is_element_displayed(): - self.errors.append('Italic text is not displayed in 1-1 chat for the recipient') - - self.verify_no_errors() - - @marks.skip - @marks.testrail_id(2781) - def test_timestamp_in_chats(self): - self.create_drivers(2) - device_1, device_2 = self.drivers[0], self.drivers[1] - sign_in_1, sign_in_2 = SignInView(device_1), SignInView(device_2) - username_1 = 'user_%s' % get_current_time() - device_1_home, device_2_home = sign_in_1.create_user(username=username_1), sign_in_2.create_user() - device_2_public_key = device_2_home.get_public_key() - device_2_home.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key) - - message = 'test text' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - sent_time = datetime.strptime(device_1.device_time, '%a %b %d %H:%M:%S GMT %Y').strftime("%I:%M %p") - if not device_1_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in 1-1 chat for the sender') - if device_1_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is displayed in 1-1 chat for the sender') - - device_2_chat = device_2_home.get_chat_with_user(username_1).click() - if not device_2_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in 1-1 chat for the recipient') - if not device_2_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is not displayed in 1-1 chat for the recipient') - - device_1_chat.get_back_to_home_view() - device_2_chat.get_back_to_home_view() - chat_name = device_1_home.get_public_chat_name() - device_1_home.join_public_chat(chat_name) - device_2_home.join_public_chat(chat_name) - - device_2_chat.chat_message_input.send_keys(message) - device_2_chat.send_message_button.click() - sent_time = datetime.strptime(device_2.device_time, '%a %b %d %H:%M:%S GMT %Y').strftime("%I:%M %p") - if not device_2_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in public chat for the sender') - if device_2_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is displayed in public chat for the sender') - - if not device_1_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in public chat for the recipient') - if not device_1_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is not displayed in 1-1 chat for the recipient') - - self.verify_no_errors() - - -@marks.all -@marks.chat -class TestMessagesOneToOneChatSingle(SingleDeviceTestCase): - - @marks.testrail_id(1390) - @marks.smoke_1 - def test_copy_and_paste_messages(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - - home.join_public_chat(''.join(random.choice(string.ascii_lowercase) for _ in range(7))) - chat = sign_in.get_chat_view() - message_text = 'test' - message_input = chat.chat_message_input - message_input.send_keys(message_text) - chat.send_message_button.click() - - chat.chat_element_by_text(message_text).long_press_element() - chat.element_by_text('Copy to clipboard').click() - - message_input.paste_text_from_clipboard() - if message_input.text != message_text: - self.errors.append('Message text was not copied in a public chat') - - chat.get_back_to_home_view() - home.add_contact(group_chat_users['A_USER']['public_key']) - message_input.send_keys(message_text) - chat.send_message_button.click() - - chat.chat_element_by_text(message_text).long_press_element() - chat.element_by_text('Copy to clipboard').click() - - message_input.paste_text_from_clipboard() - if message_input.text != message_text: - self.errors.append('Message text was not copied in 1-1 chat') - self.verify_no_errors() - - @marks.testrail_id(1398) - @marks.smoke_1 - def test_delete_cut_and_paste_messages(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - chat = home.add_contact(group_chat_users['B_USER']['public_key']) - - message_text = 'test' - message_input = chat.chat_message_input - message_input.send_keys(message_text) - - message_input.delete_last_symbols(2) - assert message_input.text == message_text[:-2] - - message_input.cut_text() - - message_input.paste_text_from_clipboard() - chat.send_message_button.click() - - chat.chat_element_by_text(message_text[:-2] + ' ').wait_for_visibility_of_element(2) - - @marks.testrail_id(2106) - @marks.smoke_1 - def test_send_emoji(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - - home.join_public_chat(home.get_public_chat_name()) - chat = sign_in.get_chat_view() - emoji_name = random.choice(list(emoji.EMOJI_UNICODE)) - emoji_unicode = emoji.EMOJI_UNICODE[emoji_name] - chat.chat_message_input.send_keys(emoji.emojize(emoji_name)) - chat.send_message_button.click() - - if not chat.chat_element_by_text(emoji_unicode).is_element_displayed(): - self.errors.append('Message with emoji was not sent in public chat') - - chat.get_back_to_home_view() - home.add_contact(group_chat_users['C_USER']['public_key']) - chat.chat_message_input.send_keys(emoji.emojize(emoji_name)) - chat.send_message_button.click() - - if not chat.chat_element_by_text(emoji_unicode).is_element_displayed(): - self.errors.append('Message with emoji was not sent in 1-1 chat') - self.verify_no_errors() diff --git a/test/appium/tests/atomic/chats/test_public.py b/test/appium/tests/atomic/chats/test_public.py deleted file mode 100644 index 2fd9879618c..00000000000 --- a/test/appium/tests/atomic/chats/test_public.py +++ /dev/null @@ -1,121 +0,0 @@ -import pytest -from tests import marks -from tests.base_test_case import MultipleDeviceTestCase, SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.chat -class TestPublicChatMultipleDevice(MultipleDeviceTestCase): - - @marks.testrail_id(1383) - @marks.smoke_1 - def test_public_chat_messaging(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - username_1, username_2 = 'user_1', 'user_2' - home_1, home_2 = device_1.create_user(username=username_1), device_2.create_user(username=username_2) - public_key_2 = home_2.get_public_key() - home_2.home_button.click() - - home_1.add_contact(public_key_2) - home_1.get_back_to_home_view() - - public_chat_name = home_1.get_public_chat_name() - chat_1, chat_2 = home_1.join_public_chat(public_chat_name), home_2.join_public_chat(public_chat_name) - - message = 'hello' - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - - chat_2.verify_message_is_under_today_text(message, self.errors) - if chat_2.chat_element_by_text(message).username.text != username_1: - self.errors.append("Username '%s' is not shown next to the received message" % username_1) - - if chat_1.element_by_text(username_1).is_element_displayed(): - self.errors.append("Username '%s' is shown for the sender" % username_1) - - self.verify_no_errors() - - @marks.testrail_id(3706) - def test_public_chat_clear_history(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - chat_name = device_1.get_public_chat_name() - for sign_in in device_1, device_2: - home = sign_in.create_user() - home.join_public_chat(chat_name) - chat_1, chat_2 = device_1.get_chat_view(), device_2.get_chat_view() - message_1, message_2, message_3 = 'm1', 'm2', 'm3' - chat_1.chat_message_input.send_keys(message_1) - chat_1.send_message_button.click() - chat_2.element_by_text(message_1).is_element_present() - - chat_2.chat_message_input.send_keys(message_2) - chat_2.send_message_button.click() - chat_1.element_by_text(message_2).is_element_present() - chat_1.chat_options.click() - chat_1.clear_history_button.click() - chat_1.clear_button.click() - chat_2.chat_message_input.send_keys(message_3) - chat_2.send_message_button.click() - chat_1.element_by_text(message_3).is_element_present() - for message in message_1, message_2: - if chat_1.element_starts_with_text(message).is_element_present(): - pytest.fail("Message '%s' is shown, but public chat history has been cleared" % message) - home_1 = chat_1.get_back_to_home_view() - home_1.relogin() - home_1.element_by_text('#' + chat_name).click() - for message in message_1, message_2: - if chat_1.element_starts_with_text(message).is_element_present(): - pytest.fail("Message '%s' is shown after re-login, but public chat history has been cleared" % message) - - @marks.testrail_id(3729) - def test_unread_messages_counter_public_chat(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1, home_2 = device_1.create_user(), device_2.create_user() - - chat_name = home_1.get_public_chat_name() - chat_1, chat_2 = home_1.join_public_chat(chat_name), home_2.join_public_chat(chat_name) - home_1.get_back_to_home_view() - - message = 'test message' - chat_2.chat_message_input.send_keys(message) - chat_2.send_message_button.click() - - if home_1.home_button.counter.text != '1': - self.errors.append('New messages counter is not shown on Home button') - - chat_element = home_1.get_chat_with_user('#' + chat_name) - if chat_element.new_messages_counter.text != '1': - self.errors.append('New messages counter is not shown on chat element') - - chat_element.click() - home_1.get_back_to_home_view() - - if home_1.home_button.counter.is_element_displayed(): - self.errors.append('New messages counter is shown on Home button for already seen message') - - if chat_element.new_messages_counter.is_element_displayed(): - self.errors.append('New messages counter is shown on chat element for already seen message') - self.verify_no_errors() - - -@marks.chat -class TestPublicChatSingleDevice(SingleDeviceTestCase): - - @marks.skip - @marks.testrail_id(3752) - def test_send_korean_characters(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - chat_name = home.get_public_chat_name() - public_chat = home.join_public_chat(chat_name) - message = '파란하늘' - public_chat.chat_message_input.send_keys(message) - if public_chat.chat_message_input.text != message: - self.errors.append('Korean characters are not displayed properly in the chat message input') - public_chat.send_message_button.click() - if not public_chat.chat_element_by_text(message).is_element_displayed(): - self.errors.append('Message with korean characters is not shown') - self.verify_no_errors() diff --git a/test/appium/tests/atomic/dapps_and_browsing/test_browsing.py b/test/appium/tests/atomic/dapps_and_browsing/test_browsing.py deleted file mode 100644 index 07a317f9f4f..00000000000 --- a/test/appium/tests/atomic/dapps_and_browsing/test_browsing.py +++ /dev/null @@ -1,128 +0,0 @@ -import pytest -from tests import marks, connection_not_secure_text, connection_is_secure_text -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@pytest.mark.all -class TestBrowsing(SingleDeviceTestCase): - - @marks.testrail_id(1411) - def test_browse_page_with_non_english_text(self): - sign_in = SignInView(self.driver) - home_view = sign_in.create_user() - start_new_chat = home_view.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.enter_url_editbox.set_value('www.wikipedia.org') - start_new_chat.confirm() - browsing_view = home_view.get_base_web_view() - browsing_view.wait_for_d_aap_to_load() - wiki_texts = ['Español', '日本語', 'Français', '中文', 'Português'] - for wiki_text in wiki_texts: - browsing_view.find_text_part(wiki_text, 15) - - @marks.testrail_id(2174) - def test_open_invalid_link(self): - sign_in = SignInView(self.driver) - home_view = sign_in.create_user() - start_new_chat = home_view.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.enter_url_editbox.set_value('invalid.takoe') - start_new_chat.confirm() - browsing_view = home_view.get_base_web_view() - browsing_view.find_text_part('Unable to load page') - browsing_view.cross_icon.click() - if home_view.element_by_text('Browser').is_element_displayed(): - pytest.fail('Browser entity is shown for an invalid link') - - @marks.testrail_id(3705) - def test_connection_is_not_secure(self): - sign_in = SignInView(self.driver) - home_view = sign_in.create_user() - start_new_chat = home_view.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.enter_url_editbox.set_value('www.bbc.com') - start_new_chat.confirm() - browsing_view = home_view.get_base_web_view() - browsing_view.url_edit_box_lock_icon.click() - browsing_view.find_full_text(connection_not_secure_text) - - @marks.testrail_id(3814) - def test_connection_is_secure(self): - sign_in = SignInView(self.driver) - home_view = sign_in.create_user() - start_new_chat = home_view.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.enter_url_editbox.set_value('https://www.bbc.com') - start_new_chat.confirm() - browsing_view = home_view.get_base_web_view() - browsing_view.url_edit_box_lock_icon.click() - browsing_view.find_full_text(connection_is_secure_text) - browsing_view.cross_icon.click() - start_new_chat_view = home_view.plus_button.click() - start_new_chat_view.open_d_app_button.click() - start_new_chat_view.element_by_text('Airswap').click() - start_new_chat_view.open_button.click() - browsing_view.url_edit_box_lock_icon.click() - browsing_view.find_full_text(connection_is_secure_text) - - @marks.testrail_id(3731) - def test_swipe_to_delete_browser_entry(self): - sign_in = SignInView(self.driver) - home_view = sign_in.create_user() - start_new_chat = home_view.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.enter_url_editbox.set_value('google.com') - start_new_chat.confirm() - browsing_view = home_view.get_base_web_view() - browsing_view.cross_icon.click() - home_view.get_chat_with_user('Browser').swipe_and_delete() - home_view.relogin() - if home_view.get_chat_with_user('Browser').is_element_present(20): - pytest.fail('The browser entry is present after re-login') - - @marks.testrail_id(1396) - @marks.smoke_1 - def test_open_google_com_via_open_dapp(self): - sign_in_view = SignInView(self.driver) - home = sign_in_view.create_user() - start_new_chat = home.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.enter_url_editbox.set_value('google.com') - start_new_chat.confirm() - browsing_view = start_new_chat.get_base_web_view() - browsing_view.wait_for_d_aap_to_load() - assert browsing_view.element_by_text('Google').is_element_displayed() - - @marks.testrail_id(1397) - @marks.smoke_1 - def test_back_forward_buttons_browsing_website(self): - sign_in = SignInView(self.driver) - home = sign_in.create_user() - start_new_chat = home.plus_button.click() - start_new_chat.open_d_app_button.click() - start_new_chat.enter_url_editbox.set_value('www.wikipedia.org') - start_new_chat.confirm() - browsing_view = start_new_chat.get_base_web_view() - browsing_view.wait_for_d_aap_to_load() - - browsing_view.element_by_text_part('Русский', 'button').click() - browsing_view.find_text_part('Избранная статья') - browsing_view.browser_previous_page_button.click() - browsing_view.find_text_part('English', 15) - - browsing_view.browser_next_page_button.click() - browsing_view.find_text_part('Избранная статья') - browsing_view.back_to_home_button.click() - - @marks.testrail_id(3783) - @marks.smoke_1 - def test_refresh_button_browsing_app_webview(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.transactions_button.click() - status_test_dapp.find_full_text('Sign message') - status_test_dapp.browser_refresh_page_button.click() - status_test_dapp.find_full_text('defaultAccount') diff --git a/test/appium/tests/atomic/dapps_and_browsing/test_dapps.py b/test/appium/tests/atomic/dapps_and_browsing/test_dapps.py deleted file mode 100644 index b5fcf8b6e77..00000000000 --- a/test/appium/tests/atomic/dapps_and_browsing/test_dapps.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -from tests import marks, basic_user -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -@pytest.mark.all -class TestDApps(SingleDeviceTestCase): - - @marks.testrail_id(3782) - @marks.smoke_1 - def test_filters_from_daap(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.transactions_button.click() - status_test_dapp.test_filters_button.click() - for element in status_test_dapp.element_by_text('eth_uninstallFilter'), status_test_dapp.ok_button: - if element.is_element_displayed(10): - pytest.fail("'Test filters' button produced an error") - - @marks.testrail_id(3789) - def test_request_public_key_status_test_daap(self): - user = basic_user - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(passphrase=user['passphrase'], password=user['password']) - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.status_api_button.click() - status_test_dapp.request_contact_code_button.click() - status_test_dapp.do_not_allow_button.click() - if status_test_dapp.element_by_text(user['public_key']).is_element_displayed(): - pytest.fail('Public key is returned but access was not allowed') - status_test_dapp.request_contact_code_button.click() - status_test_dapp.ok_button.click() - if not status_test_dapp.element_by_text(user['public_key']).is_element_displayed(): - pytest.fail('Public key is not returned') diff --git a/test/appium/tests/atomic/dapps_and_browsing/test_deep_links.py b/test/appium/tests/atomic/dapps_and_browsing/test_deep_links.py deleted file mode 100644 index c101d53b13b..00000000000 --- a/test/appium/tests/atomic/dapps_and_browsing/test_deep_links.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -from selenium.common.exceptions import NoSuchElementException - -from support.device_apps import start_web_browser -from tests import marks -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -class TestDeepLinks(SingleDeviceTestCase): - - @marks.testrail_id(3781) - def test_open_public_chat_using_deep_link(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - self.driver.close_app() - start_web_browser(self.driver) - chat_name = sign_in_view.get_public_chat_name() - sign_in_view.send_as_keyevent('https://get.status.im/chat/public/%s' % chat_name) - sign_in_view.confirm() - open_button = sign_in_view.element_by_text('Open in Status') - open_button.wait_for_visibility_of_element() - open_button.click() - sign_in_view.sign_in() - chat_view = sign_in_view.get_chat_view() - try: - assert chat_view.user_name_text.text == '#' + chat_name - except (AssertionError, NoSuchElementException): - pytest.fail("Public chat '%s' is not opened" % chat_name) diff --git a/test/appium/tests/atomic/transactions/test_daaps_transactions.py b/test/appium/tests/atomic/transactions/test_daaps_transactions.py deleted file mode 100644 index 147db720f13..00000000000 --- a/test/appium/tests/atomic/transactions/test_daaps_transactions.py +++ /dev/null @@ -1,106 +0,0 @@ -import pytest - -from tests import transaction_users, marks, common_password -from tests.base_test_case import SingleDeviceTestCase -from views.sign_in_view import SignInView - - -class TestTransactionDApp(SingleDeviceTestCase): - - @marks.testrail_id(769) - @marks.smoke_1 - def test_send_transaction_from_daap(self): - sender = transaction_users['B_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - address = transaction_users['B_USER']['address'] - initial_balance = self.network_api.get_balance(address) - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.assets_button.click() - send_transaction_view = status_test_dapp.request_stt_button.click() - wallet_view = send_transaction_view.get_wallet_view() - wallet_view.done_button.click() - wallet_view.yes_button.click() - send_transaction_view.sign_transaction(sender['password']) - self.network_api.verify_balance_is_updated(initial_balance, address) - - @marks.testrail_id(3716) - @marks.smoke_1 - def test_sign_message_from_daap(self): - password = 'password_for_daap' - sign_in_view = SignInView(self.driver) - sign_in_view.create_user(password=password) - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.transactions_button.click() - send_transaction_view = status_test_dapp.sign_message_button.click() - send_transaction_view.find_full_text('Test message') - send_transaction_view.sign_transaction_button.click_until_presence_of_element( - send_transaction_view.enter_password_input) - send_transaction_view.enter_password_input.send_keys(password) - send_transaction_view.sign_transaction_button.click() - - @marks.testrail_id(3696) - @marks.smoke_1 - def test_deploy_contract_from_daap(self): - sender = transaction_users['E_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.transactions_button.click() - send_transaction_view = status_test_dapp.deploy_contract_button.click() - send_transaction_view.sign_transaction(sender['password']) - for text in 'Contract deployed at: ', 'Call contract get function', \ - 'Call contract set function', 'Call function 2 times in a row': - if not status_test_dapp.element_by_text(text).is_element_displayed(120): - pytest.fail('Contract was not created') - - @marks.logcat - @marks.testrail_id(3772) - def test_logcat_send_transaction_from_daap(self): - sender = transaction_users['B_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.assets_button.click() - send_transaction_view = status_test_dapp.request_stt_button.click() - send_transaction_view.sign_transaction(sender['password']) - send_transaction_view.check_no_values_in_logcat(password=sender['password']) - - @marks.logcat - @marks.testrail_id(3775) - def test_logcat_sign_message_from_daap(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.transactions_button.click() - send_transaction_view = status_test_dapp.sign_message_button.click() - send_transaction_view.sign_transaction_button.click_until_presence_of_element( - send_transaction_view.enter_password_input) - send_transaction_view.enter_password_input.send_keys(common_password) - send_transaction_view.sign_transaction_button.click() - send_transaction_view.check_no_values_in_logcat(password=common_password) - - @marks.testrail_id(1380) - @marks.smoke_1 - def test_request_eth_in_status_test_dapp(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - status_test_dapp = sign_in_view.open_status_test_dapp() - status_test_dapp.wait_for_d_aap_to_load() - status_test_dapp.assets_button.click() - status_test_dapp.request_eth_button.click() - status_test_dapp.element_by_text('Faucet request recieved').wait_for_visibility_of_element() - status_test_dapp.ok_button.click() - status_test_dapp.cross_icon.click() - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - wallet_view.wait_balance_changed_on_wallet_screen() diff --git a/test/appium/tests/atomic/transactions/test_wallet.py b/test/appium/tests/atomic/transactions/test_wallet.py deleted file mode 100644 index 9049e5b7450..00000000000 --- a/test/appium/tests/atomic/transactions/test_wallet.py +++ /dev/null @@ -1,401 +0,0 @@ -import random - -import pytest - -from tests import transaction_users, transaction_users_wallet, marks, common_password -from tests.base_test_case import SingleDeviceTestCase, MultipleDeviceTestCase -from views.sign_in_view import SignInView - - -@marks.transaction -class TestTransactionWalletSingleDevice(SingleDeviceTestCase): - - @marks.testrail_id(766) - @marks.smoke_1 - def test_send_eth_from_wallet_to_contact(self): - recipient = transaction_users['F_USER'] - sender = transaction_users['E_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - home_view = sign_in_view.get_home_view() - home_view.add_contact(recipient['public_key']) - home_view.get_back_to_home_view() - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.click() - transaction_amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(transaction_amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.recent_recipients_button.click() - recent_recipient = send_transaction.element_by_text(recipient['username']) - send_transaction.recent_recipients_button.click_until_presence_of_element(recent_recipient) - recent_recipient.click() - send_transaction.sign_transaction(sender['password']) - self.network_api.find_transaction_by_unique_amount(sender['address'], transaction_amount) - - @marks.testrail_id(767) - @marks.smoke_1 - def test_send_eth_from_wallet_to_address(self): - recipient = transaction_users['E_USER'] - sender = transaction_users['F_USER'] - sign_in_view = SignInView(self.driver) - home_view = sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.click() - transaction_amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(transaction_amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient['address']) - send_transaction.done_button.click() - send_transaction.sign_transaction(sender['password']) - self.network_api.find_transaction_by_unique_amount(sender['address'], transaction_amount) - - @marks.testrail_id(1430) - @marks.smoke_1 - def test_send_stt_from_wallet(self): - sender = transaction_users_wallet['A_USER'] - recipient = transaction_users_wallet['B_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - home_view = sign_in_view.get_home_view() - home_view.add_contact(recipient['public_key']) - home_view.get_back_to_home_view() - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - stt_button = send_transaction.asset_by_name('STT') - send_transaction.select_asset_button.click_until_presence_of_element(stt_button) - stt_button.click() - send_transaction.amount_edit_box.click() - amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient['address']) - send_transaction.done_button.click() - send_transaction.sign_transaction(sender['password']) - self.network_api.find_transaction_by_unique_amount(recipient['address'], amount, token=True) - - @marks.testrail_id(2164) - def test_transaction_wrong_password_wallet(self): - recipient = transaction_users['E_USER'] - sender = transaction_users['F_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - home_view = sign_in_view.get_home_view() - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.click() - transaction_amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(transaction_amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient['address']) - send_transaction.done_button.click() - send_transaction.sign_transaction_button.click() - send_transaction.enter_password_input.click() - send_transaction.enter_password_input.send_keys('wrong_password') - send_transaction.sign_transaction_button.click() - send_transaction.find_full_text('Wrong password', 20) - - @marks.testrail_id(1452) - def test_transaction_appears_in_history(self): - recipient = transaction_users['B_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - home_view = sign_in_view.get_home_view() - transaction_amount = home_view.get_unique_amount() - sender_public_key = home_view.get_public_key() - sender_address = home_view.public_key_to_address(sender_public_key) - home_view.home_button.click() - self.network_api.get_donate(sender_address) - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - wallet_view.wait_balance_changed_on_wallet_screen() - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.click() - send_transaction.amount_edit_box.set_value(transaction_amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient['address']) - send_transaction.done_button.click() - send_transaction.sign_transaction(common_password) - self.network_api.find_transaction_by_unique_amount(recipient['address'], transaction_amount) - transactions_view = wallet_view.transaction_history_button.click() - transactions_view.transactions_table.find_transaction(amount=transaction_amount) - - @marks.testrail_id(2163) - def test_send_eth_from_wallet_incorrect_address(self): - recipient = transaction_users['E_USER'] - sender = transaction_users['F_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - home_view = sign_in_view.get_home_view() - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.click() - transaction_amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(transaction_amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient['public_key']) - send_transaction.done_button.click() - send_transaction.find_text_part('Invalid address:', 20) - - @marks.logcat - @marks.testrail_id(3770) - def test_logcat_send_transaction_from_wallet(self): - sender = transaction_users['E_USER'] - recipient = transaction_users['F_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - home_view = sign_in_view.get_home_view() - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.click() - transaction_amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(transaction_amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient['address']) - send_transaction.done_button.click() - send_transaction.sign_transaction(sender['password']) - send_transaction.check_no_values_in_logcat(password=sender['password']) - - @marks.testrail_id(3746) - @marks.smoke_1 - def test_send_token_with_7_decimals(self): - sender = transaction_users_wallet['A_USER'] - recipient = transaction_users_wallet['B_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - home_view = sign_in_view.get_home_view() - home_view.add_contact(recipient['public_key']) - home_view.get_back_to_home_view() - wallet_view = home_view.wallet_button.click() - wallet_view.set_up_wallet() - wallet_view.options_button.click() - wallet_view.manage_assets_button.click() - wallet_view.asset_checkbox_by_name('ADI').click() - wallet_view.done_button.click() - send_transaction = wallet_view.send_transaction_button.click() - adi_button = send_transaction.asset_by_name('ADI') - send_transaction.select_asset_button.click_until_presence_of_element(adi_button) - adi_button.click() - send_transaction.amount_edit_box.click() - amount = '0.0%s' % str(random.randint(100000, 999999)).strip('0') - send_transaction.amount_edit_box.set_value(amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient['address']) - send_transaction.done_button.click() - send_transaction.sign_transaction(sender['password']) - self.network_api.find_transaction_by_unique_amount(sender['address'], amount, token=True, decimals=7) - - @marks.testrail_id(3747) - @marks.smoke_1 - def test_token_with_more_than_allowed_decimals(self): - sender = transaction_users_wallet['A_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - wallet_view.options_button.click() - wallet_view.manage_assets_button.click() - wallet_view.asset_checkbox_by_name('ADI').click() - wallet_view.done_button.click() - send_transaction = wallet_view.send_transaction_button.click() - adi_button = send_transaction.asset_by_name('ADI') - send_transaction.select_asset_button.click_until_presence_of_element(adi_button) - adi_button.click() - send_transaction.amount_edit_box.click() - amount = '0.0%s' % str(random.randint(1000000, 9999999)).strip('0') - send_transaction.amount_edit_box.set_value(amount) - error_text = 'Amount is too precise. Max number of decimals is 7.' - if not send_transaction.element_by_text(error_text).is_element_displayed(): - self.errors.append('Warning about too precise amount is not shown when sending a transaction') - send_transaction.back_button.click() - wallet_view.receive_transaction_button.click() - wallet_view.send_transaction_request.click() - send_transaction.select_asset_button.click_until_presence_of_element(adi_button) - adi_button.click() - send_transaction.amount_edit_box.set_value(amount) - error_text = 'Amount is too precise. Max number of decimals is 7.' - if not send_transaction.element_by_text(error_text).is_element_displayed(): - self.errors.append('Warning about too precise amount is not shown when requesting a transaction') - self.verify_no_errors() - - @marks.testrail_id(1405) - def test_send_valid_amount_after_insufficient_funds_error(self): - sender = transaction_users['H_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - bigger_amount = wallet_view.get_eth_value() + 1 - send_transaction = wallet_view.send_transaction_button.click() - amount_edit_box = send_transaction.amount_edit_box - amount_edit_box.click() - amount_edit_box.set_value(bigger_amount) - send_transaction.element_by_text('Insufficient funds').wait_for_visibility_of_element(5) - - valid_amount = send_transaction.get_unique_amount() - amount_edit_box.clear() - amount_edit_box.set_value(valid_amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(transaction_users['G_USER']['address']) - send_transaction.done_button.click() - send_transaction.sign_transaction(sender['password']) - self.network_api.find_transaction_by_unique_amount(sender['address'], valid_amount) - - @marks.testrail_id(3764) - def test_insufficient_funds_wallet_0_balance(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.set_value(1) - error_text = send_transaction.element_by_text('Insufficient funds') - if not error_text.is_element_displayed(): - self.errors.append("'Insufficient funds' error is now shown when sending 1 ETH from wallet with balance 0") - send_transaction.select_asset_button.click() - send_transaction.asset_by_name('STT').click() - send_transaction.amount_edit_box.set_value(1) - if not error_text.is_element_displayed(): - self.errors.append("'Insufficient funds' error is now shown when sending 1 STT from wallet with balance 0") - self.verify_no_errors() - - @marks.testrail_id(3792) - def test_insufficient_funds_wallet_positive_balance(self): - sender = transaction_users_wallet['A_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - eth_value = wallet_view.get_eth_value() - stt_value = wallet_view.get_stt_value() - if eth_value == 0 or stt_value == 0: - pytest.fail('No funds!') - send_transaction = wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.set_value(round(eth_value + 1)) - error_text = send_transaction.element_by_text('Insufficient funds') - if not error_text.is_element_displayed(): - self.errors.append( - "'Insufficient funds' error is now shown when sending %s ETH from wallet with balance %s" % ( - round(eth_value + 1), eth_value)) - send_transaction.select_asset_button.click() - send_transaction.asset_by_name('STT').click() - send_transaction.amount_edit_box.set_value(round(stt_value + 1)) - if not error_text.is_element_displayed(): - self.errors.append( - "'Insufficient funds' error is now shown when sending %s STT from wallet with balance %s" % ( - round(stt_value + 1), stt_value)) - self.verify_no_errors() - - @marks.testrail_id(3728) - def test_modify_transaction_fee_values(self): - sender = transaction_users['H_USER'] - sign_in_view = SignInView(self.driver) - sign_in_view.recover_access(sender['passphrase'], sender['password']) - wallet_view = sign_in_view.wallet_button.click() - wallet_view.set_up_wallet() - send_transaction = wallet_view.send_transaction_button.click() - - amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - recipient_address = transaction_users['G_USER']['address'] - send_transaction.enter_recipient_address_input.set_value(recipient_address) - send_transaction.done_button.click() - send_transaction.advanced_button.click() - send_transaction.transaction_fee_button.click() - send_transaction.gas_limit_input.clear() - send_transaction.gas_limit_input.set_value('1') - send_transaction.gas_price_input.clear() - send_transaction.gas_price_input.set_value('1') - send_transaction.total_fee_input.click() - send_transaction.done_button.click() - send_transaction.sign_transaction_button.click_until_presence_of_element(send_transaction.enter_password_input) - send_transaction.enter_password_input.send_keys(sender['password']) - send_transaction.sign_transaction_button.click() - send_transaction.element_by_text('intrinsic gas too low').wait_for_visibility_of_element() - send_transaction.ok_button.click() - - wallet_view.send_transaction_button.click() - send_transaction.amount_edit_box.set_value(amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.enter_recipient_address_button.click() - send_transaction.enter_recipient_address_input.set_value(recipient_address) - send_transaction.done_button.click() - - send_transaction.advanced_button.click() - send_transaction.transaction_fee_button.click() - send_transaction.gas_limit_input.clear() - gas_limit = '1005000' - send_transaction.gas_limit_input.set_value(gas_limit) - send_transaction.gas_price_input.clear() - gas_price = '24' - send_transaction.gas_price_input.set_value(gas_price) - send_transaction.total_fee_input.click() - send_transaction.done_button.click() - send_transaction.sign_transaction(sender['password']) - self.network_api.find_transaction_by_unique_amount(sender['address'], amount) - - -@marks.transaction -class TestTransactionWalletMultipleDevice(MultipleDeviceTestCase): - - @marks.testrail_id(3761) - @marks.smoke_1 - def test_transaction_message_sending_from_wallet(self): - recipient = transaction_users['D_USER'] - sender = transaction_users['C_USER'] - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1 = device_1.recover_access(passphrase=sender['passphrase'], password=sender['password']) - home_2 = device_2.recover_access(passphrase=recipient['passphrase'], password=recipient['password']) - - chat_1 = home_1.add_contact(recipient['public_key']) - chat_1.get_back_to_home_view() - - wallet_1 = home_1.wallet_button.click() - wallet_1.set_up_wallet() - send_transaction = wallet_1.send_transaction_button.click() - send_transaction.amount_edit_box.click() - amount = send_transaction.get_unique_amount() - send_transaction.amount_edit_box.set_value(amount) - send_transaction.confirm() - send_transaction.chose_recipient_button.click() - send_transaction.recent_recipients_button.click() - send_transaction.element_by_text_part(recipient['username']).click() - send_transaction.sign_transaction(sender['password']) - - wallet_1.home_button.click() - home_1.get_chat_with_user(recipient['username']).click() - if not chat_1.chat_element_by_text(amount).is_element_displayed(): - self.errors.append('Transaction message is not shown in 1-1 chat for the sender') - chat_2 = home_2.get_chat_with_user(sender['username']).click() - if not chat_2.chat_element_by_text(amount).is_element_displayed(): - self.errors.append('Transaction message is not shown in 1-1 chat for the recipient') - self.verify_no_errors() diff --git a/test/appium/tests/base_test_case.py b/test/appium/tests/base_test_case.py index d58fcd30ab4..c981ec8147e 100644 --- a/test/appium/tests/base_test_case.py +++ b/test/appium/tests/base_test_case.py @@ -1,94 +1,112 @@ -import pytest -import sys -import re -import subprocess import asyncio +import base64 +import logging +import re +import sys +from abc import ABCMeta, abstractmethod +from http.client import RemoteDisconnected -from support.message_reliability_report import create_one_to_one_chat_report, create_public_chat_report -from support.api.network_api import NetworkApi -from os import environ +import pytest +import requests from appium import webdriver -from abc import ABCMeta, abstractmethod -from selenium.common.exceptions import WebDriverException -from tests import test_suite_data, start_threads +from appium.options.common import AppiumOptions +from appium.webdriver.common.mobileby import MobileBy +from appium.webdriver.connectiontype import ConnectionType +from selenium.common.exceptions import NoSuchElementException, WebDriverException +from urllib3.exceptions import MaxRetryError, ProtocolError + +from support.api.network_api import NetworkApi +from tests import test_suite_data, start_threads, pytest_config_global, transl +from tests.conftest import github_report, run_name, lambda_test_username, lambda_test_access_key from views.base_view import BaseView +executor_lambda_test = 'https://%s:%s@mobile-hub.lambdatest.com/wd/hub' % (lambda_test_username, lambda_test_access_key) + +executor_local = 'http://localhost:4723/wd/hub' + +implicit_wait = 5 + +def get_app_package(): + """ + Determines the app package dynamically based on the APK configuration. + :return: The app package name. + """ + apk = pytest_config_global['apk'] + app_folder = "im.status.ethereum" # Default app package name + + if re.findall(r'pr\d\d\d\d\d', apk) or re.findall(r'\d\d\d\d\d.apk', apk): + app_folder += ".pr" # Append `.pr` in specific cases + + return app_folder + +def get_lambda_test_capabilities_real_device(): + capabilities = { + "lt:options": { + "w3c": True, + "platformName": "android", + "deviceName": "Pixel 8", + "platformVersion": "14", + "app": pytest_config_global['lt_apk_url'], + "devicelog": True, + "visual": True, + "video": True, + "build": run_name, + "name": test_suite_data.current_test.group_name, + "idleTimeout": 1000, + "isRealMobile": True + } + } + options = AppiumOptions() + options.load_capabilities(capabilities) + return options + + +def get_lambda_test_capabilities_emulator(platform_version: int = 14, device_name: str = "Pixel 6"): + capabilities = { + "lt:options": { + "w3c": True, + "platformName": "android", + "deviceName": device_name, + "appiumVersion": "2.1.3", + "platformVersion": str(platform_version), + "app": pytest_config_global['lt_apk_url'], + "devicelog": True, + "visual": True, + "video": True, + "build": run_name, + "name": test_suite_data.current_test.group_name, + "idleTimeout": 1000, + # "enableImageInjection": True, + # "uploadMedia": ["lt://MEDIA2b3e34e2b0ee4928b9fc38c603f98191",], # "lt://MEDIAcfc1b4f1af0740759254404186bbe4f1"] + }, + "appium:options": { + "automationName": "UiAutomator2", + "hideKeyboard": True + } + } + options = AppiumOptions() + options.load_capabilities(capabilities) + return options + + +def get_app_path(): + return '/storage/emulated/0/Android/data/%s/files/Download/' % get_app_package() + + +def pull_logs_folder(driver): + result = driver.pull_folder(get_app_path()) + return base64.b64decode(result) + class AbstractTestCase: __metaclass__ = ABCMeta - @property - def sauce_username(self): - return environ.get('SAUCE_USERNAME') - - @property - def sauce_access_key(self): - return environ.get('SAUCE_ACCESS_KEY') - - @property - def executor_sauce_lab(self): - return 'http://%s:%s@ondemand.saucelabs.com:80/wd/hub' % (self.sauce_username, self.sauce_access_key) - - @property - def executor_local(self): - return 'http://localhost:4723/wd/hub' - - def print_sauce_lab_info(self, driver): + def print_lt_session_info(self, driver): sys.stdout = sys.stderr - print("SauceOnDemandSessionID=%s job-name=%s" % (driver.session_id, - pytest.config.getoption('build'))) - - def add_local_devices_to_capabilities(self): - updated_capabilities = list() - raw_out = re.split(r'[\r\\n]+', str(subprocess.check_output(['adb', 'devices'])).rstrip()) - for line in raw_out[1:]: - serial = re.findall(r"(([\d.\d:]*\d+)|\bemulator-\d+)", line) - if serial: - capabilities = self.capabilities_local - capabilities['udid'] = serial[0][0] - updated_capabilities.append(capabilities) - return updated_capabilities - - @property - def capabilities_sauce_lab(self): - desired_caps = dict() - desired_caps['app'] = 'sauce-storage:' + test_suite_data.apk_name - - desired_caps['build'] = pytest.config.getoption('build') - desired_caps['name'] = test_suite_data.current_test.name - desired_caps['platformName'] = 'Android' - desired_caps['appiumVersion'] = '1.7.2' - desired_caps['platformVersion'] = '7.1' - desired_caps['deviceName'] = 'Android GoogleAPI Emulator' - desired_caps['deviceOrientation'] = "portrait" - desired_caps['commandTimeout'] = 600 - desired_caps['idleTimeout'] = 1000 - desired_caps['unicodeKeyboard'] = True - desired_caps['automationName'] = 'UiAutomator2' - desired_caps['setWebContentDebuggingEnabled'] = True - desired_caps['ignoreUnimportantViews'] = False - desired_caps['enableNotificationListener'] = True - return desired_caps - - def update_capabilities_sauce_lab(self, new_capabilities: dict): - caps = self.capabilities_sauce_lab.copy() - caps.update(new_capabilities) - return caps + print("LambdaTestSessionID=%s job-name=%s" % (driver.session_id, run_name)) - @property - def capabilities_local(self): - desired_caps = dict() - desired_caps['app'] = pytest.config.getoption('apk') - desired_caps['deviceName'] = 'nexus_5' - desired_caps['platformName'] = 'Android' - desired_caps['appiumVersion'] = '1.7.2' - desired_caps['platformVersion'] = '7.1' - desired_caps['newCommandTimeout'] = 600 - desired_caps['fullReset'] = False - desired_caps['unicodeKeyboard'] = True - desired_caps['automationName'] = 'UiAutomator2' - desired_caps['setWebContentDebuggingEnabled'] = True - return desired_caps + def get_translation_by_key(self, key): + return transl[key] @abstractmethod def setup_method(self, method): @@ -100,138 +118,224 @@ def teardown_method(self, method): @property def environment(self): - return pytest.config.getoption('env') - - @property - def implicitly_wait(self): - return 8 - - errors = [] + return pytest_config_global['env'] network_api = NetworkApi() - test_fairy_warning_is_shown = bool() - - def verify_no_errors(self): - if self.errors: - pytest.fail('. '.join([self.errors.pop(0) for _ in range(len(self.errors))])) - - -class SingleDeviceTestCase(AbstractTestCase): - - def setup_method(self, method): - capabilities = {'local': {'executor': self.executor_local, - 'capabilities': self.capabilities_local}, - 'sauce': {'executor': self.executor_sauce_lab, - 'capabilities': self.capabilities_sauce_lab}} - counter = 0 - self.driver = None - while not self.driver and counter <= 3: - try: - self.driver = webdriver.Remote(capabilities[self.environment]['executor'], - capabilities[self.environment]['capabilities']) - self.driver.implicitly_wait(self.implicitly_wait) - view = BaseView(self.driver) - view.accept_agreements() - self.test_fairy_warning_is_shown = view.test_fairy_warning.is_shown - test_suite_data.current_test.testruns[-1].jobs.append(self.driver.session_id) - break - except WebDriverException: - counter += 1 - - def teardown_method(self, method): - if self.environment == 'sauce': - self.print_sauce_lab_info(self.driver) + @staticmethod + def get_alert_text(driver): try: - self.driver.quit() - except (WebDriverException, AttributeError): - pass + return driver.find_element(MobileBy.ID, 'android:id/message').text + except NoSuchElementException: + return None + def add_alert_text_to_report(self, driver): + try: + alert_text = self.get_alert_text(driver) + if alert_text: + test_suite_data.current_test.testruns[-1].error = "%s; also Unexpected Alert is shown: '%s'" % ( + test_suite_data.current_test.testruns[-1].error, alert_text + ) + except (RemoteDisconnected, ProtocolError): + test_suite_data.current_test.testruns[-1].error = "%s; \n RemoteDisconnected" % \ + test_suite_data.current_test.testruns[-1].error -class LocalMultipleDeviceTestCase(AbstractTestCase): - def setup_method(self, method): - self.drivers = dict() +class Driver(webdriver.Remote): - def create_drivers(self, quantity): - capabilities = self.add_local_devices_to_capabilities() - for driver in range(quantity): - self.drivers[driver] = webdriver.Remote(self.executor_local, capabilities[driver]) - self.drivers[driver].implicitly_wait(self.implicitly_wait) - BaseView(self.drivers[driver]).accept_agreements() - test_suite_data.current_test.testruns[-1].jobs.append(self.drivers[driver].session_id) + @property + def number(self): + return test_suite_data.current_test.testruns[-1].jobs[self.session_id] + + def info(self, text: str, device=True): + if device: + text = 'Device %s: %s ' % (self.number, text) + logging.info(text) + test_suite_data.current_test.testruns[-1].steps.append(text) + + def fail(self, text: str): + pytest.fail('Device %s: %s' % (self.number, text)) + + def update_lt_session_status(self, index, status): + data = { + "action": "setTestStatus", + "arguments": { + "status": status, + "remark": "Device %s" % index + } + } + self.execute_script("lambda-hook: %s" % str(data).replace("'", "\"")) + + +class Errors: + def __init__(self): + self.errors = [] + + def _add_error(self, view: BaseView, text): + """ + Adds an error message to the list and logs it. + + Args: + view (BaseView): The view object containing the driver. + text (str): The error message to log. + """ + error_text = f"Device {view.driver.number}: {text}" + self.errors.append(error_text) + view.driver.log_event("appium", error_text) + + def append(self, view: BaseView, errors: list | str): + """ + Appends errors to the error list and logs them. + + Args: + view (BaseView): The view object containing the driver. + errors (str | list): A single error message or a list of error messages. + + Raises: + TypeError: If the provided errors argument is not a string or a list of strings. + """ + if isinstance(errors, str): + self._add_error(view, errors) + elif isinstance(errors, list) and all(isinstance(error, str) for error in errors): + for error in errors: + self._add_error(view, error) + else: + raise TypeError(f"Invalid type for errors: {type(errors)}. Expected str or list of str.") - def teardown_method(self, method): - for driver in self.drivers: + def verify_no_errors(self): + if self.errors: + pytest.fail('\n '.join([self.errors.pop(0) for _ in range(len(self.errors))])) + + +def create_shared_drivers(quantity: int, platform_version: int = 14, device_name: str = "Pixel 6"): + drivers = dict() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + print('LT Executor: %s' % executor_lambda_test) + try: + drivers = loop.run_until_complete( + start_threads(test_suite_data.current_test.name, + quantity, + Driver, + drivers, + command_executor=executor_lambda_test, + options=get_lambda_test_capabilities_emulator(platform_version, device_name))) + if len(drivers) < quantity: + test_suite_data.current_test.testruns[-1].error = "Not all %s drivers are created" % quantity + + for i in range(quantity): + test_suite_data.current_test.testruns[-1].jobs[drivers[i].session_id] = i + 1 + drivers[i].implicitly_wait(implicit_wait) + drivers[i].update_settings({"enforceXPath1": True}) + drivers[i].set_network_connection(ConnectionType.ALL_NETWORK_ON) + return drivers, loop + except (MaxRetryError, AttributeError) as e: + test_suite_data.current_test.testruns[-1].error = str(e) + for i, driver in drivers.items(): try: - self.drivers[driver].quit() - except WebDriverException: + driver.update_lt_session_status(i + 1, "failed") + driver.quit() + except (WebDriverException, AttributeError): pass + raise e -class SauceMultipleDeviceTestCase(AbstractTestCase): - - @classmethod - def setup_class(cls): - cls.loop = asyncio.new_event_loop() - asyncio.set_event_loop(cls.loop) +class MultipleSharedDeviceTestCase(AbstractTestCase): def setup_method(self, method): - self.drivers = dict() - - def create_drivers(self, quantity=2, max_duration=1800, custom_implicitly_wait=None, offline_mode=False): - capabilities = {'maxDuration': max_duration} - if offline_mode: - capabilities['platformVersion'] = '6.0' - self.drivers = self.loop.run_until_complete(start_threads(quantity, - webdriver.Remote, - self.drivers, - self.executor_sauce_lab, - self.update_capabilities_sauce_lab(capabilities))) - for driver in range(quantity): - self.drivers[driver].implicitly_wait( - custom_implicitly_wait if custom_implicitly_wait else self.implicitly_wait) - BaseView(self.drivers[driver]).accept_agreements() - test_suite_data.current_test.testruns[-1].jobs.append(self.drivers[driver].session_id) + if not self.drivers: + pytest.fail(test_suite_data.current_test.testruns[-1].error) + for _, driver in self.drivers.items(): + driver.execute_script("lambda-testCase-start=%s" % method.__name__) + driver.log_event("appium", "Started %s" % method.__name__) + jobs = test_suite_data.current_test.testruns[-1].jobs + if not jobs: + for index, driver in self.drivers.items(): + jobs[driver.session_id] = index + 1 + self.errors = Errors() + test_suite_data.current_test.group_name = self.__class__.__name__ def teardown_method(self, method): + log_names, log_contents = [], [] for driver in self.drivers: try: - self.print_sauce_lab_info(self.drivers[driver]) - self.drivers[driver].quit() - except (WebDriverException, AttributeError): + self.print_lt_session_info(self.drivers[driver]) + self.add_alert_text_to_report(self.drivers[driver]) + log_names.append( + '%s_logs_%s.zip' % (test_suite_data.current_test.name, str(self.drivers[driver].number))) + log_contents.append(pull_logs_folder(self.drivers[driver])) + except (WebDriverException, AttributeError, RemoteDisconnected, ProtocolError): pass + finally: + if not log_contents: + test_suite_data.current_test.testruns[-1].error += "/nCan't extract logs!" + try: + logs = {log_names[i]: log_contents[i] for i in range(len(log_names))} + test_suite_data.current_test.logs_paths = github_report.save_logs(logs) + except IndexError: + pass + + @pytest.fixture(scope='class', autouse=True) + def prepare(self, request): + test_suite_data.current_test.group_name = request.cls.__name__ + try: + request.cls.prepare_devices(request) + finally: + for item, value in request.__dict__.items(): + setattr(request.cls, item, value) @classmethod def teardown_class(cls): - cls.loop.close() - - -environment = LocalMultipleDeviceTestCase if pytest.config.getoption('env') == 'local' else SauceMultipleDeviceTestCase - - -class MultipleDeviceTestCase(environment): - - def setup_method(self, method): - super(MultipleDeviceTestCase, self).setup_method(method) - self.senders = dict() - - def teardown_method(self, method): - for user in self.senders: - self.network_api.faucet(address=self.senders[user]['address']) - super(MultipleDeviceTestCase, self).teardown_method(method) - - -class MessageReliabilityTestCase(MultipleDeviceTestCase): + requests_session = requests.Session() + requests_session.auth = (lambda_test_username, lambda_test_access_key) + if test_suite_data.tests[0].testruns[-1].error and 'setup failed' in test_suite_data.tests[0].testruns[ + -1].error: + group_setup_failed = True + else: + group_setup_failed = False + log_contents, log_names = list(), list() + try: + for i, driver in cls.drivers.items(): + if group_setup_failed: + try: + log_contents.append(pull_logs_folder(driver=driver)) + log_names.append('%s_logs_%s.zip' % (cls.__name__, i)) + except (WebDriverException, AttributeError, RemoteDisconnected, ProtocolError): + test_suite_data.current_test.testruns[-1].error += "/nCan't extract logs!" + lt_session_status = "failed" + else: + lt_session_status = "passed" + try: + driver.update_lt_session_status(i + 1, lt_session_status) + driver.quit() + except (WebDriverException, RemoteDisconnected): + pass + # url = 'https://api.%s/rest/v1/%s/jobs/%s/assets/%s' % (apibase, sauce_username, session_id, "log.json") + # try: + # WebDriverWait(driver, 60, 2).until(lambda _: requests_session.get(url).status_code == 200) + # commands = requests_session.get(url).json() + # for command in commands: + # try: + # if command['message'].startswith("Started "): + # for test in test_suite_data.tests: + # if command['message'] == "Started %s" % test.name: + # test.testruns[-1].first_commands[session_id] = commands.index(command) + 1 + # except KeyError: + # continue + # except (RemoteDisconnected, requests.exceptions.ConnectionError, TimeoutException): + # pass + except AttributeError: + pass + finally: + try: + cls.loop.close() + except AttributeError: + pass - def setup_method(self, method): - super(MessageReliabilityTestCase, self).setup_method(method) - self.one_to_one_chat_data = dict() - self.public_chat_data = dict() + logs = dict(zip(log_names, log_contents)) + logs_paths = github_report.save_logs(logs) - def teardown_method(self, method): - if self.one_to_one_chat_data: - create_one_to_one_chat_report(self.one_to_one_chat_data) - if self.public_chat_data: - create_public_chat_report(self.public_chat_data) - super(MultipleDeviceTestCase, self).teardown_method(method) + for test in test_suite_data.tests: + if group_setup_failed: + test.logs_paths = logs_paths + github_report.save_test(test) diff --git a/test/appium/tests/communities/test_community.py b/test/appium/tests/communities/test_community.py new file mode 100644 index 00000000000..b09f028fb6e --- /dev/null +++ b/test/appium/tests/communities/test_community.py @@ -0,0 +1,1406 @@ +import datetime +import random +import time + +import emoji +import pytest +from _pytest.outcomes import Failed +from appium.webdriver.connectiontype import ConnectionType +from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException + +from tests import marks, run_in_parallel, pytest_config_global, transl +from tests.base_test_case import create_shared_drivers, MultipleSharedDeviceTestCase +from views.chat_view import CommunityView +from views.dbs.waku_backup import user as waku_user +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="two_1") +@marks.nightly +class TestCommunityOneDeviceMerged(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in = SignInView(self.drivers[0]) + + self.home = self.sign_in.create_user() + self.username = self.home.get_username() + self.home.communities_tab.click_until_presence_of_element(self.home.plus_community_button) + self.community_name = "closed community" + self.channel_name = "cats" + self.community = self.home.create_community(community_type="closed") + + self.home.get_chat(self.community_name, community=True).click() + self.community_view = self.home.get_community_view() + self.channel = self.community_view.get_channel(self.channel_name).click() + + @marks.testrail_id(702846) + def test_community_navigate_to_channel_when_relaunch(self): + text_message = 'some_text' + if not self.channel.chat_message_input.is_element_displayed(): + self.home.navigate_back_to_home_view() + self.home.get_to_community_channel_from_home(self.community_name) + + self.channel.send_message(text_message) + self.channel.chat_element_by_text(text_message).wait_for_visibility_of_element() + self.channel.reopen_app(user_name=self.username) + if not self.channel.chat_element_by_text(text_message).is_element_displayed(30): + self.drivers[0].fail("Not navigated to channel view after reopening app") + + @marks.testrail_id(702742) + @marks.smoke + def test_community_copy_and_paste_message_in_chat_input(self): + message_texts = ['mmmeowesage_text', 'https://status.im'] + if not self.channel.chat_message_input.is_element_displayed(): + self.home.navigate_back_to_home_view() + self.home.get_to_community_channel_from_home(self.community_name) + + for message in message_texts: + self.channel.send_message(message) + self.channel.copy_message_text(message) + actual_copied_text = self.channel.driver.get_clipboard_text() + if actual_copied_text != message: + self.errors.append( + self.channel, + 'Message %s text was not copied in community channel, text in clipboard %s' % actual_copied_text) + + self.errors.verify_no_errors() + + @marks.testrail_id(702869) + def test_community_undo_delete_message(self): + if not self.channel.chat_message_input.is_element_displayed(): + self.home.navigate_back_to_home_view() + self.home.get_to_community_channel_from_home(self.community_name) + message_to_delete = "message to delete and undo" + self.channel.send_message(message_to_delete) + self.channel.chat_element_by_text(message_to_delete).wait_for_sent_state() + self.channel.delete_message_in_chat(message_to_delete) + try: + self.channel.element_by_text("Undo").click() + except StaleElementReferenceException: + pytest.fail("Can't press Undo button, not enough time") + try: + self.channel.chat_element_by_text(message_to_delete).wait_for_visibility_of_element() + except TimeoutException: + pytest.fail("Message was not restored by clicking 'Undo' button") + if self.channel.element_starts_with_text("Message deleted").is_element_displayed(): + pytest.fail("Text about deleted message is shown in the chat") + + @marks.testrail_id(703382) + def test_community_mute_community_and_channel(self): + self.home.navigate_to_communities_view() + self.home.just_fyi("Mute community and check that channels are also muted") + self.home.mute_chat_long_press(chat_name=self.community_name, mute_period="mute-for-1-hour", community=True) + device_time = self.home.driver.device_time + current_time = datetime.datetime.strptime(device_time, "%Y-%m-%dT%H:%M:%S%z") + expected_times = [current_time + datetime.timedelta(minutes=i) for i in range(59, 62)] + expected_texts = ["Muted until %s %s" % ( + exp_time.strftime('%H:%M'), "today" if current_time.hour < 23 else "tomorrow" + ) for exp_time in expected_times] + + self.home.get_chat(self.community_name, community=True).long_press_element() + self.home.unmute_community_button.wait_for_visibility_of_element() + try: + current_text = self.home.unmute_community_button.unmute_caption_text + if current_text not in expected_texts: + self.errors.append(self.home, "Text '%s' is not shown for muted community" % expected_texts[1]) + except NoSuchElementException: + self.errors.append(self.home, "Caption with text 'Muted until...' is not shown for muted community") + self.home.click_system_back_button() + + self.home.get_chat(self.community_name, community=True).click() + self.community_view.get_channel(self.channel_name).long_press_element() + self.home.mute_channel_button.wait_for_visibility_of_element() + try: + current_text = self.home.mute_channel_button.unmute_caption_text + if current_text not in expected_texts: + self.errors.append(self.home, + "Text '%s' is not shown for a channel in muted community" % expected_texts[1]) + except NoSuchElementException: + self.errors.append(self.home, + "Caption with text 'Muted until...' is not shown for a channel in muted community") + + self.home.just_fyi("Unmute channel and check that the community is also unmuted") + self.home.mute_channel_button.click() + self.home.navigate_to_communities_view() + self.home.get_chat(self.community_name, community=True).long_press_element() + if not self.home.element_by_text("Mute community").is_element_displayed(): + self.errors.append(self.home, "Community is not unmuted when channel is unmuted") + self.home.click_system_back_button() + + self.home.just_fyi("Mute channel and check that community is not muted") + self.home.get_chat(self.community_name, community=True).click() + self.home.mute_chat_long_press(chat_name=self.channel_name, mute_period="mute-for-1-week", + community_channel=True) + device_time = self.home.driver.device_time + current_time = datetime.datetime.strptime(device_time, "%Y-%m-%dT%H:%M:%S%z") + expected_times = [current_time + datetime.timedelta(days=7, minutes=i) for i in range(-1, 2)] + expected_texts = ["Muted until %s" % exp_time.strftime('%H:%M %a %-d %b') for exp_time in expected_times] + self.community_view.get_channel(self.channel_name).long_press_element() + self.home.mute_channel_button.wait_for_visibility_of_element() + try: + current_text = self.home.mute_channel_button.unmute_caption_text + if current_text not in expected_texts: + self.errors.append(self.home, + "Text '%s' is not shown for a muted community channel" % expected_texts[1]) + except NoSuchElementException: + self.errors.append(self.home, + "Caption with text '%s' is not shown for a muted community channel" % expected_texts[1]) + self.home.click_system_back_button() + self.home.navigate_to_communities_view() + self.home.get_chat(self.community_name, community=True).long_press_element() + if self.home.element_by_text_part("Muted until").is_element_displayed() or \ + self.home.mute_community_button.text != transl["mute-community"]: + self.errors.append(self.home, "Community is muted when channel is muted") + self.home.click_system_back_button() + + self.errors.verify_no_errors() + + @marks.testrail_id(703133) + def test_restore_multiaccount_with_waku_backup_remove_profile_switch(self): + self.home.reopen_app(sign_in=False) + self.home.just_fyi("Restore user with predefined communities and contacts") + self.sign_in.recover_access(passphrase=waku_user.seed, second_user=True) + recover_user_name = self.home.get_username() + + self.home.just_fyi("Check contacts/blocked users") + self.home.chats_tab.click() + self.home.contacts_tab.click() + contacts_number = self.home.get_contact_rows_count() + # Todo: enable when https://github.com/status-im/status-mobile/issues/18096 is fixed + # if contacts_number != len(waku_user.contacts): + # self.errors.append( + # "Incorrect contacts number restored: %s instead of %s" % (contacts_number, len(waku_user.contacts))) + for contact in waku_user.contacts: + if not self.home.element_by_text(contact).is_element_displayed(30): + self.errors.append(self.home, '%s was not restored as a contact from waku backup!' % contact) + # Disabled for simple check as sometimes from waku-backup users restored with 3-random names + # self.home.click_system_back_button_until_element_is_shown() + # contact_row = self.home.contact_details_row(index=i + 1) + # shown_name_text = contact_row.username_text.text + # if shown_name_text in waku_user.contacts: + # waku_user.contacts.remove(shown_name_text) + # continue + # else: + # contact_row.click() + # shown_name_text = profile.default_username_text.text + # if shown_name_text in waku_user.contacts: + # waku_user.contacts.remove(shown_name_text) + # continue + # else: + # chat = self.home.get_chat_view() + # chat.profile_send_message_button.click() + # for name in waku_user.contacts: + # if chat.element_starts_with_text(name).is_element_displayed(sec=20): + # waku_user.contacts.remove(name) + # continue + # if waku_user.contacts: + # self.errors.append( + # "Contact(s) was (were) not restored from backup: %s!" % ", ".join(waku_user.contacts)) + + self.home.just_fyi("Check restored communities") + self.home.communities_tab.click() + for key in ['admin_open', 'member_open', 'admin_closed', 'member_closed']: + if not self.home.element_by_text(waku_user.communities[key]).is_element_displayed(30): + self.errors.append(self.home, "%s was not restored from waku-backup!!" % key) + # TODO: there is a bug when pending community sometimes restored as joined; needs investigation + # self.home.opened_communities_tab.click() + # if not self.home.element_by_text(waku_user.communities['member_pending']).is_element_displayed(30): + # self.errors.append("Pending community %s was not restored from waku-backup!" % waku_user.communities['member_pending']) + + if not pytest_config_global['pr_number']: + self.home.just_fyi("Perform back up") + self.home.navigate_back_to_home_view() + profile = self.home.profile_button.click() + profile.logout_button.scroll_to_element() + profile.advanced_button.click() + profile.backup_settings_button.click() + profile.perform_backup_button.click() + + self.home.just_fyi("Check that can login with different user") + self.home.reopen_app(user_name=self.username) + self.home.navigate_to_communities_view() + if self.home.element_by_text(waku_user.communities['admin_open']).is_element_displayed(30): + self.errors.append(self.home, "Community of previous user is shown!") + + self.home.just_fyi("Check that can remove user from logged out state") + self.home.reopen_app(sign_in=False) + self.sign_in.show_profiles_button.click_if_shown() + user_card = self.sign_in.get_user_profile_by_name(username=recover_user_name) + user_card.open_user_options() + self.sign_in.remove_profile_button.click() + if not self.sign_in.element_by_translation_id("remove-profile-confirm-message").is_element_displayed(30): + self.errors.append(self.home, "Warning is not shown on removing profile!") + self.sign_in.element_by_translation_id("remove").click() + + self.home.just_fyi("Check that removed user is not shown in the list anymore") + self.home.reopen_app(sign_in=False) + if self.sign_in.element_by_text(recover_user_name).is_element_displayed(): + self.errors.append(self.home, "Removed user is re-appeared after relogin!") + + self.errors.verify_no_errors() + + @marks.testrail_id(703503) + @marks.xfail(reason="Might fail if the Discover curation dapp is too slow to load > 2 mins") + def test_community_discovery(self): + try: + # workaround for case if a user is logged out in the previous test + self.sign_in.explore_new_status_button.click_if_shown() + self.sign_in.sign_in(user_name=self.username) + except NoSuchElementException: + pass + self.home.navigate_back_to_home_view() + self.home.just_fyi("Turn off testnet in the profile settings") + profile = self.home.profile_button.click() + profile.switch_network() + + self.home.just_fyi("Check Discover Communities content") + self.home.navigate_to_communities_view() + self.home.discover_communities_button.click() + self.home.community_card_item.wait_for_elements(seconds=120) + + expected_communities = { + # ' 0xUX': ['Design', 'Ethereum', 'Collaboration'], + 'Status': ['Web3', 'Blockchain', 'Ethereum'], + # 'Status Inu': ['News', 'Social', 'Web3'], + } + for community_name, tags in expected_communities.items(): + self.home.just_fyi("Check %s community tags in the Discover communities screen" % community_name) + card = self.home.get_discover_community_card_by_name(community_name=community_name) + try: + card.wait_for_visibility_of_element(90) + # if community_name == 'Status': + # card.swipe_to_web_element() + missing_tags = list() + for text in tags: + try: + card.get_child_element_by_text(text=text).wait_for_element() + except TimeoutException: + missing_tags.append(text) + if missing_tags: + self.errors.append(self.home, + "Community '%s' is missing tag(s) %s." % (community_name, ','.join(tags))) + # if community_name == 'Status': + self.home.just_fyi("Check Status community screen") + card.click() + if self.community_view.join_button.is_element_differs_from_template( + 'status_community_join_button.png'): + self.errors.append(self.home, "Status community Join button is different from expected template.") + if self.community_view.community_logo.is_element_differs_from_template('status_community_logo.png'): + self.errors.append(self.home, "Status community logo is different from expected template.") + + # self.community_view.close_community_view_button.click() + # self.home.swipe_up() + + except TimeoutException: + self.errors.append(self.home, + "Community '%s' is not in the Discover Communities list." % community_name) + + self.errors.verify_no_errors() + # Note: this test should always be the LAST ONE in the group because it turns on mainnet in the app! + + +@pytest.mark.xdist_group(name="one_2") +@marks.nightly +class TestCommunityMultipleDeviceMerged(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + self.loop.run_until_complete(run_in_parallel(((self.device_1.create_user, {'enable_notifications': True}), + (self.device_2.create_user,)))) + self.homes = self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.public_key_2 = self.home_2.get_public_key() + self.profile_1 = self.home_1.get_profile_view() + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(self.public_key_2) + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + self.text_message = 'hello' + + # self.home_2.just_fyi("Send message to contact (need for blocking contact) test") + self.home_1.get_chat(self.username_2).wait_for_visibility_of_element() + self.chat_1 = self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message('hey') + self.chat_2 = self.home_2.get_chat(self.username_1).click() + + self.home_1.just_fyi("Open community to message") + self.home_1.navigate_to_communities_view() + self.community_name = "open community" + self.channel_name = 'general' + self.home_1.create_community(community_type="open") + self.channel_1 = self.home_1.get_to_community_channel_from_home(self.community_name) + self.channel_1.send_message(self.text_message) + + self.community_1, self.community_2 = self.home_1.get_community_view(), self.home_2.get_community_view() + self.community_1.invite_to_community(self.community_name, self.username_2) + self.home_1.get_to_community_channel_from_home(self.community_name) + + self.home_2.just_fyi("Send message to contact (need for blocking contact) test") + self.chat_2.send_message(self.text_message) + self.community_2.join_community() + self.channel_2 = self.community_2.get_channel(self.channel_name).click() + self.channel_2.chat_message_input.wait_for_visibility_of_element(20) + + @marks.smoke + @marks.testrail_id(702838) + def test_community_message_send_check_timestamps_sender_username(self): + message = self.text_message + sent_time_variants = self.channel_1.convert_device_time_to_chat_timestamp() + timestamp = self.channel_1.chat_element_by_text(message).timestamp + if sent_time_variants and timestamp: + if timestamp not in sent_time_variants: + self.errors.append(self.channel_1, "Timestamp is not shown, expected: '%s', in fact: '%s'" % + (", ".join(sent_time_variants), timestamp)) + self.channel_1.verify_message_is_under_today_text(message, self.errors) + self.channel_2.send_message("one more message") + new_message = "new message" + self.channel_1.send_message(new_message) + self.channel_2.verify_message_is_under_today_text(new_message, self.errors, 60) + if self.channel_2.chat_element_by_text(new_message).username.text != self.username_1: + self.errors.append(self.channel_2, + "Default username '%s' is not shown next to the received message" % self.username_1) + self.errors.verify_no_errors() + + @marks.smoke + @marks.testrail_id(702843) + def test_community_message_edit(self): + message_before_edit, message_after_edit = 'Message BEFORE edit', "Message AFTER edit 2" + self.channel_1.send_message(message_before_edit) + self.channel_1.chat_element_by_text(message_before_edit).wait_for_sent_state() + self.channel_1.edit_message_in_chat(message_before_edit, message_after_edit) + for channel in (self.channel_1, self.channel_2): + if not channel.element_by_text_part(message_after_edit).is_element_displayed(60): + self.errors.append(channel, 'Message is not edited') + message_text_after_edit = message_after_edit + ' (Edited)' + self.channel_2.set_reaction(message_text_after_edit) + try: + self.channel_1.chat_element_by_text(message_text_after_edit).emojis_below_message().wait_for_element_text(1) + except (Failed, NoSuchElementException): + self.errors.append(self.channel_1, "Message reaction is not shown for the sender") + self.errors.verify_no_errors() + + @marks.smoke + @marks.testrail_id(702839) + def test_community_message_delete(self): + message_to_delete_everyone = 'delete for everyone' + message_to_delete_for_me = 'delete for me' + self.channel_2.send_message(message_to_delete_everyone) + self.channel_2.chat_element_by_text(message_to_delete_everyone).wait_for_sent_state() + self.home_2.just_fyi('Delete for message everyone. Checking that message is deleted for all members') + self.channel_2.delete_message_in_chat(message_to_delete_everyone) + for channel in (self.channel_1, self.channel_2): + if not channel.chat_element_by_text(message_to_delete_everyone).is_element_disappeared(30): + self.errors.append(channel, "Deleted message is shown in channel") + if not self.channel_2.element_by_translation_id('message-deleted-for-everyone').is_element_displayed(30): + self.errors.append(self.channel_2, "System message about deletion for everyone is not displayed") + + self.home_2.just_fyi( + 'Deleting message for me. Checking that message is deleted only for the author of the message') + self.channel_2.send_message(message_to_delete_for_me) + self.channel_1.chat_element_by_text(message_to_delete_for_me).wait_for_element(120) + self.channel_2.delete_message_in_chat(message_to_delete_for_me, everyone=False) + if not self.channel_2.chat_element_by_text(message_to_delete_for_me).is_element_disappeared(30): + self.errors.append(self.channel_2, "Deleted for me message is shown in channel for the author of message") + if not self.channel_2.element_by_translation_id('message-deleted-for-you').is_element_displayed(30): + self.errors.append(self.channel_2, "System message about deletion for you is not displayed") + if not self.channel_1.chat_element_by_text(message_to_delete_for_me).is_element_displayed(30): + self.errors.append(self.channel_1, "Deleted for me message is deleted all channel members") + self.errors.verify_no_errors() + + @marks.testrail_id(703194) + def test_community_several_images_send_reply(self): + self.home_1.just_fyi('Send several images in 1-1 chat from Gallery') + image_description = 'gallery' + self.channel_1.send_images_with_description(image_description, [0, 1]) + + self.channel_2.just_fyi("Check gallery on second device") + self.channel_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(self.community_name) + chat_element = self.channel_2.chat_element_by_text(image_description) + try: + chat_element.wait_for_visibility_of_element(120) + received = True + image = chat_element.image_container_in_message + if (image.is_element_differs_from_template("images_gallery.png", 5) and + image.is_element_differs_from_template("images_gallery_inverted.png", 5)): + self.errors.append(self.channel_2, "Gallery message does not match the template!") + except TimeoutException: + self.errors.append(self.channel_2, "Gallery message was not received") + received = False + + if received: + self.channel_2.just_fyi("Checking an ability to save and share an image from gallery") + chat_element.image_container_in_message.image_by_index(1).click() + if not self.channel_2.share_image_icon_button.is_element_displayed(): + self.errors.append(self.channel_2, "Can't share an image from gallery.") + if self.channel_2.view_image_options_button.is_element_displayed(): + self.channel_2.view_image_options_button.click() + if not self.channel_2.save_image_icon_button.is_element_displayed(): + self.errors.append(self.channel_2, "Can't save an image from gallery.") + else: + self.errors.append(self.channel_2, "Image options button is not shown for an image from gallery.") + + self.channel_2.navigate_back_to_chat_view() + + self.channel_2.just_fyi("Can reply to gallery") + self.channel_2.quote_message(image_description) + message_text = 'reply to gallery' + self.channel_2.chat_message_input.send_keys(message_text) + self.channel_2.send_message_button.click() + chat_element_1 = self.channel_1.chat_element_by_text(message_text) + if not chat_element_1.is_element_displayed( + sec=60) or chat_element_1.replied_message_text != image_description: + self.errors.append(self.channel_1, 'Reply message was not received by the sender') + self.errors.verify_no_errors() + + @marks.smoke + @marks.xfail(reason="Might fail from time to time as reaction are set too slowly") # TODO: needs investigation + @marks.testrail_id(702859) + def test_community_one_image_send_reply_set_reaction(self): + self.home_1.just_fyi('Send image in 1-1 chat from Gallery') + image_description = 'description' + self.channel_1.send_images_with_description(image_description) + + self.home_2.just_fyi('Check image, description and options for receiver') + if not self.channel_2.chat_element_by_text(image_description).is_element_displayed(60): + self.channel_2.hide_keyboard_if_shown() + self.channel_2.chat_element_by_text(image_description).wait_for_visibility_of_element(10) + if not self.channel_2.chat_element_by_text( + image_description).image_in_message.is_element_image_similar_to_template('image_1_chat_view.png'): + self.errors.append(self.channel_2, "Not expected image is shown to the receiver") + + if not self.channel_1.chat_element_by_text(image_description).is_element_displayed(60): + self.channel_1.hide_keyboard_if_shown() + self.channel_1.chat_element_by_text(image_description).image_in_message.click() + self.home_1.just_fyi('Save image') + self.channel_1.view_image_options_button.click() + self.channel_1.save_image_icon_button.click() + toast_element = self.channel_1.toast_content_element + if toast_element.is_element_displayed(): + toast_element_text = toast_element.text + if toast_element_text != self.channel_1.get_translation_by_key("photo-saved"): + self.errors.append(self.channel_1, + "Shown message '%s' doesn't match expected '%s' after saving an image." % ( + toast_element_text, self.channel_1.get_translation_by_key("photo-saved"))) + else: + self.errors.append(self.channel_1, "Message about saving a photo is not shown.") + self.channel_1.navigate_back_to_chat_view() + + self.channel_1.just_fyi("Check that image is saved in gallery") + self.channel_1.show_images_button.click() + self.channel_1.allow_all_button.click_if_shown() + if not self.channel_1.get_image_by_index(0).is_element_image_similar_to_template( + "image_1_gallery_view.png"): + self.errors.append(self.channel_1, 'Saved image is not shown in Recent') + self.channel_1.click_system_back_button() + + self.home_2.just_fyi('Check share option on opened image') + self.channel_2.chat_element_by_text(image_description).image_in_message.click() + self.channel_2.share_image_icon_button.click() + self.channel_2.element_starts_with_text("Drive").click() + try: + self.channel_2.wait_for_current_package_to_be('com.google.android.apps.docs') + except TimeoutException: + self.errors.append(self.channel_2, "Can't share image") + self.channel_2.navigate_back_to_chat_view() + + self.channel_2.just_fyi("Can reply to images") + # self.channel_2.chat_element_by_text(image_description).long_press_element_by_coordinate(rel_x=0.8, rel_y=0.8) + # self.channel_2.reply_message_button.click() + self.channel_2.quote_message(image_description) + message_text = 'reply to image' + self.channel_2.chat_message_input.send_keys(message_text) + self.channel_2.send_message_button.click() + chat_element_1 = self.channel_1.chat_element_by_text(message_text) + if not chat_element_1.is_element_displayed(sec=60) or chat_element_1.replied_message_text != image_description: + self.errors.append(self.channel_1, 'Reply message was not received by the sender') + self.channel_2.just_fyi("Set a reaction for the image message") + self.channel_2.set_reaction(message=image_description) + try: + self.channel_1.chat_element_by_text(image_description).emojis_below_message().wait_for_element_text(1) + except (Failed, NoSuchElementException): + self.errors.append(self.channel_1, "Image message reaction is not shown for the sender") + self.channel_1.just_fyi("Set a reaction for the message reply") + self.channel_2.set_reaction(message=image_description, emoji="love") + try: + self.channel_2.chat_element_by_text(message_text).emojis_below_message( + emoji="love").wait_for_element_text(1) + except (Failed, NoSuchElementException): + self.errors.append(self.channel_2, "Reply message reaction is not shown for the reply sender") + + self.errors.verify_no_errors() + + @marks.testrail_id(702840) + def test_community_emoji_send_copy_paste_reply(self): + self.channel_1.navigate_back_to_chat_view() + emoji_name = random.choice(list(emoji.EMOJI_UNICODE)) + emoji_unicode = emoji.EMOJI_UNICODE[emoji_name] + emoji_message = emoji.emojize(emoji_name) + self.channel_1.send_message(emoji_message) + for channel in self.channel_1, self.channel_2: + if not channel.chat_element_by_text(emoji_unicode).is_element_displayed(30): + self.errors.append(channel, 'Message with emoji was not sent or received in community channel') + + self.channel_1.just_fyi("Can copy and paste emojis") + self.channel_1.copy_message_text(emoji_unicode) + actual_copied_text = self.channel_1.driver.get_clipboard_text() + if actual_copied_text != emoji_unicode: + self.errors.append(self.channel_1, + 'Emoji message was not copied, text in clipboard is %s' % actual_copied_text) + + self.channel_1.just_fyi("Can reply to emojis") + if not self.channel_2.chat_message_input.is_element_displayed(): + self.home_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(self.community_name) + self.channel_2.quote_message(emoji_unicode) + message_text = 'test message' + self.channel_2.chat_message_input.send_keys(message_text) + self.channel_2.send_message_button.click() + chat_element_1 = self.channel_1.chat_element_by_text(message_text) + chat_element_1.wait_for_element(60) + if chat_element_1.replied_message_text != emoji_unicode: + self.errors.append(self.channel_1, 'Reply message is not reply to original message!') + self.errors.verify_no_errors() + + @marks.testrail_id(702844) + @marks.xfail(reason="Youtube links preview is not loaded on LambdaTest emulators, needs investigation") + def test_community_links_with_previews_github_youtube_twitter_gif_send_enable(self): + preview_urls = { + # TODO: disabled because of the bug in 15891 + # 'giphy':{'url': 'https://giphy.com/gifs/this-is-fine-QMHoU66sBXqqLqYvGO', + # 'title': 'This Is Fine GIF - Find & Share on GIPHY', + # 'description': 'Discover & share this Meme GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.', + # 'link': 'giphy.com'}, + 'github_pr': {'url': 'https://github.com/status-im/status-mobile/pull/11707', + 'title': 'Update translations by jinhojang6 · Pull Request #11707 · status-im/status-mobile', + 'description': 'Update translation json files of 19 languages.', + 'link': 'github.com'}, + 'yotube_short': { + 'url': 'https://youtu.be/Je7yErjEVt4', + 'title': 'Status, your gateway to Ethereum', + 'description': 'Learn more at https://status.im. This video aims to provide an explanation ' + 'and brief preview of the utility that will be supported by the Status App - provid...', + 'link': 'youtu.be'}, + 'yotube_full': { + 'url': 'https://www.youtube.com/watch?v=XN-SVmuJH2g&list=PLbrz7IuP1hrgNtYe9g6YHwHO6F3OqNMao', + 'title': 'Status & Keycard – Hardware-Enforced Security', + 'description': 'With Status and Keycard, you can enable hardware enforced authorizations to ' + 'your Status account and transactions. Two-factor authentication to access your ac...', + 'link': 'www.youtube.com'}, + 'yotube_mobile': { + 'url': 'https://m.youtube.com/watch?v=Je7yErjEVt4', + 'title': 'Status, your gateway to Ethereum', + 'description': 'Learn more at https://status.im. This video aims to provide an explanation ' + 'and brief preview of the utility that will be supported by the Status App - provid...', + 'link': 'm.youtube.com', + }, + + # twitter link is temporary removed from check as current xpath locator in message.preview_title is not applicable for this type of links + # 'twitter': { + # 'url': 'https://twitter.com/ethdotorg/status/1445161651771162627?s=20', + # 'txt': "We've rethought how we translate content, allowing us to translate", + # 'subtitle': 'Twitter' + # } + } + for home in self.home_1, self.home_2: + if not home.chat_floating_screen.is_element_displayed(): + home.navigate_to_communities_view() + home.get_to_community_channel_from_home(self.community_name) + + for key, data in preview_urls.items(): + self.home_2.just_fyi("Checking %s preview case" % key) + url = data['url'] + self.channel_2.chat_message_input.send_keys(url) + try: + self.channel_2.url_preview_composer.wait_for_element(20) + except TimeoutException: + self.errors.append(self.channel_1, "No preview is loaded for url %s" % url) + self.channel_2.send_message_button.click() + continue + shown_title = self.channel_2.url_preview_composer_text.text + if shown_title != data['title']: + self.errors.append(self.channel_2, "Preview text is not expected, it is '%s'" % shown_title) + self.channel_2.send_message_button.click() + message = self.channel_1.get_preview_message_by_text(url) + message.wait_for_element(60) + if not message.preview_image: + self.errors.append(self.channel_1, "No preview image is shown for %s" % url) + shown_title = message.preview_title.text + if shown_title != data['title']: + self.errors.append(self.channel_1, + "Title is not equal expected for '%s', actual is '%s'" % (url, shown_title)) + shown_description = message.preview_subtitle.text + if shown_description != data['description']: + self.errors.append(self.channel_1, + "Description is not equal expected for '%s', actual is '%s'" % ( + url, shown_description)) + shown_link = message.preview_link.text + if shown_link != data['link']: + self.errors.append(self.channel_1, + "Link is not equal expected for '%s', actual is '%s'" % (url, shown_link)) + + self.channel_1.just_fyi("Set reaction and check it") + message_with_reaction = list(preview_urls.values())[-1]['url'] + self.channel_1.set_reaction(message=message_with_reaction, emoji="laugh") + if not self.channel_2.chat_element_by_text(message_with_reaction).emojis_below_message( + emoji="laugh").is_element_displayed(10): + self.channel_2.hide_keyboard_if_shown() + try: + self.channel_2.chat_element_by_text(message_with_reaction).emojis_below_message( + emoji="laugh").wait_for_element_text(1) + except (Failed, NoSuchElementException): + self.errors.append(self.channel_2, "Link message reaction is not shown for the sender") + + self.errors.verify_no_errors() + + @marks.testrail_id(702841) + def test_community_unread_messages_badge(self): + self.channel_1.navigate_to_communities_view() + message = 'test message' + if not self.home_2.chat_floating_screen.is_element_displayed(): + self.home_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(self.community_name) + self.channel_2.send_message(message) + self.home_1.just_fyi('Check new messages badge is shown for community') + community_element_1 = self.home_1.get_chat(self.community_name, community=True) + if not community_element_1.new_messages_grey_dot.is_element_displayed(sec=30): + self.errors.append(self.home_1, 'New message community badge is not shown') + + community_1 = community_element_1.click() + channel_1_element = community_1.get_channel(self.channel_name) + + self.home_1.just_fyi('Check new messages badge is shown for channel') + if not channel_1_element.new_messages_grey_dot.is_element_displayed(): + self.errors.append(self.channel_1, 'New messages channel badge is not shown on channel') + channel_1_element.click() + self.errors.verify_no_errors() + + @marks.testrail_id(702894) + def test_community_contact_block_unblock_offline(self): + for i, channel in enumerate([self.channel_1, self.channel_2]): + if not channel.chat_message_input.is_element_displayed(): + channel.navigate_to_communities_view() + self.homes[i].get_to_community_channel_from_home(self.community_name) + + self.channel_1.send_message('message to get avatar of user 2 visible in next message') + + self.channel_2.just_fyi("Sending message before block") + message_to_disappear = "I should not be in chat" + self.channel_2.send_message(message_to_disappear) + + self.chat_1.just_fyi('Block user') + self.channel_1.chat_element_by_text(message_to_disappear).wait_for_visibility_of_element(30) + chat_element = self.channel_1.chat_element_by_text(message_to_disappear) + chat_element.find_element() + chat_element.member_photo.click() + self.channel_1.block_contact() + + self.chat_1.just_fyi('Check that messages from blocked user are hidden in public chat and close app') + if not self.chat_1.chat_element_by_text(message_to_disappear).is_element_disappeared(30): + self.errors.append(self.chat_1, "Messages from blocked user is not cleared in public chat ") + self.chat_1.navigate_to_chats_view() + if not self.home_1.element_by_translation_id("no-messages").is_element_displayed(): + self.errors.append("1-1 chat from blocked user is not removed and messages home is not empty!") + self.chat_1.driver.set_network_connection(ConnectionType.AIRPLANE_MODE) + + self.home_2.just_fyi('Send message to public chat while device 1 is offline') + message_blocked, message_unblocked = "Message from blocked user", "Hurray! unblocked" + self.channel_2.send_message(message_blocked) + + self.chat_1.just_fyi('Check that new messages from blocked user are not delivered') + self.chat_1.driver.set_network_connection(ConnectionType.ALL_NETWORK_ON) + self.home_1.communities_tab.click() + self.home_1.get_chat(self.community_name, community=True).click() + self.home_1.get_chat(self.channel_name, community_channel=True).click() + for message in message_to_disappear, message_blocked: + if self.chat_1.chat_element_by_text(message).is_element_displayed(30): + self.errors.append(self.chat_1, + "'%s' from blocked user is fetched from offline in community channel" % message) + + self.chat_1.just_fyi('Unblock user and check that can see further messages') + # TODO: still no blocked users in new UI + profile_1 = self.home_1.get_profile_view() + self.home_1.navigate_back_to_home_view() + self.chat_1.profile_button.click() + profile_1.profile_messages_button.click() + profile_1.profile_blocked_users_button.wait_and_click() + self.home_1.get_chat(self.username_2).profile_unblock_button.click() + self.chat_1.confirm_block_contact_button.click() + self.chat_1.click_system_back_button(times=3) + + self.home_2.just_fyi("Check that can send message in community after unblock") + self.chat_2.send_message(message_unblocked) + self.home_1.communities_tab.click() + self.home_1.get_chat(self.community_name, community=True).click() + self.home_1.get_chat(self.channel_name, community_channel=True).click() + self.chat_1.hide_keyboard_if_shown() + if not self.chat_1.chat_element_by_text(message_unblocked).is_element_displayed(120): + self.errors.append(self.chat_1, + "%s was not received in public chat after user unblock!" % message_unblocked) + self.errors.verify_no_errors() + + self.home_1.just_fyi("Add blocked user to contacts again after removing(removed automatically when blocked)") + chat_element = self.channel_1.chat_element_by_text(message_unblocked) + chat_element.find_element() + chat_element.member_photo.click() + self.channel_1.profile_send_contact_request_button.click() + self.channel_1.contact_request_message_input.send_keys("hi") + self.channel_1.confirm_send_contact_request_button.click() + self.home_2.just_fyi("Accept contact request after being unblocked") + self.home_2.navigate_back_to_home_view() + self.home_2.handle_contact_request(self.username_1) + self.channel_1.profile_send_message_button.click_until_absense_of_element( + desired_element=self.channel_1.profile_send_message_button, attempts=20, timeout=3) + try: + self.chat_1.send_message("piy") + unblocked = True + except TimeoutException: + unblocked = False + self.errors.append(self.chat_1, "Chat with unblocked user was not enabled after 1 minute") + + if unblocked: + self.home_2.just_fyi("Check message in 1-1 chat after unblock") + self.home_2.chats_tab.click() + self.home_2.get_chat(self.username_1).click() + self.chat_2.send_message(message_unblocked) + try: + self.chat_2.chat_element_by_text(message_unblocked).wait_for_status_to_be(expected_status='Delivered', + timeout=120) + if not self.chat_1.chat_element_by_text(message_unblocked).is_element_displayed(30): + self.errors.append(self.chat_1, "Message was not received in 1-1 chat after user unblock!") + except TimeoutException: + self.errors.append(self.chat_2, 'Message was not delivered after back up online.') + + self.errors.verify_no_errors() + + @marks.testrail_id(703086) + def test_community_mark_all_messages_as_read(self): + for home in self.home_1, self.home_2: + home.navigate_to_communities_view() + self.home_2.get_chat(self.community_name, community=True).click() + self.community_2.get_channel(self.channel_name).click() + self.channel_2.send_message(self.text_message) + community_1_element = self.community_1.get_chat(self.community_name, community=True) + if not community_1_element.new_messages_grey_dot.is_element_displayed(90): + self.errors.append(self.community_1, 'New messages counter is not shown in home > Community element') + community_1_element.click() + channel_1_element = self.community_1.get_chat(self.channel_name, community_channel=True) + if not channel_1_element.new_messages_grey_dot.is_element_displayed(): + self.errors.append(self.community_1, "New messages counter is not shown in community channel element") + self.community_1.click_system_back_button() + mark_as_read_button = self.community_1.mark_all_messages_as_read_button + self.home_1.community_floating_screen.wait_for_invisibility_of_element() + community_1_element.long_press_until_element_is_shown(mark_as_read_button) + mark_as_read_button.click() + if community_1_element.new_messages_grey_dot.is_element_displayed(): + self.errors.append(self.community_1, + 'Unread messages badge is shown in community element while there are no unread messages') + community_1_element.click() + if channel_1_element.new_messages_grey_dot.is_element_displayed(): + self.errors.append( + self.community_1, + "New messages badge is shown in community channel element while there are no unread messages") + self.errors.verify_no_errors() + + @marks.testrail_id(704615) + def test_community_edit_delete_message_when_offline(self): + self.channel_2.just_fyi("Sending messages for edit and delete") + message_to_edit, message_to_delete = "message to edit", "message to delete" + self.channel_2.send_message(message_to_edit) + self.channel_2.send_message(message_to_delete) + self.home_1.navigate_to_communities_view() + self.home_1.get_chat(self.community_name, community=True).click() + self.community_1.get_channel(self.channel_name).click() + self.channel_1.just_fyi("Receiver is checking if initial messages were delivered") + for message in message_to_edit, message_to_delete: + if not self.channel_1.chat_element_by_text(message).is_element_displayed(30): + self.channel_1.driver.fail("Message '%s' was not received" % message) + + self.channel_2.just_fyi("Turning on airplane mode and editing/deleting messages") + self.channel_2.driver.set_network_connection(ConnectionType.AIRPLANE_MODE) + message_after_edit = "text after edit" + self.channel_2.edit_message_in_chat(message_to_edit, message_after_edit) + self.channel_2.delete_message_in_chat(message_to_delete) + self.channel_2.just_fyi("Turning on network connection") + self.channel_2.driver.set_network_connection(ConnectionType.ALL_NETWORK_ON) + + self.channel_1.just_fyi("Receiver is checking if messages were updated and deleted") + if not self.channel_1.chat_element_by_text(message_after_edit).is_element_displayed(30): + self.errors.append(self.channel_1, + "Updated message '%s' is not delivered to the receiver" % message_after_edit) + if not self.channel_1.chat_element_by_text(message_to_delete).is_element_disappeared(): + self.errors.append(self.channel_1, "Message '%s' was not deleted for the receiver" % message_to_delete) + self.errors.verify_no_errors() + + +@pytest.mark.xdist_group(name="two_2") +@marks.nightly +class TestCommunityMultipleDeviceMergedTwo(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + self.loop.run_until_complete(run_in_parallel(((self.device_1.create_user, {'enable_notifications': True}), + (self.device_2.create_user, {'enable_notifications': True})))) + self.homes = self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.public_key_2 = self.home_2.get_public_key() + self.profile_1 = self.home_1.get_profile_view() + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(self.public_key_2) + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + self.text_message = 'hello' + + self.chat_1 = self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message('hey') + self.chat_2 = self.home_2.get_chat(self.username_1).click() + + self.home_1.just_fyi("Open community to message") + self.home_1.navigate_to_communities_view() + self.community_name = "open community" + self.channel_name = 'general' + self.home_1.create_community(community_type="open") + + self.community_1, self.community_2 = self.home_1.get_community_view(), self.home_2.get_community_view() + self.community_1.invite_to_community(self.community_name, self.username_2) + self.channel_1 = self.home_1.get_to_community_channel_from_home(self.community_name) + + self.home_2.just_fyi("Send message to contact (need for blocking contact) test") + self.chat_2.send_message(self.text_message) + self.community_2.join_community() + self.channel_2 = self.community_2.get_channel(self.channel_name).click() + + @marks.testrail_id(702786) + def test_community_mentions_push_notification(self): + self.home_1.navigate_back_to_home_view() + self.device_1.open_notification_bar() + + self.device_2.just_fyi("Invited member sends a message with a mention") + self.channel_2.send_message("hi") + self.channel_2.mention_user(self.username_1) + self.channel_2.send_message_button.click() + + self.device_1.just_fyi("Admin gets push notification with the mention and tap it") + message_received = False + pn_element = self.home_1.get_pn(pn_text=self.username_1, wait_time=90) + if pn_element: + pn_element.click() + message_received = True + if not self.channel_1.chat_element_by_text(self.username_1).is_element_displayed(): + if self.channel_1.chat_message_input.is_element_displayed(): + self.errors.append(self.channel_1, + "Message with the mention is not shown in the chat for the admin") + else: + self.errors.append(self.channel_1, + "Channel did not open by clicking on a notification with the mention for admin") + else: + self.errors.append(self.channel_1, "Push notification with the mention was not received by admin") + + if not self.channel_1.chat_message_input.is_element_displayed(): + self.channel_1.navigate_to_communities_view() + self.home_1.get_to_community_channel_from_home(self.community_name) + + if message_received: + self.channel_1.just_fyi("Set reaction for the message with a mention") + self.channel_1.set_reaction(message=self.username_1, emoji="sad", times_to_long_press=2) + + self.device_2.just_fyi("Sender edits the message with a mention") + chat_element = self.channel_2.chat_element_by_text(self.username_1) + chat_element.wait_for_sent_state() + chat_element.long_press_without_release() + edit_done = False + expected_message = "" + try: + self.channel_2.element_by_translation_id("edit-message").double_click() + time.sleep(1) + for i in range(29, 32): + self.channel_2.driver.press_keycode(i) + input_text = self.channel_2.chat_message_input.text + if 'cba' in input_text: # sometimes method press_keycode adds symbols to the beginning of line + expected_message = "0 cba (Edited)" + else: + expected_message = "0 abc (Edited)" + self.channel_2.send_message_button.click() + edit_done = True + if chat_element.message_body_with_mention.text != expected_message: + self.errors.append(self.channel_2, "Edited message is not shown correctly for the sender") + except NoSuchElementException: + self.errors.append(self.channel_2, "Can not edit a message with a mention") + if edit_done: + element = self.channel_1.chat_element_by_text(self.username_1).message_body_with_mention + if not element.is_element_displayed(10) or element.text != expected_message: + self.errors.append(self.channel_1, "Edited message is not shown correctly for the (receiver) admin") + + self.device_2.just_fyi("Sender checks the reaction for a message with a mention") + try: + self.channel_2.chat_element_by_text(self.username_1).emojis_below_message( + emoji="sad").wait_for_element_text(text=1, wait_time=60) + except (Failed, NoSuchElementException): + self.errors.append(self.channel_2, "Message reaction is not shown for the sender") + + self.home_2.navigate_back_to_home_view() + if not self.channel_1.chat_message_input.is_element_displayed(): + self.channel_1.navigate_to_communities_view() + self.home_1.get_chat(self.community_name, community=True).click() + self.community_1.get_channel(self.channel_name).click() + + self.device_1.just_fyi("Admin sends a message with a mention") + self.channel_1.mention_user(self.username_2) + self.channel_1.send_message_button.click() + self.device_2.just_fyi("Invited member gets push notification with the mention and tap it") + self.device_2.open_notification_bar() + push_notification_element = self.home_2.get_pn(pn_text=self.username_2, wait_time=90) + if push_notification_element: + push_notification_element.click() + if not self.channel_2.chat_element_by_text(self.username_2).is_element_displayed(): + if self.channel_2.chat_message_input.is_element_displayed(): + self.errors.append(self.channel_2, + "Message with the mention is not shown in the chat for the invited member") + else: + self.errors.append( + self.channel_2, + "Channel did not open by clicking on a notification with the mention for the invited member") + else: + self.errors.append(self.channel_2, + "Push notification with the mention was not received by the invited member") + self.errors.verify_no_errors() + + @marks.testrail_id(702809) + def test_community_markdown_support(self): + markdown = { + 'bold text in asterics': '**', + 'bold text in underscores': '__', + 'italic text in asteric': '*', + 'italic text in underscore': '_', + 'inline code': '`', + 'code blocks': '```', + 'quote reply (one row)': '>', + } + + for home in self.homes: + home.navigate_to_communities_view() + community = home.get_chat(self.community_name, community=True).click() + community.get_channel(self.channel_name).click() + + for message, symbol in markdown.items(): + self.home_1.just_fyi('Checking that "%s" is applied (%s) in community channel' % (message, symbol)) + message_to_send = symbol + message + symbol if 'quote' not in message else symbol + message + self.channel_2.send_message(message_to_send) + if not self.channel_2.chat_element_by_text(message).is_element_displayed(): + self.errors.append( + self.channel_2, + '%s is not displayed with markdown in community channel for the sender \n' % message) + + if not self.channel_1.chat_element_by_text(message).is_element_displayed(60): + self.errors.append( + self.channel_1, + '%s is not displayed with markdown in community channel for the recipient \n' % message) + + for home in self.homes: + home.navigate_to_chats_view() + home.recent_tab.click() + + self.home_1.get_chat(self.username_2).click() + self.home_2.get_chat(self.username_1).click() + + for message, symbol in markdown.items(): + self.home_1.just_fyi('Checking that "%s" is applied (%s) in 1-1 chat' % (message, symbol)) + message_to_send = symbol + message + symbol if 'quote' not in message else symbol + message + self.chat_1.send_message(message_to_send) + if not self.chat_1.chat_element_by_text(message).is_element_displayed(30): + self.errors.append( + self.chat_1, '%s is not displayed with markdown in 1-1 chat for the sender (device 1) \n' % message) + + if not self.chat_2.chat_element_by_text(message).is_element_displayed(60): + self.errors.append( + self.chat_2, + '%s is not displayed with markdown in 1-1 chat for the recipient (device 2) \n' % message) + + self.errors.verify_no_errors() + + @marks.testrail_id(702845) + def test_community_leave(self): + self.home_2.navigate_to_communities_view() + community = self.home_2.get_chat(self.community_name, community=True) + community_to_leave = CommunityView(self.drivers[1]) + community.long_press_until_element_is_shown(community_to_leave.leave_community_button) + community_to_leave.leave_community_button.click() + community_to_leave.leave_community_button.click() + if not community.is_element_disappeared(): + self.errors.append(self.home_2, 'Community is still shown in the list after leave') + self.errors.verify_no_errors() + + @marks.xfail(reason="Can't navigate to a community channel before joining the community, " + "https://github.com/status-im/status-mobile/issues/21776") + @marks.testrail_id(702948) + def test_community_hashtag_links_to_community_channels(self): + self.home_2.navigate_to_chats_view() + self.home_1.navigate_to_communities_view() + + self.home_1.just_fyi("Device 1 creates a closed community") + self.home_1.create_community(community_type="closed") + community_name = "closed community" + self.community_1.invite_to_community(community_name, self.username_2) + self.community_1.get_to_community_channel_from_home(community_name, "general") + control_message_general_chat = "this message should be visible to the user before joining" + self.channel_1.send_message(control_message_general_chat) + + self.home_2.just_fyi("Device 2 checks the community channels and message inside") + self.home_2.get_chat(self.username_1).click() + control_message_1_1_chat = "it is just a message text" + self.chat_2.send_message(control_message_1_1_chat) + self.chat_2.chat_element_by_text("https://status.app/c/").click_on_link_inside_message_body() + closed_com_channels = dogs_channel, cats_channel = "dogs", "cats" # not checking the last channel '# rules' to avoid scrolling + not_shown = list() + for chan_name in closed_com_channels: + if not self.community_2.get_channel(chan_name).is_element_displayed(): + not_shown.append(chan_name) + if not_shown: + self.errors.append(self.community_2, + "Not all channels are shown in community before joining: %s" % not_shown) + chan = self.community_2.get_channel("general") + if chan.is_element_displayed(): + chan.find_element().click() + if not self.channel_2.chat_element_by_text(control_message_general_chat).is_element_displayed(20): + self.errors.append(self.channel_2, "Message in community channel is not visible for user before join") + else: + not_shown.append("general") + + self.home_2.just_fyi("Device 2 joins the community") + self.home_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + self.community_2.join_community(open_community=False) + + self.home_1.just_fyi("Device 1 accepts the community request") + self.home_1.navigate_back_to_home_view() + try: + self.home_1.notifications_unread_badge.wait_for_visibility_of_element(120) + except TimeoutException: + self.errors.append(self.home_1, "Unread indicator is not shown in notifications on membership request") + self.home_1.open_activity_center_button.click() + reply_element = self.home_1.get_element_from_activity_center_view(self.username_2) + reply_element.title.swipe_right_on_element(width_percentage=2.5) + self.home_1.activity_notification_swipe_button.click() + self.home_1.close_activity_centre.click() + + cats_message = "Where is a cat?" + + self.home_1.just_fyi("Device 1 sends a message in the cats channel") + self.home_1.get_to_community_channel_from_home(community_name=community_name, channel_name=cats_channel) + self.channel_1.send_message(cats_message) + self.channel_1.navigate_to_communities_view() + self.home_1.get_to_community_channel_from_home(community_name=community_name, channel_name=dogs_channel) + + self.home_1.just_fyi("Device 1 sends a message with hashtag in the dogs channel") + message_with_hashtag = "#cats" + self.channel_1.send_message(message_with_hashtag) + + self.home_2.just_fyi("Device 2 clicks on the message with hashtag in the community channel") + self.home_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(community_name, dogs_channel) + self.channel_2.chat_element_by_text(message_with_hashtag).click_on_link_inside_message_body() + if not self.channel_2.chat_element_by_text(cats_message).is_element_displayed(30): + self.errors.append(self.channel_2, "Receiver was not navigated to the cats channel") + + self.home_1.just_fyi("Device 1 clicks on the message with hashtag in the community channel") + self.channel_1.chat_element_by_text(message_with_hashtag).click_on_link_inside_message_body() + if not self.channel_1.chat_element_by_text(cats_message).is_element_displayed(30): + self.errors.append(self.channel_1, "Sender was not navigated to the cats channel") + + for home in self.homes: + home.navigate_to_chats_view() + + self.home_2.just_fyi("Device 2 sends a message with hashtag in 1-1 chat") + self.home_2.get_chat(self.username_1).click() + self.chat_2.send_message(message_with_hashtag) + + self.home_1.just_fyi("Device 1 clicks on the message with hashtag in 1-1 chat") + self.home_1.get_chat(self.username_2).click() + self.chat_1.chat_element_by_text(message_with_hashtag).click_on_link_inside_message_body() + if self.chat_1.chat_element_by_text(control_message_1_1_chat).is_element_disappeared(): + self.errors.append(self.chat_1, "Receiver was navigated out of 1-1 chat") + + self.home_2.just_fyi("Device 2 clicks on the message with hashtag in 1-1 chat") + self.chat_2.chat_element_by_text(message_with_hashtag).click_on_link_inside_message_body() + if self.chat_2.chat_element_by_text(control_message_1_1_chat).is_element_disappeared(): + self.errors.append(self.chat_2, "Sender was navigated out of 1-1 chat") + + self.errors.verify_no_errors() + + @marks.xfail(reason="Can't navigate to a community channel before joining the community, " + "https://github.com/status-im/status-mobile/issues/21776") + @marks.testrail_id(703629) + def test_community_join_when_node_owner_offline(self): + for home in self.homes: + home.navigate_to_communities_view() + if self.home_2.get_chat(self.community_name, community=True).is_element_displayed(): + CommunityView(self.home_2.driver).leave_community(self.community_name) + + self.home_1.just_fyi("Device 1 creates open community") + self.home_1.create_community(community_type="open") + community_name = "open community" + self.community_1.invite_to_community(community_name, self.username_2) + self.community_1.get_to_community_channel_from_home(community_name, "general") + control_message_general_chat = "this message should be visible to the user before joining" + self.channel_1.send_message(control_message_general_chat) + try: + self.channel_1.chat_element_by_text(control_message_general_chat).wait_for_status_to_be( + expected_status='Delivered', + timeout=60) + message_sent = True + except TimeoutException: + message_sent = False + + self.home_1.just_fyi("Device 1 goes offline") + app_package = self.device_1.driver.current_package + self.device_1.driver.terminate_app(app_package) + + self.home_2.just_fyi("Device 2 requests to join the community") + self.home_2.chats_tab.click() + self.home_2.get_chat(self.username_1).click() + self.community_2.join_community(open_community=False) + exp_text = "You requested to join “%s”" % community_name + if self.community_2.toast_content_element.is_element_displayed(10): + cur_text = self.community_2.toast_content_element.text + if cur_text != exp_text: + self.errors.append(self.community_2, + "Text \"%s\" in shown toast element doesn't match expected \"%s\"" % ( + cur_text, exp_text)) + else: + self.errors.append(self.community_2, "Toast element with the text \"%s\" doesn't appear" % exp_text) + if not self.community_2.community_status_pending.is_element_displayed(): + self.errors.append(self.community_2, "Pending status is not displayed") + general_channel = self.community_2.get_channel("general") + if general_channel.is_element_displayed(): + general_channel.find_element().click() + if not self.channel_2.chat_element_by_text(control_message_general_chat).is_element_displayed(30): + self.errors.append( + self.community_2, + "Message in community channel is not visible for user before join, it was indicated as " \ + "%s sent for the sender before he went offline" % ("" if message_sent else "not")) + else: + self.errors.append(self.community_2, "Community channel is not displayed for user before join") + self.community_2.toast_content_element.wait_for_invisibility_of_element(30) + self.home_2.navigate_to_communities_view() + self.home_2.pending_communities_tab.click() + if self.home_2.get_chat(community_name, community=True).is_element_displayed(): + self.home_2.get_chat(community_name, community=True).click() + else: + self.errors.append(self.home_2, "%s is not listed inside Pending communities tab" % community_name) + + self.home_1.just_fyi("Device 1 goes back online") + self.home_1.driver.activate_app(app_package) + self.device_1.sign_in(user_name=self.username_1) + + self.home_2.just_fyi("Device 2 checks that he's joined the community") + exp_text = "You joined “%s”" % community_name + if self.community_2.toast_content_element.is_element_displayed(60): + cur_text = self.community_2.toast_content_element.text + if cur_text != exp_text: + self.errors.append(self.community_2, + "Text \"%s\" in shown toast element doesn't match expected \"%s\"" % ( + cur_text, exp_text)) + # else: + # self.errors.append("Toast element with the text \"%s\" doesn't appear" % exp_text) + # ToDo: add verification when toast is fixed + if not self.community_2.community_status_joined.is_element_displayed(): + self.errors.append(self.community_2, "Joined status is not displayed") + self.community_2.close_community_view_button.click_until_absense_of_element( + self.community_2.close_community_view_button) + self.home_2.joined_communities_tab.click() + chat_element = self.home_2.get_chat(community_name, community=True) + if chat_element.is_element_displayed(30): + chat_element.click() + else: + self.errors.append(self.home_2, "%s is not listed inside Joined communities tab" % community_name) + + self.errors.verify_no_errors() + + +@pytest.mark.xdist_group(name="six_2") +@marks.nightly +class TestCommunityMultipleDeviceMergedThree(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + self.loop.run_until_complete(run_in_parallel(((self.device_1.create_user,), + (self.device_2.create_user,)))) + self.homes = self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.public_key_2 = self.home_2.get_public_key() + self.profile_1 = self.home_1.get_profile_view() + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(self.public_key_2) + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + self.text_message = 'hello' + + self.chat_1 = self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message('hey') + self.chat_2 = self.home_2.get_chat(self.username_1).click() + + self.home_1.just_fyi("Open community to message") + self.home_1.navigate_to_communities_view() + self.community_name = "open community" + self.channel_name = 'general' + self.home_1.create_community(community_type="open") + + self.community_1, self.community_2 = self.home_1.get_community_view(), self.home_2.get_community_view() + self.community_1.invite_to_community(self.community_name, self.username_2) + self.channel_1 = self.home_1.get_to_community_channel_from_home(self.community_name) + + self.community_2.join_community() + self.channel_2 = self.community_2.get_channel(self.channel_name).click() + + @marks.testrail_id(741924) + @marks.xfail(reason="Community message can be fetched from offline when using mobile data") + def test_community_mobile_data_and_wi_fi_only_fetching(self): + self.device_2.just_fyi("Receiver turns wi-fi off") + self.device_2.navigate_back_to_home_view() + self.device_2.driver.set_network_connection(ConnectionType.DATA_ONLY) + # ToDo: pop up is not shown on LT emulators, needs more investigation + # self.device_2.turn_wi_fi_off() + # + # for translation in ['which-connection-to-use', 'mobile-and-wifi', 'wifi-only']: + # if not self.device_2.element_by_translation_id(translation).is_element_displayed(): + # self.errors.append(self.device_2, + # "Element with text '%s' is not shown when switching to mobile data" % translation) + profile_2 = self.device_2.profile_button.click() + profile_2.syncing_button.scroll_and_click() + profile_2.sync_and_backup_button.click() + profile_2.wi_fi_only_button.click() + self.home_2.navigate_to_communities_view() + self.home_2.discover_communities_button.click() + if self.home_2.community_card_item.is_element_displayed(10): + self.errors.append(self.home_2, + "Community can be fetched while using mobile data with wi-fi only syncing settings") + + message_1 = 'message text 1' + self.channel_1.just_fyi("Sender sends a message in the community channel when receiver is using mobile data") + self.channel_1.send_message(message_1) + self.home_2.click_system_back_button() + self.home_2.get_to_community_channel_from_home(self.community_name) + if not self.channel_2.chat_element_by_text(message_1).is_element_displayed(120): + self.errors.append(self.device_2, + "Can't receive the message '%s' in community when using mobile data" % message_1) + + self.device_2.just_fyi("Receiver turns internet off") + self.device_2.driver.set_network_connection(ConnectionType.NO_CONNECTION) + message_2 = 'message text 2' + self.channel_1.just_fyi("Sender sends messages in the community channel and 1-1 chat when receiver is offline") + self.channel_1.send_message(message_2) + self.channel_1.chat_element_by_text(message_2).wait_for_sent_state() + self.device_1.navigate_to_chats_view() + self.home_1.get_chat(self.username_2).click() + message_3 = 'message text 3' + self.chat_1.send_message(message_3) + self.chat_1.chat_element_by_text(message_3).wait_for_sent_state() + + self.device_2.just_fyi("Receiver turns mobile data on") + self.device_2.driver.set_network_connection(ConnectionType.DATA_ONLY) + if self.channel_2.chat_element_by_text(message_2).is_element_displayed(120): + self.errors.append( + self.device_2, + "Message '%s' in community, which is sent when receiver was offline, is received using mobile data" % message_2) + self.device_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + if not self.chat_2.chat_element_by_text(message_3).is_element_displayed(120): + self.errors.append( + self.device_2, + "Message '%s' in 1-1 chat, which is sent when receiver was offline, is missed" % message_3) + + self.device_2.just_fyi("Receiver turns wi-fi on") + self.device_2.driver.set_network_connection(ConnectionType.WIFI_ONLY) + self.home_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(self.community_name) + if not self.channel_2.chat_element_by_text(message_2).is_element_displayed(120): + self.errors.append( + self.device_2, + "Message '%s' in community channel, which is sent when receiver was offline, is missed after turning wi-fi on" % message_2) + self.errors.verify_no_errors() + + @marks.testrail_id(741925) + def test_community_send_message_from_offline(self): + self.device_1.just_fyi("Device 1 goes offline and send a message in 1-1 chat") + self.device_1.driver.set_network_connection(ConnectionType.NO_CONNECTION) + + message_1_1 = 'message in 1-1 chat' + + def _send_message_1_1(): + self.device_1.navigate_to_chats_view() + self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message(message_1_1) + + self.device_2.just_fyi("Device 2 goes offline and send a message in community") + self.device_2.driver.set_network_connection(ConnectionType.NO_CONNECTION) + + message_community = 'message community' + + def _send_message_community(): + self.device_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(self.community_name) + self.channel_2.send_message(message_community) + + self.loop.run_until_complete(run_in_parallel(((_send_message_1_1,), (_send_message_community,)))) + + self.device_1.just_fyi("Device 1 goes back online and checks a message in community channel") + self.device_1.driver.set_network_connection(ConnectionType.WIFI_ONLY) + + def _check_message_community(): + self.device_1.navigate_to_communities_view() + self.home_1.get_to_community_channel_from_home(self.community_name) + if not self.channel_1.chat_element_by_text(message_community).is_element_displayed(120): + self.errors.append( + self.device_1, + "Can't receive the message '%s' in community if it's sent from offline" % message_community) + + self.device_2.just_fyi("Device 2 goes back online and checks a message in 1-1 chat") + self.device_2.driver.set_network_connection(ConnectionType.WIFI_ONLY) + + def _check_message_1_1(): + self.device_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + if not self.chat_2.chat_element_by_text(message_1_1).is_element_displayed(120): + self.errors.append( + self.device_1, + "Can't receive the message '%s' in 1-1 chat if it's sent from offline" % message_1_1) + + self.loop.run_until_complete(run_in_parallel(((_check_message_community,), (_check_message_1_1,)))) + + self.errors.verify_no_errors() + + @marks.testrail_id(741926) + def test_community_messaging_on_mobile_data(self): + self.device_1.just_fyi("Device 1 turns mobile data on and send a message in community channel") + self.device_1.driver.set_network_connection(ConnectionType.DATA_ONLY) + self.device_2.just_fyi("Device 2 turns mobile data on and checks receiving a message in community channel") + self.device_2.driver.set_network_connection(ConnectionType.DATA_ONLY) + + self.device_1.navigate_to_communities_view() + self.home_1.get_to_community_channel_from_home(self.community_name) + message_community = 'message community' + self.channel_1.send_message(message_community) + self.device_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(self.community_name) + if not self.channel_2.chat_element_by_text(message_community).is_element_displayed(60): + self.errors.append(self.channel_2, + "Message with text '%s' was not received in community" % message_community) + + self.device_2.just_fyi("Device 2 sends a message in 1-1 chat") + self.device_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + message_1_1 = "message 1-1 chat" + self.chat_2.send_message(message_1_1) + + self.device_1.just_fyi("Device 1 checks a message receiving in 1-1 chat") + self.device_1.navigate_to_chats_view() + self.home_1.get_chat(self.username_2).click() + if not self.chat_1.chat_element_by_text(message_1_1).is_element_displayed(60): + self.errors.append(self.chat_1, + "Message with text '%s' was not received in 1-1 chat" % message_1_1) + + self.errors.verify_no_errors() diff --git a/test/appium/tests/conftest.py b/test/appium/tests/conftest.py index 5a7408814e4..d40ce9b3ced 100644 --- a/test/appium/tests/conftest.py +++ b/test/appium/tests/conftest.py @@ -1,23 +1,26 @@ -from _pytest.runner import runtestprotocol - -from support.test_rerun import should_rerun_test -from tests import test_suite_data -import requests -import pytest +import json +import os +import re +import signal +import time +from contextlib import contextmanager +from dataclasses import dataclass from datetime import datetime +from http.client import RemoteDisconnected from os import environ -from io import BytesIO -from sauceclient import SauceClient -from support.github_report import GithubHtmlReport -from support.testrail_report import TestrailReport -sauce_username = environ.get('SAUCE_USERNAME') -sauce_access_key = environ.get('SAUCE_ACCESS_KEY') -github_token = environ.get('GIT_HUB_TOKEN') +import pytest +import requests +from filelock import FileLock +from requests.exceptions import ConnectionError as c_er + +import tests +from tests import test_suite_data + +lambda_test_username = environ.get('LAMBDA_TEST_USERNAME') +lambda_test_access_key = environ.get('LAMBDA_TEST_ACCESS_KEY') -sauce = SauceClient(sauce_username, sauce_access_key) -github_report = GithubHtmlReport(sauce_username, sauce_access_key) -testrail_report = TestrailReport(sauce_username, sauce_access_key) +github_token = environ.get('GIT_HUB_TOKEN') def pytest_addoption(parser): @@ -31,9 +34,9 @@ def pytest_addoption(parser): help='Url or local path to apk') parser.addoption('--env', action='store', - default='sauce', - help='Specify environment: local/sauce/api') - parser.addoption('--log', + default='lt', + help='Specify environment: local/lt/api') + parser.addoption('--log_steps', action='store', default=False, help='Display each test step in terminal as plain text: True/False') @@ -45,155 +48,255 @@ def pytest_addoption(parser): action='store', default=False, help='boolean; For creating testrail report per run') - parser.addoption('--network', - action='store', - default='ropsten', - help='string; ropsten or rinkeby') - - # message reliability - parser.addoption('--rerun_count', - action='store', - default=0, - help='How many times tests should be re-run if failed') - parser.addoption('--messages_number', - action='store', - default=20, - help='Messages number') - parser.addoption('--message_wait_time', - action='store', - default=20, - help='Max time to wait for a message to be received') - parser.addoption('--participants_number', - action='store', - default=5, - help='Public chat participants number') - parser.addoption('--chat_name', - action='store', + parser.addoption("--run_testrail_ids", + action="store", + metavar="NAME", default=None, - help='Public chat name') - parser.addoption('--user_public_key', - action='store', + help="only run tests matching the environment NAME.") + parser.addoption("--apk_upgrade", + action="store", + metavar="NAME", default=None, - help='Public key of user for 1-1 chat') + help='Url or local path to apk for upgrade') + +@dataclass +class Option: + pass -def get_rerun_count(): - return int(pytest.config.getoption('rerun_count')) + +option = Option() +testrail_report = None +github_report = None +run_name = None def is_master(config): - return not hasattr(config, 'slaveinput') + return not hasattr(config, 'workerinput') -def is_uploaded(): - stored_files = sauce.storage.get_stored_files() - for i in range(len(stored_files['files'])): - if stored_files['files'][i]['name'] == test_suite_data.apk_name: - return True +@contextmanager +def _upload_time_limit(seconds): + def signal_handler(signum, frame): + raise TimeoutError("Apk upload took more than %s seconds" % seconds) + signal.signal(signal.SIGALRM, signal_handler) + signal.alarm(seconds) + try: + start_time = time.time() + yield + print("Apk upload took %s seconds" % round(time.time() - start_time)) + finally: + signal.alarm(0) -def pytest_configure(config): - if config.getoption('log'): - import logging - logging.basicConfig(level=logging.INFO) - if config.getoption('env') != 'api': - test_suite_data.apk_name = ([i for i in [i for i in config.getoption('apk').split('/') - if '.apk' in i]])[0] - if is_master(config): - if config.getoption('testrail_report'): - pr_number = config.getoption('pr_number') - if pr_number: - run_number = len(testrail_report.get_runs(pr_number)) + 1 - run_name = 'PR-%s run #%s' % (pr_number, run_number) - else: - run_name = test_suite_data.apk_name - testrail_report.add_run(run_name) - if config.getoption('env') == 'sauce': - if not is_uploaded(): - if 'http' in config.getoption('apk'): - response = requests.get(config.getoption('apk'), stream=True) - response.raise_for_status() - file = BytesIO(response.content) - del response - requests.post('http://saucelabs.com/rest/v1/storage/' - + sauce_username + '/' + test_suite_data.apk_name + '?overwrite=true', - auth=(sauce_username, sauce_access_key), - data=file, - headers={'Content-Type': 'application/octet-stream'}) - else: - sauce.storage.upload_file(config.getoption('apk')) +class UploadApkException(Exception): + pass -def pytest_unconfigure(config): - if is_master(config): - if config.getoption('pr_number'): - from github import Github - repo = Github(github_token).get_user('status-im').get_repo('status-react') - pull = repo.get_pull(int(config.getoption('pr_number'))) - pull.create_issue_comment(github_report.build_html_report()) - if config.getoption('testrail_report'): - testrail_report.add_results() +def _upload_and_check_response(apk_file_path): + from support.lambda_test import upload_apk + with _upload_time_limit(1000): + return upload_apk(apk_file_path) -@pytest.mark.hookwrapper -def pytest_runtest_makereport(item, call): - outcome = yield - report = outcome.get_result() - if report.when == 'call': - is_sauce_env = pytest.config.getoption('env') == 'sauce' - current_test = test_suite_data.current_test - if report.failed: - current_test.testruns[-1].error = report.longreprtext - if is_sauce_env: - update_sauce_jobs(current_test.name, current_test.testruns[-1].jobs, report.passed) - github_report.save_test(current_test) +def _upload_and_check_response_with_retries(apk_file_path, retries=3): + for _ in range(retries): + try: + return _upload_and_check_response(apk_file_path) + except (ConnectionError, RemoteDisconnected, c_er): + time.sleep(10) -def update_sauce_jobs(test_name, job_ids, passed): - for job_id in job_ids: - sauce.jobs.update_job(job_id, name=test_name, passed=passed) +def _download_apk(url): + # Absolute path added to handle CI runs. + apk_path = os.path.join(os.path.dirname(__file__), test_suite_data.apk_name) -def get_testrail_case_id(obj): - if 'testrail_id' in obj.keywords._markers: - return obj.keywords._markers['testrail_id'].args[0] + print('Downloading: %s' % url) + try: + resp = requests.get(url) + resp.raise_for_status() + except requests.RequestException as err: + print(resp.text) + raise err + with open(apk_path, 'wb') as f: + f.write(resp.content) -def pytest_runtest_setup(item): - test_suite_data.set_current_test(item.name, testrail_case_id=get_testrail_case_id(item)) - test_suite_data.current_test.create_new_testrun() + return apk_path -def pytest_runtest_protocol(item, nextitem): - for i in range(get_rerun_count()): - reports = runtestprotocol(item, nextitem=nextitem) - for report in reports: - if report.failed and should_rerun_test(report.longreprtext): - break # rerun +def get_run_name(config, new_one=False): + pr_number = config.getoption('pr_number') + if config.getoption('testrail_report'): + if pr_number: + if new_one: + run_number = len(testrail_report.get_runs(pr_number)) + 1 + else: + run_number = len(testrail_report.get_runs(pr_number)) + return 'PR-%s run #%s (%s)' % (pr_number, run_number, test_suite_data.apk_name.split('-')[4]) else: - return True # no need to rerun + return test_suite_data.apk_name + else: + return config.getoption('build') -@pytest.fixture -def messages_number(): - return int(pytest.config.getoption('messages_number')) +def pytest_configure(config): + global option + option = config.option + from support.testrail_report import TestrailReport + global testrail_report + testrail_report = TestrailReport() + from support.github_report import GithubHtmlReport + global github_report + github_report = GithubHtmlReport() + tests.pytest_config_global = vars(config.option) + config.addinivalue_line("markers", "testrail_id(name): empty") + + if config.getoption('log_steps'): + import logging + logging.basicConfig(level=logging.INFO) + if config.getoption('env') == 'api': + return + + test_suite_data.apk_name = ([i for i in [i for i in config.getoption('apk').split('/') + if '.apk' in i]])[0] + global run_name + if is_master(config) and config.getoption('testrail_report'): + run_name = get_run_name(config, new_one=True) + testrail_report.add_run(run_name) + else: + run_name = get_run_name(config, new_one=False) + + if not is_master(config): + return + + pr_number = config.getoption('pr_number') + if pr_number: + from github import Github + repo = Github(github_token).get_user('status-im').get_repo('status-mobile') + pull = repo.get_pull(int(pr_number)) + pull.get_commits()[0].create_status( + state='pending', + context='Mobile e2e tests', + description='e2e tests are running' + ) + + +@pytest.fixture(scope='session', autouse=True) +def upload_apk(tmp_path_factory): + fn = tmp_path_factory.getbasetemp().parent / "lt_apk.json" + with FileLock(str(fn) + ".lock"): + if fn.is_file(): + data = json.loads(fn.read_text()) + tests.pytest_config_global['lt_apk_url'] = data['lambda_test_apk_url'] + else: + apk_src = tests.pytest_config_global['apk'] + if apk_src.startswith('http'): + apk_path = _download_apk(apk_src) + else: + apk_path = apk_src + tests.pytest_config_global['lt_apk_url'] = _upload_and_check_response(apk_path) + fn.write_text(json.dumps({'lambda_test_apk_url': tests.pytest_config_global['lt_apk_url']})) + if apk_src.startswith('http'): + os.remove(apk_path) -@pytest.fixture -def message_wait_time(): - return int(pytest.config.getoption('message_wait_time')) +def pytest_unconfigure(config): + if is_master(config): + if config.getoption('testrail_report'): + testrail_report.add_results() + if config.getoption('pr_number'): + from github import Github + repo = Github(github_token).get_user('status-im').get_repo('status-mobile') + pull = repo.get_pull(int(config.getoption('pr_number'))) + comment = pull.create_issue_comment(github_report.build_html_report(testrail_report.run_id)) + if not testrail_report.is_run_successful(): + pull.get_commits()[0].create_status(state='failure', context='Mobile e2e tests', + description='Failure - e2e tests are failed', + target_url=comment.html_url) + else: + pull.get_commits()[0].create_status(state='success', context='Mobile e2e tests', + description='Success - e2e tests are passed', + target_url=comment.html_url) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + case_ids_set = item.config.getoption("run_testrail_ids") + + def catch_error(): + error = report.longreprtext + failure_pattern = 'E.*Message:|E.*Error:|E.*Failed:' + exception = re.findall(failure_pattern, error) + if exception: + error = error.replace(re.findall(failure_pattern, report.longreprtext)[0], '') + return error + + secured_test = "secured" in item.keywords._markers or "secured" in item.parent.keywords._markers + + if report.when == 'setup': + is_group = "xdist_group" in item.keywords._markers or "xdist_group" in item.parent.keywords._markers + error_intro, error = 'Test setup failed:', '' + final_error = '%s %s' % (error_intro, error) + if (hasattr(report, 'wasxfail') and not case_ids_set) or (hasattr(report, 'wasxfail') and ( + str([mark.args[0] for mark in item.iter_markers(name='testrail_id')][0]) in str(case_ids_set))): + if '[NOTRUN]' in report.wasxfail: + test_suite_data.set_current_test(test_name=item.name, testrail_case_id=get_testrail_case_id(item), + secured=secured_test) + test_suite_data.current_test.create_new_testrun() + if is_group: + test_suite_data.current_test.group_name = item.instance.__class__.__name__ + test_suite_data.current_test.testruns[-1].xfail = report.wasxfail + test_suite_data.current_test.testruns[-1].run = False + error_intro, error = 'Test is not run, e2e blocker ', report.wasxfail + final_error = "%s [[%s]]" % (error_intro, error) + else: + if is_group: + test_suite_data.current_test.group_name = item.instance.__class__.__name__ + error = catch_error() + final_error = '%s %s [[%s]]' % (error_intro, error, report.wasxfail) + else: + if is_group and report.failed: + test_suite_data.current_test.group_name = item.instance.__class__.__name__ + error = catch_error() + final_error = '%s %s' % (error_intro, error) + if error: + test_suite_data.current_test.testruns[-1].error = final_error + github_report.save_test(test_suite_data.current_test) -@pytest.fixture -def participants_number(): - return int(pytest.config.getoption('participants_number')) + if report.when == 'call': + current_test = test_suite_data.current_test + error = catch_error() + if report.failed: + current_test.testruns[-1].error = error + if (hasattr(report, 'wasxfail') and not case_ids_set) or (hasattr(report, 'wasxfail') and ( + str([mark.args[0] for mark in item.iter_markers(name='testrail_id')][0]) in str(case_ids_set))): + current_test.testruns[-1].xfail = report.wasxfail + if '[NOTRUN]' in report.wasxfail: + current_test.testruns[-1].run = False + if error: + current_test.testruns[-1].error = '%s [[%s]]' % (error, report.wasxfail) -@pytest.fixture -def chat_name(): - return pytest.config.getoption('chat_name') +def get_testrail_case_id(item): + testrail_id = item.get_closest_marker('testrail_id') + if testrail_id: + return testrail_id.args[0] -@pytest.fixture -def user_public_key(): - return pytest.config.getoption('user_public_key') +def pytest_runtest_setup(item): + try: + testrail_id = [mark.args[0] for mark in item.iter_markers(name='testrail_id')][0] + except IndexError: + pass + run_testrail_ids = item.config.getoption("run_testrail_ids") + if run_testrail_ids: + if str(testrail_id) not in list(run_testrail_ids.split(",")): + pytest.skip("test requires testrail case id %s" % testrail_id) + secured = bool([mark for mark in item.iter_markers(name='secured')]) + test_suite_data.set_current_test(test_name=item.name, testrail_case_id=get_testrail_case_id(item), secured=secured) + test_suite_data.current_test.create_new_testrun() diff --git a/test/appium/tests/deep_links/test_deep_and_universal_links.py b/test/appium/tests/deep_links/test_deep_and_universal_links.py new file mode 100644 index 00000000000..f94fc2e7159 --- /dev/null +++ b/test/appium/tests/deep_links/test_deep_and_universal_links.py @@ -0,0 +1,153 @@ +import pytest + +from tests import marks +from tests.base_test_case import create_shared_drivers, MultipleSharedDeviceTestCase +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="one_1") +@marks.nightly +class TestDeepLinksOneDevice(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.driver = self.drivers[0] + self.sign_in = SignInView(self.driver) + + self.home = self.sign_in.create_user() + self.username = self.home.get_username() + self.home.communities_tab.click_until_presence_of_element(self.home.plus_community_button) + self.open_community_name = "open community" + self.channel_name = "general" + self.community = self.home.create_community(community_type="open") + self.profile_view = self.home.get_profile_view() + self.browser_view = self.home.get_dapp_view() + self.home.get_chat(self.open_community_name, community=True).click() + self.community_view = self.home.get_community_view() + self.open_community_url = self.community_view.copy_community_link() + self.channel = self.community_view.get_channel(self.channel_name).click() + + @marks.testrail_id(704613) + def test_links_open_universal_links_from_chat(self): + profile_urls = { + "https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj": "zQ3...arJQSj", + "https://status.app/u#zQ3shVVxZMwLVEQvuu1KF6h4D2mzVyCC4F4mHLZm5dz5XU1aa": "zQ3...5XU1aa", + "https://status.app/u/CweACg0KC1Rlc3RVc2VyRTJFAw==#zQ3shcFXYnGXxJZnsMThziUNMwyA5uGLp58bLGmfb3qaWD1F6": "TestUserE2E"} + + for url, text in profile_urls.items(): + self.channel.just_fyi("Opening profile '%s' by the url %s" % (text, url)) + self.channel.chat_message_input.clear() + self.channel.send_message(url) + self.channel.chat_element_by_text(url).click_on_link_inside_message_body() + if self.channel.profile_send_contact_request_button.is_element_displayed(10): + username_text = self.profile_view.contact_name_text.text + if not (username_text.endswith(url[-6:]) or username_text == text): + self.errors.append(self.channel, "Incorrect username is shown for profile url %s" % url) + else: + self.errors.append(self.channel, "Profile was not opened by the profile url %s" % url) + self.profile_view.close_button.click() + + self.errors.verify_no_errors() + + @marks.testrail_id(702775) + def test_links_deep_links_profile(self): + self.home.navigate_back_to_home_view() + self.home.browser_tab.click() + + profile_links = { + "status-app://u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj": None, + "status-app://u#zQ3shVVxZMwLVEQvuu1KF6h4D2mzVyCC4F4mHLZm5dz5XU1aa": None, + "status-app://u/Ow==#zQ3shsKnV5HJMWJR61c6dssWzHszdbLfBoMF1gcLtSQAYdw2d": "Restored desktop", + "status-app://u/CweACg0KC1Rlc3RVc2VyRTJFAw==#zQ3shcFXYnGXxJZnsMThziUNMwyA5uGLp58bLGmfb3qaWD1F6": "TestUserE2E" + } + for link, text in profile_links.items(): + self.channel.just_fyi("Opening profile link %s" % link) + self.browser_view.open_url(link) + shown_name_text = self.profile_view.contact_name_text.text + if text: + name_is_shown = shown_name_text == text or shown_name_text.endswith(link[-6:]) + else: + name_is_shown = shown_name_text.endswith(link[-6:]) + if not self.channel.profile_send_contact_request_button.is_element_displayed(10) or not name_is_shown: + self.errors.append(self.channel, "Profile was not opened by the profile deep link %s" % link) + self.browser_view.click_system_back_button() + + self.errors.verify_no_errors() + + @marks.testrail_id(739307) + def test_deep_links_communities(self): + closed_community_name, snt_community_name = "closed community", "SNT community" + self.sign_in.reopen_app(sign_in=True, user_name=self.username) + if self.home.communities_tab.is_element_displayed(): + self.home.communities_tab.click() + else: + self.home.navigate_to_communities_view() + self.home.create_community(community_type="closed") + if not self.community_view.community_options_button.is_element_displayed(): + self.home.get_chat(closed_community_name, community=True).click() + closed_community_url = self.community_view.copy_community_link() + self.home.navigate_back_to_home_view() + self.home.create_community(community_type="token-gated") + if not self.community_view.community_options_button.is_element_displayed(): + self.home.get_chat(snt_community_name, community=True).click() + snt_community_url = self.community_view.copy_community_link() + self.home.reopen_app(sign_in=False) + self.sign_in.create_user(first_user=False) + self.home.browser_tab.click() + + old, new = "https://status.app/", "status-app://" + community_links = { + snt_community_name: snt_community_url.replace(old, new), + self.open_community_name: self.open_community_url.replace(old, new), + closed_community_name: closed_community_url.replace(old, new) + } + for text, link in community_links.items(): + self.channel.just_fyi("Opening community '%s' by the link %s" % (text, link)) + self.browser_view.open_url(link) + if text == snt_community_name: + if self.community_view.community_title.text != text: + self.errors.append(self.community_view, + "Community '%s' was not requested to join by the deep link %s" % (text, link)) + else: + if not self.community_view.join_button.is_element_displayed( + 10) or self.community_view.community_title.text != text: + self.errors.append(self.community_view, + "Community '%s' was not requested to join by the deep link %s" % (text, link)) + if text != closed_community_name: # the last one + self.home.click_system_back_button() + self.errors.verify_no_errors() + + @marks.testrail_id(704614) + @marks.skip # ToDo: the feature is not ready yet + def test_links_open_universal_links_from_other_apps(self): + app_package = self.driver.current_package + self.home.just_fyi("Opening a profile URL from google search bar when user is still logged in") + profile_url = "https://status.app/u#zQ3shVVxZMwLVEQvuu1KF6h4D2mzVyCC4F4mHLZm5dz5XU1aa" + self.home.click_system_home_button() + self.home.open_link_from_google_search_app(profile_url, app_package) + if not self.channel.profile_add_to_contacts_button.is_element_displayed( + 10) or not self.profile_view.default_username_text.text.endswith(profile_url[-6:]): + self.errors.append(self.profile_view, + "Profile was not opened by the url %s when user is logged in" % profile_url) + + self.home.just_fyi("Opening a community URL from google search bar when user is logged out") + self.driver.terminate_app(app_package) + community_url = "https://status.app/c/Ow==#zQ3shbmfT3hvh4mKa1v6uAjjyztQEroh8Mfn6Ckegjd7LT3XK" + self.home.open_link_from_google_search_app(community_url, app_package) + self.sign_in.sign_in(user_name=self.username) + if not self.community_view.join_button.is_element_displayed(10): + self.errors.append(self.community_view, + "Closed community was not requested to join by the url %s" % community_url) + + # ToDo: enable when https://github.com/status-im/status-mobile/issues/18074 is fixed + # self.home.just_fyi("Opening a community channel URL from google search bar with no account created") + # self.driver.reset() + # self.home.click_system_home_button() + # channel_url = "https://status.app/cc/Ow==#zQ3shbmfT3hvh4mKa1v6uAjjyztQEroh8Mfn6Ckegjd7LT3XK" + # self.home.open_link_from_google_search_app(channel_url, app_package) + # self.sign_in.create_user() + # if not self.home.element_by_translation_id( + # "community-admins-will-review-your-request").is_element_displayed(10): + # self.errors.append("Created user was not redirected to a community channel by the url %s" % channel_url) + + self.errors.verify_no_errors() diff --git a/test/appium/tests/different_os/test_different_android_versions.py b/test/appium/tests/different_os/test_different_android_versions.py new file mode 100644 index 00000000000..cc6631b5ad2 --- /dev/null +++ b/test/appium/tests/different_os/test_different_android_versions.py @@ -0,0 +1,35 @@ +import pytest + +from base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from tests import marks +from views.sign_in_view import SignInView + + +def _check_account_creation(driver): + sign_in_view = SignInView(driver) + home_view = sign_in_view.create_user() + home_view.profile_button.wait_for_visibility_of_element() + + +@pytest.mark.xdist_group(name="two_1") +@marks.nightly +class TestAndroid12(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(quantity=1, platform_version=12, device_name="Pixel 5") + + @marks.testrail_id(741806) + def test_create_account_android_12(self): + _check_account_creation(self.drivers[0]) + + +@pytest.mark.xdist_group(name="three_1") +@marks.nightly +class TestAndroid13(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(quantity=1, platform_version=13) + + @marks.testrail_id(741807) + def test_create_account_android_13(self): + _check_account_creation(self.drivers[0]) diff --git a/test/appium/tests/marks.py b/test/appium/tests/marks.py index d802d2e03f4..eaa885eadb2 100644 --- a/test/appium/tests/marks.py +++ b/test/appium/tests/marks.py @@ -1,20 +1,9 @@ import pytest -pr = pytest.mark.pr -testrail_case_id = pytest.mark.testrail_case_id -testrail_id = pytest.mark.testrail_id # atomic tests -smoke_1 = pytest.mark.smoke_1 +testrail_id = pytest.mark.testrail_id +nightly = pytest.mark.nightly +smoke = pytest.mark.smoke -account = pytest.mark.account -all = pytest.mark.all -api = pytest.mark.api -chat = pytest.mark.chat -chat_management = pytest.mark.chat_management -dapps = pytest.mark.dapps -message_reliability = pytest.mark.message_reliability -transaction = pytest.mark.transaction -wallet = pytest.mark.wallet -wallet_modal = pytest.mark.wallet_modal -sign_in = pytest.mark.sign_in +flaky = pytest.mark.flaky skip = pytest.mark.skip -logcat = pytest.mark.logcat +xfail = pytest.mark.xfail diff --git a/test/appium/tests/private_chats/test_group_chat.py b/test/appium/tests/private_chats/test_group_chat.py new file mode 100644 index 00000000000..0be1e38a01c --- /dev/null +++ b/test/appium/tests/private_chats/test_group_chat.py @@ -0,0 +1,538 @@ +import datetime + +import pytest +from _pytest.outcomes import Failed +from appium.webdriver.connectiontype import ConnectionType +from selenium.common.exceptions import NoSuchElementException, TimeoutException + +from tests import marks, run_in_parallel, transl +from tests.base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from views.chat_view import ChatView +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="one_3") +@marks.nightly +class TestGroupChatMultipleDeviceMergedNewUI(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(3) + self.message_before_adding = 'message before adding new user' + self.message_to_admin = 'Hey, admin!' + self.public_keys, self.usernames, self.chats = {}, {}, {} + self.sign_in_views = [SignInView(self.drivers[key]) for key in self.drivers] + self.loop.run_until_complete( + run_in_parallel( + ( + (self.sign_in_views[0].create_user, {'enable_notifications': True}), + (self.sign_in_views[1].create_user, {'enable_notifications': True}), + (self.sign_in_views[2].create_user, {'enable_notifications': True}) + ) + ) + ) + self.homes = [sign_in.get_home_view() for sign_in in self.sign_in_views] + self.usernames = self.loop.run_until_complete( + run_in_parallel( + ( + (self.homes[0].get_username,), + (self.homes[1].get_username,), + (self.homes[2].get_username,) + ) + ) + ) + self.public_keys = self.loop.run_until_complete( + run_in_parallel( + ( + (self.homes[1].get_public_key,), + (self.homes[2].get_public_key,) + ) + ) + ) + + self.homes[0].just_fyi('Admin adds future members to contacts') + self.homes[0].navigate_to_chats_view() + for i in range(2): + self.homes[0].add_contact(self.public_keys[i]) + + self.homes[0].just_fyi('Members add admin to contacts to see PNs and put app in background') + for i in range(1, 3): + self.homes[i].navigate_to_chats_view() + self.loop.run_until_complete( + run_in_parallel( + ( + (self.homes[1].handle_contact_request, {'username': self.usernames[0]}), + (self.homes[2].handle_contact_request, {'username': self.usernames[0]}) + ) + ) + ) + for i in range(1, 3): + self.homes[i].navigate_back_to_home_view() + + self.homes[0].just_fyi('Admin creates group chat') + self.chat_name = self.homes[0].get_random_chat_name() + self.homes[0].chats_tab.click() + self.chats[0] = self.homes[0].create_group_chat(user_names_to_add=[self.usernames[1], self.usernames[2]], + group_chat_name=self.chat_name) + for i in range(1, 3): + self.chats[i] = ChatView(self.drivers[i]) + + self.chats[0].send_message(self.message_before_adding) + + @marks.smoke + @marks.testrail_id(702807) + def test_group_chat_join_send_text_messages_push(self): + message_to_admin = self.message_to_admin + [self.homes[i].navigate_back_to_home_view() for i in range(3)] + self.homes[1].get_chat(self.chat_name).click() + self.chats[0].open_notification_bar() + + self.chats[1].send_message(message_to_admin) + + self.chats[0].just_fyi('Check that PN is received and after tap you are redirected to group chat') + pn = self.homes[0].get_pn(message_to_admin) + if pn: + pn.click() + else: + self.errors.append(self.homes[0], "No PN was received on new message for message in group chat") + self.homes[0].click_system_back_button() + self.homes[0].get_chat(self.chat_name).click() + + self.chats[1].just_fyi('Check message status and message delivery') + self.chats[1].chat_element_by_text(message_to_admin).wait_for_status_to_be('Delivered', timeout=120) + if not self.chats[0].chat_element_by_text(message_to_admin).is_element_displayed(30): + self.errors.append(self.chats[0], 'Message %s was not received by admin' % message_to_admin) + self.errors.verify_no_errors() + + @marks.testrail_id(703202) + def test_group_chat_reactions(self): + [self.homes[i].navigate_back_to_home_view() for i in range(3)] + [self.homes[i].get_chat(self.chat_name).click() for i in range(3)] + message = "This is a test message to check some reactions." + self.chats[0].just_fyi("Admin sends a message") + self.chats[0].send_message(message) + + self.chats[1].just_fyi("Member_1 sets 2 reactions on the message: 'thumbs-up' and 'love'") + self.chats[1].set_reaction(message=message, emoji="thumbs-up") + self.chats[1].set_reaction(message=message, emoji="love") + + self.chats[2].just_fyi("Member_2 sets 2 reactions on the message: 'thumbs-up' and 'laugh'") + self.chats[2].add_remove_same_reaction(emoji="thumbs-up") + self.chats[2].set_reaction(message=message, emoji="laugh") + + def _check_reactions_count(chat_view_index): + self.chats[chat_view_index].just_fyi("Checking reactions count for each group member and admin") + chat_element = self.chats[chat_view_index].chat_element_by_text(message) + chat_element.emojis_below_message(emoji="thumbs-up").wait_for_element_text(text=2, wait_time=60) + chat_element.emojis_below_message(emoji="love").wait_for_element_text(text=1, wait_time=60) + chat_element.emojis_below_message(emoji="laugh").wait_for_element_text(text=1, wait_time=60) + + self.loop.run_until_complete(run_in_parallel(( + (_check_reactions_count, {'chat_view_index': 0}), + (_check_reactions_count, {'chat_view_index': 1}), + (_check_reactions_count, {'chat_view_index': 2}) + ))) + + self.chats[0].just_fyi("Admin checks info about voted users") + self.chats[0].chat_element_by_text(message).emojis_below_message(emoji="thumbs-up").long_press_without_release() + if not self.chats[0].user_list_element_by_name( + self.usernames[1]).is_element_displayed() or not self.chats[0].user_list_element_by_name( + self.usernames[2]).is_element_displayed(): + self.errors.append(self.chats[0], "Incorrect users are shown for 'thumbs-up' reaction.") + + self.chats[0].authors_for_reaction(emoji="love").double_click() + if not self.chats[0].user_list_element_by_name( + self.usernames[1]).is_element_displayed() or self.chats[0].user_list_element_by_name( + self.usernames[2]).is_element_displayed(): + self.errors.append(self.chats[0], "Incorrect users are shown for 'love' reaction.") + + self.chats[0].authors_for_reaction(emoji="laugh").click() + if self.chats[0].user_list_element_by_name( + self.usernames[1]).is_element_displayed() or not self.chats[0].user_list_element_by_name( + self.usernames[2]).is_element_displayed(): + self.errors.append(self.chats[0], "Incorrect users are shown for 'laugh' reaction.") + + self.chats[0].just_fyi("Admin opens member_2 profile") + self.chats[0].user_list_element_by_name(self.usernames[2]).click() + try: + username_shown = self.chats[0].get_profile_view().contact_name_text.text + if username_shown != self.usernames[2]: + self.errors.append( + self.chats[0], + "Incorrect profile is opened from the list of reactions, username is %s but expected to be %s" % ( + username_shown, self.usernames[2]) + ) + except NoSuchElementException: + self.errors.append(self.chats[0], "User profile was not opened from the list of reactions") + self.chats[0].navigate_back_to_chat_view() + + self.chats[1].just_fyi("Member_1 removes 'thumbs-up' reaction and adds 'sad' one") + self.chats[1].add_remove_same_reaction(emoji="thumbs-up") + self.chats[1].set_reaction(message=message, emoji="sad") + + self.chats[2].just_fyi("Member_2 removes 'laugh' reaction and adds 'sad' one") + self.chats[2].add_remove_same_reaction(emoji="laugh") + self.chats[2].add_remove_same_reaction(emoji="sad") + + def _check_reactions_count_after_change(chat_view_index): + self.chats[chat_view_index].just_fyi( + "Checking reactions count for each group member and admin after they were changed") + chat_element = self.chats[chat_view_index].chat_element_by_text(message) + try: + chat_element.emojis_below_message(emoji="thumbs-up").wait_for_element_text(text=1, wait_time=60) + chat_element.emojis_below_message(emoji="love").wait_for_element_text(text=1, wait_time=60) + chat_element.emojis_below_message(emoji="sad").wait_for_element_text(text=2, wait_time=60) + except (Failed, NoSuchElementException): + self.errors.append(self.chats[chat_view_index], + "Incorrect reactions count for %s after changing the reactions" % self.usernames[ + chat_view_index]) + + self.loop.run_until_complete(run_in_parallel(( + (_check_reactions_count_after_change, {'chat_view_index': 0}), + (_check_reactions_count_after_change, {'chat_view_index': 1}), + (_check_reactions_count_after_change, {'chat_view_index': 2}) + ))) + + self.chats[0].just_fyi("Admin relogins") + self.chats[0].reopen_app(user_name=self.usernames[0]) + if not self.chats[0].chat_message_input.is_element_displayed(15): + self.homes[0].get_chat(self.chat_name).click() + + self.chats[0].just_fyi("Admin checks reactions count after relogin") + message_element = self.chats[0].chat_element_by_text(message) + try: + message_element.emojis_below_message(emoji="thumbs-up").wait_for_element_text(text=1, wait_time=60) + message_element.emojis_below_message(emoji="love").wait_for_element_text(text=1, wait_time=60) + message_element.emojis_below_message(emoji="sad").wait_for_element_text(text=2, wait_time=60) + except (Failed, NoSuchElementException): + self.errors.append(self.chats[0], "Incorrect reactions count after relogin") + + for chat in self.chats[1], self.chats[2]: + chat.just_fyi("Just making the session not to quit") + chat.navigate_back_to_home_view() + + self.chats[0].just_fyi("Admin checks info about voted users after relogin") + message_element.emojis_below_message(emoji="thumbs-up").long_press_without_release() + if self.chats[0].user_list_element_by_name( + self.usernames[1]).is_element_displayed() or not self.chats[0].user_list_element_by_name( + self.usernames[2]).is_element_displayed(): + self.errors.append(self.chats[0], "Incorrect users are shown for 'thumbs-up' reaction after relogin.") + + self.chats[0].authors_for_reaction(emoji="love").double_click() + if not self.chats[0].user_list_element_by_name( + self.usernames[1]).is_element_displayed() or self.chats[0].user_list_element_by_name( + self.usernames[2]).is_element_displayed(): + self.errors.append(self.chats[0], "Incorrect users are shown for 'love' reaction after relogin.") + + self.chats[0].authors_for_reaction(emoji="sad").click() + if not self.chats[0].user_list_element_by_name( + self.usernames[1]).is_element_displayed() or not self.chats[0].user_list_element_by_name( + self.usernames[2]).is_element_displayed(): + self.errors.append(self.chats[0], "Incorrect users are shown for 'laugh' reaction after relogin.") + + self.errors.verify_no_errors() + + @marks.testrail_id(703297) + def test_group_chat_send_image_save_and_share(self): + [self.homes[i].navigate_back_to_home_view() for i in range(3)] + for i in range(3): + self.homes[i].get_chat(self.chat_name).click() + + self.chats[1].just_fyi("Member_1 sends an image") + image_description = "test image" + self.chats[1].send_images_with_description(description=image_description, indexes=[1]) + + self.chats[0].just_fyi("Admin checks image message") + chat_element = self.chats[0].chat_element_by_text(image_description) + chat_element.wait_for_visibility_of_element(60) + if not chat_element.image_in_message.is_element_image_similar_to_template('image_2_chat_view.png'): + self.errors.append(self.chats[0], "Not expected image is shown to the admin.") + + self.chats[2].just_fyi("Member_2 checks image message") + chat_element = self.chats[2].chat_element_by_text(image_description) + chat_element.wait_for_visibility_of_element(60) + if not chat_element.image_in_message.is_element_image_similar_to_template('image_2_chat_view.png'): + self.errors.append(self.chats[2], "Not expected image is shown to the member_2.") + + self.chats[0].just_fyi("Admin opens the image and shares it") + self.chats[0].chat_element_by_text(image_description).image_in_message.click() + self.chats[0].share_image_icon_button.click() + self.chats[0].element_starts_with_text("Drive").click() + try: + self.chats[0].wait_for_current_package_to_be('com.google.android.apps.docs') + except TimeoutException: + self.errors.append(self.chats[0], "Admin can't share an image via Gmail.") + self.chats[0].navigate_back_to_chat_view() + + self.chats[1].navigate_back_to_home_view() + app_package = self.drivers[2].current_package + + self.chats[2].just_fyi("Member_2 opens the image and saves it") + self.chats[2].chat_element_by_text(image_description).image_in_message.click() + self.chats[2].view_image_options_button.click() + self.chats[2].save_image_icon_button.click() + toast_element = self.chats[2].toast_content_element + try: + toast_element_text = toast_element.wait_for_visibility_of_element().text + if toast_element_text != self.chats[2].get_translation_by_key("photo-saved"): + self.errors.append( + self.chats[2], + "Shown message '%s' doesn't match expected '%s' after saving an image for member_2." % ( + toast_element_text, self.chats[2].get_translation_by_key("photo-saved"))) + except TimeoutException: + self.errors.append(self.chats[2], "Message about saving a photo is not shown for member_2.") + self.chats[2].navigate_back_to_chat_view() + + # workaround for app closed after navigating back from gallery + if not self.chats[2].chat_message_input.is_element_displayed(): + self.drivers[2].activate_app(app_package) + SignInView(self.drivers[2]).sign_in(user_name=self.usernames[2]) + self.homes[2].chats_tab.click() + self.homes[2].get_chat(self.chat_name).click() + + self.chats[2].just_fyi("Member_2 checks that image was saved in gallery") + self.chats[2].show_images_button.click() + self.chats[2].allow_all_button.click_if_shown() + if not self.chats[2].get_image_by_index(0).is_element_image_similar_to_template("image_2_gallery_view.png"): + self.errors.append(self.chats[2], "Image is not saved to gallery for member_2.") + self.chats[2].navigate_back_to_home_view() + + # workaround for app closed after navigating back from gallery + if not self.chats[2].chats_tab.is_element_displayed(): + self.drivers[2].activate_app(app_package) + SignInView(self.drivers[2]).sign_in(user_name=self.usernames[2]) + self.homes[2].chats_tab.click() + + self.errors.verify_no_errors() + + @marks.testrail_id(702808) + def test_group_chat_offline_pn(self): + def _proceed_to_chat(index): + self.homes[index].navigate_to_chats_view() + self.homes[index].groups_tab.click() + self.homes[index].get_chat(self.chat_name).click() + + self.loop.run_until_complete(run_in_parallel(( + (_proceed_to_chat, {'index': 1}), + (_proceed_to_chat, {'index': 2}) + ))) + + message_1, message_2 = 'message from old member', 'message from new member' + + self.homes[0].just_fyi("Put admin device to offline and send messages from members") + self.homes[0].navigate_back_to_home_view() + app_package = self.drivers[0].current_package + self.homes[0].driver.set_network_connection(ConnectionType.AIRPLANE_MODE) + self.chats[1].send_message(message_1) + self.chats[2].send_message(message_2) + + self.homes[0].just_fyi("Put admin device to online and check that messages and PNs will be fetched") + self.homes[0].driver.set_network_connection(ConnectionType.ALL_NETWORK_ON) + self.homes[0].connection_offline_icon.wait_for_invisibility_of_element(60) + self.homes[0].open_notification_bar() + for message in (message_1, message_2): + if self.homes[0].element_by_text(message).is_element_displayed(30): + break + else: + self.errors.append(self.homes[0], 'Messages PN was not fetched from offline') + self.homes[0].click_system_back_button() + # workaround for app closed after opening notifications + if not self.homes[0].chats_tab.is_element_displayed(): + self.drivers[0].activate_app(app_package) + SignInView(self.drivers[0]).sign_in(user_name=self.usernames[0]) + self.homes[0].chats_tab.click() + self.homes[0].get_chat(self.chat_name).click() + + def _check_messages(index): + self.chats[index].just_fyi("Check that messages are shown for user %s" % self.usernames[index]) + for message_text in (message_1, message_2): + if not self.chats[index].chat_element_by_text(message_text).is_element_displayed(60): + self.errors.append(self.chats[index], '%s if not shown for device %s' % (message_text, index)) + + self.loop.run_until_complete(run_in_parallel(( + (_check_messages, {'index': 0}), + (_check_messages, {'index': 1}), + (_check_messages, {'index': 2}) + ))) + + self.errors.verify_no_errors() + + @marks.testrail_id(702732) + def test_group_chat_pin_messages(self): + [self.homes[i].navigate_back_to_home_view() for i in range(3)] + [self.homes[i].get_chat(self.chat_name).click() for i in range(3)] + + self.message_1, self.message_2, self.message_3, self.message_4 = \ + "Message 1", "Message 2", "Message 3", "Message 4" + self.chats[0].just_fyi("Enter group chat and pin message there. It's pinned for both members.") + + self.chats[0].send_message(self.message_1) + self.chats[0].pin_message(self.message_1, "pin-to-chat") + for chat in self.chats[0], self.chats[1]: + chat.chat_element_by_text(self.message_1).wait_for_visibility_of_element(60) + if not chat.chat_element_by_text(self.message_1).pinned_by_label.is_element_displayed(30): + self.errors.append(chat, "Message 1 is not pinned in group chat!") + + self.chats[0].just_fyi("Check that non admin user can not unpin messages") + self.chats[1].chat_element_by_text(self.message_1).long_press_without_release() + if self.chats[1].element_by_translation_id("unpin-from-chat").is_element_displayed(): + self.errors.append(self.chats[1], "Unpin option is available for non-admin user") + self.chats[1].tap_by_coordinates(500, 100) + self.chats[1].tap_by_coordinates(500, 100) + + # not implemented yet : + + # self.home_1.just_fyi("Grant another user with admin rights and check he can unpin message now") + # self.group_chat_1.chat_options.click() + # group_info = self.group_chat_1.group_info.click() + # options = group_info.get_username_options(self.default_username_2).click() + # options.make_admin_button.click() + # self.group_chat_2.click_system_back_button() + # self.group_chat_2.pin_message(self.message_1, action="unpin") + # if (self.group_chat_1.chat_element_by_text(self.message_1).pinned_by_label.is_element_displayed() and + # self.group_chat_2.chat_element_by_text(self.message_1).pinned_by_label.is_element_displayed()): + # self.errors.append("Message failed to be unpinned by user who granted admin permissions!") + + self.chats[0].just_fyi("Send, pin messages and check they are pinned") + for message in self.message_2, self.message_3: + # here group_chat_1 should be changed to group_chat_2 after enabling the previous block + self.chats[0].send_message(message) + self.chats[0].pin_message(message, 'pin-to-chat') + for chat in self.chats[0], self.chats[1]: + chat.chat_element_by_text(message).wait_for_visibility_of_element(60) + if not chat.chat_element_by_text(message).pinned_by_label.is_element_displayed(30): + self.errors.append(chat, "%s is not pinned in group chat!" % message) + + self.chats[0].just_fyi("Check that a user can not pin more than 3 messages") + self.chats[0].send_message(self.message_4) + self.chats[0].pin_message(self.message_4, 'pin-to-chat') + self.chats[0].view_pinned_messages_button.click_until_presence_of_element(self.chats[0].pinned_messages_list) + if self.chats[0].pinned_messages_list.message_element_by_text(self.message_4).is_element_displayed(): + self.errors.append(self.chats[0], "Can pin more than 3 messages") + self.chats[0].pinned_messages_list.message_element_by_text(self.message_2).long_press_without_release() + self.chats[0].element_by_translation_id('unpin-from-chat').click() + self.chats[0].pin_message(self.message_4, 'pin-to-chat') + for chat in self.chats[0], self.chats[1]: + if not chat.chat_element_by_text(self.message_4).pinned_by_label.is_element_displayed(30): + self.errors.append(chat, "Message 4 is not pinned in group chat after unpinning previous one") + + def _check_pinned_messages(index): + self.chats[index].just_fyi("Check pinned messages count and content for user %s" % self.usernames[index]) + count = self.chats[index].pinned_messages_count.text + if count != '3': + self.errors.append(self.chats[index], + "Pinned messages count %s doesn't match expected 3 for user %s" % ( + count, self.usernames[index])) + self.chats[index].pinned_messages_count.click() + for message_text in self.message_1, self.message_3, self.message_4: + pinned_by = self.chats[index].pinned_messages_list.get_message_pinned_by_text(message_text) + if pinned_by.is_element_displayed(): + text = pinned_by.text.strip() + expected_text = "You" if index == 0 else self.usernames[0] + if text != expected_text: + self.errors.append(self.chats[index], + "Pinned by '%s' doesn't match expected '%s' for user %s" % ( + text, expected_text, self.usernames[index]) + ) + else: + self.errors.append(self.chats[index], + "Message '%s' is missed on Pinned messages list for user %s" % ( + message_text, self.usernames[index]) + ) + + self.loop.run_until_complete(run_in_parallel(( + (_check_pinned_messages, {'index': 0}), + (_check_pinned_messages, {'index': 1}) + ))) + + self.errors.verify_no_errors() + + @marks.testrail_id(703495) + def test_group_chat_mute_chat(self): + [self.homes[i].navigate_back_to_home_view() for i in range(3)] + + self.homes[1].just_fyi("Member 1 mutes the chat for 8 hours") + self.homes[1].mute_chat_long_press(self.chat_name, "mute-for-8-hours") + device_time = self.homes[1].driver.device_time + current_time = datetime.datetime.strptime(device_time, "%Y-%m-%dT%H:%M:%S%z") + expected_times = [current_time + datetime.timedelta(minutes=i) for i in range(479, 482)] + expected_texts = [ + "Muted until %s %s" % (exp_time.strftime('%H:%M'), "today" if current_time.hour < 16 else "tomorrow") for + exp_time in expected_times] + chat = self.homes[1].get_chat(self.chat_name) + chat.long_press_without_release() + if self.homes[1].mute_chat_button.text != transl["unmute-chat"]: + pytest.fail("Chat is not muted") + current_text = self.homes[1].mute_chat_button.unmute_caption_text + if current_text not in expected_texts: + self.errors.append(self.homes[1], "Text '%s' is not shown for muted chat" % expected_texts[1]) + self.homes[1].click_system_back_button() + try: + initial_counter = int(self.homes[1].chats_tab.counter.text) + except NoSuchElementException: + initial_counter = 0 + + self.homes[0].just_fyi("Admin sends a message") + muted_message = "Text message in the muted chat" + self.homes[0].get_chat(self.chat_name).click() + self.chats[0].send_message(muted_message) + + self.homes[1].just_fyi("Member 1 checks that chat is muted and message is received") + if chat.new_messages_grey_dot.is_element_displayed(30): + self.errors.append(self.homes[1], "New messages grey dot near chat name is shown after mute") + try: + after_mute_counter = int(self.homes[1].chats_tab.counter.text) + except NoSuchElementException: + after_mute_counter = 0 + if after_mute_counter > initial_counter: + self.errors.append(self.homes[1], + "New messages counter near chats tab button is %s after mute, but should be %s" % ( + after_mute_counter, initial_counter)) + if not chat.chat_preview.text.startswith("%s: %s" % (self.usernames[0], muted_message[:25])): + self.errors.append(self.homes[1], + "Message text '%s' is not shown in chat preview after mute" % muted_message) + chat.click() + if not self.chats[1].chat_element_by_text(muted_message).is_element_displayed(30): + self.errors.append(self.chats[1], + "Message '%s' is not shown in chat for %s (Member 1) after mute" % ( + muted_message, self.usernames[1])) + self.chats[1].navigate_back_to_home_view() + + self.chats[1].just_fyi("Member 1 unmutes the chat") + chat.long_press_without_release() + self.homes[1].mute_chat_button.double_click() + chat.long_press_without_release() + if self.homes[1].element_starts_with_text("Muted until").is_element_displayed(): + self.errors.append(self.homes[1], "Chat is still muted after being unmuted") + self.errors.verify_no_errors() + if self.homes[1].mute_chat_button.is_element_displayed(): + self.homes[1].click_system_back_button() + + unmuted_message = "Chat is unmuted now" + self.homes[2].just_fyi("Member 2 sends a message") + self.homes[2].get_chat(self.chat_name).click() + try: + initial_counter = int(self.homes[1].chats_tab.counter.text) + except NoSuchElementException: + initial_counter = 0 + self.chats[2].send_message(unmuted_message) + self.homes[1].just_fyi("Member 1 checks that chat is unmuted and message is received") + if not chat.new_messages_grey_dot.is_element_displayed(30): + self.errors.append(self.homes[1], "New messages counter near chat name is not shown after unmute") + try: + after_mute_counter = int(self.homes[1].chats_tab.counter.text) + except NoSuchElementException: + after_mute_counter = 0 + if after_mute_counter != initial_counter: + self.errors.append(self.homes[1], + "New messages counter near chats tab button is %s after unmute, but should be %s" % ( + after_mute_counter, initial_counter + 1)) + if not chat.chat_preview.text.startswith("%s: %s" % (self.usernames[2], unmuted_message)): + self.errors.append(self.homes[1], + "Message text '%s' is not shown in chat preview after unmute" % unmuted_message) + chat.click() + if not self.chats[1].chat_element_by_text(unmuted_message).is_element_displayed(30): + self.errors.append(self.chats[1], + "Message '%s' is not shown in chat for %s (Member 1) after unmute" % ( + unmuted_message, self.usernames[1])) + + self.errors.verify_no_errors() diff --git a/test/appium/tests/private_chats/test_one_to_one_chat.py b/test/appium/tests/private_chats/test_one_to_one_chat.py new file mode 100644 index 00000000000..a345b9ea81f --- /dev/null +++ b/test/appium/tests/private_chats/test_one_to_one_chat.py @@ -0,0 +1,708 @@ +import random + +import emoji +import pytest +from _pytest.outcomes import Failed +from appium.webdriver.connectiontype import ConnectionType +from selenium.common.exceptions import TimeoutException, NoSuchElementException + +from tests import marks, run_in_parallel, transl +from tests.base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from views.base_element import Button +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="three_2") +@marks.nightly +class TestOneToOneChatMultipleSharedDevicesNewUi(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + + self.loop.run_until_complete(run_in_parallel(((self.device_1.create_user, {'enable_notifications': True}), + (self.device_2.create_user, {'enable_notifications': True})))) + self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.homes = (self.home_1, self.home_2) + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.profile_1, self.profile_2 = (home.get_profile_view() for home in self.homes) + self.public_key_2 = self.home_2.get_public_key() + + self.profile_1.just_fyi("Sending contact request via Profile > Contacts") + self.username_1 = "user_1" + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(public_key=self.public_key_2, username=self.username_1) + + self.home_2.just_fyi("Accepting contact request from activity centre") + + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + + self.profile_1.just_fyi("Sending message to contact via Messages > Recent") + self.chat_1 = self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message('hey') + self.home_2.navigate_back_to_home_view() + self.chat_2 = self.home_2.get_chat(self.username_1).click() + self.message_1, self.message_2, self.message_3, self.message_4 = \ + "Message 1", "Message 2", "Message 3", "Message 4" + + @marks.smoke + @marks.xfail(reason="Might fail from time to time as reaction are set too slowly") # TODO: needs investigation + @marks.testrail_id(702730) + def test_1_1_chat_message_reaction(self): + message_from_sender = "Message sender" + self.device_1.just_fyi("Sender start 1-1 chat, set 'thumbs-up' emoji and check counter") + self.chat_1.send_message(message_from_sender) + self.chat_1.chat_element_by_text(message_from_sender).wait_for_sent_state(120) + self.chat_1.set_reaction(message_from_sender) + + message_sender = self.chat_1.chat_element_by_text(message_from_sender) + message_sender.emojis_below_message().wait_for_element_text(1) + + self.device_2.just_fyi( + "Receiver also sets 'thumbs-up' emoji and verifies counter on received message in 1-1 chat") + message_receiver = self.chat_2.chat_element_by_text(message_from_sender) + message_receiver.emojis_below_message(emoji="thumbs-up").wait_for_element_text(1, 90) + self.chat_2.add_remove_same_reaction() + message_receiver.emojis_below_message(emoji="thumbs-up").wait_for_element_text(2) + message_sender.emojis_below_message(emoji="thumbs-up").wait_for_element_text(2, 90) + + self.device_2.just_fyi( + "Receiver removes 'thumbs-up' emoji and verify that counter will decrease for both users") + self.chat_2.add_remove_same_reaction() + message_receiver.emojis_below_message(emoji="thumbs-up").wait_for_element_text(1) + message_sender.emojis_below_message(emoji="thumbs-up").wait_for_element_text(1, 90) + + self.device_2.just_fyi("Receiver sets another reaction ('love'). Check it's shown for both sender and receiver") + self.chat_2.set_reaction(message_from_sender, emoji="love") + message_receiver.emojis_below_message(emoji="thumbs-up").wait_for_element_text(1) + message_sender.emojis_below_message(emoji="thumbs-up").wait_for_element_text(1, 90) + message_receiver.emojis_below_message(emoji="love").wait_for_element_text(1) + message_sender.emojis_below_message(emoji="love").wait_for_element_text(1, 90) + + self.device_1.just_fyi("Sender votes for 'love' reaction. Check reactions counters") + self.chat_1.add_remove_same_reaction(emoji="love") + message_receiver.emojis_below_message(emoji="thumbs-up").wait_for_element_text(1) + message_sender.emojis_below_message(emoji="thumbs-up").wait_for_element_text(1) + message_receiver.emojis_below_message(emoji="love").wait_for_element_text(2, 90) + message_sender.emojis_below_message(emoji="love").wait_for_element_text(2) + + self.device_1.just_fyi("Check emojis info") + message_sender.emojis_below_message(emoji="love").long_press_without_release() + if not self.chat_1.user_list_element_by_name( + self.username_1).is_element_displayed() or not self.chat_1.user_list_element_by_name( + self.username_2).is_element_displayed(): + self.errors.append(self.chat_1, "Incorrect users are shown for 'love' reaction.") + + self.chat_1.authors_for_reaction(emoji="thumbs-up").double_click() + if not self.chat_1.user_list_element_by_name( + self.username_1).is_element_displayed() or self.chat_1.user_list_element_by_name( + self.username_2).is_element_displayed(): + self.errors.append(self.chat_1, "Incorrect users are shown for 'thumbs-up' reaction.") + self.chat_1.driver.press_keycode(4) + + self.errors.verify_no_errors() + + @marks.testrail_id(702782) + # TODO: too complex, should be splitted + def test_1_1_chat_emoji_send_reply_and_open_link(self): + self.chat_1.navigate_back_to_chat_view() + self.chat_2.navigate_back_to_chat_view() + self.home_1.just_fyi("Check that can send emoji in 1-1 chat") + emoji_name = random.choice(list(emoji.EMOJI_UNICODE)) + emoji_unicode = emoji.EMOJI_UNICODE[emoji_name] + self.chat_1.send_message(emoji.emojize(emoji_name)) + for chat in self.chat_1, self.chat_2: + if not chat.chat_element_by_text(emoji_unicode).is_element_displayed(): + self.errors.append(chat, 'Message with emoji was not sent or received in 1-1 chat') + self.chat_1.quote_message(emoji_unicode) + actual_text = self.chat_1.quote_username_in_message_input.text + if actual_text != "You": + self.errors.append( + self.chat_1, + "'You' is not displayed in reply quote snippet replying to own message, '%s' instead" % actual_text) + + self.chat_1.just_fyi("Clear quote and check there is not snippet anymore") + self.chat_1.cancel_reply_button.click() + if self.chat_1.cancel_reply_button.is_element_displayed(): + self.errors.append(self.chat_1, "Message quote kept in public chat input after it was cancelled") + + self.chat_1.just_fyi("Send reply") + self.chat_1.quote_message(emoji_unicode) + reply_to_message_from_sender = "hey, reply" + self.chat_1.send_message(reply_to_message_from_sender) + + self.chat_1.just_fyi("Receiver verifies received reply...") + if self.chat_2.chat_element_by_text(reply_to_message_from_sender).replied_message_text != emoji_unicode: + self.errors.append(self.chat_2, "No reply received in 1-1 chat") + else: + self.chat_2.just_fyi("Device 2 sets a reaction on the message reply. Device 1 checks the reaction") + self.chat_1.set_reaction(reply_to_message_from_sender) + try: + self.chat_1.chat_element_by_text( + reply_to_message_from_sender).emojis_below_message().wait_for_element_text(1) + except Failed: + self.errors.append(self.chat_1, "Reply message reaction is not shown for the sender") + + self.home_1.just_fyi("Check that link can be opened and replied from 1-1 chat") + reply = 'reply to link' + url_message = 'Test with link: https://status.im/ here should be nothing unusual.' + self.chat_1.send_message(url_message) + self.chat_2.chat_element_by_text(url_message).wait_for_element(60) + self.chat_2.quote_message(url_message) + self.chat_2.send_message(reply) + replied_message = self.chat_1.chat_element_by_text(reply) + if replied_message.replied_message_text != url_message: + self.errors.append(self.chat_1, + "Reply for '%s' not present in message received in public chat" % url_message) + + self.chat_2.just_fyi("Device 2 sets a reaction on the message with a link. Device 1 checks the reaction") + self.chat_2.set_reaction(url_message) + try: + self.chat_1.chat_element_by_text(url_message).emojis_below_message().wait_for_element_text(1) + except (Failed, NoSuchElementException): + self.errors.append(self.chat_1, "Link message reaction is not shown for the sender") + + self.home_2.just_fyi("Check 'Open in Status' option") + # url_to_open = 'http://status.app' + url_to_open = 'https://github.com/status-im/status-mobile' + self.chat_1.send_message(url_to_open) + chat_element = self.chat_2.chat_element_by_text(url_to_open) + if chat_element.is_element_displayed(120): + chat_element.click_on_link_inside_message_body() + self.chat_2.open_in_android_button.click() + try: + self.chat_2.wait_for_current_package_to_be('com.android.chrome') + except TimeoutException: + self.errors.append(self.chat_2, 'URL was not opened from 1-1 chat') + else: + self.chat_2.element_by_text("No thanks").click_if_shown() + text_element = self.chat_2.element_by_text("a free (libre) open source, mobile OS for Ethereum") + sign_in_button = Button(self.chat_2.driver, xpath="//android.view.View[@content-desc='Sign in']") + if not text_element.is_element_displayed() or not sign_in_button.is_element_displayed(): + self.errors.append(self.chat_2, 'URL was not opened from 1-1 chat') + else: + self.errors.append(self.chat_2, "Message with URL was not received") + + self.errors.verify_no_errors() + + @marks.smoke + @marks.testrail_id(702731) + def test_1_1_chat_pin_messages(self): + self.home_1.just_fyi("Check that Device1 can pin own message in 1-1 chat") + self.chat_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + self.chat_1.send_message(self.message_1) + self.chat_1.send_message(self.message_2) + self.chat_1.chat_element_by_text(self.message_1).wait_for_status_to_be(expected_status="Delivered", timeout=60) + self.chat_1.pin_message(self.message_1, action='pin-to-chat') + if not self.chat_1.chat_element_by_text(self.message_1).pinned_by_label.is_element_displayed(): + self.drivers[0].fail("Message is not pinned!") + + self.home_1.just_fyi("Check that Device2 can pin Device1 message in 1-1 chat and two pinned " + "messages are in Device1 profile") + self.chat_2.pin_message(self.message_2, action='pin-to-chat') + for chat_number, chat in enumerate([self.chat_1, self.chat_2]): + chat.pinned_messages_count.wait_for_element_text(text="2", + wait_time=60, + message="Pinned messages count is not 2 as expected!") + + chat.just_fyi("Check pinned messages are visible in Pinned panel for user %s" % (chat_number + 1)) + chat.pinned_messages_count.click() + for message in self.message_1, self.message_2: + pinned_by = chat.pinned_messages_list.get_message_pinned_by_text(message) + if pinned_by.is_element_displayed(): + text = pinned_by.text.strip() + if chat_number == 0: + expected_text = "You" if message == self.message_1 else self.username_2 + else: + expected_text = "You" if message == self.message_2 else self.username_1 + if text != expected_text: + self.errors.append(chat, + "Pinned by '%s' doesn't match expected '%s' for user %s" % ( + text, expected_text, chat_number + 1) + ) + else: + self.errors.append(chat, + "Message '%s' is missed on Pinned messages list for user %s" % ( + message, chat_number + 1) + ) + chat.click_system_back_button() + + self.home_1.just_fyi("Check that Device1 can not pin more than 3 messages and 'Unpin' dialog appears") + for message in (self.message_3, self.message_4): + self.chat_1.send_message(message) + self.chat_1.chat_element_by_text(message).wait_for_status_to_be(expected_status="Delivered", timeout=60) + self.chat_1.pin_message(message, action='pin-to-chat') + # if not self.chat_1.pin_limit_popover.is_element_displayed(): + # self.errors.append("Pin limit popover is not displayed when pinning more than 3 messages") + self.chat_1.view_pinned_messages_button.click_until_presence_of_element(self.chat_1.pinned_messages_list) + if self.chat_1.pinned_messages_list.get_pinned_messages_number() > 3 \ + or self.chat_1.pinned_messages_list.message_element_by_text(self.message_4).is_element_displayed(): + self.errors.append(self.chat_1, "Can pin more than 3 messages in chat") + else: + self.chat_1.pinned_messages_list.message_element_by_text(self.message_2).long_press_without_release() + self.home_1.just_fyi("Unpin one message so that another could be pinned") + self.chat_1.element_by_translation_id('unpin-from-chat').click() + self.chat_1.pin_message(self.message_4, 'pin-to-chat') + for chat in self.chat_1, self.chat_2: + if not chat.chat_element_by_text(self.message_4).pinned_by_label.is_element_displayed(30): + self.errors.append(chat, "Message 4 is not pinned in chat after unpinning previous one") + + for chat_number, chat in enumerate([self.chat_1, self.chat_2]): + chat.just_fyi("Check pinned messages are visible in Pinned panel for user %s" % (chat_number + 1)) + count = chat.pinned_messages_count.text + if count != '3': + self.errors.append(chat, + "Pinned messages count is %s but should be 3 for user %s" % (count, chat_number + 1)) + + self.home_1.just_fyi("Unpin one message and check it's unpinned for another user") + self.chat_1.tap_by_coordinates(500, 100) + self.chat_1.view_pinned_messages_button.click_until_presence_of_element(self.chat_1.pinned_messages_list) + pinned_message = self.chat_1.pinned_messages_list.message_element_by_text(self.message_4) + pinned_message.long_press_without_release() + self.chat_1.element_by_translation_id("unpin-from-chat").click() + try: + self.chat_2.chat_element_by_text(self.message_4).pinned_by_label.wait_for_invisibility_of_element(60) + except TimeoutException: + self.errors.append(self.chat_2, "'%s' is not unpinned!" % self.message_4) + + for chat_number, chat in enumerate([self.chat_1, self.chat_2]): + try: + chat.pinned_messages_count.wait_for_element_text(text='2', wait_time=60) + except Failed: + self.errors.append( + chat, "Pinned messages count is not 2 after unpinning the last pinned message for user %s" % ( + chat_number + 1) + ) + self.errors.verify_no_errors() + + @marks.testrail_id(702745) + def test_1_1_chat_non_latin_messages_stack_update_profile_photo(self): + self.home_1.navigate_back_to_home_view() + self.home_1.profile_button.click() + self.profile_1.edit_profile_picture(image_index=0) + + self.chat_2.just_fyi("Send messages with non-latin symbols") + self.home_1.click_system_back_button() + self.home_1.chats_tab.click() + self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message("just a text") # Sending a message here so the next ones will be in a separate line + + self.home_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + messages = ['hello', '¿Cómo estás tu año?', 'ё, доброго вечерочка', '® æ ç ♥'] + for message in messages: + self.chat_2.send_message(message) + if not self.chat_1.chat_element_by_text(message).is_element_displayed(30): + self.errors.append(self.chat_1, "Message with text '%s' was not received" % message) + + self.chat_2.just_fyi("Checking updated member photo, timestamp and username on message") + self.chat_2.hide_keyboard_if_shown() + try: + timestamp = self.chat_2.chat_element_by_text(messages[0]).timestamp + sent_time_variants = self.chat_2.convert_device_time_to_chat_timestamp() + if timestamp not in sent_time_variants: + self.errors.append(self.chat_2, + 'Timestamp on message %s does not correspond expected %s' % ( + timestamp, sent_time_variants)) + except NoSuchElementException: + self.errors.append(self.chat_2, "No timestamp on message %s" % messages[0]) + for message in [messages[1], messages[2]]: + if self.chat_2.chat_element_by_text(message).member_photo.is_element_displayed(): + self.errors.append(self.chat_2, + '%s is not stack to 1st(they are sent in less than 5 minutes)!' % message) + + self.chat_1.just_fyi("Sending message") + message = 'profile_photo' + self.chat_1.send_message(message) + self.chat_2.chat_element_by_text(message).wait_for_visibility_of_element(30) + + self.chat_1.just_fyi("Go back to chat view and checking that profile photo is updated") + if not self.chat_2.chat_message_input.is_element_displayed(): + self.home_2.get_chat(self.username_1).click() + if self.chat_2.chat_element_by_text(message).member_photo.is_element_differs_from_template( + "profile_image_in_1_1_chat.png", diff=7): + self.errors.append(self.chat_2, "Image of user in 1-1 chat is too different from template!") + self.errors.verify_no_errors() + + @marks.testrail_id(702813) + def test_1_1_chat_push_emoji(self): + message_no_pn, message = 'No PN', 'Text push notification' + + self.home_1.navigate_back_to_home_view() + self.home_2.navigate_to_chats_view() + self.home_2.profile_button.click() + + self.device_2.just_fyi("Device 2 puts app on background being on Profile view to receive PN with text") + app_package = self.device_2.driver.current_package + self.device_2.put_app_to_background() + self.device_2.open_notification_bar() + if not self.chat_1.chat_message_input.is_element_displayed(): + self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message(message) + + self.device_1.just_fyi("Device 1 puts app on background to receive emoji push notification") + self.device_1.navigate_back_to_home_view() + self.device_1.profile_button.click() + + self.device_2.just_fyi("Check text push notification and tap it") + if not self.home_2.get_pn(message): + self.device_2.click_system_back_button() + self.device_2.driver.activate_app(app_package) + self.device_2.driver.fail("Push notification with text was not received") + chat_2 = self.device_2.click_upon_push_notification_by_text(message) + + self.device_2.just_fyi("Send emoji message to Device 1 while it's on background") + self.device_1.put_app_to_background() + self.device_1.open_notification_bar() + emoji_message = random.choice(list(emoji.EMOJI_UNICODE)) + emoji_unicode = emoji.EMOJI_UNICODE[emoji_message] + chat_2.send_message(emoji.emojize(emoji_message)) + + self.device_1.just_fyi("Device 1 checks PN with emoji") + if not self.device_1.element_by_text_part(emoji_unicode).is_element_displayed(120): + self.device_1.click_system_back_button() + self.device_1.driver.activate_app(app_package) + self.device_1.driver.fail("Push notification with emoji was not received") + chat_1 = self.device_1.click_upon_push_notification_by_text(emoji_unicode) + + self.device_1.just_fyi("Check Device 1 is actually on chat") + if not (chat_1.element_by_text_part(message).is_element_displayed(15) + and chat_1.element_by_text_part(emoji_unicode).is_element_displayed()): + self.device_1.driver.fail("Failed to open chat view after tap on PN") + + self.device_1.just_fyi("Checks there are no PN after message was seen") + [home.open_notification_bar() for home in self.homes] + if self.device_2.element_by_text_part(message).is_element_displayed(): + self.errors.append(self.chat_2, "PN are keep staying after message '%s' was seen by user" % message) + if self.device_1.element_by_text_part(emoji_unicode).is_element_displayed(): + self.errors.append(self.chat_1, "PN are keep staying after message '%s' was seen by user" % emoji_unicode) + self.errors.verify_no_errors() + + @marks.testrail_id(702855) + def test_1_1_chat_edit_message(self): + for home in self.homes: + home.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + self.home_1.get_chat(self.username_2).click() + + self.device_2.just_fyi( + "Device 2 sends text message and edits it in 1-1 chat. Device 2 checks edited message is shown") + message_before_edit_1_1, message_after_edit_1_1 = "Message before edit 1-1", "AFTER" + self.chat_2.send_message(message_before_edit_1_1) + self.chat_2.chat_element_by_text(message_before_edit_1_1).wait_for_status_to_be("Delivered") + self.chat_2.edit_message_in_chat(message_before_edit_1_1, message_after_edit_1_1) + message_text_after_edit = message_after_edit_1_1 + ' (Edited)' + chat_element = self.chat_1.chat_element_by_text(message_text_after_edit) + if not chat_element.is_element_displayed(30): + self.errors.append(self.chat_1, 'No edited message in 1-1 chat displayed') + else: + self.device_1.just_fyi("Device 1 sets a reaction on the edited message. Device 2 checks the reaction") + self.chat_1.set_reaction(message_text_after_edit) + try: + self.chat_1.chat_element_by_text( + message_text_after_edit).emojis_below_message().wait_for_element_text(1) + except Failed: + self.errors.append(self.chat_1, "Message reaction is not shown for the sender") + self.errors.verify_no_errors() + + @marks.smoke + @marks.testrail_id(703391) + def test_1_1_chat_send_image_save_and_share(self): + if not self.chat_2.chat_message_input.is_element_displayed(): + self.chat_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + if not self.chat_1.chat_message_input.is_element_displayed(): + self.chat_1.navigate_to_chats_view() + self.home_1.get_chat(self.username_2).click() + + self.chat_1.just_fyi("Device 1 sends an image") + image_description = "test image" + self.chat_1.send_images_with_description(description=image_description, indexes=[0]) + + self.chat_2.just_fyi("Device 2 checks image message") + if not self.chat_2.chat_element_by_text(image_description).is_element_displayed(30): + self.chat_2.hide_keyboard_if_shown() + self.chat_2.chat_element_by_text(image_description).wait_for_visibility_of_element(30) + if not self.chat_2.chat_element_by_text( + image_description).image_in_message.is_element_image_similar_to_template('image_1_chat_view.png'): + self.errors.append(self.chat_2, "Not expected image is shown to the receiver.") + + for chat in self.chat_1, self.chat_2: + chat.just_fyi("Open the image and share it") + if not chat.chat_element_by_text(image_description).image_in_message.is_element_displayed(): + chat.hide_keyboard_if_shown() + chat.chat_element_by_text(image_description).image_in_message.click() + chat.share_image_icon_button.click() + chat.element_starts_with_text("Drive").click() + try: + chat.wait_for_current_package_to_be('com.google.android.apps.docs') + except TimeoutException: + self.errors.append(chat, + "%s can't share an image via Gmail." % ( + "Sender" if chat is self.chat_1 else "Receiver")) + chat.navigate_back_to_chat_view() + + for chat in self.chat_1, self.chat_2: + chat.just_fyi("Open the image and save it") + device_name = "sender" if chat is self.chat_1 else "receiver" + chat.chat_element_by_text(image_description).image_in_message.click() + chat.view_image_options_button.click() + chat.save_image_icon_button.click() + toast_element = chat.toast_content_element + if toast_element.is_element_displayed(): + toast_element_text = toast_element.text + if toast_element_text != chat.get_translation_by_key("photo-saved"): + self.errors.append( + chat, "Shown message '%s' doesn't match expected '%s' after saving an image for %s." % ( + toast_element_text, chat.get_translation_by_key("photo-saved"), device_name)) + else: + self.errors.append(chat, "Message about saving a photo is not shown for %s." % device_name) + chat.navigate_back_to_chat_view() + + for chat in self.chat_1, self.chat_2: + chat.just_fyi("Check that image is saved in gallery") + chat.show_images_button.click() + chat.allow_all_button.click_if_shown() + if not chat.get_image_by_index(0).is_element_image_similar_to_template("image_1_gallery_view.png"): + self.errors.append(chat, + "Image is not saved to gallery for %s." % ( + "sender" if chat is self.chat_1 else "receiver")) + chat.click_system_back_button() + + self.errors.verify_no_errors() + + @marks.testrail_id(702733) + def test_1_1_chat_text_message_delete_push_disappear(self): + if not self.chat_2.chat_message_input.is_element_displayed(): + self.chat_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + if not self.chat_1.chat_message_input.is_element_displayed(): + self.chat_1.navigate_to_chats_view() + self.home_1.get_chat(self.username_2).click() + app_package = self.chat_1.driver.current_package + + self.device_2.just_fyi("Verify Device1 can not edit and delete received message from Device2") + message_after_edit_1_1 = 'smth I should edit' + message_to_delete_for_me = 'message to delete for me' + self.chat_2.send_message(message_after_edit_1_1) + self.chat_2.chat_element_by_text(message_after_edit_1_1).wait_for_status_to_be("Delivered") + chat_1_element = self.chat_1.chat_element_by_text(message_after_edit_1_1) + chat_1_element.long_press_without_release() + for action in ("edit", "delete-for-everyone"): + if self.chat_1.element_by_translation_id(action).is_element_displayed(): + self.errors.append(self.chat_1, 'Option to %s someone else message available!' % action) + self.home_1.tap_by_coordinates(500, 100) + + self.device_2.just_fyi("Delete message for me and check it is only deleted for the author") + self.chat_2.send_message(message_to_delete_for_me) + try: + timeout = 60 + self.chat_2.chat_element_by_text(message_to_delete_for_me).wait_for_status_to_be("Delivered", timeout) + self.chat_2.delete_message_in_chat(message_to_delete_for_me, everyone=False) + except TimeoutException: + self.errors.append(self.chat_2, "Message status was not changed to 'Delivered' after %s s" % timeout) + else: + if not self.chat_2.chat_element_by_text(message_to_delete_for_me).is_element_disappeared(20): + self.errors.append(self.chat_2, "Deleted for me message is shown in chat for the author of message") + if not self.chat_2.element_by_translation_id('message-deleted-for-you').is_element_displayed(20): + self.errors.append(self.chat_2, "System message about deletion for you is not displayed") + if not self.chat_1.chat_element_by_text(message_to_delete_for_me).is_element_displayed(20): + self.errors.append(self.chat_1, "Deleted for me message is deleted for both users") + + self.device_2.just_fyi("Delete message for everyone and check it is not shown in chat preview on home") + self.chat_2.delete_message_in_chat(message_after_edit_1_1) + for chat in (self.chat_2, self.chat_1): + if chat.chat_element_by_text(message_after_edit_1_1).is_element_displayed(30): + self.errors.append(chat, "Deleted message is shown in chat view for 1-1 chat") + self.chat_1.navigate_back_to_home_view() + if self.home_1.element_by_text(message_after_edit_1_1).is_element_displayed(30): + self.errors.append(self.home_1, "Deleted message is shown on chat element on home screen") + + self.device_2.just_fyi("Send one more message and check that PN will be deleted with message deletion") + message_to_delete = 'DELETE ME' + self.home_1.put_app_to_background() + self.home_1.open_notification_bar() + self.chat_2.send_message(message_to_delete) + self.chat_2.chat_element_by_text(message_to_delete).wait_for_sent_state() + if not self.home_1.get_pn(message_to_delete): + self.home_1.click_system_back_button() + self.device_2.driver.activate_app(app_package) + self.errors.append(self.home_1, "Push notification doesn't appear") + self.chat_2.delete_message_in_chat(message_to_delete) + pn_to_disappear = self.home_1.get_pn(message_to_delete) + if pn_to_disappear: + if not pn_to_disappear.is_element_disappeared(90): + self.errors.append(self.home_1, "Push notification was not removed after initial message deletion") + self.errors.verify_no_errors() + + +@pytest.mark.xdist_group(name="four_2") +@marks.nightly +class TestOneToOneChatMultipleSharedDevicesNewUiTwo(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + + self.loop.run_until_complete(run_in_parallel(((self.device_1.create_user, {'enable_notifications': True}), + (self.device_2.create_user, {'enable_notifications': True})))) + self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.homes = (self.home_1, self.home_2) + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.profile_1, self.profile_2 = (home.get_profile_view() for home in self.homes) + self.public_key_2 = self.home_2.get_public_key() + + self.profile_1.just_fyi("Sending contact request via Profile > Contacts") + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(self.public_key_2) + + self.home_2.just_fyi("Accepting contact request from activity centre") + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + + self.profile_1.just_fyi("Sending message to contact via Messages > Recent") + self.chat_1 = self.home_1.get_chat(self.username_2).click() + self.chat_1.send_message('hey') + self.home_2.navigate_back_to_home_view() + self.chat_2 = self.home_2.get_chat(self.username_1).click() + self.message_1, self.message_2, self.message_3, self.message_4 = \ + "Message 1", "Message 2", "Message 3", "Message 4" + + @marks.skip # ToDo: can't be implemented with current SauceLabs emulators screen resolution + def test_1_1_chat_send_image_with_camera(self): + self.chat_1.just_fyi("Device 1 sends a camera image") + image_description = "camera test" + self.chat_1.send_image_with_camera(description=image_description) + for chat in self.chat_1, self.chat_2: + chat_name = "sender" if chat.driver.number == 0 else "receiver" + chat.just_fyi("%s checks image message" % chat_name.capitalize()) + chat_element = chat.chat_element_by_text(image_description) + if chat_element.is_element_displayed(30): + if not chat_element.image_in_message.is_element_image_similar_to_template('saucelabs_camera_image.png'): + self.errors.append(chat, "Not expected image is shown to the %s." % chat_name) + else: + self.errors.append(chat, "Message with camera image is not shown in chat for %s" % chat_name) + self.errors.verify_no_errors() + + @marks.testrail_id(702783) + def test_1_1_chat_is_shown_message_sent_delivered_from_offline(self): + self.home_1.just_fyi('Turn on airplane mode and check that offline status is shown on home view') + for home in self.homes: + home.driver.set_network_connection(ConnectionType.AIRPLANE_MODE) + + # Not implemented yet + # self.home_1.connection_offline_icon.wait_and_click(20) + # for element in self.home_1.not_connected_to_node_text, self.home_1.not_connected_to_peers_text: + # if not element.is_element_displayed(): + # self.errors.append( + # 'Element "%s" is not shown in Connection status screen if device is offline' % element.locator) + # self.home_1.click_system_back_button() + + message_1 = 'test message' + + self.home_2.just_fyi('Device2 checks "Sending" status when sending message from offline') + if not self.chat_2.chat_message_input.is_element_displayed(): + self.home_2.chats_tab.click() + self.home_2.get_chat(self.username_1).click() + self.chat_2.send_message(message_1) + status = self.chat_2.chat_element_by_text(message_1).status + if not (status == 'Sending' or status == 'Sent'): + self.errors.append(self.chat_2, 'Message status is not "Sending", it is "%s"!' % status) + + self.home_2.just_fyi('Device2 goes back online and checks that status of the message is changed to "delivered"') + for home in self.homes: + home.driver.set_network_connection(ConnectionType.ALL_NETWORK_ON) + + self.home_1.just_fyi('Device1 goes back online and checks that 1-1 chat will be fetched') + if not self.chat_1.chat_element_by_text(message_1).is_element_displayed(120): + self.errors.append(self.chat_1, "Message was not delivered after resending from offline") + + self.home_2.just_fyi('Device1 goes back online and checks that 1-1 chat will be fetched') + try: + self.chat_2.chat_element_by_text(message_1).wait_for_status_to_be(expected_status='Delivered', timeout=120) + except TimeoutException as e: + self.errors.append(self.chat_2, '%s after back up online!' % e.msg) + self.errors.verify_no_errors() + + @marks.testrail_id(703496) + def test_1_1_chat_mute_chat(self): + self.home_1.navigate_to_chats_view() + self.home_1.just_fyi("Mute chat") + self.home_1.mute_chat_long_press(self.username_2) + + muted_message = "should be muted" + self.chat_2.send_message(muted_message) + chat = self.home_1.get_chat(self.username_2) + if chat.new_messages_counter.is_element_displayed(30) or self.home_1.chats_tab.counter.is_element_displayed(10): + self.errors.append(self.home_1, "New messages counter is shown after mute") + if not chat.chat_preview.text.startswith(muted_message): + self.errors.append(self.home_1, "Message text '%s' is not shown in chat preview after mute" % muted_message) + chat.click() + if not self.chat_1.chat_element_by_text(muted_message).is_element_displayed(30): + self.errors.append(self.chat_1, "Message '%s' is not shown in chat for receiver after mute" % muted_message) + + self.chat_1.just_fyi("Unmute chat") + self.chat_1.navigate_back_to_home_view() + chat.long_press_without_release() + if self.home_1.mute_chat_button.text != transl["unmute-chat"]: + self.errors.append(self.home_1, "Chat is not muted") + expected_text = "Muted until you turn it back on" + if not self.home_1.element_by_text(expected_text).is_element_displayed(): + self.errors.append(self.home_1, "Text '%s' is not shown for muted chat" % expected_text) + self.home_1.mute_chat_button.double_click() + + unmuted_message = "after unmute" + self.chat_2.send_message(unmuted_message) + if not chat.new_messages_counter.is_element_displayed( + 30) or not self.home_1.chats_tab.counter.is_element_displayed(10): + self.errors.append(self.home_1, "New messages counter is not shown after unmute") + if not chat.chat_preview.text.startswith(unmuted_message): + self.errors.append(self.home_1, + "Message text '%s' is not shown in chat preview after unmute" % unmuted_message) + chat.click() + if not self.chat_1.chat_element_by_text(unmuted_message).is_element_displayed(30): + self.errors.append(self.chat_1, + "Message '%s' is not shown in chat for receiver after unmute" % unmuted_message) + + self.errors.verify_no_errors() + + @marks.testrail_id(702784) + def test_1_1_chat_delete_via_long_press_relogin(self): + self.home_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + self.chat_2.chat_message_input.wait_for_visibility_of_element() + self.home_2.just_fyi("Getting chat history") + chat_history = list() + for element in self.chat_2.chat_element_by_text(text='').message_text_content.find_elements(): + chat_history.append(element.text) + if not chat_history: + self.errors.append(self.chat_2, "No chat history was loaded") + + self.home_2.just_fyi("Deleting chat via delete button and check it will not reappear after relaunching app") + self.home_2.navigate_to_chats_view() + self.home_2.delete_chat_long_press(username=self.username_1) + chat = self.home_2.get_chat_from_home_view(self.username_1) + if chat.is_element_displayed(): + self.errors.append(self.chat_2, + "Deleted '%s' chat is shown, but the chat has been deleted" % self.username_1) + self.home_2.reopen_app(user_name=self.username_2) + if chat.is_element_displayed(15): + self.errors.append( + self.chat_2, + "Deleted chat '%s' is shown after re-login, but the chat has been deleted" % self.username_1) + chat.click() + else: + self.home_2.contacts_tab.click() + chat.find_element().click() + self.chat_2.profile_send_message_button.click() + + lost_messages = list() + for message_text in chat_history: + if not self.chat_2.chat_element_by_text(message_text).is_element_displayed(): + lost_messages.append(message_text) + if lost_messages: + self.errors.append(self.chat_2, + "Message(s) missed in 1-1 chat after deleting the chat and relogin: %s" % lost_messages) + self.errors.verify_no_errors() diff --git a/test/appium/tests/profile/test_fallback.py b/test/appium/tests/profile/test_fallback.py new file mode 100644 index 00000000000..57a1d3784cc --- /dev/null +++ b/test/appium/tests/profile/test_fallback.py @@ -0,0 +1,383 @@ +import pytest +from selenium.common import TimeoutException + +from base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from support.helpers import generate_wallet_address +from tests import marks, run_in_parallel, transl +from users import transaction_senders +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="one_3") +@marks.nightly +class TestFallbackMultipleDevice(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(3) + self.sign_in_1, self.sign_in_2, self.sign_in_3 = SignInView(self.drivers[0]), SignInView( + self.drivers[1]), SignInView(self.drivers[2]) + self.home_1, self.home_2, self.home_3 = self.sign_in_1.get_home_view(), self.sign_in_2.get_home_view(), \ + self.sign_in_3.get_home_view() + self.sign_in_1.just_fyi("Device 1: create a new user") + self.sign_in_3.just_fyi("Device 3: create a new user") + self.loop.run_until_complete( + run_in_parallel(((self.sign_in_1.create_user,), + (self.sign_in_3.create_user,)))) + self.user_name_1, self.user_name_3 = self.home_1.get_username(), self.home_3.get_username() + self.profile_1 = self.home_1.profile_button.click() + self.profile_2 = self.home_2.get_profile_view() + self.sign_in_3.just_fyi("Device 3: get public key") + self.recovery_phrase, self.public_key_3 = self.loop.run_until_complete( + run_in_parallel(((self.profile_1.backup_recovery_phrase, {}), + (self.home_3.get_public_key, {})))) + self.home_2.driver.get_clipboard_text() # just pinging 2nd device to save the connection + self.profile_1.navigate_to_chats_view() + self.home_1.just_fyi("Device 1: add the 3rd user as a contact") + self.home_1.add_contact(self.public_key_3) + self.home_3.just_fyi("Device 3: accepting contact request from the 1st user") + self.home_3.handle_contact_request(self.user_name_1) + self.profile_1.just_fyi("Device 1: get sync code") + self.home_1.profile_button.click() + self.sync_code = self.profile_1.get_sync_code() + + @marks.testrail_id(740220) + def test_fallback_sync_with_error(self): + self.sign_in_2.just_fyi("Device 2: try syncing profile") + self.sign_in_2.sync_profile(sync_code=self.sync_code) + self.sign_in_2.progress_screen_title.wait_for_element() + assert self.sign_in_2.progress_screen_title.text == "Oops, something’s wrong!" + self.home_3.chats_tab.is_element_displayed() # just pinging 3rd device to save the connection + + @marks.testrail_id(740221) + def test_fallback_with_correct_seed_phrase(self): + self.sign_in_2.just_fyi("Device 2: recover a profile with backed up seed phrase") + self.sign_in_2.try_seed_phrase_button.click() + self.sign_in_2.recover_access(passphrase=self.recovery_phrase, after_sync_code=True) + self.profile_1.click_system_back_button() + + self.home_2.just_fyi("Getting device 2 name") + self.home_2.profile_button.click() + self.profile_2.syncing_button.scroll_and_click() + self.profile_2.paired_devices_button.click() + device_2_name = self.profile_2.get_current_device_name() + self.profile_2.navigate_back_to_home_view() + + self.home_3.chats_tab.is_element_displayed() # just pinging 3rd device to save the connection + + self.profile_1.just_fyi("Check device 2 is shown in not paired devices list in profile 1") + device_1_name = self.profile_1.get_current_device_name() + device_element = self.profile_1.get_paired_device_by_name(device_2_name) + if device_element.is_element_displayed(): + if not device_element.get_pair_button.is_element_displayed(): + self.errors.append( + self.profile_1, + "Pair button is absent for the device 2 inside Paired devices list of profile 1 before pairing") + else: + self.errors.append(self.profile_1, + "Device 2 is not shown in Paired devices list for device 1 before pairing") + self.profile_1.navigate_back_to_home_view() + + for home in self.home_1, self.home_2: + home.notifications_unread_badge.wait_for_visibility_of_element(30) + home.open_activity_center_button.click_until_presence_of_element(home.close_activity_centre) + + self.home_1.just_fyi("Checking pairing request on device 1") + a_c_element = self.home_1.get_activity_center_element_by_text(transl['review-pairing-request']) + if a_c_element.title.text != transl['new-device-detected']: + self.errors.append(self.home_1, + "Notification with title '%s' is not shown in the activity center for the device 1" % + transl[ + 'new-device-detected']) + a_c_element.review_pairing_request_button.click() + device_id_1 = self.home_1.get_new_device_installation_id() + + self.home_3.chats_tab.is_element_displayed() # just pinging 3rd device to save the connection + + self.home_2.just_fyi("Checking sync profile on device 2") + a_c_element = self.home_2.get_activity_center_element_by_text(transl['more-details']) + if a_c_element.title.text != transl['sync-your-profile']: + self.errors.append(self.home_2, + "Notification with title '%s' is not shown in the activity center for the device 2" % + transl[ + 'sync-your-profile']) + a_c_element.more_details_button.click() + device_id_2 = self.home_2.get_new_device_installation_id() + + if device_id_1 != device_id_2: + self.errors.append(self.home_2, "Device ids don't match on the activity center notifications") + + self.home_1.just_fyi("Confirm pairing request on device 1") + self.home_1.element_by_translation_id('pair-and-sync').click() + self.home_2.element_by_translation_id('close').click() + + self.home_1.close_activity_centre.click() + self.home_2.close_activity_centre.click() + + self.home_3.chats_tab.is_element_displayed() # just pinging 3rd device to save the connection + + self.home_1.just_fyi("Device 1: Check that the device 2 is shown in paired devices list") + self.home_1.profile_button.click() + self.profile_1.syncing_button.scroll_and_click() + self.profile_1.paired_devices_button.click() + device_element = self.profile_1.get_paired_device_by_name(device_2_name) + if device_element.is_element_displayed(): + if not device_element.get_unpair_button.is_element_displayed(): + self.errors.append( + self.profile_1, + "Unpair button is absent for the device 2 inside Paired devices list of profile 1 after pairing") + else: + self.errors.append(self.profile_1, + "Device 2 is not shown in Paired devices list for device 1 after pairing") + + self.home_2.just_fyi("Device 2: Check that the device 1 is shown paired devices list") + self.home_2.profile_button.click() + self.profile_2.syncing_button.scroll_and_click() + self.profile_2.paired_devices_button.click() + device_element = self.profile_2.get_paired_device_by_name(device_1_name) + if device_element.is_element_displayed(): + if not device_element.get_unpair_button.is_element_displayed(): + self.errors.append( + self.profile_2, + "Unpair button is absent for the device 1 inside Paired devices list of profile 2 after pairing") + else: + self.errors.append(self.profile_2, + "Device 1 is not shown in Paired devices list for device 2 after pairing") + + self.home_3.just_fyi("Device 3: send a message to user 1") + self.home_3.chats_tab.click() + chat_3 = self.home_3.get_chat(self.user_name_1).click() + message = "Test message" + chat_3.send_message(message) + + def _check_message(home_view, index): + home_view.just_fyi("Device %s: check the message from the user 3 is received" % index) + home_view.navigate_to_chats_view() + try: + chat_element = home_view.get_chat(self.user_name_3) + chat_element.wait_for_visibility_of_element(60) + chat_view = chat_element.click() + chat_view.chat_element_by_text(message).wait_for_visibility_of_element(60) + except TimeoutException: + self.errors.append(home_view, "Message is not received by the user %s" % index) + + self.loop.run_until_complete( + run_in_parallel(((_check_message, {'home_view': self.home_1, 'index': 1}), + (_check_message, {'home_view': self.home_2, 'index': 2})))) + + self.errors.verify_no_errors() + + @marks.testrail_id(741054) + def test_fallback_add_key_pair(self): + expected_addresses = generate_wallet_address(passphrase=self.recovery_phrase, number=4) + account_to_add = transaction_senders['ETH_1'] + self.home_1.navigate_back_to_home_view() + self.home_2.navigate_back_to_home_view() + wallet_1 = self.home_1.wallet_tab.click() + wallet_2 = self.home_2.wallet_tab.click() + + wallet_1.just_fyi("Device 1: add a new regular account") + regular_account_name = "New regular account" + regular_derivation_path = wallet_1.add_regular_account(account_name=regular_account_name) + regular_account_address = wallet_1.get_account_address().split(':')[-1] + account_element = wallet_1.get_account_element(account_name=regular_account_name) + wallet_1.close_account_button.click_until_presence_of_element(account_element) + + if regular_account_address != expected_addresses[1]: + self.errors.append(wallet_1, "Newly added regular account address %s doesn't match expected %s" % ( + regular_account_address, expected_addresses[1])) + if regular_account_address not in expected_addresses: + self.errors.append(wallet_1, + "Newly added regular account address %s is not in the list of expected addresses %s" % ( + regular_account_address, expected_addresses)) + + wallet_1.just_fyi("Device 1: add a new key pair account by importing recovery phrase") + account_element.swipe_left_on_element() + imported_key_pair_account_name = "Imported account" + imported_key_pair_name = "Imported key pair" + imported_key_pair_account_address = '0x' + account_to_add['address'].lower() + imported_key_pair_derivation_path = wallet_1.import_key_pair_using_recovery_phrase( + account_name=imported_key_pair_account_name, + passphrase=account_to_add['passphrase'], + key_pair_name=imported_key_pair_name) + + wallet_1.close_account_button.click_until_presence_of_element(account_element) + + wallet_1.just_fyi("Device 1: verify default and added key pairs details") + account_element.swipe_left_on_element() + wallet_1.get_account_element(account_name=imported_key_pair_account_name).swipe_left_on_element() + if not wallet_1.add_account_button.is_element_displayed(): + wallet_1.get_account_element(account_name=imported_key_pair_account_name).swipe_left_on_element() + wallet_1.add_account_button.click() + wallet_1.create_account_button.click() + wallet_1.edit_key_pair_button.click() + expected_texts_regular = [ + regular_account_name, + "%s...%s" % (regular_account_address[:5], regular_account_address[-3:]) + ] + for text in expected_texts_regular: + if not wallet_1.default_key_pair_container.get_child_element_by_text_part(text).is_element_displayed(): + self.errors.append(wallet_1, "Newly added regular account is not shown in default key pair list") + break + expected_texts_key_pair = [ + imported_key_pair_account_name, imported_key_pair_name, + "%s...%s" % (imported_key_pair_account_address[:5], imported_key_pair_account_address[-3:]) + ] + for text in expected_texts_key_pair: + if not wallet_1.added_key_pair_container.get_child_element_by_text_part(text).is_element_displayed(): + self.errors.append(wallet_1, "Newly added regular account is not shown in default key pair list") + break + + wallet_1.just_fyi("Device 1: add a new key pair account by generating a new key pair") + generated_key_pair_account_name = "Generated account" + generated_key_pair_name = "Generated key pair" + generated_key_pair_derivation_path, generated_passphrase = wallet_1.generate_new_key_pair( + account_name=generated_key_pair_account_name, + key_pair_name=generated_key_pair_name) + generated_key_pair_account_address = wallet_1.get_account_address().split(':')[-1] + wallet_1.close_account_button.click_until_presence_of_element(account_element) + + expected_addresses = generate_wallet_address(passphrase=generated_passphrase, number=4) + if generated_key_pair_account_address != expected_addresses[0]: + self.errors.append(wallet_1, "Generated key pair account address %s doesn't match expected %s" % ( + generated_key_pair_account_address, expected_addresses[0])) + if generated_key_pair_account_address not in expected_addresses: + self.errors.append(wallet_1, + "Generated key pair account address %s is not in the list of expected addresses %s" % ( + generated_key_pair_account_address, expected_addresses)) + + self.home_2.just_fyi("Device 2: check imported accounts are shown before importing key pair") + self.home_2.profile_button.click() + self.profile_2.profile_wallet_button.click() + self.profile_2.key_pairs_and_accounts_button.click() + if not self.profile_2.get_missing_key_pair_by_name(key_pair_name=imported_key_pair_name).is_element_displayed(): + self.errors.append(self.profile_2, + "New imported key pair is not shown in profile as missing before importing") + if not self.profile_2.get_missing_key_pair_by_name( + key_pair_name=generated_key_pair_name).is_element_displayed(): + self.errors.append(self.profile_2, "Generated key pair is not shown in profile as missing before importing") + if not self.profile_2.get_key_pair_account_by_name(account_name=regular_account_name).is_element_displayed(): + self.errors.append( + self.profile_2, + "Newly added regular account is not shown in profile as on device before importing key pair") + self.profile_2.get_missing_key_pair_by_name(key_pair_name=generated_key_pair_name).options_button.click() + if not self.profile_2.import_by_entering_recovery_phrase_button.is_element_displayed(): + self.errors.append(self.profile_2, "Can not import key pair account from profile") + self.profile_2.navigate_back_to_home_view() + self.home_2.wallet_tab.click() + + wallet_2.just_fyi("Device 2: import key pair") + wallet_2.get_account_element(account_name=regular_account_name).swipe_left_on_element() + wallet_2.get_account_element(account_name=imported_key_pair_account_name).click() + wallet_2.element_by_translation_id("import-key-pair").click() + self.sign_in_2.passphrase_edit_box.send_keys(account_to_add['passphrase']) + wallet_2.slide_and_confirm_with_password() + + self.home_2.just_fyi("Device 2: check imported accounts are shown in profile as added after importing key pair") + self.home_2.profile_button.click() + self.profile_2.profile_wallet_button.click() + self.profile_2.key_pairs_and_accounts_button.click() + if self.profile_2.get_key_pair_account_by_name(account_name=regular_account_name).is_element_displayed(): + address_text = self.profile_2.get_key_pair_account_by_name(account_name=regular_account_name).address.text + if address_text != '...'.join((regular_account_address[:5], regular_account_address[-3:])): + self.errors.append( + self.profile_2, + "Incorrect wallet address if shown for regular account after importing: " + address_text) + else: + self.errors.append(self.profile_2, + "Newly added regular account is not shown in profile after importing key pair") + + account_element = self.profile_2.get_key_pair_account_by_name( + account_name=imported_key_pair_account_name) + if account_element.is_element_displayed(): + address_text = account_element.address.text + if address_text != '...'.join( + (imported_key_pair_account_address[:5], imported_key_pair_account_address[-3:])): + self.errors.append( + self.profile_2, + "Incorrect wallet address if shown for imported key pair account after importing: " + address_text) + else: + self.errors.append( + self.profile_2, + "Imported key pair account is not shown in profile as on device after importing key pair") + + if not self.profile_2.get_missing_key_pair_by_name( + key_pair_name=generated_key_pair_name).is_element_displayed(): + self.errors.append( + self.profile_2, + "Generated key pair account is not shown in profile as missing after importing the first key pair") + self.profile_2.navigate_back_to_home_view() + self.home_2.wallet_tab.click() + + # ToDo: Arbiscan API is down, looking for analogue + # wallet_2.just_fyi("Device 2: check wallet balance") + # wallet_2.set_network_in_wallet(network_name='Arbitrum') + # expected_balance = self.network_api.get_balance(key_pair_account_address) + # shown_balance = wallet_2.get_asset(asset_name='Ether').get_amount() + # if shown_balance != round(expected_balance, 5): + # self.errors.append("Device 2: ETH balance %s doesn't match expected %s" % (shown_balance, expected_balance)) + + wallet_2.just_fyi("Device 2: check derivation paths of the regular and key pair accounts") + account_element = wallet_2.get_account_element(account_name=regular_account_name) + account_element.click() + wallet_2.about_tab.click() + der_path = wallet_2.account_about_derivation_path_text.text + if der_path != regular_derivation_path: + self.errors.append(wallet_2, "Incorrect derivation path %s is shown for the regular account" % der_path) + wallet_2.close_account_button.click_until_presence_of_element(account_element) + account_element.swipe_left_on_element() + wallet_2.get_account_element(account_name=imported_key_pair_account_name).click() + wallet_2.about_tab.click() + der_path = wallet_2.account_about_derivation_path_text.text + if der_path != imported_key_pair_derivation_path: + self.errors.append(wallet_2, + "Incorrect derivation path %s is shown for the imported key pair account" % der_path) + wallet_2.close_account_button.click_until_presence_of_element(account_element) + account_element.swipe_left_on_element() + wallet_2.get_account_element(account_name=generated_key_pair_account_name).click() + wallet_2.element_by_translation_id("import-key-pair").click() + self.sign_in_2.passphrase_edit_box.send_keys(generated_passphrase) + wallet_2.slide_and_confirm_with_password() + wallet_2.get_account_element(account_name=generated_key_pair_account_name).click() + wallet_2.about_tab.click() + der_path = wallet_2.account_about_derivation_path_text.text + if der_path != generated_key_pair_derivation_path: + self.errors.append(wallet_2, + "Incorrect derivation path %s is shown for the generated key pair account" % der_path) + if not wallet_2.element_by_text_part(generated_key_pair_account_address).is_element_displayed(): + self.errors.append( + "Generated key pair address %s is absent in About tab" % generated_key_pair_account_address) + self.errors.verify_no_errors() + + @marks.testrail_id(740222) + def test_fallback_validate_seed_phrase(self): + self.sign_in_2.reopen_app(sign_in=False) + + self.sign_in_2.just_fyi("Device 2: try syncing profile") + self.sign_in_2.explore_new_status_button.click_if_shown() + self.sign_in_2.sync_profile(sync_code=self.sync_code, first_user=False) + self.sign_in_2.progress_screen_title.wait_for_element() + assert self.sign_in_2.progress_screen_title.text == "Oops, something’s wrong!" + + self.sign_in_2.just_fyi("Device 2: try invalid passphrase") + self.sign_in_2.try_seed_phrase_button.click() + self.sign_in_2.passphrase_edit_box.send_keys(' '.join(['asset'] * 12)) + self.sign_in_2.continue_button.click() + if not self.sign_in_2.element_by_translation_id('seed-phrase-invalid').is_element_displayed(): + self.errors.append(self.sign_in_2, "Error message is not displayed for invalid recovery phrase") + + self.sign_in_2.just_fyi("Device 2: try creating an account with another valid passphrase") + self.sign_in_2.passphrase_edit_box.clear() + self.sign_in_2.passphrase_edit_box.send_keys(transaction_senders['A']['passphrase']) + self.sign_in_2.continue_button.click() + if not self.sign_in_2.password_input.is_element_displayed(): + self.errors.append(self.sign_in_2, "Can't recover an access with a valid passphrase") + self.sign_in_2.click_system_back_button(times=2) + + self.sign_in_2.just_fyi("Device 2: try recovering an account which is already synced") + self.sign_in_2.passphrase_edit_box.clear() + self.sign_in_2.passphrase_edit_box.send_keys(self.recovery_phrase) + self.sign_in_2.continue_button.click() + if not self.sign_in_2.element_by_translation_id('account-already-exist-error').is_element_displayed(): + self.errors.append(self.sign_in_2, "Error is not shown for already synced account") + + self.errors.verify_no_errors() diff --git a/test/appium/tests/profile/test_profile.py b/test/appium/tests/profile/test_profile.py new file mode 100644 index 00000000000..da429a462e2 --- /dev/null +++ b/test/appium/tests/profile/test_profile.py @@ -0,0 +1,371 @@ +import math +from random import choice +from string import printable + +import pytest +from currency_converter import CurrencyConverter +from selenium.common import NoSuchElementException, TimeoutException + +from tests import marks, run_in_parallel +from tests.base_test_case import create_shared_drivers, MultipleSharedDeviceTestCase +from users import transaction_senders +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="one_1") # ToDo: find group +@marks.nightly +class TestProfileOneDevice(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in = SignInView(self.drivers[0]) + self.home = self.sign_in.create_user() + self.profile = self.home.profile_button.click() + self.username = self.profile.default_username_text.text + + @marks.testrail_id(741966) + def test_profile_back_up_seed_phrase_validation(self): + self.profile.backup_recovery_phrase_button.click() + for checkbox in self.profile.checkbox_button.find_elements(): + checkbox.click() + self.profile.button_one.click() + recovery_phrase = self.profile.get_recovery_phrase() + self.profile.button_one.click() + word_number = self.profile.recovery_phrase_word_number.number + self.profile.get_incorrect_word_button(recovery_phrase[word_number]).click() + if not self.profile.element_by_translation_id('oops-wrong-word').is_element_displayed(): + self.errors.append(self.profile, + "Error message 'Oops! Wrong word' is not shown on the first seed phrase validation step") + self.profile.get_incorrect_word_button(recovery_phrase[word_number]).click() + if not self.profile.element_by_translation_id( + 'do-not-cheat').is_element_displayed() or not self.profile.element_by_translation_id( + 'do-not-cheat-description').is_element_displayed(): + self.errors.append( + self.profile, + "Expected messages are not shown for the second attempt with incorrect recovery phrase word") + self.profile.button_one.click() + if not self.profile.recovery_phrase_table.is_element_displayed(): + self.errors.append(self.profile, "Recovery phrase is not shown after failed attempts to check it") + self.errors.verify_no_errors() # the test fails if the recovery phrase is not shown on this step + + self.profile.button_one.click() + self.profile.fill_recovery_phrase_checking_words(recovery_phrase) + if not self.profile.element_by_translation_id('written-seed-ready').is_element_displayed(): + self.errors.append(self.profile, "Can't complete backup") + + self.profile.navigate_back_to_main_profile_view() + if not self.profile.backup_recovery_phrase_button.is_element_displayed(): + self.errors.append(self.profile, "Backup recovery phrase button is not shown after failed validation") + self.profile.reopen_app(sign_in=True, user_name=self.username) + self.home.profile_button.click() + if not self.profile.backup_recovery_phrase_button.is_element_displayed(): + self.errors.append(self.profile, "Backup recovery phrase button is not shown after relogin") + self.errors.verify_no_errors() + + @marks.testrail_id(741967) + def test_profile_change_currency(self): + self.sign_in.reopen_app(sign_in=False) + self.sign_in.recover_access(passphrase=transaction_senders['ETH_2']['passphrase'], second_user=True) + wallet = self.sign_in.wallet_tab.click() + amount_text = float(wallet.total_balance_text.text[1:]) + expected_amount = CurrencyConverter().convert(amount_text, 'USD', 'EUR') + wallet.profile_button.click() + self.profile.change_currency('EUR') + self.profile.click_system_back_button() + balance_text = wallet.total_balance_text.text + new_currency, new_amount = balance_text[0], float(balance_text[1:]) + if new_currency != '€': + self.errors.append(wallet, "Currency in wallet is %s but should be €" % new_currency) + if not math.isclose(new_amount, expected_amount, rel_tol=0.1): + self.errors.append(wallet, "Amount in wallet is %s but expected to be %s" % (new_amount, expected_amount)) + self.errors.verify_no_errors() + + +@pytest.mark.xdist_group(name="five_2") +@marks.nightly +class TestProfileMultipleDevices(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(2) + self.device_1, self.device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) + self.loop.run_until_complete(run_in_parallel(((self.device_1.create_user,), (self.device_2.create_user,)))) + self.homes = self.home_1, self.home_2 = self.device_1.get_home_view(), self.device_2.get_home_view() + self.username_1, self.username_2 = self.home_1.get_username(), self.home_2.get_username() + self.public_key_2 = self.home_2.get_public_key() + self.profile_1 = self.home_1.get_profile_view() + self.profile_2 = self.home_2.get_profile_view() + self.home_1.navigate_to_chats_view() + self.home_1.add_contact(self.public_key_2) + self.home_2.navigate_to_chats_view() + self.home_2.handle_contact_request(self.username_1) + + self.home_1.get_chat(self.username_2).wait_for_visibility_of_element() + self.chat_2 = self.home_2.get_chat(self.username_1).click() + + self.home_1.just_fyi("Open community to message") + self.home_1.communities_tab.click() + self.community_name = "open community" + self.channel_name = 'general' + self.home_1.create_community(community_type="open") + self.channel_1 = self.home_1.get_to_community_channel_from_home(self.community_name) + + self.community_1, self.community_2 = self.home_1.get_community_view(), self.home_2.get_community_view() + self.community_1.invite_to_community(self.community_name, self.username_2) + self.channel_1 = self.home_1.get_to_community_channel_from_home(self.community_name) + self.community_2.join_community() + self.community_2.get_channel(self.channel_name).click() + self.message_community_2 = "message community from User 2" + self.chat_2.send_message(self.message_community_2) + self.message_community_1 = 'message in community from User 1' + self.channel_1.send_message(self.message_community_1) + self.channel_1.navigate_to_chats_view() + self.chat_1 = self.home_1.get_chat(self.username_2).click() + self.message_1_1_from_1 = "message 1-1 chat from user 1" + self.message_1_1_from_2 = "message 1-1 chat from user 2" + self.chat_2.navigate_to_chats_view() + self.home_2.get_chat(self.username_1).click() + self.chat_1.send_message(self.message_1_1_from_1) + self.chat_2.send_message(self.message_1_1_from_2) + self.new_username_1 = 'user 1' + + @marks.smoke + @marks.testrail_id(741968) + def test_profile_change_username(self): + self.home_1.navigate_back_to_home_view() + self.home_1.just_fyi("User 1 updates username in the profile") + self.home_1.profile_button.click() + self.profile_1.edit_username(self.new_username_1) + self.profile_1.navigate_back_to_main_profile_view() + if self.profile_1.default_username_text.text != self.new_username_1: + pytest.fail("Device 1: Username is not updated") + + self.home_1.just_fyi("User 1 sends a new message in 1-1 chat") + self.home_1.navigate_to_chats_view() + self.home_1.get_chat(self.username_2).click() + new_message = 'new message' + self.chat_1.send_message(new_message) + self.chat_1.chat_element_by_text(new_message).wait_for_sent_state() + + self.home_2.just_fyi("User 2 checks updated username in the contacts list") + self.chat_2.navigate_to_chats_view() + self.home_2.contacts_tab.click() + if not self.home_2.get_chat_from_home_view(self.new_username_1).is_element_displayed(30): + self.errors.append(self.home_2, "Updated username is absent in the contacts list") + + self.home_2.just_fyi("User 2 checks updated username in the 1-1 chat and user's profile") + self.home_2.recent_tab.click() + if self.home_2.get_chat_from_home_view(self.new_username_1).is_element_displayed(): + self.home_2.get_chat(self.new_username_1).click() + message_element = self.chat_2.chat_element_by_text(new_message) + message_element.wait_for_element(30) + if message_element.username.text != self.new_username_1: + self.errors.append(self.chat_2, + "Updated username '%s' is not shown in the 1-1 chat" % self.new_username_1) + message_element.member_photo.click() + if self.chat_2.get_profile_view().contact_name_text.text != self.new_username_1: + self.errors.append(self.chat_2, "Updated username is not shown in the user's profile") + else: + self.errors.append(self.home_2, "Can't find chat with updated username") + + self.home_2.just_fyi("User 2 checks updated username in the community channel and mentions list") + self.home_2.navigate_to_communities_view() + self.home_2.get_to_community_channel_from_home(self.community_name) + self.chat_2.chat_element_by_text(self.message_community_1).wait_for_element(30) + if self.chat_2.chat_element_by_text(self.message_community_1).username.text != self.new_username_1: + self.errors.append(self.chat_2, + "Updated username '%s' is not shown in the community channel" % self.new_username_1) + self.chat_2.chat_message_input.send_keys("@") + self.chat_2.mentions_list.wait_for_element() + if not self.chat_2.user_list_element_by_name(self.new_username_1).is_element_displayed(): + self.errors.append(self.chat_2, "Updated username is not shown in the mentions list") + + self.home_1.just_fyi("User 1 checks that updated username is shown in the login screen") + try: + self.home_1.reopen_app(sign_in=True, user_name=self.new_username_1) + except NoSuchElementException: + self.errors.append(self.home_1, "Updated username is absent on the login screen") + + self.errors.verify_no_errors() + + @marks.smoke + @marks.testrail_id(741969) + def test_profile_change_profile_photo(self): + self.home_2.navigate_back_to_home_view() + self.home_2.just_fyi("User 2 updates profile image") + self.home_2.profile_button.click() + self.profile_2.edit_profile_picture(image_index=0) + if self.profile_2.user_avatar.is_element_differs_from_template("profile_image.png", diff=7): + self.errors.append(self.profile_2, "Updated profile image is not shown for the profile owner") + + self.home_1.just_fyi("User 1 checks updated profile photo in the community channel") + self.home_1.click_system_back_button_until_absence_of_element(self.profile_1.default_username_text) + self.home_1.navigate_to_communities_view() + self.home_1.get_to_community_channel_from_home(self.community_name) + self.chat_1.chat_element_by_text(self.message_community_2).wait_for_element(30) + if self.chat_1.chat_element_by_text(self.message_community_2).member_photo.is_element_differs_from_template( + "profile_image_in_1_1_chat.png", diff=7): + self.errors.append(self.chat_1, "Updated image is not shown in the community channel") + self.chat_1.navigate_to_chats_view() + + self.home_1.just_fyi("User 1 checks updated profile photo in the contacts list") + self.home_1.contacts_tab.click() + chat_element = self.home_1.get_chat(self.username_2) + if chat_element.is_element_displayed(): + if chat_element.chat_image.is_element_differs_from_template("profile_image_in_1_1_chat.png", diff=7): + self.errors.append(self.home_1, + "Chat image of user 2 doesn't match expected in the contacts list of user 1") + else: + self.errors.append(self.home_1, "User 2 is absent in the contacts list of user 1") + + self.home_1.just_fyi("User 1 checks updated profile image in the 1-1 chat and user's profile") + self.home_1.recent_tab.click() + if chat_element.is_element_displayed(): + chat_element.click() + message_element = self.chat_1.chat_element_by_text(self.message_1_1_from_2) + message_element.wait_for_element(30) + if message_element.member_photo.is_element_differs_from_template("profile_image_in_1_1_chat.png", diff=7): + self.errors.append(self.chat_1, "Updated profile image is not shown in the 1-1 chat") + message_element.member_photo.click() + if self.chat_1.get_profile_view().profile_picture.is_element_differs_from_template( + "profile_image_contacts_profile.png"): + self.errors.append(self.chat_1, "Updated profile image is not shown in the user's profile") + self.chat_1.click_system_back_button() + else: + self.errors.append(self.home_2, "Can't find chat with user 2") + + self.home_2.just_fyi("User 2 checks that updated profile image is shown in the login screen") + self.home_2.reopen_app(sign_in=False) + if self.device_2.get_user_profile_by_name(self.username_2).profile_image.is_element_differs_from_template( + "profile_image_sign_in_view.png", diff=7): + self.errors.append(self.device_2, "Updated profile image is not shown on the sign in view") + self.device_2.sign_in(self.username_2) + self.errors.verify_no_errors() + + @marks.testrail_id(741970) + def test_profile_set_bio(self): + self.home_2.just_fyi("Device 2 edits bio in profile") + self.home_2.navigate_back_to_home_view() + self.home_2.profile_button.click() + self.profile_2.edit_profile_button.click() + self.profile_2.edit_bio_button.click() + new_bio = ''.join(choice(printable) for _ in range(240)) + self.profile_2.edit_profile_input.send_keys(new_bio + '1') + if not self.profile_2.element_by_translation_id('bio-is-too-long').is_element_displayed(): + self.errors.append(self.profile_2, "Error message is not shown for 241 chars in the edit bio input field") + self.profile_2.driver.press_keycode(67) # deleting the last character + self.profile_2.save_bio_button.click() + self.profile_2.navigate_back_to_main_profile_view() + if self.profile_2.bio_text.text != new_bio: + self.errors.append(self.profile_2, "Updated bio is not displayed in the user's profile") + self.profile_2.click_system_back_button() + + self.home_1.just_fyi("User 1 checks updated bio in the user's 2 profile") + self.home_1.click_system_back_button_until_absence_of_element(self.profile_1.default_username_text) + self.home_1.navigate_to_chats_view() + self.home_1.contacts_tab.click() + self.home_1.get_chat(self.username_2).find_element().click() + if self.chat_1.contact_bio_text.text != new_bio: + self.errors.append(self.profile_1, "Updated bio is not displayed in the contact's profile") + self.errors.verify_no_errors() + + @marks.testrail_id(741971) + def test_profile_change_accent_color(self): + self.home_1.navigate_back_to_home_view() + self.home_1.just_fyi("User 1 changes accent colour") + self.home_1.profile_button.click() + self.profile_1.change_accent_colour(colour_name='magenta') + self.profile_1.click_system_back_button() + red, green, blue = self.profile_1.user_avatar.get_element_rgb() + if red < 70 or blue < 30: + self.errors.append( + self.profile_1, + "Accent color is not properly applied in own profile, (red {}%, green {}%, blue {}%)".format(red, + green, + blue)) + self.home_2.just_fyi("User 2 checks updated accent color of user 1 in 1-1 chat and his profile") + self.home_2.click_system_back_button_until_absence_of_element(self.profile_2.default_username_text) + self.home_2.navigate_to_chats_view() + if self.home_2.get_chat(self.username_1).is_element_displayed(): + chat_element = self.home_2.get_chat(self.username_1) + else: + chat_element = self.home_2.get_chat(self.new_username_1) + red, green, blue = chat_element.chat_image.get_element_rgb() + if red < 90 or blue < 40: + self.errors.append( + self.profile_2, + "Accent color is not applied for the chat preview image, (red {}%, green {}%, blue {}%)".format( + red, + green, + blue)) + chat_element.click() + self.chat_2.chat_message_input.send_keys('test') + red, green, blue = self.chat_2.send_message_button.get_element_rgb() + if red < 90 or blue < 40: + self.errors.append( + self.profile_2, + "Accent color is not applied for the send message button in chat, (red {}%, green {}%, blue {}%)".format( + red, + green, + blue)) + self.chat_2.options_button.click() + self.chat_2.view_profile_button.click() + red, green, blue = self.profile_2.profile_picture.get_element_rgb() + if red < 90 or blue < 40: + self.errors.append( + self.profile_2, + "Accent color is not applied for the profile picture, (red {}%, green {}%, blue {}%)".format( + red, + green, + blue)) + red, green, blue = self.chat_2.profile_send_message_button.get_element_rgb() + if red < 90 or blue < 40: + self.errors.append( + self.profile_2, + "Accent color is not applied for the send message button in profile, (red {}%, green {}%, blue {}%)".format( + red, + green, + blue)) + self.errors.verify_no_errors() + + @marks.testrail_id(741972) + def test_profile_allow_new_contact_requests_toggle(self): + self.device_1.just_fyi("Device 1 creates anew profile") + self.device_1.reopen_app(sign_in=False) + self.device_1.create_user(first_user=False) + public_key = self.home_1.get_public_key() + self.home_1.just_fyi("Device 1 disables new contact requests toggle in profile") + self.home_1.profile_button.click() + self.profile_1.turn_new_contact_requests_toggle('off') + self.profile_1.click_system_back_button() + + self.home_2.just_fyi("Device 2 adds newly created user as a contact") + self.home_2.navigate_to_chats_view() + self.home_2.add_contact(public_key=public_key, close_profile=False) + + self.home_1.just_fyi("Device 1 checks that no contact request is received") + try: + self.home_1.notifications_unread_badge.wait_for_visibility_of_element(30) + self.errors.append(self.home_1, "Contact request received with turned off 'Allow contact requests' toggle") + except TimeoutException: + pass + + self.home_1.just_fyi("Device 1 enables new contact requests toggle in profile") + self.home_1.profile_button.click() + self.profile_1.turn_new_contact_requests_toggle('on') + self.profile_1.click_system_back_button() + + self.home_2.just_fyi("Device 2 resends a contact request") + self.chat_2.block_contact() + self.chat_2.profile_unblock_button.click() + self.chat_2.profile_send_contact_request_button.click() + self.chat_2.contact_request_message_input.send_keys("hi") + self.chat_2.confirm_send_contact_request_button.click() + + self.home_1.just_fyi("Device 1 checks that contact request is received") + try: + self.home_1.notifications_unread_badge.wait_for_visibility_of_element(60) + except TimeoutException: + self.errors.append(self.home_1, + "Contact request was not received with turned on 'Allow contact requests' toggle") + + self.errors.verify_no_errors() diff --git a/test/appium/tests/test_api.py b/test/appium/tests/test_api.py deleted file mode 100644 index aee9df9b9cb..00000000000 --- a/test/appium/tests/test_api.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import pytest -import re -import time - -from os import path - -from support.api.third_parties_api import get_token_info -from tests import marks - - -def get_parameters(): - directory = os.sep.join(__file__.split(os.sep)[:-4]) - file_path = path.join(directory, 'src/status_im/utils/ethereum/tokens.cljs') - with open(file_path, 'r') as f: - data = f.read() - return re.findall('{:symbol\s*:(.*)\n\s*:name\s*"(.*)"\n\s*:address\s*"(.*)"\n\s*:decimals\s*(.*)}', data) - - -class TestAPi(object): - - @marks.api - @pytest.mark.parametrize('symbol,name,address,decimals', get_parameters()) - def test_tokens_verification(self, symbol, name, address, decimals): - res = get_token_info(address) - errors = list() - if str(res['decimals']) != decimals: - errors.append("decimals value %s doesn't match expected %s" % (decimals, res['decimals'])) - if res['symbol'] != symbol: - errors.append("symbol '%s' doesn't match expected '%s'" % (symbol, res['symbol'])) - if res['name'] != name: - errors.append("token name '%s' doesn't match expected '%s'" % (name, res['name'])) - if errors: - pytest.fail('For address %s %s' % (address, ', '.join(errors))) - - @staticmethod - def teardown(): - time.sleep(3) diff --git a/test/appium/tests/test_message_reliability.py b/test/appium/tests/test_message_reliability.py deleted file mode 100644 index 8d7123b5612..00000000000 --- a/test/appium/tests/test_message_reliability.py +++ /dev/null @@ -1,271 +0,0 @@ -import random -import string -import time - -from itertools import cycle -from timeit import timeit - -from appium.webdriver.common.mobileby import MobileBy -from selenium.common.exceptions import TimeoutException -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.wait import WebDriverWait - -from tests import info, marks -from tests.base_test_case import MessageReliabilityTestCase -from views.base_element import BaseButton -from views.sign_in_view import SignInView - - -def wrapper(func, *args, **kwargs): - def wrapped(): - return func(*args, **kwargs) - - return wrapped - - -@marks.message_reliability -class TestMessageReliability(MessageReliabilityTestCase): - - def test_message_reliability_1_1_chat(self, messages_number, message_wait_time): - user_a_sent_messages = 0 - user_a_received_messages = 0 - user_b_sent_messages = 0 - user_b_received_messages = 0 - user_a_message_time = dict() - user_b_message_time = dict() - try: - self.create_drivers(2, max_duration=10800, custom_implicitly_wait=2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - device_1_home, device_2_home = device_1.create_user(username='user_a'), device_2.create_user( - username='user_b') - device_2_public_key = device_2_home.get_public_key() - device_2_home.home_button.click() - device_1_chat = device_1_home.add_contact(device_2_public_key) - device_1_chat.chat_message_input.send_keys('hello') - device_1_chat.send_message_button.click() - device_2_home.element_by_text('hello').click() - device_2_chat = device_2_home.get_chat_view() - device_2_chat.add_to_contacts.click() - - start_time = time.time() - for i in range(int(messages_number / 2)): - message_1 = ''.join(random.sample(string.ascii_lowercase, k=10)) - device_1_chat.chat_message_input.send_keys(message_1) - device_1_chat.send_message_button.click() - user_a_sent_messages += 1 - try: - user_b_receive_time = timeit(wrapper(device_2_chat.wait_for_element_starts_with_text, - message_1, message_wait_time), - number=1) - duration_time = round(time.time() - start_time, ndigits=2) - user_b_message_time[duration_time] = user_b_receive_time - user_b_received_messages += 1 - except TimeoutException: - info("Message with text '%s' was not received by user_b" % message_1) - message_2 = ''.join(random.sample(string.ascii_lowercase, k=10)) - device_2_chat.chat_message_input.send_keys(message_2) - device_2_chat.send_message_button.click() - user_b_sent_messages += 1 - try: - user_a_receive_time = timeit(wrapper(device_1_chat.wait_for_element_starts_with_text, - message_2, message_wait_time), - number=1) - duration_time = round(time.time() - start_time, ndigits=2) - user_a_message_time[duration_time] = user_a_receive_time - user_a_received_messages += 1 - except TimeoutException: - info("Message with text '%s' was not received by user_a" % message_2) - finally: - self.one_to_one_chat_data['user_a'] = {'sent_messages': user_a_sent_messages, - 'message_time': user_a_message_time} - self.one_to_one_chat_data['user_b'] = {'sent_messages': user_b_sent_messages, - 'message_time': user_b_message_time} - - def test_message_reliability_1_1_chat_with_predefined_user(self, messages_number, user_public_key): - self.create_drivers(1, max_duration=10800, custom_implicitly_wait=2) - sign_in_view = SignInView(self.drivers[0]) - home_view = sign_in_view.create_user(username='user_a') - home_view.add_contact(user_public_key) - chat_view = home_view.get_chat_view() - for i in range(messages_number): - message_text = '%s %s' % (i + 1, ''.join(random.sample(string.ascii_lowercase, k=10))) - chat_view.chat_message_input.send_keys(message_text) - chat_view.send_message_button.click() - - def test_message_reliability_public_chat(self, messages_number, message_wait_time, participants_number, chat_name): - self.public_chat_data['sent_messages'] = int() - self.public_chat_data['message_time'] = dict() - - self.create_drivers(participants_number, max_duration=10800, custom_implicitly_wait=2) - users = ['user_%s' % i for i in range(participants_number)] - chat_views = list() - chat_name = chat_name if chat_name else ''.join(random.choice(string.ascii_lowercase) for _ in range(7)) - for i in range(participants_number): - device = SignInView(self.drivers[i]) - home_view = device.create_user(username=users[i]) - home_view.join_public_chat(chat_name) - chat_views.append(home_view.get_chat_view()) - - start_time = time.time() - repeat = cycle(range(participants_number)) - next_user = next(repeat) - while self.public_chat_data['sent_messages'] <= messages_number: - this_user, next_user = next_user, next(repeat) - message_text = '%s %s' % (self.public_chat_data['sent_messages'] + 1, - ''.join(random.sample(string.ascii_lowercase, k=10))) - chat_views[this_user].chat_message_input.send_keys(message_text) - chat_views[this_user].send_message_button.click() - self.public_chat_data['sent_messages'] += 1 - try: - receive_time = timeit(wrapper(chat_views[next_user].wait_for_element_starts_with_text, - message_text, message_wait_time), - number=1) - duration_time = round(time.time() - start_time, ndigits=2) - self.public_chat_data['message_time'][duration_time] = receive_time - except TimeoutException: - pass - if self.public_chat_data['sent_messages'] == messages_number: - break - - def test_message_reliability_offline_public_chat(self, messages_number, message_wait_time, chat_name): - self.public_chat_data['sent_messages'] = int() - self.public_chat_data['message_time'] = dict() - - self.create_drivers(1, max_duration=10800, custom_implicitly_wait=2, offline_mode=True) - driver = self.drivers[0] - sign_in_view = SignInView(driver) - home_view = sign_in_view.create_user() - chat_name = chat_name if chat_name else home_view.get_public_chat_name() - home_view.join_public_chat(chat_name) - - start_time = time.time() - iterations = int(messages_number / 10 if messages_number > 10 else messages_number) - for _ in range(iterations): - home_view.get_back_to_home_view() - driver.set_network_connection(1) # airplane mode - - sent_messages_texts = self.network_api.start_chat_bot(chat_name=chat_name, messages_number=10) - self.public_chat_data['sent_messages'] += 10 - - driver.set_network_connection(2) # turning on WiFi connection - - home_view.get_chat_with_user('#' + chat_name).click() - chat_view = home_view.get_chat_view() - for message in sent_messages_texts: - try: - receive_time = timeit(wrapper(chat_view.wait_for_element_starts_with_text, - message, message_wait_time), - number=1) - duration_time = round(time.time() - start_time, ndigits=2) - self.public_chat_data['message_time'][duration_time] = receive_time - except TimeoutException: - pass - - def test_message_reliability_offline_1_1_chat(self, messages_number, message_wait_time): - user_a_sent_messages = 0 - user_a_received_messages = 0 - user_b_sent_messages = 0 - user_b_received_messages = 0 - user_a_message_time = dict() - user_b_message_time = dict() - try: - self.create_drivers(2, max_duration=10800, custom_implicitly_wait=2, offline_mode=True) - device_1, device_2 = self.drivers[0], self.drivers[1] - sign_in_1, sign_in_2 = SignInView(device_1), SignInView(device_2) - sign_in_1.create_user(username='user_a') - sign_in_2.create_user(username='user_b') - device_1_home, device_2_home = sign_in_1.get_home_view(), sign_in_2.get_home_view() - device_2_public_key = device_2_home.get_public_key() - device_2_home.home_button.click() - device_1_home.add_contact(device_2_public_key) - device_1_chat = device_1_home.get_chat_view() - device_1_chat.chat_message_input.send_keys('hello') - device_1_chat.send_message_button.click() - device_2_home.element_by_text('hello').click() - device_2_chat = device_2_home.get_chat_view() - device_2_chat.add_to_contacts.click() - - iterations = int((messages_number / 10 if messages_number > 10 else messages_number) / 2) - start_time = time.time() - for i in range(iterations): - device_2.set_network_connection(1) # airplane mode - - messages_1 = list() - for _ in range(10): - message_1 = '%s %s' % (user_a_sent_messages + 1, ''.join(random.sample(string.ascii_lowercase, - k=10))) - device_1_chat.chat_message_input.send_keys(message_1) - device_1_chat.send_message_button.click() - messages_1.append(messages_1) - user_a_sent_messages += 1 - - device_2.set_network_connection(2) # turning on WiFi connection - - for message in messages_1: - try: - user_b_receive_time = timeit(wrapper(device_2_chat.wait_for_element_starts_with_text, - message, message_wait_time), - number=1) - duration_time = round(time.time() - start_time, ndigits=2) - user_b_message_time[duration_time] = user_b_receive_time - user_b_received_messages += 1 - except TimeoutException: - info("Message with text '%s' was not received by user_b" % message) - - messages_2 = list() - for _ in range(10): - message_2 = '%s %s' % (user_b_sent_messages + 1, ''.join(random.sample(string.ascii_lowercase, - k=10))) - device_2_chat.chat_message_input.send_keys(message_2) - device_2_chat.send_message_button.click() - messages_2.append(message_2) - user_b_sent_messages += 1 - for message in messages_2: - try: - user_a_receive_time = timeit(wrapper(device_1_chat.wait_for_element_starts_with_text, - message, message_wait_time), - number=1) - duration_time = round(time.time() - start_time, ndigits=2) - user_a_message_time[duration_time] = user_a_receive_time - user_a_received_messages += 1 - except TimeoutException: - info("Message with text '%s' was not received by user_a" % message) - finally: - self.one_to_one_chat_data['user_a'] = {'sent_messages': user_a_sent_messages, - 'message_time': user_a_message_time} - self.one_to_one_chat_data['user_b'] = {'sent_messages': user_b_sent_messages, - 'message_time': user_b_message_time} - - def test_message_reliability_push_notifications(self, message_wait_time): - self.create_drivers(2, max_duration=10800, custom_implicitly_wait=2) - device_1, device_2 = self.drivers[0], self.drivers[1] - sign_in_1, sign_in_2 = SignInView(device_1), SignInView(device_2) - sign_in_1.create_user(username='user_a') - sign_in_2.create_user(username='user_b') - device_1_home, device_2_home = sign_in_1.get_home_view(), sign_in_2.get_home_view() - device_2_public_key = device_2_home.get_public_key() - device_2_home.home_button.click() - - device_2.close_app() - - device_1_home.add_contact(device_2_public_key) - device_1_chat = device_1_home.get_chat_view() - device_1_chat.chat_message_input.send_keys('hello') - device_1_chat.send_message_button.click() - - device_2.open_notifications() - try: - WebDriverWait(device_2, message_wait_time) \ - .until( - expected_conditions.presence_of_element_located((MobileBy.XPATH, '//*[contains(@text, "Status")]'))) - element = BaseButton(device_2) - element.locator = element.Locator.xpath_selector("//*[contains(@text,'Status')]") - element.click() - except TimeoutException as exception: - exception.msg = "Push notification is not received during '%s' seconds" % message_wait_time - raise exception - - sign_in_2.sign_in() - device_2_home.element_by_text('hello').click() - device_2_chat = device_2_home.get_chat_view() - device_2_chat.wait_for_element_starts_with_text('hello') diff --git a/test/appium/tests/wallet/test_collectibles.py b/test/appium/tests/wallet/test_collectibles.py new file mode 100644 index 00000000000..0d632e3b62c --- /dev/null +++ b/test/appium/tests/wallet/test_collectibles.py @@ -0,0 +1,206 @@ +import re +import time + +import pytest +from selenium.common import TimeoutException + +from base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from tests import marks +from users import transaction_senders +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="six_2") +@marks.nightly +@marks.smoke +class TestWalletCollectibles(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in_view = SignInView(self.drivers[0]) + self.sender, self.receiver = transaction_senders['ETH_1'], transaction_senders['ETH_2'] + self.sender['wallet_address'] = '0x' + self.sender['address'] + self.receiver['wallet_address'] = '0x' + self.receiver['address'] + self.sign_in_view.recover_access(passphrase=self.sender['passphrase']) + + self.home_view = self.sign_in_view.get_home_view() + self.sender_username = self.home_view.get_username() + self.profile_view = self.home_view.profile_button.click() + self.profile_view.switch_network() + self.home_view.navigate_back_to_home_view() + self.home_view.wallet_tab.click() + self.wallet_view = self.home_view.get_wallet_view() + self.account_name = 'Account 1' + self.sender_short_address = self.sender['wallet_address'].replace(self.sender['wallet_address'][6:-3], + '…').lower() + self.receiver_short_address = self.receiver['wallet_address'].replace(self.receiver['wallet_address'][6:-3], + '…').lower() + self.network_name = 'Base' + + @marks.testrail_id(741839) + def test_wallet_collectibles_balance(self): + self.wallet_view.collectibles_tab.click() + for _ in range(4): + self.wallet_view.pull_to_refresh() + if not self.wallet_view.element_by_text('No collectibles').is_element_displayed(): + return + time.sleep(5) + + self.wallet_view.set_network_in_wallet(self.network_name) + collectibles = { + "BVL": {"quantity": 2, + "info": {"Account": "Account 1", + "Network": "Base", + "category": "Football Player", + "rank": "Star", + "type": "Modric"}}, + "Glitch Punks": {"quantity": 1, + "info": {"Account": "Account 1", + "Network": "Base", + "Race": "Skull Blue", + "Mouth": "Lipstick Green", + "Eyes": "Femme Shade Eyes Variant 3", + "Face": "Pipe", + "Ear Accessory": "Silver Stud Cross Combo", + "Nose": "Bot Nose 3", + "Eye Accessory": "Nouns", + "Head": "Double Spike"}} + } + for collectible_name, data in collectibles.items(): + self.wallet_view.just_fyi("Check %s collectible info and image" % collectible_name) + try: + element = self.wallet_view.get_collectible_element(collectible_name) + element.wait_for_element() + except TimeoutException: + self.errors.append(self.wallet_view, "Collectible '%s' is not displayed" % collectible_name) + continue + if element.image_element.is_element_differs_from_template( + '%s_collectible_image_template.png' % collectible_name): + self.errors.append(self.wallet_view, "%s image doesn't match expected template" % collectible_name) + if element.quantity != data['quantity']: + self.errors.append(self.wallet_view, "%s quantity %s doesn't match expected %s" % ( + collectible_name, element.quantity, data['quantity'])) + self.wallet_view.just_fyi("Check %s collectible expanded info" % collectible_name) + element.click() + if self.wallet_view.expanded_collectible_image.is_element_differs_from_template( + '%s_expanded_collectible_image_template.png' % collectible_name): + self.errors.append(self.wallet_view, + "%s expanded image doesn't match expected template" % collectible_name) + self.wallet_view.driver.swipe(500, 2000, 500, 300) + for item, expected_text in data['info'].items(): + try: + text = self.wallet_view.get_data_item_element_text(item) + if text != expected_text: + self.errors.append(self.wallet_view, "%s: shown %s text '%s' doesn't match expected '%s'" % ( + collectible_name, item, text, expected_text)) + except TimeoutException: + self.errors.append(self.wallet_view, "%s: %s data item is not shown" % (collectible_name, item)) + self.wallet_view.click_system_back_button() + self.errors.verify_no_errors() + + @marks.testrail_id(741840) + def test_wallet_send_collectible(self): + self.wallet_view.get_account_element().click() + self.wallet_view.send_button.click() + self.wallet_view.address_text_input.send_keys(self.receiver['wallet_address']) + self.wallet_view.continue_button.click() + self.wallet_view.collectibles_tab_on_select_token_view.click() + self.wallet_view.get_collectible_element('BVL').wait_and_click(20) + self.wallet_view.button_one.click() + data_to_find_on_review = { + self.account_name: ("From", self.wallet_view.from_data_container), + self.sender_short_address: ("From", self.wallet_view.from_data_container), + self.network_name: ("On", self.wallet_view.on_data_container), + self.receiver_short_address: ("To", self.wallet_view.to_data_container) + } + + for text, (label, container) in data_to_find_on_review.items(): + if not container.get_child_element_by_text(text).is_element_displayed(): + self.errors.append( + self.wallet_view, + "Text %s is not shown inside '%s' container on the Review Send page" % (text, label) + ) + data_to_check = { + 'Est. time': r'~\d+ sec', + 'Max fees': r" 60: + self.errors.append( + self.wallet_view, + "Unexpected value '%s' is shown for est. time on the Review Send page" % text) + else: + if text != expected_value: + self.errors.append( + self.wallet_view, + "%s text %s doesn't match expected %s on the Review Send page" % ( + key, text, expected_value)) + except TimeoutException: + self.errors.append(self.wallet_view, "%s is not shown on the Review Send page" % key) + + self.wallet_view.slide_button_track.slide() + if not self.wallet_view.password_input.is_element_displayed(): + self.errors.append(self.wallet_view, "Can't confirm transaction") + + self.wallet_view.click_system_back_button(times=6) + self.errors.verify_no_errors() + + @marks.testrail_id(741841) + def test_wallet_collectible_send_from_expanded_info_view(self): + self.wallet_view.collectibles_tab.click() + self.wallet_view.get_collectible_element('Glitch Punks').scroll_and_click() + self.wallet_view.send_from_collectible_info_button.click() + self.wallet_view.address_text_input.send_keys(self.receiver['wallet_address']) + self.wallet_view.continue_button.click() + data_to_find_on_review = { + self.account_name: ("From", self.wallet_view.from_data_container), + self.sender_short_address: ("From", self.wallet_view.from_data_container), + self.network_name: ("On", self.wallet_view.on_data_container), + self.receiver_short_address: ("To", self.wallet_view.to_data_container) + } + + for text, (label, container) in data_to_find_on_review.items(): + if not container.get_child_element_by_text(text).is_element_displayed(): + self.errors.append( + self.wallet_view, + "Text %s is not shown inside '%s' container on the Review Send page" % (text, label) + ) + data_to_check = { + 'Est. time': r'~\d+ sec', + 'Max fees': r" 60: + self.errors.append( + self.wallet_view, + "Unexpected value '%s' is shown for est. time on the Review Send page" % text) + else: + if text != expected_value: + self.errors.append( + self.wallet_view, + "'%s' text '%s' doesn't match expected '%s' on the Review Send page" % ( + key, text, expected_value)) + except TimeoutException: + self.errors.append(self.wallet_view, "%s is not shown on the Review Send page" % key) + + self.wallet_view.slide_button_track.slide() + if not self.wallet_view.password_input.is_element_displayed(): + self.errors.append(self.wallet_view, "Can't confirm transaction") + + self.errors.verify_no_errors() diff --git a/test/appium/tests/wallet/test_wallet_connect.py b/test/appium/tests/wallet/test_wallet_connect.py new file mode 100644 index 00000000000..9bc973dd417 --- /dev/null +++ b/test/appium/tests/wallet/test_wallet_connect.py @@ -0,0 +1,348 @@ +import re + +import pytest +from selenium.common import TimeoutException + +import marks +from base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from users import transaction_senders +from views.sign_in_view import SignInView +from views.web_views.external_browser_view import BridgeStatusNetworkView, UniswapView + +status_dapp_url = 'https://bridge.status.network' +status_dapp_name = 'Status Network Bridge' + + +@marks.nightly +@pytest.mark.xdist_group(name="two_1") +class TestWalletConnectBaseChecks(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in_view = SignInView(self.drivers[0]) + self.sign_in_view.create_user() + self.home_view = self.sign_in_view.get_home_view() + self.username = self.home_view.get_username() + self.wallet_view = self.sign_in_view.wallet_tab.click() + self.browser_view = BridgeStatusNetworkView(self.drivers[0]) + self.account_1_name = 'Account 1' + + @marks.testrail_id(742897) + def test_wallet_connect_disconnect(self): + self.wallet_view.just_fyi( + "Open %s in an external browser and try connecting to Status" % status_dapp_url) + self.browser_view.connect_status_wallet() + for text in status_dapp_name, status_dapp_url: + if not self.wallet_view.element_by_text(text).is_element_displayed(): + self.errors.append(self.wallet_view, + "Text '%s' is not displayed when connecting to Status wallet" % text) + expected_data = {'Account': 'Account 1', 'Networks': 'Mainnet, Status'} + for key, value in expected_data.items(): + try: + data = self.wallet_view.get_data_item_element_text(data_item_name=key) + if data != value: + self.errors.append( + self.wallet_view, + "%s value '%s' doesn't match expected '%s' when connecting to Status" % (key, data, value)) + except TimeoutException: + self.errors.append(self.wallet_view, "%s data is not shown when connecting to Status" % key) + self.wallet_view.wallet_connect_button.click() + self.wallet_view.just_fyi("Check that %s is added to connected dApps") + self.wallet_view.get_account_element().click() + self.wallet_view.connected_dapps_button.click() + dapp_element = self.wallet_view.get_connected_dapp_element_by_name(dapp_name=status_dapp_name) + if dapp_element.is_element_displayed(): + if dapp_element.url_text.text != status_dapp_url: + self.errors.append(self.wallet_view, + "DApp url %s is not shown for the connected dApp" % status_dapp_url) + else: + self.errors.append(self.wallet_view, "%s is not shown in connected dApps" % status_dapp_name) + + self.wallet_view.just_fyi("Check that dApp is connected in the browser") + status_app_package = self.drivers[0].current_package + self.browser_view.open_browser() + if self.browser_view.connect_wallet_button.is_element_displayed(): + self.errors.append(self.wallet_view, "DApp is not connected in the browser") + + self.wallet_view.just_fyi("Check that dApp can be disconnected") + self.drivers[0].activate_app(status_app_package) + if dapp_element.is_element_displayed(): + dapp_element.disconnect() + if not self.wallet_view.element_by_translation_id('no-dapps').is_element_displayed(): + self.errors.append(self.wallet_view, "DApp was not disconnected") + self.errors.verify_no_errors() + + @marks.testrail_id(742898) + def test_wallet_connect_decline_and_select_account(self): + self.wallet_view.navigate_to_wallet_view() + self.wallet_view.just_fyi("Add new wallet account") + new_account_name = "New Account" + self.wallet_view.add_regular_account(account_name=new_account_name) + + self.wallet_view.just_fyi("Decline connection to Status dApp") + self.browser_view.open_browser() + if self.browser_view.connect_wallet_button.is_element_displayed(): + refresh = False + else: + refresh = True + self.browser_view.connect_status_wallet(refresh=refresh) + self.wallet_view.wallet_decline_button.click() + self.browser_view.open_browser() + self.browser_view.element_by_text('Connection declined').wait_for_element() + self.browser_view.element_by_text('Try again').click() + + self.wallet_view.just_fyi("Connect Status dApp with selecting newly created account") + self.wallet_view.select_account_to_connect_dapp(account_name=new_account_name) + self.wallet_view.wallet_connect_button.click() + self.wallet_view.navigate_to_wallet_view() + self.wallet_view.get_account_element(account_name=new_account_name).click() + self.wallet_view.connected_dapps_button.click() + dapp_element = self.wallet_view.get_connected_dapp_element_by_name(dapp_name=status_dapp_name) + if dapp_element.is_element_displayed(): + if dapp_element.url_text.text != status_dapp_url: + self.errors.append(self.wallet_view, + "DApp url %s is not shown for the connected dApp" % status_dapp_url) + else: + self.errors.append(self.wallet_view, "%s is not shown in connected dApps" % status_dapp_name) + self.errors.verify_no_errors() + + +@marks.nightly +@pytest.mark.xdist_group(name="two_1") +class TestWalletConnectDifferentNetworks(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in_view = SignInView(self.drivers[0]) + self.sign_in_view.create_user() + self.home_view = self.sign_in_view.get_home_view() + self.username = self.home_view.get_username() + self.wallet_view = self.sign_in_view.wallet_tab.click() + self.browser_view = BridgeStatusNetworkView(self.drivers[0]) + self.account_1_name = 'Account 1' + self.profile_view = self.home_view.get_profile_view() + self.status_app_package = self.drivers[0].current_package + + @marks.testrail_id(742899) + def test_wallet_connect_testnet_dapp(self): + self.home_view.navigate_back_to_home_view() + if self.home_view.testnet_mode_enabled: + self.home_view.just_fyi("Switch to mainnet") + self.home_view.profile_button.click() + self.profile_view.switch_network() + self.home_view.navigate_back_to_home_view() + self.home_view.just_fyi("Try connecting testnet dApp when being on mainnet") + self.browser_view.connect_status_wallet() + self.browser_view.open_browser() + self.browser_view.element_by_text('Connection declined').wait_for_element() + self.browser_view.connect_wallet_button.click() + + self.drivers[0].activate_app(self.status_app_package) + self.home_view.just_fyi("Switch to testnet") + self.home_view.profile_button.click() + self.profile_view.switch_network() + self.home_view.navigate_back_to_home_view() + + self.wallet_view.just_fyi("Connect to dApp on testnet") + self.browser_view.connect_status_wallet(refresh=False) + self.wallet_view.wallet_connect_button.click() + + self.wallet_view.just_fyi("Switch to mainnet and check that testnet dApp is not shown") + self.wallet_view.profile_button.click() + self.profile_view.switch_network() + self.wallet_view.navigate_to_wallet_view() + self.wallet_view.get_account_element(account_name=self.account_1_name).click() + self.wallet_view.connected_dapps_button.click() + if not self.wallet_view.element_by_translation_id('no-dapps').is_element_displayed(): + pytest.fail("%s dApp is shown on mainnet" % status_dapp_name) + + @marks.testrail_id(742900) + def test_wallet_connect_mainnet_dapp(self): + self.home_view.navigate_back_to_home_view() + if self.home_view.testnet_mode_enabled: + self.home_view.just_fyi("Switch to mainnet") + self.home_view.profile_button.click() + self.profile_view.switch_network() + self.home_view.navigate_back_to_home_view() + self.wallet_view.just_fyi("Connect dApp on mainnet") + UniswapView(self.drivers[0]).connect_status_wallet() + self.wallet_view.wallet_connect_button.click() + self.wallet_view.get_account_element(account_name=self.account_1_name).click() + self.wallet_view.connected_dapps_button.click_until_presence_of_element(self.wallet_view.add_dapp_button) + dapp_name = 'Uniswap' + dapp_url = 'https://app.uniswap.org' + dapp_element = self.wallet_view.get_connected_dapp_element_by_name(dapp_name=dapp_name) + if dapp_element.is_element_displayed(): + if dapp_element.url_text.text != dapp_url: + self.errors.append(self.wallet_view, + "DApp url %s is not shown for the connected dApp on mainnet" % dapp_url) + else: + self.errors.append(self.wallet_view, "%s is not shown in connected dApps on mainnet" % dapp_name) + + self.wallet_view.just_fyi("Switch to testnet and check that mainnet dApp is not shown") + self.wallet_view.navigate_back_to_home_view() + profile_view = self.wallet_view.profile_button.click() + profile_view.switch_network() + self.wallet_view.navigate_to_wallet_view() + self.wallet_view.get_account_element(account_name=self.account_1_name).click() + self.wallet_view.connected_dapps_button.click() + if dapp_element.is_element_displayed(): + self.errors.append(self.wallet_view, "%s dApp is shown on testnet" % dapp_name) + self.errors.verify_no_errors() + + +@marks.nightly +@pytest.mark.xdist_group(name="two_1") +class TestWalletConnectSignTransactions(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in_view = SignInView(self.drivers[0]) + self.user = transaction_senders['ETH_2'] + self.user['wallet_address'] = '0x' + self.user['address'] + self.sign_in_view.recover_access(passphrase=self.user['passphrase']) + self.profile_1_username = self.sign_in_view.get_home_view().get_username() + self.wallet_view = self.sign_in_view.wallet_tab.click() + self.app_package = self.wallet_view.driver.current_package + self.browser_view = BridgeStatusNetworkView(self.drivers[0]) + self.wallet_view.just_fyi("Connect %s dApp" % status_dapp_url) + self.browser_view.connect_status_wallet() + self.wallet_view.wallet_connect_button.click() + + @marks.testrail_id(742901) + def test_wallet_connect_sign_transaction(self): + self.wallet_view.just_fyi("Make bridge transaction from the connected dApp") + self.browser_view.open_browser() + self.browser_view.amount_input.send_keys('0.000001') + self.browser_view.bridge_button.scroll_and_click() + data_to_check = { + 'Network': 'Sepolia', + 'Max fees': r"\d+ sec' + } + for key, expected_value in data_to_check.items(): + try: + text = self.wallet_view.get_data_item_element_text(data_item_name=key) + if key == 'Max fees': + if not re.findall(expected_value, text): + self.errors.append(self.wallet_view, + "Max fee is not a number - %s on the Review Transaction page" % text) + elif key == 'Est. time': + if not re.findall(expected_value, text) or int(re.findall(r'\d+', text)[0]) > 60: + self.errors.append( + self.wallet_view, "Unexpected Est. time value - %s on the Review Transaction page" % text) + else: + if text != expected_value: + self.errors.append( + self.wallet_view, + "%s text %s doesn't match expected %s on the Review Transaction page" % ( + key, text, expected_value)) + except TimeoutException: + self.errors.append(self.wallet_view, "%s is not shown on the Review Transaction page" % key) + self.wallet_view.slide_and_confirm_with_password() + self.browser_view.open_browser() + if self.browser_view.element_by_text('Transaction confirmed!').is_element_displayed(60): + self.browser_view.element_by_text('START A NEW TRANSACTION').click() + else: + self.errors.append(self.wallet_view, "Transaction was not confirmed") + self.errors.verify_no_errors() + + @marks.testrail_id(742943) + def test_wallet_connect_reject_sign_in_request(self): + self.wallet_view.just_fyi("Make signing request and reject it") + self.browser_view.open_browser() + self.browser_view.amount_input.send_keys('0.000001') + self.browser_view.bridge_button.scroll_and_click() + self.wallet_view.close_sign_message_button.wait_and_click(5) + error_message = '%s sign request rejected' % status_dapp_name + try: + self.wallet_view.element_by_text(error_message).wait_for_element() + except TimeoutException: + self.errors.append(self.wallet_view, "Error message '%s' is not shown when rejecting sign request") + + self.wallet_view.just_fyi('Check that new sign in request can be confirmed') + self.browser_view.open_browser() + self.browser_view.amount_input.send_keys('0.000001') + self.browser_view.bridge_button.scroll_and_click() + self.wallet_view.slide_and_confirm_with_password() + self.browser_view.open_browser() + if self.browser_view.element_by_text('Transaction confirmed!').is_element_displayed(60): + self.browser_view.element_by_text('START A NEW TRANSACTION').click() + else: + self.errors.append(self.wallet_view, "Transaction was not confirmed after rejecting previous request") + + self.errors.verify_no_errors() + + @marks.testrail_id(742942) + def test_wallet_connect_multiple_profiles(self): + self.wallet_view.just_fyi("Reopen app and create a new profile") + self.wallet_view.driver.activate_app(self.app_package) + self.wallet_view.reopen_app(sign_in=False) + self.sign_in_view.create_user(first_user=False) + self.wallet_view.wallet_tab.click() + self.wallet_view.just_fyi("Check that dApp connected to another profile is not displayed") + self.wallet_view.get_account_element().click() + self.wallet_view.connected_dapps_button.click() + dapp_element = self.wallet_view.get_connected_dapp_element_by_name(dapp_name=status_dapp_name) + if dapp_element.is_element_displayed(): + self.errors.append(self.wallet_view, "DApp, which is connected to another profile, is shown") + self.browser_view.open_browser() + self.browser_view.amount_input.send_keys('0.000001') + self.browser_view.bridge_button.scroll_and_click() + error_message = 'Bridge.status.network requires an unsupported network' + try: + self.wallet_view.element_starts_with_text(error_message).wait_for_element() + except TimeoutException: + self.errors.append( + self.wallet_view, + "Error message '%s' doesn't appear when signing bridge transaction with a wrong profile" % error_message) + if self.wallet_view.slide_button_track.is_element_displayed(): + self.errors.append(self.wallet_view, "Can sign transaction with wrong profile") + self.errors.verify_no_errors() + + @marks.testrail_id(742944) + @marks.xfail( + reason="Sign modal doesn't appear when logged out, https://github.com/status-im/status-mobile/issues/22586") + def test_wallet_connect_sign_request_when_logged_out(self): + self.wallet_view.just_fyi("Close app to log out") + app_package = self.wallet_view.driver.current_package + self.wallet_view.terminate_app(app_package) + self.wallet_view.just_fyi("Make signing request while logged out") + self.browser_view.open_browser() + self.browser_view.amount_input.send_keys('0.000001') + self.browser_view.bridge_button.scroll_and_click() + self.wallet_view.wait_for_application_to_be_running(app_package) + self.sign_in_view.sign_in(user_name=self.profile_1_username) + if not self.wallet_view.slide_button_track.is_element_displayed(): + pytest.fail( + "Transaction request modal is not shown when making a sign request with fully closed Status app") + + +@marks.nightly +@pytest.mark.xdist_group(name="two_1") +class TestWalletConnectLoggedOut(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in_view = SignInView(self.drivers[0]) + self.sign_in_view.create_user() + self.home_view = self.sign_in_view.get_home_view() + self.username = self.home_view.get_username() + self.wallet_view = self.sign_in_view.wallet_tab.click() + self.browser_view = BridgeStatusNetworkView(self.drivers[0]) + self.account_1_name = 'Account 1' + + @marks.testrail_id(742945) + def test_wallet_connect_logged_out(self): + self.home_view.just_fyi("Close app to log out") + self.home_view.reopen_app(sign_in=False) + + self.home_view.just_fyi("Try connecting Status dApp when logged out") + self.browser_view.connect_status_wallet() + self.sign_in_view.sign_in(user_name=self.username) + self.wallet_view.wallet_connect_button.wait_and_click() + + self.wallet_view.just_fyi("Check that connection is successful in browser") + self.browser_view.open_browser() + if self.browser_view.connect_wallet_button.is_element_displayed(): + pytest.fail("DApp is not connected in the browser") diff --git a/test/appium/tests/wallet/test_wallet_mainnet_no_send.py b/test/appium/tests/wallet/test_wallet_mainnet_no_send.py new file mode 100644 index 00000000000..912e18168f9 --- /dev/null +++ b/test/appium/tests/wallet/test_wallet_mainnet_no_send.py @@ -0,0 +1,450 @@ +import re + +import pytest +from _pytest.outcomes import Failed +from selenium.common import NoSuchElementException, TimeoutException + +from base_test_case import MultipleSharedDeviceTestCase, create_shared_drivers +from tests import marks +from users import transaction_senders +from views.sign_in_view import SignInView + + +@pytest.mark.xdist_group(name="three_1") +@marks.nightly +@marks.smoke +class TestWalletOneDevice(MultipleSharedDeviceTestCase): + + def prepare_devices(self): + self.drivers, self.loop = create_shared_drivers(1) + self.sign_in_view = SignInView(self.drivers[0]) + self.sender, self.receiver = transaction_senders['ETH_1'], transaction_senders['ETH_2'] + self.total_balance = {'Ether': 0.0362, 'Status': 15.0, 'Uniswap': 0.7, 'Dai Stablecoin': 0.0} + self.mainnet_balance = {'Ether': 0.015, 'Status': 10.0, 'Uniswap': 0.2, + 'Dai Stablecoin': 0.0} + self.optimism_balance = {'Ether': 0.0011, 'Status': 5.0, 'Uniswap': 0, 'Dai Stablecoin': 0.0} + self.arb_balance = {'Ether': 0.0051, 'Status': 0.0, 'Uniswap': 0.5, 'Dai Stablecoin': 0.0} + self.base_balance = {'Ether': 0.015, 'Status': 0.0, 'Uniswap': 0.0, 'Dai Stablecoin': 0.0} + self.sender['wallet_address'] = '0x' + self.sender['address'] + self.receiver['wallet_address'] = '0x' + self.receiver['address'] + self.sign_in_view.recover_access(passphrase=self.sender['passphrase']) + + self.home_view = self.sign_in_view.get_home_view() + self.sender_username = self.home_view.get_username() + self.profile_view = self.home_view.profile_button.click() + self.profile_view.switch_network() + self.home_view.navigate_back_to_home_view() + self.wallet_view = self.home_view.wallet_tab.click() + self.account_name = 'Account 1' + + @marks.testrail_id(740490) # TODO: add USDc check back when token collision USDc and USDCN is resolved + def test_wallet_balance_mainnet(self): + self.wallet_view.just_fyi("Checking total balance") + real_balance = {} + for asset in self.total_balance: + real_balance[asset] = self.wallet_view.get_asset(asset).get_amount() + + for asset in self.total_balance: + if real_balance[asset] != self.total_balance[asset]: + self.errors.append(self.wallet_view, "For the %s the wrong value %s is shown, expected %s in total" % + (asset, real_balance[asset], self.total_balance[asset])) + expected_balances = { + 'Ethereum': self.mainnet_balance, + 'Arbitrum': self.arb_balance, + 'Optimism': self.optimism_balance, + 'Base': self.base_balance + } + + for network in expected_balances: + self.wallet_view.just_fyi("Checking total balance on %s network" % network) + self.wallet_view.set_network_in_wallet(network) + real_balance = {} + for asset in expected_balances[network]: + real_balance[asset] = self.wallet_view.get_asset(asset).get_amount() + for asset in expected_balances[network]: + if real_balance[asset] != expected_balances[network][asset]: + self.errors.append(self.wallet_view, "For the %s the wrong value %s is shown, expected %s on %s" % + (asset, real_balance[asset], expected_balances[network][asset], network)) + self.wallet_view.network_drop_down.click() + + self.errors.verify_no_errors() + + @marks.testrail_id(741554) + def test_wallet_send_flow_mainnet(self): + self.wallet_view.get_account_element().click() + self.wallet_view.send_button.click() + self.wallet_view.address_text_input.send_keys(self.receiver['wallet_address']) + self.wallet_view.continue_button.click() + + asset_data = { + 'Ether': {'networks': ['Ethereum', 'Arbitrum'], 'amount': '0.00001'}, + 'Status': {'networks': ['Ethereum', 'Optimism'], 'amount': '1'} + } + for asset, data in asset_data.items(): + for network in data['networks']: + self.wallet_view.just_fyi("Checking the send flow for %s on %s" % (asset, network)) + self.wallet_view.select_asset(asset) + self.wallet_view.select_network(network) + self.wallet_view.set_amount(data['amount']) + try: + max_fees_text = self.wallet_view.get_data_item_element_text(data_item_name='Max fees') + if not re.findall(r" 60: + self.errors.append( + self.wallet_view, + "%s on %s: unexpected Est. time value - %s on the Review Send page" % ( + asset, network, text)) + else: + if text != expected_value: + self.errors.append( + self.wallet_view, + "%s on %s: %s text %s doesn't match expected %s on the Review Send page" % ( + asset, network, key, text, expected_value)) + except TimeoutException: + self.errors.append(self.wallet_view, + "%s on %s: %s is not shown on the Review Send page" % (asset, network, key)) + + self.wallet_view.slide_button_track.slide() + if not self.wallet_view.password_input.is_element_displayed(): + self.errors.append(self.wallet_view, "%s on %s: can't confirm transaction" % (asset, network)) + self.wallet_view.click_system_back_button_until_presence_of_element( + element=self.wallet_view.element_by_text('Select token'), attempts=4) + self.errors.verify_no_errors() + + @marks.testrail_id(741555) + def test_wallet_swap_flow_mainnet(self): + self.sign_in_view.reopen_app(user_name=self.sender_username) + self.home_view.wallet_tab.click() + self.wallet_view.get_account_element().wait_for_rendering_ended_and_click() + self.wallet_view.swap_button.wait_for_rendering_ended_and_click() + for network in ['Ethereum', 'Optimism']: + self.wallet_view.just_fyi("Checking the Swap flow for SNT on %s" % network) + self.wallet_view.select_asset('Status') + self.wallet_view.select_network(network) + self.wallet_view.set_amount('1') + data_to_check = { + 'Max fees': r" diff: + result = True + return result + + def is_element_image_similar_to_template(self, template_path: str = ''): + image_template = os.sep.join(__file__.split(os.sep)[:-1]) + '/elements_templates/%s' % template_path + template = imagehash.average_hash(Image.open(image_template)) + element_image = imagehash.average_hash(self.image) + return not bool(template - element_image) + + def get_element_rgb(self, colors: int = 2): + palette = self.image.quantize(colors=colors).getpalette() + percentage = webcolors.rgb_to_rgb_percent((palette[:3])) + return (float(percentage.red.strip('%')), + float(percentage.green.strip('%')), + float(percentage.blue.strip('%'))) + + def get_element_coordinates(self): element = self.find_element() - location, size = element.location, element.size + location = element.location + size = element.size + return location, size + + def swipe_left_on_element(self): + self.driver.info("Swiping left on element %s" % self.name) + location, size = self.get_element_coordinates() x, y = location['x'], location['y'] width, height = size['width'], size['height'] self.driver.swipe(start_x=x + width * 0.75, start_y=y + height / 2, end_x=x, end_y=y + height / 2) - def long_press_element(self): + def swipe_right_on_element(self, width_percentage=0.9, start_x=0): + self.driver.info("Swiping right on element %s" % self.name) + location, size = self.get_element_coordinates() + x, y = location['x'], location['y'] + width, height = size['width'], size['height'] + self.driver.swipe(start_x=x + start_x, start_y=y + height / 2, end_x=x + width * width_percentage, + end_y=y + height / 2) + + def swipe_to_web_element(self, depth=700): element = self.find_element() - info('Long press %s' % self.name) - action = TouchAction(self.driver) - action.long_press(element).release().perform() + location = element.location + x, y = location['x'], location['y'] + self.driver.swipe(start_x=x, start_y=y, end_x=x, end_y=depth) - def measure_time_before_element_appears(self, max_wait_time=30): - def wrapper(): - return self.wait_for_visibility_of_element(max_wait_time) + def long_press_element(self, element_to_release_on=None): + element = self.find_element() + self.driver.info("Long press on `%s`" % self.name) + action = ActionChains(self.driver) + action.click_and_hold(element).perform() + time.sleep(2) + if element_to_release_on: + action.release(element_to_release_on.find_element()) + action.perform() + else: + action.release(element) + action.perform() + + def long_press_without_release(self): + action = ActionChains(self.driver) + action.click_and_hold(self.find_element()).perform() + + def long_press_until_element_is_shown(self, expected_element): + element = self.find_element() + self.driver.info("Long press on `%s` until expected element is shown" % self.name) + action = ActionChains(self.driver) + for _ in range(3): + action.click_and_hold(element).perform() + time.sleep(2) + action.release(element).perform() + if expected_element.is_element_displayed(): + return - return timeit(wrapper, number=1) + def long_press_element_by_coordinate(self, rel_x=0.8, rel_y=0.8): + element = self.find_element() + location = element.location + size = element.size + x = int(location['x'] + size['width'] * rel_x) + y = int(location['y'] + size['height'] * rel_y) + action = ActionChains(self.driver) + action.move_to_element_with_offset(to_element=element, xoffset=x, yoffset=y).click_and_hold().perform() + action.release(element).perform() + + def click_inside_element_by_coordinate(self, rel_x=0.8, rel_y=0.8, times_to_click=1): + location, size = self.get_element_coordinates() + x = int(location['x'] + size['width'] * rel_x) + y = int(location['y'] + size['height'] * rel_y) + [self.driver.tap([(x, y)], 150) for _ in range(times_to_click)] + + @staticmethod + def get_translation_by_key(key): + return transl[key] - def measure_time_while_element_is_shown(self, max_wait_time=30): - def wrapper(): - return self.wait_for_invisibility_of_element(max_wait_time) + @staticmethod + def exclude_emoji(value): + return 'emoji' if value in emoji.UNICODE_EMOJI else value - return timeit(wrapper, number=1) + def get_child_element_by_text(self, text: str): + return BaseElement(self.driver, prefix=self.locator, xpath="//*[@text='%s']" % text) + def get_child_element_by_text_part(self, text: str): + return BaseElement(self.driver, prefix=self.locator, xpath="//*[contains(@text,'%s')]" % text) -class BaseEditBox(BaseElement): - def __init__(self, driver): - super(BaseEditBox, self).__init__(driver) +class EditBox(BaseElement): + + def __init__(self, driver, **kwargs): + super(EditBox, self).__init__(driver, **kwargs) def send_keys(self, value): self.find_element().send_keys(value) - info("Type '%s' to %s" % (value, self.name)) - - def set_value(self, value): - self.find_element().set_value(value) - info("Type '%s' to %s" % (value, self.name)) + self.driver.info("Type `%s` to `%s`" % (self.exclude_emoji(value), self.name)) def clear(self): self.find_element().clear() - info('Clear text in %s' % self.name) + self.driver.info("Clear text in `%s`" % self.name) def delete_last_symbols(self, number_of_symbols_to_delete: int): - info('Delete last %s symbols from %s' % (number_of_symbols_to_delete, self.name)) + self.driver.info("Delete last `%s` symbols from `%s`" % (number_of_symbols_to_delete, self.name)) self.click() for _ in range(number_of_symbols_to_delete): time.sleep(1) self.driver.press_keycode(67) def paste_text_from_clipboard(self): - info('Paste text from clipboard into %s' % self.name) + self.driver.info("Paste text from clipboard into `%s`" % self.name) self.long_press_element() time.sleep(2) - action = TouchAction(self.driver) - location = self.find_element().location + action = ActionChains(self.driver) + element = self.find_element() + location = element.location x, y = location['x'], location['y'] - action.press(x=x + 100, y=y - 50).release().perform() + action.move_by_offset(xoffset=x + 25, yoffset=y - 50).click().perform() + action.release(element).perform() def cut_text(self): - info('Cut text in %s' % self.name) - location = self.find_element().location + self.driver.info("Cut text in `%s`" % self.name) + element = self.find_element() + location = element.location x, y = location['x'], location['y'] - action = TouchAction(self.driver) - action.long_press(x=x, y=y).release().perform() + action = ActionChains(self.driver) + action.move_by_offset(xoffset=x, yoffset=y).click_and_hold().perform() + action.release(element).perform() time.sleep(2) - action.press(x=x + 50, y=y - 50).release().perform() - + action.move_by_offset(xoffset=x + 50, yoffset=y - 50).click().perform() + action.release(element).perform() -class BaseText(BaseElement): - def __init__(self, driver): - super(BaseText, self).__init__(driver) +class Text(BaseElement): + def __init__(self, driver, **kwargs): + super(Text, self).__init__(driver, **kwargs) @property def text(self): text = self.find_element().text - info('%s is %s' % (self.name, text)) + self.driver.info("`%s` is `%s`" % (self.name, text)) return text -class BaseButton(BaseElement): +class Button(BaseElement): - def __init__(self, driver): - super(BaseButton, self).__init__(driver) + def __init__(self, driver, **kwargs): + super(Button, self).__init__(driver, **kwargs) - def click(self): - self.find_element().click() - info('Tap on %s' % self.name) - return self.navigate() - - def click_until_presence_of_element(self, desired_element, attempts=3): + def click_until_absense_of_element(self, desired_element, attempts=3, timeout=1): counter = 0 - while not desired_element.is_element_present(1) and counter <= attempts: + self.driver.info("Click until `%s` by `%s`: `%s` is NOT presented" % ( + desired_element.name, desired_element.by, desired_element.locator)) + while desired_element.is_element_displayed(timeout) and counter <= attempts: try: - info('Tap on %s' % self.name) self.find_element().click() - info('Wait for %s' % desired_element.name) - desired_element.wait_for_element(5) - return self.navigate() - except (NoSuchElementException, TimeoutException): counter += 1 + except (NoSuchElementException, TimeoutException, StaleElementReferenceException): + return self.navigate() + + +class SilentButton(Button): + def find_element(self): + for _ in range(3): + try: + return self.driver.find_element(self.by, self.locator) + except NoSuchElementException: + raise NoSuchElementException( + "Device %s: `%s` by `%s`:`%s` not found on the screen" % ( + self.driver.number, self.name, self.by, self.locator)) from None + except Exception as exception: + if 'Internal Server Error' in str(exception): + continue + + def click(self): + self.find_element().click() + return self.navigate() + + @property + def text(self): + text = self.find_element().text + return text + + +class CheckBox(Button): + def __init__(self, driver, **kwargs): + super(Button, self).__init__(driver, **kwargs) + + def __define_desired_element(self, elem_accessibility): + desired_element_accessibility_id = elem_accessibility + if self.accessibility_id is not None and ':' in self.accessibility_id: + desired_element_accessibility_id = ':%s' % elem_accessibility + return desired_element_accessibility_id + + def enable(self): + self.click_until_presence_of_element(Button(self.driver, + accessibility_id=self.__define_desired_element("checkbox-on"))) + return self.navigate() + + def disable(self): + self.click_until_presence_of_element(Button(self.driver, + accessibility_id=self.__define_desired_element("checkbox-off"))) + return self.navigate() diff --git a/test/appium/views/base_view.py b/test/appium/views/base_view.py index bc954b10702..a5bc0888a33 100644 --- a/test/appium/views/base_view.py +++ b/test/appium/views/base_view.py @@ -1,338 +1,272 @@ import random import string import time -import base64 -import pytest -import re -import zbarlight -from tests import info, common_password -from eth_keys import datatypes + +from appium.webdriver import WebElement +from appium.webdriver.applicationstate import ApplicationState from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException -from PIL import Image -from datetime import datetime -from io import BytesIO -from views.base_element import BaseButton, BaseElement, BaseEditBox, BaseText +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.wait import WebDriverWait + +from tests import common_password, transl +from views.base_element import Button, BaseElement, EditBox, Text, CheckBox -class BackButton(BaseButton): +class BackButton(Button): def __init__(self, driver): - super(BackButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('back-button') + super().__init__(driver, accessibility_id="back-button") def click(self, times_to_click: int = 1): for _ in range(times_to_click): self.find_element().click() - info('Tap on %s' % self.name) return self.navigate() -class AllowButton(BaseButton): +class AllowButton(Button): def __init__(self, driver): - super(AllowButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Allow' or @text='ALLOW']") + super().__init__(driver, translation_id="allow", uppercase=True) - def click(self): + def click(self, times_to_click=3): try: - for _ in range(3): + for _ in range(times_to_click): self.find_element().click() - info('Tap on %s' % self.name) except NoSuchElementException: pass -class DenyButton(BaseButton): - def __init__(self, driver): - super(DenyButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='DENY']") - - -class DeleteButton(BaseButton): - def __init__(self, driver): - super(DeleteButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='DELETE']") - - -class YesButton(BaseButton): - def __init__(self, driver): - super(YesButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='YES']") - - -class NoButton(BaseButton): - def __init__(self, driver): - super(NoButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='No']") - - -class OkButton(BaseButton): - def __init__(self, driver): - super(OkButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='OK']") +class UnreadMessagesCountText(Text): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, + xpath="%s//*[@resource-id='counter-component']/android.widget.TextView" % parent_locator) -class ContinueButton(BaseButton): - def __init__(self, driver): - super(ContinueButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='CONTINUE' or @text='Continue']") - - -class TabButton(BaseButton): - +class TabButton(Button): @property def counter(self): - class Counter(BaseText): - def __init__(self, driver, parent_locator): - super(Counter, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[@content-desc='%s']//android.view.ViewGroup[2]/android.widget.TextView" % parent_locator) + return UnreadMessagesCountText(self.driver, parent_locator='//*[@content-desc="%s"]' % self.accessibility_id) - return Counter(self.driver, self.locator.value) + +class CommunitiesTab(TabButton): + def __init__(self, driver): + super().__init__(driver, accessibility_id="communities-stack-tab") -class HomeButton(TabButton): +class ChatsTab(TabButton): def __init__(self, driver): - super(HomeButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('home-tab-button') + super().__init__(driver, accessibility_id="chats-stack-tab") def navigate(self): from views.home_view import HomeView return HomeView(self.driver) - def click(self): - from views.home_view import PlusButton - self.click_until_presence_of_element(PlusButton(self.driver)) - return self.navigate() - -class WalletButton(TabButton): +class WalletTab(TabButton): def __init__(self, driver): - super(WalletButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('wallet-tab-button') + super().__init__(driver, accessibility_id="wallet-stack-tab") def navigate(self): from views.wallet_view import WalletView return WalletView(self.driver) - def click(self): - info('Tap on %s' % self.name) - from views.wallet_view import SetUpButton, SendTransactionButton - for _ in range(3): - self.find_element().click() - if SetUpButton(self.driver).is_element_displayed() or SendTransactionButton( - self.driver).is_element_displayed(): - return self.navigate() + +class BrowserTab(TabButton): + def __init__(self, driver): + super().__init__(driver, accessibility_id="browser-stack-tab") + + def navigate(self): + from views.dapps_view import DappsView + return DappsView(self.driver) class ProfileButton(TabButton): def __init__(self, driver): - super(ProfileButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('profile-tab-button') + super().__init__(driver, accessibility_id="open-profile") def navigate(self): from views.profile_view import ProfileView return ProfileView(self.driver) - def click(self): - from views.profile_view import ShareMyContactKeyButton - self.click_until_presence_of_element(ShareMyContactKeyButton(self.driver)) + def click(self, desired_element_text='privacy'): + if not self.is_element_displayed(): + ChatsTab(self.driver).click() + from views.profile_view import ProfileView + self.click_until_presence_of_element(ProfileView(self.driver).profile_password_button) return self.navigate() -class SaveButton(BaseButton): +class SendMessageButton(Button): def __init__(self, driver): - super(SaveButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//android.widget.TextView[@text='SAVE']") + super().__init__(driver, accessibility_id="send-message-button") -class NextButton(BaseButton): +class SlideButton(Button): def __init__(self, driver): - super(NextButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//android.widget.TextView[@text='NEXT']") + super().__init__(driver, xpath="//*[@resource-id='slide-button-track']") - -class DoneButton(BaseButton): - def __init__(self, driver): - super(DoneButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//android.widget.TextView[@text='DONE']") - - -class AppsButton(BaseButton): - def __init__(self, driver): - super(AppsButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("Apps") - - -class StatusAppIcon(BaseButton): - def __init__(self, driver): - super(StatusAppIcon, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[@text='Status']") - - -class SendMessageButton(BaseButton): - def __init__(self, driver): - super(SendMessageButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("send-message-button") - - def click(self): - self.find_element().click() - info('Tap on %s' % self.name) - - -class ConnectionStatusText(BaseText): - def __init__(self, driver): - super(ConnectionStatusText, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[@content-desc='connection-status-text']/android.widget.TextView") - - -class TestFairyWarning(BaseText): - def __init__(self, driver): - super(TestFairyWarning, self).__init__(driver) - self.locator = self.Locator.text_part_selector('session recording') - self.is_shown = bool() - - -class OkContinueButton(BaseButton): - - def __init__(self, driver): - super(OkContinueButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='OK, CONTINUE']") - - -class DiscardButton(BaseButton): - - def __init__(self, driver): - super(DiscardButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='DISCARD']") - - -class ConfirmButton(BaseButton): - def __init__(self, driver): - super(ConfirmButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='CONFIRM']") - - -class ProgressBar(BaseElement): - def __init__(self, driver, parent_locator: str = ''): - super(ProgressBar, self).__init__(driver) - self.locator = self.Locator.xpath_selector(parent_locator + '//android.widget.ProgressBar') - - -class WalletModalButton(BaseButton): - def __init__(self, driver): - super(WalletModalButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('wallet-modal-button') - - def navigate(self): - from views.wallet_view import WalletView - return WalletView(self.driver) - - -class CrossIcon(BaseButton): - - def __init__(self, driver): - super(CrossIcon, self).__init__(driver) - self.locator = self.Locator.xpath_selector('(//android.view.ViewGroup[@content-desc="icon"])[1]') + def slide(self): + self.swipe_right_on_element(width_percentage=1.3, start_x=100) class BaseView(object): def __init__(self, driver): self.driver = driver - self.send_message_button = SendMessageButton(self.driver) - self.home_button = HomeButton(self.driver) - self.wallet_button = WalletButton(self.driver) + self.password_input = EditBox(self.driver, accessibility_id="password-input") + from views.sign_in_view import LogInButton + self.login_button = LogInButton(self.driver) + self.continue_button = Button(self.driver, accessibility_id='continue-button') + self.view_id_tracker = Text(self.driver, xpath="//*[@content-desc='view-id-tracker']/android.widget.TextView") + self.testnet_mode_enabled_element = BaseElement(self.driver, accessibility_id='Testnet mode enabled') + + # Tabs + self.communities_tab = CommunitiesTab(self.driver) self.profile_button = ProfileButton(self.driver) - - self.yes_button = YesButton(self.driver) - self.no_button = NoButton(self.driver) + self.chats_tab = ChatsTab(self.driver) + self.wallet_tab = WalletTab(self.driver) + self.browser_tab = BrowserTab(self.driver) + + # Floating screens (introduced by https://github.com/status-im/status-mobile/pull/16438) + self.chat_floating_screen = BaseElement(self.driver, accessibility_id=":chat-floating-screen") + self.community_floating_screen = BaseElement(self.driver, + accessibility_id=":community-overview-floating-screen") + self.discover_communities_floating_screen = BaseElement(self.driver, + accessibility_id=":discover-communities-floating-screen") + + self.jump_to_button = Button(self.driver, accessibility_id="jump-to") + self.yes_button = Button(self.driver, xpath="//*[@text='YES' or @text='GOT IT']") + self.no_button = Button(self.driver, translation_id="no") self.back_button = BackButton(self.driver) self.allow_button = AllowButton(self.driver) - self.deny_button = DenyButton(self.driver) - self.continue_button = ContinueButton(self.driver) - self.ok_button = OkButton(self.driver) - self.next_button = NextButton(self.driver) - self.save_button = SaveButton(self.driver) - self.done_button = DoneButton(self.driver) - self.delete_button = DeleteButton(self.driver) - self.ok_continue_button = OkContinueButton(self.driver) - self.discard_button = DiscardButton(self.driver) - self.confirm_button = ConfirmButton(self.driver) - self.connection_status = ConnectionStatusText(self.driver) - self.cross_icon = CrossIcon(self.driver) - - self.apps_button = AppsButton(self.driver) - self.status_app_icon = StatusAppIcon(self.driver) - - self.test_fairy_warning = TestFairyWarning(self.driver) - - self.wallet_modal_button = WalletModalButton(self.driver) + self.allow_all_button = Button(self.driver, xpath="//*[@text='Allow all']") + self.allow_all_the_time = Button(self.driver, xpath="//*[@text='Allow all the time']") + self.deny_button = Button(self.driver, translation_id="deny", uppercase=True) + self.continue_button = Button(self.driver, translation_id="continue", uppercase=True) + self.ok_button = Button(self.driver, xpath="//*[@text='OK' or @text='Ok']") + self.add_button = Button(self.driver, translation_id="add") + self.save_button = Button(self.driver, translation_id="save") + self.done_button = Button(self.driver, accessibility_id="Done") + self.delete_button = Button(self.driver, translation_id="delete", uppercase=True) + self.ok_continue_button = Button(self.driver, accessibility_id="Okay, continue") + self.confirm_button = Button(self.driver, accessibility_id='Confirm') + self.options_button = Button(self.driver, accessibility_id="options-button") + + self.native_close_button = Button(self.driver, id="android:id/aerr_close") + self.close_button = Button(self.driver, accessibility_id="back-button") + self.navigate_up_button = Button(self.driver, accessibility_id="Navigate Up") + self.ok_got_it_button = Button(self.driver, accessibility_id="Okay, got it") + self.status_in_background_button = Button(self.driver, xpath="//*[contains(@content-desc,'Status')]") + self.cancel_button = Button(self.driver, translation_id="cancel", uppercase=True) + self.search_input = EditBox(self.driver, accessibility_id="search-input") + self.toast_content_element = BaseElement(self.driver, accessibility_id="toast-content") + self.next_button = Button(self.driver, accessibility_id="next-button") + self.native_alert_title = Text(self.driver, xpath="//*[@resource-id='android:id/alertTitle']") + + # share contact screen + self.show_qr_code_button = Button(self.driver, accessibility_id="show-qr-button") + + self.link_to_profile_button = Button(self.driver, accessibility_id="share-qr-code-info-text") + self.sharing_text_native = Text(self.driver, xpath="//*[@resource-id='android:id/content_preview_text']") + + # checkboxes and toggles + self.checkbox_button = CheckBox( + self.driver, xpath="//*[@content-desc='checkbox-off'][@resource-id='checkbox-component']") + self.slide_button_track = SlideButton(self.driver) + + self.button_one = Button(self.driver, accessibility_id='button-one') + + # external browser + self.open_in_android_button = Button(self.driver, translation_id="browsing-open-in-android-web-browser") self.element_types = { 'base': BaseElement, - 'button': BaseButton, - 'edit_box': BaseEditBox, - 'text': BaseText + 'button': Button, + 'edit_box': EditBox, + 'text': Text } - def accept_agreements(self): - iterations = int() - from views.sign_in_view import CreateAccountButton, PasswordInput - if self.test_fairy_warning.is_element_displayed(10): - self.test_fairy_warning.is_shown = True - while iterations <= 3 and not (CreateAccountButton(self.driver).is_element_displayed(2) or PasswordInput( - self.driver).is_element_displayed(2)): - for button in self.ok_button, self.continue_button: - try: - button.wait_for_element(3) - button.click() - except (NoSuchElementException, TimeoutException): - pass - iterations += 1 - - @property - def logcat(self): - logcat = self.driver.get_log("logcat") - if len(logcat) > 1000: - return str([i for i in logcat if 'appium' not in str(i).lower()]) - raise TimeoutError('Logcat is empty') + @staticmethod + def get_translation_by_key(translation_id): + return transl[translation_id] def confirm(self): - info("Tap 'Confirm' on native keyboard") + self.driver.info("Tap 'Confirm' on native keyboard") self.driver.press_keycode(66) - def confirm_until_presence_of_element(self, desired_element, attempts=3): - counter = 0 - while not desired_element.is_element_present(1) and counter <= attempts: - try: - self.confirm() - info('Wait for %s' % desired_element.name) - desired_element.wait_for_element(5) - return - except TimeoutException: - counter += 1 + def just_fyi(self, some_str): + self.driver.info('# STEP: %s' % some_str, device=False) + self.driver.log_event("appium", "STEP: %s" % some_str) - def click_system_back_button(self): - info('Click system back button') - self.driver.press_keycode(4) + def hide_keyboard_if_shown(self): + if self.driver.is_keyboard_shown(): + self.click_system_back_button() - def cut_text(self): - info('Cut text') - self.driver.press_keycode(277) + def click_system_back_button(self, times=1): + self.driver.info('Click system back button') + for _ in range(times): + self.driver.press_keycode(4) - def copy_text(self): - info('Copy text') - self.driver.press_keycode(278) + def click_system_back_button_until_presence_of_element(self, element, attempts=3): + counter = 0 + while not element.is_element_displayed(1) and counter <= attempts: + self.driver.press_keycode(4) + try: + element.wait_for_element(2) + return self + except (NoSuchElementException, TimeoutException): + counter += 1 + else: + self.driver.info("Could not reach %s by pressing system back button" % element.name) - def paste_text(self): - info('Paste text') - self.driver.press_keycode(279) + def click_system_back_button_until_absence_of_element(self, element, attempts=3): + for _ in range(attempts): + try: + element.wait_for_invisibility_of_element(3) + return + except TimeoutException: + self.driver.press_keycode(4) - def send_as_keyevent(self, string): + def navigate_back_to_home_view(self): + home_views_texts = ['wallet-stack', 'chats-stack', 'communities-stack'] + for _ in range(4): + try: + if self.view_id_tracker.text in home_views_texts: + return + else: + self.click_system_back_button() + except NoSuchElementException: + self.click_system_back_button() + except StaleElementReferenceException: + continue + + def navigate_to_chats_view(self): + self.navigate_back_to_home_view() + self.chats_tab.click() + + def navigate_to_communities_view(self): + self.navigate_back_to_home_view() + self.communities_tab.click() + + def navigate_to_wallet_view(self): + self.navigate_back_to_home_view() + self.wallet_tab.click() + + def navigate_back_to_chat_view(self): + self.click_system_back_button_until_presence_of_element(self.get_chat_view().chat_message_input) + + def navigate_back_to_wallet_view(self, attempts=3): + element = self.get_wallet_view().network_drop_down + self.click_system_back_button_until_presence_of_element(element) + + def click_system_home_button(self): + self.driver.info('Press system Home button') + self.driver.press_keycode(3) + + def put_app_to_background(self): + self.driver.info('App to background') + self.driver.press_keycode(187) + + def send_as_keyevent(self, keyevent): + self.driver.info("Sending as keyevent `%s`" % keyevent) keys = {'0': 7, '1': 8, '2': 9, '3': 10, '4': 11, '5': 12, '6': 13, '7': 14, '8': 15, '9': 16, ',': 55, '-': 69, '+': 81, '.': 56, '/': 76, '\\': 73, ';': 74, ' ': 62, @@ -342,64 +276,36 @@ def send_as_keyevent(self, string): 'k': 39, 'l': 40, 'm': 41, 'n': 42, 'o': 43, 'p': 44, 'q': 45, 'r': 46, 's': 47, 't': 48, 'u': 49, 'v': 50, 'w': 51, 'x': 52, 'y': 53, 'z': 54} time.sleep(3) - info("Enter '%s' using native keyboard" % string) - for i in string: - if type(keys[i]) is list: + for i in keyevent: + if i.isalpha() and i.isupper(): + keycode, metastate = keys[i.lower()], 64 # META_SHIFT_LEFT_ON Constant Value: 64. Example: i='n' -> 'N' + elif type(keys[i]) is list: keycode, metastate = keys[i][0], keys[i][1] else: keycode, metastate = keys[i], None self.driver.press_keycode(keycode=keycode, metastate=metastate) - def find_full_text(self, text, wait_time=60): - info("Looking for full text: '%s'" % text) - element = BaseElement(self.driver) - element.locator = element.Locator.text_selector(text) - return element.wait_for_element(wait_time) - - def find_text_part(self, text, wait_time=60): - info("Looking for a text part: '%s'" % text) - element = BaseElement(self.driver) - element.locator = element.Locator.text_part_selector(text) - return element.wait_for_element(wait_time) - def element_by_text(self, text, element_type='button'): - info("Looking for an element by text: '%s'" % text) element = self.element_types[element_type](self.driver) - element.locator = element.Locator.text_selector(text) + element.locator = '//*[@text="%s"]' % text return element def element_by_text_part(self, text, element_type='button'): - info("Looking for an element by text part: '%s'" % text) - element = self.element_types[element_type](self.driver) - element.locator = element.Locator.text_part_selector(text) - return element - - def element_starts_with_text(self, text, element_type='base'): - info("Looking for full text: '%s'" % text) element = self.element_types[element_type](self.driver) - element.locator = element.Locator.xpath_selector("//*[starts-with(@text,'%s')]" % text) + element.locator = '//*[contains(@text, "' + text + '")]' return element - def wait_for_element_starts_with_text(self, text, wait_time=60): - info("Looking for full text: '%s'" % text) - element = BaseElement(self.driver) - element.locator = element.Locator.xpath_selector("//*[starts-with(@text,'%s')]" % text) - return element.wait_for_element(wait_time) - - def element_by_accessibility_id(self, accessibility_id, element_type='button'): - info("Looking for an element by accessibility id: '%s'" % accessibility_id) - element = self.element_types[element_type](self.driver) - element.locator = element.Locator.accessibility_id(accessibility_id) + def element_starts_with_text(self, text, element_type='button'): + element = self.element_types[element_type](self.driver, xpath="//*[starts-with(@text,'%s')]" % text) return element - def element_by_xpath(self, xpath, element_type='button'): - info("Looking for an element by xpath: '%s'" % xpath) - element = self.element_types[element_type](self.driver) - element.locator = element.Locator.xpath_selector(xpath) + def element_by_translation_id(self, translation_id, element_type='button', uppercase=False): + element = self.element_types[element_type](self.driver, translation_id=translation_id, uppercase=uppercase) return element - def swipe_down(self): - self.driver.swipe(500, 500, 500, 1000) + def get_dapp_view(self): + from views.dapps_view import DappsView + return DappsView(self.driver) def get_home_view(self): from views.home_view import HomeView @@ -409,14 +315,14 @@ def get_chat_view(self): from views.chat_view import ChatView return ChatView(self.driver) + def get_community_view(self): + from views.chat_view import CommunityView + return CommunityView(self.driver) + def get_sign_in_view(self): from views.sign_in_view import SignInView return SignInView(self.driver) - def get_send_transaction_view(self): - from views.send_transaction_view import SendTransactionView - return SendTransactionView(self.driver) - def get_base_web_view(self): from views.web_views.base_web_view import BaseWebView return BaseWebView(self.driver) @@ -430,76 +336,109 @@ def get_wallet_view(self): return WalletView(self.driver) @staticmethod - def get_unique_amount(): - return '0.0%s' % datetime.now().strftime('%-m%-d%-H%-M%-S').strip('0') - - @staticmethod - def get_public_chat_name(): + def get_random_chat_name(): return ''.join(random.choice(string.ascii_lowercase) for _ in range(7)) - def get_text_from_qr(self): - image = Image.open(BytesIO(base64.b64decode(self.driver.get_screenshot_as_base64()))) - image.load() - try: - return str(zbarlight.scan_codes('qrcode', image)[0])[2:][:132] - except IndexError: - raise BaseException('No data in QR code') + def click_on_floating_jump_to(self): + self.hide_keyboard_if_shown() + if self.chat_floating_screen.is_element_displayed(1): + Button(self.driver, xpath='//*[@content-desc="%s"]//*[@content-desc="%s"]' % + (self.chat_floating_screen.accessibility_id, + self.jump_to_button.accessibility_id)).click() + elif self.community_floating_screen.is_element_displayed(1): + Button(self.driver, xpath='//*[@content-desc="%s"]//*[@content-desc="%s"]' % + (self.community_floating_screen.accessibility_id, + self.jump_to_button.accessibility_id)).click() + else: + self.jump_to_button.click() + + def wait_for_application_to_be_running(self, app_package: str, wait_time: int = 3): + for _ in range(wait_time): + if self.driver.query_app_state(app_package) == ApplicationState.RUNNING_IN_FOREGROUND: + return + time.sleep(1) + raise TimeoutException(msg="Status app is not running in foreground after %s sec" % wait_time) - def public_key_to_address(self, public_key): - raw_public_key = bytearray.fromhex(public_key.replace('0x04', '')) - return datatypes.PublicKey(raw_public_key).to_address()[2:] + def wait_for_application_to_not_run(self, app_package: str, wait_time: int = 3): + for _ in range(wait_time): + if self.driver.query_app_state(app_package) == ApplicationState.NOT_RUNNING: + return + time.sleep(1) + raise TimeoutException(msg="Status app is not terminated after %s sec" % wait_time) + + def terminate_app(self, app_package): + self.driver.terminate_app(app_package) + self.wait_for_application_to_not_run(app_package=app_package) + + def reopen_app(self, password=common_password, sign_in=True, user_name=None): + app_package = self.driver.current_package + self.terminate_app(app_package) + self.driver.activate_app(app_package) + self.wait_for_application_to_be_running(app_package) + if sign_in: + sign_in_view = self.get_sign_in_view() + sign_in_view.sign_in(user_name, password) + + def click_upon_push_notification_by_text(self, text): + element = self.element_by_text_part(text) + self.driver.info("Click on PN with text: '%s'" % element.exclude_emoji(text)) + element.click() + return self.get_chat_view() + + def open_notification_bar(self): + self.driver.open_notifications() + + def tap_by_coordinates(self, x, y): + self.driver.tap(positions=[(x, y)]) + + def wait_for_current_package_to_be(self, expected_package_name: str, timeout: int = 10): + start_time = time.time() + while time.time() - start_time <= timeout: + package = self.driver.current_package + if package == expected_package_name: + return + time.sleep(1) + raise TimeoutException("Driver current package is '%s' after %s seconds" % (package, timeout)) - def get_back_to_home_view(self): - counter = 0 - while not self.home_button.is_element_displayed(2): - try: - if counter >= 5: - return - self.back_button.click() - except (NoSuchElementException, TimeoutException): - counter += 1 - return self.get_home_view() - - def relogin(self, password=common_password): - self.get_back_to_home_view() - profile_view = self.profile_button.click() - profile_view.logout() - sign_in_view = self.get_sign_in_view() - sign_in_view.sign_in(password) - - def get_public_key(self): - profile_view = self.profile_button.click() - profile_view.share_my_contact_key_button.click() - profile_view.public_key_text.wait_for_visibility_of_element() - public_key = profile_view.public_key_text.text - profile_view.cross_icon.click() - return public_key - - def share_via_messenger(self): - self.element_by_text('Messenger').click() - self.element_by_text('NEW MESSAGE').click() - self.send_as_keyevent('+0') - self.confirm() - self.element_by_accessibility_id('Send Message').click() - - def reconnect(self): - connect_status = self.connection_status - for i in range(3): - if connect_status.is_element_displayed(5, ignored_exceptions=StaleElementReferenceException): - if 'Tap to reconnect' in connect_status.text: - try: - connect_status.click() - except AttributeError: - pass - try: - connect_status.wait_for_invisibility_of_element() - except TimeoutException as e: - if i == 2: - e.msg = "Can't reconnect to mail server after 3 attempts" - raise e - - def check_no_values_in_logcat(self, **kwargs): - logcat = self.logcat - for key, value in kwargs.items(): - if re.findall('\W%s$|\W%s\W' % (value, value), logcat): - pytest.fail('%s in logcat!!!' % key.capitalize(), pytrace=False) + def wait_for_staleness_of_element(self, element_instance: WebElement, seconds=10): + try: + return WebDriverWait(self.driver, seconds).until(expected_conditions.staleness_of(element_instance)) + except TimeoutException: + raise TimeoutException( + "Device %s: expected element is not stale after %s seconds" % (self.driver.number, seconds)) from None + + def open_link_from_google_search_app(self, link_text: str, app_package: str): + Button(self.driver, accessibility_id="Search").click() + EditBox(self.driver, xpath="//android.widget.EditText").send_keys(link_text) + self.driver.press_keycode(66) + text_to_click = "Status PR" if app_package.endswith(".pr") else "Status" + Button(self.driver, + xpath="//*[@resource-id='android:id/resolver_list']//*[@text='%s']" % text_to_click).click_if_shown() + Button(self.driver, xpath="//*[@resource-id='android:id/button_once']").click() + + def turn_wi_fi_off(self): + self.driver.swipe(900, 0, 900, 1000) + self.element_by_text('Internet').click() + Button(self.driver, accessibility_id='Wi‑Fi').click() + self.element_by_text('Done').click() + self.click_system_back_button_until_presence_of_element(self.chats_tab) + + def pull_to_refresh(self): + self.driver.info("Pull to refresh page to get updated data") + screen_size = self.driver.get_window_size() + width = screen_size["width"] // 2 + start_y = int(screen_size["height"] * 0.3) + end_y = int(screen_size["height"] * 0.7) + + self.driver.execute_script("mobile: swipeGesture", { + "left": width, + "top": start_y, + "width": 10, + "height": end_y - start_y, + "direction": "down", + "percent": 0.75 + }) + + @property + def testnet_mode_enabled(self) -> bool: + return self.testnet_mode_enabled_element.is_element_displayed() diff --git a/test/appium/views/chat_view.py b/test/appium/views/chat_view.py index cb8a6649295..41b6bc0857b 100644 --- a/test/appium/views/chat_view.py +++ b/test/appium/views/chat_view.py @@ -1,379 +1,627 @@ +import re import time +from datetime import datetime, timedelta +import dateutil.parser import pytest -from selenium.common.exceptions import TimeoutException, NoSuchElementException -from tests import info -from views.base_element import BaseButton, BaseEditBox, BaseText -from views.base_view import BaseView, ProgressBar -from views.profile_view import ProfilePictureElement, PublicKeyText +from appium.webdriver.common.appiumby import AppiumBy +from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException, \ + InvalidElementStateException +from selenium.webdriver import ActionChains +from tests import emojis, common_password +from views.base_element import Button, EditBox, Text, BaseElement, SilentButton +from views.base_view import BaseView +from views.home_view import HomeView -class ChatMessageInput(BaseEditBox): + +class OpenInStatusButton(Button): def __init__(self, driver): - super(ChatMessageInput, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-message-input') + super().__init__(driver, translation_id="browsing-open-in-status") + + def navigate(self): + from views.web_views.base_web_view import BaseWebView + return BaseWebView(self.driver) -class AddToContacts(BaseButton): +class ViewProfileButton(Button): def __init__(self, driver): - super(AddToContacts, self).__init__(driver) - self.locator = self.Locator.accessibility_id('add-to-contacts-button') + super().__init__(driver, translation_id="view-profile") + + def navigate(self): + return ChatView(self.driver) -class UserNameText(BaseText): +class ChatOptionsButton(Button): def __init__(self, driver): - super(UserNameText, self).__init__(driver) - self.locator = \ - self.Locator.accessibility_id('chat-name-text') + super().__init__(driver, accessibility_id="chat-menu-button") + def click(self): + self.click_until_presence_of_element(HomeView(self.driver).mark_all_messages_as_read_button) -class TransactionPopupText(BaseText): - def __init__(self, driver): - super(TransactionPopupText, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Specify amount']") + def navigate(self): + return ChatView(self.driver) -class SendCommand(BaseButton): +class ProfileSendMessageButton(Button): def __init__(self, driver): - super(SendCommand, self).__init__(driver) - self.locator = self.Locator.accessibility_id('send-button') + super().__init__(driver, accessibility_id="icon, Send message") + def navigate(self): + return ChatView(self.driver) -class RequestCommand(BaseButton): - def __init__(self, driver): - super(RequestCommand, self).__init__(driver) - self.locator = self.Locator.accessibility_id('request-button') +class ChatElementByText(Text): + def __init__(self, driver, text): + self.message_text = text + self.chat_item_locator = "android.view.ViewGroup[@content-desc='chat-item']" + if text in ["image", "sticker", "audio"]: + self.message_locator = "//android.view.ViewGroup[@content-desc='%s-message']" % text + else: + self.message_locator = "//*[starts-with(@text,'%s')]" % text + super().__init__(driver, prefix=self.message_locator, xpath="/ancestor::%s" % self.chat_item_locator) -class AssetCommand(BaseButton): - def __init__(self, driver, asset): - super(AssetCommand, self).__init__(driver) - self.locator = self.Locator.text_selector(asset) + def find_element(self): + for _ in range(2): + try: + return super(ChatElementByText, self).find_element() + except NoSuchElementException: + self.wait_for_visibility_of_element(20) + @property + def image_in_reply(self): + class ImageInReply(BaseElement): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="//android.widget.ImageView") -class ChatMenuButton(BaseButton): - def __init__(self, driver): - super(ChatMenuButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-menu-button') + try: + return ImageInReply(self.driver, self.locator) + except NoSuchElementException: + return '' + @property + def timestamp_command_message(self): + class TimeStampText(Button): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, xpath="(%s//android.widget.TextView)[last()]" % parent_locator) -class MembersButton(BaseButton): + return TimeStampText(self.driver, self.locator) - def __init__(self, driver): - super(MembersButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('(//android.view.ViewGroup[@content-desc="action"])[1]') + @property + def timestamp(self): + class TimeStampText(Button): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, xpath="%s//*[@content-desc='message-timestamp']" % parent_locator) + return TimeStampText(self.driver, self.locator).text -class DeleteChatButton(BaseButton): + @property + def member_photo(self): + class MemberPhoto(Button): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="//*[@content-desc='user-avatar']") - def __init__(self, driver): - super(DeleteChatButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//*[@text="Delete chat"]') + return MemberPhoto(self.driver, self.locator) + @property + def username(self): + class Username(Text): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, + xpath="/android.view.ViewGroup/android.widget.TextView[1]") -class ClearHistoryButton(BaseButton): + return Username(self.driver, self.locator) - def __init__(self, driver): - super(ClearHistoryButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//*[@text="Clear history"]') + @property + def message_body(self): + return Text( + self.driver, + xpath="//%s//android.widget.TextView[contains(@text,'%s')]" % (self.chat_item_locator, self.message_text) + ) + @property + def message_body_with_mention(self): + return Text(self.driver, + xpath=self.message_body.locator + "/../following-sibling::android.widget.TextView") -class LeaveChatButton(BaseButton): + def click_on_link_inside_message_body(self): + self.message_body.wait_for_visibility_of_element(30) + self.message_body.click_inside_element_by_coordinate(rel_x=0.1, rel_y=0.9) - def __init__(self, driver): - super(LeaveChatButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//*[@text="Leave public chat"]') + def wait_for_sent_state(self, wait_time=30): + return BaseElement(self.driver, prefix=self.locator, + xpath="//*[@content-desc='message-sent']").is_element_displayed(wait_time) + @property + def uncollapse(self) -> bool: + class Collapse(Button): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, + xpath="/../../..//android.widget.ImageView[@content-desc='icon']") -class ClearButton(BaseButton): + return Collapse(self.driver, self.locator).is_element_displayed() - def __init__(self, driver): - super(ClearButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//*[@text="CLEAR"]') + @property + def status(self) -> str: + Text(self.driver, xpath=self.locator).click() + status_element = Text(self.driver, prefix=self.locator, + xpath="//*[@content-desc='message-status']/android.widget.TextView") + status = '' + i = 1 + + while i < 5: + i += 1 + if Text(self.driver, prefix=self.locator, + xpath="//*[@content-desc='message-sending']").is_element_displayed(2): + status = "Sending" + break + else: + Text(self.driver, xpath=self.locator).click() + try: + if status_element.is_element_displayed(2): + status = status_element.text + break + except StaleElementReferenceException: + pass + time.sleep(2) + return status + + def wait_for_status_to_be(self, expected_status: str, timeout: int = 30): + self.driver.info("Waiting for message to be sent for %s sec" % timeout) + start_time = time.time() + current_status = 'not set' + while time.time() - start_time <= timeout: + current_status = self.status + if current_status == expected_status: + return + time.sleep(1) + raise TimeoutException("Message status was not changed to %s, it's %s" % (expected_status, current_status)) + @property + def sent_status_checkmark(self) -> object: + return Text(self.driver, prefix=self.locator, xpath="//*[@content-desc='sent']") -class LeaveButton(BaseButton): + @property + def replied_message_text(self): + class RepliedMessageText(Text): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, + # prefix=parent_locator, + # xpath="/preceding::android.widget.TextView[@content-desc='quoted-message']" + accessibility_id='quoted-message') - def __init__(self, driver): - super(LeaveButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//*[@text="LEAVE"]') + try: + # return RepliedMessageText(self.driver, self.message_locator).text + return self.find_element().find_element(by=AppiumBy.ACCESSIBILITY_ID, value='quoted-message').text + except NoSuchElementException: + return '' + @property + def replied_to_username_text(self): + class RepliedToUsernameText(Text): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, + xpath="/preceding-sibling::*[1]/android.widget.TextView[1]") -class ChatSettings(BaseButton): - def __init__(self, driver): - super(ChatSettings, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Settings']") + try: + return RepliedToUsernameText(self.driver, self.message_locator).text + except NoSuchElementException: + return '' + def emojis_below_message(self, emoji: str = 'thumbs-up'): + class EmojisNumber(Text): + def __init__(self, driver, parent_locator: str): + self.emoji = emoji + self.emojis_id = 'emoji-reaction-%s' % str(emojis[self.emoji]) + super().__init__(driver, prefix=parent_locator, + xpath="/../..//*[@content-desc='%s']/android.widget.TextView[2]" % self.emojis_id) + + @property + def text(self): + try: + text = self.find_element().text + self.driver.info("%s is '%s' for '%s'" % (self.name, text, self.emoji)) + return int(text.strip()) + except NoSuchElementException: + return 0 + + return EmojisNumber(self.driver, self.locator) -class UserOptions(BaseButton): - def __init__(self, driver): - super(UserOptions, self).__init__(driver) - self.locator = self.Locator.accessibility_id('options') + @property + def image_in_message(self): + try: + self.driver.info("Trying to access image inside message with text '%s'" % self.message_text) + ChatElementByText(self.driver, self.message_text).wait_for_sent_state(60) + return Button(self.driver, + xpath="%s//android.widget.ImageView[@content-desc='image-message']" % self.locator) + except NoSuchElementException: + self.driver.fail("No image is found in message!") + class ImageContainer(Button): + def __init__(self, driver, parent_locator): + super().__init__(driver, xpath='%s//*[@content-desc="image-container"]' % parent_locator) -class RemoveButton(BaseButton): - def __init__(self, driver): - super(RemoveButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Remove']") + def image_by_index(self, index: int): + return BaseElement(self.driver, xpath="(%s//android.widget.ImageView)[%s]" % (self.locator, index)) + @property + def image_container_in_message(self): + try: + self.driver.info( + "Trying to access images (image container) inside message with text '%s'" % self.message_text) + ChatElementByText(self.driver, self.message_text).wait_for_sent_state(60) + return self.ImageContainer(self.driver, self.locator) + except NoSuchElementException: + self.driver.fail("No image container is found in message!") -class FirstRecipient(BaseButton): - def __init__(self, driver): - super(FirstRecipient, self).__init__(driver) - self.locator = self.Locator.accessibility_id('contact-item') + @property + def pinned_by_label(self): + class PinnedByLabelText(Text): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, + xpath="/..//android.view.ViewGroup[@content-desc='pinned-by']") + return PinnedByLabelText(self.driver, self.locator) -class MoreUsersButton(BaseButton): - def __init__(self, driver): - super(MoreUsersButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[contains(@text, 'MORE')]") + @property + def message_text_content(self): + return Text( + self.driver, + xpath="//%s//*[@content-desc='message-text-content']/android.widget.TextView" % self.chat_item_locator + ) -class OpenInStatusButton(BaseButton): - def __init__(self, driver): - super(OpenInStatusButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Open in Status']") +class UsernameOptions(Button): + def __init__(self, driver, username): + super().__init__(driver, xpath="//*[@text='%s']/..//*[@content-desc='menu-option']" % username) def navigate(self): - from views.web_views.base_web_view import BaseWebView - return BaseWebView(self.driver) + return ChatView(self.driver) + def click(self): + self.scroll_to_element() + self.wait_for_element().click() + return self.navigate() -class CommandsButton(BaseButton): - def __init__(self, driver): - super(CommandsButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-commands-button') +class UsernameCheckbox(Button): + def __init__(self, driver, username, state_on): + self.username = username + super().__init__(driver, xpath="//*[@text='%s']/..//*[@content-desc='checkbox-%s']" % ( + username, 'on' if state_on else 'off')) -class ViewProfileButton(BaseButton): - def __init__(self, driver): - super(ViewProfileButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//*[@text="View profile"]') + def click(self): + try: + self.scroll_to_element(20).click() + except NoSuchElementException: + self.scroll_to_element(direction='up', depth=20).click() -class NoMessagesInChatText(BaseText): +class CommunityView(HomeView): def __init__(self, driver): - super(NoMessagesInChatText, self).__init__(driver) - self.locator = self.Locator.text_part_selector( - 'Any messages you send here are encrypted and can only be read by you and') + super().__init__(driver) + + # Community info page + self.community_options_button = Button(self.driver, accessibility_id="community-options-for-community") + self.community_membership_request_value = Text( + self.driver, translation_id="members-label", + suffix='/following-sibling::android.view.ViewGroup/android.widget.TextView') + self.members_button = Button(self.driver, translation_id="members-label") + self.leave_community_button = Button(self.driver, translation_id="leave-community") + self.share_community_button = Button(self.driver, accessibility_id="share-community") + self.invite_people_from_contacts_button = Button(self.driver, accessibility_id="invite-people-from-contacts") + + # Members + self.membership_requests_button = Button(self.driver, translation_id="membership-requests") + + # Requesting access to community / joining community + self.request_access_button = Button(self.driver, translation_id="request-access") + self.membership_request_pending_text = Text(self.driver, translation_id="membership-request-pending") + self.join_button = Button(self.driver, accessibility_id="show-request-to-join-screen-button") + self.slide_to_request_to_join_button = Button( + self.driver, xpath="(//*[@resource-id='slide-button-track']//*[@content-desc='icon'])[1]") + + # Communities initial page + self.close_community_view_button = Button(self.driver, accessibility_id="back-button") + self.community_title = Text(self.driver, accessibility_id="community-title") + self.community_logo = BaseElement( + self.driver, + xpath="//*[*[*[@content-desc='community-title']]]/following-sibling::*/android.widget.ImageView") + self.community_description_text = Text(self.driver, accessibility_id="community-description-text") + self.community_status_joined = Text(self.driver, accessibility_id="status-tag-positive") + self.community_status_pending = Text(self.driver, accessibility_id="status-tag-pending") + + def join_community(self, password=common_password, open_community=True): + self.driver.info("Joining community") + chat_element = ChatView(self.driver).chat_element_by_text("https://status.app/c/") + chat_element.wait_for_visibility_of_element(60) + chat_element.click_on_link_inside_message_body() + self.join_button.wait_and_click(120) + self.slide_to_request_to_join_button.swipe_right_on_element(width_percentage=16) + self.password_input.send_keys(password) + self.login_button.click() + if open_community: + self.community_status_joined.wait_for_visibility_of_element(60) + + def get_channel(self, channel_name: str): + self.driver.info("Getting %s channel element in community" % channel_name) + chat_element = self.get_chat(username=channel_name, community_channel=True, wait_time=30) + return chat_element + + def leave_community(self, community_name: str): + self.driver.info("Leaving %s" % community_name) + home = self.get_home_view() + home.communities_tab.click() + community_element = home.get_chat(community_name, community=True) + community_element.long_press_until_element_is_shown(self.leave_community_button) + self.leave_community_button.click() + self.leave_community_button.click() + + def get_channel_avatar(self, channel_name='general'): + return Button(self.driver, xpath='//*[@text="# %s"]/../*[@content-desc="channel-avatar"]' % channel_name) + + def copy_community_link(self): + self.driver.info("Copy community link") + self.community_options_button.click() + self.share_community_button.click() + text = self.sharing_text_native.text + self.click_system_back_button(times=2) + return text + + def handle_membership_request(self, username: str, approve=True): + self.driver.info("Handling membership request of user '%s', approve='%s'" % (username, str(approve))) + self.members_button.click() + self.membership_requests_button.click() + approve_suffix, decline_suffix = '/following-sibling::android.view.ViewGroup[1]', '/following-sibling::android.view.ViewGroup[2]' + if approve: + Button(self.driver, xpath="//*[starts-with(@text,'%s')]%s" % (username, approve_suffix)).click() + else: + Button(self.driver, xpath="//*[starts-with(@text,'%s')]%s" % (username, decline_suffix)).click() + self.close_button.click() + + def invite_to_community(self, community_name, user_names_to_invite): + if isinstance(user_names_to_invite, str): + user_names_to_invite = [user_names_to_invite] + self.driver.info("Share to %s community" % ', '.join(map(str, user_names_to_invite))) + self.navigate_back_to_home_view() + home = self.get_home_view() + home.communities_tab.click() + community_element = home.get_chat(community_name, community=True) + community_element.long_press_until_element_is_shown(self.share_community_button) + self.invite_people_from_contacts_button.click() + for user_name in user_names_to_invite: + xpath = "//*[@content-desc='user-avatar']/following-sibling::android.widget.TextView[@text='%s']" % user_name + Button(self.driver, xpath=xpath).click() + self.next_button.click() + + +class PreviewMessage(ChatElementByText): + def __init__(self, driver, text: str): + super().__init__(driver, text=text) + + @staticmethod + def return_element_or_empty(obj): + try: + return obj.scroll_to_element() + except NoSuchElementException: + return '' + @property + def preview_image(self): + class PreviewImage(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="//*[@content-desc='thumbnail']") -class ProfileSendMessageButton(BaseButton): - def __init__(self, driver): - super(ProfileSendMessageButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('start-conversation-button') + return PreviewMessage.return_element_or_empty(PreviewImage(self.driver, self.locator)) + @property + def preview_title(self): + class PreviewTitle(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="//*[@content-desc='title']") -class ProfileSendTransactionButton(BaseButton): - def __init__(self, driver): - super(ProfileSendTransactionButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('send-transaction-button') + return PreviewMessage.return_element_or_empty(PreviewTitle(self.driver, self.locator)) + @property + def preview_subtitle(self): + class PreviewSubTitle(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="//*[@content-desc='description']") -class ChatElementByText(BaseText): - def __init__(self, driver, text): - super(ChatElementByText, self).__init__(driver) - self.message_text = text - self.locator = self.Locator.xpath_selector( - "//*[starts-with(@text,'%s')]/ancestor::android.view.ViewGroup[@content-desc='chat-item']" % text) - - def find_element(self): - info("Looking for message with text '%s'" % self.message_text) - for _ in range(2): - try: - return super(ChatElementByText, self).find_element() - except NoSuchElementException: - ChatView(self.driver).reconnect() + return PreviewMessage.return_element_or_empty(PreviewSubTitle(self.driver, self.locator)) @property - def status(self): - class StatusText(BaseText): + def preview_link(self): + class PreviewLink(SilentButton): def __init__(self, driver, parent_locator: str): - super(StatusText, self).__init__(driver) - text = "//android.widget.TextView[@text='Seen' or @text='Sent' or " \ - "@text='Not sent. Tap for options' or @text='Network mismatch']" - self.locator = self.Locator.xpath_selector(parent_locator + text) + super().__init__(driver, prefix=parent_locator, xpath="//*[@content-desc='link']") - return StatusText(self.driver, self.locator.value) + return PreviewMessage.return_element_or_empty(PreviewLink(self.driver, self.locator)) - @property - def progress_bar(self): - return ProgressBar(self.driver, self.locator.value) + +class CommunityLinkPreviewMessage(ChatElementByText): + def __init__(self, driver, text: str): + super().__init__(driver, text=text) + self.locator += "//*[@text='%s']" % self.get_translation_by_key('community') @property - def member_photo(self): - class MemberPhoto(BaseButton): - def __init__(self, driver, parent_locator): - super(MemberPhoto, self).__init__(driver) - self.locator = self.Locator.xpath_selector(parent_locator + "//*[@content-desc='member-photo']") + def community_name(self) -> str: + class CommunityName(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="/following-sibling::android.widget.TextView[1]") - return MemberPhoto(self.driver, self.locator.value) + return CommunityName(self.driver, self.locator).text @property - def username(self): - class Username(BaseText): - def __init__(self, driver, parent_locator): - super(Username, self).__init__(driver) - self.locator = self.Locator.xpath_selector(parent_locator + "/*[1][name()='android.widget.TextView']") + def community_description(self) -> str: + class CommunityDescription(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="/following-sibling::android.widget.TextView[2]") - return Username(self.driver, self.locator.value) + return CommunityDescription(self.driver, self.locator).text @property - def send_request_button(self): - class SendRequestButton(BaseButton): - def __init__(self, driver, parent_locator): - super(SendRequestButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector(parent_locator + '//*[@text="Send"]') + def community_members_amount(self) -> int: + class CommunityMembers(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="/following-sibling::android.widget.TextView[3]") - return SendRequestButton(self.driver, self.locator.value) + members_string = CommunityMembers(self.driver, self.locator).text - def contains_text(self, text, wait_time=5) -> bool: - element = BaseText(self.driver) - element.locator = element.Locator.xpath_selector( - self.locator.value + "//android.view.ViewGroup//android.widget.TextView[contains(@text,'%s')]" % text) - return element.is_element_displayed(wait_time) + return int(re.search(r'\d+', members_string).group()) + def view(self) -> object: + class CommunityViewButton(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, + xpath="/..//*[@text='%s']" % self.get_translation_by_key("view")) -class ChatView(BaseView): + CommunityViewButton(self.driver, self.locator).click() + CommunityView(self.driver).request_access_button.wait_for_element(20) + return CommunityView(self.driver) + + +class PinnedMessagesList(BaseElement): def __init__(self, driver): - super(ChatView, self).__init__(driver) + super().__init__(driver, xpath="//*[@content-desc='pinned-messages-menu']") - self.chat_message_input = ChatMessageInput(self.driver) - self.add_to_contacts = AddToContacts(self.driver) - self.user_name_text = UserNameText(self.driver) - self.no_messages_in_chat = NoMessagesInChatText(self.driver) - - self.commands_button = CommandsButton(self.driver) - self.send_command = SendCommand(self.driver) - self.request_command = RequestCommand(self.driver) - - self.chat_options = ChatMenuButton(self.driver) - self.members_button = MembersButton(self.driver) - self.delete_chat_button = DeleteChatButton(self.driver) - self.clear_history_button = ClearHistoryButton(self.driver) - self.clear_button = ClearButton(self.driver) - self.leave_chat_button = LeaveChatButton(self.driver) - self.leave_button = LeaveButton(self.driver) - - self.chat_settings = ChatSettings(self.driver) - self.view_profile_button = ViewProfileButton(self.driver) - self.more_users_button = MoreUsersButton(self.driver) - self.user_options = UserOptions(self.driver) - self.remove_button = RemoveButton(self.driver) + def get_pinned_messages_number(self): + self.driver.info("Getting number of pinned messages inside pinned messages list element") + element = BaseElement(self.driver, prefix=self.locator, xpath="//*[@content-desc='message-sent']") + return len(element.find_elements()) - self.first_recipient_button = FirstRecipient(self.driver) + def message_element_by_text(self, text): + message_element = Button(self.driver, prefix=self.locator, xpath="//*[starts-with(@text,'%s')]" % text) + self.driver.info("Looking for a pinned message by text: %s" % message_element.exclude_emoji(text)) + return message_element - self.open_in_status_button = OpenInStatusButton(self.driver) + def get_message_pinned_by_text(self, text): + xpath = "//*[starts-with(@text,'%s')]/../../*[@content-desc='pinned-by']/android.widget.TextView" % text + pinned_by_element = Text(self.driver, prefix=self.locator, xpath=xpath) + self.driver.info("Looking for a pinned by message with text: %s" % text) + return pinned_by_element - # Contact's profile - self.contact_profile_picture = ProfilePictureElement(self.driver) - self.profile_send_message = ProfileSendMessageButton(self.driver) - self.profile_send_transaction = ProfileSendTransactionButton(self.driver) - self.public_key_text = PublicKeyText(self.driver) - - def wait_for_syncing_complete(self): - info('Waiting for syncing complete:') - while True: - try: - sync = self.find_text_part('Syncing', 10) - info(sync.text) - except TimeoutException: - break - def wait_for_message_in_one_to_one_chat(self, expected_message: str, errors: list, wait_time: int = 20): - try: - element = ChatElementByText(self.driver, expected_message) - element.wait_for_element(wait_time) - except TimeoutException: - errors.append('Message with text "%s" was not received' % expected_message) - - def wait_for_messages(self, username: str, expected_messages: list, errors: list, wait_time: int = 30): - expected_messages = expected_messages if type(expected_messages) == list else [expected_messages] - repeat = 0 - received_messages = list() - while repeat <= wait_time: - for message in expected_messages: - if self.element_starts_with_text(message, 'text').is_element_present(1): - received_messages.append(message) - if not set(expected_messages) - set(received_messages): - break - time.sleep(3) - repeat += 3 - if set(expected_messages) - set(received_messages): - errors.append('Not received messages from user %s: "%s"' % (username, ', '.join( - [i for i in list(set(expected_messages) - set(received_messages))]))) - - def send_funds_to_request(self, amount, sender_password, wallet_set_up=False): - gas_popup = self.element_by_text_part('Specify amount') - send_request_button = self.chat_element_by_text(amount).send_request_button - send_request_button.click_until_presence_of_element(gas_popup) - send_transaction = self.get_send_transaction_view() - if wallet_set_up: - wallet_view = self.get_wallet_view() - self.send_message_button.click_until_presence_of_element(wallet_view.sign_in_phrase) - wallet_view.done_button.click() - wallet_view.yes_button.click() - else: - self.send_message_button.click_until_presence_of_element(send_transaction.sign_transaction_button) - send_transaction.sign_transaction(sender_password) - - def delete_chat(self): - self.chat_options.click() - self.delete_chat_button.click() - self.delete_button.click() - - def clear_history(self): - self.chat_options.click() - self.clear_history_button.click() - self.clear_button.click() - - def send_transaction_in_1_1_chat(self, asset, amount, password, wallet_set_up=False): - self.commands_button.click() - self.send_command.click() - self.asset_by_name(asset).click() - self.send_as_keyevent(amount) - send_transaction_view = self.get_send_transaction_view() - if wallet_set_up: - wallet_view = self.get_wallet_view() - self.send_message_button.click_until_presence_of_element(wallet_view.sign_in_phrase) - wallet_view.done_button.click() - wallet_view.yes_button.click() - else: - self.send_message_button.click_until_presence_of_element(send_transaction_view.sign_transaction_button) - send_transaction_view.sign_transaction(password) - chat_elem = self.chat_element_by_text(amount) - chat_elem.wait_for_visibility_of_element() - chat_elem.progress_bar.wait_for_invisibility_of_element(20) - if chat_elem.status.text not in ('Sent', 'Delivered', 'Seen'): - pytest.fail('Sent transaction message was not sent') - - def send_transaction_in_group_chat(self, amount, password, recipient): - self.commands_button.click() - self.send_command.click() - self.find_full_text(recipient['username']).click() - self.send_as_keyevent(amount) - self.send_message_button.click() +class ChatMessageInput(EditBox): + def __init__(self, driver): + super().__init__(driver, accessibility_id="chat-message-input") + + def paste_text_from_clipboard(self): + action = ActionChains(self.driver) + element = self.find_element() + location = element.location + x, y = location['x'], location['y'] + action.move_by_offset(xoffset=x + 250, yoffset=y).click_and_hold().perform() # long press + action.release(element).perform() + action.move_by_offset(xoffset=x + 50, yoffset=y - 50).click().perform() # tap Paste + action.release(element).perform() + + def click_inside(self): + action = ActionChains(self.driver) + element = self.find_element() + location = element.location + x, y = location['x'] + 250, location['y'] + action.move_to_element_with_offset(to_element=element, xoffset=x, yoffset=y).click().perform() + action.release(element).perform() - send_transaction_view = self.get_send_transaction_view() - self.send_message_button.click_until_presence_of_element(send_transaction_view.sign_transaction_button) - send_transaction_view.sign_transaction(password) - send_transaction_view.find_full_text(amount) - self.find_full_text('to ' + recipient['username'], 10) - - def request_transaction_in_1_1_chat(self, asset, amount): - self.commands_button.click() - self.request_command.click() - self.asset_by_name(asset).click() - self.send_as_keyevent(amount) - self.send_message_button.click() + +class ChatView(BaseView): + def __init__(self, driver): + super().__init__(driver) + + # Start new chat + self.public_key_edit_box = EditBox(self.driver, accessibility_id="enter-contact-code-input") + self.view_profile_new_contact_button = Button(self.driver, accessibility_id="new-contact-button") + + ## Options + self.chat_options = ChatOptionsButton(self.driver) + self.delete_chat_button = Button(self.driver, translation_id="close-chat") + self.clear_history_button = Button(self.driver, translation_id="clear-history") + self.reply_message_button = Button(self.driver, translation_id="message-reply") + self.share_chat_button = Button(self.driver, accessibility_id="share-chat-button") + self.clear_button = Button(self.driver, translation_id="clear-history") + self.view_profile_button = ViewProfileButton(self.driver) + self.view_profile_by_avatar_button = Button(self.driver, accessibility_id="member-photo") + self.user_options = Button(self.driver, accessibility_id="options") + self.open_in_status_button = OpenInStatusButton(self.driver) + + # Chat input + self.chat_message_input = ChatMessageInput(self.driver) + self.cancel_reply_button = Button(self.driver, accessibility_id="reply-cancel-button") + self.url_preview_composer = Button(self.driver, accessibility_id="url-preview") + self.url_preview_composer_text = Text(self.driver, + xpath='//*[@content-desc="url-preview"]//*[@content-desc="title"]') + self.quote_username_in_message_input = EditBox( + self.driver, + xpath="//*[@content-desc='reply-cancel-button']/preceding::android.widget.TextView[3]") + + # General chat view + self.history_start_icon = Button(self.driver, accessibility_id="history-chat") + self.contact_request_button = Button(self.driver, accessibility_id="contact-request--button") + + # Communities + self.mentions_list = BaseElement(self.driver, accessibility_id="mentions-list") + + # New UI + self.pinned_messages_count = Button(self.driver, + xpath="//*[@content-desc='pins-count']//android.widget.TextView") + self.pinned_messages_list = PinnedMessagesList(self.driver) + self.pin_limit_popover = BaseElement(self.driver, translation_id="pin-limit-reached") + self.view_pinned_messages_button = Button(self.driver, accessibility_id="pinned-banner") + + # Images + self.show_images_button = Button(self.driver, accessibility_id="open-images-button") + self.take_photo_button = Button(self.driver, accessibility_id="camera-button") + self.snap_button = Button(self.driver, accessibility_id="snap") + self.images_confirm_selection_button = Button(self.driver, accessibility_id="confirm-selection") + self.share_image_icon_button = Button(self.driver, accessibility_id="share-image") + self.view_image_options_button = Button(self.driver, accessibility_id="image-options") + self.save_image_icon_button = Button(self.driver, accessibility_id="save-image") + + # Group chats + self.leave_chat_button = Button(self.driver, accessibility_id="leave-chat-button") + self.leave_button = Button(self.driver, translation_id="leave", uppercase=True) + self.remove_user_button = Button(self.driver, accessibility_id="remove-from-chat") + self.make_admin_button = Button(self.driver, accessibility_id="make-admin") + self.edit_group_chat_name_button = Button(self.driver, accessibility_id="edit-button") + self.edit_group_chat_name_edit_box = EditBox(self.driver, accessibility_id="new-chat-name") + self.done_button = Button(self.driver, accessibility_id="done") + self.create_group_chat_button = Button(self.driver, accessibility_id="Create group chat") + + # Contact's profile + self.contact_bio_text = Text( + self.driver, xpath="//*[@content-desc='username-status-icon']/following-sibling::*/android.widget.TextView") + self.profile_send_message_button = ProfileSendMessageButton(self.driver) + self.profile_options_button = Button(self.driver, accessibility_id="contact-actions") + self.profile_block_contact_button = Button(self.driver, accessibility_id="block-user") + self.confirm_block_contact_button = Button(self.driver, accessibility_id="block-contact") + self.profile_unblock_button = Button(self.driver, accessibility_id="icon, Unblock") + self.profile_send_contact_request_button = Button(self.driver, accessibility_id="icon, Send contact request") + self.contact_request_message_input = EditBox(self.driver, accessibility_id="contact-request-message") + self.confirm_send_contact_request_button = EditBox(self.driver, accessibility_id="send-contact-request") + self.profile_add_to_contacts_button = Button(self.driver, accessibility_id="Add to contacts-item-button") + self.profile_remove_from_contacts = Button(self.driver, accessibility_id="Remove from contacts-item-button") + self.profile_nickname_button = Button(self.driver, accessibility_id="profile-nickname-item") + self.nickname_input_field = EditBox(self.driver, accessibility_id="nickname-input") + self.remove_from_contacts = Button(self.driver, accessibility_id="Remove from contacts-item-button") + + def get_preview_message_by_text(self, text=None) -> object: + self.driver.info('Getting preview message for link: %s' % text) + return PreviewMessage(self.driver, text) + + def get_username_checkbox(self, username: str, state_on=False): + self.driver.info("Getting %s checkbox" % username) + return UsernameCheckbox(self.driver, username, state_on) def chat_element_by_text(self, text): - info("Looking for a message by text: '%s'" % text) - return ChatElementByText(self.driver, text) + chat_element = ChatElementByText(self.driver, text) + self.driver.info("Looking for a message by text: %s" % chat_element.exclude_emoji(text)) + return chat_element - def verify_message_is_under_today_text(self, text, errors): + def verify_message_is_under_today_text(self, text, errors, timeout=10): + self.driver.info("Verifying that '%s' is under today" % text) message_element = self.chat_element_by_text(text) - message_element.wait_for_visibility_of_element() + message_element.wait_for_visibility_of_element(timeout) message_location = message_element.find_element().location['y'] today_text_element = self.element_by_text('Today').find_element() today_location = today_text_element.location['y'] @@ -381,7 +629,147 @@ def verify_message_is_under_today_text(self, text, errors): if message_location < today_location + today_height: errors.append("Message '%s' is not under 'Today' text" % text) - def asset_by_name(self, asset_name): - element = BaseButton(self.driver) - element.locator = element.Locator.text_selector(asset_name) - return element + def send_message(self, message: str = 'test message', wait_chat_input_sec=5): + self.driver.info("Sending message '%s'" % BaseElement(self.driver).exclude_emoji(message)) + self.chat_message_input.wait_for_element(wait_chat_input_sec) + for _ in range(3): + try: + self.chat_message_input.send_keys(message) + break + except (StaleElementReferenceException, InvalidElementStateException): + time.sleep(1) + except Exception as e: + raise e + else: + raise StaleElementReferenceException(msg="Can't send keys to chat message input, loading") + try: + self.send_message_button.click() + except NoSuchElementException: + self.chat_message_input.clear() + self.chat_message_input.send_keys(message) + self.send_message_button.click() + + def pin_message(self, message, action="pin"): + self.driver.info("Looking for message '%s' pin" % message) + element = self.element_by_translation_id(action) + self.chat_element_by_text(message).long_press_until_element_is_shown(element) + if not element.is_element_displayed(): + self.driver.fail( + "Device %s: Action '%s' is not displayed for the message '%s'" % (self.driver.number, action, message)) + element.click_until_absense_of_element(element) + + def edit_message_in_chat(self, message_to_edit, message_to_update): + self.driver.info("Looking for message '%s' to edit it" % message_to_edit) + self.chat_element_by_text(message_to_edit).message_body.long_press_without_release() + self.element_by_translation_id("edit-message").double_click() + self.chat_message_input.clear() + self.chat_message_input.send_keys(message_to_update) + self.send_message_button.click() + + def delete_message_in_chat(self, message, everyone=True): + self.driver.info("Looking for message '%s' to delete it" % message) + if everyone: + delete_button = self.element_by_translation_id("delete-for-everyone") + else: + delete_button = self.element_by_translation_id("delete-for-me") + self.chat_element_by_text(message).message_body.long_press_without_release() + delete_button.double_click() + + def copy_message_text(self, message_text): + self.driver.info("Copying '%s' message via long press" % message_text) + self.chat_element_by_text(message_text).wait_for_visibility_of_element() + self.chat_element_by_text(message_text).long_press_without_release() + self.element_by_translation_id("copy-text").double_click() + + def quote_message(self, message: str): + self.driver.info("Quoting '%s' message" % message) + element = self.chat_element_by_text(message) + element.wait_for_sent_state() + element.long_press_without_release() + self.reply_message_button.double_click() + + def set_reaction(self, message: str, emoji: str = 'thumbs-up', emoji_message: bool = False, + times_to_long_press: int = 1): + self.driver.info("Setting '%s' reaction" % emoji) + element = Button(self.driver, accessibility_id='reaction-%s' % emoji) + if emoji_message: + self.element_by_text_part(message).long_press_without_release() + else: + for _ in range(times_to_long_press): + self.chat_element_by_text(message).long_press_without_release() + element.wait_for_element() + element.double_click() + element.wait_for_invisibility_of_element() + + def add_remove_same_reaction(self, emoji: str = 'thumbs-up'): + self.driver.info("Adding one more '%s' reaction or removing an added one" % emoji) + key = emojis[emoji] + element = Button(self.driver, accessibility_id='emoji-reaction-%s' % key) + element.wait_and_click() + + def block_contact(self): + self.driver.info("Block contact from other user profile") + self.profile_options_button.click() + self.profile_block_contact_button.click() + self.confirm_block_contact_button.click() + + def set_nickname(self, nickname, close_profile=True): + self.driver.info("Setting nickname:%s" % nickname) + self.profile_nickname_button.click() + self.nickname_input_field.send_keys(nickname) + self.element_by_text('Done').click() + if close_profile: + self.close_button.click() + + def convert_device_time_to_chat_timestamp(self) -> list: + sent_time_object = dateutil.parser.parse(self.driver.device_time) + timestamp = datetime.strptime("%s:%s" % (sent_time_object.hour, sent_time_object.minute), '%H:%M').strftime( + "%I:%M %p") + timestamp_obj = datetime.strptime(timestamp, '%I:%M %p') + possible_timestamps_obj = [timestamp_obj + timedelta(0, 0, 0, 0, 1), timestamp_obj, + timestamp_obj - timedelta(0, 0, 0, 0, 1), timestamp_obj - timedelta(0, 0, 0, 0, 2)] + timestamps = list(map(lambda x: x.strftime("%I:%M %p"), possible_timestamps_obj)) + final_timestamps = [t[1:] if t[0] == '0' else t for t in timestamps] + return final_timestamps + + def user_list_element_by_name(self, user_name: str): + return BaseElement(self.driver, xpath="//*[@content-desc='user-list']//*[@text='%s']" % user_name) + + def mention_user(self, user_name: str): + self.driver.info("Mention user %s in the chat" % user_name) + self.chat_message_input.send_keys("@") + try: + self.mentions_list.wait_for_element() + self.user_list_element_by_name(user_name).wait_for_rendering_ended_and_click() + except TimeoutException: + self.driver.fail("Mentions list is not shown") + + def get_image_by_index(self, index=0): + return Button(self.driver, accessibility_id="image-%s" % index) + + def send_images_with_description(self, description, indexes=None): + if indexes is None: + indexes = [0] + self.show_images_button.click() + self.allow_button.click_if_shown() + self.allow_all_button.click_if_shown() + try: + [self.get_image_by_index(i).click() for i in indexes] + except NoSuchElementException: + self.click_system_back_button() + pytest.fail("Can't send image(s) with index(es) %s" % indexes) + self.images_confirm_selection_button.click() + self.chat_message_input.send_keys(description) + self.send_message_button.click() + + def send_image_with_camera(self, description=None): + self.take_photo_button.click() + self.allow_button.click_if_shown() + self.snap_button.click() + self.element_by_translation_id("use-photo").click() + if description: + self.chat_message_input.send_keys(description) + self.send_message_button.click() + + def authors_for_reaction(self, emoji: str): + return Button(self.driver, accessibility_id='authors-for-reaction-%s' % emojis[emoji]) diff --git a/test/appium/views/contacts_view.py b/test/appium/views/contacts_view.py deleted file mode 100644 index a1d436681f1..00000000000 --- a/test/appium/views/contacts_view.py +++ /dev/null @@ -1,53 +0,0 @@ -from views.base_element import BaseButton, BaseEditBox -from views.base_view import BaseView - - -class StatusTestDAppButton(BaseButton): - - def __init__(self, driver): - super(StatusTestDAppButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Status Test DApp') - - def navigate(self): - from views.web_views.status_test_dapp import StatusTestDAppView - return StatusTestDAppView(self.driver) - - -class PlusButton(BaseButton): - def __init__(self, driver): - super(PlusButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//android.widget.TextView[@text='+']") - - -class PublicKeyEditBox(BaseEditBox): - def __init__(self, driver): - super(PublicKeyEditBox, self).__init__(driver) - self.locator = self.Locator.accessibility_id('enter-contact-code-input') - - -class ScanContactCodeButton(BaseEditBox): - def __init__(self, driver): - super(ScanContactCodeButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('scan-contact-code-button') - - -class ConfirmPublicKeyButton(BaseButton): - def __init__(self, driver): - super(ConfirmPublicKeyButton, self).__init__(driver) - self.locator = \ - self.Locator.xpath_selector('(//android.view.ViewGroup[@content-desc="icon"])[2]') - - -class ContactsView(BaseView): - - def __init__(self, driver): - super(ContactsView, self).__init__(driver) - self.driver = driver - - self.plus_button = PlusButton(self.driver) - self.public_key_edit_box = PublicKeyEditBox(self.driver) - self.scan_contact_code_button = ScanContactCodeButton(self.driver) - self.confirm_public_key_button = ConfirmPublicKeyButton(self.driver) - - self.status_test_dapp_button = StatusTestDAppButton(self.driver) diff --git a/test/appium/views/dapps_view.py b/test/appium/views/dapps_view.py new file mode 100644 index 00000000000..c73982b8fa3 --- /dev/null +++ b/test/appium/views/dapps_view.py @@ -0,0 +1,27 @@ +from views.base_element import EditBox, BaseElement +from views.base_view import BaseView + + +class DappsView(BaseView): + def __init__(self, driver): + super(DappsView, self).__init__(driver) + + self.enter_url_editbox = EditBox(self.driver, accessibility_id="dapp-url-input") + self.web_page = BaseElement(self.driver, xpath="(//android.webkit.WebView)[1]") + + def open_url(self, url): + self.driver.info("Open url '%s'" % url) + from views.web_views.base_web_view import BaseWebView + web_view = BaseWebView(self.driver) + if not self.enter_url_editbox.is_element_displayed(): + web_view.open_tabs_button.click_if_shown() + web_view.open_new_tab_plus_button.click_if_shown() + self.enter_url_editbox.wait_for_visibility_of_element(20) + self.enter_url_editbox.click() + self.enter_url_editbox.clear() + self.enter_url_editbox.send_keys(url) + self.confirm() + from views.web_views.base_web_view import BaseWebView + BaseWebView(self.driver).wait_for_d_aap_to_load() + return self.get_base_web_view() + diff --git a/test/appium/views/dbs/waku_backup/user.py b/test/appium/views/dbs/waku_backup/user.py new file mode 100644 index 00000000000..5c6d8bc09fd --- /dev/null +++ b/test/appium/views/dbs/waku_backup/user.py @@ -0,0 +1,14 @@ +seed = 'staff actress trash route grab crime leaf uniform dizzy reform issue keep' +display_name = 'End to end' +communities = { + 'admin_open': 'open community', + 'member_open': 'e2e_open', + 'admin_closed': 'closed community', + 'member_closed': 'test_comm_enc', + 'member_pending': 'RC1 testing community' +} + +# contacts = ['Test_contact', 'MyCustomNickname'] # enable back when https://github.com/status-im/status-mobile/issues/15500 is fixed + +contacts = ['zQ3...dWXh5', 'zQ3...Vacac'] +blocked_user = 'Clear Flat Milkweedbug' diff --git a/test/appium/views/elements_templates/BVL_collectible_image_template.png b/test/appium/views/elements_templates/BVL_collectible_image_template.png new file mode 100644 index 00000000000..1c95f0c856e Binary files /dev/null and b/test/appium/views/elements_templates/BVL_collectible_image_template.png differ diff --git a/test/appium/views/elements_templates/BVL_expanded_collectible_image_template.png b/test/appium/views/elements_templates/BVL_expanded_collectible_image_template.png new file mode 100644 index 00000000000..ac619931304 Binary files /dev/null and b/test/appium/views/elements_templates/BVL_expanded_collectible_image_template.png differ diff --git a/test/appium/views/elements_templates/Glitch Punks_collectible_image_template.png b/test/appium/views/elements_templates/Glitch Punks_collectible_image_template.png new file mode 100644 index 00000000000..232f8b92305 Binary files /dev/null and b/test/appium/views/elements_templates/Glitch Punks_collectible_image_template.png differ diff --git a/test/appium/views/elements_templates/Glitch Punks_expanded_collectible_image_template.png b/test/appium/views/elements_templates/Glitch Punks_expanded_collectible_image_template.png new file mode 100644 index 00000000000..44abc97310c Binary files /dev/null and b/test/appium/views/elements_templates/Glitch Punks_expanded_collectible_image_template.png differ diff --git a/test/appium/views/elements_templates/image_1_chat_view.png b/test/appium/views/elements_templates/image_1_chat_view.png new file mode 100644 index 00000000000..86dfc905da1 Binary files /dev/null and b/test/appium/views/elements_templates/image_1_chat_view.png differ diff --git a/test/appium/views/elements_templates/image_1_gallery_view.png b/test/appium/views/elements_templates/image_1_gallery_view.png new file mode 100644 index 00000000000..63f5d0988c8 Binary files /dev/null and b/test/appium/views/elements_templates/image_1_gallery_view.png differ diff --git a/test/appium/views/elements_templates/image_2_chat_view.png b/test/appium/views/elements_templates/image_2_chat_view.png new file mode 100644 index 00000000000..5ff415b098b Binary files /dev/null and b/test/appium/views/elements_templates/image_2_chat_view.png differ diff --git a/test/appium/views/elements_templates/image_2_gallery_view.png b/test/appium/views/elements_templates/image_2_gallery_view.png new file mode 100644 index 00000000000..cca25c931ff Binary files /dev/null and b/test/appium/views/elements_templates/image_2_gallery_view.png differ diff --git a/test/appium/views/elements_templates/images_gallery.png b/test/appium/views/elements_templates/images_gallery.png new file mode 100644 index 00000000000..014f16119ec Binary files /dev/null and b/test/appium/views/elements_templates/images_gallery.png differ diff --git a/test/appium/views/elements_templates/images_gallery_inverted.png b/test/appium/views/elements_templates/images_gallery_inverted.png new file mode 100644 index 00000000000..ab0e2bb94e6 Binary files /dev/null and b/test/appium/views/elements_templates/images_gallery_inverted.png differ diff --git a/test/appium/views/elements_templates/profile_image.png b/test/appium/views/elements_templates/profile_image.png new file mode 100644 index 00000000000..27adbbf28d1 Binary files /dev/null and b/test/appium/views/elements_templates/profile_image.png differ diff --git a/test/appium/views/elements_templates/profile_image_contacts_profile.png b/test/appium/views/elements_templates/profile_image_contacts_profile.png new file mode 100644 index 00000000000..b694703e73c Binary files /dev/null and b/test/appium/views/elements_templates/profile_image_contacts_profile.png differ diff --git a/test/appium/views/elements_templates/profile_image_in_1_1_chat.png b/test/appium/views/elements_templates/profile_image_in_1_1_chat.png new file mode 100644 index 00000000000..b82024e56a7 Binary files /dev/null and b/test/appium/views/elements_templates/profile_image_in_1_1_chat.png differ diff --git a/test/appium/views/elements_templates/profile_image_sign_in_view.png b/test/appium/views/elements_templates/profile_image_sign_in_view.png new file mode 100644 index 00000000000..eb17b5e9990 Binary files /dev/null and b/test/appium/views/elements_templates/profile_image_sign_in_view.png differ diff --git a/test/appium/views/elements_templates/sauce_logo.png b/test/appium/views/elements_templates/sauce_logo.png deleted file mode 100644 index 0edf863e833..00000000000 Binary files a/test/appium/views/elements_templates/sauce_logo.png and /dev/null differ diff --git a/test/appium/views/elements_templates/sauce_logo_red.png b/test/appium/views/elements_templates/sauce_logo_red.png deleted file mode 100644 index 486c23f49eb..00000000000 Binary files a/test/appium/views/elements_templates/sauce_logo_red.png and /dev/null differ diff --git a/test/appium/views/elements_templates/saucelabs_sauce.png b/test/appium/views/elements_templates/saucelabs_sauce.png deleted file mode 100644 index 2985b19bab6..00000000000 Binary files a/test/appium/views/elements_templates/saucelabs_sauce.png and /dev/null differ diff --git a/test/appium/views/elements_templates/status_community_join_button.png b/test/appium/views/elements_templates/status_community_join_button.png new file mode 100644 index 00000000000..c1ea4ee1a60 Binary files /dev/null and b/test/appium/views/elements_templates/status_community_join_button.png differ diff --git a/test/appium/views/elements_templates/status_community_logo.png b/test/appium/views/elements_templates/status_community_logo.png new file mode 100644 index 00000000000..d4a6320edf4 Binary files /dev/null and b/test/appium/views/elements_templates/status_community_logo.png differ diff --git a/test/appium/views/home_view.py b/test/appium/views/home_view.py index 5346a444f1a..0cb24326db1 100644 --- a/test/appium/views/home_view.py +++ b/test/appium/views/home_view.py @@ -1,155 +1,540 @@ -from tests import info -import time +from appium.webdriver.common.mobileby import MobileBy from selenium.common.exceptions import TimeoutException, NoSuchElementException -from views.base_element import BaseButton, BaseText, BaseElement -from views.base_view import BaseView +from typing_extensions import Literal - -class WelcomeImageElement(BaseElement): - def __init__(self, driver): - super(WelcomeImageElement, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//android.widget.ImageView') +from views.base_element import Button, Text, BaseElement, SilentButton +from views.base_view import BaseView, UnreadMessagesCountText -class PlusButton(BaseButton): - def __init__(self, driver): - super(PlusButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("new-chat-button") +class ChatButton(Button): + def __init__(self, driver, **kwargs): + super().__init__(driver, **kwargs) def navigate(self): - from views.start_new_chat_view import StartNewChatView - return StartNewChatView(self.driver) + from views.chat_view import ChatView + return ChatView(self.driver) - def click(self): - from views.start_new_chat_view import StartNewChatButton - desired_element = StartNewChatButton(self.driver) - self.click_until_presence_of_element(desired_element=desired_element) - return self.navigate() + +class ActivityTabButton(Button): + def __init__(self, driver, **kwargs): + super().__init__(driver, **kwargs) + + @property + def counter(self): + return BaseElement(self.driver, + xpath='//*[@content-desc="%s"]//*[@content-desc="notification-dot"]' % self.accessibility_id) -class ChatElement(BaseButton): - def __init__(self, driver, username_part): - super(ChatElement, self).__init__(driver) +class ChatElement(SilentButton): + def __init__(self, driver, username_part, community=False, community_channel=False): self.username = username_part - self.locator = self.Locator.xpath_selector( - "//*[@content-desc='chat-item'][.//*[starts-with(@text,'%s')]]" % self.username) + self.community = community + self.community_channel = community_channel + if self.community_channel: + super().__init__( + driver, + xpath="//*[@content-desc='channel-list-item']//*[starts-with(@text,'# %s')]/.." % username_part) + elif community: + super().__init__( + driver, + xpath="//*[@content-desc='chat-name-text'][starts-with(@text,'%s')]/.." % username_part) + else: + super().__init__( + driver, + xpath="//*[@content-desc='author-primary-name'][starts-with(@text,'%s')]/.." % username_part) def navigate(self): - from views.chat_view import ChatView - return ChatView(self.driver) + if self.community: + from views.chat_view import CommunityView + return CommunityView(self.driver) + else: + from views.chat_view import ChatView + return ChatView(self.driver) def click(self): - from views.chat_view import ChatMessageInput - desired_element = ChatMessageInput(self.driver) + if self.community: + from views.chat_view import CommunityView + desired_element = CommunityView(self.driver).community_title + else: + from views.chat_view import ChatView + desired_element = ChatView(self.driver).chat_message_input self.click_until_presence_of_element(desired_element=desired_element) + return self.navigate() def find_element(self): - info('Looking for %s' % self.name) for i in range(2): try: return super(ChatElement, self).find_element() except NoSuchElementException as e: if i == 0: - HomeView(self.driver).reconnect() + self.wait_for_visibility_of_element(20) else: - e.msg = 'Unable to find chat with user %s' % self.username + e.msg = 'Device %s: Unable to find chat with name %s' % (self.driver.number, self.username) raise e @property - def swipe_delete_button(self): - class DeleteButton(BaseButton): + def new_messages_counter(self): + if self.community: + return UnreadMessagesCountText(self.driver, self.locator) + + class NewMessageCounterText(Text): def __init__(self, driver, parent_locator: str): - super(DeleteButton, self).__init__(driver) - locator_str = "/android.view.ViewGroup/*[@content-desc='icon']" - self.locator = self.Locator.xpath_selector(parent_locator + locator_str) + super().__init__( + driver, + xpath="%s//*[@content-desc='new-message-counter']/android.widget.TextView" % parent_locator) - return DeleteButton(self.driver, self.locator.value) + return NewMessageCounterText(self.driver, self.locator) - def swipe_and_delete(self): - counter = 0 - while counter < 10: - self.swipe_element() - if self.swipe_delete_button.is_element_present(): - break - time.sleep(10) - counter += 1 - self.swipe_delete_button.click() + @property + def chat_preview(self): + class PreveiewMessageText(Text): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, xpath="%s//*[@content-desc='chat-message-text']" % parent_locator) + + return PreveiewMessageText(self.driver, self.locator) @property - def new_messages_counter(self): - class UnreadMessagesCountText(BaseText): + def no_message_preview(self): + class NoMessageText(Text): def __init__(self, driver, parent_locator: str): - super(UnreadMessagesCountText, self).__init__(driver) - locator_str = "//*[@content-desc='unread-messages-count-text']" - self.locator = self.Locator.xpath_selector(parent_locator + locator_str) + super().__init__(driver, xpath="%s//*[@content-desc='no-messages-text']" % parent_locator) - return UnreadMessagesCountText(self.driver, self.locator.value) + return NoMessageText(self.driver, self.locator) + @property + def new_messages_grey_dot(self): + class UnreadMessagesPublicChat(BaseElement): + def __init__(self, driver, parent_locator): + super().__init__(driver, xpath="%s/*[@content-desc='unviewed-messages-public']" % parent_locator) -class ChatNameText(BaseText): - def __init__(self, driver): - super(ChatNameText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-name-text') + return UnreadMessagesPublicChat(self.driver, self.locator) + + @property + def chat_image(self): + return BaseElement(self.driver, xpath=self.locator + "//*[@content-desc='profile-picture']") + + @property + def profile_unblock_button(self): + return Button(self.driver, xpath=self.locator + "/*[@content-desc='Unblock']") + + +class ActivityCenterElement(SilentButton): + def __init__(self, driver, username): + self.chat_name = username + super().__init__(driver, + xpath="//*[contains(@text, '%s')]/ancestor::*[@content-desc='activity']" % username) + + @property + def title(self): + return Text(self.driver, xpath=self.locator + '//*[@content-desc="activity-title"]') + + @property + def unread_indicator(self): + return Button(self.driver, xpath=self.locator + '//*[@content-desc="activity-unread-indicator"]') + + @property + def message_body(self): + return Button(self.driver, xpath=self.locator + '//*[@content-desc="activity-message-body"]') + + @property + def context_tag_text(self): + return Text(self.driver, xpath=self.locator + '//*[@content-desc="context-tag"]/android.widget.TextView').text + + @property + def pending_status_tag(self): + return Text(self.driver, xpath=self.locator + '//*[@content-desc="status-tag-pending"]') + + @property + def review_pairing_request_button(self): + return Button(self.driver, accessibility_id='Review pairing request') + + @property + def more_details_button(self): + return Button(self.driver, accessibility_id='More details') + + def handle_cr(self, element_accessibility: str): + Button( + self.driver, + xpath=self.locator + '/*[@content-desc="%s"]' % element_accessibility + ).wait_for_rendering_ended_and_click() + + def accept_contact_request(self): + self.handle_cr("accept-contact-request") + + def decline_contact_request(self): + self.handle_cr("decline-contact-request") + + def cancel_contact_request(self): + self.handle_cr("cancel-contact-request") + + +class PushNotificationElement(SilentButton): + def __init__(self, driver, pn_text): + self.pn_text = pn_text + super().__init__(driver, xpath="//*[@text='%s']" % pn_text) + + @property + def icon(self): + class PnIconElement(BaseElement): + def __init__(self, driver, parent_locator): + super().__init__(driver, + xpath="%s/../../../../*/*[@resource-id='android:id/message_icon']" % parent_locator) + + return PnIconElement(self.driver, self.locator) + + @property + def username(self): + class PnUsername(BaseElement): + def __init__(self, driver, parent_locator): + super().__init__(driver, + xpath="%s/../../*[@resource-id='android:id/message_name']" % parent_locator) + + return PnUsername(self.driver, self.locator).text + + @property + def group_chat_icon(self): + class GroupChatIconElement(BaseElement): + def __init__(self, driver, parent_locator): + super().__init__(driver, + xpath="%s/../../../../*[@resource-id='android:id/right_icon_container']" % parent_locator) + + return GroupChatIconElement(self.driver, self.locator) + + +class ContactDetailsRow(BaseElement): + def __init__(self, driver, username=None, index=None): + main_locator = "//*[@content-desc='user-list']" + if username: + xpath_locator = "%s[*[contains(@text,'%s')]]" % (main_locator, username) + elif index: + xpath_locator = "%s[%s]" % (main_locator, index) + else: + xpath_locator = main_locator + super().__init__(driver, xpath=xpath_locator) + + self.options_button = Button(self.driver, xpath="(%s//android.widget.ImageView)[2]" % xpath_locator) + self.username_text = Text(self.driver, xpath="(%s//android.widget.TextView)[2]" % xpath_locator) + + +class MuteButton(Button): + def __init__(self, driver, accessibility_id): + super().__init__(driver=driver, accessibility_id=accessibility_id) + + @property + def text(self): + return self.find_element().find_element(by=MobileBy.CLASS_NAME, value="android.widget.TextView").text + + @property + def unmute_caption_text(self): + return self.find_element().find_element(by=MobileBy.XPATH, value="//android.widget.TextView[2]").text -class ChatUrlText(BaseText): +class ShareQRCodeInfoText(Text): def __init__(self, driver): - super(ChatUrlText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-url-text') + super().__init__(driver, accessibility_id="share-qr-code-info-text") + + @property + def text(self): + return self.find_element().find_element(by=MobileBy.XPATH, value="/android.widget.TextView").text class HomeView(BaseView): def __init__(self, driver): - super(HomeView, self).__init__(driver) - self.welcome_image = WelcomeImageElement(self.driver) - self.plus_button = PlusButton(self.driver) - self.chat_name_text = ChatNameText(self.driver) - self.chat_url_text = ChatUrlText(self.driver) - - def wait_for_syncing_complete(self): - info('Waiting for syncing complete:') - while True: + super().__init__(driver) + + self.skip_editing_profile_button = Button(self.driver, xpath="//*[@content-desc='button-two']//*[@text='Skip']") + self.edit_profile_button = Button(self.driver, xpath="//*[@content-desc='button-one']//*[@text='Edit Profile']") + self.submit_create_profile_button = Button(self.driver, accessibility_id="submit-create-profile-button") + self.plus_community_button = Button(self.driver, accessibility_id="new-communities-button") + self.chat_name_text = Text(self.driver, accessibility_id="chat-name-text") + self.start_new_chat_button = ChatButton(self.driver, accessibility_id="start-1-1-chat-button") + self.new_group_chat_button = ChatButton(self.driver, accessibility_id="start-group-chat-button") + self.join_public_chat_button = ChatButton(self.driver, accessibility_id="join-public-chat-button") + self.universal_qr_scanner_button = Button(self.driver, accessibility_id="universal-qr-scanner") + self.invite_friends_button = Button(self.driver, accessibility_id="invite-friends-button") + self.stop_status_service_button = Button(self.driver, accessibility_id="STOP") + self.my_profile_on_start_new_chat_button = Button(self.driver, + xpath="//*[@content-desc='current-account-photo']") + self.communities_button = ChatButton(self.driver, accessibility_id="create-community") + self.create_closed_community_button = ChatButton(self.driver, accessibility_id="create-closed-community") + self.create_open_community_button = ChatButton(self.driver, accessibility_id="create-open-community") + self.create_token_gated_community_button = ChatButton(self.driver, + accessibility_id="create-token-gated-community") + self.ens_banner_close_button = Button(self.driver, accessibility_id=":ens-banner-close-button") + self.user_name_text = Text( + self.driver, xpath="//*[@text='User found']/following-sibling::*/android.widget.TextView[1]") + + # Notification centre + self.notifications_button = Button(self.driver, accessibility_id="notifications-button") + self.notifications_unread_badge = BaseElement( + self.driver, xpath="//*[@content-desc='activity-center-unread-count']/android.widget.TextView") + self.open_activity_center_button = Button(self.driver, accessibility_id="open-activity-center-button") + self.close_activity_centre = Button(self.driver, accessibility_id="close-activity-center") + + self.notifications_select_button = Button(self.driver, translation_id="select") + self.notifications_reject_and_delete_button = Button(self.driver, accessibility_id="reject-and-delete" + "-activity-center") + self.notifications_accept_and_add_button = Button(self.driver, + accessibility_id="accept-and-add-activity-center") + self.notifications_select_all = Button(self.driver, xpath="(//android.widget.CheckBox[" + "@content-desc='checkbox-off'])[1]") + + # Tabs and elements on messages home view + self.recent_tab = Button(self.driver, accessibility_id="tab-recent") + self.groups_tab = Button(self.driver, accessibility_id="tab-groups") + self.contacts_tab = Button(self.driver, accessibility_id="tab-contacts") + self.contact_new_badge = Button(self.driver, accessibility_id="notification-dot") + self.pending_contact_request_button = Button(self.driver, + accessibility_id="open-activity-center-contact-requests") + self.pending_contact_request_text = Text( + self.driver, + xpath='//*[@content-desc="pending-contact-requests-count"]/android.widget.TextView') + + # Tabs and elements on community home view + self.pending_communities_tab = Button(self.driver, accessibility_id="pending-tab") + self.joined_communities_tab = Button(self.driver, accessibility_id="joined-tab") + self.opened_communities_tab = Button(self.driver, accessibility_id="opened-tab") + + # Options on long tap + self.chats_menu_invite_friends_button = Button(self.driver, accessibility_id="chats-menu-invite-friends-button") + self.close_chat_button = Button(self.driver, accessibility_id="close-chat") + self.confirm_closing_chat_button = Button(self.driver, accessibility_id="Confirm") + self.clear_history_button = Button(self.driver, accessibility_id="clear-history") + self.mute_chat_button = MuteButton(self.driver, accessibility_id="mute-chat") + self.mute_community_button = MuteButton(self.driver, accessibility_id="mute-community") + self.unmute_community_button = MuteButton(self.driver, accessibility_id="unmute-community") + self.mute_channel_button = MuteButton(self.driver, accessibility_id="chat-toggle-muted") + self.mark_all_messages_as_read_button = Button(self.driver, accessibility_id="mark-as-read") + + # Connection icons + self.connection_offline_icon = Button(self.driver, accessibility_id="conn-button-offline") + self.new_chat_button = Button(self.driver, accessibility_id="new-chat-button") + self.discover_communities_button = Button(self.driver, accessibility_id="communities-home-discover-card") + + # Bottom sheet + self.start_a_new_chat_bottom_sheet_button = Button(self.driver, accessibility_id="start-a-new-chat") + self.add_a_contact_chat_bottom_sheet_button = Button(self.driver, accessibility_id="add-a-contact") + + # Activity centre + self.all_activity_tab_button = ActivityTabButton(self.driver, translation_id="all") + self.mention_activity_tab_button = ActivityTabButton(self.driver, accessibility_id="tab-mention") + self.reply_activity_tab_button = ActivityTabButton(self.driver, accessibility_id="tab-reply") + self.activity_notification_swipe_button = Button(self.driver, accessibility_id="notification-swipe") + self.activity_unread_filter_button = Button(self.driver, accessibility_id="selector-filter") + self.more_options_activity_button = Button(self.driver, accessibility_id="activity-center-open-more") + self.mark_all_read_activity_button = Button(self.driver, translation_id="mark-all-notifications-as-read") + + # Share tab + self.share_qr_code_info_text = ShareQRCodeInfoText(self.driver) + self.link_to_profile_button = Button(self.driver, accessibility_id="link-to-profile") + self.link_to_profile_text = Text(self.driver, accessibility_id="share-qr-code-info-text") + self.close_share_tab_button = Button(self.driver, accessibility_id="close-shell-share-tab") + self.qr_code_image_element = BaseElement(self.driver, accessibility_id='share-qr-code') + self.share_profile_tab_button = Button(self.driver, accessibility_id="Profile") + self.share_wallet_tab_button = Button(self.driver, accessibility_id="Wallet") + self.account_avatar = BaseElement(self.driver, accessibility_id="account-avatar") + self.account_name_text = Text( + self.driver, xpath="//*[@content-desc='link-to-profile']/preceding-sibling::android.widget.TextView") + self.share_link_to_profile_button = Button(self.driver, accessibility_id='link-to-profile') + # Discover communities + self.community_card_item = BaseElement(self.driver, accessibility_id="community-card-item") + + def get_activity_center_element_by_text(self, text_part): + return ActivityCenterElement(self.driver, text_part) + + def get_chat(self, username, community=False, community_channel=False, wait_time=10): + if community: + self.driver.info("Looking for community: '%s'" % username) + else: + self.driver.info("Looking for chat: '%s'" % username) + chat_element = ChatElement(self.driver, username[:25], community=community, community_channel=community_channel) + if not chat_element.is_element_displayed(wait_time) and community is False and community_channel is False: + if self.notifications_unread_badge.is_element_displayed(30): + chat_in_ac = self.get_activity_center_element_by_text(username[:25]) + self.open_activity_center_button.click_until_presence_of_element(chat_in_ac) + chat_in_ac.wait_for_element(20) + chat_in_ac.click() + return chat_element + + def get_to_community_channel_from_home(self, community_name, channel_name='general'): + community_view = self.get_community_view() + self.get_chat(community_name, community=True).click() + return community_view.get_channel(channel_name).click() + + def get_chat_from_home_view(self, username): + self.driver.info("Looking for chat: '%s'" % username) + chat_element = ChatElement(self.driver, username[:25]) + return chat_element + + def get_element_from_activity_center_view(self, message_body): + self.driver.info("Looking for activity center element: '%s'" % message_body) + chat_element = self.get_activity_center_element_by_text(message_body) + return chat_element + + def handle_contact_request(self, username: str, action: str = 'accept', notifications_count: int = 1): + if self.toast_content_element.is_element_displayed(10): + self.toast_content_element.wait_for_invisibility_of_element() + try: + self.notifications_unread_badge.wait_for_visibility_of_element(seconds=120) + except TimeoutException: + pass + if notifications_count > 1: try: - sync = self.find_text_part('Syncing', 10) - info(sync.text) + self.notifications_unread_badge.wait_for_element_text(text=str(notifications_count), wait_time=120) except TimeoutException: - break - - def get_chat_with_user(self, username): - return ChatElement(self.driver, username[:25]) - - def add_contact(self, public_key): - start_new_chat = self.plus_button.click() - start_new_chat.start_new_chat_button.click() - start_new_chat.public_key_edit_box.set_value(public_key) - one_to_one_chat = self.get_chat_view() - start_new_chat.confirm_until_presence_of_element(one_to_one_chat.chat_message_input) - return one_to_one_chat - - def start_1_1_chat(self, username): - start_new_chat = self.plus_button.click() - start_new_chat.start_new_chat_button.click() - self.element_by_text(username).click() - from views.chat_view import ChatView - return ChatView(self.driver) + pass + self.open_activity_center_button.click_until_presence_of_element(self.close_activity_centre) + chat_element = self.get_activity_center_element_by_text(username[:25]) + try: + if action == 'accept': + self.driver.info("Accepting incoming CR for %s" % username) + chat_element.accept_contact_request() + elif action == 'decline': + self.driver.info("Rejecting incoming CR for %s" % username) + chat_element.decline_contact_request() + elif action == 'cancel': + self.driver.info("Canceling outgoing CR for %s" % username) + chat_element.cancel_contact_request() + else: + self.driver.fail("Illegal option for CR!") + except (NoSuchElementException, TimeoutException): + self.driver.fail("No contact request received from %s" % username) + finally: + self.close_activity_centre.wait_for_rendering_ended_and_click() + self.chats_tab.wait_for_visibility_of_element() + + def add_contact(self, public_key, username='', nickname='', close_profile=True): + self.driver.info("Adding user to Contacts via chats > add new contact") + self.new_chat_button.click() + if username: + self.edit_profile_button.click() + self.get_sign_in_view().profile_title_input.send_keys(username) + self.submit_create_profile_button.click() + else: + self.skip_editing_profile_button.click_if_shown() + self.add_a_contact_chat_bottom_sheet_button.click() + + chat = self.get_chat_view() + chat.public_key_edit_box.click() + chat.public_key_edit_box.send_keys(public_key) + chat.element_by_translation_id("user-found").wait_for_visibility_of_element() + if not chat.view_profile_new_contact_button.is_element_displayed(): + chat.click_system_back_button() + chat.view_profile_new_contact_button.click_until_presence_of_element(chat.profile_send_contact_request_button) + chat.profile_send_contact_request_button.click() + chat.contact_request_message_input.send_keys("hi") + chat.confirm_send_contact_request_button.click() + if nickname: + chat.set_nickname(nickname) + if close_profile: + chat.close_button.click_until_absense_of_element(chat.close_button) def create_group_chat(self, user_names_to_add: list, group_chat_name: str = 'new_group_chat'): - start_new_chat = self.plus_button.click() - start_new_chat.new_group_chat_button.click() + self.driver.info("## Creating group chat '%s'" % group_chat_name, device=False) + self.new_chat_button.click() + chat = self.get_chat_view() + self.start_a_new_chat_bottom_sheet_button.click() for user_name in user_names_to_add: - user_contact = start_new_chat.get_username_checkbox(user_name) - user_contact.scroll_to_element() - user_contact.click() - start_new_chat.next_button.click() - start_new_chat.chat_name_editbox.send_keys(group_chat_name) - start_new_chat.create_button.click() - - def join_public_chat(self, chat_name: str): - start_new_chat = self.plus_button.click() - start_new_chat.join_public_chat_button.click() - start_new_chat.chat_name_editbox.set_value(chat_name) - time.sleep(2) - chat_view = self.get_chat_view() - start_new_chat.confirm_until_presence_of_element(chat_view.chat_message_input) - return chat_view + check_box = chat.get_username_checkbox(user_name) + if not check_box.is_element_displayed(): + raise NoSuchElementException( + "User with the name '%s' is not in contacts list so can't create a group chat" % user_name) + check_box.click_until_presence_of_element(chat.get_username_checkbox(user_name, state_on=True)) + self.next_button.click() + self.get_sign_in_view().profile_title_input.send_keys(group_chat_name) + chat.create_group_chat_button.click() + self.driver.info("## Group chat %s is created successfully!" % group_chat_name, device=False) + return chat + + def create_community(self, community_type: Literal["open", "closed", "token-gated"]): + self.driver.info("## Creating %s community" % community_type) + self.plus_community_button.click() + if community_type == "open": + self.create_open_community_button.click() + elif community_type == "closed": + self.create_closed_community_button.click() + elif community_type == "token-gated": + self.create_token_gated_community_button.click() + else: + raise ValueError("Incorrect community type is set") + + def delete_chat_long_press(self, username): + self.driver.info("Deleting chat '%s' by long press" % username) + self.get_chat(username).long_press_without_release() + self.close_chat_button.double_click() + self.confirm_closing_chat_button.click() + + def clear_chat_long_press(self, username): + self.driver.info("Clearing history in chat '%s' by long press" % username) + self.get_chat(username).long_press_element() + self.clear_history_button.click() + from views.chat_view import ChatView + ChatView(self.driver).clear_button.click() + + def mute_chat_long_press(self, chat_name, mute_period="mute-till-unmute", community=False, community_channel=False): + self.driver.info("Muting chat with %s" % chat_name) + self.get_chat(username=chat_name, community=community, + community_channel=community_channel).long_press_without_release() + if community: + element = self.mute_community_button + elif community_channel: + element = self.mute_channel_button + else: + element = self.mute_chat_button + element.double_click() + self.element_by_translation_id(mute_period).click() + + def get_pn(self, pn_text: str, wait_time: int = 60): + self.driver.info("Getting PN by '%s'" % pn_text) + expected_element = PushNotificationElement(self.driver, pn_text) + return expected_element if expected_element.is_element_displayed(wait_time) else False + + def contact_details_row(self, username=None, index=None): + return ContactDetailsRow(self.driver, username=username, index=index) + + def get_contact_rows_count(self): + return len(ContactDetailsRow(self.driver).find_elements()) + + def get_link_to_profile(self): + self.driver.info('Getting profile link via share profile QR') + self.show_qr_code_button.click() + self.share_profile_tab_button.click() + self.link_to_profile_text.click() + link_to_profile = self.driver.get_clipboard_text() + if not link_to_profile: + raise NoSuchElementException("Can't get link to profile") + return link_to_profile + + def get_public_key(self): + self.driver.info("Getting public key via Share tab") + link_to_profile = self.get_link_to_profile() + self.click_system_back_button() + return link_to_profile.split("#")[-1] + + def copy_wallet_address(self): + self.share_link_to_profile_button.click() + address = self.sharing_text_native.text + self.click_system_back_button() + return address + + def get_discover_community_card_by_name(self, community_name: str): + return BaseElement( + self.driver, + xpath="//*[@content-desc='%s'][descendant::*[@content-desc='chat-name-text'][@text='%s']]" % ( + self.community_card_item.accessibility_id, community_name) + ) + + def get_new_device_installation_id(self): + element = Text(self.driver, accessibility_id='new-device-installation-id') + element.wait_for_visibility_of_element() + return element.text + + def get_username(self): + self.toast_content_element.wait_for_invisibility_of_element() + profile_view = self.get_profile_view() + self.profile_button.click_until_presence_of_element(profile_view.default_username_text) + profile_view.default_username_text.wait_for_element(3) + username = profile_view.default_username_text.text + profile_view.click_system_back_button() + return username diff --git a/test/appium/views/profile_view.py b/test/appium/views/profile_view.py index 26111f7b547..b5b3d5e8365 100644 --- a/test/appium/views/profile_view.py +++ b/test/appium/views/profile_view.py @@ -1,511 +1,389 @@ import time -from tests import info -from tests.base_test_case import AbstractTestCase -from views.base_element import BaseText, BaseButton, BaseEditBox, BaseElement -from views.base_view import BaseView - - -class PublicKeyText(BaseText): - - def __init__(self, driver): - super(PublicKeyText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('address-text') - @property - def text(self): - text = self.scroll_to_element().text - info('%s is %s' % (self.name, text)) - return text - - -class ProfileAddressText(BaseText): - - def __init__(self, driver): - super(ProfileAddressText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('profile-address') +import pytest +from selenium.common import NoSuchElementException, StaleElementReferenceException +from tests import common_password +from tests.base_test_case import AbstractTestCase +from views.base_element import Text, Button, EditBox, SilentButton, BaseElement +from views.base_view import BaseView -class OptionsButton(BaseButton): +class OptionsButton(Button): def __init__(self, driver): - super(OptionsButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - '(//android.view.ViewGroup[@content-desc="icon"])[2]') - - class UserStatusBox(BaseButton): - - def __init__(self, driver): - super(OptionsButton.UserStatusBox, self).__init__(driver) - self.locator = self.Locator.xpath_selector('(//android.widget.ScrollView)[2]//android.widget.TextView') - - class UsernameInput(BaseEditBox): - - def __init__(self, driver): - super(OptionsButton.UsernameInput, self).__init__(driver) - self.locator = self.Locator.accessibility_id('username-input') - - class UserStatusInput(BaseEditBox): - - def __init__(self, driver): - super(OptionsButton.UserStatusInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector('(//android.widget.EditText)[2]') - + super().__init__(driver, xpath="(//android.view.ViewGroup[@content-desc='icon'])[2]") -class NetworkSettingsButton(BaseButton): +class LogoutButton(SilentButton): def __init__(self, driver): - super(NetworkSettingsButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('network-button') + super().__init__(driver, translation_id="logout") - class NetworkButton(BaseButton): - def __init__(self, driver, network): - super(NetworkSettingsButton.NetworkButton, self).__init__(driver) - self.locator = self.Locator.text_selector(network) - - class ConnectButton(BaseButton): - - def __init__(self, driver): - super(NetworkSettingsButton.ConnectButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('network-connect-button') - - -class LogoutButton(BaseButton): - - def __init__(self, driver): - super(LogoutButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('log-out-button') + def click(self): + self.scroll_to_element().click() + return self.navigate() class LogoutDialog(BaseView): def __init__(self, driver): - super(LogoutDialog, self).__init__(driver) + super().__init__(driver) self.logout_button = LogoutDialog.LogoutButton(driver) - class LogoutButton(BaseButton): + class LogoutButton(SilentButton): def __init__(self, driver): - super(LogoutDialog.LogoutButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='LOG OUT' or @text='Log out']") + super().__init__(driver, translation_id="logout", uppercase=True) def navigate(self): from views.sign_in_view import SignInView return SignInView(self.driver) -class ConfirmLogoutButton(BaseButton): - +class AdvancedButton(Button): def __init__(self, driver): - super(ConfirmLogoutButton, self).__init__(driver) - self.locator = self.Locator.text_selector('LOG OUT') - - -class UserNameText(BaseText): - def __init__(self, driver): - super(UserNameText, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - '//android.widget.ImageView[@content-desc="chat-icon"]/../../android.widget.TextView') - - -class ShareMyContactKeyButton(BaseButton): - - def __init__(self, driver): - super(ShareMyContactKeyButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('share-my-contact-code-button') - - -class EditButton(BaseButton): - - def __init__(self, driver): - super(EditButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('edit-button') - - -class ProfilePictureElement(BaseElement): - def __init__(self, driver): - super(ProfilePictureElement, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-icon') - - -class EditPictureButton(BaseButton): - - def __init__(self, driver): - super(EditPictureButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('edit-profile-photo-button') - - -class ConfirmEditButton(BaseButton): - - def __init__(self, driver): - super(ConfirmEditButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('done-button') - - -class CrossIcon(BaseButton): - - def __init__(self, driver): - super(CrossIcon, self).__init__(driver) - self.locator = self.Locator.accessibility_id('done-button') - - -class ShareButton(BaseButton): - - def __init__(self, driver): - super(ShareButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('share-code-button') - - -class AdvancedButton(BaseButton): - - def __init__(self, driver): - super(AdvancedButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Advanced') + super().__init__(driver, accessibility_id="icon, Advanced, label-component, icon") def click(self): self.scroll_to_element().click() - info('Tap on %s' % self.name) return self.navigate() -class BackupRecoveryPhraseButton(BaseButton): - +class RecoveryPhraseTable(Text): def __init__(self, driver): - super(BackupRecoveryPhraseButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Backup your recovery phrase') + super().__init__(driver, translation_id="backup-recovery-phrase", + suffix="/following-sibling::android.view.ViewGroup[2]//android.widget.TextView") -class OkContinueButton(BaseButton): - - def __init__(self, driver): - super(OkContinueButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='OK, CONTINUE']") - - -class RecoveryPhraseTable(BaseText): - +class RecoveryPhraseWordNumberText(Text): def __init__(self, driver): - super(RecoveryPhraseTable, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - '//android.widget.FrameLayout/android.view.ViewGroup[3]/android.widget.TextView') - - -class RecoveryPhraseWordNumberText(BaseText): - - def __init__(self, driver): - super(RecoveryPhraseWordNumberText, self).__init__(driver) - self.locator = self.Locator.text_part_selector('#') + super().__init__(driver, xpath="//*[@content-desc='number-container']/android.widget.TextView") @property def number(self): time.sleep(1) - return int(self.find_element().text.split('#')[1]) - - -class RecoveryPhraseWordInput(BaseEditBox): + return int(self.find_element().text) - def __init__(self, driver): - super(RecoveryPhraseWordInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//android.widget.EditText') - - -class OkGotItButton(BaseButton): +class HelpButton(Button): def __init__(self, driver): - super(OkGotItButton, self).__init__(driver) - self.locator = self.Locator.text_selector('OK, GOT IT') - - -class DebugModeToggle(BaseButton): - - def __init__(self, driver): - super(DebugModeToggle, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.Switch") + super().__init__(driver, accessibility_id="help-button") def click(self): - self.scroll_to_element() - super(DebugModeToggle, self).click() - - -class SelectFromGalleryButton(BaseButton): - - def __init__(self, driver): - super(SelectFromGalleryButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Select from gallery') - - -class CaptureButton(BaseButton): - - def __init__(self, driver): - super(CaptureButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Capture') - - -class MainCurrencyButton(BaseButton): - - def __init__(self, driver): - super(MainCurrencyButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("currency-button") - - -class PlusButton(BaseButton): - - def __init__(self, driver): - super(PlusButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("(//android.view.ViewGroup[@content-desc='icon'])[2]") - - -class RopstenChainButton(BaseButton): - - def __init__(self, driver): - super(RopstenChainButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[contains(@text,'Ropsten test network')]/following-sibling::android.widget.CheckBox[1]") - - -class SpecifyNameInput(BaseEditBox): - - def __init__(self, driver): - super(SpecifyNameInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Name']/following-sibling::*[1]/android.widget.EditText") - - -class CustomNetworkURL(BaseEditBox): - - def __init__(self, driver): - super(CustomNetworkURL, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[@text='RPC URL']/following-sibling::*[1]/android.widget.EditText") - - -class HelpButton(BaseButton): - - def __init__(self, driver): - super(HelpButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("help-button") - - -class RequestFeatureButton(BaseButton): - - def __init__(self, driver): - super(RequestFeatureButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("request-feature-button") - - -class SubmitBugButton(BaseButton): - - def __init__(self, driver): - super(SubmitBugButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("submit-bug-button") - - -class FaqButton(BaseButton): - - def __init__(self, driver): - super(FaqButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id("faq-button") - - def navigate(self): - from views.web_views.base_web_view import BaseWebView - return BaseWebView(self.driver) - - -class BootnodesButton(BaseButton): - - def __init__(self, driver): - super(BootnodesButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('bootnodes-settings-button') - - -class AddBootnodeButton(BaseButton): - - def __init__(self, driver): - super(AddBootnodeButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("(//*[@content-desc='icon'])[2]") - - -class BootnodeNameInput(BaseEditBox): - - def __init__(self, driver): - super(BootnodeNameInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.EditText[@text='Specify a name']") - - -class BootnodeAddressInput(BaseEditBox): - - def __init__(self, driver): - super(BootnodeAddressInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.EditText[@text='Specify bootnode address']") - - -class EnableBootnodesToggle(BaseEditBox): - - def __init__(self, driver): - super(EnableBootnodesToggle, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//android.widget.Switch') - - -class MailServerButton(BaseButton): - - def __init__(self, driver): - super(MailServerButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('offline-messages-settings-button') - + self.scroll_to_element().click() -class MailServerAddressInput(BaseEditBox): +class SyncSettingsButton(Button): def __init__(self, driver): - super(MailServerAddressInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.EditText[@text='Specify a mailserver address']") - + super().__init__(driver, accessibility_id="sync-settings-button") -class MailServerElement(BaseButton): - - def __init__(self, driver, server_name): - super(MailServerElement, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@content-desc='mailserver-item']//*[@text='%s']" % server_name) - - -class MailServerConnectButton(BaseButton): - - def __init__(self, driver): - super(MailServerConnectButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('mailserver-connect-button') + def click(self): + self.scroll_to_element().click() class ProfileView(BaseView): def __init__(self, driver): - super(ProfileView, self).__init__(driver) - self.driver = driver - + super().__init__(driver) self.options_button = OptionsButton(self.driver) - self.username_input = OptionsButton.UsernameInput(self.driver) - self.user_status_box = OptionsButton.UserStatusBox(self.driver) - self.user_status_input = OptionsButton.UserStatusInput(self.driver) - self.public_key_text = PublicKeyText(self.driver) - self.profile_address_text = ProfileAddressText(self.driver) - - self.network_settings_button = NetworkSettingsButton(self.driver) - self.plus_button = PlusButton(self.driver) - self.ropsten_chain_button = RopstenChainButton(self.driver) - self.custom_network_url = CustomNetworkURL(self.driver) - self.specify_name_input = SpecifyNameInput(self.driver) - self.connect_button = NetworkSettingsButton.ConnectButton(self.driver) - self.logout_button = LogoutButton(self.driver) - self.logout_dialog = LogoutDialog(self.driver) - self.confirm_logout_button = ConfirmLogoutButton(self.driver) - - self.main_currency_button = MainCurrencyButton(self.driver) - - self.username_text = UserNameText(self.driver) - self.share_my_contact_key_button = ShareMyContactKeyButton(self.driver) - self.edit_button = EditButton(self.driver) - self.profile_picture = ProfilePictureElement(self.driver) - self.edit_picture_button = EditPictureButton(self.driver) - self.confirm_edit_button = ConfirmEditButton(self.driver) - self.cross_icon = CrossIcon(self.driver) - self.share_button = ShareButton(self.driver) - self.advanced_button = AdvancedButton(self.driver) - self.debug_mode_toggle = DebugModeToggle(self.driver) - # Backup seed phrase - self.backup_recovery_phrase_button = BackupRecoveryPhraseButton(self.driver) - self.ok_continue_button = OkContinueButton(self.driver) + # Header + self.default_username_text = Text(self.driver, accessibility_id="username") + self.bio_text = Text(self.driver, + xpath="//*[@content-desc='username']/following-sibling::*/android.widget.TextView") + self.contact_name_text = Text(self.driver, accessibility_id="contact-name") + self.profile_picture = BaseElement(self.driver, accessibility_id="profile-picture") + self.user_avatar = BaseElement( + self.driver, xpath="//*[@content-desc='dropdown']/preceding-sibling::*/*[@content-desc='user-avatar']") + self.online_indicator = Button(self.driver, accessibility_id="online-profile-photo-dot") + self.crop_photo_button = Button(self.driver, accessibility_id="Crop") + self.shutter_button = Button(self.driver, accessibility_id="Shutter") + self.accept_photo_button = Button(self.driver, accessibility_id="Done") + + # Contacts + self.contacts_button = Button(self.driver, accessibility_id="contacts-button") + + # Privacy and security + self.privacy_and_security_button = Button(self.driver, accessibility_id="privacy-and-security-settings-button") + self.reset_password_button = Button(self.driver, accessibility_id="reset-password") + self.current_password_edit_box = EditBox(self.driver, accessibility_id="current-password") + self.new_password_edit_box = EditBox(self.driver, accessibility_id="new-password") + self.confirm_new_password_edit_box = EditBox(self.driver, accessibility_id="confirm-new-password") + self.current_password_wrong_text = Text(self.driver, accessibility_id="current-password-error") + + # Appearance + self.appearance_button = Button(self.driver, accessibility_id="appearance-settings-button") + + ## Backup recovery phrase + self.backup_recovery_phrase_button = Button( + self.driver, accessibility_id="icon, Backup recovery phrase, label-component, icon") + self.recovery_phrase_table = RecoveryPhraseTable(self.driver) self.recovery_phrase_word_number = RecoveryPhraseWordNumberText(self.driver) - self.recovery_phrase_word_input = RecoveryPhraseWordInput(self.driver) - self.ok_got_it_button = OkGotItButton(self.driver) - - self.select_from_gallery_button = SelectFromGalleryButton(self.driver) - self.capture_button = CaptureButton(self.driver) - - self.help_button = HelpButton(self.driver) - self.request_feature_button = RequestFeatureButton(self.driver) - self.submit_bug_button = SubmitBugButton(self.driver) - self.faq_button = FaqButton(self.driver) - - # Bootnodes - self.bootnodes_button = BootnodesButton(self.driver) - self.bootnode_address_input = BootnodeAddressInput(self.driver) - self.enable_bootnodes = EnableBootnodesToggle(self.driver) - - # Mailservers - self.mail_server_button = MailServerButton(self.driver) - self.mail_server_address_input = MailServerAddressInput(self.driver) - self.mail_server_connect_button = MailServerConnectButton(self.driver) - - def switch_network(self, network): - self.advanced_button.click() - self.debug_mode_toggle.click() - self.network_settings_button.scroll_to_element() - self.network_settings_button.click() - network_button = NetworkSettingsButton.NetworkButton(self.driver, network) - network_button.click() - self.connect_button.click() - from views.sign_in_view import SignInView - return SignInView(self.driver) - - def add_custom_network(self): - self.advanced_button.click() - self.debug_mode_toggle.click() - self.network_settings_button.scroll_to_element() - self.network_settings_button.click() - self.plus_button.click_until_presence_of_element(self.ropsten_chain_button) - self.ropsten_chain_button.click() - self.custom_network_url.send_keys('https://ropsten.infura.io/iMko0kJNQUdhbCSaJcox') - self.specify_name_input.send_keys('custom_ropsten') - self.save_button.click() - self.element_by_text_part('custom_ropsten').click_until_presence_of_element(self.connect_button) - self.connect_button.click() - return self.get_sign_in_view() - - def get_address(self): - profile_view = self.profile_button.click() - return profile_view.profile_address_text.text + + # Notifications + self.profile_notifications_button = Button(self.driver, + accessibility_id="icon, Notifications, label-component, icon") + self.profile_notifications_toggle_button = Button(self.driver, + accessibility_id="Show notifications, label-component") + self.push_notification_toggle = Button( + self.driver, xpath="//*[@content-desc='notifications-button']//*[@content-desc='switch']") + self.wallet_push_notifications = Button(self.driver, accessibility_id="notifications-button") + + # Sync settings + self.sync_settings_button = SyncSettingsButton(self.driver) + self.backup_settings_button = Button(self.driver, accessibility_id="backup-settings-button") + self.perform_backup_button = Button(self.driver, translation_id="perform-backup") + + ## Device syncing + self.syncing_button = Button(self.driver, accessibility_id="icon, Syncing, label-component, icon") + self.paired_devices_button = Button( + self.driver, xpath="//android.view.ViewGroup[contains(@content-desc,'icon, Paired devices,')]") + self.sync_plus_button = Button( + self.driver, + xpath="//*[@text='Paired devices']/following-sibling::android.view.ViewGroup[@content-desc='icon']") + self.sync_and_backup_button = Button(self.driver, xpath="//*[@text='Sync and backup']") + self.wi_fi_only_button = Button(self.driver, accessibility_id='wifi-only-action') + + # Advanced + self.advanced_button = AdvancedButton(self.driver) + self.log_level_setting_button = Button(self.driver, accessibility_id="log-level-settings-button") + self.fleet_setting_button = Button(self.driver, accessibility_id="fleet-settings-button") + + # Need help + self.submit_bug_button = Button(self.driver, accessibility_id="submit-bug-button") + self.bug_description_edit_box = EditBox(self.driver, accessibility_id="bug-report-description") + self.bug_steps_edit_box = EditBox(self.driver, accessibility_id="bug-report-steps") + self.bug_submit_button = Button(self.driver, accessibility_id="bug-report-submit") + self.request_a_feature_button = Button(self.driver, accessibility_id="request-a-feature-button") + + # About + self.app_version_text = Text(self.driver, xpath="//*[@content-desc='app-version']//android.widget.TextView[2]") + self.node_version_text = Text(self.driver, + xpath="//*[@content-desc='node-version']//android.widget.TextView[2]") + + # Logout + self.logout_button = LogoutButton(self.driver) + self.logout_dialog = LogoutDialog(self.driver) + self.confirm_logout_button = Button(self.driver, translation_id="logout", uppercase=True) + + # New profile + self.edit_profile_button = Button(self.driver, accessibility_id="icon, Edit Profile, label-component, icon") + self.change_profile_photo_button = Button( + self.driver, + xpath="//*[@content-desc='user-avatar']/following-sibling::android.view.ViewGroup[@content-desc='icon']") + self.take_photo_button = Button(self.driver, accessibility_id="take-photo-button") + self.select_from_gallery_button = Button(self.driver, accessibility_id="select-from-gallery-button") + self.profile_password_button = Button(self.driver, accessibility_id="icon, Password, label-component, icon") + self.profile_messages_button = Button(self.driver, accessibility_id="icon, Messages, label-component, icon") + self.profile_blocked_users_button = Button(self.driver, accessibility_id="Blocked users, label-component, icon") + self.profile_wallet_button = Button(self.driver, accessibility_id="icon, Wallet, label-component, icon") + self.profile_networks_button = Button(self.driver, accessibility_id='icon, Networks, label-component, icon') + self.network_settings_button = Button(self.driver, accessibility_id="Network settings, label-component, icon") + self.testnet_mode_toggle = Button(self.driver, accessibility_id="icon, Testnet mode, label-component") + self.confirm_testnet_mode_change_button = Button(self.driver, accessibility_id="confirm-testnet-mode-change") + self.key_pairs_and_accounts_button = Button(self.driver, + accessibility_id="Key pairs and accounts, label-component, icon") + self.import_by_entering_recovery_phrase_button = Button(self.driver, accessibility_id="import-seed-phrase") + + # Edit profile + self.edit_profile_name_button = Button(self.driver, accessibility_id='Name, label-component, icon') + self.edit_profile_input = EditBox(self.driver, accessibility_id='input') + self.save_name_button = Button(self.driver, accessibility_id='Save name') + self.edit_bio_button = Button(self.driver, accessibility_id='Bio, label-component, icon') + self.save_bio_button = Button(self.driver, accessibility_id='Save bio') + self.edit_accent_colour = Button(self.driver, accessibility_id='Accent colour, label-component, icon') + self.save_colour_button = Button(self.driver, accessibility_id='Save colour') + + # Language and currency + self.language_and_currency_button = Button( + self.driver, accessibility_id='icon, Language and currency, label-component, icon') + self.change_currency_button = Button( + self.driver, + xpath="//*[@text='Currency']/following-sibling::*/*[contains(@content-desc,'label-component, icon')]") + + def navigate_back_to_main_profile_view(self, attempts=3): + for _ in range(attempts): + try: + if self.view_id_tracker.text == 'settings': + return + except NoSuchElementException: + self.click_system_back_button() + except StaleElementReferenceException: + continue + + def switch_network(self): + self.driver.info("Toggling test mode") + self.profile_networks_button.scroll_and_click() + self.testnet_mode_toggle.click() + self.confirm_testnet_mode_change_button.click() + + def switch_push_notifications(self): + self.driver.info("Enabling push notifications via Profile") + self.profile_notifications_button.scroll_and_click() + self.profile_notifications_toggle_button.click() + self.allow_button.click_if_shown() def get_recovery_phrase(self): text = [i.text for i in self.recovery_phrase_table.find_elements()] return dict(zip(map(int, text[::2]), text[1::2])) + def get_correct_word_button(self, word: str): + return Button(self.driver, accessibility_id=word) + + def get_incorrect_word_button(self, word: str): + try: + button = Button(self.driver, + xpath="//*[@content-desc='%s']/following-sibling::android.view.ViewGroup" % word) + button.find_element() + except NoSuchElementException: + button = Button(self.driver, + xpath="//*[@content-desc='%s']/preceding-sibling::*/*/android.widget.TextView" % word) + return button + + def fill_recovery_phrase_checking_words(self, recovery_phrase: dict): + for _ in range(4): + word_number = self.recovery_phrase_word_number.number + self.get_correct_word_button(recovery_phrase[word_number]).click() + def backup_recovery_phrase(self): + self.just_fyi("Back up recovery phrase") self.backup_recovery_phrase_button.click() - self.ok_continue_button.click() + for checkbox in self.checkbox_button.find_elements(): + checkbox.click() + self.button_one.click() recovery_phrase = self.get_recovery_phrase() - self.next_button.click() - word_number = self.recovery_phrase_word_number.number - self.recovery_phrase_word_input.set_value(recovery_phrase[word_number]) - self.next_button.click() - word_number_1 = self.recovery_phrase_word_number.number - self.recovery_phrase_word_input.set_value(recovery_phrase[word_number_1]) - self.done_button.click() - self.yes_button.click() - self.ok_got_it_button.click() - return recovery_phrase - - def edit_profile_picture(self, file_name: str): - if not AbstractTestCase().environment == 'sauce': - raise NotImplementedError('Test case is implemented to run on SauceLabs only') - self.profile_picture.template = file_name - self.edit_button.click() - self.swipe_down() - self.edit_picture_button.click() - self.select_from_gallery_button.click() - if self.allow_button.is_element_displayed(sec=10): + self.button_one.click() + self.fill_recovery_phrase_checking_words(recovery_phrase) + self.checkbox_button.click() + self.button_one.click() + return ' '.join(recovery_phrase.values()) + + def edit_profile_picture(self, image_index: int, update_by="Gallery"): + self.driver.info("## Setting custom profile image", device=False) + if AbstractTestCase().environment != 'lt': + raise NotImplementedError('Test case is implemented to run on LambdaTest only') + self.edit_profile_button.click() + self.change_profile_photo_button.click() + if update_by == "Gallery": + self.select_from_gallery_button.click() + self.select_photo_from_gallery_by_index(image_index) + else: + ## take by Photo + self.take_photo() + self.click_system_back_button() + self.profile_picture.click() + self.take_photo() + self.accept_photo_button.click() + self.crop_photo_button.click() + self.driver.info("## Custom profile image has been set", device=False) + self.click_system_back_button() + + def edit_username(self, new_username: str): + self.edit_profile_button.click() + self.edit_profile_name_button.click() + self.edit_profile_input.clear() + self.edit_profile_input.send_keys(new_username) + self.save_name_button.click() + + def take_photo(self): + self.take_photo_button.click() + if self.allow_button.is_element_displayed(sec=5): self.allow_button.click() - picture = self.element_by_text(file_name) - if not picture.is_element_displayed(2): - for element_text in 'Images', 'DCIM': - self.element_by_text(element_text).click() - picture.click() - self.confirm_edit_button.click() + if self.allow_all_the_time.is_element_displayed(sec=5): + self.allow_all_the_time.click() + if self.element_by_text("NEXT").is_element_displayed(sec=5): + self.element_by_text("NEXT").click() + self.shutter_button.click() + + def select_photo_from_gallery_by_index(self, image_index: int): + self.allow_button.click_if_shown() + self.allow_all_button.click_if_shown() + image_element = Button(self.driver, class_name="androidx.cardview.widget.CardView") + try: + image_element.find_elements()[image_index].click() + except IndexError: + self.click_system_back_button(times=2) + raise NoSuchElementException("Image with index %s was not found" % image_index) from None + + def change_accent_colour(self, colour_name: str): + self.edit_profile_button.click() + self.edit_accent_colour.click() + Button(self.driver, accessibility_id=colour_name).click() + self.save_colour_button.click() def logout(self): + self.driver.info("Logging out") self.logout_button.click() - return self.logout_dialog.logout_button.click() - - def set_currency(self, desired_currency='Euro (EUR)'): - self.main_currency_button.click() - desired_currency = self.element_by_text(desired_currency) - desired_currency.scroll_to_element() - desired_currency.click() - - def mail_server_by_name(self, server_name): - return MailServerElement(self.driver, server_name) + self.logout_dialog.logout_button.click() + self.logout_button.wait_for_invisibility_of_element(30) + + def get_sync_code(self): + self.syncing_button.scroll_and_click() + self.paired_devices_button.click() + self.sync_plus_button.click() + for checkbox in Button( + self.driver, + xpath="//*[@content-desc='checkbox-off'][@resource-id='checkbox-component']").find_elements(): + checkbox.click() + self.continue_button.click() + self.slide_button_track.swipe_right_on_element(width_percentage=1.3) + password_input = self.password_input.find_element() + password_input.send_keys(common_password) + self.login_button.click() + self.wait_for_staleness_of_element(password_input) + return self.password_input.text + + def get_current_device_name(self): + element = BaseElement( + self.driver, + xpath="//android.view.ViewGroup/android.view.ViewGroup[@content-desc='status-tag-positive']/..") + return element.attribute_value('content-desc').split(',')[1].strip() + + def get_paired_device_by_name(self, device_name: str): + + class PairedDeviceElement(BaseElement): + def __init__(self, driver, device_name): + super().__init__(driver, xpath="//*[@content-desc='icon, %s, label-component']" % device_name) + + @property + def get_pair_button(self): + return Button(self.driver, xpath=self.locator + "//*[@content-desc='Pair']") + + @property + def get_unpair_button(self): + return Button(self.driver, xpath=self.locator + "//*[@content-desc='Unpair']") + + return PairedDeviceElement(self.driver, device_name) + + def get_key_pair_account_by_name(self, account_name: str): + class KeyPairAccountElement(BaseElement): + def __init__(self, driver, account_name): + locator = "//*[@content-desc='account-avatar']/following-sibling::*[@text='%s']" % account_name + super().__init__(driver, xpath=locator) + self.address = Text(driver, xpath=locator + "/following-sibling::android.widget.TextView") + + return KeyPairAccountElement(self.driver, account_name) + + def get_missing_key_pair_by_name(self, key_pair_name: str): + class MissingKeyPairElement(BaseElement): + def __init__(self, driver, key_pair_name): + locator = "//*[@content-desc='missing-keypair-item']//*[@text='%s']" % key_pair_name + super().__init__(driver, xpath=locator) + self.options_button = Button(driver, xpath=locator + "/../..//*[@content-desc='options-button']") + + return MissingKeyPairElement(self.driver, key_pair_name) + + def turn_new_contact_requests_toggle(self, state: str = 'on'): + self.profile_messages_button.click() + element = Button( + self.driver, + xpath="//*[@content-desc='Allow new contact requests, label-component']//*[@resource-id='toggle-component']") + element.click() + if element.attribute_value('content-desc') != 'toggle-%s' % state: + pytest.fail("Allow new contact requests toggle was not turned %s" % state) + self.click_system_back_button() + + def change_currency(self, new_currency: str): + self.language_and_currency_button.scroll_and_click() + self.change_currency_button.click() + self.element_by_text_part(new_currency).click() + time.sleep(1) + self.navigate_back_to_main_profile_view(2) diff --git a/test/appium/views/recover_access_view.py b/test/appium/views/recover_access_view.py deleted file mode 100644 index 99baeb1f37d..00000000000 --- a/test/appium/views/recover_access_view.py +++ /dev/null @@ -1,26 +0,0 @@ -from views.base_element import BaseEditBox, BaseButton -from views.sign_in_view import SignInView - - -class PassphraseInput(BaseEditBox): - - def __init__(self, driver): - super(PassphraseInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.EditText[contains(@text,'phrase')]") - - -class ConfirmRecoverAccess(BaseButton): - - def __init__(self, driver): - super(ConfirmRecoverAccess, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='RECOVER ACCESS']") - - -class RecoverAccessView(SignInView): - - def __init__(self, driver): - super(RecoverAccessView, self).__init__(driver) - self.driver = driver - - self.passphrase_input = PassphraseInput(self.driver) - self.confirm_recover_access = ConfirmRecoverAccess(self.driver) diff --git a/test/appium/views/send_transaction_view.py b/test/appium/views/send_transaction_view.py deleted file mode 100644 index c9b2d6ddbc6..00000000000 --- a/test/appium/views/send_transaction_view.py +++ /dev/null @@ -1,185 +0,0 @@ -from views.base_element import BaseText -from views.base_element import BaseButton, BaseEditBox -from views.base_view import BaseView, OkButton, ProgressBar - - -class FirstRecipient(BaseButton): - def __init__(self, driver): - super(FirstRecipient, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-icon') - - -class CancelButton(BaseButton): - def __init__(self, driver): - super(CancelButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('cancel-button') - - -class SignTransactionButton(BaseButton): - def __init__(self, driver): - super(SignTransactionButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('sign-transaction-button') - - -class AmountEditBox(BaseEditBox, BaseButton): - - def __init__(self, driver): - super(AmountEditBox, self).__init__(driver) - self.locator = self.Locator.accessibility_id('amount-input') - - -class SignInPhraseText(BaseText): - def __init__(self, driver): - super(SignInPhraseText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('signing-phrase-text') - - -class PasswordInput(BaseEditBox): - def __init__(self, driver): - super(PasswordInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Password']") - - -class EnterPasswordInput(BaseEditBox): - def __init__(self, driver): - super(EnterPasswordInput, self).__init__(driver) - self.locator = self.Locator.accessibility_id('enter-password-input') - - -class GotItButton(BaseButton): - def __init__(self, driver): - super(GotItButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('got-it-button') - - -class ChooseRecipientButton(BaseButton): - - def __init__(self, driver): - super(ChooseRecipientButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('choose-recipient-button') - - -class EnterRecipientAddressButton(BaseButton): - def __init__(self, driver): - super(EnterRecipientAddressButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Enter recipient address']") - - -class ScanQRCodeButton(BaseButton): - def __init__(self, driver): - super(ScanQRCodeButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Scan QR code') - - -class EnterRecipientAddressInput(BaseEditBox): - def __init__(self, driver): - super(EnterRecipientAddressInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Enter recipient address']") - - -class RecentRecipientsButton(BaseButton): - def __init__(self, driver): - super(RecentRecipientsButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Contacts']") - - -class SelectAssetButton(BaseButton): - def __init__(self, driver): - super(SelectAssetButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('choose-asset-button') - - -class AssetButton(BaseButton): - def __init__(self, driver, asset_name): - super(AssetButton, self).__init__(driver) - self.locator = self.Locator.text_part_selector(asset_name) - - -class ErrorDialog(BaseView): - def __init__(self, driver): - super(ErrorDialog, self).__init__(driver) - self.ok_button = OkButton(driver) - - def wait_for_error_message(self, error_message, wait_time=30): - element = self.element_by_text_part(error_message) - return element.wait_for_element(wait_time) - - -class AdvancedButton(BaseButton): - def __init__(self, driver): - super(AdvancedButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('advanced-button') - - -class TransactionFeeButton(BaseButton): - def __init__(self, driver): - super(TransactionFeeButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('transaction-fee-button') - - -class GasLimitInput(BaseEditBox): - def __init__(self, driver): - super(GasLimitInput, self).__init__(driver) - self.locator = self.Locator.accessibility_id('gas-limit-input') - - -class GasPriceInput(BaseEditBox): - def __init__(self, driver): - super(GasPriceInput, self).__init__(driver) - self.locator = self.Locator.accessibility_id('gas-price-input') - - -class TotalFeeInput(BaseEditBox): - def __init__(self, driver): - super(TotalFeeInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@content-desc='total-fee-input']/android.widget.TextView") - - -class ShareButton(BaseButton): - - def __init__(self, driver): - super(ShareButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('share-button') - - -class SendTransactionView(BaseView): - def __init__(self, driver): - super(SendTransactionView, self).__init__(driver) - - self.chose_recipient_button = ChooseRecipientButton(self.driver) - self.enter_recipient_address_button = EnterRecipientAddressButton(self.driver) - self.scan_qr_code_button = ScanQRCodeButton(self.driver) - self.enter_recipient_address_input = EnterRecipientAddressInput(self.driver) - self.first_recipient_button = FirstRecipient(self.driver) - self.recent_recipients_button = RecentRecipientsButton(self.driver) - self.amount_edit_box = AmountEditBox(self.driver) - - self.advanced_button = AdvancedButton(self.driver) - self.transaction_fee_button = TransactionFeeButton(self.driver) - self.gas_limit_input = GasLimitInput(self.driver) - self.gas_price_input = GasPriceInput(self.driver) - self.total_fee_input = TotalFeeInput(self.driver) - - self.cancel_button = CancelButton(self.driver) - self.sign_transaction_button = SignTransactionButton(self.driver) - self.sign_in_phrase_text = SignInPhraseText(self.driver) - self.password_input = PasswordInput(self.driver) - self.enter_password_input = EnterPasswordInput(self.driver) - self.got_it_button = GotItButton(self.driver) - - self.select_asset_button = SelectAssetButton(self.driver) - - self.error_dialog = ErrorDialog(self.driver) - - self.share_button = ShareButton(self.driver) - self.progress_bar = ProgressBar(self.driver) - - def sign_transaction(self, sender_password): - self.sign_transaction_button.click_until_presence_of_element(self.enter_password_input) - self.enter_password_input.send_keys(sender_password) - self.sign_transaction_button.click_until_presence_of_element(self.progress_bar) - self.progress_bar.wait_for_invisibility_of_element(60) - self.got_it_button.click() - - def asset_by_name(self, asset_name): - return AssetButton(self.driver, asset_name) diff --git a/test/appium/views/sign_in_view.py b/test/appium/views/sign_in_view.py index d6d661c4c16..0df32ddcbd5 100644 --- a/test/appium/views/sign_in_view.py +++ b/test/appium/views/sign_in_view.py @@ -1,150 +1,153 @@ from selenium.common.exceptions import NoSuchElementException -from tests import get_current_time, common_password -from views.base_element import BaseButton, BaseEditBox +from tests import common_password +from views.base_element import Button, EditBox, Text, BaseElement from views.base_view import BaseView -class AccountButton(BaseButton): - - def __init__(self, driver): - super(AccountButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[contains(@text,'0x')]") - - -class PasswordInput(BaseEditBox): - - def __init__(self, driver): - super(PasswordInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Password']" - "/following-sibling::android.view.ViewGroup/android.widget.EditText") - - -class SignInButton(BaseButton): - +class LogInButton(Button): def __init__(self, driver): - super(SignInButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Sign in' or @text='SIGN IN']") + super().__init__(driver, accessibility_id="login-button") def navigate(self): from views.home_view import HomeView return HomeView(self.driver) -class RecoverAccessButton(BaseButton): - - def __init__(self, driver): - super(RecoverAccessButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Recover access']") - - def navigate(self): - from views.recover_access_view import RecoverAccessView - return RecoverAccessView(self.driver) - - -class CreateAccountButton(BaseButton): - def __init__(self, driver): - super(CreateAccountButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//android.widget.TextView[@text='CREATE ACCOUNT' or @text='CREATE NEW ACCOUNT']") - - -class IHaveAccountButton(RecoverAccessButton): - def __init__(self, driver): - super(IHaveAccountButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='I ALREADY HAVE AN ACCOUNT']") - - -class AddExistingAccountButton(RecoverAccessButton): - def __init__(self, driver): - super(AddExistingAccountButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='ADD EXISTING ACCOUNT']") - - -class ConfirmPasswordInput(BaseEditBox): - def __init__(self, driver): - super(ConfirmPasswordInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Confirm']") - - -class NameInput(BaseEditBox): - def __init__(self, driver): - super(NameInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.EditText") +class UserProfileElement(Button): + def __init__(self, driver, username: str = None, index: int = None): + self.username = username + self.index = index + if username: + self.locator = "//*[@text='%s']//ancestor::android.view.ViewGroup[@content-desc='profile-card']" % self.username + elif index: + self.locator = "(//*[@content-desc='profile-card'])[%s]" % self.index + else: + raise AttributeError("Either username or profile index should be defined") + super().__init__(driver, xpath=self.locator) + def open_user_options(self): + Button(self.driver, xpath='%s//*[@content-desc="profile-card-options"]' % self.locator).click() -class OtherAccountsButton(BaseButton): - - def __init__(self, driver): - super(OtherAccountsButton, self).__init__(driver) - self.locator = self.Locator.text_selector('OTHER ACCOUNTS') + @property + def profile_image(self): + return BaseElement(self.driver, xpath=self.locator + "//*[@content-desc='profile-picture']") class SignInView(BaseView): def __init__(self, driver): - super(SignInView, self).__init__(driver) + super().__init__(driver) self.driver = driver - self.account_button = AccountButton(self.driver) - self.password_input = PasswordInput(self.driver) - self.sign_in_button = SignInButton(self.driver) - self.recover_access_button = RecoverAccessButton(self.driver) - - # new design - self.create_account_button = CreateAccountButton(self.driver) - self.i_have_account_button = IHaveAccountButton(self.driver) - self.add_existing_account_button = AddExistingAccountButton(self.driver) - self.confirm_password_input = ConfirmPasswordInput(self.driver) - self.name_input = NameInput(self.driver) - self.other_accounts_button = OtherAccountsButton(self.driver) - - def create_user(self, username: str = '', password=common_password): - self.create_account_button.click() - self.password_input.set_value(password) - self.next_button.click() - self.confirm_password_input.set_value(password) - self.next_button.click() - - self.element_by_text_part('Display name').wait_for_element(30) - username = username if username else 'user_%s' % get_current_time() - self.name_input.set_value(username) - self.confirm() - - self.next_button.click() - return self.get_home_view() - - def recover_access(self, passphrase, password): - recover_access_view = self.i_have_account_button.click() - recover_access_view.passphrase_input.click() - recover_access_view.send_as_keyevent(passphrase) - recover_access_view.password_input.click() - recover_access_view.send_as_keyevent(password) - recover_access_view.sign_in_button.click() + # intro screen + self.explore_new_status_button = Button(self.driver, accessibility_id="explore-new-status") + self.create_profile_button = Button(self.driver, accessibility_id='new-to-status-button') + self.log_in_button = Button(self.driver, accessibility_id='log-in') + self.maybe_later_button = Button(self.driver, accessibility_id="maybe-later-button") + self.log_in_by_syncing_button = Button(self.driver, accessibility_id="log-in-by-syncing-icon-card") + self.enter_sync_code_button = Button(self.driver, accessibility_id="Enter sync code") + self.enter_sync_code_input = EditBox(self.driver, accessibility_id="enter-sync-code-input") + self.progress_screen_title = Text(self.driver, accessibility_id="progress-screen-title") + self.try_seed_phrase_button = Button(self.driver, accessibility_id="try-seed-phrase-button") + + # migrate multiaccount + self.options_button = Button(self.driver, xpath="//androidx.appcompat.widget.LinearLayoutCompat") + + # New onboarding + self.start_fresh_lets_go_button = Button(self.driver, accessibility_id="start-fresh-main-card") + self.profile_title_input = EditBox(self.driver, accessibility_id="profile-title-input") + self.profile_confirm_password_button = Button(self.driver, accessibility_id="Confirm password") + self.use_recovery_phrase_button = Button(self.driver, accessibility_id="use-a-recovery-phrase-icon-card") + self.passphrase_edit_box = EditBox(self.driver, accessibility_id="passphrase-input") + self.show_profiles_button = Button(self.driver, accessibility_id="show-profiles") + self.plus_profiles_button = Button(self.driver, accessibility_id="show-new-account-options") + self.create_new_profile_button = Button(self.driver, accessibility_id="create-new-profile") + self.sync_or_recover_new_profile_button = Button(self.driver, accessibility_id="multi-profile") + self.remove_profile_button = Button(self.driver, accessibility_id="remove-profile") + + def set_password(self, password: str): + input_elements = self.password_input.wait_for_elements() + input_elements[0].send_keys(password) + input_elements[1].click() + input_elements[1].send_keys(password) + self.profile_confirm_password_button.click() + + def create_user(self, password=common_password, first_user=True, enable_notifications=False): + self.driver.info("## Creating new multiaccount with password:'%s'" % password, device=False) + if first_user: + self.create_profile_button.click() + self.maybe_later_button.wait_and_click() + else: + if self.show_profiles_button.is_element_displayed(20): + self.show_profiles_button.click() + self.plus_profiles_button.click() + self.create_new_profile_button.click() + self.start_fresh_lets_go_button.click() + self.set_password(password) + self.chats_tab.wait_for_visibility_of_element(30) + self.driver.info("## New multiaccount is created successfully!", device=False) + home_view = self.get_home_view() + if enable_notifications: + profile_view = home_view.profile_button.click() + profile_view.switch_push_notifications() + profile_view.click_system_back_button(times=2) + return home_view + + def recover_access(self, passphrase: str, password: str = common_password, enable_notifications=False, + second_user=False, after_sync_code=False): + self.driver.info("## Recover access (password:%s)" % password, device=False) + + if not after_sync_code: + if not second_user: + self.create_profile_button.click() + self.maybe_later_button.wait_and_click() + else: + self.show_profiles_button.click_if_shown() + self.plus_profiles_button.click() + self.create_new_profile_button.click() + self.use_recovery_phrase_button.click() + self.passphrase_edit_box.send_keys(passphrase) + self.continue_button.click() + self.set_password(password) + self.chats_tab.wait_for_visibility_of_element(30) + self.driver.info("## Multiaccount is recovered successfully!", device=False) + home_view = self.get_home_view() + if enable_notifications: + profile_view = home_view.profile_button.click() + profile_view.switch_push_notifications() + return home_view + + def sync_profile(self, sync_code: str, first_user: bool = True): + if first_user: + self.log_in_button.click() + self.maybe_later_button.click() + else: + self.show_profiles_button.click_if_shown() + self.plus_profiles_button.click() + self.sync_or_recover_new_profile_button.click() + self.log_in_by_syncing_button.click() + for checkbox in self.checkbox_button.find_elements(): + checkbox.click() + self.continue_button.click() + self.enter_sync_code_button.click() + self.enter_sync_code_input.send_keys(sync_code) + self.confirm_button.click() + + def sign_in(self, user_name, password=common_password): + self.driver.info("## Sign in (password: %s)" % password, device=False) + self.show_profiles_button.click_if_shown() + self.get_user_profile_by_name(user_name).click() + self.password_input.wait_for_visibility_of_element(10) + self.password_input.send_keys(password) + self.login_button.click() + self.driver.info("## Signed in successfully!", device=False) return self.get_home_view() - def open_status_test_dapp(self): - profile_view = self.profile_button.click() - profile_view.advanced_button.click() - profile_view.debug_mode_toggle.click() - home_view = profile_view.home_button.click() - start_new_chat_view = home_view.plus_button.click() - start_new_chat_view.open_d_app_button.click() - start_new_chat_view.status_test_dapp_button.scroll_to_element() - status_test_daap = start_new_chat_view.status_test_dapp_button.click() - start_new_chat_view.open_button.click() - return status_test_daap - - def sign_in(self, password=common_password): - if self.ok_button.is_element_displayed(): - self.ok_button.click() - self.password_input.set_value(password) - return self.sign_in_button.click() - - def click_account_by_position(self, position: int): - if self.ok_button.is_element_displayed(): - self.ok_button.click() - try: - self.account_button.find_elements()[position].click() - except IndexError: - raise NoSuchElementException('Unable to find account by position %s' % position) from None + def get_user_profile_by_name(self, username: str): + self.driver.info("Getting username card by '%s'" % username) + expected_element = UserProfileElement(self.driver, username=username) + if expected_element.is_element_displayed(10): + return expected_element + else: + raise NoSuchElementException(msg="User %s is not found!" % username) diff --git a/test/appium/views/start_new_chat_view.py b/test/appium/views/start_new_chat_view.py deleted file mode 100644 index d0e4f0ebefc..00000000000 --- a/test/appium/views/start_new_chat_view.py +++ /dev/null @@ -1,84 +0,0 @@ -from views.base_element import BaseButton, BaseEditBox -from views.contacts_view import ContactsView - - -class StartNewChatButton(BaseButton): - def __init__(self, driver): - super(StartNewChatButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('start-1-1-chat-button') - - -class NewGroupChatButton(BaseButton): - - def __init__(self, driver): - super(NewGroupChatButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('start-group-chat-button') - - -class JoinPublicChatButton(BaseButton): - - def __init__(self, driver): - super(JoinPublicChatButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('join-public-chat-button') - - -class ChatNameEditBox(BaseEditBox): - def __init__(self, driver): - super(ChatNameEditBox, self).__init__(driver) - self.locator = self.Locator.accessibility_id('chat-name-input') - - -class OpenDAppButton(BaseButton): - def __init__(self, driver): - super(OpenDAppButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('open-dapp-button') - - -class OpenButton(BaseButton): - def __init__(self, driver): - super(OpenButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('open-dapp-button') - - -class CreateButton(BaseButton): - def __init__(self, driver): - super(CreateButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('create-button') - - -class EnterUrlEditbox(BaseEditBox): - def __init__(self, driver): - super(EnterUrlEditbox, self).__init__(driver) - self.locator = self.Locator.accessibility_id('dapp-url-input') - - -class UsernameCheckbox(BaseButton): - def __init__(self, driver, username): - super(UsernameCheckbox, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='%s']/../../android.widget.CheckBox" % username) - - -class InviteFriendsButton(BaseButton): - def __init__(self, driver): - super(InviteFriendsButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('invite-friends-button') - - -class StartNewChatView(ContactsView): - def __init__(self, driver): - super(StartNewChatView, self).__init__(driver) - - self.start_new_chat_button = StartNewChatButton(self.driver) - self.new_group_chat_button = NewGroupChatButton(self.driver) - self.join_public_chat_button = JoinPublicChatButton(self.driver) - - self.open_d_app_button = OpenDAppButton(self.driver) - self.open_button = OpenButton(self.driver) - self.invite_friends_button = InviteFriendsButton(self.driver) - - self.chat_name_editbox = ChatNameEditBox(self.driver) - self.enter_url_editbox = EnterUrlEditbox(self.driver) - self.create_button = CreateButton(self.driver) - - def get_username_checkbox(self, username: str): - return UsernameCheckbox(self.driver, username) diff --git a/test/appium/views/transactions_view.py b/test/appium/views/transactions_view.py deleted file mode 100644 index 8e3318bb457..00000000000 --- a/test/appium/views/transactions_view.py +++ /dev/null @@ -1,128 +0,0 @@ -import time - -import pytest -from selenium.common.exceptions import NoSuchElementException -from views.base_element import BaseElement, BaseButton, BaseText -from views.base_view import BaseView - - -class OptionsButton(BaseButton): - def __init__(self, driver): - super(OptionsButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - '(//android.view.ViewGroup[@content-desc="icon"])[2]') - - class CopyTransactionHashButton(BaseButton): - def __init__(self, driver): - super(OptionsButton.CopyTransactionHashButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Copy transaction ID') - - class OpenOnEtherscanButton(BaseButton): - def __init__(self, driver): - super(OptionsButton.OpenOnEtherscanButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Open on Etherscan.io') - - -class TransactionTable(BaseElement): - def __init__(self, driver): - super(TransactionTable, self).__init__(driver) - self.driver = driver - self.locator = self.Locator.xpath_selector("//android.support.v4.view.ViewPager") - - class TransactionElement(BaseButton): - def __init__(self, driver): - super(TransactionTable.TransactionElement, self).__init__(driver) - - @staticmethod - def by_amount(driver, amount: str): - element = TransactionTable.TransactionElement(driver) - element.locator = element.Locator.xpath_selector( - "(//android.widget.TextView[contains(@text,'%s ETH')])" % amount) - return element - - @staticmethod - def by_index(driver, index: int): - element = TransactionTable.TransactionElement(driver) - element.locator = element.Locator.xpath_selector( - '(//android.view.ViewGroup[@content-desc="transaction-item"])[%d]' % (index + 1)) - return element - - class TransactionDetailsView(BaseView): - def __init__(self, driver): - super(TransactionTable.TransactionElement.TransactionDetailsView, self).__init__(driver) - self.driver = driver - self.locators = dict() - self.options_button = OptionsButton(driver) - self.copy_transaction_hash_button = OptionsButton.CopyTransactionHashButton(driver) - self.open_transaction_on_etherscan_button = OptionsButton.OpenOnEtherscanButton(driver) - - self.locators['transaction_hash'] = "//android.widget.TextView[@text='Hash']/following-sibling::*[1]" - self.locators['sender_address'] = "//*[@content-desc='sender-address-text']" - self.locators['recipient_address'] = "//*[@content-desc='recipient-address-text']" - - class DetailsTextElement(BaseText): - def __init__(self, driver, locator): - super(TransactionTable.TransactionElement.TransactionDetailsView.DetailsTextElement, - self).__init__(driver) - self.locator = self.Locator.xpath_selector(locator) - - def get_transaction_hash(self) -> str: - return self.DetailsTextElement(driver=self.driver, locator=self.locators['transaction_hash']).text - - def get_sender_address(self) -> str: - return self.DetailsTextElement(driver=self.driver, locator=self.locators['sender_address']).text - - def get_recipient_address(self) -> str: - return self.DetailsTextElement(driver=self.driver, locator=self.locators['recipient_address']).text - - def navigate(self): - return self.TransactionDetailsView(self.driver) - - def transaction_by_index(self, index: int): - return self.TransactionElement.by_index(self.driver, index=index) - - def transaction_by_amount(self, amount: str): - return self.TransactionElement.by_amount(self.driver, amount=amount.replace(',', '.')) - - def find_transaction(self, amount: str) -> TransactionElement: - element = self.transaction_by_amount(amount=amount) - for i in range(9): - try: - element.find_element() - return element - except NoSuchElementException: - time.sleep(5) - self.refresh_transactions() - pytest.fail('Transaction was not found on Wallet/Transaction screen') - - def refresh_transactions(self): - self.driver.swipe(500, 500, 500, 1000) - - def get_transactions_number(self): - element = self.TransactionElement(self.driver) - element.locator = element.Locator.xpath_selector('//android.view.ViewGroup[@content-desc="transaction-item"]') - return len(element.find_elements()) - - -class FiltersButton(BaseButton): - def __init__(self, driver): - super(FiltersButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('filters-button') - - -class FilterCheckbox(BaseButton): - def __init__(self, driver, filter_name): - super(FilterCheckbox, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[@text='%s']/following-sibling::*[@content-desc='checkbox']" % filter_name) - - -class TransactionsView(BaseView): - def __init__(self, driver): - super(TransactionsView, self).__init__(driver) - self.driver = driver - self.filters_button = FiltersButton(self.driver) - self.transactions_table = TransactionTable(self.driver) - - def filter_checkbox(self, filter_name): - return FilterCheckbox(self.driver, filter_name) diff --git a/test/appium/views/wallet_view.py b/test/appium/views/wallet_view.py index 43f91abbee6..79fce225e9d 100644 --- a/test/appium/views/wallet_view.py +++ b/test/appium/views/wallet_view.py @@ -1,267 +1,611 @@ -from tests import info +import datetime import time -from views.base_view import BaseView -from views.base_element import BaseButton, BaseText - - -class SendTransactionButton(BaseButton): - - def __init__(self, driver): - super(SendTransactionButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('send-transaction-button') - - def navigate(self): - from views.send_transaction_view import SendTransactionView - return SendTransactionView(self.driver) - - -class ReceiveTransactionButton(BaseButton): - - def __init__(self, driver): - super(ReceiveTransactionButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('receive-transaction-button') - - def navigate(self): - from views.send_transaction_view import SendTransactionView - return SendTransactionView(self.driver) - - -class SendRequestButton(BaseButton): - - def __init__(self, driver): - super(SendRequestButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('sent-request-button') - - -class ChooseRecipientButton(BaseButton): - - def __init__(self, driver): - super(ChooseRecipientButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('choose-recipient-button') - - -class TransactionHistoryButton(BaseButton): - - def __init__(self, driver): - super(TransactionHistoryButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('transaction-history-button') - - def navigate(self): - from views.transactions_view import TransactionsView - return TransactionsView(self.driver) - - -class ChooseFromContactsButton(BaseButton): - def __init__(self, driver): - super(ChooseFromContactsButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Choose From Contacts']") - - -class EthAssetText(BaseText): - def __init__(self, driver): - super(EthAssetText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('eth-asset-value-text') - - -class STTAssetText(BaseText): - def __init__(self, driver): - super(STTAssetText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('stt-asset-value-text') - - -class UsdTotalValueText(BaseText): - def __init__(self, driver): - super(UsdTotalValueText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('total-amount-value-text') - - -class SendTransactionRequestButton(BaseButton): - def __init__(self, driver): - super(SendTransactionRequestButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('sent-transaction-request-button') - - -class OptionsButton(BaseButton): - def __init__(self, driver): - super(OptionsButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('options-menu-button') +from typing import Literal -class ManageAssetsButton(BaseButton): - def __init__(self, driver): - super(ManageAssetsButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='Manage Assets']") - - -class STTCheckBox(BaseButton): - def __init__(self, driver): - super(STTCheckBox, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='STT']/../android.widget.CheckBox") - +import pytest +from selenium.common import NoSuchElementException -class DoneButton(BaseButton): - def __init__(self, driver): - super(DoneButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('done-button') - - -class QRCodeImage(BaseButton): - def __init__(self, driver): - super(QRCodeImage, self).__init__(driver) - self.locator = self.Locator.accessibility_id('qr-code-image') +from tests import common_password +from views.base_element import Button, EditBox, Text, BaseElement +from views.base_view import BaseView +from views.chat_view import ChatView +from views.home_view import HomeView +from views.sign_in_view import SignInView -class AddressText(BaseButton): - def __init__(self, driver): - super(AddressText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('address-text') - +class AssetElement(Button): + def __init__(self, driver, asset_name): + self.asset_name = asset_name + self.locator = ( + "//android.view.ViewGroup[@content-desc='container']/android.widget.TextView[@text='%s']" + % self.asset_name + ) + super().__init__(driver=driver, xpath=self.locator) + + def _parse_amount(self, element) -> float: + """ + Helper method to parse and return the numeric amount from a given element. + Handles cases where the amount starts with a currency symbol or '<'. + """ + try: + # Extract the first part of the text + amount = element.text.split()[0] + + # Handle cases where the amount starts with '<' + if '<' in amount: + return 0.01 if '0.01' in amount else 0.0 # Covers cases like '<$0.01' + + # Strip any leading non-numeric characters (e.g., currency symbols) + numeric_part = ''.join(c for c in amount if c.isdigit() or c == '.') + + # Convert the remaining string to float + return float(numeric_part) + except (ValueError, IndexError): + # Fail the test if parsing fails + pytest.fail(f"Cannot parse amount for {self.asset_name}") + return 0.0 # Optional fallback + + def get_amount(self): + """ + Get the crypto amount for the asset. + """ + element = Text(self.driver, xpath=self.locator + "/../android.widget.TextView[3]") + element.scroll_to_element() + return self._parse_amount(element) + + def get_fiat_balance(self): + """ + Get the fiat balance for the asset. + """ + element = Text(self.driver, xpath=self.locator + "/../android.widget.TextView[2]") + element.scroll_to_element() + amount = element.text.split()[1:] + return self._parse_amount(element) + + +class CollectibleItemElement(Button): + + def __init__(self, driver, collectible_name): + self.collectible_name = collectible_name + self.locator = "//*[@content-desc='collectible-list-item']//*[contains(@text,'%s')]/../.." % collectible_name + super().__init__(driver=driver, xpath=self.locator) + self.image_element = BaseElement(self.driver, xpath=self.locator + "//android.widget.ImageView") -class SetUpButton(BaseButton): - def __init__(self, driver): - super(SetUpButton, self).__init__(driver) - self.locator = self.Locator.text_selector("LET’S GET SET UP") + @property + def quantity(self): + counter_element = BaseElement( + self.driver, xpath=self.locator + "//*[@content-desc='collectible-counter']/android.widget.TextView") + try: + return int(counter_element.text.strip('x')) + except NoSuchElementException: + return 1 -class SignInPhraseText(BaseText): - def __init__(self, driver): - super(SignInPhraseText, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[contains(@text,'phrase')]/preceding-sibling::*[1]/android.widget.TextView") +class ActivityElement(BaseElement): + def __init__(self, driver, index: int): + self.locator = "(//*[@content-desc='wallet-activity'])[%s]" % index + super().__init__(driver=driver, xpath=self.locator) @property - def list(self): - return [element.text for element in self.find_elements()] + def header(self): + return Text(self.driver, prefix=self.locator, xpath="//*[@content-desc='transaction-header']").text @property - def string(self): - return ' '.join(self.list) - - -class AssetTextElement(BaseText): - def __init__(self, driver, asset_name): - super(AssetTextElement, self).__init__(driver) - self.locator = self.Locator.accessibility_id('%s-asset-value-text' % asset_name.lower()) - + def timestamp(self): + return Text(self.driver, prefix=self.locator, xpath="//*[@content-desc='transaction-timestamp']").text -class AssetCheckBox(BaseButton): - def __init__(self, driver, asset_name): - super(AssetCheckBox, self).__init__(driver) - self.asset_name = asset_name - self.locator = self.Locator.xpath_selector("//*[@text='%s']/../android.widget.CheckBox" % self.asset_name) + @property + def options(self): + return Button(self.driver, prefix=self.locator, xpath="//*[@content-desc='transaction-timestamp']/following::*[@content-desc='icon'][1]") - def click(self): - self.scroll_to_element().click() - info('Click %s asset checkbox' % self.asset_name) + @property + def amount(self): + return Text(self.driver, prefix=self.locator, + xpath="//*[@content-desc='context-tag'][1]/android.widget.TextView").text + @property + def send_account_from_text(self): + return Text(self.driver, prefix=self.locator, + xpath="//*[@content-desc='context-tag'][2]/android.widget.TextView").text -class TotalAmountText(BaseText): + @property + def swap_amount_to_text(self): + return Text(self.driver, prefix=self.locator, + xpath="//*[@content-desc='context-tag'][2]/android.widget.TextView").text - def __init__(self, driver): - super(TotalAmountText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('total-amount-value-text') + @property + def account_to_text(self): + return Text(self.driver, prefix=self.locator, + xpath="//*[@content-desc='context-tag'][3]/android.widget.TextView").text + @property + def on_network(self): + return Text(self.driver, prefix=self.locator, + xpath="//*[@content-desc='context-tag'][4]/android.widget.TextView").text -class CurrencyText(BaseText): - def __init__(self, driver): - super(CurrencyText, self).__init__(driver) - self.locator = self.Locator.accessibility_id('total-amount-currency-text') +class ConfirmationViewInfoContainer(BaseElement): + def __init__(self, driver, label_name: str): + self.locator = "//*[@text='%s']/following-sibling::android.view.ViewGroup[1]" % label_name + super().__init__(driver, xpath=self.locator) -class BackupRecoveryPhrase(BaseButton): - def __init__(self, driver): - super(BackupRecoveryPhrase, self).__init__(driver) - self.locator = self.Locator.text_selector('Backup your recovery phrase') - - def navigate(self): - from views.profile_view import ProfileView - return ProfileView(self.driver) + @property + def amount_text(self): + return Text(self.driver, xpath=self.locator + "/*[@content-desc='networks']/android.widget.TextView").text class WalletView(BaseView): def __init__(self, driver): - super(WalletView, self).__init__(driver) - self.driver = driver - - self.send_transaction_button = SendTransactionButton(self.driver) - self.transaction_history_button = TransactionHistoryButton(self.driver) - self.eth_asset_value = EthAssetText(self.driver) - self.stt_asset_value = STTAssetText(self.driver) - self.usd_total_value = UsdTotalValueText(self.driver) - - self.send_transaction_request = SendTransactionRequestButton(self.driver) - self.receive_transaction_button = ReceiveTransactionButton(self.driver) - - self.send_request_button = SendRequestButton(self.driver) - self.options_button = OptionsButton(self.driver) - self.manage_assets_button = ManageAssetsButton(self.driver) - self.stt_check_box = STTCheckBox(self.driver) - self.done_button = DoneButton(self.driver) - - self.qr_code_image = QRCodeImage(self.driver) - self.address_text = AddressText(self.driver) - - self.set_up_button = SetUpButton(self.driver) - self.sign_in_phrase = SignInPhraseText(self.driver) - - self.total_amount_text = TotalAmountText(self.driver) - self.currency_text = CurrencyText(self.driver) - self.backup_recovery_phrase = BackupRecoveryPhrase(self.driver) - - def get_usd_total_value(self): - import re - return float(re.sub('[$,]', '', self.usd_total_value.text)) - - def get_eth_value(self): - return float(self.eth_asset_value.text) - - def get_stt_value(self): - return float(self.stt_asset_value.text) - - def verify_currency_balance(self, expected_rate: int, errors: list): - usd = self.get_usd_total_value() - eth = self.get_eth_value() - expected_usd = round(eth * expected_rate, 2) - percentage_diff = abs((usd - expected_usd) / ((usd + expected_usd) / 2)) * 100 - if percentage_diff > 2: - errors.append('Difference between current (%s) and expected (%s) USD balance > 2%%!!' % (usd, expected_usd)) - else: - info('Current USD balance %s is ok' % usd) - - def wait_balance_changed_on_wallet_screen(self, expected_balance=0.1, wait_time=300): - counter = 0 - while True: - if counter >= wait_time: - info('Balance is not changed during %s seconds!' % wait_time) - return - elif self.get_eth_value() != expected_balance: - counter += 10 - time.sleep(10) - self.swipe_down() - info('Waiting %s seconds for ETH update' % counter) + super().__init__(driver) + # Wallet view + self.total_balance_text = Text( + self.driver, xpath="//*[@content-desc='network-dropdown']/preceding-sibling::android.widget.TextView") + self.network_drop_down = Button(self.driver, accessibility_id='network-dropdown') + self.connected_dapps_button = Button( + self.driver, xpath="//*[@content-desc='network-dropdown']/../following-sibling::*[@content-desc='icon']") + self.collectibles_tab = Button(self.driver, accessibility_id='collectibles-tab') + self.add_account_button = Button(self.driver, accessibility_id='add-account') + + # Account adding + # ToDo: add unique accessibility ids for the next 2 elements: + self.create_account_button = HomeView(self.driver).start_a_new_chat_bottom_sheet_button + self.add_account_to_watch = HomeView(self.driver).add_a_contact_chat_bottom_sheet_button + self.address_to_watch_input = EditBox(self.driver, accessibility_id='add-address-to-watch') + self.account_has_activity_label = Text(self.driver, accessibility_id='account-has-activity') + self.add_account_continue_button = Button(self.driver, accessibility_id='Continue') + self.add_watched_address_button = Button(self.driver, accessibility_id='confirm-button-label') + self.add_account_derivation_path_text = Text( + self.driver, xpath="//*[contains(@content-desc,'icon, Derivation path')]/android.widget.TextView[2]") + + # Account view + self.close_account_button = Button(self.driver, accessibility_id='top-bar') + self.account_name_text = Text( + self.driver, xpath="//*[@content-desc='account-avatar']/../following-sibling::android.widget.TextView[1]") + self.account_emoji_button = Button(self.driver, accessibility_id='account-emoji') + self.send_button = Button(self.driver, accessibility_id='send') + self.swap_button = Button(self.driver, accessibility_id='swap') + self.bridge_button = Button(self.driver, accessibility_id='bridge') + self.send_from_drawer_button = Button( + self.driver, xpath="//*[@content-desc='send']/*[@content-desc='left-icon-for-action']") + self.swap_asset_from_drawer_button = Button(self.driver, + xpath="//*[@content-desc='swap']/*[@content-desc='left-icon-for-action']") + self.copy_address_button = Button(self.driver, accessibility_id='copy-address') + self.share_address_button = Button(self.driver, accessibility_id='share-account') + self.remove_account_button = Button(self.driver, accessibility_id='remove-account') + self.derivation_path_note_checkbox = Button(self.driver, accessibility_id='checkbox-off') + self.account_about_derivation_path_text = Text( + self.driver, xpath="//*[@content-desc='derivation-path-icon']/following-sibling::*[2]") + + self.activity_tab = Button(self.driver, accessibility_id='activity-tab') + self.about_tab = Button(self.driver, accessibility_id='about') + + # Sending transaction + self.address_text_input = EditBox(self.driver, accessibility_id='address-text-input') + self.collectibles_tab_on_select_token_view = Button(self.driver, accessibility_id='Collectibles') + self.amount_input = EditBox(self.driver, xpath="//android.widget.EditText") + self.from_network_text = Text( + self.driver, xpath="(//*[@content-desc='loading']/following-sibling::android.widget.TextView)[1]") + self.done_button = Button(self.driver, accessibility_id='done') + self.amount_input_increase_button = Button(self.driver, accessibility_id='amount-input-inc-button') + + # Review Send and Review Bridge screens + self.from_data_container = ConfirmationViewInfoContainer(self.driver, label_name='From') + self.to_data_container = ConfirmationViewInfoContainer(self.driver, label_name='To') + self.on_data_container = ConfirmationViewInfoContainer(self.driver, label_name='On') + + # Advanced tx settings + self.advanced_tx_button = Button(self.driver, accessibility_id='advanced-button') + self.custom_tx_params_button = Button(self.driver, translation_id='custom') + self.custom_max_base_fee_button = Button(self.driver, translation_id='max-base-fee') + self.custom_max_prio_fee_button = Button(self.driver, translation_id='priority-fee') + self.custom_nonce_button = Button(self.driver, translation_id='nonce') + self.custom_max_gas_amount_button = Button(self.driver, translation_id='max-gas-amount') + + # Swap flow + self.approve_swap_button = Button(self.driver, accessibility_id='Approve') + self.spending_cap_approval_info_container = BaseElement( + self.driver, + xpath="//*[@content-desc='spending-cap-label']/following-sibling::*[@content-desc='approval-info'][1]") + self.account_approval_info_container = BaseElement( + self.driver, + xpath="//*[@content-desc='account-label']/following-sibling::*[@content-desc='approval-info'][1]") + self.token_approval_info_container = BaseElement( + self.driver, + xpath="//*[@content-desc='token-label']/following-sibling::*[@content-desc='approval-info'][1]") + self.spender_contract_approval_info_container = BaseElement( + self.driver, + xpath="//*[@content-desc='spender-contract-label']/following-sibling::*[@content-desc='approval-info'][1]") + + self.max_fees_text = Text(self.driver, + xpath="//*[@text='Max fees']/following-sibling::android.widget.TextView[1]") + self.max_slippage_text = Text(self.driver, xpath="//*[@text='Max slippage']/following-sibling::*[1]") + self.est_time_text = Text(self.driver, xpath="//*[@text='Est. time']/following-sibling::*[1]") + self.swap_receive_amount_summary_text = Text(self.driver, + xpath="//android.widget.TextView[@content-desc='summary-section-receive']/following-sibling::android.view.ViewGroup[1]/android.widget.TextView[1]") + + # Edit key pairs + self.edit_key_pair_button = Button(self.driver, accessibility_id="Edit") + self.key_pairs_plus_button = Button(self.driver, accessibility_id="standard-title-action") + self.default_key_pair_container = BaseElement(self.driver, + xpath="//*[@content-desc='user-avatar, title, details']") + self.added_key_pair_container = BaseElement(self.driver, + xpath="//*[@content-desc='icon, title, details']") + self.generate_new_keypair_button = Button(self.driver, accessibility_id="generate-new-keypair") + self.import_using_recovery_phrase_button = Button(self.driver, accessibility_id="import-using-phrase") + self.key_pair_continue_button = Button(self.driver, accessibility_id="Continue") + self.key_pair_name_input = EditBox( + self.driver, xpath="//*[@text='Key pair name']/..//following-sibling::*/*[@content-desc='input']") + self.passphrase_text_element = Text( + self.driver, xpath="//*[@resource-id='counter-component']/following-sibling::android.widget.TextView") + self.passphrase_word_number_container = Text( + self.driver, xpath="//*[@content-desc='number-container']/android.widget.TextView") + + # Collectible view + self.expanded_collectible_image = BaseElement( + self.driver, xpath="//*[@content-desc='expanded-collectible']//android.widget.ImageView") + self.send_from_collectible_info_button = Button(self.driver, accessibility_id="icon, Send") + + + # Tx activity + self.copy_tx_hash_button = Button(self.driver, accessibility_id="copy-transaction-hash") + + # dApp adding + self.add_dapp_button = Button(self.driver, accessibility_id='connected-dapps-add') + self.wallet_connect_button = Button(self.driver, accessibility_id='wc-connect') + self.wallet_decline_button = Button(self.driver, accessibility_id='wc-deny-connection') + self.select_account_to_connect_dapp_button = Button(self.driver, accessibility_id='icon-right') + self.close_connected_dapps_button = Button(self.driver, accessibility_id='connected-dapps-close') + self.close_sign_message_button = Button(self.driver, accessibility_id='wallet-connect-sign-message-close') + + def set_network_in_wallet(self, network_name: str): + class NetworksCheckboxElement(Button): + def __init__(self, driver, index=0): + locator = "//*[@content-desc='network-item']" + if index: + locator += '[%s]' % index + super().__init__(driver=driver, xpath=locator) + + @property + def name(self) -> str: + return Text(self.driver, xpath=self.locator + "/android.widget.TextView[1]").text + + @property + def is_selected(self) -> bool: + attr = Button(self.driver, + xpath=self.locator + "//*[@resource-id='checkbox-component']").attribute_value( + 'content-desc') + return True if attr.endswith('on') else False + + self.network_drop_down.click() + for i in range(len(NetworksCheckboxElement(self.driver).find_elements())): + network_element = NetworksCheckboxElement(self.driver, index=i + 1) + if network_element.name == network_name: + if not network_element.is_selected: + network_element.click() else: - info('Transaction received, balance updated!') - return - - def get_sign_in_phrase(self): - return ' '.join([element.text for element in self.sign_in_phrase.find_elements()]) - - def set_up_wallet(self): - self.set_up_button.click() - phrase = self.sign_in_phrase.string - self.done_button.click() - self.yes_button.click() - return phrase - - def get_wallet_address(self): - self.receive_transaction_button.click() - address = self.address_text.text - self.back_button.click() - return address + if network_element.is_selected: + network_element.click() + + self.network_drop_down.click() + + def get_account_element(self, account_name: str = 'Account 1'): + return Button(self.driver, xpath="//android.view.ViewGroup[contains(@content-desc,'%s')]" % account_name) + + def get_asset(self, asset_name: str): + element = AssetElement(driver=self.driver, asset_name=asset_name) + element.scroll_to_element(down_start_y=0.89, down_end_y=0.8) + return element + + def get_collectible_element(self, collectible_name: str): + return CollectibleItemElement(driver=self.driver, collectible_name=collectible_name) + + def select_asset(self, asset_name: str): + Button(driver=self.driver, + xpath="//*[@content-desc='token-network']/android.widget.TextView[@text='%s']" % asset_name).click() + + def select_network(self, network_name: str): + Button(driver=self.driver, + xpath="//*[@content-desc='network-list']/*[@text='%s']" % network_name).click() + + def slide_and_confirm_with_password(self): + self.slide_button_track.slide() + self.password_input.send_keys(common_password) + self.login_button.click() + + def confirm_transaction(self): + self.button_one.click() + for _ in range(3): + if self.slide_button_track.is_element_displayed(): + break + time.sleep(5) + self.button_one.click() + self.slide_and_confirm_with_password() + + def set_amount(self, amount: str): + for i in amount: + Button(self.driver, accessibility_id='keyboard-key-%s' % i).click() + + def clear_value_and_set_with_custom_keyboard(self, value: str): + self.clear_amount() + self.set_amount(value) + self.button_one.click() + + def clear_nonce_field(self): + Button(self.driver, xpath='//com.horcrux.svg.SvgView[@content-desc="icon"]').click() + + def clear_amount(self): + """ + Tap on the clear button until the amount input field is empty. + """ + clear_button = Button(self.driver, accessibility_id="icon-label") + edit_box = EditBox(self.driver, xpath="//android.widget.EditText") + while edit_box.text != "" and edit_box.text != "0": + clear_button.click() + + def set_amount_and_address(self, address: str, asset_name: str, amount, network_name=""): + self.send_button.click_until_presence_of_element(self.address_text_input) + self.address_text_input.send_keys(address) + self.continue_button.click() + self.select_asset(asset_name) + if network_name: + self.select_network(network_name) + self.set_amount(str(amount)) + + def send_asset(self, address: str, asset_name: str, amount, network_name: str, account='Account 1'): + self.set_amount_and_address(address, asset_name, amount, network_name) + self.confirm_transaction() + + def send_asset_from_drawer(self, address: str, asset_name: str, amount, network_name: str): + asset_element = self.get_asset(asset_name) + asset_element.long_press_without_release() + self.send_from_drawer_button.double_click() + self.select_network(network_name) + self.address_text_input.send_keys(address) + self.continue_button.click() + self.set_amount(str(amount)) + self.confirm_transaction() + + def swap_asset_from_drawer(self, asset_name: str, amount, network_name: str, decimals_to: int = 4, + asset_to: str = 'SNT'): + asset_element = self.get_asset(asset_name) + asset_element.long_press_without_release() + self.swap_asset_from_drawer_button.double_click() + self.select_network(network_name) + self.set_amount(str(amount)) + self.max_fees_text.wait_for_visibility_of_element(30) + self.button_one.click_until_presence_of_element(self.swap_receive_amount_summary_text) + expected_amount = self.get_receive_swap_amount(decimals_to) + est_time, slippage, fees = self.est_time_text.text, self.max_slippage_text.text, self.max_fees_text.text + self.slide_and_confirm_with_password() + return {'receive_amount': expected_amount, + 'est_time': est_time, + 'est_slippage': slippage, + 'max_fees': fees} + + def copy_tx_hash(self): + self.get_activity_element().options.click_until_presence_of_element(self.copy_tx_hash_button) + self.copy_tx_hash_button.click() + return self.driver.get_clipboard_text() + + def add_regular_account(self, account_name: str): + if not self.add_account_button.is_element_displayed(): + self.get_account_element().swipe_left_on_element() + self.add_account_button.click() + self.create_account_button.click() + derivation_path = self.add_account_derivation_path_text.text + SignInView(self.driver).profile_title_input.send_keys(account_name) + self.slide_and_confirm_with_password() + return derivation_path.replace(' ', '') + + def add_watch_only_account(self, address: str, account_name: str): + self.add_account_button.click() + self.add_account_to_watch.click() + self.address_to_watch_input.send_keys(address) + self.add_account_continue_button.click() + SignInView(self.driver).profile_title_input.send_keys(account_name) + self.add_watched_address_button.click() + + def remove_account(self, account_name: str, watch_only: bool = False): + self.get_account_element(account_name=account_name).click() + self.account_emoji_button.click() + self.remove_account_button.click() + if not watch_only: + self.derivation_path_note_checkbox.click() + self.button_one.click() + + def get_activity_element(self, index=1): + return ActivityElement(self.driver, index=index) + + def generate_new_key_pair(self, account_name: str, key_pair_name: str): + self.key_pairs_plus_button.click() + self.generate_new_keypair_button.click() + for checkbox in self.checkbox_button.find_elements(): + checkbox.click() + self.element_by_translation_id("reveal-phrase").click() + passphrase = [i.text for i in self.passphrase_text_element.find_elements()] + self.element_by_translation_id("i-have-written").click() + for _ in range(4): + element = self.passphrase_word_number_container.find_element() + number = int(element.text) + Button(self.driver, accessibility_id=passphrase[number - 1]).click() + self.wait_for_staleness_of_element(element) + self.checkbox_button.click() + self.element_by_translation_id("done").click() + self.key_pair_name_input.send_keys(key_pair_name) + self.key_pair_continue_button.click() + derivation_path = self.add_account_derivation_path_text.text + SignInView(self.driver).profile_title_input.send_keys(account_name) + self.slide_and_confirm_with_password() + return derivation_path.replace(' ', ''), ' '.join(passphrase) + + def import_key_pair_using_recovery_phrase(self, account_name: str, passphrase: str, key_pair_name: str): + self.add_account_button.click() + self.create_account_button.click() + signin_view = SignInView(self.driver) + signin_view.profile_title_input.send_keys(account_name) + self.edit_key_pair_button.click() + self.key_pairs_plus_button.click() + self.import_using_recovery_phrase_button.click() + signin_view.passphrase_edit_box.send_keys(passphrase) + self.key_pair_continue_button.click() + self.key_pair_name_input.send_keys(key_pair_name) + self.key_pair_continue_button.click() + derivation_path = self.add_account_derivation_path_text.text + self.slide_and_confirm_with_password() + return derivation_path.replace(' ', '') + + def get_account_address(self): + self.account_emoji_button.click_until_presence_of_element(self.copy_address_button) + self.share_address_button.click() + wallet_address = self.sharing_text_native.text + self.click_system_back_button() + return wallet_address + + def get_data_item_element_text(self, data_item_name: str): + element = Text(self.driver, xpath="//*[@text='%s']/following-sibling::android.widget.TextView" % data_item_name) + element.wait_for_element(15) + return element.text + + def wait_for_swap_input_to_be_shown(self): + locator = "//*[@content-desc='swap-input'][2]//*[@content-desc='token-avatar']" \ + "/following-sibling::*//*[starts-with(@text,'0.000')]" + BaseElement(self.driver, xpath=locator).wait_for_visibility_of_element() + + def get_route_element(self, route_name: str): + class RouteElement(BaseElement): + def __init__(self, driver, route_name): + self.locator = "//*[@text='%s']/following-sibling::*[@content-desc='container'][%s]" % ( + route_name.capitalize(), 1 if route_name == 'from' else 2) + super().__init__(driver, xpath=self.locator) + + @property + def amount_text(self): + return Text(self.driver, xpath="(%s//android.widget.TextView)[1]" % self.locator).text + + @property + def network_text(self): + return Text(self.driver, xpath="(%s//android.widget.TextView)[2]" % self.locator).text + + return RouteElement(self.driver, route_name) + + @staticmethod + def round_amount_float(amount, decimals=4): + return round(float(amount), decimals) + + def wait_for_wallet_balance_to_update(self, expected_amount, asset='Ether', decimals=4, fiat=False): + self.just_fyi(f"Checking {asset} balance, expected value: {expected_amount}, fiat: {fiat}") + + start_time = time.time() + + while time.time() - start_time < 120: + self.pull_to_refresh() + if fiat is False: + new_balance = self.round_amount_float(self.get_asset(asset_name=asset).get_amount(), decimals) + else: + new_balance = self.get_asset(asset_name=asset).get_fiat_balance() - def asset_by_name(self, asset_name): - return AssetTextElement(self.driver, asset_name) + # Exit early if the balance is updated + if new_balance == expected_amount: + return - def asset_checkbox_by_name(self, asset_name): - return AssetCheckBox(self.driver, asset_name) + time.sleep(10) # Wait before retrying + + # If the balance is not updated within the timeout, log an error and raise an exception + error_message = f"{asset} balance is {new_balance} but expected {expected_amount}, fiat: {fiat}" + raise TimeoutError(error_message) + + def check_last_transaction_in_activity(self, device_time, amount, + tx_type: Literal['Send', 'Receive', 'Swap'] = 'Send', + asset='ETH', + from_account_name='Account 1', + send_to_account='', + swap_asset_to='SNT', + swap_amount_to='0.000000000000000000', + network='Status Network', + navigate_to_main_screen=True): + errors = list() + current_time = datetime.datetime.strptime(device_time, "%Y-%m-%dT%H:%M:%S%z") + expected_time = "Today %s" % current_time.strftime('%-I:%M %p') + possible_times = [expected_time, + "Today %s" % (current_time + datetime.timedelta(minutes=1)).strftime('%-I:%M %p')] + receiver_variants = [send_to_account.replace(send_to_account[5:-3], '...').lower(), 'receiver eth2'] + self.just_fyi("Checking the last transaction in the activity tab") + + try: + activity_element = self.get_activity_element() + common_checks = { + "header": (activity_element.header, tx_type), + "timestamp": (activity_element.timestamp, possible_times), + "amount": (activity_element.amount, f"{amount} {asset}"), + "on_network": (activity_element.on_network, network), + } + if tx_type == 'Send': + additional_checks = { + "from_text": (activity_element.send_account_from_text, from_account_name), + "to_text": (activity_element.account_to_text, receiver_variants), + } + elif tx_type == 'Swap': + additional_checks = { + "amount_to": (activity_element.swap_amount_to_text, f"{swap_amount_to} {swap_asset_to}"), + "in_account": (activity_element.account_to_text, from_account_name), + } + else: + additional_checks = {} + + checks = {**common_checks, **additional_checks} + + for name, (left, right) in checks.items(): + if isinstance(right, list): + if left not in right: + errors.append( + f"Verification of last transaction failed." + f"The failed check: {name} (Expected: {right} to contain: {left})") + else: + if left != right: + errors.append( + f"Verification of last transaction failed." + f"The failed check: {name} (Expected: {right}, Found: {left})") + + + + except NoSuchElementException: + errors.append("Can't find the last transaction") + finally: + if navigate_to_main_screen: + self.close_account_button.click_until_presence_of_element(self.show_qr_code_button) + return errors + + def get_balance(self, asset='Ether', fiat=False): + self.just_fyi("Getting %s amount of the wallet of the sender before transaction" % asset) + return self.get_asset(asset_name=asset).get_amount() if fiat is False else self.get_asset( + asset_name=asset).get_fiat_balance() + + def get_receive_swap_amount(self, decimals=18): + self.just_fyi("Getting swap Receive amount for on review page") + return self.round_amount_float(self.swap_receive_amount_summary_text.text.split()[0], decimals) + + def get_custom_tx_element(self, text): + return Text(self.driver, xpath="//*[@content-desc[contains(., '%s')]]" % text) + + + def get_connected_dapp_element_by_name(self, dapp_name: str): + class ConnectedDAppElement(BaseElement): + def __init__(self, driver, dapp_name): + self.locator = "//*[contains(@content-desc,'dapp-')][*[@text='%s']]" % dapp_name + super().__init__(driver, xpath=self.locator) + self.url_text = Text(self.driver, xpath=self.locator + "//*[starts-with(@text,'http')]") + self.disconnect_button = Button(self.driver, xpath=self.locator + "//*[@content-desc='icon']") + + def disconnect(self): + self.disconnect_button.click() + ChatView(self.driver).confirm_block_contact_button.click() + + return ConnectedDAppElement(self.driver, dapp_name) + + def select_account_to_connect_dapp(self, account_name: str): + self.select_account_to_connect_dapp_button.click() + Button(self.driver, xpath="//*[@content-desc='container']/*[@text='%s']" % account_name).click() + + \ No newline at end of file diff --git a/test/appium/views/web_views/base_web_view.py b/test/appium/views/web_views/base_web_view.py index 0b9ab5e97fa..5a5afdf71e0 100644 --- a/test/appium/views/web_views/base_web_view.py +++ b/test/appium/views/web_views/base_web_view.py @@ -1,94 +1,27 @@ import time -import pytest -from views.base_element import BaseElement, BaseEditBox, BaseButton +from views.base_element import Button from views.base_view import BaseView -class ProgressBarIcon(BaseElement): - - def __init__(self, driver): - super(ProgressBarIcon, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.ProgressBar") - - -class WebLinkEditBox(BaseEditBox): - - def __init__(self, driver): - super(WebLinkEditBox, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.EditText") - - -class BackToHomeButton(BaseButton): - def __init__(self, driver): - super(BackToHomeButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector('(//android.view.ViewGroup[@content-desc="icon"])[1]') - - -class BrowserPreviousPageButton(BaseButton): - def __init__(self, driver): - super(BrowserPreviousPageButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('previou-page-button') - - -class BrowserNextPageButton(BaseButton): - def __init__(self, driver): - super(BrowserNextPageButton, self).__init__(driver) - self.locator = self.Locator.accessibility_id('next-page-button') - - -class BrowserRefreshPageButton(BaseButton): - def __init__(self, driver): - super(BrowserRefreshPageButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[@content-desc='next-page-button']/following-sibling::*/*[@content-desc='icon']") - - -class WebViewBrowserButton(BaseButton): - def __init__(self, driver): - super(WebViewBrowserButton, self).__init__(driver) - self.locator = self.Locator.text_part_selector('WebView Browser Tester') - - -class AlwaysButton(BaseButton): - def __init__(self, driver): - super(AlwaysButton, self).__init__(driver) - self.locator = self.Locator.text_part_selector('ALWAYS') - - -class URLEditBoxLockIcon(BaseEditBox): - - def __init__(self, driver): - super(URLEditBoxLockIcon, self).__init__(driver) - self.locator = self.Locator.xpath_selector( - "//*[@content-desc='wallet-modal-button']/../preceding-sibling::*[1]//*[@content-desc='icon']") - - class BaseWebView(BaseView): def __init__(self, driver): - super(BaseWebView, self).__init__(driver) - self.driver = driver + super().__init__(driver) - self.progress_bar_icon = ProgressBarIcon(self.driver) + self.progress_bar_icon = Button(self.driver, xpath="//android.widget.ProgressBar") + self.options_button = Button(self.driver, accessibility_id="browser-options") + self.open_tabs_button = Button(self.driver, accessibility_id="browser-open-tabs") + self.open_new_tab_plus_button = Button(self.driver, accessibility_id="plus-button") - self.url_edit_box_lock_icon = URLEditBoxLockIcon(self.driver) - self.back_to_home_button = BackToHomeButton(self.driver) - self.browser_previous_page_button = BrowserPreviousPageButton(self.driver) - self.browser_next_page_button = BrowserNextPageButton(self.driver) - - self.web_view_browser = WebViewBrowserButton(self.driver) - self.always_button = AlwaysButton(self.driver) - self.browser_refresh_page_button = BrowserRefreshPageButton(self.driver) def wait_for_d_aap_to_load(self, wait_time=35): + self.driver.info("Waiting %ss for dapp to load" % wait_time) counter = 0 - while self.progress_bar_icon.is_element_present(5): + while self.progress_bar_icon.is_element_displayed(5): time.sleep(1) counter += 1 if counter > wait_time: - pytest.fail("Page is not loaded during %s seconds" % wait_time) + self.driver.fail("Page is not loaded during %s seconds" % wait_time) + - def open_in_webview(self): - self.web_view_browser.click() - self.always_button.click() diff --git a/test/appium/views/web_views/external_browser_view.py b/test/appium/views/web_views/external_browser_view.py new file mode 100644 index 00000000000..4d7a877c266 --- /dev/null +++ b/test/appium/views/web_views/external_browser_view.py @@ -0,0 +1,83 @@ +import time + +from appium.webdriver.applicationstate import ApplicationState +from selenium.common import TimeoutException + +from base_test_case import Driver +from views.base_element import Button, EditBox +from views.base_view import BaseView + +browser_app_package = 'com.android.chrome' + + +class BaseBrowserView(BaseView): + def __init__(self, driver: Driver): + super().__init__(driver) + self.search_box_element = EditBox(self.driver, id='com.android.chrome:id/search_box_text') + self.url_input = EditBox(self.driver, id='com.android.chrome:id/url_bar') + self.no_thanks_button = Button(self.driver, id='com.android.chrome:id/negative_button') + self.search_wallet_input = EditBox( + self.driver, xpath="//*[@text='All Wallets']/../../../following-sibling::*//android.widget.EditText") + self.status_app_button = Button(self.driver, xpath="//*[starts-with(@text,'Status Status')]") + + def wait_for_app_to_run(self, app_package: str, wait_time: int = 3): + for _ in range(wait_time): + if self.driver.query_app_state(app_package) == ApplicationState.RUNNING_IN_FOREGROUND: + return + time.sleep(1) + raise TimeoutException( + msg="Application %s is not running in foreground after %s sec" % (app_package, wait_time)) + + def open_browser(self): + self.driver.activate_app(browser_app_package) + self.wait_for_app_to_run(browser_app_package) + self.no_thanks_button.click_if_shown(2) + + def open_url(self, url: str): + if self.url_input.is_element_displayed(): + self.url_input.click() + self.url_input.clear() + self.url_input.send_keys(url) + else: + self.search_box_element.send_keys(url) + self.driver.press_keycode(66) + + +class BridgeStatusNetworkView(BaseBrowserView): + def __init__(self, driver): + super().__init__(driver) + self.connect_wallet_button = Button(self.driver, xpath="//*[@resource-id='wallet-connect-btn']") + self.all_wallets_button = Button(self.driver, xpath="//*[starts-with(@text,'All Wallets')]") + self.amount_input = EditBox(self.driver, xpath="//*[@resource-id='amount-input']") + self.bridge_button = Button(self.driver, xpath="//*[@text='BRIDGE']") + + def connect_status_wallet(self, refresh=True): + status_app_package = self.driver.current_package + self.open_browser() + if refresh: + self.open_url('https://bridge.status.network/') + self.element_by_text('GOT IT').click_if_shown(2) + self.connect_wallet_button.click() + if self.status_app_button.is_element_displayed(): + self.status_app_button.click() + else: + self.all_wallets_button.click() + self.search_wallet_input.wait_for_visibility_of_element(3) + self.search_wallet_input.send_keys('Status') + self.status_app_button.wait_and_click() + self.wait_for_app_to_run(status_app_package) + + +class UniswapView(BaseBrowserView): + + def connect_status_wallet(self): + status_app_package = self.driver.current_package + self.open_browser() + self.open_url('https://app.uniswap.org/swap') + self.element_by_text('Connect wallet').wait_and_click() + self.element_by_text('Other wallets').wait_and_click() + self.element_by_text('WalletConnect').click() + self.search_wallet_input.wait_for_visibility_of_element(3) + self.search_wallet_input.send_keys('Status') + self.status_app_button.wait_and_click() + self.wait_for_app_to_run(status_app_package) diff --git a/test/appium/views/web_views/status_test_dapp.py b/test/appium/views/web_views/status_test_dapp.py deleted file mode 100644 index 7387e940f02..00000000000 --- a/test/appium/views/web_views/status_test_dapp.py +++ /dev/null @@ -1,89 +0,0 @@ -from views.web_views.base_web_view import BaseWebView, BaseButton - - -class AssetsButton(BaseButton): - - def __init__(self, driver): - super(AssetsButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Assets') - - class RequestETHButton(BaseButton): - def __init__(self, driver): - super(AssetsButton.RequestETHButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Request Ropsten ETH') - - class RequestSTTButton(BaseButton): - def __init__(self, driver): - super(AssetsButton.RequestSTTButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Request STT') - - def navigate(self): - from views.send_transaction_view import SendTransactionView - return SendTransactionView(self.driver) - - -class TransactionsButton(BaseButton): - - def __init__(self, driver): - super(TransactionsButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Transactions') - - class SignMessageButton(BaseButton): - def __init__(self, driver): - super(TransactionsButton.SignMessageButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Sign message') - - def navigate(self): - from views.send_transaction_view import SendTransactionView - return SendTransactionView(self.driver) - - class DeployContractButton(BaseButton): - def __init__(self, driver): - super(TransactionsButton.DeployContractButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Deploy simple contract') - - def navigate(self): - from views.send_transaction_view import SendTransactionView - return SendTransactionView(self.driver) - - class TestFiltersButton(BaseButton): - def __init__(self, driver): - super(TransactionsButton.TestFiltersButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Test filters') - - -class StatusAPIButton(BaseButton): - - def __init__(self, driver): - super(StatusAPIButton, self).__init__(driver) - self.locator = self.Locator.text_selector('Status API') - - class RequestContactCodeButton(BaseButton): - def __init__(self, driver): - super(StatusAPIButton.RequestContactCodeButton, self).__init__(driver) - self.locator = self.Locator.text_part_selector('Request contact code') - - class DoNotAllowButton(BaseButton): - def __init__(self, driver): - super(StatusAPIButton.DoNotAllowButton, self).__init__(driver) - self.locator = self.Locator.text_selector("DON'T ALLOW") - - -class StatusTestDAppView(BaseWebView): - - def __init__(self, driver): - super(StatusTestDAppView, self).__init__(driver) - self.driver = driver - - self.assets_button = AssetsButton(self.driver) - self.request_eth_button = AssetsButton.RequestETHButton(self.driver) - self.request_stt_button = AssetsButton.RequestSTTButton(self.driver) - - self.transactions_button = TransactionsButton(self.driver) - self.sign_message_button = TransactionsButton.SignMessageButton(self.driver) - self.deploy_contract_button = TransactionsButton.DeployContractButton(self.driver) - self.test_filters_button = TransactionsButton.TestFiltersButton(self.driver) - - self.status_api_button = StatusAPIButton(self.driver) - self.request_contact_code_button = StatusAPIButton.RequestContactCodeButton(self.driver) - self.do_not_allow_button = StatusAPIButton.DoNotAllowButton(self.driver) diff --git a/test/cljs/cljsjs/create_react_class.cljs b/test/cljs/cljsjs/create_react_class.cljs deleted file mode 100644 index 2b565554c1f..00000000000 --- a/test/cljs/cljsjs/create_react_class.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns cljsjs.create-react-class) diff --git a/test/cljs/cljsjs/react.cljs b/test/cljs/cljsjs/react.cljs deleted file mode 100644 index 69f52779e9a..00000000000 --- a/test/cljs/cljsjs/react.cljs +++ /dev/null @@ -1,2 +0,0 @@ -(ns cljsjs.react) - diff --git a/test/cljs/cljsjs/react/dom.cljs b/test/cljs/cljsjs/react/dom.cljs deleted file mode 100644 index 3c69f0090fa..00000000000 --- a/test/cljs/cljsjs/react/dom.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns cljsjs.react.dom) \ No newline at end of file diff --git a/test/cljs/cljsjs/react/dom/server.cljs b/test/cljs/cljsjs/react/dom/server.cljs deleted file mode 100644 index c7ffcf638fe..00000000000 --- a/test/cljs/cljsjs/react/dom/server.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns cljsjs.react.dom.server) \ No newline at end of file diff --git a/test/cljs/status_im/react_native/js_dependencies.cljs b/test/cljs/status_im/react_native/js_dependencies.cljs deleted file mode 100644 index 30ba09cf022..00000000000 --- a/test/cljs/status_im/react_native/js_dependencies.cljs +++ /dev/null @@ -1,48 +0,0 @@ -(ns status-im.react-native.js-dependencies) - -(def action-button #js {:default #js {:Item #js {}}}) -(def config #js {:default #js {}}) -(def camera #js {:constants #js {}}) -(def dialogs #js {}) -(def dismiss-keyboard #js {}) -(def emoji-picker #js {:default #js {}}) -(def fs #js {}) -(def http-bridge #js {}) -(def i18n #js {:locale "en"}) -(def image-crop-picker #js {}) -(def image-resizer #js {}) -(def instabug #js {}) -(def linear-gradient #js {}) -(def nfc #js {}) -(def orientation #js {}) -(def popup-menu #js {}) -(def qr-code #js {}) -(def react-native - #js {:NativeModules #js {} - :Animated #js {:View #js {} - :Text #js {}} - :DeviceEventEmitter #js {:addListener (fn [])} - :Dimensions #js {:get (fn [])}}) -(def realm #js {:schemaVersion (fn []) - :defaultPath "/tmp/realm" - :close (fn [])}) -(def vector-icons #js {:default #js {}}) -(def webview-bridge #js {:default #js {}}) -(def svg #js {:default #js {}}) -(def react-native-fcm #js {:default #js {}}) - -(def snoopy #js {:default #js {}}) -(def snoopy-filter #js {:default #js {}}) -(def snoopy-bars #js {:default #js {}}) -(def snoopy-buffer #js {:default #js {}}) -(def EventEmmiter #js {}) -(def fetch #js {}) -(def testfairy #js {}) - -(def background-timer #js {:setTimeout js/setTimeout - :setInterval js/setInterval - :clearTimeout js/clearTimeout - :clearInterval js/clearInterval}) - -(def keychain #js {:setGenericPassword (constantly (.resolve js/Promise true))}) -(def secure-random #(.resolve js/Promise (clj->js (range 0 %)))) diff --git a/test/cljs/status_im/test/accounts/events.cljs b/test/cljs/status_im/test/accounts/events.cljs deleted file mode 100644 index 190a4612f74..00000000000 --- a/test/cljs/status_im/test/accounts/events.cljs +++ /dev/null @@ -1,73 +0,0 @@ -(ns status-im.test.accounts.events - (:require [cljs.test :refer-macros [deftest is testing]] - reagent.core - [re-frame.core :as rf] - [day8.re-frame.test :refer-macros [run-test-sync]] - status-im.ui.screens.db - status-im.ui.screens.subs - [status-im.ui.screens.events :as events] - [status-im.ui.screens.accounts.events :as account-events] - [status-im.constants :as constants])) - -(def account-id "5648abf29215d3817bec65007be83a0f11d13ad6") - -(def account-from-realm - {:last-updated 1502965625859 - :address "c348abf29215d3817bec65007be83a0f11d13ad6" - :email nil - :signed-up? true - :name "Sleepy Serene Leopardseal" - :photo-path "photo" - :debug? false - :signing-phrase "baby atom base" - :status "be the hero of your own journey" - :network constants/default-network - :networks constants/default-networks - :public-key "0x049b3a8c04f2c5bccda91c1f5e6434ae72957e93a31c0301b4563eda1d6ce419f63c503ebaee143115f96c1f04f232a7a22ca0454e9ee3d579ad1f870315b151d0"}) - -(def new-account - {:address account-id - :signed-up? true - :name "Disloyal Trusting Rainbowfish" - :photo-path "new-account-photo" - :status "the future starts today, not tomorrow" - :network constants/default-network - :networks constants/default-networks - :signing-phrase "long loan limo" - :public-key "0x04f5722fba79eb36d73263417531007f43d13af76c6233573a8e3e60f667710611feba0785d751b50609bfc0b7cef35448875c5392c0a91948c95798a0ce600847"}) - -(defn test-fixtures [] - (rf/reg-fx ::events/init-store #()) - - (rf/reg-fx ::account-events/save-account #()) - (rf/reg-fx ::account-events/broadcast-account-update #()) - (rf/reg-fx ::account-events/send-keys-update #()) - - (rf/reg-cofx - ::account-events/get-all-accounts - (fn [coeffects _] - (assoc coeffects :all-accounts [account-from-realm])))) - -#_(deftest accounts-events - "load-accounts - add-account - account-update - account-update-keys" - - (run-test-sync - - (test-fixtures) - - (rf/dispatch [:initialize-db]) - (rf/dispatch [:set :accounts/current-account-id account-id]) - - (let [accounts (rf/subscribe [:get-accounts])] - - (testing ":load-accounts event" - - ;;Assert the initial state - (is (and (map? @accounts) (empty? @accounts))) - - (rf/dispatch [:load-accounts]) - - (is (= {(:address account-from-realm) account-from-realm} @accounts)))))) diff --git a/test/cljs/status_im/test/browser/events.cljs b/test/cljs/status_im/test/browser/events.cljs deleted file mode 100644 index 85c2122bb36..00000000000 --- a/test/cljs/status_im/test/browser/events.cljs +++ /dev/null @@ -1,183 +0,0 @@ -(ns status-im.test.browser.events - (:require [cljs.test :refer-macros [deftest is testing]] - [day8.re-frame.test :refer-macros [run-test-sync]] - [status-im.ui.screens.events :as events] - status-im.ui.screens.db - status-im.ui.screens.subs - [re-frame.core :as re-frame] - [status-im.models.browser :as model])) - -(defn test-fixtures [] - - (re-frame/reg-fx ::events/init-store #()) - - (re-frame/reg-fx :browse #()) - (re-frame/reg-fx :data-store/tx #()) - - (re-frame/reg-cofx - :data-store/all-browsers - (fn [coeffects _] - (assoc coeffects :all-stored-browsers []))) - - (re-frame/reg-cofx - :data-store/all-dapp-permissions - (fn [coeffects _] - (assoc coeffects :all-dapp-permissions []))) - - (re-frame/reg-fx :send-to-bridge-fx #()) - - (re-frame/reg-fx - :show-dapp-permission-confirmation-fx - (fn [[permission {:keys [dapp-name permissions-data index] :as params}]] - (if (and (= dapp-name "test.com") (#{0 1} index)) - (re-frame/dispatch [:next-dapp-permission params permission permissions-data]) - (re-frame/dispatch [:next-dapp-permission params]))))) - -(deftest browser-events - - (run-test-sync - - (test-fixtures) - - (re-frame/dispatch [:initialize-db]) - (re-frame/dispatch [:initialize-browsers]) - - (let [browsers (re-frame/subscribe [:browsers]) - dapp1-url "cryptokitties.co" - dapp2-url "http://test2.com"] - - (testing "open and remove dapps" - - (is (zero? (count @browsers))) - - (re-frame/dispatch [:open-url-in-browser dapp1-url]) - - (is (= 1 (count @browsers))) - - (re-frame/dispatch [:open-url-in-browser dapp2-url]) - - (is (= 2 (count @browsers))) - - (let [browser1 (first (vals @browsers)) - browser2 (second (vals @browsers))] - (is (and (:dapp? browser1) - (not (:dapp? browser2)))) - (is (and (zero? (:history-index browser1)) - (zero? (:history-index browser2)))) - (is (and (= [(str "http://" dapp1-url) (:history browser1)]) - (= [dapp2-url] (:history browser2))))) - - (re-frame/dispatch [:remove-browser dapp1-url]) - - (is (= 1 (count @browsers)))) - - (testing "navigate dapp" - - (re-frame/dispatch [:open-browser (first (vals @browsers))]) - - (let [browser (re-frame/subscribe [:get-current-browser]) - dapp2-url2 (str dapp2-url "/nav2") - dapp2-url3 (str dapp2-url "/nav3")] - - (is (zero? (:history-index @browser))) - (is (= [dapp2-url] (:history @browser))) - - (is (and (not (model/can-go-back? @browser)) - (not (model/can-go-forward? @browser)))) - - (re-frame/dispatch [:browser-nav-back]) - (re-frame/dispatch [:browser-nav-forward]) - - (re-frame/dispatch [:update-browser-on-nav-change @browser dapp2-url2 false]) - - (is (= 1 (:history-index @browser))) - (is (= [dapp2-url dapp2-url2] (:history @browser))) - - (is (and (model/can-go-back? @browser) - (not (model/can-go-forward? @browser)))) - - (re-frame/dispatch [:browser-nav-back @browser]) - - (is (zero? (:history-index @browser))) - (is (= [dapp2-url dapp2-url2] (:history @browser))) - - (is (and (not (model/can-go-back? @browser)) - (model/can-go-forward? @browser))) - - (re-frame/dispatch [:update-browser-on-nav-change @browser dapp2-url3 false]) - - (is (= 1 (:history-index @browser))) - (is (= [dapp2-url dapp2-url3] (:history @browser))) - - (re-frame/dispatch [:browser-nav-back @browser]) - - (is (zero? (:history-index @browser))) - (is (= [dapp2-url dapp2-url3] (:history @browser))) - - (re-frame/dispatch [:browser-nav-forward @browser]) - - (is (= 1 (:history-index @browser))) - (is (= [dapp2-url dapp2-url3] (:history @browser)))))) - - (re-frame/dispatch [:initialize-dapp-permissions]) - - (let [dapps-permissions (re-frame/subscribe [:get :dapps/permissions]) - dapp-name "test.com" - dapp-name2 "test2.org"] - - (testing "dapps permissions" - - (is (zero? (count @dapps-permissions))) - - (re-frame/dispatch [:on-bridge-message {:type "status-api-request" - :host dapp-name - :permissions ["FAKE_PERMISSION"]} - nil nil]) - - (is (= {:dapp dapp-name - :permissions []} - (get @dapps-permissions dapp-name))) - - (re-frame/dispatch [:on-bridge-message {:type "status-api-request" - :host dapp-name - :permissions ["CONTACT_CODE"]} - nil nil]) - - (is (= 1 (count @dapps-permissions))) - - (is (= {:dapp dapp-name - :permissions ["CONTACT_CODE"]} - (get @dapps-permissions dapp-name))) - - (re-frame/dispatch [:on-bridge-message {:type "status-api-request" - :host dapp-name - :permissions ["CONTACT_CODE" "FAKE_PERMISSION"]} - nil nil]) - - (is (= 1 (count @dapps-permissions))) - - (is (= {:dapp dapp-name - :permissions ["CONTACT_CODE"]} - (get @dapps-permissions dapp-name))) - - (re-frame/dispatch [:on-bridge-message {:type "status-api-request" - :host dapp-name - :permissions ["FAKE_PERMISSION"]} - nil nil]) - - (is (= 1 (count @dapps-permissions))) - - (is (= {:dapp dapp-name - :permissions ["CONTACT_CODE"]} - (get @dapps-permissions dapp-name))) - - (re-frame/dispatch [:on-bridge-message {:type "status-api-request" - :host dapp-name2 - :permissions ["CONTACT_CODE"]} - nil nil]) - - (is (= 2 (count @dapps-permissions))) - - (is (= {:dapp dapp-name2 - :permissions []} - (get @dapps-permissions dapp-name2))))))) diff --git a/test/cljs/status_im/test/chat/commands/core.cljs b/test/cljs/status_im/test/chat/commands/core.cljs deleted file mode 100644 index b081323535f..00000000000 --- a/test/cljs/status_im/test/chat/commands/core.cljs +++ /dev/null @@ -1,178 +0,0 @@ -(ns status-im.test.chat.commands.core - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.chat.commands.core :as core] - [status-im.chat.commands.protocol :as protocol])) - -(defn- fake-suggestion - [selected-event-creator value] - (selected-event-creator value)) - -(def test-command-parameters - [{:id :first-param - :type :text - ;; pass function as mock-up for suggestions component, so we can - ;; just test the correct injection of `:set-command-parameter` event - :suggestions fake-suggestion} - {:id :second-param - :type :text} - {:id :last-param - :type :text - :suggestions fake-suggestion}]) - -(deftype TestCommand [] - protocol/Command - (id [_] "test-command") - (scope [_] #{:personal-chats :group-chats :public-chats}) - (description [_] "Another test command") - (parameters [_] test-command-parameters) - (validate [_ parameters _] - (when-not (every? (comp string? second) parameters) - "Not all parameters are filled and of the correct type")) - (on-send [_ _ _]) - (on-receive [_ _ _]) - (short-preview [_ command-message] - [:text (str "Test-command, first-param: " - (get-in command-message [:content :params :first-param]))]) - (preview [_ command-message] - [:text (str "Test-command, params: " - (apply str (map [:first-param :second-param :last-param] - (get-in command-message [:content :params]))))])) - -(def another-test-command-parameters - [{:id :first-param - :type :text}]) - -(deftype AnotherTestCommand [] - protocol/Command - (id [_] "another-test-command") - (scope [_] #{:public-chats}) - (description [_] "Another test command") - (parameters [_] another-test-command-parameters) - (validate [_ parameters _] - (when-not (every? (comp string? second) parameters) - "Not all parameters are filled and of the correct type")) - (on-send [_ _ _]) - (on-receive [_ _ _]) - (short-preview [_ command-message] - [:text (str "Test-command, first-param: " - (get-in command-message [:content :params :first-param]))]) - (preview [_ command-message] - [:text (str "Test-command, params: " - (apply str (map [:first-param] - (get-in command-message [:content :params]))))])) - -(def TestCommandInstance (TestCommand.)) -(def AnotherTestCommandInstance (AnotherTestCommand.)) - -(deftest index-commands-test - (let [fx (core/index-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}})] - (testing "Primary composite key index for command is correctly created" - (is (= TestCommandInstance - (get-in fx [:db :id->command - (core/command-id TestCommandInstance) :type])))) - (testing "Suggestions for parameters are injected with correct selection events" - (is (= [:set-command-parameter false 0 "first-value"] - ((get-in fx [:db :id->command - (core/command-id TestCommandInstance) :params - 0 :suggestions]) - "first-value"))) - (is (= [:set-command-parameter true 2 "last-value"] - ((get-in fx [:db :id->command - (core/command-id TestCommandInstance) :params - 2 :suggestions]) - "last-value")))) - (testing "Access scope indexes are correctly created" - (is (contains? (get-in fx [:db :access-scope->command-id #{:personal-chats}]) - (core/command-id TestCommandInstance))) - (is (not (contains? (get-in fx [:db :access-scope->command-id #{:personal-chats}]) - (core/command-id AnotherTestCommandInstance)))) - (is (contains? (get-in fx [:db :access-scope->command-id #{:group-chats}]) - (core/command-id TestCommandInstance))) - (is (contains? (get-in fx [:db :access-scope->command-id #{:public-chats}]) - (core/command-id TestCommandInstance))) - (is (contains? (get-in fx [:db :access-scope->command-id #{:public-chats}]) - (core/command-id AnotherTestCommandInstance)))))) - -(deftest chat-commands-test - (let [fx (core/index-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}})] - (testing "That relevant commands are looked up for chat" - (is (= #{TestCommandInstance AnotherTestCommandInstance} - (into #{} - (map (comp :type second)) - (core/chat-commands (get-in fx [:db :id->command]) - (get-in fx [:db :access-scope->command-id]) - {:chat-id "topic" - :group-chat true - :public? true})))) - (is (= #{TestCommandInstance} - (into #{} - (map (comp :type second)) - (core/chat-commands (get-in fx [:db :id->command]) - (get-in fx [:db :access-scope->command-id]) - {:chat-id "group" - :group-chat true})))) - (is (= #{TestCommandInstance} - (into #{} - (map (comp :type second)) - (core/chat-commands (get-in fx [:db :id->command]) - (get-in fx [:db :access-scope->command-id]) - {:chat-id "contact"}))))))) - -(deftest selected-chat-command-test - (let [fx (core/index-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}}) - commands (core/chat-commands (get-in fx [:db :id->command]) - (get-in fx [:db :access-scope->command-id]) - {:chat-id "contact"})] - (testing "Text not beggining with the command special charactes `/` is recognised" - (is (not (core/selected-chat-command "test-command 1" nil commands)))) - (testing "Command not matching any available commands is not recognised as well" - (is (not (core/selected-chat-command "/another-test-command" nil commands)))) - (testing "Available correctly entered command is recognised" - (is (= TestCommandInstance - (get (core/selected-chat-command "/test-command" nil commands) :type)))) - (testing "Command completion and param position are determined as well" - (let [{:keys [current-param-position command-completion]} - (core/selected-chat-command "/test-command 1 " 17 commands)] - (is (= 1 current-param-position)) - (is (= :less-then-needed command-completion))) - (let [{:keys [current-param-position command-completion]} - (core/selected-chat-command "/test-command 1 2 3" 20 commands)] - (is (= 2 current-param-position)) - (is (= :complete command-completion)))))) - -(deftest set-command-parameter-test - (testing "Setting command parameter correctly updates the text input" - (let [create-cofx (fn [input-text] - {:db {:chats {"test" {:input-text input-text}} - :current-chat-id "test"}})] - (is (= "/test-command first-value " - (get-in (core/set-command-parameter - false 0 "first-value" - (create-cofx "/test-command")) - [:db :chats "test" :input-text]))) - (is (= "/test-command first-value second-value \"last value\"" - (get-in (core/set-command-parameter - false 1 "second-value" - (create-cofx "/test-command first-value edited \"last value\"")) - [:db :chats "test" :input-text]))) - (is (= "/test-command first-value second-value \"last value\"" - (get-in (core/set-command-parameter - false 2 "last value" - (create-cofx "/test-command first-value second-value")) - [:db :chats "test" :input-text])))))) - -(deftest parse-parameters-test - (testing "testing that parse-parameters work correctly" - (is (= {:first-param "1" - :second-param "2" - :last-param "3"} - (core/parse-parameters test-command-parameters "/test-command 1 2 3"))) - (is (= {:first-param "1" - :second-param "2 2" - :last-param "3"} - (core/parse-parameters test-command-parameters "/test-command 1 \"2 2\" 3"))) - (is (= {:first-param "1" - :second-param "2"} - (core/parse-parameters test-command-parameters "/test-command 1 2"))) - (is (= {} - (core/parse-parameters test-command-parameters "/test-command "))))) diff --git a/test/cljs/status_im/test/chat/commands/impl/transactions.cljs b/test/cljs/status_im/test/chat/commands/impl/transactions.cljs deleted file mode 100644 index df6b8993942..00000000000 --- a/test/cljs/status_im/test/chat/commands/impl/transactions.cljs +++ /dev/null @@ -1,75 +0,0 @@ -(ns status-im.test.chat.commands.impl.transactions - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.chat.commands.impl.transactions :as transactions] - [status-im.chat.commands.protocol :as protocol])) - -(def cofx {:db {:account/account {:settings {:wallet {:visible-tokens {:mainnet #{:SNT}}}} - :wallet-set-up-passed? true} - :chain "mainnet" - :current-chat-id "recipient" - :contacts/contacts {"recipient" {:name "Recipient" - :address "0xAA" - :whisper-identity "0xBB"}}}}) - -;; testing the `/send` command - -(def personal-send-command (transactions/PersonalSendCommand.)) - -(deftest personal-send-command-test - (testing "That correct parameters are defined" - (is (= (into #{} (map :id) (protocol/parameters personal-send-command)) - #{:asset :amount}))) - (testing "Parameters validation" - (is (= (protocol/validate personal-send-command {:asset "TST"} cofx) - {:title "Invalid Asset" - :description "Unknown token - TST"})) - (is (= (protocol/validate personal-send-command {:asset "SNT"} cofx) - {:title "Amount" - :description "Amount must be specified"})) - (is (= (protocol/validate personal-send-command {:asset "SNT" :amount "a"} cofx) - {:title "Amount" - :description "Amount is not valid number"})) - (is (= (protocol/validate personal-send-command {:asset "ETH" :amount "0.54354353454353453453454353453445345545"} cofx) - {:title "Amount" - :description "Max number of decimals is 18"})) - (is (= (protocol/validate personal-send-command {:asset "ETH" :amount "0.01"} cofx) - nil))) - (testing "Yielding control prefills wallet" - (let [fx (protocol/yield-control personal-send-command {:asset "ETH" :amount "0.01"} cofx)] - (is (= (get-in fx [:db :wallet :send-transaction :amount-text]) "0.01")) - (is (= (get-in fx [:db :wallet :send-transaction :symbol]) :ETH))))) - -;; testing the `/request` command - -(def personal-request-command (transactions/PersonalRequestCommand.)) - -(deftest personal-send-command-test - (testing "That correct parameters are defined" - (is (= (into #{} (map :id) (protocol/parameters personal-request-command)) - #{:asset :amount}))) - (testing "Parameters validation" - (is (= (protocol/validate personal-request-command {:asset "TST"} cofx) - {:title "Invalid Asset" - :description "Unknown token - TST"})) - (is (= (protocol/validate personal-request-command {:asset "SNT"} cofx) - {:title "Amount" - :description "Amount must be specified"})) - (is (= (protocol/validate personal-request-command {:asset "SNT" :amount "a"} cofx) - {:title "Amount" - :description "Amount is not valid number"})) - (is (= (protocol/validate personal-request-command {:asset "ETH" :amount "0.54354353454353453453454353453445345545"} cofx) - {:title "Amount" - :description "Max number of decimals is 18"})) - (is (= (protocol/validate personal-request-command {:asset "ETH" :amount "0.01"} cofx) - nil))) - (testing "On receive adds pending request when `/request` command is received" - (let [fx (protocol/on-receive personal-request-command - {:chat-id "recipient" - :message-id "0xAA"} - cofx)] - (is (= (get-in fx [:db :chats "recipient" :requests "0xAA"]) - {:chat-id "recipient" - :message-id "0xAA" - :response "send" - :status "open"}))))) - diff --git a/test/cljs/status_im/test/chat/events.cljs b/test/cljs/status_im/test/chat/events.cljs deleted file mode 100644 index 47ed12d1e76..00000000000 --- a/test/cljs/status_im/test/chat/events.cljs +++ /dev/null @@ -1,82 +0,0 @@ -(ns status-im.test.chat.events - (:require [cljs.test :refer [deftest is testing]] - reagent.core - [re-frame.core :as rf] - [day8.re-frame.test :refer [run-test-sync]] - [status-im.constants :as const] - [status-im.chat.events :as chat-events])) - -(def test-db - {:current-public-key "me" - :chats {"status" {:public? true - :unviewed-messages #{"6" "5" "4" "3" "2" "1"} - :message-statuses {"6" {"me" {:message-id "6" - :chat-id "status" - :whisper-identity "me" - :status :received}} - "5" {"me" {:message-id "5" - :chat-id "status" - :whisper-identity "me" - :status :received}} - "4" {"me" {:message-id "4" - :chat-id "status" - :whisper-identity "me" - :status :received}}}} - "opened" {:unviewed-messages #{} - :message-statuses {"1" {"me" {:message-id "1" - :chat-id "opened" - :whisper-identity "me" - :status :seen}}}} - "1-1" {:unviewed-messages #{"6" "5" "4" "3" "2" "1"} - :message-statuses {"6" {"me" {:message-id "6" - :chat-id "status" - :whisper-identity "me" - :status :received}} - "5" {"me" {:message-id "5" - :chat-id "status" - :whisper-identity "me" - :status :received}} - "4" {"me" {:message-id "4" - :chat-id "status" - :whisper-identity "me" - :status :received}}}}}}) - -(deftest mark-messages-seen - (testing "Marking messages seen correctly marks loaded messages as seen and updates absolute unviewed set" - (let [fx (chat-events/mark-messages-seen "status" {:db test-db}) - me (:current-public-key test-db)] - (is (= '(:seen :seen :seen) - (map (fn [[_ v]] - (get-in v [me :status])) - (get-in fx [:db :chats "status" :message-statuses])))) - (is (= 1 (count (:data-store/tx fx)))) - (is (= nil (:shh/post fx))) ;; for public chats, no confirmation is sent out - (is (= #{"3" "2" "1"} (get-in fx [:db :chats "status" :unviewed-messages]))))) - - (testing "With empty unviewed set, no effects are produced" - (is (= nil (chat-events/mark-messages-seen "opened" {:db test-db})))) - - (testing "For 1-1 chat, we send seen messages confirmation to the recipient as well" - (is (= #{"4" "5" "6"} - (set (get-in (chat-events/mark-messages-seen "1-1" {:db test-db}) - [:shh/post 0 :message :payload :message-ids])))))) - -(deftest show-profile-test - (testing "default behaviour" - (testing "it navigates to profile but forgets the navigation" - (let [{:keys [db]} (chat-events/show-profile - "a" - false - {:db {:navigation-stack '(:home)}})] - (is (= "a" (:contacts/identity db))) - (is (= '(:home) (:navigation-stack db))) - (is (= :profile (:view-id db)))))) - (testing "keep-navigation? on" - (testing "it navigates to profile and keeps the navigation" - (let [{:keys [db]} (chat-events/show-profile - "a" - true - {:db {:navigation-stack '(:home)}})] - (is (= "a" (:contacts/identity db))) - (is (= '(:profile :home) (:navigation-stack db))) - (is (= :profile (:view-id db))))))) diff --git a/test/cljs/status_im/test/chat/models.cljs b/test/cljs/status_im/test/chat/models.cljs deleted file mode 100644 index c6f2451dd3c..00000000000 --- a/test/cljs/status_im/test/chat/models.cljs +++ /dev/null @@ -1,212 +0,0 @@ -(ns status-im.test.chat.models - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.clocks :as utils.clocks] - [status-im.chat.models :as chat])) - -(deftest upsert-chat-test - (testing "upserting a non existing chat" - (let [chat-id "some-chat-id" - contact-name "contact-name" - chat-props {:chat-id chat-id - :extra-prop "some"} - cofx {:now "now" - :db {:contacts/contacts {chat-id - {:name contact-name}}}} - response (chat/upsert-chat chat-props cofx) - actual-chat (get-in response [:db :chats chat-id]) - store-chat-fx (:data-store/tx response)] - (testing "it adds the chat to the chats collection" - (is actual-chat)) - (testing "it adds the extra props" - (is (= "some" (:extra-prop actual-chat)))) - (testing "it adds the chat id" - (is (= chat-id (:chat-id actual-chat)))) - (testing "it pulls the name from the contacts" - (is (= contact-name (:name actual-chat)))) - (testing "it sets the timestamp" - (is (= "now" (:timestamp actual-chat)))) - (testing "it adds the contact-id to the contact field" - (is (= chat-id (-> actual-chat :contacts first)))) - (testing "it adds the fx to store a chat" - (is store-chat-fx)))) - (testing "upserting an existing chat" - (let [chat-id "some-chat-id" - chat-props {:chat-id chat-id - :name "new-name" - :extra-prop "some"} - cofx {:db {:chats {chat-id {:is-active true - :name "old-name"}}}} - response (chat/upsert-chat chat-props cofx) - actual-chat (get-in response [:db :chats chat-id]) - store-chat-fx (:data-store/tx response)] - (testing "it adds the chat to the chats collection" - (is actual-chat)) - (testing "it adds the extra props" - (is (= "some" (:extra-prop actual-chat)))) - (testing "it updates existins props" - (is (= "new-name" (:name actual-chat)))) - (testing "it adds the fx to store a chat" - (is store-chat-fx)))) - (testing "upserting a deleted chat" - (let [chat-id "some-chat-id" - contact-name "contact-name" - chat-props {:chat-id chat-id - :name "new-name" - :extra-prop "some"} - cofx {:some-cofx "b" - :db {:chats {chat-id {:is-active false - :name "old-name"}}}}] - (testing "it updates it if is-active is passed" - (is (get-in (chat/upsert-chat (assoc chat-props :is-active true) cofx) [:db :chats chat-id :is-active]))) - (testing "it returns the db unchanged" - (is (= {:db (:db cofx)} (chat/upsert-chat chat-props cofx))))))) - -(deftest add-group-chat - (let [chat-id "chat-id" - chat-name "chat-name" - admin "admin" - participants ["a"] - fx (chat/add-group-chat chat-id chat-name admin participants {}) - store-fx (:data-store/tx fx) - group-chat (get-in fx [:db :chats chat-id])] - (testing "it saves the chat in the database" - (is store-fx)) - (testing "it sets the name" - (is (= chat-name (:name group-chat)))) - (testing "it sets the admin" - (is (= admin (:group-admin group-chat)))) - (testing "it sets the participants" - (is (= participants (:contacts group-chat)))) - (testing "it sets the chat-id" - (is (= chat-id (:chat-id group-chat)))) - (testing "it sets the group-chat flag" - (is (:group-chat group-chat))) - (testing "it does not sets the public flag" - (is (not (:public? group-chat)))))) - -(deftest add-public-chat - (let [topic "topic" - fx (chat/add-public-chat topic {}) - store-fx (:data-store/tx fx) - chat (get-in fx [:db :chats topic])] - (testing "it saves the chat in the database" - (is store-fx)) - (testing "it sets the name" - (is (= topic (:name chat)))) - (testing "it sets the participants" - (is (= [] (:contacts chat)))) - (testing "it sets the chat-id" - (is (= topic (:chat-id chat)))) - (testing "it sets the group-chat flag" - (is (:group-chat chat))) - (testing "it does not sets the public flag" - (is (:public? chat))))) - -(deftest clear-history-test - (let [chat-id "1" - cofx {:db {:chats {chat-id {:message-groups {:something "a"} - :messages {"1" {:clock-value 1} - "2" {:clock-value 10} - "3" {:clock-value 2}} - :unviewed-messages #{"3"} - :not-loaded-message-ids #{"2" "3"}}}}}] - (testing "it deletes all the messages" - (let [actual (chat/clear-history chat-id cofx)] - (is (= {} (get-in actual [:db :chats chat-id :messages]))))) - (testing "it deletes all the message groups" - (let [actual (chat/clear-history chat-id cofx)] - (is (= {} (get-in actual [:db :chats chat-id :message-groups]))))) - (testing "it deletes unviewed messages set" - (let [actual (chat/clear-history chat-id cofx)] - (is (= #{} (get-in actual [:db :chats chat-id :unviewed-messages]))))) - (testing "it deletes not loaded message ids set" - (let [actual (chat/clear-history chat-id cofx)] - (is (= #{} (get-in actual [:db :chats chat-id :not-loaded-message-ids]))))) - (testing "it sets a deleted-at-clock-value equal to the last message clock-value" - (let [actual (chat/clear-history chat-id cofx)] - (is (= 10 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) - (testing "it does not override the deleted-at-clock-value when there are no messages" - (let [actual (chat/clear-history chat-id - (update-in cofx - [:db :chats chat-id] - assoc - :messages {} - :deleted-at-clock-value 100))] - (is (= 100 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) - (testing "it set the deleted-at-clock-value to now the chat has no messages nor previous deleted-at" - (with-redefs [utils.clocks/send (constantly 42)] - (let [actual (chat/clear-history chat-id - (update-in cofx - [:db :chats chat-id] - assoc - :messages {}))] - (is (= 42 (get-in actual [:db :chats chat-id :deleted-at-clock-value])))))) - (testing "it adds the relevant transactions for realm" - (let [actual (chat/clear-history chat-id cofx)] - (is (:data-store/tx actual)) - (is (= 2 (count (:data-store/tx actual)))))))) - -(deftest remove-chat-test - (let [chat-id "1" - cofx {:db {:transport/chats {chat-id {}} - :chats {chat-id {:messages {"1" {:clock-value 1} - "2" {:clock-value 10} - "3" {:clock-value 2}}}}}}] - (testing "it deletes all the messages" - (let [actual (chat/remove-chat chat-id cofx)] - (is (= {} (get-in actual [:db :chats chat-id :messages]))))) - (testing "it sets a deleted-at-clock-value equal to the last message clock-value" - (let [actual (chat/remove-chat chat-id cofx)] - (is (= 10 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) - (testing "it sets the chat as inactive" - (let [actual (chat/remove-chat chat-id cofx)] - (is (= false (get-in actual [:db :chats chat-id :is-active]))))) - (testing "it removes it from transport if it's a public chat" - (let [actual (chat/remove-chat chat-id - (update-in - cofx - [:db :chats chat-id] - assoc - :group-chat true - :public? true))] - (is (not (get-in actual [:db :transport/chats chat-id]))))) - (testing "it sends a leave group request if it's a group-chat" - (let [actual (chat/remove-chat chat-id - (assoc-in - cofx - [:db :chats chat-id :group-chat] - true))] - (is (:shh/post actual)) - (testing "it does not remove transport, only after send is successful" - (is (get-in actual [:db :transport/chats chat-id]))))) - (testing "it does not remove it from transport if it's a one-to-one" - (let [actual (chat/remove-chat chat-id cofx)] - (is (get-in actual [:db :transport/chats chat-id])))) - (testing "it adds the relevant transactions for realm" - (let [actual (chat/remove-chat chat-id cofx)] - (is (:data-store/tx actual)) - (is (= 3 (count (:data-store/tx actual)))))))) - -(deftest multi-user-chat? - (let [chat-id "1"] - (testing "it returns true if it's a group chat" - (let [cofx {:db {:chats {chat-id {:group-chat true}}}}] - (is (chat/multi-user-chat? chat-id cofx)))) - (testing "it returns true if it's a public chat" - (let [cofx {:db {:chats {chat-id {:public? true :group-chat true}}}}] - (is (chat/multi-user-chat? chat-id cofx)))) - (testing "it returns false if it's a 1-to-1 chat" - (let [cofx {:db {:chats {chat-id {}}}}] - (is (not (chat/multi-user-chat? chat-id cofx))))))) - -(deftest group-chat? - (let [chat-id "1"] - (testing "it returns true if it's a group chat" - (let [cofx {:db {:chats {chat-id {:group-chat true}}}}] - (is (chat/group-chat? chat-id cofx)))) - (testing "it returns false if it's a public chat" - (let [cofx {:db {:chats {chat-id {:public? true :group-chat true}}}}] - (is (not (chat/group-chat? chat-id cofx))))) - (testing "it returns false if it's a 1-to-1 chat" - (let [cofx {:db {:chats {chat-id {}}}}] - (is (not (chat/group-chat? chat-id cofx))))))) diff --git a/test/cljs/status_im/test/chat/models/input.cljs b/test/cljs/status_im/test/chat/models/input.cljs deleted file mode 100644 index 2e10b0adee8..00000000000 --- a/test/cljs/status_im/test/chat/models/input.cljs +++ /dev/null @@ -1,100 +0,0 @@ -(ns status-im.test.chat.models.input - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.chat.constants :as constants] - [status-im.utils.config :as config] - [status-im.utils.datetime :as datetime] - [status-im.chat.models.input :as input])) - -(deftest text->emoji - (is (nil? (input/text->emoji nil))) - (is (= "" (input/text->emoji ""))) - (is (= "test" (input/text->emoji "test"))) - (is (= "word1 \uD83D\uDC4D word2" (input/text->emoji "word1 :+1: word2")))) - -(deftest starts-as-command? - (is (not (input/starts-as-command? nil))) - (is (not (input/text-ends-with-space? ""))) - (is (not (input/text-ends-with-space? "word1 word2 word3"))) - (is (input/text-ends-with-space? "word1 word2 "))) - -(deftest split-command-args - (is (nil? (input/split-command-args nil))) - (is (= [""] (input/split-command-args ""))) - (is (= ["@browse" "google.com"] (input/split-command-args "@browse google.com"))) - (is (= ["@browse" "google.com"] (input/split-command-args " @browse google.com "))) - (is (= ["/send" "1.0" "John Doe"] (input/split-command-args "/send 1.0 \"John Doe\""))) - (is (= ["/send" "1.0" "John Doe"] (input/split-command-args "/send 1.0 \"John Doe\" ")))) - -(deftest join-command-args - (is (nil? (input/join-command-args nil))) - (is (= "" (input/join-command-args [""]))) - (is (= "/send 1.0 \"John Doe\"" (input/join-command-args ["/send" "1.0" "John Doe"])))) - -(deftest process-cooldown-fx - (let [db {:current-chat-id "chat" - :chats {"chat" {:public? true}} - :chat/cooldowns 0 - :chat/spam-messages-frequency 0 - :chat/cooldown-enabled? false}] - (with-redefs [datetime/timestamp (constantly 1527675198542) - config/spam-button-detection-enabled? true] - (testing "no spamming detected" - (let [expected {:db (assoc db :chat/last-outgoing-message-sent-at 1527675198542)} - actual (input/process-cooldown {:db db})] - (is (= expected actual)))) - - (testing "spamming detected in 1-1" - (let [db (assoc db - :chats {"chat" {:public? false}} - :chat/spam-messages-frequency constants/spam-message-frequency-threshold - :chat/last-outgoing-message-sent-at (- 1527675198542 900)) - expected nil - actual (input/process-cooldown {:db db})] - (is (= expected actual)))) - - (testing "spamming detected" - (let [db (assoc db - :chat/last-outgoing-message-sent-at (- 1527675198542 900) - :chat/spam-messages-frequency constants/spam-message-frequency-threshold) - expected {:db (assoc db - :chat/last-outgoing-message-sent-at 1527675198542 - :chat/cooldowns 1 - :chat/spam-messages-frequency 0 - :chat/cooldown-enabled? true) - :show-cooldown-warning nil - :dispatch-later [{:dispatch [:disable-cooldown] - :ms (constants/cooldown-periods-ms 1)}]} - actual (input/process-cooldown {:db db})] - (is (= expected actual)))) - - (testing "spamming detected twice" - (let [db (assoc db - :chat/cooldowns 1 - :chat/last-outgoing-message-sent-at (- 1527675198542 900) - :chat/spam-messages-frequency constants/spam-message-frequency-threshold) - expected {:db (assoc db - :chat/last-outgoing-message-sent-at 1527675198542 - :chat/cooldowns 2 - :chat/spam-messages-frequency 0 - :chat/cooldown-enabled? true) - :show-cooldown-warning nil - :dispatch-later [{:dispatch [:disable-cooldown] - :ms (constants/cooldown-periods-ms 2)}]} - actual (input/process-cooldown {:db db})] - (is (= expected actual)))) - - (testing "spamming reaching cooldown threshold" - (let [db (assoc db - :chat/cooldowns (dec constants/cooldown-reset-threshold) - :chat/last-outgoing-message-sent-at (- 1527675198542 900) - :chat/spam-messages-frequency constants/spam-message-frequency-threshold) - expected {:db (assoc db - :chat/last-outgoing-message-sent-at 1527675198542 - :chat/cooldowns 0 - :chat/spam-messages-frequency 0 - :chat/cooldown-enabled? true) - :show-cooldown-warning nil - :dispatch-later [{:dispatch [:disable-cooldown] - :ms (constants/cooldown-periods-ms 3)}]} - actual (input/process-cooldown {:db db})] - (is (= expected actual))))))) diff --git a/test/cljs/status_im/test/chat/models/message.cljs b/test/cljs/status_im/test/chat/models/message.cljs deleted file mode 100644 index 56040232ad5..00000000000 --- a/test/cljs/status_im/test/chat/models/message.cljs +++ /dev/null @@ -1,171 +0,0 @@ -(ns status-im.test.chat.models.message - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.transport.message.v1.protocol :as protocol] - [status-im.chat.models.message :as message] - [status-im.utils.datetime :as time])) - -(deftest add-to-chat? - (testing "it returns true when it's not in loaded message" - (is (message/add-to-chat? {:db {:chats {"a" {}}}} - {:message-id "message-id" - :from "a" - :clock-value 1 - :chat-id "a"}))) - (testing "it returns false when it's already in the loaded message" - (is (not (message/add-to-chat? {:db {:chats {"a" {:messages {"message-id" {}}}}}} - {:message-id "message-id" - :from "a" - :clock-value 1 - :chat-id "a"})))) - (testing "it returns false when it's already in the not-loaded-message-ids" - (is (not (message/add-to-chat? {:db {:chats {"a" {:not-loaded-message-ids #{"message-id"}}}}} - {:message-id "message-id" - :from "a" - :clock-value 1 - :chat-id "a"})))) - (testing "it returns false when the clock-value is the same as the deleted-clock-value in chat" - (is (not (message/add-to-chat? {:db {:chats {"a" {:deleted-at-clock-value 1}}}} - {:message-id "message-id" - :from "a" - :clock-value 1 - :chat-id "a"})))) - (testing "it returns true when the clock-value is greater than the deleted-clock-value in chat" - (is (message/add-to-chat? {:db {:chats {"a" {:deleted-at-clock-value 1}}}} - {:message-id "message-id" - :from "a" - :clock-value 2 - :chat-id "a"}))) - (testing "it returns false when the clock-value is less than the deleted-clock-value in chat" - (is (not (message/add-to-chat? {:db {:chats {"a" {:deleted-at-clock-value 1}}}} - {:message-id "message-id" - :from "a" - :clock-value 0 - :chat-id "a"}))))) - -(deftest add-own-received-message - (let [db {:current-public-key "me" - :view-id :chat - :current-chat-id "chat-id" - :chats {"chat-id" {:messages {}}}}] - (testing "a message coming from you!" - (let [actual (message/receive - {:from "me" - :message-id "id" - :chat-id "chat-id" - :content "b" - :clock-value 1} - {:db db}) - message (get-in actual [:db :chats "chat-id" :messages "id"]) - status (get-in actual [:db :chats "chat-id" :message-statuses "id" "me" :status])] - (testing "it adds the message" - (is message)) - (testing "it marks the message as outgoing" - (is (= true (:outgoing message)))) - (testing "it confirm the message as processed" - (is (:confirm-messages-processed actual))) - (testing "it stores the message" - (is (:data-store/tx actual))) - (testing "it does not send a seen confirmation" - (is (not (:shh/post actual)))) - (testing "it marks it as sent" - (is (= :sent status))))))) - -(deftest receive-send-seen - (let [db {:db {:chats {"chat-id" {}} - :account/account {:public-key "a"} - :current-chat-id "chat-id" - :view-id :chat}} - message {:chat-id "chat-id" - :from "a" - :message-id "1" - :clock-value 0 - :timestamp 0} - extract-seen (comp :payload :message first :shh/post)] - (testing "it send a seen message when the chat is 1-to-1 and is open" - (is (instance? protocol/MessagesSeen - (extract-seen (message/receive message db)))) - (is (= #{"1"} (:message-ids (extract-seen (message/receive message db)))))) - (testing "it does not send any when the chat is public" - (is (nil? (extract-seen - (message/receive - message - (assoc-in db [:db :chats "chat-id" :public?] true)))))) - (testing "it does not send any when we are in a different chat" - (is (nil? (extract-seen - (message/receive - message - (assoc-in db [:db :current-chat-id] :different)))))) - (testing "it does not send any when we are not in a chat view" - (is (nil? (extract-seen - (message/receive - message - (assoc-in db [:db :view-id] :home)))))))) - -(deftest group-messages - (let [cofx {:db {:chats {"chat-id" {:messages {0 {:message-id 0 - :content "a" - :clock-value 0 - :timestamp 0} - 1 {:message-id 1 - :content "b" - :clock-value 1 - :timestamp 1} - 2 {:message-id 2 - :content "c" - :clock-value 2 - :timestamp 2} - 3 {:message-id 3 - :content "d" - :clock-value 3 - :timestamp 3}}}}}} - new-messages [{:message-id 1 - :content "b" - :clock-value 1 - :timestamp 1 - :show? false} - {:message-id 2 - :content "c" - :clock-value 2 - :timestamp 2 - :show? true} - {:message-id 3 - :content "d" - :clock-value 3 - :timestamp 3 - :show? true}]] - (testing "New messages are grouped/sorted correctly, hidden messages are not grouped" - (is (= '(2 3) - (map :message-id - (-> (get-in (message/group-messages "chat-id" new-messages cofx) - [:db :chats "chat-id" :message-groups]) - first - second))))))) - -(deftest delete-message - (let [timestamp (time/now) - cofx1 {:db {:chats {"chat-id" {:messages {0 {:message-id 0 - :content "a" - :clock-value 0 - :timestamp (- timestamp 1)} - 1 {:message-id 1 - :content "b" - :clock-value 1 - :timestamp timestamp}} - :message-groups {"datetime-today" '({:message-id 1} - {:message-id 0})}}}}} - cofx2 {:db {:chats {"chat-id" {:messages {0 {:message-id 0 - :content "a" - :clock-value 0 - :timestamp timestamp}} - :message-groups {"datetime-today" '({:message-id 0})}}}}} - fx1 (message/delete-message "chat-id" 1 cofx1) - fx2 (message/delete-message "chat-id" 0 cofx2)] - (testing "Deleting message deletes it along with all references" - (is (= '(0) - (keys (get-in fx1 [:db :chats "chat-id" :messages])))) - (is (= {"datetime-today" '({:message-id 0})} - (get-in fx1 [:db :chats "chat-id" :message-groups]))) - (is (= {} - (get-in fx2 [:db :chats "chat-id" :messages]))) - (is (= {} - (get-in fx2 [:db :chats "chat-id" :message-groups])))))) diff --git a/test/cljs/status_im/test/chat/subs.cljs b/test/cljs/status_im/test/chat/subs.cljs deleted file mode 100644 index 5a54ea3da95..00000000000 --- a/test/cljs/status_im/test/chat/subs.cljs +++ /dev/null @@ -1,124 +0,0 @@ -(ns status-im.test.chat.subs - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.constants :as const] - [status-im.chat.subs :as s])) - -(deftest chat-name - (testing "it prepends # if it's a public chat" - (is (= "#withhash" (s/chat-name {:group-chat true - :chat-id "1" - :public? true - :name "withhash"} nil)))) - (testing "it leaves the name unchanged if it's a group chat" - (is (= "unchanged" (s/chat-name {:group-chat true - :chat-id "1" - :name "unchanged"} nil)))) - (testing "it pulls the name from contact if it's a one-to-one" - (is (= "this-one" (s/chat-name {:chat-id "1" - :name "not-this-one"} {:name "this-one"})))) - (testing "it generates the name from chat id if no contact" - (is (= "Blond Cooperative Coelacanth" (s/chat-name {:chat-id "1" - :name "not-this-one"} nil))))) - -(deftest message-stream-tests - (testing "messages with no interspersed datemarks" - (let [m1 {:from "1" - :datemark "a" - :outgoing false} - m2 {:from "2" - :datemark "a" - :outgoing true} - m3 {:from "2" - :datemark "a" - :outgoing true} - dm1 {:type :datemark - :value "a"} - messages [m1 m2 m3 dm1] - [actual-m1 - actual-m2 - actual-m3] (s/messages-stream messages)] - (testing "it marks only the first message as :last?" - (is (:last? actual-m1)) - (is (not (:last? actual-m2))) - (is (not (:last? actual-m3)))) - (testing "it marks the first outgoing message as :last-outgoing?" - (is (not (:last-outgoing? actual-m1))) - (is (:last-outgoing? actual-m2)) - (is (not (:last-outgoing? actual-m3)))) - (testing "it marks the message with :same-direction? when from the same author" - (is (not (:same-direction? actual-m1))) - (is (not (:same-direction? actual-m2)) - (is (:same-direction? actual-m3)))) - (testing "it marks messages from the same author next to another with :first-in-group?" - (is (:first-in-group? actual-m1)) - (is (not (:first-in-group? actual-m2))) - (is (:first-in-group? actual-m3))) - (testing "it marks messages with display-photo? when they are not outgoing and we are in a group chat" - (is (:display-photo? actual-m1)) - (is (not (:display-photo? actual-m2))) - (is (not (:display-photo? actual-m3)))) - (testing "it marks messages with display-username? when we display the photo and are the first in a group" - (is (:display-username? actual-m1)) - (is (not (:display-username? actual-m2))) - (is (not (:display-username? actual-m3)))) - (testing "it marks the last message from the same author with :last-in-group?" - (is (:last-in-group? actual-m1)) - (is (:last-in-group? actual-m2)) - (is (not (:last-in-group? actual-m3)))))) - (testing "messages with interspersed datemarks" - (let [m1 {:from "2" ; first & last in group - :timestamp 63000 - :outgoing true} - dm1 {:type :datemark - :value "a"} - m2 {:from "2" ; first & last in group as more than 1 minute after previous message - :timestamp 62000 - :outgoing false} - m3 {:from "2" ; last in group - :timestamp 1 - :outgoing false} - m4 {:from "2" ; first in group - :timestamp 0 - :outgoing false} - dm2 {:type :datemark - :value "b"} - messages [m1 dm1 m2 m3 m4 dm2] - [actual-m1 - _ - actual-m2 - actual-m3 - actual-m4 - _] (s/messages-stream messages)] - (testing "it marks the first outgoing message as :last-outgoing?" - (is (:last-outgoing? actual-m1)) - (is (not (:last-outgoing? actual-m2))) - (is (not (:last-outgoing? actual-m3))) - (is (not (:last-outgoing? actual-m4)))) - (testing "it resets :same-direction? after a datemark" - (is (not (:same-direction? actual-m1))) - (is (not (:same-direction? actual-m2)) - (is (:same-direction? actual-m3))) - (is (:same-direction? actual-m4))) - (testing "it sets :first-in-group? after a datemark" - (is (:first-in-group? actual-m1)) - (is (:first-in-group? actual-m4))) - (testing "it sets :first-in-group? if more than 60s have passed since last message" - (is (:first-in-group? actual-m2))) - (testing "it sets :last-in-group? after a datemark" - (is (:last-in-group? actual-m1)) - (is (:last-in-group? actual-m2)) - (is (:last-in-group? actual-m3)) - (is (not (:last-in-group? actual-m4))))))) - -(deftest active-chats-test - (let [active-chat-1 {:is-active true :chat-id 1} - active-chat-2 {:is-active true :chat-id 2} - console {:is-active true :chat-id const/console-chat-id} - chats {1 active-chat-1 - 2 active-chat-2 - 3 {:is-active false :chat-id 3} - const/console-chat-id console}] - (testing "it returns only chats with is-active, without console" - (is (= {1 active-chat-1 - 2 active-chat-2} - (s/active-chats chats)))))) diff --git a/test/cljs/status_im/test/chat/views/message.cljs b/test/cljs/status_im/test/chat/views/message.cljs deleted file mode 100644 index c7acade8e27..00000000000 --- a/test/cljs/status_im/test/chat/views/message.cljs +++ /dev/null @@ -1,39 +0,0 @@ -(ns status-im.test.chat.views.message - (:require [cljs.test :refer [deftest is]] - [status-im.chat.views.message.message :as message])) - -(deftest parse-url - (is (= (lazy-seq [{:text "" :url? false} - {:text "www.google.com" :url? true}]) - (message/parse-url "www.google.com"))) - (is (= (lazy-seq [{:text "" :url? false} - {:text "status.im" :url? true}]) - (message/parse-url "status.im"))) - (is (= (lazy-seq [{:text "$33.90" :url? false} nil]) - (message/parse-url "$33.90"))) - (is (= (lazy-seq [{:text "" :url? false} - {:text "https://www.google.com/?gfe_rd=cr&dcr=0&ei=P9-CWuyBGaro8AeqkYGQDQ&gws_rd=cr&fg=1" :url? true}]) - (message/parse-url "https://www.google.com/?gfe_rd=cr&dcr=0&ei=P9-CWuyBGaro8AeqkYGQDQ&gws_rd=cr&fg=1"))) - (is (= (lazy-seq [{:text "Status - " :url? false} - {:text "https://github.com/status-im/status-react" :url? true} - {:text " a Mobile Ethereum Operating System" :url? false} - nil]) - (message/parse-url "Status - https://github.com/status-im/status-react a Mobile Ethereum Operating System"))) - (is (= (lazy-seq [{:text "Browse, chat and make payments securely on the decentralized web." :url? false} nil]) - (message/parse-url "Browse, chat and make payments securely on the decentralized web."))) - (is (= (lazy-seq [{:text "test...test..." :url? false} nil]) - (message/parse-url "test...test..."))) - (is (= (lazy-seq [{:text "test..test.." :url? false} nil]) - (message/parse-url "test..test.."))) - (is (= (lazy-seq [{:text "...test" :url? false} nil]) - (message/parse-url "...test")))) - -(deftest right-to-left-text? - (is (not (message/right-to-left-text? "You are lucky today!"))) - (is (not (message/right-to-left-text? "42"))) - (is (not (message/right-to-left-text? "You are lucky today! أنت محظوظ اليوم!"))) - (is (not (message/right-to-left-text? "۱۲۳۴۵۶۷۸۹"))) - (is (not (message/right-to-left-text? "۱۲۳۴۵۶۷۸۹أنت محظوظ اليوم!"))) - (is (message/right-to-left-text? "أنت محظوظ اليوم!")) - (is (message/right-to-left-text? "أنت محظوظ اليوم! You are lucky today")) - (is (message/right-to-left-text? "יש לך מזל היום!"))) diff --git a/test/cljs/status_im/test/chat/views/photos.cljs b/test/cljs/status_im/test/chat/views/photos.cljs deleted file mode 100644 index 33739e04245..00000000000 --- a/test/cljs/status_im/test/chat/views/photos.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns status-im.test.chat.views.photos - (:require [cljs.test :refer [deftest testing is]] - [status-im.react-native.resources :as resources] - [status-im.chat.views.photos :as photos])) - -(deftest photos-test - (testing "a normal string" - (let [actual (photos/source "some-string")] - (is (= {:uri "some-string"} actual)))) - (testing "a contact string" - (with-redefs [resources/contacts {:test "something"}] - (let [actual (photos/source "contacts://test")] - (is (= "something" actual)))))) diff --git a/test/cljs/status_im/test/contacts/events.cljs b/test/cljs/status_im/test/contacts/events.cljs deleted file mode 100644 index c6d1686dc2d..00000000000 --- a/test/cljs/status_im/test/contacts/events.cljs +++ /dev/null @@ -1,436 +0,0 @@ -(ns status-im.test.contacts.events - (:require [cljs.test :refer-macros [deftest is testing]] - reagent.core - [re-frame.core :as rf] - [day8.re-frame.test :refer-macros [run-test-sync]] - status-im.ui.screens.db - [status-im.ui.screens.contacts.events :as contacts-events] - status-im.ui.screens.subs - [status-im.ui.screens.events :as events] - [status-im.utils.js-resources :as js-res])) - -(def test-contact-group - {:group-id "1501682106404-685e041e-38e7-593e-b42c-fb4cabd7faa4" - :name "Test" - :timestamp 0 - :order 0 - :pending? false - :contacts (list - {:identity "bchat"} - {:identity "Commiteth"} - {:identity "demo-bot"})}) - -(def dapps-contact-group - {:group-id "dapps" - :name "ÐApps" - :order 1 - :timestamp 0 - :contacts [{:identity "oaken-water-meter"} - {:identity "melonport"} - {:identity "bchat"} - {:identity "Dentacoin"} - {:identity "Augur"} - {:identity "Ethlance"} - {:identity "Commiteth"}] - :pending? false}) - -(def demo-bot-contact - {:address nil - :name "Demo bot" - :description nil - :hide-contact? false - :dapp-hash nil - :photo-path nil - :dapp-url nil - :bot-url "local://demo-bot" - :whisper-identity "demo-bot" - :dapp? true - :pending? false - :unremovable? false - :public-key nil}) - -(def console-contact - {:whisper-identity "console" - :name "status-console" - :photo-path "contacts://console" - :dapp? true - :unremovable? true - :bot-url "local://console-bot" - :status "intro-status" - :pending? false}) - -(defn test-fixtures [] - (rf/reg-fx ::events/init-store #()) - - (rf/reg-fx :data-store/save-all-contacts #()) - (rf/reg-fx ::contacts-events/save-contact #()) - (rf/reg-fx ::contacts-events/watch-contact #()) - (rf/reg-fx ::contacts-events/stop-watching-contact #()) - (rf/reg-fx ::contacts-events/send-contact-request-fx #()) - - (rf/reg-fx :data-store/save-chat #()) - - (rf/reg-cofx - :data-store/get-all-contacts - (fn [coeffects _] - (assoc coeffects :all-contacts []))) - - (rf/reg-cofx - :data-store/get-local-storage-data - (fn [cofx] - (assoc cofx :get-local-storage-data (constantly nil)))) - - ;;TODO implement tests later for :add-chat? and :bot-url - (rf/reg-cofx - ::contacts-events/get-default-contacts-and-groups - (fn [coeffects _] - (assoc coeffects :default-contacts (select-keys js-res/default-contacts [:demo-bot]))))) - -#_(deftest contacts-events - "load-contacts - load-contact-groups - load-default-contacts (add-contact-groups, add-contacts, add-contacts-to-group ;TODO add-chat, load-commands!) - add-contact-handler (add-new-contact-and-open-chat, status-im.contacts.events/add-new-contact, - status-im.contacts.events/send-contact-request ;TODO start-chat) - contact-request-received (update-contact, watch-contact ;TODO :update-chat!) - contact-update-received (update-contact ;TODO :update-chat!) - hide-contact (update-contact ;TODO :account-update-keys) - add-contact-handler (add-contact, status-im.contacts.events/add-new-contact - status-im.contacts.events/send-contact-request ;TODO :discoveries-send-portions) - - create-new-contact-group - set-contact-group-name - save-contact-group-order - add-selected-contacts-to-group - remove-contact-from-group - add-contacts-to-group - delete-contact-group" - - (testing "watch-contact" - (let [contact {:public-key "public-key" - :whisper-identity "whisper-identity"} - actual-fx (-> {:db {:web3 "web3"}} - (contacts-events/watch-contact contact) - ::contacts-events/watch-contact)] - - (testing "it adds a ::watch-contact effect" - (is (not (nil? actual-fx)))) - - (testing "it adds web3" - (is (= "web3" (:web3 actual-fx)))) - - (testing "it adds the watched-contact whisper-identity" - (is (= "whisper-identity" (:whisper-identity actual-fx)))) - - (testing "it adds the public key" - (is (= "public-key" (:public-key actual-fx)))))) - - (testing "send-contact-request" - (let [contact {:whisper-identity "contact-whisper-identity"} - account {:name "name" - :photo-path "photo-path" - :status "status"} - accounts {"current-account-id" account} - db {:accounts/accounts accounts - :accounts/current-account-id "current-account-id" - :web3 "web3" - :current-public-key "current-public-key" - :notifications {:fcm-token "fcm-token"}} - actual-fx (-> {:db db} - (contacts-events/send-contact-request contact) - ::contacts-events/send-contact-request-fx)] - - (testing "it adds a ::send-contact-request-fx effect" - (is (not (nil? actual-fx)))) - - (testing "it adds the current-public-key" - (is (= "current-public-key" (:current-public-key actual-fx)))) - - (testing "it adds web3" - (is (= "web3" (:web3 actual-fx)))) - - (testing "it adds the current-account-id" - (is (= "current-account-id" (:current-account-id actual-fx)))) - - (testing "it adds the fcm-token" - (is (= "fcm-token" (:fcm-token actual-fx)))) - - (testing "it adds the whisper-identity of the contact" - (is (= "contact-whisper-identity" (:whisper-identity actual-fx)))) - - (testing "it adds the current-account information" - (is (= account (select-keys actual-fx [:name - :photo-path - :status])))))) - - (run-test-sync - - (test-fixtures) - - (rf/dispatch [:initialize-db]) - - (def contacts (rf/subscribe [:get-contacts])) - (def contact-groups (rf/subscribe [:get-contact-groups])) - (def view-id (rf/subscribe [:get :view-id])) - - (testing ":load-contacts event" - - ;;Assert the initial state - (is (and (map? @contacts) (empty? @contacts))) - (rf/dispatch [:load-contacts])) - - (testing ":load-contact-groups event" - - ;;Assert the initial state - (is (and (map? @contact-groups) (empty? @contact-groups))) - (rf/dispatch [:load-contact-groups]) - (is (= {(:group-id test-contact-group) test-contact-group} - @contact-groups))) - - (testing ":load-default-contacts! event" - - ;; :load-default-contacts! event dispatches next 5 events - ;; - ;; :add-contact-groups - ;; :add-contacts - ;; :add-contacts-to-group - ;;TODO :add-chat - ;;TODO :load-commands! - (rf/dispatch [:load-default-contacts!]) - - (rf/dispatch [:set-in [:group/contact-groups "dapps" :timestamp] 0]) - - (is (= {"dapps" dapps-contact-group - (:group-id test-contact-group) test-contact-group} - @contact-groups)) - - (testing "it adds a default contact" - (is (= demo-bot-contact (get @contacts "demo-bot")))) - - (testing "it adds the console bot" - (is (= console-contact (get @contacts "console")))) - - (testing "it does not add any other contact" - (is (= 2 (count (keys @contacts)))))) - - (def new-contact-public-key "0x048f7d5d4bda298447bbb5b021a34832509bd1a8dbe4e06f9b7223d00a59b6dc14f6e142b21d3220ceb3155a6d8f40ec115cd96394d3cc7c55055b433a1758dc74") - (def new-contact-address "5392ccb49f2e9fef8b8068b3e3b5ba6c020a9aca") - (def new-contact {:name "" - :photo-path "" - :whisper-identity new-contact-public-key - :address new-contact-address - :pending? false}) - (def contact (rf/subscribe [:contact-by-identity new-contact-public-key])) - (def current-chat-id (rf/subscribe [:get-current-chat-id])) - - (testing ":add-contact-handler event - new contact" - - (rf/dispatch [:set :view-id nil]) - (rf/dispatch [:set :current-chat-id nil]) - - ;; :add-contact-handler event dispatches next 4 events for new contact - ;; - ;; :add-new-contact-and-open-chat - ;; :status-im.contacts.events/add-new-contact - ;; :status-im.contacts.events/send-contact-request - ;;TODO :start-chat - - (rf/dispatch [:set :contacts/new-identity new-contact-public-key]) - (rf/dispatch [:add-contact-handler]) - - (testing "it returns the new contact from the contact-by-identity sub" - (is (= new-contact (assoc @contact :photo-path "" :name "")))) - - (testing "it adds the new contact to the list of contacts" - (is (= new-contact - (-> @contacts - (get new-contact-public-key) - (assoc :photo-path "" :name ""))))) - - (testing "it loads the 1-1 chat" - (is (= :chat @view-id))) - - (testing "it adds the new contact to the chat" - (is (= new-contact-public-key @current-chat-id)))) - - (testing ":contact-request-received event" - - ;; :contact-request-received event dispatches next 3 events - ;; - ;; :update-contact! - ;; :watch-contact - ;;TODO :update-chat! - (rf/reg-event-db :update-chat! (fn [db _] db)) - (def received-contact {:name "test" - :profile-image "" - :address new-contact-address - :status "test status" - :fcm-token "0xwhatever"}) - (def received-contact1 (merge new-contact - (dissoc received-contact :profile-image) - {:public-key new-contact-public-key})) - - (rf/dispatch [:contact-request-received {:from new-contact-public-key - :payload {:contact received-contact}}]) - - (testing "it adds the new contact to the list of contacts" - (is (= received-contact1 - (get @contacts new-contact-public-key))))) - - (testing ":contact-update-received event" - - ;; :contact-update-received event dispatches next 2 events - ;; - ;; :update-contact! - ;;TODO :update-chat! - (let [timestamp (datetime/timestamp)] - (def received-contact2 (assoc received-contact1 - :last-updated timestamp - :status "new status" - :name "new name")) - - (rf/dispatch [:contact-update-received {:from new-contact-public-key - :payload {:content {:profile {:profile-image "" - :status "new status" - :name "new name"}} - :timestamp timestamp}}])) - - (testing "it updates the contact and set the :last-updated key" - (is (= received-contact2 - (get @contacts new-contact-public-key))))) - - (testing ":hide-contact event" - - ;; :hide-contact event dispatches next 2 events - ;; - ;; :update-contact! - ;;TODO :account-update-keys - (rf/reg-event-db :account-update-keys (fn [db _] db)) - - (rf/dispatch [:hide-contact @contact]) - - (testing "it sets the pending? flag to true" - (is (= (assoc received-contact2 :pending? true) - (get @contacts new-contact-public-key))))) - - (testing ":add-contact-handler event - :add-contact" - - ;; :add-contact-handler event dispatches next 4 events - ;; - ;; :add-contact - ;; :status-im.contacts.events/add-new-contact - ;; :status-im.contacts.events/send-contact-request - ;;TODO :discoveries-send-portions - (rf/reg-event-db :discoveries-send-portions (fn [db _] db)) - - (rf/dispatch [:set :view-id nil]) - (rf/dispatch [:set :current-chat-id nil]) - (rf/dispatch [:set :contacts/new-identity new-contact-public-key]) - (rf/dispatch [:add-contact-handler]) - - (testing "it sets the pending? flag to false" - (is (= (assoc received-contact2 :pending? false) - (get @contacts new-contact-public-key)))) - - (testing "it loads the 1-1 chat" - (is (= :chat @view-id))) - - (testing "it adds the new contact to the chat" - (is (= new-contact-public-key @current-chat-id)))) - - (testing ":create-new-contact-group event" - - (def new-group-name "new group") - (rf/dispatch [:select-contact new-contact-public-key]) - (rf/dispatch [:select-contact "demo-bot"]) - (rf/dispatch [:select-contact "browse"]) - (rf/dispatch [:deselect-contact "browse"]) - - (rf/dispatch [:create-new-contact-group new-group-name]) - - (rf/dispatch [:deselect-contact "demo-bot"]) - (rf/dispatch [:deselect-contact new-contact-public-key]) - (def new-group-id (->> @contact-groups - (vals) - (filter #(= (:name %) new-group-name)) - (first) - (:group-id))) - (def new-group {:group-id new-group-id - :name new-group-name - :order 2 - :timestamp 0 - :contacts [{:identity new-contact-public-key} - {:identity "demo-bot"}]}) - (def groups-with-new-group {new-group-id new-group - "dapps" dapps-contact-group - (:group-id test-contact-group) test-contact-group}) - (def groups-with-new-group1 (update groups-with-new-group new-group-id assoc :name "new group name")) - (def groups-with-new-group2 (-> groups-with-new-group1 - (update new-group-id assoc :order 1) - (update "dapps" assoc :order 2))) - - (rf/dispatch [:set-in [:group/contact-groups new-group-id :timestamp] 0]) - - (is (= groups-with-new-group @contact-groups))) - - (testing ":set-contact-group-name event" - - (rf/reg-event-db ::prepare-group-name - (fn [db _] (assoc db - :new-chat-name "new group name" - :group/contact-group-id new-group-id))) - - (rf/dispatch [::prepare-group-name]) - (rf/dispatch [:set-contact-group-name]) - - (is (= groups-with-new-group1 @contact-groups))) - - (testing ":save-contact-group-order event" - - (rf/reg-event-db ::prepare-groups-order - (fn [db _] - (assoc db :group/groups-order - (->> (vals (:group/contact-groups db)) - (remove :pending?) - (sort-by :order >) - (map :group-id))))) - - (rf/dispatch [::prepare-groups-order]) - (rf/dispatch [:change-contact-group-order 1 0]) - (rf/dispatch [:save-contact-group-order]) - - (is (= groups-with-new-group2 @contact-groups))) - - (testing ":add-selected-contacts-to-group event" - - (rf/dispatch [:select-contact "browse"]) - (rf/dispatch [:add-selected-contacts-to-group]) - (rf/dispatch [:deselect-contact "browse"]) - - (is (= (update groups-with-new-group2 new-group-id assoc :contacts [{:identity new-contact-public-key} - {:identity "demo-bot"} - {:identity "browse"}]) - @contact-groups))) - - (testing ":remove-contact-from-group event" - - (rf/dispatch [:remove-contact-from-group "browse" new-group-id]) - - (is (= groups-with-new-group2 @contact-groups))) - (testing ":add-contacts-to-group event" - - (rf/dispatch [:add-contacts-to-group new-group-id ["browse"]]) - - (is (= (update groups-with-new-group2 new-group-id assoc :contacts [{:identity new-contact-public-key} - {:identity "demo-bot"} - {:identity "browse"}]) - @contact-groups)) - - (rf/dispatch [:remove-contact-from-group "browse" new-group-id])) - - (testing ":delete-contact-group event" - - (rf/dispatch [:delete-contact-group]) - - (is (= (update groups-with-new-group2 new-group-id assoc :pending? true) - @contact-groups))))) diff --git a/test/cljs/status_im/test/contacts/subs.cljs b/test/cljs/status_im/test/contacts/subs.cljs deleted file mode 100644 index 1de2a0aa916..00000000000 --- a/test/cljs/status_im/test/contacts/subs.cljs +++ /dev/null @@ -1,104 +0,0 @@ -(ns status-im.test.contacts.subs - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.identicon :as identicon] - [status-im.ui.screens.contacts.subs :as contacts-subs])) - -(deftest contacts-subs - (testing "get-all-contacts-in-group-chat" - (with-redefs [identicon/identicon (constantly "generated")] - (let [chat-contact-ids ["0x04fcf40c526b09ff9fb22f4a5dbd08490ef9b64af700870f8a0ba2133f4251d5607ed83cd9047b8c2796576bc83fa0de23a13a4dced07654b8ff137fe744047917" - "0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"] - group-admin-id "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f" - contacts {"demo-bot" - {:description nil, - :last-updated 0, - :hide-contact? false, - :address nil, - :name "Demo bot", - :jail-loaded? true, - :fcm-token nil, - :dapp-url nil, - :dapp-hash nil, - :photo-path nil, - :debug? false, - :status nil, - :bot-url "local://demo-bot", - :pending? false, - :whisper-identity "demo-bot", - :last-online 0, - :dapp? true, - :public-key nil}, - "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f" - {:description nil, - :last-updated 0, - :hide-contact? false, - :address "eca8218b5ebeb2c47ba94c1b6e0a779d78fff7bc", - :name "User B", - :fcm-token nil, - :dapp-url nil, - :dapp-hash nil, - :photo-path "photo1", - :debug? false, - :status nil, - :bot-url nil, - :pending? true, - :whisper-identity - "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f", - :last-online 0, - :dapp? false, - :public-key - "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f"}, - "cryptokitties" - {:description "Collect and breed adorable digital cats.", - :last-updated 0, - :hide-contact? false, - :address nil, - :name "CryptoKitties", - :fcm-token nil, - :dapp-url "https://www.cryptokitties.co", - :dapp-hash nil, - :photo-path "contacts://cryptokitties", - :debug? false, - :status nil, - :bot-url nil, - :pending? false, - :whisper-identity "cryptokitties", - :last-online 0, - :dapp? true, - :public-key nil}} - current-account {:last-updated 0, - :address "f23d28f538fd8cd4a90c2d96ca89f5bccca5383f", - :email nil, - :signed-up? true, - :sharing-usage-data? false, - :name "User A", - :photo-path "photo2", - :public-key - "0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"}] - (is (= (contacts-subs/get-all-contacts-in-group-chat chat-contact-ids - group-admin-id - contacts - current-account) - [{:name "Snappy Impressive Leonberger" - :photo-path "generated" - :whisper-identity "0x04fcf40c526b09ff9fb22f4a5dbd08490ef9b64af700870f8a0ba2133f4251d5607ed83cd9047b8c2796576bc83fa0de23a13a4dced07654b8ff137fe744047917"} - {:name "User A" - :photo-path "photo2" - :whisper-identity "0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"} - {:description nil - :last-updated 0 - :hide-contact? false - :address "eca8218b5ebeb2c47ba94c1b6e0a779d78fff7bc" - :name "User B" - :fcm-token nil - :dapp-url nil - :dapp-hash nil - :photo-path "photo1" - :debug? false - :status nil - :bot-url nil - :pending? true - :whisper-identity "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f" - :last-online 0 - :dapp? false - :public-key "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f"}])))))) diff --git a/test/cljs/status_im/test/data_store/realm/core.cljs b/test/cljs/status_im/test/data_store/realm/core.cljs deleted file mode 100644 index d1c38e06511..00000000000 --- a/test/cljs/status_im/test/data_store/realm/core.cljs +++ /dev/null @@ -1,62 +0,0 @@ -(ns status-im.test.data-store.realm.core - (:require [cljs.test :refer-macros [deftest is testing use-fixtures]] - [status-im.utils.utils :as utils] - [clojure.string :as string] - [status-im.data-store.realm.core :as core])) - -(def migrated-realm? (atom nil)) - -(defn fixtures [f] - (reset! migrated-realm? nil) - (f)) - -(def valid-account-path - (str "/some/" (string/join (repeat 40 "a")))) - -(use-fixtures :each fixtures) - -(deftest migrate-realm - (with-redefs [core/open-realm #(reset! migrated-realm? true)] - (testing "the database does not exists" - (with-redefs [core/encrypted-realm-version (constantly -1)] - (core/migrate-realm "test-filename" [] "encryption-key") - (testing "it migrates the db" - (is @migrated-realm?)))) - (testing "the database exists" - (with-redefs [core/encrypted-realm-version (constantly 2)] - (core/migrate-realm "test-filename" [] "encryption-key") - (testing "it migrates the db" - (is @migrated-realm?)))))) - -(deftest is-account-file-test - (testing "not an account file" - (is (not (core/is-account-file? "not-one"))) - (is (not (core/is-account-file? "000000000000000009212102")))) - (testing "an account file" - (is (core/is-account-file? valid-account-path)))) - -(deftest is-realm-file-test - (testing "not a realm file" - (is (not (core/is-realm-file? "not-one"))) - (is (not (core/is-realm-file? "000000000000000009212102")))) - (testing "realm files" - (is (core/is-realm-file? "/some/new-account")) - (is (core/is-realm-file? "/some/new-account.lock")) - (is (core/is-realm-file? "/some/new-account.management")) - (is (core/is-realm-file? "/some/new-account.note")) - (is (core/is-realm-file? "/some/default.realm")) - (is (core/is-realm-file? valid-account-path)))) - -(deftest realm-management-file-test - (testing "not a management file" - (is (not (core/realm-management-file? "new-account")))) - (testing "management file" - (is (core/realm-management-file? "anything.management")) - (is (core/realm-management-file? "anything.lock")) - (is (core/realm-management-file? "anything.note")))) - -(deftest serialization - (is (nil? (core/deserialize ""))) - (is (nil? (core/deserialize "giberrish"))) - (is (nil? (core/deserialize nil))) - (is (nil? (core/deserialize (core/serialize nil))))) diff --git a/test/cljs/status_im/test/i18n.cljs b/test/cljs/status_im/test/i18n.cljs deleted file mode 100644 index 02334357a8e..00000000000 --- a/test/cljs/status_im/test/i18n.cljs +++ /dev/null @@ -1,29 +0,0 @@ -(ns status-im.test.i18n - (:require [cljs.test :refer-macros [deftest is]] - [status-im.i18n :as i18n] - [clojure.set :as set] - [cljs.spec.alpha :as spec])) - -(deftest label-options - (is (not (nil? (:key (i18n/label-options {:key nil})))))) - -(deftest locales-only-have-existing-tran-ids - (is (spec/valid? ::i18n/labels (i18n/labels-for-all-locales)) - (->> i18n/locales - (remove #(spec/valid? ::i18n/labels (i18n/locale->labels %))) - (map (fn [l] - (str "Extra translations in locale " l "\n" - (set/difference (i18n/locale->labels l) i18n/labels) - "\n\n"))) - (apply str)))) - -(deftest supported-locales-are-actually-supported - (is (set/subset? i18n/supported-locales (i18n/actual-supported-locales)) - (->> i18n/supported-locales - (remove i18n/locale-is-supported-based-on-translations?) - (map (fn [l] - (str "Missing translations in supported locale " l "\n" - (set/difference (i18n/checkpoint->labels i18n/checkpoint-to-consider-locale-supported) - (i18n/locale->labels l)) - "\n\n"))) - (apply str)))) diff --git a/test/cljs/status_im/test/models/account.cljs b/test/cljs/status_im/test/models/account.cljs deleted file mode 100644 index e0d65053811..00000000000 --- a/test/cljs/status_im/test/models/account.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.test.models.account - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.models.account :as model])) - -(deftest logged-in-test - (testing "account/account is defined" - (is (model/logged-in? {:db {:account/account {}}}))) - (testing "account/account is not there" - (is (not (model/logged-in? {:db {}}))))) diff --git a/test/cljs/status_im/test/models/bootnode.cljs b/test/cljs/status_im/test/models/bootnode.cljs deleted file mode 100644 index dcd5fc7be46..00000000000 --- a/test/cljs/status_im/test/models/bootnode.cljs +++ /dev/null @@ -1,167 +0,0 @@ -(ns status-im.test.models.bootnode - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.models.bootnode :as model])) - -(def bootnode-id "1da276e34126e93babf24ec88aac1a7602b4cbb2e11b0961d0ab5e989ca9c261aa7f7c1c85f15550a5f1e5a5ca2305b53b9280cf5894d5ecf7d257b173136d40") -(def host "167.99.209.61:30504") - -(def valid-bootnode-address (str "enode://" bootnode-id "@" host)) - -(deftest upsert-bootnode - (testing "adding a new bootnode" - (let [new-bootnode {:name {:value "name"} - :url {:value "url"}} - expected {"mainnet_rpc" {"someid" {:name "name" - :address "url" - :chain "mainnet_rpc" - :id "someid"}}} - actual (model/upsert - {:random-id "some-id" - :db {:bootnodes/manage new-bootnode - :network "mainnet_rpc" - :account/account {}}})] - (is (= expected (get-in actual [:db :account/account :bootnodes]))))) - (testing "adding an existing bootnode" - (let [new-bootnode {:id {:value "a"} - :name {:value "new-name"} - :url {:value "new-url"}} - expected {"mainnet_rpc" {"a" {:name "new-name" - :address "new-url" - :chain "mainnet_rpc" - :id "a"}}} - actual (model/upsert - {:random-id "some-id" - :db {:bootnodes/manage new-bootnode - :network "mainnet_rpc" - :account/account {:bootnodes - {"mainnet_rpc" - {"a" {:name "name" - :address "url" - :chain "mainnet_rpc" - :id "a"}}}}}})] - (is (= expected (get-in actual [:db :account/account :bootnodes])))))) - -(deftest set-input-bootnode - (testing "it validates names" - (testing "correct name" - (is (= {:db {:bootnodes/manage {:name {:value "value" - :error false}}}} - (model/set-input :name "value" {})))) - (testing "blank name" - (is (= {:db {:bootnodes/manage {:name {:value "" - :error true}}}} - (model/set-input :name "" {}))))) - (testing "it validates bootnode url" - (testing "correct url" - (is (= {:db {:bootnodes/manage {:url {:value valid-bootnode-address - :error false}}}} - (model/set-input :url valid-bootnode-address {})))) - (testing "broken url" - (is (= {:db {:bootnodes/manage {:url {:value "broken" - :error true}}}} - (model/set-input :url "broken" {})))))) - -(deftest edit-bootnode - (let [db {:network "mainnet_rpc" - :account/account - {:bootnodes - {"mainnet_rpc" - {"a" {:id "a" - :name "name" - :address valid-bootnode-address}}}}} - cofx {:db db}] - (testing "when no id is given" - (let [actual (model/edit nil cofx)] - (testing "it resets :bootnodes/manage" - (is (= {:id {:value nil - :error false} - :url {:value "" - :error true} - :name {:value "" - :error true}} - (-> actual :db :bootnodes/manage)))) - (testing "it navigates to edit-bootnode view" - (is (= [:navigate-to :edit-bootnode] - (-> actual :dispatch)))))) - (testing "when an id is given" - (testing "when the bootnode is in the list" - (let [actual (model/edit "a" cofx)] - (testing "it populates the fields with the correct values" - (is (= {:id {:value "a" - :error false} - :url {:value valid-bootnode-address - :error false} - :name {:value "name" - :error false}} - (-> actual :db :bootnodes/manage)))) - (testing "it navigates to edit-bootnode view" - (is (= [:navigate-to :edit-bootnode] - (-> actual :dispatch)))))) - (testing "when the bootnode is not in the list" - (let [actual (model/edit "not-existing" cofx)] - (testing "it populates the fields with the correct values" - (is (= {:id {:value nil - :error false} - :url {:value "" - :error true} - :name {:value "" - :error true}} - (-> actual :db :bootnodes/manage)))) - (testing "it navigates to edit-bootnode view" - (is (= [:navigate-to :edit-bootnode] - (-> actual :dispatch))))))))) - -(deftest fetch-bootnode - (testing "it fetches the bootnode from the db" - (let [cofx {:db {:network "mainnet_rpc" - :account/account {:bootnodes {"mainnet_rpc" - {"a" {:id "a" - :name "name" - :address "enode://old-id:old-password@url:port"}}}}}}] - (is (model/fetch "a" cofx))))) - -(deftest custom-bootnodes-in-use? - (testing "is on the same network" - (testing "it returns false when not enabled" - (is (not (model/custom-bootnodes-in-use? {:db {:network "mainnet_rpc"}})))) - (testing "it returns true when enabled" - (is (model/custom-bootnodes-in-use? - {:db {:network "mainnet_rpc" - :account/account {:settings - {:bootnodes - {"mainnet_rpc" true}}}}})))) - (testing "is on a different network" - (testing "it returns false when not enabled" - (is (not (model/custom-bootnodes-in-use? {:db {:network "testnet_rpc"}})))) - (testing "it returns true when enabled" - (is (not (model/custom-bootnodes-in-use? - {:db {:network "testnet_rpc" - :account/account {:settings - {:bootnodes - {"mainnnet_rpc" true}}}}})))))) - -(deftest delete-bootnode - (testing "non existing bootnode" - (let [cofx {:db {:network "mainnet_rpc" - :account/account {:bootnodes {"mainnet_rpc" - {"a" {:id "a" - :name "name" - :address "enode://old-id:old-password@url:port"}}} - :settings {:bootnodes - {"mainnnet_rpc" true}}}}} - actual (model/delete "b" cofx)] - (testing "it does not removes the bootnode" - (is (model/fetch "a" actual))))) - (testing "existing bootnode" - (let [cofx {:db {:network "mainnet_rpc" - :account/account {:bootnodes {"mainnet_rpc" - {"a" {:id "a" - :name "name" - :address "enode://old-id:old-password@url:port"}}} - :settings {:bootnodes - {"mainnnet_rpc" true}}}}} - actual (model/delete "a" cofx)] - (testing "it removes the bootnode" - (is (not (model/fetch "a" actual)))) - (testing "it updates the db" - (is (= 1 (count (:data-store/base-tx actual)))))))) diff --git a/test/cljs/status_im/test/models/contact.cljs b/test/cljs/status_im/test/models/contact.cljs deleted file mode 100644 index 3e54452c929..00000000000 --- a/test/cljs/status_im/test/models/contact.cljs +++ /dev/null @@ -1,141 +0,0 @@ -(ns status-im.test.models.contact - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.models.contact :as model])) - -(def public-key "0x04fcf40c526b09ff9fb22f4a5dbd08490ef9b64af700870f8a0ba2133f4251d5607ed83cd9047b8c2796576bc83fa0de23a13a4dced07654b8ff137fe744047917") -(def address "71adb0644e2b590e37dafdfea8bd58f0c7668c7f") - -(deftest can-add-to-contact-test - (testing "a user is already in contacts" - (is (not (model/can-add-to-contacts? {:pending? false})))) - (testing "a user is pending" - (testing "a normal user" - (is (model/can-add-to-contacts? {:pending? true}))) - (testing "a dapp" - (is (not (model/can-add-to-contacts? {:pending? true - :dapp? true}))))) - (testing "the user is not in the contacts" - (testing "a normal user" - (is (model/can-add-to-contacts? {}))) - (testing "a dapp" - (is (not (model/can-add-to-contacts? {:dapp? true})))))) - -(deftest handle-contact-update-test - (testing "the contact is not in contacts" - (let [actual (model/handle-contact-update - public-key - 1 - {:name "name" - :profile-image "image" - :address "address" - :fcm-token "token"} - {}) - contact (get-in actual [:db :contacts/contacts public-key])] - (testing "it stores the contact in the database" - (is (:data-store/tx actual))) - (testing "it adds a new contact with pending? true" - (is (= {:whisper-identity public-key - :public-key public-key - :photo-path "image" - :name "name" - :last-updated 1000 - :pending? true - :fcm-token "token" - :address "address"} contact))))) - (testing "the contact is already in contacts" - (testing "timestamp is greather than last-updated" - (let [actual (model/handle-contact-update - public-key - 1 - {:name "new-name" - :profile-image "new-image" - :address "new-address" - :fcm-token "new-token"} - {:db {:contacts/contacts - {public-key {:whisper-identity public-key - :public-key public-key - :photo-path "old-image" - :name "old-name" - :last-updated 0 - :pending? false - :fcm-token "old-token" - :address "old-address"}}}}) - contact (get-in actual [:db :contacts/contacts public-key])] - (testing "it stores the contact in the database" - (is (:data-store/tx actual))) - (testing "it updates the contact leaving pending unchanged" - (is (= {:whisper-identity public-key - :public-key public-key - :photo-path "new-image" - :name "new-name" - :last-updated 1000 - :pending? false - :fcm-token "new-token" - :address "new-address"} contact))))) - (testing "timestamp is equal than last-updated" - (let [actual (model/handle-contact-update - public-key - 1 - {:name "new-name" - :profile-image "new-image" - :address "new-address" - :fcm-token "new-token"} - {:db {:contacts/contacts - {public-key {:whisper-identity public-key - :public-key public-key - :photo-path "old-image" - :name "old-name" - :last-updated 1000 - :pending? false - :fcm-token "old-token" - :address "old-address"}}}}) - contact (get-in actual [:db :contacts/contacts public-key])] - (testing "it does nothing" - (is (nil? actual))))) - (testing "timestamp is less than last-updated" - (let [actual (model/handle-contact-update - public-key - 0 - {:name "new-name" - :profile-image "new-image" - :address "new-address" - :fcm-token "new-token"} - {:db {:contacts/contacts - {public-key {:whisper-identity public-key - :public-key public-key - :photo-path "old-image" - :name "old-name" - :last-updated 1000 - :pending? false - :fcm-token "old-token" - :address "old-address"}}}}) - contact (get-in actual [:db :contacts/contacts public-key])] - (testing "it does nothing" - (is (nil? actual)))))) - (testing "backward compatibility" - (let [actual (model/handle-contact-update - public-key - 1 - {:name "new-name" - :profile-image "new-image"} - {:db {:contacts/contacts - {public-key {:whisper-identity public-key - :public-key public-key - :photo-path "old-image" - :name "old-name" - :last-updated 0 - :pending? false}}}}) - contact (get-in actual [:db :contacts/contacts public-key])] - (testing "it stores the contact in the database" - (is (:data-store/tx actual))) - (testing "it updates the contact leaving pending unchanged" - (is (= {:whisper-identity public-key - :public-key public-key - :photo-path "new-image" - :name "new-name" - :last-updated 1000 - :pending? false - :address address} contact))))) - (testing "the message is coming from us" - (testing "it does not update contacts" - (is (nil? (model/handle-contact-update "me" 1 {} {:db {:current-public-key "me"}})))))) diff --git a/test/cljs/status_im/test/models/mailserver.cljs b/test/cljs/status_im/test/models/mailserver.cljs deleted file mode 100644 index 6f98ad361b2..00000000000 --- a/test/cljs/status_im/test/models/mailserver.cljs +++ /dev/null @@ -1,287 +0,0 @@ -(ns status-im.test.models.mailserver - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.models.mailserver :as model])) - -(def enode-id "1da276e34126e93babf24ec88aac1a7602b4cbb2e11b0961d0ab5e989ca9c261aa7f7c1c85f15550a5f1e5a5ca2305b53b9280cf5894d5ecf7d257b173136d40") -(def password "password") -(def host "167.99.209.61:30504") - -(def valid-enode-address (str "enode://" enode-id "@" host)) -(def valid-enode-url (str "enode://" enode-id ":" password "@" host)) - -(deftest valid-enode-address-test - (testing "url without password" - (let [address "enode://1da276e34126e93babf24ec88aac1a7602b4cbb2e11b0961d0ab5e989ca9c261aa7f7c1c85f15550a5f1e5a5ca2305b53b9280cf5894d5ecf7d257b173136d40@167.99.209.61:30504"] - (is (model/valid-enode-address? address)))) - (testing "url with password" - (let [address "enode://1da276e34126e93babf24ec88aac1a7602b4cbb2e11b0961d0ab5e989ca9c261aa7f7c1c85f15550a5f1e5a5ca2305b53b9280cf5894d5ecf7d257b173136d40:somepasswordwith@and:@@167.99.209.61:30504"] - (is (not (model/valid-enode-address? address))))) - (testing "invalid url" - (is (not (model/valid-enode-address? "something not valid"))))) - -(deftest valid-enode-url-test - (testing "url without password" - (let [address "enode://1da276e34126e93babf24ec88aac1a7602b4cbb2e11b0961d0ab5e989ca9c261aa7f7c1c85f15550a5f1e5a5ca2305b53b9280cf5894d5ecf7d257b173136d40@167.99.209.61:30504"] - (is (not (model/valid-enode-url? address))))) - (testing "url with password" - (let [address "enode://1da276e34126e93babf24ec88aac1a7602b4cbb2e11b0961d0ab5e989ca9c261aa7f7c1c85f15550a5f1e5a5ca2305b53b9280cf5894d5ecf7d257b173136d40:somepasswordwith@and:@@167.99.209.61:30504"] - (is (model/valid-enode-url? address)))) - (testing "invalid url" - (is (not (model/valid-enode-url? "something not valid"))))) - -(deftest address->mailserver - (testing "with password" - (let [address "enode://some-id:the-password@206.189.56.154:30504"] - (is (= {:address "enode://some-id@206.189.56.154:30504" - :password "the-password" - :user-defined true} - (model/address->mailserver address))))) - (testing "without password" - (let [address "enode://some-id@206.189.56.154:30504"] - (is (= {:address "enode://some-id@206.189.56.154:30504" - :user-defined true} - (model/address->mailserver address)))))) - -(deftest set-input - (testing "it validates names" - (testing "correct name" - (is (= {:db {:mailservers/manage {:name {:value "value" - :error false}}}} - (model/set-input :name "value" {})))) - (testing "blank name" - (is (= {:db {:mailservers/manage {:name {:value "" - :error true}}}} - (model/set-input :name "" {}))))) - (testing "it validates enodes url" - (testing "correct url" - (is (= {:db {:mailservers/manage {:url {:value valid-enode-url - :error false}}}} - (model/set-input :url valid-enode-url {})))) - (testing "broken url" - (is (= {:db {:mailservers/manage {:url {:value "broken" - :error true}}}} - (model/set-input :url "broken" {})))))) - -(deftest edit-mailserver - (let [db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :inbox/wnodes - {:mainnet {"a" {:id "a" - :address valid-enode-address - :password password - :name "name"}}}} - cofx {:db db}] - (testing "when no id is given" - (let [actual (model/edit nil cofx)] - (testing "it resets :mailserver/manage" - (is (= {:id {:value nil - :error false} - :url {:value "" - :error true} - :name {:value "" - :error true}} - (-> actual :db :mailservers/manage)))) - (testing "it navigates to edit-mailserver view" - (is (= [:navigate-to :edit-mailserver] - (-> actual :dispatch)))))) - (testing "when an id is given" - (testing "when the wnode is in the list" - (let [actual (model/edit "a" cofx)] - (testing "it populates the fields with the correct values" - (is (= {:id {:value "a" - :error false} - :url {:value valid-enode-url - :error false} - :name {:value "name" - :error false}} - (-> actual :db :mailservers/manage)))) - (testing "it navigates to edit-mailserver view" - (is (= [:navigate-to :edit-mailserver] - (-> actual :dispatch)))))) - (testing "when the wnode is not in the list" - (let [actual (model/edit "not-existing" cofx)] - (testing "it populates the fields with the correct values" - (is (= {:id {:value nil - :error false} - :url {:value "" - :error true} - :name {:value "" - :error true}} - (-> actual :db :mailservers/manage)))) - (testing "it navigates to edit-mailserver view" - (is (= [:navigate-to :edit-mailserver] - (-> actual :dispatch))))))))) - -(deftest connected-mailserver - (testing "it returns true when set in inbox/current-id" - (let [cofx {:db {:inbox/current-id "a"}}] - (is (model/connected? "a" cofx)))) - (testing "it returns false otherwise" - (is (not (model/connected? "a" {}))))) - -(deftest fetch-mailserver - (testing "it fetches the mailserver from the db" - (let [cofx {:db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :inbox/wnodes {:mainnet {"a" {:id "a" - :name "old-name" - :address "enode://old-id:old-password@url:port"}}}}}] - (is (model/fetch "a" cofx))))) - -(deftest fetch-current-mailserver - (testing "it fetches the mailserver from the db with corresponding id" - (let [cofx {:db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :inbox/current-id "a" - :inbox/wnodes {:mainnet {"a" {:id "a" - :name "old-name" - :address "enode://old-id:old-password@url:port"}}}}}] - (is (model/fetch-current cofx))))) - -(deftest set-current-mailserver - (with-redefs [rand-nth (comp last sort)] - (let [cofx {:db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :inbox/wnodes {:mainnet {"a" {} - "b" {} - "c" {} - "d" {}}}}}] - (testing "the user has already a preference" - (let [cofx (assoc-in cofx - [:db :account/account :settings] - {:wnode {:mainnet "a"}})] - (testing "the mailserver exists" - (testing "it sets the preferred mailserver" - (is (= "a" (-> (model/set-current-mailserver cofx) - :db - :inbox/current-id))))) - (testing "the mailserver does not exists" - (let [cofx (update-in cofx [:db :inbox/wnodes :mainnet] dissoc "a")] - (testing "sets a random mailserver" - (is (= "d" (-> (model/set-current-mailserver cofx) - :db - :inbox/current-id)))))))) - (testing "the user has not set an explicit preference" - (testing "current-id is not set" - (testing "it sets a random mailserver" - (is (= "d" (-> (model/set-current-mailserver cofx) - :db - :inbox/current-id))))) - (testing "current-id is set" - (testing "it sets the next mailserver" - (is (= "c" (-> (model/set-current-mailserver (assoc-in - cofx - [:db :inbox/current-id] - "b")) - :db - :inbox/current-id))) - (is (= "a" (-> (model/set-current-mailserver (assoc-in - cofx - [:db :inbox/current-id] - "d")) - :db - :inbox/current-id))) - (is (= "a" (-> (model/set-current-mailserver (assoc-in - cofx - [:db :inbox/current-id] - "non-existing")) - :db - :inbox/current-id))))))))) - -(deftest delete-mailserver - (testing "the user is not connected to the mailserver" - (let [cofx {:random-id "random-id" - :db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :inbox/wnodes {:mainnet {"a" {:id "a" - :name "old-name" - :user-defined true - :address "enode://old-id:old-password@url:port"}}}}} - actual (model/delete "a" cofx)] - (testing "it removes the mailserver from the list" - (is (not (model/fetch "a" actual)))) - (testing "it stores it in the db" - (is (= 1 (count (:data-store/tx actual))))))) - (testing "the mailserver is not user-defined" - (let [cofx {:random-id "random-id" - :db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :inbox/wnodes {:mainnet {"a" {:id "a" - :name "old-name" - :address "enode://old-id:old-password@url:port"}}}}} - actual (model/delete "a" cofx)] - (testing "it does not delete the mailserver" - (is (nil? actual))))) - (testing "the user is connected to the mailserver" - (let [cofx {:random-id "random-id" - :db {:network "mainnet_rpc" - :account/account {:settings {:wnode {:mainnet "a"}} - :networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :inbox/wnodes {:mainnet {"a" {:id "a" - :name "old-name" - :address "enode://old-id:old-password@url:port"}}}}} - actual (model/delete "a" cofx)] - (testing "it does not remove the mailserver from the list" - (is (nil? actual)))))) - -(deftest upsert-mailserver - (testing "new mailserver" - (let [cofx {:random-id "random-id" - :db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :mailservers/manage {:name {:value "test-name"} - :url {:value "enode://test-id:test-password@url:port"}} - - :inbox/wnodes {}}} - actual (model/upsert cofx)] - - (testing "it adds the enode to inbox/wnodes" - (is (= {:mainnet {"randomid" {:password "test-password" - :address "enode://test-id@url:port" - :name "test-name" - :id "randomid" - :user-defined true}}} - (get-in actual [:db :inbox/wnodes])))) - (testing "it navigates back" - (is (= [:navigate-back] - (:dispatch actual)))) - (testing "it stores it in the db" - (is (= 1 (count (:data-store/tx actual))))))) - (testing "existing mailserver" - (let [cofx {:random-id "random-id" - :db {:network "mainnet_rpc" - :account/account {:networks {"mainnet_rpc" - {:config {:NetworkId 1}}}} - :mailservers/manage {:id {:value "a"} - :name {:value "new-name"} - :url {:value "enode://new-id:new-password@url:port"}} - - :inbox/wnodes {:mainnet {"a" {:id "a" - :name "old-name" - :address "enode://old-id:old-password@url:port"}}}}} - actual (model/upsert cofx)] - (testing "it navigates back" - (is (= [:navigate-back] - (:dispatch actual)))) - (testing "it updates the enode to inbox/wnodes" - (is (= {:mainnet {"a" {:password "new-password" - :address "enode://new-id@url:port" - :name "new-name" - :id "a" - :user-defined true}}} - (get-in actual [:db :inbox/wnodes])))) - (testing "it stores it in the db" - (is (= 1 (count (:data-store/tx actual))))) - (testing "it logs the user out if connected to the current mailserver" - (let [actual (model/upsert (assoc-in cofx - [:db :inbox/current-id] "a"))] - (is (= [:logout] - (-> actual :data-store/tx first :success-event)))))))) diff --git a/test/cljs/status_im/test/models/network.cljs b/test/cljs/status_im/test/models/network.cljs deleted file mode 100644 index 17d554360d1..00000000000 --- a/test/cljs/status_im/test/models/network.cljs +++ /dev/null @@ -1,103 +0,0 @@ -(ns status-im.test.models.network - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.models.network :as model])) - -(deftest valid-rpc-url-test - (testing "nil?" - (is (not (model/valid-rpc-url? nil)))) - (testing "a blank url" - (is (not (model/valid-rpc-url? "")))) - (testing "a url without a protocol" - (is (not (model/valid-rpc-url? "something")))) - (testing "a url without a protocol" - (is (not (model/valid-rpc-url? "http://something with space")))) - (testing "a url without a hostname" - (is (not (model/valid-rpc-url? "https://")))) - (testing "an uppercase HTTP url" - (is (not (model/valid-rpc-url? "HTTP://valid.com")))) - (testing "an http url" - (is (model/valid-rpc-url? "http://valid.com"))) - (testing "an https url" - (is (model/valid-rpc-url? "https://valid.something.else"))) - (testing "a fully qualified url" - (is (model/valid-rpc-url? "https://mainnet.infura.io:6523/z6GCTmjdP3FETEJmMBI4"))) - (testing "an ip address" - (is (model/valid-rpc-url? "https://192.168.1.1"))) - (testing "localhost" - (is (model/valid-rpc-url? "https://localhost"))) - (testing "a fully qualified url, ip address" - (is (model/valid-rpc-url? "https://192.168.1.1:6523/z6GCTmjdP3FETEJmMBI4"))) - (testing "an https url not on the default port" - (is (model/valid-rpc-url? "https://valid.something.else:65323")))) - -(deftest new-network-test - (let [actual (model/new-network {:random-id "random-id"} - "network-name" - "upstream-url" - :mainnet - nil)] - (is (= {:id "randomid" - :name "network-name" - :config {:NetworkId 1 - :DataDir "/ethereum/mainnet_rpc" - :UpstreamConfig {:Enabled true - :URL "upstream-url"}}} - actual)))) - -(deftest new-network-id-test - (let [actual (model/new-network {:random-id "random-id"} - "network-name" - "upstream-url" - :mainnet - "5777")] - (is (= {:id "randomid" - :name "network-name" - :config {:NetworkId 5777 - :DataDir "/ethereum/mainnet_rpc" - :UpstreamConfig {:Enabled true - :URL "upstream-url"}}} - actual)))) - -(deftest valid-manage-test - (testing "a valid manage" - (is (model/valid-manage? {:url {:value "http://valid.com"} - :name {:value "valid"} - :chain {:value "valid"}}))) - (testing "invalid url" - (is (not (model/valid-manage? {:url {:value "invalid"} - :name {:value "valid"} - :chain {:value "valid"}})))) - - (testing "invalid name" - (is (not (model/valid-manage? {:url {:value "http://valid.com"} - :name {:value ""} - :chain {:value "valid"}})))) - - (testing "invalid chain" - (is (not (model/valid-manage? {:url {:value "http://valid.com"} - :name {:value "valid"} - :chain {:value ""}}))))) - -(deftest set-input-test - (testing "it updates and validate a field" - (is (= {:db {:networks/manage {:url {:value "http://valid.com" - :error false} - :name {:value "" - :error true} - :chain {:value "mainnet" - :error false}}}} - (model/set-input :url "http://valid.com" - {:db {:networks/manage {:url {:value "something" - :error true} - :name {:value "" - :error false} - :chain {:value "mainnet" - :error false}}}}))))) - -(deftest save - (testing "it does not save a network with an invalid url" - (is (nil? (model/save {:random-id "random" - :db {:networks/manage {:url {:value "wrong"} - :chain {:value "1"} - :name {:value "empty"}} - :account/account {}}}))))) \ No newline at end of file diff --git a/test/cljs/status_im/test/models/wallet.cljs b/test/cljs/status_im/test/models/wallet.cljs deleted file mode 100644 index 3f150894dc8..00000000000 --- a/test/cljs/status_im/test/models/wallet.cljs +++ /dev/null @@ -1,52 +0,0 @@ -(ns status-im.test.models.wallet - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.money :as money] - [status-im.models.wallet :as model])) - -(deftest valid-min-gas-price-test - (testing "not an number" - (is (= :invalid-number (model/invalid-send-parameter? :gas-price nil)))) - (testing "a number less than the minimum" - (is (= :not-enough-wei (model/invalid-send-parameter? :gas-price (money/bignumber "0.0000000001"))))) - (testing "a number greater than the mininum" - (is (not (model/invalid-send-parameter? :gas-price 3)))) - (testing "the minimum" - (is (not (model/invalid-send-parameter? :gas-price (money/bignumber "0.000000001")))))) - -(deftest valid-gas - (testing "not an number" - (is (= :invalid-number (model/invalid-send-parameter? :gas nil)))) - (testing "0" - (is (= :invalid-number (model/invalid-send-parameter? :gas 0)))) - (testing "a number" - (is (not (model/invalid-send-parameter? :gas 1))))) - -(deftest build-edit-test - (testing "an invalid edit" - (let [actual (-> {} - (model/build-edit :gas "invalid") - (model/build-edit :gas-price "0.00000000001"))] - (testing "it marks gas-price as invalid" - (is (get-in actual [:gas-price :invalid?]))) - (testing "it does not change value" - (is (= "0.00000000001" (get-in actual [:gas-price :value])))) - (testing "it marks gas as invalid" - (is (get-in actual [:gas :invalid?]))) - (testing "it does not change gas value" - (is (= "invalid" (get-in actual [:gas :value])))) - (testing "it sets max-fee to 0" - (is (= "0" (:max-fee actual)))))) - (testing "an valid edit" - (let [actual (-> {} - (model/build-edit :gas "21000") - (model/build-edit :gas-price "10"))] - (testing "it does not mark gas-price as invalid" - (is (not (get-in actual [:gas-price :invalid?])))) - (testing "it sets the value in number for gas-price, in gwei" - (is (= "10000000000" (str (get-in actual [:gas-price :value-number]))))) - (testing "it does not mark gas as invalid" - (is (not (get-in actual [:gas :invalid?])))) - (testing "it sets the value in number for gasi" - (is (= "21000" (str (get-in actual [:gas :value-number]))))) - (testing "it calculates max-fee" - (is (= "0.00021" (:max-fee actual))))))) diff --git a/test/cljs/status_im/test/profile/events.cljs b/test/cljs/status_im/test/profile/events.cljs deleted file mode 100644 index 708dcf1db7f..00000000000 --- a/test/cljs/status_im/test/profile/events.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.test.profile.events - (:require [cljs.test :refer [deftest is testing]] - reagent.core - [re-frame.core :as rf] - [day8.re-frame.test :refer [run-test-sync]] - status-im.ui.screens.db - [status-im.ui.screens.events :as events] - [status-im.ui.screens.profile.events :as profile-events] - [status-im.ui.screens.accounts.events :as account-events])) - -(def new-account - {:address "c296367a939e0957500a25ca89b70bd64b03004e" - :signed-up? true - :name "Disloyal Trusting Rainbowfish" - :photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEXw8PDYjLoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG2YFqAAABAHRSTlP//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKmfXxgAABnNJREFUeNoBaAaX+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEAAAAAAAABAQEBAQEAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAEBAQEBAQAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAQEBAQEBAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAABAQEBAQEAAAAAAAABAQEBAQEAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAEBAQEBAQAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAQEBAQEBAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKUMBsXBa1BAAAAAASUVORK5CYII=" - :status "the future starts today, not tomorrow" - :signing-phrase "baby atom limo" - :public-key "0x04f5722fba79eb36d73263417531007f43d13af76c6233573a8e3e60f667710611feba0785d751b50609bfc0b7cef35448875c5392c0a91948c95798a0ce600847"}) - -(defn test-fixtures [] - (rf/reg-fx ::events/init-store #()) - (rf/reg-fx ::account-events/save-account #())) diff --git a/test/cljs/status_im/test/protocol/core.cljs b/test/cljs/status_im/test/protocol/core.cljs deleted file mode 100644 index b1798713d8d..00000000000 --- a/test/cljs/status_im/test/protocol/core.cljs +++ /dev/null @@ -1,59 +0,0 @@ -(ns status-im.test.protocol.core - (:require [cljs.test :refer-macros [deftest is testing async]] - [cljs.nodejs :as nodejs] - re-frame.db - status-im.ui.screens.events - [re-frame.core :as rf] - [cljs.core.async :as async] - [status-im.transport.utils :as transport.utils] - [day8.re-frame.test :refer-macros [run-test-async run-test-sync] :as rf-test] - [status-im.test.protocol.node :as node] - [status-im.transport.message.v1.contact :as transport.contact] - [status-im.test.protocol.utils :as utils])) - -;; NOTE(oskarth): All these tests are evaluated in NodeJS - -(nodejs/enable-util-print!) - -(def contact-whisper-identity "0x048f7d5d4bda298447bbb5b021a34832509bd1a8dbe4e06f9b7223d00a59b6dc14f6e142b21d3220ceb3155a6d8f40ec115cd96394d3cc7c55055b433a1758dc74") -(def rpc-url (aget nodejs/process.env "WNODE_ADDRESS")) - -(def Web3 (js/require "web3")) -(defn make-web3 [] - (Web3. (Web3.providers.HttpProvider. rpc-url))) - -(defn create-keys [shh] - (let [keypair-promise (.newKeyPair shh)] - (.getPublicKey shh keypair-promise))) - -(deftest test-send-message! - (testing "send contact request & message" - (run-test-async - (let [web3 (make-web3) - shh (transport.utils/shh web3) - from (create-keys shh)] - (reset! re-frame.db/app-db {:web3 web3 - :current-public-key from}) - - (rf/reg-fx :data-store/save-chat (constantly nil)) - (rf/reg-fx :data-store/save-message (constantly nil)) - (rf/reg-fx :data-store/save-contact (constantly nil)) - (rf/reg-fx :data-store.transport/save (constantly nil)) - (rf/reg-fx :data-store/update-message (constantly nil)) - - (rf/dispatch [:open-chat-with-contact {:whisper-identity contact-whisper-identity}]) - (rf-test/wait-for [::transport.contact/send-new-sym-key] - (rf/dispatch [:set-chat-input-text "test message"]) - (rf/dispatch [:send-current-message]) - (rf-test/wait-for [:update-message-status :protocol/send-status-message-error] - (is true))))))) - -(deftest test-whisper-version! - (testing "Whisper version supported" - (async done - (let [web3 (make-web3) - shh (transport.utils/shh web3)] - (.version shh - (fn [& args] - (is (= "6.0" (second args))) - (done))))))) diff --git a/test/cljs/status_im/test/protocol/node.cljs b/test/cljs/status_im/test/protocol/node.cljs deleted file mode 100644 index 1a9b20ab083..00000000000 --- a/test/cljs/status_im/test/protocol/node.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.test.protocol.node - (:require [clojure.string :as s] - [status-im.test.protocol.utils :as utils])) - -(def identity-1 "0x04eedbaafd6adf4a9233a13e7b1c3c14461fffeba2e9054b8d456ce5f6ebeafadcbf3dce3716253fbc391277fa5a086b60b283daf61fb5b1f26895f456c2f31ae3") -(def identity-2 "0x0490161b00f2c47542d28c2e8908e77159b1720dccceb6393d7c001850122efc3b1709bcea490fd8f5634ba1a145aa0722d86b9330b0e39a8d493cb981fd459da2") -(def topic-1 "0xdeadbeef") - -(def topic-2 "0xbeefdead") diff --git a/test/cljs/status_im/test/protocol/runner.cljs b/test/cljs/status_im/test/protocol/runner.cljs deleted file mode 100644 index efaee67000c..00000000000 --- a/test/cljs/status_im/test/protocol/runner.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns status-im.test.protocol.runner - (:require [doo.runner :refer-macros [doo-tests]] - [status-im.test.protocol.core])) - -(enable-console-print!) - -;; Or doo will exit with an error, see: -;; https://github.com/bensu/doo/issues/83#issuecomment-165498172 -(set! (.-error js/console) (fn [x] (.log js/console x))) - -(set! goog.DEBUG false) - -(doo-tests 'status-im.test.protocol.core) diff --git a/test/cljs/status_im/test/protocol/utils.cljs b/test/cljs/status_im/test/protocol/utils.cljs deleted file mode 100644 index c946150a0cb..00000000000 --- a/test/cljs/status_im/test/protocol/utils.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.test.protocol.utils) - -(def fs (js/require "fs")) -(def child-process (js/require "child_process")) -(def process (js/require "process")) - -(def exist? (.-existsSync fs)) - -(defn exec-sync [command options] - (.execSync child-process command (clj->js options))) - -(defn exec [command options] - (.exec child-process command (clj->js options))) - -(defn spawn-sync [command args options] - (.spawnSync child-process command (clj->js args) (clj->js options))) - -(defn spawn [command args options] - (.spawn child-process command (clj->js args) (clj->js options))) - -(defn exit! [] - (.exit process)) - -(defn mkdir-sync [dir] - (.mkdirSync fs dir)) diff --git a/test/cljs/status_im/test/protocol/web3/inbox.cljs b/test/cljs/status_im/test/protocol/web3/inbox.cljs deleted file mode 100644 index 9fcc8e5ad98..00000000000 --- a/test/cljs/status_im/test/protocol/web3/inbox.cljs +++ /dev/null @@ -1,39 +0,0 @@ -(ns status-im.test.protocol.web3.inbox - (:require [status-im.transport.inbox :as inbox] - [status-im.transport.utils :as utils] - [cljs.test :refer-macros [deftest is testing]])) - -(def enode "enode://08d8eb6177b187049f6c97ed3f6c74fbbefb94c7ad10bafcaf4b65ce89c314dcfee0a8bc4e7a5b824dfa08b45b360cc78f34f0aff981f8386caa07652d2e601b@163.172.177.138:40404") -(def enode2 "enode://12d8eb6177b187049f6c97ed3f6c74fbbefb94c7ad10bafcaf4b65ce89c314dcfee0a8bc4e7a5b824dfa08b45b360cc78f34f0aff981f8386caa07652d2e601b@163.172.177.138:40404") - -(deftest test-extract-enode-id - (testing "Get enode id from enode uri" - (is (= "08d8eb6177b187049f6c97ed3f6c74fbbefb94c7ad10bafcaf4b65ce89c314dcfee0a8bc4e7a5b824dfa08b45b360cc78f34f0aff981f8386caa07652d2e601b" - (utils/extract-enode-id enode)))) - (testing "Get enode id from mailformed enode uri" - (is (= "" - (utils/extract-enode-id "08d8eb6177b187049f6c97ed3f6c74fbbefb94c7ad10bafcaf4b65ce89c314dcfee0a8bc4e7a5b824dfa08b45b360cc78f34f0aff981f8386caa07652d2e601b@163.172.177.138:40404")))) - (testing "Test empty string" - (is (= "" - (utils/extract-enode-id "")))) - (testing "Test nil" - (is (= "" - (utils/extract-enode-id nil))))) - -(def peers - [{:id "08d8eb6177b187049f6c97ed3f6c74fbbefb94c7ad10bafcaf4b65ce89c314dcfee0a8bc4e7a5b824dfa08b45b360cc78f34f0aff981f8386caa07652d2e601b" - :name "StatusIM/v0.9.9-unstable/linux-amd64/go1.9.2"} - {:id "0f7c65277f916ff4379fe520b875082a56e587eb3ce1c1567d9ff94206bdb05ba167c52272f20f634cd1ebdec5d9dfeb393018bfde1595d8e64a717c8b46692f" - :name "Geth/v1.7.2-stable/linux-amd64/go1.9.1"}]) - -(deftest test-registered-peer? - (testing "Peer is registered" - (is (inbox/registered-peer? peers enode))) - (testing "Peer is not peers list" - (is (not (inbox/registered-peer? peers enode2)))) - (testing "Empty peers" - (is (not (inbox/registered-peer? [] enode)))) - (testing "Empty peer" - (is (not (inbox/registered-peer? peers "")))) - (testing "Nil peer" - (is (not (inbox/registered-peer? peers nil))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs deleted file mode 100644 index e3a404a8fce..00000000000 --- a/test/cljs/status_im/test/runner.cljs +++ /dev/null @@ -1,108 +0,0 @@ -(ns status-im.test.runner - (:require [doo.runner :refer-macros [doo-tests]] - [status-im.test.chat.events] - [status-im.test.contacts.events] - [status-im.test.contacts.subs] - [status-im.test.accounts.events] - [status-im.test.data-store.realm.core] - [status-im.test.browser.events] - [status-im.test.wallet.subs] - [status-im.test.wallet.transactions.subs] - [status-im.test.wallet.transactions.views] - [status-im.test.profile.events] - [status-im.test.models.mailserver] - [status-im.test.models.bootnode] - [status-im.test.models.account] - [status-im.test.models.contact] - [status-im.test.models.network] - [status-im.test.models.wallet] - [status-im.test.transport.core] - [status-im.test.transport.inbox] - [status-im.test.transport.handlers] - [status-im.test.chat.models] - [status-im.test.chat.models.input] - [status-im.test.chat.models.message] - [status-im.test.chat.subs] - [status-im.test.chat.views.message] - [status-im.test.chat.views.photos] - [status-im.test.chat.commands.core] - [status-im.test.chat.commands.impl.transactions] - [status-im.test.i18n] - [status-im.test.protocol.web3.inbox] - [status-im.test.utils.utils] - [status-im.test.utils.money] - [status-im.test.utils.handlers-macro] - [status-im.test.utils.clocks] - [status-im.test.utils.ethereum.eip681] - [status-im.test.utils.ethereum.core] - [status-im.test.utils.random] - [status-im.test.utils.gfycat.core] - [status-im.test.utils.signing-phrase.core] - [status-im.test.utils.transducers] - [status-im.test.utils.async] - [status-im.test.utils.datetime] - [status-im.test.utils.prices] - [status-im.test.utils.keychain.core] - [status-im.test.utils.universal-links.core] - [status-im.test.utils.http] - [status-im.test.ui.screens.events] - [status-im.test.ui.screens.accounts.login.events] - [status-im.test.ui.screens.wallet.db])) - -(enable-console-print!) - -;; Or doo will exit with an error, see: -;; https://github.com/bensu/doo/issues/83#issuecomment-165498172 -(set! (.-error js/console) (fn [x] (.log js/console x))) - -(set! goog.DEBUG false) - -(doo-tests - 'status-im.test.utils.async - 'status-im.test.chat.events - 'status-im.test.chat.subs - 'status-im.test.chat.models - 'status-im.test.accounts.events - 'status-im.test.contacts.events - 'status-im.test.contacts.subs - 'status-im.test.profile.events - 'status-im.test.data-store.realm.core - 'status-im.test.models.mailserver - 'status-im.test.models.bootnode - 'status-im.test.models.account - 'status-im.test.models.contact - 'status-im.test.models.network - 'status-im.test.models.wallet - 'status-im.test.wallet.subs - 'status-im.test.wallet.transactions.subs - 'status-im.test.wallet.transactions.views - 'status-im.test.chat.models.input - 'status-im.test.chat.models.message - 'status-im.test.chat.views.message - 'status-im.test.chat.views.photos - 'status-im.test.chat.commands.core - 'status-im.test.chat.commands.impl.transactions - 'status-im.test.i18n - 'status-im.test.transport.core - 'status-im.test.transport.inbox - 'status-im.test.transport.handlers - 'status-im.test.protocol.web3.inbox - 'status-im.test.utils.utils - 'status-im.test.utils.handlers-macro - 'status-im.test.utils.money - 'status-im.test.utils.clocks - 'status-im.test.utils.ethereum.eip681 - 'status-im.test.utils.ethereum.core - 'status-im.test.utils.random - 'status-im.test.utils.gfycat.core - 'status-im.test.utils.signing-phrase.core - 'status-im.test.utils.transducers - 'status-im.test.utils.datetime - 'status-im.test.utils.prices - 'status-im.test.utils.keychain.core - 'status-im.test.utils.universal-links.core - 'status-im.test.utils.http - 'status-im.test.ui.screens.events - 'status-im.test.ui.screens.accounts.login.events - 'status-im.test.ui.screens.wallet.db - 'status-im.test.browser.events) diff --git a/test/cljs/status_im/test/screens/accounts/utils.cljs b/test/cljs/status_im/test/screens/accounts/utils.cljs deleted file mode 100644 index d609cc549e1..00000000000 --- a/test/cljs/status_im/test/screens/accounts/utils.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns status-im.test.screens.accounts.utils - (:require [clojure.test :refer-macros [deftest is]] - [status-im.ui.screens.accounts.utils :as accounts.utils])) - -(deftest test-account-update - (is (= (accounts.utils/account-update {} nil) - {:db {:account/account {}}, - :data-store/save-account {:after-update-event nil}}))) - -(deftest test-clean-seed-phrase - (is (= (accounts.utils/clean-seed-phrase nil) - {:db {:account/account {:seed-backed-up? true, :mnemonic nil}}, - :data-store/save-account {:seed-backed-up? true, :mnemonic nil, :after-update-event nil}}))) diff --git a/test/cljs/status_im/test/transport/core.cljs b/test/cljs/status_im/test/transport/core.cljs deleted file mode 100644 index b50f29027de..00000000000 --- a/test/cljs/status_im/test/transport/core.cljs +++ /dev/null @@ -1,48 +0,0 @@ -(ns status-im.test.transport.core - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.protocol.handlers :as protocol.handlers] - [status-im.transport.core :as transport])) - -(deftest init-whisper - (let [cofx {:db {:network "mainnet_rpc" - :account/account - {:networks {"mainnet_rpc" {:config {:NetworkId 1}}} - :public-key "1"}}}] - (testing "it adds the discover filter" - (is (= (:shh/add-discovery-filter (protocol.handlers/initialize-protocol cofx []))))) - (testing "it restores the sym-keys" - (is (= (:shh/restore-sym-keys (protocol.handlers/initialize-protocol cofx []))))) - (testing "custom mailservers" - (let [ms-1 {:id "1" - :chain "mainnet" - :name "name-1" - :address "address-1" - :password "password-1"} - ms-2 {:id "2" - :chain "mainnet" - :name "name-2" - :address "address-2" - :password "password-2"} - ms-3 {:id "3" - :chain "rinkeby" - :name "name-3" - :address "address-3" - :password "password-3"} - expected-wnodes {:mainnet {"1" (-> ms-1 - (dissoc :chain) - (assoc :user-defined true)) - "2" (-> ms-2 - (dissoc ms-2 :chain) - (assoc :user-defined true))} - :rinkeby {"3" (-> ms-3 - (dissoc :chain) - (assoc :user-defined true))}} - cofx-with-ms (assoc cofx - :data-store/mailservers - [ms-1 - ms-2 - ms-3])] - (is (= expected-wnodes - (get-in - (protocol.handlers/initialize-protocol cofx-with-ms []) - [:db :inbox/wnodes]))))))) diff --git a/test/cljs/status_im/test/transport/handlers.cljs b/test/cljs/status_im/test/transport/handlers.cljs deleted file mode 100644 index 42dd97b9631..00000000000 --- a/test/cljs/status_im/test/transport/handlers.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.test.transport.handlers - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.transport.handlers :as handlers])) - -(def messages #js [{:sig "0x04325367620ae20dd878dbb39f69f02c567d789dd21af8a88623dc5b529827c2812571c380a2cd8236a2851b8843d6486481166c39debf60a5d30b9099c66213e4" - :ttl 10 - :timestamp 1527692015 - :topic "0x9c22ff5f" - :payload "0x5b227e236334222c5b2246222c22746578742f706c61696e222c227e3a7075626c69632d67726f75702d757365722d6d657373616765222c3135323736393230313433383130312c313532373639323031343337375d5d" - :padding "0xbf06347cc7f9aa18b4a846032264a88f559d9b14079975d14b10648847c0543a77a80624e101c082d19b502ae3b4f97958d18abf59eb0a82afc1301aa22470495fac739a30c2f563599fa8d8e09363a43d39311596b7f119dee7b046989c08224f1ef5cdc385" - :pow 0.002631578947368421 - :hash "0x220ef9994a4fae64c112b27ed07ef910918159cbe6fcf8ac515ee2bf9a6711a0"}]) - -(deftest receive-whisper-messages-test - (testing "an error is reported" - (is (nil? (handlers/receive-whisper-messages {} ["error" #js [] nil])))) - (testing "messages is undefined" - (is (nil? (handlers/receive-whisper-messages {} [nil js/undefined nil])))) - (testing "happy path" - (let [actual (handlers/receive-whisper-messages {} [nil messages "1"])] - (testing "it add an fx for the message" - (is (:chat-received-message/add-fx actual)))))) diff --git a/test/cljs/status_im/test/transport/inbox.cljs b/test/cljs/status_im/test/transport/inbox.cljs deleted file mode 100644 index ca78f456c2b..00000000000 --- a/test/cljs/status_im/test/transport/inbox.cljs +++ /dev/null @@ -1,190 +0,0 @@ -(ns status-im.test.transport.inbox - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.transport.inbox :as inbox] - [status-im.constants :as constants])) - -(defn cofx-fixtures [sym-key registered-peer?] - {:db {:mailserver-status :connected - :network "mainnet_rpc" - :peers-summary (if registered-peer? - [{:id "wnode-id"}] - []) - :account/account {:networks constants/default-networks} - :inbox/current-id "mailserver-a" - :inbox/wnodes {:mainnet {"mailserver-a" {:sym-key-id sym-key - :address "enode://wnode-id@ip"}}}}}) - -(defn peers-summary-change-fx-result [sym-key registered-peer? registered-peer-before?] - (inbox/peers-summary-change-fx (if registered-peer-before? - [{:id "wnode-id"}] - []) - (cofx-fixtures sym-key - registered-peer?))) - -(deftest peers-summary-change-fx - (testing "Mailserver connected" - (let [result (peers-summary-change-fx-result false true false)] - (is (= (into #{} (keys result)) - #{:status-im.transport.inbox/mark-trusted-peer})))) - (testing "Mailserver disconnected, sym-key exists" - (let [result (peers-summary-change-fx-result true false true)] - (is (= (into #{} (keys result)) - #{:db :status-im.transport.inbox/add-peer :utils/dispatch-later})) - (is (= (get-in result [:db :mailserver-status]) - :connecting)))) - (testing "Mailserver disconnected, sym-key doesn't exists (unlikely situation in practice)" - (let [result (peers-summary-change-fx-result false false true)] - (is (= (into #{} (keys result)) - #{:db :status-im.transport.inbox/add-peer :utils/dispatch-later :shh/generate-sym-key-from-password})) - (is (= (get-in result [:db :mailserver-status]) - :connecting)))) - (testing "Mailserver isn't concerned by peer summary changes" - (is (= (into #{} (keys (peers-summary-change-fx-result true true true))) - #{})) - (is (= (into #{} (keys (peers-summary-change-fx-result true false false))) - #{})))) - -(deftest connect-to-mailserver - (let [db {:network "mainnet" - :inbox/current-id "wnodeid" - :inbox/wnodes - {:mainnet {"wnodeid" {:address "wnode-address" - :password "wnode-password"}}} - :account/account - {:settings {:wnode {:mainnet "wnodeid"}} - :networks {"mainnet" {:config {:NetworkId 1}}}}}] - (testing "it adds the peer" - (is (= {:wnode "wnode-address"} - (::inbox/add-peer (inbox/connect-to-mailserver {:db db}))))) - (testing "it generates a sym key if hasn't been generated before" - (is (= "wnode-password" - (-> (inbox/connect-to-mailserver {:db db}) - :shh/generate-sym-key-from-password - :password)))) - (let [wnode-with-sym-key-db (assoc-in db - [:inbox/wnodes :mainnet "wnodeid" :sym-key-id] - "somesymkeyid")] - (testing "it does not generate a sym key if already present" - (is (not (-> (inbox/connect-to-mailserver {:db wnode-with-sym-key-db}) - :shh/generate-sym-key-from-password))))))) - -(deftest request-messages - (let [db {:network "mainnet" - :mailserver-status :connected - :inbox/current-id "wnodeid" - :inbox/wnodes - {:mainnet {"wnodeid" {:address "wnode-address" - :sym-key-id "something" - :password "wnode-password"}}} - :account/account - {:networks {"mainnet" {:config {:NetworkId 1}}}} - :transport/chats - {:dont-fetch-history {:topic "dont-fetch-history"} - :fetch-history {:topic "fetch-history" - :fetch-history? true}}} - cofx {:db db :now 1000000000}] - (testing "inbox is ready" - (testing "last request is > the 7 days ago" - (let [cofx-with-last-request (assoc-in cofx [:db :account/account :last-request] 400000) - actual (inbox/request-messages cofx-with-last-request)] - (testing "it uses last request" - (is (= 400000 (get-in actual [::inbox/request-messages 0 :from])))))) - (testing "last request is < the 7 days ago" - (let [cofx-with-last-request (assoc-in cofx [:db :account/account :last-request] 2) - actual (inbox/request-messages cofx-with-last-request)] - (testing "it uses last 7 days for catching up" - (is (= 395200 (get-in actual [::inbox/request-messages 0 :from])))) - (testing "it only uses topics that dont have fetch history set" - (is (= ["0xf8946aac" "dont-fetch-history"] - (get-in actual [::inbox/request-messages 0 :topics])))) - (testing "it uses the last 24 hours to request history" - (is (= 913600 - (get-in actual [::inbox/request-messages 1 :from])))) - (testing "it fetches the right topic for history" - (is (= ["fetch-history"] - (get-in actual [::inbox/request-messages 1 :topics]))))))) - (testing "inbox is not ready" - (testing "it does not do anything" - (is (nil? (inbox/request-messages {}))))))) - -(deftest request-messages-params - (let [mailserver {:address "peer" - :sym-key-id "id"}] - (testing "from is greater that to" - (testing "it returns an empty sequence" - (is (empty? (inbox/request-inbox-messages-params mailserver 2 0 ["a" "b" "c"]))))) - (testing "from is equal to to" - (testing "it returns an empty sequence" - (is (empty? (inbox/request-inbox-messages-params mailserver 2 2 ["a" "b" "c"]))))) - (testing "to is less than the step" - (is (= #{{:topic "a" - :mailServerPeer "peer" - :symKeyID "id" - :from 0 - :to 3} - {:topic "b" - :mailServerPeer "peer" - :symKeyID "id" - :from 0 - :to 3}} - (into #{} (inbox/request-inbox-messages-params mailserver 0 3 ["a" "b"]))))) - (testing "to is equal the step" - (is (= #{{:topic "a" - :mailServerPeer "peer" - :symKeyID "id" - :from 0 - :to 86400} - {:topic "b" - :mailServerPeer "peer" - :symKeyID "id" - :from 0 - :to 86400}} - (into #{} (inbox/request-inbox-messages-params mailserver 0 86400 ["a" "b"]))))) - (testing "to is greather than the step" - (is (= #{{:topic "a" - :mailServerPeer "peer" - :symKeyID "id" - :from 0 - :to 86400} - {:topic "b" - :mailServerPeer "peer" - :symKeyID "id" - :from 0 - :to 86400} - {:topic "a" - :mailServerPeer "peer" - :symKeyID "id" - :from 86400 - :to 90000} - {:topic "b" - :mailServerPeer "peer" - :symKeyID "id" - :from 86400 - :to 90000}} - (into #{} (inbox/request-inbox-messages-params mailserver 0 90000 ["a" "b"]))))))) - -(deftest initialize-offline-inbox - (let [db {:network "mainnet" - :mailserver-status :connected - :inbox/current-id "wnodeid" - :inbox/wnodes - {:mainnet {"wnodeid" {:address "wnode-address" - :sym-key-id "something" - :password "wnode-password"}}} - :account/account - {:networks {"mainnet" {:config {:NetworkId 1}}}}}] - (testing "last-request is not set" - (testing "it sets it to now in seconds" - (is (= 10 - (get-in - (inbox/initialize-offline-inbox [] {:now 10000 :db db}) - [:db :account/account :last-request]))))) - (testing "last-request is set" - (testing "leaves it unchanged" - (is (= "sometimeago" - (get-in - (inbox/initialize-offline-inbox - [] - {:now "now" - :db (assoc-in db [:account/account :last-request] "sometimeago")}) - [:db :account/account :last-request]))))))) diff --git a/test/cljs/status_im/test/ui/screens/accounts/login/events.cljs b/test/cljs/status_im/test/ui/screens/accounts/login/events.cljs deleted file mode 100644 index 121471f6d83..00000000000 --- a/test/cljs/status_im/test/ui/screens/accounts/login/events.cljs +++ /dev/null @@ -1,116 +0,0 @@ -(ns status-im.test.ui.screens.accounts.login.events - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.config :as config] - [status-im.ui.screens.accounts.login.models :as models])) - -(deftest login-account - (let [mainnet-account {:network "mainnet_rpc" - :networks {"mainnet_rpc" {:config {:NetworkId 1}}}} - testnet-account {:network "testnet_rpc" - :networks {"testnet_rpc" {:config {:NetworkId 3}}}} - accounts {"mainnet" mainnet-account - "testnet" testnet-account} - initial-db {:db {:network "mainnet_rpc" - :accounts/accounts accounts}}] - - (testing "status-go has not started" - (let [actual (models/login-account "testnet" "password" initial-db)] - (testing "it starts status-node if it has not started" - (is (= {:NetworkId 3} - (:initialize-geth-fx - actual)))) - (testing "it logins the user after the node started" - (is (= [:login-account-internal "testnet" "password"] (get-in actual [:db :node/after-start])))))) - - (testing "status-go has started & the user is on mainnet" - (let [db (assoc-in initial-db [:db :status-node-started?] true) - actual (models/login-account "mainnet" "password" db)] - (testing "it does not start status-node if it has already started" - (is (not (:initialize-geth-fx actual)))) - (testing "it logs in the user" - (is (= ["mainnet" "password"] (:login actual)))))) - - (testing "the user has selected a different network" - (testing "status-go has started" - (let [db (assoc-in initial-db [:db :status-node-started?] true) - actual (models/login-account "testnet" "password" db)] - (testing "it dispatches start-node" - (is (get-in actual [:db :node/after-stop] [:start-node "testnet" "password"]))) - (testing "it stops status-node" - (is (contains? actual :stop-node))))) - - (testing "status-go has not started" - (let [actual (models/login-account "testnet" "password" initial-db)] - (testing "it starts status-node" - (is (= {:NetworkId 3} (:initialize-geth-fx actual)))) - (testing "it logins the user after the node started" - (is (= [:login-account-internal "testnet" "password"] (get-in actual [:db :node/after-start]))))))) - - (testing "custom bootnodes" - (let [custom-bootnodes {"a" {:id "a" - :name "name-a" - :address "address-a"} - "b" {:id "b" - :name "name-b" - :address "address-b"}} - bootnodes-db (assoc-in - initial-db - [:db :accounts/accounts "mainnet" :bootnodes] - {"mainnet_rpc" custom-bootnodes})] - - (testing "custom bootnodes enabled" - (let [bootnodes-enabled-db (assoc-in - bootnodes-db - [:db :accounts/accounts "mainnet" :settings] - {:bootnodes {"mainnet_rpc" true}}) - actual (models/login-account "mainnet" "password" bootnodes-enabled-db)] - (testing "status-node has started" - (let [db (assoc-in bootnodes-enabled-db [:db :status-node-started?] true) - actual (models/login-account "mainnet" "password" db)] - (testing "it dispatches start-node" - (is (get-in actual [:db :node/after-stop] [:start-node "testnet" "password"]))) - (testing "it stops status-node" - (is (contains? actual :stop-node))))) - (testing "status-node has not started" - (let [actual (models/login-account "mainnet" "password" bootnodes-enabled-db)] - (testing "it adds bootnodes to the config" - (is (= {:ClusterConfig {:Enabled true - :BootNodes ["address-a" "address-b"]} - :NetworkId 1} (:initialize-geth-fx actual)))) - (testing "it logins the user after the node started" - (is (= [:login-account-internal "mainnet" "password"] (get-in actual [:db :node/after-start])))))))) - - (testing "custom bootnodes not enabled" - (testing "status-node has started" - (let [db (assoc-in bootnodes-db [:db :status-node-started?] true) - actual (models/login-account "mainnet" "password" db)] - (testing "it does not start status-node if it has already started" - (is (not (:initialize-geth-fx actual)))) - (testing "it logs in the user" - (is (= ["mainnet" "password"] (:login actual)))))) - (testing "status-node has not started" - (let [actual (models/login-account "mainnet" "password" bootnodes-db)] - (testing "it starts status-node without custom bootnodes" - (is (= {:NetworkId 1} (:initialize-geth-fx actual)))) - (testing "it logins the user after the node started" - (is (= [:login-account-internal "mainnet" "password"] (get-in actual [:db :node/after-start]))))))))))) - -(deftest restart-node? - (testing "custom bootnodes is toggled off" - (with-redefs [config/bootnodes-settings-enabled? false] - (testing "it returns true when the network is different" - (is (models/restart-node? "mainnet_rpc" "mainnet" true))) - (testing "it returns false when the network is the same" - (is (not (models/restart-node? "mainnet" "mainnet" true)))))) - (testing "custom bootnodes is toggled on" - (with-redefs [config/bootnodes-settings-enabled? true] - (testing "the user is not using custom bootnodes" - (testing "it returns true when the network is different" - (is (models/restart-node? "mainnet_rpc" "mainnet" false))) - (testing "it returns false when the network is the same" - (is (not (models/restart-node? "mainnet" "mainnet" false))))) - (testing "the user is using custom bootnodes" - (testing "it returns true when the network is different" - (is (models/restart-node? "mainnet" "mainnet" true))) - (testing "it returns true when the network is the same" - (is (models/restart-node? "mainnet_rpc" "mainnet" true))))))) diff --git a/test/cljs/status_im/test/ui/screens/events.cljs b/test/cljs/status_im/test/ui/screens/events.cljs deleted file mode 100644 index fb65c118050..00000000000 --- a/test/cljs/status_im/test/ui/screens/events.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.test.ui.screens.events - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.ui.screens.events :as events])) - -(deftest initialize-db - (testing "it preserves universal-links/url" - (is (= "some-url" (get-in (events/initialize-db {:db - {:universal-links/url "some-url"}}) - [:db :universal-links/url]))))) diff --git a/test/cljs/status_im/test/ui/screens/wallet/db.cljs b/test/cljs/status_im/test/ui/screens/wallet/db.cljs deleted file mode 100644 index 29fb2ac34d7..00000000000 --- a/test/cljs/status_im/test/ui/screens/wallet/db.cljs +++ /dev/null @@ -1,52 +0,0 @@ -(ns status-im.test.ui.screens.wallet.db - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.ui.screens.wallet.db :as wallet.db] - [status-im.utils.money :as money] - [status-im.i18n :as i18n])) - -(deftest test-too-precise-amount? - (testing "try both decimal and scientific or hex format" - (is (= false (wallet.db/too-precise-amount? "100" 2))) - (is (= false (wallet.db/too-precise-amount? "100" 0))) - (is (= true (wallet.db/too-precise-amount? "100.1" 0))) - (is (= false (wallet.db/too-precise-amount? "100.23" 2))) - (is (= true (wallet.db/too-precise-amount? "100.233" 2))) - (is (= true (wallet.db/too-precise-amount? "100.0000000000000000001" 18))) - (is (= false (wallet.db/too-precise-amount? "100.000000000000000001" 18))) - (is (= false (wallet.db/too-precise-amount? "1e-18" 18))) - (is (= true (wallet.db/too-precise-amount? "1e-19" 18))) - (is (= true (wallet.db/too-precise-amount? "0xa.a" 2))) ;; 0xa.a is 10.625 - (is (= false (wallet.db/too-precise-amount? "0xa.a" 3))) - (is (= false (wallet.db/too-precise-amount? "1000" 3))))) - -(defn- equal-results? [a b] - (and (= (:error a) (:error b)) - (or (and (nil? (:amount a)) - (nil? (:amount b))) - (.eq (:amount a) (:amount b))))) - -(deftest test-parse-amount - (testing "test amount parsing" - (is (equal-results? {:value (money/bignumber "100")} (wallet.db/parse-amount "100" 2))) - (is (equal-results? {:value (money/bignumber "100")} (wallet.db/parse-amount "100" 0))) - (is (equal-results? {:error (i18n/label :t/validation-amount-is-too-precise {:decimals 0}) - :value (money/bignumber "100.1")} (wallet.db/parse-amount "100.1" 0))) - (is (equal-results? {:value (money/bignumber "100.23")} (wallet.db/parse-amount "100.23" 2))) - (is (equal-results? {:error (i18n/label :t/validation-amount-is-too-precise {:decimals 2}) - :value (money/bignumber "100.233")} (wallet.db/parse-amount "100.233" 2))) - (is (equal-results? {:error (i18n/label :t/validation-amount-is-too-precise {:decimals 18}) - :value (money/bignumber "100.0000000000000000001")} - (wallet.db/parse-amount "100.0000000000000000001" 18))) - (is (equal-results? {:value (money/bignumber "100.000000000000000001")} - (wallet.db/parse-amount "100.000000000000000001" 18))) - (is (equal-results? {:value (money/bignumber "1e-18")} (wallet.db/parse-amount "1e-18" 18))) - (is (equal-results? {:error (i18n/label :t/validation-amount-is-too-precise {:decimals 18}) - :value (money/bignumber "1e-19")} (wallet.db/parse-amount "1e-19" 18))) - (is (equal-results? {:error (i18n/label :t/validation-amount-is-too-precise {:decimals 2}) - :value (money/bignumber "10.625")} (wallet.db/parse-amount "0xa.a" 2))) - (is (equal-results? {:value (money/bignumber "10.625")} (wallet.db/parse-amount "0xa.a" 3))) - (is (equal-results? {:error (i18n/label :t/validation-amount-invalid-number) - :value nil} (wallet.db/parse-amount "SOMETHING" 5))) - (is (nil? (wallet.db/parse-amount nil 5))) - (is (nil? (wallet.db/parse-amount "" 5))) - (is (equal-results? {:value (money/bignumber "1000")} (wallet.db/parse-amount "1000" 3))))) diff --git a/test/cljs/status_im/test/utils/async.cljs b/test/cljs/status_im/test/utils/async.cljs deleted file mode 100644 index 1cf7e254b4d..00000000000 --- a/test/cljs/status_im/test/utils/async.cljs +++ /dev/null @@ -1,35 +0,0 @@ -(ns status-im.test.utils.async - (:require [cljs.test :refer-macros [deftest is testing async]] - [cljs.core.async :as async] - [status-im.utils.async :as async-util])) - -(deftest chunking-test - (testing "Accumulating result works as expected for `chunked-pipe!`" - (let [input (async/chan) - output (async/chan)] - (async-util/chunked-pipe! input output 100) - (async done - (async/go - (async/put! input 1) - (async/put! input 2) - (async/put! input 3) - ( (rand-int 10) 5) (recv! a chat-id message)) - (when (> (rand-int 10) 5) (recv! b chat-id message)) - (when (> (rand-int 10) 5) (recv! c chat-id message))) - -(defn get-last-clock-value - [db chat-id] - (if-let [messages (-> @db :chats chat-id :messages)] - (-> (sort-by :clock-value > messages) - first - :clock-value) - 0)) - -(defn save! [db chat-id message] - (swap! db - (fn [state] - (let [messages (-> state :chats chat-id :messages)] - (assoc-in state [:chats chat-id :messages] - (conj messages message)))))) - -(defn send! [db chat-id message] - (let [clock-value (get-last-clock-value db chat-id) - prepared-message (assoc message :clock-value (clocks/send clock-value))] - (save! db chat-id prepared-message) - (random-broadcast! chat-id prepared-message))) - -(defn recv! [db chat-id {:keys [clock-value] :as message}] - (let [local-clock (get-last-clock-value db chat-id) - new-clock (clocks/receive clock-value local-clock)] - (when-not (= (:from message) (:identity @db)) - (save! db chat-id (assoc message :clock-value new-clock))))) - -(defn thread [db chat-id] - (let [messages (-> @db :chats chat-id :messages)] - (sort-by :clock-value < messages))) - -(defn format-message [{:keys [from text]}] - (str from ": " text ", ")) - -(defn format-thread [thread] - (apply str (map format-message thread))) - -;; Invariant we want to maintain. -(defn ordered-increasing-text? [thread] - (let [xs (map :text thread)] - (or (empty? xs) (apply < xs)))) - -(defn simulate! [] - (send! a :foo {:from "a" :text "1"}) - (send! a :foo {:from "a" :text "2"}) - - (send! a :bar {:from "a" :text "1"}) - - (send! b :foo {:from "b" :text "3"}) - (send! c :foo {:from "c" :text "4"}) - (send! a :foo {:from "a" :text "5"}) - - (send! c :bar {:from "c" :text "7"})) - -(deftest clocks - (testing "Message order preserved" - (simulate!) - (is (ordered-increasing-text? (thread a :foo))) - (is (ordered-increasing-text? (thread b :foo))) - (is (ordered-increasing-text? (thread c :foo))) - (is (ordered-increasing-text? (thread a :bar)))) - - (testing "Bad thread recognized as such" - (let [bad-thread '({:from "a", :text "1", :clock-value 1} - {:from "c", :text "4", :clock-value 1} - {:from "a", :text "2", :clock-value 2} - {:from "a", :text "5", :clock-value 8})] - (is (not (ordered-increasing-text? bad-thread)))))) - -(deftest safe-timestamp - (testing "it caps the timestamp when a value too large is provided" - (is (< (clocks/receive js/Number.MAX_SAFE_INTEGER 0) - js/Number.MAX_SAFE_INTEGER)))) - - ;; Debugging -;;(println "******************************************") -;;(println "A's POV :foo" (format-thread (thread a :foo))) -;;(println "B's POV :foo" (format-thread (thread b :foo))) -;;(println "C's POV :foo" (format-thread (thread c :foo))) -;;(println "******************************************") diff --git a/test/cljs/status_im/test/utils/datetime.cljs b/test/cljs/status_im/test/utils/datetime.cljs deleted file mode 100644 index fc7cbdf4967..00000000000 --- a/test/cljs/status_im/test/utils/datetime.cljs +++ /dev/null @@ -1,89 +0,0 @@ -(ns status-im.test.utils.datetime - (:require [cljs.test :refer-macros [deftest is]] - [status-im.utils.datetime :as d] - [cljs-time.core :as t])) - -(defn match [name symbols] - (is (identical? (.-dateTimeSymbols_ (d/mk-fmt name d/medium-date-format)) - symbols))) - -(deftest date-time-formatter-test - (match "en-US" goog.i18n.DateTimeSymbols_en_US) - (match "en-ZZ" goog.i18n.DateTimeSymbols_en) - (match "en" goog.i18n.DateTimeSymbols_en) - (match "nb-NO" goog.i18n.DateTimeSymbols_nb) - (match "nb" goog.i18n.DateTimeSymbols_nb) - (match "whoa-WHOA" goog.i18n.DateTimeSymbols_en) - (match "whoa" goog.i18n.DateTimeSymbols_en)) - -;; 1970-01-01 00:00:00 UTC -(def epoch 0) -;; 1970-01-03 00:00:00 UTC -(def epoch-plus-3d 172800000) - -(deftest is24Hour-locale-en-test - (is (= (d/is24Hour-locsym (d/locale-symbols "en")) false))) - -(deftest is24Hour-locale-it-test - (is (= (d/is24Hour-locsym (d/locale-symbols "it")) true))) - -(deftest is24Hour-locale-nb-test - (is (= (d/is24Hour-locsym (d/locale-symbols "nb-NO")) true))) - -(deftest to-short-str-today-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/time-fmt (d/mk-fmt "us" d/short-time-format) - d/time-zone-offset (t/period :hours 0)] - (is (= (d/to-short-str epoch-plus-3d) "12:00 AM")))) - -(deftest to-short-str-today-force-24H-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/is24Hour (constantly true) - d/time-fmt (d/mk-fmt "us" d/short-time-format) - d/time-zone-offset (t/period :hours 0)] - (is (= (d/to-short-str epoch-plus-3d) "00:00")))) - -(deftest to-short-str-today-force-AMPM-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/is24Hour (constantly false) - d/time-fmt (d/mk-fmt "it" d/short-time-format) - d/time-zone-offset (t/period :hours 0)] - (is (= (d/to-short-str epoch-plus-3d) "12:00 AM")))) - -(deftest to-short-str-before-yesterday-us-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/time-zone-offset (t/period :hours 0) - d/date-fmt (d/mk-fmt "us" d/medium-date-format)] - (is (= (d/to-short-str epoch) "Jan 1, 1970")))) - -(deftest to-short-str-before-yesterday-nb-test - (with-redefs [d/time-zone-offset (t/period :hours 0) - d/date-fmt (d/mk-fmt "nb-NO" d/medium-date-format) - t/*ms-fn* (constantly epoch-plus-3d)] - (is (= (d/to-short-str epoch) "1. jan. 1970")))) - -(deftest day-relative-before-yesterday-us-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/time-zone-offset (t/period :hours 0) - d/date-fmt (d/mk-fmt "us" d/medium-date-time-format)] - (is (= (d/day-relative epoch) "Jan 1, 1970, 12:00:00 AM")))) - -(deftest day-relative-before-yesterday-nb-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/time-zone-offset (t/period :hours 0) - d/date-fmt (d/mk-fmt "nb-NO" d/medium-date-time-format)] - (is (= (d/day-relative epoch) "1. jan. 1970, 00:00:00")))) - -(deftest day-relative-before-yesterday-force-24H-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/is24Hour (constantly true) - d/time-zone-offset (t/period :hours 0) - d/date-fmt (d/mk-fmt "us" d/medium-date-time-format)] - (is (= (d/day-relative epoch) "Jan 1, 1970, 00:00:00")))) - -(deftest day-relative-before-yesterday-force-AMPM-test - (with-redefs [t/*ms-fn* (constantly epoch-plus-3d) - d/is24Hour (constantly false) - d/time-zone-offset (t/period :hours 0) - d/date-fmt (d/mk-fmt "it" d/medium-date-time-format)] - (is (= (d/day-relative epoch) "01 gen 1970, 12:00:00 AM")))) \ No newline at end of file diff --git a/test/cljs/status_im/test/utils/ethereum/core.cljs b/test/cljs/status_im/test/utils/ethereum/core.cljs deleted file mode 100644 index 16b70242031..00000000000 --- a/test/cljs/status_im/test/utils/ethereum/core.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.test.utils.ethereum.core - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.ethereum.core :as ethereum])) - -(deftest call-params - (testing "ERC20 balance-of params" - (let [contract "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" - address "0xa7cfd581060ec66414790691681732db249502bd"] - (is (= (ethereum/call-params contract "balanceOf(address)" address) - {:to "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" - :data "0x70a08231000000000000000000000000a7cfd581060ec66414790691681732db249502bd"}))))) - -(deftest valid-words? - (is (not (true? (ethereum/valid-words? ["rate" "rate"])))) - (is (not (true? (ethereum/valid-words? ["rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate?"])))) - (is (true? (ethereum/valid-words? ["rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate"])))) - -(deftest passphrase->words? - (is (= ["one" "two" "three" "for" "five" "six" "seven" "height" "nine" "ten" "eleven" "twelve"] - (ethereum/passphrase->words "one two three for five six seven height nine ten eleven twelve")) - (= ["one" "two" "three" "for" "five" "six" "seven" "height" "nine" "ten" "eleven" "twelve"] - (ethereum/passphrase->words " one two three for five six seven height nine ten eleven twelve ")))) - -(deftest chain-id->chain-keyword - (is (= (ethereum/chain-id->chain-keyword 1) :mainnet)) - (is (= (ethereum/chain-id->chain-keyword 3) :testnet)) - (is (= (ethereum/chain-id->chain-keyword 4) :rinkeby)) - (is (= (ethereum/chain-id->chain-keyword 5777) :custom))) \ No newline at end of file diff --git a/test/cljs/status_im/test/utils/ethereum/eip681.cljs b/test/cljs/status_im/test/utils/ethereum/eip681.cljs deleted file mode 100644 index e0ae6a0353a..00000000000 --- a/test/cljs/status_im/test/utils/ethereum/eip681.cljs +++ /dev/null @@ -1,85 +0,0 @@ -(ns status-im.test.utils.ethereum.eip681 - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.ethereum.eip681 :as eip681] - [status-im.utils.money :as money])) - -(deftest parse-uri - (is (= nil (eip681/parse-uri nil))) - (is (= nil (eip681/parse-uri 5))) - (is (= nil (eip681/parse-uri "random"))) - (is (= nil (eip681/parse-uri "ethereum:"))) - (is (= nil (eip681/parse-uri "ethereum:?value=1"))) - (is (= nil (eip681/parse-uri "bitcoin:0x1234"))) - (is (= nil (eip681/parse-uri "ethereum:0x1234"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7", :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?unknown=1"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7", :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "2.014e18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=2.014e18"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "-1e18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=-1e18"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "+1E18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=+1E18"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1E18" :gas "100" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1E18&gas=100"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "NOT_NUMBER" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=NOT_NUMBER"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1ETH" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1ETH"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@1?value=1e18&gas=5000"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 3} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1e18&gas=5000"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer"} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}} - (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1"))) - (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :gas "100" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}} - (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100")))) - -(deftest generate-erc20-uri - (is (= nil (eip681/generate-erc20-uri nil nil))) - (is (= "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" - (eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :SNT :value 5}))) - (is (= "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7&gas=10000&gasPrice=10000" - (eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :SNT :value 5 :gas 10000 :gasPrice 10000}))) - (is (= "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" - (eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :SNT :chain-id 1 :value 5}))) - (is (= "ethereum:0xc55cF4B03948D7EBc8b9E8BAD92643703811d162@3/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" - (eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :STT :chain-id 3 :value 5})))) - -(deftest generate-uri - (is (= nil (eip681/generate-uri nil nil))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" nil))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {}))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value nil}))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1)}))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1000000000000000000" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1e18)}))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1&gas=100" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1}))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1&gas=100" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 3}))) - (is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100" - (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" - {:gas (money/bignumber 100) :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 1}})))) - -(deftest round-trip - (let [uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1&gas=100" - {:keys [address] :as params} (eip681/parse-uri uri)] - (is (= uri (eip681/generate-uri address (dissoc params :address))))) - (let [uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3/transfer?uint256=5&address=0xc55cF4B03948D7EBc8b9E8BAD92643703811d162" - {:keys [address] :as params} (eip681/parse-uri uri)] - (is (= uri (eip681/generate-uri address (dissoc params :address)))))) - -(deftest parse-eth-value - (is (= nil (eip681/parse-eth-value nil))) - (is (= nil (eip681/parse-eth-value 1))) - (is (= nil (eip681/parse-eth-value "NOT_NUMBER"))) - (is (.equals (money/bignumber 1) (eip681/parse-eth-value "1"))) - (is (.equals (money/bignumber 2.014e18) (eip681/parse-eth-value "2.014e18"))) - (is (.equals (money/bignumber 1e18) (eip681/parse-eth-value "1ETH"))) - (is (.equals (money/bignumber -1e18) (eip681/parse-eth-value "-1e18"))) - (is (.equals (money/bignumber 1e18) (eip681/parse-eth-value "1E18"))) - (is (.equals (money/bignumber "111122223333441239") (eip681/parse-eth-value "111122223333441239")))) - -(deftest extract-request-details - (let [{:keys [value symbol address]} (eip681/extract-request-details {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1ETH"})] - (is (.equals (money/ether->wei (money/bignumber 1)) value)) - (is (= :ETH symbol)) - (is (= "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" address))) - (is (nil? (eip681/extract-request-details {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e" :chain-id 1 :function-name "unknown"}))) - (let [{:keys [value symbol address]} (eip681/extract-request-details {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e" :chain-id 1 - :function-name "transfer" :function-arguments {:uint256 1000 :address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"}})] - (is (.equals (money/bignumber 1000) value)) - (is (= :SNT symbol)) - (is (= "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" address)))) \ No newline at end of file diff --git a/test/cljs/status_im/test/utils/gfycat/core.cljs b/test/cljs/status_im/test/utils/gfycat/core.cljs deleted file mode 100644 index aacab729cb6..00000000000 --- a/test/cljs/status_im/test/utils/gfycat/core.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.test.utils.gfycat.core - (:require [cljs.test :refer-macros [deftest is]] - [status-im.utils.gfycat.core :as gfy])) - -(def message "Names are only deterministic as long as word lists do not change!\nIf you change the lists, please change the tests as well.") - -(deftest test-generate-gfy - (is (= (gfy/generate-gfy "57348975ff9199ca636207a396b915d6b6a675b4") "Winged Fitting Mosquito") message) - (is (= (gfy/generate-gfy "e63d083d2d7a13a14632983b5820529445ca8109") "Mediumvioletred Melodic Waterdogs") message) - (is (= (gfy/generate-gfy "57348975ff9199ca636207a396b915d6b6a675b4") "Winged Fitting Mosquito") message) - (is (= (gfy/generate-gfy "26cf649aebb252a75aebc588e0d9ce93789dbe0b") "Educated Upright Arcticseal") message) - (is (= (gfy/generate-gfy "e63d083d2d7a13a14632983b5820529445ca8109") "Mediumvioletred Melodic Waterdogs") message) - (is (= (gfy/generate-gfy nil) gfy/unknown-gfy) message) - (is (= (gfy/generate-gfy "0") gfy/unknown-gfy) message)) diff --git a/test/cljs/status_im/test/utils/handlers_macro.cljs b/test/cljs/status_im/test/utils/handlers_macro.cljs deleted file mode 100644 index 7340d35dfd6..00000000000 --- a/test/cljs/status_im/test/utils/handlers_macro.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns status-im.test.utils.handlers-macro - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.handlers-macro :as m])) - -(deftest merge-fx - (letfn [(add-b [cofx] - (assoc-in cofx [:db :b] "b")) - (add-c [cofx] - (assoc-in cofx [:db :c] "c")) - (add-tx [tx cofx] - (assoc cofx :data-store/tx [tx]))] - (testing "it updates db correctly" - (let [actual (m/merge-fx {:db {:a "a"}} - (add-b) - (add-c))] - (is (= {:db {:a "a" - :b "b" - :c "c"}} actual)))) - (testing "it updates db correctly when a fn don't update it" - (let [empty-fn (constantly nil) - actual (m/merge-fx {:db {:a "a"}} - (add-b) - (empty-fn) - (add-c))] - (is (= {:db {:a "a" - :b "b" - :c "c"}} actual)))) - #_(testing "it updates data-store/tx correctly" - (let [actual (m/merge-fx {:data-store/tx [1]} - (add-tx 2) - (add-tx 3))] - (is (= {:data-store/tx [1 2 3]} actual)))))) diff --git a/test/cljs/status_im/test/utils/http.cljs b/test/cljs/status_im/test/utils/http.cljs deleted file mode 100644 index ac9461b78e3..00000000000 --- a/test/cljs/status_im/test/utils/http.cljs +++ /dev/null @@ -1,55 +0,0 @@ -(ns status-im.test.utils.http - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.http :as http])) - -(deftest url-sanitize-check - (testing "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/818934.svg" - (testing "it returns true" - (is (http/url-sanitized? "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/818934.svg")))) - - (testing "https://www.cryptostrikers.com/assets/images/cards/017.svg" - (testing "it returns true" - (is (http/url-sanitized? "https://www.cryptostrikers.com/assets/images/cards/017.svg")))) - - (testing "https://www.etheremon.com/assets/images/mons_origin/025.png" - (testing "it returns true" - (is (http/url-sanitized? "https://www.etheremon.com/assets/images/mons_origin/025.png")))) - - (testing "http://www.etheremon.com/assets/images/mons_origin/025.png" - (testing "it returns false" - (is (not (http/url-sanitized? "http://www.etheremon.com/assets/images/mons_origin/025.png"))))) - - (testing "xxx:x \\\\x0Aonerror=javascript:alert(1)" - (testing "it returns false" - (is (not (http/url-sanitized? "xxx:x \\\\x0Aonerror=javascript:alert(1)"))))) - - (testing "https://www.etheremon.com/assets/images/mons_origin/025.png'<script>alert('123');</script>" - (testing "it returns false" - (is (not (http/url-sanitized? "https://www.etheremon.com/assets/images/mons_origin/025.png'<script>alert('123');</script>"))))) - - (testing "https://www.etheremon.com/assets/images/mons'<script>alert('123');</script>origin/025.png" - (testing "it returns false" - (is (not (http/url-sanitized? "https://www.etheremon.com/assets/images/mons'<script>alert('123');</script>origin/025.png"))))) - - (testing "https://www.etheremon.com/assets/images/mons_origin/025.png'>" - (testing "it returns false" - (is (not (http/url-sanitized? "https://www.etheremon.com/assets/images/mons_origin/025.png'>"))))) - - (testing "https://www.etheremon.com/assets/images/mons'>origin/025.png" - (testing "it returns false" - (is (not (http/url-sanitized? "https://www.etheremon.com/assets/images/mons'>origin/025.png")))))) - -(deftest url-host-check - (testing "Extract host/domain from URL" - (testing "Valid URL with endpoint" - (is (= "status.im" (http/url-host "https://status.im/testing")))) - (testing "Valid URL" - (is (= "status.im" (http/url-host "http://status.im")))) - (testing "Blank domainlocalhost" - (is (nil? (http/url-host "localhost:3000/testing"))))) - (testing "Return nil for Invalid URL" - (testing "Bad scheme" - (is (nil? (http/url-host "invalid//status.im/testing")))) - (testing "No scheme" - (is (nil? (http/url-host "status.im/testing")))))) - diff --git a/test/cljs/status_im/test/utils/keychain/core.cljs b/test/cljs/status_im/test/utils/keychain/core.cljs deleted file mode 100644 index e7334b48276..00000000000 --- a/test/cljs/status_im/test/utils/keychain/core.cljs +++ /dev/null @@ -1,78 +0,0 @@ -(ns status-im.test.utils.keychain.core - (:require [cljs.test :refer-macros [deftest async is testing]] - [status-im.react-native.js-dependencies :as rn] - [status-im.utils.keychain.core :as keychain])) - -(def strong-key (range 0 64)) -(def weak-key (concat (range 0 32) (take 32 (repeat 0)))) - -(defn- key->json [k] - (->> k - (keychain/bytes->js-array) - (.stringify js/JSON))) - -(deftest key-does-not-exists - (async - done - (with-redefs [rn/keychain #js {:getGenericPassword (constantly (.resolve js/Promise nil))}] - (testing "it returns a valid key" - (.. (keychain/get-encryption-key) - (then (fn [k] - (is (= strong-key (js->clj k))) - (done))) - (catch (fn [err] - (is (not err)) - (done)))))))) - -(deftest key-does-exists - (async - done - (with-redefs [rn/keychain #js {:getGenericPassword (constantly (.resolve js/Promise #js {:password (key->json (range 64 128))}))}] - (testing "it returns a valid key" - (.. (keychain/get-encryption-key) - (then (fn [k] - (is (= (range 64 128) (js->clj k))) - (done))) - (catch (fn [err] - (is (not err)) - (done)))))))) - -(deftest key-is-weak - (async - done - (with-redefs [rn/keychain #js {:getGenericPassword (constantly (.resolve js/Promise #js {:password (key->json weak-key)}))}] - (testing "it returns a valid key" - (.. (keychain/get-encryption-key) - (then (fn [_] - (is false) - (done))) - (catch (fn [{:keys [error key]}] - (is (= :weak-key error)) - (is (= weak-key (js->clj key))) - (done)))))))) - -(deftest safe-key-is-not-valid - (async - done - (with-redefs [rn/keychain #js {:getGenericPassword (constantly (.resolve js/Promise #js {:password (key->json weak-key)}))}] - (testing "it returns a valid key" - (.. (keychain/safe-get-encryption-key) - (then (fn [k] - (is (= weak-key (js->clj k))) - (done))) - (catch (fn [err] - (is (not err)) - (done)))))))) - -(deftest safe-key-is-nil - (async - done - (with-redefs [rn/keychain #js {:getGenericPassword (constantly (.resolve js/Promise #js {:password nil}))}] - (testing "it returns a valid key" - (.. (keychain/safe-get-encryption-key) - (then (fn [k] - (is (= "" (js->clj k))) - (done))) - (catch (fn [err] - (is (not err)) - (done)))))))) diff --git a/test/cljs/status_im/test/utils/money.cljs b/test/cljs/status_im/test/utils/money.cljs deleted file mode 100644 index b69a058b1c5..00000000000 --- a/test/cljs/status_im/test/utils/money.cljs +++ /dev/null @@ -1,23 +0,0 @@ -(ns status-im.test.utils.money - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.money :as money])) - -(deftest wei->ether - (testing "Numeric input, 15 significant digits" - (is (= (str (money/wei->ether 111122223333444000)) - "0.111122223333444"))) - (testing "String input, 18 significant digits" - (is (= (str (money/wei->ether "111122223333441239")) - "0.111122223333441239")))) - -(deftest valid? - (is (not (true? (money/valid? nil)))) - (is (true? (money/valid? (money/bignumber 0)))) - (is (true? (money/valid? (money/bignumber 1)))) - (is (not (true? (money/valid? (money/bignumber -1)))))) - -(deftest normalize - (is (= nil (money/normalize nil))) - (is (= "1" (money/normalize " 1 "))) - (is (= "1.1" (money/normalize "1.1"))) - (is (= "1.1" (money/normalize "1,1")))) \ No newline at end of file diff --git a/test/cljs/status_im/test/utils/prices.cljs b/test/cljs/status_im/test/utils/prices.cljs deleted file mode 100644 index b821c3cae48..00000000000 --- a/test/cljs/status_im/test/utils/prices.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.test.utils.prices - (:require [cljs.test :refer-macros [deftest is]] - [status-im.utils.prices :as prices])) - -(deftest test-format-price-resp - (is (= (prices/format-price-resp - "{\"RAW\":{\"ETH\":{\"USD\":{\"TYPE\":\"5\",\"MARKET\":\"CCCAGG\",\"FROMSYMBOL\":\"ETH\",\"TOSYMBOL\":\"USD\",\"FLAGS\":\"4\",\"PRICE\":677.91,\"LASTUPDATE\":1525241472,\"LASTVOLUME\":0.69661268,\"LASTVOLUMETO\":472.0874471092,\"LASTTRADEID\":\"1525241472.6714\",\"VOLUMEDAY\":109859.96594606241,\"VOLUMEDAYTO\":73924561.52673237,\"VOLUME24HOUR\":408811.69032152055,\"VOLUME24HOURTO\":270617692.0100031,\"OPENDAY\":670.81,\"HIGHDAY\":681.06,\"LOWDAY\":662.13,\"OPEN24HOUR\":658.68,\"HIGH24HOUR\":681.67,\"LOW24HOUR\":638.25,\"LASTMARKET\":\"Kraken\",\"CHANGE24HOUR\":19.230000000000018,\"CHANGEPCT24HOUR\":2.919475314264896,\"CHANGEDAY\":7.100000000000023,\"CHANGEPCTDAY\":1.0584219078427608,\"SUPPLY\":99176837.749,\"MKTCAP\":67232970078.42458,\"TOTALVOLUME24H\":1308785.4697758215,\"TOTALVOLUME24HTO\":880718916.8398683}},\"SNT\":{\"USD\":{\"TYPE\":\"5\",\"MARKET\":\"CCCAGG\",\"FROMSYMBOL\":\"SNT\",\"TOSYMBOL\":\"USD\",\"FLAGS\":\"1\",\"PRICE\":0.1562,\"LASTUPDATE\":1525241236,\"LASTVOLUME\":582.07218,\"LASTVOLUMETO\":90.919674516,\"LASTTRADEID\":\"237582608\",\"VOLUMEDAY\":259125.16382211001,\"VOLUMEDAYTO\":40450.5318867195,\"VOLUME24HOUR\":1451022.9449243098,\"VOLUME24HOURTO\":223465.71221695316,\"OPENDAY\":0.1573,\"HIGHDAY\":0.1592,\"LOWDAY\":0.1532,\"OPEN24HOUR\":0.15,\"HIGH24HOUR\":0.1657,\"LOW24HOUR\":0.1481,\"LASTMARKET\":\"Bitfinex\",\"CHANGE24HOUR\":0.006200000000000011,\"CHANGEPCT24HOUR\":4.133333333333341,\"CHANGEDAY\":-0.0010999999999999899,\"CHANGEPCTDAY\":-0.6993006993006928,\"SUPPLY\":6804870174.87817,\"MKTCAP\":1062920721.3159702,\"TOTALVOLUME24H\":239970152.87898657,\"TOTALVOLUME24HTO\":37480153.807917476}}},\"DISPLAY\":{\"ETH\":{\"USD\":{\"FROMSYMBOL\":\"Ξ\",\"TOSYMBOL\":\"$\",\"MARKET\":\"CryptoCompare Index\",\"PRICE\":\"$ 677.91\",\"LASTUPDATE\":\"Just now\",\"LASTVOLUME\":\"Ξ 0.6966\",\"LASTVOLUMETO\":\"$ 472.09\",\"LASTTRADEID\":\"1525241472.6714\",\"VOLUMEDAY\":\"Ξ 109,860.0\",\"VOLUMEDAYTO\":\"$ 73,924,561.5\",\"VOLUME24HOUR\":\"Ξ 408,811.7\",\"VOLUME24HOURTO\":\"$ 270,617,692.0\",\"OPENDAY\":\"$ 670.81\",\"HIGHDAY\":\"$ 681.06\",\"LOWDAY\":\"$ 662.13\",\"OPEN24HOUR\":\"$ 658.68\",\"HIGH24HOUR\":\"$ 681.67\",\"LOW24HOUR\":\"$ 638.25\",\"LASTMARKET\":\"Kraken\",\"CHANGE24HOUR\":\"$ 19.23\",\"CHANGEPCT24HOUR\":\"2.92\",\"CHANGEDAY\":\"$ 7.10\",\"CHANGEPCTDAY\":\"1.06\",\"SUPPLY\":\"Ξ 99,176,837.7\",\"MKTCAP\":\"$ 67.23 B\",\"TOTALVOLUME24H\":\"Ξ 1,308.79 K\",\"TOTALVOLUME24HTO\":\"$ 880.72 M\"}},\"SNT\":{\"USD\":{\"FROMSYMBOL\":\"SNT\",\"TOSYMBOL\":\"$\",\"MARKET\":\"CryptoCompare Index\",\"PRICE\":\"$ 0.1562\",\"LASTUPDATE\":\"3 min ago\",\"LASTVOLUME\":\"SNT 582.07\",\"LASTVOLUMETO\":\"$ 90.92\",\"LASTTRADEID\":\"237582608\",\"VOLUMEDAY\":\"SNT 259,125.2\",\"VOLUMEDAYTO\":\"$ 40,450.5\",\"VOLUME24HOUR\":\"SNT 1,451,022.9\",\"VOLUME24HOURTO\":\"$ 223,465.7\",\"OPENDAY\":\"$ 0.1573\",\"HIGHDAY\":\"$ 0.1592\",\"LOWDAY\":\"$ 0.1532\",\"OPEN24HOUR\":\"$ 0.1500\",\"HIGH24HOUR\":\"$ 0.1657\",\"LOW24HOUR\":\"$ 0.1481\",\"LASTMARKET\":\"Bitfinex\",\"CHANGE24HOUR\":\"$ 0.0062\",\"CHANGEPCT24HOUR\":\"4.13\",\"CHANGEDAY\":\"$ -0.0011\",\"CHANGEPCTDAY\":\"-0.70\",\"SUPPLY\":\"SNT 6,804,870,174.9\",\"MKTCAP\":\"$ 1,062.92 M\",\"TOTALVOLUME24H\":\"SNT 239.97 M\",\"TOTALVOLUME24HTO\":\"$ 37.48 M\"}}}}") - {:ETH {:USD {:from "ETH", :to "USD", :price 677.91, :last-day 658.68}} - :SNT {:USD {:from "SNT", :to "USD", :price 0.1562, :last-day 0.15}}}))) diff --git a/test/cljs/status_im/test/utils/random.cljs b/test/cljs/status_im/test/utils/random.cljs deleted file mode 100644 index 2095abb4d7c..00000000000 --- a/test/cljs/status_im/test/utils/random.cljs +++ /dev/null @@ -1,91 +0,0 @@ -(ns status-im.test.utils.random - (:require [cljs.test :refer-macros [deftest is]] - [status-im.utils.random :as random])) - -(deftest test-seeded-rand-int - ;try with one seed - (let [seed 0 - gen (random/rand-gen seed)] - (is (= (random/seeded-rand-int gen 100) 54)) - (is (= (random/seeded-rand-int gen 100) 59)) - (is (= (random/seeded-rand-int gen 10) 7)) - (is (= (random/seeded-rand-int gen 10) 8))) - ;repeat with the same seed, but generator re-set - (let [seed 0 - gen (random/rand-gen seed)] - (is (= (random/seeded-rand-int gen 100) 54)) - (is (= (random/seeded-rand-int gen 100) 59)) - (is (= (random/seeded-rand-int gen 10) 7)) - (is (= (random/seeded-rand-int gen 10) 8))) - ;a string seed - (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" - gen (random/rand-gen seed)] - (is (= (random/seeded-rand-int gen 100) 7)) - (is (= (random/seeded-rand-int gen 100) 4)) - (is (= (random/seeded-rand-int gen 10) 5)) - (is (= (random/seeded-rand-int gen 10) 7))) - ;a string seed again - (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" - gen (random/rand-gen seed)] - (is (= (random/seeded-rand-int gen 100) 7)) - (is (= (random/seeded-rand-int gen 100) 4)) - (is (= (random/seeded-rand-int gen 10) 5)) - (is (= (random/seeded-rand-int gen 10) 7))) - ;nil seed is the same as 0 - (let [seed 0 - gen (random/rand-gen seed)] - (is (= (random/seeded-rand-int gen 100) 54)) - (is (= (random/seeded-rand-int gen 100) 59)) - (is (= (random/seeded-rand-int gen 10) 7)) - (is (= (random/seeded-rand-int gen 10) 8)))) - -(deftest test-seeded-rand-int-boundaries - (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" - n 10 - gen (random/rand-gen seed) - sample (into #{} (repeatedly 1000 #(random/seeded-rand-int gen n)))] - ;no result should be negative - (is (empty? (filter #(< % 0) sample))) - ;no result should be larger than n - (is (empty? (filter #(>= % n) sample))) - ;and while there is a very small probability - ;it is very unlikely and probably wrong if all 1000 calls - ;got us the same number. - (is (> (count sample) 1)))) - -(deftest test-seeded-rand-nth - (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" - gen (random/rand-gen seed) - coll [:a :b :c :d :e :f :g]] - (is (= (random/seeded-rand-nth gen coll) :a)) - (is (= (random/seeded-rand-nth gen coll) :a)) - (is (= (random/seeded-rand-nth gen coll) :e)) - (is (= (random/seeded-rand-nth gen coll) :f)) - (is (= (random/seeded-rand-nth gen coll) :c))) - ;try again with the same seed but gen reset - (let [seed "6ec565f4fec866a54761524f603cf037e20c2bfa" - gen (random/rand-gen seed) - coll [:a :b :c :d :e :f :g]] - (is (= (random/seeded-rand-nth gen coll) :a)) - (is (= (random/seeded-rand-nth gen coll) :a)) - (is (= (random/seeded-rand-nth gen coll) :e)) - (is (= (random/seeded-rand-nth gen coll) :f)) - (is (= (random/seeded-rand-nth gen coll) :c))) - ;try again with a different seed - (let [seed "57348975ff9199ca636207a396b915d6b6a675b4" - gen (random/rand-gen seed) - coll [:a :b :c :d :e :f :g]] - (is (= (random/seeded-rand-nth gen coll) :f)) - (is (= (random/seeded-rand-nth gen coll) :c)) - (is (= (random/seeded-rand-nth gen coll) :e)) - (is (= (random/seeded-rand-nth gen coll) :e)) - (is (= (random/seeded-rand-nth gen coll) :a))) - ;and re-set - (let [seed "57348975ff9199ca636207a396b915d6b6a675b4" - gen (random/rand-gen seed) - coll [:a :b :c :d :e :f :g]] - (is (= (random/seeded-rand-nth gen coll) :f)) - (is (= (random/seeded-rand-nth gen coll) :c)) - (is (= (random/seeded-rand-nth gen coll) :e)) - (is (= (random/seeded-rand-nth gen coll) :e)) - (is (= (random/seeded-rand-nth gen coll) :a)))) \ No newline at end of file diff --git a/test/cljs/status_im/test/utils/signing_phrase/core.cljs b/test/cljs/status_im/test/utils/signing_phrase/core.cljs deleted file mode 100644 index c82c28f9fa1..00000000000 --- a/test/cljs/status_im/test/utils/signing_phrase/core.cljs +++ /dev/null @@ -1,11 +0,0 @@ -(ns status-im.test.utils.signing-phrase.core - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.signing-phrase.core :refer [generate]] - [clojure.string :as string])) - -(deftest test-generate - (doseq [_ (range 30)] - (let [result (generate)] - (is (not (string/starts-with? result " "))) - (is (not (string/ends-with? result " "))) - (is (= (get (frequencies result) " ")) 2)))) diff --git a/test/cljs/status_im/test/utils/transducers.cljs b/test/cljs/status_im/test/utils/transducers.cljs deleted file mode 100644 index 95758b97bc1..00000000000 --- a/test/cljs/status_im/test/utils/transducers.cljs +++ /dev/null @@ -1,61 +0,0 @@ -(ns status-im.test.utils.transducers - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.transducers :as transducers])) - -(def ^:private preview-call-1 - {:jail-id 1 - :path [:preview] - :params {:chat-id 1} - :callback (fn [] - [[:msg-id 1]])}) - -(def ^:private preview-call-2 - {:jail-id 1 - :path [:preview] - :params {:chat-id 1} - :callback (fn [] - [[:msg-id 2]])}) - -(def ^:private jail-calls - '({:jail-id 1 - :path [:suggestions] - :params {:arg 0}} - {:jail-id 1 - :path [:function] - :params {:sub :a}} - {:jail-id 1 - :path [:function] - :params {:sub :b}} - {:jail-id 1 - :path [:suggestions] - :params {:arg 1}} - {:jail-id 1 - :path [:suggestions] - :params {:arg 2}} - preview-call-1 - preview-call-2)) - -(deftest last-distinct-by-test - (testing "Elements are removed from input according to provided `compare-fn`, - when duplicate elements are removed, the last one stays" - (is (= (sequence (transducers/last-distinct-by (fn [{:keys [jail-id path] :as call}] - (if (= :suggestions (last path)) - [jail-id path] - call))) - jail-calls) - '({:jail-id 1 - :path [:suggestions] - :params {:arg 2}} - {:jail-id 1 - :path [:function] - :params {:sub :a}} - {:jail-id 1 - :path [:function] - :params {:sub :b}} - preview-call-1 - preview-call-2)))) - (testing "Edge cases with input size `N=0` and `N=1` work as well" - (is (= (sequence (transducers/last-distinct-by identity) '()) - '())) - (is (= (sequence (transducers/last-distinct-by identity) '(1)) - '(1))))) diff --git a/test/cljs/status_im/test/utils/universal_links/core.cljs b/test/cljs/status_im/test/utils/universal_links/core.cljs deleted file mode 100644 index cecbcd71eb9..00000000000 --- a/test/cljs/status_im/test/utils/universal_links/core.cljs +++ /dev/null @@ -1,86 +0,0 @@ -(ns status-im.test.utils.universal-links.core - (:require [cljs.test :refer-macros [deftest is testing]] - [re-frame.core :as re-frame] - [status-im.utils.universal-links.core :as links])) - -(deftest handle-url-test - (testing "the user is not logged in" - (testing "it stores the url for later processing" - (is (= {:db {:universal-links/url "some-url"}} - (links/handle-url "some-url" {:db {}}))))) - (testing "the user is logged in" - (let [db {:account/account {:public-key "pk"} - :universal-links/url "some-url"}] - (testing "it clears the url" - (is (nil? (get-in (links/handle-url "some-url" - {:db db}) - [:db :universal-links/url])))) - (testing "a public chat link" - (testing "it joins the chat" - (is (get-in (links/handle-url "status-im://chat/public/status" - {:db db}) - [:db :chats "status"])))) - - (testing "a browse dapp link" - (testing "it open the dapps" - (is - (= "status-im://browse/www.cryptokitties.co" - (:browse (links/handle-url "status-im://browse/www.cryptokitties.co" - {:db db})))))) - (testing "a user profile link" - (testing "it loads the profile" - (let [actual (links/handle-url "status-im://user/profile-id" - {:db db})] - (is (= "profile-id" (get-in actual [:db :contacts/identity]))) - (is (= :profile (get-in actual [:db :view-id])))))) - (testing "a not found url" - (testing "it does nothing" - (is (nil? (links/handle-url "status-im://not-existing" - {:db db})))))))) - -(deftest url-event-listener - (testing "the url is not nil" - (testing "it dispatches the url" - (let [actual (atom nil)] - (with-redefs [re-frame/dispatch #(reset! actual %)] - (links/url-event-listener #js {:url "some-url"}) - (is (= [:handle-universal-link "some-url"] @actual)))))) - (testing "the url is nil" - (testing "it does not dispatches the url" - (let [actual (atom nil)] - (with-redefs [re-frame/dispatch #(reset! actual %)] - (links/url-event-listener #js {}) - (is (= nil @actual))))))) - -(deftest universal-link-test - (testing "status-im://blah" - (testing "it returns true" - (is (links/universal-link? "status-im://blah")))) - (testing "http://get.status.im/blah" - (testing "it returns true" - (is (links/universal-link? "http://get.status.im/blah")))) - (testing "https://get.status.im/blah" - (testing "it returns true" - (is (links/universal-link? "https://get.status.im/blah")))) - (testing "unicode characters" - (testing "it returns false" - (is (not (links/universal-link? "https://get.status.im/browse/www.аррӏе.com"))))) - (testing "not-status-im://blah" - (testing "it returns false" - (is (not (links/universal-link? "https://not.status.im/blah"))))) - (testing "http://not.status.im/blah" - (testing "it returns false" - (is (not (links/universal-link? "https://not.status.im/blah"))))) - (testing "https://not.status.im/blah" - (testing "it returns false" - (is (not (links/universal-link? "https://not.status.im/blah")))))) - -(deftest stored-url-event - (testing "the url is in the database" - (testing "it returns the event" - (= [:handle-universal-link "some-url"] - (links/stored-url-event {:db {:universal-links/url "some-url"}})))) - (testing "the url is not in the database" - (testing "it returns nil" - (= nil - (links/stored-url-event {:db {}}))))) diff --git a/test/cljs/status_im/test/utils/utils.cljs b/test/cljs/status_im/test/utils/utils.cljs deleted file mode 100644 index d966e45ea45..00000000000 --- a/test/cljs/status_im/test/utils/utils.cljs +++ /dev/null @@ -1,50 +0,0 @@ -(ns status-im.test.utils.utils - (:require [cljs.test :refer-macros [deftest is]] - [status-im.utils.core :as u])) - -(deftest wrap-as-call-once-test - (let [count (atom 0)] - (letfn [(inc-count [] (swap! count inc))] - (let [f (u/wrap-call-once! inc-count)] - (is (nil? (f))) - (is (= 1 @count)) - (is (nil? (f))) - (is (= 1 @count)))))) - -(deftest truncate-str-test - (is (= (u/truncate-str "Long string" 7) "Long...")) ; threshold is less then string length - (is (= (u/truncate-str "Long string" 7 true) "Lo...ng")) ; threshold is less then string length (truncate middle) - (is (= (u/truncate-str "Long string" 11) "Long string")) ; threshold is the same as string length - (is (= (u/truncate-str "Long string" 20) "Long string"))) ; threshold is more then string length - -(deftest clean-text-test - (is (= (u/clean-text "Hello! \n\r") "Hello!") - (= (u/clean-text "Hello!") "Hello!"))) - -(deftest first-index-test - (is (= 2 (u/first-index (partial = :test) - '(:a :b :test :c :test)))) - (is (= nil (u/first-index (partial = :test) - '(:a :b :c))))) - -(deftest hash-tag?-test - (is (u/hash-tag? "#clojure")) - (is (not (u/hash-tag? "clojure"))) - (is (not (u/hash-tag? "clo#jure"))) - (is (not (u/hash-tag? "clojure#")))) - -(deftest update-if-present-test - (is (= {:a 1} (u/update-if-present {:a 0} :a inc))) - (is (= {:a 2} (u/update-if-present {:a 0} :a + 2))) - (is (= {:a 0} (u/update-if-present {:a 0} :b inc)))) - -(deftest map-values-test - (is (= {} (u/map-values {} inc))) - (is (= {:a 1} (u/map-values {:a 0} inc))) - (is (= {:a 1 :b 2} (u/map-values {:a 0 :b 1} inc)))) - -(deftest deep-merge-test - (is (= {} (u/deep-merge {} {}))) - (is (= {:a 1 :b 2} (u/deep-merge {:a 1} {:b 2}))) - (is (= {:a {:b 1 :c 2}} (u/deep-merge {:a {:b 1 :c 1}} {:a {:c 2}}))) - (is (= {:a {:b {:c 2}} :d 1} (u/deep-merge {:a {:b {:c 1}} :d 1} {:a {:b {:c 2}}})))) diff --git a/test/cljs/status_im/test/wallet/subs.cljs b/test/cljs/status_im/test/wallet/subs.cljs deleted file mode 100644 index 0be9d0d7147..00000000000 --- a/test/cljs/status_im/test/wallet/subs.cljs +++ /dev/null @@ -1,17 +0,0 @@ -(ns status-im.test.wallet.subs - (:require [cljs.test :refer-macros [deftest is]] - [status-im.utils.money :as money] - [status-im.ui.screens.wallet.subs :as s])) - -(deftest test-balance-total-value - (is (= (s/get-balance-total-value {:ETH (money/bignumber 1000000000000000000) - :SNT (money/bignumber 100000000000000000000) - :AST (money/bignumber 10000)} - {:ETH {:USD {:from "ETH", :to "USD", :price 677.91, :last-day 658.688}} - :SNT {:USD {:from "SNT", :to "USD", :price 0.1562, :last-day 0.15}} - :AST {:USD {:from "AST", :to "USD", :price 4, :last-day 3}}} - :USD - {:ETH 18 - :SNT 18 - :AST 4}) - 697.53))) diff --git a/test/cljs/status_im/test/wallet/transactions.cljs b/test/cljs/status_im/test/wallet/transactions.cljs deleted file mode 100644 index c27dd64c283..00000000000 --- a/test/cljs/status_im/test/wallet/transactions.cljs +++ /dev/null @@ -1,138 +0,0 @@ -(ns status-im.test.wallet.transactions - (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.datetime :as time] - [status-im.wallet.transactions :as wallet.transactions])) - -(deftest test-store-chat-transaction-hash - (is (= (wallet.transactions/store-chat-transaction-hash "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db" - {:db {:wallet {:chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}}}}) - {:db {:wallet {:chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b" "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}}}}))) - -(deftest test-load-missing-chat-transactions - (is (= (wallet.transactions/load-missing-chat-transactions {:db {:wallet {:transactions {"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db" - {:confirmations "15"}}} - :chats {"0x0462203ba96495c4caed83b46b01fe9b8f27af300f46cc944c6c47141969337ecfc4566dd5910323763816d350d3256a3d8f2f2a6536cfa120733aeea2efb11860" - {:messages {"0xfc6e31d35f5a722b8aa185c8c7d1cd798e546e8dcd97adcc8305ca1a3c044f0d" - {:content-type "command" - :content {:params {:tx-hash "0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}}}}} - "0x04d9bbe8e00318a70108980b45ba45c91cd406cdd7ffeb043be0ce4ec026fed3854515345b4bbce4c1694cc99240ee5fda93101ca861c3ceef548dedb5ec2a983a" - {:messages {"0x1f8a9472997b58435a43776ec5a89a12e42891f71064fd890ef3d87dda41fabf" - {:content-type "command" - :content {:params {:tx-hash "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}}}}}}}}) - - {:db {:wallet {:transactions {"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db" - {:confirmations "15"}} - :chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}} - :chats {"0x0462203ba96495c4caed83b46b01fe9b8f27af300f46cc944c6c47141969337ecfc4566dd5910323763816d350d3256a3d8f2f2a6536cfa120733aeea2efb11860" - {:messages {"0xfc6e31d35f5a722b8aa185c8c7d1cd798e546e8dcd97adcc8305ca1a3c044f0d" - {:content-type "command" - :content {:params {:tx-hash "0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}}}}} - "0x04d9bbe8e00318a70108980b45ba45c91cd406cdd7ffeb043be0ce4ec026fed3854515345b4bbce4c1694cc99240ee5fda93101ca861c3ceef548dedb5ec2a983a" - {:messages {"0x1f8a9472997b58435a43776ec5a89a12e42891f71064fd890ef3d87dda41fabf" - {:content-type "command" - :content {:params {:tx-hash "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}}}}}}}}))) - -(deftest test-sync - (with-redefs [time/timestamp (constantly 1531734120829)] - (testing "update in progress" - (is (= (wallet.transactions/sync {:db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "0"}} - :transactions-loading? true}}}) - {:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}))) - (testing "app is offline" - (is (= (wallet.transactions/sync {:db {:app-state "active" - :network-status :offline - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "0"}}}}}) - {:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}))) - (testing "app is in background" - (is (= (wallet.transactions/sync {:db {:app-state "background" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "0"}}}}}) - {:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}))) - (testing "last request was made recently" - (is (= (wallet.transactions/sync {:db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "10"}} - :transactions-last-updated-at 1531734119729 - :transactions-loading? false}}}) - {:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}))) - (testing "transaction has 0 confirmations" - (is (= (wallet.transactions/sync {:db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "0"}} - :transactions-loading? false}}}) - {:get-transactions {:account-id nil - :token-addresses (), - :chain nil - :web3 nil - :success-event :update-transactions-success - :error-event :update-transactions-fail} - :db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "0"}} - :transactions-loading? true - :transactions-last-updated-at 1531734120829 - :errors nil}} - :utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}))) - (testing "transaction has 10 confirmations" - (is (= (wallet.transactions/sync {:db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "10"}} - :transactions-loading? false}}}) - {:get-transactions {:account-id nil - :token-addresses (), - :chain nil - :web3 nil - :success-event :update-transactions-success - :error-event :update-transactions-fail} - :db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "10"}} - :transactions-loading? true - :transactions-last-updated-at 1531734120829 - :errors nil}} - :utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}))) - (testing "chat transaction is missing from wallet transactions" - (is (= (wallet.transactions/sync {:db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "15"} - "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db" - {:confirmations "20"}} - :chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b" - "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"} - :transactions-loading? false}}}) - {:get-transactions {:account-id nil - :token-addresses (), - :chain nil - :web3 nil - :success-event :update-transactions-success - :error-event :update-transactions-fail} - :db {:app-state "active" - :network-status :online - :wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295" - {:confirmations "15"} - "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db" - {:confirmations "20"}} - :chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b" - "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"} - :transactions-loading? true - :transactions-last-updated-at 1531734120829 - :errors nil}} - :utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms - :dispatch [:sync-wallet-transactions]}]}))))) diff --git a/test/cljs/status_im/test/wallet/transactions/subs.cljs b/test/cljs/status_im/test/wallet/transactions/subs.cljs deleted file mode 100644 index 01d672358f9..00000000000 --- a/test/cljs/status_im/test/wallet/transactions/subs.cljs +++ /dev/null @@ -1,28 +0,0 @@ -(ns status-im.test.wallet.transactions.subs - (:require [cljs.test :refer [deftest is testing]] - reagent.core - [re-frame.core :as re-frame] - [day8.re-frame.test :refer [run-test-sync]] - status-im.ui.screens.db - status-im.ui.screens.subs - [status-im.ui.screens.events :as events] - [status-im.ui.screens.wallet.transactions.subs :as transactions-subs])) - -(def transactions [{:timestamp "1505912551000"} - {:timestamp "1505764322000"} - {:timestamp "1505750000000"}]) - -(def grouped-transactions '({:title "20 Sep" - :key :20170920 - :data - ({:timestamp "1505912551000"})} - {:title "18 Sep" - :key :20170918 - :data - ({:timestamp "1505764322000"} - {:timestamp "1505750000000"})})) - -(deftest group-transactions-by-date - "Check if transactions are sorted by date" - (is (= (transactions-subs/group-transactions-by-date transactions) - grouped-transactions))) diff --git a/test/cljs/status_im/test/wallet/transactions/views.cljs b/test/cljs/status_im/test/wallet/transactions/views.cljs deleted file mode 100644 index 5bce9ad1881..00000000000 --- a/test/cljs/status_im/test/wallet/transactions/views.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.test.wallet.transactions.views - (:require [cljs.test :refer [deftest is testing]] - [status-im.ui.screens.wallet.transactions.views :as views])) - -(deftest filtered-transaction? - (is (not (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :outbound :checked? true}]})))) - (is (not (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :inbound :checked? false}]})))) - (is (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :inbound :checked? true}]}))) - (is (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :outbound :checked? true} {:id :inbound :checked? true}]})))) - -(deftest update-transactions - (is (= {:data '()} (views/update-transactions {:data {:type :inbound}} {:type [{:id :outbound :checked? true}]})))) \ No newline at end of file diff --git a/test/docker/README.md b/test/docker/README.md index a03acbee87b..6c2079d13df 100644 --- a/test/docker/README.md +++ b/test/docker/README.md @@ -1,4 +1,4 @@ -Status react protocol tests +Status Mobile protocol tests ===================== ## Run the tests diff --git a/test/jest/jest.config.js b/test/jest/jest.config.js new file mode 100644 index 00000000000..ff8533b28b4 --- /dev/null +++ b/test/jest/jest.config.js @@ -0,0 +1,70 @@ +const transformIgnorePatterns = () => { + const libs = [ + '@react-native', + '@react-native-community', + '@react-native-community/blur', + 'react-native', + 'react-native-config', + 'react-native-background-timer', + 'react-native-gifted-charts', + 'react-native-hole-view', + 'react-native-image-crop-picker', + 'react-native-linear-gradient', + 'react-native-permissions', + 'react-native-reanimated', + 'react-native-redash', + 'react-native-redash', + 'react-native-shadow-2', + 'react-native-shake', + 'react-native-safe-area-context', + ].join('|'); + + return [`/node_modules/(?!(${libs})/).*/`]; +}; + +const shouldUseSilentReporter = () => { + return process.env.JEST_USE_SILENT_REPORTER === 'true'; +}; + +const reporters = () => { + let reporters = []; + if (shouldUseSilentReporter()) { + reporters.push(['jest-silent-reporter', { useDots: true }]); + } else { + reporters.push('default'); + } + return reporters; +}; + +module.exports = { + preset: 'react-native', + setupFilesAfterEnv: [ + '@testing-library/jest-native/extend-expect', + '../component-spec/status_im.setup.schema_preload.js', + '../component-spec/test_helpers.component_tests_preload.js', + '../test/jest/jestSetup.js', + ], + reporters: reporters(), + setupFiles: [], + testPathIgnorePatterns: [], + moduleNameMapper: { + '^[@./a-zA-Z0-9$_-]+\\.(png|jpg|jpeg|gif)$': + '/../node_modules/react-native/Libraries/Image/RelativeImageStub', + }, + testTimeout: 60000, + transformIgnorePatterns: transformIgnorePatterns(), + // This is a workaround after upgrading to Jest 29.7.0, otherwise we get: + // + // SyntaxError: node_modules/@react-native/js-polyfills/error-guard.js: + // Missing semicolon. (14:4) + // + transform: { + '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { configFile: './babel.config.js' }], + }, + globals: { + __TEST__: true, + }, + testEnvironment: 'node', + rootDir: '../../component-spec', + testMatch: ['**/*__tests__*', '**/*.component_spec.js'], +}; diff --git a/test/jest/jestSetup.js b/test/jest/jestSetup.js new file mode 100644 index 00000000000..233d52caa13 --- /dev/null +++ b/test/jest/jestSetup.js @@ -0,0 +1,131 @@ +const WebSocket = require('ws'); +const { NativeModules } = require('react-native'); + +mockAsyncStorage = require('@react-native-async-storage/async-storage/jest/async-storage-mock'); +require('react-native-gesture-handler/jestSetup'); +require('react-native-reanimated/src/reanimated2/jestUtils').setUpTests(); + +jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); + +jest.mock('react-native-fs', () => ({ + default: {}, +})); + +jest.mock('react-native-share', () => ({ + default: {}, +})); + +jest.mock('react-native-navigation', () => ({ + getNavigationConstants: () => ({ constants: [] }), + Navigation: { + constants: async () => ({ + statusBarHeight: 10, + topBarHeight: 10, + bottomTabsHeight: 10, + }), + }, +})); + +jest.mock('react-native-background-timer', () => ({})); + +jest.mock('react-native-safe-area-context', () => ({ + initialWindowMetrics: + { + insets: + { + top: 0, + bottom: 0 + } + }, + SafeAreaProvider:{} +})); + +jest.mock('react-native-permissions', () => require('react-native-permissions/mock')); + +jest.mock('@react-native-clipboard/clipboard', () => ({ + getString: jest.fn().mockResolvedValue('mockString'), + getImagePNG: jest.fn(), + getImageJPG: jest.fn(), + setImage: jest.fn(), + setString: jest.fn(), + hasString: jest.fn().mockResolvedValue(true), + hasImage: jest.fn().mockResolvedValue(true), + hasURL: jest.fn().mockResolvedValue(true), + addListener: jest.fn(), + removeAllListeners: jest.fn(), + getEnforcing: jest.fn(), + useClipboard: jest.fn(() => ['mockString', jest.fn()]), +})); + +jest.mock('@react-native-community/audio-toolkit', () => ({ + Recorder: jest.fn().mockImplementation(() => ({ + prepare: jest.fn(), + record: jest.fn(), + toggleRecord: jest.fn(), + pause: jest.fn(), + stop: jest.fn(), + on: jest.fn(), + })), + Player: jest.fn().mockImplementation(() => ({ + prepare: jest.fn(), + playPause: jest.fn(), + play: jest.fn(), + pause: jest.fn(), + stop: jest.fn(), + seek: jest.fn(), + on: jest.fn(), + })), + MediaStates: { + DESTROYED: -2, + ERROR: -1, + IDLE: 0, + PREPARING: 1, + PREPARED: 2, + SEEKING: 3, + PLAYING: 4, + RECORDING: 4, + PAUSED: 5, + }, + PlaybackCategories: { + Playback: 1, + Ambient: 2, + SoloAmbient: 3, + }, +})); + +jest.mock('i18n-js', () => ({ + ...jest.requireActual('i18n-js'), + t: (label) => `tx:${label}`, +})); + +jest.mock('react-native-blob-util', () => ({ + default: { + config: jest.fn().mockReturnValue({ + fetch: jest.fn(), + }), + }, +})); + +jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock')); + +jest.mock('react-native-safe-area-context', () => ({ + initialWindowMetrics: + { + insets: + { + top: 0, + bottom: 0 + } + }, + SafeAreaProvider:{} +})); + +NativeModules.ReactLocalization = { + language: 'en', + locale: 'en', +}; +global.navigator = { + userAgent: 'node', +}; + +global.WebSocket = WebSocket; diff --git a/translations/af.json b/translations/af.json new file mode 100644 index 00000000000..eff9f9a5c19 --- /dev/null +++ b/translations/af.json @@ -0,0 +1,127 @@ +{ + "confirm": "Bevestig", + "amount": "Bedrag", + "members-active": { + "one": "1 lid, 1 aktief", + "other": "{{count}} lede, {{count}} aktief", + "zero": "geen lede" + }, + "chat-name": "Bynaam", + "phew-here-is-your-passphrase": "*Sjoe* dit was moeilik, hier is jou tydelike wagwoord, *skryf dit neer en hou dit veilig!* Jy sal dit nodig hê om jou rekening te herwin.", + "public-group-topic": "Onderwerp", + "chat-settings": "Geselsie-stellings", + "offline": "Aflyn", + "invited": "uitgenooi", + "address": "Adres", + "new-public-group-chat": "Sluit aan by openbare klets", + "datetime-hour": { + "one": "uur", + "other": "ure" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "Verwyder", + "add-members": "Voeg lede by", + "done": "Gedoen", + "close-chat": "Skrap klets", + "new-group-chat": "Nuwe groepgeselsie", + "sign-in": "Teken aan", + "datetime-yesterday": "gister", + "create-new-account": "Skep nuwe rekening", + "datetime-ago": "gelede", + "contacts": "Kontakte", + "got-it": "Het dit", + "active-online": "Aanlyn", + "password": "Wagwoord", + "browsing-title": "Blaai", + "discover": "Ontdekking", + "browsing-cancel": "Kanselleer", + "intro-status": "Gesels met my om jou rekening op te stel en jou stellings te verander!", + "name": "Naam", + "show-qr": "Wys QR", + "connect": "Konnekteer", + "edit": "Wysig", + "account-generation-message": "Net 'n oomblik, ek moet mal somme doen om jou rekening te skep!", + "no-messages": "Geen boodskappe", + "passphrase": "Tydelike wagwoord", + "recipient": "Ontvanger", + "members-title": "lede", + "new-group": "Nuwe groep", + "phone-e164": "Internasionaal 1", + "settings": "Stellings", + "chats": "Geselsies", + "transaction": "Transaksie", + "public-group-status": "Openbaar", + "image-source-make-photo": "Opname", + "start-conversation": "Begin gesprek", + "save": "Stoor", + "sharing-copy-to-clipboard": "Kopieer na knipbord", + "sync-in-progress": "Besig om te sinchroniseer...", + "send-transaction": "Stuur transaksie", + "incorrect-code": [ + "str", + "Jammer, die kode was nie korrek nie, voer asseblief weer in" + ], + "image-source-gallery": "Kies uit galery", + "sync-synced": "gesinchroniseer", + "status-pending": "Hangende", + "datetime-day": { + "one": "dag", + "other": "dae" + }, + "scan-qr": "Skandeer QR", + "contact-s": { + "one": "kontak", + "other": "kontakte" + }, + "next": "Volgende", + "recent": "Onlangs", + "status": "Status", + "from": "Van", + "wrong-password": "Verkeerde wagwoord", + "in-contacts": "In kontakte", + "sharing-share": "Deel...", + "type-a-message": "Tik ‘n boodskap...", + "clear-history": "Maak geskiedenis skoon", + "no-contacts": "Nog geen kontakte nie", + "datetime-today": "vandag", + "web-view-error": "oepsie, fout", + "error": "Fout", + "more": "meer", + "cancel": "Kanselleer", + "can-not-add-yourself": "Jy kan nie jouself byvoeg nie", + "add-to-contacts": "Voeg by kontakte", + "available": "Beskikbaar", + "You": "Jou", + "main-wallet": "Hoofbeursie", + "members": { + "one": "1 lid", + "other": "{{count}} lede", + "zero": "geen lede" + }, + "intro-message1": "Welkom by Status\nTik tik hierdie boodskap om jou wagwoord te stel & laat ons begin!", + "new-contact": "Nuwe kontak", + "datetime-second": { + "one": "sekonde", + "other": "sekondes" + }, + "recover": "Herstel", + "datetime-minute": { + "one": "minuut", + "other": "minute" + }, + "browsing-open-in-android-web-browser": "Maak oop in webblaaier", + "browsing-open-in-ios-web-browser": "Maak oop in webblaaier", + "edit-profile": "Redigeer profiel", + "active-unknown": "Onbekend", + "public-key": "Openbare sleutel", + "profile": "Profiel", + "none": "Geen", + "removed": "verwyder", + "message": "Boodskap", + "here-is-your-passphrase": "Hier is jou tydelike wagwoord, *skryf dit neer en hou dit veilig!* Jy sal dit nodig hê om jou rekening te herwin.", + "image-source-title": "Profielbeeld", + "left": "uitgegaan", + "to": "Aan", + "status-sent": "Gestuur", + "data": "Data" +} diff --git a/translations/ar.json b/translations/ar.json new file mode 100644 index 00000000000..8a51f216767 --- /dev/null +++ b/translations/ar.json @@ -0,0 +1,1796 @@ +{ + "You": "أنت", + "about-app": "حول", + "about-key-storage-content": "لن تصل الحالة مطلقًا إلى مفتاحك الخاص. تأكد من النسخ الاحتياطي عبارة البذور الخاصة بك. إذا فقدت هاتفك ، فهذه هي الطريقة الوحيدة للوصول إلى مفاتيحك.", + "about-key-storage-title": "حول التخزين الرئيسي", + "about-names-content": "هويتك آمنة وخاصة من خلال التصميم. تحصل على مفتاح تشفير تم إنشاؤه محليًا. الاسم والصورة هي نسخة قابلة للقراءة من هذا. إنها فريدة من نوعها. لا أحد يستطيع أن يدعي أنه أنت. لا أحد يرى اسمك الحقيقي إلا إذا قدمته أنت.", + "about-names-title": "لا يمكن تغيير الأسماء", + "about-sharing-data": "حول مشاركة البيانات", + "accept": "قبول", + "accept-and-add": "قبول وإضافة", + "accept-and-continue": "قبول ومتابعة", + "accept-and-share-address": "قبول العنوان ومشاركته", + "accept-new-chats-from": "قبول الدردشات الجديدة من", + "accept-status-tos-prefix": "أنا أقبل Status", + "access-existing-keys": "الوصول إلى المفاتيح الحالية", + "access-key": "مفتاح الوصول", + "account-added": "تمت أضافه الحساب", + "account-color": "لون الحساب", + "account-content": "يمكنك مقارنة الحسابات في الحالة إلى الحسابات المصرفية. مثل الحساب المصرفي ، عادة ما يكون للحساب عنوان ورصيد ؛ يمكنك استخدام هذا الحساب لإجراء المعاملات على الإيتييوم. يمكن أن يكون لديك حسابات متعددة في محفظتك. يمكن الوصول إليها جميعًا عن طريق فتح الحالة.", + "account-exists-title": "الحساب موجود بالفعل", + "account-is-used": "يتم استخدام الحساب مع التطبيقات اللامركزية في المتصفح.", + "account-name": "اسم الحساب", + "account-settings": "إعدادات الحساب", + "account-title": "الحساب", + "accounts": "حسابات", + "actions": "أجراءات", + "active-online": "متصل", + "active-unknown": "غير معروف", + "activity": "نشاط", + "add": "إضافة", + "add-a-watch-account": "أضافه عنوان للمشاهدة فقط", + "add-account": "إضافة حساب", + "add-account-description": "يمكنك استيراد أي نوع من حسابات Ethereum لإضافته إلى محفظة Status الخاصة بك", + "add-account-incorrect-password": "يبدو أن كلمة المرور غير صحيحة. أدخل كلمة المرور التي تستخدمها لإلغاء قفل التطبيق.", + "add-an-account": "حساب جديد", + "add-bootnode": "إضافة bootnode", + "add-contact": "إضافة جهة اتصال", + "add-custom-token": "إضافة عملة رمزية مخصص", + "add-favourite": "إضافة المفضلة", + "add-mailserver": "إضافة عقدة Status", + "add-members": "إضافة أعضاء", + "add-network": "اضافة شبكة", + "add-new-contact": "إضافة جهة اتصال جديدة", + "add-nickname": "إضافة لقب (اختياري)", + "add-node": "أضف العقدة", + "add-private-key-account": "إضافة حساب من مفتاح خاص", + "add-seed-account": "أضف حسابًا بعبارة أولية", + "add-to-contacts": "أضف إلى جهات الاتصال", + "add-to-contacts-text": "بإضافة مستخدم إلى قائمة جهات الاتصال الخاصة بك ، يمكنك مشاركة عنوان محفظتك", + "add-to-favourites": "إضافة إلى المفضلة", + "add-watch-account": "أضف حسابًا للمراقبة فقط", + "address": "عنوان", + "address-or-ens-name": "العنوان أو اسم ENS", + "address-received": "العنوان المستلم", + "address-request-accepted": "قبول طلب العنوان", + "address-requested": "العنوان المطلوب", + "advanced": "المتقدمة", + "advanced-settings": "إعدادات متقدمة", + "advertiser-description": "لقد اكتشفت Status بفضل شريك. هل تمانع إذا كانت الحالة تتحقق من عنوان IP الخاص بك مرة واحدة حتى يتم مكافأتها؟ لن يتم استخدام هذه المعلومات لأي شيء آخر وستتم إزالتها تمامًا بعد 7 أيام.", + "advertiser-starter-pack-accept": "قبول", + "advertiser-starter-pack-decline": "رفض الدعوة", + "advertiser-starter-pack-description": "إليك بعض من العملة الرقمية لتبدأ! استخدمه للحصول على ملصقات ، واسم ENS و تجربت dapps", + "advertiser-starter-pack-title": "حزمة المبتدئين", + "advertiser-title": "الخصوصية بشكل افتراضي", + "agree-by-continuing": "بالمتابعة أنت توافق \n الينا", + "all": "الكل", + "allow": "السماح", + "allow-and-send": "السماح والإرسال", + "allow-mention-notifications": "إظهار @ الإشارات", + "allowing-authorizes-this-dapp": "يسمح لـ DApp باسترداد عنوان محفظتك وتمكين Web3", + "already-have-asset": "لديك بالفعل هذا الأصل", + "amount": "المبلغ", + "anyone": "اي شخص", + "app-commit": "فرض التطبيق", + "appearance": "مظهر خارجي", + "apply": "سجل", + "approve": "موافقة", + "approve-limit": "حد الموافقة", + "approve-token": "الموافقة على عملة رمزية", + "approve-token-contract-desc": "تسمح الموافقة على عملة رمزية مع عقد بإنفاق رصيدك المميز. إذا كنت تشعر أن المشروع غير جدير بالثقة، فلا توافق على عملة رمزية معهم، أو توافق فقط على المبلغ الذي ستستخدمه معهم.", + "are-you-sure": "هل أنت متأكد؟", + "are-you-sure-description": "لن تكون قادرًا على رؤية جملة البذور الكاملة مرة أخرى", + "are-you-sure-to-cancel": "هل أنت متأكد أنك تريد إلغاء؟", + "are-you-sure?": "هل أنت متأكد؟", + "ask-in-status": "طرح سؤال أو الإبلاغ عن خطأ", + "at": "في", + "attribution-received": "{{attrib}} من المكافآت {{max}} المتلقاة", + "audio": "صوت", + "audio-recorder": "مسجل", + "audio-recorder-error": "خطأ مسجل", + "audio-recorder-max-ms-reached": "تم بلوغ أقصى وقت تسجيل", + "audio-recorder-permissions-error": "يجب عليك إعطاء الإذن لإرسال رسائل صوتية", + "authorize": "يأذن", + "available": "متاح", + "available-participants": { + "zero": "مشارك", + "one": "مشارك", + "two": "مشارك", + "few": "مشاركين", + "many": "مشاركين", + "other": "مشاركين" + }, + "back": "الى الخلف", + "back-up": "دعم", + "back-up-seed-phrase": "النسخ الاحتياطي لعبارة \"Seed \"", + "back-up-your-seed-phrase": "النسخ الاحتياطي للعبارة الأولية الخاصة بك", + "backing-up": "النسخة الإحتياطية...", + "backup-disabled": "معطل", + "backup-enabled": "تمكين", + "backup-recovery-phrase": "النسخ الاحتياطي للعبارة الأولية", + "backup-settings": "إعدادات النسخ الاحتياطي", + "backup-through-waku": "النسخ الاحتياطي من خلال واكو", + "bad-fees-description": "رسوم الأولوية الخاصة بك أقل من المعايير المقترحة لدينا.", + "balance": "توازن", + "begin-set-up": "ابدأ الإعداد", + "below-base-fee": "الحد الأقصى للرسوم أقل من الرسوم الأساسية", + "biometric-auth-android-sensor-desc": "استشعار تعمل باللمس", + "biometric-auth-android-sensor-error-desc": "فشل", + "biometric-auth-android-title": "مطلوب مصادقه", + "biometric-auth-confirm-logout": "إعادة تسجيل الدخول", + "biometric-auth-confirm-message": "مطلوب المصادقة البيومترية للمتابعة ، إن لم يكن ذلك ممكنا يرجى إعادة تسجيل الدخول باستخدام كلمة مرور حساب متعددة", + "biometric-auth-confirm-title": "يجب عليك المصادقة!", + "biometric-auth-confirm-try-again": "حاول مرة أخري", + "biometric-auth-error": "غير قادر على إجراء المصادقة البيومترية ( {{code}} )", + "biometric-auth-login-error-title": "خطا في المصادقة البيومتريه", + "biometric-auth-login-ios-fallback-label": "أدخل كلمة المرور", + "biometric-auth-reason-login": "تسجيل الدخول في الحالة", + "biometric-auth-reason-verify": "تحقق من المصادقة", + "biometric-disable-bioauth": "تمكين {{bio-type-label}}", + "biometric-disable-password-description": "إذا قمت بتعطيل هذا ، أنت سوف أيضا ", + "biometric-disable-password-title": "تعطيل حفظ كلمه المرور", + "biometric-enable": "إذا كنت لا ترغب في إدخال كلمة مرورك في كل مرة للوصول إلى التطبيق، فقم بتمكين {{bio-type-label}} تسجيل الدخول", + "biometric-enable-button": "تمكين {{bio-type-label}}", + "biometric-enable-keycard": "إذا كنت لا تريد استخدام بطاقة مفتاح في كل مرة للوصول إلى التطبيق ، فقم بتمكين {{bio-type-label}} تسجيل الدخول", + "biometric-faceid": "استشعار بالوجه", + "biometric-fingerprint": "بصمة الاصبع", + "biometric-secure-with": "آمن باستخدام {{bio-type-label}}", + "biometric-touchid": "استشعار باللمس", + "bip39-password-placeholder": "كلمة مرور BIP39", + "blank-keycard-text": "يمكنك المتابعة باستخدام بطاقة مفتاح الخاصة بك بمجرد إنشاء المفاتيح واسمك", + "blank-keycard-title": "يبدو انك قمت باستغلال \nبطاقة مفتاح فارغه", + "block": "حظر", + "block-contact": "حظر هذا المستخدم", + "block-contact-details": "سيؤدي الحظر إلى حذف الرسائل السابقة لهذا المستخدم ومنع الرسائل الجديدة من الوصول إليك", + "blocked-users": "المستخدمون المحظورون", + "bootnode-address": "عنوان Bootnode", + "bootnode-details": "تفاصيل bootnode", + "bootnode-format": "enode: \/\/ {enode-id} @ {ip-address} : {port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "تمكين bootnodes", + "bootnodes-settings": "إعدادات Bootnodes", + "browsed-websites": "سوف يظهر سجل المتصفح هنا", + "browser": "المتصفح", + "browser-not-secure": "الاتصال غير آمن! لا توقع المعاملات ولا ترسل بيانات شخصية على هذا الموقع.", + "browser-secure": "اتصال آمن. تأكد من أنك تثق بهذا الموقع بالفعل قبل توقيع المعاملات أو إدخال البيانات الشخصية.", + "browsers": "المتصفحات", + "browsing-cancel": "إلغاء", + "browsing-open-in-android-web-browser": "افتح في Android", + "browsing-open-in-ios-web-browser": "الفتح في iOS", + "browsing-open-in-status": "فتح في Status ", + "browsing-site-blocked-description1": "لقد اكتشفنا نشاطًا ضارًا محتملاً من هذا العنوان. لحمايتك ومحفظتك ، نمنع المزيد من التنقل. \n\n إذا كنت تعتقد أن هذا خطأ ، فأخبرنا بذلك", + "browsing-site-blocked-description2": "دردشة عامة.", + "browsing-site-blocked-go-back": "الرجوع للخلف", + "browsing-site-blocked-title": "هذا الموقع محظور", + "browsing-title": "تصفح", + "bug-report": "الإبلاغ عن خطأ", + "bug-report-description": "* وصف", + "bug-report-description-placeholder": "مطلوب ، لا يمكن أن يكون فارغًا", + "bug-report-steps": "خطوات لإعادة الإنتاج", + "bug-report-steps-placeholder": "- افتح التطبيق\n - قم بعمل ما\n - وبعد ذلك شيء آخر ...", + "bug-report-submit-email": "إرسال عن طريق البريد الإلكتروني مع سجلات الأرشيف", + "bug-report-submit-gh-issue": "إرسال مشكلة GitHub بدون سجلات", + "bug-report-too-short-description": "الوصف قصير جدًا", + "build-yourself": "لاستخدام التطبيق بدون شروط الاستخدام هذه ، يمكنك إنشاء الإصدار الخاص بك", + "buy-crypto": "شراء العملات المشفرة", + "buy-crypto-choose-a-service": "اختر الخدمة التي ترغب في استخدامها لشراء العملات المشفرة", + "buy-crypto-description": "ابحث عن تطبيق لامركزي لشراء العملات المشفرة الآن", + "buy-crypto-leaving": "أنت تغادر Status وتدخل إلى موقع ويب تابع لجهة خارجية لإكمال عملية الشراء", + "buy-crypto-title": "يبدو أن محفظتك فارغة", + "camera-access-error": "لمنح إذن الكاميرا المطلوب ، يرجى الانتقال إلى إعدادات النظام والتأكد من تحديد Status > الكاميرا.", + "can-not-add-yourself": "هذا أنت ، لبدء دردشة اختر شخصًا آخر", + "can-send-messages": "يمكنك إرسال واستقبال الرسائل الجديدة", + "cancel": "إلغاء", + "cancel-keycard-setup": "إلغاء اعداد بطاقة المفتاح", + "cancelling": "الإلغاء", + "cannot-read-card": "لا يمكن قراءة البطاقة. \n يرجى الاحتفاظ بها في الجزء الخلفي من هاتفك", + "cannot-use-default-pin": "رمز المرور 000000 غير مسموح به. \n يرجى استخدام رقم آخر", + "cant-open-public-chat": "لا يمكن فتح دردشة عامة", + "cant-report-bug": "لا يمكن الإبلاغ عن خطأ", + "card-is-blank": "هذه البطاقة فارغة", + "card-reseted": "تم إعادة تعيين البطاقة", + "card-unpaired": "تم إلغاء البطاقة من الجهاز الحاليPaired", + "category": "فئة", + "category-title": "عنوان الفئة", + "change-fleet": "تغيير الأسطول إلى {{fleet}}", + "change-logging-enabled": "هل أنت متأكد من رغبتك في {{enable}} التسجيل؟", + "change-pairing": "تغيير رمز الاقتران", + "change-pairing-description": "لا يؤثر تغيير رمز الاقتران على عمليات الاقتران الحالية. ومع ذلك ، فإن أي اقتران جديد سيتطلب رمزًا جديدًا.", + "change-pairing-title": "قم بإنشاء رمز إقران جديد", + "change-passcode": "تغيير رمز المرور", + "change-password": "تغيير كلمة المرور", + "change-pin": "تغيير رمز المرور المكون من ٦ أرقام", + "change-puk": "تغيير رمز PUK المكون من 12 رقمًا", + "change-tip": "تغيير إكرامية", + "changed-amount-warning": "تم تغيير المبلغ من {{old}} إلى {{new}}", + "changed-asset-warning": "تم تغيير الأصول من {{old}} إلى {{new}}", + "chaos-mode": "وضع فوضوي", + "chaos-unicorn-day": "الفوضى يونيكورن اليوم", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "دردشة", + "chat-and-transact": "الدردشة والمعاملة بشكل خاص مع أصدقائك.", + "chat-is-a-contact": "صديق", + "chat-is-not-a-contact": "غير صديق", + "chat-key": "مفتاح المحادثة", + "chat-key-content": "يتم إرسال واستقبال الرسائل على بروتوكول دردشة الحالة باستخدام مفاتيح التشفير. مفتاح الدردشة العامة هو سلسلة من الأحرف التي تشاركها مع الآخرين حتى يتمكنوا من إرسال رسائل إليك في الحالة.", + "chat-key-title": "مفتاح المحادثة", + "chat-link-previews": "معاينات ارتباط الدردشة", + "chat-name": "اسم الدردشة", + "chat-name-content": "ثلاث كلمات عشوائية، مشتقة من مفتاح الدردشة الخاص بك واستخدامها كاسم مستعار افتراضي في الدردشة. أسماء الدردشة هي فريدة من نوعها تماما. لا يمكن أن يكون أي مستخدم آخر له نفس الكلمات الثلاث.", + "chat-name-title": "اسم الدردشة", + "chat-notification-preferences": "إعدادات الإشعار", + "chat-settings": "إعدادات الدردشة", + "chats": "دردشات", + "check-on-block-explorer": "تحقق من مستكشف الكتل", + "check-on-opensea": "تحقق في opensea", + "check-your-recovery-phrase": "تحقق من العبارة الأولية الخاصة بك", + "choose-actions": "اختر الإجراءات", + "choose-authentication-method": "اختر طريقة المصادقة", + "choose-new-location-for-keystore": "اختيار موقع جديد لحفظ ملف متجر المفاتيح", + "choose-storage": "اختيار التخزين", + "clear": "واضح", + "clear-all": "مسح الكل", + "clear-history": "تاريخ واضح", + "clear-history-action": "واضح", + "clear-history-confirmation": "تصفية السجل", + "clear-history-confirmation-content": "هل تريد بالتأكيد محو سجل الدردشة هذا؟", + "clear-history-title": "تصفية السجل", + "cleared-chat-description-public": "كان الوضع هادئًا هنا. ابدأ المحادثة أو", + "close": "اغلاق", + "close-all": "إغلاق الكل", + "close-app-button": "تاكيد", + "close-app-content": "سيتوقف التطبيق ويغلق. عند إعادة فتحها ، سيتم استخدام الشبكة المحددة", + "close-app-title": "تحذير!", + "collectibles-leak-metadata": "يمكنك عرض NFTs الخاص بك هنا. إذا قمت بذلك ، فسوف تشارك محفظتك وعنوان IP الخاصين بك", + "command-button-send": "إرسال", + "communities": "مجتمعات", + "communities-alpha": "المجتمعات (ألفا)", + "communities-enabled": "المجتمعات التي تم تمكينها", + "communities-verified": "✓ تم التحقق من مجتمع Status", + "community": "مجتمع", + "community-color": "لون المجتمع", + "community-color-placeholder": "اختر لونًا", + "community-edit-title": "تعديل المجتمع", + "community-emoji-thumbnail-title": "المجتمع-الرموز التعبيرية-الصورة المصغرة-العنوان", + "community-image-pick": "اختر صورة", + "community-image-remove": "ازاله", + "community-image-take": "التقط صورة", + "community-info": "معلومات المجتمع", + "community-info-not-found": "لم يتم العثور على معلومات المجتمع", + "community-invite-title": "دعوه", + "community-key": "المفتاح الخاص بالمجتمع", + "community-key-placeholder": "اكتب المفتاح الخاص بالمجتمع", + "community-link": "رابط المجتمع", + "community-members": { + "zero": "عضو", + "one": "عضو", + "two": "أعضاء", + "few": "أعضاء", + "many": "أعضاء", + "other": "أعضاء" + }, + "community-members-title": "الأعضاء", + "community-message-preview": "دعوة للانضمام إلى {{community-name}}", + "community-private-key": "المفتاح الخاص بالمجتمع", + "community-requests-to-join-title": "طلبات العضوية", + "community-roles": "الأدوار", + "community-share-title": "مشاركه", + "community-thumbnail-image": "صورة مصغرة", + "community-thumbnail-upload": "تحميل", + "complete-hardwallet-setup": "هذه البطاقة مرتبطة الآن. أنت بحاجة إليها لتوقيع المعاملات وفتح مفاتيحك", + "completed": "اكمال", + "confirm": "تاكيد", + "confirm-new-password": "تأكيد كلمة المرور الجديدة", + "confirm-pairing-code-placeholder": "قم بتأكيد رمز الإقران ...", + "confirm-password-placeholder": "أكد رقمك السري...", + "confirmation-request": "طلب تأكيد", + "confirmations": "تأكيدات", + "confirmations-helper-text": "عندما تحتوي المعاملة على 12 تأكيدًا ، يمكنك اعتبارها قد تمت تسويتها.", + "connect": "الاتصال", + "connect-mailserver-content": "الاتصال على {{name}} ؟", + "connect-wallet": "الاتصال بالمحفظة", + "connected": "متصل", + "connected-to": "متصل بـ", + "connecting": "الاتصال...", + "connecting-requires-login": "الاتصال بشبكة أخرى يتطلب تسجيل الدخول", + "connection-request": "طلب اتصال", + "connection-status": "حالة الاتصال", + "connection-with-the-card-lost": "الاتصال مع البطاقة تم فقدانه", + "connection-with-the-card-lost-setup-text": "لاستئناف الإعداد ، امسك البطاقة \n الجزء الخلفي من هاتفك والحفاظ عليها \n بطاقة للاتصال عبر الهاتف", + "connection-with-the-card-lost-text": "للمتابعة ، امسك البطاقة في الجزء الخلفي من هاتفك", + "contact-code": "مفتاح المحادثة", + "contact-s": { + "zero": "متصل", + "one": "متصل", + "two": "متصلان", + "few": "متصلين", + "many": "متصلين", + "other": "متصلين" + }, + "contacts": "جهات الاتصال", + "contacts-descr": "ستظهر جهات الاتصال الخاصة بك هنا. ستتلقى تحديثات status من أي شخص تضيفه كجهة اتصال", + "contacts-empty": "ستظهر هنا جهات الاتصال التي تحمل أسماء ENS", + "continue": "استمر", + "continue-anyway": "استمر على أي حال", + "contract-address": "عنوان العقد", + "contract-interaction": "عقد التفاعل", + "contract-isnt-supported": "العقد غير مدعوم", + "copy-info": "نسخ المعلومات", + "copy-qr": "نسخ الكود", + "copy-to-clipboard": "نسخ", + "copy-transaction-hash": "نسخ معرف المعاملة", + "cost-fee": "التكلفة \/ رسوم", + "counter-9-plus": "9+", + "counter-99-plus": "99 +", + "create": "انشاء", + "create-a-pin": "قم بإنشاء رمز مرور مكون من ٦ أرقام", + "create-a-puk": "أنشئ رمز مرور المكون من 12 رقمًا", + "create-category": "إنشاء فئة", + "create-channel": "قم بإنشاء قناة", + "create-channel-title": "قناة جديدة", + "create-community": "أنشئ مجتمعًا", + "create-group-chat": "إنشاء دردشة جماعية", + "create-multiaccount": "إنشاء مفاتيح", + "create-new-key": "الحصول على مفاتيح جديدة", + "create-pin": "قم بإنشاء رمز مرور مكون من 6 أرقام", + "create-pin-description": "ستحتاج إلى بطاقتك + رمز المرور المكون من 6 أرقام لإلغاء تأمين Status وتأكيد المعاملات", + "created-group-chat-description": "لقد أنشأت المجموعة {{group-name}}", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "العمله", + "currency-display-name-aed": "الدرهم الإماراتي", + "currency-display-name-afn": "أفغانستان الافغانيه", + "currency-display-name-ars": "بيزو أرجنتيني", + "currency-display-name-aud": "الدولار الأسترالي", + "currency-display-name-bbd": "دولار بربادوس", + "currency-display-name-bdt": "تاكا بنغلاديشي", + "currency-display-name-bgn": "ليف بلغاري", + "currency-display-name-bhd": "الدينار البحريني", + "currency-display-name-bnd": "دولار بروني دار السلام", + "currency-display-name-bob": "البوليفيانو بوليفي", + "currency-display-name-brl": "ريال برازيلي", + "currency-display-name-btn": "نغولترم البوتاني", + "currency-display-name-cad": "دولار كندي", + "currency-display-name-chf": "فرنك سويسري", + "currency-display-name-clp": "بيزو شيلي", + "currency-display-name-cny": "الصين يوان رينمنبي", + "currency-display-name-cop": "بيزو كولومبي", + "currency-display-name-crc": "كولون كوستاريكا", + "currency-display-name-czk": "كورونا تشيكية", + "currency-display-name-dkk": "كرونا دنماركي", + "currency-display-name-dop": "بيزو جمهوريه الدومينيكان", + "currency-display-name-egp": "جنيه مصري", + "currency-display-name-etb": "Birr الاثيوبي", + "currency-display-name-eur": "اليورو", + "currency-display-name-gbp": "الجنيه الإسترليني", + "currency-display-name-gel": "لاري الجورجي", + "currency-display-name-ghs": "غانا سيدي", + "currency-display-name-hkd": "دولار هونج كونج", + "currency-display-name-hrk": "Kuna كرواتيا", + "currency-display-name-huf": "فورنت هنغاري", + "currency-display-name-idr": "اندونيسيا روبية", + "currency-display-name-ils": "إسرائيل شيكل", + "currency-display-name-inr": "روبيه هندية", + "currency-display-name-isk": "أيسلندا كرونا", + "currency-display-name-jmd": "دولار جامايكا", + "currency-display-name-jpy": "الين الياباني", + "currency-display-name-kes": "الشلن الكيني", + "currency-display-name-krw": "كوريا (جنوب) وون", + "currency-display-name-kwd": "الدينار الكويتي", + "currency-display-name-kzt": "تينغ كازاخستان", + "currency-display-name-lkr": "روبيه سيريلانكية", + "currency-display-name-mad": "درهم مغربى", + "currency-display-name-mdl": "Moldovan Leu\n", + "currency-display-name-mur": "روبيه موريشيوس", + "currency-display-name-mwk": "كواشا مالاوي", + "currency-display-name-mxn": "بيزو مكسيكي", + "currency-display-name-myr": "رينغيت ماليزيا", + "currency-display-name-mzn": "موزامبيق Metical", + "currency-display-name-nad": "دولار ناميبي", + "currency-display-name-ngn": "نيره نيجيرية", + "currency-display-name-nok": "كرونة نرويجية", + "currency-display-name-npr": "روبيه نيبالية", + "currency-display-name-nzd": "الدولار النيوزيلندي ", + "currency-display-name-omr": "ريال عماني", + "currency-display-name-pen": "بيرو سول", + "currency-display-name-pgk": "كينا بابوا غينيا الجديدة", + "currency-display-name-php": "بيزو الفلبين", + "currency-display-name-pkr": "روبيه باكستانية", + "currency-display-name-pln": "زلوتي البولندية", + "currency-display-name-pyg": "باراجواي جواراني", + "currency-display-name-qar": "ريال قطري", + "currency-display-name-ron": "ليو رومانيا", + "currency-display-name-rsd": "دينار صربيا", + "currency-display-name-rub": "الروبل الروسي ، ", + "currency-display-name-sar": "الريال السعودي", + "currency-display-name-sek": "كرونة سويدية", + "currency-display-name-sgd": "دولار سينغافوري", + "currency-display-name-thb": "بات تايلندي", + "currency-display-name-try": "الليرة التركية", + "currency-display-name-ttd": "دولار ترينيداد وتوباغو", + "currency-display-name-twd": "تايوان الدولار الجديد", + "currency-display-name-tzs": "الشلن التنزاني", + "currency-display-name-uah": "أوكرانيا هريفنيا", + "currency-display-name-ugx": "الشلن الأوغندي", + "currency-display-name-usd": "دولار الولايات المتحدة", + "currency-display-name-uyu": "أوروغواي بيزو", + "currency-display-name-vef": "فنزويلا بوليفار", + "currency-display-name-vnd": "فيتنام دونغ", + "currency-display-name-zar": "راند جنوب افريقيا", + "current": "حاليه", + "current-average": "المتوسط الحالي", + "current-average-tip": "متوسط للإكرامية الحالية", + "current-base": "الأساس الحالي", + "current-base-fee": "الرسوم الأساسية الحالية", + "current-minimum-tip": "الحد الأدنى للإكرامية الحالية", + "current-network": "الشبكة الحالية", + "current-password": "كلمة السر الحالية", + "current-pin": "أدخل رمز المرور المكون من ٦ أرقام", + "current-pin-description": "أدخل رمز المرور المكون من ٦ أرقام للمتابعة", + "custom": "مجال مخصص", + "custom-networks": "شبكات مخصصة", + "custom-node": "أنت تستخدم نقطة نهاية RPC مخصصة. قد يكون سجل التحويلات المحلية الخاص بك غير مكتمل.", + "custom-seed-phrase": "العبارة غير صالحة", + "custom-seed-phrase-text-1": "هذه العبارة الأولية لا تتوافق مع قاموسنا المدعوم. تحقق من وجود أخطاء إملائية.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "قبول وفتح", + "dapp-starter-pack-description": "إليك بعض من العملة الرقمية لتبدأ! استخدمها للحصول على ملصقات ، واسم ENS و تجربت dapps", + "dapp-starter-pack-title": "حزمة المبتدئين", + "dapp-would-like-to-connect-wallet": "ترغب في الاتصال", + "dapps": "ÐApps", + "dapps-permissions": "أذونات DApp", + "dark": "داكن", + "data": "البيانات", + "data-collected": "البيانات التي تم جمعها", + "data-collected-subtitle": "يوضح الجدول أدناه البيانات الدقيقة المخزنة وسيتم إرسالها. يتم التحقق من صحة البيانات وفقا للقواعد العامة لضمان عدم إرسال أي بيانات حساسة. لا تثق، تحقق.", + "data-syncing": "مزامنة البيانات", + "database-reset-content": "تم حذف الدردشات وجهات الاتصال والإعدادات. يمكنك استخدام حسابك مع بطاقة المفاتيح الخاص بك", + "database-reset-title": "إعادة تعيين قاعدة البيانات", + "database-reset-warning": "ستتم إعادة تعيين قاعدة البيانات. سيتم حذف الدردشات وجهات الاتصال والإعدادات", + "datetime-ago": "منذ", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "zero": "يوم", + "one": "يوم", + "two": "يومان", + "few": "أيام", + "many": "أيام", + "other": "أيام" + }, + "datetime-day-short": { + "zero": "0", + "one": "1", + "two": "2", + "few": "قليل", + "many": "كثير", + "other": "آخر" + }, + "datetime-hour": { + "zero": "ساعة", + "one": "ساعة", + "two": "ساعتان", + "few": "ساعات", + "many": "ساعات", + "other": "ساعات" + }, + "datetime-hour-short": { + "zero": "صفر", + "one": "واحد", + "two": "اثنان", + "few": "قليل", + "many": "كثير", + "other": "آخر" + }, + "datetime-minute": { + "zero": "دقيقة", + "one": "دقيقة", + "two": "دقائق", + "few": "دقائق", + "many": "دقائق", + "other": "دقائق" + }, + "datetime-minute-short": { + "zero": "صفر", + "one": "واحد", + "two": "اثنان", + "few": "قليل", + "many": "كثير", + "other": "آخر" + }, + "datetime-second": { + "zero": "ثانية", + "one": "ثانية", + "two": "ثواني", + "few": "ثواني", + "many": "ثواني", + "other": "ثواني" + }, + "datetime-second-short": { + "zero": "0", + "one": "1", + "two": "2", + "few": "", + "many": "", + "other": "" + }, + "datetime-today": "اليوم", + "datetime-yesterday": "أمس", + "decimals": "الكسور العشرية", + "decline": "رفض الدعوة", + "decryption-failed-content": "حدث خطأ في فك تشفير بياناتك. قد تحتاج إلى مسح بياناتك القديمة وإنشاء حساب جديد. انقر على \"تطبيق\" لمحو أو \"إلغاء\" للمحاولة مرة أخرى", + "default": "افتراضي", + "default-assets": "الافتراضي ERC20 و ERC721", + "default-sync-period": "سجل المزامنة لـ", + "delete": "حذف", + "delete-account": "حذف حسابي", + "delete-and-leave-group": "حذف وترك المجموعة", + "delete-bootnode": "حذف عقدة البوت", + "delete-bootnode-are-you-sure": "هل أنت متأكد أنك تريد حذف bootnode؟", + "delete-bootnode-title": "حذف عقدة البوت", + "delete-category-confirmation": "هل أنت متأكد أنك تريد حذف هذه الفئة؟", + "close-chat": "حذف الدردشة", + "close-chat-confirmation": "هل أنت متأكد أنك تريد حذف هذه الدردشة؟", + "delete-confirmation": "حذف؟", + "delete-keys-keycard": "حذف المفاتيح من بطاقة المفتاح", + "delete-mailserver": "حذف عقدة Status", + "delete-mailserver-are-you-sure": "هل أنت متأكد أنك تريد حذف عقدة Status هذه؟", + "delete-mailserver-title": "حذف عقدة Status", + "delete-message": "حذف الرسالة", + "delete-my-account": "حذف حسابي", + "delete-my-profile": "حذف ملفي الشخصي", + "delete-network-confirmation": "هل أنت متأكد أنك تريد حذف هذه الشبكة؟", + "delete-network-error": "يرجى الاتصال بشبكة مختلفة قبل حذف هذه الشبكة", + "delete-network-title": "حذف الشبكة؟", + "delete-node": "حذف العقدة", + "delete-node-are-you-sure": "هل أنت متأكد أنك تريد حذف هذه العقدة؟", + "delete-node-title": "حذف العقدة", + "delete-profile": "حذف الملف الشخصي", + "delete-profile-warning": "تحذير: إذا لم تكن لديك العبارة الأولية مكتوبة ، فستفقد الوصول إلى أموالك بعد حذف ملفك الشخصي", + "deny": "رفض", + "derivation-path": "مسار الاشتقاق", + "describe-channel": "وصف القناة", + "description": "وصف", + "dev-mode": "وضع التنمية", + "dev-mode-settings": "وضع التنمية", + "device-syncing": "مزامنة الجهاز", + "devices": "الأجهزة", + "disable": "تعطيل", + "disable-all": "تعطيل الكل", + "disable-later-in-settings": "يمكنك تعطيل هذا لاحقًا في الإعدادات", + "disabled": "معطل", + "disconnect": "قطع الاتصال", + "disconnected": "الدردشة غير متصلة بالأنترنت\n", + "discover": "اكتشاف", + "dismiss": "استبعاد", + "display-collectibles": "عرض المقتنيات", + "done": "فعله", + "dont-ask": "لا تسألني مرة أخرى", + "edit": "تصحيح", + "edit-categories": "تحرير الفئات", + "edit-channel-title": "تحرير القناة", + "edit-chats": "تصحيح الدردشات", + "edit-community": "تعديل المجتمع", + "edit-favourite": "تعديل المفضلة", + "edit-group": "تعديل المجموعة", + "edit-profile": "تعديل الملف الشخصي", + "edited": "تم التعديل", + "editing-message": "تعديل الرسالة", + "empty-activity-center": "إشعارات الدردشة الخاصة بك\n سيظهر هنا", + "empty-chat-description": "لا توجد رسائل \nفي هذه الدردشة حتى الآن", + "empty-chat-description-community": "لقد كان الوضع هادئًا هنا في آخر{{quiet-hours}} .", + "empty-chat-description-one-to-one": "يتم تشفير أي رسائل ترسلها هنا ولا يمكن قراءتها إلا من قبل أنت و", + "empty-chat-description-public": "لقد كانت هادئة هنا لآخر {{quiet-hours}} . ابدأ المحادثة أو", + "empty-chat-description-public-share-this": "مشاركه هذه المحادثة.", + "empty-keycard-required": "يتطلب بطاقة مفتاح فارغة", + "empty-pending-invitations-descr": "الأشخاص الذين يرغبون في الانضمام إلى المجموعة \n عبر رابط دعوة سيظهر هنا", + "empty-tab": "علامة تبويب فارغة", + "enable": "تمكين", + "enable-all": "تمكين الكل", + "enable-link-previews": "تمكين معاينات الارتباط في الدردشة؟", + "encrypt-with-password": "تشفير مع كلمة المرور", + "ens-10-SNT": "10 SNT", + "ens-add-username": "أضافه اسم مستخدم", + "ens-agree-to": "توافق على ", + "ens-chat-settings": "إعدادات الدردشة", + "ens-custom-domain": "مجال مخصص", + "ens-custom-username-hints": "اكتب اسم المستخدم بالكامل بما في ذلك المجال المخصص مثل username.domain.eth", + "ens-custom-username-taken": "اسم المستخدم لا ينتمي إليك :(", + "ens-deposit": "ايداع", + "ens-dismiss-message": "انقر هنا للرفض", + "ens-displayed-with": "يتم عرض الرسائل الخاصة بك للآخرين مع", + "ens-get-name": "الحصول علي اسم مستخدم عالمي", + "ens-got-it": "حسنا، تابع", + "ens-locked": "اسم المستخدم مقفل. لن تتمكن من إصداره حتى {{date}}", + "ens-name-content": "اسم مستعار مخصص لمفتاح الدردشة الذي يمكنك تسجيله باستخدام خدمة Ethereum Name Service. أسماء ENS هي أسماء مستخدمين لا مركزية.", + "ens-name-not-found": "لا يمكن تحليل اسم ENS", + "ens-name-title": "اسم ENS", + "ens-network-restriction": "متوفر فقط على Mainnet", + "ens-no-usernames": "ليس لديك اي اسم مستخدم متصل", + "ens-powered-by": "مدعوم من Ethereum اسم الخدمات", + "ens-primary-username": "اسم المستخدم الأساسي", + "ens-register": "تسجيل", + "ens-registration-failed": "لتسجيل اسم المستخدم ، يرجى المحاولة مرة أخرى.", + "ens-registration-failed-title": "فشلت المعاملة", + "ens-registration-failure": "فشل التسجيل", + "ens-registration-in-progress": "جاري التسجيل...", + "ens-release-username": "الإفراج عن اسم المستخدم", + "ens-remove-hints": "سيؤدي الاستمرار إلى ربط اسم المستخدم هذا بمفتاحك.", + "ens-remove-username": "إزالة اسم المستخدم", + "ens-saved": "متصل الآن بالمفتاح الخاص ويمكن استخدامه في Status.", + "ens-saved-title": "اسم المستخدم المضاف", + "ens-show-username": "عرض اسم مستخدم ENS الخاص بي في الدردشات", + "ens-terms-header": "شروط تسجيل الاسم", + "ens-terms-point-1": "تودع الأموال لمدة 1 سنة. سيتم قفل SNT الخاص بك ، ولكن لا تنفق.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS Registry).", + "ens-terms-point-2": "بعد عام واحد ، يمكنك تحرير الاسم واسترداد مقدمك ، أو عدم اتخاذ أي إجراء للحفاظ على الاسم.", + "ens-terms-point-3": "إذا تغيرت شروط العقد-علي سبيل المثال الحالة التي تجعل ترقيات العقد-يحق للمستخدم الإفراج عن اسم المستخدم بغض الزمن عن الوقت المحتفظ به.", + "ens-terms-point-4": "لا يمكن لوحده تحكم العقد الوصول إلى أموالك المودعة. يمكن نقلها فقط إلى العنوان الذي أرسلها.", + "ens-terms-point-5": "سيتم ربط عنوانك (عناوينك) علنا باسم ENS الخاص بك.", + "ens-terms-point-6": "يتم إنشاء أسماء المستخدمين على هيئة نطاقات فرعية لـ stateofus.eth وتخضع لشروط العقد الذكية لـ ENS.", + "ens-terms-point-7": "يمكنك تخويل العقد لنقل SNT نيابة عنك. يمكن ان يحدث هذا فقط عند الموافقة علي معامله لتخويل النقل.", + "ens-terms-point-8": "هذه الشروط مضمونة في منطق العقد الذكية في العناوين:", + "ens-terms-point-9": "{{address}} (اسم مستخدم Status )", + "ens-terms-registration": "شروط تسجيل الاسم.", + "ens-test-message": "مرحبا", + "ens-transaction-pending": "المعاملة جارية ...", + "ens-understand": "أدرك أن عنوان محفظتي سيكون مرتبطًا بشكل عام باسم المستخدم الخاص بي.", + "ens-username": "اسم المستخدم ENS", + "ens-username-already-added": "اسم المستخدم مرتبط بالفعل بالمفتاح الخاص بك ويمكن استخدامه في الحالة.", + "ens-username-available": "✓ اسم المستخدم متاح!", + "ens-username-connected": "اسم المستخدم هذا مملوك لك ومتصل بمفتاح الدردشة الخاص بك.", + "ens-username-connected-continue": "استمر في ضبط `إظهار اسم مستخدم ENS الخاص بي في المحادثات '.", + "ens-username-connected-with-different-key": "ستتطلب المتابعة معاملة لتوصيل اسم المستخدم بالمفتاح الحالي", + "ens-username-connection-confirmation": "سيتم ربط {{username}} بمجرد اكتمال المعاملة.", + "ens-username-hints": "ما لا يقل عن ٤ أحرف. الحروف اللاتينية والأرقام والأحرف الصغيرة فقط.", + "ens-username-invalid": "الحروف والأرقام فقط.", + "ens-username-invalid-name-warning": "انتهت عملية تسجيل أحد أسماء ens الخاصة بك في حالة غير صالحة. لا تستخدم اسم معاملات المحفظة وتواصل مع دعمنا على support@status.im", + "ens-username-owned": "✓ اسم المستخدم هو مملوك لك. ", + "ens-username-owned-continue": "سيؤدي الاستمرار إلى ربط اسم المستخدم هذا بمفتاحك الخاص.", + "ens-username-registration-confirmation": "لطيف! أنت تملك {{username}} بمجرد اكتمال المعاملة.", + "ens-username-registration-invalid": "تحذير! انتهت عملية التسجيل في حالة غير صالحة. لا تستخدم اسم معاملات المحفظة وتواصل مع دعمنا على support@status.im", + "ens-username-taken": "الاسم مستخدم من قبل :(", + "ens-username-you-can-follow-progress": "يمكنك متابعة التقدم المحرز في قسم سجل المعاملات في محفظتك.", + "ens-usernames": "أسماء المستخدمين ENS", + "ens-usernames-details": "تسجيل اسم مستخدم عالمي ليتم التعرف عليه بسهولة من قبل المستخدمين الآخرين", + "ens-want-custom-domain": "أنا أملك اسمًا في مجال آخر", + "ens-want-domain": "أريد مجال Stateofus.eth", + "ens-welcome-hints": "تقوم أسماء ENS بتحويل هذه العناوين المجنونة إلى أسماء مستخدمين فريدة.", + "ens-welcome-point-customize": "يمكن أن يحل اسم ENS محل اسمك العشوائي المكون من 3 كلمات في الدردشة. اسمك بدلاً من {{name}} .", + "ens-welcome-point-customize-title": "تخصيص اسم الدردشة الخاصة بك", + "ens-welcome-point-receive": "يمكن للآخرين إرسال الأموال إليك عبر الدردشة في خطوة واحدة بسيطة.", + "ens-welcome-point-receive-title": "استلام المعاملات في الدردشة", + "ens-welcome-point-register": "سجل مرة واحدة للحفاظ على الاسم للأبد. بعد عام واحد ، يمكنك إطلاق الاسم واستعادة SNT.", + "ens-welcome-point-register-title": "10 SNT للتسجيل", + "ens-welcome-point-simplify": "يمكنك تلقي الأموال إلى اسم ENS سهل المشاركة بدلاً من التجزئة السداسية العشرية (0x ...).", + "ens-welcome-point-simplify-title": "تبسيط عنوان ETH الخاص بك", + "ens-welcome-point-verify": "يمكنك التحقق من وإضافة أي أسماء مستخدمين تمتلكها في الخطوات التالية.", + "ens-welcome-point-verify-title": "هل تملك اسم مستخدم بالفعل ؟", + "ens-your-username": "اسم المستخدم الخاص بك", + "ens-your-usernames": "أسماء المستخدمين الخاصة بك", + "ens-your-your-name": "اسم ENS الخاص بك", + "enter-12-words": "أدخل 12 كلمة من العبارة الأولية ، مفصولة بمسافات واحدة", + "enter-a-private-key": "إدخال مفتاح خاص", + "enter-a-seed-phrase": "أدخل العبارة الأولية", + "enter-address": "أدخل العنوان", + "enter-contact-code": "ENS (dynamicik94) أو مفتاح الدردشة (0x04…)", + "enter-pair-code": "أدخل رمز الاقتران الخاص بك", + "enter-pair-code-description": "يمكن تعيين رمز الاقتران من عميل Status المقترن مسبقا", + "enter-password": "أدخل كلمة المرور", + "enter-password-migration-prompt": "أدخل كلمة المرور الخاصة بك لنقل جهات الاتصال والمحادثات والإعدادات جنبًا إلى جنب مع المفاتيح الخاصة بك", + "enter-pin": "أدخل رمز المرور المكون من ٦ أرقام", + "enter-puk-code": "أدخل رمز PUK", + "enter-puk-code-description": "تم حظر رمز المرور المكون من ٦ أرقام. الرجاء إدخال رمز PUK لإلغاء قفل رمز المرور.", + "enter-recipient-address-or-username": "أدخل عنوان أو اسم مستخدم المستلم", + "enter-seed-phrase": "أدخل العبارة الأولية", + "enter-url": "أدخل عنوان URL", + "enter-user-pk": "أدخل المفتاح العام للمستخدم", + "enter-watch-account-address": "امسح رمز الاستجابة السريعة ضوئيًا \n أو \n أدخل العنوان لمشاهدته", + "enter-word": "أدخل كلمة", + "enter-your-code": "أدخل رمز المرور المكون من 6 أرقام", + "enter-your-password": "ادخل رقمك السري", + "error": "خطأ", + "error-unable-to-get-balance": "غير قادر على الحصول على التوازن", + "error-unable-to-get-prices": "خطأ في تحويل العملة. قم بتجديد الشاشة للمحاولة مرة أخرى.", + "error-unable-to-get-token-balance": "غير قادر علي الحصول علي رصيد الرمز المميز", + "errors": "أخطاء", + "eth": "ETH", + "ethereum-account": "حساب الإيثريوم", + "ethereum-address": "عنوان الإيثريوم", + "ethereum-node-started-incorrectly-description": "تم بدء عقدة إيثيريوم بتكوين غير صحيح ، وسيتم إيقاف التطبيق للتعافي من هذا الشرط. معرف {{network-id}} = network id ، الفعلي = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "بدأت عقدة Ethereum بشكل غير صحيح", + "etherscan-lookup": "إبحث في Etherscan ", + "everyone": "الجميع", + "expand-all": "توسيع الكل", + "export-account": "حساب التصدير", + "export-key": "استخراج المفتاح الخاص", + "external-storage-denied": "تم رفض الوصول إلى وحدة التخزين الخارجية", + "failed": "فشل", + "faq": "الاسئله المتداولة", + "fast": "سريع", + "favourite": "المفضله", + "favourite-description": "ستظهر مواقع الويب المفضلة لديك هنا", + "favourites": "المفضلة", + "favourites-empty": "ستظهر العناوين التي تمت إضافتها إلى المفضلة هنا", + "fee-cap": "سقف الرسوم", + "fee-explanation": "الحد الأقصى للسعر الإجمالي للمعاملة. إذا تجاوزت رسوم الكتلة الأساسية هذا ، فسيتم تضمينها في الكتلة التالية برسوم أساسية أقل.", + "fee-options": "خيارات الرسوم المقترحة", + "fetch-community": "جلب المجتمع", + "fetch-messages": "↓ جلب الرسائل", + "fetch-timeline": "↓ جلب", + "fetching-community": "إحضار المجتمع ...", + "find": "العثور", + "finish": "إنهاء", + "finishing-card-setup": "الانتهاء من إعداد البطاقة", + "fleet": "اسطول", + "fleet-settings": "إعدادات الأسطول", + "follow": "اتبع", + "follow-your-interests": "اتبع اهتماماتك في واحده من العديد من المحادثات العامة", + "free": "↓ مجاني", + "from": "من عند", + "gas-amount-limit": "حد كمية الغاز", + "gas-limit": "حد الغاز", + "gas-price": "سعر الغاز", + "gas-used": "الغاز المستخدم", + "generate-a-key": "إنشاء مفاتيح", + "generate-a-new-account": "إنشاء حساب", + "generate-a-new-key": "إنشاء مفتاح جديد", + "generate-account": "إنشاء مفتاح خاص", + "generate-an-account": "إنشاء حساب", + "generate-new-key": "إنشاء مفتاح", + "generating-codes-for-pairing": "> تنزيل برنامج المنتج على البطاقة \n > توليد رموز القفل والاقتران", + "generating-keys": "يتم الآن إنشاء مفاتيح...", + "generating-mnemonic": "توليد عبارة ذاكرة", + "get-a-keycard": "احصل على بطاقة المفتاح", + "get-started": "البدء", + "get-status-at": "الحصول على Status في http:\/\/status.im", + "get-stickers": "الحصول على ملصقات", + "give-a-short-description": "قدم وصفًا موجزًا", + "give-a-short-description-community": "أعطها وصفا موجزا", + "give-permissions-camera": "إعطاء الإذن\nللوصول إلى الكاميرا", + "glossary": "المعجم", + "go-to-settings": "انتقل إلى الإعدادات...", + "got-it": "فهمت ذلك", + "grant-face-id-permissions": "لمنح إذن Face ID المطلوب ، يرجى الانتقال إلى إعدادات النظام والتأكد من تحديد الحالة > Face ID", + "group-chat": "دردشة جماعيه", + "group-chat-admin": "المشرف", + "group-chat-admin-added": "* {{member}} * أصبح مشرفًا مسؤولا", + "group-chat-all-contacts-invited": "جميع جهات الاتصال الخاصة بك موجودة بالفعل في المجموعة", + "group-chat-created": "* {{member}} * أنشأ هذا الشخص المجموعة * {{name}} *", + "group-chat-decline-invitation": "رفض الدعوة", + "group-chat-member-added": "* {{member}} * تمت دعوة هذا الشخص", + "group-chat-member-joined": "* {{member}} * انضم هذا الشخص إلى المجموعة", + "group-chat-member-removed": "* {{member}} * غادر المجموعة", + "group-chat-members-count": "{{selected}} \/ {{max}} أعضاء", + "group-chat-name-changed": "** {{member}} ** غير هذا الشخص اسم المجموعة إلى ** {{name}} **", + "group-chat-no-contacts": "ليس لديك أي جهات اتصال حتى الآن. \n قم بدعوة أصدقائك لبدء الدردشة", + "group-info": "معلومات المجموعة", + "group-invite": "دعوة المجموعة", + "group-invite-link": "رابط دعوة المجموعة", + "group-membership-request": "طلب عضوية المجموعة", + "gwei": "غواي", + "has-permissions": "لديه إذن بالوصول", + "hash": "رموز التجزئة\n", + "help": "مساعدة", + "help-capitalized": "مساعدة", + "help-center": "مركز المساعدة", + "help-improve-status": "المساعدة في تحسين Status", + "hide": "إخفاء", + "hide-content-when-switching-apps": "إخفاء معاينات الحالة عند تبديل التطبيق", + "hide-content-when-switching-apps-ios": "إخفاء المعاينة", + "history": "التاريخ", + "history-nodes": "عقد Status", + "hold-card": "امسك البطاقة في الخلف \n من هاتفك", + "home": "الصفحة الرئيسية", + "hooks": "السنانير", + "how-it-works": "كيف يعمل", + "http-gateway-error": "عفوًا ، فشل الطلب!", + "identifier": "معرف", + "image": "صورة", + "image-remove-current": "ازل الصوره الحاليه", + "image-source-gallery": "اختر من المعرض", + "image-source-make-photo": "التقاط", + "image-source-title": "تحرير الصورة", + "import": "استيراد", + "import-community": "استيراد مجتمع", + "import-community-title": "استيراد مجتمع", + "in-contacts": "في الاتصالات", + "include": "يشمل", + "incoming": "الوارده", + "incoming-transaction": "المعاملات الواردة", + "incorrect-code": [ + "str", + "آسف الرمز غير صحيح ، يرجى إدخاله مرة أخرى" + ], + "increase-gas": "زيادة الغاز", + "initialization": "التهيئة", + "install": "↓ تثبيت", + "insufficient-balance-to-cover-fee": "لا يوجد رصيد كافٍ لتغطية رسوم المعاملة", + "intro-message1": "مرحبًا بك في Status! \n انقر فوق هذه الرسالة لتعيين كلمة المرور الخاصة بك والبدء.", + "intro-privacy-policy-note1": "الحالة لا تجمع أو تشارك أو تبيع أي بيانات شخصية. من خلال الاستمرار أنت توافق على ذلك", + "intro-privacy-policy-note2": "سياسة الخصوصية", + "intro-text": "Status هي بوابتك إلى الويب اللامركزي", + "intro-text1": "منصة مفتوحة للدردشة بشكل آمن والتعامل مع Ethereum blockchain", + "intro-text2": "تم إعداد الحساب المتعدد الخاص بك. من فضلك لا تنسى أن نسجل نسخة احتياطية من العبارة الأولية في ملف التعريف الخاص", + "intro-text3": "تم إعداد الحساب المتعدد الخاص بك. من فضلك لا تنسى أن نسجل نسخة احتياطية من العبارة الأولية في ملف التعريف الخاص", + "intro-title1": "الاتصالات الخاصة حقا", + "intro-title2": "محفظة آمنة", + "intro-title3": "التطبيقات اللامركزية", + "intro-wizard-text1": "هويتك آمنة حسب التصميم. تحصل على مفتاح تشفير تم إنشاؤه", + "intro-wizard-text2": "هذا الاسم هو هويتك في الحالة. لا يمكن تغييره بمجرد اختياره.", + "intro-wizard-text3": "يتم تخزين المفتاح الخاص بك محليا. لا يوجد نسخة، فقط لديك حق الوصول.", + "intro-wizard-text4": "تأمين وتشفير المفتاح الخاص", + "intro-wizard-text6": "ستعلمك الحالة بالرسائل الجديدة. يمكنك تعديل تفضيلات الإشعارات الخاصة بك لاحقًا في الإعدادات", + "intro-wizard-title-alt4": "إنشاء كلمه مرور", + "intro-wizard-title-alt5": "تاكيد كلمه المرور", + "intro-wizard-title1": "احصل علي مفتاح", + "intro-wizard-title2": "اختيار اسم دردشة", + "intro-wizard-title3": "اختيار تخزين المفاتيح", + "intro-wizard-title4": "إنشاء رمز مرور مكون من 6 أرقام", + "intro-wizard-title5": "تأكيد رمز المرور", + "intro-wizard-title6": "تمكين الإشعارات", + "introduce-yourself": "قدم نفسك برسالة مختصرة", + "invalid-address-qr-code": "لا يحتوي رمز QR الممسوح ضوئيًا على عنوان صالح", + "invalid-format": "تنسيق غير صالح \n يجب أن يكون {{format}}", + "invalid-key-confirm": "تطبيق", + "invalid-key-content": "لا يمكن تشفير قاعدة البيانات لأن الملف تالف. أموالك ومفتاح الدردشة آمنان. لا يمكن استعادة البيانات الأخرى ، مثل الدردشات وجهات الاتصال الخاصة بك.\"{{erase-multiaccounts-data-button-text}} \" الزر ، سيزيل جميع البيانات الأخرى ويسمح لك بالوصول إلى أموالك وإرسال الرسائل", + "invalid-number": "رقم غير صالح", + "invalid-pairing-password": "كلمة مرور الاقتران غير صالحة", + "invalid-public-chat-topic": "موضوع دردشة عامة غير صالح", + "invalid-range": "التنسيق غير صالح ، يجب أن يكون بين {{min}} و {{max}}", + "invalid-username-or-key": "اسم المستخدم أو مفتاح الدردشة غير صالح", + "invite": "دعوه", + "invite-button": "دعوه", + "invite-chat-accept": "قبول", + "invite-chat-accept-join": "قبول والانضمام", + "invite-chat-intro": "تمت إحالتك من قبل صديق للانضمام إلى Status. إليك بعض من العملة الرقمية لتبدأ! استخدمه لتسجيل اسم ENS أو شراء حزمة ملصقات", + "invite-chat-name": "إحالة صديق", + "invite-chat-pending": "المعلقه", + "invite-chat-rule": "سيؤدي القبول أيضًا إلى مكافأة صديقك بمكافأة إحالة بالعملة الرقمية", + "invite-chat-starter-pack": "حزمة المبتدئين", + "invite-friends": "دعوه الأصدقاء", + "invite-instruction-fifth": "يمكنك اختيار استرداد مكافأة الإحالة الخاصة بك في أي وقت.", + "invite-instruction-first": "يمكنك إرسال رابط دعوة فريد إلى صديقك لتنزيل Status والانضمام إلينا", + "invite-instruction-fourth": "تتلقى مكافأة الإحالة الخاصة بك وصديقك حزمة المبتدئين", + "invite-instruction-second": "قاما صديقك بتنزيل الحالة وإنشاء حساب (على اندرويد)", + "invite-instruction-third": "يتم بدء محادثة مع صديقك، حيث يؤكدون الإحالة الخاصة بك", + "invite-people": "دعوة الأشخاص", + "invite-privacy-policy-public": "قمت بتثبيت Status من خلال رابط إحالة. من خلال الانضمام إلى هذه الدردشة ، فإنك تنسب الإحالة وتوافق على", + "invite-privacy-policy1": "بقبولك فإنك توافق على برنامج الإحالة", + "invite-privacy-policy2": "الشروط والأحكام.", + "invite-public-chat-home": "دعوة الإحالة", + "invite-public-chat-intro": "إليك بعض العملات المشفرة لتبدأ! استخدمه لتسجيل اسم ENS أو شراء حزمة ملصقات", + "invite-receive-account": "حساب لتلقي مكافأة الإحالة الخاصة بك", + "invite-reward": "اربح عملة مشفرة عن كل صديق تقوم بدعوته!", + "invite-reward-friend": "صديق:", + "invite-reward-friend-description": "سيتلقى صديقك حزمة مبتدئة تتكون من بعض {{reward}} للبدء", + "invite-reward-friend-name": "حزمة المبتدئين", + "invite-reward-you": "أنت:", + "invite-reward-you-description": "ادعُ صديقًا واحصل على {{reward}} كمكافأة إحالة. استخدمه للحصول على ملصقات واسم ENS وجرب التطبيقات اللامركزية", + "invite-reward-you-name": "مكافأة الإحالة", + "invite-select-account": "اختار حسابًا لتلقي مكافأة الإحالة الخاصة بك", + "invite-warning": "هذا العرض الترويجي صالح فقط لمستخدمي جهاز اندرويد غير المقيمين في الولايات المتحدة. يحتاج الصديق إلى تأكيد الإحالة في غضون 7 أيام", + "invited": "دعا", + "join": "انضم", + "join-a-community": "أو الانضمام إلى المجتمع", + "join-group-chat": "الانضمام إلى المجموعة", + "join-group-chat-description": "{{username}} دعوتك للانضمام إلى المجموعة {{group-name}}", + "join-me": "مرحبًا ، انضم إلي في Status: {{url}}", + "join-new-private-chat": "ابدأ محادثة خاصة جديدة", + "join-new-public-chat": "انضم إلى محادثة عامة", + "joined": "انضم", + "joined-group-chat-description": "لقد انضممت إلى {{group-name}} من دعوة {{username}}", + "key": "مفتاح", + "key-managment": "إدارة المفاتيح", + "key-on-device": "تم حفظ المفتاح الخاص على هذا الجهاز", + "keycard": "بطاقة المفتاح", + "keycard-access-reset": "تتم إعادة تعيين الوصول إلى بطاقة المفتاح", + "keycard-applet-install-instructions": "لتثبيت التطبيق الصغير ، يرجى اتباع التعليمات على https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "حاول تحريك البطاقة للعثور على قارئ NFC على جهازك", + "keycard-awaiting-title": "البحث جاري", + "keycard-backup": "قم بإنشاء بطاقة مفاتيح احتياطية", + "keycard-backup-success-body": "تم إنشاء بطاقة النسخ الاحتياطي بنجاح. يمكنك الآن استخدامه مع حسابك تمامًا مثل البطاقة الأساسية.", + "keycard-backup-success-title": "تم النسخ الاحتياطي بنجاح", + "keycard-blocked": "تم حظر بطاقة مفتاح. \n تحتاج إلى إعادة تعيين البطاقة لمتابعة استخدامها.", + "keycard-can-use-with-new-passcode": "يمكنك استخدام هذه البطاقة مع رمز المرور الجديد الخاص بك", + "keycard-cancel-setup-text": "سيؤدي هذا إلى إلغاء إعداد لوحة المفاتيح. يوصى بشدة بإنهاء الإعداد من أجل استخدام لوحة المفاتيح. هل تريد حقًا الإلغاء؟", + "keycard-cancel-setup-title": "عملية خطيرة", + "keycard-connected-description": "حاول إبقاء البطاقة ثابتة", + "keycard-connected-title": "متصل", + "keycard-desc": " سوف تحتاج إلى الحصول على بطاقة المفاتيح اندرويد فقط.", + "keycard-dont-ask-card": "لا تسأل عن بطاقة لتسجيل الدخول", + "keycard-enter-new-passcode": "أدخل رمز المرور الجديد {{step}}\/ 2 ", + "keycard-error-description": "قم بتوصيل البطاقة مرة أخرى للمتابعة", + "keycard-error-title": "انقطع الاتصال", + "keycard-factory-reset": "إرجاع البطاقة إلى إعدادات المصنع", + "keycard-factory-reset-text": "سيؤدي القيام بذلك إلى حذف أي عبارة ذاكرة مخزنة على البطاقة. تأكد من أن لديك نسخة احتياطية من عبارة ذاكري التي كنت تستخدمها مع Keycard هذه.", + "keycard-factory-reset-title": "هل أنت متأكد أنك تريد إجراء إعادة ضبط المصنع؟", + "keycard-free-pairing-slots": "يحتوي Keycard على {{n}} فتحات إقران مجانية", + "keycard-has-multiaccount-on-it": "هذه البطاقة ممتلئة. يمكن لكل بطاقة عقد زوج مفاتيح رئيسي واحد", + "keycard-init-description": "ضع البطاقة في الجزء الخلفي من هاتفك للمتابعة", + "keycard-init-title": "جارٍ البحث عن بطاقات ...", + "keycard-is-blocked-details": "لم يعد بإمكانك استخدام هذه البطاقة للوصول إلى هذا الحساب أو تسجيل الدخول إليه. هناك عدد كبير جدًا من محاولات رمز المرور ومحاولة PUK الفاشلة.", + "keycard-is-blocked-instructions": "للوصول إلى حسابك سوف تحتاج إلى مصنع إعادة تعيين بطاقتك. اضغط على الزر أدناه لبدء الإجراء، وسوف تحتاج الذاكرة الخاصة بك.", + "keycard-is-blocked-title": "تم حظر بطاقة المفتاح", + "keycard-is-frozen-details": "لحماية أصولك، يتم تجميد بطاقتك. أعد ضبط بطاقتك لإلغاء تجميدها وتمكن من إرسال المعاملات. يمكنك القيام بذلك مع PUK الخاص بك أو الذاكرة الخاصة بك.", + "keycard-is-frozen-factory-reset": "إعادة تعيين مع الذاكرة", + "keycard-is-frozen-reset": "إعادة الضبط مع PUK", + "keycard-is-frozen-title": "تم تجميد بطاقة المفتاح", + "keycard-onboarding-finishing-header": "الانتهاء من", + "keycard-onboarding-intro-header": "تخزين المفتاح الخاص علي بطاقة المفاتيح", + "keycard-onboarding-intro-text": "استعد ، قد يستغرق هذا بضع دقائق ، ولكن من المهم تأمين حسابك", + "keycard-onboarding-mnemonic-text": "ستحتاج أيضا إلى قطعة من الورق وقلم رصاص لكتابة عبارة البذور الخاصة بك.", + "keycard-onboarding-pairing-header": "يتم الآن اقران البطاقة...", + "keycard-onboarding-pin-text": "ستحتاج إلى إنشاء رمز مرور مكون من 6 أرقام والذي سيتم استخدامه لحماية الوصول إلى بطاقة Keycard الخاصة بك.", + "keycard-onboarding-preparing-header": "يتم الآن اعداد البطاقة...", + "keycard-onboarding-puk-code-header": "اكتب الرموز لأسفل \n وتخزينها بشكل آمن", + "keycard-onboarding-recovery-phrase-description": "مع هذه العبارة الأولية، يمكنك دائمًا استعادة مفتاحك. اكتب عبارة بذور أسفل. احتفظ به آمنًا، في وضع عدم الاتصال، وفصله عن هذا الجهاز.", + "keycard-onboarding-recovery-phrase-header": "النسخ الاحتياطي للعبارة الأولية", + "keycard-onboarding-recovery-phrase-text": "فقط لاجل عينيك. هذه هي العبارة السحرية المستخدمة لإنشاء مفتاحك.", + "keycard-onboarding-start-header": "امسك البطاقة في الخلف \n من هاتفك للبدء", + "keycard-onboarding-start-step1": "إنشاء رمز المرور", + "keycard-onboarding-start-step1-text": "حوالي ١ دقيقة. قم بإنشاء رمز مرور مكون من ٦ أرقام لتشفير المفاتيح", + "keycard-onboarding-start-step2": "اكتب في الاسفل PUK ورمز الاقتران", + "keycard-onboarding-start-step2-text": "حوالي دقيقه واحده. ستحتاج إلى قطعه من الورق وقلم رصاص لذلك", + "keycard-onboarding-start-step3": "نسخ احتياطي لعبارة البذور", + "keycard-onboarding-start-step3-text": "حوالي ١ دقيقة. أيضا قطعة من الورق وقلم رصاص ضرورية", + "keycard-onboarding-start-text": "والحفاظ على بطاقة للاتصال عبر الهاتف أثناء الإعداد. سيستغرق الإعداد حوالي 4 دقائق", + "keycard-processing-description": "حاول إبقاء البطاقة ثابتة", + "keycard-processing-title": "معالجة...", + "keycard-recover": "بطاقة مفقودة أو مجمدة؟", + "keycard-recover-text": "إذا كانت لديك عبارة ذاكري ، يمكنك إنشاء Keycard جديدة مرتبطة بهذا الحساب. يمكنك استخدام Keycard جديدة أو إعادة ضبط المصنع على بطاقة مجمدة.", + "keycard-recover-title": "إنشاء بطاقة جديدة لهذا الحساب؟", + "keycard-recovery-intro-button-text": "بدء الاسترداد", + "keycard-recovery-intro-header": "استعادة مفتاح مخزن على بطاقة مفتاح", + "keycard-recovery-intro-text": "إذا قمت بإنشاء مفتاح باستخدام بطاقة مفتاح من قبل وتريد الآن استخدام هذا المفتاح الخاص", + "keycard-recovery-no-key-header": "لا يوجد شيىء لاسترجاعه هنا", + "keycard-recovery-no-key-text": "لا تحتوي بطاقة مفتاح على أي مفتاح مخزّن. من أجل استخدامه، قم بإنشاء مفتاح جديد واختر بطاقة مفتاح لتخزين المفتاح", + "keycard-recovery-phrase-confirm-header": "تأكيد العبارة الأولية", + "keycard-recovery-phrase-confirmation-text": "لن تحصل على فرصة ثانية! إذا فقدت إمكانية الوصول ، على سبيل المثال عن طريق فقدان بطاقة المفاتيح الخاصة بك ، يمكنك الوصول إلى مفاتيحك فقط باستخدام العبارة الأولية. لا أحد ، ولكن لديك العبارة الأولية. اكتبه. ابقها آمنة.", + "keycard-recovery-phrase-confirmation-title": "هل كتبت عبارة seed ؟ ", + "keycard-recovery-success-header": "المفتاح الخاص تم استرجاعه بنجاح", + "keycard-redeem-title": "استرداد إلى", + "keycard-redeem-tx": "استرداد الأصول", + "keycard-redeem-tx-desc": "اضغط على بطاقة لتوقيع والحصول على الأصول", + "keycard-reset-passcode": "إعادة تعيين رمز المرور", + "keycard-success-description": "يمكنك إزالة البطاقة الآن", + "keycard-success-title": "النجاح", + "keycard-unauthorized-operation": "أنت غير مصرح له بإجراء هذه العملية. \n يرجى الضغط على بطاقة صالحة والمحاولة مرة أخرى.", + "keycard-upsell-subtitle": "تعزيز الأمن والراحة", + "language": "لغة", + "last-backup-performed": "آخر عملية نسخ احتياطي تم إجراؤها:", + "last-transaction": "آخر معاملة", + "learn-more": "أعرف أكثر", + "learn-more-about-keycard": "تعرف علي المزيد حول بطاقة المفاتيح", + "leave": "غادر", + "leave-chat": "مغادرة الدردشة", + "leave-chat-confirmation": "ستتم إزالة سجل الدردشة من جهازك. بعد الانضمام مرة أخرى ، لن تتمكن من استرداد أي من سجلك.", + "leave-community": "اترك المجتمع", + "leave-confirmation": "مغادرة{{chat-name}}", + "leave-group": "غادر المجموعة", + "left": "اليسار", + "les-ulc": "LES \/ ULC", + "lets-go": "لنذهب", + "light": "خلفية بيضاء", + "limit": "حد", + "linked-on": "مرتبط في {{date}}", + "load-messages-before": "قبل {{date}}", + "load-more-messages": "↓ جلب المزيد من الرسائل", + "load-more-timeline": "↓ جلب المزيد", + "loading": "جار التحميل...", + "local-notifications": "الإخطارات المحلية", + "local-notifications-subtitle": "تمكين خدمة الخلفية", + "lock-app-with": "قفل التطبيق مع", + "log-level": "تسجيل مستوى", + "log-level-settings": "إعدادات مستوى السجل", + "logging": "تسجيل", + "logging-enabled": "تمكين التسجيل؟", + "login-pin-description": "ادخل رمز المرور المكون من ٦ أرقام لفتح حسابك المتعدد", + "logout": "تسجيل الخروج", + "logout-app-content": "سيتم تسجيل الحساب عند إلغاء قفلها مرة أخرى ، سيتم استخدام الشبكة المحددة", + "logout-are-you-sure": "هل انت متأكد أنك تريد \n لتسجيل الخروج؟", + "logout-key-management": "تحتاج إلى تسجيل الخروج للوصول إلى إدارة المفاتيح.", + "logout-title": "تسجيل الخروج ؟", + "looking-for-cards": "جارٍ البحث عن بطاقات ...", + "lost-connection": "فقدان الاتصال", + "low-tip": "الإكرامية منخفضة للغاية", + "lower-than-average-tip": "أقل من متوسط الإكرامية", + "mail-should-be-configured": "يجب تغيير البريد", + "mailserver-address": "عنوان عقدة Status", + "mailserver-automatic": "الاختيار التلقائي", + "mailserver-automatic-switch-explanation": "اختر أسرع عقدة Status متاحة", + "mailserver-connection-error": "تعذر الاتصال بعقدة Status", + "mailserver-content": "عقدة في شبكة Status تقوم بتوجيه الرسائل وتخزينها لمدة تصل إلى 30 يومًا.", + "mailserver-details": "تفاصيل عقدة Status", + "mailserver-error-content": "تعذر الوصول إلى عقدة Status التي حددتها.", + "mailserver-error-title": "خطأ في الاتصال بعقدة Status", + "mailserver-format": "enode: \/\/ {enode-id} : {password} @ {ip-address} : {port}", + "mailserver-pick-another": "اختر عقدة Status أخرى", + "mailserver-reconnect": "تعذر الاتصال بعقدة Status. انقر لإعادة الاتصال", + "mailserver-request-error-content": "تم إرجاع الخطأ التالي بواسطة عقدة Status: {{error}}", + "mailserver-request-error-status": "حدث خطأ أثناء جلب السجل ، تحقق من السجلات للحصول على التفاصيل", + "mailserver-request-error-title": "خطأ في طلب عقدة Status", + "mailserver-request-retry": "إعادة محاولة الطلب", + "mailserver-retry": "إعادة المحاولة", + "mailserver-title": "عقدة Status", + "main-currency": "العملة الرئيسية", + "main-networks": "الشبكات الرئيسية", + "main-wallet": "المحفظة الرئيسية", + "mainnet-network": "الشبكة الرئيسية", + "make-admin": "جعل المشرف", + "manage-connections": "إدارة الاتصالات من داخل اتصالات التطبيق", + "manage-keys-and-storage": "إدارة المفاتيح والتخزين", + "mark-all-read": "ضع علامة بأنه تم القراءة", + "master-account": "الحساب الرئيسي", + "max-fee": "الحد الأقصى للرسوم", + "max-priority-fee": "الحد الأقصى لرسوم الأولوية", + "maximum-fee": "الحد الأقصى للرسوم", + "maximum-fee-desc": "الحد الأقصى للسعر الإجمالي للمعاملة. إذا تجاوزت رسوم الكتلة الأساسية الحالية هذا، فسيتم تضمين معاملتك في الكتلة التالية برسوم أساسية أقل.", + "maybe-later": "ربما في وقت لاحق", + "member-ban": "حظر عضو", + "member-kick": "طرد عضو", + "members": { + "zero": "عضو", + "one": "عضو", + "two": "عضوان", + "few": "أعضاء", + "many": "أعضاء", + "other": "أعضاء" + }, + "members-active": { + "zero": "عضو", + "one": "عضو", + "two": "عضوان", + "few": "أعضاء", + "many": "أعضاء", + "other": "أعضاء" + }, + "members-active-none": "لا يوجد أعضاء", + "members-count": "{{count}} أعضاء", + "members-label": "الأعضاء", + "members-limit-reached": "تم الوصول إلى أقصى عدد من الأعضاء", + "members-title": "الأعضاء", + "membership-approval": "تتطلب الموافقة", + "membership-approval-description": "مجتمعك مجاني للانضمام ، ولكن يجب أن تتم الموافقة على الأعضاء الجدد من قبل منشئ المجتمع أولاً", + "membership-button": "شروط العضوية", + "membership-declined": "تم رفض طلب العضوية", + "membership-description": "تتطلب عضوية المجموعة أن يتم قبولك من قبل مسؤول المجموعة", + "membership-ens": "طلب اسم مستخدم ENS", + "membership-ens-description": "يتطلب مجتمعك اسم مستخدم ENS ليتمكن من الانضمام", + "membership-free": "لا يوجد شرط", + "membership-free-description": "مجتمعك مجاني لأي شخص للانضمام", + "membership-invite": "تتطلب دعوة من عضو آخر", + "membership-invite-description": "لا يمكن الانضمام إلى مجتمعك إلا من خلال دعوة من أعضاء المجتمع الحاليين", + "membership-none": "لا شيء", + "membership-none-placeholder": "يمكنك أن تطلب من الأعضاء الجدد تلبية معايير معينة قبل أن يتمكنوا من الانضمام. يمكن تغيير هذا في أي وقت", + "membership-request-pending": "طلب العضوية في الإنتظار", + "membership-requests": "طلبات العضوية", + "membership-title": "شروط العضوية", + "message": "رسالة", + "message-not-sent": "لم يتم إرسال الرسالة", + "message-options-cancel": "إلغاء", + "message-reply": "الرد", + "messages": "رسائل", + "messages-from-contacts-only-subtitle": "يمكن فقط للأشخاص الذين أضفتهم كجهات اتصال بدء محادثة جديدة معك أو دعوتك إلى مجموعة", + "might-break": "قد كسر بعض ÐApps", + "migration-successful": "تم الترحيل بنجاح", + "migration-successful-text": "تم ترحيل الحساب بنجاح إلى بطاقة Keycard", + "migrations-failed-content": "{{message}} \n إصدار المخطط: الأولي {{initial-version}} ، الحالي {{current-version}}الاخير ، {{last-version}} \n\nحدث خطأ في قاعدة البيانات. أموالك ومفتاح الدردشة آمنان. لا يمكن استعادة البيانات الأخرى ، مثل الدردشات وجهات الاتصال الخاصة بك. \" {{erase-multiaccounts-data-button-text}} \" سيزيل جميع البيانات الأخرى ويسمح لك بالوصول إلى أموالك وإرسال الرسائل.", + "miners-higher-fee": "من المحتمل أن يقوم عمال المناجم بتضمين معاملتك في وقت سابق إذا دفعت رسومًا أعلى.", + "minimum-received": "تلقى الحد الأدنى", + "mobile-network-ask-me": "اسالني متى علي شبكه الجوال", + "mobile-network-continue-syncing": "متابعة المزامنة", + "mobile-network-continue-syncing-details": "يمكنك تغيير هذا لاحقًا في الإعدادات", + "mobile-network-go-to-settings": "اذهب للاعدادات", + "mobile-network-settings": "بيانات الجوال", + "mobile-network-sheet-configure": "يمكنك تكوين المزامنة في أكثر \n التفاصيل في", + "mobile-network-sheet-offline": "لا توجد شبكة Wi-Fi ، تم تعطيل مزامنة الرسائل.", + "mobile-network-sheet-offline-details": "المزامنة باستخدام شبكة الجوال متوقفة", + "mobile-network-sheet-remember-choice": "تذكر خياري", + "mobile-network-sheet-settings": "الإعدادات", + "mobile-network-start-syncing": "ابدأ المزامنة", + "mobile-network-stop-syncing": "إيقاف المزامنة", + "mobile-network-stop-syncing-details": "حتى الاتصال بشبكه Wi-fi ؟", + "mobile-network-use-mobile": "استخدام بيانات المحمول", + "mobile-network-use-mobile-data": "تستخدم Status الكثير من البيانات عند مزامنة الدردشات.", + "mobile-network-use-wifi": "واي فاي فقط", + "mobile-syncing-sheet-details": "تستخدم الحالة الكثير من البيانات عند مزامنة الدردشات.", + "mobile-syncing-sheet-title": "هل تريد المزامنة باستخدام بيانات الجوال؟", + "more": "المزيد", + "move-and-reset": "نقل وإعادة تعيين", + "move-keystore-file": "نقل ملف مخزن المفاتيح", + "move-keystore-file-to-keycard": "نقل ملف مخزن المفاتيح إلى بطاقة المفتاح؟", + "multiaccount-exists-content": "مفاتيح هذا الحساب موجودة بالفعل ولا يمكن إضافتها مرة أخرى. إذا فقدت كلمة المرور أو رمز المرور أو بطاقة المفتاح ، فقم بإلغاء تثبيت التطبيق وإعادة التثبيت والوصول إلى مفاتيحك عن طريق إدخال العبارة الأولية", + "multiaccount-exists-title": "الحساب المتعدد موجود بالفعل", + "multiaccounts-recover-enter-phrase-text": "أدخل 12 أو 15 أو 18 أو 21 أو 24 كلمة. \n افصل الكلمات بمسافة واحدة.", + "multiaccounts-recover-enter-phrase-title": "أدخل العبارة الأولية", + "mute": "كتم الصوت", + "my-accounts": "حساباتي", + "my-accounts-empty": "ستظهر حساباتك المتوفرة هنا", + "my-profile": "ملفي الشخصي", + "my-status": "حالتي", + "name": "اسم", + "name-of-token": "اسم token الخاص بك", + "name-optional": "الاسم (اختياري)", + "name-your-channel": "قم بتسمية قناتك", + "name-your-channel-placeholder": "اسم القناة", + "name-your-community": "اسم مجتمعك", + "name-your-community-placeholder": "اسم جذاب", + "need-help": "تحتاج إلى مساعدة ؟ ", + "network": "شبكة الاتصال", + "network-chain": "سلسلة الشبكة", + "network-details": "تفاصيل الشبكة", + "network-fee": "رسوم الشبكة", + "network-id": "عنوان الشبكة", + "network-info": "معلومات الشبكة", + "network-invalid-network-id": "معرف الشبكة المحدد لا يتوافق مع معرف الشبكة بواسطة عنوان RPC", + "network-invalid-status-code": "رمز status غير صالح: {{code}}", + "network-invalid-url": "عنوان URL للشبكة غير صالح", + "network-settings": "اعدادات الشبكة", + "never": "أبدا", + "new": "الجديد", + "new-category": "فئة جديدة", + "new-chat": "دردشة جديدة", + "new-community-title": "مجتمع جديد", + "new-contact": "جهة اتصال جديدة", + "new-contract": "عقد جديد", + "new-favourite": "المفضلة الجديدة", + "new-group": "مجموعة جديدة", + "new-group-chat": "دردشة جماعيه جديده", + "new-network": "شبكة جديدة", + "new-password": "كلمة مرور جديدة", + "new-pin-description": "أدخل رمز مرور جديد مكون من ٦ أرقام", + "new-public-group-chat": "انضم إلى الدردشة العامة", + "new-puk-description": "أدخل رمز المرور الجديد المكون من 12 رقمًا", + "new-status": "status جديد", + "new-tab": "علامة تبويب جديدة", + "next": "التالى", + "nickname": "الاسم المستعار", + "nickname-description": "تساعدك الألقاب في التعرف على الآخرين في Status. \n أنت فقط من يمكنه رؤية الألقاب التي أضفتها", + "no": "لا", + "no-collectibles": "لا تتوفر المقتنيات", + "no-contacts": "لا اتصالات حتى الآن", + "no-keycard-applet-on-card": "لا يوجد برنامج Keycard على البطاقة", + "no-messages": "لا توجد رسائل", + "no-pairing-slots-available": "هذه البطاقة مقترنة بالفعل بـ 5 أجهزة ولا يمكن إقرانها بهذا الجهاز. يرجى استخدام أحد الأجهزة المقترنة وتسجيل الدخول باستخدام هذه البطاقة وتحرير فتحات الاقتران على البطاقة", + "no-pinned-messages": "لا توجد رسائل مثبتة", + "no-result": "لا توجد نتائج", + "no-thanks": "لا شكراً", + "no-tokens-found": "لم يتم العثور على عملة رمزية", + "node-address": "عنوان العقدة", + "node-details": "تفاصيل العقدة", + "node-info": "معلومات العقدة", + "node-version": "نسخة العقدة", + "nodes-disabled": "عُقد Status معطلة", + "non-archival-node": "لا تدعم نقطة نهاية RPC طلبات الأرشفة. قد يكون سجل التحويلات المحلية الخاص بك غير مكتمل.", + "non-contacts": "لا جهات اتصالات", + "nonce": "اعتباطي", + "none": "لا شيء", + "normal": "طبيعي", + "not-applicable": "لا ينطبق على المعاملات غير الموقعة", + "not-connected-nodes": "غير متصل بعقدة Status", + "not-connected-to-peers": "غير متصل بأي أقران", + "not-enough-snt": "غير كافي SNT", + "not-found": "لم يتم العثور على", + "not-keycard-text": "البطاقة التي استخدمتها ليست بطاقة مفتاحيه. تحتاج إلى شراء بطاقة المفاتيح لاستخدامها", + "not-keycard-title": "لا بطاقة المفاتيح", + "not-registered": "غير مسجل", + "notification-settings": "إعدادات الإشعار", + "notifications": "الاخطارات", + "notifications-non-contacts": "إخطارات من غير جهات الاتصال", + "notifications-preferences": "تفضيلات الإشعار", + "notifications-servers": "خوادم الإشعار", + "notifications-switch": "إظهار الإشعارات", + "notifications-transactions": "معاملات المحفظة", + "notify": "اعلام", + "now": "الآن", + "off": "إيقاف", + "off-status-tree": "خارج شجرة الحالة", + "offline": "غير متصل على الانترنت", + "offline-messaging-use-history-explanation": "قم بتمكين عُقد Status لجلب الرسائل التي تم إرسالها أثناء إغلاق التطبيق. عند التمكين ، تحصل عقدة Status على عنوان IP الخاص بك. عند التعطيل ، لن تتلقى رسائل عند إغلاق التطبيق ولن تراها عند فتح التطبيق لاحقًا.", + "offline-messaging-use-history-nodes": "استخدم عقد Status", + "ok": "حسنا", + "ok-continue": "حسنا ، تابع", + "ok-got-it": "حسنا، حصلت عليه", + "ok-save-pass": "حسنا ، احفظ كلمة المرور", + "okay": "حسنا", + "on": "على", + "on-status-tree": "على شجرة Status", + "once-enabled-share-metadata": "بمجرد التمكين ، قد تشارك الروابط المنشورة في الدردشة بياناتك الوصفية مع الموقع", + "one-day": "يوم واحد", + "one-month": "شهر واحد", + "one-week": "أسبوع واحد", + "open": "فتح", + "open-chat": "فتح الدردشة", + "open-dapp": "افتح ÐApp", + "open-dapp-store": "اكتشاف ÐApps", + "open-home": "فتح...", + "open-in-new-tab": "فتح في علامة تبويب جديدة", + "open-membership": "العضوية المفتوحة", + "open-nfc-settings": "فتح إعدادات NFC", + "open-on-block-explorer": "فتح في مستكشف الكتل", + "opening-buy-crypto": "جارٍ فتح {{site}} ...", + "optimal": "الامثل", + "optional": "اختياري", + "or": "او", + "outgoing": "المنتهية", + "outgoing-transaction": "المعاملات الصادرة", + "page-camera-request-blocked": "تم حظر طلبات الكاميرا. لتمكين طلبات الكاميرا ، انتقل إلى الإعدادات", + "page-would-like-to-use-camera": "ترغب في استخدام الكاميرا الخاصة بك", + "pair": "إقران الأجهزة", + "pair-card": "إقران إلى هذا الجهاز", + "pair-code": "رمز الاقتران", + "pair-code-explanation": "أزواج بطاقة إلى جهاز مختلف (حتى 5) لفتح المفاتيح وتوقيع المعاملات باستخدام نفس بطاقة المفاتيح", + "pair-code-placeholder": "رمز الاقتران ...", + "pair-this-card": "إقران هذه البطاقة", + "pair-this-device": "الإعلان عن الجهاز", + "pair-this-device-description": "قم بإقران أجهزتك لمزامنة جهات الاتصال والدردشة بينهما", + "paired-devices": "الأجهزة المقترنة", + "pairing": "الاقران", + "pairing-card": "بطاقة الاقتران", + "pairing-changed": "تم تغيير رمز الاقتران", + "pairing-code-placeholder": "رمز الاقتران ...", + "pairing-code_error1": "رموز الاقتران غير متطابقة.", + "pairing-go-to-installation": "انتقل إلى إعدادات الاقتران", + "pairing-maximum-number-reached-content": "يرجى تعطيل أحد أجهزتك قبل تمكين جهاز جديد.", + "pairing-maximum-number-reached-title": "تم الوصول إلى أقصى عدد من الأجهزة", + "pairing-new-installation-detected-content": "تم الكشف عن جهاز جديد.\nمن أجل استخدام أجهزتك بشكل صحيح ، من المهم اقرانها وتمكينها قبل استخدامها.\nالرجاء الانتقال إلى قسم الجهاز ضمن الإعدادات لاقران أجهزتك.", + "pairing-new-installation-detected-title": "تم الكشف عن جهاز جديد", + "pairing-no-info": "لا توجد معلومات", + "pairing-please-set-a-name": "الرجاء تعيين اسم لجهازك.", + "passphrase": "عبارة المرور", + "password": "كلمه المرور", + "password-description": "على الأقل 6 خانات. ستحتاج إلى كلمة المرور هذه لفتح الحالة وتأكيد المعاملات", + "password-mismatch": "كلمة المرور الجديدة والتأكيد غير متطابقين", + "password-placeholder": "كلمه المرور...", + "password-placeholder2": "تأكيد كلمة المرور الخاصة بك", + "password-reset-in-progress": "تغيير كلمة المرور...", + "password-reset-success": "تم تغيير كلمة المرور", + "password-reset-success-message": "ستحتاج إلى تسجيل الدخول مرة أخرى", + "password_error1": "كلمات المرور غير متطابقة.", + "paste": "لصق", + "paste-json": "لصق JSON", + "pay-to-chat": "الدفع للدردشة", + "peer-content": "جهاز متصل بشبكة دردشة Status. يمكن لكل مستخدم تمثيل واحد أو أكثر من أقرانه، اعتمادًا على عدد أجهزتهم.", + "peer-title": "الند", + "peer-to-peer": "واحد الى واحد", + "peers": "الأقران", + "pending": "المعلقه", + "pending-confirmation": "في انتظار التأكيد...", + "pending-invitations": "طلبات العضوية المعلقة", + "per-gas-price-limit": "حد سعر الغاز", + "per-gas-tip-limit": "حد الغاز", + "perform-backup": "إجراء النسخ الاحتياطي", + "permissions": "أذونات", + "phone-e164": "الدولية 1", + "photos": "الصور", + "photos-access-error": "لمنح اذن الصور المطلوبة ، يرجى الانتقال إلى إعدادات النظام والتاكد من تحديد Status > الصور المختارة", + "pin": "تثبيت", + "pin-changed": "أدخل رمز المرور المكون من 6 أرقام للمتابعة", + "pin-code": "رمز المرور المكون من ٦ أرقام", + "pin-limit-reached": "تم الوصول إلى حد رقم التعريف الشخصي. قم بإلغاء تثبيت رسالة سابقة أولاً.", + "pin-mismatch": "رمز مرور خاطئ", + "pin-one-attempt": " محاولة واحدة ", + "pin-one-attempt-blocked-after": "قبل حجب بطاقة المفتاح الخاصة بك", + "pin-one-attempt-blocked-before": "كن حذرا، لديك فقط", + "pin-one-attempt-frozen-after": "قبل أن يتم تجميد بطاقة المفتاح الخاصة بك", + "pin-one-attempt-frozen-before": "كن حذرا، لديك فقط", + "pin-retries-left": "{{number}} محاولات متبقية", + "pinned-by": "تم التثبيت بواسطة", + "pinned-messages": "الرسائل المثبتة", + "pinned-messages-count": { + "zero": "", + "one": "1 الرسائل المثبتة", + "two": "", + "few": "", + "many": "", + "other": "{{count}} الرسائل المثبتة" + }, + "pinned-messages-empty": "ستظهر الرسائل المثبتة هنا. لتثبيت رسالة، اضغط مع الاستمرار مع الاستمرار واضغط على \"تثبيت\"", + "powered-by-paraswap": "مدعوم من Paraswap", + "preference": "التفضيل", + "preview-privacy": "معاينة وضع الخصوصية", + "previewing-may-share-metadata": "قد تقوم معاينة الروابط من مواقع الويب هذه بمشاركة بيانات التعريف الخاصة بك مع أصحابها", + "price-impact": "تأثير السعر", + "price-impact-desc": "التأثير السعري المقدر لهذه الصفقة. إذا تجاوزت الرسوم الأساسية الحالية لهذه الكتلة، سيتم تضمين المعاملة الخاصة بك في كتلة التالية مع رسوم أساسية أقل.", + "principles": "المبادئ", + "priority": "أولوية", + "privacy": "الخصوصيه", + "privacy-and-security": "الخصوصية والأمان", + "privacy-photos": "خصوصية صورة الملف الشخصي", + "privacy-policy": "سياسة خاصة", + "privacy-show-to-warning": "سيستمر الأشخاص الذين شاهدوا صورة ملفك الشخصي بالفعل في القيام بذلك", + "private-key": "مفتاح سري", + "private-notifications": "الإخطارات الخاصة", + "private-notifications-descr": "ستعلمك Status بالرسائل الجديدة. يمكنك تعديل تفضيلات الإشعارات الخاصة بك لاحقًا في الإعدادات", + "processing": "لحظة واحدة", + "product-information": "معلومات المنتج", + "profile": "الملف الشخصي", + "profile-deleted-content": "تم حذف ملف التعريف الخاص بك بنجاح", + "profile-deleted-keycard": "يمكنك الآن استعادة زوج مفاتيح آخر على بطاقة المفاتيح الخاص بك", + "profile-deleted-title": "تم حذف الملف الشخصي", + "profile-details": "تفاصيل الملف الشخصي", + "profile-not-found": "لم يتم العثور على ملف التعريف", + "profile-pic-pick": "اختر من المعرض", + "profile-pic-remove": "إزالة الصورة", + "profile-pic-take": "التقط صورة", + "profile-picture-updated": "تم تحديث صورة الملف الشخصي", + "public-channel": "قناة عامة", + "public-chat": "دردشة عامة", + "public-chat-description": "المحادثة هي المكان الذي يمكنك فيه الخروج مع الآخرين وتكوين صداقات والتحدث عن مواضيع تهمك.", + "public-chats": "الدردشات العامة", + "public-group-status": "عامة", + "public-group-topic": "الموضوع", + "public-key": "المفتاح العمومي", + "puk-and-pairing-codes-displayed": "PUK ورموز الاقتران المعروضة", + "puk-changed": "تم تغيير رمز مرور المكون من 12 رقمًا", + "puk-code": "رمز PUK", + "puk-code-explanation": "إذا نسيت رمز المرور المكون من 6 أرقام أو أدخلته بشكل غير صحيح 3 مرات ، فستحتاج إلى هذا الرمز لإلغاء قفل بطاقتك.", + "puk-mismatch": "رمز PUK خاطئ", + "push-failed-transaction": "فشلت معاملتك", + "push-failed-transaction-body": "{{value}}{{currency}}إلى{{to}}", + "push-inbound-transaction": "لقد تلقيت {{value}} {{currency}}", + "push-inbound-transaction-body": "من {{from}} الى{{to}}", + "push-notifications-server-enabled": "تمكين الخادم", + "push-notifications-servers": "إشعارات خوادم", + "push-outbound-transaction": "لقد أرسلت {{value}} {{currency}}", + "push-outbound-transaction-body": "من {{from}} إلى {{to}}", + "quiet-days": "{{quiet-days}} أيام", + "quiet-hours": "{{quiet-hours}} ساعات", + "re-encrypt-key": "إعادة تشفير المفتاح الخاص", + "rearrange-categories": "إعادة ترتيب الفئات", + "receive": "تلقي", + "receive-transaction": "تلقي المعاملة", + "recent": "الأخيرة", + "recent-empty": "ستظهر العناوين المستخدمة مؤخرًا هنا", + "recent-recipients": "جهات الاتصال", + "recently-used-stickers": "ستظهر الملصقات المستخدمة مؤخرًا هنا", + "recipient": "المستلم", + "recipient-code": "أدخل عنوان المستلم", + "recipient-code-placeholder": "0x ... أو username.domain.eth", + "recover": "استعادة", + "recover-key": "الوصول إلى المفاتيح الحالية", + "recover-keycard-multiaccount-not-supported": "مفاتيح هذا الحساب موجودة بالفعل ولا يمكن إضافتها مرة أخرى. إذا فقدت كلمة المرور أو رمز المرور أو بطاقة المفتاح ، فقم بإلغاء تثبيت التطبيق وإعادة التثبيت والوصول إلى مفاتيحك عن طريق إدخال العبارة الأولية", + "recover-with-keycard": "استرداد مع بطاقة المفتاح", + "recover-with-seed-phrase": "استرداد مع كلمات العبارة الأولية", + "recovering-key": "يتم الآن محاولة الوصول إلى المفتاح...", + "recovery-confirm-phrase": "تأكيد العبارة الأولية", + "recovery-phrase": " العبارة الأولية", + "recovery-success-text": "سيكون عليك إنشاء رمز أو كلمة مرور جديدة لإعادة تشفير المفتاح الخاص بك", + "recovery-typo-dialog-description": "يرجى ملاحظة ، يجب أن تستخدم العبارة الأولية نفس الكلمات وترتيبها كما تلقيتها", + "recovery-typo-dialog-title": "هل عبارة البذور صحيحة؟", + "redeem-amount": "{{quantity}}المكافآت المتاحة", + "redeem-now": "استرد الآن", + "redeem-success": "تم استرداد المكافأة بنجاح!", + "reduced-tip": "سيتم تخفيض إكرامية الأولوية", + "refresh": "تحديث", + "registered": "مسجل", + "reject": "رفض", + "reject-and-delete": "رفض وحذف", + "remember-me": "تذكرني", + "remind-me-later": "أرني هذا مرة أخرى", + "remote-notifications": "الإخطارات عن بعد", + "remote-notifications-subtitle": "تمكين إشعارات جوجل", + "remove": "ازاله", + "remove-favourite": "إزالة المفضلة", + "remove-from-chat": "إزالة من الدردشة", + "remove-from-contacts": "إزالة من الاتصالات", + "remove-from-contacts-text": "عن طريق إزالة مستخدم من قائمة جهات الاتصال الخاصة بك ، فإنك لا تخفي عنوان محفظتك عنهم", + "remove-group": "إزالة المجموعة", + "remove-network": "أزاله شبكه الاتصال", + "remove-token": "إزالة عملة رمزية", + "removed": "إزالة", + "repeat-pin": "أدخل رمز المرور المكون من 6 أرقام", + "repeat-puk": "كرر رمز مرور الجديد المكون من 12 رقمًا", + "replying-to": "الرد على{{author}}", + "report-bug-email-template": "1. وصف المشكلة\n {{description}}\n \n\n 2. خطوات التكاثر\n {{steps}}\n \n\n 3. أرفق لقطات الشاشة التي يمكن أن تعرض المشكلة، من فضلك \n", + "request-access": "طلب الدخول", + "request-feature": "طلب ميزة", + "request-membership": "طلب عضوية", + "request-pending": "الطلب في الإنتظار...", + "request-transaction": "طلب المعاملة", + "required-field": "الحقل المطلوب", + "resend-message": "إعادة إرسال", + "reset-card": "إعادة تعيين البطاقة", + "reset-card-description": "ستقوم هذه العملية باعاده تعيين البطاقة إلى الحالة الاوليه. وسوف يمحو جميع بيانات البطاقة بما في ذلك المفاتيح الخاصة. العملية ليست قابله للعكس.", + "reset-database": "إعادة تعيين قاعدة البيانات", + "reset-database-warning": "حذف الدردشات وجهات الاتصال والإعدادات. مطلوب عندما تفقد كلمة مرورك", + "reset-database-warning-keycard": "حذف الدردشات وجهات الاتصال والإعدادات.", + "reset-password": "إعادة تعيين كلمة المرور", + "restore-defaults": "استعادة الضبط الافتراضي", + "retry": "إعادة المحاولة", + "revoke-access": "إلغاء الوصول", + "rpc-url": "RPC URL", + "rpc-usage-copy": "نسخ", + "rpc-usage-filter": "أساليب التصفية", + "rpc-usage-get-stats": "تحديث", + "rpc-usage-info": "إحصائيات استخدام RPC", + "rpc-usage-reset": "إعادة ضبط", + "safe-estimate": "تقدير آمن", + "save": "حفظ", + "save-password": "حفظ كلمة المرور", + "save-password-unavailable": "اضبط رمز مرور الجهاز لحفظ كلمة المرور", + "save-password-unavailable-android": "حفظ كلمه المرور غير متوفر: قد يكون جهازك متجذرا أو يفتقر إلى ميزات الأمان الضرورية.", + "scan-qr": "مسح رمز الاستجابة السريعة", + "scan-qr-code": "مسح رمز الاستجابة السريعة مع عنوان المحفظة", + "scan-tokens": "مسح العملات الرمزية", + "search": "البحث", + "search-no-chat-found": "لا توجد نتائج بحث. هل تعني", + "secret-keys-confirmation-text": "ستحتاجهم إلى الاستمرار في استخدام Keycard في حالة فقدان هاتفك.", + "secret-keys-confirmation-title": "كتب الرموز أسفل؟", + "security": "الأمان", + "see-details": "انظر التفاصيل", + "see-it-again": "شاهدها مره أخرى", + "see-suggestions": "الاطلاع على الاقتراحات", + "seed-key-uid-mismatch": "العبارة الأولية لا تتطابق", + "seed-key-uid-mismatch-desc-1": "العبارة الأولية التي أدخلتها لا تتطابق مع {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "لإدارة المفاتيح لهذا الحساب ، تحقق من العبارة الأولية وحاول مرة أخرى.", + "seed-phrase-content": "مجموعة من الكلمات سهلة القراءة، تم اختيارها عشوائيًا من قائمة BIP39 القياسية وتستخدم لاسترداد أو الوصول إلى حساب الايثريوم الخاص بك في محافظ وأجهزة أخرى. يشار إليها أيضًا باسم \"عبارة ذاكرة\" أو \"عبارة الاسترداد\" أو \"المحفظة الاحتياطية\" عبر نظام التشفير البيئي. تستخدم معظم تطبيقات التشفير نفس المعيار لإنشاء حسابات.", + "seed-phrase-placeholder": "عبارة أصل...", + "seed-phrase-title": "العبارة الأولية", + "select": "حدد", + "select-account": "حدد حساب", + "select-account-dapp": "حدد الحساب الذي ترغب في استخدامه مع Dapps", + "select-account-first": "تحديد حساب أولاً", + "select-chat": "اختر الدردشة لبدء المراسلة", + "select-new-location-for-keys": "حدد موقعًا جديدًا لحفظ (المفاتيح) الخاص بك", + "select-token-to-receive": "حدد العملة الرمزية للاستلام", + "select-token-to-swap": "حدد العملة الرمزية للتبديل", + "selected": "مختارة", + "send-logs": "الإبلاغ عن خطأ", + "send-logs-to": "الإبلاغ عن خطأ إلى {{email}}", + "send-message": "إرسال رسالة", + "send-push-notifications": "إرسال إشعارات", + "send-push-notifications-description": "عند تعطيلها، ، لن يتم إخطار الشخص الذي يتلقى رسائلك بوصوله", + "send-request": "ارسل طلب", + "send-request-amount": "المبلغ", + "send-request-amount-max-decimals": "الحد الأقصى لعدد الكسور العشرية هو {{asset-decimals}}", + "send-request-unknown-token": "token غير معروف - {{asset}}", + "send-sending-to": "إلى {{recipient-name}}", + "send-transaction": "إرسال المعاملة", + "sending": "إرسال", + "sent-at": "أرسلت في", + "server": "الخادم", + "set-a-topic": "إنشاء موضوع", + "set-currency": "ضبط العملة الافتراضية", + "set-custom-fee": "تعيين رسوم مخصصة", + "set-dapp-access-permissions": "تعيين أذونات الوصول إلى DApp", + "set-max": "تعيين الحد الأقصى", + "settings": "الإعدادات", + "share": "مشاركه", + "share-address": "مشاركة العنوان", + "share-chat": "مشاركة الدردشة", + "share-contact-code": "مشاركة مفتاح الدردشة الخاص بي", + "share-dapp-text": "تحقق من هذا التطبيق الذي أستخدمه في Status: {{link}}", + "share-link": "مشاركة الرابط", + "share-logs": "مشاركة السجلات", + "share-my-profile": "تعديل الملف الشخصي", + "share-profile": "مشاركة الملف الشخصي", + "share-profile-link": "مشاركة رابط الملف الشخصي", + "share-public-chat-text": "تحقق من هذه الدردشة العامة على تطبيق Status: {{link}}", + "shared": "المشتركة", + "sharing-copied-to-clipboard": "نسخ", + "sharing-copy-to-clipboard": "نسخ", + "sharing-data-desc-1": "يتم التحقق من صحة البيانات وفقا للقواعد العامة لضمان عدم إرسال أي بيانات حساسة. لا تثق، تحقق.", + "sharing-data-desc-2": "يتم إرسال بيانات الاستخدام مشفرة من طرف إلى طرف عبر شبكة نظير إلى نظير الخاصة بStatus", + "sharing-data-desc-3": "بدلا من مفتاح الدردشة العادي، يتم استخدام مفتاح استخدام واحد", + "sharing-data-desc-4": "لا يمكن إقران بيانات الاستخدام بعنوان IP الخاص بك", + "sharing-data-desc-5": "البيانات التراكمية لجميع المستخدمين متاحة للجمهور", + "sharing-data-desc-6": "تتم إزالة البيانات من هاتفك بعد إرسالها", + "sharing-share": "مشاركه", + "show-less": "عرض أقل", + "show-more": "عرض المزيد", + "show-notifications": "إظهار الإشعارات", + "show-profile-pictures": "الاطلاع على صور الملف الشخصي من", + "show-profile-pictures-to": "إظهار صورة ملف التعريف الخاص بك إلى", + "show-qr": "عرض رمز الاستجابة السريعة", + "show-transaction-data": "إظهار بيانات المعاملات", + "sign-and-send": "التوقيع والإرسال", + "sign-anyway": "تسجيل على أي حال", + "sign-in": "تسجيل الدخول", + "sign-message": "توقيع الرسالة", + "sign-out": "تسجيل الخروج", + "sign-request-failed": "تعذر توقيع الرسالة", + "sign-with": "تسجيل مع", + "sign-with-password": "تسجيل الدخول بكلمة مرور", + "sign-you-in": "تسجيل دخولك في...", + "signing": "التوقيع", + "signing-a-message": "توقيع رسالة", + "signing-phrase": "عبارة التوقيع", + "skip": "تخطي", + "slippage": "انزلاق", + "slow": "بطيء", + "something-went-wrong": "هناك خطأ ما", + "soon": "قريبا", + "specify-address": "حدد العنوان", + "specify-name": "حدد اسم", + "specify-network-id": "تحديد معرف شبكه الاتصال", + "specify-rpc-url": "حدد عنوان URL لـ RPC", + "specify-server-public-key": "أدخل المفتاح العام للخادم", + "specify-symbol": "حدد رمزًا", + "start-chat": "بدء الدردشة", + "start-conversation": "بدء المحادثة", + "start-group-chat": "بدء الدردشة الجماعية", + "start-new-chat": "بدء دردشة جديده", + "starter-pack-coming": "حزمة المبتدئين قادمة في طريقك", + "starter-pack-coming-description": "يمكن أن يستغرق بضع دقائق إلى ساعات", + "starter-pack-received": "تلقى حزمة المبتدئين", + "starter-pack-received-description": "إليك بعض من العملة الرقمية لتبدأ! استخدمها للحصول على ملصقات ، واسم ENS و تجربت dapps", + "status": "Status", + "status-always-online": "دائما متصل", + "status-automatic": "تلقائي", + "status-automatic-subtitle": "تعيين الحالة تلقائيا", + "status-confirmed": "تم تأكيد", + "status-dnd": "عدم الإزعاج", + "status-dnd-subtitle": "كتم صوت كل الإشعارات", + "status-hardwallet": "Status المحفظة الصلبة", + "status-inactive": "غير نشط", + "status-inactive-subtitle": "يخفي حالتك على الإنترنت", + "status-is-open-source": "Status مفتوحة المصدر", + "status-keycard": "بطاقة مفتاح Status", + "status-mobile-descr": "Status تميل إلى استخدام الكثير من البيانات عند مزامنة الدردشات. يمكنك اختيار عدم المزامنة عند شبكة المحمول", + "status-not-sent-click": "غير مؤكد. انقر للحصول على خيارات", + "status-not-sent-tap": "غير مؤكد. اضغط للحصول على خيارات", + "status-pending": "قيد الانتظار", + "status-sent": "أرسلت", + "status-tx-not-found": "TX غير موجود", + "status-updates-descr": "ستظهر تحديثات Status هنا. أضف ملف التعريف كجهة اتصال لتلقي التحديثات على الجدول الزمني الخاص بك.", + "statuses-descr": "شارك ما يدور في ذهنك وابق على اطلاع دائم بجهات الاتصال الخاصة بك", + "statuses-my-status-descr": "شارك ما يدور في ذهنك. سيتمكن أي شخص يزور ملفك الشخصي من رؤية حالتك. سيتلقى الأشخاص الذين يضيفونك كجهة اتصال خاصة بهم تحديثاتك على مخططهم الزمني", + "step-i-of-n": "الخطوة {{step}} من {{number}}", + "sticker": "ملصق", + "sticker-market": "سوق الملصقات", + "storage": "التخزين", + "submit": "إرسال", + "submit-bug": "إرسال خطأ", + "success": "النجاح", + "suggested-min-tip": "اقترح الحد الأدنى .للإكرامية", + "suggested-price-limit": "حد السعر المقترح", + "swap": "مبادله", + "swap-details": "تفاصيل المبادلة", + "switch-to-simple-interface": "التبديل إلى واجهة بسيطة", + "symbol": "رمز", + "sync-all-devices": "مزامنة جميع الأجهزة", + "sync-in-progress": "المزامنة ...", + "sync-settings": "إعدادات المزامنة", + "sync-synced": "متزامنة", + "syncing-devices": "المزامنة ...", + "system": "نظام", + "tabs": "علامات التبويب", + "tag-was-lost": "فقدت العلامة", + "tap-card-again": "انقر على البطاقة في الجزء الخلفي من هاتفك مرة أخرى", + "terms-of-service": "شروط الاستخدام", + "test-networks": "اختبار الشبكات", + "text-input-disabled": "رجاءً انتظر لحظة...", + "thank-you": "شكرا لك", + "this-device": "هذا الجهاز", + "this-device-desc": "سيتم تشفير المفتاح الخاص بك وتخزينه بشكل آمن", + "this-is-you-signing": "هذه هي عبارة توقيعك", + "this-will-take-few-seconds": "هذا سيستغرق بضع ثوان", + "three-days": "ثلاثة ايام", + "three-words-description": "يجب أن تشاهد هذه الكلمات 3 قبل توقيع المعاملة", + "three-words-description-2": "إذا رأيت مجموعة تحرير وإدراج مختلفة، فقم بإلغاء المعاملة وتسجيل الخروج.", + "timeline": "الجدول الزمني", + "tip-cap": "أعلى الرسوم", + "to": "إلى", + "to-block": "حظر", + "to-enable-biometric": "لتمكين {{bio-type-label}} ، يجب حفظ كلمة مرورك على شاشة إلغاء القفل", + "to-encrypt-enter-password": "لتشفير الحساب الرجاء إدخال كلمه المرور", + "to-see-this-message": "لرؤية هذه الرسالة ،", + "token": "عملة رمزية", + "token-auto-validate-decimals-error": "اسم خاطئ {{symbol}} على العنوان {{address}} - اضبط على {{expected}} ولكن تم اكتشافه على أنه {{actual}}", + "token-auto-validate-name-error": "اسم خاطئ {{symbol}} على العنوان {{address}} - اضبط على {{expected}} ولكن تم اكتشافه على أنه {{actual}}", + "token-auto-validate-symbol-error": "اسم خاطئ {{symbol}} على العنوان {{address}} - اضبط على {{expected}} ولكن تم اكتشافه على أنه {{actual}}", + "token-details": "تفاصيل token", + "topic-name-error": "استخدم الأحرف الصغيرة فقط (من أ إلى ي) والأرقام والشرطات (-). لا تستخدم مفاتيح الدردشة", + "total-gas": "إجمالي الغاز", + "transaction": "المعاملات", + "transaction-data": "بيانات المعاملات", + "transaction-declined": "تم رفض الصفقة", + "transaction-description": "اعتبرها مكتملة بعد 12 تاكيد علي الشبكة.", + "transaction-details": "تفاصيل المعاملات", + "transaction-failed": "فشلت المعاملة", + "transaction-fee": "رسوم المعاملات", + "transaction-history": "سجل المعاملات", + "transaction-request": "طلب معاملة", + "transaction-sent": "تم إرسال المعاملة", + "transaction-signed": "تم توقيع المعاملة بنجاح", + "transactions": "المعاملات", + "transactions-filter-select-all": "تحديد الكل", + "transactions-filter-title": "سجل التصفية", + "transactions-history": "سجل المعاملات", + "transactions-history-empty": "لا توجد معاملات في تاريخك حتى الآن", + "transactions-history-loading": "تحميل سجل المعاملات. وهذا قد يستغرق بعض الوقت.", + "transactions-load-more": "التعرف على المزيد", + "transactions-management-enabled": "إدارة المعاملات (ألفا)", + "transactions-sign": "تسجيل", + "transfer-ma-unknown-error-desc-1": "يبدو أنه لم يتم حذف حساباتك المتعددة. ربما تمت إعادة تعيين قاعدة البيانات", + "transfer-ma-unknown-error-desc-2": "يرجى التحقق من قائمة حسابك وحاول مرة أخرى. إذا لم يكن الحساب مدرجًا ، فانتقل إلى الوصول إلى المفاتيح الموجودة للاسترداد باستخدام العبارة الأولية", + "transfers-fetching-failure": "تعذر تحديث محفوظات عمليات النقل. تحقق من الاتصال واسحب لأسفل للمحاولة مرة أخرى", + "tribute-required-by-multiaccount": "{{multiaccount-name}} يتطلب SNT لبدء الدردشة.", + "tribute-state-paid": "تحية مدفوعة", + "tribute-state-pending": "تحية جارية", + "tribute-state-required": "يتطلب {{snt-amount}} تحية SNT", + "tribute-to-talk": "تحية للحديث", + "tribute-to-talk-add-friends": "أضف الأصدقاء كجهة اتصال للسماح بالدردشات دون دفع الجزية.", + "tribute-to-talk-are-you-friends": "هل انتم اصدقاء؟", + "tribute-to-talk-ask-to-be-added": "اطلب إضافتك كجهة اتصال", + "tribute-to-talk-contact-received-your-tribute": " تلقي التحية الخاصة بك. يمكنك الآن الدردشة بشكل أمن مع بعضها البعض.", + "tribute-to-talk-desc": "أنت بحاجة إلى SNT للأشخاص الجدد لبدء الدردشة.", + "tribute-to-talk-disabled": "تحية للحديث", + "tribute-to-talk-disabled-note": "من الآن فصاعدًا ، يمكن للأشخاص الجدد بدء الدردشة معك دون إرسال SNT.", + "tribute-to-talk-enabled": "لديك تحيه للحديث ممكنة.", + "tribute-to-talk-finish-desc": "من الآن فصاعدا ، سوف تتلقي فقط دردشات من جهات الاتصال ، والناس الذين قامو بالدفع ", + "tribute-to-talk-learn-more-1": "وقتك والاهتمام هي أصولك الأكثر قيمة. يتيح لك برنامج تحية للحديث تعيين مبلغ SNT المطلوب للأشخاص الجدد لبدء محادثة معك.", + "tribute-to-talk-learn-more-2": "سيطلب من اي شخص ليس في قائمه الاتصال الخاصة بك الدفع ، ويمكنك الرد بمجرد ان يكون لديهم.", + "tribute-to-talk-learn-more-3": "يمكنك دائمًا إعادة الأموال ، ولكن للتأكد من أن الأصدقاء يمكنهم الوصول إليك بحرية ، يمكنك إضافتهم كجهة اتصال أولاً.", + "tribute-to-talk-paywall-learn-more-1": "وقتنا واهتمامنا هما أهم أصولنا. يتيح لك برنامج Tribute to Talk الاتصال بأشخاص جدد في مقابل الحصول على دفعة SNT.", + "tribute-to-talk-paywall-learn-more-2": "لبدء محادثة مع شخص لديه مجموعة تحية ، ما عليك سوى دفع SNT المطلوب وسيتم إضافتك كجهة اتصال.", + "tribute-to-talk-paywall-learn-more-3": "إذا كنت تعرفهم ، فيمكنك مشاركة ملفك الشخصي خارج Status لتتم إضافته مجانًا.", + "tribute-to-talk-pending": "تحية في انتظار التأكيد", + "tribute-to-talk-pending-note": "معاملة التحية في انتظار التأكيد على الشبكة. يمكنك التحقق من حالتها في سجل المعاملات", + "tribute-to-talk-removing-note": "ستتيح إزالة تحية للحديث للأشخاص الجدد بدء محادثة دون إرسال SNT. يتطلب إجراء الصفقة.", + "tribute-to-talk-set-snt-amount": "قم بتعيين مقدار SNT المطلوب للأشخاص الجدد لبدء الدردشة", + "tribute-to-talk-signing": "في انتظار توقيع المعاملة", + "tribute-to-talk-transaction-failed-note": "لقد فشلت المعاملة وتحية الحديث ولم يتم تتغير الاعدادات ", + "tribute-to-talk-tribute-received1": "تلقى تحية. انت و", + "tribute-to-talk-tribute-received2": " هي الآن اتصالات ويمكن الدردشة بشكل أمن مع بعضها البعض.", + "tribute-to-talk-you-require-snt": "أنت بحاجة إلى SNT للأشخاص الجدد لبدء الدردشة.", + "try-again": "حاول مرة أخري", + "try-keeping-the-card-still": "حاول إبقاء البطاقة ثابتة", + "turn-nfc-description": "تم تعطيل NFC على جهازك. يمكنك تمكينه في الإعدادات", + "turn-nfc-on": "قم بتشغيل NFC للمتابعة", + "two-minutes": "دقيقتان", + "tx-fail-description1": "هذه المعاملة من المحتمل أن تفشل. قم بالتسجيل على مسؤوليتك الخاصة باستخدام رسوم الشبكة المخصصة.", + "tx-fail-description2": "هذه المعاملة من المحتمل أن تفشل. تعيين رسوم شبكة مخصصة للتوقيع على مسؤوليتك الخاصة.", + "type": "نوع", + "type-a-message": "رسالة", + "ulc-enabled": "تمكين ULC", + "unable-to-fetch": "تعذر جلب سجل الدردشة", + "unable-to-read-this-code": "غير قادر على قراءة هذا الرمز", + "unable-to-send-messages": "تعذر إرسال الرسائل واستلامها", + "unblock": "رفع الحظر", + "unblock-contact": "إلغاء حظر هذا المستخدم", + "unknown-status-go-error": "خطأ في Status الانتقال غير معروف", + "unlimited": "غير محدود", + "unlock": "فتح", + "unmute": "إلغاء كتم الصوت", + "unpair-card": "إلغاء اقران بطاقة", + "unpair-card-confirmation": "ستقوم هذه العملية بإلغاء اقران البطاقة من الجهاز الحالي. يتطلب تخويل رمز مرور مكون من 6 أرقام. هل تريد المتابعة ؟", + "unpair-keycard": "قم بإلغاء إقران بطاقة المفاتيح من هذا الهاتف", + "unpair-keycard-warning": "رمز الإقران الخاص بك\/ PUK و PIN لم يتغير", + "unpaired-keycard-text": "إن Keycard التي قمت باستغلالها غير مرتبطة بهذا الهاتف", + "unpaired-keycard-title": "الاتصال مع البطاقة تم فقدانه", + "unpin": "فك", + "update": "تحديث", + "update-to-listen-audio": "قم بالتحديث إلى أحدث إصدار للاستماع إلى رسالة صوتية هنا!", + "update-to-see-image": "قم بالتحديث إلى أحدث إصدار لرؤية صورة جميلة هنا!", + "update-to-see-sticker": "قم بالتحديث إلى أحدث إصدار لرؤية ملصق لطيف هنا!", + "updates-to-tos": "تحديثات لشروط الاستخدام", + "updates-to-tos-desc": "قبل المتابعة، يرجى مراجعة شروط الاستخدام والتأكد من تحملك المسؤولية الكاملة عن كيفية استخدام التطبيق.", + "url": "URL", + "usd-currency": "دولار أمريكي", + "use-as-profile-picture": "استخدم كصورة للملف الشخصي", + "use-valid-contact-code": "الرجاء إدخال مفتاح دردشة صالح أو مسحه ضوئيًا", + "validation-amount-invalid-number": "المبلغ ليس رقمًا صالحًا", + "validation-amount-is-too-precise": "المبلغ دقيق جدا. الحد الأقصى لعدد الكسور العشرية هو {{decimals}} .", + "verified-community": "✓ مجتمع تم التحقق منه", + "version": "نسخة التطبيق", + "view": "عرض", + "view-cryptokitties": "عرض في CryptoKitties", + "view-cryptostrikers": "عرض في CryptoStrikers", + "view-data": "عرض البيانات", + "view-details": "عرض التفاصيل", + "view-etheremon": "عرض في Etheremon", + "view-gitcoin": "عرض في Gitcoin", + "view-on-opensea": "عرض على OpenSea", + "view-profile": "عرض الصفحة الشخصية", + "view-public-dashboard": "عرض لوحة المعلومات العامة", + "view-rules": "عرض القواعد", + "view-signing": "عرض عبارة التوقيع", + "view-superrare": "عرض في SuperRare", + "waiting-for-wifi": "لا توجد شبكة Wi-Fi ، تم تعطيل مزامنة الرسائل.", + "waiting-for-wifi-change": "الإعدادات", + "waiting-to-sign": "في انتظار توقيع المعاملة ...", + "waiting-wi-fi": "في انتظار شبكة واي فاي ...", + "waku-bloom-filter-mode": "وضع تصفية واكو بلوم", + "wakuv2-change-nodes": "هل أنت متأكد أنك تريد تغيير عقد Wakuv2؟", + "wakuv2-node-format": "\/ ip4 \/ {node-ip} \/ tcp \/ {port} \/ p2p \/ {id}", + "wakuv2-settings": "إعدادات Waku v2", + "wallet": "المحفظه", + "wallet-address": "عنوان المحفظة", + "wallet-asset": "الأصول", + "wallet-assets": "الأصول", + "wallet-backup-recovery-title": "النسخ الاحتياطي للعبارة الاولية الخاصة", + "wallet-choose-recipient": "اختيار المستلم", + "wallet-collectibles": "قابلة للجمع", + "wallet-connect": "الاتصال بالمحفظة", + "wallet-connect-2.0": "Wallet Connect 2.0", + "wallet-connect-app-connected": "متصل", + "wallet-connect-go-back": "ارجع إلى متصفحك أو تطبيق لامركزي", + "wallet-connect-proposal-description": "من خلال الاتصال ، فإنك تسمح {{name}} باسترداد عنوان حسابك وتمكين Web3", + "wallet-connect-proposal-title": "هل ترغب في الاتصال بمحفظتك", + "wallet-insufficient-funds": "رصيد غير كاف", + "wallet-insufficient-gas": "لا يكفي ETH للغاز", + "wallet-invalid-address": "عنوان خاطئ: \n {{data}}", + "wallet-invalid-address-checksum": "خطأ في العنوان: \n {{data}}", + "wallet-invalid-chain-id": "الشبكة غير متطابقة: \n {{data}} لكن السلسلة الحالية هي {{chain}}", + "wallet-key-content": "عنوان ست عشري مكون من ٦٤ حرفًا يعتمد على معيار الإيتييوم\" ويبدأ بـ 0x. أمام الجميع ، تتم مشاركة عنوان حسابك مع الآخرين عندما تريد تلقي الأموال. يُشار إليه أيضًا باسم \"عنوان الإيتييوم\" أو \"عنوان المحفظة\".", + "wallet-key-title": "عنوان الحساب", + "wallet-manage-accounts": "إدارة الحسابات", + "wallet-manage-app-connections": "إدارة اتصالات التطبيق", + "wallet-manage-assets": "إدارة الأصول", + "wallet-request": "طلب", + "wallet-send": "إرسال", + "wallet-send-min-units": "21000 وحدة على الأقل", + "wallet-send-min-wei": "الحد الأدنى wei 1 ", + "wallet-settings": "إعدادات المحفظة", + "wallet-total-value": "القيمة الإجمالية", + "wallet-transaction-total-fee": "مجموع الرسوم", + "wants-to-access-profile": "يريد الوصول إلى ملفك الشخصي", + "warning": "تحذير", + "warning-message": "عذرًا ، نحن نحد من إرسال عدة رسائل في تتابع سريع لمنع البريد العشوائي. يرجى المحاولة مرة أخرى في لحظة", + "warning-sending-to-contract-descr": "العنوان الذي أدخلته هو عقد ذكي ، وقد يؤدي إرسال الأموال إلى هذا العنوان إلى فقدان الأموال. للتفاعل مع التطبيق اللامركزي ، افتح DApp في متصفح Status DApp.", + "watch-only": "المشاهدة فقط", + "wc-brand-guide": "إرشادات حول استخدام العلامات التجارية مثل العلامات التجارية والشعارات", + "wc-disclaimer": "إخلاء المسؤولية (بما في ذلك مزودي الطرف الثالث) والضمانات والإصدارات القانونية", + "wc-dispute": "أحكام تسوية المنازعات", + "wc-how-to-use-status-app": "كيفية استخدام تطبيق Status بما في ذلك الخصوصية والأمان", + "wc-new-tos-based-on-principles-prefix": "تم تصميم شروط الاستخدام الجديدة بناءً على", + "web-view-error": "غير قادر على تحميل الصفحة", + "websites": "مواقع الويب", + "webview-camera-permission-requests": "طلبات إذن كاميرا Webview", + "webview-camera-permission-requests-subtitle": "عند التمكين ، يمكن أن تطلب مواقع الويب و التطبيقات الامركزية استخدام الكاميرا", + "welcome-blank-community-message": "ستظهر مجتمعاتك هنا.", + "welcome-blank-message": "ستظهر دردشاتك هنا. لبدء دردشات جديده اضغط علي زر ⊕", + "welcome-community-blank-message": "ستظهر قنواتك هنا. لإنشاء قناة جديدة، انقر فوق الزر ⊕ وحدد \"إنشاء قناة\"", + "welcome-community-blank-message-edit-chats": "ستظهر قنواتك هنا. لإنشاء قناة جديدة، ارجع إلى شاشة المجتمع، وانقر على الزر ⊕ وحدد \"إنشاء قناة\"", + "welcome-screen-text": "انشاء محفظتك ، دعوة الأصدقاء للدردشة\nو استعرض شعبية dapps!", + "welcome-to-status": "مرحبًا بك في Status!", + "welcome-to-status-description": "قم بإعداد محفظة التشفير الخاصة بك ، ودعوة الأصدقاء للدردشة وتصفح التطبيقات اللامركزية", + "what-changed": "ما الذي تغير", + "what-is-shared": "ما هو مشترك", + "whats-on-your-mind": "ما الذي يدور في ذهنك...", + "word-count": "عدد الكلمات", + "word-n": "كلمة # {{number}}", + "word-n-description": "للتحقق مما إذا كنت قد قمت بنسخ نسخة احتياطية من العبارة الأولية بشكل صحيح ، أدخل الكلمة # {{number}} أعلاه.", + "words-n": { + "zero": "", + "one": "كلمة", + "two": "", + "few": "", + "many": "", + "other": "كلمات" + }, + "write-down-and-store-securely": "اكتب الرموز لأسفل \n وتخزينها بشكل آمن", + "wrong-address": "عنوان خاطئ", + "wrong-card": "بطاقة خاطئة", + "wrong-card-text": "البطاقة التي تم النقر عليها لا تتوافق مع المفاتيح التي حددتها", + "wrong-contract": "عقد خاطئ", + "wrong-keycard-text": "بطاقة المفاتيح التي قمت باستغلالها غير مرتبطة بهذا الهاتف", + "wrong-keycard-title": "يبدو انك قمت باستغلال \nبطاقة مفتاح خاطئه", + "wrong-password": "كلمة مرور خاطئة", + "wrong-word": "كلمة خاطئة", + "yes": "نعم", + "you": "أنت", + "you-already-have-an-asset": "لديك بالفعل أصل {{value}}", + "you-are-all-set": "أنتم جميعا مجموعه!", + "you-are-all-set-description": "إذا فقدت هاتفك ، يمكنك الآن الوصول إلى أموالك ومفتاح الدردشة باستخدام العبارة الأولية", + "you-can-change-account": "يمكنك تغيير اسم الحساب واللون إلى ما يحلو لك", + "you-can-choose-preview-websites": "يمكنك اختيار أي من مواقع الويب التالية يمكنه معاينة ارتباط الأوصاف والصور في الدردشات", + "you-can-fetch": "يمكنك إحضار تاريخ الدردشة", + "you-dont-have-contacts": "ليس لديك أي اتصالات بعد.", + "you-dont-have-contacts-invite-friends": "ليس لديك أي جهات اتصال حتى الآن.\n ادعُ أصدقائك لبدء الدردشة.", + "you-dont-have-stickers": "ليس لديك أي ملصقات بعد", + "you-will-need-this-code": "ستحتاج هذا الرمز لفتح الحالة وتوقيع المعاملات", + "you-will-start-from-scratch": "ستبدأ من نقطة الصفر بمجموعة جديدة من المفاتيح", + "your-card-is-frozen": "تم تجميد بطاقة المفتاح الخاص بك. إعادة تعيين الوصول إلى البطاقة", + "your-contact-code": "يسمح لـ DApp إمكانية استرداد مفتاح الدردشة الخاص بك", + "your-data-belongs-to-you": "إذا فقدت العبارة الأولية الخاصة بك ، فستفقد بياناتك وأموالك", + "your-data-belongs-to-you-description": "إذا فقدت إمكانية الوصول ، على سبيل المثال عن طريق فقد هاتفك ، فلا يمكنك الوصول إلى مفاتيحك إلا باستخدام العبارة الأولية. لا أحد ، ولكن لديك العبارة الأولية. اكتبه. ابقها آمنة", + "your-keys": "مفاتيحك", + "your-price-limit": "حد سعرك", + "your-recovery-phrase": "العبارة الأولية الخاصة بك", + "your-recovery-phrase-description": "هذه هي العبارة الأولية الخاصة بك. يمكنك استخدامها لإثبات أن هذه هي محفظتك. يمكنك فقط أن ترى ذلك مرة واحدة! اكتبها على الورق واحتفظ بها في مكان آمن. ستحتاج إليها إذا فقدت أو أعدت تثبيت محفظتك.", + "your-tip-limit": "حد الإكرامية الخاص بك", + "youre-on-mobile-network": "أنت متصل بشبكة الجوال" +} diff --git a/translations/bel.json b/translations/bel.json new file mode 100644 index 00000000000..9f5fc33b493 --- /dev/null +++ b/translations/bel.json @@ -0,0 +1,106 @@ +{ + "amount": "Сума", + "members-active": { + "one": "1 член, 1 актыўны", + "other": "{{count}} члена(ау), {{count}} актыўных", + "zero": "няма членау" + }, + "chat-name": "Імя чата", + "phew-here-is-your-passphrase": [ + "str", + "*Уф*, эгэта было няпроста, вось ваша парольная фраза, *запішыце яе і захавайце ў надзейным месцы!* ", + "Яна будзе патрэбна вам для аднаўлення акаўнта." + ], + "chat-settings": "Налады чата", + "offline": "Оффлайн", + "invited": "запрошаны(на)", + "address": "Адрас", + "datetime-hour": { + "one": "гадзіна", + "other": "гадзін" + }, + "remove": "Выдаліць", + "add-members": "Дадаць членаў", + "done": "Гатова", + "new-group-chat": "Новы групавы чат", + "datetime-yesterday": "учора", + "datetime-ago": "назад", + "contacts": "Кантакты", + "active-online": "У сетцы", + "password": "Пароль", + "discover": "Пошук", + "intro-status": "Пагутарыце са мной у чаце, каб наладзіць свой рахунак і змяніць свае налады!", + "name": "Імя", + "show-qr": "Паказаць QR", + "connect": "Падлучыцца", + "edit": "Змяніць", + "account-generation-message": "Секундачку, мне трэба выканаць вар'яцка складаныя разлікі для стварэння вашага акаўнта!", + "no-messages": "Няма паведамленняў", + "passphrase": "Парольная фраза", + "recipient": "Атрымальнік", + "members-title": "Удзельнікі", + "new-group": "Новая група", + "settings": "Налады", + "chats": "Чаты", + "image-source-make-photo": "Сфатаграфаваць", + "save": "Захаваць", + "sharing-copy-to-clipboard": "Скапіяваць", + "sync-in-progress": "Сінхранізацыя...", + "incorrect-code": [ + "str", + "Выбачайце, код няправільны, увядзіце яшчэ раз" + ], + "image-source-gallery": "Выбраць з галерэі", + "sync-synced": "Сінхранізуецца", + "status-pending": "у чаканні", + "datetime-day": { + "one": "дзень", + "other": "дні" + }, + "scan-qr": "Сканаваць QR", + "next": "Працягнуць", + "recent": "Апошнія", + "status": "Статус", + "wrong-password": "Няправільны пароль", + "sharing-share": "Падзяліцца...", + "clear-history": "Ачысціць гісторыю", + "no-contacts": "Пакуль няма кантактаў", + "datetime-today": "сёння", + "web-view-error": "ой, памылка", + "cancel": "Адмена", + "can-not-add-yourself": "Вы не можаце дадаць сябе", + "add-to-contacts": "Дадаць у кантакты", + "available": "Даступна", + "You": "Вы", + "main-wallet": "Асноўны кашалёк", + "members": { + "one": "1 член", + "other": "{{count}} члена(ау)", + "zero": "няма членов" + }, + "intro-message1": "Сардэчна запрашаем у Статус\nНацісніце на гэтае паведамленне, каб усталяваць пароль і пачаць!", + "new-contact": "Новы кантакт", + "datetime-second": { + "one": "секунда", + "other": "секунды" + }, + "recover": "Аднавіць", + "datetime-minute": { + "one": "хвіліна", + "other": "хвіліны" + }, + "active-unknown": "Невядома", + "public-key": "Публічны ключ", + "profile": "Профіль", + "none": "Няма", + "removed": "выдалены(на)", + "message": "Паведамленне", + "here-is-your-passphrase": [ + "str", + "Вось ваша парольная фраза, *запішыце яе і захавайце ў надзейным месцы!* ", + "Яна будзе патрэбна вам для аднаўлення акаўнта." + ], + "image-source-title": "Mалюнак профілю", + "status-sent": "Адправіць", + "left": "засталося" +} diff --git a/translations/bn.json b/translations/bn.json new file mode 100644 index 00000000000..54f6f93347b --- /dev/null +++ b/translations/bn.json @@ -0,0 +1,1736 @@ +{ + "You": "আপনি", + "about-app": "সম্বন্ধে", + "about-key-storage-content": "Status কখনই আপনার Private key অ্যাক্সেস করবে না। আপনার বীজ বাক্যাংশ ব্যাকআপ করতে নিশ্চিত হোন। আপনি যদি আপনার ফোন হারিয়ে ফেলেন তাহলে এটি আপনার চাবি অ্যাক্সেস করার একমাত্র উপায়।", + "about-key-storage-title": "কী স্টোরেজ সম্পর্কে", + "about-names-content": "কেউ তোমার ভান করতে পারবে না! আপনি ডিফল্টভাবে বেনামী এবং কখনোই আপনার আসল নাম প্রকাশ করতে হবে না। আপনি একটি ছোট ফি জন্য একটি কাস্টম নাম নিবন্ধন করতে পারেন।", + "about-names-title": "নাম পরিবর্তন করা যাবে না", + "about-sharing-data": "ডেটা শেয়ার করার বিষয়ে", + "accept": "গ্রহণ করুন", + "accept-and-add": "গ্রহণ করুন এবং যোগ করুন", + "accept-and-continue": "গ্রহণ করুন এবং চালিয়ে যান", + "accept-and-share-address": "ঠিকানা গ্রহণ এবং শেয়ার করুন", + "accept-new-chats-from": "গ্রহণ করুন নতুন চ্যাট থেকে", + "accept-status-tos-prefix": "আমি স্ট্যাটাস গ্রহণ করি", + "access-existing-keys": "বিদ্যমান কীসমূহ অ্যাক্সেস করুন", + "access-key": "অ্যাক্সেস কী", + "account-added": "অ্যাকাউন্ট যুক্ত হয়েছে", + "account-color": "অ্যাকাউন্টের রং", + "account-content": "আপনি Status একাউন্টকে ব্যাংক একাউন্টের সাথে তুলনা করতে পারেন। ব্যাংক একাউন্টের মত, একটি অ্যাকাউন্টে সাধারণত একটি ঠিকানা এবং ব্যালেন্স থাকে আপনি এই একাউন্ট ব্যবহার করে Ethereum লেনদেন করেন। আপনার মানিব্যাগে একাধিক অ্যাকাউন্ট থাকতে পারে। Status আনলক করে সব অ্যাক্সেস করা হয়েছে।", + "account-exists-title": "একাউন্ট ইতোমধ্যে বিদ্যমান", + "account-is-used": "ব্রাউজারে ড্যাপের সাথে অ্যাকাউন্টটি ব্যবহার করা হচ্ছে।", + "account-name": "অ্যাকাউন্টের নাম", + "account-settings": "অ্যাকাউন্ট সেটিংস", + "account-title": "একাউন্ট", + "accounts": "হিসাব", + "actions": "ক্রিয়া", + "active-online": "অনলাইন", + "active-unknown": "অজানা", + "activity": "কার্যকলাপ", + "add": "যুক্ত", + "add-a-watch-account": "কেবলমাত্র দেখার জন্য ঠিকানা যুক্ত করুন", + "add-account": "একটি অ্যাকাউন্ট যুক্ত করুন", + "add-account-description": "আপনি আপনার স্ট্যাটাস ওয়ালেটে এটি যোগ করতে যেকোনো ধরনের ইথেরিয়াম অ্যাকাউন্ট আমদানি করতে পারেন", + "add-account-incorrect-password": "পাসওয়ার্ডটি ভুল বলে মনে হচ্ছে। অ্যাপ্লিকেশনটি আনলক করতে আপনি যে পাসওয়ার্ডটি ব্যবহার করেন তা প্রবেশ করুন।", + "add-an-account": "একটি অ্যাকাউন্ট যুক্ত করুন", + "add-bootnode": "বুটনোড যুক্ত করুন", + "add-contact": "পরিচিতি যুক্ত করুন", + "add-custom-token": "কাস্টম টোকেন যুক্ত করুন", + "add-favourite": "প্রিয় বিষয় যোগ করুন", + "add-mailserver": "স্ট্যাটাস নোড যুক্ত করুন", + "add-members": "সদস্য যুক্ত করুন", + "add-network": "নেটওয়ার্ক যুক্ত করুন", + "add-new-contact": "নতুন পরিচিতি যুক্ত করুন", + "add-nickname": "একটি ডাকনাম যুক্ত করুন (ঐচ্ছিক)", + "add-node": "নোড যোগ করুন", + "add-private-key-account": "ব্যক্তিগত কী থেকে অ্যাকাউন্ট যুক্ত করুন", + "add-seed-account": "একটি Seed phrase সহ অ্যাকাউন্ট যুক্ত করুন", + "add-to-contacts": "পরিচিতিগুলিতে যুক্ত করুন", + "add-to-contacts-text": "আপনার যোগাযোগ তালিকায় একজন ব্যবহারকারী যুক্ত করে, আপনি আপনার মানিব্যাগ ঠিকানা শেয়ার করুন", + "add-to-favourites": "পছন্দনীয়তে যুক্ত করুন", + "add-watch-account": "কেবলমাত্র দেখার জন্য অ্যাকাউন্ট যুক্ত করুন", + "address": "ঠিকানা", + "address-or-ens-name": "ঠিকানা বা ইএনএস নাম", + "address-received": "ঠিকানা গৃহীত হয়েছে", + "address-request-accepted": "ঠিকানা অনুরোধ গৃহীত", + "address-requested": "ঠিকানা অনুরোধ করা হয়েছে", + "advanced": "উন্নত", + "advanced-settings": "উন্নত সেটিংস", + "advertiser-description": "আপনি স্ট্যাটাস আবিষ্কার করেছেন একজন সঙ্গীকে ধন্যবাদ।স্ট্যাটাস যদি আপনার আইপি ঠিকানা একবার পরীক্ষা করে তবে তারা পুরস্কৃত হয় তাতে আপনার কি আপত্তি আছে? এই তথ্য অন্য কোন কিছুর জন্য ব্যবহার করা হবে না এবং ৭ দিন পর এটি সম্পূর্ণরূপে মুছে ফেলা হবে।", + "advertiser-starter-pack-accept": "গ্রহণ করুন", + "advertiser-starter-pack-decline": "প্রত্যাখ্যান করুন", + "advertiser-starter-pack-description": "এখানে আপনাকে শুরু করার জন্য কিছু ক্রিপ্টো আছে! স্টিকার, একটি ENS নাম পেতে এটি ব্যবহার করুন এবং DApps চেষ্টা করুন।", + "advertiser-starter-pack-title": "স্টার্টার প্যাক", + "advertiser-title": "ডিফল্টভাবে গোপনীয়তা", + "agree-by-continuing": "আপনি কি একমত আমাদের সাথে?", + "all": "সব", + "allow": "অনুমতি দিন", + "allow-and-send": "অনুমতি দিন এবং পাঠান", + "allow-mention-notifications": "উল্লেখ@ দেখান", + "allowing-authorizes-this-dapp": "আপনার ওয়ালেট ঠিকানা পুনরুদ্ধার এবং Web3 সক্রিয় করতে এই DApp অনুমোদন দেন", + "already-have-asset": "আপনার এই সম্পদ ইতিমধ্যেই আছে", + "amount": "পরিমান", + "anyone": "যে কেউ", + "app-commit": "অ্যাপ কমিট", + "appearance": "উপস্থিতি", + "apply": "প্রয়োগ করুন", + "approve": "অনুমোদন করুন", + "approve-limit": "সীমা অনুমোদন করুন", + "approve-token": "টোকেন অনুমোদন করুন", + "approve-token-contract-desc": "একটি চুক্তির সাথে একটি টোকেন অনুমোদন করা এটি আপনার টোকেন ব্যালেন্স ব্যয় করতে দেয়। আপনি যদি মনে করেন যে কোনও প্রকল্প অবিশ্বস্ত, তবে তাদের সাথে টোকেনটি অনুমোদন করবেন না, বা কেবলমাত্র আপনি তাদের সাথে যে পরিমাণ অর্থ ব্যবহার করবেন তা অনুমোদন করবেন না।", + "are-you-sure": "আপনি কি নিশ্চিত?", + "are-you-sure-description": "আপনি পুরো বীজ বাক্যাংশটি আবার দেখতে পাবেন না", + "are-you-sure-to-cancel": "আপনি কি নিশ্চিত যে আপনি বাতিল করতে চান?", + "are-you-sure?": "আপনি কি নিশ্চিত?", + "ask-in-status": "একটি প্রশ্ন জিজ্ঞাসা করুন অথবা একটি বাগ প্রতিবেদন করুন", + "at": "at", + "attribution-received": "{{সর্বোচ্চ}} বোনাস প্রাপ্ত বোনাসগুলির মধ্যে {{আট্রিব}}", + "audio": "অডিও", + "audio-recorder": "রেকর্ডার", + "audio-recorder-error": "রেকর্ডার ত্রুটি", + "audio-recorder-max-ms-reached": "সর্বোচ্চ রেকর্ডিং সময় পৌঁছেছে", + "audio-recorder-permissions-error": "অডিও বার্তা প্রেরণের জন্য আপনাকে অনুমতি দিতে হবে", + "authorize": "অনুমোদন করুন", + "available": "উপলভ্য", + "available-participants": { + "one": "আপনি আরও একটি অংশগ্রহণকারী নির্বাচন করতে পারেন", + "other": "আপনি আরও অংশগ্রহণকারী {{count}}নির্বাচন করতে পারেন" + }, + "back": "পেছনে", + "back-up": "ব্যাক আপ", + "back-up-seed-phrase": "বীজ বাক্যাংশ ব্যাক আপ করুন", + "back-up-your-seed-phrase": "আপনার বীজ বাক্যাংশ ব্যাক আপ করুন", + "backing-up": "ব্যাক আপ করা হচ্ছে...", + "backup-disabled": "অক্ষম", + "backup-enabled": "সক্রিয়", + "backup-recovery-phrase": "বীজ বাক্যাংশ ব্যাক আপ করুন", + "backup-settings": "ব্যাকআপ সেটিংস", + "backup-through-waku": "Waku এর মাধ্যমে ব্যাকআপ", + "bad-fees-description": "আপনার অগ্রাধিকার ফি আমাদের প্রস্তাবিত প্যারামিটারের নিচে।", + "balance": "ভারসাম্য", + "begin-set-up": "উপস্থাপনা শুরু করুন", + "below-base-fee": "বেস ফি এর নিচে সর্বোচ্চ ফি", + "biometric-auth-android-sensor-desc": "স্পর্শ সেন্সর", + "biometric-auth-android-sensor-error-desc": "ব্যর্থ", + "biometric-auth-android-title": "প্রমাণীকরণ প্রয়োজন", + "biometric-auth-confirm-logout": "পুনরায় লগ-ইন করুন", + "biometric-auth-confirm-message": "বায়োমেট্রিক প্রমাণীকরণ অব্যাহত রাখা আবশ্যক, যদি সম্ভব না হয় অনুগ্রহ করে আপনার পাসওয়ার্ড বা পাসকোড দিয়ে আপনার কী আনলক করুন", + "biometric-auth-confirm-title": "আপনাকে অবশ্যই প্রমাণীকরণ করতে হবে!", + "biometric-auth-confirm-try-again": "আবার চেষ্টা করুন", + "biometric-auth-error": "বায়োমেট্রিক প্রমাণীকরণ সম্পাদন করতে অক্ষম ({{code}})", + "biometric-auth-login-error-title": "বায়োমেট্রিক প্রমাণীকরণ ত্রুটি", + "biometric-auth-login-ios-fallback-label": "পাসওয়ার্ড লিখুন", + "biometric-auth-reason-login": "Status খুলুন", + "biometric-auth-reason-verify": "প্রমাণীকরণ যাচাই করুন", + "biometric-disable-bioauth": "{{bio-type-label}} নিষ্ক্রিয় করুন", + "biometric-disable-password-description": "আপনি যদি এটি অক্ষম করেন তবে আপনি এটিও করবেন", + "biometric-disable-password-title": "পাসওয়ার্ড সংরক্ষণ নিষ্ক্রিয় করুন", + "biometric-enable": "আপনি যদি অ্যাপটি অ্যাক্সেস করতে প্রতিবার আপনার পাসওয়ার্ড প্রবেশ করাতে না চান, {{bio-type-label}} সাইন ইন সক্রিয় করুন", + "biometric-enable-button": "{{bio-type-label}} সক্রিয় করুন", + "biometric-enable-keycard": "আপনি যদি অ্যাপটি অ্যাক্সেস করতে প্রতিবার আপনার কীকার্ড ব্যবহার করতে না চান, {{bio-type-label}} সাইন ইন সক্রিয় করুন", + "biometric-faceid": "ফেস আইডি", + "biometric-fingerprint": "আঙ্গুলের ছাপ", + "biometric-secure-with": "{{bio-type-label}} দিয়ে নিরাপদ", + "biometric-touchid": "টাচ আইডি", + "bip39-password-placeholder": "BIP39 পাসওয়ার্ড", + "blank-keycard-text": "আপনি আপনার কী এবং নাম তৈরি করার পর আপনি আপনার কীকার্ড দিয়ে অগ্রসর হতে পারেন", + "blank-keycard-title": "দেখে মনে হচ্ছে আপনি টেপ করেছেন\n একটি ফাঁকা কিকার্ড", + "block": "ব্লক", + "block-contact": "এই ব্যবহারকারীকে অবরুদ্ধ করুন", + "block-contact-details": "অবরোধ এই ব্যবহারকারীর পূর্ববর্তী বার্তাগুলি মুছে ফেলবে এবং নতুন বার্তাগুলি আপনার কাছে পৌঁছানো থেকে বিরত রাখবে", + "blocked-users": "অবরুদ্ধ ব্যবহারকারী", + "bootnode-address": "বুটনোড ঠিকানা", + "bootnode-details": "বুটনোড বিস্তারিত", + "bootnode-format": "এনোড: \/\/ {enode-id} @ {ip-address} : {port}", + "bootnodes": "বুটনোডস", + "bootnodes-enabled": "bootnodes সক্রিয় করা হয়েছে", + "bootnodes-settings": "bootnodes সেটিংস", + "browsed-websites": "ব্রাউজারের ইতিহাস এখানে প্রদর্শিত হবে", + "browser": "ব্রাউজার", + "browser-not-secure": "সংযোগ নিরাপদ নয়! লেনদেনে স্বাক্ষর করবেন না বা এই সাইটে ব্যক্তিগত ডেটা প্রেরণ করবেন না।", + "browser-secure": "সংযোগটি সুরক্ষিত। লেনদেনগুলি স্বাক্ষর করার আগে বা ব্যক্তিগত ডেটা প্রবেশের আগে নিশ্চিত হয়ে নিন যে আপনি এই সাইটে সত্যই বিশ্বাস করছেন।", + "browsers": "ব্রাউজারগুলি", + "browsing-cancel": "বাতিল", + "browsing-open-in-android-web-browser": "অ্যান্ড্রয়েডে খুলুন", + "browsing-open-in-ios-web-browser": "iOS-এ খুলুন", + "browsing-open-in-status": "Status খুলুন", + "browsing-site-blocked-description1": "আমরা এই ঠিকানা থেকে সম্ভাব্য বিদ্বেষপূর্ণ কার্যকলাপ সনাক্ত করেছি। আপনাকে এবং আপনার মানিব্যাগ রক্ষা করতে, আমরা আরো নেভিগেশন প্রতিরোধ করছি। আপনি যদি মনে করেন এটি একটি ত্রুটি, তাহলে আমাদের জানান।", + "browsing-site-blocked-description2": "পাবলিক চ্যাট।", + "browsing-site-blocked-go-back": "ফিরে যাও", + "browsing-site-blocked-title": "এই সাইটটি অবরুদ্ধ করা হয়েছে", + "browsing-title": "ব্রাউজ করুন", + "bug-report": "একটি বাগ রিপোর্ট করুন", + "bug-report-description": "* বর্ণনা", + "bug-report-description-placeholder": "আবশ্যক, খালি হতে পারে না", + "bug-report-steps": "ধাপ পুনর্গঠন কর", + "bug-report-steps-placeholder": "- অ্যাপ খুলুন\n- কিছু একটা করুন\n- এবং তারপর অন্য কিছু...", + "bug-report-submit-email": "লগ আর্কাইভ সহ ইমেলের মাধ্যমে জমা দিন", + "bug-report-submit-gh-issue": "লগ ছাড়া একটি GitHub সমস্যা জমা দিন", + "bug-report-too-short-description": "বর্ণনা খুবই ছোট", + "build-yourself": "এই ব্যবহারের শর্তাবলী ছাড়া অ্যাপটি ব্যবহার করতে, আপনি আপনার নিজস্ব সংস্করণ তৈরি করতে পারেন", + "buy-crypto": "ক্রিপ্টো কিনুন", + "buy-crypto-choose-a-service": "ক্রিপ্টো কেনার জন্য আপনি যে পরিষেবাটি ব্যবহার করতে চান তা চয়ন করুন", + "buy-crypto-description": "ক্রিপ্টো কিনতে এখনই একটি ড্যাপ খুঁজুন", + "buy-crypto-leaving": "আপনি স্ট্যাটাস ছেড়ে চলে যাচ্ছেন এবং আপনার ক্রয় সম্পন্ন করতে একটি তৃতীয় পক্ষের ওয়েবসাইটে প্রবেশ করছেন", + "buy-crypto-title": "মনে হচ্ছে আপনার খালি", + "camera-access-error": "প্রয়োজনীয় ক্যামেরা অনুমতি দিতে, অনুগ্রহ করে আপনার সিস্টেম সেটিংসে যান এবং নিশ্চিত করুন যে Status > ক্যামেরা নির্বাচন করা হয়েছে।", + "can-not-add-yourself": "এটি আপনি, চ্যাট শুরু করতে অন্য কাউকে বেছে নিন", + "can-send-messages": "আপনি নতুন বার্তা পাঠাতে এবং গ্রহণ করতে পারেন", + "cancel": "বাতিল", + "cancel-keycard-setup": "কী-কার্ড উপস্থাপনা বাতিল করুন", + "cancelling": "বাতিল করা হচ্ছে", + "cannot-read-card": "কার্ড পড়তে পারছি না।\nঅনুগ্রহ করে এটি আপনার ফোনের পিছনে ধরে রাখুন", + "cannot-use-default-pin": "পাসকোড 000000 অনুমোদিত নয়।\n দয়া করে অন্য নম্বরটি ব্যবহার করুন", + "cant-open-public-chat": "পাবলিক চ্যাট খোলা যাবে না", + "cant-report-bug": "একটি বাগ প্রতিবেদন করা যাবে না", + "card-is-blank": "এই কার্ডটি ফাঁকা", + "card-reseted": "কার্ড পুনরায় সেট করা হয়েছে", + "card-unpaired": "বর্তমান ডিভাইস থেকে কার্ড আনপেয়ার করা হয়েছে", + "category": "বিভাগ", + "category-title": "বিভাগের শিরোনাম", + "change-fleet": "{{fleet}} এ fleet পরিবর্তন করুন", + "change-logging-enabled": "আপনি কি নিশ্চিত যে আপনি {{enable}} লগিং করতে চান?", + "change-pairing": "পেয়ারিং কোড পরিবর্তন করুন", + "change-pairing-description": "পেয়ারিং কোড পরিবর্তন করা বর্তমান পেয়ারিংগুলিকে প্রভাবিত করে না। যাইহোক, যে কোন নতুন পেয়ারিং এর জন্য নতুন কোডের প্রয়োজন হবে।", + "change-pairing-title": "একটি নতুন পেয়ারিং কোড তৈরি করুন", + "change-passcode": "পাসকোড পরিবর্তন করুন", + "change-password": "পাসওয়ার্ড পরিবর্তন করুন", + "change-pin": "৬-সংখ্যার পাসকোড পরিবর্তন করুন", + "change-puk": "12-সংখ্যার PUK পরিবর্তন করুন", + "change-tip": "টিপ পরিবর্তন করুন", + "changed-amount-warning": "{{old}} থেকে {{new}} পর্যন্ত পরিমাণ পরিবর্তন করা হয়েছে", + "changed-asset-warning": "সম্পদ {{old}} থেকে {{new}} পরিবর্তন করা হয়েছে", + "chaos-mode": "বিশৃঙ্খলা মোড", + "chaos-unicorn-day": "বিশৃঙ্খলা ইউনিকর্ন দিবস", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "আলাপ", + "chat-and-transact": "বন্ধুদের সাথে গোপনে চ্যাট করুন এবং লেনদেন করুন", + "chat-is-a-contact": "যোগাযোগ", + "chat-is-not-a-contact": "যোগাযোগ নয়", + "chat-key": "চ্যাট কী", + "chat-key-content": "Status চ্যাট প্রোটোকলের বার্তাগুলি এনক্রিপশন কী ব্যবহার করে প্রেরণ এবং প্রাপ্ত করা হয়। সর্বজনীন চ্যাট কী এমন অক্ষরের একটি স্ট্রিং যা আপনি অন্যদের সাথে ভাগ করেন যাতে তারা আপনাকে Status বার্তা প্রেরণ করতে পারে।", + "chat-key-title": "চ্যাট কী", + "chat-link-previews": "চ্যাট লিঙ্ক পূর্বরূপ", + "chat-name": "চ্যাট নাম", + "chat-name-content": "তিনটি এলোমেলো শব্দ, আপনার চ্যাট কী থেকে অ্যালগরিদমে প্রাপ্ত এবং চ্যাটে আপনার ডিফল্ট ছদ্মনাম হিসেবে ব্যবহৃত হয়। চ্যাটের নাম সম্পূর্ণ অনন্য; অন্য কোন ব্যবহারকারীর একই তিনটি শব্দ থাকতে পারে না।", + "chat-name-title": "চ্যাট নাম", + "chat-notification-preferences": "বিজ্ঞপ্তি সেটিংস", + "chat-settings": "চ্যাট সেটিংস", + "chats": "আড্ডা", + "check-on-block-explorer": "ব্লক এক্সপ্লোরার চেক করুন", + "check-on-opensea": "ওপেনসিয়া পরীক্ষা করুন", + "check-your-recovery-phrase": "আপনার seed phrase পরীক্ষা করুন", + "choose-actions": "ক্রিয়াগুলি চয়ন করুন", + "choose-authentication-method": "একটি প্রমাণীকরণ পদ্ধতি চয়ন করুন", + "choose-new-location-for-keystore": "আপনার কীস্টোর ফাইল সংরক্ষণ করতে একটি নতুন অবস্থান চয়ন করুন", + "choose-storage": "স্টোরেজ বেছে নিন", + "clear": "পরিষ্কার", + "clear-all": "সব পরিষ্কার করে দাও", + "clear-history": "ইতিহাস পরিষ্কার করুন", + "clear-history-action": "পরিষ্কার", + "clear-history-confirmation": "ইতিহাস সাফ করবেন?", + "clear-history-confirmation-content": "আপনি কি নিশ্চিত যে আপনি এই চ্যাটের ইতিহাস মুছে ফেলতে চান?", + "clear-history-title": "ইতিহাস সাফ করবেন?", + "cleared-chat-description-public": "এটা এখানে শান্ত হয়েছে. কথোপকথন শুরু করুন বা", + "close": "বন্ধ", + "close-all": "সব বন্ধ করুন", + "close-app-button": "নিশ্চিত", + "close-app-content": "অ্যাপটি বন্ধ হয়ে যাবে এবং বন্ধ হবে। আপনি যখন এটি পুনরায় খুলবেন, নির্বাচিত নেটওয়ার্কটি ব্যবহৃত হবে", + "close-app-title": "সতর্কীকরণ!", + "collectibles-leak-metadata": "আপনি এখানে আপনার NFT প্রদর্শন করতে পারেন। যদি আপনি তা করেন, আপনি আপনার ওয়ালেট এবং আইপি ঠিকানা শেয়ার করবেন", + "command-button-send": "প্রেরণ", + "communities": "কমিউনিটি", + "communities-alpha": "সম্প্রদায় (আলফা)", + "communities-enabled": "সম্প্রদায় সক্ষম", + "communities-verified": "✓ যাচাইকৃত স্থিতি সম্প্রদায়", + "community": "সম্প্রদায়", + "community-color": "কমিউনিটির রঙ", + "community-color-placeholder": "একটি রঙ বেছে নিন", + "community-edit-title": "সম্প্রদায় সম্পাদনা করুন", + "community-emoji-thumbnail-title": "থাম্বনেইল", + "community-image-pick": "একটি চিত্র বাছাই করুন", + "community-image-remove": "অপসারণ", + "community-image-take": "একটি ছবি তুলুন", + "community-info": "কমিউনিটি তথ্য", + "community-info-not-found": "সম্প্রদায়ের তথ্য পাওয়া যায়নি", + "community-invite-title": "আমন্ত্রণ", + "community-key": "কমিউনিটি প্রাইভেট কী", + "community-key-placeholder": "আপনার সম্প্রদায়ের ব্যক্তিগত কী লিখুন", + "community-link": "কমিউনিটি লিঙ্ক", + "community-members": { + "one": "সদস্য", + "other": "সদস্যরা" + }, + "community-members-title": "সদস্যরা", + "community-message-preview": "{{community-name}} যোগ দেওয়ার আমন্ত্রণ", + "community-private-key": "কমিউনিটি প্রাইভেট কী", + "community-requests-to-join-title": "সদস্যপদের অনুরোধ", + "community-roles": "ভূমিকা", + "community-share-title": "ভাগ করুন", + "community-thumbnail-image": "থাম্বনেইল ছবি", + "community-thumbnail-upload": "আপলোড", + "complete-hardwallet-setup": "এই কার্ডটি এখন সংযুক্ত। আপনার লেনদেনগুলি স্বাক্ষর করতে এবং আপনার কীগুলি আনলক করার জন্য এটি দরকার", + "completed": "সম্পূর্ণ", + "confirm": "নিশ্চিত", + "confirm-new-password": "নতুন পাসওয়ার্ড নিশ্চিত করুন", + "confirm-pairing-code-placeholder": "আপনার জোড়া কোড নিশ্চিত করুন ...", + "confirm-password-placeholder": "আপনার পাসওয়ার্ড নিশ্চিত করুন...", + "confirmation-request": "নিশ্চিতকরণ অনুরোধ", + "confirmations": "নিশ্চিতকরণ", + "confirmations-helper-text": "যখন লেনদেনের 12 টি নিশ্চয়তা থাকে আপনি এটিকে নিষ্পত্তি হিসাবে বিবেচনা করতে পারেন।", + "connect": "সংযোগ করুন", + "connect-mailserver-content": "{{name}}-এ সংযোগ করুন?", + "connect-wallet": "ওয়ালেট সংযোগ করুন", + "connected": "সংযুক্ত", + "connected-to": "সংযুক্ত", + "connecting": "সংযুক্ত হচ্ছে ...", + "connecting-requires-login": "অন্য নেটওয়ার্কের সাথে সংযোগ স্থাপনের জন্য লগইন প্রয়োজন", + "connection-request": "সংযোগের অনুরোধ", + "connection-status": "সংযোগ অবস্থা", + "connection-with-the-card-lost": "কার্ডের সাথে সংযোগ\n বিচ্ছিন্ন হয়েছে", + "connection-with-the-card-lost-setup-text": "উপস্থাপনা পুনরায় শুরু করতে কার্ডটি ধরে রাখুন\n আপনার ফোনের পিছনে এবং রক্ষণাবেক্ষণ\n ফোন যোগাযোগে কার্ড", + "connection-with-the-card-lost-text": "আপনার ফোনের পিছনে কার্ডটি ধরে রাখুন", + "contact-code": "চ্যাট কী", + "contact-s": { + "one": "যোগাযোগ", + "other": "যোগাযোগ" + }, + "contacts": "যোগাযোগ", + "contacts-descr": "আপনার পরিচিতিগুলি এখানে উপস্থিত হবে। আপনি পরিচিতি হিসাবে যাকে যুক্ত করেন তার কাছ থেকে স্ট্যাটাস আপডেট পাবেন", + "contacts-empty": "ই এন এস নামের পরিচিতিগুলি এখানে উপস্থিত হবে", + "continue": "চালিয়ে যান", + "continue-anyway": "যাই হোক চালিয়ে যান", + "contract-address": "চুক্তির ঠিকানা", + "contract-interaction": "চুক্তি মিথস্ক্রিয়া", + "contract-isnt-supported": "চুক্তি সমর্থিত নয়", + "copy-info": "তথ্য অনুলিপি করুন", + "copy-qr": "কোড অনুলিপি করুন", + "copy-to-clipboard": "অনুলিপি", + "copy-transaction-hash": "লেনদেন আইডি অনুলিপি করুন", + "cost-fee": "খরচ \/ ফি", + "counter-9-plus": "৯+", + "counter-99-plus": "৯৯+", + "create": "তৈরি", + "create-a-pin": "একটি 6-সংখ্যার পাসকোড তৈরি করুন", + "create-a-puk": "একটি 12 সংখ্যার পিইউকে তৈরি করুন", + "create-category": "বিভাগ তৈরি করুন", + "create-channel": "একটি চ্যানেল তৈরি করুন", + "create-channel-title": "নতুন চ্যানেল", + "create-community": "একটি সম্প্রদায় তৈরি করুন", + "create-group-chat": "গ্রুপ চ্যাট তৈরি করুন", + "create-multiaccount": "কী তৈরি করুন", + "create-new-key": "নতুন চাবি পান", + "create-pin": "6-সংখ্যার পাসকোড তৈরি করুন", + "create-pin-description": "Status আনলক করতে এবং লেনদেন নিশ্চিত করতে আপনার কার্ড + এই ৬ ডিজিটের পাসকোড প্রয়োজন হবে", + "created-group-chat-description": "আপনি গ্রুপটি তৈরি করেছেন", + "cryptokitty-name": "ক্রিপ্টোকিটি #{id}}", + "currency": "মুদ্রা", + "currency-display-name-aed": "আমিরাতি দিরহাম", + "currency-display-name-afn": "আফগানিস্তান আফগানি", + "currency-display-name-ars": "আর্জেন্টিনা পেসো", + "currency-display-name-aud": "অস্ট্রেলিয়ান ডলার", + "currency-display-name-bbd": "বার্বাডোস ডলার", + "currency-display-name-bdt": "বাংলাদেশী টাকা", + "currency-display-name-bgn": "বুলগেরিয়ান লেভ", + "currency-display-name-bhd": "বাহরাইনি দিনার", + "currency-display-name-bnd": "ব্রুনাই দারুসসালাম ডলার", + "currency-display-name-bob": "বলিভিয়া বলিভিয়ানো", + "currency-display-name-brl": "ব্রাজিল রিয়াল", + "currency-display-name-btn": "ভুটানি এনগল্ট্রাম", + "currency-display-name-cad": "কানাডা ডলার", + "currency-display-name-chf": "সুইজারল্যান্ড ফ্রাঙ্ক", + "currency-display-name-clp": "চিলি পেসো", + "currency-display-name-cny": "চীন ইউয়ান রেনমিনবি", + "currency-display-name-cop": "কলম্বিয়া পেসো", + "currency-display-name-crc": "কোস্টা রিকা কোলন", + "currency-display-name-czk": "চেক কুরুনা", + "currency-display-name-dkk": "ডেনমার্ক ক্রোন", + "currency-display-name-dop": "ডোমিনিকান রিপাবলিক পেসো", + "currency-display-name-egp": "মিশর পাউন্ড", + "currency-display-name-etb": "ইথিওপিয়ান বির", + "currency-display-name-eur": "ইউরো", + "currency-display-name-gbp": "ব্রিটিশ পাউন্ড", + "currency-display-name-gel": "জর্জিয়ান লারি", + "currency-display-name-ghs": "ঘানা সেদি", + "currency-display-name-hkd": "হংকং ডলার", + "currency-display-name-hrk": "ক্রোয়েশিয়া কুনা", + "currency-display-name-huf": "হাঙ্গেরি ফরিন্ট", + "currency-display-name-idr": "ইন্দোনেশিয়া রুপিয়া", + "currency-display-name-ils": "ইজরায়েল শেকেল", + "currency-display-name-inr": "ভারত রুপি", + "currency-display-name-isk": "আইসল্যান্ড ক্রোনা", + "currency-display-name-jmd": "জ্যামাইকা ডলার", + "currency-display-name-jpy": "জাপানি ইয়েন", + "currency-display-name-kes": "কেনিয়ান শিলিং", + "currency-display-name-krw": "কোরিয়া (দক্ষিণ) ওন", + "currency-display-name-kwd": "কুয়েতি দিনার", + "currency-display-name-kzt": "কাজাখস্তান টেঙ্গে", + "currency-display-name-lkr": "শ্রীলংকা রুপি", + "currency-display-name-mad": "মরোক্কোর দিরহাম", + "currency-display-name-mdl": "মোল্দোভান লেউ", + "currency-display-name-mur": "মরিশাস রুপি", + "currency-display-name-mwk": "মালাউয়ান কোয়াছা", + "currency-display-name-mxn": "মেক্সিকো পেসো", + "currency-display-name-myr": "মালয়েশিয়া রিংগিত", + "currency-display-name-mzn": "মোজাম্বিক মেটিক্যাল", + "currency-display-name-nad": "নামিবিয়া ডলার", + "currency-display-name-ngn": "নাইজেরিয়া নাইরা", + "currency-display-name-nok": "নরওয়ে ক্রোন", + "currency-display-name-npr": "নেপাল রুপি", + "currency-display-name-nzd": "নিউজিল্যান্ড ডলার", + "currency-display-name-omr": "ওমান রিয়াল", + "currency-display-name-pen": "পেরু সোল", + "currency-display-name-pgk": "পাপুয়া নিউ গিনি কিনা", + "currency-display-name-php": "ফিলিপাইন পেসো", + "currency-display-name-pkr": "পাকিস্তান রুপি", + "currency-display-name-pln": "পোল্যান্ড জলোটি", + "currency-display-name-pyg": "প্যারাগুয়ে গুয়ারানি", + "currency-display-name-qar": "কাতার রিয়াল", + "currency-display-name-ron": "রুমানিয়া লিউ", + "currency-display-name-rsd": "সার্বিয়া দিনার", + "currency-display-name-rub": "রাশিয়া রুবেল", + "currency-display-name-sar": "সৌদি আরব রিয়াল", + "currency-display-name-sek": "সুইডেন ক্রোনা", + "currency-display-name-sgd": "সিঙ্গাপুর ডলার", + "currency-display-name-thb": "থাইল্যান্ড বাহত", + "currency-display-name-try": "তুর্কি লিরা", + "currency-display-name-ttd": "ত্রিনিদাদ ও টোবাগো ডলার", + "currency-display-name-twd": "তাইওয়ান নিউ ডলার", + "currency-display-name-tzs": "তানজানিয়ান শিলিং", + "currency-display-name-uah": "ইউক্রেন রিভনিয়া", + "currency-display-name-ugx": "উগান্ডার শিলিং", + "currency-display-name-usd": "মার্কিন যুক্তরাষ্ট্র ডলার", + "currency-display-name-uyu": "উরুগুয়ে পেসো", + "currency-display-name-vef": "ভেনেজুয়েলা বলিভার", + "currency-display-name-vnd": "ভিয়েতনাম ডং", + "currency-display-name-zar": "দক্ষিণ আফ্রিকা র্যান্ড", + "current": "বর্তমান", + "current-average": "বর্তমান গড়", + "current-average-tip": "বর্তমান গড় টিপ", + "current-base": "বর্তমান ভিত্তি", + "current-base-fee": "বর্তমান বেস ফি", + "current-minimum-tip": "বর্তমান ন্যূনতম টিপ", + "current-network": "বর্তমান নেটওয়ার্ক", + "current-password": "বর্তমান পাসওয়ার্ড", + "current-pin": "6-ডিজিটের পাসকোড প্রবেশ করান", + "current-pin-description": "অগ্রসর হতে আপনার ৬-ডিজিটের পাসকোড প্রবেশ করান", + "custom": "কাস্টম", + "custom-networks": "পছন্দসই নেটওয়ার্ক", + "custom-node": "আপনি কাস্টম RPC এন্ডপয়েন্ট ব্যবহার করছেন। আপনার স্থানীয় স্থানান্তরের ইতিহাস অসম্পূর্ণ হতে পারে।", + "custom-seed-phrase": "অবৈধ seed phrase", + "custom-seed-phrase-text-1": "এই seed phrase আমাদের সমর্থিত অভিধানের সাথে মেলে না। ভুল বানান শব্দের জন্য পরীক্ষা করুন।", + "dapp": "DApp", + "dapp-starter-pack-accept": "গ্রহণ করুন এবং খুলুন", + "dapp-starter-pack-description": "এখানে আপনাকে শুরু করার জন্য কিছু ক্রিপ্টো আছে! স্টিকার, একটি ENS নাম পেতে এটি ব্যবহার করুন এবং DApps চেষ্টা করুন।", + "dapp-starter-pack-title": "স্টার্টার প্যাক", + "dapp-would-like-to-connect-wallet": "সংযোগ করতে চান", + "dapps": "DApps", + "dapps-permissions": "DApp অনুমতি", + "dark": "অদৃশ্য", + "data": "ডাটা", + "data-collected": "তথ্য সংগ্রহ", + "data-collected-subtitle": "নীচের টেবিলটি সঠিক ডেটা দেখায় যা সংরক্ষণ করা হয় এবং পাঠানো হবে। কোনও সংবেদনশীল ডেটা প্রেরণ করা হয় না তা নিশ্চিত করার জন্য ডেটা সর্বজনীন নিয়মের বিরুদ্ধে যাচাই করা হয়। বিশ্বাস করবেন না, যাচাই করুন।", + "data-syncing": "ডাটা সিংক্রোনাইজিং", + "database-reset-content": "চ্যাট, পরিচিতি এবং সেটিংস মুছে ফেলা হয়েছে। আপনি আপনার কী -কার্ড দিয়ে আপনার অ্যাকাউন্ট ব্যবহার করতে পারেন", + "database-reset-title": "ডাটাবেস রিসেট", + "database-reset-warning": "ডাটাবেস রিসেট করা হবে। চ্যাট, পরিচিতি এবং সেটিংস মুছে ফেলা হবে", + "datetime-ago": "আগে", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "one": "দিন", + "other": "দিন" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "ঘন্টা", + "other": "ঘন্টা" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "সময়", + "other": "সময়" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "", + "other": "সেগেন্ড" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "আজ", + "datetime-yesterday": "গতকাল", + "decimals": "দশমিক", + "decline": "প্রত্যাখ্যান করুন", + "decryption-failed-content": "আপনার উপাত্ত ডিক্রিপ্ট করতে একটি ত্রুটি সংঘটিত হয়েছে। আপনার হয়ত আপনার পুরনো ডেটা মুছে ফেলতে হবে এবং একটি নতুন অ্যাকাউন্ট তৈরি করতে হবে। আবার চেষ্টা করতে মুছে ফেলতে \"প্রয়োগ করুন\" ট্যাপ করুন অথবা \"বাতিল করুন\" ট্যাপ করুন", + "default": "ডিফল্ট", + "default-assets": "ডিফল্ট ERC20 এবং ERC721", + "default-sync-period": "ইতিহাস ফিল্টার করুন", + "delete": "মুছে ফেলা", + "delete-account": "অ্যাকাউন্ট মুছে ফেলুন", + "delete-and-leave-group": "মুছে ফেলুন এবং দল ছেড়ে দিন", + "delete-bootnode": "bootnode মুছে ফেলুন", + "delete-bootnode-are-you-sure": "আপনি কি নিশ্চিত যে আপনি এই bootnode টি মুছে ফেলতে চান?", + "delete-bootnode-title": "bootnode মুছুন", + "delete-category-confirmation": "আপনি কি এই বিভাগটি মুছে ফেলার বিষয়ে নিশ্চিত?\n", + "close-chat": "চ্যাট মুছুন", + "close-chat-confirmation": "আপনি কি নিশ্চিত যে আপনি এই চ্যাটটি মুছে ফেলতে চান?", + "delete-confirmation": "মুছে ফেলা?", + "delete-keys-keycard": "কীকার্ড থেকে কীগুলি মুছুন", + "delete-mailserver": "ইতিহাস নোড মুছে ফেলুন", + "delete-mailserver-are-you-sure": "আপনি কি নিশ্চিত যে আপনি এই ইতিহাস নোডটি মুছে ফেলতে চান?", + "delete-mailserver-title": "ইতিহাস নোড মুছে ফেলুন", + "delete-message": "বার্তা মুছে ফেলুন", + "delete-my-account": "আমার অ্যাকাউন্ট মুছে ফেলুন", + "delete-my-profile": "আমার প্রোফাইল মুছে ফেলুন", + "delete-network-confirmation": "আপনি কি নিশ্চিত যে আপনি এই নেটওয়ার্কটি মুছে ফেলতে চান?", + "delete-network-error": "এটি মুছে ফেলার আগে অনুগ্রহ করে একটি ভিন্ন নেটওয়ার্কে সংযোগ করুন", + "delete-network-title": "নেটওয়ার্ক মুছে ফেলা হবে?", + "delete-node": "নোড মুছুন", + "delete-node-are-you-sure": "আপনি কি নিশ্চিত যে আপনি এই নোডটি মুছতে চান?", + "delete-node-title": "নোড মুছে দিন", + "delete-profile": "প্রোফাইল মুছে ফেলুন", + "delete-profile-warning": "সতর্কীকরণ: আপনার যদি আপনার বীজ বাক্যাংশ লেখা না থাকে, আপনি আপনার প্রোফাইল মুছে ফেলার পর আপনার তহবিলে প্রবেশাধিকার হারাবেন", + "deny": "অস্বীকার করুন", + "derivation-path": "বিবর্তনের পথ", + "describe-channel": "চ্যানেলটি বর্ণনা করুন", + "description": "বর্ণনা", + "dev-mode": "ডেভলপমেন্ট মোড", + "dev-mode-settings": "উন্নয়ন মোড সেটিংস", + "device-syncing": "ডিভাইস এককালবর্তীকরণ", + "devices": "ডিভাইসগুলি", + "disable": "অক্ষম", + "disable-all": "সব নিষ্ক্রিয় করুন", + "disable-later-in-settings": "আপনি সেটিংসে পরে এটি অক্ষম করতে পারেন৷", + "disabled": "অক্ষম", + "disconnect": "সংযোগ বিচ্ছিন্ন করুন", + "disconnected": "অফলাইনে চ্যাট করুন", + "discover": "আবিষ্কার করুন", + "dismiss": "বরখাস্ত করুন", + "display-collectibles": "সংগ্রহযোগ্য প্রদর্শন\n", + "done": "সম্পন্ন হয়েছে", + "dont-ask": "আমাকে আবার জিজ্ঞাসা করবেন না", + "edit": "সম্পাদনা", + "edit-categories": "বিভাগ সম্পাদনা করুন", + "edit-channel-title": "চ্যানেল সম্পাদনা করুন", + "edit-chats": "চ্যাট সম্পাদনা করুন", + "edit-community": "সম্প্রদায় সম্পাদনা করুন", + "edit-favourite": "প্রিয় সম্পাদনা করুন", + "edit-group": "গোষ্ঠী সম্পাদনা করুন", + "edit-profile": "প্রোফাইল সম্পাদনা করুন", + "edited": "সম্পাদিত", + "editing-message": "বার্তা সম্পাদনা করা হচ্ছে", + "empty-activity-center": "আপনার চ্যাটের বিজ্ঞপ্তি\n এখানে প্রদর্শিত হবে", + "empty-chat-description": "কোন বার্তা নেই \nএই আড্ডায় এখনো", + "empty-chat-description-community": "এখানে শেষবারের মতো শান্ত ছিল{{quiet-hours}}", + "empty-chat-description-one-to-one": "আপনি এখানে যে কোনও বার্তা প্রেরণ করেন তা এনক্রিপ্ট করা হয় এবং কেবল আপনি এবং এর মাধ্যমে পড়তে পারেন", + "empty-chat-description-public": "এটি এখানে শেষ {{quiet-hours}} এর জন্য কথোপকথন শুরু করুন \n বা", + "empty-chat-description-public-share-this": "এই চ্যাটটি শেয়ার করুন", + "empty-keycard-required": "একটি খালি কীকার্ড প্রয়োজন", + "empty-pending-invitations-descr": "যারা এই গ্রুপে যোগ দিতে চান\nএকটি আমন্ত্রণ লিংকের মাধ্যমে এখানে প্রদর্শিত হবে", + "empty-tab": "খালি ট্যাব", + "enable": "সক্ষম করুন", + "enable-all": "সব সক্রিয় করুন", + "enable-link-previews": "চ্যাটে লিঙ্কের পূর্বরূপগুলি সক্ষম করবেন?", + "encrypt-with-password": "পাসওয়ার্ড সহ এনক্রিপ্ট করুন", + "ens-10-SNT": "১০ SNT", + "ens-add-username": "ব্যবহারকারীর নাম যুক্ত করুন", + "ens-agree-to": "সম্মত হন", + "ens-chat-settings": "চ্যাট সেটিংস", + "ens-custom-domain": "কাস্টম ডোমেইন", + "ens-custom-username-hints": "username.domain.eth-এর মতো কাস্টম ডোমেইন সহ সম্পূর্ণ ব্যবহারকারীর নাম টাইপ করুন", + "ens-custom-username-taken": "ব্যবহারকারীর নাম আপনার সাথে সম্পর্কিত নয় :(", + "ens-deposit": "আমানত", + "ens-dismiss-message": "বরখাস্ত করতে এখানে ক্লিক করুন", + "ens-displayed-with": "আপনার বার্তাগুলি অন্যদের সাথে প্রদর্শিত হয়", + "ens-get-name": "একটি সার্বজনীন ব্যবহারকারীর নাম পান", + "ens-got-it": "ঠিক আছে, বুঝতে পেরেছি", + "ens-locked": "ব্যবহারকারীর নাম লক করা হয়েছে। {{date}} পর্যন্ত আপনি এটি প্রকাশ করতে পারবেন না", + "ens-name-content": "আপনার চ্যাট কী-এর জন্য কাস্টম ছদ্মনাম যা আপনি Ethereum নাম সেবা ব্যবহার করে নিবন্ধন করতে পারেন। ENS নামবিকেন্দ্রীভূত ব্যবহারকারীর নাম।", + "ens-name-not-found": "ENS নামটি সমাধান করতে পারে না", + "ens-name-title": "ENS নাম", + "ens-network-restriction": "শুধুমাত্র mainnet উপলভ্য", + "ens-no-usernames": "আপনার সাথে কোনও ব্যবহারকারীর নাম সংযুক্ত নেই", + "ens-powered-by": "ইথেরিয়াম নাম সেবা দ্বারা চালিত", + "ens-primary-username": "প্রাথমিক ব্যবহারকারীর নাম", + "ens-register": "নিবন্ধন", + "ens-registration-failed": "ব্যবহারকারীর নাম নিবন্ধন করতে, অনুগ্রহ করে আবার চেষ্টা করুন।", + "ens-registration-failed-title": "লেনদেন ব্যর্থ হয়েছে", + "ens-registration-failure": "নিবন্ধন ব্যর্থ হয়েছে", + "ens-registration-in-progress": "নিবন্ধন চলছে...", + "ens-release-username": "ব্যবহারকারীর নাম প্রকাশ করুন", + "ens-remove-hints": "অপসারণ আপনার কী থেকে ব্যবহারকারীর নামটি পৃথক করবে।", + "ens-remove-username": "ব্যবহারকারীর নাম অপসারণ করুন", + "ens-saved": "এখন আপনার চ্যাট কী-এর সাথে সংযুক্ত এবং Status ব্যবহার করা যাবে।", + "ens-saved-title": "ব্যবহারকারীর নাম যুক্ত করা হয়েছে", + "ens-show-username": "চ্যাটগুলিতে আমার ENS ব্যবহারকারীর নামটি দেখান", + "ens-terms-header": "নাম নিবন্ধনের শর্তাবলী", + "ens-terms-point-1": "তহবিল 1 বছরের জন্য জমা হয়। আপনার SNT লক হয়ে যাবে, তবে ব্যয় হবে না।", + "ens-terms-point-10": "0x0000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS রেজিস্ট্রি)।", + "ens-terms-point-2": "1 বছর পরে, আপনি নামটি প্রকাশ করতে পারেন এবং আপনার আমানত ফিরে পেতে পারেন, বা নাম রাখার জন্য কোনও পদক্ষেপ নিতে পারবেন না।", + "ens-terms-point-3": "যদি চুক্তির শর্তাবলী পরিবর্তিত হয় - উদাহরণস্বরূপ Status চুক্তি আপগ্রেড করে - ব্যবহারকারীর সময় নির্বিশেষে ব্যবহারকারীর নাম প্রকাশ করার অধিকার আছে।", + "ens-terms-point-4": "চুক্তি নিয়ন্ত্রক আপনার জমা দেওয়া তহবিল অ্যাক্সেস করতে পারে না। এগুলি কেবল তাদের পাঠানো ঠিকানায় ফিরে যেতে পারে।", + "ens-terms-point-5": "আপনার ঠিকানা(es) আপনার ENS নামের সাথে জনসম্মুখে সংযুক্ত করা হবে।", + "ens-terms-point-6": "ব্যবহারকারীর নামগুলি stateofus.eth এর সাবডোমেন নোড হিসাবে তৈরি করা হয় এবং ENS স্মার্ট চুক্তির শর্ত সাপেক্ষে।", + "ens-terms-point-7": "আপনি আপনার পক্ষ থেকে SNT স্থানান্তরের চুক্তির অনুমোদন দেন। আপনি যখন স্থানান্তরের অনুমোদন দেওয়ার জন্য একটি লেনদেন অনুমোদন করেন তখনই এটি ঘটতে পারে।", + "ens-terms-point-8": "এই শর্তাবলী ঠিকানায় স্মার্ট কন্ট্রাক্ট লজিক দ্বারা গ্যারান্টি দেওয়া হয়।", + "ens-terms-point-9": "{{address}} (Status ইউজারনেম রেজিস্ট্রার)", + "ens-terms-registration": "নাম নিবন্ধনের শর্তাবলী।", + "ens-test-message": "আরে", + "ens-transaction-pending": "লেনদেন স্থগিত...", + "ens-understand": "আমি বুঝতে পারছি যে আমার মানিব্যাগের ঠিকানা আমার ব্যবহারকারীর নামের সাথে প্রকাশ্যে সংযুক্ত করা হবে।", + "ens-username": "ENS ব্যবহারকারীর নাম", + "ens-username-already-added": "ব্যবহারকারীর নামটি ইতিমধ্যে আপনার চ্যাট কীটির সাথে সংযুক্ত এবং Status অভ্যন্তরে ব্যবহার করা যেতে পারে।", + "ens-username-available": "✓ ব্যবহারকারীর নাম উপলব্ধ!", + "ens-username-connected": "এই ব্যবহারকারীর নামটি আপনার মালিকানাধীন এবং আপনার চ্যাট কী-এর সাথে সংযুক্ত।", + "ens-username-connected-continue": "চ্যাটে আমার ENS ব্যবহারকারীর নাম দেখান' সেট অব্যাহত রাখুন।", + "ens-username-connected-with-different-key": "অব্যাহত রাখার জন্য আপনার বর্তমান চ্যাট কী-এর সাথে ব্যবহারকারীর নাম সংযুক্ত করতে একটি লেনদেনের প্রয়োজন হবে।", + "ens-username-connection-confirmation": "লেনদেন সম্পন্ন হলে {{username}} সংযুক্ত হবে।", + "ens-username-hints": "কমপক্ষে ৪ টি অক্ষর। লাতিন বর্ণ, সংখ্যা এবং কেবল ছোট হাতের অক্ষর।", + "ens-username-invalid": "শুধু অক্ষর এবং সংখ্যা.", + "ens-username-invalid-name-warning": "আপনার নামগুলির একটির নিবন্ধন প্রক্রিয়া অবৈধ অবস্থায় শেষ হয়েছে৷ ওয়ালেট লেনদেনের জন্য নামটি ব্যবহার করবেন না এবং support@status.im-এ আমাদের সহায়তার সাথে যোগাযোগ করুন", + "ens-username-owned": "® ব্যবহারকারীর নাম আপনার মালিকানাধীন। ", + "ens-username-owned-continue": "অব্যাহত রাখা এই ব্যবহারকারীর নামকে আপনার চ্যাট কী-এর সাথে সংযুক্ত করবে।", + "ens-username-registration-confirmation": "চমৎকার! লেনদেন সম্পূর্ণ হলে আপনি {{username}} এর মালিক।", + "ens-username-registration-invalid": "সতর্কতা ! রেজিস্ট্রেশন প্রক্রিয়া অবৈধ অবস্থায় শেষ হয়েছে। ওয়ালেট লেনদেনের জন্য নামটি ব্যবহার করবেন না এবং support@status.im-এ আমাদের সহায়তার সাথে যোগাযোগ করুন", + "ens-username-taken": "ব্যবহারকারীর নাম ইতিমধ্যেই নেওয়া :(", + "ens-username-you-can-follow-progress": "আপনি আপনার মানিব্যাগের লেনদেন ইতিহাস বিভাগের অগ্রগতি অনুসরণ করতে পারেন।", + "ens-usernames": "ENS ব্যবহারকারীর নাম", + "ens-usernames-details": "অন্যান্য ব্যবহারকারীদের দ্বারা সহজে স্বীকৃত হতে একটি সার্বজনীন ব্যবহারকারীর নাম নিবন্ধন করুন", + "ens-want-custom-domain": "আমি অন্য ডোমেইনে একটি নামের মালিক", + "ens-want-domain": "আমি একটি stateofus.eth ডোমেইন চাই", + "ens-welcome-hints": "ইএনএস নামগুলি সেই উন্মাদ-দীর্ঘ ঠিকানাগুলিকে অনন্য ব্যবহারকারীর নামে রূপান্তরিত করে।", + "ens-welcome-point-customize": "একটি ENS নাম চ্যাটে আপনার এলোমেলো 3-শব্দের নাম প্রতিস্থাপন করতে পারে. {{name}} এর পরিবর্তে @yourname হবে।", + "ens-welcome-point-customize-title": "আপনার চ্যাট নামটি কাস্টমাইজ করুন", + "ens-welcome-point-receive": "অন্যরা আপনাকে একটি সহজ ধাপে চ্যাটের মাধ্যমে ফান্ড পাঠাতে পারেন।", + "ens-welcome-point-receive-title": "চ্যাটে লেনদেন গ্রহণ করুন", + "ens-welcome-point-register": "নাম চিরকাল রাখার জন্য একবার নিবন্ধন করুন। ১ বছর পর আপনি নাম প্রকাশ করতে পারেন এবং আপনার SNT ফেরত পেতে পারেন।", + "ens-welcome-point-register-title": "নিবন্ধন করতে ১০ টি SNT", + "ens-welcome-point-simplify": "আপনি আপনার হেক্সাডেসিমাল হ্যাশ (0x ...) এর পরিবর্তে আপনার সহজেই ভাগ করে নেওয়ার জন্য ENS নামে তহবিলগুলি পেতে পারেন।", + "ens-welcome-point-simplify-title": "আপনার ETH ঠিকানা সরল করুন", + "ens-welcome-point-verify": "আপনি পরবর্তী পদক্ষেপগুলিতে নিজের মালিকানাধীন যে কোনও ব্যবহারকারীর নামটি যাচাই করতে এবং যুক্ত করতে পারেন।", + "ens-welcome-point-verify-title": "ইতোমধ্যে একটি ব্যবহারকারীর নামের মালিক?", + "ens-your-username": "আপনার ব্যবহারকারীর নাম", + "ens-your-usernames": "আপনার ব্যবহারকারীর নাম", + "ens-your-your-name": "আপনার ENS নাম", + "enter-12-words": "আপনার seed phrase এর 12 টি শব্দ লিখুন, একক স্পেস দ্বারা পৃথক করে", + "enter-a-private-key": "একটি ব্যক্তিগত কী লিখুন", + "enter-a-seed-phrase": "Seed phrase প্রবেশ করান", + "enter-address": "ঠিকানা লিখুন", + "enter-contact-code": "ই এন এস (vitalik94) অথবা চ্যাট কী (0x04…)", + "enter-pair-code": "আপনার জোড়া কোড প্রবেশ করান", + "enter-pair-code-description": "কী-কার্ড উপস্থাপনার সময় আপনার কাছে জোড়া কোড প্রদর্শিত হয়েছিল", + "enter-password": "পাসওয়ার্ড প্রবেশ করান", + "enter-password-migration-prompt": "আপনার কীগুলির সাথে পরিচিতি, চ্যাট এবং সেটিংস সরাতে আপনার পাসওয়ার্ড লিখুন", + "enter-pin": "6-ডিজিটের পাসকোড প্রবেশ করান", + "enter-puk-code": "PUK কোড প্রবেশ করান", + "enter-puk-code-description": "6-সংখ্যার পাসকোড ব্লক করা হয়েছে।\n পাসকোড অবরোধ মুক্ত করতে দয়া করে PUK কোড প্রবেশ করুন।", + "enter-recipient-address-or-username": "প্রাপকের ঠিকানা বা ব্যবহারকারীর নাম লিখুন", + "enter-seed-phrase": "বীজ বাক্যাংশ লিখুন", + "enter-url": "ইউআরএল লিখুন", + "enter-user-pk": "ব্যবহারকারী পাবলিক কী লিখুন", + "enter-watch-account-address": "একটি QR কোড স্ক্যান করুন\nবা\nদেখার জন্য ঠিকানা প্রবেশ করান", + "enter-word": "শব্দ লিখুন", + "enter-your-code": "আপনার 6-সংখ্যার পাসকোড প্রবেশ করান", + "enter-your-password": "আপনার পাসওয়ার্ড লিখুন", + "error": "ত্রুটি", + "error-unable-to-get-balance": "ভারসাম্য পেতে অক্ষম", + "error-unable-to-get-prices": "মুদ্রা রূপান্তর ত্রুটি। আবার চেষ্টা করতে আপনার স্ক্রীন সতেজ করুন।", + "error-unable-to-get-token-balance": "টোকেন ব্যালেন্স পেতে অক্ষম", + "errors": "ত্রুটি", + "eth": "ETH", + "ethereum-account": "Ethereum account", + "ethereum-address": "ইথেরিয়াম ঠকানা", + "ethereum-node-started-incorrectly-description": "ভুল কনফিগারেশন দিয়ে Ethereum নোড শুরু করা হয়েছে, সেই অবস্থা থেকে পুনরুদ্ধার করতে প্রয়োগণ বন্ধ করা হবে। কনফিগার করা নেটওয়ার্ক id = {{network-id}} actual = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Ethereum নোডটি ভুলভাবে শুরু হয়েছে", + "etherscan-lookup": "Etherscan এর দিকে তাকান", + "everyone": "প্রত্যেকে", + "expand-all": "সব প্রসারিত করুন", + "export-account": "একাউন্ট রপ্তানি করুন", + "export-key": "ব্যক্তিগত কী রপ্তানি করুন", + "external-storage-denied": "বাহ্যিক স্টোরেজ অ্যাক্সেস অস্বীকার করা হয়", + "failed": "ব্যর্থ", + "faq": "ঘন ঘন জিজ্ঞাসিত প্রশ্ন", + "fast": "দ্রুত", + "favourite": "প্রিয়", + "favourite-description": "আপনার প্রিয় ওয়েবসাইটগুলি এখানে উপস্থিত হবে", + "favourites": "পছন্দনীয়", + "favourites-empty": "পছন্দনীয়তে যুক্ত ঠিকানা এখানে প্রদর্শিত হবে", + "fee-cap": "ফি ক্যাপ", + "fee-explanation": "লেনদেনের জন্য সর্বাধিক সামগ্রিক মূল্য। যদি ব্লক বেস ফি এর চেয়ে বেশি হয়, তাহলে এটি নিম্ন ভিত্তি ফি সহ নিম্নলিখিত ব্লকে অন্তর্ভুক্ত করা হবে।", + "fee-options": "প্রস্তাবিত ফি অপশন", + "fetch-community": "সম্প্রদায় আনয়ন করুন", + "fetch-messages": "বার্তা আনুন", + "fetch-timeline": "আনুন", + "fetching-community": "সম্প্রদায় আনা হচ্ছে...", + "find": "অনুসন্ধান", + "finish": "সমাপ্ত", + "finishing-card-setup": "কার্ড উপস্থাপনা সমাপ্ত হচ্ছে", + "fleet": "বহর", + "fleet-settings": "ফ্লিট সেটিংস", + "follow": "অনুসরণ করুন", + "follow-your-interests": "একটি সর্বজনীন চ্যাটে যান এবং নতুন লোকের সাথে দেখা করুন", + "free": "↓ বিনামূল্যে", + "from": "থেকে", + "gas-amount-limit": "গ্যাসের সীমা", + "gas-limit": "গ্যাসের সীমা", + "gas-price": "Gas সীমা", + "gas-used": "Gas ব্যাবহার", + "generate-a-key": "কী তৈরি করুন", + "generate-a-new-account": "একটি অ্যাকাউন্ট তৈরি করুন", + "generate-a-new-key": "একটি নতুন কী তৈরি করুন", + "generate-account": "কী তৈরি করুন", + "generate-an-account": "একটি অ্যাকাউন্ট তৈরি করুন", + "generate-new-key": "কী তৈরি করুন", + "generating-codes-for-pairing": "> কার্ডে পণ্য সফটওয়্যার ডাউনলোড করা হচ্ছে\n > আনলক এবং জোড়া কোড তৈরি করা হচ্ছে", + "generating-keys": "কী তৈরি করা হচ্ছে...", + "generating-mnemonic": "Seed phrase তৈরি করা হচ্ছে", + "get-a-keycard": "একটি কীকার্ড পান", + "get-started": "শুরু করুন", + "get-status-at": "Status পান http:\/\/status.im", + "get-stickers": "স্টিকার পান", + "give-a-short-description": "একটি সংক্ষিপ্ত বিবরণ দিন", + "give-a-short-description-community": "একটি সংক্ষিপ্ত বিবরণ দিন", + "give-permissions-camera": "অনুমতি দিন\nক্যামেরা অ্যাক্সেস করতে", + "glossary": "শব্দকোষ", + "go-to-settings": "সেটিংসে যান...", + "got-it": "পেয়েছি", + "grant-face-id-permissions": "প্রয়োজনীয় ফেস আইডি অনুমতি দিতে, অনুগ্রহ করে আপনার সিস্টেম সেটিংসে যান এবং নিশ্চিত করুন যে Status> ফেস আইডি নির্বাচন করা হয়েছে।", + "group-chat": "গ্রুপ চ্যাট", + "group-chat-admin": "প্রশাসক", + "group-chat-admin-added": "** {{member}} ** প্রশাসক করা হয়েছে", + "group-chat-all-contacts-invited": "আপনার সমস্ত পরিচিতি ইতিমধ্যে গ্রুপে রয়েছে", + "group-chat-created": "** {{member}} ** গ্রুপ তৈরি করেছে ** {{name}} **", + "group-chat-decline-invitation": "আমন্ত্রণ প্রত্যাখ্যান করুন", + "group-chat-member-added": "** {{member}} ** আমন্ত্রিত হয়েছে", + "group-chat-member-joined": "** {{member}} ** গ্রুপে যোগ দিয়েছে", + "group-chat-member-removed": "** {{member}} ** গোষ্ঠীটি ছেড়ে গেছে", + "group-chat-members-count": "{{নির্বাচিত}}\/{সর্বোচ্চ}} সদস্য", + "group-chat-name-changed": "** {{member}} ** গোষ্ঠীর নাম পরিবর্তন করে ** {{name}} ** করা হয়েছে", + "group-chat-no-contacts": "আপনার এখনও কোনও যোগাযোগ নেই।\n চ্যাট শুরু করতে আপনার বন্ধুদের আমন্ত্রণ জানান", + "group-info": "গ্রুপ তথ্য", + "group-invite": "গ্রুপ আমন্ত্রণ", + "group-invite-link": "গ্রুপ আমন্ত্রণ লিংক", + "group-membership-request": "গ্রুপ সদস্যতার অনুরোধ", + "gwei": "Gwei", + "has-permissions": "অ্যাক্সেস করার অনুমতি আছে", + "hash": "হ্যাশ", + "help": "সাহায্য", + "help-capitalized": "সহায়তা", + "help-center": "সহায়তা কেন্দ্র", + "help-improve-status": "স্ট্যাটাস উন্নত করতে সাহায্য করুন", + "hide": "লুকান", + "hide-content-when-switching-apps": "স্ক্রীনশটগুলি ব্লক করুন", + "hide-content-when-switching-apps-ios": "প্রাকদর্শন লুকান", + "history": "ইতিহাস", + "history-nodes": "ইতিহাস নোড", + "hold-card": "পিছনে কার্ড ধরে রাখুন\n আপনার ফোনের", + "home": "বাড়ি", + "hooks": "হুকস", + "how-it-works": "এটা কিভাবে কাজ করে", + "http-gateway-error": "উফ, অনুরোধ ব্যর্থ হয়েছে!", + "identifier": "শনাক্তকারী", + "image": "চিত্র", + "image-remove-current": "বর্তমান ছবি অপসারণ করুন", + "image-source-gallery": "গ্যালারী থেকে নির্বাচন করুন", + "image-source-make-photo": "ক্যাপচার", + "image-source-title": "ছবি সম্পাদনা করুন", + "import": "আমদানি", + "import-community": "একটি সম্প্রদায় আমদানি করুন", + "import-community-title": "একটি সম্প্রদায় আমদানি করুন", + "in-contacts": "যোগাযোগে", + "include": "অন্তর্ভুক্ত করুন", + "incoming": "আগত", + "incoming-transaction": "আগত লেনদেন", + "incorrect-code": [ + "str", + "দুঃখিত কোডটি ভুল ছিল, দয়া করে এটি আবার লিখুন" + ], + "increase-gas": "গ্যাস বাড়ান", + "initialization": "আরম্ভ", + "install": "↓ ইনস্টল করুন", + "insufficient-balance-to-cover-fee": "লেনদেন ফি কভার করার জন্য যথেষ্ট ব্যালেন্স নেই", + "intro-message1": "Status স্বাগতম! আপনার পাসওয়ার্ড সেট করতে এবং শুরু করতে এই বার্তাটি আলতো চাপুন।", + "intro-privacy-policy-note1": "স্ট্যাটাস আপনার ব্যক্তিগত ডেটা থেকে সংগ্রহ বা লাভ করে না। অব্যাহত রেখে, আপনি এর সাথে একমত", + "intro-privacy-policy-note2": "গোপনীয়তা নীতি", + "intro-text": "Status বিকেন্দ্রীভূত ওয়েবে আপনার গেটওয়ে", + "intro-text1": "একটি পিয়ার-টু-পিয়ার, এনক্রিপ্ট করা নেটওয়ার্কে চ্যাট করুন যেখানে বার্তাসেন্সর বা হ্যাক করা যাবে না", + "intro-text2": "বিশ্বের যে কোন জায়গায় ডিজিটাল সম্পদ প্রেরণ এবং গ্রহণ করুন- কোন ব্যাংক একাউন্টের প্রয়োজন নেই", + "intro-text3": "গেম, এক্সচেঞ্জ এবং সামাজিক নেটওয়ার্ক অনুসন্ধান করুন যেখানে আপনি একা আপনার ডেটার মালিক", + "intro-title1": "সত্যিকারের ব্যক্তিগত যোগাযোগ", + "intro-title2": "নিরাপদ ক্রিপ্টো ওয়ালেট", + "intro-title3": "বিকেন্দ্রীভূত অ্যাপস", + "intro-wizard-text1": "কীগুলির একটি সেট আপনার অ্যাকাউন্ট নিয়ন্ত্রণ করে. আপনার কী আপনার ফোনে সরাসরি, যাতে আপনি সেগুলি ব্যবহার করতে পারেন", + "intro-wizard-text2": "একটা চাবি হল আড্ডার জন্য। এটি একটি পাঠযোগ্য নাম নিয়ে আসে যা পরিবর্তন করা যাবে না।", + "intro-wizard-text3": "আপনি যদি একটি কীকার্ডের মালিক হন, বর্ধিত নিরাপত্তার জন্য সেখানে আপনার চাবি সঞ্চয় করুন।", + "intro-wizard-text4": "আপনার কীসমূহ নিরাপদ এবং এনক্রিপ্ট করুন", + "intro-wizard-text6": "স্ট্যটাস আপনাকে নতুন বার্তাগুলি সম্পর্কে অবহিত করবে। আপনি পরে সেটিংসে আপনার বিজ্ঞপ্তি পছন্দগুলি সম্পাদনা করতে পারেন", + "intro-wizard-title-alt4": "একটি পাসওয়ার্ড তৈরি করুন", + "intro-wizard-title-alt5": "আপনার পাসওয়ার্ড নিশ্চিত করুন", + "intro-wizard-title1": "আপনার চাবি পান", + "intro-wizard-title2": "একটি চ্যাট নাম পছন্দ করুন", + "intro-wizard-title3": "কী স্টোরেজ পছন্দ করুন", + "intro-wizard-title4": "একটি ৬-সংখ্যার পাসকোড তৈরি করুন", + "intro-wizard-title5": "পাসকোডটি নিশ্চিত করুন", + "intro-wizard-title6": "বিজ্ঞপ্তিগুলি সক্ষম করুন", + "introduce-yourself": "একটি সংক্ষিপ্ত বার্তা দিয়ে নিজেকে পরিচয় করিয়ে দিন", + "invalid-address-qr-code": "স্ক্যান করা QR কোড একটি বৈধ ঠিকানা ধারণ করে না", + "invalid-format": "অকার্যকর বিন্যাস\nঅবশ্যই {{format}} হতে হবে", + "invalid-key-confirm": "প্রয়োগ করুন", + "invalid-key-content": "ডেটাবেস এনক্রিপ্ট করা যায় না কারণ একটি ফাইল দূষিত। আপনার তহবিল এবং চ্যাট কী নিরাপদ। অন্যান্য ডেটা, যেমন আপনার চ্যাট এবং পরিচিতিগুলি পুনরুদ্ধার করা যায় না। \" {{erase-multiaccounts-data-button-text}} \" বোতামটি, অন্যান্য সমস্ত ডেটা সরিয়ে দেবে এবং আপনাকে আপনার তহবিলগুলিতে অ্যাক্সেস করতে এবং বার্তাগুলি প্রেরণ করতে দেয়", + "invalid-number": "অকার্যকর সংখ্যা", + "invalid-pairing-password": "অকার্যকর জোড়া পাসওয়ার্ড", + "invalid-public-chat-topic": "অকার্যকর পাবলিক চ্যাট প্রসঙ্গ", + "invalid-range": "অকার্যকর বিন্যাস, অবশ্যই {{min}} এবং {{max}} এর মধ্যে থাকতে হবে", + "invalid-username-or-key": "অবৈধ ব্যবহারকারীর নাম বা চ্যাট কী", + "invite": "আমন্ত্রণ", + "invite-button": "আমন্ত্রণ", + "invite-chat-accept": "গ্রহণ করুন", + "invite-chat-accept-join": "গ্রহণ করুন এবং যোগদান করুন", + "invite-chat-intro": "Status যোগ দিতে একজন বন্ধু আপনাকে রেফার করেছে। এখানে আপনাকে শুরু করার জন্য কিছু ক্রিপ্টো আছে! একটি ENS নাম নিবন্ধন করতে বা একটি স্টিকার প্যাক কিনতে এটি ব্যবহার করুন।", + "invite-chat-name": "বন্ধু রেফারেল", + "invite-chat-pending": "বিচারাধীন", + "invite-chat-rule": "গ্রহণ করলে আপনার বন্ধুকে ক্রিপ্টো রেফারেল বোনাস প্রদান করা হবে।", + "invite-chat-starter-pack": "স্টার্টার প্যাক", + "invite-friends": "বন্ধুদের আমন্ত্রণ করুন", + "invite-instruction-fifth": "আপনি যে কোন সময় আপনার রেফারেল বোনাস পরিশোধ করতে পারেন।", + "invite-instruction-first": "স্ট্যাটাস ডাউনলোড এবং যোগদান করতে আপনি আপনার বন্ধুকে একটি অনন্য আমন্ত্রণ লিংক পাঠান", + "invite-instruction-fourth": "আপনি আপনার রেফারেল বোনাস এবং আপনার বন্ধু স্টার্টার প্যাক পাবেন", + "invite-instruction-second": "আপনার বন্ধু স্ট্যাটাস ডাউনলোড করে এবং একটি অ্যাকাউন্ট তৈরি করে (অ্যান্ড্রয়েডে)", + "invite-instruction-third": "আপনার বন্ধুর সাথে একটি চ্যাট শুরু হয়েছে, যেখানে তারা আপনার রেফারেল নিশ্চিত করে", + "invite-people": "বন্ধুদের আমন্ত্রণ করুন", + "invite-privacy-policy-public": "আপনি একটি রেফারেল লিঙ্কের মাধ্যমে স্ট্যাটাস ইনস্টল করেছেন। এই চ্যাটে যোগদান করে আপনি আপনার রেফারারকে অ্যাট্রিবিউট করেন এবং এতে সম্মত হন", + "invite-privacy-policy1": "গ্রহণ করে আপনি রেফারেল প্রোগ্রামটিতে সম্মত হন", + "invite-privacy-policy2": "শর্তাবলী।", + "invite-public-chat-home": "রেফারেল আমন্ত্রণ", + "invite-public-chat-intro": "আপনাকে শুরু করার জন্য এখানে কিছু ক্রিপ্টো আছে! একটি ENS নাম নিবন্ধন করতে বা স্টিকার প্যাক কিনতে এটি ব্যবহার করুন", + "invite-receive-account": "আপনার রেফারেল বোনাস গ্রহণের জন্য অ্যাকাউন্ট", + "invite-reward": "আপনার আমন্ত্রিত প্রতিটি বন্ধুর জন্য ক্রিপ্টো আয় করুন!", + "invite-reward-friend": "বন্ধু: ", + "invite-reward-friend-description": "আপনার বন্ধু শুরু করার জন্য কিছু {{reward}} নিয়ে গঠিত একটি স্টার্টার প্যাক পাবেন", + "invite-reward-friend-name": "স্টার্টার প্যাক", + "invite-reward-you": "আপনি: ", + "invite-reward-you-description": "একজন বন্ধুকে আমন্ত্রণ জানান এবং রেফারেল বোনাস হিসেবে {{reward}} গ্রহণ করুন। স্টিকার, একটি ই এন এস নাম পেতে এটি ব্যবহার করুন এবং ড্যাপ চেষ্টা করুন", + "invite-reward-you-name": "রেফারেল বোনাস", + "invite-select-account": "আপনার রেফারেল বোনাস পেতে একটি অ্যাকাউন্ট নির্বাচন করুন", + "invite-warning": "এই প্রমোশন শুধুমাত্র একটি অ্যান্ড্রয়েড ডিভাইস ব্যবহারকারীদের জন্য বৈধ, যারা মার্কিন যুক্তরাষ্ট্রের বাসিন্দা নয়। বন্ধুকে ৭ দিনের মধ্যে রেফারেল নিশ্চিত করতে হবে", + "invited": "আমন্ত্রিত", + "join": "যোগদান", + "join-a-community": "অথবা একটি সম্প্রদায়ে যোগ দিন", + "join-group-chat": "গ্রুপে যোগ দিন", + "join-group-chat-description": "{{username}} আপনাকে দলে যোগ দিতে আমন্ত্রণ জানিয়েছে {{group-name}}", + "join-me": "স্ট্যাটাসে আমার সাথে যোগ দিন", + "join-new-private-chat": "একটি নতুন ব্যক্তিগত চ্যাট শুরু করুন", + "join-new-public-chat": "একটি সর্বজনীন আড্ডায় যোগ দিন", + "joined": "যোগদান করা হয়েছে", + "joined-group-chat-description": "আপনি {{group-name}} আমন্ত্রণ থেকে {{username}} যোগদান করেছেন", + "key": "কী", + "key-managment": "কী ব্যবস্থাপনা", + "key-on-device": "এই ডিভাইসে ব্যক্তিগত কী সংরক্ষিত আছে", + "keycard": "কী-কার্ড", + "keycard-access-reset": "কী-কার্ড অ্যাক্সেস রিসেট করা হয়েছে", + "keycard-applet-install-instructions": "অ্যাপলেট ইনস্টল করতে অনুগ্রহ করে https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation উপর নির্দেশাবলী অনুসরণ করুন", + "keycard-awaiting-description": "আপনার ডিভাইসে NFC রিডার খুঁজতে কার্ডটি সরানোর চেষ্টা করুন", + "keycard-awaiting-title": "এখনো খুঁজছি...", + "keycard-backup": "একটি ব্যাকআপ কীকার্ড তৈরি করুন", + "keycard-backup-success-body": "ব্যাকআপ কার্ড সফলভাবে তৈরি হয়েছে। আপনি এখন এটি আপনার অ্যাকাউন্টের সাথে প্রাথমিক কার্ডের মতো ব্যবহার করতে পারেন।", + "keycard-backup-success-title": "ব্যাকআপ সফল", + "keycard-blocked": "কীকার্ড অবরুদ্ধ করা হয়েছে।\n এটি ব্যবহার চালিয়ে যেতে আপনার পুনরায় সেট করতে হবে।", + "keycard-can-use-with-new-passcode": "আপনি আপনার নতুন পাসকোড দিয়ে এই কার্ডটি ব্যবহার করতে পারেন", + "keycard-cancel-setup-text": "এটি কী-কার্ড উপস্থাপনা বাতিল করবে। কী-কার্ড ব্যবহার করার জন্য উপস্থাপনা টি শেষ করা অত্যন্ত বাঞ্ছনীয়। আপনি কি সত্যিই বাতিল করতে চান?", + "keycard-cancel-setup-title": "বিপজ্জনক অপারেশন", + "keycard-connected-description": "কার্ডটি স্থির রাখার চেষ্টা করুন", + "keycard-connected-title": "সংযুক্ত", + "keycard-desc": "আপনি একটি কী কার্ডের মালিক? এতে আপনার চাবি সংরক্ষণ করুন; লেনদেনের জন্য আপনার এটি প্রয়োজন হবে", + "keycard-dont-ask-card": "সাইন ইন করার জন্য কার্ড চাইবেন না", + "keycard-enter-new-passcode": "নতুন পাসকোড প্রবেশ করান {{step}}\/2", + "keycard-error-description": "চালিয়ে যাওয়ার জন্য কার্ডটি আবার সংযুক্ত করুন", + "keycard-error-title": "সংযোগ হারিয়ে গেছে", + "keycard-factory-reset": "কারখানার সেটিংসে কার্ড ফেরত দিন", + "keycard-factory-reset-text": "এটি সম্পাদন করলে কার্ডে সংরক্ষিত যেকোন স্মৃতিচিহ্ন মুছে যাবে। এই কীকার্ডের সাহায্যে আপনি যে স্মরণীয় বাক্যাংশটি ব্যবহার করছেন তার একটি ব্যাকআপ নিশ্চিত করুন।", + "keycard-factory-reset-title": "আপনি কি নিশ্চিত যে আপনি একটি ফ্যাক্টরি রিসেট করতে চান?", + "keycard-free-pairing-slots": "কীকার্ডে {{n}} বিনামূল্যে জোড়া স্লট আছে", + "keycard-has-multiaccount-on-it": "এই কার্ডটি পূর্ণ। প্রতিটি কার্ড একটি প্রধান কী-পেয়ার ধারণ করতে পারে", + "keycard-init-description": "অব্যাহত রাখতে আপনার ফোনের পিছনে কার্ডটি রাখুন", + "keycard-init-title": "কার্ড খুঁজছেন ...", + "keycard-is-blocked-details": "এই অ্যাকাউন্টের জন্য অ্যাক্সেস বা সাইন করতে আপনি আর এই কার্ডটি ব্যবহার করতে পারবেন না। অনেক ব্যর্থ পাসকোড এবং পিইউকে প্রচেষ্টা হয়েছে।", + "keycard-is-blocked-instructions": "আপনার অ্যাকাউন্টটি পুনরায় ইনস্টল করার পর স্ট্যটাস অ্যাক্সেস করতে এবং একটি নতুন কীকার্ড ব্যবহার করতে, আলাদা ওয়ালেট ব্যবহার করুন বা ম্যানুয়ালি কীকার্ড পুনরায় সেট করুন।", + "keycard-is-blocked-title": "কী-কার্ড অবরুদ্ধ", + "keycard-is-frozen-details": "আপনার সম্পদ রক্ষা করতে, আপনার কার্ড হিমায়িত করা হয়েছে। কী আনলক করতে এবং লেনদেন প্রেরণ করতে কার্ড অ্যাক্সেস রিসেট করুন. একটি নতুন পাসকোড তৈরি করুন এবং এই কার্ডে আপনার অ্যাকাউন্ট(গুলি) অ্যাক্সেস করতে আপনার PUK প্রবেশ করান", + "keycard-is-frozen-factory-reset": "মেমোনিক দিয়ে রিসেট করুন", + "keycard-is-frozen-reset": "কার্ড অ্যাক্সেস পুনরায় সেট করুন", + "keycard-is-frozen-title": "কী-কার্ড জমে গেছে", + "keycard-onboarding-finishing-header": "সমাপ্ত হচ্ছে", + "keycard-onboarding-intro-header": "কীকার্ডে আপনার কী সঞ্চয় করুন", + "keycard-onboarding-intro-text": "প্রস্তুত হন, এটি কয়েক মিনিট সময় নিতে পারে, তবে আপনার অ্যাকাউন্ট সুরক্ষিত করা গুরুত্বপূর্ণ", + "keycard-onboarding-mnemonic-text": "আপনার বীজ বাক্যাংশটি লিখতে আপনার একটি কাগজ এবং একটি পেন্সিলেরও প্রয়োজন হবে।", + "keycard-onboarding-pairing-header": "কার্ডটি যুক্ত করা হচ্ছে ...", + "keycard-onboarding-pin-text": "আপনাকে একটি 6-সংখ্যার পাসকোড তৈরি করতে হবে যা আপনার কীকার্ডের অ্যাক্সেস সুরক্ষিত করতে ব্যবহৃত হবে।", + "keycard-onboarding-preparing-header": "কার্ড প্রস্তুত করা হচ্ছে...", + "keycard-onboarding-puk-code-header": "কোড লিখুন\n এবং এগুলি নিরাপদে সংরক্ষণ করুন", + "keycard-onboarding-recovery-phrase-description": "আপনার চাবি ফেরত পেতে আপনার এই বীজ বাক্যাংশ প্রয়োজন. এটা লিখে রাখুন। এটি নিরাপদ, অফলাইন রাখুন, এবং এই ডিভাইস থেকে পৃথক করুন.", + "keycard-onboarding-recovery-phrase-header": "বীজ বাক্যাংশ ব্যাক আপ করুন", + "keycard-onboarding-recovery-phrase-text": "শুধু আপনার চোখের জন্য। এটি আপনার চাবি তৈরি করতে ব্যবহৃত যাদুকরী বীজ।", + "keycard-onboarding-start-header": "পিছনে কার্ড ধরে\n আপনার ফোন শুরু করতে", + "keycard-onboarding-start-step1": "একটি পাসকোড তৈরি করুন", + "keycard-onboarding-start-step1-text": "প্রায় ১ মিনিট। আপনার কী এনক্রিপ্ট করতে একটি ৬-ডিজিটের পাসকোড তৈরি করুন", + "keycard-onboarding-start-step2": "পিইউকে এবং পেয়ারিং কোড লিখুন", + "keycard-onboarding-start-step2-text": "প্রায় ১ মিনিট। তার জন্য আপনার প্রয়োজন এক টুকরো কাগজ এবং একটি পেন্সিল", + "keycard-onboarding-start-step3": "বীজ বাক্যাংশ ব্যাক আপ করুন", + "keycard-onboarding-start-step3-text": "প্রায় ১ মিনিট। এছাড়াও একটি কাগজ এবং একটি পেন্সিল প্রয়োজন", + "keycard-onboarding-start-text": "এবং ফোন যোগাযোগে কার্ড বজায় রাখুন\n উপস্থাপনার সময়। সেটআপ প্রায় ৪ মিনিট সময় লাগবে", + "keycard-processing-description": "কার্ডটি স্থির রাখার চেষ্টা করুন", + "keycard-processing-title": "প্রক্রিয়াকরণ।।।", + "keycard-recover": "হারানো বা হিমায়িত কার্ড?", + "keycard-recover-text": "যদি আপনার স্মারক বাক্যাংশ থাকে তবে আপনি এই অ্যাকাউন্টের সাথে যুক্ত একটি নতুন কীকার্ড তৈরি করতে পারেন। আপনি একটি নতুন কীকার্ড ব্যবহার করতে পারেন অথবা হিমায়িত একটিতে ফ্যাক্টরি রিসেট করতে পারেন।", + "keycard-recover-title": "এই অ্যাকাউন্টের জন্য একটি নতুন কার্ড তৈরি করবেন?", + "keycard-recovery-intro-button-text": "পুনরুদ্ধার শুরু করুন", + "keycard-recovery-intro-header": "কীকার্ডে সংরক্ষিত কীগুলি পুনরুদ্ধার করুন", + "keycard-recovery-intro-text": "আপনি যদি আগে একটি কীকার্ড ব্যবহার করে কী তৈরি করেন এবং এখন এই ডিভাইসে এই কীগুলি ব্যবহার করতে চান", + "keycard-recovery-no-key-header": "কিছুই নেই\n এখানে পুনরুদ্ধার করার", + "keycard-recovery-no-key-text": "আপনার কীকার্ডে কোন কী মজুদ নেই। এটি ব্যবহার করতে, একটি নতুন কী তৈরি করুন এবং কী সঞ্চয় করতে আপনার কীকার্ড চয়ন করুন", + "keycard-recovery-phrase-confirm-header": "বীজ বাক্যাংশ নিশ্চিত করুন", + "keycard-recovery-phrase-confirmation-text": "তুমি দ্বিতীয় বার সুযোগ পেতে পারবে না! আপনি যদি অ্যাক্সেস হারান, উদাহরণস্বরূপ আপনার কী-কার্ড হারিয়ে, আপনি শুধুমাত্র আপনার বীজ বাক্যাংশ দিয়ে আপনার কী অ্যাক্সেস করতে পারেন. কেউ না, কিন্তু তোমার বীজের বাক্যাংশ আছে। এটা লিখে দাও। এটা নিরাপদ রাখো।", + "keycard-recovery-phrase-confirmation-title": "বীজ বাক্যটি নিচে লিখেছেন?", + "keycard-recovery-success-header": "আপনার চাবি হয়েছে\n সফলভাবে পুনরুদ্ধার", + "keycard-redeem-title": "ছাড়িয়ে দিন", + "keycard-redeem-tx": "সম্পদ উদ্ধার করুন", + "keycard-redeem-tx-desc": "স্বাক্ষর করতে এবং সম্পদ গ্রহণ করতে কার্ডটি ট্যাপ করুন", + "keycard-reset-passcode": "পাসকোড রিসেট করুন", + "keycard-success-description": "আপনি এখন কার্ডটি অপসারণ করতে পারেন", + "keycard-success-title": "সাফল্য", + "keycard-unauthorized-operation": "আপনি এই অপারেশন করতে অননুমোদিত।\n বৈধ কার্ডটি আলতো চাপুন এবং আবার চেষ্টা করুন।", + "keycard-upsell-subtitle": "উন্নত নিরাপত্তা এবং সুবিধা", + "language": "ভাষা", + "last-backup-performed": "শেষ ব্যাকআপ সম্পাদিত:", + "last-transaction": "সর্বশেষ লেনদেন", + "learn-more": "আরও জানুন", + "learn-more-about-keycard": "কীকার্ড সম্পর্কে আরও জানুন", + "leave": "ছেড়ে দিন", + "leave-chat": "চ্যাট ছেড়ে দিন", + "leave-chat-confirmation": "আপনি কি নিশ্চিত যে আপনি চ্যাট ছেড়ে যেতে চান?", + "leave-community": "সম্প্রদায় ত্যাগ করুন", + "leave-confirmation": "চ্যাট ছেড়ে দিন", + "leave-group": "দল পরিত্যাগ করুন", + "left": "বাঁদিকে", + "les-ulc": "LES\/ULC", + "lets-go": "চলো যাই", + "light": "হালকা", + "limit": "সীমা", + "linked-on": "{{date}} এ সংযুক্ত", + "load-messages-before": "{{date}} এর আগে", + "load-more-messages": "আরও বার্তা আনুন", + "load-more-timeline": "আরও আনুন", + "loading": "লোড হচ্ছে ...", + "local-notifications": "স্থানীয় বিজ্ঞপ্তিসমূহ", + "local-notifications-subtitle": "ব্যাকগ্রাউন্ড পরিষেবা সক্রিয় করুন", + "lock-app-with": "সঙ্গে অ্যাপ লক করুন", + "log-level": "লগ স্তর", + "log-level-settings": "লগ স্তরের সেটিংস", + "logging": "লগিং", + "logging-enabled": "লগিং সক্রিয় করা হয়েছে?", + "login-pin-description": "আপনার কী আনলক করতে আপনার 6-ডিজিটের পাসকোড প্রবেশ করান", + "logout": "লগ আউট করুন", + "logout-app-content": "একাউন্টটি লগ আউট করা হবে। যখন আপনি এটি আবার আনলক করবেন, নির্বাচিত নেটওয়ার্ক ব্যবহার করা হবে", + "logout-are-you-sure": "আপনি কি নিশ্চিত আপনি চান\nলগ আউট করতে?", + "logout-key-management": "কী ম্যানেজমেন্ট অ্যাক্সেস করতে আপনাকে লগ আউট করতে হবে", + "logout-title": "লগ আউট?", + "looking-for-cards": "কার্ড খুঁজছেন ...", + "lost-connection": "সংযোগ হারিয়ে গেছে", + "low-tip": "টিপ খুব কম", + "lower-than-average-tip": "গড় টিপের চেয়ে কম", + "mail-should-be-configured": "মেইল ক্লায়েন্ট কনফিগার করা উচিত", + "mailserver-address": "স্ট্যাটাস নোডের ঠিকানা", + "mailserver-automatic": "স্বয়ংক্রিয় নির্বাচন", + "mailserver-automatic-switch-explanation": "উপলভ্য দ্রুততম ইতিহাস নোড চয়ন করুন", + "mailserver-connection-error": "ইতিহাস নোডের সাথে সংযোগ করা যায়নি", + "mailserver-content": "Status নেটওয়ার্কের একটি নোড যা ৩০ দিন পর্যন্ত বার্তারুট এবং সঞ্চয় করে।", + "mailserver-details": "ইতিহাস নোডের বিস্তারিত", + "mailserver-error-content": "আপনার নির্বাচিত ইতিহাস নোডে পৌঁছানো যায়নি।", + "mailserver-error-title": "ইতিহাস নোডের সাথে সংযুক্ত হতে ত্রুটি", + "mailserver-format": "এনোড: \/\/ {enode-id} : {password} @ {ip-address} : {port}", + "mailserver-pick-another": "আরেকটি ইতিহাস নোড বেছে নিন", + "mailserver-reconnect": "ইতিহাস নোডের সাথে সংযোগ করা যায়নি। পুনরায় সংযোগ করতে ট্যাপ করুন", + "mailserver-request-error-content": "নিম্নলিখিত ত্রুটিটি ইতিহাস নোড দ্বারা ফেরত দেওয়া হয়েছে: {{error}}", + "mailserver-request-error-status": "ইতিহাস আনার সময় একটি ত্রুটি সংঘটিত হয়েছে, বিস্তারিত জানার জন্য লগপরীক্ষা করুন", + "mailserver-request-error-title": "ইতিহাস নোড অনুরোধ ত্রুটি", + "mailserver-request-retry": "অনুরোধ পুনরায় চেষ্টা করুন", + "mailserver-retry": "পুনরায় চেষ্টা করা", + "mailserver-title": "ইতিহাস নোড", + "main-currency": "প্রধান মুদ্রা", + "main-networks": "প্রধান নেটওয়ার্কসমূহ", + "main-wallet": "প্রধান মানিব্যাগ", + "mainnet-network": "প্রধান নেটওয়ার্ক", + "make-admin": "প্রশাসক তৈরি করুন", + "manage-connections": "অ্যাপ্লিকেশন সংযোগগুলির মধ্যে থেকে সংযোগগুলি পরিচালনা করুন৷", + "manage-keys-and-storage": "কী এবং স্টোরেজ পরিচালনা করুন", + "mark-all-read": "সমস্ত পড়া চিহ্নিত করুন", + "master-account": "মাস্টার অ্যাকাউন্ট", + "max-fee": "সর্বোচ্চ ফি", + "max-priority-fee": "সর্বোচ্চ অগ্রাধিকার ফি", + "maximum-fee": "সর্বোচ্চ ফি", + "maximum-fee-desc": "লেনদেনের জন্য সর্বাধিক সামগ্রিক মূল্য। যদি বর্তমান ব্লক বেস ফি এটি অতিক্রম করে তবে আপনার লেনদেনটি কম বেস ফি সহ একটি নিম্নলিখিত ব্লকে অন্তর্ভুক্ত করা হবে।", + "maybe-later": "হয়তো পরে", + "member-ban": "নিষিদ্ধ সদস্য", + "member-kick": "কিক সদস্য", + "members": { + "one": "1 সদস্য", + "other": " {{count}} সদস্য" + }, + "members-active": { + "one": "সদস্য", + "other": "{{count}} সদস্য" + }, + "members-active-none": "কোন সদস্য নেই", + "members-count": "{{count}} সদস্য", + "members-label": "সদস্যরা", + "members-limit-reached": "সদস্যদের সীমা পৌঁছেছে", + "members-title": "সদস্যরা", + "membership-approval": "অনুমোদন প্রয়োজন", + "membership-approval-description": "আপনার কমিউনিটি যোগদানের জন্য স্বাধীন, কিন্তু নতুন সদস্যদের প্রথমে কমিউনিটি নির্মাতার অনুমোদন প্রয়োজন", + "membership-button": "সদস্যপদের প্রয়োজনীয়তা", + "membership-declined": "সদস্যপদের অনুরোধ প্রত্যাখ্যান করা হয়েছে", + "membership-description": "গ্রুপ মেম্বারশিপ আপনাকে গ্রুপ অ্যাডমিন দ্বারা গ্রহণ করতে হবে", + "membership-ens": "ইএনএস ব্যবহারকারীর নাম প্রয়োজন", + "membership-ens-description": "আপনার সম্প্রদায়ের যোগদান করতে সক্ষম হওয়ার জন্য একটি ইএনএস ব্যবহারকারীর নাম প্রয়োজন", + "membership-free": "কোন প্রয়োজনীয়তা নেই", + "membership-free-description": "আপনার কমিউনিটি যে কেউ যোগদানের জন্য বিনামূল্যে", + "membership-invite": "অন্য সদস্যের আমন্ত্রণ প্রয়োজন", + "membership-invite-description": "আপনার সম্প্রদায় শুধুমাত্র বিদ্যমান সম্প্রদায়ের সদস্যদের একটি আমন্ত্রণ দ্বারা যোগদান করা যেতে পারে", + "membership-none": "কিছুই না", + "membership-none-placeholder": "আপনি নতুন সদস্যদের যোগদানের আগে নির্দিষ্ট মানদণ্ড পূরণ করতে পারেন। এটি যে কোন সময় পরিবর্তন করা যেতে পারে", + "membership-request-pending": "সদস্যপদের আবেদন মুলতুবি", + "membership-requests": "সদস্যপদের অনুরোধ", + "membership-title": "সদস্যপদের প্রয়োজনীয়তা", + "message": "বার্তা", + "message-not-sent": "বার্তা প্রেরণ করা হয়নি", + "message-options-cancel": "বাতিল", + "message-reply": "উত্তর", + "messages": "বার্তাগুলি", + "messages-from-contacts-only-subtitle": "আপনার পরিচিতি হিসেবে যোগ করা লোকজনই আপনার সাথে নতুন চ্যাট শুরু করতে পারে অথবা আপনাকে একটি গ্রুপে আমন্ত্রণ জানাতে পারেন", + "might-break": "কিছু অ্যাপ ভাঙ্গতে পারে", + "migration-successful": "মাইগ্রেশন সফল", + "migration-successful-text": "অ্যাকাউন্ট সফলভাবে কীকার্ডে স্থানান্তরিত হয়েছে", + "migrations-failed-content": "{{message}}\n স্কিমা সংস্করণ: প্রাথমিক {{initial-version}} , বর্তমান {{current-version}} , শেষ {{last-version}} \n\n একটি ডাটাবেস ত্রুটি ঘটেছে। আপনার তহবিল এবং চ্যাট কী নিরাপদ। অন্যান্য ডেটা, যেমন আপনার চ্যাট এবং পরিচিতিগুলি পুনরুদ্ধার করা যায় না। \" {{erase-multiaccounts-data-button-text}} \" বোতামটি, অন্যান্য সমস্ত ডেটা সরিয়ে ফেলবে এবং আপনাকে আপনার তহবিলগুলিতে অ্যাক্সেস করতে এবং বার্তাগুলি প্রেরণের অনুমতি দেয়।", + "miners-higher-fee": "আপনি যদি উচ্চতর ফি প্রদান করেন তবে খনির লোকেরা সম্ভবত আপনার লেনদেনকে অন্তর্ভুক্ত করবে।", + "minimum-received": "ন্যূনতম প্রাপ্তি", + "mobile-network-ask-me": "মোবাইল নেটওয়ার্কে আমাকে জিজ্ঞেস করুন", + "mobile-network-continue-syncing": "এককালবর্তীকরণ অব্যাহত রাখুন", + "mobile-network-continue-syncing-details": "আপনি এটি পরে সেটিংসে পরিবর্তন করতে পারেন", + "mobile-network-go-to-settings": "সেটিংসে যান", + "mobile-network-settings": "মোবাইল ডাটা", + "mobile-network-sheet-configure": "আপনি আরো সিংক্রোনাইজিং কনফিগার করতে পারেন \nবিস্তারিত", + "mobile-network-sheet-offline": "কোনও ওয়াই-ফাই নেই, বার্তা সিঙ্ক করা অক্ষম হয়েছে।", + "mobile-network-sheet-offline-details": "মোবাইল নেটওয়ার্ক ব্যবহার করে এককালবর্তীকরণ বন্ধ", + "mobile-network-sheet-remember-choice": "আমার পছন্দ মনে রেখো", + "mobile-network-sheet-settings": "সেটিংস", + "mobile-network-start-syncing": "এককালবর্তীকরণ শুরু করুন", + "mobile-network-stop-syncing": "এককালবর্তীকরণ বন্ধ করুন", + "mobile-network-stop-syncing-details": "ওয়াই-ফাই-এর সাথে সংযুক্ত না হওয়া পর্যন্ত?", + "mobile-network-use-mobile": "মোবাইল ডাটা ব্যবহার করুন", + "mobile-network-use-mobile-data": "চ্যাট এবং ওয়ালেট সিঙ্ক করার সময় স্ট্যাটাস প্রচুর ডেটা ব্যবহার করে।", + "mobile-network-use-wifi": "শুধুমাত্র ওয়াই-ফাই", + "mobile-syncing-sheet-details": "চ্যাট এবং ওয়ালেট সিঙ্ক করার সময় Status প্রচুর ডেটা ব্যবহার করে।", + "mobile-syncing-sheet-title": "মোবাইল ডেটা ব্যবহার করে সিঙ্ক করবেন?", + "more": "আরও", + "move-and-reset": "সরান এবং পুনরায় সেট করুন", + "move-keystore-file": "কীস্টোর ফাইল সরান", + "move-keystore-file-to-keycard": "কীস্টোর ফাইলকে কীকার্ডে সরাতে চান?", + "multiaccount-exists-content": "এই একাউন্টের জন্য কী ইতোমধ্যে বিদ্যমান এবং পুনরায় যুক্ত করা যাবে না। আপনি যদি আপনার পাসওয়ার্ড, পাসকোড বা কীকার্ড হারিয়ে ফেলেন, অ্যাপটি আনইনস্টল করুন, আপনার বীজ বাক্যাংশ প্রবেশ করিয়ে আপনার কী পুনরায় ইনস্টল করুন এবং অ্যাক্সেস করুন", + "multiaccount-exists-title": "এই অ্যাকাউন্টের কীগুলি ইতিমধ্যে বিদ্যমান", + "multiaccounts-recover-enter-phrase-text": "১২,১৫,১৮,২১ বা ২৪ শব্দ প্রবেশ করান।\nএকটি একক স্থান দ্বারা শব্দ পৃথক করুন।", + "multiaccounts-recover-enter-phrase-title": "আপনার বীজ বাক্যাংশ লিখুন", + "mute": "নিঃশব্দ করুন", + "my-accounts": "আমার অ্যাকাউন্ট", + "my-accounts-empty": "আপনার উপলভ্য একাউন্টসমূহ এখানে প্রদর্শিত হবে", + "my-profile": "আমার প্রোফাইল", + "my-status": "আমার স্ট্যাটাস", + "name": "নাম", + "name-of-token": "আপনার টোকেনের নাম", + "name-optional": "নাম (ঐচ্ছিক)", + "name-your-channel": "আপনার চ্যানেলের নাম দিন", + "name-your-channel-placeholder": "চ্যানেলের নাম", + "name-your-community": "আপনার সম্প্রদায়ের নাম দিন", + "name-your-community-placeholder": "একটি আকর্ষণীয় নাম", + "need-help": "সাহায্য লাগবে?", + "network": "নেটওয়ার্ক", + "network-chain": "নেটওয়ার্ক চেইন", + "network-details": "নেটওয়ার্কের বিস্তারিত", + "network-fee": "নেটওয়ার্ক ফি", + "network-id": "নেটওয়ার্ক আইডি", + "network-info": "নেটওয়ার্ক তথ্য", + "network-invalid-network-id": "নির্দিষ্ট নেটওয়ার্ক আইডি RPC url দ্বারা নেটওয়ার্ক আইডির সাথে সামঞ্জস্যপূর্ণ নয়", + "network-invalid-status-code": "অকার্যকর স্তিতি কোড: {{code}}", + "network-invalid-url": "নেটওয়ার্ক ইউআরএল অবৈধ", + "network-settings": "নেটওয়ার্ক সেটিংস", + "never": "কখনই না", + "new": "নতুন", + "new-category": "নতুন বিভাগ", + "new-chat": "নতুন চ্যাট", + "new-community-title": "নতুন সম্প্রদায়", + "new-contact": "নতুন পরিচিতি", + "new-contract": "নতুন চুক্তি", + "new-favourite": "নতুন প্রিয়", + "new-group": "নতুন দল", + "new-group-chat": "নতুন গ্রুপ চ্যাট", + "new-network": "নতুন নেটওয়ার্ক", + "new-password": "নতুন পাসওয়ার্ড", + "new-pin-description": "নতুন 6-ডিজিটের পাসকোড প্রবেশ করান", + "new-public-group-chat": "পাবলিক চ্যাটে যোগ দিন", + "new-puk-description": "নতুন 12-সংখ্যার PUK লিখুন", + "new-status": "নতুন স্ট্যাটাস", + "new-tab": "নতুন ট্যাব", + "next": "পরবর্তী", + "nickname": "ডাকনাম", + "nickname-description": "ডাকনামগুলি আপনাকে স্ট্যাটাসে অন্যদের সনাক্ত করতে সহায়তা করে।\nআপনি যে ডাকনামগুলি যোগ করেছেন তা কেবল আপনি দেখতে পারেন", + "no": "না", + "no-collectibles": "কোনো সংগ্রহ সুপ্রাপ্য নয়", + "no-contacts": "এখনো কোন পরিচিতি নেই", + "no-keycard-applet-on-card": "কার্ডে কোন কীকার্ড অ্যাপলেট নেই", + "no-messages": "কোনো বার্তা নেই", + "no-pairing-slots-available": "এই কার্ডটি ইতোমধ্যে ৫টি ডিভাইসে জোড়া হয়েছে এবং এটির সাথে জোড়া হতে পারে না। অনুগ্রহ করে জোড়া ডিভাইসগুলির একটি ব্যবহার করুন, এই কার্ড দিয়ে লগ ইন করুন এবং কার্ডে জোড়া স্লট খালি করুন", + "no-pinned-messages": "কোন পিন করা বার্তা নেই", + "no-result": "কোন ফলাফল নেই", + "no-thanks": "না ধন্যবাদ", + "no-tokens-found": "কোনো টোকেন খুঁজে পাওয়া যায়নি", + "node-address": "নোডের ঠিকানা", + "node-details": "নোডের বিবরণ", + "node-info": "নোড তথ্য", + "node-version": "নোড সংস্করণ", + "nodes-disabled": "স্ট্যাটাস নোড নিষ্ক্রিয়", + "non-archival-node": "আরপিসি এন্ডপয়েন্ট আর্কাইভের অনুরোধ সমর্থন করে না। আপনার স্থানীয় স্থানান্তরের ইতিহাস অসম্পূর্ণ হতে পারে।", + "non-contacts": "পরিচিতি নেই", + "nonce": "nonce", + "none": "কিছুই না", + "normal": "স্বাভাবিক", + "not-applicable": "অস্বাক্ষরিত লেনদেনের জন্য প্রযোজ্য নয়", + "not-connected-nodes": "স্ট্যাটাস নোডের সাথে সংযুক্ত নয়", + "not-connected-to-peers": "কোন সমবয়সীদের সাথে সংযুক্ত নয়", + "not-enough-snt": "পর্যাপ্ত SNT নেই", + "not-found": "পাওয়া যায়নি", + "not-keycard-text": "আপনি যে কার্ডটি ব্যবহার করেছেন তা কীকার্ড নয়। এটি ব্যবহার করার জন্য আপনাকে একটি কীকার্ড কিনতে হবে", + "not-keycard-title": "কীকার্ড নয়", + "not-registered": "অনিবন্ধিত", + "notification-settings": "বিজ্ঞপ্তি", + "notifications": "বিজ্ঞপ্তি", + "notifications-non-contacts": "যোগাযোগবিহীন থেকে বিজ্ঞপ্তি", + "notifications-preferences": "বিজ্ঞপ্তি পছন্দসমূহ", + "notifications-servers": "বিজ্ঞপ্তি সার্ভার", + "notifications-switch": "বিজ্ঞপ্তিগুলি দেখান", + "notifications-transactions": "মানিব্যাগ লেনদেন", + "notify": "অবহিত", + "now": "এখন", + "off": "বন্ধ", + "off-status-tree": "অফ Status ট্রি", + "offline": "অফলাইন", + "offline-messaging-use-history-explanation": "অ্যাপটি বন্ধ করার সময় প্রেরিত বার্তা আনতে ইতিহাস নোড সক্রিয় করুন। যখন সক্রিয় করা হয়, একটি ইতিহাস নোড আপনার IP ঠিকানা পায়। যখন নিষ্ক্রিয় করা হবে তখন আপনি বার্তা পাবেন না যখন অ্যাপটি বন্ধ করা হবে এবং পরে অ্যাপটি খুললে আপনি সেগুলি দেখতে পাবেন না।", + "offline-messaging-use-history-nodes": "ইতিহাস নোড ব্যবহার করুন", + "ok": "ঠিক আছে", + "ok-continue": "ঠিক আছে, চালিয়ে যান", + "ok-got-it": "আচ্ছা আমি বুঝে গেছি", + "ok-save-pass": "ঠিক আছে, পাসওয়ার্ড সংরক্ষণ করুন", + "okay": "ঠিক আছে", + "on": "চালু করুন", + "on-status-tree": "অন Status ট্রি", + "once-enabled-share-metadata": "একবার সক্রিয় হয়ে গেলে, চ্যাটে পোস্ট করা লিংকগুলো সাইটের সাথে আপনার মেটাডাটা শেয়ার করতে পারে", + "one-day": "এক দিন", + "one-month": "এক মাস", + "one-week": "এক সপ্তাহ", + "open": "খুলুন", + "open-chat": "চ্যাট খুলুন", + "open-dapp": "DApp খুলুন", + "open-dapp-store": "DApps আবিষ্কার করুন", + "open-home": "খুলুন।।।", + "open-in-new-tab": "নতুন ট্যাবে খুলুন", + "open-membership": "উন্মুক্ত সদস্যপদ", + "open-nfc-settings": "এনএফসি সেটিংস খুলুন", + "open-on-block-explorer": "ব্লক এক্সপ্লোরারে খুলুন", + "opening-buy-crypto": "{{site}} খোলা হচ্ছে ...", + "optimal": "সর্বোত্তম", + "optional": "ঐচ্ছিক", + "or": "বা", + "outgoing": "বহির্গামী", + "outgoing-transaction": "বহির্গামী লেনদেন", + "page-camera-request-blocked": "ক্যামেরার অনুরোধ অবরুদ্ধ। ক্যামেরার অনুরোধ সক্রিয় করতে সেটিংসে যান", + "page-would-like-to-use-camera": "আপনার ক্যামেরা ব্যবহার করতে চান", + "pair": "ডিভাইসগুলি যুক্ত করুন", + "pair-card": "এই ডিভাইসে যুক্ত করুন", + "pair-code": "জোড়া কোড", + "pair-code-explanation": "কী আনলক করতে এবং একই কীকার্ডের সাথে লেনদেনে স্বাক্ষর করতে একটি ভিন্ন ডিভাইসে (৫ পর্যন্ত) কার্ড জোড়া", + "pair-code-placeholder": "জোড়া কোড...", + "pair-this-card": "এই কার্ডটি যুক্ত করুন", + "pair-this-device": "ডিভাইসের বিজ্ঞাপন দিন", + "pair-this-device-description": "তাদের মধ্যে যোগাযোগ এবং চ্যাট এককালবর্তী করতে আপনার ডিভাইসগুলি জোড়া করুন", + "paired-devices": "জোড়া ডিভাইস", + "pairing": "জোড়া", + "pairing-card": "জোড়া কার্ড", + "pairing-changed": "পেয়ারিং কোড পরিবর্তন করা হয়েছে", + "pairing-code-placeholder": "কোড জোড়া হচ্ছে ...", + "pairing-code_error1": "পেয়ারিং কোড মেলে না", + "pairing-go-to-installation": "জোড়া সেটিংসে যান", + "pairing-maximum-number-reached-content": "নতুন কোনও সক্ষম করার আগে দয়া করে আপনার একটি ডিভাইস অক্ষম করুন।", + "pairing-maximum-number-reached-title": "ডিভাইসের সর্বোচ্চ সংখ্যা পৌঁছেছে", + "pairing-new-installation-detected-content": "একটি নতুন ডিভাইস সনাক্ত করা হয়েছে।\n আপনার ডিভাইসগুলি সঠিকভাবে ব্যবহার করতে, তাদের ব্যবহারের আগে তাদের জোড়া এবং সক্ষম করা জরুরী।\n আপনার ডিভাইসগুলি যুক্ত করতে দয়া করে সেটিংসের অধীনে ডিভাইস বিভাগে যান।", + "pairing-new-installation-detected-title": "নতুন ডিভাইস শনাক্ত করা হয়েছে", + "pairing-no-info": "কোন তথ্য নেই", + "pairing-please-set-a-name": "অনুগ্রহ করে আপনার ডিভাইসের জন্য একটি নাম নির্ধারণ করুন।", + "passphrase": "পাসফ্রেজ", + "password": "পাসওয়ার্ড", + "password-description": "কমপক্ষে ৬ অক্ষর. আপনার পাসওয়ার্ড আপনার চাবিকে রক্ষা করে। স্ট্যাটাস আনলক করতে এবং লেনদেনের জন্য আপনার এটি প্রয়োজন।", + "password-mismatch": "নতুন পাসওয়ার্ড এবং নিশ্চিতকরণ মেলে না", + "password-placeholder": "পাসওয়ার্ড।।।", + "password-placeholder2": "আপনার পাসওয়ার্ড নিশ্চিত করুন", + "password-reset-in-progress": "পাসওয়ার্ড পরিবর্তন করা হচ্ছে ...", + "password-reset-success": "পাসওয়ার্ড পরিবর্তিত হয়েছে", + "password-reset-success-message": "আপনাকে আবার সাইন ইন করতে হবে", + "password_error1": "পাসওয়ার্ড মেলে না।", + "paste": "আটকান", + "paste-json": "JSON প্রতিলেপন করুন", + "pay-to-chat": "চ্যাট করতে টাকা দিন", + "peer-content": "Status চ্যাট নেটওয়ার্কের সাথে সংযুক্ত একটি ডিভাইস। প্রত্যেক ব্যবহারকারী তাদের ডিভাইসের সংখ্যার উপর নির্ভর করে এক বা একাধিক সমবয়সীদের প্রতিনিধিত্ব করতে পারে।", + "peer-title": "সমকক্ষ", + "peer-to-peer": "পিয়ার টু পিয়ার", + "peers": "সহকর্মীরা", + "pending": "বিচারাধীন", + "pending-confirmation": "অমীমাংসিত নিশ্চিতকরণ...", + "pending-invitations": "সদস্যপদের অনুরোধ স্থগিত", + "per-gas-price-limit": "প্রতি গ্যাস মূল্য সীমা", + "per-gas-tip-limit": "প্রতি গ্যাস টিপ সীমা", + "perform-backup": "ব্যাকআপ সঞ্চালন", + "permissions": "অনুমতি", + "phone-e164": "আন্তর্জাতিক ১", + "photos": "ছবি", + "photos-access-error": "প্রয়োজনীয় ফটো অনুমতি দিতে, অনুগ্রহ করে আপনার সিস্টেম সেটিংসে যান এবং নিশ্চিত করুন যে Status > ফটো নির্বাচন করা হয়েছে।", + "pin": "পিন", + "pin-changed": "৬-ডিজিটের পাসকোড পরিবর্তন করা হয়েছে", + "pin-code": "৬-ডিজিটের পাসকোড", + "pin-limit-reached": "পিনের সীমায় পৌঁছেছে। প্রথমে একটি পূর্ববর্তী বার্তা আনপিন করুন।", + "pin-mismatch": "ভুল পাসকোড", + "pin-one-attempt": "এক চেষ্টা", + "pin-one-attempt-blocked-after": "আপনার কিকার্ড ব্লক হওয়ার আগে", + "pin-one-attempt-blocked-before": "সাবধানে থাকুন, আপনার একটি মাত্র সুযোগ আছে", + "pin-one-attempt-frozen-after": "আপনার কিকার্ড হিমশীতল হওয়ার আগে", + "pin-one-attempt-frozen-before": "সাবধানে থাকুন, আপনার একটি মাত্র সুযোগ আছে", + "pin-retries-left": "{{number}} বাকি আছে", + "pinned-by": "যার দ্বারা পিন করা হয়েছে", + "pinned-messages": "পিন করা বার্তাগুলি", + "pinned-messages-count": { + "one": "১টি পিনড বার্তা", + "other": "পিন করা বার্তাগুলি" + }, + "pinned-messages-empty": "পিন করা বার্তাগুলি এখানে প্রদর্শিত হবে। একটি বার্তা পিন করতে, এটি টিপুন এবং ধরে রাখুন এবং `পিন` আলতো চাপুন", + "powered-by-paraswap": "Parswap দ্বারা চালিত", + "preference": "অগ্রাধিকার", + "preview-privacy": "প্রাইভেসি মোডের পূর্বরূপ দেখুন", + "previewing-may-share-metadata": "এই ওয়েবসাইটথেকে লিংক প্রাকদর্শন তাদের মালিকদের সাথে আপনার মেটাডাটা শেয়ার করতে পারে", + "price-impact": "দামের প্রভাব", + "price-impact-desc": "এই লেনদেনের জন্য আনুমানিক মূল্য প্রভাব. যদি বর্তমান ব্লকের বেস ফি এর বেশি হয়, তাহলে আপনার লেনদেন নিম্ন ভিত্তি ফি সহ নিম্নলিখিত ব্লকে অন্তর্ভুক্ত করা হবে।", + "principles": "নীতি", + "priority": "অগ্রাধিকার", + "privacy": "গোপনীয়তা", + "privacy-and-security": "গোপনীয়তা এবং নিরাপত্তা", + "privacy-photos": "প্রোফাইল ছবির গোপনীয়তা", + "privacy-policy": "গোপনীয়তা নীতি", + "privacy-show-to-warning": "যারা ইতিমধ্যে আপনার প্রোফাইল ছবি দেখেছেন তারা তা করতে থাকবে", + "private-key": "ব্যক্তিগত কী", + "private-notifications": "ব্যক্তিগত বিজ্ঞপ্তি", + "private-notifications-descr": "স্ট্যটাস আপনাকে নতুন বার্তাগুলি সম্পর্কে অবহিত করবে। আপনি পরে সেটিংসে আপনার বিজ্ঞপ্তি পছন্দগুলি সম্পাদনা করতে পারেন", + "processing": "শুধু একটি মুহূর্ত", + "product-information": "পণ্যের তথ্য", + "profile": "প্রোফাইল", + "profile-deleted-content": "আপনার প্রোফাইল সফলভাবে মুছে ফেলা হয়েছে", + "profile-deleted-keycard": "আপনি এখন আপনার কীকার্ডে অন্য কীপেয়ার পুনরুদ্ধার করতে পারেন", + "profile-deleted-title": "প্রোফাইল মুছে ফেলা হয়েছে", + "profile-details": "প্রোফাইল বিশদ", + "profile-not-found": "প্রোফাইল পাওয়া যায়নি", + "profile-pic-pick": "গ্যালারী থেকে নির্বাচন করুন", + "profile-pic-remove": "ছবি সরান", + "profile-pic-take": "ছবি তুলুন", + "profile-picture-updated": "প্রোফাইল ছবি আপডেট করা হয়েছে", + "public-channel": "পাবলিক চ্যানেল", + "public-chat": "পাবলিক চ্যাট", + "public-chat-description": "আপনার স্বার্থের জন্য পাবলিক চ্যাটে যোগ দিন! যে কেউ নতুন শুরু করতে পারে।", + "public-chats": "পাবলিক চ্যাট", + "public-group-status": "পাবলিক", + "public-group-topic": "বিষয়", + "public-key": "পাবলিক কী", + "puk-and-pairing-codes-displayed": "PUK এবং জোড়া কোড প্রদর্শিত হয়েছে", + "puk-changed": "12-সংখ্যার PUK পরিবর্তন করা হয়েছে", + "puk-code": "PUK কোড", + "puk-code-explanation": "আপনি যদি আপনার ৬ ডিজিটের পাসকোড ভুলে যান অথবা ভুলভাবে ৩ বার প্রবেশ করান, তাহলে আপনার কার্ড আনলক করার জন্য এই কোডের প্রয়োজন হবে।", + "puk-mismatch": "ভুল PUK কোড", + "push-failed-transaction": "আপনার লেনদেন ব্যর্থ হয়েছে", + "push-failed-transaction-body": "{{মান}} {{মুদ্রা}} থেকে {{থেকে}}", + "push-inbound-transaction": "আপনি {{মান}} {{মুদ্রা}} পেয়েছেন", + "push-inbound-transaction-body": "{{old}} থেকে {{new}} পর্যন্ত পরিমাণ পরিবর্তন করা হয়েছে", + "push-notifications-server-enabled": "সার্ভার সক্রিয় করা হয়েছে", + "push-notifications-servers": "বিজ্ঞপ্তি সার্ভারগুলি পুশ করুন", + "push-outbound-transaction": "আপনি {{value}} {{currency}} প্রেরণ করেছেন", + "push-outbound-transaction-body": "{{old}} থেকে {{new}} পর্যন্ত পরিমাণ পরিবর্তন করা হয়েছে", + "quiet-days": "{{quiet-days}} দিন", + "quiet-hours": "{{quiet-hours}} ঘন্টা", + "re-encrypt-key": "আপনার কীগুলি পুনরায় এনক্রিপ্ট করুন", + "rearrange-categories": "শ্রেণীবিন্যাস পুনর্বিন্যাস করুন", + "receive": "প্রাপ্তি", + "receive-transaction": "লেনদেন গ্রহণ করুন", + "recent": "সাম্প্রতিক", + "recent-empty": "সম্প্রতি ব্যবহৃত ঠিকানাএখানে প্রদর্শিত হবে", + "recent-recipients": "যোগাযোগ", + "recently-used-stickers": "সম্প্রতি ব্যবহৃত স্টিকার গুলো এখানে প্রদর্শিত হবে", + "recipient": "প্রাপক", + "recipient-code": "প্রাপকের ঠিকানা লিখুন", + "recipient-code-placeholder": "0x... অথবা username.domain.eth", + "recover": "পুনরুদ্ধার", + "recover-key": "বিদ্যমান কীসমূহ অ্যাক্সেস করুন", + "recover-keycard-multiaccount-not-supported": "এই একাউন্টের জন্য কী ইতোমধ্যে বিদ্যমান এবং পুনরায় যুক্ত করা যাবে না। আপনি যদি আপনার পাসওয়ার্ড, পাসকোড বা কীকার্ড হারিয়ে ফেলেন, অ্যাপটি আনইনস্টল করুন, আপনার বীজ বাক্যাংশ প্রবেশ করিয়ে আপনার কী পুনরায় ইনস্টল করুন এবং অ্যাক্সেস করুন", + "recover-with-keycard": "কীকার্ড দিয়ে পুনরুদ্ধার করুন", + "recover-with-seed-phrase": "বীজ বাক্যাংশ দিয়ে পুনরুদ্ধার করুন", + "recovering-key": "কীগুলি অ্যাক্সেস করা হচ্ছে ...", + "recovery-confirm-phrase": "বীজ বাক্যাংশ নিশ্চিত করুন", + "recovery-phrase": "বীজ বাক্যাংশ", + "recovery-success-text": "আপনার কীপুনরায় এনক্রিপ্ট করতে আপনাকে একটি নতুন কোড বা পাসওয়ার্ড তৈরি করতে হবে", + "recovery-typo-dialog-description": "অনুগ্রহ করে লক্ষ্য করুন, আপনার বীজ বাক্যাংশ টি পাওয়ার সাথে সাথে ঠিক একই শব্দ ব্যবহার করতে হবে এবং অর্ডার করতে হবে", + "recovery-typo-dialog-title": "বীজের বাক্যাংশটি কি সঠিক?", + "redeem-amount": "{{পরিমাণ}} বোনাস উপলব্ধ", + "redeem-now": "এখনি তুলুন", + "redeem-success": "বোনাস সাফল্য উদ্ধার করুন!", + "reduced-tip": "অগ্রাধিকার টিপ হ্রাস করা হবে", + "refresh": "সতেজ", + "registered": "নিবন্ধিত", + "reject": "প্রত্যাখ্যান করুন", + "reject-and-delete": "প্রত্যাখ্যান করুন এবং মুছুন", + "remember-me": "আমাকে মনে রেখো", + "remind-me-later": "আমাকে এটা আবার দেখাও", + "remote-notifications": "দূরবর্তী বিজ্ঞপ্তিগুলি", + "remote-notifications-subtitle": "Google পুশ বিজ্ঞপ্তিগুলি সক্ষম করুন", + "remove": "অপসারণ", + "remove-favourite": "প্রিয়টি সরান", + "remove-from-chat": "চ্যাট থেকে অপসারণ করুন", + "remove-from-contacts": "পরিচিতি থেকে অপসারণ করুন", + "remove-from-contacts-text": "আপনার পরিচিতি তালিকা থেকে কোনও ব্যবহারকারীকে সরিয়ে আপনি তাদের থেকে আপনার ওয়ালেট ঠিকানাটি গোপন করবেন না", + "remove-group": "গ্রুপ অপসারণ করুন", + "remove-network": "নেটওয়ার্ক অপসারণ করুন", + "remove-token": "টোকেন অপসারণ করুন", + "removed": "অপসারণ", + "repeat-pin": "নতুন ৬-সংখ্যার পাসকোড পুনরাবৃত্তি করুন", + "repeat-puk": "নতুন 12-সংখ্যা PUK পুনরাবৃত্তি করুন৷", + "replying-to": "{{author}} কে উত্তর দেওয়া হচ্ছে", + "report-bug-email-template": "1. ইস্যু বিবরণ (আপনি যে বৈশিষ্ট্যটি চান তা বর্ণনা করুন, অথবা সংক্ষেপে বাগ এবং আপনি কি করেছেন, আপনি কি আশা করেছিলেন, এবং আসলে কি ঘটবে তা বর্ণনা করুন। নিচের বিভাগসমূহ) 2. প্রজননের পদক্ষেপ (বর্ণনা করুন কিভাবে আমরা ধাপে ধাপে বাগ প্রতিলিপি করতে পারি। - Status খুলুন -... - ধাপ ৩, ইত্যাদি। 3. প্রত্যাশিত আচরণ (আপনি যা আশা করেছেন তা বর্ণনা করুন। 4. প্রকৃত আচরণ (আসলে কি ঘটেছে তা বর্ণনা করুন। 5. সমস্যা ডেমো করতে পারে দয়া করে এমন স্ক্রিনশট সংযুক্ত করুন।", + "request-access": "অ্যাক্সেসের অনুরোধ করুন", + "request-feature": "একটি বৈশিষ্ট্য অনুরোধ", + "request-membership": "সদস্যতার জন্য অনুরোধ", + "request-pending": "মুলতুবি অনুরোধ…", + "request-transaction": "অনুরোধ লেনদেন", + "required-field": "প্রয়োজনীয় ক্ষেত্র", + "resend-message": "পুনরায় পাঠান", + "reset-card": "কার্ড রিসেট করুন", + "reset-card-description": "এই অপারেশন কার্ডটিকে প্রাথমিক অবস্থায় রিসেট করবে। এটি ব্যক্তিগত কী সহ সমস্ত কার্ড ডেটা মুছে ফেলবে। অপারেশন বিপরীত হবে না।", + "reset-database": "ডাটাবেস রিসেট করুন", + "reset-database-warning": "চ্যাট, পরিচিতি এবং সেটিংস মুছুন। যখন আপনি আপনার পাসওয়ার্ড হারিয়ে ফেলবেন তখন এটি প্রয়োজনীয়", + "reset-database-warning-keycard": "চ্যাট, পরিচিতি এবং সেটিংস মুছুন।", + "reset-password": "পাসওয়ার্ড রিসেট করুন", + "restore-defaults": "ডিফল্টগুলি পুনর্বহাল করুন", + "retry": "পুনরায় চেষ্টা করা", + "revoke-access": "প্রবেশ বাতিল করুন", + "rpc-url": "আরপিসি ইউআরএল", + "rpc-usage-copy": "অনুলিপি", + "rpc-usage-filter": "ফিল্টার পদ্ধতি", + "rpc-usage-get-stats": "সতেজ", + "rpc-usage-info": "RPC ব্যবহারের পরিসংখ্যান", + "rpc-usage-reset": "রিসেট", + "safe-estimate": "নিরাপদ অনুমান", + "save": "সংরক্ষণ", + "save-password": "পাসওয়ার্ড সংরক্ষণ করুন", + "save-password-unavailable": "পাসওয়ার্ড সংরক্ষণ করতে ডিভাইস পাসকোড নির্ধারণ করুন", + "save-password-unavailable-android": "গুপ্ত-সংকেত সংরক্ষণ অনুপলভ্য: আপনার ডিভাইসটি মূল হতে পারে অথবা প্রয়োজনীয় নিরাপত্তা বৈশিষ্ট্যের অভাব হতে পারে.", + "scan-qr": "কিউআর কোড স্ক্যান করুন", + "scan-qr-code": "একটি ওয়ালেট ঠিকানা সহ একটি QR কোড স্ক্যান করুন", + "scan-tokens": "টোকেন স্ক্যান করুন", + "search": "অনুসন্ধান করুন", + "search-no-chat-found": "কোন সন্ধান ফলাফল নেই. তুমি কি বলতে চাচ্ছো", + "secret-keys-confirmation-text": "আপনি যদি কখনো আপনার ফোন হারিয়ে ফেলেন তাহলে আপনার কীকার্ড ব্যবহার চালিয়ে যেতে হবে।", + "secret-keys-confirmation-title": "কোড গুলো লিখেছেন?", + "security": "নিরাপত্তা", + "see-details": "বিস্তারিত দেখুন", + "see-it-again": "এটি আবার দেখুন", + "see-suggestions": "পরামর্শ দেখুন", + "seed-key-uid-mismatch": "বীজ মিলছে না", + "seed-key-uid-mismatch-desc-1": "আপনার লেখা বীজ বাক্যাংশ এর সাথে মেলে না", + "seed-key-uid-mismatch-desc-2": "এই অ্যাকাউন্টের কীগুলি পরিচালনা করতে আপনার বীজ বাক্যাংশ যাচাই করুন এবং আবার চেষ্টা করুন।", + "seed-phrase-content": "তালিকা থেকে এলোমেলোভাবে নির্বাচিত এবং অন্যান্য মানিব্যাগ এবং ডিভাইসে আপনার Ethereum অ্যাকাউন্ট পুনরুদ্ধার বা অ্যাক্সেস করতে ব্যবহৃত. এছাড়াও ক্রিপ্টো বাস্তুতন্ত্র জুড়ে একটি \"স্মৃতিচারণমূলক বাক্যাংশ\", \"পুনরুদ্ধার বাক্যাংশ\" বা \"ওয়ালেট ব্যাকআপ\" হিসেবে উল্লেখ করা হয়। অধিকাংশ ক্রিপ্টো অ্যাপ অ্যাকাউন্ট তৈরি করতে এই একই মান ব্যবহার করে।", + "seed-phrase-placeholder": "বীজ বাক্যাংশ ...", + "seed-phrase-title": "বীজ বাক্যাংশ", + "select": "নির্বাচন করুন", + "select-account": "অ্যাকাউন্ট নির্বাচন করুন", + "select-account-dapp": "Dapps-এর সাথে আপনি যে অ্যাকাউন্টটি ব্যবহার করতে চান তা নির্বাচন করুন", + "select-account-first": "প্রথমে একটি অ্যাকাউন্ট নির্বাচন করুন", + "select-chat": "বার্তা শুরু করতে চ্যাট নির্বাচন করুন", + "select-new-location-for-keys": "আপনার ব্যক্তিগত কী (গুলি) সংরক্ষণ করতে একটি নতুন অবস্থান নির্বাচন করুন", + "select-token-to-receive": "গ্রহণ করতে টোকেন নির্বাচন করুন", + "select-token-to-swap": "সোয়াপ করতে টোকেন নির্বাচন করুন", + "selected": "নির্বাচিত", + "send-logs": "একটি বাগ রিপোর্ট করুন", + "send-logs-to": "{{email}} এ একটি বাগ প্রতিবেদন করুন", + "send-message": "বার্তা পাঠান", + "send-push-notifications": "পুশ বিজ্ঞপ্তিগুলি পাঠান", + "send-push-notifications-description": "যখন নিষ্ক্রিয় করা হবে, যে ব্যক্তি আপনার বার্তা গ্রহণ করছে তাকে তাদের আগমন সম্পর্কে অবহিত করা হবে না", + "send-request": "অনুরোধ প্রেরণ করুন", + "send-request-amount": "পরিমান", + "send-request-amount-max-decimals": "দশমিক সংখ্যা সর্বোচ্চ {{asset-decimals}}", + "send-request-unknown-token": "অজানা টোকেন - {{asset}}", + "send-sending-to": "\t\n{{recipient-name}} to", + "send-transaction": "লেনদেন প্রেরণ করুন", + "sending": "প্রেরণ", + "sent-at": "পাঠানো হয়েছে", + "server": "সার্ভার", + "set-a-topic": "একটি বিষয় তৈরি করুন", + "set-currency": "মুদ্রা নির্ধারণ করুন", + "set-custom-fee": "কাস্টম ফি নির্ধারণ করুন", + "set-dapp-access-permissions": "DApp অ্যাক্সেস অনুমতি নির্ধারণ করুন", + "set-max": "সর্বোচ্চ নির্ধারণ করুন", + "settings": "সেটিংস", + "share": "ভাগ করুন", + "share-address": "ঠিকানা শেয়ার করুন", + "share-chat": "চ্যাট শেয়ার করুন", + "share-contact-code": "আমার চ্যাট কী শেয়ার করুন", + "share-dapp-text": "Status আমি যে DApp ব্যবহার করছি তা দেখুন: {{link}}", + "share-link": "শেয়ার লিংক", + "share-logs": "লগগুলি শেয়ার করুন", + "share-my-profile": "আমার প্রোফাইল শেয়ার করুন", + "share-profile": "প্রোফাইল শেয়ার করুন", + "share-profile-link": "প্রোফাইল লিংক শেয়ার করুন", + "share-public-chat-text": "Status অ্যাপে এই সর্বজনীন চ্যাটটি দেখুন: {{link}}", + "shared": "অংশীদারি", + "sharing-copied-to-clipboard": "অনুলিপি", + "sharing-copy-to-clipboard": "অনুলিপি", + "sharing-data-desc-1": "কোনও সংবেদনশীল তথ্য পাঠানো হয় না তা নিশ্চিত করার জন্য জনসাধারণের নিয়মের বিরুদ্ধে ডেটা বৈধ করা হয়। বিশ্বাস করবেন না, যাচাই করুন।", + "sharing-data-desc-2": "ব্যবহারের তথ্য স্ট্যাটাসের পিয়ার-টু-পিয়ার নেটওয়ার্কের উপর এনক্রিপ্ট করা এন্ড-টু-এন্ড পাঠানো হয়", + "sharing-data-desc-3": "আপনার নিয়মিত চ্যাট কী এর পরিবর্তে, একটি একক ব্যবহার কী ব্যবহার করা হয়", + "sharing-data-desc-4": "ব্যবহারের তথ্য আপনার আইপি ঠিকানার সাথে যুক্ত করা যাবে না", + "sharing-data-desc-5": "সকল ব্যবহারকারীর সমষ্টিগত তথ্য সর্বজনীনভাবে উপলব্ধ", + "sharing-data-desc-6": "ডেটা পাঠানোর পরে আপনার ফোন থেকে সরানো হয়", + "sharing-share": "ভাগ করুন", + "show-less": "কম দেখাও", + "show-more": "আরও দেখান", + "show-notifications": "বিজ্ঞপ্তিগুলি দেখান", + "show-profile-pictures": "এখান থেকে প্রোফাইল ছবি দেখুন", + "show-profile-pictures-to": "প্রোফাইল শেয়ার করুন", + "show-qr": "QR কোড দেখান", + "show-transaction-data": "লেনদেনের তথ্য দেখান", + "sign-and-send": "স্বাক্ষর করুন এবং প্রেরণ করুন", + "sign-anyway": "যাই হোক স্বাক্ষর করুন", + "sign-in": "আনলক করুন", + "sign-message": "বার্তা স্বাক্ষর করুন", + "sign-out": "সাইন আউট করুন", + "sign-request-failed": "বার্তায় স্বাক্ষর করা যায়নি", + "sign-with": "সাথে সাইন ইন করুন", + "sign-with-password": "পাসওয়ার্ড দিয়ে সাইন ইন করুন", + "sign-you-in": "আপনাকে স্বাক্ষর করা হচ্ছে...", + "signing": "স্বাক্ষর", + "signing-a-message": "একটি বার্তায় স্বাক্ষর করা হচ্ছে", + "signing-phrase": "স্বাক্ষর বাক্যাংশ", + "skip": "এড়িয়ে যান", + "slippage": "স্লিপেজ", + "slow": "ধীর", + "something-went-wrong": "কিছু ভুল হয়েছে", + "soon": "শীঘ্রই", + "specify-address": "ঠিকানা উল্লেখ করুন", + "specify-name": "একটি নাম উল্লেখ করুন", + "specify-network-id": "নেটওয়ার্ক আইডি নির্দিষ্ট ভাবে উল্লেখ করুন", + "specify-rpc-url": "একটি RPC URL উল্লেখ করুন", + "specify-server-public-key": "সার্ভার সর্বজনীন কী প্রবেশ করান", + "specify-symbol": "একটি প্রতীক উল্লেখ করুন", + "start-chat": "চ্যাট শুরু করুন", + "start-conversation": "কথোপকথন শুরু করুন", + "start-group-chat": "গ্রুপ চ্যাট শুরু করুন", + "start-new-chat": "নতুন চ্যাট শুরু করুন", + "starter-pack-coming": "স্টার্টার প্যাক আপনার পথে আসছে", + "starter-pack-coming-description": "কয়েক মিনিট থেকে কয়েক ঘন্টা সময় লাগতে পারে", + "starter-pack-received": "স্টার্টার প্যাক গৃহীত হয়েছে", + "starter-pack-received-description": "এখানে আপনাকে শুরু করার জন্য কিছু ক্রিপ্টো আছে! স্টিকার, একটি ENS নাম পেতে এটি ব্যবহার করুন এবং DApps চেষ্টা করুন।", + "status": "Status", + "status-always-online": "সবসময় অনলাইন", + "status-automatic": "স্বয়ংক্রিয়", + "status-automatic-subtitle": "স্বয়ংক্রিয়ভাবে স্থিতি সেট করুন", + "status-confirmed": "নিশ্চিত করা হয়েছে", + "status-dnd": "বিরক্ত করবেন না", + "status-dnd-subtitle": "সমস্ত বিজ্ঞপ্তি নিঃশব্দ করে", + "status-hardwallet": "Status hardwallet", + "status-inactive": "নিষ্ক্রিয়", + "status-inactive-subtitle": "আপনার অনলাইন স্ট্যাটাস লুকিয়ে রাখে", + "status-is-open-source": "স্ট্যাটাস ওপেন সোর্স", + "status-keycard": "Status কীকার্ড", + "status-mobile-descr": "চ্যাট সিঙ্ক করার সময় স্ট্যাটাস অনেক ডেটা ব্যবহার করে। মোবাইল নেটওয়ার্কে থাকাকালীন আপনি সিঙ্ক না করা বেছে নিতে পারেন৷", + "status-not-sent-click": "নিশ্চিত করা হয়নি। পছন্দসমূহের জন্য ক্লিক করুন", + "status-not-sent-tap": "নিশ্চিত করা হয়নি। পছন্দসমূহের জন্য ট্যাপ করুন", + "status-pending": "বিচারাধীন", + "status-sent": "প্রেরিত", + "status-tx-not-found": "TX খুঁজে পাওয়া যায়নি", + "status-updates-descr": "Status হালনাগাদসমূহ এখানে প্রদর্শিত হবে। আপনার সময়রেখায় হালনাগাদসমূহ পেতে পরিচিতি হিসেবে প্রোফাইলটি যুক্ত করুন।", + "statuses-descr": "আপনার মনে যা আছে তা ভাগ করুন এবং আপনার পরিচিতিগুলির সাথে আপডেট থাকুন", + "statuses-my-status-descr": "\t\nআপনার মনে যা আছে তা ভাগ করুন। আপনার প্রোফাইল দেখার জন্য যে কেউ আপনার Status দেখতে সক্ষম হবে। আপনাকে যোগাযোগ হিসাবে যুক্ত করা লোকেরা তাদের টাইমলাইনে আপনার আপডেটগুলি গ্রহণ করবে", + "step-i-of-n": "{{number}} এর পদক্ষেপ {{step}} {{number}}", + "sticker": "স্টিকার", + "sticker-market": "স্টিকার বাজার", + "storage": "সঞ্চয়", + "submit": "জমা দিন", + "submit-bug": "একটি বাগ জমা দিন", + "success": "সাফল্য", + "suggested-min-tip": "প্রস্তাবিত min. tip", + "suggested-price-limit": "প্রস্তাবিত মূল্য সীমা", + "swap": "অদলবদল", + "swap-details": "অদলবদল বিবরণ", + "switch-to-simple-interface": "সাধারণ ইন্টারফেসে স্যুইচ করুন", + "symbol": "প্রতীক", + "sync-all-devices": "সকল ডিভাইস এককালবর্তী করুন", + "sync-in-progress": "এককালবর্তী করা হচ্ছে...", + "sync-settings": "এককালবর্তীকরণ সেটিংস", + "sync-synced": "সিংক্রোনাইজে", + "syncing-devices": "এককালবর্তী করা হচ্ছে...", + "system": "পদ্ধতি", + "tabs": "ট্যাব", + "tag-was-lost": "ট্যাগ হারিয়ে গেছে", + "tap-card-again": "কার্ডটি আবার আপনার ফোনের পিছনে ট্যাপ করুন", + "terms-of-service": "ব্যবহারের শর্তাবলী", + "test-networks": "নেটওয়ার্ক পরীক্ষা করুন", + "text-input-disabled": "দয়া করে একমিনিট অপেক্ষা করো।।।", + "thank-you": "ধন্যবাদ", + "this-device": "এই যন্ত্রটি", + "this-device-desc": "আপনার কীগুলি সঙ্কেতায়িত এবং নিরাপদে আপনার ডিভাইসে সংরক্ষণ করা হবে", + "this-is-you-signing": "এটা আপনার স্বাক্ষর বাক্যাংশ", + "this-will-take-few-seconds": "এটি কয়েক সেকেন্ড সময় লাগবে", + "three-days": "তিন দিন", + "three-words-description": "প্রতিটি লেনদেনে স্বাক্ষর করার আগে আপনার এই ৩টি শব্দ দেখা উচিত", + "three-words-description-2": "আপনি যদি একটি ভিন্ন সমন্বয় দেখেন, লেনদেন বাতিল করুন এবং সাইন আউট করুন", + "timeline": "টাইমলাইন", + "tip-cap": "টিপ ক্যাপ", + "to": "করতে", + "to-block": "ব্লক", + "to-enable-biometric": "{{bio-type-label}} সক্রিয় করতে, আপনাকে অবশ্যই আনলক স্ক্রীনে আপনার পাসওয়ার্ড সংরক্ষণ করতে হবে", + "to-encrypt-enter-password": "অ্যাকাউন্ট এনক্রিপ্ট করতে আপনার পাসওয়ার্ড দিন", + "to-see-this-message": "এই বার্তাটি দেখতে,", + "token": "টোকেন", + "token-auto-validate-decimals-error": "ঠিকানা {{address}} ঠিকানায় টোকেন {{symbol}} এর জন্য ভুল দশমিক - {{expected}} সেট করা হয়েছে কিন্তু {{actual}} হিসাবে শনাক্ত করা হয়েছে", + "token-auto-validate-name-error": "ঠিকানায় {{symbol}} ঠিকানায় টোকেনের জন্য ভুল নাম {{address}} - {{expected}} সেট করা হয়েছে কিন্তু {{actual}} হিসাবে শনাক্ত করা হয়েছে", + "token-auto-validate-symbol-error": "ঠিকানায় {{symbol}} ঠিকানায় টোকেনের জন্য ভুল নাম {{address}} - {{expected}} সেট করা হয়েছে কিন্তু {{actual}} হিসাবে শনাক্ত করা হয়েছে", + "token-details": "টোকেন বিস্তারিত", + "topic-name-error": "কেবল ছোট হাতের অক্ষর (a to z), সংখ্যা এবং ড্যাশ (-) ব্যবহার করুন। চ্যাট কীগুলি ব্যবহার করবেন না", + "total-gas": "মোট গ্যাস", + "transaction": "লেনদেন", + "transaction-data": "লেনদেনের তথ্য", + "transaction-declined": "লেনদেন প্রত্যাখ্যান", + "transaction-description": "নেটওয়ার্কে 12 টি নিশ্চিতকরণের পরে এটি সম্পূর্ণ বিবেচনা করুন।", + "transaction-details": "লেনদেনের বিস্তারিত", + "transaction-failed": "লেনদেন ব্যর্থ হয়েছে", + "transaction-fee": "লেনদেন ফি", + "transaction-history": "লেনদেনের ইতিহাস", + "transaction-request": "লেনদেনের অনুরোধ", + "transaction-sent": "লেনদেন পাঠানো হয়েছে", + "transaction-signed": "লেনদেন সফলভাবে স্বাক্ষরিত হয়েছে", + "transactions": "লেনদেন", + "transactions-filter-select-all": "সব নির্বাচন করুন", + "transactions-filter-title": "ইতিহাস ফিল্টার করুন", + "transactions-history": "লেনদেনের ইতিহাস", + "transactions-history-empty": "আপনার ইতিহাসে এখনো কোন লেনদেন হয়নি", + "transactions-history-loading": "লেনদেনের ইতিহাস লোড করা হচ্ছে। এতে কিছু সময় লাগতে পারে।", + "transactions-load-more": "আরও লোড করুন", + "transactions-management-enabled": "লেনদেন ব্যবস্থাপনা (আলফা)", + "transactions-sign": "চিহ্ন", + "transfer-ma-unknown-error-desc-1": "মনে হচ্ছে আপনার মাল্টি অ্যাকাউন্ট মুছে ফেলা হয়নি। ডেটাবেস রিসেট করা হতে পারে", + "transfer-ma-unknown-error-desc-2": "অনুগ্রহ করে আপনার অ্যাকাউন্টের তালিকা টি পরীক্ষা করুন এবং আবার চেষ্টা করুন। যদি অ্যাকাউন্টটি তালিকাভুক্ত না হয় তবে বীজ বাক্যাংশের সাথে পুনরুদ্ধার করতে বিদ্যমান কীগুলিতে অ্যাক্সেস করুন", + "transfers-fetching-failure": "স্থানান্তরের ইতিহাস আপডেট করা যায়নি। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করতে নীচে টানুন", + "tribute-required-by-multiaccount": "{{multiaccount-name}} জন্য একটি চ্যাট শুরু করার জন্য SNT প্রয়োজন।", + "tribute-state-paid": "শ্রদ্ধা নিবেদন", + "tribute-state-pending": "শ্রদ্ধা নিবেদন মুলতুবি", + "tribute-state-required": "{{snt-ammount}} SNT শ্রদ্ধাঞ্জলি প্রয়োজন", + "tribute-to-talk": "কথা বলার প্রতি শ্রদ্ধা", + "tribute-to-talk-add-friends": "শ্রদ্ধা নিবেদন ছাড়াই চ্যাটগুলিকে অনুমতি দেওয়ার জন্য বন্ধুদের যোগাযোগ হিসাবে যুক্ত করুন।", + "tribute-to-talk-are-you-friends": "তুমরা কী বন্ধু?", + "tribute-to-talk-ask-to-be-added": "একটি যোগাযোগ হিসাবে যুক্ত করতে বলুন", + "tribute-to-talk-contact-received-your-tribute": " তোমার শ্রদ্ধা ঞ্জলি পেয়েছি। আপনি এখন নিরাপদে একে অপরের সাথে চ্যাট করতে পারেন।", + "tribute-to-talk-desc": "নতুন দের জন্য SNT প্রয়োজন দ্বারা আপনার মনোযোগ আকর্ষণ করুন একটি চ্যাট শুরু করার জন্য", + "tribute-to-talk-disabled": "প্রতিবন্ধী কথা বলার প্রতি শ্রদ্ধা", + "tribute-to-talk-disabled-note": "এখন থেকে, নতুন রা SNT না পাঠিয়ে আপনার সাথে চ্যাট শুরু করতে পারবেন।", + "tribute-to-talk-enabled": "আপনার কথা বলার প্রতি শ্রদ্ধা নিবেদন সক্রিয় আছে।", + "tribute-to-talk-finish-desc": "এখন থেকে, আপনি শুধুমাত্র যোগাযোগ থেকে চ্যাট পাবেন, এবং যারা অর্থ প্রদান করেছেন ", + "tribute-to-talk-learn-more-1": "আপনার সময় এবং মনোযোগ আপনার সবচেয়ে মূল্যবান সম্পদ। টকের প্রতি শ্রদ্ধা নিবেদন আপনাকে নতুন দের সাথে চ্যাট শুরু করার জন্য প্রয়োজনীয় একটি পরিমাণ SNT সেট করতে দেয়।", + "tribute-to-talk-learn-more-2": "যে কেউ আপনার কন্ট্যাক্ট লিস্টে নেই তাকে টাকা দিতে বলা হবে, এবং একবার তাদের কাছে গেলে আপনি সাড়া দিতে পারেন।", + "tribute-to-talk-learn-more-3": "আপনি সবসময় টাকা ফেরত পাঠাতে পারেন, কিন্তু বন্ধুরা যাতে স্বাধীনভাবে আপনার কাছে পৌঁছাতে পারে তা নিশ্চিত করতে, প্রথমে তাদের যোগাযোগ হিসেবে যোগ করুন।", + "tribute-to-talk-paywall-learn-more-1": "আমাদের সময় এবং মনোযোগ আমাদের সবচেয়ে মূল্যবান সম্পদ। টকের প্রতি শ্রদ্ধা নিবেদন আপনাকে একটি SNT পেমেন্টের বিনিময়ে নতুন দের সাথে যোগাযোগ করতে দেয়।", + "tribute-to-talk-paywall-learn-more-2": "যার একটি শ্রদ্ধাঞ্জলি সেট আছে তার সাথে চ্যাট শুরু করতে, শুধুমাত্র প্রয়োজনীয় SNT পরিশোধ করুন এবং আপনাকে একটি যোগাযোগ হিসেবে যুক্ত করা হবে।", + "tribute-to-talk-paywall-learn-more-3": "আপনি যদি তাদের চেনেন, আপনি Status বাইরে আপনার প্রোফাইল শেয়ার করতে পারেন বিনামূল্যে যুক্ত করতে।", + "tribute-to-talk-pending": "শ্রদ্ধা নিবেদন নিশ্চিতকরণের মুলতুবি", + "tribute-to-talk-pending-note": "শ্রদ্ধাঞ্জলি লেনদেন নেটওয়ার্কে নিশ্চিতকরণ মুলতুবি আছে। আপনি লেনদেনের ইতিহাসে এর Status পরীক্ষা করতে পারেন", + "tribute-to-talk-removing-note": "টক-এর প্রতি শ্রদ্ধা নিবেদন অপসারণ করলে নতুন রা SNT পাঠানো ছাড়াই চ্যাট শুরু করতে পারবে। একটি লেনদেন করা প্রয়োজন।", + "tribute-to-talk-set-snt-amount": "নতুন দের জন্য প্রয়োজনীয় SNT-এর পরিমাণ নির্ধারণ করুন একটি চ্যাট শুরু করার জন্য", + "tribute-to-talk-signing": "লেনদেনে স্বাক্ষর করার জন্য অপেক্ষা করা হচ্ছে", + "tribute-to-talk-transaction-failed-note": "লেনদেন ব্যর্থ হয়েছে এবং আপনার বক্তৃতা সেটিংসের প্রতি শ্রদ্ধা নিবেদন পরিবর্তন করা হয়নি", + "tribute-to-talk-tribute-received1": "শ্রদ্ধা নিবেদন। আপনি এবং", + "tribute-to-talk-tribute-received2": "এখন পরিচিতি এবং একে অপরের সাথে নিরাপদে চ্যাট করতে পারেন।", + "tribute-to-talk-you-require-snt": "নতুন দের জন্য আপনার SNT প্রয়োজন একটি চ্যাট শুরু করার জন্য।", + "try-again": "আবার চেষ্টা করুন", + "try-keeping-the-card-still": "কার্ডটি স্থির রাখার চেষ্টা করুন", + "turn-nfc-description": "এনএফসি ইয়র ডিভাইসে নিষ্ক্রিয়। আপনি সেটিংসে এটি সক্রিয় করতে পারেন", + "turn-nfc-on": "অব্যাহত রাখতে এনএফসি চালু করুন", + "two-minutes": "দুই মিনিট", + "tx-fail-description1": "এই লেনদেন ব্যর্থ হওয়ার সম্ভাবনা আছে। কাস্টম নেটওয়ার্ক ফি ব্যবহার করে আপনার নিজের ঝুঁকিতে স্বাক্ষর করুন।", + "tx-fail-description2": "এই লেনদেন ব্যর্থ হওয়ার সম্ভাবনা আছে। আপনার নিজের ঝুঁকিতে স্বাক্ষর করার জন্য একটি কাস্টম নেটওয়ার্ক ফি নির্ধারণ করুন।", + "type": "প্রকার", + "type-a-message": "বার্তা", + "ulc-enabled": "ইউএলসি সক্ষম হয়েছে", + "unable-to-fetch": "চ্যাটের ইতিহাস আনতে অক্ষম", + "unable-to-read-this-code": "এই কোডটি পড়তে অক্ষম", + "unable-to-send-messages": "বার্তাগুলি পাঠাতে এবং গ্রহণ করতে অক্ষম", + "unblock": "অবরোধ মুক্ত করুন", + "unblock-contact": "এই ব্যবহারকারীকে অবরোধ মুক্ত করুন", + "unknown-status-go-error": "অজানা status-go ত্রুটি", + "unlimited": "সীমাহীন", + "unlock": "আনলক করুন", + "unmute": "নীরব করো না", + "unpair-card": "কার্ড জোড় করা", + "unpair-card-confirmation": "এই অপারেশনটি বর্তমান ডিভাইস থেকে কার্ডটি জোড় করে দেবে। ৬-সংখ্যার পাসকোড অনুমোদন প্রয়োজন। আপনি কি এগিয়ে যেতে চান?", + "unpair-keycard": "এই ফোন থেকে কীকার্ড খুলে ফেলুন", + "unpair-keycard-warning": "আপনার পেয়ারিং কোড\/পিইউকে এবং পিন অপরিবর্তিত থাকবে", + "unpaired-keycard-text": "আপনি যে কীকার্ডটি ট্যাপ করেছেন তা এই ফোনের সাথে সংযুক্ত নয়", + "unpaired-keycard-title": "মনে হচ্ছে আপনার কার্ড আনপেয়ার করা হয়েছে", + "unpin": "আনপিন করুন", + "update": "হালনাগাদ", + "update-to-listen-audio": "এখানে একটি অডিও বার্তা শুনতে সর্বশেষ সংস্করণে হালনাগাদ করুন!", + "update-to-see-image": "এখানে একটি চমৎকার ছবি দেখতে সর্বশেষ সংস্করণে আপডেট করুন!", + "update-to-see-sticker": "এখানে একটি চমৎকার স্টিকার দেখতে সর্বশেষ সংস্করণে আপডেট করুন!", + "updates-to-tos": "ব্যবহারের শর্তাবলীতে আপডেট", + "updates-to-tos-desc": "আপনি চালিয়ে যাওয়ার আগে, অনুগ্রহ করে ব্যবহারের শর্তাবলী পর্যালোচনা করুন এবং নিশ্চিত করুন যে আপনি কীভাবে অ্যাপটি ব্যবহার করেন তার জন্য আপনি সম্পূর্ণ দায়িত্ব নেবেন।", + "url": "ইউআরএল", + "usd-currency": "Usd", + "use-as-profile-picture": "প্রোফাইল ছবি হিসেবে ব্যবহার করুন", + "use-valid-contact-code": "অনুগ্রহ করে একটি বৈধ চ্যাট কী বা ব্যবহারকারীর নাম প্রবেশ করান বা স্ক্যান করুন", + "validation-amount-invalid-number": "পরিমাণ একটি বৈধ সংখ্যা নয়", + "validation-amount-is-too-precise": "পরিমাণ খুব নিখুঁত। দশমিকের সর্বোচ্চ সংখ্যা হল {{decimals}}।", + "verified-community": "✓ যাচাইকৃত সম্প্রদায়", + "version": "অ্যাপ সংস্করণ", + "view": "দেখুন", + "view-cryptokitties": "ক্রিপ্টোকিটিতে দেখুন", + "view-cryptostrikers": "ক্রিপ্টোস্ট্রাইকারে দেখুন", + "view-data": "ডেটা দেখুন", + "view-details": "বিস্তারিত দেখুন", + "view-etheremon": "ইথেরেমনে দেখুন", + "view-gitcoin": "গিটকয়েনে দেখুন", + "view-on-opensea": "OpenSea এ দেখুন", + "view-profile": "প্রোফাইল দেখুন", + "view-public-dashboard": "সর্বজনীন ড্যাশবোর্ড দেখুন", + "view-rules": "নিয়ম দেখুন", + "view-signing": "স্বাক্ষর বাক্যাংশ প্রদর্শন করুন", + "view-superrare": "SuperRare দেখুন", + "waiting-for-wifi": "কোনও ওয়াই-ফাই নেই, বার্তা সিঙ্ক করা অক্ষম হয়েছে।", + "waiting-for-wifi-change": "সেটিংস", + "waiting-to-sign": "লেনদেনে স্বাক্ষর করার জন্য অপেক্ষা করছে...", + "waiting-wi-fi": "Wi-Fi এর জন্য অপেক্ষা করা হচ্ছে...", + "waku-bloom-filter-mode": "ওয়াকু ব্লুম ফিল্টার মোড", + "wakuv2-change-nodes": "আপনি কি নিশ্চিত যে আপনি ওয়াকুভ২ নোডপরিবর্তন করতে চান?", + "wakuv2-node-format": "\/ip4\/ {node-ip} \/tcp\/ {port} \/p2p\/ {id}", + "wakuv2-settings": "Waku v2 সেটিংস", + "wallet": "মানিব্যাগ", + "wallet-address": "মানিব্যাগের ঠিকানা", + "wallet-asset": "সম্পদ", + "wallet-assets": "সম্পদ", + "wallet-backup-recovery-title": "আপনার বীজ বাক্যাংশ ব্যাক আপ করুন", + "wallet-choose-recipient": "প্রাপক চয়ন করুন", + "wallet-collectibles": "সংগ্রহ", + "wallet-connect": "ওয়ালেট সংযোগ", + "wallet-connect-2.0": "Wallet Connect 2.0", + "wallet-connect-app-connected": "সংযুক্ত", + "wallet-connect-go-back": "আপনার ব্রাউজারে বা ড্যাপে ফিরে যান", + "wallet-connect-proposal-description": "সংযোগ করে আপনি আপনার অ্যাকাউন্ট ঠিকানা পুনরুদ্ধার করতে এবং Web3 সক্ষম করার অনুমতি দেন", + "wallet-connect-proposal-title": "আপনার ওয়ালেটের সাথে সংযোগ করতে চাই৷", + "wallet-insufficient-funds": "অপর্যাপ্ত তহবিল", + "wallet-insufficient-gas": "গ্যাসের জন্য পর্যাপ্ত ইটিএইচ নেই", + "wallet-invalid-address": "ভুল ঠিকানা:\n {{data}}", + "wallet-invalid-address-checksum": "ঠিকানায় ত্রুটি:\n {{data}}", + "wallet-invalid-chain-id": "নেটওয়ার্ক মেলে না: \n {{data}} কিন্তু বর্তমান চেইন হচ্ছে {{chain}}", + "wallet-key-content": "Ethereum স্ট্যান্ডার্ডের ভিত্তিতে এবং 0x দিয়ে শুরু করে একটি 64 টি অক্ষরের হেক্স ঠিকানা। জনসমক্ষে, যখন আপনি তহবিল পেতে চান তখন আপনার অ্যাকাউন্টের ঠিকানা অন্যের সাথে ভাগ করা হয়। একটি \"Ethereum ঠিকানা\" বা \"মানিব্যাগের ঠিকানা\" হিসাবেও উল্লেখ করা হয়।", + "wallet-key-title": "অ্যাকাউন্টের ঠিকানা", + "wallet-manage-accounts": "অ্যাকাউন্টগুলি পরিচালনা করুন", + "wallet-manage-app-connections": "অ্যাপ সংযোগ পরিচালনা করুন", + "wallet-manage-assets": "সম্পদ পরিচালনা করুন", + "wallet-request": "অনুরোধ", + "wallet-send": "প্রেরণ", + "wallet-send-min-units": "সর্বনিম্ন 21000 ইউনিট", + "wallet-send-min-wei": "ন্যূনতম 1 ওয়ে", + "wallet-settings": "মানিব্যাগ সেটিংস", + "wallet-total-value": "মোট মূল্য", + "wallet-transaction-total-fee": "মোট ফি", + "wants-to-access-profile": "আপনার প্রোফাইলে অ্যাক্সেস করতে চায়", + "warning": "সতর্কীকরণ", + "warning-message": "দুঃখিত, আমরা স্প্যাম প্রতিরোধ করতে দ্রুত বেশ কিছু বার্তা পাঠাতে সীমাবদ্ধ. অনুগ্রহ করে এক মুহূর্তের মধ্যে আবার চেষ্টা করুন", + "warning-sending-to-contract-descr": "আপনি যে ঠিকানায় প্রবেশ করেছেন তা একটি স্মার্ট চুক্তি, এই ঠিকানায় তহবিল পাঠানোর ফলে তহবিল ক্ষতিগ্রস্ত হতে পারে। একটি DApp-এর সাথে মিথস্ক্রিয়া করতে, Status DApp ব্রাউজারে DApp খুলুন।", + "watch-only": "কেবলমাত্র দেখার জন্য", + "wc-brand-guide": "ব্র্যান্ডিং যেমন ট্রেডমার্ক এবং লোগো ব্যবহার করার বিষয়ে নির্দেশিকা", + "wc-disclaimer": "Disclaimers (তৃতীয় পক্ষের প্রদানকারী সহ), ওয়ারেন্টি, এবং আইনি রিলিজ", + "wc-dispute": "বিরোধ নিষ্পত্তির বিধান", + "wc-how-to-use-status-app": "গোপনীয়তা এবং নিরাপত্তা সহ স্থিতি অ্যাপ্লিকেশনটি কীভাবে ব্যবহার করবেন", + "wc-new-tos-based-on-principles-prefix": "আমাদের উপর ভিত্তি করে ডিজাইন করা নতুন ব্যবহারের শর্তাবলী", + "web-view-error": "পৃষ্ঠা লোড করতে অক্ষম", + "websites": "ওয়েবসাইট", + "webview-camera-permission-requests": "ওয়েবভিউ ক্যামেরার অনুমতি অনুরোধ", + "webview-camera-permission-requests-subtitle": "যখন সক্রিয় করা হয়, ওয়েবসাইট এবং ড্যাপস আপনার ক্যামেরা ব্যবহার করতে বলতে পারে", + "welcome-blank-community-message": "আপনার সম্প্রদায়গুলি এখানে উপস্থিত হবে।", + "welcome-blank-message": "আপনার চ্যাট এখানে প্রদর্শিত হবে। নতুন চ্যাট শুরু করতে ⊕ বোতামটি চাপুন", + "welcome-community-blank-message": "আপনার চ্যানেলগুলি এখানে উপস্থিত হবে। একটি নতুন চ্যানেল তৈরি করতে, ⊕ বোতামে ক্লিক করুন এবং \"একটি চ্যানেল তৈরি করুন\" নির্বাচন করুন", + "welcome-community-blank-message-edit-chats": "আপনার চ্যানেল এখানে প্রদর্শিত হবে. একটি নতুন চ্যানেল তৈরি করতে, কমিউনিটি স্ক্রিনে ফিরে যান, ⊕ বোতামে ক্লিক করুন এবং \"একটি চ্যানেল তৈরি করুন\" নির্বাচন করুন।", + "welcome-screen-text": "আপনার মানিব্যাগ সেট আপ করুন, চ্যাটে বন্ধুদের আমন্ত্রণ করুন\n এবং জনপ্রিয় ড্যাপ ব্রাউজ করুন!", + "welcome-to-status": "Status স্বাগতম", + "welcome-to-status-description": "আপনার ক্রিপ্টো ওয়ালেট সেট আপ করুন, বন্ধুদের চ্যাট এবং বিকেন্দ্রীভূত অ্যাপস ব্রাউজ করতে আমন্ত্রণ জানান", + "what-changed": "কি পরিবর্তন হয়েছে", + "what-is-shared": "কি শেয়ার করা হয়", + "whats-on-your-mind": "আপনি কি ভাবছেন...", + "word-count": "শব্দ গণনা", + "word-n": "শব্দ # {{number}}", + "word-n-description": "আপনি আপনার বীজ বাক্যটি সঠিকভাবে ব্যাকআপ করেছেন কিনা তা পরীক্ষা করার জন্য, উপরে #{{number}} শব্দটি প্রবেশ করান।", + "words-n": { + "one": "১ শব্দ", + "other": "{{count}}শব্দ" + }, + "write-down-and-store-securely": "কোড লিখুন\n এবং তাদের নিরাপদে সংরক্ষণ করুন", + "wrong-address": "ভুল ঠিকানা", + "wrong-card": "ভুল কার্ড", + "wrong-card-text": "ট্যাপ করা কার্ড আপনার নির্বাচিত কীর সাথে সামঞ্জস্যপূর্ণ নয়", + "wrong-contract": "ভুল চুক্তি", + "wrong-keycard-text": "আপনি যে কীকার্ডটি ট্যাপ করেছেন তা এই ফোনের সাথে সংযুক্ত নয়", + "wrong-keycard-title": "দেখে মনে হচ্ছে আপনি টেপ করেছেন\n একটি ভুল কিকার্ড", + "wrong-password": "ভুল পাসওয়ার্ড", + "wrong-word": "ভুল শব্দ", + "yes": "হ্যাঁ", + "you": "আপনি", + "you-already-have-an-asset": "আপনার ইতোমধ্যে একটি সম্পদ আছে {{value}}", + "you-are-all-set": "তোমরা সবাই প্রস্তুত!", + "you-are-all-set-description": "আপনি যদি আপনার ফোন হারান, আপনি এখন আপনার বীজ বাক্যাংশ ব্যবহার করে আপনার তহবিল এবং চ্যাট কী অ্যাক্সেস করতে পারেন", + "you-can-change-account": "আপনি যা চান তা হিসাবের নাম এবং রং পরিবর্তন করতে পারেন", + "you-can-choose-preview-websites": "আপনি নিম্নলিখিত ওয়েবসাইটগুলির মধ্যে কোনটি চ্যাটে বর্ণনা এবং ছবির লিঙ্ক প্রাকদর্শন করতে পারেন তা চয়ন করতে পারেন", + "you-can-fetch": "আপনি চ্যাটের ইতিহাস আনতে পারেন", + "you-dont-have-contacts": "তোমার এখনো কোন যোগাযোগ নেই।", + "you-dont-have-contacts-invite-friends": "আপনার এখনও কোনও পরিচিতি নেই।\nচ্যাট করা শুরু করতে আপনার বন্ধুদের আমন্ত্রণ জানান।", + "you-dont-have-stickers": "তোমার কাছে এখনো কোন স্টিকার নেই", + "you-will-need-this-code": "Status খুলতে এবং লেনদেন সাইন করতে আপনার এই কোডটির প্রয়োজন হবে", + "you-will-start-from-scratch": "আপনি একটি নতুন সেট কী দিয়ে শুরু করবেন", + "your-card-is-frozen": "আপনার কী-কার্ড জমে গেছে। কার্ড অ্যাক্সেস রিসেট করুন", + "your-contact-code": "অ্যাক্সেস মঞ্জুর করা এই DApp আপনার চ্যাট কী পুনরুদ্ধার করতে অনুমোদন দেয়", + "your-data-belongs-to-you": "আপনি যদি আপনার বীজ বাক্যাংশ হারান তাহলে আপনি আপনার ডেটা এবং তহবিল হারান", + "your-data-belongs-to-you-description": "আপনি যদি অ্যাক্সেস হারান, উদাহরণস্বরূপ, আপনার ফোন হারিয়ে, আপনি শুধুমাত্র আপনার বীজ বাক্যাংশ দিয়ে আপনার চাবি অ্যাক্সেস করতে পারেন. কেউ না, কিন্তু তোমার বীজের বাক্যাংশ আছে। এটা লিখে দাও। এটা নিরাপদ রাখুন", + "your-keys": "আপনার চাবি", + "your-price-limit": "আপনার মূল্য সীমা", + "your-recovery-phrase": "আপনার বীজ বাক্যাংশ", + "your-recovery-phrase-description": "এটা তোমার বীজের বাক্যাংশ। আপনি এটা ব্যবহার করে প্রমাণ করতে যে এটা আপনার মানিব্যাগ। তুমি শুধু একবারই এটা দেখতে পাবে! এটা কাগজে লিখে নিরাপদ জায়গায় রাখুন। আপনি যদি আপনার মানিব্যাগ হারান বা পুনরায় ইনস্টল করেন তাহলে আপনার এটি প্রয়োজন হবে।", + "your-tip-limit": "আপনার টিপ সীমা", + "youre-on-mobile-network": "আপনি মোবাইল নেটওয়ার্কে আছেন" +} diff --git a/translations/cs.json b/translations/cs.json new file mode 100644 index 00000000000..e8fab9eada9 --- /dev/null +++ b/translations/cs.json @@ -0,0 +1,145 @@ +{ + "confirm": "Potvrdit", + "amount": "Množství", + "members-active": { + "one": "1 účastník", + "other": "{{count}} účastníci", + "zero": "žádní účastníci" + }, + "chat-name": "Název chatu", + "phew-here-is-your-passphrase": "*Uf*, to byla fuška, tady jsou bezpečnostní slova, *zapiš je a ulož na bezpečném místě!* Budou potřeba pro obnovení přístupu k účtu.", + "public-group-topic": "Téma", + "chat-settings": "Nastavení chatu", + "offline": "Offline", + "invited": "pozván", + "address": "Adresa", + "new-public-group-chat": "Přidat se k veřejnému chatu", + "datetime-hour": { + "one": "hodina", + "other": "hodin(y)" + }, + "datetime-ago-format": "{{ago}} {{number}} {{time-intervals}}", + "camera-access-error": "Pro udělení potřebných oprávnění ke kameře přejděte do nastavení systému a ujistěte se, že je vybráno Status > Kamera.", + "remove": "Odstranit", + "add-members": "Přidat členy", + "photos-access-error": "Pro udělení potřebných oprávnění k fotoaparátu přejděte do nastavení systému a ujistěte se, že je vybráno Status > Fotoaparát.", + "done": "Hotovo", + "close-chat": "Smazat chat", + "new-group-chat": "Nový skupinový chat", + "wallet": "Peněženka", + "wallet-request": "Požadavek", + "sign-in": "Přihlásit se", + "datetime-yesterday": "včera", + "create-new-account": "Vytvořit nový účet", + "datetime-ago": "uplynulo:", + "contacts": "Kontakty", + "got-it": "Mám to", + "active-online": "Online", + "password": "Heslo", + "browsing-title": "Prohlížet", + "transactions-history-empty": "V historii nemáš žádné transakce", + "discover": "Objevit", + "browsing-cancel": "Storno", + "intro-status": "Chat se mnou ti může pomoci nastavit ůčet a změnit další nastavení!", + "name": "Jméno", + "show-qr": "Zobrazit QR kǒd", + "connect": "Připojit", + "edit": "Upravit", + "account-generation-message": "Vteřinku, musím použít docela těžkou matiku na vygenerování účtu!", + "no-messages": "Žádné zprávy", + "passphrase": "Bezpečnostní slova", + "recipient": "Příjemce", + "members-title": "Členové", + "new-group": "Nová skupina", + "phone-e164": "Mezinárodní 1", + "settings": "Nastavení", + "delete": "Smazat", + "chats": "Chaty", + "transaction": "Transakce", + "public-group-status": "Veřejné", + "image-source-make-photo": "Vyfotit", + "start-conversation": "Zahájit konverzaci", + "save": "Uložit", + "sharing-copy-to-clipboard": "Kopírovat do schránky", + "sync-in-progress": "Synchronizuji...", + "send-transaction": "Odeslat transakci", + "incorrect-code": [ + "str", + "Tento kód není správný, prosím zkus to znovu." + ], + "image-source-gallery": "Vybrat z galerie", + "sync-synced": "Synchronizováno", + "status-pending": "Čekám", + "datetime-day": { + "one": "den", + "other": "dny(ů)" + }, + "wallet-send": "Poslat", + "scan-qr": "Načíst QR kód", + "contact-s": { + "one": "kontakt", + "other": "kontakty(ů)" + }, + "type": "Typ", + "next": "Další", + "recent": "Poslední", + "status": "Status", + "from": "Od", + "wrong-password": "Špatné heslo", + "in-contacts": "V kontaktech", + "transactions-sign": "Podepsat", + "sharing-share": "Sdílet...", + "type-a-message": "Napište zprávu...", + "clear-history": "Vymazat historii", + "no-contacts": "Ještě tu nejsou žádné kontakty", + "datetime-today": "dnes", + "web-view-error": "ups, chyba", + "error": "Chyba", + "more": "více", + "cancel": "Storno", + "can-not-add-yourself": "Nemůžeš přidat sebe", + "add-to-contacts": "Přidat do kontaktů", + "available": "Dostupný", + "You": "Ty", + "main-wallet": "Hlavní peněženka", + "transactions": "Transakce", + "members": { + "one": "1 účastník", + "other": "{{count}} účastníci", + "zero": "žádní účastníci" + }, + "intro-message1": [ + "str", + "Vítá vás Status\nKlepněte na tuto zprávu pro nastavení hesla a zahájení!" + ], + "transactions-history": "Historie", + "new-contact": "Nový kontakt", + "datetime-second": { + "one": "sekunda", + "other": "sekund(y)" + }, + "recover": "Obnovit", + "datetime-minute": { + "one": "minuta", + "other": "minut(y)" + }, + "browsing-open-in-android-web-browser": "Otevřít ve webovém prohlížeči", + "browsing-open-in-ios-web-browser": "Otevřít ve webovém prohlížeči", + "edit-profile": "Upravit profil", + "active-unknown": "Neznámý", + "public-key": "Veřejný klíč", + "profile": "Profil", + "none": "Žádný", + "removed": "odstraněn", + "transactions-filter-select-all": "Označit vše", + "transactions-filter-title": "Filtrovat historii", + "message": "Zpráva", + "here-is-your-passphrase": "Tady jsou bezpečnostní slova, *zapiš je a ulož na bezpečném místě!* Budou potřeba pro obnovení přístupu k účtu.", + "wallet-assets": "Aktiva", + "image-source-title": "Profilový obrázek", + "current-network": "Aktuální síť", + "left": "opuštěno", + "to": "Komu", + "status-sent": "Odesláno", + "data": "Data" +} diff --git a/translations/da.json b/translations/da.json new file mode 100644 index 00000000000..ebeba286f50 --- /dev/null +++ b/translations/da.json @@ -0,0 +1,131 @@ +{ + "confirm": "Bekræft", + "amount": "Beløb", + "members-active": { + "one": "1 medlem", + "other": "{{count}} medlemmer", + "zero": "ingen medlemmer" + }, + "chat-name": "Chatnavn", + "phew-here-is-your-passphrase": "*Puha* det var hårdt, her er din kodesætning (passphrase), *skriv den ned og gem den sikkert!* Du skal bruge den for at kunne gendanne din konto.", + "public-group-topic": "Emne", + "chat-settings": "Chatindstillinger", + "offline": "Offline", + "invited": "Inviteret", + "address": "Adresse", + "new-public-group-chat": "Deltag i offentlig samtale", + "datetime-hour": { + "one": "time", + "other": "timer" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "Gå venligst til dine systemindstillinger og sørg for at du Status > Kamera er tilladt.", + "remove": "Fjern", + "add-members": "Tilføj medlemmere", + "photos-access-error": "Gå venligst til dine systemindstillinger og sørg for at du Status > Billeder er tilladt.", + "done": "Klar", + "close-chat": "Fjern samtale", + "new-group-chat": "Ny gruppsamtale", + "sign-in": "Log på", + "datetime-yesterday": "i går", + "create-new-account": "Opret en ny konto", + "datetime-ago": "siden", + "contacts": "Kontakter", + "got-it": "Forstået", + "active-online": "Online", + "password": "Kodeord", + "browsing-title": "Browse", + "discover": "Opdag", + "browsing-cancel": "Annullér", + "intro-status": "Chat med mig for at opsætte din konto og ændre dine indstillinger!!", + "name": "Navn", + "show-qr": "Vis QR-kode", + "connect": "Tilslut", + "edit": "Rediger", + "account-generation-message": "Vent et øjeblik, jeg er nødt til at gøre nogle skøre beregninger for at generere din konto!", + "no-messages": "Ingen beskeder", + "passphrase": "Kodesætning (passphrase)", + "recipient": "Modtager", + "members-title": "Medlemmere", + "new-group": "Ny gruppe", + "phone-e164": "International 1", + "settings": "Indstillinger", + "delete": "Slet", + "chats": "Samtaler", + "transaction": "Transaktion", + "status-sent": "Sendt", + "public-group-status": "Offentlig", + "image-source-make-photo": "Tag billede", + "start-conversation": "Begynd samtale", + "save": "Gem", + "sharing-copy-to-clipboard": "Kopier til udklipsholderen", + "sync-in-progress": "Synkroniserer...", + "send-transaction": "Send transaktion", + "incorrect-code": [ + "str", + "Koden var desværre forkert. Prøv venligst igen" + ], + "image-source-gallery": "Vælg fra galleri", + "sync-synced": "Synkroniseret", + "status-pending": "Under behandling", + "datetime-day": { + "one": "dag", + "other": "dage" + }, + "scan-qr": "Skan QR-kode", + "contact-s": { + "one": "kontakt", + "other": "kontakter" + }, + "next": "Næste", + "recent": "Seneste", + "status": "Status", + "from": "Fra", + "wrong-password": "Forkert kodeord", + "in-contacts": "I kontakter", + "sharing-share": "Del...", + "type-a-message": "Skriv en besked...", + "clear-history": "Fjern historik", + "no-contacts": "Ingen kontakter endnu", + "datetime-today": "i dag", + "web-view-error": "hovsa, fejl", + "error": "Fejl", + "more": "mere", + "cancel": "Annullér", + "can-not-add-yourself": "Du kan ikke tilføje dig selv fjolle", + "add-to-contacts": "Tilføj til kontakter", + "available": "Aktiv", + "You": "Du", + "main-wallet": "Hovedpung", + "members": { + "one": "1 medlem", + "other": "{{count}} medlemmer", + "zero": "ingen medlemmer" + }, + "intro-message1": "Velkommen til Status\n Tryk på denne meddelelse for at angive din adgangskode og komme i gang!", + "new-contact": "Ny kontakt", + "datetime-second": { + "one": "sekund", + "other": "sekunder" + }, + "recover": "Gendan", + "datetime-minute": { + "one": "minut", + "other": "minutter" + }, + "browsing-open-in-android-web-browser": "Åben i web browser", + "browsing-open-in-ios-web-browser": "Åben i web browser", + "edit-profile": "Rediger profil", + "active-unknown": "Ukendt", + "public-key": "Offentlig nøgle", + "profile": "Profil", + "none": "Ingen", + "removed": "Fjernet", + "message": "Meddelelser", + "here-is-your-passphrase": "Her er din kodesætning (passphrase), *skriv den ned og gem den sikkert!* Du behøver den for at gendanne din konto.", + "image-source-title": "Profilbillede", + "current-network": "Nuværende netværk", + "left": "Forlod", + "to": "Til", + "data": "Data" +} diff --git a/translations/de.json b/translations/de.json new file mode 100644 index 00000000000..2faaa21fe6d --- /dev/null +++ b/translations/de.json @@ -0,0 +1,1715 @@ +{ + "You": "Sie", + "about-app": "Über diese App", + "about-key-storage-content": "Status wird nie auf Ihren privaten Schlüssel zugreifen. Sichern Sie unbedingt Ihre Seed-Phrase. Wenn Sie Ihr Smartphone verlieren, ist dies die einzige Möglichkeit, auf Ihre Schlüssel zuzugreifen.", + "about-key-storage-title": "Informationen zur Schlüsselspeicherung", + "about-names-content": "Niemand kann vorgeben, Sie zu sein! Sie sind standardmäßig anonym und müssen niemals Ihren richtigen Namen preisgeben. Sie können einen benutzerdefinierten Namen gegen eine geringe Gebühr registrieren.", + "about-names-title": "Namen können nicht geändert werden", + "about-sharing-data": "Über das Teilen von Daten", + "accept": "Akzeptieren", + "accept-and-add": "Akzeptieren und hinzufügen", + "accept-and-continue": "Akzeptieren und fortfahren", + "accept-and-share-address": "Adresse akzeptieren und teilen", + "accept-new-chats-from": "Neue Chats annehmen von", + "accept-status-tos-prefix": "Ich akzeptiere Status", + "access-existing-keys": "Greife auf vorhandene Schlüssel zu", + "access-key": "Zugangsschlüssel", + "account-added": "Konto hinzugefügt", + "account-color": "Farbe des Kontos", + "account-content": "Sie können Konten in Status mit Bankkonten vergleichen. Wie ein Bankkonto hat ein Konto normalerweise eine Adresse und einen Kontostand. Sie verwenden dieses Konto, um Transaktionen auf Ethereum durchzuführen. Sie können mehrere Konten in Ihrer Wallet haben, welche nach dem Entsperren von Status direkt zur Verfügung stehen.", + "account-exists-title": "Konto existiert bereits", + "account-is-used": "Das Konto wird mit Dapps im Browser verwendet.", + "account-name": "Name des Kontos", + "account-settings": "Konto-Einstellungen", + "account-title": "Konto", + "accounts": "Konten", + "actions": "Aktionen", + "active-online": "Online", + "active-unknown": "Unbekannt", + "activity": "Aktivität", + "add": "hinzufügen", + "add-a-watch-account": "Fügen Sie ein Beobachtungskonto hinzu", + "add-account": "Konto hinzufügen", + "add-account-description": "Sie können jede Art von Ethereum-Konten importieren, um sie Ihrer Status-Wallet hinzuzufügen", + "add-account-incorrect-password": "Das Passwort scheint falsch zu sein. Geben Sie das Passwort ein, mit dem Sie die App üblicherweise entsperren.", + "add-an-account": "Füge ein Konto hinzu", + "add-bootnode": "Bootnode hinzufügen", + "add-contact": "Kontakt hinzufügen", + "add-custom-token": "Benutzerdefiniertes Token hinzufügen", + "add-favourite": "Favorit hinzufügen", + "add-mailserver": "Mailserver hinzufügen", + "add-members": "Mitglieder hinzufügen", + "add-network": "Netzwerk hinzufügen", + "add-new-contact": "Neuen Kontakt hinzufügen", + "add-nickname": "Alias hinzufügen", + "add-node": "Knoten hinzufügen", + "add-private-key-account": "Konto mit privatem Schlüssel hinzufügen", + "add-seed-account": "Konto mit einer Seed-Phrase hinzufügen", + "add-to-contacts": "Zu Kontakten hinzufügen", + "add-to-contacts-text": "Durch Hinzufügen eines Benutzers zu Ihrer Kontaktliste teilen Sie Ihre Wallet-Adresse", + "add-to-favourites": "Zu Favoriten hinzufügen", + "add-watch-account": "Füge ein Beobachtungskonto hinzu", + "address": "Adresse", + "address-or-ens-name": "Adresse oder ENS-Name", + "address-received": "Adresse erhalten", + "address-request-accepted": "Adressanfrage akzeptiert", + "address-requested": "Adresse angefordert", + "advanced": "Erweitert", + "advanced-settings": "Erweiterte Einstellungen", + "advertiser-description": "Sie haben Status dank eines Partners entdeckt. Erlauben Sie, dass Status Ihre IP-Adresse einmal überprüft, damit sie belohnt werden? Diese Informationen werden für nichts anderes verwendet und nach 7 Tagen vollständig gelöscht.", + "advertiser-starter-pack-accept": "Akzeptieren", + "advertiser-starter-pack-decline": "Ablehnen", + "advertiser-starter-pack-description": "Hier ist etwas Kryptowährung für den Anfang! Benutzen Sie diese, um Sticker zu bekommen, einen ENS-Namen zu erstellen und DApps zu testen.", + "advertiser-starter-pack-title": "Starter Pack", + "advertiser-title": "Datenschutz standardmäßig", + "agree-by-continuing": "Indem Sie fortfahren, stimmen Sie zu \n zu unserer", + "all": "Alle", + "allow": "Erlauben", + "allow-and-send": "Erlauben und senden", + "allow-mention-notifications": "Zeige @ Erwähnungen an", + "allowing-authorizes-this-dapp": "Durch das Zulassen wird diese DApp berechtigt, Ihre Wallet Adresse abzurufen und Web3 zu aktivieren", + "already-have-asset": "Sie haben dieses Asset bereits", + "amount": "Betrag", + "anyone": "Jeder", + "appearance": "Erscheinungsbild", + "apply": "Anwenden", + "approve": "Genehmigen", + "approve-limit": "Limit genehmigen", + "approve-token": "Token genehmigen", + "approve-token-contract-desc": "Die Genehmigung eines Tokens mit einem Vertrag ermöglicht es ihm, Ihr Token-Guthaben auszugeben. Wenn Sie der Meinung sind, dass ein Projekt nicht vertrauenswürdig ist, genehmigen Sie das Token nicht mit ihnen oder genehmigen Sie nur den Betrag, den Sie mit ihnen verwenden werden.", + "are-you-sure": "Sind Sie sicher?", + "are-you-sure-description": "Die gesamte Seed-Phrase wird nicht mehr angezeigt", + "are-you-sure-to-cancel": "Sind Sie sicher, dass Sie abbrechen möchten?", + "are-you-sure?": "Sind Sie sicher?", + "ask-in-status": "Stellen Sie eine Frage oder melden Sie einen Fehler", + "at": "bei", + "attribution-received": "{{attrib}} von {{max}} Boni erhalten", + "audio": "Audio", + "audio-recorder": "Rekorder", + "audio-recorder-error": "Rekorderfehler", + "audio-recorder-max-ms-reached": "Maximale Aufnahmezeit erreicht", + "audio-recorder-permissions-error": "Sie müssen die Erlaubnis zum Senden von Audionachrichten erteilen", + "authorize": "Autorisieren Sie", + "available": "Verfügbar", + "available-participants": { + "one": "Sie können noch ein Mitglied auswählen", + "other": "Sie können noch {{count}} Mitglieder auswählen" + }, + "back": "Zurück", + "back-up": "Sichern", + "back-up-seed-phrase": "Sichern der Seed-Phrase", + "back-up-your-seed-phrase": "Sichern sie ihre Seed-Phrase", + "backing-up": "Sichern…", + "backup-disabled": "Deaktiviert", + "backup-enabled": "Aktiviert", + "backup-recovery-phrase": "Seed-Phrase sichern", + "backup-settings": "Backup-Einstellungen", + "backup-through-waku": "Backup über Waku", + "bad-fees-description": "Ihre Prioritätsgebühr liegt unter unseren vorgeschlagenen Parametern.", + "balance": "Kontostand", + "begin-set-up": "Beginnen Sie mit der Einrichtung", + "below-base-fee": "Maximale Gebühr unter Grundgebühr", + "biometric-auth-android-sensor-desc": "Fingerabdruck", + "biometric-auth-android-sensor-error-desc": "Fehlgeschlagen", + "biometric-auth-android-title": "Authentifizierung erforderlich", + "biometric-auth-confirm-logout": "Erneut anmelden", + "biometric-auth-confirm-message": "Die biometrische Authentifizierung ist erforderlich, um fortzufahren. Wenn dies nicht möglich ist, entsperren Sie bitte Ihre Schlüssel mit Ihrem Passwort oder Passcode", + "biometric-auth-confirm-title": "Sie müssen sich authentifizieren!", + "biometric-auth-confirm-try-again": "Erneut versuchen", + "biometric-auth-error": "Biometrische Authentifizierung nicht möglich ({{code}})", + "biometric-auth-login-error-title": "Fehler bei der biometrischen Authentifizierung", + "biometric-auth-login-ios-fallback-label": "Passwort eingeben", + "biometric-auth-reason-login": "In Status einloggen", + "biometric-auth-reason-verify": "Überprüfen Sie die Authentifizierung", + "biometric-disable-bioauth": "{{bio-type-label}} deaktivieren", + "biometric-disable-password-description": "Wenn Sie dies deaktivieren, werden Sie auch ", + "biometric-disable-password-title": "Speichern des Passworts deaktivieren", + "biometric-enable": "Wenn Sie nicht jedes Mal Ihr Passwort eingeben möchten, um auf die App zuzugreifen, aktivieren Sie die {{bio-type-label}}-Anmeldung", + "biometric-enable-button": "Aktivieren Sie {{bio-type-label}}", + "biometric-enable-keycard": "Wenn Sie Ihre Keycard nicht jedes Mal verwenden möchten, um auf die App zuzugreifen, aktivieren Sie die {{bio-type-label}}-Anmeldung", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Fingerabdruck", + "biometric-secure-with": "Mit {{bio-type-label}} sichern", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "BIP39 Passwort", + "blank-keycard-text": "Sie können mit Ihrer Keycard fortfahren, sobald Sie Ihre Schlüssel und Ihren Namen generiert haben", + "blank-keycard-title": "Wahrscheinlich haben Sie eine \nleere Keycard verwendet", + "block": "Block", + "block-contact": "Diesen Benutzer blockieren", + "block-contact-details": "Durch das Blockieren werden die vorherigen Nachrichten dieses Benutzers gelöscht. Neue Nachrichten werden nicht mehr empfangen.", + "blocked-users": "Blockierte Benutzer", + "bootnode-address": "Bootknotenadresse", + "bootnode-details": "Bootknoten-Details", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootknoten", + "bootnodes-enabled": "Bootknoten aktiviert", + "bootnodes-settings": "Bootnodes Einstellungen", + "browsed-websites": "Der Browserverlauf wird hier angezeigt", + "browser": "Browser", + "browser-not-secure": "Verbindung ist nicht sicher! Unterzeichnen Sie auf dieser Website keine Transaktionen und senden Sie keine persönlichen Daten.", + "browser-secure": "Die Verbindung ist sicher. Stellen Sie sicher, dass Sie dieser Website wirklich vertrauen, bevor Sie Transaktionen unterzeichnen oder persönliche Daten eingeben.", + "browsers": "Browser", + "browsing-cancel": "Abbrechen", + "browsing-open-in-android-web-browser": "In Android öffnen", + "browsing-open-in-ios-web-browser": "In iOS öffnen", + "browsing-open-in-status": "In Status öffnen", + "browsing-site-blocked-description1": "Wir haben potenzielle böswillige Aktivitäten von dieser Adresse aus entdeckt. Um Sie und Ihre Wallet zu schützen, verhindern wir die weitere Navigation.\n\nWenn Sie der Meinung sind, dass es sich um einen Fehler handelt, teilen Sie uns dies mit in der ", + "browsing-site-blocked-description2": " öffentlicher Chat.", + "browsing-site-blocked-go-back": "Zurück", + "browsing-site-blocked-title": "Diese Seite ist blockiert", + "browsing-title": "Durchsuchen", + "bug-report": "Melden Sie einen technischen Fehler", + "bug-report-description": "* Beschreibung", + "bug-report-description-placeholder": "Erforderlich, kann nicht leer sein", + "bug-report-steps": "Schritte zur Reproduktion", + "bug-report-steps-placeholder": "- App öffnen\n- etwas tun\n- und dann etwas anderes...", + "bug-report-submit-email": "Einreichung per E-Mail mit Archiv der Protokolle", + "bug-report-submit-gh-issue": "Einreichen eines GitHub-Problems ohne Protokolle", + "bug-report-too-short-description": "Beschreibung ist zu kurz", + "build-yourself": "Um die App ohne diese Nutzungsbedingungen zu nutzen, können Sie Ihre eigene Version erstellen", + "buy-crypto": "Krypto kaufen", + "buy-crypto-choose-a-service": "Wählen Sie einen Dienst, den Sie zum Kauf von Krypto verwenden möchten", + "buy-crypto-description": "Finden Sie eine DApp, um jetzt Kryptowährung zu kaufen", + "buy-crypto-leaving": "Sie verlassen Status und rufen eine Website eines Drittanbieters auf, um Ihren Kauf abzuschließen", + "buy-crypto-title": "Sieht aus, als wäre Ihre Wallet leer", + "camera-access-error": "Um den benötigten Kamera-Zugriff zu erlauben, gehen Sie bitte in Ihre Systemeinstellungen und stelle sicher, dass Status > Kamera ausgewählt ist.", + "can-not-add-yourself": "Das sind Sie; um einen Chat zu starten, wählen Sie jemand anderes", + "can-send-messages": "Sie können neue Nachrichten senden und empfangen", + "cancel": "Abbrechen", + "cancel-keycard-setup": "Keycard-Einrichtung abbrechen", + "cancelling": "Abbrechen", + "cannot-read-card": "Karte kann nicht gelesen werden. \nBitte halten Sie es an die Rückseite Ihres Handys!", + "cannot-use-default-pin": "Passcode 000000 ist nicht erlaubt. \nBitte verwenden Sie eine andere Nummer", + "cant-open-public-chat": "Öffentlicher Chat kann nicht geöffnet werden", + "cant-report-bug": "Ich kann keinen Fehler melden", + "card-is-blank": "Diese Karte ist leer", + "card-reseted": "Karte wurde zurückgesetzt", + "card-unpaired": "Karte wurde vom aktuellen Gerät getrennt", + "category": "Kategorie", + "category-title": "Titel der Kategorie", + "change-fleet": "Fleet in {{fleet}} ändern", + "change-logging-enabled": "Möchten Sie die Protokollierung {{enable}}?", + "change-pairing": "Pairing-Code ändern", + "change-pairing-description": "Das Ändern des Pairing-Codes hat keine Auswirkungen auf die aktuellen Pairings. Für jede neue Paarung ist jedoch der neue Code erforderlich.", + "change-pairing-title": "Erstellen eines neuen Pairing-Code", + "change-passcode": "Passcode ändern", + "change-password": "Passwort ändern", + "change-pin": "Ändern Sie den 6-stelligen Passcode", + "change-puk": "12-stelligen PUK ändern", + "change-tip": "Trinkgeld ändern", + "changed-amount-warning": "Der Betrag wurde von {{old}} zu {{new}} geändert", + "changed-asset-warning": "Asset wurde von {{old}} in {{new}} geändert", + "chaos-mode": "Chaos-Modus", + "chaos-unicorn-day": "Chaos Unicorn Day", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Chatten Sie und tauschen Sie Kryptowährung privat mit Freunden aus", + "chat-is-a-contact": "Kontakt", + "chat-is-not-a-contact": "Kein Kontakt", + "chat-key": "Chat-Key", + "chat-key-content": "Nachrichten im Status-Chat-Protokoll werden mit Verschlüsselungsschlüsseln gesendet und empfangen. Der öffentliche Chat-Schlüssel ist eine Zeichenfolge, die Sie mit anderen teilen, damit diese Ihnen Nachrichten im Status senden können.", + "chat-key-title": "Chat-Schlüssel", + "chat-link-previews": "Chat-Link-Vorschauen", + "chat-name": "Chatname", + "chat-name-content": "Drei zufällige Wörter, die algorithmisch von Ihrem Chat-Schlüssel abgeleitet und als Standardalias im Chat verwendet werden. Chat-Namen sind völlig eindeutig. Kein anderer Benutzer kann dieselben drei Wörter haben.", + "chat-name-title": "Chat-Name", + "chat-notification-preferences": "Benachrichtigungseinstellungen", + "chat-settings": "Chat-Einstellungen", + "chats": "Chats", + "check-on-block-explorer": "Überprüfen auf dem Block-Explorer", + "check-on-opensea": "auf Opensea überprüfen", + "check-your-recovery-phrase": "Überprüfen Sie Ihre Seed-Phrase", + "choose-actions": "Aktionen auswählen", + "choose-authentication-method": "Wählen Sie eine Authentifizierungsmethode", + "choose-new-location-for-keystore": "Wählen Sie einen neuen Speicherort für Ihre Keystore-Datei", + "choose-storage": "Speicherort wählen", + "clear": "Leeren", + "clear-all": "Alles löschen", + "clear-history": "Verlauf löschen", + "clear-history-action": "Leeren", + "clear-history-confirmation": "Verlauf leeren?", + "clear-history-confirmation-content": "Möchten Sie diesen Chat-Verlauf wirklich löschen?", + "clear-history-title": "Verlauf löschen?", + "close": "Schließen", + "close-all": "Alle schließen", + "close-app-button": "Bestätigen", + "close-app-content": "Die App wird angehalten und geschlossen. Wenn Sie sie erneut öffnen, wird das ausgewählte Netzwerk verwendet", + "close-app-title": "Warnung!", + "collectibles-leak-metadata": "Hier können Sie Ihre NFTs anzeigen. Wenn Sie dies tun, teilen Sie Ihre Wallet und IP-Adresse", + "command-button-send": "Senden", + "communities": "Communities", + "communities-alpha": "Communities (Alpha)", + "communities-enabled": "Communities aktiviert", + "communities-verified": "✓ Verifizierte Status Community", + "community": "Community", + "community-color": "Community Farbe", + "community-color-placeholder": "Wählen Sie eine Farbe", + "community-edit-title": "Community bearbeiten", + "community-emoji-thumbnail-title": "Miniaturansicht", + "community-image-pick": "Bild wählen", + "community-image-remove": "Entfernen", + "community-image-take": "Foto machen", + "community-info": "Community-Info", + "community-info-not-found": "Community-Informationen nicht gefunden", + "community-invite-title": "Einladen", + "community-key": "Privater Community-Schlüssel", + "community-key-placeholder": "Geben Sie Ihren privaten Community-Schlüssel ein", + "community-link": "Community-Link", + "community-members": { + "one": "{{count}} Mitglied", + "other": "{{count}} Mitglieder" + }, + "community-members-title": "Mitglieder", + "community-message-preview": "Einladung zum Beitritt zu {{community-name}}", + "community-private-key": "Privater Community-Schlüssel", + "community-requests-to-join-title": "Anfragen zur Mitgliedschaft", + "community-roles": "Rollen", + "community-share-title": "Teilen", + "community-thumbnail-image": "Vorschaubild", + "community-thumbnail-upload": "Hochladen", + "complete-hardwallet-setup": "Diese Karte ist jetzt verlinkt. Sie benötigen sie, um Transaktionen zu unterzeichnen und Ihre Schlüssel freizuschalten.", + "completed": "Abgeschlossen", + "confirm": "Bestätigen", + "confirm-new-password": "Bestätige neues Passwort", + "confirm-pairing-code-placeholder": "Bestätigen Sie Ihren Pairing-Code ...", + "confirm-password-placeholder": "Bestätigen Sie Ihr Passwort...", + "confirmation-request": "Bestätigungsanfrage", + "confirmations": "Bestätigungen", + "confirmations-helper-text": "Wenn die Transaktion 12 Bestätigungen hat, können Sie sie als abgeschlossen betrachten.", + "connect": "Verbinden", + "connect-mailserver-content": "Mit {{name}} verbinden?", + "connect-wallet": "Wallet verbinden", + "connected": "Verbunden", + "connected-to": "Verbunden mit", + "connecting": "Verbinden...", + "connecting-requires-login": "Für die Verbindung mit einem anderen Netzwerk ist eine Anmeldung erforderlich", + "connection-status": "Verbindungsstatus", + "connection-with-the-card-lost": "Verbindung zur Karte\nverloren", + "connection-with-the-card-lost-setup-text": "Um die Einrichtung fortzusetzen,\nhalten Sie die Karte direkt an die\nRückseite Ihres Smartphones", + "connection-with-the-card-lost-text": "Um fortzufahren, halten Sie die Karte an die Rückseite Ihres Smartphones", + "contact-code": "Chat-Key", + "contact-s": { + "one": "Kontakt", + "other": "Kontakte" + }, + "contacts": "Kontakte", + "contacts-descr": "Ihre Kontakte werden hier angezeigt. Sie erhalten Statusaktualisierungen von jedem, den Sie als Kontakt hinzufügen", + "contacts-empty": "Kontakte mit ENS-Namen werden hier angezeigt", + "continue": "Fortsetzen", + "continue-anyway": "Trotzdem fortfahren", + "contract-address": "Vertragsadresse", + "contract-interaction": "Vertragliche Interaktion", + "contract-isnt-supported": "Vertrag wird nicht unterstützt", + "copy-info": "Informationen kopieren", + "copy-qr": "Code kopieren", + "copy-to-clipboard": "Kopieren", + "copy-transaction-hash": "Transaktions-ID kopieren", + "cost-fee": "Kosten\/Gebühr", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Erstellen", + "create-a-pin": "Erstellen Sie einen 6-stelligen Passcode", + "create-a-puk": "Erstellen eines 12-stelligen PUKs", + "create-category": "Kategorie erstellen", + "create-channel": "Einen Kanal erstellen", + "create-channel-title": "Neuer Kanal", + "create-community": "Eine Community erstellen", + "create-group-chat": "Gruppenchat erstellen", + "create-multiaccount": "Schlüssel generieren", + "create-new-key": "Holen Sie sich neue Schlüssel", + "create-pin": "Erstellen Sie ein 6-stelliges Passwort", + "create-pin-description": "Sie benötigen Ihre Karte + diesen 6-stelligen Passcode, um Status freizuschalten und Transaktionen zu bestätigen", + "created-group-chat-description": "Sie haben die Gruppe {{group-name}} erstellt", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Währung", + "currency-display-name-aed": "VAE-Dirham", + "currency-display-name-afn": "Afghani", + "currency-display-name-ars": "Argentinischer Peso", + "currency-display-name-aud": "Australischer Dollar", + "currency-display-name-bbd": "Barbados-Dollar", + "currency-display-name-bdt": "Bangladescher Taka", + "currency-display-name-bgn": "Lew", + "currency-display-name-bhd": "Bahrain-Dinar", + "currency-display-name-bnd": "Brunei-Dollar", + "currency-display-name-bob": "Boliviano", + "currency-display-name-brl": "Brasilianischer Real", + "currency-display-name-btn": "Ngultrum", + "currency-display-name-cad": "Kanadischer Dollar", + "currency-display-name-chf": "Schweizer Franken", + "currency-display-name-clp": "Chilenischer Peso", + "currency-display-name-cny": "China Yuan Renminbi", + "currency-display-name-cop": "Kolumbianischer Peso", + "currency-display-name-crc": "Costa-Rica-Colón", + "currency-display-name-czk": "Tschechische Krone", + "currency-display-name-dkk": "Dänische Krone", + "currency-display-name-dop": "Dominikanischer Peso", + "currency-display-name-egp": "Ägyptisches Pfund", + "currency-display-name-etb": "Äthiopischer Birr", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Pfund Sterling", + "currency-display-name-gel": "Georgischer Lari", + "currency-display-name-ghs": "Cedi", + "currency-display-name-hkd": "Hongkong-Dollar", + "currency-display-name-hrk": "Kroatische Kuna", + "currency-display-name-huf": "Forint", + "currency-display-name-idr": "Indonesien Rupiah", + "currency-display-name-ils": "Schekel", + "currency-display-name-inr": "Indische Rupie", + "currency-display-name-isk": "Isländische Krone", + "currency-display-name-jmd": "Jamaika-Dollar", + "currency-display-name-jpy": "Japanische YEN", + "currency-display-name-kes": "Kenianischer Schilling", + "currency-display-name-krw": "Südkoreanischer Won", + "currency-display-name-kwd": "Kuwait-Dinar", + "currency-display-name-kzt": "Kasachstan Tenge", + "currency-display-name-lkr": "Sri-Lanka-Rupie", + "currency-display-name-mad": "Marokkanischer Dirham", + "currency-display-name-mdl": "Moldauischer Leu", + "currency-display-name-mur": "Mauritius-Rupie", + "currency-display-name-mwk": "Malawi-Kwacha", + "currency-display-name-mxn": "Mexikanischer Peso", + "currency-display-name-myr": "Malaysia Ringgit", + "currency-display-name-mzn": "Mosambik Metical", + "currency-display-name-nad": "Namibischer Dollar", + "currency-display-name-ngn": "Nigerianischer Naira", + "currency-display-name-nok": "Norwegische Krone", + "currency-display-name-npr": "Nepalesische Rupie", + "currency-display-name-nzd": "Neuseeland-Dollar", + "currency-display-name-omr": "Omanischer Rial", + "currency-display-name-pen": "Peruanischer Sol", + "currency-display-name-pgk": "Kina", + "currency-display-name-php": "Philippinischer Peso", + "currency-display-name-pkr": "Pakistanische Rupie", + "currency-display-name-pln": "Złoty", + "currency-display-name-pyg": "Paraguayischer Guaraní", + "currency-display-name-qar": "Katar Riyal", + "currency-display-name-ron": "Rumänischer Leu", + "currency-display-name-rsd": "Serbischer Dinar", + "currency-display-name-rub": "Russischer Rubel", + "currency-display-name-sar": "Saudi-Riyal", + "currency-display-name-sek": "Schwedische Krone", + "currency-display-name-sgd": "Singapur-Dollar", + "currency-display-name-thb": "Baht", + "currency-display-name-try": "Türkische Lira", + "currency-display-name-ttd": "Trinidad-und-Tobago-Dollar", + "currency-display-name-twd": "Neuer Taiwan-Dollar", + "currency-display-name-tzs": "Tansania-Schilling", + "currency-display-name-uah": "Ukrainische Hrywnja", + "currency-display-name-ugx": "Uganda-Schilling", + "currency-display-name-usd": "US-Dollar", + "currency-display-name-uyu": "Uruguayischer Peso", + "currency-display-name-vef": "Venezolanischer Bolívar", + "currency-display-name-vnd": "Vietnamesischer Đồng", + "currency-display-name-zar": "Südafrikanischer Rand", + "current": "Aktuell", + "current-average": "Aktueller Durchschnitt", + "current-average-tip": "Aktuelles durchschnittliches Trinkgeld", + "current-base": "Aktuelle Basis", + "current-base-fee": "Aktuelle Grundgebühr", + "current-minimum-tip": "Aktuelles Mindesttrinkgeld", + "current-network": "Aktuelles Netzwerk", + "current-password": "Aktuelles Passwort", + "current-pin": "Geben Sie einen 6-stelligen Passcode ein", + "current-pin-description": "Geben Sie Ihren 6-stelligen Passcode ein, um fortzufahren", + "custom": "Benutzerdefiniert", + "custom-networks": "Benutzerdefinierte Netzwerke", + "custom-node": "Sie verwenden einen benutzerdefinierten RPC-Endpunkt. Ihre lokale Übertragungshistorie ist möglicherweise unvollständig.", + "custom-seed-phrase": "Falsche Seed-Phrase", + "custom-seed-phrase-text-1": "Diese Seed-Phrase passt nicht zu unserem unterstützten Wörterbuch. Suchen Sie nach falsch geschriebenen Wörtern.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Akzeptieren und Öffnen", + "dapp-starter-pack-description": "Hier ist etwas Kryptowährung für den Anfang! Benutze diesen, um Sticker zu bekommen, einen ENS-Namen zu erstellen und DApps auszuprobieren.", + "dapp-starter-pack-title": "Starter Pack", + "dapp-would-like-to-connect-wallet": "möchte eine Verbindung zu herstellen", + "dapps": "ÐApps", + "dapps-permissions": "DApp-Berechtigungen", + "dark": "Dunkel", + "data": "Daten", + "data-collected": "Daten gesammelt", + "data-collected-subtitle": "Die folgende Tabelle zeigt die genauen Daten, die gespeichert und gesendet werden. Die Daten werden anhand öffentlicher Regeln validiert, um sicherzustellen, dass keine sensiblen Daten gesendet werden. Vertraue nicht, verifiziere.", + "data-syncing": "Synchronisiere Daten", + "database-reset-content": "Chats, Kontakte und Einstellungen wurden gelöscht. Du kannst dein Konto mit deiner Schlüsselkarte benutzen.", + "database-reset-title": "Datenbank zurücksetzen", + "database-reset-warning": "Die Datenbank wird zurückgesetzt. Chats, Kontakte und Einstellungen werden gelöscht", + "datetime-ago": "vor", + "datetime-ago-format": "{{ago}} {{number}} {{time-intervals}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "Tag", + "other": "Tage" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "Stunde", + "other": "Stunden" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "Minute", + "other": "Minuten" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "Sekunde", + "other": "Sekunden" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "heute", + "datetime-yesterday": "gestern", + "decimals": "Dezimalstellen", + "decline": "Ablehnen", + "decryption-failed-content": "Beim Entschlüsseln Ihrer Daten ist ein Fehler aufgetreten. Möglicherweise müssen Sie Ihre alten Daten löschen und ein neues Konto erstellen. Tippen Sie zum Löschen auf „Übernehmen“ oder zum erneuten Versuch auf „Abbrechen“", + "default": "Standard", + "default-assets": "Standard ERC20 und ERC721", + "default-sync-period": "Synchronisierungsverlauf für", + "delete": "Löschen", + "delete-account": "Konto löschen", + "delete-and-leave-group": "Gruppe löschen und verlassen", + "delete-bootnode": "Bootknoten löschen", + "delete-bootnode-are-you-sure": "Möchten Sie diesen Bootknoten wirklich löschen?", + "delete-bootnode-title": "Bootknoten löschen", + "delete-category-confirmation": "Möchten Sie diese Kategorie wirklich löschen?", + "close-chat": "Chat löschen", + "close-chat-confirmation": "Möchten Sie diesen Chat wirklich löschen?", + "delete-confirmation": "Löschen?", + "delete-keys-keycard": "Schlüssel von der Schlüsselkarte löschen", + "delete-mailserver": "Mailserver löschen", + "delete-mailserver-are-you-sure": "Möchten Sie diesen Mailserver wirklich löschen?", + "delete-mailserver-title": "Mailserver löschen", + "delete-message": "Nachricht löschen", + "delete-my-account": "Mein Konto löschen", + "delete-my-profile": "Mein Profil löschen", + "delete-network-confirmation": "Sind Sie sicher, dass Sie dieses Netzwerk löschen wollen?", + "delete-network-error": "Bitte verbinden Sie sich mit einem anderen Netzwerk, bevor Sie dieses löschen", + "delete-network-title": "Netzwerk löschen?", + "delete-node": "Knoten löschen", + "delete-node-are-you-sure": "Sind Sie sicher, dass Sie diesen Knoten löschen wollen?", + "delete-node-title": "Knoten löschen", + "delete-profile": "Profil löschen", + "delete-profile-warning": "Warnung: Wenn Sie Ihre Seed-Phrase nicht notiert haben, verliersen Sie nach dem Löschen Ihres Profils den Zugang zu Ihrer Wallet.", + "deny": "Verweigern", + "derivation-path": "Ableitungspfad", + "describe-channel": "Beschreiben Sie den Kanal", + "description": "Beschreibung", + "dev-mode": "Entwicklungsmodus", + "dev-mode-settings": "Einstellungen des Entwicklungsmodus", + "device-syncing": "Gerätesynchronisierung", + "devices": "Geräte", + "disable": "deaktivieren", + "disable-all": "Alle deaktivieren", + "disable-later-in-settings": "Sie können dies später in den Einstellungen deaktivieren", + "disabled": "Deaktiviert", + "disconnected": "Chat offline", + "discover": "Entdecken", + "dismiss": "Verwerfen", + "display-collectibles": "Sammlerstücke anzeigen", + "done": "Erledigt", + "dont-ask": "Frag mich nicht noch einmal", + "edit": "Bearbeiten", + "edit-channel-title": "Kanal bearbeiten", + "edit-chats": "Chats bearbeiten", + "edit-community": "Community bearbeiten", + "edit-favourite": "Favorit bearbeiten", + "edit-group": "Gruppe bearbeiten", + "edit-profile": "Profil bearbeiten", + "edited": "Bearbeitet", + "editing-message": "Nachricht bearbeiten", + "empty-activity-center": "Ihre Chat-Benachrichtigungen\nwerden hier erscheinen", + "empty-chat-description": "Es gibt noch keine Nachrichten in diesem Chat", + "empty-chat-description-community": "Es war ruhig hier in den letzten {{quiet-hours}}.", + "empty-chat-description-one-to-one": "Alle Nachrichten, die Sie hier senden, sind verschlüsselt und nur lesbar von Ihnen und ", + "empty-chat-description-public": "Hier gab es die letzten {{quiet-hours}} keine Aktivitäten. Starten Sie das Gespräch oder ", + "empty-chat-description-public-share-this": "Diesen Chat teilen", + "empty-keycard-required": "Erfordert eine leere Keycard", + "empty-pending-invitations-descr": "Gruppenanfragen über einen Einladungslink werden hier dargestellt", + "empty-tab": "Leere Registerkarte", + "enable": "Aktivieren", + "enable-all": "Alle aktivieren", + "enable-link-previews": "Linkvorschau im Chat aktivieren?", + "encrypt-with-password": "Mit Passwort verschlüsseln", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Benutzernamen hinzufügen", + "ens-agree-to": "Ich akzeptiere die ", + "ens-chat-settings": "Chat-Einstellungen", + "ens-custom-domain": "Benutzerdefinierte Domain", + "ens-custom-username-hints": "Geben Sie den gesamten Benutzernamen einschließlich der benutzerdefinierten Domain wie username.domain.eth ein", + "ens-custom-username-taken": "Benutzername gehört Ihnen nicht :(", + "ens-deposit": "Einzahlen", + "ens-dismiss-message": "Hier klicken um zu schließen", + "ens-displayed-with": "Ihre Nachrichten werden anderen angezeigt mit", + "ens-get-name": "Holen Sie sich einen universellen Benutzernamen", + "ens-got-it": "Ok, verstanden", + "ens-locked": "Benutzername gesperrt. Sie können es erst ab dem {{date}} freigeben", + "ens-name-content": "Benutzerdefinierter Alias für Ihren Chat-Schlüssel, den Sie über den Ethereum Name Service registrieren können. ENS-Namen sind dezentrale Benutzernamen.", + "ens-name-not-found": "ENS-Name kann nicht aufgelöst werden", + "ens-name-title": "ENS-Name", + "ens-network-restriction": "Nur im Hauptnetzwerk verfügbar", + "ens-no-usernames": "Sie haben keinen Benutzernamen verknüpft", + "ens-powered-by": "Unterstützt von Ethereum Name Services", + "ens-primary-username": "Primärer Benutzername", + "ens-register": "Registrieren", + "ens-registration-failed": "Versuchen Sie es erneut, um den Benutzernamen zu registrieren.", + "ens-registration-failed-title": "Transaktion fehlgeschlagen", + "ens-registration-failure": "Registrierung fehlgeschlagen", + "ens-registration-in-progress": "Registrierung läuft ...", + "ens-release-username": "Geben Sie den Benutzernamen frei", + "ens-remove-hints": "Durch Entfernen wird der Benutzername von Ihrem Schlüssel entkoppelt.", + "ens-remove-username": "Benutzernamen entfernen", + "ens-saved": " ist jetzt mit Ihrem Chat-Schlüssel verbunden und kann in Status verwendet werden.", + "ens-saved-title": "Benutzername hinzugefügt", + "ens-show-username": "Meinen ENS-Benutzernamen in Chats anzeigen", + "ens-terms-header": "Bedingungen für die Namensregistrierung", + "ens-terms-point-1": "Das Geld wird für ein Jahr hinterlegt. Ihr SNT wird gesperrt, aber nicht ausgegeben.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS-Eintragung).", + "ens-terms-point-2": "Nach einem Jahr können Sie den Namen freigeben und Ihre Einzahlung zurückerhalten oder keine Maßnahmen ergreifen und damit den Namen beibehalten.", + "ens-terms-point-3": "Wenn sich die Contract-Bedingungen ändern – z. B. Status führt Contract-Aktualisierungen durch – hat der Benutzer das Recht, den Benutzernamen freizugeben, und zwar unabhängig von der Haltezeit.", + "ens-terms-point-4": "Der Contract Controller kann nicht auf Ihre eingezahlten Beträge zugreifen. Sie können nur an die Adresse zurücknewegt werden, an die sie gesendet wurden.", + "ens-terms-point-5": "Ihre Adresse(n) werden öffentlich mit Ihrem ENS-Namen verknüpft.", + "ens-terms-point-6": "Benutzernamen werden als Subdomain-Knoten von stateofus.eth erstellt und unterliegen den ENS-Smart-Contract-Bedingungen.", + "ens-terms-point-7": "Sie ermächtigen den Contract, SNT in Ihrem Namen zu übertragen. Dies kann nur passieren, wenn Sie eine Transaktion zur Autorisierung der Übertragung genehmigen.", + "ens-terms-point-8": "Diese Bedingungen werden durch die intelligente Smart-Contract-Logik an folgenden Adressen garantiert:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Bedingungen für die Namensregistrierung.", + "ens-test-message": "Hallo", + "ens-transaction-pending": "Transaktion ausstehend...", + "ens-understand": "Ich verstehe, dass meine Wallet-Adresse mit meinem Benutzernamen öffentlich verknüpft wird.", + "ens-username": "ENS-Benutzername", + "ens-username-already-added": "Der Benutzername ist bereits mit Ihrem Chat-Schlüssel verbunden und kann in Status verwendet werden.", + "ens-username-available": "✓ Benutzername verfügbar!", + "ens-username-connected": "Dieser Benutzername gehört Ihnen und ist mit Ihrem Chat-Schlüssel verbunden.", + "ens-username-connected-continue": "Fortsetzen, um die Anzeige des ENS-Benutzernamen in Chats einzustellen", + "ens-username-connected-with-different-key": "Für die Fortsetzung ist eine Transaktion erforderlich, um den Benutzernamen mit Ihrem aktuellen Chat-Schlüssel zu verbinden.", + "ens-username-connection-confirmation": "{{username}} wird verbunden, sobald die Transaktion abgeschlossen ist.", + "ens-username-hints": "Mindestens 4 Zeichen. Nur lateinische Buchstaben, Zahlen und alles in Kleinbuchstaben.", + "ens-username-invalid": "Nur Buchstaben und Zahlen.", + "ens-username-owned": "✓ Der Benutzername gehört Ihnen. ", + "ens-username-owned-continue": "Wenn Sie fortfahren, wird dieser Benutzername mit Ihrem Chat-Schlüssel verbunden.", + "ens-username-registration-confirmation": "Super! Sie besitzen {{username}}, sobald die Transaktion abgeschlossen ist.", + "ens-username-taken": "Benutzername bereits vergeben :(", + "ens-username-you-can-follow-progress": "Sie können den Fortschritt im Abschnitt \"Transaktionsverlauf\" Ihrer Wallet verfolgen.", + "ens-usernames": "ENS-Benutzernamen", + "ens-usernames-details": "Registrieren Sie einen universellen Benutzernamen, der von anderen Benutzern leicht erkannt werden kann", + "ens-want-custom-domain": "Ich besitze einen Namen auf einer anderen Domain", + "ens-want-domain": "Ich möchte eine stateofus.eth-Domain", + "ens-welcome-hints": "ENS-Namen verwandeln diese verrückten langen Adressen in eindeutige Benutzernamen.", + "ens-welcome-point-customize": "Ein ENS Name kann ihren zufälligen 3-Wort-Namen im Chat ersetzen. Seien sie @yourname anstelle von {{name}}.", + "ens-welcome-point-customize-title": "Wählen Sie einen Chat-Namen", + "ens-welcome-point-receive": "Andere können ihnen in einem Schritt Geld per Chat senden.", + "ens-welcome-point-receive-title": "Erhalten Sie Transaktionen im Chat", + "ens-welcome-point-register": "Registrieren Sie sich ein Mal, um den Namen für immer zu behalten. Nach einem Jahr können Sie den Namen freigeben und Ihre SNT zurückerhalten.", + "ens-welcome-point-register-title": "10 SNT für die Registrierung", + "ens-welcome-point-simplify": "Sie können Geld auf Ihren einfach zu teilenden ENS-Namen anstatt auf Ihren hexadezimalen Hash (0x ...) erhalten.", + "ens-welcome-point-simplify-title": "Vereinfachen Sie Ihre ETH-Adresse", + "ens-welcome-point-verify": "In den nächsten Schritten können Sie alle Benutzernamen überprüfen und hinzufügen, die Sie besitzen.", + "ens-welcome-point-verify-title": "Haben Sie bereits einen Benutzernamen?", + "ens-your-username": "Ihr Benutzername", + "ens-your-usernames": "Ihre Benutzernamen", + "ens-your-your-name": "Ihr ENS-Name", + "enter-12-words": "Geben Sie die 12 Wörter Ihrer Seed-Phrase ein, die durch einzelne Leerzeichen getrennt sind", + "enter-a-private-key": "Geben Sie einen Schlüssel ein", + "enter-a-seed-phrase": "Geben Sie ein eine Seed-Phrase ein", + "enter-address": "Adresse eingeben", + "enter-contact-code": "ENS-Name (vitalik94) oder Chat-Key (0x04…) eingeben", + "enter-pair-code": "Geben Sie Ihren Kopplungscode ein", + "enter-pair-code-description": "Der Kopplungscode wurde Ihnen während des Keycard-Setups angezeigt", + "enter-password": "Passwort eingeben", + "enter-password-migration-prompt": "Geben Sie Ihr Passwort ein, um Kontakte, Chats und Einstellungen zusammen mit Ihren Schlüsseln zu verschieben", + "enter-pin": "Geben Sie einen 6-stelligen Passcode ein", + "enter-puk-code": "Geben Sie den PUK-Code ein", + "enter-puk-code-description": "Der 6-stellige Passcode wurde gesperrt. \n Bitte geben Sie den PUK-Code ein, um den Passcode zu entsperren.", + "enter-recipient-address-or-username": "Geben Sie die Adresse oder den Benutzernamen des Empfängers ein", + "enter-seed-phrase": "Seed-Phrase eingeben", + "enter-url": "URL eingeben", + "enter-user-pk": "Öffentlichen Schlüssel eingeben", + "enter-watch-account-address": "Scannen Sie einen QR-Code \noder \ngeben Sie die Adresse ein, die Sie sehen möchten", + "enter-word": "Wort eingeben", + "enter-your-code": "Geben Sie Ihren 6-stelligen Code ein", + "enter-your-password": "Geben Sie Ihr Passwort ein", + "error": "Fehler", + "error-unable-to-get-balance": "Abrufen des Kontostands fehlgeschlagen", + "error-unable-to-get-prices": "Währungsumrechnungsfehler. Aktualisieren Sie Ihren Bildschirm, um es erneut zu versuchen.", + "error-unable-to-get-token-balance": "Token-Guthaben kann nicht abgerufen werden", + "errors": "Fehler", + "eth": "ETH", + "ethereum-account": "Ethereum-Konto", + "ethereum-address": "Ethereum-Adresse", + "ethereum-node-started-incorrectly-description": "Der Ethereum-Knoten wurde mit einer falschen Konfiguration gestartet. Die Anwendung wird gestoppt, um diesen Zustand zu beheben. Konfigurierte Netzwerk-ID = {{network-id}} , jetzt = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Der Ethereum-Knoten wurde falsch gestartet", + "etherscan-lookup": "Schauen Sie bei Etherscan nach", + "everyone": "Jedem", + "expand-all": "Alle erweitern", + "export-account": "Konto exportieren", + "export-key": "Privaten Schlüssel exportieren", + "external-storage-denied": "Der Zugriff auf den externen Speicher wird verweigert", + "failed": "Fehlgeschlagen", + "faq": "Häufig gestellte Fragen", + "fast": "Schnell", + "favourite": "Favorit", + "favourite-description": "Ihre Lieblingswebsites werden hier angezeigt", + "favourites": "Favoriten", + "favourites-empty": "Favorisierte Adressen werden hier angezeigt", + "fee-cap": "Gebührenobergrenze", + "fee-explanation": "Maximaler Gesamtpreis für die Transaktion. Übersteigt die Blockgrundgebühr diese, wird sie in einen Folgeblock mit niedrigerer Grundgebühr eingerechnet.", + "fee-options": "Empfohlene Gebührenoptionen", + "fetch-community": "Community abrufen", + "fetch-messages": "↓ Nachrichten abrufen", + "fetch-timeline": "↓ Abrufen", + "fetching-community": "Community wird abgerufen...", + "find": "Finden", + "finish": "Fertig", + "finishing-card-setup": "Karteneinrichtung wird beendet", + "fleet": "Fleet", + "fleet-settings": "Fleet-Einstellungen", + "follow": "Folgen", + "follow-your-interests": "Gehen Sie in einen öffentlichen Chat und lernen Sie neue Leute kennen", + "free": "↓ Kostenlos", + "from": "Von", + "gas-amount-limit": "Obergrenze der Gasmenge", + "gas-limit": "Gaslimit", + "gas-price": "Gaspreis", + "gas-used": "Gas verbraucht", + "generate-a-key": "Schlüssel generieren", + "generate-a-new-account": "Konto erstellen", + "generate-a-new-key": "Neuen Schlüssel generieren", + "generate-account": "Schlüssel generieren", + "generate-an-account": "Konto erstellen", + "generate-new-key": "Schlüssel generieren", + "generating-codes-for-pairing": "> Herunterladen der Produktsoftware auf die Karte \n > Generieren von Entsperr- und Kopplungscodes", + "generating-keys": "Schlüssel werden generiert ...", + "generating-mnemonic": "Seed-Phrase generieren", + "get-a-keycard": "Holen Sie sich eine Keycard", + "get-started": "Loslegen", + "get-status-at": "Laden Sie Status unter http:\/\/status.im herunter", + "get-stickers": "Sticker hinzufügen", + "give-a-short-description": "Geben Sie eine kurze Beschreibung", + "give-a-short-description-community": "Beschreiben Sie es kurz", + "give-permissions-camera": "Erlaubnis erteilen,\num auf die Kamera zuzugreifen", + "glossary": "Glossar", + "go-to-settings": "Gehen Sie zu Einstellungen...", + "got-it": "Verstanden", + "grant-face-id-permissions": "Um die erforderliche Berechtigung für Face ID zu erteilen, gehen Sie zu Ihren Systemeinstellungen und stellen Sie sicher, dass Status > Face ID ausgewählt ist", + "group-chat": "Gruppenchat", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** wurde zum Administrator ernannt", + "group-chat-all-contacts-invited": "Alle Ihre Kontakte sind bereits in der Gruppe", + "group-chat-created": "**{{member}}** hat die Gruppe **{{name}}** erstellt", + "group-chat-decline-invitation": "Einladung ablehnen", + "group-chat-member-added": "**{{member}}** wurde eingeladen", + "group-chat-member-joined": "**{{member}}** ist der Gruppe beigetreten", + "group-chat-member-removed": "** {{member}} ** hat die Gruppe verlassen", + "group-chat-members-count": "{{selected}}\/{{max}} Mitglieder", + "group-chat-name-changed": "**{{member}}** hat den Namen der Gruppe in **{{name}}** geändert", + "group-chat-no-contacts": "Sie haben noch keine Kontakte. \nLaden Sie Ihre Freunde zum Chatten ein", + "group-info": "Gruppeninfo", + "group-invite": "Gruppeneinladung", + "group-invite-link": "Gruppeneinladungs-Link", + "group-membership-request": "Antrag auf Gruppenmitgliedschaft", + "gwei": "Gwei", + "has-permissions": "hat Zugriffsberechtigung", + "hash": "Hash", + "help": "Hilfe", + "help-capitalized": "Hilfe", + "help-center": "Hilfezentrum", + "help-improve-status": "Helfen Sie mit, Status zu verbessern", + "hide": "Ausblenden", + "hide-content-when-switching-apps": "Screenshots blockieren", + "hide-content-when-switching-apps-ios": "Vorschau ausblenden", + "history": "Transaktionsverlauf", + "history-nodes": "Verlaufsknoten", + "hold-card": "Halten Sie die Karte an der Rückseite\n Ihres Smartphones", + "home": "Startseite", + "hooks": "Hooks", + "how-it-works": "Wie es funktioniert", + "http-gateway-error": "Oh nein, Anfrage ist fehlgeschlagen", + "identifier": "Kennung", + "image": "Bild", + "image-remove-current": "Aktuelles Foto löschen", + "image-source-gallery": "Aus Galerie auswählen", + "image-source-make-photo": "mit Kamera aufnehmen", + "image-source-title": "Bild bearbeiten", + "import": "Importieren", + "import-community": "Importieren Sie eine Community", + "import-community-title": "Importieren Sie eine Community", + "in-contacts": "In Kontakten", + "include": "Enthalten", + "incoming": "Eingehend", + "incoming-transaction": "Eingehende Transaktion", + "incorrect-code": [ + "str", + "Entschuldigung, der Code war falsch, bitte geben Sie ihn erneut ein" + ], + "increase-gas": "Gas erhöhen", + "initialization": "Initialisierung", + "install": "↓ Installieren", + "intro-message1": "Willkommen bei Status!\nKlicke auf diese Nachricht, um dein Passwort festzulegen und loszulegen.", + "intro-privacy-policy-note1": "Status sammelt, teilt oder verkauft keine persönlichen Daten. Wenn Sie fortfahren, akzeptieren Sie die ", + "intro-privacy-policy-note2": "Datenschutzerklärung", + "intro-text": "Status ist Ihr Tor zum dezentralisierten Web", + "intro-text1": "Chatten Sie über ein Peer-to-Peer-Netzwerk, in dem Nachrichten nicht zensiert oder gehackt werden können", + "intro-text2": "Senden und empfangen Sie digitale Assets überall auf der Welt - kein Bankkonto erforderlich", + "intro-text3": "Entdecken Sie Spiele, Börsen und soziale Netzwerke, in denen nur Sie Ihre Daten besitzen", + "intro-title1": "Wirklich private Kommunikation", + "intro-title2": "Sichere Krypto-Wallet", + "intro-title3": "Dezentrale Apps", + "intro-wizard-text1": "Ein Schlüsselsatz steuert Ihr Konto. Ihre Schlüssel befinden sich auf Ihrem Handy, damit nur Sie sie verwenden können", + "intro-wizard-text2": "Ein Schlüssel ist für den Chat. Es kommt mit einem lesbaren Namen, der nicht geändert werden kann.", + "intro-wizard-text3": "Wenn Sie eine Keycard besitzen, bewahren Sie Ihre Schlüssel dort auf, um die Sicherheit zu erhöhen.", + "intro-wizard-text4": "Sichern und verschlüsseln Sie Ihre Schlüssel", + "intro-wizard-text6": "Status benachrichtigt Sie über neue Nachrichten. Sie können Ihre Benachrichtigungseinstellungen später in den Einstellungen bearbeiten", + "intro-wizard-title-alt4": "Erstellen Sie ein Passwort", + "intro-wizard-title-alt5": "Bestätigen Sie Ihr Passwort", + "intro-wizard-title1": "Holen Sie sich Ihre Schlüssel", + "intro-wizard-title2": "Wählen Sie einen Chat-Namen", + "intro-wizard-title3": "Schlüsselspeicher auswählen", + "intro-wizard-title4": "Erstellen Sie einen 6-stelligen Code", + "intro-wizard-title5": "Bestätigen Sie den Code", + "intro-wizard-title6": "Benachrichtigungen aktivieren", + "introduce-yourself": "Stelle dich mit einer kurzen Nachricht vor", + "invalid-address-qr-code": "Der gescannte QR-Code enthält keine gültige Adresse", + "invalid-format": "Ungültiges Format \nEs muss wie folgt aussehen: {{format}}", + "invalid-key-confirm": "Anwenden", + "invalid-key-content": "Die Datenbank kann nicht verschlüsselt werden, weil eine Datei beschädigt ist. Ihr Guthaben und Ihr Chat-Schlüssel sind sicher. Andere Daten, wie Ihre Chats und Kontakte, können nicht wiederhergestellt werden. Die Schaltfläche \"{{erase-multiaccounts-data-button-text}}\" entfernt alle anderen Daten und ermöglicht Ihnen den Zugriff auf Ihre Gelder und das Senden von Nachrichten.", + "invalid-number": "Ungültige Nummer", + "invalid-pairing-password": "Ungültiges Kopplungspasswort", + "invalid-public-chat-topic": "Ungültiges öffentliches Chat-Thema", + "invalid-range": "Ungültiges Format, es muss zwischen {{min}} und {{max}} betragen", + "invalid-username-or-key": "Ungültiger Benutzername oder Chat-Schlüssel", + "invite": "Einladen", + "invite-button": "Einladen", + "invite-chat-accept": "Akzeptieren", + "invite-chat-accept-join": "Akzeptieren und Beitreten", + "invite-chat-intro": "Sie wurden von einem Freund zur Teilnahme an Status eingeladen. Hier ist etwas Kryptowährung, um den Einstieg zu erleichtern! Verwenden Sie diese, um einen ENS-Namen zu registrieren oder ein Sticker-Paket zu kaufen.", + "invite-chat-name": "Freunde Empfehlung", + "invite-chat-pending": "Ausstehend", + "invite-chat-rule": "Die Annahme belohnt auch Ihren Freund mit einem Krypto-Empfehlungsbonus", + "invite-chat-starter-pack": "Starter Pack", + "invite-friends": "Freunde einladen", + "invite-instruction-fifth": "Du kannst deinen Empfehlungsbonus jederzeit einlösen.", + "invite-instruction-first": "Sie senden einem Freund einen eindeutigen Einladungslink zum Herunterladen und Nutzen von Status", + "invite-instruction-fourth": "Sie erhalten einen Empfehlungsbonus und Ihr Freund das Starter Pack", + "invite-instruction-second": "Ihr Freund lädt Status herunter und erstellt ein Konto (auf Android)", + "invite-instruction-third": "Ein Chat mit Ihrem Freund wird gestartet, in dem er Ihre Überweisung bestätigt", + "invite-people": "Leute einladen", + "invite-privacy-policy-public": "Sie haben Status über einen Empfehlungslink installiert. Indem Sie diesem Chat beitreten, geben Sie Ihren Referrer an und erklären sich einverstanden mit den", + "invite-privacy-policy1": "Mit der Annahme akzeptieren Sie die Empfehlungsprogramm", + "invite-privacy-policy2": "Geschäftsbedingungen.", + "invite-public-chat-home": "Einladung zur Empfehlung", + "invite-public-chat-intro": "Hier ist etwas Kryptowährung für den Anfang! Verwenden Sie es, um einen ENS-Namen zu registrieren oder ein Stickerpaket zu kaufen", + "invite-receive-account": "Konto, um Ihren Empfehlungsbonus zu erhalten", + "invite-reward": "Verdienen Sie Kryptowährung für jeden Freund, den Sie einladen!", + "invite-reward-friend": "Freund:", + "invite-reward-friend-description": "Ihr\/e Freund\/in erhält ein Starter-Paket, bestehend aus einigen {{reward}}, um anzufangen", + "invite-reward-friend-name": "Starter Pack", + "invite-reward-you": "Sie: ", + "invite-reward-you-description": "Laden Sie einen Freund ein und erhalten {{reward}} als Empfehlungsbonus. Verwenden Sie diesen, um Sticker, einen ENS-Namen zu erhalten oder DApps auszuprobieren", + "invite-reward-you-name": "Empfehlungs-Bonus", + "invite-select-account": "Wählen Sie ein Konto aus, um den Empfehlungsbonus zu erhalten", + "invite-warning": "Der Bonus ist nur für Android Benutzer gültig, welche nicht in den USA leben. Ihr Freund muss den Werbe-Link innerhalb von 7 Tagen bestätigen.", + "invited": "eingeladen", + "join": "Beitreten", + "join-a-community": "oder einer Community beitreten", + "join-group-chat": "Gruppe beitreten", + "join-group-chat-description": "{{username}} hat Sie eingeladen, der Gruppe {{group-name}} beizutreten ", + "join-me": "Hey, lass uns auf Status schreiben: {{url}}", + "join-new-private-chat": "Einen neuen privaten Chat starten", + "join-new-public-chat": "Einem öffentlichen Chat beitreten", + "joined": "Beigetreten", + "joined-group-chat-description": "Sie sind {{group-name}} durch die Einladung von {{username}} beigetreten", + "key": "Schlüssel", + "key-managment": "Schlüsselverwaltung", + "key-on-device": "Privater Schlüssel ist auf diesem Gerät gespeichert", + "keycard": "Keycard", + "keycard-access-reset": "Keycard-Zugriff ist zurückgesetzt", + "keycard-applet-install-instructions": "Befolgen Sie zum Installieren des Applets die Anweisungen unter https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Bewegen Sie die Karte, um den NFC-Leser auf Deinem Gerät zu finden", + "keycard-awaiting-title": "Immer noch auf der Suche...", + "keycard-backup": "Sicherungskarte erstellen", + "keycard-backup-success-body": "Sicherungskarte erfolgreich erstellt. Sie kann genau wie die Hauptkarte mit deinem Konto benutzt werden.", + "keycard-backup-success-title": "Sicherung erfolgreich", + "keycard-blocked": "Keycard wurde gesperrt. \nSie müssen die Karte zurücksetzen, um sie weiter verwenden zu können.", + "keycard-can-use-with-new-passcode": "Sie können diese Karte mit Ihrem neuen Zugangscode verwenden", + "keycard-cancel-setup-text": "Dadurch wird die Einrichtung der Keycard abgebrochen. Es wird dringend empfohlen, die Einrichtung abzuschließen, um die Keycard zu verwenden. Möchten Sie wirklich abbrechen?", + "keycard-cancel-setup-title": "Gefährliche Aktion", + "keycard-connected-description": "Versuchen Sie, die Karte ruhig zu halten", + "keycard-connected-title": "Verbunden", + "keycard-desc": "Besitzen Sie eine Keycard? Bewahren Sie Ihre Schlüssel darauf auf. Sie benötigen sie für Transaktionen", + "keycard-dont-ask-card": "Für die Anmeldung nicht nach einer Karte fragen", + "keycard-enter-new-passcode": "Neuen Zugangscode eingeben {{step}}\/2", + "keycard-error-description": "Schließe die Karte erneut an, um fortzufahren", + "keycard-error-title": "Verbindung unterbrochen", + "keycard-factory-reset": "Karte auf Werkseinstellungen zurücksetzen", + "keycard-factory-reset-text": "Dadurch werden alle auf der Karte gespeicherten Seed-Phrasen gelöscht. Stellen Sie sicher, dass Sie eine Sicherungskopie der Seed-Phrase haben, die Sie mit dieser Keycard verwendet haben.", + "keycard-factory-reset-title": "Sind Sie sicher, dass Sie einen Werksreset durchführen wollen?", + "keycard-free-pairing-slots": "Die Keycard verfügt über {{n}} freie Kopplungsslots", + "keycard-has-multiaccount-on-it": "Diese Karte ist voll. Jede Karte kann ein Hauptschlüsselpaar enthalten", + "keycard-init-description": "Legen Sie die Karte an die Rückseite Ihres Smartphones, um fortzufahren", + "keycard-init-title": "Suche Karten...", + "keycard-is-blocked-details": "Sie können diese Karte nicht mehr verwenden, um auf dieses Konto zuzugreifen oder es zu unterzeichnen. Es gab zu viele fehlgeschlagene Zugangscode- und PUK-Versuche.", + "keycard-is-blocked-instructions": "Um auf Ihr Konto zugreifen zu können, müssen Sie Ihre Karte auf die Werkseinstellungen zurücksetzen. Tippen Sie auf die Schaltfläche unten, um den Vorgang zu starten. Sie benötigen Ihre Seed-Phrase.", + "keycard-is-blocked-title": "Keycard ist gesperrt", + "keycard-is-frozen-details": "Um Ihr Vermögen zu schützen, wurde Ihre Karte eingefroren. Setzen Sie den Kartenzugriff zurück, um die Schlüssel wieder freizuschalten und Transaktionen zu senden. Das können Sie mit Ihrem PUK oder Ihrer Seed-Phrase.", + "keycard-is-frozen-factory-reset": "Zurücksetzen mit Mnemonik", + "keycard-is-frozen-reset": "Zurücksetzen mit PUK", + "keycard-is-frozen-title": "Keycard ist eingefroren", + "keycard-onboarding-finishing-header": "Der letzte Feinschliff...", + "keycard-onboarding-intro-header": "Speichern Sie Ihre Schlüssel auf der Keycard", + "keycard-onboarding-intro-text": "Machen Sie sich bereit, dies kann einige Minuten dauern, aber es ist wichtig, um Ihr Konto zu sichern", + "keycard-onboarding-mnemonic-text": "Sie benötigen auch ein Blatt Papier und einen Bleistift, um Ihre Seed-Phrase aufzuschreiben.", + "keycard-onboarding-pairing-header": "Karte koppeln...", + "keycard-onboarding-pin-text": "Sie müssen einen 6-stelligen Passcode erstellen, der verwendet wird, um den Zugriff auf Ihre Keycard zu schützen.", + "keycard-onboarding-preparing-header": "Karte vorbereiten...", + "keycard-onboarding-puk-code-header": "Schreiben Sie die Codes auf\nund bewahren Sie sie sicher auf", + "keycard-onboarding-recovery-phrase-description": "Sie benötigen diese Seed-Phrase, um Ihren Schlüssel zurückzubekommen. Schreiben Sie sie auf. Bewahren Sie sie sicher, offline und getrennt von diesem Gerät auf.", + "keycard-onboarding-recovery-phrase-header": "Sichern der Seed-Phrase", + "keycard-onboarding-recovery-phrase-text": "Nur für Ihre Augen! Dies ist die magische Seed-Phrase, mit der Sie Ihren Schlüssel generieren.", + "keycard-onboarding-start-header": "Halten Sie die Karte an die\nRückseite des Smartphones,\num zu beginnen", + "keycard-onboarding-start-step1": "Erstellen Sie einen Passcode", + "keycard-onboarding-start-step1-text": "Dauert nur eine Minute. Erstellen Sie einen 6-stelligen Passcode, um Ihre Schlüssel zu verschlüsseln", + "keycard-onboarding-start-step2": "Schreiben Sie die PUK und den Kopplungscode", + "keycard-onboarding-start-step2-text": "Das dauert etwa eine Minute. Sie werden dafür ein Stück Papier und einen Stift brauchen", + "keycard-onboarding-start-step3": "Seed-Phrase sichern", + "keycard-onboarding-start-step3-text": "Das dauert etwa eine Minute. Sie brauchen noch ein Stück Papier und einen Stift", + "keycard-onboarding-start-text": "Halten Sie die Karte die ganze Zeit am Smartphone\n während der Initialisierung. Das wird etwa vier Minuten dauern", + "keycard-processing-description": "Versuchen Sie, die Karte ruhig zu halten", + "keycard-processing-title": "Verarbeitung...", + "keycard-recover": "Karte verloren oder gesperrt?", + "keycard-recover-text": "Wenn Sie Ihre Seed-Phrase haben, können Sie eine neue Keycard erstellen, die mit diesem Konto verbunden ist. Sie können entweder eine neue Keycard verwenden oder eine eingefrorene Keycard auf die Werkseinstellungen zurücksetzen.", + "keycard-recover-title": "Eine neue Karte für dieses Konto erstellen?", + "keycard-recovery-intro-button-text": "Wiederherstellung beginnen", + "keycard-recovery-intro-header": "Auf Keycard gespeicherte Schlüssel wiederherstellen", + "keycard-recovery-intro-text": "Wenn Sie zuvor Schlüssel auf einer Keycard generiert haben und diese Schlüssel jetzt auf diesem Gerät verwenden möchten", + "keycard-recovery-no-key-header": "Es gibt nichts zum\nWiederherstellen", + "keycard-recovery-no-key-text": "Auf Ihrer Keycard ist kein Schlüssel gespeichert. Um sie zu verwenden, generieren Sie einen neuen Schlüssel und wählen Sie Ihre Keycard aus, um den Schlüssel zu speichern", + "keycard-recovery-phrase-confirm-header": "Bestätigen Sie die Seed-Phrase", + "keycard-recovery-phrase-confirmation-text": "Sie werden keine zweite Chance haben! Wenn Sie den Zugang verlieren, z.B. durch den Verlust Ihrer Schlüsselkarte, können Sie nur noch mit Ihrer Seed-Phrase auf Ihre Schlüssel zugreifen. Niemand außer Ihnen hat Ihre Seed-Phrase. Schreiben Sie sie auf und bewahren Sie sie sicher auf.", + "keycard-recovery-phrase-confirmation-title": "Haben Sie die Seed-Phrase aufgeschrieben?", + "keycard-recovery-success-header": "Ihre Schlüssel wurden\nerfolgreich wiederhergestellt", + "keycard-redeem-title": "Einlösen auf", + "keycard-redeem-tx": "Assets einlösen", + "keycard-redeem-tx-desc": "Halten Sie die Karte ans mobile Endgerät, um zu signieren und Assets zu erhalten", + "keycard-reset-passcode": "Zugangscode zurücksetzen", + "keycard-success-description": "Du kannst die Karte jetzt entfernen", + "keycard-success-title": "Erfolg", + "keycard-unauthorized-operation": "Sie sind nicht berechtigt, diesen Vorgang auszuführen. \n Bitte benutzen Sie eine gültige Karte und versuchen Sie es erneut.", + "keycard-upsell-subtitle": "Ihre tragbare, einfach zu bedienende Hardware-Geldbörse", + "language": "Sprache", + "last-backup-performed": "Letzte durchgeführte Sicherung:", + "last-transaction": "Letzte Transaktion", + "learn-more": "Mehr erfahren", + "learn-more-about-keycard": "Erfahren Sie mehr über Keycards", + "leave": "Verlassen", + "leave-chat": "Chat verlassen", + "leave-chat-confirmation": "Der Chatverlauf wird von Ihrem Gerät entfernt. Nach dem erneuten Beitritt können Sie Ihren Verlauf nicht mehr wiederherstellen.", + "leave-community": "Gemeinschaft verlassen", + "leave-confirmation": "Verlasse {{chat-name}}", + "leave-group": "Gruppe verlassen", + "left": "verlassen", + "les-ulc": "LES\/ULC", + "lets-go": "Los geht's", + "light": "Hell", + "limit": "Limit", + "linked-on": "Verknüpft am {{date}}", + "load-messages-before": "Vor dem {{date}}", + "load-more-messages": "↓ Weitere Nachrichten abrufen", + "load-more-timeline": "↓ Mehr laden", + "loading": "Laden…", + "local-notifications": "Lokale Benachrichtigungen", + "lock-app-with": "App sperren mit", + "log-level": "Log Einstellungen", + "log-level-settings": "Einstellungen der Protokollebene", + "logging": "Protokollierung", + "logging-enabled": "Protokollierung aktiviert?", + "login-pin-description": "Geben Sie Ihren 6-stelligen Passcode ein, um Ihre Schlüssel zu entsperren", + "logout": "Ausloggen", + "logout-app-content": "Das Konto wird abgemeldet. Wenn Sie es erneut entsperren, wird das ausgewählte Netzwerk verwendet.", + "logout-are-you-sure": "Möchten sie sich wirklich ausloggen?", + "logout-key-management": "Um auf die Schlüsselverwaltung zugreifen zu können, müssen Sie sich abmelden.", + "logout-title": "Ausloggen?", + "looking-for-cards": "Such Karten...", + "lost-connection": "Verbindung verloren", + "low-tip": "Trinkgeld ist zu niedrig", + "lower-than-average-tip": "unterdurchschnittliches Trinkgeld", + "mail-should-be-configured": "Mail-Client sollte konfiguriert werden", + "mailserver-address": "Mailserver-Adresse", + "mailserver-automatic": "Automatische Auswahl", + "mailserver-automatic-switch-explanation": "Wählen Sie den schnellsten verfügbaren Verlaufsknoten", + "mailserver-connection-error": "Verbindung zum Mailserver konnte nicht hergestellt werden", + "mailserver-content": "Ein Knoten im Statusnetzwerk, der Nachrichten für bis zu 30 Tage weiterleitet und speichert.", + "mailserver-details": "Mailserver-Details", + "mailserver-error-content": "Der von Ihnen ausgewählte Mailserver konnte nicht erreicht werden.", + "mailserver-error-title": "Fehler beim Herstellen einer Verbindung zum Mailserver", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Wählen Sie einen anderen Mailserver", + "mailserver-reconnect": "Verbindung zum Mailserver konnte nicht hergestellt werden. Tippen Sie, um die Verbindung wiederherzustellen", + "mailserver-request-error-content": "Der folgende Fehler wurde vom Mailserver zurückgegeben: {{error}}", + "mailserver-request-error-status": "Beim Abrufen des Verlaufs ist ein Fehler aufgetreten. Überprüfen Sie die Protokolle auf Details.", + "mailserver-request-error-title": "Fehler bei Mailserver-Anfrage", + "mailserver-request-retry": "Anfrage wiederholen", + "mailserver-retry": "Wiederholen", + "mailserver-title": "Mail-Server", + "main-currency": "Standardwährung", + "main-networks": "Hauptnetzwerke", + "main-wallet": "Haupt-Wallet", + "mainnet-network": "Hauptnetzwerk", + "make-admin": "Zum Admin ernennen", + "manage-keys-and-storage": "Verwalten von Schlüsseln und Speichern", + "mark-all-read": "Alles als gelesen markieren", + "master-account": "Master-Konto", + "max-fee": "Maximale Gebühr", + "max-priority-fee": "Maximale Prioritätsgebühr", + "maximum-fee": "Maximale Gebühr", + "maximum-fee-desc": "Maximaler Gesamtpreis für die Transaktion. Wenn die aktuelle Blockgrundgebühr diese übersteigt, wird Ihre Transaktion in einen nachfolgenden Block mit einer niedrigeren Grundgebühr aufgenommen.", + "maybe-later": "Vielleicht später", + "member-ban": "Mitglied sperren", + "member-kick": "Mitglied rauswerfen", + "members": { + "one": "1 Mitglied", + "other": "{{count}} Mitglieder" + }, + "members-active": { + "one": "1 Mitglied", + "other": "{{count}} Mitglieder" + }, + "members-active-none": "keine Mitglieder", + "members-count": "{{count}} Mitglieder", + "members-label": "Mitglieder", + "members-limit-reached": "Maximale Anzahl an Teilnehmern erreicht", + "members-title": "Mitglieder", + "membership-approval": "Genehmigung erforderlich", + "membership-approval-description": "Der Beitritt zu Ihrer Community ist frei, aber neue Mitglieder müssen zunächst vom Ersteller der Community genehmigt werden", + "membership-button": "Voraussetzung für die Mitgliedschaft", + "membership-declined": "Die Gruppenanfrage wurde abgelehnt", + "membership-description": "Die Einstellungen der Gruppe erfordern, dass ein Admin dich freischaltet", + "membership-ens": "ENS-Benutzername erforderlich", + "membership-ens-description": "Ihre Community benötigt einen ENS-Benutzernamen, um beitreten zu können", + "membership-free": "Keine Bedingung", + "membership-free-description": "Deiner Community kann jeder beitreten", + "membership-invite": "Einladung von einem anderen Mitglied anfordern", + "membership-invite-description": "Ihrer Community kann man nur durch eine Einladung von bestehenden Communitymitgliedern beitreten", + "membership-none": "Keine", + "membership-none-placeholder": "Sie können von neuen Mitgliedern verlangen, dass sie bestimmte Kriterien erfüllen, bevor sie beitreten können. Dies kann jederzeit geändert werden", + "membership-request-pending": "Antrag auf Mitgliedschaft steht noch aus", + "membership-requests": "Anfragen zur Mitgliedschaft", + "membership-title": "Voraussetzung für die Mitgliedschaft", + "message": "Nachricht", + "message-not-sent": "Nachricht nicht gesendet", + "message-options-cancel": "Abbrechen", + "message-reply": "Antworten", + "messages": "Nachrichten", + "messages-from-contacts-only-subtitle": "Nur Personen, die Sie als Kontakte hinzugefügt haben, können einen neuen Chat mit Ihnen beginnen oder Sie zu einer Gruppe einladen", + "might-break": "Könnte einige ÐApps zum Absturz bringen", + "migration-successful": "Migration erfolgreich", + "migration-successful-text": "Konto erfolgreich auf Schlüsselkarte migriert", + "migrations-failed-content": "{{message}}\nSchemaversion: Anfängliche {{initial-version}}, Aktuelle {{current-version}}, Letzte {{last-version}}\n\nEin Datenbankfehler ist aufgetreten. Ihr Guthaben und Ihr Chat-Schlüssel sind sicher. Andere Daten, wie Ihre Chats und Kontakte, können nicht wiederhergestellt werden. Die Schaltfläche \"{{erase-multiaccounts-data-button-text}}\" entfernt alle anderen Daten und ermöglicht Ihnen den Zugriff auf Ihr Guthaben und das Senden von Nachrichten.", + "miners-higher-fee": "Miner werden Ihre Transaktion wahrscheinlich früher bearbeiten, wenn Sie eine höhere Gebühr zahlen.", + "minimum-received": "Minimum erhalten", + "mobile-network-ask-me": "Mich fragen, wenn Mobilfunknetz aktiviert ist", + "mobile-network-continue-syncing": "Synchronisierung fortsetzen", + "mobile-network-continue-syncing-details": "Sie können das später in den Einstellungen ändern", + "mobile-network-go-to-settings": "Zu den Einstellungen", + "mobile-network-settings": "Mobile Daten", + "mobile-network-sheet-configure": "Sie können die Synchronisierung in den Einstellungen detaillierter konfigurieren", + "mobile-network-sheet-offline": "Kein WLAN, Nachrichtensynchronisierung deaktiviert.", + "mobile-network-sheet-offline-details": "Synchronisierung über Mobilfunk ist deaktiviert", + "mobile-network-sheet-remember-choice": "Auswahl speichern", + "mobile-network-sheet-settings": "Einstellungen", + "mobile-network-start-syncing": "Synchronisierung starten", + "mobile-network-stop-syncing": "Synchronisierung anhalten", + "mobile-network-stop-syncing-details": "Bis eine WLAN-Verbindung besteht?", + "mobile-network-use-mobile": "Mobile Daten nutzen", + "mobile-network-use-mobile-data": "Status hat einen hohen Datenverbrauch beim Synchronisieren von Chats.", + "mobile-network-use-wifi": "Nur Wi-Fi\/WLAN", + "mobile-syncing-sheet-details": "Status verwendet beim Synchronisieren von Chats und Wallet viel Datenvolumen", + "mobile-syncing-sheet-title": "Mobile Datenverbindung zum Synchronisieren nutzen?", + "more": "mehr", + "move-and-reset": "Verschieben und Zurücksetzen", + "move-keystore-file": "Keystore-Datei verschieben", + "move-keystore-file-to-keycard": "Keystore-Datei auf die Keycard verschieben?", + "multiaccount-exists-content": "Schlüssel für dieses Konto sind bereits vorhanden und können nicht erneut hinzugefügt werden. Wenn Sie Ihr Passwort, Ihren Passcode oder Ihre Schlüsselkarte verloren haben, deinstallieren Sie die App, installieren Sie sie neu und greifen Sie auf Ihre Schlüssel zu, indem Sie Ihre Seed-Phrase eingeben", + "multiaccount-exists-title": "Schlüssel für dieses Konto sind bereits vorhanden", + "multiaccounts-recover-enter-phrase-text": "Geben Sie 12, 15, 18, 21 oder 24 Wörter ein. \nTrennen Sie die Wörter durch ein Leerzeichen.", + "multiaccounts-recover-enter-phrase-title": "Geben Sie Ihre Seed-Phrase ein", + "mute": "Stumm", + "my-accounts": "Meine Konten", + "my-accounts-empty": "Ihre verfügbaren Konten werden hier angezeigt", + "my-profile": "Mein Profil", + "my-status": "Mein Status", + "name": "Name", + "name-of-token": "Der Name Ihres Tokens", + "name-optional": "Name (optional)", + "name-your-channel": "Benennen Sie Ihren Kanal", + "name-your-channel-placeholder": "Kanalname", + "name-your-community": "Benennen Sie Ihre Community", + "name-your-community-placeholder": "Ein einprägsamer Name", + "need-help": "Benötigen Sie Hilfe?", + "network": "Netzwerk", + "network-chain": "Netzwerkkette", + "network-details": "Netzwerkdetails", + "network-fee": "Netzwerkgebühr", + "network-id": "Netzwerk-ID", + "network-info": "Netzwerk-Information", + "network-invalid-network-id": "Die angegebene Netzwerk-ID entspricht nicht der Netzwerk-ID der RPC-URL", + "network-invalid-status-code": "Ungültiger Statuscode: {{code}}", + "network-invalid-url": "Netzwerk-URL ist ungültig", + "network-settings": "Netzwerkeinstellungen", + "never": "Niemals", + "new": "Neu", + "new-category": "Neue Kategorie", + "new-chat": "Neuer Chat", + "new-community-title": "Neue Community", + "new-contact": "Neuer Kontakt", + "new-contract": "Neuer Contract", + "new-favourite": "Neuer Favorit", + "new-group": "Neue Gruppe", + "new-group-chat": "Neuer Gruppenchat", + "new-network": "Neues Netzwerk", + "new-password": "Neues Passwort", + "new-pin-description": "Geben Sie einen neuen 6-stelligen Passcode ein", + "new-public-group-chat": "Öffentlichem Chat beitreten", + "new-puk-description": "Neuen 12-stelligen PUK eingeben", + "new-status": "Neuer Status", + "new-tab": "Neue Registerkarte", + "next": "Weiter", + "nickname": "Alias", + "nickname-description": "Aliasse helfen dir, andere einfach zu identifizieren. Nur du kannst von dir erstellte Aliasse sehen.", + "no": "Nein", + "no-collectibles": "Keine Sammlerstücke verfügbar", + "no-contacts": "Noch keine Kontakte", + "no-keycard-applet-on-card": "Kein Keycard-Applet auf der Karte", + "no-messages": "Keine Nachrichten", + "no-pairing-slots-available": "Diese Karte ist bereits mit fünf Geräten gekoppelt und kann nicht mit diesem verknüpft werden. Bitte verwenden Sie eines der gekoppelten Geräte, melden Sie sich mit dieser Karte an und geben Sie eine Kopplung auf der Karte frei", + "no-pinned-messages": "Keine angehefteten Nachrichten", + "no-result": "Keine Ergebnisse", + "no-thanks": "Nein Danke", + "no-tokens-found": "Keine Token gefunden", + "node-address": "Adresse des Knotens", + "node-details": "Details zum Knotenpunkt", + "node-info": "Knoteninformation", + "node-version": "Node-Version", + "nodes-disabled": "Knoten von Status sind deaktiviert", + "non-archival-node": "Der RPC-Endpunkt unterstützt keine Archivierungsanfragen. Ihre lokale Übertragungshistorie ist möglicherweise unvollständig.", + "non-contacts": "Nicht in meinen Kontakten", + "nonce": "Nonce", + "none": "Keine", + "normal": "Normal", + "not-applicable": "Nicht anwendbar für unsignierte Transaktionen", + "not-connected-nodes": "Mit keinem Knoten von Status verbunden", + "not-connected-to-peers": "Keine Verbindung zu einer Gegenstelle", + "not-enough-snt": "Nicht genug SNT", + "not-found": "Nicht gefunden", + "not-keycard-text": "Die von Ihnen verwendete Karte ist keine Keycard. Sie müssen eine Keycard kaufen, um sie verwenden zu können", + "not-keycard-title": "Keine Keycard", + "not-registered": "nicht registriert", + "notification-settings": "Benachrichtigungen", + "notifications": "Benachrichtigungen", + "notifications-non-contacts": "Benachrichtigungen von Nicht-Kontakten", + "notifications-preferences": "Benachrichtigungseinstellungen", + "notifications-servers": "Benachrichtigungsserver", + "notifications-switch": "Benachrichtungen anzeigen", + "notifications-transactions": "Wallet-Transaktionen", + "notify": "Benachrichtigen", + "now": "Jetzt", + "off": "Aus", + "off-status-tree": "Nicht mit Status verbunden", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Aktivieren Sie Verlaufsknoten, um Nachrichten abzurufen, die gesendet wurden, während die Anwendung geschlossen war. Wenn aktiviert, erhält ein Verlaufsknoten Ihre IP-Adresse. Wenn deaktiviert, erhalten Sie keine Nachrichten, wenn die Anwendung geschlossen wird, und sehen sie auch nicht, wenn Sie die Anwendung später öffnen.", + "offline-messaging-use-history-nodes": "Verlaufsknoten verwenden", + "ok": "OK", + "ok-continue": "Okay, weiter", + "ok-got-it": "Okay, verstanden", + "ok-save-pass": "OK, Passwort speichern", + "okay": "Okay", + "on": "Ein", + "on-status-tree": "Mit Status verbunden", + "once-enabled-share-metadata": "Sobald diese Funktion aktiviert ist, können im Chat gepostete Links Ihre Metadaten mit der Website teilen", + "one-day": "Ein Tag", + "one-month": "Ein Monat", + "one-week": "Eine Woche", + "open": "Öffnen", + "open-chat": "Chat öffnen", + "open-dapp": "ÐApp öffnen", + "open-dapp-store": "ÐApps erkunden", + "open-home": "Öffnen…", + "open-in-new-tab": "In neuer Registerkarte öffnen", + "open-membership": "Offene Mitgliedschaft", + "open-nfc-settings": "NFC-Einstellungen öffnen", + "opening-buy-crypto": "{{site}} wird geöffnet...", + "optimal": "Optimal", + "optional": "optional", + "or": "ODER", + "outgoing": "Ausgehend", + "outgoing-transaction": "Ausgehende Transaktion", + "page-camera-request-blocked": "Kameraanfragen sind blockiert. Um Kameraanfragen zu aktivieren, gehen Sie zu den Einstellungen", + "page-would-like-to-use-camera": "möchte Ihre Kamera benutzen", + "pair": "Geräte koppeln", + "pair-card": "Mit diesem Gerät koppeln", + "pair-code": "Kopplungscode", + "pair-code-explanation": "Paart die Karte mit einem anderen Gerät (bis zu 5), um Schlüssel freizuschalten und Transaktionen mit derselben Schlüsselkarte zu signieren", + "pair-code-placeholder": "Kopplungscode", + "pair-this-card": "Diese Karte koppeln", + "pair-this-device": "Gerät synchronisieren", + "pair-this-device-description": "Koppeln Sie Ihre Geräte, um Kontakte und Chats zwischen ihnen zu synchronisieren", + "paired-devices": "Gekoppelte Geräte", + "pairing": "Kopplung", + "pairing-card": "Karte verknüpfen", + "pairing-changed": "Der Pairing-Code wurde geändert", + "pairing-code-placeholder": "Pairing-Code ...", + "pairing-code_error1": "Pairing-Codes stimmen nicht überein.", + "pairing-go-to-installation": "Gehen Sie zu den Kopplungseinstellungen", + "pairing-maximum-number-reached-content": "Bitte deaktivieren Sie eines Ihrer Geräte, bevor Sie ein neues aktivieren.", + "pairing-maximum-number-reached-title": "Maximale Anzahl der Geräte erreicht", + "pairing-new-installation-detected-content": "Ein neues Gerät wurde erkannt. \nUm Ihre Geräte richtig zu verwenden, ist es wichtig, sie vor der Verwendung zu koppeln und zu aktivieren. \nBitte gehen Sie zu Einstellungen > Geräte, um Ihre Geräte zu koppeln.", + "pairing-new-installation-detected-title": "Neues Gerät erkannt", + "pairing-no-info": "Keine Informationen", + "pairing-please-set-a-name": "Bitte geben Sie einen Namen für Ihr Gerät ein.", + "passphrase": "Passphrase", + "password": "Passwort", + "password-description": "Mindestens sechs Zeichen. Ihr Passwort schützt Ihre Schlüssel. Sie benötigen es, um Status zu entsperren und Transaktionen durchzuführen.", + "password-mismatch": "Neues Passwort und Bestätigung stimmen nicht überein", + "password-placeholder": "Passwort...", + "password-placeholder2": "Bestätigen Sie Ihr Passwort", + "password-reset-in-progress": "Ändere Passwort...", + "password-reset-success": "Passwort geändert", + "password-reset-success-message": "Sie müssen sich erneut anmelden", + "password_error1": "Passwörter stimmen nicht überein.", + "paste": "Einfügen", + "paste-json": "JSON einfügen", + "pay-to-chat": "Zahlen Sie, um zu chatten", + "peer-content": "Ein Gerät, das mit dem Status-Chat-Netzwerk verbunden ist. Jeder Benutzer kann je nach Anzahl der Geräte einen oder mehrere Peers darstellen.", + "peer-title": "Peer", + "peer-to-peer": "Peer-to-Peer", + "peers": "Peers", + "pending": "Ausstehend", + "pending-confirmation": "Ausstehende Bestätigung...", + "pending-invitations": "Ausstehende Gruppenanfragen", + "per-gas-price-limit": "Preislimit pro Gas", + "per-gas-tip-limit": "Trinkgeldlimit pro Gas", + "perform-backup": "Backup durchführen", + "permissions": "Berechtigungen", + "phone-e164": "International 1", + "photos": "Fotos", + "photos-access-error": "Um den benötigten Zugriff auf Fotos zu erlauben, gehen Sie bitte in die Systemeinstellungen und stellen Sie sicher, dass Status > Fotos ausgewählt ist", + "pin": "Anheften", + "pin-changed": "6-stelliges Passwort wurde aktualisiert", + "pin-code": "6-stelliger Passcode", + "pin-limit-reached": "Limit der angehefteten Nachrichten erreicht. Lösen Sie zuerst eine vorherige Nachricht.", + "pin-mismatch": "Falscher Passcode", + "pin-one-attempt": " einen Versuch ", + "pin-one-attempt-blocked-after": "bevor Ihre Keycard gesperrt wird", + "pin-one-attempt-blocked-before": "Achtung, Sie haben nur", + "pin-one-attempt-frozen-after": "bevor Ihre Keycard eingefroren wird", + "pin-one-attempt-frozen-before": "Achtung, Sie haben nur", + "pin-retries-left": "{{number}} Versuche übrig", + "pinned-by": "Angeheftet von", + "pinned-messages": "Angeheftete Nachrichten", + "pinned-messages-count": { + "one": "1 angeheftete Nachricht", + "other": "{{count}} angeheftete Nachrichten" + }, + "pinned-messages-empty": "Angeheftete Nachrichten werden hier angezeigt. Um eine Nachricht anzuheften, halten Sie sie gedrückt und tippen Sie auf „Anheften“.", + "powered-by-paraswap": "Unterstützt von Paraswap", + "preference": "Präferenz", + "preview-privacy": "Vorschau des Datenschutzmodus", + "previewing-may-share-metadata": "Wenn Sie Links von diesen Websites in der Vorschau anzeigen, werden Ihre Metadaten möglicherweise an die Eigentümer dieser Websites weitergegeben", + "price-impact": "Auswirkungen auf den Preis", + "price-impact-desc": "Geschätzte Preisauswirkungen für diese Transaktion. Wenn die aktuelle Blockgrundgebühr diese übersteigt, wird Ihre Transaktion in einen nachfolgenden Block mit einer niedrigeren Grundgebühr aufgenommen.", + "principles": "Grundsätze", + "priority": "Priorität", + "privacy": "Datenschutz", + "privacy-and-security": "Datenschutz und Sicherheit", + "privacy-photos": "Profilfoto Datenschutz", + "privacy-policy": "Datenschutzbestimmungen", + "privacy-show-to-warning": "Personen, die Ihr Profilbild bereits gesehen haben, werden dies auch weiterhin tun", + "private-key": "Privater Schlüssel", + "private-notifications": "Private Benachrichtigungen", + "private-notifications-descr": "Status informiert Sie über neue Nachrichten. Sie können Ihre Benachrichtigungseinstellungen später in den Einstellungen bearbeiten.", + "processing": "Einen Augenblick", + "product-information": "Produktinformation", + "profile": "Profil", + "profile-deleted-content": "Ihr Profil wurde erfolgreich gelöscht", + "profile-deleted-keycard": "Sie können nun ein anderes Schlüsselpaar auf Ihrer Keycard wiederherstellen", + "profile-deleted-title": "Profil gelöscht", + "profile-details": "Profildetails", + "profile-not-found": "Profil nicht gefunden", + "profile-pic-pick": "Aus Galerie auswählen", + "profile-pic-remove": "Foto entfernen", + "profile-pic-take": "Foto aufnehmen", + "profile-picture-updated": "Profilbild aktualisiert", + "public-channel": "Öffentlicher Kanal", + "public-chat": "Öffentlicher Chat", + "public-chat-description": "Nehmen Sie an öffentlichen Chats für Ihre Interessen teil! Jeder kann einen neuen Chat starten.", + "public-chats": "Öffentliche Chats", + "public-group-status": "Öffentlich", + "public-group-topic": "Thema", + "public-key": "Öffentlicher Schlüssel", + "puk-and-pairing-codes-displayed": "PUK- und Kopplungscodes werden angezeigt", + "puk-changed": "12-stelliger PUK wurde geändert", + "puk-code": "PUK-Code", + "puk-code-explanation": "Wenn Sie Ihr 6-stelliges Passwort vergessen oder dreimal falsch eingegeben haben, benötigen Sie diesen Code, um Ihre Karte zu entsperren.", + "puk-mismatch": "Falscher PUK-Code", + "push-failed-transaction": "Ihre Transaktion ist fehlgeschlagen", + "push-failed-transaction-body": "{{value}} {{currency}} an {{to}}", + "push-inbound-transaction": "Sie haben {{value}} {{currency}} erhalten", + "push-inbound-transaction-body": "Von {{from}} zu {{to}}", + "push-notifications-server-enabled": "Server aktiviert", + "push-notifications-servers": "Push-Benachrichtigungsserver", + "push-outbound-transaction": "Sie haben {{value}} {{currency}} gesendet", + "push-outbound-transaction-body": "Von {{from}} zu {{to}}", + "quiet-days": "{{quiet-days}} Tage", + "quiet-hours": "{{quiet-hours}} Stunden", + "re-encrypt-key": "Verschlüsseln Sie Ihre Schlüssel erneut", + "rearrange-categories": "Kategorien neu anordnen", + "receive": "Empfangen", + "receive-transaction": "Transaktion empfangen", + "recent": "Kürzlich", + "recent-empty": "Kürzlich verwendete Adressen werden hier angezeigt", + "recent-recipients": "Kontakte", + "recently-used-stickers": "Kürzlich verwendete Sticker werden hier angezeigt", + "recipient": "Empfänger", + "recipient-code": "Empfängeradresse eingeben", + "recipient-code-placeholder": "0x ... oder username.domain.eth", + "recover": "Wiederherstellen", + "recover-key": "Greifen Sie auf vorhandene Schlüssel zu", + "recover-keycard-multiaccount-not-supported": "Schlüssel für dieses Konto sind bereits vorhanden und können nicht erneut hinzugefügt werden. Wenn Sie Ihr Passwort, Ihren Passcode oder Ihre Schlüsselkarte verloren haben, deinstallieren Sie die App, installieren Sie sie neu und greifen Sie auf Ihre Schlüssel zu, indem Sie Ihre Seed-Phrase eingeben", + "recover-with-keycard": "Mit Keycard wiederherstellen", + "recover-with-seed-phrase": "Wiederherstellen mit Seed-Phrase", + "recovering-key": "Zugriff auf Schlüssel...", + "recovery-confirm-phrase": "Bestätigen Sie die Seed-Phrase", + "recovery-phrase": "Seed-Phrase", + "recovery-success-text": "Sie müssen einen neuen Code oder ein neues Passwort erstellen, um Ihren Schlüssel erneut zu verschlüsseln.", + "recovery-typo-dialog-description": "Bitte beachten Sie, dass Ihre Seed-Phrase genau dieselben Wörter und dieselbe Reihenfolge verwenden muss, wie Sie sie erhalten haben.", + "recovery-typo-dialog-title": "Ist die Seed-Phrase korrekt?", + "redeem-amount": "Menge der verfügbaren Boni: {{quantity}}", + "redeem-now": "Jetzt einlösen", + "redeem-success": "Bonus erfolgreich eingelöst!", + "reduced-tip": "Prioritätstrinkgeld wird reduziert", + "refresh": "Aktualisieren", + "registered": "registriert", + "reject-and-delete": "Ablehnen und löschen", + "remember-me": "Eingeloggt bleiben", + "remind-me-later": "Nochmal anzeigen", + "remove": "Entfernen", + "remove-favourite": "Favorit entfernen", + "remove-from-chat": "Aus dem Chat entfernen", + "remove-from-contacts": "Aus Kontakten entfernen", + "remove-from-contacts-text": "Wenn Sie einen Benutzer aus Ihrer Kontaktliste entfernen, verbergen Sie Ihre Wallet-Adresse nicht vor ihm", + "remove-group": "Gruppe entfernen", + "remove-network": "Netzwerk entfernen", + "remove-token": "Token entfernen", + "removed": "entfernt", + "repeat-pin": "Wiederholen Sie den neuen 6-stelligen Passcode", + "repeat-puk": "Wiederholen Sie den neuen 12-stelligen PUK", + "replying-to": "Antwort an {{author}}", + "report-bug-email-template": "1. Problembeschreibung \n{{description}}\n\n 2. Schritte zum Reproduzieren des Problems\n {{steps}}\n \n 3. Fügen Sie bitte Screenshots bei, die das Problem darstellen", + "request-access": "Zugang anfordern", + "request-feature": "Eine Funktion anfordern", + "request-membership": "Mitgliedschaft beantragen", + "request-pending": "Gruppenanfrage wird bearbeitet...", + "request-transaction": "Transaktion anfordern", + "required-field": "Pflichtfeld", + "resend-message": "Erneut senden", + "reset-card": "Karte zurücksetzen", + "reset-card-description": "Diese Operation setzt die Karte in den Ausgangszustand zurück. Dabei werden alle Kartendaten einschließlich der privaten Schlüssel gelöscht. Die Operation ist nicht umkehrbar.", + "reset-database": "Datenbank zurücksetzen", + "reset-database-warning": "Chats, Kontakte und Einstellungen löschen. Erforderlich, wenn Sie Ihr Passwort verloren haben", + "reset-database-warning-keycard": "Chats, Kontakte und Einstellungen löschen.", + "reset-password": "Passwort zurücksetzen", + "restore-defaults": "Standardeinstellungen wiederherstellen", + "retry": "Wiederholen", + "revoke-access": "Zugangsberechtigung widerrufen", + "rpc-url": "RPC-URL", + "rpc-usage-copy": "Kopieren", + "rpc-usage-filter": "Filtermethoden", + "rpc-usage-get-stats": "Aktualisieren", + "rpc-usage-info": "RPC-Nutzungsstatistiken", + "rpc-usage-reset": "Zurücksetzen", + "safe-estimate": "Sichere Schätzung", + "save": "Speichern", + "save-password": "Passwort speichern", + "save-password-unavailable": "Legen Sie den Gerätepasscode fest, um das Passwort zu speichern", + "save-password-unavailable-android": "Speichern des Passworts nicht verfügbar: Ihr Gerät ist möglicherweise gerootet oder verfügt nicht über die erforderlichen Sicherheitsfunktionen.", + "scan-qr": "QR-Code scannen", + "scan-qr-code": "Scannen Sie einen QR-Code mit einer Wallet Adresse", + "scan-tokens": "Tokens scannen", + "search": "Suche", + "search-no-chat-found": "Keine Suchergebnisse. Meinen Sie vielleicht", + "secret-keys-confirmation-text": "Sie benötigen sie, um Ihre Keycard weiterhin verwenden zu können, falls Sie Ihr Smartphone verlieren sollten.", + "secret-keys-confirmation-title": "Codes aufgeschrieben?", + "security": "Sicherheit", + "see-details": "Siehe Details", + "see-it-again": "SEHEN SIE ES SICH WIEDER AN", + "see-suggestions": "Siehe Vorschläge", + "seed-key-uid-mismatch": "Seed-Phrase stimmt nicht überein", + "seed-key-uid-mismatch-desc-1": "Die eingegebene Seed-Phrase stimmt nicht mit {{multiaccount-name}} überein", + "seed-key-uid-mismatch-desc-2": "Um Schlüssel für dieses Konto zu verwalten, überprüfen Sie Ihre Seed-Phrase und versuchen Sie es erneut.", + "seed-phrase-content": "Eine Reihe von benutzerfreundlichen Wörtern, die zufällig aus der BIP39-Standardliste ausgewählt und zum Wiederherstellen oder Zugreifen auf Ihr Ethereum-Konto auf anderen Wallets und Geräten verwendet werden. Wird im gesamten Krypto-Ökosystem auch als \"Mnemonische Phrase\", \"Wiederherstellungsphrase\" oder \"Wallet Backup\" bezeichnet. Die meisten Krypto-Apps verwenden denselben Standard zum Generieren von Konten.", + "seed-phrase-placeholder": "Seed-Phrase", + "seed-phrase-title": "Seed-Phrase", + "select": "Auswählen", + "select-account": "Konto auswählen", + "select-account-dapp": "Wählen Sie das Konto aus, das Sie mit DApps verwenden möchten", + "select-account-first": "Wählen Sie zuerst ein Konto aus", + "select-chat": "Wählen Sie einen Chat aus, um mit dem Messaging zu beginnen", + "select-new-location-for-keys": "Wählen Sie einen neuen Speicherort für Ihre(n) privaten Schlüssel", + "select-token-to-receive": "Zu empfangendes Token auswählen", + "select-token-to-swap": "Token zum Tauschen auswählen", + "selected": "Ausgewählt", + "send-logs": "Melden Sie einen technischen Fehler", + "send-logs-to": "Einen Fehler an {{email}} melden", + "send-message": "Nachricht senden", + "send-push-notifications": "Push-Benachrichtigungen senden", + "send-push-notifications-description": "Wenn diese Option deaktiviert ist, wird die Person, die Ihre Nachrichten empfängt, nicht über ihre Ankunft informiert", + "send-request": "Anfrage senden", + "send-request-amount": "Betrag", + "send-request-amount-max-decimals": "Die maximale Anzahl von Dezimalstellen beträgt {{asset-decimals}}", + "send-request-unknown-token": "Unbekanntes Token – {{asset}}", + "send-sending-to": "an {{recipient-name}}", + "send-transaction": "Transaktion senden", + "sending": "Sende", + "sent-at": "Gesendet an", + "server": "Server", + "set-a-topic": "Erstellen Sie ein Thema", + "set-currency": "Standardwährung festlegen", + "set-custom-fee": "Benutzerdefinierte Gebühr festlegen", + "set-dapp-access-permissions": "Festlegen von DApp-Zugriffsberechtigungen", + "set-max": "Maximalwert einstellen", + "settings": "Einstellungen", + "share": "Teilen", + "share-address": "Adresse teilen", + "share-chat": "Chat teilen", + "share-contact-code": "Meinen Chat-Schlüssel teilen", + "share-dapp-text": "Schauen Sie sich diese DApp an, die ich in Status verwende: {{link}}", + "share-link": "Link teilen", + "share-logs": "Protokolle teilen", + "share-my-profile": "Mein Profil teilen", + "share-profile": "Profil teilen", + "share-profile-link": "Profillink teilen", + "share-public-chat-text": "Sehen Sie sich diesen öffentlichen Chat in der Status-App an: {{link}}", + "shared": "Geteilt", + "sharing-copied-to-clipboard": "Kopiert", + "sharing-copy-to-clipboard": "Kopieren", + "sharing-data-desc-1": "Die Daten werden anhand öffentlicher Regeln validiert, um sicherzustellen, dass keine sensiblen Daten gesendet werden. Vertraue nicht, verifiziere.", + "sharing-data-desc-2": "Nutzungsdaten werden Ende-zu-Ende verschlüsselt über das Peer-to-Peer-Netzwerk von Status gesendet", + "sharing-data-desc-3": "Anstelle Ihres normalen Chat-Schlüssels wird ein Einmalschlüssel verwendet", + "sharing-data-desc-4": "Nutzungsdaten können nicht mit Ihrer IP-Adresse in Verbindung gebracht werden", + "sharing-data-desc-5": "Kumulierte Daten aller Benutzer sind öffentlich verfügbar", + "sharing-data-desc-6": "Die Daten werden nach dem Senden von Ihrem Telefon entfernt", + "sharing-share": "Teilen", + "show-less": "Weniger anzeigen", + "show-more": "Mehr anzeigen", + "show-notifications": "Benachrichtigungen anzeigen", + "show-profile-pictures": "Profilbilder anzeigen von", + "show-profile-pictures-to": "Zeigen Sie Ihr Profilbild an", + "show-qr": "QR-Code anzeigen", + "show-transaction-data": "Transaktionsdaten anzeigen", + "sign-and-send": "Signieren und senden", + "sign-anyway": "Trotzdem unterschreiben", + "sign-in": "Anmelden", + "sign-message": "Nachricht signieren", + "sign-out": "Ausloggen", + "sign-request-failed": "Nachricht konnte nicht signiert werden", + "sign-with": "Signieren mit", + "sign-with-password": "Mit Passwort signieren", + "sign-you-in": "Sie werden angemeldet...", + "signing": "Signieren", + "signing-a-message": "Eine Nachricht signieren", + "signing-phrase": "Signierphrase", + "skip": "Überspringen", + "slippage": "Slippage", + "slow": "Langsam", + "something-went-wrong": "Etwas ist schiefgelaufen", + "soon": "Demnächst", + "specify-address": "Adresse teilen", + "specify-name": "Geben Sie einen Namen an", + "specify-network-id": "Geben Sie die Netzwerk-ID an", + "specify-rpc-url": "Spezifizieren Sie eine RPC-URL", + "specify-server-public-key": "Öffentlichen Schlüssel des Servers eingeben", + "specify-symbol": "Geben Sie ein Symbol an", + "start-chat": "Chat beginnen", + "start-conversation": "Gespräch beginnen", + "start-group-chat": "Gruppenchat starten", + "start-new-chat": "Neuen Chat starten", + "starter-pack-coming": "Starter Pack ist auf dem Weg zu Ihnen", + "starter-pack-coming-description": "Kann ein paar Minuten bis Stunden dauern", + "starter-pack-received": "Starter Pack erhalten", + "starter-pack-received-description": "Hier ist etwas Kryptowährung für den Anfang! Benutzen Sie diese, um Sticker zu bekommen, einen ENS-Namen zu erstellen und DApps zu testen.", + "status": "Status", + "status-always-online": "Immer online", + "status-automatic": "Automatisch", + "status-automatic-subtitle": "Status automatisch setzen", + "status-confirmed": "Bestätigt", + "status-dnd": "Nicht stören", + "status-dnd-subtitle": "Schaltet alle Benachrichtigungen stumm", + "status-hardwallet": "Status-Hardwallet", + "status-inactive": "Inaktiv", + "status-inactive-subtitle": "Verbirgt Ihren Online-Status", + "status-is-open-source": "Status ist Open-Source", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status neigt dazu, beim Synchronisieren von Chats eine Menge Daten zu verbrauchen. Sie können wählen, dass Sie nicht synchronisieren, wenn Sie sich im Mobilfunknetz befinden", + "status-not-sent-click": "Unbestätigt. Klicken Sie für weitere Optionen", + "status-not-sent-tap": "Unbestätigt. Tippen Sie hier, um weitere Optionen anzuzeigen", + "status-pending": "Ausstehend", + "status-sent": "Gesendet", + "status-tx-not-found": "Transaktion nicht gefunden", + "status-updates-descr": "Hier werden Statusaktualisierungen angezeigt. Fügen Sie das Profil als Kontakt hinzu, um Aktualisierungen auf Ihrer Timeline zu erhalten.", + "statuses-descr": "Teilen Sie, was Sie auf dem Herzen haben und bleiben Sie mit Ihren Kontakten auf dem Laufenden", + "statuses-my-status-descr": "Teilen Sie, was Sie auf dem Herzen haben. Jeder, der Ihr Profil besucht, kann Ihren Status sehen. Personen, die Sie als Kontakt hinzufügen, erhalten Ihre Updates in ihrer Timeline", + "step-i-of-n": "Schritt {{step}} von {{number}}", + "sticker": "Sticker", + "sticker-market": "Sticker-Markt", + "storage": "Aufbewahrung", + "submit": "Senden", + "submit-bug": "Einen Fehler einreichen", + "success": "Erfolg", + "suggested-min-tip": "Empfohlenes Mindesttrinkgeld", + "suggested-price-limit": "Vorgeschlagenes Preislimit", + "swap": "Tausch\/Swap", + "swap-details": "Details zum Tausch\/Swap", + "switch-to-simple-interface": "Wechsel zur einfachen Benutzeroberfläche", + "symbol": "Symbol", + "sync-all-devices": "Alle Geräte synchronisieren", + "sync-in-progress": "Synchronisiere...", + "sync-settings": "Synchronisierung", + "sync-synced": "Synchronisiert", + "syncing-devices": "Synchronisiere...", + "system": "System", + "tabs": "Registerkarten", + "tag-was-lost": "Tag wurde verloren", + "tap-card-again": "Um fortzufahren, halten Sie die Karte noch einmal an die Rückseite Ihres Smartphones", + "terms-of-service": "Nutzungsbedingungen", + "test-networks": "Testnetzwerke", + "text-input-disabled": "Einen Augenblick bitte...", + "thank-you": "Vielen Dank", + "this-device": "Dieses Gerät", + "this-device-desc": "Ihre Schlüssel werden verschlüsselt und sicher auf Ihrem Gerät gespeichert", + "this-is-you-signing": "Dies ist Ihre Signaturphrase", + "this-will-take-few-seconds": "Dies dauert einen Augenblick", + "three-days": "Drei Tage", + "three-words-description": "Sie sollten diese 3 Wörter sehen, bevor Sie eine Transaktion signieren", + "three-words-description-2": "Wenn Sie eine andere Kombination sehen, brechen Sie die Transaktion ab und melden Sie sich ab", + "timeline": "Timeline", + "tip-cap": "Trinkgeldobergrenze", + "to": "An", + "to-block": "Blockieren", + "to-enable-biometric": "Um {{bio-type-label}} zu aktivieren, müssen Sie Ihr Passwort auf dem Entsperrbildschirm speichern", + "to-encrypt-enter-password": "Um das Konto zu verschlüsseln, geben Sie bitte Ihr Passwort ein", + "to-see-this-message": "Um diese Nachricht zu sehen,", + "token": "Token", + "token-auto-validate-decimals-error": "Falsche Dezimalstellen für Token {{symbol}} auf der Adresse {{address}} – gesetzt auf {{expected}} aber als {{actual}} erkannt", + "token-auto-validate-name-error": "Falscher Name für Token {{symbol}} auf der Adresse {{address}} – gesetzt auf {{expected}} aber als {{actual}} erkannt", + "token-auto-validate-symbol-error": "Falsches Symbol für Token {{symbol}} auf der Adresse {{address}} – gesetzt auf {{expected}} aber als {{actual}} erkannt", + "token-details": "Token-Details", + "topic-name-error": "Verwenden Sie nur Kleinbuchstaben (a bis z), Zahlen und Striche (-). Verwenden Sie keine Chat-Schlüssel", + "total-gas": "Gas insgesamt", + "transaction": "Transaktion", + "transaction-data": "Transaktionsdetails", + "transaction-declined": "Transaktion abgelehnt", + "transaction-description": "Betrachten Sie es nach 12 Bestätigungen im Netzwerk als abgeschlossen.", + "transaction-details": "Transaktionsdetails", + "transaction-failed": "Transaktion fehlgeschlagen", + "transaction-fee": "Transaktionsgebühr", + "transaction-history": "Transaktionsverlauf", + "transaction-request": "Transaktionsanfrage", + "transaction-sent": "Transaktion gesendet", + "transaction-signed": "Die Transaktion wurde erfolgreich signiert", + "transactions": "Transaktionen", + "transactions-filter-select-all": "Alle auswählen", + "transactions-filter-title": "Filterverlauf", + "transactions-history": "Transaktionsverlauf", + "transactions-history-empty": "Noch keine Transaktionen in Ihrem Verlauf", + "transactions-history-loading": "Laden der Transaktionshistorie. Dies kann eine Weile dauern.", + "transactions-load-more": "Mehr laden", + "transactions-management-enabled": "Transaktionsmanagement (Alpha)", + "transactions-sign": "Signieren", + "transfer-ma-unknown-error-desc-1": "Es sieht so aus, als sei Ihr Multiaccount nicht gelöscht worden. Die Datenbank wurde möglicherweise zurückgesetzt", + "transfer-ma-unknown-error-desc-2": "Bitte überprüfen Sie Ihre Kontoliste und versuchen Sie es erneut. Wenn das Konto nicht aufgelistet ist, gehen Sie zu 'Auf vorhandene Schlüssel zugreifen', um es mit der Seed-Phrase wiederherzustellen", + "transfers-fetching-failure": "Der Übertragungsverlauf konnte nicht aktualisiert werden. Überprüfen Sie Ihre Verbindung und wischen Sie nach unten, um es erneut zu versuchen", + "tribute-required-by-multiaccount": "{{multiaccount-name}} benötigt SNT, um einen Chat zu starten.", + "tribute-state-paid": "Tribut bezahlt", + "tribute-state-pending": "Tribut steht aus", + "tribute-state-required": "Benötigt ein SNT-Tribut i.H.v. {{snt-amount}} ", + "tribute-to-talk": "Tribute to Talk", + "tribute-to-talk-add-friends": "Fügen Sie Freunde als Kontakte hinzu, um Chats ohne Tributzahlung zuzulassen.", + "tribute-to-talk-are-you-friends": "Sind Sie Freunde?", + "tribute-to-talk-ask-to-be-added": "Bitten Sie darum, als Kontakt hinzugefügt zu werden", + "tribute-to-talk-contact-received-your-tribute": " erhielt Ihren Tribut. Sie können jetzt sicher miteinander chatten.", + "tribute-to-talk-desc": "Monetarisieren Sie Ihre Aufmerksamkeit, indem Sie SNT verlangen, damit neue Personen mit Ihnen einen Chat starten können", + "tribute-to-talk-disabled": "Tribute to Talk deaktiviert", + "tribute-to-talk-disabled-note": "Von nun an können neue Personen einen Chat mit Ihnen beginnen, ohne SNT zu senden.", + "tribute-to-talk-enabled": "Sie haben Tribute to Talk aktiviert.", + "tribute-to-talk-finish-desc": "Von nun an erhalten Sie nur noch Chats von Kontakten und Personen, die bezahlt haben ", + "tribute-to-talk-learn-more-1": "Ihre Zeit und Aufmerksamkeit sind Ihr wertvollstes Kapital. Mit Tribute to Talk können Sie einen SNT-Betrag festlegen, der erforderlich ist, damit neue Personen einen Chat mit Ihnen beginnen können.", + "tribute-to-talk-learn-more-2": "Jeder, der nicht in Ihrer Kontaktliste steht, wird zur Zahlung aufgefordert, und Sie können antworten, sobald er dies getan hat.", + "tribute-to-talk-learn-more-3": "Sie können das Geld jederzeit zurückschicken. Um sicherzustellen, dass Freunde Sie frei erreichen können, fügen Sie diese zuerst als Kontakte hinzu.", + "tribute-to-talk-paywall-learn-more-1": "Unsere Zeit und Aufmerksamkeit sind unser wertvollstes Kapital. Mit Tribute to Talk können Sie eine SNT-Zahlung erhalten, um neue Leute kontaktieren.", + "tribute-to-talk-paywall-learn-more-2": "Um einen Chat mit jemandem zu starten, der eine Gebühr festgelegt hat, zahlen Sie einfach die erforderliche Menge an SNT und Sie werden als Kontakt hinzugefügt.", + "tribute-to-talk-paywall-learn-more-3": "Wenn Sie sie kennen, können Sie Ihr Profil außerhalb von Status freigeben, um es kostenlos hinzuzufügen.", + "tribute-to-talk-pending": "Tribut wartet auf Bestätigung", + "tribute-to-talk-pending-note": "Die Bestätigung der Tributstransaktion im Netzwerk steht noch aus. Sie können den aktuellen Status im Transaktionsverlauf überprüfen", + "tribute-to-talk-removing-note": "Durch Entfernen von Tribute to Talk können neue Personen einen Chat starten, ohne SNT zu senden. Dies erfordert eine Transaktion.", + "tribute-to-talk-set-snt-amount": "Legen Sie den SNT-Betrag fest, der erforderlich ist, damit neue Personen einen Chat mit Ihnen starten können", + "tribute-to-talk-signing": "Warten auf die Signierung der Transaktion", + "tribute-to-talk-transaction-failed-note": "Die Transaktion ist fehlgeschlagen und Ihre Einstellungen für Tribute to Talk wurden nicht geändert", + "tribute-to-talk-tribute-received1": "Tribut erhalten. Sie und ", + "tribute-to-talk-tribute-received2": " sind jetzt Kontakte und können sicher miteinander chatten.", + "tribute-to-talk-you-require-snt": "Sie fragen SNT an, damit neue Personen einen Chat starten können.", + "try-again": "Versuchen Sie es erneut", + "try-keeping-the-card-still": "Versuchen Sie, die Karte ruhig zu halten", + "turn-nfc-description": "NFC ist auf Ihrem Gerät deaktiviert. Sie können es in den Einstellungen aktivieren", + "turn-nfc-on": "Schalten Sie NFC ein, um fortzufahren", + "two-minutes": "zwei Minuten", + "tx-fail-description1": "Diese Transaktion wird wahrscheinlich scheitern. Die Unterzeichnung erfolgt auf eigenes Risiko unter Verwendung einer benutzerdefinierten Netzwerkgebühr.", + "tx-fail-description2": "Diese Transaktion wird wahrscheinlich scheitern. Legen Sie eine benutzerdefinierte Netzwerkgebühr fest, um auf eigenes Risiko zu signieren.", + "type": "Typ", + "type-a-message": "Nachricht", + "ulc-enabled": "ULC aktiviert", + "unable-to-fetch": "Chatverlauf kann nicht abgerufen werden", + "unable-to-read-this-code": "Dieser Code kann nicht gelesen werden", + "unable-to-send-messages": "Senden und Empfangen von Nachrichten nicht möglich", + "unblock": "Entsperren", + "unblock-contact": "Diesen Benutzer entsperren", + "unknown-status-go-error": "Unbekannter status-go Fehler", + "unlimited": "Unbegrenzt", + "unlock": "Freischalten", + "unmute": "Stummschaltung aufheben", + "unpair-card": "Karte entkoppeln", + "unpair-card-confirmation": "Durch diesen Vorgang wird die Karte vom aktuellen Gerät getrennt. Das erfordert eine 6-stellige Passcode-Autorisierung. Möchten Sie fortfahren?", + "unpair-keycard": "Schlüsselkarte von diesem Gerät entkoppeln", + "unpair-keycard-warning": "Dein Pairing Code\/PUK und PIN bleiben unverändert", + "unpaired-keycard-text": "Die gewählte Keycard ist nicht mit diesem Smartphone verbunden", + "unpaired-keycard-title": "Anscheinend wurde Ihre Karte entkoppelt", + "unpin": "Lösen", + "update": "Aktualisieren", + "update-to-listen-audio": "Aktualisiere auf die neueste Version, um hier eine Audiobotschaft zu hören!", + "update-to-see-image": "Aktualisiere auf die neueste Version, um hier ein schönes Bild zu sehen!", + "update-to-see-sticker": "Aktualisiere auf die neueste Version, um hier einen schönen Sticker zu sehen!", + "updates-to-tos": "Aktualisierungen der Nutzungsbedingungen", + "updates-to-tos-desc": "Bevor Sie fortfahren, lesen Sie bitte die Nutzungsbedingungen und bestätigen Sie, dass Sie die volle Verantwortung dafür übernehmen, wie Sie die App verwenden.", + "url": "URL", + "usd-currency": "USD", + "use-as-profile-picture": "Als Profilbild verwenden", + "use-valid-contact-code": "Bitte geben Sie einen gültigen Chat-Schlüssel oder Benutzernamen ein oder scannen Sie ihn ein", + "validation-amount-invalid-number": "Der Betrag ist keine gültige Zahl", + "validation-amount-is-too-precise": "Betrag ist zu genau. Die maximale Anzahl von Dezimalstellen beträgt {{decimals}}.", + "verified-community": "✓ Verifizierte Community", + "version": "App-Version", + "view": "Ansehen", + "view-cryptokitties": "In CryptoKitties anzeigen", + "view-cryptostrikers": "In CryptoStrikers anzeigen", + "view-data": "Daten anzeigen", + "view-details": "Details ansehen", + "view-etheremon": "In Etheremon anzeigen", + "view-gitcoin": "In Gitcoin anzeigen", + "view-on-opensea": "Auf OpenSea anzeigen", + "view-profile": "Profil anzeigen", + "view-public-dashboard": "Öffentliches Dashboard anzeigen", + "view-rules": "Regeln anzeigen", + "view-signing": "Signaturphrase anzeigen", + "view-superrare": "In SuperRare anzeigen", + "waiting-for-wifi": "Kein WLAN, Nachrichtensynchronisierung deaktiviert.", + "waiting-for-wifi-change": "Einstellungen", + "waiting-to-sign": "Warten auf die Signierung der Transaktion...", + "waiting-wi-fi": "Warten auf WLAN...", + "waku-bloom-filter-mode": "Waku Bloom Filtermodus", + "wakuv2-change-nodes": "Möchten Sie die 'Waku v2'-Knoten wirklich ändern?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "'Waku v2' Einstellungen", + "wallet": "Wallet", + "wallet-address": "Wallet-Adresse", + "wallet-asset": "Asset", + "wallet-assets": "Assets", + "wallet-backup-recovery-title": "Sichern Sie Ihre Seed-Phrase", + "wallet-choose-recipient": "Empfänger auswählen", + "wallet-collectibles": "Sammlerstücke", + "wallet-insufficient-funds": "Unzureichende Mittel", + "wallet-insufficient-gas": "Nicht genug ETH für Gas", + "wallet-invalid-address": "Ungültige Adresse: \n {{data}}", + "wallet-invalid-address-checksum": "Fehler in der Adresse: \n {{data}}", + "wallet-invalid-chain-id": "Netzwerk stimmt nicht überein: \n {{data}} aber das aktuelle ist {{chain}}", + "wallet-key-content": "Eine 64-stellige Hex-Adresse, die auf dem Ethereum-Standard basiert und mit 0x beginnt. In der Öffentlichkeit wird Ihre Kontoadresse mit anderen geteilt, wenn Sie Geld erhalten möchten. Wird auch als \"Ethereum-Adresse\" oder \"Wallet-Adresse\" bezeichnet.", + "wallet-key-title": "Kontoadresse", + "wallet-manage-accounts": "Konten verwalten", + "wallet-manage-assets": "Assets verwalten", + "wallet-request": "Anfrage", + "wallet-send": "Senden", + "wallet-send-min-units": "Mindestens 21000 Einheiten", + "wallet-send-min-wei": "Mindestens 1 wei", + "wallet-settings": "Wallet-Einstellungen", + "wallet-total-value": "Gesamtwert", + "wallet-transaction-total-fee": "Gesamtgebühr", + "wants-to-access-profile": "möchte auf Ihr Profil zugreifen", + "warning": "Warnung", + "warning-message": "Leider begrenzen wir mehrere Nachrichten hintereinander, um Spam zu vermeiden. Bitte versuchen Sie es gleich noch einmal", + "warning-sending-to-contract-descr": "Die eingegebene Adresse ist ein Smart Contract; wenn Sie hierhin Werte senden, können Sie diese verlieren. Um mit einer DApp zu interagieren, öffnen Sie die DApp im Status DApp Browser.", + "watch-only": "Nur zur Beobachtung", + "wc-brand-guide": "Anleitung zur Verwendung von Branding wie Marken und Logos", + "wc-disclaimer": "Haftungsausschlüsse (einschließlich Drittanbieter), Garantien und rechtliche Hinweise", + "wc-dispute": "Bestimmungen zur Streitbeilegung", + "wc-how-to-use-status-app": "So verwenden Sie die Status-App, einschließlich Datenschutz und Sicherheit", + "wc-new-tos-based-on-principles-prefix": "Neue Nutzungsbedingungen basierend auf unseren", + "web-view-error": "Seite kann nicht geladen werden", + "websites": "Webseiten", + "webview-camera-permission-requests": "Berechtigungsanforderungen für Webview-Kameras", + "webview-camera-permission-requests-subtitle": "Wenn diese Option aktiviert ist, können Websites und DApps Sie bitten, Ihre Kamera zu benutzen.", + "welcome-blank-community-message": "Ihre Communities werden hier angezeigt.", + "welcome-blank-message": "Ihre Chats werden hier angezeigt. Um neue Chats zu starten, drücken Sie die ⊕-Taste", + "welcome-community-blank-message": "Deine Chats werden hier angezeigt. Klicke oben auf die drei Punkte und wähle \"Kanal erstellen\", um einen neuen Chat zu starten", + "welcome-community-blank-message-edit-chats": "Ihre Kanäle werden hier angezeigt. Um einen neuen Kanal zu erstellen, gehen Sie zurück zum Community-Bildschirm, klicken Sie auf die Schaltfläche ⊕ und wählen Sie \"Einen Kanal erstellen\".", + "welcome-screen-text": "Richten Sie Ihre Wallet ein, laden Sie Freunde\nzum Chatten ein und stöbern Sie in beliebten DApps!", + "welcome-to-status": "Willkommen bei Status!", + "welcome-to-status-description": "Richten Sie Ihre Krypto-Wallet ein, laden Sie Freunde zum Chatten ein und durchsuchen Sie dezentrale Apps", + "what-changed": "Was sich geändert hat", + "what-is-shared": "Was wird geteilt", + "whats-on-your-mind": "Was haben Sie auf dem Herzen…", + "word-count": "Wortzahl", + "word-n": "Wort #{{number}}", + "word-n-description": "Geben Sie das Wort #{{number}} oben ein, um zu überprüfen, ob Sie Ihre Seed-Phrase korrekt gesichert haben.", + "words-n": { + "one": "1 Wort", + "other": "{{count}} Worte" + }, + "write-down-and-store-securely": "Schreiben Sie die Codes auf \n und bewahren Sie sie sicher auf", + "wrong-address": "Falsche Adresse", + "wrong-card": "Falsche Karte", + "wrong-card-text": "Die benutzte Karte entspricht nicht den von Ihnen gewählten Schlüsseln", + "wrong-contract": "Falscher Vertrag", + "wrong-keycard-text": "Die gewählte Keycard ist nicht mit diesem Smartphone verbunden", + "wrong-keycard-title": "Wahrscheinlich haben Sie eine \nfalsche Keycard verwendet", + "wrong-password": "Falsches Passwort", + "wrong-word": "Falsches Wort", + "yes": "Ja", + "you": "Sie", + "you-already-have-an-asset": "Sie haben bereits ein Asset {{value}}", + "you-are-all-set": "Es ist alles bereit!", + "you-are-all-set-description": "Wenn Sie Ihr Smartphone verlieren, können Sie jetzt mit Ihrer Seed-Phrase auf Ihre Gelder und Ihren Chat-Schlüssel zugreifen", + "you-can-change-account": "Sie können den Kontonamen und die Farbe nach Ihren Wünschen ändern", + "you-can-choose-preview-websites": "Sie können wählen, welche der folgenden Websites eine Vorschau von Beschreibungen und Bildern in Chats anzeigen können", + "you-can-fetch": "Sie können den Chatverlauf abrufen", + "you-dont-have-contacts": "Sie haben noch keine Kontakte.", + "you-dont-have-contacts-invite-friends": "Sie haben noch keine Kontakte.\nLaden Sie Ihre Freunde zum Chatten ein.", + "you-dont-have-stickers": "Sie haben noch keine Sticker", + "you-will-need-this-code": "Sie benötigen diesen Code, um Status zu öffnen und Transaktionen zu signieren", + "you-will-start-from-scratch": "Sie beginnen mit einem neuen Schlüsselsatz von vorne", + "your-card-is-frozen": "Ihre Keycard ist eingefroren. Kartenzugang zurücksetzen", + "your-contact-code": "Durch das Gewähren des Zugriffs wird diese DApp berechtigt, Ihren Chat-Schlüssel abzurufen", + "your-data-belongs-to-you": "Wenn Sie Ihre Seed-Phrase verlieren, verlieren Sie Ihre Daten und Ihr Geld", + "your-data-belongs-to-you-description": "Wenn Sie den Zugang verlieren, z.B. durch den Verlust Ihres Smartphones, können Sie auf Ihre Schlüssel nur noch mit Ihrer Seed-Phrase zugreifen. Niemand außer Sie hat Ihre Seed-Phrase. Schreiben Sie diese auf und bewahren Sie sie sicher auf.", + "your-keys": "Ihre Schlüssel", + "your-price-limit": "Ihr Preislimit", + "your-recovery-phrase": "Ihre Seed-Phrase", + "your-recovery-phrase-description": "Dies ist Ihre Seed-Phrase. Sie verwenden sie, um zu beweisen, dass dies Ihre Wallet ist. Sie bekommen sie nur einmal zu sehen! Schreiben Sie sie auf Papier und bewahren Sie es an einem sicheren Ort auf. Sie benötigen sie, wenn Sie Ihre Wallet verlieren oder neu installieren.", + "your-tip-limit": "Ihr Trinkgeldlimit", + "youre-on-mobile-network": "Sie sind im Mobilfunknetz" +} diff --git a/translations/de_ch.json b/translations/de_ch.json new file mode 100644 index 00000000000..481fba1000e --- /dev/null +++ b/translations/de_ch.json @@ -0,0 +1,93 @@ +{ + "amount": "Betrag", + "members-active": { + "one": "1 Mitglied, 1 aktiv", + "other": "{{count}} Mitglieder, {{count}} aktiv", + "zero": "Keine Mitglieder" + }, + "chat-name": "Chat Name", + "phew-here-is-your-passphrase": "*Phew* das war hart, hier ist deine Passphrase, *Schreibe diese auf und bewahre diese sicher auf!* Du benötigst diese, um dein Konto wiederherzustellen.", + "chat-settings": "Chat Einstellungen", + "offline": "Offline", + "invited": "eingeladen", + "address": "Adresse", + "datetime-hour": { + "one": "Stunde", + "other": "Stunden" + }, + "remove": "Entfernen", + "add-members": "Mitglieder hinzufügen", + "done": "Erledigt", + "new-group-chat": "Neuer Gruppenchat", + "datetime-yesterday": "gestern", + "datetime-ago": "vor", + "contacts": "Kontakte", + "active-online": "Online", + "password": "Passwort", + "discover": "Entdecken", + "intro-status": "Chat mit mir, damit ich dein Konto einrichten und deine Einstellungen ändern kann!", + "name": "Name", + "show-qr": "QR-Code anzeigen", + "connect": "Verbinden", + "edit": "Bearbeiten", + "account-generation-message": "Gib mir eine Sekunde, Ich muss wie verrückt etwas berechnen um dein Konto zu generieren!", + "no-messages": "Keine Nachrichten", + "passphrase": "Passphrase", + "recipient": "Empfänger", + "members-title": "Mitglieder", + "settings": "Einstellungen", + "chats": "Chats", + "image-source-make-photo": "Erfassen", + "save": "Speichern", + "sync-in-progress": "Synchronisierung...", + "incorrect-code": [ + "str", + "Leider ist der Code nicht korrekt. Bitte versuche es nochmals" + ], + "image-source-gallery": "Aus der Galerie auswählen", + "sync-synced": "Synchron", + "status-pending": "Anhängig", + "datetime-day": { + "one": "Tag", + "other": "Tage" + }, + "scan-qr": "QR scannen", + "recent": "Kürzlich", + "status": "Status", + "status-sent": "Gesendet", + "wrong-password": "Falsches Passwort", + "clear-history": "Verlauf löschen", + "no-contacts": "Noch keine Kontakte", + "datetime-today": "heute", + "web-view-error": "oops, Fehler", + "can-not-add-yourself": "Du kannst dich nicht selbst hinzufügen", + "add-to-contacts": "Zu Kontakten hinzufügen", + "available": "Verfügbar", + "You": "Du", + "main-wallet": "Haupt-Portemonnaie", + "members": { + "one": "1 Mitglied", + "other": "{{count}} Mitglieder", + "zero": "Keine Mitglieder" + }, + "intro-message1": "Willkommen zu Status\nTap diese Nachricht um dein Passwort einzurichten & loszulegen!", + "new-contact": "Neuer Kontakt", + "datetime-second": { + "one": "Sekunde", + "other": "Sekunden" + }, + "recover": "Wiederherstellen", + "datetime-minute": { + "one": "Minute", + "other": "Minuten" + }, + "active-unknown": "Unbekannt", + "public-key": "Öffentlicher Schlüssel", + "profile": "Profil", + "none": "Keine", + "removed": "entfernt", + "message": "Nachricht", + "here-is-your-passphrase": "Hier ist deine Passphrase, *Schreibe diese auf und bewahre diese sicher auf!* Du benötigst diese, um dein Konto wiederherzustellen.", + "image-source-title": "Profilbild", + "left": "verlassen" +} diff --git a/translations/el.json b/translations/el.json new file mode 100644 index 00000000000..dc400fd30a1 --- /dev/null +++ b/translations/el.json @@ -0,0 +1,1360 @@ +{ + "You": "Εσείς", + "about-app": "Σχετικά με", + "about-key-storage-content": "Το Status δεν θα έχει ποτέ πρόσβαση στο ιδιωτικό σας κλειδί. Φροντίστε να δημιουργήσετε αντίγραφα ασφαλείας της φράσης seed σας. Εάν χάσετε το τηλέφωνό σας, είναι ο μόνος τρόπος πρόσβασης στα κλειδιά σας.", + "about-key-storage-title": "Σχετικά με την αποθήκευση κλειδιών", + "about-names-content": "Κανείς δεν μπορεί να προσποιηθεί ότι είσαι εσύ! Είσαι ανώνυμος από προεπιλογή και δεν χρειάζεται να αποκαλύψεις το πραγματικό σου όνομα. Μπορείτε να καταχωρήσετε ένα προσαρμοσμένο όνομα έναντι μικρής χρέωσης.", + "about-names-title": "Δεν είναι δυνατή η αλλαγή των ονομάτων", + "accept": "Αποδοχή", + "accept-and-share-address": "Αποδοχή και κοινή χρήση διεύθυνσης", + "access-existing-keys": "Πρόσβαση στα υπάρχοντα κλειδιά", + "access-key": "Κλειδί πρόσβασης", + "account-added": "Προστέθηκε λογαριασμός", + "account-color": "Χρώμα λογαριασμού", + "account-content": "Μπορείτε να συγκρίνετε λογαριασμούς του Status με τραπεζικούς λογαριασμούς. Όπως ένας τραπεζικός λογαριασμός, ένας λογαριασμός έχει συνήθως διεύθυνση και υπόλοιπο. Χρησιμοποιείτε αυτόν τον λογαριασμό για συναλλαγές στο Ethereum. Μπορείτε να έχετε πολλούς λογαριασμούς στο πορτοφόλι σας. Όλα προσβάσιμα με ξεκλείδωμα Status.", + "account-exists-title": "Ο λογαριασμός υπάρχει ήδη", + "account-name": "Όνομα λογαριασμού", + "account-settings": "Ρυθμίσεις λογαριασμού", + "account-title": "Λογαριασμός", + "accounts": "Λογαριασμοί", + "active-online": "Σε σύνδεση", + "active-unknown": "Άγνωστο", + "add": "Προσθήκη", + "add-a-watch-account": "Προσθέστε μια διεύθυνση μόνο για παρακολούθηση", + "add-account": "Προσθέστε έναν λογαριασμό", + "add-account-description": "Μπορείτε να εισαγάγετε οποιονδήποτε τύπο λογαριασμού Ethereum για να τον προσθέσετε στο πορτοφόλι Status σας", + "add-account-incorrect-password": "Ο κωδικός πρόσβασης φαίνεται να είναι λανθασμένος. Εισαγάγετε τον κωδικό πρόσβασης που χρησιμοποιείτε για να ξεκλειδώσετε την εφαρμογή.", + "add-an-account": "Προσθέστε έναν λογαριασμό", + "add-bootnode": "Προσθήκη bootnode", + "add-contact": "Προσθήκη επαφής", + "add-custom-token": "Προσθήκη προσαρμοσμένου token", + "add-favourite": "Προσθήκη αγαπημένου", + "add-mailserver": "Προσθήκη κόμβου ιστορικού", + "add-members": "Προσθήκη μελών", + "add-network": "Προσθήκη δικτύου", + "add-new-contact": "Προσθήκη νέας επαφής", + "add-nickname": "Προσθέστε ένα ψευδώνυμο (προαιρετικό)", + "add-private-key-account": "Προσθήκη λογαριασμού από ιδιωτικό κλειδί", + "add-seed-account": "Προσθέστε λογαριασμό με μια φράση seed", + "add-to-contacts": "Προσθήκη στις επαφές", + "add-to-contacts-text": "Προσθέτοντας έναν χρήστη στη λίστα επαφών σας, μοιράζεστε τη διεύθυνση του πορτοφολιού σας", + "add-to-favourites": "Προσθήκη στα αγαπημένα", + "add-watch-account": "Προσθήκη λογαριασμού μόνο για παρακολούθηση", + "address": "Διεύθυνση", + "address-or-ens-name": "Διεύθυνση ή όνομα ENS", + "address-received": "Η διεύθυνση ελήφθη", + "address-request-accepted": "Το αίτημα διεύθυνσης έγινε αποδεκτό", + "address-requested": "Ζητήθηκε διεύθυνση", + "advanced": "Προηγμένη", + "advanced-settings": "Προηγμένες ρυθμίσεις", + "advertiser-description": "Ανακαλύψατε το Status χάρη σε έναν συνεργάτη. Σας πειράζει εάν το Status ελέγξει τη διεύθυνση IP σας μία φορά, ώστε να ανταμειφθεί; Αυτές οι πληροφορίες δεν θα χρησιμοποιηθούν για οτιδήποτε άλλο και θα καταργηθούν εντελώς μετά από 7 ημέρες.", + "advertiser-starter-pack-accept": "Αποδοχή", + "advertiser-starter-pack-decline": "Απόρριψη", + "advertiser-starter-pack-description": "Εδώ είναι μερικά crypto για να ξεκινήσετε! Χρησιμοποιήστε τα για να πάρετε αυτοκόλλητα, ένα όνομα ENS και να δοκιμάσετε dapps", + "advertiser-starter-pack-title": "Πακέτο εκκίνησης", + "advertiser-title": "Απόρρητο από προεπιλογή", + "agree-by-continuing": "Συνεχίζοντας, συμφωνείτε\n στο δικό μας ", + "all": "Όλα", + "allow": "Να επιτρέπεται", + "allow-mention-notifications": "Εμφάνιση @ αναφορών", + "allowing-authorizes-this-dapp": "Η άδεια επιτρέπει σε αυτήν την εφαρμογή να ανακτήσει τη διεύθυνση του πορτοφολιού σας και να ενεργοποιήσει το Web3", + "already-have-asset": "Έχετε ήδη αυτό το περιουσιακό στοιχείο", + "amount": "Ποσό", + "appearance": "Εμφάνιση", + "apply": "Εφαρμογή", + "are-you-sure": "Είστε σίγουρει?", + "are-you-sure-description": "Δεν θα μπορείτε να δείτε ξανά ολόκληρη τη φράση seed", + "are-you-sure-to-cancel": "Είστε βέβαιοι ότι θέλετε να ακυρώσετε;", + "are-you-sure?": "Είστε σίγουρει?", + "ask-in-status": "Κάντε μια ερώτηση ή αναφέρετε ένα σφάλμα", + "at": "στο", + "attribution-received": "{{attrib}} από τα {{max}} μπόνους που ελήφθησαν", + "audio": "Ήχος", + "audio-recorder": "Ηχογράφηση", + "audio-recorder-error": "Σφάλμα ηχογράφησης", + "audio-recorder-max-ms-reached": "Συμπληρώθηκε ο μέγιστος χρόνος καταγραφής", + "audio-recorder-permissions-error": "Πρέπει να δώσετε άδεια για την αποστολή ηχητικών μηνυμάτων", + "authorize": "Εξουσιοδοτώ", + "available": "Διαθέσιμα", + "available-participants": { + "one": "Μπορείτε να επιλέξετε έναν ακόμη συμμετέχοντα", + "other": "Μπορείτε να επιλέξετε {{count}} ακόμη συμμετέχοντες" + }, + "back": "Πίσω", + "back-up-seed-phrase": "Αντίγραφο ασφαλείας φράσης seed", + "back-up-your-seed-phrase": "Δημιουργήστε αντίγραφα ασφαλείας για τη φράση seed σας", + "backup-disabled": "Απενεργοποιημένα", + "backup-recovery-phrase": "Αντίγραφο ασφαλείας φράσης seed", + "balance": "Υπόλοιπο", + "begin-set-up": "Έναρξη ρύθμισης", + "biometric-auth-android-sensor-desc": "Αισθητήρας αφής", + "biometric-auth-android-sensor-error-desc": "Απέτυχε", + "biometric-auth-android-title": "Απαιτείται Πιστοποίηση", + "biometric-auth-confirm-logout": "Επανασύνδεση", + "biometric-auth-confirm-message": "Απαιτείται βιομετρικός έλεγχος ταυτότητας για να συνεχιστεί, εάν δεν είναι δυνατόν, ξεκλειδώστε τα κλειδιά σας με τον κωδικό πρόσβασης σας", + "biometric-auth-confirm-title": "Πρέπει να κάνετε έλεγχο ταυτότητας!", + "biometric-auth-confirm-try-again": "Προσπάθησε ξανά", + "biometric-auth-error": "Δεν είναι δυνατή η εκτέλεση βιομετρικού ελέγχου ταυτότητας ({{code}})", + "biometric-auth-login-error-title": "Σφάλμα βιομετρικού ελέγχου ταυτότητας", + "biometric-auth-login-ios-fallback-label": "Εισάγετε τον κωδικό πρόσβασης", + "biometric-auth-reason-login": "Σύνδεση στο Status", + "biometric-auth-reason-verify": "Επαλήθευση ελέγχου ταυτότητας", + "biometric-disable-bioauth": "απενεργοποίηση {{bio-type-label}}", + "biometric-disable-password-description": "Εάν απενεργοποιήσετε αυτό, θα ", + "biometric-disable-password-title": "Απενεργοποίηση αποθήκευσης κωδικού πρόσβασης", + "biometric-enable": "Εάν δεν θέλετε να εισάγετε τον κωδικό σας κάθε φορά για πρόσβαση στην εφαρμογή, ενεργοποιήστε τη {{bio-type-label}} σύνδεση ", + "biometric-enable-button": "Ενεργοποίηση {{bio-type-label}}", + "biometric-enable-keycard": "Εάν δεν θέλετε να χρησιμοποιείτε την Keycard σας κάθε φορά για πρόσβαση στην εφαρμογή, ενεργοποιήστε τη {{bio-type-label}} σύνδεση ", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Δακτυλικό αποτύπωμα", + "biometric-secure-with": "Ασφαλίστε με {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "blank-keycard-text": "Μπορείτε να συνεχίσετε με την κάρτα-κλειδί σας μόλις δημιουργήσετε τα κλειδιά και το όνομά σας", + "blank-keycard-title": "Φαίνεται ότι έχετε πατήσει\n μια κενή κάρτα-κλειδί", + "block": "Αποκλεισμός", + "block-contact": "Αποκλεισμός αυτού του χρήστη", + "block-contact-details": "Ο αποκλεισμός θα διαγράψει τα προηγούμενα μηνύματα αυτού του χρήστη και θα σταματήσει τη λήψη νέων από εσάς", + "blocked-users": "Αποκλεισμένοι χρήστες", + "bootnode-address": "Διεύθυνση Bootnode", + "bootnode-details": "Λεπτομέρειες Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Οι bootnodes ενεργοποιήθηκαν", + "bootnodes-settings": "Ρυθμίσεις Bootnodes", + "browsed-websites": "Το ιστορικό του προγράμματος περιήγησης θα εμφανιστεί εδώ", + "browser": "Πρόγραμμα περιήγησης", + "browser-not-secure": "Η σύνδεση δεν είναι ασφαλής! Μην υπογράφετε συναλλαγές και μην στέλνετε προσωπικά δεδομένα σε αυτόν τον ιστότοπο.", + "browser-secure": "Η σύνδεση είναι ασφαλής. Βεβαιωθείτε ότι εμπιστεύεστε πραγματικά αυτόν τον ιστότοπο πριν υπογράψετε συναλλαγές ή εισαγάγετε προσωπικά δεδομένα.", + "browsers": "Πρόγραμμα περιήγησης", + "browsing-cancel": "Ακύρωση", + "browsing-open-in-android-web-browser": "Άνοιγμα σε Android", + "browsing-open-in-ios-web-browser": "Άνοιγμα σε iOS", + "browsing-open-in-status": "Άνοιγμα στο Status", + "browsing-site-blocked-description1": "Εντοπίσαμε πιθανή κακόβουλη δραστηριότητα από αυτήν τη διεύθυνση. Για να προστατεύσουμε εσάς και το πορτοφόλι σας, εμποδίζουμε την περαιτέρω πλοήγηση. \n\n Εάν πιστεύετε ότι πρόκειται για σφάλμα, ενημερώστε μας στο ", + "browsing-site-blocked-description2": " δημόσια συνομιλία.", + "browsing-site-blocked-go-back": "Πήγαινε πίσω", + "browsing-site-blocked-title": "Αυτός ο ιστότοπος έχει αποκλειστεί", + "browsing-title": "Περιήγηση", + "bug-report": "Αναφορά σφάλματος", + "buy-crypto-description": "Βρείτε ένα dapp για να αγοράσετε crypto τώρα", + "buy-crypto-title": "Φαίνεται ότι το πορτοφόλι σας είναι άδειο", + "camera-access-error": "Για να παραχωρήσετε την απαιτούμενη άδεια κάμερας, μεταβείτε στις ρυθμίσεις του συστήματός σας και βεβαιωθείτε ότι το Status > Κάμερα είναι επιλεγμένο.", + "can-not-add-yourself": "Εσείς είστε αυτός, για να ξεκινήσετε μια συνομιλία επιλέξτε κάποιον άλλο", + "cancel": "Ακύρωση", + "cancel-keycard-setup": "Ακύρωση ρύθμισης Keycard", + "cancelling": "Ακύρωση", + "cannot-read-card": "Δεν είναι δυνατή η ανάγνωση κάρτας.\n Κρατήστε τη στο πίσω μέρος του τηλεφώνου σας", + "cannot-use-default-pin": "Ο κωδικός 000000 δεν επιτρέπεται.\n Παρακαλώ χρησιμοποιήστε έναν άλλο αριθμό", + "cant-open-public-chat": "Δεν είναι δυνατό το άνοιγμα δημόσιας συνομιλίας", + "cant-report-bug": "Δεν είναι δυνατή η αναφορά σφάλματος", + "card-is-blank": "Αυτή η κάρτα είναι κενή", + "card-reseted": "Έγινε επαναφορά της κάρτας", + "card-unpaired": "Η κάρτα έχει αποσυνδεθεί από την τρέχουσα συσκευή", + "change-fleet": "Αλλαγή στόλου σε {{fleet}}", + "change-logging-enabled": "Είστε βέβαιοι ότι θέλετε να {{enable}} σύνδεση;", + "change-passcode": "Αλλαγή κωδικού πρόσβασης", + "change-password": "Αλλαγή κωδικού πρόσβασης", + "change-pin": "Αλλάξτε τον 6ψήφιο κωδικό πρόσβασης", + "changed-amount-warning": "Το ποσό άλλαξε από {{old}} σε {{new}}", + "changed-asset-warning": "Το περιουσιακό στοιχείο άλλαξε από {{old}} σε {{new}}", + "chaos-mode": "Λειτουργία χάους", + "chaos-unicorn-day": "Ημέρα Χάους Μονόκερου", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Συνομιλία", + "chat-and-transact": "Συνομιλήστε και πραγματοποιήστε συναλλαγές ιδιωτικά με φίλους", + "chat-is-a-contact": "Επικοινωνία", + "chat-is-not-a-contact": "Δεν είναι επαφή", + "chat-key": "Κλειδί συνομιλίας", + "chat-key-content": "Τα μηνύματα στο πρωτόκολλο συνομιλίας του Status αποστέλλονται και λαμβάνονται μέσω κλειδιών κρυπτογράφησης. Το δημόσιο κλειδί συνομιλίας είναι μια σειρά χαρακτήρων που μοιράζεστε με άλλους, ώστε να μπορούν να σας στέλνουν μηνύματα στο Status.", + "chat-key-title": "Κλειδί συνομιλίας", + "chat-link-previews": "Προεπισκοπήσεις συνδέσμου συνομιλίας", + "chat-name": "Όνομα συνομιλίας", + "chat-name-content": "Τρεις τυχαίες λέξεις, που παράγονται αλγοριθμικά από το κλειδί συνομιλίας σας και χρησιμοποιούνται ως το προεπιλεγμένο ψευδώνυμό σας στη συνομιλία. Τα ονόματα συνομιλίας είναι εντελώς μοναδικά· κανένας άλλος χρήστης δεν μπορεί να έχει τις ίδιες τρεις λέξεις.", + "chat-name-title": "Όνομα Συνομιλίας", + "chat-settings": "Ρυθμίσεις συνομιλίας", + "chats": "Συζητήσεις", + "check-on-opensea": "Ελέγξτε στο opensea", + "check-your-recovery-phrase": "Ελέγξτε τη φράση seed σας", + "choose-authentication-method": "Επιλέξτε μια μέθοδο ελέγχου ταυτότητας", + "clear": "Εκκαθάριση", + "clear-all": "Εκκαθάριση όλων", + "clear-history": "Καθαρισμός ιστορικού", + "clear-history-action": "Εκκαθάριση", + "clear-history-confirmation": "Καθαρισμός ιστορικού?", + "clear-history-confirmation-content": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το ιστορικό συνομιλίας;", + "clear-history-title": "Καθαρισμός ιστορικού?", + "close": "Κλείσιμο", + "close-all": "Κλείσιμο όλων", + "close-app-button": "Επιβεβαίωση", + "close-app-content": "Η εφαρμογή θα σταματήσει και θα κλείσει. Όταν την ανοίξετε ξανά, θα χρησιμοποιηθεί το επιλεγμένο δίκτυο", + "close-app-title": "Προειδοποίηση!", + "command-button-send": "Αποστολή", + "community-image-remove": "Κατάργηση", + "community-invite-title": "Προσκαλέστε", + "community-members-title": "Μέλη", + "community-share-title": "Κοινοποίηση", + "complete-hardwallet-setup": "Αυτή η κάρτα είναι πλέον συνδεδεμένη. Τη χρειάζεστε για να υπογράψετε συναλλαγές και να ξεκλειδώσετε τα κλειδιά σας", + "completed": "Ολοκληρωμένα", + "confirm": "Επιβεβαίωση", + "confirm-password-placeholder": "Επιβεβαιώστε τον κωδικό πρόσβασης σας...", + "confirmation-request": "Αίτημα επιβεβαίωσης", + "confirmations": "Επιβεβαιώσεις", + "confirmations-helper-text": "Όταν η συναλλαγή έχει 12 επιβεβαιώσεις, μπορείτε να την θεωρήσετε διευθετημένη.", + "connect": "Σύνδεση", + "connect-mailserver-content": "Σύνδεση στο {{name}};", + "connect-wallet": "Σύνδεση πορτοφολιού", + "connected": "Συνδεδεμένος", + "connecting": "Σύνδεση...", + "connecting-requires-login": "Για τη σύνδεση σε άλλο δίκτυο απαιτείται σύνδεση λογαριασμού", + "connection-with-the-card-lost": "Η σύνδεση με την κάρτα\n έχει χαθεί", + "connection-with-the-card-lost-setup-text": "Για να συνεχίσετε τη ρύθμιση κρατήστε την κάρτα στο\nπίσω μέρος του τηλεφώνου σας και κρατήστε την κάρτα σε επαφή με το τηλέφωνο", + "connection-with-the-card-lost-text": "Για να συνεχίσετε κρατήστε την κάρτα στο πίσω μέρος του τηλεφώνου σας", + "contact-code": "Κλειδί συνομιλίας", + "contact-s": { + "one": "επαφή", + "other": "επαφές" + }, + "contacts": "Επαφές", + "contacts-descr": "Οι επαφές σας θα εμφανίζονται εδώ. Θα λάβετε ενημερώσεις κατάστασης από οποιονδήποτε προσθέτετε ως επαφή", + "contacts-empty": "Οι επαφές με ονόματα ENS θα εμφανίζονται εδώ", + "continue": "Συνέχεια", + "continue-anyway": "Συνεχίστε ούτως ή άλλως", + "contract-address": "Διεύθυνση συμβολαίου", + "contract-interaction": "Αλληλεπίδραση συμβολαίου", + "copy-info": "Αντιγραφή πληροφοριών", + "copy-qr": "Αντιγραφή κωδικού", + "copy-to-clipboard": "Αντιγραφή", + "copy-transaction-hash": "Αντιγραφή αναγνωριστικού συναλλαγής", + "cost-fee": "Κόστος\/Τέλος", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Δημιουργία", + "create-a-pin": "Δημιουργήστε έναν 6ψήφιο κωδικό πρόσβασης", + "create-group-chat": "Δημιουργία ομαδικής συνομιλίας", + "create-multiaccount": "Δημιουργία κλειδιών", + "create-new-key": "Λάβετε νέα κλειδιά", + "create-pin": "Δημιουργήστε 6ψήφιο κωδικό πρόσβασης", + "create-pin-description": "Θα χρειαστείτε την κάρτα σας + αυτόν τον 6ψήφιο κωδικό πρόσβασης για να ξεκλειδώσετε το Status και να επιβεβαιώσετε τις συναλλαγές", + "created-group-chat-description": "Δημιουργήσατε την ομάδα {{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Νόμισμα", + "currency-display-name-aed": "Ντιράμ Ηνωμένων Αραβικών Εμιράτων", + "currency-display-name-afn": "Αφγάνι Αφγανιστάν", + "currency-display-name-ars": "Πέσο Αργεντινής", + "currency-display-name-aud": "Δολάριο Αυστραλίας", + "currency-display-name-bbd": "Δολάριο Μπαρμπάντος", + "currency-display-name-bdt": "Τάκα Μπαγκλαντές", + "currency-display-name-bgn": "Λεβ Βουλγαρίας", + "currency-display-name-bhd": "Δηνάριο Μπαχρέιν", + "currency-display-name-bnd": "Δολάριο Μπρουνέι Νταρουσαλάμ", + "currency-display-name-bob": "Μπολιβιάνο Βολιβίας", + "currency-display-name-brl": "Ρεάλ Βραζιλίας", + "currency-display-name-btn": "Νγκούλτρουμ Μπουτάν", + "currency-display-name-cad": "Δολάριο Καναδά", + "currency-display-name-chf": "Φράγκο Ελβετίας", + "currency-display-name-clp": "Πέσο Χιλής", + "currency-display-name-cny": "Γιουάν Ρενμίνμπι Κίνας", + "currency-display-name-cop": "Πέσο Κολομβίας", + "currency-display-name-crc": "Κολόν Κόστα Ρίκα", + "currency-display-name-czk": "Κορόνα Τσεχίας", + "currency-display-name-dkk": "Κορόνα Δανίας", + "currency-display-name-dop": "Πέσο Δομινικανής Δημοκρατίας", + "currency-display-name-egp": "Λίρα Αιγύπτου", + "currency-display-name-etb": "Μπιρ Αιθιοπίας", + "currency-display-name-eur": "Ευρώ", + "currency-display-name-gbp": "Βρετανική Λίρα", + "currency-display-name-gel": "Λάρι Γεωργίας", + "currency-display-name-ghs": "Σέντι Γκάνας", + "currency-display-name-hkd": "Δολάριο Χονγκ Κονγκ", + "currency-display-name-hrk": "Κούνα Κροατίας", + "currency-display-name-huf": "Φιορίνι Ουγγαρίας", + "currency-display-name-idr": "Ρουπία Ινδονησίας", + "currency-display-name-ils": "Σέκελ Ισραήλ", + "currency-display-name-inr": "Ρουπία Ινδίας", + "currency-display-name-isk": "Κορώνα Ισλανδίας", + "currency-display-name-jmd": "Δολάριο Τζαμάικας", + "currency-display-name-jpy": "Γιεν Ιαπωνίας", + "currency-display-name-kes": "Σελίνι Κένυας", + "currency-display-name-krw": "Γουόν Νότιας Κορέας", + "currency-display-name-kwd": "Δηνάριο Κουβέιτ", + "currency-display-name-kzt": "Τένγκε Καζακστάν", + "currency-display-name-lkr": "Ρουπία της Σρι Λάνκα", + "currency-display-name-mad": "Ντιρχάμ Μαρόκου", + "currency-display-name-mdl": "Λέου Μολδαβίας", + "currency-display-name-mur": "Ρουπία Μαυρίκιου", + "currency-display-name-mwk": "Κουάτσα Μαλάουι", + "currency-display-name-mxn": "Πέσο Μεξικού", + "currency-display-name-myr": "Ρινγκίτ Μαλαισίας", + "currency-display-name-mzn": "Μετικάλ Μοζαμβίκης", + "currency-display-name-nad": "Δολάριο Ναμίμπια", + "currency-display-name-ngn": "Νάιρα Νιγηρίας", + "currency-display-name-nok": "Κορόνα Νορβηγίας", + "currency-display-name-npr": "Ρουπία Νεπάλ", + "currency-display-name-nzd": "Δολάριο Νέας Ζηλανδίας", + "currency-display-name-omr": "Ριάλ Ομάν", + "currency-display-name-pen": "Σολ Περού", + "currency-display-name-pgk": "Κίνα Παπούας Νέας Γουινέας", + "currency-display-name-php": "Πέσο Φιλιππίνων", + "currency-display-name-pkr": "Ρουπία Πακιστάν", + "currency-display-name-pln": "Ζλότι Πολωνίας", + "currency-display-name-pyg": "Γκουαράνι Παραγουάης", + "currency-display-name-qar": "Ριάλ Κατάρ", + "currency-display-name-ron": "Λέου Ρουμανίας", + "currency-display-name-rsd": "Δηνάριο Σερβίας", + "currency-display-name-rub": "Ρούβλι Ρωσίας", + "currency-display-name-sar": "Ριάλ Σαουδικής Αραβίας", + "currency-display-name-sek": "Κορώνα Σουηδίας", + "currency-display-name-sgd": "Δολάριο Σιγκαπούρης", + "currency-display-name-thb": "Μπατ Ταϊλάνδης", + "currency-display-name-try": "Λίρα Τουρκίας", + "currency-display-name-ttd": "Δολάριο του Τρινιντάντ και Τομπάγκο", + "currency-display-name-twd": "Νέο δολάριο Ταϊβάν", + "currency-display-name-tzs": "Σελίνι Τανζανίας", + "currency-display-name-uah": "Ουκρανία Χρυβνία", + "currency-display-name-ugx": "Σελίνι Ουγκάντας", + "currency-display-name-usd": "Δολάριο Ηνωμένων Πολιτειών", + "currency-display-name-uyu": "Πέσο Ουρουγουάης", + "currency-display-name-vef": "Μπολιβάρ Βενεζουέλας", + "currency-display-name-vnd": "Ντονγκ Βιετνάμ", + "currency-display-name-zar": "Ραντ Νότιας Αφρικής", + "current-network": "Τρέχων δίκτυο", + "current-pin": "Εισαγάγετε τον 6ψήφιο κωδικό πρόσβασης", + "current-pin-description": "Εισαγάγετε τον 6ψήφιο κωδικό πρόσβασης για να συνεχίσετε", + "custom": "Προσαρμοσμένο", + "custom-networks": "Προσαρμοσμένα δίκτυα", + "custom-seed-phrase": "Μη έγκυρη φράση seed", + "custom-seed-phrase-text-1": "Αυτή η φράση seed δεν ταιριάζει με το υποστηριζόμενο λεξικό μας. Ελέγξτε για λέξεις με ορθογραφικά λάθη.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Αποδοχή και Άνοιγμα", + "dapp-starter-pack-description": "Να και κάποια crypto για να ξεκινήσετε! Χρησιμοποιήστε τα για να λάβετε αυτοκόλλητα, ένα όνομα ENS και να δοκιμάσετε dapps", + "dapp-starter-pack-title": "Πακέτο εκκίνησης", + "dapp-would-like-to-connect-wallet": "θα ήθελε να συνδεθεί στο", + "dapps": "ÐApps", + "dapps-permissions": "Δικαιώματα DApp", + "dark": "Σκοτεινό", + "data": "Δεδομένα", + "data-syncing": "Συγχρονισμός δεδομένων", + "datetime-ago": "πριν", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "one": "μέρα", + "other": "μέρες" + }, + "datetime-day-short": { + "one": "Μ", + "other": "Μ" + }, + "datetime-hour": { + "one": "ώρα", + "other": "ώρες" + }, + "datetime-hour-short": { + "one": "Ω", + "other": "Ω" + }, + "datetime-minute": { + "one": "λεπτό", + "other": "λεπτά" + }, + "datetime-minute-short": { + "one": "Λ", + "other": "Λ" + }, + "datetime-second": { + "one": "δευτερόλεπτο", + "other": "δευτερόλεπτα" + }, + "datetime-second-short": { + "one": "Τώρα", + "other": "Μήνα" + }, + "datetime-today": "σήμερα", + "datetime-yesterday": "εχθές", + "decimals": "Δεκαδικά", + "decline": "Απόρριψη", + "decryption-failed-content": "Παρουσιάστηκε σφάλμα κατά την αποκρυπτογράφηση των δεδομένων σας. Ίσως χρειαστεί να διαγράψετε τα παλιά σας δεδομένα και να δημιουργήσετε έναν νέο λογαριασμό. Πατήστε \"Εφαρμογή\" για διαγραφή ή \"Ακύρωση\" για να δοκιμάσετε ξανά", + "default": "Προκαθορισμένο", + "default-assets": "Προεπιλεγμένα ERC20 και ERC721", + "delete": "Διαγραφή", + "delete-account": "Διαγραφή λογαριασμού", + "delete-and-leave-group": "Διαγραφή και έξοδος από την ομάδα", + "delete-bootnode": "Διαγραφή bootnode", + "delete-bootnode-are-you-sure": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον bootnode;", + "delete-bootnode-title": "Διαγραφή bootnode", + "close-chat": "Διαγραφή συνομιλίας", + "close-chat-confirmation": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη συζήτηση;", + "delete-confirmation": "Διαγραφή;", + "delete-mailserver": "Διαγραφή κόμβου ιστορικού", + "delete-mailserver-are-you-sure": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κόμβο ιστορικού;", + "delete-mailserver-title": "Διαγραφή κόμβου ιστορικού", + "delete-message": "Διαγραφή μηνύματος", + "delete-my-account": "Διαγραφή του λογαριασμού μου", + "delete-my-profile": "Διαγραφή του προφίλ μου", + "delete-network-confirmation": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το δίκτυο;", + "delete-network-error": "Συνδεθείτε σε διαφορετικό δίκτυο πριν διαγράψετε αυτό", + "delete-network-title": "Διαγραφή δικτύου;", + "delete-profile": "Διαγραφή προφίλ", + "delete-profile-warning": "Προειδοποίηση: Εάν δεν έχετε γράψει τη φράση seed σας, θα χάσετε την πρόσβαση στα χρήματά σας μετά τη διαγραφή του προφίλ σας", + "deny": "Απόρριψη", + "derivation-path": "Διαδρομή προέλευσης", + "description": "Περιγραφή", + "dev-mode": "Λειτουργία ανάπτυξης", + "dev-mode-settings": "Ρυθμίσεις λειτουργίας ανάπτυξης", + "device-syncing": "Συγχρονισμός συσκευής", + "devices": "Συσκευές", + "disable": "απενεργοποίηση", + "disabled": "Απενεργοποιημένα", + "disconnected": "Συνομιλία εκτός σύνδεσης", + "discover": "Ανακαλύψτε", + "dismiss": "Απόρριψη", + "done": "Έγινε", + "dont-ask": "Μη με ξαναρωτήσεις", + "edit": "Επεξεργασία", + "edit-favourite": "Επεξεργασία αγαπημένου", + "edit-group": "Επεξεργασία ομάδας", + "edit-profile": "Επεξεργασία προφίλ", + "empty-chat-description": "Δεν υπάρχουν μηνύματα\n σε αυτήν τη συνομιλία ακόμα", + "empty-chat-description-one-to-one": "Όποια μηνύματα στέλνετε εδώ είναι κρυπτογραφημένα και μπορούν να διαβαστούν μόνο από εσάς και ", + "empty-chat-description-public": "Ήταν ήσυχα εδώ τις τελευταίες {{quiet-hours}}. Ξεκινήστε τη συνομιλία ή ", + "empty-chat-description-public-share-this": "μοιραστείτε αυτήν τη συνομιλία.", + "empty-pending-invitations-descr": "Άτομα που επιθυμούν να συμμετάσχουν στην ομάδα\n μέσω συνδέσμου πρόσκλησης θα εμφανίζονται εδώ", + "empty-tab": "Κενή καρτέλα", + "enable": "Ενεργοποίηση", + "enable-all": "Ενεργοποίηση όλων", + "enable-link-previews": "Ενεργοποίηση προεπισκοπήσεων συνδέσμων στη συνομιλία;", + "encrypt-with-password": "Κρυπτογράφηση με κωδικό πρόσβασης", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Προσθήκη ονόματος χρήστη", + "ens-agree-to": "Συμφωνώ να", + "ens-chat-settings": "Ρυθμίσεις συνομιλίας", + "ens-custom-domain": "Προσαρμοσμένος τομέας", + "ens-custom-username-hints": "Πληκτρολογήστε ολόκληρο το όνομα χρήστη, συμπεριλαμβανομένου του προσαρμοσμένου τομέα, όπως username.domain.eth", + "ens-custom-username-taken": "Το όνομα χρήστη δεν σας ανήκει :(", + "ens-deposit": "Κατάθεση", + "ens-dismiss-message": "Κάντε κλικ εδώ για απόρριψη", + "ens-displayed-with": "Τα μηνύματά σας εμφανίζονται σε άλλους με", + "ens-get-name": "Αποκτήστε ένα καθολικό όνομα χρήστη", + "ens-got-it": "Εντάξει, το κατάλαβα", + "ens-locked": "Το όνομα χρήστη είναι κλειδωμένο. Δεν θα μπορείτε να το απελευθερώσετε μέχρι {{date}}", + "ens-name-content": "Προσαρμοσμένο ψευδώνυμο για το κλειδί συνομιλίας που μπορείτε να καταχωρήσετε χρησιμοποιώντας την υπηρεσία ονόματος Ethereum. Τα ονόματα ENS είναι αποκεντρωμένα ονόματα χρήστη.", + "ens-name-not-found": "Δεν είναι δυνατή η επίλυση του ονόματος ENS", + "ens-name-title": "Όνομα ENS", + "ens-network-restriction": "Διατίθεται μόνο στο Mainnet", + "ens-no-usernames": "Δεν έχετε συνδεδεμένο όνομα χρήστη", + "ens-powered-by": "Με την υποστήριξη των Ethereum Name Services", + "ens-primary-username": "Κύριο όνομα χρήστη", + "ens-register": "Εγγραφή", + "ens-registration-failed": "Για να καταχωρήσετε το όνομα χρήστη, δοκιμάστε ξανά.", + "ens-registration-failed-title": "Η συναλλαγή απέτυχε", + "ens-registration-failure": "Η εγγραφή απέτυχε", + "ens-registration-in-progress": "Η εγγραφή βρίσκεται σε εξέλιξη ...", + "ens-release-username": "Απελευθερώστε το όνομα χρήστη", + "ens-remove-hints": "Η κατάργηση θα αποσυνδέσει το όνομα χρήστη από το κλειδί σας.", + "ens-remove-username": "Κατάργηση ονόματος χρήστη", + "ens-saved": " είναι πλέον συνδεδεμένο με το κλειδί συνομιλίας και μπορεί να χρησιμοποιηθεί στο Status.", + "ens-saved-title": "Προστέθηκε όνομα χρήστη", + "ens-show-username": "Εμφάνιση του ονόματος χρήστη ENS στις συζητήσεις", + "ens-terms-header": "Όροι εγγραφής ονόματος", + "ens-terms-point-1": "Τα χρήματα κατατίθενται για 1 έτος. Το SNT σας θα κλειδωθεί, αλλά δεν θα δαπανηθεί.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Μητρώο ENS).", + "ens-terms-point-2": "Μετά από 1 έτος, μπορείτε να απελευθερώσετε το όνομα και να πάρετε την κατάθεσή σας πίσω, ή να μην κάνετε καμία ενέργεια για να κρατήσετε το όνομα.", + "ens-terms-point-3": "Εάν οι όροι του συμβολαίου αλλάξουν -- π.χ. το Status κάνει αναβάθμιση του συμβολαίου -- ο χρήστης έχει το δικαίωμα να αποδεσμεύσει το όνομα χρήστη ανεξάρτητα από το χρόνο κράτησης.", + "ens-terms-point-4": "Ο ελεγκτής συμβολαίου δεν μπορεί να αποκτήσει πρόσβαση στα κατατεθειμένα σας χρήματα. Μπορούν να μετακινηθούν μόνο πίσω στη διεύθυνση που τα έστειλε.", + "ens-terms-point-5": "Οι διευθύνσεις σας θα συσχετιστούν δημόσια με το ENS όνομά σας.", + "ens-terms-point-6": "Τα ονόματα χρήστη δημιουργούνται ως κόμβοι υποτομέα του stateofus.eth και υπόκεινται στους όρους του έξυπνου συμβολαίου ENS.", + "ens-terms-point-7": "Εξουσιοδοτείτε το συμβόλαιο για τη μεταφορά SNT εκ μέρους σας. Αυτό μπορεί να συμβεί μόνο όταν εγκρίνετε μια συναλλαγή για την εξουσιοδότηση της μεταφοράς.", + "ens-terms-point-8": "Αυτοί οι όροι διασφαλίζονται από τη λογική του έξυπνου συμβολαίου στις διευθύνσεις:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar) ", + "ens-terms-registration": "Όροι εγγραφής ονόματος.", + "ens-test-message": "Γεια", + "ens-transaction-pending": "Εκκρεμεί συναλλαγή...", + "ens-understand": "Κατανοώ ότι η διεύθυνση πορτοφολιού μου θα συνδεθεί δημόσια με το όνομα χρήστη μου.", + "ens-username": "Όνομα χρήστη ENS", + "ens-username-already-added": "Το όνομα χρήστη είναι ήδη συνδεδεμένο με το κλειδί συνομιλίας σας και μπορεί να χρησιμοποιηθεί εντός του Status.", + "ens-username-available": "✓ Διαθέσιμο όνομα χρήστη!", + "ens-username-connected": "Αυτό το όνομα χρήστη σας ανήκει και συνδέεται με το κλειδί συνομιλίας σας.", + "ens-username-connected-continue": "Συνεχίστε να ορίζετε «Εμφάνιση του ονόματος χρήστη ENS μου στις συζητήσεις».", + "ens-username-connected-with-different-key": "Εάν συνεχίσετε, απαιτείται μια συναλλαγή για τη σύνδεση του ονόματος χρήστη με το τρέχον κλειδί συνομιλίας σας.", + "ens-username-connection-confirmation": "{{username}} θα συνδεθεί μόλις ολοκληρωθεί η συναλλαγή.", + "ens-username-hints": "Τουλάχιστον 4 χαρακτήρες. Λατινικά γράμματα, αριθμούς και πεζά μόνο.", + "ens-username-invalid": "Γράμματα και αριθμοί μόνο.", + "ens-username-owned": "✓ Το όνομα χρήστη ανήκει σε εσάς. ", + "ens-username-owned-continue": "Εάν συνεχίσετε, θα συνδέσετε αυτό το όνομα χρήστη με το κλειδί συνομιλίας σας.", + "ens-username-registration-confirmation": "Ωραία! Είστε κάτοχος του {{username}} μόλις ολοκληρωθεί η συναλλαγή.", + "ens-username-taken": "Το όνομα χρήστη χρησιμοποιείται ήδη :(", + "ens-username-you-can-follow-progress": "Μπορείτε να παρακολουθήσετε την πρόοδο στην ενότητα Ιστορικό Συναλλαγών του πορτοφολιού σας.", + "ens-usernames": "ENS ονόματα χρήστη", + "ens-usernames-details": "Καταχωρήστε ένα καθολικό όνομα χρήστη ώστε να αναγνωρίζεται εύκολα από άλλους χρήστες", + "ens-want-custom-domain": "Έχω ένα όνομα σε άλλο τομέα", + "ens-want-domain": "Θέλω έναν τομέα stateofus.eth", + "ens-welcome-hints": "Τα ονόματα ENS μετατρέπουν αυτές τις πολύ μεγάλες διευθύνσεις σε μοναδικά ονόματα χρήστη.", + "ens-welcome-point-customize": "Ένα όνομα ENS μπορεί να αντικαταστήσει το τυχαίο όνομά σας 3 λέξεων στη συνομιλία. Γίνετε @yourname αντί για {{name}}.", + "ens-welcome-point-customize-title": "Προσαρμόστε το όνομα της συνομιλίας σας", + "ens-welcome-point-receive": "Άλλοι μπορούν να σας στείλουν χρήματα μέσω συνομιλίας με ένα απλό βήμα.", + "ens-welcome-point-receive-title": "Λάβετε συναλλαγές στη συνομιλία", + "ens-welcome-point-register": "Εγγραφείτε μία φορά για να διατηρήσετε το όνομα για πάντα. Μετά από 1 έτος, μπορείτε να αποδεσμεύσετε το όνομα και να λάβετε ξανά τα SNT.", + "ens-welcome-point-register-title": "10 SNT για εγγραφή", + "ens-welcome-point-simplify": "Μπορείτε να λάβετε χρήματα στο εύχρηστο προς διαμοιρασμό όνομα ENS αντί για το δεκαεξαδικό κρυπτογράφημα (0x...).", + "ens-welcome-point-simplify-title": "Απλοποιήστε τη διεύθυνση ETH σας", + "ens-welcome-point-verify": "Μπορείτε να επαληθεύσετε και να προσθέσετε τα ονόματα χρήστη που σας ανήκουν στα επόμενα βήματα.", + "ens-welcome-point-verify-title": "Έχετε ήδη όνομα χρήστη;", + "ens-your-username": "Το όνομα χρήστη σας", + "ens-your-usernames": "Τα ονόματα χρήστη σας", + "ens-your-your-name": "Το ENS όνομα σας", + "enter-12-words": "Εισαγάγετε τις 12 λέξεις της φράσης seed σας, χωρισμένες με μεμονωμένα κενά", + "enter-a-private-key": "Εισαγάγετε ένα ιδιωτικό κλειδί", + "enter-a-seed-phrase": "Εισαγάγετε μια φράση seed", + "enter-address": "Εισαγάγετε διεύθυνση", + "enter-contact-code": "Εισαγάγετε το όνομα χρήστη ENS ή το κλειδί συνομιλίας", + "enter-pair-code": "Εισαγάγετε τον κωδικό σύζευξης", + "enter-pair-code-description": "Ο κωδικός σύζευξης εμφανίστηκε σε εσάς κατά τη ρύθμιση της Keycard", + "enter-password": "Εισάγετε τον κωδικό πρόσβασης", + "enter-pin": "Εισαγάγετε τον 6ψήφιο κωδικό πρόσβασης", + "enter-puk-code": "Εισαγάγετε τον κωδικό PUK", + "enter-puk-code-description": "Ο 6ψήφιος κωδικός πρόσβασης έχει αποκλειστεί.\n Εισαγάγετε τον κωδικό PUK για να ξεμπλοκάρετε τον κωδικό πρόσβασης.", + "enter-recipient-address-or-username": "Εισαγάγετε τη διεύθυνση ή το όνομα χρήστη του παραλήπτη", + "enter-seed-phrase": "Εισαγάγετε φράση seed", + "enter-url": "Εισαγάγετε διεύθυνση URL", + "enter-watch-account-address": "Σάρωση κωδικού QR\n ή\n εισαγάγετε τη διεύθυνση για παρακολούθηση", + "enter-word": "Εισαγάγετε λέξη", + "enter-your-code": "Εισαγάγετε τον 6ψήφιο κωδικό πρόσβασης", + "enter-your-password": "Εισάγετε τον κωδικό σας", + "error": "Σφάλμα", + "error-unable-to-get-balance": "Δεν είναι δυνατή η λήψη υπολοίπου", + "error-unable-to-get-prices": "Σφάλμα μετατροπής νομίσματος. Ανανεώστε την οθόνη σας για να δοκιμάσετε ξανά.", + "error-unable-to-get-token-balance": "Δεν είναι δυνατή η λήψη υπολοίπου token", + "errors": "Σφάλματα", + "eth": "ETH", + "ethereum-account": "Λογαριασμός Ethereum", + "ethereum-address": "Διεύθυνση Ethereum", + "ethereum-node-started-incorrectly-description": "Ο κόμβος Ethereum ξεκίνησε με εσφαλμένη ρύθμιση παραμέτρων, η εφαρμογή θα διακοπεί για ανάκτηση από αυτήν την κατάσταση. Ρυθμισμένο αναγνωριστικό δικτύου = {{network-id}}, πραγματικό = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Ο κόμβος Ethereum ξεκίνησε εσφαλμένα", + "etherscan-lookup": "Κοιτάξτε στο Ethercan", + "export-account": "Εξαγωγή λογαριασμού", + "external-storage-denied": "Δεν επιτρέπεται η πρόσβαση σε εξωτερικό χώρο αποθήκευσης", + "failed": "Απέτυχε", + "faq": "Συχνές ερωτήσεις", + "favourite": "Αγαπημένο", + "favourite-description": "Οι αγαπημένοι σας ιστότοποι θα εμφανίζονται εδώ", + "favourites": "Αγαπημένα", + "favourites-empty": "Οι διευθύνσεις που προστέθηκαν στα αγαπημένα θα εμφανίζονται εδώ", + "fetch-messages": "↓ Λήψη μηνυμάτων", + "fetch-timeline": "↓ Λήψη", + "find": "Εύρεση", + "finish": "Τέλος", + "finishing-card-setup": "Ολοκληρώνεται η ρύθμιση της κάρτας", + "fleet": "Στόλος", + "fleet-settings": "Ρυθμίσεις στόλου", + "follow-your-interests": "Μεταβείτε σε μια δημόσια συνομιλία και γνωρίστε νέα άτομα", + "free": "↓ Δωρεάν", + "from": "Από", + "gas-limit": "Όριο Gas", + "gas-price": "Τιμή Gas", + "gas-used": "Χρησιμοποιημένο gas", + "generate-a-key": "Δημιουργία κλειδιών", + "generate-a-new-account": "Δημιουργία λογαριασμού", + "generate-a-new-key": "Δημιουργήστε ένα νέο κλειδί", + "generate-account": "Δημιουργία κλειδιών", + "generate-an-account": "Δημιουργία λογαριασμού", + "generate-new-key": "Δημιουργία κλειδιών", + "generating-codes-for-pairing": "> Λήψη λογισμικού προϊόντος στην κάρτα\n > Δημιουργία κωδικών ξεκλειδώματος & σύζευξης", + "generating-keys": "Δημιουργούνται κλειδιά...", + "generating-mnemonic": "Δημιουργία φράσης seed", + "get-a-keycard": "Αποκτήστε μια Keycard", + "get-started": "Ξεκινήστε", + "get-status-at": "Λήψη Status http:\/\/status.im", + "get-stickers": "Λάβετε αυτοκόλλητα", + "give-permissions-camera": "Δώστε άδεια\nγια πρόσβαση στην κάμερα", + "glossary": "Γλωσσάριο", + "go-to-settings": "Μεταβείτε στις Ρυθμίσεις...", + "got-it": "Το έπιασα", + "grant-face-id-permissions": "Για να παραχωρήσετε την απαιτούμενη άδεια στο Face ID, μεταβείτε στις ρυθμίσεις του συστήματός σας και βεβαιωθείτε ότι το Status > Face ID είναι επιλεγμένο", + "group-chat": "Ομαδική συζήτηση", + "group-chat-admin": "Διαχειριστής", + "group-chat-admin-added": "Ο χρήστης **{{member}}** έχει γίνει διαχειριστής", + "group-chat-all-contacts-invited": "Όλες οι επαφές σας είναι ήδη στην ομάδα", + "group-chat-created": "Ο χρήστης **{{member}}** δημιούργησε την ομάδα **{{name}}**", + "group-chat-decline-invitation": "Απόρριψη πρόσκλησης", + "group-chat-member-added": "Ο χρήστης **{{member}}** έχει προσκληθεί", + "group-chat-member-joined": "Ο χρήστης **{{member}}** συμμετέχει στην ομάδα", + "group-chat-member-removed": "Ο χρήστης **{{member}}** αποχώρησε από την ομάδα", + "group-chat-members-count": "{{selected}} \/ {{max}} μέλη", + "group-chat-name-changed": "Ο χρήστης **{{member}}** άλλαξε το όνομα της ομάδας σε **{{name}}**", + "group-chat-no-contacts": "Δεν έχετε ακόμα επαφές.\n Προσκαλέστε τους φίλους σας να για να ξεκινήσετε τη συζήτηση", + "group-info": "Πληροφορίες ομάδας", + "group-invite": "Ομαδική πρόσκληση", + "group-invite-link": "Σύνδεσμος πρόσκλησης ομάδας", + "group-membership-request": "Αίτηση μέλους ομάδας", + "gwei": "Gwei", + "has-permissions": "έχει άδεια πρόσβασης", + "hash": "Hash", + "help": "βοήθεια", + "help-capitalized": "Βοήθεια", + "help-center": "Κέντρο βοήθειας", + "hide-content-when-switching-apps": "Αποκλεισμός στιγμιότυπων οθόνης", + "hide-content-when-switching-apps-ios": "Απόκρυψη προεπισκόπησης", + "history": "Ιστορικό", + "history-nodes": "Κόμβοι ιστορικού", + "hold-card": "Κρατήστε την κάρτα στο πίσω μέρος\n του τηλεφώνου σας", + "home": "Αρχική", + "hooks": "Hooks", + "how-it-works": "Πώς λειτουργεί", + "http-gateway-error": "Ουπς, η αίτηση απέτυχε!", + "identifier": "Αναγνωριστικό", + "image": "Εικόνα", + "image-remove-current": "Κατάργηση τρέχουσας φωτογραφίας", + "image-source-gallery": "Επιλέξτε από τη συλλογή", + "image-source-make-photo": "Λήψη", + "image-source-title": "Επεξεργασία εικόνας", + "in-contacts": "στις επαφές", + "incoming": "Εισερχόμενα", + "incoming-transaction": "Εισερχόμενη συναλλαγή", + "incorrect-code": [ + "οδός", + "Δυστυχώς, ο κωδικός δεν ήταν σωστός, εισαγάγετε τον ξανά" + ], + "increase-gas": "Αύξηση Gas", + "initialization": "Αρχικοποίηση", + "install": "↓ Εγκατάσταση", + "intro-message1": "Καλώς ήλθατε στο Status!\n Πατήστε αυτό το μήνυμα για να ορίσετε τον κωδικό πρόσβασης σας και να ξεκινήσετε.", + "intro-privacy-policy-note1": "Το Status δεν συλλέγει ούτε επωφελείται από τα προσωπικά σας δεδομένα. Συνεχίζοντας, συμφωνείτε με το ", + "intro-privacy-policy-note2": "πολιτική απορρήτου", + "intro-text": "Το Status είναι η είσοδος σας στον αποκεντρωμένο ιστό", + "intro-text1": "Συνομιλήστε μέσω ενός peer-to-peer, κρυπτογραφημένου δικτύου όπου τα μηνύματα δεν μπορούν να λογοκριθούν ή να παραβιαστούν", + "intro-text2": "Αποστολή και λήψη ψηφιακών περιουσιακών στοιχείων οπουδήποτε στον κόσμο - δεν απαιτείται τραπεζικός λογαριασμός", + "intro-text3": "Εξερευνήστε παιχνίδια, συναλλαγές και κοινωνικά δίκτυα όπου μόνο εσείς κατέχετε τα δεδομένα σας", + "intro-title1": "Πραγματικά ιδιωτική επικοινωνία", + "intro-title2": "Ασφαλές πορτοφόλι crypto", + "intro-title3": "Αποκεντρωμένες εφαρμογές", + "intro-wizard-text1": "Ένα σύνολο κλειδιών ελέγχει το λογαριασμό σας. Τα κλειδιά σας ζουν στο τηλέφωνό σας, ώστε μόνο εσείς να μπορείτε να τα χρησιμοποιήσετε", + "intro-wizard-text2": "Ένα κλειδί είναι για συνομιλία. Έρχεται με ένα αναγνώσιμο όνομα που δεν μπορεί να αλλάξει.", + "intro-wizard-text3": "Εάν έχετε Keycard, αποθηκεύστε τα κλειδιά σας εκεί για βελτιωμένη ασφάλεια.", + "intro-wizard-text4": "Ασφαλίστε και κρυπτογραφήστε τα κλειδιά σας", + "intro-wizard-text6": "Το Status θα σας ειδοποιήσει για νέα μηνύματα. Μπορείτε να επεξεργαστείτε τις προτιμήσεις ειδοποιήσεων αργότερα στις ρυθμίσεις", + "intro-wizard-title-alt4": "Δημιούργησε έναν κωδικό πρόσβασης", + "intro-wizard-title-alt5": "Επιβεβαιώστε τον κωδικό πρόσβασης σας", + "intro-wizard-title1": "Πάρτε τα κλειδιά σας", + "intro-wizard-title2": "Επιλέξτε ένα όνομα συνομιλίας", + "intro-wizard-title3": "Επιλογή χώρου αποθήκευσης κλειδιών", + "intro-wizard-title4": "Δημιουργήστε έναν 6ψήφιο κωδικό πρόσβασης", + "intro-wizard-title5": "Επιβεβαιώστε τον κωδικό πρόσβασης", + "intro-wizard-title6": "Ενεργοποίηση ειδοποιήσεων", + "introduce-yourself": "Συστήστε τον εαυτό σας με ένα σύντομο μήνυμα", + "invalid-address-qr-code": "Ο σαρωμένος κωδικός QR δεν περιέχει έγκυρη διεύθυνση", + "invalid-format": "Μη έγκυρη μορφή\nΠρέπει να είναι {{format}}", + "invalid-key-confirm": "Εφαρμογή", + "invalid-key-content": "Η βάση δεδομένων δεν μπορεί να κρυπτογραφηθεί επειδή ένα αρχείο είναι κατεστραμμένο. Τα χρήματα και το κλειδί συνομιλίας σας είναι ασφαλή. Δεν είναι δυνατή η επαναφορά άλλων δεδομένων, όπως οι συζητήσεις και οι επαφές σας. Το κουμπί \"{{erase-multiaccounts-data-button-text}}\", θα καταργήσει όλα τα άλλα δεδομένα και σας επιτρέπει να έχετε πρόσβαση στα χρήματά σας και να στέλνετε μηνύματα", + "invalid-number": "Μη έγκυρος αριθμός", + "invalid-pairing-password": "Μη έγκυρος κωδικός σύζευξης", + "invalid-public-chat-topic": "Μη έγκυρο θέμα δημόσιας συνομιλίας", + "invalid-range": "Μη έγκυρη μορφή, πρέπει να είναι μεταξύ {{min}} και {{max}}", + "invite": "Προσκαλέστε", + "invite-button": "Προσκαλέστε", + "invite-chat-accept": "Αποδέχομαι", + "invite-chat-intro": "Σας παρέπεμψε ένας φίλος για να συμμετάσχετε στο Status. Εδώ είναι μερικά crypto για να ξεκινήσετε! Χρησιμοποιήστε τα για να καταχωρήσετε ένα όνομα ENS ή να αγοράσετε ένα πακέτο αυτοκόλλητων", + "invite-chat-name": "Παραπομπή φίλου", + "invite-chat-pending": "Εκκρεμούν", + "invite-chat-rule": "Η αποδοχή θα ανταμείψει επίσης το φίλο σας με ένα μπόνους παραπομπής κρυπτογράφησης", + "invite-chat-starter-pack": "Πακέτο εκκίνησης", + "invite-friends": "Πρόσκληση φίλων", + "invite-instruction-fifth": "Μπορείτε να επιλέξετε να εξαργυρώσετε το μπόνους παραπομπής σας ανά πάσα στιγμή.", + "invite-instruction-first": "Στέλνετε έναν μοναδικό σύνδεσμο πρόσκλησης στον φίλο σας για λήψη και συμμετοχή στο Status", + "invite-instruction-fourth": "Λαμβάνετε το μπόνους παραπομπής και ο φίλος σας το Πακέτο Εκκίνησης", + "invite-instruction-second": "Ο φίλος σας κατεβάζει το Status και δημιουργεί έναν λογαριασμό (σε Android)", + "invite-instruction-third": "Ξεκινά μια συνομιλία με τον φίλο σας, όπου επιβεβαιώνουν την παραπομπή σας", + "invite-privacy-policy1": "Με την αποδοχή συμφωνείτε με το πρόγραμμα παραπομπής", + "invite-privacy-policy2": "Όροι και Προϋποθέσεις.", + "invite-receive-account": "Λογαριασμός για να λάβετε το μπόνους παραπομπής σας", + "invite-reward": "Κερδίστε crypto για κάθε φίλο που προσκαλείτε!", + "invite-reward-friend": "Φίλος: ", + "invite-reward-friend-description": "Ο φίλος σας θα λάβει ένα πακέτο εκκίνησης που αποτελείται από μερικά {{reward}} για να ξεκινήσει", + "invite-reward-friend-name": "Πακέτο εκκίνησης", + "invite-reward-you": "Εσείς: ", + "invite-reward-you-description": "Προσκαλέστε έναν φίλο και λάβετε {{reward}} ως μπόνους παραπομπής. Χρησιμοποιήστε το για να λάβετε αυτοκόλλητα, ένα όνομα ENS και να δοκιμάσετε dapps", + "invite-reward-you-name": "Μπόνους παραπομπής", + "invite-select-account": "Επιλέξτε έναν λογαριασμό για να λάβετε το μπόνους παραπομπής σας", + "invite-warning": "Αυτή η προσφορά ισχύει μόνο για χρήστες συσκευών Android, οι οποίοι δεν είναι κάτοικοι των ΗΠΑ. Ο φίλος πρέπει να επιβεβαιώσει την παραπομπή εντός 7 ημερών", + "invited": "προσκάλεσε", + "join": "Συμμετοχή", + "join-group-chat": "Συμμετοχή στην ομάδα", + "join-group-chat-description": "Ο χρήστης {{username}} σας προσκάλεσε να συμμετάσχετε στην ομάδα {{group-name}}", + "join-me": "Γεια έλα μαζί μου στο Status: {{url}}", + "join-new-private-chat": "Ξεκινήστε μια νέα ιδιωτική συνομιλία", + "join-new-public-chat": "Λάβετε μέρος σε μια δημόσια συνομιλία", + "joined-group-chat-description": "Έχετε ενταχθεί στην ομάδα {{group-name}} με πρόσκληση από {{username}}", + "key": "Κλειδί", + "keycard": "Keycard", + "keycard-access-reset": "Έχει γίνει επαναφορά πρόσβασης στην Keycard", + "keycard-applet-install-instructions": "Για να εγκαταστήσετε το applet ακολουθήστε τις οδηγίες στη διεύθυνση https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Δοκιμάστε να μετακινήσετε την κάρτα γύρω για να βρείτε τη λειτουργία ανάγνωσης NFC στη συσκευή σας", + "keycard-awaiting-title": "Συνεχίζεται η αναζήτηση...", + "keycard-blocked": "Η Keycard έχει αποκλειστεί.\n Πρέπει να επαναφέρετε την κάρτα για να συνεχίσετε να τη χρησιμοποιείτε.", + "keycard-can-use-with-new-passcode": "Μπορείτε να χρησιμοποιήσετε αυτήν την κάρτα με τον νέο κωδικό πρόσβασης", + "keycard-cancel-setup-text": "Αυτό θα ακυρώσει τη ρύθμιση της κάρτας. Συνιστάται ιδιαίτερα να ολοκληρώσετε τη ρύθμιση προκειμένου να χρησιμοποιήσετε την κάρτα-κλειδί. Θέλετε πραγματικά να ακυρώσετε;", + "keycard-cancel-setup-title": "Επικίνδυνη λειτουργία", + "keycard-connected-description": "Δοκιμάστε να κρατήσετε την κάρτα ακίνητη", + "keycard-connected-title": "Συνδεδεμένος", + "keycard-desc": "Έχετε Keycard; Αποθηκεύστε τα κλειδιά σας σε αυτήν· θα την χρειαστείτε για συναλλαγές", + "keycard-dont-ask-card": "Μη ζητήσετε κάρτα για σύνδεση", + "keycard-enter-new-passcode": "Εισαγάγετε νέο κωδικό πρόσβασης {{step}}\/2", + "keycard-error-description": "Συνδέστε ξανά την κάρτα για να συνεχίσετε", + "keycard-error-title": "Η σύνδεση χάθηκε", + "keycard-free-pairing-slots": "Η Keycard έχει {{n}} υποδοχές δωρεάν ζεύξης ", + "keycard-has-multiaccount-on-it": "Αυτή η κάρτα είναι γεμάτη. Κάθε κάρτα μπορεί να κρατήσει ένα κύριο keypair", + "keycard-init-description": "Βάλτε την κάρτα στο πίσω μέρος του τηλεφώνου σας για να συνεχίσετε", + "keycard-init-title": "Αναζήτηση καρτών...", + "keycard-is-blocked-details": "Δεν μπορείτε πλέον να χρησιμοποιήσετε αυτήν την κάρτα για πρόσβαση ή εγγραφή για αυτόν τον λογαριασμό. Υπήρξαν πάρα πολλές αποτυχημένες προσπάθειες κωδικού πρόσβασης και PUK.", + "keycard-is-blocked-instructions": "Για να αποκτήσετε πρόσβαση στον λογαριασμό σας, επανεγκαταστήστε το Status και χρησιμοποιήστε μια νέα Keycard, χρησιμοποιήστε ένα διαφορετικό πορτοφόλι ή επαναφέρετε την Keycard χειροκίνητα.", + "keycard-is-blocked-title": "Η Keycard έχει αποκλειστεί", + "keycard-is-frozen-details": "Για την προστασία των περιουσιακών σας στοιχείων, η κάρτα σας έχει παγώσει. Επαναφέρετε την πρόσβαση στην κάρτα για να ξεκλειδώσετε κλειδιά και να στείλετε συναλλαγές. Δημιουργήστε έναν νέο κωδικό πρόσβασης και εισαγάγετε το PUK για πρόσβαση στους λογαριασμούς σας σε αυτήν την κάρτα", + "keycard-is-frozen-reset": "Επαναφορά πρόσβασης στην κάρτα", + "keycard-is-frozen-title": "Η Keycard έχει παγώσει", + "keycard-onboarding-finishing-header": "Ολοκλήρωση", + "keycard-onboarding-intro-header": "Αποθηκεύστε τα κλειδιά σας στην Keycard", + "keycard-onboarding-intro-text": "Ετοιμαστείτε, αυτό μπορεί να διαρκέσει λίγα λεπτά, αλλά είναι σημαντικό να προστατέψετε τον λογαριασμό σας", + "keycard-onboarding-pairing-header": "Σύζευξη της κάρτας...", + "keycard-onboarding-preparing-header": "Προετοιμασία της κάρτας...", + "keycard-onboarding-puk-code-header": "Γράψτε τους κωδικούς\n και φυλάξτε τους με ασφάλεια", + "keycard-onboarding-recovery-phrase-description": "Χρειάζεστε αυτήν τη φράση seed για να πάρετε πίσω το κλειδί σας. Σημειώστε τη. Διατηρήστε την ασφαλή, εκτός σύνδεσης και διαχωρίστε την από αυτήν τη συσκευή.", + "keycard-onboarding-recovery-phrase-header": "Αντίγραφο ασφαλείας φράσης seed", + "keycard-onboarding-recovery-phrase-text": "Μόνο για τα μάτια σας . Αυτός είναι το μαγικό seed που χρησιμοποιείται για τη δημιουργία του κλειδιού σας.", + "keycard-onboarding-start-header": "Κρατήστε την κάρτα στο πίσω μέρος\n του τηλεφώνου σας για να ξεκινήσετε", + "keycard-onboarding-start-step1": "Δημιουργήστε έναν κωδικό πρόσβασης", + "keycard-onboarding-start-step1-text": "Περίπου 1 λεπτό. Δημιουργήστε έναν 6ψήφιο κωδικό πρόσβασης για κρυπτογράφηση των κλειδιών σας", + "keycard-onboarding-start-step2": "Σημειώστε το PUK και τον κωδικό σύζευξης", + "keycard-onboarding-start-step2-text": "Περίπου 1 λεπτό. Θα χρειαστείτε ένα κομμάτι χαρτί και ένα μολύβι για αυτό", + "keycard-onboarding-start-step3": "Δημιουργήστε αντίγραφα ασφαλείας για τη φράση seed", + "keycard-onboarding-start-step3-text": "Περίπου 1 λεπτό. Απαιτούνται επίσης ένα κομμάτι χαρτί και ένα μολύβι", + "keycard-onboarding-start-text": "Και διατηρήστε την κάρτα στην τηλεφωνική επαφή\n κατά τη διάρκεια της εγκατάστασης. Η εγκατάσταση θα διαρκέσει περίπου 4 λεπτά", + "keycard-processing-description": "Δοκιμάστε να κρατήσετε την κάρτα ακίνητη", + "keycard-processing-title": "Επεξεργασία...", + "keycard-recovery-intro-button-text": "Ξεκινήστε την ανάκτηση", + "keycard-recovery-intro-header": "Ανάκτηση κλειδιών που είναι αποθηκευμένα στην κάρτα-κλειδί", + "keycard-recovery-intro-text": "Εάν δημιουργήσατε κλειδιά χρησιμοποιώντας μια κάρτα-κλειδί πριν και τώρα θέλετε να χρησιμοποιήσετε αυτά τα κλειδιά σε αυτήν τη συσκευή", + "keycard-recovery-no-key-header": "Δεν υπάρχει τίποτα\nγια ανάκτηση εδώ", + "keycard-recovery-no-key-text": "Η Keycard σας δεν έχει αποθηκευμένο κλειδί σε αυτήν. Για να την χρησιμοποιήσετε, δημιουργήστε ένα νέο κλειδί και επιλέξτε την Keycard σας για να αποθηκεύσετε το κλειδί", + "keycard-recovery-phrase-confirm-header": "Επιβεβαιώστε τη φράση seed", + "keycard-recovery-phrase-confirmation-text": "Δεν θα έχετε δεύτερη ευκαιρία! Εάν χάσετε την πρόσβαση, χάνοντας για παράδειγμα την κάρτα-κλειδί σας, μπορείτε να αποκτήσετε πρόσβαση στα κλειδιά σας μόνο με τη φράση seed σας. Κανείς, εκτός από εσάς δεν έχει τη φράση seed σας. Σημειώστε τη. Κρατήστε την ΑΣΦΑΛΗ.", + "keycard-recovery-phrase-confirmation-title": "Σημειώσατε τη φράση seed;", + "keycard-recovery-success-header": "Τα κλειδιά σας έχουν ανακτηθεί με επιτυχία", + "keycard-redeem-title": "Εξαργύρωση σε", + "keycard-redeem-tx": "Εξαργύρωση περιουσιακών στοιχείων", + "keycard-redeem-tx-desc": "Πατήστε την κάρτα για να υπογράψετε και να λάβετε περιουσιακά στοιχεία", + "keycard-reset-passcode": "Επαναφορά κωδικού πρόσβασης", + "keycard-success-description": "Μπορείτε να αφαιρέσετε την κάρτα τώρα", + "keycard-success-title": "Επιτυχία", + "keycard-unauthorized-operation": "Δεν έχετε εξουσιοδότηση για την εκτέλεση αυτής της λειτουργίας.\n Πατήστε έγκυρη κάρτα και δοκιμάστε ξανά.", + "language": "Γλώσσα", + "learn-more": "Μάθετε περισσότερα", + "learn-more-about-keycard": "Μάθετε περισσότερα για την Keycard", + "leave": "Αποχώρηση", + "leave-chat": "Αποχώρηση από συνομιλία", + "leave-chat-confirmation": "Είστε βέβαιοι ότι θέλετε να αποχωρήσετε από τη συζήτηση;", + "leave-confirmation": "Αποχώρηση από συνομιλία", + "leave-group": "Αποχώρηση από την ομάδα", + "left": "αριστερά", + "les-ulc": "LES\/ULC", + "lets-go": "Πάμε", + "light": "Φωτεινό", + "linked-on": "Συνδέθηκε στις {{date}}", + "load-messages-before": "πριν από τις {{date}}", + "load-more-messages": "↓ Λήψη περισσότερων μηνυμάτων", + "load-more-timeline": "↓ Λήψη περισσότερων", + "loading": "Φόρτωση...", + "local-notifications": "Τοπικές ειδοποιήσεις", + "lock-app-with": "Κλείδωμα εφαρμογής με", + "log-level": "Επίπεδο καταγραφής", + "log-level-settings": "Ρυθμίσεις επιπέδου καταγραφής", + "logging": "Σύνδεση", + "logging-enabled": "Η σύνδεση ενεργοποιήθηκε;", + "login-pin-description": "Εισαγάγετε τον 6ψήφιο κωδικό πρόσβασης για να ξεκλειδώσετε τα κλειδιά σας", + "logout": "Αποσύνδεση", + "logout-app-content": "Ο λογαριασμός θα αποσυνδεθεί. Όταν το ξεκλειδώσετε ξανά, θα χρησιμοποιηθεί το επιλεγμένο δίκτυο", + "logout-are-you-sure": "Είστε σίγουροι ότι θέλετε\n να αποσυνδεθείτε;", + "logout-title": "Αποσύνδεση?", + "looking-for-cards": "Αναζήτηση καρτών...", + "lost-connection": "Χάθηκε η σύνδεση", + "mail-should-be-configured": "Ο πελάτης αλληλογραφίας πρέπει να ρυθμιστεί", + "mailserver-address": "Διεύθυνση κόμβου ιστορικού", + "mailserver-automatic": "Αυτόματη επιλογή", + "mailserver-automatic-switch-explanation": "Επιλέξτε τον ταχύτερο διαθέσιμο κόμβο ιστορικού", + "mailserver-connection-error": "Δεν ήταν δυνατή η σύνδεση στον κόμβο ιστορικού", + "mailserver-content": "Ένας κόμβος στο δίκτυο του Status που δρομολογεί και αποθηκεύει μηνύματα, για έως και 30 ημέρες.", + "mailserver-details": "Λεπτομέρειες κόμβου ιστορικού", + "mailserver-error-content": "Δεν ήταν δυνατή η πρόσβαση στον κόμβο ιστορικού που επιλέξατε.", + "mailserver-error-title": "Σφάλμα σύνδεσης με κόμβο ιστορικού", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Επιλέξτε έναν άλλο κόμβο ιστορικού", + "mailserver-reconnect": "Δεν ήταν δυνατή η σύνδεση στον κόμβο ιστορικού. Πατήστε για επανασύνδεση", + "mailserver-request-error-content": "Το ακόλουθο σφάλμα επιστράφηκε από τον κόμβο ιστορικού: {{error}}", + "mailserver-request-error-status": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση ιστορικού, ελέγξτε τα αρχεία καταγραφής για λεπτομέρειες", + "mailserver-request-error-title": "Σφάλμα αιτήματος κόμβου ιστορικού", + "mailserver-request-retry": "Επανάληψη αιτήματος", + "mailserver-retry": "Επανάληψη", + "mailserver-title": "Κόμβος ιστορικού", + "main-currency": "Κύριο νόμισμα", + "main-networks": "Κύρια δίκτυα", + "main-wallet": "Κύριο πορτοφόλι", + "mainnet-network": "Κύριο δίκτυο", + "make-admin": "Κάντον διαχειριστή", + "mark-all-read": "Σήμανση όλων ως αναγνωσμένων", + "maybe-later": "Ίσως αργότερα.", + "members": { + "one": "1 μέλος", + "other": "{{count}} μέλη" + }, + "members-active": { + "one": "1 μέλος", + "other": "{{count}} μέλη" + }, + "members-active-none": "δεν υπάρχουν μέλη", + "members-count": "{{count}} μέλη", + "members-label": "Μέλη", + "members-limit-reached": "Συμπληρώθηκε το όριο των μελών", + "members-title": "Μέλη", + "membership-declined": "Το αίτημα ιδιότητας μέλους απορρίφθηκε", + "membership-description": "Η συμμετοχή στην ομάδα απαιτεί την αποδοχή από τον διαχειριστή της ομάδας", + "membership-none": "Κανένα", + "message": "Μήνυμα", + "message-not-sent": "Το μήνυμα δεν εστάλη", + "message-options-cancel": "Ακύρωση", + "message-reply": "Απάντηση", + "messages": "Μηνύματα", + "might-break": "Ίσως σπάσει μερικά ÐApps", + "migrations-failed-content": "{{message}}\n έκδοση διαγράμματος: αρχική {{initial-version}} , τρέχουσα {{current-version}} , τελευταία {{last-version}} \n\n Παρουσιάστηκε σφάλμα βάσης δεδομένων. Τα χρήματα και το κλειδί συνομιλίας σας είναι ασφαλή. Δεν είναι δυνατή η επαναφορά άλλων δεδομένων, όπως οι συζητήσεις και οι επαφές σας. Το κουμπί \"{{erase-multiaccounts-data-button-text}}\", θα καταργήσει όλα τα άλλα δεδομένα και σας επιτρέπει να έχετε πρόσβαση στα χρήματά σας και να στέλνετε μηνύματα.", + "mobile-network-ask-me": "Να ενημερώνομαι όταν βρίσκομαι σε δίκτυο κινητής τηλεφωνίας", + "mobile-network-continue-syncing": "Συνέχιση συγχρονισμού", + "mobile-network-continue-syncing-details": "Μπορείτε να το αλλάξετε αργότερα στις ρυθμίσεις", + "mobile-network-go-to-settings": "Μεταβείτε στις ρυθμίσεις", + "mobile-network-settings": "Δεδομένα κινητής τηλεφωνίας", + "mobile-network-sheet-configure": "Μπορείτε να ρυθμίσετε το συγχρονισμό σε περισσότερη\n λεπτομέρεια στο", + "mobile-network-sheet-offline": "Δεν υπάρχει Wi-Fi, ο συγχρονισμός μηνυμάτων απενεργοποιήθηκε.", + "mobile-network-sheet-offline-details": "Ο συγχρονισμός με χρήση δικτύου κινητής τηλεφωνίας είναι απενεργοποιημένος", + "mobile-network-sheet-remember-choice": "Θυμήσου την επιλογή μου", + "mobile-network-sheet-settings": "ρυθμίσεις", + "mobile-network-start-syncing": "Έναρξη συγχρονισμού", + "mobile-network-stop-syncing": "Διακοπή συγχρονισμού", + "mobile-network-stop-syncing-details": "Μέχρι να συνδεθείτε στο Wi-Fi;", + "mobile-network-use-mobile": "Χρήση δεδομένων κινητής τηλεφωνίας", + "mobile-network-use-mobile-data": "Το Status χρησιμοποιεί πολλά δεδομένα κατά το συγχρονισμό των συνομιλιών και του πορτοφολιού.", + "mobile-network-use-wifi": "Μόνο Wi-Fi", + "mobile-syncing-sheet-details": "Το Status χρησιμοποιεί πολλά δεδομένα κατά το συγχρονισμό των συνομιλιών και του πορτοφολιού.", + "mobile-syncing-sheet-title": "Συγχρονισμός με χρήση δεδομένων κινητής τηλεφωνίας;", + "more": "περισσότερα", + "multiaccount-exists-content": "Τα κλειδιά για αυτόν τον λογαριασμό υπάρχουν ήδη και δεν μπορούν να προστεθούν ξανά. Εάν έχετε χάσει τον κωδικό πρόσβασής σας ή την Keycard, απεγκαταστήστε την εφαρμογή, εγκαταστήστε ξανά και αποκτήστε πρόσβαση στα κλειδιά σας εισάγοντας τη φράση seed σας", + "multiaccount-exists-title": "Υπάρχουν ήδη κλειδιά για αυτόν τον λογαριασμό", + "multiaccounts-recover-enter-phrase-text": "Εισαγάγετε 12, 15, 18, 21 ή 24 λέξεις.\n Διαχωρίστε τις λέξεις με ένα μόνο διάστημα.", + "multiaccounts-recover-enter-phrase-title": "Εισαγάγετε τη φράση seed σας", + "mute": "Σίγαση", + "my-accounts": "Οι λογαριασμοί μου", + "my-accounts-empty": "Οι διαθέσιμοι λογαριασμοί σας θα εμφανίζονται εδώ", + "my-status": "Η κατάστασή μου", + "name": "Όνομα", + "name-of-token": "Το όνομα του token σας", + "name-optional": "Όνομα (προαιρετικό)", + "need-help": "Χρειάζεστε βοήθεια?", + "network": "Δίκτυο", + "network-chain": "Αλυσίδα δικτύου", + "network-details": "Λεπτομέρειες δικτύου", + "network-fee": "Χρέωση δικτύου", + "network-id": "Αναγνωριστικό δικτύου", + "network-info": "Πληροφορίες δικτύου", + "network-invalid-network-id": "Το καθορισμένο αναγνωριστικό δικτύου δεν αντιστοιχεί στο αναγνωριστικό δικτύου από τη διεύθυνση URL RPC", + "network-invalid-status-code": "Μη έγκυρος κωδικός κατάστασης: {{code}}", + "network-invalid-url": "Η διεύθυνση URL δικτύου δεν είναι έγκυρη", + "network-settings": "Ρυθμίσεις δικτύου", + "new": "Νέο", + "new-chat": "Νέα συνομιλία", + "new-contact": "Νέα επαφή", + "new-contract": "Νέο Συμβόλαιο", + "new-favourite": "Νέο αγαπημένο", + "new-group": "Νέα ομάδα", + "new-group-chat": "Νέα ομαδική συνομιλία", + "new-network": "Νέο δίκτυο", + "new-pin-description": "Εισαγάγετε νέο 6ψήφιο κωδικό πρόσβασης", + "new-public-group-chat": "Συμμετοχή σε δημόσια συνομιλία", + "new-status": "Νέα κατάσταση", + "new-tab": "Νέα καρτέλα", + "next": "Επόμενο", + "nickname": "Ψευδώνυμο", + "nickname-description": "Τα ψευδώνυμα σας βοηθούν να αναγνωρίσετε άλλους στο Status.\n Μόνο εσείς μπορείτε να δείτε τα ψευδώνυμα που έχετε προσθέσει", + "no": "Όχι", + "no-collectibles": "Δεν υπάρχουν διαθέσιμα συλλεκτικά αντικείμενα", + "no-contacts": "Δεν υπάρχουν επαφές ακόμη", + "no-keycard-applet-on-card": "Δεν υπάρχει εφαρμογή Keycard στην κάρτα", + "no-messages": "Δεν υπάρχουν μηνύματα", + "no-pairing-slots-available": "Έχει γίνει ήδη σύζευξη αυτής της κάρτα σε 5 συσκευές και δεν μπορεί να γίνει σε αυτήν. Χρησιμοποιήστε μία από τις συσκευές που έχει γίνει σύζευξη, συνδεθείτε με αυτήν την κάρτα και ελευθερώστε τις θέσεις σύζευξης στην κάρτα", + "no-result": "Δεν υπάρχουν αποτελέσματα", + "no-tokens-found": "Δεν βρέθηκαν tokens", + "node-info": "Πληροφορίες κόμβου", + "node-version": "Έκδοση κόμβου", + "nonce": "Όχι", + "none": "Κανένα", + "not-applicable": "Δεν ισχύει για συναλλαγές χωρίς υπογραφή", + "not-enough-snt": "Δεν υπάρχει αρκετό SNT", + "not-keycard-text": "Η κάρτα που χρησιμοποιήσατε δεν είναι Keycard. Πρέπει να αγοράσετε μια Keycard για να τη χρησιμοποιήσετε", + "not-keycard-title": "Δεν είναι Keycard", + "not-registered": "μη εγγεγραμμένος", + "notification-settings": "Ειδοποιήσεις", + "notifications": "Ειδοποιήσεις", + "notifications-non-contacts": "Ειδοποιήσεις από όλους", + "notifications-preferences": "Προτιμήσεις ειδοποιήσεων", + "notifications-servers": "Διακομιστές ειδοποιήσεων", + "notifications-switch": "Εμφάνιση ειδοποιήσεων", + "notifications-transactions": "Συναλλαγές πορτοφολιού", + "notify": "Ειδοποίηση", + "now": "Τώρα", + "off": "Ανενεργό", + "off-status-tree": "Δέντρο εκτός Status", + "offline": "Εκτός σύνδεσης", + "offline-messaging-use-history-explanation": "Ενεργοποίηση κόμβων ιστορικού για τη λήψη μηνυμάτων που στάλθηκαν ενώ η εφαρμογή ήταν κλειστή. Όταν ενεργοποιηθεί, ένας κόμβος ιστορικού λαμβάνει τη διεύθυνση IP σας. Όταν απενεργοποιηθεί, δεν θα λαμβάνετε μηνύματα όταν η εφαρμογή είναι κλειστή και δεν θα τα βλέπετε όταν ανοίγετε την εφαρμογή αργότερα.", + "offline-messaging-use-history-nodes": "Χρήση κόμβων ιστορικού", + "ok": "Εντάξει", + "ok-continue": "Εντάξει, συνεχίστε", + "ok-got-it": "Εντάξει, το κατάλαβα", + "ok-save-pass": "Εντάξει, αποθήκευση κωδικού πρόσβασης", + "okay": "Εντάξει", + "on": "Ενεργό", + "on-status-tree": "Δέντρο εντός Status", + "once-enabled-share-metadata": "Μόλις ενεργοποιηθεί, οι σύνδεσμοι που δημοσιεύονται στη συνομιλία ενδέχεται να μοιράζονται τα μεταδεδομένα σας με την τοποθεσία", + "open": "Άνοιγμα", + "open-chat": "Άνοιγμα συνομιλίας", + "open-dapp": "Άνοιγμα ÐApp", + "open-dapp-store": "Ανακαλύψτε ÐApps", + "open-home": "Ανοίξτε...", + "open-in-new-tab": "Άνοιγμα σε νέα καρτέλα", + "open-nfc-settings": "Ανοίξτε τις ρυθμίσεις NFC", + "optional": "προαιρετικό", + "or": "Ή", + "outgoing": "Εξερχόμενα", + "outgoing-transaction": "Εξερχόμενη συναλλαγή", + "page-camera-request-blocked": "τα αιτήματα κάμερας αποκλείστηκαν. Για να ενεργοποιήσετε τα αιτήματα κάμερας, μεταβείτε στις Ρυθμίσεις", + "page-would-like-to-use-camera": "θα ήθελε να χρησιμοποιήσει την κάμερα σας", + "pair": "Σύζευξη συσκευών", + "pair-card": "Σύζευξη με αυτήν τη συσκευής", + "pair-code": "Κωδικός σύζευξης", + "pair-code-explanation": "Σύζευξη κάρτας σε διαφορετική συσκευή (έως 5) για να ξεκλειδώσετε κλειδιά και να υπογράψετε συναλλαγές με την ίδια Keycard", + "pair-code-placeholder": "Κωδικός ζεύγους...", + "pair-this-card": "Σύζευξη αυτής της κάρτας", + "pair-this-device": "Διαφήμιση συσκευής", + "pair-this-device-description": "Σύζευξη των συσκευών σας για συγχρονισμό επαφών και συνομιλιών μεταξύ τους", + "paired-devices": "Σύζευξη συσκευών", + "pairing": "Σύζευξη", + "pairing-card": "Κάρτα σύζευξης", + "pairing-go-to-installation": "Μεταβείτε στις ρυθμίσεις σύζευξης", + "pairing-maximum-number-reached-content": "Απενεργοποιήστε μία από τις συσκευές σας πριν ενεργοποιήσετε μια νέα.", + "pairing-maximum-number-reached-title": "Συμπληρώθηκε ο μέγιστος αριθμός συσκευών", + "pairing-new-installation-detected-content": "Εντοπίστηκε μια νέα συσκευή.\n Για να χρησιμοποιήσετε σωστά τις συσκευές σας, είναι σημαντική η σύζευξη και η ενεργοποίηση προτού τις χρησιμοποιήσετε.\n Μεταβείτε στην ενότητα της συσκευής κάτω από τις ρυθμίσεις για να γίνει σύζευξη των συσκευών σας.", + "pairing-new-installation-detected-title": "Εντοπίστηκε νέα συσκευή", + "pairing-no-info": "Δεν υπάρχουν πληροφορίες", + "pairing-please-set-a-name": "Ορίστε ένα όνομα για τη συσκευή σας.", + "passphrase": "Φράση πρόσβασης", + "password": "Κωδικός πρόσβασης", + "password-description": "Τουλάχιστον 6 χαρακτήρες. Ο κωδικός πρόσβασης σας προστατεύει τα κλειδιά σας. Τον χρειάζεστε για να ξεκλειδώσετε το Status και να κάνετε συναλλαγές.", + "password-placeholder": "Κωδικός πρόσβασης...", + "password-placeholder2": "Επιβεβαίωση του κωδικού πρόσβασής σας", + "password_error1": "Οι κωδικοί πρόσβασης δεν ταιριάζουν.", + "paste": "Επικόλληση", + "paste-json": "Επικόλληση JSON", + "pay-to-chat": "Πληρωμή για συνομιλία", + "peer-content": "Μια συσκευή συνδεδεμένη στο δίκτυο συνομιλίας του Status. Κάθε χρήστης μπορεί να αντιπροσωπεύει έναν ή περισσότερους ομότιμους, ανάλογα με τον αριθμό των συσκευών τους.", + "peer-title": "Ομότιμος", + "peers": "Ομότιμοι", + "pending": "Εκκρεμούν", + "pending-confirmation": "Εκκρεμεί επιβεβαίωση...", + "pending-invitations": "Εκκρεμείς αιτήσεις μελών", + "permissions": "Άδειες", + "phone-e164": "Διεθνές 1", + "photos": "Φωτογραφίες", + "photos-access-error": "Για να παραχωρήσετε την άδεια των απαιτούμενων φωτογραφιών, μεταβείτε στις ρυθμίσεις του συστήματός σας και βεβαιωθείτε ότι το Status > Φωτογραφίες είναι επιλεγμένο.", + "pin-changed": "Ο 6ψήφιος κωδικός πρόσβασης άλλαξε", + "pin-code": "6ψήφιος κωδικός πρόσβασης", + "pin-mismatch": "Λανθασμένος κωδικός πρόσβασης", + "pin-one-attempt": " μία προσπάθεια ", + "pin-one-attempt-blocked-after": "πριν μπλοκαριστεί η Keycard σας", + "pin-one-attempt-blocked-before": "Προσέξτε, έχετε μόνο", + "pin-one-attempt-frozen-after": "προτού παγώσει η Keycard σας", + "pin-one-attempt-frozen-before": "Προσέξτε, έχετε μόνο", + "pin-retries-left": "Απομένουν {{number}}", + "preference": "Προτίμηση", + "preview-privacy": "Προεπισκόπηση της λειτουργίας απορρήτου", + "previewing-may-share-metadata": "Η προεπισκόπηση συνδέσμων από αυτούς τους ιστότοπους ενδέχεται να κοινοποιεί τα μεταδεδομένα σας στους κατόχους τους", + "privacy": "Απόρρητο", + "privacy-and-security": "Απόρρητο και ασφάλεια", + "privacy-policy": "Πολιτική απορρήτου", + "private-key": "Ιδιωτικό κλειδί", + "private-notifications": "Ιδιωτικές ειδοποιήσεις", + "private-notifications-descr": "Το Status θα σας ειδοποιήσει για νέα μηνύματα. Μπορείτε να επεξεργαστείτε τις προτιμήσεις ειδοποιήσεων αργότερα στις ρυθμίσεις.", + "processing": "Μια στιγμή", + "product-information": "Πληροφορίες Προϊόντος", + "profile": "Προφίλ", + "profile-deleted-content": "Το προφίλ σας διαγράφηκε με επιτυχία", + "profile-deleted-title": "Το προφίλ διαγράφηκε", + "profile-details": "Λεπτομέρειες προφίλ", + "profile-pic-pick": "Επιλέξτε από τη συλλογή", + "profile-pic-remove": "Κατάργηση φωτογραφίας", + "profile-pic-take": "Βγάλε φωτογραφία", + "public-chat": "Δημόσια συνομιλία", + "public-chat-description": "Λάβετε μέρος σε δημόσιες συνομιλίες για τα ενδιαφέροντά σας! Ο καθένας μπορεί να ξεκινήσει μια νέα.", + "public-chats": "Δημόσιες συνομιλίες", + "public-group-status": "Δημόσιο", + "public-group-topic": "Θέμα", + "public-key": "Δημόσιο κλειδί", + "puk-and-pairing-codes-displayed": "Εμφάνιση κωδικών PUK και ζεύξης", + "puk-code": "Κωδικός PUK", + "puk-code-explanation": "Εάν ξεχάσετε τον 6ψήφιο κωδικό πρόσβασης ή τον εισαγάγετε εσφαλμένα 3 φορές, θα χρειαστείτε αυτόν τον κωδικό για να ξεκλειδώσετε την κάρτα σας.", + "puk-mismatch": "Λανθασμένος κωδικός PUK", + "push-failed-transaction": "Η συναλλαγή σας απέτυχε", + "push-failed-transaction-body": "{{value}} {{currency}} στην {{to}}", + "push-inbound-transaction": "Λάβατε {{value}} {{currency}}", + "push-inbound-transaction-body": "Από {{from}} στον {{to}}", + "push-notifications-server-enabled": "Ο διακομιστής ενεργοποιήθηκε", + "push-notifications-servers": "Διακομιστές ειδοποιήσεων προώθησης", + "push-outbound-transaction": "Στείλατε {{value}} {{currency}}", + "push-outbound-transaction-body": "Από {{from}} έως {{to}}", + "quiet-days": "{{quiet-days}} ημέρες", + "quiet-hours": "{{quiet-hours}} ώρες", + "re-encrypt-key": "Ξανακρυπτογραφήστε τα κλειδιά σας", + "receive": "Λήψη", + "receive-transaction": "Λήψη συναλλαγής", + "recent": "Πρόσφατα", + "recent-empty": "Οι διευθύνσεις που χρησιμοποιήθηκαν πρόσφατα θα εμφανίζονται εδώ", + "recent-recipients": "Επαφές", + "recently-used-stickers": "Τα αυτοκόλλητα που χρησιμοποιήθηκαν πρόσφατα θα εμφανίζονται εδώ", + "recipient": "Παραλήπτης", + "recipient-code": "Εισαγάγετε τη διεύθυνση παραλήπτη", + "recipient-code-placeholder": "0x... ή username.domain.eth", + "recover": "Ανάκτηση", + "recover-key": "Πρόσβαση στα υπάρχοντα κλειδιά", + "recover-keycard-multiaccount-not-supported": "Τα κλειδιά για αυτόν τον λογαριασμό υπάρχουν ήδη και δεν μπορούν να προστεθούν ξανά. Εάν έχετε χάσει τον κωδικό πρόσβασής σας ή την Keycard, απεγκαταστήστε την εφαρμογή, εγκαταστήστε ξανά και αποκτήστε πρόσβαση στα κλειδιά σας εισάγοντας τη φράση seed σας", + "recover-with-keycard": "Ανάκτηση με Keycard", + "recovering-key": "Πρόσβαση σε κλειδιά...", + "recovery-confirm-phrase": "Επιβεβαιώστε τη φράση seed", + "recovery-phrase": "Φράση Seed", + "recovery-success-text": "Θα πρέπει να δημιουργήσετε έναν νέο κωδικό ή κωδικό πρόσβασης για να κρυπτογραφήσετε ξανά τα κλειδιά σας", + "recovery-typo-dialog-description": "Λάβετε υπόψη ότι η φράση seed σας πρέπει να χρησιμοποιεί τις ίδιες ακριβώς λέξεις και σειρά όπως την λάβατε", + "recovery-typo-dialog-title": "Είναι σωστή η φράση seed;", + "redeem-amount": "{{quantity}} διαθέσιμα μπόνους", + "redeem-now": "Εξαργυρώστε τώρα", + "redeem-success": "Εξαργυρώστε το μπόνους επιτυχίας!", + "refresh": "Ανανέωση", + "registered": "εγγεγραμμένος", + "remember-me": "Θυμήσου με", + "remind-me-later": "Δείξε μου ξανά", + "remove": "Κατάργηση", + "remove-favourite": "Κατάργηση αγαπημένου", + "remove-from-chat": "Κατάργηση από τη συνομιλία", + "remove-from-contacts": "Κατάργηση από επαφές", + "remove-from-contacts-text": "Αφαιρώντας έναν χρήστη από τη λίστα επαφών σας, δεν αποκρύπτετε τη διεύθυνση του πορτοφολιού σας από αυτόν", + "remove-group": "Κατάργηση ομάδας", + "remove-network": "Κατάργηση δικτύου", + "remove-token": "Κατάργηση token", + "removed": "αφαιρέθηκε", + "repeat-pin": "Επαναλάβετε νέο 6ψήφιο κωδικό πρόσβασης", + "report-bug-email-template": "1. Περιγραφή ζητήματος\n (Περιγράψτε τη λειτουργία που θα σας άρεσε ή συνοψίστε εν συντομία το σφάλμα και τι κάνατε, τι περιμένατε να συμβεί και τι πραγματικά συμβαίνει. Ενότητες παρακάτω) \n\n\n 2. Βήματα αναπαραγωγής\n (Περιγράψτε πώς μπορούμε να επαναλάβουμε το σφάλμα βήμα προς βήμα.)\n -Ανοιγμα Status\n -...\n - Βήμα 3 κ.λπ. \n\n\n 3. Αναμενόμενη συμπεριφορά\n (Περιγράψτε τι περιμένατε να συμβεί.) \n\n\n 4. Πραγματική συμπεριφορά\n (Περιγράψτε τι πραγματικά συνέβη.) \n\n\n 5. Επισυνάψτε στιγμιότυπα οθόνης που μπορούν να επιδείξουν το πρόβλημα, παρακαλώ\n", + "request-feature": "Ζητήστε ένα χαρακτηριστικό", + "request-membership": "Αίτηση ιδιότητας μέλους", + "request-pending": "Το αίτημά σας είναι σε αναμονή…", + "request-transaction": "Αίτημα συναλλαγής", + "required-field": "Υποχρεωτικό πεδίο", + "resend-message": "Στείλτε ξανά", + "reset-card": "Επαναφορά κάρτας", + "reset-card-description": "Αυτή η λειτουργία θα επαναφέρει την κάρτα στην αρχική κατάσταση. Θα διαγράψει όλα τα δεδομένα της κάρτας, συμπεριλαμβανομένων των ιδιωτικών κλειδιών. Η λειτουργία δεν είναι αναστρέψιμη.", + "retry": "Επανάληψη", + "revoke-access": "Ανάκληση πρόσβασης", + "rpc-url": "Διεύθυνση URL RPC", + "rpc-usage-copy": "Αντιγραφή", + "rpc-usage-get-stats": "Ανανέωση", + "save": "Αποθήκευση", + "save-password": "Αποθήκευση κωδικού", + "save-password-unavailable": "Ορίστε τον κωδικό πρόσβασης της συσκευής για να αποθηκεύσετε τον κωδικό σας", + "save-password-unavailable-android": "Η αποθήκευση κωδικού πρόσβασης δεν είναι διαθέσιμη: η συσκευή σας ενδέχεται να είναι rooted ή να μην διαθέτει τις απαραίτητες λειτουργίες ασφαλείας.", + "scan-qr": "Σάρωση κωδικού QR", + "scan-qr-code": "Σάρωση κωδικού QR με διεύθυνση πορτοφολιού", + "scan-tokens": "Σάρωση tokens", + "search": "Αναζήτηση", + "search-no-chat-found": "Δεν υπάρχουν αποτελέσματα αναζήτησης. Μήπως εννοείτε", + "secret-keys-confirmation-text": "Θα τους χρειαστείτε για να συνεχίσετε να χρησιμοποιείτε την Keycard σας σε περίπτωση που χάσετε το τηλέφωνό σας.", + "secret-keys-confirmation-title": "Σημειώσατε τους κωδικούς;", + "security": "Ασφάλεια", + "see-details": "Δείτε λεπτομέρειες", + "see-it-again": "ΝΑ ΤΟ ΞΑΝΑΔΩ", + "seed-phrase-content": "Ένα σύνολο λέξεων φιλικών προς ανάγνωση, που επιλέχθηκαν τυχαία από την καθιερωμένη λίστα BIP39 και χρησιμοποιούνται για ανάκτηση ή πρόσβαση στον Ethereum λογαριασμό σας σε άλλα πορτοφόλια και συσκευές. Αναφέρεται επίσης ως «μνημονική φράση», «φράση ανάκτησης» ή «αντίγραφο ασφαλείας πορτοφολιού» σε ολόκληρο το οικοσύστημα crypto. Οι περισσότερες εφαρμογές crypto χρησιμοποιούν το ίδιο πρότυπο για τη δημιουργία λογαριασμών.", + "seed-phrase-placeholder": "Φράση Seed", + "seed-phrase-title": "Φράση Seed", + "select": "Επιλογή", + "select-account": "Επιλογή λογαριασμού", + "select-account-dapp": "Επιλέξτε τον λογαριασμό που θέλετε να χρησιμοποιήσετε με τα Dapps", + "select-account-first": "Επιλέξτε πρώτα έναν λογαριασμό", + "select-chat": "Επιλέξτε συνομιλία για να ξεκινήσετε την αποστολή μηνυμάτων", + "selected": "Επιλεγμένο", + "send-logs": "Αναφορά σφάλματος", + "send-logs-to": "Αναφορά σφάλματος στο {{email}}", + "send-message": "Αποστολή μηνύματος", + "send-push-notifications-description": "Όταν απενεργοποιηθεί, το άτομο που λαμβάνει τα μηνύματά σας δεν θα ειδοποιηθεί για την άφιξή τους", + "send-request": "Αποστολή αιτήματος", + "send-request-amount": "Ποσό", + "send-request-amount-max-decimals": "Ο μέγιστος αριθμός δεκαδικών είναι {{asset-decimals}}", + "send-request-unknown-token": "Άγνωστο token - {{asset}}", + "send-sending-to": "στον {{recipient-name}}", + "send-transaction": "Αποστολή συναλλαγής", + "sending": "Αποστολή", + "sent-at": "Στάλθηκε στις", + "server": "Διακομιστής", + "set-a-topic": "Δημιουργία θέματος", + "set-currency": "Ορίστε νόμισμα", + "set-custom-fee": "Ορίστε προσαρμοσμένη χρέωση", + "set-dapp-access-permissions": "Ορίστε δικαιώματα πρόσβασης DApp", + "set-max": "Ορισμός μέγιστου", + "settings": "Ρυθμίσεις", + "share": "Κοινοποίηση", + "share-address": "Κοινοποίηση διεύθυνσης", + "share-chat": "Κοινοποίηση συνομιλίας", + "share-contact-code": "Κοινοποίηση του κλειδιού συνομιλίας μου", + "share-dapp-text": "Δείτε αυτό το DApp που χρησιμοποιώ στο Status: {{link}}", + "share-link": "Κοινοποίηση συνδέσμου", + "share-my-profile": "Κοινοποίηση του προφίλ μου", + "share-profile": "Κοινή χρήση προφίλ", + "share-profile-link": "Κοινή χρήση συνδέσμου προφίλ", + "share-public-chat-text": "Δείτε αυτήν τη δημόσια συνομιλία στην εφαρμογή του Status: {{link}}", + "shared": "Κοινόχρηστο", + "sharing-copied-to-clipboard": "Αντιγράφηκε", + "sharing-copy-to-clipboard": "Αντιγραφή", + "sharing-share": "Κοινοποίηση", + "show-less": "Δείξε λιγότερα", + "show-more": "Δείτε περισσότερα", + "show-notifications": "Εμφάνιση ειδοποιήσεων", + "show-qr": "Εμφάνιση κωδικού QR", + "show-transaction-data": "Εμφάνιση δεδομένων συναλλαγής", + "sign-and-send": "Υπογραφή και αποστολή", + "sign-anyway": "Υπογράψτε ούτως ή άλλως", + "sign-in": "Ξεκλείδωμα", + "sign-message": "Υπογραφή μηνύματος", + "sign-out": "Αποσύνδεση", + "sign-request-failed": "Δεν ήταν δυνατή η υπογραφή μηνύματος", + "sign-with": "Συνδεθείτε με", + "sign-with-password": "Υπογράψτε με κωδικό πρόσβασης", + "sign-you-in": "Σύνδεση…", + "signing": "Υπογραφή", + "signing-a-message": "Υπογραφή μηνύματος", + "signing-phrase": "Φράση υπογραφής", + "something-went-wrong": "Κάτι πήγε στραβά", + "soon": "Σύντομα", + "specify-address": "Καθορισμός διεύθυνσης", + "specify-name": "Καθορίστε ένα όνομα", + "specify-network-id": "Καθορίστε το αναγνωριστικό δικτύου", + "specify-rpc-url": "Καθορίστε μια διεύθυνση URL RPC", + "specify-server-public-key": "Εισαγάγετε δημόσιο κλειδί διακομιστή", + "start-chat": "Έναρξη συνομιλίας", + "start-conversation": "Έναρξη συνομιλίας", + "start-group-chat": "Έναρξη ομαδικής συνομιλίας", + "start-new-chat": "Έναρξη νέας συνομιλίας", + "starter-pack-received": "Το Πακέτο Εκκίνησης ελήφθη", + "starter-pack-received-description": "Εδώ είναι μερικά crypto για να ξεκινήσετε! Χρησιμοποιήστε τα για να πάρετε αυτοκόλλητα, ένα όνομα ENS και να δοκιμάσετε dapps", + "status": "Status", + "status-confirmed": "Επιβεβαιωμένα", + "status-hardwallet": "Status hardwallet", + "status-keycard": "Status Keycard", + "status-not-sent-click": "Δεν έχει επιβεβαιωθεί. Κάντε κλικ για επιλογές", + "status-not-sent-tap": "Δεν έχει επιβεβαιωθεί. Πατήστε για επιλογές", + "status-pending": "Εκκρεμούν", + "status-sent": "Απεσταλμένα", + "status-tx-not-found": "Το TX δεν βρέθηκε", + "status-updates-descr": "Οι ενημερώσεις κατάστασης θα εμφανίζονται εδώ. Προσθέστε το προφίλ ως επαφή για να λαμβάνετε ενημερώσεις στο χρονολόγιo σας.", + "statuses-descr": "Μοιραστείτε ό,τι έχετε στο μυαλό σας και παραμείνετε ενημερωμένοι με τις επαφές σας", + "statuses-my-status-descr": "Μοιραστείτε ό,τι έχετε στο μυαλό σας. Όποιος επισκέπτεται το προφίλ σας θα μπορεί να δει την κατάστασή σας. Τα άτομα που σας προσθέτουν ως επαφή θα λαμβάνουν τις ενημερώσεις σας στο χρονολόγιo τους", + "step-i-of-n": "Βήμα {{step}} από {{number}}", + "sticker-market": "Αγορά αυτοκόλλητων", + "storage": "Αποθήκευση", + "submit": "Υποβολή", + "submit-bug": "Υποβάλετε ένα σφάλμα", + "success": "Επιτυχία", + "symbol": "Σύμβολο", + "sync-all-devices": "Συγχρονισμός όλων των συσκευών", + "sync-in-progress": "Συγχρονισμός...", + "sync-settings": "Ρυθμίσεις συγχρονισμού", + "sync-synced": "Σε συγχρονισμό", + "syncing-devices": "Συγχρονισμός...", + "system": "Σύστημα", + "tabs": "Καρτέλες", + "tag-was-lost": "Η ετικέτα χάθηκε", + "tap-card-again": "Βάλτε ξανά την κάρτα στο πίσω μέρος του τηλεφώνου σας", + "test-networks": "Δοκιμαστικά δίκτυα", + "text-input-disabled": "Παρακαλώ περιμένετε μια στιγμή...", + "this-device": "Αυτή η συσκευή", + "this-device-desc": "Τα κλειδιά σας θα κρυπτογραφηθούν και θα αποθηκευτούν με ασφάλεια στη συσκευή σας", + "this-is-you-signing": "Αυτή είναι η φράση υπογραφής σας", + "this-will-take-few-seconds": "Αυτό θα διαρκέσει μερικά δευτερόλεπτα", + "three-words-description": "Θα πρέπει να δείτε αυτές τις 3 λέξεις πριν υπογράψετε κάθε συναλλαγή", + "three-words-description-2": "Εάν δείτε διαφορετικό συνδυασμό, ακυρώστε τη συναλλαγή και αποσυνδεθείτε", + "timeline": "Χρονοδιάγραμμα", + "to": "Προς", + "to-block": "Αποκλεισμός", + "to-enable-biometric": "Για να ενεργοποιήσετε το {{bio-type-label}}, πρέπει να αποθηκεύσετε τον κωδικό πρόσβασής σας στην οθόνη ξεκλειδώματος", + "to-encrypt-enter-password": "Για να κρυπτογραφήσετε τον λογαριασμό εισάγετε τον κωδικό πρόσβασης σας", + "to-see-this-message": "Για να δείτε αυτό το μήνυμα,", + "token-auto-validate-decimals-error": "Λανθασμένα δεκαδικά ψηφία για το token {{symbol}} στη διεύθυνση {{address}} - ορίστηκε ως {{expected}} αλλά εντοπίστηκε ως {{actual}}", + "token-auto-validate-name-error": "Λάθος όνομα για το token {{symbol}} στη διεύθυνση {{address}} - ορίστηκε ως {{expected}} αλλά εντοπίστηκε ως {{actual}}", + "token-auto-validate-symbol-error": "Λάθος σύμβολο για το token {{symbol}} στη διεύθυνση {{address}} - ορίστηκε ως {{expected}} αλλά εντοπίστηκε ως {{actual}}", + "token-details": "Λεπτομέρειες token", + "topic-name-error": "Χρησιμοποιήστε μόνο πεζά γράμματα (a έως z), αριθμούς και παύλες (-). Μην χρησιμοποιείτε τα κλειδιά συνομιλίας", + "transaction": "Συναλλαγή", + "transaction-data": "Δεδομένα συναλλαγών", + "transaction-declined": "Η συναλλαγή απορρίφθηκε", + "transaction-description": "Θεωρήστε ότι ολοκληρώθηκε μετά από 12 επιβεβαιώσεις στο δίκτυο.", + "transaction-details": "Λεπτομέρειες Συναλλαγής", + "transaction-failed": "Η συναλλαγή απέτυχε", + "transaction-history": "Ιστορικό συναλλαγών", + "transaction-request": "Αίτημα Συναλλαγής", + "transaction-sent": "Η συναλλαγή εστάλη", + "transaction-signed": "Η συναλλαγή έχει υπογραφεί με επιτυχία", + "transactions": "Συναλλαγές", + "transactions-filter-select-all": "Επιλογή όλων", + "transactions-filter-title": "Ιστορικό φίλτρου", + "transactions-history": "Ιστορικό συναλλαγών", + "transactions-history-empty": "Δεν υπάρχουν ακόμη συναλλαγές στο ιστορικό σας", + "transactions-history-loading": "Φόρτωση ιστορικού συναλλαγών. Αυτό μπορεί να διαρκέσει λίγο.", + "transactions-load-more": "Φόρτωσε περισσότερα", + "transactions-sign": "Υπογραφή", + "tribute-required-by-multiaccount": "{{multiaccount-name}} απαιτεί το SNT για να ξεκινήσει μια συζήτηση.", + "tribute-state-paid": "Το αφιέρωμα πληρώθηκε", + "tribute-state-pending": "Το αφιέρωμα εκκρεμεί", + "tribute-state-required": "Απαιτείται αφιέρωμα {{snt-amount}} SNT", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Προσθέστε φίλους ως επαφή για να επιτρέψετε συνομιλίες χωρίς πληρωμή αφιερώματος.", + "tribute-to-talk-are-you-friends": "Είστε φίλοι?", + "tribute-to-talk-ask-to-be-added": "Ζητήστε να προστεθεί ως επαφή", + "tribute-to-talk-contact-received-your-tribute": " έλαβε το αφιέρωμά σας. Τώρα μπορείτε να συνομιλήσετε με ασφάλεια μεταξύ σας.", + "tribute-to-talk-desc": "Δημιουργήστε έσοδα από την προσοχή σας, απαιτώντας SNT για την έναρξη μιας συνομιλίας από νέα άτομα", + "tribute-to-talk-disabled": "Το Tribute to Talk απενεργοποιήθηκε", + "tribute-to-talk-disabled-note": "Από τώρα και στο εξής, τα νέα άτομα μπορούν να ξεκινήσουν μια συνομιλία μαζί σας χωρίς να στείλουν SNT.", + "tribute-to-talk-enabled": "Έχετε ενεργοποιήσει το Tribute to Talk.", + "tribute-to-talk-finish-desc": "Από τώρα και στο εξής, θα λαμβάνετε μόνο συνομιλίες από επαφές και άτομα που πληρώνουν ", + "tribute-to-talk-learn-more-1": "Ο χρόνος και η προσοχή σας είναι τα πιο πολύτιμα περιουσιακά σας στοιχεία. Το Tribute to Talk σας επιτρέπει να ορίσετε ένα ποσό SNT που απαιτείται για να ξεκινήσουν νέα άτομα μια συνομιλία μαζί σας.", + "tribute-to-talk-learn-more-2": "Όποιος δεν περιλαμβάνεται στη λίστα επαφών σας θα κληθεί να πληρώσει και μπορείτε να απαντήσετε μόλις το κάνει.", + "tribute-to-talk-learn-more-3": "Μπορείτε πάντα να στείλετε τα χρήματα πίσω, αλλά για να διασφαλίσετε ότι οι φίλοι σας μπορούν να επικοινωνήσουν μαζί σας ελεύθερα, προσθέστε τα πρώτα ως επαφή.", + "tribute-to-talk-paywall-learn-more-1": "Ο χρόνος και η προσοχή μας είναι τα πιο πολύτιμα περιουσιακά στοιχεία μας. Το Tribute to Talk σάς επιτρέπει να επικοινωνείτε με νέα άτομα με αντάλλαγμα μια πληρωμή SNT.", + "tribute-to-talk-paywall-learn-more-2": "Για να ξεκινήσετε μια συνομιλία με κάποιον που έχει ένα σύνολο αφιερωμάτων, απλώς πληρώστε το απαιτούμενο SNT και θα προστεθείτε ως επαφή.", + "tribute-to-talk-paywall-learn-more-3": "Εάν τους γνωρίζετε, μπορείτε να μοιραστείτε το προφίλ σας εκτός του Status για να προστεθούν δωρεάν.", + "tribute-to-talk-pending": "Εκκρεμεί επιβεβαίωση αφιερώματος", + "tribute-to-talk-pending-note": "Εκκρεμεί επιβεβαίωση της συναλλαγής αφιερώματος στο δίκτυο. Μπορείτε να ελέγξετε την κατάστασή του στο ιστορικό συναλλαγών", + "tribute-to-talk-removing-note": "Η κατάργηση του Tribute to Talk θα επιτρέψει στα νέα άτομα να ξεκινήσουν μια συζήτηση χωρίς να στείλουν SNT. Απαιτείται μια συναλλαγή.", + "tribute-to-talk-set-snt-amount": "Ορισμός του ποσού SNT που απαιτείται για την έναρξη μιας συνομιλίας από νέα άτομα", + "tribute-to-talk-signing": "Αναμονή για υπογραφή συναλλαγής", + "tribute-to-talk-transaction-failed-note": "Η συναλλαγή απέτυχε και οι ρυθμίσεις Tribute to Talk δεν έχουν αλλάξει", + "tribute-to-talk-tribute-received1": "Το αφιέρωμα ελήφθη. Εσύ και ", + "tribute-to-talk-tribute-received2": " είναι πλέον επαφές και μπορούν να συνομιλήσουν με ασφάλεια μεταξύ τους.", + "tribute-to-talk-you-require-snt": "Χρειάζεστε SNT για να ξεκινήσετε συνομιλία με νέα άτομα.", + "try-again": "Προσπάθησε ξανά", + "try-keeping-the-card-still": "Προσπαθήστε να κρατήσετε την κάρτα ακίνητη", + "turn-nfc-description": "Το NFC είναι απενεργοποιημένο στη συσκευή σας. Μπορείτε να το ενεργοποιήσετε στις ρυθμίσεις", + "turn-nfc-on": "Ενεργοποιήστε το NFC για να συνεχίσετε", + "tx-fail-description1": "Αυτή η συναλλαγή είναι πιθανό να αποτύχει. Υπογράψτε με δική σας ευθύνη χρησιμοποιώντας προσαρμοσμένη χρέωση δικτύου.", + "tx-fail-description2": "Αυτή η συναλλαγή είναι πιθανό να αποτύχει. Ορίστε μια προσαρμοσμένη χρέωση δικτύου για να υπογράψετε με δική σας ευθύνη.", + "type": "Πληκτρολογήστε", + "type-a-message": "Μήνυμα", + "ulc-enabled": "Το ULC ενεργοποιήθηκε", + "unable-to-read-this-code": "Δεν είναι δυνατή η ανάγνωση αυτού του κωδικού", + "unblock": "Ξεμπλοκάρετε", + "unblock-contact": "Κατάργηση αποκλεισμού αυτού του χρήστη", + "unknown-status-go-error": "Άγνωστο σφάλμα status-go", + "unlock": "Ξεκλείδωμα", + "unmute": "Κατάργηση σίγασης", + "unpair-card": "Αποσύνδεση κάρτας", + "unpair-card-confirmation": "Αυτή η λειτουργία θα αποσυνδέσει την κάρτα από την τρέχουσα συσκευή. Απαιτείται εξουσιοδότηση 6ψήφιου κωδικού πρόσβασης. Θέλετε να συνεχίσετε?", + "unpaired-keycard-text": "Η Keycard που πατήσατε δεν σχετίζεται με αυτό το τηλέφωνο", + "unpaired-keycard-title": "Φαίνεται ότι η κάρτα σας έχει αποσυνδεθεί", + "update": "Ενημέρωση", + "update-to-listen-audio": "Ενημερώστε την τελευταία έκδοση για να ακούσετε ένα ηχητικό μήνυμα εδώ!", + "update-to-see-image": "Ενημερώστε την τελευταία έκδοση για να δείτε μια ωραία εικόνα εδώ!", + "update-to-see-sticker": "Ενημερώστε την τελευταία έκδοση για να δείτε ένα ωραίο αυτοκόλλητο εδώ!", + "url": "Διεύθυνση URL", + "usd-currency": "Δολάριο ΗΠΑ", + "use-valid-contact-code": "Εισαγάγετε ή σαρώστε ένα έγκυρο κλειδί συνομιλίας ή όνομα χρήστη", + "validation-amount-invalid-number": "Το ποσό δεν είναι έγκυρος αριθμός", + "validation-amount-is-too-precise": "Το ποσό είναι πολύ ακριβές. Ο μέγιστος αριθμός δεκαδικών είναι {{decimals}}.", + "version": "Έκδοση εφαρμογής", + "view-cryptokitties": "Προβολή στο CryptoKitties", + "view-cryptostrikers": "Προβολή στο CryptoStrikers", + "view-details": "Δείτε λεπτομέρειες", + "view-etheremon": "Προβολή στο Etheremon", + "view-gitcoin": "Προβολή στο Gitcoin", + "view-profile": "Εμφάνιση προφίλ", + "view-signing": "Προβολή φράσης υπογραφής", + "view-superrare": "Προβολή στο SuperRare", + "waiting-for-wifi": "Δεν υπάρχει Wi-Fi, ο συγχρονισμός μηνυμάτων απενεργοποιήθηκε.", + "waiting-for-wifi-change": "Ρυθμίσεις", + "waiting-to-sign": "Αναμονή για υπογραφή συναλλαγής...", + "waku-bloom-filter-mode": "Λειτουργία φίλτρου άνθισης Waku", + "wallet": "Πορτοφόλι", + "wallet-address": "Διεύθυνση πορτοφολιού", + "wallet-asset": "Περιουσιακό στοιχείο", + "wallet-assets": "Περιουσιακά στοιχεία", + "wallet-backup-recovery-title": "Δημιουργήστε αντίγραφα ασφαλείας για τη φράση seed σας", + "wallet-choose-recipient": "Επιλέξτε Παραλήπτη", + "wallet-collectibles": "Συλλεκτικά", + "wallet-insufficient-funds": "Ανεπαρκή κεφάλαια", + "wallet-insufficient-gas": "Δεν υπάρχει αρκετό ETH για gas", + "wallet-invalid-address": "Μη έγκυρη διεύθυνση:\n {{data}}", + "wallet-invalid-address-checksum": "Σφάλμα στη διεύθυνση:\n {{data}}", + "wallet-invalid-chain-id": "Το δίκτυο δεν ταιριάζει:\n {{data}} αλλά η τρέχουσα αλυσίδα είναι {{chain}}", + "wallet-key-content": "Δεκαεξαδική διεύθυνση 64 χαρακτήρων με βάση την προδιαγραφή του Ethereum και ξεκινώντας με 0x. Δημόσια, η διεύθυνση του λογαριασμού σας κοινοποιείται σε άλλους όταν θέλετε να λάβετε χρήματα. Αναφέρεται επίσης ως «διεύθυνση Ethereum» ή «διεύθυνση πορτοφολιού».", + "wallet-key-title": "Διεύθυνση λογαριασμού", + "wallet-manage-assets": "Διαχείριση περιουσιακών στοιχείων", + "wallet-request": "Αίτημα", + "wallet-send": "Αποστολή", + "wallet-send-min-units": "Ελάχιστο 21000 μονάδες", + "wallet-send-min-wei": "Ελάχιστο 1 wei", + "wallet-settings": "Ρυθμίσεις πορτοφολιού", + "wallet-total-value": "Συνολική αξία", + "wallet-transaction-total-fee": "Συνολικό Τέλος", + "wants-to-access-profile": "θέλει να αποκτήσει πρόσβαση στο προφίλ σας", + "warning": "Προειδοποίηση", + "warning-message": "Λυπούμαστε, περιορίζουμε την γρήγορη αποστολή πολλών μηνυμάτων για την αποτροπή ανεπιθύμητων μηνυμάτων. Δοκιμάστε ξανά σε λίγο", + "warning-sending-to-contract-descr": "Η διεύθυνση που εισαγάγατε είναι ένα έξυπνο συμβόλαιο, η αποστολή χρημάτων σε αυτήν τη διεύθυνση ενδέχεται να οδηγήσει σε απώλεια χρημάτων. Για να αλληλεπιδράσετε με ένα DApp, ανοίξτε το DApp στο Status DApp Browser.", + "watch-only": "Μόνο για παρακολούθηση", + "web-view-error": "Δεν είναι δυνατή η φόρτωση της σελίδας", + "websites": "Ιστότοποι", + "webview-camera-permission-requests": "Αιτήματα άδειας κάμερας Webview", + "webview-camera-permission-requests-subtitle": "Όταν είναι ενεργοποιημένη, ιστότοποι και dapps μπορούν να ζητήσουν να χρησιμοποιήσουν την κάμερα σας", + "welcome-blank-message": "Οι συζητήσεις σας θα εμφανιστούν εδώ. Για να ξεκινήσετε νέες συνομιλίες, πατήστε το κουμπί ⊕", + "welcome-screen-text": "Ρυθμίστε το πορτοφόλι σας, προσκαλέστε φίλους να συνομιλήσουν\n και περιηγηθείτε στα δημοφιλή dapps!", + "welcome-to-status": "Καλώς ήλθατε στο Status!", + "welcome-to-status-description": "Ρυθμίστε το πορτοφόλι crypto, προσκαλέστε φίλους για συνομιλία και περιήγηση σε αποκεντρωμένες εφαρμογές", + "whats-on-your-mind": "Τι σκέφτεστε…", + "word-count": "Καταμέτρηση λέξεων", + "word-n": "Λέξη #{{number}}", + "word-n-description": "Για να ελέγξετε αν έχετε δημιουργήσει σωστά αντίγραφα ασφαλείας της φράσης seed σας, εισαγάγετε τη λέξη #{{number}} παραπάνω.", + "words-n": { + "one": "1 λέξη", + "other": "{{count}} λέξεις" + }, + "write-down-and-store-securely": "Σημειώστε τους κωδικούς\n & αποθηκεύστε τους με ασφάλεια", + "wrong-address": "Λάθος διεύθυνση", + "wrong-card": "Λάθος κάρτα", + "wrong-card-text": "Η επιλεγμένη κάρτα δεν αντιστοιχεί στα κλειδιά που επιλέξατε", + "wrong-contract": "Λάθος συμβόλαιο", + "wrong-keycard-text": "Η Keycard που πατήσατε δεν σχετίζεται με αυτό το τηλέφωνο", + "wrong-keycard-title": "Φαίνεται ότι έχετε πατήσει \nλανθασμένη κάρτα-κλειδί", + "wrong-password": "Λάθος κωδικός", + "wrong-word": "Λάθος λέξη", + "yes": "Ναι", + "you": "εσείς", + "you-already-have-an-asset": "Έχετε ήδη ένα περιουσιακό στοιχείο {{value}}", + "you-are-all-set": "Είστε έτοιμοι!", + "you-are-all-set-description": "Εάν χάσετε το τηλέφωνό σας, μπορείτε πλέον να αποκτήσετε πρόσβαση στα χρήματά σας και στο κλειδί συνομιλίας χρησιμοποιώντας τη φράση seed σας", + "you-can-change-account": "Μπορείτε να αλλάξετε το όνομα και το χρώμα του λογαριασμού σε αυτό που θέλετε", + "you-can-choose-preview-websites": "Μπορείτε να επιλέξετε ποιοι από τους παρακάτω ιστότοπους μπορούν να κάνουν προεπισκόπηση συνδέσμου περιγραφών και εικόνων σε συζητήσεις", + "you-dont-have-contacts": "Δεν έχετε ακόμα επαφές.", + "you-dont-have-stickers": "Δεν έχετε ακόμα αυτοκόλλητα", + "you-will-need-this-code": "Θα χρειαστείτε αυτόν τον κωδικό για να ανοίξετε το Status και να υπογράψετε συναλλαγές", + "you-will-start-from-scratch": "Θα ξεκινήσετε από το μηδέν με ένα νέο σετ κλειδιών", + "your-card-is-frozen": "Η Keycard σας έχει παγώσει. Επαναφορά πρόσβασης στην κάρτα", + "your-contact-code": "Η παραχώρηση πρόσβασης επιτρέπει σε αυτό το DApp την ανάκτηση του κλειδιού συνομιλίας σας", + "your-data-belongs-to-you": "Εάν χάσετε τη φράση seed, χάνετε τα δεδομένα και τα χρήματά σας", + "your-data-belongs-to-you-description": "Εάν χάσετε την πρόσβαση, χάνοντας για παράδειγμα το τηλέφωνό σας, μπορείτε να αποκτήσετε πρόσβαση στα κλειδιά σας μόνο με τη φράση seed σας. Κανείς, εκτός από εσάς δεν έχει τη φράση seed σας. Σημειώστε τη. Κρατησε τη ασφαλη", + "your-keys": "Τα κλειδιά σας", + "your-recovery-phrase": "Η φράση seed σας", + "your-recovery-phrase-description": "Αυτή είναι η φράση seed σας. Τη χρησιμοποιείτε για να αποδείξετε ότι αυτό είναι το πορτοφόλι σας. Θα τη δείτε μόνο μία φορά! Γράψτε τη σε χαρτί και φυλάξτε τη σε ασφαλές μέρος. Θα τη χρειαστείτε εάν χάσετε ή επανεγκαταστήσετε το πορτοφόλι σας." +} diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 00000000000..04dd0f26d98 --- /dev/null +++ b/translations/en.json @@ -0,0 +1,3152 @@ +{ + "a-card-shaped-hardware-wallet": "A card-shaped hardware wallet", + "about": "About", + "about-app": "About", + "about-gas-amount": "AKA gas limit. Refers to the maximum number of computational steps (or units of gas) that a transaction can consume. It represents the complexity or amount of work required to execute a transaction or smart contract.\n\nThe gas limit is a cap on how much work the transaction can do on the blockchain. If the gas limit is set too low, the transaction may fail due to insufficient gas.", + "about-gas-price": "Gas price in blockchain refers to the fee users pay to process transactions on the network. It fluctuates based on network demand and affects how quickly transactions are confirmed.", + "about-key-storage-content": "Status will never access your private key. Be sure to backup your seed phrase. If you lose your phone it is the only way to access your keys.", + "about-key-storage-title": "About key storage", + "about-max-base-fee": "When your transaction gets included in the block, any difference between your max base fee and the actual base fee will be refunded.\n\nNote: the ETH amount shown for this value is calculated: max base fee (in GWEI) * max gas amount.", + "about-names-content": "No one can pretend to be you! You’re anonymous by default and never have to reveal your real name. You can register a custom name for a small fee.", + "about-names-title": "Names can’t be changed", + "about-nonce": "Transaction counter ensuring transactions from your account are processed in the correct order and can’t be replayed. Each new transaction increments the nonce by 1, ensuring uniqueness and preventing double-spending.\n\nIf a transaction with a lower nonce is pending, higher nonce transactions will remain in the queue until the earlier one is confirmed.", + "about-priority-fee": "AKA miner tip. A voluntary fee you can add to incentivise miners or validators to prioritise your transaction.\n\nThe higher the tip, the faster your transaction is likely to be processed, especially during periods of higher network congestion.\n\nNote: the ETH amount shown for this value is calculated: Priority fee (in GWEI) * Max gas amount", + "about-sharing-data": "About sharing data", + "accent-colour": "Accent colour", + "accent-colour-updated": "Accent colour updated", + "accept": "Accept", + "accept-and-add": "Accept and add", + "accept-and-continue": "Accept and continue", + "accept-and-share-address": "Accept and share address", + "accept-community-rules": "I agree with the community rules", + "accept-new-chats-from": "Accept new chats from", + "accept-status-tos-prefix": "By proceeding you agree with Status", + "accepted": "Accepted", + "access-existing-keys": "Access existing keys", + "access-key": "Access key", + "account": "Account", + "account-added": "Account added", + "account-already-exist-error": "The recovery phrase you entered is linked to a key pair already added to this device.", + "account-color": "Account color", + "account-content": "You can compare accounts in Status to bank accounts. Like a bank account, an account typically has an address and a balance; You use this account to transact on Ethereum. You can have multiple accounts in your wallet. All accessed by unlocking Status.", + "account-created": "{{name}} created", + "account-exists-title": "Account already exists", + "account-info": "Account info", + "account-is-used": "The account is being used with Dapps in the browser.", + "account-name": "Account name", + "account-origin-desc": "To create a new account, you can generate a key pair in the app or import an existing account.", + "account-origin-header": "Account origin", + "account-removed": "Account has been removed", + "account-settings": "Account settings", + "account-title": "Account", + "accounts": "Accounts", + "accounts-count": "{{count}} accounts", + "action-logs": "Logs of actions, including button presses and screen visits", + "actions": "Actions", + "activate": "Activate", + "active": "active", + "active-members": "Active members", + "active-networks": "Active networks", + "active-online": "Online", + "active-unknown": "Unknown", + "activity": "Activity", + "add": "Add", + "add-a-contact": "Add a contact", + "add-a-watch-account": "Add a watch-only address", + "add-account": "Add account", + "add-account-description": "Create new or import existing account", + "add-account-incorrect-password": "Password seems to be incorrect. Enter the password you use to unlock the app.", + "add-address": "Add address", + "add-address-to-save-description": "Paste, scan or type ETH address or ENS name", + "add-address-to-watch": "Add address to watch", + "add-address-to-watch-description": "Watch a public address or ENS name", + "add-an-account": "Add an account", + "add-assets": "Add assets", + "add-bootnode": "Add bootnode", + "add-contact": "Add contact", + "add-custom-token": "Add custom token", + "add-display-name-and-picture": "Add an optional display name and profile picture so others can easily recognize you", + "add-favourite": "Add favourite", + "add-mailserver": "Add Status node", + "add-me-to-your-contacts": "Please add me to your contacts", + "add-members": "Add members", + "add-network-preferences": "Add network preferences", + "add-networks-token-can-be-sent-to": "Add networks {{token-symbol}} can be sent to", + "add-new-contact": "Add new contact", + "add-nickname": "Add a nickname (optional)", + "add-nickname-title": "Add nickname", + "add-node": "Add node", + "add-preferences": "Add preferences", + "add-private-key-account": "Add account from private key", + "add-seed-account": "Add account with a seed phrase", + "add-text": "Add text", + "add-to-contacts": "Add to contacts", + "add-to-contacts-text": "By adding a user to your contact list, you share your wallet address", + "add-to-favourites": "Add to favourites", + "add-token": "Add {{token}}", + "add-watch-account": "Add a watch-only account", + "add-watched-address": "Add watched address", + "added": "added", + "added-to-group-chat": "Added to group chat", + "added-you-to": "added you to", + "address": "Address", + "address-activity": "This address has activity", + "address-already-in-use": "Address already being used", + "address-copied": "Address copied", + "address-count": { + "one": "1 address", + "other": "{{count}} addresses" + }, + "address-edited": "Address edited", + "address-name": "Address name", + "address-no-activity": "This address has no activity", + "address-or-ens-name": "Address or ENS name", + "address-placeholder": "0x123abc... or bob.eth", + "address-received": "Address received", + "address-request-accepted": "Address request accepted", + "address-requested": "Address requested", + "address-saved": "Address saved", + "address-to-share": "Addresses to share", + "addresses-dont-contain-tokens-needed": "These addresses don’t contain tokens needed to join", + "addresses-for-permissions": "Addresses for permissions", + "admin": "Admin", + "advanced": "Advanced", + "advanced-settings": "Advanced settings", + "advertiser-description": "You’ve discovered Status thanks to a partner. Do you mind if Status checks your IP address once so they get rewarded? This information will not be used for anything else and it will be removed completely after 7 days.", + "advertiser-starter-pack-accept": "Accept", + "advertiser-starter-pack-decline": "Decline", + "advertiser-starter-pack-description": "Here’s some crypto to get you started! Use it to get stickers, an ENS name and try dapps", + "advertiser-starter-pack-title": "Starter Pack", + "advertiser-title": "Privacy by default", + "agree": "Agree", + "agree-by-continuing": "By continuing you agree\n to our ", + "airdrop-addresses": "Address for airdrops", + "album-images-count": "{{album-images-count}} photos", + "all": "All", + "all-addresses": "All addresses", + "all-changes-will-be-discarded": "All changes in shared addresses for permissions will be discarded.", + "all-connections": "All Connections", + "all-currencies": "All currencies", + "all-messages": "All messages", + "all-networks": "All networks", + "all-slots-occupied": "All slots occupied", + "all-time": "All time", + "allow": "Allow", + "allow-and-send": "Allow and send", + "allow-community-mentions-notifications-description": "Mentions and replies", + "allow-mention-notifications": "Show @ mentions", + "allow-messages-and-communities-notifications": "Messages and communities", + "allow-messages-and-communities-notifications-description": "1-1 and group messages, replies and mentions in communities. Possible high battery usage.", + "allow-messenger-notifications": "Show messenger notifications", + "allow-new-contact-requests": "Allow new contact requests", + "allow-news-notifications": "News from Status", + "allow-news-notifications-description": "Uses Google services, and works even when the app is closed.", + "allowing-authorizes-this-dapp": "Allowing authorizes this DApp to retrieve your wallet address and enable Web3", + "alphabetically": "Alphabetically", + "already-have-asset": "You already have this asset", + "already-logged-in-on-the-other-device": "Already logged in on the other device", + "amount": "Amount", + "amount-missing-keypairs": "{{amount} missing key pairs", + "amount-with-currency-symbol": "{{symbol}} {{amount}}", + "and": "and", + "and-go-to": "and go to", + "anyone": "Anyone", + "app-commit": "App commit", + "app-connections": "App connections", + "app-interactions": "App interactions", + "appearance": "Appearance", + "apply": "Apply", + "apply-changes": "Apply changes", + "approve": "Approve", + "approve-amount-symbol": "Approve {{token-amount}} {{token-symbol}}", + "approve-limit": "Approve limit", + "approve-token": "Approve token", + "approve-token-contract-desc": "Approving a token with a contract allows it to spend your token balance. If you feel that a project is untrustworthy, don’t approve the token with them, or approve only the amount you will use with them.", + "approved-amount-symbol": "Approved {{token-amount}} {{token-symbol}}", + "approving-amount-symbol": "Approving {{token-amount}} {{token-symbol}}...", + "apr": "Apr", + "are-not-allowed": "{{check} are not allowed", + "are-you-sure": "Are you sure?", + "are-you-sure-description": "You will not be able to see the whole seed phrase again", + "are-you-sure-to-cancel": "Are you sure you want to cancel?", + "are-you-sure?": "Are you sure?", + "ask-in-status": "Ask a question or report a bug", + "assets": "Assets", + "at": "at", + "at-least-one-network-must-be-activated": "At least 1 network must be activated", + "attribution-received": "{{attrib}} out of {{max}} bonuses received", + "audio": "Audio", + "audio-message": "Audio message", + "audio-recorder": "Recorder", + "audio-recorder-error": "Recorder error", + "audio-recorder-max-ms-reached": "Maximum recording time reached", + "audio-recorder-permissions-error": "You have to give permission to send audio messages", + "aug": "Aug", + "authorise-with-password": "Authorise with password", + "authorize": "Authorize", + "available": "Available", + "available-participants": { + "one": "You can select one more participant", + "other": "You can select {{count}} more participants" + }, + "back": "Back", + "back-up": "Back up", + "back-up-seed-phrase": "Backup recovery phrase", + "back-up-your-seed-phrase": "Backup your recovery phrase", + "backing-up": "Backing up...", + "backup-disabled": "Disabled", + "backup-empty-keycard-only": "You can only back up to an empty Keycard.", + "backup-enabled": "Enabled", + "backup-keycard": "Backup Keycard", + "backup-keycard-created": "Backup Keycard successfully created", + "backup-keycard-instructions": "You’ll need an empty Keycard to create backup. You’ll be able to create a different PIN for backup Keycard. It might make sense to mark your Keycards, so you don’t mix them up.", + "backup-recovery-phrase": "Backup recovery phrase", + "backup-recovery-phrase-description": "Save in a secure place that only you control, these 12 words give access to all of your funds.", + "backup-settings": "Backup settings", + "backup-step-1": "Find pen and paper", + "backup-step-2": "Write down your recovery phrase", + "backup-step-3": "Find a place to store it", + "backup-step-4": "I know I can only see it once", + "backup-through-waku": "Backup through waku", + "bad-fees-description": "Your priority fee is below our suggested parameters.", + "balance": "Balance", + "base-chain-available-info": "Base chain is now on Status - view your assets, interact with dApps and swap tokens", + "be-safe-with-secure-cold-wallet": "Be safe with secure cold wallet", + "begin-set-up": "Begin setup", + "below-base-fee": "max fee below base fee", + "beta": "Beta", + "bio": "Bio", + "bio-added": "Bio added", + "bio-is-too-long": "Bio is too long", + "bio-updated": "Bio updated", + "biometric": "Biometric", + "biometric-auth-android-sensor-desc": "Touch sensor", + "biometric-auth-android-sensor-error-desc": "Failed", + "biometric-auth-android-title": "Authentication Required", + "biometric-auth-confirm-logout": "Relogin", + "biometric-auth-confirm-message": "Biometric authentication is required to continue, if not possible please unlock your keys with your password or passcode", + "biometric-auth-confirm-title": "You must authenticate!", + "biometric-auth-confirm-try-again": "Try again", + "biometric-auth-error": "Unable perform biometric authentication ({{code}})", + "biometric-auth-login-error-title": "Biometric authentication error", + "biometric-auth-login-ios-fallback-label": "Enter Password", + "biometric-auth-reason-login": "Login in Status", + "biometric-auth-reason-verify": "Verify authentication", + "biometric-disable-bioauth": "disable {{bio-type-label}}", + "biometric-disable-password-description": "If you disable this, you will also ", + "biometric-disable-password-title": "Disable password saving", + "biometric-enable": "If you don't want to enter your password each time to access the app, enable {{bio-type-label}} sign in", + "biometric-enable-button": "Enable {{bio-type-label}}", + "biometric-enable-keycard": "If you don't want to use your Keycard each time to access the app, enable {{bio-type-label}} sign in", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Fingerprint", + "biometric-secure-with": "Secure with {{bio-type-label}}", + "biometric-too-many-attempts": "Too many attempts. Enter the password manually or try again after some time", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "BIP39 password", + "biu": "BIU", + "blank-contacts-text": "Your contacts will be here", + "blank-keycard-text": "You can proceed with your keycard once you've generated your keys and name", + "blank-keycard-title": "Looks like you’ve tapped \na blank keycard", + "blank-messages-text": "Your messages will be here", + "block": "Block", + "block-contact": "Block this user", + "block-contact-details": "Blocking will delete this user's previous messages and stop new ones from reaching you", + "block-user": "Block user", + "block-user-title-message": "You will not see {{username}}'s messages, but {{username}} still can see your messages in mutual group chats and communities. {{username}} will be unable to message you.", + "block-user?": "Block User?", + "blocked-users": "Blocked users", + "blocking-a-user-message": "Blocking a user purges the database of all messages that you’ve previously received from {{username}} in all contexts.", + "bold": "Bold", + "bootnode-address": "Bootnode address", + "bootnode-details": "Bootnode details", + "bootnode-format": "enode://{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes enabled", + "bootnodes-settings": "Bootnodes settings", + "bridge": "Bridge", + "bridge-from": "Bridge from {{bridge-name}}", + "bridge-to": "Bridge {{name}} to", + "bridged-to": "Bridged to {{network}}", + "browsed-websites": "Browser history will appear here", + "browser": "Browser", + "browser-not-secure": "Connection is not secure! Do not sign transactions or send personal data on this site.", + "browser-secure": "Connection is secure. Make sure you really trust this site before signing transactions or entering personal data.", + "browsers": "Browsers", + "browsing-cancel": "Cancel", + "browsing-open-in-android-web-browser": "Open in Android", + "browsing-open-in-ios-web-browser": "Open in iOS", + "browsing-open-in-status": "Open in Status", + "browsing-site-blocked-description1": "We detected potential malicious activity from this address. To protect you and your wallet, we're preventing further navigation.\n\nIf you think this is an error, let us know in the ", + "browsing-site-blocked-description2": " public chat.", + "browsing-site-blocked-go-back": "Go back", + "browsing-site-blocked-title": "This site is blocked", + "browsing-title": "Browse", + "bsc": "BNB Smart Chain", + "bsc-collectibles-not-supported": "Displaying collectibles on BNB Smart Chain is not currently supported by Status.", + "bug-report": "Report a bug", + "bug-report-description": "* Description", + "bug-report-description-placeholder": "Required, can't be empty", + "bug-report-steps": "Steps to reproduce", + "bug-report-steps-placeholder": "- open app\n- do something\n- and then somethig else...", + "bug-report-submit-email": "Submit by email with logs archive", + "bug-report-submit-gh-issue": "Submit a GitHub issue without logs", + "bug-report-too-short-description": "Description is too short", + "build-yourself": "To use the app without these Terms of Use, you can build your own version", + "buy": "Buy", + "buy-crypto": "Buy crypto", + "buy-crypto-choose-a-service": "Choose a service you'd like to use to buy crypto", + "buy-crypto-description": "Find a dapp to buy crypto now", + "buy-crypto-leaving": "You are leaving Status and entering a third party website to complete your purchase", + "buy-crypto-title": "Looks like your wallet is empty", + "buy-ethereum": "Buy Ethereum", + "buy-keycard": "Buy Keycard", + "by-continuing-you-accept": "By continuing you accept our ", + "camera-access-error": "To grant the required camera permission, please go to your system settings and make sure that Status > Camera is selected.", + "camera-permission-denied": "Permission denied", + "can-not-add-yourself": "That's you, to start a chat choose someone else", + "can-send-messages": "You can send and receive new messages", + "cancel": "Cancel", + "cancel-keycard-setup": "Cancel Keycard setup", + "cancel-request-to-join": "Cancel request to join", + "cancel-request?": "Cancel request?", + "cancelling": "Cancelling", + "cannot-pin-desc": "You can only pin a max of 3 messages.\nUnpin at least one to pin a new one.", + "cannot-pin-title": "You can't pin this message!", + "cannot-read-card": "Can't read card.\nPlease hold it to the back of your phone", + "cannot-use-default-pin": "Passcode 000000 is not allowed.\nPlease use another number", + "cant-fetch-info": "Can't fetch info", + "cant-open-public-chat": "Can't open public chat", + "cant-report-bug": "Can't report a bug", + "cant-store-new-keys": "You can’t use it to store new keys right now", + "cant-use-keycard-device": "You can’t use this Keycard on this device", + "cant-use-right-now": "You can’t use it right now", + "card-is-blank": "This card is blank", + "card-reseted": "Card has been reseted", + "card-unpaired": "Card has been unpaired from current device", + "category": "Category", + "category-title": "Category title", + "change-fleet": "Do you want to switch to the {{fleet}} fleet?\n\nThis will require you to log in again", + "change-group-privacy": "Change group privacy", + "change-logging-enabled": "Are you sure you want to {{enable}} logging?", + "change-pairing": "Change pairing code", + "change-pairing-description": "Changing the pairing code does not affect the current pairings. However, any new pairing will require the new code.", + "change-pairing-title": "Create a new pairing code", + "change-passcode": "Change Passcode", + "change-password": "Change password", + "change-password-confirm-description": "Your data must now be re-encrypted with your new password. Please do not quit the app or turn off your device.", + "change-password-confirm-warning": "Re-encrypting data after changing password may take up to 3 min. Closing the app or locking phone while re-encryption is in progress will lead to data corruption and loss of your Status profile.", + "change-password-description": "Change password used to log in to Status and sign transactions", + "change-password-done-description": "Please log out Status and log in using your new password", + "change-password-done-header": "Re-encryption complete!", + "change-password-loading-description": "Data is now being re-encrypted with your new password, this may take up to 3min.", + "change-password-loading-header": "Keep app open on screen!", + "change-password-loading-warning": "Do not quit the app or turn off your device while in-progress.\n \nClosing the app or locking your phone while re-encryption is taking place will lead to data corruption and the loss of your Status profile.", + "change-password-new-password-label": "New password", + "change-password-new-password-placeholder": "Enter new password", + "change-password-old-password-label": "Current password", + "change-password-old-password-placeholder": "Enter current password", + "change-password-repeat-password-placeholder": "Repeat new password", + "change-pin": "Change 6-digit passcode", + "change-pin-keycard": "Change PIN", + "change-pin-keycard-description": "Use different PIN for this Keycard", + "change-pin-keycard-message": "It’s very important that you will not forget new PIN. Status is unable to restore PIN. If you forget it, Keycard will be blocked and you will need to restore it via recovery phrase or PUK.", + "change-puk": "Change 12-digit PUK", + "change-testnet-mode-logout-info": "You’ll be logged out of the app", + "change-tip": "Change tip", + "changed-amount-warning": "Amount was changed from {{old}} to {{new}}", + "changed-asset-warning": "Asset was changed from {{old}} to {{new}}", + "channel-muted-for-1-hour": "Channel muted for 1 hour \n(until {{duration}})", + "channel-muted-for-1-week": "Channel muted for 1 week \n(until {{duration}})", + "channel-muted-for-15-minutes": "Channel muted for 15 minutes \n(until {{duration}})", + "channel-muted-for-8-hours": "Channel muted for 8 hours \n(until {{duration}})", + "channel-muted-till-unmuted": "Channel muted till unmuted \n(until {{duration}})", + "channel-on-status": "Channel on Status", + "channel-unmuted-successfully": "Channel unmuted successfully!", + "chaos-mode": "Chaos mode", + "chaos-unicorn-day": "Chaos Unicorn Day", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Chat and transact privately with friends", + "chat-is-a-contact": "Contact", + "chat-is-not-a-contact": "Not a contact", + "chat-key": "Chat key", + "chat-key-content": "Messages on the Status chat protocol are sent and received using encryption keys. The public chat key is a string of characters you share with others so they can send you messages in Status.", + "chat-key-description": "Your unique public ID in Status. Others can use it to send you a contact request.", + "chat-key-title": "Chat Key", + "chat-link-previews": "Chat link previews", + "chat-muted-for-1-hour": "Chat muted for 1 hour \n(until {{duration}})", + "chat-muted-for-1-week": "Chat muted for 1 week \n(until {{duration}})", + "chat-muted-for-15-minutes": "Chat muted for 15 minutes \n(until {{duration}})", + "chat-muted-for-8-hours": "Chat muted for 8 hours \n(until {{duration}})", + "chat-muted-till-unmuted": "Chat muted till unmuted \n(until {{duration}})", + "chat-name": "Chat name", + "chat-name-content": "Three random words, derived algorithmically from your chat key and used as your default alias in chat. Chat names are completely unique; no other user can have the same three words.", + "chat-name-title": "Chat Name", + "chat-notification-preferences": "Notification settings", + "chat-settings": "Chat settings", + "chat-unmuted-successfully": "Chat unmuted successfully!", + "chat-with": "Chat with {{selected-user}}", + "chat-with-friends": "Chat privately with friends", + "chats": "Chats", + "check-before-syncing": "Check before syncing", + "check-before-syncing-doc-checkbox-1": "Connect both devices to the same network", + "check-before-syncing-doc-checkbox-2": "Make sure you are logged in on the other device", + "check-before-syncing-doc-checkbox-3": "Disable the firewall and VPN on your devices", + "check-before-syncing-doc-description": "To sync your devices successfully, make sure to check and complete these steps:", + "check-keycard": "Check your Keycard", + "check-on-block-explorer": "Check on block explorer", + "check-on-opensea": "Check on opensea", + "check-other-device-for-pairing": "Check your other device for a pairing request.", + "check-your-account-balance-and-activity": "Check your account balance and activity", + "check-your-recovery-phrase": "Check your seed phrase", + "choose-actions": "Choose actions", + "choose-authentication-method": "Choose an authentication method", + "choose-new-location-for-keystore": "Choose a new location to save your keystore file", + "choose-storage": "Choose storage", + "clear": "Clear", + "clear-all": "Clear all", + "clear-history": "Clear history", + "clear-history-action": "Clear", + "clear-history-confirmation": "Clear history?", + "clear-history-confirmation-content": "Are you sure you want to clear this chat history?", + "clear-history-title": "Clear history?", + "clear-history?": "Clear History?", + "cleared-chat-description-public": "It's been quiet here. Start the conversation or ", + "close": "Close", + "close-all": "Close all", + "close-app-button": "Confirm", + "close-app-content": "The app will stop and close. When you reopen it, the selected network will be used", + "close-app-title": "Warning!", + "close-chat": "Close chat", + "close-chat-confirmation": "This conversation will disappear from the list.\nIt will appear again if the contact sends a new message.", + "close-community": "Close community", + "close-contact-search": "Close contact search", + "code-snippet": "Code snippet", + "collectibles": "Collectibles", + "collectibles-leak-metadata": "You can display your NFTs here. If you do, you will share your wallet and IP address", + "collecting-usage-data": "Share basic usage and diagnostic data to improve Status. Opt out anytime in Settings.", + "colour": "Colour", + "command-button-send": "Send", + "common-names": "Common names", + "communities": "Communities", + "communities-alpha": "Communities (alpha)", + "communities-enabled": "Communities enabled", + "communities-only-available-in-desktop": "For now, you can only create communities on the Status desktop app.", + "communities-verified": "✓ Verified Status Community", + "community": "Community", + "community-channel": "Community channel", + "community-channel-read-requirements-met": "View only requirements met", + "community-channel-read-requirements-not-met": "View only requirements not met", + "community-channel-write-requirements-met": "View and post requirements met", + "community-channel-write-requirements-not-met": "View and post requirements not met", + "community-color": "Community colour", + "community-color-placeholder": "Pick a colour", + "community-edit-title": "Edit community", + "community-emoji-thumbnail-title": "Thumbnail", + "community-enter-channel-info": "Entering channel will reveal your public addresses to the node owner", + "community-image-delete": "", + "community-image-pick": "Pick an image", + "community-image-remove": "Remove", + "community-image-take": "Take a photo", + "community-info": "Community info", + "community-info-not-found": "Community information not found", + "community-invite-title": "Invite", + "community-join-requirements-changed": "Token requirements have changed", + "community-join-requirements-met": "Join requirements met", + "community-join-requirements-not-met": "Join requirements not met", + "community-join-requirements-tokens-lost": "You no longer have the tokens required", + "community-key": "Community private key", + "community-key-placeholder": "Type your community private key", + "community-kicked-body": "You were kicked from", + "community-kicked-heading": "Kicked from community", + "community-link": "Community link", + "community-members": { + "one": "{{count}} member", + "other": "{{count}} members" + }, + "community-members-title": "Members", + "community-message-preview": "Invitation to join {{community-name}}", + "community-on-status": "Community on Status", + "community-overview": "Community Overview", + "community-private-key": "Community private key", + "community-request-accepted": "Request accepted", + "community-request-accepted-body-text": "Now you are a member of", + "community-request-not-accepted": "Request hasn't been accepted", + "community-request-not-accepted-body-text-prefix": "Your request to join", + "community-request-not-accepted-body-text-suffix": "hasn't been accepted", + "community-request-pending": "Request pending", + "community-request-pending-body-text": "You requested to join", + "community-requests-to-join-title": "Membership requests", + "community-roles": "Roles", + "community-rules": "Community rules", + "community-share-title": "Share", + "community-thumbnail-image": "Thumbnail image", + "community-thumbnail-upload": "Upload", + "community-unmuted": "Community unmuted", + "complete-backup": "Complete backup", + "complete-hardwallet-setup": "This card is now linked. You need it to sign transactions and unlock your keys", + "completed": "Completed", + "confirm": "Confirm", + "confirm-account-origin": "Confirm account origin", + "confirm-and-leave": "Confirm and leave", + "confirm-backup": "Check your backup", + "confirm-changes": "Confirm changes", + "confirm-new-password": "Confirm new password", + "confirm-pairing-code-placeholder": "Confirm your pairing code...", + "confirm-password-placeholder": "Confirm your password...", + "confirm-selection": "Confirm selection", + "confirm-the-position": "Confirm the position of certain words in your recovery phrase", + "confirmation-request": "Confirmation request", + "confirmations": "Confirmations", + "confirmations-helper-text": "When the transaction has 12 confirmations you can consider it settled.", + "confirmed-on": "Confirmed on", + "connect": "Connect", + "connect-mailserver-content": "Connect to {{name}}?", + "connect-with-users": "Connect with users", + "connected": "Connected", + "connected-dapps": "Connected dApps", + "connected-dont-move": "Connected. Don’t move your card.", + "connected-peers": "Connected and discovered peers", + "connected-to": "Connected to", + "connecting": "Connecting...", + "connecting-requires-login": "Connecting to another network requires login", + "connection-request": "Connection Request", + "connection-status": "Connection status", + "connection-type": "Type of connection to peers", + "connection-with-the-card-lost": "Connection with the card\n has been lost", + "connection-with-the-card-lost-setup-text": "To resume the setup hold the card to\n the back of your phone and maintain\n card to phone contact", + "connection-with-the-card-lost-text": "To proceed hold the card to the back of your phone", + "contact-code": "Chat key", + "contact-profile-request-pending": "Contact request pending", + "contact-request": "Contact request", + "contact-request-accepted": "Accepted ✓", + "contact-request-accepted-toast": "{{name}} accepted your contact request", + "contact-request-chat-add": "Add {{name}} as contact to send a message", + "contact-request-chat-pending": "Your contact request is pending", + "contact-request-chat-received": "{{name}} sent you a contact request", + "contact-request-declined": "Declined ⓧ", + "contact-request-header": "👋 Contact requests", + "contact-request-is-now-a-contact": "is now a contact", + "contact-request-message-prompt": "Why should they accept your request?", + "contact-request-outgoing": "You’re trying to connect with", + "contact-request-pending": "Pending...", + "contact-request-removed-as-contact": "removed as a contact", + "contact-request-removed-you-as-contact": "removed you as a contact", + "contact-request-review": "Review contact request", + "contact-request-sent": "sent contact request", + "contact-request-sent-to": "Contact request sent to", + "contact-request-sent-toast": "{{name}} sent you a contact request", + "contact-request-was-accepted": "Contact request accepted", + "contact-request-was-ignored": "Contact request ignored", + "contact-request-was-sent": "Contact request sent", + "contact-requests": "Contact requests", + "contact-s": { + "one": "contact", + "other": "contacts" + }, + "contacts": "Contacts", + "contacts-descr": "Your contacts will appear here. You will receive status updates from anyone you add as a contact", + "contacts-empty": "Contacts with ENS names will appear here", + "continue": "Continue", + "continue-anyway": "Continue anyway", + "contract-address": "Contract address", + "contract-interaction": "Contract interaction", + "contract-isnt-supported": "Contract is not supported", + "copied-to-clipboard": "copied-to-clipboard", + "copy-address": "Copy address", + "copy-all-details": "Copy all details", + "copy-info": "Copy info", + "copy-keycard": "Create a copy of this Keycard", + "copy-message-id": "Copy message ID", + "copy-qr": "Copy code", + "copy-text": "Copy text", + "copy-to-clipboard": "Copy", + "copy-transaction-hash": "Copy transaction hash", + "correct-private-key": "Correct private key", + "cost-fee": "Cost/Fee", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Create", + "create-a-pin": "Create a 6-digit passcode", + "create-a-puk": "Create a 12-digit PUK", + "create-account": "Create account", + "create-backup-keycard": "Create backup Keycard", + "create-backup-profile-keycard": "Create backup for your profile Keycard", + "create-category": "Create category", + "create-channel": "Create a channel", + "create-channel-title": "New channel", + "create-community": "Create a community", + "create-group-chat": "Create group chat", + "create-keycard-pin": "Create Keycard PIN", + "create-multiaccount": "Generate keys", + "create-new-key": "Get new keys", + "create-new-keycard-pin": "Create new Keycard PIN", + "create-new-profile": "Create new profile", + "create-pin": "Create 6-digit passcode", + "create-pin-description": "You'll need your card + this 6-digit passcode to unlock Status and to confirm transactions", + "create-profile": "Create profile", + "create-profile-keycard": "Create profile on empty Keycard", + "create-profile-password-info-box-description": "Your Status keys are the foundation of your self-sovereign identity in Web3. You have complete control over these keys, which you can use to sign transactions, access your data, and interact with Web3 services.\n\nYour keys are always securely stored on your device and protected by your Status profile password. Status doesn't know your password and can't reset it for you. If you forget your password, you may lose access to your Status profile and wallet funds.\n\nRemember your Status password and don't share it with anyone.", + "create-profile-password-info-box-title": "About your profile password", + "created-group-chat-description": "You created the group {{group-name}}", + "crypto": "Crypto", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Currency", + "currency-display-name-aed": "Emirati Dirham", + "currency-display-name-afn": "Afghanistan Afghani", + "currency-display-name-ars": "Argentine Peso", + "currency-display-name-aud": "Australian Dollar", + "currency-display-name-bbd": "Barbados Dollar", + "currency-display-name-bdt": "Bangladeshi Taka", + "currency-display-name-bgn": "Bulgarian Lev", + "currency-display-name-bhd": "Bahraini Dinar", + "currency-display-name-bnd": "Brunei Darussalam Dollar", + "currency-display-name-bob": "Bolivia Bolíviano", + "currency-display-name-brl": "Brazil Real", + "currency-display-name-btc": "Bitcoin", + "currency-display-name-btn": "Bhutanese Ngultrum", + "currency-display-name-cad": "Canada Dollar", + "currency-display-name-chf": "Switzerland Franc", + "currency-display-name-clp": "Chile Peso", + "currency-display-name-cny": "China Yuan Renminbi", + "currency-display-name-cop": "Colombia Peso", + "currency-display-name-crc": "Costa Rica Colon", + "currency-display-name-czk": "Czech Koruna", + "currency-display-name-dai": "DAI", + "currency-display-name-dkk": "Denmark Krone", + "currency-display-name-dop": "Dominican Republic Peso", + "currency-display-name-egp": "Egypt Pound", + "currency-display-name-etb": "Ethiopian Birr", + "currency-display-name-eth": "Ethereum", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "British Pound", + "currency-display-name-gel": "Georgian Lari", + "currency-display-name-ghs": "Ghana Cedi", + "currency-display-name-hkd": "Hong Kong Dollar", + "currency-display-name-hrk": "Croatia Kuna", + "currency-display-name-huf": "Hungary Forint", + "currency-display-name-idr": "Indonesia Rupiah", + "currency-display-name-ils": "Israel Shekel", + "currency-display-name-inr": "India Rupee", + "currency-display-name-isk": "Iceland Krona", + "currency-display-name-jmd": "Jamaica Dollar", + "currency-display-name-jpy": "Japanese Yen", + "currency-display-name-kes": "Kenyan Shilling", + "currency-display-name-krw": "Korea (South) Won", + "currency-display-name-kwd": "Kuwaiti Dinar", + "currency-display-name-kzt": "Kazakhstan Tenge", + "currency-display-name-lkr": "Sri Lanka Rupee", + "currency-display-name-mad": "Moroccan Dirham", + "currency-display-name-mdl": "Moldovan Leu", + "currency-display-name-mur": "Mauritius Rupee", + "currency-display-name-mwk": "Malawian Kwacha", + "currency-display-name-mxn": "Mexico Peso", + "currency-display-name-myr": "Malaysia Ringgit", + "currency-display-name-mzn": "Mozambique Metical", + "currency-display-name-nad": "Namibia Dollar", + "currency-display-name-ngn": "Nigeria Naira", + "currency-display-name-nok": "Norway Krone", + "currency-display-name-npr": "Nepal Rupee", + "currency-display-name-nzd": "New Zealand Dollar", + "currency-display-name-omr": "Oman Rial", + "currency-display-name-pen": "Peru Sol", + "currency-display-name-pgk": "Papua New Guinean Kina", + "currency-display-name-php": "Philippines Peso", + "currency-display-name-pkr": "Pakistan Rupee", + "currency-display-name-pln": "Polish Zloty", + "currency-display-name-pyg": "Paraguay Guarani", + "currency-display-name-qar": "Qatar Riyal", + "currency-display-name-ron": "Romania Leu", + "currency-display-name-rsd": "Serbia Dinar", + "currency-display-name-rub": "Russia Ruble", + "currency-display-name-sar": "Saudi Arabia Riyal", + "currency-display-name-sek": "Sweden Krona", + "currency-display-name-sgd": "Singapore Dollar", + "currency-display-name-snt": "Status Network Token", + "currency-display-name-thb": "Thailand Baht", + "currency-display-name-try": "Turkish Lira", + "currency-display-name-ttd": "Trinidad and Tobago Dollar", + "currency-display-name-twd": "Taiwan New Dollar", + "currency-display-name-tzs": "Tanzanian Shilling", + "currency-display-name-uah": "Ukraine Hryvnia", + "currency-display-name-ugx": "Ugandan Shilling", + "currency-display-name-usd": "United States Dollar", + "currency-display-name-uyu": "Uruguay Peso", + "currency-display-name-vef": "Venezuela Bolívar", + "currency-display-name-vnd": "Vietnam Dong", + "currency-display-name-zar": "South Africa Rand", + "current": "Current", + "current-average": "Current average", + "current-average-tip": "Current average tip", + "current-base": "Current base", + "current-base-fee": "Current base fee", + "current-minimum-tip": "Current minimum tip", + "current-network": "Current network", + "current-password": "Current password", + "current-pin": "Enter 6-digit passcode", + "current-pin-description": "Enter your 6-digit passcode to proceed", + "current-units": "Current: {{current}} UNITS", + "custom": "Custom", + "custom-node": "You are using custom RPC endpoint. Your local transfers history might be incomplete.", + "custom-seed-phrase": "Invalid seed phrase", + "custom-seed-phrase-text-1": "This seed phrase doesn't match our supported dictionary. Check for misspelled words.", + "cut": "Cut", + "dapp": "ÐApp", + "dapp-requests-network-access": "DApp requests access to a disabled network", + "dapp-requests-network-access-with-limit": "DApp requests access to a disabled network, but the limit of 5 active networks is reached. Please deactivate one.", + "dapp-starter-pack-accept": "Accept and Open", + "dapp-starter-pack-description": "Here’s some crypto to get you started! Use it to get stickers, an ENS name and try dapps", + "dapp-starter-pack-title": "Starter Pack", + "dapp-will-be-able-to": "{{dapp-name}} will be able to:", + "dapp-would-like-to-connect-wallet": "would like to connect to", + "dapps": "dApps", + "dapps-permissions": "DApp permissions", + "dark": "Dark", + "data": "Data", + "data-collected": "Data collected", + "data-collected-subtitle": "The table below shows the exact data that is stored and will be sent. Data is validated against public rules to ensure no sensitive data is sent. Don’t trust, verify.", + "data-syncing": "Data syncing", + "data-usage": "Data usage", + "database-reset-content": "Chats, contacts and settings have been deleted. You can use your account with your Keycard", + "database-reset-title": "Database reset", + "database-reset-warning": "Database will be reset. Chats, contacts and settings will be deleted", + "datetime-ago": "ago", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "day", + "other": "days" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "hour", + "other": "hours" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-hour-short-lowercase": { + "one": "h", + "other": "h" + }, + "datetime-minute": { + "one": "minute", + "other": "minutes" + }, + "datetime-minute-mid": { + "one": "min", + "other": "min" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "second", + "other": "seconds" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "today", + "datetime-yesterday": "Yesterday", + "days": "Days", + "debugging": "Debugging", + "dec": "Dec", + "decimals": "Decimals", + "decline": "Decline", + "declined": "Declined", + "decryption-failed-content": "An error occured decrypting your data. You might need to erase your old data and generate a new account. Tap “Apply” to erase or “Cancel” to try again", + "default": "Default", + "default-account-placeholder": "Account Name", + "default-assets": "Default ERC20 and ERC721", + "default-ethereum-format": "Default Ethereum format", + "default-format": "Default format", + "default-sync-period": "Sync history for", + "default-watched-address-placeholder": "Watched address", + "define-amount-sent-from-network": "Define amount sent from {{network}} network", + "delete": "Delete", + "delete-account": "Delete account", + "delete-and-leave-group": "Delete and leave group", + "delete-bootnode": "Delete bootnode", + "delete-bootnode-are-you-sure": "Are you sure you want to delete this bootnode?", + "delete-bootnode-title": "Delete bootnode", + "delete-category-confirmation": "Are you sure you want to delete this category?", + "delete-confirmation": "Delete?", + "delete-for-everyone": "Delete for everyone", + "delete-for-me": "Delete for me", + "delete-keys-keycard": "Delete keys from Keycard", + "delete-mailserver": "Delete Status node", + "delete-mailserver-are-you-sure": "Are you sure you want to delete this Status node?", + "delete-mailserver-title": "Delete Status node", + "delete-message": "Delete message", + "delete-my-account": "Delete my account", + "delete-my-profile": "Delete my profile", + "delete-network-confirmation": "Are you sure you want to delete this network?", + "delete-network-error": "Please connect to a different network before deleting this one", + "delete-network-title": "Delete network?", + "delete-node": "Delete node", + "delete-node-are-you-sure": "Are you sure you want to delete this node?", + "delete-node-title": "Delete node", + "delete-profile": "Delete profile", + "delete-profile-warning": "Warning: If you don’t have your seed phrase written down, you will lose access to your funds after you delete your profile", + "deleted-this-message": "deleted this message", + "delivered": "Delivered", + "deny": "Deny", + "deposit-to-your-wallet": "Deposit to your wallet", + "derivation-path": "Derivation path", + "derivation-path-copied": "Derivation path copied", + "derivation-path-desc": "Derivation paths are the routes your Status Wallet uses to generate addresses from your private key.", + "derivation-path-header": "Derivation path", + "derive-addresses": "Derive addresses", + "describe-channel": "Describe the channel", + "description": "Description", + "desktop": "Desktop", + "destroy": "Destroy", + "details": "Details", + "dev-mode": "Development mode", + "dev-mode-settings": "Development mode settings", + "device-syncing": "Device syncing", + "devices": "Devices", + "devices-count": "{{number}} devices", + "different-keycard": "This is a different Keycard", + "disable": "disable", + "disable-all": "Disable all", + "disable-later-in-settings": "You can disable this later in Settings", + "disable-light-client": "Disable Light Client?\n\nYou’ll need to log in again.", + "disable-privacy-mode": "Disable privacy mode", + "disable-store-confirmations": "Disable Store confirmations?\n\nYou’ll need to log in again.", + "disabled": "Disabled", + "disabled-2": "disabled", + "discard": "Discard", + "discard-changes?": "Discard changes?", + "disconnect": "Disconnect", + "disconnect-dapp": "Disconnect dApp", + "disconnect-dapp-confirmation": "Are you sure you want to disconnect {{dapp}}?", + "disconnect-dapp-fail": "Failed to disconnect {{dapp}} from {{account}}", + "disconnect-dapp-success": "{{dapp}} disconnected from {{account}}", + "disconnected": "Disconnected", + "discover": "Discover", + "discover-communities": "Discover communities", + "discover-web3": "Discover web3", + "dismiss": "Dismiss", + "display": "Display", + "display-collectibles": "Display collectibles", + "do-not-cheat": "Verify your backup", + "do-not-cheat-description": "Make sure you wrote down the words in the correct order.", + "do-not-quit": "Do not quit the application or turn off your device. Doing so will lead to data corruption, loss of your Status profile and the inability to use Status.", + "do-not-quit-the-application": "Do not quit the application", + "do-not-share": "Do not share", + "documents": "Documents", + "done": "Done", + "dont-ask": "Don't ask me again", + "dont-auto-recalculate-network": "Don't auto recalculate {{network}}", + "dont-have-statatus-on-another-device": "Don't have Status on another device?", + "dont-yell-at-me": "Don’t yell at me", + "duration-estimate": "Duration estimate", + "edit": "Edit", + "edit-account": "Edit account", + "edit-categories": "Edit Categories", + "edit-channel-title": "Edit channel", + "edit-chats": "Edit chats", + "edit-community": "Edit community", + "edit-derivation-path": "Edit derivation path", + "edit-details": "Edit details", + "edit-favourite": "Edit favourite", + "edit-group": "Edit group", + "edit-message": "Edit message", + "edit-name-and-image": "Edit name and image", + "edit-name-image": "Edit name and image", + "edit-nickname": "Edit nickname", + "edit-profile": "Edit Profile", + "edit-receiver-networks": "Edit receiver networks", + "edit-shared-addresses": "Edit shared addresses", + "edit-wallet-account-colour-updated-message": "Account colour has been updated", + "edit-wallet-account-emoji-updated-message": "Account emoji has been updated", + "edit-wallet-account-name-updated-message": "Account name has been updated", + "edit-wallet-network-preferences-updated-message": "Account network preferences has been updated", + "edited": "Edited", + "editing-message": "Editing message", + "eligible-to-join-as": "Eligible to join as", + "emoji-activity": "Activity", + "emoji-and-colors-unique-error": "Emoji and colour combination must be unique", + "emoji-flags": "Flags", + "emoji-food": "Food", + "emoji-hash": "Emoji Hash", + "emoji-hash-copied": "Emojihash copied to clipboard", + "emoji-nature": "Nature", + "emoji-no-results-description": "Try something like “rainbow”", + "emoji-no-results-title": "No emojis match your search", + "emoji-objects": "Objects", + "emoji-people": "People", + "emoji-recent": "Recent", + "emoji-search-placeholder": "Search emojis", + "emoji-symbols": "Symbols", + "emoji-travel": "Travel", + "emojihash": "Emojihash", + "emojihash-description": "A visual representation of your chat key. It will help other users recognize your profile.", + "emojis": "Emojis", + "empty-activity-center": "Your chat notifications\nwill appear here", + "empty-card-info": "To generate new key pair, or import existent non-profile keys to the Keycard, please use the Status desktop app. If you'd like these features on mobile, feel free to upvote them and discuss them in Status community.", + "empty-chat-description": "There are no messages \nin this chat yet", + "empty-chat-description-community": "It's been quiet here for the last {{quiet-hours}}.", + "empty-chat-description-one-to-one": "Any messages you send here are encrypted and can only be read by you and ", + "empty-chat-description-public": "It's been quiet here for the last {{quiet-hours}}. Start the conversation or ", + "empty-chat-description-public-share-this": "share this chat.", + "empty-keycard-required": "Requires an empty Keycard", + "empty-notifications-admin-tab": "No unread admin notifications", + "empty-notifications-all-tab": "No unread notifications", + "empty-notifications-contact-requests-tab": "No contact requests notifications", + "empty-notifications-identity-verification-tab": "No identity verification requests", + "empty-notifications-membership-tab": "No membership notifications", + "empty-notifications-mentions-tab": "No unread mention notifications", + "empty-notifications-replies-tab": "No unread reply notifications to your messages", + "empty-notifications-subtitle-read": "Your notifications will be here", + "empty-notifications-subtitle-unread": "Unread notifications will be here", + "empty-notifications-system-tab": "No system notifications", + "empty-notifications-title-read": "No notifications", + "empty-notifications-title-unread": "You're up to date", + "empty-notifications-transactions-tab": "No transaction notifications", + "empty-pending-invitations-descr": "People who wish to join the group\nvia an invite link will appear here", + "empty-tab": "Empty tab", + "empty-tab-description": "C'mon do something...", + "enable": "Enable", + "enable-access-to-camera": "Enable access to camera", + "enable-access-to-local-network": "Enable access to local network", + "enable-all": "Enable all", + "enable-biometrics": "Enable biometrics", + "enable-camera": "Enable camera", + "enable-light-client": "Enable Light Client?\n\nYou’ll need to log in again.", + "enable-link-previews": "Enable link previews in chat?", + "enable-network-access": "Enable network access", + "enable-network-to-sign-requests": "to sign requests from", + "enable-news-notifications-third-party": "Include notifications delivered through third-party providers", + "enable-notifications": "Enable notifications", + "enable-notifications-info-description": "Describe how using Firebase affects privacy.", + "enable-notifications-sub-title": "Receive notifications about your new messages, wallet transactions and news", + "enable-privacy-mode": "Enable privacy mode", + "enable-store-confirmations": "Enable Store confirmations?\n\nYou’ll need to log in again.", + "enabled": "enabled", + "enabled-push-notifications": "Enable push notifications", + "encrypt-with-password": "Encrypt with password", + "encrypted-key-pairs": "Encrypted key pairs", + "encrypted-key-pairs-code": "Encrypted key pairs code", + "ending-not-allowed": "{{ending}} ending is not allowed", + "ends-with-space": "Cannot end with space", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Add username", + "ens-agree-to": "Agree to ", + "ens-chat-settings": "Chat settings", + "ens-custom-domain": "Custom domain", + "ens-custom-username-hints": "Type the entire username including the custom domain like username.domain.eth", + "ens-custom-username-taken": "Username doesn’t belong to you :(", + "ens-deposit": "Deposit", + "ens-dismiss-message": "Click here to dismiss", + "ens-displayed-with": "Your messages are displayed to others with", + "ens-get-name": "Get a universal username", + "ens-got-it": "Ok, got it", + "ens-locked": "Username locked. You won’t be able to release it until {{date}}", + "ens-name-content": "Custom alias for your chat key that you can register using the Ethereum Name Service. ENS names are decentralized usernames.", + "ens-name-not-found": "Cannot resolve ENS name", + "ens-name-title": "ENS Name", + "ens-network-restriction": "Only available on Ethereum", + "ens-no-usernames": "You don't have any username connected", + "ens-or-chat-key": "ENS or Chatkey", + "ens-powered-by": "Powered by Ethereum Name Services", + "ens-primary-username": "Primary username", + "ens-register": "Register", + "ens-registration-failed": "To register the username, please try again.", + "ens-registration-failed-title": "Transaction failed", + "ens-registration-failure": "Registration failed", + "ens-registration-in-progress": "Registration in progress...", + "ens-release-username": "Release username", + "ens-remove-hints": "Removing will detach the username from your key.", + "ens-remove-username": "Remove username", + "ens-saved": " is now connected with your chat key and can be used in Status.", + "ens-saved-title": "Username added", + "ens-show-username": "Show my ENS username in chats", + "ens-terms-header": "Terms of name registration", + "ens-terms-point-1": "Funds are deposited for 1 year. Your SNT will be locked, but not spent.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS Registry).", + "ens-terms-point-2": "After 1 year, you can release the name and get your deposit back, or take no action to keep the name.", + "ens-terms-point-3": "If terms of the contract change — e.g. Status makes contract upgrades — user has the right to release the username regardless of time held.", + "ens-terms-point-4": "The contract controller cannot access your deposited funds. They can only be moved back to the address that sent them.", + "ens-terms-point-5": "Your address(es) will be publicly associated with your ENS name.", + "ens-terms-point-6": "Usernames are created as subdomain nodes of stateofus.eth and are subject to the ENS smart contract terms.", + "ens-terms-point-7": "You authorize the contract to transfer SNT on your behalf. This can only occur when you approve a transaction to authorize the transfer.", + "ens-terms-point-8": "These terms are guaranteed by the smart contract logic at addresses:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar) ", + "ens-terms-registration": "Terms of name registration.", + "ens-test-message": "Hey", + "ens-transaction-pending": "Transaction pending...", + "ens-understand": "I understand that my wallet address will be publicly connected to my username.", + "ens-username": "ENS username", + "ens-username-already-added": "Username is already connected with your chat key and can be used inside Status.", + "ens-username-available": "✓ Username available!", + "ens-username-connected": "This user name is owned by you and connected with your chat key.", + "ens-username-connected-continue": "Continue to set `Show my ENS username in chats`.", + "ens-username-connected-with-different-key": "Continuing will require a transaction to connect the username with your current chat key.", + "ens-username-connection-confirmation": "{{username}} will be connected once the transaction is complete.", + "ens-username-hints": "At least 4 characters. Latin letters, numbers, and lowercase only.", + "ens-username-invalid": "Letters and numbers only.", + "ens-username-invalid-name-warning": "Registration process of one of your ens names has finished in invalid state. DO NOT USE the name for wallet transactions and reach out to our support at support@status.im", + "ens-username-owned": "✓ Username is owned by you. ", + "ens-username-owned-continue": "Continuing will connect this username with your chat key.", + "ens-username-registration-confirmation": "Nice! You own {{username}} once the transaction is complete.", + "ens-username-registration-invalid": "Warning! Registration process has finished in invalid state. DO NOT USE the name for wallet transactions and reach out to our support at support@status.im", + "ens-username-taken": "Username already taken :(", + "ens-username-you-can-follow-progress": "You can follow the progress in the Transaction History section of your wallet.", + "ens-usernames": "ENS usernames", + "ens-usernames-details": "Register a universal username to be easily recognized by other users", + "ens-want-custom-domain": "I own a name on another domain", + "ens-want-domain": "I want a stateofus.eth domain", + "ens-welcome-hints": "ENS names transform those crazy-long addresses into unique usernames.", + "ens-welcome-point-customize": "An ENS name can replace your random 3-word name in chat. Be @yourname instead of {{name}}.", + "ens-welcome-point-customize-title": "Customize your chat name", + "ens-welcome-point-receive": "Others can send you funds via chat in one simple step.", + "ens-welcome-point-receive-title": "Receive transactions in chat", + "ens-welcome-point-register": "Register once to keep the name forever. After 1 year you can release the name and get your SNT back.", + "ens-welcome-point-register-title": "10 SNT to register", + "ens-welcome-point-simplify": "You can receive funds to your easy-to-share ENS name rather than your hexadecimal hash (0x...).", + "ens-welcome-point-simplify-title": "Simplify your ETH address", + "ens-welcome-point-verify": "You can verify and add any usernames you own in the next steps.", + "ens-welcome-point-verify-title": "Already own a username?", + "ens-your-username": "Your username", + "ens-your-usernames": "Your usernames", + "ens-your-your-name": "Your ENS name", + "ensure-both-devices-are-on-the-same-network": "Ensure both devices are on the same network", + "ensure-qr-code-is-in-focus-to-scan": "Ensure that the QR code is in focus to scan", + "ensure-written-recovery": "Ensure you have written down your recovery phrase and have a safe place to keep it. Remember, anyone who has your recovery phrase has access to your funds.", + "enter-12-words": "Enter the 12 words of your seed phrase, separated by single spaces", + "enter-a-private-key": "Enter a private key", + "enter-a-seed-phrase": "Enter a seed phrase", + "enter-address": "Enter address", + "enter-another-recovery-phrase": "Enter another recovery phrase", + "enter-channel": "Enter channel", + "enter-chat-key": "Enter chat key or scan a QR", + "enter-contact-code": "ENS (vitalik94) or chat key (0x04…)", + "enter-current-keycard-pin": "Enter current Keycard PIN", + "enter-eth": "Enter any ETH address or ENS name.", + "enter-keycard-pin": "Enter Keycard PIN", + "enter-pair-code": "Enter your pairing code", + "enter-pair-code-description": "Pairing code can be set from an already paired Status client", + "enter-password": "Enter password", + "enter-password-migration-prompt": "Enter your password to move contacts, chats and settings along with your keys", + "enter-pin": "Enter 6-digit passcode", + "enter-private-key": "Enter the private key of an address", + "enter-private-key-placeholder": "Enter your private key", + "enter-puk-code": "Enter PUK code", + "enter-puk-code-description": "6-digit passcode has been blocked.\n Please enter PUK code to unblock passcode.", + "enter-recipient-address-or-username": "Enter address or username of the recipient", + "enter-recovery-phrase": "Enter recovery phrase", + "enter-sync-code": "Enter sync code", + "enter-url": "Enter URL", + "enter-user-pk": "Enter user public key", + "enter-watch-account-address": "Scan a QR code\nor\nenter the address to watch", + "enter-word": "Enter word", + "enter-your-code": "Enter your 6-digit passcode", + "enter-your-password": "Enter your password", + "epoch-number": "Epoch {{number}}", + "error": "Error", + "error-loading-audio": "Error while loading audio", + "error-syncing-connection-failed": "Oops! Connection failed. Try again", + "error-this-is-not-a-sync-qr-code": "Oops! This is not a sync QR code", + "error-unable-to-get-balance": "Unable to get balance", + "error-unable-to-get-prices": "Currency conversion error. Refresh your screen to try again.", + "error-unable-to-get-token-balance": "Unable to get token balance", + "errors": "Errors", + "est-time": "Est. time", + "eth": "ETH", + "eth-or-ens": "ETH address or ENS name.", + "ethereum-address": "Ethereum address", + "ethereum-node-started-incorrectly-description": "Ethereum node was started with incorrect configuration, application will be stopped to recover from that condition. Configured network id = {{network-id}}, actual = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Ethereum node started incorrectly", + "etherscan-lookup": "Look up on Etherscan", + "everyone": "Everyone", + "everyone-mention": "everyone", + "existing-saved-address": "Existing saved address", + "expand-all": "Expand all", + "experienced-web3": "Experienced in Web3?", + "explore-the-decentralized-web": "Explore and interact with the decentralized web", + "explore-the-new-status": "Explore the new Status", + "export-account": "Export account", + "export-key": "Export private key", + "external-link": "External link", + "external-storage-denied": "Access to external storage is denied", + "factory-reset": "Factory reset", + "factory-reset-keycard": "Factory reset this Keycard", + "factory-reset-warning": "Please ensure you have a backup of your recovery phrase before proceeding with the factory reset. If you want to switch to password authentication instead of Keycard, remove the profile from the device and log in with your recovery phrase.", + "failed": "Failed", + "failed-on": "Failed on", + "failed-to-fetch-community": "Failed to fetch community", + "failed-to-migrate-key-pair": "Failed to migrate key pair", + "faq": "Frequently asked questions", + "fast": "Fast", + "favorite-communities": "Your favourite communities", + "favourite": "Favourite", + "favourite-description": "Your favourite websites will appear here", + "favourites": "Favourites", + "favourites-empty": "Addresses added to favourites will appear here", + "feature-flags": "Feature Flags", + "feature-that-will-be-unavailable": "Features that will be unavailable:", + "feature-that-will-be-unavailable-item-1": "Wallet (Swap, Send, Token data, etc.)", + "feature-that-will-be-unavailable-item-2": "Market (Token data, prices, and news, etc.)", + "feature-that-will-be-unavailable-item-3": "Token-gated communities", + "feature-that-will-be-unavailable-item-4": "Mobile Push-notifications", + "feature-that-will-be-unavailable-item-5": "Status news, etc.", + "feature-unavailable": "Feature unavailable", + "feature-unavailable-for-keycard-description": "It’s not currently supported for Keycard users.", + "feature-unavailable-info": "If you'd like this feature on mobile, feel free to ", + "featured": "Featured", + "features-and-balances-disabled": "Balances and some features are unavailable", + "feb": "Feb", + "fee-cap": "Fee cap", + "fee-current-gwei": "Current: {{current}} GWEI", + "fee-explanation": "Maximum overall price for the transaction. If the block base fee exceeds this, it will be included in a following block with a lower base fee.", + "fee-options": "Suggested fee options", + "fees": "Fees", + "fetch-community": "Fetch community", + "fetch-messages": "Fetch messages", + "fetch-timeline": "↓ Fetch", + "fetching-community": "Fetching community...", + "fetching-the-price-took-longer-than-expected": "Fetching the price took longer than expected.\nPlease, try again later.", + "finalise-migration": "Finalise migration", + "finalise-migration-instructions": "Finalise migration to use this Keycard for signing in and authorising transactions on this device", + "finalized-on": "Finalized on", + "find": "Find", + "find-it-in-setting": "Find it in Settings on your other synced device", + "find-sync-code": "Find sync code", + "find-your-friends": "Find your friends with their ENS or Chatkey", + "finish": "Finish", + "finishing-card-setup": "Finishing card setup", + "fleet": "Fleet", + "fleet-settings": "Fleet settings", + "follow": "Follow", + "follow-your-interests": "Jump into a public chat and meet new people", + "for": "for", + "for-airdrops": "For airdrops", + "forgot-password": "Forgot password?", + "forgot-your-password-info-create-new-password": "Create a new password", + "forgot-your-password-info-create-new-password-description": "Enter a new password and you're all set! You will be able to use your new password.", + "forgot-your-password-info-description": "To recover your password follow these steps:", + "forgot-your-password-info-reinstall-app": "Reinstall the Status app", + "forgot-your-password-info-reinstall-app-description": "Re-download the app from your store.", + "forgot-your-password-info-remove-app": "Remove the Status app", + "forgot-your-password-info-remove-app-description": "This will erase all your data from the device, including your password.", + "forgot-your-password-info-signup-with-key": "with your existing keys", + "forgot-your-password-info-signup-with-key-description": "Access with your seed phrase or with your Keycard.", + "forgot-your-password-info-title": "Forgot your password?", + "fr": "Fr", + "free": "↓ Free", + "fri": "Fri", + "from": "from", + "from-all-profiles-on-device": "From all profiles on device", + "from-capitalized": "From", + "from-label": "From", + "gas-amount": "Gas amount", + "gas-amount-current": "Current: {{current}}", + "gas-amount-higher": "Too high. Maximum: {{high}}", + "gas-amount-higher-than": "Higher than necessary (current: {{current}})", + "gas-amount-limit": "Gas amount limit", + "gas-amount-lower": "Too low. Minimum: {{low}}", + "gas-amount-lower-than": "Lower than current (current: {{current}})", + "gas-limit": "Gas limit", + "gas-price": "Gas price", + "gas-price-current": "Current: {{current}} GWEI", + "gas-price-higher": "Too high. Maximum: {{high}}", + "gas-price-higher-than": "Higher than necessary (current: {{current}})", + "gas-price-lower": "Too low. Minimum: {{low}}", + "gas-price-lower-than": "Lower than current (current: {{current}})", + "gas-used": "Gas used", + "gated": "Gated", + "generate-a-key": "Generate keys", + "generate-a-new-account": "Generate an account", + "generate-a-new-key": "Generate a new key", + "generate-account": "Generate keys", + "generate-an-account": "Generate an account", + "generate-keys": "Generate keys", + "generate-new-key": "Generate keys", + "generate-new-keypair": "Generate new key pair", + "generate-scan-sync-code": "Generate Scan Sync Code", + "generating-codes-for-pairing": "> Downloading product software to card\n > Generating unlocking & pairing codes", + "generating-keypair": "Generating key pair...", + "generating-keys": "Generating keys...", + "generating-mnemonic": "Generating seed phrase", + "generic-error": "Error: {{generic-error}}", + "get-a-keycard": "Get a Keycard", + "get-keycard": "Get Keycard", + "get-started": "Get started", + "get-status-at": "Get Status at http://status.im", + "get-stickers": "Get Stickers", + "getting-started-description": "Status is a unique messaging app that combines decentralized technology, privacy-focused design, and Web3 capabilities to offer a unique and secure communication experience.\n\nYour Status keys are the foundation of your self-sovereign identity in Web3. They serve as a secure means of accessing and managing your personal information and identity. With Status, you own, control and manage your data and digital identity without relying on centralized organisations.\n\nChoose one of these options to create your Status profile:", + "getting-started-generate-keys-description": "Create a pair of cryptographic keys representing your Status identity. Unlike centralized apps, Status doesn't rely on your username or email address to identify your profile. Instead, it uses your private and public keys. This option creates a new Ethereum address.", + "getting-started-generate-keys-from-recovery-phrase": "Generate keys from recovery phrase", + "getting-started-generate-keys-from-recovery-phrase-description": "You can use your existing Ethereum address to create your Status profile. If you already use a non-custodial Ethereum wallet (like Metamask or Trust Wallet), you already have an Ethereum address and a recovery phrase.\n\nYour keys are always securely stored on your device and protected by your Status password.", + "getting-started-generate-keys-on-keycard": "Generate keys on a Keycard", + "getting-started-generate-keys-on-keycard-description": "A Keycard is a physical card (similar to a credit card) that securely stores your Status keys. Using a Keycard adds an extra layer of security to your digital assets and identity. This option creates a new Ethereum address.", + "getting-started-with-status": "Getting started with Status", + "gif": "GIF", + "github-repos": "GitHub repos", + "give-a-short-description": "Give a short description", + "give-a-short-description-community": "Give it a short description", + "give-permissions-camera": "Give permission\nto access camera", + "glossary": "Glossary", + "go-to": "Go to", + "go-to-settings": "Go to Settings...", + "got-it": "Got it", + "grant-face-id-permissions": "To grant the required Face ID permission, please go to your system settings and make sure that Status > Face ID is selected", + "grant-fingerprints-permissions": "To grant the required fingerprints permission, please go to your system settings and make sure that Status > Fingerprints is selected", + "group-chat": "Group chat", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** has been made admin", + "group-chat-all-contacts-invited": "All your contacts are already in the group", + "group-chat-created": "**{{member}}** created the group **{{name}}**", + "group-chat-decline-invitation": "Decline invitation", + "group-chat-member-added": "**{{member}}** has been invited", + "group-chat-member-joined": "**{{member}}** has joined the group", + "group-chat-member-removed": "**{{member}}** left the group", + "group-chat-members-count": "{{selected}}/{{max}} members", + "group-chat-name-changed": "**{{member}}** changed the group's name to **{{name}}**", + "group-chat-no-contacts": "You don't have any contacts yet.\nInvite your friends to start chatting", + "group-chat-not-member": "You are not a member of this group", + "group-details": "Group details", + "group-info": "Group info", + "group-invite": "Group invite", + "group-invite-link": "Group invite link", + "group-membership-request": "Group membership request", + "groups": "Groups", + "gwei": "Gwei", + "hang-in-there": "Hang in there! Just a few more seconds!", + "has-permissions": "has permission to access", + "hash": "Hash", + "have-a-sync-code?": "Have a sync code?", + "help": "help", + "help-capitalized": "Help", + "help-center": "Help Center", + "help-improve-status": "Help improve Status", + "help-us-improve-status": "Help us improve Status", + "help-us-improve-status-bullet-point-1": "Collect basic usage data like taps and page views", + "help-us-improve-status-bullet-point-2": "Collect core diagnostics, like bandwidth usage", + "help-us-improve-status-bullet-point-3": "Never collect your profile information or wallet address", + "help-us-improve-status-bullet-point-4": "Never collect information you input or send", + "help-us-improve-status-bullet-point-5": "Never sell your usage analytics data", + "help-us-improve-status-subtitle": "We’ll collect anonymous analytics and diagnostics from your app to enhance Status’s quality and performance.", + "here-is-a-cat-in-a-box-instead": "Here’s a cat in a box instead", + "hex": "Hex", + "hide": "Hide", + "hide-content-when-switching-apps": "Block screenshots", + "hide-content-when-switching-apps-ios": "Hide preview", + "history": "History", + "history-nodes": "History nodes", + "hit-photos-limit": "You can only add {{max-photos}} photos to your message", + "hold-card": "Hold card to the back\n of your phone", + "hold-phone-near-keycard": "Hold your Keycard near NFC sensor", + "hold-to-post-1": "Hold", + "hold-to-post-2": "to post", + "home": "Home", + "hooks": "Hooks", + "how-it-works": "How it works", + "how-to-backup": "How to backup your recovery phrase", + "how-to-pair": "How to pair", + "how-to-scan": "How to scan", + "http-gateway-error": "Oops, request failed!", + "i-dont-have-status-on-another-device": "I don’t have Status on another device", + "i-have-written": "I have written it down on paper", + "identicon-ring": "Identicon ring", + "identicon-ring-explanation": "This multicoloured ring around your profile picture represents your chat key.", + "identifier": "Identifier", + "identity-verification": "Identity verification", + "identity-verification-request": "Identity verification", + "identity-verification-request-sent": "asks", + "if-you-cancel": "If you cancel, you can request to join this community at any point.", + "if-you-have-status-on-another-device": "If you have Status on another device", + "ignore": "Ignore", + "im-new": "I'm new", + "image": "Image", + "image-remove-current": "Remove current photo", + "image-source-gallery": "Select from gallery", + "image-source-make-photo": "Capture", + "image-source-title": "Edit picture", + "images": "images", + "import": "Import", + "import-by-entering-private-key": "Import by entering private key", + "import-by-entering-recovery-phrase": "Import by entering recovery phrase", + "import-by-scanning-encrypted-qr": "Import by scanning encrypted QR", + "import-community": "Import a community", + "import-community-title": "Import a community", + "import-from-keycard": "Import from Keycard", + "import-key-pair": "Import key pair", + "import-key-pair-keycard": "Import profile key pair to Keycard", + "import-keypair-steps": "{{account-name}} was derived from your {{keypair-name}} key pair, which has not yet been imported to this device. To transact using this account, you will need to import the {{keypair-name}} key pair first.", + "import-keypair-to-use-account": "Import key pair to use this account", + "import-private-key": "Import private key", + "import-private-key-info": "New addresses cannot be derived from an account imported from a private key. Import using a seed phrase if you wish to derive addresses.", + "import-profile-key-pair": "Import profile key pair", + "import-recovery-phrase-to-keycard": "Import recovery phrase to Keycard", + "import-to-use-derived-accounts": "Import to use derived accounts", + "import-using-phrase": "Import using recovery phrase", + "in": "in", + "in-contacts": "In contacts", + "include": "Include", + "incoming": "Incoming", + "incoming-transaction": "Incoming transaction", + "incorrect-code": ["str", "Sorry the code was incorrect, please enter it again"], + "incorrect-private-key": "This is not the private key for this key pair", + "increase-gas": "Increase Gas", + "information-you-input-and-send": "Information you input and send", + "initialization": "Initialization", + "input-data": "Input data", + "install": "↓ Install", + "instruction-after-qr-generated": "On your other device, navigate to the Syncing screen and select “Scan sync”", + "insufficient-balance-to-cover-fee": "not enough balance to cover transaction fee", + "insufficient-funds-for-swaps": "Insufficient funds for swap", + "intro-message1": "Welcome to Status!\nTap this message to set your password and get started.", + "intro-privacy-policy": "Privacy Policy", + "intro-privacy-policy-note1": "Status does not collect or profit from your personal data. By continuing, you agree with the ", + "intro-privacy-policy-note2": "privacy policy", + "intro-privacy-statement": "Privacy Statement", + "intro-text": "Status is your gateway to the decentralized web", + "intro-text1": "Chat over a peer-to-peer, encrypted network where messages can’t be censored or hacked", + "intro-text2": "Send and receive digital assets anywhere in the world—no bank account required", + "intro-text3": "Explore games, exchanges and social networks where you alone own your data", + "intro-title1": "Truly private communication", + "intro-title2": "Secure crypto wallet", + "intro-title3": "Decentralized apps", + "intro-wizard-text1": "A set of keys controls your account. Your keys live on your phone, so only you can use them", + "intro-wizard-text2": "One key is for chat. It comes with a readable name that can’t be changed.", + "intro-wizard-text3": "If you own a Keycard, store your keys there for enhanced security.", + "intro-wizard-text4": "Secure and encrypt your keys", + "intro-wizard-text6": "Status will notify you about new messages. You can edit your notification preferences later in settings", + "intro-wizard-title-alt4": "Create a password", + "intro-wizard-title-alt5": "Confirm your password", + "intro-wizard-title1": "Get your keys", + "intro-wizard-title2": "Choose a chat name", + "intro-wizard-title3": "Choose key storage", + "intro-wizard-title4": "Create a 6-digit passcode", + "intro-wizard-title5": "Confirm the passcode", + "intro-wizard-title6": "Enable notifications", + "introduce-yourself": "Introduce yourself with a brief message", + "invalid-address": "It’s not Ethereum address or ENS name", + "invalid-address-qr-code": "Scanned QR code doesn't contain a valid address", + "invalid-characters-bio": "Invalid characters. Standard keyboard characters and emojis only.", + "invalid-ens-or-key": "Invalid ENS or Chat key", + "invalid-format": "Invalid format\nMust be {{format}}", + "invalid-key-confirm": "Apply", + "invalid-key-content": "Database can’t be encrypted because a file is corrupt. Your funds and chat key are safe. Other data, like your chats and contacts, cannot be restored. “{{erase-multiaccounts-data-button-text}}” button, will remove all other data and allows you to access your funds and send messages", + "invalid-key-pair-qr": "This does not look like a key pair QR code", + "invalid-number": "Invalid number", + "invalid-pairing-password": "Invalid pairing password", + "invalid-private-key": "It’s not a valid private key", + "invalid-public-chat-topic": "Invalid public chat topic", + "invalid-qr": "Oops! This QR doesn’t work with Status", + "invalid-range": "Invalid format, must be between {{min}} and {{max}}", + "invalid-username-or-key": "Invalid username or chat key", + "invite": "Invite", + "invite-1-user": "Invite 1 user", + "invite-button": "Invite", + "invite-chat-accept": "Accept", + "invite-chat-accept-join": "Accept and Join", + "invite-chat-intro": "You were referred by a friend to join Status. Here’s some crypto to get you started! Use it to register an ENS name or buy a sticker pack", + "invite-chat-name": "Friend referral", + "invite-chat-pending": "Pending", + "invite-chat-rule": "Accepting will also reward your friend with a crypto referral bonus", + "invite-chat-starter-pack": "Starter Pack", + "invite-contacts": "Invite people from contacts list", + "invite-friend-to-status": "Invite friends to Status", + "invite-friends": "Invite friends", + "invite-friends-and-family": "Invite your friends and family to Status", + "invite-friends-to-status": "Invite friends to Status", + "invite-instruction-fifth": "You can choose to redeem your referral bonus anytime.", + "invite-instruction-first": "You send a unique invite link to your friend to download and join Status", + "invite-instruction-fourth": "You receive your referral bonus and your friend the Starter Pack", + "invite-instruction-second": "Your friend downloads Status and creates an account (on Android)", + "invite-instruction-third": "A chat with your friend is started, where they confirm your referral", + "invite-n-users": "Invite {{count}} users", + "invite-people": "Invite people", + "invite-people-from-contacts": "Invite people from contact list", + "invite-privacy-policy-public": "You installed Status through a referral link. By joining this chat you attribute your referrer and agree to the", + "invite-privacy-policy1": "By accepting you agree to the referral program", + "invite-privacy-policy2": "Terms and Conditions.", + "invite-public-chat-home": "Referral invitation", + "invite-public-chat-intro": "Here’s some crypto to get you started! Use it to register an ENS name or buy a sticker pack", + "invite-receive-account": "Account to receive your referral bonus", + "invite-reward": "Earn crypto for every friend you invite!", + "invite-reward-friend": "Friend: ", + "invite-reward-friend-description": "Your friend will receive a Starter Pack consisting of some {{reward}} to get started", + "invite-reward-friend-name": "Starter Pack", + "invite-reward-you": "You: ", + "invite-reward-you-description": "Invite a friend and receive {{reward}} as referral bonus. Use it to get stickers, an ENS name and try dapps", + "invite-reward-you-name": "Referral bonus", + "invite-select-account": "Select an account to receive your referral bonus", + "invite-to-community": "Invite to community", + "invite-warning": "This promotion is only valid for users of an Android device, who aren't residents of US. Friend needs to confirm referral within 7 days", + "invited": "invited", + "ip-address": "IP address", + "ip-addresses-uuid": "IP addresses and UUID", + "italic": "Italic", + "jan": "Jan", + "join": "Join", + "join-a-community": "or join a community", + "join-as": "Join as {{role}}", + "join-community-to-post": "Join community to post", + "join-group-chat": "Join group", + "join-group-chat-description": "{{username}} invited you to join the group {{group-name}}", + "join-me": "Hey join me on Status: {{url}}", + "join-more-users": "Join {{user1}}, {{user2}} and {{left-count}} more", + "join-new-private-chat": "Start a new private chat", + "join-new-public-chat": "Join a public chat", + "join-one-user": "Join {{user}}", + "join-open-community": "Join Community", + "join-request": "Join request", + "join-two-users": "Join {{user1}} and {{user2}}", + "joined": "Joined", + "joined-community": "You joined “{{community}}”", + "joined-group-chat-description": "You've joined {{group-name}} from invitation by {{username}}", + "joining-token-gated-communities-disabled": "Joining token-gated communities is disabled.", + "jul": "Jul", + "jun": "Jun", + "just-introduce-yourself": "Introduce yourself", + "K": "K", + "keep-card-steady": "Keep the card steady under the phone", + "keep-key": "KeepKey", + "key": "Key", + "key-managment": "Key management", + "key-name-error-emoji": "Emojis are not allowed", + "key-name-error-length": "Key name too long", + "key-name-error-special-char": "Special characters are not allowed", + "key-name-error-taken": "Key pair name already in use", + "key-name-error-too-short": "Key pair name must be at least {{count}} characters", + "key-on-device": "Private key is saved on this device", + "key-pair-erased": "Key pair will be erased from Keycard", + "key-pair-imported-successfully": "{{name}} key pair imported successfully", + "key-pair-migrated-successfully": "Profile key pair successfully migrated", + "key-pair-name-updated": "Key pair name updated", + "key-pair-removed": "Key pair and derived accounts has been removed", + "key-pairs-successfully-imported": "{{count}} key pairs successfully imported", + "keycard": "Keycard", + "keycard-access-reset": "Keycard access is reset", + "keycard-applet-install-instructions": "To install the applet please follow the instructions on https://github.com/status-im/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Try moving the card around to find the NFC reader on your device", + "keycard-awaiting-title": "Still looking...", + "keycard-backup": "Create a backup for your profile Keycard", + "keycard-backup-success-body": "Backup card created successfully. You can now use it with your account just like the primary card.", + "keycard-backup-success-title": "Backup successful", + "keycard-blocked": "Keycard is blocked", + "keycard-can-use-with-new-passcode": "You can use this card with your new passcode", + "keycard-cancel-setup-text": "This will cancel keycard setup. It's highly recommended to finish the setup in order to use keycard. Do you really want to cancel?", + "keycard-cancel-setup-title": "Dangerous operation", + "keycard-connected-description": "Try keeping the card still", + "keycard-connected-title": "Connected", + "keycard-contains-key-pair": "Keycard contains your profile key pair", + "keycard-desc": "Own a Keycard? Store your keys on it; you’ll need it for transactions", + "keycard-documentation": "Keycard is a hardware wallet in a credit-card sized smartcard that works with the Status app. You can store your Status keys and sign transactions securely with it.\n\nKeycard is designed to be mobile-friendly and uses contactless NFC technology to communicate with your phone, so you can sign transactions with just a tap of the card. It is lightweight, convenient, and can fit in your wallet, pocket, or phone case.\n\nProtect your assets and accounts with the Keycard, a cold storage hardware wallet that provides a contactless and convenient experience.", + "keycard-dont-ask-card": "Don't ask for card to sign in", + "keycard-empty": "Keycard is empty", + "keycard-empty-ready": "Keycard is empty and ready to be used", + "keycard-enter-new-passcode": "Enter new passcode {{step}}/2", + "keycard-error-description": "Connect the card again to continue", + "keycard-error-title": "Connection lost", + "keycard-factory-reset": "Return card to factory settings", + "keycard-factory-reset-text": "Performing this will delete any mnemonic phrase stored on the card. Make sure you have a backup of the mnemonic phrase you've been using with this Keycard.", + "keycard-factory-reset-title": "Are you sure you want to perform a factory reset?", + "keycard-free-pairing-slots": "Keycard has {{n}} free pairing slots", + "keycard-full": "Keycard is full", + "keycard-has-keypair": "If Keycard has a profile key pair on it", + "keycard-has-multiaccount-on-it": "This card is full. Each card can hold one main key pair", + "keycard-init-description": "Put the card to the back of your phone to continue", + "keycard-init-title": "Looking for cards...", + "keycard-is-blocked-details": "You can no longer use this card to access or sign for this account. There have been too many failed passcode and PUK attempts.", + "keycard-is-blocked-instructions": "To access your account you will need to factory reset your card. Tap the button below to start the procedure, you will need your mnemonic.", + "keycard-is-blocked-title": "Keycard is blocked", + "keycard-is-frozen-details": "To protect your assets, your card is frozen. Reset your card to unfreeze it and be able to send transactions. You can do this with your PUK or your mnemonic.", + "keycard-is-frozen-factory-reset": "Reset with mnemonic", + "keycard-is-frozen-reset": "Reset with PUK", + "keycard-is-frozen-title": "Keycard is frozen", + "keycard-migration-failed-instruction-1": "Try logging out and logging back in with Keycard; everything might work fine.", + "keycard-migration-failed-instruction-2": "If the key pair hasn’t migrated to Keycard, remove your profile, and recover it using your recovery phrase.", + "keycard-not-empty": "Keycard is not empty", + "keycard-onboarding-finishing-header": "Finishing up", + "keycard-onboarding-intro-header": "Store your keys on Keycard", + "keycard-onboarding-intro-text": "Get ready, this might take a few minutes, but it's important to secure your account", + "keycard-onboarding-mnemonic-text": "You will also need a piece of paper and a pencil to write down your seed phrase.", + "keycard-onboarding-pairing-header": "Pairing the card...", + "keycard-onboarding-pin-text": "You will need to create a 6-digit passcode which will be used to protect access to your Keycard.", + "keycard-onboarding-preparing-header": "Preparing the card...", + "keycard-onboarding-puk-code-header": "Write codes down\n and store them securely", + "keycard-onboarding-recovery-phrase-description": "You need this seed phrase to get your key back. Write it down. Keep it safe, offline, and separate from this device.", + "keycard-onboarding-recovery-phrase-header": "Back up seed phrase", + "keycard-onboarding-recovery-phrase-text": "For your eyes only. This is the magical seed used to generate your key.", + "keycard-onboarding-start-header": "Hold card to the back\n of your phone to start", + "keycard-onboarding-start-step1": "Create a passcode", + "keycard-onboarding-start-step1-text": "Around 1 minute. Create a 6-digit passcode to encrypt your keys", + "keycard-onboarding-start-step2": "Write down PUK and the pairing code", + "keycard-onboarding-start-step2-text": "Around 1 minute. You are going to need a piece of paper and a pencil for that", + "keycard-onboarding-start-step3": "Back up the seed phrase", + "keycard-onboarding-start-step3-text": "Around 1 minute. Also a piece of paper and a pencil are necessary", + "keycard-onboarding-start-text": "And maintain card to phone contact\n during the setup. The setup will take around 4 minutes", + "keycard-pin-change-failed": "Failed to change Keycard PIN", + "keycard-pin-change-failed-description": "Keycard PIN was not updated", + "keycard-pin-changed": "Keycard PIN changed successfully", + "keycard-pin-changed-description": "Now you required to use new PIN for this Keycard", + "keycard-processing-description": "Try keeping the card still", + "keycard-processing-title": "Processing...", + "keycard-recover": "lost or frozen card?", + "keycard-recover-text": "If you have your mnemonic phrase you can create a new Keycard associated with this account. You can use either a new Keycard or perform factory reset on a frozen one.", + "keycard-recover-title": "Create a new card for this account?", + "keycard-recovery-intro-button-text": "Begin recovery", + "keycard-recovery-intro-header": "Recover keys stored on keycard", + "keycard-recovery-intro-text": "If you generated keys using a keycard before and now want to use these keys on this device", + "keycard-recovery-no-key-header": "There’s nothing to \nrecover here", + "keycard-recovery-no-key-text": "Your Keycard has no key stored on it. In order to use it, generate a new key and choose your Keycard to store the key", + "keycard-recovery-phrase-confirm-header": "Confirm seed phrase", + "keycard-recovery-phrase-confirmation-text": "You won’t have a second chance! If you lose access, for example by losing your keycard, you can only access your keys with your seed phrase. No one, but you has your seed phrase. Write it down. Keep it safe.", + "keycard-recovery-phrase-confirmation-title": "Written the seed phrase down?", + "keycard-recovery-success-header": "Your keys have been\n successfully recovered", + "keycard-redeem-title": "Redeem to", + "keycard-redeem-tx": "Redeem assets", + "keycard-redeem-tx-desc": "Tap the card to sign and receive assets", + "keycard-reset-failed": "Failed to reset Keycard", + "keycard-reset-passcode": "Reset passcode", + "keycard-reset-success": "Keycard has been reset", + "keycard-success-description": "You may remove the card now", + "keycard-success-title": "Success", + "keycard-unauthorized-operation": "You're unauthorized to perform this operation.\n Please tap valid card and try again.", + "keycard-unblocked": "Keycard unblocked", + "keycard-unblocked-description": "You’ll need this Keycard to login to Status and transact with derived accounts.", + "keycard-upsell-subtitle": "Enhanced security and convenience", + "keypair-name": "Key pair name", + "keypair-name-description": "Name key pair for your own personal reference", + "keypair-name-input-placeholder": "Collectibles key pair, Old vault....", + "keypair-title": "{{name}}'s default key pair", + "keypairs": "Key pairs", + "keypairs-accounts-and-addresses": "Key pairs, accounts and addresses", + "keypairs-and-accounts": "Key pairs and accounts", + "keypairs-description": "Select key pair to derive your new account from", + "keys-saved": "Keys saved!", + "kicked": "Kicked", + "language": "Language", + "language-and-currency": "Language and currency", + "last-backup-performed": "Last backup performed:", + "last-transaction": "Last transaction", + "last-transaction-is": "Last transaction: {{number}}", + "latest": "Latest:", + "layer-2": "Layer 2", + "learn-more": "Learn more", + "learn-more-about-keycard": "Learn more about Keycard", + "learn-more-keycard": "Learn more about Keycard", + "leave": "Leave", + "leave-chat": "Leave chat", + "leave-chat-confirmation": "Chat history will be removed from your device. After rejoining you won't be able to retrieve any of your history.", + "leave-community": "Leave community", + "leave-community-farewell": "We’ll be sad to see you go but remember, you can come back at any time! All shared addresses will be unshared.", + "leave-community-message": "We’ll be sad to see you go but remember, you can come back at any time!", + "leave-community?": "Leave community?", + "leave-confirmation": "Leave {{chat-name}}", + "leave-group": "Leave group", + "leave-group?": "Leave Group?", + "ledger": "Ledger", + "ledger-live": "Ledger live", + "left": "left", + "left-community": "You left “{{community}}”", + "legacy": "Legacy", + "les-ulc": "LES/ULC", + "lets-go": "Let's go!", + "lifestyle": "Lifestyle", + "light": "Light", + "light-client-enabled": "Light client", + "limit": "Limit", + "link-preview-loading-message": "Generating preview", + "link-to-community": "Link to community", + "link-to-profile": "Link to profile", + "link-to-profile-copied": "Link to Profile copied to clipboard", + "linked-on": "Linked on {{date}}", + "links": "Links", + "load-messages-before": "before {{date}}", + "load-more-messages": "↓ Fetch more messages", + "load-more-timeline": "↓ Fetch more", + "loading": "Loading...", + "local-notifications": "Local notifications", + "local-notifications-subtitle": "Enable background service", + "local-pairing-experimental-mode": "Local Pairing Mode (alpha)", + "lock-app-with": "Lock app with", + "log-in": "Log in", + "log-in-by-syncing": "Log in by syncing", + "log-in-by-syncing-subtitle": "If you have your profile on another device", + "log-in-with-keycard": "Log in with Keycard", + "log-in-with-keycard-subtitle": "If your profile keys are stored on a Keycard", + "log-in-with-recovery-phrase": "Log in with recovery phrase", + "log-in-with-recovery-phrase-subtitle": "If you have your Status recovery phrase", + "log-level": "Log level", + "log-level-settings": "Log level settings", + "log-out-remove": "Log out & Remove profile", + "log-out-remove-profile": "Log out and remove profile from this device", + "logged-in": "Logged in", + "logging": "Logging", + "logging-enabled": "Logging enabled?", + "login-pin-description": "Enter your 6-digit passcode to unlock your keys", + "login-with-keycard": "Log in with this Keycard", + "logout": "Log out", + "logout-app-content": "The account will be logged out. When you unlock it again, the selected network will be used", + "logout-are-you-sure": "Are you sure you want\nto log out?", + "logout-key-management": "You need to log out to access key management.", + "logout-login-keycard": "Log out and login with this Keycard", + "logout-title": "Log out?", + "logs-of-actions-withing-the-app": "Logs of actions within the app, including button presses and screen visits", + "looking-for-cards": "Looking for cards...", + "lost-connection": "Lost connection", + "low-tip": "tip is too low", + "lower-than-average-tip": "lower than average tip", + "M": "M", + "mail-should-be-configured": "Mail client should be configured", + "mailserver-address": "Status node address", + "mailserver-automatic": "Automatic selection", + "mailserver-automatic-switch-explanation": "Choose the fastest Status node available", + "mailserver-connection-error": "Could not connect to Status node", + "mailserver-content": "A node in the Status network that routes and stores messages, for up to 30 days.", + "mailserver-details": "Status node details", + "mailserver-error-content": "The Status node you selected couldn't be reached.", + "mailserver-error-title": "Error connecting to Status node", + "mailserver-format": "enode://{enode-id}@{ip-address}:{port}", + "mailserver-pick-another": "Pick another Status node", + "mailserver-reconnect": "Could not connect to Status node. Tap to reconnect", + "mailserver-request-error-content": "The following error was returned by the Status node: {{error}}", + "mailserver-request-error-status": "An error occured while fetching history, check the logs for details", + "mailserver-request-error-title": "Status node request error", + "mailserver-request-retry": "Retry request", + "mailserver-retry": "Retry", + "mailserver-title": "Status node", + "main-account": "Main account", + "main-currency": "Main currency", + "main-networks": "Main networks", + "main-wallet": "Main Wallet", + "mainnet": "“Ethereum", + "make-admin": "Make admin", + "make-moderator": "Make moderator", + "make-one-it-is-easy-we-promise": "Make one, it’s easy, we promise!", + "make-sure-keycard": "Make sure the card you scanned is a Keycard.", + "make-sure-no-camera-warning": "Make sure no camera or person can see this screen before revealing", + "manage-connections": "Manage connections from within Application Connections", + "manage-keycard": "Manage Keycard", + "manage-keys-and-storage": "Manage keys and storage", + "manage-members": "Manage members", + "manage-networks": "Manage networks", + "manage-tokens": "Manage tokens", + "mar": "Mar", + "mark-all-notifications-as-read": "Mark all notifications as read", + "mark-all-read": "Mark all read", + "mark-as-read": "Mark as read", + "mark-untrustworthy": "Mark as Untrustworthy", + "mark-user-untrustworthy": "Mark {{username}} as untrustworthy", + "market": "Market", + "market-cap": "Market cap", + "master-account": "Master account", + "max": "Max: {{number}}", + "max-2-decimals": "Max. 2 decimals", + "max-active-networks-message": "Disable one of the networks to enable this one", + "max-active-networks-title": "A maximum of {{count}} networks can be enabled simultaneously", + "max-base-fee": "Max base fee", + "max-base-fee-current": "Current: {{current}}", + "max-base-fee-higher": "Higher than necessary (current: {{current}})", + "max-base-fee-lower": "Lower than priority fee: {{priority-fee}}", + "max-base-fee-lower-recommended": "Lower than recommended (current: {{current}})", + "max-fee": "Max fee", + "max-fees": "Max fees", + "max-gas-amount": "Max gas amount", + "max-priority-fee": "Max priority fee", + "max-slippage": "Max slippage", + "max-token": "Max: {{number}} {{token-symbol}}", + "maximum-fee": "Maximum fee", + "maximum-fee-desc": "Maximum overall price for the transaction. If the current block base fee exceeds this, your transaction will be included in a following block with a lower base fee.", + "may": "May", + "maybe-later": "Maybe later", + "member": "Member", + "member-ban": "Ban member", + "member-kick": "Kick member", + "members": { + "one": "1 member", + "other": "{{count}} members" + }, + "members-active": { + "one": "1 member", + "other": "{{count}} members" + }, + "members-active-none": "no members", + "members-count": "{{count}} members", + "members-label": "Members", + "members-limit-reached": "Members limit reached", + "members-title": "Members", + "membership": "Membership", + "membership-approval": "Require approval", + "membership-approval-description": "Your community is free to join, but new members are required to be approved by the community creator first", + "membership-button": "Membership requirement", + "membership-declined": "Membership request was declined", + "membership-description": "Group membership requires you to be accepted by the group admin", + "membership-ens": "Require ENS username", + "membership-ens-description": "Your community requires an ENS username to be able to join", + "membership-free": "No requirement", + "membership-free-description": "Your community is free for anyone to join", + "membership-invite": "Require invite from another member", + "membership-invite-description": "Your community can only be joined by an invitation from existing community members", + "membership-none": "None", + "membership-none-placeholder": "You can require new members to meet certain criteria before they can join. This can be changed at any time", + "membership-request-denied": "Membership request denied", + "membership-request-pending": "Membership request pending", + "membership-requests": "Membership requests", + "membership-requirements-not-met": "Membership requirements not met", + "membership-title": "Membership requirement", + "mention": "Mention", + "mentions": "Mentions", + "message": "Message", + "message-deleted": "Message deleted", + "message-deleted-for-everyone": "Message deleted for everyone", + "message-deleted-for-everyone-count": { + "one": "1 message deleted for everyone", + "other": "{{count}} messages deleted for everyone" + }, + "message-deleted-for-you": "Message deleted for you", + "message-deleted-for-you-count": { + "one": "1 message deleted for you", + "other": "{{count}} messages deleted for you" + }, + "message-not-sent": "Message not sent", + "message-options-cancel": "Cancel", + "message-reply": "Reply", + "messages": "Messages", + "messages-from-contacts-only-subtitle": "Only people you added as contacts can start a new chat with you or invite you to a group", + "messages-gap-warning": "Some messages might be missing", + "might-break": "Might break some ÐApps", + "migrate-key-pair-authorise": "To migrate profile key pair to Keycard you need to authorise with profile password.", + "migrate-key-pair-keycard": "Migrate profile key pair to Keycard", + "migrate-key-pair-keycard-default-key": "{{name}} is your default Status key pair. Migrating this key pair to Keycard will require you to use your Keycard to login to Status and to transact with the key pair’s derived accounts.\n\nKey pair will be removed from device and stored on Keycard.", + "migrate-key-pair-keycard-info": "Re-encrypting your data with your new Keycard login method may take some time, during which you won’t be able to use the app.", + "migration-successful": "Migration successful", + "migration-successful-text": "Account succesfully migrated to Keycard", + "migrations-failed-content": "{{message}}\nschema version: initial {{initial-version}}, current {{current-version}}, last {{last-version}}\n\nA database error occured. Your funds and chat key are safe. Other data, like your chats and contacts, cannot be restored. \"{{erase-multiaccounts-data-button-text}}\" button, will remove all other data and allows you to access your funds and send messages.", + "miners-higher-fee": "Miners will likely inlcude your transaction earlier if you pay a higher fee.", + "minimum-characters": "Minimum {{min-chars}} characters", + "minimum-received": "Minimum received", + "mint": "Mint", + "mo": "Mo", + "mobile": "Mobile", + "mobile-and-wifi": "Mobile and Wi-Fi", + "mobile-data-and-wifi": "Mobile data and Wi-Fi", + "mobile-network-ask-me": "Ask me when on mobile network", + "mobile-network-continue-syncing": "Continue syncing", + "mobile-network-continue-syncing-details": "You can change this later in settings", + "mobile-network-go-to-settings": "Go to settings", + "mobile-network-settings": "Mobile data", + "mobile-network-sheet-configure": "You can configure syncing in more \ndetail in", + "mobile-network-sheet-offline": "No Wi-fi, message syncing disabled.", + "mobile-network-sheet-offline-details": "Syncing using mobile network is off", + "mobile-network-sheet-remember-choice": "Remember my choice", + "mobile-network-sheet-settings": "settings", + "mobile-network-start-syncing": "Start syncing", + "mobile-network-stop-syncing": "Stop syncing", + "mobile-network-stop-syncing-details": "Until connected to Wi-Fi?", + "mobile-network-use-mobile": "Use mobile data", + "mobile-network-use-mobile-data": "Status uses a lot of data when syncing chats and wallet.", + "mobile-network-use-wifi": "Wi-Fi only", + "mobile-syncing-sheet-details": "Status uses a lot of data when syncing chats and wallet.", + "mobile-syncing-sheet-title": "Sync using mobile data?", + "mon": "Mon", + "more": "more", + "more-details": "More details", + "more-details-in-privacy-policy-1": "For more details refer to our ", + "more-details-in-privacy-policy-1-onboarding": "For more details and other cases where we handle your data, refer to our ", + "more-details-in-privacy-policy-2": "Privacy Policy", + "more-details-in-privacy-policy-settings-1": "For details on this and other potential limited data processing by Status, see our ", + "move-and-reset": "Move and Reset", + "move-keystore-file": "Move keystore file", + "move-keystore-file-to-keycard": "Move keystore file to keycard?", + "multiaccount-exists-content": "Keys for this account already exist and can’t be added again. If you’ve lost your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your seed phrase", + "multiaccount-exists-title": "Keys for this account already exist", + "multiaccounts-recover-enter-phrase-text": "Enter 12, 15, 18, 21 or 24 words.\nSeparate words by a single space.", + "multiaccounts-recover-enter-phrase-title": "Enter your seed phrase", + "multichain": "Multichain", + "music": "Music", + "mutal-contacts": "Mutual contacts", + "mute": "Mute", + "mute-channel": "Mute channel", + "mute-chat": "Mute chat", + "mute-chat-capitialized": "Mute Chat", + "mute-community": "Mute community", + "mute-for-1-hour": "For 1 hour", + "mute-for-1-week": "For 7 days", + "mute-for-15-mins": "For 15 min", + "mute-for-8-hours": "For 8 hours", + "mute-group": "Mute group", + "mute-till-unmute": "Until you turn it back on", + "muted-until": "Muted until {{duration}}", + "mutual-contact-requests": "Mutual contact requests", + "my-accounts": "My accounts", + "my-accounts-empty": "Your available accounts will appear here", + "my-albums": "My albums", + "my-profile": "My profile", + "my-status": "My status", + "n-m-people": "{{n}}/{{m}} people", + "n-photos": "{{count}} photos", + "n-users-were-invited": "{{count}} users were invited", + "name": "Name", + "name-ens-or-address": "Name, ENS, or address", + "name-must-differ-error": "Name must differ from other accounts", + "name-of-token": "The name of your token", + "name-optional": "Name (optional)", + "name-updated": "Name updated", + "name-your-channel": "Name your channel", + "name-your-channel-placeholder": "Channel name", + "name-your-community": "Name your community", + "name-your-community-placeholder": "A catchy name", + "name-your-group": "Name your group", + "need-help": "Need help?", + "negative": "Negative", + "network": "Network", + "network-activated": "{{network}} activated.", + "network-activated-and-deactivated": "{{network-activated}} activated. {{network-deactivated}} deactivated.", + "network-behavior": "Network behavior", + "network-chain": "Network chain", + "network-fee": "Network fee", + "network-id": "Network ID", + "network-invalid-network-id": "Specified network id doesn't correspond to network id by RPC url", + "network-invalid-status-code": "Invalid status code: {{code}}", + "network-invalid-url": "Network URL is invalid", + "network-not-supported": "Networks not supported", + "network-preferences": "Network preferences", + "network-preferences-desc-1": "Select which networks this address is happy to receive funds on", + "network-preferences-desc-2": "Select which networks to receive funds on", + "network-settings": "Networks", + "networks": "Networks", + "never": "Never", + "new": "New", + "new-category": "New category", + "new-chat": "New chat", + "new-community-title": "New community", + "new-contact": "New contact", + "new-contract": "New Contract", + "new-device-detected": "New device detected", + "new-device-detected-other-device-message": "Check your other device for a pairing request. Ensure that the this device ID displayed on your other device. Only proceed with pairing and syncing if the IDs are identical.", + "new-device-detected-recovered-device-message": "A new device has been detected. You can see device ID below and on your other device. Only confirm the request if device ID matches.", + "new-favourite": "New favourite", + "new-group": "New group", + "new-group-chat": "New group chat", + "new-group-limit": "You can only add {{max-contacts}} contacts to the group chat", + "new-key-pair-keycard": "New key pair will be stored on Keycard", + "new-messages-header": "New Messages", + "new-network": "New network", + "new-network-info": "{{network}} is now on Status - view your assets, interact with dApps and swap tokens", + "new-password": "New password", + "new-pin-description": "Enter new 6-digit passcode", + "new-public-group-chat": "Join public chat", + "new-puk-description": "Enter new 12-digit PUK", + "new-status": "New status", + "new-tab": "New tab", + "new-to-status": "I’m new to Status", + "new-ui": "New UI", + "news": "News", + "next": "Next", + "next-you-will": "Next, you will be asked to confirm the position of certain words in your recovery phrase", + "NFT": "NFT", + "nickname": "Nickname", + "nickname-added": "Nickname for {{primary-name}} added", + "nickname-description": "Nicknames help you identify others in Status.\nOnly you can see the nicknames you’ve added", + "nickname-is-too-long": "Nickname is too long", + "nickname-removed": "Nickname removed", + "nickname-updated": "Nickname for {{primary-name}} updated", + "nickname-visible-to-you": "Nickname will only be visible to you", + "nine-days": "Nine days", + "no": "No", + "no-activity": "No activity", + "no-addresses-selected": "At least 1 address must be shared with community", + "no-collectibles": "No collectibles", + "no-collectibles-description": "Don't be a bored ape", + "no-communities": "No communities", + "no-communities-description": "go full nyan, find your community", + "no-communities-description-strikethrough": "Never", + "no-contacts": "No contacts", + "no-contacts-description": "This is fine, just invite your mother", + "no-dapps": "No connected dApps", + "no-dapps-description": "We want dApps!", + "no-fees": "No fees", + "no-group-chats": "No group chats", + "no-group-chats-description": "Much fun. Have friends. Wow!", + "no-key-pair-keycard": "There is no key pair on this Keycard", + "no-keycard-applet-on-card": "No Keycard applet on card", + "no-messages": "No messages", + "no-messages-description": "Here’s a cat in a box instead", + "no-one": "No one", + "no-opened-communities": "No opened communities", + "no-opened-communities-description": "But... feels good man", + "no-other-accounts": "No other accounts", + "no-pairing-slots-available": "This card is already paired to 5 devices and cannot pair to this one. Please use one of the paired devices, log in with this card and free up pairing slots on the card", + "no-pending-communities": "No pending communities", + "no-pending-communities-description": "It's toaster", + "no-permissions": "No permissions", + "no-permissions-to-post": "Sorry, you don't have permissions to post in this channel", + "no-pinned-messages": "No pinned messages", + "no-pinned-messages-desc": "Just keep pinning, just keep pinning\nWhat do we do? We pin, pin, pin", + "no-recent-transactions": "No recent transactions", + "no-relevant-tokens": "No relevant tokens", + "no-result": "No results", + "no-routes-found": "No routes found", + "no-routes-found-confirmation": "No routes found. Token type may not be supported or you may don't have enough ETH to cover gas.", + "no-saved-addresses": "No saved addresses", + "no-thanks": "No thanks", + "no-tokens-found": "No tokens found", + "node-address": "Node address", + "node-details": "Node details", + "node-info": "Node info", + "node-version": "Node version", + "nodes-disabled": "Status nodes disabled", + "non-archival-node": "RPC endpoint doesn't support archival requests. Your local transfers history might be incomplete.", + "non-contacts": "Non contacts", + "nonce": "Nonce", + "nonce-higher": "Higher than suggested nonce of {{number}}", + "none": "None", + "normal": "Normal", + "not-a-chatkey": "This is not a chatkey", + "not-applicable": "Not applicable for unsigned transactions", + "not-available": "Not available", + "not-connected-nodes": "Not connected to a status node", + "not-connected-to-peers": "Not connected to any peers", + "not-enough-assets": "Not enough assets to complete transaction", + "not-enough-assets-for-transaction": "Not enough assets for transaction", + "not-enough-assets-to-pay-gas-fees": "Not enough assets to pay gas fees", + "not-enough-liquidity": "Not enough liquidity. Lower token amount or try again later.", + "not-enough-snt": "Not enough SNT", + "not-found": "Not found", + "not-keycard-text": "The card you used is not a Keycard. You need to purchase a Keycard to use it", + "not-keycard-title": "Not a Keycard", + "not-now": "Not now", + "not-paired-with-this-device": "Not paired with this device", + "not-preferred-by-receiver": "Not preferred by receiver", + "not-registered": "not registered", + "nothing-found": "Nothing found", + "notification-settings": "Notification settings", + "notifications": "Notifications", + "notifications-marked-as-read": "{{count}} notifications marked as read", + "notifications-non-contacts": "From non-contacts", + "notifications-non-contacts-description": "Contact requests and group messages", + "notifications-preferences": "Notification preferences", + "notifications-servers": "Notification servers", + "notifications-switch": "Show notifications", + "notifications-transactions": "Wallet transactions", + "notify": "Notify", + "nov": "Nov", + "now": "Now", + "number-of-messages-sent": "Number of messages sent to you", + "oct": "Oct", + "off": "Off", + "off-status-tree": "Off Status tree", + "offline": "Offline", + "offline-community-member": "Offline", + "offline-messaging-use-history-explanation": "Enable Status nodes to fetch messages that were sent while the app was closed. When enabled, a Status node gets your IP address. When disabled you will not receive messages when the app is closed and will not see them when you open the app later.", + "offline-messaging-use-history-nodes": "Use Status nodes", + "ok": "OK", + "ok-continue": "Okay, continue", + "ok-got-it": "Okay, got it", + "ok-save-pass": "OK, save password", + "okay": "Okay", + "on": "on", + "on-capitalized": "On", + "on-device": "On device", + "on-keycard": "On Keycard", + "on-status-tree": "On Status tree", + "on-the-web": "On the web", + "once-enabled-share-metadata": "Once enabled, links posted in the chat may share your metadata with the site", + "one-day": "One day", + "one-month": "One month", + "one-month-int": "1 month", + "one-photo": "1 photo", + "one-time": "One time", + "one-user-was-invited": "1 user was invited", + "one-week": "One week", + "one-week-int": "1 week", + "one-year": "1 year", + "online": "Online", + "online-community-member": "Online", + "online-now": "Online now", + "only-mentions": "Only @mentions", + "oops-not-keycard": "Oops, this isn’t a Keycard", + "oops-this-qr-does-not-contain-an-address": "Oops! This QR does not contain an address", + "oops-wrong-password": "Oops, wrong password!", + "oops-wrong-word": "Oops! Wrong word", + "op-explorer": "OP Explorer", + "open": "Open", + "open-chat": "Open chat", + "open-dapp": "Open ÐApp", + "open-dapp-store": "Discover ÐApps", + "open-home": "Open...", + "open-in-new-tab": "Open in new tab", + "open-membership": "Open membership", + "open-nfc-settings": "Open NFC settings", + "open-on-block-explorer": "Open on block explorer", + "open-status-on-your-other-device": "Open Status on your other device", + "open-your": "Open your", + "opened": "Opened", + "opening-buy-crypto": "Opening {{site}}...", + "opensea": "OpenSea", + "optimal": "Optimal", + "optional": "optional", + "or": "OR", + "or-tap": "or tap", + "origin": "Origin", + "origin-desc": "Origin is where your key pair (your private and public key) comes from. You can generate a new key pair or import an existing private key.", + "origin-header": "Origin", + "os-app-version-bandwidth": "OS, app version and bandwidth usage", + "other": "Other", + "other-options": "Other options", + "outgoing": "Outgoing", + "outgoing-transaction": "Outgoing transaction", + "overview": "Overview", + "own-keycard": "Already own a Keycard?", + "own-your-crypto": "Own your crypto", + "owner": "Owner", + "page-camera-request-blocked": "camera requests blocked. To enable camera requests go to Settings", + "page-would-like-to-use-camera": "would like to use your camera", + "pair": "Pair", + "pair-and-sync": "Pair and Sync", + "pair-card": "Pair to this device", + "pair-code": "Pair code", + "pair-code-explanation": "Pairs card to a different device (up to 5) to unlock keys and sign transactions with the same Keycard", + "pair-code-placeholder": "Pair code...", + "pair-device-toast": "Device successfully paired", + "pair-devices": "Pair devices", + "pair-new-device-and-sync": "Pair new device and sync profile", + "pair-this-card": "Pair this card", + "pair-this-device": "Advertise device", + "pair-this-device-and-sync": "Pair this device and sync profile", + "pair-this-device-description": "Pair your devices to sync contacts and chats between them", + "paired-devices": "Paired devices", + "paired-with-this-device": "Paired with this device", + "pairing": "Pairing", + "pairing-card": "Pairing card", + "pairing-changed": "Pairing code has been changed", + "pairing-code-placeholder": "Pairing code...", + "pairing-code_error1": "Pairing codes don't match.", + "pairing-go-to-installation": "Go to pairing settings", + "pairing-maximum-number-reached-content": "Please disable one of your devices before enabling a new one.", + "pairing-maximum-number-reached-title": "Max number of devices reached", + "pairing-new-installation-detected-content": "A new device has been detected.\nIn order to use your devices correctly, it's important to pair and enable them before using them.\nPlease go to the device section under settings to pair your devices.", + "pairing-new-installation-detected-title": "New device detected", + "pairing-no-info": "No info", + "pairing-please-set-a-name": "Please set a name for your device.", + "pairing-slots-occupied": "All pairing slots are occupied", + "paraswap-error": "Paraswap error: {{paraswap-error}}", + "passphrase": "Passphrase", + "password": "Password", + "password-creation-confirm": "Confirm password", + "password-creation-disclaimer": "I understand my password can't be recovered", + "password-creation-dont-match": "Passwords do not match", + "password-creation-hint": "Minimum 10 characters", + "password-creation-match": "Passwords match", + "password-creation-max-length-hint": "Maximum 100 characters", + "password-creation-placeholder-1": "Type password", + "password-creation-placeholder-2": "Repeat password", + "password-creation-subtitle": "This password can't be recovered", + "password-creation-tips-1": "Lower case", + "password-creation-tips-2": "Upper case", + "password-creation-tips-3": "Numbers", + "password-creation-tips-4": "Symbols", + "password-creation-tips-title": "Tips to improve", + "password-creation-title": "Create profile password", + "password-description": "At least 6 characters. Your password protects your keys. You need it to unlock Status and transact.", + "password-mismatch": "New password and confirmation does not match", + "password-placeholder": "Password...", + "password-placeholder2": "Confirm your password", + "password-reset-in-progress": "Changing password...", + "password-reset-success": "Password changed", + "password-reset-success-message": "You will need to sign in again", + "password_error1": "Passwords don't match.", + "paste": "Paste", + "paste-json": "Paste JSON", + "path-format": "Path format", + "pay": "Pay", + "pay-to-chat": "Pay to chat", + "peer-content": "A device connected to the Status chat network. Each user can represent one or more peers, depending on their number of devices.", + "peer-syncing": "Peer syncing", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Peers", + "peers-count": "Peers count", + "peers-stats": "Peers stats", + "pending": "Pending", + "pending-confirmation": "Pending confirmation...", + "pending-invitations": "Pending membership requests", + "pending-on": "Pending on", + "pending-requests": "Pending requests", + "per-gas-price-limit": "Per-gas price limit", + "per-gas-tip-limit": "Per-gas tip limit", + "perform-backup": "Perform backup", + "permissions": "Permissions", + "phone-e164": "International 1", + "photo": "Photo", + "photo-caps": "PHOTO", + "photo-saved": "Photo saved to your device", + "photos": "Photos", + "photos-access-error": "To grant the required photos permission, please go to your system settings and make sure that Status > Photos is selected.", + "pin": "Pin", + "pin-changed": "6-digit passcode has been changed", + "pin-code": "6-digit passcode", + "pin-limit-reached": "Pin limit reached. Unpin a previous message first.", + "pin-mismatch": "Wrong passcode", + "pin-needed-login-sign": "You’ll need this PIN to login and sign transactions", + "pin-not-match": "PIN do not match", + "pin-one-attempt": " one attempt ", + "pin-one-attempt-blocked-after": "before your Keycard gets blocked", + "pin-one-attempt-blocked-before": "Be careful, you have only", + "pin-one-attempt-frozen-after": "before your Keycard gets frozen", + "pin-one-attempt-frozen-before": "Be careful, you have only", + "pin-retries-left": { + "one": "Wrong PIN, 1 attempt left", + "other": "Wrong PIN, {{count}} attempts left" + }, + "pin-to-channel": "Pin to the channel", + "pin-to-chat": "Pin to the chat", + "Pinned-a-message": "Pinned a message", + "pinned-a-message": "pinned a message", + "pinned-by": "Pinned by", + "pinned-messages": "Pinned messages", + "pinned-messages-2": "Pinned \nmessages", + "pinned-messages-count": { + "one": "1 pinned message", + "other": "{{count}} pinned messages" + }, + "pinned-messages-empty": "Pinned messages will appear here. To pin a message, press and hold it and tap `Pin`", + "podcasts": "Podcasts", + "popular-assets-on-network": "Popular assets on {{network}}", + "popular-currencies": "Popular currencies", + "positive": "Positive", + "powered-by-paraswap": "Powered by Paraswap", + "preference": "Preference", + "preferred-by-receiver": "Preferred by receiver", + "preparing-status-for-you": "Preparing Status for you", + "press": "Press", + "preview-privacy": "Preview privacy mode", + "previewing-may-share-metadata": "Previewing links from these websites may share your metadata with their owners", + "price-impact": "Price impact", + "price-impact-desc": "Estimated price impact for this transaction. If the current block base fee exceeds this, your transaction will be included in a following block with a lower base fee.", + "price-impact-too-high": "Price impact too high. Lower token amount or try again later.", + "principles": "Principles", + "priority": "Priority", + "priority-fee": "Priority fee", + "priority-fee-current": "Current: {{low}} - {{high}}", + "priority-fee-higher": "Higher than necessary: {{low}} - {{high}}", + "priority-fee-higher-max-base": "Higher than max base fee: {{max-base-fee}}", + "priority-fee-lower": "Lower than suggested: {{low}} - {{high}}", + "privacy": "Privacy", + "privacy-and-security": "Privacy and security", + "privacy-mode": "Privacy mode", + "privacy-mode-message-1": "Status uses essential third-party services to make your experience more convenient, efficient, and engaging. While only necessary integrations are included, users who prefer to avoid any external services can disable them entirely - though this may limit functionality and affect usability.", + "privacy-mode-message-2": "Only disable third-party services if you're aware of the trade-offs. Re-enable them anytime in Settings.", + "privacy-mode-more-details": "More details about third-party services", + "privacy-photos": "Profile Photo Privacy", + "privacy-policy": "Privacy policy", + "privacy-show-to-warning": "People who have already seen your profile picture will continue to do so", + "private-key": "Private key", + "private-key-public-address": "Public address of private key", + "private-notifications": "Private notifications", + "private-notifications-descr": "Status will notify you about new messages. You can edit your notification preferences later in settings.", + "proceed-anyway": "Proceed anyway", + "processing": "Just a moment", + "product-information": "Product Information", + "profile": "Profile", + "profile-already-added": "Profile already added", + "profile-bio": "Profile bio", + "profile-deleted-content": "Your profile was successfully deleted", + "profile-deleted-keycard": "You can now restore another key pair on your Keycard", + "profile-deleted-title": "Profile deleted", + "profile-details": "Profile details", + "profile-keys-on-keycard": "If your profile keys are stored on a Keycard", + "profile-linked-keycard": "Profile linked with the key pair on this Keycard has already been added to the device", + "profile-name": "Profile name", + "profile-name-is-too-long": "Profile name is too long", + "profile-not-found": "Profile not found", + "profile-password": "Profile password", + "profile-pic-pick": "Select from gallery", + "profile-pic-remove": "Remove photo", + "profile-pic-take": "Take photo", + "profile-picture-added": "Profile picture added", + "profile-picture-removed": "Profile picture removed", + "profile-picture-updated": "Profile picture updated", + "profiles-on-device": "Profiles on device", + "provider-is-down": "The provider for the following chain(s) is down: {{chains}}", + "public": "Public", + "public-channel": "Public channel", + "public-chat": "Public chat", + "public-chat-description": "Join public chats for your interests! Anyone can start a new one.", + "public-chats": "Public chats", + "public-group-status": "Public", + "public-group-topic": "Topic", + "public-key": "Public key", + "puk-and-pairing-codes-displayed": "PUK and pairing codes displayed", + "puk-changed": "12-digit PUK has been changed", + "puk-code": "PUK code", + "puk-code-explanation": "If you forget your 6-digit passcode or enter it incorrectly 3 times, you'll need this code to unlock your card.", + "puk-mismatch": "Wrong PUK code", + "push-failed-transaction": "Your transaction failed", + "push-failed-transaction-body": "{{value}} {{currency}} to {{to}}", + "push-inbound-transaction": "You received {{value}} {{currency}}", + "push-inbound-transaction-body": "From {{from}} to {{to}}", + "push-notifications-blocked-android": "Android push notifications are disabled.", + "push-notifications-blocked-ios": "iOS push notifications are disabled.", + "push-notifications-server-enabled": "Server enabled", + "push-notifications-servers": "Push notification servers", + "push-outbound-transaction": "You sent {{value}} {{currency}}", + "push-outbound-transaction-body": "From {{from}} to {{to}}", + "quiet-days": "{{quiet-days}} days", + "quiet-hours": "{{quiet-hours}} hours", + "re-encrypt": "Re-encrypt", + "re-encrypt-data": "Re-encrypt your data", + "re-encrypt-key": "Re-encrypt your keys", + "re-encrypting-data": "Re-encrypting data", + "read": "Read", + "read-more": "Read more", + "ready-add-keypair-keycard": "Ready to add key pair to Keycard", + "ready-add-keypair-keycard-documentation": "When you create a Status profile, the Status app generates cryptographic keys for your profile authentication and authorization.\n\nYour Keycard stores the cryptographic keys for your Status profile, permanently offline and isolated from the internet, and also secures your Wallet accounts.", + "ready-generate-keypair": "Ready to generate key pair on Keycard", + "ready-keycard": "Get your Keycard ready", + "ready-to-change-pin": "Ready to change PIN", + "ready-to-import-keypair-keycard": "Ready to import key pair to the Keycard", + "ready-to-migrate-key-pair": "Ready to migrate profile key pair to the Keycard", + "ready-to-scan": "Ready to Scan", + "ready-to-unblock-keycard": "Ready to unblock Keycard", + "rearrange-categories": "Rearrange Categories", + "receive": "Receive", + "receive-at-least": "Receive at least", + "receive-transaction": "Receive transaction", + "received": "Received", + "receiver-networks-warning": "Changing these settings may result in sending tokens to networks the recipient doesn't use", + "recent": "Recent", + "recent-empty": "Recently used addresses will appear here", + "recent-recipients": "Contacts", + "recently-used-stickers": "Recently used stickers will appear here", + "recipient": "Recipient", + "recipient-code": "Enter recipient address", + "recipient-code-placeholder": "0x... or username.domain.eth", + "recipient-gets": "Recipient gets", + "recover": "Recover", + "recover-key": "Access existing keys", + "recover-keycard-multiaccount-not-supported": "Keys for this account already exist and can't be added again. If you've lost your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your seed phrase", + "recover-status-profile": "Recover your Status profile with recovery phrase", + "recover-with-keycard": "Recover with Keycard", + "recover-with-seed-phrase": "Recover with seed phrase", + "recovering-key": "Accessing keys...", + "recovery-confirm-phrase": "Confirm seed phrase", + "recovery-phrase": "Recovery phrase", + "recovery-success-text": "You will have to create a new code or password to re-encrypt your keys", + "recovery-typo-dialog-description": "Please note, your seed phrase must use the exact same words and order as you received it", + "recovery-typo-dialog-title": "Is the seed phrase correct?", + "recurrent": "Recurrent", + "redeem-amount": "{{quantity}} bonuses available", + "redeem-now": "Redeem now", + "redeem-success": "Redeem bonus success!", + "reduced-tip": "priority tip will be reduced", + "refresh": "Refresh", + "registered": "registered", + "registered-keycards": "Registered Keycards", + "reject": "Reject", + "reject-and-delete": "Reject and delete", + "remember-me": "Remember me", + "remind-me-later": "Show me this again", + "remote-notifications": "Remote notifications", + "remote-notifications-subtitle": "Enable google push notifications", + "remove": "Remove", + "remove-account": "Remove account", + "remove-account-confirmation": "I have taken note of the derivation path", + "remove-account-desc": "The account will be removed from all of your synced devices. Make sure you have a backup of your key pair or recovery phrase and derivation path (if it’s not default).", + "remove-account-title": "Remove account", + "remove-address": "Remove address", + "remove-contact": "Remove contact", + "remove-favourite": "Remove favourite", + "remove-from-chat": "Remove from chat", + "remove-from-contacts": "Remove from contacts", + "remove-from-contacts-text": "By removing a user from your contact list you do not hide your wallet address from them", + "remove-group": "Remove group", + "remove-key-pair-and-derived-accounts": "Remove key pair and derived accounts", + "remove-keycard-content": "Remove all content from this Keycard", + "remove-network": "Remove network", + "remove-nickname": "Remove nickname", + "remove-nickname-toast": "You have removed {{secondary-name}}'s nickname", + "remove-phone-case": "Remove your phone case if you have one", + "remove-private-key-address-desc": "The account will be removed from all of your synced devices. Make sure you have a backup of your key pair or recovery phrase.", + "remove-profile-confirm-message": "All profile data will removed from device.", + "remove-profile-message": "Remove profile from this device", + "remove-profile?": "Remove profile?", + "remove-saved-address": "Remove saved address", + "remove-saved-address-description": "Transaction history relating to this address will no longer be labelled ‘{{name}}’.", + "remove-token": "Remove token", + "remove-user-from-group": "Remove {{username}} from the group", + "remove-watched-address-desc": "The watched address will be removed from all of your synced devices.", + "remove-watched-address-title": "Remove watched address", + "removed": "removed", + "removed-from-contacts": "Removed from contacts", + "rename": "Rename", + "rename-key-pair": "Rename key pair", + "repeat-keycard-pin": "Repeat Keycard PIN", + "repeat-new-keycard-pin": "Repeat new Keycard PIN", + "repeat-pin": "Repeat new 6-digit passcode", + "repeat-puk": "Repeat new 12-digit PUK", + "replied": "Replied", + "replies": "Replies", + "replying-to": "Replying to {{author}}", + "report-bug-email-template": "1. Issue Description\n{{description}}\n\n\n2. Steps to reproduce\n{{steps}}\n\n\n3. Attach screenshots that can demo the problem, please\n", + "request-access": "Request access", + "request-feature": "Request a feature", + "request-membership": "Request membership", + "request-pending": "Request pending…", + "request-processed-after-node-online": "Community control node is offline. Request will be processed once it's back online.", + "request-to-join": "Request to join", + "request-to-join-community": "Request to join community", + "request-to-join-community-pending": "Request to join community pending", + "request-to-join-disclaimer": "Joining the community will reveal your public addresses to the owner", + "request-transaction": "Request transaction", + "request-txns-and-message-signing": "Request transactions and message signing", + "requested-to-join-community": "You requested to join “{{community}}”", + "requesting": "Requesting", + "required-field": "Required field", + "resend-message": "Resend", + "reset": "Reset", + "reset-card": "Reset card", + "reset-card-description": "This operation will reset card to initial state. It will erase all card data including private keys. Operation is not reversible.", + "reset-database": "Reset database", + "reset-database-warning": "Delete chats, contacts and settings. Required when you’ve lost your password", + "reset-database-warning-keycard": "Delete chats, contacts and settings.", + "reset-password": "Reset password", + "restore-defaults": "Restore Defaults", + "retake": "Retake", + "retry": "Retry", + "reveal-address": "Reveal address", + "reveal-phrase": "Reveal phrase", + "reveal-qr-code": "Reveal QR code", + "reveal-sync-code": "Reveal sync code", + "review-bridge": "Review bridge", + "review-pairing-request": "Review pairing request", + "review-send": "Review send", + "review-swap": "Review swap", + "revoke-access": "Revoke access", + "ropsten-testnet": "Ropsten Testnet", + "rpc-usage-copy": "Copy", + "rpc-usage-filter": "Filter", + "rpc-usage-filter-methods": "Filter methods", + "rpc-usage-filtered-total": "{{filtered-total}} of {{total}}", + "rpc-usage-get-stats": "Refresh", + "rpc-usage-info": "RPC usage stats", + "rpc-usage-reset": "Reset", + "rpc-usage-total": "Total", + "rss-privacy-warning": "Fetches news from external sites which may expose your IP", + "sa": "Sa", + "safe-estimate": "Safe estimate", + "sat": "Sat", + "save": "Save", + "save-address": "Save address", + "save-bio": "Save bio", + "save-changes": "Save changes", + "save-colour": "Save colour", + "save-image-library": "Save image to library", + "save-image-to-photos": "Save image to Photos", + "save-name": "Save name", + "save-password": "Save password", + "save-password-unavailable": "Set device passcode to save password", + "save-password-unavailable-android": "Save password is unavailable: your device may be rooted or lacks necessary security features.", + "saved": "Saved", + "saved-address-network-preference-selection-description": "Only change if you know which networks the address owner is happy to to receive funds on", + "saved-address-removed": "Saved address removed", + "saved-addresses": "Saved addresses", + "saved-addresses-limit-reached-toast": "Limit of 20 saved addresses reached. Remove a saved address to add a new one.", + "saving-keys-to-device": "Saving keys to device...", + "say-hi": "Say hi", + "scan-an-account-qr-code": "Scan an account QR code", + "scan-an-address-qr-code": "Scan an address QR code", + "scan-empty-keycard": "Scan empty Keycard", + "scan-key-pairs-qr-code": "Scan key pairs QR code", + "scan-keycard": "Scan Keycard", + "scan-keycard-actions": "Scan Keycard to see available actions", + "scan-or-enter-a-sync-code": "Scan or enter a sync code", + "scan-or-enter-sync-code": "Scan or enter sync code", + "scan-or-enter-sync-code-seen-on-this-device": "Scan or enter sync code seen on this device", + "scan-previous-keycard": "Please scan Keycard you previously scanned", + "scan-qr": "Scan QR", + "scan-qr-code": "Scan QR code", + "scan-sync-code": "Scan Sync Code", + "scan-sync-code-placeholder": "cs2:4FH...", + "scan-sync-qr-code": "Scan QR code", + "scan-the-qr-code-or-copy-the-sync-code": "Scan the QR code or copy the sync code", + "scan-tokens": "Scan tokens", + "scan-with-status-app": "Scan with the Status app on another device", + "scanning": "Scanning...", + "scanning-for-activity": "Scanning for activity...", + "search": "Search", + "search-assets": "Search assets", + "search-contacts": "Search contacts", + "search-currencies": "Search currencies", + "search-discover-communities": "Search communities or categories", + "search-no-chat-found": "No search results. Do you mean", + "searching-for-activity": "Searching for activity...", + "secret-keys-confirmation-text": "You will need them to continue to use your Keycard in case you ever lose your phone.", + "secret-keys-confirmation-title": "Written the codes down?", + "secure-wallet-card": "A secure and private cold wallet in a card format", + "security": "Security", + "see-details": "See details", + "see-it-again": "SEE IT AGAIN", + "see-keycard-ready": "Let’s see what’s on your Keycard. Ready?", + "see-recovery-phrase-again": "View recovery phrase", + "see-sticker-set": "See the full sticker set", + "see-suggestions": "See suggestions", + "seed-key-uid-mismatch": "Seed doesn't match", + "seed-key-uid-mismatch-desc-1": "The seed phrase you entered does not match {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "To manage keys for this account verify your seed phrase and try again.", + "seed-phrase-content": "A set of friendly-to-read words, randomly selected from the BIP39 standard list and used to recover or access your Ethereum account on other wallets and devices. Also referred to as a “mnemonic phrase,” “recovery phrase” or “wallet backup” across the crypto ecosystem. Most crypto apps use this same standard to generate accounts.", + "seed-phrase-dont-match": "The phrase you’ve entered not matching key pair that you want to back up", + "seed-phrase-dont-match-migrate-flow": "The phrase you’ve entered does not match your Status profile key pair", + "seed-phrase-error": "Recovery phrase contains invalid words", + "seed-phrase-incorrect": "Recovery phrase does not match key pair", + "seed-phrase-info": "Enter 12, 18 or 24 words separated by spaces", + "seed-phrase-invalid": "Invalid recovery phrase", + "seed-phrase-placeholder": "Type or paste your recovery phrase", + "seed-phrase-title": "Seed Phrase", + "seed-phrase-words-exceeded": "Recovery phrase cannot exceed 24 words", + "seed-phrase-words-uppercase": "Recovery phrase cannot contain uppercase characters", + "select": "Select", + "select-account": "Select account", + "select-account-dapp": "Select the account you wish to use with Dapps", + "select-account-first": "Select an account first", + "select-another-account": "Select another account", + "select-asset": "Select asset", + "select-asset-on": "On", + "select-asset-to-buy": "Select asset to buy", + "select-asset-to-pay": "Select asset to pay", + "select-asset-to-receive": "Select asset to receive", + "select-asset-via": "via", + "select-chat": "Select chat to start messaging", + "select-network": "Select network", + "select-network-for-buying": "Select network for buying", + "select-network-to-bridge-from": "Select network to bridge from", + "select-network-to-receive": "Select network to receive", + "select-networks": "Select networks", + "select-new-location-for-keys": "Select a new location to save your private key(s)", + "select-profile": "Select profile", + "select-token": "Select token", + "select-token-to-receive": "Select token to receive", + "select-token-to-swap": "Select token to Swap", + "selected": "Selected", + "selected-count-from-max": "{{selected}}/{{max}}", + "send": "Send", + "send-community-link": "Send community link", + "send-contact-request": "Send contact request", + "send-contact-request-message": "To start a chat you need to become contacts", + "send-from-network": "Send from {{network}}", + "send-limit": "Max: {{limit}}", + "send-logs": "Report a bug", + "send-logs-to": "Report a bug to {{email}}", + "send-message": "Send message", + "send-push-notifications": "Send Push Notifications", + "send-push-notifications-description": "When disabled, the person receiving your messages won't be notified of their arrival", + "send-reply": "Send reply", + "send-request": "Send request", + "send-request-amount": "Amount", + "send-request-amount-max-decimals": "Max number of decimals is {{asset-decimals}}", + "send-request-unknown-token": "Unknown token - {{asset}}", + "send-sending-to": "to {{recipient-name}}", + "send-to": "Send to", + "send-to-this-address": "Send to this address", + "send-to-user": "Send to {{user}}", + "send-transaction": "Send transaction", + "sending": "Sending", + "sending-to-networks-the-receiver-does-not-prefer": "Sending to networks the receiver does not prefer may result in recipient having difficulty accessing the sent tokens.", + "sending-to-unpreferred-networks": "Sending to unpreferred networks", + "sending-with-elipsis": "Sending...", + "sending-with-ellipsis": "Sending...", + "sent": "Sent", + "sent-a-gif": "Sent a GIF", + "sent-a-photo": "Sent a photo", + "sent-a-sticker": "Sent a Sticker", + "sent-at": "Sent at", + "sent-audio-message": "Sent audio message", + "sent-n-photos": "Sent {{number}} photos", + "sep": "Sep", + "sepolia-active": "Sepolia active", + "server": "Server", + "set-a-topic": "Create a topic", + "set-currency": "Set currency", + "set-custom-fee": "Set custom fee", + "set-dapp-access-permissions": "Set DApp access permissions", + "set-max": "Set max", + "set-nickname-toast": "You have renamed {{primary-name}} as {{nickname}}", + "set-pre-login-log-level": "Set pre-login log", + "set-spending-cap": "Set spending cap", + "set-spending-cap-of": "Set spending cap of", + "set-up-sync": "Set up sync", + "set-your-own-fees-and-nonce": "Set your own fees and nonce", + "settings": "Settings", + "setup-group-chat": "Setup group chat", + "setup-keycard": "Setup Keycard", + "setup-keycard-description": "Set up your Keycard to log in, sign transactions, create backups and much more.", + "setup-syncing": "Pair devices to sync", + "share": "Share", + "share-account": "Share account", + "share-address": "Share address", + "share-address-title": "{{address}} address", + "share-all-current-and-future-addresses": "Share all current and future addresses", + "share-channel": "Share channel", + "share-chat": "Share chat", + "share-collectible": "Share collectible", + "share-community": "Share community", + "share-contact-code": "Share my chat key", + "share-dapp-text": "Check out this DApp I'm using on Status: {{link}}", + "share-details": "Share details", + "share-image": "Share image", + "share-invite-link": "Share an invite link", + "share-link": "Share link", + "share-link-to-block-explorer": "Share link to {{block-explorer-name}}", + "share-logs": "Share logs", + "share-my-profile": "Share my profile", + "share-opensea-link": "Share OpenSea link", + "share-profile": "Share profile", + "share-profile-link": "Share profile link", + "share-public-chat-text": "Check out this public chat on the Status app: {{link}}", + "share-usage-data": "Share usage data", + "share-usage-data-with-status": "Share usage data with Status", + "shared": "Shared", + "shared-a-community": "Shared a community", + "sharing-copied-to-clipboard": "Copied", + "sharing-copy-to-clipboard": "Copy", + "sharing-data-desc-1": "Data is validated against public rules to ensure no sensitive data is sent. Don’t trust, verify.", + "sharing-data-desc-2": "Usage data is sent end-to-end encrypted over Status’ peer-to-peer network", + "sharing-data-desc-3": "Instead of your regular chat key, a single use key is used", + "sharing-data-desc-4": "Usage data cannot be associated with your IP address", + "sharing-data-desc-5": "Cumulative data of all users is publicaly available", + "sharing-data-desc-6": "The data is removed from your phone after they are sent", + "sharing-share": "Share", + "sharing-usage-data-can-be-turned-off": "Sharing usage data can be turned off anytime in Settings / Privacy and Security.", + "shell-placeholder-subtitle": "Jump between your communities, messages,\nwallet accounts and browser tabs", + "shell-placeholder-title": "Your open tabs will be here", + "show-address-qr": "Show address QR", + "show-encrypted-qr-of-key-pair": "Show encrypted QR of key pair", + "show-existing-keys": "Show Existing Keys", + "show-less": "Show less", + "show-more": "Show more", + "show-network-balances": "Show network balances", + "show-notifications": "Show notifications", + "show-profile-pictures": "See profile pictures from", + "show-profile-pictures-to": "Show your profile picture to", + "show-qr": "Show QR code", + "show-transaction-data": "Show transaction data", + "showcase": "Showcase", + "sign transactions": "sign transactions", + "sign-and-send": "Sign and send", + "sign-anyway": "Sign anyway", + "sign-in": "Sign in", + "sign-in-by-syncing": "Sign in by syncing", + "sign-message": "Sign Message", + "sign-out": "Sign out", + "sign-request-failed": "Could not sign message", + "sign-up": "Sign up", + "sign-with": "Sign with", + "sign-with-password": "Sign with password", + "sign-you-in": "Signing you in…", + "signing": "Signing", + "signing-a-message": "Signing a message", + "signing-in-from-another-device": "Signing in from the other device", + "signing-phrase": "Signing phrase", + "simple": "Simple", + "skip": "Skip", + "slide-to-authorise": "Slide to authorise", + "slide-to-bridge": "Slide to bridge", + "slide-to-create-account": "Slide to create account", + "slide-to-import": "Slide to import", + "slide-to-remove-key-pair": "Slide to remove key pair", + "slide-to-request-to-join": "Slide to request to join", + "slide-to-reveal-code": "Slide to reveal code", + "slide-to-reveal-qr-code": "Slide to reveal QR code", + "slide-to-send": "Slide to send", + "slide-to-sign": "Slide to sign", + "slide-to-swap": "Slide to swap", + "slippage": "Slippage", + "slippage-cant-be-more-than-30": "Slippage can't be more than 30", + "slippage-may-be-higher-than-necessary": "Slippage may be higher than necessary", + "slippage-settings": "Slippage settings", + "slippage-settings-description": "Your transaction will revert if the price changes more than the slippage percentage", + "slippage-should-be-more-than-0": "Slippage should be more than 0", + "slow": "Slow", + "something-about-you": "Something about you", + "something-didnt-go-as-planned": "Something didn't go as planned", + "something-went-wrong": "Something went wrong", + "something-went-wrong-please-try-again-later": "Something went wrong. Modify swap parameters or try again later.", + "soon": "Soon", + "sort-communities": "Sort communities", + "special-characters": "Special characters", + "specify-address": "Specify address", + "specify-name": "Specify a name", + "specify-network-id": "Specify network id", + "specify-server-public-key": "Enter server public key", + "specify-symbol": "Specify a symbol", + "spender-contract": "Spender contract", + "spending-cap": "Spending cap", + "spending-cap-failed": "Spending cap failed: {{token-amount}} {{token-symbol}} for {{provider-name}} in {{account-name}}", + "spending-cap-set": "Spending cap set: {{token-amount}} {{token-symbol}} for {{provider-name}} in {{account-name}}", + "start-chat": "Start chat", + "start-conversation": "Start conversation", + "start-fresh": "Start fresh", + "start-fresh-subtitle": "Create a new profile from scratch", + "start-group-chat": "Start group chat", + "start-new-chat": "Start new chat", + "start-using-status": "Start using Status", + "start-with-space": "Cannot start with space", + "starter-pack-coming": "Starter Pack coming your way", + "starter-pack-coming-description": "Can take a few minutes to hours", + "starter-pack-received": "Starter Pack received", + "starter-pack-received-description": "Here’s some crypto to get you started! Use it to get stickers, an ENS name and try dapps", + "status": "Status", + "status-always-online": "Always Online", + "status-automatic": "Automatic", + "status-automatic-subtitle": "Set status automatically", + "status-confirmed": "Confirmed", + "status-dnd": "Do not disturb", + "status-dnd-subtitle": "Mutes all notifications", + "status-hardwallet": "Status hardwallet", + "status-help": "Status Help", + "status-inactive": "Inactive", + "status-inactive-subtitle": "Hides your online status", + "status-is-a-secure-messaging-app": "Status is a secure messaging app, crypto wallet and web3 browser built with the state of the art technology", + "status-is-open-source": "Status is open-source", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status tends to use a lot of data when syncing chats. You can choose not to sync when on mobile network", + "status-news-rss": "Receive Status News via RSS", + "status-not-sent-click": "Not confirmed. Click for options", + "status-not-sent-tap": "Not confirmed. Tap for options", + "status-pending": "Pending", + "status-sent": "Sent", + "status-tx-not-found": "TX not found", + "status-updates-descr": "Status updates will appear here. Add the profile as a contact to receive updates on your timeline.", + "statuses-descr": "Share what’s on your mind and stay updated with your contacts", + "statuses-my-status-descr": "Share what’s on your mind. Anyone visiting your profile will be able to see your status. People who add you as their contact will receive your updates on their timeline", + "step-i-of-n": "Step {{step}} of {{number}}", + "sticker": "Sticker", + "sticker-market": "Sticker market", + "storage": "Storage", + "store-confirmations": "Store confirmations", + "store-key-pair-on-keycard": "Store your existing key pair on Keycard", + "store-your-asets-on-keycard": "Store your assets on Keycard", + "strength-divider-okay-label": "Okay", + "strength-divider-strong-label": "Strong", + "strength-divider-very-strong-label": "Very strong", + "strength-divider-very-weak-label": "Very weak", + "strength-divider-weak-label": "Weak", + "strikethrough": "Strikethrough", + "su": "Su", + "submit": "Submit", + "submit-bug": "Submit a bug", + "success": "Success", + "successful-connection": "Successful Connection", + "successful-messages-rate": "Rate of successfully sent messages", + "suggested-min-tip": "Suggested min. tip", + "suggested-price-limit": "Suggested price limit", + "sun": "Sun", + "swap": "Swap", + "swap-details": "Swap details", + "swap-exchange-rate-in-crypto": "1 {{pay-token-symbol}} ≈ {{exchange-rate}} {{receive-token-symbol}}", + "swap-failed": "Swap failed", + "swapped-to": "Swapped {{pay-amount}} {{pay-token-symbol}} to {{receive-amount}} {{receive-token-symbol}}", + "swapping-to": "Swapping {{pay-amount}} {{pay-token-symbol}} to {{receive-amount}} {{receive-token-symbol}}", + "swaps-powered-by": "Powered by {{provider}}", + "switch-to-simple-interface": "Switch to simple interface", + "symbol": "Symbol", + "sync-all-devices": "Sync all devices", + "sync-and-backup": "Sync and backup", + "sync-code": "Sync Code", + "sync-code-generated": "Sync code generated", + "sync-devices-complete-sub-title": "Your devices are now in sync", + "sync-devices-complete-title": "Device sync complete!", + "sync-devices-error-sub-title": "Try again and double-check the instructions", + "sync-devices-error-title": "Oops, something’s wrong!", + "sync-devices-result-sub-title": "Your devices are now in sync", + "sync-devices-sub-title": "Please keep both devices switched on and connected to the same network until sync is complete.", + "sync-devices-title": "Syncing devices...", + "sync-in-progress": "Syncing...", + "sync-new-device": "Sync new device", + "sync-or-recover-profile": "Sync or recover profile", + "sync-profile-title": "Syncing profile...", + "sync-settings": "Sync settings", + "sync-synced": "In sync", + "sync-your-profile": "Sync your profile", + "synced-devices": "Synced Devices", + "synced-with": "Synced with", + "synchronise-your-data-across-your-devices": "Synchronise your data across your devices", + "syncing": "Syncing", + "syncing-connection-message": "App uses a lot of data when syncing and making backups. If you have a limited data plan, consider syncing over Wi-Fi only. ", + "syncing-devices": "Syncing...", + "syncing-wifi-connection-warning": "If you choose to sync over Wi-Fi only, messages sent to you while you are offline will be delivered once you connect to Wi-Fi.", + "system": "System", + "tabs": "Tabs", + "tag-was-lost": "Tag was lost", + "tap": "Tap", + "tap-card-again": "Tap the card to the back of your phone again", + "tap-on": "Tap on", + "terms-and-conditions": "Terms and Conditions", + "terms-of-service": "Terms of Use", + "testnet-mode": "Testnet mode", + "testnet-mode-disable-description": "Are you sure you want to turn off Testnet mode? All future transactions will be performed on live networks with real funds.", + "testnet-mode-enable-description": "In this mode, all blockchain data displayed will come from testnets and all blockchain interactions will be with testnets.\nTestnet mode switches the entire app to using testnets only. Please switch this mode on only if you know exactly why you need to use it.", + "testnet-mode-enabled": "Testnet mode enabled", + "testnet-mode-prompt-content": "You are about to switch the network mode. This will log you out and you will have to login again.", + "testnet-mode-prompt-title": "Warning!", + "testnet-not-available": "Testnet not available", + "text-input-disabled": "Please wait a moment...", + "th": "Th", + "thank-you": "Thank you", + "the-key-pair-and-derived-accounts-will-be-removed": "The key pair and derived accounts will be removed from all of your synced devices. Make sure you have a backup of your keys or seed phrase before proceeding.", + "third-party-services": "Third-party services ", + "this-account-has-no-activity": "This account has no activity", + "this-address-has-activity": "This address has activity", + "this-address-has-no-activity": "This address has no activity", + "this-address-is-already-saved": "This address is already saved", + "this-device": "This device", + "this-device-desc": "Your keys will be encrypted and securely stored on your device", + "this-ens-name-is-not-registered-yet": "This ENS name is not registered yet", + "this-is-not-an-eth-address-or-ens-name": "This is not an Ethereum address or ENS name", + "this-is-you-signing": "This is your signing phrase", + "this-message-was-deleted": "This message was deleted", + "this-qr-does-not-contain-any-missing-key-pair": "This QR does not contain any missing key pairs", + "this-qr-does-not-contain-key-pair": "This QR does not contain {{name}} key pair", + "this-will-take-few-seconds": "This will take a few seconds", + "three-days": "Three days", + "three-months-int": "3 months", + "three-words-description": "You should see these 3 words before signing each transaction", + "three-words-description-2": "If you see a different combination, cancel the transaction and sign out", + "thu": "Thu", + "time-24h": "24h", + "time-in-sec": "{{seconds}} sec", + "timeline": "Timeline", + "tip-cap": "Tip cap", + "tips-scan-keycard": "Tips to scan your Keycard", + "to": "to", + "to-block": "Block", + "to-capitalized": "To", + "to-enable-biometric": "To enable {{bio-type-label}}, you must save your password on the unlock screen", + "to-encrypt-enter-password": "To encrypt the account please enter your password", + "to-label": "To", + "to-pair-with-your-other-device-in-the-network": "To pair with your other device in the network", + "to-scan-a-qr-enable-your-camera": "To scan a QR, enable your camera", + "to-see-this-message": "To see this message,", + "token": "Token", + "token-auto-validate-decimals-error": "Wrong decimals for token {{symbol}} at address {{address}} - set to {{expected}} but detected as {{actual}}", + "token-auto-validate-name-error": "Wrong name for token {{symbol}} at address {{address}} - set to {{expected}} but detected as {{actual}}", + "token-auto-validate-symbol-error": "Wrong symbol for token {{symbol}} at address {{address}} - set to {{expected}} but detected as {{actual}}", + "token-details": "Token details", + "token-gated-communities": "Token-gated communities", + "token-gated-communities-info": "If you want to join a token-gated community, you need to meet the token requirements set by the community Owner.\n\nThe Status app automatically checks if you meet the community requirements. If you meet the requirements, the option to join the community is available.", + "token-master": "Token Master", + "token-not-available-on-networks": "{{token-symbol}} is not available on {{networks}}.", + "token-not-available-on-receiver-networks": "{{token-symbol}} is not available on the recipient’s desired networks. Proceed with caution.", + "token-owner": "Token Owner", + "token-price": "Token Price", + "topic-name-error": "Use only lowercase letters (a to z), numbers & dashes (-). Do not use chat keys", + "total-gas": "Total gas", + "total-members": "Total members", + "traits": "Traits", + "transacation-finalised": "Transaction finalised!", + "transaction": "Transaction", + "transaction-confirmed": "Transaction confirmed!", + "transaction-data": "Transaction data", + "transaction-declined": "Transaction declined", + "transaction-description": "Consider it complete after 12 confirmations on the network.", + "transaction-details": "Transaction details", + "transaction-failed": "Transaction failed", + "transaction-fee": "Transaction fee", + "transaction-hash-copied-to-clipboard": "Transaction hash copied to clipboard", + "transaction-history": "Transaction history", + "transaction-request": "Transaction Request", + "transaction-sent": "Transaction sent", + "transaction-settings": "Transaction settings", + "transaction-signed": "The transaction has been successfully signed", + "transactions": "Transactions", + "transactions-filter-select-all": "Select all", + "transactions-filter-title": "Filter history", + "transactions-history": "Transaction history", + "transactions-history-empty": "No transactions in your history yet", + "transactions-history-loading": "Loading transaction history. This might take a while.", + "transactions-load-more": "Load more", + "transactions-management-enabled": "Transaction management (alpha)", + "transactions-sign": "Sign", + "transfer-ma-unknown-error-desc-1": "It looks like your multiaccount was not deleted. Database may have been reset", + "transfer-ma-unknown-error-desc-2": "Please check your account list and try again. If the account is not listed go to Access existing keys to recover with seed phrase", + "transfers-fetching-failure": "Transfers history could not be updated. Check your connection and pull down to try again", + "tribute-required-by-multiaccount": "{{multiaccount-name}} requires SNT to start a chat.", + "tribute-state-paid": "Tribute paid", + "tribute-state-pending": "Tribute pending", + "tribute-state-required": "Requires {{snt-amount}} SNT tribute", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Add friends as a contact to allow chats without tribute payment.", + "tribute-to-talk-are-you-friends": "Are you friends?", + "tribute-to-talk-ask-to-be-added": "Ask to be added as a contact", + "tribute-to-talk-contact-received-your-tribute": " received your tribute. You can now securely chat with each other.", + "tribute-to-talk-desc": "Monetize your attention by requiring SNT for new people to start a chat", + "tribute-to-talk-disabled": "Tribute to Talk disabled", + "tribute-to-talk-disabled-note": "From now on, new people can start a chat with you without sending SNT.", + "tribute-to-talk-enabled": "You have Tribute to Talk enabled.", + "tribute-to-talk-finish-desc": "From now on, you will only receive chats from contacts, and people who paid ", + "tribute-to-talk-learn-more-1": "Your time and attention are your most valuable assets. Tribute to Talk lets you set an amount of SNT required for new people to start a chat with you.", + "tribute-to-talk-learn-more-2": "Anyone who is not in your contact list will be asked to pay, and you can respond once they have.", + "tribute-to-talk-learn-more-3": "You can always send the money back, but to ensure that friends can reach you freely, add them as a contact first.", + "tribute-to-talk-paywall-learn-more-1": "Our time and attention are our most valuable assets. Tribute to Talk lets you contact new people in exchange for an SNT payment.", + "tribute-to-talk-paywall-learn-more-2": "To start a chat with someone who has a tribute set, simply pay the required SNT and you will be added as a contact.", + "tribute-to-talk-paywall-learn-more-3": "If you know them, you can share your profile outside of Status to be added for free.", + "tribute-to-talk-pending": "Tribute pending confirmation", + "tribute-to-talk-pending-note": "Tribute transaction is pending confirmation on the network. You can check its status in transaction history", + "tribute-to-talk-removing-note": "Removing Tribute to Talk will allow new people to start a chat without sending SNT. Requires a transaction to be made.", + "tribute-to-talk-set-snt-amount": "Set the amount of SNT required for new people to start a chat", + "tribute-to-talk-signing": "Waiting to sign transaction", + "tribute-to-talk-transaction-failed-note": "Transaction has failed and your Tribute to Talk settings have not been changed", + "tribute-to-talk-tribute-received1": "Tribute received. You and ", + "tribute-to-talk-tribute-received2": " are now contacts and can securely chat with each other.", + "tribute-to-talk-you-require-snt": "You require SNT for new people to start a chat.", + "trip-accounts": "Trip accounts", + "try-again": "Try again", + "try-keeping-the-card-still": "Try keeping the card still", + "try-to-search-something-else": "Try to search something else", + "try-with-different-currency": "Try with a different currency", + "try-your-luck-again": "Try your luck again!", + "tu": "Tu", + "tue": "Tue", + "turn-nfc-description": "NFC is disabled on yor device. You can enable it in settings", + "turn-nfc-on": "Turn NFC on to continue", + "turn-off-testnet-mode": "Turn off testnet mode", + "turn-on-testnet-mode": "Turn on testnet mode", + "two-minutes": "two minutes", + "tx-fail-description1": "This transaction is likely to fail. Sign at your own risk using custom network fee.", + "tx-fail-description2": "This transaction is likely to fail. Set a custom network fee to sign at your own risk.", + "type": "Type", + "type-a-message": "Message", + "type-nickname": "Type nickname", + "type-nonce": "Type nonce", + "type-pairing-code": "Type or paste pairing code", + "type-slippage": "Type slippage", + "type-some-chat-key": "zQ3...1sgt5N", + "type-something": "Type something", + "type-your-password": "Type your password", + "type-your-path": "Type your own derivation path", + "ulc-enabled": "ULC enabled", + "unable-to-fetch": "Unable to fetch chat history", + "unable-to-read-this-code": "Unable to read this code", + "unable-to-send-messages": "Unable to send and receive messages", + "unblock": "Unblock", + "unblock-contact": "Unblock this user", + "unblock-keycard": "Unblock Keycard", + "unblock-keycard-instructions": "You’ll be asked for recovery phrase. After unblocking you’ll be able to continue using Keycard to log in to Status and sign transactions on key pair derived accounts.", + "unblock-keycard-recovery": "Unblock Keycard with recovery phrase", + "unblocking-a-user-message": "After unblocking {{username}}, you will be able to connect with them as a contact and see their messages in group chats and communities.", + "understanding-bonder-fees-description": "The bridged amount can be lower than the amount you send due to Bonder fees. These fees cover the cost of providing liquidity and managing risks associated with your transfer. The fee ranges from 0.05% to 0.30%, depending on factors such as transaction volume and route specifics.", + "understanding-bonder-fees-title": "Understanding Bonder fees", + "undo": "Undo", + "unique-identifiers": "Unique profile identifiers", + "universally-unique-identifiers-of-device": "Universally Unique Identifiers of device", + "unknown": "Unknown", + "unknown-status-go-error": "Unknown status-go error", + "unlimited": "Unlimited", + "unlock": "Unlock", + "unlock-reset-instructions": "To unlock or factory reset the Keycard, please use the Status desktop app. If you'd like this feature on mobile, feel free to upvote them and discuss in the Status community.", + "unmute": "Unmute", + "unmute-channel": "Unmute channel", + "unmute-chat": "Unmute chat", + "unmute-community": "Unmute community", + "unmute-group": "Unmute group", + "unpair": "Unpair", + "unpair-card": "Unpair card", + "unpair-card-confirmation": "This operation will unpair card from current device. Requires 6-digit passcode authorization. Do you want to proceed?", + "unpair-device": "Unpair device?", + "unpair-device-description": "This device will no longer synchronize with {{name}} until you pair them again.", + "unpair-device-toast": "Device successfully unpaired", + "unpair-keycard": "Unpair Keycard from this phone", + "unpair-keycard-warning": "Your pairing code/PUK & PIN remain unchanged", + "unpaired-keycard-text": "The Keycard you tapped is not associated with this phone", + "unpaired-keycard-title": "Looks like your card has been unpaired", + "unpin": "Unpin", + "unpin-from-channel": "Unpin from the channel", + "unpin-from-chat": "Unpin from the chat", + "unread": "Unread", + "unsupported-file": "Unsupported file", + "until": "until", + "until-you-turn-it-back-on": "you turn it back on", + "untrustworthy": "Untrustworthy", + "update": "Update", + "update-account-name": "Update account name", + "update-group-chat": "Update group chat", + "update-nickname-title": "Update nickname", + "update-to-listen-audio": "Update to latest version to listen to an audio message here!", + "update-to-see-image": "Update to latest version to see a nice image here!", + "update-to-see-sticker": "Update to latest version to see a nice sticker here!", + "updates-to-tos": "Updates to Terms of Use", + "updates-to-tos-desc": "Before you continue, please review the Terms of Use and confirm you take full responsibility for how you use the app.", + "upvote-it": "upvote it", + "urgent": "Urgent", + "url": "URL", + "usage-data-shared-from-all-profiles": "Usage data will be shared from all profiles added to device. ", + "usd-currency": "USD", + "use-a-recovery-phrase": "Use a recovery phrase", + "use-a-recovery-phrase-subtitle": "If you already have an Ethereum wallet", + "use-an-empty-keycard": "Use an empty Keycard", + "use-an-empty-keycard-subtitle": "Store your new profile keys on Keycard", + "use-as-profile-picture": "Use as profile picture", + "use-backup-keycard": "Use as backup Keycard", + "use-biometrics": "Use biometrics to fill in your password", + "use-keycard": "Use Keycard", + "use-keycard-for-status": "From now on, use this Keycard to login to Status and transact with derived accounts. To start using it, logout and log back in with this Keycard.", + "use-keycard-instead-password": "Start using Keycard instead of password", + "use-keycard-login-sign": "Use this Keycard to sign in and authorize transactions", + "use-keycard-subtitle": "Keys will be stored on your Keycard", + "use-keypair-keycard": "Use the key pair stored on Keycard", + "use-photo": "Use Photo", + "use-recovery-phrase": "Use recovery phrase", + "use-the-multichain-wallet": "Multi-chain wallet for swaps and buys", + "use-valid-contact-code": "Please enter or scan a valid chat key or username", + "user-blocked": "{{username}} blocked", + "user-deleted-a-message": "{{user}} deleted a message", + "user-found": "User found", + "user-keycard": "{{name}} Card", + "user-keypair": "{{name}}'s Key pair", + "user-pinned-a-message": "{{user}} pinned a message", + "user-replied": "{{user}} replied", + "user-sent-a-gif": "{{user}} sent a GIF", + "user-sent-a-photo": "{{user}} sent a photo", + "user-sent-a-sticker": "{{user}} sent a Sticker", + "user-sent-audio-message": "{{user}} sent audio message", + "user-sent-n-photos": "{{user}} sent {{number}} photos", + "user-shared-a-community": "{{user}} shared a community", + "user-unblocked": "{{username}} unblocked", + "valid-for-time": "Valid for {{valid-for}}", + "validation-amount-invalid-number": "Amount is not a valid number", + "validation-amount-is-too-precise": "Amount is too precise. Max number of decimals is {{decimals}}.", + "value-higher-than-send-amount": "This value is higher than entered amount to send", + "verified-community": "✓ Verified community", + "version": "App version", + "via": "via", + "via-card-or-bank": "Via card or bank", + "view": "View", + "view-address-on-block-explorer": "View address on {{block-explorer-name}}", + "view-channel-members-and-details": "View channel members and details", + "view-community-rules": "View Community Rules", + "view-cryptokitties": "View in CryptoKitties", + "view-cryptostrikers": "View in CryptoStrikers", + "view-data": "View data", + "view-details": "View Details", + "view-etheremon": "View in Etheremon", + "view-gitcoin": "View in Gitcoin", + "view-members": "View members", + "view-on-block-explorer": "View on {{block-explorer-name}}", + "view-on-opensea": "View on OpenSea", + "view-on-status-explorer": "View on Status Explorer", + "view-pinned-messages": "View pinned messages", + "view-profile": "View profile", + "view-public-dashboard": "View public dashboard", + "view-rules": "View rules", + "view-signing": "View signing phrase", + "view-superrare": "View in SuperRare", + "view-token-gating": "View token requirements", + "visit-dapp": "Visit dApp", + "vote-to-feature": "Vote to feature this community", + "waiting-for-wifi": "No Wi-fi, message syncing disabled.", + "waiting-for-wifi-change": "Settings", + "waiting-to-sign": "Waiting to sign transaction...", + "waiting-wi-fi": "Waiting for Wi-Fi…", + "waku-backup": "Waku backup", + "waku-backup-enabled": "Backup enabled", + "waku-bloom-filter-mode": "Waku bloom filter mode", + "wakuv2-change-nodes": "Are you sure you want to change Wakuv2 nodes?", + "wakuv2-node-format": "/ip4/{node-ip}/tcp/{port}/p2p/{id}", + "wakuv2-settings": "Waku v2 settings", + "wallet": "Wallet", + "wallet-activity-beta-message": "Activity is in beta. If transactions are missing, check", + "wallet-address": "Wallet address", + "wallet-asset": "Asset", + "wallet-assets": "Assets", + "wallet-backup-recovery-title": "Back up your seed phrase", + "wallet-choose-recipient": "Choose Recipient", + "wallet-collectibles": "Collectibles", + "wallet-connect": "Wallet Connect", + "wallet-connect-2.0": "Wallet Connect 2.0", + "wallet-connect-app-connected": "is connected", + "wallet-connect-connections-error": "Failed to get dApps connections", + "wallet-connect-go-back": "Go back to your browser or dapp", + "wallet-connect-label": "WalletConnect", + "wallet-connect-message-request-success-toast": "Message signed, go back to the dApp", + "wallet-connect-networks-not-supported": "{{dapp}} requires an unsupported network.", + "wallet-connect-no-internet-warning": "Oops, you have no internet. Try again later!", + "wallet-connect-proposal-approved-toast": "Connected to {{dapp}}, go back to the dApp", + "wallet-connect-proposal-description": "By connecting you allow {{name}} to retrieve your account address and enable Web3", + "wallet-connect-proposal-expired": "WalletConnect proposal has expired", + "wallet-connect-proposal-rejected-toast": "Connection request from {{dapp}} declined", + "wallet-connect-proposal-title": "Would like to connect with your wallet", + "wallet-connect-qr-expired": "WalletConnect QR has expired", + "wallet-connect-request-expired": "WalletConnect request has expired", + "wallet-connect-send-transaction-header": "wants you to send this transaction with", + "wallet-connect-send-transaction-warning": "Send transactions only if you trust the dApp", + "wallet-connect-sign-message-header": "wants you to sign the message with", + "wallet-connect-sign-message-warning": "Sign messages only if you trust the dApp", + "wallet-connect-sign-request-rejected-toast": "{{dapp}} sign request rejected", + "wallet-connect-sign-transaction-header": "wants you to sign this transaction with", + "wallet-connect-sign-transaction-warning": "Sign transactions only if you trust the dApp", + "wallet-connect-sign-warning": "Sign only if you trust the dApp", + "wallet-connect-something-went-wrong": "Oops, something went wrong with this request", + "wallet-connect-transaction-request-success-toast": "Transaction signed, go back to the dApp", + "wallet-connect-typed-data-wrong-chain-id-warning": "Wrong network in the request data. Expected '{{expected-chain}}', but got '{{wrong-chain}}'", + "wallet-connect-version-not-supported": "WalletConnect version {{version}} is not supported", + "wallet-connect-via": "via", + "wallet-connect-wrong-qr": "It’s not a WalletConnect QR", + "wallet-insufficient-funds": "Insufficient funds", + "wallet-insufficient-gas": "Not enough ETH for gas", + "wallet-invalid-address": "Invalid address: \n {{data}}", + "wallet-invalid-address-checksum": "Error in address: \n {{data}}", + "wallet-invalid-chain-id": "Network does not match: \n {{data}} but current chain is {{chain}}", + "wallet-key-content": "A 64 character hex address based on the Ethereum standard and beginning with 0x. Public-facing, your account address is shared with others when you want to receive funds. Also referred to as an “Ethereum address” or “wallet address.”", + "wallet-key-title": "Account address", + "wallet-manage-accounts": "Manage accounts", + "wallet-manage-app-connections": "Manage app connections", + "wallet-manage-assets": "Manage assets", + "wallet-request": "Request", + "wallet-send": "Send", + "wallet-send-min-units": "Min 21000 units", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Wallet settings", + "wallet-total-value": "Total value", + "wallet-transaction-total-fee": "Total Fee", + "wallet-updated-at": "Updated {{at}}", + "want-to-create-community": "Want to create your own community?", + "wants-to-access-profile": "wants to access to your profile", + "wants-to-join": "wants to join", + "warning": "Warning", + "warning-message": "Sorry, we limit sending several messages in quick succession to prevent spam. Please try again in a moment", + "warning-sending-to-contract-descr": "The address you entered is a smart contract, sending funds to this address may result in loss of funds. To interact with a DApp, open the DApp in the Status DApp Browser.", + "watch-only": "Watch-only", + "watched-account-removed": "Watched address has been removed", + "watched-address": "Watched address", + "watched-addresses-limit-reached-toast": "Limit of 20 watched addresses reached: remove a watched address to add a new one.", + "ways-to-buy": "Ways to buy", + "ways-to-buy-assets": "Ways to buy assets", + "wc-brand-guide": "Guidance on using branding such as trademarks and logos", + "wc-disclaimer": "Disclaimers (including third party providers), warranties, and legal releases", + "wc-dispute": "Dispute resolution provisions", + "wc-how-to-use-status-app": "How to use the Status app including privacy and security", + "wc-new-tos-based-on-principles-prefix": "New Terms of Use designed based on our", + "we": "We", + "we-will-receive-from-all-profiles": "We will receive from all profiles:", + "we-will-receive-from-the-current-profile": "We will receive from the current profile:", + "web-view-error": "Unable to load page", + "website": "Website", + "websites": "Websites", + "webview-camera-permission-requests": "Webview camera permission requests", + "webview-camera-permission-requests-subtitle": "When enabled, websites and dapps can ask to use your camera", + "wed": "Wed", + "welcome-back": "Welcome back!", + "welcome-blank-community-message": "Your communities will appear here.", + "welcome-blank-message": "Your chats will appear here. To start new chats press the ⊕ button", + "welcome-community-blank-message": "Your channels will appear here. To create a new channel, click on the ⊕ button and select \"Create a channel\"", + "welcome-community-blank-message-edit-chats": "Your channels will appear here. To create a new channel, go back to the community screen, click on the ⊕ button and select \"Create a channel\"", + "welcome-screen-text": "Set up your wallet, invite friends to chat\n and browse popular dapps!", + "welcome-to-status": "Welcome to Status!", + "welcome-to-status-description": "Set up your crypto wallet, invite friends to chat and browse decentralized apps", + "welcome-to-web3": "Welcome to web3!", + "welcome-to-web3-sub-title": "What are you waiting for? Go explore!", + "what-are-you-waiting-for": "What are you waiting for?", + "what-changed": "What changed", + "what-is-shared": "What is shared", + "what-to-do": "What you would like to do?", + "what-we-will-receive": "What we will receive:", + "what-we-wont-receive": "What we won't receive:", + "what-you-can-do": "What can you do?", + "whats-on-your-mind": "What’s on your mind…", + "which-connection-to-use": "Which connection to use for syncing?", + "who-are-you-looking-for": "Who are you looking for ?", + "wifi-and-mobile-data": "Wi-Fi and mobile data", + "wifi-only": "Wi-Fi only", + "will-be-shared-from-all-profiles": "Will be shared from all profiles on device", + "will-be-shared-from-the-current-profile": "Will be shared from the current profile", + "will-be-shared-from-the-current-profile": "Will be shared from the current profile", + "with-full-encryption": "With full metadata privacy and e2e encryption", + "with-recovery-phrase": "With recovery phrase", + "word-count": "Word count", + "word-n": "Word #{{number}}", + "word-n-description": "In order to check if you have backed up your seed phrase correctly, enter the word #{{number}} above.", + "words-n": { + "one": "1 word", + "other": "{{count}} words" + }, + "write-down-and-store-securely": "Write codes down\n & store them securely", + "written-seed-ready": "I have written down my recovery phrase and ready to complete backup and remove it from device", + "wrong-address": "Wrong address", + "wrong-card": "Wrong card", + "wrong-card-text": "Tapped card does not correspond to the keys you selected", + "wrong-contract": "Wrong contract", + "wrong-keycard-text": "The Keycard you tapped is not associated with this phone", + "wrong-keycard-title": "Looks like you’ve tapped \na wrong keycard", + "wrong-password": "Wrong password", + "wrong-word": "Wrong word", + "x-counter": "x{{counter}}", + "yes": "Yes", + "You": "You", + "you": "you", + "you-already-have-an-asset": "You already have an asset {{value}}", + "you-already-use-status": "You already use Status", + "you-are-a": "You’re a", + "you-are-a-role": "You’re a {{role}}", + "you-are-all-set": "You’re all set!", + "you-are-all-set-description": "If you lose your phone, you can now access your funds and chat key using your seed phrase", + "you-can-change-account": "You can change the account name and color to what you wish", + "you-can-change-later-in-settings": "You can always change it later in Settings", + "you-can-choose-preview-websites": "You can choose which of the following websites can preview link of descriptions and pictures in chats", + "you-can-fetch": "You can fetch chat history", + "you-canceled-the-request": "You canceled the request to join", + "you-cannot-add-your-own-account-as-a-saved-address": "You cannot add your own account as a saved address", + "you-colon": "You:", + "you-deleted-a-message": "You deleted a message", + "you-dont-have-contacts": "You don’t have any contacts yet.", + "you-dont-have-contacts-invite-friends": "You don’t have any contacts yet.\nInvite your friends to start chatting.", + "you-dont-have-stickers": "You don’t have any stickers yet", + "you-eligible-to-join": "You’re eligible to join", + "you-eligible-to-join-as": "You’re eligible to join as {{role}}", + "you-have-no-contacts": "You have no contacts", + "you-hodl": "You hodl:", + "you-hold-number-of-hold-tokens-of-these": "You hold {{number-of-hold-tokens}} of these:", + "you-like-to-type-43-characters": "Do you like to type 43 characters?", + "you-may-also-experience": "You may also experience:", + "you-may-also-experience-item-1": "Missing or invalid data", + "you-may-also-experience-item-2": "Errors and unexpected behavior", + "you-must-always-hold": "You must always hold:", + "you-must-hold": "You must hodl:", + "you-must-now-hold": "You must now hold:", + "you-not-eligible-to-join": "You’re not eligible to join", + "you-not-eligible-to-join-as": "You’re not eligible to join as {{role}}", + "you-pinned-a-message": "You pinned a message", + "you-replied": "You replied", + "you-sent-a-gif": "You sent a GIF", + "you-sent-a-photo": "You sent a photo", + "you-sent-a-sticker": "You sent a Sticker", + "you-sent-audio-message": "You sent audio message", + "you-sent-n-photos": "You sent {{number}} photos", + "you-shared-a-community": "You shared a community", + "you-will-be-a": "You’ll be a", + "you-will-need-this-code": "You'll need this code to open Status and sign transactions", + "you-will-start-from-scratch": "You will start from scratch with a new set of keys", + "your-addresses": "Your addresses", + "your-answer": "Your answer", + "your-assets-on-network": "Your assets on {{network}}", + "your-card-is-frozen": "Your Keycard is frozen. Reset card access", + "your-contact-code": "Granting access authorizes this DApp to retrieve your chat key", + "your-data-belongs-to-you": "If you lose your seed phrase you lose your data and funds", + "your-data-belongs-to-you-description": "If you lose access, for example by losing your phone, you can only access your keys with your seed phrase. No one, but you has your seed phrase. Write it down. Keep it safe", + "your-identifiers": "Your identicon ring, chat key and emojihash will help others tell you from impersonators", + "your-key-pair-name-is-too-long": "Your key pair name is too long", + "your-key-pair-name-is-too-short": "Your key pair name is too short", + "your-keys": "Your keys", + "your-name": "Your name", + "your-price-limit": "Your price limit", + "your-profile-information": "Your profile information", + "your-recovery-phrase": "Your seed phrase", + "your-recovery-phrase-description": "This is your seed phrase. You use it to prove that this is your wallet. You only get to see it once! Write it on paper and keep it in a secure place. You will need it if you lose or reinstall your wallet.", + "your-tip-limit": "Your tip limit", + "youre-on-mobile-network": "You’re on mobile network" +} diff --git a/translations/es.json b/translations/es.json new file mode 100644 index 00000000000..7db8bd92447 --- /dev/null +++ b/translations/es.json @@ -0,0 +1,1499 @@ +{ + "You": "Tú", + "about-app": "Acerca de", + "about-key-storage-content": "Status nunca accederá a tu clave privada. Asegúrate de respaldar tu frase semilla. Si pierdes el teléfono es la única manera de acceder a tus claves.", + "about-key-storage-title": "Acerca del almacenamiento de claves", + "about-names-content": "¡Nadie puede fingir ser tú! Eres anónimo por defecto y no tienes que revelar tu nombre real. Puedes registrar un nombre personalizado por una pequeña tarifa.", + "about-names-title": "Los nombres no se pueden cambiar", + "accept": "Aceptar", + "accept-and-add": "Aceptar y añadir", + "accept-and-continue": "Aceptar y continuar", + "accept-and-share-address": "Aceptar y compartir dirección", + "access-existing-keys": "Ingresar claves existentes", + "access-key": "Clave de acceso", + "account-added": "Cuenta agregada", + "account-color": "Color de la cuenta", + "account-content": "Puedes comparar las cuentas en Status con las cuentas bancarias. Al igual que una cuenta bancaria, una cuenta suele tener una dirección y un saldo. Eta cuenta se utiliza para realizar transacciones con Ethereum. Puedes tener varias cuentas en tu billetera y acceder a ellas mediante Status.", + "account-exists-title": "Esta cuenta ya existe", + "account-name": "Nombre de la cuenta", + "account-settings": "Ajustes de la cuenta", + "account-title": "Cuenta", + "accounts": "Cuentas", + "actions": "Acciones", + "active-online": "En línea", + "active-unknown": "Desconocido", + "add": "Agregar", + "add-a-watch-account": "Agregar una dirección de solo observación", + "add-account": "Agregar una cuenta", + "add-account-description": "Puedes importar cualquier tipo de cuenta de Ethereum para agregarla a tu billetera de Status", + "add-account-incorrect-password": "La contraseña parece ser incorrecta. Ingresa la contraseña que usas para desbloquear la app.", + "add-an-account": "Agregar una cuenta", + "add-bootnode": "Agregar bootnode", + "add-contact": "Agregar contacto", + "add-custom-token": "Agregar un token personalizado", + "add-favourite": "Agregar favorito", + "add-mailserver": "Agregar nodo de Status", + "add-members": "Agregar miembros", + "add-network": "Agregar red", + "add-new-contact": "Agregar nuevo contacto", + "add-nickname": "Agregar un apodo (opcional)", + "add-private-key-account": "Agregar una cuenta desde una clave privada", + "add-seed-account": "Agregar una cuenta con una frase semilla", + "add-to-contacts": "Agregar a contactos", + "add-to-contacts-text": "Al agregar un usuario a tu lista de contactos, compartes la dirección de tu billetera", + "add-to-favourites": "Agregar a favoritos", + "add-watch-account": "Agregar una cuenta sólo para mirar", + "address": "Dirección", + "address-or-ens-name": "Dirección o nombre ENS", + "address-received": "Dirección recibida", + "address-request-accepted": "Solicitud de dirección aceptada", + "address-requested": "Dirección solicitada", + "advanced": "Avanzado", + "advanced-settings": "Ajustes avanzados", + "advertiser-description": "Descubriste Status gracias a un socio nuestro. ¿Podemos chequear tu IP para poder recompensarlo? Esta información no será usada para nada más, y será removida completamente después de 7 días", + "advertiser-starter-pack-accept": "Aceptar", + "advertiser-starter-pack-decline": "Rechazar", + "advertiser-starter-pack-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "advertiser-starter-pack-title": "Paquete de Bienvenida", + "advertiser-title": "Privacidad por defecto", + "agree-by-continuing": "Al continuar, aceptas \n nuestras", + "all": "Todos", + "allow": "Permitir", + "allow-and-send": "Permitir y enviar", + "allow-mention-notifications": "Mostrar @ menciones", + "allowing-authorizes-this-dapp": "Permitir autoriza a esta DApp a recuperar la dirección de tu billetera y habilitar Web3", + "already-have-asset": "Ya tienes este activo", + "amount": "Cantidad", + "appearance": "Apariencia", + "apply": "Aplicar", + "are-you-sure": "¿Estás seguro\/a?", + "are-you-sure-description": "No podrás volver a ver toda la frase semilla", + "are-you-sure-to-cancel": "¿Seguro que quieres cancelar?", + "are-you-sure?": "¿Estás seguro\/a?", + "ask-in-status": "Haz una pregunta o reporta un error", + "at": "en", + "attribution-received": "{{attrib}} de {{max}} bonos recibidos", + "audio": "Audio", + "audio-recorder": "Grabadora", + "audio-recorder-error": "Error de la grabadora", + "audio-recorder-max-ms-reached": "Tiempo máximo de grabación alcanzado", + "audio-recorder-permissions-error": "Tenés que dar permiso para enviar mensajes de audio", + "authorize": "Autorizar", + "available": "Disponible", + "available-participants": { + "one": "Puedes seleccionar un participante más", + "other": "Puedes seleccionar {{count}} participantes más" + }, + "back": "Atrás", + "back-up": "Respaldo", + "back-up-seed-phrase": "Respaldar la frase semilla", + "back-up-your-seed-phrase": "Respalda tu frase semilla", + "backup-disabled": "Deshabilitado", + "backup-recovery-phrase": "Respaldar la frase semilla", + "balance": "Saldo", + "begin-set-up": "Empezar la configuración", + "biometric-auth-android-sensor-desc": "Sensor táctil", + "biometric-auth-android-sensor-error-desc": "Fallido", + "biometric-auth-android-title": "Autenticación requerida", + "biometric-auth-confirm-logout": "Volver a iniciar sesión", + "biometric-auth-confirm-message": "La autentificación biométrica es necesaria para continuar, si no es posible, desbloquea tus claves con tu contraseña o código de acceso.", + "biometric-auth-confirm-title": "¡Debes autenticarte!", + "biometric-auth-confirm-try-again": "Inténtalo de nuevo", + "biometric-auth-error": "No se puede realizar la autenticación biométrica ({{code}})", + "biometric-auth-login-error-title": "Error de autenticación biométrica", + "biometric-auth-login-ios-fallback-label": "Ingresa la contraseña", + "biometric-auth-reason-login": "Iniciar sesión en Status", + "biometric-auth-reason-verify": "Verificar autenticación", + "biometric-disable-bioauth": "deshabilitar {{bio-type-label}}", + "biometric-disable-password-description": "Si deshabilitas esto, también ", + "biometric-disable-password-title": "Deshabilitar guardar contraseña", + "biometric-enable": "Si no deseas ingresar tu contraseña cada vez que accedes a la app, habilita el inicio de sesión {{bio-type-label}}", + "biometric-enable-button": "Habilitar {{bio-type-label}}", + "biometric-enable-keycard": "Si no deseas usar la Keycard cada vez que accedes a la app, habilita el inicio de sesión {{bio-type-label}}", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Huella dactilar", + "biometric-secure-with": "Asegurar con {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "blank-keycard-text": "Puedes continuar con tu keycard una vez que hayas generado tus claves y tu nombre", + "blank-keycard-title": "Parece que has pulsado \nuna keycard vacía", + "block": "Bloquear", + "block-contact": "Bloquear a este usuario", + "block-contact-details": "El bloqueo eliminará los mensajes anteriores de este usuario y evitará que recibas otros nuevos", + "blocked-users": "Usuarios bloqueados", + "bootnode-address": "Dirección de bootnode", + "bootnode-details": "Detalles de Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes habilitados", + "bootnodes-settings": "Ajustes de bootnode", + "browsed-websites": "El historial del navegador aparecerá aquí", + "browser": "Navegador", + "browser-not-secure": "¡La conexión no es segura! No firmes transacciones o envíes datos personales en este sitio.", + "browser-secure": "La conexión es segura. Asegúrate de que realmente confías en este sitio antes de firmar transacciones o ingresar datos personales.", + "browsers": "Navegadores", + "browsing-cancel": "Cancelar", + "browsing-open-in-android-web-browser": "Abrir en Android", + "browsing-open-in-ios-web-browser": "Abrir en iOS", + "browsing-open-in-status": "Abrir en Status", + "browsing-site-blocked-description1": "Hemos detectado posibles actividades maliciosas desde esta dirección. Para protegerte a ti y a tu billetera, prevenimos la navegación.\n\nSi crees que esto es un error, háznoslo saber en el", + "browsing-site-blocked-description2": "chat público", + "browsing-site-blocked-go-back": "Regresar", + "browsing-site-blocked-title": "Este sitio está bloqueado", + "browsing-title": "Navegar", + "bug-report": "Reportar un error", + "build-yourself": "Para usar esta aplicación sin estos Términos y Condiciones, puedes construir tu propia versión", + "buy-crypto": "Comprar criptomonedas", + "buy-crypto-choose-a-service": "Elige un servicio que quieras usar para comprar criptomonedas", + "buy-crypto-description": "Encuentra una dapp para comprar cripto ahora", + "buy-crypto-leaving": "Estás saliendo de Status y entrando en un sitio web de terceros para completar tu compra", + "buy-crypto-title": "Parece que tu billetera está vacía", + "camera-access-error": "Para permitir el acceso a la cámara, por favor, ve a los ajustes del sistema y asegúrate de que esté seleccionada la opción Status > Cámara.", + "can-not-add-yourself": "Ese eres tú. Para iniciar un chat, elige a alguien más", + "can-send-messages": "Puedes enviar y recibir mensajes nuevos", + "cancel": "Cancelar", + "cancel-keycard-setup": "Cancelar la configuración de Keycard", + "cancelling": "Cancelando", + "cannot-read-card": "No se puede leer la tarjeta.\nPor favor, mantenla en la parte posterior de tu teléfono", + "cannot-use-default-pin": "No está permitido el código de acceso 000000. \nPor favor, utiliza otro número", + "cant-open-public-chat": "No se puede abrir el chat público", + "cant-report-bug": "No puedo reportar un error", + "card-is-blank": "Esta tarjeta está en blanco", + "card-reseted": "La tarjeta ha sido restablecida", + "card-unpaired": "La tarjeta se ha desvinculado del dispositivo actual", + "change-fleet": "Cambiar fleet a {{fleet}}", + "change-logging-enabled": "¿Está seguro\/a de que deseas {{enable}} iniciar sesión?", + "change-passcode": "Cambiar código de acceso", + "change-password": "Cambiar contraseña", + "change-pin": "Cambiar el código de acceso de 6 dígitos", + "changed-amount-warning": "La cantidad se cambió de {{old}} a {{new}}", + "changed-asset-warning": "El activo se cambió de {{old}} a {{new}}.", + "chaos-mode": "Modo caos", + "chaos-unicorn-day": "Día del Unicornio del Caos", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Chatea y realiza transacciones privadas con amigos", + "chat-is-a-contact": "Contacto", + "chat-is-not-a-contact": "No es contacto", + "chat-key": "Clave de chat", + "chat-key-content": "Los mensajes en el protocolo de chat de Status se envían y reciben utilizando claves de encriptación. La clave pública de chat es una cadena de caracteres que compartes con otros para que puedan enviarte mensajes en Status.", + "chat-key-title": "Clave de chat", + "chat-link-previews": "Vistas previas de enlaces de chat", + "chat-name": "Nombre del chat", + "chat-name-content": "Tres palabras aleatorias, derivadas algorítmicamente de tu clave de chat y utilizadas como tu alias predeterminado en el chat. Los nombres de los chats son completamente únicos; ningún otro usuario puede tener las mismas tres palabras.", + "chat-name-title": "Nombre del chat", + "chat-notification-preferences": "Ajustes de las notificaciones", + "chat-settings": "Ajustes de chat", + "chats": "Chats", + "check-on-opensea": "Comprobar en opensea", + "check-your-recovery-phrase": "Comprueba tu frase semilla", + "choose-actions": "Elige acciones", + "choose-authentication-method": "Elije un método de autenticación", + "choose-new-location-for-keystore": "Selecciona una nueva ubicación para guardar el archivo keystore", + "choose-storage": "Elegir almacenamiento", + "clear": "Limpiar", + "clear-all": "Borrar todo", + "clear-history": "Borrar historial", + "clear-history-action": "Limpiar", + "clear-history-confirmation": "¿Borrar historial?", + "clear-history-confirmation-content": "¿Seguro que quieres limpiar este historial de chat?", + "clear-history-title": "¿Borrar historial?", + "close": "Cerrar", + "close-all": "Cerrar todo", + "close-app-button": "Confirmar", + "close-app-content": "La aplicación se detendrá y cerrará. Cuando la vuelvas a abrir, se utilizará la red seleccionada", + "close-app-title": "¡Advertencia!", + "command-button-send": "Enviar", + "communities": "Comunidades", + "communities-alpha": "Comunidades (alfa)", + "communities-verified": "✓ Comunidad Status verificada", + "community-color": "Color de la comunidad", + "community-color-placeholder": "Elige un color", + "community-edit-title": "Editar comunidad", + "community-image-pick": "Elige una imagen", + "community-image-remove": "Eliminar", + "community-image-take": "Tomar una foto", + "community-invite-title": "Invitar", + "community-key": "Clave privada de la comunidad", + "community-key-placeholder": "Escribe la clave privada de la comunidad", + "community-members": { + "one": "miembro", + "other": "miembros" + }, + "community-members-title": "Miembros", + "community-private-key": "Clave privada comunitaria", + "community-requests-to-join-title": "Solicitudes de membresía", + "community-roles": "Rol", + "community-share-title": "Compartir...", + "community-thumbnail-image": "Imagen en miniatura", + "community-thumbnail-upload": "Subir", + "complete-hardwallet-setup": "Esta tarjeta ahora está vinculada. La necesitarás para firmar transacciones y desbloquear tus claves", + "completed": "Completado", + "confirm": "Confirmar", + "confirm-password-placeholder": "Confirma tu contraseña", + "confirmation-request": "Solicitud de confirmación", + "confirmations": "Confirmaciones", + "confirmations-helper-text": "Cuando la transacción tenga 12 confirmaciones, puede considerarla resuelta.", + "connect": "Conectar", + "connect-mailserver-content": "¿Conectar con {{name}}?", + "connect-wallet": "Conectar billetera", + "connected": "Conectado", + "connected-to": "Conectado a", + "connecting": "Conectando...", + "connecting-requires-login": "Para conectarse a otra red es necesario iniciar sesión", + "connection-status": "Estado de conexión", + "connection-with-the-card-lost": "La conexión con la tarjeta\nse ha perdido", + "connection-with-the-card-lost-setup-text": "Para reanudar la configuración, sostén la tarjeta en\n la parte trasera del teléfono y mantén la\n tarjeta en contacto con el teléfono", + "connection-with-the-card-lost-text": "Para proceder, mantén la tarjeta en la parte posterior de tu teléfono", + "contact-code": "Clave de chat", + "contact-s": { + "one": "contacto", + "other": "contactos" + }, + "contacts": "Contactos", + "contacts-descr": "Tus contactos aparecerán aquí. Recibirá actualizaciones de estado de cualquier persona que agregue como contacto", + "contacts-empty": "Contactos con nombres ENS aparecerán aquí", + "continue": "Continuar", + "continue-anyway": "Continuar de todas maneras", + "contract-address": "Dirección del contrato", + "contract-interaction": "Interacción con el contrato", + "copy-info": "Copiar información", + "copy-qr": "Copiar código", + "copy-to-clipboard": "Copiar", + "copy-transaction-hash": "Copiar ID de transacción", + "cost-fee": "Costo\/Tarifa", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Crear", + "create-a-pin": "Crear un código de acceso de 6 dígitos", + "create-channel": "Crea un canal", + "create-channel-title": "Nuevo canal", + "create-community": "Crea una comunidad", + "create-group-chat": "Crear chat grupal", + "create-multiaccount": "Generar claves", + "create-new-key": "Obtener nuevas claves", + "create-pin": "Crea un código de acceso de 6 dígitos", + "create-pin-description": "Necesitarás tu tarjeta y este código de 6 dígitos para desbloquear Status y confirmar las transacciones", + "created-group-chat-description": "Creaste el grupo {{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Moneda", + "currency-display-name-aed": "Dírham de los Emiratos Árabes Unidos", + "currency-display-name-afn": "Afgani afgano", + "currency-display-name-ars": "Peso argentino", + "currency-display-name-aud": "Dólar australiano", + "currency-display-name-bbd": "Dólar de Barbados", + "currency-display-name-bdt": "Taka bangladeshí", + "currency-display-name-bgn": "Lev búlgaro", + "currency-display-name-bhd": "Dinar bahreiní", + "currency-display-name-bnd": "Dólar de Brunéi", + "currency-display-name-bob": "Boliviano de Bolivia", + "currency-display-name-brl": "Real brasileño", + "currency-display-name-btn": "Ngultrum butanés", + "currency-display-name-cad": "Dólar canadiense", + "currency-display-name-chf": "Franco suizo", + "currency-display-name-clp": "Peso chileno", + "currency-display-name-cny": "Yuan chino", + "currency-display-name-cop": "Peso colombiano", + "currency-display-name-crc": "Colón costarricense", + "currency-display-name-czk": "Corona checa", + "currency-display-name-dkk": "Corona danesa", + "currency-display-name-dop": "Peso dominicano", + "currency-display-name-egp": "Libra egipcia", + "currency-display-name-etb": "Birr etíope", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Libra esterlina", + "currency-display-name-gel": "Lari georgiano", + "currency-display-name-ghs": "Cedi ghanés", + "currency-display-name-hkd": "Dólar hongkonés", + "currency-display-name-hrk": "Kuna croata", + "currency-display-name-huf": "Forinto húngaro", + "currency-display-name-idr": "Rupia indonesia", + "currency-display-name-ils": "Nuevo séquel israelí", + "currency-display-name-inr": "Rupia india", + "currency-display-name-isk": "Corona islandesa", + "currency-display-name-jmd": "Dólar jamaiquino", + "currency-display-name-jpy": "Yen japonés", + "currency-display-name-kes": "Chelín keniano", + "currency-display-name-krw": "Won surcoreano", + "currency-display-name-kwd": "Dinar kuwaití", + "currency-display-name-kzt": "Tenge kazajo", + "currency-display-name-lkr": "Rupia de Sri Lanka", + "currency-display-name-mad": "Dírham marroquí", + "currency-display-name-mdl": "Leu moldavo", + "currency-display-name-mur": "Rupia mauriciana", + "currency-display-name-mwk": "Kwacha malauí", + "currency-display-name-mxn": "Peso mexicano", + "currency-display-name-myr": "Ringgit malayo", + "currency-display-name-mzn": "Metical mozambiqueño", + "currency-display-name-nad": "Dólar namibio", + "currency-display-name-ngn": "Naira nigeriana", + "currency-display-name-nok": "Corona noruega", + "currency-display-name-npr": "Rupia nepalí", + "currency-display-name-nzd": "Dólar neozelandés", + "currency-display-name-omr": "Rial omaní", + "currency-display-name-pen": "Sol peruano", + "currency-display-name-pgk": "Kina (Papúa Nueva Guinea)", + "currency-display-name-php": "Peso filipino", + "currency-display-name-pkr": "Rupia pakistaní", + "currency-display-name-pln": "Esloti polaco", + "currency-display-name-pyg": "Guaraní paraguayo", + "currency-display-name-qar": "Riyal qatarí", + "currency-display-name-ron": "Leu rumano", + "currency-display-name-rsd": "Dinar serbio", + "currency-display-name-rub": "Rublo ruso", + "currency-display-name-sar": "Riyal saudí", + "currency-display-name-sek": "Corona sueca", + "currency-display-name-sgd": "Dólar singapurense", + "currency-display-name-thb": "Baht tailandés", + "currency-display-name-try": "Lira turca", + "currency-display-name-ttd": "Dólar de Trinidad y Tobago", + "currency-display-name-twd": "Nuevo dólar taiwanés", + "currency-display-name-tzs": "Chelín tanzano", + "currency-display-name-uah": "Grivna ucraniana", + "currency-display-name-ugx": "Chelín ugandés", + "currency-display-name-usd": "Dólar estadounidense", + "currency-display-name-uyu": "Peso uruguayo", + "currency-display-name-vef": "Bolívar venezolano", + "currency-display-name-vnd": "Dong vietnamita", + "currency-display-name-zar": "Rand sudafricano", + "current": "Actual", + "current-network": "Red actual", + "current-pin": "Ingresa el código de acceso de 6 dígitos", + "current-pin-description": "Ingresa tu código de acceso de 6 dígitos para continuar", + "custom": "Personalizado", + "custom-networks": "Redes personalizadas", + "custom-node": "Estás utilizando un punto de conexión RPC personalizado. Es posible que el historial de transferencias locales esté incompleto.", + "custom-seed-phrase": "Frase semilla inválida", + "custom-seed-phrase-text-1": "Esta frase semilla no coincide con nuestro diccionario compatible. Revisa si hay palabras mal escritas.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Aceptar y Abrir", + "dapp-starter-pack-description": "¡Aquí tienes algunas criptomonedas para comenzar! Úsalas para obtener pegatinas, un nombre ENS y probar dapps", + "dapp-starter-pack-title": "Paquete de Bienvenida", + "dapp-would-like-to-connect-wallet": "quisiera conectarse a", + "dapps": "ÐApps", + "dapps-permissions": "Permisos de DApp", + "dark": "Oscuro", + "data": "Datos", + "data-syncing": "Sincronización de datos", + "database-reset-content": "Los chats, contactos y ajustes han sido eliminados. Puedes usar tu cuenta con tu Keycard", + "database-reset-title": "Base de datos reestablecida", + "database-reset-warning": "La base de datos se restablecerá. Se borrarán los chats, los contactos y los ajustes.", + "datetime-ago": "hace", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "día", + "other": "días" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "hora", + "other": "horas" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "minuto", + "other": "minutos" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "segundo", + "other": "segundos" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "hoy", + "datetime-yesterday": "ayer", + "decimals": "Decimales", + "decline": "Rechazar", + "decryption-failed-content": "Ha ocurrido un error al desencriptar tus datos. Es posible que tengas que generar unas nuevas claves y borrar tus datos antiguos. Pulsa \"Aplicar\" para borrar o \"Cancelar\" para volver a intentarlo", + "default": "Predeterminados", + "default-assets": "Por defecto ERC20 y ERC721", + "delete": "Eliminar", + "delete-account": "Borrar cuenta", + "delete-and-leave-group": "Eliminar y salir del grupo", + "delete-bootnode": "Eliminar bootnode", + "delete-bootnode-are-you-sure": "¿Seguro que quieres eliminar este bootnode?", + "delete-bootnode-title": "Eliminar bootnode", + "close-chat": "Eliminar chat", + "close-chat-confirmation": "¿Estás seguro de que quieres eliminar este chat?", + "delete-confirmation": "¿Eliminar?", + "delete-keys-keycard": "Borrar claves de Keycard", + "delete-mailserver": "Eliminar nodo de Status", + "delete-mailserver-are-you-sure": "¿Está seguro de que desea eliminar este nodo de Status?", + "delete-mailserver-title": "Eliminar nodo de Status", + "delete-message": "Eliminar mensaje", + "delete-my-account": "Borrar mi cuenta", + "delete-my-profile": "Eliminar mi perfil", + "delete-network-confirmation": "¿Seguro que quieres eliminar esta red?", + "delete-network-error": "Por favor, conéctate a una red diferente antes de eliminar esta", + "delete-network-title": "¿Eliminar red?", + "delete-profile": "Eliminar perfil", + "delete-profile-warning": "Advertencia: Si no tiene tu frase de semilla escrita, perderá el acceso a sus fondos después de eliminar su perfil", + "deny": "Negar", + "derivation-path": "Ruta de derivación", + "describe-channel": "Describa el canal", + "description": "Descripción", + "dev-mode": "Modo de desarrollo", + "dev-mode-settings": "Ajustes del modo de desarrollo", + "device-syncing": "Sincronización de dispositivos", + "devices": "Dispositivos", + "disable": "deshabilitar", + "disable-all": "Deshabilitar todo", + "disabled": "Deshabilitado", + "disconnected": "Chat sin conexión", + "discover": "Descubrir", + "dismiss": "Descartar", + "done": "Listo", + "dont-ask": "No me vuelvas a preguntar", + "edit": "Editar", + "edit-channel-title": "Editar el canal", + "edit-chats": "Editar chats", + "edit-community": "Editar comunidad", + "edit-favourite": "Editar favorito", + "edit-group": "Editar grupo", + "edit-profile": "Editar perfil", + "empty-activity-center": "Tus notificaciones de chat aparecerán aquí", + "empty-chat-description": "No hay mensajes \nen este chat todavía", + "empty-chat-description-community": "Ha estado tranquilo aquí durante {{quiet-hours}} .", + "empty-chat-description-one-to-one": "Cualquier mensaje que envíes aquí está encriptado y solo puedes leerlo tú y", + "empty-chat-description-public": "Ha estado tranquilo aquí durante las últimas {{quiet-hours}}. Inicia la conversación o ", + "empty-chat-description-public-share-this": "comparte este chat", + "empty-keycard-required": "Requiere una Keycard vacía", + "empty-pending-invitations-descr": "Las personas que deseen unirse al grupo\na través de un enlace de invitación aparecerán aquí", + "empty-tab": "Pestaña vacía", + "enable": "Habilitar", + "enable-all": "Habilitar todos", + "enable-link-previews": "¿Habilitar vistas previas de enlaces en el chat?", + "encrypt-with-password": "Encriptar con contraseña", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Agregar nombre de usuario", + "ens-agree-to": "Acepto", + "ens-chat-settings": "Ajustes de chat", + "ens-custom-domain": "Dominio personalizado", + "ens-custom-username-hints": "Escribe el nombre de usuario completo, incluido el dominio personalizado como username.domain.eth", + "ens-custom-username-taken": "El nombre de usuario no te pertenece :(", + "ens-deposit": "Depósito", + "ens-dismiss-message": "Pulsa aquí para descartar", + "ens-displayed-with": "Tus mensajes se muestran a otros con", + "ens-get-name": "Obtén un nombre de usuario universal", + "ens-got-it": "Entendido", + "ens-locked": "Nombre de usuario bloqueado. No lo podrás liberar hasta el {{date}}", + "ens-name-content": "Alias personalizado para tu clave de chat que puedes registrar utilizando Ethereum Name Service. Los nombres ENS son nombres de usuario descentralizados.", + "ens-name-not-found": "No se puede resolver el nombre ENS", + "ens-name-title": "Nombre ENS", + "ens-network-restriction": "Solo disponible en Mainnet", + "ens-no-usernames": "No tienes ningún nombre de usuario conectado", + "ens-powered-by": "Propulsado por Ethereum Name Services", + "ens-primary-username": "Nombre de usuario principal", + "ens-register": "Registrar", + "ens-registration-failed": "Por favor, inténtalo de nuevo para registrar el nombre de usuario.", + "ens-registration-failed-title": "La transacción ha fallado", + "ens-registration-failure": "Registro fallido", + "ens-registration-in-progress": "Registro en curso...", + "ens-release-username": "Liberar nombre de usuario", + "ens-remove-hints": "La eliminación separará el nombre de usuario de tu clave.", + "ens-remove-username": "Eliminar nombre de usuario", + "ens-saved": "ahora está conectado con tu clave de chat y se puede usar en Status.", + "ens-saved-title": "Nombre de usuario agregado", + "ens-show-username": "Mostrar mi nombre de usuario ENS en los chats", + "ens-terms-header": "Términos de registro de nombre", + "ens-terms-point-1": "Los fondos se depositan por 1 año. Tu SNT se bloqueará, pero no se gastará.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Registro ENS).", + "ens-terms-point-2": "Después de 1 año, puedes liberar el nombre y recuperar tu depósito, o no tomar ninguna acción para mantener el nombre.", + "ens-terms-point-3": "Si los términos del contrato cambian —por ejemplo, Status actualiza el contrato— el usuario tiene derecho a liberar el nombre de usuario independientemente del tiempo retenido.", + "ens-terms-point-4": "El controlador del contrato no puede acceder a tus fondos depositados. Sólo pueden ser devueltos a la dirección que los envió.", + "ens-terms-point-5": "Tu(s) dirección(es) se asociará(n) públicamente con tu nombre ENS.", + "ens-terms-point-6": "Los nombres de usuario se crean como nodos de subdominio de stateofus.eth y están sujetos a los términos del contrato inteligente de ENS.", + "ens-terms-point-7": "Autorizas el contrato para transferir SNT en tu nombre. Esto solo puede ocurrir cuando apruebas una transacción para autorizar la transferencia.", + "ens-terms-point-8": "Estos términos están garantizados por la lógica del contrato inteligente en las direcciones:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Términos de registro de nombre.", + "ens-test-message": "Hola", + "ens-transaction-pending": "Transacción pendiente...", + "ens-understand": "Entiendo que la dirección de mi billetera estará conectada públicamente a mi nombre de usuario.", + "ens-username": "Nombre de usuario ENS", + "ens-username-already-added": "El nombre de usuario ya está conectado con tu clave de chat y se puede usar dentro de Status.", + "ens-username-available": "✓ ¡Nombre de usuario disponible!", + "ens-username-connected": "Este nombre de usuario es de tu propiedad y está conectado con tu clave de chat.", + "ens-username-connected-continue": "Continuar configurando «Mostrar mi nombre de usuario ENS en los chats».", + "ens-username-connected-with-different-key": "Continuar requerirá una transacción para conectar el nombre de usuario con tu clave de chat actual.", + "ens-username-connection-confirmation": "{{username}} se conectará una vez que se complete la transacción.", + "ens-username-hints": "Al menos 4 caracteres. Letras latinas, números y sólo minúsculas.", + "ens-username-invalid": "Sólo letras y números.", + "ens-username-owned": "✓ El nombre de usuario es tuyo.", + "ens-username-owned-continue": "Continuar conectará este nombre de usuario con tu clave de chat.", + "ens-username-registration-confirmation": "¡Bien! Eres el propietario de {{username}} una vez completada la transacción.", + "ens-username-taken": "Nombre de usuario ya está tomado :(", + "ens-username-you-can-follow-progress": "Puedes seguir el progreso en la sección de Historial de transacciones de tu billetera.", + "ens-usernames": "Nombres de usuario ENS", + "ens-usernames-details": "Registra un nombre de usuario universal para que otros usuarios te reconozcan fácilmente", + "ens-want-custom-domain": "Tengo un nombre en otro dominio", + "ens-want-domain": "Quiero un dominio stateofus.eth", + "ens-welcome-hints": "Los nombres ENS transforman esas direcciones larguísimas en nombres de usuario únicos.", + "ens-welcome-point-customize": "Un nombre ENS puede reemplazar tu nombre aleatorio de 3 palabras en el chat. Sé @yourname en lugar de {{name}}.", + "ens-welcome-point-customize-title": "Personaliza tu nombre de chat", + "ens-welcome-point-receive": "Otros pueden enviarte fondos desde el chat en un simple paso.", + "ens-welcome-point-receive-title": "Recibe transacciones en el chat", + "ens-welcome-point-register": "Regístrate una vez para mantener el nombre para siempre. Después de 1 año, podrás liberar el nombre y recuperar tus SNT.", + "ens-welcome-point-register-title": "10 SNT para registrar", + "ens-welcome-point-simplify": "Puedes recibir fondos a tu ENS fácil de compartir, en vez de tu hash hexadecimal (0x...)", + "ens-welcome-point-simplify-title": "Simplifica tu dirección ETH", + "ens-welcome-point-verify": "Puedes verificar y agregar cualquier nombre de usuario que poseas en los siguientes pasos.", + "ens-welcome-point-verify-title": "¿Ya tienes un nombre de usuario?", + "ens-your-username": "Tu nombre de usuario", + "ens-your-usernames": "Tus nombres de usuario", + "ens-your-your-name": "Tu nombre ENS", + "enter-12-words": "Ingresa las 12 palabras de tu frase semilla separadas por espacios", + "enter-a-private-key": "Ingresa una clave privada", + "enter-a-seed-phrase": "Ingresa una frase semilla", + "enter-address": "Ingresar dirección", + "enter-contact-code": "ENS (juanca94) o clave de chat (0x04…)", + "enter-pair-code": "Ingresa tu código de vinculación", + "enter-pair-code-description": "El código de vinculación puede establecerse desde un cliente Status ya vinculado", + "enter-password": "Ingresa la contraseña", + "enter-pin": "Ingresa el código de acceso de 6 dígitos", + "enter-puk-code": "Ingresa el código PUK", + "enter-puk-code-description": "El código de acceso de 6 dígitos ha sido bloqueado. \n Ingresa el código PUK para desbloquear el código de acceso.", + "enter-recipient-address-or-username": "Ingresa la dirección o nombre de usuario del destinatario", + "enter-seed-phrase": "Ingresar la frase semilla", + "enter-url": "Ingresa una URL", + "enter-user-pk": "Ingresa la clave pública de usuario", + "enter-watch-account-address": "Escanea un código QR \n o \n ingresa la dirección para observar", + "enter-word": "Ingresa la palabra", + "enter-your-code": "Ingresa tu código de acceso de 6 dígitos", + "enter-your-password": "Ingresa tu contraseña", + "error": "Error", + "error-unable-to-get-balance": "No se puede obtener el saldo", + "error-unable-to-get-prices": "Error de conversión de moneda. Refresca tu pantalla para intentarlo de nuevo.", + "error-unable-to-get-token-balance": "No se puede obtener el saldo del token", + "errors": "Errores", + "eth": "ETH", + "ethereum-account": "Cuenta Ethereum", + "ethereum-address": "Dirección de Ethereum", + "ethereum-node-started-incorrectly-description": "El nodo Ethereum se inició con una configuración incorrecta. La aplicación se detendrá para recuperarse de esa condición. ID de red configurada = {{network-id}}, actual = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "El nodo Ethereum se inició incorrectamente", + "etherscan-lookup": "Ver en Etherscan", + "everyone": "Todos", + "export-account": "Exportar la cuenta", + "export-key": "Exportar clave privada", + "external-storage-denied": "Se niega el acceso al almacenamiento externo", + "failed": "Fallido", + "faq": "Preguntas frecuentes", + "favourite": "Favorito", + "favourite-description": "Tus sitios web favoritos aparecerán aquí", + "favourites": "Favoritos", + "favourites-empty": "Las direcciones agregadas a favoritos aparecerán aquí", + "fetch-messages": "↓ Recuperar mensajes", + "fetch-timeline": "↓ Obtener", + "find": "Encontrar", + "finish": "Finalizar", + "finishing-card-setup": "Finalizando la configuración de la tarjeta", + "fleet": "Fleet", + "fleet-settings": "Ajustes de fleet", + "follow": "Seguir", + "follow-your-interests": "Entra en un chat público y conoce gente nueva", + "free": "↓ Gratis", + "from": "De", + "gas-limit": "Límite de gas", + "gas-price": "Precio del gas", + "gas-used": "Gas utilizado", + "generate-a-key": "Generar claves", + "generate-a-new-account": "Generar una cuenta", + "generate-a-new-key": "Generar una nueva clave", + "generate-account": "Generar claves", + "generate-an-account": "Generar una cuenta", + "generate-new-key": "Generar claves", + "generating-codes-for-pairing": "> Descargando el software del producto a la tarjeta \n > Generando códigos de desbloqueo y vinculación", + "generating-keys": "Generando claves...", + "generating-mnemonic": "Generando frase semilla", + "get-a-keycard": "Obtén una Keycard", + "get-started": "Iniciar", + "get-status-at": "Descarga Status en http:\/\/status.im\/es", + "get-stickers": "Obtener pegatinas", + "give-a-short-description": "Haz una breve descripción", + "give-a-short-description-community": "Descríbelo brevemente", + "give-permissions-camera": "Dar permiso\npara acceder a la cámara", + "glossary": "Glosario", + "go-to-settings": "Ir a Ajustes", + "got-it": "Entendido", + "grant-face-id-permissions": "Para conceder el permiso de Face ID requerido, ve a los ajustes de tu sistema y asegúrate de que esté seleccionado Status > Face ID", + "group-chat": "Chat grupal", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** se ha convertido en admin", + "group-chat-all-contacts-invited": "Todos tus contactos ya están en el grupo", + "group-chat-created": "**{{member}}** creó el grupo **{{name}}**", + "group-chat-decline-invitation": "Rechazar invitación", + "group-chat-member-added": "**{{member}}** ha sido invitado", + "group-chat-member-joined": "**{{member}}** se ha unido al grupo", + "group-chat-member-removed": "**{{member}}** abandonó el grupo", + "group-chat-members-count": "{{selected}}\/{{max}} miembros", + "group-chat-name-changed": "**{{member}}** cambió el nombre del grupo a **{{name}}**", + "group-chat-no-contacts": "Aún no tienes contactos. \nInvita a tus amigos a comenzar a chatear", + "group-info": "Información del grupo", + "group-invite": "Invitación de grupo", + "group-invite-link": "Enlace de invitación de grupo", + "group-membership-request": "Solicitud de membresía grupal", + "gwei": "Gwei", + "has-permissions": "tiene permiso para acceder", + "hash": "Hash", + "help": "ayuda", + "help-capitalized": "Ayuda", + "help-center": "Centro de ayuda", + "help-improve-status": "Ayuda a mejorar Status", + "hide-content-when-switching-apps": "Bloquear capturas de pantalla", + "hide-content-when-switching-apps-ios": "Ocultar vista previa", + "history": "Historial", + "history-nodes": "Nodos de Status", + "hold-card": "Sostén la tarjeta en la parte posterior de tu teléfono", + "home": "Inicio", + "hooks": "Hooks", + "how-it-works": "Cómo funciona", + "http-gateway-error": "¡Oops, la solicitud falló!", + "identifier": "Identificador", + "image": "Imagen", + "image-remove-current": "Eliminar la foto actual", + "image-source-gallery": "Seleccionar de la galería", + "image-source-make-photo": "Tomar foto", + "image-source-title": "Editar imagen", + "import": "Importar", + "import-community": "Importar una comunidad", + "import-community-title": "Importar una comunidad", + "in-contacts": "En contactos", + "incoming": "Entrante", + "incoming-transaction": "Transacción entrante", + "incorrect-code": [ + "str", + "Lo sentimos, el código es incorrecto. Por favor, escríbelo de nuevo" + ], + "increase-gas": "Aumentar el gas", + "initialization": "Inicialización", + "install": "↓ Instalar", + "intro-message1": "¡Bienvenid@ a Status!\nToca este mensaje para establecer tu contraseña y empezar.", + "intro-privacy-policy-note1": "Status no recopila ni se beneficia de tus datos personales. Al continuar, aceptas la ", + "intro-privacy-policy-note2": "política de privacidad", + "intro-text": "Status es tu puerta de enlace a la web descentralizada", + "intro-text1": "Chatea a través de una red encriptada peer-to-peer donde los mensajes no pueden ser censurados o pirateados", + "intro-text2": "Envía y recibe activos digitales en cualquier parte del mundo. No se requiere cuenta bancaria", + "intro-text3": "Explora juegos, plataformas de intercambio y redes sociales en las que solo tú eres propietario de tus datos", + "intro-title1": "Comunicación verdaderamente privada", + "intro-title2": "Criptobilletera segura", + "intro-title3": "Apps descentralizadas", + "intro-wizard-text1": "Un set de claves controla tu cuenta. Las claves se almacenan en tu teléfono, por lo que solo tú puedes usarlas.", + "intro-wizard-text2": "Una clave es para chatear. Viene con un nombre legible que no se puede cambiar.", + "intro-wizard-text3": "¿Tienes una Keycard? Guarda tus claves en ella; la necesitarás para las transacciones", + "intro-wizard-text4": "Asegura y encripta tus claves", + "intro-wizard-text6": "Status te notificará sobre los nuevos mensajes. Puedes editar tus preferencias de notificación más adelante en los ajustes", + "intro-wizard-title-alt4": "Crea una contraseña", + "intro-wizard-title-alt5": "Confirma tu contraseña", + "intro-wizard-title1": "Obtener las claves", + "intro-wizard-title2": "Elige un nombre de chat", + "intro-wizard-title3": "Elegir almacenamiento de claves", + "intro-wizard-title4": "Crear un código de acceso de 6 dígitos", + "intro-wizard-title5": "Confirma la clave de acceso", + "intro-wizard-title6": "Habilitar notificaciones", + "introduce-yourself": "Preséntate con un breve mensaje", + "invalid-address-qr-code": "El código QR escaneado no contiene una dirección válida", + "invalid-format": "Formato inválido \n Debe ser {{format}}", + "invalid-key-confirm": "Aplicar", + "invalid-key-content": "La base de datos no se puede encriptar porque un archivo está dañado. Tus fondos y claves de chat están a salvo. Otros datos, como chats y contactos, no se podrán restaurar. Presionar el botón \"{{erase-multiaccounts-data-button-text}}\" eliminará otros datos y te permitirá acceder a tus fondos y enviar mensajes", + "invalid-number": "Número inválido", + "invalid-pairing-password": "Contraseña de vinculación inválida", + "invalid-public-chat-topic": "Tema de chat público no válido", + "invalid-range": "Formato inválido, debe estar entre {{min}} y {{max}}", + "invite": "Invitar", + "invite-button": "Invitá", + "invite-chat-accept": "Aceptar", + "invite-chat-accept-join": "Aceptar y Unirse", + "invite-chat-intro": "Fuiste referido por un amigo para unirte a Status. Acá tenés algunas criptomonedas para empezar! Usalas para registrar un nombre ENS o comprar un paquete de stickers", + "invite-chat-name": "Referencia de amigos", + "invite-chat-pending": "Pendiente", + "invite-chat-rule": "Si aceptás también recompensarás a tu amigo con un bono de referencia criptográfica", + "invite-chat-starter-pack": "Paquete de Bienvenida", + "invite-friends": "Invitar a amigos", + "invite-instruction-fifth": "Puede elegir canjear su bono de referencia en cualquier momento.", + "invite-instruction-first": "Envías un enlace de invitación único a tu amigo para descargar y unirse a Status", + "invite-instruction-fourth": "Tú recibes el bono de referencia y tu amigo, el Paquete de Bienvenida", + "invite-instruction-second": "Tu amigo descarga Status y crea una cuenta (en Android)", + "invite-instruction-third": "Se inicia un chat con tu amigo, donde confirman tu referencia", + "invite-people": "Invitar a otros", + "invite-privacy-policy-public": "Instalaste Status a través de un enlace de referido. Al unirte a este chat, atribuyes su referencia y aceptas las", + "invite-privacy-policy1": "Al aceptar, estás de acuerdo con el programa de referidos", + "invite-privacy-policy2": "Términos y Condiciones.", + "invite-public-chat-home": "Invitación de referencia", + "invite-public-chat-intro": "¡Aquí tienes algunas criptomonedas para comenzar! Úsalas para registrar un nombre ENS o comprar un paquete de pegatinas", + "invite-receive-account": "Cuenta para recibir tu bono de referencia", + "invite-reward": "¡Gana criptomonedas por cada amigo que invites!", + "invite-reward-friend": "Amigo:", + "invite-reward-friend-description": "Tu amigo recibirá un Paquete de Bienvenida que consta de {{reward}} para comenzar", + "invite-reward-friend-name": "Paquete de Bienvenida", + "invite-reward-you": "Vos:", + "invite-reward-you-description": "Invitá a un amigo y recibí {{reward}} como bono de referencia. Usalo para obtener stickers, un nombre ENS y probar dapps", + "invite-reward-you-name": "Bono de referencia", + "invite-select-account": "Seleccioná una cuenta para recibir tu bono de referencia", + "invite-warning": "Esta promoción solo es válida para usuarios de un dispositivo Android que no sean residentes de EE. UU. Un amigo debe confirmar la referencia en un plazo de 7 días.", + "invited": "invitado", + "join": "Unirse", + "join-a-community": "o únete a una comunidad", + "join-group-chat": "Unirse al grupo", + "join-group-chat-description": "{{username}} te invitó a unirte al grupo {{group-name}}", + "join-me": "Hola, unite a Status: {{url}}", + "join-new-private-chat": "Iniciar un nuevo chat privado", + "join-new-public-chat": "Unirse a un chat público", + "joined": "Unido", + "joined-group-chat-description": "Te has unido a {{group-name}} desde la invitación de {{username}}", + "key": "Clave", + "key-managment": "Administración de claves", + "key-on-device": "La clave privada está guardada en este dispositivo", + "keycard": "Keycard", + "keycard-access-reset": "Acceso a Keycard restablecido", + "keycard-applet-install-instructions": "Para instalar el applet, sigue las instrucciones en https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Intenta mover la tarjeta para encontrar el lector NFC en el dispositivo", + "keycard-awaiting-title": "Todavía buscando...", + "keycard-blocked": "Keycard ha sido bloqueada. \n Necesitas reiniciar la tarjeta para continuar usándola.", + "keycard-can-use-with-new-passcode": "Puedes usar esta tarjeta con tu nuevo código de acceso", + "keycard-cancel-setup-text": "Esto cancelará la configuración de keycard. Se recomienda encarecidamente finalizar la configuración para usar keycard. ¿De verdad quieres cancelar?", + "keycard-cancel-setup-title": "Operación peligrosa", + "keycard-connected-description": "Intenta no mover la tarjeta", + "keycard-connected-title": "Conectado", + "keycard-desc": "¿Tienes una Keycard? Guarda tus claves allí; la necesitarás para las transacciones", + "keycard-dont-ask-card": "No pedir tarjeta para iniciar sesión", + "keycard-enter-new-passcode": "Ingresa el nuevo código de acceso {{step}}\/2", + "keycard-error-description": "Vuelva a conectar la tarjeta para continuar", + "keycard-error-title": "Conexión perdida", + "keycard-free-pairing-slots": "Keycard tiene {{n}} espacios de vinculación libres", + "keycard-has-multiaccount-on-it": "Esta tarjeta está llena. Cada tarjeta puede mantener un par de claves principales", + "keycard-init-description": "Coloca la tarjeta en la parte posterior del teléfono para continuar", + "keycard-init-title": "Buscando tarjetas...", + "keycard-is-blocked-details": "Ya no puedes usar esta tarjeta para acceder o firmar en esta cuenta. Ha habido demasiados intentos fallidos de código de acceso y PUK.", + "keycard-is-blocked-instructions": "Para acceder a tu cuenta tendrás que restablecer de fábrica tu tarjeta. Pulsa el botón de abajo para iniciar el procedimiento. Necesitarás tu mnemónico.", + "keycard-is-blocked-title": "La Keycard está bloqueada", + "keycard-is-frozen-details": "Para proteger tus activos, tu tarjeta está congelada. Restablece tu tarjeta para desbloquearla y enviar transacciones. Puedes hacerlo con tu PUK o mnemónico.", + "keycard-is-frozen-reset": "Restablecer con PUK", + "keycard-is-frozen-title": "La Keycard está congelada", + "keycard-onboarding-finishing-header": "Terminando", + "keycard-onboarding-intro-header": "Guarda tus claves en Keycard", + "keycard-onboarding-intro-text": "Prepárate, esto puede tomar unos minutos, pero es importante para asegurar tu cuenta", + "keycard-onboarding-pairing-header": "Vinculando la tarjeta...", + "keycard-onboarding-preparing-header": "Preparando la tarjeta...", + "keycard-onboarding-puk-code-header": "Escribe los códigos\n y guárdalos de forma segura", + "keycard-onboarding-recovery-phrase-description": "Necesitas esta frase semilla para obtener de nuevo tu clave. Anótala y guárdala de forma segura, en físico y separada de este dispositivo.", + "keycard-onboarding-recovery-phrase-header": "Respalda tu frase semilla", + "keycard-onboarding-recovery-phrase-text": "Sólo para tus ojos. Esta es la semilla mágica usada para generar tu clave.", + "keycard-onboarding-start-header": "Mantén la tarjeta en la parte posterior\n de tu teléfono para iniciar", + "keycard-onboarding-start-step1": "Crea un código de acceso", + "keycard-onboarding-start-step1-text": "Alrededor de 1 minuto. Crea un código de acceso de 6 dígitos para encriptar tus claves", + "keycard-onboarding-start-step2": "Anota el código de vinculación y PUK", + "keycard-onboarding-start-step2-text": "Alrededor de 1 minuto. Vas a necesitar un papel y un lápiz para eso", + "keycard-onboarding-start-step3": "Respalda la frase semilla", + "keycard-onboarding-start-step3-text": "Alrededor de 1 minuto. También vas a necesitar un papel y un lápiz para eso", + "keycard-onboarding-start-text": "Y mantén el contacto entre la tarjeta y el teléfono\n durante la configuración. La configuración tomará alrededor de 4 minutos", + "keycard-processing-description": "Intenta no mover la tarjeta", + "keycard-processing-title": "Procesando...", + "keycard-recovery-intro-button-text": "Iniciar la recuperación", + "keycard-recovery-intro-header": "Recuperar claves almacenadas en Keycard", + "keycard-recovery-intro-text": "Si antes generaste claves con keycard y ahora deseas utilizarlas en este dispositivo", + "keycard-recovery-no-key-header": "No hay nada que \nrecuperar aquí", + "keycard-recovery-no-key-text": "Tu Keycard no tiene ninguna clave almacenada. Para usarla, genera una nueva clave y elige tu Keycard para almacenar la clave", + "keycard-recovery-phrase-confirm-header": "Confirmar frase semilla", + "keycard-recovery-phrase-confirmation-text": "¡No tendrás una segunda oportunidad! Si pierdes acceso, por ejemplo, al extraviar tu Keycard, solo podrás acceder a tus claves con tu frase semilla. Nadie aparte de ti tiene tu frase semilla. Anótala y guárdala de forma segura.", + "keycard-recovery-phrase-confirmation-title": "¿Has anotado la frase semilla?", + "keycard-recovery-success-header": "Tus claves han sido \n recuperadas con éxito", + "keycard-redeem-title": "Canjear a", + "keycard-redeem-tx": "Canjea activos", + "keycard-redeem-tx-desc": "Toca la tarjeta para firmar y recibir activos", + "keycard-reset-passcode": "Restablecer código de acceso", + "keycard-success-description": "Puedes quitar la tarjeta ahora", + "keycard-success-title": "Éxito", + "keycard-unauthorized-operation": "No estás autorizado a realizar esta operación.\n Por favor, toca la tarjeta válida e inténtalo de nuevo.", + "keycard-upsell-subtitle": "Mayor seguridad y conveniencia", + "language": "Idioma", + "learn-more": "Más información", + "learn-more-about-keycard": "Más información sobre Keycard", + "leave": "Salir", + "leave-chat": "Abandonar chat", + "leave-chat-confirmation": "¿Estás seguro de que quieres abandonar el chat?", + "leave-community": "Salir de la comunidad", + "leave-confirmation": "Abandonar chat", + "leave-group": "Abandonar grupo", + "left": "salió", + "les-ulc": "LES\/ULC", + "lets-go": "Vamos", + "light": "Claro", + "linked-on": "Vinculado en {{date}}", + "load-messages-before": "antes del {{date}}", + "load-more-messages": "↓ Recuperar más mensajes", + "load-more-timeline": "↓ Obtener más", + "loading": "Cargando...", + "local-notifications": "Notificaciones locales", + "lock-app-with": "Bloquear app con", + "log-level": "Nivel de registro", + "log-level-settings": "Ajustes del nivel de registro", + "logging": "Registro", + "logging-enabled": "¿Registro habilitado?", + "login-pin-description": "Ingresa tu código de acceso de 6 dígitos para desbloquear tus claves", + "logout": "Cerrar sesión", + "logout-app-content": "Se cerrará la sesión de la cuenta. Cuando la desbloquees de nuevo, se utilizará la red seleccionada", + "logout-are-you-sure": "¿Seguro que quieres cerrar la sesión?", + "logout-title": "¿Cerrar sesión?", + "looking-for-cards": "Buscando tarjetas...", + "lost-connection": "Conexión perdida", + "mail-should-be-configured": "El cliente de correo debe ser configurado", + "mailserver-address": "Dirección del nodo de Status", + "mailserver-automatic": "Selección automática", + "mailserver-automatic-switch-explanation": "Elija el nodo de Status más rápido disponible", + "mailserver-connection-error": "No se pudo conectar al nodo de Status", + "mailserver-content": "Un nodo en la red de Status que envía y almacena mensajes, hasta por 30 días.", + "mailserver-details": "Detalles del nodo de Status", + "mailserver-error-content": "No se pudo acceder al nodo de Status que seleccionó.", + "mailserver-error-title": "Error al conectarse al nodo de Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Elija otro nodo de Status", + "mailserver-reconnect": "No se pudo conectar con el nodo de Status. Pulse para reconectar", + "mailserver-request-error-content": "El siguiente error fue devuelto por el nodo de Status: {{error}}", + "mailserver-request-error-status": "Se produjo un error al recuperar el historial. Por favor, verifica los registros para obtener más detalles", + "mailserver-request-error-title": "Error de solicitud del nodo de Status", + "mailserver-request-retry": "Solicitud de reintento", + "mailserver-retry": "Reintentar", + "mailserver-title": "Nodo de Status", + "main-currency": "Moneda principal", + "main-networks": "Redes principales", + "main-wallet": "Billetera Principal", + "mainnet-network": "Red principal", + "make-admin": "Hacer admin", + "manage-keys-and-storage": "Administrar las claves y el almacenamiento", + "mark-all-read": "Marcar todo como leído", + "master-account": "Cuenta principal", + "maybe-later": "Quizás más tarde", + "member-kick": "Eliminar miembro", + "members": { + "one": "1 miembro", + "other": "{{count}} miembros" + }, + "members-active": { + "one": "1 miembro", + "other": "{{count}} miembros" + }, + "members-active-none": "no hay miembros", + "members-count": "{{count}} miembros", + "members-label": "Miembros", + "members-limit-reached": "Límite de miembros alcanzado", + "members-title": "Miembros", + "membership-approval": "Requiere aprobación", + "membership-approval-description": "Puedes unirte de manera gratuita, pero el creador de la comunidad deberá aprobar primero a los nuevos miembros", + "membership-button": "Requisito de membresía", + "membership-declined": "Se rechazó la solicitud de membresía", + "membership-description": "La membresía grupal requiere que seas aceptado por el administrador del grupo", + "membership-ens": "Requiere un nombre de usuario ENS", + "membership-ens-description": "Tu comunidad requiere que tengas un nombre usuario ENS para unirte", + "membership-free": "Sin requisitos", + "membership-free-description": "Cualquier persona puede unirse a tu comunidad", + "membership-invite": "Necesitas invitación de otro miembro", + "membership-invite-description": "Únicamente puedes unirte a la comunidad mediante invitación de un miembro existente", + "membership-none": "Ninguno", + "membership-none-placeholder": "Puedes exigir que los nuevos miembros cumplan con ciertos criterios antes de poder unirse. Esto se puede cambiar en cualquier momento.", + "membership-request-pending": "Solicitud de membresía pendiente", + "membership-requests": "Solicitudes de membresía", + "membership-title": "Requisito de membresía", + "message": "Mensaje", + "message-not-sent": "Mensaje no enviado", + "message-options-cancel": "Cancelar", + "message-reply": "Responder", + "messages": "Mensajes", + "might-break": "Puede que se rompan algunas ÐApps", + "migrations-failed-content": "{{message}} \n versión del esquema: inicial {{initial-version}}, actual {{current-version}}, último {{last-version}} \n\nOcurrió un problema de base de datos. Tus fondos y la clave de chat están a salvo. Otros datos, como chats y contactos, no se podrán recuperar. Presionar el botón \"{{erase-multiaccounts-data-button-text}}\" eliminará el resto de datos y te permitirá acceder a tus fondos y enviar mensajes.", + "mobile-network-ask-me": "Preguntarme cuando esté en la red móvil", + "mobile-network-continue-syncing": "Continuar sincronizando", + "mobile-network-continue-syncing-details": "Puedes cambiarlo más adelante en los ajustes", + "mobile-network-go-to-settings": "Ir a ajustes", + "mobile-network-settings": "Datos móviles", + "mobile-network-sheet-configure": "Puedes configurar la sincronización en más \ndetalle en", + "mobile-network-sheet-offline": "Sin wifi, la sincronización de mensajes está deshabilitada.", + "mobile-network-sheet-offline-details": "La sincronización mediante la red móvil está desactivada", + "mobile-network-sheet-remember-choice": "Recordar mi elección", + "mobile-network-sheet-settings": "ajustes", + "mobile-network-start-syncing": "Iniciar la sincronización", + "mobile-network-stop-syncing": "Detener la sincronización", + "mobile-network-stop-syncing-details": "¿Hasta que te conectes a wifi?", + "mobile-network-use-mobile": "Usar datos móviles", + "mobile-network-use-mobile-data": "Status utiliza muchos datos al sincronizar los chats y la billetera.", + "mobile-network-use-wifi": "Sólo wifi", + "mobile-syncing-sheet-details": "Status utiliza muchos datos al sincronizar los chats y la billetera.", + "mobile-syncing-sheet-title": "¿Sincronizar usando los datos móviles?", + "more": "más", + "move-and-reset": "Mover y restablecer", + "move-keystore-file": "Mover el archivo keystore", + "move-keystore-file-to-keycard": "¿Mover el archivo keystore a la keycard?", + "multiaccount-exists-content": "Las claves para esta cuenta ya existen y no se pueden añadir de nuevo. Si perdiste tu contraseña, código de acceso o Keycard, desinstala la app, reinstálala y accede insertando tu frase semilla", + "multiaccount-exists-title": "Las claves para esta cuenta ya existen", + "multiaccounts-recover-enter-phrase-text": "Ingresa 12, 15, 18, 21 o 24 palabras. \n Separa las palabras por un solo espacio.", + "multiaccounts-recover-enter-phrase-title": "Ingresa tu frase semilla", + "mute": "Silenciar", + "my-accounts": "Mis cuentas", + "my-accounts-empty": "Tus cuentas disponibles aparecerán aquí", + "my-status": "Mi estado", + "name": "Nombre", + "name-of-token": "El nombre de tu token", + "name-optional": "Nombre (opcional)", + "name-your-channel": "Asigna un nombre a tu canal", + "name-your-channel-placeholder": "Nombre del canal", + "name-your-community": "Asigna un nombre a tu comunidad", + "name-your-community-placeholder": "Elige un nombre memorable", + "need-help": "¿Necesitas ayuda?", + "network": "Red", + "network-chain": "Cadena de la red", + "network-details": "Detalles de red", + "network-fee": "Tarifa de la red", + "network-id": "ID de red", + "network-info": "Información de la red", + "network-invalid-network-id": "El id de red especificado no corresponde al id de red por el URL de RPC", + "network-invalid-status-code": "Código de estado inválido: {{code}}", + "network-invalid-url": "La URL de la red no es válida", + "network-settings": "Ajustes de la red", + "new": "Nuevo", + "new-chat": "Nuevo chat", + "new-community-title": "Nueva comunidad", + "new-contact": "Nuevo contacto", + "new-contract": "Nuevo contrato", + "new-favourite": "Nuevo favorito", + "new-group": "Nuevo grupo", + "new-group-chat": "Nuevo grupo de chat", + "new-network": "Nueva red", + "new-pin-description": "Ingresa el nuevo código de acceso de 6 dígitos", + "new-public-group-chat": "Unirte a chat público", + "new-status": "Nuevo estado", + "new-tab": "Nueva pestaña", + "next": "Siguiente", + "nickname": "Apodo", + "nickname-description": "Los apodos te ayudan a identificar a otras personas en Status. \n Solo tú puedes ver los apodos que agregaste", + "no": "No", + "no-collectibles": "No hay coleccionables disponibles", + "no-contacts": "Aún no hay contactos", + "no-keycard-applet-on-card": "No hay applet de Keycard en la tarjeta", + "no-messages": "No hay mensajes", + "no-pairing-slots-available": "Esta tarjeta ya está vinculada con 5 dispositivos y no puede ser vinculada con este dispositivo. Por favor, utiliza uno de los dispositivos vinculados, inicia sesión con esta tarjeta y libera los espacios de vinculación en la tarjeta", + "no-result": "Sin resultados", + "no-thanks": "No gracias", + "no-tokens-found": "No se encontraron tokens", + "node-info": "Información del nodo", + "node-version": "Versión de nodo", + "nodes-disabled": "Nodos de Status deshabilitados", + "non-archival-node": "El punto de conexión de RPC no admite solicitudes de archivado. Es posible que el historial de transferencias locales esté incompleto.", + "nonce": "Nonce", + "none": "Ninguno", + "not-applicable": "No aplicable para transacciones no firmadas", + "not-connected-nodes": "Sin conexión a un nodo de Status", + "not-connected-to-peers": "Sin conexión a ningún nodo", + "not-enough-snt": "No hay suficiente SNT", + "not-keycard-text": "La tarjeta que usaste no es una Keycard. Necesitas comprar una Keycard para usarla", + "not-keycard-title": "No es una Keycard", + "not-registered": "no registrado", + "notification-settings": "Notificaciones", + "notifications": "Notificaciones", + "notifications-non-contacts": "Notificaciones de no contactos", + "notifications-preferences": "Preferencias de notificación", + "notifications-servers": "Servidores de notificaciones", + "notifications-switch": "Mostrar notificaciones", + "notifications-transactions": "Transacciones de billetera", + "notify": "Notificar", + "now": "Ahora", + "off": "Apagado", + "off-status-tree": "Derivado fuera de Status", + "offline": "Desconectado", + "offline-messaging-use-history-explanation": "Habilite los nodos del historial para recuperar los mensajes que se enviaron mientras la aplicación estaba cerrada. Cuando está habilitado, un nodo de historial obtiene su dirección IP. Cuando está deshabilitado, no recibirá mensajes cuando la aplicación esté cerrada y no los verá cuando abra la aplicación más tarde.", + "offline-messaging-use-history-nodes": "Usar nodos de Status", + "ok": "OK", + "ok-continue": "Vale, continuar", + "ok-got-it": "Entendido", + "ok-save-pass": "OK, guardar contraseña", + "okay": "Vale", + "on": "En", + "on-status-tree": "En el árbol de Status", + "once-enabled-share-metadata": "Una vez habilitados, los enlaces publicados en el chat pueden compartir sus metadatos con el sitio.", + "open": "Abrir", + "open-chat": "Abrir chat", + "open-dapp": "Abrir ÐApp", + "open-dapp-store": "Descubre ÐApps", + "open-home": "Abrir...", + "open-in-new-tab": "Abrir en una pestaña nueva", + "open-membership": "Abrir membresía", + "open-nfc-settings": "Abrir ajustes de NFC", + "opening-buy-crypto": "Abriendo {{site}} ...", + "optional": "opcional", + "or": "O", + "outgoing": "Saliente", + "outgoing-transaction": "Transacción saliente", + "page-camera-request-blocked": "solicitudes de cámara bloqueadas. Para habilitar las solicitudes de la cámara, ve a Ajustes", + "page-would-like-to-use-camera": "quisiera usar la cámara", + "pair": "Vincular dispositivos", + "pair-card": "Vincular a este dispositivo", + "pair-code": "Vincular código", + "pair-code-explanation": "Vincula la tarjeta a un dispositivo diferente (hasta 5) para desbloquear claves y firmar transacciones con la misma Keycard", + "pair-code-placeholder": "Código de vinculación...", + "pair-this-card": "Vincula esta tarjeta", + "pair-this-device": "Publicar el dispositivo", + "pair-this-device-description": "Vincula tus dispositivos para sincronizar los contactos y chats entre ellos", + "paired-devices": "Dispositivos vinculados", + "pairing": "Vinculando", + "pairing-card": "Vinculando la tarjeta", + "pairing-go-to-installation": "Ir a los ajustes de vinculación", + "pairing-maximum-number-reached-content": "Por favor, deshabilita uno de tus dispositivos antes de habilitar uno nuevo.", + "pairing-maximum-number-reached-title": "Número máximo de dispositivos alcanzados", + "pairing-new-installation-detected-content": "Se ha detectado un nuevo dispositivo. \nPara usar tus dispositivos correctamente, es importante vincularlos y habilitarlos antes de usarlos. \nVe a la sección de dispositivos en los ajustes para vincular tus dispositivos.", + "pairing-new-installation-detected-title": "Nuevo dispositivo detectado", + "pairing-no-info": "Sin información", + "pairing-please-set-a-name": "Por favor, establece un nombre para tu dispositivo.", + "passphrase": "Frase de contraseña", + "password": "Contraseña", + "password-description": "Al menos 6 caracteres. Tu contraseña protege tus claves. La necesitas para desbloquear Status y realizar transacciones.", + "password-placeholder": "Contraseña...", + "password-placeholder2": "Confirma tu contraseña", + "password-reset-in-progress": "Cambiando contraseña...", + "password_error1": "Las contraseñas no coinciden.", + "paste": "Pegar", + "paste-json": "Pegar JSON", + "pay-to-chat": "Pagar para chatear", + "peer-content": "Un dispositivo conectado a la red de chat de Status. Cada usuario puede representar uno o más peers, dependiendo de su número de dispositivos.", + "peer-title": "Pares", + "peer-to-peer": "Peer to peer", + "peers": "Pares", + "pending": "Pendiente", + "pending-confirmation": "Confirmación pendiente...", + "pending-invitations": "Solicitudes de membresía pendientes", + "permissions": "Permisos", + "phone-e164": "Internacional 1", + "photos": "Fotos", + "photos-access-error": "Para permitir el acceso a fotos, por favor, ve a los ajustes del sistema y asegúrate de que esté seleccionada la opción Status > Fotos", + "pin-changed": "Se ha cambiado el código de acceso de 6 dígitos", + "pin-code": "Código de acceso de 6 dígitos", + "pin-mismatch": "Código de acceso incorrecto", + "pin-one-attempt": "un intento", + "pin-one-attempt-blocked-after": "antes de que tu Keycard se bloquee", + "pin-one-attempt-blocked-before": "Ten cuidado, solo tienes", + "pin-one-attempt-frozen-after": "antes de que tu Keycard se congele", + "pin-one-attempt-frozen-before": "Ten cuidado, solo tienes", + "pin-retries-left": "Te quedan {{number}} reintentos", + "preference": "Preferencia", + "preview-privacy": "Vista previa del modo de privacidad", + "previewing-may-share-metadata": "La vista previa de los enlaces de estos sitios web puede compartir sus metadatos con sus propietarios.", + "privacy": "Privacidad", + "privacy-and-security": "Privacidad y seguridad", + "privacy-policy": "Política de privacidad", + "private-key": "Clave privada", + "private-notifications": "Notificaciones privadas", + "private-notifications-descr": "Status te notificará sobre los nuevos mensajes. Puedes editar tus preferencias de notificación más adelante en los ajustes.", + "processing": "Un momento", + "product-information": "Información del producto", + "profile": "Perfil", + "profile-deleted-content": "Tu perfil fue eliminado con éxito", + "profile-deleted-keycard": "Ahora puedes restaurar otro par de claves en tu Keypair", + "profile-deleted-title": "Perfil eliminado", + "profile-details": "Detalles del perfil", + "profile-pic-pick": "Seleccionar de la galería", + "profile-pic-remove": "Eliminar foto", + "profile-pic-take": "Tomar foto", + "public-channel": "Canal público", + "public-chat": "Chat público", + "public-chat-description": "¡Únete a chats públicos de tu interés! Cualquiera puede comenzar uno nuevo.", + "public-chats": "Chats públicos", + "public-group-status": "Público", + "public-group-topic": "Tema", + "public-key": "Clave pública", + "puk-and-pairing-codes-displayed": "PUK y códigos de vinculación mostrados", + "puk-code": "Código PUK", + "puk-code-explanation": "Si olvidas tu código de acceso de 6 dígitos o lo ingresas incorrectamente 3 veces, necesitarás este código para desbloquear tu tarjeta.", + "puk-mismatch": "El código PUK no coincide", + "push-failed-transaction": "Su transacción falló", + "push-failed-transaction-body": "{{value}} {{currency}} a {{to}}", + "push-inbound-transaction": "Recibiste {{value}} {{currency}}", + "push-inbound-transaction-body": "De {{from}} a {{to}}", + "push-notifications-server-enabled": "Servidor habilitado", + "push-notifications-servers": "Servidores de notificaciones push", + "push-outbound-transaction": "Enviaste {{value}} {{currency}}", + "push-outbound-transaction-body": "De {{from}} a {{to}}", + "quiet-days": "{{quiet-days}} días", + "quiet-hours": "{{quiet-hours}} horas", + "re-encrypt-key": "Vuelve a encriptar tus claves", + "receive": "Recibir", + "receive-transaction": "Recibir transacción", + "recent": "Recientes", + "recent-empty": "Las direcciones utilizadas recientemente aparecerán aquí", + "recent-recipients": "Contactos", + "recently-used-stickers": "Las pegatinas usadas recientemente aparecerán aquí", + "recipient": "Receptor", + "recipient-code": "Ingresa la dirección del destinatario", + "recipient-code-placeholder": "0x... o username.domain.eth", + "recover": "Recuperar", + "recover-key": "Ingresar claves existentes", + "recover-keycard-multiaccount-not-supported": "Las claves para esta cuenta ya existen y no se pueden añadir de nuevo. Si perdiste tu contraseña, código de acceso o Keycard, desinstala la app, reinstálala y accede insertando tu frase semilla", + "recover-with-keycard": "Recuperar con Keycard", + "recover-with-seed-phrase": "Recuperar con frase semilla", + "recovering-key": "Ingresando las claves...", + "recovery-confirm-phrase": "Confirmar frase semilla", + "recovery-phrase": "Frase semilla", + "recovery-success-text": "Deberás crear un nuevo código o contraseña para volver a encriptar tus claves", + "recovery-typo-dialog-description": "Recuerda, tu frase semilla deben ser exactamente las mismas palabras y el orden en que recibiste", + "recovery-typo-dialog-title": "¿Es correcta la frase semilla?", + "redeem-amount": "{{quantity}} bonos disponibles", + "redeem-now": "Canjear ahora", + "redeem-success": "¡Canje del bono exitoso!", + "refresh": "Actualizar", + "registered": "registrado", + "remember-me": "Recuérdame", + "remind-me-later": "Verlo de nuevo", + "remove": "Eliminar", + "remove-favourite": "Quitar favorito", + "remove-from-chat": "Eliminar del chat", + "remove-from-contacts": "Eliminar de contactos", + "remove-from-contacts-text": "Al eliminar a un usuario de tu lista de contactos, no ocultas la dirección de tu billetera de ellos", + "remove-group": "Eliminar grupo", + "remove-network": "Eliminar red", + "remove-token": "Eliminar token", + "removed": "eliminado", + "repeat-pin": "Repite el nuevo código de acceso de 6 dígitos", + "replying-to": "Respondiendo a {{author}}", + "report-bug-email-template": "1. Descripción del problema \n (Describe la función que deseas, o resume brevemente el error y lo que hiciste, lo que esperabas que sucediera y lo que realmente sucede. Secciones a continuación) \n\n\n 2. Pasos para reproducir \n (Describe cómo podemos replicar el error paso a paso). \n -Abre Status \n -... \n -Paso 3, etc. \n\n\n 3. Resultado esperado \n (Describe lo que esperabas que sucediera). \n\n\n 4. Resultado real \n (Describe lo que realmente sucedió). \n\n\n 5. Adjunta capturas de pantalla que puedan demostrar el problema\n", + "request-access": "Solicitar acceso", + "request-feature": "Solicitar una funcionalidad", + "request-membership": "Solicitar membresía", + "request-pending": "Pedido pendiente…", + "request-transaction": "Solicitar transacción", + "required-field": "Campo obligatorio", + "resend-message": "Reenviar", + "reset-card": "Reiniciar tarjeta", + "reset-card-description": "Esta operación restablecerá la tarjeta al estado inicial. Borrará todos los datos de la tarjeta, incluidas las claves privadas. La operación no es reversible.", + "reset-database": "Restablecer base de datos", + "reset-database-warning": "Elimina chats, contactos y ajustes. Se requiere cuando has perdido tu contraseña", + "reset-database-warning-keycard": "Borrar chats, contactos y ajustes", + "restore-defaults": "Restaurar los valores predeterminados", + "retry": "Reintentar", + "revoke-access": "Revocar el acceso", + "rpc-url": "RPC URL", + "rpc-usage-copy": "Copiar", + "rpc-usage-get-stats": "Actualizar", + "save": "Guardar", + "save-password": "Guardar contraseña", + "save-password-unavailable": "Establece el código de acceso del dispositivo para guardar la contraseña", + "save-password-unavailable-android": "Guardar la contraseña no está disponible: tu dispositivo puede estar rooteado o carecer de las características de seguridad necesarias.", + "scan-qr": "Escanear QR", + "scan-qr-code": "Escanea un código QR con una dirección de billetera", + "scan-tokens": "Escanear tokens", + "search": "Buscar", + "search-no-chat-found": "No hay resultados de búsqueda. ¿Querés decir", + "secret-keys-confirmation-text": "Los necesitará para seguir usando su Keycard en caso de que alguna vez pierda su teléfono.", + "secret-keys-confirmation-title": "¿Escribiste los códigos?", + "security": "Seguridad", + "see-details": "Ver detalles", + "see-it-again": "VERLO OTRA VEZ", + "seed-key-uid-mismatch": "La frase semilla no coincide", + "seed-key-uid-mismatch-desc-1": "La frase semilla que ingresaste no coincide con {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Para administrar claves para esta cuenta verifica tu frase semilla e inténtalo de nuevo", + "seed-phrase-content": "Un set de palabras fáciles de leer, seleccionadas al azar de la lista estándar BIP39 y utilizadas para recuperar o acceder a tu cuenta de Ethereum en otras billeteras y dispositivos. También se conoce como «frase mnemónica»,«frase de recuperación» o «copia de seguridad de billetera» en todo el cripto ecosistema. La mayoría de las cripto apps usan este mismo estándar para generar cuentas.", + "seed-phrase-placeholder": "Frase semilla...", + "seed-phrase-title": "Frase semilla", + "select": "Seleccionar", + "select-account": "Seleccionar cuenta", + "select-account-dapp": "Selecciona la cuenta que deseas usar con Dapps", + "select-account-first": "Selecciona una cuenta primero", + "select-chat": "Selecciona el chat para comenzar a enviar mensajes", + "select-new-location-for-keys": "Selecciona una nueva ubicación para guardar las claves privadas", + "selected": "Seleccionado", + "send-logs": "Reportar un error", + "send-logs-to": "Reportar un error a {{email}}", + "send-message": "Enviar mensaje", + "send-push-notifications-description": "Cuando está deshabilitado, la persona que recibe tus mensajes no será notificada de su llegada.", + "send-request": "Enviar solicitud", + "send-request-amount": "Cantidad", + "send-request-amount-max-decimals": "El número máximo de decimales es {{asset-decimals}}", + "send-request-unknown-token": "Token desconocido: {{asset}}", + "send-sending-to": "para {{recipient-name}}", + "send-transaction": "Enviar transacción", + "sending": "Enviando", + "sent-at": "Enviado a", + "server": "Servidor", + "set-a-topic": "Crear un tema", + "set-currency": "Establecer moneda", + "set-custom-fee": "Establecer tarifa personalizada", + "set-dapp-access-permissions": "Establecer permisos de acceso de DApp", + "set-max": "Establecer máximo", + "settings": "Ajustes", + "share": "Compartir", + "share-address": "Compartir dirección", + "share-chat": "Compartir chat", + "share-contact-code": "Compartir mi clave de chat", + "share-dapp-text": "Echa un vistazo a esta DApp que utilizo en Status: {{link}}", + "share-link": "Compartir enlace", + "share-my-profile": "Compartir mi perfil", + "share-profile": "Compartir perfil", + "share-profile-link": "Compartir enlace del perfil", + "share-public-chat-text": "Echa un vistazo a este chat público en la app de Status: {{link}}", + "shared": "Compartido", + "sharing-copied-to-clipboard": "Copiado", + "sharing-copy-to-clipboard": "Copiar", + "sharing-data-desc-6": "La información es removida de tu teléfono después de ser enviada", + "sharing-share": "Compartir", + "show-less": "Mostrar menos", + "show-more": "Mostrar más", + "show-notifications": "Mostrar notificaciones", + "show-profile-pictures": "Mostrar fotos de perfil de", + "show-qr": "Mostrar código QR", + "show-transaction-data": "Mostrar los datos de la transacción", + "sign-and-send": "Firmar y enviar", + "sign-anyway": "Firmar de todas maneras", + "sign-in": "Registrarse", + "sign-message": "Firmar mensaje", + "sign-out": "Cerrar sesión", + "sign-request-failed": "No se pudo firmar el mensaje", + "sign-with": "Firmar con", + "sign-with-password": "Firmar con contraseña", + "sign-you-in": "Iniciando sesión...", + "signing": "Firma", + "signing-a-message": "Firmando un mensaje", + "signing-phrase": "Frase de firma", + "something-went-wrong": "Algo salió mal", + "soon": "Pronto", + "specify-address": "Especifica la dirección", + "specify-name": "Especifica un nombre", + "specify-network-id": "Especifica ID de la red", + "specify-rpc-url": "Especifica una URL RPC", + "specify-server-public-key": "Ingresa la clave pública del servidor", + "start-chat": "Iniciar chat", + "start-conversation": "Empezar conversación", + "start-group-chat": "Iniciar chat grupal", + "start-new-chat": "Iniciar nuevo chat", + "starter-pack-coming": "El Paquete de Bienvenida viene en camino", + "starter-pack-coming-description": "Puede tomar de unos minutos a horas", + "starter-pack-received": "Paquete de Bienvenida", + "starter-pack-received-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "status": "Status", + "status-confirmed": "Confirmado", + "status-hardwallet": "Status hardwallet", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status tiende a usar una gran cantidad de datos al sincronizar chats. Puedes optar por no sincronizar cuando estás en la red móvil", + "status-not-sent-click": "No está confirmado. Haz clic para ver las opciones", + "status-not-sent-tap": "No confirmado. Toca para ver las opciones", + "status-pending": "Pendiente", + "status-sent": "Enviado", + "status-tx-not-found": "TX no encontrada", + "status-updates-descr": "Las actualizaciones de estado aparecerán aquí. Agregue el perfil como un contacto para recibir actualizaciones en su línea de tiempo.", + "statuses-descr": "Comparte lo que tienes en mente y mantente al día con tus contactos", + "statuses-my-status-descr": "Comparte lo que tienes en mente. Cualquiera que visite su perfil podrá ver su estado. Las personas que te agreguen como contacto recibirán tus actualizaciones en su línea de tiempo.", + "step-i-of-n": "Paso {{step}} de {{number}}", + "sticker-market": "Tienda de pegatinas", + "storage": "Almacenamiento", + "submit": "Enviar", + "submit-bug": "Reportar un error", + "success": "Éxito", + "symbol": "Símbolo", + "sync-all-devices": "Sincronizar todos los dispositivos", + "sync-in-progress": "Sincronizando...", + "sync-settings": "Ajustes de sincronización", + "sync-synced": "Sincronizado", + "syncing-devices": "Sincronizando...", + "system": "Sistema", + "tabs": "Pestañas", + "tag-was-lost": "La etiqueta se perdió", + "tap-card-again": "Vuelve a tocar la tarjeta con la parte trasera de tu teléfono.", + "terms-of-service": "Condiciones de uso", + "test-networks": "Redes de prueba", + "text-input-disabled": "Por favor, espera un momento...", + "thank-you": "Gracias", + "this-device": "Este dispositivo", + "this-device-desc": "Tus claves serán encriptadas y almacenadas de forma segura en tu dispositivo", + "this-is-you-signing": "Esta es tu frase de firma", + "this-will-take-few-seconds": "Esto tomará unos segundos", + "three-words-description": "Deberías ver estas tres palabras antes de firmar cada transacción", + "three-words-description-2": "Si ves una combinación diferente, cancela la transacción y cierra sesión", + "timeline": "Línea de tiempo", + "to": "Para", + "to-block": "Bloquear", + "to-enable-biometric": "Para habilitar {{bio-type-label}}, debes guardar tu contraseña en la pantalla de desbloqueo", + "to-encrypt-enter-password": "Para encriptar la cuenta, por favor, ingresa tu contraseña", + "to-see-this-message": "Para ver este mensaje,", + "token-auto-validate-decimals-error": "Decimales incorrectos para el token {{symbol}} en la dirección {{address}} - establecido en {{expected}} pero detectado como {{actual}}", + "token-auto-validate-name-error": "Nombre incorrecto para el token {{symbol}} en la dirección {{address}} - establecido en {{expected}} pero detectado como {{actual}}", + "token-auto-validate-symbol-error": "Símbolo incorrecto para el token {{symbol}} en la dirección {{address}} - configurado en {{expected}} pero detectado como {{actual}}", + "token-details": "Detalles del token", + "topic-name-error": "Utiliza solo letras minúsculas (a-z), números y guiones (-). No uses las claves de chat", + "transaction": "Transacción", + "transaction-data": "Transaction data", + "transaction-declined": "Transacción rechazada", + "transaction-description": "Transacciones con 12 confirmaciones pueden considerarse irreversibles", + "transaction-details": "Detalles de la transacción", + "transaction-failed": "Transacción fallida", + "transaction-history": "Historial de transacciones", + "transaction-request": "Solicitud de transacción", + "transaction-sent": "Transacción enviada", + "transaction-signed": "La transacción se ha firmado con éxito", + "transactions": "Transacciones", + "transactions-filter-select-all": "Seleccionar todo", + "transactions-filter-title": "Filtrar historial", + "transactions-history": "Historial de transacciones", + "transactions-history-empty": "Aún no hay transacciones en tu historial", + "transactions-history-loading": "Cargando historial de transacciones. Esto podrá tomar un tiempo.", + "transactions-load-more": "Mostrar más", + "transactions-sign": "Firmar", + "transfer-ma-unknown-error-desc-1": "Parece que tu multicuenta no se eliminó. Es posible que la base de datos se haya restablecido", + "transfer-ma-unknown-error-desc-2": "Por favor, comprueba tu lista de cuentas e inténtalo de nuevo. Si la cuenta no aparece en la lista, ve a Acceder a las claves existentes para recuperarlas con la frase semilla", + "transfers-fetching-failure": "No se ha podido actualizar el historial de transferencias. Comprueba la conexión y refresca la página para intentarlo de nuevo", + "tribute-required-by-multiaccount": "{{multiaccount-name}} requiere SNT para iniciar un chat.", + "tribute-state-paid": "Tributo pagado", + "tribute-state-pending": "Tributo pendiente", + "tribute-state-required": "Requiere {{snt-amount}} tributo de SNT", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Agregar amigos como contacto para permitir chats sin pago de tributo.", + "tribute-to-talk-are-you-friends": "¿Sois amigos?", + "tribute-to-talk-ask-to-be-added": "Pide añadirlo como contacto", + "tribute-to-talk-contact-received-your-tribute": "recibió tu tributo. Ahora podéis conversar con seguridad.", + "tribute-to-talk-desc": "Monetiza tu atención requiriendo SNT para que la gente nueva inicie un chat", + "tribute-to-talk-disabled": "Tribute to Talk deshabilitado", + "tribute-to-talk-disabled-note": "A partir de ahora, las personas nuevas pueden iniciar un chat contigo sin enviar SNT.", + "tribute-to-talk-enabled": "Tienes Tribute to Talk habilitado.", + "tribute-to-talk-finish-desc": "A partir de ahora, sólo recibirás chats de los contactos, y de la gente que pagó ", + "tribute-to-talk-learn-more-1": "Tu tiempo y atención son tus activos más valiosos. Tribute to Talk te permite configurar la cantidad de SNT requerida para que nuevas personas inicien un chat contigo.", + "tribute-to-talk-learn-more-2": "Cualquier persona que no esté en tu lista de contactos deberá pagar y tú podrás responder una vez que lo hayan hecho.", + "tribute-to-talk-learn-more-3": "Siempre puedes devolver el dinero, pero para asegurarte de que tus amigos puedan contactarte libremente, añádelos primero como contacto.", + "tribute-to-talk-paywall-learn-more-1": "Nuestro tiempo y atención son nuestros activos más valiosos. Tribute to Talk te permite contactar a nuevas personas a cambio de un pago de SNT.", + "tribute-to-talk-paywall-learn-more-2": "Para iniciar un chat con alguien que tiene configurado tributos, simplemente paga el SNT requerido y serás agregado como contacto.", + "tribute-to-talk-paywall-learn-more-3": "Si los conoces, puedes compartir tu perfil fuera de Status para ser agregado de forma gratuita.", + "tribute-to-talk-pending": "Tributo pendiente de confirmación", + "tribute-to-talk-pending-note": "La transacción de tributo está pendiente de confirmación en la red. Puedes verificar el status en el historial de transacciones", + "tribute-to-talk-removing-note": "Eliminar Tribute to Talk permitirá que nuevas personas inicien un chat sin enviar SNT. Requiere que se realice una transacción.", + "tribute-to-talk-set-snt-amount": "Establece la cantidad de SNT requerida para que nuevas personas inicien un chat", + "tribute-to-talk-signing": "Esperando para firmar la transacción", + "tribute-to-talk-transaction-failed-note": "La transacción ha fallado y tu configuración de Tribute to Talk no ha cambiado", + "tribute-to-talk-tribute-received1": "Tributo recibido. Tú y ", + "tribute-to-talk-tribute-received2": "ahora sois contactos y podéis chatear de forma segura.", + "tribute-to-talk-you-require-snt": "Se necesita SNT para que las personas nuevas inicien un chat.", + "try-again": "Inténtalo de nuevo", + "try-keeping-the-card-still": "Intenta no mover la tarjeta", + "turn-nfc-description": "NFC está deshabilitado en tu dispositivo. Puedes habilitarlo en los ajustes", + "turn-nfc-on": "Activa NFC para continuar", + "tx-fail-description1": "Es probable que esta transacción falle. Firma bajo tu propio riesgo utilizando una tarifa de red personalizada.", + "tx-fail-description2": "Es probable que esta transacción falle. Establece una tarifa de red personalizada para firmar bajo tu propio riesgo.", + "type": "Tipo", + "type-a-message": "Mensaje", + "ulc-enabled": "ULC habilitado", + "unable-to-fetch": "No es posible recuperar el historial de chat", + "unable-to-read-this-code": "No se puede leer este código", + "unable-to-send-messages": "No es posible enviar o recibir mensajes", + "unblock": "Desbloquear", + "unblock-contact": "Desbloquear a este usuario", + "unknown-status-go-error": "Error de status-go desconocido", + "unlock": "Desbloquear", + "unmute": "Activar sonido", + "unpair-card": "Desvincular tarjeta", + "unpair-card-confirmation": "Esta operación desvinculará la tarjeta del dispositivo actual. Requiere autorización del código de acceso de 6 dígitos. ¿Quieres continuar?", + "unpair-keycard": "Desvincular la tarjeta de acceso de este teléfono", + "unpair-keycard-warning": "Tu código de vinculación\/ PUK y PIN permanecen sin cambios", + "unpaired-keycard-text": "La Keycard que pulsaste no está asociada con este teléfono", + "unpaired-keycard-title": "Parece que tu tarjeta ha sido desvinculada", + "update": "Actualizar", + "update-to-listen-audio": "¡Actualizá a la última versión para escuchar un mensaje de audio aquí!", + "update-to-see-image": "¡Actualizá a la última versión para ver una buena imagen aquí!", + "update-to-see-sticker": "¡Actualizá a la última versión para ver una bonita pegatina aquí!", + "url": "URL", + "usd-currency": "Dólar estadounidense", + "use-valid-contact-code": "Por favor, ingresa o escanea una clave de contacto o nombre de usuario válido", + "validation-amount-invalid-number": "La cantidad no es un número válido", + "validation-amount-is-too-precise": "La cantidad es muy precisa. El número máximo de decimales es {{decimals}}.", + "version": "Versión de la app", + "view": "Ver", + "view-cryptokitties": "Ver en CryptoKitties", + "view-cryptostrikers": "Ver en CryptoStrikers", + "view-details": "Ver detalles", + "view-etheremon": "Ver en Etheremon", + "view-gitcoin": "Ver en Gitcoin", + "view-profile": "Ver perfil", + "view-signing": "Ver la frase de firma", + "view-superrare": "Ver en SuperRare", + "waiting-for-wifi": "Sin wifi, la sincronización de mensajes está deshabilitada.", + "waiting-for-wifi-change": "Ajustes", + "waiting-to-sign": "Esperando para firmar la transacción...", + "waiting-wi-fi": "Esperando wifi…", + "waku-bloom-filter-mode": "Modo de filtro Waku bloom", + "wallet": "Billetera", + "wallet-address": "Dirección de la billetera", + "wallet-asset": "Activo", + "wallet-assets": "Activos", + "wallet-backup-recovery-title": "Respalda tu frase semilla", + "wallet-choose-recipient": "Elegir destinatario", + "wallet-collectibles": "Coleccionables", + "wallet-insufficient-funds": "Fondos insuficientes", + "wallet-insufficient-gas": "No hay suficiente ETH para el gas", + "wallet-invalid-address": "Dirección inválida: \n {{data}}", + "wallet-invalid-address-checksum": "Error en la dirección: \n {{data}}", + "wallet-invalid-chain-id": "La red no coincide: \n {{data}} pero la cadena actual es {{chain}}", + "wallet-key-content": "Una dirección hexadecimal de 64 caracteres basada en el estándar Ethereum y que comienza con 0x. De cara al público, tu clave de billetera se comparte con otros cuando deseas recibir fondos. También se conoce como «dirección de Ethereum» o «dirección de billetera».", + "wallet-key-title": "Dirección de la cuenta", + "wallet-manage-assets": "Administrar activos", + "wallet-request": "Solicitud", + "wallet-send": "Enviar", + "wallet-send-min-units": "Mínimo 21 000 unidades", + "wallet-send-min-wei": "Mín. 1 wei", + "wallet-settings": "Ajustes de billetera", + "wallet-total-value": "Valor total", + "wallet-transaction-total-fee": "Costo total", + "wants-to-access-profile": "quiere acceder a tu perfil", + "warning": "Advertencia", + "warning-message": "Lo sentimos, limitamos el envío de varios mensajes en rápida sucesión para evitar el spam. Por favor, inténtalo de nuevo en un momento.", + "warning-sending-to-contract-descr": "La dirección que ingresó es un contrato inteligente, enviar fondos a esta dirección puede resultar en la pérdida de fondos. Para interactuar con una DApp, abra la DApp en el navegador de Status.", + "watch-only": "Sólo para ver", + "web-view-error": "No se puede cargar la página", + "websites": "Sitios web", + "webview-camera-permission-requests": "Solicitudes de permisos de la cámara Webview", + "webview-camera-permission-requests-subtitle": "Cuando está habilitado, los sitios web y las dapps pueden solicitar el uso de la cámara", + "welcome-blank-community-message": "Tus comunidades aparecerán aquí.", + "welcome-blank-message": "Tus chats aparecerán aquí. Para iniciar nuevos chats, presiona el botón ⊕", + "welcome-community-blank-message": "Tus chats aparecerán aquí. Para iniciar nuevos chats, pulsa en los 3 puntos de arriba y selecciona «Crear un canal».", + "welcome-screen-text": "¡Configura tu billetera, invita a amigos a chatear \n y busca dapps populares!", + "welcome-to-status": "¡Bienvenidos a Status!", + "welcome-to-status-description": "Configura tu criptobilletera, invita a tus amigos a chatear y busca apps descentralizadas", + "whats-on-your-mind": "Lo que tienes en mente...", + "word-count": "Número de palabras", + "word-n": "Palabra #{{number}}", + "word-n-description": "Para comprobar si has realizado una copia de seguridad de la frase semilla correctamente, escribe arriba la palabra #{{number}}.", + "words-n": { + "one": "1 palabra", + "other": "{{count}} palabras" + }, + "write-down-and-store-securely": "Anota los códigos\n y guárdalos de forma segura", + "wrong-address": "Dirección incorrecta", + "wrong-card": "Tarjeta incorrecta", + "wrong-card-text": "Has tocado una tarjeta que no corresponde a las claves seleccionadas", + "wrong-contract": "Contrato incorrecto", + "wrong-keycard-text": "La Keycard que pulsaste no está asociada con este teléfono", + "wrong-keycard-title": "Parece que has pulsado \nuna keycard incorrecta", + "wrong-password": "Contraseña incorrecta", + "wrong-word": "Palabra incorrecta", + "yes": "Sí", + "you": "tú", + "you-already-have-an-asset": "Ya tienes un activo {{value}}", + "you-are-all-set": "¡Está todo listo!", + "you-are-all-set-description": "Ahora, si pierdes tu teléfono, puedes acceder a tus fondos y clave de chat usando la frase semilla", + "you-can-change-account": "Puedes cambiar el nombre y el color de la cuenta a lo que deseas", + "you-can-choose-preview-websites": "Puede elegir cuál de los siguientes sitios web puede previsualizar el enlace de las descripciones e imágenes en los chats", + "you-can-fetch": "Puedes recuperar el historial de chat", + "you-dont-have-contacts": "Todavía no tenés ningún contacto.", + "you-dont-have-stickers": "Todavía no tienes pegatinas", + "you-will-need-this-code": "Necesitarás este código para abrir Status y firmar transacciones", + "you-will-start-from-scratch": "Comenzarás desde cero con un nuevo set de claves", + "your-card-is-frozen": "Tu Keycard está congelada. Restablece el acceso a la tarjeta", + "your-contact-code": "Conceder acceso autoriza a esta DApp a obtener tu clave de chat", + "your-data-belongs-to-you": "Si pierdes la frase semilla, pierdes tus datos y fondos", + "your-data-belongs-to-you-description": "Si pierdes acceso, por ejemplo, al perder tu teléfono, solo puedes acceder a tus claves con tu frase semilla. Nadie aparte de ti tiene tu frase semilla. Anótala y guárdala de forma segura.", + "your-keys": "Tus claves", + "your-recovery-phrase": "Tu frase semilla", + "your-recovery-phrase-description": "Esta es tu frase semilla. La usas para comprobar que esta es tu billetera. ¡Sólo la verás una vez! Escríbela en un papel y guárdala en un lugar seguro. La necesitarás si pierdes o reinstalas tu billetera.", + "youre-on-mobile-network": "Estás en una red móvil" +} diff --git a/translations/es_419.json b/translations/es_419.json new file mode 100644 index 00000000000..1bc4b9ac681 --- /dev/null +++ b/translations/es_419.json @@ -0,0 +1,1814 @@ +{ + "K": "K", + "M": "M", + "NFT": "NFT", + "You": "Tú", + "about-app": "Acerca de", + "about-key-storage-content": "Status nunca accederá a tu clave privada. Asegúrate de respaldar tu frase semilla. Si pierdes tu teléfono es la única manera de acceder a tus claves.", + "about-key-storage-title": "Acerca del almacenamiento de claves", + "about-names-content": "¡Nadie puede pretender ser tú! Eres anónimo por defecto y nunca tienes que revelar tu nombre real. Puedes registrar un nombre personalizado por una pequeña tarifa.", + "about-names-title": "Los nombres no se pueden cambiar", + "about-sharing-data": "Sobre compartir datos", + "accept": "Aceptar", + "accept-and-add": "Aceptar y agregar", + "accept-and-continue": "Aceptar y continuar", + "accept-and-share-address": "Aceptar y compartir dirección", + "accept-community-rules": "Acepto las reglas de la comunidad", + "accept-new-chats-from": "Aceptar nuevos mensajes de", + "accept-status-tos-prefix": "Acepto Status", + "access-existing-keys": "Ingresar claves existentes", + "access-key": "Clave de acceso", + "account-added": "Cuenta agregada", + "account-color": "Color de la cuenta", + "account-content": "Puedes comparar las cuentas en Status con las cuentas bancarias. Al igual que una cuenta bancaria, una cuenta suele tener una dirección y un saldo; se utiliza esta cuenta para realizar transacciones con Ethereum. Puedes tener varias cuentas en tu billetera. Todas accedidas en Status.", + "account-exists-title": "Cuenta ya existe", + "account-is-used": "La cuenta se está usando con Dapps en el navegador.", + "account-name": "Nombre de la cuenta", + "account-settings": "Ajustes de la cuenta", + "account-title": "Cuenta", + "accounts": "Cuentas", + "actions": "Acciones", + "active-members": "Miembros activos", + "active-online": "En línea", + "active-unknown": "Desconocido", + "activity": "Actividad", + "add": "Agregar", + "add-a-watch-account": "Agregar una dirección de solo observación", + "add-account": "Agregar una cuenta", + "add-account-description": "Puedes importar cualquier tipo de cuenta de Ethereum para agregarla a tu billetera de Status", + "add-account-incorrect-password": "La contraseña parece ser incorrecta. Ingresa la contraseña que usas para desbloquear la app.", + "add-an-account": "Agregar una cuenta", + "add-bootnode": "Agregar bootnode", + "add-contact": "Agregar contacto", + "add-custom-token": "Agregar un token personalizado", + "add-favourite": "Agregar favorito", + "add-mailserver": "Agregar nodo de Status", + "add-members": "Agregar miembros", + "add-network": "Agregar red", + "add-new-contact": "Agregar nuevo contacto", + "add-nickname": "Agregar un apodo (opcional)", + "add-node": "Agregar nodo", + "add-private-key-account": "Ingresa una cuenta de una llave privada", + "add-seed-account": "Agrega una cuenta con una frase semilla", + "add-to-contacts": "Agregar a contactos", + "add-to-contacts-text": "Al agregar un usuario a tu lista de contactos, compartes la dirección de tu billetera", + "add-to-favourites": "Agregar a favoritos", + "add-watch-account": "Agrega una cuenta sólo para mirar", + "added": "añadido", + "address": "Dirección", + "address-or-ens-name": "Dirección o nombre ENS", + "address-received": "Dirección recibida", + "address-request-accepted": "Solicitud de dirección aceptada", + "address-requested": "Dirección solicitada", + "admin": "Admin", + "advanced": "Avanzado", + "advanced-settings": "Ajustes avanzados", + "advertiser-description": "Descubriste Status gracias a un socio nuestro. ¿Podemos chequear tu IP para poder recompensarlo? Esta información no será usada para nada más, y será removida completamente después de 7 días", + "advertiser-starter-pack-accept": "Aceptar", + "advertiser-starter-pack-decline": "Rechazar", + "advertiser-starter-pack-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "advertiser-starter-pack-title": "Paquete de Bienvenida", + "advertiser-title": "Privacidad por defecto", + "agree-by-continuing": "Al continuar, aceptas \n nuestras", + "all": "Todos", + "all-connections": "Todas las conexiones", + "allow": "Permitir", + "allow-and-send": "Permitir y enviar", + "allow-mention-notifications": "Mostrar @ menciones", + "allowing-authorizes-this-dapp": "Permitir autoriza a esta DApp a recuperar la dirección de tu billetera y habilitar Web3", + "alphabetically": "Alfabéticamente", + "already-have-asset": "Ya tienes este activo", + "amount": "Cantidad", + "and": "y", + "anyone": "Cualquiera", + "app-connections": "Conexiones de la App", + "appearance": "Apariencia", + "apply": "Aplicar", + "approve": "Aprobar", + "approve-limit": "Aprobar el límite", + "approve-token": "Aprobar el token", + "approve-token-contract-desc": "Aprobar un token con un contrato te permite gastar tu saldo de tokens. Si sientes que un proyecto no es confiable, no apruebes el token con ellos, o aprueba sólo la cantidad que vas a utilizar con ellos.\n", + "are-you-sure": "¿Estás seguro\/a?", + "are-you-sure-description": "No podrás volver a ver toda la frase semilla de nuevo", + "are-you-sure-to-cancel": "¿Estas seguro que quieres cancelar?", + "are-you-sure?": "¿Estás seguro\/a?", + "ask-in-status": "Haz una pregunta o reporta un error", + "at": "en", + "attribution-received": "{{attrib}} de {{max}} bonos recibidos", + "audio": "Audio", + "audio-message": "Mensaje de audio", + "audio-recorder": "Grabadora", + "audio-recorder-error": "Error de la grabadora", + "audio-recorder-max-ms-reached": "Tiempo máximo de grabación alcanzado", + "audio-recorder-permissions-error": "Tenés que dar permiso para enviar mensajes de audio", + "authorize": "Autorizar", + "available": "Disponible", + "available-participants": { + "one": "Puedes seleccionar un participante más", + "other": "Puedes seleccionar {{count}} participantes más" + }, + "back": "Atrás", + "back-up": "Respaldo", + "back-up-seed-phrase": "Respalda la frase semilla", + "back-up-your-seed-phrase": "Respalda tu frase semilla", + "backing-up": "Creando copia de seguridad ...", + "backup-disabled": "Deshabilitado", + "backup-enabled": "Activado", + "backup-recovery-phrase": "Copia de seguridad de frase semilla", + "backup-settings": "Ajustes de copia de seguridad", + "backup-through-waku": "Copia de seguridad a través de waku", + "bad-fees-description": "Tu tarifa de prioridad está por debajo de nuestros parámetros sugeridos.", + "balance": "Saldo", + "begin-set-up": "empezar la configuración", + "below-base-fee": "tarifa máxima por debajo de la tarifa base", + "biometric-auth-android-sensor-desc": "Sensor táctil", + "biometric-auth-android-sensor-error-desc": "Fallido", + "biometric-auth-android-title": "Autenticación requerida", + "biometric-auth-confirm-logout": "Volver a iniciar sesión", + "biometric-auth-confirm-message": "Para continuar, la autenticación biométrica es necesaria, si no es posible inicia sesión de nuevo usando la contraseña de las llaves.", + "biometric-auth-confirm-title": "¡Debes autenticarte!", + "biometric-auth-confirm-try-again": "Inténtalo de nuevo", + "biometric-auth-error": "No se puede realizar la autenticación biométrica ({{code}})", + "biometric-auth-login-error-title": "Error de autenticación biométrica", + "biometric-auth-login-ios-fallback-label": "Ingresa la contraseña", + "biometric-auth-reason-login": "Iniciar sesión en Status", + "biometric-auth-reason-verify": "Verificar autenticación", + "biometric-disable-bioauth": "deshabilitar {{bio-type-label}}", + "biometric-disable-password-description": "Si deshabilitas esto, también", + "biometric-disable-password-title": "Deshabilitar guardar contraseña", + "biometric-enable": "Si no deseas ingresar tu contraseña cada vez para acceder a la app, habilita el inicio de sesión {{bio-type-label}}", + "biometric-enable-button": "Habilitar {{bio-type-label}}", + "biometric-enable-keycard": "Si no deseas usar tu Keycard cada vez para acceder a la app, habilita el inicio de sesión {{bio-type-label}}", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Huella dactilar", + "biometric-secure-with": "Asegurar con {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "Contraseña BIP39", + "blank-keycard-text": "Puedes continuar con tu keycard una vez que hayas generado tus claves y tu nombre", + "blank-keycard-title": "Parece que has hecho tapping \nuna keycard en blanco", + "blank-messages-text": "Tus mensajes estarán aquí", + "block": "Bloque", + "block-contact": "Bloquear este usuario", + "block-contact-details": "El bloqueo eliminará los mensajes anteriores de este usuario y evitará que otros nuevos lleguen a ti", + "block-user": "Bloquear usuario", + "blocked-users": "Usuarios bloqueados", + "bold": "Negrita", + "bootnode-address": "Dirección de bootnode", + "bootnode-details": "Detalles de Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes habilitados", + "bootnodes-settings": "Ajustes de bootnode", + "browsed-websites": "El historial del navegador aparecerá aquí", + "browser": "Navegador", + "browser-not-secure": "¡La conexión no es segura! No firmes transacciones o envíes datos personales en este sitio.", + "browser-secure": "La conexión es segura. Asegúrate de que realmente confías en este sitio antes de firmar transacciones o ingresar datos personales.", + "browsers": "Navegadores", + "browsing-cancel": "Cancelar", + "browsing-open-in-android-web-browser": "Abrir en Android", + "browsing-open-in-ios-web-browser": "Abrir en iOS", + "browsing-open-in-status": "Abrir en Status", + "browsing-site-blocked-description1": "Hemos detectado posibles actividades maliciosas desde esta dirección. Para protegerte a ti y a tu billetera, prevenimos la navegación.\n\nSi crees que esto es un error, haznoslo saber en la", + "browsing-site-blocked-description2": "chat público", + "browsing-site-blocked-go-back": "Regresar", + "browsing-site-blocked-title": "Este sitio está bloqueado", + "browsing-title": "Navegar", + "bug-report": "Reportar un error", + "bug-report-description": "*Descripción", + "bug-report-description-placeholder": "Requerido, no puede estar vacío", + "bug-report-steps": "Pasos para la reproducir", + "bug-report-steps-placeholder": "- abrir la app\n- hacer algo\n- y luego algo más...", + "bug-report-submit-email": "Enviar por correo electrónico con el archivo de registros", + "bug-report-submit-gh-issue": "Enviar un problema de GitHub sin registros", + "bug-report-too-short-description": "La descripción es muy corta", + "build-yourself": "Para utilizar la aplicación sin estas Condiciones de Uso, puede crear su propia versión", + "buy-crypto": "Comprar criptomonedas", + "buy-crypto-choose-a-service": "Elige un servicio que quieras usar para comprar criptomonedas", + "buy-crypto-description": "Encuentre una dapp para comprar cripto ahora", + "buy-crypto-leaving": "Estás saliendo de Status y entrando en un sitio web de terceros para completar tu compra", + "buy-crypto-title": "Parece que tu billetera está vacía", + "camera-access-error": "Para permitir el acceso a la cámara, por favor ve a ajustes de sistema y asegúrate que la opción Status > Cámara está seleccionada.", + "can-not-add-yourself": "Ese eres tú, para iniciar un chat elige a alguien más", + "can-send-messages": "Puedes enviar y recibir mensajes nuevos", + "cancel": "Cancelar", + "cancel-keycard-setup": "Cancelar la configuración de Keycard", + "cancelling": "Cancelando", + "cannot-pin-desc": "Solo puedes anclar un máximo de 3 mensajes.\nDesancla al menos uno para anclar uno nuevo.", + "cannot-pin-title": "¡No puedes anclar este mensaje!", + "cannot-read-card": "No puede leer la tarjeta.\nPor favor, mantenla en la parte posterior de tu teléfono", + "cannot-use-default-pin": "No está permitido el código de acceso 000000. \nPor favor utiliza otro número", + "cant-open-public-chat": "No se puede abrir el chat público", + "cant-report-bug": "No puedo reportar un error", + "card-is-blank": "Esta tarjeta esta en blanco", + "card-reseted": "La tarjeta ha sido reseteada", + "card-unpaired": "La tarjeta se ha desvinculado del dispositivo actual", + "category": "Categoría", + "category-title": "Título de la categoría", + "change-fleet": "Cambiar fleet a {{fleet}}", + "change-logging-enabled": "¿Está seguro\/a de que deseas {{enable}} iniciar sesión?", + "change-pairing": "Cambiar el código de emparejamiento", + "change-pairing-description": "Cambiar el código de emparejamiento no afecta a los emparejamientos actuales. Sin embargo, cualquier nuevo emparejamiento requerirá el nuevo código.", + "change-pairing-title": "Crea un nuevo código de emparejamiento", + "change-passcode": "Cambiar código de acceso", + "change-password": "Cambiar contraseña", + "change-pin": "Cambiar el código de acceso de 6 dígitos", + "change-puk": "Cambiar PUK de 12 dígitos", + "change-tip": "Cambiar propina", + "changed-amount-warning": "La cantidad se cambió de {{old}} a {{new}}", + "changed-asset-warning": "El activo se cambió de {{old}} a {{new}}", + "chaos-mode": "Modo caos", + "chaos-unicorn-day": "Día del Unicornio del Caos", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Chatea y realiza transacciones privadas con amigos", + "chat-is-a-contact": "Contacto", + "chat-is-not-a-contact": "No es contacto", + "chat-key": "Clave de chat", + "chat-key-content": "Los mensajes en el protocolo de chat de Status se envían y reciben utilizando claves de encriptación. La clave pública de chat es una cadena de caracteres que compartes con otros para que puedan enviarte mensajes en Status.", + "chat-key-title": "Clave de chat", + "chat-link-previews": "Vistas previas de enlaces de chat", + "chat-name": "nombre de chat", + "chat-name-content": "Tres palabras aleatorias, derivadas algorítmicamente de tu clave de chat y utilizadas como tu alias predeterminado en el chat. Los nombres de los chats son completamente únicos; ningún otro usuario puede tener las mismas tres palabras.", + "chat-name-title": "Nombre del chat", + "chat-notification-preferences": "Configuración de las notificaciones", + "chat-settings": "Ajustes de chat", + "chats": "Chats", + "check-on-opensea": "Comprobar en opensea", + "check-your-recovery-phrase": "Comprueba tu frase semilla", + "choose-actions": "Elige acciones", + "choose-authentication-method": "Elije un método de autenticación", + "choose-new-location-for-keystore": "Seleccione una nueva ubicación para guardar su archivo keystore", + "choose-storage": "Elegir almacenamiento", + "clear": "Limpiar", + "clear-all": "Borrar todo", + "clear-history": "Borrar historial", + "clear-history-action": "Limpiar", + "clear-history-confirmation": "¿Borrar historial?", + "clear-history-confirmation-content": "¿Seguro que quieres limpiar este historial de chat?", + "clear-history-title": "¿Borrar historial?", + "close": "Cerrar", + "close-all": "Cerrar todo", + "close-app-button": "Confirmar", + "close-app-content": "La aplicación se detendrá y cerrará. Cuando la vuelvas a abrir, se utilizará la red seleccionada", + "close-app-title": "¡Advertencia!", + "collectibles-leak-metadata": "Puedes mostrar tus NFTs aquí. Si lo haces, compartirás tu billetera y tu dirección IP", + "command-button-send": "Enviar", + "communities": "Comunidades", + "communities-alpha": "Comunidades (alfa)", + "communities-enabled": "Comunidades habilitadas", + "communities-verified": "Comunidad de Status verificada", + "community": "Comunidad", + "community-channel": "Canal de comunidad", + "community-color": "Color de la comunidad", + "community-color-placeholder": "Elige un color", + "community-edit-title": "Editar comunidad", + "community-emoji-thumbnail-title": "Miniatura", + "community-image-pick": "Elige una imagen", + "community-image-remove": "Remover", + "community-image-take": "Toma una foto", + "community-info": "Información sobre la Comunidad", + "community-info-not-found": "No se encontró la información de la comunidad", + "community-invite-title": "Invitá", + "community-key": "Llave privada de la comunidad", + "community-key-placeholder": "Escribe la llave privada de la comunidad", + "community-link": "Link de comunidad", + "community-members": { + "one": "miembro", + "other": "miembros" + }, + "community-members-title": "Miembros", + "community-message-preview": "Invitación para unirse a {{community-name}}", + "community-notification-settings": "Configuración de notificaciones", + "community-private-key": "Clave privada comunitaria", + "community-requests-to-join-title": "Solicitudes de membresía", + "community-roles": "Rol", + "community-rules": "Reglas de la comunidad", + "community-share-title": "Compartir...", + "community-thumbnail-image": "Imagen en miniatura", + "community-thumbnail-upload": "Subir", + "complete-hardwallet-setup": "Esta tarjeta ahora está vinculada. La necesitarás para firmar transacciones y desbloquear tus llaves", + "completed": "Completado", + "confirm": "Confirmar", + "confirm-new-password": "Confirmar nueva contraseña", + "confirm-pairing-code-placeholder": "Confirma tu código de emparejamiento ...", + "confirm-password-placeholder": "Confirmá tu contraseña...", + "confirmation-request": "Solicitud de confirmación", + "confirmations": "Confirmaciones", + "confirmations-helper-text": "Cuando la transacción tenga 12 confirmaciones, puede considerarla resuelta.", + "connect": "Conectar", + "connect-mailserver-content": "¿Conectar con {{name}}?", + "connect-wallet": "Conectar billetera", + "connected": "Conectado", + "connected-to": "Conectado a", + "connecting": "Conectando...", + "connecting-requires-login": "Para conectarse a otra red es necesario iniciar sesión", + "connection-status": "Estado de conexión", + "connection-with-the-card-lost": "La conexión con la tarjeta\nse ha perdido", + "connection-with-the-card-lost-setup-text": "Para reanudar la configuración, manten la tarjeta en\n la parte trasera de tu teléfono y mantener la\n tarjeta en contacto con el teléfono", + "connection-with-the-card-lost-text": "Para proceder, manten la tarjeta en la parte posterior de tu teléfono", + "contact-code": "Clave de chat", + "contact-request-sent": "solicitud de contacto enviada", + "contact-requests": "Solicitudes de contacto", + "contact-s": { + "one": "contacto", + "other": "contactos" + }, + "contacts": "Contactos", + "contacts-descr": "Tus contactos aparecerán aquí. Recibirá actualizaciones de estado de cualquier persona que agregue como contacto", + "contacts-empty": "Contactos con nombres ENS aparecerán aquí", + "continue": "Continuar", + "continue-anyway": "Continuar de todas maneras", + "contract-address": "Dirección del contrato", + "contract-interaction": "Interacción con el contrato", + "contract-isnt-supported": "El contrato no es compatible", + "copy-info": "Copiar información", + "copy-qr": "Copiar código", + "copy-text": "Copiar texto", + "copy-to-clipboard": "Copiar", + "copy-transaction-hash": "Copiar ID de transacción", + "cost-fee": "Costo\/Tarifa", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Crear", + "create-a-pin": "Crear un código de acceso de 6 dígitos", + "create-a-puk": "Crea un PUK de 12 dígitos", + "create-category": "Crear categoría", + "create-channel": "Crea un canal", + "create-channel-title": "Nuevo canal", + "create-community": "Crea una comunidad", + "create-group-chat": "Crear chat grupal", + "create-multiaccount": "Generar llaves", + "create-new-key": "Obtener nuevas claves", + "create-pin": "Crea un código de acceso de 6 dígitos", + "create-pin-description": "Necesitarás tu tarjeta + este código de 6 dígitos para desbloquear Status y confirmar las transacciones", + "created-group-chat-description": "Creaste el grupo {{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Moneda", + "currency-display-name-aed": "Dirham de los Emiratos Árabes Unidos", + "currency-display-name-afn": "Afgani afgano", + "currency-display-name-ars": "Peso argentino", + "currency-display-name-aud": "Dólar australiano", + "currency-display-name-bbd": "Dólar de Barbados", + "currency-display-name-bdt": "Taka bangladeshí", + "currency-display-name-bgn": "Lev búlgaro", + "currency-display-name-bhd": "Dinar bahreiní", + "currency-display-name-bnd": "Dólar de Brunéi", + "currency-display-name-bob": "Bolivia Bolíviano", + "currency-display-name-brl": "Real brasilero", + "currency-display-name-btn": "Ngultrum butanés", + "currency-display-name-cad": "Dólar canadiense", + "currency-display-name-chf": "Franco suizo", + "currency-display-name-clp": "Peso chileno", + "currency-display-name-cny": "Yuan chino", + "currency-display-name-cop": "Peso colombiano", + "currency-display-name-crc": "Colón costarricense", + "currency-display-name-czk": "Corona checa", + "currency-display-name-dkk": "Corona danesa", + "currency-display-name-dop": "Peso de la República Dominicana", + "currency-display-name-egp": "Libra egipcia", + "currency-display-name-etb": "Birr etíope", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Libra esterlina", + "currency-display-name-gel": "Lari georgiano", + "currency-display-name-ghs": "Cedi ghanés", + "currency-display-name-hkd": "Dólar de Hong Kong", + "currency-display-name-hrk": "Croacia Kuna", + "currency-display-name-huf": "Forinto húngaro", + "currency-display-name-idr": "Rupia Indonesia", + "currency-display-name-ils": "Nuevo séquel (Israel)", + "currency-display-name-inr": "Rupia india", + "currency-display-name-isk": "Corona islandesa", + "currency-display-name-jmd": "Dólar jamaiquino", + "currency-display-name-jpy": "Yen japonés", + "currency-display-name-kes": "Chelín keniano", + "currency-display-name-krw": "Won surcoreano", + "currency-display-name-kwd": "Dinar kuwaití", + "currency-display-name-kzt": "Tenge kazajo", + "currency-display-name-lkr": "Rupia de Sri Lanka", + "currency-display-name-mad": "Dirham marroquí", + "currency-display-name-mdl": "Leu moldavo", + "currency-display-name-mur": "Rupia de Mauricio", + "currency-display-name-mwk": "Kwacha malauí", + "currency-display-name-mxn": "Peso mexicano", + "currency-display-name-myr": "Ringgit malayo", + "currency-display-name-mzn": "Metical mozambiqueño", + "currency-display-name-nad": "Dólar namibio", + "currency-display-name-ngn": "Naira nigeriana", + "currency-display-name-nok": "Corona noruega", + "currency-display-name-npr": "Rupia nepalí", + "currency-display-name-nzd": "Dólar neozelandés", + "currency-display-name-omr": "Rial omaní", + "currency-display-name-pen": "Sol peruano", + "currency-display-name-pgk": "Kina de Papúa Nueva Guinea", + "currency-display-name-php": "Peso filipino", + "currency-display-name-pkr": "Rupia pakistaní", + "currency-display-name-pln": "Zloty de Polonia", + "currency-display-name-pyg": "Guaraní paraguayo", + "currency-display-name-qar": "Riyal qatarí", + "currency-display-name-ron": "Leu de Rumanía", + "currency-display-name-rsd": "Dinar serbio", + "currency-display-name-rub": "Rublo ruso", + "currency-display-name-sar": "Arabia Saudita Riyal", + "currency-display-name-sek": "Corona sueca", + "currency-display-name-sgd": "Dólar de Singapur", + "currency-display-name-thb": "Baht tailandés", + "currency-display-name-try": "Lira turca", + "currency-display-name-ttd": "Dólar de Trinidad y Tobago", + "currency-display-name-twd": "Nuevo dólar taiwanés", + "currency-display-name-tzs": "Chelín tanzano", + "currency-display-name-uah": "Grivna (Ucrania)", + "currency-display-name-ugx": "Chelín ugandés", + "currency-display-name-usd": "Dólar estadounidense", + "currency-display-name-uyu": "Peso uruguayo", + "currency-display-name-vef": "Bolívar venezolano", + "currency-display-name-vnd": "Dong vietnamita", + "currency-display-name-zar": "Rand sudafricano", + "current": "Actual", + "current-average": "Promedio actual", + "current-average-tip": "Promedio de propina actual", + "current-base": "Base actual", + "current-base-fee": "Tarifa básica actual", + "current-minimum-tip": "Propina mínima actual", + "current-network": "Red actual", + "current-password": "Contraseña actual", + "current-pin": "Ingresa el código de acceso de 6 dígitos", + "current-pin-description": "Ingresa tu código de acceso de 6 dígitos para continuar", + "custom": "Personalizado", + "custom-networks": "Redes personalizadas", + "custom-node": "Está utilizando un punto de conexión RPC personalizado. Es posible que el historial de transferencias locales esté incompleto.", + "custom-seed-phrase": "Frase semilla inválida", + "custom-seed-phrase-text-1": "Esta frase semilla no coincide con nuestro diccionario compatible. Revisa si hay palabras mal escritas.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Aceptar y Abrir", + "dapp-starter-pack-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "dapp-starter-pack-title": "Paquete de Bienvenida", + "dapp-would-like-to-connect-wallet": "quisiera conectarse a", + "dapps": "ÐApps", + "dapps-permissions": "Permisos de DApp", + "dark": "Oscuro", + "data": "Datos", + "data-collected": "Datos recogidos", + "data-collected-subtitle": "La siguiente tabla muestra los datos exactos que se almacenan y se enviarán. Los datos se validan contra las normas públicas para garantizar que ningún dato sensible sea enviado. No confíes, verifica.", + "data-syncing": "Sincronización de datos", + "database-reset-content": "Se han eliminado los chats, los contactos y la configuración. Ahora puedes usar tu cuenta con tu Keycard", + "database-reset-title": "Reiniciar la base de datos", + "database-reset-warning": "La base de datos se restablecerá. Se borrarán los chats, los contactos y la configuración.", + "datetime-ago": "hace", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "día", + "other": "días" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "hora", + "other": "horas" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "minuto", + "other": "minutos" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "segundo", + "other": "segundos" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "hoy", + "datetime-yesterday": "Ayer", + "decimals": "Decimales", + "decline": "Rechazar", + "decryption-failed-content": "Ocurrió un error desencriptando tus datos. Es posible que tengas que generar unas nuevas llaves y borrar tus datos antiguos pulsando \"Aplicar\", o haz clic en \"Cancelar\" para volver a intentarlo", + "default": "Predeterminados", + "default-assets": "Por defecto ERC20 y ERC721", + "default-sync-period": "Sincronizar historial para", + "delete": "Eliminar", + "delete-account": "Borrar cuenta", + "delete-and-leave-group": "Eliminar y salir del grupo", + "delete-bootnode": "Eliminar bootnode", + "delete-bootnode-are-you-sure": "¿Seguro que quieres eliminar este bootnode?", + "delete-bootnode-title": "Eliminar bootnode", + "delete-category-confirmation": "¿Estás seguro que deseas eliminar esta categoría?", + "close-chat": "Eliminar chat", + "close-chat-confirmation": "¿Estás seguro de que quieres eliminar este chat?", + "delete-confirmation": "¿Eliminar?", + "delete-for-everyone": "Borrar para todos", + "delete-for-me": "Eliminar para mi", + "delete-keys-keycard": "Eliminar llaves de Keycard", + "delete-mailserver": "Eliminar nodo de Status", + "delete-mailserver-are-you-sure": "¿Está seguro de que desea eliminar este nodo de Status?", + "delete-mailserver-title": "Eliminar nodo de Status", + "delete-message": "Eliminar mensaje", + "delete-my-account": "Borrar mi cuenta", + "delete-my-profile": "Eliminar mi perfil", + "delete-network-confirmation": "¿Seguro que quieres eliminar esta red?", + "delete-network-error": "Por favor, conéctate a una red diferente antes de eliminar esta", + "delete-network-title": "¿Eliminar red?", + "delete-node": "Eliminar nodo", + "delete-node-are-you-sure": "¿Estás seguro de que quieres eliminar este nodo?", + "delete-node-title": "Eliminar nodo", + "delete-profile": "Eliminar perfil", + "delete-profile-warning": "Advertencia: Si no tiene tu frase de semilla escrita, perderá el acceso a sus fondos después de eliminar su perfil", + "deny": "Negar", + "derivation-path": "Ruta de derivación", + "describe-channel": "Describir el canal", + "description": "Descripción", + "dev-mode": "Modo de desarrollo", + "dev-mode-settings": "Ajustes del modo de desarrollo", + "device-syncing": "Sincronización de dispositivos", + "devices": "Dispositivos", + "disable": "deshabilitar", + "disable-all": "Desactivar todo", + "disable-later-in-settings": "Puedes desactivar esto luego en los Ajustes", + "disabled": "Deshabilitado", + "disconnected": "Chat sin conexión", + "discover": "Descubrir", + "discover-communities": "Descubre comunidades", + "dismiss": "Descartar", + "display-collectibles": "Mostrar coleccionables", + "done": "Listo", + "dont-ask": "No me vuelvas a preguntar", + "edit": "Editar", + "edit-channel-title": "Editar canal", + "edit-chats": "Editar conversaciones", + "edit-community": "Editar comunidad", + "edit-favourite": "Editar favorito", + "edit-group": "Editar grupo", + "edit-message": "Editar mensaje", + "edit-name-and-image": "Editar nombre e imagen", + "edit-name-image": "Editar nombre e imagen", + "edit-nickname": "Editar apodo", + "edit-profile": "Editar perfil", + "edited": "Editado", + "editing-message": "Editando mensaje", + "empty-activity-center": "Sus notificaciones de chat\naparecerán acá", + "empty-chat-description": "No hay mensajes \nen este chat todavía", + "empty-chat-description-community": "Ha estado tranquilo aquí durante {{quiet-hours}} .", + "empty-chat-description-one-to-one": "Cualquier mensaje que envíes aquí está encriptado y solo puedes leerlo tú y", + "empty-chat-description-public": "Ha estado tranquilo aquí durante las últimas {{quiet-hours}}. Inicia la conversación o", + "empty-chat-description-public-share-this": "comparte este chat", + "empty-keycard-required": "Requiere una Keycard vacía", + "empty-pending-invitations-descr": "Las personas que deseen unirse al grupo\na través de un enlace de invitación aparecerá aquí", + "empty-tab": "Pestaña vacía", + "enable": "Habilitar", + "enable-all": "Habilitar todos", + "enable-link-previews": "¿Habilitar vistas previas de enlaces en el chat?", + "encrypt-with-password": "Encriptar con contraseña", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Agregar nombre de usuario", + "ens-agree-to": "De acuerdo con", + "ens-chat-settings": "Ajustes de chat", + "ens-custom-domain": "Dominio personalizado", + "ens-custom-username-hints": "Escribe el nombre de usuario completo, incluido el dominio personalizado como username.domain.eth", + "ens-custom-username-taken": "El nombre de usuario no te pertenece :(", + "ens-deposit": "Depósito", + "ens-dismiss-message": "Haga clic para descartar", + "ens-displayed-with": "Tus mensajes se muestran a otros con", + "ens-get-name": "Obtén un nombre de usuario universal", + "ens-got-it": "Ok, lo tengo.", + "ens-locked": "Nombre de usuario bloqueado. No lo podrás liberar hasta el {{date}}", + "ens-name-content": "Alias personalizado para tu clave de chat que puedes registrar utilizando Ethereum Name Service. Los nombres ENS son nombres de usuario descentralizados.", + "ens-name-not-found": "No se puede resolver el nombre ENS", + "ens-name-title": "Nombre ENS", + "ens-network-restriction": "Solo disponible en Mainnet", + "ens-no-usernames": "No tienes ningún nombre de usuario conectado", + "ens-powered-by": "Propulsado por Ethereum Name Services", + "ens-primary-username": "Nombre de usuario principal", + "ens-register": "Registrar", + "ens-registration-failed": "Para registrar el nombre de usuario, por favor, intenta de nuevo.", + "ens-registration-failed-title": "La transacción ha fallado", + "ens-registration-failure": "Registro fallido", + "ens-registration-in-progress": "Registro en curso...", + "ens-release-username": "Liberar nombre de usuario", + "ens-remove-hints": "La eliminación separará el nombre de usuario de tu clave.", + "ens-remove-username": "Eliminar nombre de usuario", + "ens-saved": "ahora está conectado con tu clave de chat y se puede usar en Status.", + "ens-saved-title": "Nombre de usuario agregado", + "ens-show-username": "Mostrar mi nombre de usuario ENS en los chats", + "ens-terms-header": "Términos de registro de nombre", + "ens-terms-point-1": "Los fondos se depositan por 1 año. Tu SNT se bloqueará, pero no se gastará.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Registro ENS).", + "ens-terms-point-2": "Después de 1 año, puedes liberar el nombre y recuperar tu depósito, o no tomar ninguna acción para mantener el nombre.", + "ens-terms-point-3": "Si los términos del contrato cambian – por ejemplo, Status realiza actualizaciones del contrato – el usuario tiene derecho a liberar el nombre de usuario independientemente del tiempo retenido.", + "ens-terms-point-4": "El controlador del contrato no puede acceder a tus fondos depositados. Sólo pueden ser devueltos a la dirección que los envió.", + "ens-terms-point-5": "Tu(s) dirección(es) se asociará(n) públicamente con tu nombre ENS.", + "ens-terms-point-6": "Los nombres de usuario se crean como nodos de subdominio de stateofus.eth y están sujetos a los términos del contrato inteligente de ENS.", + "ens-terms-point-7": "Autorizas el contrato para transferir SNT en tu nombre. Esto solo puede ocurrir cuando apruebas una transacción para autorizar la transferencia.", + "ens-terms-point-8": "Estos términos están garantizados por la lógica del contrato inteligente en las direcciones:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Términos de registro de nombre.", + "ens-test-message": "Hey", + "ens-transaction-pending": "Transacción pendiente...", + "ens-understand": "Entiendo que la dirección de mi billetera estará conectada públicamente a mi nombre de usuario.", + "ens-username": "Nombre de usuario ENS", + "ens-username-already-added": "El nombre de usuario ya está conectado con tu clave de chat y se puede usar dentro de Status.", + "ens-username-available": "✓ ¡Nombre de usuario disponible!", + "ens-username-connected": "Este nombre de usuario es de tu propiedad y está conectado con tu clave de chat.", + "ens-username-connected-continue": "Continuar configurando `Mostrar mi nombre de usuario ENS en los chats`.", + "ens-username-connected-with-different-key": "Continuar requerirá una transacción para conectar el nombre de usuario con tu clave de chat actual.", + "ens-username-connection-confirmation": "{{username}} se conectará una vez que se complete la transacción.", + "ens-username-hints": "Al menos 4 caracteres. Letras latinas, números y sólo minúsculas.", + "ens-username-invalid": "Sólo letras y números.", + "ens-username-owned": "✓ El nombre de usuario es tuyo.", + "ens-username-owned-continue": "Continuar conectará este nombre de usuario con tu clave de chat.", + "ens-username-registration-confirmation": "¡Bien! Eres el propietario de {{username}} una vez completada la transacción.", + "ens-username-taken": "Nombre de usuario ya está tomado :(", + "ens-username-you-can-follow-progress": "Puedes seguir el progreso en la sección de Historial de transacciones de tu billetera.", + "ens-usernames": "Nombres de usuario ENS", + "ens-usernames-details": "Registra un nombre de usuario universal para ser reconocido fácilmente por otros usuarios", + "ens-want-custom-domain": "Tengo un nombre en otro dominio", + "ens-want-domain": "Quiero un dominio stateofus.eth", + "ens-welcome-hints": "Los nombres ENS transforman esas locas direcciones largas en nombres de usuario únicos.", + "ens-welcome-point-customize": "Un nombre ENS puede reemplazar tu nombre aleatorio de 3 palabras en el chat. Sea @yourname en lugar de {{name}}.", + "ens-welcome-point-customize-title": "Personaliza tu nombre de chat", + "ens-welcome-point-receive": "Otros pueden enviarte fondos desde el chat en un simple paso.", + "ens-welcome-point-receive-title": "Recibe transacciones en el chat", + "ens-welcome-point-register": "Regístrate una vez para mantener el nombre para siempre. Después de 1 año, podrás liberar el nombre y recuperar tus SNT.", + "ens-welcome-point-register-title": "10 SNT a registrar", + "ens-welcome-point-simplify": "Puedes recibir fondos a tu ENS fácil de compartir, en vez de tu hash hexadecimal (0x...)", + "ens-welcome-point-simplify-title": "Simplifica tu dirección ETH", + "ens-welcome-point-verify": "Puedes verificar y agregar cualquier nombre de usuario que poseas en los siguientes pasos.", + "ens-welcome-point-verify-title": "¿Ya tienes un nombre de usuario?", + "ens-your-username": "Tu nombre de usuario", + "ens-your-usernames": "Tus nombres de usuario", + "ens-your-your-name": "Tu nombre ENS", + "enter-12-words": "Ingresa las 12 palabras de tu frase semilla, separadas por espacios", + "enter-a-private-key": "Ingresa una clave privada", + "enter-a-seed-phrase": "Ingresa una frase semilla", + "enter-address": "Ingresar dirección", + "enter-contact-code": "ENS (vitalik94) o clave de chat (0x04...)", + "enter-pair-code": "Ingrese tu código de vinculación", + "enter-pair-code-description": "Se te mostró el código de vinculación durante la configuración de Keycard", + "enter-password": "Ingresa la contraseña", + "enter-password-migration-prompt": "Ingresa tu contraseña para mover contactos, chats y ajustes junto con sus claves", + "enter-pin": "Ingresa el código de acceso de 6 dígitos", + "enter-puk-code": "Ingresa el código PUK", + "enter-puk-code-description": "El código de acceso de 6 dígitos ha sido bloqueado. \n Ingresa el código PUK para desbloquear el código de acceso.", + "enter-recipient-address-or-username": "Ingresá la dirección o nombre de usuario del destinatario", + "enter-seed-phrase": "Ingresa la frase semilla", + "enter-url": "Ingresa una URL", + "enter-user-pk": "Ingrese la clave pública del usuario", + "enter-watch-account-address": "Escanea un código QR \n o \n ingrese la dirección para observar", + "enter-word": "Ingresar palabra", + "enter-your-code": "Ingresa tu código de acceso de 6 dígitos", + "enter-your-password": "Ingresa tu contraseña", + "error": "Error", + "error-unable-to-get-balance": "No se puede obtener el saldo", + "error-unable-to-get-prices": "Error de conversión de moneda. Refresca tu pantalla para intentarlo de nuevo.", + "error-unable-to-get-token-balance": "No se puede obtener el saldo del token", + "errors": "Errores", + "eth": "ETH", + "ethereum-address": "Dirección de Ethereum", + "ethereum-node-started-incorrectly-description": "El nodo Ethereum se inició con una configuración incorrecta, la aplicación se detendrá para recuperarse de esa condición. ID de red configurado = {{network-id}}, actual = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "El nodo Ethereum se inició incorrectamente", + "etherscan-lookup": "Ver en Etherscan", + "everyone": "Todos", + "expand-all": "Ver todo", + "export-account": "Exportar la cuenta", + "export-key": "Exportar llave privada", + "external-link": "Link externo", + "external-storage-denied": "Se niega el acceso al almacenamiento externo", + "failed": "Fallido", + "faq": "Preguntas frecuentes", + "fast": "Rápida", + "favourite": "Favorito", + "favourite-description": "Tus sitios web favoritos aparecerán aquí", + "favourites": "Favoritos", + "favourites-empty": "Las direcciones agregadas a favoritos aparecerán aquí", + "featured": "Destacado", + "fee-cap": "Tope de tarifa", + "fee-explanation": "Precio total máximo por la transacción. Si la tarifa base del bloque supera este, se incluirá en un bloque siguiente con una tarifa base más baja.", + "fee-options": "Opciones de tarifas sugeridas", + "fetch-community": "Buscar la comunidad", + "fetch-messages": "recuperar-mensajes", + "fetch-timeline": "↓ Obtener", + "fetching-community": "Buscando comunidad...", + "find": "Encontrar", + "finish": "Finalizar", + "finishing-card-setup": "Finalizando la configuración de la tarjeta", + "fleet": "Fleet", + "fleet-settings": "Ajustes de fleet", + "follow": "Seguir", + "follow-your-interests": "Entra en un chat público y conoce gente nueva", + "free": "↓ Gratis", + "from": "De", + "gas-amount-limit": "Límite de cantidad de gas", + "gas-limit": "Límite de gas", + "gas-price": "Precio del gas", + "gas-used": "Gas utilizado", + "gated": "Vinculado", + "generate-a-key": "Generar claves", + "generate-a-new-account": "Genera una Cuenta", + "generate-a-new-key": "Generar una nueva clave", + "generate-account": "Generar claves", + "generate-an-account": "Genera una Cuenta", + "generate-new-key": "Generar claves", + "generating-codes-for-pairing": "> Descargando el software del producto a la tarjeta \n > Generando códigos de desbloqueo y vinculación", + "generating-keys": "Generando claves...", + "generating-mnemonic": "Generando frase semilla", + "get-a-keycard": "Obtenga una Keycard", + "get-started": "Iniciar", + "get-status-at": "Obtén Status en http:\/\/status.im\/es", + "get-stickers": "Obtén Stickers", + "gif": "GIF", + "give-a-short-description": "Dale una breve descripción", + "give-a-short-description-community": "Dale una breve descripción", + "give-permissions-camera": "Da permiso\npara acceder a la cámara", + "glossary": "Glosario", + "go-to-settings": "Ir a Configuración", + "got-it": "Entendido", + "grant-face-id-permissions": "Para conceder el permiso de Face ID requerido, ve a los ajustes de tu sistema y asegúrate de que esté seleccionado Status > Face ID", + "group-chat": "Chat grupal", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** se ha convertido en admin", + "group-chat-all-contacts-invited": "Todos tus contactos ya están en el grupo.", + "group-chat-created": "**{{member}}** creó el grupo **{{name}}**", + "group-chat-decline-invitation": "Rechazar invitación", + "group-chat-member-added": "**{{member}}** ha sido invitado", + "group-chat-member-joined": "**{{member}}** se ha unido al grupo", + "group-chat-member-removed": "**{{member}}** abandonó el grupo", + "group-chat-members-count": "{{selected}}\/{{max}} miembros", + "group-chat-name-changed": "**{{member}}** cambió el nombre del grupo a **{{name}}**", + "group-chat-no-contacts": "Aún no tienes contactos. \nInvita a tus amigos a comenzar a chatear", + "group-info": "Información del grupo", + "group-invite": "Invitación de grupo", + "group-invite-link": "Enlace de invitación de grupo", + "group-membership-request": "Solicitud de membresía grupal", + "groups": "Grupos", + "gwei": "Gwei", + "has-permissions": "tiene permiso para acceder", + "hash": "Hash", + "help": "ayuda", + "help-capitalized": "Ayuda", + "help-center": "Centro de ayuda", + "help-improve-status": "Ayuda a mejorar Status", + "hide": "Ocultar", + "hide-content-when-switching-apps": "Bloquear capturas de pantalla", + "hide-content-when-switching-apps-ios": "Ocultar vista previa", + "history": "Historial", + "history-nodes": "Nodos de Status", + "hold-card": "sostén la tarjeta en la parte posterior de tu teléfono", + "home": "Inicio", + "hooks": "Hooks", + "how-it-works": "Cómo funciona", + "http-gateway-error": "¡Oops, la solicitud falló!", + "identifier": "Identificador", + "identity-verification": "Verificación de identidad", + "identity-verification-request": "Verificación de identidad", + "image": "Imagen", + "image-remove-current": "Eliminar la foto actual", + "image-source-gallery": "Seleccionar de la galería", + "image-source-make-photo": "Tomar foto", + "image-source-title": "Editar imagen", + "import": "Importar", + "import-community": "Importar una comunidad", + "import-community-title": "Importar una comunidad", + "in-contacts": "En contactos", + "include": "Incluir", + "incoming": "Entrante", + "incoming-transaction": "Transacción entrante", + "incorrect-code": [ + "str", + "Lo sentimos, el código es incorrecto, escríbelo de nuevo" + ], + "increase-gas": "Aumentar el gas", + "initialization": "Inicialización", + "install": "↓ Instalar", + "insufficient-balance-to-cover-fee": "Balance insuficiente para cubrir el gas de transacción", + "intro-message1": "¡Bienvenid@ a Status!\nToca este mensaje para configurar tu contraseña y empezar.", + "intro-privacy-policy-note1": "Status no recopila ni se beneficia de tus datos personales. Al continuar, tú estás de acuerdo con la ", + "intro-privacy-policy-note2": "política de privacidad", + "intro-text": "Status es tu puerta de enlace a la web descentralizada", + "intro-text1": "Chatea a través de una red encriptada peer-to-peer donde los mensajes no pueden ser censurados o hackeados", + "intro-text2": "Envía y recibe activos digitales en cualquier parte del mundo – no se requiere cuenta bancaria", + "intro-text3": "Explora juegos, exchanges y redes sociales en las que solo tú eres propietario de tus datos", + "intro-title1": "Comunicación verdaderamente privada", + "intro-title2": "Cripto-billetera segura", + "intro-title3": "Apps descentralizadas", + "intro-wizard-text1": "Un set de claves controla tu cuenta. Tus claves viven en tu teléfono, por lo que solo tú puede usarlas.", + "intro-wizard-text2": "Una clave es para chatear. Viene con un nombre legible que no se puede cambiar.", + "intro-wizard-text3": "¿Tienes una Keycard? Guarda tus claves en ella; la necesitarás para las transacciones", + "intro-wizard-text4": "Asegura y encripta tus claves", + "intro-wizard-text6": "Status te notificará sobre los nuevos mensajes. Puedes editar tus preferencias de notificación más adelante en la configuración", + "intro-wizard-title-alt4": "Crea una contraseña", + "intro-wizard-title-alt5": "Confirma tu contraseña", + "intro-wizard-title1": "Obtén tus claves", + "intro-wizard-title2": "Elige un nombre de chat", + "intro-wizard-title3": "Elegir almacenamiento de claves", + "intro-wizard-title4": "Crear un código de acceso de 6 dígitos", + "intro-wizard-title5": "Confirma la clave de acceso", + "intro-wizard-title6": "Habilitar las notificaciones", + "introduce-yourself": "Preséntate con un breve mensaje", + "invalid-address-qr-code": "El código QR escaneado no contiene una dirección válida", + "invalid-format": "Formato inválido \n Debe ser {{format}}", + "invalid-key-confirm": "Aplicar", + "invalid-key-content": "La base de datos no se puede encriptar porque el archivo está dañado. Tus fondos y llaves de chat están a salvo. Otros datos, como chats y contactos, no se podrán restaurar. El botón \"{{erase-multiaccounts-data-button-text}}\", eliminará otros datos y te permitirá acceder a tus fondos y enviar mensajes", + "invalid-number": "Número inválido", + "invalid-pairing-password": "Contraseña de vinculación inválida", + "invalid-public-chat-topic": "Tema de chat público no válido", + "invalid-range": "Formato inválido, debe estar entre {{min}} y {{max}}", + "invalid-username-or-key": "Nombre de usuario o clave de chat no válidos", + "invite": "invitar", + "invite-button": "Invitá", + "invite-chat-accept": "Aceptar", + "invite-chat-accept-join": "Aceptar y Unirse", + "invite-chat-intro": "Fuiste referido por un amigo para unirte a Status. Acá tenés algunas criptomonedas para empezar! Usalas para registrar un nombre ENS o comprar un paquete de stickers", + "invite-chat-name": "Referencia de amigos", + "invite-chat-pending": "Pendiente", + "invite-chat-rule": "Si aceptás también recompensarás a tu amigo con un bono de referencia criptográfica", + "invite-chat-starter-pack": "Paquete de Bienvenida", + "invite-contacts": "Invitar personas de la lista de contactos", + "invite-friends": "Invitar a amigos", + "invite-friends-to-status": "Invitar amigos a Status", + "invite-instruction-fifth": "Puede elegir canjear su bono de referencia en cualquier momento.", + "invite-instruction-first": "Envías un enlace de invitación único a tu amigo para descargar y unirse a Status", + "invite-instruction-fourth": "Vos recibís tu bono de referencia y tu amigo el Paquete de Bienvenida", + "invite-instruction-second": "Tu amigo descarga Status y crea una cuenta (en Android)", + "invite-instruction-third": "Se inicia un chat con tu amigo, donde confirman tu referencia", + "invite-people": "Invitar a otros", + "invite-privacy-policy-public": "Instalaste Status a través de un enlace de referido. Al unirte a este chat, atribuyes su referencia y acepta las", + "invite-privacy-policy1": "Al aceptar, estás de acuerdo con el programa de referidos", + "invite-privacy-policy2": "Términos y Condiciones.", + "invite-public-chat-home": "Invitación de referencia", + "invite-public-chat-intro": "¡Aquí tienes algunas criptomonedas para que comiences! Úselas para registrar un nombre ENS o comprar un paquete de stickers", + "invite-receive-account": "Cuenta para recibir tu bono de referencia", + "invite-reward": "¡Gana criptomonedas por cada amigo que invites!", + "invite-reward-friend": "Amigo:", + "invite-reward-friend-description": "Tu amigo recibirá un Paquete de Bienvenida que consta de {{reward}} para comenzar", + "invite-reward-friend-name": "Paquete de Bienvenida", + "invite-reward-you": "Vos:", + "invite-reward-you-description": "Invitá a un amigo y recibí {{reward}} como bono de referencia. Usalo para obtener stickers, un nombre ENS y probar dapps", + "invite-reward-you-name": "Bono de referencia", + "invite-select-account": "Seleccioná una cuenta para recibir tu bono de referencia", + "invite-warning": "Esta promoción solo es válida para usuarios de un dispositivo Android que no sean residentes de EE. UU. Un amigo debe confirmar la referencia en un plazo de 7 días.", + "invited": "invitado", + "italic": "Cursiva", + "join": "Unite", + "join-a-community": "o únete a una comunidad", + "join-group-chat": "Unirse al grupo", + "join-group-chat-description": "{{username}} te invitó a unirte al grupo {{group-name}}", + "join-me": "Ey, unite a Status: {{url}}", + "join-new-private-chat": "Iniciar un nuevo chat privado", + "join-new-public-chat": "Unirse a un chat público", + "join-open-community": "Unirse a la Comunidad", + "joined": "Unido", + "joined-group-chat-description": "Te has unido a {{group-name}} desde la invitación de {{username}}", + "key": "Clave", + "key-managment": "Administración de claves", + "key-on-device": "La clave privada está guardada en este dispositivo", + "keycard": "Keycard", + "keycard-access-reset": "Acceso a la Keycard está restablecido", + "keycard-applet-install-instructions": "Para instalar el applet, sigue las instrucciones en https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Intente mover la tarjeta para encontrar el lector NFC en su dispositivo", + "keycard-awaiting-title": "Todavía buscando...", + "keycard-backup": "Crear una copia de seguridad de tu Keycard", + "keycard-backup-success-body": "Copia de respaldo creada con éxito. Ahora puedes utilizarla con tu cuenta al igual que la tarjeta principal.", + "keycard-backup-success-title": "Copia de seguridad exitosa", + "keycard-blocked": "Keycard ha sido bloqueada. \n Necesitas reiniciar la tarjeta para continuar usándola.", + "keycard-can-use-with-new-passcode": "Puedes usar esta tarjeta con tu nuevo código de accesto", + "keycard-cancel-setup-text": "Esto cancelará la configuración de keycard. Se muy recomendable finalizar la configuración para usar keycard. ¿De verdad quieres cancelar?", + "keycard-cancel-setup-title": "Operación peligrosa", + "keycard-connected-description": "Intenta mantener la tarjeta quieta", + "keycard-connected-title": "Conectado", + "keycard-desc": "¿Tienes una Keycard? Guarda tus claves allí; la necesitarás para las transacciones", + "keycard-dont-ask-card": "No pedir tarjeta para iniciar sesión", + "keycard-enter-new-passcode": "Ingresa el nuevo código de acceso {{step}}\/2", + "keycard-error-description": "Vuelva a conectar la tarjeta para continuar.", + "keycard-error-title": "Conexión perdida", + "keycard-factory-reset": "Restablecer la tarjeta a la configuración de fábrica", + "keycard-factory-reset-text": "Hacer esto eliminará cualquier mnemónico almacenado en la tarjeta. Asegúrate de tener una copia de seguridad de la frase mnemónica que ha estado usando con esta Keycard.", + "keycard-factory-reset-title": "¿Estás seguro que quieres restablecer a las condiciones de fábrica?", + "keycard-free-pairing-slots": "Keycard tiene {{n}} espacios de vinculación libres", + "keycard-has-multiaccount-on-it": "Esta tarjeta está llena. Cada tarjeta puede mantener un par de llaves principales", + "keycard-init-description": "Coloque la tarjeta en la parte posterior de su teléfono para continuar", + "keycard-init-title": "Buscando tarjetas...", + "keycard-is-blocked-details": "Ya no puedes usar esta tarjeta para acceder o firmar esta cuenta. Ha habido demasiados intentos fallidos de código de acceso y PUK.", + "keycard-is-blocked-instructions": "Para acceder a tu cuenta, re-instala Status y usa una nueva Keycard, usa una billetera diferente o reinicia Keycard manualmente.", + "keycard-is-blocked-title": "La Keycard está bloqueada", + "keycard-is-frozen-details": "Para proteger tus activos, tu tarjeta está congelada. Restablece el acceso a la tarjeta para desbloquear llaves y enviar transacciones. Crea un nuevo código de acceso e ingrese su PUK para acceder a su(s) cuenta(s) en esta tarjeta", + "keycard-is-frozen-factory-reset": "Restablecer con mnemónico", + "keycard-is-frozen-reset": "Restablece el acceso a la tarjeta", + "keycard-is-frozen-title": "La Keycard está congelada", + "keycard-onboarding-finishing-header": "Terminando", + "keycard-onboarding-intro-header": "Guarda tus claves en Keycard", + "keycard-onboarding-intro-text": "Prepárate, esto puede tomar unos minutos, pero es importante para asegurar tu cuenta", + "keycard-onboarding-mnemonic-text": "También necesitarás un papel y un lápiz para escribir tu frase semilla.", + "keycard-onboarding-pairing-header": "Vinculando la tarjeta...", + "keycard-onboarding-pin-text": "Deberás crear un código de acceso de 6 dígitos que se utilizará para proteger tu Keycard.", + "keycard-onboarding-preparing-header": "Preparando la tarjeta...", + "keycard-onboarding-puk-code-header": "Escribe los códigos\n y guárdalos de forma segura", + "keycard-onboarding-recovery-phrase-description": "Necesitas esta frase semilla para obtener de nuevo tu clave. Anótala. Manténla segura, fuera de línea y separada de este dispositivo.", + "keycard-onboarding-recovery-phrase-header": "Respalda tu frase semilla", + "keycard-onboarding-recovery-phrase-text": "Sólo para tus ojos. Esta es la semilla mágica usada para generar tu clave.", + "keycard-onboarding-start-header": "Manten la tarjeta en la parte posterior\n de tu teléfono para iniciar", + "keycard-onboarding-start-step1": "Crea un código de acceso", + "keycard-onboarding-start-step1-text": "Alrededor de 1 minuto. Crea un código de acceso de 6 dígitos para encriptar tus claves", + "keycard-onboarding-start-step2": "Escribe el código de vinculación y PUK", + "keycard-onboarding-start-step2-text": "Alrededor de 1 minuto. Vas a necesitar un papel y un lápiz para eso", + "keycard-onboarding-start-step3": "Respaldar la frase semilla", + "keycard-onboarding-start-step3-text": "Alrededor de 1 minuto. También son necesarios un pedazo de papel y un lápiz.", + "keycard-onboarding-start-text": "Y manten el contacto entre la tarjeta y el teléfono\n durante la configuración. La configuración tomará alrededor de 4 minutos", + "keycard-processing-description": "Intenta mantener la tarjeta quieta", + "keycard-processing-title": "Procesando...", + "keycard-recover": "tarjeta perdida o congelada?", + "keycard-recover-text": "Si tienes tu frase mnemotécnica puedes crear una nueva Keycard asociada a esta cuenta. Puedes utilizar ya sea una nueva Keycard o realizar el restablecimiento de una congelada.", + "keycard-recover-title": "¿Crear una nueva tarjeta para esta cuenta?", + "keycard-recovery-intro-button-text": "Iniciar la recuperación", + "keycard-recovery-intro-header": "Recuperar claves almacenadas en Keycard", + "keycard-recovery-intro-text": "Si antes generaste claves con keycard y ahora deseas utilizarlas en este dispositivo", + "keycard-recovery-no-key-header": "No hay nada que \nrecuperar aquí", + "keycard-recovery-no-key-text": "Tu Keycard no tiene ninguna clave almacenada en ella. Para usarla, genera una nueva clave y elige tu Keycard para almacenar la clave", + "keycard-recovery-phrase-confirm-header": "Confirmar frase semilla", + "keycard-recovery-phrase-confirmation-text": "¡No tendrás una segunda oportunidad! Si pierdes acceso, por ejemplo al extraviar tu Keycard, solo podrás acceder a tus llaves con tu frase semilla. Nadie a parte de ti tiene tu frase semilla. Anótala. Mantenla guardada.", + "keycard-recovery-phrase-confirmation-title": "¿Escribiste la frase semilla?", + "keycard-recovery-success-header": "Tus claves han sido \n recuperadas con éxito", + "keycard-redeem-title": "Canjear a", + "keycard-redeem-tx": "Canjea activos", + "keycard-redeem-tx-desc": "Toque con la tarjeta para firmar y recibir activos", + "keycard-reset-passcode": "Restablece el código de acceso", + "keycard-success-description": "Puedes quitar la tarjeta ahora", + "keycard-success-title": "Éxito", + "keycard-unauthorized-operation": "No estás autorizado a realizar esta operación.\n Por favor, toca la tarjeta válida e inténtalo de nuevo.", + "keycard-upsell-subtitle": "Mayor seguridad y conveniencia", + "kicked": "Eliminado", + "language": "Idioma", + "last-backup-performed": "Última copia de seguridad realizada:", + "last-transaction": "Última transacción", + "learn-more": "Aprende más", + "learn-more-about-keycard": "Más información sobre Keycard", + "leave": "Salir", + "leave-chat": "Abandonar chat", + "leave-chat-confirmation": "El historial de la conversación se eliminará de tu dispositivo. Al reincorporarte no podrás recuperar tu historial.", + "leave-community": "Salir de la comunidad", + "leave-community-message": "Estaremos tristes que te vayas, pero recuerda, ¡puedes volver en cualquier momento!", + "leave-community?": "¿Abandonar comunidad?", + "leave-confirmation": "Salir {{chat-name}}", + "leave-group": "Salir del grupo", + "left": "salió", + "les-ulc": "LES\/ULC", + "lets-go": "Vamos", + "lifestyle": "Estilo de vida", + "light": "Luz", + "limit": "Límite", + "link-to-community": "Link a comunidad", + "linked-on": "Vinculado en {{date}}", + "load-messages-before": "antes del {{date}}", + "load-more-messages": "↓ Recuperar más mensajes", + "load-more-timeline": "↓ Obtener más", + "loading": "Cargando...", + "local-notifications": "Notificaciones locales", + "lock-app-with": "Bloquear app con", + "log-level": "Nivel de registro", + "log-level-settings": "Ajustes del nivel de registro", + "logging": "Registro", + "logging-enabled": "¿Registro habilitado?", + "login-pin-description": "Ingresa tu código de acceso de 6 dígitos para desbloquear tus llaves", + "logout": "Cerrar sesión", + "logout-app-content": "Se cerrará la sesión de la cuenta. Cuando lo desbloquees de nuevo, se utilizará la red seleccionada", + "logout-are-you-sure": "¿Seguro que quieres cerrar la sesión?", + "logout-key-management": "Es necesario cerrar la sesión para acceder a la gestión de claves.", + "logout-title": "¿Cerrar sesión?", + "looking-for-cards": "Buscando tarjetas...", + "lost-connection": "Conexión perdida", + "low-tip": "la propina es demasiado baja", + "lower-than-average-tip": "inferior a la propina promedio", + "mail-should-be-configured": "El cliente de correo debe ser configurado", + "mailserver-address": "Dirección del nodo de Status", + "mailserver-automatic": "Selección automática", + "mailserver-automatic-switch-explanation": "Elija el nodo de Status más rápido disponible", + "mailserver-connection-error": "No se pudo conectar al nodo de Status", + "mailserver-content": "Un nodo en la red de Status que enruta y almacena mensajes, hasta por 30 días.", + "mailserver-details": "Detalles del nodo de Status", + "mailserver-error-content": "No se pudo acceder al nodo de Status que seleccionó.", + "mailserver-error-title": "Error al conectarse al nodo de Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Elija otro nodo de Status", + "mailserver-reconnect": "No se pudo conectar con el nodo de Status. Pulse para reconectar", + "mailserver-request-error-content": "El siguiente error fue devuelto por el nodo de Status: {{error}}", + "mailserver-request-error-status": "Se produjo un error al recuperar el historial, verifica los registros para obtener más detalles", + "mailserver-request-error-title": "Error de solicitud del nodo de Status", + "mailserver-request-retry": "Reintentar solicitud", + "mailserver-retry": "Reintentar", + "mailserver-title": "Nodo de Status", + "main-currency": "Moneda principal", + "main-networks": "Redes principales", + "main-wallet": "Billetera Principal", + "mainnet-network": "Red principal", + "make-admin": "Hacer admin", + "manage-keys-and-storage": "Administrar las claves y el almacenamiento", + "mark-all-read": "Marcar todo como leído", + "mark-as-read": "Marcar como leído", + "mark-untrustworthy": "Marcar como no confiable", + "master-account": "Cuenta principal", + "max-fee": "Tarifa máx.", + "max-priority-fee": "Tarifa máxima de prioridad", + "maximum-fee": "Tarifa máxima", + "maximum-fee-desc": "Precio global máximo de la operación. Si la tarifa base del bloque actual excede este precio, su transacción será incluida en un bloque siguiente con una tarifa base más baja.", + "maybe-later": "Quizas mas tarde", + "member-ban": "Prohibir miembro", + "member-kick": "Eliminar miembro", + "members": { + "one": "1 miembro", + "other": "{{count}} miembros" + }, + "members-active": { + "one": "1 miembro", + "other": "{{count}} miembros" + }, + "members-active-none": "no hay miembros", + "members-count": "{{count}} miembros", + "members-label": "Miembros", + "members-limit-reached": "Límite de miembros alcanzado", + "members-title": "Miembros", + "membership": "Membresía", + "membership-approval": "Requiere aprobación", + "membership-approval-description": "Puedes unirte de manera gratuita, pero los nuevos miembros necesitan ser aprobados primero por el creador de comunidad primero", + "membership-button": "Requisito de membresía", + "membership-declined": "Se rechazó la solicitud de membresía", + "membership-description": "La membresía grupal requiere que seas aceptado por el administrador del grupo", + "membership-ens": "Requiere un usuario ENS", + "membership-ens-description": "Tu comunidad requiere que tengas un usuario ENS para unirte", + "membership-free": "Sin requisitos", + "membership-free-description": "Cualquier persona puede unirse a tu comunidad", + "membership-invite": "Necesitas invitación de otro miembro", + "membership-invite-description": "Únicamente puedes unirte a la comunidad mediante invitación de un miembro existente", + "membership-none": "Ninguno", + "membership-none-placeholder": "Puede exigir que los nuevos miembros cumplan con ciertos criterios antes de poder unirse. Esto se puede cambiar en cualquier momento.", + "membership-request-pending": "Solicitud de membresía pendiente", + "membership-requests": "Solicitudes de membresía", + "membership-title": "Requisito de membresía", + "mentions": "Menciones", + "message": "Mensaje", + "message-deleted": "Mensaje eliminado", + "message-deleted-for-you": "Mensaje eliminado para ti", + "message-not-sent": "Mensaje no enviado", + "message-options-cancel": "Cancelar", + "message-reply": "Responder", + "messages": "Mensajes", + "messages-from-contacts-only-subtitle": "Solo las personas que agregaste como contactos pueden iniciar un nuevo chat o invitarte a un grupo", + "messages-gap-warning": "Es posible que falten algunos mensajes", + "might-break": "Puede que se rompan algunas ÐApps", + "migration-successful": "Migración exitosa", + "migration-successful-text": "Cuenta migrada con éxito a la Keycard", + "migrations-failed-content": "{{message}} \n versión del esquema: inicial {{initial-version}}, actual {{current-version}} , último {{last-version}} \n\nOcurrió un problema de base de datos. Tus fondos y la llave de chat están a salvo. Otros datos, como chats y contactos, no se podrán recuperar. Presionar el botón {{erase-multiaccounts-data-button-text}}\" eliminará el resto de datos y te permitirá acceder a tus fondos y enviar mensajes.", + "miners-higher-fee": "Es probable que los mineros incluyan tu transacción antes si pagas una tarifa más alta.", + "minimum-received": "Mínimo recibido", + "mobile-network-ask-me": "Pregúntarme cuando esté en la red móvil", + "mobile-network-continue-syncing": "Continuar sincronizando", + "mobile-network-continue-syncing-details": "Puedes cambiar esto más adelante en los ajustes", + "mobile-network-go-to-settings": "Ir a ajustes", + "mobile-network-settings": "Datos móviles", + "mobile-network-sheet-configure": "Puedes configurar la sincronización en más \ndetalle en", + "mobile-network-sheet-offline": "Sin Wi-Fi, la sincronización de mensajes está deshabilitada.", + "mobile-network-sheet-offline-details": "La sincronización mediante la red móvil está desactivada", + "mobile-network-sheet-remember-choice": "Recuerda mi elección", + "mobile-network-sheet-settings": "ajustes", + "mobile-network-start-syncing": "Iniciar la sincronización", + "mobile-network-stop-syncing": "Detener la sincronización", + "mobile-network-stop-syncing-details": "¿Hasta que se conecta a Wi-Fi?", + "mobile-network-use-mobile": "Usar datos móviles", + "mobile-network-use-mobile-data": "Status utiliza muchos datos al sincronizar los chats y la billetera.", + "mobile-network-use-wifi": "Sólo Wi-Fi", + "mobile-syncing-sheet-details": "Status utiliza muchos datos al sincronizar los chats y la billetera.", + "mobile-syncing-sheet-title": "¿Sincronizar usando los datos del móvil?", + "more": "más", + "move-and-reset": "Mover y restablecer", + "move-keystore-file": "Mover el archivo keystore", + "move-keystore-file-to-keycard": "¿Mover el archivo keystore a la keycard?", + "multiaccount-exists-content": "Las llaves para esta cuenta ya existen y no se pueden añadir de nuevo. Si perdiste tu contraseña, código de acceso o Keycard, desinstala la app, reinstálala y accede insertando tu frase semilla", + "multiaccount-exists-title": "Las llaves para esta cuenta ya existen", + "multiaccounts-recover-enter-phrase-text": "Ingresa 12, 15, 18, 21 o 24 palabras. \n Separa las palabras por un solo espacio.", + "multiaccounts-recover-enter-phrase-title": "Ingresa tu frase semilla", + "music": "Musica", + "mutal-contacts": "Contactos mutuos", + "mute": "Silenciar", + "mute-chat": "Silenciar chat", + "mute-community": "Silenciar comunidad", + "my-accounts": "Mis cuentas", + "my-accounts-empty": "Tus cuentas disponibles aparecerán aquí", + "my-profile": "Mi perfil", + "my-status": "Mi estado", + "n-photos": "{{count}} fotos", + "name": "Nombre", + "name-of-token": "El nombre de tu token", + "name-optional": "Nombre (opcional)", + "name-your-channel": "Asigna un nombre a tu canal", + "name-your-channel-placeholder": "Nombre del canal", + "name-your-community": "Asigna un nombre a tu comunidad", + "name-your-community-placeholder": "Elige un nombre memorable", + "need-help": "¿Necesitas ayuda?", + "network": "Red", + "network-chain": "Cadena de la red", + "network-details": "Detalles de red", + "network-fee": "Tarifa de la red", + "network-id": "Identidad de la red", + "network-info": "Información de la red", + "network-invalid-network-id": "El id de red especificado no corresponde al id de red por la url de RPC", + "network-invalid-status-code": "Código de estado inválido: {{code}}", + "network-invalid-url": "La URL de la red no es válida", + "network-settings": "Ajustes de la red", + "never": "Nunca", + "new": "Nuevo", + "new-category": "Nueva categoría", + "new-chat": "Nuevo chat", + "new-community-title": "Nueva comunidad", + "new-contact": "Nuevo contacto", + "new-contract": "Nuevo contrato", + "new-favourite": "Nuevo favorito", + "new-group": "Nuevo grupo", + "new-group-chat": "Nuevo grupo de chat", + "new-messages-header": "Nuevos mensajes", + "new-network": "Nueva red", + "new-password": "Nueva contraseña", + "new-pin-description": "Ingresa el nuevo código de acceso de 6 dígitos", + "new-public-group-chat": "Unirte a chat público", + "new-puk-description": "Ingrese un nuevo PUK de 12 dígitos", + "new-status": "Nuevo estado", + "new-tab": "Nueva pestaña", + "next": "Siguiente", + "nickname": "Apodo", + "nickname-description": "Los apodos te ayudan a identificar a otras personas en Status. \n Solo tú puedes ver los apodos que agregaste", + "no": "No", + "no-collectibles": "No hay coleccionables disponibles", + "no-contacts": "Aún no hay contactos", + "no-keycard-applet-on-card": "No hay applet de Keycard en la tarjeta", + "no-messages": "No hay mensajes", + "no-pairing-slots-available": "Esta tarjeta ya está vinculada con 5 dispositivos y no puede ser vinculada con éste dispositivo. Por favor, utiliza uno de los dispositivos vinculados, inicia sesión con esta tarjeta y libera los espacios de vinculación en la tarjeta", + "no-pinned-messages": "Sin mensajes fijados", + "no-pinned-messages-community-desc": "Este canal no tiene ningún mensaje anclado.", + "no-pinned-messages-desc": "Este chat no tiene ningún mensaje anclado", + "no-result": "Sin resultados", + "no-thanks": "No, gracias", + "no-tokens-found": "No se encontraron tokens", + "node-address": "Dirección del nodo", + "node-details": "Detalles de nodo", + "node-info": "Información del nodo", + "node-version": "Versión de nodo", + "nodes-disabled": "Nodos de Status deshabilitados", + "non-archival-node": "El punto de conexión de RPC no admite solicitudes de archivado. Es posible que el historial de transferencias locales esté incompleto.", + "non-contacts": "Ningún contacto", + "nonce": "Nonce", + "none": "ninguno", + "normal": "Normal", + "not-applicable": "No aplicable para transacciones no firmadas", + "not-connected-nodes": "Sin conexión a un nodo de Status", + "not-connected-to-peers": "Sin conexión a ningún nodo", + "not-enough-snt": "No hay suficiente SNT", + "not-found": "No encontrado", + "not-keycard-text": "La tarjeta que usaste no es una Keycard. Necesitas comprar una Keycard para usarla", + "not-keycard-title": "No es una Keycard", + "not-registered": "no registrado", + "notification-settings": "Notificaciones", + "notifications": "Notificaciones", + "notifications-non-contacts": "Notificaciones de no contactos", + "notifications-preferences": "Preferencias de notificación", + "notifications-servers": "Servidores de notificaciones", + "notifications-switch": "Mostrar notificaciones", + "notifications-transactions": "Transacciones de billetera", + "notify": "Notificar", + "now": "Ahora", + "off": "Apagado", + "off-status-tree": "Derivado fuera de Status", + "offline": "Desconectado", + "offline-messaging-use-history-explanation": "Habilite los nodos del historial para recuperar los mensajes que se enviaron mientras la aplicación estaba cerrada. Cuando está habilitado, un nodo de historial obtiene su dirección IP. Cuando está deshabilitado, no recibirá mensajes cuando la aplicación esté cerrada y no los verá cuando abra la aplicación más tarde.", + "offline-messaging-use-history-nodes": "Usar nodos de Status", + "ok": "OK", + "ok-continue": "Ok, continuar", + "ok-got-it": "Ok, lo tengo", + "ok-save-pass": "OK, guardar contraseña", + "okay": "Bien", + "on": "En", + "on-status-tree": "En el árbol de Status", + "once-enabled-share-metadata": "Una vez habilitados, los enlaces publicados en el chat pueden compartir sus metadatos con el sitio.", + "one-day": "Un día", + "one-month": "Un mes", + "one-week": "Una semana", + "online": "En línea", + "open": "Abrir", + "open-chat": "Abrir chat", + "open-dapp": "Abrir ÐApp", + "open-dapp-store": "Descubre ÐApps", + "open-home": "Abrir...", + "open-in-new-tab": "Abrir en una pestaña nueva", + "open-membership": "Abrir membresía", + "open-nfc-settings": "Abrir ajustes de NFC", + "opened": "Abierto", + "opening-buy-crypto": "Abriendo {{site}} ...", + "optimal": "Óptima", + "optional": "opcional", + "or": "o", + "outgoing": "Saliente", + "outgoing-transaction": "Transacción saliente", + "page-camera-request-blocked": "solicitudes de cámara bloqueadas. Para habilitar las solicitudes de la cámara, vaya a Configuración", + "page-would-like-to-use-camera": "quisiera usar su cámara", + "pair": "Vincular dispositivos", + "pair-card": "Vincular a este dispositivo", + "pair-code": "Código par", + "pair-code-explanation": "Vincula la tarjeta a un dispositivo diferente para desbloquear llaves y firmar transacciones con la misma Keycard", + "pair-code-placeholder": "Código de emparejamiento...", + "pair-this-card": "Vincula esta tarjeta", + "pair-this-device": "Publicar el dispositivo", + "pair-this-device-description": "Vincula tus dispositivos para sincronizar los contactos y chats entre ellos", + "paired-devices": "Dispositivos vinculados", + "pairing": "Vinculando", + "pairing-card": "Vinculando la tarjeta", + "pairing-changed": "Se ha cambiado el código de emparejamiento", + "pairing-code-placeholder": "Código de emparejamiento...", + "pairing-code_error1": "Los códigos de emparejamiento no coinciden.", + "pairing-go-to-installation": "Ir a los ajustes de vinculación", + "pairing-maximum-number-reached-content": "Por favor, deshabilita uno de tus dispositivos antes de habilitar uno nuevo.", + "pairing-maximum-number-reached-title": "Número máximo de dispositivos alcanzados", + "pairing-new-installation-detected-content": "Se ha detectado un nuevo dispositivo. \nPara usar tus dispositivos correctamente, es importante vincularlos y habilitarlos antes de usarlos. \nVe a la sección del dispositivo en los ajustes para vincular tus dispositivos.", + "pairing-new-installation-detected-title": "Nuevo dispositivo detectado", + "pairing-no-info": "Sin información", + "pairing-please-set-a-name": "Por favor configura un nombre para tu dispositivo.", + "passphrase": "Frase de contraseña", + "password": "Contraseña", + "password-description": "Al menos 6 caracteres. Tu contraseña protege tus claves. La necesitas para desbloquear Status y realizar transacciones.", + "password-mismatch": "Nueva contraseña y confirmación no coinciden", + "password-placeholder": "Contraseña...", + "password-placeholder2": "Confirma tu contraseña", + "password-reset-in-progress": "Cambiando contraseña...", + "password-reset-success": "Contraseña cambiada", + "password-reset-success-message": "Tendrá que iniciar sesión de nuevo", + "password_error1": "Las contraseñas no coinciden.", + "paste": "Pegar", + "paste-json": "Pegar JSON", + "pay-to-chat": "Pagar para chatear", + "peer-content": "Un dispositivo conectado a la red de chat de Status. Cada usuario puede representar uno o más peers, dependiendo de su número de dispositivos.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Pares", + "pending": "En espera", + "pending-confirmation": "Confirmación pendiente...", + "pending-invitations": "Solicitudes de membresía pendientes", + "pending-requests": "Solicitudes pendientes", + "per-gas-price-limit": "Límite del precio por gas", + "per-gas-tip-limit": "Límite de propina por gas", + "perform-backup": "Realizar copia de seguridad", + "permissions": "Permisos", + "phone-e164": "Internacional 1", + "photos": "Fotos", + "photos-access-error": "Para permitir el acceso a fotos, porfavor, ve a ajustes de sistema y asegúrate que este seleccionada la opción Status > Fotos", + "pin": "Fijar", + "pin-changed": "Se ha cambiado el código de acceso de 6 dígitos", + "pin-code": "Código de acceso de 6 dígitos", + "pin-limit-reached": "Límite de pines alcanzado. Desancla primero un mensaje anterior.", + "pin-mismatch": "Código de acceso incorrecto", + "pin-one-attempt": "un intento", + "pin-one-attempt-blocked-after": "antes de que tu Keycard se bloquee", + "pin-one-attempt-blocked-before": "Ten cuidado, solo tienes", + "pin-one-attempt-frozen-after": "antes de que tu Keycard se congele", + "pin-one-attempt-frozen-before": "Ten cuidado, solo tienes", + "pin-retries-left": "{{number}} intentos restantes", + "pin-to-channel": "Anclar al canal", + "pin-to-chat": "Anclar al chat", + "pinned-a-message": "anclado a un mensaje", + "pinned-by": "Fijado por", + "pinned-messages": "Mensajes fijados", + "pinned-messages-count": { + "one": "1 mensaje fijado", + "other": "{{count}} mensajes fijados" + }, + "pinned-messages-empty": "Los mensajes fijados aparecerán aquí. Para fijar un mensaje, mantenga pulsado y seleccione 'Pin'", + "podcasts": "Podcasts", + "powered-by-paraswap": "Desarrollado por Paraswap", + "preference": "Preferencia", + "preview-privacy": "Vista previa del modo de privacidad", + "previewing-may-share-metadata": "La vista previa de los enlaces de estos sitios web puede compartir sus metadatos con sus propietarios.", + "price-impact": "Impacto del precio", + "price-impact-desc": "Impacto del precio estimado para esta transacción. Si la tarifa base del bloque actual excede esto, tu transacción será incluida en un bloque siguiente con una tarifa base más baja.", + "principles": "Fundamentos", + "priority": "Prioridad", + "privacy": "Privacidad", + "privacy-and-security": "Privacidad y seguridad", + "privacy-photos": "Privacidad de la foto del perfil", + "privacy-policy": "Política de privacidad", + "privacy-show-to-warning": "Las personas que ya han visto tu foto de perfil seguirán haciéndolo", + "private-key": "Llave privada", + "private-notifications": "Notificaciones privadas", + "private-notifications-descr": "Status te notificará sobre los nuevos mensajes. Podés editar tus preferencias de notificación más adelante en la configuración.", + "processing": "Un momento.", + "product-information": "Información del Producto", + "profile": "Perfil", + "profile-deleted-content": "Su perfil fue eliminado con éxito", + "profile-deleted-keycard": "Ahora puedes restaurar otro par de claves en tu Keycard", + "profile-deleted-title": "Perfil eliminado", + "profile-details": "Detalles del perfil", + "profile-not-found": "Perfil no encontrado", + "profile-pic-pick": "Seleccionar de la galería", + "profile-pic-remove": "Quitar foto", + "profile-pic-take": "Tomar foto", + "profile-picture-updated": "Foto de perfil actualizada", + "public": "Público", + "public-channel": "Canal público", + "public-chat": "Chat público", + "public-chat-description": "¡Únete a chats públicos para tus intereses! Cualquiera puede comenzar uno nuevo.", + "public-chats": "Chats públicos", + "public-group-status": "Público", + "public-group-topic": "Tema", + "public-key": "Clave pública", + "puk-and-pairing-codes-displayed": "PUK y códigos de vinculación mostrados", + "puk-changed": "Se ha cambiado el PUK de 12 dígitos", + "puk-code": "Código PUK", + "puk-code-explanation": "Si olvidas tu código de acceso de 6 dígitos o lo ingresas incorrectamente 3 veces, necesitarás este código para desbloquear tu tarjeta.", + "puk-mismatch": "El código PUK no coincide", + "push-failed-transaction": "Su transacción falló", + "push-failed-transaction-body": "{{value}} {{currency}} a {{to}}", + "push-inbound-transaction": "Recibiste {{value}} {{currency}}", + "push-inbound-transaction-body": "De {{from}} a {{to}}", + "push-notifications-server-enabled": "Servidor habilitado", + "push-notifications-servers": "Servidores de notificaciones push", + "push-outbound-transaction": "Enviaste {{value}} {{currency}}", + "push-outbound-transaction-body": "De {{from}} a {{to}}", + "quiet-days": "{{quiet-days}} días", + "quiet-hours": "{{quiet-hours}} horas", + "re-encrypt-key": "Vuelve a encriptar tus claves", + "rearrange-categories": "Reorganizar Categorías", + "receive": "Recibir", + "receive-transaction": "Recibir transacción", + "received": "Recibido", + "recent": "Estados recientes", + "recent-empty": "Las direcciones utilizadas recientemente aparecerán aquí", + "recent-recipients": "Contactos", + "recently-used-stickers": "Las stickers usadas recientemente aparecerán aquí", + "recipient": "Receptor", + "recipient-code": "Ingresa la dirección del destinatario", + "recipient-code-placeholder": "0x... o username.domain.eth", + "recover": "Recuperar", + "recover-key": "Ingresar claves existentes", + "recover-keycard-multiaccount-not-supported": "Las llaves para esta cuenta ya existen y no se pueden añadir de nuevo. Si perdiste tu contraseña, código de acceso o Keycard, desinstala la app, reinstálala y accede insertando tu frase semilla", + "recover-with-keycard": "Recuperar con Keycard", + "recover-with-seed-phrase": "Recuperar con frase semilla", + "recovering-key": "Ingresando las claves...", + "recovery-confirm-phrase": "Confirmar frase semilla", + "recovery-phrase": "Frase semilla", + "recovery-success-text": "Deberás crear un nuevo código o contraseña para volver a encriptar tus claves", + "recovery-typo-dialog-description": "Recuerda, tu frase semilla debe ser exactamente las mismas palabras y el orden que recibiste", + "recovery-typo-dialog-title": "¿Es correcta la frase semilla?", + "redeem-amount": "{{quantity}} bonos disponibles", + "redeem-now": "Canjear ahora", + "redeem-success": "¡Canje del bono exitoso!", + "reduced-tip": "la propina de prioridad se reducirá", + "refresh": "Actualizar", + "registered": "registrado", + "reject-and-delete": "Rechazar y eliminar", + "remember-me": "Recuérdame", + "remind-me-later": "Muéstrame esto de nuevo", + "remote-notifications": "Notificaciones remotas", + "remove": "Remover", + "remove-favourite": "Quitar favorito", + "remove-from-chat": "Eliminar del chat", + "remove-from-contacts": "Eliminar de contactos", + "remove-from-contacts-text": "Al eliminar a un usuario de tu lista de contactos, no oculta la dirección de tu billetera de ellos", + "remove-group": "Eliminar grupo", + "remove-network": "Eliminar red", + "remove-token": "Eliminar token", + "removed": "eliminado", + "rename": "Renombrar", + "repeat-pin": "Repite el nuevo código de acceso de 6 dígitos", + "repeat-puk": "Repita el nuevo PUK de 12 dígitos", + "replies": "Respuestas", + "replying-to": "Respondiendo a {{author}}", + "report-bug-email-template": "1. Descripción del problema\n{{description}}\n\n\n2. Pasos para reproducir\n{{steps}}\n\n\n3. Adjunta capturas de pantalla que demuestren el problema por favor\n", + "request-access": "Solicitar acceso", + "request-feature": "Solicitar una funcionalidad", + "request-membership": "Solicitar membresía", + "request-pending": "Pedido pendiente…", + "request-to-join": "Solicitar unirse", + "request-transaction": "Solicitar transacción", + "required-field": "Campo requerido", + "resend-message": "Reenviar", + "reset-card": "Reiniciar la tarjeta", + "reset-card-description": "Esta operación restablecerá la tarjeta al estado inicial. Borrará todos los datos de la tarjeta, incluidas las claves privadas. La operación no es reversible.", + "reset-database": "Restablecer base de datos", + "reset-database-warning": "Elimina chats, contactos y configuraciones. Se requiere cuando has perdido tu contraseña", + "reset-database-warning-keycard": "Borrar chats, contactos y ajustes.", + "reset-password": "Cambiar contraseña", + "restore-defaults": "Restaurar los valores predeterminados", + "retry": "Reintentar", + "revoke-access": "Revocar el acceso", + "rpc-url": "RPC URL", + "rpc-usage-copy": "Copiar", + "rpc-usage-filter": "Métodos de filtro", + "rpc-usage-filter-methods": "Métodos de filtro", + "rpc-usage-get-stats": "Actualizar", + "rpc-usage-info": "Estadísticas de uso de RPC", + "rpc-usage-reset": "Restablecer", + "safe-estimate": "Seguridad estimada", + "save": "Guardar", + "save-image-library": "Guardar imagen en la biblioteca", + "save-password": "Guardar contraseña", + "save-password-unavailable": "Establece el código de acceso del dispositivo para guardar la contraseña", + "save-password-unavailable-android": "Guardar la contraseña no está disponible: tu dispositivo puede estar rooteado o carecer de las características de seguridad necesarias.", + "scan-qr": "Escanear QR", + "scan-qr-code": "Escanea un código QR con una dirección de billetera", + "scan-tokens": "Escanear tokens", + "search": "Buscar", + "search-contacts": "Buscar contactos", + "search-no-chat-found": "No hay resultados de búsqueda. ¿Querés decir", + "secret-keys-confirmation-text": "Los necesitará para seguir usando su Keycard en caso de que alguna vez pierda su teléfono.", + "secret-keys-confirmation-title": "¿Escribiste los códigos?", + "security": "Seguridad", + "see-details": "Ver detalles", + "see-it-again": "VERLO OTRA VEZ", + "see-sticker-set": "Ver el set de stickers completo", + "see-suggestions": "Ver sugerencias", + "seed-key-uid-mismatch": "La frase semilla no coincide", + "seed-key-uid-mismatch-desc-1": "La frase semilla que ingresó no coincide con {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Para administrar claves para esta cuenta verifique su frase semilla e inténtelo de nuevo", + "seed-phrase-content": "Un set de palabras fáciles de leer, seleccionadas al azar de la lista estándar BIP39 y utilizadas para recuperar o acceder a tu cuenta de Ethereum en otras billeteras y dispositivos. También se conoce como \"frase mnemónica\", \"frase de recuperación\" o \"copia de seguridad de billetera\" en todo el cripto ecosistema. La mayoría de las cripto apps usan este mismo estándar para generar cuentas.", + "seed-phrase-placeholder": "Frase semilla...", + "seed-phrase-title": "Frase Semilla", + "select": "Seleccionar", + "select-account": "Seleccionar cuenta", + "select-account-dapp": "Selecciona la cuenta que deseas usar con Dapps", + "select-account-first": "Selecciona una cuenta primero", + "select-chat": "Selecciona el chat para comenzar a enviar mensajes", + "select-new-location-for-keys": "Seleccione una nueva ubicación para guardar sus claves privadas", + "select-token-to-receive": "Selecciona el token a recibir", + "select-token-to-swap": "Selecciona el token a intercambiar", + "selected": "Seleccionado", + "send": "Enviar", + "send-logs": "Reportar un error", + "send-logs-to": "Reportar un error a {{email}}", + "send-message": "Enviar mensaje", + "send-push-notifications-description": "Cuando está deshabilitado, la persona que recibe sus mensajes no será notificada de su llegada.", + "send-request": "Enviar solicitud", + "send-request-amount": "Cantidad", + "send-request-amount-max-decimals": "El número máximo de decimales es {{asset-decimals}}", + "send-request-unknown-token": "Token desconocido - {{asset}}", + "send-sending-to": "para {{recipient-name}}", + "send-transaction": "Enviar transacción", + "sending": "Enviando", + "sent": "Enviado", + "sent-at": "Enviado a", + "server": "Servidor", + "set-a-topic": "Crear un tema", + "set-currency": "Establecer moneda", + "set-custom-fee": "Establecer tarifa personalizada", + "set-dapp-access-permissions": "Establecer permisos de acceso DApp", + "set-max": "Establecer máximo", + "settings": "Ajustes", + "share": "Compartir", + "share-address": "Compartir dirección", + "share-chat": "Compartir chat", + "share-community": "Compartir comunidad", + "share-contact-code": "Compartir mi clave de chat", + "share-dapp-text": "Echa un vistazo a esta DApp que estoy usando en Status: {{link}}", + "share-image": "Compartir imagen", + "share-invite-link": "Compartir un link de invitación", + "share-link": "Compartir enlace", + "share-logs": "Compartir registros", + "share-my-profile": "Compartir mi perfil", + "share-profile": "Compartir perfil", + "share-profile-link": "Compartir enlace del perfil", + "share-public-chat-text": "Echa un vistazo a este chat público en la app de Status: {{link}}", + "shared": "Compartido", + "sharing-copied-to-clipboard": "Copiado", + "sharing-copy-to-clipboard": "Copiar", + "sharing-data-desc-1": "Los datos se validan con las reglas públicas para asegurar que no se envíen datos confidenciales. No confíes, verifica.", + "sharing-data-desc-2": "Los datos de uso se envían cifrados de extremo a extremo a través de la red peer-to-peer de Status", + "sharing-data-desc-3": "En lugar de la clave de chat habitual, se utiliza una clave de uso único", + "sharing-data-desc-4": "Los datos de uso no pueden asociarse con su dirección IP", + "sharing-data-desc-5": "Los datos acumulativos de todos los usuarios están disponibles públicamente", + "sharing-data-desc-6": "Los datos se eliminan de su teléfono después de que se envían", + "sharing-share": "Compartir", + "shell-placeholder-subtitle": "Abra pestañas de sus comunidades, mensajes, billetera y ventanas del navegador", + "shell-placeholder-title": "Sus aplicaciones se ejecutarán aquí", + "show-less": "Mostrar menos", + "show-more": "Mostrar más", + "show-notifications": "Mostrar notificaciones", + "show-profile-pictures": "Mostrar las fotos de perfil de", + "show-profile-pictures-to": "Muestra tu foto de perfil a", + "show-qr": "Mostrar código QR", + "show-transaction-data": "Mostrar los datos de la transacción", + "sign-and-send": "Firmar y enviar", + "sign-anyway": "Continuar de todas maneras", + "sign-in": "Regístrate", + "sign-message": "Firmar mensaje", + "sign-out": "Cerrar sesión", + "sign-request-failed": "No se pudo firmar el mensaje", + "sign-with": "Firma con", + "sign-with-password": "Firmar con contraseña", + "sign-you-in": "Iniciando sesión...", + "signing": "Firma", + "signing-a-message": "Firmando un mensaje", + "signing-phrase": "Frase de firma", + "skip": "Omitir", + "slippage": "Desviación", + "slow": "Lenta", + "something-went-wrong": "Algo salió mal", + "soon": "Pronto", + "sort-communities": "Ordenar comunidades", + "specify-address": "Especificar dirección", + "specify-name": "Especifica un nombre", + "specify-network-id": "Especifica ID de red", + "specify-rpc-url": "Especifica una URL RPC", + "specify-server-public-key": "Ingrese la clave pública del servidor", + "specify-symbol": "Especifica un símbolo", + "start-chat": "Iniciar chat", + "start-conversation": "Empezar conversación", + "start-group-chat": "Iniciar chat grupal", + "start-new-chat": "Iniciar nuevo chat", + "starter-pack-coming": "El Paquete de Inicio viene en camino", + "starter-pack-coming-description": "Puede tomar de unos minutos a horas", + "starter-pack-received": "Paquete de Bienvenida", + "starter-pack-received-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "status": "Status", + "status-always-online": "Siempre en línea", + "status-automatic": "Automático", + "status-automatic-subtitle": "Establecer estado automáticamente", + "status-confirmed": "Confirmado", + "status-dnd": "No interrumpir", + "status-dnd-subtitle": "Silencia todas las notificaciones", + "status-hardwallet": "Status hardwallet", + "status-inactive": "Inactivo", + "status-inactive-subtitle": "Oculta tu estado en línea", + "status-is-open-source": "Status es código abierto", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status tiende a usar una gran cantidad de datos al sincronizar chats. Puedes optar por no sincronizar cuando estás en la red móvil", + "status-not-sent-click": "No está confirmado. Haz clic para ver las opciones", + "status-not-sent-tap": "No está confirmado. Pulsa para ver las opciones", + "status-pending": "Pendiente", + "status-sent": "Enviado", + "status-tx-not-found": "TX no encontrada", + "status-updates-descr": "Las actualizaciones de estado aparecerán aquí. Agregue el perfil como un contacto para recibir actualizaciones en su línea de tiempo.", + "statuses-descr": "Comparte lo que tienes en mente y mantente al día con tus contactos", + "statuses-my-status-descr": "Comparte lo que tienes en mente. Cualquiera que visite su perfil podrá ver su estado. Las personas que te agreguen como contacto recibirán tus actualizaciones en su línea de tiempo.", + "step-i-of-n": "Paso {{step}} de {{number}}", + "sticker": "Pegatina", + "sticker-market": "Sticker market", + "storage": "Almacenamiento", + "strikethrough": "Tachado", + "submit": "Enviar", + "submit-bug": "Reportar un error", + "success": "Éxito", + "successful-connection": "Conexión exitosa", + "suggested-min-tip": "Propina mín. sugerida", + "suggested-price-limit": "Límite de precio sugerido", + "swap": "Intercambiar", + "swap-details": "Detalles del intercambio", + "switch-to-simple-interface": "Cambiar a interfaz sencilla", + "symbol": "Símbolo", + "sync-all-devices": "Sincronizar todos los dispositivos", + "sync-in-progress": "Sincronizando...", + "sync-settings": "Ajustes de sincronización", + "sync-synced": "Sincronizado", + "syncing-devices": "Sincronizando...", + "system": "Sistema", + "tabs": "Pestañas", + "tag-was-lost": "La etiqueta se perdió", + "tap-card-again": "Vuelve a tocar la tarjeta con la parte trasera de tu teléfono.", + "terms-of-service": "Términos de uso", + "test-networks": "Redes de prueba", + "text-input-disabled": "Por favor espera un momento...", + "thank-you": "Gracias", + "this-device": "Este dispositivo", + "this-device-desc": "Tus claves serán encriptadas y almacenadas de forma segura en tu dispositivo", + "this-is-you-signing": "Esta es tu frase de firma", + "this-will-take-few-seconds": "Esto tomará unos segundos", + "three-days": "Tres días", + "three-words-description": "Deberías ver estas tres palabras antes de firmar cada transacción", + "three-words-description-2": "Si ves una combinación diferente, cancela la transacción y cierra sesión", + "timeline": "Línea de tiempo", + "tip-cap": "Tope de propina", + "to": "Para", + "to-block": "Bloquear", + "to-enable-biometric": "Para habilitar {{bio-type-label}}, debes guardar tu contraseña en la pantalla de desbloqueo", + "to-encrypt-enter-password": "Para encriptar la cuenta, por favor, ingresa tu contraseña", + "to-see-this-message": "Para ver este mensaje,", + "token": "Ficha", + "token-auto-validate-decimals-error": "Decimales incorrectos para el token {{symbol}} en la dirección {{address}} - establecido en {{expected}} pero detectado como {{actual}}", + "token-auto-validate-name-error": "Nombre incorrecto para el token {{symbol}} en la dirección {{address}} - establecido en {{expected}} pero detectado como {{actual}}", + "token-auto-validate-symbol-error": "Símbolo incorrecto para el token {{symbol}} en la dirección {{address}} - configurado en {{expected}} pero detectado como {{actual}}", + "token-details": "Detalles del token", + "token-price": "Precio del token", + "topic-name-error": "Utiliza solo letras minúsculas (a - z), números y guiones (-). No uses las claves de chat", + "total-gas": "Total del gas", + "total-members": "Total de miembros", + "transaction": "Transacción", + "transaction-data": "Transaction data", + "transaction-declined": "Transacción rechazada", + "transaction-description": "Transacciones con 12 confirmaciones pueden considerarse irreversibles", + "transaction-details": "Detalles de la transacción", + "transaction-failed": "Transacción fallida", + "transaction-fee": "Tarifa de transacción", + "transaction-history": "Historial de transacciones", + "transaction-request": "Solicitud de transacción", + "transaction-sent": "Transacción enviada", + "transaction-signed": "La transacción se ha firmado con éxito", + "transactions": "Transacciones", + "transactions-filter-select-all": "Seleccionar todo", + "transactions-filter-title": "Filtrar historial", + "transactions-history": "Historial de transacciones", + "transactions-history-empty": "Aún no hay transacciones en tu historial", + "transactions-history-loading": "Cargando historial de transacciones. Esto podrá tomar un tiempo.", + "transactions-load-more": "Mostrar más", + "transactions-management-enabled": "Gestión de transacciones (alfa)", + "transactions-sign": "Firmar", + "transfer-ma-unknown-error-desc-1": "Parece que su multicuenta no se eliminó. Es posible que la base de datos se haya restablecido", + "transfer-ma-unknown-error-desc-2": "Por favor verifique su lista de cuentas e intente de nuevo. Si la cuenta no está listada vaya a Acceder llaves existentes para recuperar con frase semilla", + "transfers-fetching-failure": "El historial de transferencias no pudo ser actualizado. Compruebe su conexión y refresque para intentarlo de nuevo", + "tribute-required-by-multiaccount": "{{multiaccount-name}} requiere SNT para iniciar un chat.", + "tribute-state-paid": "Tributo pagado", + "tribute-state-pending": "Tributo pendiente", + "tribute-state-required": "Requiere tributo de {{snt-amount}} SNT", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Agregar amigos como un contacto para permitir chats sin pago de tributo.", + "tribute-to-talk-are-you-friends": "¿Son amigos?", + "tribute-to-talk-ask-to-be-added": "Pide que te añadan como contacto", + "tribute-to-talk-contact-received-your-tribute": "recibió tu tributo. Ahora pueden conversar con seguridad entre ustedes.", + "tribute-to-talk-desc": "Monetiza tu atención requiriendo SNT para que la gente nueva inicie un chat", + "tribute-to-talk-disabled": "Tribute to Talk deshabilitado", + "tribute-to-talk-disabled-note": "A partir de ahora, las personas nuevas pueden iniciar un chat contigo sin enviar SNT.", + "tribute-to-talk-enabled": "Tienes Tribute to Talk habilitado.", + "tribute-to-talk-finish-desc": "A partir de ahora, sólo recibirás mensajes de los contactos, y de la gente que pagó", + "tribute-to-talk-learn-more-1": "Tu tiempo y atención son Tus activos más valiosos. Tribute to Talk te permite configurar la cantidad de SNT requerida para que nuevas personas inicien un chat contigo.", + "tribute-to-talk-learn-more-2": "Cualquier persona que no esté en tu lista de contactos deberá pagar, y tu puedes responder una vez que lo hayan hecho.", + "tribute-to-talk-learn-more-3": "Siempre puedes devolver el dinero, pero para asegurarte de que tus amigos puedan contactarte libremente, añádelos primero como contacto.", + "tribute-to-talk-paywall-learn-more-1": "Nuestro tiempo y atención son nuestros activos más valiosos. Tribute to Talk te permite contactar a nuevas personas a cambio de un pago de SNT.", + "tribute-to-talk-paywall-learn-more-2": "Para iniciar un chat con alguien que tiene configurado tributos, simplemente paga el SNT requerido y serás agregado como un contacto.", + "tribute-to-talk-paywall-learn-more-3": "Si los conoces, puedes compartir tu perfil fuera de Status para ser agregado de forma gratuita.", + "tribute-to-talk-pending": "Tributo pendiente de confirmación", + "tribute-to-talk-pending-note": "La transacción de tributo está pendiente de confirmación en la red. Puedes verificar el status en el historial de transacciones", + "tribute-to-talk-removing-note": "Eliminar Tribute to Talk permitirá que nuevas personas inicien un chat sin enviar SNT. Requiere que se realice una transacción.", + "tribute-to-talk-set-snt-amount": "Establece la cantidad de SNT requerida para que nuevas personas inicien un chat", + "tribute-to-talk-signing": "Esperando para firmar la transacción", + "tribute-to-talk-transaction-failed-note": "La transacción ha fallado y tu configuración de Tribute to Talk no ha cambiado", + "tribute-to-talk-tribute-received1": "El tributo fue recibido. Tú y", + "tribute-to-talk-tribute-received2": "ahora son contactos y pueden chatear de forma segura entre sí.", + "tribute-to-talk-you-require-snt": "Se necesita SNT para que las personas nuevas inicien un chat.", + "try-again": "Inténtalo de nuevo", + "try-keeping-the-card-still": "Intenta mantener la tarjeta quieta", + "turn-nfc-description": "NFC está deshabilitado en su dispositivo. Puedes habilitarlo en la configuración", + "turn-nfc-on": "activar nfc", + "two-minutes": "dos minutos", + "tx-fail-description1": "Es probable que esta transacción falle. Firme bajo su propio riesgo utilizando una tarifa de red personalizada.", + "tx-fail-description2": "Es probable que esta transacción falle. Establezca una tarifa de red personalizada para firmar bajo su propio riesgo.", + "type": "Tipo", + "type-a-message": "Mensaje", + "ulc-enabled": "ULC habilitado", + "unable-to-fetch": "No es posible recuperar el historial de chat", + "unable-to-read-this-code": "No se puede leer este código", + "unable-to-send-messages": "No es posible enviar o recibir mensajes", + "unblock": "Desbloquear", + "unblock-contact": "Desbloquear este usuario", + "undo": "Deshacer", + "unknown-status-go-error": "Error de status-go desconocido", + "unlimited": "Ilimitado", + "unlock": "Desbloquear", + "unmute": "Activar sonido", + "unmute-chat": "Dejar de silenciar chat", + "unmute-community": "Dejar de silenciar comunidad", + "unpair-card": "Desvincular tarjeta", + "unpair-card-confirmation": "Esta operación desvinculará la tarjeta del dispositivo actual. Requiere autorización del código de acceso de 6 dígitos. ¿Quieres continuar?", + "unpair-keycard": "Desemparejar la Keycard de este teléfono", + "unpair-keycard-warning": "El código de emparejamiento\/PUK y el PIN no cambian", + "unpaired-keycard-text": "La Keycard que tocaste no está asociada con este teléfono", + "unpaired-keycard-title": "Parece que tu tarjeta ha sido desvinculada", + "unpin": "Desanclar", + "unpin-from-channel": "Desanclar del canal", + "unpin-from-chat": "Desanclar del chat", + "untrustworthy": "No confiable", + "update": "Actualizar", + "update-to-listen-audio": "¡Actualizá a la última versión para escuchar un mensaje de audio acá!", + "update-to-see-image": "¡Actualizá a la última versión para ver una buena imagen acá!", + "update-to-see-sticker": "¡Actualizá a la última versión para ver un lindo sticker acá!", + "updates-to-tos": "Actualizaciones de las Condiciones de Uso", + "updates-to-tos-desc": "Antes de continuar, por favor, revise las Condiciones de Uso y confirme que asume toda la responsabilidad por el uso de la aplicación.", + "url": "URL", + "usd-currency": "Dólar estadounidense", + "use-as-profile-picture": "Usar como foto de perfil", + "use-valid-contact-code": "Por favor, ingresa o escanea una clave de contacto o nombre de usuario válido", + "validation-amount-invalid-number": "La cantidad no es un número válido", + "validation-amount-is-too-precise": "La cantidad es muy precisa. La cantidad máxima de decimales es {{decimals}}.", + "verified-community": "✓ Comunidad verificada", + "version": "Versión de la app", + "view": "Ver", + "view-community-rules": "Ver Reglas de Comunidad", + "view-cryptokitties": "Ver en CryptoKitties", + "view-cryptostrikers": "Ver en CryptoStrikers", + "view-data": "Ver datos", + "view-details": "Ver detalles", + "view-etheremon": "Ver en Etheremon", + "view-gitcoin": "Ver en Gitcoin", + "view-members": "Ver miembros", + "view-on-opensea": "Ver en OpenSea", + "view-pinned-messages": "Ver mensajes anclados", + "view-profile": "Ver perfil", + "view-public-dashboard": "Ver panel público", + "view-rules": "Ver reglas", + "view-signing": "Ver la frase de firma", + "view-superrare": "Ver en SuperRare", + "vote-to-feature": "Votar para destacar esta comunidad", + "waiting-for-wifi": "Sin Wi-Fi, la sincronización de mensajes está deshabilitada.", + "waiting-for-wifi-change": "Ajustes", + "waiting-to-sign": "Esperando para firmar la transacción...", + "waiting-wi-fi": "Esperando Wi-Fi…", + "waku-bloom-filter-mode": "Modo de filtro Waku bloom", + "wakuv2-change-nodes": "¿Estás seguro de que quieres cambiar los nodos de Wakuv2?", + "wakuv2-node-format": "\/ip4\/{nodo-ip}\/tcp\/{puerto}\/p2p\/{id}", + "wakuv2-settings": "Ajustes de Waku v2", + "wallet": "Billetera", + "wallet-address": "Dirección de la billetera", + "wallet-asset": "Activo", + "wallet-assets": "Activos", + "wallet-backup-recovery-title": "Respalda tu frase semilla", + "wallet-choose-recipient": "Elegir destinatario", + "wallet-collectibles": "Coleccionables", + "wallet-insufficient-funds": "Fondos insuficientes", + "wallet-insufficient-gas": "No hay suficiente ETH para el gas", + "wallet-invalid-address": "Dirección inválida: \n {{data}}", + "wallet-invalid-address-checksum": "Error en la dirección: \n {{data}}", + "wallet-invalid-chain-id": "La red no coincide: \n {{data}} pero la cadena actual es {{chain}}", + "wallet-key-content": "Una dirección hexadecimal de 64 caracteres basada en el estándar Ethereum y que comienza con 0x. De cara al público, tu clave de billetera se comparte con otros cuando deseas recibir fondos. También se conoce como \"dirección de Ethereum\" o \"dirección de billetera\".", + "wallet-key-title": "Dirección de la cuenta", + "wallet-manage-accounts": "Gestionar cuentas", + "wallet-manage-assets": "Administrar activos", + "wallet-request": "Solicitud", + "wallet-send": "Enviar", + "wallet-send-min-units": "Mínimo 21000 unidades", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Ajustes de billetera", + "wallet-total-value": "Valor total", + "wallet-transaction-total-fee": "Costo total", + "wants-to-access-profile": "quiere acceder a tu perfil", + "warning": "Advertencia", + "warning-message": "Lo sentimos, limitamos el envío de varios mensajes en rápida sucesión para evitar el spam. Por favor, inténtalo de nuevo en un momento.", + "warning-sending-to-contract-descr": "La dirección que ingresó es un contrato inteligente, enviar fondos a esta dirección puede resultar en la pérdida de fondos. Para interactuar con una DApp, abra la DApp en el navegador de Status.", + "watch-only": "Sólo para ver", + "wc-brand-guide": "Orientación sobre el uso de identidad de marca como marcas registradas y logotipos", + "wc-disclaimer": "Aclaraciones (incluyendo proveedores externos), garantías y liberaciones legales", + "wc-dispute": "Disposiciones sobre solución de controversias", + "wc-how-to-use-status-app": "Cómo utilizar la app Status, incluyendo privacidad y seguridad", + "wc-new-tos-based-on-principles-prefix": "Nuevas Condiciones de Uso diseñadas sobre la base de nuestro", + "web-view-error": "No se puede cargar la página", + "websites": "Sitios web", + "webview-camera-permission-requests": "Solicitudes de permisos de la cámara Webview", + "webview-camera-permission-requests-subtitle": "Cuando está habilitado, los sitios web y las dapps pueden solicitar el uso de su cámara", + "welcome-blank-community-message": "Tus comunidades aparecerán aquí.", + "welcome-blank-message": "Tus chats aparecerán aquí. Para iniciar nuevos chats, presiona el botón ⊕", + "welcome-community-blank-message": "Sus canales aparecerán aquí. Para crear un nuevo canal, haz clic en el botón ⊕ y selecciona \"Crear un canal\"", + "welcome-community-blank-message-edit-chats": "Tus canales aparecerán aquí. Para crear un nuevo canal, vuelve a la pantalla de la comunidad, haz clic en el botón ⊕ y selecciona \"Crear un canal\"", + "welcome-screen-text": "¡Configura tu billetera, invita a amigos a chatear \n y busca dapps populares!", + "welcome-to-status": "¡Bienvenidx a Status!", + "welcome-to-status-description": "Configura tu cripto billetera, invita a tus amigos a chatear y navegar apps descentralizadas", + "what-changed": "Lo que cambió", + "what-is-shared": "Qué es compartido", + "whats-on-your-mind": "Lo que tienes en mente...", + "whats-trending": "Mira lo que está de moda", + "word-count": "Número de palabras", + "word-n": "Palabra #{{number}}", + "word-n-description": "Para comprobar si has realizado una copia de seguridad de la frase semilla correctamente, escribe arriba la palabra #{{number}}.", + "words-n": { + "one": "1 palabra", + "other": "{{count}} palabras" + }, + "write-down-and-store-securely": "Escribe los códigos\n y guárdalos de forma segura", + "wrong-address": "Dirección incorrecta", + "wrong-card": "Tarjeta equivocada", + "wrong-card-text": "Haz tocado una tarjeta que no corresponde a las llaves seleccionadas", + "wrong-contract": "Contrato incorrecto", + "wrong-keycard-text": "La Keycard que tocaste no está asociada con este teléfono", + "wrong-keycard-title": "Parece que has hecho tapping \nuna keycard incorrecta", + "wrong-password": "Contraseña incorrecta", + "wrong-word": "Palabra incorrecta", + "yes": "Sí", + "you": "tú", + "you-already-have-an-asset": "Ya tienes un activo {{value}}", + "you-are-all-set": "¡Estás listo!", + "you-are-all-set-description": "Ahora, si pierdes tu teléfono, puedes acceder a tus fondos y llave de chat usando la frase semilla", + "you-can-change-account": "Puedes cambiar el nombre y el color de la cuenta a lo que deseas", + "you-can-choose-preview-websites": "Puede elegir cuál de los siguientes sitios web puede previsualizar el enlace de las descripciones e imágenes en los chats", + "you-can-fetch": "Puedes recuperar el historial de chat", + "you-dont-have-contacts": "Todavía no tenés ningún contacto.", + "you-dont-have-contacts-invite-friends": "Aún no tienes contactos.\nInvita a tus amigos a empezar a chatear.", + "you-dont-have-stickers": "Todavía no tienes stickers", + "you-will-need-this-code": "Necesitarás este código para abrir Status y firmar transacciones", + "you-will-start-from-scratch": "Comenzarás desde cero con un nuevo set de claves", + "your-card-is-frozen": "Tu Keycard está congelada. Restablece el acceso a la tarjeta", + "your-contact-code": "Conceder acceso autoriza a esta DApp a obtener tu clave de chat", + "your-data-belongs-to-you": "Si pierdes tu frase semilla, pierdes tus datos y fondos", + "your-data-belongs-to-you-description": "Si pierdes acceso, por ejemplo al perder tu teléfono, solo puedes acceder a tus llaves con tu frase semilla. Nadie aparte de ti tiene tu frase semilla. Anótala. Mantenla segura.", + "your-keys": "Tus claves", + "your-price-limit": "Tu límite de precio", + "your-recovery-phrase": "Tu frase semilla", + "your-recovery-phrase-description": "Esta es tu frase semilla. La usas para comprobar que esta es tu billetera. ¡Sólo la verás una vez! Escríbela en un papel y guárdala en un lugar seguro. La necesitarás si pierdes o reinstalas tu billetera.", + "your-tip-limit": "Tu límite de propina", + "youre-on-mobile-network": "Estás en una red móvil" +} diff --git a/translations/es_AR.json b/translations/es_AR.json new file mode 100644 index 00000000000..49610d3a2d9 --- /dev/null +++ b/translations/es_AR.json @@ -0,0 +1,1506 @@ +{ + "You": "Tú", + "about-app": "Acerca de", + "about-key-storage-content": "Status nunca accederá a tu clave privada. Asegúrate de respaldar tu frase semilla. Si pierdes tu teléfono es la única manera de acceder a tus claves.", + "about-key-storage-title": "Acerca del almacenamiento de claves", + "about-names-content": "¡Nadie puede pretender ser tú! Eres anónimo por defecto y nunca tienes que revelar tu nombre real. Puedes registrar un nombre personalizado por una pequeña tarifa.", + "about-names-title": "Los nombres no se pueden cambiar", + "accept": "Aceptar", + "accept-and-share-address": "Aceptar y compartir dirección", + "access-existing-keys": "Ingresar claves existentes", + "access-key": "Clave de acceso", + "account-added": "Cuenta agregada", + "account-color": "Color de la cuenta", + "account-content": "Puedes comparar las cuentas en Status con las cuentas bancarias. Al igual que una cuenta bancaria, una cuenta suele tener una dirección y un saldo; se utiliza esta cuenta para realizar transacciones con Ethereum. Puedes tener varias cuentas en tu billetera. Todas accedidas en Status.", + "account-exists-title": "Cuenta ya existe", + "account-name": "Nombre de la cuenta", + "account-settings": "Ajustes de la cuenta", + "account-title": "Cuenta", + "accounts": "Cuentas", + "actions": "Acciones", + "active-online": "En línea", + "active-unknown": "Desconocido", + "add": "Agregar", + "add-a-watch-account": "Agregar una dirección de solo observación", + "add-account": "Agregar una cuenta", + "add-account-description": "Puedes importar cualquier tipo de cuenta de Ethereum para agregarla a tu billetera de Status", + "add-account-incorrect-password": "La contraseña parece ser incorrecta. Ingresa la contraseña que usas para desbloquear la app.", + "add-an-account": "Agregar una cuenta", + "add-bootnode": "Agregar bootnode", + "add-contact": "Agregar contacto", + "add-custom-token": "Agregar un token personalizado", + "add-favourite": "Agregar favorito", + "add-mailserver": "Agregar nodo de Status", + "add-members": "Agregar miembros", + "add-network": "Agregar red", + "add-new-contact": "Agregar nuevo contacto", + "add-nickname": "Agregar un apodo (opcional)", + "add-private-key-account": "Ingresa una cuenta de una llave privada", + "add-seed-account": "Agrega una cuenta con una frase semilla", + "add-to-contacts": "Agregar a contactos", + "add-to-contacts-text": "Al agregar un usuario a tu lista de contactos, compartes la dirección de tu billetera", + "add-to-favourites": "Agregar a favoritos", + "add-watch-account": "Agrega una cuenta sólo para mirar", + "address": "Dirección", + "address-or-ens-name": "Dirección o nombre ENS", + "address-received": "Dirección recibida", + "address-request-accepted": "Solicitud de dirección aceptada", + "address-requested": "Dirección solicitada", + "advanced": "Avanzado", + "advanced-settings": "Ajustes avanzados", + "advertiser-description": "Descubriste Status gracias a un socio nuestro. ¿Podemos chequear tu IP para poder recompensarlo? Esta información no será usada para nada más, y será removida completamente después de 7 días", + "advertiser-starter-pack-accept": "Aceptar", + "advertiser-starter-pack-decline": "Rechazar", + "advertiser-starter-pack-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "advertiser-starter-pack-title": "Paquete de Bienvenida", + "advertiser-title": "Privacidad por defecto", + "agree-by-continuing": "Al continuar, aceptas \n nuestras", + "all": "Todos", + "allow": "Permitir", + "allow-mention-notifications": "Mostrar @ menciones", + "allowing-authorizes-this-dapp": "Permitir autoriza a esta DApp a recuperar la dirección de tu billetera y habilitar Web3", + "already-have-asset": "Ya tienes este activo", + "amount": "Cantidad", + "appearance": "Apariencia", + "apply": "Aplicar", + "are-you-sure": "¿Estás seguro\/a?", + "are-you-sure-description": "No podrás volver a ver toda la frase semilla de nuevo", + "are-you-sure-to-cancel": "¿Estas seguro que quieres cancelar?", + "are-you-sure?": "¿Estás seguro\/a?", + "ask-in-status": "Haz una pregunta o reporta un error", + "at": "en", + "attribution-received": "{{attrib}} de {{max}} bonos recibidos", + "audio": "Audio", + "audio-recorder": "Grabadora", + "audio-recorder-error": "Error de la grabadora", + "audio-recorder-max-ms-reached": "Tiempo máximo de grabación alcanzado", + "audio-recorder-permissions-error": "Tenés que dar permiso para enviar mensajes de audio", + "authorize": "Autorizar", + "available": "Disponible", + "available-participants": { + "one": "Puedes seleccionar un participante más", + "other": "Puedes seleccionar {{count}} participantes más" + }, + "back": "Atrás", + "back-up": "Respaldo", + "back-up-seed-phrase": "Respalda la frase semilla", + "back-up-your-seed-phrase": "Respalda tu frase semilla", + "backup-disabled": "Deshabilitado", + "backup-recovery-phrase": "Respalda la frase semilla", + "balance": "Saldo", + "begin-set-up": "empezar la configuración", + "biometric-auth-android-sensor-desc": "Sensor táctil", + "biometric-auth-android-sensor-error-desc": "Fallido", + "biometric-auth-android-title": "Autenticación requerida", + "biometric-auth-confirm-logout": "Volver a iniciar sesión", + "biometric-auth-confirm-message": "Para continuar, la autenticación biométrica es necesaria, si no es posible inicia sesión de nuevo usando la contraseña de las llaves.", + "biometric-auth-confirm-title": "¡Debes autenticarte!", + "biometric-auth-confirm-try-again": "Inténtalo de nuevo", + "biometric-auth-error": "No se puede realizar la autenticación biométrica ({{code}})", + "biometric-auth-login-error-title": "Error de autenticación biométrica", + "biometric-auth-login-ios-fallback-label": "Ingresa la contraseña", + "biometric-auth-reason-login": "Iniciar sesión en Status", + "biometric-auth-reason-verify": "Verificar autenticación", + "biometric-disable-bioauth": "deshabilitar {{bio-type-label}}", + "biometric-disable-password-description": "Si deshabilitas esto, también ", + "biometric-disable-password-title": "Deshabilitar guardar contraseña", + "biometric-enable": "Si no deseas ingresar tu contraseña cada vez para acceder a la app, habilita el inicio de sesión {{bio-type-label}}", + "biometric-enable-button": "Habilitar {{bio-type-label}}", + "biometric-enable-keycard": "Si no deseas usar tu Keycard cada vez para acceder a la app, habilita el inicio de sesión {{bio-type-label}}", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Huella dactilar", + "biometric-secure-with": "Asegurar con {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "blank-keycard-text": "Puedes continuar con tu keycard una vez que hayas generado tus claves y tu nombre", + "blank-keycard-title": "Parece que has hecho tapping \nuna keycard en blanco", + "block": "Bloque", + "block-contact": "Bloquear este usuario", + "block-contact-details": "El bloqueo eliminará los mensajes anteriores de este usuario y evitará que otros nuevos lleguen a ti", + "blocked-users": "Usuarios bloqueados", + "bootnode-address": "Dirección de bootnode", + "bootnode-details": "Detalles de Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes habilitados", + "bootnodes-settings": "Ajustes de bootnode", + "browsed-websites": "El historial del navegador aparecerá aquí", + "browser": "Navegador", + "browser-not-secure": "¡La conexión no es segura! No firmes transacciones o envíes datos personales en este sitio.", + "browser-secure": "La conexión es segura. Asegúrate de que realmente confías en este sitio antes de firmar transacciones o ingresar datos personales.", + "browsers": "Navegadores", + "browsing-cancel": "Cancelar", + "browsing-open-in-android-web-browser": "Abrir en Android", + "browsing-open-in-ios-web-browser": "Abrir en iOS", + "browsing-open-in-status": "Abrir en Status", + "browsing-site-blocked-description1": "Hemos detectado posibles actividades maliciosas desde esta dirección. Para protegerte a ti y a tu billetera, prevenimos la navegación.\n\nSi crees que esto es un error, haznoslo saber en la", + "browsing-site-blocked-description2": "chat público", + "browsing-site-blocked-go-back": "Regresa", + "browsing-site-blocked-title": "Este sitio está bloqueado", + "browsing-title": "Navegar", + "bug-report": "Reportar un error", + "buy-crypto": "Comprar criptomonedas", + "buy-crypto-choose-a-service": "Elige un servicio que quieras usar para comprar criptomonedas", + "buy-crypto-description": "Encuentre una dapp para comprar cripto ahora", + "buy-crypto-leaving": "Estás saliendo de Status y entrando en un sitio web de terceros para completar tu compra", + "buy-crypto-title": "Parece que tu billetera está vacía", + "camera-access-error": "Para permitir el acceso a la cámara, porfavor, ve a ajustes de sistema y asegúrate que este seleccionada la opción Status > Cámara", + "can-not-add-yourself": "Ese eres tú, para iniciar un chat elige a alguien más", + "can-send-messages": "Puedes enviar y recibir mensajes nuevos", + "cancel": "Cancelar", + "cancel-keycard-setup": "Cancelar la configuración de Keycard", + "cancelling": "Cancelando", + "cannot-read-card": "No puede leer la tarjeta.\nPor favor, mantenla en la parte posterior de tu teléfono", + "cannot-use-default-pin": "No está permitido el código de acceso 000000. \nPor favor utiliza otro número", + "cant-open-public-chat": "No se puede abrir el chat público", + "cant-report-bug": "No puedo reportar un error", + "card-is-blank": "Esta tarjeta esta en blanco", + "card-reseted": "La tarjeta ha sido reseteada", + "card-unpaired": "La tarjeta se ha desvinculado del dispositivo actual", + "category": "Categoría", + "change-fleet": "Cambiar fleet a {{fleet}}", + "change-logging-enabled": "¿Está seguro\/a de que deseas {{enable}} iniciar sesión?", + "change-passcode": "Cambiar código de acceso", + "change-password": "Cambiar contraseña", + "change-pin": "Cambiar el código de acceso de 6 dígitos", + "change-tip": "Cambiar propina", + "changed-amount-warning": "La cantidad se cambió de {{old}} a {{new}}", + "changed-asset-warning": "El activo se cambió de {{old}} a {{new}}.", + "chaos-mode": "Modo caos", + "chaos-unicorn-day": "Día del Unicornio del Caos", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Chatea y realiza transacciones privadas con amigos", + "chat-is-a-contact": "Contacto", + "chat-is-not-a-contact": "No es contacto", + "chat-key": "Clave de chat", + "chat-key-content": "Los mensajes en el protocolo de chat de Status se envían y reciben utilizando claves de encriptación. La clave pública de chat es una cadena de caracteres que compartes con otros para que puedan enviarte mensajes en Status.", + "chat-key-title": "Clave de chat", + "chat-link-previews": "Vistas previas de enlaces de chat", + "chat-name": "Nombre del chat", + "chat-name-content": "Tres palabras aleatorias, derivadas algorítmicamente de tu clave de chat y utilizadas como tu alias predeterminado en el chat. Los nombres de los chats son completamente únicos; ningún otro usuario puede tener las mismas tres palabras.", + "chat-name-title": "Nombre del chat", + "chat-notification-preferences": "Configuración de las notificaciones", + "chat-settings": "Ajustes de chat", + "chats": "Chats", + "check-on-opensea": "Comprobar en opensea", + "check-your-recovery-phrase": "Comprueba tu frase semilla", + "choose-actions": "Elige acciones", + "choose-authentication-method": "Elije un método de autenticación", + "choose-new-location-for-keystore": "Seleccione una nueva ubicación para guardar su archivo keystore", + "choose-storage": "Elegir almacenamiento", + "clear": "Limpiar", + "clear-all": "Borrar todo", + "clear-history": "Borrar historial", + "clear-history-action": "Limpiar", + "clear-history-confirmation": "¿Borrar historial?", + "clear-history-confirmation-content": "¿Seguro que quieres limpiar este historial de chat?", + "clear-history-title": "¿Borrar historial?", + "close": "Cerrar", + "close-all": "Cerrar todo", + "close-app-button": "Confirmar", + "close-app-content": "La aplicación se detendrá y cerrará. Cuando la vuelvas a abrir, se utilizará la red seleccionada", + "close-app-title": "¡Advertencia!", + "command-button-send": "Enviar", + "communities": "Comunidades", + "communities-alpha": "Comunidades (alfa)", + "communities-verified": "✓ Comunidad de Status Verificada", + "community-color": "Color de la comunidad", + "community-color-placeholder": "Elige un color", + "community-edit-title": "Editar comunidad", + "community-image-pick": "Elige una imagen", + "community-image-remove": "Remover", + "community-image-take": "Toma una foto", + "community-invite-title": "Invitá", + "community-key": "Clave privada de la comunidad", + "community-key-placeholder": "Escriba su clave privada de la comunidad", + "community-members": { + "one": "miembro", + "other": "miembros" + }, + "community-members-title": "Miembros", + "community-message-preview": "Invitación para unirse a {{community-name}}", + "community-private-key": "Clave privada de la comunidad", + "community-requests-to-join-title": "Solicitudes de membresía", + "community-roles": "Roles", + "community-share-title": "Compartir", + "community-thumbnail-image": "Imagen en miniatura", + "community-thumbnail-upload": "Subir", + "complete-hardwallet-setup": "Esta tarjeta ahora está vinculada. La necesitarás para firmar transacciones y desbloquear tus llaves", + "completed": "Completado", + "confirm": "Confirmar", + "confirm-password-placeholder": "Confirmá tu contraseña...", + "confirmation-request": "Solicitud de confirmación", + "confirmations": "Confirmaciones", + "confirmations-helper-text": "Cuando la transacción tenga 12 confirmaciones, puede considerarla resuelta.", + "connect": "Conectar", + "connect-mailserver-content": "¿Conectar con {{name}}?", + "connect-wallet": "Conectar billetera", + "connected": "Conectado", + "connected-to": "Conectado a", + "connecting": "Conectando...", + "connecting-requires-login": "Para conectarse a otra red es necesario iniciar sesión", + "connection-status": "Estado de conexión", + "connection-with-the-card-lost": "La conexión con la tarjeta\nse ha perdido", + "connection-with-the-card-lost-setup-text": "Para reanudar la configuración, manten la tarjeta en\n la parte trasera de tu teléfono y mantener la\n tarjeta en contacto con el teléfono", + "connection-with-the-card-lost-text": "Para proceder, manten la tarjeta en la parte posterior de tu teléfono", + "contact-code": "Clave de chat", + "contact-s": { + "one": "contacto", + "other": "contactos" + }, + "contacts": "Contactos", + "contacts-descr": "Tus contactos aparecerán aquí. Recibirá actualizaciones de estado de cualquier persona que agregue como contacto", + "contacts-empty": "Contactos con nombres ENS aparecerán aquí", + "continue": "Continuar", + "continue-anyway": "Continuar de todas maneras", + "contract-address": "Dirección del contrato", + "contract-interaction": "Interacción con el contrato", + "copy-info": "Copiar información", + "copy-qr": "Copiar código", + "copy-to-clipboard": "Copiar", + "copy-transaction-hash": "Copiar ID de transacción", + "cost-fee": "Costo\/Tarifa", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Crear", + "create-a-pin": "Crear un código de acceso de 6 dígitos", + "create-channel": "Crear un canal", + "create-channel-title": "Nuevo canal", + "create-community": "Crear una comunidad", + "create-group-chat": "Crear chat grupal", + "create-multiaccount": "Generar llaves", + "create-new-key": "Obtener nuevas claves", + "create-pin": "Crea un código de acceso de 6 dígitos", + "create-pin-description": "Necesitarás tu tarjeta + este código de 6 dígitos para desbloquear Status y confirmar las transacciones", + "created-group-chat-description": "Creaste el grupo {{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Moneda", + "currency-display-name-aed": "Dirham de los Emiratos Árabes Unidos", + "currency-display-name-afn": "Afgani afgano", + "currency-display-name-ars": "Peso argentino", + "currency-display-name-aud": "Dólar australiano", + "currency-display-name-bbd": "Dólar de Barbados", + "currency-display-name-bdt": "Taka bangladeshí", + "currency-display-name-bgn": "Lev búlgaro", + "currency-display-name-bhd": "Dinar bahreiní", + "currency-display-name-bnd": "Dólar de Brunéi", + "currency-display-name-bob": "Bolivia Bolíviano", + "currency-display-name-brl": "Real brasileño", + "currency-display-name-btn": "Ngultrum butanés", + "currency-display-name-cad": "Dólar canadiense", + "currency-display-name-chf": "Franco suizo", + "currency-display-name-clp": "Peso chileno", + "currency-display-name-cny": "Yuan chino", + "currency-display-name-cop": "Peso colombiano", + "currency-display-name-crc": "Colón costarricense", + "currency-display-name-czk": "Corona checa", + "currency-display-name-dkk": "Corona danesa", + "currency-display-name-dop": "Peso de la República Dominicana", + "currency-display-name-egp": "Libra egipcia", + "currency-display-name-etb": "Birr etíope", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Libra esterlina", + "currency-display-name-gel": "Lari georgiano", + "currency-display-name-ghs": "Cedi (Ghana)", + "currency-display-name-hkd": "Dólar de Hong Kong", + "currency-display-name-hrk": "Croacia Kuna", + "currency-display-name-huf": "Forinto húngaro", + "currency-display-name-idr": "Rupia Indonesia", + "currency-display-name-ils": "Nuevo séquel (Israel)", + "currency-display-name-inr": "Rupia india", + "currency-display-name-isk": "Corona islandesa", + "currency-display-name-jmd": "Dólar jamaiquino", + "currency-display-name-jpy": "Yen japonés", + "currency-display-name-kes": "Chelín keniano", + "currency-display-name-krw": "Won surcoreano", + "currency-display-name-kwd": "Dinar kuwaití", + "currency-display-name-kzt": "Tenge kazajo", + "currency-display-name-lkr": "Rupia de Sri Lanka", + "currency-display-name-mad": "Dirham marroquí", + "currency-display-name-mdl": "Leu moldavo", + "currency-display-name-mur": "Rupia de Mauricio", + "currency-display-name-mwk": "Kwacha malauí", + "currency-display-name-mxn": "Peso mexicano", + "currency-display-name-myr": "Ringgit malayo", + "currency-display-name-mzn": "Metical mozambiqueño", + "currency-display-name-nad": "Dólar namibio", + "currency-display-name-ngn": "Naira (Nigeria)", + "currency-display-name-nok": "Corona noruega", + "currency-display-name-npr": "Rupia nepalí", + "currency-display-name-nzd": "Dólar neozelandés", + "currency-display-name-omr": "Rial omaní", + "currency-display-name-pen": "Sol peruano", + "currency-display-name-pgk": "Kina (Papúa Nueva Guinea)", + "currency-display-name-php": "Peso filipino", + "currency-display-name-pkr": "Rupia pakistaní", + "currency-display-name-pln": "Zloty de Polonia", + "currency-display-name-pyg": "Guaraní paraguayo", + "currency-display-name-qar": "Riyal qatarí", + "currency-display-name-ron": "Leu de Rumanía", + "currency-display-name-rsd": "Dinar serbio", + "currency-display-name-rub": "Rublo ruso", + "currency-display-name-sar": "Arabia Saudita Riyal", + "currency-display-name-sek": "Corona sueca", + "currency-display-name-sgd": "Dólar de Singapur", + "currency-display-name-thb": "Baht tailandés", + "currency-display-name-try": "Lira turca", + "currency-display-name-ttd": "Dólar de Trinidad y Tobago", + "currency-display-name-twd": "Nuevo dólar taiwanés", + "currency-display-name-tzs": "Chelín tanzano", + "currency-display-name-uah": "Grivna (Ucrania)", + "currency-display-name-ugx": "Chelín ugandés", + "currency-display-name-usd": "Dólar estadounidense", + "currency-display-name-uyu": "Peso uruguayo", + "currency-display-name-vef": "Bolívar venezolano", + "currency-display-name-vnd": "Dong vietnamita", + "currency-display-name-zar": "Rand sudafricano", + "current": "Actual", + "current-average-tip": "Propina promedio actual", + "current-minimum-tip": "Propina mínima actual", + "current-network": "Red actual", + "current-pin": "Ingresa el código de acceso de 6 dígitos", + "current-pin-description": "Ingresa tu código de acceso de 6 dígitos para continuar", + "custom": "Personalizado", + "custom-networks": "Redes personalizadas", + "custom-node": "Estás utilizando un punto de conexión RPC personalizado. Es posible que el historial de transferencias locales esté incompleto.", + "custom-seed-phrase": "Frase semilla inválida", + "custom-seed-phrase-text-1": "Esta frase semilla no coincide con nuestro diccionario compatible. Revisa si hay palabras mal escritas.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Aceptar y Abrir", + "dapp-starter-pack-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "dapp-starter-pack-title": "Paquete de Bienvenida", + "dapp-would-like-to-connect-wallet": "quisiera conectarse a", + "dapps": "ÐApps", + "dapps-permissions": "Permisos de DApp", + "dark": "Oscuro", + "data": "Datos", + "data-syncing": "Sincronización de datos", + "database-reset-content": "Se han eliminado los chats, los contactos y la configuración. Podés usar tu cuenta con tu Keycard", + "database-reset-title": "Restablecer de la base de datos", + "database-reset-warning": "La base de datos se restablecerá. Se borrarán los chats, los contactos y la configuración", + "datetime-ago": "hace", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "día", + "other": "días" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "hora", + "other": "horas" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "minuto", + "other": "minutos" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "segundo", + "other": "segundos" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "hoy", + "datetime-yesterday": "ayer", + "decimals": "Decimales", + "decline": "Rechazar", + "decryption-failed-content": "Ocurrió un error desencriptando tus datos. Es posible que tengas que generar unas nuevas llaves y borrar tus datos antiguos pulsando \"Aplicar\", o haz clic en \"Cancelar\" para volver a intentarlo", + "default": "Predeterminados", + "default-assets": "Por defecto ERC20 y ERC721", + "delete": "Eliminar", + "delete-account": "Borrar cuenta", + "delete-and-leave-group": "Eliminar y salir del grupo", + "delete-bootnode": "Eliminar bootnode", + "delete-bootnode-are-you-sure": "¿Seguro que quieres eliminar este bootnode?", + "delete-bootnode-title": "Eliminar bootnode", + "close-chat": "Eliminar chat", + "close-chat-confirmation": "¿Estás seguro de que quieres eliminar este chat?", + "delete-confirmation": "¿Eliminar?", + "delete-keys-keycard": "Borrar llaves de la Keycard", + "delete-mailserver": "Eliminar nodo de Status", + "delete-mailserver-are-you-sure": "¿Estás seguro de que deseas eliminar este nodo de Status?", + "delete-mailserver-title": "Eliminar nodo de Status", + "delete-message": "Eliminar mensaje", + "delete-my-account": "Borrar mi cuenta", + "delete-my-profile": "Eliminar mi perfil", + "delete-network-confirmation": "¿Seguro que quieres eliminar esta red?", + "delete-network-error": "Por favor, conéctate a una red diferente antes de eliminar esta", + "delete-network-title": "¿Eliminar red?", + "delete-profile": "Eliminar perfil", + "delete-profile-warning": "Advertencia: Si no tiene tu frase de semilla escrita, perderá el acceso a sus fondos después de eliminar su perfil", + "deny": "Negar", + "derivation-path": "Ruta de derivación", + "description": "Descripción", + "dev-mode": "Modo de desarrollo", + "dev-mode-settings": "Ajustes del modo de desarrollo", + "device-syncing": "Sincronización de dispositivos", + "devices": "Dispositivos", + "disable": "deshabilitar", + "disable-all": "Deshabilitar todo", + "disabled": "Deshabilitado", + "disconnected": "Chat sin conexión", + "discover": "Descubrir", + "dismiss": "Descartar", + "done": "Listo", + "dont-ask": "No me vuelvas a preguntar", + "edit": "Editar", + "edit-chats": "Editar chats", + "edit-community": "Editar comunidad", + "edit-favourite": "Editar favorito", + "edit-group": "Editar grupo", + "edit-profile": "Editar perfil", + "empty-chat-description": "No hay mensajes \nen este chat todavía", + "empty-chat-description-community": "Ha estado tranquilo aquí durante las últimas {{quiet-hours}}.", + "empty-chat-description-one-to-one": "Cualquier mensaje que envíes aquí está encriptado y solo puedes leerlo tú y", + "empty-chat-description-public": "Ha estado tranquilo aquí durante las últimas {{quiet-hours}}. Inicia la conversación o ", + "empty-chat-description-public-share-this": "comparte este chat", + "empty-keycard-required": "Requiere una Keycard vacía", + "empty-pending-invitations-descr": "Las personas que deseen unirse al grupo\na través de un enlace de invitación aparecerá aquí", + "empty-tab": "Pestaña vacía", + "enable": "Habilitar", + "enable-all": "Habilitar todos", + "enable-link-previews": "¿Habilitar vistas previas de enlaces en el chat?", + "encrypt-with-password": "Encriptar con contraseña", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Agregar nombre de usuario", + "ens-agree-to": "De acuerdo con", + "ens-chat-settings": "Ajustes de chat", + "ens-custom-domain": "Dominio personalizado", + "ens-custom-username-hints": "Escribe el nombre de usuario completo, incluido el dominio personalizado como username.domain.eth", + "ens-custom-username-taken": "El nombre de usuario no te pertenece :(", + "ens-deposit": "Depósito", + "ens-dismiss-message": "Haga clic para descartar", + "ens-displayed-with": "Tus mensajes se muestran a otros con", + "ens-get-name": "Obtén un nombre de usuario universal", + "ens-got-it": "Ok, lo tengo.", + "ens-locked": "Nombre de usuario bloqueado. No lo podrás liberar hasta el {{date}}", + "ens-name-content": "Alias personalizado para tu clave de chat que puedes registrar utilizando Ethereum Name Service. Los nombres ENS son nombres de usuario descentralizados.", + "ens-name-not-found": "No se puede resolver el nombre ENS", + "ens-name-title": "Nombre ENS", + "ens-network-restriction": "Solo disponible en Mainnet", + "ens-no-usernames": "No tienes ningún nombre de usuario conectado", + "ens-powered-by": "Propulsado por Ethereum Name Services", + "ens-primary-username": "Nombre de usuario principal", + "ens-register": "Registrar", + "ens-registration-failed": "Para registrar el nombre de usuario, por favor, intenta de nuevo.", + "ens-registration-failed-title": "La transacción ha fallado", + "ens-registration-failure": "Registro fallido", + "ens-registration-in-progress": "Registro en curso...", + "ens-release-username": "Liberar nombre de usuario", + "ens-remove-hints": "La eliminación separará el nombre de usuario de tu clave.", + "ens-remove-username": "Eliminar nombre de usuario", + "ens-saved": "ahora está conectado con tu clave de chat y se puede usar en Status.", + "ens-saved-title": "Nombre de usuario agregado", + "ens-show-username": "Mostrar mi nombre de usuario ENS en los chats", + "ens-terms-header": "Términos de registro de nombre", + "ens-terms-point-1": "Los fondos se depositan por 1 año. Tu SNT se bloqueará, pero no se gastará.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Registro ENS).", + "ens-terms-point-2": "Después de 1 año, puedes liberar el nombre y recuperar tu depósito, o no tomar ninguna acción para mantener el nombre.", + "ens-terms-point-3": "Si los términos del contrato cambian – por ejemplo, Status realiza actualizaciones del contrato – el usuario tiene derecho a liberar el nombre de usuario independientemente del tiempo retenido.", + "ens-terms-point-4": "El controlador del contrato no puede acceder a tus fondos depositados. Sólo pueden ser devueltos a la dirección que los envió.", + "ens-terms-point-5": "Tu(s) dirección(es) se asociará(n) públicamente con tu nombre ENS.", + "ens-terms-point-6": "Los nombres de usuario se crean como nodos de subdominio de stateofus.eth y están sujetos a los términos del contrato inteligente de ENS.", + "ens-terms-point-7": "Autorizas el contrato para transferir SNT en tu nombre. Esto solo puede ocurrir cuando apruebas una transacción para autorizar la transferencia.", + "ens-terms-point-8": "Estos términos están garantizados por la lógica del contrato inteligente en las direcciones:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Términos de registro de nombre.", + "ens-test-message": "Hey", + "ens-transaction-pending": "Transacción pendiente...", + "ens-understand": "Entiendo que la dirección de mi billetera estará conectada públicamente a mi nombre de usuario.", + "ens-username": "Nombre de usuario ENS", + "ens-username-already-added": "El nombre de usuario ya está conectado con tu clave de chat y se puede usar dentro de Status.", + "ens-username-available": "✓ ¡Nombre de usuario disponible!", + "ens-username-connected": "Este nombre de usuario es de tu propiedad y está conectado con tu clave de chat.", + "ens-username-connected-continue": "Continuar configurando `Mostrar mi nombre de usuario ENS en los chats`.", + "ens-username-connected-with-different-key": "Continuar requerirá una transacción para conectar el nombre de usuario con tu clave de chat actual.", + "ens-username-connection-confirmation": "{{username}} se conectará una vez que se complete la transacción.", + "ens-username-hints": "Al menos 4 caracteres. Letras latinas, números y sólo minúsculas.", + "ens-username-invalid": "Sólo letras y números.", + "ens-username-owned": "✓ El nombre de usuario es tuyo.", + "ens-username-owned-continue": "Continuar conectará este nombre de usuario con tu clave de chat.", + "ens-username-registration-confirmation": "¡Bien! Eres el propietario de {{username}} una vez completada la transacción.", + "ens-username-taken": "Nombre de usuario ya está tomado :(", + "ens-username-you-can-follow-progress": "Puedes seguir el progreso en la sección de Historial de transacciones de tu billetera.", + "ens-usernames": "Nombres de usuario ENS", + "ens-usernames-details": "Registra un nombre de usuario universal para ser reconocido fácilmente por otros usuarios", + "ens-want-custom-domain": "Tengo un nombre en otro dominio", + "ens-want-domain": "Quiero un dominio stateofus.eth", + "ens-welcome-hints": "Los nombres ENS transforman esas locas direcciones largas en nombres de usuario únicos.", + "ens-welcome-point-customize": "Un nombre ENS puede reemplazar tu nombre aleatorio de 3 palabras en el chat. Sea @yourname en lugar de {{name}}.", + "ens-welcome-point-customize-title": "Personaliza tu nombre de chat", + "ens-welcome-point-receive": "Otros pueden enviarte fondos desde el chat en un simple paso.", + "ens-welcome-point-receive-title": "Recibe transacciones en el chat", + "ens-welcome-point-register": "Regístrate una vez para mantener el nombre para siempre. Después de 1 año, podrás liberar el nombre y recuperar tus SNT.", + "ens-welcome-point-register-title": "10 SNT a registrar", + "ens-welcome-point-simplify": "Puedes recibir fondos a tu ENS fácil de compartir, en vez de tu hash hexadecimal (0x...)", + "ens-welcome-point-simplify-title": "Simplifica tu dirección ETH", + "ens-welcome-point-verify": "Puedes verificar y agregar cualquier nombre de usuario que poseas en los siguientes pasos.", + "ens-welcome-point-verify-title": "¿Ya tienes un nombre de usuario?", + "ens-your-username": "Tu nombre de usuario", + "ens-your-usernames": "Tus nombres de usuario", + "ens-your-your-name": "Tu nombre ENS", + "enter-12-words": "Ingresa las 12 palabras de tu frase semilla, separadas por espacios", + "enter-a-private-key": "Ingresa una clave privada", + "enter-a-seed-phrase": "Ingresa una frase semilla", + "enter-address": "Ingresar dirección", + "enter-contact-code": "Ingresa el nombre de usuario ENS o la clave de chat", + "enter-pair-code": "Ingrese tu código de vinculación", + "enter-pair-code-description": "Se te mostró el código de vinculación durante la configuración de Keycard", + "enter-password": "Ingresa la contraseña", + "enter-pin": "Ingresa el código de acceso de 6 dígitos", + "enter-puk-code": "Ingresa el código PUK", + "enter-puk-code-description": "El código de acceso de 6 dígitos ha sido bloqueado. \n Ingresa el código PUK para desbloquear el código de acceso.", + "enter-recipient-address-or-username": "Ingresá la dirección o nombre de usuario del destinatario", + "enter-seed-phrase": "Ingresa la frase semilla", + "enter-url": "Ingresa una URL", + "enter-user-pk": "Ingrese la clave pública del usuario", + "enter-watch-account-address": "Escanea un código QR \n o \n ingrese la dirección para observar", + "enter-word": "Ingresar palabra", + "enter-your-code": "Ingresa tu código de acceso de 6 dígitos", + "enter-your-password": "Ingresa tu contraseña", + "error": "Error", + "error-unable-to-get-balance": "No se puede obtener el saldo", + "error-unable-to-get-prices": "Error de conversión de moneda. Refresca tu pantalla para intentarlo de nuevo.", + "error-unable-to-get-token-balance": "No se puede obtener el saldo del token", + "errors": "Errores", + "eth": "ETH", + "ethereum-account": "Cuenta Ethereum", + "ethereum-address": "Dirección de Ethereum", + "ethereum-node-started-incorrectly-description": "El nodo Ethereum se inició con una configuración incorrecta, la aplicación se detendrá para recuperarse de esa condición. ID de red configurado = {{network-id}}, actual = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "El nodo Ethereum se inició incorrectamente", + "etherscan-lookup": "Ver en Etherscan", + "everyone": "Todos", + "export-account": "Exportar la cuenta", + "export-key": "Exportar llave privada", + "external-storage-denied": "Se niega el acceso al almacenamiento externo", + "failed": "Fallido", + "faq": "Preguntas frecuentes", + "fast": "Rápido", + "favourite": "Favorito", + "favourite-description": "Tus sitios web favoritos aparecerán aquí", + "favourites": "Favoritos", + "favourites-empty": "Las direcciones agregadas a favoritos aparecerán aquí", + "fetch-messages": "↓ Recuperar mensajes", + "fetch-timeline": "↓ Obtener", + "find": "Encontrar", + "finish": "Finalizar", + "finishing-card-setup": "Finalizando la configuración de la tarjeta", + "fleet": "Fleet", + "fleet-settings": "Ajustes de fleet", + "follow": "Seguir", + "follow-your-interests": "Entra en un chat público y conoce gente nueva", + "free": "↓ Gratis", + "from": "De", + "gas-limit": "Límite de gas", + "gas-price": "Precio del gas", + "gas-used": "Gas utilizado", + "generate-a-key": "Generar claves", + "generate-a-new-account": "Genera una Cuenta", + "generate-a-new-key": "Generar una nueva clave", + "generate-account": "Generar claves", + "generate-an-account": "Genera una Cuenta", + "generate-new-key": "Generar claves", + "generating-codes-for-pairing": "> Descargando el software del producto a la tarjeta \n > Generando códigos de desbloqueo y vinculación", + "generating-keys": "Generando claves...", + "generating-mnemonic": "Generando frase semilla", + "get-a-keycard": "Obtenga una Keycard", + "get-started": "Iniciar", + "get-status-at": "Obtén Status en http:\/\/status.im\/es", + "get-stickers": "Obtén Stickers", + "give-a-short-description": "Dar una breve descripción", + "give-a-short-description-community": "Dale una breve descripción", + "give-permissions-camera": "Da permiso\npara acceder a la cámara", + "glossary": "Glosario", + "go-to-settings": "Ir a Configuración", + "got-it": "Entendido", + "grant-face-id-permissions": "Para conceder el permiso de Face ID requerido, ve a los ajustes de tu sistema y asegúrate de que esté seleccionado Status > Face ID", + "group-chat": "Chat grupal", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** se ha convertido en admin", + "group-chat-all-contacts-invited": "Todos tus contactos ya están en el grupo.", + "group-chat-created": "**{{member}}** creó el grupo **{{name}}**", + "group-chat-decline-invitation": "Rechazar invitación", + "group-chat-member-added": "**{{member}}** ha sido invitado", + "group-chat-member-joined": "**{{member}}** se ha unido al grupo", + "group-chat-member-removed": "**{{member}}** abandonó el grupo", + "group-chat-members-count": "{{selected}}\/{{max}} miembros", + "group-chat-name-changed": "**{{member}}** cambió el nombre del grupo a **{{name}}**", + "group-chat-no-contacts": "Aún no tienes contactos. \nInvita a tus amigos a comenzar a chatear", + "group-info": "Información del grupo", + "group-invite": "Invitación de grupo", + "group-invite-link": "Enlace de invitación de grupo", + "group-membership-request": "Solicitud de membresía grupal", + "gwei": "Gwei", + "has-permissions": "tiene permiso para acceder", + "hash": "Hash", + "help": "ayuda", + "help-capitalized": "Ayuda", + "help-center": "Centro de ayuda", + "hide-content-when-switching-apps": "Bloquear capturas de pantalla", + "hide-content-when-switching-apps-ios": "Ocultar vista previa", + "history": "Historial", + "history-nodes": "Nodos de Status", + "hold-card": "sostén la tarjeta en la parte posterior de tu teléfono", + "home": "Inicio", + "hooks": "Hooks", + "how-it-works": "Cómo funciona", + "http-gateway-error": "¡Oops, la solicitud falló!", + "identifier": "Identificador", + "image": "Imagen", + "image-remove-current": "Eliminar la foto actual", + "image-source-gallery": "Seleccionar de la galería", + "image-source-make-photo": "Tomar foto", + "image-source-title": "Editar imagen", + "import": "Importar", + "import-community": "Importar una comunidad", + "import-community-title": "Importar una comunidad", + "in-contacts": "En contactos", + "incoming": "Entrante", + "incoming-transaction": "Transacción entrante", + "incorrect-code": [ + "str", + "Lo sentimos, el código es incorrecto, escríbelo de nuevo" + ], + "increase-gas": "Aumentar el gas", + "initialization": "Inicialización", + "install": "↓ Instalar", + "intro-message1": "¡Bienvenid@ a Status!\nToca este mensaje para configurar tu contraseña y empezar.", + "intro-privacy-policy-note1": "Status no recopila ni se beneficia de tus datos personales. Al continuar, tú estás de acuerdo con la ", + "intro-privacy-policy-note2": "política de privacidad", + "intro-text": "Status es tu puerta de enlace a la web descentralizada", + "intro-text1": "Chatea a través de una red encriptada peer-to-peer donde los mensajes no pueden ser censurados o hackeados", + "intro-text2": "Envía y recibe activos digitales en cualquier parte del mundo – no se requiere cuenta bancaria", + "intro-text3": "Explora juegos, exchanges y redes sociales en las que solo tú eres propietario de tus datos", + "intro-title1": "Comunicación verdaderamente privada", + "intro-title2": "Cripto-billetera segura", + "intro-title3": "Apps descentralizadas", + "intro-wizard-text1": "Un set de claves controla tu cuenta. Tus claves viven en tu teléfono, por lo que solo tú puede usarlas.", + "intro-wizard-text2": "Una clave es para chatear. Viene con un nombre legible que no se puede cambiar.", + "intro-wizard-text3": "¿Tienes una Keycard? Guarda tus claves en ella; la necesitarás para las transacciones", + "intro-wizard-text4": "Asegura y encripta tus claves", + "intro-wizard-text6": "Status te notificará sobre los nuevos mensajes. Puedes editar tus preferencias de notificación más adelante en la configuración", + "intro-wizard-title-alt4": "Crea una contraseña", + "intro-wizard-title-alt5": "Confirma tu contraseña", + "intro-wizard-title1": "Obtén tus claves", + "intro-wizard-title2": "Elige un nombre de chat", + "intro-wizard-title3": "Elegir almacenamiento de claves", + "intro-wizard-title4": "Crear un código de acceso de 6 dígitos", + "intro-wizard-title5": "Confirma la clave de acceso", + "intro-wizard-title6": "Habilitar las notificaciones", + "introduce-yourself": "Preséntate con un breve mensaje", + "invalid-address-qr-code": "El código QR escaneado no contiene una dirección válida", + "invalid-format": "Formato inválido \n Debe ser {{format}}", + "invalid-key-confirm": "Aplicar", + "invalid-key-content": "La base de datos no se puede encriptar porque el archivo está dañado. Tus fondos y llaves de chat están a salvo. Otros datos, como chats y contactos, no se podrán restaurar. Presionar el botón {{erase-multiaccounts-data-button-text}}\" eliminará otros datos y te permitirá acceder a tus fondos y enviar mensajes", + "invalid-number": "Número inválido", + "invalid-pairing-password": "Contraseña de vinculación inválida", + "invalid-public-chat-topic": "Tema de chat público no válido", + "invalid-range": "Formato inválido, debe estar entre {{min}} y {{max}}", + "invite": "Invitá", + "invite-button": "Invitá", + "invite-chat-accept": "Aceptar", + "invite-chat-accept-join": "Aceptar y Unirse", + "invite-chat-intro": "Fuiste referido por un amigo para unirte a Status. Acá tenés algunas criptomonedas para empezar! Usalas para registrar un nombre ENS o comprar un paquete de stickers", + "invite-chat-name": "Referencia de amigos", + "invite-chat-pending": "Pendiente", + "invite-chat-rule": "Si aceptás también recompensarás a tu amigo con un bono de referencia criptográfica", + "invite-chat-starter-pack": "Paquete de Bienvenida", + "invite-friends": "Invitar a amigos", + "invite-instruction-fifth": "Puede elegir canjear su bono de referencia en cualquier momento.", + "invite-instruction-first": "Envías un enlace de invitación único a tu amigo para descargar y unirse a Status", + "invite-instruction-fourth": "Vos recibís tu bono de referencia y tu amigo el Paquete de Bienvenida", + "invite-instruction-second": "Tu amigo descarga Status y crea una cuenta (en Android)", + "invite-instruction-third": "Se inicia un chat con tu amigo, donde confirman tu referencia", + "invite-people": "Invitar a personas", + "invite-privacy-policy-public": "Instaló Status a través de un enlace de referencia. Al unirte a este chat atribuyes a tu referente y aceptas el", + "invite-privacy-policy1": "Al aceptar, estás de acuerdo con el programa de referidos", + "invite-privacy-policy2": "Términos y Condiciones.", + "invite-public-chat-home": "Invitación de referencia", + "invite-public-chat-intro": "¡Aquí tienes algunas criptomonedas para que comiences! Úsalo para registrar un nombre ENS o comprar un paquete de stickers", + "invite-receive-account": "Cuenta para recibir tu bono de referencia", + "invite-reward": "¡Gana criptomonedas por cada amigo que invites!", + "invite-reward-friend": "Amigo:", + "invite-reward-friend-description": "Tu amigo recibirá un Paquete de Bienvenida que consta de {{reward}} para comenzar", + "invite-reward-friend-name": "Paquete de Bienvenida", + "invite-reward-you": "Vos:", + "invite-reward-you-description": "Invitá a un amigo y recibí {{reward}} como bono de referencia. Usalo para obtener stickers, un nombre ENS y probar dapps", + "invite-reward-you-name": "Bono de referencia", + "invite-select-account": "Seleccioná una cuenta para recibir tu bono de referencia", + "invite-warning": "Esta promoción solo es válida para usuarios de un dispositivo Android que no sean residentes de EE. UU. Un amigo debe confirmar la referencia en un plazo de 7 días.", + "invited": "invitado", + "join": "Unite", + "join-a-community": "o únete a una comunidad", + "join-group-chat": "Unirse al grupo", + "join-group-chat-description": "{{username}} te invitó a unirte al grupo {{group-name}}", + "join-me": "Ey, unite a Status: {{url}}", + "join-new-private-chat": "Iniciar un nuevo chat privado", + "join-new-public-chat": "Unirse a un chat público", + "joined": "Unido", + "joined-group-chat-description": "Te has unido a {{group-name}} desde la invitación de {{username}}", + "key": "Clave", + "key-managment": "Administración de claves", + "key-on-device": "La clave privada está guardada en este dispositivo", + "keycard": "Keycard", + "keycard-access-reset": "Acceso a la Keycard está restablecido", + "keycard-applet-install-instructions": "Para instalar el applet, sigue las instrucciones en https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Intente mover la tarjeta para encontrar el lector NFC en su dispositivo", + "keycard-awaiting-title": "Todavía buscando...", + "keycard-backup": "Crear una copia de seguridad de la Keycard", + "keycard-backup-success-body": "Tarjeta de respaldo creada con éxito. Ahora podes usarla con tu cuenta al igual que la tarjeta principal.", + "keycard-backup-success-title": "Copia de seguridad exitosa", + "keycard-blocked": "Keycard ha sido bloqueada. \n Necesitas reiniciar la tarjeta para continuar usándola.", + "keycard-can-use-with-new-passcode": "Puedes usar esta tarjeta con tu nuevo código de accesto", + "keycard-cancel-setup-text": "Esto cancelará la configuración de keycard. Se muy recomendable finalizar la configuración para usar keycard. ¿De verdad quieres cancelar?", + "keycard-cancel-setup-title": "Operación peligrosa", + "keycard-connected-description": "Intenta mantener la tarjeta quieta", + "keycard-connected-title": "Conectado", + "keycard-desc": "¿Tienes una Keycard? Guarda tus claves allí; la necesitarás para las transacciones", + "keycard-dont-ask-card": "No pedir tarjeta para iniciar sesión", + "keycard-enter-new-passcode": "Ingresa el nuevo código de acceso {{step}}\/2", + "keycard-error-description": "Vuelva a conectar la tarjeta para continuar.", + "keycard-error-title": "Conexión perdida", + "keycard-free-pairing-slots": "Keycard tiene {{n}} espacios de vinculación libres", + "keycard-has-multiaccount-on-it": "Esta tarjeta está llena. Cada tarjeta puede mantener un par de llaves principales", + "keycard-init-description": "Coloque la tarjeta en la parte posterior de su teléfono para continuar", + "keycard-init-title": "Buscando tarjetas...", + "keycard-is-blocked-details": "Ya no puedes usar esta tarjeta para acceder o firmar esta cuenta. Ha habido demasiados intentos fallidos de código de acceso y PUK.", + "keycard-is-blocked-instructions": "Para acceder a tu cuenta, re-instala Status y usa una nueva Keycard, usa una billetera diferente o reinicia Keycard manualmente.", + "keycard-is-blocked-title": "La Keycard está bloqueada", + "keycard-is-frozen-details": "Para proteger tus activos, tu tarjeta está congelada. Restablece el acceso a la tarjeta para desbloquear llaves y enviar transacciones. Crea un nuevo código de acceso e ingrese su PUK para acceder a su(s) cuenta(s) en esta tarjeta", + "keycard-is-frozen-reset": "Restablece el acceso a la tarjeta", + "keycard-is-frozen-title": "La Keycard está congelada", + "keycard-onboarding-finishing-header": "Terminando", + "keycard-onboarding-intro-header": "Guarda tus claves en Keycard", + "keycard-onboarding-intro-text": "Prepárate, esto puede tomar unos minutos, pero es importante para asegurar tu cuenta", + "keycard-onboarding-pairing-header": "Vinculando la tarjeta...", + "keycard-onboarding-preparing-header": "Preparando la tarjeta...", + "keycard-onboarding-puk-code-header": "Escribe los códigos\n y guárdalos de forma segura", + "keycard-onboarding-recovery-phrase-description": "Necesitas esta frase semilla para obtener de nuevo tu clave. Anótala. Manténla segura, fuera de línea y separada de este dispositivo.", + "keycard-onboarding-recovery-phrase-header": "Respalda tu frase semilla", + "keycard-onboarding-recovery-phrase-text": "Sólo para tus ojos. Esta es la semilla mágica usada para generar tu clave.", + "keycard-onboarding-start-header": "Manten la tarjeta en la parte posterior\n de tu teléfono para iniciar", + "keycard-onboarding-start-step1": "Crea un código de acceso", + "keycard-onboarding-start-step1-text": "Alrededor de 1 minuto. Crea un código de acceso de 6 dígitos para encriptar tus claves", + "keycard-onboarding-start-step2": "Escribe el código de vinculación y PUK", + "keycard-onboarding-start-step2-text": "Alrededor de 1 minuto. Vas a necesitar un papel y un lápiz para eso", + "keycard-onboarding-start-step3": "Respaldar la frase semilla", + "keycard-onboarding-start-step3-text": "Alrededor de 1 minuto. También son necesarios un pedazo de papel y un lápiz.", + "keycard-onboarding-start-text": "Y manten el contacto entre la tarjeta y el teléfono\n durante la configuración. La configuración tomará alrededor de 4 minutos", + "keycard-processing-description": "Intenta mantener la tarjeta quieta", + "keycard-processing-title": "Procesando...", + "keycard-recovery-intro-button-text": "Iniciar la recuperación", + "keycard-recovery-intro-header": "Recuperar claves almacenadas en Keycard", + "keycard-recovery-intro-text": "Si antes generaste claves con keycard y ahora deseas utilizarlas en este dispositivo", + "keycard-recovery-no-key-header": "No hay nada que \nrecuperar aquí", + "keycard-recovery-no-key-text": "Tu Keycard no tiene ninguna clave almacenada en ella. Para usarla, genera una nueva clave y elige tu Keycard para almacenar la clave", + "keycard-recovery-phrase-confirm-header": "Confirmar frase semilla", + "keycard-recovery-phrase-confirmation-text": "¡No tendrás una segunda oportunidad! Si pierdes acceso, por ejemplo al extraviar tu Keycard, solo podrás acceder a tus llaves con tu frase semilla. Nadie a parte de ti tiene tu frase semilla. Anótala. Mantenla guardada.", + "keycard-recovery-phrase-confirmation-title": "¿Escribiste la frase semilla?", + "keycard-recovery-success-header": "Tus claves han sido \n recuperadas con éxito", + "keycard-redeem-title": "Canjear a", + "keycard-redeem-tx": "Canjea activos", + "keycard-redeem-tx-desc": "Toque con la tarjeta para firmar y recibir activos", + "keycard-reset-passcode": "Restablece el código de acceso", + "keycard-success-description": "Puedes quitar la tarjeta ahora", + "keycard-success-title": "Éxito", + "keycard-unauthorized-operation": "No estás autorizado a realizar esta operación.\n Por favor, toca la tarjeta válida e inténtalo de nuevo.", + "keycard-upsell-subtitle": "Mayor seguridad y conveniencia", + "language": "Idioma", + "learn-more": "Aprende más", + "learn-more-about-keycard": "Más información sobre Keycard", + "leave": "Salir", + "leave-chat": "Abandonar chat", + "leave-chat-confirmation": "¿Estás seguro de que quieres abandonar el chat?", + "leave-community": "Salir de la comunidad", + "leave-confirmation": "Abandonar chat", + "leave-group": "Abandonar grupo", + "left": "salió", + "les-ulc": "LES\/ULC", + "lets-go": "Vamos", + "light": "Luz", + "linked-on": "Vinculado en {{date}}", + "load-messages-before": "antes del {{date}}", + "load-more-messages": "↓ Recuperar más mensajes", + "load-more-timeline": "↓ Obtener más", + "loading": "Cargando...", + "local-notifications": "Notificaciones locales", + "lock-app-with": "Bloquear app con", + "log-level": "Nivel de registro", + "log-level-settings": "Ajustes del nivel de registro", + "logging": "Registro", + "logging-enabled": "¿Registro habilitado?", + "login-pin-description": "Ingresa tu código de acceso de 6 dígitos para desbloquear tus llaves", + "logout": "Cerrar sesión", + "logout-app-content": "Se cerrará la sesión de la cuenta. Cuando lo desbloquees de nuevo, se utilizará la red seleccionada", + "logout-are-you-sure": "¿Seguro que quieres cerrar la sesión?", + "logout-key-management": "Debe cerrar la sesión para acceder a la administración de claves.", + "logout-title": "¿Cerrar sesión?", + "looking-for-cards": "Buscando tarjetas...", + "lost-connection": "Conexión perdida", + "low-tip": "la propina es demasiado baja", + "mail-should-be-configured": "El cliente de correo debe ser configurado", + "mailserver-address": "Dirección del nodo de Status", + "mailserver-automatic": "Selección automática", + "mailserver-automatic-switch-explanation": "Elija el nodo de Status más rápido disponible", + "mailserver-connection-error": "No se pudo conectar al nodo de Status", + "mailserver-content": "Un nodo en la red de Status que enruta y almacena mensajes, hasta por 30 días.", + "mailserver-details": "Detalles del nodo de Status", + "mailserver-error-content": "No se pudo acceder al nodo de Status que seleccionó.", + "mailserver-error-title": "Error al conectarse al nodo de Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Elija otro nodo de Status", + "mailserver-reconnect": "No se pudo conectar con el nodo de Status. Pulsá para reconectar", + "mailserver-request-error-content": "El siguiente error fue devuelto por el nodo de Status: {{error}}", + "mailserver-request-error-status": "Se produjo un error al recuperar el historial, verifica los registros para obtener más detalles", + "mailserver-request-error-title": "Error de solicitud del nodo de Status", + "mailserver-request-retry": "Reintentar solicitud", + "mailserver-retry": "Reintentar", + "mailserver-title": "Nodo de Status", + "main-currency": "Moneda principal", + "main-networks": "Redes principales", + "main-wallet": "Billetera Principal", + "mainnet-network": "Red principal", + "make-admin": "Hacer admin", + "manage-keys-and-storage": "Administrar las llaves y el almacenamiento", + "mark-all-read": "Marcar todo como leído", + "master-account": "Cuenta maestra", + "maximum-fee": "Tarifa máxima", + "maybe-later": "Quizas mas tarde", + "member-ban": "Prohibir miembro", + "member-kick": "Eliminar miembro", + "members": { + "one": "1 miembro", + "other": "{{count}} miembros" + }, + "members-active": { + "one": "1 miembro", + "other": "{{count}} miembros" + }, + "members-active-none": "no hay miembros", + "members-count": "{{count}} miembros", + "members-label": "Miembros", + "members-limit-reached": "Límite de miembros alcanzado", + "members-title": "Miembros", + "membership-approval": "Requiere aprobación", + "membership-approval-description": "Puedes unirte de manera gratuita, pero los nuevos miembros necesitan ser aprobados primero por el creador de la comunidad", + "membership-button": "Requisito de membresía", + "membership-declined": "Se rechazó la solicitud de membresía", + "membership-description": "La membresía grupal requiere que seas aceptado por el administrador del grupo", + "membership-ens": "Requiere un usuario ENS", + "membership-ens-description": "Tu comunidad requiere que tengas un usuario ENS para unirte", + "membership-free": "Sin requisitos", + "membership-free-description": "Cualquier persona puede unirse a tu comunidad", + "membership-invite": "Necesitas invitación de otro miembro", + "membership-invite-description": "Únicamente puedes unirte a la comunidad mediante invitación de un miembro existente", + "membership-none": "Ninguno", + "membership-none-placeholder": "Podes exigir que los nuevos miembros cumplan con ciertos criterios antes de poder unirse. Esto se puede cambiar en cualquier momento", + "membership-request-pending": "Solicitud de membresía pendiente", + "membership-requests": "Solicitudes de membresía", + "membership-title": "Requisito de membresía", + "message": "Mensaje", + "message-not-sent": "Mensaje no enviado", + "message-options-cancel": "Cancelar", + "message-reply": "Responder", + "messages": "Mensajes", + "might-break": "Puede que se rompan algunas ÐApps", + "migrations-failed-content": "{{message}} \n versión del esquema: inicial {{initial-version}}, actual {{current-version}} , último {{last-version}} \n\nOcurrió un problema de base de datos. Tus fondos y la llave de chat están a salvo. Otros datos, como chats y contactos, no se podrán recuperar. Presionar el botón {{erase-multiaccounts-data-button-text}}\" eliminará el resto de datos y te permitirá acceder a tus fondos y enviar mensajes.", + "mobile-network-ask-me": "Pregúntarme cuando esté en la red móvil", + "mobile-network-continue-syncing": "Continuar sincronizando", + "mobile-network-continue-syncing-details": "Puedes cambiar esto más adelante en los ajustes", + "mobile-network-go-to-settings": "Ir a ajustes", + "mobile-network-settings": "Datos móviles", + "mobile-network-sheet-configure": "Puedes configurar la sincronización en más \ndetalle en", + "mobile-network-sheet-offline": "Sin Wi-Fi, la sincronización de mensajes está deshabilitada.", + "mobile-network-sheet-offline-details": "La sincronización mediante la red móvil está desactivada", + "mobile-network-sheet-remember-choice": "Recuerda mi elección", + "mobile-network-sheet-settings": "ajustes", + "mobile-network-start-syncing": "Iniciar la sincronización", + "mobile-network-stop-syncing": "Detener la sincronización", + "mobile-network-stop-syncing-details": "¿Hasta que se conecta a Wi-Fi?", + "mobile-network-use-mobile": "Usar datos móviles", + "mobile-network-use-mobile-data": "Status utiliza muchos datos al sincronizar los chats y la billetera.", + "mobile-network-use-wifi": "Sólo Wi-Fi", + "mobile-syncing-sheet-details": "Status utiliza muchos datos al sincronizar los chats y la billetera.", + "mobile-syncing-sheet-title": "¿Sincronizar usando los datos del móvil?", + "more": "más", + "move-and-reset": "Mover y Restablecer", + "move-keystore-file": "Mover el archivo keystore", + "move-keystore-file-to-keycard": "¿Mover el archivo keystore a la keycard?", + "multiaccount-exists-content": "Las llaves para esta cuenta ya existen y no se pueden añadir de nuevo. Si perdiste tu contraseña, código de acceso o Keycard, desinstala la app, reinstálala y accede insertando tu frase semilla", + "multiaccount-exists-title": "Las llaves para esta cuenta ya existen", + "multiaccounts-recover-enter-phrase-text": "Ingresa 12, 15, 18, 21 o 24 palabras. \n Separa las palabras por un solo espacio.", + "multiaccounts-recover-enter-phrase-title": "Ingresa tu frase semilla", + "mute": "Silenciar", + "my-accounts": "Mis cuentas", + "my-accounts-empty": "Tus cuentas disponibles aparecerán aquí", + "my-status": "Mi estado", + "name": "Nombre", + "name-of-token": "El nombre de tu token", + "name-optional": "Nombre (opcional)", + "name-your-channel": "Nombra tu canal", + "name-your-channel-placeholder": "Nombre del canal", + "name-your-community": "Nombra tu comunidad", + "name-your-community-placeholder": "Un nombre pegadizo", + "need-help": "¿Necesitas ayuda?", + "network": "Red", + "network-chain": "Cadena de la red", + "network-details": "Detalles de red", + "network-fee": "Tarifa de la red", + "network-id": "ID de red", + "network-info": "Información de la red", + "network-invalid-network-id": "El id de red especificado no corresponde al id de red por la url de RPC", + "network-invalid-status-code": "Código de estado inválido: {{code}}", + "network-invalid-url": "La URL de la red no es válida", + "network-settings": "Ajustes de la red", + "new": "Nuevo", + "new-chat": "Nuevo chat", + "new-community-title": "Nueva comunidad", + "new-contact": "Nuevo contacto", + "new-contract": "Nuevo contrato", + "new-favourite": "Nuevo favorito", + "new-group": "Nuevo grupo", + "new-group-chat": "Nuevo grupo de chat", + "new-network": "Nueva red", + "new-pin-description": "Ingresa el nuevo código de acceso de 6 dígitos", + "new-public-group-chat": "Unirte a chat público", + "new-status": "Nuevo estado", + "new-tab": "Nueva pestaña", + "next": "Siguiente", + "nickname": "Apodo", + "nickname-description": "Los apodos te ayudan a identificar a otras personas en Status. \n Solo tú puedes ver los apodos que agregaste", + "no": "No", + "no-collectibles": "No hay coleccionables disponibles", + "no-contacts": "Aún no hay contactos", + "no-keycard-applet-on-card": "No hay applet de Keycard en la tarjeta", + "no-messages": "No hay mensajes", + "no-pairing-slots-available": "Esta tarjeta ya está vinculada con 5 dispositivos y no puede ser vinculada con éste dispositivo. Por favor, utiliza uno de los dispositivos vinculados, inicia sesión con esta tarjeta y libera los espacios de vinculación en la tarjeta", + "no-result": "Sin resultados", + "no-tokens-found": "No se encontraron tokens", + "node-info": "Información del nodo", + "node-version": "Versión de nodo", + "nodes-disabled": "Nodos de Status deshabilitados", + "non-archival-node": "El punto de conexión de RPC no admite solicitudes de archivado. Es posible que el historial de transferencias locales esté incompleto.", + "nonce": "Nonce", + "none": "Ninguno", + "not-applicable": "No aplicable para transacciones no firmadas", + "not-connected-nodes": "Sin conexión a un nodo de Status", + "not-connected-to-peers": "Sin conexión a ningún nodo", + "not-enough-snt": "No hay suficiente SNT", + "not-keycard-text": "La tarjeta que usaste no es una Keycard. Necesitas comprar una Keycard para usarla", + "not-keycard-title": "No es una Keycard", + "not-registered": "no registrado", + "notification-settings": "Notificaciones", + "notifications": "Notificaciones", + "notifications-non-contacts": "Notificaciones de no contactos", + "notifications-preferences": "Preferencias de notificación", + "notifications-servers": "Servidores de notificaciones", + "notifications-switch": "Mostrar notificaciones", + "notifications-transactions": "Transacciones de billetera", + "notify": "Notificar", + "now": "Ahora", + "off": "Apagado", + "off-status-tree": "Derivado fuera de Status", + "offline": "Desconectado", + "offline-messaging-use-history-explanation": "Habilite los nodos de Status para recuperar los mensajes que se enviaron mientras la aplicación estaba cerrada. Cuando está habilitado, un nodo de Status obtiene su dirección IP. Cuando está deshabilitado, no recibirá mensajes cuando la aplicación esté cerrada y no los verá cuando abra la aplicación más tarde.", + "offline-messaging-use-history-nodes": "Usar nodos de Status", + "ok": "OK", + "ok-continue": "Ok, continuar", + "ok-got-it": "Ok, lo tengo", + "ok-save-pass": "OK, guardar contraseña", + "okay": "Bien", + "on": "En", + "on-status-tree": "En el árbol de Status", + "once-enabled-share-metadata": "Una vez habilitados, los enlaces publicados en el chat pueden compartir sus metadatos con el sitio.", + "open": "Abrir", + "open-chat": "Abrir chat", + "open-dapp": "Abrir ÐApp", + "open-dapp-store": "Descubre ÐApps", + "open-home": "Abrir...", + "open-in-new-tab": "Abrir en una pestaña nueva", + "open-membership": "Abrir membresía", + "open-nfc-settings": "Abrir ajustes de NFC", + "opening-buy-crypto": "Abriendo {{site}}...", + "optimal": "Óptimo", + "optional": "opcional", + "or": "O", + "outgoing": "Saliente", + "outgoing-transaction": "Transacción saliente", + "page-camera-request-blocked": "solicitudes de cámara bloqueadas. Para habilitar las solicitudes de la cámara, vaya a Configuración", + "page-would-like-to-use-camera": "quisiera usar su cámara", + "pair": "Vincular dispositivos", + "pair-card": "Vincular a este dispositivo", + "pair-code": "Código par", + "pair-code-explanation": "Vincula la tarjeta a un dispositivo diferente para desbloquear llaves y firmar transacciones con la misma Keycard", + "pair-code-placeholder": "Código de emparejamiento...", + "pair-this-card": "Vincula esta tarjeta", + "pair-this-device": "Publicar el dispositivo", + "pair-this-device-description": "Vincula tus dispositivos para sincronizar los contactos y chats entre ellos", + "paired-devices": "Dispositivos vinculados", + "pairing": "Vinculando", + "pairing-card": "Vinculando la tarjeta", + "pairing-go-to-installation": "Ir a los ajustes de vinculación", + "pairing-maximum-number-reached-content": "Por favor, deshabilita uno de tus dispositivos antes de habilitar uno nuevo.", + "pairing-maximum-number-reached-title": "Número máximo de dispositivos alcanzados", + "pairing-new-installation-detected-content": "Se ha detectado un nuevo dispositivo. \nPara usar tus dispositivos correctamente, es importante vincularlos y habilitarlos antes de usarlos. \nVe a la sección del dispositivo en los ajustes para vincular tus dispositivos.", + "pairing-new-installation-detected-title": "Nuevo dispositivo detectado", + "pairing-no-info": "Sin información", + "pairing-please-set-a-name": "Por favor configura un nombre para tu dispositivo.", + "passphrase": "Frase de contraseña", + "password": "Contraseña", + "password-description": "Al menos 6 caracteres. Tu contraseña protege tus claves. La necesitas para desbloquear Status y realizar transacciones.", + "password-placeholder": "Contraseña...", + "password-placeholder2": "Confirma tu contraseña", + "password_error1": "Las contraseñas no coinciden.", + "paste": "Pegar", + "paste-json": "Pegar JSON", + "pay-to-chat": "Pagar para chatear", + "peer-content": "Un dispositivo conectado a la red de chat de Status. Cada usuario puede representar uno o más peers, dependiendo de su número de dispositivos.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Pares", + "pending": "Pendiente", + "pending-confirmation": "Confirmación pendiente...", + "pending-invitations": "Solicitudes de membresía pendientes", + "permissions": "Permisos", + "phone-e164": "Internacional 1", + "photos": "Fotos", + "photos-access-error": "Para permitir el acceso a fotos, porfavor, ve a ajustes de sistema y asegúrate que este seleccionada la opción Status > Fotos", + "pin-changed": "Se ha cambiado el código de acceso de 6 dígitos", + "pin-code": "Código de acceso de 6 dígitos", + "pin-mismatch": "Código de acceso incorrecto", + "pin-one-attempt": "un intento", + "pin-one-attempt-blocked-after": "antes de que tu Keycard se bloquee", + "pin-one-attempt-blocked-before": "Ten cuidado, solo tienes", + "pin-one-attempt-frozen-after": "antes de que tu Keycard se congele", + "pin-one-attempt-frozen-before": "Ten cuidado, solo tienes", + "pin-retries-left": "Te quedan {{number}} reintentos", + "preference": "Preferencia", + "preview-privacy": "Vista previa del modo de privacidad", + "previewing-may-share-metadata": "La vista previa de los enlaces de estos sitios web puede compartir sus metadatos con sus propietarios.", + "privacy": "Privacidad", + "privacy-and-security": "Privacidad y seguridad", + "privacy-policy": "Política de privacidad", + "private-key": "Llave privada", + "private-notifications": "Notificaciones privadas", + "private-notifications-descr": "Status te notificará sobre los nuevos mensajes. Podés editar tus preferencias de notificación más adelante en la configuración.", + "processing": "Un momento.", + "product-information": "Información del Producto", + "profile": "Perfil", + "profile-deleted-content": "Su perfil fue eliminado con éxito", + "profile-deleted-keycard": "Ahora puede restaurar otro par de llaves en tu Keycard", + "profile-deleted-title": "Perfil eliminado", + "profile-details": "Detalles del perfil", + "profile-pic-pick": "Seleccionar de la galería", + "profile-pic-remove": "Quitar foto", + "profile-pic-take": "Tomar foto", + "public-chat": "Chat público", + "public-chat-description": "¡Únete a chats públicos para tus intereses! Cualquiera puede comenzar uno nuevo.", + "public-chats": "Chats públicos", + "public-group-status": "Público", + "public-group-topic": "Tema", + "public-key": "Clave pública", + "puk-and-pairing-codes-displayed": "PUK y códigos de vinculación mostrados", + "puk-code": "Código PUK", + "puk-code-explanation": "Si olvidas tu código de acceso de 6 dígitos o lo ingresas incorrectamente 3 veces, necesitarás este código para desbloquear tu tarjeta.", + "puk-mismatch": "El código PUK no coincide", + "push-failed-transaction": "Su transacción falló", + "push-failed-transaction-body": "{{value}} {{currency}} a {{to}}", + "push-inbound-transaction": "Recibiste {{value}} {{currency}}", + "push-inbound-transaction-body": "De {{from}} a {{to}}", + "push-notifications-server-enabled": "Servidor habilitado", + "push-notifications-servers": "Servidores de notificaciones push", + "push-outbound-transaction": "Enviaste {{value}} {{currency}}", + "push-outbound-transaction-body": "De {{from}} a {{to}}", + "quiet-days": "{{quiet-days}} días", + "quiet-hours": "{{quiet-hours}} horas", + "re-encrypt-key": "Vuelve a encriptar tus claves", + "receive": "Recibir", + "receive-transaction": "Recibir transacción", + "recent": "Estados recientes", + "recent-empty": "Las direcciones utilizadas recientemente aparecerán aquí", + "recent-recipients": "Contactos", + "recently-used-stickers": "Las stickers usadas recientemente aparecerán aquí", + "recipient": "Receptor", + "recipient-code": "Ingresa la dirección del destinatario", + "recipient-code-placeholder": "0x... o username.domain.eth", + "recover": "Recuperar", + "recover-key": "Ingresar claves existentes", + "recover-keycard-multiaccount-not-supported": "Las llaves para esta cuenta ya existen y no se pueden añadir de nuevo. Si perdiste tu contraseña, código de acceso o Keycard, desinstala la app, reinstálala y accede insertando tu frase semilla", + "recover-with-keycard": "Recuperar con Keycard", + "recover-with-seed-phrase": "Recuperar con frase semilla", + "recovering-key": "Ingresando las claves...", + "recovery-confirm-phrase": "Confirmar frase semilla", + "recovery-phrase": "Frase semilla", + "recovery-success-text": "Deberás crear un nuevo código o contraseña para volver a encriptar tus claves", + "recovery-typo-dialog-description": "Recuerda, tu frase semilla deben ser exactamente las mismas palabras y el orden que recibiste", + "recovery-typo-dialog-title": "¿Es correcta la frase semilla?", + "redeem-amount": "{{quantity}} bonos disponibles", + "redeem-now": "Canjear ahora", + "redeem-success": "¡Canje del bono exitoso!", + "refresh": "Actualizar", + "registered": "registrado", + "remember-me": "Recuérdame", + "remind-me-later": "Muéstrame esto de nuevo", + "remove": "Remover", + "remove-favourite": "Quitar favorito", + "remove-from-chat": "Eliminar del chat", + "remove-from-contacts": "Eliminar de contactos", + "remove-from-contacts-text": "Al eliminar a un usuario de tu lista de contactos, no oculta la dirección de tu billetera de ellos", + "remove-group": "Eliminar grupo", + "remove-network": "Eliminar red", + "remove-token": "Eliminar token", + "removed": "eliminado", + "repeat-pin": "Repite el nuevo código de acceso de 6 dígitos", + "report-bug-email-template": "1. Descripción del problema \n (Describe la función que deseas, o resume brevemente el error y lo que hiciste, lo que esperabas que sucediera y lo que realmente sucede. Secciones a continuación) \n\n\n 2. Pasos para reproducir \n (Describe cómo podemos replicar el error paso a paso). \n -Abre Status \n -... \n -Paso 3, etc. \n\n\n 3. Comportamiento esperado \n (Describe lo que esperabas que sucediera). \n\n\n 4. Comportamiento real \n (Describe lo que realmente sucedió). \n\n\n 5. Adjunta capturas de pantalla que puedan demostrar el problema, por favor \n", + "request-access": "Solicitar acceso", + "request-feature": "Solicitar una funcionalidad", + "request-membership": "Solicitar membresía", + "request-pending": "Pedido pendiente…", + "request-transaction": "Solicitar transacción", + "required-field": "Campo requerido", + "resend-message": "Reenviar", + "reset-card": "Reiniciar la tarjeta", + "reset-card-description": "Esta operación restablecerá la tarjeta al estado inicial. Borrará todos los datos de la tarjeta, incluidas las claves privadas. La operación no es reversible.", + "reset-database": "Restablecer base de datos", + "reset-database-warning": "Elimina chats, contactos y configuraciones. Se requiere cuando has perdido tu contraseña", + "reset-database-warning-keycard": "Borrar chats, contactos y configuraciones.", + "restore-defaults": "Restaurar los Valores Predeterminados", + "retry": "Reintentar", + "revoke-access": "Revocar el acceso", + "rpc-url": "RPC URL", + "rpc-usage-copy": "Copiar", + "rpc-usage-filter": "Métodos de filtrado", + "rpc-usage-get-stats": "Actualizar", + "rpc-usage-info": "Estadísticas de uso de RPC", + "rpc-usage-reset": "Restablecer", + "save": "Guardar", + "save-password": "Guardar contraseña", + "save-password-unavailable": "Establece el código de acceso del dispositivo para guardar la contraseña", + "save-password-unavailable-android": "Guardar la contraseña no está disponible: tu dispositivo puede estar rooteado o carecer de las características de seguridad necesarias.", + "scan-qr": "Escanear QR", + "scan-qr-code": "Escanea un código QR con una dirección de billetera", + "scan-tokens": "Escanear tokens", + "search": "Buscar", + "search-no-chat-found": "No hay resultados de búsqueda. ¿Querés decir", + "secret-keys-confirmation-text": "Los necesitará para seguir usando su Keycard en caso de que alguna vez pierda su teléfono.", + "secret-keys-confirmation-title": "¿Escribiste los códigos?", + "security": "Seguridad", + "see-details": "Ver detalles", + "see-it-again": "VERLO OTRA VEZ", + "see-suggestions": "Ver sugerencias", + "seed-key-uid-mismatch": "La frase semilla no coincide", + "seed-key-uid-mismatch-desc-1": "La frase semilla que ingresó no coincide con {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Para administrar claves para esta cuenta verifique su frase semilla e inténtelo de nuevo.", + "seed-phrase-content": "Un set de palabras fáciles de leer, seleccionadas al azar de la lista estándar BIP39 y utilizadas para recuperar o acceder a tu cuenta de Ethereum en otras billeteras y dispositivos. También se conoce como \"frase mnemónica\", \"frase de recuperación\" o \"copia de seguridad de billetera\" en todo el cripto ecosistema. La mayoría de las cripto apps usan este mismo estándar para generar cuentas.", + "seed-phrase-placeholder": "Frase semilla...", + "seed-phrase-title": "Frase Semilla", + "select": "Seleccionar", + "select-account": "Seleccionar cuenta", + "select-account-dapp": "Selecciona la cuenta que deseas usar con Dapps", + "select-account-first": "Selecciona una cuenta primero", + "select-chat": "Selecciona el chat para comenzar a enviar mensajes", + "select-new-location-for-keys": "Seleccione una nueva ubicación para guardar sus claves privadas", + "selected": "Seleccionado", + "send-logs": "Reportar un error", + "send-logs-to": "Reportar un error a {{email}}", + "send-message": "Enviar mensaje", + "send-push-notifications-description": "Cuando está deshabilitado, la persona que recibe sus mensajes no será notificada de su llegada.", + "send-request": "Enviar solicitud", + "send-request-amount": "Cantidad", + "send-request-amount-max-decimals": "El número máximo de decimales es {{asset-decimals}}", + "send-request-unknown-token": "Token desconocido - {{asset}}", + "send-sending-to": "para {{recipient-name}}", + "send-transaction": "Enviar transacción", + "sending": "Enviando", + "sent-at": "Enviado a", + "server": "Servidor", + "set-a-topic": "Crear un tema", + "set-currency": "Establecer moneda", + "set-custom-fee": "Establecer tarifa personalizada", + "set-dapp-access-permissions": "Establecer permisos de acceso DApp", + "set-max": "Establecer máximo", + "settings": "Ajustes", + "share": "Compartir", + "share-address": "Compartir dirección", + "share-chat": "Compartir chat", + "share-contact-code": "Compartir mi clave de chat", + "share-dapp-text": "Echa un vistazo a esta DApp que estoy usando en Status: {{link}}", + "share-link": "Compartir enlace", + "share-my-profile": "Compartir mi perfil", + "share-profile": "Compartir perfil", + "share-profile-link": "Compartir enlace del perfil", + "share-public-chat-text": "Echa un vistazo a este chat público en la app de Status: {{link}}", + "shared": "Compartido", + "sharing-copied-to-clipboard": "Copiado", + "sharing-copy-to-clipboard": "Copiar", + "sharing-share": "Compartir", + "show-less": "Mostrar menos", + "show-more": "Mostrar más", + "show-notifications": "Mostrar notificaciones", + "show-profile-pictures": "Mostrar fotos de perfil de", + "show-qr": "Mostrar código QR", + "show-transaction-data": "Mostrar los datos de la transacción", + "sign-and-send": "Firmar y enviar", + "sign-anyway": "Continuar de todas maneras", + "sign-in": "Inicia sesión", + "sign-message": "Firmar mensaje", + "sign-out": "Cerrar sesión", + "sign-request-failed": "No se pudo firmar el mensaje", + "sign-with": "Firma con", + "sign-with-password": "Firmar con contraseña", + "sign-you-in": "Iniciando sesión...", + "signing": "Firma", + "signing-a-message": "Firmando un mensaje", + "signing-phrase": "Frase de firma", + "slow": "Lento", + "something-went-wrong": "Algo salió mal", + "soon": "Pronto", + "specify-address": "Especificar dirección", + "specify-name": "Especifica un nombre", + "specify-network-id": "Especifica ID de red", + "specify-rpc-url": "Especifica una URL RPC", + "specify-server-public-key": "Ingrese la clave pública del servidor", + "start-chat": "Iniciar chat", + "start-conversation": "Empezar conversación", + "start-group-chat": "Iniciar chat grupal", + "start-new-chat": "Iniciar nuevo chat", + "starter-pack-coming": "El Paquete de Inicio va hacia ti", + "starter-pack-coming-description": "Puede tomar de unos minutos a horas", + "starter-pack-received": "Paquete de Bienvenida", + "starter-pack-received-description": "¡Acá tenés algunas criptomonedas para comenzar! Usalas para obtener pegatinas, un nombre ENS y probar dapps", + "status": "Status", + "status-confirmed": "Confirmado", + "status-hardwallet": "Status hardwallet", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status tiende a usar una gran cantidad de datos al sincronizar chats. Podes optar por no sincronizar cuando estás en la red móvil", + "status-not-sent-click": "No está confirmado. Haz clic para ver las opciones", + "status-not-sent-tap": "No enviado. Toca para ver las opciones", + "status-pending": "Pendiente", + "status-sent": "Enviado", + "status-tx-not-found": "TX no encontrada", + "status-updates-descr": "Las actualizaciones de estado aparecerán aquí. Agregue el perfil como un contacto para recibir actualizaciones en su línea de tiempo.", + "statuses-descr": "Comparte lo que tienes en mente y mantente al día con tus contactos", + "statuses-my-status-descr": "Comparte lo que tienes en mente. Cualquiera que visite su perfil podrá ver su estado. Las personas que te agreguen como contacto recibirán tus actualizaciones en su línea de tiempo.", + "step-i-of-n": "Paso {{step}} de {{number}}", + "sticker-market": "Sticker market", + "storage": "Almacenamiento", + "submit": "Enviar", + "submit-bug": "Reportar un error", + "success": "Éxito", + "symbol": "Símbolo", + "sync-all-devices": "Sincronizar todos los dispositivos", + "sync-in-progress": "Sincronizando...", + "sync-settings": "Ajustes de sincronización", + "sync-synced": "Sincronizado", + "syncing-devices": "Sincronizando...", + "system": "Sistema", + "tabs": "Pestañas", + "tag-was-lost": "La etiqueta se perdió", + "tap-card-again": "Vuelve a tocar la tarjeta con la parte trasera de tu teléfono.", + "test-networks": "Redes de prueba", + "text-input-disabled": "Por favor espera un momento...", + "this-device": "Este dispositivo", + "this-device-desc": "Tus claves serán encriptadas y almacenadas de forma segura en tu dispositivo", + "this-is-you-signing": "Esta es tu frase de firma", + "this-will-take-few-seconds": "Esto tomará unos segundos", + "three-words-description": "Deberías ver estas tres palabras antes de firmar cada transacción", + "three-words-description-2": "Si ves una combinación diferente, cancela la transacción y cierra sesión", + "timeline": "Línea de tiempo", + "to": "Para", + "to-block": "Bloquear", + "to-enable-biometric": "Para habilitar {{bio-type-label}}, debes guardar tu contraseña en la pantalla de desbloqueo", + "to-encrypt-enter-password": "Para encriptar la cuenta, por favor, ingresa tu contraseña", + "to-see-this-message": "Para ver este mensaje,", + "token-auto-validate-decimals-error": "Decimales incorrectos para el token {{symbol}} en la dirección {{address}} - establecido en {{expected}} pero detectado como {{actual}}", + "token-auto-validate-name-error": "Nombre incorrecto para el token {{symbol}} en la dirección {{address}} - establecido en {{expected}} pero detectado como {{actual}}", + "token-auto-validate-symbol-error": "Símbolo incorrecto para el token {{symbol}} en la dirección {{address}} - configurado en {{expected}} pero detectado como {{actual}}", + "token-details": "Detalles del token", + "topic-name-error": "Utiliza solo letras minúsculas (a - z), números y guiones (-). No uses las claves de chat", + "transaction": "Transacción", + "transaction-data": "Transaction data", + "transaction-declined": "Transacción rechazada", + "transaction-description": "Transacciones con 12 confirmaciones pueden considerarse irreversibles", + "transaction-details": "Detalles de la transacción", + "transaction-failed": "Transacción fallida", + "transaction-history": "Historial de transacciones", + "transaction-request": "Solicitud de transacción", + "transaction-sent": "Transacción enviada", + "transaction-signed": "La transacción se ha firmado con éxito", + "transactions": "Transacciones", + "transactions-filter-select-all": "Seleccionar todo", + "transactions-filter-title": "Filtrar historial", + "transactions-history": "Historial de transacciones", + "transactions-history-empty": "Aún no hay transacciones en tu historial", + "transactions-history-loading": "Cargando historial de transacciones. Esto podrá tomar un tiempo.", + "transactions-load-more": "Mostrar más", + "transactions-sign": "Firmar", + "transfer-ma-unknown-error-desc-1": "Parece que su multicuenta no se eliminó. Es posible que la base de datos se haya restablecido", + "transfer-ma-unknown-error-desc-2": "Por favor verificá tu lista de cuentas e intentá de nuevo. Si la cuenta no está listada andá a Acceder llaves existentes para recuperar con frase semilla", + "transfers-fetching-failure": "El historial de transferencias no pudo ser actualizado. Verificá tu conexión y refresca para intentarlo de nuevo", + "tribute-required-by-multiaccount": "{{multiaccount-name}} requiere SNT para iniciar un chat.", + "tribute-state-paid": "Tributo pagado", + "tribute-state-pending": "Tributo pendiente", + "tribute-state-required": "Requiere tributo de {{snt-amount}} SNT", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Agregar amigos como un contacto para permitir chats sin pago de tributo.", + "tribute-to-talk-are-you-friends": "¿Son amigos?", + "tribute-to-talk-ask-to-be-added": "Pide que te añadan como contacto", + "tribute-to-talk-contact-received-your-tribute": "recibió tu tributo. Ahora pueden conversar con seguridad entre ustedes.", + "tribute-to-talk-desc": "Monetiza tu atención requiriendo SNT para que la gente nueva inicie un chat", + "tribute-to-talk-disabled": "Tribute to Talk deshabilitado", + "tribute-to-talk-disabled-note": "A partir de ahora, las personas nuevas pueden iniciar un chat contigo sin enviar SNT.", + "tribute-to-talk-enabled": "Tienes Tribute to Talk habilitado.", + "tribute-to-talk-finish-desc": "A partir de ahora, sólo recibirás chats de los contactos, y de la gente que pagó ", + "tribute-to-talk-learn-more-1": "Tu tiempo y atención son Tus activos más valiosos. Tribute to Talk te permite configurar la cantidad de SNT requerida para que nuevas personas inicien un chat contigo.", + "tribute-to-talk-learn-more-2": "Cualquier persona que no esté en tu lista de contactos deberá pagar, y tu puedes responder una vez que lo hayan hecho.", + "tribute-to-talk-learn-more-3": "Siempre puedes devolver el dinero, pero para asegurarte de que tus amigos puedan contactarte libremente, añádelos primero como contacto.", + "tribute-to-talk-paywall-learn-more-1": "Nuestro tiempo y atención son nuestros activos más valiosos. Tribute to Talk te permite contactar a nuevas personas a cambio de un pago de SNT.", + "tribute-to-talk-paywall-learn-more-2": "Para iniciar un chat con alguien que tiene configurado tributos, simplemente paga el SNT requerido y serás agregado como un contacto.", + "tribute-to-talk-paywall-learn-more-3": "Si los conoces, puedes compartir tu perfil fuera de Status para ser agregado de forma gratuita.", + "tribute-to-talk-pending": "Tributo pendiente de confirmación", + "tribute-to-talk-pending-note": "La transacción de tributo está pendiente de confirmación en la red. Puedes verificar el status en el historial de transacciones", + "tribute-to-talk-removing-note": "Eliminar Tribute to Talk permitirá que nuevas personas inicien un chat sin enviar SNT. Requiere que se realice una transacción.", + "tribute-to-talk-set-snt-amount": "Establece la cantidad de SNT requerida para que nuevas personas inicien un chat", + "tribute-to-talk-signing": "Esperando para firmar la transacción", + "tribute-to-talk-transaction-failed-note": "La transacción ha fallado y tu configuración de Tribute to Talk no ha cambiado", + "tribute-to-talk-tribute-received1": "El tributo fue recibido. Tú y ", + "tribute-to-talk-tribute-received2": "ahora son contactos y pueden chatear de forma segura entre sí.", + "tribute-to-talk-you-require-snt": "Se necesita SNT para que las personas nuevas inicien un chat.", + "try-again": "Inténtalo de nuevo", + "try-keeping-the-card-still": "Intenta mantener la tarjeta quieta", + "turn-nfc-description": "NFC está deshabilitado en su dispositivo. Puedes habilitarlo en la configuración", + "turn-nfc-on": "activar nfc", + "tx-fail-description1": "Es probable que esta transacción falle. Firme bajo su propio riesgo utilizando una tarifa de red personalizada.", + "tx-fail-description2": "Es probable que esta transacción falle. Establezca una tarifa de red personalizada para firmar bajo su propio riesgo.", + "type": "Tipo", + "type-a-message": "Mensaje", + "ulc-enabled": "ULC habilitado", + "unable-to-fetch": "No es posible recuperar el historial de chat", + "unable-to-read-this-code": "No se puede leer este código", + "unable-to-send-messages": "No es posible enviar o recibir mensajes", + "unblock": "Desbloquear", + "unblock-contact": "Desbloquear este usuario", + "unknown-status-go-error": "Error de status-go desconocido", + "unlock": "Desbloquear", + "unmute": "Activar sonido", + "unpair-card": "Desvincular tarjeta", + "unpair-card-confirmation": "Esta operación desvinculará la tarjeta del dispositivo actual. Requiere autorización del código de acceso de 6 dígitos. ¿Quieres continuar?", + "unpair-keycard": "Desemparejar la Keycard de este teléfono", + "unpair-keycard-warning": "Tu código de emparejamiento\/PUK y PIN permanecen sin cambios", + "unpaired-keycard-text": "La Keycard que tocaste no está asociada con este teléfono", + "unpaired-keycard-title": "Parece que tu tarjeta ha sido desvinculada", + "update": "Actualizar", + "update-to-listen-audio": "¡Actualizá a la última versión para escuchar un mensaje de audio acá!", + "update-to-see-image": "¡Actualizá a la última versión para ver una buena imagen acá!", + "update-to-see-sticker": "¡Actualizá a la última versión para ver un lindo sticker acá!", + "url": "URL", + "usd-currency": "Dólar estadounidense", + "use-valid-contact-code": "Por favor, ingresa o escanea una clave de contacto o nombre de usuario válido", + "validation-amount-invalid-number": "La cantidad no es un número válido", + "validation-amount-is-too-precise": "La cantidad es muy precisa. La cantidad máxima de decimales es {{decimals}} .", + "version": "Versión de la app", + "view": "Ver", + "view-cryptokitties": "Ver en CryptoKitties", + "view-cryptostrikers": "Ver en CryptoStrikers", + "view-details": "Ver detalles", + "view-etheremon": "Ver en Etheremon", + "view-gitcoin": "Ver en Gitcoin", + "view-profile": "Ver perfil", + "view-signing": "Ver la frase de firma", + "view-superrare": "Ver en SuperRare", + "waiting-for-wifi": "Sin Wi-Fi, la sincronización de mensajes está deshabilitada.", + "waiting-for-wifi-change": "Ajustes", + "waiting-to-sign": "Esperando para firmar la transacción...", + "waiting-wi-fi": "Esperando Wi-Fi…", + "waku-bloom-filter-mode": "Modo de filtro Waku bloom", + "wallet": "Billetera", + "wallet-address": "Dirección de la billetera", + "wallet-asset": "Activo", + "wallet-assets": "Activos", + "wallet-backup-recovery-title": "Respalda tu frase semilla", + "wallet-choose-recipient": "Elegir destinatario", + "wallet-collectibles": "Coleccionables", + "wallet-insufficient-funds": "Fondos insuficientes", + "wallet-insufficient-gas": "No hay suficiente ETH para el gas", + "wallet-invalid-address": "Dirección inválida: \n {{data}}", + "wallet-invalid-address-checksum": "Error en la dirección: \n {{data}}", + "wallet-invalid-chain-id": "La red no coincide: \n {{data}} pero la cadena actual es {{chain}}", + "wallet-key-content": "Una dirección hexadecimal de 64 caracteres basada en el estándar Ethereum y que comienza con 0x. De cara al público, tu clave de billetera se comparte con otros cuando deseas recibir fondos. También se conoce como \"dirección de Ethereum\" o \"dirección de billetera\".", + "wallet-key-title": "Dirección de la cuenta", + "wallet-manage-accounts": "Administrar cuentas", + "wallet-manage-assets": "Administrar activos", + "wallet-request": "Solicitud", + "wallet-send": "Enviar", + "wallet-send-min-units": "Mínimo 21000 unidades", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Ajustes de billetera", + "wallet-total-value": "Valor total", + "wallet-transaction-total-fee": "Costo total", + "wants-to-access-profile": "quiere acceder a tu perfil", + "warning": "Advertencia", + "warning-message": "Lo sentimos, limitamos el envío de varios mensajes en rápida sucesión para evitar el spam. Por favor, inténtalo de nuevo en un momento.", + "warning-sending-to-contract-descr": "La dirección que ingresó es un contrato inteligente, enviar fondos a esta dirección puede resultar en la pérdida de fondos. Para interactuar con una DApp, abra la DApp en el navegador de Status.", + "watch-only": "Sólo para ver", + "web-view-error": "No se puede cargar la página", + "websites": "Sitios web", + "webview-camera-permission-requests": "Solicitudes de permisos de la cámara Webview", + "webview-camera-permission-requests-subtitle": "Cuando está habilitado, los sitios web y las dapps pueden solicitar el uso de su cámara", + "welcome-blank-community-message": "Tus comunidades aparecerán aquí.", + "welcome-blank-message": "Tus chats aparecerán aquí. Para iniciar nuevos chats, presiona el botón ⊕", + "welcome-community-blank-message": "Tus chats aparecerán aquí. Para iniciar nuevos chats, haga clic en los 3 puntos de arriba y seleccione \"Crear un canal\"", + "welcome-screen-text": "¡Configura tu billetera, invita a amigos a chatear \n y busca dapps populares!", + "welcome-to-status": "¡Bienvenidos a Status!", + "welcome-to-status-description": "Configura tu cripto billetera, invita a tus amigos a chatear y navegar apps descentralizadas", + "whats-on-your-mind": "Lo que tienes en mente...", + "word-count": "Número de palabras", + "word-n": "Palabra #{{number}}", + "word-n-description": "Para comprobar si has realizado una copia de seguridad de la frase semilla correctamente, escribe arriba la palabra #{{number}}.", + "words-n": { + "one": "1 palabra", + "other": "{{count}} palabras" + }, + "write-down-and-store-securely": "Escribe los códigos\n y guárdalos de forma segura", + "wrong-address": "Dirección incorrecta", + "wrong-card": "Tarjeta equivocada", + "wrong-card-text": "Haz tocado una tarjeta que no corresponde a las llaves seleccionadas", + "wrong-contract": "Contrato incorrecto", + "wrong-keycard-text": "La Keycard que tocaste no está asociada con este teléfono", + "wrong-keycard-title": "Parece que has hecho tapping \nuna keycard incorrecta", + "wrong-password": "Contraseña incorrecta", + "wrong-word": "Palabra incorrecta", + "yes": "Sí", + "you": "tú", + "you-already-have-an-asset": "Ya tienes un activo {{value}}", + "you-are-all-set": "¡Estás listo!", + "you-are-all-set-description": "Ahora, si pierdes tu teléfono, puedes acceder a tus fondos y llave de chat usando la frase semilla", + "you-can-change-account": "Puedes cambiar el nombre y el color de la cuenta a lo que deseas", + "you-can-choose-preview-websites": "Puede elegir cuál de los siguientes sitios web puede previsualizar el enlace de las descripciones e imágenes en los chats", + "you-can-fetch": "Puedes recuperar el historial de chat", + "you-dont-have-contacts": "Todavía no tenés ningún contacto.", + "you-dont-have-stickers": "Todavía no tienes stickers", + "you-will-need-this-code": "Necesitarás este código para abrir Status y firmar transacciones", + "you-will-start-from-scratch": "Comenzarás desde cero con un nuevo set de claves", + "your-card-is-frozen": "Tu Keycard está congelada. Restablece el acceso a la tarjeta", + "your-contact-code": "Conceder acceso autoriza a esta DApp a obtener tu clave de chat", + "your-data-belongs-to-you": "Si pierdes tu frase semilla, pierdes tus datos y fondos", + "your-data-belongs-to-you-description": "Si pierdes acceso, por ejemplo al perder tu teléfono, solo puedes acceder a tus llaves con tu frase semilla. Nadie aparte de ti tiene tu frase semilla. Anótala. Mantenla segura.", + "your-keys": "Tus claves", + "your-price-limit": "Tu límite de precio", + "your-recovery-phrase": "Tu frase semilla", + "your-recovery-phrase-description": "Esta es tu frase semilla. La usas para comprobar que esta es tu billetera. ¡Sólo la verás una vez! Escríbela en un papel y guárdala en un lugar seguro. La necesitarás si pierdes o reinstalas tu billetera.", + "your-tip-limit": "Tu límite de propina", + "youre-on-mobile-network": "Estás en una red móvil" +} diff --git a/translations/es_mx.json b/translations/es_mx.json new file mode 100644 index 00000000000..7dd41b0e967 --- /dev/null +++ b/translations/es_mx.json @@ -0,0 +1,130 @@ +{ + "confirm": "Confirmar", + "amount": "Cantidad", + "members-active": { + "one": "1 miembro", + "other": "{{count}} miembros", + "zero": "no hay miembros" + }, + "chat-name": "Nombre del chat", + "phew-here-is-your-passphrase": "*Uff* eso fue difícil, esta es tu frase de contraseña, *¡escríbela y guárdala en un lugar seguro!* La necesitarás para recuperar tu cuenta.", + "public-group-topic": "Tema", + "chat-settings": "Ajustes de chat", + "offline": "Desconectado", + "invited": "invitado", + "address": "Dirección", + "new-public-group-chat": "Unirte a chat público", + "datetime-hour": { + "one": "hora", + "other": "horas" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "Para permitir el acceso a la cámara, porfavor, ve a configuración de sistema y asegúrate que la opción 'Status > Cámara' está seleccionada.", + "remove": "Remover", + "add-members": "Agregar miembros", + "photos-access-error": "Para permitir el acceso a fotos, porfavor, ve a configuración de sistema y asegúrate que la opción 'Status > Fotos' está seleccionada.", + "done": "Listo", + "close-chat": "Eliminar chat", + "new-group-chat": "Nuevo grupo de chat", + "sign-in": "Regístrate", + "datetime-yesterday": "ayer", + "create-new-account": "Crear nueva cuenta", + "datetime-ago": "hace", + "contacts": "Nuevo contacto", + "got-it": "Entendido", + "active-online": "En línea", + "password": "Contraseña", + "browsing-title": "Navegar", + "discover": "Descubrir", + "browsing-cancel": "Cancelar", + "intro-status": "¡Chatea conmigo para establecer tu cuenta y modificar tu configuración!", + "name": "Nombre", + "show-qr": "Mostrar QR", + "connect": "Conectar", + "edit": "Editar", + "account-generation-message": "Dame un segundo, ¡haré algunos cálculos para generar tu cuenta!", + "no-messages": "No hay mensajes", + "passphrase": "Frase de contraseña", + "recipient": "Receptor", + "members-title": "Miembros", + "new-group": "Nuevo grupo", + "phone-e164": "Internacional 1", + "settings": "Ajustes", + "delete": "Eliminar", + "chats": "Chats", + "transaction": "Transacción", + "public-group-status": "Público", + "image-source-make-photo": "Tomar foto", + "start-conversation": "Empezar conversación", + "save": "Guardar", + "sharing-copy-to-clipboard": "Copiar", + "sync-in-progress": "Sincronizando...", + "send-transaction": "Enviar transacción", + "incorrect-code": [ + "str", + "Lo sentimos, el código es incorrecto, escríbelo de nuevo" + ], + "image-source-gallery": "Seleccionar de la galería", + "sync-synced": "Sincronizado", + "status-pending": "Pendiente", + "datetime-day": { + "one": "día", + "other": "días" + }, + "scan-qr": "Escanear QR", + "contact-s": { + "one": "contacto", + "other": "contactos" + }, + "next": "Siguiente", + "recent": "Recientes", + "status": "Estado", + "from": "De", + "wrong-password": "Contraseña incorrecta", + "in-contacts": "En contactos", + "sharing-share": "Compartir...", + "type-a-message": "Escribe un mensaje...", + "clear-history": "Borrar historial", + "no-contacts": "Aún no hay contactos", + "datetime-today": "hoy", + "web-view-error": "oops, error", + "error": "Error", + "more": "más", + "cancel": "Cancelar", + "can-not-add-yourself": "No puedes agregarte a ti mismo", + "add-to-contacts": "Agregar a contactos", + "available": "Disponible", + "You": "Tú", + "main-wallet": "Wallet principal", + "members": { + "one": "1 miembro", + "other": "{{count}} miembros", + "zero": "no hay miembros" + }, + "intro-message1": "¡Bienvenid@ a Status\nToca este mensaje para configurar tu contraseña y empezar!", + "datetime-second": { + "one": "segundo", + "other": "segundos" + }, + "recover": "Recuperar", + "datetime-minute": { + "one": "minuto", + "other": "minutos" + }, + "browsing-open-in-android-web-browser": "Abrir en navegador", + "browsing-open-in-ios-web-browser": "Abrir en navegador", + "edit-profile": "Editar perfil", + "active-unknown": "Desconocido", + "public-key": "Llave pública", + "profile": "Perfil", + "none": "Ninguno", + "removed": "eliminado", + "message": "Mensaje", + "here-is-your-passphrase": "Esta es tu frase de contraseña, *¡escríbela y guárdala en un lugar seguro!* La necesitarás para recuperar tu cuenta.", + "image-source-title": "Imagen de perfil", + "current-network": "Red actual", + "left": "salió", + "to": "Para", + "status-sent": "Enviado", + "data": "Datos" +} diff --git a/translations/fa.json b/translations/fa.json new file mode 100644 index 00000000000..133094ed5c4 --- /dev/null +++ b/translations/fa.json @@ -0,0 +1,1229 @@ +{ + "You": "شما", + "about-app": "درباره", + "about-key-storage-content": "استاتوس هرگز به کلید خصوصی شما دسترسی نخواهد داشت. حتماً از عبارت دانه خود نسخه پشتیبان تهیه کنید. اگر تلفن خود را گم کنید ، این تنها راه دسترسی به کلیدهای شماست.", + "about-key-storage-title": "درباره ذخیره کلید", + "about-names-content": "هیچ کس نمی تواند وانمود کند که شما هستید! شما به طور پیش فرض ناشناس هستید و هرگز مجبور نیستید نام واقعی خود را فاش کنید. می توانید با هزینه ای ناچیز نام دلخواه خود را ثبت کنید.", + "about-names-title": "نامها را نمی توان تغییر داد", + "accept": "پذیرفتن", + "accept-and-share-address": "پذیرش و به اشتراک گذاری آدرس", + "accept-new-chats-from": "پذیرش گفتگو های جدید از", + "access-existing-keys": "دسترسی به کلیدهای موجود", + "access-key": "کلید دسترسی", + "account-added": "حساب اضافه شد", + "account-color": "رنگ حساب", + "account-content": "می‌توانید حساب‌های موجود در استاتوس را با حساب‌های بانکی مقایسه کنید. مانند یک حساب بانکی، یک حساب به طور معمول دارای یک آدرس و مانده است. شما از این حساب برای معامله در اتریوم استفاده می‌کنید. در کیف پول می‌توانید چندین حساب کاربری داشته باشید. با باز کردن استاتوس به همه آنها میتوان دسترسی پیدا کرد.", + "account-exists-title": "حساب در حال حاضر وجود دارد", + "account-name": "نام حساب", + "account-settings": "تنظیمات حساب", + "account-title": "حساب", + "accounts": "حساب ها", + "active-online": "آنلاین", + "active-unknown": "ناشناخته", + "add": "اضافه کردن", + "add-a-watch-account": "افزودن یک آدرس فقط دیدنی", + "add-account": "افزودن حساب", + "add-account-description": "شما می توانید هر نوع حساب اتریوم را وارد کنید تا آن را به کیف پول استاتوس خود اضافه کنید", + "add-account-incorrect-password": "کلمه عبور نادرست است. کلمه عبور مورد استفاده برای باز کردن قفل برنامه را وارد کنید.", + "add-an-account": "افزودن حساب", + "add-bootnode": "اضافه کردن bootnode", + "add-contact": "افزودن مخاطب", + "add-custom-token": "افزودن نشانه سفارشی", + "add-mailserver": "اضافه کردن mailserver", + "add-members": "افزودن اعضا", + "add-network": "اضافه کردن شبکه", + "add-new-contact": "افزودن مخاطب جدید", + "add-node": "افزودن گره", + "add-private-key-account": "اضافه کردن حساب از طریق کلید خصوصی", + "add-seed-account": "اضافه کردن حساب از طریق کلمات پشتیبان", + "add-text": "افزودن متن", + "add-to-contacts": "افزودن به مخاطبین", + "add-to-contacts-text": "با اضافه کردن یک کاربر به لیست مخاطبین ، آدرس کیف پول خود را به اشتراک می گذارید", + "add-watch-account": "افزودم حساب تنها نمایشی\n", + "address": "آدرس", + "address-received": "آدرس دریافت شد", + "address-request-accepted": "درخواست آدرس پذیرفته شد", + "address-requested": "آدرس درخواست شد", + "advanced": "پیشرفته", + "advanced-settings": "تنظیمات پیشرفته", + "advertiser-description": "شما به لطف یک شریک اِستاتوس را کشف کرده اید. آیا مشکلی ندارید که Status یک بار آدرس IP شما را بررسی کند تا به آنها پاداش داده شود؟ این اطلاعات برای هیچ چیز دیگری استفاده نمی شود و پس از 7 روز به طور کامل حذف می شود.", + "advertiser-starter-pack-accept": "پذیرفتن", + "advertiser-starter-pack-decline": "نپذیرفتن", + "advertiser-starter-pack-description": "در اینجا مقداری رمزارز برای شروع کار وجود دارد! برای به دست آوردن استیکرها و یک نام ENS از آن استفاده کنید، و اپ‌های غیرمتمرکز را امتحان کنید", + "advertiser-starter-pack-title": "بستۀ آغازگر", + "advertiser-title": "حریم خصوصی همیشگی", + "agree-by-continuing": "در صورتی که ادامه دهید \nموافقت خواهید کرد با", + "all": "همه", + "allow": "اجازه دادن", + "allowing-authorizes-this-dapp": "به این DApp اجازه داده می شود تا آدرس کیف پول شما را بازیابی و Web3 را فعال کند", + "already-have-asset": "شما قبلاً این دارایی را دارید", + "amount": "مقدار", + "anyone": "هر کس", + "appearance": "ظاهر", + "apply": "اعمال", + "are-you-sure": "آیا مطمئن هستید؟", + "are-you-sure-description": "شما قادر نخواهید بود که کل عبارت دانه را دوباره مشاهده نمایید", + "are-you-sure-to-cancel": "آیا مطمئن هستید؟", + "are-you-sure?": "آیا مطمئن هستید؟", + "ask-in-status": "یک سوال بپرسید یا یک اشکال را گزارش کنید", + "at": "در", + "attribution-received": "{{attrib}} تا از {{max}} پاداش دریافت شده است", + "audio": "صدا", + "audio-recorder": "ضبط کننده", + "audio-recorder-error": "خطای ضبط کننده", + "audio-recorder-max-ms-reached": "حداکثر زمان ضبط فرا رسیده است", + "audio-recorder-permissions-error": "شما باید مجوز ارسال پیام‌های صوتی را بدهید", + "authorize": "مجاز کردن", + "available": "در دسترس", + "available-participants": { + "one": "میتوانید یک عضو دیگر انتخاب کنید", + "other": "میتوانید {{count}} عضو دیگر انتخاب کنید" + }, + "back": "بازگشت", + "back-up-seed-phrase": "از دانه نسخه پشتیبان تهیه کنید", + "back-up-your-seed-phrase": "پشتیبان گیری از عبارت دانه", + "backup-disabled": "غیر فعال", + "backup-recovery-phrase": "از دانه نسخه پشتیبان تهیه کنید", + "balance": "موجودی", + "begin-set-up": "شروع راه اندازی", + "biometric-auth-android-sensor-desc": "حسگر لمسی", + "biometric-auth-android-sensor-error-desc": "ناموفق", + "biometric-auth-android-title": "نیاز به تایید اعتبار", + "biometric-auth-confirm-logout": "ورود مجدد", + "biometric-auth-confirm-message": "احراز هویت بیومتریک برای ادامه مورد نیاز است، اگر امکان پذیر نیست لطفا کلید های خود را با رمز عبور یا کد عبور خود باز کنید", + "biometric-auth-confirm-title": "شما باید احراز هویت کنید!", + "biometric-auth-confirm-try-again": "دوباره امتحان کنید", + "biometric-auth-error": "انجام احراز هویت بیومتریک امکان‌پذیر نیست ({{code}})", + "biometric-auth-login-error-title": "خطای احراز هویت بیومتریک", + "biometric-auth-login-ios-fallback-label": "رمز عبور را وارد کنید", + "biometric-auth-reason-login": "ورود به حساب کاربری استتوس", + "biometric-auth-reason-verify": "تأیید اعتبار", + "biometric-disable-bioauth": "غیرفعال کردن {{bio-type-label}}", + "biometric-disable-password-description": "در صورت غیر فعال کردن این گزینه شما همچنین", + "biometric-disable-password-title": "غیر فعال کردن ذخیره رمز عبور", + "biometric-enable": "اگر نمی‌خواهید برای دسترسی به برنامه هربار رمز ورود خود را وارد کنید {{bio-type-label}} را فعال کنید", + "biometric-enable-button": "فعال کردن {{bio-type-label}}", + "biometric-enable-keycard": "اگر نمی‌خواهید هر بار از کارت کلید خود برای دسترسی به برنامه استفاده کنید، ورود {{bio-type-label}} را فعال کنید.", + "biometric-faceid": "حسگر تشخیص چهره", + "biometric-fingerprint": "اثر انگشت", + "biometric-secure-with": "ایمن با {{bio-type-label}}", + "biometric-touchid": "حسگر اثر انگشت", + "blank-contacts-text": "مخاطبین شما اینجا خواهند بود", + "blank-keycard-text": "هنگامی که کلید ها و نام خود را ایجاد کرده اید ، می توانید با keycard خود اقدام کنید", + "blank-keycard-title": "به نظر می رسد که شما به یک keycard خالی ضربه زدید", + "blank-messages-text": "پیامهای شما اینجا خواهد بود", + "block": "مسدود کردن", + "block-contact": "انسداد کاربر", + "block-contact-details": "انسداد این کاربر موجب حذف پیامهای قبلی شده و از دریافت پیامهای جدید جلوگیری میکند", + "block-user": "انسداد کاربر", + "block-user?": "انسداد کاربر؟", + "blocked-users": "کاربران مسدود شده", + "bold": "برجسته", + "bootnode-address": "آدرس Bootnode", + "bootnode-details": "جزئیات Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnode ها", + "bootnodes-enabled": "فعال شدن Bootnode ها", + "bootnodes-settings": "تنظیمات Bootnode ها", + "browsed-websites": "تاریخچه مرورگر در اینجا ظاهر می شود", + "browser": "مرورگر", + "browser-not-secure": "اتصال امن نیست! معاملات را امضا نکنید یا اطلاعات شخصی را در این سایت ارسال نکنید.", + "browser-secure": "اتصال امن است. اطمینان حاصل کنید که قبل از امضای معاملات یا وارد کردن اطلاعات شخصی ، به این سایت اطمینان دارید.", + "browsers": "مرورگرها", + "browsing-cancel": "لغو", + "browsing-open-in-android-web-browser": "باز کردن در اندروید", + "browsing-open-in-ios-web-browser": "باز کردن در iOS", + "browsing-open-in-status": "باز کردن در استاتوس", + "browsing-site-blocked-description1": "ما فعالیت مخرب بالقوه ای را از این آدرس شناسایی کرده ایم. برای محافظت از شما و کیف پولتان، ما از پیشروی این موضوع جلوگیری می کنیم.. \n\n اگر فکر می کنید این یک خطا است، به ما اطلاع دهید", + "browsing-site-blocked-description2": " چت عمومی.", + "browsing-site-blocked-go-back": "بازگشت", + "browsing-site-blocked-title": "این سایت مسدود شده است", + "browsing-title": "مرور کردن", + "bug-report": "ثبت یک اشکال", + "bug-report-description": "* شرح", + "bug-report-description-placeholder": "متن اجباری", + "bug-report-steps": "گام های بازتولید", + "bug-report-steps-placeholder": "- برنامه را باز کن\n- کاري انجام بده\n-و بعدش يک کار ديگر ...", + "bug-report-submit-email": "ارسال از طریق ایمیل با آرشیو سیاهه های مربوط", + "bug-report-submit-gh-issue": "ارسال یک مشکل به گیتهاب بدون سیاهه", + "bug-report-too-short-description": "شرح زیادی کوتاه است", + "camera-access-error": "برای اعطای مجوز مورد نیاز به دوربین، لطفا به تنظیمات گوشی خود بروید و مطمئن شوید که Status > Camera انتخاب شده است.", + "can-not-add-yourself": "این شما هستید ، برای شروع گفتگو ، شخص دیگری را انتخاب کنید", + "cancel": "لغو", + "cancel-keycard-setup": "لغو تنظیمات Keycard", + "cannot-pin-desc": "تنها قادر به سنجاق سه پیام هستید. برای سنجاق پیامهای بیشتر یکی از قبلیها را حذف کنید.", + "cannot-pin-title": "نمیتوانید این پیام را سنجاق کنید!", + "cannot-read-card": "امکان خواندن کارت نبود.\nدر ادامه کارت را پشت تلفن همراه خود قرار دهید.", + "cannot-use-default-pin": "کد عبور 000000 مجاز نبود.\nلطفا شماره دیگری را استفاده کنید", + "cant-report-bug": "گزارش خطا امکانپذیر نیست", + "card-is-blank": "این کارت خالی است", + "card-reseted": "کارت دوباره تنظیم شده است", + "card-unpaired": "کارت دیگر با دستگاه فعلی جفت نیست", + "category-title": "عنوان دسته بندی", + "change-fleet": "تغییر Fleet به {{fleet}}", + "change-group-privacy": "تغییر حریم خصوصی گروه", + "change-logging-enabled": "آیا یقین دارید که میخواهید ثبت سیاهه را {{enable}} کنید؟", + "change-pairing": "تغییر کد جفت سازی", + "change-pairing-description": "تغییر کد جفت سازی تاثیری بر جفت های کنونی ندارد. اما هر جفت سازی جدید نیاز به این کد جدید خواهد داشت.", + "change-pairing-title": "ساخت یک کد جفت سازی تازه", + "change-passcode": "تغییر کد عبور", + "change-password": "تغییر رمز عبور", + "change-pin": "تغییر کد عبور شش رقمی", + "change-puk": "تغییر PUK 12 رقمی", + "changed-amount-warning": "تغییر مقدار از {{old}} به {{new}}", + "changed-asset-warning": "تغییر دارایی از {{old}} به {{new}}", + "chaos-mode": "حالت آشوب", + "chaos-unicorn-day": "Chaos Unicorn Day", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "چت", + "chat-and-transact": "گفتگو و تراکنش خصوصی با دوستان", + "chat-is-a-contact": "مخاطب", + "chat-is-not-a-contact": "مخاطب معتبر انتخاب نشده", + "chat-key": "کلید چت", + "chat-key-content": "پیام‌های چت استاتوس با استفاده از کلیدهای رمزگذاری ارسال و دریافت می‌شوند. کلید چت عمومی مجموعه ای از کاراکترهایی است که با دیگران به اشتراک می‌گذارید تا بتوانند برای شما پیام‌هایی را در استاتوس ارسال کنند.", + "chat-key-title": "کلید چت", + "chat-name": "نام گفتگو", + "chat-name-content": "سه کلمه تصادفی، مشتق از الگوریتمی از کلید گپ شما و به عنوان نام اصلی پیش فرض شما در گپ استفاده می‌شود. نام چت کاملاً یکتا است. هیچ کاربر دیگری نمی‌تواند همان سه کلمه را داشته باشد.", + "chat-name-title": "نام چت", + "chat-notification-preferences": "تنظیمات اعلان", + "chat-settings": "تنظیمات گفتگو", + "chats": "گفتگوها", + "check-your-recovery-phrase": "از عبارت دانه خود نسخه پشتیبان تهیه کنید", + "choose-authentication-method": "روش تأیید اعتبار برای محافظت از حساب خود را انتخاب کنید", + "clear": "پاک کردن", + "clear-all": "پاک کردن همه", + "clear-history": "پاک کردن تاریخچه", + "clear-history-action": "پاک کردن", + "clear-history-confirmation": "پاک کردن تاریخچه ؟", + "clear-history-confirmation-content": "آیا مطمئن هستید که می خواهید این تاریخچه چت را پاک کنید؟", + "clear-history-title": "پاک کردن تاریخچه ؟", + "clear-history?": "حذف تاریخچه؟", + "cleared-chat-description-public": "اینجا خبری نبوده است. گفتگویی شروع کنید یا", + "close": "بستن", + "close-app-button": "تایید", + "close-app-content": "برنامه متوقف می شود و بسته خواهد شد. هنگامی که شما آن را دوباره باز کنید ، شبکه انتخاب شده استفاده می شود", + "close-app-title": "هشدار!", + "command-button-send": "ارسال", + "communities": "جوامع", + "communities-alpha": "جوامع (آلفا)", + "communities-verified": "✓ انجمن تایید شده", + "community-color": "رنگ جامعه", + "community-color-placeholder": "انتخاب رنگ", + "community-edit-title": "ویرایش جامعه", + "community-emoji-thumbnail-title": "بندانگشتی", + "community-image-pick": "انتخاب تصویر", + "community-image-remove": "حذف", + "community-image-take": "گرفتن عکس", + "community-invite-title": "دعوت از دوستان", + "community-key": "کلید خصوصی جامعه", + "community-key-placeholder": "کلید خصوصی جامعه را تایپ کنید", + "community-link": "پیوند جامعه", + "community-members": { + "one": "{{count}} عضو", + "other": "{{count}} عضو" + }, + "community-members-title": "اعضا", + "community-notification-settings": "تنظیمات اعلان", + "community-private-key": "کلید خصوصی جامعه", + "community-requests-to-join-title": "درخواست های عضویت", + "community-roles": "نقشها", + "community-share-title": "اشتراک گذاری", + "community-thumbnail-image": "تصویر بندانگشتی", + "community-thumbnail-upload": "بارگذاری", + "complete-hardwallet-setup": "این کارت اکنون متصل است. برای امضای تراکنشها و باز کردن کلیدها به آن نیاز دارید", + "completed": "تکمیل", + "confirm": "تایید", + "confirm-password-placeholder": "رمز عبور خود را تأیید کنید ...", + "confirmation-request": "درخواست تایید", + "confirmations": "تاییدیه ها", + "confirmations-helper-text": "هنگامی که معامله 12 تاییدیه داشته باشد ، می توانید آن را تسویه کنید.", + "connect": "اتصال", + "connect-mailserver-content": "اتصال به {{name}}؟", + "connected": "متصل", + "connected-to": "متصل به ", + "connecting": "در حال اتصال...", + "connecting-requires-login": "اتصال به شبکه دیگر نیاز به ورود دارد", + "connection-with-the-card-lost": "برای ادامه ، کارت را دوباره وصل کنید", + "connection-with-the-card-lost-setup-text": "برای ادامه عملیات، کارت را پشت تلفن همراه خود قرار دهید.", + "connection-with-the-card-lost-text": "برای ادامه عملیات، کارت را پشت تلفن همراه خود قرار دهید.", + "contact-code": "کلید چت", + "contact-requests": "درخواست های تماس", + "contact-s": { + "one": "مخاطب", + "other": "مخاطب" + }, + "contacts": "مخاطبین", + "continue": "ادامه", + "continue-anyway": "ادامه بده", + "contract-address": "آدرس قرارداد", + "contract-interaction": "تعامل قرارداد", + "copy-info": "کپی اطلاعات", + "copy-qr": "کپی کردن کد", + "copy-to-clipboard": "کپی", + "copy-transaction-hash": "کپی شناسه تراکنش", + "cost-fee": "هزینه", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "ایجاد", + "create-a-puk": "ایجاد یک PUK 12 رقمی", + "create-category": "ساخت یک دسته بندی", + "create-channel": "ساخت کانال", + "create-channel-title": "کانال جدید", + "create-community": "ساخت یک جامعه", + "create-group-chat": "ایجاد یگ گفتگوی گروهی", + "create-multiaccount": "ایجاد کلید", + "create-new-key": "دریافت کلیدهای جدید", + "create-pin": "ایجاد کد عبور 6 رقمی", + "create-pin-description": "شما به این کارت و این کد عبور 6 رقمی برای باز کردن اِستاتوس و تایید تراکنشها نیاز خواهید داشت.", + "created-group-chat-description": "گروه را ایجاد کردید", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "ارز", + "currency-display-name-aed": "درهم امارات", + "currency-display-name-afn": "افغانی افغانستان", + "currency-display-name-ars": "پزو آرژانتین", + "currency-display-name-aud": "دلار استرالیا", + "currency-display-name-bbd": "دلار باربادوس", + "currency-display-name-bdt": "تاکا بنگلادش", + "currency-display-name-bgn": "لو بلغارستان", + "currency-display-name-bhd": "دینار بحرین", + "currency-display-name-bnd": "دلار برونئی", + "currency-display-name-bob": "بولیویانو بولیوی", + "currency-display-name-brl": "رئال برزیل", + "currency-display-name-btn": "Bhutanese Ngultrum", + "currency-display-name-cad": "دلار کانادا", + "currency-display-name-chf": "فرانک سوئیس", + "currency-display-name-clp": "پزو شیلی", + "currency-display-name-cny": "یوآن چین", + "currency-display-name-cop": "کلمبیا پزو", + "currency-display-name-crc": "کولون کاستاریکا", + "currency-display-name-czk": "کرونای چک", + "currency-display-name-dkk": "کرون دانمارک", + "currency-display-name-dop": "پزو جمهوری دومینیکن", + "currency-display-name-egp": "پوند مصر", + "currency-display-name-etb": "بیر اتیوپی", + "currency-display-name-eur": "یورو", + "currency-display-name-gbp": "پوند انگلیس", + "currency-display-name-gel": "لاری گرجستان", + "currency-display-name-ghs": "سدی غنا", + "currency-display-name-hkd": "دلار هنگ کنگ", + "currency-display-name-hrk": "کونا کرواسی", + "currency-display-name-huf": "فورینت مجارستان", + "currency-display-name-idr": "روپیه اندونزی", + "currency-display-name-ils": "شِکِل اسرائیل", + "currency-display-name-inr": "روپیه هند", + "currency-display-name-isk": "کرون ایسلند", + "currency-display-name-jmd": "دلار جامائیکا", + "currency-display-name-jpy": "ین ژاپن", + "currency-display-name-kes": "شیلینگ کنیا", + "currency-display-name-krw": "وون کره جنوبی", + "currency-display-name-kwd": "دینار کویت", + "currency-display-name-kzt": "تنگ قزاقستان", + "currency-display-name-lkr": "روپیه سریلانکا", + "currency-display-name-mad": "درهم مراکش", + "currency-display-name-mdl": "لیو مولداوی", + "currency-display-name-mur": "روپیه موریس", + "currency-display-name-mwk": "مالاویا کواچا", + "currency-display-name-mxn": "پزو مکزیک", + "currency-display-name-myr": "رینگیت مالزی", + "currency-display-name-mzn": "متیکال موزامبیک", + "currency-display-name-nad": "دلار نامیبیا", + "currency-display-name-ngn": "نایرا نیجریه", + "currency-display-name-nok": "کرون نروژ", + "currency-display-name-npr": "روپیه نپال", + "currency-display-name-nzd": "دلار نیوزیلند", + "currency-display-name-omr": "ریال عمان", + "currency-display-name-pen": "سُل پرو", + "currency-display-name-pgk": "پاپوآ گینه نو", + "currency-display-name-php": "پزو فیلیپین", + "currency-display-name-pkr": "روپیه پاکستان", + "currency-display-name-pln": "زُلتی لهستان", + "currency-display-name-pyg": "گوارانی پاراگوئه", + "currency-display-name-qar": "ریال قطر", + "currency-display-name-ron": "لیو رومانی", + "currency-display-name-rsd": "دینار صربستان", + "currency-display-name-rub": "روبل روسیه", + "currency-display-name-sar": "ریال عربستان سعودی", + "currency-display-name-sek": "کرون سوئد", + "currency-display-name-sgd": "دلار سنگاپور", + "currency-display-name-thb": "بات تایلند", + "currency-display-name-try": "لیره ترکیه", + "currency-display-name-ttd": "دلار ترینیداد و توباگو", + "currency-display-name-twd": "دلار جدید تایوان", + "currency-display-name-tzs": "شیلینگ تانزانیا", + "currency-display-name-uah": "Hryvnia اوکراین", + "currency-display-name-ugx": "شیلینگ اوگاندا", + "currency-display-name-usd": "دلار آمریکا", + "currency-display-name-uyu": "پزوی اوروگوئه", + "currency-display-name-vef": "بولیوار ونزوئلا", + "currency-display-name-vnd": "دونگ ویتنام", + "currency-display-name-zar": "رند آفریقای جنوبی", + "current-network": "شبکه جاری", + "current-pin": "کد عبور 6 رقمی را وارد کنید", + "current-pin-description": "کد عبور 6 رقمی را برای ادامه وارد کنید", + "custom": "سفارشی", + "custom-networks": "شبکه های سفارشی", + "custom-seed-phrase": "عبارت ترکیبی رویشی نامعتبر", + "custom-seed-phrase-text-1": "این عبارت دانه با دیکشنری پشتیبانی شده ما مطابقت ندارد. کلمات اشتباه را بررسی کنید.", + "cut": "بریدن", + "dapp": "برنامه غیرمتمرکز", + "dapp-starter-pack-accept": "پذیرش و باز کردن", + "dapp-starter-pack-description": "در اینجا مقداری رمزارز برای شروع کار وجود دارد! برای به دست آوردن استیکرها و یک نام ENS از آن استفاده کنید، و اپ‌های غیرمتمرکز را امتحان کنید", + "dapp-starter-pack-title": "بستۀ آغازگر", + "dapp-would-like-to-connect-wallet": "مایل به اتصال به", + "dapps": "برنامه های غیرمتمرکز", + "dapps-permissions": "مجوزهای DApp", + "dark": "تیره\n", + "data": "داده", + "data-syncing": "همگام سازی داده‌ها", + "datetime-ago": "پیش", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "روز", + "other": "روز" + }, + "datetime-day-short": { + "one": "ر", + "other": "ر" + }, + "datetime-hour": { + "one": "ساعت", + "other": "ساعت" + }, + "datetime-hour-short": { + "one": "س", + "other": "س" + }, + "datetime-minute": { + "one": "دقیقه", + "other": "دقیقه" + }, + "datetime-minute-short": { + "one": "د", + "other": "د" + }, + "datetime-second": { + "one": "ثانیه", + "other": "ثانیه" + }, + "datetime-second-short": { + "one": "ث", + "other": "ث" + }, + "datetime-today": "امروز", + "decimals": "اعشاری ها", + "decline": "نپذیرفتن", + "decryption-failed-content": "ما قادر به رمزگشایی اطلاعات شما نبودیم، در صورت نیاز به ایجاد حساب جدید داشته باشید و پاک کردن داده های قدیمی خود روی «اعمال» ضربه بزنید. با کلیک بر روی \"لغو\"، دوباره امتحان کنید", + "default": "پیش فرض", + "delete": "حذف", + "delete-account": "حذف حساب کاربری", + "delete-and-leave-group": "حذف و ترک گروه", + "delete-bootnode": "حذف bootnode", + "delete-bootnode-are-you-sure": "آیا مطمئن هستید که میخواهید این bootnode را حذف کنید؟", + "delete-bootnode-title": "حذف bootnode", + "delete-category-confirmation": "از حذف این دسته بندی مطمئن هستید؟", + "close-chat": "حذف گفتگو؟", + "close-chat-confirmation": "آیا مطمئنید که می خواهید این چت را حذف کنید ؟", + "delete-confirmation": "حذف؟", + "delete-mailserver": "حذف mailserver", + "delete-mailserver-are-you-sure": "آیا مطمئن هستید که میخواهید این Mailserver را حذف کنید؟", + "delete-mailserver-title": "حذف mailserver", + "delete-message": "حذف پیام", + "delete-my-account": "حساب من را حذف کن!", + "delete-my-profile": "پروفایل مرا حذف کن", + "delete-network-confirmation": "آیا مطمئنید که می خواهید این شبکه را حذف کنید ؟", + "delete-network-error": "لطفاً قبل از حذف این شبکه به شبکه دیگری وصل شوید", + "delete-network-title": "حذف شبکه؟", + "delete-node": "حذف گره", + "delete-node-are-you-sure": "از حذف این گره مطمئن هستید؟", + "delete-node-title": "حذف گره", + "delete-profile": "حذف کردن پروفایل", + "delete-profile-warning": "هشدار: اگر عبارت بازیابی خود را یادداشت نکنید، پس از حذف پروفایل خود، دسترسی به دارایی‌های خود را از دست خواهید داد", + "deny": "رد", + "derivation-path": "مسیر استخراج", + "describe-channel": "کانال را شرح دهید", + "description": "توضیحات", + "dev-mode": "حالت توسعه", + "dev-mode-settings": "تنظیمات حالت برنامه نویسی و توسعه", + "device-syncing": "همگام سازی دستگاه", + "devices": "دستگاه ها", + "disable": "غیرفعال کردن", + "disabled": "غیر فعال", + "disconnected": "چت آفلاین", + "discover": "کشف کردن", + "dismiss": "رد", + "done": "انجام شد", + "edit": "ویرایش", + "edit-channel-title": "ویرایش کانال", + "edit-chats": "ویرایش گفتگوها", + "edit-community": "ویرایش جامعه", + "edit-group": "ویرایش مشخصات گروه", + "edit-name-and-image": "ویراش نام و تصویر", + "edit-name-image": "ویراش نام و تصویر", + "edit-nickname": "ویرایش نام مستعار", + "edit-profile": "ویرایش مشخصات", + "edited": "ویرایش شده", + "editing-message": "در حال ویرایش پیام", + "empty-chat-description": "هنوز هیچ پیامی در این گفتگو ارسال نشده است.", + "empty-chat-description-community": "اینجا به مدت {{quiet-hours}} ساعت خبری نبوده است.", + "empty-chat-description-one-to-one": "پیام های ارسال شده در اینجا رمزگذاری شده و فقط توسط شما خوانده می شوند", + "empty-chat-description-public": "به مدت {{quiet-hours}} اینجا خبری نبوده است. گفتگویی شروع کنید یا", + "enable": "فعال کردن", + "encrypt-with-password": "رمزگذاری با رمزعبور", + "ens-add-username": "افزودن نام کاربری", + "ens-agree-to": "موافق هستید با", + "ens-chat-settings": "تنظیمات چت", + "ens-custom-domain": "دامنه سفارشی", + "ens-custom-username-hints": "نام کاربری کامل را شامل دامنه سفارشی تایپ کنید مانند username.domain.eth", + "ens-custom-username-taken": "نام کاربری متعلق به شما نیست :(", + "ens-deposit": "سپردن پول", + "ens-dismiss-message": "برای رد کردن اینجا را کلیک کنید", + "ens-displayed-with": "پیام های شما به دیگران نمایش داده می شود", + "ens-get-name": "نام کاربری جهانی بگیرید", + "ens-locked": "نام کاربری قفل است.نمیتوانید آن را تا {{date}} آزاد کنید.", + "ens-name-content": "نام مستعار دلخواه برای کلید گپ خود که می‌توانید با استفاده از سرویس نام اتریوم ثبت نام کنید. نام‌های ENS نام‌های غیر متمرکز هستند.", + "ens-name-not-found": "نمی توان نام ENS را حل کرد", + "ens-name-title": "نام ENS", + "ens-network-restriction": "تنها روی Mainnet موجود است", + "ens-no-usernames": "هیچ نام کاربری متصل ندارید", + "ens-powered-by": "پشتیبانی شده توسط خدمات نام اتریوم", + "ens-primary-username": "نام کاربری اصلی", + "ens-register": "ثبت نام", + "ens-registration-failed": "لطفا برای ثبت نام کاربری دوباره تلاش کنید.", + "ens-registration-failure": "ثبت نام ناموفق بود", + "ens-registration-in-progress": "ثبت نام در حال انجام است...", + "ens-release-username": "آزاد کردن نام کاربری", + "ens-remove-hints": "حذف موجب انفصال نام کاربری از کلید خواهد شد.", + "ens-remove-username": "حذف نام کاربری", + "ens-saved": ".اکنون با کلید گفتگویتان مرتبط شده و میتواند در اِستاتوس استفاده شود.", + "ens-saved-title": "نام کاربری اضافه شد", + "ens-show-username": "نمایش نام کاربری ENS من در چت", + "ens-terms-header": "شرایط ثبت نام", + "ens-terms-point-1": "وجوه به مدت 1 سال نگه داشته می شود. SNT شما قفل می شود، اما خرج نمی شود.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (رجیستری ENS).", + "ens-terms-point-2": "پس از 1 سال، می توانید نام را آزاد کنید و سپرده خود را پس بگیرید یا هیچ اقدامی برای حفظ نام انجام ندهید.", + "ens-terms-point-3": "اگر شرایط قرارداد تغییر کند - برای مثال اِستاتوس قرارداد را تغییر دهد - کاربر حق دارد نام کاربری را بدون در نظر گرفتن زمان نگهداری آن آزاد کند.", + "ens-terms-point-4": "کنترل کننده قرارداد نمیتواند به وجوه سپرده شما دسترسی داشته باشد. آنها فقط می توانند به آدرسی که آنها را فرستاده منتقل شوند.", + "ens-terms-point-5": "آدرس(های) شما به صورت عمومی با نام ENS شما مرتبط خواهد شد.", + "ens-terms-point-6": "نامهای کاربری به عنوان گره های زیردامنه stateofus.eth ساخته میشوند و تحت شرایط قرارداد هوشمند ENS هستند.", + "ens-terms-point-7": "شما به قرارداد اجازه می دهید که SNT را از طرف شما منتقل کند. این تنها زمانی رخ می دهد که شما تراکنش را برای مجوز انتقال تأیید کنید.", + "ens-terms-point-8": "این شرایط توسط منطق قرارداد هوشمند در این آدرس ها تضمین شده است:", + "ens-terms-registration": "شرایط ثبت نام.", + "ens-test-message": "سلام", + "ens-transaction-pending": "تراکنش در انتظار...", + "ens-understand": "می دانم که آدرس کیف پول من به صورت عمومی به نام کاربری من متصل می شود.", + "ens-username": "نام کاربری ENS", + "ens-username-already-added": "نام کاربری در حال حاضر با کلید چت شما متصل است و می تواند در داخل استتوس مورد استفاده قرار گیرد.", + "ens-username-available": "✓ نام کاربری موجود است!", + "ens-username-connected": "این نام کاربری متعلق به شما است و با کلید چت شما مرتبط است.", + "ens-username-connected-continue": "ادامه دادن با تنظیم \"نمایش ENS نام کاربری من در گپ ها.", + "ens-username-connected-with-different-key": "ادامه اتصال به یک تراکنش نیاز دارد تا بتوانید نام کاربری خود را با کلید چت فعلی خود وصل کنید.", + "ens-username-connection-confirmation": "{{username}} پس از انجام معامله متصل می شود.", + "ens-username-hints": "حداقل 4 کاراکتر باشد. تنها حروف لاتین، اعداد و حروف کوچک.", + "ens-username-invalid": "تنها اعداد و حروف", + "ens-username-invalid-name-warning": "روند ثبت نام یکی از نام های ens شما در حالت نامعتبر به پایان رسیده است. از این نام برای تراکنش های کیف پول استفاده نکنید و با پشتیبانی ما در support@status.im تماس بگیرید", + "ens-username-owned": "نام کاربری شما تنها متعلق به شماست.", + "ens-username-owned-continue": "ادامه با این نام کاربری و با کلید گپ شما.", + "ens-username-registration-confirmation": "عالی! شما {{username}} خود را بعد از اتمام معامله خواهید داشت.", + "ens-username-registration-invalid": "هشدار! روند ثبت نام در حالت نامعتبر تمام شده است. از این نام برای تراکنش های کیف پول استفاده نکنید و با پشتیبانی ما در support@status.im تماس بگیرید", + "ens-username-taken": "نام کاربری قبلاً گرفته شده است :(", + "ens-username-you-can-follow-progress": "شما می توانید پیشرفت کیف پول خود را در بخش تاریخچه معامله دنبال کنید.", + "ens-usernames": "نامهای کاربری ENS", + "ens-usernames-details": "یک نام کاربری جهانی ثبت کنید تا براحتی توسط دیگران شناخته شوید", + "ens-want-custom-domain": "من صاحب یک نام روی یک دامنه دیگر هستم", + "ens-want-domain": "من یک دامنه stateofus.eth میخواهم", + "ens-welcome-hints": "نامهای ENS آدرس های بلند را به نامهای کاربری ممتاز بدل میکند.", + "ens-welcome-point-customize": "یک نام ENS میتواند جایگزین نام ۳ کلمه ای تصادفی در گفتگو شود. به جای {{name}} @اسمـشما باشید. ", + "ens-welcome-point-customize-title": "نام گفتگوی خود را سفارشی کنید.", + "ens-welcome-point-receive-title": "تراکنشها را در گفتگو دریافت کنید", + "ens-welcome-point-register": "برای حفظ نام برای همیشه یک بار ثبت نام کنید. پس از 1 سال می توانید نام را آزاد کنید و SNT خود را پس بگیرید.", + "ens-welcome-point-register-title": "10 SNT برای ثبت نام", + "ens-welcome-point-simplify": "می توانید به جای هش هگزادسیمال خود (0x...) وجوهی را با نام راحتتر ENS خود دریافت کنید.", + "ens-welcome-point-simplify-title": "آدرس ETH خود را ساده سازی کنید", + "ens-welcome-point-verify": "در مراحل بعدی می توانید نام کاربری خود را تأیید کرده و اضافه کنید.", + "ens-welcome-point-verify-title": "از قبل یک نام کاربری دارید؟", + "ens-your-username": "نام کاربری شما", + "ens-your-usernames": "نام های کاربری شما", + "ens-your-your-name": "نام ENS شما", + "enter-12-words": "12 کلمه عبارت دانه خود را وارد کنید، کلمات را با یک فاصله از هم جدا کنید ", + "enter-a-private-key": "یک کلید خصوصی را وارد کنید", + "enter-a-seed-phrase": "یک عبارت درج ابتدایی را وارد کنید", + "enter-address": "آدرس را وارد کنید", + "enter-contact-code": "نام کاربری یا کلید گپ را وارد کنید", + "enter-pair-code": "کد را برای اتصال دستگاههای دیگر وارد کنید", + "enter-pair-code-description": "کد جفت‌سازی میتواند از طریق یک سرویس گیرنده جفت شده اِستاتوس تنظیم شود.", + "enter-password": "رمز عبور را وارد کنید", + "enter-password-migration-prompt": "رمز عبور خود را وارد کنید تا مخاطبین، گفتگوها و تنظیمات بعلاوه کلیدهایتان منتقل شود.", + "enter-pin": "کد عبور 6 رقمی را وارد کنید", + "enter-puk-code": "کد PUK را وارد کنید", + "enter-puk-code-description": "کد عبور 6 رقمی مسدود شده است.\nلطفا کد PUK را برای باز کردن کد عبور وارد کنید.", + "enter-recipient-address-or-username": "آدرس یا نام کاربری گیرنده را وارد کنید", + "enter-seed-phrase": "عبارت دانه را وارد کنید", + "enter-url": "آدرس را وارد کنید", + "enter-user-pk": "کلید عمومی کاربر را وارد کنید", + "enter-watch-account-address": "کد QR را اسکن کنید \n یا \n آدرس را جهت ملاحظه وارد کنید", + "enter-word": "کلمه را وارد کنید", + "enter-your-code": "کدعبور 6 رقمی خود را وارد کنید", + "enter-your-password": "رمز عبور خود را وارد کنید", + "error": "خطا", + "error-unable-to-get-balance": "عدم توانایی در گرفتن تعادل", + "error-unable-to-get-prices": "خطای تبدیل ارز. صفحه را دوباره بازخوانی کنید", + "error-unable-to-get-token-balance": "عدم توانایی در دریافت موجودی توکن", + "errors": "خطاها", + "eth": "ETH", + "ethereum-node-started-incorrectly-description": "گره اتریوم با پیکربندی نادرست راه اندازی شد، برنامه برای اصلاح شرایط متوقف می شود. شناسه شبکه پیکربندی شده = {{network-id}} ، کنونی = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "گره اتریوم به طور نادرست اجرا شد.", + "etherscan-lookup": "در Etherscan جستجو کنید", + "export-account": "صدور حساب", + "export-key": "صدور کلید خصوصی", + "failed": "ناموفق", + "faq": "پرسش های متداول", + "fetch-timeline": "↓ واکشی", + "find": "پیدا کردن", + "finish": "پایان", + "finishing-card-setup": "در حال تکمیل راه اندازی کارت", + "fleet": "ناوگان", + "fleet-settings": "تنظیمات Fleet", + "follow": "دنبال کردن", + "follow-your-interests": "پرش به یک چت عمومی و دیدار با افراد جدید", + "free": "رایگان", + "from": "از جانب", + "gas-limit": "محدودیت سوخت (Gas)", + "gas-price": "قیمت بنزین", + "gas-used": "سوخت (Gas) مورد استفاده", + "generate-a-key": "تولید کلیدها", + "generate-a-new-account": "ایجاد خودکار یک حساب کاربری", + "generate-a-new-key": "ایجاد خودکار یک کلید جدید", + "generate-account": "تولید کننده خودکار کلید", + "generate-an-account": "ایجاد یک حساب کاربری", + "generate-new-key": "تولید کننده خودکار کلید", + "generating-codes-for-pairing": "> دانلود نرم افزار به کارت \n > ایجاد کدهای قفل و دستگاههای متصل", + "generating-keys": "تولید کلید...", + "generating-mnemonic": "در حال تولید عبارت درج ابتدایی", + "get-started": "شروع کنید", + "get-status-at": "دریافت استاتوس در http:\/\/status.im", + "get-stickers": "دریافت عکس برگردان ها", + "give-a-short-description": "یک شرح کوتاه دهید", + "give-a-short-description-community": "یک توضیح کوتاه دهید", + "glossary": "واژه نامه", + "go-to-settings": "رفتن به تنظیمات...", + "got-it": "متوجه شدم", + "grant-face-id-permissions": "برای اعطای مجوز Face ID مورد نیاز ، لطفا به تنظیمات سیستم خود بروید و مطمئن شوید که Status > Face ID انتخاب شده است", + "group-chat": "چت گروهی", + "group-chat-admin": "مدیر", + "group-chat-admin-added": "{{member}} اکنون مدیر است", + "group-chat-all-contacts-invited": "همه مخاطبین شما در حال حاضر در گروه هستند", + "group-chat-created": "**{{member}}** گروه **{{name}}** را ایجاد کرد", + "group-chat-decline-invitation": "نپذیرفتن دعوت", + "group-chat-member-added": "**{{member}}** دعوت شده است", + "group-chat-member-joined": "**{{member}}** به گروه پیوست", + "group-chat-member-removed": "**{{member}}** گروه را ترک کرد", + "group-chat-members-count": "{{selected}}\/{{max}} عضو", + "group-chat-name-changed": "**{{member}}** نام گروه را به **{{name}}** تغییر داد", + "group-chat-no-contacts": "شما هنوز مخاطبی ندارید.\nدوستانتان را برای گفتگو دعوت کنید", + "group-details": "جزییات گروه", + "group-info": "اطلاعات گروه", + "groups": "گروه ها", + "gwei": "Gwei", + "hash": "رمزگذاری", + "help": "کمک", + "help-capitalized": "کمک", + "help-center": "مرکز راهنمایی", + "hide-content-when-switching-apps-ios": "پیش نمایش مخفی شود", + "history": "تاریخچه", + "history-nodes": "گره های اِستاتوس", + "hold-card": "کارت را پشت تلفن همراه خود نگه دارید", + "home": "خانه", + "hooks": "قلاب", + "how-it-works": "چگونه کار می‌کند", + "http-gateway-error": "ای بابا، درخواست ناموفق بود!", + "identifier": "شناسه", + "identity-verification-request": "صحت سنجی هویت", + "image-remove-current": "حذف تصویر کنونی", + "image-source-gallery": "انتخاب از گالری", + "image-source-make-photo": "دوربین", + "image-source-title": "ویرایش تصویر", + "import": "وارد کردن", + "import-community": "وارد کردن جامعه", + "import-community-title": "وارد کردن جامعه", + "in-contacts": "در میان مخاطبین", + "incoming": "ورودی", + "incoming-transaction": "تراکنش دریافتی", + "incorrect-code": [ + "str", + "با عرض پوزش کد وارد شده اشتباه بود، لطفا دوباره تلاش کنید" + ], + "initialization": "مقداردهی اولیه", + "install": "↓ نصب", + "intro-message1": "به استتوس خوش آمدید! \n برای تنظیم گذرواژه و شروع به کار، روی این پیام ضربه بزنید", + "intro-privacy-policy-note1": "استاتوس از اطلاعات شخصی شما قصد سود آوری ندارد. با ادامه این فرآیند، شما با توافقنامه ما موافق هستید ", + "intro-privacy-policy-note2": "سیاست حفظ حریم خصوصی", + "intro-text": "وضعیت دروازه ورود به وب غیرمتمرکز است", + "intro-text1": "چت بر روی یک شبکه همکار رمزگذاری شده ، که در آن پیام ها نمی توانند سانسور یا هک شوند", + "intro-text2": "ارسال و دریافت دارایی های دیجیتالی در هر نقطه از جهان-بدون نیاز به داشتن حساب بانکی", + "intro-text3": "کاوش بازی ها ، مبادلات و شبکه های اجتماعی که در آن شما به تنهایی داده های خود را دارید", + "intro-title1": "ارتباطات واقعا خصوصی", + "intro-title2": "کیف پول رمزنگاری شده امن", + "intro-title3": "برنامه های غیر متمرکز", + "intro-wizard-text1": "مجموعه ای از کلیدها حساب شما را کنترل می کنند. کلیدهای شما روی تلفن شما هستند ، بنابراین فقط می توانید از آنها استفاده کنید", + "intro-wizard-text2": "هر کلید برای یک گپ است. این عبارت فقط قابل خواندن است و قابل تغییر نیست.", + "intro-wizard-text3": "اگر یک Keycard دارید ، کلیدهای امنیتی خود را برای امنیت بیشتر ذخیره کنید.", + "intro-wizard-text4": "ایمن سازی و رمزگذاری کلیدها", + "intro-wizard-text6": "استاتوس در مورد پیام های جدید به شما اطلاع می دهد. می توانید تنظیمات برگزیده اعلان خود را بعداً در تنظیمات ویرایش کنید", + "intro-wizard-title-alt4": "ایجاد یک رمزعبور", + "intro-wizard-title-alt5": "تأیید رمزعبور", + "intro-wizard-title1": "کلیدهای خود را دریافت کنید", + "intro-wizard-title2": "یک نام چت انتخاب کنید", + "intro-wizard-title3": "انتخاب کلید ذخیره سازی", + "intro-wizard-title4": "ایجاد یک کد عبور 6 رقمی", + "intro-wizard-title5": "تأیید کد عبور", + "intro-wizard-title6": "فعال کردن اعلان ها", + "invalid-address-qr-code": "کد QR اسکن شده حاوی یک آدرس معتبر نیست", + "invalid-format": "قالب نامعتبر\nباید {{format}} باشد", + "invalid-key-confirm": "اعمال", + "invalid-key-content": " پایگاه داده Multiaccount ها به دلیل خراب شدن فایل رمزگذاری نمی شود. هیچ راهی برای بازیابی آن وجود ندارد. اگر دکمه \"لغو\" را فشار دهید ، هیچ اتفاقی نخواهد افتاد. اگر دکمه \" {{erase-multiaccounts-data-button-text}} \" را فشار دهید ، پایگاه داده multiaccount ها حذف می شود و شما قادر به باز کردن multiaccount خواهید بود. تمام داده های multiaccount ها از بین می روند.", + "invalid-number": "شماره نامعتبر", + "invalid-pairing-password": "رمز جفت سازی نامعتبر", + "invalid-range": "قالب نامعتبر است، باید بین {{min}} و {{max}} باشد", + "invalid-username-or-key": "نام کاربری یا کلید گفتگوی نامعتبر ", + "invite-button": "دعوت از دوستان", + "invite-chat-accept": "پذیرفتن", + "invite-chat-accept-join": "پذیرش و پیوستن", + "invite-chat-intro": "در اینجا مقداری رمزارز برای شروع کار وجود دارد! از آن برای ثبت نام یک نام ENS یا خرید یک بسته استیکر استفاده کنید", + "invite-chat-name": "ارجاع دوست", + "invite-chat-pending": "در انتظار", + "invite-chat-rule": "با پذیرفتن این، دوست شما هم پاداش ارجاع را به صورت رمزارز دریافت خواهد کرد", + "invite-chat-starter-pack": "بستۀ آغازگر", + "invite-contacts": "دعوت اشخاص از فهرست تماس", + "invite-friends": "دعوت از دوستان", + "invite-friends-to-status": "دعوت دوستان به استاتوس", + "invite-instruction-fifth": "شما می توانید انتخاب کنید که پاداش ارجاع خود را در هر زمان بازخرید کنید.", + "invite-instruction-first": "شما برای دانلود و پیوستن دوستتان به استتس، یک لینک دعوت منحصر به فرد را به او ارسال می‌کنید", + "invite-instruction-fourth": "شما جایزۀ ارجاع خود را دریافت میکنید و دوستتان بستۀ آغازگر را دریافت می‌کند", + "invite-instruction-second": "دوست شما استتس را دانلود و یک حساب کاربری (در اندروید) ایجاد می‌کند", + "invite-instruction-third": "وقتی دوستتان ارجاع شما را تائید کند، گفتگو با او آغاز می‌شود", + "invite-people": "دعوت اشخاص", + "invite-people-from-contacts": "دعوت اشخاص از فهرست تماسها", + "invite-privacy-policy-public": "اِستاتوس را از طریق پیوند ارجاع نصب کردید. با پیوستن به این گفتگو، ارجاع دهنده خود را نسبت می دهید و با آن موافقت می کنید", + "invite-privacy-policy1": "در صورت پذیرش شما با برنامۀ ارجاع موافق هستید", + "invite-privacy-policy2": "قوانین و شرایط.", + "invite-public-chat-home": "دعوت نامه ارجاع", + "invite-public-chat-intro": "در اینجا مقداری رمزارز برای شروع کار وجود دارد! از آن برای ثبت نام یک نام ENS یا خرید یک بسته عکس برگردان استفاده کنید", + "invite-receive-account": "حساب کاربری برای دریافت پاداش ارجاع شما", + "invite-reward": "برای هر دوستی که دعوت می‌کنید، {{value}} کسب کنید!", + "invite-reward-friend": "دوست:", + "invite-reward-friend-description": "دوست شما برای شروع کار، یک بستۀ آغازگر شامل مقداری {{reward}} دریافت می‌کند", + "invite-reward-friend-name": "بستۀ آغازگر", + "invite-reward-you": "شما", + "invite-reward-you-description": "دعوت از یک دوست و دریافت {{reward}} به عنوان نشانه ای از قدردانی ما.", + "invite-reward-you-name": "پاداش ارجاع", + "invite-select-account": "برای دریافت پاداش ارجاعتان یک حساب کاربری انتخاب کنید", + "invite-warning": "این تبلیغ فقط برای کاربران یک دستگاه Android که ساکن ایالات متحده نیستند معتبر است. دوستتان باید در عرض 7 روز ارجاع را تایید کند", + "invited": "دعوت شده", + "italic": "مورب", + "join": "پیوستن", + "join-a-community": "یا به یک جامعه بپیوندند", + "join-group-chat": "پیوستن به گروه", + "join-group-chat-description": "{{username}} شما را دعوت به پیوستن به {{group-name}} کرد", + "join-me": "سلام به من در اِستاتوس بپیوندید:{{url}}", + "join-new-private-chat": "یک گفتگوی خصوصی جدید را آغاز کنید", + "join-new-public-chat": "پیوستن به چت عمومی", + "joined-group-chat-description": "شما به واسطه دعوت {{username}} به {{group-name}}پیوسته اید", + "key": "کلید", + "keycard-access-reset": "دسترسی به کارت کلید بازنشانی شده است", + "keycard-awaiting-description": " کارت را برای خوانده شدن توسط درگاه NFC موبایل در اطراف موبایل حرکت دهید.", + "keycard-awaiting-title": "در حال جستحو ....", + "keycard-cancel-setup-text": "با این کار نصب keycard لغو می شود. قویا توصیه می‌گردد به منظور استفاده از keycard ، نصب را به پایان برسانید. آیا واقعاً می خواهید عملیات لغو شود؟", + "keycard-cancel-setup-title": "عملیات خطرناک", + "keycard-connected-description": "کارت را همچنان نگه دارید", + "keycard-connected-title": "متصل شده", + "keycard-desc": "صاحب یک Keycard هستید؟ کلیدهای خود را روی آن ذخیره کنید. شما برای معاملات به آن احتیاج دارید", + "keycard-dont-ask-card": "برای ورود کارت را درخواست نکن", + "keycard-error-description": "برای ادامه، کارت را دوباره وصل کنید", + "keycard-error-title": "اتصال شما قطع شده است", + "keycard-has-multiaccount-on-it": "این کارت در حال حاضر چند حساب در آن است. اگر می خواهید آن را تغییر دهید ، ابتدا وارد شوید و کارت خود را بازنشانی کنید. اگر می خواهید چند حساب keycard را وارد کنید ، لطفا از \"افزودن چندحساب موجود\" استفاده نمایید", + "keycard-init-description": "در ادامه کارت را پشت تلفن همراه خود قرار دهید.", + "keycard-init-title": "در حال جستجوی کارت‌ها ...", + "keycard-onboarding-finishing-header": "به پایان رسید", + "keycard-onboarding-intro-header": "ذخیره کلید های خود بر روی Keycard", + "keycard-onboarding-intro-text": "آماده شوید ، این فرآیند ممکن است چند دقیقه طول بکشد ، اما این مهم است که حساب خود را ایمن کنید", + "keycard-onboarding-pairing-header": "اتصال کارت...", + "keycard-onboarding-preparing-header": "در حال آماده سازی کارت...", + "keycard-onboarding-puk-code-header": "کدها را یادداشت کرده و در مکان امنی نگه داری کنید", + "keycard-onboarding-recovery-phrase-description": "برای برگرداندن کلید خود به این عبارت دانه نیاز دارید. آن را یادداشت کنید. آن را ایمن ، آفلاین و جدا از این دستگاه نگه دارید.", + "keycard-onboarding-recovery-phrase-header": "از دانه یک پشتیبان تهیه کنید", + "keycard-onboarding-recovery-phrase-text": "فقط برای شما. این یک دانه جادویی است که برای تولید کلید شما استفاده می شود.", + "keycard-onboarding-start-header": "برای ادامه عملیات، کارت را پشت تلفن همراه خود قرار دهید.", + "keycard-onboarding-start-step1": "ایجاد کد عبور", + "keycard-onboarding-start-step1-text": "حدود 1 دقیقه. یک رمز عبور 6 رقمی برای رمزگذاری کلید های خود ایجاد کنید", + "keycard-onboarding-start-step2": "PUK و کد همتاسازی را یادداشت کنید", + "keycard-onboarding-start-step2-text": "حدود 1 دقیقه. شما برای این کار به یک قلم و کاغذ احتیاج دارید", + "keycard-onboarding-start-step3": "از دانه نسخه پشتیبان تهیه کنید", + "keycard-onboarding-start-step3-text": "حدود 1 دقیقه. یک قلم و کاغذ نیز لازم است", + "keycard-onboarding-start-text": "در طول فرآیند کارت را نزدیک دستگاه نگه دارید. فرآیند نصب حدود ۴ دقیقه زمان می‌برد", + "keycard-processing-description": "کارت را همچنان نگه دارید", + "keycard-processing-title": "در حال پردازش...", + "keycard-recovery-intro-button-text": "شروع بازیابی", + "keycard-recovery-intro-header": "بازیابی کلیدهای ذخیره شده روی keycard", + "keycard-recovery-intro-text": "اگر قبلا کلیدهایی را با استفاده از keycard ایجاد کرده اید و اکنون می خواهید از آن کلیدها در این دستگاه استفاده کنید", + "keycard-recovery-no-key-header": "چیزی برای \n بازیابی وجود ندارد", + "keycard-recovery-no-key-text": "Keycard شما هیچ کلید ذخیره شده روی آن ندارد. برای استفاده از آن ، یک کلید جدید تولید کنید و Keycard خود را برای ذخیره کردن کلید انتخاب کنید", + "keycard-recovery-phrase-confirm-header": "عبارت دانه را تأیید کنید", + "keycard-recovery-phrase-confirmation-text": "شانس دوم نخواهید داشت! اگر دستگاه خود را گم کرده یا تغییر دهید ، نمی توانید به کلیدها یا دارایی های مرتبط با آن دسترسی پیدا کنید ، مگر اینکه عبارت دانه خود را داشته باشید.", + "keycard-recovery-success-header": "کلیدهای شما \n با موفقیت بازیابی شدند", + "keycard-redeem-tx-desc": "برای امضا و دریافت دارایی‌ها روی کارت ضربه بزنید", + "keycard-success-description": "اکنون می‌توانید کارت را حذف کنید", + "keycard-success-title": "موفقیت", + "language": "زبان", + "learn-more": "بیشتر بدانید", + "learn-more-about-keycard": "درباره Keycard بیشتر بدانید", + "leave": "ترک کردن", + "leave-chat": "ترک کردن گفتگو", + "leave-chat-confirmation": "آیا مطمئن هستید که می خواهید چت را ترک کنید؟", + "leave-community": "ترک جامعه", + "leave-community-message": "از رفتنت ناراحت خواهیم شد، اما یادت باشد، هر زمان که بخواهی می‌توانی برگردی!", + "leave-community?": "ترک جامعه؟", + "leave-confirmation": "ترک کردن گفتگو", + "leave-group": "ترک گروه", + "leave-group?": "ترک گروه؟", + "left": "ترک کرد", + "les-ulc": "LES\/ULC", + "lets-go": "بزن بریم", + "light": "روشن", + "lock-app-with": "قفل کردن برنامه با", + "log-level": "سطح لاگ", + "log-level-settings": "تنظیمات سطح لاگ", + "logout": "خروج", + "logout-are-you-sure": "آیا مطمئنید می خواهید\nبرای خروج از سیستم ؟", + "logout-title": "خروج از سیستم ؟", + "looking-for-cards": "در حال جستجوی کارت‌ها ...", + "mail-should-be-configured": "سرویس استفاده از ایمیل باید تنظیم شود", + "mailserver-address": "آدرس mailserver", + "mailserver-connection-error": "اتصال به Mailserver مقدور نیست", + "mailserver-content": "گره ای در شبکه استاتوس است که پیام‌ها را ذخیره و مسیریابی می‌کند، به مدت 30 روز.", + "mailserver-details": "جزئیات Mailserver", + "mailserver-reconnect": "اتصال به mailserver امکان پذیر نیست. برای اتصال مجدد ضربه بزنید", + "mailserver-title": "Mailserver", + "main-currency": "ارز اصلی", + "main-networks": "شبکه های اصلی", + "main-wallet": "کیف پول اصلی", + "mainnet-network": "شبکه اصلی", + "manage-members": "مدیریت اعضا", + "mark-all-read": " به عنوان خوانده شده همه را علامتگذاری کن", + "mark-as-read": "نشانگذاری به عنوان خوانده شده", + "mark-untrustworthy": "علامتگذاری به عنوان غیر قابل اعتماد", + "mark-user-untrustworthy": "علامتگذاری {{username}} به عنوان غیر قابل اعتماد", + "maybe-later": "شاید بعدا", + "member-ban": "ممنوع کردن عضو", + "member-kick": "اخراج عضو", + "members": { + "one": "عضو", + "other": "عضو" + }, + "members-active": { + "one": "عضو", + "other": "عضو" + }, + "members-active-none": "بدون عضو", + "members-count": "{{count}} نفر", + "members-label": "اعضا", + "members-title": "اعضا", + "membership-approval": "اجبار تایید عضو", + "membership-approval-description": "عضویت در جامعه شما آزاد است،اما اعضای جدید باید اول توسط سازنده جامعه تایید شوند.", + "membership-button": "نیازمندی عضویت", + "membership-ens": "اجبار نام کاربری ENS", + "membership-ens-description": "برای عضویت در جامعه شما نیاز به نام کاربری ENS است.", + "membership-free": "بدون نیازمندی", + "membership-free-description": "عضویت در جامعه شما آزاد است", + "membership-invite": "اجبار دعوت از عضوی دیگر", + "membership-invite-description": "تنها با دعوت از اعضای موجود انجمن می توان به انجمن شما ملحق شد", + "membership-none": "هیچ کدام", + "membership-none-placeholder": "میتوانید شرایط خاصی برای اعضای جدید قبل عضویت ایجاد کنید. این حالت را میتوانید بعدا تغییر دهید.", + "membership-request-pending": "درخواست عضویت در انتظار", + "membership-requests": "درخواست های عضویت", + "membership-title": "نیازمندی عضویت", + "message": "پیام", + "message-not-sent": "پیام ارسال نشد", + "message-options-cancel": "لغو", + "message-reply": "پاسخ", + "messages-from-contacts-only-subtitle": "تنها کسانی که مخاطب شما هستند میتوانند گفتگوی جدید آغاز کرذه یا شما را به گروهی اضافه کنند", + "migration-successful": "مهاجرت موفق بود.", + "migration-successful-text": "حساب با موفقیت به کارت هوشمند منتقل شد.", + "mobile-network-use-wifi": "فقط Wi-Fi", + "more": "بیشتر", + "multiaccount-exists-content": "این چندحساب از قبل موجود است. فقط باید آن را از حالت قفل دربیاورید.", + "multiaccount-exists-title": "این حساب کاربری در حال حاضر وجود دارد", + "multiaccounts-recover-enter-phrase-text": "عبارت اعداد ۱۲ ۱۵ ۱۸ ۲۱ یا ۲۴ را به همراه یک فاصله در میان هر عبارت عددی وارد کنید", + "multiaccounts-recover-enter-phrase-title": "عبارت تولید گر خود را واردکنید", + "mute-chat": "بیصدا کردن گفتگو", + "mute-community": "بیصدا کردن جامعه", + "mute-group": "بیصدا کردن گروه", + "my-accounts": "حسابهای من", + "my-accounts-empty": "حساب های در دسترس شما در اینجا نشان داده می شود", + "name": "نام", + "name-optional": "نام (اختیاری)", + "name-your-channel": "کانال را نامگذاری کنید", + "name-your-channel-placeholder": "نام کانال", + "name-your-community": "جامعه را نامگذاری کنید", + "name-your-community-placeholder": "یک نام گیرا", + "need-help": "نیاز به راهنمایی دارید ؟", + "network": "شبکه", + "network-chain": "شبکه های زنجیره ای", + "network-details": "جزئیات شبکه", + "network-id": "شناسه شبکه", + "network-info": "اطلاعات شبکه", + "network-settings": "تنظیمات شبکه", + "new": "جدید", + "new-category": "دسته بندی جدید", + "new-chat": "گفتگوی جدید", + "new-community-title": "جامعه جدید", + "new-contact": "مخاطب جدید", + "new-contract": "قرارداد جدید", + "new-group": "گروه جدید", + "new-group-chat": "چت گروهی جدید", + "new-network": "شبکه جدید", + "new-public-group-chat": "پیوستن به چت عمومی", + "next": "بعدی", + "no": "نه", + "no-contacts": "هنوز هیچ مخاطبی موجود نیست", + "no-messages": "هیچ پیامی موجود نیست", + "no-pinned-messages-community-desc": "این کانال هیچ پیام سنجاق شده ای ندارد.", + "no-pinned-messages-desc": "این گفتگو هیچ پیام سنجاق شده ای ندارد.", + "node-version": "ورژن گره", + "nonce": "Nonce", + "none": "هیچ کدام", + "not-applicable": "برای معاملات بدون امضا قابل استفاده نیست", + "not-enough-snt": "SNT کافی نیست", + "not-keycard-text": "کارت مورد استفاده شما یک Keycard نیست. برای استفاده از آن، شما باید یک Keycard خریداری کنید", + "not-keycard-title": "این یک Keycard نیست", + "notifications": "اعلان ها", + "notify": "مطلع ساختن", + "off": "خاموش", + "off-status-tree": "بیرون از وضعیت درختی", + "offline": "آفلاین", + "ok": "تایید", + "ok-continue": "بسیار خوب ، ادامه دهید", + "ok-got-it": "باشه ، فهمیدم", + "ok-save-pass": "بسیار خوب ، ذخیره رمز عبور", + "on": "روشن", + "on-status-tree": "در وضعیت درختی", + "online": "آنلاین", + "only-mentions": "تنها @ذکر شده ها", + "open": "باز کن", + "open-dapp": "باز کردن برنامه غیرمتمرکز", + "open-home": "باز کن", + "open-membership": "عضویت باز", + "open-nfc-settings": "تنظیمات NFC را باز کنید", + "or": "یا", + "outgoing": "خروجی", + "outgoing-transaction": "تراکنش ارسال", + "pair": "دستگاه های متصل شده", + "pair-code-placeholder": "کد همگام سازی", + "pair-this-card": "همگام سازی این کارت", + "pair-this-device-description": "برای همگام سازی مخاطبین و گپ های بین آنها ، دستگاه های خود را متصل کنید", + "paired-devices": "دستگاه های متصل شده", + "pairing-go-to-installation": "رفتن به تنظیمات همتاسازی", + "pairing-maximum-number-reached-content": "لطفاً قبل از فعال کردن دستگاه جدید ، یکی از دستگاههای خود را غیرفعال کنید.", + "pairing-maximum-number-reached-title": "تعداد دستگاهها به حداکثر رسیده است", + "pairing-new-installation-detected-title": "دستگاه جدید شناسایی شد", + "pairing-please-set-a-name": "لطفا یک نام برای دستگاه خود تنظیم کنید.", + "passphrase": "عبارت عبور", + "password": "کلمه عبور", + "password-description": "حداقل 6 حرف. رمز عبور شما از کلیدهای شما محافظت می کند. برای باز کردن استاتوس و معامله به آن نیاز دارید.", + "password-placeholder": "رمز عبور...", + "password-placeholder2": "تأیید رمزعبور", + "password_error1": "کلمات عبور مطابقت ندارند", + "paste-json": "چسباندن JSON", + "peer-content": "دستگاهی متصل به شبکه چت استاتوس. هر کاربر بسته به تعداد دستگاه خود ، می تواند یک یا چند دستگاه همسان را نمایندگی کند.", + "peer-title": "همتا", + "pending": "انتظار", + "pending-requests": "درخواست های در انتظار", + "permissions": "مجوزها", + "phone-e164": "بین المللی 1", + "photos-access-error": "برای اعطای مجوز درسترسی به عکس ها، لطفا وارد تنظیمات گوشی شده و مطمئن شوید قسمت استاتوس > عکس ها انتخاب شده است.", + "pin-mismatch": "کد عبور اشتباه", + "pin-one-attempt": "یک تلاش", + "pin-to-channel": "سنجاق به کانال", + "pinned-messages-2": "پیامهای سنجاق شده", + "preference": "اولویت", + "privacy": "حریم خصوصی", + "privacy-and-security": "حریم خصوصی و امنیت", + "privacy-policy": "سیاست حفظ حریم خصوصی", + "private-key": "کلید خصوصی", + "private-notifications": "اعلان‌های خصوصی", + "private-notifications-descr": "استاتوس در مورد پیام‌های جدید به شما اطلاع می‌دهد. می‌توانید تنظیمات برگزیده اعلان خود را بعداً در تنظیمات ویرایش کنید", + "profile": "مشخصات", + "profile-deleted-content": "پروفایل شما با موفقیت حذف شد", + "profile-deleted-keycard": "اکنون میتوانید جفت کلید دیگری روی کارت کلیدتان بازیابی کنید ", + "profile-deleted-title": "پروفایل حذف شد", + "profile-details": "جزئیات پروفایل", + "profile-pic-pick": "انتخاب از گالری", + "profile-pic-remove": "برداشتن عکس", + "profile-pic-take": "گرفتن عکس", + "public": "عمومی", + "public-chat": "چت عمومی", + "public-chat-description": "به گپ های عمومی بپیوندید! هر کس می تواند یکی از آنها را شروع کند.", + "public-chats": "چت های عمومی", + "public-group-status": "عمومی", + "public-group-topic": "عنوان", + "public-key": "کلید عمومی", + "puk-code": "کد PUK", + "puk-code-explanation": "اگر رمز عبور 6 رقمی خود را فراموش کرده یا 3 بار به اشتباه آن را وارد کنید ، برای باز کردن قفل کارت خود به این کد احتیاج دارید.", + "re-encrypt-key": "رمزگذاری مجدد کلیدها", + "rearrange-categories": "مرتب سازی دسته ها", + "receive": "دریافت", + "receive-transaction": "دریافت تراکنش", + "received": "دریافت شده", + "recent": "استتوس ها اخیر", + "recent-empty": "آدرسهای اخیراً استفاده شده در اینجا ظاهر می شوند", + "recent-recipients": "مخاطبین", + "recipient": "گیرنده", + "recipient-code": "آدرس گیرنده را وارد کنید", + "recover": "بازیابی", + "recover-key": "دسترسی به کلید های موجود", + "recover-keycard-multiaccount-not-supported": "بازیابی چند حساب keycard با کلمه عبور پشتیبانی نمی شود", + "recover-with-keycard": "بازیابی با Keycard", + "recovering-key": "دسترسی به کلیدها ...", + "recovery-confirm-phrase": "تایید عبارت دانه", + "recovery-phrase": "عبارت دانه", + "recovery-success-text": "شما باید یک کد یا رمزعبور جدید ایجاد کنید تا کلیدهایتان مجددا رمزگذاری شود", + "recovery-typo-dialog-description": "اگر کلمات اشتباه را وارد کنید ، به جای بازیابی یک حساب کاربری جدید multiaccount ایجاد خواهید کرد.", + "recovery-typo-dialog-title": "آیا عبارت دانه صحیح است؟", + "redeem-amount": "{{quantity}} پاداش موجود است", + "redeem-now": "برداشت کنید", + "redeem-success": "برداشت پاداش موفقیت آمیز بود!", + "remember-me": "مرا به خاطر بسپار", + "remind-me-later": "این را دوباره به من نشان بده", + "remove": "حذف", + "remove-from-chat": "حذف از چت", + "remove-from-contacts": "حذف از مخاطبین", + "remove-from-contacts-text": "با حذف یک کاربر از لیست مخاطبین ، آدرس کیف پولتان از آنها پنهان نمی‌شود ", + "remove-network": "حذف شبکه", + "removed": "حذف شده", + "rename": "تغییر نام", + "repeat-pin": "کد عبور 6 رقمی جدید را تکرار کنید", + "replied": "پاسخ داده شده", + "request-access": "درخواست دسترسی", + "request-feature": "درخواست یک ویژگی", + "request-transaction": "درخواست معامله", + "required-field": "گزینه مورد نیاز", + "resend-message": "ارسال مجدد", + "rpc-url": "RPC URL", + "save": "ذخیره", + "save-password": "ذخیره رمز عبور", + "save-password-unavailable": "برای ذخیره کلمه عبور روی تلفن خود رمز بگذارید", + "scan-qr": "اسکن کد QR", + "scan-qr-code": "اسکن کد QR با یک آدرس کیف پول", + "search-contacts": "جستجو مخاطبین", + "search-discover-communities": "جستجوی دسته ها و جوامع", + "search-no-chat-found": "هیچ نتیجه‌ای برای جستجو یافت نشد. منظور شما این است که", + "secret-keys-confirmation-title": "کدها را یادداشت نمودید؟", + "security": "امنیت", + "seed-phrase-content": "مجموعه ای از کلمات ، که به طور تصادفی از لیست استاندارد BIP39 انتخاب شده اند و برای بازیابی یا دسترسی به حساب اتریوم خود در سایر کیف پول ها و دستگاه ها استفاده می شوند. همچنین در اکوسیستم رمزنگاری اصطلاح \"یادآوری\" ، \"عبارت بازیابی\" یا \"پشتیبان گیری از کیف پول\" گفته می شود. اکثر برنامه های رمزنگاری برای تولید حساب کاربری از همین استاندارد استفاده می کنند.", + "seed-phrase-placeholder": "عبارت بازیابی...", + "seed-phrase-title": "عبارت موثر در ایجاد", + "select": "انتخاب", + "select-account": "انتخاب حساب", + "select-account-dapp": "حسابی را که می خواهید با Dapp استفاده کنید انتخاب کنید", + "select-account-first": "ابتدا یک حساب کاربری انتخاب کنید", + "select-chat": "برای شروع پیام ، گپ را انتخاب کنید", + "selected": "انتخاب شده", + "send": "ارسال", + "send-message": "ارسال پیام", + "send-reply": "ارسال پاسخ", + "send-request": "ارسال درخواست", + "send-request-amount": "مقدار", + "send-request-amount-max-decimals": "حداکثر تعداد اعشار {{asset-decimals}}", + "send-request-unknown-token": "توکن ناشناخته - {{asset}}", + "send-sending-to": "به {{recipient-name}}", + "send-transaction": "ارسال", + "sent": "ارسال شد", + "sent-at": "ارسال شده در", + "set-a-topic": "ایجاد یک موضوع", + "set-currency": "تنظیم ارز پیش فرض", + "set-custom-fee": "انتخاب کارمزد به صورت دستی", + "set-dapp-access-permissions": "تنظیم مجوزهای دسترسی DApp", + "set-max": "تنظیم حداکثر", + "settings": "تنظیمات", + "share": "اشتراک گذاری", + "share-address": "به اشتراک گذاری آدرس", + "share-chat": "اشتراک گذاری چت", + "share-community": "به اشتراک گذاری جامعه", + "share-contact-code": "به اشتراک گذاشتن کلید چت من", + "share-dapp-text": "بررسی این DApp من در استاتوس: {{link}}", + "share-invite-link": "اشتراک گذاری پیوند دعوت", + "share-link": "اشتراک گذاری لینک", + "share-my-profile": "اشتراک گذاری پروفایل من", + "share-profile-link": "اشتراک گذاری لینک پروفایل", + "share-public-chat-text": "این گپ عمومی را در برنامه استاتوس بررسی کنید: {{link}}", + "shared": "مشترک", + "sharing-copied-to-clipboard": "کپی شده", + "sharing-copy-to-clipboard": "ذخیره در کلیپ بورد", + "sharing-share": "اشتراک گذاری...", + "shell-placeholder-title": "برنامه های شما اینجا اجرا خواهد شد", + "show-less": "نمایش کمتر", + "show-more": "نمایش بیشتر", + "show-qr": "نمایش کد QR", + "sign-and-send": "رمز امضا و ارسال", + "sign-anyway": "به هر حال امضا کنید", + "sign-in": "باز کردن", + "sign-message": "پیام را امضا کنید", + "sign-out": "خروج از سیستم", + "sign-request-failed": "امضای پیام ناموفق بود", + "sign-with": "ورود به سیستم", + "sign-you-in": "امضای شما در...", + "skip": "پرش", + "soon": "به زودی", + "specify-name": "تعیین نام", + "specify-network-id": "تعیین شناسه شبکه", + "specify-rpc-url": "یک URL RPC مشخص کنید", + "start-chat": "شروع چت", + "start-conversation": "شروع مکالمه", + "start-group-chat": "شروع گپ گروهی", + "start-new-chat": "شروع چت جدید", + "starter-pack-coming": "بسته شروع در راه است", + "starter-pack-coming-description": "ممکن است چند دقیقه تا چند ساعت طول بکشد", + "starter-pack-received": "بستۀ آغازگر دریافت شد", + "starter-pack-received-description": "در اینجا مقداری رمزارز برای شروع کار وجود دارد! برای به دست آوردن استیکرها و یک نام ENS از آن استفاده کنید، و اپ‌های غیرمتمرکز را امتحان کنید", + "status": "استتوس", + "status-confirmed": "تایید شده", + "status-hardwallet": "کیف پول استتوس", + "status-not-sent-click": "تأیید نشده. برای گزینه های بیشتر کلیک کنید", + "status-not-sent-tap": "تأیید نشده. برای گزینه های بیشتر ضربه بزنید", + "status-pending": "در انتظار", + "status-sent": "ارسال شد", + "status-tx-not-found": "TX یافت نشد", + "step-i-of-n": "مرحله {{step}} از {{number}}", + "storage": "ذخیره سازی", + "strikethrough": "خط زدن", + "submit": "ارسال", + "submit-bug": "ارسال یک اشکال", + "success": "موفقیت", + "sync-all-devices": "همگام سازی همه دستگاه ها", + "sync-in-progress": "در حال همگام سازی...", + "sync-settings": "تنظیمات همگام سازی", + "sync-synced": "همگام سازی شده", + "syncing-devices": "همگام سازی ...", + "system": "سامانه", + "test-networks": "شبکه های آزمایشی", + "text-input-disabled": "لطفا یک لحظه صبر کنید...", + "this-device": "این دستگاه", + "this-device-desc": "کلیدهای شما رمزگذاری و با امنیت کامل در دستگاه شما ذخیره شد", + "this-is-you-signing": "امضای شما", + "to": "به", + "to-enable-biometric": "برای فعال کردن {{bio-type-label}}، شما باید رمز ورود خود را در صفحه غیرقفل ذخیره کنید", + "to-encrypt-enter-password": "برای رمزگذاری حساب، رمز ورود خود را وارد کنید", + "topic-name-error": "فقط از حروف کوچک (a تا z) ، اعداد و خط تیره استفاده کنید (-). از کلیدهای چت استفاده نکنید", + "transaction": "تراکنش", + "transaction-declined": "تراکنش رد شد", + "transaction-description": "پس از 12 تاییدیه در شبکه ، آن را بررسی کنید.", + "transaction-details": "جزئیات تراکنش", + "transaction-failed": "تراکنش ناموفق", + "transaction-history": "تاریخچه معامله", + "transaction-request": "درخواست تراکنش", + "transaction-sent": "تراکنش ارسال شد", + "transactions": "تراکنش ها", + "transactions-filter-select-all": "انتخاب همه", + "transactions-filter-title": "تاریخچه فیلتر", + "transactions-history": "تاریخچه تراکنش", + "transactions-history-empty": "در تاریخچه شما هنوز هیچ تراکنشی ثبت نشده است", + "transactions-load-more": "بارگذاری بیشتر", + "transactions-sign": "امضا", + "tribute-required-by-multiaccount": "{{multiaccount-name}} برای شروع گپ به SNT نیاز دارد.", + "tribute-to-talk-finish-desc": "از الان به بعد شما تنها پیام افرادی که حساب کاربری تایید شده داشته باشند را دریافت خواهید کرد.", + "try-keeping-the-card-still": "کارت را همچنان نگه دارید", + "turn-nfc-description": "NFC در دستگاه شما غیرفعال است. می توانید آن را در تنظیمات فعال کنید", + "turn-nfc-on": "NFC را برای ادامه روشن کنید", + "tx-fail-description1": "این تراکنش احتمالاً شکست خواهد خورد. با پذیرش ریسک، هزینه شبکه را به صورت سفارشی تنظیم کنید.", + "tx-fail-description2": "این تراکنش احتمالاً شکست خواهد خورد. با پذیرش ریسک، هزینه شبکه را به صورت سفارشی تنظیم کنید.", + "type": "نوع", + "type-a-message": "پیامی بنویسید...", + "type-something": "چیزی بنویسید", + "ulc-enabled": "ULC فعال است", + "unable-to-read-this-code": "خواندن این کد مقدور نیست", + "unblock": "رفع انسداد", + "unknown-status-go-error": "خطای وضعیت ناشناخته", + "unlock": "باز کردن", + "unmute-chat": "باصدا کردن گفتگو", + "unmute-community": "صدادار کردن جامعه", + "unpaired-keycard-text": "Keycard که به آن ضربه زدید با این تلفن همراه نیست", + "unpaired-keycard-title": "به نظر می رسد کارت شما بدون پرداخت هزینه بوده است", + "unpin-from-channel": "برداشتن سنجاق کانال", + "untrustworthy": "غیر قابل اعتماد", + "update-to-listen-audio": "برای شنیدن یک پیام صوتی در اینجا، اپلیکیشن خود را به آخرین نسخه بروزرسانی کنید", + "update-to-see-image": "برای دیدن یک تصویر زیبا در اینجا، اپلیکیشن خود را به آخرین نسخه بروزرسانی کنید", + "update-to-see-sticker": "برای دیدن یک استیکر زیبا در اینجا، اپلیکیشن خود را به آخرین نسخه بروزرسانی کنید", + "url": "آدرس", + "usd-currency": "دلار آمریکا", + "use-valid-contact-code": "لطفا یک کلید چت یا نام کاربری معتبر را وارد یا اسکن کنید", + "validation-amount-invalid-number": "مقدار وارد شده صحیح نیست", + "version": "نسخه برنامه", + "view-community-rules": "نمایش قوانین جامعه ", + "view-cryptokitties": "نمایش در CryptoKitties", + "view-cryptostrikers": "مشاهده در CryptoStrikers", + "view-details": "مشاهده جزئیات", + "view-etheremon": "مشاهده در Etheremon", + "view-members": "نمایش اعضا", + "view-pinned-messages": "نمایش پیام های سنجاق شده", + "view-profile": "مشاهده پروفایل", + "view-signing": "مشاهده عبارت امضا", + "view-superrare": "مشاهده در SuperRare", + "vote-to-feature": "به این جامعه برای مطرح شدن آن رای دهید", + "waku-bloom-filter-mode": "حالت فلیتر Waku bloom", + "wallet": "کیف پول", + "wallet-address": "شناسه یا آدرس حساب", + "wallet-asset": "دارایی", + "wallet-assets": "دارایی ها", + "wallet-backup-recovery-title": "از عبارت دانه خود نسخه پشتیبان تهیه کنید", + "wallet-choose-recipient": "گیرنده را انتخاب کنید", + "wallet-collectibles": "آلبوم", + "wallet-insufficient-funds": "بودجه ناکافی", + "wallet-insufficient-gas": "به اندازه کافی اتریوم برای سوخت (Gas) نیست", + "wallet-invalid-address": "آدرس نامعتبر: \n {{data}}", + "wallet-invalid-chain-id": "شبکه مطابقت ندارد: \n {{data}} اما زنجیره فعلی هست {{chain}}", + "wallet-key-content": "آدرس هگزادسیمال ۶۴ کارکتری مظابق با استاندارد اتریوم که با 0x شروع می شود که برای دریافت مبلغ در اختیار دیگران گذارده می شود.که به آن آدرس اتریون یا آدرس گیف پول نیز اطلاق میشود.", + "wallet-key-title": "آدرس یا شناسه حساب", + "wallet-manage-assets": "مدیریت دارایی ها", + "wallet-request": "درخواست", + "wallet-send": "ارسال", + "wallet-settings": "تنظیمات کیف پول", + "wallet-total-value": "مقدار کل", + "wallet-transaction-total-fee": "هزینه کل", + "wants-to-access-profile": "می خواهد به پروفایل شما دسترسی پیدا کند", + "warning": "هشدار", + "warning-message": "متأسفیم ، برای جلوگیری از هرزنامه ، در ارسال چندین پیام به صورت متوالی محدود هستیم. لطفاً چند لحظه دیگر دوباره امتحان کنید", + "watch-only": "تنها نمایشی", + "web-view-error": "بارگیری صفحه امکان پذیر نیست", + "welcome-blank-message": "گفتگوهای شما در اینجا ظاهر خواهد شد. برای شروع گفتگوهای جدید دکمه ⊕ را فشار دهید", + "welcome-screen-text": "کیف پول خود را تنظیم کنید ، دوستان را به گپ دعوت کنید \n و در dapp هاى محبوب گردش کنید ", + "welcome-to-status": "به استاتوس خوش آمدید", + "welcome-to-status-description": "کیف پول خود را تنظیم کنید، دوستان را به گپ دعوت کنید \n و در dappهاى محبوب گردش کنید ", + "whats-trending": "ببینید چه چیزی محبوب است", + "word-count": "شمارش لغات", + "word-n": "کلمه # {{number}}", + "word-n-description": "برای اینکه بررسی کنید که آیا ازعبارت دانه خود به درستی پشتیبان تهیه کرده اید ، کلمه # {{number}} بالا وارد کنید.", + "words-n": { + "one": "۱ کلمه\nکلمات {{count}}", + "other": "کلمات" + }, + "write-down-and-store-securely": "کدها را یادداشت کرده و در مکان امنی نگه داری کنید", + "wrong-keycard-text": "Keycard که به آن ضربه زدید با این تلفن همراه نیست", + "wrong-keycard-title": "به نظر می رسد که شما به یک keycard اشتباه ضربه زدید", + "wrong-password": "گذرواژه اشتباه است", + "wrong-word": "کلمه اشتباه", + "yes": "بله", + "you": "شما", + "you-are-all-set": "همه چیز آماده است!", + "you-are-all-set-description": "اکنون اگر تلفن خود را گم کردید می توانید مبلغ حساب خود را با استفاده از عبارت دانه بازیابی کنید.", + "you-can-change-account": "شما می توانید نام و رنگ حساب را بصورت دلخواه تغییر دهید", + "you-dont-have-contacts": "شما هنوز هیچ مخاطبی ندارید.", + "you-will-need-this-code": "برای باز کردن استاتوس و امضای معاملات به این کد احتیاج دارید", + "you-will-start-from-scratch": "شما از ابتدا با یک مجموعه جدید از کلیدها شروع خواهید کرد.", + "your-answer": "پاسخ شما", + "your-contact-code": "اعطای دسترسی به این DApp تا کلید چت خود را بازیابی کند", + "your-data-belongs-to-you": "اگر عبارت دانه خود را گم کنید ، داده ها و بودجه خود را از دست می دهید", + "your-data-belongs-to-you-description": "اگر شما عبارت دانه خود را از دست بدهید، استاتوس نمی تواند به شما کمک کند تا چندحساب خود را بازیابی کنید. شما مسئول امنیت داده های خود هستید و پشتیبان گیری از عبارت دانه خودتان بهترین حفاظت است.", + "your-keys": "کلیدهای شما", + "your-recovery-phrase": "عبارت دانه شما", + "your-recovery-phrase-description": "این عبارت بازیابی شماست. شما از آن برای اثبات اینکه این کیف پول شماست استفاده می کنید. شما فقط همین یکبار آنرا مشاهده میکنید! آن را روی کاغذ بنویسید و در مکانی امن نگه دارید. اگر کیف پول خود را از دست بدهید یا مجددا نصب کنید، به آن نیاز خواهید داشت." +} diff --git a/translations/fi.json b/translations/fi.json new file mode 100644 index 00000000000..49255410a89 --- /dev/null +++ b/translations/fi.json @@ -0,0 +1,131 @@ +{ + "confirm": "Vahvista", + "amount": "Määrä", + "members-active": { + "one": "1 käyttäjä", + "other": "{{count}} käyttäjää", + "zero": "ei käyttäjiä" + }, + "chat-name": "Keskustelun nimi", + "phew-here-is-your-passphrase": "*Phew* se oli vaikeaa, tässä on tunnuslauseesi, *kirjoita tämä ylös ja pidä tallessa!* Tarvitset sitä palauttaaksesi tilisi.", + "public-group-topic": "Otsikko", + "chat-settings": "Keskustelun asetukset", + "offline": "Poissa", + "invited": "kutsuttu", + "address": "Osoite", + "new-public-group-chat": "Liity julkiseen keskusteluun", + "datetime-hour": { + "one": "tunti", + "other": "tuntia" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "Myöntääksesi vaadittu kameran käyttölupa, siirry järjestelmäasetuksiin ja varmista, että Status > Kamera on valittu.", + "remove": "Poista", + "add-members": "Lisää käyttäjiä", + "photos-access-error": "Myöntääksesi vaadittu kuvien käyttölupa, siirry järjestelmäasetuksiin ja varmista, että Status > Kuvat on valittu.", + "done": "Valmis", + "close-chat": "Poista keskustelu", + "new-group-chat": "Uusi ryhmäkeskustelu", + "sign-in": "Luo tili", + "datetime-yesterday": "eilen", + "create-new-account": "Luo uusi tili", + "datetime-ago": "sitten", + "contacts": "Yhteystiedot", + "got-it": "Vastaanotettu", + "active-online": "Linjalla", + "password": "Salasana", + "browsing-title": "Selaa", + "discover": "Löydä", + "browsing-cancel": "Peruuta", + "intro-status": "Keskustele kanssani määrittääksesi tai muuttaaksesi tilisi asetuksia!", + "name": "Nimi", + "show-qr": "Näytä QR", + "connect": "Yhdistä", + "edit": "Muokkaa", + "account-generation-message": "Ole hyvä ja odota hetki, käynnistän kvanttitietokoneeni tilisi luomiseksi!", + "no-messages": "Ei viestejä", + "passphrase": "Tunnuslause", + "recipient": "Vastaanottaja", + "members-title": "Jäsenet", + "new-group": "Uusi ryhmä", + "phone-e164": "Kansainvälinen 1", + "settings": "Asetukset", + "delete": "Poista", + "chats": "Keskustelut", + "transaction": "Tapahtuma", + "public-group-status": "Julkinen", + "image-source-make-photo": "Kuvakaappaus", + "start-conversation": "Aloita keskustelu", + "save": "Tallenna", + "sharing-copy-to-clipboard": "Kopioi leikepöydälle", + "sync-in-progress": "Synkronoidaan...", + "send-transaction": "Lähetä tapahtuma", + "incorrect-code": [ + "str", + "Virheellinen koodi, ole hyvä ja yritä uudelleen" + ], + "image-source-gallery": "Valitse galleriasta", + "sync-synced": "Synkronoitu", + "status-pending": "Odottaa", + "datetime-day": { + "one": "päivä", + "other": "päivää" + }, + "scan-qr": "Skannaa QR", + "contact-s": { + "one": "yhteystieto", + "other": "yhteystietoa" + }, + "next": "Seuraava", + "recent": "Uusimmat", + "status": "Tila", + "from": "Lähettäjältä", + "wrong-password": "Väärä salasana", + "in-contacts": "Kontakteissa", + "sharing-share": "Jaa...", + "type-a-message": "Kirjoita viesti...", + "clear-history": "Tyhjennä historia", + "no-contacts": "No contacts yet", + "datetime-today": "tänään", + "web-view-error": "oops, virhe", + "error": "Virhe", + "more": "lisää", + "cancel": "Peruuta", + "can-not-add-yourself": "Et voi lisätä itseäsi", + "add-to-contacts": "Lisää kontakteihin", + "available": "Saatavilla", + "You": "Sinä", + "main-wallet": "Ensisijainen Lompakko", + "members": { + "one": "1 käyttäjä", + "other": "{{count}} käyttäjää", + "zero": "ei käyttäjiä" + }, + "intro-message1": "Tervetuloa Statukseen\nNapauta tätä viestiä asettaaksesi salasanasi ja alkuun pääsemiseksi!", + "new-contact": "Uusi yhteystieto", + "datetime-second": { + "one": "sekunti", + "other": "sekuntia" + }, + "recover": "Palauta", + "datetime-minute": { + "one": "minuutti", + "other": "minuuttia" + }, + "browsing-open-in-android-web-browser": "Avaa selaimessa", + "browsing-open-in-ios-web-browser": "Avaa selaimessa", + "edit-profile": "Muokkaa profiilia", + "active-unknown": "Ei tiedossa", + "public-key": "Julkinen avain", + "profile": "Profiili", + "none": "Tyhjä", + "removed": "poistettu", + "message": "Viesti", + "here-is-your-passphrase": "Tässä on tunnuslauseesi, *kirjoita tämä ylös ja pidä tallessa!* Tarvitset sitä palauttaaksesi tilisi.", + "image-source-title": "Profiilikuva", + "current-network": "Nykyinen verkko", + "left": "poistui", + "to": "Vastaanottajalle", + "status-sent": "Lähetetty", + "data": "Tieto" +} diff --git a/translations/fil.json b/translations/fil.json new file mode 100644 index 00000000000..4fa988215cf --- /dev/null +++ b/translations/fil.json @@ -0,0 +1,1796 @@ +{ + "You": "ikaw", + "about-app": "Tungkol sa", + "about-key-storage-content": "Ang Status ay hindi kailanman mai-access ang iyong pribadong key. Siguraduhing i-backup ang iyong seen phrase. Kung nawala mo ang iyong telepono ito ang tanging paraan upang ma-access ang iyong mga keys.", + "about-key-storage-title": "Tungkol sa pangunahing imbakan", + "about-names-content": "Walang sinuman ang maaaring magpanggap na ikaw! Hindi ka nagpapakilala sa pamamagitan ng default at hindi mo kailangang ihayag ang iyong tunay na pangalan. Maaari kang magrehistro ng isang pasadyang pangalan para sa isang maliit na bayad.", + "about-names-title": "Hindi mababago ang mga pangalan", + "about-sharing-data": "Tungkol sa pagbabahagi ng data", + "accept": "Tanggapin", + "accept-and-add": "Tanggapin at idagdag", + "accept-and-continue": "Tanggapin at ipagpatuloy", + "accept-and-share-address": "Tanggapin at ibahagi ang address", + "accept-new-chats-from": "Tumanggap ng mga bagong chat mula sa", + "accept-status-tos-prefix": "Tumatanggap ako ng Status", + "access-existing-keys": "I-access ang umiiral na mga key", + "access-key": "I-access ang key", + "account-added": "Idinagdag ang account", + "account-color": "Kulay ng account", + "account-content": "Maaari mong ihambing ang mga account sa Status at sa bank accounts. Tulad ng isang account sa bangko, isang account ay karaniwang mayroong address at isang balanse; Ginagamit mo ang account na ito upang mag-transact sa Ethereum. Maaari kang magkaroon ng maraming mga account sa iyong pitaka o wallet. Lahat ng na-access sa pamamagitan ng pag-unlock ng Status.", + "account-exists-title": "Mayroon nang account", + "account-is-used": "Ginagamit ang account sa mga Dapps sa browser.", + "account-name": "Pangalan ng account", + "account-settings": "Mga setting ng account", + "account-title": "Account", + "accounts": "Accounts", + "actions": "Mga kilos", + "active-online": "Online", + "active-unknown": "Hindi kilala", + "activity": "Aktibidad", + "add": "Idagdag", + "add-a-watch-account": "Magdagdag ng isang address na panonood lamang", + "add-account": "Magdagdag ng account", + "add-account-description": "Maaari kang mag-import ng anumang uri ng Ethereum account para ma-idagdag ito para sa iyong Status Wallet", + "add-account-incorrect-password": "Tila hindi tama ang password. Ilagay ang password na iyong ginagamit para mabuksan ang app.", + "add-an-account": "Magdagdag ng isang account", + "add-bootnode": "Idagdag ang bootnode", + "add-contact": "Magdagdag ng contact", + "add-custom-token": "Magdagdag ng custom token", + "add-favourite": "Magdagdag ng paborito", + "add-mailserver": "Magdagdag ng Status node", + "add-members": "Magdagdag ng mga miyembro", + "add-network": "Magdagdag ng network", + "add-new-contact": "Magdagdag ng bagong contact", + "add-nickname": "Magdagdag ng palayaw (opsyonal)", + "add-node": "Magdagdag ng node", + "add-private-key-account": "Magdagdag ng account mula sa pribadong key", + "add-seed-account": "Magdagdag ng account para sa seed phrase", + "add-to-contacts": "Idagdag sa mga contact", + "add-to-contacts-text": "Sa pamamagitan ng pagdaragdag ng isang user sa iyong listahan ng contact, ibinahagi mo ang iyong wallet address", + "add-to-favourites": "Idagdag sa mga Paborito", + "add-watch-account": "Magdagdag ng isang account na panonood lamang", + "address": "Address", + "address-or-ens-name": "Address o pangalan ng ENS", + "address-received": "Natanggap ang address", + "address-request-accepted": "Tinanggap ang kahilingan sa address", + "address-requested": "Hiniling ang address", + "advanced": "Advanced", + "advanced-settings": "Mga advanced na setting", + "advertiser-description": "Natuklasan mo ang Status salamat sa isang kasosyo. Naisip mo ba kung susuriin ng Status ang iyong IP address nang isang beses para makakuha sila ng gantimpala? Ang impormasyong ito ay hindi gagamitin para sa anumang bagay at ito ay lubos na aalisin pagkatapos ng 7 araw.", + "advertiser-starter-pack-accept": "Tanggapin", + "advertiser-starter-pack-decline": "Tanggihan", + "advertiser-starter-pack-description": "Narito ang ilang mga crypto upang makapagsimula ka! Gamitin ito upang makakuha ng mga sticker, isang pangalan ng ENS at subukan ang mga dapps", + "advertiser-starter-pack-title": "Starter Pack", + "advertiser-title": "Default sa privacy", + "agree-by-continuing": "Sa pamamagitan ng pagpapatuloy sumasang-ayon ka\nsa aming", + "all": "Lahat", + "allow": "Payagan", + "allow-and-send": "Payagan at ipadala", + "allow-mention-notifications": "Ipakita @ banggitin", + "allowing-authorizes-this-dapp": "Pinapayagan ang nagpapahintulot sa DApp na ito na makuha ang iyong address ng pitaka at paganahin ang Web3", + "already-have-asset": "Mayroon ka nang asset na ito", + "amount": "Halaga", + "anyone": "Sinuman", + "app-commit": "App commit", + "appearance": "Hitsura", + "apply": "Mag-apply", + "approve": "Aprubahan", + "approve-limit": "Aprubahan ang limitasyon", + "approve-token": "Aprubahan ang token", + "approve-token-contract-desc": "Ang pag-apruba sa isang token na may kontrata ay nagbibigay-daan dito na gastusin ang iyong balanse sa token. Kung sa tingin mo ay hindi mapagkakatiwalaan ang isang proyekto, huwag aprubahan ang token sa kanila, o aprubahan lang ang halagang gagamitin mo sa kanila.", + "are-you-sure": "Sigurado ka ba?", + "are-you-sure-description": "Hindi mo na makikitang muli ang seed phrase", + "are-you-sure-to-cancel": "Sigurado ka bang gusto mong kanselahin?", + "are-you-sure?": "Sigurado ka ba?", + "ask-in-status": "Magtanong ng isang katanungan o mag-ulat ng isang bug", + "at": "sa", + "attribution-received": "{{attrib}} mula sa {{max}} bonus na natanggap", + "audio": "Audio", + "audio-recorder": "Recorder", + "audio-recorder-error": "Error sa recorder", + "audio-recorder-max-ms-reached": "Naabot ang maximum na oras ng pag-record", + "audio-recorder-permissions-error": "Kailangan mong magbigay ng pahintulot upang magpadala ng mga audio message", + "authorize": "Awtorisado", + "available": "Available", + "available-participants": { + "zero": "wala", + "one": "isa", + "two": "dalawa", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "back": "Balik", + "back-up": "I-back up", + "back-up-seed-phrase": "Back up ng seed phrase", + "back-up-your-seed-phrase": "I-back up ang iyong seed phrase", + "backing-up": "Bina-back up...", + "backup-disabled": "Disabled", + "backup-enabled": "Paganahin", + "backup-recovery-phrase": "Back up seed phrase", + "backup-settings": "Mga setting ng backup", + "backup-through-waku": "I-backup sa pamamagitan ng waku", + "bad-fees-description": "Ang iyong priyoridad na bayad ay mas mababa sa aming mga iminungkahing parameter.", + "balance": "Balanse", + "begin-set-up": "Simulan ang pag-setup", + "below-base-fee": "pinakamataas na bayarin mas mababa sa batayang bayarin", + "biometric-auth-android-sensor-desc": "Pindutin ang sensor", + "biometric-auth-android-sensor-error-desc": "Nabigo", + "biometric-auth-android-title": "Kinakailangan ang pagpapatunay", + "biometric-auth-confirm-logout": "Relogin", + "biometric-auth-confirm-message": "Kinakailangan ang Biometric authentication para magpatuloy, kung hindi pwede mangyaring i-unlock ang iyong keys gamit ang iyong password or passcode", + "biometric-auth-confirm-title": "Dapat mong patunayan!", + "biometric-auth-confirm-try-again": "Subukan muli", + "biometric-auth-error": "Hindi ma-perform ang pagpapatunay ng biometric ({{code}})", + "biometric-auth-login-error-title": "Error sa authentication ng biometric", + "biometric-auth-login-ios-fallback-label": "Ilagay ang password", + "biometric-auth-reason-login": "Mag-login sa Status", + "biometric-auth-reason-verify": "Patunayan ang pagpapatunay", + "biometric-disable-bioauth": "hindi paganahin{{bio-type-label}}", + "biometric-disable-password-description": "Kung hindi mo paganahin ito, magagawa mo rin", + "biometric-disable-password-title": "Huwag paganahin ang pag-save ng password", + "biometric-enable": "Kung hindi mo nais na ipasok ang iyong password sa bawat oras upang ma-access ang app, paganahin{{bio-type-label}} mag sign-in", + "biometric-enable-button": "Paganahin{{bio-type-label}}", + "biometric-enable-keycard": "Kung ayaw mong gamitin ang iyong Keycard sa bawat oras na ma-access ang app, paganahin{{bio-type-label}}mag-sign in", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Fingerprint", + "biometric-secure-with": "Secure sa{{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "BIP39 password", + "blank-keycard-text": "Maaari kang magpatuloy sa iyong keycard kapag nabuo mo ang iyong mga keys at pangalan", + "blank-keycard-title": "Mukhang naka-tap ka ng\nisang blangkong keycard", + "block": "Block", + "block-contact": "I-block ang gumagamit na ito", + "block-contact-details": "Ang pag-block ay tatanggalin ang mga naunang mensahe ng gumagamit na ito at pipigilan ang mga bago na maabot sa iyo.", + "blocked-users": "Na-block ang mga gumagamit", + "bootnode-address": "Bootnode address", + "bootnode-details": "Mga detalye sa Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes enabled", + "bootnodes-settings": "Bootnodes settings", + "browsed-websites": "Ang kasaysayan ng browser ay lilitaw dito", + "browser": "Browser", + "browser-not-secure": "Hindi secure ang koneksyon! Huwag mag-sign ng transaksyon o magpadala ng personal na data sa site na ito.", + "browser-secure": "Ang koneksyon ay ligtas. Siguraduhin na talagang pinagkakatiwalaan mo ang site na ito bago mag-sign ng mga transaksyon o pagpasok ng personal na data.", + "browsers": "Mga Browser", + "browsing-cancel": "Pagkansela", + "browsing-open-in-android-web-browser": "Buksan sa Android", + "browsing-open-in-ios-web-browser": "Buksan sa iOS", + "browsing-open-in-status": "Buksan ang Status", + "browsing-site-blocked-description1": "Nakita namin ang mga potensyal na nakakahamak na aktibidad mula sa address na ito. Upang maprotektahan ka at ang iyong pitaka, pinipigilan namin ang karagdagang pag-navigate.\n\nKung sa palagay mo ito ay isang error, ipaalam sa amin sa", + "browsing-site-blocked-description2": "pampublikong chat.", + "browsing-site-blocked-go-back": "Bumalik ka", + "browsing-site-blocked-title": "Na-block ang site na ito", + "browsing-title": "Browse", + "bug-report": "I-report ang bug", + "bug-report-description": "deskripsyon", + "bug-report-description-placeholder": "Kailangan, hindi maaaring walang laman", + "bug-report-steps": "Mga hakbang sa pagpaparami", + "bug-report-steps-placeholder": "- buksan ang app\n - gumawa ng paraan\n - at pagkatapos ay iba pa...", + "bug-report-submit-email": "Isumite sa pamamagitan ng email na may logs archive", + "bug-report-submit-gh-issue": "Magsumite ng isyu sa GitHub nang walang mga log", + "bug-report-too-short-description": "Ang paglalarawan ay masyadong maikli", + "build-yourself": "Upang magamit ang app nang wala ang mga Tuntunin ng Serbisyo na ito, maaari kang bumuo ng iyong sariling bersyon", + "buy-crypto": "Bumili ng crypto", + "buy-crypto-choose-a-service": "Pumili ng serbisyo na nais mong gamitin upang bumili ng crypto", + "buy-crypto-description": "Maghanap ng dapp upang bumili ng crypto ngayon", + "buy-crypto-leaving": "Ikaw ay umaalis sa Status at pumapasok sa isang third party na website upang makumpleto ang iyong pagbili", + "buy-crypto-title": "Mukhang walang laman ang iyong wallet", + "camera-access-error": "Upang bigyan ang kinakailangang pahintulot ng camera, mangyaring pumunta sa iyong mga setting ng system at tiyakin na ang Status > Napili ang camera.", + "can-not-add-yourself": "Ikaw yan, upang magsimula ng isang chat pumili ng ibang tao", + "can-send-messages": "Maaari kang magpadala at makatanggap ng mga bagong mensahe", + "cancel": "kansela", + "cancel-keycard-setup": "Ikansela ang pag-setup ng Keycard", + "cancelling": "Pagkansela", + "cannot-read-card": "Hindi mabasa ang card.\nMangyaring hawakan ito sa likod ng iyong telepono", + "cannot-use-default-pin": "Hindi pinapayagan ang passcode 000000.\nMangyaring gumamit ng iba pang numero", + "cant-open-public-chat": "Hindi mabuksan ang pampublikong chat", + "cant-report-bug": "Hindi maiulat ang isang bug", + "card-is-blank": "Blangko ang card na ito", + "card-reseted": "Natapos na ang card", + "card-unpaired": "Ang card ay walang bayad mula sa kasalukuyang aparato", + "category": "Kategorya", + "category-title": "Pamagat ng kategorya", + "change-fleet": "Baguhin ang fleet sa{{fleet}}", + "change-logging-enabled": "Sigurado ka bang gusto mo{{enable}}logging?", + "change-pairing": "Baguhin ang pairing code", + "change-pairing-description": "Ang pagbabago ng pairing code ay hindi nakakaapekto sa kasalukuyang mga pares. Gayunpaman, ang anumang bagong pagpapares ay mangangailangan ng bagong code.", + "change-pairing-title": "Gumawa ng panibagong pairing code", + "change-passcode": "Baguhin ang Passcode", + "change-password": "Palitan ang password", + "change-pin": "Baguhin ang 6-digit na Passcode", + "change-puk": "Baguhin ang 12-digit na PUK", + "change-tip": "Baguhin ang tip", + "changed-amount-warning": "Ang halaga ay binago mula sa{{old}} hangang sa{{new}}", + "changed-asset-warning": "Binago si Asset mula sa{{old}} hangang sa{{new}}", + "chaos-mode": "Chaos mode", + "chaos-unicorn-day": "Araw ng Chaos Unicorn", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Makipag-chat at makipag-transaksyon nang pribado sa mga kaibigan", + "chat-is-a-contact": "Makipag-ugnay", + "chat-is-not-a-contact": "Hindi isang contact", + "chat-key": "Chat key", + "chat-key-content": "Mensahe sa Status Chat Protocol ay ipinapadala at natanggap gamit ang mga encryption keys. Mga public chat key ay isang string ng mga character na ibinabahagi mo sa iba upang maaari silang magpadala sa iyo ng mga mensahe sa Status.", + "chat-key-title": "Chat Key", + "chat-link-previews": "Mga preview ng chat link", + "chat-name": "Pangalan ng chat", + "chat-name-content": "Tatlong random na salita, nagmula algorithmically galing sa iyong chat key at gamitin mo ito para sa iyong default na alyas sa chat. Pangalan ng Chat ay ganap na natatangi o kakaiba, walang ibang user ang maaring magkapareho sa tatlong salita.", + "chat-name-title": "Chat Name", + "chat-notification-preferences": "Mga setting ng notification", + "chat-settings": "Mga setting ng chat", + "chats": "Chats", + "check-on-block-explorer": "Tingnan sa block explorer", + "check-on-opensea": "Tingnan sa opensea", + "check-your-recovery-phrase": "Suriin ang iyong seed phrase", + "choose-actions": "Pumili ng mga aksyon", + "choose-authentication-method": "Pumili ng isang paraan ng pagpapatunay", + "choose-new-location-for-keystore": "Pumili ng isang bagong lokasyon upang i-save ang iyong keystore file", + "choose-storage": "Pumili ng imbakan", + "clear": "Malinaw", + "clear-all": "Alisin lahat", + "clear-history": "Alisin ang history", + "clear-history-action": "Malinaw", + "clear-history-confirmation": "Alisin ang history?", + "clear-history-confirmation-content": "Sigurado ka bang nais mong limasin ang kasaysayan ng chat na ito?", + "clear-history-title": "Alisin ang history?", + "cleared-chat-description-public": "Ang tahimik dito. Simulan ang usapan o", + "close": "Isara", + "close-all": "Isara lahat", + "close-app-button": "Kumpirma", + "close-app-content": "Ang app ay titigil at isara. Kapag binuksan mo ito, gagamitin ang napiling network", + "close-app-title": "Babala!", + "collectibles-leak-metadata": "Maaari mong ipakita ang iyong mga NFT dito. Kung gagawin mo ito, ibabahagi mo ang iyong wallet at IP address", + "command-button-send": "Ipadala", + "communities": "Mga Komunidad", + "communities-alpha": "Mga Komunidad (alpha)", + "communities-enabled": "Pinagana ang mga komunidad", + "communities-verified": "✓ Na-verify na Komunidad ng Status", + "community": "Komunidad", + "community-color": "Kulay ng pamayanan", + "community-color-placeholder": "Pumili ng kulay", + "community-edit-title": "I-edit ang pamayanan", + "community-emoji-thumbnail-title": "Thumbnail", + "community-image-pick": "Pumili ng imahe", + "community-image-remove": "Alisin", + "community-image-take": "Kunan ng litrato", + "community-info": "Impormasyon sa komunidad", + "community-info-not-found": "Hindi nahanap ang impormasyon sa komunidad", + "community-invite-title": "Mag-anyaya", + "community-key": "Pribadong key ng komunidad", + "community-key-placeholder": "I-type ang iyong community private key", + "community-link": "Link ng komunidad", + "community-members": { + "zero": "sero", + "one": "isa", + "two": "dalawa", + "few": "konti", + "many": "marami", + "other": "iba" + }, + "community-members-title": "Mga kasapi", + "community-message-preview": "Imbitasyon na sumali sa {{community-name}}", + "community-private-key": "Pribadong key ng komunidad", + "community-requests-to-join-title": "Mga kahilingan sa pagiging miyembro", + "community-roles": "Mga Tungkulin", + "community-share-title": "Ibahagi", + "community-thumbnail-image": "Thumbnail na imahe", + "community-thumbnail-upload": "I-upload", + "complete-hardwallet-setup": "Ang card na ito ay naka-link ngayon. Kailangan mo ito upang mag-sign ng transaksyon at para ma-unlock ang iyong keys", + "completed": "Nakumpleto", + "confirm": "Kumpirma", + "confirm-new-password": "Kumpirmahin ang bagong password", + "confirm-pairing-code-placeholder": "Kumpirmahin ang iyong pairing code...", + "confirm-password-placeholder": "Kumpirmahin ang iyong password...", + "confirmation-request": "Kahilingan sa Pagkumpirma", + "confirmations": "Pagkumpirma", + "confirmations-helper-text": "Kapag ang transaksyon ay may 12 kumpirmasyon maaari mong isaalang-alang na naayos ito.", + "connect": "Kumonekta", + "connect-mailserver-content": "kumonekta sa {{name}}?", + "connect-wallet": "Ikonekta ang pitaka", + "connected": "Nakakonekta", + "connected-to": "Nakakonekta sa", + "connecting": "Pagkonekta ...", + "connecting-requires-login": "Ang pagkonekta sa isa pang network ay nangangailangan ng pag-login", + "connection-request": "Maghiling ng Koneksyon", + "connection-status": "Koneksyon ng Status", + "connection-with-the-card-lost": "Ang iyong koneksyon sa iyong Card ay nawala", + "connection-with-the-card-lost-setup-text": "Para ipagpatuloy ang pag-setup hawakan ang card sa\nlikod ng iyong phone at panatilihin\nang card sa contact sa telepono", + "connection-with-the-card-lost-text": "Para magpatuloy hawakan ang card sa likod ng iyong telepono", + "contact-code": "Chat key", + "contact-s": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunting", + "many": "maraming", + "other": "iba pang" + }, + "contacts": "Mga contact", + "contacts-descr": "Lilitaw ang iyong mga contact dito. Makakatanggap ka ng mga pag-update ng Status mula sa sinumang idagdag mo bilang isang contact", + "contacts-empty": "komontak gamit ang pangalan ng ENS na lilitaw dito", + "continue": "Magpatuloy", + "continue-anyway": "Magpatuloy pa rin", + "contract-address": "Contract address", + "contract-interaction": "Pakikipag-ugnayan sa kontrata", + "contract-isnt-supported": "Ang kontrata ay hindi suportado", + "copy-info": "Kopyahin ang impormasyon", + "copy-qr": "Kopyahin ang code", + "copy-to-clipboard": "Kopya", + "copy-transaction-hash": "Kopyahin ang transaksyon ID", + "cost-fee": "Gastos \/ Bayad", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Lumikha", + "create-a-pin": "Gumawa ng 6-digit passcode", + "create-a-puk": "Gumawa ng 12-digit PUK", + "create-category": "Lumikha ng kategorya", + "create-channel": "Lumikha ng channel", + "create-channel-title": "Bagong channel", + "create-community": "Lumikha ng isang pamayanan", + "create-group-chat": "Gumawa ng group chat", + "create-multiaccount": "Lumikha ng Keys", + "create-new-key": "Kumuha ng mga bagong keys", + "create-pin": "Lumikha ng 6-digit na passcode", + "create-pin-description": "kailangan mo ang iyong card + ang 6-digit na passcode na ito upang mai-unlock ang Status at upang kumpirmahin ang mga transaksyon.", + "created-group-chat-description": "Ikaw ay nakagawa ng grupo{{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Pera", + "currency-display-name-aed": "Emirati Dirham", + "currency-display-name-afn": "Afghanistan Afghani", + "currency-display-name-ars": "Argentine Peso", + "currency-display-name-aud": "Australian Dollar", + "currency-display-name-bbd": "Barbados Dollar", + "currency-display-name-bdt": "Bangladeshi Taka", + "currency-display-name-bgn": "Bulgarian Lev", + "currency-display-name-bhd": "Bahraini Dinar", + "currency-display-name-bnd": "Brunei Darussalam Dollar", + "currency-display-name-bob": "Bolivia Bolíviano", + "currency-display-name-brl": "Brazil Real", + "currency-display-name-btn": "Bhutanese Ngultrum", + "currency-display-name-cad": "Canada Dollar", + "currency-display-name-chf": "Switzerland Franc", + "currency-display-name-clp": "Chile Peso", + "currency-display-name-cny": "China Yuan Renminbi", + "currency-display-name-cop": "Colombia Peso", + "currency-display-name-crc": "Costa Rica Colon", + "currency-display-name-czk": "Czech Koruna", + "currency-display-name-dkk": "Denmark Krone", + "currency-display-name-dop": "Dominican Republic Peso", + "currency-display-name-egp": "Egypt Pound", + "currency-display-name-etb": "Ethiopian Birr", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "British Pound", + "currency-display-name-gel": "Georgian Lari", + "currency-display-name-ghs": "Ghana Cedi", + "currency-display-name-hkd": "Hong Kong Dollar", + "currency-display-name-hrk": "Croatia Kuna", + "currency-display-name-huf": "Hungary Forint", + "currency-display-name-idr": "Indonesia Rupiah", + "currency-display-name-ils": "Israel Shekel", + "currency-display-name-inr": "India Rupee", + "currency-display-name-isk": "Iceland Krona", + "currency-display-name-jmd": "Jamaica Dollar", + "currency-display-name-jpy": "Japanese Yen", + "currency-display-name-kes": "Kenyan Shilling", + "currency-display-name-krw": "Korea (South) Won", + "currency-display-name-kwd": "Kuwaiti Dinar", + "currency-display-name-kzt": "Kazakhstan Tenge", + "currency-display-name-lkr": "Sri Lanka Rupee", + "currency-display-name-mad": "Moroccan Dirham", + "currency-display-name-mdl": "Moldovan Leu", + "currency-display-name-mur": "Mauritius Rupee", + "currency-display-name-mwk": "Malawian Kwacha", + "currency-display-name-mxn": "Mexico Peso", + "currency-display-name-myr": "Malaysia Ringgit", + "currency-display-name-mzn": "Mozambique Metical", + "currency-display-name-nad": "Namibia Dollar", + "currency-display-name-ngn": "Nigeria Naira", + "currency-display-name-nok": "Norway Krone", + "currency-display-name-npr": "Nepal Rupee", + "currency-display-name-nzd": "New Zealand Dollar", + "currency-display-name-omr": "Oman Rial", + "currency-display-name-pen": "Peru Sol", + "currency-display-name-pgk": "Papua New Guinean Kina", + "currency-display-name-php": "Philippines Peso", + "currency-display-name-pkr": "Pakistan Rupee", + "currency-display-name-pln": "Polish Zloty", + "currency-display-name-pyg": "Paraguay Guarani", + "currency-display-name-qar": "Qatar Riyal", + "currency-display-name-ron": "Romania Leu", + "currency-display-name-rsd": "Serbia Dinar", + "currency-display-name-rub": "Russia Ruble", + "currency-display-name-sar": "Saudi Arabia Riyal", + "currency-display-name-sek": "Sweden Krona", + "currency-display-name-sgd": "Singapore Dollar", + "currency-display-name-thb": "Thailand Baht", + "currency-display-name-try": "Turkish Lira", + "currency-display-name-ttd": "Trinidad at Tobago Dollar", + "currency-display-name-twd": "Taiwan New Dollar", + "currency-display-name-tzs": "Tanzanian Shilling", + "currency-display-name-uah": "walang mga kasapi", + "currency-display-name-ugx": "Ugandan Shilling", + "currency-display-name-usd": "United States Dollar", + "currency-display-name-uyu": "Uruguay Peso", + "currency-display-name-vef": "Venezuela Bolívar", + "currency-display-name-vnd": "Vietnam Dong", + "currency-display-name-zar": "South Africa Rand", + "current": "Kasalukuyan", + "current-average": "Kasalukuyang average", + "current-average-tip": "Kasalukuyang pamantayan na tip", + "current-base": "Kasalukuyang base", + "current-base-fee": "Kasalukuyang batayan sa bayarin", + "current-minimum-tip": "kasalukuyang minimum na tip", + "current-network": "Kasalukuyang network", + "current-password": "Kasalukuyang password", + "current-pin": "Ilagay ang 6-digit na passcode", + "current-pin-description": "Ilagay ang iyong 6-digit na passcode upang magpatuloy", + "custom": "Pasadyang", + "custom-networks": "Pasadyang mga network", + "custom-node": "Ikaw ay gumagamit ng custom RPC endpoint. Ang iyong lokal na kasaysayan ng paglilipat ay maaring hindi kumpleto", + "custom-seed-phrase": "Hindi wastong seed phrase", + "custom-seed-phrase-text-1": "Ang seed phrase ay hindi tumutugma sa aming suportadong diksyunaryo. Suriin para sa mga maling salita.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Tanggapin at Buksan", + "dapp-starter-pack-description": "Narito ang ilang mga crypto upang makapagsimula ka! Gamitin ito upang makakuha ng mga sticker, isang pangalan ng ENS at subukan ang mga dapps", + "dapp-starter-pack-title": "Starter Pack", + "dapp-would-like-to-connect-wallet": "nais kumonekta sa", + "dapps": "ÐApps", + "dapps-permissions": "Mga pahintulot ng DApp", + "dark": "Madilim", + "data": "Data", + "data-collected": "Nakolektang data", + "data-collected-subtitle": "Ipinapakita ng talahanayan sa ibaba ang eksaktong data na nakaimbak at ipapadala. Ang data ay napatunayan laban sa mga pampublikong panuntunan upang matiyak na walang sensitibong data na naipadala. Huwag magtiwala, i-verify.", + "data-syncing": "Pag-sync ng data", + "database-reset-content": "Ang mga chat, contact at setting ay tinanggal. Maaari mong gamitin ang iyong account sa iyong Keycard", + "database-reset-title": "I-reset ang Database", + "database-reset-warning": "Mare-reset ang database. Ang mga chat, contact at setting ay tatanggalin", + "datetime-ago": "nakaraan", + "datetime-ago-format": "{{number}}{{time-intervals}}{{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunting", + "many": "maraming", + "other": "iba pang" + }, + "datetime-day-short": { + "zero": "walang araw", + "one": "isang araw", + "two": "dalawang araw", + "few": "ilang araw", + "many": "maraming araw", + "other": "ibang araw" + }, + "datetime-hour": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "datetime-hour-short": { + "zero": "walang oras", + "one": "isang oras", + "two": "dalawang oras", + "few": "kaunting oras", + "many": "maraming oras", + "other": "ibang oras" + }, + "datetime-minute": { + "zero": "minuto", + "one": "isang", + "two": "dalawa", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "datetime-minute-short": { + "zero": "walang minuto", + "one": "isang minuto", + "two": "dalawang minuto", + "few": "ilang minuto", + "many": "maraming minuto", + "other": "ibang minuto" + }, + "datetime-second": { + "zero": "zero", + "one": "isa", + "two": "dalawa", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "datetime-second-short": { + "zero": "walang segundo", + "one": "isang segundo", + "two": "dalawang segundo", + "few": "ilang segundo", + "many": "maraming segundo", + "other": "iba pang mga segundo" + }, + "datetime-today": "ngayon", + "datetime-yesterday": "kahapon", + "decimals": "Decimals", + "decline": "Tanggihan", + "decryption-failed-content": "Isang error na naganap sa pag-decrypting ng iyong data. Maaaring kailanganin mong burahin ang iyong dating data at makabuo ng isang bagong account. Tapikin ang \"Mag-apply\" upang burahin o \"Ikansela\" upang subukang muli", + "default": "Default", + "default-assets": "Default na ERC20 at ERC721", + "default-sync-period": "Pag-sync ng history para sa", + "delete": "Tanggalin", + "delete-account": "Tanggalin ang account", + "delete-and-leave-group": "Tanggalin at iwanan ang pangkat", + "delete-bootnode": "Tanggalin ang bootnode", + "delete-bootnode-are-you-sure": "Sigurado ka bang gusto mong tanggalin ang bootnode na ito?", + "delete-bootnode-title": "Tanggalin ang bootnode", + "delete-category-confirmation": "Sigurado ka bang gusto mong tanggalin ang kategoryang ito?", + "close-chat": "Tanggalin ang chat", + "close-chat-confirmation": "Sigurado ka bang gusto mong tanggalin ang chat na ito?", + "delete-confirmation": "Tanggalin?", + "delete-keys-keycard": "Tanggalin ang mga key mula sa Keycard", + "delete-mailserver": "Tanggalin ang Status node", + "delete-mailserver-are-you-sure": "Sigurado ka bang nais mong burahin ang Status node?", + "delete-mailserver-title": "Burahin ang Status node", + "delete-message": "Tanggalin ang mensahe", + "delete-my-account": "Tanggalin ang aking account", + "delete-my-profile": "Tanggalin ang aking profile", + "delete-network-confirmation": "Sigurado ka bang gusto mong tanggalin ang network na ito?", + "delete-network-error": "Mangyaring kumonekta sa ibang network bago matanggal ang isang ito", + "delete-network-title": "Tanggalin ang network?", + "delete-node": "Tanggalin ang node", + "delete-node-are-you-sure": "Sigurado ka bang nais mong tanggalin ang node na ito?", + "delete-node-title": "Tanggalin ang node", + "delete-profile": "Tanggalin ang profile", + "delete-profile-warning": "Babala: Kung hindi mo naisulat ang iyong seed phrase, mawawalan ka ng pag-access sa iyong mga pondo matapos mong tanggalin ang iyong profile", + "deny": "Deny", + "derivation-path": "Landas ng dereksyon", + "describe-channel": "Ilarawan ang channel", + "description": "Paglalarawan", + "dev-mode": "Mode ng pag-unlad", + "dev-mode-settings": "Pag-unlad ng mga setting ng mode", + "device-syncing": "Pag-sync ng aparato", + "devices": "Aparato", + "disable": "huwag paganahin", + "disable-all": "Huwag paganahin ang lahat", + "disable-later-in-settings": "Maaari mo itong hindi paganahin sa ibang pagkakataon sa Settings", + "disabled": "Disabled", + "disconnect": "Idiskonekta", + "disconnected": "Chat offline", + "discover": "tuklasin", + "dismiss": "Iwaksi", + "display-collectibles": "Ipakita ang mga koleksyon", + "done": "Tapos na", + "dont-ask": "Huwag mo ulit akong tanungin", + "edit": "Edit", + "edit-categories": "I-edit ang Mga Kategorya", + "edit-channel-title": "Edit Channel", + "edit-chats": "I-edit ang mga chat", + "edit-community": "I-edit ang pamayanan", + "edit-favourite": "baguhin ang mga paborito", + "edit-group": "I-edit ang grupo", + "edit-profile": "Ibahin ang profile", + "edited": "Na-edit", + "editing-message": "Pag-edit ng mensahe", + "empty-activity-center": "Ang iyong mga abiso sa chat\n lilitaw dito", + "empty-chat-description": "Walang mga mensahe\nsa chat na ito", + "empty-chat-description-community": "Tahimik dito sa huling {{quiet-hours}} .", + "empty-chat-description-one-to-one": "Ang anumang mga mensahe na ipinadala mo dito ay naka-encrypt at maaari mo lamang itong basahin at", + "empty-chat-description-public": "Masyadong tahimik dito para sa huling{{quiet-hours}}Simulan ang pag-uusap o", + "empty-chat-description-public-share-this": "ibahagi ang chat na ito.", + "empty-keycard-required": "Nangangailangan ng isang walang laman na Keycard", + "empty-pending-invitations-descr": "Ang mga taong nais sumali sa pangkat \n sa pamamagitan ng isang link ng imbitasyon ay lilitaw dito", + "empty-tab": "Walang laman na tab", + "enable": "Paganahin", + "enable-all": "Paganahin lahat", + "enable-link-previews": "Paganahin ang mga link previews sa chat?", + "encrypt-with-password": "I-encrypt gamit ang password", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Magdagdag ng username", + "ens-agree-to": "Sang-ayon sa", + "ens-chat-settings": "Mga setting ng chat", + "ens-custom-domain": "Custom domain", + "ens-custom-username-hints": "I-type ang buong username kabilang ang pasadyang domain tulad ng username.domain.eth", + "ens-custom-username-taken": "Hindi kabilang sa iyo ang Username :(", + "ens-deposit": "Deposito", + "ens-dismiss-message": "I-click dito upang idespatsa", + "ens-displayed-with": "Ang iyong mga mensahe ay ipinapakita sa iba kasama", + "ens-get-name": "Kumuha ng isang unibersal na username", + "ens-got-it": "Ok, nakuha ito", + "ens-locked": "Naka-lock ang Username. Hindi mo mai-release ito hanggang{{date}}", + "ens-name-content": "Pasadyang alyas para sa iyong susi sa chat na maaari iparehistro gamit ang serbisyong pangalan ng Ethereum. Ang mga pangalan ng ENS ay desentralisado na mga username.", + "ens-name-not-found": "Hindi malutas ang pangalan ng ENS", + "ens-name-title": "Pangalan ng ENS", + "ens-network-restriction": "Magagamit lamang sa Mainnet", + "ens-no-usernames": "Wala kang koneksyon sa anumang username", + "ens-powered-by": "Pinapagana ng Ethereum Name Services", + "ens-primary-username": "Pangunahing username", + "ens-register": "Magrehistro", + "ens-registration-failed": "Para mairehistro ang username, mangyaring subukang muli.", + "ens-registration-failed-title": "Nabigo ang transaksyon", + "ens-registration-failure": "Nabigo ang pagrehistro", + "ens-registration-in-progress": "Pino-progreso ang Rehistro", + "ens-release-username": "Bitawan ang username", + "ens-remove-hints": "Ang pagtanggal ay tatanggalin ang username mula sa iyong key.", + "ens-remove-username": "Alisin ang username", + "ens-saved": "ay konektado ngayon sa iyong chat key at maaaring magamit sa Status.", + "ens-saved-title": "Idinagdag ang username", + "ens-show-username": "Ipakita ang aking pangalan ng ENS sa mga chat", + "ens-terms-header": "Mga tuntunin ng pagpaparehistro ng pangalan", + "ens-terms-point-1": "Ang mga pondo ay idineposito para sa 1 taon. Ang iyong SNT ay mai-lock, ngunit hindi ginugol.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS Registry).", + "ens-terms-point-2": "Matapos ang 1 taon, maaari mong palabasin ang pangalan at ibabalik ang iyong deposito, o walang pagkilos upang mapanatili ang pangalan.", + "ens-terms-point-3": "Kung ang mga termino ng pagbabago sa kontrata - e.g Status gumagawa ng mga pag-upgrade ng kontrata - May karapatan ang gumagamit na palabasin ang username anuman ang gaganapin sa oras.", + "ens-terms-point-4": "Hindi ma-access ng Controller ng kontrata ang iyong mga na-deposito na pondo. Maaari lamang silang ilipat sa address na nagpadala sa kanila.", + "ens-terms-point-5": "Ang iyong address (es) ay maiugnay sa publiko sa iyong pangalan ng ENS.", + "ens-terms-point-6": "Ang mga username ay nilikha bilang mga subdomain node ng stateofus.eth at napapailalim sa ENS smart contract sa mga terms.", + "ens-terms-point-7": "Pinapayagan mo ang kontrata para ilipat ang SNT sa iyong ngalan. Magagawa lamang ito kapag inaprubahan mo ang isang transaksyon upang pahintulutan ang paglipat.", + "ens-terms-point-8": "Ang mga salitang ito ay ginagarantiyahan ng smart contract logic sa address:", + "ens-terms-point-9": "{{address}}(Status UsernameRegistrar)", + "ens-terms-registration": "Mga tuntunin ng pagpaparehistro ng pangalan.", + "ens-test-message": "Hey", + "ens-transaction-pending": "Naka-pending ang transaksyon ...", + "ens-understand": "Naiintindihan ko na ang aking wallet address ay maiugnay sa publiko sa aking username.", + "ens-username": "Username ng ENS", + "ens-username-already-added": "Nakakonekta na ang Username sa iyong chat key at maaaring magamit sa loob ng Status.", + "ens-username-available": "Ang username ay magagamit", + "ens-username-connected": "Ang pangalan ng gumagamit na ito ay pagmamay-ari mo at konektado sa iyong chat key.", + "ens-username-connected-continue": "Ipagpapatuloy na itakda ang aking pangalan sa ENS sa chat.", + "ens-username-connected-with-different-key": "Ang pagpapatuloy ay mangangailangan ng isang transaksyon upang ikonekta ang username sa iyong chat key.", + "ens-username-connection-confirmation": "{{username}} ay konektado sa sandaling kumpleto na ang transaksyon.", + "ens-username-hints": "Hindi bababa sa 4 na character. Mga letrang Latin, numero, at maliit na titik lamang.", + "ens-username-invalid": "Mga letra at mga numero lang.", + "ens-username-invalid-name-warning": "Ang proseso ng pagpaparehistro ng isa sa iyong mga pangalan ng en ay tapos na sa hindi wastong estado. HUWAG GAMITIN ang pangalan para sa mga transaksyon sa wallet at makipag-ugnayan sa aming suport sa support@status.im", + "ens-username-owned": "✓ Ang username ay pagmamay-ari mo.", + "ens-username-owned-continue": "Ang pagpapatuloy ay magkakokonekta sa username na ito sa iyong chat key.", + "ens-username-registration-confirmation": "Magaling! Pag-aari mo ang{{username}}sa sandaling kumpleto na ang transaksyon", + "ens-username-registration-invalid": "Babala! Ang proseso ng pagpaparehistro ay tapos na sa hindi wastong estado. HUWAG GAMITIN ang pangalan para sa mga transaksyon sa wallet at makipag-ugnayan sa aming suport sa support@status.im", + "ens-username-taken": "Nakuha na ang username :(", + "ens-username-you-can-follow-progress": "Maaari mong sundin ang pag-unlad sa seksyon ng Kasaysayan ng Transaksyon ng iyong wallet.", + "ens-usernames": "Mga pangalan ng ENS", + "ens-usernames-details": "Magrehistro ng isang unibersal na username upang madaling makilala ng ibang mga gumagamit", + "ens-want-custom-domain": "Nagmamay-ari ako ng isang pangalan sa isa pang domain", + "ens-want-domain": "Gusto ko ng isang stateofus.eth domain", + "ens-welcome-hints": "Binago ni ENS ang mga pangalan na sobrang habang address sa mga natatanging pangalan.", + "ens-welcome-point-customize": "Ang pangalan ng ENS ay maaaring palitan ang iyong 3 random word na pangalan sa chat. Maging @iyongpangalan sa halip na {{name}}.", + "ens-welcome-point-customize-title": "I-customize ang iyong pangalan ng chat", + "ens-welcome-point-receive": "Ang iba ay maaaring magpadala sa iyo ng mga pondo sa pamamagitan ng chat sa isang simpleng hakbang.", + "ens-welcome-point-receive-title": "Tumanggap ng mga transaksyon sa chat", + "ens-welcome-point-register": "Magrehistro ng isang beses upang mapanatili ang pangalan magpakailanman. Matapos ang 1 taon maaari mong pakawalan ang pangalan at maibalik ang iyong SNT.", + "ens-welcome-point-register-title": "10 SNT upang magparehistro", + "ens-welcome-point-simplify": "Maaari kang makatanggap ng mga pondo sa iyong easy-to-share ENS name kaysa sa iyong hexadecimal hash (0x...).", + "ens-welcome-point-simplify-title": "Pasimplehin ang iyong ETH address", + "ens-welcome-point-verify": "Maaari mong i-verify at magdagdag ng anumang mga username na pagmamay-ari mo sa susunod na mga hakbang.", + "ens-welcome-point-verify-title": "Mayroon ka bang isang username?", + "ens-your-username": "Ang iyong username", + "ens-your-usernames": "Ang iyong mga username", + "ens-your-your-name": "Ang iyong pangalan ng ENS", + "enter-12-words": "Ilagay ang 12 words ng iyong seed phrase, pinaghiwalay ng mga solong puwang", + "enter-a-private-key": "Ilagay ang pribadong susi", + "enter-a-seed-phrase": "Ilagay ang seed phrase", + "enter-address": "Ilagay ang address", + "enter-contact-code": "ilagay ang ENS username o chat key", + "enter-pair-code": "ilagay ang iyong pares na code", + "enter-pair-code-description": "Ang pairing code ay ipinakita sa iyo sa pag-setup ng Keycard", + "enter-password": "Ilagay ang password", + "enter-password-migration-prompt": "Ipasok ang iyong password upang ilipat ang mga contact, chat at setting kasama ang iyong mga key", + "enter-pin": "Ilagay ang iyong 6-digit na passcode", + "enter-puk-code": "Ilagay ang PUK code", + "enter-puk-code-description": "Na-block ang 6-digit na passcode.\nMangyaring ilagay ang PUK code upang i-unblock ang passcode.", + "enter-recipient-address-or-username": "Ilagay ang address o username ng tatanggap", + "enter-seed-phrase": "Ilagay ang seed phrase", + "enter-url": "Ilagay ang URL", + "enter-user-pk": "Ilagay ang user public key", + "enter-watch-account-address": "Scan ang QR code o i-enter ang address para makita", + "enter-word": "ilagay ang word", + "enter-your-code": "Ilagay ang iyong 6-digit na passcode", + "enter-your-password": "Ilagay ang iyong password", + "error": "Error", + "error-unable-to-get-balance": "Hindi makakakuha ng balanse", + "error-unable-to-get-prices": "Error sa conversion ng Pera. I-refresh ang iyong screen upang subukang muli.", + "error-unable-to-get-token-balance": "hindi makukuha ang balance ng token", + "errors": "Mga Pagkakamali", + "eth": "ETH", + "ethereum-account": "Ethereum account", + "ethereum-address": "Address ng Ethereum", + "ethereum-node-started-incorrectly-description": "Ang Ethereum node ay sinimulan ng maling pagsasaayos, titigil ang aplikasyon upang mabawi mula sa kondisyong iyon. Na-configure ang network id ={{network-id}}, aktwal ={{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Ang Ethereum node ay nagsimula nang hindi wasto", + "etherscan-lookup": "Tumingin sa Etherscan", + "everyone": "Lahat", + "expand-all": "Palawakin lahat", + "export-account": "I-export ang account", + "export-key": "I-export ang pribadong key", + "external-storage-denied": "Tinanggihan ang pag-access sa external storage", + "failed": "Nabigo", + "faq": "Mga madalas na tinatanong", + "fast": "Mabilis", + "favourite": "Mga Paborito", + "favourite-description": "Lahat ng paboritong website ay lalabas dito", + "favourites": "Mga paborito", + "favourites-empty": "Lilitaw dito ang mga address na idinagdag sa mga paborito", + "fee-cap": "Bayad sa bayarin", + "fee-explanation": "pinakamataas na pangkalahatang presyo para sa transaksyon. Kung lumampas dito ang bayad sa batayan ng block, isasama ito sa isang sumusunod na block na may mas mababang batayan sa bayarin", + "fee-options": "Iminungkahing mga pagpipilian sa bayad", + "fetch-community": "Kumuha ng pamayanan", + "fetch-messages": "↓ Kumuha ng mga mensahe", + "fetch-timeline": "↓ Fetch", + "fetching-community": "Kinukuha ang pamayanan ...", + "find": "Maghanap", + "finish": "Tapos na", + "finishing-card-setup": "Pag-setup ng card", + "fleet": "Fleet", + "fleet-settings": "Fleet settings", + "follow": "Sundan", + "follow-your-interests": "Sumali sa isang pampublikong chat at makilala ang mga bagong tao", + "free": "↓ Libre", + "from": "Mula sa", + "gas-amount-limit": "Hangganan ng halaga ng gas", + "gas-limit": "Gas limit", + "gas-price": "Presyo ng Gas", + "gas-used": "Ginamit na gas", + "generate-a-key": "Bumuo ng mga keys", + "generate-a-new-account": "Lumikha ng account", + "generate-a-new-key": "Lumikha ng isang bagong key", + "generate-account": "Bumuo ng mga keys", + "generate-an-account": "lumikha ng account", + "generate-new-key": "Lumikha ng bagong keys", + "generating-codes-for-pairing": "> Pag-download ng software ng produkto sa card\n> Bumubuo ng pag-unlock at mga pagpapares code", + "generating-keys": "Lumilikha ng keys", + "generating-mnemonic": "Bumubuo ng seed phrase", + "get-a-keycard": "Kumuha ng isang Keycard", + "get-started": "Magsimula", + "get-status-at": "Kumuha ng Status sa http:\/\/status.im", + "get-stickers": "Kumuha ng Sticker", + "give-a-short-description": "Magbigay ng maikling deskripsyon", + "give-a-short-description-community": "Bigyan ito ng isang maikling paglalarawan", + "give-permissions-camera": "Magbigay ng pahintulot\nupang ma-access ang camera", + "glossary": "Glossary", + "go-to-settings": "Pumunta sa Mga Setting ...", + "got-it": "Nakuha ko", + "grant-face-id-permissions": "Upang mabigyan ang kinakailangang pahintulot ng ID gamit ng iyong mukha, mangyaring pumunta sa iyong mga setting ng system at tiyaking napili ang Status > Face ID", + "group-chat": "Group chat", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** ay ginawang admin", + "group-chat-all-contacts-invited": "Ang lahat ng iyong mga contact ay nasa grupo na", + "group-chat-created": "**{{member}}**nilikha ang pangkat**{{name}}**", + "group-chat-decline-invitation": "Tanggihan ang paanyaya", + "group-chat-member-added": "**{{member}}** ay inimbitahan", + "group-chat-member-joined": "**{{member}}** ay sumali sa group", + "group-chat-member-removed": "**{{member}}** umalis sa group", + "group-chat-members-count": "{{selected}}\/{{max}}miyembro", + "group-chat-name-changed": "**{{member}}** Palitan ang pangalan ng group sa **{{name}}**", + "group-chat-no-contacts": "Wala ka pang mga contact.\nAnyayahan ang iyong mga kaibigan na simulan ang pakikipag-chat", + "group-info": "Impormasyon sa grupo", + "group-invite": "Paanyaya sa pangkat", + "group-invite-link": "Link ng pangkat na anyayahan", + "group-membership-request": "Kahilingan sa pagiging kasapi ng pangkat", + "gwei": "Gwei", + "has-permissions": "ay may pahintulot sa pag access", + "hash": "Hash", + "help": "tumulong", + "help-capitalized": "Tulong", + "help-center": "Sentro ng tulong", + "help-improve-status": "Tulungan mapabuti ang Status", + "hide": "Tago", + "hide-content-when-switching-apps": "Block screenshots", + "hide-content-when-switching-apps-ios": "Itago ang preview", + "history": "Kasaysayan", + "history-nodes": "Status nodes", + "hold-card": "Hold card sa likod\nng iyong telepono", + "home": "Home", + "hooks": "Hooks", + "how-it-works": "Paano ito gumagana", + "http-gateway-error": "Oops, nabigo ang kahilingan!", + "identifier": "Identifier", + "image": "Imahe", + "image-remove-current": "Alisin ang kasalukuyang larawan", + "image-source-gallery": "Pumili mula sa gallery", + "image-source-make-photo": "Kumuha", + "image-source-title": "I-edit ang larawan", + "import": "angkat", + "import-community": "Mag-import ng isang pamayanan", + "import-community-title": "Mag-import ng isang pamayanan", + "in-contacts": "Sa mga contact", + "include": "Isama", + "incoming": "Paparating", + "incoming-transaction": "Papasok na transaksyon", + "incorrect-code": [ + "str", + "Paumanhin hindi tama ang code, mangyaring ilagay muli" + ], + "increase-gas": "Taasan ang Gas", + "initialization": "Pagsisimula", + "install": "↓ Install", + "insufficient-balance-to-cover-fee": "hindi sapat na balanse upang masakop ang bayad sa transaksyon", + "intro-message1": "Maligayang pagdating sa Status!\nTapikin ang mensaheng ito upang itakda ang iyong password at magsimula.", + "intro-privacy-policy-note1": "Ang Status ay hindi kinukolekta o kumikita sa iyong personal na data. Sa pamamagitan ng pagpapatuloy, sumasang-ayon ka sa", + "intro-privacy-policy-note2": "patakaran sa privacy", + "intro-text": "Status ay ang iyong gateway sa desentralisadong web", + "intro-text1": "Makipag-chat sa isang peer-to-peer, naka-encrypt na network kung saan ang mga mensahe ay hindi mai-censor o mai-hack", + "intro-text2": "Magpadala at tumanggap ng mga digital na assets sa kahit saan sa mundo — walang kinakailangang bank account", + "intro-text3": "Galugarin ang mga laro, palitan at mga social network kung saan nag-iisa kang nagmamay-ari ng iyong data", + "intro-title1": "Tunay na pribadong komunikasyon", + "intro-title2": "Secure ang crypto wallet", + "intro-title3": "Desentralisadong apps", + "intro-wizard-text1": "Kinokontrol ng isang hanay ng mga keys ang iyong account. Nakatira ang iyong mga keys sa iyong telepono, kaya maaari mo lamang itong magamit", + "intro-wizard-text2": "Ang isang key ay para sa chat. Ito ay may nabasang pangalan na hindi mababago.", + "intro-wizard-text3": "Kung nagmamay-ari ka ng isang Keycard, itago ang iyong mga keys doon para sa pinahusay na seguridad.", + "intro-wizard-text4": "I-secure at i-encrypt ang iyong mga keys", + "intro-wizard-text6": "Ang Status ay magbabatid sa iyo tungkol sa mga bagong mensahe. Maaari mong mai-edit ang iyong mga kagustuhan sa abiso mamaya sa mga setting", + "intro-wizard-title-alt4": "Gumawa ng password", + "intro-wizard-title-alt5": "Kumpirmahin ang iyong password", + "intro-wizard-title1": "Kunin ang iyong mga keys", + "intro-wizard-title2": "Pumili ng isang pangalan ng chat", + "intro-wizard-title3": "Piliin ang key storage", + "intro-wizard-title4": "Gumawa ng 6-digit passcode", + "intro-wizard-title5": "Kumpirmahin ang passcode", + "intro-wizard-title6": "Paganahin ang mga notification", + "introduce-yourself": "Ipakilala ang iyong sarili sa isang maikling mensahe", + "invalid-address-qr-code": "Ang naka-scan na QR code ay hindi naglalaman ng isang wastong address", + "invalid-format": "Hindi wastong format\nDapat{{format}}", + "invalid-key-confirm": "Apply", + "invalid-key-content": "Ang Database ay hindi mai-encrypt dahil ang file ay corrupt. Ang iyong funds at chat key ay ligtas. Ang iba pang data, katulad ng iyong chats at contacts, hindi na maibabalik. \"{{erase-multiaccounts-data-button-text}}\" button, aalisin ang lahat ng iba pang data at magpapahintulot sa iyo na ma-access ang iyong mga pondo at magpadala ng mga mensahe.", + "invalid-number": "Di-wastong numero", + "invalid-pairing-password": "Di-wastong pagpapares ng password", + "invalid-public-chat-topic": "Hind wastong pampublikong paksa", + "invalid-range": "Hindi wastong format, dapat nasa pagitan{{min}}at{{max}}", + "invalid-username-or-key": "invalid ang username o ang chat key", + "invite": "Mag-anyaya", + "invite-button": "Mag-anyaya", + "invite-chat-accept": "Tanggapin", + "invite-chat-accept-join": "Tanggapin at Sumali", + "invite-chat-intro": "Ikaw ay ni-refer ng isang kaibigan na sumali sa Status. Narito ang ilang crypto upang makapagsimula ka! Gamitin ito sa pag reghistro ng pangalan ng ENS o bumili ng sticker pack", + "invite-chat-name": "Referral ng kaibigan", + "invite-chat-pending": "Naghihintay", + "invite-chat-rule": "Ang pagtanggap ay gagantimpalaan din ang iyong kaibigan ng isang crypto referral bonus", + "invite-chat-starter-pack": "Starter Pack", + "invite-friends": "Mag-imbita ng mga kaibigan", + "invite-instruction-fifth": "Maaari kang pumili para i-redeem ang iyong referral bonus anumang oras.", + "invite-instruction-first": "Nagpadala ka ng isang natatanging link ng imbitasyon sa iyong kaibigan upang i-download at sumali sa Status", + "invite-instruction-fourth": "Ikaw ay makakatanggap ng referral bonus at ang iyong kaibigan ng Starter Pack", + "invite-instruction-second": "Ang iyong kaibigan ay nag download ng Status at gumawa ng account (sa Android)", + "invite-instruction-third": "Sinimulan ang isang chat sa iyong kaibigan, kung saan kumpirmahin ang iyong referral", + "invite-people": "Mag-imbita ng mga tao", + "invite-privacy-policy-public": "Nag-install ka ng Status sa pamamagitan ng isang referral na link. Sa pamamagitan ng pagsali sa chat na ito naiugnay mo ang iyong referrer at sumasang-ayon sa", + "invite-privacy-policy1": "Sa pamamagitan ng pagtanggap sumasang-ayon ka sa programa ng referral", + "invite-privacy-policy2": "Mga Tuntunin at Kondisyon.", + "invite-public-chat-home": "Imbitasyon ng referral", + "invite-public-chat-intro": "Narito ang ilang crypto upang makapagsimula ka! Gamitin ito upang magparehistro ng isang pangalan ng ENS o bumili ng isang sticker pack", + "invite-receive-account": "Account upang matanggap ang iyong referral bonus", + "invite-reward": "Kumita ng crypto sa bawat kaibigan na inaanyayahan mo!", + "invite-reward-friend": "Kaibigan: ", + "invite-reward-friend-description": "Ang iyong kaibigan ay makakatanggap ng Starter Pack na binubuo ng ilang {{reward}} upang makapagsimula", + "invite-reward-friend-name": "Starter Pack", + "invite-reward-you": "Ikaw:", + "invite-reward-you-description": "Mag-imbita ng mga kaibigan at tanggapin ang {{reward}} bilang isang referral bonus. Gamitin ito para sa stickers, pangalan ng ENS at subukan ang iba pang dapps", + "invite-reward-you-name": "Referral bonus", + "invite-select-account": "Pumili ng isang account para matanggap ang iyong referral bonus", + "invite-warning": "Ang promosyon na ito ay may bisa lamang para sa mga gumagamit ng Android device, na hindi residente ng US. Kailangang kumpirmahin ng kaibigan ang referral sa loob ng 7 araw", + "invited": "inimbitahan", + "join": "Sumali", + "join-a-community": "o sumali sa isang pamayanan", + "join-group-chat": "Sumali sa group", + "join-group-chat-description": "{{username}}inanyayahan kang sumali sa grupo{{group-name}}", + "join-me": "Hali ka, samahan mo ako sa Status: {{url}}", + "join-new-private-chat": "Magsimula ng bagong pribadong chat", + "join-new-public-chat": "Sumali sa pampublikong chat", + "joined": "Sumali", + "joined-group-chat-description": "Sumali ka sa{{group-name}}mula sa paanyaya ni{{username}}", + "key": "Key", + "key-managment": "pangunahing pamamahala", + "key-on-device": "Ang pribadong key ay nai-save sa device na ito", + "keycard": "Keycard", + "keycard-access-reset": "Ang pag-access sa Keycard ay naka-reset", + "keycard-applet-install-instructions": "Para mai-install ang applet mangyaring sundin ang mga tagubilin sa https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "subukang galawin ang card paikot para makita ng NFC at mabasa sa iyong aparato.", + "keycard-awaiting-title": "Naghahanap pa rin...", + "keycard-backup": "Lumikha ng isang backup na Keycard", + "keycard-backup-success-body": "Matagumpay na nilikha ang backup card. Maaari mo na itong gamitin sa iyong account tulad ng pangunahing card.", + "keycard-backup-success-title": "Matagumpay ang backup", + "keycard-blocked": "Na-block ang Keycard.\nKailangan mong i-reset ang card upang magpatuloy sa paggamit nito.", + "keycard-can-use-with-new-passcode": "Maaari mong gamitin ang card na ito sa iyong bagong passcode", + "keycard-cancel-setup-text": "Kanselahin nito ang pag-setup ng keycard. Ito'y lubos na inirerekumenda na tapusin ang pag-setup upang magamit ang keycard. Nais mo bang kanselahin?", + "keycard-cancel-setup-title": "Mapanganib na operasyon", + "keycard-connected-description": "Subukang panatilihin pa rin ang card", + "keycard-connected-title": "Nakakonekta", + "keycard-desc": "May-ari ng isang Keycard? Itago ang iyong mga keys dito; kakailanganin mo ito para sa mga transaksyon", + "keycard-dont-ask-card": "Huwag humingi ng card upang mag-sign in", + "keycard-enter-new-passcode": "Ilagay ang bagong passcode {{step}}\/2", + "keycard-error-description": "i-konek ang card para makapag simula ulit", + "keycard-error-title": "Nawala ang koneksyon", + "keycard-factory-reset": "Ibalik ang card sa factory settings", + "keycard-factory-reset-text": "Ang pagsagawa nito ay matatanggal ang anumang mnemonic phrase na nakaimbak sa card. Tiyaking mayroon kang backup ng mnemonic phrase na ginagamit mo gamit ang Keycard na ito.", + "keycard-factory-reset-title": "Sigurado ka bang nais mong limasin ang kasaysayan ng chat na ito?", + "keycard-free-pairing-slots": "Ang Keycard ay mayroong {{n}} libreng pagpapares ng mga puwang", + "keycard-has-multiaccount-on-it": "Ang card na ito ay puno na. Ang bawat card ay maaaring humawak ng isang pangunahing keypair", + "keycard-init-description": "Ilagay ang card sa likod ng iyong telepono upang magpatuloy", + "keycard-init-title": "Naghahanap ng mga kards", + "keycard-is-blocked-details": "Hindi mo na magagamit ang card na ito upang ma-access o mag-sign para sa account na ito. Napakaraming nabigo na passcode at mga pagtatangka ng PUK.", + "keycard-is-blocked-instructions": "Para ma-access ang iyong account maaring i-reinstall ang Status at gumamit ng bagong Keycard, gumamit ng ibang wallet o i-reset ang keycard ng manu-mano.", + "keycard-is-blocked-title": "Na-block ang Keycard", + "keycard-is-frozen-details": "Upang maprotektahan ang iyong mga assets, naka-freeze ang iyong card. I-reset ang iyong card upang ma-freeze ito at makapagpadala ng mga transaksyon. Magagawa mo ito sa iyong PUK o sa iyong mnemonic.", + "keycard-is-frozen-factory-reset": "I-reset sa mnemonic", + "keycard-is-frozen-reset": "I-reset sa PUK", + "keycard-is-frozen-title": "Ang Keycard ay frozen", + "keycard-onboarding-finishing-header": "Tinatapos na", + "keycard-onboarding-intro-header": "Itago ang iyong mga keys sa Keycard", + "keycard-onboarding-intro-text": "Maghanda, maaaring tumagal ito ng ilang minuto, ngunit mahalaga na ma-secure ang iyong account", + "keycard-onboarding-mnemonic-text": "Kakailanganin mo rin ang isang piraso ng papel at isang lapis upang isulat ang iyong seed phrase", + "keycard-onboarding-pairing-header": "Pagpapares ng card ...", + "keycard-onboarding-pin-text": "Kakailanganin mong lumikha ng 6-digit na passcode na gagamitin upang maprotektahan ang pag-access sa iyong Keycard.", + "keycard-onboarding-preparing-header": "Paghahanda ng card....", + "keycard-onboarding-puk-code-header": "Isulat ang mga code\n at itago ang mga ito nang ligtas", + "keycard-onboarding-recovery-phrase-description": "Kailangan mo ang seed phrase para mabalik ang iyong key. Isulat mo. Panatilihing ligtas, offline, at paghiwalayin sa aparatong ito.", + "keycard-onboarding-recovery-phrase-header": "Back up seed phrase", + "keycard-onboarding-recovery-phrase-text": "Para sa iyong mga mata lamang. Ito ang magical seed na ginamit upang makabuo ng iyong key.", + "keycard-onboarding-start-header": "I-hold ang card sa likod\n ng iyong telepono upang magsimula", + "keycard-onboarding-start-step1": "Gumawa ng passcode", + "keycard-onboarding-start-step1-text": "Halos 1 minuto. Lumikha ng isang 6-digit na passcode upang i-encrypt ang iyong mga key", + "keycard-onboarding-start-step2": "Isulat ang PUK at ang pairing code", + "keycard-onboarding-start-step2-text": "Halos 1 minuto. Kakailanganin mo ang isang piraso ng papel at isang lapis para doon", + "keycard-onboarding-start-step3": "I-back up ang seed phrase", + "keycard-onboarding-start-step3-text": "Halos 1 minuto. Gayundin ang isang piraso ng papel at isang lapis ay kinakailangan", + "keycard-onboarding-start-text": "At panatilihin ang card sa contact sa telepono\n sa pag-setup. Ang pag-setup ay aabutin ng halos 4 na minuto", + "keycard-processing-description": "Subukang panatilihin pa rin ang card", + "keycard-processing-title": "Nagproproseso", + "keycard-recover": "nawala o frozen card?", + "keycard-recover-text": "Kung mayroon kang mnemonic phrase maaari kang lumikha ng isang bagong Keycard na nauugnay sa account na ito. Maaari mong gamitin ang alinman sa bagong Keycard o isagawa ang factory reset sa naka-freeze.", + "keycard-recover-title": "gumawa ng bagong card sa account na ito?", + "keycard-recovery-intro-button-text": "Simulan ang pagbawi", + "keycard-recovery-intro-header": "Ibalik ang mga key na naka-imbak sa keycard", + "keycard-recovery-intro-text": "Kung ikaw ay bumuo ng mga keys gamit ang keycard nuon at ngayon gusto mo gamitin ang mga keys sa mga aparato.", + "keycard-recovery-no-key-header": "Wala naman\nma recover dito", + "keycard-recovery-no-key-text": "Ang iyong Keycard ay walang nakaimbak na key dito. Upang magamit ito, bumuo ng isang bagong key at piliin ang iyong Keycard upang maiimbak ang key", + "keycard-recovery-phrase-confirm-header": "Kumpirmahin ang seed phrase", + "keycard-recovery-phrase-confirmation-text": "Hindi ka magkakaroon ng pangalawang pagkakataon! Kung nawalan ka ng access, halimbawa sa pagkawala ng iyong keycard, maaari mo lamang ma access ang iyong keys kasama ang iyong seed phrase. Walang sinuman, kundi ikaw ay mayroon ng iyong seed phrase. Isulat mo ito at panatilihing ligtas.", + "keycard-recovery-phrase-confirmation-title": "Isinulat ang seed phrase?", + "keycard-recovery-success-header": "Ang iyong mga keys ay\nmatagumpay na nabawi", + "keycard-redeem-title": "I-Redeem sa", + "keycard-redeem-tx": "Mag Redeem ng Assets", + "keycard-redeem-tx-desc": "Tapikin ang card upang mag-sign at makatanggap ng mga assets", + "keycard-reset-passcode": "Reset passcode", + "keycard-success-description": "maari mo ng alisin ang card ngayon", + "keycard-success-title": "Tagumpay", + "keycard-unauthorized-operation": "Hindi ka awtorisado upang maisagawa ang operasyong ito.\nMangyaring i-tap ang wastong card at subukang muli.", + "keycard-upsell-subtitle": "Pinahusay na seguridad at kaginhawaan", + "language": "Lengwahe", + "last-backup-performed": "Ang huling backup na ginawa:", + "last-transaction": "huling transaksyon", + "learn-more": "Dagdagan ang nalalaman", + "learn-more-about-keycard": "Matuto nang higit pa tungkol sa Keycard", + "leave": "Umalis", + "leave-chat": "Umalis sa chat", + "leave-chat-confirmation": "Aalisin ang iyong kasaysayan sa chat sa iyong aparato. Pagkatapos muling pumasok hindi mo magagawang makuha ang anumang kasaysayan mo.", + "leave-community": "Umalis sa pamayanan", + "leave-confirmation": "Umalis{{chat-name}}", + "leave-group": "Umalis sa grupo", + "left": "Umalis", + "les-ulc": "LES\/ULC", + "lets-go": "Tara na", + "light": "Mailaw", + "limit": "limitasyon", + "linked-on": "Naka-link {{date}}", + "load-messages-before": "bago ang {{date}}", + "load-more-messages": "↓ Kumuha ng higit pang mga mensahe", + "load-more-timeline": "↓ Dagdagan pa", + "loading": "Loading...", + "local-notifications": "Mga lokal na abiso", + "local-notifications-subtitle": "Paganahin ang serbisyo sa background", + "lock-app-with": "I-lock ang app na may", + "log-level": "Log level", + "log-level-settings": "Mga setting ng antas ng log", + "logging": "Logging", + "logging-enabled": "Paganahin ang logging?", + "login-pin-description": "Ilagay ang 6-digit passcode para ma-unlock ang iyong keys", + "logout": "Log out", + "logout-app-content": "Ang account ay mai-log out. Kapag binuksan mo ulit ito, gagamitin ang napiling network", + "logout-are-you-sure": "Sigurado ka bang gusto mo\nmag-log out?", + "logout-key-management": "Kailangan mong mag log out upang ma access ang key management", + "logout-title": "Log out?", + "looking-for-cards": "Naghahanap ng mga cards", + "lost-connection": "Nawala ang koneksyon", + "low-tip": "ang tip ay masyadong mababa", + "lower-than-average-tip": "mas mababa sa pamantayan na tip", + "mail-should-be-configured": "Ang mail client ay dapat isaayos", + "mailserver-address": "Status node address", + "mailserver-automatic": "Awtomatikong pagpili", + "mailserver-automatic-switch-explanation": "Piliin ang pinakamabilis na available sa Status node", + "mailserver-connection-error": "Hindi makakonekta sa Status node", + "mailserver-content": "Ang isang node sa Status network na ruta at nag-iimbak ng mga mensahe, hanggang sa 30 araw.", + "mailserver-details": "Mga detalye ng Status node", + "mailserver-error-content": "Ang pinili mong Status node ay hindi maabot", + "mailserver-error-title": "Error sa pagkonekta sa Status node", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Pumili ng iba pang Status node", + "mailserver-reconnect": "Hindi makakonekta sa Status node. I-tap upang muling kumonekta", + "mailserver-request-error-content": "Ang sumusunod na error ay naibalik ng Status node: {{error}}", + "mailserver-request-error-status": "Isang error na naganap habang kumukuha ng kasaysayan, suriin ang mga tala para sa mga detalye", + "mailserver-request-error-title": "Ang paghiling ng Status node ay error", + "mailserver-request-retry": "Muling hilingin", + "mailserver-retry": "Ulitin", + "mailserver-title": "Status node", + "main-currency": "Pangunahing pera", + "main-networks": "pangunahing networks", + "main-wallet": "Pangunahing Wallet", + "mainnet-network": "Pangunahing network", + "make-admin": "Gumawa ng admin", + "manage-connections": "Pamahalaan ang mga koneksyon mula sa loob ng aplikasyon Connections", + "manage-keys-and-storage": "Pamahalaan ang mga key at imbakan", + "mark-all-read": "Markahan ang lahat ng nabasa", + "master-account": "Master account", + "max-fee": "pinakamataas na bayad", + "max-priority-fee": "mataas na priority fee", + "maximum-fee": "pinakamataas na bayad", + "maximum-fee-desc": "Pinakamataas na kabuuang presyo para sa transaksyon. Kung ang kasalukuyang block base fee ay lumampas dito, ang iyong transaksyon ay isasama sa isang sumusunod na block o bloke na may mas mababang base fee.", + "maybe-later": "Baka mamaya", + "member-ban": "mag-ban ng miyembro", + "member-kick": "Kick member", + "members": { + "zero": "wala", + "one": "isang", + "two": "dalawang", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "members-active": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunting", + "many": "maraming", + "other": "iba pang" + }, + "members-active-none": "walang mga miyembro", + "members-count": "{{count}} miyembro", + "members-label": "Mga kasapi", + "members-limit-reached": "Naabot na ang limitasyon ng mga miyembro", + "members-title": "Mga kasapi", + "membership-approval": "Mangangailangan ng pag-apruba", + "membership-approval-description": "Ang iyong komunidad ay malayang sumali, ngunit ang mga bagong kasapi ay kinakailangang maaprubahan muna ng lumikha ng komunidad", + "membership-button": "Kahilingan sa pagiging kasapi ng pangkat", + "membership-declined": "Tinanggihan ang kahilingan sa pagiging miyembro", + "membership-description": "Ang pagiging kasapi ng pangkat ay kinakailangan kang tanggapin ng admin", + "membership-ens": "Kailangan ng ENS username", + "membership-ens-description": "Ang iyong komunidad ay nangangailangan ng isang ENS username upang makasali", + "membership-free": "Walang kinakailangan", + "membership-free-description": "Bukas ang iyong pamayanan para sumali ang sinuman", + "membership-invite": "Humiling ng paanyaya mula sa ibang miyembro", + "membership-invite-description": "Ang iyong komunidad ay maaari lamang sumali sa pamamagitan ng isang paanyaya mula sa existing na miyembro ng komunidad", + "membership-none": "Wala", + "membership-none-placeholder": "Maaari kang mangailangan ng mga bagong kasapi upang matugunan ang ilang mga pamantayan bago sila sumali. Maaari itong mabago anumang oras", + "membership-request-pending": "Nakabinbin ang kahilingan sa pagiging miyembro", + "membership-requests": "Mga kahilingan sa pagiging miyembro", + "membership-title": "Kahilingan sa pagiging kasapi ng pangkat", + "message": "Mensahe", + "message-not-sent": "Hindi ipinadala ang mensahe", + "message-options-cancel": "Pagkansela", + "message-reply": "Sagot", + "messages": "mensahe", + "messages-from-contacts-only-subtitle": "Ang mga tao lamang na iyong idinagdag bilang mga contact ay maaaring magsimula ng isang bagong chat sa iyo o mag-anyaya sa iyo sa isang grupo", + "might-break": "Maaaring masira ang ilang ÐApps", + "migration-successful": "Matagumpay ang paglipat", + "migration-successful-text": "Matagumpay na na-migrate ang account sa Keycard", + "migrations-failed-content": "{{message}}\nschema version: paunang{{initial-version}},kasalukuyang{{current-version}},huli{{last-version}}\n\nNagkaroon ng isang error sa database. Ang iyong funds at chat key ay ligtas. Ang iba pang data, kagaya ng iyong chats at mga contacts, ay hindi maibabalik. \"{{erase-multiaccounts-data-button-text}}\" button, aalisin ang lahat ng iba pang data at magpapahintulot sa iyo na ma-access ang iyong mga pondo at magpadala ng mga mensahe.", + "miners-higher-fee": "Malamang isasama ng mga minero ang iyong transaksyon mas maaga kung magbabayad ka ng mas mataas na bayad.", + "minimum-received": "Minimum na natanggap", + "mobile-network-ask-me": "Tanungin mo ako sa mobile network", + "mobile-network-continue-syncing": "Magpatuloy sa pag-sync", + "mobile-network-continue-syncing-details": "Maaari mong baguhin ito sa ibang pagkakataon sa mga setting", + "mobile-network-go-to-settings": "Pumunta sa mga setting", + "mobile-network-settings": "Mobile data", + "mobile-network-sheet-configure": "Maaari mong i-configure ang pag-sync sa higit pang\ndetalye sa", + "mobile-network-sheet-offline": "Walang Wi-fi, hindi pinagana ang pag-sync ng mensahe.", + "mobile-network-sheet-offline-details": "Ang pag-sync gamit ang mobile network ay naka-off", + "mobile-network-sheet-remember-choice": "Tandaan ang aking pinili", + "mobile-network-sheet-settings": "settings", + "mobile-network-start-syncing": "Simulan ang pag-sync", + "mobile-network-stop-syncing": "Itigil ang pag-sync", + "mobile-network-stop-syncing-details": "Hanggang sa konektado sa Wi-Fi?", + "mobile-network-use-mobile": "Gumamit ng mobile data", + "mobile-network-use-mobile-data": "Ang Status ay gumagamit ng maraming data kapag nag-sync sa mga chats at wallet.", + "mobile-network-use-wifi": "Wi-Fi lang", + "mobile-syncing-sheet-details": "Ang Status ay gumagamit ng maraming data kapag nag-syng sa chats at wallet.", + "mobile-syncing-sheet-title": "Pag-sync gamit ang mobile data?", + "more": "higit pa", + "move-and-reset": "Ilipat at I-reset", + "move-keystore-file": "Ilipat ang keystore file", + "move-keystore-file-to-keycard": "Ilipat ang keystore file sa keycard?", + "multiaccount-exists-content": "Ang Keys sa account na ito ay mayroon na at hindi maaaring idagdag muli. Kung nawala ang iyong password, passcode o Keycard, i-uninstall ang app, reinstall at i-access ang iyong key sa pamamagitan ng iyong seed phrase", + "multiaccount-exists-title": "Ang Keys sa account na ito ay mayroon na", + "multiaccounts-recover-enter-phrase-text": "Ilagay ang 12, 15, 18, 21 o 24 na mga salita.\nPaghiwalayin ang mga salita sa pamamagitan ng isang puwang.", + "multiaccounts-recover-enter-phrase-title": "Ilagay ang seed phrase", + "mute": "Mute", + "my-accounts": "Ang aking accounts", + "my-accounts-empty": "Lilitaw dito ang iyong mga magagamit na account", + "my-profile": "Profile", + "my-status": "Ang status ko", + "name": "Pangalan", + "name-of-token": "Ang pangalan ng iyong token", + "name-optional": "Pangalan (opsyonal)", + "name-your-channel": "Pangalanan ang iyong channel", + "name-your-channel-placeholder": "Pangalan ng channel", + "name-your-community": "Pangalanan ang iyong pamayanan", + "name-your-community-placeholder": "Isang kapansin pansin na pangalan", + "need-help": "Kailangan ng tulong?", + "network": "Network", + "network-chain": "Network chain", + "network-details": "Mga detalye ng network", + "network-fee": "Bayad sa network", + "network-id": "Network ID", + "network-info": "Impormasyon sa network", + "network-invalid-network-id": "Ang tinukoy na id ng network ay hindi tumutugma sa id ng network ng RPC url", + "network-invalid-status-code": "Hindi wastong katayuan ng status:{{code}}", + "network-invalid-url": "Hindi wasto ang URL ng Network", + "network-settings": "Mga setting ng network", + "never": "Hindi kailanman", + "new": "bago", + "new-category": "Bagong kategorya", + "new-chat": "Bagong chat", + "new-community-title": "Bagong komunidad", + "new-contact": "Bagong kontak", + "new-contract": "Bagong Kontrata", + "new-favourite": "Bagong paborito", + "new-group": "Bagong grupo", + "new-group-chat": "Panibagong group chat", + "new-network": "Bagong network", + "new-password": "Bagong password", + "new-pin-description": "Ilagay ang baong 6-digit na passcode", + "new-public-group-chat": "Sumali sa pampublikong Chat", + "new-puk-description": "ilagay ang bagong 12-digit PUK", + "new-status": "Bagong Status", + "new-tab": "Bagong tab", + "next": "Sunod", + "nickname": "Palayaw", + "nickname-description": "Ang mga palayaw ay tinutulungan kang makilala ang iba sa Status. \n Ikaw lang ang makakakita ng mga palayaw na idinagdag mo", + "no": "Hindi", + "no-collectibles": "Walang magagamit na mga koleksyon", + "no-contacts": "Wala pang contact", + "no-keycard-applet-on-card": "Walang applet Keycard sa card", + "no-messages": "Walang mensahe", + "no-pairing-slots-available": "Ang kard na ito ay nakapares na sa 5 na aparato at hindi maaaring ipares sa isang ito. Mangyaring gumamit ng isa sa mga ipinares na aparato, mag-log in gamit ang card na ito at libre ang pagpapares ng mga puwang sa card", + "no-pinned-messages": "Walang mga naka-pin na mensahe", + "no-result": "walang resulta", + "no-thanks": "Salamat nalang", + "no-tokens-found": "Walang nahanap na mga token", + "node-address": "Address ng node", + "node-details": "Mga detalye ng node", + "node-info": "Node info", + "node-version": "Bersyon ng nod", + "nodes-disabled": "Hindi pinapagana ang Status nodes", + "non-archival-node": "Ang RPC endpoint ay hindi suportado ng archival request. Ang iyong lokal na kasaysayan ng paglilipat ay maaring hindi makumpleto.", + "non-contacts": "Wala pang contact", + "nonce": "Nonce", + "none": "Wala", + "normal": "Normal", + "not-applicable": "Hindi naaangkop para sa mga hindi naka -ignign na transaksyon", + "not-connected-nodes": "Hindi nakakonekta sa isang status node", + "not-connected-to-peers": "Hindi konektado sa anumang mga kapantay", + "not-enough-snt": "Hindi sapat ang iyong SNT", + "not-found": "Hindi mahanap", + "not-keycard-text": "Ang card na iyong ginamit ay hindi isang Keycard. Kailangan mong bumili ng isang Keycard upang magamit ito.", + "not-keycard-title": "Hindi isang Keycard", + "not-registered": "hindi rehistrado", + "notification-settings": "Mga Abiso", + "notifications": "Mga notification", + "notifications-non-contacts": "Mga notification mula sa mga hindi contact", + "notifications-preferences": "Mga kagustuhan sa abiso", + "notifications-servers": "Notification servers", + "notifications-switch": "Ipakita ang mga notification", + "notifications-transactions": "Mga transaksyon sa wallet", + "notify": "Ipaalam", + "now": "Ngayon", + "off": "Off", + "off-status-tree": "Off Status tree", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Paganahin ang Status node upang kumuha ng mga mensahe na ipinadala habang ang app ay sarado. Kapag pinagana, ang status node ay kukunin ang iyong IP address. Kapag hindi pinagana hindi ka makakatanggap ng mga mensahe kapag ang app ay sarado at hindi makikita ang mga ito kapag binuksan mo ang app sa paglaon.", + "offline-messaging-use-history-nodes": "Gumamit ng Status nodes", + "ok": "OK", + "ok-continue": "Okay, magpatuloy", + "ok-got-it": "Okay, nakuha ko", + "ok-save-pass": "OK, i-save ang password", + "okay": "Okay", + "on": "On", + "on-status-tree": "Sa Puno ng Status", + "once-enabled-share-metadata": "Kapag pinagana, ang mga link na nai-post sa chat ay maaaring ibahagi ang iyong metadata sa site", + "one-day": "Isang araw", + "one-month": "Isang buwan", + "one-week": "Isang linggo", + "open": "bukas", + "open-chat": "Buksan ang chat", + "open-dapp": "Buksan ang ÐApp", + "open-dapp-store": "Tuklasin ang ÐApps", + "open-home": "Buksan ...", + "open-in-new-tab": "Magbukas ng bagong tab", + "open-membership": "Bukas na miyembro", + "open-nfc-settings": "Buksan ang settings ng NFC", + "open-on-block-explorer": "Buksan ang block explorer", + "opening-buy-crypto": "Pagbubukas{{site}}", + "optimal": "pinakamainam", + "optional": "opsyonal", + "or": "O", + "outgoing": "Papalabas", + "outgoing-transaction": "Papalabas na transaksyon", + "page-camera-request-blocked": "hinarangan ang mga kahilingan sa camera. Upang paganahin ang mga kahilingan sa camera pumunta sa Mga Setting", + "page-would-like-to-use-camera": "nais mo bang gamitin ang iyong camera", + "pair": "I-Pares ang aparato", + "pair-card": "Ipares sa aparatong ito", + "pair-code": "Pair codePares ng code", + "pair-code-explanation": "Pagpapares card sa ibang aparato (hanggang sa 5) upang i-unlock ang mga key at mag-sign ng mga transaksyon sa parehong Keycard", + "pair-code-placeholder": "pares ng code", + "pair-this-card": "Ipares ang card na ito", + "pair-this-device": "Mag-advertise ng aparato", + "pair-this-device-description": "Ipares ang iyong mga aparato upang i-sync ang mga contact at chat sa pagitan nila", + "paired-devices": "Mga aparato na ipinares", + "pairing": "Pagpapares", + "pairing-card": "Pagpapares card", + "pairing-changed": "Ang pairing code ay napalitan na", + "pairing-code-placeholder": "Pairing code...", + "pairing-code_error1": "Ang iyong pairing code ay di tumutugma", + "pairing-go-to-installation": "Pumunta sa mga setting ng pagpapares", + "pairing-maximum-number-reached-content": "Mangyaring huwag paganahin ang isa sa iyong mga aparato bago paganahin ang bago.", + "pairing-maximum-number-reached-title": "Naabot ang maximum na bilang ng mga aparato", + "pairing-new-installation-detected-content": "Ang isang bagong aparato ay napansin.\nUpang magamit nang tama ang iyong mga aparato, mahalaga na ipares at paganahin ang mga ito bago gamitin.\nMangyaring pumunta sa seksyon ng aparato sa ilalim ng mga setting upang ipares ang iyong mga aparato.", + "pairing-new-installation-detected-title": "May nakitang bagong device\/aparato", + "pairing-no-info": "Walang impormasyon", + "pairing-please-set-a-name": "Mangyaring magtakda ng isang pangalan para sa iyong aparato.", + "passphrase": "Passphrase", + "password": "Password", + "password-description": "Ng hindi bababa sa 6 na mga character. Pinoprotektahan ng iyong password ang iyong mga susi. Kailangan mo ito upang i-unlock ang Status at lumipat.", + "password-mismatch": "Ang bagong password at kumpirmasyon ay hindi tugma", + "password-placeholder": "Password...", + "password-placeholder2": "Kumpirmahin ang iyong password", + "password-reset-in-progress": "Pagbago ng password", + "password-reset-success": "I-reset ang password", + "password-reset-success-message": "Kakailanganin mong mag-sign in muli", + "password_error1": "Hindi tumutugma ang mga password.", + "paste": "Paste", + "paste-json": "Idikit JSON", + "pay-to-chat": "Magbayad sa chat", + "peer-content": "Ang isang aparato na konektado sa Status chat network. Ang bawat gumagamit ay maaaring kumatawan sa isa o higit pang mga kapantay depende sa kanilang bilang ng mga aparato.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Peers", + "pending": "Naghihintay", + "pending-confirmation": "Pending na kumpirmasyon ...", + "pending-invitations": "Nakabinbin ang mga kahilingan sa pagiging miyembro", + "per-gas-price-limit": "Limitasyon sa presyo ng bawat gas", + "per-gas-tip-limit": "Limitasyon sa tip ng bawat gas", + "perform-backup": "Magsagawa ng backup", + "permissions": "Pahintulot", + "phone-e164": "International 1", + "photos": "Litrato", + "photos-access-error": "Upang bigyan ang kinakailangang pahintulot ng mga larawan, mangyaring pumunta sa mga setting ng iyong system at tiyakin na ang Status >Napiling mga larawan.", + "pin": "Pin", + "pin-changed": "Binago ang 6-digit na passcode", + "pin-code": "6-digit passcode", + "pin-limit-reached": "Naabot ang limitasyon ng pag-pin. I-unpin muna ang isang nakaraang mensahe.", + "pin-mismatch": "Maling passcode", + "pin-one-attempt": "isang pagtatangka", + "pin-one-attempt-blocked-after": "bago maharang ang iyong Keycard", + "pin-one-attempt-blocked-before": "Mag-ingat, mayroon ka lamang", + "pin-one-attempt-frozen-after": "bago mag-frozen ang iyong Keycard", + "pin-one-attempt-frozen-before": "Mag-ingat, mayroon ka lamang", + "pin-retries-left": "{{number}} natitirang mga pagtatangka", + "pinned-by": "Naka-pin ni", + "pinned-messages": "Naka-pin na mga mensahe", + "pinned-messages-count": { + "zero": "sero", + "one": "isa", + "two": "dalawa", + "few": "kaunti", + "many": "marami", + "other": "iba pa" + }, + "pinned-messages-empty": "Lilitaw dito ang mga naka-pin na mensahe. Upang mai-pin ang isang mensahe, pindutin nang matagal ito at i-tap ang 'I-pin`", + "powered-by-paraswap": "Pinapatakbo ng Paraswap", + "preference": "Kagustuhan", + "preview-privacy": "I-preview ang mode ng privacy", + "previewing-may-share-metadata": "Ang pag-preview ng mga link mula sa mga website na ito ay maaaring ibahagi ang iyong metadata sa kanilang mga may-ari", + "price-impact": "Epekto sa presyo", + "price-impact-desc": "Tinantyang epekto sa presyo para sa transaksyong ito. Kung ang kasalukuyang block base fee ay lumampas dito, ang iyong transaksyon ay isasama sa isang sumusunod na block na may mas mababang base fee.", + "principles": "Mga Prinsipyo", + "priority": "Priyoridad", + "privacy": "Pagkapribado", + "privacy-and-security": "Pagkapribado at seguridad", + "privacy-photos": "Pagkapribado ng Larawan sa Profile", + "privacy-policy": "Patakaran sa pagkapribado", + "privacy-show-to-warning": "Ang mga taong nakakita na ng iyong larawan sa profile ay magpapatuloy na gawin ito", + "private-key": "Pribadong key", + "private-notifications": "Pribadong mga abiso", + "private-notifications-descr": "Aabisuhan ka ng Status tungkol sa mga bagong mensahe. Maaari mong mai-edit ang iyong mga kagustuhan sa notification mamaya sa mga setting.", + "processing": "Sandali lang", + "product-information": "Impormasyon sa Produkto", + "profile": "Profile", + "profile-deleted-content": "Matagumpay na tinanggal ang iyong profile", + "profile-deleted-keycard": "Maaari mo na ngayong ibalik ang isa pang keypair sa iyong Keycard", + "profile-deleted-title": "Natanggal ang Profile", + "profile-details": "Mga detalye ng profile", + "profile-not-found": "Hindi mahanap ang profile", + "profile-pic-pick": "Pumili mula sa gallery", + "profile-pic-remove": "Alisin ang larawan", + "profile-pic-take": "Kunan ng litrato", + "profile-picture-updated": "Nai-update ang larawan sa profile", + "public-channel": "Pampublikong Channel", + "public-chat": "Pampublikong chat", + "public-chat-description": "Sumali sa mga pampublikong chat para sa iyong mga interest! Kahit sino ay maaaring magsimula ng bago.", + "public-chats": "Mga pampublikong chat", + "public-group-status": "Pampubliko", + "public-group-topic": "Paksa", + "public-key": "Public key", + "puk-and-pairing-codes-displayed": "Ipinapakita ang mga PUK at pagpapares ng mga code", + "puk-changed": "Napalitan na ang 12-digit PUK", + "puk-code": "PUK code", + "puk-code-explanation": "Kung nakalimutan mo ang iyong 6-digit na passcode o hindi tama nang ipinasok nang 3 beses, kakailanganin mo ang code na ito upang i-unlock ang iyong card.", + "puk-mismatch": "Mali ang PUK code", + "push-failed-transaction": "Nabigo ang iyong transaksyon", + "push-failed-transaction-body": "{{value}} {{currency}} hanggang {{to}}", + "push-inbound-transaction": "Nakatanggap ka ng {{value}} {{currency}}", + "push-inbound-transaction-body": "Mula sa {{from}} hanggang sa {{to}}", + "push-notifications-server-enabled": "Pinagana ang server", + "push-notifications-servers": "Push notification servers", + "push-outbound-transaction": "Nagpadala ka ng {{value}} {{currency}}", + "push-outbound-transaction-body": "Mula sa {{from}} hanggang sa {{to}}", + "quiet-days": "{{quiet-days}}araw", + "quiet-hours": "{{quiet-hours}}oras", + "re-encrypt-key": "I-encrypt muli ang iyong mga keys", + "rearrange-categories": "Ayusin muli ang Mga Kategorya", + "receive": "Tumanggap", + "receive-transaction": "Tumanggap ng transaksyon", + "recent": "Kamakailan", + "recent-empty": "Ang mga nagamit na addresses ay lalabas dito", + "recent-recipients": "Contacts", + "recently-used-stickers": "Ang mga kamakailang ginamit na sticker ay lilitaw dito", + "recipient": "Tagatanggap", + "recipient-code": "ilagay ang address ng tatanggap", + "recipient-code-placeholder": "0x.. o username.domain.eth", + "recover": "Bawiin", + "recover-key": "I-access ang umiiral na mga key", + "recover-keycard-multiaccount-not-supported": "Ang keys sa account na ito ay mayroon na at hindi na maaring idagdag muli. Kung nawala ang iyong password, passcode o Keycard, i-uninstall ang app, reinstall at i-access ang iyong key sa pamamagitan ng iyong seed phrase", + "recover-with-keycard": "I-recover gamit ang Keycard", + "recover-with-seed-phrase": "Bawiin gamit ang seed phrase", + "recovering-key": "Pag-access sa mga key ...", + "recovery-confirm-phrase": "kumpirmahin ang seed phrase", + "recovery-phrase": "Seed phrase", + "recovery-success-text": "Kailangan mong lumikha ng isang bagong code o password upang muling i-encrypt ang iyong mga key", + "recovery-typo-dialog-description": "Paalala, ang iyong seed phrase dapat eksaktong, parehong mga salita at pagkakasunud-sunod habang natanggap mo ito", + "recovery-typo-dialog-title": "ang seed phrase ay tama?", + "redeem-amount": "{{quantity}} makukuhang mga bonus", + "redeem-now": "Kunin ngayon", + "redeem-success": "Kunin ang tagumpay ng bonus!", + "reduced-tip": "ang priyoridad na tip ay babawasan", + "refresh": "Refresh", + "registered": "rehistrado", + "reject": "Tanggihan", + "reject-and-delete": "Tanggihan at tanggalin", + "remember-me": "Tandaan mo ako", + "remind-me-later": "Ipakita mo sa akin ito muli", + "remote-notifications": "Mga malalayong abiso", + "remote-notifications-subtitle": "Paganahin ang google push notifications\nMagdagdag ng Termino...", + "remove": "Alisin", + "remove-favourite": "Mag-alis ng paborito", + "remove-from-chat": "Alisin sa chat", + "remove-from-contacts": "Tanggalin sa mga kontak", + "remove-from-contacts-text": "Sa pamamagitan ng pag-alis ng isang user sa iyong listahan ng kontak hindi mo ma itago ang iyong wallet address sa kanila.", + "remove-group": "Alisin ang pangkat", + "remove-network": "Alisin ang network", + "remove-token": "Alisin ang token", + "removed": "alisin", + "repeat-pin": "Ulitin ang bagong 6-digit na passcode", + "repeat-puk": "Ulitin ang bagong 12-digit PUK", + "replying-to": "Tumugon kay {{author}}", + "report-bug-email-template": "1. Paglalarawan ng Isyu\n{{description}}\n\n\n2. Mga hakbang sa pagpaparami\n{{steps}}\n\n3. Maglakip ng mga screenshot na maaaring mag-demo ng problema, mangyaring", + "request-access": "Humiling ng pag-access", + "request-feature": "Humiling ng isang tampok", + "request-membership": "Humiling ng pagiging miyembro", + "request-pending": "Nakabinbin ang kahilingan…", + "request-transaction": "Humiling ng transaksyon", + "required-field": "Required field", + "resend-message": "I-resend", + "reset-card": "I-reset ang card", + "reset-card-description": "Ang operasyon na ito ay i-reset ang card sa paunang estado. Tatanggalin nito ang lahat ng data ng kard kasama ang mga pribadong key. Hindi maibabalik ang operasyon.", + "reset-database": "I-reset ang database", + "reset-database-warning": "Tanggalin ang mga chats, contacts at settings. Kinakailangan kapag nawala mo ang iyong password", + "reset-database-warning-keycard": "Burahin ang mga chat, contact at settings.", + "reset-password": "I-reset ang password", + "restore-defaults": "Ibalik sa dating ayos", + "retry": "Ulitin", + "revoke-access": "Bawiin ang access", + "rpc-url": "RPC URL", + "rpc-usage-copy": "Kopya", + "rpc-usage-filter": "Mga pamamaraan sa pagsala", + "rpc-usage-get-stats": "Refresh", + "rpc-usage-info": "Mga istatistika ng paggamit ng RPC", + "rpc-usage-reset": "I-reset", + "safe-estimate": "Ligtas na pagtatantya", + "save": "I-save", + "save-password": "I-save ang password", + "save-password-unavailable": "Itakda ang passcode ng aparato upang i-save ang password", + "save-password-unavailable-android": "Hindi magagamit ang pag-save ng password: ang iyong aparato ay maaaring ma-root o walang kinakailangang mga tampok ng seguridad.", + "scan-qr": "I-scan ang QR code", + "scan-qr-code": "I-scan ang isang QR code na may isang address ng pitaka", + "scan-tokens": "I-scan ang tokens", + "search": "Paghahanap", + "search-no-chat-found": "Walang resulta. Ang ibig mong sabihin ay", + "secret-keys-confirmation-text": "Kakailanganin mo ang mga ito upang magpatuloy na gamitin ang iyong Keycard kung sakaling mawala mo ang iyong telepono.", + "secret-keys-confirmation-title": "Nasulat mo na ang mga codes?", + "security": "Seguridad", + "see-details": "Tignan ang detalye", + "see-it-again": "Tingnan muli", + "see-suggestions": "Tingnan ang mga mungkahi", + "seed-key-uid-mismatch": "Hindi tumutugma ang seed", + "seed-key-uid-mismatch-desc-1": "Ang seed phrase na ipinasok mo ay hindi tumutugma sa {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Upang maisagawa ang mga keys para sa account na ito, i-verify ang iyong seed phrase at subukang muli.", + "seed-phrase-content": "Isang hanay ng mga friendly-to-read na salita, at sapalarang napili mula sa BIP39 ay karaniwang listahan at ginagamit para ma access ang iyong Ethereum account sa iba pang wallets at devices. Tinukoy din bilang a “mnemonic phrase,” “recovery phrase” o “wallet backup” sa buong crypto ecosystem. Karamihan sa crypto apps ginagamit ang parehong standard to generate accounts.", + "seed-phrase-placeholder": "Seed phrase...", + "seed-phrase-title": "Seed Phrase", + "select": "Piliin", + "select-account": "Piliin ang account", + "select-account-dapp": "Piliin ang account na nais mong gamitin sa mga Dapp", + "select-account-first": "Piliin muna ang isang account", + "select-chat": "Piliin ang chat upang simulan ang pagmemensahe", + "select-new-location-for-keys": "Pumili ng isang bagong lokasyon upang i-save ang iyong (mga) pribadong keys", + "select-token-to-receive": "Pumili ng token na matatanggap", + "select-token-to-swap": "Pumili ng token na Ipapalit", + "selected": "Napili", + "send-logs": "I-report ang bug", + "send-logs-to": "Mag-ulat ng isang bug sa{{email}}", + "send-message": "Magpadala ng Mensahe", + "send-push-notifications": "Magpadala ng Mga Push Notification", + "send-push-notifications-description": "Kapag naka disabled, ang taong tumatanggap ng iyong mga mensahe ay hindi aabisuhan tungkol sa pagdating nito.", + "send-request": "Magpadala ng kahilingan", + "send-request-amount": "Halaga", + "send-request-amount-max-decimals": "Ang Max Number ng desimal ay{{asset-decimals}}", + "send-request-unknown-token": "Hindi kilalang token -{{asset}}", + "send-sending-to": "para kay{{recipient-name}}", + "send-transaction": "Magpadala ng transaksyon", + "sending": "Nagpapadala", + "sent-at": "Ipinadala sa", + "server": "Server", + "set-a-topic": "gumawa ng topic", + "set-currency": "Itakda ang pera", + "set-custom-fee": "Itakda ang pasadyang bayad", + "set-dapp-access-permissions": "Itakda ang mga pahintulot sa pag-access ng DApp", + "set-max": "Itakda ang max", + "settings": "Settings", + "share": "Ibahagi", + "share-address": "Share address", + "share-chat": "Ibahagi ang chat", + "share-contact-code": "Ibahagi ang aking susi sa chat", + "share-dapp-text": "Tingnan ang DApp na ginagamit ko sa Status:{{link}}", + "share-link": "Ibahagi ang Link", + "share-logs": "Ibahagi ang mga log", + "share-my-profile": "Ibahagi ang link sa profile", + "share-profile": "Ibahagi ang profile", + "share-profile-link": "Ibahagi ang link sa profile", + "share-public-chat-text": "Tingnan ang pampublikong chat na ito sa Status app:{{link}}", + "shared": "Ibinahagi", + "sharing-copied-to-clipboard": "Kinopya", + "sharing-copy-to-clipboard": "Kopya", + "sharing-data-desc-1": "Ang data ay napatunayan laban sa mga pampublikong panuntunan upang matiyak na walang sensitibong data na naipadala. Huwag magtiwala, i-verify.", + "sharing-data-desc-2": "Ang usage data ay ipinadala end-to-end na naka-encrypt sa peer-to-peer network ng Status", + "sharing-data-desc-3": "Sa halip na iyong regular na chat key, ginagamit ang isang solong key para magamit", + "sharing-data-desc-4": "Ang usage data ay hindi maaaring maiugnay sa iyong IP address", + "sharing-data-desc-5": "Ang Cumulative data ng lahat ng mga gumagamit ay magagamit pampubliko", + "sharing-data-desc-6": "Ang data ay tinanggal mula sa iyong telepono pagkatapos maipadala ang mga ito", + "sharing-share": "Ibahagi", + "show-less": "Magpakita ng mas kaunti", + "show-more": "Magpakita pa", + "show-notifications": "Ipakita ang mga notification", + "show-profile-pictures": "Tingnan ang mga larawan sa profile", + "show-profile-pictures-to": "Ipakita ang iyong larawan sa profile sa", + "show-qr": "Ipakita ang QR code", + "show-transaction-data": "Ipakita ang data ng transaksyon", + "sign-and-send": "Mag-sign at ipadala", + "sign-anyway": "Mag-sign pa rin", + "sign-in": "mag sign-in", + "sign-message": "Mag-sign Message", + "sign-out": "Mag-sign out", + "sign-request-failed": "Hindi ma-sign na mensahe", + "sign-with": "Mag-sign in", + "sign-with-password": "Mag-sign in gamit ang password", + "sign-you-in": "Nag-sign in ka ...", + "signing": "Pag-sign", + "signing-a-message": "Pag-sign ng isang mensahe", + "signing-phrase": "Pag sign ng phrase", + "skip": "Laktawan", + "slippage": "Slippage", + "slow": "Mabagal", + "something-went-wrong": "May nangyari na mali", + "soon": "Sa lalong madaling panahon", + "specify-address": "Tukuyin ang address", + "specify-name": "Tukuyin ang isang pangalan", + "specify-network-id": "Tukuyin ang id ng network", + "specify-rpc-url": "Tukuyin ang isang URL ng RPC", + "specify-server-public-key": "Ilagay ang server public key", + "specify-symbol": "Tukuyin ang simbolo", + "start-chat": "Simulan ang chat", + "start-conversation": "Simulan ang pag-uusap", + "start-group-chat": "Simulan ang group chat", + "start-new-chat": "Magsimula ng bagong chat", + "starter-pack-coming": "Darating sa inyo ang starter pack", + "starter-pack-coming-description": "Maaaring tumagal ng ilang minuto hanggang oras", + "starter-pack-received": "Natanggap ang Starter Pack", + "starter-pack-received-description": "Narito ang ilang mga crypto upang makapagsimula ka! Gamitin ito upang makakuha ng mga sticker, isang pangalan ng ENS at subukan ang mga dapps", + "status": "Status", + "status-always-online": "Laging Online", + "status-automatic": "Awtomatiko", + "status-automatic-subtitle": "Awtomatikong itakda ang status", + "status-confirmed": "Nakumpirma", + "status-dnd": "Huwag abalahin", + "status-dnd-subtitle": "I-mute ang lahat ng notification", + "status-hardwallet": "Status hardwallet", + "status-inactive": "Hindi aktibo", + "status-inactive-subtitle": "Itinatago ang iyong online na status", + "status-is-open-source": "Ang Status ay bukas na mapagkukunan", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Ang Status ay may kaugaliang gumamit ng maraming data kapag nagsi-sync ng mga chat. Maaari mong piliing hindi mag-sync kapag nasa mobile network", + "status-not-sent-click": "Hindi kumpirmado. Mag-click para sa mga pagpipilian", + "status-not-sent-tap": "Hindi kumpirmado. I-tap para sa mga pagpipilian", + "status-pending": "Naghihintay", + "status-sent": "Ipinadala", + "status-tx-not-found": "Hindi nahanap ang TX", + "status-updates-descr": "Ang Status updates ay lalabas dito. Idagdag ang profile bilang isang contact upang makatanggap ng mga update sa iyong timeline.", + "statuses-descr": "Ibahagi kung ano ang nasa isip mo at manatiling nai-update sa iyong mga contact", + "statuses-my-status-descr": "Ibahagi kung ano ang nasa isip mo. Ang sinumang bumibisita sa iyong profile ay makakakita ng iyong kalagayan. Ang mga taong in-add ka bilang kanilang contact ay makakatanggap ng iyong mga update sa kanilang timeline", + "step-i-of-n": "gabay{{step}}sa{{number}}", + "sticker": "Sticker", + "sticker-market": "Sticker market", + "storage": "Imbakan", + "submit": "Ipasa", + "submit-bug": "Magsumite ng isang bug", + "success": "Tagumpay", + "suggested-min-tip": "iminungkahing minimum na tip", + "suggested-price-limit": "Iminumungkahing limitasyon sa presyo", + "swap": "magpalit", + "swap-details": "Magpalit ng mga detalye", + "switch-to-simple-interface": "Lumipat sa simpleng interface", + "symbol": "Simbolo", + "sync-all-devices": "Sync lahat ng aparato", + "sync-in-progress": "Syncing...", + "sync-settings": "Mga setting ng pag-sync", + "sync-synced": "Sa pag-sync", + "syncing-devices": "Syncing...", + "system": "System", + "tabs": "tabs", + "tag-was-lost": "nawala ang tag", + "tap-card-again": "Tapikin muli ang card sa likod ng iyong telepono", + "terms-of-service": "Mga Tuntunin ng Paggamit", + "test-networks": "Mga network ng pagsubok", + "text-input-disabled": "Mangyaring maghintay sandali ...", + "thank-you": "Salamat", + "this-device": "Ang aparato na ito", + "this-device-desc": "Ang iyong mga keys ay naka-encrypt at ligtas na nakaimbak sa iyong aparato", + "this-is-you-signing": "Ito ang iyong signing phrase", + "this-will-take-few-seconds": "Ito ay tatagal ng ilang segundo", + "three-days": "Tatlong araw", + "three-words-description": "Dapat mong makita ang mga 3 salita bago mag-sign sa bawat transaksyon", + "three-words-description-2": "Kung nakakita ka ng ibang kombinasyon, kanselahin ang transaksyon at mag-sign out", + "timeline": "Timeline", + "tip-cap": "Tip cap", + "to": "Para kay", + "to-block": "Block", + "to-enable-biometric": "Para paganahin{{bio-type-label}} kailangan mong i-save ang iyong password sa unlock screen", + "to-encrypt-enter-password": "Para ma encrypt ang account ilagay ang iyong password", + "to-see-this-message": "Upang makita ang mensaheng ito,", + "token": "Token", + "token-auto-validate-decimals-error": "Maling mga decimals para sa token {{symbol}} sa address {{address}} - itakda sa {{expected}} ngunit nakita bilang {{actual}}", + "token-auto-validate-name-error": "Maling pangalan para sa token {{symbol}} sa address {{address}} - itakda sa {{expected}} ngunit nakita bilang {{actual}}", + "token-auto-validate-symbol-error": "Maling simbolo para sa token{{symbol}}sa address{{address}}- itakda sa{{expected}}ngunit nakita bilang{{actual}}", + "token-details": "detalye ng token", + "topic-name-error": "Gumamit lamang ng mga maliliit na titik (a to z), numbero at dashes (-) Huwag gumamit ng chat keys", + "total-gas": "Kabuuang gas", + "transaction": "Transaksyon", + "transaction-data": "Data ng transaksyon", + "transaction-declined": "Hindi tinanggap ang transaksyon", + "transaction-description": "Isaalang-alang itong kumpleto pagkatapos ng 12 kumpirmasyon sa network.", + "transaction-details": "Mga detalye ng transaksyon", + "transaction-failed": "Nabigo ang transaksyon", + "transaction-fee": "Bayad sa transaksyon", + "transaction-history": "Kasaysayan ng Transaksyon", + "transaction-request": "Kahilingan sa Transaksyon", + "transaction-sent": "Ipinadala ang transaksyon", + "transaction-signed": "Ang transaksyon ay matagumpay na naka-sign", + "transactions": "Transaksyon", + "transactions-filter-select-all": "Piliin lahat", + "transactions-filter-title": "Filter ng kasaysayan", + "transactions-history": "Kasaysayan ng Transaksyon", + "transactions-history-empty": "Wala pang transaksyon sa iyong kasaysayan", + "transactions-history-loading": "Naglo-load ng kasaysayan ng transaksyon. Maaaring tumagal ito ng ilang sandali.", + "transactions-load-more": "Mag-load pa", + "transactions-management-enabled": "Pangangasiwa ng transaksyon", + "transactions-sign": "Sign", + "transfer-ma-unknown-error-desc-1": "Mukhang hindi tinanggal ang iyong multiaccount. Maaaring na-reset ang database", + "transfer-ma-unknown-error-desc-2": "Mangyaring suriin ang iyong listahan ng account at subukang muli. Kung ang account ay hindi nakalista pumunta sa Access existing keys upang mabawi gamit ang seed phrase", + "transfers-fetching-failure": "Hindi ma-update ang kasaysayan ng paglilipat. Suriin ang iyong koneksyon at hilahin pababa upang subukang muli", + "tribute-required-by-multiaccount": "{{multiaccount-name}}nangangailangan ng SNT para magsimula ng isang chat.", + "tribute-state-paid": "Bayad ang nai-ambag", + "tribute-state-pending": "Pending na tribute", + "tribute-state-required": "Nangangailangan{{snt-amount}}SNT tribute", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Magdagdag ng mga kaibigan bilang isang contact upang payagan ang mga chat nang walang bayad sa pagkilala.", + "tribute-to-talk-are-you-friends": "Magkaibigan ba kayo?", + "tribute-to-talk-ask-to-be-added": "Hilingin na maidagdag bilang isang contact", + "tribute-to-talk-contact-received-your-tribute": "natanggap ang iyong parangal. Maaari ka nang ligtas na makipag-chat sa bawat isa.", + "tribute-to-talk-desc": "I-monetize iyong pansin sa pamamagitan ng pag-uutos sa SNT para sa mga bagong tao upang magsimula ng isang chat", + "tribute-to-talk-disabled": "Ang Tribute to Talk ay di pinagana", + "tribute-to-talk-disabled-note": "Simula ngayon, ang mga baguhan ay maaaring magsimula ng isang chat sa iyo nang hindi nagpapadala ng SNT.", + "tribute-to-talk-enabled": "Pinapagana mo ang Tribute to Talk", + "tribute-to-talk-finish-desc": "Mula ngayon, makakatanggap ka lamang ng mga chat mula sa mga contact, at mga taong nagbayad", + "tribute-to-talk-learn-more-1": "Ang iyong oras at atensyon ay ang iyong pinakamahalagang pag-aari. Hinahayaan ka ng Tribute to Talk na magtakda ka ng isang halaga ng SNT na kinakailangan para sa mga bagong tao upang magsimula ng isang chat sa iyo.", + "tribute-to-talk-learn-more-2": "Ang sinumang wala sa iyong listahan ng contact ay hihilingin na bayaran, at maaari kang tumugon sa sandaling mayroon sila.", + "tribute-to-talk-learn-more-3": "Maaari mong palaging maibalik ang pera, ngunit upang matiyak na ang mga kaibigan ay maaaring maabot mo nang malayang, idagdag ang mga ito bilang isang contact muna.", + "tribute-to-talk-paywall-learn-more-1": "Ang aming oras at pansin ay ang aming pinakamahalagang pag-aari. Hinahayaan ka ng Tribute to Talk na makipag-ugnay sa mga bagong tao kapalit ng pagbabayad sa SNT.", + "tribute-to-talk-paywall-learn-more-2": "Para magsimula ng isang chat sa isang taong may set ng pagkilala, bayaran lamang ang kinakailangang SNT at idadagdag ka bilang isang contact.", + "tribute-to-talk-paywall-learn-more-3": "Kung kilala mo ang mga ito, maaari mong ibahagi ang iyong profile sa labas ng Status na idaragdag nang libre.", + "tribute-to-talk-pending": "nakabinbing kumpirmasyon ng Tribute", + "tribute-to-talk-pending-note": "Tribute transaction ay nakabinbing kumpirmasyon sa network. Maaari mong suriin ito sa Status sa kasaysayan ng transaksyon", + "tribute-to-talk-removing-note": "Ang pag-alis ng Tribute to tak ay papayagan ang mga baguhan na magsimula ng isang chat nang hindi nagpapadala ng SNT. Nangangailangan ng isang transaksyon na gagawin.", + "tribute-to-talk-set-snt-amount": "Itakda ang halaga ng SNT na kinakailangan para sa mga bagong tao upang magsimula ng isang chat", + "tribute-to-talk-signing": "Naghihintay na mag-sign transaksyon", + "tribute-to-talk-transaction-failed-note": "Nabigo ang transaksyon at ang iyong mga setting ng Tribute to Talk ay hindi nabago", + "tribute-to-talk-tribute-received1": "Natanggap ang ambag. Ikaw at", + "tribute-to-talk-tribute-received2": "ngayon ay mga contact at maaaring ligtas na makipag-chat sa bawat isa.", + "tribute-to-talk-you-require-snt": "Kailangan mo ng SNT para sa mga bagong tao na magsimula ng isang chat.", + "try-again": "Subukan muli", + "try-keeping-the-card-still": "Subukang panatilihin pa rin ang card", + "turn-nfc-description": "NFC ay hindi pinagana sa iyong aparato. Maaari mo itong paganahin sa mga setting", + "turn-nfc-on": "I-on ang NFC upang magpatuloy", + "two-minutes": "dalawang minuto", + "tx-fail-description1": "Ang transaksyon na ito ay malamang na mabigo. Mag-sign sa iyong sariling panganib gamit ang pasadyang bayad sa network.", + "tx-fail-description2": "Ang transaksyon na ito ay malamang na mabigo. Magtakda ng isang pasadyang bayad sa network upang mag-sign sa iyong sariling peligro.", + "type": "Uri", + "type-a-message": "Mensahe", + "ulc-enabled": "Pinapagana ang ULC", + "unable-to-fetch": "Hindi makuha ang kasaysayan ng chat", + "unable-to-read-this-code": "Hindi mabasa ang code na ito", + "unable-to-send-messages": "Hindi makapadala at makatanggap ng mga mensahe", + "unblock": "I-unblock", + "unblock-contact": "I-unblock ang gumagamit na ito", + "unknown-status-go-error": "hindi kinikilala status-go error", + "unlimited": "Walang limitasyon", + "unlock": "I-unlock", + "unmute": "Unmute", + "unpair-card": "Ang kawalan ng pag-asa card", + "unpair-card-confirmation": "Ang operasyon na ito ay mawawalan ng pag-asa ang card mula sa kasalukuyang aparato. Nangangailangan ng pahintulot ng 6-digit na passcode. Gusto mo bang magpatuloy?", + "unpair-keycard": "Hindi pagpares ng keycard mula sa iyong telepono.", + "unpair-keycard-warning": "Ang iyong pairing code\/PUK at PIN ay mananatiling hindi nagbago", + "unpaired-keycard-text": "Ang Keycard na iyong tinapik ay hindi nauugnay sa teleponong ito", + "unpaired-keycard-title": "Mukhang hindi naipares ang iyong card", + "unpin": "I-unpin", + "update": "Update", + "update-to-listen-audio": "I-update sa pinakabagong bersyon upang makinig sa isang audio message dito!", + "update-to-see-image": "I-update sa pinakabagong bersyon upang makita ang isang magandang imahe dito!", + "update-to-see-sticker": "I-update sa pinakabagong bersyon upang makita ang isang magandang sticker dito!", + "updates-to-tos": "Mga Update sa Mga Tuntunin ng Serbisyo", + "updates-to-tos-desc": "Bago ka magpatuloy, mangyaring suriin ang Mga Tuntunin ng Serbisyo at kumpirmahing buong responsibilidad mo para sa kung paano mo ginagamit ang app.", + "url": "URL", + "usd-currency": "USD", + "use-as-profile-picture": "Gamitin bilang larawan sa profile", + "use-valid-contact-code": "Mangyaring mag-enter o mag-scan ng isang wastong chat key o username", + "validation-amount-invalid-number": "Ang halaga ay hindi isang wastong numero", + "validation-amount-is-too-precise": "Masyadong tumpak ang halaga. Sobrang bilang ng mga decimals ay{{decimals}}", + "verified-community": "✓beripikadong komunidad", + "version": "Bersyon ng app", + "view": "Tingnan", + "view-cryptokitties": "Tingnan sa CryptoKitties", + "view-cryptostrikers": "Tingnan sa CryptoStrikers", + "view-data": "Tingnan ang data", + "view-details": "Tingnan ang mga detalye", + "view-etheremon": "Tingnan sa Etheremon", + "view-gitcoin": "Tingnan sa Gitcoin", + "view-on-opensea": "Tingnan sa OpenSea", + "view-profile": "Tingnan ang Profile", + "view-public-dashboard": "Tingnan ang pampublikong dashboard", + "view-rules": "Tingnan ang mga patakaran", + "view-signing": "Tingnan ang signing phrase", + "view-superrare": "Tingnan sa SuperRare", + "waiting-for-wifi": "Walang Wi-fi, hindi pinagana ang pag-sync ng mensahe.", + "waiting-for-wifi-change": "Mga settings", + "waiting-to-sign": "Naghihintay na mag-sign ng transaksyon ...", + "waiting-wi-fi": "Naghihintay para sa Wi-Fi…", + "waku-bloom-filter-mode": "Waku Bloom mode ng filter", + "wakuv2-change-nodes": "Sigurado ka bang nais mong baguhin ang mga node ng Wakuv2?", + "wakuv2-node-format": "\/ ip4 \/ {node-ip} \/ tcp \/ {port} \/ p2p \/ {id}", + "wakuv2-settings": "Settings sa Waku V2", + "wallet": "Pitaka", + "wallet-address": "Address ng Wallet", + "wallet-asset": "Asset", + "wallet-assets": "Assets", + "wallet-backup-recovery-title": "Back up mo ang seed phrase", + "wallet-choose-recipient": "Piliin ang Tumatanggap", + "wallet-collectibles": "Mga Kolektibo", + "wallet-connect": "Kumonekta sa Wallet", + "wallet-connect-2.0": "Magconnect sa Wallet 2.0", + "wallet-connect-app-connected": "ay konektado", + "wallet-connect-go-back": "Bumalik sa iyong browser o dapp", + "wallet-connect-proposal-description": "Sa pamamagitan ng pagkonekta pinapayagan mong si {{name}} makuha ang address ng iyong account at paganahin ang Web3", + "wallet-connect-proposal-title": "Gustong kumonekta sa iyong wallet", + "wallet-insufficient-funds": "Hindi sapat na pondo", + "wallet-insufficient-gas": "hindi sapat ang gas ng ETH mo", + "wallet-invalid-address": "Di-wastong address::\n{{data}}", + "wallet-invalid-address-checksum": "Error sa address:\n{{data}}", + "wallet-invalid-chain-id": "Hindi tumutugma ang network:\n{{data}}ngunit ang kasalukuyang chain ay{{chain}}", + "wallet-key-content": "Ang isang 64 character na hex address batay sa pamantayan ng Ethereum at nagsisimula sa 0x. na Humaharap sa publiko, ang iyong account address ay na maibahagi sa iba kung gusto mo matanggap ang pondo. Tinukoy din bilang isang \"Ethereum address\" o \"wallet address.\"", + "wallet-key-title": "Address ng account", + "wallet-manage-accounts": "Pamahalaan ang mga account", + "wallet-manage-app-connections": "Pamahalaan ang mga koneksyon sa app", + "wallet-manage-assets": "Pamahalaan ang mga assets", + "wallet-request": "Kahilingan", + "wallet-send": "Ipadala", + "wallet-send-min-units": "Min 21000 units", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Settings ng Pitaka", + "wallet-total-value": "Kabuuang halaga", + "wallet-transaction-total-fee": "Kabuuang Bayad", + "wants-to-access-profile": "nais na ma-access sa iyong profile", + "warning": "Warning", + "warning-message": "Paumanhin, nililimitahan namin ang pagpapadala ng maraming mga mensahe nang mabilis na magkakasunod upang maiwasan ang spam. Mangyaring subukang muli sa isang sandali", + "warning-sending-to-contract-descr": "Ang address na iyong pinasok ay isang smart contract, magpadala ng pondo sa address na ito ay pwedeng magresulta ng pagkawala ng mga pondo. Makipag-ugnay sa DApp, bukas ang DApp sa anumang Status DApp Browser.", + "watch-only": "panoorin lamang", + "wc-brand-guide": "Patnubay sa paggamit ng tatak tulad ng mga trademark at logo", + "wc-disclaimer": "Mga Disclaimer (kabilang ang mga third party provider), mga warranty, at ligal na paglabas", + "wc-dispute": "Mga probisyon sa paglutas ng hindi pagtatalo", + "wc-how-to-use-status-app": "Paano magagamit ang Status app kasama ang privacy at seguridad", + "wc-new-tos-based-on-principles-prefix": "Mga Bagong Tuntunin ng Serbisyo na dinisenyo batay sa aming", + "web-view-error": "Hindi ma-load ang pahina", + "websites": "Websites", + "webview-camera-permission-requests": "Mga kahilingan sa pahintulot sa Webview camera", + "webview-camera-permission-requests-subtitle": "Kapag pinagana, ang mga website at dapps ay maaaring hilingin na gamitin ang iyong camera", + "welcome-blank-community-message": "Lilitaw ang iyong mga komunidad dito.", + "welcome-blank-message": "Ang iyong mga chat ay lilitaw dito. Upang simulan ang mga bagong chat pindutin ang button ⊕", + "welcome-community-blank-message": "Lilitaw ang channel mo rito. Upang gumawa ng bagong channel, i-click ang ⊕ button at pillin ang \"Gumawa ng Channel\"", + "welcome-community-blank-message-edit-chats": "Lalabas dito ang iyong mga channel. Upang lumikha ng bagong channel, bumalik sa screen ng komunidad, mag-click sa ⊕ button at piliin ang \"Gumawa ng channel\"", + "welcome-screen-text": "I-set up ang iyong wallet, anyayahan ang mga kaibigan na mag-chat\nat mag-browse ng mga sikat na dapps!", + "welcome-to-status": "Maligayang pagdating sa Status!", + "welcome-to-status-description": "I-set up ang iyong crypto wallet, anyayahan ang mga kaibigan na mag-chat at mag-browse sa mga desentralisadong apps", + "what-changed": "Ano ang nagbago", + "what-is-shared": "Ano ang ibinabahagi", + "whats-on-your-mind": "Ano ang nasa isip mo…", + "word-count": "Bilang ng salita", + "word-n": "Word #{{number}}", + "word-n-description": "Upang masuri kung nai-back up ang iyong seed phrase ng tama, ilagay ang word#{{number}}sa itaas.", + "words-n": { + "zero": "", + "one": "Isang salita", + "two": "", + "few": "", + "many": "", + "other": "mga salita" + }, + "write-down-and-store-securely": "Isulat ang mga code\nat itago sila ng ligtas", + "wrong-address": "Maling address", + "wrong-card": "maling card", + "wrong-card-text": "Nag-tap ka ng isang kard na hindi tumutugma sa mga keys na iyong napili", + "wrong-contract": "Maling contract", + "wrong-keycard-text": "Ang Keycard na iyong tinapik ay hindi nauugnay sa teleponong ito", + "wrong-keycard-title": "Mukhang naka-tap ka ng\nisang maling keycard", + "wrong-password": "Maling password", + "wrong-word": "Maling salita", + "yes": "Oo", + "you": "ikaw", + "you-already-have-an-asset": "Mayroon ka nang isang asset na {{value}}", + "you-are-all-set": "Handa ka na!", + "you-are-all-set-description": "Kung mawala ang iyong telepono, maaari mo na ngayong ma-access ang iyong mga pondo at chat key gamit ang iyong seed phrase", + "you-can-change-account": "Maaari mong baguhin ang pangalan at kulay ng account sa nais mo", + "you-can-choose-preview-websites": "Maaari kang pumili kung alin sa mga sumusunod na website ang maaaring mag-preview ng link ng mga paglalarawan at larawan sa mga chat", + "you-can-fetch": "Maaari mong makuha ang kasaysayan ng chat", + "you-dont-have-contacts": "Wala ka pang mga contact.", + "you-dont-have-contacts-invite-friends": "Wala ka pang contact.\n Imbitahin ang iyong mga kaibigan na magsimulang mag-chat.", + "you-dont-have-stickers": "Wala ka pang sticker", + "you-will-need-this-code": "Kakailanganin mo ang code na ito upang buksan ang Status at mga transaksyon sa pag-sign", + "you-will-start-from-scratch": "Magsisimula ka mula sa simula ng isang bagong hanay", + "your-card-is-frozen": "Ang iyong Keycard ay naka frozen. I-reset ang card access", + "your-contact-code": "Pinapahintulutan ng pagbibigay ng pag-access ang DApp na makuha ang iyong susi sa chat", + "your-data-belongs-to-you": "Kung mawala mo ang iyong seed phrase mawawala mo rin, ang iyong data at pondo", + "your-data-belongs-to-you-description": "Kung nawalan ka ng pag-access, halimbawa sa pagkawala ng iyong telepono, maaari mo lamang ma-access ang iyong mga key gamit ang iyong seed phrase. Walang sinuman dapat nakaalam ng iyong seed phrase kung hindi ikaw lamang, kung mangyaring Isulat mo at Panatilihing ligtas", + "your-keys": "Ang iyong susi", + "your-price-limit": "Ang iyong limitasyon sa presyo", + "your-recovery-phrase": "Ang iyong seed phrase", + "your-recovery-phrase-description": "ito ang iyong seed phrase. Ginagamit mo ito upang patunayan na ito ang iyong pitaka. ikaw lamang makita ito nang isang beses! Isulat ito sa papel at itago ito sa isang ligtas na lugar. Kakailanganin mo ito kung nawala o muling i-install ang iyong pitaka.", + "your-tip-limit": "ang iyong tip limit", + "youre-on-mobile-network": "Ikaw ay nasa mobile network" +} diff --git a/translations/fr.json b/translations/fr.json new file mode 100644 index 00000000000..d7fdf6a45c0 --- /dev/null +++ b/translations/fr.json @@ -0,0 +1,1800 @@ +{ + "K": "K", + "M": "M", + "NFT": "NFT", + "You": "Vous", + "about-app": "À propos", + "about-key-storage-content": "Status n'aura jamais accès à votre clé privée. Assurez-vous de sauvegarder votre phrase de récupération. Si vous perdez votre téléphone, c’est le seul moyen d’accéder à vos clés.", + "about-key-storage-title": "A propos du stockage des clés", + "about-names-content": "Personne ne peut prendre votre identité! Vous êtes anonyme par défaut et n'avez jamais à révéler votre vrai nom. Vous pouvez enregistrer un nom personnalisé à peu de frais.", + "about-names-title": "Les noms ne peuvent pas être modifiés", + "about-sharing-data": "À propos du partage de données", + "accept": "Accepter", + "accept-and-add": "Accepter et ajouter", + "accept-and-continue": "Accepter et continuer", + "accept-and-share-address": "Accepter et partager l'adresse", + "accept-community-rules": "J'accepte les règles de la communauté", + "accept-new-chats-from": "Accepter les conversations de", + "accept-status-tos-prefix": "J'accepte Status", + "access-existing-keys": "Récupérer des clés existantes", + "access-key": "Clé d'accès", + "account-added": "Compte ajouté", + "account-color": "Couleur du compte", + "account-content": "Vous pouvez comparer les comptes de Status à des comptes bancaires. Comme un compte bancaire, un compte a généralement une adresse et un solde; Vous utilisez ce compte pour effectuer des transactions avec Ethereum. Vous pouvez avoir plusieurs comptes dans votre portefeuille. Tous accessibles en déverrouillant le Status", + "account-exists-title": "Le compte existe déjà", + "account-is-used": "Ce compte est actuellement utilisé avec des applications décentralisées dans le navigateur.", + "account-name": "Nom du compte", + "account-settings": "Paramètres du compte", + "account-title": "Compte", + "accounts": "Comptes", + "actions": "Actions", + "active-members": "Membres actifs", + "active-online": "En ligne", + "active-unknown": "Inconnu", + "activity": "Activité", + "add": "Ajouter", + "add-a-watch-account": "Ajouter un compte de veille uniquement", + "add-account": "Ajouter un compte", + "add-account-description": "Vous pouvez importer tout type de compte Ethereum pour l'ajouter à votre portefeuille Status.", + "add-account-incorrect-password": "Le mot de passe semble être incorrect. Entrez le mot de passe que vous utilisez pour déverrouiller l'application.", + "add-an-account": "Ajouter un compte", + "add-bootnode": "Ajouter bootnode", + "add-contact": "Ajouter un contact", + "add-custom-token": "Ajouter un jeton personnalisé", + "add-favourite": "Ajouter un favori", + "add-mailserver": "Ajouter un nœud de Status", + "add-members": "Ajouter des membres", + "add-network": "Ajouter réseau", + "add-new-contact": "Ajouter un nouveau contact", + "add-nickname": "Ajouter un pseudo (facultatif)", + "add-node": "Ajouter un nœud", + "add-private-key-account": "Ajouter un compte à partir d'une clé privée", + "add-seed-account": "Ajouter un compte avec une phrase de récupération", + "add-text": "Ajouter du texte", + "add-to-contacts": "Ajouter aux contacts", + "add-to-contacts-text": "En ajoutant un utilisateur à votre liste de contacts, vous partagez l'adresse de votre portefeuille.", + "add-to-favourites": "Ajouter aux favoris", + "add-watch-account": "Ajouter un compte de veille uniquement", + "added": "ajouté", + "address": "Adresse", + "address-or-ens-name": "Adresse ou nom d'utilisateur ENS", + "address-received": "Adresse reçue", + "address-request-accepted": "Demande d'adresse acceptée", + "address-requested": "Adresse demandée", + "admin": "Admin", + "advanced": "Avancé", + "advanced-settings": "Réglages avancés", + "advertiser-description": "Vous avez découvert Status grâce à un partenaire. Acceptez-vous que Status vérifie votre adresse IP une seule fois afin d'être récompensé ? Ces informations ne seront utilisées pour rien d'autre et seront entièrement supprimées après 7 jours.", + "advertiser-starter-pack-accept": "Accepter", + "advertiser-starter-pack-decline": "Décliner", + "advertiser-starter-pack-description": "Voici quelques crypto pour démarrer ! Utilisez-les pour obtenir des autocollants, un nom ENS et essayer des dapps", + "advertiser-starter-pack-title": "Pack de démarrage", + "advertiser-title": "Paramètres de confidentialité par défaut", + "agree-by-continuing": "En continuant, vous acceptez \n notre", + "all": "Tout", + "all-connections": "Toutes les connexions", + "allow": "Autoriser", + "allow-and-send": "Autoriser et envoyer", + "allow-mention-notifications": "Afficher les mentions @", + "allowing-authorizes-this-dapp": "Autoriser cette DApp à récupérer votre adresse de portefeuille et à activer Web3", + "alphabetically": "Alphabétiquement", + "already-have-asset": "Vous avez déjà cet actif", + "amount": "Montant", + "and": "et", + "anyone": "Tout le monde", + "app-connections": "Connexions de l'appli", + "appearance": "Apparence", + "apply": "Appliquer", + "approve": "Approuver", + "are-you-sure": "Êtes-vous sûr?", + "are-you-sure-description": "Vous ne pourrez jamais plus voir toute la phrase de récupération", + "are-you-sure-to-cancel": "Êtes-vous sûr de vouloir annuler ?", + "are-you-sure?": "Êtes-vous sûr?", + "ask-in-status": "Poser une question ou signaler un bug", + "at": "à", + "attribution-received": "{{attrib}} sur {{max}} bonus reçus", + "audio": "Audio", + "audio-message": "Message audio", + "audio-recorder": "Enregistreur", + "audio-recorder-error": "Erreur d'enregistrement", + "audio-recorder-max-ms-reached": "Temps d’enregistrement maximal atteint", + "audio-recorder-permissions-error": "Vous devez donner l'autorisation d'envoyer des messages audio", + "authorize": "Autoriser", + "available": "Disponible", + "available-participants": { + "one": "Vous pouvez choisir un participant de plus", + "other": "Vous pouvez choisir {{count}} participants en plus" + }, + "back": "Retour", + "back-up": "Sauvegarder", + "back-up-seed-phrase": "Sauvegardez votre phrase de récupération", + "back-up-your-seed-phrase": "Sauvegardez votre phrase de récupération", + "backup-disabled": "Désactivé", + "backup-recovery-phrase": "Phrase de récupération de sauvegarde", + "bad-fees-description": "Vos frais de priorité sont inférieurs à ceux que nous conseillons.", + "balance": "Solde", + "begin-set-up": "Commencer l'installation", + "below-base-fee": "plafond des frais inférieur au prix de base", + "biometric-auth-android-sensor-desc": "Capteur tactile", + "biometric-auth-android-sensor-error-desc": "Échec", + "biometric-auth-android-title": "Authentification requise", + "biometric-auth-confirm-logout": "Reconnecter", + "biometric-auth-confirm-message": "L'authentification biométrique est nécessaire pour continuer, si ce n'est pas possible, veuillez vous reconnecter avec ton mot de passe de votre multicompte.", + "biometric-auth-confirm-title": "Vous devez vous authentifier!", + "biometric-auth-confirm-try-again": "Essayez à nouveau", + "biometric-auth-error": "Impossible d'effectuer l'authentification biométrique ( {{code}} )", + "biometric-auth-login-error-title": "Erreur d'authentification biométrique", + "biometric-auth-login-ios-fallback-label": "Entrer le mot de passe", + "biometric-auth-reason-login": "Se connecter à Status", + "biometric-auth-reason-verify": "Vérifier l'authentification", + "biometric-disable-bioauth": "désactiver {{bio-type-label}}", + "biometric-disable-password-description": "Si vous désactivez cette option, vous allez également", + "biometric-disable-password-title": "Désactiver l'enregistrement du mot de passe", + "biometric-enable": "Si vous ne souhaitez pas utiliser votre Keycard à chaque fois pour accéder à l'application, activez {{bio-type-label}}", + "biometric-enable-button": "Activer {{bio-type-label}}", + "biometric-enable-keycard": "Si vous ne souhaitez pas utiliser votre Keycard à chaque fois pour accéder à l'application, activez {{bio-type-label}}", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Empreinte digitale", + "biometric-secure-with": "Sécurisé avec {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "Mot de passe BIP39", + "biu": "Gras, italique et souligné (BIU)", + "blank-contacts-text": "Vos contacts apparaîtront ici", + "blank-keycard-text": "Vous pouvez continuer avec votre Keycard une fois que vous avez généré vos clés et votre nom.", + "blank-keycard-title": "On dirait que vous avez utilisé \nune Keycard vierge", + "blank-messages-text": "Vos messages seront ici", + "block": "Bloc", + "block-contact": "Bloquer le contact", + "block-contact-details": "Le blocage supprimera les messages précédents de cet utilisateur et empêchera les nouveaux de vous atteindre.", + "block-user": "Bloquer l'utilisateur", + "block-user?": "Bloquer l'utilisateur ?", + "blocked-users": "Utilisateurs bloqués", + "bold": "Gras", + "bootnode-address": "Adresse Bootnode", + "bootnode-details": "Détails du bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes activés", + "bootnodes-settings": "Paramètres Bootnodes", + "browsed-websites": "Les sites Web parcourus apparaîtront ici.", + "browser": "Navigateur", + "browser-not-secure": "La connexion n'est pas sécurisée ! Ne signez pas de transactions et n'envoyez pas de données personnelles sur ce site.", + "browser-secure": "La connexion est sécurisée. Assurez-vous de faire vraiment confiance à ce site avant de signer des transactions ou d'entrer des données personnelles.", + "browsers": "Navigateurs", + "browsing-cancel": "Annuler", + "browsing-open-in-android-web-browser": "Ouvrir dans Android", + "browsing-open-in-ios-web-browser": "Ouvrir dans iOS", + "browsing-open-in-status": "Ouvert dans Status", + "browsing-site-blocked-description1": "Nous avons détecté une activité malveillante potentielle à partir de cette adresse. Pour vous protéger et protéger votre portefeuille, nous empêchons toute navigation ultérieure. \n\n Si vous pensez que c'est une erreur, faites-le nous savoir dans le", + "browsing-site-blocked-description2": " chat public.", + "browsing-site-blocked-go-back": "Revenir en arrière", + "browsing-site-blocked-title": "Ce site est bloqué", + "browsing-title": "Parcourir", + "bug-report": "Signaler un bug", + "build-yourself": "Pour utiliser cette application sans accepter ces conditions d'utilisation, vous pouvez en développer votre propre version", + "buy-crypto": "Acheter de la crypto", + "buy-crypto-choose-a-service": "Utilisez le service de votre choix pour acheter de la crypto", + "buy-crypto-description": "Trouver une DApp pour acheter de la crypto", + "buy-crypto-leaving": "Vous quittez Status et accédez maintenant à un site tiers afin de finaliser votre achat", + "buy-crypto-title": "On dirait que votre portefeuille est vide", + "camera-access-error": "Pour accorder l'autorisation requise pour la caméra, accédez aux paramètres de votre système et assurez-vous que Status > Caméra est sélectionné.", + "can-not-add-yourself": "C'est vous, pour commencer un chat, choisissez quelqu'un d'autre", + "can-send-messages": "Vous pouvez envoyer et recevoir de nouveaux messages", + "cancel": "Annuler", + "cancel-keycard-setup": "Annuler la configuration de la Carte d'accès", + "cancelling": "Annulation", + "cannot-pin-desc": "Vous pouvez épingler un maximum de trois messages. Retirez au moins une épingle pour épingler un nouveau message.", + "cannot-pin-title": "Vous ne pouvez pas épingler ce message !", + "cannot-read-card": "Je ne peux pas lire la carte.\nTenez-la à l'arrière de votre téléphone.", + "cannot-use-default-pin": "Le code PIN 000000 n'est pas autorisé. \nVeuillez utiliser un autre numéro", + "cant-open-public-chat": "Impossible d'ouvrir le chat public", + "cant-report-bug": "Impossible de signaler un bogue", + "card-is-blank": "Cette carte est vierge", + "card-reseted": "La carte a été réinitialisée", + "card-unpaired": "La carte a été dissociée du périphérique actuel", + "category": "Catégorie", + "category-title": "Titre de la catégorie", + "change-fleet": "Changer la fleet en {{fleet}}", + "change-group-privacy": "Modifier la confidentialité du groupe", + "change-logging-enabled": "Êtes-vous sûr de vouloir {{enable}} vous connecter?", + "change-pairing": "Changer le code de couplage", + "change-pairing-description": "Changer le code de couplage n'affecte pas les appariements déjà effectués. Cependant, tout les nouveaux appariements nécessiteront le nouveau code.", + "change-pairing-title": "Choisir un nouveau code de couplage", + "change-passcode": "Changer le code d'accès", + "change-password": "Changer le mot de passe", + "change-pin": "Changer le code PIN", + "change-puk": "Changer le PUK à 12 chiffres", + "change-tip": "Changer la valeur du pourboire", + "changed-amount-warning": "Le montant a été changé de {{old}} à {{new}}", + "changed-asset-warning": "L'actif a été changé de {{old}} à {{new}}", + "chaos-mode": "Mode Chaos", + "chaos-unicorn-day": "Journée de la Licorne du Chaos", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Discuter et effectuer des transactions en privé avec vos amis.", + "chat-is-a-contact": "Contact", + "chat-is-not-a-contact": "Pas un contact", + "chat-key": "Code de contact", + "chat-key-content": "Les messages sur le protocole de conversation de Status sont envoyés et reçus à l'aide de clés de chiffrement. Le code de contact public est une chaîne de caractères que vous partagez avec les autres afin qu'ils puissent vous envoyer des messages dans Status.", + "chat-key-title": "Code de contact", + "chat-link-previews": "Aperçu des liens de chat", + "chat-name": "nom du chat", + "chat-name-content": "Trois mots aléatoires, dérivés algorithmiquement de votre code de contact et utilisés comme pseudonyme par défaut dans les discussions. Les noms de discussions sont complètement uniques; aucun autre utilisateur ne peut avoir les mêmes trois mots.", + "chat-name-title": "Nom de discussion", + "chat-notification-preferences": "Paramètres de notification", + "chat-settings": "Paramètres de Chat", + "chats": "Chats", + "check-on-opensea": "Vérifier sur OpenSea", + "check-your-recovery-phrase": "Vérifiez votre phrase de récupération", + "choose-actions": "Choisir des actions", + "choose-authentication-method": "Choisissez une méthode d'authentification pour protéger votre multicompte", + "choose-new-location-for-keystore": "Choisissez un nouvel emplacement pour enregistrer votre fichier de clés", + "choose-storage": "Choisissez le stockage", + "clear": "Effacer", + "clear-all": "Tout effacer", + "clear-history": "Effacer l'historique", + "clear-history-action": "Effacer", + "clear-history-confirmation": "Effacer l'historique?", + "clear-history-confirmation-content": "Êtes-vous sûr de vouloir effacer l'historique de ce chat ?", + "clear-history-title": "Effacer l'historique?", + "clear-history?": "Effacer l'historique ?", + "close": "Fermer", + "close-all": "Tout fermer", + "close-app-button": "Confirmer", + "close-app-content": "L'application va s'arrêter et se fermer. Lorsque vous le rouvrirez, le réseau sélectionné sera utilisé", + "close-app-title": "Avertissement!", + "collectibles-leak-metadata": "Vous pouvez exposer vos NFT ici. Si vous les faites, l'adresse de votre portefeuille et votre adresse IP seront également affichées.", + "command-button-send": "Envoyer", + "communities": "Communautés", + "communities-alpha": "Communautés (alpha)", + "communities-enabled": "Communautés activées", + "communities-verified": "✓ Communauté Status vérifiée", + "community": "Communauté", + "community-channel": "Chaîne communautaire", + "community-channel-read-requirements-met": "Les conditions requises \"afficher uniquement\" sont remplies", + "community-channel-read-requirements-not-met": "Les conditions requises pour \"afficher uniquement\" ne sont pas remplies", + "community-channel-write-requirements-met": "Les conditions d'affichage et de publication sont remplies", + "community-channel-write-requirements-not-met": "Les conditions d'affichage et de publication ne sont pas remplies", + "community-color": "Couleur de la communauté", + "community-color-placeholder": "Choisissez une couleur", + "community-edit-title": "Modifier la communauté", + "community-emoji-thumbnail-title": "Miniature", + "community-enter-channel-info": "Quand vous entrez dans la chaîne, le propriétaire du nœud pourra voir vos adresses publiques", + "community-image-pick": "Choisissez une image", + "community-image-remove": "Enlever", + "community-image-take": "Prenez une photo", + "community-info": "Informations de la communauté", + "community-info-not-found": "Informations de la communauté introuvable", + "community-invite-title": "Inviter", + "community-join-requirements-changed": "Les critères de jetons ont changé", + "community-join-requirements-met": "Les conditions d'adhésion sont remplies", + "community-join-requirements-not-met": "Les conditions d'adhésion ne sont pas remplies", + "community-join-requirements-tokens-lost": "Vous n'avez plus les jetons requis", + "community-key": "Clé privée de la communauté", + "community-key-placeholder": "Entrez la clé privée de votre communauté", + "community-link": "Lien de la communauté", + "community-members": { + "one": "{{count}} membre", + "other": "{{count}} membres" + }, + "community-members-title": "Membres", + "community-message-preview": "Invitation à rejoindre {{community-name}}", + "community-notification-settings": "Paramètres de notification", + "community-private-key": "Clé privée de la communauté", + "community-requests-to-join-title": "Demandes d'adhésion", + "community-roles": "Rôles", + "community-rules": "Règles communautaires", + "community-share-title": "Partager", + "community-thumbnail-image": "Vignette", + "community-thumbnail-upload": "Télécharger", + "complete-hardwallet-setup": "Cette carte est maintenant une partie essentielle de votre sécurité multi-compte. Les transactions ne peuvent pas être envoyées sans elle.", + "completed": "Terminé", + "confirm": "Confirmer", + "confirm-new-password": "Confirmer le nouveau mot de passe", + "confirm-pairing-code-placeholder": "Confirmez votre code de couplage...", + "confirm-password-placeholder": "Confirmer votre mot de passe", + "confirmation-request": "Demande de confirmation", + "confirmations": "Confirmations", + "confirmations-helper-text": "Lorsque la transaction a 12 confirmations, vous pouvez la considérer comme réglée.", + "connect": "Connecter", + "connect-mailserver-content": "Se connecter à {{name}} ?", + "connect-wallet": "Connecter le portefeuille", + "connected": "Connecté", + "connected-to": "Connecté à", + "connecting": "Connexion...", + "connecting-requires-login": "La connexion à un autre réseau nécessite une connexion", + "connection-status": "Statut de la connexion", + "connection-with-the-card-lost": "Connexion avec la carte\n a été perdu", + "connection-with-the-card-lost-setup-text": "Pour reprendre l'installation, maintenez la carte à l'arrière de votre téléphone et maintenez-la près du téléphone", + "connection-with-the-card-lost-text": "Pour continuer, maintenir la carte au dos de votre téléphone", + "contact-code": "Code de contact", + "contact-request-sent": "demande de contact envoyée", + "contact-requests": "Demandes de contact", + "contact-s": { + "one": "contact", + "other": "contacts" + }, + "contacts": "Contacts", + "contacts-descr": "Vos contacts s'afficheront ici. Vous recevrez des mises à jour de statut de la part de tous vos contacts", + "contacts-empty": "Les contacts avec des noms d'utilisateur ENS apparaîtront ici", + "continue": "Continuer", + "continue-anyway": "Continuer quand même", + "contract-address": "Adresse du contrat", + "contract-interaction": "Interaction contractuelle", + "copy-info": "Copier l'information", + "copy-qr": "Copier le code", + "copy-text": "Copier le texte", + "copy-to-clipboard": "Copie", + "copy-transaction-hash": "Copier l'ID de transaction", + "cost-fee": "Coût\/Frais", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Créer", + "create-a-pin": "Créer un code PIN à 6 chiffres", + "create-a-puk": "Créer le PUK à 12 chiffres", + "create-category": "Créer une catégorie", + "create-channel": "Créer une chaîne", + "create-channel-title": "Nouvelle chaîne", + "create-community": "Créer une communauté", + "create-group-chat": "Créer un chat de groupe", + "create-multiaccount": "Créer multicompte", + "create-new-key": "Générer une nouvelle clé", + "create-pin": "Créer un code PIN à chiffres", + "create-pin-description": "Vous aurez besoin de votre carte et de ce code PIN à 6 chiffres pour vous connecter et confirmer les transactions.", + "created-group-chat-description": "Vous avez créé le groupe {{group-name}}", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Devise", + "currency-display-name-aed": "Dirham émirati", + "currency-display-name-afn": "Afghanistan Afghani", + "currency-display-name-ars": "Peso argentin", + "currency-display-name-aud": "Dollar australien", + "currency-display-name-bbd": "Dollar de la Barbade", + "currency-display-name-bdt": "Taka bangladais", + "currency-display-name-bgn": "Lev bulgare", + "currency-display-name-bhd": "Dinar bahreïni", + "currency-display-name-bnd": "Dollar de Brunéi Darussalam", + "currency-display-name-bob": "Bolíviano Bolivien", + "currency-display-name-brl": "Brésil Real", + "currency-display-name-btn": "Ngultrum bhoutanais", + "currency-display-name-cad": "Dollar canadien", + "currency-display-name-chf": "Franc Suisse", + "currency-display-name-clp": "peso chilien", + "currency-display-name-cny": "Yuan Renminbi Chinois", + "currency-display-name-cop": "Peso colombien", + "currency-display-name-crc": "Costa Rica Colon", + "currency-display-name-czk": "Couronne tchèque", + "currency-display-name-dkk": "Couronne danoise", + "currency-display-name-dop": "République Dominicaine Peso", + "currency-display-name-egp": "Livre égyptienne", + "currency-display-name-etb": "Birr éthiopien", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Livre sterling", + "currency-display-name-gel": "Lari Géorgien", + "currency-display-name-ghs": "Cedi du Ghana", + "currency-display-name-hkd": "Dollar de Hong Kong", + "currency-display-name-hrk": "Kuna Croate", + "currency-display-name-huf": "Hongrie Forint", + "currency-display-name-idr": "Roupie indonésienne", + "currency-display-name-ils": "Israël Shekel", + "currency-display-name-inr": "Roupie indienne", + "currency-display-name-isk": "Couronne islandaise", + "currency-display-name-jmd": "Dollar jamaïcain", + "currency-display-name-jpy": "Yen japonais", + "currency-display-name-kes": "Shilling Kenyan", + "currency-display-name-krw": "Corée (Sud) Won", + "currency-display-name-kwd": "Dinar Koweïtien", + "currency-display-name-kzt": "Kazakhstan Tenge", + "currency-display-name-lkr": "Roupie du Sri Lanka", + "currency-display-name-mad": "Dirham Marocain", + "currency-display-name-mdl": "Leu moldave", + "currency-display-name-mur": "Roupie mauricienne", + "currency-display-name-mwk": "Kwacha malawien", + "currency-display-name-mxn": "Peso mexicain", + "currency-display-name-myr": "Ringgit De Malaisie", + "currency-display-name-mzn": "Mozambique Metical", + "currency-display-name-nad": "Dollar Namibien", + "currency-display-name-ngn": "Nigéria Naira", + "currency-display-name-nok": "Couronne norvégienne", + "currency-display-name-npr": "Roupie Du Népal", + "currency-display-name-nzd": "Dollar néo-zélandais", + "currency-display-name-omr": "Rial d'Oman", + "currency-display-name-pen": "Pérou Sol", + "currency-display-name-pgk": "Kina de Papouasie-Nouvelle-Guinée", + "currency-display-name-php": "Peso Philippines", + "currency-display-name-pkr": "Roupie pakistanaise", + "currency-display-name-pln": "Zloty polonais", + "currency-display-name-pyg": "Guarani Paraguayen", + "currency-display-name-qar": "Riyal Du Qatar", + "currency-display-name-ron": "Leu Roumain", + "currency-display-name-rsd": "Dinar Serbe", + "currency-display-name-rub": "Rouble russe", + "currency-display-name-sar": "Arabie Saoudite Riyal", + "currency-display-name-sek": "Couronne suédoise", + "currency-display-name-sgd": "Dollar de Singapour", + "currency-display-name-thb": "Baht thaïlandais", + "currency-display-name-try": "Lire turque", + "currency-display-name-ttd": "Dollar de Trinité-et-Tobago", + "currency-display-name-twd": "Taiwan Nouveau Dollar", + "currency-display-name-tzs": "Shilling tanzanien", + "currency-display-name-uah": "Hryvnia ukrainien", + "currency-display-name-ugx": "Shilling Ougandais", + "currency-display-name-usd": "Dollar des États-Unis", + "currency-display-name-uyu": "Peso uruguayen", + "currency-display-name-vef": "Bolívar Du Venezuela", + "currency-display-name-vnd": "Dong vietnamien", + "currency-display-name-zar": "Afrique du Sud Rand", + "current": "Actuel", + "current-average-tip": "Moyenne actuelle des pourboires", + "current-base-fee": "Tarif de base actuel", + "current-minimum-tip": "Pourboire minimum actuel", + "current-network": "Réseau actuel", + "current-password": "Mot de passe actuel", + "current-pin": "Entrez le code PIN à 6 chiffres", + "current-pin-description": "Entrez votre code PIN pour continuer", + "custom": "Personnalisé", + "custom-networks": "Réseaux personnalisés", + "custom-node": "Vous utilisez un paramètre RPC personnalisé. L'historique de vos transferts locaux est peut-être incomplet.", + "custom-seed-phrase": "Phrase de récupération non valide", + "custom-seed-phrase-text-1": "Cette phrase de récupération ne correspond pas à notre dictionnaire. Vérifiez les mots mal orthographiés.", + "cut": "Couper", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Accepter et Ouvrir", + "dapp-starter-pack-description": "Voici quelques crypto pour démarrer ! Utilisez-les pour obtenir des autocollants, un nom ENS et essayer des dapps", + "dapp-starter-pack-title": "Kit de départ", + "dapp-would-like-to-connect-wallet": "souhaite se connecter à votre portefeuille", + "dapps": "ÐApps", + "dapps-permissions": "Autorisations DApp", + "dark": "Foncé", + "data": "Données", + "data-collected": "Données collectées", + "data-collected-subtitle": "Le tableau ci-dessous montre les données exactes qui sont stockées et envoyées. Les données sont validées contre les règles publiques pour s'assurer qu'aucune donnée sensible ne soit envoyée. Ne faites pas confiance, vérifiez.", + "data-syncing": "Synchronisation des données", + "database-reset-content": "Discussions, contacts et paramètres ont été supprimés. Vous pouvez utiliser votre compte avec votre Keycard", + "database-reset-title": "Réinitialiser la base de données", + "database-reset-warning": "La base de données sera réinitialisée. Les discussions, les contacts et les paramètres seront supprimés", + "datetime-ago": "il y a", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "one": "jour", + "other": "jours" + }, + "datetime-day-short": { + "one": "j", + "other": "j" + }, + "datetime-hour": { + "one": "heure", + "other": "heures" + }, + "datetime-hour-short": { + "one": "h", + "other": "h" + }, + "datetime-minute": { + "one": "minute", + "other": "minutes" + }, + "datetime-minute-short": { + "one": "m", + "other": "m" + }, + "datetime-second": { + "one": "seconde", + "other": "secondes" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "aujourd'hui", + "datetime-yesterday": "Hier", + "decimals": "Décimales", + "decline": "Décliner", + "decryption-failed-content": "Une erreur s'est produite lors du décryptage de vos données. Vous devrez peut-être effacer vos anciennes données et générer un nouveau compte. Appuyez sur \"Appliquer\" pour effacer ou \"Annuler\" pour réessayer", + "default": "Défaut", + "default-assets": "ERC20 et ERC721 par défaut", + "default-sync-period": "Synchroniser l'historique de", + "delete": "Supprimer", + "delete-account": "Supprimer mon compte", + "delete-and-leave-group": "Supprimer et quitter le groupe", + "delete-bootnode": "Supprimer bootnode", + "delete-bootnode-are-you-sure": "Êtes-vous sûr de vouloir supprimer ce bootnode ?", + "delete-bootnode-title": "Supprimer bootnode", + "close-chat": "Supprimer le chat", + "close-chat-confirmation": "Êtes-vous sûr de vouloir supprimer ce chat?", + "delete-confirmation": "Supprimer?", + "delete-for-everyone": "Supprimer pour tous", + "delete-for-me": "Supprimer", + "delete-keys-keycard": "Supprimer les clés de la Keycard", + "delete-mailserver": "Supprimer le nœud de Status", + "delete-mailserver-are-you-sure": "Êtes-vous sûr de vouloir supprimer ce nœud de Status ?", + "delete-mailserver-title": "Supprimer le nœud de Status", + "delete-message": "Supprimer le message", + "delete-my-account": "Supprimer mon compte", + "delete-my-profile": "Supprimer mon profil", + "delete-network-confirmation": "Êtes-vous sûr de vouloir supprimer ce réseau ?", + "delete-network-error": "Veuillez vous connecter à un autre réseau avant de supprimer celui-ci.", + "delete-network-title": "Supprimer le réseau?", + "delete-node": "Supprimer le nœud", + "delete-node-are-you-sure": "Êtes-vous sûr de vouloir supprimer ce nœud ?", + "delete-node-title": "Supprimer le nœud", + "delete-profile": "Supprimer le profil", + "delete-profile-warning": "Avertissement : Si vous n’avez pas écrit votre phrase seed, vous perdrez l’accès à vos fonds après avoir supprimé votre profil", + "deny": "Refuser", + "derivation-path": "Chemin de dérivation", + "describe-channel": "Description du canal", + "description": "Description", + "dev-mode": "Mode de développement", + "dev-mode-settings": "Paramètres du mode de développement", + "device-syncing": "Synchronisation des appareils", + "devices": "Appareils", + "disable": "désactiver", + "disable-all": "Tout désactiver", + "disable-later-in-settings": "Vous pouvez désactiver cette fonction plus tard dans les paramètres", + "disabled": "Désactivé", + "disconnected": "Chat hors ligne", + "discover": "Découvrir", + "discover-communities": "Découvrez les communautés", + "dismiss": "Rejeter", + "display-collectibles": "Afficher les collectors", + "done": "Fait", + "dont-ask": "Ne plus demander", + "edit": "Modifier", + "edit-channel-title": "Modifier le canal", + "edit-chats": "Modifier les discussions", + "edit-community": "Modifier la communauté", + "edit-favourite": "Modifier le favori", + "edit-group": "Modifier le groupe", + "edit-message": "Modifier le message", + "edit-name-and-image": "Modifier le nom et l'image", + "edit-name-image": "Modifier le nom et l'image", + "edit-nickname": "Modifier le pseudo", + "edit-profile": "Modifier le profil", + "edited": "Modifié", + "editing-message": "Modification du message", + "empty-activity-center": "Les notifications de vos discussions apparaîtront ici", + "empty-chat-description": "Il n'y a pas de messages \ndans ce chat pour le moment", + "empty-chat-description-community": "C'est calme ici depuis {{quiet-hours}} .", + "empty-chat-description-one-to-one": "Tous les messages que vous envoyez ici sont cryptés et ne peuvent être lus que par vous.", + "empty-chat-description-public": "Les dernières {{quiet-hours}} ont été calmes. Commencer la conversation ou", + "empty-chat-description-public-share-this": "partager ce chat.", + "empty-keycard-required": "Nécessite une Keycard vide", + "empty-pending-invitations-descr": "Les personnes souhaitant rejoindre le groupe avec un lien d'invitation apparaîtront ici", + "empty-tab": "Onglet vide", + "enable": "activer", + "enable-all": "Tout activer", + "enable-link-previews": "Activer l'aperçu des liens dans le chat ?", + "encrypt-with-password": "Crypter avec mot de passe", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Ajouter un nom d'utilisateur", + "ens-agree-to": "Accepter de", + "ens-chat-settings": "Paramètres de Chat", + "ens-custom-domain": "Domaine personnalisé", + "ens-custom-username-hints": "Tapez le nom d'utilisateur complet, y compris le domaine personnalisé comme nom d'utilisateur.domaine.eth.eth", + "ens-custom-username-taken": "Le nom d'utilisateur ne vous appartient pas :(", + "ens-deposit": "Dépôt", + "ens-dismiss-message": "Cliquez ici pour ignorer", + "ens-displayed-with": "Vos messages sont affichés aux autres avec", + "ens-get-name": "Obtenir un nom d'utilisateur universel", + "ens-got-it": "Ok, j'ai compris", + "ens-locked": "Nom d'utilisateur verrouillé. Vous ne pourrez pas le déverrouiller avant le {{date}}", + "ens-name-content": "Pseudonyme personnalisé pour votre code de contact que vous pouvez enregistrer à l'aide du service du service de nom Ethereum ENS. Les noms ENS sont des noms d'utilisateur décentralisés.", + "ens-name-not-found": "Impossible de résoudre le nom d'utilisateur ENS", + "ens-name-title": "Nom ENS", + "ens-network-restriction": "Disponible uniquement sur Mainnet", + "ens-no-usernames": "Vous n'avez aucun nom d'utilisateur connecté", + "ens-powered-by": "Propulsé par Ethereum Name Services", + "ens-primary-username": "Nom d'utilisateur principal", + "ens-register": "S'inscrire", + "ens-registration-failed": "Pour enregistrer le nom d'utilisateur, veuillez réessayer.", + "ens-registration-failed-title": "La transaction a échoué", + "ens-registration-failure": "L'enregistrement a échoué", + "ens-registration-in-progress": "Enregistrement en cours...", + "ens-release-username": "Nom d'utilisateur libre", + "ens-remove-hints": "La suppression séparera le nom d'utilisateur de votre clé.", + "ens-remove-username": "Supprimer le nom d'utilisateur", + "ens-saved": "est maintenant connecté avec votre code de contact et peut être utilisé sur Status.", + "ens-saved-title": "Nom d'utilisateur ajouté", + "ens-show-username": "Afficher mon nom d'utilisateur ENS dans les chats", + "ens-terms-header": "Conditions d'enregistrement des noms", + "ens-terms-point-1": "Les fonds sont déposés pendant 1 an. Votre SNT sera verrouillé, mais pas dépensé.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (registre ENS).", + "ens-terms-point-2": "Après 1 an, vous pouvez libérer le nom et récupérer votre dépôt, ou ne prendre aucune mesure pour garder le nom.", + "ens-terms-point-3": "Si les conditions du contrat changent - par exemple, si Status fait des mises à jour du contrat - l'utilisateur a le droit de libérer le nom d'utilisateur quelle que soit la durée du contrat.", + "ens-terms-point-4": "Le contrôleur des contrats ne peut pas accéder à vos fonds déposés. Ils ne peuvent être déplacés qu'à l'adresse qui les a envoyés.", + "ens-terms-point-5": "Votre (vos) adresse(s) sera (seront) publiquement associée(s) à votre nom ENS.", + "ens-terms-point-6": "Les noms d'utilisateurs sont créés en tant que nœuds de sous domaines de stateofus.eth et sont soumis aux conditions du contrat intelligent ENS.", + "ens-terms-point-7": "Vous autorisez le contrat à transférer SNT en votre nom. Cela ne peut se produire que lorsque vous approuvez une transaction pour autoriser le transfert.", + "ens-terms-point-8": "Ces conditions sont garanties par la logique contractuelle intelligente des adresses :", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Conditions d'enregistrement des noms.", + "ens-test-message": "Salut", + "ens-transaction-pending": "Transaction en cours.....", + "ens-understand": "Je comprends que l'adresse de mon portefeuille sera publiquement connectée à mon nom d'utilisateur.", + "ens-username": "Nom d'utilisateur ENS", + "ens-username-already-added": "Le nom d'utilisateur est maintenant connecté avec votre clé et peut être utilisé dans Status.", + "ens-username-available": "✓ Nom d'utilisateur disponible !", + "ens-username-connected": "Ce nom d'utilisateur vous appartient et est connecté à votre code de contact", + "ens-username-connected-continue": "Continuer à établir 'afficher mon nom d'utilisateur ENS dans les discussions'.", + "ens-username-connected-with-different-key": "Pour continuer, vous devrez effectuer une transaction pour connecter votre nom d'utilisateur à votre clé.", + "ens-username-connection-confirmation": "{{username}} sera connecté quand la transaction sera terminée.", + "ens-username-hints": "Au moins 4 caractères. Lettres latines, chiffres et minuscules seulement.", + "ens-username-invalid": "Lettres et chiffres uniquement", + "ens-username-owned": "✓ Le nom d'utilisateur vous appartient.", + "ens-username-owned-continue": "En continuant, vous connecterez ce nom d'utilisateur avec votre clé.", + "ens-username-registration-confirmation": "Joli! {{username}} vous appartient quand la transaction est terminée.", + "ens-username-taken": "Nom d'utilisateur déjà pris :(", + "ens-username-you-can-follow-progress": "Vous pouvez suivre l'évolution dans la section Historique des transactions de votre portefeuille.", + "ens-usernames": "Noms d'utilisateurs ENS", + "ens-usernames-details": "Enregistrer un nom d'utilisateur universel pour être facilement reconnu par les autres utilisateurs", + "ens-want-custom-domain": "Je possède un nom sur un autre domaine", + "ens-want-domain": "Je veux un domaine stateofus.eth", + "ens-welcome-hints": "Les noms ENS transforment ces adresses d'une longueur insensée en noms d'utilisateur uniques.", + "ens-welcome-point-customize": "Un nom ENS peut remplacer votre nom composé de trois mots aléatoires dans les conversations. Vous serez @votrenom plutôt que {{name}}.", + "ens-welcome-point-customize-title": "Choisissez votre pseudonyme", + "ens-welcome-point-receive": "D'autres utilisateurs peuvent vous envoyer des fonds en une étape simple.", + "ens-welcome-point-receive-title": "Recevez des transactions dans la discussion", + "ens-welcome-point-register": "Inscrivez-vous une fois pour garder le nom pour toujours. Après 1 an, vous pouvez libérer le nom et récupérer votre SNT.", + "ens-welcome-point-register-title": "10 SNT pour s'enregistrer", + "ens-welcome-point-simplify": "Vous pouvez recevoir des fonds grâce à votre nom ENS facile à partager plutôt qu'à votre hash hexadécimal (0x...).", + "ens-welcome-point-simplify-title": "Simplifiez votre adresse ETH", + "ens-welcome-point-verify": "Vous pouvez vérifier et ajouter tous les noms d'utilisateur que vous possédez au cours des prochaines étapes.", + "ens-welcome-point-verify-title": "Possédez-vous déjà un nom d'utilisateur ?", + "ens-your-username": "Votre nom d'utilisateur", + "ens-your-usernames": "Vos noms d'utilisateurs", + "ens-your-your-name": "Votre nom ENS", + "enter-12-words": "Entrez les 12 mots de votre phrase de récupération, séparés par un espace", + "enter-a-private-key": "Entrer une clé privée", + "enter-a-seed-phrase": "Entrer une phrase de récupération", + "enter-address": "Entrer l'adresse", + "enter-channel": "Entrer dans la chaîne", + "enter-contact-code": "ENS (vitalik94) ou clé de chat (0x04…)", + "enter-pair-code": "Entrez votre code d'authentification", + "enter-pair-code-description": "Le code de jumelage peut être défini à partir d'un client Status déjà jumelé.", + "enter-password": "Entrer le mot de passe", + "enter-password-migration-prompt": "Renseignez votre mot de passe pour transférer vos contacts, discussions et paramètres liés à vos clés", + "enter-pin": "Entrez le code PIN à 6 chiffres", + "enter-puk-code": "Entrez le code PUK", + "enter-puk-code-description": "Le code PIN a été bloqué. \nVeuillez entrer le code PUK pour débloquer le code PIN.", + "enter-recipient-address-or-username": "Entrer l'adresse ou le nom d'utilisateur du destinataire", + "enter-seed-phrase": "Entrez la phrase de récupération", + "enter-url": "Entrer l'URL", + "enter-user-pk": "Entrez la clé publique de l'utilisateur", + "enter-watch-account-address": "Scannez un QR code \n ou \n entrez l'adresse à regarder", + "enter-word": "Entrer le mot", + "enter-your-code": "Entrez votre code à 6 chiffres", + "enter-your-password": "Entrez votre mot de passe...", + "error": "Erreur", + "error-unable-to-get-balance": "Impossible d'obtenir le solde", + "error-unable-to-get-prices": "Erreur de conversion de devise. Actualisez votre écran pour réessayer.", + "error-unable-to-get-token-balance": "Impossible d'obtenir le solde du token", + "errors": "Erreurs", + "eth": "ETH", + "ethereum-address": "Adresse Ethereum", + "ethereum-node-started-incorrectly-description": "Le nœud Ethereum a été démarré avec une configuration incorrecte, l'application sera arrêtée pour récupérer de cette condition. Identifiant réseau configuré = {{network-id}} , actuel = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Le noeud Ethereum a mal démarré", + "etherscan-lookup": "Regarder sur Etherscan", + "everyone": "Tous", + "expand-all": "Développer tout", + "export-account": "Exporter le compte", + "export-key": "Exporter la clé privée", + "external-link": "Lien externe", + "external-storage-denied": "Accès non-autorisé au stockage externe", + "failed": "Échoué", + "faq": "Foire aux questions", + "fast": "Rapide", + "favourite": "Favoris", + "favourite-description": "Vos sites Web préférés apparaîtront ici", + "favourites": "Favoris", + "favourites-empty": "Les adresses ajoutées aux favoris apparaîtront ici", + "featured": "Présenté", + "fee-cap": "Plafond des frais", + "fee-explanation": "Prix global maximal pour la transaction. Si le tarif de base du bloc actuel sont supérieurs, la transaction sera incluse dans le bloc suivant avec un tarif de base inférieur.", + "fee-options": "Options de frais suggérées", + "fetch-community": "Rechercher la communauté", + "fetch-messages": "récupérer-les-messages", + "fetch-timeline": "Afficher", + "fetching-community": "À la recherche de la communauté...", + "find": "Trouver", + "finish": "Finir", + "finishing-card-setup": "Finition de la configuration de la carte", + "fleet": "Fleet", + "fleet-settings": "Paramètres de la fleet", + "follow": "Suivre", + "follow-your-interests": "Se lancer dans une conversation publique et rencontrez de nouvelles personnes", + "free": "↓ Gratuit", + "from": "De", + "gas-amount-limit": "Plafond de \"gas\"", + "gas-limit": "Limite de gaz", + "gas-price": "Prix du gaz", + "gas-used": "Gaz utilisé", + "gated": "Securisé", + "generate-a-key": "Générer une clé", + "generate-a-new-account": "Créez une nouvelle clé", + "generate-a-new-key": "Générer une nouvelle clé", + "generate-account": "Créez une clé", + "generate-an-account": "Générez un compte", + "generate-new-key": "Générer une nouvelle clé", + "generating-codes-for-pairing": "> Téléchargement du logiciel produit sur carte\n > Génération de codes de déverrouillage et de couplage", + "generating-keys": "Génération de clés ...", + "generating-mnemonic": "Génération d'une phrase de récupération", + "get-a-keycard": "Obtenez une Keycard", + "get-started": "Commencer", + "get-status-at": "Obtenir Status à http:\/\/status.im", + "get-stickers": "Obtenir des autocollants", + "gif": "GIF", + "give-a-short-description": "Donnez une brève description", + "give-a-short-description-community": "Donnez une brève description", + "give-permissions-camera": "Donner la permission \n pour accéder à la caméra", + "glossary": "Glossaire", + "go-to-settings": "Aller aux paramètres...", + "got-it": "J'ai compris", + "grant-face-id-permissions": "Pour accorder l'autorisation Face ID requise, accédez aux paramètres de votre système et assurez-vous que l'option Status > Face ID est sélectionnée.", + "group-chat": "Chat de groupe", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** a été nommé administrateur", + "group-chat-all-contacts-invited": "Tous vos contacts sont déjà dans le groupe", + "group-chat-created": "**{{member}}** a créé le groupe **{{name}}**", + "group-chat-decline-invitation": "Décliner l'invitation", + "group-chat-member-added": "**{{member}}** a été invité", + "group-chat-member-joined": "**{{member}}** a rejoint le groupe", + "group-chat-member-removed": "**{{member}}** a quitté le groupe", + "group-chat-members-count": "{{selected}} \/ {{max}} membres", + "group-chat-name-changed": "** {{member}} ** a changé le nom du groupe en ** {{name}} **", + "group-chat-no-contacts": "Vous n'avez pas encore de contacts.\nInvitez vos amis à commencer à chatter", + "group-details": "Détails du groupe", + "group-info": "Informations sur le groupe", + "group-invite": "Invitation au groupe", + "group-invite-link": "Lien d'invitation au groupe", + "group-membership-request": "Demande d'inscription au groupe", + "groups": "Groupes", + "gwei": "Gwei", + "has-permissions": "a la permission d'accéder", + "hash": "Hash", + "help": "aide", + "help-capitalized": "Aide", + "help-center": "Centre d'aide", + "help-improve-status": "Aider à améliorer Status", + "hide": "Masquer", + "hide-content-when-switching-apps": "Bloquer les captures d'écran", + "hide-content-when-switching-apps-ios": "Masquer l'aperçu", + "history": "Histoire", + "history-nodes": "Nœuds de Status", + "hold-card": "Tenir la carte à l'arrière\n de votre téléphone", + "home": "Accueil", + "hooks": "Crochets", + "how-it-works": "Comment ça marche", + "http-gateway-error": "Oups, la demande a échoué!", + "identifier": "Identifiant", + "identity-verification": "Vérification d'identité", + "identity-verification-request": "Vérification d'identité", + "identity-verification-request-sent": "demande", + "image": "Image", + "image-remove-current": "Supprimer la photo actuelle", + "image-source-gallery": "Sélectionnez à partir de la galerie", + "image-source-make-photo": "Capturer", + "image-source-title": "Modifier l'image", + "import": "Importer", + "import-community": "Importer une communauté", + "import-community-title": "Importer une communauté", + "in-contacts": "Dans les contacts", + "include": "Inclure", + "incoming": "Entrant", + "incoming-transaction": "Transaction entrante", + "incorrect-code": [ + "str", + "Désolé, le code était incorrect, veuillez le saisir à nouveau" + ], + "increase-gas": "Rajouter du gaz", + "initialization": "Initialisation", + "install": "Installer", + "intro-message1": "Bienvenue sur Status! \n Appuyez sur ce message pour définir votre mot de passe et commencer.", + "intro-privacy-policy-note1": "Status ne collecte, ne partage et ne vend aucune donnée personnelle. En continuant, vous acceptez la ", + "intro-privacy-policy-note2": "politique de respect de la vie privée", + "intro-text": "Status est votre porte d'entrée vers le web décentralisé", + "intro-text1": "Discutez sur un réseau peer-to-peer crypté, où vos messages ne peuvent être ni censurés ni piratés", + "intro-text2": "Envoyez et recevez des bien numériques partout dans le monde — sans compte bancaire requis", + "intro-text3": "Découvrez des jeux, des plateformes d'échanges et des réseaux sociaux où vous êtes seul propriétaire de vos données", + "intro-title1": "Communication privée et sécurisée", + "intro-title2": "Portefeuille crypto sécurisé", + "intro-title3": "Applications décentralisées", + "intro-wizard-text1": "Votre compte est contrôlé par une clé. Cette clé est stockée dans votre téléphone, vous seul y avez accès", + "intro-wizard-text2": "Une clé est utilisée pour discuter. Elle est liée à nom qui ne peut pas être modifié.", + "intro-wizard-text3": "Si vous possédez une Keycard, stockez-y vos clés pour une sécurité renforcée.", + "intro-wizard-text4": "Sécurisez et cryptez vos clés", + "intro-wizard-text6": "Facilitez la signature et l'envoi de transactions en activant la signature par empreinte digitale.", + "intro-wizard-title-alt4": "Créer un mot de passe", + "intro-wizard-title-alt5": "Confirmer votre mot de passe", + "intro-wizard-title1": "intro-assistant-titre1", + "intro-wizard-title2": "Choisissez un pseudo", + "intro-wizard-title3": "Sélectionnez où stocker les clés", + "intro-wizard-title4": "Créer un code à 6 chiffres", + "intro-wizard-title5": "Confirmer le code", + "intro-wizard-title6": "Activer les notifications", + "introduce-yourself": "Présentez-vous avec un message court", + "invalid-address-qr-code": "Le QR code scanné ne contient pas d'adresse valide", + "invalid-format": "Format invalide \nDoit être {{format}}", + "invalid-key-confirm": "Appliquer", + "invalid-key-content": "La base de données multi-comptes ne peut pas être cryptée car le fichier est corrompu. Il n'y a aucun moyen de le restaurer. Si vous appuyez sur le bouton \"Annuler\", rien ne se passera. Si vous appuyez sur le bouton \" {{erase-multiaccounts-data-button-text}} \", la base de données multi-comptes sera supprimée et vous pourrez déverrouiller plusieurs comptes. Toutes les données multi-comptes seront perdues.", + "invalid-number": "Numéro invalide", + "invalid-pairing-password": "Mot de passe d'appariement invalide", + "invalid-public-chat-topic": "Sujet de chat public non valide", + "invalid-range": "Format non valide, doit être compris entre {{min}} et {{max}}.", + "invalid-username-or-key": "Nom d'utilisateur ou discussion invalide", + "invite": "inviter", + "invite-button": "Inviter", + "invite-chat-accept": "Accepter", + "invite-chat-accept-join": "Accepter et rejoindre", + "invite-chat-intro": "Un ami vous a invité à rejoindre Status. Voici quelques crypto pour démarrer ! Utilisez-les pour enregistrer un nom ENS ou acheter un pack d'autocollants", + "invite-chat-name": "Référence d'ami", + "invite-chat-pending": "En attente", + "invite-chat-rule": "Accepter récompensera également votre ami avec un bonus de parrainage crypto", + "invite-chat-starter-pack": "Kit de départ", + "invite-contacts": "Inviter des personnes à partir de la liste de contacts", + "invite-friends": "Inviter des amis", + "invite-friends-to-status": "Inviter des amis à utiliser Status", + "invite-instruction-fifth": "Vous pouvez utiliser votre bonus de parrainage quand vous le souhaité", + "invite-instruction-first": "Vous envoyez un lien d'invitation unique à votre ami pour télécharger et rejoindre Status", + "invite-instruction-fourth": "Vous recevez votre bonus de référence et votre ami le Pack Starter", + "invite-instruction-second": "Votre ami télécharge Status et crée un compte (sur Android)", + "invite-instruction-third": "Un chat avec votre ami est lancé, où il confirme votre recommandation", + "invite-people": "Inviter des personnes", + "invite-people-from-contacts": "Inviter des personnes de la liste de contacts", + "invite-privacy-policy-public": "Vous avez installé Status avec un lien de parrainage. En rejoignant cette discussion, vous attribuez votre parrain et acceptez ", + "invite-privacy-policy1": "En acceptant que vous acceptiez le programme de référence", + "invite-privacy-policy2": "Conditions générales.", + "invite-public-chat-home": "Invitation par parrainage", + "invite-public-chat-intro": "Voici quelques crypto pour démarrer ! Utilisez-les pour obtenir des autocollants, un nom ENS et essayer des dapps", + "invite-receive-account": "Compte pour recevoir votre bonus de référence", + "invite-reward": "Gagnez du crypto pour chaque ami invité !", + "invite-reward-friend": "Ami:", + "invite-reward-friend-description": "Votre ami recevra un kit de départ composé de{{reward}} pour commencer", + "invite-reward-friend-name": "Kit de départ", + "invite-reward-you": "Toi", + "invite-reward-you-description": "Invitez un ami et recevez {{reward}} un bonus de référence en gage de notre gratitude. Utilisez-le pour obtenir des autocollants, un nom d'utilisateur ENS et essayer les Dapps", + "invite-reward-you-name": "Bonus de référence", + "invite-select-account": "Sélectionnez un compte pour recevoir votre bonus de référence", + "invite-warning": "Cette promotion est valide uniquement pour les utilisateurs d'un appareil Android ne résidant pas aux États-Unis. L'ami doit confirmer sa référence dans les 7 jours", + "invited": "invité", + "italic": "Italique", + "join": "Joindre", + "join-a-community": "ou rejoindre une communauté", + "join-group-chat": "Rejoindre le groupe", + "join-group-chat-description": "{{username}} vous a invité à rejoindre le groupe {{group-name}}", + "join-me": "Hey rejoignez-moi sur Status: {{url}}", + "join-new-private-chat": "Démarrer un nouveau chat privé", + "join-new-public-chat": "Rejoignez un chat public", + "join-open-community": "Rejoindre la communauté", + "joined": "Rejoint", + "joined-group-chat-description": "Vous avez rejoint {{group-name}} à l'invitation de {{username}}", + "key": "Clé", + "key-managment": "Gestion des clés", + "key-on-device": "La clé privée est enregistrée sur cet appareil", + "keycard": "Keycard", + "keycard-access-reset": "L'accès par Keycard est réinitialisé", + "keycard-applet-install-instructions": "Pour installer l'applet, suivez les instructions sur https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation.", + "keycard-awaiting-description": "Essayez de bouger la carte pour trouver le lecteur NFC sur votre appareil", + "keycard-awaiting-title": "Encore en train de rechercher", + "keycard-backup": "Créer une Keycard de sauvegarde", + "keycard-backup-success-body": "Carte de sauvegarde créée avec succès. Vous pouvez maintenant l'utiliser avec votre compte, tout comme la carte principale.", + "keycard-backup-success-title": "Sauvegarde réussie", + "keycard-blocked": "La Keycard a été bloquée. \nVous devez réinitialiser la carte pour continuer à l'utiliser.", + "keycard-can-use-with-new-passcode": "Vous pouvez utiliser cette carte avec votre nouveau code d’accès", + "keycard-cancel-setup-text": "Ceci annulera la configuration de la carte d'accès. Il est fortement recommandé de terminer l'installation afin d'utiliser la carte d'accès. Voulez-vous vraiment annuler ?", + "keycard-cancel-setup-title": "Opération dangereuse", + "keycard-connected-description": "Essayez de ne pas bouger la carte", + "keycard-connected-title": "Connecté", + "keycard-desc": "Vous possédez une Keycard ? Stockez vos clés dessus; vous en aurez besoin pour vos transactions", + "keycard-dont-ask-card": "Ne pas demander de carte pour se connecter", + "keycard-enter-new-passcode": "Entrez nouveau code d'accès {{step}}\/2", + "keycard-error-description": "Connectez à nouveau la carte pour continuer", + "keycard-error-title": "Connexion perdue", + "keycard-factory-reset": "Rétablir les paramètres usine de la carte", + "keycard-factory-reset-text": "Cette opération supprimera toutes les phrases mnémonique sur la carte. Vérifiez d'avoir une sauvegarde de la phrase mnémonique de cette carte", + "keycard-factory-reset-title": "Voulez-vous vraiment effectuer une réinitialisation d’usine ?", + "keycard-free-pairing-slots": "Keycard dispose {{n}} de ports de couplage gratuits", + "keycard-has-multiaccount-on-it": "Cette carte a déjà un multi-compte dessus. Si vous souhaitez la changer, connectez-vous d'abord et réinitialisez votre carte. Si vous souhaitez importer un multi-compte par carte d'accès, veuillez utiliser \"Ajouter un multi-compte existant\".", + "keycard-init-description": "Mettez la carte à l'arrière de votre téléphone pour continuer", + "keycard-init-title": "A la recherche de cartes...", + "keycard-is-blocked-details": "Vous ne pouvez plus utiliser cette carte pour accéder à ce compte ou le signer. Il y a eu trop de tentatives infructueuses de code d'accès et de PUK.", + "keycard-is-blocked-instructions": "Pour accéder à votre compte, réinstallez Status et utilisez une nouvelle Keycard, utilisez un autre portefeuille ou réinitialisez-la Keycard manuellement.", + "keycard-is-blocked-title": "Keycard est bloqué", + "keycard-is-frozen-details": "Afin de protéger vos avoirs, votre carte est bloquée. Réinitialisez votre carte pour la débloquer et pour effectuer des transactions. Vous pouvez utiliser votre PUK ou votre phrase mnémonique.", + "keycard-is-frozen-factory-reset": "Réinitialiser avec mnémonique", + "keycard-is-frozen-reset": "Réinitialiser l'accès à la carte avec le PUK", + "keycard-is-frozen-title": "Keycard est gelée", + "keycard-onboarding-finishing-header": "Presque terminé", + "keycard-onboarding-intro-header": "Stockez vos clés sur votre Keycard", + "keycard-onboarding-intro-text": "Soyez-prêt, cela va prendre quelques minutes, mais ceci est important pour sécuriser votre compte.", + "keycard-onboarding-mnemonic-text": "Vous aurez également besoin d'un morceau de papier et d'un crayon pour écrire votre phrase mnémonique.", + "keycard-onboarding-pairing-header": "Jumelage de la carte...", + "keycard-onboarding-pin-text": "Vous devrez créer un code à 6 chiffres qui sera utilisé pour protéger l'accès à votre Keycard.", + "keycard-onboarding-preparing-header": "Préparation de la carte...", + "keycard-onboarding-puk-code-header": "Écrivez les codes\n et les stocker en toute sécurité", + "keycard-onboarding-recovery-phrase-description": "Avec cette phrase de récupération, vous pouvez toujours récupérer votre clé. Notez la phrase de récupération. Gardez-la en sécurité, hors ligne et séparé de cet appareil.", + "keycard-onboarding-recovery-phrase-header": "Sauvegardez la phrase de récupération", + "keycard-onboarding-recovery-phrase-text": "Rien que pour vos yeux. C'est la phrase de récupération magique utilisée pour générer votre clé.", + "keycard-onboarding-start-header": "Tenez la carte au dos \n de votre téléphone pour commencer", + "keycard-onboarding-start-step1": "Créer un code d'accès", + "keycard-onboarding-start-step1-text": "Environ 1 minute. Créez un code à 6 chiffres pour crypter vos clés", + "keycard-onboarding-start-step2": "Notez le code PUK et le code de couplage", + "keycard-onboarding-start-step2-text": "Environ 1 minute. Vous allez avoir besoin d'un bout de papier et d'un crayon pour ça", + "keycard-onboarding-start-step3": "Sauvegardez la phrase de récupération", + "keycard-onboarding-start-step3-text": "Environ 1 minute. Aussi un morceau de papier et un crayon sont nécessaires", + "keycard-onboarding-start-text": "Et maintenez la carte au contact téléphonique \n pendant la configuration. L'installation prendra environ 4 minutes", + "keycard-processing-description": "Essayez de ne pas bouger la carte", + "keycard-processing-title": "Traitement en cours...", + "keycard-recover": "Carte perdu ou gelée ?", + "keycard-recover-text": "Si vous avez votre phrase mnémonique, vous pouvez créer une nouvelle Keycard associée à ce compte. Vous pouvez soit utiliser une nouvelle Keycard, ou réinitialiser une carte gelée.", + "keycard-recover-title": "Créer une nouvelle carte pour ce compte ?", + "keycard-recovery-intro-button-text": "Commencer la récupération", + "keycard-recovery-intro-header": "Récupérer une clé stockée sur une Keycard", + "keycard-recovery-intro-text": "Si vous avez précédemment généré une clé à l'aide d'une Keycard et que vous souhaitez à présent utiliser cette clé sur cet appareil", + "keycard-recovery-no-key-header": "Il n'y a rien à \n récupérer ici", + "keycard-recovery-no-key-text": "Votre Keycard n'a pas de clé stockée dessus. Pour l'utiliser, générez une nouvelle clé et choisissez votre Keycard pour stocker la clé.", + "keycard-recovery-phrase-confirm-header": "Confirmer la phrase de récupération", + "keycard-recovery-phrase-confirmation-text": "Attention, dernière chance ! Sans la phrase de récupération, vous ne pourrez accéder ni à votre clé, ni aux actifs qui lui sont associés si vous perdez votre appareil.", + "keycard-recovery-phrase-confirmation-title": "Vous avez écrit la phrase de récupération ?", + "keycard-recovery-success-header": "Votre clé a été récupérée avec succès", + "keycard-redeem-title": "Échangez contre", + "keycard-redeem-tx": "Remboursement des actifs", + "keycard-redeem-tx-desc": "Appuyez sur la carte pour signer et recevoir des actifs", + "keycard-reset-passcode": "Réinitialiser le code d’accès", + "keycard-success-description": "Vous pouvez maintenant retirer la carte", + "keycard-success-title": "Succès", + "keycard-unauthorized-operation": "Vous n'êtes pas autorisé à effectuer cette opération.\nS'il vous plaît rentrez une carte valide et essayer à nouveau.", + "keycard-upsell-subtitle": "Sécurité et fonctionnalité renforcées", + "language": "Langue", + "learn-more": "En savoir plus", + "learn-more-about-keycard": "En savoir plus sur la Keycard", + "leave": "Partir", + "leave-chat": "Quitter le chat", + "leave-chat-confirmation": "L'historique de vos conversations sera définitivement effacée de votre appareil. Vous ne pourrez plus les retrouver même si vous décidiez de vous réinscrire dans le futur.", + "leave-community": "Quitter la communauté", + "leave-community-message": "Nous serons tristes de vous voir partir, mais n'oubliez pas que vous pouvez revenir à tout moment !", + "leave-community?": "Quitter la communauté ?", + "leave-confirmation": "Quitter {{chat-name}}", + "leave-group": "Quitter le groupe", + "leave-group?": "Quitter le groupe ?", + "left": "gauche", + "les-ulc": "LES \/ ULC", + "lets-go": "Allons-y", + "lifestyle": "Mode de vie", + "light": "Clair", + "link-to-community": "Lien vers la communauté", + "linked-on": "Lié le {{date}}", + "load-messages-before": "avant le {{date}}", + "load-more-messages": "↓ Récupérer plus de messages", + "load-more-timeline": "Afficher plus", + "loading": "Chargement...", + "local-notifications": "Notifications locales", + "lock-app-with": "Verrouiller l'application avec", + "log-level": "Niveau de journal", + "log-level-settings": "Paramètres de niveau de journal", + "logging": "Connexion", + "logging-enabled": "Connexion activée?", + "login-pin-description": "Entrez votre code PIN à 6 chiffres pour vous connecter à votre multi-compte", + "logout": "Déconnecter", + "logout-app-content": "Le multi-compte sera déconnecté. Si vous vous reconnectez, le réseau sélectionné sera utilisé", + "logout-are-you-sure": "Êtes-vous sûr que vous voulez\npour vous déconnecter?", + "logout-key-management": "Vous devez vous déconnecter pour accéder à la gestion des clés.", + "logout-title": "Se déconnecter ?", + "looking-for-cards": "A la recherche de cartes...", + "lost-connection": "Connexion perdue", + "low-tip": "le pourboire est trop bas", + "lower-than-average-tip": "pourboire inférieur à la moyenne", + "mail-should-be-configured": "Le client e-mail doit être configuré", + "mailserver-address": "Adresse du nœud de Status", + "mailserver-automatic": "Sélection automatique", + "mailserver-automatic-switch-explanation": "Choisissez le nœud d'historique le plus rapide disponible", + "mailserver-connection-error": "Impossible de se connecter au nœud de Status", + "mailserver-content": "Un nœud dans le réseau Status qui dirige et stocke les messages pour une durée maximale de 30 jours.", + "mailserver-details": "Détails du nœud de Status", + "mailserver-error-content": "Le nœud de Status sélectionné n'a pas pu être joint.", + "mailserver-error-title": "Erreur de connexion au nœud de Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address} : {port}", + "mailserver-pick-another": "Choisir un autre nœud de Status", + "mailserver-reconnect": "Impossible de se connecter au nœud de Status. Appuyez pour vous reconnecter", + "mailserver-request-error-content": "L'erreur suivante a été renvoyée par le nœud de Status: {{error}}", + "mailserver-request-error-status": "Une erreur s'est produite lors de l'extraction de l'historique, vérifiez les journaux pour plus de détails.", + "mailserver-request-error-title": "Erreur de requête de serveur du nœud de Status", + "mailserver-request-retry": "Réessayer la demande", + "mailserver-retry": "Réessayer", + "mailserver-title": "Nœud de Status", + "main-currency": "Devise principale", + "main-networks": "Principaux réseaux", + "main-wallet": "Portefeuille principal", + "mainnet-network": "Réseau principal", + "make-admin": "Faire admin", + "make-moderator": "Attribuer le rôle de modérateur", + "manage-keys-and-storage": "Gérer les clés et le stockage", + "manage-members": "Gérer les membres", + "mark-all-read": "Marquer tout comme lu", + "mark-as-read": "Marquer comme lu", + "mark-untrustworthy": "Marquer comme non fiable", + "mark-user-untrustworthy": "Marquer {{username}} comme non fiable", + "master-account": "Compte principal", + "max-fee": "Frais maxima", + "max-priority-fee": "Frais de priorité maxima", + "maximum-fee": "Frais maxima", + "maybe-later": "Peut-être plus tard", + "member-ban": "Bannir le membre", + "member-kick": "Bannir le membre", + "members": { + "one": "1 membre", + "other": "{{count}} membres" + }, + "members-active": { + "one": "1 membre", + "other": "{{count}} membres" + }, + "members-active-none": "pas de membres", + "members-count": "{{count}} membres", + "members-label": "Membres", + "members-limit-reached": "Nombre maximum de membres atteint", + "members-title": "Membres", + "membership": "Adhésion", + "membership-approval": "Exiger une approbation", + "membership-approval-description": "L'adhésion à votre communauté est gratuite mais les nouveaux membres doivent d'abord être approuvés par le créateur de la communauté.", + "membership-button": "Conditions d'adhésion", + "membership-declined": "La demande d'inscription a été refusée", + "membership-description": "L'inscription au groupe doit être approuvée par l'administrateur du groupe", + "membership-ens": "Nécessite un nom d'utilisateur ENS", + "membership-ens-description": "Votre communauté nécessite un nom d'utilisateur ENS pour pouvoir y accéder", + "membership-free": "Sans conditions", + "membership-free-description": "Votre communauté est accessible à tous", + "membership-invite": "Exige l'invitation d'un autre membre", + "membership-invite-description": "Votre communauté ne peut être rejointe que sur invitation des membres de la communauté", + "membership-none": "Aucun", + "membership-none-placeholder": "Vous pouvez exiger que les nouveaux membres répondent à certains critères avant de pouvoir adhérer. Ces critères peuvent être modifiés à tout moment.", + "membership-request-denied": "Demande d'adhésion refusée", + "membership-request-pending": "Demande d'adhésion en cours", + "membership-requests": "Demandes d'adhésion", + "membership-title": "Conditions d'adhésion", + "mention": "Mentionner", + "mentions": "Mentions", + "message": "Message", + "message-deleted": "Message supprimé", + "message-deleted-for-you": "Message supprimé pour vous", + "message-not-sent": "Message non envoyé", + "message-options-cancel": "Annuler", + "message-reply": "Répondre", + "messages": "Messages", + "messages-from-contacts-only-subtitle": "Seulement vos contacts peuvent commencer une conversation avec vous ou vous inviter dans un groupe", + "messages-gap-warning": "Certains messages peuvent être manquants", + "might-break": "Peut casser quelques ÐApps", + "migration-successful": "Migration réussie", + "migration-successful-text": "Compte migré avec succès vers Keycard", + "migrations-failed-content": "{{message}}\nversion du schéma : initiale {{initial-version}}, actuelle {{current-version}}, dernière {{last-version}}\n\nUne erreur de base de données s’est produite. Vos fonds et votre clé de chat sont en sécurité. D’autres données, comme vos conversations et vos contacts, ne peuvent pas être restaurées. Le bouton « {{erase-multiaccounts-data-button-text}} » supprime toutes les autres données et vous permet d’accéder à vos fonds et d’envoyer des messages.", + "miners-higher-fee": "Il est plus probable que les mineurs fassent passer votre transaction en priorité si vous payez plus de frais.", + "mobile-network-ask-me": "Me demander quand sur le réseau mobile", + "mobile-network-continue-syncing": "Continuer de synchroniser", + "mobile-network-continue-syncing-details": "Vous pouvez aussi modifier cela dans les paramètres", + "mobile-network-go-to-settings": "Aller aux paramètres", + "mobile-network-settings": "Données mobiles", + "mobile-network-sheet-configure": "Vous pouvez configurer la synchronisation\nplus précisément dans", + "mobile-network-sheet-offline": "Pas de Wi-fi, la synchronisation des messages est désactivée.", + "mobile-network-sheet-offline-details": "La synchronisation via le réseau mobile est désactivée", + "mobile-network-sheet-remember-choice": "Mémoriser mon choix", + "mobile-network-sheet-settings": "réglages", + "mobile-network-start-syncing": "Lancer la synchronisation", + "mobile-network-stop-syncing": "Arrêter la synchronisation", + "mobile-network-stop-syncing-details": "Jusqu'à ce que vous soyez en Wi-Fi ?", + "mobile-network-use-mobile": "Utiliser les données mobiles", + "mobile-network-use-mobile-data": "Status utilise beaucoup de données lors de la synchronisation des discussions et du portefeuille.", + "mobile-network-use-wifi": "Wi-Fi uniquement", + "mobile-syncing-sheet-details": "Status utilise beaucoup de données lors de la synchronisation des discussions et du portefeuille", + "mobile-syncing-sheet-title": "Synchroniser en utilisant les données mobiles ?", + "more": "plus", + "move-and-reset": "Déplacer et réinitialiser", + "move-keystore-file": "Déplacer le fichier de clés", + "move-keystore-file-to-keycard": "Déplacer le fichier de clés vers la Keycard ?", + "multiaccount-exists-content": "Ce multi-compte existe déjà. Si vous avez perdu votre mot de passe, votre code ou votre Keycard vous devez juste le déverrouiller.", + "multiaccount-exists-title": "Le multi-compte existe déjà", + "multiaccounts-recover-enter-phrase-text": "Entrez 12, 15, 18, 21 ou 24 mots. \nSéparez les mots par un seul espace.", + "multiaccounts-recover-enter-phrase-title": "Entrez votre phrase de récupération", + "music": "Musique", + "mutal-contacts": "Contacts mutuels", + "mute": "mode silencieux", + "mute-chat": "Masquer le chat", + "mute-community": "Masquer la communauté", + "mute-group": "Masquer le groupe", + "my-accounts": "Mes comptes", + "my-accounts-empty": "Vos comptes disponibles apparaîtront ici", + "my-profile": "Mon profil", + "my-status": "Mon statut", + "n-photos": "{{count}} photos", + "name": "Nom", + "name-of-token": "Le nom de votre jeton", + "name-optional": "Nom (optionnel)", + "name-your-channel": "Nommez votre chaîne", + "name-your-channel-placeholder": "Nom du réseau", + "name-your-community": "Nommez votre communauté", + "name-your-community-placeholder": "Un nom accrocheur", + "need-help": "Besoin d'aide ?", + "network": "Réseau", + "network-chain": "Chaîne de réseau", + "network-details": "Détails du réseau", + "network-fee": "Frais de réseau", + "network-id": "ID réseau", + "network-info": "Informations sur le réseau", + "network-invalid-network-id": "L'identifiant réseau spécifié ne correspond pas à l'identifiant réseau par l'url RPC", + "network-invalid-status-code": "Code status non valide: {{code}}", + "network-invalid-url": "L'URL du réseau est invalide", + "network-settings": "Paramètres réseau", + "new": "Nouveau", + "new-category": "Nouvelle catégorie", + "new-chat": "Nouveau chat", + "new-community-title": "Nouvelle communauté", + "new-contact": "Nouveau contact", + "new-contract": "Nouveau contrat", + "new-favourite": "Nouveau favori", + "new-group": "Nouveau groupe", + "new-group-chat": "Nouveau chat de groupe", + "new-messages-header": "Nouveaux messages", + "new-network": "Nouveau réseau", + "new-password": "Nouveau mot de passe", + "new-pin-description": "Entrez le nouveau code PIN à 6 chiffres", + "new-public-group-chat": "Rejoindre le chat public", + "new-puk-description": "Entrez le nouveau PUK à 12 chiffres", + "new-status": "Nouveau statut", + "new-tab": "Nouvel onglet", + "next": "Suivant", + "nickname": "Pseudo", + "nickname-description": "Les pseudos aident à identifier les autres membres dans Status. Vous êtes le seul à seul voir vos pseudos", + "no": "Non", + "no-collectibles": "Aucun collector disponible", + "no-contacts": "Pas encore de contacts", + "no-keycard-applet-on-card": "Pas d'applet Keycard sur la carte", + "no-messages": "Pas de messages", + "no-pairing-slots-available": "Cette carte est déjà couplée à 5 appareils et ne peut pas être couplée à celui-ci. Veuillez utiliser l'un des appareils jumelés, vous connecter avec cette carte et libérer des fentes de couplage sur la carte.", + "no-pinned-messages": "Aucun message épinglé", + "no-pinned-messages-community-desc": "Cette chaîne ne contient aucun message épinglé.", + "no-pinned-messages-desc": "Ce chat ne contient aucun message épinglé.", + "no-result": "Aucun résultat", + "no-thanks": "Non merci", + "no-tokens-found": "Aucun jeton trouvé", + "node-address": "Adresse du nœud", + "node-details": "Détails du nœud", + "node-info": "Informations sur les nœuds", + "node-version": "Version nœud", + "nodes-disabled": "Nœuds de Status désactivés", + "non-archival-node": "Le paramètre RPC ne prend pas en charge les demandes d'archivage. L'historique de vos transferts locaux est peut-être incomplet.", + "non-contacts": "Non contacts", + "nonce": "Nonce", + "none": "aucun", + "normal": "Normal", + "not-applicable": "Non applicable pour les transactions non signées", + "not-connected-nodes": "Le nœud de Status n'est pas connecté", + "not-connected-to-peers": "Pas de connexion à des pairs", + "not-enough-snt": "Pas assez SNT", + "not-found": "Introuvable", + "not-keycard-text": "La carte que vous avez utilisée n'est pas une Keycard. Vous devez acheter une Keycard pour l'utiliser", + "not-keycard-title": "Pas une Keycard", + "not-registered": "pas inscrit", + "notification-settings": "Notifications", + "notifications": "Notifications", + "notifications-non-contacts": "Notifications hors contacts", + "notifications-preferences": "Préférences de notification", + "notifications-servers": "Serveurs de notification", + "notifications-switch": "Afficher les notifications", + "notifications-transactions": "Transactions dans le portefeuille", + "notify": "Notifier", + "now": "Maintenant", + "off": "Désactivé", + "off-status-tree": "Arbre Status désactivé", + "offline": "Hors ligne", + "offline-messaging-use-history-explanation": "Activez les nœuds d'historique pour récupérer les messages envoyés quand l'application était fermée. Lorsqu'il est activé, un nœud d'historique obtient votre adresse IP. Lorsqu'il est désactivé, vous ne recevrez pas de messages si l'application est fermée et vous ne les verrez pas lors de l'ouverture ultérieure de l'application.", + "offline-messaging-use-history-nodes": "Utiliser les nœuds d'historique", + "ok": "OK", + "ok-continue": "D'accord, continuez", + "ok-got-it": "OK, j'ai compris", + "ok-save-pass": "OK, enregistrer le mot de passe", + "okay": "D'accord", + "on": "Sur", + "on-status-tree": "Sur l'arbre Status", + "once-enabled-share-metadata": "Une fois activés, les liens publiés dans le chat peuvent partager vos métadonnées avec le site", + "one-day": "Un jour", + "one-month": "Un mois", + "one-week": "Une semaine", + "online": "En ligne", + "only-mentions": "Uniquement les @mentions", + "open": "Ouvrir", + "open-chat": "Ouvrir le chat", + "open-dapp": "Ouvrir ÐApp", + "open-dapp-store": "Découvrir les ÐApps", + "open-home": "Ouvrir...", + "open-in-new-tab": "Ouvrir dans un nouvel onglet", + "open-membership": "Participation ouverte", + "open-nfc-settings": "Ouvrir les paramètres NFC", + "opened": "Ouvert", + "opening-buy-crypto": "Ouverture de {{site}} ...", + "optimal": "Optimal", + "optional": "optionnel", + "or": "ou", + "outgoing": "Sortant", + "outgoing-transaction": "Transaction sortante", + "owner": "Propriétaire", + "page-camera-request-blocked": "Demandes d'utilisation de la caméra bloquées. Pour activer les demandes, allez aux Paramètres", + "page-would-like-to-use-camera": "souhaite utiliser votre caméra", + "pair": "Coupler les appareils", + "pair-card": "Coupler à cet appareil", + "pair-code": "Code de couplage", + "pair-code-explanation": "Couplez la Keycard avez différents appareils (jusqu'à 5) afin de déverrouiller et signer des transactions avec la même Keycard", + "pair-code-placeholder": "Code de paire ...", + "pair-this-card": "Associez cette carte", + "pair-this-device": "Publicité de l'appareil", + "pair-this-device-description": "Jumelez vos appareils pour synchroniser les contacts et les conversations entre eux.", + "paired-devices": "Appareils jumelés", + "pairing": "Jumelage", + "pairing-card": "Carte de couplage", + "pairing-changed": "Le code de couplage a été modifié", + "pairing-code-placeholder": "Code de couplage...", + "pairing-code_error1": "Les codes de couplage ne correspondent pas.", + "pairing-go-to-installation": "Aller aux paramètres de couplage", + "pairing-maximum-number-reached-content": "Veuillez désactiver l'un de vos appareils avant d'en activer un nouveau.", + "pairing-maximum-number-reached-title": "Nombre maximum d'appareils atteints", + "pairing-new-installation-detected-content": "Un nouveau périphérique a été détecté. \nPour utiliser vos appareils correctement, il est important de les coupler et de les activer avant de les utiliser. \nVeuillez vous rendre dans la section des appareils sous Paramètres pour coupler vos appareils.", + "pairing-new-installation-detected-title": "Nouvel appareil détecté", + "pairing-no-info": "Pas d'infos", + "pairing-please-set-a-name": "Veuillez définir un nom pour votre appareil.", + "passphrase": "Passphrase", + "password": "Mot de passe", + "password-description": "Au moins 6 caractères. Vous aurez besoin de ce mot de passe pour ouvrir Status et confirmer les transactions.", + "password-mismatch": "Le nouveau mot de passe et la confirmation ne correspondent pas", + "password-placeholder": "Mot de passe", + "password-placeholder2": "Confirmer votre mot de passe", + "password-reset-in-progress": "Modification du mot de passe...", + "password-reset-success": "Mot de passe réinitialisé", + "password-reset-success-message": "Vous devrez vous reconnecter", + "password_error1": "Les mots de passe ne correspondent pas.", + "paste": "Coller", + "paste-json": "Coller JSON", + "pay-to-chat": "Payer pour chatter", + "peer-content": "Un appareil connecté au réseau de discussion Status. Chaque utilisateur peut représenter un ou plusieurs pairs, selon leur nombre d'appareils.", + "peer-title": "Pair", + "peer-to-peer": "Pair-à-pair", + "peers": "Pairs", + "pending": "En attente", + "pending-confirmation": "Dans l'attente d'une confirmation....", + "pending-invitations": "Demandes d'inscription en cours", + "pending-requests": "Demandes en cours", + "per-gas-price-limit": "Plafond du prix \"\/gas\"", + "per-gas-tip-limit": "Plafond du pourboire \"\/gas\"", + "permissions": "Permissions", + "phone-e164": "International 1", + "photos": "Photos", + "photos-access-error": "Pour accorder l'autorisation d'utiliser les photos requises, veuillez vous rendre dans les paramètres de votre système et vous assurer que l'option Status > Photos est sélectionnée.", + "pin": "Épingler", + "pin-changed": "Le code PIN a été changé", + "pin-code": "Code PIN à 6 chiffres", + "pin-limit-reached": "Limite d'épinglage atteinte. Retirez d'abord un message précédent.", + "pin-mismatch": "Code PIN incorrect", + "pin-one-attempt": "une tentative", + "pin-one-attempt-blocked-after": "avant que votre Keycard ne soit bloquée", + "pin-one-attempt-blocked-before": "Attention, vous n'avez que", + "pin-one-attempt-frozen-after": "avant que votre Keycard ne soit gelée", + "pin-one-attempt-frozen-before": "Attention, vous n'avez que", + "pin-retries-left": "Il vous reste {{number}} tentatives", + "pin-to-channel": "Épingler à la chaîne", + "pin-to-chat": "Épingler au chat", + "pinned-a-message": "a épinglé un message", + "pinned-by": "Épinglé par", + "pinned-messages": "Messages épinglés", + "pinned-messages-2": "Messages épinglés", + "pinned-messages-count": { + "one": "1 message épinglé", + "other": "{{count}} messages épinglés" + }, + "pinned-messages-empty": "Les messages épinglés apparaîtront ici. Pour épingler un message, maintenez-le enfoncé et appuyez sur « Épingler »", + "podcasts": "Podcasts", + "preference": "Préférence", + "preview-privacy": "Aperçu du mode de confidentialité", + "previewing-may-share-metadata": "Prévisualiser les liens de ces sites internet peut partager vos métadonnées avec leurs propriétaires", + "principles": "Principes", + "privacy": "Confidentialité", + "privacy-and-security": "Confidentialité et sécurité", + "privacy-photos": "Confidentialité de la photo de profil", + "privacy-policy": "Politique de confidentialité", + "privacy-show-to-warning": "Votre photo de profil continuera à s'afficher pour les personnes qui l'ont déjà vue", + "private-key": "Clé privée", + "private-notifications": "Notifications", + "private-notifications-descr": "Status vous informera des nouveaux messages. Vous pouvez modifier vos préférences de notification ultérieurement dans les paramètres.", + "processing": "Veuillez patienter un instant...", + "product-information": "Informations sur le produit", + "profile": "Profil", + "profile-deleted-content": "Votre profil a été supprimé avec succès", + "profile-deleted-keycard": "Vous pouvez maintenant restaurer une autre biclé sur votre Keycard", + "profile-deleted-title": "Profil supprimé", + "profile-details": "Détails du profil", + "profile-not-found": "Profil non trouvé", + "profile-pic-pick": "Sélectionnez à partir de la galerie", + "profile-pic-remove": "Supprimer la photo", + "profile-pic-take": "Prendre une photo", + "profile-picture-updated": "Votre photo de profil a été mise à jour", + "public": "Public", + "public-channel": "Canal public", + "public-chat": "Chat public", + "public-chat-description": "Une discussion publique est l'endroit pour communiquer avec les autres, se faire des amis et parler des sujets qui vous intéressent.", + "public-chats": "Chats publics", + "public-group-status": "Public", + "public-group-topic": "Sujet", + "public-key": "Clé publique", + "puk-and-pairing-codes-displayed": "Affichage des codes PUK et des codes de couplages", + "puk-changed": "Le PUK à 12 chiffres a été modifié", + "puk-code": "Code PUK", + "puk-code-explanation": "Si vous oubliez votre code PIN à 6 chiffres ou si vous le saisissez 3 fois de manière incorrecte, vous aurez besoin de ce code pour déverrouiller votre carte.", + "puk-mismatch": "Le code PUK ne correspond pas", + "push-failed-transaction": "Votre transaction a échoué", + "push-failed-transaction-body": "{{value}} {{currency}} à {{to}}", + "push-inbound-transaction": "Vous avez reçu {{value}} {{currency}}", + "push-inbound-transaction-body": "De {{from}} au {{to}}", + "push-notifications-server-enabled": "Serveur activé", + "push-notifications-servers": "serveurs de notifications push", + "push-outbound-transaction": "Vous avez envoyé {{value}} {{currency}}", + "push-outbound-transaction-body": "De {{from}} au {{to}}", + "quiet-days": "{{quiet-days}} jours", + "quiet-hours": "{{quiet-hours}} heures", + "re-encrypt-key": "Crypter à nouveau votre clé", + "receive": "Recevoir", + "receive-transaction": "Recevoir la transaction", + "received": "Reçu", + "recent": "Récent", + "recent-empty": "Les adresses récemment utilisées apparaîtront ici", + "recent-recipients": "Contacts", + "recently-used-stickers": "Les autocollants récemment utilisés apparaîtront ici", + "recipient": "Bénéficiaire", + "recipient-code": "Entrez l'adresse du destinataire", + "recipient-code-placeholder": "0x... ou username.domain.eth", + "recover": "Récupérer", + "recover-key": "Récupérer des clés existantes", + "recover-keycard-multiaccount-not-supported": "Les clés de ce compte existent déjà et ne peuvent pas être ajoutées. Si vous avez perdu votre mot de passe, code d'accès ou Keycard, désinstallez l'application, réinstallez la et vous pourrez accéder à vos clés en utilisant votre phrase mnémonique", + "recover-with-keycard": "Récupérer avec la Keycard", + "recover-with-seed-phrase": "Récupérer avec une phrase de récupération", + "recovering-key": "Récupération de la clé ...", + "recovery-confirm-phrase": "Confirmer la phrase de récupération", + "recovery-phrase": "Phrase de récupération", + "recovery-success-text": "Vous devrez créer un nouveau code ou mot de passe pour recrypter votre clé", + "recovery-typo-dialog-description": "Veuillez noter que votre phrase de départ doit utiliser exactement les mêmes mots et l'ordre que vous l'avez reçu", + "recovery-typo-dialog-title": "La phrase de récupérations est-elle correcte?", + "redeem-amount": "{{quantity}} bonus disponibles", + "redeem-now": "Échangez maintenant", + "redeem-success": "L'échange du bonus a fonctionné !", + "reduced-tip": "le pourboire de priorité sera réduit", + "refresh": "Rafraîchir", + "registered": "inscrit", + "reject-and-delete": "Refuser et supprimer", + "remember-me": "Se souvenir de moi", + "remind-me-later": "Me le rappeler plus tard", + "remove": "Enlever", + "remove-favourite": "Supprimer le favori", + "remove-from-chat": "Supprimer du chat", + "remove-from-contacts": "Retirer des contacts", + "remove-from-contacts-text": "En supprimant un utilisateur de votre liste de contacts, vous ne leur cachez pas l'adresse de votre portefeuille", + "remove-group": "Supprimer le groupe", + "remove-network": "Supprimer le réseau", + "remove-token": "Supprimer le jeton", + "remove-user-from-group": "Supprimer {{username}} du groupe", + "removed": "supprimé", + "rename": "Renommer", + "repeat-pin": "Répéter le nouveau code PIN", + "repeat-puk": "Répéter le nouveau code PUK", + "replied": "Répondu", + "replies": "Réponses", + "replying-to": "Répondre à {{author}}", + "report-bug-email-template": "1. Description du problème \n (Décrivez la fonctionnalité que vous souhaitez ou résumez brièvement le bogue et ce que vous avez fait, ce à quoi vous vous attendiez et ce qui se passe réellement. Sections ci-dessous) \n\n\n 2. Étapes à suivre pour reproduire \n (Décrivez comment nous pouvons répliquer le bogue étape par étape.) \n - Ouvrir Status \n -... \n - Étape 3, etc. \n\n\n 3. Comportement attendu \n (Décrivez ce que vous attendiez.) \n\n\n 4. Comportement réel \n (Décrivez ce qui s'est réellement passé.) \n\n\n 5. Joignez des captures d’écran permettant de faire la démonstration du problème. \n", + "request-access": "Demander l’accès", + "request-feature": "Demander une fonctionnalité", + "request-membership": "Demande d'inscription", + "request-pending": "Demande en cours...", + "request-to-join": "Demande d'adhésion", + "request-transaction": "Demande de transaction", + "required-field": "Champ requis", + "resend-message": "Renvoyer", + "reset-card": "Réinitialiser la carte", + "reset-card-description": "Cette opération réinitialisera la carte à son état initial. Toutes les données de la carte, y compris les clés privées, seront effacées. L'opération n'est pas réversible.", + "reset-database": "Réinitialiser la base de données", + "reset-database-warning": "Supprimez les discussions, les contacts et les paramètres. Requis lorsque vous avez perdu votre mot de passe", + "reset-database-warning-keycard": "Supprimez les discussions, les contacts et les paramètres.", + "reset-password": "Réinitialiser le mot de passe", + "restore-defaults": "Restaurer les valeurs par défaut", + "retry": "Réessayer", + "revoke-access": "Révoquer l'accès", + "rpc-url": "URL RPC", + "rpc-usage-copy": "Copier", + "rpc-usage-filter": "Méthodes de filtre", + "rpc-usage-filter-methods": "Méthodes de filtre", + "rpc-usage-get-stats": "Actualiser", + "rpc-usage-info": "Statistiques d'utilisation du RPC", + "rpc-usage-reset": "Réinitialiser", + "save": "Sauvegarder", + "save-image-library": "Enregistrer l'image dans la bibliothèque", + "save-password": "Enregistrer le mot de passe", + "save-password-unavailable": "Définir le mot de passe de l'appareil pour enregistrer le mot de passe", + "save-password-unavailable-android": "Enregistrer le mot de passe n'est pas disponible : il se peut que votre appareil soit enraciné ou ne dispose pas des fonctions de sécurité nécessaires.", + "scan-qr": "Scannez le QR code", + "scan-qr-code": "Scannez un QR code avec une adresse de portefeuille", + "scan-tokens": "Scanner les jetons", + "search": "Rechercher", + "search-contacts": "Rechercher des contacts", + "search-discover-communities": "Rechercher des communautés ou des catégories", + "search-no-chat-found": "Aucun résultat trouvé. Voulez-vous dire", + "secret-keys-confirmation-text": "Vous en aurez besoin pour continuer à utiliser votre Keycard au cas où vous perdriez votre téléphone.", + "secret-keys-confirmation-title": "Avez-vous écrit les codes?", + "security": "Sécurité", + "see-details": "Voir les détails", + "see-it-again": "VOIR DE NOUVEAU", + "see-sticker-set": "Voir toute la série d'autocollants", + "see-suggestions": "Voir les suggestions", + "seed-key-uid-mismatch": "Phrase mnémonique ne correspond pas", + "seed-key-uid-mismatch-desc-1": "La phrase de récupération saisie ne correspond pas à {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Pour gérer les clés de ce compte, vérifiez votre phrase de récupération et réessayez.", + "seed-phrase-content": "Un ensemble de mots faciles à lire, sélectionnés au hasard dans la liste standard BIP39 et utilisés pour récupérer votre compte ou accéder à votre compte Ethereum sur d'autres portefeuilles et appareils. Également appelé « phrase mnémonique », « expression de récupération » ou « sauvegarde de portefeuille » dans l'écosystème cryptographique. La plupart des applications cryptographique utilisent ce même standard pour générer des comptes.", + "seed-phrase-placeholder": "Phrase de récupération", + "seed-phrase-title": "Phrase de récupération", + "select": "Sélectionner", + "select-account": "Sélectionner le compte", + "select-account-dapp": "Sélectionnez le compte que vous souhaitez utiliser avec Dapps", + "select-account-first": "Sélectionner d'abord un compte", + "select-chat": "Sélectionnez le chat pour commencer la messagerie", + "select-new-location-for-keys": "Sélectionnez un nouvel emplacement pour enregistrer votre\/vos clé(s) privée(s)", + "selected": "Sélectionné", + "send": "Envoyer", + "send-logs": "Signaler un bug", + "send-logs-to": "Signaler un bogue à {{email}}", + "send-message": "Envoyer un message", + "send-push-notifications-description": "En cas de désactivation, la personne qui reçoit vos messages ne sera pas avertie de leur réception", + "send-reply": "Envoyer la réponse", + "send-request": "Envoyer une requête", + "send-request-amount": "Montant", + "send-request-amount-max-decimals": "Le nombre maximum de décimales est {{asset-decimals}}", + "send-request-unknown-token": "Jeton inconnu - {{asset}}", + "send-sending-to": "à {{recipient-name}}", + "send-transaction": "Envoyer la transaction", + "sending": "Envoi", + "sent": "Envoyé", + "sent-at": "Envoyé à", + "server": "Serveur", + "set-a-topic": "Créer un sujet", + "set-currency": "Choisir votre devise", + "set-custom-fee": "établir frais personnalisés", + "set-dapp-access-permissions": "Définir les autorisations d'accès DApp", + "set-max": "Choisir le maximum", + "settings": "Réglages", + "share": "Partager", + "share-address": "Partager l'adresse", + "share-chat": "Partager un chat", + "share-community": "Partager la communauté", + "share-contact-code": "Partager mon code de contact", + "share-dapp-text": "Découvrez ce DApp que j'utilise sur Status: {{link}}", + "share-image": "Partager l'image", + "share-invite-link": "Partager un lien d'invitation", + "share-link": "Partager le lien", + "share-my-profile": "Partager mon profil", + "share-profile": "Partager le profil", + "share-profile-link": "Partager le lien du profil", + "share-public-chat-text": "Découvrez ce chat public sur l'application Status: {{link}}", + "shared": "Partagé", + "sharing-copied-to-clipboard": "Copié", + "sharing-copy-to-clipboard": "Copier", + "sharing-data-desc-1": "Les données sont validées par rapport aux règles publiques afin de garantir qu'aucune donnée sensible n'est envoyée. Ne faites pas confiance, vérifiez.", + "sharing-data-desc-2": "Les données d'utilisation sont envoyées cryptées de bout en bout sur le réseau peer-to-peer de Status.", + "sharing-data-desc-3": "Au lieu de votre clé de chat habituelle, une clé à usage unique est utilisée", + "sharing-data-desc-4": "Les données d'utilisation ne peuvent pas être associées à votre adresse IP", + "sharing-data-desc-5": "Les données cumulées de tous les utilisateurs sont disponibles publiquement.", + "sharing-data-desc-6": "Les données sont supprimées de votre téléphone après leur envoi.", + "sharing-share": "Partager...", + "shell-placeholder-subtitle": "Ouvrir les onglets de vos communautés, de vos messages, de votre compte portefeuille et de vos fenêtres de navigation", + "shell-placeholder-title": "Vos applications seront exécutées ici", + "show-less": "Afficher moins", + "show-more": "Afficher plus", + "show-notifications": "Afficher les notifications", + "show-profile-pictures": "Afficher les photos de profil de", + "show-profile-pictures-to": "Révélez votre photo de profil à", + "show-qr": "Afficher le QR code", + "show-transaction-data": "Afficher les données de transaction", + "sign-and-send": "Signer et envoyer", + "sign-anyway": "Signez quand même", + "sign-in": "Se connecter", + "sign-message": "Signer un message", + "sign-out": "Se déconnecter", + "sign-request-failed": "Impossible de signer le message", + "sign-with": "Se connecter avec", + "sign-with-password": "Signer avec mot de passe", + "sign-you-in": "Vous connecter...", + "signing": "Signer", + "signing-a-message": "Signature d'un message", + "signing-phrase": "Phrase de signature", + "skip": "Passer", + "slow": "Lent", + "something-went-wrong": "Quelque chose a mal tourné", + "soon": "Bientôt", + "sort-communities": "Trier les communautés", + "specify-address": "Spécifiez l'adresse", + "specify-name": "Spécifiez un nom", + "specify-network-id": "Spécifier l'id réseau", + "specify-rpc-url": "Spécifier une URL RPC", + "specify-server-public-key": "Entrez la clé publique du serveur", + "start-chat": "Démarrer le chat", + "start-conversation": "Démarrer la conversation", + "start-group-chat": "Démarrer le chat de groupe", + "start-new-chat": "Démarrer un nouveau chat", + "starter-pack-coming": "Le kit de départ arrive", + "starter-pack-coming-description": "Peut prendre de quelques minutes à quelques heures", + "starter-pack-received": "Pack de démarrage reçu", + "starter-pack-received-description": "Voici quelques crypto pour démarrer ! Utilisez-les pour obtenir des autocollants, un nom ENS et essayer des dapps", + "status": "Status", + "status-confirmed": "Confirmé", + "status-hardwallet": "Porte-monnaie électronique Status", + "status-is-open-source": "Status est à code ouvert", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status a tendance à utiliser beaucoup de données lors de la synchronisation des discussions. Vous pouvez choisir de ne pas synchroniser lorsque vous êtes sur le réseau mobile", + "status-not-sent-click": "Non confirmé. Cliquez pour les options", + "status-not-sent-tap": "Pas confirmé. Appuyez sur options pour", + "status-pending": "En attente", + "status-sent": "Envoyé", + "status-tx-not-found": "TX non trouvé", + "status-updates-descr": "Les mises à jour de statut s’affichent ici. Ajoutez le profil à vos contacts pour recevoir les mises à jour sur votre journal.", + "statuses-descr": "Partagez vos idées et restez à jour avec vos contacts", + "statuses-my-status-descr": "Partagez vos idées. Toute personne visitant votre profil pourra voir votre statut. Les personnes qui vous ajoutent comme contact recevront vos mises à jour sur leur chronologie", + "step-i-of-n": "Étape {{step}} de {{number}}", + "sticker": "Autocollant", + "sticker-market": "Marché des autocollants", + "storage": "Stockage", + "strikethrough": "Barré", + "submit": "Soumettre", + "submit-bug": "Soumettre un bogue", + "success": "Succès", + "successful-connection": "Connexion réussie", + "suggested-min-tip": "Pourboire minimum conseillé", + "suggested-price-limit": "Prix maximum conseillé", + "symbol": "Symbole", + "sync-all-devices": "Synchroniser tous les appareils", + "sync-in-progress": "Synchronisation...", + "sync-settings": "Paramètres de synchronisation", + "sync-synced": "En sync", + "syncing-devices": "Synchronisation...", + "system": "Système", + "tabs": "Onglets", + "tag-was-lost": "L'étiquette a été perdue", + "tap-card-again": "Poser à nouveau la carte à l'arrière de votre téléphone", + "terms-of-service": "Conditions d'utilisation", + "test-networks": "Réseaux de test", + "text-input-disabled": "Veuillez patienter un instant...", + "thank-you": "Merci", + "this-device": "Cet appareil", + "this-device-desc": "Votre clé sera cryptée et stockée en toute sécurité", + "this-is-you-signing": "Voici votre phrase de signature", + "this-will-take-few-seconds": "Cela prendra quelques secondes", + "three-days": "Trois jours", + "three-words-description": "Vous devriez voir ces trois mots avant de signer chaque transaction", + "three-words-description-2": "Si vous voyez une combinaison différente, annulez la transaction et déconnectez-vous.", + "timeline": "Chronologie", + "tip-cap": "Plafond des pourboires", + "to": "À", + "to-block": "Bloc", + "to-enable-biometric": "Pour activer {{bio-type-label}}, vous devez enregistrer votre mot de passe.", + "to-encrypt-enter-password": "Pour crypter le compte, veuillez entrer votre mot de passe", + "to-see-this-message": "Pour voir ce message,", + "token-auto-validate-decimals-error": "Décimales erronées pour le jeton {{symbol}} à l'adresse {{address}} - défini sur {{expected}} mais détecté comme étant {{actual}}", + "token-auto-validate-name-error": "Nom incorrect pour le jeton {{symbol}} à l'adresse {{address}} - défini sur {{expected}} mais détecté comme {{actual}}", + "token-auto-validate-symbol-error": "Mauvais symbole pour le jeton {{symbol}} à l'adresse {{address}} - défini sur {{expected}} mais détecté comme {{actual}}", + "token-details": "Détails du jeton", + "token-price": "Prix du jeton", + "topic-name-error": "Utilisez uniquement des lettres minuscules (a à z), des chiffres et des tirets (-). Ne pas utiliser les codes de contact", + "total-members": "Nombre total de membres", + "transaction": "Transaction", + "transaction-data": "Données de transaction", + "transaction-declined": "Transaction refusée", + "transaction-description": "Considérez-le comme terminé après 12 confirmations sur le réseau.", + "transaction-details": "Détails de la transaction", + "transaction-failed": "La transaction a échoué", + "transaction-history": "Historique des transactions", + "transaction-request": "Demande de transaction", + "transaction-sent": "Transaction envoyée", + "transaction-signed": "La transaction a été signée avec succès", + "transactions": "Transactions", + "transactions-filter-select-all": "Tout sélectionner", + "transactions-filter-title": "Filtrer l'historique", + "transactions-history": "Historique des transactions", + "transactions-history-empty": "Aucune transaction dans votre historique pour le moment", + "transactions-history-loading": "Chargement de l'historique des transactions. Cela peut prendre un certain temps.", + "transactions-load-more": "Afficher plus", + "transactions-management-enabled": "Gestion des transactions (alpha)", + "transactions-sign": "Connecter", + "transfer-ma-unknown-error-desc-1": "Il semble que votre multi-compte n'a pas été supprimé. La base de données a peut-être été réinitialisée", + "transfer-ma-unknown-error-desc-2": "Veuillez vérifier votre liste de comptes et réessayer. Si le compte n'est pas répertorié, allez sur le lien Accéder aux clés existantes pour récupérer avec votre phrase récupération", + "transfers-fetching-failure": "L'historique des transferts n'a pas pu être mis à jour. Vérifiez votre connexion et tirez vers le bas pour réessayer", + "tribute-required-by-multiaccount": "{{multiaccount-name}} requiert des SNT pour commencer un chat.", + "tribute-state-paid": "Hommage rendu", + "tribute-state-pending": "Hommage en attente", + "tribute-state-required": "Requiert un {{snt-amount}} hommage SNT", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Ajoutez des amis en tant que contact pour autoriser les discussions sans paiement tribute.", + "tribute-to-talk-are-you-friends": "Êtes-vous amis?", + "tribute-to-talk-ask-to-be-added": "Demander à être ajouté en tant que contact", + "tribute-to-talk-contact-received-your-tribute": " a reçu votre hommage. Vous pouvez maintenant discuter entre vous en toute sécurité.", + "tribute-to-talk-desc": "Monétisez votre attention en exigeant des SNT pour de nouvelles personnes de commencer un chat", + "tribute-to-talk-disabled": "Tribute to Talk désactivé", + "tribute-to-talk-disabled-note": "A partir de maintenant, de nouvelles personnes peuvent commencer un chat avec vous sans envoyer des SNT.", + "tribute-to-talk-enabled": "Vous avez activé Tribute to Talk.", + "tribute-to-talk-finish-desc": "À partir de maintenant, vous ne recevrez plus que des chats de vos contacts, et des personnes qui ont payé ", + "tribute-to-talk-learn-more-1": "Votre temps et votre attention sont vos atouts les plus précieux. Tribute to Talk vous permet de définir la quantité de SNT requise pour permettre à de nouvelles personnes de commencer une conversation avec vous.", + "tribute-to-talk-learn-more-2": "Toute personne ne figurant pas dans votre liste de contacts sera priée de payer et vous pourrez répondre une fois qu'ils l'auront été.", + "tribute-to-talk-learn-more-3": "Vous pouvez toujours renvoyer l'argent, mais pour que vos amis puissent vous joindre librement, ajoutez-les d'abord en tant que contact.", + "tribute-to-talk-paywall-learn-more-1": "Notre temps et notre attention sont nos atouts les plus précieux. Tribute to Talk vous permet de contacter de nouvelles personnes en échange d'un paiement SNT.", + "tribute-to-talk-paywall-learn-more-2": "Pour commencer une conversation avec quelqu'un qui a un ensemble d'hommage, il suffit de payer le SNT requis et vous serez ajouté comme un contact.", + "tribute-to-talk-paywall-learn-more-3": "Si vous les connaissez, vous pouvez partager votre profil en dehors de Status pour être ajouté gratuitement.", + "tribute-to-talk-pending": "Tribute en attente de confirmation", + "tribute-to-talk-pending-note": "La transaction Tribute est en attente de confirmation sur le réseau. Vous pouvez vérifier son statut dans l'historique des transactions", + "tribute-to-talk-removing-note": "La suppression de Tribute to Talk permettra aux nouvelles personnes de démarrer une discussion sans envoyer des SNT. Nécessite une transaction à effectuer.", + "tribute-to-talk-set-snt-amount": "Définir la quantité de SNT requise pour que les nouvelles personnes commencent un chat", + "tribute-to-talk-signing": "En attente de la signature de la transaction", + "tribute-to-talk-transaction-failed-note": "La transaction a échoué et vos paramètres Tribute to Talk n'ont pas été modifiés.", + "tribute-to-talk-tribute-received1": "Hommage reçu. Vous et ", + "tribute-to-talk-tribute-received2": " sont maintenant des contacts et peuvent discuter entre eux en toute sécurité.", + "tribute-to-talk-you-require-snt": "Vous avez besoin de SNT pour que de nouvelles personnes commencent un chat.", + "try-again": "Essayez à nouveau", + "try-keeping-the-card-still": "Essayez de ne pas bouger la carte", + "turn-nfc-description": "NFC est désactivé sur votre appareil. Vous pouvez l'activer dans les paramètres", + "turn-nfc-on": "Allumez le NFC pour continuer", + "two-minutes": "deux minutes", + "tx-fail-description1": "Cette transaction est susceptible d'échouer. Signez à vos propres risques en utilisant les frais de réseau personnalisés.", + "tx-fail-description2": "Cette transaction est susceptible d'échouer. Fixez des frais de réseau personnalisés pour signer à vos propres risques.", + "type": "Type", + "type-a-message": "Message", + "type-something": "Écrire quelque chose", + "ulc-enabled": "ULC activé", + "unable-to-fetch": "Impossible de récupérer l'historique des discussions", + "unable-to-read-this-code": "Impossible de lire ce code", + "unable-to-send-messages": "Impossible d'envoyer et de recevoir des messages", + "unblock": "Débloquer", + "unblock-contact": "Débloquer le contact", + "undo": "Annuler", + "unknown-status-go-error": "Erreur inconnue status-go", + "unlock": "Déverrouiller", + "unmute": "Réactiver", + "unmute-chat": "Réactiver le chat", + "unmute-community": "Réactiver la communauté", + "unmute-group": "Réactiver le groupe", + "unpair-card": "Dissocier la carte", + "unpair-card-confirmation": "Cette opération va dissocier la carte du périphérique actuel. Nécessite une autorisation par code PIN. Voulez-vous poursuivre?", + "unpair-keycard": "Dissocier la Keycard de ce téléphone", + "unpair-keycard-warning": "Votre code de jumelage PUK (clé personnelle de déblocage) et PIN restent inchangés", + "unpaired-keycard-text": "La Keycard que vous avez utilisée n'est pas associée à ce téléphone", + "unpaired-keycard-title": "On dirait que votre carte n'est plus jumelée", + "unpin": "Retirer l'épingle", + "unpin-from-channel": "Retirer de la chaîne", + "unpin-from-chat": "Retirer l'épingle du chat", + "untrustworthy": "Non fiable", + "update": "Mettre à jour", + "update-to-listen-audio": "Mettez à jour la dernière version pour écouter un message audio ici !", + "update-to-see-image": "Mettez à jour la dernière version pour voir une belle image ici !", + "update-to-see-sticker": "Mettez à jour la dernière version pour voir un joli autocollant ici !", + "updates-to-tos": "Mises à jour des conditions d’utilisation", + "updates-to-tos-desc": "Avant de continuer, veuillez consulter les Conditions d’utilisation et confirmer que vous assumez l’entière responsabilité de la façon dont vous utilisez l’application.", + "url": "URL", + "usd-currency": "USD", + "use-as-profile-picture": "Utiliser une photo de profil", + "use-valid-contact-code": "Veuillez entrer ou scanner un code de contact ou un nom d'utilisateur valide.", + "validation-amount-invalid-number": "Le montant n'est pas un nombre valide", + "validation-amount-is-too-precise": "Le montant est trop précis. Le nombre maximum de décimales est {{decimals}} .", + "verified-community": "✓ Communauté vérifiée", + "version": "Version de l'application", + "view": "Vue", + "view-community-rules": "Afficher les règles de la communauté", + "view-cryptokitties": "Voir dans CryptoKitties", + "view-cryptostrikers": "Afficher dans CryptoStrikers", + "view-data": "Voir les données", + "view-details": "Voir les détails", + "view-etheremon": "Voir dans Etheremon", + "view-gitcoin": "Voir sur Gitcoin", + "view-members": "Voir les membres", + "view-on-opensea": "Voir sur OpenSea", + "view-pinned-messages": "Afficher les messages épinglés", + "view-profile": "Voir le profil", + "view-public-dashboard": "Voir le tableau de bord public", + "view-rules": "Voir les règles", + "view-signing": "Voir la phrase de signature", + "view-superrare": "Vue en SuperRare", + "vote-to-feature": "Voter pour mettre en avant cette communauté", + "waiting-for-wifi": "Historique synchronisé hors ligne, en attente de Wi-Fi.", + "waiting-for-wifi-change": "Paramètres", + "waiting-to-sign": "En attente de la signature de la transaction....", + "waiting-wi-fi": "En attente de Wi-Fi…", + "waku-bloom-filter-mode": "Mode filtre de Bloom de Waku", + "wakuv2-change-nodes": "Êtes-vous sûr de vouloir changer les nœuds Wakuv2 ?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "Réglages Waku v2", + "wallet": "Portefeuille", + "wallet-address": "Adresse de portefeuille", + "wallet-asset": "Actif", + "wallet-assets": "Actifs", + "wallet-backup-recovery-title": "Sauvegardez votre phrase de récupération", + "wallet-choose-recipient": "Choisissez le destinataire", + "wallet-collectibles": "Collectors", + "wallet-insufficient-funds": "Fonds insuffisants", + "wallet-insufficient-gas": "Pas assez d'ETH pour le gaz", + "wallet-invalid-address": "Adresse invalide: \n {{data}}", + "wallet-invalid-address-checksum": "Erreur d'adresse: \n {{data}}", + "wallet-invalid-chain-id": "Le réseau ne correspond pas: \n {{data}} mais la chaîne actuelle est {{chain}}", + "wallet-key-content": "Une adresse hexadécimale de 64 caractères basée sur la norme Ethereum et commençant par 0x. Face au public, votre clé de portefeuille est partagée avec les autres quand vous souhaitez recevoir des fonds. Également appelée «adresse Ethereum» ou «adresse de portefeuille».", + "wallet-key-title": "Adresse du compte", + "wallet-manage-accounts": "Gérer les comptes", + "wallet-manage-assets": "Gérer les actifs", + "wallet-request": "Requête", + "wallet-send": "Envoyer", + "wallet-send-min-units": "Min 21000 unités", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Paramètres du portefeuille", + "wallet-total-value": "Valeur totale", + "wallet-transaction-total-fee": "Total des frais", + "wants-to-access-profile": "veut accéder à votre profil", + "warning": "Avertissement", + "warning-message": "Désolé, nous limitons l'envoi de plusieurs messages en succession rapide pour éviter le spam. Veuillez réessayer dans un instant.", + "warning-sending-to-contract-descr": "L'adresse que vous avez saisie est un contrat intelligent, l'envoi de fonds à cette adresse peut entraîner une perte de fonds. Pour interagir avec un DApp, ouvrez le DApp dans le navigateur Status DApp.", + "watch-only": "Compte de veille uniquement", + "wc-brand-guide": "Conseils sur l'utilisation des marques telles que les marques et les logos", + "wc-disclaimer": "Avis de non-responsabilité (y compris des fournisseurs tiers), garanties et décharges légales", + "wc-dispute": "Dispositions relatives au règlement des litiges", + "wc-how-to-use-status-app": "Comment utiliser l'application Status pour la confidentialité et la sécurité", + "wc-new-tos-based-on-principles-prefix": "De nouvelles conditions d'utilisation conçues sur la base de nos", + "web-view-error": "Impossible de charger la page", + "websites": "Sites internet", + "webview-camera-permission-requests": "Demandes d'autorisation de la caméra en Webview", + "webview-camera-permission-requests-subtitle": "Lorsqu'ils sont activés, les sites Web et les Dapps peuvent faire une demande d'utilisation de votre caméra", + "welcome-blank-community-message": "Vos communautés apparaîtront ici.", + "welcome-blank-message": "Vos discussions apparaîtront ici. Pour démarrer une nouvelle discussion, appuyez sur la touche ⊕", + "welcome-community-blank-message": "Vos discussions apparaîtront ici. Pour démarrer de nouvelles discussions, cliquez sur les trois points ci-dessus et sélectionnez \"Créer une chaîne\"", + "welcome-screen-text": "Configurez votre portefeuille, invitez vos amis à chatter\n et parcourez les dapps populaires !", + "welcome-to-status": "Bienvenue sur Status", + "welcome-to-status-description": "Configurez votre portefeuille crypto, invitez vos amis à discuter et parcourez les applications décentralisées", + "what-changed": "Ce qui a changé", + "what-is-shared": "Ce qui est partagé", + "whats-on-your-mind": "À quoi pensez-vous ?", + "whats-trending": "Découvrez les tendances", + "word-count": "Nombre de mots", + "word-n": "Mot # {{number}}", + "word-n-description": "Afin de vérifier si vous avez correctement sauvegardé votre phrase de récupération, entrez le mot #{{number}} ci-dessus.", + "words-n": { + "one": "1 mot", + "other": "{{count}} mots" + }, + "write-down-and-store-securely": "Écrire des codes \n & stockez-les en toute sécurité", + "wrong-address": "Mauvaise adresse", + "wrong-card": "Mauvaise carte", + "wrong-card-text": "Vous avez tapé sur une carte qui ne correspond pas au multi-compte sélectionné.", + "wrong-contract": "Mauvais contrat", + "wrong-keycard-text": "La Keycard que vous avez utilisée n'est pas associée à ce téléphone", + "wrong-keycard-title": "On dirait que vous avez utilisée \nune mauvaise Keycard", + "wrong-password": "Mauvais mot de passe", + "wrong-word": "Mauvais mot", + "yes": "Oui", + "you": "vous", + "you-already-have-an-asset": "Vous avez déjà un atout {{value}}", + "you-are-all-set": "Vous êtes prêts !", + "you-are-all-set-description": "Si vous perdez votre téléphone, vous pouvez maintenant accéder à vos fonds et à votre clé de chat en utilisant votre expression de semences", + "you-can-change-account": "Vous pouvez changer le nom du compte et la couleur à votre guise", + "you-can-choose-preview-websites": "Vous pouvez choisir lequel des sites internet suivants peut prévisualiser le lien des descriptions et des images dans les chats", + "you-can-fetch": "Vous pouvez récupérer l'historique des discussions", + "you-dont-have-contacts": "Pas encore de contacts", + "you-dont-have-contacts-invite-friends": "Vous n'avez pas encore de contacts.\nInvitez vos amis à discuter.", + "you-dont-have-stickers": "Vous n'avez pas encore de stickers", + "you-must-always-hold": "Vous devez toujours détenir :", + "you-must-hold": "Vous devez détenir :", + "you-must-now-hold": "Vous devez maintenant détenir :", + "you-will-need-this-code": "Vous aurez besoin de ce code pour ouvrir Status et signer des transactions.", + "you-will-start-from-scratch": "Vous recommencerez depuis le début avec un nouvel ensemble de clés", + "your-answer": "Votre réponse", + "your-card-is-frozen": "Votre Keycard est gelée. Réinitialiser l'accès à la carte", + "your-contact-code": "Accorder un accès autorise ce DApp à récupérer votre code de contact", + "your-data-belongs-to-you": "Si vous perdez votre phrase de récupération, vous perdez vos données et vos fonds", + "your-data-belongs-to-you-description": "Status ne peut pas vous aider à récupérer votre multi-compte si vous perdez votre phrase de récupération. Vous êtes responsable de la sécurité de vos données. Ecrire votre phrase de récupération quelque part sur constitue la meilleure protection.", + "your-keys": "Vos clés", + "your-price-limit": "Votre plafond des frais", + "your-recovery-phrase": "Votre phrase de récupération", + "your-recovery-phrase-description": "Ceci est votre phrase de récupération. Vous l'utilisez pour prouver qu'il s'agit de votre portefeuille. Vous ne le voyez qu'une fois! Ecrivez-le sur du papier et conservez-le dans un endroit sûr. Vous en aurez besoin si vous perdez ou réinstallez votre portefeuille.", + "your-tip-limit": "Votre plafond des pourboires", + "youre-on-mobile-network": "Vous êtes sur un réseau mobile" +} diff --git a/translations/fr_ch.json b/translations/fr_ch.json new file mode 100644 index 00000000000..2d14ccd4da7 --- /dev/null +++ b/translations/fr_ch.json @@ -0,0 +1,129 @@ +{ + "confirm": "Confirmer", + "amount": "Montant", + "members-active": { + "one": "1 membre, 1 actif", + "other": "{{count}} membres, {{count}} actif(s)", + "zero": "pas de membres" + }, + "chat-name": "Nom de chat", + "phew-here-is-your-passphrase": "*Pffff* c'était dur, voici votre phrase de passe, *notez-la et gardez-la en sécurité!* Vous en aurez besoin pour récupérer votre compte.", + "public-group-topic": "Sujet", + "chat-settings": "Paramètres de chat", + "offline": "Hors ligne", + "invited": "invité", + "address": "Adresse", + "new-public-group-chat": "Rejoindre le chat public", + "datetime-hour": { + "one": "heure", + "other": "heures" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "Supprimer", + "add-members": "Ajouter des membres", + "done": "Terminé", + "close-chat": "Supprimer la discussion", + "new-group-chat": "Nouveau chat de groupe", + "sign-in": "S'identifier", + "datetime-yesterday": "hier", + "create-new-account": "Créer un nouveau compte", + "datetime-ago": "avant", + "contacts": "Contacts", + "got-it": "J'ai compris", + "active-online": "En ligne", + "password": "Mot de passe", + "browsing-title": "Parcourir", + "discover": "Découverte", + "browsing-cancel": "Annuler", + "intro-status": "Chattez avec moi pour configurer votre compte et changer vos paramètres !", + "name": "Nom", + "show-qr": "Montrer QR", + "connect": "Se connecter", + "edit": "Modifier", + "account-generation-message": "Une seconde svp, je dois lancer quelques formules magiques pour générer votre compte !", + "no-messages": "Pas de messages", + "passphrase": "Phrase de passe", + "recipient": "Destinataire", + "members-title": "Membres", + "new-group": "Nouveau groupe", + "phone-e164": "International 1", + "settings": "Paramètres", + "chats": "Chats", + "transaction": "Transaction", + "public-group-status": "Public", + "image-source-make-photo": "Capture", + "start-conversation": "Débuter une conversation", + "save": "Sauvegarder", + "sharing-copy-to-clipboard": "Copier vers le presse-papiers", + "sync-in-progress": "En cours de synchronisation...", + "send-transaction": "Envoyer la transaction", + "incorrect-code": [ + "str", + "Désolé, le code est incorrect, veuillez réessayer" + ], + "image-source-gallery": "Sélectionner dans la galerie", + "sync-synced": "Synchronisé", + "status-pending": "En attendant", + "datetime-day": { + "one": "jour", + "other": "jours" + }, + "scan-qr": "Scannner QR", + "contact-s": { + "one": "contact", + "other": "contacts" + }, + "next": "Suivant", + "recent": "Récent", + "status": "Statut", + "from": "De", + "wrong-password": "Mauvais mot de passe", + "in-contacts": "Dans les contacts", + "sharing-share": "Partagez…", + "type-a-message": "Tapez un message…", + "clear-history": "Effacer l'historique", + "no-contacts": "Pas encore de contacts", + "datetime-today": "aujourd'hui", + "web-view-error": "oups, erreur", + "error": "Erreur", + "more": "plus", + "cancel": "Annuler", + "can-not-add-yourself": "Vous ne pouvez pas vous ajouter vous-même", + "add-to-contacts": "Ajouter aux contacts", + "available": "Disponible", + "You": "Vous", + "main-wallet": "Portefeuille principal", + "members": { + "one": "1 membre", + "other": "{{count}} membres", + "zero": "pas de membres" + }, + "intro-message1": "Bienvenue dans le statut\nTapez ce message pour établir votre mot de passe et vous lancer !", + "new-contact": "Nouveau Contact", + "datetime-second": { + "one": "seconde", + "other": "secondes" + }, + "recover": "Restaurer", + "datetime-minute": { + "one": "minute", + "other": "minutes" + }, + "browsing-open-in-android-web-browser": "Ouvrir dans le navigateur", + "browsing-open-in-ios-web-browser": "Ouvrir dans le navigateur", + "edit-profile": "Modifier le profil", + "active-unknown": "Inconnu", + "public-key": "Clé publique", + "profile": "Profil", + "none": "Aucun", + "removed": "supprimé", + "message": "Message", + "here-is-your-passphrase": "Voici votre phrase de passe, *notez-la et gardez-la en sécurité!* Vous en aurez besoin pour récupérer votre compte.", + "image-source-title": "Image du profil", + "left": "restant", + "to": "À", + "status-not-sent-click": "Non confirmé. Cliquez pour les options", + "status-not-sent-tap": "Pas confirmé. Appuyez sur options pour", + "status-sent": "Envoyé", + "data": "Données" +} diff --git a/translations/fy.json b/translations/fy.json new file mode 100644 index 00000000000..83a90c27c26 --- /dev/null +++ b/translations/fy.json @@ -0,0 +1,131 @@ +{ + "confirm": "Befestigje", + "amount": "Bedrach", + "members-active": { + "one": "1 lid, 1 aktive", + "other": "{{count}} leden, {{count}} aktive", + "zero": "gjin leden" + }, + "chat-name": "Chatnamme", + "phew-here-is-your-passphrase": "*Poah* dat wie dreech, hjir is dyn wachtsin, *skriuw dizze op en bewarje him goed!* Do silst him nedich ha om dyn account te herstelle.", + "public-group-topic": "Ûnderwerp", + "chat-settings": "Chatynstellingen", + "offline": "Offline", + "invited": "útnoege", + "address": "Adres", + "new-public-group-chat": "Doch mei yn iepenbare chat", + "datetime-hour": { + "one": "oere", + "other": "oeren" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "Om de kamera tastimming te jaan, gea nei dyn systeem ynstellings en wêz wis dat Status > Camera selektearre is.", + "remove": "Fuortsmite", + "add-members": "Foegje leden ta", + "photos-access-error": "Om ús tastimming te jaan oan dyn foto's, gea nei dyn systeem ynstellings en wêz wis dat Status > Foto's selektearre is.", + "done": "Klear", + "close-chat": "Chat fuortsmite", + "new-group-chat": "Nije groepchat", + "sign-in": "Ynlogge", + "datetime-yesterday": "juster", + "create-new-account": "Nij akkount oanmeitsje", + "datetime-ago": "lyn", + "contacts": "Kontaktpersoanen", + "got-it": "Befetsje ik", + "active-online": "Online", + "password": "Wachtwurd", + "browsing-title": "Blêdzje", + "discover": "Ûntdekking", + "browsing-cancel": "Ôfbrekke", + "intro-status": "Chat mei my om dyn account yn te stellen en dyn ynstellingen te wizigjen!", + "name": "Namme", + "show-qr": "Toan QR", + "connect": "Ferbine", + "edit": "Bewurkje", + "account-generation-message": "Jou my in momintsje, ik moat wat yngewikkelde berekkeningen dwaan om dyn account oan te meitsjen !", + "no-messages": "Gjin berjochten", + "passphrase": "Wachtsin", + "recipient": "Ûntfanger", + "members-title": "Leden", + "new-group": "Nije groep", + "phone-e164": "Ynternasjonaal 1", + "settings": "Ynstellingen", + "delete": "Wiskje", + "chats": "Chats", + "transaction": "Transaksje", + "public-group-status": "Iepenbier", + "image-source-make-photo": "Foto nimme", + "start-conversation": "Start konversaasje", + "save": "Opslaan", + "sharing-copy-to-clipboard": "Nei it klamboerd ta kopiearje", + "sync-in-progress": "Wurdt syngronisearre ...", + "send-transaction": "Stjoer transaksje", + "incorrect-code": [ + "str", + "Sorry, de koade wie ferkeard, fier hem opnei yn" + ], + "image-source-gallery": "Kies út galerij", + "sync-synced": "Syngronisearre", + "status-pending": "Yn behanneling", + "datetime-day": { + "one": "dei", + "other": "dagen" + }, + "scan-qr": "QR scanne", + "contact-s": { + "one": "kontakt", + "other": "kontakten" + }, + "next": "Folgjende", + "recent": "Koartlyn", + "status": "Status", + "from": "Fan", + "wrong-password": "Ferkeard wachtwurd", + "in-contacts": "Yn kontakten", + "sharing-share": "Diele...", + "type-a-message": "Typ in berjocht...", + "clear-history": "Wis skiednis", + "no-contacts": "Noch gjin kontaktpersoanen", + "datetime-today": "hjoed", + "web-view-error": "oeps, flater", + "error": "Flater", + "more": "mear", + "cancel": "Ôfbrekke", + "can-not-add-yourself": "Do kinst dysels net tafoegje", + "add-to-contacts": "Oan kontaktpersoanen tafoegje", + "available": "Beskikber", + "You": "Do", + "main-wallet": "Haadbeurs", + "members": { + "one": "1 lid", + "other": "{{count}} leden", + "zero": "gjin leden" + }, + "intro-message1": "Wolkom by Status\nTik op dit berjocht om dyn wachtwurd yn te stellen en oan de slach te gean!", + "new-contact": "Nije kontaktpersoanen", + "datetime-second": { + "one": "sekonde ", + "other": " sekonden" + }, + "recover": "Herstelle", + "datetime-minute": { + "one": "minút", + "other": "minuten" + }, + "browsing-open-in-android-web-browser": "Iepenje yn web blêder", + "browsing-open-in-ios-web-browser": "Iepenje yn web blêder", + "edit-profile": "Profyl oanpasse", + "active-unknown": "Ûnbekend", + "public-key": "Iepenbiere kaai", + "profile": "Profyl", + "none": "Gjin", + "removed": "ferwidere", + "message": "Berjocht", + "here-is-your-passphrase": "Dat wie dreech, hjir is dyn wachtsin, *skriuw dizze op en bewarje him goed!* Do silst him nedich ha om dyn account te herstelle.", + "image-source-title": "Profylfoto", + "current-network": "Aktuele netwurk", + "left": "gie fuort", + "to": "Ta", + "status-sent": "Ferstjoerd", + "data": "Data" +} diff --git a/translations/he.json b/translations/he.json new file mode 100644 index 00000000000..91f0bf4482d --- /dev/null +++ b/translations/he.json @@ -0,0 +1,131 @@ +{ + "confirm": "אשר", + "amount": "כמות", + "members-active": { + "one": "חבר אחד", + "other": "{{count}} חברים", + "zero": "אין חברים" + }, + "chat-name": "שם הצ'אט", + "phew-here-is-your-passphrase": "פוו זה היה קשה, הנה משפט הקוד שלך, *תרשום אותו ותשמור עליו!* אתה תצטרך אותו כדי לשחזר את המשתמש שלך", + "public-group-topic": "נושא", + "chat-settings": "הגדרות צ'אט", + "offline": "לא מחובר", + "invited": "הוזמן", + "address": "כתובת", + "new-public-group-chat": "הצטרף לצ'אט פומבי", + "datetime-hour": { + "one": "שעה", + "other": "שעות" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": ".כדי להעניק את הרשאת המצלמה הנדרשת, בבקשה, תלכו להגדרות המערכת ותוודאו שהאופציה שנבחרה היא סטטוס > מצלמה", + "remove": "הסר", + "add-members": "הוסף מספרים", + "photos-access-error": ".כדי להעניק את הרשאת התמונות הנדרשת, בבקשה, תלכו להגדרות המערכת ותוודאו שהאופציה שנבחרה היא סטטוס > תמונות", + "done": "הושלם", + "close-chat": "מחק צ'אט", + "new-group-chat": "צ'אט קבוצתי חדש", + "sign-in": "התחבר", + "datetime-yesterday": "אתמול", + "create-new-account": "יצר איש קשר חדש", + "datetime-ago": "לפני", + "contacts": "אנשי קשר", + "got-it": "קיבלתי", + "active-online": "מחובר", + "password": "סיסמא", + "browsing-title": "דפדף", + "discover": "גילוי", + "browsing-cancel": "בטל", + "intro-status": "דבר איתי כדי להסדיר את המשתמש שלך ולשנות את ההגדרות", + "name": "שם", + "show-qr": "הראה ברקוד", + "connect": "התחבר", + "edit": "ערוך", + "account-generation-message": "!שניה, אני הולך להשתמש בקצת קסם מתמטי ולייצר את חשבונך", + "no-messages": "אין הודעות", + "passphrase": "משפט קוד", + "recipient": "מקבל", + "members-title": "חבר", + "new-group": "קבוצה חדשה", + "phone-e164": "בין לאומי 1", + "settings": "הגדרות", + "delete": "מחק", + "chats": "צ'אטים", + "transaction": "העברות", + "public-group-status": "פומבי", + "image-source-make-photo": "צלם", + "start-conversation": "התחל שיחה", + "save": "שמור", + "sharing-copy-to-clipboard": "העתק לשולחן העבודה", + "sync-in-progress": "...מסנכרן", + "send-transaction": "שלח עיסקה", + "incorrect-code": [ + "str", + "סליחה הקוד שגוי, אנא נסה שנית" + ], + "image-source-gallery": "בחר מהגלריה", + "sync-synced": "מסונכרן", + "status-pending": "ממתין", + "datetime-day": { + "one": "יום", + "other": "ימים" + }, + "scan-qr": "סרוק ברקוד", + "contact-s": { + "one": "איש קשר", + "other": "אנשי קשר" + }, + "next": "הבא", + "recent": "חדש", + "status": "סטטוס", + "from": "מ", + "wrong-password": "סיסמא שגויה", + "in-contacts": "באנשי קשר", + "sharing-share": "...שתף", + "type-a-message": "...הקלד הודעה", + "clear-history": "נקה היסטוריה", + "no-contacts": "אין שום אנשי קשר", + "datetime-today": "היום", + "web-view-error": "אופס,טעות", + "error": "שגיאה", + "more": "עוד", + "cancel": "בטל", + "can-not-add-yourself": "אתה לא יכול להוסיף את עצמך", + "add-to-contacts": "הוסף לאנשי קשר", + "available": "זמין", + "You": "אתה", + "main-wallet": "ארנק ראשי", + "members": { + "one": "חבר אחד", + "other": "{{count}} חברים", + "zero": "אין חברים" + }, + "intro-message1": "!ברוך הבא לסטטוס הקש על הודעה זו כדי לייצר סיסמא ולהתחיל", + "new-contact": "איש קשר חדש", + "datetime-second": { + "one": "שניה", + "other": "שניות" + }, + "recover": "שחזר", + "datetime-minute": { + "one": "דקה", + "other": "דקות" + }, + "browsing-open-in-android-web-browser": "פתח בדפדפן", + "browsing-open-in-ios-web-browser": "פתח בדפדפן", + "edit-profile": "ערוך פרופיל", + "active-unknown": "לא ידוע", + "public-key": "מפתח פומבי", + "profile": "פרופיל", + "none": "אף אחד", + "removed": "הוסר", + "message": "הודעה", + "here-is-your-passphrase": "הנה משפט הקוד שלך, *תרשום אותו ותשמור עליו!* אתה תצטרך אותו כדי לשחזר את המשתמש שלך.", + "image-source-title": "תמונת פרופיל", + "current-network": "Current network", + "left": "עזב", + "to": "ל", + "status-sent": "נשלחה", + "data": "נתונים" +} diff --git a/translations/hi.json b/translations/hi.json new file mode 100644 index 00000000000..d01af59245d --- /dev/null +++ b/translations/hi.json @@ -0,0 +1,1874 @@ +{ + "K": "K", + "M": "एम", + "NFT": "एनएफटी", + "You": "आप", + "about-app": "लगभग", + "about-key-storage-content": "Status कभी भी आपकी निजी कुंजी तक नहीं पहुंच पाएगी। अपने बीज वाक्यांश का बैकअप लेना सुनिश्चित करें। यदि आप अपना फोन खो देते हैं तो यह आपकी चाबियों तक पहुंचने का एकमात्र तरीका है।", + "about-key-storage-title": "कुंजी भंडारण के बारे में", + "about-names-content": "कोई भी आपके होने का दिखावा नहीं कर सकता! आप डिफ़ॉल्ट रूप से गुमनाम हैं और आपको कभी भी अपना वास्तविक नाम प्रकट नहीं करना है। आप एक छोटे से शुल्क के लिए एक कस्टम नाम पंजीकृत कर सकते हैं।", + "about-names-title": "नाम नहीं बदले जा सकते", + "about-sharing-data": "डेटा साझा करने के बारे में", + "accept": "स्वीकार करना", + "accept-and-add": "स्वीकार करें और जोड़ें", + "accept-and-continue": "स्वीकार करें और जारी रखें", + "accept-and-share-address": "स्वीकार करें और पता साझा करें", + "accept-community-rules": "मैं समुदाय के नियमों से सहमत हूं", + "accept-new-chats-from": "से नई चैट स्वीकार करें", + "accept-status-tos-prefix": "मैं Status स्वीकार करता हूं", + "accepted": "स्वीकृत", + "access-existing-keys": "मौजूदा कुंजियों तक पहुंचें", + "access-key": "आगमन चाबी", + "account-added": "खाता जोड़ा गया", + "account-color": "खाते का रंग", + "account-content": "आप स्टेटस में खातों की तुलना बैंक खातों से कर सकते हैं। बैंक खाते की तरह, खाते में आमतौर पर एक पता और शेष राशि होती है; आप इस खाते का उपयोग इथेरियम पर लेनदेन करने के लिए करते हैं। आपके वॉलेट में कई खाते हो सकते हैं। स्थिति को अनलॉक करके सभी को एक्सेस किया गया।", + "account-exists-title": "खाता पहले से मौजूद है", + "account-is-used": "खाते का उपयोग ब्राउज़र में Dapps के साथ किया जा रहा है।", + "account-name": "खाता नाम", + "account-settings": "अकाउंट सेटिंग", + "account-title": "खाता", + "accounts": "हिसाब किताब", + "actions": "कार्रवाई", + "active-members": "सक्रिय सदस्य", + "active-online": "ऑनलाइन", + "active-unknown": "अज्ञात", + "activity": "गतिविधि", + "add": "जोड़ें", + "add-a-watch-account": "केवल देखने के लिए पता जोड़ें", + "add-account": "एक खाता जोड़ें", + "add-account-description": "आप इसे अपने स्टेटस वॉलेट में जोड़ने के लिए किसी भी प्रकार के एथेरियम खाते को आयात कर सकते हैं", + "add-account-incorrect-password": "पासवर्ड गलत लगता है। वह पासवर्ड दर्ज करें जिसका उपयोग आप ऐप को अनलॉक करने के लिए करते हैं।", + "add-an-account": "एक खाता जोड़ें", + "add-bootnode": "बूटनोड जोड़ें", + "add-contact": "संपर्क जोड़ें", + "add-custom-token": "कस्टम टोकन जोड़ें", + "add-favourite": "पसंदीदा जोड़ें", + "add-mailserver": "Status नोड जोड़ें", + "add-members": "सदस्यों को जोड़ें", + "add-network": "नेटवर्क जोड़ें", + "add-new-contact": "नए संपर्क को जोड़े", + "add-nickname": "एक उपनाम जोड़ें (वैकल्पिक)", + "add-node": "नोड जोड़ें", + "add-private-key-account": "निजी कुंजी से खाता जोड़ें", + "add-seed-account": "एक बीज वाक्यांश के साथ खाता जोड़ें", + "add-text": "शब्द जोड़ें", + "add-to-contacts": "संपर्क के खाते में जोड़ दे", + "add-to-contacts-text": "किसी उपयोगकर्ता को अपनी संपर्क सूची में जोड़कर, आप अपना बटुआ पता साझा करते हैं", + "add-to-favourites": "पसंदीदा में जोड़ें", + "add-watch-account": "केवल देखने के लिए खाता जोड़ें", + "added": "जोड़ा", + "address": "पता", + "address-or-ens-name": "पता या ईएनएस नाम", + "address-received": "पता प्राप्त", + "address-request-accepted": "पता अनुरोध स्वीकार किया गया", + "address-requested": "पता अनुरोधित", + "admin": "व्यवस्थापक", + "advanced": "विकसित", + "advanced-settings": "एडवांस सेटिंग", + "advertiser-description": "आपने एक भागीदार के लिए धन्यवाद स्थिति की खोज की है। क्या आपको ऐतराज है अगर Status एक बार आपके आईपी पते की जांच कर ले ताकि उन्हें इनाम मिल जाए? इस जानकारी का उपयोग किसी और चीज के लिए नहीं किया जाएगा और इसे 7 दिनों के बाद पूरी तरह से हटा दिया जाएगा।", + "advertiser-starter-pack-accept": "स्वीकार करना", + "advertiser-starter-pack-decline": "पतन", + "advertiser-starter-pack-description": "शुरू करने के लिए यहां कुछ क्रिप्टो है! स्टिकर, एक ENS नाम प्राप्त करने के लिए इसका उपयोग करें और dapps आज़माएं", + "advertiser-starter-pack-title": "स्टार्टर पैक", + "advertiser-title": "डिफ़ॉल्ट रूप से गोपनीयता", + "agree-by-continuing": "जारी रखकर आप सहमत हैं\n हमारे लिए", + "all": "सभी", + "all-connections": "सभी कनेक्शन", + "allow": "अनुमति देना", + "allow-and-send": "अनुमति दें और भेजें", + "allow-mention-notifications": "@ उल्लेख दिखाएं", + "allowing-authorizes-this-dapp": "अनुमति देना इस DApp को आपके वॉलेट पते को पुनः प्राप्त करने और Web3 को सक्षम करने के लिए अधिकृत करता है", + "alphabetically": "वर्णानुक्रम", + "already-have-asset": "आपके पास पहले से ही यह संपत्ति है", + "amount": "राशि", + "and": "तथा", + "anyone": "कोई भी", + "app-commit": "ऐप्लिकेशन कमिट", + "app-connections": "ऐप्लिकेशन कनेक्शन", + "appearance": "उपस्थिति", + "apply": "आवेदन करना", + "approve": "मंज़ूरी देना", + "approve-limit": "सीमा अनुमोदित करें", + "approve-token": "टोकन स्वीकृत करें", + "approve-token-contract-desc": "एक अनुबंध के साथ एक टोकन को मंजूरी देने से यह आपकी टोकन शेष राशि खर्च कर सकता है। यदि आपको लगता है कि कोई परियोजना अविश्वसनीय है, तो उनके साथ टोकन स्वीकृत न करें, या केवल उस राशि को स्वीकृत करें जिसका आप उनके साथ उपयोग करेंगे।", + "are-you-sure": "क्या आपको यकीन है?", + "are-you-sure-description": "आप पूरे बीज वाक्यांश को दोबारा नहीं देख पाएंगे", + "are-you-sure-to-cancel": "पक्का आप रद्द करना चाहते हैं?", + "are-you-sure?": "क्या आपको यकीन है?", + "ask-in-status": "कोई प्रश्न पूछें या बग की रिपोर्ट करें", + "at": "पर", + "attribution-received": "{{attrib}} प्राप्त हुए {{max}} बोनस में से", + "audio": "ऑडियो", + "audio-message": "ऑडियो संदेश", + "audio-recorder": "रिकॉर्डर", + "audio-recorder-error": "रिकॉर्डर त्रुटि", + "audio-recorder-max-ms-reached": "अधिकतम रिकॉर्डिंग समय तक पहुंच गया", + "audio-recorder-permissions-error": "आपको ऑडियो संदेश भेजने की अनुमति देनी होगी", + "authorize": "अधिकृत", + "available": "उपलब्ध", + "available-participants": { + "one": "You can select one more participant", + "other": "You can select {{count}} more participants" + }, + "back": "वापस", + "back-up": "बैक अप", + "back-up-seed-phrase": "बैक अप बीज वाक्यांश", + "back-up-your-seed-phrase": "अपने बीज वाक्यांश का बैकअप लें", + "backing-up": "बैकअप ले रहा है...", + "backup-disabled": "अक्षम", + "backup-enabled": "सक्रिय", + "backup-recovery-phrase": "बैक अप बीज वाक्यांश", + "backup-settings": "बैकअप सेटिंग्स", + "backup-through-waku": "waku . के माध्यम से बैकअप", + "bad-fees-description": "आपका प्राथमिकता शुल्क हमारे सुझाए गए मापदंडों से कम है।", + "balance": "संतुलन", + "begin-set-up": "सेटअप शुरू करें", + "below-base-fee": "आधार शुल्क से कम अधिकतम शुल्क", + "biometric-auth-android-sensor-desc": "स्पर्श संवेदक", + "biometric-auth-android-sensor-error-desc": "अनुत्तीर्ण होना", + "biometric-auth-android-title": "प्रमाणीकरण की आवश्यकता है", + "biometric-auth-confirm-logout": "पुन लॉगिन", + "biometric-auth-confirm-message": "बायोमेट्रिक प्रमाणीकरण जारी रखने के लिए आवश्यक है, यदि संभव नहीं है तो कृपया अपने पासवर्ड या पासकोड के साथ अपनी चाबियाँ अनलॉक करें", + "biometric-auth-confirm-title": "आपको प्रमाणित करना होगा!", + "biometric-auth-confirm-try-again": "पुनः प्रयास करें", + "biometric-auth-error": "बायोमेट्रिक प्रमाणीकरण करने में असमर्थ ( {{code}} )", + "biometric-auth-login-error-title": "बायोमेट्रिक प्रमाणीकरण त्रुटि", + "biometric-auth-login-ios-fallback-label": "पास वर्ड दर्ज करें", + "biometric-auth-reason-login": "Status में लॉगिन करें", + "biometric-auth-reason-verify": "प्रमाणीकरण सत्यापित करें", + "biometric-disable-bioauth": "अक्षम {{bio-type-label}}", + "biometric-disable-password-description": "यदि आप इसे अक्षम करते हैं, तो आप भी करेंगे", + "biometric-disable-password-title": "पासवर्ड सहेजना अक्षम करें", + "biometric-enable": "यदि आप ऐप को एक्सेस करने के लिए हर बार अपना पासवर्ड दर्ज नहीं करना चाहते हैं, तो {{bio-type-label}} साइन इन सक्षम करें", + "biometric-enable-button": "{{bio-type-label}} सक्षम करें", + "biometric-enable-keycard": "यदि आप ऐप को एक्सेस करने के लिए हर बार अपने कीकार्ड का उपयोग नहीं करना चाहते हैं, तो {{bio-type-label}} साइन इन सक्षम करें", + "biometric-faceid": "फेस आईडी", + "biometric-fingerprint": "अंगुली की छाप", + "biometric-secure-with": "{{bio-type-label}} साथ सुरक्षित", + "biometric-touchid": "टच आईडी", + "bip39-password-placeholder": "BIP39 पासवर्ड", + "biu": "BIU", + "blank-contacts-text": "आपके संपर्क यहां होंगे", + "blank-keycard-text": "एक बार जब आप अपनी चाबियां और नाम बना लेते हैं तो आप अपने कीकार्ड के साथ आगे बढ़ सकते हैं", + "blank-keycard-title": "ऐसा लगता है कि आपने टैप किया है\n एक खाली कीकार्ड", + "blank-messages-text": "आपके संदेश यहां होंगे", + "block": "अवरोध पैदा करना", + "block-contact": "इस उपयोगकर्ता को ब्लॉक करें", + "block-contact-details": "अवरोधित करने से इस उपयोगकर्ता के पिछले संदेश हट जाएंगे और नए संदेशों को आप तक पहुंचने से रोक दिया जाएगा", + "block-user": "उपयोगकर्ता को ब्लॉक करें", + "blocked-users": "रोके गए उपयोगकर्ता", + "bold": "बोल्ड", + "bootnode-address": "बूटनोड पता", + "bootnode-details": "बूटनोड विवरण", + "bootnode-format": "एनोड: \/\/ {enode-id} @ {ip-address} : {port}", + "bootnodes": "बूटनोड्स", + "bootnodes-enabled": "बूटनोड्स सक्षम", + "bootnodes-settings": "बूटनोड्स सेटिंग्स", + "browsed-websites": "ब्राउज़र इतिहास यहां दिखाई देगा", + "browser": "ब्राउज़र", + "browser-not-secure": "कनेक्शन सुरक्षित नहीं है! इस साइट पर लेनदेन पर हस्ताक्षर न करें या व्यक्तिगत डेटा न भेजें।", + "browser-secure": "कनेक्शन सुरक्षित है। लेन-देन पर हस्ताक्षर करने या व्यक्तिगत डेटा दर्ज करने से पहले सुनिश्चित करें कि आप वास्तव में इस साइट पर भरोसा करते हैं।", + "browsers": "ब्राउज़र्स", + "browsing-cancel": "रद्द करना", + "browsing-open-in-android-web-browser": "Android में खोलें", + "browsing-open-in-ios-web-browser": "iOS में खोलें", + "browsing-open-in-status": "Status में खोलें", + "browsing-site-blocked-description1": "हमें इस पते से संभावित दुर्भावनापूर्ण गतिविधि का पता चला है। आपकी और आपके वॉलेट की सुरक्षा के लिए, हम आगे नेविगेशन को रोक रहे हैं। \n\n अगर आपको लगता है कि यह एक त्रुटि है, तो हमें इसमें बताएं", + "browsing-site-blocked-description2": "सार्वजनिक चैट।", + "browsing-site-blocked-go-back": "वापस जाओ", + "browsing-site-blocked-title": "यह साइट अवरुद्ध है", + "browsing-title": "ब्राउज़", + "bug-report": "एक बग रिपोर्ट करो", + "bug-report-description": "* विवरण", + "bug-report-description-placeholder": "आवश्यक, खाली नहीं हो सकता", + "bug-report-steps": "प्रजनन के चरण", + "bug-report-steps-placeholder": "- ऐप खोलो\n - कुछ करो\n - और फिर कुछ और...", + "bug-report-submit-email": "लॉग संग्रह के साथ ईमेल द्वारा सबमिट करें", + "bug-report-submit-gh-issue": "लॉग के बिना GitHub समस्या सबमिट करें", + "bug-report-too-short-description": "विवरण बहुत छोटा है", + "build-yourself": "इन उपयोग की शर्तों के बिना ऐप का उपयोग करने के लिए, आप अपना स्वयं का संस्करण बना सकते हैं", + "buy-crypto": "क्रिप्टो खरीदें", + "buy-crypto-choose-a-service": "क्रिप्टो खरीदने के लिए आप जिस सेवा का उपयोग करना चाहते हैं उसे चुनें", + "buy-crypto-description": "अभी क्रिप्टो खरीदने के लिए एक डैप खोजें", + "buy-crypto-leaving": "आप Status छोड़ रहे हैं और अपनी खरीदारी पूरी करने के लिए किसी तीसरे पक्ष की वेबसाइट में प्रवेश कर रहे हैं", + "buy-crypto-title": "लगता है आपका बटुआ खाली है", + "camera-access-error": "आवश्यक कैमरा अनुमति देने के लिए, कृपया अपनी सिस्टम सेटिंग में जाएं और सुनिश्चित करें किStatus> कैमरा चयनित है।", + "can-not-add-yourself": "वह आप हैं, चैट शुरू करने के लिए किसी और को चुनें", + "can-send-messages": "आप नए संदेश भेज और प्राप्त कर सकते हैं", + "cancel": "रद्द करना", + "cancel-keycard-setup": "कीकार्ड सेटअप रद्द करें", + "cancelling": "रद्द कर रहा है", + "cannot-read-card": "कार्ड नहीं पढ़ सकते।\n कृपया इसे अपने फ़ोन के पीछे रखें", + "cannot-use-default-pin": "पासकोड 000000 की अनुमति नहीं है।\n कृपया किसी अन्य नंबर का उपयोग करें", + "cant-open-public-chat": "सार्वजनिक चैट नहीं खोल सकते", + "cant-report-bug": "बग की रिपोर्ट नहीं कर सकता", + "card-is-blank": "यह कार्ड खाली है", + "card-reseted": "कार्ड रीसेट कर दिया गया है", + "card-unpaired": "कार्ड को वर्तमान डिवाइस से अयुग्मित कर दिया गया है", + "category": "श्रेणी", + "category-title": "श्रेणी शीर्षक", + "change-fleet": "बेड़े को {{fleet}} में बदलें", + "change-logging-enabled": "क्या आप वाकई {{enable}} लॉगिंग करना चाहते हैं?", + "change-pairing": "पेयरिंग कोड बदलें", + "change-pairing-description": "पेयरिंग कोड बदलने से वर्तमान पेयरिंग प्रभावित नहीं होती है। हालांकि, किसी भी नई जोड़ी के लिए नए कोड की आवश्यकता होगी।", + "change-pairing-title": "नया पेयरिंग कोड बनाएं", + "change-passcode": "पासकोड बदलें", + "change-password": "पासवर्ड बदलें", + "change-pin": "6 अंकों का पासकोड बदलें", + "change-puk": "12 अंकों का PUK बदलें", + "change-tip": "टिप बदलें", + "changed-amount-warning": "राशि को {{old}} से {{new}} में बदल दिया गया था", + "changed-asset-warning": "एसेट को {{old}} से बदलकर {{new}} कर दिया गया था", + "chaos-mode": "अराजक अंदाज़", + "chaos-unicorn-day": "अराजकता गेंडा दिवस", + "chaos-unicorn-day-details": "!", + "chat": "बात करना", + "chat-and-transact": "दोस्तों के साथ निजी तौर पर चैट करें और लेन-देन करें", + "chat-is-a-contact": "संपर्क करना", + "chat-is-not-a-contact": "संपर्क नहीं", + "chat-key": "चैट कुंजी", + "chat-key-content": "Status चैट प्रोटोकॉल पर संदेश एन्क्रिप्शन कुंजियों का उपयोग करके भेजे और प्राप्त किए जाते हैं। सार्वजनिक चैट कुंजी वर्णों की एक स्ट्रिंग है जिसे आप दूसरों के साथ साझा करते हैं ताकि वे आपको Status में संदेश भेज सकें।", + "chat-key-title": "चैट कुंजी", + "chat-link-previews": "चैट लिंक पूर्वावलोकन", + "chat-name": "चैट का नाम", + "chat-name-content": "तीन यादृच्छिक शब्द, आपकी चैट कुंजी से एल्गोरिथम के रूप में व्युत्पन्न और चैट में आपके डिफ़ॉल्ट उपनाम के रूप में उपयोग किए जाते हैं। चैट नाम पूरी तरह से अद्वितीय हैं; किसी अन्य उपयोगकर्ता के पास समान तीन शब्द नहीं हो सकते।", + "chat-name-title": "चैट नाम", + "chat-notification-preferences": "अधिसूचना सेटिंग", + "chat-settings": "चैट सेटिंग्स", + "chats": "चैट", + "check-on-block-explorer": "ब्लॉक एक्सप्लोरर पर चेक करें", + "check-on-opensea": "ओपनसी पर चेक करें", + "check-your-recovery-phrase": "अपने बीज वाक्यांश की जाँच करें", + "choose-actions": "क्रिया चुनें", + "choose-authentication-method": "एक प्रमाणीकरण विधि चुनें", + "choose-new-location-for-keystore": "अपनी कीस्टोर फ़ाइल को सहेजने के लिए एक नया स्थान चुनें", + "choose-storage": "भंडारण चुनें", + "clear": "साफ़", + "clear-all": "सभी साफ करें", + "clear-history": "इतिहास मिटा दें", + "clear-history-action": "साफ़", + "clear-history-confirmation": "इतिहास मिटा दें?", + "clear-history-confirmation-content": "क्या आप वाकई इस चैट इतिहास को मिटाना चाहते हैं?", + "clear-history-title": "इतिहास मिटा दें?", + "clear-history?": "इतिहास मिटा दें", + "cleared-chat-description-public": "यहां सन्नाटा पसरा हुआ है। बातचीत शुरू करें या", + "close": "बंद करे", + "close-all": "सब बंद करें", + "close-app-button": "पुष्टि करना", + "close-app-content": "ऐप बंद हो जाएगा और बंद हो जाएगा। जब आप इसे फिर से खोलते हैं, तो चयनित नेटवर्क का उपयोग किया जाएगा", + "close-app-title": "चेतावनी!", + "collectibles-leak-metadata": "आप यहां अपने एनएफटी प्रदर्शित कर सकते हैं। अगर आप ऐसा करते हैं, तो आप अपना वॉलेट और आईपी पता साझा करेंगे", + "command-button-send": "भेजना", + "communities": "समुदाय", + "communities-alpha": "समुदाय (अल्फ़ा)", + "communities-enabled": "समुदाय सक्षम", + "communities-verified": "✓ सत्यापित स्टेटस समुदाय", + "community": "समुदाय", + "community-channel": "सामुदायिक चैनल", + "community-channel-read-requirements-met": "केवल पूरी की गई आवश्यकताओं को देखें", + "community-channel-read-requirements-not-met": "केवल वे आवश्यकताएं देखें जो पूरी नहीं हुई हैं", + "community-channel-write-requirements-met": "देखें और पोस्ट की आवश्यकताएं पूरी हुईं", + "community-channel-write-requirements-not-met": "देखें और पोस्ट आवश्यकताएँ पूरी नहीं हुईं", + "community-color": "सामुदायिक रंग", + "community-color-placeholder": "एक रंग चुनें", + "community-edit-title": "समुदाय संपादित करें", + "community-emoji-thumbnail-title": "थंबनेल", + "community-enter-channel-info": "चैनल में प्रवेश करने से आपके सार्वजनिक पते नोड स्वामी के सामने प्रकट हो जाएंगे", + "community-image-delete": "Empty", + "community-image-pick": "एक छवि चुनें", + "community-image-remove": "हटाना", + "community-image-take": "एक तस्वीर लें", + "community-info": "समुदाय की जानकारी", + "community-info-not-found": "समुदाय की जानकारी नहीं मिली", + "community-invite-title": "आमंत्रित करना", + "community-join-requirements-changed": "टोकन आवश्यकताएं बदल गई हैं", + "community-join-requirements-met": "शामिल होने की आवश्यकताएं पूरी हुईं", + "community-join-requirements-not-met": "शामिल होने की आवश्यकताएं पूरी नहीं हुईं", + "community-join-requirements-tokens-lost": "अब आपके पास आवश्यक टोकन नहीं हैं", + "community-key": "सामुदायिक निजी कुंजी", + "community-key-placeholder": "अपनी समुदाय निजी कुंजी टाइप करें", + "community-link": "सामुदायिक लिंक", + "community-members": { + "one": "{{count}} सदस्य", + "other": "{{count}} सदस्यों" + }, + "community-members-title": "सदस्य", + "community-message-preview": "{{community-name}} में शामिल होने का आमंत्रण", + "community-notification-settings": "अधिसूचना सेटिंग", + "community-private-key": "सामुदायिक निजी कुंजी", + "community-requests-to-join-title": "सदस्यता अनुरोध", + "community-roles": "भूमिकाएँ", + "community-rules": "सामुदायिक नियम", + "community-share-title": "साझा करना", + "community-thumbnail-image": "थंबनेल छवि", + "community-thumbnail-upload": "डालना", + "complete-hardwallet-setup": "यह कार्ड अब लिंक हो गया है। लेन-देन पर हस्ताक्षर करने और अपनी चाबियों को अनलॉक करने के लिए आपको इसकी आवश्यकता है", + "completed": "पुरा होना।", + "confirm": "पुष्टि करना", + "confirm-new-password": "नए पासवर्ड की पुष्टि करें", + "confirm-pairing-code-placeholder": "अपने युग्मन कोड की पुष्टि करें...", + "confirm-password-placeholder": "अपने पासवर्ड की पुष्टि करें...", + "confirmation-request": "पुष्टिकरण के लिए अनुरोध", + "confirmations": "पुष्टिकरण", + "confirmations-helper-text": "जब लेन-देन में 12 पुष्टिकरण होते हैं, तो आप इसे व्यवस्थित मान सकते हैं।", + "connect": "जोड़ना", + "connect-mailserver-content": "{{name}} से कनेक्ट करें?", + "connect-wallet": "वॉलेट कनेक्ट करें", + "connected": "जुड़े हुए", + "connected-to": "से जुड़ा", + "connecting": "कनेक्ट हो रहा है...", + "connecting-requires-login": "दूसरे नेटवर्क से कनेक्ट करने के लिए लॉगिन की आवश्यकता है", + "connection-request": "कनेक्शन अनुरोध", + "connection-status": "संपर्क स्थिति", + "connection-with-the-card-lost": "कार्ड के साथ कनेक्शन\n खो गया है", + "connection-with-the-card-lost-setup-text": "सेटअप फिर से शुरू करने के लिए कार्ड को पकड़ें\n अपने फोन के पीछे और बनाए रखें\n कार्ड से फोन संपर्क", + "connection-with-the-card-lost-text": "आगे बढ़ने के लिए कार्ड को अपने फ़ोन के पीछे पकड़ें", + "contact-code": "चैट कुंजी", + "contact-request": "संपर्क अनुरोध", + "contact-request-accepted": "स्वीकृत ✓", + "contact-request-declined": "अस्वीकृत (x)", + "contact-request-header": "👋 संपर्क अनुरोध", + "contact-request-pending": "लंबित...", + "contact-request-sent": "संपर्क अनुरोध भेजा गया", + "contact-requests": "संपर्क अनुरोध", + "contact-s": { + "one": "contact", + "other": "contacts" + }, + "contacts": "संपर्क", + "contacts-descr": "आपके संपर्क यहां दिखाई देंगे। आप संपर्क के रूप में जोड़े गए किसी भी व्यक्ति से Status अपडेट प्राप्त करेंगे", + "contacts-empty": "ईएनएस नामों वाले संपर्क यहां दिखाई देंगे", + "continue": "जारी रखें", + "continue-anyway": "फिर भी जारी रखें", + "contract-address": "अनुबंध का पता", + "contract-interaction": "अनुबंध बातचीत", + "contract-isnt-supported": "अनुबंध समर्थित नहीं है", + "copy-info": "जानकारी कॉपी करें", + "copy-qr": "कॉपी कोड", + "copy-text": "पाठ कॉपी करें", + "copy-to-clipboard": "प्रतिलिपि", + "copy-transaction-hash": "लेन-देन आईडी कॉपी करें", + "cost-fee": "लागत\/शुल्क", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "सृजन करना", + "create-a-pin": "6 अंकों का पासकोड बनाएं", + "create-a-puk": "12 अंकों का PUK बनाएं", + "create-category": "श्रेणी बनाएं", + "create-channel": "एक चैनल बनाएं", + "create-channel-title": "नया चैनल", + "create-community": "एक समुदाय बनाएं", + "create-group-chat": "ग्रुप चैट बनाएं", + "create-multiaccount": "चाबियाँ उत्पन्न करें", + "create-new-key": "नई चाबियां प्राप्त करें", + "create-pin": "6 अंकों का पासकोड बनाएं", + "create-pin-description": "Status को अनलॉक करने और लेन-देन की पुष्टि करने के लिए आपको अपने कार्ड + इस 6-अंकीय पासकोड की आवश्यकता होगी", + "created-group-chat-description": "आपने {{group-name}} बनाया है", + "cryptokitty-name": "क्रिप्टोकिट्टी # {{id}}", + "currency": "मुद्रा", + "currency-display-name-aed": "अमीराती दिरहाम", + "currency-display-name-afn": "अफ़ग़ानिस्तान", + "currency-display-name-ars": "अर्जेंटीना पेसो", + "currency-display-name-aud": "ऑस्ट्रलियन डॉलर", + "currency-display-name-bbd": "बारबाडोस डॉलर", + "currency-display-name-bdt": "बांग्लादेशी टका", + "currency-display-name-bgn": "बल्गेरियाई लेवी", + "currency-display-name-bhd": "बहरीन दिनारी", + "currency-display-name-bnd": "ब्रुनेई दारुस्सलाम डॉलर", + "currency-display-name-bob": "बोलीविया बोलिवियानो", + "currency-display-name-brl": "ब्राजील रियल", + "currency-display-name-btn": "भूटानी न्गुलट्रूम", + "currency-display-name-cad": "कनाडा डॉलर", + "currency-display-name-chf": "स्विट्ज़रलैंड फ़्रैंक", + "currency-display-name-clp": "चिली पेसो", + "currency-display-name-cny": "चीन युआन रॅन्मिन्बी", + "currency-display-name-cop": "कोलंबिया पेसो", + "currency-display-name-crc": "कोस्टा रिका कोलोन", + "currency-display-name-czk": "चेक कोरुना", + "currency-display-name-dkk": "डेनमार्क क्रोन", + "currency-display-name-dop": "डोमिनिकन गणराज्य पेसो", + "currency-display-name-egp": "मिस्र पाउंड", + "currency-display-name-etb": "इथियोपियन बिररू", + "currency-display-name-eur": "यूरो", + "currency-display-name-gbp": "ब्रिटिश पाउंड", + "currency-display-name-gel": "जॉर्जियाई लारिक", + "currency-display-name-ghs": "घाना सेडि", + "currency-display-name-hkd": "हांगकांग का डॉलर", + "currency-display-name-hrk": "क्रोएशिया कुनाक", + "currency-display-name-huf": "हंगरी फ़ोरिंटो", + "currency-display-name-idr": "इंडोनेशिया रुपिया", + "currency-display-name-ils": "इज़राइल शेकेल", + "currency-display-name-inr": "भारतीय रुपया", + "currency-display-name-isk": "आइसलैंड क्रोना", + "currency-display-name-jmd": "जमैका डॉलर", + "currency-display-name-jpy": "जापानी येन", + "currency-display-name-kes": "केन्याई शिलिंग", + "currency-display-name-krw": "कोरिया (दक्षिण) वोन", + "currency-display-name-kwd": "कुवैती दिनारी", + "currency-display-name-kzt": "कज़ाखस्तान टेंगे", + "currency-display-name-lkr": "श्रीलंका रुपया", + "currency-display-name-mad": "मोरक्कन दिरहम", + "currency-display-name-mdl": "मोल्दोवन ल्यू", + "currency-display-name-mur": "मॉरीशस रुपया", + "currency-display-name-mwk": "मलावी क्वाचा", + "currency-display-name-mxn": "मेक्सिको पेसो", + "currency-display-name-myr": "मलेशिया रिंगित", + "currency-display-name-mzn": "मोज़ाम्बिक मेटिकल", + "currency-display-name-nad": "नामीबिया डॉलर", + "currency-display-name-ngn": "नाइजीरिया नायरा", + "currency-display-name-nok": "नॉर्वे क्रोन", + "currency-display-name-npr": "नेपाल रुपया", + "currency-display-name-nzd": "न्यूज़ीलैंड डॉलर", + "currency-display-name-omr": "ओमान रियाल", + "currency-display-name-pen": "पेरू सोलो", + "currency-display-name-pgk": "पापुआ न्यू गिनी किना", + "currency-display-name-php": "फिलीपींस पेसो", + "currency-display-name-pkr": "पाकिस्तानी रुपया", + "currency-display-name-pln": "पोलिश ज़्लॉटी", + "currency-display-name-pyg": "पराग्वे गुआरानी", + "currency-display-name-qar": "कतर रियाल", + "currency-display-name-ron": "रोमानिया लेउ", + "currency-display-name-rsd": "सर्बिया दिनारी", + "currency-display-name-rub": "रूस रूबल", + "currency-display-name-sar": "सऊदी अरब रियाल", + "currency-display-name-sek": "स्वीडन क्रोना", + "currency-display-name-sgd": "सिंगापुर का डॉलर", + "currency-display-name-thb": "थाईलैंड बहतो", + "currency-display-name-try": "तुर्की लीरास", + "currency-display-name-ttd": "त्रिनिदाद और टोबैगो डॉलर", + "currency-display-name-twd": "ताइवान नया डॉलर", + "currency-display-name-tzs": "तंजानिया शिलिंग", + "currency-display-name-uah": "यूक्रेन रिव्निया", + "currency-display-name-ugx": "युगांडा शिलिंग", + "currency-display-name-usd": "यूनाइटेड स्टेट का डॉलर", + "currency-display-name-uyu": "उरुग्वे पेसो", + "currency-display-name-vef": "वेनेज़ुएला बोलिवार", + "currency-display-name-vnd": "वियतनाम डोंग", + "currency-display-name-zar": "दक्षिण अफ्रीका रैंड", + "current": "वर्तमान", + "current-average": "वर्तमान औसत", + "current-average-tip": "वर्तमान औसत टिप", + "current-base": "वर्तमान आधार", + "current-base-fee": "वर्तमान आधार शुल्क", + "current-minimum-tip": "वर्तमान न्यूनतम टिप", + "current-network": "वर्तमान नेटवर्क", + "current-password": "वर्तमान पासवर्ड", + "current-pin": "6 अंकों का पासकोड दर्ज करें", + "current-pin-description": "आगे बढ़ने के लिए अपना 6 अंकों का पासकोड दर्ज करें", + "custom": "रिवाज़", + "custom-networks": "कस्टम नेटवर्क", + "custom-node": "आप कस्टम RPC समापन बिंदु का उपयोग कर रहे हैं। आपका स्थानीय स्थानान्तरण इतिहास अधूरा हो सकता है।", + "custom-seed-phrase": "अमान्य बीज वाक्यांश", + "custom-seed-phrase-text-1": "यह बीज वाक्यांश हमारे समर्थित शब्दकोश से मेल नहीं खाता। गलत वर्तनी वाले शब्दों की जाँच करें।", + "dapp": "ऐप", + "dapp-starter-pack-accept": "स्वीकार करें और खोलें", + "dapp-starter-pack-description": "शुरू करने के लिए यहां कुछ क्रिप्टो है! स्टिकर, एक ENS नाम प्राप्त करने के लिए इसका उपयोग करें और dapps आज़माएं", + "dapp-starter-pack-title": "स्टार्टर पैक", + "dapp-would-like-to-connect-wallet": "से जुड़ना चाहते हैं", + "dapps": "ऐप्स", + "dapps-permissions": "डीएपी अनुमतियाँ", + "dark": "अंधेरा", + "data": "डेटा", + "data-collected": "आकड़ों को एकत्र किया", + "data-collected-subtitle": "नीचे दी गई तालिका सटीक डेटा दिखाती है जो संग्रहीत और भेजा जाएगा। कोई संवेदनशील डेटा नहीं भेजा जाता है यह सुनिश्चित करने के लिए डेटा को सार्वजनिक नियमों के विरुद्ध मान्य किया जाता है। विश्वास मत करो, सत्यापित करो।", + "data-syncing": "डेटा सिंकिंग", + "database-reset-content": "चैट, संपर्क और सेटिंग्स हटा दी गई हैं। आप अपने खाते का उपयोग अपने कीकार्ड के साथ कर सकते हैं", + "database-reset-title": "डेटाबेस रीसेट", + "database-reset-warning": "डेटाबेस रीसेट हो जाएगा। चैट, संपर्क और सेटिंग हटा दी जाएंगी", + "datetime-ago": "पहले", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "day", + "other": "days" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "hour", + "other": "hours" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "minute", + "other": "minutes" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "second", + "other": "seconds" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "आज", + "datetime-yesterday": "कल", + "decimals": "दशमलव", + "decline": "पतन", + "declined": "इंकार कर दिया", + "decryption-failed-content": "आपका डेटा डिक्रिप्ट करने में एक त्रुटि हुई। आपको अपने पुराने डेटा को मिटाने और एक नया खाता बनाने की आवश्यकता हो सकती है। मिटाने के लिए \"लागू करें\" पर टैप करें या फिर से कोशिश करने के लिए \"रद्द करें\" पर टैप करें", + "default": "चूक", + "default-assets": "डिफ़ॉल्ट ERC20 और ERC721", + "default-sync-period": "के लिए सिंक इतिहास", + "delete": "मिटाना", + "delete-account": "खाता हटा दो", + "delete-and-leave-group": "समूह हटाएं और छोड़ें", + "delete-bootnode": "बूटनोड हटाएं", + "delete-bootnode-are-you-sure": "क्या आप वाकई इस बूटनोड को हटाना चाहते हैं?", + "delete-bootnode-title": "बूटनोड हटाएं", + "delete-category-confirmation": "क्या आप वाकई इस श्रेणी को हटाना चाहते हैं?", + "close-chat": "चैट हटाएं", + "close-chat-confirmation": "क्या आप वाकई इस चैट को हटाना चाहते हैं?", + "delete-confirmation": "मिटाना?", + "delete-for-everyone": "सभी के लिए हटाएं", + "delete-for-me": "मेरे लिए हटाएं", + "delete-keys-keycard": "Keycard से कुंजियाँ हटाएं", + "delete-mailserver": "Status नोड हटाएं", + "delete-mailserver-are-you-sure": "क्या आप वाकई इस Status नोड को हटाना चाहते हैं?", + "delete-mailserver-title": "Status नोड हटाएं", + "delete-message": "संदेश को हटाएं", + "delete-my-account": "मेरा एकाउंट हटा दो", + "delete-my-profile": "मेरी प्रोफ़ाइल हटाएं", + "delete-network-confirmation": "क्या आप वाकई इस नेटवर्क को हटाना चाहते हैं?", + "delete-network-error": "कृपया इसे हटाने से पहले किसी दूसरे नेटवर्क से कनेक्ट करें", + "delete-network-title": "नेटवर्क मिटाएं?", + "delete-node": "नोड हटाएँ", + "delete-node-are-you-sure": "क्या आप वाकई इस श्रेणी को हटाना चाहते हैं?", + "delete-node-title": "नोड हटाएँ", + "delete-profile": "प्रोफ़ाइल हटाएं", + "delete-profile-warning": "चेतावनी: यदि आपने अपना बीज वाक्यांश नहीं लिखा है, तो आप अपनी प्रोफ़ाइल हटाने के बाद अपने धन तक पहुंच खो देंगे", + "deny": "मना करना", + "derivation-path": "व्युत्पत्ति पथ", + "describe-channel": "चैनल का वर्णन करें", + "description": "विवरण", + "dev-mode": "विकास मोड", + "dev-mode-settings": "विकास मोड सेटिंग्स", + "device-syncing": "डिवाइस सिंकिंग", + "devices": "उपकरण", + "disable": "अक्षम करना", + "disable-all": "सबको सक्षम कर दो", + "disable-later-in-settings": "आप इसे बाद में सेटिंग में अक्षम कर सकते हैं", + "disabled": "अक्षम", + "disconnect": "डिस्कनेक्ट", + "disconnected": "ऑफ़लाइन चैट करें", + "discover": "खोज करना", + "discover-communities": "समुदायों की खोज करें", + "dismiss": "नकार देना", + "display-collectibles": "संग्रहणीय प्रदर्शित करें", + "done": "पूर्ण", + "dont-ask": "मुझसे दोबारा मत पूछो", + "edit": "संपादन करना", + "edit-categories": "श्रेणियाँ संपादित करें", + "edit-channel-title": "चैनल संपादित करें", + "edit-chats": "चैट संपादित करें", + "edit-community": "समुदाय संपादित करें", + "edit-favourite": "पसंदीदा संपादित करें", + "edit-group": "समूह संपादित करें", + "edit-message": "संदेश संपादित करें", + "edit-name-and-image": "नाम और छवि संपादित करें", + "edit-name-image": "नाम और छवि संपादित करें", + "edit-profile": "प्रोफ़ाइल संपादित करें", + "edited": "संपादित", + "editing-message": "संदेश संपादित करना", + "empty-activity-center": "आपकी चैट सूचनाएं\n यहाँ दिखाई देगा", + "empty-chat-description": "कोई संदेश नहीं है\n इस चैट में अभी तक", + "empty-chat-description-community": "यहाँ पिछले {{quiet-hours}} ।", + "empty-chat-description-one-to-one": "आपके द्वारा यहां भेजे गए कोई भी संदेश एन्क्रिप्ट किए गए हैं और केवल आपके द्वारा पढ़े जा सकते हैं और", + "empty-chat-description-public": "यहाँ पिछले {{quiet-hours}} । बातचीत शुरू करें या", + "empty-chat-description-public-share-this": "इस चैट को साझा करें।", + "empty-keycard-required": "एक खाली कीकार्ड की आवश्यकता है", + "empty-pending-invitations-descr": "जो लोग समूह में शामिल होना चाहते हैं\n एक आमंत्रण लिंक के माध्यम से यहां दिखाई देगा", + "empty-tab": "खाली टैब", + "enable": "सक्षम", + "enable-all": "सभी को सक्षम करें", + "enable-link-previews": "चैट में लिंक पूर्वावलोकन सक्षम करें?", + "encrypt-with-password": "पासवर्ड से एन्क्रिप्ट करें", + "ens-10-SNT": "10 एसएनटी", + "ens-add-username": "उपयोगकर्ता नाम जोड़ें", + "ens-agree-to": "सहमत होना", + "ens-chat-settings": "चैट सेटिंग्स", + "ens-custom-domain": "कस्टम डोमेन", + "ens-custom-username-hints": "कस्टम डोमेन जैसे username.domain.eth . सहित संपूर्ण उपयोगकर्ता नाम टाइप करें", + "ens-custom-username-taken": "उपयोगकर्ता नाम आपका नहीं है :(", + "ens-deposit": "जमा करना", + "ens-dismiss-message": "खारिज करने के लिए यहां क्लिक करें", + "ens-displayed-with": "आपके संदेश दूसरों को प्रदर्शित होते हैं", + "ens-get-name": "एक सार्वभौमिक उपयोगकर्ता नाम प्राप्त करें", + "ens-got-it": "ठीक मिल गया", + "ens-locked": "उपयोगकर्ता नाम लॉक किया गया। आप इसे {{date}} तक रिलीज़ नहीं कर पाएंगे", + "ens-name-content": "आपकी चैट कुंजी के लिए कस्टम उपनाम जिसे आप इथेरियम नाम सेवा का उपयोग करके पंजीकृत कर सकते हैं। ENS नाम विकेंद्रीकृत उपयोगकर्ता नाम हैं।", + "ens-name-not-found": "ईएनएस नाम हल नहीं कर सकता", + "ens-name-title": "ईएनएस नाम", + "ens-network-restriction": "केवल मेननेट पर उपलब्ध है", + "ens-no-usernames": "आपका कोई उपयोगकर्ता नाम जुड़ा नहीं है", + "ens-powered-by": "एथेरियम नाम सेवाओं द्वारा संचालित", + "ens-primary-username": "प्राथमिक उपयोगकर्ता नाम", + "ens-register": "पंजीकरण करवाना", + "ens-registration-failed": "उपयोगकर्ता नाम पंजीकृत करने के लिए, कृपया पुन: प्रयास करें।", + "ens-registration-failed-title": "लेन - देन विफल", + "ens-registration-failure": "पंजीकरण विफल", + "ens-registration-in-progress": "पंजीकरण जारी है...", + "ens-release-username": "उपयोगकर्ता नाम जारी करें", + "ens-remove-hints": "हटाने से उपयोगकर्ता नाम आपकी कुंजी से अलग हो जाएगा.", + "ens-remove-username": "उपयोगकर्ता नाम हटाएं", + "ens-saved": "अब आपकी चैट कुंजी से कनेक्ट हो गया है और Status में उपयोग किया जा सकता है।", + "ens-saved-title": "उपयोगकर्ता नाम जोड़ा गया", + "ens-show-username": "चैट में मेरा ईएनएस उपयोगकर्ता नाम दिखाएं", + "ens-terms-header": "नाम पंजीकरण की शर्तें", + "ens-terms-point-1": "फंड 1 साल के लिए जमा किया जाता है। आपका SNT लॉक हो जाएगा, लेकिन खर्च नहीं होगा।", + "ens-terms-point-10": "0x000000000C2E074eC69A0dFb2997BA6C7d2e1e (ईएनएस रजिस्ट्री)।", + "ens-terms-point-2": "1 वर्ष के बाद, आप नाम जारी कर सकते हैं और अपनी जमा राशि वापस प्राप्त कर सकते हैं, या नाम रखने के लिए कोई कार्रवाई नहीं कर सकते हैं।", + "ens-terms-point-3": "यदि अनुबंध की शर्तें बदल जाती हैं - उदाहरण के लिए Status अनुबंध को अपग्रेड करती है - उपयोगकर्ता को उपयोगकर्ता नाम जारी करने का अधिकार है, भले ही समय की परवाह किए बिना।", + "ens-terms-point-4": "अनुबंध नियंत्रक आपके जमा किए गए धन तक नहीं पहुंच सकता है। उन्हें केवल उस पते पर वापस ले जाया जा सकता है जिसने उन्हें भेजा था।", + "ens-terms-point-5": "आपका पता (पते) सार्वजनिक रूप से आपके ईएनएस नाम से जुड़ा होगा।", + "ens-terms-point-6": "उपयोगकर्ता नाम Stateofus.eth के उपडोमेन नोड्स के रूप में बनाए जाते हैं और ENS स्मार्ट अनुबंध शर्तों के अधीन होते हैं।", + "ens-terms-point-7": "आप अपनी ओर से एसएनटी को स्थानांतरित करने के लिए अनुबंध को अधिकृत करते हैं। यह तभी हो सकता है जब आप हस्तांतरण को अधिकृत करने के लिए किसी लेन-देन को स्वीकृति देते हैं।", + "ens-terms-point-8": "पते पर स्मार्ट अनुबंध तर्क द्वारा इन शर्तों की गारंटी दी जाती है:", + "ens-terms-point-9": "{{address}} (Status उपयोगकर्ता नाम रजिस्ट्रार)", + "ens-terms-registration": "नाम पंजीकरण की शर्तें।", + "ens-test-message": "अरे", + "ens-transaction-pending": "लेन-देन लंबित...", + "ens-understand": "मैं समझता\/समझती हूं कि मेरे वॉलेट का पता सार्वजनिक रूप से मेरे उपयोगकर्ता नाम से जुड़ा होगा।", + "ens-username": "ईएनएस उपयोगकर्ता नाम", + "ens-username-already-added": "उपयोगकर्ता नाम पहले से ही आपकी चैट कुंजी से जुड़ा हुआ है और Status के अंदर उपयोग किया जा सकता है।", + "ens-username-available": "उपयोगकर्ता नाम उपलब्ध है!", + "ens-username-connected": "यह उपयोगकर्ता नाम आपके स्वामित्व में है और आपकी चैट कुंजी से जुड़ा है।", + "ens-username-connected-continue": "'चैट में मेरा ईएनएस उपयोगकर्ता नाम दिखाएं' सेट करना जारी रखें।", + "ens-username-connected-with-different-key": "जारी रखने के लिए उपयोगकर्ता नाम को आपकी वर्तमान चैट कुंजी से जोड़ने के लिए लेन-देन की आवश्यकता होगी।", + "ens-username-connection-confirmation": "लेन-देन पूरा होने के बाद {{username}} कनेक्ट हो जाएगा।", + "ens-username-hints": "कम से कम 4 अक्षर। केवल लैटिन अक्षर, संख्याएं और लोअरकेस।", + "ens-username-invalid": "सिर्फ अक्षर और अंक।", + "ens-username-invalid-name-warning": "आपके एनएनएस नामों में से एक की पंजीकरण प्रक्रिया अमान्य स्थिति में समाप्त हो गई है। वॉलेट लेनदेन के लिए नाम का उपयोग न करें और support@status.im . पर हमारे समर्थन तक पहुंचें", + "ens-username-owned": "उपयोगकर्ता नाम आपके स्वामित्व में है।", + "ens-username-owned-continue": "जारी रखने से यह उपयोगकर्ता नाम आपकी चैट कुंजी से कनेक्ट हो जाएगा।", + "ens-username-registration-confirmation": "अच्छा! लेन-देन पूरा होने के बाद आप {{username}} के स्वामी हैं।", + "ens-username-registration-invalid": "चेतावनी! अमान्य स्थिति में पंजीकरण प्रक्रिया समाप्त हो गई है। वॉलेट लेनदेन के लिए नाम का उपयोग न करें और support@status.im . पर हमारे समर्थन तक पहुंचें", + "ens-username-taken": "उपयोगकर्ता नाम पहले ही लिया जा चुका है :(", + "ens-username-you-can-follow-progress": "आप अपने वॉलेट के लेन-देन इतिहास अनुभाग में प्रगति का अनुसरण कर सकते हैं।", + "ens-usernames": "ईएनएस उपयोगकर्ता नाम", + "ens-usernames-details": "अन्य उपयोगकर्ताओं द्वारा आसानी से पहचाने जाने के लिए एक सार्वभौमिक उपयोगकर्ता नाम पंजीकृत करें", + "ens-want-custom-domain": "मेरे पास दूसरे डोमेन पर एक नाम है", + "ens-want-domain": "मुझे एक Stateofus.eth डोमेन चाहिए", + "ens-welcome-hints": "ENS नाम उन पागल-लंबे पतों को अद्वितीय उपयोगकर्ता नामों में बदल देते हैं।", + "ens-welcome-point-customize": "एक ईएनएस नाम चैट में आपके यादृच्छिक 3-शब्द नाम को बदल सकता है। {{name}} के बजाय @yourname बनें।", + "ens-welcome-point-customize-title": "अपना चैट नाम अनुकूलित करें", + "ens-welcome-point-receive": "अन्य लोग आपको एक आसान चरण में चैट के माध्यम से धनराशि भेज सकते हैं।", + "ens-welcome-point-receive-title": "चैट में लेनदेन प्राप्त करें", + "ens-welcome-point-register": "नाम हमेशा कायम रखने के लिए एक बार रजिस्टर करें। 1 वर्ष के बाद आप नाम जारी कर सकते हैं और अपना एसएनटी वापस प्राप्त कर सकते हैं।", + "ens-welcome-point-register-title": "10 एसएनटी रजिस्टर करने के लिए", + "ens-welcome-point-simplify": "आप अपने हेक्साडेसिमल हैश (0x...) के बजाय अपने साझा करने में आसान ईएनएस नाम पर धन प्राप्त कर सकते हैं।", + "ens-welcome-point-simplify-title": "अपने ईटीएच पते को सरल बनाएं", + "ens-welcome-point-verify": "आप अगले चरणों में अपने स्वामित्व वाले किसी भी उपयोगकर्ता नाम को सत्यापित और जोड़ सकते हैं।", + "ens-welcome-point-verify-title": "पहले से ही एक उपयोगकर्ता नाम के मालिक हैं?", + "ens-your-username": "तुम्हारा प्रयोगकर्ती नाम", + "ens-your-usernames": "आपके उपयोगकर्ता नाम", + "ens-your-your-name": "आपका ईएनएस नाम", + "enter-12-words": "अपने बीज वाक्यांश के 12 शब्द दर्ज करें, एकल रिक्त स्थान से अलग करें", + "enter-a-private-key": "एक निजी कुंजी दर्ज करें", + "enter-a-seed-phrase": "बीज वाक्यांश दर्ज करें", + "enter-address": "पता दर्ज करें", + "enter-channel": "चैनल दर्ज करें", + "enter-contact-code": "ENS (vitalik94) या चैट कुंजी (0x04…)", + "enter-pair-code": "अपना पेयरिंग कोड दर्ज करें", + "enter-pair-code-description": "पेयरिंग कोड पहले से युग्मित Status क्लाइंट से सेट किया जा सकता है", + "enter-password": "पास वर्ड दर्ज करें", + "enter-password-migration-prompt": "संपर्क, चैट और सेटिंग को अपनी कुंजियों के साथ स्थानांतरित करने के लिए अपना पासवर्ड दर्ज करें", + "enter-pin": "6 अंकों का पासकोड दर्ज करें", + "enter-puk-code": "PUK कोड दर्ज करें", + "enter-puk-code-description": "6 अंकों का पासकोड ब्लॉक कर दिया गया है।\n पासकोड को अनवरोधित करने के लिए कृपया PUK कोड दर्ज करें।", + "enter-recipient-address-or-username": "प्राप्तकर्ता का पता या उपयोगकर्ता नाम दर्ज करें", + "enter-seed-phrase": "बीज वाक्यांश दर्ज करें", + "enter-url": "यू आर एल दर्ज करो", + "enter-user-pk": "उपयोगकर्ता सार्वजनिक कुंजी दर्ज करें", + "enter-watch-account-address": "एक क्यूआर कोड स्कैन करें\n या\n देखने के लिए पता दर्ज करें", + "enter-word": "शब्द दर्ज करें", + "enter-your-code": "अपना 6 अंकों का पासकोड दर्ज करें", + "enter-your-password": "अपना पासवर्ड डालें", + "error": "त्रुटि", + "error-unable-to-get-balance": "संतुलन प्राप्त करने में असमर्थ", + "error-unable-to-get-prices": "मुद्रा रूपांतरण त्रुटि। पुन: प्रयास करने के लिए अपनी स्क्रीन रीफ़्रेश करें।", + "error-unable-to-get-token-balance": "टोकन बैलेंस प्राप्त करने में असमर्थ", + "errors": "त्रुटियाँ", + "eth": "ईटीएच", + "ethereum-address": "एथेरियम पता", + "ethereum-node-started-incorrectly-description": "एथेरियम नोड को गलत कॉन्फ़िगरेशन के साथ शुरू किया गया था, उस स्थिति से उबरने के लिए एप्लिकेशन को रोक दिया जाएगा। कॉन्फ़िगर किया गया नेटवर्क आईडी = {{network-id}} , वास्तविक = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "एथेरियम नोड गलत तरीके से शुरू हुआ", + "etherscan-lookup": "इथरस्कैन पर देखें", + "everyone": "हर कोई", + "expand-all": "सभी का विस्तार करें", + "export-account": "निर्यात खाता", + "export-key": "निजी कुंजी निर्यात करें", + "external-link": "बाहरी लिंक", + "external-storage-denied": "बाहरी संग्रहण तक पहुंच अस्वीकार कर दी गई है", + "failed": "अनुत्तीर्ण होना", + "faq": "अक्सर पूछे जाने वाले प्रश्नों", + "fast": "तेज", + "favourite": "पसंदीदा", + "favourite-description": "आपकी पसंदीदा वेबसाइट यहां दिखाई देंगी", + "favourites": "पसंदीदा", + "favourites-empty": "पसंदीदा में जोड़े गए पते यहां दिखाई देंगे", + "featured": "विशेष रुप से प्रदर्शित", + "fee-cap": "शुल्क सीमा", + "fee-explanation": "लेन-देन के लिए अधिकतम समग्र मूल्य। यदि ब्लॉक आधार शुल्क इससे अधिक है, तो इसे निम्न आधार शुल्क के साथ निम्नलिखित ब्लॉक में शामिल किया जाएगा।", + "fee-options": "सुझाए गए शुल्क विकल्प☺", + "fetch-community": "समुदाय लायें", + "fetch-messages": "संदेश प्राप्त करें", + "fetch-timeline": "लायें", + "fetching-community": "समुदाय लाया जा रहा है...", + "find": "खोज", + "finish": "खत्म करना", + "finishing-card-setup": "फिनिशिंग कार्ड सेटअप", + "fleet": "बेड़ा", + "fleet-settings": "बेड़े सेटिंग्स", + "follow": "अनुसरण", + "follow-your-interests": "सार्वजनिक चैट में शामिल हों और नए लोगों से मिलें", + "free": "मुक्त", + "from": "से", + "gas-amount-limit": "गैस राशि सीमा", + "gas-limit": "गैस सीमा", + "gas-price": "गैस की कीमत", + "gas-used": "इस्तेमाल की जाने वाली गैस", + "gated": "गेटेड", + "generate-a-key": "चाबियाँ उत्पन्न करें", + "generate-a-new-account": "एक खाता उत्पन्न करें", + "generate-a-new-key": "एक नई कुंजी उत्पन्न करें", + "generate-account": "चाबियाँ उत्पन्न करें", + "generate-an-account": "एक खाता उत्पन्न करें", + "generate-new-key": "चाबियाँ उत्पन्न करें", + "generating-codes-for-pairing": "> कार्ड में उत्पाद सॉफ्टवेयर डाउनलोड करना\n > अनलॉकिंग और पेयरिंग कोड जनरेट करना", + "generating-keys": "कुंजियाँ बनाई जा रही हैं...", + "generating-mnemonic": "बीज वाक्यांश उत्पन्न करना", + "get-a-keycard": "एक कीकार्ड प्राप्त करें", + "get-started": "शुरू हो जाओ", + "get-status-at": "http:\/\/status.im . पर Status प्राप्त करें", + "get-stickers": "स्टिकर प्राप्त करें", + "gif": "जीआईएफ", + "give-a-short-description": "संक्षिप्त विवरण दें", + "give-a-short-description-community": "इसका संक्षिप्त विवरण दें", + "give-permissions-camera": "अनुमति दे\n कैमरा एक्सेस करने के लिए", + "glossary": "शब्दकोष", + "go-to-settings": "सेटिंग्स में जाओ...", + "got-it": "समझ गया", + "grant-face-id-permissions": "आवश्यक फेस आईडी अनुमति देने के लिए, कृपया अपने सिस्टम सेटिंग्स पर जाएं और सुनिश्चित करें कि Status > फेस आईडी चुना गया है", + "group-chat": "समूह बातचीत", + "group-chat-admin": "व्यवस्थापक", + "group-chat-admin-added": "** {{member}} ** को व्यवस्थापक बना दिया गया है", + "group-chat-all-contacts-invited": "आपके सभी संपर्क पहले से ही समूह में हैं", + "group-chat-created": "** {{member}} ** ने समूह बनाया ** {{name}} **", + "group-chat-decline-invitation": "आमंत्रण अस्वीकार करें", + "group-chat-member-added": "** {{member}} ** को आमंत्रित किया गया है", + "group-chat-member-joined": "** {{member}} ** समूह में शामिल हो गया है", + "group-chat-member-removed": "** {{member}} ** ने समूह छोड़ दिया", + "group-chat-members-count": "{{selected}} \/ {{max}} सदस्य", + "group-chat-name-changed": "** {{member}} ** ने समूह का नाम बदलकर ** {{name}} ** कर दिया", + "group-chat-no-contacts": "आपके पास अभी तक कोई संपर्क नहीं है।\n चैटिंग शुरू करने के लिए अपने दोस्तों को आमंत्रित करें", + "group-info": "समूह की जानकारी", + "group-invite": "समूह आमंत्रण", + "group-invite-link": "समूह आमंत्रण लिंक", + "group-membership-request": "समूह सदस्यता अनुरोध", + "groups": "समूहों", + "gwei": "ग्वि", + "has-permissions": "एक्सेस करने की अनुमति है", + "hash": "हैश", + "help": "मदद", + "help-capitalized": "मदद", + "help-center": "सहायता केंद्र", + "help-improve-status": "स्टेटस सुधारने में मदद करें", + "hide": "छुपाना", + "hide-content-when-switching-apps": "स्क्रीनशॉट को ब्लॉक करें", + "hide-content-when-switching-apps-ios": "पूर्वावलोकन छुपाएं", + "history": "इतिहास", + "history-nodes": "स्टेटस नोड्स", + "hold-card": "कार्ड को पीछे से पकड़ें\n आपके फोन का", + "home": "घर", + "hooks": "हुक्स", + "how-it-works": "यह काम किस प्रकार करता है", + "http-gateway-error": "ओह, अनुरोध विफल!", + "identifier": "पहचानकर्ता", + "identity-verification": "पहचान की जाँच", + "identity-verification-request": "पहचान की जाँच", + "image": "छवि", + "image-remove-current": "वर्तमान तस्वीर हटा दे", + "image-source-gallery": "गैलरी से चुनें", + "image-source-make-photo": "कब्ज़ा करना", + "image-source-title": "तस्वीर संपादित करें", + "import": "आयात", + "import-community": "एक समुदाय आयात करें", + "import-community-title": "एक समुदाय आयात करें", + "in-contacts": "संपर्कों में", + "include": "शामिल करना", + "incoming": "आने वाली", + "incoming-transaction": "आवक लेनदेन", + "incorrect-code": [ + "एसटीआर", + "क्षमा करें, कोड गलत था, कृपया इसे फिर से दर्ज करें" + ], + "increase-gas": "गैस बढ़ाएँ", + "initialization": "प्रारंभ", + "install": "स्थापित करें", + "insufficient-balance-to-cover-fee": "लेन-देन शुल्क को कवर करने के लिए पर्याप्त शेष राशि नहीं", + "intro-message1": "Status में आपका स्वागत है!\n अपना पासवर्ड सेट करने और आरंभ करने के लिए इस संदेश को टैप करें।", + "intro-privacy-policy-note1": "Status आपके व्यक्तिगत डेटा को एकत्र या लाभ नहीं करता है। जारी रखकर, आप इससे सहमत हैं", + "intro-privacy-policy-note2": "गोपनीयता नीति", + "intro-text": "Status विकेंद्रीकृत वेब के लिए आपका प्रवेश द्वार है", + "intro-text1": "पीयर-टू-पीयर, एन्क्रिप्टेड नेटवर्क पर चैट करें जहां संदेशों को सेंसर या हैक नहीं किया जा सकता है", + "intro-text2": "दुनिया में कहीं भी डिजिटल संपत्ति भेजें और प्राप्त करें—किसी बैंक खाते की आवश्यकता नहीं है", + "intro-text3": "ऐसे गेम, एक्सचेंज और सोशल नेटवर्क एक्सप्लोर करें जहां आप अकेले अपने डेटा के मालिक हैं", + "intro-title1": "वास्तव में निजी संचार", + "intro-title2": "सुरक्षित क्रिप्टो वॉलेट", + "intro-title3": "विकेंद्रीकृत ऐप्स", + "intro-wizard-text1": "चाबियों का एक सेट आपके खाते को नियंत्रित करता है। आपकी चाबियां आपके फ़ोन पर रहती हैं, इसलिए केवल आप ही उनका उपयोग कर सकते हैं", + "intro-wizard-text2": "एक कुंजी चैट के लिए है। यह एक पठनीय नाम के साथ आता है जिसे बदला नहीं जा सकता।", + "intro-wizard-text3": "यदि आपके पास एक कीकार्ड है, तो बेहतर सुरक्षा के लिए अपनी चाबियां वहां संगृहीत करें।", + "intro-wizard-text4": "अपनी कुंजियों को सुरक्षित और एन्क्रिप्ट करें", + "intro-wizard-text6": "Status आपको नए संदेशों के बारे में सूचित करेगी। आप अपनी अधिसूचना प्राथमिकताओं को बाद में सेटिंग में संपादित कर सकते हैं", + "intro-wizard-title-alt4": "एक पासवर्ड बनाएं", + "intro-wizard-title-alt5": "अपने पासवर्ड की पुष्टि करें", + "intro-wizard-title1": "अपनी चाबियां प्राप्त करें", + "intro-wizard-title2": "एक चैट नाम चुनें", + "intro-wizard-title3": "कुंजी भंडारण चुनें", + "intro-wizard-title4": "6 अंकों का पासकोड बनाएं", + "intro-wizard-title5": "पासकोड की पुष्टि करें", + "intro-wizard-title6": "सूचनाएं सक्षम करें", + "introduce-yourself": "एक संक्षिप्त संदेश के साथ अपना परिचय दें", + "invalid-address-qr-code": "स्कैन किए गए क्यूआर कोड में कोई मान्य पता नहीं है", + "invalid-format": "अवैध प्रारूप\n {{format}} होना चाहिए", + "invalid-key-confirm": "आवेदन करना", + "invalid-key-content": "फ़ाइल दूषित होने के कारण डेटाबेस को एन्क्रिप्ट नहीं किया जा सकता है। आपके फंड और चैट कुंजी सुरक्षित हैं। अन्य डेटा, जैसे आपकी चैट और संपर्क, को पुनर्स्थापित नहीं किया जा सकता है। “ {{erase-multiaccounts-data-button-text}} ” बटन, अन्य सभी डेटा को हटा देगा और आपको अपने फंड तक पहुंचने और संदेश भेजने की अनुमति देगा", + "invalid-number": "अमान्य संख्या", + "invalid-pairing-password": "अमान्य युग्मन पासवर्ड", + "invalid-public-chat-topic": "अमान्य सार्वजनिक चैट विषय", + "invalid-range": "अमान्य प्रारूप, {{min}} और {{max}} के बीच होना चाहिए", + "invalid-username-or-key": "अमान्य उपयोगकर्ता नाम या चैट कुंजी", + "invite": "आमंत्रित करें", + "invite-button": "आमंत्रित करना", + "invite-chat-accept": "स्वीकार करना", + "invite-chat-accept-join": "स्वीकार करें और जुड़ें", + "invite-chat-intro": "स्टेटस में शामिल होने के लिए आपको किसी मित्र द्वारा भेजा गया था। शुरू करने के लिए यहां कुछ क्रिप्टो है! ENS नाम पंजीकृत करने या स्टिकर पैक खरीदने के लिए इसका उपयोग करें", + "invite-chat-name": "मित्र रेफरल", + "invite-chat-pending": "लंबित", + "invite-chat-rule": "स्वीकार करने से आपके मित्र को क्रिप्टो रेफरल बोनस भी मिलेगा", + "invite-chat-starter-pack": "स्टार्टर पैक", + "invite-contacts": "संपर्क सूची से लोगों को आमंत्रित करें", + "invite-friends": "मित्रों को आमंत्रित करें", + "invite-friends-to-status": "दोस्तों को स्टेटस पर आमंत्रित करें", + "invite-instruction-fifth": "आप अपने रेफ़रल बोनस को कभी भी भुनाना चुन सकते हैं।", + "invite-instruction-first": "स्टेटस डाउनलोड करने और इसमें शामिल होने के लिए आप अपने मित्र को एक अनूठा आमंत्रण लिंक भेजते हैं", + "invite-instruction-fourth": "आप अपना रेफरल बोनस और अपने मित्र को स्टार्टर पैक प्राप्त करते हैं", + "invite-instruction-second": "आपका मित्र स्टेटस डाउनलोड करता है और एक खाता बनाता है (Android पर)", + "invite-instruction-third": "आपके मित्र के साथ एक चैट शुरू की जाती है, जहां वे आपके रेफ़रल की पुष्टि करते हैं", + "invite-people": "लोगों को आमंत्रित करो", + "invite-privacy-policy-public": "आपने रेफ़रल लिंक के माध्यम से Status स्थापित किया है। इस चैट में शामिल होकर आप अपने रेफ़रलकर्ता को श्रेय देते हैं और इससे सहमत होते हैं", + "invite-privacy-policy1": "स्वीकार करके आप रेफरल कार्यक्रम से सहमत होते हैं", + "invite-privacy-policy2": "नियम और शर्तें।", + "invite-public-chat-home": "रेफरल आमंत्रण", + "invite-public-chat-intro": "शुरू करने के लिए यहां कुछ क्रिप्टो है! ENS नाम पंजीकृत करने या स्टिकर पैक खरीदने के लिए इसका उपयोग करें", + "invite-receive-account": "अपना रेफ़रल बोनस प्राप्त करने के लिए खाता", + "invite-reward": "आपके द्वारा आमंत्रित प्रत्येक मित्र के लिए क्रिप्टो कमाएँ!", + "invite-reward-friend": "दोस्त:", + "invite-reward-friend-description": "आरंभ करने के लिए आपके मित्र को कुछ {{reward}} से युक्त एक स्टार्टर पैक प्राप्त होगा", + "invite-reward-friend-name": "स्टार्टर पैक", + "invite-reward-you": "आप:", + "invite-reward-you-description": "किसी मित्र को आमंत्रित करें और रेफ़रल बोनस के रूप में {{reward}} प्राप्त करें। स्टिकर, एक ENS नाम प्राप्त करने के लिए इसका उपयोग करें और dapps आज़माएं", + "invite-reward-you-name": "रेफरल बोनस", + "invite-select-account": "अपना रेफ़रल बोनस प्राप्त करने के लिए एक खाते का चयन करें", + "invite-warning": "यह प्रचार केवल Android डिवाइस के उपयोगकर्ताओं के लिए मान्य है, जो यूएस के निवासी नहीं हैं। मित्र को 7 दिनों के भीतर रेफ़रल की पुष्टि करनी होगी", + "invited": "आमंत्रित", + "italic": "तिरछा", + "join": "जोड़ना", + "join-a-community": "या एक समुदाय में शामिल हों", + "join-group-chat": "समूह में शामिल हों", + "join-group-chat-description": "{{username}} ने आपको समूह में शामिल होने के लिए आमंत्रित किया {{group-name}}", + "join-me": "स्टेटस पर मुझसे जुड़ें: {{url}}", + "join-new-private-chat": "एक नई निजी चैट शुरू करें", + "join-new-public-chat": "एक सार्वजनिक चैट में शामिल हों", + "join-open-community": "समुदाय में शामिल हों", + "joined": "में शामिल हो गए", + "joined-group-chat-description": "आप {{username}} के आमंत्रण से {{group-name}} में शामिल हुए हैं", + "key": "चाबी", + "key-managment": "मुख्य प्रबंधन", + "key-on-device": "इस डिवाइस पर निजी कुंजी सहेजी गई है", + "keycard": "कुंजी कार्ड", + "keycard-access-reset": "कीकार्ड पहुंच रीसेट है", + "keycard-applet-install-instructions": "एप्लेट स्थापित करने के लिए कृपया https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation पर दिए गए निर्देशों का पालन करें", + "keycard-awaiting-description": "अपने डिवाइस पर NFC रीडर खोजने के लिए कार्ड को इधर-उधर घुमाने का प्रयास करें", + "keycard-awaiting-title": "अभी भी देख रहा है...", + "keycard-backup": "एक बैकअप कीकार्ड बनाएं", + "keycard-backup-success-body": "बैकअप कार्ड सफलतापूर्वक बनाया गया। अब आप इसे अपने खाते के साथ प्राथमिक कार्ड की तरह ही उपयोग कर सकते हैं।", + "keycard-backup-success-title": "बैकअप सफल", + "keycard-blocked": "कीकार्ड ब्लॉक कर दिया गया है।\n कार्ड का उपयोग जारी रखने के लिए आपको उसे रीसेट करना होगा।", + "keycard-can-use-with-new-passcode": "आप इस कार्ड का उपयोग अपने नए पासकोड के साथ कर सकते हैं", + "keycard-cancel-setup-text": "यह कीकार्ड सेटअप रद्द कर देगा। कीकार्ड का उपयोग करने के लिए सेटअप को पूरा करने की अत्यधिक अनुशंसा की जाती है। क्या आप वाकई रद्द करना चाहते हैं?", + "keycard-cancel-setup-title": "खतरनाक ऑपरेशन", + "keycard-connected-description": "कार्ड को स्थिर रखने का प्रयास करें", + "keycard-connected-title": "जुड़े हुए", + "keycard-desc": "एक कीकार्ड के मालिक हैं? उस पर अपनी चाबियाँ संग्रहीत करें; आपको लेनदेन के लिए इसकी आवश्यकता होगी", + "keycard-dont-ask-card": "साइन इन करने के लिए कार्ड न मांगें", + "keycard-enter-new-passcode": "नया पासकोड दर्ज करें {{step}} \/2", + "keycard-error-description": "जारी रखने के लिए कार्ड को फिर से कनेक्ट करें", + "keycard-error-title": "संपर्क टूट गया", + "keycard-factory-reset": "फ़ैक्टरी सेटिंग पर कार्ड लौटाएं", + "keycard-factory-reset-text": "ऐसा करने से कार्ड पर संग्रहीत कोई भी स्मरक वाक्यांश हटा दिया जाएगा। सुनिश्चित करें कि आपके पास इस कीकार्ड के साथ उपयोग किए जा रहे स्मरक वाक्यांश का बैकअप है।", + "keycard-factory-reset-title": "क्या आप वाकई फ़ैक्टरी रीसेट करना चाहते हैं?", + "keycard-free-pairing-slots": "कीकार्ड में {{n}} फ्री पेयरिंग स्लॉट हैं", + "keycard-has-multiaccount-on-it": "यह कार्ड भरा हुआ है। प्रत्येक कार्ड में एक मुख्य की-जोड़ी हो सकती है", + "keycard-init-description": "जारी रखने के लिए कार्ड को अपने फ़ोन के पीछे रखें", + "keycard-init-title": "कार्ड ढूंढ रहे हैं...", + "keycard-is-blocked-details": "अब आप इस कार्ड का उपयोग इस खाते तक पहुंचने या उस पर हस्ताक्षर करने के लिए नहीं कर सकते हैं। बहुत सारे असफल पासकोड और PUK प्रयास हुए हैं।", + "keycard-is-blocked-instructions": "अपने खाते तक पहुंचने के लिए आपको अपना कार्ड फ़ैक्टरी रीसेट करना होगा। प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन पर टैप करें, आपको अपने स्मरक की आवश्यकता होगी।", + "keycard-is-blocked-title": "कीकार्ड अवरुद्ध है", + "keycard-is-frozen-details": "आपकी संपत्ति की सुरक्षा के लिए, आपका कार्ड फ्रीज कर दिया गया है। इसे अनफ्रीज करने के लिए अपना कार्ड रीसेट करें और लेनदेन भेजने में सक्षम हों। आप इसे अपने PUK या अपने स्मरक के साथ कर सकते हैं।", + "keycard-is-frozen-factory-reset": "स्मरक के साथ रीसेट करें", + "keycard-is-frozen-reset": "PUK . के साथ रीसेट करें", + "keycard-is-frozen-title": "कीकार्ड फ़्रीज़ हो गया है", + "keycard-onboarding-finishing-header": "पूरी तरह खत्म करना", + "keycard-onboarding-intro-header": "कीकार्ड पर अपनी चाबियां संगृहीत करें", + "keycard-onboarding-intro-text": "तैयार हो जाइए, इसमें कुछ मिनट लग सकते हैं, लेकिन आपके खाते को सुरक्षित करना महत्वपूर्ण है", + "keycard-onboarding-mnemonic-text": "अपने बीज वाक्यांश को लिखने के लिए आपको एक कागज के टुकड़े और एक पेंसिल की भी आवश्यकता होगी।", + "keycard-onboarding-pairing-header": "कार्ड जोड़ा जा रहा है...", + "keycard-onboarding-pin-text": "आपको एक 6-अंकीय पासकोड बनाना होगा जिसका उपयोग आपके कीकार्ड तक पहुंच की सुरक्षा के लिए किया जाएगा।", + "keycard-onboarding-preparing-header": "कार्ड तैयार किया जा रहा है...", + "keycard-onboarding-puk-code-header": "कोड नीचे लिखें\n और उन्हें सुरक्षित रूप से स्टोर करें", + "keycard-onboarding-recovery-phrase-description": "अपनी कुंजी वापस पाने के लिए आपको इस बीज वाक्यांश की आवश्यकता है। नीचे लिखें। इसे सुरक्षित, ऑफ़लाइन और इस डिवाइस से अलग रखें.", + "keycard-onboarding-recovery-phrase-header": "बैक अप बीज वाक्यांश", + "keycard-onboarding-recovery-phrase-text": "केवल तुम्हारी आँखों के लिए। यह वह जादुई बीज है जिसका उपयोग आपकी चाबी बनाने के लिए किया जाता है।", + "keycard-onboarding-start-header": "कार्ड को पीछे से पकड़ें\n शुरू करने के लिए अपने फोन का", + "keycard-onboarding-start-step1": "पासकोड बनाएं", + "keycard-onboarding-start-step1-text": "लगभग 1 मिनट। अपनी कुंजियों को एन्क्रिप्ट करने के लिए 6 अंकों का पासकोड बनाएं", + "keycard-onboarding-start-step2": "PUK और पेयरिंग कोड लिखें", + "keycard-onboarding-start-step2-text": "लगभग 1 मिनट। इसके लिए आपको एक कागज के टुकड़े और एक पेंसिल की आवश्यकता होगी", + "keycard-onboarding-start-step3": "बीज वाक्यांश का बैकअप लें", + "keycard-onboarding-start-step3-text": "लगभग 1 मिनट। कागज का एक टुकड़ा और एक पेंसिल भी आवश्यक है", + "keycard-onboarding-start-text": "और कार्ड से फोन संपर्क बनाए रखें\n सेटअप के दौरान। सेटअप में लगभग 4 मिनट का समय लगेगा", + "keycard-processing-description": "कार्ड को स्थिर रखने का प्रयास करें", + "keycard-processing-title": "संसाधित किया जा रहा है...", + "keycard-recover": "खोया या जमे हुए कार्ड?", + "keycard-recover-text": "यदि आपके पास अपना मुहावरा है तो आप इस खाते से संबद्ध एक नया कीकार्ड बना सकते हैं। आप या तो नए कीकार्ड का उपयोग कर सकते हैं या फ्रोजन पर फ़ैक्टरी रीसेट कर सकते हैं।", + "keycard-recover-title": "इस खाते के लिए नया कार्ड बनाएं?", + "keycard-recovery-intro-button-text": "वसूली शुरू करें", + "keycard-recovery-intro-header": "कीकार्ड पर संग्रहीत कुंजियाँ पुनर्प्राप्त करें", + "keycard-recovery-intro-text": "यदि आपने पहले और अब इस उपकरण पर इन कुंजियों का उपयोग करना चाहते हैं, तो आपने कीकार्ड का उपयोग करके कुंजियाँ बनाई हैं", + "keycard-recovery-no-key-header": "करने के लिए कुछ भी नहीं है\n यहां पुनर्प्राप्त करें", + "keycard-recovery-no-key-text": "आपके कीकार्ड में कोई कुंजी संग्रहीत नहीं है। इसका उपयोग करने के लिए, एक नई कुंजी बनाएं और कुंजी को संग्रहीत करने के लिए अपना कीकार्ड चुनें", + "keycard-recovery-phrase-confirm-header": "बीज वाक्यांश की पुष्टि करें", + "keycard-recovery-phrase-confirmation-text": "आपके पास दूसरा मौका नहीं होगा! यदि आप एक्सेस खो देते हैं, उदाहरण के लिए, अपना कीकार्ड खो जाने से, आप केवल अपनी कुंजी को अपने बीज वाक्यांश के साथ एक्सेस कर सकते हैं। कोई नहीं, लेकिन आपके पास अपना बीज वाक्यांश है। नीचे लिखें। उसे सुरक्षित रखें।", + "keycard-recovery-phrase-confirmation-title": "बीज वाक्यांश नीचे लिखा?", + "keycard-recovery-success-header": "आपकी चाबियां हो गई हैं\n सफलतापूर्वक पुनर्प्राप्त", + "keycard-redeem-title": "रिडीम करें", + "keycard-redeem-tx": "संपत्ति भुनाएं", + "keycard-redeem-tx-desc": "एसेट पर हस्ताक्षर करने और प्राप्त करने के लिए कार्ड पर टैप करें", + "keycard-reset-passcode": "पासकोड रीसेट करें", + "keycard-success-description": "आप अभी कार्ड निकाल सकते हैं", + "keycard-success-title": "सफलता", + "keycard-unauthorized-operation": "आप इस कार्रवाई को करने के लिए अनधिकृत हैं।\n कृपया मान्य कार्ड पर टैप करें और पुनः प्रयास करें।", + "keycard-upsell-subtitle": "बढ़ी सुरक्षा और सुविधा", + "kicked": "लात मारी", + "language": "भाषा", + "last-backup-performed": "अंतिम बैकअप निष्पादित किया गया:", + "last-transaction": "अंतिम लेनदेन", + "learn-more": "और अधिक जानें", + "learn-more-about-keycard": "Keycard के बारे में और जानें", + "leave": "छोड़", + "leave-chat": "चैट छोड़ें", + "leave-chat-confirmation": "आपके डिवाइस से चैट इतिहास हटा दिया जाएगा। फिर से जुड़ने के बाद आप अपना कोई भी इतिहास पुनः प्राप्त नहीं कर पाएंगे।", + "leave-community": "समुदाय छोड़ो", + "leave-community-message": "आपको जाते हुए देखकर हमें दुख होगा लेकिन याद रखें, आप किसी भी समय वापस आ सकते हैं!", + "leave-community?": "समुदाय छोड़ें?", + "leave-confirmation": "{{chat-name}} छोड़ें", + "leave-group": "समूह छोड़ दें", + "left": "बाएँ", + "les-ulc": "एलईएस\/यूएलसी", + "lets-go": "चलिए चलते हैं", + "lifestyle": "जीवन शैली", + "light": "रोशनी", + "limit": "सीमा", + "link-to-community": "समुदाय से लिंक करें", + "linked-on": "{{date}} को लिंक किया गया", + "load-messages-before": "{{date}} से पहले", + "load-more-messages": "↓ अधिक संदेश प्राप्त करें", + "load-more-timeline": "अधिक प्राप्त करें", + "loading": "लोड हो रहा है...", + "local-notifications": "स्थानीय सूचनाएं", + "local-notifications-subtitle": "पृष्ठभूमि सेवा सक्षम करें", + "lock-app-with": "लॉक ऐप", + "log-level": "छांटने का स्तर", + "log-level-settings": "लॉग स्तर सेटिंग्स", + "logging": "लॉगिंग", + "logging-enabled": "लॉगिंग सक्षम है?", + "login-pin-description": "अपनी चाबियों को अनलॉक करने के लिए अपना 6-अंकीय पासकोड दर्ज करें", + "logout": "लॉग आउट", + "logout-app-content": "खाता लॉग आउट हो जाएगा। जब आप इसे फिर से अनलॉक करते हैं, तो चयनित नेटवर्क का उपयोग किया जाएगा", + "logout-are-you-sure": "क्या आप वाकई यह चाहते हैं\n लॉग आउट करने के लिए?", + "logout-key-management": "आपको कुंजी प्रबंधन तक पहुँचने के लिए लॉग आउट करने की आवश्यकता है.", + "logout-title": "लॉग आउट?", + "looking-for-cards": "कार्ड ढूंढ रहे हैं...", + "lost-connection": "खोया तार", + "low-tip": "टिप बहुत कम है", + "lower-than-average-tip": "औसत टिप से कम", + "mail-should-be-configured": "मेल क्लाइंट को कॉन्फ़िगर किया जाना चाहिए", + "mailserver-address": "Status नोड पता", + "mailserver-automatic": "स्वचालित चयन", + "mailserver-automatic-switch-explanation": "उपलब्ध सबसे तेज़ Status नोड चुनें", + "mailserver-connection-error": "Status नोड से कनेक्ट नहीं हो सका", + "mailserver-content": "स्टेटस नेटवर्क में एक नोड जो संदेशों को 30 दिनों तक रूट और स्टोर करता है।", + "mailserver-details": "Status नोड विवरण", + "mailserver-error-content": "आपके द्वारा चयनित Status नोड तक नहीं पहुंचा जा सका.", + "mailserver-error-title": "Status नोड से जुड़ने में त्रुटि", + "mailserver-format": "एनोड: \/\/ {enode-id} : {password} @ {ip-address} : {port}", + "mailserver-pick-another": "एक और Status नोड चुनें", + "mailserver-reconnect": "Status नोड से कनेक्ट नहीं हो सका। पुनः कनेक्ट करने के लिए टैप करें", + "mailserver-request-error-content": "निम्न त्रुटि Status नोड द्वारा दी गई थी: {{error}}", + "mailserver-request-error-status": "इतिहास लाते समय एक त्रुटि हुई, विवरण के लिए लॉग जांचें", + "mailserver-request-error-title": "Status नोड अनुरोध त्रुटि", + "mailserver-request-retry": "पुन: प्रयास अनुरोध", + "mailserver-retry": "पुन: प्रयास करें", + "mailserver-title": "स्टेटस नोड", + "main-account": "मुख्य खाता", + "main-currency": "मुख्य मुद्रा", + "main-networks": "मुख्य नेटवर्क", + "main-wallet": "मुख्य वॉलेट", + "mainnet-network": "मुख्य नेटवर्क", + "make-admin": "एडमिन बनाओ", + "make-moderator": "मॉडरेटर बनाएं", + "manage-connections": "अनुप्रयोग कनेक्शंस के भीतर से कनेक्शंस प्रबंधित करें", + "manage-keys-and-storage": "चाबियाँ और भंडारण प्रबंधित करें", + "manage-members": "सदस्यों का प्रबंधन करें", + "mark-all-read": "सभी को पढ़ा दिखाएं", + "mark-as-read": "पढ़े हुए का चिह्नित", + "mark-untrustworthy": "अविश्वसनीय के रूप में चिह्नित करें", + "master-account": "मास्टर खाता", + "max-fee": "अधिकतम शुल्क", + "max-priority-fee": "अधिकतम प्राथमिकता शुल्क", + "maximum-fee": "अधिकतम शुल्क", + "maximum-fee-desc": "लेन-देन के लिए अधिकतम समग्र मूल्य। यदि वर्तमान ब्लॉक आधार शुल्क इससे अधिक है, तो आपके लेन-देन को निम्न आधार शुल्क के साथ निम्नलिखित ब्लॉक में शामिल किया जाएगा।", + "maybe-later": "शायद बाद में", + "member-ban": "सदस्य को प्रतिबंधित करें", + "member-kick": "किक सदस्य", + "members": { + "one": "1 member", + "other": "{{count}} members" + }, + "members-active": { + "one": "1 member", + "other": "{{count}} members" + }, + "members-active-none": "कोई सदस्य नहीं", + "members-count": "{{count}} सदस्य", + "members-label": "सदस्यों", + "members-limit-reached": "सदस्यों की सीमा पूरी हो गई", + "members-title": "सदस्यों", + "membership": "सदस्यता", + "membership-approval": "अनुमोदन की आवश्यकता है", + "membership-approval-description": "आपका समुदाय शामिल होने के लिए स्वतंत्र है, लेकिन नए सदस्यों को पहले समुदाय निर्माता द्वारा अनुमोदित होना आवश्यक है", + "membership-button": "सदस्यता की आवश्यकता", + "membership-declined": "सदस्यता अनुरोध अस्वीकार कर दिया गया था", + "membership-description": "समूह सदस्यता के लिए आपको समूह व्यवस्थापक द्वारा स्वीकार किया जाना आवश्यक है", + "membership-ens": "ईएनएस उपयोगकर्ता नाम की आवश्यकता है", + "membership-ens-description": "आपके समुदाय को शामिल होने के लिए एक ENS उपयोगकर्ता नाम की आवश्यकता है", + "membership-free": "आवश्यकता नही है", + "membership-free-description": "आपका समुदाय किसी के भी शामिल होने के लिए स्वतंत्र है", + "membership-invite": "किसी अन्य सदस्य से आमंत्रण की आवश्यकता है", + "membership-invite-description": "आपके समुदाय में केवल मौजूदा समुदाय के सदस्यों के आमंत्रण द्वारा ही शामिल किया जा सकता है", + "membership-none": "कोई भी नहीं", + "membership-none-placeholder": "नए सदस्यों के शामिल होने से पहले आपको कुछ मानदंडों को पूरा करने की आवश्यकता हो सकती है। इसे किसी भी समय बदला जा सकता है", + "membership-request-denied": "सदस्यता अनुरोध अस्वीकृत", + "membership-request-pending": "सदस्यता अनुरोध लंबित", + "membership-requests": "सदस्यता अनुरोध", + "membership-title": "सदस्यता की आवश्यकता", + "mention": "उल्लेख", + "mentions": "उल्लेख", + "message": "संदेश", + "message-deleted": "संदेश हटाये", + "message-deleted-for-everyone": "संदेश सभी के लिए हटा दिया गया", + "message-deleted-for-you": "आपके लिए संदेश हटा दिया गया", + "message-not-sent": "संदेश नहीं भेजा गया", + "message-options-cancel": "रद्द करना", + "message-reply": "जवाब", + "messages": "संदेशों", + "messages-from-contacts-only-subtitle": "केवल वे लोग जिन्हें आपने संपर्क के रूप में जोड़ा है, वे ही आपके साथ एक नई चैट शुरू कर सकते हैं या आपको किसी समूह में आमंत्रित कर सकते हैं", + "messages-gap-warning": "कुछ संदेश गायब हो सकते हैं", + "might-break": "कुछ ऐप्स . तोड़ सकते हैं", + "migration-successful": "माइग्रेशन सफल", + "migration-successful-text": "खाता सफलतापूर्वक Keycard में माइग्रेट हो गया", + "migrations-failed-content": "{{message}}\n स्कीमा संस्करण: प्रारंभिक {{initial-version}} , वर्तमान {{current-version}} , अंतिम {{last-version}} \n\n एक डेटाबेस त्रुटि हुई। आपके फंड और चैट कुंजी सुरक्षित हैं। अन्य डेटा, जैसे आपकी चैट और संपर्क, को पुनर्स्थापित नहीं किया जा सकता है। \" {{erase-multiaccounts-data-button-text}} \" बटन, अन्य सभी डेटा को हटा देगा और आपको अपने फंड तक पहुंचने और संदेश भेजने की अनुमति देगा।", + "miners-higher-fee": "यदि आप अधिक शुल्क का भुगतान करते हैं तो खनिक आपके लेन-देन को पहले शामिल कर सकते हैं।", + "minimum-received": "न्यूनतम प्राप्त", + "mobile-network-ask-me": "मोबाइल नेटवर्क पर मुझसे पूछें", + "mobile-network-continue-syncing": "समन्वयन जारी रखें", + "mobile-network-continue-syncing-details": "आप इसे बाद में सेटिंग में बदल सकते हैं", + "mobile-network-go-to-settings": "सेटिंग्स में जाओ", + "mobile-network-settings": "मोबाइल सामग्री", + "mobile-network-sheet-configure": "आप सिंकिंग को और अधिक में कॉन्फ़िगर कर सकते हैं\n विवरण में", + "mobile-network-sheet-offline": "कोई वाई-फ़ाई नहीं, संदेश समन्वयन अक्षम है.", + "mobile-network-sheet-offline-details": "मोबाइल नेटवर्क का उपयोग करके समन्वयन करना बंद है", + "mobile-network-sheet-remember-choice": "मेरी पसंद याद रखें", + "mobile-network-sheet-settings": "समायोजन", + "mobile-network-start-syncing": "समन्वयन प्रारंभ करें", + "mobile-network-stop-syncing": "सिंक करना बंद करें", + "mobile-network-stop-syncing-details": "वाई-फ़ाई से कनेक्ट होने तक?", + "mobile-network-use-mobile": "मोबाइल डेटा का उपयोग करें", + "mobile-network-use-mobile-data": "चैट और वॉलेट को सिंक करते समय स्टेटस बहुत अधिक डेटा का उपयोग करता है।", + "mobile-network-use-wifi": "केवल वाई - फाई", + "mobile-syncing-sheet-details": "चैट और वॉलेट को सिंक करते समय स्टेटस बहुत अधिक डेटा का उपयोग करता है।", + "mobile-syncing-sheet-title": "मोबाइल डेटा का उपयोग करके सिंक करें?", + "more": "अधिक", + "move-and-reset": "ले जाएँ और रीसेट करें", + "move-keystore-file": "कीस्टोर फ़ाइल ले जाएँ", + "move-keystore-file-to-keycard": "कीस्टोर फ़ाइल को कीकार्ड में ले जाएँ?", + "multiaccount-exists-content": "इस खाते की कुंजियां पहले से मौजूद हैं और इन्हें दोबारा नहीं जोड़ा जा सकता. यदि आपने अपना पासवर्ड, पासकोड या कीकार्ड खो दिया है, तो ऐप को अनइंस्टॉल करें, पुनः स्थापित करें और अपना बीज वाक्यांश दर्ज करके अपनी कुंजियों तक पहुंचें", + "multiaccount-exists-title": "इस खाते की कुंजियाँ पहले से मौजूद हैं", + "multiaccounts-recover-enter-phrase-text": "12, 15, 18, 21 या 24 शब्द दर्ज करें।\n एक ही स्थान से शब्दों को अलग करें।", + "multiaccounts-recover-enter-phrase-title": "अपना बीज वाक्यांश दर्ज करें", + "music": "संगीत", + "mutal-contacts": "आपसी संपर्क", + "mute": "आवाज़ बंद करना", + "mute-chat": "म्यूट चैट", + "mute-community": "समुदाय को म्यूट करें", + "mutual-contact-requests": "पारस्परिक संपर्क अनुरोध", + "my-accounts": "मेरा खाता", + "my-accounts-empty": "आपके उपलब्ध खाते यहां दिखाई देंगे", + "my-profile": "मेरी प्रोफाइल", + "my-status": "मेरी Status", + "n-photos": "{{count}} फ़ोटो", + "name": "नाम", + "name-of-token": "आपके टोकन का नाम", + "name-optional": "नाम: (वैकल्पिक)", + "name-your-channel": "अपने चैनल को नाम दें", + "name-your-channel-placeholder": "चैनल का नाम", + "name-your-community": "अपने समुदाय को नाम दें", + "name-your-community-placeholder": "एक आकर्षक नाम", + "need-help": "मदद की ज़रूरत है?", + "negative": "नेगटिव", + "network": "नेटवर्क", + "network-chain": "नेटवर्क श्रृंखला", + "network-details": "नेटवर्क विवरण", + "network-fee": "नेटवर्क शुल्क", + "network-id": "नेटवर्क आईडी", + "network-info": "नेटवर्क जानकारी", + "network-invalid-network-id": "निर्दिष्ट नेटवर्क आईडी RPC url द्वारा नेटवर्क आईडी के अनुरूप नहीं है", + "network-invalid-status-code": "अमान्य स्थिति कोड: {{code}}", + "network-invalid-url": "नेटवर्क URL अमान्य है", + "network-settings": "संजाल विन्यास", + "never": "कभी नहीं", + "new": "नया", + "new-category": "नई श्रेणी", + "new-chat": "नई चैट", + "new-community-title": "नया समुदाय", + "new-contact": "नया कॉन्ट्रैक्ट", + "new-contract": "नया सम्पर्क", + "new-favourite": "नया पसंदीदा", + "new-group": "नया समूह", + "new-group-chat": "नया समूह चैट", + "new-messages-header": "नए संदेश", + "new-network": "नया नेटवर्क", + "new-password": "नया पासवर्ड", + "new-pin-description": "नया 6-अंकीय पासकोड दर्ज करें", + "new-public-group-chat": "सार्वजनिक चैट में शामिल हों", + "new-puk-description": "नया 12-अंकीय PUK दर्ज करें", + "new-status": "नई Status", + "new-tab": "नया टैब", + "new-ui": "नया यूआई", + "next": "अगला", + "nickname": "उपनाम", + "nickname-description": "उपनाम आपको Status में दूसरों की पहचान करने में मदद करते हैं।\n आपके द्वारा जोड़े गए प्रचलित नाम केवल आप ही देख सकते हैं", + "no": "नहीं", + "no-collectibles": "कोई संग्रहणीय उपलब्ध नहीं", + "no-contacts": "अभी तक कोई संपर्क नहीं", + "no-keycard-applet-on-card": "कार्ड पर कोई कीकार्ड एप्लेट नहीं", + "no-messages": "कोई संदेश नहीं", + "no-pairing-slots-available": "यह कार्ड पहले से ही 5 उपकरणों के साथ जोड़ा गया है और इसे इससे नहीं जोड़ा जा सकता है। कृपया युग्मित उपकरणों में से किसी एक का उपयोग करें, इस कार्ड से लॉग इन करें और कार्ड पर युग्मन स्लॉट खाली करें", + "no-pinned-messages": "कोई पिन किए गए संदेश नहीं", + "no-result": "कोई परिणाम नहीं", + "no-thanks": "जी नहीं, धन्यवाद", + "no-tokens-found": "कोई टोकन नहीं मिला", + "node-address": "नोड विवरण", + "node-details": "नोड विवरण", + "node-info": "नोड जानकारी", + "node-version": "नोड संस्करण", + "nodes-disabled": "स्टेटस नोड्स अक्षम", + "non-archival-node": "RPC समापन बिंदु अभिलेखीय अनुरोधों का समर्थन नहीं करता है। आपका स्थानीय स्थानान्तरण इतिहास अधूरा हो सकता है।", + "non-contacts": "गैर संपर्क", + "nonce": "अस्थायी रूप से", + "none": "कोई नहीं", + "normal": "सामान्य", + "not-applicable": "अहस्ताक्षरित लेनदेन के लिए लागू नहीं", + "not-connected-nodes": "स्टेटस नोड से जुड़ा नहीं है", + "not-connected-to-peers": "किसी भी साथी से जुड़ा नहीं है", + "not-enough-snt": "पर्याप्त नहीं SNT", + "not-found": "नहीं मिला", + "not-keycard-text": "आपके द्वारा उपयोग किया गया कार्ड कीकार्ड नहीं है। इसका उपयोग करने के लिए आपको एक कीकार्ड खरीदना होगा", + "not-keycard-title": "कीकार्ड नहीं", + "not-registered": "पंजीकृत नहीं है", + "notification-settings": "सूचनाएं", + "notifications": "सूचनाएं", + "notifications-non-contacts": "गैर-संपर्कों से सूचनाएं", + "notifications-preferences": "सूचना की प्राथमिकताएं", + "notifications-servers": "अधिसूचना सर्वर", + "notifications-switch": "सूचनाएं दिखाएं", + "notifications-transactions": "वॉलेट लेनदेन", + "notify": "सूचित करें", + "now": "अब", + "off": "बंद", + "off-status-tree": "ऑफ स्टेटस ट्री", + "offline": "ऑफलाइन", + "offline-messaging-use-history-explanation": "ऐप बंद होने के दौरान भेजे गए संदेशों को लाने के लिए Status नोड्स सक्षम करें। सक्षम होने पर, एक स्थिति नोड को आपका आईपी पता मिल जाता है। अक्षम होने पर आपको ऐप बंद होने पर संदेश प्राप्त नहीं होंगे और जब आप बाद में ऐप खोलेंगे तो उन्हें नहीं देख पाएंगे।", + "offline-messaging-use-history-nodes": "Status नोड्स का प्रयोग करें", + "ok": "ठीक है", + "ok-continue": "ठीक है, जारी रखें", + "ok-got-it": "ठीक है समझ आ गया", + "ok-save-pass": "ठीक है, पासवर्ड सेव करें", + "okay": "ठीक", + "on": "पर", + "on-status-tree": "स्टेटस ट्री पर", + "once-enabled-share-metadata": "एक बार सक्षम होने पर, चैट में पोस्ट किए गए लिंक साइट के साथ आपका मेटाडेटा साझा कर सकते हैं", + "one-day": "एक दिन", + "one-month": "एक माह", + "one-week": "एक हफ्ता", + "online": "ऑनलाइन", + "open": "खुला", + "open-chat": "चैट खोलें", + "open-dapp": "ऐप . खोलें", + "open-dapp-store": "ऐप्स . खोजें", + "open-home": "खुला...", + "open-in-new-tab": "नए टैब में खोलें", + "open-membership": "खुली सदस्यता", + "open-nfc-settings": "एनएफसी सेटिंग्स खोलें", + "open-on-block-explorer": "ब्लॉक एक्सप्लोरर पर खोलें", + "opened": "खुल गया", + "opening-buy-crypto": "Opening {{site}}...", + "optimal": "इष्टतम", + "optional": "वैकल्पिक", + "or": "या", + "outgoing": "मिलनसार", + "outgoing-transaction": "आउटगोइंग लेनदेन", + "owner": "स्वामी", + "page-camera-request-blocked": "कैमरा अनुरोध अवरुद्ध। कैमरा अनुरोधों को सक्षम करने के लिए सेटिंग्स पर जाएं", + "page-would-like-to-use-camera": "अपने कैमरे का उपयोग करना चाहेंगे", + "pair": "जोड़ी उपकरण", + "pair-card": "इस डिवाइस से जोड़े", + "pair-code": "जोड़ी कोड", + "pair-code-explanation": "कुंजी को अनलॉक करने और उसी कीकार्ड से लेन-देन पर हस्ताक्षर करने के लिए कार्ड को किसी भिन्न डिवाइस (5 तक तक) से जोड़ता है", + "pair-code-placeholder": "कोड जोड़े...", + "pair-this-card": "इस कार्ड को जोड़ो", + "pair-this-device": "विज्ञापन डिवाइस", + "pair-this-device-description": "संपर्कों और उनके बीच चैट को सिंक करने के लिए अपने उपकरणों को जोड़ें", + "paired-devices": "युग्मित उपकरण", + "pairing": "बाँधना", + "pairing-card": "पेयरिंग कार्ड", + "pairing-changed": "पेयरिंग कोड बदल दिया गया है", + "pairing-code-placeholder": "कोड जोड़ा जा रहा है...", + "pairing-code_error1": "पेयरिंग कोड मेल नहीं खाते।", + "pairing-go-to-installation": "पेयरिंग सेटिंग में जाएं", + "pairing-maximum-number-reached-content": "कृपया एक नया डिवाइस सक्षम करने से पहले अपने किसी एक डिवाइस को अक्षम कर दें।", + "pairing-maximum-number-reached-title": "उपकरणों की अधिकतम संख्या तक पहुंच गया", + "pairing-new-installation-detected-content": "एक नए उपकरण का पता चला है।\n अपने उपकरणों का सही ढंग से उपयोग करने के लिए, उनका उपयोग करने से पहले उन्हें युग्मित करना और सक्षम करना महत्वपूर्ण है।\n कृपया अपने डिवाइस को पेयर करने के लिए सेटिंग के तहत डिवाइस सेक्शन में जाएं।", + "pairing-new-installation-detected-title": "नए उपकरण का पता चला", + "pairing-no-info": "कोई सूचना नहीं", + "pairing-please-set-a-name": "कृपया अपने डिवाइस के लिए एक नाम सेट करें।", + "passphrase": "पदबंध", + "password": "पासवर्ड", + "password-description": "कम से कम 6 वर्ण। आपका पासवर्ड आपकी चाबियों की सुरक्षा करता है। आपको स्टेटस अनलॉक करने और लेन-देन करने के लिए इसकी आवश्यकता है।", + "password-mismatch": "नया पासवर्ड और पुष्टि मेल नहीं खाता", + "password-placeholder": "पासवर्ड...", + "password-placeholder2": "अपने पासवर्ड की पुष्टि करें", + "password-reset-in-progress": "पासवर्ड बदल रहा है...", + "password-reset-success": "पासवर्ड बदला गया", + "password-reset-success-message": "आपको फिर से साइन इन करना होगा", + "password_error1": "पासवर्ड मेल नहीं खाते।", + "paste": "पेस्ट करें", + "paste-json": "JSON पेस्ट करें", + "pay-to-chat": "चैट करने के लिए भुगतान करें", + "peer-content": "Status चैट नेटवर्क से जुड़ा एक उपकरण। प्रत्येक उपयोगकर्ता अपने उपकरणों की संख्या के आधार पर एक या अधिक साथियों का प्रतिनिधित्व कर सकता है।", + "peer-title": "समकक्ष", + "peer-to-peer": "पीयर टू पीयर", + "peers": "समकक्ष लोग", + "peers-count": "साथियों की गिनती", + "peers-stats": "सहकर्मी आँकड़े", + "pending": "लंबित", + "pending-confirmation": "पुष्टि लंबित...", + "pending-invitations": "लंबित सदस्यता अनुरोध", + "pending-requests": "अनुरोध लंबित", + "per-gas-price-limit": "प्रति गैस मूल्य सीमा", + "per-gas-tip-limit": "प्रति-गैस टिप सीमा", + "perform-backup": "बैकअप निष्पादित करें", + "permissions": "अनुमतियां", + "phone-e164": "अंतर्राष्ट्रीय 1", + "photos": "तस्वीरें", + "photos-access-error": "आवश्यक फ़ोटो अनुमति देने के लिए, कृपया अपनी सिस्टम सेटिंग में जाएं और सुनिश्चित करें कि Status > फ़ोटो चयनित है।", + "pin": "पिन", + "pin-changed": "6 अंकों का पासकोड बदल दिया गया है", + "pin-code": "6 अंकों का पासकोड", + "pin-limit-reached": "पिन की सीमा पूरी हो गई है. पहले किसी पिछले संदेश को अनपिन करें।", + "pin-mismatch": "गलत पासकोड", + "pin-one-attempt": "एक प्रयास", + "pin-one-attempt-blocked-after": "इससे पहले कि आपका कीकार्ड ब्लॉक हो जाए", + "pin-one-attempt-blocked-before": "सावधान रहें, आपके पास केवल", + "pin-one-attempt-frozen-after": "इससे पहले कि आपका कीकार्ड फ़्रीज़ हो जाए", + "pin-one-attempt-frozen-before": "सावधान रहें, आपके पास केवल", + "pin-retries-left": "{{number}} प्रयास बाकी हैं", + "pin-to-channel": "चैनल पर पिन करें", + "pin-to-chat": "चैट में पिन करें", + "pinned-a-message": "एक संदेश पिन किया", + "pinned-by": "द्वारा पिन किया गया", + "pinned-messages": "पिन किए गए संदेश", + "pinned-messages-count": { + "one": "1 पिन किया गया संदेश", + "other": "{{count}} पिन किए गए संदेश" + }, + "pinned-messages-empty": "पिन किए गए संदेश यहां दिखाई देंगे. किसी संदेश को पिन करने के लिए, उसे दबाकर रखें और `पिन करें` पर टैप करें", + "podcasts": "पॉडकास्ट", + "positive": "पॉज़िटीव", + "powered-by-paraswap": "Paraswap . द्वारा संचालित", + "preference": "पसंद", + "preview-privacy": "पूर्वावलोकन गोपनीयता मोड", + "previewing-may-share-metadata": "इन वेबसाइटों के लिंक का पूर्वावलोकन करने से आपका मेटाडेटा उनके स्वामियों के साथ साझा किया जा सकता है", + "price-impact": "मूल्य प्रभाव", + "price-impact-desc": "इस लेनदेन के लिए अनुमानित मूल्य प्रभाव। यदि वर्तमान ब्लॉक आधार शुल्क इससे अधिक है, तो आपके लेन-देन को निम्न आधार शुल्क के साथ निम्नलिखित ब्लॉक में शामिल किया जाएगा।", + "principles": "सिद्धांतों", + "priority": "प्राथमिकता", + "privacy": "गोपनीयता", + "privacy-and-security": "गोपनीयता और सुरक्षा", + "privacy-photos": "प्रोफ़ाइल फ़ोटो गोपनीयता", + "privacy-policy": "गोपनीयता नीति", + "privacy-show-to-warning": "जिन लोगों ने आपकी प्रोफ़ाइल तस्वीर पहले ही देख ली है, वे आगे भी ऐसा करते रहेंगे", + "private-key": "निजी चाबी", + "private-notifications": "निजी सूचनाएं", + "private-notifications-descr": "Status आपको नए संदेशों के बारे में सूचित करेगी। आप अपनी अधिसूचना प्राथमिकताएं बाद में सेटिंग में संपादित कर सकते हैं।", + "processing": "बस एक पल", + "product-information": "उत्पाद की जानकारी", + "profile": "प्रोफ़ाइल", + "profile-deleted-content": "आपकी प्रोफ़ाइल सफलतापूर्वक हटा दी गई थी", + "profile-deleted-keycard": "अब आप अपने कीकार्ड पर एक और की-जोड़ी को पुनर्स्थापित कर सकते हैं", + "profile-deleted-title": "प्रोफ़ाइल मिटा दी गई है", + "profile-details": "प्रोफ़ाइल विवरण", + "profile-not-found": "प्रोफ़ाइल नहीं मिला", + "profile-pic-pick": "गैलरी से चुनें", + "profile-pic-remove": "फोटो हटाएं", + "profile-pic-take": "फोटो लो", + "profile-picture-updated": "प्रोफ़ाइल चित्र अपडेट किया गया", + "public": "जनता", + "public-channel": "सार्वजनिक चैनल", + "public-chat": "सार्वजनिक चैट", + "public-chat-description": "अपनी रुचियों के लिए सार्वजनिक चैट में शामिल हों! कोई भी नई शुरुआत कर सकता है।", + "public-chats": "सार्वजनिक चैट", + "public-group-status": "जनता", + "public-group-topic": "विषय", + "public-key": "सार्वजनिक कुंजी", + "puk-and-pairing-codes-displayed": "PUK और पेयरिंग कोड प्रदर्शित", + "puk-changed": "12 अंकों का PUK बदल दिया गया है", + "puk-code": "पीयूके कोड", + "puk-code-explanation": "यदि आप अपना 6 अंकों का पासकोड भूल जाते हैं या इसे 3 बार गलत तरीके से दर्ज करते हैं, तो आपको अपना कार्ड अनलॉक करने के लिए इस कोड की आवश्यकता होगी।", + "puk-mismatch": "गलत PUK कोड", + "push-failed-transaction": "आपका लेन-देन विफल रहा", + "push-failed-transaction-body": "{{value}} {{currency}} से {{to}}", + "push-inbound-transaction": "आपने {{value}} {{currency}} प्राप्त किया", + "push-inbound-transaction-body": "{{from}} से {{to}} तक", + "push-notifications-server-enabled": "सर्वर सक्षम", + "push-notifications-servers": "पुश सूचना सर्वर", + "push-outbound-transaction": "आपने {{value}} {{currency}} भेजा है", + "push-outbound-transaction-body": "{{from}} से {{to}} तक", + "quiet-days": "{{quiet-days}} दिन", + "quiet-hours": "{{quiet-hours}} घंटे", + "re-encrypt-key": "अपनी कुंजियों को फिर से एन्क्रिप्ट करें", + "rearrange-categories": "श्रेणियाँ पुनर्व्यवस्थित करें", + "receive": "पाना", + "receive-transaction": "लेन-देन प्राप्त करें", + "received": "प्राप्त हुआ", + "recent": "हाल का", + "recent-empty": "हाल ही में उपयोग किए गए पते यहां दिखाई देंगे", + "recent-recipients": "संपर्क", + "recently-used-stickers": "हाल ही में उपयोग किए गए स्टिकर यहां दिखाई देंगे", + "recipient": "प्राप्तकर्ता", + "recipient-code": "प्राप्तकर्ता का पता दर्ज करें", + "recipient-code-placeholder": "0x... या username.domain.eth", + "recover": "वसूल करना", + "recover-key": "मौजूदा कुंजियों तक पहुंचें", + "recover-keycard-multiaccount-not-supported": "इस खाते की कुंजियां पहले से मौजूद हैं और इन्हें दोबारा नहीं जोड़ा जा सकता. यदि आपने अपना पासवर्ड, पासकोड या कीकार्ड खो दिया है, तो ऐप को अनइंस्टॉल करें, पुनः स्थापित करें और अपना बीज वाक्यांश दर्ज करके अपनी कुंजियों तक पहुंचें", + "recover-with-keycard": "Keycard के साथ पुनर्प्राप्त करें", + "recover-with-seed-phrase": "बीज वाक्यांश के साथ पुनर्प्राप्त करें", + "recovering-key": "कुंजियों तक पहुंचना...", + "recovery-confirm-phrase": "बीज वाक्यांश की पुष्टि करें", + "recovery-phrase": "बीज वाक्यांश", + "recovery-success-text": "अपनी कुंजियों को फिर से एन्क्रिप्ट करने के लिए आपको एक नया कोड या पासवर्ड बनाना होगा", + "recovery-typo-dialog-description": "कृपया ध्यान दें, आपके बीज वाक्यांश को ठीक उसी शब्द और क्रम का उपयोग करना चाहिए जैसा आपने इसे प्राप्त किया था", + "recovery-typo-dialog-title": "क्या बीज वाक्यांश सही है?", + "redeem-amount": "{{quantity}} बोनस उपलब्ध है", + "redeem-now": "अब एवज करें", + "redeem-success": "बोनस सफलता भुनाएं!", + "reduced-tip": "प्राथमिकता टिप कम हो जाएगी", + "refresh": "ताज़ा करना", + "registered": "दर्ज कराई", + "reject": "अस्वीकार", + "reject-and-delete": "अस्वीकार करें और हटाएं", + "remember-me": "पहचाना की नहीं", + "remind-me-later": "मुझे इसे फिर से दिखाओ", + "remote-notifications": "दूरस्थ सूचनाएँ", + "remote-notifications-subtitle": "Google पुश सूचनाएँ सक्षम करें", + "remove": "हटाना", + "remove-favourite": "पसंदीदा हटाएं", + "remove-from-chat": "चैट से हटाएं", + "remove-from-contacts": "संपर्कों में से निकालें", + "remove-from-contacts-text": "किसी उपयोगकर्ता को अपनी संपर्क सूची से हटाकर आप उनसे अपना बटुआ पता नहीं छिपाते हैं", + "remove-group": "समूह हटाएं", + "remove-network": "नेटवर्क हटाएं", + "remove-token": "टोकन हटाएं", + "removed": "निकाला गया", + "removed-from-contacts": "संपर्कों से हटाया गया", + "rename": "नाम बदलें", + "repeat-pin": "नया 6-अंकीय पासकोड दोहराएं", + "repeat-puk": "नया 12-अंकीय PUK दोहराएं", + "replies": "जवाब", + "replying-to": "{{author}} को प्रत्युत्तर देना", + "report-bug-email-template": "1. मुद्दा विवरण\n {{description}}\n \n\n 2. पुनरुत्पादन के चरण\n {{steps}}\n \n\n 3. स्क्रीनशॉट संलग्न करें जो समस्या को प्रदर्शित कर सकते हैं, कृपया\n", + "request-access": "अनुरोध का उपयोग", + "request-feature": "एक सुविधा का अनुरोध करें", + "request-membership": "सदस्यता का अनुरोध करें", + "request-pending": "अनुरोध अपूर्ण है…", + "request-processed-after-node-online": "समुदाय स्वामी नोड के वापस ऑनलाइन होने पर आपके अनुरोध पर कार्रवाई की जाएगी।", + "request-to-join": "शामिल होने का अनुरोध", + "request-transaction": "लेनदेन का अनुरोध करें", + "required-field": "आवश्यक क्षेत्र", + "resend-message": "पुन: भेजें", + "reset-card": "कार्ड रीसेट करें", + "reset-card-description": "यह कार्रवाई कार्ड को प्रारंभिक स्थिति में रीसेट कर देगी। यह निजी कुंजी सहित सभी कार्ड डेटा मिटा देगा। ऑपरेशन प्रतिवर्ती नहीं है।", + "reset-database": "डेटाबेस रीसेट करें", + "reset-database-warning": "चैट, संपर्क और सेटिंग्स हटाएं। आवश्यक है जब आप अपना पासवर्ड खो चुके हों", + "reset-database-warning-keycard": "चैट, संपर्क और सेटिंग्स हटाएं।", + "reset-password": "पासवर्ड रीसेट", + "restore-defaults": "डिफॉल्ट्स का पुनःस्थापन", + "retry": "पुन: प्रयास करें", + "revoke-access": "एक्सेस अक्षम करें", + "rpc-url": "आरपीसी यूआरएल", + "rpc-usage-copy": "प्रतिलिपि", + "rpc-usage-filter": "फ़िल्टर के तरीके", + "rpc-usage-filter-methods": "फ़िल्टर के तरीके", + "rpc-usage-filtered-total": "{{filtered-total}} {{total}}", + "rpc-usage-get-stats": "ताज़ा करना", + "rpc-usage-info": "RPC उपयोग के आँकड़े", + "rpc-usage-reset": "रीसेट", + "rpc-usage-total": "कुल मूल्य", + "safe-estimate": "सुरक्षित अनुमान", + "save": "बचाना", + "save-image-library": "छवि को पुस्तकालय में सहेजें", + "save-password": "पासवर्ड को बचाओ", + "save-password-unavailable": "पासवर्ड बचाने के लिए डिवाइस पासकोड सेट करें", + "save-password-unavailable-android": "पासवर्ड सहेजना अनुपलब्ध है: आपका डिवाइस रूट हो सकता है या आवश्यक सुरक्षा सुविधाओं का अभाव हो सकता है।", + "say-hi": "हाय कहो", + "scan-qr": "क्यू आर कोड स्कैन करें", + "scan-qr-code": "वॉलेट पते के साथ QR कोड स्कैन करें", + "scan-tokens": "टोकन स्कैन करें", + "search": "खोज", + "search-contacts": "संपर्क खोजें", + "search-no-chat-found": "खोजने पर कोई परिणाम नहीं मिला। क्या मतलब है आपका", + "secret-keys-confirmation-text": "यदि आप कभी भी अपना फ़ोन खो देते हैं, तो आपको अपने कीकार्ड का उपयोग जारी रखने के लिए उनकी आवश्यकता होगी।", + "secret-keys-confirmation-title": "कोड लिख दिया?", + "security": "सुरक्षा", + "see-details": "विस्तृत जानकारी देखें", + "see-it-again": "इसे फिर से देखें", + "see-sticker-set": "पूरा स्टिकर सेट देखें", + "see-suggestions": "सुझाव देखें", + "seed-key-uid-mismatch": "बीज मेल नहीं खाता", + "seed-key-uid-mismatch-desc-1": "आपके द्वारा दर्ज किया गया बीज वाक्यांश {{multiaccount-name}} मेल नहीं खाता", + "seed-key-uid-mismatch-desc-2": "इस खाते की कुंजी प्रबंधित करने के लिए अपने बीज वाक्यांश को सत्यापित करें और पुनः प्रयास करें।", + "seed-phrase-content": "पढ़ने के लिए अनुकूल शब्दों का एक सेट, जिसे BIP39 मानक सूची से बेतरतीब ढंग से चुना गया है और अन्य पर्स और उपकरणों पर आपके एथेरियम खाते को पुनर्प्राप्त या एक्सेस करने के लिए उपयोग किया जाता है। क्रिप्टो पारिस्थितिकी तंत्र में \"स्मरक वाक्यांश,\" \"पुनर्प्राप्ति वाक्यांश\" या \"वॉलेट बैकअप\" के रूप में भी जाना जाता है। अधिकांश क्रिप्टो ऐप खाते बनाने के लिए इसी मानक का उपयोग करते हैं।", + "seed-phrase-placeholder": "बीज वाक्यांश...", + "seed-phrase-title": "बीज वाक्यांश", + "select": "चुनना", + "select-account": "खाता चुनें", + "select-account-dapp": "उस खाते का चयन करें जिसे आप डैप्स के साथ उपयोग करना चाहते हैं", + "select-account-first": "पहले एक खाता चुनें", + "select-chat": "मैसेजिंग शुरू करने के लिए चैट का चयन करें", + "select-new-location-for-keys": "अपनी निजी कुंजी (कुंजी) सहेजने के लिए एक नया स्थान चुनें", + "select-token-to-receive": "प्राप्त करने के लिए टोकन का चयन करें", + "select-token-to-swap": "स्वैप करने के लिए टोकन का चयन करें", + "selected": "चुन लिया", + "send": "भेजें", + "send-contact-request-message": "चैट शुरू करने के लिए आपको संपर्क बनने की जरूरत है", + "send-logs": "एक बग रिपोर्ट करो", + "send-logs-to": "{{email}} को बग की रिपोर्ट करें", + "send-message": "मेसेज भेजें", + "send-push-notifications": "पुश सूचनाएं भेजें", + "send-push-notifications-description": "अक्षम होने पर, आपके संदेश प्राप्त करने वाले व्यक्ति को उनके आने की सूचना नहीं दी जाएगी", + "send-request": "अनुरोध भेजा", + "send-request-amount": "राशि", + "send-request-amount-max-decimals": "दशमलव की अधिकतम संख्या {{asset-decimals}}", + "send-request-unknown-token": "अज्ञात टोकन - {{asset}}", + "send-sending-to": "को {{recipient-name}}", + "send-transaction": "लेन-देन भेजें", + "sending": "भेजना", + "sent": "भेज दिया", + "sent-at": "पर भेजा गया", + "server": "सर्वर", + "set-a-topic": "एक विषय बनाएं", + "set-currency": "मुद्रा सेट करें", + "set-custom-fee": "कस्टम शुल्क सेट करें", + "set-dapp-access-permissions": "डीएपी एक्सेस अनुमतियां सेट करें", + "set-max": "अधिकतम सेट करें", + "settings": "समायोजन", + "share": "साझा करना", + "share-address": "पता साझा करें", + "share-chat": "चैट साझा करें", + "share-community": "समुदाय साझा करें", + "share-contact-code": "मेरी चैट कुंजी साझा करें", + "share-dapp-text": "इस डीएपी को देखें जो मैं Status पर उपयोग कर रहा हूं: {{link}}", + "share-image": "चित्र साझा करें", + "share-invite-link": "एक आमंत्रण लिंक साझा करें", + "share-link": "लिंक शेयर करें", + "share-logs": "लॉग साझा करें", + "share-my-profile": "मेरी प्रोफ़ाइल साझा करें", + "share-profile": "प्रोफ़ाइल साझा करें", + "share-profile-link": "प्रोफ़ाइल लिंक साझा करें", + "share-public-chat-text": "इस सार्वजनिक चैट को Status ऐप पर देखें: {{link}}", + "shared": "साझा", + "sharing-copied-to-clipboard": "कॉपी किया गया", + "sharing-copy-to-clipboard": "प्रतिलिपि", + "sharing-data-desc-1": "यह सुनिश्चित करने के लिए कि कोई संवेदनशील डेटा नहीं भेजा जाता है, डेटा को सार्वजनिक नियमों के विरुद्ध मान्य किया जाता है। भरोसा मत करो, सत्यापित करो।☺", + "sharing-data-desc-2": "उपयोग डेटा स्टेटस 'पीयर-टू-पीयर नेटवर्क' पर एंड-टू-एंड एन्क्रिप्टेड भेजा जाता है", + "sharing-data-desc-3": "आपकी नियमित चैट कुंजी के बजाय, एकल उपयोग कुंजी का उपयोग किया जाता है", + "sharing-data-desc-4": "उपयोग डेटा को आपके आईपी पते से संबद्ध नहीं किया जा सकता", + "sharing-data-desc-5": "सभी उपयोगकर्ताओं का संचयी डेटा सार्वजनिक रूप से उपलब्ध है", + "sharing-data-desc-6": "डेटा भेजे जाने के बाद आपके फ़ोन से डेटा हटा दिया जाता है", + "sharing-share": "साझा करना", + "shell-placeholder-subtitle": "अपने समुदायों, संदेशों के टैब खोलें,\n वॉलेट खाता और ब्राउज़र विंडो", + "shell-placeholder-title": "आपके ऐप्स यहां चलेंगे", + "show-less": "कम दिखाएं", + "show-more": "और दिखाओ", + "show-notifications": "सूचनाएं दिखाएं", + "show-profile-pictures": "से प्रोफ़ाइल चित्र देखें", + "show-profile-pictures-to": "को अपना प्रोफ़ाइल चित्र दिखाएं", + "show-qr": "क्यूआर कोड दिखाएं", + "show-transaction-data": "लेन-देन डेटा दिखाएं", + "sign-and-send": "साइन करें और भेजें", + "sign-anyway": "वैसे भी साइन करें", + "sign-in": "साइन इन करें", + "sign-message": "संदेश पर हस्ताक्षर करें", + "sign-out": "साइन आउट", + "sign-request-failed": "संदेश पर हस्ताक्षर नहीं कर सका", + "sign-with": "के साथ साइन इन करें", + "sign-with-password": "पासवर्ड से साइन करें", + "sign-you-in": "आपको साइन इन कर रहा है…", + "signing": "हस्ताक्षर", + "signing-a-message": "एक संदेश पर हस्ताक्षर करना", + "signing-phrase": "हस्ताक्षर वाक्यांश", + "skip": "छोडना", + "slippage": "फिसलन", + "slow": "धीमा", + "something-went-wrong": "कुछ गलत हो गया", + "soon": "जल्दी", + "sort-communities": "समुदायों को सॉर्ट करें", + "specify-address": "पता निर्दिष्ट करें", + "specify-name": "एक नाम निर्दिष्ट करें", + "specify-network-id": "नेटवर्क आईडी निर्दिष्ट करें", + "specify-rpc-url": "RPC URL निर्दिष्ट करें", + "specify-server-public-key": "सर्वर सार्वजनिक कुंजी दर्ज करें", + "specify-symbol": "एक प्रतीक निर्दिष्ट करें", + "start-chat": "बातचीत शुरू कीजिए", + "start-conversation": "बातचीत शुरू करें", + "start-group-chat": "समूह चैट प्रारंभ करें", + "start-new-chat": "नई चैट शुरू करें", + "starter-pack-coming": "स्टार्टर पैक आपके रास्ते में आ रहा है", + "starter-pack-coming-description": "कुछ मिनटों से लेकर घंटों तक का समय लग सकता है", + "starter-pack-received": "स्टार्टर पैक प्राप्त", + "starter-pack-received-description": "शुरू करने के लिए यहां कुछ क्रिप्टो है! स्टिकर, एक ENS नाम प्राप्त करने के लिए इसका उपयोग करें और dapps आज़माएं", + "status": "Status", + "status-always-online": "हमेशा ऑनलाइन", + "status-automatic": "स्वचालित", + "status-automatic-subtitle": "स्वचालित रूप से status सेट करें", + "status-confirmed": "की पुष्टि की", + "status-dnd": "परेशान न करें", + "status-dnd-subtitle": "सभी सूचनाओं को म्यूट करता है", + "status-hardwallet": "Status हार्डवॉलेट", + "status-inactive": "निष्क्रिय", + "status-inactive-subtitle": "आपकी ऑनलाइन स्थिति छुपाता है", + "status-is-open-source": "Status ओपन-सोर्स है", + "status-keycard": "Status कीकार्ड", + "status-mobile-descr": "चैट को सिंक करते समय Status बहुत अधिक डेटा का उपयोग करती है। मोबाइल नेटवर्क पर होने पर आप सिंक नहीं करना चुन सकते हैं", + "status-not-sent-click": "पुष्टि नहीं। विकल्पों के लिए क्लिक करें", + "status-not-sent-tap": "पुष्टि नहीं। विकल्पों के लिए टैप करें", + "status-pending": "लंबित", + "status-sent": "भेजा", + "status-tx-not-found": "TX नहीं मिला", + "status-updates-descr": "स्टेटस अपडेट यहां दिखाई देंगे। अपनी टाइमलाइन पर अपडेट प्राप्त करने के लिए प्रोफ़ाइल को संपर्क के रूप में जोड़ें।", + "statuses-descr": "जो आपके मन में है उसे साझा करें और अपने संपर्कों से अपडेट रहें", + "statuses-my-status-descr": "जो आपके मन में है उसे साझा करें। आपकी प्रोफ़ाइल पर जाने वाला कोई भी व्यक्ति आपकी स्थिति देख सकेगा। जो लोग आपको अपने संपर्क के रूप में जोड़ते हैं, उन्हें आपके अपडेट उनकी टाइमलाइन पर प्राप्त होंगे", + "step-i-of-n": "{{number}} का चरण {{step}} }", + "sticker": "स्टीकर", + "sticker-market": "स्टिकर बाजार", + "storage": "भंडारण", + "strikethrough": "स्ट्राइकथ्रू", + "submit": "प्रस्तुत", + "submit-bug": "एक बग सबमिट करें", + "success": "सफलता", + "successful-connection": "सफल कनेक्शन", + "suggested-min-tip": "सुझाया गया मि. बख्शीश", + "suggested-price-limit": "सुझाई गई मूल्य सीमा", + "swap": "अदला-बदली", + "swap-details": "स्वैप विवरण", + "switch-to-simple-interface": "साधारण इंटरफ़ेस पर स्विच करें", + "symbol": "प्रतीक", + "sync-all-devices": "सभी उपकरणों को सिंक करें", + "sync-in-progress": "सिंक किया जा रहा है...", + "sync-settings": "सिंक सेटिंग्स", + "sync-synced": "मेल में", + "syncing-devices": "सिंक किया जा रहा है...", + "system": "प्रणाली", + "tabs": "टैब", + "tag-was-lost": "टैग खो गया था", + "tap-card-again": "अपने फ़ोन के पीछे कार्ड को फिर से टैप करें", + "terms-of-service": "उपयोग की शर्तें", + "test-networks": "टेस्ट नेटवर्क", + "text-input-disabled": "एक क्षण प्रतीक्षा करें...", + "thank-you": "शुक्रिया", + "this-device": "यह डिवाइस", + "this-device-desc": "आपकी कुंजियों को एन्क्रिप्ट किया जाएगा और आपके डिवाइस पर सुरक्षित रूप से संग्रहीत किया जाएगा", + "this-is-you-signing": "यह आपका हस्ताक्षर वाक्यांश है", + "this-will-take-few-seconds": "इसमें कुछ सेकंड लगेंगे", + "three-days": "तीन दिन", + "three-words-description": "प्रत्येक लेनदेन पर हस्ताक्षर करने से पहले आपको इन 3 शब्दों को देखना चाहिए", + "three-words-description-2": "यदि आप कोई भिन्न संयोजन देखते हैं, तो लेन-देन रद्द करें और साइन आउट करें", + "timeline": "समय", + "tip-cap": "टिप कैप", + "to": "तक", + "to-block": "अवरोध पैदा करना", + "to-enable-biometric": "{{bio-type-label}} को सक्षम करने के लिए, आपको अनलॉक स्क्रीन पर अपना पासवर्ड सहेजना होगा", + "to-encrypt-enter-password": "खाते को एन्क्रिप्ट करने के लिए कृपया अपना पासवर्ड दर्ज करें", + "to-see-this-message": "यह संदेश देखने के लिए,", + "token": "टोकन", + "token-auto-validate-decimals-error": "{{address}} {{symbol}} के लिए गलत दशमलव - {{expected}} पर सेट लेकिन {{actual}} के रूप में पाया गया", + "token-auto-validate-name-error": "{{address}} पर {{symbol}} टोकन के लिए गलत नाम - {{expected}} पर सेट किया गया लेकिन {{actual}} के रूप में पाया गया", + "token-auto-validate-symbol-error": "{{address}} {{symbol}} के लिए गलत प्रतीक - {{expected}} पर सेट किया गया लेकिन {{actual}} के रूप में पाया गया", + "token-details": "टोकन विवरण", + "token-price": "टोकन मूल्य", + "topic-name-error": "केवल लोअरकेस अक्षरों (a से z), संख्याओं और डैश (-) का उपयोग करें। चैट कुंजियों का उपयोग न करें", + "total-gas": "कुल गैस", + "total-members": "कुल सदस्य", + "transaction": "लेन-देन", + "transaction-data": "लेन - देन के डेटा", + "transaction-declined": "लेन - देन से इनकार कर दिया गया है", + "transaction-description": "नेटवर्क पर 12 पुष्टि के बाद इसे पूरा करने पर विचार करें।", + "transaction-details": "लेनदेन का विवरण", + "transaction-failed": "लेन - देन विफल", + "transaction-fee": "लेन-देन शुल्क", + "transaction-history": "लेनदेन का इतिहास", + "transaction-request": "लेनदेन अनुरोध", + "transaction-sent": "लेन-देन भेजा गया", + "transaction-signed": "लेन-देन पर सफलतापूर्वक हस्ताक्षर किए गए हैं", + "transactions": "लेनदेन", + "transactions-filter-select-all": "सभी का चयन करे", + "transactions-filter-title": "फ़िल्टर इतिहास", + "transactions-history": "लेनदेन का इतिहास", + "transactions-history-empty": "आपके इतिहास में अभी तक कोई लेन-देन नहीं है", + "transactions-history-loading": "लेन-देन इतिहास लोड हो रहा है. इसमें कुछ समय लग सकता है।", + "transactions-load-more": "और लोड करें", + "transactions-management-enabled": "लेनदेन प्रबंधन (अल्फा)", + "transactions-sign": "संकेत", + "transfer-ma-unknown-error-desc-1": "ऐसा लगता है कि आपका बहुखाता हटाया नहीं गया था। डेटाबेस रीसेट किया गया हो सकता है", + "transfer-ma-unknown-error-desc-2": "कृपया अपनी खाता सूची जांचें और पुन: प्रयास करें। यदि खाता सूचीबद्ध नहीं है, तो बीज वाक्यांश के साथ पुनर्प्राप्त करने के लिए मौजूदा कुंजियों तक पहुंचें पर जाएं", + "transfers-fetching-failure": "स्थानांतरण इतिहास अपडेट नहीं किया जा सका। अपना कनेक्शन जांचें और पुन: प्रयास करने के लिए नीचे खींचें", + "tribute-required-by-multiaccount": "{{multiaccount-name}} को चैट शुरू करने के लिए SNT की आवश्यकता है।", + "tribute-state-paid": "श्रद्धांजलि अर्पित", + "tribute-state-pending": "श्रद्धांजलि लंबित", + "tribute-state-required": "{{snt-amount}} SNT श्रद्धांजलि की आवश्यकता है", + "tribute-to-talk": "बात करने के लिए श्रद्धांजलि", + "tribute-to-talk-add-friends": "श्रद्धांजलि भुगतान के बिना चैट की अनुमति देने के लिए मित्रों को संपर्क के रूप में जोड़ें।", + "tribute-to-talk-are-you-friends": "क्या तुम दोस्त हो?", + "tribute-to-talk-ask-to-be-added": "संपर्क के रूप में जोड़े जाने के लिए कहें", + "tribute-to-talk-contact-received-your-tribute": "आपकी श्रद्धांजलि प्राप्त की। अब आप सुरक्षित रूप से एक दूसरे के साथ चैट कर सकते हैं।", + "tribute-to-talk-desc": "नए लोगों के लिए चैट शुरू करने के लिए एसएनटी की आवश्यकता के द्वारा अपना ध्यान मुद्रीकृत करें", + "tribute-to-talk-disabled": "टॉक टू ट्रिब्यूट डिसेबल्ड", + "tribute-to-talk-disabled-note": "अब से, नए लोग बिना SNT भेजे आपके साथ चैट शुरू कर सकते हैं।", + "tribute-to-talk-enabled": "आपके पास ट्रिब्यूट टू टॉक सक्षम है।", + "tribute-to-talk-finish-desc": "अब से, आप केवल संपर्कों और भुगतान करने वाले लोगों से ही चैट प्राप्त करेंगे", + "tribute-to-talk-learn-more-1": "आपका समय और ध्यान आपकी सबसे मूल्यवान संपत्ति हैं। टॉक टू ट्रिब्यूट आपको नए लोगों के लिए आपके साथ चैट शुरू करने के लिए आवश्यक एसएनटी की मात्रा निर्धारित करने देता है।", + "tribute-to-talk-learn-more-2": "जो कोई भी आपकी संपर्क सूची में नहीं है, उसे भुगतान करने के लिए कहा जाएगा, और उनके पास होने पर आप जवाब दे सकते हैं।", + "tribute-to-talk-learn-more-3": "आप हमेशा पैसे वापस भेज सकते हैं, लेकिन यह सुनिश्चित करने के लिए कि मित्र आप तक स्वतंत्र रूप से पहुंच सकें, पहले उन्हें संपर्क के रूप में जोड़ें।", + "tribute-to-talk-paywall-learn-more-1": "हमारा समय और ध्यान हमारी सबसे मूल्यवान संपत्ति है। ट्रिब्यूट टू टॉक आपको एसएनटी भुगतान के बदले नए लोगों से संपर्क करने देता है।", + "tribute-to-talk-paywall-learn-more-2": "किसी ऐसे व्यक्ति के साथ चैट शुरू करने के लिए जिसके पास ट्रिब्यूट सेट है, बस आवश्यक एसएनटी का भुगतान करें और आपको एक संपर्क के रूप में जोड़ा जाएगा।", + "tribute-to-talk-paywall-learn-more-3": "यदि आप उन्हें जानते हैं, तो आप मुफ्त में जोड़े जाने के लिए अपनी प्रोफ़ाइल Status के बाहर साझा कर सकते हैं।", + "tribute-to-talk-pending": "श्रद्धांजलि लंबित पुष्टि", + "tribute-to-talk-pending-note": "श्रद्धांजलि लेनदेन की पुष्टि नेटवर्क पर लंबित है। आप लेन-देन इतिहास में इसकी स्थिति की जांच कर सकते हैं", + "tribute-to-talk-removing-note": "ट्रिब्यूट टू टॉक को हटाने से नए लोग बिना एसएनटी भेजे चैट शुरू कर सकेंगे। लेन-देन करने की आवश्यकता है।", + "tribute-to-talk-set-snt-amount": "नए लोगों के लिए चैट शुरू करने के लिए आवश्यक एसएनटी की मात्रा निर्धारित करें", + "tribute-to-talk-signing": "लेन-देन पर हस्ताक्षर करने की प्रतीक्षा कर रहा है", + "tribute-to-talk-transaction-failed-note": "लेन-देन विफल हो गया है और आपकी ट्रिब्यूट टू टॉक सेटिंग को नहीं बदला गया है", + "tribute-to-talk-tribute-received1": "श्रद्धांजलि मिली। आप और", + "tribute-to-talk-tribute-received2": "अब संपर्क हैं और एक दूसरे के साथ सुरक्षित रूप से चैट कर सकते हैं।", + "tribute-to-talk-you-require-snt": "नए लोगों के लिए चैट शुरू करने के लिए आपको SNT की आवश्यकता होती है।", + "try-again": "पुनः प्रयास करें", + "try-keeping-the-card-still": "कार्ड को स्थिर रखने का प्रयास करें", + "turn-nfc-description": "आपके डिवाइस पर NFC अक्षम है। आप इसे सेटिंग में सक्षम कर सकते हैं", + "turn-nfc-on": "जारी रखने के लिए NFC चालू करें", + "two-minutes": "दो मिनट", + "tx-fail-description1": "यह लेनदेन विफल होने की संभावना है। कस्टम नेटवर्क शुल्क का उपयोग करके अपने जोखिम पर साइन इन करें।", + "tx-fail-description2": "यह लेनदेन विफल होने की संभावना है। अपने जोखिम पर हस्ताक्षर करने के लिए एक कस्टम नेटवर्क शुल्क निर्धारित करें।", + "type": "प्रकार", + "type-a-message": "संदेश", + "ulc-enabled": "यूएलसी सक्षम", + "unable-to-fetch": "चैट इतिहास लाने में असमर्थ", + "unable-to-read-this-code": "इस कोड को पढ़ने में असमर्थ", + "unable-to-send-messages": "संदेश भेजने और प्राप्त करने में असमर्थ", + "unblock": "अनब्लॉक", + "unblock-contact": "इस उपयोगकर्ता को अनब्लॉक करें", + "undo": "पूर्ववत", + "unknown-status-go-error": "अज्ञात स्थिति-जाने त्रुटि", + "unlimited": "असीमित", + "unlock": "अनलॉक", + "unmute": "अनम्यूट", + "unmute-chat": "चैट अनम्यूट करें", + "unmute-community": "समुदाय अनम्यूट करें", + "unmute-group": "समूह अनम्यूट करें", + "unpair-card": "अनपेयर कार्ड", + "unpair-card-confirmation": "यह कार्रवाई मौजूदा डिवाइस से कार्ड को अलग कर देगी। 6-अंकीय पासकोड प्राधिकरण की आवश्यकता है। क्या आप आगे बढ़ना चाहते हैं?", + "unpair-keycard": "इस फ़ोन से कीकार्ड अनपेयर करें", + "unpair-keycard-warning": "आपका पेयरिंग कोड\/PUK और पिन अपरिवर्तित रहता है", + "unpaired-keycard-text": "आपके द्वारा टैप किया गया कीकार्ड इस फ़ोन से संबद्ध नहीं है", + "unpaired-keycard-title": "ऐसा लगता है कि आपका कार्ड अयुग्मित कर दिया गया है", + "unpin": "अनपिन", + "unpin-from-channel": "चैनल से अनपिन करें", + "unpin-from-chat": "चैट से अनपिन करें", + "untrustworthy": "अविश्वसनीय चिह्नित करें", + "update": "अपडेट करना", + "update-to-listen-audio": "यहां ऑडियो संदेश सुनने के लिए नवीनतम संस्करण में अपडेट करें!", + "update-to-see-image": "यहां एक अच्छी छवि देखने के लिए नवीनतम संस्करण में अपडेट करें!", + "update-to-see-sticker": "यहां एक अच्छा स्टिकर देखने के लिए नवीनतम संस्करण में अपडेट करें!", + "updates-to-tos": "उपयोग की शर्तों के अपडेट", + "updates-to-tos-desc": "जारी रखने से पहले, कृपया उपयोग की शर्तों की समीक्षा करें और पुष्टि करें कि आप ऐप का उपयोग कैसे करते हैं, इसकी पूरी जिम्मेदारी लेते हैं।", + "url": "यूआरएल", + "usd-currency": "USD", + "use-as-profile-picture": "प्रोफ़ाइल चित्र के रूप में उपयोग करें", + "use-valid-contact-code": "कृपया कोई मान्य चैट कुंजी या उपयोगकर्ता नाम दर्ज करें या स्कैन करें", + "validation-amount-invalid-number": "राशि कोई मान्य संख्या नहीं है", + "validation-amount-is-too-precise": "राशि बहुत सटीक है। दशमलव की अधिकतम संख्या {{decimals}} ।", + "verified-community": "सत्यापित समुदाय", + "version": "एप्लिकेशन वेरीज़न", + "view": "देखना", + "view-community-rules": "समुदाय नियम देखें", + "view-cryptokitties": "क्रिप्टोकरंसी में देखें", + "view-cryptostrikers": "क्रिप्टोस्ट्राइकर्स में देखें", + "view-data": "डेटा देखें", + "view-details": "विवरण देखें", + "view-etheremon": "एथेरमोन में देखें", + "view-gitcoin": "गिटकोइन में देखें", + "view-members": "सदस्यों को देखें", + "view-on-opensea": "ओपनसी पर देखें", + "view-profile": "प्रोफ़ाइल देखें", + "view-public-dashboard": "सार्वजनिक डैशबोर्ड देखें", + "view-rules": "नियम देखें", + "view-signing": "हस्ताक्षर वाक्यांश देखें", + "view-superrare": "सुपर रेयर में देखें", + "vote-to-feature": "इस समुदाय को दिखाने के लिए वोट करें", + "waiting-for-wifi": "कोई वाई-फ़ाई नहीं, संदेश समन्वयन अक्षम है.", + "waiting-for-wifi-change": "समायोजन", + "waiting-to-sign": "लेन-देन पर हस्ताक्षर करने के लिए प्रतीक्षारत...", + "waiting-wi-fi": "वाई-फ़ाई की प्रतीक्षा की जा रही है…", + "waku-bloom-filter-mode": "वाकू ब्लूम फ़िल्टर मोड", + "wakuv2-change-nodes": "क्या आप वाकई Wakuv2 नोड्स बदलना चाहते हैं?", + "wakuv2-node-format": "\/आईपी4\/ {node-ip} \/टीसीपी\/ {port} \/पी2पी\/ {id}", + "wakuv2-settings": "वाकू v2 सेटिंग्स", + "wallet": "बटुआ", + "wallet-address": "वॉलेट का पता", + "wallet-asset": "संपत्ति", + "wallet-assets": "संपत्तियां", + "wallet-backup-recovery-title": "अपने बीज वाक्यांश का बैकअप लें", + "wallet-choose-recipient": "प्राप्तकर्ता चुनें", + "wallet-collectibles": "संग्रह", + "wallet-connect": "वॉलेट कनेक्ट", + "wallet-connect-2.0": "वॉलेट कनेक्ट 2.0", + "wallet-connect-app-connected": "जुड़ा हुआ है", + "wallet-connect-go-back": "अपने ब्राउज़र या dapp पर वापस जाएँ", + "wallet-connect-proposal-description": "कनेक्ट करके आप {{name}} को अपना खाता पता पुनर्प्राप्त करने और Web3 को सक्षम करने की अनुमति देते हैं", + "wallet-connect-proposal-title": "अपने वॉलेट से जुड़ना चाहेंगे", + "wallet-insufficient-funds": "अपर्याप्त कोष", + "wallet-insufficient-gas": "गैस के लिए पर्याप्त ईटीएच नहीं", + "wallet-invalid-address": "अमान्य पता:\n {{data}}", + "wallet-invalid-address-checksum": "पते में त्रुटि:\n {{data}}", + "wallet-invalid-chain-id": "नेटवर्क मेल नहीं खाता:\n {{data}} लेकिन वर्तमान श्रृंखला {{chain}}", + "wallet-key-content": "एथेरियम मानक पर आधारित और 0x से शुरू होने वाला 64 वर्ण का हेक्स पता। जब आप धन प्राप्त करना चाहते हैं, तो सार्वजनिक रूप से, आपके खाते का पता दूसरों के साथ साझा किया जाता है। इसे \"एथेरियम एड्रेस\" या \"वॉलेट एड्रेस\" के रूप में भी जाना जाता है।", + "wallet-key-title": "खाते का पता", + "wallet-manage-accounts": "खाते प्रबंधित करें", + "wallet-manage-app-connections": "ऐप्लिकेशन कनेक्शंस प्रबंधित करें", + "wallet-manage-assets": "संपत्ति प्रबंधित करें", + "wallet-request": "अनुरोध", + "wallet-send": "भेजना", + "wallet-send-min-units": "न्यूनतम 21000 इकाइयां", + "wallet-send-min-wei": "न्यूनतम 1 वी", + "wallet-settings": "वॉलेट सेटिंग", + "wallet-total-value": "कुल मूल्य", + "wallet-transaction-total-fee": "कुल शुल्क", + "wants-to-access-profile": "आपकी प्रोफ़ाइल तक पहुंचना चाहता है", + "warning": "चेतावनी", + "warning-message": "क्षमा करें, हम स्पैम को रोकने के लिए एक के बाद एक कई संदेश भेजने को सीमित करते हैं। कृपया एक क्षण में पुन: प्रयास करें", + "warning-sending-to-contract-descr": "आपके द्वारा दर्ज किया गया पता एक स्मार्ट अनुबंध है, इस पते पर धनराशि भेजने से धन की हानि हो सकती है। डीएपी के साथ बातचीत करने के लिए, Status डीएपी ब्राउज़र में डीएपी खोलें।", + "watch-only": "घड़ी-केवल", + "wc-brand-guide": "ट्रेडमार्क और लोगो जैसे ब्रांडिंग का उपयोग करने पर मार्गदर्शन", + "wc-disclaimer": "अस्वीकरण (तृतीय पक्ष प्रदाताओं सहित), वारंटी, और कानूनी रिलीज", + "wc-dispute": "विवाद समाधान प्रावधान", + "wc-how-to-use-status-app": "गोपनीयता और सुरक्षा सहित स्टेटस ऐप का उपयोग कैसे करें", + "wc-new-tos-based-on-principles-prefix": "हमारे के आधार पर डिज़ाइन की गई उपयोग की नई शर्तें", + "web-view-error": "पेज लोड करने में असमर्थ", + "websites": "वेबसाइटें", + "webview-camera-permission-requests": "वेबव्यू कैमरा अनुमति अनुरोध", + "webview-camera-permission-requests-subtitle": "सक्षम होने पर, वेबसाइट और डैप आपके कैमरे का उपयोग करने के लिए कह सकते हैं", + "welcome-blank-community-message": "आपके समुदाय यहां दिखाई देंगे.", + "welcome-blank-message": "आपकी चैट यहां दिखाई देंगी। नई चैट शुरू करने के लिए बटन दबाएं", + "welcome-community-blank-message": "आपके चैनल यहां दिखाई देंगे. एक नया चैनल बनाने के लिए, बटन पर क्लिक करें और \"चैनल बनाएं\" चुनें", + "welcome-community-blank-message-edit-chats": "आपके चैनल यहां दिखाई देंगे. एक नया चैनल बनाने के लिए, सामुदायिक स्क्रीन पर वापस जाएं, बटन पर क्लिक करें और \"चैनल बनाएं\" चुनें", + "welcome-screen-text": "अपना वॉलेट सेट करें, दोस्तों को चैट करने के लिए आमंत्रित करें\n और लोकप्रिय डैप ब्राउज़ करें!", + "welcome-to-status": "Status में आपका स्वागत है!", + "welcome-to-status-description": "अपना क्रिप्टो वॉलेट सेट करें, दोस्तों को चैट करने और विकेन्द्रीकृत ऐप्स ब्राउज़ करने के लिए आमंत्रित करें", + "what-changed": "किया बदल गया", + "what-is-shared": "क्या साझा किया जाता है", + "whats-on-your-mind": "आपके दिमाग में क्या है…", + "whats-trending": "देखें कि क्या चलन में है", + "word-count": "शब्द गणना", + "word-n": "शब्द # {{number}}", + "word-n-description": "यह जांचने के लिए कि क्या आपने अपने बीज वाक्यांश का सही ढंग से बैकअप लिया है, ऊपर # {{number}} शब्द दर्ज करें।", + "words-n": { + "one": "1 word", + "other": "{{count}} words" + }, + "write-down-and-store-securely": "कोड नीचे लिखें\n और उन्हें सुरक्षित रूप से स्टोर करें", + "wrong-address": "गलत पता", + "wrong-card": "गलत कार्ड", + "wrong-card-text": "टैप किया गया कार्ड आपके द्वारा चुनी गई कुंजियों के अनुरूप नहीं है", + "wrong-contract": "गलत अनुबंध", + "wrong-keycard-text": "आपके द्वारा टैप किया गया कीकार्ड इस फ़ोन से संबद्ध नहीं है", + "wrong-keycard-title": "ऐसा लगता है कि आपने टैप किया है\n एक गलत कीकार्ड", + "wrong-password": "गलत पासवर्ड", + "wrong-word": "गलत शब्द", + "yes": "हां", + "you": "तुम", + "you-already-have-an-asset": "आपके पास पहले से ही एक संपत्ति है {{value}}", + "you-are-all-set": "तुम सब सेट हो!", + "you-are-all-set-description": "यदि आप अपना फोन खो देते हैं, तो अब आप अपने बीज वाक्यांश का उपयोग करके अपने फंड और चैट कुंजी तक पहुंच सकते हैं", + "you-can-change-account": "आप खाते का नाम और रंग अपनी इच्छानुसार बदल सकते हैं", + "you-can-choose-preview-websites": "आप चुन सकते हैं कि निम्नलिखित में से कौन सी वेबसाइट चैट में विवरण और चित्रों के लिंक का पूर्वावलोकन कर सकती है", + "you-can-fetch": "आप चैट इतिहास ला सकते हैं", + "you-dont-have-contacts": "आपके पास अभी तक कोई संपर्क नहीं है।", + "you-dont-have-contacts-invite-friends": "आपके पास अभी तक कोई संपर्क नहीं है।\n चैट शुरू करने के लिए अपने दोस्तों को आमंत्रित करें।", + "you-dont-have-stickers": "आपके पास अभी तक कोई स्टिकर नहीं है", + "you-must-always-hold": "आपको हमेशा धारण करना चाहिए:", + "you-must-hold": "आपको धारण करना चाहिए:", + "you-must-now-hold": "अब आपको धारण करना चाहिए:", + "you-will-need-this-code": "Status खोलने और लेन-देन पर हस्ताक्षर करने के लिए आपको इस कोड की आवश्यकता होगी", + "you-will-start-from-scratch": "आप नए सिरे से चाबियों के एक नए सेट के साथ शुरुआत करेंगे", + "your-card-is-frozen": "आपका कीकार्ड फ़्रीज़ हो गया है। कार्ड एक्सेस रीसेट करें", + "your-contact-code": "पहुँच प्रदान करना इस DApp को आपकी चैट कुंजी पुनः प्राप्त करने के लिए अधिकृत करता है", + "your-data-belongs-to-you": "यदि आप अपना बीज वाक्यांश खो देते हैं तो आप अपना डेटा और धन खो देते हैं", + "your-data-belongs-to-you-description": "यदि आप पहुंच खो देते हैं, उदाहरण के लिए अपना फोन खो देने से, आप केवल अपने बीज वाक्यांश के साथ अपनी कुंजियों तक पहुंच सकते हैं। कोई नहीं, लेकिन आपके पास अपना बीज वाक्यांश है। नीचे लिखें। उसे सुरक्षित रखें", + "your-keys": "आपकी चाभियां", + "your-price-limit": "आपकी कीमत सीमा", + "your-recovery-phrase": "आपका बीज वाक्यांश", + "your-recovery-phrase-description": "यह आपका बीज वाक्यांश है। आप इसका उपयोग यह साबित करने के लिए करते हैं कि यह आपका बटुआ है। आप इसे केवल एक बार देख सकते हैं! इसे कागज पर लिखकर सुरक्षित स्थान पर रख दें। यदि आप अपना बटुआ खो देते हैं या पुनः स्थापित करते हैं तो आपको इसकी आवश्यकता होगी।", + "your-tip-limit": "आपकी टिप सीमा", + "youre-on-mobile-network": "आप मोबाइल नेटवर्क पर हैं" +} diff --git a/translations/hu.json b/translations/hu.json new file mode 100644 index 00000000000..21b4750509c --- /dev/null +++ b/translations/hu.json @@ -0,0 +1,127 @@ +{ + "confirm": "Megerősít", + "amount": "Összeg", + "members-active": { + "one": "1 tag, 1 aktív", + "other": "{{count}} tag, {{count}} aktív", + "zero": "nincsenek tagok" + }, + "chat-name": "Csevegés neve", + "phew-here-is-your-passphrase": "*Hűha* ez nehéz volt, de kész a jelmondatod, *írd fel valahova és vigyázz rá!* Szükséged lesz hozzá felhasználói fiókod helyreállításához.", + "public-group-topic": "Téma", + "chat-settings": "Csevegés beállítások", + "offline": "Offline", + "invited": "meghívott", + "address": "Cím", + "new-public-group-chat": "Csatlakozás nyilvános csevegéshez", + "datetime-hour": { + "one": "óra", + "other": "óra" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "Eltávolítás", + "add-members": "Tagok hozzáadása", + "done": "Kész", + "close-chat": "Csevegés törlése", + "new-group-chat": "Új csoportos csevegés", + "sign-in": "Bejelentkezés", + "datetime-yesterday": "tegnap", + "create-new-account": "Új fiók létrehozása", + "datetime-ago": "ezelőtt", + "contacts": "Kapcsolatok", + "got-it": "Értem", + "active-online": "Online", + "password": "Jelszó", + "browsing-title": "Böngészés", + "discover": "Felfedezés", + "browsing-cancel": "Mégse", + "intro-status": "Csevegj velem felhasználói fiókod létrehozásáról és beállításaid megváltoztatásáról!", + "name": "Név", + "show-qr": "QR mutatása", + "connect": "Kapcsolódás", + "edit": "Szerkesztés", + "account-generation-message": "Adj egy percet, varázsolok egy kicsit és létre is hozom a felhasználói fiókodat!", + "no-messages": "Nincsenek üzenetek", + "passphrase": "Jelmondat", + "recipient": "Címzett", + "members-title": "Tagok", + "new-group": "Új csoport", + "phone-e164": "1. nemzetközi", + "settings": "Beállítások", + "chats": "Csevegések", + "transaction": "Tranzakció", + "public-group-status": "Nyilvános", + "image-source-make-photo": "Rögzítés", + "start-conversation": "Beszélgetés indítása", + "save": "Mentés", + "sharing-copy-to-clipboard": "Másolás vágólapra", + "sync-in-progress": "Szinkronizálás...", + "send-transaction": "Tranzakció küldése", + "incorrect-code": [ + "str", + "Sajnáljuk, hibás kód, kérjük, add meg újból" + ], + "image-source-gallery": "Kiválasztás a galériából", + "sync-synced": "Szinkronizálás folyamatban", + "status-pending": "Függőben levő", + "datetime-day": { + "one": "nap", + "other": "nap" + }, + "scan-qr": "QR beolvasása", + "contact-s": { + "one": "névjegy", + "other": "névjegyek" + }, + "next": "Következő", + "recent": "Legutóbbi", + "status": "Állapot", + "from": "Feladó", + "wrong-password": "Hibás jelszó", + "in-contacts": "Névjegyekben", + "sharing-share": "Megosztás...", + "type-a-message": "Írj egy üzenetet...", + "clear-history": "Előzmények törlése", + "no-contacts": "Még nincsenek kapcsolatok", + "datetime-today": "ma", + "web-view-error": "hoppá, hiba", + "error": "Hiba", + "more": "több", + "cancel": "Mégse", + "can-not-add-yourself": "Magadat nem adhatod hozzá", + "add-to-contacts": "Hozzáadás a kapcsolatokhoz", + "available": "Elérhető", + "You": "Te", + "main-wallet": "Fő zseb", + "members": { + "one": "1 tag", + "other": "{{count}} tag", + "zero": "nincsenek tagok" + }, + "intro-message1": "Üdv az Állapotnál\nÉrints erre a üzenetre, állítsd be a jelszavad és fogj hozzá!", + "new-contact": "Új kapcsolat", + "datetime-second": { + "one": "másodperc", + "other": "másodperc" + }, + "recover": "Visszaállítás", + "datetime-minute": { + "one": "perc", + "other": "perc" + }, + "browsing-open-in-android-web-browser": "Megnyitás új böngészőben", + "browsing-open-in-ios-web-browser": "Megnyitás új böngészőben", + "edit-profile": "Profil szerkesztése", + "active-unknown": "Ismeretlen", + "public-key": "Nyilvános kulcs", + "profile": "Profil", + "none": "Semmi", + "removed": "eltávolított", + "message": "Üzenet", + "here-is-your-passphrase": "Kész a jelmondatod, *írd fel valahová és őrizd meg!* Szükséged lesz hozzá felhasználói fiókod helyreállításához.", + "image-source-title": "Profilkép", + "left": "maradt", + "to": "Címzett", + "status-sent": "Elküldve", + "data": "Adatok" +} diff --git a/translations/id.json b/translations/id.json new file mode 100644 index 00000000000..098dab7fcc6 --- /dev/null +++ b/translations/id.json @@ -0,0 +1,1418 @@ +{ + "You": "Kamu", + "about-app": "Tentang", + "about-key-storage-content": "Status tidak akan pernah mengakses kunci pribadi Anda. Pastikan untuk mencadangkan frase seed Anda. Jika Anda kehilangan ponsel, itu adalah satu-satunya cara untuk mengakses kunci Anda.", + "about-key-storage-title": "Tentang penyimpanan kunci", + "about-names-content": "Tidak ada yang bisa berpura-pura menjadi Anda! Anda anonim secara default dan tidak perlu mengungkapkan nama asli Anda. Anda dapat mendaftarkan nama khusus dengan sedikit biaya.", + "about-names-title": "Nama tidak dapat diubah", + "about-sharing-data": "Tentang berbagi data", + "about-sharing-data-subtitle": "Sebagai proyek komunitas open-source, Status meminta Anda untuk membagikan data anonim dan non-invasif untuk meningkatkan aplikasi. Ini sangat memilih, dan dapat diubah kapan saja di Pengaturan.", + "accept": "Menerima", + "accept-and-add": "Terima dan tambahkan", + "accept-and-share-address": "Terima dan bagikan alamat", + "accept-new-chats-from": "Terima obrolan baru dari", + "access-existing-keys": "Akses kunci yang ada", + "access-key": "Kunci akses", + "account-added": "Akun ditambahkan", + "account-color": "Warna akun", + "account-content": "Anda dapat membandingkan akun dalam Status dengan rekening bank. Seperti halnya rekening bank, akun biasanya memiliki alamat dan saldo; Anda menggunakan akun ini untuk bertransaksi di Ethereum. Anda dapat memiliki beberapa akun di dompet Anda. Semua diakses dengan membuka Status.", + "account-exists-title": "Akun sudah ada", + "account-name": "Nama akun", + "account-settings": "Pengaturan akun", + "account-title": "Akun", + "accounts": "Akun", + "active-online": "Online", + "active-unknown": "Tidak dikenal", + "add": "Menambahkan", + "add-a-watch-account": "Tambahkan alamat khusus arloji", + "add-account": "Tambahkan akun", + "add-account-description": "Anda dapat mengimpor segala jenis akun Ethereum untuk menambahkannya ke dompet Status Anda", + "add-account-incorrect-password": "Kata sandi sepertinya salah. Masukkan kata sandi yang Anda gunakan untuk membuka kunci aplikasi.", + "add-an-account": "Tambahkan akun", + "add-bootnode": "Tambahkan bootnode", + "add-contact": "Tambah kontak", + "add-custom-token": "Tambahkan token khusus", + "add-mailserver": "Tambahkan simpul sejarah", + "add-members": "Tambahkan anggota", + "add-network": "Tambahkan jaringan", + "add-new-contact": "Tambahkan kontak baru", + "add-nickname": "Tambahkan nama panggilan (opsional)", + "add-private-key-account": "Tambahkan akun dari kunci pribadi", + "add-seed-account": "Tambahkan akun dengan frase awal", + "add-to-contacts": "Tambahkan ke Kontak", + "add-to-contacts-text": "Dengan menambahkan pengguna ke daftar kontak Anda, Anda membagikan alamat dompet Anda", + "add-to-favourites": "Tambahkan ke favorit", + "add-watch-account": "Tambahkan akun khusus arloji", + "address": "Alamat", + "address-or-ens-name": "Alamat atau nama ENS", + "address-received": "Alamat diterima", + "address-request-accepted": "Permintaan alamat diterima", + "address-requested": "Alamat yang diminta", + "advanced": "Maju", + "advanced-settings": "Pengaturan lanjutan", + "advertiser-description": "Anda telah menemukan Status berkat mitra. Apakah Anda keberatan jika Status memeriksa alamat IP Anda sekali sehingga mendapat imbalan? Informasi ini tidak akan digunakan untuk hal lain dan akan dihapus sepenuhnya setelah 7 hari.", + "advertiser-starter-pack-accept": "Menerima", + "advertiser-starter-pack-decline": "Menurun", + "advertiser-starter-pack-description": "Inilah beberapa crypto untuk Anda mulai! Gunakan untuk mendapatkan stiker, nama ENS, dan coba dapps", + "advertiser-starter-pack-title": "Paket awal", + "advertiser-title": "Privasi secara default", + "agree-by-continuing": "Dengan melanjutkan, Anda setuju \nuntuk kita", + "all": "Semua", + "allow": "Mengizinkan", + "allow-and-send": "Perbolehkan dan kirim", + "allow-mention-notifications": "Tampilkan @ sebutan", + "allowing-authorizes-this-dapp": "Mengizinkan otorisasi DApp ini untuk mengambil alamat dompet Anda dan mengaktifkan Web3", + "already-have-asset": "Anda sudah memiliki aset ini", + "amount": "Jumlah", + "anyone": "Siapa saja", + "appearance": "Penampilan", + "apply": "Menerapkan", + "are-you-sure": "Apakah kamu yakin?", + "are-you-sure-description": "Anda tidak akan dapat melihat seluruh frase seed lagi", + "are-you-sure-to-cancel": "Anda yakin ingin membatalkan?", + "are-you-sure?": "Apakah kamu yakin?", + "ask-in-status": "Ajukan pertanyaan atau laporkan bug", + "at": "at", + "attribution-received": "{{attrib}} dari {{max}} bonus diterima", + "audio": "Audio", + "audio-recorder": "Perekam", + "audio-recorder-error": "Kesalahan perekam", + "audio-recorder-max-ms-reached": "Waktu perekaman maksimum tercapai", + "audio-recorder-permissions-error": "Anda harus memberikan izin untuk mengirim pesan audio", + "authorize": "Mengizinkan", + "available": "Tersedia", + "available-participants": { + "other": "Anda dapat memilih {{count}} lebih banyak peserta" + }, + "back": "Kembali", + "back-up-seed-phrase": "Cadangkan phrase seed", + "back-up-your-seed-phrase": "Cadangkan frase seed Anda", + "backup-disabled": "Dengan disabilitas", + "backup-recovery-phrase": "Cadangkan phrase seed", + "balance": "Keseimbangan", + "begin-set-up": "Mulai pengaturan", + "biometric-auth-android-sensor-desc": "Sensor sentuh", + "biometric-auth-android-sensor-error-desc": "Gagal", + "biometric-auth-android-title": "Diperlukan Otentikasi", + "biometric-auth-confirm-logout": "Masuk kembali", + "biometric-auth-confirm-message": "Otentikasi biometrik diperlukan untuk melanjutkan, jika tidak memungkinkan silakan buka kunci Anda dengan kata sandi atau kode sandi Anda", + "biometric-auth-confirm-title": "Anda harus mengautentikasi!", + "biometric-auth-confirm-try-again": "Coba lagi", + "biometric-auth-error": "Tidak dapat melakukan otentikasi biometrik ( {{code}} )", + "biometric-auth-login-error-title": "Kesalahan otentikasi biometrik", + "biometric-auth-login-ios-fallback-label": "Masukkan kata kunci", + "biometric-auth-reason-login": "Masuk dalam Status", + "biometric-auth-reason-verify": "Verifikasi otentikasi", + "biometric-disable-bioauth": "nonaktifkan {{bio-type-label}}", + "biometric-disable-password-description": "Jika Anda menonaktifkan ini, Anda juga akan", + "biometric-disable-password-title": "Nonaktifkan penyimpanan kata sandi", + "biometric-enable": "Jika Anda tidak ingin memasukkan kata sandi setiap kali mengakses aplikasi, aktifkan {{bio-type-label}} masuk", + "biometric-enable-button": "Aktifkan {{bio-type-label}}", + "biometric-enable-keycard": "Jika Anda tidak ingin menggunakan Kartu Kunci setiap kali mengakses aplikasi, aktifkan {{bio-type-label}} masuk", + "biometric-faceid": "ID Wajah", + "biometric-fingerprint": "Sidik jari", + "biometric-secure-with": "Amankan dengan {{bio-type-label}}", + "biometric-touchid": "ID Sentuhan", + "bip39-password-placeholder": "Kata sandi BIP39", + "blank-keycard-text": "Anda dapat melanjutkan dengan kartu kunci setelah membuat kunci dan nama", + "blank-keycard-title": "Sepertinya Anda sudah mengetuk \nkartu kunci kosong", + "block": "Blok", + "block-contact": "Blokir pengguna ini", + "block-contact-details": "Pemblokiran akan menghapus pesan pengguna sebelumnya dan menghentikan pesan baru untuk menghubungi Anda", + "blocked-users": "Pengguna diblokir", + "bootnode-address": "Alamat bootnode", + "bootnode-details": "Detail bootnode", + "bootnode-format": "enode: \/\/ {enode-id} @ {ip-address} : {port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes diaktifkan", + "bootnodes-settings": "Pengaturan bootnodes", + "browsed-websites": "Riwayat browser akan muncul di sini", + "browser": "Browser", + "browser-not-secure": "Koneksi tidak aman! Jangan menandatangani transaksi atau mengirim data pribadi di situs ini.", + "browser-secure": "Koneksi aman. Pastikan Anda benar-benar mempercayai situs ini sebelum menandatangani transaksi atau memasukkan data pribadi.", + "browsers": "Browser", + "browsing-cancel": "Membatalkan", + "browsing-open-in-android-web-browser": "Buka di Android", + "browsing-open-in-ios-web-browser": "Buka di iOS", + "browsing-open-in-status": "Buka di Status", + "browsing-site-blocked-description1": "Kami mendeteksi kemungkinan aktivitas jahat dari alamat ini. Untuk melindungi Anda dan dompet Anda, kami mencegah navigasi lebih lanjut. \n\nJika menurut Anda ini adalah kesalahan, beri tau kami di", + "browsing-site-blocked-description2": "obrolan publik.", + "browsing-site-blocked-go-back": "Kembali", + "browsing-site-blocked-title": "Situs ini diblokir", + "browsing-title": "Telusuri", + "bug-report": "Laporkan bug", + "buy-crypto": "Beli crypto", + "buy-crypto-choose-a-service": "Pilih layanan yang ingin Anda gunakan untuk membeli crypto", + "buy-crypto-leaving": "Anda meninggalkan Status dan memasuki situs web pihak ketiga untuk menyelesaikan pembelian Anda", + "camera-access-error": "Untuk memberikan izin kamera yang diperlukan, buka pengaturan sistem Anda dan pastikan Status > Kamera dipilih.", + "can-not-add-yourself": "Itu Anda, untuk memulai obrolan pilih orang lain", + "can-send-messages": "Anda dapat mengirim dan menerima pesan baru", + "cancel": "Membatalkan", + "cancel-keycard-setup": "Batalkan pengaturan kartu kunci", + "cancelling": "Membatalkan", + "cannot-read-card": "Tidak dapat membaca kartu. \nHarap pegang di bagian belakang ponsel Anda", + "cannot-use-default-pin": "Kode akses 000000 tidak diizinkan. \n Silakan gunakan nomor lain", + "cant-open-public-chat": "Tidak dapat membuka obrolan publik", + "cant-report-bug": "Tidak dapat melaporkan bug", + "card-is-blank": "Kartu ini kosong", + "card-reseted": "Kartu telah diatur ulang", + "card-unpaired": "Kartu tidak berpasangan dari perangkat saat ini", + "change-fleet": "Ubah armada ke {{fleet}}", + "change-logging-enabled": "Anda yakin ingin {{enable}} masuk?", + "change-pairing": "Ubah kode pasangan", + "change-pairing-description": "Mengubah kode pemasangan tidak memengaruhi pemasangan saat ini. Namun, setiap pasangan baru akan membutuhkan kode baru.", + "change-pairing-title": "Buat kode pasangan baru", + "change-passcode": "Ubah Kode Sandi", + "change-password": "Ganti kata sandi", + "change-pin": "Ubah kode sandi 6 digit", + "change-puk": "Ubah PUK 12 digit", + "changed-amount-warning": "Jumlah diubah dari {{old}} menjadi {{new}}", + "changed-asset-warning": "Aset diubah dari {{old}} menjadi {{new}}", + "chaos-mode": "Mode kekacauan", + "chaos-unicorn-day": "Chaos Unicorn Day", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Obrolan", + "chat-and-transact": "Mengobrol dan bertransaksi secara pribadi dengan teman", + "chat-is-a-contact": "Kontak", + "chat-is-not-a-contact": "Bukan kontak", + "chat-key": "Kunci obrolan", + "chat-key-content": "Pesan pada protokol obrolan Status dikirim dan diterima menggunakan kunci enkripsi. Kunci obrolan publik adalah serangkaian karakter yang Anda bagikan dengan orang lain sehingga mereka dapat mengirimi Anda pesan dalam Status.", + "chat-key-title": "Kunci Obrolan", + "chat-link-previews": "Pratinjau tautan obrolan", + "chat-name": "Nama obrolan", + "chat-name-content": "Tiga kata acak, diturunkan secara algoritmik dari kunci obrolan Anda dan digunakan sebagai alias default Anda dalam obrolan. Nama obrolan sepenuhnya unik; tidak ada pengguna lain yang memiliki tiga kata yang sama.", + "chat-name-title": "Nama Obrolan", + "chat-settings": "Pengaturan Chat", + "chats": "Obrolan", + "check-on-opensea": "Periksa opensea", + "check-your-recovery-phrase": "Periksa frasa benih Anda", + "choose-authentication-method": "Pilih metode otentikasi", + "clear": "Bersihkan", + "clear-all": "Bersihkan semua", + "clear-history": "Bersihkan riwayat", + "clear-history-action": "Bersihkan", + "clear-history-confirmation": "Hapus riwayat?", + "clear-history-confirmation-content": "Apakah Anda yakin ingin menghapus riwayat obrolan ini?", + "clear-history-title": "Hapus riwayat?", + "close": "Menutup", + "close-app-button": "Konfirmasi", + "close-app-content": "Aplikasi akan berhenti dan ditutup. Ketika Anda membukanya kembali, jaringan yang dipilih akan digunakan", + "close-app-title": "Peringatan!", + "command-button-send": "Kirim", + "communities-enabled": "Komunitas diaktifkan", + "community-image-remove": "Menghapus", + "community-info": "Info komunitas", + "community-invite-title": "Undang", + "community-link": "Tautan komunitas", + "community-members-title": "Anggota", + "community-share-title": "Bagikan", + "complete-hardwallet-setup": "Kartu ini sekarang ditautkan. Anda membutuhkannya untuk menandatangani transaksi dan membuka kunci kunci Anda", + "completed": "Lengkap", + "confirm": "Konfirmasi", + "confirm-new-password": "Konfirmasi kata sandi baru", + "confirm-pairing-code-placeholder": "Konfirmasikan kode pemasanganmu...", + "confirm-password-placeholder": "Konfirmasi password Anda...", + "confirmation-request": "Permintaan konfirmasi", + "confirmations": "Konfirmasi", + "confirmations-helper-text": "Ketika transaksi memiliki 12 konfirmasi, Anda dapat menganggapnya telah selesai.", + "connect": "Menghubung", + "connect-mailserver-content": "Hubungkan ke {{name}} ?", + "connected": "Terhubung", + "connected-to": "Terhubung dengan", + "connecting": "Menghubungkan ...", + "connecting-requires-login": "Menghubungkan ke jaringan lain membutuhkan login", + "connection-status": "Koneksi Status", + "connection-with-the-card-lost": "Koneksi dengan kartu \ntelah hilang", + "connection-with-the-card-lost-setup-text": "Untuk melanjutkan pengaturan, tahan kartu \n bagian belakang ponsel Anda dan rawat \n kartu ke kontak telepon", + "connection-with-the-card-lost-text": "Untuk melanjutkan memegang kartu ke bagian belakang ponsel Anda", + "contact-code": "Kunci obrolan", + "contact-s": { + "other": "kontak" + }, + "contacts": "Kontak", + "contacts-descr": "Kontak Anda akan muncul di sini. Anda akan menerima pembaruan status dari siapa pun yang Anda tambahkan sebagai kontak", + "contacts-empty": "Kontak dengan nama ENS akan muncul di sini", + "continue": "Lanjutkan", + "continue-anyway": "Tetap lanjutkan", + "contract-address": "Alamat kontrak", + "contract-interaction": "Interaksi kontrak", + "copy-info": "Salin info", + "copy-qr": "Salin kode", + "copy-to-clipboard": "Salinan", + "copy-transaction-hash": "Salin ID transaksi", + "cost-fee": "Biaya\/Biaya", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Membuat", + "create-a-pin": "Buat kode sandi 6 digit", + "create-a-puk": "Membuat PUK 12 digit", + "create-group-chat": "Buat obrolan grup", + "create-multiaccount": "Hasilkan kunci", + "create-new-key": "Dapatkan kunci baru", + "create-pin": "Buat kode sandi 6 digit", + "create-pin-description": "Anda akan memerlukan kartu Anda + kode sandi 6 digit ini untuk membuka Status dan mengonfirmasi transaksi", + "created-group-chat-description": "Anda membuat grup {{group-name}}", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Mata uang", + "currency-display-name-aed": "Emirati Dirham", + "currency-display-name-afn": "Afghanistan Afghan", + "currency-display-name-ars": "Peso Argentina", + "currency-display-name-aud": "Dollar Australia", + "currency-display-name-bbd": "Dolar Barbados", + "currency-display-name-bdt": "Taka Bangladesh", + "currency-display-name-bgn": "Lev Bulgaria", + "currency-display-name-bhd": "Dinar Bahrain", + "currency-display-name-bnd": "Dollar Brunei Darussalam", + "currency-display-name-bob": "Bolivia Bolíviano", + "currency-display-name-brl": "Brazil Real", + "currency-display-name-btn": "Ngultrum Bhutan", + "currency-display-name-cad": "Dollar Canada", + "currency-display-name-chf": "Franc Swiss", + "currency-display-name-clp": "Peso Chili", + "currency-display-name-cny": "Renminbi China Yuan", + "currency-display-name-cop": "Peso Kolombia", + "currency-display-name-crc": "Colon Kosta Rika", + "currency-display-name-czk": "Koruna Ceko", + "currency-display-name-dkk": "Denmark Krone", + "currency-display-name-dop": "Peso Republik Dominika", + "currency-display-name-egp": "Pound Mesir", + "currency-display-name-etb": "Birr Etiopia", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Pound Inggris", + "currency-display-name-gel": "Lari Georgia", + "currency-display-name-ghs": "Ghana Cedi", + "currency-display-name-hkd": "Dollar Hongkong", + "currency-display-name-hrk": "Kroasia Kuna", + "currency-display-name-huf": "Forint Hongaria", + "currency-display-name-idr": "Rupiah Indonesia", + "currency-display-name-ils": "Israel Shekel", + "currency-display-name-inr": "Rupee India", + "currency-display-name-isk": "Krona Islandia", + "currency-display-name-jmd": "Dollar Jamaika", + "currency-display-name-jpy": "Yen jepang", + "currency-display-name-kes": "Shilling Kenya", + "currency-display-name-krw": "Korea (Selatan) Won", + "currency-display-name-kwd": "Kuwait Dinar", + "currency-display-name-kzt": "Tenge Kazakhstan", + "currency-display-name-lkr": "Rupee Sri Lanka", + "currency-display-name-mad": "Dirham Maroko", + "currency-display-name-mdl": "Moldovan Leu", + "currency-display-name-mur": "Mauritius Rupee", + "currency-display-name-mwk": "Kwacha Malawi", + "currency-display-name-mxn": "Peso Meksiko", + "currency-display-name-myr": "Ringgit Malaysia", + "currency-display-name-mzn": "Mozambik Metis", + "currency-display-name-nad": "Dolar Namibia", + "currency-display-name-ngn": "Nigeria Naira", + "currency-display-name-nok": "Krone Norwegia", + "currency-display-name-npr": "Rupee Nepal", + "currency-display-name-nzd": "Dolar Selandia Baru", + "currency-display-name-omr": "Rial Oman", + "currency-display-name-pen": "Peru Sol", + "currency-display-name-pgk": "Papua New Guinea Kina", + "currency-display-name-php": "Peso Filipina", + "currency-display-name-pkr": "Rupee Pakistan", + "currency-display-name-pln": "Polandia Zloty", + "currency-display-name-pyg": "Paraguay Guarani", + "currency-display-name-qar": "Qatar Riyal", + "currency-display-name-ron": "Romania Leu", + "currency-display-name-rsd": "Dinar Serbia", + "currency-display-name-rub": "Ruble Rusia", + "currency-display-name-sar": "Riyal Arab Saudi", + "currency-display-name-sek": "Krona Swedia", + "currency-display-name-sgd": "Dollar Singapur", + "currency-display-name-thb": "Baht Thailand", + "currency-display-name-try": "Lira Turki", + "currency-display-name-ttd": "Trinidad dan Dollar Tobago", + "currency-display-name-twd": "Dolar Baru Taiwan", + "currency-display-name-tzs": "Shilling Tanzania", + "currency-display-name-uah": "Ukraina Hryvnia", + "currency-display-name-ugx": "Shilling Uganda", + "currency-display-name-usd": "Dolar Amerika Serikat", + "currency-display-name-uyu": "Peso Uruguay", + "currency-display-name-vef": "Venezuela Bolivar", + "currency-display-name-vnd": "Viet Nam Dong", + "currency-display-name-zar": "Rand Afrika Selatan", + "current-network": "Jaringan saat ini", + "current-password": "Kata sandi saat ini", + "current-pin": "Masukkan kode sandi 6 digit", + "current-pin-description": "Masukkan kode sandi 6 digit Anda untuk melanjutkan", + "custom": "Kustom", + "custom-networks": "Jaringan khusus", + "custom-node": "Anda menggunakan titik akhir RPC kustom. Riwayat transfer lokal Anda mungkin tidak lengkap.", + "custom-seed-phrase": "Seed phrase tidak valid", + "custom-seed-phrase-text-1": "Frasa seed ini tidak cocok dengan kamus kami yang didukung. Periksa kata-kata yang salah eja.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Terima dan Buka", + "dapp-starter-pack-description": "Inilah beberapa crypto untuk Anda mulai! Gunakan untuk mendapatkan stiker, nama ENS, dan coba dapps", + "dapp-starter-pack-title": "Paket awal", + "dapp-would-like-to-connect-wallet": "ingin terhubung ke", + "dapps": "ÐApps", + "dapps-permissions": "Izin DApp", + "dark": "Gelap", + "data": "Data", + "data-collected": "Data yang dikumpulkan", + "data-collected-subtitle": "Tabel di bawah ini menunjukkan data persis yang disimpan dan akan dikirim. Data divalidasi terhadap aturan publik untuk memastikan tidak ada data sensitif yang dikirim. Jangan percaya, verifikasi.", + "data-syncing": "Sinkronisasi data", + "datetime-ago": "lalu", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "other": "hari" + }, + "datetime-day-short": { + "other": "D" + }, + "datetime-hour": { + "other": "jam" + }, + "datetime-hour-short": { + "other": "H" + }, + "datetime-minute": { + "other": "menit" + }, + "datetime-minute-short": { + "other": "M" + }, + "datetime-second": { + "other": "detik" + }, + "datetime-second-short": { + "other": "S" + }, + "datetime-today": "hari ini", + "datetime-yesterday": "kemarin", + "decimals": "Desimal", + "decline": "Menurun", + "decryption-failed-content": "Terjadi kesalahan saat mendekripsi data Anda. Anda mungkin perlu menghapus data lama Anda dan membuat akun baru. Ketuk \"Terapkan\" untuk menghapus atau \"Batal\" untuk mencoba lagi", + "default": "Default", + "default-assets": "ERC20 dan ERC721 default", + "default-sync-period": "Sinkronkan riwayat untuk", + "delete": "Menghapus?", + "delete-account": "Hapus akun", + "delete-and-leave-group": "Hapus dan tinggalkan grup", + "delete-bootnode": "Hapus bootnode", + "delete-bootnode-are-you-sure": "Anda yakin ingin menghapus bootnode ini?", + "delete-bootnode-title": "Hapus bootnode", + "close-chat": "Hapus obrolan", + "close-chat-confirmation": "Anda yakin ingin menghapus obrolan ini?", + "delete-confirmation": "Menghapus?", + "delete-mailserver": "Hapus node riwayat", + "delete-mailserver-are-you-sure": "Anda yakin ingin menghapus node riwayat ini?", + "delete-mailserver-title": "Hapus node riwayat", + "delete-message": "Hapus pesan", + "delete-my-account": "Hapus akun Saya", + "delete-my-profile": "Hapus profil saya", + "delete-network-confirmation": "Anda yakin ingin menghapus jaringan ini?", + "delete-network-error": "Harap sambungkan ke jaringan lain sebelum menghapus yang ini", + "delete-network-title": "Hapus jaringan?", + "delete-profile": "Hapus profil", + "delete-profile-warning": "Peringatan: Jika Anda tidak menuliskan frase awal Anda, Anda akan kehilangan akses ke dana Anda setelah Anda menghapus profil Anda", + "deny": "Menyangkal", + "derivation-path": "Jalur derivasi", + "describe-channel": "Menjelaskan salurannya", + "description": "Deskripsi", + "dev-mode": "Mode pengembangan", + "dev-mode-settings": "Pengaturan mode pengembangan", + "device-syncing": "Sinkronisasi perangkat", + "devices": "Perangkat", + "disable": "nonaktifkan", + "disabled": "Dengan disabilitas", + "disconnected": "Obrolan offline", + "discover": "Menemukan", + "dismiss": "Memberhentikan", + "done": "Selesai", + "dont-ask": "Jangan tanya saya lagi", + "edit": "Edit", + "edit-channel-title": "Edit saluran", + "edit-group": "Edit grup", + "edit-profile": "Sunting profil", + "empty-chat-description": "Tidak ada pesan \ndalam obrolan ini", + "empty-chat-description-one-to-one": "Pesan apa pun yang Anda kirim di sini dienkripsi dan hanya dapat dibaca oleh Anda dan", + "empty-chat-description-public": "Sudah sepi di sini selama {{quiet-hours}} . Mulai percakapan atau", + "empty-chat-description-public-share-this": "bagikan obrolan ini.", + "empty-pending-invitations-descr": "Orang yang ingin bergabung dengan grup\n melalui link undangan akan muncul di sini", + "enable": "Memungkinkan", + "enable-all": "Aktifkan semua", + "enable-link-previews": "Aktifkan pratinjau tautan dalam obrolan?", + "encrypt-with-password": "Enkripsi dengan kata sandi", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Tambahkan nama pengguna", + "ens-agree-to": "Setuju untuk", + "ens-chat-settings": "Pengaturan Chat", + "ens-custom-domain": "Domain khusus", + "ens-custom-username-hints": "Ketik seluruh nama pengguna termasuk domain khusus seperti namapengguna.domain.eth", + "ens-custom-username-taken": "Nama pengguna bukan milik Anda :(", + "ens-deposit": "Menyetorkan", + "ens-dismiss-message": "Klik di sini untuk menolak", + "ens-displayed-with": "Pesan Anda ditampilkan kepada orang lain dengan", + "ens-get-name": "Dapatkan nama pengguna universal", + "ens-got-it": "OK mengerti", + "ens-locked": "Nama pengguna terkunci. Anda tidak akan bisa merilisnya sampai {{date}}", + "ens-name-content": "Alias khusus untuk kunci obrolan yang dapat Anda daftarkan menggunakan Layanan Nama Ethereum. Nama ENS adalah nama pengguna yang didesentralisasi.", + "ens-name-not-found": "Tidak dapat menentukan nama ENS", + "ens-name-title": "Nama ENS", + "ens-network-restriction": "Hanya tersedia di Mainnet", + "ens-no-usernames": "Anda tidak memiliki nama pengguna yang terhubung", + "ens-powered-by": "Didukung oleh Ethereum Layanan Nama ", + "ens-primary-username": "Nama pengguna utama", + "ens-register": "Daftar", + "ens-registration-failed": "Untuk mendaftarkan nama pengguna, silakan coba lagi.", + "ens-registration-failed-title": "transaksi gagal", + "ens-registration-failure": "Registrasi gagal", + "ens-registration-in-progress": "Pendaftaran sedang berlangsung ...", + "ens-release-username": "Lepaskan nama pengguna", + "ens-remove-hints": "Menghapus akan melepaskan nama pengguna dari kunci Anda.", + "ens-remove-username": "Hapus nama pengguna", + "ens-saved": "sekarang terhubung dengan kunci obrolan Anda dan dapat digunakan dalam Status.", + "ens-saved-title": "Nama pengguna ditambahkan", + "ens-show-username": "Tampilkan nama pengguna ENS saya dalam obrolan", + "ens-terms-header": "Ketentuan registrasi nama", + "ens-terms-point-1": "Dana disetor selama 1 tahun. SNT Anda akan dikunci, tetapi tidak digunakan.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Daftar ENS).", + "ens-terms-point-2": "Setelah 1 tahun, Anda dapat merilis nama dan mendapatkan kembali setoran Anda, atau tidak mengambil tindakan apa pun untuk mempertahankan nama tersebut.", + "ens-terms-point-3": "Jika ketentuan kontrak berubah - misalnya Status membuat peningkatan kontrak - pengguna memiliki hak untuk melepaskan nama pengguna terlepas dari waktu yang dipegang.", + "ens-terms-point-4": "Pengontrol kontrak tidak dapat mengakses dana yang Anda setorkan. Mereka hanya dapat dipindahkan kembali ke alamat yang mengirim mereka.", + "ens-terms-point-5": "Alamat Anda akan dikaitkan secara publik dengan nama ENS Anda.", + "ens-terms-point-6": "Nama pengguna dibuat sebagai simpul subdomain stateofus.eth dan tunduk pada persyaratan kontrak pintar ENS.", + "ens-terms-point-7": "Anda mengesahkan kontrak untuk mentransfer SNT atas nama Anda. Ini hanya dapat terjadi ketika Anda menyetujui transaksi untuk mengotorisasi transfer.", + "ens-terms-point-8": "Ketentuan-ketentuan ini dijamin oleh logika kontrak pintar di alamat:", + "ens-terms-point-9": "{{address}}(DaftarkanNamapengguna Status)", + "ens-terms-registration": "Ketentuan pendaftaran nama.", + "ens-test-message": "Hai", + "ens-transaction-pending": "Menunggu transaksi...", + "ens-understand": "Saya mengerti bahwa alamat dompet saya akan terhubung secara publik ke nama pengguna saya.", + "ens-username": "Nama pengguna ENS", + "ens-username-already-added": "Nama pengguna sudah terhubung dengan kunci obrolan Anda dan dapat digunakan di dalam Status.", + "ens-username-available": "✓ Nama pengguna tersedia!", + "ens-username-connected": "Nama pengguna ini milik Anda dan terhubung dengan kunci obrolan Anda.", + "ens-username-connected-continue": "Lanjutkan untuk mengatur `Tampilkan nama pengguna ENS saya di obrolan`.", + "ens-username-connected-with-different-key": "Melanjutkan akan membutuhkan transaksi untuk menghubungkan nama pengguna dengan kunci obrolan Anda saat ini.", + "ens-username-connection-confirmation": "{{username}} akan terhubung setelah transaksi selesai.", + "ens-username-hints": "Setidaknya 4 karakter. Hanya huruf latin, angka, dan huruf kecil.", + "ens-username-invalid": "Huruf dan angka saja.", + "ens-username-owned": "✓ Nama pengguna dimiliki oleh Anda.", + "ens-username-owned-continue": "Melanjutkan akan menghubungkan nama pengguna ini dengan kunci obrolan Anda.", + "ens-username-registration-confirmation": "Bagus! Anda memiliki {{username}} setelah transaksi selesai.", + "ens-username-taken": "Nama pengguna sudah diambil :(", + "ens-username-you-can-follow-progress": "Anda dapat mengikuti kemajuan di bagian Riwayat Transaksi dompet Anda.", + "ens-usernames": "ENS nama pengguna", + "ens-usernames-details": "Daftarkan nama pengguna universal agar mudah dikenali oleh pengguna lain", + "ens-want-custom-domain": "Saya memiliki nama di domain lain", + "ens-want-domain": "Saya ingin domain stateofus.eth", + "ens-welcome-hints": "Nama ENS mengubah alamat panjang gila itu menjadi nama pengguna unik.", + "ens-welcome-point-customize": "Nama ENS dapat menggantikan nama 3-kata acak Anda dalam obrolan. Jadilah @jurnas alih-alih {{name}} .", + "ens-welcome-point-customize-title": "Kustomisasi nama obrolan Anda", + "ens-welcome-point-receive": "Orang lain dapat mengirimi Anda dana melalui obrolan dalam satu langkah sederhana.", + "ens-welcome-point-receive-title": "Terima transaksi dalam obrolan", + "ens-welcome-point-register": "Daftarkan sekali untuk menjaga nama selamanya. Setelah 1 tahun Anda dapat merilis nama dan mendapatkan SNT Anda kembali.", + "ens-welcome-point-register-title": "10 SNT untuk mendaftar", + "ens-welcome-point-simplify": "Anda dapat menerima dana dengan nama ENS yang mudah dibagikan daripada hash heksadesimal Anda (0x ...).", + "ens-welcome-point-simplify-title": "Sederhanakan alamat ETH anda", + "ens-welcome-point-verify": "Anda dapat memverifikasi dan menambahkan nama pengguna apa pun yang Anda miliki di langkah selanjutnya.", + "ens-welcome-point-verify-title": "Sudah memiliki nama pengguna?", + "ens-your-username": "Nama pengguna Anda", + "ens-your-usernames": "Nama pengguna Anda", + "ens-your-your-name": "Nama ENS Anda", + "enter-12-words": "Masukkan 12 kata dari frase seed Anda, dipisahkan dengan spasi tunggal", + "enter-a-private-key": "Masukkan kunci pribadi", + "enter-a-seed-phrase": "Masukkan seed phrase", + "enter-address": "Masukkan alamat", + "enter-contact-code": "Masukkan nama pengguna ENS atau kunci obrolan", + "enter-pair-code": "Masukkan kode pemasangan Anda", + "enter-pair-code-description": "Kode pemasangan ditampilkan kepada Anda selama pengaturan Kartu Kunci", + "enter-password": "Masukkan kata kunci", + "enter-pin": "Masukkan kode sandi 6 digit", + "enter-puk-code": "Masukkan kode PUK", + "enter-puk-code-description": "Kode akses 6 digit telah diblokir. \n Silakan masukkan kode PUK untuk membuka blokir kode sandi.", + "enter-recipient-address-or-username": "Masukkan alamat dompet atau nama pengguna penerima", + "enter-seed-phrase": "Masukkan frase seed", + "enter-url": "Masukkan URL", + "enter-watch-account-address": "Memindai kode QR \natau \nmasukkan alamat untuk menonton", + "enter-word": "Masukkan kata", + "enter-your-code": "Masukkan kode sandi 6 digit Anda", + "enter-your-password": "Masukkan kata sandi Anda", + "error": "Kesalahan", + "error-unable-to-get-balance": "Tidak bisa mendapatkan keseimbangan", + "error-unable-to-get-prices": "Kesalahan konversi mata uang. Refresh layar Anda untuk mencoba lagi.", + "error-unable-to-get-token-balance": "Tidak dapat memperoleh saldo token", + "errors": "Kesalahan", + "eth": "ETH", + "ethereum-account": "Akun Ethereum", + "ethereum-address": "Alamat Ethereum", + "ethereum-node-started-incorrectly-description": "Node Ethereum dimulai dengan konfigurasi yang salah, aplikasi akan dihentikan untuk memulihkan dari kondisi itu. Id jaringan yang dikonfigurasikan = {{network-id}} , aktual = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Node Ethereum mulai salah", + "etherscan-lookup": "Lihatlah Etherscan", + "expand-all": "Perluas semua", + "export-account": "Akun ekspor", + "external-storage-denied": "Akses ke penyimpanan eksternal ditolak", + "failed": "Gagal", + "faq": "Pertanyaan yang sering diajukan", + "favourites": "Favorit", + "favourites-empty": "Alamat yang ditambahkan ke favorit akan muncul di sini", + "fetch-messages": "↓ Mengambil pesan", + "find": "Temukan", + "finish": "Selesai", + "finishing-card-setup": "Penyiapan kartu selesai", + "fleet": "Armada kapal", + "fleet-settings": "Pengaturan armada", + "follow-your-interests": "Langsung ke obrolan publik dan bertemu orang baru", + "free": "↓ Gratis", + "from": "Dari", + "gas-limit": "Batas gas", + "gas-price": "Harga Gas", + "gas-used": "Gas yang digunakan", + "generate-a-key": "Buat kunci", + "generate-a-new-account": "Buat akun", + "generate-a-new-key": "Hasilkan kunci baru", + "generate-account": "Buat kunci", + "generate-an-account": "Buat akun", + "generate-new-key": "Buat kunci", + "generating-codes-for-pairing": "> Mengunduh perangkat lunak produk ke kartu \n > Menghasilkan kode unlocking & pairing", + "generating-keys": "Membuat kunci...", + "generating-mnemonic": "Menghasilkan frase seed", + "get-a-keycard": "Dapatkan Kartu Kunci", + "get-started": "Memulai", + "get-status-at": "Dapatkan Status di http:\/\/status.im", + "get-stickers": "Dapatkan Stiker", + "give-permissions-camera": "Berikan izin\nuntuk mengakses kamera", + "glossary": "Glosarium", + "go-to-settings": "Pergi ke pengaturan...", + "got-it": "Mengerti", + "grant-face-id-permissions": "Untuk memberikan izin ID Wajah yang diperlukan, buka pengaturan sistem Anda dan pastikan Status > ID Wajah dipilih", + "group-chat": "Obrolan grup", + "group-chat-admin": "Admin", + "group-chat-admin-added": "** {{member}} ** telah menjadi admin", + "group-chat-all-contacts-invited": "Semua kontak Anda sudah ada di grup", + "group-chat-created": "** {{member}} ** membuat grup ** {{name}} **", + "group-chat-decline-invitation": "Tolak undangan", + "group-chat-member-added": "** {{member}} ** telah diundang", + "group-chat-member-joined": "** {{member}} ** telah bergabung dengan grup", + "group-chat-member-removed": "** {{member}} ** meninggalkan grup", + "group-chat-members-count": "{{selected}} \/ {{max}} anggota", + "group-chat-name-changed": "** {{member}} ** mengubah nama grup menjadi ** {{name}} **", + "group-chat-no-contacts": "Anda belum memiliki kontak. \n Undang teman Anda untuk mulai mengobrol", + "group-info": "Info grup", + "group-invite": "Undangan grup", + "group-invite-link": "Tautan undangan grup", + "group-membership-request": "Permintaan keanggotaan grup", + "gwei": "Gwei", + "hash": "Hash", + "help": "Tolong", + "help-capitalized": "Tolong", + "help-center": "Pusat Bantuan", + "help-improve-status": "Membantu meningkatkan Status", + "hide-content-when-switching-apps": "Blokir tangkapan layar", + "hide-content-when-switching-apps-ios": "Sembunyikan pratinjau", + "history": "Sejarah", + "history-nodes": "Node riwayat", + "hold-card": "Pegang kartu di bagian belakang \n telepon Anda", + "home": "Rumah", + "hooks": "Kait", + "how-it-works": "Bagaimana itu bekerja", + "http-gateway-error": "Ups, permintaan gagal!", + "identifier": "Identifier", + "image": "Gambar", + "image-remove-current": "Hapus foto saat ini", + "image-source-gallery": "Pilih dari galeri", + "image-source-make-photo": "Menangkap", + "image-source-title": "Edit gambar", + "in-contacts": "Dalam kontak", + "incoming": "Masuk", + "incoming-transaction": "Transaksi masuk", + "incorrect-code": [ + "str", + "Maaf kodenya salah, silakan masukkan lagi" + ], + "increase-gas": "Tingkatkan Gas", + "initialization": "Inisialisasi", + "install": "↓ Instal", + "intro-message1": "Selamat datang di Status! \nKetuk pesan ini untuk mengatur kata sandi Anda dan memulai.", + "intro-privacy-policy-note1": "Status tidak mengumpulkan atau mengambil keuntungan dari data pribadi Anda. Dengan melanjutkan, Anda setuju dengan", + "intro-privacy-policy-note2": "Kebijakan pribadi", + "intro-text": "Status adalah gerbang Anda ke web yang didesentralisasi", + "intro-text1": "Mengobrol melalui jaringan terenkripsi peer-to-peer, tempat pesan tidak dapat disensor atau diretas", + "intro-text2": "Kirim dan terima aset digital di mana saja di dunia — tidak perlu rekening bank", + "intro-text3": "Jelajahi game, pertukaran, dan jejaring sosial tempat Anda memiliki data sendiriData", + "intro-title1": "Komunikasi yang benar-benar pribadi", + "intro-title2": "Amankan dompet crypto", + "intro-title3": "Aplikasi terdesentralisasi", + "intro-wizard-text1": "Satu set kunci mengontrol akun Anda. Kunci Anda hidup di ponsel Anda, jadi hanya Anda yang dapat menggunakannya", + "intro-wizard-text2": "Salah satu kuncinya adalah untuk chatting. Itu datang dengan nama yang dapat dibaca yang tidak dapat diubah.", + "intro-wizard-text3": "Jika Anda memiliki Kartu Kunci, simpan kunci Anda di sana untuk meningkatkan keamanan.", + "intro-wizard-text4": "Amankan dan enkripsi kunci Anda", + "intro-wizard-text6": "Status akan memberi tahu Anda tentang pesan baru. Anda dapat mengedit preferensi pemberitahuan nanti di pengaturan", + "intro-wizard-title-alt4": "Membuat kata sandi", + "intro-wizard-title-alt5": "Konfirmasi password Anda", + "intro-wizard-title1": "Dapatkan kunci Anda", + "intro-wizard-title2": "Pilih nama obrolan", + "intro-wizard-title3": "Pilih penyimpanan kunci", + "intro-wizard-title4": "Buat kode sandi 6 digit", + "intro-wizard-title5": "Konfirmasikan kode sandi", + "intro-wizard-title6": "Aktifkan notifikasi", + "introduce-yourself": "Perkenalkan diri Anda dengan pesan singkat", + "invalid-address-qr-code": "Kode QR yang dipindai tidak mengandung alamat yang valid", + "invalid-format": "Format yang tidak valid \n Harus {{format}}", + "invalid-key-confirm": "Menerapkan", + "invalid-key-content": "Database Multiaccount tidak dapat dienkripsi karena file rusak. Tidak ada cara untuk mengembalikannya. Jika Anda menekan tombol \"Batal\", tidak ada yang terjadi. Jika Anda menekan tombol \" {{erase-multiaccounts-data-button-text}} \", db multiaccount akan dihapus dan Anda akan bisa membuka kunci multiaccount. Semua data multi akun akan hilang.", + "invalid-number": "Nomor tidak valid", + "invalid-pairing-password": "Kata sandi penyandingan tidak valid", + "invalid-public-chat-topic": "Topik obrolan publik tidak valid", + "invalid-range": "Format tidak valid, harus antara {{min}} dan {{max}}", + "invalid-username-or-key": "Nama pengguna atau kunci obrolan tidak valid", + "invite": "Undang", + "invite-button": "Undang", + "invite-chat-accept": "Menerima", + "invite-chat-intro": "Anda dirujuk oleh seorang teman untuk bergabung dengan Status. Inilah beberapa crypto untuk Anda mulai! Gunakan untuk mendaftarkan nama ENS atau membeli paket stiker", + "invite-chat-name": "Referensi teman", + "invite-chat-pending": "Tertunda", + "invite-chat-rule": "Menerima juga akan memberi teman Anda bonus rujukan crypto", + "invite-chat-starter-pack": "Paket awal", + "invite-friends": "Mengundang teman-teman", + "invite-instruction-fifth": "Anda dapat memilih untuk menebus bonus referensi Anda kapan saja.", + "invite-instruction-first": "Anda mengirim tautan undangan unik ke teman Anda untuk mengunduh dan bergabung dengan Status", + "invite-instruction-fourth": "Anda menerima bonus referensi dan teman Anda Paket Pemula", + "invite-instruction-second": "Teman Anda mengunduh Status dan membuat akun (di Android)", + "invite-instruction-third": "Obrolan dengan teman Anda dimulai, di mana mereka mengkonfirmasi rujukan Anda", + "invite-privacy-policy1": "Dengan menerima Anda setuju dengan program rujukan", + "invite-privacy-policy2": "Syarat dan ketentuan.", + "invite-receive-account": "Anda dapat memilih untuk menebus bonus referensi Anda kapan saja.", + "invite-reward": "Hasilkan crypto untuk setiap teman yang Anda undang!", + "invite-reward-friend": "Teman:", + "invite-reward-friend-description": "Teman Anda akan menerima Paket Pemula yang terdiri dari beberapa {{reward}} untuk memulai", + "invite-reward-friend-name": "Paket awal", + "invite-reward-you": "Kamu:", + "invite-reward-you-description": "Undang teman dan terima {{reward}} sebagai bonus rujukan. Gunakan untuk mendapatkan stiker, nama ENS, dan coba dapps", + "invite-reward-you-name": "Bonus referensi", + "invite-select-account": "Pilih akun untuk menerima bonus referensi Anda", + "invite-warning": "Promosi ini hanya berlaku untuk pengguna perangkat Android, yang bukan penduduk AS. Teman perlu mengkonfirmasi rujukan dalam 7 hari", + "invited": "diundang", + "join": "Bergabung", + "join-group-chat": "Bergabung dengan grup", + "join-group-chat-description": "{{username}} mengundang Anda untuk bergabung dengan grup {{group-name}}", + "join-me": "Hai bergabunglah dengan saya di Status: {{url}}", + "join-new-private-chat": "Mulai obrolan pribadi baru", + "join-new-public-chat": "Bergabunglah dengan obrolan publik", + "joined-group-chat-description": "Anda telah bergabung dengan {{group-name}} dari undangan oleh {{username}}", + "key": "Kunci", + "keycard": "Kartu kunci", + "keycard-access-reset": "Akses kartu kunci diatur ulang", + "keycard-applet-install-instructions": "Untuk menginstal applet silakan ikuti instruksi di https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Coba gerakkan kartu untuk menemukan pembaca NFC di perangkat Anda", + "keycard-awaiting-title": "Masih mencari...", + "keycard-backup": "Buat Kartu Kunci cadangan", + "keycard-backup-success-body": "Kartu cadangan berhasil dibuat. Anda sekarang dapat menggunakannya dengan akun Anda seperti kartu utama.", + "keycard-backup-success-title": "Pencadangan berhasil", + "keycard-blocked": "Kartu kunci telah diblokir. \n Anda perlu mengatur ulang kartu untuk terus menggunakannya.", + "keycard-can-use-with-new-passcode": "Anda dapat menggunakan kartu ini dengan kode sandi baru Anda", + "keycard-cancel-setup-text": "Ini akan membatalkan pengaturan kartu kunci. Sangat disarankan untuk menyelesaikan pengaturan untuk menggunakan kartu kunci. Apakah Anda benar-benar ingin membatalkan?", + "keycard-cancel-setup-title": "Operasi berbahaya", + "keycard-connected-description": "Cobalah menjaga kartu tetap", + "keycard-connected-title": "Terhubung", + "keycard-desc": "Memiliki Kartu kunci? Simpan kunci Anda di atasnya; Anda akan membutuhkannya untuk transaksi", + "keycard-dont-ask-card": "Jangan minta kartu untuk masuk", + "keycard-enter-new-passcode": "Masukkan kode sandi baru{{step}}\/2", + "keycard-error-description": "Hubungkan kartu lagi untuk melanjutkan", + "keycard-error-title": "Koneksi terputus", + "keycard-factory-reset": "Kembalikan kartu ke pengaturan pabrik", + "keycard-factory-reset-text": "Melakukan ini akan menghapus semua frase mnemonic yang tersimpan di kartu. Pastikan Anda memiliki cadangan frasa mnemonic yang telah Anda gunakan dengan Kartu Kunci ini.", + "keycard-factory-reset-title": "Apakah Anda yakin ingin melakukan reset pabrik?", + "keycard-free-pairing-slots": "Keycard memiliki {{n}} slot pasangan gratis", + "keycard-has-multiaccount-on-it": "Kartu ini sudah penuh. Setiap kartu dapat menampung satu pasangan kunci utama", + "keycard-init-description": "Letakkan kartu di bagian belakang ponsel Anda untuk melanjutkan", + "keycard-init-title": "Mencari kartu ...", + "keycard-is-blocked-details": "Anda tidak dapat lagi menggunakan kartu ini untuk mengakses atau menandatangani akun ini. Terlalu banyak kode sandi dan upaya PUK yang gagal.", + "keycard-is-blocked-instructions": "Untuk mengakses akun Anda, instal ulang Status dan gunakan Kartu Kunci baru, gunakan dompet lain atau setel ulang Kartu Kunci secara manual.", + "keycard-is-blocked-title": "Kartu kunci diblokir", + "keycard-is-frozen-details": "Untuk melindungi aset Anda, kartu Anda dibekukan. Atur ulang akses kartu untuk membuka kunci dan mengirim transaksi. Buat kode sandi baru dan masukkan PUK Anda untuk mengakses akun Anda di kartu ini", + "keycard-is-frozen-factory-reset": "Setel ulang dengan mnemonik", + "keycard-is-frozen-reset": "Setel ulang akses kartu", + "keycard-is-frozen-title": "Kartu kunci dibekukan", + "keycard-onboarding-finishing-header": "Menyelesaikan", + "keycard-onboarding-intro-header": "Simpan kunci Anda di Kartu Kunci", + "keycard-onboarding-intro-text": "Bersiaplah, ini mungkin memerlukan beberapa menit, tetapi penting untuk mengamankan akun Anda", + "keycard-onboarding-mnemonic-text": "Anda juga akan membutuhkan selembar kertas dan pensil untuk menuliskan frase awal Anda.", + "keycard-onboarding-pairing-header": "Memasangkan kartu...", + "keycard-onboarding-pin-text": "Anda harus membuat kode sandi 6 digit yang akan digunakan untuk melindungi akses ke Kartu Kunci Anda.", + "keycard-onboarding-preparing-header": "Mempersiapkan kartu...", + "keycard-onboarding-puk-code-header": "Tulis kode \ndan simpan dengan aman", + "keycard-onboarding-recovery-phrase-description": "Anda perlu frase awal ini untuk mendapatkan kembali kunci Anda. Tuliskan. Tetap aman, offline, dan terpisah dari perangkat ini.", + "keycard-onboarding-recovery-phrase-header": "Cadangkan phrase seed", + "keycard-onboarding-recovery-phrase-text": "Hanya untuk mata kamu. Ini adalah benih ajaib yang digunakan untuk menghasilkan kunci Anda.", + "keycard-onboarding-start-header": "Pegang kartu di bagian belakang \ntelepon Anda untuk memulai", + "keycard-onboarding-start-step1": "Buat kode sandi", + "keycard-onboarding-start-step1-text": "Sekitar 1 menit. Buat kode sandi 6 digit untuk mengenkripsi kunci Anda", + "keycard-onboarding-start-step2": "Tuliskan PUK dan kode pasangan", + "keycard-onboarding-start-step2-text": "Sekitar 1 menit. Anda akan membutuhkan selembar kertas dan pensil untuk itu", + "keycard-onboarding-start-step3": "Cadangkan frase seed", + "keycard-onboarding-start-step3-text": "Sekitar 1 menit. Juga selembar kertas dan pensil diperlukan", + "keycard-onboarding-start-text": "Dan pertahankan kontak kartu ke telepon \nselama pengaturan. Pengaturan akan memakan waktu sekitar 4 menit", + "keycard-processing-description": "Cobalah menjaga kartu tetap", + "keycard-processing-title": "Pengolahan...", + "keycard-recover": "kartu hilang atau beku?", + "keycard-recover-text": "Jika Anda memiliki frase mnemonik, Anda dapat membuat Kartu Kunci baru yang terkait dengan akun ini. Anda dapat menggunakan Kartu Kunci baru atau melakukan reset pabrik pada Kartu Kunci yang dibekukan.", + "keycard-recover-title": "Buat kartu baru untuk akun ini?", + "keycard-recovery-intro-button-text": "Mulai pemulihan", + "keycard-recovery-intro-header": "Pulihkan kunci yang disimpan pada kartu kunci", + "keycard-recovery-intro-text": "Jika Anda membuat kunci menggunakan kartu kunci sebelumnya dan sekarang ingin menggunakan kunci ini pada perangkat ini", + "keycard-recovery-no-key-header": "Tidak ada yang bisa dilakukan \npulihkan di sini", + "keycard-recovery-no-key-text": "Kartu Kunci Anda tidak memiliki kunci yang tersimpan di dalamnya. Untuk menggunakannya, buat kunci baru dan pilih Kartu Kunci Anda untuk menyimpan kunci", + "keycard-recovery-phrase-confirm-header": "Konfirmasikan frase seed", + "keycard-recovery-phrase-confirmation-text": "Anda tidak akan memiliki kesempatan kedua! Jika Anda kehilangan akses, misalnya dengan kehilangan kartu kunci Anda, Anda hanya dapat mengakses kunci Anda dengan frase benih Anda. Tidak seorang pun, tetapi Anda memiliki frase benih Anda. Tuliskan. Amankan.", + "keycard-recovery-phrase-confirmation-title": "Dituliskan frase seed turun?", + "keycard-recovery-success-header": "Kunci Anda telah \nberhasil dipulihkan", + "keycard-redeem-title": "Tukarkan ke", + "keycard-redeem-tx": "Tebus aset", + "keycard-redeem-tx-desc": "Ketuk kartu untuk menandatangani dan menerima aset", + "keycard-reset-passcode": "Setel ulang kode sandi", + "keycard-success-description": "Anda dapat mengeluarkan kartu sekarang", + "keycard-success-title": "Sukses", + "keycard-unauthorized-operation": "Anda tidak sah untuk melakukan operasi ini. \n Silakan ketuk kartu yang valid dan coba lagi.", + "language": "Bahasa", + "learn-more": "Belajarlah lagi", + "learn-more-about-keycard": "Pelajari lebih lanjut tentang Kartu Kunci", + "leave": "Meninggalkan", + "leave-chat": "Tinggalkan obrolan", + "leave-chat-confirmation": "Anda yakin ingin keluar dari obrolan?", + "leave-confirmation": "Tinggalkan obrolan", + "leave-group": "Meninggalkan grup", + "left": "kiri", + "les-ulc": "LES \/ ULC", + "lets-go": "Ayo pergi", + "light": "Cahaya", + "linked-on": "Tertaut pada {{date}}", + "load-messages-before": "sebelum {{date}}", + "load-more-messages": "↓ Ambil lebih banyak pesan", + "loading": "Memuat...", + "local-notifications": "Pemberitahuan lokal", + "lock-app-with": "Kunci aplikasi dengan", + "log-level": "Tingkat log", + "log-level-settings": "Pengaturan level log", + "logging": "Penebangan", + "logging-enabled": "Logging diaktifkan?", + "login-pin-description": "Masukkan kode sandi 6 digit Anda untuk membuka kunci Anda", + "logout": "Keluar", + "logout-app-content": "Akun akan keluar. Ketika Anda membuka kunci lagi, jaringan yang dipilih akan digunakan", + "logout-are-you-sure": "Apakah kamu yakin kamu mau \nuntuk keluar?", + "logout-title": "Keluar?", + "looking-for-cards": "Mencari kartu ...", + "lost-connection": "Kehilangan koneksi", + "mail-should-be-configured": "Klien e-mail harus dikonfigurasi", + "mailserver-address": "Alamat node sejarah", + "mailserver-automatic": "Seleksi otomatis", + "mailserver-automatic-switch-explanation": "Pilih node riwayat tercepat yang tersedia", + "mailserver-connection-error": "Tidak dapat terhubung ke simpul riwayat", + "mailserver-content": "Node di jaringan Status yang merutekan dan menyimpan pesan, hingga 30 hari.", + "mailserver-details": "Rincian node sejarah", + "mailserver-error-content": "Node riwayat yang Anda pilih tidak dapat dijangkau.", + "mailserver-error-title": "Terjadi kesalahan saat menyambungkan ke simpul riwayat", + "mailserver-format": "enode: \/\/ {enode-id} : {password} @ {ip-address} : {port}", + "mailserver-pick-another": "Pilih simpul riwayat lain", + "mailserver-reconnect": "Tidak dapat terhubung ke simpul riwayat. Ketuk untuk menyambungkan kembali", + "mailserver-request-error-content": "Kesalahan berikut dikembalikan oleh simpul riwayat: {{error}}", + "mailserver-request-error-status": "Terjadi kesalahan saat mengambil riwayat, periksa detail log", + "mailserver-request-error-title": "Kesalahan permintaan node riwayat", + "mailserver-request-retry": "Coba lagi permintaan", + "mailserver-retry": "Mencoba kembali", + "mailserver-title": "Node sejarah", + "main-currency": "Mata uang utama", + "main-networks": "Jaringan utama", + "main-wallet": "Dompet Utama", + "mainnet-network": "Jaringan utama", + "make-admin": "Membuat admin", + "mark-all-read": "Tandai semua sudah dibaca", + "maybe-later": "Mungkin nanti", + "member-ban": "Larangan anggota", + "members": { + "other": " {{count}} anggota" + }, + "members-active": { + "other": "anggota" + }, + "members-active-none": "tidak ada anggota", + "members-count": "{{count}} anggota", + "members-label": "Anggota", + "members-limit-reached": "Batas anggota tercapai", + "members-title": "Anggota", + "membership-declined": "Permintaan keanggotaan ditolak", + "membership-description": "Keanggotaan grup mengharuskan Anda untuk diterima oleh admin grup", + "membership-none": "Tidak ada", + "message": "Pesan", + "message-not-sent": "Pesan tidak terkirim", + "message-options-cancel": "Membatalkan", + "message-reply": "Balasan", + "messages": "Pesan", + "messages-from-contacts-only-subtitle": "Hanya orang yang Anda tambahkan sebagai kontak yang bisa memulai obrolan baru dengan Anda atau mengundang Anda ke grup", + "might-break": "Mungkin merusak beberapa ÐApps", + "migrations-failed-content": "{{message}}\n versi skema: awal {{initial-version}} , saat ini {{current-version}} , terakhir {{last-version}}\n\nTerjadi kesalahan basis data. Dana dan kunci obrolan Anda aman. Data lain, seperti obrolan dan kontak Anda, tidak dapat dipulihkan. \"{{erase-multiaccounts-data-button-text}}\", akan menghapus semua data lainnya dan memungkinkan Anda mengakses dana dan mengirim pesan.", + "mobile-network-ask-me": "Tanya saya kapan di jaringan seluler", + "mobile-network-continue-syncing": "Lanjutkan sinkronisasi", + "mobile-network-continue-syncing-details": "Anda dapat mengubahnya nanti dalam pengaturan", + "mobile-network-go-to-settings": "Pergi ke pengaturan", + "mobile-network-settings": "Data seluler", + "mobile-network-sheet-configure": "Anda dapat mengonfigurasi sinkronisasi lebih lanjut \n detail dalam", + "mobile-network-sheet-offline": "Tidak ada Wi-Fi, sinkronisasi pesan dinonaktifkan.", + "mobile-network-sheet-offline-details": "Sinkronisasi menggunakan jaringan seluler tidak aktif", + "mobile-network-sheet-remember-choice": "Ingat pilihan saya", + "mobile-network-sheet-settings": "pengaturan", + "mobile-network-start-syncing": "Mulai sinkronisasi", + "mobile-network-stop-syncing": "Hentikan sinkronisasi", + "mobile-network-stop-syncing-details": "Sampai terhubung ke Wi-Fi?", + "mobile-network-use-mobile": "Gunakan data seluler", + "mobile-network-use-mobile-data": "Status menggunakan banyak data saat menyinkronkan obrolan dan dompet.", + "mobile-network-use-wifi": "Hanya wifi", + "mobile-syncing-sheet-details": "Status menggunakan banyak data saat menyinkronkan obrolan dan dompet.", + "mobile-syncing-sheet-title": "Sinkronkan menggunakan data seluler?", + "more": "lagi", + "multiaccount-exists-content": "Kunci untuk akun ini sudah ada dan tidak dapat ditambahkan lagi. Jika Anda kehilangan kata sandi, kode sandi, atau Kartu Kunci, hapus instalan aplikasi, instal ulang, dan akses kunci Anda dengan memasukkan frase benih Anda", + "multiaccount-exists-title": "Kunci untuk akun ini sudah ada", + "multiaccounts-recover-enter-phrase-text": "Masukkan 12, 15, 18, 21 atau 24 kata. \n Pisahkan kata dengan satu spasi.", + "multiaccounts-recover-enter-phrase-title": "Masukkan frasa seed Anda", + "mute": "Bisu", + "my-accounts": "Akun saya", + "my-accounts-empty": "Akun Anda yang tersedia akan muncul di sini", + "my-profile": "Profil saya", + "my-status": "Status saya", + "name": "Nama", + "name-of-token": "Nama token Anda", + "name-optional": "Nama (opsional)", + "need-help": "Butuh bantuan?", + "network": "Jaringan", + "network-chain": "Rantai jaringan", + "network-details": "Rincian jaringan", + "network-fee": "Biaya jaringan", + "network-id": "ID jaringan", + "network-info": "Info jaringan", + "network-invalid-network-id": "Id jaringan yang ditentukan tidak sesuai dengan id jaringan oleh url RPC", + "network-invalid-status-code": "Kode status tidak valid: {{code}}", + "network-invalid-url": "Jaringan URL tidak valid", + "network-settings": "Pengaturan jaringan", + "new": "Baru", + "new-chat": "Obrolan baru", + "new-contact": "Kontak baru", + "new-contract": "Kontrak baru", + "new-favourite": "Favorit baru", + "new-group": "Grup baru", + "new-group-chat": "Obrolan grup baru", + "new-network": "Jaringan baru", + "new-password": "Kata sandi baru", + "new-pin-description": "Masukkan kode sandi 6 digit yang baru", + "new-public-group-chat": "Bergabunglah dengan obrolan publik", + "new-puk-description": "Masukkan PUK 12 digit baru", + "new-status": "Status baru", + "next": "Lanjut", + "nickname": "Nama panggilan", + "nickname-description": "Nama panggilan membantu Anda mengidentifikasi orang lain di Status.\nHanya Anda yang dapat melihat nama panggilan yang Anda tambahkan", + "no": "Tidak", + "no-collectibles": "Tidak ada koleksi yang tersedia", + "no-contacts": "Belum ada kontak", + "no-keycard-applet-on-card": "Tidak ada applet kartu kunci pada kartu", + "no-messages": "Tidak ada pesan", + "no-pairing-slots-available": "Kartu ini sudah dipasangkan ke 5 perangkat dan tidak dapat dipasangkan dengan yang ini. Silakan gunakan salah satu perangkat yang dipasangkan, masuk dengan kartu ini dan membebaskan slot pasangan pada kartu", + "no-result": "Tidak ada hasil", + "no-thanks": "Tidak terima kasih", + "no-tokens-found": "Token tidak ditemukan", + "node-info": "Info simpul", + "node-version": "Versi simpul", + "nodes-disabled": "Node status dinonaktifkan", + "non-archival-node": "Titik akhir RPC tidak mendukung permintaan pengarsipan. Riwayat transfer lokal Anda mungkin tidak lengkap.", + "nonce": "Nonce", + "none": "Tidak ada", + "not-applicable": "Tidak berlaku untuk transaksi yang tidak ditandatangani", + "not-connected-nodes": "Tidak terhubung ke simpul status", + "not-connected-to-peers": "Tidak terhubung kepada sesama", + "not-enough-snt": "SNT tidak cukup", + "not-keycard-text": "Kartu yang Anda gunakan bukan Kartu Kunci. Anda perlu membeli Kartu Kunci untuk menggunakannya", + "not-keycard-title": "Bukan kartu kunci", + "not-registered": "tidak terdaftar", + "notification-settings": "Notifikasi", + "notifications": "Notifikasi", + "notifications-non-contacts": "Pemberitahuan dari non-kontak", + "notifications-preferences": "Preferensi pemberitahuan", + "notifications-servers": "Server pemberitahuan", + "notifications-switch": "Tampilkan pemberitahuan", + "notifications-transactions": "Transaksi dompet", + "notify": "Memberitahu", + "now": "Sekarang", + "off": "Off", + "off-status-tree": "Tidak Aktif Pohon Status", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Aktifkan node riwayat untuk mengambil pesan yang dikirim saat aplikasi ditutup. Saat diaktifkan, simpul riwayat mendapatkan alamat IP Anda. Saat dinonaktifkan, Anda tidak akan menerima pesan saat aplikasi ditutup dan tidak akan melihatnya saat Anda membuka aplikasi nanti.", + "offline-messaging-use-history-nodes": "Gunakan node riwayat", + "ok": "baik", + "ok-continue": "Oke, lanjutkan", + "ok-got-it": "OK saya mengerti", + "ok-save-pass": "OKE, simpan kata sandi", + "okay": "baik", + "on": "On", + "on-status-tree": "Di pohon Status", + "once-enabled-share-metadata": "Setelah diaktifkan, tautan yang diposting dalam obrolan dapat membagikan metadata Anda dengan situs", + "one-day": "Satu hari", + "one-month": "Satu bulan", + "one-week": "Satu minggu", + "open": "Buka", + "open-dapp": "Buka ÐApp", + "open-dapp-store": "Temukan ÐApps", + "open-home": "Buka...", + "open-nfc-settings": "Buka pengaturan NFC", + "opening-buy-crypto": "Pembukaan", + "optional": "pilihan", + "or": "ATAU", + "outgoing": "Keluar", + "outgoing-transaction": "Transaksi keluar", + "page-camera-request-blocked": "permintaan kamera diblokir. Untuk mengaktifkan permintaan kamera, buka Pengaturan", + "page-would-like-to-use-camera": "ingin menggunakan kamera Anda", + "pair": "Pasangkan perangkat", + "pair-card": "Pasangkan ke perangkat ini", + "pair-code": "Kode pasangan", + "pair-code-explanation": "Pasangkan kartu ke perangkat yang berbeda (hingga 5) untuk membuka kunci kunci dan menandatangani transaksi dengan Kartu Kunci yang sama", + "pair-code-placeholder": "Kode pasangan...", + "pair-this-card": "Pasangkan kartu ini", + "pair-this-device": "Iklankan perangkat", + "pair-this-device-description": "Pasangkan perangkat Anda untuk menyinkronkan kontak dan mengobrol di antara mereka", + "paired-devices": "Perangkat yang dipasangkan", + "pairing": "Pemasangan", + "pairing-card": "Kartu pemasangan", + "pairing-changed": "Kode pemasangan telah diubah", + "pairing-code-placeholder": "Kode pemasangan...", + "pairing-code_error1": "Kode pemasangan tidak cocok.", + "pairing-go-to-installation": "Pergi ke pengaturan pemasangan", + "pairing-maximum-number-reached-content": "Harap nonaktifkan salah satu perangkat Anda sebelum mengaktifkan yang baru.", + "pairing-maximum-number-reached-title": "Jumlah perangkat maksimum tercapai", + "pairing-new-installation-detected-content": "Perangkat baru telah terdeteksi. \nUntuk menggunakan perangkat Anda dengan benar, penting untuk memasangkan dan mengaktifkannya sebelum menggunakannya. \nSilakan buka bagian perangkat di bawah pengaturan untuk memasangkan perangkat Anda.", + "pairing-new-installation-detected-title": "Perangkat baru terdeteksi", + "pairing-no-info": "Tidak ada informasi", + "pairing-please-set-a-name": "Silakan tentukan nama untuk perangkat Anda.", + "passphrase": "Frasa sandi", + "password": "Kata sandi", + "password-description": "Setidaknya 6 karakter. Kata sandi Anda melindungi kunci Anda. Anda memerlukannya untuk membuka Status dan bertransaksi.", + "password-mismatch": "Kata sandi dan konfirmasi baru tidak cocok", + "password-placeholder": "Kata sandi...", + "password-placeholder2": "Konfirmasi password Anda", + "password-reset-success": "Reset kata sandi", + "password_error1": "Kata sandi tidak cocok.", + "paste": "Tempel", + "paste-json": "Tempel JSON", + "pay-to-chat": "Bayar untuk mengobrol", + "peer-content": "Perangkat yang terhubung ke jaringan obrolan Status. Setiap pengguna dapat mewakili satu atau beberapa rekan, tergantung pada jumlah perangkat mereka.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Rekan", + "pending": "Tertunda", + "pending-confirmation": "Konfirmasi tertunda ...", + "pending-invitations": "Permintaan keanggotaan menunggu keputusan", + "permissions": "Izin", + "phone-e164": "Internasional 1", + "photos": "Foto", + "photos-access-error": "Untuk memberikan izin kepada foto yang diperlukan, buka pengaturan sistem Anda dan pastikan Status > Foto dipilih.", + "pin-changed": "Kode sandi 6 digit telah diubah", + "pin-code": "Kode sandi 6 digit", + "pin-mismatch": "Kode sandi salah", + "pin-one-attempt": "satu upaya", + "pin-one-attempt-blocked-after": "sebelum Kartu Kunci Anda diblokir", + "pin-one-attempt-blocked-before": "Hati-hati, Anda hanya punya", + "pin-one-attempt-frozen-after": "sebelum Kartu Kunci Anda dibekukan", + "pin-one-attempt-frozen-before": "Hati-hati, Anda hanya punya", + "pin-retries-left": "{{number}} percobaan tersisa", + "preference": "Pilihan", + "preview-privacy": "Pratinjau mode privasi", + "previewing-may-share-metadata": "Pratinjau tautan dari situs web ini dapat membagikan metadata Anda dengan pemiliknya", + "privacy": "Pribadi", + "privacy-and-security": "Privasi dan keamanan", + "privacy-policy": "Kebijakan pribadi", + "private-key": "Kunci pribadi", + "private-notifications": "Pemberitahuan pribadi", + "private-notifications-descr": "Status akan memberi tahu Anda tentang pesan baru. Anda dapat mengedit preferensi pemberitahuan Anda nanti di pengaturan.", + "processing": "Tungu sebentar", + "product-information": "informasi produk", + "profile": "Profil", + "profile-deleted-content": "Profil Anda berhasil dihapus", + "profile-deleted-title": "Profil dihapus", + "profile-details": "Profil lengkap", + "profile-pic-pick": "Pilih dari galeri", + "public-channel": "Saluran publik", + "public-chat": "Obrolan publik", + "public-chat-description": "Bergabunglah dengan obrolan publik untuk minat Anda! Siapa pun dapat memulai yang baru.", + "public-chats": "Obrolan publik", + "public-group-status": "Publik", + "public-group-topic": "Topik", + "public-key": "Kunci publik", + "puk-and-pairing-codes-displayed": "PUK dan kode pasangan ditampilkan", + "puk-changed": "PUK 12 digit telah diubah", + "puk-code": "Kode PUK", + "puk-code-explanation": "Jika Anda lupa kode sandi 6 digit atau salah memasukkan kode 3 kali, Anda perlu kode ini untuk membuka kunci kartu Anda.", + "puk-mismatch": "Kode PUK salah", + "push-failed-transaction": "Transaksi Anda gagal", + "push-failed-transaction-body": "Transaksi gagal", + "push-inbound-transaction": "Anda menerima {{value}} {{currency}}", + "push-inbound-transaction-body": "Dari {{from}} sampai {{to}}", + "push-notifications-server-enabled": "Server diaktifkan", + "push-notifications-servers": "Server pemberitahuan push", + "push-outbound-transaction": "Anda mengirim {{value}} {{currency}}", + "push-outbound-transaction-body": "Dari {{from}} sampai {{to}}", + "quiet-days": "{{quiet-days}} hari", + "quiet-hours": "{{quiet-hours}} jam", + "re-encrypt-key": "Enkripsi ulang kunci Anda", + "receive": "Menerima", + "receive-transaction": "Terima transaksi", + "recent": "Baru", + "recent-empty": "Alamat yang baru-baru ini digunakan akan muncul di sini", + "recent-recipients": "Kontak", + "recently-used-stickers": "Stiker yang baru saja digunakan akan muncul di sini", + "recipient": "Penerima", + "recipient-code": "Masukkan alamat penerima", + "recipient-code-placeholder": "0x ... atau username.domain.eth", + "recover": "Memulihkan", + "recover-key": "Akses kunci yang ada", + "recover-keycard-multiaccount-not-supported": "Kunci untuk akun ini sudah ada dan tidak dapat ditambahkan lagi. Jika Anda kehilangan kata sandi, kode sandi, atau Kartu Kunci, hapus instalan aplikasi, instal ulang, dan akses kunci Anda dengan memasukkan frase benih Anda", + "recover-with-keycard": "Pulihkan dengan Keycard", + "recovering-key": "Mengakses kunci...", + "recovery-confirm-phrase": "Konfirmasikan frase seed", + "recovery-phrase": "Frase benih", + "recovery-success-text": "Anda harus membuat kode atau kata sandi baru untuk mengenkripsi ulang kunci Anda", + "recovery-typo-dialog-description": "Harap dicatat, frase awal Anda harus menggunakan kata-kata dan urutan yang sama persis seperti saat Anda menerimanya", + "recovery-typo-dialog-title": "Apakah frase seed benar?", + "redeem-amount": "{{quantity}} bonus tersedia", + "redeem-now": "Tukarkan sekarang", + "redeem-success": "Penukaran bonus sukses!", + "refresh": "Menyegarkan", + "registered": "terdaftar", + "remember-me": "Ingat saya", + "remind-me-later": "Tunjukkan ini lagi", + "remove": "Menghapus", + "remove-from-chat": "Hapus dari obrolan", + "remove-from-contacts": "Hapus dari kontak", + "remove-from-contacts-text": "Dengan menghapus pengguna dari daftar kontak Anda, Anda tidak menyembunyikan alamat dompet Anda dari mereka", + "remove-group": "Hapus grup", + "remove-network": "Hapus jaringan", + "remove-token": "Hapus token", + "removed": "dihapus", + "repeat-pin": "Ulangi kode sandi 6 digit yang baru", + "repeat-puk": "Masukkan kode sandi 6 digit yang baru", + "report-bug-email-template": "1. Deskripsi Masalah \n (Jelaskan fitur yang Anda inginkan, atau singkatkan bug dan apa yang Anda lakukan, apa yang Anda harapkan terjadi, dan apa yang sebenarnya terjadi. Bagian di bawah) \n\n\n 2. Langkah-langkah mereproduksi \n (Jelaskan bagaimana kita dapat mereplikasi bug langkah demi langkah.) \n Status -Buka \n -... \n -Langkah 3, dll. \n\n\n 3. Perilaku yang diharapkan \n (Jelaskan apa yang Anda harapkan terjadi.) \n\n\n 4. Perilaku aktual \n (Jelaskan apa yang sebenarnya terjadi.) \n\n\n 5. Tolong pasang screenshot yang bisa menunjukkan masalah \n", + "request-feature": "Minta fitur", + "request-membership": "Minta keanggotaan", + "request-pending": "Permintaan tertunda…", + "request-transaction": "Minta transaksi", + "required-field": "kolom yang harus diisi", + "resend-message": "Kirim ulang", + "reset-card": "Setel ulang kartu", + "reset-card-description": "Operasi ini akan mengatur ulang kartu ke keadaan awal. Ini akan menghapus semua data kartu termasuk kunci pribadi. Pengoperasian tidak dapat dibalik.", + "reset-password": "Setel ulang kata sandi", + "restore-defaults": "Pulihkan Default", + "retry": "Mencoba kembali", + "revoke-access": "Mencabut akses", + "rpc-url": "URL RPC", + "rpc-usage-copy": "Salinan", + "rpc-usage-get-stats": "Menyegarkan", + "save": "Menyimpan", + "save-password": "Simpan kata sandi", + "save-password-unavailable": "Setel kode sandi perangkat untuk menyimpan kata sandi", + "save-password-unavailable-android": "Simpan kata sandi tidak tersedia: perangkat Anda mungkin di-rooting atau tidak memiliki fitur keamanan yang diperlukan.", + "scan-qr": "Pindai kode QR", + "scan-qr-code": "Memindai kode QR dengan alamat dompet", + "scan-tokens": "Pindai token", + "search": "Cari", + "search-no-chat-found": "Tidak ada hasil pencarian. Maksud kamu", + "secret-keys-confirmation-text": "Anda akan membutuhkan mereka untuk terus menggunakan Kartu Kunci Anda jika Anda kehilangan telepon Anda.", + "secret-keys-confirmation-title": "Menulis kode ke bawah?", + "security": "Keamanan", + "see-details": "Lihat detail", + "see-it-again": "MELIHATNYA LAGI", + "seed-phrase-content": "Seperangkat kata yang mudah dibaca, dipilih secara acak dari daftar standar BIP39 dan digunakan untuk memulihkan atau mengakses akun Ethereum Anda di dompet dan perangkat lain. Juga disebut sebagai \"frasa mnemonik,\" \"frasa pemulihan\" atau \"dompet cadangan\" di seluruh ekosistem crypto. Sebagian besar aplikasi crypto menggunakan standar yang sama ini untuk menghasilkan akun.", + "seed-phrase-placeholder": "Frase benih...", + "seed-phrase-title": "Seed Phrase", + "select": "Pilih", + "select-account": "Pilih akun", + "select-account-dapp": "Pilih akun yang ingin Anda gunakan dengan Dapps", + "select-account-first": "Pilih akun terlebih dahulu", + "select-chat": "Pilih obrolan untuk memulai olahpesan", + "selected": "Terpilih", + "send-logs": "Laporkan bug", + "send-logs-to": "Laporkan bug ke {{email}}", + "send-message": "Mengirim pesan", + "send-push-notifications-description": "Jika dinonaktifkan, orang yang menerima pesan Anda tidak akan diberi tahu tentang kedatangannya", + "send-request": "Kirim permintaan", + "send-request-amount": "Jumlah", + "send-request-amount-max-decimals": "Jumlah desimal maksimum adalah {{asset-decimals}}", + "send-request-unknown-token": "Token tidak dikenal - {{asset}}", + "send-sending-to": "ke {{recipient-name}}", + "send-transaction": "Kirim transaksi", + "sending": "Mengirim", + "sent-at": "Dikirim pada", + "server": "Server", + "set-a-topic": "Buat topik", + "set-currency": "Tetapkan mata uang", + "set-custom-fee": "Tetapkan biaya khusus", + "set-dapp-access-permissions": "Tetapkan izin akses DApp", + "set-max": "Tetapkan maks", + "settings": "Pengaturan", + "share": "Bagikan", + "share-address": "Bagikan alamat", + "share-chat": "Bagikan obrolan", + "share-contact-code": "Bagikan kunci obrolan saya", + "share-dapp-text": "Lihat DApp ini yang saya gunakan di Status: {{link}}", + "share-link": "Membagikan tautan", + "share-my-profile": "Bagikan profil saya", + "share-profile": "Bagikan profil", + "share-profile-link": "Bagikan tautan profil", + "share-public-chat-text": "Lihat obrolan publik ini di aplikasi Status: {{link}}", + "shared": "Bersama", + "sharing-copied-to-clipboard": "Disalin", + "sharing-copy-to-clipboard": "Salinan", + "sharing-data-desc-1": "Data divalidasi berdasarkan aturan publik untuk memastikan tidak ada data sensitif yang dikirim. Jangan percaya, verifikasi.", + "sharing-data-desc-2": "Data penggunaan dikirim dari ujung ke ujung yang dienkripsi melalui jaringan peer-to-peer Status", + "sharing-data-desc-3": "Alih-alih kunci obrolan reguler Anda, kunci sekali pakai digunakan", + "sharing-data-desc-4": "Data pengguna tidak dapat di kaitkan dengan alamat IP anda", + "sharing-data-desc-5": "Data kumulatif semua pengguna tersedia untuk umum", + "sharing-data-desc-6": "Data dihapus dari ponsel Anda setelah dikirim", + "sharing-share": "Bagikan", + "show-less": "Tampilkan lebih sedikit", + "show-more": "Menampilkan lebih banyak", + "show-notifications": "Tampilkan pemberitahuan", + "show-profile-pictures": "Tunjukkan foto profil", + "show-qr": "Tampilkan kode QR", + "show-transaction-data": "Tampilkan data transaksi", + "sign-and-send": "Tanda tangani dan kirim", + "sign-anyway": "Tandatangani saja", + "sign-in": "Membuka kunci", + "sign-message": "Tanda Pesan", + "sign-out": "Keluar", + "sign-request-failed": "Tidak dapat menandatangani pesan", + "sign-with": "Masuk dengan", + "sign-with-password": "Masuk dengan kata sandi", + "sign-you-in": "Masuk Anda ...", + "signing": "Menandatangani", + "signing-a-message": "Menandatangani pesan", + "signing-phrase": "Menandatangani frasa", + "something-went-wrong": "Ada yang salah", + "soon": "Segera", + "specify-address": "Tentukan alamat", + "specify-name": "Tentukan nama", + "specify-network-id": "Tentukan id jaringan", + "specify-rpc-url": "Tentukan URL RPC", + "specify-server-public-key": "Masukkan kunci publik server", + "start-chat": "Memulai obrolan", + "start-conversation": "Mulai percakapan", + "start-group-chat": "Mulai obrolan grup", + "start-new-chat": "Mulai obrolan baru", + "starter-pack-received": "Paket Perdana diterima", + "starter-pack-received-description": "Inilah beberapa crypto untuk Anda mulai! Gunakan untuk mendapatkan stiker, nama ENS, dan coba dapps", + "status": "Status", + "status-confirmed": "Dikonfirmasi", + "status-hardwallet": "Status hardwallet", + "status-keycard": "Kartu Kunci Status", + "status-mobile-descr": "Status cenderung menggunakan banyak data saat menyinkronkan obrolan. Anda dapat memilih untuk tidak menyinkronkan saat menggunakan jaringan seluler", + "status-not-sent-click": "Tidak dikonfirmasi Klik untuk opsi", + "status-not-sent-tap": "Tidak dikonfirmasi Ketuk untuk opsi", + "status-pending": "Tertunda", + "status-sent": "Terkirim", + "status-tx-not-found": "TX tidak ditemukan", + "status-updates-descr": "Pembaruan status akan muncul di sini. Tambahkan profil sebagai kontak untuk menerima pembaruan tentang timeline Anda.", + "statuses-descr": "Bagikan apa yang ada di pikiran Anda dan dapatkan info terbaru dengan kontak Anda", + "statuses-my-status-descr": "Bagikan apa yang Anda pikirkan. Siapa pun yang mengunjungi profil Anda akan dapat melihat status Anda. Orang yang menambahkan Anda sebagai kontaknya akan menerima pembaruan Anda di timeline mereka", + "step-i-of-n": "Langkah {{step}} dari {{number}}", + "sticker-market": "Pasar stiker", + "storage": "Penyimpanan", + "submit": "Kirimkan", + "submit-bug": "Kirimkan bug", + "success": "Sukses", + "symbol": "Simbol", + "sync-all-devices": "Sinkronkan semua perangkat", + "sync-in-progress": "Menyinkronkan...", + "sync-settings": "Pengaturan sinkronisasi", + "sync-synced": "Dalam sinkronisasi", + "syncing-devices": "Menyinkronkan...", + "system": "Sistem", + "tag-was-lost": "Tag hilang", + "tap-card-again": "Ketuk kartu ke bagian belakang ponsel Anda lagi", + "test-networks": "Uji jaringan", + "text-input-disabled": "Silakan tunggu beberapa saat...", + "thank-you": "Terima kasih", + "this-device": "Alat ini", + "this-device-desc": "Kunci Anda akan dienkripsi dan disimpan dengan aman di perangkat Anda", + "this-is-you-signing": "Ini adalah frasa penandatanganan Anda", + "this-will-take-few-seconds": "Ini akan memakan waktu beberapa detik", + "three-days": "Tiga hari", + "three-words-description": "Anda harus melihat 3 kata ini sebelum menandatangani setiap transaksi", + "three-words-description-2": "Jika Anda melihat kombinasi yang berbeda, batalkan transaksi dan keluar", + "timeline": "Linimasa", + "to": "Untuk", + "to-block": "Blok", + "to-enable-biometric": "Untuk mengaktifkan {{bio-type-label}} , Anda harus menyimpan kata sandi di layar buka kunci", + "to-encrypt-enter-password": "Untuk mengenkripsi akun, harap masukkan kata sandi Anda", + "to-see-this-message": "Untuk melihat pesan ini,", + "token-auto-validate-decimals-error": "Desimal salah untuk token {{symbol}} di alamat {{address}} - setel ke {{expected}} tetapi terdeteksi sebagai {{actual}}", + "token-auto-validate-name-error": "Nama salah untuk token {{symbol}} di alamat {{address}} - atur ke {{expected}} tetapi dideteksi sebagai {{actual}}", + "token-auto-validate-symbol-error": "Simbol salah untuk token {{symbol}} di alamat {{address}} - setel ke {{expected}} tetapi terdeteksi sebagai {{actual}}", + "token-details": "Detail token", + "topic-name-error": "Gunakan hanya huruf kecil (a hingga z), angka & tanda hubung (-). Jangan gunakan tombol obrolan", + "transaction": "Transaksi", + "transaction-data": "Data transaksi", + "transaction-declined": "Transaksi ditolak", + "transaction-description": "Anggap itu selesai setelah 12 konfirmasi pada jaringan.", + "transaction-details": "Detil transaksi", + "transaction-failed": "transaksi gagal", + "transaction-history": "Sejarah transaksi", + "transaction-request": "Permintaan Transaksi", + "transaction-sent": "Transaksi terkirim", + "transaction-signed": "Transaksi telah berhasil ditandatangani", + "transactions": "Transaksi", + "transactions-filter-select-all": "Pilih Semua", + "transactions-filter-title": "Filter riwayat", + "transactions-history": "Sejarah transaksi", + "transactions-history-empty": "Belum ada transaksi dalam riwayat Anda", + "transactions-history-loading": "Memuat riwayat transaksi. Ini mungkin perlu waktu.", + "transactions-load-more": "Muat lebih banyak", + "transactions-sign": "Tanda", + "tribute-required-by-multiaccount": "{{multiaccount-name}} membutuhkan SNT untuk memulai obrolan.", + "tribute-state-paid": "Upeti dibayar", + "tribute-state-pending": "Upeti tertunda", + "tribute-state-required": "Membutuhkan {{snt-amount}} upeti SNT", + "tribute-to-talk": "Upeti untuk berbicara", + "tribute-to-talk-add-friends": "Tambahkan teman sebagai kontak untuk memungkinkan obrolan tanpa pembayaran upeti.", + "tribute-to-talk-are-you-friends": "Apakah kalian berteman?", + "tribute-to-talk-ask-to-be-added": "Minta ditambahkan sebagai kontak", + "tribute-to-talk-contact-received-your-tribute": "menerima upeti Anda. Sekarang Anda dapat saling mengobrol dengan aman.", + "tribute-to-talk-desc": "Dapatkan uang dari perhatian Anda dengan meminta SNT agar orang baru memulai obrolan", + "tribute-to-talk-disabled": "Tribute to Talk dinonaktifkan", + "tribute-to-talk-disabled-note": "Mulai sekarang, orang baru dapat memulai obrolan dengan Anda tanpa mengirim SNT.", + "tribute-to-talk-enabled": "Anda mengaktifkan Tribute to Talk.", + "tribute-to-talk-finish-desc": "Mulai sekarang, Anda hanya akan menerima obrolan dari kontak, dan orang-orang yang membayar", + "tribute-to-talk-learn-more-1": "Waktu dan perhatian Anda adalah aset Anda yang paling berharga. Tribute to Talk memungkinkan Anda mengatur jumlah SNT yang diperlukan bagi orang baru untuk memulai obrolan dengan Anda.", + "tribute-to-talk-learn-more-2": "Siapa pun yang tidak ada dalam daftar kontak Anda akan diminta untuk membayar, dan Anda dapat merespons begitu mereka memilikinya.", + "tribute-to-talk-learn-more-3": "Anda selalu dapat mengirim uang kembali, tetapi untuk memastikan bahwa teman-teman dapat menghubungi Anda dengan bebas, tambahkan mereka sebagai kontak terlebih dahulu.", + "tribute-to-talk-paywall-learn-more-1": "Waktu dan perhatian kita adalah aset kita yang paling berharga. Tribute to Talk memungkinkan Anda menghubungi orang baru dengan imbalan pembayaran SNT.", + "tribute-to-talk-paywall-learn-more-2": "Untuk memulai obrolan dengan seseorang yang memiliki set upeti, cukup bayar SNT yang diperlukan dan Anda akan ditambahkan sebagai kontak.", + "tribute-to-talk-paywall-learn-more-3": "Jika Anda mengenal mereka, Anda dapat membagikan profil Anda di luar Status untuk ditambahkan secara gratis.", + "tribute-to-talk-pending": "Upeti konfirmasi tertunda", + "tribute-to-talk-pending-note": "Transaksi penghormatan sedang menunggu konfirmasi di jaringan. Anda dapat memeriksa statusnya dalam riwayat transaksi", + "tribute-to-talk-removing-note": "Menghapus Tribute to Talk akan memungkinkan orang baru memulai obrolan tanpa mengirim SNT. Membutuhkan transaksi yang harus dilakukan.", + "tribute-to-talk-set-snt-amount": "Tetapkan jumlah SNT yang diperlukan untuk orang baru untuk memulai obrolan", + "tribute-to-talk-signing": "Menunggu untuk tanda transaksi", + "tribute-to-talk-transaction-failed-note": "Transaksi gagal dan pengaturan Tribute to Talk Anda belum diubah", + "tribute-to-talk-tribute-received1": "Penghargaan diterima. Anda dan", + "tribute-to-talk-tribute-received2": "sekarang menjadi kontak dan dapat saling mengobrol dengan aman.", + "tribute-to-talk-you-require-snt": "Anda memerlukan SNT untuk orang baru untuk memulai obrolan.", + "try-again": "Coba lagi", + "try-keeping-the-card-still": "Cobalah menjaga kartu tetap", + "turn-nfc-description": "NFC dinonaktifkan pada perangkat Anda. Anda dapat mengaktifkannya di pengaturan", + "turn-nfc-on": "Aktifkan NFC untuk melanjutkan", + "tx-fail-description1": "Transaksi ini kemungkinan besar gagal. Tandatangani dengan risiko Anda sendiri menggunakan biaya jaringan khusus.", + "tx-fail-description2": "Transaksi ini kemungkinan besar gagal. Tetapkan biaya jaringan khusus untuk menandatangani dengan risiko Anda sendiri.", + "type": "Tipe", + "type-a-message": "Pesan", + "ulc-enabled": "ULC diaktifkan", + "unable-to-fetch": "Tidak dapat mengambil riwayat obrolan", + "unable-to-read-this-code": "Tidak dapat membaca kode ini", + "unable-to-send-messages": "Tidak dapat mengirim dan menerima pesan", + "unblock": "Bebaskan", + "unblock-contact": "Buka blokir pengguna ini", + "unknown-status-go-error": "Status-go error tidak dikenal", + "unlock": "Membuka kunci", + "unmute": "Suarakan", + "unpair-card": "Putuskan pasangan kartu", + "unpair-card-confirmation": "Operasi ini akan memutuskan pemasangan kartu dari perangkat saat ini. Membutuhkan otorisasi kode sandi 6 digit. Apakah Anda ingin melanjutkan?", + "unpaired-keycard-text": "Kartu kunci yang Anda ketuk tidak terkait dengan ponsel ini", + "unpaired-keycard-title": "Sepertinya kartu Anda tidak berpasangan", + "update": "Memperbarui", + "update-to-listen-audio": "Perbarui ke versi terbaru untuk mendengarkan pesan audio di sini!", + "update-to-see-image": "Perbarui ke versi terbaru untuk melihat gambar yang bagus di sini!", + "update-to-see-sticker": "Perbarui ke versi terbaru untuk melihat stiker yang bagus di sini!", + "url": "URL", + "usd-currency": "USD", + "use-valid-contact-code": "Silakan masukkan atau pindai kunci obrolan atau nama pengguna yang valid", + "validation-amount-invalid-number": "Jumlahnya bukan angka yang valid", + "validation-amount-is-too-precise": "Jumlahnya terlalu tepat. Jumlah desimal maksimum adalah {{decimals}} .", + "version": "Versi aplikasi", + "view-cryptokitties": "Lihat di CryptoKitties", + "view-cryptostrikers": "Lihat di CryptoStrikers", + "view-data": "Menampilkan data", + "view-details": "Melihat rincian", + "view-etheremon": "Lihat di Etheremon", + "view-gitcoin": "Lihat di Gitcoin", + "view-profile": "Tampilkan profil", + "view-public-dashboard": "Menampilkan dasbor publik", + "view-rules": "Lihat aturan", + "view-signing": "Lihat frasa penandatanganan", + "view-superrare": "Lihat di SuperRare", + "waiting-for-wifi": "Tidak ada Wi-Fi, sinkronisasi pesan dinonaktifkan.", + "waiting-for-wifi-change": "Pengaturan", + "waiting-to-sign": "Menunggu untuk tanda transaksi ...", + "waiting-wi-fi": "Menunggu Wi-Fi…", + "waku-bloom-filter-mode": "Mode filter mekar Waku", + "wallet": "Dompet", + "wallet-address": "Alamat Dompet", + "wallet-asset": "Aset", + "wallet-assets": "Aset", + "wallet-backup-recovery-title": "Cadangkan frase seed Anda", + "wallet-choose-recipient": "Pilih Penerima", + "wallet-collectibles": "Koleksi", + "wallet-insufficient-funds": "Dana tidak mencukupi", + "wallet-insufficient-gas": "ETH untuk gas tidak cukup", + "wallet-invalid-address": "Alamat tidak valid: \n {{data}}", + "wallet-invalid-address-checksum": "Kesalahan dalam alamat: \n {{data}}", + "wallet-invalid-chain-id": "Jaringan tidak cocok: \n {{data}} tetapi rantai saat ini adalah {{chain}}", + "wallet-key-content": "Alamat hex 64 karakter berdasarkan standar Ethereum dan dimulai dengan 0x. Menghadap publik, alamat akun Anda dibagikan kepada orang lain saat Anda ingin menerima dana. Juga disebut sebagai \"alamat Ethereum\" atau \"alamat dompet.\"", + "wallet-key-title": "Alamat akun", + "wallet-manage-assets": "Kelola aset", + "wallet-request": "Permintaan", + "wallet-send": "Kirim", + "wallet-send-min-units": "Min 21000 unit", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Pengaturan dompet", + "wallet-total-value": "Nilai total", + "wallet-transaction-total-fee": "Total Biaya", + "wants-to-access-profile": "ingin mengakses profil Anda", + "warning": "Peringatan", + "warning-message": "Maaf, kami membatasi pengiriman beberapa pesan secara berurutan untuk mencegah spam. Silakan coba lagi sebentar lagi", + "warning-sending-to-contract-descr": "Alamat yang Anda masukkan adalah kontrak pintar, mengirim dana ke alamat ini dapat mengakibatkan hilangnya dana. Untuk berinteraksi dengan DApp, buka DApp di Status DApp Browser.", + "watch-only": "Hanya menonton", + "web-view-error": "Tidak dapat memuat halaman", + "websites": "Situs web", + "webview-camera-permission-requests": "Permintaan izin kamera webview", + "webview-camera-permission-requests-subtitle": "Saat diaktifkan, situs web dan aplikasi dapat meminta untuk menggunakan kamera Anda", + "welcome-blank-message": "Obrolan Anda akan muncul di sini. Untuk memulai obrolan baru tekan tombol ⊕", + "welcome-screen-text": "Siapkan dompet Anda, undang teman untuk mengobrol \ndan jelajahi dapps populer!", + "welcome-to-status": "Selamat datang di Status!", + "welcome-to-status-description": "Siapkan dompet crypto Anda, undang teman untuk mengobrol dan jelajahi aplikasi yang didesentralisasi", + "what-is-shared": "Apa yang dibagikan?", + "whats-on-your-mind": "Apa yang sedang kamu pikirkan…", + "word-count": "Jumlah kata", + "word-n": "Word # {{number}}", + "word-n-description": "Untuk memeriksa apakah Anda telah membuat cadangan frase seed Anda dengan benar, masukkan kata # {{number}} atas.", + "words-n": { + "other": "kata-kata" + }, + "write-down-and-store-securely": "Tulis kode \n & simpan dengan aman", + "wrong-address": "Salah alamat", + "wrong-card": "Kartu yang salah", + "wrong-card-text": "Kartu yang disadap tidak sesuai dengan kunci yang Anda pilih", + "wrong-contract": "Kontrak salah", + "wrong-keycard-text": "Kartu kunci yang Anda ketuk tidak terkait dengan ponsel ini", + "wrong-keycard-title": "Sepertinya Anda sudah mengetuk \n kartu kunci yang salah", + "wrong-password": "Kata sandi salah", + "wrong-word": "Kata yang salah", + "yes": "Iya", + "you": "kamu", + "you-already-have-an-asset": "Anda sudah memiliki aset {{value}}", + "you-are-all-set": "Anda sudah siap!", + "you-are-all-set-description": "Jika Anda kehilangan telepon, Anda sekarang dapat mengakses dana dan kunci obrolan menggunakan frase benih Anda", + "you-can-change-account": "Anda dapat mengubah nama akun dan warna sesuai keinginan Anda", + "you-can-choose-preview-websites": "Anda dapat memilih dari situs web berikut ini yang dapat melihat pratinjau tautan deskripsi dan gambar dalam obrolan", + "you-can-fetch": "Anda dapat mengambil riwayat obrolan", + "you-dont-have-contacts": "Anda belum memiliki kontak apa pun.", + "you-dont-have-contacts-invite-friends": "Anda belum memiliki kontak.\nUndang teman Anda untuk mulai mengobrol.", + "you-dont-have-stickers": "Anda belum memiliki stiker", + "you-will-need-this-code": "Anda memerlukan kode ini untuk membuka Status dan menandatangani transaksi", + "you-will-start-from-scratch": "Anda akan mulai dari awal dengan satu set kunci baru", + "your-card-is-frozen": "Kartu Kunci Anda dibekukan. Setel ulang akses kartu", + "your-contact-code": "Memberi akses memberi otorisasi DApp ini untuk mengambil kunci obrolan Anda", + "your-data-belongs-to-you": "Jika Anda kehilangan frase seed Anda, Anda kehilangan data dan dana Anda", + "your-data-belongs-to-you-description": "Jika Anda kehilangan akses, misalnya dengan kehilangan telepon Anda, Anda hanya dapat mengakses kunci Anda dengan frase seed Anda. Tidak seorang pun, tetapi Anda memiliki frase benih Anda. Tuliskan. Amankan", + "your-keys": "Kunci Anda", + "your-recovery-phrase": "Frase seed Anda", + "your-recovery-phrase-description": "Ini adalah frase awal Anda. Anda menggunakannya untuk membuktikan bahwa ini adalah dompet Anda. Anda hanya bisa melihatnya sekali! Tulis di atas kertas dan simpan di tempat yang aman. Anda akan membutuhkannya jika Anda kehilangan atau menginstal ulang dompet Anda.", + "youre-on-mobile-network": "Anda berada di jaringan seluler" +} diff --git a/translations/it.json b/translations/it.json new file mode 100644 index 00000000000..5e4dc0e4672 --- /dev/null +++ b/translations/it.json @@ -0,0 +1,1713 @@ +{ + "You": "Tu", + "about-app": "A proposito di", + "about-key-storage-content": "Status non accederà mai alla tua chiave privata. Assicurati di eseguire il backup della tua frase di recupero. Se si perde il telefono è l'unico modo per accedere alle tue chiavi .", + "about-key-storage-title": "Informazioni sulla memorizzazione delle chiavi", + "about-names-content": "Nessuno può fingere di essere te! Sei anonimo di default e non dovrai mai rivelare il tuo vero nome. È possibile registrare un nome personalizzato per una piccola quota.", + "about-names-title": "I nomi non possono essere cambiati", + "about-sharing-data": "Informazioni sulla condivisione dei dati", + "accept": "Accetta", + "accept-and-add": "Accetta e aggiungi", + "accept-and-continue": "Accetta e continua", + "accept-and-share-address": "Accetta e condividi indirizzo", + "accept-new-chats-from": "Accetta nuove chat da", + "accept-status-tos-prefix": "Accetto Status", + "access-existing-keys": "Accedi a chiavi esistenti", + "access-key": "Accedi a chiavi esistenti", + "account-added": "Account aggiunto", + "account-color": "Colore dell'account", + "account-content": "Puoi considerare tuoi account in Status come dei conti in banca. Come tali hanno un numero di conto (indirizzo ethereum) e un saldo; puoi usare questi account per effettuare transazioni su Ethereum. Puoi avere più di un account nel tuo portafoglio, tutti accessibili appena fai login in Status.", + "account-exists-title": "Questo account esiste già!", + "account-is-used": "L'account è stato usato con Dapps nel browser.", + "account-name": "Nome utente", + "account-settings": "Impostazioni account", + "account-title": "Account", + "accounts": "Accounts", + "actions": "Azioni", + "active-online": "In linea", + "active-unknown": "Sconosciuto", + "activity": "Attività", + "add": "Aggiungi", + "add-a-watch-account": "Aggiungi un indirizzo di sola lettura", + "add-account": "Aggiungi un account", + "add-account-description": "Puoi importare qualsiasi tipo di account Ethereum per aggiungerlo al tuo portafoglio Status.", + "add-account-incorrect-password": "La password sembra essere errata. Inserisci la password che usi per sbloccare l'app.", + "add-an-account": "Aggiungi un account", + "add-bootnode": "Aggiungi bootnode", + "add-contact": "Aggiungi ai contatti", + "add-custom-token": "Aggiungi token personalizzato", + "add-favourite": "Aggiungi preferito", + "add-mailserver": "Aggiungi nodo Status", + "add-members": "Aggiungi partecipanti", + "add-network": "Aggiungi rete", + "add-new-contact": "Aggiungi nuovo contatto", + "add-nickname": "Aggiungi un soprannome (facoltativo)", + "add-node": "Aggiungi Nodo", + "add-private-key-account": "Aggiungi un account usando la tua chiave privata", + "add-seed-account": "Aggiungi account usando la tua frase mnemonica", + "add-to-contacts": "Aggiungi ai contatti", + "add-to-contacts-text": "Aggiungendo un utente all'elenco dei contatti, condividi l'indirizzo del tuo portafoglio", + "add-to-favourites": "Aggiungi ai preferiti", + "add-watch-account": "Aggiungi un account di sola lettura", + "address": "Indirizzo", + "address-or-ens-name": "Indirizzo o nome ENS", + "address-received": "Indirizzo ricevuto", + "address-request-accepted": "Richiesta di indirizzo accettata", + "address-requested": "Indirizzo richiesto", + "advanced": "Avanzate", + "advanced-settings": "Impostazioni avanzate", + "advertiser-description": "Hai scoperto Status grazie a un tuo contatto. Permetti a Status di controllare il tuo indirizzo IP solo questa volta in modo che il tuo referente venga premiato? Queste informazioni verranno utilizzate solo a questo scopo e verranno rimosse completamente dopo 7 giorni.", + "advertiser-starter-pack-accept": "Accetta", + "advertiser-starter-pack-decline": "Rifiuta", + "advertiser-starter-pack-description": "Ecco alcuni token per iniziare! Usali per ottenere adesivi, un nome ENS e per provare dapps", + "advertiser-starter-pack-title": "Starter Pack", + "advertiser-title": "Privacy attiva come impostazione predefinita", + "agree-by-continuing": "Continuando accetti \n i nostri", + "all": "Tutti", + "allow": "Consenti", + "allow-and-send": "Consenti e invia", + "allow-mention-notifications": "Mostra @ menzioni", + "allowing-authorizes-this-dapp": "Autorizzare questo DApp a recuperare l'indirizzo del tuo portafoglio e abilitare Web3", + "already-have-asset": "Hai già questo cryptoasset", + "amount": "Importo", + "anyone": "Chiunque", + "appearance": "Aspetto", + "apply": "Applica", + "approve": "Approva", + "approve-limit": "Approva il limite", + "approve-token": "Approva token", + "approve-token-contract-desc": "L'approvazione di un token con un contratto gli consente di spendere il saldo dei token. Se ritieni che un progetto non sia affidabile, non approvare il token con loro o approvare solo l'importo che utilizzerai con loro.", + "are-you-sure": "Sei sicuro?", + "are-you-sure-description": "Non sarai più in grado di vedere l'intera frase di recupero", + "are-you-sure-to-cancel": "Sei sicuro di voler cancellare ?", + "are-you-sure?": "Sei sicuro?", + "ask-in-status": "Fai una domanda o segnala un bug", + "at": "a", + "attribution-received": "{{attrib}} dei {{max}} bonus ricevuti", + "audio": "Audio", + "audio-recorder": "Registratore", + "audio-recorder-error": "Errore di registrazione", + "audio-recorder-max-ms-reached": "Tempo massimo di registrazione raggiunto", + "audio-recorder-permissions-error": "Devi dare l'autorizzazione per inviare messaggi audio", + "authorize": "Autorizza", + "available": "Disponibile", + "available-participants": { + "one": "Puoi selezione solo un'altro partecipante", + "other": "Puoi selezionare ancora {{count}} participanti" + }, + "back": "Indietro", + "back-up": "Backup", + "back-up-seed-phrase": "Backup frase di recupero", + "back-up-your-seed-phrase": "Backup frase di recupero", + "backing-up": "Backup in corso...", + "backup-disabled": "Disabilitato", + "backup-enabled": "Abilitato", + "backup-recovery-phrase": "Backup della frase di recupero", + "backup-settings": "Impostazioni di backup", + "backup-through-waku": "Backup tramite waku", + "bad-fees-description": "La tua tariffa prioritaria è inferiore ai nostri parametri suggeriti.", + "balance": "Saldo", + "begin-set-up": "Inizia la configurazione", + "below-base-fee": "tariffa massima inferiore alla tariffa base", + "biometric-auth-android-sensor-desc": "Sensore tattile", + "biometric-auth-android-sensor-error-desc": "Fallita", + "biometric-auth-android-title": "Autenticazione richiesta", + "biometric-auth-confirm-logout": "Ripetere il login", + "biometric-auth-confirm-message": "L'autenticazione biometrica è necessaria per continuare, se non è possibile si prega di sbloccare le chiavi usando la password o la codice di accesso.", + "biometric-auth-confirm-title": "Devi autenticarti!", + "biometric-auth-confirm-try-again": "Riprova", + "biometric-auth-error": "Impossibile eseguire l'autenticazione biometrica ({{code}})", + "biometric-auth-login-error-title": "Errore di autenticazione biometrica", + "biometric-auth-login-ios-fallback-label": "Inserisci la password", + "biometric-auth-reason-login": "Accedi in Status", + "biometric-auth-reason-verify": "Verifica autenticazione", + "biometric-disable-bioauth": "disabilita {{bio-type-label}}", + "biometric-disable-password-description": "Disabilitando questa opzione, si ", + "biometric-disable-password-title": "Disabilita il salvataggio della password", + "biometric-enable": "Se non vuoi digitare ogni volta la password per accedere all'app, abilita l'accesso via {{bio-type-label}}", + "biometric-enable-button": "Abilita {{bio-type-label}}", + "biometric-enable-keycard": "Se non vuoi utilizzare ogni volta la Keycard per accedere all'app, abilita l'accesso via {{bio-type-label}}", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "impronta digitale", + "biometric-secure-with": "Proteggi con {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "Password BIP39", + "blank-keycard-text": "È possibile procedere con la tua keycard una volta che hai generato le tue chiavi ed il tuo nome", + "blank-keycard-title": "Sembra che tu abbia usato \nuna keycard vuota", + "block": "Blocco", + "block-contact": "Blocca questo utente", + "block-contact-details": "Il blocco eliminerà i messaggi già ricevuti da questo utente e impedirà di riceverne altri", + "blocked-users": "Utenti bloccati", + "bootnode-address": "Indirizzo bootnode", + "bootnode-details": "Dettagli bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes abilitati", + "bootnodes-settings": "Impostazioni dei bootnode", + "browsed-websites": "La cronologia del browser verrà visualizzata qui", + "browser": "Browser", + "browser-not-secure": "La connessione non è sicura! Non firmare transazioni o inviare dati personali su questo sito.", + "browser-secure": "La connessione è sicura. Assicurati di fidarti davvero a questo sito prima di firmare transazioni o di inserire dati personali.", + "browsers": "Browsers", + "browsing-cancel": "Annulla", + "browsing-open-in-android-web-browser": "Apri in Android", + "browsing-open-in-ios-web-browser": "Apri in iOS", + "browsing-open-in-status": "Apri in Status", + "browsing-site-blocked-description1": "Abbiamo rilevato potenziali attività malevole da questo indirizzo. Per proteggere te e il tuo portafoglio, stiamobloccando la navigazione. \n\n Se ritieni che si tratti di un errore, faccelo sapere in", + "browsing-site-blocked-description2": "chat pubblica.", + "browsing-site-blocked-go-back": "Torna indietro", + "browsing-site-blocked-title": "Questo sito è bloccato", + "browsing-title": "Sfoglia", + "bug-report": "Segnala un bug", + "bug-report-description": "* Descrizione", + "bug-report-description-placeholder": "Obbligatorio, non può essere vuoto", + "bug-report-steps": "Passi per riprodurre", + "bug-report-steps-placeholder": "- apri l'app\n - fai qualcosa\n - e poi qualcos'altro...", + "bug-report-submit-email": "Invia via e-mail con l'archivio dei log", + "bug-report-submit-gh-issue": "Invia un problema GitHub senza log", + "bug-report-too-short-description": "La descrizione è troppo breve", + "build-yourself": "Per utilizzare l'applicazione senza queste Condizioni di servizio, è possibile creare la propria versione", + "buy-crypto": "Acquista criptovaluta", + "buy-crypto-choose-a-service": "Scegli un servizio che desideri utilizzare per acquistare criptovalute", + "buy-crypto-description": "Trova una dapp per acquistare cripto", + "buy-crypto-leaving": "Stai lasciando Status ed entrando in un sito web di terzi per completare il tuo acquisto", + "buy-crypto-title": "Sembra che il tuo portafoglio sia vuoto", + "camera-access-error": "Per concedere l'autorizzazione richiesta per la videocamera, andare alle impostazioni del sistema e assicurarsi che Status > Videocamera sia selezionato.", + "can-not-add-yourself": "Questo sei tu, per iniziare una chat scegli qualcun altro", + "can-send-messages": "Puoi inviare e ricevere nuovi messaggi", + "cancel": "Annulla", + "cancel-keycard-setup": "Annullare la configurazione della Keycard", + "cancelling": "Cancellazione in corso", + "cannot-read-card": "Non riesco a leggere la carta.\nPer favore, tienilo sul retro del telefono.", + "cannot-use-default-pin": "Il codice di accesso 000000 non è consentito.\nSi prega di utilizzare un altro numero", + "cant-open-public-chat": "Impossibile aprire la chat pubblica", + "cant-report-bug": "Impossibile segnalare un bug", + "card-is-blank": "Questa carta è vuota", + "card-reseted": "La carta è stata resettata", + "card-unpaired": "La carta è stata disaccoppiata dal dispositivo corrente", + "category": "Categoria", + "category-title": "Titolo della categoria", + "change-fleet": "Cambia fleet in {{fleet}}", + "change-logging-enabled": "Sei sicuro di voler {{enable}} il logging?", + "change-pairing": "Modifica il codice di accoppiamento", + "change-pairing-description": "La modifica del codice di abbinamento non influisce sugli abbinamenti correnti. Tuttavia, qualsiasi nuovo abbinamento richiederà il nuovo codice.", + "change-pairing-title": "Creare un nuovo codice di accoppiamento", + "change-passcode": "Cambia codice di accesso", + "change-password": "Cambia la password", + "change-pin": "Cambia codice di accesso a 6 cifre", + "change-puk": "Modifica il PUK a 12 cifre", + "change-tip": "Modifica mancia", + "changed-amount-warning": "L'importo è stato modificato da {{old}} a {{new}}", + "changed-asset-warning": "La risorsa è stata modificata da {{old}} a {{new}}", + "chaos-mode": "Modalità caos", + "chaos-unicorn-day": "Chaos Unicorn Day", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Chatta ed effettua transazioni in privato con gli amici", + "chat-is-a-contact": "È nella lista contatti", + "chat-is-not-a-contact": "Non è nella lista contatti", + "chat-key": "Chiave chat", + "chat-key-content": "I messaggi sul protocollo Status vengono inviati e ricevuti tramite chiavi di cifratura. La chiave di chat pubblica è una stringa di caratteri che condividete con gli altri in modo che possano inviarvi messaggi.", + "chat-key-title": "Chiave di chat", + "chat-link-previews": "Anteprime dei link della chat", + "chat-name": "Nome chat", + "chat-name-content": "Tre parole casuali, derivate algoritmicamente dalla chiave della chat e utilizzate come alias predefinito nella chat. I nomi delle chat sono unici; nessun altro utente può avere le stesse tre parole.", + "chat-name-title": "Nome della chat", + "chat-notification-preferences": "Impostazioni sulle notifiche", + "chat-settings": "Impostazioni chat", + "chats": "Chat", + "check-on-opensea": "Controlla su opensea", + "check-your-recovery-phrase": "Controlla la tua frase di recupero", + "choose-actions": "Scegli azioni", + "choose-authentication-method": "Scegli un metodo di autenticazione.", + "choose-new-location-for-keystore": "Scegli una nuova posizione per salvare il file del keystore", + "choose-storage": "Scegli l'archiviazione", + "clear": "Cancella tutto", + "clear-all": "Cancella tutto", + "clear-history": "Cancellare la cronologia", + "clear-history-action": "Cancella tutto", + "clear-history-confirmation": "Cancellare la cronologia?", + "clear-history-confirmation-content": "Sei sicuro di voler cancellare la cronologia di questa chat?", + "clear-history-title": "Cancellare la cronologia?", + "close": "Chiudi", + "close-all": "Chiudi tutto", + "close-app-button": "Conferma", + "close-app-content": "L'app si fermerà e si chiuderà. Quando la riapri, verrà utilizzata la rete selezionata", + "close-app-title": "Attenzione!", + "collectibles-leak-metadata": "Puoi visualizzare i tuoi NFT qui. Se lo fai, condividerai il tuo portafoglio e l'indirizzo IP", + "command-button-send": "Invia", + "communities": "Comunità", + "communities-alpha": "Comunità (alfa)", + "communities-enabled": "Comunità abilitate", + "communities-verified": "✓ Comunità di Status verificata", + "community": "Comunità", + "community-color": "Colore della comunità", + "community-color-placeholder": "Scegli un colore", + "community-edit-title": "Modifica comunità", + "community-emoji-thumbnail-title": "Miniatura", + "community-image-pick": "Scegli un'immagine", + "community-image-remove": "Rimuovi", + "community-image-take": "Scatta una foto", + "community-info": "Informazioni sulla comunità", + "community-info-not-found": "Informazioni sulla comunità non trovate", + "community-invite-title": "Invita", + "community-key": "Chiave privata della comunità", + "community-key-placeholder": "Digita la chiave privata della community", + "community-link": "Link alla comunità", + "community-members": { + "one": "membro", + "other": "membri" + }, + "community-members-title": "Partecipanti", + "community-message-preview": "Invito a unirsi a {{nome della comunità}}", + "community-private-key": "Chiave privata della comunità", + "community-requests-to-join-title": "Richieste di adesione", + "community-roles": "Ruoli", + "community-share-title": "Condividi", + "community-thumbnail-image": "Immagine di anteprima", + "community-thumbnail-upload": "Carica", + "complete-hardwallet-setup": "La carta è ora collegata. Sarà richiesta per firmare le tue transazioni e sbloccare le tue chiavi.", + "completed": "Completato", + "confirm": "Conferma", + "confirm-new-password": "Conferma la nuova password", + "confirm-pairing-code-placeholder": "Conferma il codice di accoppiamento", + "confirm-password-placeholder": "Conferma la tua password...", + "confirmation-request": "Richiesta di conferma", + "confirmations": "Conferme", + "confirmations-helper-text": "Quando la transazione ha 12 conferme si può considerare conclusa.", + "connect": "Collega", + "connect-mailserver-content": "Connettersi a {{name}} ?", + "connect-wallet": "Collega portafoglio", + "connected": "Connesso", + "connected-to": "Connesso a", + "connecting": "Connessione in corso...", + "connecting-requires-login": "La connessione ad un'altra rete richiede il login", + "connection-status": "Stato della connessione", + "connection-with-the-card-lost": "La connessione con la carta \n è stata persa", + "connection-with-the-card-lost-setup-text": "Per riprendere l'installazione, tenere\nla carta appoggiata alla parte posteriore\ndel telefono e mantenere contatto.", + "connection-with-the-card-lost-text": "Per procedere, tieni la carta sul retro del telefono", + "contact-code": "Chiave chat", + "contact-s": { + "one": "contatto", + "other": "contatti" + }, + "contacts": "Contatti", + "contacts-descr": "I tuoi contatti appariranno qui. Riceverai aggiornamenti di stato da chiunque aggiungerai come contatto", + "contacts-empty": "I contatti con nomi ENS verranno visualizzati qui", + "continue": "Continua", + "continue-anyway": "Continua comunque", + "contract-address": "Indirizzo del Contract", + "contract-interaction": "Interazione con Smart Contract", + "contract-isnt-supported": "Il contratto non è supportato", + "copy-info": "Copia info", + "copy-qr": "Copia codice", + "copy-to-clipboard": "Copia", + "copy-transaction-hash": "Copia ID transazione", + "cost-fee": "Costo\/Tassa", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Crea", + "create-a-pin": "Crea un codice di accesso a 6 cifre", + "create-a-puk": "Crea il nuovo PUK di 12 caratteri", + "create-category": "Crea Categoria", + "create-channel": "Crea un canale", + "create-channel-title": "Nuovo canale", + "create-community": "Crea una comunità", + "create-group-chat": "Crea chat di gruppo", + "create-multiaccount": "Genera le chiavi", + "create-new-key": "Ottieni nuove chiavi", + "create-pin": "Crea un codice di accesso a 6 cifre", + "create-pin-description": "Per sbloccare Status e confermare le transazioni avrai bisogno della tua carta + questo codice di accesso a 6 cifre", + "created-group-chat-description": "Hai creato il gruppo {{group-name}}", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Valuta", + "currency-display-name-aed": "Dirham degli Emirati", + "currency-display-name-afn": "Afghani", + "currency-display-name-ars": "Peso argentino", + "currency-display-name-aud": "Dollaro Australiano", + "currency-display-name-bbd": "Dollaro delle Barbados", + "currency-display-name-bdt": "Taka bengalese", + "currency-display-name-bgn": "Lev bulgaro", + "currency-display-name-bhd": "Dinaro del Bahrein", + "currency-display-name-bnd": "Dollaro del Brunei", + "currency-display-name-bob": "Boliviano", + "currency-display-name-brl": "Real brasiliano\n", + "currency-display-name-btn": "Ngultrum del Bhutan", + "currency-display-name-cad": "Dollaro canadese", + "currency-display-name-chf": "Franco Svizzero", + "currency-display-name-clp": "Peso Cileno", + "currency-display-name-cny": "Renminbi cinese", + "currency-display-name-cop": "Peso colombiano", + "currency-display-name-crc": "Colón Costaricano", + "currency-display-name-czk": "Corona Ceca", + "currency-display-name-dkk": "Corona Danese", + "currency-display-name-dop": "Peso della Repubblica Dominicana", + "currency-display-name-egp": "Sterlina egiziana", + "currency-display-name-etb": "Birr etiope", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Sterlina Inglese", + "currency-display-name-gel": "Lari georgiano", + "currency-display-name-ghs": "Cedi ghanese", + "currency-display-name-hkd": "Dollaro di Hong Kong", + "currency-display-name-hrk": "Kuna croata", + "currency-display-name-huf": "Fiorino Ungherese", + "currency-display-name-idr": "Rupia indonesiana", + "currency-display-name-ils": "Shekel Israeliano", + "currency-display-name-inr": "Rupia Indiana", + "currency-display-name-isk": "Corona Islandese", + "currency-display-name-jmd": "Dollaro giamaicano", + "currency-display-name-jpy": "Yen Giapponese", + "currency-display-name-kes": "Scellino keniota", + "currency-display-name-krw": "Won sudcoreano", + "currency-display-name-kwd": "Dinaro kuwaitiano", + "currency-display-name-kzt": "Kazakistan Tenge", + "currency-display-name-lkr": "Rupia dello Sri Lanka", + "currency-display-name-mad": "Dirham Marocchino", + "currency-display-name-mdl": "Leu moldavo", + "currency-display-name-mur": "Rupia delle Mauritius", + "currency-display-name-mwk": "Kwacha malawiano", + "currency-display-name-mxn": "Peso messicano", + "currency-display-name-myr": "Ringgit Malesiano", + "currency-display-name-mzn": "Metical del Mozambico", + "currency-display-name-nad": "Dollaro della Namibia", + "currency-display-name-ngn": "Naira nigeriano", + "currency-display-name-nok": "Corona norvegese", + "currency-display-name-npr": "Rupia del Nepal", + "currency-display-name-nzd": "Dollaro Neozelandese", + "currency-display-name-omr": "Oman Rial", + "currency-display-name-pen": "Nuevo sol peruviano", + "currency-display-name-pgk": "Kina papuana", + "currency-display-name-php": "Peso delle Filippine", + "currency-display-name-pkr": "Rupia del Pakistan", + "currency-display-name-pln": "Zloty Polacco", + "currency-display-name-pyg": "Guarani paraguaiano", + "currency-display-name-qar": "Riyal del Qatar", + "currency-display-name-ron": "Leu rumeno", + "currency-display-name-rsd": "Dinaro serbo", + "currency-display-name-rub": "Rublo russo", + "currency-display-name-sar": "Arabia Saudita Riyal", + "currency-display-name-sek": "Corona Svedese", + "currency-display-name-sgd": "Dollaro di Singapore", + "currency-display-name-thb": "Baht thailandese", + "currency-display-name-try": "Lira turca", + "currency-display-name-ttd": "Dollaro di Trinidad e Tobago", + "currency-display-name-twd": "Dollaro taiwanese", + "currency-display-name-tzs": "Scellino tanzaniano", + "currency-display-name-uah": "Grivnia Ucraina", + "currency-display-name-ugx": "Scellino ugandese", + "currency-display-name-usd": "Dollaro Americano", + "currency-display-name-uyu": "Peso dell'Uruguay", + "currency-display-name-vef": "Bolívar venezuelano", + "currency-display-name-vnd": "Dong Vietnamita", + "currency-display-name-zar": "Rand sudafricano", + "current": "Attuale", + "current-average": "Fee media attuale", + "current-average-tip": "Attuale mancia media", + "current-base": "Fee Base attuale", + "current-base-fee": "Tariffa base attuale", + "current-minimum-tip": "Attuale mancia minima", + "current-network": "Rete corrente", + "current-password": "Password attuale", + "current-pin": "Inserisci il codice di accesso a 6 cifre", + "current-pin-description": "Inserisci il tuo codice di accesso a 6 cifre per procedere", + "custom": "Personalizzato", + "custom-networks": "Reti personalizzate", + "custom-node": "Stai utilizzando un endpoint RPC personalizzato. La cronologia dei trasferimenti locali potrebbe essere incompleta.", + "custom-seed-phrase": "Frase di recupero errata", + "custom-seed-phrase-text-1": "Questa frase di recupero contiene parole non presenti nel nostro dizionario. Controlla se ci sono errori.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Accetta e apri", + "dapp-starter-pack-description": "Ecco alcuni token per iniziare! Usali per ottenere adesivi, un nome ENS e per provare dapps", + "dapp-starter-pack-title": "Pacchetto di benvenuto", + "dapp-would-like-to-connect-wallet": "vorrebbe connettersi a", + "dapps": "ÐApps", + "dapps-permissions": "Autorizzazioni DApp", + "dark": "Scuro", + "data": "Dati", + "data-collected": "Dati raccolti", + "data-collected-subtitle": "La tabella seguente mostra i dati esatti che vengono memorizzati e verranno inviati. I dati vengono convalidati rispetto alle regole pubbliche per garantire che non vengano inviati dati sensibili. Non fidarti, verifica.", + "data-syncing": "Sincronizzazione in corso", + "database-reset-content": "Le chat, i contatti e le impostazioni sono stati cancellati. Puoi usare il tuo account con la tua Keycard", + "database-reset-title": "Resetta il database", + "database-reset-warning": "Il database verrà ripristinato. Chat, contatti e impostazioni verranno eliminati", + "datetime-ago": "fa", + "datetime-ago-format": "{{number}}{{time-intervals}}{{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "one": "giorno", + "other": "giorni" + }, + "datetime-day-short": { + "one": "G", + "other": "GG" + }, + "datetime-hour": { + "one": "ora", + "other": "ore" + }, + "datetime-hour-short": { + "one": "O", + "other": "O" + }, + "datetime-minute": { + "one": "minuto", + "other": "minuti" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "secondo", + "other": "secondi" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "oggi", + "datetime-yesterday": "Ieri", + "decimals": "Decimali", + "decline": "Rifiuta", + "decryption-failed-content": "Non siamo stati in grado di decrittografare i tuoi dati. Potresti dover cancellare i tuoi vecchi dati e creare un nuovo account. Tocca su “Applica” per cancellare e ricreare, \"Annulla\" per riprovare.", + "default": "Predefinito", + "default-assets": "ERC20 e ERC721 predefiniti", + "default-sync-period": "Sincronizza cronologia per", + "delete": "Elimina", + "delete-account": "Elimina account", + "delete-and-leave-group": "Elimina e lascia il gruppo", + "delete-bootnode": "Elimina bootnode", + "delete-bootnode-are-you-sure": "Sei sicuro di voler eliminare questo bootnode?", + "delete-bootnode-title": "Elimina bootnode", + "delete-category-confirmation": "Sei sicuro di voler eliminare questa categoria?", + "close-chat": "Elimina chat", + "close-chat-confirmation": "Sei sicuro di voler eliminare questa chat?", + "delete-confirmation": "Eliminare?", + "delete-keys-keycard": "Elimina chiavi da Keycard", + "delete-mailserver": "Elimina nodo Status", + "delete-mailserver-are-you-sure": "Sei sicuro di voler eliminare questo nodo Status?", + "delete-mailserver-title": "Elimina il nodo Status", + "delete-message": "Cancella il messaggio", + "delete-my-account": "Cancella il mio account", + "delete-my-profile": "Elimina il mio profilo", + "delete-network-confirmation": "Sei sicuro di voler eliminare questa rete?", + "delete-network-error": "Si prega di connettersi ad un'altra rete prima di cancellare questa", + "delete-network-title": "Cancellare la rete?", + "delete-node": "Nodo cancellato", + "delete-node-are-you-sure": "Sei sicuro di voler cancellare questo Nodo?", + "delete-node-title": "Cancella Nodo", + "delete-profile": "Elimina profilo", + "delete-profile-warning": "Attenzione: se non hai scritto la tua frase di sicurezza, perderai l'accesso ai tuoi fondi dopo aver eliminato il tuo profilo", + "deny": "Nega", + "derivation-path": "Percorso di derivazione", + "describe-channel": "Descrivi il canale", + "description": "Descrizione", + "dev-mode": "Modalità di sviluppo", + "dev-mode-settings": "Impostazioni modalità di sviluppo", + "device-syncing": "Sincronizzazione del dispositivo", + "devices": "Dispositivi", + "disable": "disabilitare", + "disable-all": "Disabilita tutto", + "disable-later-in-settings": "Puoi disabilitarlo in seguito in Impostazioni", + "disabled": "Disabilitato", + "disconnected": "Chat offline", + "discover": "Scopri", + "dismiss": "Chiudi", + "display-collectibles": "Mostra oggetti da collezione", + "done": "Fatto", + "dont-ask": "Non chiedermelo più", + "edit": "Modifica", + "edit-channel-title": "Modifica canale", + "edit-chats": "Modifica le chat", + "edit-community": "Modifica comunità", + "edit-favourite": "Modifica preferito", + "edit-group": "Modifica gruppo", + "edit-profile": "Modifica Profilo", + "edited": "Modificato", + "editing-message": "Modifica del messaggio", + "empty-activity-center": "Le tue notifiche della chat\nappariranno qui", + "empty-chat-description": "Non ci sono ancora\n messaggi in questa chat", + "empty-chat-description-community": "È stato tranquillo qui nelle ultime {{quiet-hours}} .", + "empty-chat-description-one-to-one": "Tutti i messaggi che invii qui sono crittografati e possono essere letti solo da te e ", + "empty-chat-description-public": "Nelle ultime {{quiet-hours}} non si è chattato. Inizia la conversazione o ", + "empty-chat-description-public-share-this": "condividi questa chat.", + "empty-keycard-required": "Richiede una Keycard vuota", + "empty-pending-invitations-descr": "Le persone che desiderano unirsi al gruppo\ntramite un link di invito appariranno qui", + "empty-tab": "Scheda Vuota", + "enable": "Abilitare", + "enable-all": "Abilita tutto", + "enable-link-previews": "Abilitare le anteprime dei collegamenti nella chat?", + "encrypt-with-password": "Codifica con la tua password", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Aggiungi nome utente", + "ens-agree-to": "Accetto", + "ens-chat-settings": "Impostazioni chat", + "ens-custom-domain": "Dominio personalizzato", + "ens-custom-username-hints": "Digita l'intero nome utente incluso il dominio personalizzato come nomeutente.dominio.eth", + "ens-custom-username-taken": "Il nome utente non ti appartiene :(", + "ens-deposit": "Deposita", + "ens-dismiss-message": "Clicca qui per chiudere", + "ens-displayed-with": "I tuoi messaggi vengono visualizzati agli altri con", + "ens-get-name": "Ottieni un nome utente universale", + "ens-got-it": "Ok capito", + "ens-locked": "Nome utente bloccato. Non sarà possibile rilasciarlo fino al {{date}}", + "ens-name-content": "Alias personalizzato per la tua chiave di chat che puoi registrare usando Ethereum Name Service. I nomi ENS sono nomi utente decentralizzati.", + "ens-name-not-found": "Impossibile risolvere il nome ENS", + "ens-name-title": "Nome ENS", + "ens-network-restriction": "Disponibile solo su Mainnet", + "ens-no-usernames": "Non hai nessun nome utente collegato", + "ens-powered-by": "Offerto da Ethereum Name Services", + "ens-primary-username": "Nome utente principale", + "ens-register": "Registrati", + "ens-registration-failed": "Per registrare il nome utente, per favore prova di nuovo.", + "ens-registration-failed-title": "Transazione fallita", + "ens-registration-failure": "Registrazione fallita", + "ens-registration-in-progress": "Registrazione in corso...", + "ens-release-username": "Rilascia nome utente", + "ens-remove-hints": "La rimozione scollegherà il nome utente dalla chiave.", + "ens-remove-username": "Rimuovi nome utente", + "ens-saved": "è ora collegato con la chiave di chat e può essere utilizzato in Status.", + "ens-saved-title": "Nome utente aggiunto", + "ens-show-username": "Mostra il mio nome utente ENS nelle chat", + "ens-terms-header": "Termini di registrazione del nome", + "ens-terms-point-1": "I fondi vengono depositati per 1 anno. Il tuo SNT sarà bloccato, ma non speso.", + "ens-terms-point-10": "0x314159265dd8dbb310642f98f50c066173c1259b (Registro ENS).", + "ens-terms-point-2": "Dopo 1 anno, puoi rilasciare il nome e riavere il tuo deposito oppure non agire per mantenerlo.", + "ens-terms-point-3": "Se i termini del contratto cambiano - ad es. Status effettua aggiornamenti del contratto - l'utente ha il diritto di rilasciare il nome utente indipendentemente dal tempo trascorso.", + "ens-terms-point-4": "Il controllore del contract non può accedere ai fondi depositati. Possono solo essere spostati indietro all'indirizzo che li ha inviati.", + "ens-terms-point-5": "I tuoi indirizzi saranno associati pubblicamente al tuo nome ENS.", + "ens-terms-point-6": "I nomi utente vengono creati come nodi sottodominio di stateofus.eth e sono soggetti ai termini dello Smart Contract ENS.", + "ens-terms-point-7": "Autorizzi il contract a trasferire SNT per tuo conto. Ciò può verificarsi solo quando si approva una transazione per autorizzare il trasferimento.", + "ens-terms-point-8": "Questi termini sono garantiti dalla logica dello Smart Contract agli indirizzi:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Condizioni di registrazione del nome.", + "ens-test-message": "Hey", + "ens-transaction-pending": "Transazione in attesa...", + "ens-understand": "Comprendo che il mio indirizzo di portafoglio sarà collegato pubblicamente al mio nome utente.", + "ens-username": "Nome ENS", + "ens-username-already-added": "Il nome utente è già collegato alla tua chiave di chat e può essere utilizzato all'interno di Status.", + "ens-username-available": "✓ Nome utente disponibile!", + "ens-username-connected": "Questo nome utente è di tua proprietà ed è associato alla tua chiave di chat.", + "ens-username-connected-continue": "Continua a impostare `Mostra il mio nome utente ENS nelle chat`.", + "ens-username-connected-with-different-key": "Per continuare sarà necessaria una transazione per collegare il nome utente con la tua chiave di chat.", + "ens-username-connection-confirmation": "{{username}} verrà collegato una volta completata la transazione.", + "ens-username-hints": "Almeno 4 caratteri. Solo lettere latine, numeri e lettere minuscole.", + "ens-username-invalid": "Solo lettere e numeri.", + "ens-username-owned": "✓ Il nome utente è di tua proprietà.", + "ens-username-owned-continue": "Continuando assocerai questo nome utente alla tua chiave di chat.", + "ens-username-registration-confirmation": "Bello! Una volta completata la transazione possiederai {{username}}.", + "ens-username-taken": "Nome utente già preso :(", + "ens-username-you-can-follow-progress": "Puoi seguire i progressi nella sezione Cronologia transazioni del tuo portafoglio.", + "ens-usernames": "Nomi utente ENS", + "ens-usernames-details": "Registrare un nome utente universale per essere facilmente riconoscibile dagli altri utenti", + "ens-want-custom-domain": "Possiedo un nome su un altro dominio", + "ens-want-domain": "Voglio un dominio stateofus.eth", + "ens-welcome-hints": "I nomi ENS trasformano quei lunghi e folli indirizzi in nomi utente unici.", + "ens-welcome-point-customize": "Un nome ENS può sostituire il tuo nome casuale di 3 parole nella chat. Diventa @nomeutente anziché {{name}} .", + "ens-welcome-point-customize-title": "Personalizza il tuo nome in chat", + "ens-welcome-point-receive": "In un semplice passaggio, altri utenti possono inviarti dei fondi via chat.", + "ens-welcome-point-receive-title": "Ricevi transazioni in chat", + "ens-welcome-point-register": "Registra il nome una sola volta per tenerlo per sempre. Dopo un anno puoi rilasciarlo e riavere indietro i tuoi SNT.", + "ens-welcome-point-register-title": "10 SNT per la registrazione", + "ens-welcome-point-simplify": "Puoi ricevere fondi tramite il tuo nome ENS invece di dover condividere il tuo indirizzo esadecimale (0x...).", + "ens-welcome-point-simplify-title": "Semplifica il tuo indirizzo ETH", + "ens-welcome-point-verify": "Puoi verificare e aggiungere qualsiasi nome utente che già possiedi.", + "ens-welcome-point-verify-title": "Possiedi già un nome utente?", + "ens-your-username": "Il tuo nome utente", + "ens-your-usernames": "I tuoi nomi utente", + "ens-your-your-name": "Il tuo nome ENS", + "enter-12-words": "Inserisci le 12 parole della frase di recupero, separate da spazi singoli", + "enter-a-private-key": "Inserisci una chiave privata", + "enter-a-seed-phrase": "Inserisci la tua frase di recupero", + "enter-address": "Inserisci indirizzo", + "enter-contact-code": "Chiave ENS (vitalik94) o chat (0x04…)", + "enter-pair-code": "Inserisci il tuo codice di accoppiamento", + "enter-pair-code-description": "Il codice di accoppiamento può essere impostato da un client di Status già associato", + "enter-password": "Inserire la password", + "enter-password-migration-prompt": "Inserisci la tua password per spostare contatti, chat e impostazioni insieme alle tue chiavi", + "enter-pin": "Inserisci il codice di accesso a 6 cifre", + "enter-puk-code": "Inserisci il codice PUK", + "enter-puk-code-description": "Il codice di accesso a 6 cifre è stato bloccato.\n Inserisci il codice PUK per sbloccare il codice di accesso.", + "enter-recipient-address-or-username": "Immettere l'indirizzo o il nome utente del destinatario", + "enter-seed-phrase": "Inserisci la tua frase di recupero", + "enter-url": "Inserisci l'URL", + "enter-user-pk": "Inserisci la chiave pubblica dell'utente", + "enter-watch-account-address": "Scansiona un QR code \no \ninserisci l'indirizzo da tenere d'occhio", + "enter-word": "Inserisci parola", + "enter-your-code": "Inserisci il tuo codice di accesso a 6 cifre", + "enter-your-password": "Inserisci la tua password...", + "error": "Errore", + "error-unable-to-get-balance": "Impossibile ottenere il saldo", + "error-unable-to-get-prices": "Errore di conversione valuta. Aggiorna lo schermo per riprovare.", + "error-unable-to-get-token-balance": "Impossibile ottenere il saldo token", + "errors": "Errori", + "eth": "ETH", + "ethereum-account": "Profilo Ethereum", + "ethereum-address": "Indirizzo di Ethereum", + "ethereum-node-started-incorrectly-description": "Il nodo Ethereum è stato avviato con una configurazione errata, l'applicazione verrà arrestata per ripristinare tale condizione. ID di rete configurato = {{network-id}} , effettivo = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Il nodo Ethereum è stato avviato in modo errato", + "etherscan-lookup": "Guarda su Etherscan", + "everyone": "Tutti", + "expand-all": "Espandi", + "export-account": "Esporta account", + "export-key": "Esporta chiave privata", + "external-storage-denied": "L'accesso alla memoria esterna è negato", + "failed": "Fallita", + "faq": "Domande frequenti", + "fast": "Veloce", + "favourite": "Preferiti", + "favourite-description": "I tuoi siti web preferiti verranno visualizzati qui", + "favourites": "Preferiti", + "favourites-empty": "Gli indirizzi aggiunti ai preferiti verranno visualizzati qui", + "fee-cap": "Limite tariffa", + "fee-explanation": "Prezzo complessivo massimo per la transazione. Se la tariffa di base del blocco supera questa cifra, verrà inclusa in un blocco successivo con una tariffa di base inferiore.", + "fee-options": "Opzioni suggerite per la tariffa", + "fetch-community": "Recupera la comunità", + "fetch-messages": "↓ Recupera messaggi", + "fetch-timeline": "Carica", + "fetching-community": "Recupero della comunità in corso...", + "find": "Trova", + "finish": "Finito", + "finishing-card-setup": "Completamento della configurazione della carta", + "fleet": "Fleet", + "fleet-settings": "Impostazioni Fleet", + "follow": "Segui", + "follow-your-interests": "Entra in una chat pubblica e incontra nuove persone", + "free": "Gratis", + "from": "Da", + "gas-amount-limit": "Limite valore Gas", + "gas-limit": "Limite di gas", + "gas-price": "Prezzo del gas", + "gas-used": "Gas utilizzato", + "generate-a-key": "Genera le chiavi", + "generate-a-new-account": "Genera un nuovo account", + "generate-a-new-key": "Genera nuove chiavi", + "generate-account": "Genera le chiavi", + "generate-an-account": "Genera un account", + "generate-new-key": "Genera le chiavi", + "generating-codes-for-pairing": "> Download del software di prodotto sulla scheda \n > Generazione di codici di sblocco e associazione", + "generating-keys": "Generazione delle chiavi in corso...", + "generating-mnemonic": "Generazione della frase di recupero", + "get-a-keycard": "Acquista una Keycard", + "get-started": "Iniziare", + "get-status-at": "Scarica Status su http:\/\/status.im", + "get-stickers": "Scarica adesivi", + "give-a-short-description": "Fornisci una breve descrizione", + "give-a-short-description-community": "Fornisci una breve descrizione", + "give-permissions-camera": "Dare il permesso \n per accedere alla telecamera", + "glossary": "Glossario", + "go-to-settings": "Vai alle impostazioni...", + "got-it": "Ho capito", + "grant-face-id-permissions": "Per concedere l'autorizzazione Face ID richiesta, andare alle impostazioni del sistema e assicurarsi che Status > Face ID sia selezionato", + "group-chat": "Chat di gruppo", + "group-chat-admin": "Amministratore", + "group-chat-admin-added": "** {{member}} ** è stato reso amministratore", + "group-chat-all-contacts-invited": "Tutti i tuoi contatti sono già nel gruppo", + "group-chat-created": "** {{member}} ** ha creato il gruppo ** {{name}} **", + "group-chat-decline-invitation": "Rifiuta l'invito", + "group-chat-member-added": "** {{member}} ** è stato invitato", + "group-chat-member-joined": "** {{member}} ** si è unito al gruppo", + "group-chat-member-removed": "** {{member}} ** ha lasciato il gruppo", + "group-chat-members-count": "{{selected}}\/{{max}} partecipanti", + "group-chat-name-changed": "** {{member}} ** ha cambiato il nome del gruppo in ** {{name}} **", + "group-chat-no-contacts": "Non hai ancora alcun contatto. \n Invita i tuoi amici a iniziare a chattare", + "group-info": "Informazioni sul gruppo", + "group-invite": "Invito di gruppo", + "group-invite-link": "Link di invito al gruppo", + "group-membership-request": "Richiesta di adesione al gruppo", + "gwei": "Gwei", + "has-permissions": "ha il permesso di accedere", + "hash": "Hash", + "help": "aiuto", + "help-capitalized": "Aiuto", + "help-center": "Centro Assistenza", + "help-improve-status": "Aiutaci a migliorare Status", + "hide": "Nascondi", + "hide-content-when-switching-apps": "Blocco degli screenshot", + "hide-content-when-switching-apps-ios": "Nascondi anteprima", + "history": "Cronologia", + "history-nodes": "Nodi Status", + "hold-card": "Tieni la carta sul retro \n del tuo telefono", + "home": "Home", + "hooks": "Hooks", + "how-it-works": "Come funziona", + "http-gateway-error": "Attenzione, richiesta non riuscita!", + "identifier": "Identificativo", + "image": "Immagine", + "image-remove-current": "Rimuovi la foto corrente", + "image-source-gallery": "Seleziona dalla galleria", + "image-source-make-photo": "Scatta foto", + "image-source-title": "Modifica foto", + "import": "Importa", + "import-community": "Importa una comunità", + "import-community-title": "Importa una comunità", + "in-contacts": "Nei contatti", + "include": "Include", + "incoming": "In arrivo", + "incoming-transaction": "Transazione in entrata", + "incorrect-code": [ + "str", + "Spiacenti, il codice non è corretto, ti preghiamo di inserirlo di nuovo" + ], + "increase-gas": "Aumenta il Gas", + "initialization": "Inizializzazione", + "install": "Installa", + "intro-message1": "Benvenuto in Status! \n Tocca questo messaggio per impostare la password e iniziare.", + "intro-privacy-policy-note1": "Status non raccoglie, non condivide e non vende dati personali. Continuando siete d'accordo con l'", + "intro-privacy-policy-note2": "Informativa sulla privacy", + "intro-text": "Status è la tua porta di accesso al web decentralizzato", + "intro-text1": "Chat su una rete peer-to-peer, criptata, dove i messaggi non possono essere censurati o hackerati", + "intro-text2": "Invia e ricevi cryptoasset in qualsiasi parte del mondo, non è necessario un conto bancario", + "intro-text3": "Esplora giochi, exchange e social network dove solo tu possiedi i tuoi dati", + "intro-title1": "Comunicazione veramente privata", + "intro-title2": "Proteggi il tuo portafoglio crypto", + "intro-title3": "App decentralizzate", + "intro-wizard-text1": "Una serie di chiavi controlla il tuo account. Le tue chiavi vivono sul tuo telefono, quindi solo tu puoi usarle", + "intro-wizard-text2": "Una chiave è per la chat. Ha un nome leggibile che non può essere modificato.", + "intro-wizard-text3": "Se possiedi una Keycard, memorizzaci tue chiavi per una maggiore sicurezza", + "intro-wizard-text4": "Proteggi e cripta le tue chiavi", + "intro-wizard-text6": "Status ti avviserà di nuovi messaggi. Puoi modificare le tue preferenze di notifica in un secondo momento nelle impostazioni", + "intro-wizard-title-alt4": "Crea una password", + "intro-wizard-title-alt5": "Conferma la tua password", + "intro-wizard-title1": "Ottieni le tue chiavi", + "intro-wizard-title2": "Scegli un nome per la chat", + "intro-wizard-title3": "Selezionare l'archivio delle chiavi", + "intro-wizard-title4": "Crea un codice di accesso a 6 cifre", + "intro-wizard-title5": "Conferma il codice di accesso", + "intro-wizard-title6": "Abilita le notifiche", + "introduce-yourself": "Presentati con un breve messaggio", + "invalid-address-qr-code": "Il QR code scansionato non contiene un indirizzo valido", + "invalid-format": "Formato non valido \n Deve essere {{format}}", + "invalid-key-confirm": "Applica", + "invalid-key-content": "Il database non può essere crittografato perché il file è danneggiato. I tuoi fondi e le chiavi sono al sicuro. Altri dati, come le tue chat la tua lista di contatti, non possono essere recuperati. Premendo \"{{erase-multiaccounts-data-button-text}}\" tutti i tuoi dati saranno cancellati ma potrai nuovamente accedere ai tuoi fondi e ricominciare a chattare.", + "invalid-number": "Numero non valido", + "invalid-pairing-password": "Password di associazione non valida", + "invalid-public-chat-topic": "Argomento della chat pubblica non valido", + "invalid-range": "Formato non valido, deve essere compreso tra {{min}} e {{max}}", + "invalid-username-or-key": "Nome utente o chiave chat non validi", + "invite": "Invita", + "invite-button": "Invita", + "invite-chat-accept": "Accetta", + "invite-chat-accept-join": "Accetta e unisciti", + "invite-chat-intro": "Sei stato invitato da un amico a usare Status. Ecco alcuni token per iniziare! Usalo per registrare un nome ENS o acquistare un pacchetto di adesivi", + "invite-chat-name": "Segnala amici", + "invite-chat-pending": "In attesa", + "invite-chat-rule": "L'accettazione ricompenserà anche il tuo amico con un bonus in token", + "invite-chat-starter-pack": "Starter Pack", + "invite-friends": "Invita amici", + "invite-instruction-fifth": "Puoi scegliere di riscattare il tuo bonus per la segnalazione in qualsiasi momento.", + "invite-instruction-first": "Invia un link univoco di invito al tuo amico per scaricare e iscriverti a Status", + "invite-instruction-fourth": "Ricevi il tuo bonus di segnalazione e il tuo amico lo Starter Pack", + "invite-instruction-second": "Il tuo amico scarica Status e crea un account (su Android)", + "invite-instruction-third": "E' stata avviata una chat con il tuo amico, dove confermare la tua segnalazione", + "invite-people": "Invita", + "invite-privacy-policy-public": "Hai installato Status tramite un link di riferimento. Unendoti a questa chat attribuisci il tuo referrer e accetti il", + "invite-privacy-policy1": "Accettando confermi di le condizioni del programma di affiliazione", + "invite-privacy-policy2": "Termini e condizioni.", + "invite-public-chat-home": "Invito referral", + "invite-public-chat-intro": "Ecco alcuni token per iniziare! Usali per ottenere adesivi, un nome ENS e per provare dapps", + "invite-receive-account": "Conto per ricevere il vostro bonus di affiliazione", + "invite-reward": "Guadagna token per ogni amico che inviti!", + "invite-reward-friend": "Amico: ", + "invite-reward-friend-description": "Il tuo amico riceverà uno Starter Pack composto da una {{{ricompensa}} per iniziare", + "invite-reward-friend-name": "Starter Pack", + "invite-reward-you": "Tu:", + "invite-reward-you-description": "Invita un amico e ricevi una {{{{reward}}}} in segno di gratitudine. Usala per comprare adesivi, un nome ENS e per provare le Dapps.", + "invite-reward-you-name": "Bonus per segnalazione", + "invite-select-account": "Seleziona il Conto per ricevere il vostro bonus di affiliazione", + "invite-warning": "Questa promozione è valida solo per gli utenti di un dispositivo Android, che non sono residenti negli Stati Uniti. L'amico\/a deve confermare il codice promozionale entro 7 giorni", + "invited": "invitato", + "join": "Iscriviti", + "join-a-community": "o unisciti ad una comunità", + "join-group-chat": "Unisciti al gruppo", + "join-group-chat-description": "{{username}} ti ha invitato a far parte del gruppo {{group-name}}", + "join-me": "Ciao, unisciti a me su Status: {{url}}", + "join-new-private-chat": "Avvia una nuova chat privata", + "join-new-public-chat": "Partecipa a una chat pubblica", + "joined": "Unito", + "joined-group-chat-description": "Ti sei iscritto a {{group-name}} dall'invito da {{username}}", + "key": "Chiave", + "key-managment": "Gestione delle chiavi", + "key-on-device": "La chiave privata è salvata su questo dispositivo", + "keycard": "Keycard", + "keycard-access-reset": "L'accesso alla keycard è stato reimpostato", + "keycard-applet-install-instructions": "Per installare l'applet si prega di seguire le istruzioni su https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Provate a spostare la carta per trovare il lettore NFC sul vostro dispositivo", + "keycard-awaiting-title": "Sto ancora cercando...", + "keycard-backup": "Crea una Keycard di backup", + "keycard-backup-success-body": "Carta di backup creata correttamente. Ora puoi usarla con il tuo account proprio come la carta principale.", + "keycard-backup-success-title": "Backup riuscito", + "keycard-blocked": "Keycard è stata bloccata. \n È necessario ripristinare la carta per continuare a utilizzarla.", + "keycard-can-use-with-new-passcode": "Puoi usare questa carta con il tuo nuovo codice di accesso", + "keycard-cancel-setup-text": "Questa operazione annullerà la configurazione della keycard. Si consiglia vivamente di completare la configurazione per utilizzare la chiave. Vuoi davvero annullare?", + "keycard-cancel-setup-title": "Operazione pericolosa", + "keycard-connected-description": "Prova a tenere ferma la carta", + "keycard-connected-title": "Connesso", + "keycard-desc": "Possiedi una Keycard? Memorizzaci tue chiavi; ne avrai bisogno per le transazioni", + "keycard-dont-ask-card": "Non chiedere la Keycard per accedere", + "keycard-enter-new-passcode": "Inserisci il nuovo codice di accesso {{step}} \/ 2", + "keycard-error-description": "Collegare nuovamente la carta per continuare", + "keycard-error-title": "Connessione persa", + "keycard-factory-reset": "Ripristina la carta alle impostazioni di fabbrica", + "keycard-factory-reset-text": "L'esecuzione di questa operazione cancellerà qualsiasi frase mnemonica memorizzata sulla carta. Assicurati di avere un backup della frase mnemonica che hai utilizzato con questa Keycard.", + "keycard-factory-reset-title": "Sei sicuro di voler eseguire un ripristino dei dati di fabbrica?", + "keycard-free-pairing-slots": "La carta Keycard ha {{n}} slot di accoppiamento liberi", + "keycard-has-multiaccount-on-it": "Questa carta è piena. Ogni carta può contenere una sola coppia di chiavi principali.", + "keycard-init-description": "Posiziona la carta sul retro del telefono per continuare", + "keycard-init-title": "Cerco le carte...", + "keycard-is-blocked-details": "Non è più possibile utilizzare questa carta per accedere o firmare per questo account. Ci sono stati troppi tentativi falliti di codice di accesso e PUK.", + "keycard-is-blocked-instructions": "Per accedere al tuo account dovrai ripristinare le impostazioni di fabbrica della tua carta. Tocca il pulsante in basso per avviare la procedura, ti servirà il tuo mnemonico.", + "keycard-is-blocked-title": "La keycard è bloccata", + "keycard-is-frozen-details": "Per proteggere i tuoi beni, la tua carta è congelata. Ripristina la tua carta per sbloccarla ed essere in grado di inviare transazioni. Puoi farlo con il tuo PUK o il tuo mnemonico.", + "keycard-is-frozen-factory-reset": "Ripristina con mnemonico", + "keycard-is-frozen-reset": "Ripristina con PUK", + "keycard-is-frozen-title": "La keycard è congelata", + "keycard-onboarding-finishing-header": "Finendo", + "keycard-onboarding-intro-header": "Conservare le tue chiavi su Keycard", + "keycard-onboarding-intro-text": "Preparati, potrebbero essere necessari alcuni minuti, ma è importante proteggere il tuo account", + "keycard-onboarding-mnemonic-text": "Avrai anche bisogno di un pezzo di carta e una penna per scrivere la tua frase seed.", + "keycard-onboarding-pairing-header": "Accoppiamento della carta in corso...", + "keycard-onboarding-pin-text": "Dovrai creare un passcode di 6 cifre che verrà utilizzato per proteggere l'accesso alla tua Keycard.", + "keycard-onboarding-preparing-header": "Preparazione della carta in corso...", + "keycard-onboarding-puk-code-header": "Scrivi i codici \n e conservarli in modo sicuro", + "keycard-onboarding-recovery-phrase-description": "Per riaccedere alle tue chiavi serve questa frase di recupero. Scrivila. Tienila al sicuro, offline e separata da questo dispositivo.", + "keycard-onboarding-recovery-phrase-header": "Backup della frase di recupero", + "keycard-onboarding-recovery-phrase-text": "Solo per i tuoi occhi. Questa è la magica frase di recupero usata per generare le tue chiavi.", + "keycard-onboarding-start-header": "Tieni la carta sul retro \n del tuo telefono per iniziare", + "keycard-onboarding-start-step1": "Crea un codice di accesso", + "keycard-onboarding-start-step1-text": "Circa 1 minuto. Creare un codice di accesso a 6 cifre per cifrare le tue chiavi", + "keycard-onboarding-start-step2": "Scrivi PUK e il codice di accoppiamento", + "keycard-onboarding-start-step2-text": "Circa 1 minuto. Avrai bisogno di un pezzo di carta e una matita per questo", + "keycard-onboarding-start-step3": "Eseguire il backup della frase di recupero", + "keycard-onboarding-start-step3-text": "Circa 1 minuto. Sono necessari anche un pezzo di carta e una matita.", + "keycard-onboarding-start-text": "E mantieni il contatto da carta a telefono \n durante l'installazione. L'installazione richiederà circa 4 minuti", + "keycard-processing-description": "Prova a tenere ferma la carta", + "keycard-processing-title": "Elaborazione...", + "keycard-recover": "carta smarrita o bloccata?", + "keycard-recover-text": "Se hai la tua frase mnemonica puoi creare una nuova Keycard associata a questo account. Puoi utilizzare una nuova Keycard o eseguire il ripristino delle impostazioni di fabbrica su una congelata.", + "keycard-recover-title": "Creare una nuova carta per questo account?", + "keycard-recovery-intro-button-text": "Inizia il recupero", + "keycard-recovery-intro-header": "Recupera le chiavi memorizzata sulla keycard", + "keycard-recovery-intro-text": "Se in precedenza hai generato le chiavi utilizzando una Keycard e ora desideri utilizzare le stesse chiave su questo dispositivo", + "keycard-recovery-no-key-header": "Non c'è niente da \nrecuperare qui", + "keycard-recovery-no-key-text": "La tua Keycard non contiene alcuna chiave. Per utilizzarla, genera una nuova chiave e scegli la tua Keycard come supporto di memorizzazione", + "keycard-recovery-phrase-confirm-header": "Conferma la frase di recupero", + "keycard-recovery-phrase-confirmation-text": "Non avrai una seconda possibilità! Se perdi o cambi telefono, o se perdi la tua keycard, potrai accedere alle tue chiavi soltanto usando la tua frase di recupero. Soltanto tu la possiedi. Scrivila e tienila in un posto sicuro.", + "keycard-recovery-phrase-confirmation-title": "Hai scritto la frase di recupero ?", + "keycard-recovery-success-header": "Le tue chiavi sono state\n recuperate con successo", + "keycard-redeem-title": "Riscatta su", + "keycard-redeem-tx": "Riscatta cryptoasset", + "keycard-redeem-tx-desc": "Tocca con la carta per firmare e ricevere cryptoasset", + "keycard-reset-passcode": "Reimposta il codice di accesso", + "keycard-success-description": "Ora è possibile rimuovere la carta", + "keycard-success-title": "Successo", + "keycard-unauthorized-operation": "Non sei autorizzato ad eseguire questa operazione.\n Usa una carta valida e riprovare.", + "keycard-upsell-subtitle": "Maggiore sicurezza e facilità d'uso", + "language": "Lingua", + "last-backup-performed": "Ultimo backup eseguito:", + "last-transaction": "Ultima transazione", + "learn-more": "Per saperne di più", + "learn-more-about-keycard": "Per saperne di più sulla Keycard", + "leave": "Lascia", + "leave-chat": "Lascia la chat", + "leave-chat-confirmation": "La cronologia chat verrà rimossa dal tuo dispositivo. Dopo essere rientrato, non sarai in grado di recuperare nulla della tua cronologia.", + "leave-community": "Abbandona la comunità", + "leave-confirmation": "Abbandona {{chat-name}}", + "leave-group": "Abbandona gruppo", + "left": "ha lasciato", + "les-ulc": "LES\/ULC", + "lets-go": "Andiamo", + "light": "Chiaro", + "limit": "Limite", + "linked-on": "Collegato il {{date}}", + "load-messages-before": "prima di {{date}}", + "load-more-messages": "↓ Recuperare altri messaggi", + "load-more-timeline": "Carica altro", + "loading": "Caricamento in corso...", + "local-notifications": "Notifiche locali", + "lock-app-with": "Blocca l'app con", + "log-level": "Livello log", + "log-level-settings": "Impostazioni livello log", + "logging": "Logging", + "logging-enabled": "Registrazione abilitata?", + "login-pin-description": "Inserisci il tuo codice di accesso a 6 cifre per sbloccare le tue chiavi.", + "logout": "Esci", + "logout-app-content": "L'account verrà disconnesso. Quando lo sblocchi di nuovo, verrà utilizzata la rete selezionata", + "logout-are-you-sure": "Sei sicuro di voler uscire?", + "logout-key-management": "È necessario disconnettersi per accedere alla gestione delle chiavi.", + "logout-title": "Uscire?", + "looking-for-cards": "Cerco le carte...", + "lost-connection": "Connessione persa", + "low-tip": "la mancia è troppo bassa", + "lower-than-average-tip": "mancia inferiore alla media", + "mail-should-be-configured": "Il client di posta deve essere configurato", + "mailserver-address": "Indirizzo del nodo Status", + "mailserver-automatic": "Selezione automatica", + "mailserver-automatic-switch-explanation": "Scegli il nodo Status più veloce che è disponibile", + "mailserver-connection-error": "Impossibile connettersi al nodo Status", + "mailserver-content": "Un nodo nella rete Status che instrada e archivia i messaggi, per un massimo di 30 giorni.", + "mailserver-details": "Dettagli del nodo Status", + "mailserver-error-content": "Impossibile raggiungere il nodo Status selezionato.", + "mailserver-error-title": "Errore durante la connessione al nodo Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Scegli un altro nodo Status", + "mailserver-reconnect": "Impossibile connettersi al nodo Status. Tocca per riconnetterti", + "mailserver-request-error-content": "Il seguente errore è stato restituito dal nodo Status: {{error}}", + "mailserver-request-error-status": "Si è verificato un errore durante il recupero della cronologia, controllare i log per i dettagli", + "mailserver-request-error-title": "Errore di richiesta al nodo Status", + "mailserver-request-retry": "Riprova richiesta", + "mailserver-retry": "Riprova", + "mailserver-title": "Nodo Status", + "main-currency": "Valuta principale", + "main-networks": "Reti principali", + "main-wallet": "Portafoglio principale", + "mainnet-network": "Rete principale", + "make-admin": "Rendi amministratore", + "manage-keys-and-storage": "Gestisci chiavi e spazio di archiviazione", + "mark-all-read": "Segna tutti come letti", + "master-account": "Conto principale", + "max-fee": "Tariffa massima", + "max-priority-fee": "Tariffa prioritaria massima", + "maximum-fee": "Tariffa massima", + "maximum-fee-desc": "Prezzo complessivo massimo per la transazione. Se l'attuale commissione di base del blocco supera questa cifra, la tua transazione verrà inclusa in un blocco successivo con una commissione di base inferiore.", + "maybe-later": "Ricordamelo più tardi", + "member-ban": "Espelli membro", + "member-kick": "Espelli utente", + "members": { + "one": "1 partecipante", + "other": "{{count}} partecipanti" + }, + "members-active": { + "one": "1 partecipante", + "other": "{{count}} partecipanti" + }, + "members-active-none": "nessun partecipante", + "members-count": "{{count}} membri", + "members-label": "Partecipanti", + "members-limit-reached": "Raggiunto limite di membri", + "members-title": "Partecipanti", + "membership-approval": "Richiedi approvazione", + "membership-approval-description": "La tua comunità è libera , ma i nuovi membri devono prima essere approvati dal creatore della comunità", + "membership-button": "Requisito di iscrizione", + "membership-declined": "La tua richiesta di adesione è stata rifiutata", + "membership-description": "L'adesione al gruppo richiede l'accettazione della tua richiesta da parte dell'amministratore", + "membership-ens": "Richiedi un nome utente ENS", + "membership-ens-description": "La tua comunità richiede un nome utente ENS per poter partecipare", + "membership-free": "Nessun requisito", + "membership-free-description": "La tua community permette a chiunque di unirsi", + "membership-invite": "Richiedi invito da un altro membro", + "membership-invite-description": "Ci si può unire alla tua comunità solo su invito di membri già iscritti", + "membership-none": "Nessuno", + "membership-none-placeholder": "È possibile richiedere ai nuovi membri di soddisfare determinati criteri prima che possano aderire. Questo può essere cambiato in qualsiasi momento", + "membership-request-pending": "Richiesta di iscrizione in sospeso", + "membership-requests": "Richieste di adesione", + "membership-title": "Requisito di iscrizione", + "message": "Messaggio", + "message-not-sent": "Messaggio non inviato", + "message-options-cancel": "Annulla", + "message-reply": "Rispondi", + "messages": "Messaggi", + "messages-from-contacts-only-subtitle": "Solo le persone che hai aggiunto come contatti possono avviare una nuova chat con te o invitarti a un gruppo", + "might-break": "Potrebbe rompere alcune ÐApps", + "migration-successful": "Migrazione riuscita", + "migration-successful-text": "Conto migrato con successo a Keycard", + "migrations-failed-content": "{{message}} \n versione dello schema: iniziale {{initial-version}} , attuale {{current-version}} , ultima {{last-version}} \n\nC'è stato un errore sul database. I tuoi fondi e le chiavi sono al sicuro. Altri dati, come le tue chat la tua lista di contatti, non possono essere recuperati. Premendo \"{{erase-multiaccounts-data-button-text}} \" tutti i tuoi dati saranno cancellati ma potrai nuovamente accedere ai tuoi fondi e ricominciare a chattare.", + "miners-higher-fee": "È più facile che i miners includano la tua transazione prima se paghi una commissione più alta.", + "minimum-received": "Minimo ricevuto", + "mobile-network-ask-me": "Chiedimi quando sono su rete mobile", + "mobile-network-continue-syncing": "Continua la sincronizzazione", + "mobile-network-continue-syncing-details": "Puoi modificarlo in seguito nelle impostazioni", + "mobile-network-go-to-settings": "Vai alle impostazioni", + "mobile-network-settings": "Dati mobili", + "mobile-network-sheet-configure": "Puoi configurare la sincronizzazione in dettagliatamente in", + "mobile-network-sheet-offline": "Niente Wi-Fi, sincronizzazione messaggi disabilitata.", + "mobile-network-sheet-offline-details": "La sincronizzazione tramite rete mobile è disattivata", + "mobile-network-sheet-remember-choice": "Ricorda la mia scelta", + "mobile-network-sheet-settings": "Impostazioni", + "mobile-network-start-syncing": "Inizia la sincronizzazione", + "mobile-network-stop-syncing": "Interrompere la sincronizzazione", + "mobile-network-stop-syncing-details": "Fino a quando non si è connessi al Wi-Fi?", + "mobile-network-use-mobile": "Usa i dati mobili", + "mobile-network-use-mobile-data": "Status utilizza molti dati durante la sincronizzazione delle chat e del portafoglio.", + "mobile-network-use-wifi": "Solo Wi-Fi", + "mobile-syncing-sheet-details": "Status utilizza molti dati durante la sincronizzazione delle chat e del portafoglio.", + "mobile-syncing-sheet-title": "Sincronizza utilizzando i dati della rete mobile?", + "more": "di più", + "move-and-reset": "Sposta e resetta", + "move-keystore-file": "Sposta il file keystore", + "move-keystore-file-to-keycard": "Spostare il file del keystore nella keycard?", + "multiaccount-exists-content": "Le chiavi per questo account esistono già e non possono essere aggiunte di nuovo. Se hai perso la password, il codice di accesso o la Keycard, disinstalla l'app, reinstalla e accedi alle tue chiavi inserendo la tua frase di recupero", + "multiaccount-exists-title": "Le chiavi di questo account esistono già", + "multiaccounts-recover-enter-phrase-text": "Inserisci 12, 15, 18, 21 o 24 parole.\nSeparale con un singolo spazio.", + "multiaccounts-recover-enter-phrase-title": "Inserisci la tua frase di recupero", + "mute": "Silenzia notifiche", + "my-accounts": "I miei account", + "my-accounts-empty": "I tuoi account disponibili verranno visualizzati qui", + "my-profile": "Il mio profilo", + "my-status": "Il mio stato", + "name": "Nome", + "name-of-token": "Il nome del tuo token", + "name-optional": "Nome (facoltativo)", + "name-your-channel": "Dai un nome al tuo canale", + "name-your-channel-placeholder": "Nome canale", + "name-your-community": "Dai un nome alla tua comunità", + "name-your-community-placeholder": "Un nome accattivante", + "need-help": "Hai bisogno di aiuto?", + "network": "Rete", + "network-chain": "Reti", + "network-details": "Dettagli della rete", + "network-fee": "Commissione di rete", + "network-id": "ID di rete", + "network-info": "Informazioni sulla rete", + "network-invalid-network-id": "L'ID di rete specificato non corrisponde all'ID della rete accessibile all'url RCP", + "network-invalid-status-code": "Codice Status non valido: {{code}}", + "network-invalid-url": "L'URL di rete non è valido", + "network-settings": "Impostazioni di rete", + "never": "Mai", + "new": "Nuovo", + "new-category": "Nuova Categoria", + "new-chat": "Nuova chat", + "new-community-title": "Nuova comunità", + "new-contact": "Nuovo contatto", + "new-contract": "Nuovo Contract", + "new-favourite": "Nuovo preferito", + "new-group": "Nuovo gruppo", + "new-group-chat": "Nuova chat di gruppo", + "new-network": "Nuova rete", + "new-password": "Nuova password", + "new-pin-description": "Inserisci un nuovo codice di accesso a 6 cifre", + "new-public-group-chat": "Entra in una chat pubblica", + "new-puk-description": "Inserisci il nuovo PUK di 12 caratteri", + "new-status": "Nuovo stato", + "new-tab": "Nuova scheda", + "next": "Prossimo", + "nickname": "Soprannome", + "nickname-description": "I soprannomi ti aiutano a identificare gli altri in Status.\nSolo tu puoi vedere i soprannomi che hai usato", + "no": "No", + "no-collectibles": "Nessun collezionabile disponibile", + "no-contacts": "Nessun contatto ancora", + "no-keycard-applet-on-card": "Nessuna applet Keycard sulla carta", + "no-messages": "Nessun messaggio", + "no-pairing-slots-available": "Questa carta è già accoppiata a 5 dispositivi e non può essere accoppiata a questo. Si prega di utilizzare uno dei dispositivi accoppiati, effettuare il login con questa carta e liberare gli slot di accoppiamento sulla carta.", + "no-pinned-messages": "Nessun messaggio appuntato", + "no-result": "Nessun risultato", + "no-thanks": "No grazie", + "no-tokens-found": "Nessun token trovato", + "node-address": "Indirizzo Nodo", + "node-details": "Dettagli del nodo", + "node-info": "Informazioni sul nodo", + "node-version": "Versione del nodo", + "nodes-disabled": "Nodi di Status disabilitati", + "non-archival-node": "L'endpoint RPC non supporta le richieste di archiviazione. La cronologia dei trasferimenti locali potrebbe essere incompleta.", + "non-contacts": "Non nei contatti", + "nonce": "Nonce", + "none": "Nessuno", + "normal": "Normale", + "not-applicable": "Non applicabile per transazioni non firmate", + "not-connected-nodes": "Non connesso ad un nodo di Status", + "not-connected-to-peers": "Non connesso ad alcun contatto", + "not-enough-snt": "Non hai abbastanza Token SNT", + "not-found": "Non trovato", + "not-keycard-text": "La carta che hai usato non è una Keycard. È necessario acquistare una Keycard per usarla", + "not-keycard-title": "Non una Keycard", + "not-registered": "non registrato", + "notification-settings": "Impostazioni sulle notifiche", + "notifications": "Notifiche", + "notifications-non-contacts": "Notifiche da parte di persone sconosciute", + "notifications-preferences": "Preferenze sulle notifiche", + "notifications-servers": "Server di notifica", + "notifications-switch": "Mostra notifiche", + "notifications-transactions": "Transazioni del portafoglio", + "notify": "Notifica", + "now": "Ora", + "off": "Spento", + "off-status-tree": "Fuori Status", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Abilita i nodi della cronologia per recuperare i messaggi inviati mentre l'app era chiusa. Quando abilitato, un nodo della cronologia ottiene il tuo indirizzo IP. Quando disabilitato non riceverai messaggi quando l'app è chiusa e non li vedrai quando apri l'app in seguito.", + "offline-messaging-use-history-nodes": "Usa i nodi di Status", + "ok": "OK", + "ok-continue": "Ok, continua", + "ok-got-it": "Ok, capito", + "ok-save-pass": "OK, salva la password", + "okay": "Va bene", + "on": "Su", + "on-status-tree": "In Status", + "once-enabled-share-metadata": "Una volta abilitati, i link pubblicati nella chat possono condividere i tuoi metadati con il sito", + "one-day": "Un giorno", + "one-month": "Un mese", + "one-week": "Una settimana", + "open": "Apri", + "open-chat": "Apri chat", + "open-dapp": "Apri ÐApp", + "open-dapp-store": "Scopri ÐApps", + "open-home": "Apri...", + "open-in-new-tab": "Apri in una nuova scheda", + "open-membership": "Iscrizione aperta", + "open-nfc-settings": "Apri le impostazioni NFC", + "opening-buy-crypto": "Apertura in corso di {{site}} ...", + "optimal": "Ottimale", + "optional": "opzionale", + "or": "O", + "outgoing": "In uscita", + "outgoing-transaction": "Transazione in uscita", + "page-camera-request-blocked": "Le richieste di accesso alla telecamera sono bloccate. Per abilitare l'accesso alla telecamera vai su Impostazioni", + "page-would-like-to-use-camera": "desidera utilizzare la vostra fotocamera", + "pair": "Associare dispositivi", + "pair-card": "Associa a questo dispositivo", + "pair-code": "Codice di accoppiamento", + "pair-code-explanation": "Associa la carta a un dispositivo diverso (fino a 5) per sbloccare chiavi e firmare transazioni con la stessa Keycard.", + "pair-code-placeholder": "Codice di associazione...", + "pair-this-card": "Abbina questa carta", + "pair-this-device": "Associa questo dispositivo", + "pair-this-device-description": "Associa i tuoi dispositivi per sincronizzare contatti e chat tra di loro", + "paired-devices": "Dispositivi associati", + "pairing": "Accoppiamento", + "pairing-card": "Accoppiamento carta", + "pairing-changed": "Il codice di accoppiamento è stato modificato", + "pairing-code-placeholder": "Codice di accoppiamento...", + "pairing-code_error1": "I codici di accoppiamento non corrispondono", + "pairing-go-to-installation": "Vai alle impostazioni di accoppiamento", + "pairing-maximum-number-reached-content": "Disabilita uno dei tuoi dispositivi prima di abilitarne uno nuovo.", + "pairing-maximum-number-reached-title": "Numero massimo di dispositivi raggiunto", + "pairing-new-installation-detected-content": "È stato rilevato un nuovo dispositivo. \n Per utilizzare correttamente i tuoi dispositivi, è importante associarli e abilitarli prima di utilizzarli. \n Vai in Profilo->Impostazioni di sincronizzazione->Dispositivi per associare i tuoi dispositivi.", + "pairing-new-installation-detected-title": "Nuovo dispositivo rilevato", + "pairing-no-info": "Nessuna informazione", + "pairing-please-set-a-name": "Si prega di impostare un nome per il dispositivo.", + "passphrase": "Frase d'accesso", + "password": "Password", + "password-description": "Almeno 6 caratteri. La tua password protegge le tue chiavi. Ne hai bisogno per sbloccare Status e per effettuare transazioni.", + "password-mismatch": "Le password non corrispondono.", + "password-placeholder": "Password...", + "password-placeholder2": "Conferma la tua password", + "password-reset-in-progress": "Modifica password in corso...", + "password-reset-success": "Reimposta la password", + "password-reset-success-message": "Dovrai accedere di nuovo", + "password_error1": "Le password non corrispondono.", + "paste": "Incolla", + "paste-json": "Incolla JSON", + "pay-to-chat": "Paga per chattare", + "peer-content": "Un dispositivo collegato alla rete di chat di Status. Ogni utente può rappresentare uno o più peer, a seconda del numero di dispositivi.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Peers", + "pending": "In attesa", + "pending-confirmation": "In attesa di conferma...", + "pending-invitations": "Richieste di adesione in sospeso", + "per-gas-price-limit": "Limite di prezzo per gas", + "per-gas-tip-limit": "Limite mancia per gas", + "perform-backup": "Procedi al backup", + "permissions": "Autorizzazioni", + "phone-e164": "Internazionale 1", + "photos": "Fotografie", + "photos-access-error": "Per concedere le autorizzazioni necessarie per le foto, vai alle impostazioni del tuo sistema e assicurati che Status > Foto sia selezionato.", + "pin": "Appunta", + "pin-changed": "Il codice di accesso a 6 cifre è stato modificato", + "pin-code": "Codice di accesso a 6 cifre", + "pin-limit-reached": "Limite pin raggiunto. Deseleziona un messaggio precedente.", + "pin-mismatch": "Codice di accesso errato", + "pin-one-attempt": " un tentativo ", + "pin-one-attempt-blocked-after": "prima che la tua Keycard venga bloccata", + "pin-one-attempt-blocked-before": "Fai attenzione, hai solo", + "pin-one-attempt-frozen-after": "prima che la tua Keycard venga congelata", + "pin-one-attempt-frozen-before": "Fai attenzione, hai solo", + "pin-retries-left": "{{number}} tentativi rimasti", + "pinned-by": "Appuntato da", + "pinned-messages": "Messaggi appuntati", + "pinned-messages-count": { + "one": "1 messaggio appuntato", + "other": " {{count}} messaggi appuntati" + }, + "pinned-messages-empty": "I messaggi appuntati verranno visualizzati qui. Per appuntare un messaggio, tieni premuto e tocca `Appunta`", + "powered-by-paraswap": "Fornito da Paraswap", + "preference": "Impostazione", + "preview-privacy": "Anteprima modalità privacy", + "previewing-may-share-metadata": "La visualizzazione in anteprima dei collegamenti da questi siti Web potrebbe condividere i metadati con i loro proprietari", + "price-impact": "Impatto sul prezzo", + "price-impact-desc": "Impatto sul prezzo stimato per questa transazione. Se l'attuale commissione di base del blocco supera questa cifra, la tua transazione verrà inclusa in un blocco successivo con una commissione di base inferiore.", + "principles": "Principi", + "priority": "Priorità", + "privacy": "Privacy", + "privacy-and-security": "Privacy e sicurezza", + "privacy-photos": "Privacy dell'immagine del profilo", + "privacy-policy": "Informativa sulla privacy", + "privacy-show-to-warning": "Le persone che hanno già visto la tua immagine del profilo continueranno a farlo", + "private-key": "Chiave privata", + "private-notifications": "Notifiche private", + "private-notifications-descr": "Status ti notificherà nuovi messaggi. Puoi modificare le tue preferenze anche in un secondo tempo in Impostazioni.", + "processing": "Solo un momento", + "product-information": "Informazioni sul prodotto", + "profile": "Profilo", + "profile-deleted-content": "Il tuo profilo è stato cancellato con successo", + "profile-deleted-keycard": "Ora puoi ripristinare un'altra coppia di chiavi sulla tua Keycard", + "profile-deleted-title": "Profilo cancellato", + "profile-details": "Dettagli del profilo", + "profile-not-found": "Profilo non trovato", + "profile-pic-pick": "Seleziona dalla galleria", + "profile-pic-remove": "Rimuovi foto", + "profile-pic-take": "Scatta foto", + "profile-picture-updated": "Foto profilo aggiornata", + "public-channel": "Canale pubblico", + "public-chat": "Chat pubblica", + "public-chat-description": "Partecipa a chat pubbliche sui tuoi interessi! Chiunque può iniziare una nuova.", + "public-chats": "Chat pubbliche", + "public-group-status": "Pubblico", + "public-group-topic": "Argomento", + "public-key": "Chiave pubblica", + "puk-and-pairing-codes-displayed": "PUK e codici di associazione visualizzati", + "puk-changed": "Il nuovo PUK di 12 caratteri è cambiato", + "puk-code": "Codice PUK", + "puk-code-explanation": "Se dimentichi il tuo codice di accesso a 6 cifre o lo inserisci in modo errato per 3 volte, avrai bisogno di questo codice per sbloccare la tua carta.", + "puk-mismatch": "Codice PUK errato", + "push-failed-transaction": "La tua transazione non è andata a buon fine", + "push-failed-transaction-body": "{{value}} {{currency}} a {{to}}", + "push-inbound-transaction": "Hai ricevuto {{value}} {{currency}}", + "push-inbound-transaction-body": "Da {{from}} a {{to}}", + "push-notifications-server-enabled": "Server abilitato", + "push-notifications-servers": "Invia notifiche push", + "push-outbound-transaction": "Hai inviato {{value}} {{currency}}", + "push-outbound-transaction-body": "Da {{from}} a {{to}}", + "quiet-days": "{{quiet-days}} giorni", + "quiet-hours": "{{quiet-hours}} ore", + "re-encrypt-key": "Cripta nuovamente la tue chiavi", + "rearrange-categories": "Riorganizza le categorie", + "receive": "Ricevi", + "receive-transaction": "Ricevi transazione", + "recent": "Recente", + "recent-empty": "Gli indirizzi utilizzati di recente verranno visualizzati qui", + "recent-recipients": "Contatti", + "recently-used-stickers": "Gli adesivi utilizzati di recente appariranno qui", + "recipient": "Destinatario", + "recipient-code": "Inserisci l'indirizzo del destinatario", + "recipient-code-placeholder": "0x ... o username.domain.eth", + "recover": "Recupera", + "recover-key": "Accedi a chiavi esistenti", + "recover-keycard-multiaccount-not-supported": "Le chiavi di questo account esistono già e non possono essere riaggiunte. Se hai perso la password, il codice di accesso o la Keycard, disinstalla l'app, reinstallala e accedi alle tue chiavi inserendo la frase di recupero", + "recover-with-keycard": "Ripristina con Keycard", + "recover-with-seed-phrase": "Recupera con la frase seme", + "recovering-key": "Recupero delle chiavi in corso...", + "recovery-confirm-phrase": "Conferma frase di recupero", + "recovery-phrase": "Frase di recupero", + "recovery-success-text": "Dovrai creare un nuovo codice o password per criptare nuovamente le tue chiavi", + "recovery-typo-dialog-description": "Nota bene: le parole della tua frase di recupero devono essere inserite identiche e nello stesso ordine in cui le hai ricevute", + "recovery-typo-dialog-title": "La frase di recupero è corretta?", + "redeem-amount": "{{quantity}} bonus disponibili", + "redeem-now": "Riscatta ora", + "redeem-success": "Il riscatto del tuo bonus è andato a buon fine!", + "reduced-tip": "la mancia prioritaria sarà ridotta", + "refresh": "Aggiorna", + "registered": "registrato", + "reject-and-delete": "Rifiuta ed elimina", + "remember-me": "Ricordati di me", + "remind-me-later": "Ricordamelo più tardi", + "remove": "Rimuovi", + "remove-favourite": "Rimuovi preferito", + "remove-from-chat": "Rimuovi dalla chat", + "remove-from-contacts": "Rimuovi dai contatti", + "remove-from-contacts-text": "Rimuovendo un utente dalla tua lista dei contatti non nascondi il tuo indirizzo portafoglio da loro", + "remove-group": "Rimuovi il gruppo", + "remove-network": "Rimuovi rete", + "remove-token": "Rimuovi token", + "removed": "rimosso", + "repeat-pin": "Ripeti il codice a 6 cifre", + "repeat-puk": "Ripeti il nuovo PUK di 12 caratteri", + "replying-to": "Rispondendo a {{author}}", + "report-bug-email-template": "1. Descrizione del problema\n {{description}}\n \n\n 2. Passi per riprodurre\n {{steps}}\n \n\n 3. Allega cortesemente gli screenshot che possono dimostrare il problema\n", + "request-access": "Richiedi accesso", + "request-feature": "Richiedi una funzionalità", + "request-membership": "Richiedi adesione", + "request-pending": "Richiesta in attesa di approvazione ...", + "request-transaction": "Richiedi transazione", + "required-field": "Campo obbligatorio", + "resend-message": "Invia di nuovo", + "reset-card": "Reimposta carta", + "reset-card-description": "Questa operazione reimposterà lo stato iniziale della carta. Cancellerà tutti i dati della carta, incluse le chiavi private. L'operazione non è reversibile.", + "reset-database": "Reimposta database", + "reset-database-warning": "Elimina chat, contatti e impostazioni. Obbligatorio quando hai perso la password", + "reset-database-warning-keycard": "Elimina chat, contatti e impostazioni.", + "reset-password": "Reimposta password", + "restore-defaults": "Ripristina i valori predefiniti", + "retry": "Riprova", + "revoke-access": "Revoca l'accesso", + "rpc-url": "URL RPC", + "rpc-usage-copy": "Copia", + "rpc-usage-filter": "Metodi di filtro", + "rpc-usage-get-stats": "Aggiorna", + "rpc-usage-info": "Statistiche di utilizzo RPC", + "rpc-usage-reset": "Resetta", + "safe-estimate": "Stima sicura", + "save": "Salva", + "save-password": "Salva la password", + "save-password-unavailable": "Impostare il codice di accesso del dispositivo per salvare la password", + "save-password-unavailable-android": "Salva password non è disponibile: il dispositivo potrebbe essere rootato o privo delle funzionalità di sicurezza necessarie.", + "scan-qr": "Scansiona il codice QR", + "scan-qr-code": "Scansione di un codice QR con indirizzo del portafoglio", + "scan-tokens": "Scansiona Tokens", + "search": "Ricerca", + "search-no-chat-found": "Nessun risultato trovato. Volevi dire", + "secret-keys-confirmation-text": "Ti serviranno per continuare a utilizzare la tua Keycard nel caso in cui perdessi il telefono.", + "secret-keys-confirmation-title": "Hai scritto i codici?", + "security": "Sicurezza", + "see-details": "Vedi dettagli", + "see-it-again": "VEDI DI NUOVO", + "see-suggestions": "Vedi suggerimenti", + "seed-key-uid-mismatch": "Il seme non combacia", + "seed-key-uid-mismatch-desc-1": "La frase seme inserita non corrisponde a {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Per gestire le chiavi per questo account, verifica la tua frase seme e riprova.", + "seed-phrase-content": "Un insieme di parole facili da leggere, selezionate casualmente dall'elenco standard BIP39 e utilizzate per recuperare o accedere al tuo account Ethereum su altri portafogli e dispositivi. Definita anche \"frase mnemonica\", \"frase di recupero\" o \"backup del portafoglio\" in tutto l'ecosistema crittografico. La maggior parte delle app di crittografia utilizza questo stesso standard per generare account.", + "seed-phrase-placeholder": "Frase di recupero...", + "seed-phrase-title": "Frase di recupero", + "select": "Seleziona", + "select-account": "Seleziona un account", + "select-account-dapp": "Seleziona l'account che desideri utilizzare con le Dapps", + "select-account-first": "Seleziona prima un account", + "select-chat": "Seleziona chat per avviare la messaggistica", + "select-new-location-for-keys": "Seleziona una nuova posizione per salvare le tue chiavi private", + "select-token-to-receive": "Seleziona il token da ricevere", + "select-token-to-swap": "Seleziona il token da scambiare", + "selected": "Selezionato", + "send-logs": "Segnala un bug", + "send-logs-to": "Segnala un bug a {{email}}", + "send-message": "Invia messaggio", + "send-push-notifications-description": "Quando è disabilitato, la persona che riceve i tuoi messaggi non verrà informata del loro arrivo", + "send-request": "Invia richiesta", + "send-request-amount": "Importo", + "send-request-amount-max-decimals": "Il numero massimo di decimali è {{asset-decimals}}", + "send-request-unknown-token": "Token sconosciuto - {{asset}}", + "send-sending-to": "a {{recipient-name}}", + "send-transaction": "Invia transazione", + "sending": "Inviando", + "sent-at": "Inviato alle", + "server": "Server", + "set-a-topic": "Crea un argomento", + "set-currency": "Imposta la valuta predefinita", + "set-custom-fee": "Imposta una commissione personalizzata", + "set-dapp-access-permissions": "Impostare le autorizzazioni di accesso alle DApp", + "set-max": "Imposta valore massimo", + "settings": "Impostazioni", + "share": "Condividi", + "share-address": "Condividi l'indirizzo", + "share-chat": "Condividi chat", + "share-contact-code": "Condividi la mia chiave di chat", + "share-dapp-text": "Dai un'occhiata a questa DApp che sto usando su Status: {{link}}", + "share-link": "Condividi link", + "share-logs": "Condividi i log", + "share-my-profile": "Condividi il mio profilo", + "share-profile": "Condividi profilo", + "share-profile-link": "Condividi il link del profilo", + "share-public-chat-text": "Dai un'occhiata a questa chat pubblica sull'app Status: {{link}}", + "shared": "Condiviso", + "sharing-copied-to-clipboard": "Copiato", + "sharing-copy-to-clipboard": "Copia", + "sharing-data-desc-1": "I dati vengono convalidati rispetto alle regole pubbliche per garantire che non vengano inviati dati sensibili. Non fidarti, verifica.", + "sharing-data-desc-2": "I dati di utilizzo vengono inviati crittografati end-to-end sulla rete peer-to-peer di Status", + "sharing-data-desc-3": "Invece della normale chiave di chat, viene usata una chiave di uso singolo", + "sharing-data-desc-4": "I dati di utilizzo non possono essere associati al tuo indirizzo IP", + "sharing-data-desc-5": "I dati cumulativi di tutti gli utenti sono disponibili pubblicamente", + "sharing-data-desc-6": "I dati vengono rimossi dal telefono dopo l'invio", + "sharing-share": "Condividi", + "show-less": "Mostra meno", + "show-more": "Mostra di più", + "show-notifications": "Mostra notifiche", + "show-profile-pictures": "Mostra le foto del profilo da", + "show-profile-pictures-to": "Mostra la tua immagine del profilo a", + "show-qr": "Mostra codice QR", + "show-transaction-data": "Mostra i dettagli della transazione", + "sign-and-send": "Firma e invia", + "sign-anyway": "Accedi comunque", + "sign-in": "Accedi", + "sign-message": "Firma il messaggio", + "sign-out": "Esci", + "sign-request-failed": "Impossibile firmare il messaggio", + "sign-with": "Iscriviti con", + "sign-with-password": "Firma con password", + "sign-you-in": "Accesso in corso ...", + "signing": "Firmando", + "signing-a-message": "Firma un messaggio", + "signing-phrase": "Frase di firma", + "skip": "Salta", + "slippage": "Slittamento", + "slow": "Lenta", + "something-went-wrong": "Qualcosa è andato storto", + "soon": "Presto", + "specify-address": "Specifica l'indirizzo", + "specify-name": "Specifica un nome", + "specify-network-id": "Specificare l'id di rete", + "specify-rpc-url": "Specificare un URL RPC", + "specify-server-public-key": "Inserisci la chiave pubblica del server", + "specify-symbol": "Specifica un simbolo", + "start-chat": "inizia chat", + "start-conversation": "Avvia conversazione", + "start-group-chat": "Inizia chat di gruppo", + "start-new-chat": "Inizia una nuova chat", + "starter-pack-coming": "Starter Pack in arrivo", + "starter-pack-coming-description": "Può richiedere da pochi minuti a ore", + "starter-pack-received": "Starter Pack ricevuto", + "starter-pack-received-description": "Ecco alcuni token per iniziare! Usali per ottenere adesivi, un nome ENS e per provare dapps", + "status": "Status", + "status-always-online": "Sempre online", + "status-automatic": "Automatico", + "status-automatic-subtitle": "Imposta automaticamente lo stato", + "status-confirmed": "Confermata", + "status-dnd": "Non disturbare", + "status-dnd-subtitle": "Silenzia tutte le notifiche", + "status-hardwallet": "Status hardwallet", + "status-inactive": "Non attivo", + "status-inactive-subtitle": "Nascondi il tuo stato online", + "status-is-open-source": "Status è open-source", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status tende a utilizzare molti dati durante la sincronizzazione delle chat. Puoi scegliere di non sincronizzare quando sei su rete mobile", + "status-not-sent-click": "Non confermata. Clicca per le opzioni", + "status-not-sent-tap": "Non confermata. Tocca per le opzioni", + "status-pending": "In attesa", + "status-sent": "Inviato", + "status-tx-not-found": "TX non trovata", + "status-updates-descr": "Gli aggiornamenti di Status verranno visualizzati qui. Aggiungi il profilo come contatto per ricevere aggiornamenti sulla tua cronologia.", + "statuses-descr": "Condividi quello che stai pensando e rimani aggiornato con i tuoi contatti", + "statuses-my-status-descr": "Condividi quello che stai pensando. Chiunque visiti il tuo profilo potrà vedere il tuo stato. Le persone che ti aggiungono come loro contatto riceveranno i tuoi aggiornamenti sulla loro timeline", + "step-i-of-n": "Passo {{step}} di {{number}}.", + "sticker": "Adesivo", + "sticker-market": "Mercato degli sticker", + "storage": "Archivio", + "submit": "Invia", + "submit-bug": "Segnala un bug", + "success": "Successo", + "suggested-min-tip": "Mancia minima suggerita", + "suggested-price-limit": "Limite di prezzo suggerito", + "swap": "Converti", + "swap-details": "Dettagli dello scambio", + "switch-to-simple-interface": "Passa all'interfaccia semplificata", + "symbol": "Simbolo", + "sync-all-devices": "Sincronizza tutti i dispositivi", + "sync-in-progress": "Sincronizzazione...", + "sync-settings": "Impostazioni di sincronizzazione", + "sync-synced": "In sincronizzazione", + "syncing-devices": "Sincronizzazione...", + "system": "Sistema", + "tabs": "Schede", + "tag-was-lost": "Il tag è andato perso", + "tap-card-again": "Posiziona nuovamente la carta sul retro del telefono", + "terms-of-service": "Termini di utilizzo", + "test-networks": "Reti di test", + "text-input-disabled": "Per favore aspetta un momento...", + "thank-you": "Grazie", + "this-device": "Questo dispositivo", + "this-device-desc": "Le tue chiavi saranno criptate e conservate in modo sicuro nel tuo dispositivo", + "this-is-you-signing": "Questa è la tua frase di firma", + "this-will-take-few-seconds": "Richiederà alcuni secondi", + "three-days": "Tre giorni", + "three-words-description": "Dovresti vedere queste 3 parole prima di firmare ogni transazione", + "three-words-description-2": "Se vedi una combinazione diversa, annulla la transazione ed esci", + "timeline": "Timeline", + "tip-cap": "Limite mancia", + "to": "A", + "to-block": "Blocco", + "to-enable-biometric": "Per abilitare {{bio-type-label}}, devi salvare la password nella schermata di sblocco", + "to-encrypt-enter-password": "Per crittografare l'account, inserisci la tua password", + "to-see-this-message": "Per vedere questo messaggio,", + "token": "Token", + "token-auto-validate-decimals-error": "Decimali errati per il token {{symbol}} all'indirizzo {{address}} - impostato su {{expected}} ma rilevato come {{actual}}", + "token-auto-validate-name-error": "Nome errato per il token {{symbol}} all'indirizzo {{address}} - impostato su {{expected}} ma rilevato come {{actual}}", + "token-auto-validate-symbol-error": "Simbolo errato per il token {{symbol}} all'indirizzo {{address}} - impostato su {{expected}} ma rilevato come {{actual}}", + "token-details": "Dettagli token", + "topic-name-error": "Usa solo lettere minuscole (dalla a alla z), numeri e trattini (-). Non utilizzare le chiavi chat", + "total-gas": "Fee di transazione totale", + "transaction": "Transazione", + "transaction-data": "Dettagli della transazione", + "transaction-declined": "Transazione rifiutata", + "transaction-description": "Consideralo completo dopo 12 conferme sulla rete.", + "transaction-details": "Dettagli della transazione", + "transaction-failed": "Transazione fallita", + "transaction-fee": "Costo transazione", + "transaction-history": "Cronologia delle transazioni", + "transaction-request": "Richiesta Transazione", + "transaction-sent": "Transazione inviata", + "transaction-signed": "La transazione è stata firmata correttamente", + "transactions": "Transazioni", + "transactions-filter-select-all": "Seleziona tutto", + "transactions-filter-title": "Filtra cronologia", + "transactions-history": "Cronologia delle transazioni", + "transactions-history-empty": "Nessuna transazione nella tua cronologia", + "transactions-history-loading": "Caricamento dello storico delle transazioni. Potrebbe volerci un po' di tempo.", + "transactions-load-more": "Caricane ancora", + "transactions-management-enabled": "Gestione delle transazioni (alfa)", + "transactions-sign": "Firma", + "transfer-ma-unknown-error-desc-1": "Sembra che il tuo multiaccount non sia stato cancellato. Il database potrebbe essere stato resettato", + "transfer-ma-unknown-error-desc-2": "Ti invitiamo a controllare l'elenco dei tuoi account e riprovare. Se l'account non è elencato, vai su Accedi alle chiavi esistenti per ripristinarlo con frase seme", + "transfers-fetching-failure": "Impossibile aggiornare la cronologia dei trasferimenti. Controlla la connessione e scorri verso il basso per riprovare", + "tribute-required-by-multiaccount": "{{multiaccount-name}} richiede degli SNT per avviare una chat.", + "tribute-state-paid": "Omaggio pagato", + "tribute-state-pending": "Omaggio in sospeso", + "tribute-state-required": "Richiede {{snt-amount}} SNT omaggio", + "tribute-to-talk": "Tribute to Talk", + "tribute-to-talk-add-friends": "Aggiungi amici come contatto per consentire le chat senza pagamento omaggio.", + "tribute-to-talk-are-you-friends": "Siete amici?", + "tribute-to-talk-ask-to-be-added": "Chiedi di essere aggiunto come contatto", + "tribute-to-talk-contact-received-your-tribute": " ha ricevuto il tuo tributo. Ora potete chattare tra di voi in tutta sicurezza.", + "tribute-to-talk-desc": "Monetizza la tua attenzione richiedendo SNT alle nuove persone che avviano una chat", + "tribute-to-talk-disabled": "Tribute to Talk disabilitato", + "tribute-to-talk-disabled-note": "Da ora in poi, nuove persone potranno iniziare una chat con te senza inviare SNT.", + "tribute-to-talk-enabled": "Tribute to Talk è abilitato.", + "tribute-to-talk-finish-desc": "Da ora in poi, riceverai solo chat da contatti e persone che hanno pagato. ", + "tribute-to-talk-learn-more-1": "Il tuo tempo e la tua attenzione sono i tuoi beni più preziosi. Tribute to Talk ti consente di impostare una quantità di SNT richiesta per le nuove persone per iniziare una chat con te.", + "tribute-to-talk-learn-more-2": "Chiunque non sia nella tua lista di contatti sarà invitato a pagare, e potrai rispondere una volta che l'avranno fatto.", + "tribute-to-talk-learn-more-3": "Puoi sempre restituire i soldi, ma per assicurarti che gli amici possano contattarti liberamente, aggiungili prima come contatto.", + "tribute-to-talk-paywall-learn-more-1": "Il nostro tempo e la nostra attenzione sono i nostri beni più preziosi. Tribute to Talk ti permette di contattare nuove persone in cambio di un pagamento SNT.", + "tribute-to-talk-paywall-learn-more-2": "Per iniziare una chat con qualcuno che ha un tributo impostato, paga semplicemente il SNT richiesto e verrai aggiunto come contatto.", + "tribute-to-talk-paywall-learn-more-3": "Se li conosci, puoi condividere il tuo profilo al di fuori di Status per essere aggiunto gratuitamente.", + "tribute-to-talk-pending": "Omaggio in attesa di conferma", + "tribute-to-talk-pending-note": "La transazione Tribute è in attesa di conferma sulla rete. Puoi verificarne lo stato nella cronologia delle transazioni", + "tribute-to-talk-removing-note": "La rimozione di Tribute to Talk consentirà a nuove persone di avviare una chat senza inviare SNT. Richiede una transazione da effettuare.", + "tribute-to-talk-set-snt-amount": "Imposta la quantità di SNT necessaria alle nuove persone per iniziare una chat.", + "tribute-to-talk-signing": "In attesa di firmare la transazione", + "tribute-to-talk-transaction-failed-note": "Transazione non riuscita e le impostazioni di Tribute to Talk non sono state modificate", + "tribute-to-talk-tribute-received1": "Tributo ricevuto. Tu e", + "tribute-to-talk-tribute-received2": " sono ora contatti e possono chattare in sicurezza tra loro.", + "tribute-to-talk-you-require-snt": "È necessario SNT per le nuove persone per avviare una chat.", + "try-again": "Riprova", + "try-keeping-the-card-still": "Prova a tenere ferma la carta", + "turn-nfc-description": "NFC è disabilitato sul tuo dispositivo. Puoi abilitarlo nelle impostazioni", + "turn-nfc-on": "Attiva NFC per continuare", + "two-minutes": "due minuti", + "tx-fail-description1": "Questa transazione rischia di fallire. Accedi a tuo rischio usando una commissione personalizzata di rete.", + "tx-fail-description2": "Questa transazione rischia di fallire. Imposta una commissione di rete per accedere a tuo rischio.", + "type": "Tipo", + "type-a-message": "Scrivi un messaggio...", + "ulc-enabled": "ULC abilitato", + "unable-to-fetch": "Impossibile recuperare la cronologia chat", + "unable-to-read-this-code": "Impossibile leggere questo codice", + "unable-to-send-messages": "Impossibile inviare e ricevere messaggi", + "unblock": "Sblocca", + "unblock-contact": "Sblocca questo utente", + "unknown-status-go-error": "Errore di status-go sconosciuto", + "unlimited": "Illimitato", + "unlock": "Sblocca", + "unmute": "Riattiva", + "unpair-card": "Dissocia carta", + "unpair-card-confirmation": "Questa operazione disaccoppierà la carta dal dispositivo corrente. Richiede l'autorizzazione tramite il codice di accesso a 6 cifre. Vuoi procedere?", + "unpair-keycard": "Disaccoppia la Keycard da questo telefono", + "unpair-keycard-warning": "Il codice di associazione\/PUK & PIN rimane invariato", + "unpaired-keycard-text": "La Keycard che hai usato non è associata a questo telefono", + "unpaired-keycard-title": "Sembra che la tua carta sia stata disaccoppiata", + "unpin": "Deseleziona", + "update": "Aggiorna", + "update-to-listen-audio": "Aggiorna all'ultima versione per ascoltare un messaggio audio qui!", + "update-to-see-image": "Aggiorna all'ultima versione per vedere una bella immagine qui!", + "update-to-see-sticker": "Aggiorna all'ultima versione per vedere un bel adesivo qui!", + "updates-to-tos": "Aggiornamenti ai Termini di servizio", + "updates-to-tos-desc": "Prima di continuare, consulta i Termini di servizio e conferma di assumerti la piena responsabilità di come utilizzi l'applicazione.", + "url": "URL", + "usd-currency": "USD", + "use-as-profile-picture": "Usa come immagine del profilo", + "use-valid-contact-code": "Inserisci o scansiona una chiave chat o un nome utente valido", + "validation-amount-invalid-number": "L'importo non è valido", + "validation-amount-is-too-precise": "L'importo è troppo preciso. Il numero massimo di decimali è {{decimals}} .", + "verified-community": "✓ Comunità verificata", + "version": "Versione App", + "view": "Visualizza", + "view-cryptokitties": "Visualizza in CryptoKitties", + "view-cryptostrikers": "Visualizza in CryptoStrikers", + "view-data": "Vedi dati", + "view-details": "Dettagli", + "view-etheremon": "Visualizza in Etheremon", + "view-gitcoin": "Visualizza in Gitcoin", + "view-on-opensea": "Visualizza su OpenSea", + "view-profile": "Visualizza profilo", + "view-public-dashboard": "Visualizza la dashboard pubblica", + "view-rules": "Visualizza regole", + "view-signing": "Visualizza la frase di firma", + "view-superrare": "Visualizza in SuperRare", + "waiting-for-wifi": "Niente Wi-Fi, sincronizzazione messaggi disabilitata.", + "waiting-for-wifi-change": "Impostazioni", + "waiting-to-sign": "In attesa di firmare la transazione...", + "waiting-wi-fi": "In attesa di rete Wi-Fi ...", + "waku-bloom-filter-mode": "Modalità Waku bloom filter", + "wakuv2-change-nodes": "Sei sicuro di voler cambiare i nodi Wakuv2 ?", + "wakuv2-node-format": "\/ip4\/ {node-ip} \/tcp\/ {port} \/p2p\/ {id}", + "wakuv2-settings": "Impostazioni Waku v2", + "wallet": "Portafoglio", + "wallet-address": "Indirizzo del portafoglio", + "wallet-asset": "Asset", + "wallet-assets": "Monete", + "wallet-backup-recovery-title": "Esegui il backup della frase di recupero", + "wallet-choose-recipient": "Scegli il destinatario", + "wallet-collectibles": "Collezionabili", + "wallet-insufficient-funds": "Fondi non sufficienti", + "wallet-insufficient-gas": "ETH non sufficiente per il gas", + "wallet-invalid-address": "Indirizzo non valido: \n {{data}}", + "wallet-invalid-address-checksum": "Errore nell'indirizzo: \n {{data}}", + "wallet-invalid-chain-id": "La rete non corrisponde: \n {{data}} ma la rete attuale è {{chain}}", + "wallet-key-content": "Un indirizzo esadecimale di 64 caratteri basato sullo standard Ethereum e che inizia con 0x. Viene condiviso con gli altri quando si desidera ricevere dei fondi. Chiamato anche \"indirizzo di Ethereum\" o \"indirizzo del portafoglio\".", + "wallet-key-title": "Indirizzo dell'account", + "wallet-manage-accounts": "Gestisci account", + "wallet-manage-assets": "Gestisci cryptoasset", + "wallet-request": "Richiedi", + "wallet-send": "Invia", + "wallet-send-min-units": "Minimo 21000 unità", + "wallet-send-min-wei": "Minimo 1 wei", + "wallet-settings": "Impostazioni portafoglio", + "wallet-total-value": "Valore complessivo", + "wallet-transaction-total-fee": "Commissione totale", + "wants-to-access-profile": "vuole accedere al tuo profilo", + "warning": "Attenzione", + "warning-message": "Spiacenti, limitiamo l'invio di più messaggi in rapida successione per prevenire lo spam. Riprova tra un momento", + "warning-sending-to-contract-descr": "L'indirizzo che hai inserito è quello di un contratto intelligente, l'invio di fondi a questo indirizzo potrebbe comportare la perdita di fondi. Per interagire con una DApp, apri la DApp nella DApp Browser di Status.", + "watch-only": "sola lettura", + "wc-brand-guide": "Guida all'uso di marchi come marchi e loghi", + "wc-disclaimer": "Dichiarazioni di non responsabilità (inclusi fornitori di terze parti), garanzie e rilasci legali", + "wc-dispute": "Disposizioni per la risoluzione delle controversie", + "wc-how-to-use-status-app": "Come usare l'app Status, compresa la privacy e la sicurezza", + "wc-new-tos-based-on-principles-prefix": "Nuovi Termini di servizio progettati in base ai nostri", + "web-view-error": "Impossibile caricare la pagina", + "websites": "Siti web", + "webview-camera-permission-requests": "Richieste di autorizzazione della fotocamera in WebView", + "webview-camera-permission-requests-subtitle": "Quando è abilitata, i siti web e le dapps possono chiedere di utilizzare la tua fotocamera", + "welcome-blank-community-message": "Le tue comunità appariranno qui.", + "welcome-blank-message": "Le tue chat appariranno qui. Per iniziare nuove chat, premi il tasto ⊕", + "welcome-community-blank-message": "I tuoi canali verranno visualizzati qui. Per creare un nuovo canale, fai clic sul pulsante ⊕ e seleziona \"Crea un canale\"", + "welcome-community-blank-message-edit-chats": "I tuoi canali verranno visualizzati qui. Per creare un nuovo canale, torna alla schermata della community, fai clic sul pulsante ⊕ e seleziona \"Crea un canale\"", + "welcome-screen-text": "Configura il tuo portafoglio, invita gli amici a chattare\n ed a sfogliare app popolari!", + "welcome-to-status": "Benvenuto in Status!", + "welcome-to-status-description": "Configura il tuo portafoglio, invita gli amici a chattare\n ed a sfogliare app decentralizzate!", + "what-changed": "Cosa è cambiato", + "what-is-shared": "Cosa è condiviso", + "whats-on-your-mind": "Cos'hai in mente…", + "word-count": "Conteggio parole", + "word-n": "Parola # {{number}}", + "word-n-description": "Per verificare se è stato eseguito correttamente il backup della frase di recupero, immettere la parola # {{number}} sopra.", + "words-n": { + "one": "una parola", + "other": "{{count}} parole" + }, + "write-down-and-store-securely": "Scrivi i codici \n e conservali in modo sicuro", + "wrong-address": "Indirizzo errato", + "wrong-card": "Carta sbagliata", + "wrong-card-text": "Hai usato una carta che non corrisponde alle chiavi che hai selezionato.", + "wrong-contract": "Contract errato", + "wrong-keycard-text": "La Keycard che hai usato non è associata a questo telefono", + "wrong-keycard-title": "Sembra che tu abbia usato \nuna chiave sbagliata", + "wrong-password": "Password errata", + "wrong-word": "Parola sbagliata", + "yes": "Si", + "you": "tu", + "you-already-have-an-asset": "Hai già un cryptoasset {{value}}", + "you-are-all-set": "È tutto pronto!", + "you-are-all-set-description": "Se perdi il telefono, usando la frase di recupero puoi ripristinare le chiavi e riavere accesso ai tuoi fondi e alle tue chat", + "you-can-change-account": "È possibile modificare il nome e il colore dell'account in base alle proprie esigenze", + "you-can-choose-preview-websites": "Puoi scegliere quale dei seguenti siti web può visualizzare in anteprima i link delle descrizioni e delle immagini nelle chat", + "you-can-fetch": "Puoi recuperare la cronologia chat", + "you-dont-have-contacts": "Non hai ancora alcun contatto.", + "you-dont-have-contacts-invite-friends": "Non hai ancora nessun contatto.\n Invita i tuoi amici a iniziare a chattare.", + "you-dont-have-stickers": "Non hai ancora nessun adesivo.", + "you-will-need-this-code": "Avrai bisogno di questo codice per aprire Status e firmare le transazioni", + "you-will-start-from-scratch": "Inizierai da zero con un nuovo set di chiavi", + "your-card-is-frozen": "La tua Keycard è bloccata. Ripristina l'accesso alla carta", + "your-contact-code": "Concedendo l'accesso autorizzi questa DApp a recuperare la tua chiave chat", + "your-data-belongs-to-you": "Se perdi la frase di recupero perderai dati e fondi", + "your-data-belongs-to-you-description": "Status non può aiutarti a recuperare le tue chiavi se perdi la frase di recupero. Soltanto tu la possiedi. Scrivila e tienila in un posto sicuro.", + "your-keys": "Le tue chiavi", + "your-price-limit": "Il tuo limite di prezzo", + "your-recovery-phrase": "La tua frase di recupero", + "your-recovery-phrase-description": "Questa è la tua frase di recupero. La usi per dimostrare che questo è il tuo portafoglio. Puoi vederlo solo una volta! Scrivilo su carta e conservalo in un luogo sicuro. Ne avrai bisogno se perdi o reinstalli il tuo portafoglio.", + "your-tip-limit": "Il tuo limite di mancia", + "youre-on-mobile-network": "Stai usando una rete mobile" +} diff --git a/translations/it_ch.json b/translations/it_ch.json new file mode 100644 index 00000000000..44cb8c8883b --- /dev/null +++ b/translations/it_ch.json @@ -0,0 +1,127 @@ +{ + "confirm": "Conferma", + "amount": "Saldo", + "members-active": { + "one": "1 membro, 1 attivo", + "other": "{{count}} membri, {{count}} attivi", + "zero": "nessun membro" + }, + "chat-name": "Nome chat", + "phew-here-is-your-passphrase": "*Wow* È stato difficile, ecco qui la tua passphrase, *prendi nota e conservala in un luogo sicuro!* Ti servirà per ripristinare il tuo conto.", + "public-group-topic": "Argomento", + "chat-settings": "Impostazioni conversazioni", + "offline": "Offline", + "invited": "È stato invitato", + "address": "Indirizzo", + "new-public-group-chat": "Entra in chat pubblica", + "datetime-hour": { + "one": "ora", + "other": "ore" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "Rimuovi", + "add-members": "Aggiungi membri", + "done": "OK", + "close-chat": "Elimina chat", + "new-group-chat": "Nuova conversazione di gruppo", + "sign-in": "Accedi", + "datetime-yesterday": "ieri", + "create-new-account": "Crea nuovo conto", + "datetime-ago": "fa", + "contacts": "Contatti", + "got-it": "OK", + "active-online": "Online", + "password": "Password", + "browsing-title": "Sfoglia", + "discover": "Scoperta", + "browsing-cancel": "Annulla", + "intro-status": "Avvia una conversazione con me per impostare il tuo conto e modificare le tue impostazioni!", + "name": "Nome", + "show-qr": "Mostra QR", + "connect": "Effettua connessione", + "edit": "Modifica", + "account-generation-message": "Dammi un secondo, devo eseguire dei calcoli matematici complessi per generare il tuo conto!", + "no-messages": "Nessun messaggio", + "passphrase": "Passphrase", + "recipient": "Beneficiario", + "members-title": "Membri", + "new-group": "Nuovo gruppo", + "phone-e164": "Internazionale 1", + "settings": "Impostazioni", + "chats": "Conversazioni", + "transaction": "Transazione", + "public-group-status": "Pubblico", + "image-source-make-photo": "Scatta", + "start-conversation": "Inizia conversazione", + "save": "Salva", + "sharing-copy-to-clipboard": "Copia negli appunti", + "sync-in-progress": "Sincronizzazione in corso...", + "send-transaction": "Invia transazione", + "incorrect-code": [ + "str", + "Il codice inserito è errato, riprova" + ], + "image-source-gallery": "Seleziona dalla galleria immagini", + "sync-synced": "Sincronizzato", + "status-pending": "In attesa di", + "datetime-day": { + "one": "giorno", + "other": "giorni" + }, + "scan-qr": "Scansiona QR", + "contact-s": { + "one": "contatto", + "other": "contatti" + }, + "next": "Avanti", + "recent": "Recente", + "status": "Stato", + "from": "Da", + "wrong-password": "Password errata", + "in-contacts": "Nei contatti", + "sharing-share": "Condividi…", + "type-a-message": "Digita un messaggio…", + "clear-history": "Cancella cronologia", + "no-contacts": "Nessun contatto registrato", + "datetime-today": "oggi", + "web-view-error": "Ops, si è verificato un errore", + "error": "Errore", + "more": "altro", + "cancel": "Annulla", + "can-not-add-yourself": "Non puoi aggiungere te stesso", + "add-to-contacts": "Aggiungi ai contatti", + "available": "Disponibile", + "You": "Tu", + "main-wallet": "Wallet principale", + "members": { + "one": "1 membro", + "other": "{{count}} membri", + "zero": "nessun membro" + }, + "intro-message1": "Benvenuto su Status\nTocca questo messaggio per impostare la tua password e iniziare!", + "new-contact": "Nuovo contatto", + "datetime-second": { + "one": "secondo", + "other": "secondi" + }, + "recover": "Ripristina", + "datetime-minute": { + "one": "minuto", + "other": "minuti" + }, + "browsing-open-in-android-web-browser": "Apri nel browser", + "browsing-open-in-ios-web-browser": "Apri nel browser", + "edit-profile": "Modifica profilo", + "active-unknown": "Sconosciuto", + "public-key": "Chiave pubblica", + "profile": "Profilo", + "none": "Nessuna", + "removed": "È stato rimosso", + "message": "Messaggio", + "here-is-your-passphrase": "Ecco qui la tua passphrase, *prendi nota e conservala in un luogo sicuro!* Ti servirà per ripristinare il tuo conto.", + "image-source-title": "Immagine profilo", + "left": "È uscito", + "to": "A", + "status-sent": "Inviato", + "data": "Dati" +} diff --git a/translations/ja.json b/translations/ja.json new file mode 100644 index 00000000000..494dbfe181e --- /dev/null +++ b/translations/ja.json @@ -0,0 +1,2239 @@ +{ + "K": "K", + "M": "M", + "NFT": "NFT", + "Pinned-a-message": "メッセージを固定", + "You": "あなた", + "about": "About", + "about-app": "このアプリについて", + "about-key-storage-content": "Statusがあなたの秘密鍵にアクセスすることはありません。シードフレーズのバックアップを忘れないようにしてください。デバイスを紛失した際に復元する唯一の手段となります。", + "about-key-storage-title": "鍵の保管について", + "about-names-content": "誰もあなたになりすますことはできません。あなたは初期設定時から匿名の状態であり、本名を明らかにする必要はありません。わずかな料金でカスタム名を登録することができます。", + "about-names-title": "名前について", + "about-sharing-data": "データの共有について", + "accent-colour": "アクセントカラー", + "accept": "許可", + "accept-and-add": "許可して追加する", + "accept-and-continue": "許可して続行", + "accept-and-share-address": "受け取りアドレスを送る", + "accept-community-rules": "コミュニティルールに同意します", + "accept-new-chats-from": "新規チャットを許可 ", + "accept-status-tos-prefix": "Statusを受け入れる", + "accepted": "追加されました", + "access-existing-keys": "既存の鍵で復元する", + "access-key": "アクセスキー", + "account-added": "アカウントを追加しました", + "account-color": "アカウントの色", + "account-content": "Statusのアカウントを銀行口座と比べてみましょう。銀行口座では通常、1つのアカウントに1つの口座番号と口座残高が紐づいています。Statusでは1つのウォレットで複数のアカウントを持つことができます。", + "account-exists-title": "アカウントはすでに存在しています", + "account-is-used": "アカウントはブラウザのDappsで使用されています。", + "account-name": "アカウント名", + "account-settings": "アカウント設定", + "account-title": "アカウント", + "accounts": "アカウント", + "actions": "アクション", + "active-members": "アクティブメンバー", + "active-online": "オンライン", + "active-unknown": "不明", + "activity": "アクティビティ", + "add": "追加", + "add-a-contact": "連絡先を追加", + "add-a-watch-account": "読み取り専用アカウントを追加", + "add-account": "アカウントを追加", + "add-account-description": "あらゆるタイプのイーサリアムアカウントをStatusウォレットにインポートすることができます", + "add-account-incorrect-password": "パスワードが間違っています。アプリのロックを解除するために使用するパスワードを入力してください。", + "add-address": "アドレスを追加してウォッチする", + "add-address-description": "アドレスまたはENSをウォッチする", + "add-an-account": "アカウントを追加", + "add-bootnode": "Bootnodeを追加", + "add-contact": "連絡先を追加", + "add-custom-token": "カスタムトークンを追加", + "add-favourite": "お気に入りに追加する", + "add-mailserver": "Statusノードを追加", + "add-me-to-your-contacts": "連絡先に追加してください", + "add-members": "メンバーを追加", + "add-network": "ネットワークを追加", + "add-new-contact": "新しい連絡先を追加", + "add-nickname": "ニックネームを追加する(オプション)", + "add-nickname-title": "ニックネームを追加", + "add-node": "ノードを追加する", + "add-private-key-account": "秘密鍵からアカウントを追加する", + "add-seed-account": "シードフレーズでアカウントを追加する", + "add-text": "テキストを追加", + "add-to-contacts": "連絡先に追加", + "add-to-contacts-text": "連絡先リストにユーザを追加することで、ウォレットアドレスを共有します。", + "add-to-favourites": "お気に入りに追加", + "add-watch-account": "読み取り専用アカウントを追加", + "added": "追加した", + "added-to-group-chat": "グループチャットに追加", + "added-you-to": "あなたを追加しました", + "address": "アドレス", + "address-or-ens-name": "アドレスまたは ENS 名", + "address-received": "送金できます", + "address-request-accepted": "受け取り準備ができました", + "address-requested": "受け取り承認待ち", + "admin": "管理者", + "advanced": "高度な設定", + "advanced-settings": "高度な設定", + "advertiser-description": "Statusは一度だけあなたのIPアドレスをチェックします。取得された情報は他のいかなる目的にも利用されることはなく、7日後に完全に消去されます。", + "advertiser-starter-pack-accept": "承認", + "advertiser-starter-pack-decline": "拒否", + "advertiser-starter-pack-description": "初回特典のトークンを受け取れます。トークンを使ってステッカーやENS名を購入してみよう。", + "advertiser-starter-pack-title": "スターターパック", + "advertiser-title": "プライバシーポリシー", + "agree-by-continuing": "以下に同意して続行する。", + "all": "すべて", + "all-connections": "すべての接続", + "all-messages": "すべてのメッセージ", + "all-time": "全期間", + "allow": "許可", + "allow-and-send": "許可して送信", + "allow-mention-notifications": "@メンションを表示する", + "allowing-authorizes-this-dapp": "アクセスを許可すると、このDAppはあなたのウォレットアドレスを取得し、Web3を有効にします。", + "alphabetically": "アルファベット順", + "already-have-asset": "このアセットはすでに持っています", + "already-logged-in-on-the-other-device": "他のデバイスですでにログインしています", + "amount": "金額", + "and": "と", + "and-go-to": "次へ進む", + "anyone": "全員", + "app-commit": "コミットハッシュ", + "app-connections": "アプリ接続", + "appearance": "画面表示", + "apply": "適用する", + "approve": "承認する", + "approve-limit": "限度額の承認", + "approve-token": "トークンを承認", + "approve-token-contract-desc": "トークンを承認することにより、そのコントラクトはあなたのトークン残高を使用できるようになります。疑わしい場合には、トークンを承認しないか、使う分だけを承認するようにしましょう。", + "apr": "4月", + "are-not-allowed": "{{check}}は使用できません", + "are-you-sure": "間違いないですか?", + "are-you-sure-description": "シードフレーズは再度表示することはできません", + "are-you-sure-to-cancel": "キャンセルしてもよいですか?", + "are-you-sure?": "間違いないですか?", + "ask-in-status": "質問するまたはバグを報告する", + "assets": "アセット", + "at": "at", + "attribution-received": "{{attrib}} \/ {{max}} ボーナスを受け取り済み", + "audio": "音声", + "audio-message": "音声メッセージ", + "audio-recorder": "レコーダー", + "audio-recorder-error": "レコーダーエラー", + "audio-recorder-max-ms-reached": "最大録音時間に達しました", + "audio-recorder-permissions-error": "音声メッセージを送信するにはマイクを許可する必要があります", + "aug": "8月", + "authorize": "承認する", + "available": "利用可能", + "available-participants": { + "other": "あと {{count}}人の参加者を選択できます" + }, + "back": "戻る", + "back-up": "バックアップ", + "back-up-seed-phrase": "シードフレーズをバックアップする", + "back-up-your-seed-phrase": "シードフレーズをバックアップする", + "backing-up": "バックアップしています...", + "backup-disabled": "無効", + "backup-enabled": "有効", + "backup-recovery-phrase": "リカバリーフレーズのバックアップ", + "backup-settings": "バックアップ設定", + "backup-through-waku": "wakuにバックアップ", + "bad-fees-description": "優先料金が推奨値を下回っています。", + "balance": "残高", + "begin-set-up": "セットアップを開始", + "below-base-fee": "最大料金が基本料金を下回っています", + "biometric-auth-android-sensor-desc": "タッチセンサー", + "biometric-auth-android-sensor-error-desc": "失敗しました", + "biometric-auth-android-title": "認証が必要です", + "biometric-auth-confirm-logout": "再ログイン", + "biometric-auth-confirm-message": "続行するには生体認証が必要です。不可能な場合はパスワードまたはパスコードを使用してサインインしてください。", + "biometric-auth-confirm-title": "認証する必要があります", + "biometric-auth-confirm-try-again": "再試行する", + "biometric-auth-error": "生体認証を実行できません ( {{code}} )", + "biometric-auth-login-error-title": "生体認証エラー", + "biometric-auth-login-ios-fallback-label": "パスワードを入力", + "biometric-auth-reason-login": "Statusにログイン", + "biometric-auth-reason-verify": "認証を確認", + "biometric-disable-bioauth": "{{bio-type-label}} を無効にする", + "biometric-disable-password-description": "これを無効にすると、", + "biometric-disable-password-title": "パスワード保存を無効にする", + "biometric-enable": "アプリを開くたびにパスワードを入力したくない場合は、{{bio-type-label}} サインインを有効にしてください", + "biometric-enable-button": "{{bio-type-label}} を有効にする", + "biometric-enable-keycard": "毎回キーカードを使用してアプリにアクセスしたくない場合は、 {{bio-type-label}}サインインを有効にしてください", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "指紋", + "biometric-secure-with": "{{bio-type-label}}でセキュアに", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "BIP39パスワード", + "biu": "BIU", + "blank-contacts-text": "連絡先がここに表示されます", + "blank-keycard-text": "鍵と名前を生成したらKeycardで先に進むことができます", + "blank-keycard-title": "空のKeycardをタップしたようです", + "blank-messages-text": "あなたのメッセージはここにあります", + "block": "ブロック", + "block-contact": "このユーザをブロックする", + "block-contact-details": "ブロックするとこのユーザのメッセージは削除され、新たなメッセージもあなたに届かなくなります", + "block-user": "ユーザをブロック", + "block-user?": "ユーザをブロックしますか?", + "blocked-users": "ブロックしたユーザ", + "bold": "太字", + "bootnode-address": "Bootnodeアドレス", + "bootnode-details": "Bootnodeの詳細", + "bootnode-format": "enode:\/\/{enode-id}@{ipアドレス}:{ポート}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodeが有効になっています", + "bootnodes-settings": "Bootnodeの設定", + "bridge": "ブリッジ", + "browsed-websites": "Webサイトの閲覧履歴がここに表示されます。", + "browser": "ブラウザ", + "browser-not-secure": "接続は安全ではありません。このサイトでは個人情報を入力したりトランザクションに署名したりしないでください。", + "browser-secure": "接続は安全です。個人情報の入力やトランザクションへの署名を行う際は、このサイトが信頼できるサイトか確認してください。", + "browsers": "ブラウザ", + "browsing-cancel": "キャンセル", + "browsing-open-in-android-web-browser": "Androidで開く", + "browsing-open-in-ios-web-browser": "iOSで開く", + "browsing-open-in-status": "Statusで開く", + "browsing-site-blocked-description1": "このアドレスから潜在的な悪意のあるアクティビティを検出しました。ウォレットを保護するために、操作を制限しています。 \n\nエラーと思われる場合は、こちらにご連絡ください。", + "browsing-site-blocked-description2": "公開チャット", + "browsing-site-blocked-go-back": "戻る", + "browsing-site-blocked-title": "このサイトはブロックされています", + "browsing-title": "ブラウズ", + "bug-report": "バグを報告", + "bug-report-description": "* 説明", + "bug-report-description-placeholder": "入力必須", + "bug-report-steps": "再現手順", + "bug-report-steps-placeholder": "- アプリを開く\n- 何かをする\n- そしてまた別のことをする...", + "bug-report-submit-email": "ログアーカイブをメールで提出する", + "bug-report-submit-gh-issue": "ログなしでGitHubイシューを提出する", + "bug-report-too-short-description": "説明文が短すぎます。", + "build-yourself": "利用規約への同意なしにアプリを利用するには、独自のバージョンを構築してお使いください", + "buy": "購入", + "buy-crypto": "暗号資産を購入する", + "buy-crypto-choose-a-service": "暗号資産の購入方法を選択します", + "buy-crypto-description": "DAppで暗号資産を購入する", + "buy-crypto-leaving": "Statusから購入先のWebサイトにアクセスします", + "buy-crypto-title": "ウォレットが空のようです", + "by-continuing-you-accept": "以下を承諾して続行する ", + "camera-access-error": "カメラにアクセスするには、システム設定から「Status → カメラ」で有効になっているか確認してください。", + "camera-permission-denied": "アクセスが拒否されました", + "can-not-add-yourself": "自分自身を追加することはできません", + "can-send-messages": "新しいメッセージを送受信できます", + "cancel": "キャンセル", + "cancel-keycard-setup": "Keycardのセットアップをキャンセル", + "cancel-request-to-join": "参加リクエストをキャンセル", + "cancel-request?": "リクエストをキャンセルしますか?", + "cancelling": "キャンセル", + "cannot-pin-desc": "ピン留めできるメッセージは最大3つです。\n新しいメッセージをピン留めするには、少なくとも1つのピン留めを解除してください。", + "cannot-pin-title": "このメッセージはピン留めできません", + "cannot-read-card": "カードが読み取れません。 \nスマートフォンの裏側に当てたままにしてください", + "cannot-use-default-pin": "パスコード 000000 は許可されていません。 \n別の番号を使ってください", + "cant-open-public-chat": "公開チャットを開くことができません", + "cant-report-bug": "バグを報告できません", + "card-is-blank": "カードが空です", + "card-reseted": "カードがリセットされました", + "card-unpaired": "カードのペアリングが現在のデバイスから解除されました", + "category": "カテゴリー", + "category-title": "カテゴリー名", + "change-fleet": "Fleetを {{fleet}} に変更する", + "change-group-privacy": "グループのプライバシーを変更する", + "change-logging-enabled": "本当にログを{{enable}}にしますか?", + "change-pairing": "ペアリングコードを変更する", + "change-pairing-description": "ペアリングコードを変更しても、現在のペアリングに影響はありません。ただし新たにペアリングする場合は新しいコードが必要になります。", + "change-pairing-title": "新たにペアリングコードを作成する", + "change-passcode": "パスコードの変更", + "change-password": "パスワードの変更", + "change-pin": "6桁のパスコードを変更する", + "change-puk": "12桁のPUKを変更する", + "change-tip": "チップを変更", + "changed-amount-warning": "金額が {{old}} から {{new}} に変更されました", + "changed-asset-warning": "アセットが {{old}} から {{new}} に変更されました", + "chaos-mode": "カオスモード", + "chaos-unicorn-day": "カオスユニコーンデー", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "チャット", + "chat-and-transact": "友達とプライベートチャットする", + "chat-is-a-contact": "連絡先に登録済み", + "chat-is-not-a-contact": "連絡先に登録されていません", + "chat-key": "チャットキー", + "chat-key-content": "Statusチャットプロトコルではメッセージは暗号化キーを使用して送受信されます。暗号化には公開チャットキーが使われています。あなたは公開チャットキーを友達に伝え、友達はそれを用いてメッセージを送信しています。", + "chat-key-description": "Statusにおけるあなたの公開IDです。他の人があなたに友だちリクエストをするために使用できます。", + "chat-key-title": "チャットキー", + "chat-link-previews": "リンクプレビュー", + "chat-name": "チャット名", + "chat-name-content": "チャットユーザ名は、3つのランダムな英単語として生成されます。すべてのユーザが異なるチャットユーザ名で生成され、他のユーザが同じ3つの英単語を持つことはありません。", + "chat-name-title": "チャットユーザ名", + "chat-notification-preferences": "通知設定", + "chat-settings": "チャット設定", + "chat-with": "{{selected-user}}とチャット", + "chat-with-friends": "友だちとプライベートチャットする", + "chats": "チャット", + "check-on-block-explorer": "ブロックエクスプローラを確認する", + "check-on-opensea": "OpenSeaでチェック", + "check-your-recovery-phrase": "シードフレーズを確認してください", + "choose-actions": "アクションを選択する", + "choose-authentication-method": "認証方法を選択します", + "choose-new-location-for-keystore": "キーストアファイルの格納先を選択します", + "choose-storage": "ストレージを選択する", + "clear": "消去", + "clear-all": "すべてクリア", + "clear-history": "履歴を消去", + "clear-history-action": "消去", + "clear-history-confirmation": "履歴を消去しますか?", + "clear-history-confirmation-content": "本当にチャット履歴を消去しますか?", + "clear-history-title": "履歴を消去しますか?", + "clear-history?": "履歴をクリアしますか?", + "cleared-chat-description-public": "ここは静かですね。会話を始めるか ", + "close": "閉じる", + "close-all": "すべて閉じる", + "close-app-button": "確認", + "close-app-content": "アプリを停止して閉じます。再度開くと選択していたネットワークが使用されます", + "close-app-title": "警告!", + "close-contact-search": "連絡先検索を閉じる", + "collectibles": "コレクティブル", + "collectibles-leak-metadata": "ここにNFTを表示できます。その場合、あなたのウォレットとIPアドレスを共有することになります。", + "colour": "カラー", + "command-button-send": "送信", + "common-names": "一般的な名称", + "communities": "コミュニティ", + "communities-alpha": "コミュニティ(アルファ)", + "communities-enabled": "コミュニティを有効にする", + "communities-verified": "✓Status認証済みコミュニティ", + "community": "コミュニティ", + "community-admins-will-review-your-request": "コミュニティ管理者がリクエストを確認します", + "community-channel": "コミュニティチャンネル", + "community-channel-read-requirements-met": "条件に合うもののみを表示", + "community-channel-read-requirements-not-met": "条件に合わないもののみを表示", + "community-channel-write-requirements-met": "条件に合うものを表示および投稿", + "community-channel-write-requirements-not-met": "条件に合わないものを表示および投稿", + "community-color": "コミュニティカラー", + "community-color-placeholder": "色を選ぶ", + "community-edit-title": "コミュニティの編集", + "community-emoji-thumbnail-title": "サムネイル", + "community-enter-channel-info": "チャンネルに入ると、あなたの公開アドレスはノード所有者に公開されます", + "community-image-delete": "Empty", + "community-image-pick": "画像を選ぶ", + "community-image-remove": "削除", + "community-image-take": "写真を撮る", + "community-info": "コミュニティ情報", + "community-info-not-found": "コミュニティ情報が見つかりません", + "community-invite-title": "招待する", + "community-join-requirements-changed": "トークン条件が変更されました", + "community-join-requirements-met": "参加条件を満たしています", + "community-join-requirements-not-met": "参加条件を満たしていません", + "community-join-requirements-tokens-lost": "必要なトークンがありません", + "community-key": "コミュニティの秘密鍵", + "community-key-placeholder": "コミュニティの秘密鍵を入力してください", + "community-kicked-body": "あなたは追い出されました", + "community-kicked-heading": "コミュニティから追放", + "community-link": "コミュニティリンク", + "community-members": { + "other": "{{count}} 名" + }, + "community-members-title": "メンバー", + "community-message-preview": "{{community-name}}への招待状", + "community-private-key": "コミュニティの秘密鍵", + "community-request-accepted": "リクエストが承認されました", + "community-request-accepted-body-text": "今、あなたはこちらのメンバーです", + "community-request-not-accepted": "リクエストが受け入れられませんでした", + "community-request-not-accepted-body-text-prefix": "参加リクエスト", + "community-request-not-accepted-body-text-suffix": "受け入れられませんでした", + "community-request-pending": "リクエスト保留中", + "community-request-pending-body-text": "参加をリクエストしました", + "community-requests-to-join-title": "参加リクエスト", + "community-roles": "役割", + "community-rules": "コミュニティルール", + "community-share-title": "共有", + "community-thumbnail-image": "アイコン画像", + "community-thumbnail-upload": "アップロード", + "complete-hardwallet-setup": "このカードは現在リンクされています。トランザクションの署名やロック解除の際に必要になります。", + "completed": "完了", + "confirm": "確認", + "confirm-new-password": "新しいパスワードを再入力", + "confirm-pairing-code-placeholder": "ペアリングコードを再入力...", + "confirm-password-placeholder": "パスワードを再入力...", + "confirm-selection": "選択内容を確認する", + "confirmation-request": "承認リクエスト", + "confirmations": "承認", + "confirmations-helper-text": "12の承認を得た場合に完了と見なすことができます。", + "connect": "接続", + "connect-mailserver-content": "{{name}} に接続しますか?", + "connect-wallet": "ウォレットを接続する", + "connect-with-users": "ユーザとつながる", + "connected": "接続済み", + "connected-to": "接続先", + "connecting": "接続しています...", + "connecting-requires-login": "別のネットワークに接続するにはログインが必要です", + "connection-request": "接続要求", + "connection-status": "接続状況", + "connection-with-the-card-lost": "カードとの接続が失われました", + "connection-with-the-card-lost-setup-text": "セットアップを再開するにはカードをスマートフォンの背面に当てたままにしてください", + "connection-with-the-card-lost-text": "セットアップを再開するにはカードをスマートフォンの背面に当てたままにしてください", + "contact-code": "チャットキー", + "contact-request": "友だちリクエスト", + "contact-request-accepted": "追加されました ✓", + "contact-request-accepted-toast": "{{name}} があなたの友だちリクエストを許可しました", + "contact-request-chat-add": "{{name}}を追加してメッセージを送信", + "contact-request-chat-pending": "友だちリクエストは保留中です", + "contact-request-declined": "却下されました ⓧ", + "contact-request-header": "👋 友だちリクエスト", + "contact-request-is-now-a-contact": "が連絡先に追加されました", + "contact-request-outgoing": "接続中... ", + "contact-request-pending": "依頼中...", + "contact-request-removed-as-contact": "連絡先として削除されました", + "contact-request-removed-you-as-contact": "連絡先として削除されました", + "contact-request-sent": "友だちリクエストを送る", + "contact-request-sent-to": "友だちリクエストを送信:", + "contact-request-sent-toast": "{{name}} から友だちリクエストが届きました", + "contact-request-was-accepted": "友だちリクエストを受け付けました", + "contact-requests": "友だちリクエスト", + "contact-s": { + "other": "の連絡先" + }, + "contacts": "連絡先", + "contacts-descr": "連絡先がここに表示されます。連絡先に追加した人のステータスを受け取ります。", + "contacts-empty": "連絡先のENS名がここに表示されます", + "continue": "続行する", + "continue-anyway": "気にせず続ける", + "contract-address": "コントラクトアドレス", + "contract-interaction": "コントラクトの実行", + "contract-isnt-supported": "コントラクトはサポートされていません", + "copy-info": "情報をコピー", + "copy-qr": "コードをコピー", + "copy-text": "テキストをコピー", + "copy-to-clipboard": "コピー", + "copy-transaction-hash": "トランザクションIDをコピー", + "cost-fee": "コスト\/手数料", + "counter-9-plus": "9+", + "counter-99-plus": "99以上", + "create": "作成", + "create-a-pin": "6桁のパスコードを作成する", + "create-a-puk": "12桁のPUKを作成する", + "create-category": "カテゴリーを作成", + "create-channel": "チャネルを作成する", + "create-channel-title": "新規チャネル", + "create-community": "コミュニティを作成する", + "create-group-chat": "グループチャットを作成", + "create-multiaccount": "新規に鍵を作る", + "create-new-key": "新しい鍵を生成", + "create-new-profile": "新しいプロフィールを作成", + "create-pin": "6桁のパスコードを作成", + "create-pin-description": "ログインとトランザクションの承認にカードと6桁のパスコードが必要になります。", + "create-profile": "プロフィールを作成", + "create-profile-password-info-box-description": "Statusキーは、Web3での自己管理型アイデンティティの基盤です。これらのキーはあなたが完全に制御でき、トランザクション署名、データへのアクセス、Web3サービスとのやり取りに使用できます。\n\nキーは常にデバイスに安全に保存され、Statusプロフィールパスワードで保護されます。Statusはパスワードを知らず、パスワードをリセットすることはできません。パスワードを忘れると、Statusプロフィールとウォレットの資金にアクセスできなくなる可能性があります。\n\nStatusパスワードは忘れずに、誰にも共有しないでください。", + "create-profile-password-info-box-title": "プロフィールパスワードについて", + "created-group-chat-description": "グループ {{group-name}} を作成しました", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "通貨", + "currency-display-name-aed": "エミラティディルハム", + "currency-display-name-afn": "アフガニスタンアフガニ", + "currency-display-name-ars": "アルゼンチンペソ", + "currency-display-name-aud": "オーストラリアドル", + "currency-display-name-bbd": "バルバドスドル", + "currency-display-name-bdt": "バングラデシュタカ", + "currency-display-name-bgn": "ブルガリアレフ", + "currency-display-name-bhd": "バーレーンディナール", + "currency-display-name-bnd": "ブルネイダルサラームドル", + "currency-display-name-bob": "ボリビアボリビアノ", + "currency-display-name-brl": "ブラジルレアル", + "currency-display-name-btn": "ブータンニュルガム", + "currency-display-name-cad": "カナダドル", + "currency-display-name-chf": "スイスフラン", + "currency-display-name-clp": "チリペソ", + "currency-display-name-cny": "中国人民元", + "currency-display-name-cop": "コロンビアペソ", + "currency-display-name-crc": "コスタリカコロン", + "currency-display-name-czk": "チェココルナ", + "currency-display-name-dkk": "デンマーククローネ", + "currency-display-name-dop": "ドミニカ共和国ペソ", + "currency-display-name-egp": "エジプトポンド", + "currency-display-name-etb": "エチオピアブル", + "currency-display-name-eur": "ユーロ", + "currency-display-name-gbp": "英国ポンド", + "currency-display-name-gel": "ジョージアラリ", + "currency-display-name-ghs": "ガーナセディ", + "currency-display-name-hkd": "香港ドル", + "currency-display-name-hrk": "クロアチアクーナ", + "currency-display-name-huf": "ハンガリーフォリント", + "currency-display-name-idr": "インドネシアルピア", + "currency-display-name-ils": "イスラエルシェケル", + "currency-display-name-inr": "インドルピー", + "currency-display-name-isk": "アイスランドクローナ", + "currency-display-name-jmd": "ジャマイカドル", + "currency-display-name-jpy": "日本円", + "currency-display-name-kes": "ケニアシリング", + "currency-display-name-krw": "韓国ウォン", + "currency-display-name-kwd": "クウェートディナール", + "currency-display-name-kzt": "カザフスタンテンゲ", + "currency-display-name-lkr": "スリランカルピー", + "currency-display-name-mad": "モロッコディルハム", + "currency-display-name-mdl": "モルドバレイ", + "currency-display-name-mur": "モーリシャスルピー", + "currency-display-name-mwk": "マラウィアンクワチャ", + "currency-display-name-mxn": "メキシコペソ", + "currency-display-name-myr": "マレーシアリンギット", + "currency-display-name-mzn": "モザンビークメチカル", + "currency-display-name-nad": "ナミビアドル", + "currency-display-name-ngn": "ナイジェリアナイラ", + "currency-display-name-nok": "ノルウェークローネ", + "currency-display-name-npr": "ネパールルピー", + "currency-display-name-nzd": "ニュージーランドドル", + "currency-display-name-omr": "オマーンリアル", + "currency-display-name-pen": "ペルーソル", + "currency-display-name-pgk": "パプアニューギニアキナ", + "currency-display-name-php": "フィリピンペソ", + "currency-display-name-pkr": "パキスタンルピー", + "currency-display-name-pln": "ポーランドズウォティ", + "currency-display-name-pyg": "パラグアイグアラニ", + "currency-display-name-qar": "カタールリヤル", + "currency-display-name-ron": "ルーマニアレイ", + "currency-display-name-rsd": "セルビアディナール", + "currency-display-name-rub": "ロシアルーブル", + "currency-display-name-sar": "サウジアラビアリヤル", + "currency-display-name-sek": "スウェーデンクローナ", + "currency-display-name-sgd": "シンガポールドル", + "currency-display-name-thb": "タイバーツ", + "currency-display-name-try": "トルコリラ", + "currency-display-name-ttd": "トリニダードトバゴドル", + "currency-display-name-twd": "台湾ドル", + "currency-display-name-tzs": "タンザニアシリング", + "currency-display-name-uah": "ウクライナグリブナ", + "currency-display-name-ugx": "ウガンダシリング", + "currency-display-name-usd": "米ドル", + "currency-display-name-uyu": "ウルグアイペソ", + "currency-display-name-vef": "ベネズエラボリバル", + "currency-display-name-vnd": "ベトナムドン", + "currency-display-name-zar": "南アフリカランド", + "current": "現在", + "current-average": "現在の平均", + "current-average-tip": "現在の平均チップ", + "current-base": "現在の基本料金", + "current-base-fee": "現在の基本手数料", + "current-minimum-tip": "現在の最小チップ", + "current-network": "現在のネットワーク", + "current-password": "現在のパスワード", + "current-pin": "6桁のパスコードを入力", + "current-pin-description": "続行するには6桁のパスコードを入力します", + "custom": "カスタム", + "custom-networks": "カスタムネットワーク", + "custom-node": "カスタムRPCエンドポイントを使用しています。ローカルの転送履歴が不完全となる場合があります。", + "custom-seed-phrase": "無効なシードフレーズ", + "custom-seed-phrase-text-1": "シードフレーズが一致しません。入力した単語にスペルミスがないか確認してください。", + "cut": "カット", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "許可して開く", + "dapp-starter-pack-description": "初回特典のトークンを受け取れます。トークンを使ってステッカーやENS名を購入してみよう。", + "dapp-starter-pack-title": "スターターパック", + "dapp-would-like-to-connect-wallet": "があなたのウォレットにアクセスしようとしています", + "dapps": "ÐApps", + "dapps-permissions": "DAppのアクセス許可", + "dark": "ダーク", + "data": "データ", + "data-collected": "収集されたデータ", + "data-collected-subtitle": "下の表は、保存されているデータと送信されるデータの正確な内容を示しています。データは公的なルールに照らし合わせて検証され、機密データが送信されないようになっています。 \"Don't trust, verify.\"", + "data-syncing": "データ同期", + "database-reset-content": "チャット、連絡先、設定が削除されました。Keycardでアカウントを利用できます。", + "database-reset-title": "データベースのリセット", + "database-reset-warning": "データベースがリセットされます。チャット、連絡先、設定が削除されます。", + "datetime-ago": "前", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "other": "日" + }, + "datetime-day-short": { + "other": "日" + }, + "datetime-hour": { + "other": "時間" + }, + "datetime-hour-short": { + "other": "時間" + }, + "datetime-minute": { + "other": "分" + }, + "datetime-minute-short": { + "other": "分" + }, + "datetime-second": { + "other": "秒" + }, + "datetime-second-short": { + "other": "秒" + }, + "datetime-today": "今日", + "datetime-yesterday": "昨日", + "days": "日", + "dec": "12月", + "decimals": "小数点桁数", + "decline": "取り消す", + "declined": "却下されました", + "decryption-failed-content": "データを復号化できませんでした。「適用」をタップして古いデータを削除し新しいアカウントを作成し直すか、「キャンセル」を押してもう一度やり直してください。", + "default": "デフォルト", + "default-assets": "ERC20・ERC721トークン", + "default-sync-period": "同期履歴 ", + "delete": "削除", + "delete-account": "アカウントを削除する", + "delete-and-leave-group": "グループを削除して退出する", + "delete-bootnode": "Bootnodeを削除", + "delete-bootnode-are-you-sure": "本当にこのbootnodeを削除しますか?", + "delete-bootnode-title": "Bootnodeを削除", + "delete-category-confirmation": "このカテゴリを削除しますか?", + "close-chat": "チャットを削除する", + "close-chat-confirmation": "本当にこのチャットを削除しますか?", + "delete-confirmation": "削除しますか?", + "delete-for-everyone": "全員に削除", + "delete-for-me": "削除", + "delete-keys-keycard": "Keycardから鍵を削除する", + "delete-mailserver": "Statusノードを削除する", + "delete-mailserver-are-you-sure": "本当にこのStatusノードを削除しますか?", + "delete-mailserver-title": "Statusノードを削除する", + "delete-message": "メッセージを削除", + "delete-my-account": "アカウントを削除する", + "delete-my-profile": "プロフィールを削除する", + "delete-network-confirmation": "本当にこのネットワークを削除しますか?", + "delete-network-error": "このネットワークを削除する前に別のネットワークに接続してください", + "delete-network-title": "ネットワークを削除しますか?", + "delete-node": "ノードを削除する", + "delete-node-are-you-sure": "このノードを削除しますか?", + "delete-node-title": "ノードを削除", + "delete-profile": "プロフィールを削除する", + "delete-profile-warning": "警告:シードフレーズを保管していない場合、プロファイルを削除後は一切の資金にアクセスできなくなります。", + "deleted-this-message": "メッセージを削除しました", + "delivered": "配信済み", + "deny": "拒否", + "derivation-path": "Derivation path", + "describe-channel": "チャネルの説明", + "description": "説明", + "desktop": "デスクトップ", + "destroy": "破壊する", + "dev-mode": "デベロッパーモード", + "dev-mode-settings": "開発モードの設定", + "device-syncing": "デバイスの同期", + "devices": "デバイス", + "disable": "無効", + "disable-all": "すべて無効にする", + "disable-later-in-settings": "これは後で設定画面で無効にできます", + "disabled": "無効", + "disconnect": "接続解除", + "disconnected": "チャットがオフライン", + "discover": "見つける", + "discover-communities": "コミュニティを探す", + "dismiss": "無視する", + "display-collectibles": "コレクションを表示", + "done": "完了", + "dont-ask": "このサイトでは許可しない", + "duration-estimate": "推定期間", + "edit": "編集", + "edit-categories": "カテゴリを編集する", + "edit-channel-title": "チャネルを編集", + "edit-chats": "チャットを編集", + "edit-community": "コミュニティを編集する", + "edit-favourite": "お気に入りを編集する", + "edit-group": "グループを編集", + "edit-message": "メッセージの編集", + "edit-name-and-image": "名前と画像を編集する", + "edit-name-image": "名前と画像を編集する", + "edit-nickname": "ニックネームを編集", + "edit-profile": "プロフィールを編集", + "edited": "編集済み", + "editing-message": "メッセージの編集", + "emoji-activity": "アクティビティ", + "emoji-flags": "フラグ", + "emoji-food": "フード", + "emoji-hash": "エモジハッシュ", + "emoji-hash-copied": "エモジハッシュがクリップボードにコピーされました", + "emoji-nature": "自然", + "emoji-no-results-description": "「虹」などを試してみましょう", + "emoji-no-results-title": "検索に一致する絵文字はありません", + "emoji-objects": "オブジェクト", + "emoji-people": "人々", + "emoji-recent": "最近", + "emoji-search-placeholder": "絵文字を検索", + "emoji-symbols": "シンボル", + "emoji-travel": "旅行", + "emojihash": "エモジハッシュ", + "emojihash-description": "あなたのチャットキーを視覚的に表現したものです。他のユーザがあなたのプロフィールを認識するのに役立ちます。", + "emojis": "絵文字", + "empty-activity-center": "チャットの通知が\nここに表示されます", + "empty-chat-description": "このチャットにはまだメッセージがありません", + "empty-chat-description-community": "ここでは{{quiet-hours}}時間会話がありません。", + "empty-chat-description-one-to-one": "ここで送信されるあらゆるメッセージは暗号化され、他の人が読むことはできません", + "empty-chat-description-public": "ここでは{{quiet-hours}}時間会話がありません。会話を始めましょう", + "empty-chat-description-public-share-this": "このチャットを共有", + "empty-keycard": "空のKeycard", + "empty-keycard-required": "空のキーカードが必要です", + "empty-notifications-admin-tab": "未読の管理者通知はありません", + "empty-notifications-all-tab": "未読の通知はありません", + "empty-notifications-contact-requests-tab": "友だちリクエストはありません", + "empty-notifications-identity-verification-tab": "本人確認の要求はありません", + "empty-notifications-membership-tab": "入会通知はありません", + "empty-notifications-mentions-tab": "未読のメンション通知はありません", + "empty-notifications-replies-tab": "未読の返信通知はありません", + "empty-notifications-subtitle-read": "通知はここに表示されます", + "empty-notifications-subtitle-unread": "未読の通知はこちら", + "empty-notifications-system-tab": "システム通知はありません", + "empty-notifications-title-read": "通知なし", + "empty-notifications-title-unread": "最新の状態です", + "empty-notifications-transactions-tab": "取引通知はありません", + "empty-pending-invitations-descr": "招待リンクを見てグループ参加をリクエストした人がここに表示されます", + "empty-tab": "空のタブ", + "empty-tab-description": "C'mon do something...", + "enable": "有効", + "enable-access-to-camera": "カメラへのアクセスを有効にする", + "enable-all": "有効にする", + "enable-biometrics": "生体認証を有効にする", + "enable-camera": "カメラを有効にする", + "enable-link-previews": "プレビューを有効にしますか?", + "enable-notifications-sub-title": "誰かがあなたにメッセージや暗号資産を送ったときに通知を受け取ります", + "encrypt-with-password": "パスワードで暗号化", + "ending-not-allowed": "{{ending}}で終わるものは使用できません", + "ens-10-SNT": "10 SNT", + "ens-add-username": "ユーザ名を追加", + "ens-agree-to": "に同意する", + "ens-chat-settings": "チャット設定", + "ens-custom-domain": "カスタムドメイン", + "ens-custom-username-hints": "username.domain.eth のようにカスタムドメインを含むユーザ名全体を入力してください", + "ens-custom-username-taken": "このユーザ名はあなたのものではありません :(", + "ens-deposit": "デポジット", + "ens-dismiss-message": "クリックして閉じる", + "ens-displayed-with": "チャットでこの名前を表示します", + "ens-get-name": "ユニバーサルユーザ名を取得", + "ens-got-it": "わかりました", + "ens-locked": "ユーザ名はロックされています。 {{date}}まで返却できません", + "ens-name-content": "ENS名とは、複雑なチャットキーを読みやすい名前に代替するイーサリアムネームサービスのサービスです。ENS名は分散型ユーザ名とも呼ばれます。", + "ens-name-not-found": "ENS名を見つけられません", + "ens-name-title": "ENS名", + "ens-network-restriction": "メインネットでのみ利用可能", + "ens-no-usernames": "接続しているユーザ名がありません", + "ens-or-chat-key": "ENSまたはチャットキー", + "ens-powered-by": "Powered by Ethereum Name Services", + "ens-primary-username": "表示するユーザ名", + "ens-register": "登録", + "ens-registration-failed": "ユーザ名を登録するには、もう1度やり直してください。", + "ens-registration-failed-title": "トランザクション処理が失敗しました", + "ens-registration-failure": "登録に失敗しました", + "ens-registration-in-progress": "登録中...", + "ens-release-username": "ユーザ名を返却する", + "ens-remove-hints": "削除すると、あなたの鍵からユーザ名が切り離されます。", + "ens-remove-username": "ユーザ名を削除", + "ens-saved": " とあなたのチャットキーとの紐付けが完了しました。", + "ens-saved-title": "ユーザ名が追加されました", + "ens-show-username": "チャットで自分のENS名を表示する", + "ens-terms-header": "ENS名 登録規約", + "ens-terms-point-1": "資産は1年間デポジットされます。あなたのSNTはロックされますが消費はされません。", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e(ENSレジストリ)。", + "ens-terms-point-2": "1年後にENS名を返却し、デポジットを取り戻すことができます。何もしなければ名前は維持されます。", + "ens-terms-point-3": "もしStatusがコントラクトをアップグレードする等によりコントラクトの規約が変更された場合は、ユーザはデポジット期間に関係なくENS名を返却する権利を持ちます。", + "ens-terms-point-4": "コントラクトの管理者はあなたがデポジットした資産にアクセスすることはできません。コントラクトは資産を送信元のアドレスに送り返す機能のみを有しています。", + "ens-terms-point-5": "あなたのアドレスはパブリックにENS名と紐づけられます", + "ens-terms-point-6": "ユーザ名はstateofus.ethのサブドメインとして作成されます。ENSのスマートコントラクトの規約に従います。", + "ens-terms-point-7": "コントラクトがあなたに代わってSNTを送信することを承認します。これはあなたが送信のトランザクションを承認した時のみ発生します。", + "ens-terms-point-8": "これらの規約は以下のアドレスにおけるスマートコントラクトのロジックによって保証されます:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar) ", + "ens-terms-registration": "ENS名 登録規約", + "ens-test-message": "こんにちは", + "ens-transaction-pending": "トランザクション処理中", + "ens-understand": "私のウォレットアドレスとユーザ名がパブリックに紐づけられることを理解しました。", + "ens-username": "ENS名", + "ens-username-already-added": "ユーザ名はすでにチャットキーに紐付けられました。Statusでご利用いただけます。", + "ens-username-available": "✓このユーザ名は利用可能です!", + "ens-username-connected": "このユーザ名はあなたが所有しており、あなたのチャットキーと接続されています。", + "ens-username-connected-continue": "続行して「チャットでENS名を表示する」設定を行います。", + "ens-username-connected-with-different-key": "ユーザ名を現在のあなたのチャットキーに紐付けるため処理が必要です。", + "ens-username-connection-confirmation": "処理が完了すると {{username}} が紐付けられます。", + "ens-username-hints": "4文字以上、英数字、小文字のみ", + "ens-username-invalid": "英数字のみ", + "ens-username-invalid-name-warning": "ENS登録手続きが無効な状態で終了したものがあります。その名前を取引に使用しないでください。support@ status.im までご連絡ください。", + "ens-username-owned": "✓このユーザ名はあなたが所有しています。 ", + "ens-username-owned-continue": "続行してこのユーザ名とあなたのチャットキーを紐付ける。", + "ens-username-registration-confirmation": "素晴らしい!処理が終わったら{{username}}は登録完了です。", + "ens-username-registration-invalid": "警告: 登録手続きが無効な状態で終了しました。この名前を取引に使用しないでください。support@status.im にご連絡ください。", + "ens-username-taken": "このユーザ名はすでに使われています :(", + "ens-username-you-can-follow-progress": "ウォレットの取引履歴で処理状況を確認できます。", + "ens-usernames": "ENS名", + "ens-usernames-details": "シンプルなあなただけのユーザ名を登録する", + "ens-want-custom-domain": "他のドメイン名を所有しています", + "ens-want-domain": "stateofus.eth のドメインを取得する", + "ens-welcome-hints": "ENS名は、複雑なアドレスをシンプルなあなただけのユーザ名に変換します。", + "ens-welcome-point-customize": "ENS名を登録することで、あなたのユーザ名(3つの単語)をシンプルなものに変更できます。「{{name}}」の代わりに「@ユーザ名」で表示できます。", + "ens-welcome-point-customize-title": "チャット名をカスタマイズする", + "ens-welcome-point-receive": "友達はチャット画面の中で簡単にあなたに資産を送ることができます。", + "ens-welcome-point-receive-title": "チャット画面で資産を受け取る", + "ens-welcome-point-register": "登録時にデポジットしたSNTは、1年後にENS名を返却して返金として受け取ることもできます。ユーザ名をそのまま維持する場合は、特に何もする必要はありません。", + "ens-welcome-point-register-title": "登録には 10 SNT が必要です", + "ens-welcome-point-simplify": "資産を送ってもらうときも、複雑なアドレス(0x ...から始まる文字列)ではなくシンプルなENS名で受け取ることができます。", + "ens-welcome-point-simplify-title": "ETHアドレスをシンプルにする", + "ens-welcome-point-verify": "すでにお使いのユーザ名がある場合も、次のステップで追加できます。", + "ens-welcome-point-verify-title": "すでにユーザ名をお持ちですか?", + "ens-your-username": "あなたのユーザ名", + "ens-your-usernames": "あなたのユーザ名", + "ens-your-your-name": "あなたのENS名", + "ensure-qr-code-in-focus-to-scan": "QRコードに焦点を合わせてください", + "ensure-qr-code-is-in-focus-to-scan": "QRコードに焦点を合わせてください", + "enter-12-words": "シードフレーズの12の英単語を半角スペースで区切って入力してください", + "enter-a-private-key": "秘密鍵を入力", + "enter-a-seed-phrase": "シードフレーズを入力", + "enter-address": "アドレスを入力", + "enter-channel": "チャンネルに入る", + "enter-contact-code": "ENS名またはチャットキーを入力", + "enter-pair-code": "ペアリングコードを入力", + "enter-pair-code-description": "ペアリングコードは、Keycardのセットアップ時に表示されたコードです。", + "enter-password": "パスワードを入力", + "enter-password-migration-prompt": "パスワードを入力して、連絡先、チャット、設定を鍵と一緒に移行します", + "enter-pin": "6桁のパスコードを入力", + "enter-puk-code": "PUKコードを入力", + "enter-puk-code-description": "6桁のパスコードがブロックされました。 \nパスコードをブロック解除するには、PUKコードを入力してください。", + "enter-recipient-address-or-username": "受取人のアドレスまたはユーザ名を入力します。", + "enter-seed-phrase": "シードフレーズを入力", + "enter-sync-code": "同期コードを入力", + "enter-url": "URLを入力", + "enter-user-pk": "ユーザ公開鍵を入力してください", + "enter-watch-account-address": "QRコードをスキャンする\nまたは\nアドレスを入力する", + "enter-word": "単語を入力", + "enter-your-code": "6桁のパスコードを入力", + "enter-your-password": "パスワードを入力", + "error": "エラー", + "error-loading-audio": "オーディオの読み込み中にエラーが発生しました", + "error-syncing-connection-failed": "おっと!接続に失敗しました。もう一度やり直してください", + "error-this-is-not-a-sync-qr-code": "おっと!これは同期QRコードではありません", + "error-unable-to-get-balance": "残高を取得できません", + "error-unable-to-get-prices": "通貨変換エラー。画面を更新してもう一度やり直してください。", + "error-unable-to-get-token-balance": "トークン残高を取得できません", + "errors": "エラー", + "eth": "ETH", + "ethereum-address": "イーサリアムアドレス", + "ethereum-node-started-incorrectly-description": "イーサリアムノードが誤った設定で起動されたため、アプリケーションを停止しています。ネットワークIDは{{network-id}}に設定されていますが、正しくは{{fetched-network-id}}です。", + "ethereum-node-started-incorrectly-title": "イーサリアムノードが正しく起動できませんでした", + "etherscan-lookup": "Etherscanで調べる", + "everyone": "全員", + "everyone-mention": "全員", + "expand-all": "すべて展開", + "experienced-web3": "Web3の経験はありますか?", + "export-account": "アカウントをエクスポート", + "export-key": "秘密鍵をエクスポートする", + "external-link": "外部リンク", + "external-storage-denied": "外部ストレージへのアクセスが拒否されました", + "failed": "失敗しました", + "faq": "よくある質問", + "fast": "Fast", + "favorite-communities": "お気に入りのコミュニティ", + "favourite": "お気に入り", + "favourite-description": "お気に入りに追加したウェブサイトがここに表示されます", + "favourites": "お気に入り", + "favourites-empty": "お気に入りに追加したアドレスがここに表示されます", + "featured": "Featured", + "feb": "2月", + "fee-cap": "手数料上限", + "fee-explanation": "トランザクションの最大の全体価格。ブロックの基本料金がこれを超える場合は、基本料金の低い次のブロックに含まれます。", + "fee-options": "推奨手数料オプション", + "fetch-community": "コミュニティを更新", + "fetch-messages": "↓メッセージを取得する", + "fetch-timeline": "読み込む", + "fetching-community": "コミュニティを更新...", + "find": "見つける", + "find-sync-code": "同期コードを見つける", + "find-your-friends": "ENSまたはチャットキーで友だちを見つける", + "finish": "完了", + "finishing-card-setup": "カードのセットアップを完了しています", + "fleet": "Fleet", + "fleet-settings": "Fleet設定", + "follow": "フォローする", + "follow-your-interests": "公開チャットに参加して新しい人たちと会話する", + "forgot-password": "パスワードを忘れた方はこちら", + "forgot-your-password-info-create-new-password": "新しいパスワードを作成する", + "forgot-your-password-info-create-new-password-description": "新しいパスワードを入力したらすべて完了です。新しいパスワードを使用できるようになります。", + "forgot-your-password-info-description": "パスワードを復旧するには、以下の手順に従います:", + "forgot-your-password-info-reinstall-app": "Statusアプリを再インストールする", + "forgot-your-password-info-reinstall-app-description": "ストアからアプリを再ダウンロードしてください。", + "forgot-your-password-info-remove-app": "Statusアプリを削除する", + "forgot-your-password-info-remove-app-description": "これにより、パスワードを含むすべてのデータがデバイスから消去されます。", + "forgot-your-password-info-signup-with-key": "既存のキーを使用して", + "forgot-your-password-info-signup-with-key-description": "シードフレーズまたはKeycardでアクセスします。", + "forgot-your-password-info-title": "パスワードをお忘れですか?", + "fr": "金", + "free": "↓ FREE", + "fri": "金", + "from": "From", + "from-capitalized": "From", + "gas-amount-limit": "ガス合計金額", + "gas-limit": "Gas limit", + "gas-price": "Gas price", + "gas-used": "ガス消費", + "gated": "Gated", + "generate-a-key": "鍵を生成", + "generate-a-new-account": "新規アカウントを作る", + "generate-a-new-key": "新規に鍵を作る", + "generate-account": "新規に鍵を作る", + "generate-an-account": "アカウントを追加", + "generate-keys": "鍵を生成", + "generate-keys-subtitle": "Statusの新しい自己主権型アイデンティティ", + "generate-new-key": "新規に鍵を作る", + "generate-scan-sync-code": "スキャン同期コードを生成", + "generating-codes-for-pairing": "プロダクトソフトウェアをダウンロードしています\nアンロックコードとペアリングコードを生成しています", + "generating-keys": "鍵を生成しています...", + "generating-mnemonic": "シードフレーズを生成しています", + "get-a-keycard": "Keycardを購入", + "get-started": "始める", + "get-status-at": "https:\/\/status.im でStatusを入手", + "get-stickers": "ステッカーを入手", + "getting-started-description": "Statusは、分散型テクノロジー、プライバシー重視のデザイン、Web3機能を組み合わせた、ユニークで安全なコミュニケーション体験を提供するメッセージングアプリです。\n\nStatusキーは、Web3での自己管理型アイデンティティの基盤です。これらは、安全に個人情報とIDにアクセスする手段として機能します。Statusを使用すると、一元管理された組織に依存することなく、データとデジタルIDを所有、制御、管理できます。\n\n次のいずれかのオプションを選択して、Statusプロフィールを作成します。", + "getting-started-generate-keys-description": "Statusでのアイデンティティを表す暗号鍵のペアを作成します。中央集権的なアプリとは異なり、Statusではあなたのプロフィール作成にユーザ名やメールアドレスを必要としません。代わりに、秘密鍵と公開鍵が使用されます。このオプションでは、新しいイーサリアムアドレスを作成します。", + "getting-started-generate-keys-from-recovery-phrase": "リカバリフレーズからキーを生成", + "getting-started-generate-keys-from-recovery-phrase-description": "既存のイーサリアムアドレスを使用して、Statusプロフィールを作成することができます。すでに非保管型のイーサリアムウォレット(メタマスクやトラストウォレットなど)を使用している場合、イーサリアムアドレスとリカバリフレーズをすでに持っています。\n\nキーは常にデバイスに安全に保存され、Statusパスワードによって保護されています。", + "getting-started-generate-keys-on-keycard": "Keycardにキーを生成する", + "getting-started-generate-keys-on-keycard-description": "Keycardは、Statusキーを安全に保管する物理的なカード(クレジットカードに似ています)です。Keycardを使用すると、デジタル資産とIDのセキュリティがさらに強化されます。このオプションでは、新しいイーサリアムアドレスを作成します。", + "getting-started-with-status": "Statusを始める", + "gif": "GIF", + "give-a-short-description": "簡単な説明を加えてください。", + "give-a-short-description-community": "簡単な説明を加えてください。", + "give-permissions-camera": "カメラを許可する", + "glossary": "用語集", + "go-to-settings": "設定に移動...", + "got-it": "わかりました", + "grant-face-id-permissions": "Face IDを利用する場合は、システム環境設定に移動し「Status → Face ID」が有効になっていることを確認してください", + "group-chat": "グループチャット", + "group-chat-admin": "管理者", + "group-chat-admin-added": "{{member}} が管理者になりました", + "group-chat-all-contacts-invited": "すべての連絡先がグループに参加しています。", + "group-chat-created": "{{member}} がグループ {{name}} を作成しました", + "group-chat-decline-invitation": "招待を辞退する", + "group-chat-member-added": "{{member}} が招待されました", + "group-chat-member-joined": "{{member}} がグループに参加しました", + "group-chat-member-removed": "{{member}} がグループを退出しました", + "group-chat-members-count": "{{selected}}\/{{max}} メンバー", + "group-chat-name-changed": "{{member}} がグループ名を {{name}} に変更しました", + "group-chat-no-contacts": "まだ連絡先がありません。 \n友達を招待してチャットを始めましょう", + "group-chat-not-member": "あなたはこのグループのメンバーではありません", + "group-details": "グループの詳細", + "group-info": "グループ情報", + "group-invite": "グループへの招待", + "group-invite-link": "グループ招待リンク", + "group-membership-request": "グループへの参加を要求しています", + "groups": "グループ", + "gwei": "Gwei", + "has-permissions": "アクセスする権限があります", + "hash": "ハッシュ", + "have-a-sync-code?": "同期コードをお持ちですか?", + "help": "ヘルプ", + "help-capitalized": "ヘルプ", + "help-center": "ヘルプセンター", + "help-improve-status": "Statusの改善にご協力ください", + "here-is-a-cat-in-a-box-instead": "代わりに箱の中の猫を紹介します", + "hide": "閉じる", + "hide-content-when-switching-apps": "スクリーンショットをブロックする", + "hide-content-when-switching-apps-ios": "プレビューを非表示", + "history": "履歴", + "history-nodes": "Statusノード", + "hold-card": "カードをスマートフォンの背面にあててください", + "home": "ホーム", + "hooks": "フック", + "how-it-works": "友達招待の手順", + "how-to-pair": "ペアリング方法", + "how-to-scan": "スキャンする方法", + "http-gateway-error": "リクエストが失敗しました", + "i-dont-have-status-on-another-device": "他の端末でStatusを使用していません", + "identicon-ring": "IDリング", + "identicon-ring-explanation": "プロフィール写真を囲むこのカラフルなリングは、あなたのチャットキーを表しています。", + "identifier": "識別子", + "identity-verification": "本人確認", + "identity-verification-request": "本人確認", + "identity-verification-request-sent": "要求", + "if-you-cancel": "キャンセルした場合も、いつでもこのコミュニティへの参加を再リクエストできます。", + "im-new": "初めてです", + "image": "画像", + "image-remove-current": "現在の写真を削除", + "image-source-gallery": "ギャラリーから選択", + "image-source-make-photo": "キャプチャ", + "image-source-title": "画像の編集", + "images": "画像", + "import": "インポートする", + "import-community": "コミュニティをインポートする", + "import-community-title": "コミュニティをインポートする", + "in": "Empty", + "in-contacts": "連絡先", + "include": "含む", + "incoming": "受信", + "incoming-transaction": "送信トランザクション", + "incorrect-code": [ + "str", + "コードが正しくありません。もう一度入力してください" + ], + "increase-gas": "手数料を追加", + "initialization": "初期化", + "input-data": "入力データ", + "install": "↓ インストール", + "instruction-after-qr-generated": "他のデバイスで [同期] 画面に移動し、[同期スキャン] を選択します。", + "insufficient-balance-to-cover-fee": "トランザクション手数料を補う残高がありません", + "intro-message1": "Statusへようこそ!このメッセージをタップしてパスワードを設定して始めよう!", + "intro-privacy-policy-note1": "Statusはいかなる個人情報も収集・共有・販売しません。以下に同意して続行する。", + "intro-privacy-policy-note2": "プライバシーポリシー", + "intro-text": "Statusは分散型ウェブへのゲートウェイです", + "intro-text1": "1対1で暗号化されたネットワークでは、メッセージを誰かに盗み見られる心配はありません", + "intro-text2": "世界中のどこでもデジタル資産を送受信できます。銀行口座は必要ありません。", + "intro-text3": "お気に入りのゲーム、SNS、取引所アプリを探して、自分のデータを自分だけのものにしよう。", + "intro-title1": "完全にプライベートなコミュニケーション", + "intro-title2": "セキュアな暗号資産ウォレット", + "intro-title3": "分散型アプリ", + "intro-wizard-text1": "いくつかの鍵を使ってあなたのアカウントを管理します。鍵はお使いのデバイスに保存され、あなただけが使用することができます。", + "intro-wizard-text2": "こちらはチャット用のあなたの名前と鍵になります。好きなものを1つを選んでください。これは後で変更できません。", + "intro-wizard-text3": "Keycardをお持ちの場合は、セキュリティを強化するためにKeycardに鍵を保管してください。", + "intro-wizard-text4": "鍵を暗号化し保護する", + "intro-wizard-text6": "Statusは新着メッセージを通知します。これは後から通知設定で変更できます。", + "intro-wizard-title-alt4": "パスワードを作成", + "intro-wizard-title-alt5": "パスワードを確認", + "intro-wizard-title1": "はじめに鍵を取得する", + "intro-wizard-title2": "チャット名を選ぶ", + "intro-wizard-title3": "鍵の保管場所を選択", + "intro-wizard-title4": "6桁のパスコードを作成する", + "intro-wizard-title5": "パスコードを確認", + "intro-wizard-title6": "通知を有効にする", + "introduce-yourself": "簡単なメッセージで自己紹介", + "invalid-address-qr-code": "スキャンしたQRコードに有効なアドレスが含まれていません", + "invalid-ens-or-key": "無効な ENS またはチャット キー", + "invalid-format": "無効な形式\n {{format}}の形式である必要があります", + "invalid-key-confirm": "適用する", + "invalid-key-content": "ファイルが破損しているため、複数アカウントのデータベースが暗号化できません。回復する方法はありません。キャンセルボタンを押しても何も起こりません。{{erase-multiaccounts-data-button-text}}ボタンを押すと複数アカウントのデータベースは削除され、複数アカウントのロックが解除されます。その場合、複数アカウントのデータは消失します。", + "invalid-number": "無効な数字", + "invalid-pairing-password": "無効なペアリングパスワード", + "invalid-public-chat-topic": "無効な公開チャットトピック", + "invalid-range": "フォーマットが無効です。 {{min}}と{{max}}の間にする必要があります", + "invalid-username-or-key": "無効なユーザ名またはチャットキー", + "invite": "招待する", + "invite-button": "招待する", + "invite-chat-accept": "受け入れる", + "invite-chat-accept-join": "承諾して参加する", + "invite-chat-intro": "友達からStatusへの招待がありました。初めての方は初回特典のトークンを受け取れます。トークンを使ってステッカーやENS名を購入してみましょう。", + "invite-chat-name": "友達紹介", + "invite-chat-pending": "ペンディング", + "invite-chat-rule": "受け入れると友達にも紹介ボーナスがもらえます", + "invite-chat-starter-pack": "スターターパック", + "invite-contacts": "連絡先リストから招待する", + "invite-friends": "友達を招待", + "invite-friends-and-family": "友人や家族をStatusに招待する", + "invite-friends-to-status": "友だちをStatusに招待する", + "invite-instruction-fifth": "紹介ボーナスはいつでも受け取ることができます。", + "invite-instruction-first": "友達に招待リンクを送って、友達がStatusをダウンロードして参加します。", + "invite-instruction-fourth": "あなたは紹介ボーナスを受け取り、友達はスターターパックを受け取ります", + "invite-instruction-second": "友達がStatusをダウンロードしてアカウントを作成します。", + "invite-instruction-third": "友達とのチャットが開始されたときに紹介ボーナスが反映されます。", + "invite-people": "招待する", + "invite-people-from-contacts": "連絡先リストから招待する", + "invite-privacy-policy-public": "招待リンクを通じてStatusをインストールしました。このチャットに参加することで招待者を紐付け、以下の内容に同意します。", + "invite-privacy-policy1": "お客様が紹介プログラムを利用するにあたり、以下の規約を承諾するものとします。", + "invite-privacy-policy2": "利用規約", + "invite-public-chat-home": "招待", + "invite-public-chat-intro": "初回特典のトークンを受け取れます。トークンを使ってステッカーやENS名を購入してみよう。", + "invite-receive-account": "紹介ボーナスを受け取るアカウント", + "invite-reward": "友達を紹介するごとにボーナスがもらえます!", + "invite-reward-friend": "友達:", + "invite-reward-friend-description": "友達は{{reward}}を含むスターターパックを受け取ります", + "invite-reward-friend-name": "スターターパック", + "invite-reward-you": "あなた:", + "invite-reward-you-description": "友達を招待すると、紹介ボーナスとして{{reward}}を受け取れます。受け取ったボーナスは、ステッカーやENS名の購入に利用できます。", + "invite-reward-you-name": "紹介ボーナス", + "invite-select-account": "紹介ボーナスを受け取るアカウントを選択してください", + "invite-warning": "このプロモーションは、米国の居住者ではないAndroidデバイスのユーザのみが対象です。友達は7日以内に招待を確認する必要があります。", + "invited": "招待済", + "italic": "イタリック", + "jan": "1月", + "join": "参加", + "join-a-community": "またはコミュニティに参加する", + "join-group-chat": "グループに参加", + "join-group-chat-description": "{{username}} があなたをグループ {{group-name}} に招待しました", + "join-me": "Statusに参加しよう!: {{url}}", + "join-more-users": "{{user1}}、 {{user2}}、他{{left-count}}名を参加させる", + "join-new-private-chat": "新しいプライベート チャットを開始する", + "join-new-public-chat": "公開チャットに参加", + "join-one-user": "{{user}}を参加させる", + "join-open-community": "コミュニティに参加", + "join-request": "参加リクエスト", + "join-two-users": "{{user1}}と{{user2}}を参加させる", + "joined": "参加中", + "joined-community": "{{community}} に参加しました", + "joined-group-chat-description": "あなたは {{username}} の招待で {{group-name}} に参加しました", + "jul": "7月", + "jun": "6月", + "key": "鍵", + "key-managment": "鍵の管理", + "key-on-device": "秘密鍵はこのデバイスに保存されています。", + "keycard": "Keycard", + "keycard-access-reset": "Keycardがリセットされました", + "keycard-applet-install-instructions": "アプレットをインストールするには https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation の手順に従ってください。", + "keycard-awaiting-description": "お使いのスマートフォン(またはNFCリーダー)でカードを読み取ります。", + "keycard-awaiting-title": "探しています...", + "keycard-backup": "バックアップカードの作成", + "keycard-backup-success-body": "バックアップカードの作成に成功しました。メインカードと同じアカウントで使用することができます。", + "keycard-backup-success-title": "バックアップ成功", + "keycard-blocked": "Keycardがブロックされました。\n引き続き使用するにはカードをリセットする必要があります。", + "keycard-can-use-with-new-passcode": "新たにパスコードを設定してカードを使用できます", + "keycard-cancel-setup-text": "この操作はKeycardのセットアップをキャンセルします。Keycardを使用するにはセットアップを完了させることを強くお勧めします。本当にキャンセルしますか?", + "keycard-cancel-setup-title": "キャンセルしますか?", + "keycard-connected-description": "カードを動かさないでください", + "keycard-connected-title": "接続済み", + "keycard-desc": "キーカードをお持ちですか?そこにあなたの鍵を保管してください。トランザクション時に必要になります", + "keycard-dont-ask-card": "サインインにカードを要求しない", + "keycard-enter-new-passcode": "新しいパスコードを入力してください {{step}} \/ 2", + "keycard-error-description": "カードをもう一度接続して続行します", + "keycard-error-title": "接続が失敗しました", + "keycard-factory-reset": "カードを初期状態に戻す", + "keycard-factory-reset-text": "これを実行すると、カードに保存されているシードフレーズが削除されます。このKeycardで使用するシードフレーズのバックアップを保存したことを確認してください。", + "keycard-factory-reset-title": "本当に初期化しますか?", + "keycard-free-pairing-slots": "キーカードには{{n}}個の空きペアリングスロットがあります", + "keycard-has-multiaccount-on-it": "このカードは登録済みです。1つのカードには1つの鍵のみを保持できます。", + "keycard-init-description": "お使いのスマートフォン(またはNFCリーダー)をカードにかざしてください。", + "keycard-init-title": "カードを探しています...", + "keycard-is-blocked-details": "パスコード入力に複数回失敗したため、アカウントにアクセスできなくなりました。", + "keycard-is-blocked-instructions": "アカウントにアクセスするには、カードを初期化する必要があります。下のボタンをタップして手続きを開始してください。その際、シードフレーズが必要になります。", + "keycard-is-blocked-title": "キーカードがロックされました", + "keycard-is-frozen-details": "あなたの資産を保護するためカードがロックされました。カードをリセットすることで再度利用が可能になります。PUKまたはシードフレーズを用いて初期化してください。", + "keycard-is-frozen-factory-reset": "シードフレーズで初期化する", + "keycard-is-frozen-reset": "PUKでリセットする", + "keycard-is-frozen-title": "Keycardがロックされました", + "keycard-onboarding-finishing-header": "完了しています", + "keycard-onboarding-intro-header": "Keycardに鍵を保存する", + "keycard-onboarding-intro-text": "準備ができました。アカウントの安全性を確保するため以下のステップを進みます。", + "keycard-onboarding-mnemonic-text": "シードフレーズを書き留めるため紙とペンを用意してください。", + "keycard-onboarding-pairing-header": "カードをペアリング中", + "keycard-onboarding-pin-text": "Keycardへのアクセスを保護するため、6桁のパスコードを作成します。", + "keycard-onboarding-preparing-header": "カードを準備中", + "keycard-onboarding-puk-code-header": "コードを書き留めて、安全な場所で保管してください", + "keycard-onboarding-recovery-phrase-description": "シードフレーズを使ってこのアカウントを復元することができます。シードフレーズを書き留め、安全な場所で保管してください。", + "keycard-onboarding-recovery-phrase-header": "シードフレーズをバックアップ", + "keycard-onboarding-recovery-phrase-text": "これはあなたの鍵を生成するのに使われる魔法の種です。他の誰にも見られないようにしてください。", + "keycard-onboarding-start-header": "開始するにはカードをスマートフォンの背面に当てたままにしてください", + "keycard-onboarding-start-step1": "パスコードを作成", + "keycard-onboarding-start-step1-text": "鍵を暗号化する6桁のパスコードを作成します。このプロセスには約1分かかります。", + "keycard-onboarding-start-step2": "PUKコードとペアリングコードを書き留めてください", + "keycard-onboarding-start-step2-text": "紙とペンを用意してください。このプロセスには約1分かかります。", + "keycard-onboarding-start-step3": "シードフレーズのバックアップ", + "keycard-onboarding-start-step3-text": "紙とペンを用意してください。このプロセスには約1分かかります。", + "keycard-onboarding-start-text": "セットアップ中はカードとスマートフォンを接触させたままにしてください。このプロセスには4分前後かかります。", + "keycard-processing-description": "カードを動かさないでください", + "keycard-processing-title": "処理中...", + "keycard-recover": "カードを紛失、凍結していませんか?", + "keycard-recover-text": "シードフレーズを用いることで、このアカウントに関連付けた新しいKeycardを作成することができます。新しいKeycardを使うのも、凍結中のKeycardを初期化して使うこともできます。", + "keycard-recover-title": "このアカウントで新しいカードを作成しますか?", + "keycard-recovery-intro-button-text": "復元を開始", + "keycard-recovery-intro-header": "Keycardに保存された鍵を復元します", + "keycard-recovery-intro-text": "以前にKeycardを使用して鍵を生成し、その鍵をこのデバイスで使用したい場合は、次のステップに進んでください。", + "keycard-recovery-no-key-header": "復元できるものがありません", + "keycard-recovery-no-key-text": "このKeycardには鍵が保存されていません。使用するためには新しい鍵を生成してKeycardを鍵の保存先に選んでください", + "keycard-recovery-phrase-confirm-header": "シードフレーズを確認", + "keycard-recovery-phrase-confirmation-text": "最終確認です。Keycardの紛失等でアクセスできなくなった場合には、このシードフレーズを用いてキーへのアクセスを行います。他の誰かに見られないようシードフレーズを書き留めた後は安全な場所で保管してください。", + "keycard-recovery-phrase-confirmation-title": "シードフレーズを書き留めましたか?", + "keycard-recovery-success-header": "鍵の復元に成功しました", + "keycard-redeem-title": "受け取るアカウント", + "keycard-redeem-tx": "資産を受け取る", + "keycard-redeem-tx-desc": "カードをタップして資産を受け取ります", + "keycard-reset-passcode": "パスコードをリセット", + "keycard-success-description": "今すぐカードを削除できます", + "keycard-success-title": "成功", + "keycard-unauthorized-operation": "この操作を実行する権限がありません。 \n有効なカードをタップしてもう一度やり直してください。", + "keycard-upsell-subtitle": "さらなるセキュリティと使いやすさ", + "keypair-title": "{{name}}のデフォルトキーペア", + "keys-saved": "キーが保存されました!", + "kicked": "Kicked", + "language": "言語", + "last-backup-performed": "最後に実行されたバックアップ:", + "last-transaction": "最後のトランザクション", + "learn-more": "もっと詳しく知る", + "learn-more-about-keycard": "Keycardについてもっと知る", + "leave": "退出", + "leave-chat": "チャットを退出する", + "leave-chat-confirmation": "チャットの履歴がデバイスから削除されます。チャット再開時に履歴を復元することはできません。", + "leave-community": "コミュニティを退会する", + "leave-community-message": "お別れするのは寂しいですが、いつでも戻ってきてください!", + "leave-community?": "コミュニティから退出しますか?", + "leave-confirmation": "{{chat-name}}から退出する", + "leave-group": "グループを退出", + "leave-group?": "退出しますか?", + "left": "退出しました", + "left-community": "{{community}} を退出しました", + "les-ulc": "LES \/ ULC", + "lets-go": "始める", + "lifestyle": "ライフスタイル", + "light": "ライト", + "limit": "限度額", + "link-preview-loading-message": "プレビューを生成", + "link-to-community": "コミュニティへのリンク", + "link-to-profile": "プロフィールへのリンク", + "link-to-profile-copied": "プロフィールへのリンクがクリップボードにコピーされました", + "linked-on": "{{date}} にリンクされました", + "load-messages-before": "{{date}}より前", + "load-more-messages": "↓もっとメッセージを取得する", + "load-more-timeline": "さらに読み込む", + "loading": "読み込み中...", + "local-notifications": "ローカル通知", + "local-notifications-subtitle": "バックグラウンドサービスを有効にする", + "local-pairing-experimental-mode": "ローカルペアリングモード (アルファ版)", + "lock-app-with": "アプリをロックする", + "log-in": "ログイン", + "log-level": "ログレベル", + "log-level-settings": "ログレベル設定", + "logged-in": "ログインしました", + "logging": "ログ", + "logging-enabled": "ログが有効ですか?", + "login-pin-description": "6桁のパスコードを入力してロック解除します", + "logout": "ログアウト", + "logout-app-content": "アカウントをログアウトします。再度ロック解除する際、選択したネットワークが使われます。", + "logout-are-you-sure": "本当にログアウトしますか?", + "logout-key-management": "キー管理にアクセスするには、ログアウトする必要があります。", + "logout-title": "ログアウトしますか?", + "looking-for-cards": "カードを探しています...", + "lost-connection": "接続が切れました", + "low-tip": "チップが少なすぎます", + "lower-than-average-tip": "平均チップを下回っています", + "mail-should-be-configured": "メールクライアントを構成する必要があります", + "mailserver-address": "Statusノードアドレス", + "mailserver-automatic": "自動選択", + "mailserver-automatic-switch-explanation": "利用可能な最速のStatusノードを選択します", + "mailserver-connection-error": "Statusノードに接続できませんでした", + "mailserver-content": "メッセージをルーティングおよび保存するStatusネットワーク内のノード。それらの情報はノード内に最大30日間保存されます。", + "mailserver-details": "Statusノードの詳細", + "mailserver-error-content": "選択したStatusノードにアクセスできませんでした。", + "mailserver-error-title": "Statusノードへの接続エラー", + "mailserver-format": "enode:\/\/{enode-id}:{パスワード}@{ipアドレス}:{ポート}", + "mailserver-pick-another": "別のStatusノードを選択する", + "mailserver-reconnect": "Statusノードに接続できませんでした。タップして再接続してください", + "mailserver-request-error-content": "Statusノードから以下のエラーが返されました:{{error}}", + "mailserver-request-error-status": "履歴の取得中にエラーが発生しました。詳細についてはログを確認してください。", + "mailserver-request-error-title": "Statusノードリクエストエラー", + "mailserver-request-retry": "リクエストを再試行する", + "mailserver-retry": "再試行", + "mailserver-title": "Statusノード", + "main-account": "メインアカウント", + "main-currency": "メイン通貨", + "main-networks": "メインネット", + "main-wallet": "メインウォレット", + "mainnet-network": "メインネット", + "make-admin": "管理者にする", + "make-moderator": "モデレーターにする", + "make-one-it-is-easy-we-promise": "簡単に作ってみましょう!", + "manage-connections": "アプリケーションの接続を管理する", + "manage-keys-and-storage": "鍵とストレージを管理する", + "manage-members": "メンバーの管理", + "mar": "3月", + "mark-all-notifications-as-read": "すべての通知を既読にする", + "mark-all-read": "すべて既読にする", + "mark-as-read": "既読にする", + "mark-untrustworthy": "信頼できないとしてマークする", + "mark-user-untrustworthy": "{{username}}を信頼できないとマークする", + "master-account": "マスターアカウント", + "max-fee": "最大料金", + "max-fees": "最大料金", + "max-priority-fee": "最大優先料金", + "maximum-fee": "最大料金", + "maximum-fee-desc": "トランザクション全体の最大料金です。現在のブロックの基本料金がこれを超えた場合、取引は低い基本料金で次のブロックに含まれます。", + "may": "5月", + "maybe-later": "後にする", + "member-ban": "BANされたメンバー", + "member-kick": "メンバーをキックする", + "members": { + "other": "メンバー" + }, + "members-active": { + "other": "メンバー" + }, + "members-active-none": "メンバーがいません", + "members-count": "{{count}}メンバー", + "members-label": "メンバー", + "members-limit-reached": "メンバー数の上限に達しました", + "members-title": "メンバー", + "membership": "メンバーシップ", + "membership-approval": "承認を必要とする", + "membership-approval-description": "コミュニティには無料で参加できますが、新規メンバーはコミュニティ作成者の承認が必要です", + "membership-button": "メンバー参加条件", + "membership-declined": "グループへの参加が拒否されました", + "membership-description": "グループへの参加にはグループ管理者の承認が必要です", + "membership-ens": "ENSユーザ名が必要", + "membership-ens-description": "コミュニティに参加するには ENS ユーザ名が必要です", + "membership-free": "要件はありません", + "membership-free-description": "コミュニティに誰でも無料で参加できます", + "membership-invite": "他のメンバーからの招待が必要", + "membership-invite-description": "コミュニティは既存メンバーからの招待によってのみ参加できます。", + "membership-none": "なし", + "membership-none-placeholder": "新規メンバーの参加に参加条件を設定することができます。これはいつでも変更することができます。", + "membership-request-denied": "参加リクエストが却下されました", + "membership-request-pending": "参加リクエスト保留中", + "membership-requests": "参加リクエスト", + "membership-title": "メンバー参加条件", + "mention": "メンション", + "mentions": "メンション", + "message": "メッセージ", + "message-deleted": "メッセージを削除しました", + "message-deleted-for-everyone": "全員のメッセージを削除しました", + "message-deleted-for-everyone-count": { + "other": "全員のメッセージを削除しました" + }, + "message-deleted-for-you": "メッセージを削除しました", + "message-deleted-for-you-count": { + "other": "メッセージを削除しました" + }, + "message-not-sent": "メッセージが送信されていません", + "message-options-cancel": "キャンセル", + "message-reply": "返信", + "messages": "メッセージ", + "messages-from-contacts-only-subtitle": "連絡先に追加した人だけが、あなたと新しいチャットを開始したり、あなたをグループに招待できます", + "messages-gap-warning": "一部のメッセージが表示されない場合があります", + "might-break": "いくつかのDAppsが機能しない可能性があります", + "migration-successful": "移行完了", + "migration-successful-text": "Keycardへのアカウント移行が完了しました。", + "migrations-failed-content": "{{message}}\nスキーマバージョン:初期 {{initial-version}}、現在 {{current-version}}、最終 {{last-version}}\n\nデータベースエラーが発生しました。あなたの資産とチャットキーは安全です。チャットや連絡先などその他のデータは復元できません。{{erase-multiaccounts-data-button-text}}ボタンを押すと、その他のデータがすべて削除され、あなたの資産にアクセスしたり、メッセージを送信することができます。", + "miners-higher-fee": "手数料を高く設定することで、より早く取引が処理されます。", + "minimum-received": "最低受取額", + "mint": "ミント", + "mo": "月", + "mobile": "モバイル", + "mobile-network-ask-me": "モバイルネットワークを使用するときは尋ねる", + "mobile-network-continue-syncing": "同期を続ける", + "mobile-network-continue-syncing-details": "この設定は後で変更できます", + "mobile-network-go-to-settings": "設定に移動", + "mobile-network-settings": "モバイルデータ", + "mobile-network-sheet-configure": "同期設定の詳細はこちらで確認できます。", + "mobile-network-sheet-offline": "メッセージが同期できません", + "mobile-network-sheet-offline-details": "モバイルネットワークでの同期がオフになっています。", + "mobile-network-sheet-remember-choice": "この設定を保存する", + "mobile-network-sheet-settings": "設定", + "mobile-network-start-syncing": "同期を開始", + "mobile-network-stop-syncing": "同期を停止する", + "mobile-network-stop-syncing-details": "Wi-Fiに接続するまで停止しますか?", + "mobile-network-use-mobile": "モバイルデータを使用", + "mobile-network-use-mobile-data": "Statusはチャットとウォレットの同期に大量のデータ通信を使用します。", + "mobile-network-use-wifi": "Wi-Fiのみ", + "mobile-syncing-sheet-details": "Statusはチャットとウォレットの同期に大量のデータ通信を使用します。", + "mobile-syncing-sheet-title": "モバイルデータを使用して同期する", + "mon": "月", + "more": "もっとみる", + "move-and-reset": "移動してリセットする", + "move-keystore-file": "キーストアファイルを移動する", + "move-keystore-file-to-keycard": "キーストアファイルをKeycardに移しますか?", + "multiaccount-exists-content": "このアカウントのキーはすでに存在します。再度キーを追加することはできません。もしパスワード、パスコード、キーカードを紛失した場合は、アプリをアンインストールし、シードフレーズを入力してキーを再インストールしてアクセスしてください。", + "multiaccount-exists-title": "このアカウントのキーはすでに存在します。", + "multiaccounts-recover-enter-phrase-text": "12、15、18、21、または24単語。各単語はシングルスペースで区切ってください。", + "multiaccounts-recover-enter-phrase-title": "シードフレーズを入力", + "music": "音楽", + "mutal-contacts": "友だち", + "mute": "ミュート", + "mute-channel": "チャンネルをミュート", + "mute-chat": "チャットをミュート", + "mute-chat-capitialized": "チャットをミュート", + "mute-community": "コミュニティをミュート", + "mute-for-1-hour": "1時間", + "mute-for-1-week": "7日間", + "mute-for-15-mins": "15分間", + "mute-for-8-hours": "8時間", + "mute-group": "グループをミュート", + "mute-till-unmute": "電源を入れ直すまで", + "mutual-contact-requests": "友だちリクエスト", + "my-accounts": "アカウント", + "my-accounts-empty": "利用可能なアカウントがここに表示されます", + "my-albums": "マイアルバム", + "my-profile": "マイプロフィール", + "my-status": "マイステータス", + "n-photos": "{{count}}枚の写真", + "name": "名前", + "name-ens-or-address": "名前、ENS、またはアドレス", + "name-of-token": "トークンの名前", + "name-optional": "名前(オプション)", + "name-your-channel": "チャネルに名前を付ける", + "name-your-channel-placeholder": "チャネル名", + "name-your-community": "コミュニティ名", + "name-your-community-placeholder": "キャッチーな名前", + "need-help": "ヘルプが必要ですか?", + "negative": "ネガティブ", + "network": "ネットワーク", + "network-chain": "ネットワークチェーン", + "network-details": "ネットワーク詳細", + "network-fee": "ネットワーク手数料", + "network-id": "ネットワークID", + "network-info": "ネットワーク情報", + "network-invalid-network-id": "指定されたネットワークIDはRPC URLによるネットワークIDと一致していません", + "network-invalid-status-code": "無効なステータスコード:{{code}}", + "network-invalid-url": "ネットワークURLが無効です", + "network-settings": "ネットワーク設定", + "never": "閉じる", + "new": "New", + "new-category": "新規カテゴリー", + "new-chat": "新しいチャットを開始", + "new-community-title": "新規コミュニティ", + "new-contact": "新しい連絡先", + "new-contract": "新しいコントラクト", + "new-favourite": "新しいお気に入り", + "new-group": "新しいグループ", + "new-group-chat": "新しいグループチャット", + "new-messages-header": "新着メッセージ", + "new-network": "新しいネットワーク", + "new-password": "新しいパスワード", + "new-pin-description": "新しい6桁のパスコードを入力", + "new-public-group-chat": "公開チャットに参加", + "new-puk-description": "新しい12桁のPUKを入力", + "new-status": "新しいステータス", + "new-tab": "新しいタブ", + "new-to-status": "Statusは初めてです", + "new-ui": "新しいUI", + "next": "次へ", + "nickname": "ニックネーム", + "nickname-description": "ニックネームは、あなたが相手を識別しやすくするために使うものです。相手にはニックネームは知らされません。", + "nickname-visible-to-you": "ニックネームはあなただけに表示されます", + "no": "いいえ", + "no-activity": "活動なし", + "no-collectibles": "利用可能なコレクションはありません", + "no-collectibles-description": "Don't be a bored ape", + "no-communities": "コミュニティがありません", + "no-communities-description-strikethrough": "閉じる", + "no-dapps": "接続されているdAppsがありません", + "no-keycard-applet-on-card": "カードにKeycardアプレットがありません。", + "no-messages": "メッセージがありません", + "no-other-accounts": "他のアカウントはありません", + "no-pairing-slots-available": "このカードはすでに5台のデバイスとペアリング済みのため、ペアリングできません。いずれかのペアリング済みデバイスでログインし、カードのペアリングを解除してください", + "no-permissions": "権限がありません", + "no-pinned-messages": "ピン留めされたメッセージはありません", + "no-pinned-messages-community-desc": "ピン留めされたメッセージはありません", + "no-pinned-messages-desc": "ピン留めされたメッセージはありません", + "no-recent-transactions": "最近の取引はありません", + "no-result": "結果がありません", + "no-saved-addresses": "保存されたアドレスはありません", + "no-thanks": "結構です", + "no-tokens-found": "トークンが見つかりません", + "node-address": "ノードアドレス", + "node-details": "ノードの詳細", + "node-info": "ノード情報", + "node-version": "ノードバージョン", + "nodes-disabled": "Statusノードが無効", + "non-archival-node": "RPC エンドポイントはアーカイブリクエストをサポートしていません。ローカルの履歴が不完全な場合があります。", + "non-contacts": "連絡先なし", + "nonce": "Nonce", + "none": "なし", + "normal": "通常", + "not-a-chatkey": "これはチャットキーではありません", + "not-applicable": "未署名のトランザクションには適用されません", + "not-connected-nodes": "Statusノードに接続されていません", + "not-connected-to-peers": "どのピアにも接続されていません", + "not-enough-snt": "SNTが足りません", + "not-found": "Not found", + "not-keycard-text": "使用されたカードはKeycardではありません。Keycardを購入してください。", + "not-keycard-title": "Keycardではありません", + "not-registered": "未登録", + "notification-settings": "通知設定", + "notifications": "通知", + "notifications-marked-as-read": "{{count}}件の既読通知", + "notifications-non-contacts": "連絡先以外からの通知", + "notifications-preferences": "通知設定", + "notifications-servers": "通知サーバ", + "notifications-switch": "通知を表示する", + "notifications-transactions": "ウォレットのトランザクション", + "notify": "通知する", + "nov": "11月", + "now": "今", + "oct": "10月", + "off": "Off", + "off-status-tree": "Off Status tree", + "offline": "オフライン", + "offline-messaging-use-history-explanation": "Statusノードを有効にすることで、アプリを閉じている間に受信できなかったメッセージを読み込みに行くことができます。有効にすると、StatusノードはあなたのIPアドレスを取得します。無効にすると、アプリを閉じている間はメッセージを受信せず、後でアプリを開いたときにメッセージが表示されません。", + "offline-messaging-use-history-nodes": "Statusノードを使用する", + "ok": "OK", + "ok-continue": "続行する", + "ok-got-it": "わかりました", + "ok-save-pass": "パスワードを保存する", + "okay": "オーケー", + "on": "On", + "on-device": "On device", + "on-keycard": "On Keycard", + "on-status-tree": "On Status tree", + "once-enabled-share-metadata": "プレビューを有効にした場合、ウェブサイト保有者にあなたのメタデータが共有されます。", + "one-day": "1日", + "one-month": "1ヶ月", + "one-month-int": "1ヶ月", + "one-week": "1週間", + "one-week-int": "1週間", + "one-year": "1年", + "online": "オンライン", + "online-now": "オンラインです", + "only-6-images": "メッセージに追加できる画像は6枚までです", + "only-mentions": "@メンションのみ", + "oops-wrong-password": "おっと、パスワードが間違っています!", + "open": "開く", + "open-chat": "チャットを開く", + "open-dapp": "ÐAppを開く", + "open-dapp-store": "ÐAppsを見つける", + "open-home": "開く", + "open-in-new-tab": "新しいタブで開く", + "open-membership": "オープンメンバーシップ", + "open-nfc-settings": "NFC設定を開く", + "open-on-block-explorer": "ブロックエクスプローラで開く", + "open-status-on-your-other-device": "他のデバイスでStatusを開く", + "opened": "Opened", + "opening-buy-crypto": "{{site}}を開いています", + "optimal": "最適", + "optional": "オプション", + "or": "または", + "or-tap": "またはタップ", + "origin": "Origin", + "outgoing": "送信", + "outgoing-transaction": "送金", + "owner": "オーナー", + "page-camera-request-blocked": "カメラが無効になっています。設定でカメラを許可してください。", + "page-would-like-to-use-camera": "カメラの使用を許可しますか", + "pair": "ペア", + "pair-card": "このデバイスにペアリングする", + "pair-code": "ペアコード", + "pair-code-explanation": "カードを別のデバイス(最大5台)とペアリングし、同じKeycardでロック解除やトランザクションの署名に使うことができます。", + "pair-code-placeholder": "ペアリングコード...", + "pair-devices": "デバイスのペアリング", + "pair-this-card": "このカードをペアリングする", + "pair-this-device": "デバイスの同期", + "pair-this-device-description": "デバイスをペアリングして、デバイス間で連絡先とチャットを同期します", + "paired-devices": "ペアリング済みデバイス", + "pairing": "ペアリング", + "pairing-card": "カードをペアリング", + "pairing-changed": "ペアリングコードが変更されました", + "pairing-code-placeholder": "ペアリングコード...", + "pairing-code_error1": "ペアリングコードが一致しません。", + "pairing-go-to-installation": "ペアリング設定に進む", + "pairing-maximum-number-reached-content": "新しいデバイスを有効化する前に、デバイスの1つを無効にしてください。", + "pairing-maximum-number-reached-title": "最大デバイス数に達しました", + "pairing-new-installation-detected-content": "新しいデバイスが検出されました。 \nデバイスを正しく使用するためには、使用する前にペアリングして有効にすることが重要です。 \nデバイスをペアリングするには、設定画面のデバイス設定に移動してください。", + "pairing-new-installation-detected-title": "新しいデバイスが検出されました", + "pairing-no-info": "情報なし", + "pairing-please-set-a-name": "デバイスの名前を設定してください。", + "passphrase": "パスフレーズ", + "password": "パスワード", + "password-creation-confirm": "パスワードを再入力", + "password-creation-disclaimer": "パスワードが復旧できないことを理解しました", + "password-creation-dont-match": "パスワードが一致しません", + "password-creation-hint": "最低10文字", + "password-creation-match": "パスワードが一致しました", + "password-creation-placeholder-1": "パスワードを入力", + "password-creation-placeholder-2": "パスワードを再入力", + "password-creation-subtitle": "Statusにログインしてトランザクションに署名するには", + "password-creation-tips-1": "小文字", + "password-creation-tips-2": "大文字", + "password-creation-tips-3": "数字", + "password-creation-tips-4": "シンボル", + "password-creation-tips-title": "パスワードを改善するためのヒント", + "password-creation-title": "プロフィールパスワードの作成", + "password-description": "最低6文字です。Statusを開く時とトランザクションを確認する時にこのパスワードが必要になります。", + "password-mismatch": "再入力したパスワードと一致しません", + "password-placeholder": "パスワード...", + "password-placeholder2": "パスワードを確認", + "password-reset-in-progress": "パスワードの変更...", + "password-reset-success": "パスワードリセット完了", + "password-reset-success-message": "再度サインインしてください", + "password_error1": "パスワードが一致しません。", + "paste": "貼り付け", + "paste-json": "JSONを貼り付け", + "pay-to-chat": "チャットに支払う", + "peer-content": "あなたがお使いのデバイスはすべて同じStatusチャットネットワークに接続されます。1つのユーザ名を複数のデバイスで使うことができます。", + "peer-title": "デバイス", + "peer-to-peer": "ピアツーピア", + "peers": "デバイス", + "peers-count": "ピア数", + "peers-stats": "ピア統計", + "pending": "ペンディング", + "pending-confirmation": "承認待ち...", + "pending-invitations": "保留中のリクエスト", + "pending-requests": "保留中のリクエスト", + "per-gas-price-limit": "ガスあたりの手数料", + "per-gas-tip-limit": "ガスあたりのチップ金額", + "perform-backup": "バックアップを実行", + "permissions": "許可", + "phone-e164": "インターナショナル1", + "photo-caps": "写真", + "photo-saved": "写真がデバイスに保存されました", + "photos": "写真", + "photos-access-error": "写真にアクセスするには、システム設定から「Status → 写真」が有効になっているか確認してください。", + "pin": "ピン留め", + "pin-changed": "6桁のパスコードが変更されました", + "pin-code": "6桁のパスコード", + "pin-limit-reached": "ピン数の上限に達しました。先に前のメッセージのピンを外してください。", + "pin-mismatch": "パスコードが一致しません", + "pin-one-attempt": " 1回 ", + "pin-one-attempt-blocked-after": "間違えるとKeycardがロックされます。", + "pin-one-attempt-blocked-before": "注意してください、あと", + "pin-one-attempt-frozen-after": "間違えるとKeycardがロックされます。", + "pin-one-attempt-frozen-before": "注意してください、あと", + "pin-retries-left": "あと{{number}}回試行できます", + "pin-to-channel": "チャンネルにピン留めする", + "pin-to-chat": "チャットにピン留めする", + "pinned-a-message": "メッセージを固定しました", + "pinned-by": "Pinned by", + "pinned-messages": "ピン留めされたメッセージ", + "pinned-messages-2": "ピン留めされたメッセージ", + "pinned-messages-count": { + "other": "ピン留めされたメッセージ" + }, + "pinned-messages-empty": "ピン留めされたメッセージがここに表示されます。メッセージを固定するには、メッセージを長押しし「ピン」をタップします。", + "podcasts": "ポッドキャスト", + "positive": "ポジティブ", + "powered-by-paraswap": "Powered by Paraswap", + "preference": "外観モード", + "press": "プレス", + "preview-privacy": "プライバシーモードのプレビュー", + "previewing-may-share-metadata": "プレビューを有効にする場合、ウェブサイト所有者にあなたのメタデータが共有されます。", + "price-impact": "価格への影響", + "price-impact-desc": "この取引による推定価格影響額。現在のブロックの基本料金がこれを上回る場合、取引はより低い基本料金の次のブロックに含まれます。", + "principles": "原則", + "priority": "プライオリティ", + "privacy": "プライバシー", + "privacy-and-security": "プライバシーとセキュリティ", + "privacy-photos": "プロフィール写真のプライバシー", + "privacy-policy": "プライバシーポリシー", + "privacy-show-to-warning": "一度あなたのプロフィール写真を表示したことがある人には引き続き表示されます。", + "private-key": "秘密鍵", + "private-notifications": "プライベート通知", + "private-notifications-descr": "Statusは新着メッセージを通知します。これは後から通知設定で変更できます。", + "processing": "処理中", + "product-information": "プロダクト情報", + "profile": "プロフィール", + "profile-deleted-content": "あなたのプロフィールは正常に削除されました", + "profile-deleted-keycard": "別のキーペアをKeycardに復元することができます。", + "profile-deleted-title": "プロファイルが削除されました", + "profile-details": "プロフィールの詳細", + "profile-not-found": "プロフィールが見つかりません", + "profile-password": "プロフィールのパスワード", + "profile-pic-pick": "ギャラリーから選択", + "profile-pic-remove": "写真を削除する", + "profile-pic-take": "写真を撮る", + "profile-picture-updated": "プロフィール画像が更新されました", + "profiles-on-device": "デバイス上のプロフィール", + "public": "パブリック", + "public-channel": "公開チャネル", + "public-chat": "パブリックチャット", + "public-chat-description": "興味がある公開チャットに参加してみましょう。誰でも新しい公開チャットを作ることができます。", + "public-chats": "公開チャット", + "public-group-status": "パブリック", + "public-group-topic": "トピック", + "public-key": "公開鍵", + "puk-and-pairing-codes-displayed": "PUKとペアリングコードが表示されています", + "puk-changed": "12桁のPUKが変更されました", + "puk-code": "PUKコード", + "puk-code-explanation": "6桁のPINコードを忘れたり3回間違って入力した場合は、カードのアンロックにこのコードが必要になります", + "puk-mismatch": "PUKコードが一致しません", + "push-failed-transaction": "トランザクションが失敗しました", + "push-failed-transaction-body": "{{value}} {{currency}} を {{to}} へ", + "push-inbound-transaction": "{{value}} {{currency}} を受け取りました", + "push-inbound-transaction-body": "{{from}} から {{to}} へ", + "push-notifications-server-enabled": "サーバを有効にする", + "push-notifications-servers": "プッシュ通知サーバ", + "push-outbound-transaction": "{{value}} {{currency}} を送りました", + "push-outbound-transaction-body": "{{from}} から {{to}} へ", + "quiet-days": "{{quiet-days}}日", + "quiet-hours": "{{quiet-hours}}時間", + "re-encrypt-key": "鍵を再暗号化", + "read": "見る", + "read-more": "もっと読む", + "rearrange-categories": "カテゴリを並べ替える", + "receive": "受信", + "receive-transaction": "トランザクションを受信", + "received": "受信済み", + "recent": "最近", + "recent-empty": "最近使用したアドレスがここに表示されます", + "recent-recipients": "連絡先", + "recently-used-stickers": "最近使用したステッカーがここに表示されます", + "recipient": "受信者", + "recipient-code": "受信者アドレスを入力", + "recipient-code-placeholder": "0x..から始まるアドレスまたはENS名(username.domain.eth)", + "recover": "回復する", + "recover-key": "既存の鍵を復元する", + "recover-keycard-multiaccount-not-supported": "このアカウントの鍵はすでに存在します。再び鍵を追加することはできません。もしパスワード、パスコード、Keycardを紛失した場合は、アプリをアンインストールし、シードフレーズを入力して鍵を再インストールしてアクセスし直してください。", + "recover-with-keycard": "Keycardで復元する", + "recover-with-seed-phrase": "シードフレーズで復元する", + "recovering-key": "鍵を復元しています", + "recovery-confirm-phrase": "シードフレーズを確認", + "recovery-phrase": "シードフレーズ", + "recovery-success-text": "鍵を再暗号化するため、新しくパスコードを設定します。", + "recovery-typo-dialog-description": "シードフレーズは、書き留めた時とまったく同じ言葉、順番で使用してください。", + "recovery-typo-dialog-title": "シードフレーズは正しいですか?", + "redeem-amount": "{{quantity}}ボーナスを受け取り可能です", + "redeem-now": "今すぐ受け取る", + "redeem-success": "ボーナスを受け取りました!", + "reduced-tip": "優先チップが削減されます", + "refresh": "更新", + "registered": "登録済み", + "reject": "拒否", + "reject-and-delete": "拒否して削除する", + "remember-me": "ログインを記憶", + "remind-me-later": "後で知らせる", + "remote-notifications": "リモート通知", + "remote-notifications-subtitle": "プッシュ通知を有効にする", + "remove": "削除", + "remove-favourite": "お気に入りから削除する", + "remove-from-chat": "チャットから削除", + "remove-from-contacts": "連絡先から削除", + "remove-from-contacts-text": "連絡先リストからユーザを削除する場合にも、あなたのウォレットアドレスを非表示にすることはありません。", + "remove-group": "グループを削除", + "remove-network": "ネットワークを削除", + "remove-nickname": "ニックネームを削除", + "remove-nickname-toast": "{{secondary-name}}のニックネームを削除しました", + "remove-profile-confirm-message": "すべてのプロフィールデータがデバイスから削除されます。", + "remove-profile-message": "このデバイスからプロフィールを削除", + "remove-profile?": "プロフィールを削除しますか?", + "remove-token": "トークンを削除", + "remove-user-from-group": "{{username}}をグループから削除", + "removed": "削除されました", + "removed-from-contacts": "友だちから削除されました", + "rename": "名前を変更", + "repeat-pin": "新しいパスコードを再入力", + "repeat-puk": "新しい12桁のPUKを繰り返します", + "replied": "返信済み", + "replies": "返信", + "replying-to": "{{author}}に返信する", + "report-bug-email-template": "1.問題の説明\n{{description}}\n\n\n2.再現する手順\n{{steps}}\n\n\n3.問題を確認できるスクリーンショットを添付してください\n", + "request-access": "アクセスを要求する", + "request-feature": "機能をリクエスト", + "request-membership": "参加をリクエストする", + "request-pending": "リクエスト中...", + "request-to-join": "参加リクエストする", + "request-to-join-community": "コミュニティへの参加リクエスト", + "request-transaction": "トランザクションのリクエスト", + "requested-to-join-community": "{{community}} への参加をリクエストしました", + "requesting": "リクエスト中", + "required-field": "必須のフィールド", + "resend-message": "再送信", + "reset-card": "カードをリセットする", + "reset-card-description": "この操作はカードを初期状態にリセットします。秘密鍵を含むすべてのカードデータが消去されます。操作は元に戻せません。", + "reset-database": "データベースをリセットする", + "reset-database-warning": "チャット、連絡先、設定を削除します。パスワードを紛失した時に必要な処理です。", + "reset-database-warning-keycard": "チャット、連絡先、設定を削除します。", + "reset-password": "パスワードをリセットする", + "restore-defaults": "デフォルトに戻す", + "retake": "もう一度撮る", + "retry": "再試行", + "reveal-sync-code": "同期コードを表示する", + "revoke-access": "アクセス権を取り消す", + "rpc-url": "RPCのURL", + "rpc-usage-copy": "コピー", + "rpc-usage-filter": "フィルター方法", + "rpc-usage-filter-methods": "フィルター方法", + "rpc-usage-filtered-total": "{{filtered-total}} of {{total}}", + "rpc-usage-get-stats": "更新", + "rpc-usage-info": "RPCの使用状況の統計", + "rpc-usage-reset": "リセット", + "rpc-usage-total": "合計", + "sa": "土", + "safe-estimate": "安全な見積", + "sat": "土", + "save": "保存", + "save-image-library": "画像をライブラリに保存する", + "save-password": "パスワードを保存", + "save-password-unavailable": "パスワードを保存するためにデバイスパスコードを設定", + "save-password-unavailable-android": "パスワードを保存できません:お使いのデバイスがルート化されているか、必要なセキュリティ機能がありません。", + "saved": "保存しました", + "saving-keys-to-device": "キーをデバイスに保存しています...", + "say-hi": "挨拶しよう", + "scan-or-enter-a-sync-code": "同期コードをスキャンまたは入力します", + "scan-or-enter-sync-code": "同期コードをスキャンまたは入力する", + "scan-or-enter-sync-code-seen-on-this-device": "このデバイスに表示される同期コードをスキャンまたは入力してください", + "scan-qr": "QRコードをスキャン", + "scan-qr-code": "ウォレットアドレスのQRコードをスキャン", + "scan-sync-code": "同期コードをスキャン", + "scan-sync-qr-code": "QRコードをスキャン", + "scan-the-qr-code-or-copy-the-sync-code": "QRコードを読み取るか、同期コードをコピーしてください", + "scan-tokens": "トークンをスキャンする", + "search": "検索", + "search-contacts": "連絡先を検索", + "search-discover-communities": "コミュニティまたはカテゴリを検索", + "search-no-chat-found": "検索結果なし。もしかして", + "secret-keys-confirmation-text": "スマートフォンを紛失した場合でもKeycardを使い続けるためにこれが必要になります。", + "secret-keys-confirmation-title": "コードを書き留めましたか?", + "security": "セキュリティ", + "see-details": "詳細を見る", + "see-it-again": "もう一度見る", + "see-sticker-set": "ステッカーセットをすべて見る", + "see-suggestions": "推奨を見る", + "seed-key-uid-mismatch": "シードが一致しません", + "seed-key-uid-mismatch-desc-1": "入力したシードフレーズが{{multiaccount-name}}と一致しません。", + "seed-key-uid-mismatch-desc-2": "このアカウントの鍵を管理するには、シードフレーズを確認してからもう一度お試しください。", + "seed-phrase-content": "「シードフレーズ」は、他のウォレットやデバイスでイーサリアムアカウントを復旧またはアクセスするために使用されます。ほとんどの暗号アプリがこれと同じ仕組みを使用してアカウントを生成しています。このフレーズは「ニーモニックフレーズ」、「リカバリフレーズ」または「ウォレットバックアップ」とも呼ばれます。\n※BIP39標準リストからランダムに選択される英単語の組み合わせとして生成されます。", + "seed-phrase-title": "シードフレーズ", + "select": "選択", + "select-account": "アカウントを選択", + "select-account-dapp": "Dappsで使用するアカウントを選択します", + "select-account-first": "最初にアカウントを選択する", + "select-chat": "チャットを選択してメッセージを開始する", + "select-new-location-for-keys": "秘密鍵の格納先を選択します。", + "select-token-to-receive": "受け取るトークンを選択", + "select-token-to-swap": "スワップするトークンを選択", + "selected": "選択済み", + "selected-count-from-max": "{{selected}}\/{{max}}", + "send": "送信", + "send-contact-request-message": "チャットを開始するには友だち追加する必要があります", + "send-logs": "バグを報告", + "send-logs-to": "{{email}}にバグを報告", + "send-message": "メッセージを送信", + "send-push-notifications": "プッシュ通知を送る", + "send-push-notifications-description": "無効にすると、送信相手に新着メッセージが通知されなくなります。", + "send-reply": "返信する", + "send-request": "リクエストを送信", + "send-request-amount": "金額", + "send-request-amount-max-decimals": "小数点桁数は最大 {{asset-decimals}} 桁です", + "send-request-unknown-token": "不明なトークン - {{asset}}", + "send-sending-to": "{{recipient-name}}へ", + "send-to": "送信先", + "send-transaction": "トランザクションを送信", + "sending": "送信中", + "sent": "送信済み", + "sent-a-gif": "GIFを送る", + "sent-a-photo": "写真を送る", + "sent-a-sticker": "ステッカーを送る", + "sent-at": "に送信済み", + "sent-audio-message": "音声メッセージを送りました", + "sent-n-photos": "{{number}}枚の写真を送りました", + "sep": "9月", + "server": "サーバ", + "set-a-topic": "トピックを作成", + "set-currency": "通貨を設定する", + "set-custom-fee": "カスタム手数料を設定", + "set-dapp-access-permissions": "DApp アクセス許可", + "set-max": "最大を設定", + "set-nickname-toast": "{{primary-name}}を{{nickname}}に変更しました", + "settings": "設定", + "setup-group-chat": "グループチャットを設定する", + "setup-syncing": "同期のセットアップ", + "share": "シェア", + "share-address": "アドレスを共有", + "share-chat": "チャットを共有", + "share-community": "コミュニティを共有する", + "share-contact-code": "チャットキーを共有", + "share-dapp-text": "このDAppをチェックしてください {{link}}", + "share-image": "画像を共有する", + "share-invite-link": "招待リンクを共有する", + "share-link": "共有リンク", + "share-logs": "ログを共有", + "share-my-profile": "プロフィールを共有する", + "share-profile": "プロフィールを共有", + "share-profile-link": "プロフィールのリンクを共有する", + "share-public-chat-text": "Statusアプリのこの公開チャットをチェックしてみてください {{link}}", + "shared": "共有", + "shared-a-community": "コミュニティを共有する", + "sharing-copied-to-clipboard": "コピーしました", + "sharing-copy-to-clipboard": "コピー", + "sharing-data-desc-1": "データは公的なルールに照らし合わせて検証され、機密データが送信されないようになっています。 \"Don't trust, verify.\"", + "sharing-data-desc-2": "使用状況データはエンドツーエンド暗号化され、Statusのピアツーピアネットワークを介して送信されます。", + "sharing-data-desc-3": "通常のチャット キーの代わりに、使い捨てキーが用いられます。", + "sharing-data-desc-4": "使用状況データがIPアドレスと関連付けられることはありません。", + "sharing-data-desc-5": "すべてのユーザの累積データが公開されます", + "sharing-data-desc-6": "データは送信後にこの電話から削除されます", + "sharing-share": "共有", + "shell-placeholder-subtitle": "コミュニティ、メッセージ、\nウォレット アカウントとブラウザを開く", + "shell-placeholder-title": "アプリはここで実行されます", + "show-existing-keys": "既存のキーを表示", + "show-less": "少なく表示", + "show-more": "もっとみる", + "show-notifications": "通知を表示する", + "show-profile-pictures": "プロフィール写真の公開範囲", + "show-profile-pictures-to": "プロフィール写真の公開範囲", + "show-qr": "QRコードを表示", + "show-transaction-data": "トランザクション情報を表示する", + "sign-and-send": "署名して送信", + "sign-anyway": "署名する", + "sign-in": "サインイン", + "sign-in-by-syncing": "同期してサインインする", + "sign-message": "メッセージに署名", + "sign-out": "サインアウト", + "sign-request-failed": "メッセージに署名できませんでした", + "sign-up": "登録する", + "sign-with": "署名 ", + "sign-with-password": "パスワードで署名する", + "sign-you-in": "サインインしています…", + "signing": "署名中", + "signing-a-message": "メッセージに署名しています", + "signing-in-from-another-device": "他のデバイスからサインインする", + "signing-phrase": "署名フレーズ", + "simple": "シンプル", + "skip": "スキップ", + "slide-create": "スライドしてアカウントを作成", + "slippage": "スリッページ", + "slow": "Slow", + "something-went-wrong": "うまくいきませんでした", + "soon": "もうすぐ", + "sort-communities": "コミュニティを並べ替え", + "special-characters": "特殊文字", + "specify-address": "アドレスを入力", + "specify-name": "名前を入力", + "specify-network-id": "ネットワークIDを入力", + "specify-rpc-url": "RPCのURLを入力", + "specify-server-public-key": "サーバの公開鍵を入力してください", + "specify-symbol": "シンボルを入力", + "start-chat": "チャットを開始", + "start-conversation": "会話を始める", + "start-group-chat": "グループチャットを開始", + "start-new-chat": "新しいチャットを開始", + "start-using-status": "Statusを始める", + "starter-pack-coming": "スターターパックをお届けします", + "starter-pack-coming-description": "数分から数時間かかる場合があります", + "starter-pack-received": "スターターパックを受け取りました", + "starter-pack-received-description": "初回特典のトークンを受け取れます。トークンを使ってステッカーやENS名を購入してみよう。", + "status": "Status", + "status-always-online": "常にオンライン", + "status-automatic": "自動", + "status-automatic-subtitle": "ステータスの自動設定", + "status-confirmed": "承認済み", + "status-dnd": "退席中", + "status-dnd-subtitle": "すべての通知をミュートする", + "status-hardwallet": "Statusハードウォレット", + "status-inactive": "非表示", + "status-inactive-subtitle": "オンラインのステータスを非表示にする", + "status-is-open-source": "Statusはオープンソースです", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Statusはチャットの同期に大量のデータを使用します。モバイルネットワークに接続しているときは同期しないこともできます", + "status-not-sent-click": "承認されていません。オプションを表示", + "status-not-sent-tap": "送信エラー。タップしてオプションを見る", + "status-pending": "ペンディング", + "status-sent": "送信済み", + "status-tx-not-found": "トランザクションが見つかりません", + "status-updates-descr": "ステータスの更新情報がここに表示されます。連絡先から友達のプロフィールを追加して、タイムラインで更新情報を受け取りましょう。", + "statuses-descr": "今のあなたの気分を友達に共有しましょう。", + "statuses-my-status-descr": "いまの気分を共有しましょう。あなたのプロフィールを訪れた人は誰でもあなたのステータスを見ることができます。あなたを連絡先に追加した人は、あなたの更新情報を自分のタイムラインで受け取ります。", + "step-i-of-n": "{{step}}{{number}} ステップ", + "sticker": "ステッカー", + "sticker-market": "ステッカーマーケット", + "storage": "ストレージ", + "strength-divider-okay-label": "オーケー", + "strength-divider-strong-label": "強い", + "strength-divider-very-strong-label": "非常に強い", + "strength-divider-very-weak-label": "非常に弱い", + "strength-divider-weak-label": "弱い", + "strikethrough": "取り消し線", + "su": "日", + "submit": "送信", + "submit-bug": "バグを報告する", + "success": "成功", + "successful-connection": "接続成功", + "suggested-min-tip": "推奨される最小チップ", + "suggested-price-limit": "推奨金額", + "sun": "日", + "swap": "スワップ", + "swap-details": "スワップ内容", + "switch-to-simple-interface": "シンプルなインターフェースに切替", + "symbol": "シンボル", + "sync-all-devices": "すべてのデバイスを同期", + "sync-code": "同期コード", + "sync-code-generated": "同期コードが生成されました", + "sync-devices-complete-sub-title": "お使いのデバイスが同期されました", + "sync-devices-complete-title": "デバイスの同期が完了しました!", + "sync-devices-error-title": "おっと、何かが間違っています", + "sync-devices-result-sub-title": "お使いのデバイスが同期されました", + "sync-devices-sub-title": "同期が完了するまで、両方のデバイスの電源を入れ、インターネットに接続したままにしてください。", + "sync-devices-title": "デバイスを同期中...", + "sync-in-progress": "同期しています...", + "sync-settings": "同期設定", + "sync-synced": "同期中", + "synced-devices": "同期済みデバイス", + "synced-with": "同期済み", + "synchronise-your-data-across-your-devices": "デバイス間でデータを同期する", + "syncing": "同期中", + "syncing-devices": "同期しています...", + "system": "自動", + "tabs": "タブ", + "tag-was-lost": "タグが失われました", + "tap": "タップ", + "tap-card-again": "もう一度カードをタップしてください", + "tap-on": "タップする", + "terms-of-service": "利用規約", + "test-networks": "テストネット", + "text-input-disabled": "しばらくお待ちください...", + "th": "木", + "thank-you": "Thank you", + "this-device": "このデバイス", + "this-device-desc": "あなたの鍵は暗号化され安全に保管されます", + "this-is-you-signing": "これはあなたの署名フレーズです", + "this-message-was-deleted": "このメッセージは削除されました", + "this-will-take-few-seconds": "このプロセスには数秒かかります", + "three-days": "3日", + "three-months-int": "3ヶ月", + "three-words-description": "トランザクションに署名を行う際、必ずこの3つの単語が表示されます。", + "three-words-description-2": "異なる組み合わせが表示された場合は、処理を中断してログアウトしてください。", + "thu": "木", + "timeline": "タイムライン", + "tip-cap": "チップ上限", + "to": "To", + "to-block": "ブロック", + "to-capitalized": "To", + "to-enable-biometric": "{{bio-type-label}}を有効にするには、ロック解除画面でパスワードを保存する必要があります", + "to-encrypt-enter-password": "アカウントを暗号化するには、パスワードを入力してください", + "to-scan-a-qr-enable-your-camera": "QRを読み取るにはカメラを有効にしてください", + "to-see-this-message": "このメッセージを見るには", + "token": "トークン", + "token-auto-validate-decimals-error": "{{address}} のトークン {{symbol}} の小数点桁数が間違っています。{{expected}} と設定されていますが {{actual}} と検出されました", + "token-auto-validate-name-error": "{{address}} のトークン {{symbol}} の名前が間違っています。{{expected}} と設定されていますが {{actual}} と検出されました", + "token-auto-validate-symbol-error": "{{address}} のトークン {{symbol}} のシンボルが間違っています。{{expected}} と設定されていますが {{actual}} と検出されました", + "token-details": "トークンの詳細", + "token-gated-communities": "トークンゲート型コミュニティ", + "token-gated-communities-info": "このトピックに関連するものを紹介します。これにより、ユーザはより多くのコンテキストを取得し、より深く理解できるようになります。", + "token-price": "トークン価格", + "topic-name-error": "小文字 (aからz)、数字とダッシュ (-) のみを使用できます。連絡先コードは使用しないでください。", + "total-gas": "ガス合計", + "total-members": "総メンバー数", + "transaction": "トランザクション", + "transaction-data": "トランザクション情報", + "transaction-declined": "トランザクションが失敗しました", + "transaction-description": "ネットワーク上で 12 承認を得た後に完了となります", + "transaction-details": "トランザクションの詳細", + "transaction-failed": "トランザクションが失敗しました", + "transaction-fee": "トランザクション手数料", + "transaction-history": "トランザクション履歴", + "transaction-request": "トランザクションリクエスト", + "transaction-sent": "送信されたトランザクション", + "transaction-signed": "トランザクションは正常に署名されました", + "transactions": "トランザクション", + "transactions-filter-select-all": "すべて選択", + "transactions-filter-title": "履歴をフィルタする", + "transactions-history": "トランザクション履歴", + "transactions-history-empty": "トランザクション履歴がまだありません", + "transactions-history-loading": "トランザクション履歴を読み込んでいます。しばらく時間がかかる場合があります。", + "transactions-load-more": "さらに読み込む", + "transactions-management-enabled": "トランザクション管理(アルファ版)", + "transactions-sign": "署名", + "transfer-ma-unknown-error-desc-1": "マルチアカウントが削除されていないようです。データベースがリセットされた可能性があります。", + "transfer-ma-unknown-error-desc-2": "アカウント一覧をご確認の上、再度お試しください。アカウントが表示されていない場合は、シードフレーズを使用して既存の鍵で復元してください。", + "transfers-fetching-failure": "送信履歴を更新できませんでした。通信状況を確認し、プルダウンして再試行してください", + "tribute-required-by-multiaccount": "{{multiaccount-name}} はチャットを開始するのにSNTを要求しています。", + "tribute-state-paid": "Tributeが支払われました", + "tribute-state-pending": "Tributeが待機中です", + "tribute-state-required": "{{snt-amount}} SNTのTributeが必要です", + "tribute-to-talk": "Tribute to Talk", + "tribute-to-talk-add-friends": "友達を連絡先として登録すればTributeを送ることなくチャットを開始できます。", + "tribute-to-talk-are-you-friends": "友達ですか?", + "tribute-to-talk-ask-to-be-added": "連絡先として追加するよう依頼する", + "tribute-to-talk-contact-received-your-tribute": "Tributeを受け取り、セキュアにチャットを行えるようになりました。", + "tribute-to-talk-desc": "新しくチャットを始める人にSNTを要求すれば人からの注目を収益に変えることができます", + "tribute-to-talk-disabled": "Tribute to Talkが無効になっています", + "tribute-to-talk-disabled-note": "これからは新しくチャットを始める人はSNTを送信しなくてもあなたとチャットを始めることができます。", + "tribute-to-talk-enabled": "Tribute to Talkを有効にしました。", + "tribute-to-talk-finish-desc": "今後は連絡先からのチャットと支払いを行ったユーザからのチャットのみを受信します", + "tribute-to-talk-learn-more-1": "あなたの時間とあなたに集まる注目は価値のある資産です。Tribute to Talkはあなたと新しくチャットを開始するときに指定した額のSNTを要求することができます。", + "tribute-to-talk-learn-more-2": "連絡先リストに登録されていない人は支払いを要求され、支払いを行った人にのみ返信することができます。", + "tribute-to-talk-learn-more-3": "いつでも返金することもできます。連絡先リストに登録されている友達はいつでも無料であなたに連絡できます。", + "tribute-to-talk-paywall-learn-more-1": "時間と自分に集まる注目は価値ある資産です。Trubute to Talkはあなたとのチャットの対価としてSNTの支払いを受け付けることができます。", + "tribute-to-talk-paywall-learn-more-2": "トリビュート設定した人とチャットを開始する、連絡先に追加してもらうには、決められたSNTを支払う必要があります。", + "tribute-to-talk-paywall-learn-more-3": "知っている人にはStatusの外部でプロフィールを共有して無料で追加してもらうことができます。", + "tribute-to-talk-pending": "Tributeの承認待ち", + "tribute-to-talk-pending-note": "Tributeトランザクションはネットワークによる承認待ちです。トランザクションヒストリーで状態を確認することができます。", + "tribute-to-talk-removing-note": "Tribute to Talkを削除するとSNTを送らずにあなたとチャットを開始できるようになります。これにはトランザクションが発生します。", + "tribute-to-talk-set-snt-amount": "新しくチャットを始める人に要求するSNTの額を設定してください", + "tribute-to-talk-signing": "トランザクションへの署名を待っています", + "tribute-to-talk-transaction-failed-note": "トランザクションが失敗し、Tribute to Talkの設定の変更は保存されませんでした", + "tribute-to-talk-tribute-received1": "Tributeを受け取りました。", + "tribute-to-talk-tribute-received2": " は連絡先に追加され、セキュアにチャットできます。", + "tribute-to-talk-you-require-snt": "新しくチャットを始める人にはSNTを要求します。", + "try-again": "再試行する", + "try-keeping-the-card-still": "カードを動かさないでください", + "try-your-luck-again": "もう一度運試しをしてください!", + "tu": "火", + "tue": "火", + "turn-nfc-description": "NFCが無効になっています。設定を確認してください。", + "turn-nfc-on": "続行するにはNFCをオンにしてください", + "two-minutes": "2分", + "tx-fail-description1": "この処理は失敗する可能性があります。カスタム手数料を確認してください。", + "tx-fail-description2": "この処理は失敗する可能性があります。カスタム手数料を確認してください。", + "type": "タイプ", + "type-a-message": "メッセージ...", + "type-nickname": "ニックネームを入力", + "type-something": "何かを入力", + "type-your-password": "パスワードを入力してください", + "ulc-enabled": "ULC が有効", + "unable-to-fetch": "チャット履歴を取得できません", + "unable-to-read-this-code": "このコードを読み込めません", + "unable-to-send-messages": "メッセージを送受信できません", + "unblock": "ブロック解除", + "unblock-contact": "ブロックを解除する", + "undo": "元に戻す", + "unique-identifiers": "一意のプロファイル識別子", + "unknown-status-go-error": "不明なstatus-goのエラー", + "unlimited": "無制限", + "unlock": "ロック解除", + "unmute": "ミュート解除", + "unmute-chat": "チャットをミュート解除", + "unmute-community": "コミュニティのミュート解除", + "unmute-group": "グループをミュート解除", + "unpair-card": "カードのペアリングを解除", + "unpair-card-confirmation": "この操作は現在のデバイスからカードのペアリングを解除します。 6桁のパスコードが必要です。続行しますか?", + "unpair-keycard": "このスマートフォンとKeycardのペアリングを解除する", + "unpair-keycard-warning": "ペアリングコード、PUK、PIN に変更はありません。", + "unpaired-keycard-text": "タップしたKeycardはこのスマートフォンと紐づいていません", + "unpaired-keycard-title": "カードのペアリングが解除されたようです", + "unpin": "ピン留めを解除", + "unpin-from-channel": "チャンネルからピン留め解除", + "unpin-from-chat": "チャットからピン留め解除", + "unread": "未読", + "untrustworthy": "信用できない", + "update": "更新", + "update-to-listen-audio": "最新バージョンに更新して、音声メッセージを受け取ろう", + "update-to-see-image": "最新バージョンに更新して、素敵な写真を受け取ろう", + "update-to-see-sticker": "最新バージョンに更新して、新しいステッカーを見つけよう", + "updates-to-tos": "利用規約の更新", + "updates-to-tos-desc": "続行する前に利用規約を確認し、アプリの使用方法について全責任を負うことを確認してください。", + "url": "URL", + "usd-currency": "米ドル", + "use-as-profile-picture": "プロフィール画像として使う", + "use-biometrics": "生体認証を使用してパスワードを入力する", + "use-keycard": "Keycardを使用する", + "use-keycard-subtitle": "鍵はKeycardに保存されます", + "use-photo": "写真を使用する", + "use-recovery-phrase": "リカバリーフレーズを使う", + "use-recovery-phrase-subtitle": "すでにイーサリアムアドレスをお持ちの場合", + "use-valid-contact-code": "有効なチャットキーまたはユーザ名を入力してください", + "user-deleted-a-message": "{{user}}はメッセージを削除しました", + "user-found": "ユーザが見つかりました", + "user-keycard": "{{name}}カード", + "user-keypair": "{{name}}のキーペア", + "user-pinned-a-message": "{{user}}がメッセージを固定しました", + "user-replied": "{{user}}が返信しました", + "user-sent-a-gif": "{{user}}がGIFを送りました", + "user-sent-a-photo": "{{user}}が写真を送りました", + "user-sent-a-sticker": "{{user}}がステッカーを送りました", + "user-sent-audio-message": "{{user}}が音声メッセージを送りました", + "user-sent-n-photos": "{{user}}が{{number}}枚の写真を送りました", + "user-shared-a-community": "{{user}}がコミュニティを共有しました", + "valid-for-time": "有効期限:{{valid-for}}", + "validation-amount-invalid-number": "金額が正しくありません", + "validation-amount-is-too-precise": "金額が細かすぎます。小数点以下を {{decimals}} 桁以下にしてください。", + "verified-community": "✓ 検証済みコミュニティ", + "version": "アプリバージョン", + "via": "経由", + "view": "表示する", + "view-community-rules": "コミュニティルールを表示", + "view-cryptokitties": "CryptoKittiesで表示", + "view-cryptostrikers": "CryptoStrikersで表示", + "view-data": "データを見る", + "view-details": "詳細を表示", + "view-etheremon": "Etheremonで表示", + "view-gitcoin": "Gitcoinで表示", + "view-members": "メンバーを表示", + "view-on-opensea": "OpenSeaで見る", + "view-pinned-messages": "ピン留めされたメッセージを表示する", + "view-profile": "プロフィールを見る", + "view-public-dashboard": "公開ダッシュボードを表示", + "view-rules": "ルールを見る", + "view-signing": "署名フレーズを表示", + "view-superrare": "SuperRareで表示", + "vote-to-feature": "このコミュニティの特集に投票する", + "waiting-for-wifi": "Wi-Fiがなくメッセージ同期が停止しています", + "waiting-for-wifi-change": "設定", + "waiting-to-sign": "トランザクションへの署名を待っています...", + "waiting-wi-fi": "Wi-Fiを探しています…", + "waku-bloom-filter-mode": "WAKUブルームフィルタモード", + "wakuv2-change-nodes": "本当にWaku v2ノードを変更しますか?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "Waku v2設定", + "wallet": "ウォレット", + "wallet-address": "ウォレットアドレス", + "wallet-asset": "アセット", + "wallet-assets": "アセット", + "wallet-backup-recovery-title": "シードフレーズのバックアップ", + "wallet-choose-recipient": "受信者を選択", + "wallet-collectibles": "コレクション", + "wallet-connect": "Wallet Connect", + "wallet-connect-2.0": "Wallet Connect 2.0", + "wallet-connect-app-connected": "接続済み", + "wallet-connect-go-back": "ブラウザまたはdappに戻る", + "wallet-connect-proposal-description": "接続すると{{name}}があなたのアドレスを取得してWeb3を有効にすることを許可します", + "wallet-connect-proposal-title": "ウォレットを接続しますか", + "wallet-insufficient-funds": "残高が足りません", + "wallet-insufficient-gas": "Gasに必要なETHが足りません", + "wallet-invalid-address": "無効なアドレス: \n {{data}}", + "wallet-invalid-address-checksum": "アドレスにエラーがあります\n{{data}}", + "wallet-invalid-chain-id": "ネットワークが一致しません: \n 現在は{{chain}}、{{data}}に接続してください", + "wallet-key-content": "あなたが相手からアセットを受け取るとき、64桁の文字列からなる0xで始まるイーサリアム標準アドレスが相手に共有されます。このアドレスは「イーサリアムアドレス」または「ウォレットアドレス」とも呼ばれます。", + "wallet-key-title": "アカウントアドレス", + "wallet-manage-accounts": "アカウント管理", + "wallet-manage-app-connections": "接続を管理する", + "wallet-manage-assets": "アセットを管理する", + "wallet-request": "リクエスト", + "wallet-send": "送信", + "wallet-send-min-units": "21000以上", + "wallet-send-min-wei": "最低1wei", + "wallet-settings": "ウォレット設定", + "wallet-total-value": "総額", + "wallet-transaction-total-fee": "合計手数料", + "wants-to-access-profile": "あなたのプロフィールにアクセスしようとしています", + "wants-to-join": "参加したい", + "warning": "警告", + "warning-message": "スパム防止のため、複数のメッセージを連続して素早く送信することを制限しています。もう一度やり直してください。", + "warning-sending-to-contract-descr": "入力したアドレスはスマートコントラクトアドレスです。このアドレスに資産を送ると、資産が失われる可能性があります。DAppで操作するには、Status DAppブラウザでDAppを開きます。", + "watch-only": "読み取り専用", + "wc-brand-guide": "商標やロゴなどのブランドの使用に関するガイダンス", + "wc-disclaimer": "免責事項(サードパーティプロバイダを含む)、保証、法的リリース", + "wc-dispute": "紛争解決規定", + "wc-how-to-use-status-app": "プライバシーとセキュリティを含むStatusアプリの使い方", + "wc-new-tos-based-on-principles-prefix": "新しい利用規約は原則に基づいて設計されています", + "we": "水", + "web-view-error": "ページを読み込めません", + "websites": "ウェブサイト", + "webview-camera-permission-requests": "ウェブカメラの許可リクエスト", + "webview-camera-permission-requests-subtitle": "有効にすると、ウェブサイトやDappsがカメラの使用を要求できます。", + "wed": "水", + "welcome-back": "おかえりなさい!", + "welcome-blank-community-message": "あなたのコミュニティがここに表示されます。", + "welcome-blank-message": "進行中のチャットがここに表示されます。新しいチャットを開始するには、⊕ボタンを押してください。", + "welcome-community-blank-message": "チャンネルがここに表示されます。新しいチャンネルを作成するには、⊕ボタンをクリックして「チャンネルを作成」を選択します", + "welcome-community-blank-message-edit-chats": "参加中のチャンネルがここに表示されます。新しいチャンネルを作成するには、コミュニティ画面に戻って⊕ボタンをクリックし \"チャンネルを作成\" を選択します。", + "welcome-screen-text": "ウォレットをセットアップし、友達を招待して人気のDAppsをブラウズしましょう!", + "welcome-to-status": "Statusへようこそ", + "welcome-to-status-description": "ウォレットをセットアップして、友達とのチャットや分散型アプリを始めましょう", + "welcome-to-web3": "web3へようこそ!", + "welcome-to-web3-sub-title": "ためらうことはない。探検に行きましょう!", + "what-changed": "変更点", + "what-is-shared": "共有されるもの", + "whats-on-your-mind": "いまどうしてる?", + "who-are-you-looking-for": "誰をお探しですか?", + "word-count": "単語数", + "word-n": "{{number}}番目の単語", + "word-n-description": "リカバリーフレーズを正しくバックアップできたか確認するために、{{number}}番目の単語を入力してください。", + "words-n": { + "other": "単語" + }, + "write-down-and-store-securely": "コードを書き留めて安全に保管してください", + "wrong-address": "アドレスが間違っています", + "wrong-card": "カードが間違っています", + "wrong-card-text": "タップしたカードは、あなたが選択した鍵と一致しません。", + "wrong-contract": "コントラクトが間違っています", + "wrong-keycard-text": "タップしたKeycardはこのスマートフォンと紐づいていません", + "wrong-keycard-title": "間違ったKeycardをタップしたようです", + "wrong-password": "パスワードが間違っています", + "wrong-word": "単語が間違っています", + "x-counter": "x {{counter}}", + "yes": "はい", + "you": "あなた", + "you-already-have-an-asset": "すでに{{value}}を持っています", + "you-are-all-set": "設定完了です", + "you-are-all-set-description": "これでスマートフォンを紛失してもリカバリーフレーズを使ってアカウントを復元できます。", + "you-can-change-account": "アカウント名と色を変更することができます", + "you-can-choose-preview-websites": "サイトごとにリンクのプレビューを有効にするかどうかを選択できます。", + "you-can-fetch": "チャット履歴を取得できます", + "you-canceled-the-request": "参加リクエストをキャンセルしました", + "you-colon": "あなた:", + "you-deleted-a-message": "メッセージを削除しました", + "you-dont-have-contacts": "まだ連絡先がありません。", + "you-dont-have-contacts-invite-friends": "まだ連絡先が登録されていません。\n友達を招待してチャットを始めましょう。", + "you-dont-have-stickers": "まだステッカーを持っていません", + "you-eligible-to-join": "参加資格があります", + "you-have-no-contacts": "連絡先がありません", + "you-hold-number-of-hold-tokens-of-these": "次のうち{{number-of-hold-tokens}}を保有しています:", + "you-like-to-type-43-characters": "43文字を入力したいですか?", + "you-must-hold": "保有条件:", + "you-not-eligible-to-join": "参加資格がありません", + "you-pinned-a-message": "メッセージを固定しました", + "you-replied": "返信しました", + "you-sent-a-gif": "GIFを送りました", + "you-sent-a-photo": "写真を送りました", + "you-sent-a-sticker": "ステッカーを送りました", + "you-sent-audio-message": "音声メッセージを送りました", + "you-sent-n-photos": "{{number}}枚の写真を送りました", + "you-shared-a-community": "コミュニティを共有しました", + "you-will-need-this-code": "Statusアプリを開いたり、トランザクションに署名する際にこのコードが必要になります。", + "you-will-start-from-scratch": "新しい鍵のセットを使って最初から始める", + "your-answer": "あなたの答え", + "your-card-is-frozen": "Keycardがロックされました。カードをリセットして", + "your-contact-code": "アクセスを許可すると、このDAppはあなたのチャットキーを取得します。", + "your-data-belongs-to-you": "シードフレーズを紛失するとあなたの資産とデータが失われます", + "your-data-belongs-to-you-description": "スマートフォンの紛失等でアクセスできなくなった場合には、このシードフレーズを用いて鍵へのアクセスを行います。他の誰かに見られないようシードフレーズを書き留めた後は安全な場所で保管してください。", + "your-identifiers": "IDリング、チャットキー、エモジハッシュで、なりすましを見分けることができます", + "your-keys": "あなたの鍵", + "your-price-limit": "料金設定値", + "your-recovery-phrase": "シードフレーズ", + "your-recovery-phrase-description": "これはあなたのシードフレーズです。ウォレットがあなたのものであることを証明するために使用します。一度だけしか見ることができません。紙に書くか安全な場所で保管してください。紛失やアプリを再インストールする際に必要になります。", + "your-tip-limit": "チップ設定値", + "youre-on-mobile-network": "モバイルネットワークを利用しています" +} diff --git a/translations/ko.json b/translations/ko.json new file mode 100644 index 00000000000..b19428bbe49 --- /dev/null +++ b/translations/ko.json @@ -0,0 +1,1721 @@ +{ + "You": "사용자", + "about-app": "앱 정보", + "about-key-storage-content": "스테이터스는 절대로 사용자의 개인 키에 접근할 수 없습니다. 시드 구문을 안전하게 백업해주세요. 스마트폰을 잃어버려도 시드 구문을 통해 계정을 불러올 수 있습니다.", + "about-key-storage-title": "키 저장소 정보", + "about-names-content": "그 누구도 사용자를 사칭할 수 없습니다. 사용자 계정은 기본적으로 익명이며, 사용자는 실제 이름을 밝힐 필요가 없습니다. 약간의 비용을 지불하면 원하는 이름을 등록할 수 있습니다.", + "about-names-title": "이름은 변경할 수 없습니다", + "about-sharing-data": "데이터 공유 정보", + "accept": "수락", + "accept-and-add": "수락 및 추가", + "accept-and-continue": "동의하고 계속", + "accept-and-share-address": "주소 수락 및 공유", + "accept-new-chats-from": "다음의 새 채팅 수락", + "accept-status-tos-prefix": "스테이터스에 동의합니다", + "access-existing-keys": "기존 키 액세스", + "access-key": "키 불러오기", + "account-added": "계정 추가됨", + "account-color": "계정 색상", + "account-content": "스테이터스 계정을 은행 계좌처럼 생각하셔도 좋습니다. 스테이터스 계정은 은행 계좌처럼 주소와 잔액을 가지고 있습니다. 여러분은 이 계정으로 이더리움 상에서 트랜잭션(Transaction, 거래)를 수행할 수 있습니다. 또한 하나의 지갑에 다수의 계정을 둘 수 있는데, 하나의 스테이터스 계정에 접속해 연계된 지갑에 모두에 접근할 수 있습니다.", + "account-exists-title": "계정이 이미 존재합니다", + "account-is-used": "계정은 브라우저에서 디앱과 함께 사용됩니다.", + "account-name": "계정 이름", + "account-settings": "계정 설정", + "account-title": "계정", + "accounts": "계정", + "actions": "작업", + "active-online": "온라인", + "active-unknown": "알 수 없음", + "activity": "활동", + "add": "추가", + "add-a-watch-account": "열람 전용 계정 추가", + "add-account": "계정 추가", + "add-account-description": "모든 유형의 이더리움 계정을 스테이터스 지갑에 불러올 수 있습니다", + "add-account-incorrect-password": "비밀번호가 올바르지 않습니다. 앱 잠금을 해제하는데 사용하는 비밀번호를 입력해주세요.", + "add-an-account": "계정 추가", + "add-bootnode": "부트노드 추가", + "add-contact": "연락처 추가", + "add-custom-token": "커스텀 토큰 추가", + "add-favourite": "즐겨찾기 추가", + "add-mailserver": "스테이터스 노드 추가", + "add-members": "멤버 추가", + "add-network": "네트워크 추가", + "add-new-contact": "새 연락처 추가", + "add-nickname": "닉네임 추가 (선택)", + "add-node": "노드 추가", + "add-private-key-account": "개인 키로 계정 추가", + "add-seed-account": "시드 구문으로 계정 추가", + "add-to-contacts": "연락처에 추가", + "add-to-contacts-text": "특정 사용자를 연락처에 추가하면, 사용자의 지갑 주소를 공유하게 됩니다", + "add-to-favourites": "즐겨찾기에 추가", + "add-watch-account": "열람 전용 계정 추가", + "address": "주소", + "address-or-ens-name": "주소 또는 ENS 이름", + "address-received": "주소 받음", + "address-request-accepted": "주소 요청 수락됨", + "address-requested": "주소 요청됨", + "advanced": "고급 설정", + "advanced-settings": "고급 설정", + "advertiser-description": "친구의 초대로 스테이터스에 입문하셨습니다. 스테이터스가 귀하의 IP를 한 번만 확인하여 보상을 진행해도 괜찮을까요? 이 정보는 다른 용도로 절대 사용되지 않으며, 7일 후에 완전히 삭제됩니다.", + "advertiser-starter-pack-accept": "수락", + "advertiser-starter-pack-decline": "거절", + "advertiser-starter-pack-description": "스테이터스를 시작하는 데 도움이 될 암호화폐입니다! SNT를 활용하여 이모티콘을 구입하고, ENS 이름을 등록하거나 디앱을 사용해보세요", + "advertiser-starter-pack-title": "스타터 팩", + "advertiser-title": "개인 정보 보호", + "agree-by-continuing": "계속하면 아래 약관에 동의하는 것으로 간주됩니다.\n 스테이터스 ", + "all": "전체", + "allow": "허용", + "allow-and-send": "허용 및 보내기", + "allow-mention-notifications": "@ 멘션 표시", + "allowing-authorizes-this-dapp": "웹3를 활용하기 위해, 디앱이 사용자의 지갑 주소를 검색하는 것을 허용합니다", + "already-have-asset": "이미 해당 자산을 가지고 있습니다", + "amount": "수량", + "anyone": "누구든지", + "app-commit": "앱 커밋", + "appearance": "테마", + "apply": "적용", + "approve": "승인", + "approve-limit": "한도 승인", + "approve-token": "토큰 승인", + "approve-token-contract-desc": "컨트랙트로 토큰을 승인하면 토큰 잔액을 사용할 수 있게 됩니다. 프로젝트를 신뢰할 수 없다고 생각하면 토큰을 승인하지 않거나, 사용할 금액만 승인해주세요.", + "are-you-sure": "확실히 확인하셨습니까?", + "are-you-sure-description": "지금부터 전체 시드 구문을 다시 확인할 수 없습니다", + "are-you-sure-to-cancel": "취소하시겠습니까?", + "are-you-sure?": "정말 삭제하시겠어요?", + "ask-in-status": "질문 및 버그 신고", + "at": "로", + "attribution-received": "총 {{max}}개 중 {{attrib}}개 보너스 받음", + "audio": "오디오", + "audio-recorder": "리코더", + "audio-recorder-error": "리코더 오류", + "audio-recorder-max-ms-reached": "최대 녹음 제한 시간에 도달했습니다", + "audio-recorder-permissions-error": "오디오 메시지를 보내려면 권한을 허용해야 합니다", + "authorize": "거래 승인", + "available": "이용 가능", + "available-participants": { + "other": "{{count}}명 더 선택할 수 있습니다" + }, + "back": "뒤로", + "back-up": "백업", + "back-up-seed-phrase": "시드 구문 백업", + "back-up-your-seed-phrase": "시드 구문 백업", + "backing-up": "백업 중...", + "backup-disabled": "비활성화", + "backup-enabled": "활성화 됨", + "backup-recovery-phrase": "시드 구문 백업", + "backup-settings": "백업 설정", + "backup-through-waku": "와쿠를 통한 백업", + "bad-fees-description": "우선 수수료가 제안된 매개변수보다 낮습니다.", + "balance": "잔액", + "begin-set-up": "설정 시작", + "below-base-fee": "기본 수수료 이하의 최대 수수료", + "biometric-auth-android-sensor-desc": "터치 센서", + "biometric-auth-android-sensor-error-desc": "실패", + "biometric-auth-android-title": "인증 필요", + "biometric-auth-confirm-logout": "재 로그인", + "biometric-auth-confirm-message": "계속하려면 생체 인증이 필요합니다. 문제가 있을 경우 계정 비밀번호 또는 패스코드를 사용하여 해제해주세요", + "biometric-auth-confirm-title": "반드시 인증해주시기 바랍니다!", + "biometric-auth-confirm-try-again": "다시 시도", + "biometric-auth-error": "생체 인식 인증 진행 불가 ( {{code}} )", + "biometric-auth-login-error-title": "생체 인식 인증 오류", + "biometric-auth-login-ios-fallback-label": "비밀번호 입력", + "biometric-auth-reason-login": "스테이터스 로그인", + "biometric-auth-reason-verify": "인증 확인", + "biometric-disable-bioauth": "{{bio-type-label}} 사용 안 함", + "biometric-disable-password-description": "이 기능을 사용하지 않으면,", + "biometric-disable-password-title": "비밀번호 저장 안함", + "biometric-enable": "비밀번호를 매번 입력하지 않으려면, {{bio-type-label}} 로그인을 사용하세요", + "biometric-enable-button": "{{bio-type-label}} 사용", + "biometric-enable-keycard": "매번 키카드를 사용하지 않으려면, {{bio-type-label}} 로그인을 사용하세요", + "biometric-faceid": "페이스 ID", + "biometric-fingerprint": "지문", + "biometric-secure-with": "{{bio-type-label}}(으)로 보호", + "biometric-touchid": "터치 ID", + "bip39-password-placeholder": "BIP39 비밀번호", + "blank-keycard-text": "키와 이름을 생성하면 키카드로 계속 진행할 수 있습니다", + "blank-keycard-title": "비어있는 키카드를\n사용하셨습니다", + "block": "차단", + "block-contact": "사용자 차단", + "block-contact-details": "차단 기능을 사용하면 해당 사용자의 이전 메시지를 모두 삭제하고, 새 메시지가 귀하에게 전송되지 않습니다", + "blocked-users": "차단된 사용자", + "bootnode-address": "부트노드 주소", + "bootnode-details": "부트노드 상세정보", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "부트노드 (Bootnode)", + "bootnodes-enabled": "부트노드 사용", + "bootnodes-settings": "부트노드 설정", + "browsed-websites": "브라우저 기록이 여기에 표시됩니다", + "browser": "브라우저", + "browser-not-secure": "연결이 안전하지 않습니다! 이 사이트에서 트랜잭션에 서명하거나 개인정보를 보내지 마십시오.", + "browser-secure": "연결이 안전합니다. 사이트를 정말 신뢰하는지 확인한 후 트랜잭션에 서명하거나 개인 정보를 입력하세요.", + "browsers": "브라우저", + "browsing-cancel": "취소", + "browsing-open-in-android-web-browser": "웹 브라우저로 열기", + "browsing-open-in-ios-web-browser": "웹 브라우저로 열기", + "browsing-open-in-status": "스테이터스로 열기", + "browsing-site-blocked-description1": "이 주소에서 잠재적인 악성 활동을 발견했습니다.사용자의 지갑을 보호하기 위해 추가적인 탐색을 금지했습니다. \n\n문제가 없다고 생각하시면, 저희에게 알려주시기 바랍니다", + "browsing-site-blocked-description2": " 오픈채팅.", + "browsing-site-blocked-go-back": "돌아가기", + "browsing-site-blocked-title": "이 사이트는 차단되었습니다", + "browsing-title": "브라우저", + "bug-report": "버그 신고", + "bug-report-description": "* 설명", + "bug-report-description-placeholder": "필수, 비워둘 수 없음", + "bug-report-steps": "재현 단계", + "bug-report-steps-placeholder": "- 앱 열기\n- 무언가 하기\n- 그리고 나서 다른...", + "bug-report-submit-email": "로그 아카이브와 함께 이메일로 제출", + "bug-report-submit-gh-issue": "로그 없이 GitHub 이슈 제출", + "bug-report-too-short-description": "설명이 너무 짧습니다", + "build-yourself": "서비스 약관없이 앱을 사용하려면, 독자적인 버전을 빌드하면 됩니다", + "buy-crypto": "암호화폐 구매", + "buy-crypto-choose-a-service": "암호화폐 구매를 위해 사용할 서비스를 선택하세요", + "buy-crypto-description": "암호화폐를 구입하기 위한 디앱을 찾아보세요", + "buy-crypto-leaving": "구매를 완료하기 위해 스테이터스를 떠나 써드 파티 웹 사이트에 접속합니다", + "buy-crypto-title": "지갑이 비어 있습니다", + "camera-access-error": "카메라 권한을 승인하려면 시스템 설정에서 스테이터스 > 카메라가 선택되어 있는지 확인해 주세요.", + "can-not-add-yourself": "이건 본인입니다. 다른 사용자를 선택해주세요", + "can-send-messages": "새 메시지를 주고 받을 수 있습니다", + "cancel": "취소", + "cancel-keycard-setup": "키카드 설정 취소", + "cancelling": "취소", + "cannot-read-card": "카드를 읽을 수 없습니다. \n휴대 전화 뒷면에 위치해주세요", + "cannot-use-default-pin": "패스코드 000000은 허용되지 않습니다. \n다른 번호를 사용해주세요", + "cant-open-public-chat": "오픈채팅을 열 수 없습니다", + "cant-report-bug": "버그를 신고할 수 없습니다", + "card-is-blank": "카드가 비어 있습니다", + "card-reseted": "카드가 재설정되었습니다", + "card-unpaired": "이 기기에서 카드 페어링이 해제되었습니다", + "category": "카테고리", + "category-title": "카테고리 제목", + "change-fleet": "플릿을 {{fleet}}(으)로 변경", + "change-logging-enabled": "{{enable}} 로깅하시겠습니까?", + "change-pairing": "페어링 코드 변경", + "change-pairing-description": "페어링 코드를 변경해도 현재 페어링에는 영향을 주지 않습니다. 그러나 새 페어링에는 새 코드가 필요합니다.", + "change-pairing-title": "새 페어링 코드 만들기", + "change-passcode": "패스코드 변경", + "change-password": "비밀번호 변경", + "change-pin": "6자리 패스코드 변경", + "change-puk": "12자리 PUK 변경", + "change-tip": "팁 변경", + "changed-amount-warning": "수량이 {{old}}에서 {{new}}(으)로 변경되었습니다", + "changed-asset-warning": "자산이 {{old}}에서 {{new}}(으)로 변경되었습니다", + "chaos-mode": "카오스 모드", + "chaos-unicorn-day": "카오스 유니콘 데이", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "채팅", + "chat-and-transact": "검열과 감시 없는 안전한 채팅과 거래를 시작해보세요", + "chat-is-a-contact": "연락처에 등록되어 있음", + "chat-is-not-a-contact": "연락처에 없음", + "chat-key": "채팅 키", + "chat-key-content": "스테이터스 채팅 프로토콜의 메시지는 암호화 키를 활용하여 메시지를 주고 받습니다. 공개 채팅 키는 다른 사람들과 메시지를 주고 받기 위해 공유되는 문자열을 의미합니다.", + "chat-key-title": "채팅 키", + "chat-link-previews": "채팅 링크 미리보기", + "chat-name": "채팅 이름", + "chat-name-content": "무작위로 선택된 세 단어로, 채팅 키를 특수한 알고리즘에 대입하여 산출하며 채팅의 기본 닉네임으로 표시됩니다. 채팅 이름은 완전히 고유의 값으로, 다른 유저들과 채팅 이름이 중복될 수 없습니다.", + "chat-name-title": "채팅 이름", + "chat-notification-preferences": "알림 설정", + "chat-settings": "채팅 설정", + "chats": "채팅", + "check-on-block-explorer": "블록 익스플로러에서 확인", + "check-on-opensea": "Opensea에서 확인", + "check-your-recovery-phrase": "시드 구문 확인", + "choose-actions": "작업 선택", + "choose-authentication-method": "계정 인증 방법을 선택하세요", + "choose-new-location-for-keystore": "키스토어 파일을 저장할 새 위치를 선택하세요", + "choose-storage": "저장소 선택", + "clear": "지우기", + "clear-all": "모두 지우기", + "clear-history": "기록 삭제", + "clear-history-action": "지우기", + "clear-history-confirmation": "기록을 삭제하시겠습니까?", + "clear-history-confirmation-content": "채팅 기록을 지우시겠습니까?", + "clear-history-title": "기록을 삭제하시겠습니까?", + "cleared-chat-description-public": "최근 대화 내용이 없습니다. 대화를 시작하거나", + "close": "닫기", + "close-all": "모두 닫기", + "close-app-button": "확인", + "close-app-content": "앱이 종료됩니다. 앱을 다시 실행하면 설정한 네트워크가 사용됩니다", + "close-app-title": "주의!", + "collectibles-leak-metadata": "여기에 NFT를 표시할 수 있습니다. 이 경우 지갑과 IP 주소를 공유하게 됩니다", + "command-button-send": "보내기", + "communities": "커뮤니티", + "communities-alpha": "커뮤니티 (알파)", + "communities-enabled": "커뮤니티 사용", + "communities-verified": "✓ 검증된 스테이터스 커뮤니티", + "community": "커뮤니티", + "community-color": "커뮤니티 색상", + "community-color-placeholder": "색상 선택", + "community-edit-title": "커뮤니티 편집", + "community-emoji-thumbnail-title": "썸네일", + "community-image-pick": "이미지 선택", + "community-image-remove": "제거", + "community-image-take": "사진 촬영", + "community-info": "커뮤니티 정보", + "community-info-not-found": "커뮤니티 정보를 찾을 수 없습니다", + "community-invite-title": "초대", + "community-key": "커뮤니티 개인키", + "community-key-placeholder": "커뮤니티 개인키 입력", + "community-link": "커뮤니티 링크", + "community-members": { + "other": "멤버" + }, + "community-members-title": "멤버", + "community-message-preview": "{{community-name}} 가입 초대", + "community-private-key": "커뮤니티 개인키", + "community-requests-to-join-title": "멤버십 요청", + "community-roles": "역할", + "community-share-title": "공유", + "community-thumbnail-image": "미리보기 이미지", + "community-thumbnail-upload": "업로드", + "complete-hardwallet-setup": "카드가 연결되었습니다. 이 카드로 거래에 서명하거나 계정을 열람할 수 있습니다", + "completed": "완료", + "confirm": "비밀번호 확인", + "confirm-new-password": "새 비밀번호 확인", + "confirm-pairing-code-placeholder": "페어링 코드 확인...", + "confirm-password-placeholder": "비밀번호 확인...", + "confirmation-request": "확인 요청", + "confirmations": "확인", + "confirmations-helper-text": "12번 이상 확인된 트랜잭션은 확정된 것으로 간주됩니다.", + "connect": "접속", + "connect-mailserver-content": "{{name}}에 연결하시겠습니까?", + "connect-wallet": "지갑 연결", + "connected": "연결됨", + "connected-to": "다음으로 연결:", + "connecting": "연결 중...", + "connecting-requires-login": "다른 네트워크에 연결하려면 로그인하세요", + "connection-request": "연결 요청", + "connection-status": "연결 상태", + "connection-with-the-card-lost": "카드와 연결이\n 끊어졌습니다", + "connection-with-the-card-lost-setup-text": "설치를 다시 시작하려면 \n스마트폰 뒷면에 카드를 대고 \n접촉을 유지해주시기 바랍니다", + "connection-with-the-card-lost-text": "계속하려면, 카드를 스마트폰의 뒷면에 대주세요", + "contact-code": "채팅 키", + "contact-s": { + "other": "연락처" + }, + "contacts": "연락처", + "contacts-descr": "연락처는 여기에 표시됩니다. 연락처에 추가한 모든 이들의 상태 업데이트를 받게 됩니다.", + "contacts-empty": "ENS 이름을 보유한 연락처는 여기에 표시됩니다", + "continue": "계속", + "continue-anyway": "계속 진행", + "contract-address": "컨트랙트 주소", + "contract-interaction": "컨트랙트 상호 작용", + "contract-isnt-supported": "컨트랙트가 지원되지 않습니다", + "copy-info": "정보 복사", + "copy-qr": "코드 복사", + "copy-to-clipboard": "클립보드에 복사", + "copy-transaction-hash": "트랜잭션 ID 복사", + "cost-fee": "비용", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "만들기", + "create-a-pin": "6자리 패스코드 만들기", + "create-a-puk": "12자리 PUK 만들기", + "create-category": "카테고리 생성", + "create-channel": "채널 만들기", + "create-channel-title": "새 채널", + "create-community": "커뮤니티 만들기", + "create-group-chat": "그룹 채팅 생성", + "create-multiaccount": "키 생성", + "create-new-key": "새로운 키 획득", + "create-pin": "6자리 패스코드만들기", + "create-pin-description": "스테이터스를 해제하고 트랜잭션을 확인하려면 카드와 6자리 패스코드가 필요합니다", + "created-group-chat-description": "{{group-name}} 그룹을 생성했습니다", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "통화", + "currency-display-name-aed": "아랍에미레이트 디르함", + "currency-display-name-afn": "아프가니스탄 아프가니", + "currency-display-name-ars": "아르헨티나 페소", + "currency-display-name-aud": "호주 달러", + "currency-display-name-bbd": "바베이도스 달러", + "currency-display-name-bdt": "방글라데시 타카", + "currency-display-name-bgn": "불가리아 레프", + "currency-display-name-bhd": "바레인 디나르", + "currency-display-name-bnd": "브루나이 달러", + "currency-display-name-bob": "볼리비아 볼리비아노", + "currency-display-name-brl": "브라질 헤알", + "currency-display-name-btn": "부탄 눌트럼", + "currency-display-name-cad": "캐나다 달러", + "currency-display-name-chf": "스위스 프랑", + "currency-display-name-clp": "칠레 페소", + "currency-display-name-cny": "중국 위안화", + "currency-display-name-cop": "콜롬비아 페소", + "currency-display-name-crc": "코스타리카 콜론", + "currency-display-name-czk": "체코 코루나", + "currency-display-name-dkk": "덴마크 크로네", + "currency-display-name-dop": "도미니카 공화국 페소", + "currency-display-name-egp": "이집트 파운드", + "currency-display-name-etb": "에티오피아 비르", + "currency-display-name-eur": "유로", + "currency-display-name-gbp": "영국 파운드", + "currency-display-name-gel": "조지아 라리", + "currency-display-name-ghs": "가나 세디", + "currency-display-name-hkd": "홍콩 달러", + "currency-display-name-hrk": "크로아티아 쿠나", + "currency-display-name-huf": "헝가리 포린트", + "currency-display-name-idr": "인도네시아 루피아", + "currency-display-name-ils": "이스라엘 셰켈", + "currency-display-name-inr": "인도 루피", + "currency-display-name-isk": "아이슬란드 크로나", + "currency-display-name-jmd": "자메이카 달러", + "currency-display-name-jpy": "일본 엔", + "currency-display-name-kes": "케냐 실링", + "currency-display-name-krw": "대한민국 원", + "currency-display-name-kwd": "쿠웨이트 디나르", + "currency-display-name-kzt": "카자흐스탄 텡게", + "currency-display-name-lkr": "스리랑카 루피", + "currency-display-name-mad": "모로코 디람", + "currency-display-name-mdl": "몰도바 레우", + "currency-display-name-mur": "모리셔스 루피", + "currency-display-name-mwk": "말라위 콰차", + "currency-display-name-mxn": "멕시코 페소", + "currency-display-name-myr": "말레이시아 링깃", + "currency-display-name-mzn": "모잠비크 메티칼", + "currency-display-name-nad": "나미비아 달러", + "currency-display-name-ngn": "나이지리아 나이라", + "currency-display-name-nok": "노르웨이 크로네", + "currency-display-name-npr": "네팔 루피", + "currency-display-name-nzd": "뉴질랜드 달러", + "currency-display-name-omr": "오만 리알", + "currency-display-name-pen": "페루 솔", + "currency-display-name-pgk": "파푸아 뉴기니 키나", + "currency-display-name-php": "필리핀 페소", + "currency-display-name-pkr": "파키스탄 루피", + "currency-display-name-pln": "폴란드 즈워티", + "currency-display-name-pyg": "파라과이 과라니", + "currency-display-name-qar": "카타르 리얄", + "currency-display-name-ron": "루마니아 레우", + "currency-display-name-rsd": "세르비아 디나르", + "currency-display-name-rub": "러시아 루블", + "currency-display-name-sar": "사우디 아라비아 리얄", + "currency-display-name-sek": "스웨덴 크로나", + "currency-display-name-sgd": "싱가포르 달러", + "currency-display-name-thb": "태국 바트", + "currency-display-name-try": "터키 리라", + "currency-display-name-ttd": "트리니다드 토바고 달러", + "currency-display-name-twd": "대만 달러", + "currency-display-name-tzs": "탄자니아 실링", + "currency-display-name-uah": "우크라이나 흐리브냐", + "currency-display-name-ugx": "우간다 실링", + "currency-display-name-usd": "미국 달러", + "currency-display-name-uyu": "우루과이 페소", + "currency-display-name-vef": "베네수엘라 볼리바르", + "currency-display-name-vnd": "베트남 동", + "currency-display-name-zar": "남아프리카 공화국 랜드", + "current": "현재", + "current-average": "현재 평균", + "current-average-tip": "현재 평균 팁", + "current-base": "현재 베이스", + "current-base-fee": "현재 기본 수수료", + "current-minimum-tip": "현재 최소 팁", + "current-network": "현재 네트워크", + "current-password": "현재 비밀번호", + "current-pin": "6자리 패스코드 입력", + "current-pin-description": "6자리 패스코드를 입력해 계속 진행하세요", + "custom": "커스텀 토큰", + "custom-networks": "사용자 정의 네트워크", + "custom-node": "사용자 지정 RPC 엔드포인트를 사용하고 있습니다. 로컬 전송 기록이 불완전 할 수 있습니다.", + "custom-seed-phrase": "잘못된 시드 구문", + "custom-seed-phrase-text-1": "시드 구문 단어들이 스테이터스 딕셔너리와 일치하지 않습니다. 철자를 확인해주세요.", + "dapp": "디앱", + "dapp-starter-pack-accept": "동의하고 열기", + "dapp-starter-pack-description": "스테이터스를 시작하는 데 도움이 될 암호화폐입니다! SNT를 활용하여 이모티콘을 구입하고, ENS 이름을 등록하거나 디앱을 브라우징해보세요", + "dapp-starter-pack-title": "스타터 팩", + "dapp-would-like-to-connect-wallet": "를 아래 지갑에 연결합니다", + "dapps": "디앱", + "dapps-permissions": "디앱 권한", + "dark": "다크", + "data": "데이터", + "data-collected": "데이터 수집 완료", + "data-collected-subtitle": "아래 표에서 저장 및 전송될 정확한 데이터를 확인하세요. 데이터의 경우, 중요한 데이터가 전송되지 않도록 공공 규칙에 따라 검증됩니다. 글만 보고 믿지 말고 직접 확인하세요.", + "data-syncing": "데이터 동기화", + "database-reset-content": "채팅, 연락처, 설정이 모두 삭제되었습니다. 키카드와 계정을 사용하실 수 있습니다", + "database-reset-title": "데이터베이스 초기화", + "database-reset-warning": "데이터베이스가 초기화됩니다. 채팅, 연락처 및 설정이 삭제됩니다", + "datetime-ago": "전", + "datetime-ago-format": "{{number}}{{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "other": "일" + }, + "datetime-day-short": { + "other": "일" + }, + "datetime-hour": { + "other": "시간" + }, + "datetime-hour-short": { + "other": "시간" + }, + "datetime-minute": { + "other": "분" + }, + "datetime-minute-short": { + "other": "분" + }, + "datetime-second": { + "other": "초" + }, + "datetime-second-short": { + "other": "초" + }, + "datetime-today": "오늘", + "datetime-yesterday": "어제", + "decimals": "소수점 (Decimals)", + "decline": "거절", + "decryption-failed-content": "데이터를 복호화하는 중에 오류가 발생했습니다. 오래된 데이터를 삭제하고 새로운 계정을 생성해야 합니다. \"적용\"으로 기존 데이터를 지우거나 \"취소\"로 다시 시도해주세요", + "default": "기본 설정", + "default-assets": "기본 ERC20 및 ERC721", + "default-sync-period": "동기화 기록:", + "delete": "삭제", + "delete-account": "계정 삭제", + "delete-and-leave-group": "그룹 삭제 및 탈퇴", + "delete-bootnode": "부트노드 삭제", + "delete-bootnode-are-you-sure": "이 부트노드를 삭제하시겠습니까?", + "delete-bootnode-title": "부트노드 삭제", + "delete-category-confirmation": "이 카테고리를 삭제하시겠습니까?", + "close-chat": "채팅방 삭제", + "close-chat-confirmation": "이 채팅을 삭제하시겠습니까?", + "delete-confirmation": "삭제하시겠습니까?", + "delete-keys-keycard": "키카드에서 키 삭제", + "delete-mailserver": "스테이터스 노드 삭제", + "delete-mailserver-are-you-sure": "이 스테이터스 노드를 삭제하시겠습니까?", + "delete-mailserver-title": "스테이터스 노드 삭제", + "delete-message": "메시지 삭제", + "delete-my-account": "계정 삭제", + "delete-my-profile": "내 프로필 삭제", + "delete-network-confirmation": "이 네트워크를 삭제하시겠습니까?", + "delete-network-error": "이 네트워크를 삭제하기 전에 다른 네트워크에 연결하세요", + "delete-network-title": "네트워크를 삭제하시겠습니까?", + "delete-node": "노드 삭제", + "delete-node-are-you-sure": "이 노드를 삭제하시겠습니까?", + "delete-node-title": "노드 삭제", + "delete-profile": "프로필 삭제", + "delete-profile-warning": "경고: 시드 구문을 별도로 백업하지 않은 경우, 프로필을 삭제하면 내 자산에 접근할 수 없게 됩니다", + "deny": "거절", + "derivation-path": "파생 경로", + "describe-channel": "채널 설명", + "description": "설명", + "dev-mode": "개발 모드", + "dev-mode-settings": "개발 모드 설정", + "device-syncing": "기기 동기화", + "devices": "기기", + "disable": "비활성화", + "disable-all": "모두 비활성화", + "disable-later-in-settings": "나중에 설정에서 비활성화할 수 있습니다", + "disabled": "비활성화", + "disconnect": "연결 해제", + "disconnected": "연결 중...", + "discover": "찾아보기", + "dismiss": "무시", + "display-collectibles": "수집품 표시", + "done": "완료", + "dont-ask": "다시 묻지 않기", + "edit": "편집", + "edit-categories": "카테고리 편집", + "edit-channel-title": "채널 편집", + "edit-chats": "채팅방 편집", + "edit-community": "커뮤니티 편집", + "edit-favourite": "즐겨찾기 편집", + "edit-group": "그룹 편집", + "edit-profile": "프로필 편집", + "edited": "수정 됨", + "editing-message": "메시지 편집", + "empty-activity-center": "채팅 알림이\n여기에 나타납니다", + "empty-chat-description": "이 채팅에 아직\n메시지가 없습니다", + "empty-chat-description-community": "지난 {{quiet-hours}} 동안 이곳은 활동이 없었습니다.", + "empty-chat-description-one-to-one": "채팅방의 모든 메시지는 암호화되며, 귀하와 상대방만 열람할 수 있습니다. 채팅 상대: ", + "empty-chat-description-public": "이 채팅방은 지난 {{quiet-hours}} 동안 대화가 없었습니다. 대화를 시작하거나 ", + "empty-chat-description-public-share-this": "이 채팅을 공유하세요.", + "empty-keycard-required": "빈 키카드가 필요합니다", + "empty-pending-invitations-descr": "초대 링크를 통해 그룹에 가입하고 싶은 사용자들이\n여기에 표시됩니다", + "empty-tab": "빈 탭", + "enable": "사용", + "enable-all": "모두 활성화", + "enable-link-previews": "링크 미리보기를 활성화할까요?", + "encrypt-with-password": "비밀번호로 암호화", + "ens-10-SNT": "10 SNT", + "ens-add-username": "사용자 이름 추가", + "ens-agree-to": "다음에 동의합니다 ", + "ens-chat-settings": "채팅 설정", + "ens-custom-domain": "커스텀 도메인", + "ens-custom-username-hints": "username.domain.eth와 같은 커스텀 도메인 형식의 사용자 이름을 입력해주세요", + "ens-custom-username-taken": "소유한 사용자 이름이 아닙니다 :(", + "ens-deposit": "입금", + "ens-dismiss-message": "닫으려면 여기를 클릭하세요", + "ens-displayed-with": "귀하의 메시지가 다른 사용자에게 다음과 같이 표시됩니다", + "ens-get-name": "범용 사용자 이름 등록", + "ens-got-it": "알겠습니다", + "ens-locked": "사용자 이름이 등록되었습니다. {{date}}까지 이름을 해지할 수 없습니다", + "ens-name-content": "이더리움 네임 서비스(Ethereum Name Service)로 채팅키와 연결되는 커스텀 닉네임. ENS 이름은 탈중앙화된 사용자 이름입니다.", + "ens-name-not-found": "ENS 이름을 확인할 수 없습니다", + "ens-name-title": "ENS 이름", + "ens-network-restriction": "Mainnet에서만 사용 가능", + "ens-no-usernames": "연결된 사용자 이름이 없습니다", + "ens-powered-by": "Powered by Ethereum Name Services", + "ens-primary-username": "주 사용자 이름", + "ens-register": "등록하기", + "ens-registration-failed": "사용자 이름을 등록하려면 다시 시도해주세요", + "ens-registration-failed-title": "트랜잭션 실패", + "ens-registration-failure": "등록 실패", + "ens-registration-in-progress": "등록 중...", + "ens-release-username": "사용자 이름 계약 해지", + "ens-remove-hints": "제거하면 키와 사용자 이름이 분리됩니다.", + "ens-remove-username": "사용자 이름 삭제", + "ens-saved": "은 채팅 키와 연결되어 스테이터스에서 사용 가능합니다.", + "ens-saved-title": "사용자 이름 추가됨", + "ens-show-username": "채팅창에 내 ENS 이름 표시", + "ens-terms-header": "이름 등록 약관", + "ens-terms-point-1": "자산은 1년 동안 예치됩니다. 여러분의 SNT는 잠기게 되며, 별도로 지출되지 않습니다.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS 레지스트리).", + "ens-terms-point-2": "1년 후, 등록한 이름의 계약을 해지하고 예치금을 돌려받을 수 있습니다. 이름을 유지하고 싶을 경우에는 별도의 조치를 할 필요가 없습니다.", + "ens-terms-point-3": "스테이터스가 컨트랙트를 업그레이드 하는 등으로 이용 약관을 변경하는 경우, 사용자는 남은 기간에 상관 없이 사용자 이름을 해지할 수 있습니다.", + "ens-terms-point-4": "컨트랙트 관리자는 사용자의 예치금에 접근할 수 없습니다. 이는 등록된 주소로만 반환됩니다.", + "ens-terms-point-5": "사용자의 주소는 ENS 이름과 공개적으로 연동됩니다.", + "ens-terms-point-6": "사용자 이름은 stateofus.eth 의 서브 도메인 노드로 생성되며, ENS 스마트 컨트랙트 이용 약관을 따릅니다.", + "ens-terms-point-7": "사용자를 대신하여 SNT를 양도하는 계약을 승인합니다. 이는 트랜잭션에 전송 권한을 부여할 때부터 유효합니다.", + "ens-terms-point-8": "이러한 조항은 다음 주소의 스마트 컨트랙트 로직으로 보장됩니다:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar) ", + "ens-terms-registration": "이름 등록 약관.", + "ens-test-message": "안녕하세요", + "ens-transaction-pending": "트랜잭션 대기 중...", + "ens-understand": "지갑 주소와 사용자 이름이 공개적으로 연결됨을 이해합니다.", + "ens-username": "ENS 사용자 이름", + "ens-username-already-added": "사용자 이름은 사용자의 채팅 키와 연결되며, 스테이터스에서 사용 가능합니다.", + "ens-username-available": "✓ 사용자 이름을 사용할 수 있습니다!", + "ens-username-connected": "이 사용자 이름은 사용자가 소유하며, 채팅 키와 연결됩니다.", + "ens-username-connected-continue": "'채팅에서 ENS 사용자 이름 표시'를 설정합니다.", + "ens-username-connected-with-different-key": "사용자 이름과 현재 키를 연결하는 트랜잭션이 필요합니다.", + "ens-username-connection-confirmation": "트랜잭션이 완료되면 {{username}}와 연결됩니다", + "ens-username-hints": "최소 4자 이상이어야 하며, 알파벳 소문자 및 숫자만 가능합니다.", + "ens-username-invalid": "문자와 숫자만 가능.", + "ens-username-invalid-name-warning": "ENS 이름 중 하나의 등록 프로세스에 문제가 발생했습니다. 지갑 트랜잭션에 해당 ENS 이름을 사용하지 마시고, support@status.im으로 지원팀에 문의해주세요", + "ens-username-owned": "✓ 이미 소유한 이름입니다. ", + "ens-username-owned-continue": "계속하면 이 사용자 이름이 채팅 키와 연결됩니다.", + "ens-username-registration-confirmation": "좋습니다! 트랜잭션이 완료되면 {{username}}(을)를 소유하게 됩니다", + "ens-username-registration-invalid": "경고! 등록 프로세스에 문제가 발생했습니다. 지갑 트랜잭션에 해당 ENS 이름을 사용하지 마시고, support@status으로 문의해주세요", + "ens-username-taken": "이미 사용 중인 이름입니다 :(", + "ens-username-you-can-follow-progress": "지갑의 트랜잭션 기록 섹션에서 진행 상황을 확인할 수 있습니다.", + "ens-usernames": "ENS 이름", + "ens-usernames-details": "다른 사용자가 쉽게 인지할 수 있도록 범용 사용자 이름을 등록하세요", + "ens-want-custom-domain": "다른 도메인의 이름 가져오기", + "ens-want-domain": "stateofus.eth 도메인 획득하기", + "ens-welcome-hints": "ENS 이름은 굉장히 긴 주소를 직관적인 사용자 이름으로 변환합니다.", + "ens-welcome-point-customize": "ENS 이름은 채팅에서 보이는 무작위 영단어 3개짜리 이름을 대체하는 닉네임입니다. {{name}} 대신 @닉네임 을 사용하세요.", + "ens-welcome-point-customize-title": "채팅 이름을 원하는 대로 만들어보세요", + "ens-welcome-point-receive": "다른 사람들과 더욱 간편하게 거래하세요.", + "ens-welcome-point-receive-title": "채팅으로 트랜잭션을 진행하세요", + "ens-welcome-point-register": "한 번 이름을 등록하면, 해당 이름을 영원히 보유할 수 있습니다. 등록한 지 1년 후, 이름을 공개하고 SNT를 돌려받을 수 있습니다.", + "ens-welcome-point-register-title": "10 SNT만으로 등록할 수 있습니다", + "ens-welcome-point-simplify": "(0x...)로 시작하는 16진수 해시값 대신, 읽고 쓰기 쉬운 ENS 이름으로 거래를 시작하세요.", + "ens-welcome-point-simplify-title": "ETH 주소를 단순화하세요", + "ens-welcome-point-verify": "다음 단계에서 소유한 사용자 이름을 인증하고 추가하세요.", + "ens-welcome-point-verify-title": "이미 사용자 이름이 있으신가요?", + "ens-your-username": "사용자 이름", + "ens-your-usernames": "사용자 이름", + "ens-your-your-name": "ENS 이름", + "enter-12-words": "12단어의 시드 구문을 입력하되 띄어쓰기로 구분해주세요", + "enter-a-private-key": "개인 키 입력", + "enter-a-seed-phrase": "시드 구문 입력", + "enter-address": "주소 입력", + "enter-contact-code": "ENS (vitalik94) 또는 채팅 키 (0x04…)", + "enter-pair-code": "페어링 코드 입력", + "enter-pair-code-description": "이미 페어링 된 스테이터스 클라이언트에서 페어링 코드를 설정할 수 있습니다", + "enter-password": "비밀번호 입력", + "enter-password-migration-prompt": "암호를 입력하고 키와 함께 연락처, 채팅 및 설정을 이전합니다", + "enter-pin": "6자리 패스코드 입력", + "enter-puk-code": "PUK 코드 입력", + "enter-puk-code-description": "6자리 패스코드가 차단되었습니다. \n PUK 코드를 입력해 차단을 해제하세요.", + "enter-recipient-address-or-username": "수신인의 지갑 주소 또는 사용자 이름을 입력하세요", + "enter-seed-phrase": "시드 구문 입력", + "enter-url": "URL 입력", + "enter-user-pk": "사용자 공개키 입력", + "enter-watch-account-address": "QR 코드 스캔 \n또는 \n열람할 주소를 입력하세요", + "enter-word": "단어 입력", + "enter-your-code": "6자리 패스코드 입력", + "enter-your-password": "비밀번호 입력", + "error": "오류", + "error-unable-to-get-balance": "잔액을 가져올 수 없습니다", + "error-unable-to-get-prices": "통화 변환 오류가 발생했습니다. 다시 시도하려면 화면을 새로고침하세요.", + "error-unable-to-get-token-balance": "토큰 잔액을 가져올 수 없습니다", + "errors": "오류", + "eth": "ETH", + "ethereum-account": "이더리움 계정", + "ethereum-address": "이더리움 주소", + "ethereum-node-started-incorrectly-description": "이더리움 노드가 잘못된 구성으로 시작되어, 어플리케이션이 중지되고 해당 상태에서 복구됩니다. 구성된 네트워크 ID = {{network-id}} , 실제 ID = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "이더리움 노드가 잘못 시작되었습니다", + "etherscan-lookup": "Etherscan에서 보기", + "everyone": "모두", + "expand-all": "모두 확장", + "export-account": "계정 정보 내보내기", + "export-key": "개인키 내보내기", + "external-storage-denied": "외부 저장소에 대한 액세스가 거부되었습니다", + "failed": "실패", + "faq": "자주 묻는 질문", + "fast": "빠름", + "favourite": "즐겨찾기", + "favourite-description": "즐겨 찾는 웹사이트가 여기에 표시됩니다", + "favourites": "즐겨찾기", + "favourites-empty": "즐겨찾기에 추가된 주소는 여기에 표시됩니다.", + "fee-cap": "수수료 한도", + "fee-explanation": "트랜잭션의 최대 가격입니다. 블록 기본 수수료가 이를 초과하는 경우, 기본 요금이 낮은 다음 블록에 포함됩니다.", + "fee-options": "권장 수수료 옵션", + "fetch-community": "커뮤니티 가져오기", + "fetch-messages": "↓ 메시지 가져오기", + "fetch-timeline": "↓ 가져오기", + "fetching-community": "커뮤니티 가져오는 중...", + "find": "찾기", + "finish": "완료", + "finishing-card-setup": "카드 설정 마무리", + "fleet": "플릿(Fleet)", + "fleet-settings": "플릿(Fleet) 설정", + "follow": "팔로우", + "follow-your-interests": "오픈채팅에서 새로운 사람들을 만나보세요", + "free": "↓ 무료", + "from": "보내는 주소", + "gas-amount-limit": "가스량 한도", + "gas-limit": "가스 한도", + "gas-price": "가스 가격", + "gas-used": "사용된 가스", + "generate-a-key": "키 생성", + "generate-a-new-account": "계정 생성", + "generate-a-new-key": "새로운 키 생성", + "generate-account": "키 생성", + "generate-an-account": "계정 생성", + "generate-new-key": "키 생성", + "generating-codes-for-pairing": "> 제품 소프트웨어를 카드에 다운로드\n > 잠금 해제 및 페어링 코드 생성", + "generating-keys": "키 생성 중...", + "generating-mnemonic": "시드 구문 생성", + "get-a-keycard": "키카드를 획득하세요", + "get-started": "시작", + "get-status-at": "http:\/\/status.im 에서 스테이터스를 만나보세요", + "get-stickers": "이모티콘 가져오기", + "give-a-short-description": "짧은 설명을 더하세요", + "give-a-short-description-community": "짧은 설명을 더하세요", + "give-permissions-camera": "카메라 접근 \n권한 허용", + "glossary": "용어 사전", + "go-to-settings": "설정으로 이동...", + "got-it": "확인했습니다", + "grant-face-id-permissions": "Face ID 사용을 승인하려면, 시스템 설정에서 스테이터스의 Face ID를 활성화해주세요", + "group-chat": "그룹 채팅", + "group-chat-admin": "관리자", + "group-chat-admin-added": "** {{member}} ** 님이 관리자로 지정됨", + "group-chat-all-contacts-invited": "이미 모든 연락처가 그룹에 있습니다.", + "group-chat-created": "** {{member}} ** 님이 ** {{name}} ** 그룹 생성", + "group-chat-decline-invitation": "초대 거절", + "group-chat-member-added": "** {{member}} ** 님이 초대됨", + "group-chat-member-joined": "** {{member}} ** 님이 그룹에 참여", + "group-chat-member-removed": "** {{member}} ** 님이 그룹을 떠남", + "group-chat-members-count": "{{selected}}\/{{max}} 멤버", + "group-chat-name-changed": "** {{member}} ** 님이 그룹명을 ** {{name}} ** (으)로 변경", + "group-chat-no-contacts": "아직 연락처가 없습니다. \n친구들을 초대하고 채팅을 시작해보세요", + "group-info": "그룹 정보", + "group-invite": "그룹 초대", + "group-invite-link": "그룹 초대 링크", + "group-membership-request": "그룹 멤버십 요청", + "gwei": "Gwei", + "has-permissions": "에게 다음 지갑 접근 권한을 허용합니다", + "hash": "해시값", + "help": "도움말", + "help-capitalized": "도움말", + "help-center": "도움말 센터", + "help-improve-status": "스테이터스가 발전할 수 있게 도와주세요", + "hide": "숨기기", + "hide-content-when-switching-apps": "화면 캡처 차단", + "hide-content-when-switching-apps-ios": "미리보기 숨기기", + "history": "기록", + "history-nodes": "스테이터스 노드", + "hold-card": "카드를 휴대전화 뒷면에 위치해주세요", + "home": "홈", + "hooks": "후크 (Hooks)", + "how-it-works": "어떻게 시작하나요?", + "http-gateway-error": "죄송합니다. 요청이 실패했습니다!", + "identifier": "식별자", + "image": "이미지", + "image-remove-current": "현재 사진 삭제", + "image-source-gallery": "갤러리에서 선택하기", + "image-source-make-photo": "촬영", + "image-source-title": "사진 편집", + "import": "불러오기", + "import-community": "커뮤니티 가져오기", + "import-community-title": "커뮤니티 가져오기", + "in-contacts": "연락처에 등록되어 있습니다", + "include": "포함", + "incoming": "입금", + "incoming-transaction": "입금 트랜잭션", + "incorrect-code": [ + "str", + "잘못된 코드입니다. 다시 입력해 주세요" + ], + "increase-gas": "가스 증가", + "initialization": "초기화", + "install": "↓ 설치", + "insufficient-balance-to-cover-fee": "거래 수수료를 위한 잔액이 충분하지 않습니다", + "intro-message1": "스테이터스에 오신 것을 환영합니다!\n이 메시지를 탭하여 비밀번호를 설정하고 시작하세요.", + "intro-privacy-policy-note1": "스테이터스는 절대로 개인 정보를 수집, 공유 및 판매하지 않습니다. 계속하면 다음에 동의하는 것으로 간주됩니다: ", + "intro-privacy-policy-note2": "개인 정보 정책", + "intro-text": "스테이터스는 탈중앙 웹으로 향하는 첫걸음입니다", + "intro-text1": "검열과 해킹으로부터 자유로운 P2P 네트워크로 자유롭게 채팅하세요", + "intro-text2": "전세계 어디서든 디지털 자산을 주고 받을 수 있습니다. 은행 계좌가 필요하지 않습니다", + "intro-text3": "여러분의 데이터를 안전하게 지키는 게임, 거래소, SNS를 체험해보세요", + "intro-title1": "검열 없는 안전한 커뮤니케이션", + "intro-title2": "안전한 암호화폐 지갑", + "intro-title3": "탈중앙 앱", + "intro-wizard-text1": "키는 사용자의 스마트폰에 저장됩니다. 저장된 키가 스테이터스 계정을 관리합니다.", + "intro-wizard-text2": "키 하나는 채팅 이름으로 사용합니다. 읽을 수 있는 이름이며, 앞으로 변경할 수 없습니다.", + "intro-wizard-text3": "키카드를 보유하고 있다면, 더욱 안전한 보안을 위해 키카드에 키를 저장하세요.", + "intro-wizard-text4": "키 보안 및 암호화", + "intro-wizard-text6": "스테이터스는 새로운 메시지를 수신하면 사용자에게 알림을 전달합니다. 사용자는 추후에 설정에서 알림 옵션을 변경할 수 있습니다.", + "intro-wizard-title-alt4": "비밀번호 만들기", + "intro-wizard-title-alt5": "비밀번호 확인", + "intro-wizard-title1": "키 생성하기", + "intro-wizard-title2": "채팅 이름 선택", + "intro-wizard-title3": "키 저장소 선택", + "intro-wizard-title4": "6자리 패스코드 만들기", + "intro-wizard-title5": "패스코드 확인", + "intro-wizard-title6": "알림 사용", + "introduce-yourself": "간단한 메시지로 자신을 소개해주세요", + "invalid-address-qr-code": "스캔한 QR 코드에 유효한 주소가 포함되어 있지 않습니다", + "invalid-format": "잘못된 형식\n{{format}} 이어야 합니다", + "invalid-key-confirm": "적용", + "invalid-key-content": "파일이 손상되어 데이터베이스를 암호화할 수 없습니다. 자산과 채팅 키는 안전하지만, 채팅과 연락처 같은 데이터는 복구될 수 없습니다. \"{{erase-multiaccounts-data-button-text}}\" 버튼으로 다른 모든 데이터를 삭제하고, 기존 자산 접근과 메시지 전송을 그대로 이용하세요", + "invalid-number": "잘못된 숫자", + "invalid-pairing-password": "잘못된 페어링 비밀번호", + "invalid-public-chat-topic": "잘못된 오픈채팅 주제", + "invalid-range": "잘못된 형식, {{min}} ~ {{max}} 사이 여야합니다", + "invalid-username-or-key": "잘못된 사용자 이름 또는 채팅 키", + "invite": "초대", + "invite-button": "초대", + "invite-chat-accept": "수락", + "invite-chat-accept-join": "수락 및 참여", + "invite-chat-intro": "친구가 스테이터스로 초대했습니다. 여러분의 시작을 도울 암호화폐가 준비되어 있습니다! SNT를 통해 ENS 이름을 등록하거나 이모티콘을 구매해보세요", + "invite-chat-name": "친구 초대", + "invite-chat-pending": "대기 중", + "invite-chat-rule": "수락하면 사용자의 친구는 암호화폐로 초대 보너스를 받게 됩니다", + "invite-chat-starter-pack": "스타터 팩", + "invite-friends": "친구 초대", + "invite-instruction-fifth": "초대 보너스는 언제든지 환급 가능합니다.", + "invite-instruction-first": "친구가 스테이터스를 다운로드하고 사용할 수 있도록 특별 초대 링크를 보내세요", + "invite-instruction-fourth": "사용자는 초대 보너스를, 친구는 스타터 팩을 받게 됩니다", + "invite-instruction-second": "친구는 스테이터스를 다운로드하고 계정을 만듭니다 (안드로이드만 가능)", + "invite-instruction-third": "초대가 확인되면 친구와의 채팅이 시작됩니다.", + "invite-people": "친구 초대", + "invite-privacy-policy-public": "초대 링크를 통해 스테이터스를 설치했습니다. 이 채팅에 참여하면 추천인에게 보상을 제공하고 다음에 동의하는 것으로 간주됩니다", + "invite-privacy-policy1": "수락을 누르면 초대 프로그램 약관에 동의하는 것으로 간주됩니다.", + "invite-privacy-policy2": "이용 약관.", + "invite-public-chat-home": "추천 초대", + "invite-public-chat-intro": "스테이터스 사용에 도움이 될 암호화폐입니다! ENS 이름을 등록하거나 이모티콘 패키지를 구입해보세요", + "invite-receive-account": "초대 보너스를 받을 계정", + "invite-reward": "친구를 초대하고 암호화폐를 받아보세요!", + "invite-reward-friend": "친구: ", + "invite-reward-friend-description": "친구는 {{reward}}로 구성된 스테이터스 스타터 팩을 받고, 스테이터스를 시작합니다", + "invite-reward-friend-name": "스타터 팩", + "invite-reward-you": "사용자: ", + "invite-reward-you-description": "친구를 초대하고 {{reward}} 초대 보너스를 받아보세요! 받은 보너스로 스테이터스에서 이모티콘을 구입하거나, ENS 이름 등록 및 디앱에 활용할 수 있습니다", + "invite-reward-you-name": "초대 보너스", + "invite-select-account": "초대 보너스를 받을 계정 선택", + "invite-warning": "이 프로모션은 미국 거주자가 아니며 안드로이드 기기를 사용하는 유저에게만 유효합니다. 초대받은 친구는 7일 이내로 초대를 확인해야 합니다", + "invited": "초대됨", + "join": "참여", + "join-a-community": "또는 커뮤니티에 참여", + "join-group-chat": "그룹 참여", + "join-group-chat-description": "{{username}} 님이 {{group-name}} 그룹에 초대", + "join-me": "스테이터스에서 만나요: {{url}}", + "join-new-private-chat": "1:1 채팅 시작", + "join-new-public-chat": "오픈채팅 참여", + "joined": "참여", + "joined-group-chat-description": "{{username}} 님의 초대로 {{group-name}} 에 입장했습니다", + "key": "키", + "key-managment": "키 관리", + "key-on-device": "개인 키가 이 기기에 저장됩니다", + "keycard": "키카드", + "keycard-access-reset": "키카드 접근 권한이 초기화되었습니다", + "keycard-applet-install-instructions": "애플릿을 설치하려면 https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation 를 확인해주세요", + "keycard-awaiting-description": "카드를 기기 NFC 접촉면에 놓아주시기 바랍니다", + "keycard-awaiting-title": "탐색 중...", + "keycard-backup": "백업 키카드 만들기", + "keycard-backup-success-body": "백업 카드가 성공적으로 생성되었습니다. 이제 기본 카드처럼 계정에서 사용할 수 있습니다.", + "keycard-backup-success-title": "백업 성공", + "keycard-blocked": "키카드가 차단되었습니다. \n계속 사용하려면 카드를 재설정해야 합니다.", + "keycard-can-use-with-new-passcode": "새 패스코드로 이 카드를 사용할 수 있습니다.", + "keycard-cancel-setup-text": "키카드 설치가 취소됩니다. 키카드를 사용하려면 설치를 완료해야 합니다. 설치를 취소하시겠습니까?", + "keycard-cancel-setup-title": "위험한 작업", + "keycard-connected-description": "카드와 기기의 접촉을 유지하세요", + "keycard-connected-title": "연결됨", + "keycard-desc": "키카드를 보유하고 계신가요? 트랜잭션에 사용할 키를 키카드에 저장하세요", + "keycard-dont-ask-card": "로그인 시 카드 요청 안함", + "keycard-enter-new-passcode": "새 패스코드 입력 {{step}} \/ 2", + "keycard-error-description": "다음 단계를 진행하기 위하여 카드를 다시 연결해주시기 바랍니다", + "keycard-error-title": "연결이 끊어졌습니다", + "keycard-factory-reset": "공장 설정으로 카드 초기화", + "keycard-factory-reset-text": "카드에 저장된 모든 니모닉 구문이 삭제됩니다. 이 키카드와 함께 사용했던 니모닉 구문의 백업이 있는지 확인해주세요.", + "keycard-factory-reset-title": "공장 초기화를 진행하시겠습니까?", + "keycard-free-pairing-slots": "키카드에는 {{n}}개의 무료 페어링 슬롯이 있습니다", + "keycard-has-multiaccount-on-it": "카드가 이미 가득 찼습니다. 카드는 하나의 키 쌍을 보유할 수 있습니다.", + "keycard-init-description": "카드를 기기 뒷면에 대고 그 상태를 유지해주세요", + "keycard-init-title": "카드 검색 중...", + "keycard-is-blocked-details": "이제 이 카드를 사용해 서명하거나 계정에 접근할 수 없습니다. 패스코드 및 PUK 접근 시도 실패 횟수가 너무 많습니다.", + "keycard-is-blocked-instructions": "계정에 액세스하려면 카드를 공장 초기화해야 합니다. 아래 버튼을 탭해 진행하세요. 니모닉이 필요합니다.", + "keycard-is-blocked-title": "키카드가 차단되었습니다", + "keycard-is-frozen-details": "자산을 지키기 위해 카드가 동결되었습니다. 계정에 접근하기 위한 카드 접근 권한을 재설정하고, 트랜잭션을 진행하시기 바랍니다. PUK 또는 니모닉으로 진행할 수 있습니다.", + "keycard-is-frozen-factory-reset": "니모닉으로 재설정", + "keycard-is-frozen-reset": "PUK 재설정", + "keycard-is-frozen-title": "키카드가 잠겼습니다", + "keycard-onboarding-finishing-header": "마무리 작업 진행 중", + "keycard-onboarding-intro-header": "키카드에 키를 저장하세요", + "keycard-onboarding-intro-text": "이 작업은 몇 분 정도 소요되며, 여러분의 계정을 안전하게 보호하기 위해 중요한 작업입니다.", + "keycard-onboarding-mnemonic-text": "또한 시드 구문을 적을 종이와 연필이 필요합니다.", + "keycard-onboarding-pairing-header": "카드 페어링 중...", + "keycard-onboarding-pin-text": "키카드에 대한 액세스를 보호하기 위한 6자리 패스코드를 생성해야 합니다.", + "keycard-onboarding-preparing-header": "카드 준비 중...", + "keycard-onboarding-puk-code-header": "메모하신 후 \n 안전한 장소에 보관해주세요", + "keycard-onboarding-recovery-phrase-description": "이 시드 구문 단어들을 통해 계정을 복구할 수 있습니다. 이 단어들을 오프라인상에 안전하게 보관하시길 바랍니다.", + "keycard-onboarding-recovery-phrase-header": "시드 구문을 백업하세요", + "keycard-onboarding-recovery-phrase-text": "이 시드 단어들은 사용자 키를 생성하는 데 사용됩니다. 이 단어들을 타인에게 공유하면 안됩니다.", + "keycard-onboarding-start-header": "스마트폰 뒷면에 \n 카드를 대주세요", + "keycard-onboarding-start-step1": "패스코드 생성", + "keycard-onboarding-start-step1-text": "키를 암호화하기 위한 6자리 패스코드 생성. 코드 생성에는 약 1분 정도 소요됩니다.", + "keycard-onboarding-start-step2": "PUK 와 페어링 코드를 메모해주세요", + "keycard-onboarding-start-step2-text": "메모를 위해 필기도구가 필요할 수 있습니다. 메모에는 1분 정도 시간이 소요됩니다", + "keycard-onboarding-start-step3": "시드 구문을 백업하세요", + "keycard-onboarding-start-step3-text": "메모를 위한 필기도구를 준비해 주세요. 메모는 1분 정도 시간이 소요됩니다", + "keycard-onboarding-start-text": "설치하는 동안 카드를 스마트폰과 접촉된 상태로 유지해주세요. 설치 과정은 약 4분 정도 소요됩니다", + "keycard-processing-description": "카드와 기기의 접촉을 유지하세요", + "keycard-processing-title": "처리 중", + "keycard-recover": "분실 또는 동결된 카드인가요?", + "keycard-recover-text": "니모닉 구문이 있는 경우, 이 계정과 연결된 새 키카드를 만들 수 있습니다. 새 키카드를 사용하거나 동결된 키카드에서 공장 초기화를 진행할 수 있습니다.", + "keycard-recover-title": "이 계정에 대한 새 카드를 만드시겠어요?", + "keycard-recovery-intro-button-text": "복구 시작", + "keycard-recovery-intro-header": "키카드에 저장된 키 복구하기", + "keycard-recovery-intro-text": "이전에 키카드로 키를 생성했고, 지금 이 기기에서 해당 키를 사용하려는 경우", + "keycard-recovery-no-key-header": "복구할 대상이\n없습니다", + "keycard-recovery-no-key-text": "이 키카드에는 키가 저장되어 있지 않습니다. 새로운 키를 생성하고 키카드에 키를 저장해주세요", + "keycard-recovery-phrase-confirm-header": "시드 구문 확인", + "keycard-recovery-phrase-confirmation-text": "두 번째 기회는 없습니다! 키카드를 분실하여 접근 권한을 손실하면, 시드 구문으로만 계정에 접근할 수 있습니다. 오로지 여러분만이 이 시드 구문을 관리할 수 있습니다. 반드시 안전하게 백업하시기 바랍니다.", + "keycard-recovery-phrase-confirmation-title": "시드 구문을 모두 메모하셨나요?", + "keycard-recovery-success-header": "키가 성공적으로\n 복구되었습니다", + "keycard-redeem-title": "수령 위치", + "keycard-redeem-tx": "자산 입금", + "keycard-redeem-tx-desc": "카드를 탭하여 서명하고, 자산을 수령하세요", + "keycard-reset-passcode": "패스코드 재설정", + "keycard-success-description": "이제 카드를 제거해도 좋습니다", + "keycard-success-title": "성공", + "keycard-unauthorized-operation": "이 작업을 수행할 권한이 없습니다.\n 올바른 카드를 탭해 다시 시도하세요.", + "keycard-upsell-subtitle": "향상된 보안과 편리성", + "language": "언어", + "last-backup-performed": "마지막 백업 수행:", + "last-transaction": "마지막 트랜잭션", + "learn-more": "더 알아보기", + "learn-more-about-keycard": "키카드에 대해 자세히 알아보기", + "leave": "나가기", + "leave-chat": "채팅방 나가기", + "leave-chat-confirmation": "채팅 기록이 기기에서 삭제됩니다. 다시 채팅 기록을 검색할 수 없습니다.", + "leave-community": "커뮤니티 나가기", + "leave-confirmation": "{{chat-name}} 나가기", + "leave-group": "그룹 나가기", + "left": "나감", + "les-ulc": "LES\/ULC", + "lets-go": "시작하기", + "light": "라이트", + "limit": "한계", + "linked-on": "{{date}} 에 연결", + "load-messages-before": "{{date}} 전", + "load-more-messages": "↓ 메시지 더 가져오기", + "load-more-timeline": "↓ 더 가져오기", + "loading": "로딩 중...", + "local-notifications": "알림", + "local-notifications-subtitle": "백그라운드 서비스 활성화", + "lock-app-with": "앱 잠금 수단:", + "log-level": "로그 수준", + "log-level-settings": "로그 수준 설정", + "logging": "로깅", + "logging-enabled": "로깅을 활성화하시겠습니까?", + "login-pin-description": "6자리 패스코드를 입력해 키를 해제하세요", + "logout": "로그아웃", + "logout-app-content": "계정에서 로그아웃힙니다. 다시 로그인하면 선택한 네트워크를 사용합니다", + "logout-are-you-sure": "로그아웃하시겠습니까?", + "logout-key-management": "키 관리에 액세스하려면 로그아웃해야 합니다.", + "logout-title": "로그아웃하시겠습니까?", + "looking-for-cards": "카드 검색 중...", + "lost-connection": "연결 끊김", + "low-tip": "팁이 너무 낮습니다", + "lower-than-average-tip": "평균보다 낮은 팁", + "mail-should-be-configured": "메일 클라이언트를 구성해야 합니다", + "mailserver-address": "스테이터스 노드 주소", + "mailserver-automatic": "자동 선택", + "mailserver-automatic-switch-explanation": "가장 빠른 스테이터스 노드 선택", + "mailserver-connection-error": "Status 노드에 연결할 수 없습니다", + "mailserver-content": "메시지를 전달하거나 최대 30일까지 메시지를 저장하는 스테이터스 네트워크 노드.", + "mailserver-details": "스테이터스 노드 세부 정보", + "mailserver-error-content": "선택한 스테이터스 노드에 연결할 수 없습니다.", + "mailserver-error-title": "스테이터스 노드에 연결하던 중 오류가 발생했습니다", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "다른 Status 노드 선택", + "mailserver-reconnect": "스테이터스 노드에 연결할 수 없습니다. 화면을 터치하여 다시 연결하세요", + "mailserver-request-error-content": "스테이터스 노드가 다음 오류를 반환했습니다: {{error}}", + "mailserver-request-error-status": "기록을 가져 오던 중 오류가 발생했습니다. 자세한 내용은 로그를 확인해주세요", + "mailserver-request-error-title": "스테이터스 노드 요청 오류", + "mailserver-request-retry": "다시 요청", + "mailserver-retry": "다시 시도", + "mailserver-title": "스테이터스 노드", + "main-currency": "주요 통화", + "main-networks": "메인 네트워크", + "main-wallet": "내 지갑", + "mainnet-network": "메인 네트워크", + "make-admin": "관리자로 지정", + "manage-connections": "애플리케이션 연결에서 연결 관리", + "manage-keys-and-storage": "키 및 저장소 관리", + "mark-all-read": "모두 읽음 처리", + "master-account": "마스터 계정", + "max-fee": "최대 수수료", + "max-priority-fee": "최대 우선 수수료", + "maximum-fee": "최대 수수료", + "maximum-fee-desc": "트랜잭션 전반의 최대 가격입니다. 현재 블록 기본 수수료가 이를 초과하는 경우 기본 수수료가 낮은 다음 블록에 거래가 포함됩니다.", + "maybe-later": "나중에", + "member-ban": "멤버 내보내기", + "member-kick": "멤버 내보내기", + "members": { + "other": "{{count}} 명" + }, + "members-active": { + "other": "{{count}} 명" + }, + "members-active-none": "멤버 없음", + "members-count": "{{count}} 명", + "members-label": "멤버", + "members-limit-reached": "멤버 한도에 도달했습니다", + "members-title": "멤버", + "membership-approval": "승인 필요", + "membership-approval-description": "커뮤니티는 무료로 가입할 수 있지만 신규 회원은 먼저 커뮤니티 생성인의 승인을 받아야 합니다.", + "membership-button": "멤버십 요건", + "membership-declined": "멤버쉽 요청이 거절되었습니다", + "membership-description": "그룹 멤버십은 그룹 관리자의 승인이 필요합니다", + "membership-ens": "ENS 사용자 이름 필요", + "membership-ens-description": "사용자의 커뮤니티에 가입하려면 ENS 사용자 이름이 필요합니다", + "membership-free": "요구 사항 없음", + "membership-free-description": "사용자의 커뮤니티는 누구나 자유롭게 가입할 수 있습니다", + "membership-invite": "다른 멤버의 초대 필요", + "membership-invite-description": "사용자의 커뮤니티는 오직 기존 멤버의 초대를 통해서만 참여할 수 있습니다", + "membership-none": "없음", + "membership-none-placeholder": "신규 회원이 가입하기 전에 특정 기준을 충족하도록 요구할 수 있습니다. 기준은 언제든지 변경할 수 있습니다.", + "membership-request-pending": "멤버십 요청 대기 중", + "membership-requests": "멤버십 요청", + "membership-title": "멤버십 요건", + "message": "메시지", + "message-not-sent": "메시지가 전송되지 않았습니다", + "message-options-cancel": "취소", + "message-reply": "답장", + "messages": "메시지", + "messages-from-contacts-only-subtitle": "연락처에 추가한 친구만이 그룹으로 사용자를 초대하거나 새 채팅을 시작할 수 있습니다", + "might-break": "일부 디앱에 오류를 일으킬 수 있습니다", + "migration-successful": "마이그레이션 성공", + "migration-successful-text": "계정이 키카드로 성공적으로 마이그레이션되었습니다", + "migrations-failed-content": "{{message}} \n스키마 버전: 초기 {{initial-version}} , 현재 {{current-version}} , 마지막 {{last-version}} \n\n데이터베이스 오류가 발생했습니다. 자산과 채팅 키는 안전하지만 채팅과 연락처 같은 데이터는 복구될 수 없습니다. \"{{erase-multiaccounts-data-button-text}}\" 버튼으로 다른 모든 데이터를 삭제하고, 기존 자산 접근과 메시지 전송을 그대로 이용하세요.", + "miners-higher-fee": "더 높은 수수료를 지불하면 채굴자들이 거래를 더 일찍 처리할 가능성이 높아집니다.", + "minimum-received": "최소 수신", + "mobile-network-ask-me": "데이터를 사용할 때 다시 묻기", + "mobile-network-continue-syncing": "동기화 계속", + "mobile-network-continue-syncing-details": "나중에 설정을 변경할 수 있습니다.", + "mobile-network-go-to-settings": "설정으로 이동", + "mobile-network-settings": "모바일 데이터", + "mobile-network-sheet-configure": "여기에서 동기화를 더 상세히\n설정하세요:", + "mobile-network-sheet-offline": "Wi-Fi 없음, 메시지 동기화 비활성화.", + "mobile-network-sheet-offline-details": "모바일 데이터를 활용한 동기화가 중지되어 있습니다", + "mobile-network-sheet-remember-choice": "내 선택 기억하기", + "mobile-network-sheet-settings": "설정", + "mobile-network-start-syncing": "동기화 시작", + "mobile-network-stop-syncing": "동기화 중지", + "mobile-network-stop-syncing-details": "Wi-Fi에 연결될 때까지 동기화를 중지할까요?", + "mobile-network-use-mobile": "모바일 데이터 사용", + "mobile-network-use-mobile-data": "스테이터스는 채팅 및 지갑 동기화 시, 많은 데이터를 소모합니다.", + "mobile-network-use-wifi": "Wi-Fi만 사용", + "mobile-syncing-sheet-details": "스테이터스는 채팅 및 지갑 동기화 시, 많은 데이터를 소모합니다.", + "mobile-syncing-sheet-title": "모바일 데이터로 동기화하시겠습니까?", + "more": "더 보기", + "move-and-reset": "이동 및 초기화", + "move-keystore-file": "키스토어 파일 이동", + "move-keystore-file-to-keycard": "키스토어 파일을 키카드로 이동하시겠어요?", + "multiaccount-exists-content": "이 계정을 위한 키가 이미 존재하여 다시 추가할 수 없습니다. 만약 비밀번호, 패스코드, 또는 키카드를 잊어버렸다면, 스테이터스 앱을 재설치하고 시드 구문을 입력해 키에 액세스해주시기 바랍니다", + "multiaccount-exists-title": "이 계정을 위한 키가 이미 존재합니다", + "multiaccounts-recover-enter-phrase-text": "12, 15, 18, 21 또는 24 단어로 구성된 시드 구문을 입력해주세요. 단어는 한 칸씩 띄어서 구분하세요.", + "multiaccounts-recover-enter-phrase-title": "내 시드 구문 입력", + "mute": "알림 끄기", + "my-accounts": "내 계정", + "my-accounts-empty": "사용 가능한 계정이 여기에 표시됩니다", + "my-profile": "내 프로필", + "my-status": "내 상태", + "name": "이름", + "name-of-token": "토큰 이름", + "name-optional": "이름 (선택)", + "name-your-channel": "채널 이름 지정", + "name-your-channel-placeholder": "채널명", + "name-your-community": "커뮤니티 이름 지정", + "name-your-community-placeholder": "기억하기 쉬운 이름", + "need-help": "도움이 필요하신가요?", + "network": "네트워크", + "network-chain": "네트워크 체인", + "network-details": "네트워크 세부정보", + "network-fee": "네트워크 수수료", + "network-id": "네트워크 ID", + "network-info": "네트워크 정보", + "network-invalid-network-id": "지정된 네트워크 ID가 RPC url의 네트워크 ID와 대응되지 않습니다", + "network-invalid-status-code": "잘못된 상태 코드 : {{code}}", + "network-invalid-url": "네트워크 URL이 잘못되었습니다", + "network-settings": "네트워크 설정", + "never": "절대", + "new": "신규", + "new-category": "새 카테고리", + "new-chat": "새 채팅", + "new-community-title": "새 커뮤니티", + "new-contact": "새 연락처", + "new-contract": "새 컨트랙트", + "new-favourite": "새 즐겨찾기", + "new-group": "새 그룹", + "new-group-chat": "새 그룹 채팅", + "new-network": "새 네트워크", + "new-password": "새 비밀번호", + "new-pin-description": "6자리 새 패스코드 입력", + "new-public-group-chat": "오픈채팅 참여", + "new-puk-description": "새로운 12자리 PUK 입력", + "new-status": "새 상태", + "new-tab": "새 탭", + "next": "다음", + "nickname": "닉네임", + "nickname-description": "닉네임은 스테이터스에서 다른 사용자를 식별하는데 사용됩니다. 설정한 닉네임은 사용자만 볼 수 있습니다", + "no": "아니오", + "no-collectibles": "수집품 없음", + "no-contacts": "아직 연락처가 없습니다", + "no-keycard-applet-on-card": "카드에 키카드 애플릿이 없습니다", + "no-messages": "메시지 없음", + "no-pairing-slots-available": "이 카드는 이미 5개의 기기에 페어링 되어 있어 이 기기와는 페어링 할 수 없습니다. 페어링 된 기기 중 하나를 사용해 이 카드로 로그인하고 카드의 페어링 슬롯을 확보하세요", + "no-pinned-messages": "고정된 메시지 없음", + "no-result": "결과 없음", + "no-thanks": "괜찮습니다", + "no-tokens-found": "토큰 없음", + "node-address": "노드 주소", + "node-details": "노드 세부정보", + "node-info": "노드 정보", + "node-version": "노드 버전", + "nodes-disabled": "스테이터스 노드 비활성화", + "non-archival-node": "RPC 엔드포인트는 보관 요청을 지원하지 않습니다. 로컬 전송 기록이 불완전할 수 있습니다.", + "non-contacts": "연락처 아님", + "nonce": "Nonce", + "none": "해당 없음", + "normal": "일반", + "not-applicable": "서명되지 않은 트랜잭션에 사용할 수 없음", + "not-connected-nodes": "스테이터스 노드에 연결되지 않았습니다", + "not-connected-to-peers": "어떤 피어에도 연결되지 않았습니다", + "not-enough-snt": "SNT가 부족합니다", + "not-found": "찾을 수 없습니다", + "not-keycard-text": "사용한 카드는 키카드가 아닙니다. 이 기능은 키카드를 구입해야 사용할 수 있습니다.", + "not-keycard-title": "키카드 아님", + "not-registered": "등록되지 않음", + "notification-settings": "알림", + "notifications": "알림", + "notifications-non-contacts": "연락처 미등록자에게 오는 알림", + "notifications-preferences": "알림 설정", + "notifications-servers": "알림 서버", + "notifications-switch": "알림 표시", + "notifications-transactions": "지갑 트랜잭션", + "notify": "알림", + "now": "방금 막", + "off": "끄기", + "off-status-tree": "스테이터스 트리 끄기", + "offline": "오프라인", + "offline-messaging-use-history-explanation": "앱이 종료된 동안 전송된 메시지를 확인하기 위해 스테이터스 노드를 활성화 합니다. 스테이터스 노드를 활성화하면, 스테이터스 노드는 여러분의 IP 주소를 사용하게 됩니다. 스테이터스 노드를 비활성화하면, 앱이 종료된 동안 수신된 메시지를 확인할 수 없습니다.", + "offline-messaging-use-history-nodes": "스테이터스 노드 사용", + "ok": "확인", + "ok-continue": "네, 알겠습니다", + "ok-got-it": "확인", + "ok-save-pass": "예, 비밀번호를 저장합니다", + "okay": "예", + "on": "켜기", + "on-status-tree": "스테이터스 트리 켜기", + "once-enabled-share-metadata": "미리보기를 활성화하면, 채팅방에 올라간 링크를 통해 메타 데이터가 웹사이트와 공유될 수 있습니다", + "one-day": "1일", + "one-month": "1달", + "one-week": "1주", + "open": "열기", + "open-chat": "채팅 열기", + "open-dapp": "디앱 브라우저", + "open-dapp-store": "디앱 찾아보기", + "open-home": "열기...", + "open-in-new-tab": "새 탭에서 열기", + "open-membership": "멤버십 오픈", + "open-nfc-settings": "NFC 설정 열기", + "open-on-block-explorer": "블록 익스플로러에서 열기", + "opening-buy-crypto": "{{site}} 여는 중...", + "optimal": "최적", + "optional": "선택", + "or": "또는", + "outgoing": "송금", + "outgoing-transaction": "출금 트랜잭션", + "page-camera-request-blocked": "카메라 요청이 차단되었습니다. 카메라 요청을 활성화하려면 설정으로 이동해주세요", + "page-would-like-to-use-camera": "에서 카메라 사용을 요청합니다", + "pair": "기기 페어링", + "pair-card": "페어 카드", + "pair-code": "페어 코드", + "pair-code-explanation": "이 카드를 다른 기기(최대 5개)와 연동하여 계정을 열람하고 트랜잭션에 서명할 수 있습니다", + "pair-code-placeholder": "페어링 코드...", + "pair-this-card": "이 카드 페어링", + "pair-this-device": "기기 페어링", + "pair-this-device-description": "기기를 페어링하여 연락처와 채팅을 동기화합니다", + "paired-devices": "페어링 된 기기", + "pairing": "페어링", + "pairing-card": "카드 페어링", + "pairing-changed": "페어링 코드가 변경되었습니다", + "pairing-code-placeholder": "페어링 코드...", + "pairing-code_error1": "페어링 코드가 일치하지 않습니다.", + "pairing-go-to-installation": "페어링 설정으로 이동", + "pairing-maximum-number-reached-content": "새 기기를 활성화하려면 이전 기기 중 하나를 비활성화해주세요", + "pairing-maximum-number-reached-title": "최대 기기 수에 도달했습니다", + "pairing-new-installation-detected-content": "다른 기기가 감지되었습니다.\n기기를 올바르게 사용하려면, 새 기기를 사용하기 전에 페어링을 사용하도록 설정해야 합니다. \n아래 설정의 기기 섹션으로 이동하여 기기를 페어링 해주세요.", + "pairing-new-installation-detected-title": "새 기기 감지", + "pairing-no-info": "정보 없음", + "pairing-please-set-a-name": "기기의 이름을 설정해주세요", + "passphrase": "패스 구문", + "password": "비밀번호", + "password-description": "최소 6자리의 비밀번호를 생성해주세요. 비밀번호는 스테이터스 앱을 사용하고 거래를 진행하는 데 사용됩니다.", + "password-mismatch": "비밀번호가 일치하지 않습니다", + "password-placeholder": "비밀번호...", + "password-placeholder2": "비밀번호 확인", + "password-reset-in-progress": "비밀번호 변경 중...", + "password-reset-success": "비밀번호 변경됨", + "password-reset-success-message": "다시 로그인해야 합니다", + "password_error1": "비밀번호가 일치하지 않습니다.", + "paste": "붙여넣기", + "paste-json": "JSON 붙여넣기", + "pay-to-chat": "지불하고 채팅 시작", + "peer-content": "스테이터스 채팅 네트워크에 연결되는 디바이스. 각 유저는 연결된 기기 수에 따라 하나 또는 하나 이상의 피어로 나타나게 됩니다.", + "peer-title": "피어", + "peer-to-peer": "P2P", + "peers": "피어", + "pending": "대기 중", + "pending-confirmation": "확인 대기 중...", + "pending-invitations": "대기 중인 멤버십 요청", + "per-gas-price-limit": "가스당 가격 제한", + "per-gas-tip-limit": "가스당 팁 제한", + "perform-backup": "백업 수행", + "permissions": "권한", + "phone-e164": "국제전화 1", + "photos": "사진", + "photos-access-error": "사진 접근 권한을 승인하려면 시스템 설정에서 스테이터스 > 사진이 선택되어 있는지 확인해 주세요.", + "pin": "고정", + "pin-changed": "6자리 패스코드가 변경되었습니다", + "pin-code": "6자리 패스코드", + "pin-limit-reached": "핀 한도에 도달했습니다. 먼저 이전 메시지를 고정 해제해주세요", + "pin-mismatch": "잘못된 비밀번호", + "pin-one-attempt": " 한 번의 시도가 실패하면 ", + "pin-one-attempt-blocked-after": "키카드가 차단됩니다", + "pin-one-attempt-blocked-before": "조심하세요. 마지막", + "pin-one-attempt-frozen-after": "키카드가 동결됩니다", + "pin-one-attempt-frozen-before": "조심하세요. 마지막", + "pin-retries-left": "남은 재시도 가능 횟수: {{number}}번", + "pinned-by": "고정한 사람", + "pinned-messages": "고정된 메시지", + "pinned-messages-count": { + "other": "고정된 메시지 {{count}} 개" + }, + "pinned-messages-empty": "고정된 메시지가 여기에 표시됩니다. 메시지를 고정하려면, 메시지를 길게 누르고 '고정'을 탭하세요.", + "powered-by-paraswap": "Powered by Paraswap", + "preference": "설정", + "preview-privacy": "미리보기 방지 모드", + "previewing-may-share-metadata": "링크 미리보기를 사용하면 메타 데이터가 해당 웹사이트와 공유될 수 있습니다", + "price-impact": "가격 영향", + "price-impact-desc": "이 트랜잭션에 대한 예상 가격 영향입니다. 현재 블록 기본 수수료가 이를 초과하는 경우 기본 수수료가 낮은 다음 블록에 거래가 포함됩니다.", + "principles": "원칙", + "priority": "우선 순위", + "privacy": "개인 정보", + "privacy-and-security": "개인 정보 및 보안", + "privacy-photos": "프로필 사진 개인 정보 보호", + "privacy-policy": "개인 정보 보호 정책", + "privacy-show-to-warning": "이미 프로필 사진을 본 사람들은 프로필을 계속 볼 수 있습니다", + "private-key": "개인 키", + "private-notifications": "프라이빗 알림", + "private-notifications-descr": "스테이터스가 새로운 메시지를 수신하면 사용자에게 알림을 전달합니다. 사용자는 설정에서 옵션을 변경할 수 있습니다.", + "processing": "잠시만 기다려주세요", + "product-information": "제품 정보", + "profile": "프로필", + "profile-deleted-content": "프로필이 성공적으로 삭제되었습니다", + "profile-deleted-keycard": "이제 키카드에서 다른 키 쌍을 복구하세요", + "profile-deleted-title": "프로필 삭제 완료", + "profile-details": "프로필 세부 사항", + "profile-not-found": "프로필을 찾을 수 없습니다", + "profile-pic-pick": "갤러리에서 선택", + "profile-pic-remove": "사진 삭제", + "profile-pic-take": "사진 촬영", + "profile-picture-updated": "프로필 사진 업데이트 됨", + "public-channel": "공공 채널", + "public-chat": "오픈채팅", + "public-chat-description": "관심사에 따라 오픈채팅에 참여해보세요! 누구든지 새로운 채팅을 시작할 수 있습니다.", + "public-chats": "오픈채팅", + "public-group-status": "오픈채팅", + "public-group-topic": "주제", + "public-key": "공개 키", + "puk-and-pairing-codes-displayed": "PUK 및 코드 표시", + "puk-changed": "12자리 PUK가 변경되었습니다", + "puk-code": "PUK 코드", + "puk-code-explanation": "6자리 패스코드를 잊어버리거나 코드를 3회 잘못 입력할 경우, 이 코드를 통해 카드 계정 잠금을 해제할 수 있습니다.", + "puk-mismatch": "PUK 코드가 일치하지 않습니다", + "push-failed-transaction": "트랜잭션이 실패했습니다", + "push-failed-transaction-body": "{{value}} {{currency}} - {{to}}", + "push-inbound-transaction": "{{value}} {{currency}}를 받았습니다", + "push-inbound-transaction-body": "{{from}} 에서 {{to}}", + "push-notifications-server-enabled": "서버 활성화", + "push-notifications-servers": "푸시 알림 서버", + "push-outbound-transaction": "{{value}} {{currency}}를 보냈습니다", + "push-outbound-transaction-body": "{{from}} 에서 {{to}}", + "quiet-days": "{{quiet-days}} 일", + "quiet-hours": "{{quiet-hours}} 시간", + "re-encrypt-key": "키 재암호화", + "rearrange-categories": "카테고리 재정렬", + "receive": "받기", + "receive-transaction": "트랜잭션 받기", + "recent": "최근", + "recent-empty": "최근에 사용한 주소가 여기에 표시됩니다", + "recent-recipients": "연락처", + "recently-used-stickers": "최근 사용한 이모티콘이 여기에 나타납니다", + "recipient": "수신인", + "recipient-code": "수신인 주소 입력", + "recipient-code-placeholder": "0x... 또는 username.domain.eth", + "recover": "복구", + "recover-key": "기존 키 액세스", + "recover-keycard-multiaccount-not-supported": "이 계정을 위한 키가 이미 존재하여 다시 추가할 수 없습니다. 만약 비밀번호, 패스코드, 또는 키카드를 잊어버렸다면, 스테이터스 앱을 재설치하고 시드 구문을 입력해 키에 액세스해주시기 바랍니다", + "recover-with-keycard": "키카드로 복구하기", + "recover-with-seed-phrase": "시드 구문으로 복구", + "recovering-key": "키 액세스 중...", + "recovery-confirm-phrase": "시드 구문 확인", + "recovery-phrase": "시드 구문", + "recovery-success-text": "새로운 코드 혹은 비밀번호를 생성해 키를 재암호화해야 합니다.", + "recovery-typo-dialog-description": "시드 구문은 처음 받은 것과 정확히 동일한 철자와 순서로 사용해야 합니다", + "recovery-typo-dialog-title": "시드 구문이 정확한가요?", + "redeem-amount": "{{quantity}} 보너스 수령 가능", + "redeem-now": "보너스 받기", + "redeem-success": "성공적으로 보너스를 받았습니다!", + "reduced-tip": "우선순위 팁이 줄어듭니다", + "refresh": "새로고침", + "registered": "등록됨", + "reject": "거절", + "reject-and-delete": "거절 및 삭제", + "remember-me": "기억하기", + "remind-me-later": "나중에 다시 확인", + "remote-notifications": "원격 알림", + "remote-notifications-subtitle": "구글 푸시 알림 활성화", + "remove": "삭제", + "remove-favourite": "즐겨찾기 제거", + "remove-from-chat": "채팅에서 내보내기", + "remove-from-contacts": "연락처 삭제", + "remove-from-contacts-text": "연락처에서 특정 사용자를 삭제해도, 사용자의 지갑 주소는 숨길 수 없습니다", + "remove-group": "그룹 삭제", + "remove-network": "네트워크 삭제", + "remove-token": "토큰 제거", + "removed": "퇴장됨", + "repeat-pin": "6자리 패스코드 다시 입력", + "repeat-puk": "새로운 12자리 PUK 반복", + "replying-to": "에 회신{{author}}", + "report-bug-email-template": "1. 문제 설명\n {{description}}\n \n\n 2. 재현 단계\n {{steps}}\n \n\n 3. 문제를 시연할 수 있는 스크린샷을 첨부하세요.\n", + "request-access": "접근 권한 요청", + "request-feature": "기능 요청", + "request-membership": "멤버쉽 요청", + "request-pending": "요청 대기 중…", + "request-transaction": "트랜잭션 요청", + "required-field": "필수 입력란", + "resend-message": "재전송", + "reset-card": "카드 재설정", + "reset-card-description": "이 작업은 카드를 초기 상태로 재설정합니다. 개인 키를 포함한 모든 카드 데이터가 지워집니다. 이 작업은 되돌릴 수 없습니다.", + "reset-database": "데이터베이스 초기화", + "reset-database-warning": "채팅, 연락처 및 설정을 삭제합니다. 비밀번호를 분실한 경우 필요합니다", + "reset-database-warning-keycard": "채팅, 연락처, 설정을 삭제합니다.", + "reset-password": "비밀번호 초기화", + "restore-defaults": "기본값 복원", + "retry": "다시 시도", + "revoke-access": "접근 권한 취소", + "rpc-url": "RPC URL", + "rpc-usage-copy": "클립보드에 복사", + "rpc-usage-filter": "필터 방법", + "rpc-usage-get-stats": "새로고침", + "rpc-usage-info": "RPC 사용 통계", + "rpc-usage-reset": "초기화", + "safe-estimate": "안전한 추정치", + "save": "저장", + "save-password": "비밀번호 저장", + "save-password-unavailable": "비밀번호를 저장하기 위해 기기 패스코드를 설정해주세요", + "save-password-unavailable-android": "비밀번호 저장을 사용할 수 없습니다. 사용자의 기기가 루팅되었거나, 필요한 보안 기능이 없을 수 있습니다.", + "scan-qr": "QR코드 스캔", + "scan-qr-code": "지갑 주소 QR 코드 스캔", + "scan-tokens": "토큰 스캔", + "search": "검색", + "search-no-chat-found": "검색 결과가 없습니다. 이것을 찾으셨나요?", + "secret-keys-confirmation-text": "이는 스마트폰을 잃어버릴 경우에도 키카드를 사용하기 위해 필요합니다.", + "secret-keys-confirmation-title": "코드를 적어두셨나요?", + "security": "보안", + "see-details": "세부 정보 확인", + "see-it-again": "다시 보기", + "see-suggestions": "제안 보기", + "seed-key-uid-mismatch": "시드가 일치하지 않습니다", + "seed-key-uid-mismatch-desc-1": "입력한 시드 구문이 {{multiaccount-name}} 와 일치하지 않습니다", + "seed-key-uid-mismatch-desc-2": "본 계정의 키를 관리하려면, 시드 구문을 인증하고 다시 시도하세요.", + "seed-phrase-content": "BIP39 표준 리스트에서 무작위로 선별된 읽기 쉬운 단어들로서, 다른 지갑 및 디바이스에서 여러분의 이더리움 지갑을 복구하거나 불러올때 사용합니다. 시드 구문은 크립토 생태계에서 \"니모닉 구문\", \"복구 구문\", 또는 \"지갑 백업\"라고도 칭합니다. 대부분의 크립토 앱들은 계정을 생성하기 위해 동일한 표준을 채택하고 있습니다.", + "seed-phrase-placeholder": "시드 구문...", + "seed-phrase-title": "시드 구문", + "select": "선택", + "select-account": "계정 선택", + "select-account-dapp": "디앱에 사용할 계정을 선택해주세요", + "select-account-first": "먼저 계정을 선택하세요", + "select-chat": "메시지를 주고받기 시작할 채팅을 선택해주세요", + "select-new-location-for-keys": "개인 키를 저장할 새 위치를 선택하세요", + "select-token-to-receive": "받을 토큰 선택", + "select-token-to-swap": "스왑할 토큰 선택", + "selected": "인기 채팅방", + "send-logs": "버그 신고", + "send-logs-to": "{{email}}에 버그 신고", + "send-message": "메시지 보내기", + "send-push-notifications": "푸시 알림 보내기", + "send-push-notifications-description": "비활성화되면, 메시지를 받는 사람에게 알림이 표시되지 않습니다", + "send-request": "요청 전송", + "send-request-amount": "수량", + "send-request-amount-max-decimals": "최대 소수점 자릿수는 {{asset-decimals}}입니다.", + "send-request-unknown-token": "알 수 없는 토큰 - {{asset}}", + "send-sending-to": "수신인: {{recipient-name}}", + "send-transaction": "트랜잭션 보내기", + "sending": "송금액:", + "sent-at": "보낸 시각:", + "server": "서버", + "set-a-topic": "채팅 주제 생성", + "set-currency": "통화 설정", + "set-custom-fee": "사용자 지정 수수료 설정", + "set-dapp-access-permissions": "디앱 접근 권한 설정", + "set-max": "최대값 설정", + "settings": "설정", + "share": "공유", + "share-address": "주소 공유", + "share-chat": "채팅 공유", + "share-contact-code": "채팅 키 공유하기", + "share-dapp-text": "스테이터스에서 사용하는 디앱 확인: {{link}}", + "share-link": "링크 공유", + "share-logs": "로그 공유", + "share-my-profile": "내 프로필 공유", + "share-profile": "프로필 공유", + "share-profile-link": "프로필 링크 공유", + "share-public-chat-text": "스테이터스 앱에서 오픈채팅에 참여하세요: {{link}}", + "shared": "공유된", + "sharing-copied-to-clipboard": "클립보드에 복사됨", + "sharing-copy-to-clipboard": "클립보드로 복사", + "sharing-data-desc-1": "데이터의 경우, 중요한 데이터가 전송되지 않도록 공공 규칙에 따라 검증됩니다. 글만 보고 믿지 말고 직접 확인하세요.", + "sharing-data-desc-2": "사용 데이터는 스테이터스의 P2P 네트워크를 통해 종단간 암호화되어 전송됩니다", + "sharing-data-desc-3": "일반 채팅 키 대신 일회용 키가 사용됩니다.", + "sharing-data-desc-4": "사용 데이터는 IP 주소와 연동될 수 없습니다", + "sharing-data-desc-5": "모든 사용자의 누적 데이터는 공개적으로 사용될 수 있습니다", + "sharing-data-desc-6": "데이터가 전송된 후 휴대폰에서 제거됩니다", + "sharing-share": "공유하기", + "show-less": "숨기기", + "show-more": "더 보기", + "show-notifications": "알림 표시", + "show-profile-pictures": "프로필 사진 보기", + "show-profile-pictures-to": "프로필 사진 보여주기", + "show-qr": "QR코드 표시", + "show-transaction-data": "트랜잭션 데이터 표시", + "sign-and-send": "서명 및 송금", + "sign-anyway": "서명 진행하기", + "sign-in": "로그인", + "sign-message": "메시지 서명", + "sign-out": "로그아웃", + "sign-request-failed": "메시지에 서명할 수 없습니다", + "sign-with": "다음으로 서명", + "sign-with-password": "비밀번호로 서명", + "sign-you-in": "로그인 하는 중...", + "signing": "서명", + "signing-a-message": "메시지 서명", + "signing-phrase": "서명 구문", + "skip": "건너 뛰기", + "slippage": "슬리피지", + "slow": "느림", + "something-went-wrong": "문제가 발생했습니다", + "soon": "곧", + "specify-address": "주소 지정", + "specify-name": "이름 지정", + "specify-network-id": "네트워크 ID 지정", + "specify-rpc-url": "RPC URL 지정", + "specify-server-public-key": "서버 공개키 입력", + "specify-symbol": "기호 지정", + "start-chat": "채팅 시작", + "start-conversation": "대화 시작", + "start-group-chat": "그룹 채팅 시작", + "start-new-chat": "새 채팅 시작", + "starter-pack-coming": "곧 스타터 팩이 지급됩니다", + "starter-pack-coming-description": "몇 분에서 몇 시간이 걸릴 수 있습니다", + "starter-pack-received": "스타터 팩을 받았습니다", + "starter-pack-received-description": "스테이터스를 시작하는 데 도움이 될 암호화폐입니다! SNT를 활용하여 이모티콘을 구입하고, ENS 이름을 등록하거나 디앱을 사용해보세요", + "status": "스테이터스", + "status-always-online": "항상 온라인", + "status-automatic": "자동", + "status-automatic-subtitle": "자동으로 상태 설정", + "status-confirmed": "확인됨", + "status-dnd": "방해 금지", + "status-dnd-subtitle": "모든 알림 음소거", + "status-hardwallet": "스테이터스 하드월렛", + "status-inactive": "비활성", + "status-inactive-subtitle": "온라인 상태 숨기기", + "status-is-open-source": "스테이터스는 오픈 소스입니다", + "status-keycard": "스테이터스 키카드", + "status-mobile-descr": "모바일 데이터로 채팅을 동기화하면 데이터를 많이 소모하게 될 수 있습니다. 설정을 통해 모바일 데이터를 사용한 동기화를 비활성화 할 수 있습니다.", + "status-not-sent-click": "전송되지 않았습니다. 옵션을 클릭하세요", + "status-not-sent-tap": "전송 실패. 옵션을 선택하세요", + "status-pending": "대기 중", + "status-sent": "전송됨", + "status-tx-not-found": "TX를 찾을 수 없습니다", + "status-updates-descr": "상태 업데이트가 여기에 표시됩니다. 타임라인 업데이트를 받으려면 프로필을 연락처로 추가하세요.", + "statuses-descr": "생각을 공유하고 연락처에 등록된 이들의 소식을 받아보세요", + "statuses-my-status-descr": "무슨 생각을 하고 계신가요? 여러분의 프로필에 방문하는 모든 사람들은 여러분의 상태 메시지를 확인할 수 있습니다. 여러분을 연락처에 등록한 사람은 여러분의 타임라인 업데이트를 확인할 수 있습니다", + "step-i-of-n": "{{number}}단계 중 {{step}}단계", + "sticker": "이모티콘", + "sticker-market": "이모티콘 마켓", + "storage": "저장소", + "submit": "로그인", + "submit-bug": "버그 제출", + "success": "성공", + "suggested-min-tip": "최소 팁 추천", + "suggested-price-limit": "권장 가격 제한", + "swap": "스왑", + "swap-details": "스왑 세부정보", + "switch-to-simple-interface": "간단한 인터페이스로 전환", + "symbol": "기호 (Symbol)", + "sync-all-devices": "모든 기기 동기화", + "sync-in-progress": "동기화 중...", + "sync-settings": "동기화 설정", + "sync-synced": "동기화 됨", + "syncing-devices": "동기화 중...", + "system": "시스템", + "tabs": "탭", + "tag-was-lost": "태그가 손실되었습니다", + "tap-card-again": "스마트폰 뒷면에 카드를 다시 터치해주세요", + "terms-of-service": "이용 약관", + "test-networks": "테스트 네트워크", + "text-input-disabled": "잠시만 기다려주세요...", + "thank-you": "감사합니다", + "this-device": "스마트폰에 저장", + "this-device-desc": "키는 암호화되어 사용자의 기기에 안전하게 저장됩니다", + "this-is-you-signing": "사용자의 서명 구문입니다", + "this-will-take-few-seconds": "이 작업은 몇 초가 소요됩니다", + "three-days": "3일", + "three-words-description": "트랜잭션 서명 전 이 3단어가 표시됩니다", + "three-words-description-2": "다른 단어가 표시되면, 트랜잭션을 취소하고 로그아웃하세요", + "timeline": "타임라인", + "tip-cap": "팁 한도", + "to": "받는 주소", + "to-block": "차단", + "to-enable-biometric": "{{bio-type-label}}를 활성화 하려면, 사용자의 잠금 해제 화면에 비밀번호를 저장해야 합니다", + "to-encrypt-enter-password": "계정을 암호화하려면 비밀번호를 입력해주세요", + "to-see-this-message": "이 메시지를 보려면,", + "token": "토큰", + "token-auto-validate-decimals-error": "{{symbol}} 토큰 주소 {{address}} 의 자릿수가 잘못 되었습니다. {{expected}} (으)로 세팅되었으나 {{actual}} 로 감지되었습니다.", + "token-auto-validate-name-error": "{{symbol}} 토큰 주소 {{address}} 의 이름이 잘못되었습니다. {{expected}} (으)로 세팅 되었으나 {{actual}} 로 감지되었습니다.", + "token-auto-validate-symbol-error": "{{symbol}} 토큰 주소 {{address}} 의 심볼이 잘못 되었습니다. {{expected}} (으)로 세팅되었으나 {{actual}} 로 감지되었습니다.", + "token-details": "토큰 세부 정보", + "topic-name-error": "소문자(a 에서 z), 숫자 및 하이픈(-)만 사용할 수 있습니다. 채팅 키는 사용할 수 없습니다.", + "total-gas": "총 가스", + "transaction": "트랜잭션", + "transaction-data": "트랜잭션 데이터", + "transaction-declined": "트랜잭션 거절됨", + "transaction-description": "네트워크에서 12번 이상 확인된 거래는 완료된 것으로 간주됩니다.", + "transaction-details": "트랜잭션 세부정보", + "transaction-failed": "트랜잭션 실패", + "transaction-fee": "거래 수수료", + "transaction-history": "트랜잭션 기록", + "transaction-request": "트랜잭션 요청", + "transaction-sent": "트랜잭션 전송됨", + "transaction-signed": "트랜잭션이 성공적으로 서명되었습니다", + "transactions": "트랜잭션", + "transactions-filter-select-all": "모두 선택", + "transactions-filter-title": "기록 필터", + "transactions-history": "트랜잭션 기록", + "transactions-history-empty": "아직 트랜잭션 기록이 없습니다", + "transactions-history-loading": "트랜잭션 기록을 로드 중입니다. 시간이 걸릴 수 있습니다.", + "transactions-load-more": "더 불러오기", + "transactions-management-enabled": "트랜잭션 관리 (알파)", + "transactions-sign": "서명", + "transfer-ma-unknown-error-desc-1": "다중 계정이 삭제되지 않은 것 같습니다. 데이터베이스가 초기화되었을 수도 있습니다", + "transfer-ma-unknown-error-desc-2": "계정 목록을 확인하고 다시 시도해주세요. 계정이 뜨지 않으면 기존 키 액세스로 이동하여 시드 구문으로 복구해주세요", + "transfers-fetching-failure": "전송 기록을 업데이트할 수 없습니다. 연결 상태를 확인하고 아래로 당겨 다시 시도하세요", + "tribute-required-by-multiaccount": "{{multiaccount-name}}님과 채팅을 시작하려면 SNT가 필요합니다.", + "tribute-state-paid": "트리뷰트 지불", + "tribute-state-pending": "트리뷰트 대기 중", + "tribute-state-required": "{{snt-amount}} SNT 트리뷰트 필요", + "tribute-to-talk": "트리뷰트 투 토크", + "tribute-to-talk-add-friends": "연락처에 등록된 지인은 트리뷰트 계약금 지불 없이 귀하에게 메시지를 보낼 수 있습니다.", + "tribute-to-talk-are-you-friends": "친구신가요?", + "tribute-to-talk-ask-to-be-added": "연락처 추가를 요청하세요", + "tribute-to-talk-contact-received-your-tribute": " 님이 귀하의 트리뷰트를 받았습니다. 귀하는 이제 연락처로 추가되어 자유롭게 채팅을 시작할 수 있습니다.", + "tribute-to-talk-desc": "새로운 사용자가 채팅을 신청하면 SNT를 요구해, 귀하의 시간에 금전적 가치를 부여하세요.", + "tribute-to-talk-disabled": "트리뷰트 투 토크 비활성화", + "tribute-to-talk-disabled-note": "지금부터 신규 사용자가 귀하에게 SNT를 보내지 않고도 채팅을 시작할 수 있습니다.", + "tribute-to-talk-enabled": "트리뷰트 투 토크를 활성화하셨습니다.", + "tribute-to-talk-finish-desc": "지금부터는 연락처에 등록된 사용자 및 트리뷰트를 지불한 사용자들로부터만 메시지를 받을 수 있습니다 ", + "tribute-to-talk-learn-more-1": "여러분의 시간은 가장 소중한 자산입니다. 트리뷰트 투 토크는 새로운 사용자가 귀하와 채팅을 시작하기 위해 필요한 SNT 계약금을 설정하여, 불필요한 메시지를 방지합니다.", + "tribute-to-talk-learn-more-2": "연락처에 없는 사용자는 귀하와 채팅을 시작하기 위해 트리뷰트를 지불해야 하며, 지불이 완료되면 귀하가 이에 응할 수 있습니다.", + "tribute-to-talk-learn-more-3": "받은 트리뷰트를 언제든지 되돌려 줄 수 있으나, 해당 사용자가 귀하에게 자유롭게 연락할 수 있도록 하려면 사용자를 연락처에 추가해주세요.", + "tribute-to-talk-paywall-learn-more-1": "시간이야말로 가장 소중한 자산입니다. 트리뷰트 투 토크는 새로운 사용자가 귀하와 채팅을 시작하기 위해 필요한 SNT 계약금을 설정하여, 불필요한 메시지를 방지합니다.", + "tribute-to-talk-paywall-learn-more-2": "트리뷰트를 설정한 사용자와 채팅을 시작하려 할 경우, 필요한 만큼 SNT를 지불하면 상대방의 연락처에 추가될 수 있습니다.", + "tribute-to-talk-paywall-learn-more-3": "만약 귀하가 이 사용자를 알고 있을 경우, 귀하의 프로필을 스테이터스 외부에 공유하면 무료로 추가하게 할 수 있습니다.", + "tribute-to-talk-pending": "트리뷰트 확인 대기중", + "tribute-to-talk-pending-note": "트리뷰트 트랜잭션이 네트워크에서 확인 대기 중입니다. 트랜잭션 기록에서 상태를 확인하실 수 있습니다", + "tribute-to-talk-removing-note": "트리뷰트 투 토크를 비활성화하면 새로운 사용자가 귀하에게 SNT 계약금 없이 메시지를 보낼 수 있습니다. 이 경우 트랜잭션 서명이 필요합니다.", + "tribute-to-talk-set-snt-amount": "새로운 사용자가 귀하와 채팅을 시작할 때 필요한 SNT 계약금을 설정합니다", + "tribute-to-talk-signing": "트랜잭션 서명 대기 중", + "tribute-to-talk-transaction-failed-note": "트랜잭션이 실패했습니다. 귀하의 트리뷰트 설정이 변경되지 않았습니다", + "tribute-to-talk-tribute-received1": "트리뷰트를 받았습니다. 당신과", + "tribute-to-talk-tribute-received2": "이제 상대방과 안전하게 채팅할 수 있습니다.", + "tribute-to-talk-you-require-snt": "귀하와 채팅을 시작하려는 새로운 사용자에게 SNT를 요구하세요.", + "try-again": "다시 시도", + "try-keeping-the-card-still": "카드와 기기의 접촉을 유지하세요", + "turn-nfc-description": "해당 기기에 NFC 기능이 비활성화되어있습니다. NFC 기능을 활성화해주세요", + "turn-nfc-on": "NFC를 켜 계속하기", + "two-minutes": "2분", + "tx-fail-description1": "이 트랜잭션은 실패할 수 있습니다. 사용자 지정 네트워크 수수료로 트랜잭션 실패 위험을 조절하세요.", + "tx-fail-description2": "이 트랜잭션은 실패할 수 있습니다. 직접 네트워크 수수료를 설정하여 실패 위험을 조절하세요.", + "type": "종류", + "type-a-message": "메시지", + "ulc-enabled": "ULC 사용", + "unable-to-fetch": "채팅 기록을 가져올 수 없습니다", + "unable-to-read-this-code": "코드를 읽을 수 없습니다", + "unable-to-send-messages": "메시지를 주고 받을 수 없습니다", + "unblock": "차단 해제", + "unblock-contact": "사용자 차단 해제", + "unknown-status-go-error": "알 수 없는 status-go 오류", + "unlimited": "무제한", + "unlock": "잠금 해제", + "unmute": "메시지 및 알림 수신", + "unpair-card": "카드 페어링 해제", + "unpair-card-confirmation": "이 작업은 현재 기기와 페어링을 해제합니다. 인증을 위해 6자리 패스코드가 필요합니다. 계속하시겠습니까?", + "unpair-keycard": "스마트폰에서 키카드 페어링 해제", + "unpair-keycard-warning": "페어링 코드 \/ PUK 및 PIN은 변경되지 않습니다", + "unpaired-keycard-text": "사용한 키카드가 이 스마트폰과 연동되어 있지 않습니다", + "unpaired-keycard-title": "카드가 페어링 되어 있지 않습니다", + "unpin": "고정 해제", + "update": "업데이트", + "update-to-listen-audio": "오디오 메시지를 들으려면 최신 버전으로 업데이트해주세요!", + "update-to-see-image": "멋진 이미지를 확인하려면 최신 버전으로 업데이트 해주세요!", + "update-to-see-sticker": "멋진 이모티콘을 확인하려면 최신 버전으로 업데이트해주세요!", + "updates-to-tos": "서비스 약관 업데이트", + "updates-to-tos-desc": "계속하기 전에 서비스 약관을 검토하고 앱 사용에 대한 책임이 사용자에게 있음을 확인해주세요", + "url": "URL", + "usd-currency": "USD", + "use-as-profile-picture": "프로필 사진으로 사용", + "use-valid-contact-code": "유효한 채팅 키 또는 사용자 이름을 입력하거나 스캔하세요", + "validation-amount-invalid-number": "수량이 잘못되었습니다", + "validation-amount-is-too-precise": "액수가 너무 정확합니다. 최대 소수점 자리수는 {{decimals}}입니다.", + "verified-community": "✓ 검증된 커뮤니티", + "version": "앱 버전", + "view": "보기", + "view-cryptokitties": "CryptoKitties에서 보기", + "view-cryptostrikers": "CryptoStrikers에서 보기", + "view-data": "데이터 보기", + "view-details": "세부 정보 보기", + "view-etheremon": "Etheremon에서보기", + "view-gitcoin": "Gitcoin에서 보기", + "view-on-opensea": "OpenSea에서 보기", + "view-profile": "프로필 보기", + "view-public-dashboard": "공공 대시보드 보기", + "view-rules": "규칙 보기", + "view-signing": "서명 구문 보기", + "view-superrare": "SuperRare에서 보기", + "waiting-for-wifi": "Wi-Fi 없음, 메시지 동기화 비활성화.", + "waiting-for-wifi-change": "설정", + "waiting-to-sign": "트랜잭션 서명 대기 중...", + "waiting-wi-fi": "와이파이 대기 중…", + "waku-bloom-filter-mode": "와쿠 블룸 필터 모드", + "wakuv2-change-nodes": "Wakuv2 노드를 변경하시겠습니까?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "와쿠 v2 설정", + "wallet": "지갑", + "wallet-address": "지갑 주소", + "wallet-asset": "자산", + "wallet-assets": "자산", + "wallet-backup-recovery-title": "시드 구문 백업", + "wallet-choose-recipient": "수신인 선택", + "wallet-collectibles": "수집품", + "wallet-connect": "월렛 커넥트", + "wallet-connect-2.0": "월렛 커넥트 2.0", + "wallet-connect-app-connected": "연결됨", + "wallet-connect-go-back": "브라우저 또는 디앱으로 돌아가기", + "wallet-connect-proposal-description": "연결하면 {{name}} 이(가) 귀하의 계정 주소를 검색하고 Web3를 활성화할 수 있습니다", + "wallet-connect-proposal-title": "지갑과 연결합니다", + "wallet-insufficient-funds": "자산 부족", + "wallet-insufficient-gas": "가스로 사용할 ETH가 부족합니다", + "wallet-invalid-address": "잘못된 주소: \n {{data}}", + "wallet-invalid-address-checksum": "잘못된 주소: \n {{data}}", + "wallet-invalid-chain-id": "네트워크가 일치하지 않습니다: \n {{data}}, 하지만 현재 체인은 {{chain}}입니다.", + "wallet-key-content": "0x로 시작하는 64글자 16진수 주소로 이더리움 표준을 기반으로 합니다. 계정 주소를 다른 사람들에게 공유하여 암호화폐 자산을 받을 수 있습니다. 지갑 키는 \"이더리움 주소\" 또는 \"지갑 주소\"라고도 합니다.", + "wallet-key-title": "계정 주소", + "wallet-manage-accounts": "계정 관리", + "wallet-manage-app-connections": "앱 연결 관리", + "wallet-manage-assets": "자산 관리", + "wallet-request": "요청", + "wallet-send": "보내기", + "wallet-send-min-units": "최소 21000 유닛", + "wallet-send-min-wei": "최소 1 wei", + "wallet-settings": "지갑 설정", + "wallet-total-value": "총 자산", + "wallet-transaction-total-fee": "총 비용", + "wants-to-access-profile": "이 프로필 접근을 요청합니다", + "warning": "경고", + "warning-message": "죄송합니다. 스팸을 방지하기 위해 여러 메시지를 빠르게 연속적으로 보내는 것을 제한하고 있습니다. 잠시 후 다시 시도하세요.", + "warning-sending-to-contract-descr": "입력하신 주소는 스마트 컨트랙트 주소로, 이 주소로 자산을 보내게 되면 사용자의 자산을 잃을 수 있습니다. 디앱과 소통하려면, 스테이터스 디앱 브라우저에서 디앱을 열어주세요.", + "watch-only": "열람 전용", + "wc-brand-guide": "상표, 로고 등 브랜딩 사용 안내", + "wc-disclaimer": "면책 조항 (타사 제공 업체 포함), 보증 및 법적 릴리스", + "wc-dispute": "분쟁 해결 조항", + "wc-how-to-use-status-app": "개인 정보 보호 및 보안을 포함한 스테이터스 앱 사용 방법", + "wc-new-tos-based-on-principles-prefix": "새로운 서비스 약관은 다음에 기반에 설계되었습니다", + "web-view-error": "페이지를 로드할 수 없습니다", + "websites": "웹사이트", + "webview-camera-permission-requests": "웹뷰 카메라 권한 요청", + "webview-camera-permission-requests-subtitle": "활성화되면 웹사이트와 디앱에서 카메라 사용을 요청할 수 있습니다", + "welcome-blank-community-message": "사용자의 커뮤니티가 여기에 표시됩니다.", + "welcome-blank-message": "사용자의 채팅 목록이 여기에 표시됩니다. ⊕ 버튼을 눌러 새로운 채팅을 시작해보세요", + "welcome-community-blank-message": "채널이 여기에 표시됩니다. 새 채널을 만들려면 ⊕ 버튼을 클릭하고 \"채널 만들기\"를 선택하세요.", + "welcome-community-blank-message-edit-chats": "채널이 여기에 표시됩니다. 새 채널을 만들려면 커뮤니티 화면으로 돌아가서 ⊕ 버튼을 클릭하고 \"채널 만들기\"를 선택하세요", + "welcome-screen-text": "지갑을 설정하고, 친구를 초대해 채팅하고 \n디앱을 브라우징하세요!", + "welcome-to-status": "환영합니다!", + "welcome-to-status-description": "암호화폐 지갑을 설정하고, 친구들을 초대해서 채팅하거나 디앱을 둘러보세요", + "what-changed": "변경된 사항", + "what-is-shared": "공유 내용", + "whats-on-your-mind": "무엇을 공유하고 싶으신가요?", + "word-count": "단어 수", + "word-n": "{{number}}번째 단어", + "word-n-description": "시드 구문을 정확히 백업했는지 확인하기 위해, {{number}}번째 단어를 입력해주세요.", + "words-n": { + "other": "단어" + }, + "write-down-and-store-securely": "코드를 메모해 두고\n안전하게 보관하세요", + "wrong-address": "잘못된 주소", + "wrong-card": "잘못된 카드", + "wrong-card-text": "사용한 카드와 키가 호환되지 않습니다.", + "wrong-contract": "잘못된 컨트랙트", + "wrong-keycard-text": "사용한 키카드가 이 스마트폰과 연동되어 있지 않습니다", + "wrong-keycard-title": "잘못된 키카드를\n사용하셨습니다", + "wrong-password": "잘못된 비밀번호", + "wrong-word": "잘못된 단어", + "yes": "예", + "you": "사용자", + "you-already-have-an-asset": "이미 {{value}} 자산을 가지고 있습니다", + "you-are-all-set": "설정이 완료되었습니다!", + "you-are-all-set-description": "스마트폰을 분실한 경우, 시드 구문을 사용하여 계정과 자산에 액세스할 수 있습니다", + "you-can-change-account": "계정 이름과 색상을 원하는대로 변경할 수 있습니다", + "you-can-choose-preview-websites": "다음 목록에서 채팅 내 링크의 설명 및 이미지 미리보기 기능을 선택적으로 활성화할 수 있습니다", + "you-can-fetch": "채팅 기록을 가져올 수 있습니다", + "you-dont-have-contacts": "아직 연락처가 없습니다.", + "you-dont-have-contacts-invite-friends": "아직 연락처가 없습니다. \n친구들을 초대하고 채팅을 시작하세요.", + "you-dont-have-stickers": "아직 이모티콘이 없습니다", + "you-will-need-this-code": "이 코드는 스테이터스 및 트랜잭션 서명에 필요합니다", + "you-will-start-from-scratch": "처음부터 새로운 키로 다시 시작합니다", + "your-card-is-frozen": "키카드가 잠겼습니다. 카드 접근 권한을 초기화하세요", + "your-contact-code": "디앱에 접근 권한을 부여하여 채팅 키를 검색합니다", + "your-data-belongs-to-you": "시드 구문을 잃어버리면 데이터와 자산을 분실하게 됩니다", + "your-data-belongs-to-you-description": "스마트폰을 분실하거나 접근 권한을 상실한 경우, 이 시드 구문을 통해 계정에 접근할 수 있습니다. 오로지 본인만이 이 시드 구문을 관리할 수 있습니다. 메모해두고 안전하게 관리하시기 바랍니다.", + "your-keys": "키 목록", + "your-price-limit": "가격 한도", + "your-recovery-phrase": "내 시드 구문", + "your-recovery-phrase-description": "위 12단어가 사용자의 시드 구문입니다. 이 구문은 사용자의 지갑을 증명하기 위해 반드시 필요하며, 이번 한 번만 확인할 수 있습니다. 지갑을 분실하거나 재설치하는 경우 반드시 필요하므로, 종이에 적어두고 안전한 장소에 보관하세요.", + "your-tip-limit": "팁 한도", + "youre-on-mobile-network": "모바일 네트워크를 사용 중입니다" +} diff --git a/translations/la.json b/translations/la.json new file mode 100644 index 00000000000..0b7c1681301 --- /dev/null +++ b/translations/la.json @@ -0,0 +1,94 @@ +{ + "amount": "summa", + "members-active": { + "one": "I soladis, I aktive", + "other": "{{count}} sodalis, {{count}} aktivi", + "zero": "nullus sodalis" + }, + "chat-name": "nomen colloquii", + "phew-here-is-your-passphrase": "*uph* erat gravor. hic phrasis arcana sui est, *exscribe et adserva!* necesse est, ut benificium sui recreare.", + "chat-settings": "moderationes colloquiorum", + "offline": "abesse", + "invited": "invitavisse", + "address": "adloquium", + "datetime-hour": { + "one": "hora", + "other": "horae" + }, + "datetime-ago-format": "{{ago}} {{number}} {{time-intervals}}", + "remove": "amovere", + "add-members": "sodalis adjungere", + "done": "paratus", + "new-group-chat": "novum colloquium circli", + "datetime-yesterday": "heri", + "datetime-ago": "anto", + "contacts": "contagiis", + "active-online": "adesse", + "password": "signum arcanum", + "discover": "invenite", + "intro-status": "habere colloquium cum me, ut syngraphus sui congerit et moderationes sui mutare!", + "name": "nomen", + "show-qr": "QR-nota elucere", + "connect": "iungere", + "edit": "commutare", + "account-generation-message": "mane! cumputo benificium sui.", + "no-messages": "ne nuntii", + "passphrase": "phrasis arcana", + "recipient": "acceptor", + "members-title": "sodalis", + "settings": "moderationes", + "chats": "colloquia", + "image-source-make-photo": "photographere", + "save": "apothecare", + "sync-in-progress": "synchronizari...", + "incorrect-code": [ + "str", + "nos paenite, nota falsa, perscribe denuo" + ], + "image-source-gallery": "eligere ad pinacothecae", + "sync-synced": "synchronizatum esse", + "status-pending": "tractare", + "datetime-day": { + "one": "dies", + "other": "dies" + }, + "scan-qr": "QR-nota photographere", + "recent": "novae", + "status": "status", + "wrong-password": "falsum signum acranum", + "clear-history": "processus amovere", + "no-contacts": "Nulla contagio", + "datetime-today": "hodie", + "web-view-error": "Erratum ad adspectus", + "can-not-add-yourself": "ne potest adjungere ipse!", + "add-to-contacts": "adicere ad contagio", + "available": "relicus", + "You": "tu", + "main-wallet": "crumera caput", + "members": { + "one": "I sodalis", + "other": "{{count}} sodalis", + "zero": "nullus sodalis" + }, + "intro-message1": "salve! hic Status est!.\n attige id nuntium, ut signum arcanum sui congere et incipere!", + "new-contact": "novum contagium", + "datetime-second": { + "one": "pars minuta secunda horae", + "other": "pars minuta secunda horae" + }, + "recover": "recreare", + "datetime-minute": { + "one": "pars minuta horae", + "other": "pars minuta horae" + }, + "active-unknown": "incognitus", + "public-key": "publica clavis", + "profile": "biographia", + "none": "nihil", + "removed": "reliquisse", + "message": "nuntium", + "here-is-your-passphrase": "hic phrasis arcana sui est, *exscribe et adserva!* necesse est, ut benificium sui recreare.", + "image-source-title": "photographia bigraphiae", + "status-sent": "deditum iri", + "left": "desse" +} diff --git a/translations/lt.json b/translations/lt.json new file mode 100644 index 00000000000..709f34e0e94 --- /dev/null +++ b/translations/lt.json @@ -0,0 +1,196 @@ +{ + "validation-amount-invalid-number": "Kiekio skaičius netinkamas", + "transaction-details": "Sandėrių detalės", + "confirm": "Patvirtinti", + "description": "Aprašymas", + "amount": "Paskyra", + "open": "Atidaryti", + "close-app-title": "Įspėjimas!", + "members-active": { + "one": "1 narys", + "other": "{{count}} nariai", + "zero": "jokių narių" + }, + "chat-name": "Pokalbio pavadinimas", + "phew-here-is-your-passphrase": "Vajė, buvo sunkoka, čia yra jūsų slaptažodis, *išsaugokite jį!* jeigu reikės atkurti jūsų paskyrą.", + "public-group-topic": "Tema", + "chat-settings": "Pokalbio nustatymai", + "offline": "Neprisijungęs", + "invited": "pakviestas", + "address": "Adresas", + "new-public-group-chat": "Jungtis prie grupinio pokalbio", + "datetime-hour": { + "one": "valanda", + "other": "valandos" + }, + "wallet-settings": "Piniginės nustatymai", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "close-app-button": "Patvirtinti", + "block": "Blokas", + "camera-access-error": "Norėdami suteikti reikalingą leidimą filmuoti, eikite į savo sistemos nustatymus ir įsitikinkite, kad pasirinktas Status > Camera.", + "wallet-invalid-address": "Neteisingas adresas: \n {{data}}", + "remove": "Pašalinti", + "add-members": "Pridėti narių", + "yes": "Taip", + "dapps": "DApp'ai", + "network-settings": "Tinklo nustatymai", + "photos-access-error": "Norėdami suteikti reikalingą leidimą fotografuoti, eikite į savo sistemos nustatymus ir įsitikinkite, kad pasirinktas Status > Photo.", + "hash": "Hash", + "done": "Baigta", + "close-chat": "Trinti pokalbį", + "new-group-chat": "Naujas grupinis pokalbis", + "wallet": "Piniginė", + "wallet-request": "Užklausa", + "sign-in": "Prisijungti", + "datetime-yesterday": "vakar", + "create-new-account": "Sukurti naują paskyrą", + "datetime-ago": "prieš", + "contacts": "Kontaktai", + "got-it": "Gavo", + "public-chats": "Vieši pokalbiai", + "not-applicable": "Netaikoma nepasirašytiems sandėriams", + "active-online": "Prisijungęs", + "password": "Slaptažodis", + "send-request": "Siųsti užklausą", + "paste-json": "Įklijuoti JSON", + "browsing-title": "Naršyti", + "transactions-history-empty": "Tu neturi sandorių istorijos", + "discover": "Atrasti", + "browsing-cancel": "Naršyti", + "intro-status": "Kalbėkite su manimi, kad nustatytumėte savo paskyrą ir pakeistumėte savo nustatymus!", + "name": "Vardas", + "gas-price": "Gas kaina", + "validation-amount-is-too-precise": "Suma yra per daug tiksli. Mažiausias vienetas, kurį galite siųsti, yra 1 Wei (1x10^-18 ETH)", + "copy-transaction-hash": "Kopijuoti sandėrio hash", + "show-qr": "Rodyti QR", + "connect": "Jungtis", + "edit": "Redaguoti", + "account-generation-message": "Sekundėlę, vyksta nemenki skaičiavimai! Generuojama jūsų paskyra!", + "remove-network": "Nutolęs tinklas", + "no-messages": "Jokių žinučių", + "passphrase": "Slaptafrazė", + "recipient": "Gavėjas", + "members-title": "Nariams", + "new-group": "Nauja grupė", + "phone-e164": "Tarptautinis 1", + "connected": "Jungtis", + "rpc-url": "RPC URL", + "settings": "Nustatymai", + "specify-rpc-url": "Nurodykite RPC URL", + "gas-limit": "Gas limitas", + "delete": "Ištrinti", + "chats": "Pokalbiai", + "transaction-sent": "Sandėris išsiųstas", + "transaction": "Sandėris", + "public-group-status": "Viešas", + "image-source-make-photo": "Fotografuoti", + "chat": "Pokalbis", + "start-conversation": "Pradėti pokalbį", + "save": "išsaugoti", + "all": "Viskas", + "confirmations-helper-text": "Jei norite būti tikri, kad jūsų sandoris nebus kompromituotas, palaukite, kol jis gaus mažiausiai 10 blokų patvirtinimą", + "sharing-copy-to-clipboard": "Kopijuoti į iškarpinę", + "sync-in-progress": "Sinchronizuojama...", + "enter-password": "Įveskite slaptažodį", + "send-transaction": "Siųsti sandėrį", + "confirmations": "Patvirtinimas", + "incorrect-code": [ + "str", + "Atsiprašome, kodas neteisingas, prašome įvesti jį dar kartą" + ], + "image-source-gallery": "Pasirinkti iš galerijos", + "sync-synced": "Sinchronizuota", + "currency": "Valiuta", + "status-pending": "Laukiama", + "connecting-requires-login": "Prisijungimas prie kito tinklo reikalauja prisijungimo", + "datetime-day": { + "one": "diena", + "other": "dienos" + }, + "request-transaction": "Reikalauti sandėrio", + "wallet-send": "Siųsti", + "scan-qr": "Skanuoti QR", + "contact-s": { + "one": "kontaktas", + "other": "kontaktai" + }, + "gas-used": "Gas sunaudota", + "type": "Tipas", + "next": "Sekantis", + "recent": "Paskiausios būsenos", + "open-on-etherscan": "Atidaryti Etherscan", + "share": "Dalintis", + "status": "Status", + "from": "Iš", + "wrong-password": "Neteisingas slaptažodis", + "in-contacts": "Kontaktuose", + "transactions-sign": "Pasirašyti", + "sharing-share": "Dalintis...", + "type-a-message": "Įveskite žinutę...", + "usd-currency": "USD", + "url": "URL", + "add-network": "Pridėti tinklą", + "unknown-status-go-error": "Nežinoma status-go klaida", + "clear-history": "Naikinti istoriją", + "no-contacts": "Jokių kontaktų nerasta", + "soon": "Greitai", + "close-app-content": "Programa bus uždaryta. Kai iš naujo paleisite ją, pasirinktas tinklas bus naudojamas.", + "datetime-today": "šiandien", + "web-view-error": "vajė, klaida", + "error": "Klaida", + "more": "daugiau", + "cancel": "Atšaukti", + "can-not-add-yourself": "Negalima pridėti savęs", + "transaction-description": "Jei norite būti tikri, kad jūsų sandoris nebus kompromituotas, palaukite, kol jis gaus mažiausiai 10 blokų patvirtinimą", + "add-to-contacts": "Pridėti prie kontaktų", + "available": "Prieinama", + "You": "Tu", + "main-wallet": "Pagrindinė piniginė", + "transactions": "Sandėriai", + "members": { + "one": "1 narys", + "other": "{{count}} nariai", + "zero": "jokių narių" + }, + "intro-message1": "Sveiki atvykę į Status!\nPalieskite šį pranešimą, kad nustatytumėte slaptažodį ir pradėtumėte!", + "eth": "ETH", + "transactions-history": "Istorija", + "new-contact": "Nauji kontaktai", + "datetime-second": { + "one": "sekundė", + "other": "sekundės" + }, + "recover": "Atkurti", + "wallet-total-value": "Bendra vertė", + "nonce": "Nonce", + "new-network": "Naujas tinklas", + "datetime-minute": { + "one": "minutė", + "other": "minutės" + }, + "browsing-open-in-android-web-browser": "Atverti naršyklėje", + "browsing-open-in-ios-web-browser": "Atverti naršyklėje", + "wallet-insufficient-funds": "Nepakankamos lėšos", + "edit-profile": "Redaguoti profilį", + "active-unknown": "Nežinomas", + "wallet-invalid-chain-id": "Tinklas nesutampa su: \n {{data}}", + "transaction-failed": "Sandoris nepavyko", + "public-key": "Viešasis raktas", + "profile": "Profilis", + "wallet-choose-recipient": "Pasirinkti gavėją", + "none": "Jokio", + "removed": "perkeltas", + "no": "Ne", + "transactions-filter-select-all": "Rinktis viską", + "transactions-filter-title": "Filtruoti istoriją", + "message": "Žinutė", + "here-is-your-passphrase": "Čia yra jūsų slaptafrazė, *išsaugokite ją!* jeigu reikės atkurti jūsų paskyrą.", + "wallet-assets": "Aktyvai", + "image-source-title": "Profilio atvaizdas", + "current-network": "Dabartinis tinklas", + "left": "išėjo", + "to": "Į", + "data": "Duomenys", + "status-sent": "Nusiųsta", + "cost-fee": "Kaina/Mokestis" +} diff --git a/translations/lv.json b/translations/lv.json new file mode 100644 index 00000000000..b8d6977c53e --- /dev/null +++ b/translations/lv.json @@ -0,0 +1,131 @@ +{ + "confirm": "Apstiprināt", + "amount": "Summa", + "members-active": { + "one": "1 dalībnieks", + "other": "{{count}} dalībnieki", + "zero": "nav dalībnieku" + }, + "chat-name": "Čata nosaukums", + "phew-here-is-your-passphrase": "*Fuh* tas bija grūti, re, te ir tavs jauns passphrase, *pieraksti un sargā to!* Tev viņš būs vajadzīgs lai atgūtu pieju.", + "public-group-topic": "Temats", + "chat-settings": "Čata iestatījumi", + "offline": "Offline", + "invited": "uzaicināts", + "address": "Adrese", + "new-public-group-chat": "Pievienoties publiskajam čatam", + "datetime-hour": { + "one": "stunda", + "other": "stundas" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "Kļūda, nav atļaujas piekļūt kamerai. Lūdzu, iestatījumos pārliecinies, ka Status > Camera ir izvēlēts.", + "remove": "Noņemt", + "add-members": "Pievienot biedrus", + "photos-access-error": "Kļūda, nav atļaujas piekļūt fotogrāfijām. Lūdzu, iestatījumos pārliecinies, ka Status > Photos ir izvēlēts.", + "done": "Darīts", + "close-chat": "Dzēst čatu", + "new-group-chat": "Jauna grupa", + "sign-in": "Ieiet", + "datetime-yesterday": "vakar", + "create-new-account": "Izveidot jaunu kontu", + "datetime-ago": "pirms", + "contacts": "Kontakti", + "got-it": "Got it", + "active-online": "Online", + "password": "Parole", + "browsing-title": "Browse", + "discover": "Discover", + "browsing-cancel": "Atcelt", + "intro-status": "Čato ar mani, ja gribi mainīt ustatījumus!", + "name": "Vārds", + "show-qr": "Parādīt QR", + "connect": "Savienoties", + "edit": "Rediģēt", + "account-generation-message": "Lūdzu uzgaidi...", + "no-messages": "Nav īsziņu", + "passphrase": "Passphrase", + "recipient": "Saņēmējs", + "members-title": "Dalībnieki", + "new-group": "Jauna grupa", + "phone-e164": "International 1", + "settings": "Iestatījumi", + "delete": "Dzēst", + "chats": "Čati", + "transaction": "Transakcija", + "public-group-status": "Publisks", + "image-source-make-photo": "Fotografēt", + "start-conversation": "Sākt sarunu", + "save": "Saglabāt", + "sharing-copy-to-clipboard": "Copy to clipboard", + "sync-in-progress": "Sinhronizē...", + "send-transaction": "Sūtīt transakciju", + "incorrect-code": [ + "str", + "Nepareizs kods, mēģiniet vēlreiz" + ], + "image-source-gallery": "Izvēlēties no galerijas", + "sync-synced": "Sinhronizēts", + "status-pending": "Neizlemts", + "datetime-day": { + "one": "diena", + "other": "dienas" + }, + "scan-qr": "Skanēt QR", + "contact-s": { + "one": "kontakts", + "other": "kontakti" + }, + "next": "Tālāk", + "recent": "Recent", + "status": "Status", + "from": "No", + "wrong-password": "Parole ievadīta nepareizi", + "in-contacts": "Kontaktos", + "sharing-share": "Dalīties...", + "type-a-message": "Raksti īsziņu...", + "clear-history": "Dzēst vēsturi", + "no-contacts": "Kontaktu nav", + "datetime-today": "šodien", + "web-view-error": "ups, kļūda", + "error": "Kļūda", + "more": "vairāk", + "cancel": "Atcelt", + "can-not-add-yourself": "Tu nevari pievienot sevi", + "add-to-contacts": "Pievieno kontaktu", + "available": "Pieejams", + "You": "Tu", + "main-wallet": "Galvenais maks", + "members": { + "one": "1 dalībnieks", + "other": "{{count}} dalībnieki", + "zero": "nav dalībnieku" + }, + "intro-message1": "Sveicināti! Uzspied lai sāktu!", + "new-contact": "Jauns kontakts", + "datetime-second": { + "one": "sekunde", + "other": "sekundes" + }, + "recover": "Atgūt", + "datetime-minute": { + "one": "minūte", + "other": "minūtes" + }, + "browsing-open-in-android-web-browser": "Atvert pārlūkprogrammā", + "browsing-open-in-ios-web-browser": "Atvert pārlūkprogrammā", + "edit-profile": "Rediģēt profilu", + "active-unknown": "Nezināms", + "public-key": "Public key", + "profile": "Profils", + "none": "None", + "removed": "noņemts", + "message": "Īsziņa", + "here-is-your-passphrase": "Te ir tavs jauns passphrase, *pieraksti un sargā to!* Tev viņš būs vajadzīgs lai atgūtu pieju savam kontam.", + "image-source-title": "Profila bilde", + "current-network": "Tīkls", + "left": "atstāja grupu", + "to": "Kam", + "status-sent": "Aizsūtīts", + "data": "Dati" +} diff --git a/translations/ms.json b/translations/ms.json new file mode 100644 index 00000000000..0a5541872f9 --- /dev/null +++ b/translations/ms.json @@ -0,0 +1,1630 @@ +{ + "You": "Anda", + "about-app": "Mengenai", + "about-key-storage-content": "Status tidak akan mengakses kunci peribadi anda. Pastikan anda membuat cadangan frasa benih anda. Sekiranya anda kehilangan telefon, itu satu-satunya cara untuk mengakses kunci anda.", + "about-key-storage-title": "Mengenai penyimpanan kunci", + "about-names-content": "Tidak ada yang boleh berpura-pura menjadi anda! Anda tidak dikenali secara lalai dan tidak perlu mendedahkan nama sebenar anda. Anda boleh mendaftarkan nama khas dengan sedikit bayaran.", + "about-names-title": "Nama tidak boleh diubah", + "about-sharing-data": "Mengenai perkongsian data", + "accept": "Terima", + "accept-and-add": "Terima dan tambah", + "accept-and-continue": "Terima dan teruskan", + "accept-and-share-address": "Terima dan kongsi alamat", + "accept-new-chats-from": "Terima sembang baru daripada", + "accept-status-tos-prefix": "Saya terima Status", + "access-existing-keys": "Akses kunci yang ada", + "access-key": "Kunci akses", + "account-added": "Akaun ditambahkan", + "account-color": "Warna akaun", + "account-content": "Anda boleh membandingkan akaun dalam Status dengan akaun bank. Seperti akaun bank, akaun biasanya mempunyai alamat dan baki; Anda menggunakan akaun ini untuk bertransaksi di Ethereum. Anda boleh mempunyai banyak akaun di dompet anda. Semua diakses dengan membuka kunci Status.", + "account-exists-title": "akaun telah wujud", + "account-is-used": "Akaun sedang digunakan dengan Dapps dalam pelayar.", + "account-name": "Nama akaun", + "account-settings": "Tetapan akaun", + "account-title": "Akaun", + "accounts": "Akaun", + "actions": "Tindakan", + "active-online": "Dalam talian", + "active-unknown": "Tidak diketahui", + "activity": "Aktiviti", + "add": "Tambah", + "add-a-watch-account": "Tambahkan alamat jam tangan sahaja", + "add-account": "Tambahkan akaun", + "add-account-description": "Anda boleh mengimport sebarang jenis akaun Ethereum untuk menambahkannya ke dompet Status anda", + "add-account-incorrect-password": "Kata laluan nampaknya tidak betul. Masukkan kata laluan yang anda gunakan untuk membuka kunci aplikasi.", + "add-an-account": "Tambahkan akaun", + "add-bootnode": "Tambah bootnode", + "add-contact": "Tambahkan kenalan", + "add-custom-token": "Tambahkan token tersuai", + "add-favourite": "Tambah kegemaran", + "add-mailserver": "Tambahkan nod sejarah", + "add-members": "Tambah ahli", + "add-network": "Tambah rangkaian", + "add-new-contact": "Tambah kenalan baru", + "add-nickname": "Tambahkan nama panggilan (pilihan)", + "add-node": "Tambah nod", + "add-private-key-account": "Tambahkan akaun dari kunci peribadi", + "add-seed-account": "Tambahkan akaun dengan frasa biji", + "add-to-contacts": "Tambahkan ke kenalan", + "add-to-contacts-text": "Dengan menambahkan pengguna ke senarai kenalan anda, anda berkongsi alamat dompet anda", + "add-to-favourites": "Tambah ke kegemaran", + "add-watch-account": "Tambahkan akaun jam tangan sahaja", + "address": "Alamat", + "address-or-ens-name": "Alamat atau nama ENS", + "address-received": "Alamat diterima", + "address-request-accepted": "Permintaan alamat diterima", + "address-requested": "Alamat diminta", + "advanced": "Mahir", + "advanced-settings": "Tetapan lanjutan", + "advertiser-description": "Anda telah menemui Status menerusi rakan kongsi. Adakah anda keberatan jika Status memeriksa alamat IP anda apabila rakan kongsi anda menerima ganjaran? Maklumat ini tidak akan digunakan untuk perkara lain dan akan dibuang sepenuhnya selepas 7 hari.", + "advertiser-starter-pack-accept": "Terima", + "advertiser-starter-pack-decline": "Tolak", + "advertiser-starter-pack-description": "Berikut adalah beberapa kripto sebagai permulaan! Gunakannya untuk mendapatkan pelekat, nama ENS dan mencuba dapps", + "advertiser-starter-pack-title": "Pek Permulaan", + "advertiser-title": "Dasar privasi", + "agree-by-continuing": "Dengan meneruskan anda bersetuju\n kepada kita", + "all": "Semua", + "allow": "Benarkan", + "allow-and-send": "Benarkan dan hantar", + "allow-mention-notifications": "Tunjukkan @ sebutan", + "allowing-authorizes-this-dapp": "Membolehkan membenarkan DApp ini untuk mengambil alamat dompet anda dan mengaktifkan Web3", + "already-have-asset": "Anda sudah mempunyai aset ini", + "amount": "Jumlah", + "anyone": "Sesiapa", + "appearance": "Penampilan", + "apply": "Memohon", + "are-you-sure": "Adakah anda pasti?", + "are-you-sure-description": "Anda tidak akan dapat melihat frasa benih lagi", + "are-you-sure-to-cancel": "Adakah anda pasti mahu membatalkan?", + "are-you-sure?": "Adakah anda pasti?", + "ask-in-status": "Kemukakan soalan atau laporkan pepijat", + "at": "di", + "attribution-received": "{{attrib}} daripada {{max}} bonus telah diterima", + "audio": "Audio", + "audio-recorder": "Perakam", + "audio-recorder-error": "Kesalahan perakam", + "audio-recorder-max-ms-reached": "Masa rakaman maksimum dicapai", + "audio-recorder-permissions-error": "Anda harus memberi kebenaran untuk menghantar mesej audio", + "authorize": "Memberi kebenaran", + "available": "Ada", + "available-participants": { + "other": "Anda boleh memilih {{count}} lagi peserta" + }, + "back": "Belakang", + "back-up": "Sandarkan", + "back-up-seed-phrase": "Cadangan frasa biji", + "back-up-your-seed-phrase": "Sandarkan frasa benih anda", + "backup-disabled": "Dinyahaktifkan", + "backup-recovery-phrase": "Sandar frasa pemulihan", + "bad-fees-description": "Bayaran keutamaan anda adalah di bawah parameter yang kami cadangkan.", + "balance": "Seimbang", + "begin-set-up": "Mulakan persediaan", + "below-base-fee": "bayaran maksimum di bawah bayaran asas", + "biometric-auth-android-sensor-desc": "Sensor sentuh", + "biometric-auth-android-sensor-error-desc": "Gagal", + "biometric-auth-android-title": "Pengesahan diperlukan", + "biometric-auth-confirm-logout": "Relogin", + "biometric-auth-confirm-message": "Pengesahan biometrik diperlukan untuk meneruskan, jika tidak mungkin, buka kunci kunci anda dengan kata laluan atau kod laluan anda", + "biometric-auth-confirm-title": "Anda mesti mengesahkan!", + "biometric-auth-confirm-try-again": "Cuba lagi", + "biometric-auth-error": "Tidak dapat melakukan pengesahan biometrik ( {{code}} )", + "biometric-auth-login-error-title": "Kesalahan pengesahan biometrik", + "biometric-auth-login-ios-fallback-label": "Masukkan kata laluan", + "biometric-auth-reason-login": "Log masuk dalam Status", + "biometric-auth-reason-verify": "Sahkan pengesahan", + "biometric-disable-bioauth": "nyahdayakan {{bio-type-label}}", + "biometric-disable-password-description": "Sekiranya anda melumpuhkan ini, anda juga akan", + "biometric-disable-password-title": "Lumpuhkan simpanan kata laluan", + "biometric-enable": "Jika anda tidak mahu memasukkan kata laluan anda setiap kali untuk mencapai aplikasi, dayakan {{bio-type-label}} daftar masuk", + "biometric-enable-button": "Dayakan {{bio-type-label}}", + "biometric-enable-keycard": "Sekiranya anda tidak mahu menggunakan Kad Kunci anda setiap kali untuk mengakses aplikasi, aktifkan log masuk {{bio-type-label}}", + "biometric-faceid": "ID Muka", + "biometric-fingerprint": "Cap Jari", + "biometric-secure-with": "Selamat dengan {{bio-type-label}}", + "biometric-touchid": "ID Sentuh", + "bip39-password-placeholder": "Kata laluan BIP39", + "blank-keycard-text": "Anda boleh meneruskan kad kekunci anda setelah membuat kunci dan nama", + "blank-keycard-title": "Nampaknya anda sudah mengetuk\n kad kekunci kosong", + "block": "Sekat", + "block-contact": "Sekat pengguna ini", + "block-contact-details": "Penyekatan akan menghapuskan mesej terdahulu pengguna ini dan menghentikan yang baru daripada menghubungi anda", + "blocked-users": "Pengguna yang disekat", + "bootnode-address": "Alamat bootnode", + "bootnode-details": "Butiran but but", + "bootnode-format": "enode: \/\/ {enode-id} @ {ip-address} : {port}", + "bootnodes": "Kod but", + "bootnodes-enabled": "Bootnode didayakan", + "bootnodes-settings": "Tetapan kod but", + "browsed-websites": "Sejarah penyemak imbas akan muncul di sini", + "browser": "Browser", + "browser-not-secure": "Sambungan tidak selamat! Jangan menandatangani transaksi atau menghantar data peribadi di laman web ini.", + "browser-secure": "Sambungan selamat. Pastikan anda benar-benar mempercayai laman web ini sebelum menandatangani transaksi atau memasukkan data peribadi.", + "browsers": "Browser", + "browsing-cancel": "Membatalkan", + "browsing-open-in-android-web-browser": "Buka di Android", + "browsing-open-in-ios-web-browser": "Buka di iOS", + "browsing-open-in-status": "Buka dalam Status", + "browsing-site-blocked-description1": "Kami mengesan kemungkinan terdapatnya aktiviti berbahaya dari alamat ini. Untuk melindungi anda dan dompet anda, kami menghalang navigasi lebih jauh. \n\n Sekiranya anda rasa ini adalah kesalahan, beri tahu kami di", + "browsing-site-blocked-description2": "sembang awam.", + "browsing-site-blocked-go-back": "Pergi balik", + "browsing-site-blocked-title": "Laman web ini disekat", + "browsing-title": "Layari", + "bug-report": "Laporkan pepijat", + "build-yourself": "Untuk menggunakan aplikasi tanpa Terma Penggunaan ini, anda boleh membina versi anda sendiri", + "buy-crypto": "Beli kripto", + "buy-crypto-choose-a-service": "Pilih perkhidmatan yang anda ingin gunakan untuk membeli kripto", + "buy-crypto-description": "Cari dapp untuk membeli kripto sekarang", + "buy-crypto-leaving": "Anda akan meninggalkan Status dan memasuki laman web pihak ketiga untuk menyelesaikan pembelian anda", + "buy-crypto-title": "Nampaknya dompet anda kosong", + "camera-access-error": "Untuk memberikan izin kamera yang diperlukan, pergi ke tetapan sistem anda dan pastikan Status > Kamera dipilih.", + "can-not-add-yourself": "Itu ialah anda, pilih orang lain untuk memulakan perbualan", + "can-send-messages": "Anda boleh menghantar dan menerima mesej-mesej baru", + "cancel": "Membatalkan", + "cancel-keycard-setup": "Batalkan penyediaan Kad Kekunci", + "cancelling": "Membatalkan", + "cannot-read-card": "Tidak dapat membaca kad.\n Tahan di bahagian belakang telefon anda", + "cannot-use-default-pin": "Kod laluan 000000 tidak dibenarkan. \nSila gunakan nombor lain", + "cant-open-public-chat": "Tidak dapat membuka sembang awam", + "cant-report-bug": "Tidak dapat melaporkan pepijat", + "card-is-blank": "Kad ini kosong", + "card-reseted": "Kad telah ditetapkan semula", + "card-unpaired": "Kad telah dinyahpasang daripada peranti semasa", + "category": "Kategori", + "category-title": "Tajuk kategori", + "change-fleet": "Tukar armada menjadi {{fleet}}", + "change-logging-enabled": "Adakah anda pasti mahu {{enable}} log?", + "change-pairing": "Tukar kod berpasangan", + "change-pairing-description": "Mengubah kod pasangan tidak menjejaskan pasangan semasa. Walau bagaimanapun, sebarang pasangan baru akan memerlukan kod baru.", + "change-pairing-title": "Buat kod pasangan baru", + "change-passcode": "Tukar Kod Laluan", + "change-password": "Tukar Kata Laluan", + "change-pin": "Tukar kod laluan 6-digit", + "change-puk": "Tukar PUK 12 digit", + "change-tip": "Tukar tip", + "changed-amount-warning": "Jumlah diubah dari {{old}} menjadi {{new}}", + "changed-asset-warning": "Aset ditukar dari {{old}} menjadi {{new}}", + "chaos-mode": "Mod kekacauan", + "chaos-unicorn-day": "Hari Kekacauan Unicorn", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Berbual", + "chat-and-transact": "Berbual dan bertransaksi secara peribadi dengan rakan", + "chat-is-a-contact": "Hubungi", + "chat-is-not-a-contact": "Bukan kenalan", + "chat-key": "Kekunci sembang", + "chat-key-content": "Mesej pada protokol sembang Status dihantar dan diterima menggunakan kunci penyulitan. Kunci sembang awam adalah rentetan watak yang anda kongsi dengan orang lain supaya mereka dapat menghantar mesej kepada anda dalam Status", + "chat-key-title": "Kunci Sembang", + "chat-link-previews": "Pratonton pautan sembang", + "chat-name": "Nama Chat", + "chat-name-content": "Tiga perkataan rawak, diambil secara algoritma dari kekunci sembang anda dan digunakan sebagai alias lalai anda dalam sembang. Nama sembang benar-benar unik; tidak ada pengguna lain yang boleh mempunyai tiga perkataan yang sama.", + "chat-name-title": "nama Chat", + "chat-notification-preferences": "Tetapan pemberitahuan", + "chat-settings": "Tetapan sembang", + "chats": "Sembang", + "check-on-opensea": "Semak opensea", + "check-your-recovery-phrase": "Semak frasa pemulihan anda", + "choose-actions": "Pilih tindakan", + "choose-authentication-method": "Pilih kaedah pengesahan", + "choose-new-location-for-keystore": "Pilih lokasi baru untuk menyimpan fail kunci anda", + "choose-storage": "Pilih simpanan", + "clear": "Jelas", + "clear-all": "Kosongkan semua", + "clear-history": "Sejarah yang jelas", + "clear-history-action": "Jelas", + "clear-history-confirmation": "Hapus sejarah?", + "clear-history-confirmation-content": "Adakah anda pasti mahu membersihkan sejarah sembang ini?", + "clear-history-title": "Hapus sejarah?", + "close": "Tutup", + "close-all": "Tutup semua", + "close-app-button": "Sahkan", + "close-app-content": "Aplikasi akan berhenti dan ditutup. Apabila anda membukanya semula, rangkaian yang dipilih akan digunakan", + "close-app-title": "Amaran!", + "command-button-send": "Hantar", + "communities": "Komuniti", + "communities-alpha": "Komuniti (alpha)", + "communities-enabled": "Komuniti didayakan", + "communities-verified": "✓ Komuniti Status Yang Disahkan", + "community": "Komuniti", + "community-color": "Warna komuniti", + "community-color-placeholder": "Pilih warna", + "community-edit-title": "Edit komuniti", + "community-emoji-thumbnail-title": "Thumbnail", + "community-image-pick": "Pilih gambar", + "community-image-remove": "Buang", + "community-image-take": "Ambil gambar", + "community-info": "Maklumat komuniti", + "community-info-not-found": "Maklumat komuniti tidak dijumpai", + "community-invite-title": "Jemput", + "community-key": "Kunci peribadi komuniti", + "community-key-placeholder": "Taipkan kunci peribadi komuniti anda", + "community-link": "Pautan komuniti", + "community-members-title": "Ahli", + "community-message-preview": "Jemputan untuk menyertai {{community-name}}", + "community-private-key": "Kunci peribadi komuniti", + "community-requests-to-join-title": "Permintaan keahlian", + "community-roles": "Peranan", + "community-share-title": "Berkongsi", + "community-thumbnail-image": "Imej lakaran kenit", + "community-thumbnail-upload": "Muat naik", + "complete-hardwallet-setup": "Kad ini kini dipaut. Anda memerlukannya untuk menandatangani transaksi dan membuka kunci anda", + "completed": "Selesai", + "confirm": "Sahkan", + "confirm-new-password": "Sahkan kata laluan baru", + "confirm-pairing-code-placeholder": "Sahkan kod pasangan anda ...", + "confirm-password-placeholder": "Mengesahkan kata laluan anda...", + "confirmation-request": "Permintaan pengesahan", + "confirmations": "Pengesahan", + "confirmations-helper-text": "Apabila transaksi mempunyai 12 pengesahan, anda boleh mempertimbangkannya untuk menyelesaikannya.", + "connect": "Sambung", + "connect-mailserver-content": "Sambung ke {{name}} ?", + "connect-wallet": "Sambungkan dompet", + "connected": "Bersambung", + "connected-to": "Bersambung dengan", + "connecting": "Menyambung ...", + "connecting-requires-login": "Untuk menyambung ke rangkaian lain memerlukan log masuk", + "connection-status": "Status sambungan", + "connection-with-the-card-lost": "Sambungan dengan kad\n telah hilang", + "connection-with-the-card-lost-setup-text": "Untuk menyambung semula, simpan kad ke\n bahagian belakang telefon anda dan jaga\n kad ke kenalan telefon", + "connection-with-the-card-lost-text": "Untuk meneruskan, pegang kad ke bahagian belakang telefon anda", + "contact-code": "Kod pengenalan", + "contact-s": { + "other": "kenalan" + }, + "contacts": "Kenalan", + "contacts-descr": "Kenalan anda akan muncul di sini. Anda akan menerima kemas kini status dari sesiapa sahaja yang anda tambahkan sebagai kenalan", + "contacts-empty": "Kenalan dengan nama ENS akan muncul di sini", + "continue": "Teruskan", + "continue-anyway": "Teruskan pula", + "contract-address": "Alamat kontrak", + "contract-interaction": "Interaksi kontrak", + "copy-info": "Salin maklumat", + "copy-qr": "Salin kod", + "copy-to-clipboard": "Salinan", + "copy-transaction-hash": "Salin ID transaksi", + "cost-fee": "Kos \/ Bayaran", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Buat", + "create-a-pin": "Cipta kod laluan 6-digit", + "create-a-puk": "Buat PUK 12-digit", + "create-category": "Cipta kategori", + "create-channel": "Buat saluran", + "create-channel-title": "Saluran baru", + "create-community": "Buat komuniti", + "create-group-chat": "Cipta perbualan kumpulan", + "create-multiaccount": "Menjana kunci", + "create-new-key": "Dapatkan kunci baru", + "create-pin": "Buat kod laluan 6 digit", + "create-pin-description": "Anda memerlukan kad anda + kod laluan 6 digit ini untuk membuka Status dan mengesahkan transaksi", + "created-group-chat-description": "Anda telah mencipta kumpulan {{group-name}}", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Mata Wang", + "currency-display-name-aed": "Dirham Emirat", + "currency-display-name-afn": "Afghanistan Afghanistan", + "currency-display-name-ars": "Peso Argentina", + "currency-display-name-aud": "Dolar Australia", + "currency-display-name-bbd": "Dolar Barbados", + "currency-display-name-bdt": "Taka Bangladesh", + "currency-display-name-bgn": "Lev Bulgaria", + "currency-display-name-bhd": "Dinar Bahrain", + "currency-display-name-bnd": "Dolar Brunei Darussalam", + "currency-display-name-bob": "Bolíviano Bolivia", + "currency-display-name-brl": "Brazil Sebenar", + "currency-display-name-btn": "Ngultrum Bhutan", + "currency-display-name-cad": "Dolar Kanada", + "currency-display-name-chf": "Franc Switzerland", + "currency-display-name-clp": "Peso Cili", + "currency-display-name-cny": "China Yuan Renminbi", + "currency-display-name-cop": "Peso Colombia", + "currency-display-name-crc": "Kolon Costa Rica", + "currency-display-name-czk": "Koruna Czech", + "currency-display-name-dkk": "Krone Denmark", + "currency-display-name-dop": "Peso Republik Dominika", + "currency-display-name-egp": "Pound Mesir", + "currency-display-name-etb": "Birr Ethiopia", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Pound Britain", + "currency-display-name-gel": "Lari Georgia", + "currency-display-name-ghs": "Ghana Cedi", + "currency-display-name-hkd": "Dolar Hong Kong", + "currency-display-name-hrk": "Kuna Croatia", + "currency-display-name-huf": "Forint Hungary", + "currency-display-name-idr": "Rupiah Indonesia", + "currency-display-name-ils": "Israel Shekel", + "currency-display-name-inr": "Rupee India", + "currency-display-name-isk": "Iceland Krona", + "currency-display-name-jmd": "Dolar Jamaica", + "currency-display-name-jpy": "Yen Jepun", + "currency-display-name-kes": "Shilling Kenya", + "currency-display-name-krw": "Korea (Selatan) Menang", + "currency-display-name-kwd": "Dinar Kuwait", + "currency-display-name-kzt": "Kazakhstan Tenge", + "currency-display-name-lkr": "Rupee Sri Lanka", + "currency-display-name-mad": "Dirham Maghribi", + "currency-display-name-mdl": "Moldovan Leu", + "currency-display-name-mur": "Rupee Mauritius", + "currency-display-name-mwk": "Malawian Kwacha", + "currency-display-name-mxn": "Peso Mexico", + "currency-display-name-myr": "Ringgit Malaysia", + "currency-display-name-mzn": "Metzikal Mozambik", + "currency-display-name-nad": "Dolar Namibia", + "currency-display-name-ngn": "Nigeria Naira", + "currency-display-name-nok": "Krone Norway", + "currency-display-name-npr": "Rupee Nepal", + "currency-display-name-nzd": "Dolar New Zealand", + "currency-display-name-omr": "Oman Rial", + "currency-display-name-pen": "Peru Sol", + "currency-display-name-pgk": "Papua New Guinean Kina", + "currency-display-name-php": "Peso Filipina", + "currency-display-name-pkr": "Rupee Pakistan", + "currency-display-name-pln": "Poland Zloty", + "currency-display-name-pyg": "Paraguay Guarani", + "currency-display-name-qar": "Qatar Riyal", + "currency-display-name-ron": "Romania Leu", + "currency-display-name-rsd": "Dinar Serbia", + "currency-display-name-rub": "Rubel Rusia", + "currency-display-name-sar": "Riyal Arab Saudi", + "currency-display-name-sek": "Krona Sweden", + "currency-display-name-sgd": "Dolar Singapura", + "currency-display-name-thb": "Baht Thailand", + "currency-display-name-try": "Lira Turki", + "currency-display-name-ttd": "Dolar Trinidad dan Tobago", + "currency-display-name-twd": "Dolar Baru Taiwan", + "currency-display-name-tzs": "Shilling Tanzania", + "currency-display-name-uah": "Ukraine Hryvnia", + "currency-display-name-ugx": "Shilling Uganda", + "currency-display-name-usd": "Dolar Amerika Syarikat", + "currency-display-name-uyu": "Peso Uruguay", + "currency-display-name-vef": "Venezuela Bolívar", + "currency-display-name-vnd": "Dong Vietnam", + "currency-display-name-zar": "Rand Afrika Selatan", + "current": "Semasa", + "current-average-tip": "Tip purata semasa", + "current-base-fee": "Bayaran asas semasa", + "current-minimum-tip": "Tip minimum semasa", + "current-network": "Rangkaian semasa", + "current-password": "Kata laluan semasa", + "current-pin": "Masukkan kod laluan 6-digit", + "current-pin-description": "Masukkan kod laluan 6-digit anda untuk teruskan", + "custom": "Adat", + "custom-networks": "Rangkaian khas", + "custom-node": "Anda menggunakan titik akhir RPC tersuai. Sejarah pemindahan tempatan anda mungkin tidak lengkap.", + "custom-seed-phrase": "Frasa biji tidak sah", + "custom-seed-phrase-text-1": "Frasa benih ini tidak sepadan dengan kamus kami yang disokong. Semak perkataan yang disalah eja.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Terima dan Buka", + "dapp-starter-pack-description": "Berikut adalah beberapa kripto sebagai permulaan! Gunakannya untuk mendapatkan pelekat, nama ENS dan mencuba dapps", + "dapp-starter-pack-title": "Pek Permulaan", + "dapp-would-like-to-connect-wallet": "ingin sambungkan ke", + "dapps": "ÐApps", + "dapps-permissions": "Kebenaran DApp", + "dark": "Gelap", + "data": "Data", + "data-collected": "Data dikumpul", + "data-collected-subtitle": "Jadual di bawah menunjukkan data tepat yang disimpan dan akan dihantar. Data disahkan berdasarkan peraturan awam untuk memastikan tidak ada data sensitif yang dikirim. Jangan percaya, sahkan.", + "data-syncing": "Penyelarasan data", + "database-reset-content": "Sembang, kenalan dan tetapan telah dihapuskan. Anda boleh menggunakan akaun anda dengan Kad Kunci anda", + "database-reset-title": "Set semula pangkalan data", + "database-reset-warning": "Pangkalan data akan ditetapkan semula. Sembang, kenalan dan tetapan akan dihapuskan", + "datetime-ago": "yang lalu", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "other": "hari" + }, + "datetime-day-short": { + "other": "D" + }, + "datetime-hour": { + "other": "jam" + }, + "datetime-hour-short": { + "other": "H" + }, + "datetime-minute": { + "other": "minit" + }, + "datetime-minute-short": { + "other": "M" + }, + "datetime-second": { + "other": "saat" + }, + "datetime-second-short": { + "other": "S" + }, + "datetime-today": "Hari ini", + "datetime-yesterday": "semalam", + "decimals": "Perpuluhan", + "decline": "Tolak", + "decryption-failed-content": "Ralat berlaku semasa menyahsulitkan data anda. Anda mungkin perlu menghapus data lama anda dan menghasilkan akaun baru. Ketuk \"Terapkan\" untuk menghapus atau \"Batal\" untuk mencuba lagi", + "default": "Lalai", + "default-assets": "Lalai ERC20 dan ERC721", + "default-sync-period": "Segerakkan sejarah untuk", + "delete": "Padam", + "delete-account": "Padamkan akaun", + "delete-and-leave-group": "Padam dan tinggalkan kumpulan", + "delete-bootnode": "Padamkan bootnode", + "delete-bootnode-are-you-sure": "Adakah anda pasti mahu memadam bootnode ini?", + "delete-bootnode-title": "Padamkan bootnode", + "close-chat": "Padamkan sembang", + "close-chat-confirmation": "Adakah anda pasti mahu memadamkan sembang ini?", + "delete-confirmation": "Padamkan?", + "delete-keys-keycard": "Padamkan kunci dari Kad Kunci", + "delete-mailserver": "Padamkan nod sejarah", + "delete-mailserver-are-you-sure": "Adakah anda pasti mahu memadamkan nod sejarah ini?", + "delete-mailserver-title": "Padamkan nod sejarah", + "delete-message": "Padamkan mesej", + "delete-my-account": "Padamkan akaun saya", + "delete-my-profile": "Padamkan profil saya", + "delete-network-confirmation": "Adakah anda pasti mahu memadamkan rangkaian ini?", + "delete-network-error": "Sila sambungkan ke rangkaian lain sebelum memadamkannya", + "delete-network-title": "Padamkan rangkaian?", + "delete-node": "Padamkan nod", + "delete-node-are-you-sure": "Adakah anda pasti mahu memadamkan nod ini?", + "delete-node-title": "Padamkan nod", + "delete-profile": "Padamkan profil", + "delete-profile-warning": "Amaran: Sekiranya anda tidak menuliskan frasa benih anda, anda akan kehilangan akses ke dana anda setelah anda menghapus profil anda", + "deny": "Tolak", + "derivation-path": "Jalan terbitan", + "describe-channel": "Terangkan saluran", + "description": "Penerangan", + "dev-mode": "Mod pembangunan", + "dev-mode-settings": "Tetapan mod pembangunan", + "device-syncing": "Penyegerakan peranti", + "devices": "Peranti", + "disable": "nyahdayakan", + "disable-all": "Nyahdaya semua", + "disabled": "Dinyahaktifkan", + "disconnected": "Berbual di luar talian", + "discover": "Cari", + "dismiss": "Ketepikan", + "done": "Dilakukan", + "dont-ask": "Jangan tanya saya lagi", + "edit": "Edit", + "edit-channel-title": "Edit saluran", + "edit-chats": "Sunting perbualan", + "edit-community": "Edit komuniti", + "edit-favourite": "Edit kegemaran", + "edit-group": "Edit kumpulan", + "edit-profile": "Sunting profil", + "edited": "Disunting", + "editing-message": "Menyunting mesej", + "empty-activity-center": "Pemberitahuan sembang anda\n akan muncul di sini", + "empty-chat-description": "Tidak ada mesej\n dalam sembang ini", + "empty-chat-description-community": "Tenang di sini selama {{quiet-hours}} .", + "empty-chat-description-one-to-one": "Sebarang mesej yang anda hantar di sini disulitkan dan hanya dapat dibaca oleh anda dan", + "empty-chat-description-public": "Tenang di sini sejak {{quiet-hours}} . Mulakan perbualan atau", + "empty-chat-description-public-share-this": "kongsi sembang ini.", + "empty-keycard-required": "Perlu Kad Kunci kosong", + "empty-pending-invitations-descr": "Orang yang ingin menyertai kumpulan ini\n melalui pautan jemputan akan muncul di sini", + "empty-tab": "Tab kosong", + "enable": "Aktifkan", + "enable-all": "Aktifkan semua", + "enable-link-previews": "Dayakan pratonton pautan dalam sembang?", + "encrypt-with-password": "Enkripsi dengan kata laluan", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Tambahkan nama pengguna", + "ens-agree-to": "Bersetuju untuk", + "ens-chat-settings": "Tetapan sembang", + "ens-custom-domain": "Domain tersuai", + "ens-custom-username-hints": "Taip keseluruhan nama pengguna termasuk domain tersuai seperti nama pengguna.domain.eth", + "ens-custom-username-taken": "Nama pengguna bukan milik anda :(", + "ens-deposit": "Deposit", + "ens-dismiss-message": "Klik di sini untuk menolak", + "ens-displayed-with": "Mesej anda ditunjukkan kepada orang lain dengan", + "ens-get-name": "Dapatkan nama pengguna sejagat", + "ens-got-it": "Ok, faham", + "ens-locked": "Nama pengguna dikunci. Anda tidak akan dapat melepaskannya sehingga {{date}}", + "ens-name-content": "Alias khusus untuk kunci sembang anda yang boleh anda daftarkan menggunakan Ethereum Name Service Nama ENS adalah nama pengguna yang terdesentralisasi.", + "ens-name-not-found": "Tidak dapat menyelesaikan nama ENS", + "ens-name-title": "Nama ENS", + "ens-network-restriction": "Hanya terdapat di Mainnet", + "ens-no-usernames": "Anda tidak mempunyai nama pengguna yang disambungkan", + "ens-powered-by": "Dikuasakan oleh Ethereum Name Services", + "ens-primary-username": "Nama pengguna utama", + "ens-register": "Daftar", + "ens-registration-failed": "Untuk mendaftarkan nama pengguna, sila cuba lagi.", + "ens-registration-failed-title": "transaksi gagal", + "ens-registration-failure": "Pendaftaran gagal", + "ens-registration-in-progress": "Pendaftaran sedang dijalankan ...", + "ens-release-username": "Lepaskan nama pengguna", + "ens-remove-hints": "Mengeluarkan akan melepaskan nama pengguna dari kunci anda.", + "ens-remove-username": "Alih keluar nama pengguna", + "ens-saved": "kini dihubungkan dengan kekunci sembang anda dan boleh digunakan dalam Status.", + "ens-saved-title": "Nama pengguna ditambah", + "ens-show-username": "Tunjukkan nama pengguna ENS saya dalam sembang", + "ens-terms-header": "Syarat pendaftaran nama", + "ens-terms-point-1": "Dana disimpan selama 1 tahun. SNT anda akan dikunci, tetapi tidak dibelanjakan.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Pendaftaran ENS).", + "ens-terms-point-2": "Selepas 1 tahun, anda boleh melepaskan nama dan mendapatkan deposit anda kembali, atau tidak mengambil tindakan untuk menyimpan nama tersebut.", + "ens-terms-point-3": "Sekiranya syarat-syarat kontrak berubah - misalnya Status membuat peningkatan kontrak - pengguna berhak melepaskan nama pengguna tanpa mengira waktu yang ditahan.", + "ens-terms-point-4": "Pengawal kontrak tidak dapat mengakses dana simpanan anda. Mereka hanya boleh dipindahkan ke alamat yang menghantarnya.", + "ens-terms-point-5": "Alamat anda akan dikaitkan secara terbuka dengan nama ENS anda.", + "ens-terms-point-6": "Nama pengguna dibuat sebagai node subdomain stateofus.eth dan tertakluk kepada terma kontrak pintar ENS.", + "ens-terms-point-7": "Anda membenarkan kontrak untuk memindahkan SNT bagi pihak anda. Ini hanya boleh berlaku apabila anda menyetujui transaksi untuk membenarkan pemindahan.", + "ens-terms-point-8": "Syarat-syarat ini dijamin oleh logik kontrak pintar di alamat:", + "ens-terms-point-9": "{{address}} (Nama Pengguna StatusPendaftar)", + "ens-terms-registration": "Syarat pendaftaran nama.", + "ens-test-message": "Hey", + "ens-transaction-pending": "Transaksi belum selesai ...", + "ens-understand": "Saya faham bahawa alamat dompet saya akan dihubungkan secara terbuka ke nama pengguna saya.", + "ens-username": "Nama pengguna ENS", + "ens-username-already-added": "Nama pengguna sudah dihubungkan dengan kunci sembang anda dan boleh digunakan di dalam Status.", + "ens-username-available": "✓ Nama pengguna ada!", + "ens-username-connected": "Nama pengguna ini dimiliki oleh anda dan dihubungkan dengan kunci sembang anda.", + "ens-username-connected-continue": "Terus menetapkan `Tunjukkan nama pengguna ENS saya dalam sembang '.", + "ens-username-connected-with-different-key": "Melanjutkan akan memerlukan transaksi untuk menghubungkan nama pengguna dengan kunci sembang semasa anda.", + "ens-username-connection-confirmation": "{{username}} akan disambungkan setelah transaksi selesai.", + "ens-username-hints": "Sekurang-kurangnya 4 watak. Huruf Latin, angka, dan huruf kecil sahaja.", + "ens-username-invalid": "Huruf dan nombor sahaja.", + "ens-username-owned": "✓ Nama pengguna dimiliki oleh anda.", + "ens-username-owned-continue": "Meneruskan akan menghubungkan nama pengguna ini dengan kekunci sembang anda.", + "ens-username-registration-confirmation": "Bagus! Anda memiliki {{username}} setelah transaksi selesai.", + "ens-username-taken": "Nama pengguna sudah diambil :(", + "ens-username-you-can-follow-progress": "Anda boleh mengikuti perkembangan di bahagian Sejarah Transaksi dompet anda.", + "ens-usernames": "Nama pengguna ENS", + "ens-usernames-details": "Daftarkan nama pengguna sejagat agar mudah dikenali oleh pengguna lain", + "ens-want-custom-domain": "Saya mempunyai nama di domain lain", + "ens-want-domain": "Saya mahukan domain stateofus.eth", + "ens-welcome-hints": "Nama ENS mengubah alamat lama menjadi nama pengguna yang unik.", + "ens-welcome-point-customize": "Nama ENS boleh menggantikan nama 3 perkataan rawak anda dalam sembang. Jadilah @ namaanda dan bukannya {{name}} .", + "ens-welcome-point-customize-title": "Sesuaikan nama sembang anda", + "ens-welcome-point-receive": "Orang lain boleh menghantar dana melalui chat dalam satu langkah mudah.", + "ens-welcome-point-receive-title": "Terima urus niaga dalam sembang", + "ens-welcome-point-register": "Daftar sekali untuk mengekalkan nama itu selama-lamanya. Selepas 1 tahun, anda boleh mengeluarkan nama dan mendapatkan kembali SNT anda.", + "ens-welcome-point-register-title": "10 SNT untuk mendaftar", + "ens-welcome-point-simplify": "Anda boleh menerima dana untuk nama ENS yang senang anda kongsi dan bukannya hash heksadesimal (0x ...).", + "ens-welcome-point-simplify-title": "Permudahkan alamat ETH anda", + "ens-welcome-point-verify": "Anda boleh mengesahkan dan menambahkan nama pengguna yang anda miliki dalam langkah seterusnya.", + "ens-welcome-point-verify-title": "Sudah memiliki nama pengguna?", + "ens-your-username": "Nama pengguna anda", + "ens-your-usernames": "Nama pengguna anda", + "ens-your-your-name": "Nama ENS anda", + "enter-12-words": "Masukkan 12 perkataan frasa pemulihan anda, dipisahkan oleh ruang tunggal", + "enter-a-private-key": "Masukkan kunci peribadi", + "enter-a-seed-phrase": "Masukkan frasa biji", + "enter-address": "Masukkan alamat", + "enter-contact-code": "ENS (vitalik94) atau kekunci sembang (0x04…)", + "enter-pair-code": "Masukkan kod pasangan anda", + "enter-pair-code-description": "Kod berpasangan boleh ditetapkan dari klien Status yang sudah dipasangkan", + "enter-password": "Masukkan kata laluan", + "enter-password-migration-prompt": "Masukkan kata laluan anda untuk memindahkan kenalan, pembualan dan tetapan bersama kunci anda", + "enter-pin": "Masukkan kod laluan 6-digit", + "enter-puk-code": "Masukkan kod PUK", + "enter-puk-code-description": "Kod laluan 6-digit telah disekat. \n Sila masukkan kod PUK untuk menyahsekat kod laluan.", + "enter-recipient-address-or-username": "Masukkan alamat atau nama pengguna penerima", + "enter-seed-phrase": "Masukkan frasa biji", + "enter-url": "Masukkan URL", + "enter-user-pk": "Masukkan kekunci awam pengguna", + "enter-watch-account-address": "Imbas kod QR\n atau\n masukkan alamat untuk ditonton", + "enter-word": "Masukkan perkataan", + "enter-your-code": "Masukkan kod laluan 6 digit anda", + "enter-your-password": "Masukkan kata laluan anda", + "error": "Ralat", + "error-unable-to-get-balance": "Tidak dapat memperoleh baki", + "error-unable-to-get-prices": "Kesalahan penukaran mata wang. Muat semula skrin anda untuk mencuba lagi.", + "error-unable-to-get-token-balance": "Tidak dapat memperoleh baki token", + "errors": "Kesalahan", + "eth": "ETH", + "ethereum-account": "Akaun Ethereum", + "ethereum-address": "Alamat Ethereum", + "ethereum-node-started-incorrectly-description": "Ethereum node dimulakan dengan konfigurasi yang salah, aplikasi akan dihentikan untuk pulih dari keadaan itu. Id rangkaian yang dikonfigurasi = {{network-id}} , sebenar = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Node Ethereum bermula dengan tidak betul", + "etherscan-lookup": "Cari Etherscan", + "everyone": "Semua orang", + "expand-all": "Kembang semua", + "export-account": "Eksport akaun", + "export-key": "Eksport kunci peribadi", + "external-storage-denied": "Akses ke storan luaran ditolak", + "failed": "Gagal", + "faq": "Soalan lazim", + "fast": "Cepat", + "favourite": "Kegemaran", + "favourite-description": "Laman web kegemaran anda akan muncul di sini", + "favourites": "Kegemaran", + "favourites-empty": "Alamat yang ditambah kepada kegemaran akan muncul di sini", + "fee-cap": "Had yuran", + "fee-explanation": "Harga keseluruhan maksimum untuk transaksi. Sekiranya bayaran asas blok melebihi ini, ia akan dimasukkan ke dalam blok berikut dengan yuran asas yang lebih rendah.", + "fee-options": "Pilihan yuran yang dicadangkan", + "fetch-messages": "↓ Ambil mesej", + "fetch-timeline": "↓ Ambil", + "find": "Mencari", + "finish": "Selesai", + "finishing-card-setup": "Menyelesaikan penyediaan kad", + "fleet": "Fleet", + "fleet-settings": "Tetapan armada", + "follow": "Ikut", + "follow-your-interests": "Lompat ke sembang awam dan temui orang baru", + "free": "↓ Percuma", + "from": "Dari", + "gas-amount-limit": "Had jumlah gas", + "gas-limit": "Had gas", + "gas-price": "Harga gas", + "gas-used": "Gas digunakan", + "generate-a-key": "Menjana kunci", + "generate-a-new-account": "Jana akaun", + "generate-a-new-key": "Jana kunci baru", + "generate-account": "Menjana kunci", + "generate-an-account": "Jana akaun", + "generate-new-key": "Menjana kunci", + "generating-codes-for-pairing": "> Memuat turun perisian produk ke kad\n > Menjana kod buka kunci & berpasangan", + "generating-keys": "Menjana kunci ...", + "generating-mnemonic": "Menjanakan frasa pemulihan", + "get-a-keycard": "Dapatkan Kad Kunci", + "get-started": "Mulakan", + "get-status-at": "Dapatkan Status di http:\/\/status.im", + "get-stickers": "Dapatkan Pelekat", + "give-a-short-description": "Beri penerangan ringkas", + "give-a-short-description-community": "Berikan penerangan ringkas", + "give-permissions-camera": "Memberi keizinan\n untuk mengakses kamera", + "glossary": "Glosari", + "go-to-settings": "Pergi ke Tetapan ...", + "got-it": "Faham", + "grant-face-id-permissions": "Untuk memberikan kebenaran ID Wajah yang diperlukan, buka tetapan sistem anda dan pastikan Status > ID Wajah dipilih", + "group-chat": "Sembang berkumpulan", + "group-chat-admin": "Pentadbir", + "group-chat-admin-added": "**{{member}}** telah dijadikan pentadbir", + "group-chat-all-contacts-invited": "Semua kenalan anda sudah ada dalam kumpulan", + "group-chat-created": "**{{member}}** mencipta kumpulan **{{name}}**", + "group-chat-decline-invitation": "Tolak jemputan", + "group-chat-member-added": "**{{member}}** telah dijemput", + "group-chat-member-joined": "**{{member}}** telah menyertai kumpulan", + "group-chat-member-removed": "**{{member}}** telah meninggalkan kumpulan", + "group-chat-members-count": "{{selected}}\/{{max}} ahli", + "group-chat-name-changed": "**{{member}}** mengubah nama kumpalan ke **{{name}}**", + "group-chat-no-contacts": "Anda belum mempunyai kenalan.\n Jemput rakan anda untuk mula berbual", + "group-info": "Maklumat kumpulan", + "group-invite": "Jemputan kumpulan", + "group-invite-link": "Pautan jemputan kumpulan", + "group-membership-request": "Permintaan keahlian kumpulan", + "gwei": "Gwei", + "has-permissions": "mempunyai kebenaran untuk mengakses", + "hash": "Hash", + "help": "menolong", + "help-capitalized": "Tolonglah", + "help-center": "Pusat Bantuan", + "help-improve-status": "Bantu bertambah baik Status", + "hide": "Sembunyi", + "hide-content-when-switching-apps": "Sekat tangkapan skrin", + "hide-content-when-switching-apps-ios": "Sembunyikan pratonton", + "history": "Sejarah", + "history-nodes": "Nod sejarah", + "hold-card": "Pegang kad ke belakang\n telefon anda", + "home": "Rumah", + "hooks": "Cangkuk", + "how-it-works": "Bagaimana ia berfungsi", + "http-gateway-error": "Alamak, permintaan gagal!", + "identifier": "Pengenal", + "image": "Imej", + "image-remove-current": "Keluarkan foto semasa", + "image-source-gallery": "Pilih dari galeri", + "image-source-make-photo": "Menangkap", + "image-source-title": "Edit gambar", + "import": "Import", + "import-community": "Import komuniti", + "import-community-title": "Import komuniti", + "in-contacts": "Dalam kenalan", + "include": "Termasuk", + "incoming": "Masuk", + "incoming-transaction": "Urus niaga masuk", + "incorrect-code": [ + "str", + "Maaf kodnya tidak betul, masukkan sekali lagi" + ], + "increase-gas": "Menambah Gas", + "initialization": "Permulaan", + "install": "Pasang", + "intro-message1": "Selamat Datang ke Status!\n Ketik mesej ini untuk menetapkan kata laluan dan memulakannya.", + "intro-privacy-policy-note1": "Status tidak mengumpulkan atau mendapat keuntungan dari data peribadi anda. Dengan meneruskan, anda bersetuju dengan", + "intro-privacy-policy-note2": "dasar privasi", + "intro-text": "Status adalah pintu masuk anda ke web yang terdesentralisasi", + "intro-text1": "Berbual melalui rangkaian peer-to-peer, disulitkan di mana mesej tidak dapat disensor atau diretas", + "intro-text2": "Hantar dan terima aset digital di mana sahaja di dunia — tidak memerlukan akaun bank", + "intro-text3": "Terokai permainan, pertukaran dan rangkaian sosial di mana anda sendiri memiliki data anda", + "intro-title1": "Komunikasi yang benar-benar peribadi", + "intro-title2": "Dompet crypto selamat", + "intro-title3": "Aplikasi terpusat", + "intro-wizard-text1": "Satu set kunci mengawal akaun anda. Kekunci anda tinggal di telefon anda, jadi hanya anda yang dapat menggunakannya", + "intro-wizard-text2": "Satu kunci adalah untuk berbual. Ia dilengkapi dengan nama yang dapat dibaca yang tidak dapat diubah.", + "intro-wizard-text3": "Sekiranya anda memiliki Kad Kunci, simpan kunci anda di sana untuk keselamatan yang lebih baik.", + "intro-wizard-text4": "Selamat dan menyulitkan kunci anda", + "intro-wizard-text6": "Status akan memberitahu anda mengenai mesej baru. Anda boleh mengedit pilihan pemberitahuan anda kemudian dalam tetapan", + "intro-wizard-title-alt4": "Buat kata laluan", + "intro-wizard-title-alt5": "Mengesahkan kata laluan anda", + "intro-wizard-title1": "Dapatkan kunci anda", + "intro-wizard-title2": "Pilih nama sembang", + "intro-wizard-title3": "Pilih simpanan kunci", + "intro-wizard-title4": "Buat kod laluan 6 digit", + "intro-wizard-title5": "Sahkan kod laluan", + "intro-wizard-title6": "Dayakan pemberitahuan", + "introduce-yourself": "Perkenalkan diri anda dengan pesanan ringkas", + "invalid-address-qr-code": "Kod QR yang diimbas tidak mengandungi alamat yang sah", + "invalid-format": "Format tidak sah\n Mesti {{format}}", + "invalid-key-confirm": "Memohon", + "invalid-key-content": "Pangkalan data tidak dapat dienkripsi kerana fail rosak. Dana dan kod pengenalan anda selamat. Data lain, seperti perbualan dan kenalan anda, tidak dapat dipulihkan. Butang \"{{erase-multiaccounts-data-button-text}}\" akan membuang semua data lain dan membenarkan anda untuk mengakses dana anda dan menghantar mesej", + "invalid-number": "Nombor tidak sah", + "invalid-pairing-password": "Kata laluan berpasangan tidak sah", + "invalid-public-chat-topic": "Topik sembang awam tidak sah", + "invalid-range": "Format tidak sah, mestilah antara {{min}} dan {{max}}", + "invalid-username-or-key": "Nama pengguna atau kekunci sembang tidak sah", + "invite": "Jemput", + "invite-button": "Jemput", + "invite-chat-accept": "Terima", + "invite-chat-accept-join": "Terima dan Sertai", + "invite-chat-intro": "Anda dirujuk oleh rakan untuk menyertai Status. Inilah beberapa crypto untuk memulakan anda! Gunakan untuk mendaftarkan nama ENS atau membeli pek pelekat", + "invite-chat-name": "Rujukan rakan", + "invite-chat-pending": "Belum selesai", + "invite-chat-rule": "Penerimaan juga akan menawarkan ganjaran rujukan bonus kripto kepada rakan anda", + "invite-chat-starter-pack": "Pek permulaan", + "invite-friends": "Menjemput kawan-kawan", + "invite-instruction-fifth": "Anda boleh memilih untuk menebus rujukan bonus anda pada bila-bila masa.", + "invite-instruction-first": "Anda menghantar pautan jemputan unik kepada rakan anda untuk memuat turun dan menyertai Status", + "invite-instruction-fourth": "Anda menerima bonus rujukan dan rakan anda Starter Pack", + "invite-instruction-second": "Rakan anda memuat turun Status dan membuat akaun (di Android)", + "invite-instruction-third": "Sembang dengan rakan anda dimulakan, di mana mereka mengesahkan rujukan anda", + "invite-people": "Jemput orang", + "invite-privacy-policy-public": "Anda memasang Status melalui pautan rujukan. Dengan menyertai sembang ini, anda mengaitkan perujuk anda dan bersetuju dengan", + "invite-privacy-policy1": "Dengan menerima anda bersetuju dengan program rujukan", + "invite-privacy-policy2": "Terma dan syarat.", + "invite-public-chat-home": "Jemputan rujukan", + "invite-public-chat-intro": "Berikut adalah beberapa kripto untuk memulakan anda! Gunakannya untuk mendaftarkan nama ENS atau membeli pek pelekat", + "invite-receive-account": "Akaun untuk menerima bonus rujukan anda", + "invite-reward": "Dapatkan crypto untuk setiap rakan yang anda jemput!", + "invite-reward-friend": "Kawan:", + "invite-reward-friend-description": "Rakan anda akan menerima Starter Pack yang terdiri daripada beberapa {{reward}} untuk memulakan", + "invite-reward-friend-name": "Pek permulaan", + "invite-reward-you": "Anda: ", + "invite-reward-you-description": "Jemput rakan dan terima {{reward}} sebagai bonus rujukan. Gunakan untuk mendapatkan pelekat, nama ENS dan cuba dapps", + "invite-reward-you-name": "Bonus rujukan", + "invite-select-account": "Pilih akaun untuk menerima bonus rujukan anda", + "invite-warning": "Promosi ini hanya sah untuk pengguna peranti Android yang bukan residen Amerika Syarikat. Rakan perlu mengesahkan rujukan dalam tempoh 7 hari", + "invited": "dijemput", + "join": "Sertailah", + "join-a-community": "atau sertai komuniti", + "join-group-chat": "Sertai kumpulan", + "join-group-chat-description": "{{username}} menjemput anda untuk menyertai kumpulan {{group-name}}", + "join-me": "Hai, sertai saya di Status: {{url}}", + "join-new-private-chat": "Mulakan sembang peribadi baru", + "join-new-public-chat": "Sertai sembang awam", + "joined": "Menyertai", + "joined-group-chat-description": "Anda telah menyertai {{group-name}} daripada jemputan oleh {{username}}", + "key": "Kunci", + "key-managment": "Pengurusan kekunci", + "key-on-device": "Kunci peribadi disimpan pada peranti ini", + "keycard": "Kad kunci", + "keycard-access-reset": "Akses kad kekunci ditetapkan semula", + "keycard-applet-install-instructions": "Untuk memasang applet sila ikuti arahan di https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Cuba gerakkan kad untuk mencari pembaca NFC pada peranti anda", + "keycard-awaiting-title": "Masih mencari...", + "keycard-backup": "Buat Kad Kekunci sandaran", + "keycard-backup-success-body": "Kad sandaran berjaya dibuat. Anda kini boleh menggunakannya dengan akaun anda seperti kad utama.", + "keycard-backup-success-title": "Sandaran berjaya", + "keycard-blocked": "Kad kunci telah disekat. \nAnda perlu menetapkan semula kad untuk terus menggunakannya.", + "keycard-can-use-with-new-passcode": "Anda boleh menggunakan kad ini dengan kod laluan baru anda", + "keycard-cancel-setup-text": "Ini akan membatalkan penyediaan kad kunci. Sangat disarankan untuk menyelesaikan persediaan untuk menggunakan kad kunci. Adakah anda benar-benar mahu membatalkan?", + "keycard-cancel-setup-title": "Operasi berbahaya", + "keycard-connected-description": "Cuba simpan kad itu", + "keycard-connected-title": "Disambungkan", + "keycard-desc": "Memiliki Kad Kunci? Simpan kunci anda di atasnya; anda memerlukannya untuk transaksi", + "keycard-dont-ask-card": "Jangan meminta kad untuk log masuk", + "keycard-enter-new-passcode": "Masukkan kod laluan baru {{step}} \/ 2", + "keycard-error-description": "Sambungkan kad sekali lagi untuk meneruskan", + "keycard-error-title": "Sambungan terputus", + "keycard-factory-reset": "Kembalikan kad ke tetapan kilang", + "keycard-factory-reset-text": "Melakukan ini akan menghapus sebarang frasa mnemonik yang tersimpan di kad. Pastikan anda mempunyai sandaran frasa mnemonik yang telah anda gunakan dengan Kad Kunci ini.", + "keycard-factory-reset-title": "Adakah anda pasti mahu melakukan tetapan semula kilang?", + "keycard-free-pairing-slots": "Kad kekunci mempunyai slot berpasangan percuma {{n}}", + "keycard-has-multiaccount-on-it": "Kad ini penuh. Setiap kad boleh menyimpan satu pad kekunci utama", + "keycard-init-description": "Letakkan kad di bahagian belakang telefon anda untuk meneruskan", + "keycard-init-title": "Mencari kad ...", + "keycard-is-blocked-details": "Anda tidak lagi dapat menggunakan kad ini untuk mengakses atau menandatangani akaun ini. Terlalu banyak percubaan kod laluan dan PUK yang gagal.", + "keycard-is-blocked-instructions": "Untuk mengakses akaun anda, anda perlu menetapkan semula kad anda. Ketik butang di bawah untuk memulakan prosedur, anda memerlukan mnemonic anda.", + "keycard-is-blocked-title": "Kad Kekunci disekat", + "keycard-is-frozen-details": "Untuk melindungi aset anda, kad anda dibekukan. Tetapkan semula kad anda untuk membekukan kad tersebut dan dapat menghantar transaksi. Anda boleh melakukan ini dengan PUK atau mnemonic anda.", + "keycard-is-frozen-factory-reset": "Set semula dengan mnemonic", + "keycard-is-frozen-reset": "Set semula dengan PUK", + "keycard-is-frozen-title": "Kad Kunci dibekukan", + "keycard-onboarding-finishing-header": "Selesai", + "keycard-onboarding-intro-header": "Simpan kunci anda di Keycard", + "keycard-onboarding-intro-text": "Bersedia, ini mungkin mengambil masa beberapa minit, tetapi penting untuk melindungi akaun anda", + "keycard-onboarding-mnemonic-text": "Anda juga memerlukan sekeping kertas dan pensil untuk menulis frasa benih anda.", + "keycard-onboarding-pairing-header": "Memasangkan kad ...", + "keycard-onboarding-pin-text": "Anda perlu membuat kod laluan 6 digit yang akan digunakan untuk melindungi akses ke Kad Kunci anda.", + "keycard-onboarding-preparing-header": "Menyiapkan kad ...", + "keycard-onboarding-puk-code-header": "Tuliskan kod\n dan simpan dengan selamat", + "keycard-onboarding-recovery-phrase-description": "Anda memerlukan frasa biji ini untuk mendapatkan kembali kunci anda. Tuliskan. Simpan dengan selamat, di luar talian, dan terpisah dari peranti ini.", + "keycard-onboarding-recovery-phrase-header": "Cadangan frasa biji", + "keycard-onboarding-recovery-phrase-text": "Untuk mata anda sahaja. Ini adalah biji ajaib yang digunakan untuk menghasilkan kunci anda.", + "keycard-onboarding-start-header": "Pegang kad ke belakang\n telefon anda untuk bermula", + "keycard-onboarding-start-step1": "Buat kod laluan", + "keycard-onboarding-start-step1-text": "Lebih kurang 1 minit. Buat kod laluan 6 digit untuk menyulitkan kunci anda", + "keycard-onboarding-start-step2": "Tuliskan PUK dan kod pasangan", + "keycard-onboarding-start-step2-text": "Lebih kurang 1 minit. Anda akan memerlukan sehelai kertas dan pensil untuk itu", + "keycard-onboarding-start-step3": "Sandarkan frasa biji", + "keycard-onboarding-start-step3-text": "Lebih kurang 1 minit. Sekeping kertas dan pensil juga diperlukan", + "keycard-onboarding-start-text": "Dan simpan kad ke kenalan telefon\n semasa persediaan. Penyediaan akan memakan masa sekitar 4 minit", + "keycard-processing-description": "Cuba simpan kad itu", + "keycard-processing-title": "Memproses ...", + "keycard-recover": "kad hilang atau beku?", + "keycard-recover-text": "Sekiranya anda mempunyai frasa mnemonik anda, anda boleh membuat Kad Kunci baru yang berkaitan dengan akaun ini. Anda boleh menggunakan Kad Kunci baru atau melakukan tetapan semula kilang pada kad yang dibekukan.", + "keycard-recover-title": "Buat kad baru untuk akaun ini?", + "keycard-recovery-intro-button-text": "Mulakan pemulihan", + "keycard-recovery-intro-header": "Pulihkan kunci yang disimpan pada kad kekunci", + "keycard-recovery-intro-text": "Sekiranya anda menghasilkan kunci menggunakan kad kekunci sebelum dan sekarang mahu menggunakan kunci ini pada peranti ini", + "keycard-recovery-no-key-header": "Tidak ada apa-apa\n pulih di sini", + "keycard-recovery-no-key-text": "Kad Kunci anda tidak mempunyai kunci yang tersimpan di dalamnya. Untuk menggunakannya, buat kunci baru dan pilih Kad Kunci anda untuk menyimpan kunci", + "keycard-recovery-phrase-confirm-header": "Sahkan frasa biji", + "keycard-recovery-phrase-confirmation-text": "Anda tidak akan mempunyai peluang kedua! Sekiranya anda kehilangan akses, misalnya dengan kehilangan kad kunci, anda hanya dapat mengakses kunci anda dengan frasa pilihan anda. Tiada siapa, tetapi anda mempunyai ungkapan benih anda. Tuliskan. Pastikan ia selamat.", + "keycard-recovery-phrase-confirmation-title": "Menulis frasa benih ke bawah?", + "keycard-recovery-success-header": "Kunci anda telah\n berjaya pulih", + "keycard-redeem-title": "Tebus ke", + "keycard-redeem-tx": "Tebus aset", + "keycard-redeem-tx-desc": "Ketik kad untuk menandatangani dan menerima aset", + "keycard-reset-passcode": "Tetapkan semula kod laluan", + "keycard-success-description": "Anda boleh mengeluarkan kad sekarang", + "keycard-success-title": "Kejayaan", + "keycard-unauthorized-operation": "Anda tidak dibenarkan melakukan operasi ini.\n Sila ketik kad yang sah dan cuba lagi.", + "keycard-upsell-subtitle": "Keselamatan dan kemudahan yang dipertingkatkan", + "language": "Bahasa", + "learn-more": "Ketahui lebih lanjut", + "learn-more-about-keycard": "Ketahui lebih lanjut mengenai Kad Kunci", + "leave": "Tinggalkan", + "leave-chat": "Tinggalkan sembang", + "leave-chat-confirmation": "Sejarah sembang akan dialih keluar daripada peranti anda. Selepas menyertai semula, anda tidak akan dapat memperoleh semula mana-mana sejarah anda.", + "leave-community": "Tinggalkan komuniti", + "leave-confirmation": "Tinggalkan {{chat-name}}", + "leave-group": "Tinggalkan kumpulan", + "left": "dibiarkan", + "les-ulc": "LES \/ ULC", + "lets-go": "Mari pergi", + "light": "Cahaya", + "linked-on": "Dipaut pada {{date}}", + "load-messages-before": "sebelum {{date}}", + "load-more-messages": "↓ Ambil lebih banyak mesej", + "load-more-timeline": "↓ Ambil lebih banyak lagi", + "loading": "Memuat ...", + "local-notifications": "Pemberitahuan tempatan", + "lock-app-with": "Kunci aplikasi dengan", + "log-level": "Tahap log", + "log-level-settings": "Tetapan tahap log", + "logging": "Pembalakan", + "logging-enabled": "Pembalakan diaktifkan?", + "login-pin-description": "Masukkan kod laluan 6-digit anda untuk membuka kunci anda", + "logout": "Log keluar", + "logout-app-content": "Akaun akan dilog keluar. Apabila anda membukanya lagi, rangkaian yang dipilih akan digunakan", + "logout-are-you-sure": "Adakah anda pasti mahu\n untuk log keluar?", + "logout-key-management": "Anda perlu log keluar untuk mencapai pengurusan kekunci.", + "logout-title": "Log keluar?", + "looking-for-cards": "Mencari kad ...", + "lost-connection": "Putus hubungan", + "low-tip": "tip terlalu rendah", + "lower-than-average-tip": "tip rendah daripada purata", + "mail-should-be-configured": "Pelanggan mel harus dikonfigurasi", + "mailserver-address": "Alamat nod sejarah", + "mailserver-automatic": "Pilihan automatik", + "mailserver-automatic-switch-explanation": "Pilih nod sejarah terpantas yang ada", + "mailserver-connection-error": "Tidak dapat menyambung ke nod sejarah", + "mailserver-content": "Node dalam rangkaian Status yang merutekan dan menyimpan mesej, sehingga 30 hari.", + "mailserver-details": "Perincian nod sejarah", + "mailserver-error-content": "Nod Status yang anda pilih tidak dapat dicapai.", + "mailserver-error-title": "Ralat semasa menyambung ke nod Status", + "mailserver-format": "enode: \/\/ {enode-id} : {password} @ {ip-address} : {port}", + "mailserver-pick-another": "Pilih nod sejarah yang lain", + "mailserver-reconnect": "Tidak dapat menyambung ke nod sejarah. Ketik untuk menyambung semula", + "mailserver-request-error-content": "Ralat berikut telah dikembalikan oleh nod Status: {{error}}", + "mailserver-request-error-status": "Berlaku ralat semasa mengambil sejarah, periksa log untuk mendapatkan perincian", + "mailserver-request-error-title": "Ralat permintaan nod status", + "mailserver-request-retry": "Cuba semula permintaan", + "mailserver-retry": "Cuba lagi", + "mailserver-title": "Nod sejarah", + "main-currency": "Mata wang utama", + "main-networks": "Rangkaian utama", + "main-wallet": "Dompet Utama", + "mainnet-network": "Rangkaian utama", + "make-admin": "Buat pentadbir", + "manage-keys-and-storage": "Urus kekunci dan storan", + "mark-all-read": "Tandakan semua telah dibaca", + "master-account": "Akaun induk", + "max-fee": "Bayaran maksimum", + "max-priority-fee": "Bayaran keutamaan maksimum", + "maximum-fee": "Bayaran maksimum", + "maybe-later": "Mungkin kemudian", + "member-ban": "Melarang ahli", + "member-kick": "Tendang ahli", + "members": { + "other": "{{count}} ahli" + }, + "members-active": { + "other": "{{count}} ahli" + }, + "members-active-none": "tiada ahli", + "members-count": "{{count}} ahli", + "members-label": "Ahli", + "members-limit-reached": "Had ahli tercapai", + "members-title": "Ahli", + "membership-approval": "Memerlukan kelulusan", + "membership-approval-description": "Komuniti anda bebas untuk menyertai, tetapi ahli baru perlu diluluskan oleh pencipta komuniti terlebih dahulu", + "membership-button": "Syarat keahlian", + "membership-declined": "Permintaan keahlian ditolak", + "membership-description": "Keahlian kumpulan menghendaki anda diterima oleh pentadbir kumpulan", + "membership-ens": "Perlu nama pengguna ENS", + "membership-ens-description": "Komuniti anda memerlukan nama pengguna ENS untuk dapat menyertai", + "membership-free": "Tiada syarat", + "membership-free-description": "Komuniti anda bebas untuk disertai oleh sesiapa sahaja", + "membership-invite": "Memerlukan jemputan daripada ahli lain", + "membership-invite-description": "Komuniti anda hanya boleh disertai dengan jemputan daripada ahli komuniti sedia ada", + "membership-none": "Tiada", + "membership-none-placeholder": "Anda boleh meminta ahli baru untuk memenuhi kriteria tertentu sebelum mereka dapat bergabung. Ini boleh diubah pada bila-bila masa", + "membership-request-pending": "Permintaan keahlian belum selesai", + "membership-requests": "Permintaan keahlian", + "membership-title": "Syarat keahlian", + "message": "Mesej", + "message-not-sent": "Mesej tidak dihantar", + "message-options-cancel": "Batal", + "message-reply": "Balas", + "messages": "Mesej", + "messages-from-contacts-only-subtitle": "Hanya orang yang anda tambahkan sebagai kenalan yang boleh memulakan sembang baru dengan anda atau menjemput anda ke kumpulan", + "might-break": "Mungkin memecahkan beberapa ÐApps", + "migration-successful": "Pemindahan berjaya", + "migration-successful-text": "Akaun berjaya dipindahkan ke Kad Kunci", + "migrations-failed-content": "{{message}}\nversi skema: asal {{initial-version}}, semasa {{current-version}}, terakhir {{last-version}}\n\nRalat pangkalan data berlaku. Dana dan kod pengenalan anda selamat. Data lain, seperti perbualan dan kenalan anda, tidak dapat dipulihkan. Butang \"{{erase-multiaccounts-data-button-text}}\", akan membuang semua data lain dan membenarkan anda untuk mengakses dana dan menghantar mesej.", + "miners-higher-fee": "Pelombong mungkin akan memasukkan transaksi anda lebih awal jika anda bayar lebih tinggi.", + "mobile-network-ask-me": "Tanya saya semasa berada di rangkaian mudah alih", + "mobile-network-continue-syncing": "Teruskan penyegerakan", + "mobile-network-continue-syncing-details": "Anda boleh mengubahnya kemudian dalam tetapan", + "mobile-network-go-to-settings": "Pergi ke tetapan", + "mobile-network-settings": "Data mudah alih", + "mobile-network-sheet-configure": "Anda boleh mengkonfigurasi penyegerakan dengan lebih banyak lagi\n terperinci dalam", + "mobile-network-sheet-offline": "Tanpa Wi-fi, penyegerakan mesej dilumpuhkan.", + "mobile-network-sheet-offline-details": "Penyegerakan menggunakan rangkaian mudah alih dimatikan", + "mobile-network-sheet-remember-choice": "Ingat pilihan saya", + "mobile-network-sheet-settings": "Seting", + "mobile-network-start-syncing": "Mula menyegerakkan", + "mobile-network-stop-syncing": "Berhenti menyegerakkan", + "mobile-network-stop-syncing-details": "Sehingga disambungkan ke Wi-Fi?", + "mobile-network-use-mobile": "Gunakan data mudah alih", + "mobile-network-use-mobile-data": "Status menggunakan banyak data semasa menyegerakkan sembang dan dompet.", + "mobile-network-use-wifi": "Wi-Fi sahaja", + "mobile-syncing-sheet-details": "Status menggunakan banyak data semasa menyegerakkan sembang dan dompet.", + "mobile-syncing-sheet-title": "Segerakkan menggunakan data mudah alih?", + "more": "lebih banyak lagi", + "move-and-reset": "Alih dan Set Semula", + "move-keystore-file": "Pindahkan fail keystore", + "move-keystore-file-to-keycard": "Pindahkan fail kunci ke kad kekunci?", + "multiaccount-exists-content": "Kunci untuk akaun ini sudah ada dan tidak dapat ditambahkan lagi. Sekiranya anda kehilangan kata laluan, kod laluan atau Kad Kunci, nyahpasang aplikasi, pasang semula dan akses kunci anda dengan memasukkan frasa pilihan", + "multiaccount-exists-title": "Kunci untuk akaun ini sudah ada", + "multiaccounts-recover-enter-phrase-text": "Masukkan 12, 15, 18, 21 atau 24 perkataan.\n Pisahkan perkataan dengan satu ruang.", + "multiaccounts-recover-enter-phrase-title": "Masukkan frasa benih anda", + "mute": "Bisu", + "my-accounts": "Akaun saya", + "my-accounts-empty": "Akaun anda yang ada akan muncul di sini", + "my-profile": "Profil saya", + "my-status": "Status saya", + "name": "Nama", + "name-of-token": "Nama token anda", + "name-optional": "Nama (pilihan)", + "name-your-channel": "Namakan saluran anda", + "name-your-channel-placeholder": "Nama saluran", + "name-your-community": "Namakan komuniti anda", + "name-your-community-placeholder": "Nama yang menarik", + "need-help": "Perlukan bantuan?", + "network": "Rangkaian", + "network-chain": "Rangkaian rangkaian", + "network-details": "Perincian rangkaian", + "network-fee": "Bayaran rangkaian", + "network-id": "ID Rangkaian", + "network-info": "Maklumat rangkaian", + "network-invalid-network-id": "ID rangkaian yang ditentukan tidak sepadan dengan ID rangkaian oleh URL RPC", + "network-invalid-status-code": "Kod status tidak sah: {{code}}", + "network-invalid-url": "URL rangkaian tidak sah", + "network-settings": "Tetapan rangkaian", + "new": "Baru", + "new-category": "Kategori baru", + "new-chat": "Sembang baru", + "new-community-title": "Komuniti baru", + "new-contact": "Kenalan baru", + "new-contract": "Kontrak Baru", + "new-favourite": "Kegemaran baru", + "new-group": "Kumpulan baru", + "new-group-chat": "Sembang kumpulan baru", + "new-network": "Rangkaian baru", + "new-password": "Kata laluan baru", + "new-pin-description": "Masukkan kod laluan 6-digit baru", + "new-public-group-chat": "Sertai sembang awam", + "new-puk-description": "Masukkan 12-digit PUK baru", + "new-status": "Status baru", + "new-tab": "Tab baru", + "next": "Seterusnya", + "nickname": "Nama Panggilan", + "nickname-description": "Nama panggilan membantu anda mengenal pasti orang lain dalam Status.\n Hanya anda yang dapat melihat nama panggilan yang telah anda tambahkan", + "no": "Tidak", + "no-collectibles": "Tidak ada koleksi yang tersedia", + "no-contacts": "Belum ada kenalan", + "no-keycard-applet-on-card": "Tiada Keycard applet pada kad", + "no-messages": "Tiada mesej", + "no-pairing-slots-available": "Kad ini sudah dipasangkan dengan 5 peranti dan tidak dapat dipasangkan dengan kad ini. Sila gunakan salah satu peranti berpasangan, log masuk dengan kad ini dan lepaskan slot pasangan pada kad", + "no-pinned-messages": "Tiada mesej yang dipinkan", + "no-result": "Tiada keputusan", + "no-thanks": "Tidak, Terima kasih", + "no-tokens-found": "Token tidak dijumpai", + "node-address": "Alamat nod", + "node-details": "Butiran nod", + "node-info": "Maklumat nod", + "node-version": "Versi nod", + "nodes-disabled": "Nod status dinyahaktifkan", + "non-archival-node": "Titik akhir RPC tidak menyokong permintaan arkib. Sejarah pemindahan tempatan anda mungkin tidak lengkap.", + "non-contacts": "Bukan kenalan", + "nonce": "Bukan", + "none": "Tiada", + "normal": "Biasa", + "not-applicable": "Tidak terpakai untuk transaksi yang tidak ditandatangani", + "not-connected-nodes": "Tidak disambungkan ke nod status", + "not-connected-to-peers": "Tidak berhubung dengan peer", + "not-enough-snt": "Tidak cukup SNT", + "not-found": "Tidak dijumpai", + "not-keycard-text": "Kad yang anda gunakan bukan Kad Kunci. Anda perlu membeli Kad Kunci untuk menggunakannya", + "not-keycard-title": "Bukan Kad Kunci", + "not-registered": "tidak berdaftar", + "notification-settings": "Pemberitahuan", + "notifications": "Pemberitahuan", + "notifications-non-contacts": "Pemberitahuan daripada bukan kenalan", + "notifications-preferences": "Pilihan pemberitahuan", + "notifications-servers": "Pelayan pemberitahuan", + "notifications-switch": "Tunjukkan pemberitahuan", + "notifications-transactions": "Urus niaga dompet", + "notify": "Maklumkan", + "now": "Sekarang", + "off": "Mati", + "off-status-tree": "Pokok Status Mati", + "offline": "Luar talian", + "offline-messaging-use-history-explanation": "Benarkan nod Status untuk mengambil mesej yang telah dihantar semasa aplikasi ditutup. Apabila diaktifkan, nod Status mendapat alamat IP anda. Apabila dinyahdayakan, anda tidak akan menerima mesej apabila aplikasi ditutup dan tidak akan melihatnya apabila anda membuka aplikasi kemudian.", + "offline-messaging-use-history-nodes": "Gunakan nod Status", + "ok": "okey", + "ok-continue": "Ok, teruskan", + "ok-got-it": "Ok, teruskan", + "ok-save-pass": "OK, simpan kata laluan", + "okay": "baik", + "on": "Dihidupkan", + "on-status-tree": "Pada pokok Status", + "once-enabled-share-metadata": "Setelah didayakan, pautan yang disiarkan dalam sembang mungkin berkongsi metadata anda dengan tapak", + "one-day": "Satu hari", + "one-month": "Satu bulan", + "one-week": "Satu minggu", + "open": "Buka", + "open-chat": "Buka sembang", + "open-dapp": "Buka Ð Aplikasi", + "open-dapp-store": "Cari ÐApps", + "open-home": "Buka...", + "open-in-new-tab": "Buka di tab baru", + "open-membership": "Buka keahlian", + "open-nfc-settings": "Buka tetapan NFC", + "opening-buy-crypto": "Membuka {{site}}...", + "optimal": "Optimum", + "optional": "pilihan", + "or": "Atau", + "outgoing": "Keluar", + "outgoing-transaction": "Urus niaga keluar", + "page-camera-request-blocked": "permintaan kamera disekat. Untuk mengaktifkan permintaan kamera, pergi ke Tetapan", + "page-would-like-to-use-camera": "ingin menggunakan kamera anda", + "pair": "Pasangkan peranti", + "pair-card": "Pasangkan ke peranti ini", + "pair-code": "Kod berpasangan", + "pair-code-explanation": "Pasangkan kad ke peranti lain (hingga 5) untuk membuka kunci dan menandatangani urus niaga dengan Kad Kunci yang sama", + "pair-code-placeholder": "Pasangkan kod ...", + "pair-this-card": "Pasangkan kad ini", + "pair-this-device": "Iklankan peranti", + "pair-this-device-description": "Pasangkan peranti anda untuk menyegerakkan kenalan dan sembang di antara mereka", + "paired-devices": "Peranti berpasangan", + "pairing": "Berpasangan", + "pairing-card": "Kad berpasangan", + "pairing-changed": "Kod pasangan telah diubah", + "pairing-code-placeholder": "Meyambungkan kod ...", + "pairing-code_error1": "Kod berpasangan tidak sepadan.", + "pairing-go-to-installation": "Pergi ke tetapan memasang", + "pairing-maximum-number-reached-content": "Sila nyahdayakan salah satu peranti anda sebelum mengaktifkan yang baru.", + "pairing-maximum-number-reached-title": "Bilangan maksimum peranti dicapai", + "pairing-new-installation-detected-content": "Peranti baru telah dikesan.\nUntuk menggunakan peranti anda dengan betul, ia adalah penting untuk memasang dan mengaktifkannya sebelum menggunakannya.\nSila pergi ke bahagian peranti di bawah tetapan untuk memasangkan peranti anda.", + "pairing-new-installation-detected-title": "Peranti baru dikesan", + "pairing-no-info": "Tiada maklumat", + "pairing-please-set-a-name": "Sila tetapkan nama untuk peranti anda.", + "passphrase": "Frasa Laluan", + "password": "Kata Laluan", + "password-description": "Sekurang-kurangnya 6 karakter. Kata laluan anda memelihara kod anda. Anda akan memerlukan kata laluan ini untuk membuka Status dan bertransaksi.", + "password-mismatch": "Kata laluan dan pengesahan baru tidak sepadan", + "password-placeholder": "Kata Laluan ...", + "password-placeholder2": "Mengesahkan kata laluan anda", + "password-reset-in-progress": "Menukar kata laluan ...", + "password-reset-success": "Kata laluan ditukar", + "password-reset-success-message": "Anda perlu log masuk lagi", + "password_error1": "Kata laluan tidak sepadan.", + "paste": "Tampal", + "paste-json": "Tampal JSON", + "pay-to-chat": "Bayar untuk berbual", + "peer-content": "Peranti yang disambungkan ke rangkaian sembang Status. Setiap pengguna dapat mewakili satu atau lebih rakan sebaya, bergantung pada jumlah peranti mereka.", + "peer-title": "Rakan sebaya", + "peer-to-peer": "Peer to peer", + "peers": "Rakan sebaya", + "pending": "Yang belum selesai", + "pending-confirmation": "Menunggu pengesahan ...", + "pending-invitations": "Permintaan keahlian yang belum selesai", + "per-gas-price-limit": "Had harga berdasarkan gas", + "per-gas-tip-limit": "Had jumlah gas", + "permissions": "Kebenaran", + "phone-e164": "Antarabangsa 1", + "photos": "Gambar", + "photos-access-error": "Untuk memberikan izin foto yang diperlukan, pergi ke tetapan sistem anda dan pastikan Status > Foto dipilih.", + "pin": "Pin", + "pin-changed": "Kod laluan 6-digit telah ditukar", + "pin-code": "Kod laluan 6-digit", + "pin-limit-reached": "Had pin tercapai. Lepas pin mesej sebelumnya terlebih dahulu.", + "pin-mismatch": "Kod laluan yang salah", + "pin-one-attempt": "satu percubaan", + "pin-one-attempt-blocked-after": "sebelum Kad Kunci anda disekat", + "pin-one-attempt-blocked-before": "Hati-hati, anda hanya ada", + "pin-one-attempt-frozen-after": "sebelum Kad Kunci anda dibekukan", + "pin-one-attempt-frozen-before": "Hati-hati, anda hanya ada", + "pin-retries-left": "{{number}} percubaan tinggal", + "pinned-by": "Dipinkan oleh", + "pinned-messages": "Mesej-mesej yang dipinkan", + "pinned-messages-empty": "Mesej-mesej yang dipinkan akan muncul di sini. Untuk mengepin mesej, tekan dan tahannya dan ketik 'Pin'", + "preference": "Pilihan", + "preview-privacy": "Pratonton mod privasi", + "previewing-may-share-metadata": "Pratonton pautan dari laman web ini dapat berkongsi metadata anda dengan pemiliknya", + "principles": "Prinsip", + "privacy": "Privasi", + "privacy-and-security": "Privasi dan keselamatan", + "privacy-photos": "Privasi Foto Profil", + "privacy-policy": "Dasar Privasi", + "private-key": "Kunci peribadi", + "private-notifications": "Pemberitahuan peribadi", + "private-notifications-descr": "Status akan memberitahu anda mengenai mesej baru. Anda boleh mengedit pilihan pemberitahuan anda kemudian dalam tetapan.", + "processing": "Sekejap", + "product-information": "Maklumat Produk", + "profile": "Profil", + "profile-deleted-content": "Profil anda berjaya dipadamkan", + "profile-deleted-keycard": "Anda kini boleh memulihkan pad kekunci lain pada Kad Kunci anda", + "profile-deleted-title": "Profil dipadamkan", + "profile-details": "Maklumat profil", + "profile-not-found": "Profil tidak dijumpai", + "profile-pic-pick": "Pilih dari galeri", + "profile-pic-remove": "Buang foto", + "profile-pic-take": "Ambil gambar", + "public-channel": "Saluran awam", + "public-chat": "Sembang awam", + "public-chat-description": "Sertai sembang awam untuk minat anda! Sesiapa sahaja boleh memulakan yang baru.", + "public-chats": "Sembang awam", + "public-group-status": "Awam", + "public-group-topic": "Topik", + "public-key": "Kunci awam", + "puk-and-pairing-codes-displayed": "Kod PUK dan kod pasangan dipaparkan", + "puk-changed": "PUK 12 digit telah diubah", + "puk-code": "Kod PUK", + "puk-code-explanation": "Sekiranya anda terlupa kod laluan 6 digit anda atau memasukkannya dengan salah 3 kali, anda memerlukan kod ini untuk membuka kunci kad anda.", + "puk-mismatch": "Kod PUK salah", + "push-failed-transaction": "Urus niaga anda gagal", + "push-failed-transaction-body": "{{value}} {{currency}} hingga {{to}}", + "push-inbound-transaction": "Anda menerima {{value}} {{currency}}", + "push-inbound-transaction-body": "Dari {{from}} hingga {{to}}", + "push-notifications-server-enabled": "Pelayan didayakan", + "push-notifications-servers": "Tolak pelayan pemberitahuan", + "push-outbound-transaction": "Anda telah menghantar {{value}} {{currency}}", + "push-outbound-transaction-body": "Dari {{from}} hingga {{to}}", + "quiet-days": "{{quiet-days}} hari", + "quiet-hours": "{{quiet-hours}} jam", + "re-encrypt-key": "Masukkan semula kunci anda", + "receive": "Menerima", + "receive-transaction": "Terima urus niaga", + "recent": "Terkini", + "recent-empty": "Alamat yang baru digunakan akan muncul di sini", + "recent-recipients": "Kenalan", + "recently-used-stickers": "Pelekat yang baru digunakan akan terpapar di sini", + "recipient": "Penerima", + "recipient-code": "Masukkan alamat penerima", + "recipient-code-placeholder": "0x ... atau nama pengguna.domain.eth", + "recover": "Pulihkan", + "recover-key": "Akses kunci yang ada", + "recover-keycard-multiaccount-not-supported": "Kunci untuk akaun ini sudah ada dan tidak dapat ditambahkan lagi. Sekiranya anda kehilangan kata laluan, kod laluan atau Kad Kunci, nyahpasang aplikasi, pasang semula dan akses kunci anda dengan memasukkan frasa pilihan anda", + "recover-with-keycard": "Pulihkan dengan Kad Kunci", + "recover-with-seed-phrase": "Pulih dengan frasa benih", + "recovering-key": "Mengakses kunci ...", + "recovery-confirm-phrase": "Sahkan frasa", + "recovery-phrase": "Frasa pemulihan", + "recovery-success-text": "Anda perlu membuat kod atau kata laluan baru untuk menyulitkan semula kunci anda", + "recovery-typo-dialog-description": "Harap maklum, ungkapan benih anda mesti menggunakan perkataan dan pesanan yang sama persis seperti yang anda terima", + "recovery-typo-dialog-title": "Adakah frasa tersebut tepat?", + "redeem-amount": "{{quantity}} bonus yang dipunyai", + "redeem-now": "Tebus sekarang", + "redeem-success": "Penebusan bonus berjaya!", + "reduced-tip": "tip keutamaan akan dikurangkan", + "refresh": "Segarkan", + "registered": "berdaftar", + "reject-and-delete": "Tolak dan padam", + "remember-me": "Ingat saya", + "remind-me-later": "Tunjukkan ini lagi", + "remove": "Keluarkan", + "remove-favourite": "Buang kegemaran", + "remove-from-chat": "Alih keluar dari sembang", + "remove-from-contacts": "Alih keluar dari kenalan", + "remove-from-contacts-text": "Dengan mengeluarkan pengguna dari senarai kenalan anda, anda tidak menyembunyikan alamat dompet anda dari mereka", + "remove-group": "Keluarkan kumpulan", + "remove-network": "Keluarkan rangkaian", + "remove-token": "Keluarkan token", + "removed": "dikeluarkan", + "repeat-pin": "Ulangi kod laluan 6 digit baru", + "repeat-puk": "Ulangi PUK 12 digit baru", + "replying-to": "Membalas kepada {{author}}", + "report-bug-email-template": "1. Penerangan Isu\n (Jelaskan ciri yang anda mahukan, atau ringkaskan pepijat dan apa yang anda buat, apa yang anda harapkan berlaku, dan apa yang sebenarnya berlaku. Bahagian di bawah) \n\n\n 2. Langkah untuk membiak\n (Terangkan bagaimana kita dapat meniru bug langkah demi langkah.)\n -Buka Status\n -...\n -Langkah 3, dll. \n\n\n 3. Tingkah laku yang diharapkan\n (Jelaskan apa yang anda jangkakan akan berlaku.) \n\n\n 4. Tingkah laku sebenar\n (Huraikan apa yang sebenarnya berlaku.) \n\n\n 5. Lampirkan tangkapan skrin yang dapat menunjukkan masalah\n", + "request-access": "Minta akses", + "request-feature": "Mohon ciri", + "request-membership": "Minta keahlian", + "request-pending": "Permintaan tertangguh…", + "request-transaction": "Minta transaksi", + "required-field": "Medan wajib", + "resend-message": "Hantar semula", + "reset-card": "Tetapkan semula kad", + "reset-card-description": "Operasi ini akan menetapkan semula kad ke keadaan asal. Ia akan memadamkan semua data kad termasuk kunci peribadi. Operasi ini tidak dapat dipulihkan kembali.", + "reset-database": "Tetapkan semula pangkalan data", + "reset-database-warning": "Padamkan sembang, kenalan dan seting. Diperlukan apabila anda kehilangan kata laluan anda", + "reset-database-warning-keycard": "Padamkan sembang, kenalan dan tetapan.", + "reset-password": "Set semula kata laluan", + "restore-defaults": "Kembali kepada asal", + "retry": "Cuba lagi", + "revoke-access": "Batalkan akses", + "rpc-url": "URL RPC", + "rpc-usage-copy": "Salinan", + "rpc-usage-filter": "Kaedah tapisan", + "rpc-usage-get-stats": "Segarkan", + "rpc-usage-info": "Statistik penggunaan RPC", + "rpc-usage-reset": "Set semula", + "save": "Jimat", + "save-password": "Simpan katalaluan", + "save-password-unavailable": "Tetapkan kod laluan peranti untuk menyimpan kata laluan", + "save-password-unavailable-android": "Simpan kata laluan tidak tersedia: peranti anda mungkin di-root atau tidak mempunyai ciri keselamatan yang diperlukan.", + "scan-qr": "Imbas kod QR", + "scan-qr-code": "Imbas kod QR dengan alamat dompet", + "scan-tokens": "Imbasan ", + "search": "Cari", + "search-no-chat-found": "Tiada hasil carian. Adakah awak bermaksud", + "secret-keys-confirmation-text": "Anda memerlukan mereka untuk terus menggunakan Kad Kunci anda sekiranya anda kehilangan telefon.", + "secret-keys-confirmation-title": "Menulis kod?", + "security": "Keselamatan", + "see-details": "Lihat perincian", + "see-it-again": "LIHAT LAGI", + "see-suggestions": "Lihat cadangan", + "seed-key-uid-mismatch": "Benih tidak sepadan", + "seed-key-uid-mismatch-desc-1": "Frasa benih yang anda masukkan tidak sepadan dengan {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Untuk menguruskan kekunci untuk akaun ini menentusahkan frasa benih anda dan cuba lagi.", + "seed-phrase-content": "Satu set perkataan yang mudah dibaca, dipilih secara rawak dari senarai standard BIP39 dan digunakan untuk memulihkan atau mengakses akaun Ethereum anda di dompet dan peranti lain. Juga disebut sebagai \"frasa mnemonik\", \"frasa pemulihan\" atau \"sandaran dompet\" di seluruh ekosistem crypto. Sebilangan besar aplikasi crypto menggunakan standard yang sama untuk menjana akaun.", + "seed-phrase-placeholder": "Frasa benih ...", + "seed-phrase-title": "Frasa Benih", + "select": "Pilih", + "select-account": "Pilih akaun", + "select-account-dapp": "Pilih akaun yang ingin anda gunakan dengan Dapps", + "select-account-first": "Pilih akaun terlebih dahulu", + "select-chat": "Pilih sembang untuk memulakan pesanan", + "select-new-location-for-keys": "Pilih lokasi baru untuk menyimpan kunci peribadi anda", + "selected": "Terpilih", + "send-logs": "Laporkan pepijat", + "send-logs-to": "Laporkan pepijat ke {{email}}", + "send-message": "Menghantar mesej", + "send-push-notifications-description": "Apabila dilumpuhkan, orang yang menerima mesej anda tidak akan diberitahu mengenai kedatangan mereka", + "send-request": "Hantar permintaan", + "send-request-amount": "Jumlah", + "send-request-amount-max-decimals": "Bilangan perpuluhan maksimum ialah {{asset-decimals}}", + "send-request-unknown-token": "Token tidak diketahui - {{asset}}", + "send-sending-to": "kepada {{recipient-name}}", + "send-transaction": "Hantar transaksi", + "sending": "Menghantar", + "sent-at": "Dihantar pada", + "server": "Pelayan", + "set-a-topic": "Buat topik", + "set-currency": "Tetapkan mata wang", + "set-custom-fee": "Tetapkan bayaran tersuai", + "set-dapp-access-permissions": "Tetapkan kebenaran akses DApp", + "set-max": "Tetapkan maks", + "settings": "Tetapan", + "share": "Berkongsi", + "share-address": "Kongsi alamat", + "share-chat": "Kongsi sembang", + "share-contact-code": "Kongsi kod pengenalan saya", + "share-dapp-text": "Lihat DApp ini yang saya gunakan di Status: {{link}}", + "share-link": "Kongsi pautan", + "share-my-profile": "Kongsi profil saya", + "share-profile": "Kongsi profil", + "share-profile-link": "Kongsi pautan profil", + "share-public-chat-text": "Lihat sembang umum ini di aplikasi Status: {{link}}", + "shared": "Dikongsi", + "sharing-copied-to-clipboard": "Disalin", + "sharing-copy-to-clipboard": "Salinan", + "sharing-data-desc-1": "Data disahkan berdasarkan peraturan awam untuk memastikan tidak ada data sensitif yang dikirim. Jangan percaya, sahkan.", + "sharing-data-desc-2": "Data penggunaan dihantar dari hujung ke hujung dienkripsi melalui rangkaian peer-to-peer Status", + "sharing-data-desc-3": "Daripada kekunci sembang biasa anda, kunci penggunaan sekali sahaja digunakan", + "sharing-data-desc-4": "Penggunaan data tidak akan dikaitkan dengan alamat IP anda", + "sharing-data-desc-5": "Data kumulatif semua pengguna boleh didapati secara umum", + "sharing-data-desc-6": "Data dialih keluar daripada telefon anda selepas ia dihantar", + "sharing-share": "Berkongsi", + "show-less": "Tunjukkan kurang", + "show-more": "Tunjukkan lagi", + "show-notifications": "Tunjukkan pemberitahuan", + "show-profile-pictures": "Lihat gambar profil daripada", + "show-profile-pictures-to": "Tunjukkan gambar profil anda kepada", + "show-qr": "Tunjukkan kod QR", + "show-transaction-data": "Tunjukkan data transaksi", + "sign-and-send": "Tandatangan dan hantar", + "sign-anyway": "Masuk pula", + "sign-in": "Daftar masuk", + "sign-message": "Mesej Tanda", + "sign-out": "Keluar", + "sign-request-failed": "Tidak dapat menandatangani mesej", + "sign-with": "Masuk dengan", + "sign-with-password": "Masuk dengan kata laluan", + "sign-you-in": "Log masuk anda…", + "signing": "Menandatangani", + "signing-a-message": "Menandatangani mesej", + "signing-phrase": "Menulis frasa", + "skip": "Langkau", + "slow": "Perlahan", + "something-went-wrong": "Ada yang tidak kena", + "soon": "Tidak lama lagi", + "specify-address": "Nyatakan alamat", + "specify-name": "Nyatakan nama", + "specify-network-id": "Tentukan id rangkaian", + "specify-rpc-url": "Tentukan URL RPC", + "specify-server-public-key": "Masukkan kunci awam pelayan", + "start-chat": "Mulakan sembang", + "start-conversation": "Mulakan perbualan", + "start-group-chat": "Mulakan sembang kumpulan", + "start-new-chat": "Mulakan sembang baru", + "starter-pack-coming": "Starter Pack akan datang", + "starter-pack-coming-description": "Boleh mengambil masa beberapa minit hingga berjam-jam", + "starter-pack-received": "Pek Permulaan", + "starter-pack-received-description": "Berikut adalah beberapa kripto sebagai permulaan! Gunakannya untuk mendapatkan pelekat, nama ENS dan mencuba dapps", + "status": "Status", + "status-confirmed": "Disahkan", + "status-hardwallet": "Hardwallet status", + "status-is-open-source": "Status adalah sumber terbuka", + "status-keycard": "Kad Kekunci Status", + "status-mobile-descr": "Status cenderung menggunakan banyak data semasa menyegerakkan sembang. Anda boleh memilih untuk tidak menyegerakkan ketika berada di rangkaian mudah alih", + "status-not-sent-click": "Tidak disahkan. Klik untuk pilihan", + "status-not-sent-tap": "Tidak disahkan. Ketik untuk pilihan", + "status-pending": "Yang belum selesai", + "status-sent": "Dihantar", + "status-tx-not-found": "TX tidak dijumpai", + "status-updates-descr": "Kemas kini status akan muncul di sini. Tambahkan profil sebagai kenalan untuk menerima kemas kini mengenai garis masa anda.", + "statuses-descr": "Kongsi perkara yang ada di fikiran anda dan kekal dikemaskini dengan kenalan anda", + "statuses-my-status-descr": "Kongsikan apa yang ada di fikiran anda. Sesiapa yang mengunjungi profil anda akan dapat melihat status anda. Orang yang menambahkan anda sebagai kenalan mereka akan menerima kemas kini anda pada garis masa mereka", + "step-i-of-n": "Langkah {{step}} daripada {{number}}", + "sticker": "Pelekat", + "sticker-market": "Pasaran pelekat", + "storage": "Penyimpanan", + "submit": "Hantar", + "submit-bug": "Hantar pepijat", + "success": "Kejayaan", + "suggested-min-tip": "Cadangan tip minima", + "suggested-price-limit": "Had harga yang dicadangkan", + "symbol": "Simbol", + "sync-all-devices": "Segerakkan semua peranti", + "sync-in-progress": "Menyegerakkan ...", + "sync-settings": "Tetapan penyegerakan", + "sync-synced": "Dalam penyegerakan", + "syncing-devices": "Sedang disegerakkan...", + "system": "Sistem", + "tabs": "Tab-tab", + "tag-was-lost": "Tag telah hilang", + "tap-card-again": "Ketik kad ke bahagian belakang telefon anda sekali lagi", + "terms-of-service": "Syarat Penggunaan", + "test-networks": "Rangkaian ujian", + "text-input-disabled": "Sila tunggu sebentar...", + "thank-you": "Terima kasih", + "this-device": "Peranti ini", + "this-device-desc": "Kunci anda akan dienkripsi dan disimpan dengan selamat di peranti anda", + "this-is-you-signing": "Ini adalah frasa menandatangani anda", + "this-will-take-few-seconds": "Ini akan mengambil masa beberapa saat", + "three-days": "Tiga hari", + "three-words-description": "Anda harus melihat 3 perkataan ini sebelum menandatangani setiap transaksi", + "three-words-description-2": "Sekiranya anda melihat kombinasi yang berbeza, batalkan transaksi dan log keluar", + "timeline": "Garis Masa", + "tip-cap": "Had petua", + "to": "Untuk", + "to-block": "Sekat", + "to-enable-biometric": "Untuk mengaktifkan {{bio-type-label}} , anda mesti menyimpan kata laluan anda di skrin buka kunci", + "to-encrypt-enter-password": "Untuk menyulitkan akaun sila masukkan kata laluan anda", + "to-see-this-message": "Untuk melihat mesej ini,", + "token-auto-validate-decimals-error": "Perpuluhan token yang salah {{symbol}} pada alamat {{address}} - ditetapkan ke {{expected}} tetapi dikesan sebagai {{actual}}", + "token-auto-validate-name-error": "Nama token yang salah {{symbol}} di alamat {{address}} - ditetapkan ke {{expected}} tetapi dikesan sebagai {{actual}}", + "token-auto-validate-symbol-error": "Simbol salah untuk token {{symbol}} di alamat {{address}} - ditetapkan ke {{expected}} tetapi dikesan sebagai {{actual}}", + "token-details": "Butiran token", + "topic-name-error": "Gunakan hanya huruf kecil (a hingga z), nombor & sengkang (-). Jangan gunakan kod pengenalan", + "transaction": "Transaksi", + "transaction-data": "Data transaksi", + "transaction-declined": "Transaksi ditolak", + "transaction-description": "Transaksi dengan 12 pengesahan boleh dianggap tidak dapat diubah lagi", + "transaction-details": "Butiran transaksi", + "transaction-failed": "transaksi gagal", + "transaction-history": "Sejarah transaksi", + "transaction-request": "Permintaan Transaksi", + "transaction-sent": "Transaksi dihantar", + "transaction-signed": "Urus niaga berjaya ditandatangani", + "transactions": "Urus Niaga", + "transactions-filter-select-all": "Pilih semua", + "transactions-filter-title": "Tapis sejarah", + "transactions-history": "Sejarah transaksi", + "transactions-history-empty": "Belum ada transaksi dalam sejarah anda", + "transactions-history-loading": "Memuatkan sejarah transaksi. Ini mungkin mengambil masa.", + "transactions-load-more": "Tambah lagi", + "transactions-management-enabled": "Pengurusan urus niaga (alpha)", + "transactions-sign": "Tanda", + "transfer-ma-unknown-error-desc-1": "Nampaknya banyak akaun anda tidak dipadamkan. Pangkalan data mungkin telah ditetapkan semula", + "transfer-ma-unknown-error-desc-2": "Sila periksa senarai akaun anda dan cuba lagi. Sekiranya akaun tidak disenaraikan pergi ke Akses kunci yang ada untuk pulih dengan frasa biji", + "transfers-fetching-failure": "Sejarah pemindahan tidak dapat dikemaskini. Semak sambungan anda dan tarik ke bawah untuk mencuba lagi", + "tribute-required-by-multiaccount": "{{multiaccount-name}} memerlukan SNT untuk memulakan sembang.", + "tribute-state-paid": "Penghormatan dibayar", + "tribute-state-pending": "Penghormatan belum selesai", + "tribute-state-required": "Memerlukan penghormatan SNT {{snt-amount}}", + "tribute-to-talk": "Penghormatan untuk bercakap", + "tribute-to-talk-add-friends": "Tambahkan rakan sebagai kenalan untuk membenarkan sembang tanpa pembayaran penghormatan.", + "tribute-to-talk-are-you-friends": "Adakah anda kawan?", + "tribute-to-talk-ask-to-be-added": "Minta ditambahkan sebagai kenalan", + "tribute-to-talk-contact-received-your-tribute": "menerima penghormatan anda. Anda kini boleh berbual dengan selamat antara satu sama lain.", + "tribute-to-talk-desc": "Wangkan perhatian anda dengan meminta SNT untuk orang baru memulakan sembang", + "tribute-to-talk-disabled": "Tribute to Talk dilumpuhkan", + "tribute-to-talk-disabled-note": "Mulai sekarang, orang baru dapat memulakan sembang dengan anda tanpa menghantar SNT.", + "tribute-to-talk-enabled": "Anda telah mengaktifkan Tribute to Talk.", + "tribute-to-talk-finish-desc": "Mulai sekarang, anda hanya akan menerima sembang dari kenalan dan orang yang membayar", + "tribute-to-talk-learn-more-1": "Masa dan perhatian anda adalah aset paling berharga anda. Tribute to Talk membolehkan anda menetapkan jumlah SNT yang diperlukan bagi orang baru untuk memulakan sembang dengan anda.", + "tribute-to-talk-learn-more-2": "Sesiapa yang tidak ada dalam senarai kenalan anda akan diminta untuk membayar, dan anda boleh membalasnya setelah mereka memilikinya.", + "tribute-to-talk-learn-more-3": "Anda selalu dapat mengembalikan wang, tetapi untuk memastikan rakan dapat menghubungi anda dengan bebas, tambahkan mereka sebagai kenalan terlebih dahulu.", + "tribute-to-talk-paywall-learn-more-1": "Masa dan perhatian kita adalah aset paling berharga kita. Tribute to Talk membolehkan anda menghubungi orang baru sebagai pertukaran untuk pembayaran SNT.", + "tribute-to-talk-paywall-learn-more-2": "Untuk memulakan sembang dengan seseorang yang mempunyai set penghargaan, cukup bayar SNT yang diperlukan dan anda akan ditambahkan sebagai kenalan.", + "tribute-to-talk-paywall-learn-more-3": "Sekiranya anda mengenali mereka, anda boleh berkongsi profil anda di luar Status untuk ditambahkan secara percuma.", + "tribute-to-talk-pending": "Penghormatan menunggu pengesahan", + "tribute-to-talk-pending-note": "Transaksi penghormatan menunggu pengesahan di rangkaian. Anda boleh menyemak statusnya dalam sejarah transaksi", + "tribute-to-talk-removing-note": "Mengeluarkan Tribute to Talk akan membolehkan orang baru memulakan sembang tanpa menghantar SNT. Memerlukan transaksi yang akan dibuat.", + "tribute-to-talk-set-snt-amount": "Tetapkan jumlah SNT yang diperlukan bagi orang baru untuk memulakan sembang", + "tribute-to-talk-signing": "Menunggu untuk menandatangani transaksi", + "tribute-to-talk-transaction-failed-note": "Transaksi telah gagal dan tetapan Tribute to Talk anda belum diubah", + "tribute-to-talk-tribute-received1": "Penghormatan diterima. Anda dan", + "tribute-to-talk-tribute-received2": "kini menjadi kenalan dan boleh berbual dengan selamat antara satu sama lain", + "tribute-to-talk-you-require-snt": "Anda memerlukan SNT untuk orang baru untuk memulakan sembang.", + "try-again": "Cuba lagi", + "try-keeping-the-card-still": "Cuba simpan kad itu", + "turn-nfc-description": "NFC dilumpuhkan pada peranti anda. Anda boleh mengaktifkannya dalam tetapan", + "turn-nfc-on": "Hidupkan NFC untuk meneruskan", + "tx-fail-description1": "Urus niaga ini kemungkinan akan gagal. Masuk dengan risiko anda sendiri menggunakan bayaran rangkaian khusus.", + "tx-fail-description2": "Urus niaga ini kemungkinan akan gagal. Tetapkan bayaran rangkaian khusus untuk menandatangani dengan risiko anda sendiri.", + "type": "Jenis", + "type-a-message": "Mesej", + "ulc-enabled": "ULC didayakan", + "unable-to-fetch": "Tidak dapat mengambil sejarah sembang", + "unable-to-read-this-code": "Tidak dapat membaca kod ini", + "unable-to-send-messages": "Tidak dapat menghantar dan menerima mesej-mesej", + "unblock": "Buka sekatan", + "unblock-contact": "Nyahsekat pengguna ini", + "unknown-status-go-error": "Ralat status-go yang tidak diketahui", + "unlock": "Buka kunci", + "unmute": "Tidak bersuara", + "unpair-card": "Nyahpasang kad", + "unpair-card-confirmation": "Operasi ini akan memutuskan kad dari peranti semasa. Memerlukan kebenaran kod laluan 6 digit. Adakah anda mahu meneruskan?", + "unpair-keycard": "Nyahpasang Kad Kunci dari telefon ini", + "unpair-keycard-warning": "Kod pasangan \/ PUK & PIN anda tidak berubah", + "unpaired-keycard-text": "Kad Kunci yang anda ketik tidak dikaitkan dengan telefon ini", + "unpaired-keycard-title": "Nampaknya kad anda tidak berpasangan", + "unpin": "Lepaskan pin", + "update": "Kemas kini", + "update-to-listen-audio": "Kemas kini ke versi terkini untuk mendengar mesej audio di sini!", + "update-to-see-image": "Kemas kini ke versi terkini untuk melihat gambar yang bagus di sini!", + "update-to-see-sticker": "Kemas kini ke versi terkini untuk melihat pelekat yang bagus di sini!", + "updates-to-tos": "Kemas kini kepada Syarat-syarat Penggunaan", + "updates-to-tos-desc": "Sebelum anda meneruskan, sila tinjau Syarat Penggunaan dan sahkan bahawa anda bertanggungjawab sepenuhnya mengenai cara anda menggunakan aplikasi ini.", + "url": "URL", + "usd-currency": "USD", + "use-valid-contact-code": "Sila masukkan atau imbas kod pengenalan atau nama pengguna yang sah", + "validation-amount-invalid-number": "Jumlah bukan nombor yang sah", + "validation-amount-is-too-precise": "Jumlahnya terlalu tepat. Bilangan perpuluhan maksimum ialah {{decimals}} .", + "verified-community": "✓ Komuniti yang disahkan", + "version": "Versi apl", + "view": "Lihat", + "view-cryptokitties": "Lihat di CryptoKitties", + "view-cryptostrikers": "Lihat di CryptoStrikers", + "view-data": "Lihat data", + "view-details": "Lihat butiran", + "view-etheremon": "Lihat di Etheremon", + "view-gitcoin": "Lihat di Gitcoin", + "view-profile": "Lihat profil", + "view-public-dashboard": "Lihat papan pemuka awam", + "view-rules": "Lihat peraturan", + "view-signing": "Lihat frasa menandatangani", + "view-superrare": "Lihat di SuperRare", + "waiting-for-wifi": "Tanpa Wi-fi, penyegerakan mesej dilumpuhkan.", + "waiting-for-wifi-change": "Tetapan", + "waiting-to-sign": "Menunggu untuk menandatangani transaksi ...", + "waiting-wi-fi": "Menunggu Wi-Fi...", + "waku-bloom-filter-mode": "Mod penapis mekar Waku", + "wakuv2-change-nodes": "Adakah anda pasti mahu menukar nod Wakuv2?", + "wakuv2-node-format": "\/ ip4 \/ {node-ip} \/ tcp \/ {port} \/ p2p \/ {id}", + "wakuv2-settings": "Tetapan Waku v2", + "wallet": "Dompet", + "wallet-address": "Alamat dompet", + "wallet-asset": "Aset", + "wallet-assets": "Aset", + "wallet-backup-recovery-title": "Sandarkan frasa pemulihan anda", + "wallet-choose-recipient": "Pilih Penerima", + "wallet-collectibles": "Barang koleksi", + "wallet-insufficient-funds": "Kekurangan dana", + "wallet-insufficient-gas": "Tidak cukup ETH untuk gas", + "wallet-invalid-address": "Alamat tidak sah:\n {{data}}", + "wallet-invalid-address-checksum": "Ralat dalam alamat:\n {{data}}", + "wallet-invalid-chain-id": "Rangkaian tidak sepadan:\n {{data}} tetapi rantaian semasa adalah {{chain}}", + "wallet-key-content": "Alamat hex 64 aksara berdasarkan standard Ethereum dan bermula dengan 0x. Secara terbuka, alamat akaun anda akan dikongsi dengan orang lain semasa anda ingin menerima dana. Juga disebut sebagai \"alamat Ethereum\" atau \"alamat dompet.\"", + "wallet-key-title": "Alamat akaun", + "wallet-manage-accounts": "Urus akaun", + "wallet-manage-assets": "Urus Aset", + "wallet-request": "Permintaan", + "wallet-send": "Hantar", + "wallet-send-min-units": "Min 21000 unit", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Tetapan dompet", + "wallet-total-value": "Jumlah nilai", + "wallet-transaction-total-fee": "Jumlah Bayaran", + "wants-to-access-profile": "mahu mengakses ke profil anda", + "warning": "Amaran", + "warning-message": "Maaf, kami mengehadkan menghantar beberapa mesej berturut-turut untuk mengelakkan spam. Sila cuba sebentar lagi", + "warning-sending-to-contract-descr": "Alamat yang anda masukkan adalah kontrak pintar, menghantar dana ke alamat ini dapat mengakibatkan kehilangan dana. Untuk berinteraksi dengan DApp, buka DApp di Status DApp Browser.", + "watch-only": "Hanya menonton", + "wc-brand-guide": "Panduan penggunaan penjenamaan seperti tanda dagangan dan logo", + "wc-disclaimer": "Penafian (termasuk penyedia pihak ketiga), jaminan, dan pelepasan undang-undang", + "wc-dispute": "Peruntukan penyelesaian pertikaian", + "wc-how-to-use-status-app": "Cara menggunakan aplikasi Status termasuk privasi dan keselamatan", + "wc-new-tos-based-on-principles-prefix": "Terma Penggunaan Baru yang direka berdasarkan kami", + "web-view-error": "Tidak dapat memuatkan halaman", + "websites": "Laman web", + "webview-camera-permission-requests": "Permintaan kebenaran kamera webview", + "webview-camera-permission-requests-subtitle": "Apabila diaktifkan, laman web dan dapp dapat meminta untuk menggunakan kamera anda", + "welcome-blank-community-message": "Komuniti anda akan muncul di sini.", + "welcome-blank-message": "Sembang anda akan muncul di sini. Untuk memulakan sembang baru tekan butang ⊕", + "welcome-community-blank-message": "Sembang anda akan muncul di sini. Untuk memulakan sembang baru, klik pada 3 titik di atas dan pilih \"Buat saluran\"", + "welcome-screen-text": "Sediakan dompet anda, jemput rakan untuk berbual\n dan layari dapp yang popular!", + "welcome-to-status": "Selamat datang ke Status", + "welcome-to-status-description": "Sediakan dompet crypto anda, jemput rakan untuk berbual dan melayari aplikasi terdesentralisasi", + "what-changed": "Apa yang berubah", + "what-is-shared": "Apa yang dikongsi", + "whats-on-your-mind": "Apa yang kamu fikirkan…", + "word-count": "Kiraan perkataan", + "word-n": "Perkataan # {{number}}", + "word-n-description": "Untuk memeriksa sama ada anda telah menyandarkan frasa pemulihan anda dengan tepat, masukkan perkataan #{{number}} di atas.", + "words-n": { + "other": "1 kata" + }, + "write-down-and-store-securely": "Tuliskan kod\n & simpan dengan selamat", + "wrong-address": "Salah alamat", + "wrong-card": "Kad yang salah", + "wrong-card-text": "Kad yang diketuk tidak sesuai dengan kunci yang anda pilih", + "wrong-contract": "Kontrak yang salah", + "wrong-keycard-text": "Kad Kunci yang anda ketik tidak dikaitkan dengan telefon ini", + "wrong-keycard-title": "Nampaknya anda sudah mengetuk\n kad kekunci yang salah", + "wrong-password": "Salah kata laluan", + "wrong-word": "Perkataan yang salah", + "yes": "Ya", + "you": "anda", + "you-already-have-an-asset": "Anda sudah mempunyai aset {{value}}", + "you-are-all-set": "Anda telah siap sedia!", + "you-are-all-set-description": "Jika anda kehilangan telefon anda, anda boleh mengakses dana dan kod pengenalan anda menggunakan frasa pemulihan", + "you-can-change-account": "Anda boleh menukar nama dan warna akaun mengikut kehendak anda", + "you-can-choose-preview-websites": "Anda boleh memilih mana dari laman web berikut yang dapat melihat pratonton pautan keterangan dan gambar dalam sembang", + "you-can-fetch": "Anda boleh ambil sejarah sembang", + "you-dont-have-contacts": "Anda belum mempunyai kenalan.", + "you-dont-have-contacts-invite-friends": "Anda belum mempunyai kenalan.\n Jemput rakan anda untuk mula berbual.", + "you-dont-have-stickers": "Anda tidak mempunyai sebarang pelekat lagi", + "you-will-need-this-code": "Anda memerlukan kod ini untuk membuka Status dan menandatangani transaksi", + "you-will-start-from-scratch": "Anda akan bermula dari awal dengan satu set kunci baru", + "your-card-is-frozen": "Kad Kunci anda dibekukan. Tetapkan semula akses kad", + "your-contact-code": "Memberi akses membenaran DApp ini untuk mengambil kod pengenalan anda", + "your-data-belongs-to-you": "Jika anda kehilangan frasa pemulihan anda, anda kehilangan data dan dana anda", + "your-data-belongs-to-you-description": "Sekiranya anda kehilangan akses, misalnya dengan kehilangan telefon anda, anda hanya dapat mengakses kunci anda dengan frasa pilihan anda. Tiada siapa, tetapi anda mempunyai ungkapan benih anda. Tuliskan. Pastikan ia selamat", + "your-keys": "Kunci anda", + "your-price-limit": "Had harga anda", + "your-recovery-phrase": "Frasa pemulihan anda", + "your-recovery-phrase-description": "Ini adalah frasa pemulihan anda. Anda menggunakannya untuk membuktikan bahawa ini adalah dompet anda. Anda hanya dapat melihatnya sekali sahaja! Tuliskannya pada kertas dan simpan di tempat yang selamat. Anda akan memerlukannya jika anda kehilangan atau memasang semula dompet anda.", + "your-tip-limit": "Had tip anda", + "youre-on-mobile-network": "Anda menggunakan rangkaian mudah alih" +} diff --git a/translations/nb.json b/translations/nb.json new file mode 100644 index 00000000000..530b3ef3f8c --- /dev/null +++ b/translations/nb.json @@ -0,0 +1,145 @@ +{ + "confirm": "Bekreft", + "amount": "Beløp", + "members-active": { + "one": "1 medlem", + "other": "{{count}} medlemmer", + "zero": "ingen medlemmer" + }, + "chat-name": "Kallenavn", + "phew-here-is-your-passphrase": "*Phew* det var vrient, her er din nye passordfrase, *skriv den ned og hold den for deg selv!* Du vil trenge den for å kunne gjenopprette kontoen.", + "public-group-topic": "Emne", + "chat-settings": "Chat instillinger", + "offline": "Avlogget", + "invited": "invitert", + "address": "Adresse", + "new-public-group-chat": "Bli med i offentlig chat", + "datetime-hour": { + "one": "time", + "other": "timer" + }, + "wallet-settings": "Lommeboksinstillinger", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "For å tillate bruk av kamera, gå til systeminstillinger og sørg for at Status > Kamera er aktivert.", + "remove": "Fjern", + "add-members": "Legg til medlemmer", + "photos-access-error": "For å tillate bruk av bilder, gå til systeminstillinger og sørg for at Status > Bilder er aktivert.", + "done": "Ferdig", + "close-chat": "Fjern melding", + "new-group-chat": "Ny gruppemelding", + "wallet": "Lommebok", + "wallet-request": "Forespørsel", + "sign-in": "Logg på", + "datetime-yesterday": "i går", + "create-new-account": "Opprett konto", + "datetime-ago": "siden", + "contacts": "Kontaker", + "got-it": "Mottatt", + "active-online": "Pålogget", + "password": "Passord", + "browsing-title": "Utforsk", + "transactions-history-empty": "Du har ingen tidligere transaksjoner", + "discover": "Oppdag", + "browsing-cancel": "Avbryt", + "intro-status": "Chat med meg for å sette opp kontoen din og endre instillinger!", + "name": "Navn", + "show-qr": "Vis QR-kode", + "connect": "Koble til", + "edit": "Rediger", + "account-generation-message": "Gi meg et øyeblikk! Jeg må granske universet for å generere en konto til deg!", + "no-messages": "Ingen meldinger", + "passphrase": "Passordfrase", + "recipient": "Mottager", + "members-title": "Medlemmer", + "new-group": "Ny gruppe", + "phone-e164": "Internasjonal 1", + "settings": "Instillinger", + "delete": "Slett", + "chats": "Chats", + "transaction": "Transaksjon", + "public-group-status": "Offentlig", + "image-source-make-photo": "Opptak", + "start-conversation": "Start en samtale", + "save": "Lagre", + "sharing-copy-to-clipboard": "Kopier til utklippstavle", + "sync-in-progress": "Synkroniserer...", + "send-transaction": "Send overføring", + "incorrect-code": [ + "str", + "Beklager, prøv på ny" + ], + "image-source-gallery": "Velg fra bilder", + "sync-synced": "Synkronisert", + "status-pending": "Venter", + "datetime-day": { + "one": "dag", + "other": "dager" + }, + "wallet-send": "Send", + "scan-qr": "Skan QR-kode", + "contact-s": { + "one": "kontakt", + "other": "kontaker" + }, + "type": "Type", + "next": "Neste", + "recent": "Nylig", + "status": "Status", + "from": "Fra", + "wrong-password": "Feil passord", + "in-contacts": "I kontaker", + "transactions-sign": "Signer", + "sharing-share": "Del...", + "type-a-message": "Skriv en melding...", + "unknown-status-go-error": "Ukjent status-go feil", + "clear-history": "Fjern historikk", + "no-contacts": "Ingen kontaker enda", + "datetime-today": "i dag", + "web-view-error": "oops, feil", + "error": "Feil", + "more": "mer", + "cancel": "Avbryt", + "can-not-add-yourself": "Du kan ikke legge til deg selv", + "add-to-contacts": "Legg til i kontakter", + "available": "Tilgjengelig", + "You": "Du", + "main-wallet": "Hovedlommebok", + "transactions": "Transaksjoner", + "members": { + "one": "1 medlem", + "other": "{{count}} medlemmer", + "zero": "ingen medlemmer" + }, + "intro-message1": "Velkommen til Status\nTrykk på denne beskjeden for å sette et passord og komme i gang!", + "transactions-history": "Historikk", + "new-contact": "Ny kontakt", + "datetime-second": { + "one": "sekund", + "other": "sekunder" + }, + "recover": "Gjenopprett", + "wallet-total-value": "Total verdi", + "datetime-minute": { + "one": "minutt", + "other": "minutter" + }, + "browsing-open-in-android-web-browser": "Åpne i nettleser", + "browsing-open-in-ios-web-browser": "Åpne i nettleser", + "edit-profile": "Rediger profil", + "active-unknown": "Ukjent", + "public-key": "Offentlig nøkkel", + "profile": "Profil", + "none": "Ingen", + "removed": "fjernet", + "transactions-filter-select-all": "Velg alle", + "transactions-filter-title": "Filtrer historikk", + "message": "Melding", + "here-is-your-passphrase": "Her er din nye passordfrase, *skriv den ned og hold den for deg selv!* Du vil trenge den for å kunne gjenopprette kontoen.", + "wallet-assets": "Assets", + "image-source-title": "Profilbilde", + "current-network": "Nåværende nettverk", + "left": "venstre", + "to": "Til", + "status-sent": "Sendt", + "data": "Data" +} diff --git a/translations/ne.json b/translations/ne.json new file mode 100644 index 00000000000..a12a5406043 --- /dev/null +++ b/translations/ne.json @@ -0,0 +1,474 @@ +{ + "about-app": "बारेमा", + "active-online": "अनलाइन", + "active-unknown": "अज्ञात", + "add": "Tambah", + "add-bootnode": "Bootnode थप्नुहोस्", + "add-mailserver": "Tambah mailserver", + "add-members": "सदस्यहरु जोड्नुहोस्", + "add-network": "Tambah rangkaian", + "add-to-contacts": "सम्पर्कहरुमा जोड्नुहोस्", + "address": "ठेगाना", + "advanced": "Lanjutan", + "advanced-settings": "Tetapan lanjutan", + "agree-by-continuing": "Dengan meneruskan anda bersetuju dengan ", + "all": "सबै", + "allow": "Benarkan", + "allowing-authorizes-this-dapp": "Membenarkan DApp ini untuk mengambil alamat dompet anda dan mengaktifkan Web3", + "amount": "रकम", + "apply": "Setuju", + "are-you-sure?": "के तपाईँ निश्चित हुनुहुन्छ?", + "at": "pada", + "available": "उपलब्ध", + "begin-set-up": "mulakan penyediaan", + "block": "ब्लक गर्नुहोस्", + "bootnode-address": "Alamat Bootnode", + "bootnode-details": "Bootnode विवरणहरू", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes diaktifkan", + "bootnodes-settings": "Tetapan Bootnodes", + "browser": "ब्राउजर", + "browser-not-secure": "Sambungan tidak selamat! Jangan menandatangani transaksi atau menghantar data peribadi di laman web ini.", + "browser-secure": "Sambungan adalah selamat. Pastikan anda benar-benar mempercayai laman web ini sebelum menandatangani transaksi atau memasukkan data peribadi.", + "browsing-cancel": "रद्द गर्नुहोस्", + "browsing-open-in-android-web-browser": "वेब ब्राउजरमा खोल्ने", + "browsing-open-in-ios-web-browser": "वेब ब्राउजरमा खोल्ने", + "browsing-open-in-status": "स्टेटसमा खोल्नुहोस्", + "browsing-site-blocked-description1": "Kami mengesan aktiviti berniat jahat dari alamat ini. Untuk melindungi anda dan dompet anda, kami menghalang navigasi selanjutnya. \n\nJika anda berpendapat bahawa ini adalah satu kesilapan, beritahu kami di ", + "browsing-site-blocked-description2": " perbualan umum.", + "browsing-site-blocked-go-back": "Kembali", + "browsing-site-blocked-title": "Laman ini disekat", + "browsing-title": "ब्राउज", + "camera-access-error": "कृपया क्यामेरा उपयोग गर्न आवश्यक अनुमति प्रदान गर्न, सिस्टम सेटिङ्ग भित्र गएर स्टेटस > क्यमेरा चयन भएको स्थिति निश्चित गर्नुहोस् ।", + "cancel": "रद्द गर्नुहोस्", + "change-fleet": "Tukar fleet ke {{fleet}}", + "changed-amount-warning": "Amaun telah ditukar daripada {{old}} kepada {{new}}", + "changed-asset-warning": "सम्पत्ति {{old}} बाट {{new}} मा परिवर्तन भएको थियो।", + "chat": "च्याट", + "chat-name": "च्याट नाम", + "chat-settings": "च्याट सेटिङ्गहरु", + "chats": "च्याटहरु", + "choose-authentication-method": "Pilih kaedah pengesahan untuk melindungi akaun anda", + "clear": "Hapus", + "clear-history": "इतिहास खाली गर्नुहोस्", + "clear-history-action": "Hapus", + "clear-history-confirmation": "इतिहास मेटाउने हो?", + "clear-history-confirmation-content": "Adakah anda pasti untuk memadamkan sejarah perbualan ini?", + "clear-history-title": "इतिहास मेटाउने हो?", + "close-app-button": "पुष्टि गर्नुहोस्", + "close-app-content": "Aplikasi ini akan berhenti dan ditutup. Apabila anda membuka semula, rangkaian yang dipilih akan digunakan", + "close-app-title": "चेतावनी!", + "command-button-send": "Hantar", + "confirm": "पुष्टि गर्नुहोस्", + "confirmations": "Pengesahan", + "confirmations-helper-text": "Apabila transaksi mempunyai 12 pengesahan, anda boleh menganggap ia telah diselesaikan.", + "connect": "कनेक्ट", + "connect-mailserver-content": "Sambung ke {{name}}?", + "connected": "जडित", + "connecting-requires-login": "Menyambung ke rangkaian lain memerlukan daftar masuk", + "contact-s": { + "one": "सम्पर्क", + "other": "सम्पर्कहरु" + }, + "contacts": "सम्पर्कहरु", + "copy-qr": "Salin kod", + "copy-transaction-hash": "लेनदेन आईडी प्रतिलिपि गर्नुहोस्", + "cost-fee": "Kos\/Fi", + "counter-9-plus": "9+", + "create": "सिर्जना गर्नुहोस्", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Matawang", + "currency-display-name-aed": "Dirham Emiriah", + "currency-display-name-afn": "Afghani Afghanistan", + "currency-display-name-ars": "अर्जेण्टीना पीसो", + "currency-display-name-aud": "Dolar Australia", + "currency-display-name-bbd": "बार्बाडोस डलर", + "currency-display-name-bdt": "बांग्लादेशी ताका", + "currency-display-name-bgn": "बल्गेरियाई लेभ", + "currency-display-name-bhd": "Dinar Bahrain", + "currency-display-name-bnd": "ब्रुनेई दारुसलाम डलर", + "currency-display-name-bob": "Bolíviano Bolivia", + "currency-display-name-brl": "ब्राजील रियल", + "currency-display-name-btn": "भूटानी नगुलट्रम", + "currency-display-name-cad": "क्यानाडा डलर", + "currency-display-name-chf": "Franc Switzerland", + "currency-display-name-clp": "Peso Chile", + "currency-display-name-cny": "Yuan Renminbi China", + "currency-display-name-cop": "Peso Colombia", + "currency-display-name-crc": "Colon Costa Rica", + "currency-display-name-czk": "Koruna Czech", + "currency-display-name-dkk": "डेनमार्क क्रौन", + "currency-display-name-dop": "Peso Republik Dominica", + "currency-display-name-egp": "मिस्र पाउंड", + "currency-display-name-etb": "Birr Ethiopia", + "currency-display-name-eur": "यूरो", + "currency-display-name-gbp": "Pound British", + "currency-display-name-gel": "जर्जियाली लारी", + "currency-display-name-ghs": "Cedi Ghana", + "currency-display-name-hkd": "Dolar Hong Kong", + "currency-display-name-hrk": "Kuna Croatia", + "currency-display-name-huf": "Forint Hungary", + "currency-display-name-idr": "Rupiah Indonesia", + "currency-display-name-ils": "Shekel Israel", + "currency-display-name-inr": "भारत रुपैयाँ", + "currency-display-name-isk": "Krona Sweden", + "currency-display-name-jmd": "जमैका डलर", + "currency-display-name-jpy": "Yen Jepun", + "currency-display-name-kes": "केन्याई शिलिंग", + "currency-display-name-krw": "कोरिया (दक्षिण) वोन", + "currency-display-name-kwd": "Dinar Kuwait", + "currency-display-name-kzt": "Tenge Kazakhstan", + "currency-display-name-lkr": "Rupee Sri Lanka", + "currency-display-name-mad": "Dirham Maghribi", + "currency-display-name-mdl": "Leu Moldova", + "currency-display-name-mur": "Rupee Mauritius", + "currency-display-name-mwk": "Kwacha Malawi", + "currency-display-name-mxn": "मेक्सिको पीसो", + "currency-display-name-myr": "मलेशिया रिंगित", + "currency-display-name-mzn": "मोजाम्बिक मेटिकल", + "currency-display-name-nad": "नामिबिया डलर", + "currency-display-name-ngn": "नाइजेरिया नाइरा", + "currency-display-name-nok": "नर्वे क्रौन", + "currency-display-name-npr": "नेपाल रुपैयाँ", + "currency-display-name-nzd": "Dolar New Zealand", + "currency-display-name-omr": "ओमन रियाल", + "currency-display-name-pen": "Sol Peru", + "currency-display-name-pgk": "Kina Papua New Guinea", + "currency-display-name-php": "Peso Filipina", + "currency-display-name-pkr": "पाकिस्तान रुपैयाँ", + "currency-display-name-pln": "पोल्याण्ड ज्लटी", + "currency-display-name-pyg": "Guarani Paraguay", + "currency-display-name-qar": "Riyal Qatar", + "currency-display-name-ron": "Leu Romania", + "currency-display-name-rsd": "Dinar Serbia", + "currency-display-name-rub": "रूस रुबल", + "currency-display-name-sar": "Riyal Arab Saudi", + "currency-display-name-sek": "Krona Sweden", + "currency-display-name-sgd": "Dolar Singapura", + "currency-display-name-thb": "Baht Thailand", + "currency-display-name-try": "Lira Turki", + "currency-display-name-ttd": "Dolar Trinidad dan Tobago", + "currency-display-name-twd": "ताइवान नयाँ डलर", + "currency-display-name-tzs": "तान्जानियन शिलिंग", + "currency-display-name-uah": "Hryvnia Ukraine", + "currency-display-name-ugx": "Shilling Uganda", + "currency-display-name-usd": "Dolar Amerika Syarikat", + "currency-display-name-uyu": "उरुग्वे पेसो", + "currency-display-name-vef": "भेनेजुएला बोलिवर", + "currency-display-name-vnd": "भियतनाम दांग", + "currency-display-name-zar": "दक्षिण अफ्रीका रेण्ड", + "current-network": "चल्ती नेटवर्क", + "custom": "Pilihan Lain", + "custom-networks": "Rangkaian lain", + "dapp": "ÐApp", + "dapps": "ÐApps", + "data": "डाटा", + "datetime-ago": "पहिले", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-day": { + "one": "दिन", + "other": "दिन" + }, + "datetime-hour": { + "one": "घण्टा", + "other": "घण्टा" + }, + "datetime-minute": { + "one": "मिनेट", + "other": "मिनेट" + }, + "datetime-second": { + "one": "सेकेन्ड", + "other": "सेकेन्ड" + }, + "datetime-today": "आज", + "datetime-yesterday": "हिजो", + "decryption-failed-content": "Kami tidak dapat untuk menyahsulitkan data anda, anda mungkin perlu mencipta akaun baru dan memadamkan data lama anda dengan mengetik \"Pilih\". Klik pada \"Batal\", untuk mencuba lagi", + "delete": "मेटाउनुहोस्", + "delete-bootnode": "Bootnode मेटाउनुहोस्", + "delete-bootnode-are-you-sure": "Adakah anda pasti untuk memadamkan bootnode ini?", + "delete-bootnode-title": "Padamkan bootnode", + "close-chat": "च्याट मेटाउने", + "close-chat-confirmation": "Adakah anda pasti untuk memadamkan perbualan ini?", + "delete-confirmation": "Padam?", + "delete-mailserver": "Padam mailserver", + "delete-mailserver-are-you-sure": "Adakah anda pasti untuk memadamkan mailserver ini?", + "delete-mailserver-title": "Padam mailserver", + "delete-message": "सन्देश मेटाउनुहोस्", + "delete-network-confirmation": "Adakah anda pasti mahu memadamkan rangkaian ini?", + "delete-network-error": "Sila sambungkan ke rangkaian lain sebelum memadamkan ini", + "delete-network-title": "Padam rangkaian?", + "deny": "Nafikan", + "description": "विवरण", + "dev-mode": "Mod pembangunan", + "disconnected": "Menyambung ke peer...", + "discover": "भेट्टाउनुहोस्", + "done": "सम्पन्न", + "edit": "सम्पादन", + "edit-profile": "प्रोफाइल सम्पादन", + "empty-chat-description": "अहिलेसम्म कुनै सन्देशहरू छैनन्", + "empty-chat-description-one-to-one": "Sebarang mesej yang anda hantar disini adalah disulitkan dan hanya boleh dibaca oleh anda dan ", + "enter-address": "ठेगान प्रविष्ट गर्ने", + "enter-pair-code": "Masukkan kod pautan", + "enter-pair-code-description": "Diperlukan untuk memautkan kad anda dengan peranti ini", + "enter-password": "Masukkan kata laluan", + "enter-url": "युआरएल प्रविष्ट गर्नुहोस्", + "enter-word": "Masukkan perkataan", + "error": "त्रुटि", + "error-unable-to-get-balance": "Tidak boleh mendapatkan baki", + "error-unable-to-get-prices": "Ralat penukaran matawang. Muat semula skrin anda untuk mencuba lagi.", + "error-unable-to-get-token-balance": "Tidak boleh mendapatkan baki token", + "errors": "त्रुटिहरू", + "eth": "ETH", + "failed": "Gagal", + "faq": "Soalan lazim", + "find": "Cari", + "fleet": "Fleet", + "fleet-settings": "Tetapan fleet", + "from": "बाट", + "gas-limit": "ग्यास सीमा", + "gas-price": "ग्याँस मूल्य", + "gas-used": "Gas digunakan", + "generating-codes-for-pairing": "Menjana kod pautan untuk berhubung dengan akaun Status anda.", + "get-status-at": "Dapatkan Status di http:\/\/status.im", + "go-to-settings": "Pergi ke Tetapan", + "got-it": "पाइयो", + "group-chat": "Perbualan kumpulan", + "group-info": "Maklumat kumpulan", + "gwei": "Gwei", + "hash": "ह्यास", + "help": "bantuan", + "help-center": "Pusat Bantuan", + "hold-card": "pegang kad dibelakang telefon anda", + "home": "Utama", + "hooks": "Cangkuk", + "identifier": "Pengenalpasti", + "image-remove-current": "वर्तमान फोटो हटाउनुहोस्", + "image-source-gallery": "ग्यालेरीबाट चयन गर्नुहोस्", + "image-source-make-photo": "खिच्नुहोस", + "image-source-title": "प्रोफाइल तस्विर", + "in-contacts": "सम्पर्कहरुमा समावेस छ", + "incoming": "Masuk", + "incorrect-code": [ + "str", + "माफ गर्नुहोस्, त्यो कोड गलत रहेछ, कृपया पुनः प्रविष्ट गर्नुहोस्" + ], + "intro-message1": "स्टेटसमा तपाईंलाई स्वागत छ\nतपाईंको पास्वर्ड सेटअप र सुरुवात गर्नको लागि यो सन्देसमा ट्याप गर्नुहोस् ।", + "intro-text": "Status adalah pintu gerbang anda ke web yang terdesentralisasi", + "invalid-key-confirm": "Setuju", + "invalid-number": "Nombor tidak sah", + "invite-friends": "Jemput rakan", + "invited": "आमन्त्रित", + "keycard-connected-title": "जडित", + "keycard-success-title": "Berjaya", + "leave": "Tinggalkan", + "leave-group": "Tinggalkan kumpulan", + "left": "बाहिरियो", + "log-level": "लग स्तर", + "log-level-settings": "Tetapan tahap log", + "logout": "लग आउट", + "logout-are-you-sure": "Adakah anda pasti mahu daftar keluar?", + "logout-title": "Daftar keluar?", + "mailserver-address": "Alamat Mailserver", + "mailserver-connection-error": "Tidak dapat bersambung ke mailserver", + "mailserver-details": "Mailserver विवरणहरू", + "mailserver-reconnect": "Tidak dapat bersambung ke mailserver. Ketik untuk menyambung semula", + "mailserver-title": "Mailserver", + "main-currency": "मुख्य मुद्रा", + "main-networks": "मुख्य नेटवर्कहरू", + "main-wallet": "मुख्य वालेट", + "mainnet-network": "मुख्य नेटवर्क", + "members": { + "one": "१ सदस्य", + "other": "{{count}} सदस्यहरु" + }, + "members-active": { + "one": "१ सदस्य", + "other": "{{count}} सदस्यहरु" + }, + "members-active-none": "tiada ahli", + "members-title": "सदस्यहरु", + "message": "सन्देश", + "message-not-sent": "Mesej tidak dihantar", + "message-options-cancel": "रद्द गर्नुहोस्", + "message-reply": "जवाफ दिनुहोस्", + "more": "अरु", + "name": "नाम", + "need-help": "सहयोग चाहियो?", + "network": "Rangkaian", + "network-chain": "नेटवर्क श्रृंखला", + "network-details": "Maklumat rangkaian", + "network-id": "नेटवर्क आईडी", + "network-settings": "नेटवर्क सेटिङहरू", + "new": "नयाँ", + "new-chat": "नयाँ च्याट", + "new-contact": "नया सम्पर्क", + "new-contract": "Kontrak Baru", + "new-group": "नया समूह", + "new-group-chat": "नया सामूहिक च्याट", + "new-network": "Rangkaian baru", + "new-public-group-chat": "सार्वजनिक च्याटमा भाग लिने", + "next": "अर्को", + "no": "Tidak", + "no-contacts": "हाल कुनै सम्पर्कहरु छैनन्", + "no-messages": "सन्देशहरु खाली", + "no-pairing-slots-available": "Tiada slot pautan tersedia. \n Anda boleh menyahpasang peranti yang telah dipautkan dengan kad", + "nonce": "Nonce", + "none": "बिना कुनै", + "not-applicable": "अहस्ताक्षरित लेनदेनको लागी लागू छैन", + "notifications": "Pemberitahuan", + "off": "Tidak aktif", + "offline": "अफलाइन", + "offline-messaging": "Mailserver", + "offline-messaging-settings": "Tetapan mailserver", + "ok": "ठिक छ", + "on": "Pada", + "open": "खोल्नुहोस्", + "open-dapp": "ÐApp खोल्नुहोस्", + "open-on-etherscan": "Buka pada Etherscan.io", + "outgoing": "Keluar", + "pair-card": "paut kad", + "pair-code": "जोडा कोड", + "pair-code-explanation": "Pautkan kad dengan peranti berbeza yang mempunyai akaun Status yang sama padanya", + "pairing-card": "Memautkan kad", + "passphrase": "गुप्तवाक्यांश", + "password": "पासवर्ड", + "password_error1": "Kata laluan tidak sepadan.", + "password-placeholder2": "आफ्नो पासवर्ड निश्चित गर्नुहोस्", + "paste-json": "टाँस्नुहोस् JSON", + "pending": "Belum selesai", + "permissions": "अनुमतिहरू", + "phone-e164": "अन्तराष्ट्रिय १", + "photos-access-error": "कृपया फोटो उपयोग गर्न आवश्यक अनुमति प्रदान गर्न, सिस्टम सेटिङ्ग भित्र गएर स्टेटस > फोटो चयन भएको स्थिति निश्चित गर्नुहोस् ।", + "profile": "प्रोफाइल", + "public-chat": "सार्वजनिक कुराकानी", + "public-chats": "सार्वजनिक च्याटहरू", + "public-group-status": "सार्वजनिक", + "public-group-topic": "विषय", + "public-key": "सार्वजनिक सांचो", + "puk-code": "PUK कोड", + "receive": "प्राप्त गर्नुहोस्", + "receive-transaction": "लेनदेन प्राप्त गर्नुहोस्", + "recent": "भर्खरको", + "recent-recipients": "Kenalan", + "recipient": "प्रापक", + "recipient-code": "Masukkan alamat penerima", + "recover": "पुनर्स्थापना", + "recovery-typo-dialog-description": "Sekiranya anda memasukkan kata-kata yang salah, anda akan membuat akaun baru dan bukannya memulihkan akaun yang lama.", + "remove": "हटाउनुहोस्", + "remove-from-chat": "च्याटबाट हटाउनुहोस्", + "remove-from-contacts": "सम्पर्कहरुबाट हटाउनुहोस्", + "remove-network": "सञ्जाल हटाउनुहोस्", + "removed": "हटाइयो", + "request-feature": "एउटा सुविधा अनुरोध गर्नुहोस्", + "request-transaction": "Minta transaksi", + "required-field": "आवश्यक फिल्ड", + "resend-message": "Hantar semula", + "rpc-url": "URL RPC", + "save": "साँच्नुहोस्", + "save-password": "लगआउट सम्म पासवर्ड बचत गर्नुहोस्", + "save-password-unavailable": "पासवर्ड सुरक्षित गर्न उपकरण पासकोड सेट गर्नुहोस्", + "scan-qr": "QR स्क्यान गर्नुहोस्", + "scan-qr-code": "Imbas kod QR dengan alamat dompet", + "secret-keys-confirmation-text": "Tulis ini sekarang kerana anda tidak akan dapat melihat skrin ini lagi", + "secret-keys-confirmation-title": "Adakah anda telah menulisnya?", + "selected": "चयन गरिएको", + "send-message": "सन्देश पठाउनुहोस्", + "send-request": "अनुरोध पठाउने", + "send-request-amount": "Jumlah", + "send-request-amount-max-decimals": "Bilangan maksimum perpuluhan adalah {{asset-decimals}}", + "send-request-unknown-token": "Token tidak diketahui - {{asset}}", + "send-transaction": "कारोबार पठाउनुहोस्", + "sent-at": "मा पठाइएको", + "set-a-topic": "Cipta topik", + "settings": "सेटिङ्गहरु", + "share": "Kongsi", + "share-chat": "Kongsi perbualan", + "share-dapp-text": "Lihat DApp yang saya gunakan di Status: {{link}}", + "share-link": "Kongsi pautan", + "share-my-profile": "Kongsi profil saya", + "share-profile-link": "Kongsi alamat profil", + "share-public-chat-text": "Lihat perbualan kumpulan ini di Status: {{link}}", + "sharing-copied-to-clipboard": "क्लिपबोर्डमा प्रतिलिपि गरियो", + "sharing-copy-to-clipboard": "क्लिप्बोर्डमा प्रतिलिपि राख्नुहोस्", + "sharing-share": "बांड्नुहोस् ॰॰॰", + "show-less": "Paparkan dengan kurang", + "show-more": "Paparkan lagi", + "show-qr": "QR देखाउने", + "sign-message": "Tandatangan Mesej", + "sign-you-in": "Mendaftar anda masuk...", + "soon": "Akan datang", + "specify-name": "एक नाम निर्दिष्ट गर्नुहोस्", + "specify-network-id": "Tetapkan id rangkaian", + "specify-rpc-url": "RPC URL निर्दिष्ट गर्नुहोस्", + "start-chat": "च्याट सुरू गर्नुहोस्", + "start-conversation": "कुराकानी सुरु गर्नुहोस्", + "start-group-chat": "Mulakan perbualan kumpulan", + "start-new-chat": "Mulakan perbualan baru", + "status": "स्टेटस", + "status-confirmed": "पुष्टि भयो", + "status-hardwallet": "Dompet fizikal Status", + "status-pending": "बाँकी छ", + "status-sent": "पठाइसकेको", + "status-tx-not-found": "TX फेला परेन", + "step-i-of-n": "Langkah {{step}} daripada {{number}}", + "submit-bug": "Hantar laporan kerentanan", + "success": "Berjaya", + "sync-in-progress": "एकनास बनाइंदै छ ॰॰॰", + "sync-synced": "एकनास छ", + "test-networks": "परीक्षण नेटवर्कहरू", + "text-input-disabled": "कृपया एक क्षण प्रतीक्षा गर्नुहोस् ...", + "to": "तर्फ", + "transaction": "कारोबार", + "transaction-description": "Transaksi dengan 12 pengesahan boleh dianggap tidak dapat diubah lagi", + "transaction-details": "लेनदेन विवरणहरू", + "transaction-failed": "Transaksi gagal", + "transaction-history": "Sejarah Transaksi", + "transaction-request": "Permintaan Transaksi", + "transaction-sent": "कारोबार पठाइयो", + "transactions": "Transaksi", + "transactions-filter-select-all": "Pilih semua", + "transactions-filter-title": "Tapis sejarah", + "transactions-history": "Sejarah transaksi", + "transactions-history-empty": "तपाईको इतिहासमा अझै कुनै लेनदेन छैन", + "transactions-sign": "Ditandatangan", + "turn-nfc-on": "Hidupkan nfc", + "type": "Jenis", + "type-a-message": "सन्देश टाइप गरौं ॰॰॰", + "unable-to-read-this-code": "यो कोड पढ्न असमर्थ", + "unknown-status-go-error": "Ralat status-go yang tidak diketahui", + "url": "URL", + "usd-currency": "USD", + "validation-amount-invalid-number": "राशि वैध संख्या होइन", + "validation-amount-is-too-precise": "Jumlah terlalu banyak. Jumlah maksimum perpuluhan adalah {{decimals}} .", + "view-cryptokitties": "CryptoKitties मा हेर्नुहोस्", + "view-cryptostrikers": "Lihat di CryptoStrikers", + "view-etheremon": "Etheremon मा हेर्नुहोस्", + "view-profile": "Lihat profil", + "view-superrare": "Lihat di SuperRare", + "wallet": "वालेट", + "wallet-asset": "Aset", + "wallet-assets": "Aset", + "wallet-choose-recipient": "Pilih Penerima", + "wallet-collectibles": "Koleksi", + "wallet-insufficient-funds": "Kekurangan dana", + "wallet-insufficient-gas": "Tidak cukup ETH untuk gas", + "wallet-invalid-address": "अवैध ठेगाना: \n {{data}}", + "wallet-invalid-chain-id": "Rangkaian tidak sepadan: \n {{data}} tetapi rangkaian semasa adalah {{chain}}", + "wallet-request": "अनुरोध", + "wallet-send": "Hantar", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "वालेट सेटिङहरू", + "wallet-total-value": "Jumlah nilai", + "wallet-transaction-total-fee": "Jumlah Fi", + "wants-to-access-profile": "ingin mengakses profil anda", + "warning-message": "Maaf, kami menghadkan penghantaran mesej secara berturut-turut untuk mencegah spam. Sila cuba kembali sebentar lagi", + "web-view-error": "ओहोऽऽऽ, त्रुटि", + "word-n": "Perkataan #{{number}}", + "write-down-and-store-securely": "Tuliskan ini dan simpannya dengan selamat", + "wrong-password": "गलत पासवर्ड", + "wrong-word": "गलत शब्द", + "yes": "हो", + "You": "तपाईं", + "you-are-all-set": "Anda telah siap sedia!" +} diff --git a/translations/nl.json b/translations/nl.json new file mode 100644 index 00000000000..c375bf7110c --- /dev/null +++ b/translations/nl.json @@ -0,0 +1,1639 @@ +{ + "You": "Jij", + "about-app": "Over", + "about-key-storage-content": "Status heeft nooit toegang tot de private key. Maak een back-up van de seed phrase. Als je de telefoon verliest, is dit de enige manier om toegang te krijgen tot je keys.", + "about-key-storage-title": "Over key-opslag", + "about-names-content": "Niemand kan zich als jou voordoen! Je bent standaard anoniem en hoeft nooit je echte naam te onthullen. Je kunt tegen een kleine vergoeding een aangepaste naam registreren.", + "about-names-title": "Namen kunnen niet veranderd worden.", + "about-sharing-data": "Over delen van data", + "accept": "Accepteren", + "accept-and-add": "Accepteren en toevoegen", + "accept-and-continue": "Accepteren en doorgaan", + "accept-and-share-address": "Accepteer en deel adres", + "accept-new-chats-from": "Accepteer nieuwe chats van", + "accept-status-tos-prefix": "Status accepteren", + "access-existing-keys": "Toegang tot bestaande keys", + "access-key": "Toegang tot key", + "account-added": "Account toegevoegd", + "account-color": "Accountkleur", + "account-content": "Accounts in Status kun je vergelijken met bankrekeningen. Net als een bankrekening heeft een account een adres en een saldo; Je gebruikt deze account om transacties te doen op Ethereum. Je kunt meerdere accounts in je wallet hebben. Allemaal toegankelijk door Status te ontgrendelen.", + "account-exists-title": "Account bestaat al", + "account-is-used": "Het account wordt gebruikt met Dapps in de browser.", + "account-name": "Accountnaam", + "account-settings": "Account instellingen", + "account-title": "Account", + "accounts": "Accounts", + "actions": "Acties", + "active-online": "Online", + "active-unknown": "Onbekend", + "activity": "Activiteit", + "add": "Toevoegen", + "add-a-watch-account": "Voeg een watch-only adres toe", + "add-account": "Een account toevoegen", + "add-account-description": "Je kunt elk type Ethereum-account importeren om het aan je Status-wallet toe te voegen", + "add-account-incorrect-password": "Wachtwoord lijkt onjuist te zijn. Voer wachtwoord in dat je gebruikt om app te ontgrendelen.", + "add-an-account": "Een account toevoegen", + "add-bootnode": "Bootnode toevoegen", + "add-contact": "Contact toevoegen", + "add-custom-token": "Aangepaste token toevoegen", + "add-favourite": "Favoriet toevoegen", + "add-mailserver": "Toevoegen Status-node", + "add-members": "Leden toevoegen", + "add-network": "Toevoegen netwerk", + "add-new-contact": "Nieuw contact toevoegen", + "add-nickname": "Voeg een bijnaam toe (optioneel)", + "add-private-key-account": "Account toevoegen van private-key", + "add-seed-account": "Account toevoegen met seed phrase ", + "add-to-contacts": "Toevoegen aan contacten", + "add-to-contacts-text": "Door een gebruiker toe te voegen aan uw contactenlijst, deelt je jouw wallet-adres met hen.", + "add-to-favourites": "Toevoegen aan favorieten", + "add-watch-account": "Voeg een watch-only account toe", + "address": "Adres", + "address-or-ens-name": "Adres of ENS-naam", + "address-received": "Adres ontvangen", + "address-request-accepted": "Adresverzoek geaccepteerd", + "address-requested": "Adres verzocht", + "advanced": "Gevorderd", + "advanced-settings": "Geavanceerde instellingen", + "advertiser-description": "Je hebt Status dankzij een partner ontdekt. Vind je het erg als Status je IP-adres één keer controleert, zodat ze worden beloond? Deze informatie wordt nergens anders voor gebruikt en wordt na 7 dagen volledig verwijderd.", + "advertiser-starter-pack-accept": "Accepteren", + "advertiser-starter-pack-decline": "Afwijzen", + "advertiser-starter-pack-description": "Hier is wat crypto om je op weg te helpen! Gebruik het voor stickers, een ENS naam en om dapps te proberen", + "advertiser-starter-pack-title": "Starterspakket", + "advertiser-title": "Privacy is standaard", + "agree-by-continuing": "Door verder te gaan ga je akkoord\n met onze ", + "all": "Alle", + "allow": "Toestaan", + "allow-and-send": "Toestaan en verzenden", + "allow-mention-notifications": "Toon @ vermeldingen", + "allowing-authorizes-this-dapp": "Toestaan autoriseert deze DApp om je wallet-adres op te halen en Web3 in te schakelen", + "already-have-asset": "Je hebt deze asset al", + "amount": "Bedrag", + "anyone": "Iedereen", + "appearance": "Thema", + "apply": "Toepassen", + "are-you-sure": "Weet je het zeker?", + "are-you-sure-description": "De seed-phrase kun je hierna niet meer zien", + "are-you-sure-to-cancel": "Weet je zeker dat je wilt annuleren?", + "are-you-sure?": "Weet je het zeker?", + "ask-in-status": "Stel een vraag of meld een bug", + "at": "op", + "attribution-received": "{{attrib}} van {{max}} ontvangen bonussen", + "audio": "Audio", + "audio-recorder": "Recorder", + "audio-recorder-error": "Recorder fout", + "audio-recorder-max-ms-reached": "Maximale opnametijd bereikt", + "audio-recorder-permissions-error": "Je moet toestemming geven om audioberichten te sturen", + "authorize": "Machtigen", + "available": "Beschikbaar", + "available-participants": { + "one": "Je kan nog één deelnemer selecteren", + "other": "Je kan {{count}} deelnemers selecteren" + }, + "back": "Terug", + "back-up": "Maak back-up", + "back-up-seed-phrase": "Seed phrase", + "back-up-your-seed-phrase": "Maak een back-up van je seed-phrase", + "backup-disabled": "Uitgeschakeld", + "backup-recovery-phrase": "Back up seed phrase", + "bad-fees-description": "Je prioriteitstarief ligt onder onze voorgestelde parameters.", + "balance": "Balans", + "begin-set-up": "Start instellen", + "below-base-fee": "maximale tarief onder base fee", + "biometric-auth-android-sensor-desc": "Aanraaksensor", + "biometric-auth-android-sensor-error-desc": "Mislukt", + "biometric-auth-android-title": "Verificatie vereist", + "biometric-auth-confirm-logout": "Opnieuw inloggen", + "biometric-auth-confirm-message": "Biometrische verificatie is vereist om door te gaan, zo niet mogelijk, ontgrendel je keys met wachtwoord of passcode", + "biometric-auth-confirm-title": "Je moet authenticeren!", + "biometric-auth-confirm-try-again": "Probeer opnieuw", + "biometric-auth-error": "Kan geen biometrische verificatie uitvoeren ({{code}})", + "biometric-auth-login-error-title": "Biometrische authenticatiefout", + "biometric-auth-login-ios-fallback-label": "Voer wachtwoord in", + "biometric-auth-reason-login": "In Status inloggen", + "biometric-auth-reason-verify": "Verifieer verificatie", + "biometric-disable-bioauth": "{{bio-type-label}} uitschakelen", + "biometric-disable-password-description": "Als j dit uitschakelt, zult je ook ", + "biometric-disable-password-title": "Wachtwoord opslaan uitschakelen", + "biometric-enable": "Als je niet elke keer je wachtwoord wilt invoeren om toegang te krijgen tot de app, schakel je {{bio-type-label}} inloggen in", + "biometric-enable-button": "{{bio-type-label}} inschakelen", + "biometric-enable-keycard": "Als je niet elke keer je Keycard wilt gebruiken om toegang te krijgen tot de app, schakel dan {{bio-type-label}} inloggen", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Vingerafdruk", + "biometric-secure-with": "Beveilig met {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "BIP39-wachtwoord", + "blank-keycard-text": "Je kunt pas doorgaan met je keycard nadat je keys en een naam hebt aangemaakt", + "blank-keycard-title": "Het lijkt erop dat je een blanco keycard hebt gebruikt", + "block": "Blokkeer", + "block-contact": "Blokkeer deze gebruiker", + "block-contact-details": "Door te blokkeren worden vorige berichten van deze gebruiker verwijderd en kunnen nieuwe berichten je niet meer bereiken", + "blocked-users": "Geblokkeerde gebruikers", + "bootnode-address": "Bootnode-adres", + "bootnode-details": "Bootnode-gegevens", + "bootnode-format": "enode:\/\/{enode-id}@{ip-adres}:{poort}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes ingeschakeld", + "bootnodes-settings": "Bootnodes-instellingen", + "browsed-websites": "Browsergeschiedenis verschijnt hier", + "browser": "Browser", + "browser-not-secure": "De verbinding is niet veilig! Teken geen transacties en verstuur geen persoonlijke gegevens via deze site.", + "browser-secure": "De verbinding is veilig. Zorg ervoor dat je deze site echt vertrouwt voordat je transacties ondertekent of persoonlijke gegevens invoert.", + "browsers": "Browsers", + "browsing-cancel": "Annuleren", + "browsing-open-in-android-web-browser": "Open in Android", + "browsing-open-in-ios-web-browser": "Open in iOS", + "browsing-open-in-status": "Open in Status", + "browsing-site-blocked-description1": "We hebben mogelijk schadelijke activiteiten vanaf dit adres waargenomen. Om jou en je wallet te beschermen, voorkomen we verdere navigatie.\n\nAls je denkt dat dit een fout is, laat het ons weten in de ", + "browsing-site-blocked-description2": " publieke chat.", + "browsing-site-blocked-go-back": "Ga terug", + "browsing-site-blocked-title": "Deze site is geblokkeerd", + "browsing-title": "Bladeren", + "bug-report": "Meld een bug", + "build-yourself": "Om de app te gebruiken zonder deze Gebruiksvoorwaarden, kun je jouw eigen versie bouwen", + "buy-crypto": "Crypto kopen", + "buy-crypto-choose-a-service": "Selecteer een service die u wilt gebruiken om crypto te kopen", + "buy-crypto-description": "Vind een dapp om nu crypto te kopen", + "buy-crypto-leaving": "U verlaat Status en gaat naar een website van een derde partij om uw aankoop te voltooien", + "buy-crypto-title": "Het lijkt erop dat je wallet leeg is", + "camera-access-error": "Om de vereiste cameratoestemming te verlenen, ga je naar je systeeminstellingen en zorg je ervoor dat Status > Camera is geselecteerd.", + "can-not-add-yourself": "Dat ben jij, om een chat te starten, kies iemand anders", + "can-send-messages": "Je kunt nieuwe berichten verzenden en ontvangen", + "cancel": "Annuleren", + "cancel-keycard-setup": "Annuleren Keycard instellen", + "cancelling": "Annuleren", + "cannot-read-card": "Kan card niet lezen.\nHoudt het tegen de achterkant van je telefoon", + "cannot-use-default-pin": "Passcode 000000 is niet toegestaan.\nGebruik een ander nummer", + "cant-open-public-chat": "Kan geen publieke chat openen", + "cant-report-bug": "Kan een bug niet melden", + "card-is-blank": "Deze card is leeg", + "card-reseted": "Card is gereset", + "card-unpaired": "Card is ontkoppeld van het huidige apparaat", + "category": "Categorie", + "category-title": "Categorie titel", + "change-fleet": "Verander fleet in {{fleet}}", + "change-logging-enabled": "Weet je zeker dat je loggen wilt {{enable}}?", + "change-pairing": "Wijzig koppelcode", + "change-pairing-description": "Het wijzigen van de koppelcode heeft geen invloed op de huidige koppelingen. Voor elke nieuwe koppeling is echter de nieuwe code vereist.", + "change-pairing-title": "Maak een nieuwe koppelcode", + "change-passcode": "Toegangscode wijzigen", + "change-password": "Wachtwoord wijzigen", + "change-pin": "Wijzig 6-cijferige passcode", + "change-puk": "Wijzig 12-cijferige PUK", + "change-tip": "Aanpassen fooi", + "changed-amount-warning": "Bedrag is gewijzigd van {{old}} in {{new}}", + "changed-asset-warning": "Asset is gewijzigd van {{old}} in {{new}}", + "chaos-mode": "Chaos-modus", + "chaos-unicorn-day": "Chaos Unicorn-dag", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Privé chatten en transacties doen met vrienden", + "chat-is-a-contact": "Contact", + "chat-is-not-a-contact": "Geen contactpersoon", + "chat-key": "Chat-key", + "chat-key-content": "Berichten op het Status-chatprotocol worden verzonden en ontvangen met behulp van encryptie-keys. De publieke chat-key is een reeks tekens die je met anderen deelt, zodat ze jou in Status berichten kunnen sturen.", + "chat-key-title": "Chat-key", + "chat-link-previews": "Chat-link previews", + "chat-name": "Chatnaam", + "chat-name-content": "Drie willekeurige woorden, algoritmisch afgeleid van je chat-key en gebruikt als je standaardalias in de chat. Chatnamen zijn volledig uniek; geen enkele andere gebruiker kan dezelfde drie woorden hebben.", + "chat-name-title": "Chatnaam", + "chat-notification-preferences": "Meldingsinstellingen", + "chat-settings": "Chat-instellingen", + "chats": "Chats", + "check-on-opensea": "Controleer op opensea", + "check-your-recovery-phrase": "Controleer je seed phrase", + "choose-actions": "Selecteer actie", + "choose-authentication-method": "Kies een verificatiemethode", + "choose-new-location-for-keystore": "Selecteer een nieuwe locatie om je keystore-bestand op te slaan", + "choose-storage": "Selecteer opslag", + "clear": "Wissen", + "clear-all": "Alles wissen", + "clear-history": "Wissen geschiedenis", + "clear-history-action": "Wissen", + "clear-history-confirmation": "Wissen geschiedenis?", + "clear-history-confirmation-content": "Weet je zeker dat je deze chatgeschiedenis wilt wissen?", + "clear-history-title": "Wissen geschiedenis?", + "close": "Sluiten", + "close-all": "Sluit alles", + "close-app-button": "Bevestig", + "close-app-content": "De app zal stoppen en sluiten. Wanneer je de app heropent, wordt het geselecteerde netwerk gebruikt", + "close-app-title": "Waarschuwing!", + "command-button-send": "Verzenden", + "communities": "Communities", + "communities-alpha": "Communities (alfa)", + "communities-enabled": "Communities ingeschakeld", + "communities-verified": "✓ Geverifieerde Status Community", + "community": "Community", + "community-color": "Community-kleur", + "community-color-placeholder": "Kies een kleur", + "community-edit-title": "Bewerk community", + "community-image-pick": "Kies een afbeelding", + "community-image-remove": "Verwijderen", + "community-image-take": "Neem foto", + "community-info": "Community-info", + "community-info-not-found": "Community-informatie niet gevonden", + "community-invite-title": "Uitnodigen", + "community-key": "Community-private-key", + "community-key-placeholder": "Typ je community-private-key", + "community-link": "Communitylink", + "community-members": { + "one": "{{count}} deelnemer", + "other": "{{count}} deelnemers" + }, + "community-members-title": "Deelnemers", + "community-message-preview": "Uitnodiging om deelnemer te worden van {{community-name}}", + "community-private-key": "Community private-key", + "community-requests-to-join-title": "Deelnameverzoeken", + "community-roles": "Rollen", + "community-share-title": "Delen", + "community-thumbnail-image": "Miniatuurafbeelding", + "community-thumbnail-upload": "Uploaden", + "complete-hardwallet-setup": "Deze card is nu gekoppeld. Je hebt het nodig om transacties te ondertekenen en je keys te ontgrendelen", + "completed": "Voltooid", + "confirm": "Bevestig", + "confirm-new-password": "Bevestig nieuw wachtwoord", + "confirm-pairing-code-placeholder": "Bevestig je koppelcode ...", + "confirm-password-placeholder": "Bevestig je wachtwoord...", + "confirmation-request": "Bevestigingsverzoek", + "confirmations": "Bevestigingen", + "confirmations-helper-text": "Wanneer de transactie 12 bevestigingen heeft kun je het als verrekend beschouwen.", + "connect": "Verbinden", + "connect-mailserver-content": "Verbinden met {{name}} ?", + "connect-wallet": "Wallet verbinden", + "connected": "Verbonden", + "connected-to": "Verbonden met", + "connecting": "Verbinden...", + "connecting-requires-login": "Verbinding maken met een ander netwerk vereist inloggen", + "connection-status": "Verbindingsstatus", + "connection-with-the-card-lost": "Verbinding met de card\n is verloren", + "connection-with-the-card-lost-setup-text": "Om met instellen verder te gaan houd je de card tegen de achterkant van de telefoon en zorg je ervoor dat de card in contact blijft met de telefoon.", + "connection-with-the-card-lost-text": "Om verder te gaan, houd je de card tegen de achterkant van de telefoon", + "contact-code": "Chat-key", + "contact-s": { + "one": "contact", + "other": "contacten" + }, + "contacts": "Contacten", + "contacts-descr": "Je contacten verschijnen hier. Je ontvangt statusupdates van iedereen die u als contactpersoon toevoegt", + "contacts-empty": "Contacten met ENS-namen verschijnen hier", + "continue": "Doorgaan", + "continue-anyway": "Toch doorgaan", + "contract-address": "Contract adres", + "contract-interaction": "Contract-interactie", + "copy-info": "Kopieer informatie", + "copy-qr": "Kopieer code", + "copy-to-clipboard": "Kopiëren", + "copy-transaction-hash": "Kopieer transactie-ID", + "cost-fee": "Kosten\/Tarief", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Maken", + "create-a-pin": "Maak een 6-cijferige passcode", + "create-a-puk": "Maak een 12-cijferige PUK-code", + "create-category": "Categorie maken", + "create-channel": "Maak een kanaal", + "create-channel-title": "Nieuw kanaal", + "create-community": "Maak een community", + "create-group-chat": "Maak groepschat", + "create-multiaccount": "Keys aanmaken", + "create-new-key": "Nieuwe key aanmaken", + "create-pin": "Maak 6-cijferige passcode", + "create-pin-description": "Je hebt je card + deze 6-cijferige passcode nodig om Status te ontgrendelen en transacties te bevestigen", + "created-group-chat-description": "Je hebt de groep {{group-name}} gemaakt", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Valuta", + "currency-display-name-aed": "Emirati Dirham", + "currency-display-name-afn": "Afghaanse Afghani", + "currency-display-name-ars": "Argentijnse Peso", + "currency-display-name-aud": "Australische Dollar", + "currency-display-name-bbd": "Barbados Dollar", + "currency-display-name-bdt": "Bengalese Taka", + "currency-display-name-bgn": "Bulgaarse Lev", + "currency-display-name-bhd": "Bahreinse Dinar", + "currency-display-name-bnd": "Brunei Darussalam-Dollar", + "currency-display-name-bob": "Boliviaanse Boliviano", + "currency-display-name-brl": "Braziliaanse Reaal", + "currency-display-name-btn": "Bhutaanse Ngultrum", + "currency-display-name-cad": "Canadese Dollar", + "currency-display-name-chf": "Zwitserse Frank", + "currency-display-name-clp": "Chileense Peso", + "currency-display-name-cny": "Chinese Yuan Renminbi", + "currency-display-name-cop": "Colombiaanse Peso", + "currency-display-name-crc": "Costa Ricaanse Colon", + "currency-display-name-czk": "Tsjechische Kroon", + "currency-display-name-dkk": "Deense Kroon", + "currency-display-name-dop": "Dominicaanse Republiek Peso", + "currency-display-name-egp": "Egyptische Pond", + "currency-display-name-etb": "Ethiopische Birr", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Britse Pond", + "currency-display-name-gel": "Georgische Lari", + "currency-display-name-ghs": "Ghanese Cedi", + "currency-display-name-hkd": "Hong Kong Dollar", + "currency-display-name-hrk": "Kroatische Kuna", + "currency-display-name-huf": "Hongaarse Forint", + "currency-display-name-idr": "Indonesische Rupiah", + "currency-display-name-ils": "Israël Shekel", + "currency-display-name-inr": "Indiase Roepie", + "currency-display-name-isk": "IJslandse Kroon", + "currency-display-name-jmd": "Jamaicaanse Dollar", + "currency-display-name-jpy": "Japanse Yen", + "currency-display-name-kes": "Keniaanse Shilling", + "currency-display-name-krw": "Zuid-Koreaanse Won", + "currency-display-name-kwd": "Koeweitse Dinar", + "currency-display-name-kzt": "Kazachstan Tenge", + "currency-display-name-lkr": "Sri Lankaanse Roepie", + "currency-display-name-mad": "Marokkaanse Dirham", + "currency-display-name-mdl": "Moldavische Leu", + "currency-display-name-mur": "Mauritiaanse Roepie", + "currency-display-name-mwk": "Malawiaanse Kwacha", + "currency-display-name-mxn": "Mexicaanse Peso", + "currency-display-name-myr": "Maleise Ringgit", + "currency-display-name-mzn": "Mozambique Metical", + "currency-display-name-nad": "Namibische Dollar", + "currency-display-name-ngn": "Nigeriaanse Naira", + "currency-display-name-nok": "Noorse Kroon", + "currency-display-name-npr": "Nepalese Roepie", + "currency-display-name-nzd": "Nieuw Zeelandse Dollar", + "currency-display-name-omr": "Omaanse Rial", + "currency-display-name-pen": "Peruaanse Sol", + "currency-display-name-pgk": "Papoea-Nieuw-Guinea Kina", + "currency-display-name-php": "Filipijnse Peso", + "currency-display-name-pkr": "Pakistaanse Roepie", + "currency-display-name-pln": "Poolse Zloty", + "currency-display-name-pyg": "Paraguayaanse Guarani", + "currency-display-name-qar": "Qatar Riyal", + "currency-display-name-ron": "Roemeense Leu", + "currency-display-name-rsd": "Servische Dinar", + "currency-display-name-rub": "Russische Roebel", + "currency-display-name-sar": "Saoedi-Arabië Riyal", + "currency-display-name-sek": "Zweedse Kroon", + "currency-display-name-sgd": "Singapore Dollar", + "currency-display-name-thb": "Thailand Baht", + "currency-display-name-try": "Turkse Lira", + "currency-display-name-ttd": "Trinidad en Tobago Dollar", + "currency-display-name-twd": "Taiwanese Dollar", + "currency-display-name-tzs": "Tanzaniaanse Shilling", + "currency-display-name-uah": "Oekraïnse Grivna", + "currency-display-name-ugx": "Oegandese Shilling", + "currency-display-name-usd": "Amerikaanse Dollar", + "currency-display-name-uyu": "Uruguayaanse Peso", + "currency-display-name-vef": "Venezolaanse Bolivar", + "currency-display-name-vnd": "Vietnamese Dong", + "currency-display-name-zar": "Zuid-Afrikaanse Rand", + "current": "Huidig", + "current-average-tip": "Huidige gemiddelde fooi", + "current-base-fee": "Huidige base fee", + "current-minimum-tip": "Huidige minimum fooi", + "current-network": "Huidig netwerk", + "current-password": "Huidig wachtwoord", + "current-pin": "Voer 6-cijferige passcode in", + "current-pin-description": "Voer de 6-cijferige passcode in om door te gaan", + "custom": "Aangepast", + "custom-networks": "Aangepaste netwerken", + "custom-node": "Je gebruikt een aangepast RPC-eindpunt. Uw lokale transfergeschiedenis is mogelijk onvolledig.", + "custom-seed-phrase": "Afwijkende seed phrase", + "custom-seed-phrase-text-1": "De seed phrase komt niet overeen met ons ondersteunende woordenboek. Controleer op verkeerd gespelde woorden.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Accepteren en openen", + "dapp-starter-pack-description": "Hier is wat crypto om je op weg te helpen! Gebruik het om stickers, een ENS naam te krijgen en dapps te proberen", + "dapp-starter-pack-title": "Startpakket", + "dapp-would-like-to-connect-wallet": "wil graag verbinden met", + "dapps": "ÐApps", + "dapps-permissions": "DApp-permissies", + "dark": "Donker", + "data": "Gegevens", + "data-collected": "Verzamelde data", + "data-collected-subtitle": "Onderstaande tabel toont de exacte data die wordt opgeslagen en verzonden. Data wordt gevalideerd volgens openbare regels om ervoor te zorgen dat er geen gevoelige data wordt verzonden. Niet vertrouwen, verifiëren.", + "data-syncing": "Gegevens synchroniseren", + "database-reset-content": "Chats, contacten en instellingen zijn verwijderd. Je kunt het account gebruiken met je Keycard", + "database-reset-title": "Database resetten", + "database-reset-warning": "Database wordt gereset. Chats, contacten en instellingen worden verwijderd", + "datetime-ago": "geleden", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "dag", + "other": "dagen" + }, + "datetime-day-short": { + "one": "d", + "other": "d" + }, + "datetime-hour": { + "one": "uur", + "other": "uren" + }, + "datetime-hour-short": { + "one": "u", + "other": "u" + }, + "datetime-minute": { + "one": "minuut", + "other": "minuten" + }, + "datetime-minute-short": { + "one": "m", + "other": "m" + }, + "datetime-second": { + "one": "seconde", + "other": "seconden" + }, + "datetime-second-short": { + "one": "s", + "other": "s" + }, + "datetime-today": "vandaag", + "datetime-yesterday": "gisteren", + "decimals": "Decimalen", + "decline": "Afwijzen", + "decryption-failed-content": "Er is een fout opgetreden bij het decoderen van je gegevens. Mogelijk moet je de oude gegevens wissen en een nieuwe account aanmaken. Tik op \"Toepassen\" om te wissen of \"Annuleren\" om opnieuw te proberen", + "default": "Standaard", + "default-assets": "Standaard ERC20 en ERC721", + "default-sync-period": "Synchronisatie geschiedenis voor", + "delete": "Verwijderen", + "delete-account": "Account verwijderen", + "delete-and-leave-group": "Groep verwijderen en verlaten", + "delete-bootnode": "Verwijder bootnode", + "delete-bootnode-are-you-sure": "Weet je zeker dat je deze bootnode wilt verwijderen?", + "delete-bootnode-title": "Verwijder bootnode", + "close-chat": "Chat verwijderen", + "close-chat-confirmation": "Weet je zeker dat je deze chat wilt verwijderen?", + "delete-confirmation": "Verwijderen?", + "delete-keys-keycard": "Verwijder keys van Keycard", + "delete-mailserver": "Verwijderen Status-node", + "delete-mailserver-are-you-sure": "Weet je zeker dat je deze status-node wilt verwijderen?", + "delete-mailserver-title": "Status-node verwijderen", + "delete-message": "Verwijder bericht", + "delete-my-account": "Mijn account verwijderen", + "delete-my-profile": "Mijn profiel verwijderen", + "delete-network-confirmation": "Weet je zeker dat je dit netwerk wilt verwijderen?", + "delete-network-error": "Maak verbinding met een ander netwerk voordat je deze verwijdert", + "delete-network-title": "Netwerk verwijderen?", + "delete-profile": "Profiel verwijderen", + "delete-profile-warning": "Waarschuwing: als je de seed phrase niet hebt opgeschreven, verlies je toegang tot je middelen nadat je het profiel hebt verwijderd", + "deny": "Weigeren", + "derivation-path": "Afgeleid pad", + "describe-channel": "Kanaal beschrijven", + "description": "Beschrijving", + "dev-mode": "Ontwikkelmodus", + "dev-mode-settings": "Instellingen ontwikkelingsmodus", + "device-syncing": "Apparaat synchroniseren", + "devices": "Apparaten", + "disable": "uitschakelen", + "disable-all": "Alles uitschakelen", + "disabled": "Uitgeschakeld", + "disconnected": "Chat offline", + "discover": "Ontdek", + "dismiss": "Afwijzen", + "done": "Klaar", + "dont-ask": "Vraag niet opnieuw", + "edit": "Bewerken", + "edit-channel-title": "Kanaal bewerken", + "edit-chats": "Chats bewerken", + "edit-community": "Bewerk community", + "edit-favourite": "Favoriet bewerken", + "edit-group": "Groep bewerken", + "edit-profile": "Profiel bewerken", + "edited": "Bewerkt", + "editing-message": "Bericht bewerken", + "empty-activity-center": "Je chat-berichten\nverschijnen hier", + "empty-chat-description": "Er zijn nog geen berichten \nin deze chat", + "empty-chat-description-community": "Het is hier de afgelopen {{quiet-hours}} stil geweest.", + "empty-chat-description-one-to-one": "Alle berichten die je hier verzendt, zijn gecodeerd en kunnen alleen worden gelezen door jou en", + "empty-chat-description-public": "Het is hier de afgelopen {{quiet-hours}} stil geweest. Start het gesprek of ", + "empty-chat-description-public-share-this": "deel deze chat.", + "empty-keycard-required": "Vereist een lege keycard", + "empty-pending-invitations-descr": "Mensen die met groep willen deelnemen via\neen uitnodigingslink zullen hier verschijnen", + "empty-tab": "Leeg tabblad", + "enable": "Inschakelen", + "enable-all": "Alles inschakelen", + "enable-link-previews": "Inschakelen link-previews in chat?", + "encrypt-with-password": "Versleutel met wachtwoord", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Gebruikersnaam toevoegen", + "ens-agree-to": "Ga akkoord met ", + "ens-chat-settings": "Chat-instellingen", + "ens-custom-domain": "Aangepast domein", + "ens-custom-username-hints": "Typ de volledige gebruikersnaam inclusief het aangepaste domein zoals username.domain.eth", + "ens-custom-username-taken": "Gebruikersnaam is niet van jou :(", + "ens-deposit": "Storting", + "ens-dismiss-message": "Klik hier om te sluiten", + "ens-displayed-with": "Uw berichten worden aan anderen weergegeven met", + "ens-get-name": "Een universele gebruikersnaam aanvragen", + "ens-got-it": "Oké, ik snap het", + "ens-locked": "Gebruikersnaam vergrendeld. Je kunt het niet vrijgeven tot {{date}}", + "ens-name-content": "Eigen alias voor je chats-key die je kunt registreren met de Ethereum Name Service. ENS-namen zijn gedecentraliseerde gebruikersnamen.", + "ens-name-not-found": "Kan ENS-naam niet vinden", + "ens-name-title": "ENS-naam", + "ens-network-restriction": "Alleen beschikbaar op Mainnet", + "ens-no-usernames": "Je hebt geen gebruikersnaam verbonden", + "ens-powered-by": "Mogelijk gemaakt door Ethereum Name Services", + "ens-primary-username": "Primaire gebruikersnaam", + "ens-register": "Registreren", + "ens-registration-failed": "Probeer opnieuw om de gebruikersnaam te registreren.", + "ens-registration-failed-title": "Transactie mislukt", + "ens-registration-failure": "Registratie mislukt", + "ens-registration-in-progress": "Registratie bezig ...", + "ens-release-username": "Gebruikersnaam vrijgeven", + "ens-remove-hints": "Verwijderen zal gebruikersnaam van de key losmaken.", + "ens-remove-username": "Gebruikersnaam verwijderen", + "ens-saved": " is nu verbonden met je key en kan worden gebruikt in Status.", + "ens-saved-title": "Gebruikersnaam toegevoegd", + "ens-show-username": "Mijn ENS-gebruikersnaam weergeven in chats", + "ens-terms-header": "Voorwaarden voor naamregistratie", + "ens-terms-point-1": "Middelen worden voor 1 jaar gestort. Je SNT-tokens worden vastgehouden, maar niet uitgegeven.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS-register).", + "ens-terms-point-2": "Na 1 jaar kun je de naam vrijgeven en je aanbetaling terugkrijgen, of geen actie ondernemen om de naam te behouden.", + "ens-terms-point-3": "Als de voorwaarden van het contract veranderen - bv. Status maakt contractupgrades - heeft de gebruiker het recht om de gebruikersnaam vrij te geven, ongeacht de vastgehouden tijd.", + "ens-terms-point-4": "De contractbeheerder heeft geen toegang tot jouw gestorte middelen. Ze kunnen alleen terug worden gestuurd naar het adres wat ze heeft verzonden.", + "ens-terms-point-5": "Je adres(sen) zullen publiekelijk worden gekoppeld met je ENS-naam.", + "ens-terms-point-6": "Gebruikersnamen worden gemaakt als subdomein-nodes van stateofus.eth en zijn onderworpen aan de ENS-smart-contract voorwaarden.", + "ens-terms-point-7": "Je machtigt het contract om namens jou SNT over te maken. Dit kan alleen gebeuren als je een transactie goedkeurt om het overmaken te autoriseren.", + "ens-terms-point-8": "Deze voorwaarden worden gegarandeerd door de smart-contract logica van adressen:", + "ens-terms-point-9": "{{address}} (Status GebruikersnaamRegistratie)", + "ens-terms-registration": "Voorwaarden voor naamregistratie", + "ens-test-message": "Hallo", + "ens-transaction-pending": "Transactie in afwachting...", + "ens-understand": "Ik begrijp dat mijn wallet-adres openbaar zal worden verbonden met mijn gebruikersnaam.", + "ens-username": "ENS gebruikersnaam", + "ens-username-already-added": "Gebruikersnaam is al verbonden met je chat-key en kan gebruikt worden in Status.", + "ens-username-available": "✓ Gebruikersnaam beschikbaar!", + "ens-username-connected": "Deze gebruikersnaam is jouw eigendom en is verbonden met de chat-key.", + "ens-username-connected-continue": "Ga door om `Mijn ENS-gebruikersnaam weergeven in chats` in te stellen.", + "ens-username-connected-with-different-key": "Doorgaan zal een transactie vereisen om gebruikersnaam te verbinden met uw huidige chat-key.", + "ens-username-connection-confirmation": "{{username}} wordt verbonden zodra de transactie is voltooid.", + "ens-username-hints": "Minimaal 4 tekens. Allen Latijnse letters, cijfers en kleine letters.", + "ens-username-invalid": "Alleen letters en cijfers.", + "ens-username-owned": "✓ Gebruikersnaam is jouw eigendom. ", + "ens-username-owned-continue": "Doorgaan zal deze gebruikersnaam verbinden met je chat-key.", + "ens-username-registration-confirmation": "Mooi! Je bezit {{username}} zodra de transactie voltooid is.", + "ens-username-taken": "Gebruikersnaam is al bezet :(", + "ens-username-you-can-follow-progress": "Je kunt de voortgang volgen in het Transactie Geschiedenis gedeelte van je wallet.", + "ens-usernames": "ENS gebruikersnamen", + "ens-usernames-details": "Registreer een universele gebruikersnaam om makkelijk door andere gebruikers herkend te worden", + "ens-want-custom-domain": "Ik bezit een naam op een ander domein", + "ens-want-domain": "Ik wil een stateofus.eth domein", + "ens-welcome-hints": "ENS-namen zetten die hele lange adressen om in unieke gebruikersnamen.", + "ens-welcome-point-customize": "Een ENS-naam kan je willekeurige drie-woorden-naam in de chat vervangen. Wees @jenaam in plaats van {{name}}.", + "ens-welcome-point-customize-title": "Je chatnaam aanpassen", + "ens-welcome-point-receive": "Anderen kunnen je middelen sturen via chat met één simpele stap.", + "ens-welcome-point-receive-title": "Ontvang transacties in de chat", + "ens-welcome-point-register": "Registreer éénmaal om naam voor altijd te behouden. Na 1 jaar kunt je de naam vrijgeven en je SNT terugkrijgen.", + "ens-welcome-point-register-title": "10 SNT om te registreren", + "ens-welcome-point-simplify": "Je kunt middelen ontvangen op je eenvoudig te delen ENS-naam in plaats van op je hexadecimale hash (0x ...).", + "ens-welcome-point-simplify-title": "Vereenvoudig je ETH-adres", + "ens-welcome-point-verify": "Je kunt al je gebruikersnamen verifiëren en toevoegen in de volgende stappen.", + "ens-welcome-point-verify-title": "Heeft u al een gebruikersnaam?", + "ens-your-username": "Je gebruikersnaam", + "ens-your-usernames": "Je gebruikersnamen", + "ens-your-your-name": "Je ENS-naam", + "enter-12-words": "Voer de 12 woorden van je seed phrase in, gescheiden door spaties", + "enter-a-private-key": "Private-key invoeren", + "enter-a-seed-phrase": "Seed phrase invoeren", + "enter-address": "Adres invoeren", + "enter-contact-code": "ENS (vitalik94) of chat-key (0x04...)", + "enter-pair-code": "Voer je koppelcode in", + "enter-pair-code-description": "De koppelcode werd aan je weergegeven tijdens instellen van de Keycard", + "enter-password": "Voer wachtwoord in", + "enter-password-migration-prompt": "Voer wachtwoord in om contacten, chats en instellingen samen met je keys te verplaatsen", + "enter-pin": "Voer 6-cijferige passcode in", + "enter-puk-code": "Voer de PUK-code in", + "enter-puk-code-description": "6-cijferige passcode is geblokkeerd.\n Voer de PUK-code in om de passcode te deblokkeren.", + "enter-recipient-address-or-username": "Voer adres of gebruikersnaam van de ontvanger in", + "enter-seed-phrase": "Seed phrase invoeren", + "enter-url": "URL invoeren", + "enter-user-pk": "Voer public-key gebruiker in", + "enter-watch-account-address": "Scan een QR-code\nof\nvoer adres in om te kijken", + "enter-word": "Woord invoeren", + "enter-your-code": "Voer je 6-cijferige code in", + "enter-your-password": "Voer je wachtwoord in", + "error": "Fout", + "error-unable-to-get-balance": "Kan geen saldo vinden", + "error-unable-to-get-prices": "Fout bij het omrekenen van valuta. Vernieuw je scherm om het opnieuw te proberen.", + "error-unable-to-get-token-balance": "Kan tokensaldo niet krijgen", + "errors": "Fouten", + "eth": "ETH", + "ethereum-account": "Ethereum-account", + "ethereum-address": "Ethereum-adres", + "ethereum-node-started-incorrectly-description": "Ethereum-node werd gestart met onjuiste configuratie, applicatie zal worden gestopt om daarvan te herstellen. Geconfigureerd netwerk id = {{network-id}}, daadwerkelijk = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Ethereum-node onjuist gestart", + "etherscan-lookup": "Kijk op Etherscan", + "everyone": "Iedereen", + "expand-all": "Alles uitvouwen", + "export-account": "Account exporteren", + "export-key": "Exporteren private-key", + "external-storage-denied": "Toegang tot externe opslag is geweigerd", + "failed": "Mislukt", + "faq": "Vaak gestelde vragen", + "fast": "Snel", + "favourite": "Favoriet", + "favourite-description": "Je favoriete websites verschijnen hier", + "favourites": "Favorieten", + "favourites-empty": "Adressen toegevoegd aan favorieten verschijnen hier", + "fee-cap": "Tarieflimiet", + "fee-explanation": "Maximale totale prijs voor de transactie. Als de base fee van het blok dit overschrijdt, wordt deze opgenomen in een volgend blok met een lagere base fee.", + "fee-options": "Voorgestelde tariefopties", + "fetch-messages": "↓ Berichten ophalen", + "fetch-timeline": "↓ Ophalen", + "find": "Zoeken", + "finish": "Voltooien", + "finishing-card-setup": "Card-instellen voltooien", + "fleet": "Fleet", + "fleet-settings": "Fleet-instellingen", + "follow": "Volgen", + "follow-your-interests": "Spring in een publieke chat en ontmoet nieuwe mensen", + "free": "↓ Gratis", + "from": "Van", + "gas-amount-limit": "Limiet gas-bedrag", + "gas-limit": "Gas-limiet", + "gas-price": "Gas-prijs", + "gas-used": "Gas gebruikt", + "generate-a-key": "Keys aanmaken", + "generate-a-new-account": "Maak een account aan", + "generate-a-new-key": "Maak een nieuwe key aan", + "generate-account": "Keys aanmaken", + "generate-an-account": "Maak een account aan", + "generate-new-key": "Keys aanmaken", + "generating-codes-for-pairing": "> Downloaden productsoftware naar card\n > Genereren van ontgrendel & koppelcodes", + "generating-keys": "Keys genereren....", + "generating-mnemonic": "Seed phrase wordt aangemaakt", + "get-a-keycard": "Bestel een Keycard", + "get-started": "Starten", + "get-status-at": "Download Status op http:\/\/status.im", + "get-stickers": "Haal stickers", + "give-a-short-description": "Geef een korte beschrijving", + "give-a-short-description-community": "Geef het een korte beschrijving", + "give-permissions-camera": "Geef toestemming\nvoor toegang tot de camera", + "glossary": "Woordenlijst", + "go-to-settings": "Ga naar Instellingen...", + "got-it": "Begrepen", + "grant-face-id-permissions": "Om de vereiste toestemming voor Face ID te verlenen, ga je naar systeeminstellingen en zorg je ervoor dat Status > Face ID is geselecteerd", + "group-chat": "Groepschat", + "group-chat-admin": "Beheerder", + "group-chat-admin-added": "**{{member}}** is beheerder geworden", + "group-chat-all-contacts-invited": "Al uw contacten zitten al in de groep", + "group-chat-created": "**{{member}}** heeft de groep **{{name}}** gemaakt", + "group-chat-decline-invitation": "Uitnodiging afslaan", + "group-chat-member-added": "**{{member}}** is uitgenodigd", + "group-chat-member-joined": "**{{member}}** heeft zich bij de groep aangesloten", + "group-chat-member-removed": "**{{member}}** heeft de groep verlaten", + "group-chat-members-count": "{{selected}}\/ {{max}} deelnemers", + "group-chat-name-changed": "**{{member}}** heeft groepsnaam gewijzigd in **{{name}}**", + "group-chat-no-contacts": "Je hebt nog geen contacten.\n Nodig je vrienden uit om te chatten", + "group-info": "Groepsinfo", + "group-invite": "Groep-uitnodiging", + "group-invite-link": "Groep-uitnodigingslink", + "group-membership-request": "Groep deelnameverzoek", + "gwei": "Gwei", + "has-permissions": "heeft toestemming voor toegang", + "hash": "Hash", + "help": "hulp", + "help-capitalized": "Help", + "help-center": "Hulpcentrum", + "help-improve-status": "Help om Status te verbeteren", + "hide": "Verberg", + "hide-content-when-switching-apps": "Blokkeer screenshots maken", + "hide-content-when-switching-apps-ios": "Verberg preview", + "history": "Geschiedenis", + "history-nodes": "Status-nodes", + "hold-card": "Houd de card tegen de achterkant\n van je telefoon", + "home": "Home", + "hooks": "Hooks", + "how-it-works": "Hoe het werkt", + "http-gateway-error": "Oeps, verzoek mislukt!", + "identifier": "Identifier", + "image": "Afbeelding", + "image-remove-current": "Verwijder huidige foto", + "image-source-gallery": "Selecteer uit galerij", + "image-source-make-photo": "Vastleggen", + "image-source-title": "Foto bewerken", + "import": "Importeren", + "import-community": "Importeer een community", + "import-community-title": "Importeer een community", + "in-contacts": "In contacten", + "include": "Includeer", + "incoming": "Inkomend", + "incoming-transaction": "Inkomende transactie", + "incorrect-code": [ + "str", + "Sorry, de code was onjuist, voer het opnieuw in" + ], + "increase-gas": "Verhoog Gas", + "initialization": "Initialisatie", + "install": "↓ Installeer", + "intro-message1": "Welkom bij Status!\n Tik op dit bericht om je wachtwoord in te stellen en aan de slag te gaan.", + "intro-privacy-policy-note1": "Status verzamelt geen of profiteert niet van je persoonlijke gegevens. Door verder te gaan, ga je akkoord met het ", + "intro-privacy-policy-note2": "privacybeleid", + "intro-text": "Status is je toegangspoort tot het gedecentraliseerde web", + "intro-text1": "Chat via een peer-to-peer, versleuteld netwerk waar berichten niet gecensureerd of gehackt kunnen worden", + "intro-text2": "Verstuur en ontvang digitale assets overal ter wereld - geen bankrekening vereist", + "intro-text3": "Ontdek games, exchanges en sociale netwerken waar jij alleen eigenaar bent van je gegevens", + "intro-title1": "Echte privé-communicatie", + "intro-title2": "Veilige crypto-wallet", + "intro-title3": "Gedecentraliseerde apps", + "intro-wizard-text1": "Een set keys beheert je account. De keys staan op je telefoon, dus alleen jij kunt ze gebruiken", + "intro-wizard-text2": "Deze key wordt gebruikt voor chat. Het komt met een leesbare naam die niet kan worden veranderd.", + "intro-wizard-text3": "Als je een Keycard hebt, bewaar dan je keys daar voor extra veiligheid.", + "intro-wizard-text4": "Beveilig en versleutel je keys", + "intro-wizard-text6": "Status zal je melden over nieuwe berichten. Je kunt meldingsvoorkeuren later in de instellingen bewerken", + "intro-wizard-title-alt4": "Maak een wachtwoord", + "intro-wizard-title-alt5": "Bevestig je wachtwoord", + "intro-wizard-title1": "Ontvang je keys", + "intro-wizard-title2": "Kies een chatnaam", + "intro-wizard-title3": "Key-opslag selecteren", + "intro-wizard-title4": "Maak een 6-cijferige passcode", + "intro-wizard-title5": "Bevestig de passcode", + "intro-wizard-title6": "Meldingen inschakelen", + "introduce-yourself": "Stel jezelf voor met een kort bericht", + "invalid-address-qr-code": "Gescande QR-code bevat geen geldig adres", + "invalid-format": "Ongeldig formaat\nMoet {{format}} zijn", + "invalid-key-confirm": "Toepassen", + "invalid-key-content": "Database kan niet worden versleuteld omdat een bestand corrupt is. Je middelen en chat-key zijn veilig. Andere gegevens, zoals je chats en contacten, kunnen niet worden hersteld. \"{{erase-multiaccounts-data-button-text}}\" knop, zal alle andere gegevens verwijderen en geeft je toegang tot je middelen en het verzenden van berichten", + "invalid-number": "Ongeldig nummer", + "invalid-pairing-password": "Ongeldig koppel-wachtwoord", + "invalid-public-chat-topic": "Ongeldig publiek chatonderwerp", + "invalid-range": "Ongeldig formaat, moet tussen {{min}} en {{max}} liggen", + "invalid-username-or-key": "Ongeldige gebruikersnaam of chat key", + "invite": "Uitnodigen", + "invite-button": "Uitnodigen", + "invite-chat-accept": "Accepteren", + "invite-chat-accept-join": "Accepteren en toetreden", + "invite-chat-intro": "Je werd doorverwezen door een vriend om Status te installeren. Hier is wat crypto om je op weg te helpen! Gebruik het om een ENS-naam te registreren of een stickerpakket te kopen", + "invite-chat-name": "Verwijs een vriend door", + "invite-chat-pending": "In afwachting", + "invite-chat-rule": "Als je accepteert, wordt je vriend ook beloond met een crypto-doorverwijsbonus", + "invite-chat-starter-pack": "Starterspakket", + "invite-friends": "Vrienden uitnodigen", + "invite-instruction-fifth": "Je kunt er op ieder moment voor kiezen om je doorverwijsbonus in te wisselen.", + "invite-instruction-first": "Je stuurt een unieke uitnodigingslink naar je vriend om status te downloaden en te installeren", + "invite-instruction-fourth": "Je ontvangt je doorverwijsbonus en je vriend het starterspakket", + "invite-instruction-second": "Je vriend downloadt Status en maakt een account aan (op Android)", + "invite-instruction-third": "Er wordt een chat met je vriend gestart, waarin de doorverwijzing bevestigd wordt", + "invite-people": "Mensen uitnodigen", + "invite-privacy-policy-public": "Je hebt Status geïnstalleerd via een doorverwijs-link. Door deel te nemen aan deze chat, beloon je uw verwijzer toe en gaat u akkoord met de", + "invite-privacy-policy1": "Door te accepteren ga je akkoord met het doorverwijsprogramma", + "invite-privacy-policy2": "Algemene voorwaarden.", + "invite-public-chat-home": "Doorverwijsuitnodiging", + "invite-public-chat-intro": "Hier is wat crypto om je op weg te helpen! Gebruik het voor een ENS naam en stickers", + "invite-receive-account": "Account om doorverwijsbonus op te ontvangen", + "invite-reward": "Verdien crypto voor elke vriend die je uitnodigt!", + "invite-reward-friend": "Vriend:", + "invite-reward-friend-description": "Je vriend ontvangt een starterspakket dat bestaat uit enkele {{reward}} om aan de slag te gaan", + "invite-reward-friend-name": "Starterspakket", + "invite-reward-you": "Jij: ", + "invite-reward-you-description": "Nodig een vriend uit en ontvang {{reward}} als doorverwijsbonus. Gebruik het om stickers te krijgen, een ENS naam en probeer dapps", + "invite-reward-you-name": "Doorverwijsbonus", + "invite-select-account": "Selecteer een account om doorverwijsbonus te ontvangen", + "invite-warning": "Deze promotie is alleen geldig voor gebruikers van een Android toestel, die geen inwoner zijn van de VS. Vriend moet doorverwijzing binnen 7 dagen bevestigen", + "invited": "uitgenodigd", + "join": "Deelnemen", + "join-a-community": "of word lid van een community", + "join-group-chat": "Aan groep deelnemen", + "join-group-chat-description": "{{username}} heeft je uitgenodigd om deelnemer te worden van groep {{group-name}}", + "join-me": "Hey, doe mee met Status: {{url}}", + "join-new-private-chat": "Start een nieuwe privéchat", + "join-new-public-chat": "Deelnemen aan een publieke chat", + "joined": "Toegetreden", + "joined-group-chat-description": "Je bent deelnemer geworden van {{group-name}} via een uitnodiging van {{username}}", + "key": "Key", + "key-managment": "Key-beheer", + "key-on-device": "De private-key is opgeslagen op dit apparaat", + "keycard": "Keycard", + "keycard-access-reset": "Keycard-toegang is gereset", + "keycard-applet-install-instructions": "Om de applet te installeren volgt je de instructies op https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Beweeg de card om de NFC-lezer op uw apparaat te vinden", + "keycard-awaiting-title": "Nog steeds op zoek...", + "keycard-backup": "Maak een back-up Keycard", + "keycard-backup-success-body": "Back-upkaart maken is gelukt. Je kunt het nu met je account gebruiken, net als met de primaire kaart.", + "keycard-backup-success-title": "Back-up geslaagd", + "keycard-blocked": "Keycard is geblokkeerd.\nJe moet de card resetten om deze te kunnen blijven gebruiken.", + "keycard-can-use-with-new-passcode": "Je kunt deze card gebruiken met je nieuwe passcode", + "keycard-cancel-setup-text": "Hiermee wordt instellen van de keycard geannuleerd. Het wordt sterk aangeraden instellen af te ronden om de keycard te gebruiken. Wil je echt annuleren?", + "keycard-cancel-setup-title": "Gevaarlijke handeling", + "keycard-connected-description": "Probeer de card stil te houden", + "keycard-connected-title": "Verbonden", + "keycard-desc": "Heb je een Keycard? Bewaar je keys erop; je hebt ze nodig voor transacties", + "keycard-dont-ask-card": "Vraag niet om card om in te loggen", + "keycard-enter-new-passcode": "Voer een nieuwe passcode in {{step}} \/ 2", + "keycard-error-description": "Verbind de card opnieuw om door te gaan", + "keycard-error-title": "Verbinding verbroken", + "keycard-factory-reset": "Card terugzetten naar fabrieksinstellingen", + "keycard-factory-reset-text": "Als je dit uitvoert, worden alle mnemonic-phrases die op de card zijn opgeslagen, verwijderd. Zorg ervoor dat je een back-up hebt van de mnemonic-phrase die je met deze Keycard hebt gebruikt.", + "keycard-factory-reset-title": "Weet je zeker dat je een fabrieksreset wilt uitvoeren?", + "keycard-free-pairing-slots": "Keycard heeft {{n}} vrije koppelslots", + "keycard-has-multiaccount-on-it": "Deze card is vol. Elke card kan één key-paar bevatten", + "keycard-init-description": "Houdt de card nog eens tegen de achterkant van je telefoon om door te gaan", + "keycard-init-title": "Op zoek naar cards...", + "keycard-is-blocked-details": "Je kunt deze card niet langer gebruiken voor toegang of tekenen voor deze account. Er zijn te veel mislukte passcode- en PUK-pogingen geweest.", + "keycard-is-blocked-instructions": "Om toegang tot je account te krijgen moet je een fabrieksreset op de card uitvoeren. Tik op de onderstaande knop om de procedure te starten, je hebt jouw mnemonic-phrase nodig.", + "keycard-is-blocked-title": "Keycard is geblokkeerd", + "keycard-is-frozen-details": "Om je assets te beschermen, is je card bevroren. Reset de card om deze te deblokkeren en transacties te kunnen verzenden. Je kunt dit doen met jouw PUK of mnemonic-phrase.", + "keycard-is-frozen-factory-reset": "Reset met mnemonic", + "keycard-is-frozen-reset": "Reset met PUK", + "keycard-is-frozen-title": "Keycard is geblokkeerd", + "keycard-onboarding-finishing-header": "Afronden", + "keycard-onboarding-intro-header": "Bewaar je keys op Keycard", + "keycard-onboarding-intro-text": "Maak je klaar, dit kan een paar minuten duren, maar het is belangrijk om je account te beveiligen", + "keycard-onboarding-mnemonic-text": "Je hebt ook een stuk papier en een potlood nodig om je seed-phrase op te schrijven.", + "keycard-onboarding-pairing-header": "De card koppelen...", + "keycard-onboarding-pin-text": "Je moet een 6-cijferige toegangscode maken die wordt gebruikt om toegang tot je Keycard te beveiligen.", + "keycard-onboarding-preparing-header": "Card aan het voorbereiden...", + "keycard-onboarding-puk-code-header": "Schrijf de codes op\n en bewaar ze veilig", + "keycard-onboarding-recovery-phrase-description": "Je hebt deze seed phrase nodig om je key terug te krijgen. Schrijf het op. Bewaar het veilig, offline en apart van dit apparaat.", + "keycard-onboarding-recovery-phrase-header": "Maak een back up seed phrase", + "keycard-onboarding-recovery-phrase-text": "Alleen voor jouw ogen. Dit is de magische seed phrase die gebruikt wordt om je key aan te maken.", + "keycard-onboarding-start-header": "Houdt de card tegen de achterkant van je telefoon om te starten", + "keycard-onboarding-start-step1": "Maak een passcode", + "keycard-onboarding-start-step1-text": "Ongeveer 1 minuut. Maak een 6-cijferige passcode om je keys te versleutelen", + "keycard-onboarding-start-step2": "Schrijf de PUK en koppelcode op", + "keycard-onboarding-start-step2-text": "Ongeveer 1 minuut. Je hebt daar papier en potlood voor nodig", + "keycard-onboarding-start-step3": "Maak een back-up van de seed phrase", + "keycard-onboarding-start-step3-text": "Ongeveer 1 minuut. Ook papier en potlood zijn nodig", + "keycard-onboarding-start-text": "En houdt de card in contact met de telefoon tijdens het instellen. Instellen duurt ongeveer 4 minuten", + "keycard-processing-description": "probeer de kaar stil te houden", + "keycard-processing-title": "Verwerken...", + "keycard-recover": "verloren of bevroren card?", + "keycard-recover-text": "Als je de mnemonic-phrase hebt, kun je een nieuwe Keycard maken die aan dit account gekoppeld is. Je kunt een nieuwe Keycard gebruiken of een fabrieksreset uitvoeren op een bevroren Keycard.", + "keycard-recover-title": "Een nieuwe card voor dit account maken?", + "keycard-recovery-intro-button-text": "Start herstel", + "keycard-recovery-intro-header": "Herstel keys die op de keycard opgeslagen zijn", + "keycard-recovery-intro-text": "Als je eerder met een keycard keys hebt aangemaakt en deze nu op dit apparaat wilt gebruiken", + "keycard-recovery-no-key-header": "Er is hier niets om\nte herstellen", + "keycard-recovery-no-key-text": "Op je Keycard is geen key opgeslagen. Om deze te gebruiken, maak je een nieuwe key aan en kies je de Keycard om de key op te slaan", + "keycard-recovery-phrase-confirm-header": "Bevestig seed-phrase", + "keycard-recovery-phrase-confirmation-text": "Je krijgt geen tweede kans! Als je de toegang verliest, bijvoorbeeld door de keycard te verliezen, heb je alleen toegang tot je keys met de seed phrase. Alleen jij hebt de seed phrase. Schrijf het op. Hou het veilig.", + "keycard-recovery-phrase-confirmation-title": "Heb je de seed phrase opgeschreven?", + "keycard-recovery-success-header": "Je keys zijn\n met succes hersteld", + "keycard-redeem-title": "Inwisselen naar", + "keycard-redeem-tx": "Inwisselen assets", + "keycard-redeem-tx-desc": "Tik op de card om te tekenen en assets te ontvangen", + "keycard-reset-passcode": "Reset passcode", + "keycard-success-description": "Je mag de card nu verwijderen", + "keycard-success-title": "Succes", + "keycard-unauthorized-operation": "Je bent niet geautoriseerd om deze handeling uit te voeren.\nGebruik een geldige card en probeer het opnieuw.", + "keycard-upsell-subtitle": "Verbeterde veiligheid en gemak", + "language": "Taal", + "learn-more": "Meer leren", + "learn-more-about-keycard": "Meer informatie over Keycard", + "leave": "Verlaten", + "leave-chat": "Chat verlaten", + "leave-chat-confirmation": "Weet je zeker dat je de chat wilt verlaten?", + "leave-community": "Verlaat community", + "leave-confirmation": "Chat verlaten", + "leave-group": "Verlaat groep", + "left": "links", + "les-ulc": "LES \/ ULC", + "lets-go": "Let's go", + "light": "Licht", + "linked-on": "Gekoppeld op {{date}}", + "load-messages-before": "voor {{date}}", + "load-more-messages": "↓ Haal meer berichten op", + "load-more-timeline": "↓ Meer ophalen", + "loading": "Laden...", + "local-notifications": "Lokale meldingen", + "lock-app-with": "App vergrendelen met", + "log-level": "Log-niveau", + "log-level-settings": "Instellingen logniveau", + "logging": "Loggen", + "logging-enabled": "Loggen ingeschakeld?", + "login-pin-description": "Voer de 6-cijferige passcode in om je keys te ontgrendelen", + "logout": "Uitloggen", + "logout-app-content": "Het account wordt uitgelogd. Als je het weer ontgrendelt, wordt het geselecteerde netwerk gebruikt", + "logout-are-you-sure": "Weet je zeker dat je wilt\n uitloggen?", + "logout-key-management": "U moet uitloggen om toegang te krijgen tot het key-beheer.", + "logout-title": "Uitloggen?", + "looking-for-cards": "Op zoek naar cards...", + "lost-connection": "Verbinding verbroken", + "low-tip": "fooi is te laag", + "lower-than-average-tip": "lager dan gemiddelde fooi", + "mail-should-be-configured": "Mail client moet geconfigureerd zijn", + "mailserver-address": "Status-node adres", + "mailserver-automatic": "Automatische selectie", + "mailserver-automatic-switch-explanation": "Kies de snelst beschikbare Status-node", + "mailserver-connection-error": "Kon geen verbinding maken met Status-node", + "mailserver-content": "Een node in het Statusnetwerk dat berichten doorstuurt en opslaat, voor maximaal 30 dagen.", + "mailserver-details": "Status-node gegevens", + "mailserver-error-content": "De Status-node die je hebt geselecteerd kon niet worden bereikt.", + "mailserver-error-title": "Fout bij verbinden met Status-node", + "mailserver-format": "enode:\/\/{enode-id}:{wachtwoord}@{ip-adres}:{poort}", + "mailserver-pick-another": "Kies een andere Status-node", + "mailserver-reconnect": "Kan geen verbinding maken met Status-node. Tik om opnieuw te verbinden", + "mailserver-request-error-content": "De volgende fout werd door de Status-node teruggestuurd: {{error}}", + "mailserver-request-error-status": "Er is een fout opgetreden tijdens het ophalen van de geschiedenis, controleer de logs voor details", + "mailserver-request-error-title": "Status-node verzoekfout", + "mailserver-request-retry": "Verzoek opnieuw proberen", + "mailserver-retry": "Opnieuw proberen", + "mailserver-title": "Status-node", + "main-currency": "Hoofdvaluta", + "main-networks": "Hoofdnetwerken", + "main-wallet": "Hoofd-wallet", + "mainnet-network": "Hoofdnetwerk", + "make-admin": "Beheerder maken", + "manage-keys-and-storage": "Beheer keys en opslag", + "mark-all-read": "Markeer alles als gelezen", + "master-account": "Master-account", + "max-fee": "Maximale tarief", + "max-priority-fee": "Maximale prioriteitstarief", + "maximum-fee": "Maximale tarief", + "maybe-later": "Misschien later", + "member-ban": "Deelnemer verbannen", + "member-kick": "Verwijder deelnemer", + "members": { + "one": "1 deelnemer", + "other": "{{count}} deelnemers" + }, + "members-active": { + "one": "deelnemer", + "other": "deelnemers" + }, + "members-active-none": "geen deelnemers", + "members-count": "{{count}} deelnemers", + "members-label": "Deelnemers", + "members-limit-reached": "Deelnemers-limiet bereikt", + "members-title": "Deelnemers", + "membership-approval": "Goedkeuring vereisen", + "membership-approval-description": "Men kan gratis aan je community deelnemen, alleen moeten nieuwe deelnemers wel eerst door de maker van de community goedgekeurd worden", + "membership-button": "Criterium deelnemerschap", + "membership-declined": "Deelnameverzoek is afgewezen", + "membership-description": "Voor groepsdeelname moet je worden geaccepteerd door de groepsbeheerder", + "membership-ens": "ENS-gebruikersnaam vereist", + "membership-ens-description": "Je community vereist een ENS-gebruikersnaam om te kunnen deelnemen", + "membership-free": "Geen vereiste", + "membership-free-description": "Iedereen kan gratis aan je community deelnemen", + "membership-invite": "Uitnodiging van een ander lid vereisen", + "membership-invite-description": "Je kunt alleen deelnemen aan je community door een uitnodiging van bestaande community-deelnemers", + "membership-none": "Geen", + "membership-none-placeholder": "Je kunt nieuwe leden vragen om aan bepaalde criteria te voldoen voordat ze deel kunnen nemen. Dit kan op elk moment worden gewijzigd", + "membership-request-pending": "Deelnameverzoek in afwachting", + "membership-requests": "Deelnameverzoeken", + "membership-title": "Criterium deelnemerschap", + "message": "Bericht", + "message-not-sent": "Bericht niet verzonden", + "message-options-cancel": "Annuleren", + "message-reply": "Antwoord", + "messages": "Berichten", + "messages-from-contacts-only-subtitle": "Alleen mensen die je hebt toegevoegd als contacten, kunnen een nieuwe chat met je beginnen of je uitnodigen voor een groep", + "might-break": "Kan sommige ÐApps breken", + "migration-successful": "Migratie geslaagd", + "migration-successful-text": "Account succesvol gemigreerd naar Keycard", + "migrations-failed-content": "{{message}}\n schemaversie: eerste {{initial-version}}, huidige {{current-version}}, laatste {{last-version}} \n\nEr is een databasefout opgetreden. Je geld en chat-key zijn veilig. Andere gegevens, zoals je chats en contacten, kunnen niet worden hersteld. \"{{erase-multiaccounts-data-button-text}}\" knop, zal alle andere gegevens verwijderen en geeft je toegang tot je middelen en het versturen van berichten.", + "miners-higher-fee": "Miners zullen je transactie waarschijnlijk eerder opnemen als je een hoger tarief betaalt.", + "mobile-network-ask-me": "Vraag me wanneer mobiele dataverbinding", + "mobile-network-continue-syncing": "Ga door met synchroniseren", + "mobile-network-continue-syncing-details": "Je kunt dit later wijzigen in de instellingen", + "mobile-network-go-to-settings": "Ga naar instellingen", + "mobile-network-settings": "Mobiele dataverbinding", + "mobile-network-sheet-configure": "Je kunt synchronisatie verder configureren in", + "mobile-network-sheet-offline": "Geen wifi, berichten synchroniseren uitgeschakeld.", + "mobile-network-sheet-offline-details": "Synchroniseren via mobiel netwerk is uitgeschakeld", + "mobile-network-sheet-remember-choice": "Onthoud mijn keuze", + "mobile-network-sheet-settings": "instellingen", + "mobile-network-start-syncing": "Start synchroniseren", + "mobile-network-stop-syncing": "Stop met synchroniseren", + "mobile-network-stop-syncing-details": "Totdat er wifi-verbinding is?", + "mobile-network-use-mobile": "Gebruik mobiele dataverbinding", + "mobile-network-use-mobile-data": "Status gebruikt veel data bij het synchroniseren van chats en wallet.", + "mobile-network-use-wifi": "Alleen wifi", + "mobile-syncing-sheet-details": "Status gebruikt veel data bij het synchroniseren van chats en wallet.", + "mobile-syncing-sheet-title": "Synchroniseren met mobiele dataverbinding?", + "more": "meer", + "move-and-reset": "Verplaatsen en resetten", + "move-keystore-file": "Verplaats keystore-bestand", + "move-keystore-file-to-keycard": "Verplaats keystore-bestand naar keycard?", + "multiaccount-exists-content": "Keys voor dit account bestaan al en kunnen niet opnieuw worden toegevoegd. Als je je wachtwoord, toegangscode of Keycard bent kwijtgeraakt: verwijder de app, herinstalleer de app, en voer je seed phrase in", + "multiaccount-exists-title": "Keys voor dit account bestaan al", + "multiaccounts-recover-enter-phrase-text": "12, 15, 18, 21 of 24 woorden invoeren.\nScheid woorden met één spatie.", + "multiaccounts-recover-enter-phrase-title": "Voor je seed phrase in", + "mute": "Mute", + "my-accounts": "Mijn accounts", + "my-accounts-empty": "Uw beschikbare accounts verschijnen hier", + "my-profile": "Mijn profiel", + "my-status": "Mijn status", + "name": "Naam", + "name-of-token": "De naam van jouw token", + "name-optional": "Naam (optioneel)", + "name-your-channel": "Geef je kanaal een naam", + "name-your-channel-placeholder": "Kanaalnaam", + "name-your-community": "Geef je community een naam", + "name-your-community-placeholder": "Een pakkende naam", + "need-help": "Hulp nodig?", + "network": "Netwerk", + "network-chain": "Netwerkketen", + "network-details": "Netwerkgegevens", + "network-fee": "Netwerkkosten", + "network-id": "Netwerk-ID", + "network-info": "Netwerk info", + "network-invalid-network-id": "Opgegeven netwerk-id komt niet overeen met netwerk-id door RPC-url", + "network-invalid-status-code": "Ongeldige statuscode: {{code}}", + "network-invalid-url": "Netwerk-URL is ongeldig", + "network-settings": "Netwerkinstellingen", + "new": "Nieuw", + "new-category": "Nieuwe categorie", + "new-chat": "Nieuwe chat", + "new-community-title": "Nieuwe community", + "new-contact": "Nieuw contact", + "new-contract": "Nieuw contract", + "new-favourite": "Nieuwe favoriet", + "new-group": "Nieuwe groep", + "new-group-chat": "Nieuwe groepschat", + "new-network": "Nieuw netwerk", + "new-password": "Nieuw wachtwoord", + "new-pin-description": "Voer nieuwe 6-cijferige passcode in", + "new-public-group-chat": "Neem deel aan publieke chat", + "new-puk-description": "Voer nieuwe 12-cijferige PUK in", + "new-status": "Nieuwe status", + "new-tab": "Nieuw tabblad", + "next": "Volgende", + "nickname": "Bijnaam", + "nickname-description": "Bijnamen helpen je anderen te identificeren in Status.\nAlleen jij kunt de bijnamen zien die je hebt toegevoegd", + "no": "Nee", + "no-collectibles": "Geen collectibles beschikbaar", + "no-contacts": "Nog geen contacten", + "no-keycard-applet-on-card": "Geen Keycard applet op card", + "no-messages": "Geen berichten", + "no-pairing-slots-available": "Deze card is al aan 5 apparaten gekoppeld en kan hier niet aan worden gekoppeld. Gebruik één van de gekoppelde apparaten, log in met deze card en maak koppelings-slots op de card vrij", + "no-pinned-messages": "Geen vastgepinde berichten", + "no-result": "Geen resultaten", + "no-thanks": "Nee dank je", + "no-tokens-found": "Geen tokens gevonden", + "node-info": "Node informatie", + "node-version": "Node-versie", + "nodes-disabled": "Status-nodes uitgeschakeld", + "non-archival-node": "RPC-eindpunt ondersteunt geen archiveringsverzoeken. Je lokale transfergeschiedenis is mogelijk onvolledig.", + "non-contacts": "Niet-contacten", + "nonce": "Nonce", + "none": "Geen", + "normal": "Normaal", + "not-applicable": "Niet van toepassing op niet-ondertekende transacties", + "not-connected-nodes": "Niet verbonden met een Status-node", + "not-connected-to-peers": "Niet verbonden met peers", + "not-enough-snt": "Niet genoeg SNT", + "not-found": "Niet gevonden", + "not-keycard-text": "De card die je hebt gebruikt, is geen Keycard. Je moet een Keycard aanschaffen om deze te kunnen gebruiken.", + "not-keycard-title": "Geen Keycard", + "not-registered": "niet geregistreerd", + "notification-settings": "Meldingen", + "notifications": "Meldingen", + "notifications-non-contacts": "Meldingen van niet-contacten", + "notifications-preferences": "Voorkeuren voor meldingen", + "notifications-servers": "Notificatie-servers", + "notifications-switch": "Meldingen tonen", + "notifications-transactions": "Wallet-transacties", + "notify": "Melden", + "now": "Nu", + "off": "Uit", + "off-status-tree": "Uit Status-tree", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Schakel Status-nodes in om berichten op te halen die zijn verzonden terwijl de app gesloten was. Wanneer ingeschakeld, krijgt een Status-node je IP-adres. Wanneer uitgeschakeld, ontvang je geen berichten als de app gesloten is en zie je ze niet als je de app later opent.", + "offline-messaging-use-history-nodes": "Gebruik Status-nodes", + "ok": "OK", + "ok-continue": "Oké, doorgaan", + "ok-got-it": "Oké, ik snap het", + "ok-save-pass": "OK, wachtwoord opslaan", + "okay": "Oké", + "on": "Aan", + "on-status-tree": "In Status boom", + "once-enabled-share-metadata": "Eenmaal ingeschakeld, kunnen links die in de chat zijn geplaatst, uw metadata met de site delen", + "one-day": "Één dag", + "one-month": "Één maand", + "one-week": "Één week", + "open": "Open", + "open-chat": "Chat openen", + "open-dapp": "Open ÐApp", + "open-dapp-store": "Ontdek ÐApps", + "open-home": "Open...", + "open-in-new-tab": "Open in nieuw tabblad", + "open-membership": "Open deelname", + "open-nfc-settings": "Open NFC-instellingen", + "opening-buy-crypto": "Openen {{site}}...", + "optimal": "Optimaal", + "optional": "optioneel", + "or": "OF", + "outgoing": "Uitgaand", + "outgoing-transaction": "Uitgaande transactie", + "page-camera-request-blocked": "cameraverzoeken geblokkeerd. Ga naar Instellingen om cameraverzoeken in te schakelen", + "page-would-like-to-use-camera": "wil je camera gebruiken", + "pair": "Koppel apparaten", + "pair-card": "Koppel aan dit apparaat", + "pair-code": "Koppelcode", + "pair-code-explanation": "Koppelt de card aan een ander apparaat (maximaal 5) om keys te ontgrendelen en transacties te ondertekenen met dezelfde Keycard", + "pair-code-placeholder": "Koppelcode ...", + "pair-this-card": "Deze kaart koppelen", + "pair-this-device": "Adverteer apparaat", + "pair-this-device-description": "Koppel de apparaten om contacten en chats tussen hen te synchroniseren", + "paired-devices": "Gekoppelde apparaten", + "pairing": "Koppelen", + "pairing-card": "Koppel card", + "pairing-changed": "De koppelcode is gewijzigd", + "pairing-code-placeholder": "Koppelcode...", + "pairing-code_error1": "Koppelcodes komen niet overeen.", + "pairing-go-to-installation": "Ga naar koppelinstellingen", + "pairing-maximum-number-reached-content": "Schakel één van de apparaten uit voordat je een nieuwe inschakelt.", + "pairing-maximum-number-reached-title": "Maximaal aantal apparaten bereikt", + "pairing-new-installation-detected-content": "Er is een nieuw apparaat ontdekt.\nOm de apparaten correct te gebruiken, is het belangrijk om ze te koppelen en in te schakelen voordat je ze gebruikt.\nGa naar de apparaatsectie onder instellingen om de apparaten te koppelen.", + "pairing-new-installation-detected-title": "Nieuw apparaat ontdekt", + "pairing-no-info": "Geen informatie", + "pairing-please-set-a-name": "Stel naam in voor je apparaat.", + "passphrase": "Passphrase", + "password": "Wachtwoord", + "password-description": "Tenminste 6 tekens. Je wachtwoord beschermt je keys. Je hebt het nodig om Status te ontgrendelen en transacties te doen.", + "password-mismatch": "Nieuw wachtwoord en bevestiging komen niet overeen", + "password-placeholder": "Wachtwoord...", + "password-placeholder2": "Bevestig je wachtwoord", + "password-reset-in-progress": "Wachtwoord wijzigen...", + "password-reset-success": "Wachtwoord veranderd", + "password-reset-success-message": "Je zult opnieuw moeten inloggen", + "password_error1": "Wachtwoorden komen niet overeen.", + "paste": "Plakken", + "paste-json": "Plak JSON", + "pay-to-chat": "Betaal om te chatten", + "peer-content": "Een apparaat verbonden met het Status-chatnetwerk. Elke gebruiker kan één of meer peers vertegenwoordigen, afhankelijk van hun aantal apparaten.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Peers", + "pending": "In afwachting", + "pending-confirmation": "In afwachting van bevestiging...", + "pending-invitations": "Onbeantwoorde deelnameverzoeken", + "per-gas-price-limit": "Prijslimiet per gas", + "per-gas-tip-limit": "Fooilimiet per gas", + "permissions": "Toestemmingen", + "phone-e164": "Internationaal 1", + "photos": "Foto's", + "photos-access-error": "Om de vereiste toestemming voor foto's te verlenen, ga je naar de systeeminstellingen en zorgt je ervoor dat Status > Foto's is geselecteerd.", + "pin": "Pinnen", + "pin-changed": "6-cijferige passcode is gewijzigd", + "pin-code": "6-cijferige passcode", + "pin-limit-reached": "Pin limiet bereikt. Verwijder eerst de pin van een vorig bericht.", + "pin-mismatch": "Verkeerde passcode", + "pin-one-attempt": " één poging ", + "pin-one-attempt-blocked-after": "voordat je Keycard wordt geblokkeerd", + "pin-one-attempt-blocked-before": "Wees voorzichtig, je hebt maar", + "pin-one-attempt-frozen-after": "voordat je Keycard wordt geblokkeerd", + "pin-one-attempt-frozen-before": "Wees voorzichtig, je hebt maar", + "pin-retries-left": "{{number}} pogingen over", + "pinned-by": "Vastgepind door", + "pinned-messages": "Gepinde berichten", + "pinned-messages-count": { + "one": "1 vastgepind bericht", + "other": "{{count}} vastgepinde berichten" + }, + "pinned-messages-empty": "Vastgepinde berichten verschijnen hier. Om een bericht vast te pinnen, houd je het ingedrukt en tikt u op `Pinnen`.", + "preference": "Voorkeur", + "preview-privacy": "Voorbeeld privacymodus", + "previewing-may-share-metadata": "Het is mogelijk dat link-previews van deze websites metadata met hun eigenaren delen", + "principles": "Principes", + "privacy": "Privacy", + "privacy-and-security": "Privacy en beveiliging", + "privacy-policy": "Privacybeleid", + "private-key": "Private-key", + "private-notifications": "Lokale meldingen", + "private-notifications-descr": "Status zal je melden over nieuwe berichten. Je kunt meldingsvoorkeuren later in de instellingen bewerken.", + "processing": "Een ogenblik", + "product-information": "Productinformatie", + "profile": "Profiel", + "profile-deleted-content": "Je profiel is succesvol verwijderd", + "profile-deleted-keycard": "Je kunt nu een ander keypair op je Keycard herstellen", + "profile-deleted-title": "Profiel verwijderd", + "profile-details": "Profielgegevens", + "profile-not-found": "Profiel niet gevonden", + "profile-pic-pick": "Selecteer uit galerij", + "profile-pic-remove": "Verwijder foto", + "profile-pic-take": "Neem foto", + "public-channel": "Publiek kanaal", + "public-chat": "Publieke chat", + "public-chat-description": "Doe mee aan publieke chats voor jouw interesses! Iedereen kan een nieuwe starten.", + "public-chats": "Publieke chats", + "public-group-status": "Publiek", + "public-group-topic": "Onderwerp", + "public-key": "Public-key", + "puk-and-pairing-codes-displayed": "PUK- en koppelcodes weergegeven", + "puk-changed": "12-cijferige PUK is gewijzigd", + "puk-code": "PUK-code", + "puk-code-explanation": "Als je de 6-cijferige passcode vergeet of deze driemaal verkeerd invoert, heb je deze code nodig om jouw card te ontgrendelen.", + "puk-mismatch": "Verkeerde PUK-code", + "push-failed-transaction": "Transactie mislukt", + "push-failed-transaction-body": "{{value}} {{currency}} naar {{to}}", + "push-inbound-transaction": "Je hebt {{value}} {{currency}} ontvangen", + "push-inbound-transaction-body": "Van {{from}} naar {{to}}", + "push-notifications-server-enabled": "Server ingeschakeld", + "push-notifications-servers": "Push-notificatie servers", + "push-outbound-transaction": "Je hebt {{value}} {{currency}} verzonden", + "push-outbound-transaction-body": "Van {{from}} naar {{to}}", + "quiet-days": "{{quiet-days}} dagen", + "quiet-hours": "{{quiet-hours}} uur", + "re-encrypt-key": "Keys opnieuw versleutelen", + "receive": "Onvang", + "receive-transaction": "Ontvang transactie", + "recent": "Recent", + "recent-empty": "Recent gebruikte adressen verschijnen hier", + "recent-recipients": "Contacten", + "recently-used-stickers": "Recent gebruikte stickers verschijnen hier", + "recipient": "Ontvanger", + "recipient-code": "Voer adres van ontvanger in", + "recipient-code-placeholder": "0x ... of username.domain.eth", + "recover": "Herstellen", + "recover-key": "Toegang tot bestaande keys", + "recover-keycard-multiaccount-not-supported": "Keys voor dit account bestaan al en kunnen niet opnieuw worden toegevoegd. Als je wachtwoord, toegangscode of Keycard bent kwijtgeraakt: verwijder de app, herinstalleer de app, en voer je seed phrase in", + "recover-with-keycard": "Herstellen met Keycard", + "recover-with-seed-phrase": "Instellen met seed phrase", + "recovering-key": "Toegang tot keys...", + "recovery-confirm-phrase": "Bevestig seed phrase", + "recovery-phrase": "Seed phrase", + "recovery-success-text": "Je zult een nieuwe code of wachtwoord moeten aanmaken om uw keys opnieuw te versleutelen", + "recovery-typo-dialog-description": "Let op, je seed phrase moet exact dezelfde woorden en volgorde gebruiken als dat je het hebt ontvangen", + "recovery-typo-dialog-title": "Klopt de seed phrase?", + "redeem-amount": "{{quantity}} bonussen beschikbaar", + "redeem-now": "Nu inwisselen", + "redeem-success": "Inwisselen bonus succes!", + "reduced-tip": "prioriteitsfooi wordt verlaagd", + "refresh": "Vernieuwen", + "registered": "geregistreerd", + "reject-and-delete": "Afwijzen en verwijderen", + "remember-me": "Onthoud mij", + "remind-me-later": "Later nog eens zien", + "remove": "Verwijderen", + "remove-favourite": "Verwijder favoriet", + "remove-from-chat": "Verwijder uit chat", + "remove-from-contacts": "Verwijderen uit contacten", + "remove-from-contacts-text": "Door een gebruiker uit je contactenlijst te verwijderen, verbergt je jouw wallet-adres niet voor hen.", + "remove-group": "Groep verwijderen", + "remove-network": "Netwerk verwijderen", + "remove-token": "Token verwijderen", + "removed": "verwijderd", + "repeat-pin": "Herhaal nieuw 6-cijferige passcode", + "repeat-puk": "Herhaal nieuwe 12-cijferige PUK", + "replying-to": "{{author}} beantwoorden", + "report-bug-email-template": "1. Probleemomschrijving \n (Beschrijf de functie die je wilt, of vat de bug kort samen en wat je deed, wat je had verwacht en wat er daadwerkelijk gebeurde. Secties hieronder) \n\n\n 2. Stappen om te reproduceren \n (Beschrijf hoe we de bug stap voor stap kunnen repliceren.) \n -Open Status \n -... \n -Stap 3, etc. \n\n\n 3. Verwacht gedrag \n (Beschrijf wat je had verwacht.) \n\n\n 4. Feitelijk gedrag \n (Beschrijf wat er echt is gebeurd.) \n\n\n 5. Voeg screenshots toe die het probleem kunnen demonstreren \n", + "request-access": "Verzoek toegang", + "request-feature": "Verzoek een nieuwe functie", + "request-membership": "Verzoek deelname", + "request-pending": "Verzoek in afwachting...", + "request-transaction": "Verzoek transactie", + "required-field": "Verplicht veld", + "resend-message": "Opnieuw verzenden", + "reset-card": "Reset card", + "reset-card-description": "Deze handeling zet de card terug naar oorspronkelijke staat. Alle card-gegevens, inclusief de private-keys, worden gewist. De handeling is niet omkeerbaar.", + "reset-database": "Reset de database", + "reset-database-warning": "Verwijder chats, contacten en instellingen. Vereist als je het wachtwoord bent vergeten", + "reset-database-warning-keycard": "Verwijder chats, contacten en instellingen.", + "reset-password": "Wachtwoord resetten", + "restore-defaults": "Standaardwaarden herstellen", + "retry": "Opnieuw proberen", + "revoke-access": "Toegang intrekken", + "rpc-url": "RPC-URL", + "rpc-usage-copy": "Kopiëren", + "rpc-usage-filter": "Filtermethoden", + "rpc-usage-get-stats": "Vernieuwen", + "rpc-usage-info": "RPC-gebruiksstatistieken", + "rpc-usage-reset": "Reset", + "save": "Opslaan", + "save-password": "Wachtwoord opslaan", + "save-password-unavailable": "Instellen passcode van apparaat om wachtwoord op te slaan", + "save-password-unavailable-android": "Wachtwoord opslaan is niet beschikbaar: je toestel is wellicht geroot of mist de nodige beveiligingsfuncties.", + "scan-qr": "Scan QR-code", + "scan-qr-code": "Scan QR-code met een wallet-adres", + "scan-tokens": "Tokens scannen", + "search": "Zoeken", + "search-no-chat-found": "Geen zoekresultaten. Bedoel je", + "secret-keys-confirmation-text": "Je hebt ze nodig om jouw Keycard te kunnen blijven gebruiken, mocht je de telefoon ooit verliezen.", + "secret-keys-confirmation-title": "Heb je de codes opgeschreven?", + "security": "Veiligheid", + "see-details": "Zie gegevens", + "see-it-again": "ZIE HET OPNIEUW", + "see-suggestions": "Zie suggesties", + "seed-key-uid-mismatch": "Seed phrase komt niet overeen", + "seed-key-uid-mismatch-desc-1": "De seed phrase die je hebt ingevoerd, komt niet overeen met {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Controleer je seed phrase en probeer het opnieuw.", + "seed-phrase-content": "Een set eenvoudig te lezen woorden, willekeurig geselecteerd uit de BIP39-standaardlijst en gebruikt om je Ethereum-account op andere wallets en apparaten te herstellen of te openen. Ook wel een \"mnemonic phrase\", \"recovery phrase\" of \"wallet backup\" genoemd in het crypto-ecosysteem. Meeste crypto-apps gebruiken deze standaard om accounts te genereren.", + "seed-phrase-placeholder": "Seed phrase", + "seed-phrase-title": "Seed phrase", + "select": "Selecteer", + "select-account": "Selecteer account", + "select-account-dapp": "Selecteer het account dat je met Dapps wilt gebruiken", + "select-account-first": "Selecteer eerst een account", + "select-chat": "Selecteer chat om berichten te versturen", + "select-new-location-for-keys": "Selecteer een nieuwe locatie om uw private-key(s) op te slaan", + "selected": "Geselecteerde", + "send-logs": "Meld een bug", + "send-logs-to": "Een bug rapporteren aan {{email}}", + "send-message": "Bericht verzenden", + "send-push-notifications-description": "Wanneer uitgeschakeld, wordt de persoon die uw berichten ontvangt, niet op de hoogte gesteld van hun aankomst", + "send-request": "Verzend verzoek", + "send-request-amount": "Bedrag", + "send-request-amount-max-decimals": "Maximaal aantal decimalen is {{asset-decimals}}", + "send-request-unknown-token": "Onbekend token - {{asset}}", + "send-sending-to": "naar {{recipient-name}}", + "send-transaction": "Transactie verzenden", + "sending": "Verzenden", + "sent-at": "Verzonden om", + "server": "Server", + "set-a-topic": "Maak een onderwerp", + "set-currency": "Valuta instellen", + "set-custom-fee": "Stel aangepast tarief in", + "set-dapp-access-permissions": "DApp-toegangsrechten instellen", + "set-max": "Zet op max", + "settings": "Instellingen", + "share": "Delen", + "share-address": "Adres delen", + "share-chat": "Deel chat", + "share-contact-code": "Deel mijn chat-key", + "share-dapp-text": "Kijk eens naar deze DApp die ik op Status gebruik: {{link}}", + "share-link": "Link delen", + "share-my-profile": "Deel mijn profiel", + "share-profile": "Profiel delen", + "share-profile-link": "Deel profiel-link", + "share-public-chat-text": "Bekijk deze publieke chat in de Status-app: {{link}}", + "shared": "Gedeeld", + "sharing-copied-to-clipboard": "Gekopieerd", + "sharing-copy-to-clipboard": "Kopiëren", + "sharing-data-desc-1": "Data wordt gevalideerd aan de hand van openbare regels om zeker te zijn dat geen gevoelige data wordt verzonden. Vertrouw niet, verifieer.", + "sharing-data-desc-2": "Gebruiksdata wordt end-to-end versleuteld verzonden via het peer-to-peer-netwerk van Status", + "sharing-data-desc-3": "In plaats van je gewone chat-key wordt een eenmalige-key gebruikt", + "sharing-data-desc-4": "Gebruiksdata kan niet worden gekoppeld aan je IP-adres", + "sharing-data-desc-5": "Cumulatieve data van alle gebruikers zijn openbaar beschikbaar", + "sharing-data-desc-6": "De data wordt van je telefoon verwijderd nadat het is verzonden", + "sharing-share": "Delen", + "show-less": "Minder weergeven", + "show-more": "Toon meer", + "show-notifications": "Meldingen weergeven", + "show-profile-pictures": "Toon profielfoto's van", + "show-qr": "Toon QR-code", + "show-transaction-data": "Weergeven transactiegegevens", + "sign-and-send": "Tekenen en verzenden", + "sign-anyway": "Toch tekenen", + "sign-in": "Inloggen", + "sign-message": "Teken bericht", + "sign-out": "Uitloggen", + "sign-request-failed": "Kon bericht niet ondertekenen", + "sign-with": "Teken met", + "sign-with-password": "Tekenen met wachtwoord", + "sign-you-in": "Ontsleutelen", + "signing": "Ondertekenen", + "signing-a-message": "Handtekening plaatsen", + "signing-phrase": "Geheime code", + "skip": "Overslaan", + "slow": "Traag", + "something-went-wrong": "Er is iets fout gegaan", + "soon": "Binnenkort", + "specify-address": "Adres opgeven", + "specify-name": "Geef een naam op", + "specify-network-id": "Specificeer netwerk-id", + "specify-rpc-url": "Geef een RPC-URL op", + "specify-server-public-key": "Voer public-key van de server in", + "start-chat": "Start chat", + "start-conversation": "Start gesprek", + "start-group-chat": "Start groepschat", + "start-new-chat": "Start nieuwe chat", + "starter-pack-coming": "Starterspakket komt jouw kant op", + "starter-pack-coming-description": "Het kan enkele minuten tot uren duren", + "starter-pack-received": "Starter Pack ontvangen", + "starter-pack-received-description": "Hier is wat crypto om je op weg te helpen! Gebruik het voor stickers, een ENS naam en om dapps te proberen", + "status": "Status", + "status-confirmed": "Bevestigd", + "status-hardwallet": "Status hardwallet", + "status-is-open-source": "Status is open-source", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status gebruikt veel data bij het synchroniseren van chats. Je kunt ervoor kiezen om niet te synchroniseren als je een mobiele dataverbinding hebt", + "status-not-sent-click": "Niet bevestigd. Klik voor opties", + "status-not-sent-tap": "Niet bevestigd. Tik voor opties", + "status-pending": "In afwachting", + "status-sent": "Verzonden", + "status-tx-not-found": "TX niet gevonden", + "status-updates-descr": "Statusupdates verschijnen hier. Voeg het profiel als contactpersoon toe om updates te ontvangen op je tijdlijn.", + "statuses-descr": "Deel wat je bezighoudt en blijf op de hoogte van je contacten", + "statuses-my-status-descr": "Deel wat je bezighoudt. Iedereen die uw profiel bezoekt, kan je status zien. Mensen die jou als contactpersoon toevoegen, ontvangen je updates op hun tijdlijn", + "step-i-of-n": "Stap {{step}} van {{number}}", + "sticker": "Sticker", + "sticker-market": "Stickermarkt", + "storage": "Opslag", + "submit": "Bevestigen", + "submit-bug": "Een bug inzenden", + "success": "Succes", + "suggested-min-tip": "Voorgestelde min. fooi", + "suggested-price-limit": "Voorgestelde prijslimiet", + "symbol": "Symbool", + "sync-all-devices": "Synchroniseer alle apparaten", + "sync-in-progress": "Synchroniseren...", + "sync-settings": "Synchronisatie-instellingen", + "sync-synced": "In sync", + "syncing-devices": "Synchroniseren...", + "system": "Systeem", + "tabs": "Tabbladen", + "tag-was-lost": "Tag is verloren", + "tap-card-again": "Houdt de card nog eens tegen de achterkant van je telefoon.", + "terms-of-service": "Gebruiksvoorwaarden", + "test-networks": "Test netwerken", + "text-input-disabled": "Een ogenblik alstublieft...", + "thank-you": "Bedankt", + "this-device": "Dit apparaat", + "this-device-desc": "Je keys worden versleuteld en veilig op je apparaat opgeslagen", + "this-is-you-signing": "Dit is je geheime code", + "this-will-take-few-seconds": "Dit duurt een paar seconden", + "three-days": "Drie dagen", + "three-words-description": "Je moet deze 3 woorden zien voordat je elke transactie tekent", + "three-words-description-2": "Als je een andere combinatie ziet, annuleer de transactie en log uit", + "timeline": "Tijdlijn", + "tip-cap": "Fooilimiet", + "to": "Naar", + "to-block": "Blokkeren", + "to-enable-biometric": "Om {{bio-type-label}} in te schakelen, moet je het wachtwoord opslaan op het ontgrendelscherm", + "to-encrypt-enter-password": "Voer uw wachtwoord in om je account te versleutelen.", + "to-see-this-message": "Om dit bericht te zien,", + "token-auto-validate-decimals-error": "Verkeerde decimalen voor token {{symbol}} op adres {{address}} - ingesteld op {{expected}} maar gedetecteerd als {{actual}}", + "token-auto-validate-name-error": "Verkeerde naam voor token {{symbol}} op adres {{address}} - ingesteld op {{expected}} maar gedetecteerd als {{actual}}", + "token-auto-validate-symbol-error": "Verkeerd symbool voor token {{symbol}} op adres {{address}} - ingesteld op {{expected}} maar gedetecteerd als {{actual}}", + "token-details": "Token-gegevens", + "topic-name-error": "Gebruik alleen kleine letters (a tot z), cijfers & streepjes (-). Gebruik geen chat-keys", + "transaction": "Transactie", + "transaction-data": "Transactie gegevens", + "transaction-declined": "Transactie afgewezen", + "transaction-description": "Beschouw het als voltooid na 12 bevestigingen op het netwerk.", + "transaction-details": "Transactiegegevens", + "transaction-failed": "Transactie mislukt", + "transaction-history": "Transactie geschiedenis", + "transaction-request": "Transactieverzoek", + "transaction-sent": "Transactie verzonden", + "transaction-signed": "De transactie is ondertekend", + "transactions": "Transacties", + "transactions-filter-select-all": "Selecteer alles", + "transactions-filter-title": "Filter geschiedenis", + "transactions-history": "Transactie geschiedenis", + "transactions-history-empty": "Nog geen transacties in je geschiedenis", + "transactions-history-loading": "Laden transactie geschiedenis. Dit kan even duren.", + "transactions-load-more": "Meer laden", + "transactions-management-enabled": "Transactiebeheer (alfa)", + "transactions-sign": "Teken", + "transfer-ma-unknown-error-desc-1": "Het lijkt erop dat je multiaccount niet is verwijderd. Database is mogelijk gereset", + "transfer-ma-unknown-error-desc-2": "Controleer je accountlijst en probeer het opnieuw. Als account niet in de lijst staat, ga naar Toegang tot bestaande keys om te herstellen met seed phrase", + "transfers-fetching-failure": "De tranfer-geschiedenis kon niet worden bijgewerkt. Controleer uw verbinding en trek naar beneden om opnieuw te proberen", + "tribute-required-by-multiaccount": "{{multiaccount-naam}} heeft SNT nodig om een chat te starten.", + "tribute-state-paid": "Tribute betaald", + "tribute-state-pending": "Tribute in afwachting", + "tribute-state-required": "Vereist {{snt-amount}} SNT-tribute", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Voeg vrienden toe als contact om chats mogelijk te maken zonder tribute-betaling.", + "tribute-to-talk-are-you-friends": "Zijn jullie vrienden?", + "tribute-to-talk-ask-to-be-added": "Vraag om als contact te worden toegevoegd", + "tribute-to-talk-contact-received-your-tribute": " heeft je tribute ontvangen. Jullie kunnen nu veilig met elkaar chatten.", + "tribute-to-talk-desc": "Verdien geld voor jouw aandacht door SNT te vragen aan nieuwe mensen om een chat te starten", + "tribute-to-talk-disabled": "Tribute to Talk uitgeschakeld", + "tribute-to-talk-disabled-note": "Vanaf nu kunnen nieuwe mensen een chat met je starten zonder SNT te sturen.", + "tribute-to-talk-enabled": "Je hebt Tribute to Talk ingeschakeld.", + "tribute-to-talk-finish-desc": "Vanaf nu zul je alleen chats ontvangen van contacten, en van mensen die betaald hebben ", + "tribute-to-talk-learn-more-1": "Je tijd en aandacht zijn je meest waardevolle bezit. Met Tribute to Talk kun je een hoeveelheid SNT instellen die nieuwe mensen nodig hebben om een chat met je te starten.", + "tribute-to-talk-learn-more-2": "Iedereen die niet in je contactlijst staat, zal worden gevraagd te betalen, en je kunt reageren zodra ze dat hebben gedaan.", + "tribute-to-talk-learn-more-3": "Je kunt het geld altijd terugsturen, maar om ervoor te zorgen dat vrienden je vrij kunnen bereiken, voeg je ze eerst toe als contact.", + "tribute-to-talk-paywall-learn-more-1": "Onze tijd en aandacht zijn ons meest waardevolle bezit. Met Tribute to Talk kun je contact leggen met nieuwe mensen in ruil voor een SNT-betaling.", + "tribute-to-talk-paywall-learn-more-2": "Om een chat te starten met iemand die een tribute-set heeft, betaal je gewoon de vereiste SNT en zal je worden toegevoegd als contact.", + "tribute-to-talk-paywall-learn-more-3": "Als je ze kent, kun je buiten Status je profiel delen om gratis te worden toegevoegd.", + "tribute-to-talk-pending": "Tribute in afwachting van bevestiging", + "tribute-to-talk-pending-note": "Tribute-transactie is in afwachting van bevestiging op het netwerk. Je kunt de status controleren in de transactiegeschiedenis", + "tribute-to-talk-removing-note": "Als je Tribute to Talk verwijdert, kunnen nieuwe mensen een chat starten zonder SNT te verzenden. Vereist dat er een transactie wordt uitgevoerd.", + "tribute-to-talk-set-snt-amount": "Stel de hoeveelheid SNT in die nodig is voor nieuwe mensen om een chat te starten", + "tribute-to-talk-signing": "Wachten om transactie te tekenen", + "tribute-to-talk-transaction-failed-note": "De transactie is mislukt en je instellingen voor Tribute to Talk zijn niet gewijzigd", + "tribute-to-talk-tribute-received1": "Tribute ontvangen. Jij en ", + "tribute-to-talk-tribute-received2": " zijn nu contacten en kunnen veilig met elkaar chatten.", + "tribute-to-talk-you-require-snt": "Je hebt SNT nodig voor nieuwe mensen om een chat te starten.", + "try-again": "Probeer het opnieuw", + "try-keeping-the-card-still": "Probeer de card stil te houden", + "turn-nfc-description": "NFC is uitgeschakeld op uw apparaat. U kunt het inschakelen in de instellingen", + "turn-nfc-on": "Schakel NFC in om door te gaan", + "tx-fail-description1": "Deze transactie zal waarschijnlijk mislukken. Stel aangepast netwerktarief in om op eigen risico te ondertekenen.", + "tx-fail-description2": "Deze transactie zal waarschijnlijk mislukken. Stel aangepast netwerktarief in om op eigen risico te ondertekenen.", + "type": "Type", + "type-a-message": "Bericht", + "ulc-enabled": "ULC ingeschakeld", + "unable-to-fetch": "Kan chatgeschiedenis niet ophalen", + "unable-to-read-this-code": "Kan deze code niet lezen", + "unable-to-send-messages": "Kan geen berichten verzenden en ontvangen", + "unblock": "Deblokkeren", + "unblock-contact": "Deblokkeer deze gebruiker", + "unknown-status-go-error": "Onbekende status-go-fout", + "unlock": "Ontgrendelen", + "unmute": "Unmute", + "unpair-card": "Card ontkoppelen", + "unpair-card-confirmation": "Deze handeling ontkoppelt de card van het huidige apparaat. Hiervoor is een 6-cijferige toegangscode vereist. Wil je doorgaan?", + "unpair-keycard": "Keycard loskoppelen van deze telefoon", + "unpair-keycard-warning": "Je koppelcode \/ PUK en PIN blijven ongewijzigd", + "unpaired-keycard-text": "De gebruikte Keycard is niet aan deze telefoon gekoppeld", + "unpaired-keycard-title": "Het lijkt erop dat je kaart niet is gekoppeld", + "unpin": "Pin losmaken", + "update": "Update", + "update-to-listen-audio": "Update naar de nieuwste versie om hier naar een audiobericht te luisteren!", + "update-to-see-image": "Update naar de nieuwste versie om hier een leuke afbeelding te zien!", + "update-to-see-sticker": "Update naar de nieuwste versie om hier een leuke sticker te zien!", + "updates-to-tos": "Updates van Gebruiksvoorwaarden", + "updates-to-tos-desc": "Lees voor doorgaan de Gebruiksvoorwaarden door en bevestig dat je volle verantwoordelijkheid neemt voor hoe je de app gebruikt.", + "url": "URL", + "usd-currency": "USD", + "use-valid-contact-code": "Voer een geldige chat-key of gebruikersnaam in of scan deze", + "validation-amount-invalid-number": "Bedrag is geen geldig getal", + "validation-amount-is-too-precise": "Bedrag is te nauwkeurig. Het maximale aantal decimalen is {{decimals}}.", + "verified-community": "✓ Geverifieerde community", + "version": "App-versie", + "view": "View", + "view-cryptokitties": "Bekijk in CryptoKitties", + "view-cryptostrikers": "Bekijk in CryptoStrikers", + "view-data": "Data weergeven", + "view-details": "Bekijken gegevens", + "view-etheremon": "Bekijk in Etheremon", + "view-gitcoin": "Bekijk in Gitcoin", + "view-profile": "Profiel weergeven", + "view-public-dashboard": "Openbaar dashboard weergeven", + "view-rules": "Regels weergeven", + "view-signing": "Bekijk de geheime code", + "view-superrare": "Bekijk in SuperRare", + "waiting-for-wifi": "Geen wifi, berichten synchroniseren uitgeschakeld.", + "waiting-for-wifi-change": "Instellingen", + "waiting-to-sign": "Wachten om transactie te tekenen...", + "waiting-wi-fi": "Wachten op wifi…", + "waku-bloom-filter-mode": "Waku bloom filter-modus", + "wallet": "Wallet", + "wallet-address": "Wallet-adres", + "wallet-asset": "Asset", + "wallet-assets": "Assets", + "wallet-backup-recovery-title": "Maak een back-up van je seed phrase", + "wallet-choose-recipient": "Kies Ontvanger", + "wallet-collectibles": "Collectibles", + "wallet-insufficient-funds": "Onvoldoende middelen", + "wallet-insufficient-gas": "Niet genoeg ETH voor gas", + "wallet-invalid-address": "Ongeldig adres:\n {{data}}", + "wallet-invalid-address-checksum": "Fout in adres:\n {{data}}", + "wallet-invalid-chain-id": "Netwerk komt niet overeen:\n {{data}} maar huidige chain is {{chain}}", + "wallet-key-content": "Een hex-adres van 64 tekens op basis van de Ethereum-standaard en beginnent met 0x. Je accountadres wordt openbaar gemaakt en met anderen gedeeld wanneer je middelen wilt ontvangen. Ook wel een \"Ethereum-adres\" of \"wallet-adres\" genoemd.", + "wallet-key-title": "Accountadres", + "wallet-manage-accounts": "Accounts beheren", + "wallet-manage-assets": "Beheer assets", + "wallet-request": "Verzoek", + "wallet-send": "Verzenden", + "wallet-send-min-units": "Min 21000 eenheden", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Wallet-instellingen", + "wallet-total-value": "Totale waarde", + "wallet-transaction-total-fee": "Totale Tarief", + "wants-to-access-profile": "wil toegang tot je profiel", + "warning": "Waarschuwing", + "warning-message": "Sorry, we beperken het snel na elkaar verzenden van meerdere berichten om spam te voorkomen. Probeer het later nog eens", + "warning-sending-to-contract-descr": "Het adres dat u hebt ingevoerd is een smart contract, het verzenden van middelen naar dit adres kan leiden tot verlies van middelen. Als u met een DApp wilt communiceren, opent u de DApp in de Status DApp-browser.", + "watch-only": "Alleen kijken", + "wc-brand-guide": "Richtlijnen voor het gebruik van branding zoals handelsmerken en logo's", + "wc-disclaimer": "Disclaimers (inclusief externe leveranciers), garanties en wettelijke verklaringen", + "wc-dispute": "Bepalingen voor geschillenbeslechting", + "wc-how-to-use-status-app": "Hoe de Status-app te gebruiken, inclusief privacy en beveiliging", + "wc-new-tos-based-on-principles-prefix": "Nieuwe gebruiksvoorwaarden ontworpen op basis van onze", + "web-view-error": "Kan pagina niet laden", + "websites": "Websites", + "webview-camera-permission-requests": "Webview cameraverzoeken", + "webview-camera-permission-requests-subtitle": "Wanneer ingeschakeld, kunnen websites en dapps vragen om je camera te gebruiken", + "welcome-blank-community-message": "Je community's verschijnen hier.", + "welcome-blank-message": "Je chats verschijnen hier. Druk op de ⊕-knop om nieuwe chats te starten", + "welcome-community-blank-message": "Je chats verschijnen hier. Klik op de 3 stippen hierboven en selecteer \"Maak een kanaal\" om nieuwe chats te starten.", + "welcome-screen-text": "Stel uw wallet in, nodig vrienden uit om te chatten en blader door populaire dapps!", + "welcome-to-status": "Welkom bij Status!", + "welcome-to-status-description": "Stel je crypto-wallet in, nodig vrienden uit om te chatten en blader door decentralized-apps", + "what-changed": "Wat is er veranderd", + "what-is-shared": "Wat wordt gedeeld", + "whats-on-your-mind": "Wat houdt je bezig...", + "word-count": "Aantal woorden", + "word-n": "Woord #{{number}}", + "word-n-description": "Om te controleren of je de seed phrase correct hebt opgeslagen, voer je hierboven woord #{{number}} in.", + "words-n": { + "one": "1 woord", + "other": "{{count}} woorden" + }, + "write-down-and-store-securely": "Schrijf codes op\n & bewaar ze veilig", + "wrong-address": "Verkeerde adres", + "wrong-card": "Verkeerde card", + "wrong-card-text": "Gebruikte card komt niet overeen met de geselecteerde keys", + "wrong-contract": "Verkeerde contract", + "wrong-keycard-text": "De gebruikte Keycard is niet aan deze telefoon gekoppeld", + "wrong-keycard-title": "Het lijkt erop dat je een verkeerde keycard hebt gebruikt", + "wrong-password": "Verkeerd wachtwoord", + "wrong-word": "Verkeerd woord", + "yes": "Ja", + "you": "jij", + "you-already-have-an-asset": "Je hebt al een asset {{value}}", + "you-are-all-set": "Je bent klaar!", + "you-are-all-set-description": "Als je de telefoon verliest, heb je nu toegang tot je data, crypto en chat key met behulp van je seed phrase", + "you-can-change-account": "Je kunt de accountnaam en -kleur naar wens wijzigen", + "you-can-choose-preview-websites": "Je kunt kiezen welke van de volgende websites een link-preview van beschrijvingen en afbeeldingen in chats kunnen tonen", + "you-can-fetch": "Je kunt chatgeschiedenis ophalen", + "you-dont-have-contacts": "Je hebt nog geen contacten.", + "you-dont-have-contacts-invite-friends": "Je hebt nog geen contacten.\nNodig je vrienden uit om te beginnen met chatten.", + "you-dont-have-stickers": "Je hebt nog geen stickers", + "you-will-need-this-code": "Je hebt deze code nodig om Status te openen en transacties te tekenen", + "you-will-start-from-scratch": "Je start helemaal opnieuw met een nieuwe set keys", + "your-card-is-frozen": "Je Keycard is geblokkeerd. Card-toegang resetten", + "your-contact-code": "Toegang verlenen machtigt deze DApp om je chat-key op te halen", + "your-data-belongs-to-you": "Als je de seed phrase verliest, verlies je toegang tot je data, crypto en chat key", + "your-data-belongs-to-you-description": "Als je de toegang verliest, bijvoorbeeld doordat je de telefoon verliest, heb je alleen toegang tot je keys met je seed-phrase. Niemand behalve jij heeft de seed phrase. Schrijf het op. Bewaar het veilig", + "your-keys": "Je keys", + "your-price-limit": "Jouw prijslimiet", + "your-recovery-phrase": "Jouw seed phrase", + "your-recovery-phrase-description": "Dit is je seed phrase. Je gebruikt het om te bewijzen dat dit je wallet is. Je krijgt het maar één keer te zien! Schrijf het op papier en bewaar het op een veilige plaats. Je hebt het nodig als je de wallet verliest of opnieuw installeert.", + "your-tip-limit": "Jouw fooilimiet", + "youre-on-mobile-network": "Je hebt een mobiele dataverbinding" +} diff --git a/translations/pl.json b/translations/pl.json new file mode 100644 index 00000000000..237acb045f0 --- /dev/null +++ b/translations/pl.json @@ -0,0 +1,1743 @@ +{ + "You": "Ty", + "about-app": "O aplikacji", + "about-key-storage-content": "Status nigdy nie uzyska dostępu do Twojego klucza prywatnego. Pamiętaj, aby wykonać kopię zapasową frazy odzyskiwania (Seed). Jeśli zgubisz telefon, jest to jedyny sposób, aby uzyskać dostęp do kluczy.", + "about-key-storage-title": "Informacje o przechowywaniu kluczy", + "about-names-content": "Nikt nie może udawać, że jest Tobą! Domyślnie jesteś anonimowy i nigdy nie musisz ujawniać swojego prawdziwego imienia. Możesz zarejestrować własną, niestandardową nazwę za niewielką opłatą.", + "about-names-title": "Nazwy nie mogą być zmieniane", + "about-sharing-data": "O udostępnianiu danych", + "accept": "Akceptuj", + "accept-and-add": "Zaakceptuj i dodaj", + "accept-and-continue": "Zaakceptuj i kontynuuj", + "accept-and-share-address": "Zaakceptuj i udostępnij adres", + "accept-new-chats-from": "Akceptuj nowe czaty od", + "accept-status-tos-prefix": "Akceptuję Status", + "access-existing-keys": "Uzyskaj dostęp do istniejących kluczy", + "access-key": "Uzyskaj dostęp do kluczy", + "account-added": "Konto dodane", + "account-color": "Kolor konta", + "account-content": "Możesz porównać konta w Statusie do kont bankowych. Podobnie jak konto bankowe, konto zazwyczaj ma adres i saldo; Używasz tego konta do transakcji na Ethereum. Możesz mieć wiele kont w swoim portfelu. Wszystkie dostępne po odblokowaniu Statusu.", + "account-exists-title": "Konto już istnieje", + "account-is-used": "Konto jest używane z Dapps w przeglądarce.", + "account-name": "Nazwa konta", + "account-settings": "Ustawienia konta", + "account-title": "Konto", + "accounts": "Konta", + "actions": "Działania", + "active-online": "Online", + "active-unknown": "Nieznany", + "activity": "Aktywność", + "add": "Dodaj", + "add-a-watch-account": "Dodaj adresy tylko do obserwacji", + "add-account": "Dodaj konto", + "add-account-description": "Możesz zaimportować dowolny typ konta Ethereum, aby dodać je do swojego portfela Status", + "add-account-incorrect-password": "Hasło wygląda na nieprawidłowe. Wprowadź hasło, którego używasz do odblokowania aplikacji.", + "add-an-account": "Dodaj konto", + "add-bootnode": "Dodaj bootnode", + "add-contact": "Dodaj kontakt", + "add-custom-token": "Dodaj niestandardowy token", + "add-favourite": "Dodaj ulubione", + "add-mailserver": "Dodaj węzeł Status", + "add-members": "Dodaj użytkowników", + "add-network": "Dodaj sieć", + "add-new-contact": "Dodaj nowy kontakt", + "add-nickname": "Dodaj pseudonim (opcjonalnie)", + "add-node": "Dodaj węzeł", + "add-private-key-account": "Dodaj konto z klucza prywatnego", + "add-seed-account": "Dodaj konto z frazą odzyskiwania (Seed)", + "add-to-contacts": "Dodaj do kontaktów", + "add-to-contacts-text": "Dodając użytkownika do swojej listy kontaktów, udostępniasz mu swój adres portfela", + "add-to-favourites": "Dodaj do ulubionych", + "add-watch-account": "Dodaj konto tylko do oglądania", + "address": "Adres", + "address-or-ens-name": "Adres lub nazwa ENS", + "address-received": "Adres otrzymany", + "address-request-accepted": "Żądanie adresu zostało zaakceptowane", + "address-requested": "Żądany adres", + "advanced": "Zaawansowane", + "advanced-settings": "Zaawansowane ustawienia", + "advertiser-description": "Odkryłeś Status dzięki partnerowi. Czy masz coś przeciwko, jeśli Status sprawdzi tylko raz Twój adres IP, aby go wynagrodzić? Informacje te nie zostaną wykorzystane do niczego innego i zostaną całkowicie usunięte po 7 dniach.", + "advertiser-starter-pack-accept": "Akceptuj", + "advertiser-starter-pack-decline": "Odrzuć", + "advertiser-starter-pack-description": "Oto trochę kryptowalut na dobry początek! Użyj ich, aby zarejestrować nazwę ENS lub kupić pakiet naklejek", + "advertiser-starter-pack-title": "Pakiet startowy", + "advertiser-title": "Prywatność domyślnie", + "agree-by-continuing": "Kontynuując, zgadzasz się \nna naszą ", + "all": "Wszystko", + "allow": "Zezwól", + "allow-and-send": "Zezwól i wyślij", + "allow-mention-notifications": "Pokaż @ wzmianki", + "allowing-authorizes-this-dapp": "Zgadzając się, umożliwisz tej zdecentralizowanej aplikacji na pobranie adresu Twojego portfela i uruchomieniu Web3", + "already-have-asset": "Masz już to aktywo", + "amount": "Ilość", + "anyone": "Ktoś", + "appearance": "Wygląd", + "apply": "Zastosuj", + "approve": "Zatwierdź", + "approve-limit": "Zatwierdź limit", + "approve-token": "Zatwierdź token", + "approve-token-contract-desc": "Zatwierdzenie tokena z kontraktem pozwala mu na wydanie Twojego salda tokenów. Jeśli uważasz, że dany projekt jest niegodny zaufania, nie zatwierdzaj z nim tokena, lub zatwierdzaj z nim tylko kwotę, którą wykorzystasz.", + "are-you-sure": "Jesteś pewny?", + "are-you-sure-description": "Nie będziesz w stanie ponownie zobaczyć całej frazy odzyskiwania (Seed)", + "are-you-sure-to-cancel": "Czy na pewno chcesz anulować?", + "are-you-sure?": "Jesteś pewny?", + "ask-in-status": "Zadaj pytanie lub zgłoś błąd", + "at": "w", + "attribution-received": "{{attrib}} z {{max}} otrzymanych premii", + "audio": "Audio", + "audio-recorder": "Dyktafon", + "audio-recorder-error": "Błąd Dyktafonu", + "audio-recorder-max-ms-reached": "Osiągnięto maksymalny czas nagrywania", + "audio-recorder-permissions-error": "Musisz wyrazić zgodę na wysyłanie wiadomości audio", + "authorize": "Autoryzuj", + "available": "Dostępny", + "available-participants": { + "one": "jednego", + "few": "kilku", + "many": "wielu", + "other": "innych" + }, + "back": "Powrót", + "back-up": "Utwórz kopię zapasową", + "back-up-seed-phrase": "Utwórz kopię zapasową frazy odzyskiwania (Seed)", + "back-up-your-seed-phrase": "Utwórz kopię zapasową frazy odzyskiwania (Seed)", + "backing-up": "Tworzenie kopii zapasowej...", + "backup-disabled": "Wyłączone", + "backup-enabled": "Włączony", + "backup-recovery-phrase": "Utwórz kopię zapasową frazy odzyskiwania (Seed)", + "backup-settings": "Ustawienia kopii zapasowej", + "backup-through-waku": "Backup przez waku", + "bad-fees-description": "Twoja opłata priorytetowa jest poniżej naszych sugerowanych parametrów.", + "balance": "Saldo", + "begin-set-up": "Rozpocznij konfigurację", + "below-base-fee": "opłata maksymalna poniżej opłaty podstawowej", + "biometric-auth-android-sensor-desc": "Sensor Touch ID", + "biometric-auth-android-sensor-error-desc": "Niepowodzenie", + "biometric-auth-android-title": "Wymagane uwierzytelnienie", + "biometric-auth-confirm-logout": "Zaloguj się ponownie", + "biometric-auth-confirm-message": "Aby kontynuować, wymagane jest uwierzytelnianie biometryczne, a jeśli nie jest to możliwe, odblokuj klucze za pomocą hasła lub kodu dostępu", + "biometric-auth-confirm-title": "Musisz się uwierzytelnić!", + "biometric-auth-confirm-try-again": "Spróbuj ponownie", + "biometric-auth-error": "Nie można przeprowadzić uwierzytelnienia biometrycznego ({{code}})", + "biometric-auth-login-error-title": "Błąd uwierzytelniania biometrycznego", + "biometric-auth-login-ios-fallback-label": "Wprowadź hasło", + "biometric-auth-reason-login": "Zaloguj się do Status", + "biometric-auth-reason-verify": "Zweryfikuj uwierzytelnianie", + "biometric-disable-bioauth": "wyłącz {{bio-type-label}}", + "biometric-disable-password-description": "Jeśli to wyłączysz, również ", + "biometric-disable-password-title": "Wyłącz zapisywanie hasła", + "biometric-enable": "Jeśli nie chcesz wpisywać swojego hasła za każdym razem, aby uzyskać dostęp do aplikacji, włącz logowanie {{bio-type-label}}.", + "biometric-enable-button": "Włącz {{bio-type-label}}", + "biometric-enable-keycard": "Jeśli nie chcesz używać karty Keycard za każdym razem, aby uzyskać dostęp do aplikacji, włącz opcję logowanie {{bio-type-label}}", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Odcisk palca", + "biometric-secure-with": "Zabezpiecz za pomocą {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "hasło BIP39", + "blank-keycard-text": "Możesz kontynuować korzystanie z karty dostępu, kiedy wygenerujesz swoje klucze oraz imię", + "blank-keycard-title": "Wygląda na to, że kliknąłeś \nzłą kartę.", + "block": "Blok", + "block-contact": "Zablokuj tego użytkownika", + "block-contact-details": "Zablokowanie usunie poprzednie wiadomości tego użytkownika i uniemożliwi otrzymywanie nowych od Ciebie.", + "blocked-users": "Zablokowani użytkownicy", + "bootnode-address": "Adres bootnode", + "bootnode-details": "Szczegóły bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes włączone", + "bootnodes-settings": "Opcje bootnodes", + "browsed-websites": "Historia przeglądarki pojawi się tutaj", + "browser": "Przeglądarka", + "browser-not-secure": "Połączenie nie jest bezpieczne! Nie podpisuj transakcji ani nie przesyłaj danych osobowych na tej stronie.", + "browser-secure": "Połączenie jest bezpieczne. Upewnij się, że naprawdę ufasz tej stronie przed podpisaniem transakcji lub wprowadzeniem danych osobowych.", + "browsers": "Przeglądarki", + "browsing-cancel": "Anuluj", + "browsing-open-in-android-web-browser": "Otwórz w Android", + "browsing-open-in-ios-web-browser": "Otwórz w iOS", + "browsing-open-in-status": "Otwórz w Status", + "browsing-site-blocked-description1": "Wykryliśmy potencjalną szkodliwą aktywność z tego adresu. Aby Cię chronić oraz Twój portfel, uniemożliwiamy dalszą nawigację. \n\nJeśli uważasz, że to błąd, daj nam znać w ", + "browsing-site-blocked-description2": " czat publiczny.", + "browsing-site-blocked-go-back": "Wróć", + "browsing-site-blocked-title": "Ta strona jest zablokowana", + "browsing-title": "Przeglądaj", + "bug-report": "Zgłoś błąd", + "bug-report-description": "* Opis", + "bug-report-description-placeholder": "Wymagane, nie może być puste", + "bug-report-steps": "Kroki do odtworzenia", + "bug-report-steps-placeholder": "- otwórz aplikację\n- zrób coś\n- a potem jeszcze coś...", + "bug-report-submit-email": "Prześlij e-mailem z archiwum logów", + "bug-report-submit-gh-issue": "Zgłoś problem na GitHubie bez logów", + "bug-report-too-short-description": "Opis jest za krótki", + "build-yourself": "Aby korzystać z aplikacji bez niniejszych Warunków Użytkowania, możesz zbudować własną wersję", + "buy-crypto": "Kup kryptowaluty", + "buy-crypto-choose-a-service": "Wybierz usługę, której chcesz użyć do zakupu kryptowalut", + "buy-crypto-description": "Znajdź zdecentralizowaną aplikację, aby kupić kryptowaluty", + "buy-crypto-leaving": "Opuszczasz aplikację Status i wchodzisz na zewnętrzną stronę internetową w celu sfinalizowania zakupu.", + "buy-crypto-title": "Wygląda na to, że Twój portfel jest pusty", + "camera-access-error": "Aby nadać wymagane uprawnienia dla aparatu, przejdź do ustawień systemowych i upewnij się, że zaznaczono opcję Status > Aparat.", + "can-not-add-yourself": "To ty, aby rozpocząć rozmowę, wybierz kogoś innego", + "can-send-messages": "Możesz wysyłać i odbierać nowe wiadomości", + "cancel": "Anuluj", + "cancel-keycard-setup": "Anuluj konfigurację karty Keycard", + "cancelling": "Anulowanie", + "cannot-read-card": "Nie można odczytać karty.\n Przyłóż ją do tylnej części telefonu", + "cannot-use-default-pin": "Kod dostępu 000000 jest niedozwolony.\n Użyj innego numeru", + "cant-open-public-chat": "Nie można utworzyć czatu publicznego", + "cant-report-bug": "Nie można zgłosić błędu", + "card-is-blank": "Ta karta jest pusta", + "card-reseted": "Karta została zresetowana", + "card-unpaired": "Karta została odłączona od bieżącego urządzenia", + "category": "Kategoria", + "category-title": "Tytuł kategorii", + "change-fleet": "Zmień fleet na {{fleet}}", + "change-logging-enabled": "Czy na pewno chcesz {{enable}} logowanie?", + "change-pairing": "Zmień kod parowania", + "change-pairing-description": "Zmiana kodu parowania nie wpływa na bieżące sparowania. Jednak każde nowe parowanie będzie wymagać nowego kodu.", + "change-pairing-title": "Utwórz nowy kod parowania", + "change-passcode": "Zmiana kodu dostępu", + "change-password": "Zmień hasło", + "change-pin": "Zmień 6-cyfrowy kod dostępu", + "change-puk": "Zmień 12-cyfrowy PUK", + "change-tip": "Zmień napiwek", + "changed-amount-warning": "Kwota została zmieniona z {{old}} na {{new}}", + "changed-asset-warning": "Aktywo zostało zmienione z {{old}} na {{new}}", + "chaos-mode": "Tryb Chaos", + "chaos-unicorn-day": "Chaos Dzień Jednorożca", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Czat", + "chat-and-transact": "Czatuj i przeprowadzaj prywatne transakcje z przyjaciółmi", + "chat-is-a-contact": "Kontakt", + "chat-is-not-a-contact": "To nie kontakt", + "chat-key": "Klucz czatu", + "chat-key-content": "Wiadomości w protokole czatu Status są wysyłane i odbierane przy użyciu kluczy szyfrujących. Klucz czatu publicznego to ciąg znaków, które udostępniasz innym, aby mogli wysyłać Ci wiadomości w statusie.", + "chat-key-title": "Klucz czatu", + "chat-link-previews": "Podgląd linków czatu", + "chat-name": "Nazwa czatu", + "chat-name-content": "Trzy losowe słowa, uzyskane algorytmicznie z twojego klucza czatu i używane jako twoja domyślna nazwa na czacie. Nazwy czatów są całkowicie unikalne; żaden inny użytkownik nie może mieć takich samych trzech słów, nazwy.", + "chat-name-title": "Nazwa czatu", + "chat-notification-preferences": "Ustawienia powiadomień", + "chat-settings": "Ustawienia czatu", + "chats": "Czaty", + "check-on-opensea": "Sprawdź na Opensea", + "check-your-recovery-phrase": "Sprawdź swoją frazę odzyskiwania (Seed)", + "choose-actions": "Wybierz działania", + "choose-authentication-method": "Wybierz metodę uwierzytelniania", + "choose-new-location-for-keystore": "Wybierz nową lokalizację, aby zapisać plik keystore", + "choose-storage": "Wybierz miejsce na dane", + "clear": "Wyczyść", + "clear-all": "Wyczyść wszystko", + "clear-history": "Wyczyść historię", + "clear-history-action": "Wyczyść", + "clear-history-confirmation": "Wyczyścić historię?", + "clear-history-confirmation-content": "Czy na pewno chcesz wyczyścić historię czatu?", + "clear-history-title": "Wyczyścić historię?", + "close": "Zamknij", + "close-all": "Zamknij wszystkie", + "close-app-button": "Potwierdź", + "close-app-content": "Aplikacja zatrzyma się i zamknie. Po ponownym otwarciu wybrana sieć zostanie użyta.", + "close-app-title": "Ostrzeżenie!", + "collectibles-leak-metadata": "Tutaj możesz wyświetlić swoje NFT. Jeśli to zrobisz, udostępnisz swój portfel i adres IP", + "command-button-send": "Wyślij", + "communities": "Społeczności", + "communities-alpha": "Społeczności (alpha)", + "communities-enabled": "Społeczności włączone", + "communities-verified": "✓ Zweryfikowana Społeczność Statusu", + "community": "Społeczność", + "community-color": "Kolor społeczności", + "community-color-placeholder": "Wybierz kolor", + "community-edit-title": "Edytuj społeczność", + "community-emoji-thumbnail-title": "Miniaturka", + "community-image-pick": "Wybierz obraz", + "community-image-remove": "Usuń", + "community-image-take": "Zrób zdjęcie", + "community-info": "Informacje o społeczności", + "community-info-not-found": "Nie znaleziono informacji o społeczności", + "community-invite-title": "Zaproś", + "community-key": "Klucz prywatny społeczności", + "community-key-placeholder": "Wpisz klucz prywatny swojej społeczności", + "community-link": "Link do społeczności", + "community-members": { + "one": "jeden", + "few": "kilku", + "many": "wielu", + "other": "inni" + }, + "community-members-title": "Użytkownicy", + "community-message-preview": "Zaproszenie do dołączenia do {{community-name}}", + "community-private-key": "Klucz prywatny społeczności", + "community-requests-to-join-title": "Prośby o członkostwo", + "community-roles": "Role", + "community-share-title": "Udostępnij", + "community-thumbnail-image": "Miniatura", + "community-thumbnail-upload": "Prześlij", + "complete-hardwallet-setup": "Ta karta jest teraz połączona. Potrzebujesz jej do podpisywania transakcji i odblokowywania kluczy", + "completed": "Zakończono", + "confirm": "Potwierdź", + "confirm-new-password": "Potwierdź nowe hasło", + "confirm-pairing-code-placeholder": "Potwierdź swój kod parowania...", + "confirm-password-placeholder": "Potwierdź swoje hasło", + "confirmation-request": "Prośba o potwierdzenie", + "confirmations": "Potwierdzenia", + "confirmations-helper-text": "Kiedy transakcja ma 12 potwierdzeń, możesz uznać ją za uregulowaną.", + "connect": "Połącz", + "connect-mailserver-content": "Połącz z {{name}}?", + "connect-wallet": "Podłącz portfel", + "connected": "Połączono", + "connected-to": "Podłączony do", + "connecting": "Trwa łączenie...", + "connecting-requires-login": "Połączenie z inną siecią wymaga logowania", + "connection-status": "Stan połączenia", + "connection-with-the-card-lost": "Połączenie z kartą\n zostało utracone", + "connection-with-the-card-lost-setup-text": "Aby wznowić konfigurację, przyłóż kartę \nz tyłu telefonu i utrzymuj fizyczny kontakt \nmiędzy kartą a telefonem", + "connection-with-the-card-lost-text": "Aby kontynuować, przyłóż kartę do tylnej części telefonu", + "contact-code": "Klucz czatu", + "contact-s": { + "one": "kontakt", + "few": "kontaktów", + "many": "kontaktów", + "other": "kontakty" + }, + "contacts": "Kontakty", + "contacts-descr": "W tym miejscu pojawią się Twoje kontakty. Będziesz otrzymywać aktualizacje statusu od każdej osoby, którą dodasz jako kontakt.", + "contacts-empty": "Kontakty z nazwami ENS będą wyświetlane tutaj", + "continue": "Kontynuuj", + "continue-anyway": "Mimo to kontynuuj", + "contract-address": "Adres kontraktu", + "contract-interaction": "Interakcja kontraktu", + "contract-isnt-supported": "Kontrakt nie jest obsługiwany", + "copy-info": "Kopiuj informacje", + "copy-qr": "Skopiuj kod", + "copy-to-clipboard": "Kopiuj", + "copy-transaction-hash": "Skopiuj ID transakcji", + "cost-fee": "Koszt\/Opłata", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Utwórz", + "create-a-pin": "Utwórz 6-cyfrowy kod dostępu", + "create-a-puk": "Utwórz 12-cyfrowy PUK", + "create-category": "Utwórz kategorię", + "create-channel": "Utwórz kanał", + "create-channel-title": "Nowy kanał", + "create-community": "Utwórz społeczność", + "create-group-chat": "Utwórz czat grupowy", + "create-multiaccount": "Wygeneruj klucze", + "create-new-key": "Uzyskaj nowe klucze", + "create-pin": "Utwórz 6-cyfrowy kod dostępu", + "create-pin-description": "Będziesz potrzebować swojej karty i 6-cyfrowego kodu, aby odblokować aplikację Status oraz potwierdzać transakcje.", + "created-group-chat-description": "Grupa {{group-name}}została utworzona", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Waluta", + "currency-display-name-aed": "Dirham Zjednoczonych Emiratów Arabskich", + "currency-display-name-afn": "Afgani afganistańskie", + "currency-display-name-ars": "Argentyńskie Peso", + "currency-display-name-aud": "Dolar australijski", + "currency-display-name-bbd": "Dolar Barbadosu", + "currency-display-name-bdt": "Taka", + "currency-display-name-bgn": "Bułgarski Lew", + "currency-display-name-bhd": "Bahrański dinar", + "currency-display-name-bnd": "Dolar Brunei", + "currency-display-name-bob": "Boliwijski boliwar", + "currency-display-name-brl": "Brazylijski Real", + "currency-display-name-btn": "Ngultrum", + "currency-display-name-cad": "Dolar Kanadyjski", + "currency-display-name-chf": "Frank szwajcarski", + "currency-display-name-clp": "Peso chilijskie", + "currency-display-name-cny": "Juan chiński Renminbi", + "currency-display-name-cop": "Peso kolumbijskie", + "currency-display-name-crc": "Colon kostarykański", + "currency-display-name-czk": "Korona czeska", + "currency-display-name-dkk": "Korona Duńska", + "currency-display-name-dop": "Peso dominikańskie", + "currency-display-name-egp": "Funt Egipski", + "currency-display-name-etb": "Birr etiopski", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Funt brytyjski", + "currency-display-name-gel": "Gruzińskie Lari", + "currency-display-name-ghs": "Cedi ghańskie", + "currency-display-name-hkd": "Dolar Hongkoński", + "currency-display-name-hrk": "Kuna chorwacka", + "currency-display-name-huf": "Forint węgierski", + "currency-display-name-idr": "Rupia indonezyjska", + "currency-display-name-ils": "Szekel izraelski", + "currency-display-name-inr": "Indyjska Rupia", + "currency-display-name-isk": "Korona islandzka", + "currency-display-name-jmd": "Dolar Jamajski", + "currency-display-name-jpy": "Jen japoński", + "currency-display-name-kes": "Kenijski Szyling", + "currency-display-name-krw": "Południowokoreański Won", + "currency-display-name-kwd": "Dinar kuwejcki", + "currency-display-name-kzt": "Kazachstański Tenge", + "currency-display-name-lkr": "Rupia lankijska", + "currency-display-name-mad": "Dirham marokański", + "currency-display-name-mdl": "Mołdawski Lej", + "currency-display-name-mur": "Rupia maurytyjska", + "currency-display-name-mwk": "Kwacha malawijska", + "currency-display-name-mxn": "Meksykańskie Peso", + "currency-display-name-myr": "Malezyjski Ringgit", + "currency-display-name-mzn": "Mozambijski Metikal", + "currency-display-name-nad": "Dolar Namibijski", + "currency-display-name-ngn": "Nigeryjska Naira", + "currency-display-name-nok": "Korona Norweska", + "currency-display-name-npr": "Nepalska Rupia", + "currency-display-name-nzd": "Dolar nowozelandzki", + "currency-display-name-omr": "Rial Omański", + "currency-display-name-pen": "Sol peruwiański", + "currency-display-name-pgk": "Kina Papui-Nowej Gwinei", + "currency-display-name-php": "Peso filipińskie", + "currency-display-name-pkr": "Pakistańska Rupia", + "currency-display-name-pln": "Polski złoty", + "currency-display-name-pyg": "Paragwajskie Guarani", + "currency-display-name-qar": "Rial Katarski", + "currency-display-name-ron": "Rumuński Lej", + "currency-display-name-rsd": "Serbski dinar", + "currency-display-name-rub": "Rosyjski Rubel", + "currency-display-name-sar": "Rial Saudyjski", + "currency-display-name-sek": "Korona szwedzka", + "currency-display-name-sgd": "Dolar singapurski", + "currency-display-name-thb": "Baht tajlandzki", + "currency-display-name-try": "Lira Turecka", + "currency-display-name-ttd": "Dolar Trynidadu i Tobago", + "currency-display-name-twd": "Dolar Tajwański", + "currency-display-name-tzs": "Tanzański Szyling", + "currency-display-name-uah": "Hrywna ukraińska", + "currency-display-name-ugx": "Ugandyjski Szyling", + "currency-display-name-usd": "Dolar amerykański", + "currency-display-name-uyu": "Urugwajskie Peso", + "currency-display-name-vef": "Boliwar Wenezuelski", + "currency-display-name-vnd": "Wietnam Dong", + "currency-display-name-zar": "Południowoafrykański Rand", + "current": "Obecne", + "current-average": "Aktualna średnia", + "current-average-tip": "Aktualny średni napiwek", + "current-base": "Aktualna baza", + "current-base-fee": "Aktualna opłata podstawowa", + "current-minimum-tip": "Aktualna minimalny napiwek", + "current-network": "Obecna sieć", + "current-password": "Aktualne hasło", + "current-pin": "Wprowadź 6-cyfrowy kod dostępu", + "current-pin-description": "Wprowadź swój 6-cyfrowy kod dostępu, aby kontynuować.", + "custom": "Niestandardowe", + "custom-networks": "Niestandardowe ustawienia sieci", + "custom-node": "Używasz niestandardowego punktu końcowego RPC. Twoja historia lokalnych przelewów może być niekompletna.", + "custom-seed-phrase": "Nieprawidłowa fraza odzyskiwania (Seed)", + "custom-seed-phrase-text-1": "Ta fraza odzyskiwania (Seed) nie pasuje do naszego obsługiwanego słownika. Sprawdź, czy nie ma błędnie wpisanych słów.", + "dapp": "dApp", + "dapp-starter-pack-accept": "Zaakceptuj i otwórz", + "dapp-starter-pack-description": "Oto trochę kryptowalut na dobry początek! Użyj ich, aby kupić naklejki, zmienić nazwę ENS lub korzystać ze zdecentralizowanych aplikacji", + "dapp-starter-pack-title": "Pakiet startowy", + "dapp-would-like-to-connect-wallet": "chciałby połączyć się z", + "dapps": "dAppsy", + "dapps-permissions": "Uprawnienia zdecentralizowanych aplikacji (dApps)", + "dark": "Ciemny", + "data": "Dane", + "data-collected": "Zebrane dane", + "data-collected-subtitle": "Poniższa tabela pokazuje dokładne dane, które są przechowywane i zostaną wysłane. Dane są sprawdzane zgodnie z zasadami publicznymi, aby zapewnić, że nie są wysyłane żadne dane wrażliwe. Nie ufaj, zweryfikuj.", + "data-syncing": "Synchronizacja danych", + "database-reset-content": "Czaty, kontakty i ustawienia zostały usunięte. Możesz używać swojego konta za pomocą karty Keycard", + "database-reset-title": "Reset bazy danych", + "database-reset-warning": "Baza danych zostanie zresetowana. Czaty, kontakty i ustawienia zostaną usunięte", + "datetime-ago": "temu", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "dzień", + "few": "dni", + "many": "dni", + "other": "dni" + }, + "datetime-day-short": { + "one": "D", + "few": "D", + "many": "D", + "other": "D" + }, + "datetime-hour": { + "one": "godzina", + "few": "godzin", + "many": "godzin", + "other": "godzin(y)" + }, + "datetime-hour-short": { + "one": "G", + "few": "G", + "many": "G", + "other": "G" + }, + "datetime-minute": { + "one": "minuta", + "few": "minut", + "many": "minut", + "other": "minut(y)" + }, + "datetime-minute-short": { + "one": "M", + "few": "M", + "many": "M", + "other": "M" + }, + "datetime-second": { + "one": "sekunda", + "few": "sekund", + "many": "sekund", + "other": "sekund(y)" + }, + "datetime-second-short": { + "one": "S", + "few": "S", + "many": "S", + "other": "S" + }, + "datetime-today": "dzisiaj", + "datetime-yesterday": "wczoraj", + "decimals": "Liczby po przecinku", + "decline": "Odrzuć", + "decryption-failed-content": "Nie udało się odszyfrować Twoich danych. Może być konieczne usunięcie starych danych i utworzenie nowego konta. Naciśnij 'Zastosuj', aby wymazać dane lub wybierz 'Anuluj' aby spróbować ponownie", + "default": "Domyślne", + "default-assets": "Domyślne ERC20 i ERC721", + "default-sync-period": "Synchronizuj historię dla", + "delete": "Usuń", + "delete-account": "Usuń konto", + "delete-and-leave-group": "Usuń i opuść grupę", + "delete-bootnode": "Usuń bootnode", + "delete-bootnode-are-you-sure": "Czy na pewno chcesz usunąć ten bootnode?", + "delete-bootnode-title": "Usuń bootnode", + "delete-category-confirmation": "Czy na pewno chcesz usunąć tę kategorię?", + "close-chat": "Usuń czat", + "close-chat-confirmation": "Czy na pewno chcesz usunąć ten czat?", + "delete-confirmation": "Usunąć?", + "delete-keys-keycard": "Usuń klucze z karty Keycard", + "delete-mailserver": "Usuń węzeł Status", + "delete-mailserver-are-you-sure": "Czy na pewno chcesz usunąć ten węzeł Status?", + "delete-mailserver-title": "Usuń węzeł Status", + "delete-message": "Usuń wiadomość", + "delete-my-account": "Usuń moje konto", + "delete-my-profile": "Usuń mój profil", + "delete-network-confirmation": "Czy na pewno chcesz usunąć tę sieć?", + "delete-network-error": "Przed usunięciem tej sieci proszę połączyć się z inną siecią", + "delete-network-title": "Usunąć sieć?", + "delete-node": "Usuń węzeł", + "delete-node-are-you-sure": "Czy na pewno chcesz usunąć ten węzeł?", + "delete-node-title": "Usuń węzeł", + "delete-profile": "Usuń profil", + "delete-profile-warning": "Ostrzeżenie: jeśli nie zapisałeś swojej frazy Seed, utracisz dostęp do swoich funduszy po usunięciu swojego profilu", + "deny": "Odmów", + "derivation-path": "Ścieżka wyprowadzenia", + "describe-channel": "Opisz kanał", + "description": "Opis", + "dev-mode": "Tryb deweloperski", + "dev-mode-settings": "Ustawienia trybu deweloperskiego", + "device-syncing": "Synchronizacja urządzeń", + "devices": "Urządzenia", + "disable": "wyłącz", + "disable-all": "Wyłącz wszystkie", + "disable-later-in-settings": "Możesz wyłączyć to później w Ustawieniach", + "disabled": "Wyłączone", + "disconnected": "Łączenie z peerami…", + "discover": "Odkryj", + "dismiss": "Odrzuć", + "display-collectibles": "Eksponuj kolekcje", + "done": "Zrobione", + "dont-ask": "Nie pytaj mnie ponownie", + "edit": "Edytuj", + "edit-channel-title": "Edytuj kanał", + "edit-chats": "Edytuj czaty", + "edit-community": "Edytuj społeczność", + "edit-favourite": "Edytuj ulubione", + "edit-group": "Edytuj grupę", + "edit-profile": "Edytuj profil", + "edited": "Edytowane", + "editing-message": "Edycja wiadomości", + "empty-activity-center": "Twoje powiadomienia z czatu\npojawią się tutaj", + "empty-chat-description": "W tym czacie nie ma \njeszcze wiadomości", + "empty-chat-description-community": "Było tu cicho przez ostatnie {{quiet-hours}}.", + "empty-chat-description-one-to-one": "Wszystkie wysyłane tutaj wiadomości są szyfrowane i mogą być odczytane tylko przez Ciebie i ", + "empty-chat-description-public": "Przez ostatnie {{quiet-hours}} było tu cicho. Rozpocznij rozmowę lub ", + "empty-chat-description-public-share-this": "udostępnij ten czat.", + "empty-keycard-required": "Wymaga pustej karty Keycard", + "empty-pending-invitations-descr": "Osoby, które chcą dołączyć do grupy\npoprzez link zapraszający pojawią się tutaj", + "empty-tab": "Pusta karta", + "enable": "Włącz", + "enable-all": "Włącz wszystkie", + "enable-link-previews": "Włączyć podgląd linków na czacie?", + "encrypt-with-password": "Szyfruj za pomocą hasła", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Dodaj nazwę użytkownika", + "ens-agree-to": "Zgadzam się na ", + "ens-chat-settings": "Ustawienia czatu", + "ens-custom-domain": "Domena niestandardowa", + "ens-custom-username-hints": "Wpisz całą nazwę użytkownika, w tym domenę niestandardową, taką jak nazwaużytkownika.domena.eth", + "ens-custom-username-taken": "Nazwa użytkownika nie należy do Ciebie :(", + "ens-deposit": "Depozyt", + "ens-dismiss-message": "Kliknij tutaj, aby odrzucić", + "ens-displayed-with": "Twoje wiadomości są wyświetlane innym osobom za pomocą", + "ens-get-name": "Uzyskaj uniwersalną nazwę użytkownika", + "ens-got-it": "Ok, rozumiem", + "ens-locked": "Nazwa użytkownika zablokowana. Nie będzie można jej uwolnić do {{date}}", + "ens-name-content": "Niestandardowa nazwa dla twojego klucza czatu, którą możesz zarejestrować za pomocą usługi Ethereum Name Service, w skrócie ENS. Nazwy ENS to zdecentralizowane nazwy użytkowników.", + "ens-name-not-found": "Nie można rozpoznać nazwy ENS", + "ens-name-title": "Nazwa ENS", + "ens-network-restriction": "Dostępne tylko na Mainnet", + "ens-no-usernames": "Nie masz podłączonej żadnej nazwy użytkownika", + "ens-powered-by": "Napędzane przez Ethereum Name Services", + "ens-primary-username": "Podstawowa nazwa użytkownika", + "ens-register": "Zarejestruj", + "ens-registration-failed": "Aby zarejestrować nazwę użytkownika, proszę spróbować ponownie.", + "ens-registration-failed-title": "Transakcja nie powiodła się", + "ens-registration-failure": "Rejestracja nie powiodła się", + "ens-registration-in-progress": "Rejestracja w toku ...", + "ens-release-username": "Zwolnij nazwę użytkownika", + "ens-remove-hints": "Usunięcie spowoduje odłączenie nazwy użytkownika od twojego klucza.", + "ens-remove-username": "Usuń nazwę użytkownika", + "ens-saved": "jest teraz połączony z twoim kluczem czatu i może być używany w aplikacji Status.", + "ens-saved-title": "Nazwa użytkownika została dodana", + "ens-show-username": "Pokaż moją nazwę użytkownika ENS w czatach", + "ens-terms-header": "Warunki rejestracji nazwy.", + "ens-terms-point-1": "Fundusze są zdeponowane na 1 rok. Twoje SNT zostaną zablokowane, ale nie zostaną wydane.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Rejestr ENS).", + "ens-terms-point-2": "Po upływie 1 roku możesz zwolnić nazwę i odzyskać depozyt lub nie podejmować żadnych działań, aby ją zachować.", + "ens-terms-point-3": "Jeśli warunki umowy ulegną zmianie — np. Status dokona aktualizacji umowy — użytkownik ma prawo do zwolnienia nazwy użytkownika bez względu na czas posiadania.", + "ens-terms-point-4": "Kontroler kontraktu nie ma dostępu do Twoich zdeponowanych środków. Mogą być one jedynie przeniesione z powrotem na adres, który je wysłał.", + "ens-terms-point-5": "Twój adres lub adresy będą publicznie powiązane z twoją nazwą ENS.", + "ens-terms-point-6": "Nazwy użytkowników są tworzone jako węzły subdomeny stateofus.eth i podlegają warunkom inteligentnego kontraktu (smart contract) ENS.", + "ens-terms-point-7": "Upoważniasz kontrakt do przeniesienia SNT w Twoim imieniu. Może to nastąpić tylko wtedy, gdy zatwierdzisz transakcję, aby autoryzować przelew.", + "ens-terms-point-8": "Warunki te są gwarantowane przez logikę inteligentnego kontraktu w adresach:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar) ", + "ens-terms-registration": "Warunki rejestracji nazwy.", + "ens-test-message": "Hej", + "ens-transaction-pending": "Transakcja w toku...", + "ens-understand": "Rozumiem, że mój adres portfela będzie publicznie połączony z moją nazwą użytkownika.", + "ens-username": "Nazwa użytkownika ENS", + "ens-username-already-added": "Nazwa użytkownika jest już połączona z twoim kluczem czatu i może być używana wewnątrz aplikacji Status.", + "ens-username-available": "✓ Nazwa użytkownika jest dostępna!", + "ens-username-connected": "Ta nazwa użytkownika należy do Ciebie i jest połączona z Twoim kluczem czatu.", + "ens-username-connected-continue": "Kontynuuj ustawianie opcji `Pokazuj moją nazwę użytkownika ENS w czatach`.", + "ens-username-connected-with-different-key": "Kontynuacja będzie wymagać transakcji w celu połączenia nazwy użytkownika z aktualnym kluczem czatu.", + "ens-username-connection-confirmation": "{{username}} zostanie połączony po zakończeniu transakcji.", + "ens-username-hints": "Co najmniej 4 znaki. Tylko litery łacińskie, cyfry i małe litery.", + "ens-username-invalid": "Tylko cyfry i litery.", + "ens-username-owned": "✓ Nazwa użytkownika jest twoją własnością.", + "ens-username-owned-continue": "Kontynuacja spowoduje połączenie tej nazwy użytkownika z twoim kluczem czatu.", + "ens-username-registration-confirmation": "Świetnie! Zostaniesz właścicielem {{username}} po zakończeniu transakcji.", + "ens-username-taken": "Nazwa użytkownika jest już zajęta :(", + "ens-username-you-can-follow-progress": "Możesz śledzić postęp w sekcji „Historia Transakcji” w swoim portfelu.", + "ens-usernames": "Nazwy użytkowników ENS", + "ens-usernames-details": "Zarejestruj uniwersalną nazwę użytkownika, aby być łatwo rozpoznawalnym przez innych użytkowników", + "ens-want-custom-domain": "Jestem właścicielem nazwy w innej domenie", + "ens-want-domain": "Chcę mieć domenę stateofus.eth", + "ens-welcome-hints": "Nazwy ENS przekształcają szalenie długie adresy w unikalne nazwy użytkowników.", + "ens-welcome-point-customize": "Nazwa ENS może zastąpić twoje losowe 3-słowne imię na czacie. Bądź @własnanazwa zamiast {{name}}.", + "ens-welcome-point-customize-title": "Dostosuj nazwę swojego czatu", + "ens-welcome-point-receive": "Inni mogą wysłać Ci fundusze za pośrednictwem czatu w jednym prostym kroku.", + "ens-welcome-point-receive-title": "Odbieraj transakcje na czacie", + "ens-welcome-point-register": "Zarejestruj się raz, aby zachować nazwę na zawsze. Po 1 roku możesz zwolnić nazwę i odzyskać SNT.", + "ens-welcome-point-register-title": "10 SNT do rejestracji", + "ens-welcome-point-simplify": "Możesz otrzymać środki na swoją łatwą do udostępnienia nazwę ENS zamiast na swój szesnastkowy hash (0x ...).", + "ens-welcome-point-simplify-title": "Uprość swój adres ETH", + "ens-welcome-point-verify": "W kolejnych krokach możesz zweryfikować i dodać dowolne posiadane przez Ciebie nazwy użytkowników.", + "ens-welcome-point-verify-title": "Posiadasz już nazwę użytkownika?", + "ens-your-username": "Twoja nazwa użytkownika", + "ens-your-usernames": "Twoje nazwy użytkownika", + "ens-your-your-name": "Twoja nazwa ENS", + "enter-12-words": "Wpisz 12 słów swojej frazy odzyskiwania (Seed), oddzielonych pojedynczymi spacjami.", + "enter-a-private-key": "Wprowadź klucz prywatny", + "enter-a-seed-phrase": "Wpisz frazę odzyskiwania (Seed)", + "enter-address": "Wprowadź adres", + "enter-contact-code": "ENS (vitalik94) lub klucz czatu (0x04...)", + "enter-pair-code": "Wprowadź swój kod parowania", + "enter-pair-code-description": "Kod parowania może być ustawiony z już sparowanego klienta Status.", + "enter-password": "Wprowadź hasło", + "enter-password-migration-prompt": "Wprowadź swoje hasło, aby przenieść kontakty, czaty i ustawienia wraz z Twoimi kluczami", + "enter-pin": "Wprowadź 6-cyfrowy kod dostępu", + "enter-puk-code": "Wprowadź kod PUK.", + "enter-puk-code-description": "6-cyfrowy kod dostępu został zablokowany.\n Wprowadź kod PUK, aby odblokować kod dostępu.", + "enter-recipient-address-or-username": "Wprowadź adres lub nazwę użytkownika odbiorcy", + "enter-seed-phrase": "Wpisz frazę odzyskiwania (Seed)", + "enter-url": "Wprowadź adres URL", + "enter-user-pk": "Wprowadź klucz publiczny użytkownika", + "enter-watch-account-address": "Zeskanuj kod QR\nlub\nwprowadź adres do obejrzenia", + "enter-word": "Wprowadź słowo", + "enter-your-code": "Wprowadź swój 6-cyfrowy kod dostępu.", + "enter-your-password": "Wprowadź swoje hasło.", + "error": "Błąd", + "error-unable-to-get-balance": "Nie można uzyskać historii transakcji", + "error-unable-to-get-prices": "Błąd konwersji waluty. Odśwież ekran, aby spróbować ponownie.", + "error-unable-to-get-token-balance": "Nie można uzyskać historii transakcji", + "errors": "Błędy", + "eth": "ETH", + "ethereum-account": "Konto Ethereum", + "ethereum-address": "Adres Ethereum", + "ethereum-node-started-incorrectly-description": "Węzeł Ethereum został uruchomiony z nieprawidłową konfiguracją, aplikacja zostanie zatrzymana w celu przywrócenia stanu pierwotnego. Skonfigurowany identyfikator sieci = {{network-id}}, rzeczywisty = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Węzeł Ethereum został uruchomiony nieprawidłowo", + "etherscan-lookup": "Wyszukaj na Etherscan", + "everyone": "Wszystkich", + "expand-all": "Rozwiń wszystko", + "export-account": "Eksportuj konto", + "export-key": "Eksportuj klucz prywatny", + "external-storage-denied": "Odmowa dostępu do zewnętrznej pamięci masowej", + "failed": "Niepowodzenie", + "faq": "Często zadawane pytania", + "fast": "Szybki", + "favourite": "Ulubione", + "favourite-description": "Twoje ulubione strony pojawią się tutaj", + "favourites": "Ulubione", + "favourites-empty": "Adresy dodane do ulubionych pojawią się tutaj", + "fee-cap": "Pułap opłat", + "fee-explanation": "Maksymalna łączna cena za transakcję. Jeżeli opłata bazowa bloku przekroczy tę kwotę, zostanie ona uwzględniona w kolejnym bloku z niższą opłatą bazową.", + "fee-options": "Proponowane opcje opłat", + "fetch-community": "Społeczność pobierania", + "fetch-messages": "↓ Pobierz wiadomości", + "fetch-timeline": "↓ Pobierz", + "fetching-community": "Pobieranie społeczności...", + "find": "Znajdź", + "finish": "Zakończ", + "finishing-card-setup": "Kończę konfigurację karty", + "fleet": "Fleet", + "fleet-settings": "Ustawienia fleet", + "follow": "Śledź", + "follow-your-interests": "Dołącz do publicznego czatu i poznaj nowych ludzi", + "free": "↓ Darmowy", + "from": "Od", + "gas-amount-limit": "Limit ilości gazu", + "gas-limit": "Limit Gasu", + "gas-price": "Cena gasu", + "gas-used": "Użyty gas", + "generate-a-key": "Generuj klucze", + "generate-a-new-account": "Wygeneruj konto", + "generate-a-new-key": "Wygeneruj nowy klucz", + "generate-account": "Generuj klucze", + "generate-an-account": "Wygeneruj konto", + "generate-new-key": "Generuj klucze", + "generating-codes-for-pairing": "> Pobieranie oprogramowania produktu na kartę\n > Generowanie kodów odblokowujących i parujących", + "generating-keys": "Generowanie kluczy ...", + "generating-mnemonic": "Generowanie frazy odzyskiwania (Seed)", + "get-a-keycard": "Zdobądź kartę Keycard", + "get-started": "Rozpocznij", + "get-status-at": "Pobierz Status na http:\/\/status.im", + "get-stickers": "Pobierz naklejki", + "give-a-short-description": "Podaj krótki opis", + "give-a-short-description-community": "Podaj krótki opis", + "give-permissions-camera": "Daj pozwolenie\nna dostęp do kamery", + "glossary": "Słowniczek", + "go-to-settings": "Przejdź do Ustawień", + "got-it": "Rozumiem", + "grant-face-id-permissions": "Aby nadać wymagane uprawnienia Face ID, należy przejść do ustawień systemowych i upewnić się, że opcja Status > Face ID jest zaznaczona", + "group-chat": "Czat grupowy", + "group-chat-admin": "Administrator", + "group-chat-admin-added": "**{{member}}** został administratorem", + "group-chat-all-contacts-invited": "Wszystkie Twoje kontakty są już w grupie", + "group-chat-created": "**{{member}}** utworzył grupę **{{name}}**.", + "group-chat-decline-invitation": "Odrzuć zaproszenie", + "group-chat-member-added": "**{{member}}** został zaproszony", + "group-chat-member-joined": "**{{member}}** dołączył do grupy", + "group-chat-member-removed": "** {{member}} ** opuścił grupę", + "group-chat-members-count": "{{selected}} \/ {{max}} członków", + "group-chat-name-changed": "{{member}} zmienił nazwę grupy na {{name}}", + "group-chat-no-contacts": "Nie masz jeszcze żadnych kontaktów.\nZaproś swoich znajomych do rozpoczęcia czatu", + "group-info": "Informacje o grupie", + "group-invite": "Zaproszenie do grupy", + "group-invite-link": "Link z zaproszeniem do grupy", + "group-membership-request": "Prośba o członkostwo w grupie", + "gwei": "Gwei", + "has-permissions": "ma pozwolenie na dostęp", + "hash": "Hash", + "help": "pomoc", + "help-capitalized": "Pomoc", + "help-center": "Centrum pomocy", + "help-improve-status": "Pomóż ulepszyć Status", + "hide": "Ukryj", + "hide-content-when-switching-apps": "Zablokuj zrzuty ekranu", + "hide-content-when-switching-apps-ios": "Ukryj podgląd", + "history": "Historia", + "history-nodes": "Węzły Status", + "hold-card": "przytrzymaj kartę z tyłu telefonu", + "home": "Start", + "hooks": "Hooks", + "how-it-works": "Jak to działa?", + "http-gateway-error": "Ups, żądanie się nie powiodło!", + "identifier": "Identyfikator", + "image": "Obraz", + "image-remove-current": "Usuń obecne zdjęcie", + "image-source-gallery": "Wybierz z galerii", + "image-source-make-photo": "Przechwyć", + "image-source-title": "Edytuj zdjęcie", + "import": "Importuj", + "import-community": "Importuj społeczność", + "import-community-title": "Importuj społeczność", + "in-contacts": "W kontaktach", + "include": "Dołącz", + "incoming": "Przychodzące", + "incoming-transaction": "Transakcja przychodząca", + "incorrect-code": [ + "str", + "Przepraszamy, kod jest nieprawidłowy. Prosimy wprowadzić kod ponownie" + ], + "increase-gas": "Zwiększ Gas", + "initialization": "Rozpoczynanie", + "install": "↓ Zainstaluj", + "intro-message1": "Witamy w Status!\nKliknij tę wiadomość, aby ustawić hasło i rozpocząć!", + "intro-privacy-policy-note1": "Status nie zbiera ani nie czerpie korzyści z twoich danych osobowych. Kontynuując, zgadzasz się z", + "intro-privacy-policy-note2": "Polityka prywatności", + "intro-text": "Status jest twoją bramą do zdecentralizowanej sieci.", + "intro-text1": "Rozmawiaj za pośrednictwem sieci typu peer-to-peer, w której wiadomości nie mogą być cenzurowane ani hakowane", + "intro-text2": "Wysyłaj i odbieraj cyfrowe aktywa w dowolnym miejscu na świecie — bez konieczności posiadania konta bankowego", + "intro-text3": "Odkrywaj gry, giełdy i sieci społecznościowe, w których tylko Ty jesteś właścicielem swoich danych", + "intro-title1": "Prawdziwie prywatna komunikacja", + "intro-title2": "Bezpieczny portfel kryptowalutowy", + "intro-title3": "Zdecentralizowane aplikacje", + "intro-wizard-text1": "Zestaw kluczy kontroluje Twoje konto. Twoje klucze są przechowywane w telefonie, więc tylko ty możesz ich używać", + "intro-wizard-text2": "Jeden klucz jest przeznaczony do czatu. Dostarczany jest z czytelną nazwą, której nie można zmienić.", + "intro-wizard-text3": "Jeśli posiadasz kartę Keycard, przechowuj w niej klucze dla większego bezpieczeństwa.", + "intro-wizard-text4": "Zabezpiecz i zaszyfruj swoje klucze", + "intro-wizard-text6": "Status będzie Cię powiadamiał o nowych wiadomościach. Możesz później edytować swoje preferencje dotyczące powiadomień w ustawieniach.", + "intro-wizard-title-alt4": "Utwórz hasło", + "intro-wizard-title-alt5": "Potwierdź swoje hasło", + "intro-wizard-title1": "Zdobądź swoje klucze", + "intro-wizard-title2": "Wybierz nazwę czatu", + "intro-wizard-title3": "Wybierz miejsce przechowywania klucza", + "intro-wizard-title4": "Utwórz 6-cyfrowy kod dostępu", + "intro-wizard-title5": "Potwierdź kod dostępu", + "intro-wizard-title6": "Włącz powiadomienia", + "introduce-yourself": "Przedstaw się za pomocą krótkiego opisu", + "invalid-address-qr-code": "Zeskanowany kod QR nie zawiera prawidłowego adresu", + "invalid-format": "Niepoprawny format\nMusi być {{format}}", + "invalid-key-confirm": "Zastosuj", + "invalid-key-content": "Baza danych nie może zostać zaszyfrowana, ponieważ plik jest uszkodzony. Twoje środki i klucz czatu są bezpieczne. Inne dane, takie jak czaty i kontakty, nie mogą zostać przywrócone. Przycisk '{{erase-multiaccounts-data-button-text}}' usunie wszystkie inne dane i pozwoli Ci na dostęp do Twoich środków i wysyłanie wiadomości.", + "invalid-number": "Nieprawidłowy numer", + "invalid-pairing-password": "Nieprawidłowe hasło parowania", + "invalid-public-chat-topic": "Nieprawidłowy temat czatu publicznego", + "invalid-range": "Nieprawidłowy format musi zawierać się między {{min}} a {{max}}", + "invalid-username-or-key": "Nieprawidłowa nazwa użytkownika lub klucz czatu", + "invite": "Zaproś", + "invite-button": "Zaproś", + "invite-chat-accept": "Przyjmij", + "invite-chat-accept-join": "Zaakceptuj i dołącz", + "invite-chat-intro": "Zostałeś polecony przez znajomego, aby dołączyć do Status. Oto trochę kryptowalut na dobry początek! Użyj Ich, aby zarejestrować nazwę ENS lub kupić pakiet naklejek.", + "invite-chat-name": "Polecenie Znajomego", + "invite-chat-pending": "Oczekujące", + "invite-chat-rule": "Akceptacja wynagrodzi również znajomego premią w postaci bonusu kryptowalutowego", + "invite-chat-starter-pack": "Pakiet startowy", + "invite-friends": "Zaproś znajomych", + "invite-instruction-fifth": "Bonus za polecenie możesz zrealizować w dowolnym momencie.", + "invite-instruction-first": "Wysyłasz znajomemu unikalny link z zaproszeniem do pobrania i dołączenia do platformy Status", + "invite-instruction-fourth": "Otrzymasz premię za polecenie, a twój znajomy pakiet startowy", + "invite-instruction-second": "Twój znajomy pobiera Status i tworzy konto (na Androidzie)", + "invite-instruction-third": "Czat z Twoim znajomym rozpoczął się, w którym to potwierdzi on Twoje polecenie", + "invite-people": "Zaproś ludzi", + "invite-privacy-policy-public": "Zainstalowałeś Status poprzez link polecający. Dołączając do tego czatu, przypisujesz się do osoby polecającej i zgadzasz się na", + "invite-privacy-policy1": "Akceptując wyrażasz zgodę na program poleceń", + "invite-privacy-policy2": "Warunki i zasady.", + "invite-public-chat-home": "Zaproszenie polecające", + "invite-public-chat-intro": "Oto trochę kryptowalut na dobry początek! Użyj ich, aby zarejestrować nazwę ENS lub kupić pakiet naklejek", + "invite-receive-account": "Konto, aby otrzymać premię za polecenie", + "invite-reward": "Zarabiaj kryptowaluty za każdego znajomego, którego zaprosisz!", + "invite-reward-friend": "Znajomy: ", + "invite-reward-friend-description": "Twój znajomy otrzyma Pakiet Startowy składający się z {{reward}} na początek", + "invite-reward-friend-name": "Pakiet startowy", + "invite-reward-you": "Ty:", + "invite-reward-you-description": "Zaproś znajomego i otrzymaj {{reward}} jako bonus za polecenie. Użyj go, aby uzyskać naklejki, nazwę ENS i korzystać ze zdecentralizowanych aplikacji", + "invite-reward-you-name": "Premia za polecenie", + "invite-select-account": "Wybierz konto, aby otrzymać premię za polecenie", + "invite-warning": "Ta promocja jest ważna tylko dla użytkowników urządzeń z systemem Android, którzy nie są mieszkańcami USA. Znajomy musi potwierdzić polecenie w ciągu 7 dni.", + "invited": "zaproszeni", + "join": "Dołącz", + "join-a-community": "lub dołącz do społeczności", + "join-group-chat": "Dołącz do grupy", + "join-group-chat-description": "{{username}}zaprosił Cię abyś dołączył do grupy {{group-name}}", + "join-me": "Hej, dołącz do mnie na Status: {{url}}", + "join-new-private-chat": "Rozpocznij nowy prywatny czat", + "join-new-public-chat": "Dołącz do publicznego czatu", + "joined": "Dołączono", + "joined-group-chat-description": "Dołączyłeś do {{group-name}} z zaproszenia od {{username}}.", + "key": "Klucz", + "key-managment": "Zarządzanie kluczami", + "key-on-device": "Klucz prywatny jest zapisany na tym urządzeniu", + "keycard": "Karta Keycard", + "keycard-access-reset": "Dostęp do karty jest zresetowany", + "keycard-applet-install-instructions": "Aby zainstalować aplet, postępuj zgodnie z instrukcjami na https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Spróbuj przesunąć kartę, aby znaleźć czytnik NFC w swoim urządzeniu", + "keycard-awaiting-title": "Nadal szukam...", + "keycard-backup": "Utwórz kopię zapasową karty Keycard", + "keycard-backup-success-body": "Karta zapasowa została utworzona pomyślnie. Możesz jej teraz używać na swoim koncie tak samo, jak karty głównej.", + "keycard-backup-success-title": "Wykonanie kopii zapasowej zakończone sukcesem", + "keycard-blocked": "Karta kluczy została zablokowana.\nAby kontynuować korzystanie z karty, musisz zresetować kartę.", + "keycard-can-use-with-new-passcode": "Możesz używać tej karty z nowym kodem dostępu", + "keycard-cancel-setup-text": "Spowoduje to anulowanie konfiguracji karty kluczy. Zdecydowanie zaleca się zakończenie konfiguracji w celu użycia karty Keycard. Czy naprawdę chcesz anulować?", + "keycard-cancel-setup-title": "Niebezpieczna operacja", + "keycard-connected-description": "Spróbuj trzymać kartę nieruchomo", + "keycard-connected-title": "Połączono", + "keycard-desc": "Posiadasz kartę Keycard? Przechowuj na niej swoje klucze; będzie Ci ona potrzebna do transakcji", + "keycard-dont-ask-card": "Nie pytaj o kartę, aby się zalogować", + "keycard-enter-new-passcode": "Wprowadź nowy kod dostępu {{step}}\/2", + "keycard-error-description": "Podłącz ponownie kartę, aby kontynuować", + "keycard-error-title": "Utracono połączenie", + "keycard-factory-reset": "Przywróć kartę do ustawień fabrycznych", + "keycard-factory-reset-text": "Wykonanie tej czynności spowoduje usunięcie wszystkich fraz mnemotechnicznych zapisanych na karcie. Upewnij się, że masz kopię zapasową wyrażenia mnemotechnicznego, którego używałeś z tą kartą.", + "keycard-factory-reset-title": "Czy na pewno chcesz przywrócić ustawienia fabryczne?", + "keycard-free-pairing-slots": "Keycard ma {{n}} wolne miejsca na parowanie", + "keycard-has-multiaccount-on-it": "Ta karta jest pełna. Każda karta może pomieścić jedną główną parę kluczy", + "keycard-init-description": "Przyłóż kartę do tylnej części telefonu, aby kontynuować", + "keycard-init-title": "Szukam kart ...", + "keycard-is-blocked-details": "Nie możesz już używać tej karty do uzyskiwania dostępu lub zalogowania się na to konto. Było zbyt wiele nieudanych prób wprowadzenia kodu dostępu i kodu PUK.", + "keycard-is-blocked-instructions": "Aby uzyskać dostęp do swojego konta, musisz zresetować kartę do ustawień fabrycznych. Naciśnij poniższy przycisk, aby rozpocząć procedurę, będziesz potrzebować swojego wyrażenia mnemomicznego.", + "keycard-is-blocked-title": "Karta Keycard jest zablokowana", + "keycard-is-frozen-details": "Aby chronić swoje zasoby, karta została zawieszona. Zresetuj kartę, aby ją odblokować i móc wysyłać transakcje. Możesz to zrobić za pomocą PUK lub wyrażenia mnemomicznego.", + "keycard-is-frozen-factory-reset": "Zresetuj regułą mnemotechniczną,", + "keycard-is-frozen-reset": "Zresetuj za pomocą PUK", + "keycard-is-frozen-title": "Karta Keycard jest zamrożona", + "keycard-onboarding-finishing-header": "Kończę", + "keycard-onboarding-intro-header": "Przechowuj swoje klucze na karcie Keycard", + "keycard-onboarding-intro-text": "Przygotuj się, może to zająć kilka minut, ale ważne jest to, abyś zabezpieczył swoje konto", + "keycard-onboarding-mnemonic-text": "Będziesz również potrzebował kartki papieru i ołówka, aby zapisać swoje hasło.", + "keycard-onboarding-pairing-header": "Parowanie karty ...", + "keycard-onboarding-pin-text": "Będziesz musiał utworzyć 6-cyfrowy kod dostępu, który będzie używany do ochrony dostępu do Twojej karty Keycard.", + "keycard-onboarding-preparing-header": "Przygotowanie karty ...", + "keycard-onboarding-puk-code-header": "Zapisz kody\n i przechowuj je bezpiecznie", + "keycard-onboarding-recovery-phrase-description": "Potrzebujesz tej frazy odzyskiwania (Seed), aby odzyskać swój klucz. Zapisz ją. Przechowuj ją w bezpiecznym miejscu, poza siecią i poza tym urządzeniem.", + "keycard-onboarding-recovery-phrase-header": "Utwórz kopię zapasową frazy odzyskiwania (Seed)", + "keycard-onboarding-recovery-phrase-text": "Tylko dla twoich oczu. To jest magiczny Seed użyty do wygenerowania Twojego klucza.", + "keycard-onboarding-start-header": "Przytrzymaj kartę z tyłu\n telefonu, aby rozpocząć", + "keycard-onboarding-start-step1": "Utwórz kod dostępu", + "keycard-onboarding-start-step1-text": "Około 1 minuty. Utwórz 6-cyfrowy kod dostępu, aby zaszyfrować swoje klucze", + "keycard-onboarding-start-step2": "Zapisz PUK i kod parowania", + "keycard-onboarding-start-step2-text": "Około 1 minuty. Będziesz potrzebować do tego kartki papieru i długopisu", + "keycard-onboarding-start-step3": "Utwórz kopię zapasową frazy odzyskiwania (Seed)", + "keycard-onboarding-start-step3-text": "Około 1 minuty. Na tym etapie również potrzebna jest kartka papieru i długopis", + "keycard-onboarding-start-text": "I utrzymuj kontakt między kartą a telefonem\n podczas konfiguracji. Konfiguracja zajmie około 4 minut", + "keycard-processing-description": "Spróbuj trzymać kartę nieruchomo", + "keycard-processing-title": "Przetwarzanie...", + "keycard-recover": "karta zgubiona bądź zawieszona?", + "keycard-recover-text": "Jeśli masz swoje wyrażenie mnemoniczne, możesz utworzyć nową kartę Keycard powiązaną z tym kontem. Możesz użyć nowej karty Keycard lub przywrócić ustawienia fabryczne zawieszonej karty.", + "keycard-recover-title": "Czy utworzyć nową kartę dla tego konta?", + "keycard-recovery-intro-button-text": "Rozpocznij odzyskiwanie", + "keycard-recovery-intro-header": "Odzyskaj klucze przechowywane na karcie Keycard", + "keycard-recovery-intro-text": "Jeśli wcześniej generowałeś klucze za pomocą karty Keycard, a teraz chcesz używać tych kluczy na tym urządzeniu", + "keycard-recovery-no-key-header": "Nie ma tu nic do odzyskiwania", + "keycard-recovery-no-key-text": "Twoja karta Keycard nie zawiera żadnego klucza. Aby z niej skorzystać, wygeneruj nowy klucz i wybierz swoją kartę Keycard przechowywać klucz", + "keycard-recovery-phrase-confirm-header": "Potwierdź frazę odzyskiwania (Seed)", + "keycard-recovery-phrase-confirmation-text": "Nie będziesz mieć drugiej szansy! Jeżeli utracisz dostęp, na przykład poprzez utratę karty Keycard, możesz uzyskać dostęp do twoich kluczy jedynie dzięki twojej frazie Seed. Nikt, oprócz Ciebie nie posiada Twojej frazy Seed. Zapisz ją i przechowuj w bezpiecznym miejscu.", + "keycard-recovery-phrase-confirmation-title": "Czy fraza odzyskiwania (Seed) została zapisana?", + "keycard-recovery-success-header": "Twoje klucze zostały\n pomyślnie odzyskane", + "keycard-redeem-title": "Odbierz na", + "keycard-redeem-tx": "Odbierz aktywa", + "keycard-redeem-tx-desc": "Stuknij w kartę, aby podpisać i odebrać aktywa", + "keycard-reset-passcode": "Resetuj kod dostępu.", + "keycard-success-description": "Możesz teraz wyjąć kartę", + "keycard-success-title": "Sukces", + "keycard-unauthorized-operation": "Nie masz uprawnień do wykonania tej operacji.\n Wybierz ważną kartę i spróbuj ponownie.", + "keycard-upsell-subtitle": "Zwiększone bezpieczeństwo i wygoda", + "language": "Język", + "last-backup-performed": "Ostatnia wykonana kopia zapasowa:", + "last-transaction": "Ostatnia transakcja", + "learn-more": "Dowiedz się więcej", + "learn-more-about-keycard": "Dowiedz się więcej o Keycard", + "leave": "Opuść", + "leave-chat": "Opuść czat", + "leave-chat-confirmation": "Historia czatu zostanie usunięta z Twojego urządzenia. Po ponownym dołączeniu nie będziesz w stanie odzyskać żadnej swojej historii.", + "leave-community": "Opuść społeczność", + "leave-confirmation": "Opuść {{chat-name}}", + "leave-group": "Opuść grupę", + "left": "pozostało", + "les-ulc": "LES\/ULC", + "lets-go": "Zaczynamy", + "light": "Jasny", + "limit": "Limit", + "linked-on": "Połączono w dniu {{date}}", + "load-messages-before": "przed {{date}}", + "load-more-messages": "↓ Pobierz więcej wiadomości", + "load-more-timeline": "↓ Pobierz więcej", + "loading": "Ładowanie...", + "local-notifications": "Powiadomienia lokalne", + "lock-app-with": "Zablokuj aplikację za pomocą", + "log-level": "Poziom Log", + "log-level-settings": "Ustawienia poziomu logów", + "logging": "Logowanie", + "logging-enabled": "Logowanie włączone?", + "login-pin-description": "Wprowadź swój 6-cyfrowy kod dostępu, aby odblokować swoje klucze", + "logout": "Wyloguj", + "logout-app-content": "Konto zostanie wylogowane. Po ponownym odblokowaniu będzie używana wybrana sieć.", + "logout-are-you-sure": "Czy na pewno chcesz\nsię wylogować?", + "logout-key-management": "Musisz się wylogować, aby uzyskać dostęp do zarządzania kluczami.", + "logout-title": "Wylogować?", + "looking-for-cards": "Szukam kart...", + "lost-connection": "Utracono połączenie", + "low-tip": "napiwek jest za niski", + "lower-than-average-tip": "niższy niż przeciętny napiwek", + "mail-should-be-configured": "Aplikacja mailowa powinna być skonfigurowana", + "mailserver-address": "Adres węzła Status", + "mailserver-automatic": "Automatyczna selekcja", + "mailserver-automatic-switch-explanation": "Wybierz najszybszy dostępny węzeł dla Status", + "mailserver-connection-error": "Nie udało się połączyć z węzłem Status", + "mailserver-content": "Węzeł w sieci Status, który kieruje i przechowuje wiadomości przez maksymalnie 30 dni.", + "mailserver-details": "Szczegóły węzła Status", + "mailserver-error-content": "Wybrany węzeł Status nie może zostać osiągnięty.", + "mailserver-error-title": "Błąd podczas łączenia się z węzłem Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Wybierz inny węzeł Status", + "mailserver-reconnect": "Nie udało się połączyć z węzłem Status. Kliknij, aby ponowić połączenie", + "mailserver-request-error-content": "Następujący błąd został zwrócony przez węzeł Status: {{error}}", + "mailserver-request-error-status": "Wystąpił błąd podczas pobierania historii, sprawdź logi, aby uzyskać szczegóły", + "mailserver-request-error-title": "Błąd podczas prośby otrzymania węzła Status", + "mailserver-request-retry": "Ponów prośbę", + "mailserver-retry": "Ponów", + "mailserver-title": "Węzeł Status", + "main-currency": "Główna waluta", + "main-networks": "Główne sieci", + "main-wallet": "Portfel główny", + "mainnet-network": "Sieć główna", + "make-admin": "Uczyń administratorem", + "manage-keys-and-storage": "Zarządzanie kluczami i przechowywaniem", + "mark-all-read": "Oznacz wszystkie jako przeczytane", + "master-account": "Konto główne", + "max-fee": "Maksymalna opłata", + "max-priority-fee": "Maksymalna opłata priorytetowa", + "maximum-fee": "Maksymalna opłata", + "maximum-fee-desc": "Maksymalna łączna cena za transakcję. Jeśli aktualna opłata bazowa za blok przekracza tę wartość, Twoja transakcja zostanie uwzględniona w kolejnym bloku z niższą opłatą bazową.", + "maybe-later": "Może później", + "member-ban": "Zablokuj członka", + "member-kick": "Wyrzuć członka", + "members": { + "one": "1 użytkownik", + "few": "użytkowników", + "many": "użytkowników", + "other": "Użytkownicy: {{count}}" + }, + "members-active": { + "one": "1 członek", + "few": "członków", + "many": "członków", + "other": "{{count}} członkowie" + }, + "members-active-none": "brak członków", + "members-count": "{{count}} członków", + "members-label": "Użytkownicy", + "members-limit-reached": "Osiągnięto limit członków", + "members-title": "Użytkownicy", + "membership-approval": "Wymagaj zatwierdzenia", + "membership-approval-description": "Twoja społeczność może bezpłatnie dołączyć, ale nowi członkowie muszą najpierw zostać zatwierdzeni przez twórcę społeczności", + "membership-button": "Wymagane członkostwo", + "membership-declined": "Wniosek o członkostwo został odrzucony", + "membership-description": "Członkostwo w grupie wymaga akceptacji przez administratora grupy", + "membership-ens": "Wymagaj nazwy użytkownika ENS", + "membership-ens-description": "Twoja społeczność wymaga nazwy użytkownika ENS, aby móc się do niej przyłączyć.", + "membership-free": "Brak wymagań", + "membership-free-description": "Twoja społeczność jest darmowa i każdy może do niej dołączyć", + "membership-invite": "Wymagaj zaproszenia od innego członka", + "membership-invite-description": "Do Twojej społeczności można dołączyć tylko poprzez zaproszenie od istniejących członków społeczności", + "membership-none": "Brak", + "membership-none-placeholder": "Możesz wymagać, aby nowi członkowie spełniali pewne kryteria, zanim będą mogli dołączyć. To może być zmienione w każdej chwili", + "membership-request-pending": "Wniosek o członkostwo w trakcie rozpatrywania", + "membership-requests": "Prośby o członkostwo", + "membership-title": "Wymagane członkostwo", + "message": "Wiadomość", + "message-not-sent": "Wiadomość nie wysłana", + "message-options-cancel": "Anuluj", + "message-reply": "Odpowiedz", + "messages": "Wiadomości", + "messages-from-contacts-only-subtitle": "Tylko osoby dodane jako kontakty mogą rozpocząć nowy czat z Tobą lub zaprosić Cię do grupy", + "might-break": "Może zepsuć niektóre zdecentralizowane aplikacje", + "migration-successful": "Migracja powiodła się", + "migration-successful-text": "Konto pomyślnie zmigrowane do Keycard", + "migrations-failed-content": "{{message}}\n wersja schematu: początkowa {{initial-version}}, aktualna {{current-version}}, ostatnia {{last-version}} \n\nWystąpił błąd bazy danych. Twoje fundusze i klucz czatu są bezpieczne. Inne dane, takie jak czaty i kontakty, nie mogą zostać przywrócone. Przycisk '{{erase-multiaccounts-data-button-text}}' usunie wszystkie inne dane i umożliwi Ci dostęp do twoich środków oraz wysyłania wiadomości.", + "miners-higher-fee": "Górnicy prawdopodobnie uwzględnią Twoją transakcję wcześniej, jeśli zapłacisz wyższą opłatę.", + "minimum-received": "Otrzymane minimum", + "mobile-network-ask-me": "Zapytaj mnie, gdy używam sieci komórkowej", + "mobile-network-continue-syncing": "Kontynuuj synchronizację", + "mobile-network-continue-syncing-details": "Możesz to później zmienić w ustawieniach", + "mobile-network-go-to-settings": "Przejdź do ustawień", + "mobile-network-settings": "Dane komórkowe", + "mobile-network-sheet-configure": "Możesz skonfigurować synchronizację bardziej\nszczegółowo w", + "mobile-network-sheet-offline": "Brak dostępu do sieci Wi-Fi, synchronizacja wiadomości jest wyłączona.", + "mobile-network-sheet-offline-details": "Synchronizacja przy użyciu danych komórkowych jest wyłączona", + "mobile-network-sheet-remember-choice": "Zapamiętaj mój wybór", + "mobile-network-sheet-settings": "ustawienia", + "mobile-network-start-syncing": "Rozpocznij synchronizację", + "mobile-network-stop-syncing": "Zatrzymaj synchronizację", + "mobile-network-stop-syncing-details": "Do czasu połączenia z Wi-Fi?", + "mobile-network-use-mobile": "Użyj danych komórkowych", + "mobile-network-use-mobile-data": "Status zużywa dużo danych podczas synchronizacji czatów i portfela.", + "mobile-network-use-wifi": "Tylko Wi-Fi", + "mobile-syncing-sheet-details": "Status zużywa dużo danych podczas synchronizacji czatów i portfela.", + "mobile-syncing-sheet-title": "Synchronizować przy użyciu danych komórkowych?", + "more": "więcej", + "move-and-reset": "Przenieś i zresetuj", + "move-keystore-file": "Przenieś plik keystore", + "move-keystore-file-to-keycard": "Przenieść plik keystore na kartę Keycard?", + "multiaccount-exists-content": "Klucze do tego konta już istnieją i nie mogą być dodane ponownie. Jeśli zgubiłeś hasło, kod dostępu lub kartę Keycard, odinstaluj aplikację, zainstaluj ją ponownie i uzyskaj dostęp do kluczy, wprowadzając swoją frazę Seed", + "multiaccount-exists-title": "Klucze do tego konta już istnieją", + "multiaccounts-recover-enter-phrase-text": "Wprowadź 12, 15, 18, 21 lub 24 słowa.\nOddziel słowa pojedynczą spacją.", + "multiaccounts-recover-enter-phrase-title": "Wpisz swoją frazę odzyskiwania (Seed)", + "mute": "Wycisz", + "my-accounts": "Moje konta", + "my-accounts-empty": "Twoje dostępne konta pojawią się tutaj", + "my-profile": "Mój profil", + "my-status": "Mój status", + "name": "Nazwa", + "name-of-token": "Nazwa twojego tokena", + "name-optional": "Nazwa (opcjonalnie)", + "name-your-channel": "Nazwij swój kanał", + "name-your-channel-placeholder": "Nazwa kanału", + "name-your-community": "Nazwij swoją społeczność", + "name-your-community-placeholder": "Chwytliwe imię", + "need-help": "Potrzebujesz pomocy?", + "network": "Sieć", + "network-chain": "Łańcuch sieci", + "network-details": "Szczegóły sieci", + "network-fee": "Opłata sieciowa", + "network-id": "Identyfikator sieci", + "network-info": "Informacje o sieci", + "network-invalid-network-id": "Podany identyfikator sieci nie jest zgodny z identyfikatorem sieci podanym w adresie URL RPC", + "network-invalid-status-code": "Nieprawidłowy kod statusu: {{code}}", + "network-invalid-url": "Adres URL sieci jest nieprawidłowy", + "network-settings": "Ustawienia sieci", + "never": "Nigdy", + "new": "Nowy", + "new-category": "Nowa kategoria", + "new-chat": "Nowy czat", + "new-community-title": "Nowa społeczność", + "new-contact": "Nowy kontakt", + "new-contract": "Nowy kontrakt", + "new-favourite": "Nowe ulubione", + "new-group": "Nowa grupa", + "new-group-chat": "Nowy czat grupowy", + "new-network": "Nowa sieć", + "new-password": "Nowe hasło", + "new-pin-description": "Powtórz nowy 6-cyfrowy kod dostępu.", + "new-public-group-chat": "Dołącz do publicznego czatu", + "new-puk-description": "Wprowadź nowy 12-cyfrowy PUK", + "new-status": "Nowy status", + "new-tab": "Nowa karta", + "next": "Dalej", + "nickname": "Pseudonim", + "nickname-description": "Pseudonimy pomagają Ci identyfikować innych w Statusie.\nTylko Ty możesz zobaczyć dodane przez siebie pseudonimy.", + "no": "Nie", + "no-collectibles": "Brak dostępnych przedmiotów kolekcjonerskich", + "no-contacts": "W tej chwili brak kontaktów", + "no-keycard-applet-on-card": "Brak apletu Keycard na karcie", + "no-messages": "Brak wiadomości", + "no-pairing-slots-available": "Ta karta jest już sparowana z 5 urządzeniami i nie można jej sparować z tym urządzeniem. Użyj jednego ze sparowanych urządzeń, zaloguj się za pomocą tej karty i zwolnij gniazda parowania na karcie", + "no-pinned-messages": "Brak przypiętych wiadomości", + "no-result": "Brak wyników", + "no-thanks": "Nie, dziękuję", + "no-tokens-found": "Nie znaleziono tokenów", + "node-address": "Adres węzła", + "node-details": "Szczegóły węzła", + "node-info": "Informacje o węźle", + "node-version": "Wersja węzła", + "nodes-disabled": "Węzły Status wyłączone", + "non-archival-node": "Punkt końcowy RPC nie obsługuje żądań archiwalnych. Twoja historia lokalnych przelewów może być niekompletna.", + "non-contacts": "Spoza listy kontaktów", + "nonce": "Nonce", + "none": "Brak", + "normal": "Normalny", + "not-applicable": "Nie dotyczy niepodpisanych transakcji", + "not-connected-nodes": "Nie podłączony do węzła Statusu", + "not-connected-to-peers": "Nie jest połączony z żadnym peerem", + "not-enough-snt": "Niewystarczająca ilość SNT", + "not-found": "Nie znaleziono", + "not-keycard-text": "Karta, której użyłeś, nie jest kartą Keycard. Aby z niej korzystać, należy zakupić kartę Keycard.", + "not-keycard-title": "To nie jest karta Keycard", + "not-registered": "niezarejestrowany", + "notification-settings": "Powiadomienia", + "notifications": "Powiadomienia", + "notifications-non-contacts": "Powiadomienia od osób niebędących kontaktami", + "notifications-preferences": "Preferencje powiadomień", + "notifications-servers": "Serwery powiadomień", + "notifications-switch": "Pokaż powiadomienia", + "notifications-transactions": "Transakcje portfelowe", + "notify": "Powiadamiaj", + "now": "Teraz", + "off": "Wył.", + "off-status-tree": "Off Status tree", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Włącz węzły aplikacji Status, aby pobrać wiadomości, które zostały wysłane, gdy aplikacja była zamknięta. Po włączeniu węzeł Status pobiera Twój adres IP. Po wyłączeniu nie będziesz otrzymywać wiadomości, gdy aplikacja jest zamknięta, i nie zobaczysz ich po późniejszym otwarciu aplikacji.", + "offline-messaging-use-history-nodes": "Użyj nodów Status", + "ok": "OK", + "ok-continue": "OK, kontynuuj", + "ok-got-it": "OK, rozumiem", + "ok-save-pass": "OK, zapisz hasło", + "okay": "W porządku", + "on": "Wł.", + "on-status-tree": "On Status tree", + "once-enabled-share-metadata": "Po włączeniu tej funkcji łącza publikowane na czacie mogą udostępniać witrynie metadane użytkownika.", + "one-day": "Jeden dzień", + "one-month": "Jeden miesiąc", + "one-week": "Jeden tydzień", + "open": "Otwórz", + "open-chat": "Utwórz czat publiczny", + "open-dapp": "Otwórz dAppkę", + "open-dapp-store": "Odkryj dAppsy", + "open-home": "Otwórz...", + "open-in-new-tab": "Otwórz w nowej karcie", + "open-membership": "Otwarte członkostwo", + "open-nfc-settings": "Otwórz ustawienia NFC", + "opening-buy-crypto": "Otwieram {{site}} ...", + "optimal": "Optymalny", + "optional": "opcjonalne", + "or": "LUB", + "outgoing": "Wychodzące", + "outgoing-transaction": "Transakcja wychodząca", + "page-camera-request-blocked": "dostęp do aparatu zablokowany. Aby odblokować dostęp do kamery, przejdź do Ustawień", + "page-would-like-to-use-camera": "chciałby użyć twojego aparatu", + "pair": "Sparuj urządzenia", + "pair-card": "Sparuj z tym urządzeniem", + "pair-code": "Kod parowania", + "pair-code-explanation": "Paruje kartę z innym urządzeniem (maksymalnie z 5), aby odblokować klucze i podpisywać transakcje za pomocą tej samej karty Keycard", + "pair-code-placeholder": "Kod parowania", + "pair-this-card": "Sparuj tę kartę", + "pair-this-device": "Reklamuj urządzenie", + "pair-this-device-description": "Sparuj swoje urządzenia, aby synchronizować między nimi kontakty i czaty", + "paired-devices": "Sparowane urządzenia", + "pairing": "Parowanie", + "pairing-card": "Karta parowania", + "pairing-changed": "Kod parowania został zmieniony", + "pairing-code-placeholder": "Kod parowania...", + "pairing-code_error1": "Kody parowania nie są zgodne.", + "pairing-go-to-installation": "Przejdź do ustawień parowania", + "pairing-maximum-number-reached-content": "Proszę wyłączyć jedno z urządzeń przed włączeniem nowego.", + "pairing-maximum-number-reached-title": "Osiągnięto maksymalną liczbę urządzeń", + "pairing-new-installation-detected-content": "Zostało wykryte nowe urządzenie.\nAby prawidłowo korzystać z urządzeń, ważne jest, aby je sparować i włączyć przed ich użyciem.\nPrzejdź do sekcji 'Urządzenia' w ustawieniach, aby sparować urządzenia.", + "pairing-new-installation-detected-title": "Wykryto nowe urządzenie", + "pairing-no-info": "Brak informacji", + "pairing-please-set-a-name": "Nadaj nazwę swojemu urządzeniu.", + "passphrase": "Passphrase", + "password": "Hasło", + "password-description": "Przynajmniej 6 znaków. Twoje hasło chroni Twoje klucze. Potrzebujesz go, aby odblokować aplikację Status i dokonywać transakcji.", + "password-mismatch": "Nowe hasło i potwierdzenie nie pasują do siebie", + "password-placeholder": "Hasło...", + "password-placeholder2": "Potwierdź hasło", + "password-reset-in-progress": "Zmiana hasła...", + "password-reset-success": "Hasło zostało zmienione", + "password-reset-success-message": "Musisz się ponownie zalogować", + "password_error1": "Hasła nie pasują do siebie.", + "paste": "Wklej", + "paste-json": "Wklej JSON", + "pay-to-chat": "Zapłać, by rozmawiać", + "peer-content": "Urządzenie podłączone do sieci czatu aplikacji Status. Każdy użytkownik może reprezentować jedno lub więcej połączeń, w zależności od liczby posiadanych urządzeń.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer (Od osoby do osoby)", + "peers": "Peery", + "pending": "W oczekiwaniu", + "pending-confirmation": "Oczekujące potwierdzenie...", + "pending-invitations": "Oczekujące wnioski o członkostwo", + "per-gas-price-limit": "Limit ceny za gaz", + "per-gas-tip-limit": "Limit na końcówkę gazu", + "perform-backup": "Wykonaj kopię zapasową", + "permissions": "Uprawnienia", + "phone-e164": "Międzynarodowy 1", + "photos": "Zdjęcia", + "photos-access-error": "Aby nadać wymagane uprawnienia do zdjęć, przejdź do ustawień systemowych i upewnij się, że zaznaczono opcję Status > Zdjęcia.", + "pin": "Przypnij", + "pin-changed": "6-cyfrowy kod dostępu został zmieniony", + "pin-code": "6-cyfrowy kod dostępu.", + "pin-limit-reached": "Osiągnięto limit przypięć. Najpierw odepnij poprzednią wiadomość.", + "pin-mismatch": "Nieprawidłowy kod dostępu", + "pin-one-attempt": " jedną próbę ", + "pin-one-attempt-blocked-after": "zanim Twoja karta Keycard zostanie zablokowana", + "pin-one-attempt-blocked-before": "Uważaj, masz tylko", + "pin-one-attempt-frozen-after": "zanim Twoja karta Keycard zostanie zamrożona", + "pin-one-attempt-frozen-before": "Uważaj, masz tylko", + "pin-retries-left": "Pozostało {{number}}", + "pinned-by": "Przypięte przez", + "pinned-messages": "Przypięte wiadomości", + "pinned-messages-count": { + "one": "Jeden", + "few": "Kilka", + "many": "Wiele", + "other": "Inne" + }, + "pinned-messages-empty": "Tutaj pojawią się przypięte wiadomości. Aby przypiąć wiadomość, naciśnij i przytrzymaj ją, a następnie dotknij \"Przypnij\"", + "powered-by-paraswap": "Obsługiwane przez Paraswap", + "preference": "Preferencje", + "preview-privacy": "Podgląd trybu prywatności", + "previewing-may-share-metadata": "Podgląd linków z tych witryn internetowych może spowodować udostępnienie twoich metadanych ich właścicielom", + "price-impact": "Wpływ ceny", + "price-impact-desc": "Szacowany wpływ ceny dla tej transakcji. Jeśli aktualna opłata bazowa za blok przekracza tę wartość, Twoja transakcja zostanie uwzględniona w kolejnym bloku z niższą opłatą bazową.", + "principles": "Zasady", + "priority": "Priorytet", + "privacy": "Prywatność", + "privacy-and-security": "Prywatność i bezpieczeństwo", + "privacy-photos": "Prywatność zdjęcia profilowego", + "privacy-policy": "Polityka prywatności", + "privacy-show-to-warning": "Osoby, które widziały już Twoje zdjęcie profilowe, będą nadal to robić", + "private-key": "Klucz prywatny", + "private-notifications": "Prywatne powiadomienia", + "private-notifications-descr": "Status będzie Cię powiadamiał o nowych wiadomościach. Możesz edytować swoje preferencje dotyczące powiadomień później w ustawieniach.", + "processing": "Momencik", + "product-information": "Informacje o produkcie", + "profile": "Profil", + "profile-deleted-content": "Twój profil został pomyślnie usunięty", + "profile-deleted-keycard": "Możesz teraz przywrócić kolejną parę kluczy na swojej karcie Keycard", + "profile-deleted-title": "Profil usunięty", + "profile-details": "Szczegóły profilu", + "profile-not-found": "Nie znaleziono użytkownika", + "profile-pic-pick": "Wybierz z galerii", + "profile-pic-remove": "Usuń zdjęcie", + "profile-pic-take": "Zrób zdjęcie", + "profile-picture-updated": "Zaktualizowano zdjęcie profilowe", + "public-channel": "Kanał publiczny", + "public-chat": "Czat publiczny", + "public-chat-description": "Dołącz do publicznych czatów dotyczących twoich zainteresowań! Każdy może założyć nowy.", + "public-chats": "Czaty publiczne", + "public-group-status": "Publiczny", + "public-group-topic": "Temat", + "public-key": "Klucz publiczny", + "puk-and-pairing-codes-displayed": "Wyświetlany kod PUK i kody parowania", + "puk-changed": "Zmieniono 12-cyfrowy PUK", + "puk-code": "Kod PUK", + "puk-code-explanation": "Jeśli zapomnisz swojego 6-cyfrowego kodu dostępu lub wprowadzisz go niepoprawnie 3 razy, będziesz potrzebować tego kodu do odblokowania karty.", + "puk-mismatch": "Błędny kod PUK", + "push-failed-transaction": "Twoja transakcja się nie powiodła", + "push-failed-transaction-body": "{{value}} {{currency}} do {{to}}", + "push-inbound-transaction": "Otrzymałeś {{value}} {{currency}}", + "push-inbound-transaction-body": "Od {{from}} do {{to}}", + "push-notifications-server-enabled": "Serwer włączony", + "push-notifications-servers": "Serwery powiadomień Push", + "push-outbound-transaction": "Wysłałeś {{value}} {{currency}}", + "push-outbound-transaction-body": "Od {{from}} do {{to}}", + "quiet-days": "{{quiet-days}} dni", + "quiet-hours": "{{quiet-hours}} godzin", + "re-encrypt-key": "Zaszyfruj ponownie swoje klucze", + "rearrange-categories": "Zmień kolejność kategorii", + "receive": "Otrzymaj", + "receive-transaction": "Otrzymaj transakcję", + "recent": "Najnowsze", + "recent-empty": "Ostatnio używane adresy pojawią się tutaj", + "recent-recipients": "Kontakty", + "recently-used-stickers": "Tutaj pojawią się ostatnio używane naklejki", + "recipient": "Odbiorca", + "recipient-code": "Wprowadź adres odbiorcy", + "recipient-code-placeholder": "0x ... lub nazwa_użytkownika.domena.eth", + "recover": "Odzyskaj", + "recover-key": "Uzyskaj dostęp do istniejących kluczy", + "recover-keycard-multiaccount-not-supported": "Klucze do tego konta już istnieją i nie mogą być dodane ponownie. Jeśli zgubiłeś hasło, kod dostępu lub kartę Keycard, odinstaluj aplikację, zainstaluj ją ponownie i uzyskaj dostęp do kluczy, wprowadzając swoją frazę Seed", + "recover-with-keycard": "Odzyskaj za pomocą karty Keycard", + "recover-with-seed-phrase": "Odzyskaj za pomocą frazy Seed", + "recovering-key": "Uzyskiwanie dostępu do kluczy...", + "recovery-confirm-phrase": "Potwierdź frazę odzyskiwania (Seed)", + "recovery-phrase": "Fraza odzyskiwania (Seed)", + "recovery-success-text": "Będziesz musiał stworzyć nowy kod lub hasło, aby ponownie zaszyfrować swoje klucze", + "recovery-typo-dialog-description": "Zwróć uwagę, że twoja fraza Seed musi posiadać te same słowa i kolejność, w jakiej je otrzymałeś", + "recovery-typo-dialog-title": "Czy fraza odzyskiwania (Seed) jest poprawna?", + "redeem-amount": "Dostępne bonusy: {{quantity}}", + "redeem-now": "Zrealizuj teraz", + "redeem-success": "Skutecznie zrealizowany bonus!", + "reduced-tip": "napiwek priorytetowy zostanie zmniejszony", + "refresh": "Odśwież", + "registered": "zarejestrowany", + "reject-and-delete": "Odrzuć i usuń", + "remember-me": "Zapamiętaj mnie", + "remind-me-later": "Pokaż mi to jeszcze raz", + "remove": "Usuń", + "remove-favourite": "Usuń ulubione", + "remove-from-chat": "Usuń z czatu", + "remove-from-contacts": "Usuń z kontaktów", + "remove-from-contacts-text": "Usuwając użytkownika z listy kontaktów, nie ukrywasz przed nim adresu swojego portfela.", + "remove-group": "Usuń grupę", + "remove-network": "Usuń sieć", + "remove-token": "Usuń token", + "removed": "usunięci", + "repeat-pin": "Powtórz nowy 6-cyfrowy kod dostępu.", + "repeat-puk": "Powtórz nowy 12-cyfrowy PUK", + "replying-to": "Odpowiedź do {{author}}", + "report-bug-email-template": "1. Opis problemu\n {{description}}\n \n\n 2. Kroki do reprodukcji\n {{steps}}\n \n\n 3. Proszę, dołącz zrzuty ekranu, które mogą pokazać problem\n", + "request-access": "Żądanie dostępu", + "request-feature": "Poproś o funkcję", + "request-membership": "Wnioskuj o członkostwo", + "request-pending": "Prośba w toku...", + "request-transaction": "Poproś o transakcję", + "required-field": "Pole wymagane", + "resend-message": "Wyślij ponownie", + "reset-card": "Zresetuj kartę", + "reset-card-description": "Ta operacja zresetuje kartę do stanu początkowego. Spowoduje to usunięcie wszystkich danych karty, w tym kluczy prywatnych. Operacja jest nieodwracalna.", + "reset-database": "Zresetuj bazę danych", + "reset-database-warning": "Usuń czaty, kontakty i ustawienia. Wymagane w przypadku utraty hasła", + "reset-database-warning-keycard": "Usuń czaty, kontakty i ustawienia.", + "reset-password": "Zresetuj hasło", + "restore-defaults": "Przywróć domyślne", + "retry": "Ponów próbę", + "revoke-access": "Odwołaj dostęp", + "rpc-url": "URL RPC", + "rpc-usage-copy": "Skopiuj do schowka", + "rpc-usage-filter": "Metody filtrowania", + "rpc-usage-get-stats": "Odśwież", + "rpc-usage-info": "Statystyki użycia RPC", + "rpc-usage-reset": "Resetuj", + "safe-estimate": "Bezpieczne oszacowanie", + "save": "Zapisz", + "save-password": "Zapisz hasło aż do wylogowania", + "save-password-unavailable": "Ustaw hasło urządzenia, aby zapisać hasło", + "save-password-unavailable-android": "Zapisanie hasła jest niedostępne: Twoje urządzenie może być zrootowane lub nie ma niezbędnych funkcji zabezpieczeń.", + "scan-qr": "Skanuj QR", + "scan-qr-code": "Zeskanuj kod QR z adresem portfela", + "scan-tokens": "Skanuj tokeny", + "search": "Szukaj", + "search-no-chat-found": "Brak wyników wyszukiwania. Czy masz na myśli", + "secret-keys-confirmation-text": "Będziesz ich potrzebować, aby nadal korzystać z karty Keycard na wypadek, w przypadku kiedy utracisz swój telefon.", + "secret-keys-confirmation-title": "Kody zapisane?", + "security": "Bezpieczeństwo", + "see-details": "Zobacz szczegóły", + "see-it-again": "ZOBACZ PONOWNIE", + "see-suggestions": "Zobacz propozycje", + "seed-key-uid-mismatch": "Podana fraza Seed nie pasuje", + "seed-key-uid-mismatch-desc-1": "Wprowadzona fraza Seed nie pasuje do {{multiaccount-name}}.", + "seed-key-uid-mismatch-desc-2": "Aby zarządzać kluczami dla tego konta, zweryfikuj swoją frazę odzyskiwania (Seed) i spróbuj ponownie.", + "seed-phrase-content": "Zestaw łatwych do czytania słów, losowo wybranych ze standardowej listy BIP39 i używanych do odzyskiwania lub uzyskiwania dostępu do konta Ethereum na innych portfelach i urządzeniach. Określany również jako „fraza mnemoniczna”, „fraza odzyskiwania” lub „kopia zapasowa portfela” w całym ekosystemie kryptowalut. Większość aplikacji kryptowalutowych używa tego samego standardu do generowania kont.", + "seed-phrase-placeholder": "Fraza odzyskiwania (Seed)", + "seed-phrase-title": "Fraza odzyskiwania (Seed)", + "select": "Wybierz", + "select-account": "Wybierz konto", + "select-account-dapp": "Wybierz konto, którego chcesz używać ze zdecentralizowanymi aplikacjami", + "select-account-first": "Najpierw wybierz konto", + "select-chat": "Wybierz czat, aby rozpocząć wysyłanie wiadomości", + "select-new-location-for-keys": "Wybierz nową lokalizację do zapisania klucza prywatnego lub kluczy prywatnych", + "select-token-to-receive": "Wybierz token do otrzymania", + "select-token-to-swap": "Wybierz token do zamiany", + "selected": "Wybrany", + "send-logs": "Zgłoś błąd", + "send-logs-to": "Zgłoś błąd do {{email}}", + "send-message": "Wyślij wiadomość", + "send-push-notifications-description": "W przypadku wyłączenia osoba odbierająca twoje wiadomości nie zostanie powiadomiona o ich przybyciu", + "send-request": "Wyślij prośbę transakcji", + "send-request-amount": "Ilość", + "send-request-amount-max-decimals": "Maks. liczba miejsc po przecinku to {{asset-decimals}}", + "send-request-unknown-token": "Nieznany token - {{asset}}", + "send-sending-to": "do {{recipient-name}}", + "send-transaction": "Wyślij transakcję", + "sending": "Wysyłanie", + "sent-at": "Wysłano w", + "server": "Serwer", + "set-a-topic": "Utwórz temat", + "set-currency": "Ustaw walutę", + "set-custom-fee": "Ustaw opłatę niestandardową", + "set-dapp-access-permissions": "Ustaw uprawnienia dostępu zdecentralizowanym aplikacjom", + "set-max": "Ustaw maksymalnie", + "settings": "Ustawienia", + "share": "Udostępnij", + "share-address": "Udostępnij adres", + "share-chat": "Udostępnij czat", + "share-contact-code": "Udostępnij mój klucz czatu", + "share-dapp-text": "Sprawdź tą zdecentralizowaną aplikację, której używam na platformie Status: {{link}}", + "share-link": "Udostępnij link", + "share-logs": "Udostępniaj dzienniki", + "share-my-profile": "Udostępnij mój profil", + "share-profile": "Udostępnij profil", + "share-profile-link": "Udostępnij link do profilu", + "share-public-chat-text": "Sprawdź ten czat publiczny w aplikacji Status: {{link}}", + "shared": "Udostępnione", + "sharing-copied-to-clipboard": "Skopiowane do schowka", + "sharing-copy-to-clipboard": "Skopiuj do schowka", + "sharing-data-desc-1": "Dane są sprawdzane zgodnie z zasadami publicznymi, aby zapewnić, że nie są wysyłane żadne dane wrażliwe. Nie ufaj, zweryfikuj.", + "sharing-data-desc-2": "Dane użytkowania kompleksowo zaszyfrowane są przesyłane przez sieć peer-to-peer Status", + "sharing-data-desc-3": "Zamiast zwykłego klucza czatu używany jest klucz jednorazowy", + "sharing-data-desc-4": "Dane użytkowania nie mogą być powiązane z Twoim adresem IP", + "sharing-data-desc-5": "Zbiorcze dane wszystkich użytkowników są publicznie dostępne", + "sharing-data-desc-6": "Dane są usuwane z telefonu po ich wysłaniu", + "sharing-share": "Udostępnij", + "show-less": "Pokaż mniej", + "show-more": "Pokaż więcej", + "show-notifications": "Pokaż powiadomienia", + "show-profile-pictures": "Zobacz zdjęcia profilowe z", + "show-profile-pictures-to": "Pokaż swoje zdjęcie profilowe dla", + "show-qr": "Pokaż kod QR", + "show-transaction-data": "Pokaż dane transakcji", + "sign-and-send": "Podpisz i wyślij", + "sign-anyway": "Podpisz mimo to", + "sign-in": "Zaloguj się", + "sign-message": "Podpisz wiadomość", + "sign-out": "Wyloguj się", + "sign-request-failed": "Nie można podpisać wiadomości", + "sign-with": "Podpisz się za pomocą", + "sign-with-password": "Podpisz się hasłem", + "sign-you-in": "Logujemy Cię…", + "signing": "Podpisywanie", + "signing-a-message": "Podpisywanie wiadomości", + "signing-phrase": "Fraza podpisująca", + "skip": "Pomiń", + "slippage": "Wahanie", + "slow": "Powolny", + "something-went-wrong": "Coś poszło nie tak", + "soon": "Wkrótce", + "specify-address": "Określ adres", + "specify-name": "Podaj nazwę", + "specify-network-id": "Podaj id sieci", + "specify-rpc-url": "Podaj URL RPC", + "specify-server-public-key": "Wprowadź klucz publiczny serwera", + "specify-symbol": "Określ symbol", + "start-chat": "Rozpocznij czat", + "start-conversation": "Rozpocznij rozmowę", + "start-group-chat": "Rozpocznij czat grupowy", + "start-new-chat": "Rozpocznij nowy czat", + "starter-pack-coming": "Pakiet startowy już w drodze", + "starter-pack-coming-description": "Może to zająć od kilku minut do kilku godzin", + "starter-pack-received": "Otrzymano pakiet startowy", + "starter-pack-received-description": "Oto trochę kryptowalut na dobry początek! Użyj ich, aby kupić naklejki, zmienić nazwę ENS lub korzystać ze zdecentralizowanych aplikacji", + "status": "Status", + "status-always-online": "Zawsze online", + "status-automatic": "Automatycznie", + "status-automatic-subtitle": "Ustaw status automatycznie", + "status-confirmed": "Potwierdzone", + "status-dnd": "Nie przeszkadzać", + "status-dnd-subtitle": "Wycisz wszystkie powiadomienia", + "status-hardwallet": "Portfel sprzętowy Status", + "status-inactive": "Nieaktywny", + "status-inactive-subtitle": "Ukrywa Twój status online", + "status-is-open-source": "Status jest otwartoźródłowy", + "status-keycard": "Karta Keycard aplikacji Status", + "status-mobile-descr": "Status ma tendencję do zużywania dużej ilości danych podczas synchronizowania czatów. Możesz zrezygnować z synchronizacji, gdy korzystasz z sieci komórkowej.", + "status-not-sent-click": "Nie wysłano. Dotknij, aby zobaczyć opcje", + "status-not-sent-tap": "Nie potwierdzona. Dotknij, aby zobaczyć opcje", + "status-pending": "W oczekiwaniu", + "status-sent": "Wysłano", + "status-tx-not-found": "Nie znaleziono TX", + "status-updates-descr": "Tutaj będą pojawiać się aktualizacje statusów. Dodaj profil jako kontakt, aby otrzymywać aktualizacje na swojej osi czasu.", + "statuses-descr": "Dziel się tym, co myślisz, i bądź na bieżąco ze swoimi kontaktami", + "statuses-my-status-descr": "Podziel się tym, czym chcesz. Każdy, kto odwiedzi twój profil, będzie w stanie zobaczyć twój status. Osoby, które dodadzą Cię do swoich kontaktów, będą otrzymywać aktualizacje dotyczące Ciebie na swojej osi czasu.", + "step-i-of-n": "Krok {{step}} z {{number}}", + "sticker": "Naklejka", + "sticker-market": "Rynek naklejek", + "storage": "Przechowywanie", + "submit": "Prześlij", + "submit-bug": "Zgłoś błąd", + "success": "Sukces", + "suggested-min-tip": "Sugerowany min. napiwek", + "suggested-price-limit": "Sugerowany limit ceny", + "swap": "Zamień", + "swap-details": "Szczegóły zamiany", + "switch-to-simple-interface": "Przełącz na prosty interfejs", + "symbol": "Symbol", + "sync-all-devices": "Synchronizuj wszystkie urządzenia", + "sync-in-progress": "Synchronizacja...", + "sync-settings": "Ustawienia synchronizacji", + "sync-synced": "W synchronizacji", + "syncing-devices": "Synchronizuję ...", + "system": "System", + "tabs": "Karty", + "tag-was-lost": "Znacznik został zgubiony", + "tap-card-again": "Przyłóż ponownie kartę do tylnej części telefonu", + "terms-of-service": "Warunki Użytkowania", + "test-networks": "Sieci testowe", + "text-input-disabled": "Proszę poczekać chwilę…", + "thank-you": "Dziękuję", + "this-device": "To urządzenie", + "this-device-desc": "Twoje klucze będą zaszyfrowane i bezpiecznie przechowywane na Twoim urządzeniu", + "this-is-you-signing": "To jest Twoja fraza podpisująca", + "this-will-take-few-seconds": "Zajmie to kilka sekund.", + "three-days": "Trzy dni", + "three-words-description": "Powinieneś zobaczyć te 3 słowa przed podpisaniem każdej transakcji", + "three-words-description-2": "Jeśli zobaczysz inną kombinację, anuluj transakcję i wyloguj się", + "timeline": "Oś czasu", + "tip-cap": "Pułap napiwku", + "to": "Do", + "to-block": "Zablokuj", + "to-enable-biometric": "Aby włączyć {{bio-type-label}}, musisz zapisać swoje hasło na ekranie odblokowania.", + "to-encrypt-enter-password": "Aby zaszyfrować konto, wprowadź swoje hasło", + "to-see-this-message": "Aby zobaczyć tę wiadomość,", + "token": "Token", + "token-auto-validate-decimals-error": "Błędne oznaczenia dziesiętne dla tokena {{symbol}} pod adresem {{address}} - ustawione na {{expected}} ale wykryte jako {{actual}}", + "token-auto-validate-name-error": "Nieprawidłowa nazwa tokena {{symbol}} pod adresem {{address}} - ustawiona na {{expected}}, ale wykryta jako {{actual}}", + "token-auto-validate-symbol-error": "Nieprawidłowa nazwa tokena {{symbol}} pod adresem {{address}} - ustawiona na {{expected}}, ale wykryta jako {{actual}}", + "token-details": "Szczegóły tokena", + "topic-name-error": "Używaj tylko małych liter (od a do z), cyfr i myślników (-). Nie używaj kluczy czatu", + "total-gas": "Całkowity gas", + "transaction": "Transakcja", + "transaction-data": "Dane transakcyjne", + "transaction-declined": "Transakcja odrzucona", + "transaction-description": "Uznaj ją za kompletną po 12 potwierdzeniach w sieci.", + "transaction-details": "Szczegóły transakcji", + "transaction-failed": "Transakcja nieudana", + "transaction-fee": "Opłata transakcyjna", + "transaction-history": "Historia transakcji", + "transaction-request": "Żądanie transakcji", + "transaction-sent": "Transakcja wysłana", + "transaction-signed": "Transakcja została pomyślnie podpisana", + "transactions": "Transakcje", + "transactions-filter-select-all": "Zaznacz wszystko", + "transactions-filter-title": "Filtruj historię", + "transactions-history": "Historia transakcji", + "transactions-history-empty": "Brak transakcji w Twojej historii", + "transactions-history-loading": "Ładowanie historii transakcji. Może to chwilę potrwać.", + "transactions-load-more": "Załaduj więcej", + "transactions-management-enabled": "Zarządzanie transakcjami (alfa)", + "transactions-sign": "Podpisz", + "transfer-ma-unknown-error-desc-1": "Wygląda na to, że Twoje multikonto nie zostało usunięte. Baza danych mogła zostać zresetowana", + "transfer-ma-unknown-error-desc-2": "Sprawdź listę swoich kont i spróbuj ponownie. Jeśli konto nie jest wymienione, przejdź do sekcji 'Dostań się do istniejących kluczy', aby odzyskać je za pomocą frazy odzyskiwania (Seed)", + "transfers-fetching-failure": "Historia przelewów nie może zostać zaktualizowana. Sprawdź swoje połączenie i pociągnij w dół, aby spróbować ponownie", + "tribute-required-by-multiaccount": "{{multiaccount-name}} wymaga SNT do rozpoczęcia czatu.", + "tribute-state-paid": "Danina zapłacona", + "tribute-state-pending": "Danina w toku", + "tribute-state-required": "Wymagana danina w postaci {{snt-amount}} SNT", + "tribute-to-talk": "Danina dla Rozmowy", + "tribute-to-talk-add-friends": "Dodaj znajomych jako kontakt, aby umożliwić czaty bez płacenia daniny.", + "tribute-to-talk-are-you-friends": "Jesteście przyjaciółmi?", + "tribute-to-talk-ask-to-be-added": "Poproś o dodanie jako kontakt", + "tribute-to-talk-contact-received-your-tribute": " otrzymał twoją daninę. Możecie teraz bezpiecznie ze sobą rozmawiać.", + "tribute-to-talk-desc": "Zarabiaj na swojej uwadze, wymagając SNT od nowych osób do rozpoczęcia czatu", + "tribute-to-talk-disabled": "Danina dla Rozmowy wyłączona", + "tribute-to-talk-disabled-note": "Od teraz nowe osoby mogą rozpocząć z Tobą czat bez wysyłania SNT.", + "tribute-to-talk-enabled": "Masz włączoną funkcję Danina dla Rozmowy.", + "tribute-to-talk-finish-desc": "Od tej pory będziesz otrzymywać wiadomości tylko od kontaktów i osób, które zapłaciły", + "tribute-to-talk-learn-more-1": "Twój czas i uwaga są Twoimi najcenniejszymi aktywami. Funkcja 'Danina dla Rozmowy' pozwala Ci ustawić ilość SNT wymaganą dla nowych osób, aby rozpocząć z Tobą czat.", + "tribute-to-talk-learn-more-2": "Każdy, kto nie znajduje się na twojej liście kontaktów, zostanie poproszony o zapłatę, a Ty możesz odpowiedzieć, gdy już to zrobi.", + "tribute-to-talk-learn-more-3": "Zawsze możesz odesłać pieniądze z powrotem, ale aby mieć pewność, że przyjaciele mogą swobodnie do Ciebie dotrzeć, dodaj ich najpierw jako kontakt.", + "tribute-to-talk-paywall-learn-more-1": "Nasz czas i uwaga są naszymi najcenniejszymi aktywami. Danina dla Rozmowy pozwala Ci nawiązać kontakt z innymi ludźmi w zamian za płatność w SNT", + "tribute-to-talk-paywall-learn-more-2": "Aby rozpocząć rozmowę z kimś, kto ma ustawioną daninę, po prostu zapłać wymaganą ilość SNT, a zostaniesz dodany jako kontakt.", + "tribute-to-talk-paywall-learn-more-3": "Jeśli ich znasz, możesz udostępnić swój profil poza aplikacją Status, aby zostać dodanym za darmo.", + "tribute-to-talk-pending": "Danina oczekująca na potwierdzenie", + "tribute-to-talk-pending-note": "Danina oczekuje na potwierdzenie w sieci. Możesz sprawdzić jej stan w historii transakcji", + "tribute-to-talk-removing-note": "Usunięcie opcji Danina dla Rozmowy pozwoli nowym osobom na rozpoczęcie czatu bez wysyłania SNT. Wymaga dokonania transakcji.", + "tribute-to-talk-set-snt-amount": "Ustaw ilość SNT wymaganą dla nowych osób do rozpoczęcia czatu", + "tribute-to-talk-signing": "Oczekiwanie na podpisanie transakcji...", + "tribute-to-talk-transaction-failed-note": "Transakcja nie powiodła się, a Twoje ustawienia Daniny dla Rozmowy nie zostały zmienione", + "tribute-to-talk-tribute-received1": "Danina otrzymana. Ty i", + "tribute-to-talk-tribute-received2": "są teraz kontaktami i mogą bezpiecznie rozmawiać ze sobą.", + "tribute-to-talk-you-require-snt": "Wymagasz SNT dla nowych osób, aby rozpocząć czat.", + "try-again": "Spróbuj ponownie", + "try-keeping-the-card-still": "Spróbuj trzymać kartę nieruchomo", + "turn-nfc-description": "NFC jest wyłączone na Twoim urządzeniu. Możesz je włączyć w ustawieniach", + "turn-nfc-on": "Włącz NFC aby kontynuować", + "two-minutes": "dwie minuty", + "tx-fail-description1": "Ta transakcja prawdopodobnie się nie powiedzie. Podpisuj na własne ryzyko, korzystając z niestandardowej opłaty sieciowej.", + "tx-fail-description2": "Ta transakcja prawdopodobnie się nie powiedzie. Ustaw niestandardową opłatę sieciową, aby podpisać transakcję na własne ryzyko.", + "type": "Rodzaj", + "type-a-message": "Napisz wiadomość", + "ulc-enabled": "ULC włączone", + "unable-to-fetch": "Nie można pobrać historii czatu", + "unable-to-read-this-code": "Nie można odczytać tego kodu", + "unable-to-send-messages": "Nie można wysyłać i odbierać wiadomości", + "unblock": "Odblokuj", + "unblock-contact": "Odblokuj tego użytkownika", + "unknown-status-go-error": "Nieznany błąd status-go", + "unlimited": "Bez ograniczeń", + "unlock": "Odblokuj", + "unmute": "Wyłącz wyciszenie", + "unpair-card": "Odłącz kartę", + "unpair-card-confirmation": "Ta operacja spowoduje odłączenie karty od bieżącego urządzenia. Wymagana autoryzacja 6-cyfrowym kodem dostępu. Czy chcesz kontynuować?", + "unpair-keycard": "Odepnij kartę Keycard od tego telefonu", + "unpair-keycard-warning": "Twój kod parowania\/PUK i PIN pozostają bez zmian", + "unpaired-keycard-text": "Wybrana karta Keycard nie jest powiązana z tym telefonem", + "unpaired-keycard-title": "Wygląda na to, że Twoja karta została odparowana", + "unpin": "Odepnij", + "update": "Aktualizacja", + "update-to-listen-audio": "Zaktualizuj do najnowszej wersji, aby odsłuchać wiadomość audio tutaj!", + "update-to-see-image": "Zaktualizuj do najnowszej wersji, aby zobaczyć tutaj ładny obraz!", + "update-to-see-sticker": "Zaktualizuj do najnowszej wersji, aby zobaczyć tutaj ładną naklejkę!", + "updates-to-tos": "Aktualizacje Warunków Użytkowania", + "updates-to-tos-desc": "Zanim przejdziesz dalej, zapoznaj się z Warunkami Użytkowania i potwierdź, że bierzesz pełną odpowiedzialność za sposób korzystania z aplikacji.", + "url": "URL", + "usd-currency": "USD", + "use-as-profile-picture": "Użyj jako zdjęcie profilowe", + "use-valid-contact-code": "Wprowadź lub zeskanuj prawidłowy klucz czatu, lub nazwę użytkownika", + "validation-amount-invalid-number": "Kwota nie jest prawidłową liczbą", + "validation-amount-is-too-precise": "Kwota jest zbyt dokładna. Maksymalna liczba miejsc po przecinku wynosi {{decimals}}.", + "verified-community": "✓ Zweryfikowana społeczność", + "version": "Wersja aplikacji", + "view": "Zobacz", + "view-cryptokitties": "Zobacz w CryptoKitties", + "view-cryptostrikers": "Zobacz w CryptoStrikers", + "view-data": "Wyświetl dane", + "view-details": "Wyświetl szczegóły", + "view-etheremon": "Zobacz w Etheremon", + "view-gitcoin": "Zobacz w Gitcoin", + "view-on-opensea": "Wyświetl na OpenSea", + "view-profile": "Zobacz profil", + "view-public-dashboard": "Wyświetl publiczny pulpit nawigacyjny", + "view-rules": "Wyświetl zasady", + "view-signing": "Wyświetl frazę podpisującą", + "view-superrare": "Zobacz w SuperRare", + "waiting-for-wifi": "Brak dostępu do sieci Wi-Fi, synchronizacja wiadomości jest wyłączona.", + "waiting-for-wifi-change": "Ustawienia", + "waiting-to-sign": "Oczekiwanie na podpisanie transakcji...", + "waiting-wi-fi": "Czekam na Wi-Fi…", + "waku-bloom-filter-mode": "Tryb filtra Waku Bloom", + "wakuv2-change-nodes": "Czy na pewno chcesz zmienić węzły Waku v2?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "Ustawienia Waku v2", + "wallet": "Portfel", + "wallet-address": "Adres portfela", + "wallet-asset": "Aktywo", + "wallet-assets": "Aktywa", + "wallet-backup-recovery-title": "Utwórz kopię zapasową frazy odzyskiwania (Seed)", + "wallet-choose-recipient": "Wybierz Odbiorcę", + "wallet-collectibles": "Artykuły kolekcjonerskie", + "wallet-insufficient-funds": "Niewystarczające środki", + "wallet-insufficient-gas": "Za mało ETH żeby zapłacić za gas", + "wallet-invalid-address": "Błędny adres: \n{{data}}", + "wallet-invalid-address-checksum": "Błąd w adresie:\n {{data}}", + "wallet-invalid-chain-id": "Typ sieci nie pasuje do:\n {{data}}. Aktualny łańcuch to: {{chain}}.", + "wallet-key-content": "64-znakowy adres szesnastkowy oparty na standardzie Ethereum i rozpoczynający się od 0x. Publiczny adres Twojego konta jest udostępniany innym osobom, gdy chcesz otrzymać środki. Nazywany jest również „adresem Ethereum” lub „adresem portfela”.", + "wallet-key-title": "Adres konta", + "wallet-manage-accounts": "Zarządzaj kontami", + "wallet-manage-assets": "Zarządzaj aktywami", + "wallet-request": "Prośba", + "wallet-send": "Wyślij", + "wallet-send-min-units": "Minimum 21000 jednostek", + "wallet-send-min-wei": "Min 1 Gwei", + "wallet-settings": "Ustawienia portfela", + "wallet-total-value": "Całkowita wartość", + "wallet-transaction-total-fee": "Całkowita opłata", + "wants-to-access-profile": "chce uzyskać dostęp do Twojego profilu", + "warning": "Ostrzeżenie", + "warning-message": "Niestety, ograniczamy wysyłanie kilku wiadomości w krótkich odstępach czasu, aby zapobiec spamowi. Spróbuj ponownie za chwilę", + "warning-sending-to-contract-descr": "Podany adres to smart contract, wysyłanie środków na ten adres może skutkować ich utratą. Aby wejść w interakcję ze zdecentralizowaną aplikacją, otwórz ją w przeglądarce Status dApp.", + "watch-only": "Tylko do oglądania", + "wc-brand-guide": "Wskazówki dotyczące używania brandingu, takiego jak znaki towarowe i logo", + "wc-disclaimer": "Zastrzeżenia (w tym dostawcy zewnętrzni), gwarancje i oświadczenia prawne", + "wc-dispute": "Postanowienia dotyczące rozstrzygania sporów", + "wc-how-to-use-status-app": "Jak korzystać z aplikacji Status, w tym zachowanie prywatności i bezpieczeństwa", + "wc-new-tos-based-on-principles-prefix": "Nowe Warunki Użytkowania zaprojektowane w oparciu o nasze", + "web-view-error": "Nie można załadować strony", + "websites": "Strony internetowe", + "webview-camera-permission-requests": "Prośby o pozwolenie na dostęp do kamery internetowej", + "webview-camera-permission-requests-subtitle": "Po włączeniu tej funkcji strony internetowe i zdecentralizowane aplikacje mogą poprosić o użycie kamery.", + "welcome-blank-community-message": "Twoje społeczności pojawią się tutaj.", + "welcome-blank-message": "Twoje czaty pojawią się tutaj. Aby rozpocząć nowe czaty, naciśnij przycisk ⊕", + "welcome-community-blank-message": "Tutaj pojawią się Twoje kanały. Aby utworzyć nowy kanał, kliknij przycisk ⊕ i wybierz \"Utwórz kanał\"", + "welcome-community-blank-message-edit-chats": "Tutaj pojawią się Twoje kanały. Aby utworzyć nowy kanał, wróć do ekranu społeczności, kliknij przycisk ⊕ i wybierz \"Utwórz kanał\"", + "welcome-screen-text": "Skonfiguruj swój portfel, zaproś znajomych na czat\n i przeglądaj popularne zdecentralizowane aplikacje!", + "welcome-to-status": "Witamy w Status!", + "welcome-to-status-description": "Skonfiguruj swój portfel kryptowalutowy, zaproś znajomych do czatu i przeglądaj zdecentralizowane aplikacje", + "what-changed": "Co się zmieniło", + "what-is-shared": "Co jest udostępnione", + "whats-on-your-mind": "Co ci chodzi po głowie...", + "word-count": "Liczba słów", + "word-n": "Słowo #{{number}}", + "word-n-description": "Aby sprawdzić, czy poprawnie zapisałeś kopię zapasową swojej frazy odzyskiwania (Seed), wprowadź słowo #{{number}} powyżej.", + "words-n": { + "one": "jedno", + "few": "kilka", + "many": "wiele", + "other": "inne" + }, + "write-down-and-store-securely": "Zapisz kody\n i przechowuj je bezpiecznie", + "wrong-address": "Nieprawidłowy adres", + "wrong-card": "Zła karta", + "wrong-card-text": "Wybrana karta nie odpowiada wybranym kluczom", + "wrong-contract": "Niewłaściwa kontrakt", + "wrong-keycard-text": "Wybrana karta Keycard nie jest powiązana z tym telefonem", + "wrong-keycard-title": "Wygląda na to, że kliknąłeś \nzłą kartę.", + "wrong-password": "Nieprawidłowe hasło", + "wrong-word": "Złe słowo", + "yes": "Tak", + "you": "ty", + "you-already-have-an-asset": "Masz już aktywo {{value}}", + "you-are-all-set": "Wszystko gotowe!", + "you-are-all-set-description": "Jeśli zgubisz swój telefon, możesz teraz uzyskać dostęp do swoich funduszy i klucza czatu, używając frazy Seed", + "you-can-change-account": "Możesz zmienić nazwę konta i kolor na taki, jaki chcesz.", + "you-can-choose-preview-websites": "Możesz wybrać, które z poniższych serwisów mogą wyświetlać podgląd linków do opisów i zdjęć w czatach", + "you-can-fetch": "Możesz pobrać historię czatu", + "you-dont-have-contacts": "Nie masz jeszcze żadnych kontaktów.", + "you-dont-have-contacts-invite-friends": "Nie masz jeszcze żadnych kontaktów.\nZaproś swoich znajomych do rozpoczęcia czatu", + "you-dont-have-stickers": "Nie masz jeszcze żadnych naklejek", + "you-will-need-this-code": "Będziesz potrzebować tego kodu, aby otworzyć aplikację Status i podpisywać transakcje", + "you-will-start-from-scratch": "Zaczniesz od początku z nowym zestawem kluczy", + "your-card-is-frozen": "Twoja karta Keycard jest zamrożona. Zresetuj dostęp do karty", + "your-contact-code": "Przyznanie dostępu upoważnia tą dAppkę do odzyskania klucza czatu", + "your-data-belongs-to-you": "Jeżeli zgubisz swoją frazę Seed, stracisz swoje dane i środki", + "your-data-belongs-to-you-description": "Jeśli utracisz dostęp, na przykład poprzez zgubienie swojego telefonu, możesz uzyskać dostęp do swoich kluczy tylko za pomocą frazy Seed. Nikt oprócz Ciebie nie ma Twojej frazy Seed. Zapisz ją. Przechowuj ją bezpiecznie", + "your-keys": "Twoje klucze", + "your-price-limit": "Twój limit ceny", + "your-recovery-phrase": "Twoja fraza Seed", + "your-recovery-phrase-description": "To jest Twoja fraza odzyskiwania (Seed). Używasz jej, aby udowodnić, że to Twój portfel. Możesz ją zobaczyć tylko raz! Zapisz ją na papierze i przechowuj w bezpiecznym miejscu. Będzie ona potrzebna, jeśli stracisz lub ponownie zainstalujesz Twój portfel.", + "your-tip-limit": "Twój limit napiwków", + "youre-on-mobile-network": "Korzystasz z sieci komórkowej" +} diff --git a/translations/pt.json b/translations/pt.json new file mode 100644 index 00000000000..8cee347327a --- /dev/null +++ b/translations/pt.json @@ -0,0 +1,1476 @@ +{ + "You": "Você", + "about-app": "Sobre", + "about-key-storage-content": "A Status nunca irá acessar sua chave privada. Não esqueça de salvar a sua frase. Se você perder seu telefone, essa é a única maneira de acessar suas chaves.", + "about-key-storage-title": "Sobre o armazenamento de chaves", + "about-names-content": "Ninguém pode fingir ser você! Você é anônimo por padrão e nunca precisa revelar seu nome real. Você pode registrar um nome personalizado por uma pequena taxa.", + "about-names-title": "Os nomes não podem ser alterados", + "accept": "Aceitar", + "accept-and-share-address": "Aceitar e compartilhar endereço", + "access-existing-keys": "Acessar chaves existentes", + "access-key": "Chave de acesso", + "account-added": "Conta adicionada", + "account-color": "Cor da conta", + "account-content": "Você pode comparar contas na Status com contas bancárias. Como uma conta bancária, uma conta normalmente possui um endereço e um saldo; Você usa essa conta para realizar transações no Ethereum. Você pode ter várias contas na sua carteira. Todas acessadas desbloqueando a Status.", + "account-exists-title": "A conta já existe", + "account-name": "Nome da conta", + "account-settings": "Configurações da conta", + "account-title": "Conta", + "accounts": "Contas", + "actions": "Ações", + "active-online": "Online", + "active-unknown": "Desconhecido", + "add": "Adicionar", + "add-a-watch-account": "Endereço somente leitura", + "add-account": "Adicionar conta", + "add-account-description": "Você pode importar qualquer tipo de conta Ethereum para adicioná-lo à sua carteira Status", + "add-account-incorrect-password": "A senha parece estar incorreta. Digite a senha que você usa para desbloquear o aplicativo.", + "add-an-account": "Adicione uma conta", + "add-bootnode": "Adicionar nó de inicialização", + "add-contact": "Adicionar contato", + "add-custom-token": "Adicionar token personalizado", + "add-favourite": "Adicionar favorito", + "add-mailserver": "Adicionar nó da Status", + "add-members": "Adicionar membros", + "add-network": "Adicionar rede", + "add-new-contact": "Adicionar novo contato", + "add-nickname": "Adicionar um apelido (opcional)", + "add-private-key-account": "Adicionar conta a partir de chave privada", + "add-seed-account": "Adicionar conta com uma frase-semente", + "add-to-contacts": "Adicionar aos contatos", + "add-to-contacts-text": "Ao adicionar um usuário à sua lista de contatos, você compartilha o endereço da sua carteira", + "add-to-favourites": "Adicionar aos favoritos", + "add-watch-account": "Adicionar uma conta somente para exibição", + "address": "Endereço", + "address-or-ens-name": "Endereço ou nome ENS", + "address-received": "Endereço recebido", + "address-request-accepted": "Solicitação de endereço aceita", + "address-requested": "Endereço solicitado", + "advanced": "Avançado", + "advanced-settings": "Configurações avançadas", + "advertiser-description": "Você descobriu a Status graças a um parceiro. Você se importa se a Status verificar seu endereço IP uma vez para que ele seja recompensado? Esta informação não será usada para mais nada e será removida completamente após 7 dias.", + "advertiser-starter-pack-accept": "Aceitar", + "advertiser-starter-pack-decline": "Recusar", + "advertiser-starter-pack-description": "Aqui estão algumas criptografias para você começar! Use-o para obter adesivos, um nome ENS e experimente os dapps", + "advertiser-starter-pack-title": "Pacote Inicial", + "advertiser-title": "Privacidade por padrão", + "agree-by-continuing": "Ao continuar, você concorda \n com nosso", + "all": "Todos", + "allow": "Permitir", + "allow-mention-notifications": "Mostrar @ menções", + "allowing-authorizes-this-dapp": "Permitir que este DApp recupere o endereço da sua carteira e ative o Web3", + "already-have-asset": "Você já tem esse recurso", + "amount": "Quantia", + "appearance": "Aparência", + "apply": "Aplicar", + "are-you-sure": "Você tem certeza?", + "are-you-sure-description": "Você não poderá ver a frase-semente inteira novamente", + "are-you-sure-to-cancel": "Você tem certeza que deseja cancelar?", + "are-you-sure?": "Você tem certeza?", + "ask-in-status": "Faça uma pergunta ou relate um bug", + "at": "em", + "attribution-received": "{{attrib}} de {{max}} bônus recebidos", + "audio": "Áudio", + "audio-recorder": "Gravador", + "audio-recorder-error": "Erro no gravador", + "audio-recorder-max-ms-reached": "Tempo máximo de gravação atingido", + "audio-recorder-permissions-error": "Você precisa dar permissão para enviar mensagens de áudio", + "authorize": "Autorizar", + "available": "Disponível", + "available-participants": { + "one": "Você pode selecionar mais um participante", + "other": "Você pode selecionar mais {{count}} participantes" + }, + "back": "Voltar", + "back-up": "Cópia de segurança", + "back-up-seed-phrase": "Fazer backup da frase-semente", + "back-up-your-seed-phrase": "Fazer backup da frase-semente", + "backup-disabled": "Desativado", + "backup-recovery-phrase": "Cópia de segurança da frase-semente", + "balance": "Saldo", + "begin-set-up": "Iniciar configuração", + "biometric-auth-android-sensor-desc": "Toque no sensor", + "biometric-auth-android-sensor-error-desc": "Falhou", + "biometric-auth-android-title": "Autenticação Necessária", + "biometric-auth-confirm-logout": "Reentrar", + "biometric-auth-confirm-message": "A autenticação biométrica é necessária para continuar, se não for possível, desbloqueie suas chaves com sua senha ou código de acesso", + "biometric-auth-confirm-title": "Você deve autenticar!", + "biometric-auth-confirm-try-again": "Tente novamente", + "biometric-auth-error": "Não foi possível executar a autenticação biométrica ( {{code}} )", + "biometric-auth-login-error-title": "Erro de autenticação biométrica", + "biometric-auth-login-ios-fallback-label": "Digite a senha", + "biometric-auth-reason-login": "Login na Status", + "biometric-auth-reason-verify": "Verificar autenticação", + "biometric-disable-bioauth": "desativar {{bio-type-label}}", + "biometric-disable-password-description": "Se você desativar isso, você também irá ", + "biometric-disable-password-title": "Desativar senha salva", + "biometric-enable": "Se você não quiser inserir sua senha todas às vezes para acessar o aplicativo, ative o início de sessão por {{bio-type-label}}", + "biometric-enable-button": "Ativar {{bio-type-label}}", + "biometric-enable-keycard": "Se você não quiser usar seu Keycard cada vez que acessar o aplicativo, ative {{bio-type-label}} para iniciar a sessão", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Impressão digital", + "biometric-secure-with": "Proteger com {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "blank-keycard-text": "Você pode continuar com o seu depois de gerar as chaves e o nome", + "blank-keycard-title": "Parece que você selecionou\num Keycard em branco.", + "block": "Bloquear ", + "block-contact": "Bloqueie esse usuário", + "block-contact-details": "O bloqueio excluirá as mensagens anteriores deste usuário e impedirá que novas cheguem até você", + "blocked-users": "Usuários bloqueados", + "bootnode-address": "Endereço do nó de inicialização", + "bootnode-details": "Detalhes do nó de inicialização", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Nós de inicialização", + "bootnodes-enabled": "Nós de inicialização ativados", + "bootnodes-settings": "Configurações de nós de inicialização", + "browsed-websites": "O histórico do navegador aparecerá aqui", + "browser": "Navegador", + "browser-not-secure": "A conexão não é segura! Não assine transações nem envie dados pessoais neste site.", + "browser-secure": "A conexão é segura. Confie realmente neste site antes de assinar transações ou inserir dados pessoais.", + "browsers": "Navegadores", + "browsing-cancel": "Cancelar", + "browsing-open-in-android-web-browser": "Abrir no Android", + "browsing-open-in-ios-web-browser": "Abrir no iOS", + "browsing-open-in-status": "Abrir em Status", + "browsing-site-blocked-description1": "Detectamos atividades maliciosas em potencial neste endereço. Para proteger você e sua carteira, estamos impedindo mais navegação. \n\nSe você acha que isso é um erro, informe-nos no", + "browsing-site-blocked-description2": " conversa pública.", + "browsing-site-blocked-go-back": "Voltar", + "browsing-site-blocked-title": "Este site está bloqueado", + "browsing-title": "Buscar", + "bug-report": "Relatar uma falha", + "buy-crypto": "Comprar criptomoedas", + "buy-crypto-choose-a-service": "Escolha um serviço que você gostaria de utilizar para comprar criptomoedas", + "buy-crypto-description": "Encontre um dapp para comprar criptomoedas agora", + "buy-crypto-leaving": "Você está saindo da Status e entrando em um site de terceiros para concluir sua compra", + "buy-crypto-title": "Parece que sua carteira está vazia", + "camera-access-error": "Para conceder a permissão de câmera necessária, vá para as configurações do sistema e verifique se Status > Câmera está selecionado.", + "can-not-add-yourself": "É você, para iniciar um bate-papo, escolha outra pessoa", + "can-send-messages": "Você pode enviar e receber novas mensagens", + "cancel": "Cancelar", + "cancel-keycard-setup": "Cancelar a configuração do Keycard ", + "cancelling": "Cancelando", + "cannot-read-card": "Não é possível ler o cartão. Por favor, segure-o na parte de trás do seu telefone", + "cannot-use-default-pin": "A senha 000000 não é permitida. \nPor favor, use outra senha", + "cant-open-public-chat": "Não é possível abrir este chat público", + "cant-report-bug": "Não é possível relatar um bug", + "card-is-blank": "Este cartão está em branco", + "card-reseted": "O cartão foi redefinido", + "card-unpaired": "Cartão foi desemparelhado do dispositivo atual", + "change-fleet": "Mude a frota para {{fleet}}", + "change-logging-enabled": "Tem certeza de que deseja {{enable}} logs?", + "change-passcode": "Alterar código de acesso", + "change-password": "Alterar senha", + "change-pin": "Alterar senha de 6 dígitos", + "changed-amount-warning": "O valor foi alterado de {{old}} para {{new}}", + "changed-asset-warning": "O ativo foi alterado de {{old}} para {{new}}", + "chaos-mode": "Modo caos", + "chaos-unicorn-day": "Dia do Caos do Unicórnio", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Bate-papo", + "chat-and-transact": "Converse e faça transações privadamente com amigos", + "chat-is-a-contact": "Contato", + "chat-is-not-a-contact": "Não é um contato", + "chat-key": "Chave de bate-papo", + "chat-key-content": "As mensagens no protocolo de bate-papo Status são enviadas e recebidas usando chaves de criptografia. A chave de bate-papo público é uma sequência de caracteres que você compartilha com outras pessoas para que elas possam enviar mensagens para você em Status.", + "chat-key-title": "Chave de bate-papo", + "chat-link-previews": "Pré-visualizações de links de chat", + "chat-name": "Nome do chat", + "chat-name-content": "Três palavras aleatórias, derivadas algoritmicamente da sua chave de bate-papo e usadas como apelido padrão no bate-papo. Os nomes de bate-papo são completamente únicos; nenhum outro usuário pode ter as mesmas três palavras.", + "chat-name-title": "Nome no bate-papo", + "chat-notification-preferences": "Configurações de notificação", + "chat-settings": "Configurações do chat", + "chats": "Chats", + "check-on-opensea": "Confira no opensea", + "check-your-recovery-phrase": "Verifique sua palavra semente", + "choose-actions": "Escolha ações", + "choose-authentication-method": "Escolha um método de autenticação", + "choose-new-location-for-keystore": "Escolha um novo local para salvar seu arquivo de armazenamento de chaves", + "choose-storage": "Escolha o armazenamento", + "clear": "Limpar", + "clear-all": "Limpar tudo", + "clear-history": "Apagar histórico", + "clear-history-action": "Limpar", + "clear-history-confirmation": "Apagar o histórico?", + "clear-history-confirmation-content": "Tem certeza de que deseja limpar este histórico de bate-papo?", + "clear-history-title": "Apagar o histórico?", + "close": "Fechar", + "close-all": "Fechar tudo", + "close-app-button": "Confirmar", + "close-app-content": "O aplicativo irá parar e fechar. Quando você reabri-lo, a rede selecionada será usada", + "close-app-title": "Atenção!", + "command-button-send": "Enviar", + "communities": "Comunidades", + "communities-alpha": "Comunidades (alfa)", + "communities-verified": "✓ Comunidade da Status verificada", + "community-color": "Cor da comunidade", + "community-color-placeholder": "Escolha uma cor", + "community-edit-title": "Editar comunidade", + "community-image-pick": "Escolha uma imagem", + "community-image-remove": "Remover", + "community-image-take": "Tirar foto", + "community-invite-title": "Convidar", + "community-key": "Chave privada da comunidade", + "community-key-placeholder": "Digite a chave privada da sua comunidade", + "community-members": { + "one": "{{count}} membro", + "other": "{{count}} membros" + }, + "community-members-title": "Membros", + "community-private-key": "Chave privada da comunidade", + "community-requests-to-join-title": "Pedidos de participação", + "community-roles": "Funções", + "community-share-title": "Compartilhar", + "community-thumbnail-image": "Imagem em miniatura", + "community-thumbnail-upload": "Upload", + "complete-hardwallet-setup": "Este cartão agora está vinculado. Você precisa dele para assinar transações e desbloquear suas chaves", + "completed": "Concluído", + "confirm": "Confirmar", + "confirm-password-placeholder": "Confirme sua senha…", + "confirmation-request": "Pedido de confirmação", + "confirmations": "Confirmações", + "confirmations-helper-text": "Quando a transação tiver 12 confirmações, você poderá considerá-la liquidada.", + "connect": "Conectar", + "connect-mailserver-content": "Conectar-se a {{name}} ?", + "connect-wallet": "Conectar carteira", + "connected": "Conectado", + "connected-to": "Conectado a", + "connecting": "Conectando...", + "connecting-requires-login": "Conectar-se a outra rede requer login", + "connection-status": "Status de conexão", + "connection-with-the-card-lost": "Conexão com o cartão \n foi perdido", + "connection-with-the-card-lost-setup-text": "Para retomar a configuração, segure o cartão na\n parte de trás do seu telefone e mantenha \n cartão para contato telefônico", + "connection-with-the-card-lost-text": "Para continuar, segure o cartão na parte de trás do seu telefone", + "contact-code": "Chave de contato", + "contact-s": { + "one": "contato", + "other": "contatos" + }, + "contacts": "Contatos", + "contacts-descr": "Seus contatos aparecerão aqui. Você receberá atualizações de status de qualquer pessoa que adicionar como contato", + "contacts-empty": "Contatos com nomes ENS aparecerão aqui", + "continue": "Continuar", + "continue-anyway": "Continuar mesmo assim", + "contract-address": "Endereço do contrato", + "contract-interaction": "Interação contratual", + "copy-info": "Copiar informação", + "copy-qr": "Copiar código", + "copy-to-clipboard": "Copiar", + "copy-transaction-hash": "Copiar ID da transação", + "cost-fee": "Custo\/Taxa", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Criar", + "create-a-pin": "Crie uma senha de 6 dígitos", + "create-channel": "Criar um canal", + "create-channel-title": "Novo canal", + "create-community": "Criar uma comunidade", + "create-group-chat": "Criar sala de conversação do grupo", + "create-multiaccount": "Gerar chaves", + "create-new-key": "Obter novas chaves", + "create-pin": "Criar senha de 6 dígitos", + "create-pin-description": "Você precisará do seu cartão + esta senha de 6 dígitos para desbloquear a Status e confirmar transações", + "created-group-chat-description": "Você criou o grupo {{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Moeda", + "currency-display-name-aed": "Dirham de los Emiratos Árabes Unidos", + "currency-display-name-afn": "Afeganistão Afegão", + "currency-display-name-ars": "Peso argentino", + "currency-display-name-aud": "Dólar australiano", + "currency-display-name-bbd": "Dólar de Barbados", + "currency-display-name-bdt": "Taka do Bangladesh", + "currency-display-name-bgn": "Lev búlgaro", + "currency-display-name-bhd": "Dinar do Bahrein", + "currency-display-name-bnd": "Dólar de Brunéi", + "currency-display-name-bob": "Boliviano", + "currency-display-name-brl": "Real do Brasil", + "currency-display-name-btn": "Ngultrum butanés", + "currency-display-name-cad": "Dólar canadiense", + "currency-display-name-chf": "Franco suíço", + "currency-display-name-clp": "Peso chileno", + "currency-display-name-cny": "China Yuan Renminbi", + "currency-display-name-cop": "Peso colombiano", + "currency-display-name-crc": "Cólon da Costa Rica", + "currency-display-name-czk": "Corona checa", + "currency-display-name-dkk": "Coroa da Dinamarca", + "currency-display-name-dop": "Peso da República Dominicana", + "currency-display-name-egp": "Libra egipcia", + "currency-display-name-etb": "Birr etíope", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Libra esterlina", + "currency-display-name-gel": "Lari georgiano", + "currency-display-name-ghs": "Ghana Cedi", + "currency-display-name-hkd": "Dólar de Hong Kong", + "currency-display-name-hrk": "Kuna da Croácia", + "currency-display-name-huf": "Forint da Hungria", + "currency-display-name-idr": "Rupias indonésias", + "currency-display-name-ils": "Shekel de Israel", + "currency-display-name-inr": "Rupia india", + "currency-display-name-isk": "Coroa da Islândia", + "currency-display-name-jmd": "Dólar jamaiquino", + "currency-display-name-jpy": "Yen japonês", + "currency-display-name-kes": "Chelín keniano", + "currency-display-name-krw": "Won sul-coreano", + "currency-display-name-kwd": "Dinar do Kuwait", + "currency-display-name-kzt": "Tenge kazajo", + "currency-display-name-lkr": "Rupia de Sri Lanka", + "currency-display-name-mad": "Dirham marroquino", + "currency-display-name-mdl": "Leu da Moldávia", + "currency-display-name-mur": "Rúpia das Maurícias", + "currency-display-name-mwk": "Kwacha do Malawi", + "currency-display-name-mxn": "Peso mexicano", + "currency-display-name-myr": "Ringgit malayo", + "currency-display-name-mzn": "Metical mozambiqueño", + "currency-display-name-nad": "Dólar namibio", + "currency-display-name-ngn": "Naira (Nigeria)", + "currency-display-name-nok": "Corona noruega", + "currency-display-name-npr": "Rupia nepalí", + "currency-display-name-nzd": "Dólar da Nova Zelândia", + "currency-display-name-omr": "Rial de Omã", + "currency-display-name-pen": "Sol peruano", + "currency-display-name-pgk": "Kina Papua-Nova Guiné", + "currency-display-name-php": "Peso das Filipinas", + "currency-display-name-pkr": "Rupia pakistaní", + "currency-display-name-pln": "Zloty (Polonia)", + "currency-display-name-pyg": "Guaraní (Paraguay)", + "currency-display-name-qar": "Rial do Qatar", + "currency-display-name-ron": "Leu da Roménia", + "currency-display-name-rsd": "Dinar da Sérvia", + "currency-display-name-rub": "Rublo da Rússia", + "currency-display-name-sar": "Riyal da Arábia Saudita", + "currency-display-name-sek": "Coroa da Suécia", + "currency-display-name-sgd": "Dólar de Singapura", + "currency-display-name-thb": "Baht da Tailândia", + "currency-display-name-try": "Lira turca", + "currency-display-name-ttd": "Dólar de Trinidad e Tobago", + "currency-display-name-twd": "Dólar Novo de Taiwan", + "currency-display-name-tzs": "Chelín tanzano", + "currency-display-name-uah": "Ucrânia Hryvnia", + "currency-display-name-ugx": "Xelim ugandês", + "currency-display-name-usd": "Dólar dos Estados Unidos", + "currency-display-name-uyu": "Peso uruguayo", + "currency-display-name-vef": "Bolívar venezolano", + "currency-display-name-vnd": "Dong vietnamita", + "currency-display-name-zar": "Rand sul-africano", + "current": "Atual", + "current-network": "Rede atual", + "current-pin": "Digite a senha de 6 dígitos", + "current-pin-description": "Digite sua senha de 6 dígitos para continuar", + "custom": "Personalizado", + "custom-networks": "Redes personalizadas", + "custom-node": "Você está usando um endpoint RPC personalizado. Seu histórico de transferências locais pode estar incompleto.", + "custom-seed-phrase": "Frase-semente inválida", + "custom-seed-phrase-text-1": "Esta frase inicial não corresponde ao nosso dicionário suportado. Verifique se há palavras com erros ortográficos.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Aceitar e Abrir", + "dapp-starter-pack-description": "Aqui estão algumas criptografias para você começar! Use-o para obter adesivos, um nome ENS e experimente os dapps", + "dapp-starter-pack-title": "Pacote Inicial", + "dapp-would-like-to-connect-wallet": "gostaria de se conectar a", + "dapps": "ÐApps", + "dapps-permissions": "Permissões do DApp", + "dark": "Escuro", + "data": "Dados", + "data-syncing": "Sincronização de dados", + "database-reset-warning": "O banco de dados será redefinido. Chats, contatos e configurações serão excluídos", + "datetime-ago": "atrás", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "dia", + "other": "dias" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "hora", + "other": "horas" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "minuto", + "other": "minutos" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "segundo", + "other": "segundos" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "hoje", + "datetime-yesterday": "ontem", + "decimals": "Decimais", + "decline": "Recusar", + "decryption-failed-content": "Ocorreu um erro ao descriptografar seus dados. Você pode precisar apagar seus dados antigos e gerar uma nova conta. Toque em “Aplicar” para apagar ou “Cancelar” para tentar novamente", + "default": "Padrão", + "default-assets": "ERC20 e ERC721 padrão", + "delete": "Excluir", + "delete-account": "Remover conta", + "delete-and-leave-group": "Excluir e sair do grupo", + "delete-bootnode": "Excluir nó de inicialização", + "delete-bootnode-are-you-sure": "Tem certeza de que deseja excluir este nó de inicialização?", + "delete-bootnode-title": "Eliminar bootnode", + "close-chat": "Excluir conversa", + "close-chat-confirmation": "Tem certeza de que deseja excluir este bate-papo?", + "delete-confirmation": "Excluir?", + "delete-mailserver": "Excluir nó da Status", + "delete-mailserver-are-you-sure": "Tem certeza de que deseja excluir este nó da Status?", + "delete-mailserver-title": "Excluir nó da Status", + "delete-message": "Apagar mensagem", + "delete-my-account": "Deletar minha conta", + "delete-my-profile": "Excluir meu perfil", + "delete-network-confirmation": "Tem certeza de que deseja excluir esta rede?", + "delete-network-error": "Conecte-se a uma rede diferente antes de exclui-la", + "delete-network-title": "Excluir rede?", + "delete-profile": "Excluir perfil", + "delete-profile-warning": "Aviso: Se você não tiver sua frase semente escrita, você perderá o acesso aos seus fundos depois de excluir seu perfil", + "deny": "Negar", + "derivation-path": "Caminho de derivação", + "description": "Descrição", + "dev-mode": "Modo de desenvolvimento", + "dev-mode-settings": "Configurações do modo de desenvolvimento", + "device-syncing": "Sincronização de dispositivos", + "devices": "Dispositivos", + "disable": "desativar", + "disabled": "Desativado", + "disconnected": "Bate-papo offline", + "discover": "Descobrir", + "dismiss": "Dispensar", + "done": "Concluído", + "dont-ask": "Não me pergunte novamente", + "edit": "Editar", + "edit-chats": "Editar chats", + "edit-community": "Editar comunidade", + "edit-favourite": "Editar favorito", + "edit-group": "Editar grupo", + "edit-profile": "Editar perfil", + "empty-chat-description": "Não há mensagens \n neste bate-papo ainda", + "empty-chat-description-community": "Tem estado quieto aqui nas últimas {{quiet-hours}}.", + "empty-chat-description-one-to-one": "Todas as mensagens que você envia aqui são criptografadas e só podem ser lidas por você e ", + "empty-chat-description-public": "Está quieto aqui nas últimas {{quiet-hours}}. Inicie a conversa ou ", + "empty-chat-description-public-share-this": "compartilhe a sala.", + "empty-keycard-required": "Requer um Keycard vazio", + "empty-pending-invitations-descr": "Pessoas que desejam entrar no grupo \n através de um link de convite aparecerão aqui", + "empty-tab": "Aba vazia", + "enable": "Ativar", + "enable-all": "Habilitar todos", + "enable-link-previews": "Ativar visualizações de link no chat?", + "encrypt-with-password": "Criptografar com senha", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Adicionar nome do usuário ", + "ens-agree-to": "Concordar com", + "ens-chat-settings": "Configurações de bate-papo", + "ens-custom-domain": "Domínio personalizado", + "ens-custom-username-hints": "Digite o nome de usuário inteiro, incluindo o domínio personalizado, como nomedeusuario.domínio.eth", + "ens-custom-username-taken": "O nome de usuário não pertence a você :(", + "ens-deposit": "Depósito", + "ens-dismiss-message": "Clique aqui para fechar", + "ens-displayed-with": "Suas mensagens são exibidas para outras pessoas com", + "ens-get-name": "Obter nome universal", + "ens-got-it": "Ok, entendi.", + "ens-locked": "Nome de usuário bloqueado. Você não poderá liberá-lo até {{date}}", + "ens-name-content": "Apelido personalizado para sua chave de bate-papo que você pode registrar usando o Ethereum Name Service. Os nomes do ENS são nomes de usuário descentralizados.", + "ens-name-not-found": "Não é possível resolver o nome ENS", + "ens-name-title": "Nome ENS", + "ens-network-restriction": "Disponível apenas na Mainnet", + "ens-no-usernames": "Você não tem nenhum nome de usuário conectado", + "ens-powered-by": "Desenvolvido com Ethereum Name Services", + "ens-primary-username": "Nome de usuário principal", + "ens-register": "Registre-se", + "ens-registration-failed": "Para registrar o nome do usuário, tente novamente.", + "ens-registration-failed-title": "Falha na transação", + "ens-registration-failure": "Erro ao registrar", + "ens-registration-in-progress": "Registrando…", + "ens-release-username": "Liberar nome de usuário", + "ens-remove-hints": "A remoção desanexará o nome de usuário da sua chave.", + "ens-remove-username": "Remover nome do usuário", + "ens-saved": " agora está conectado com sua chave de bate-papo e pode ser usado em Status.", + "ens-saved-title": "Nome de usuário adicionado", + "ens-show-username": "Mostrar meu nome de usuário ENS em chats", + "ens-terms-header": "Termos de registro de nome", + "ens-terms-point-1": "Os fundos são depositados por 1 ano. Seu SNT será bloqueado, mas não será gasto.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Registro ENS).", + "ens-terms-point-2": "Após 1 ano, você pode liberar o nome e receber seu depósito de volta ou não tomar nenhuma ação para mantê-lo.", + "ens-terms-point-3": "Se os termos do contrato mudarem — por exemplo, a Status faz atualizações do contrato — o usuário tem o direito de liberar o nome de usuário, independentemente do tempo de espera.", + "ens-terms-point-4": "O controlador do contrato não pode acessar seus fundos depositados. Eles só podem ser movidos de volta para o endereço que os enviou.", + "ens-terms-point-5": "Seus endereços serão publicamente associados ao seu nome no ENS.", + "ens-terms-point-6": "Os nomes de usuários são criados como nós de subdomínio stateofus.eth e estão sujeitos aos termos do contrato inteligente do ENS.", + "ens-terms-point-7": "Você autoriza o contrato a transferir o SNT em seu nome. Isso só pode ocorrer quando você aprovar uma transação para autorizar a transferência.", + "ens-terms-point-8": "Estes termos são garantidos pela lógica do contrato inteligente nos endereços:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Termos de registo do nome.", + "ens-test-message": "Olá", + "ens-transaction-pending": "Transação pendente ...", + "ens-understand": "Entendo que meu endereço de carteira estará publicamente conectado ao meu usuário.", + "ens-username": "Nome de usuário ENS", + "ens-username-already-added": "O nome de usuário já está conectado à sua chave de bate-papo e pode ser usado dentro da Status.", + "ens-username-available": "✓ Nome de usuário disponível!", + "ens-username-connected": "Esse nome de usuário é de sua propriedade e conectado à sua chave de bate-papo.", + "ens-username-connected-continue": "Continue configurando 'Mostrar meu nome de usuário do ENS nos bate-papos'.", + "ens-username-connected-with-different-key": "Continuar exigirá uma transação para conectar o nome de usuário com sua chave de bate-papo atual.", + "ens-username-connection-confirmation": "{{username}} será conectado assim que a transação for concluída.", + "ens-username-hints": "Pelo menos 4 caracteres. Usando letras minúsculas e números.", + "ens-username-invalid": "Apenas letras e números.", + "ens-username-owned": "✓ O nome do usuário é seu.", + "ens-username-owned-continue": "Continuar conectará esse nome de usuário com sua chave de bate-papo.", + "ens-username-registration-confirmation": "Ótimo! Você será o proprietário do {{username}} assim que a transação for concluída.", + "ens-username-taken": "Nome de usuário já utilizado :(", + "ens-username-you-can-follow-progress": "Você pode acompanhar o progresso na seção Histórico de Transações da sua carteira.", + "ens-usernames": "Nomes de usuário ENS", + "ens-usernames-details": "Registre um nome de usuário universal para ser facilmente reconhecido por outros usuários", + "ens-want-custom-domain": "Eu possuo um nome em outro domínio", + "ens-want-domain": "Eu quero um domínio stateofus.eth", + "ens-welcome-hints": "Os nomes do ENS transformam esses endereços malucos em nomes de usuário exclusivos.", + "ens-welcome-point-customize": "Um nome ENS pode substituir seu pseudo-nome aleatório de três palavras no chat. Seja @seunome em vez de {{name}} .", + "ens-welcome-point-customize-title": "Personalize seu nome de bate-papo", + "ens-welcome-point-receive": "Outros podem enviar fundos via chat em uma única etapa.", + "ens-welcome-point-receive-title": "Receba transações no chat", + "ens-welcome-point-register": "Registre-se uma vez para manter o nome para sempre. Após 1 ano, você pode liberar o nome e recuperar seu SNT.", + "ens-welcome-point-register-title": "10 SNT para se registar", + "ens-welcome-point-simplify": "Você pode receber fundos em um nome ENS fácil de compartilhar, em vez do seu endereço hexadecimal (0x ...).", + "ens-welcome-point-simplify-title": "Simplifique seu endereço ETH", + "ens-welcome-point-verify": "Você pode verificar e adicionar qualquer nome de usuário que você possui nos próximos passos.", + "ens-welcome-point-verify-title": "Já possui um nome de usuário?", + "ens-your-username": "Seu nome de usuário", + "ens-your-usernames": "Seus nomes de usuários", + "ens-your-your-name": "Seu nome ENS", + "enter-12-words": "Digite as 12 palavras da sua frase inicial, separadas por espaços únicos", + "enter-a-private-key": "Digite uma chave privada", + "enter-a-seed-phrase": "Digite uma frase-semente", + "enter-address": "Inserir endereço", + "enter-contact-code": "Insira o nome de usuário ENS ou a chave do chat", + "enter-pair-code": "Digite seu código de emparelhamento", + "enter-pair-code-description": "O código de emparelhamento foi exibido durante a configuração do Keycard", + "enter-password": "Digite a senha", + "enter-pin": "Digite a senha de 6 dígitos", + "enter-puk-code": "Digite o código PUK", + "enter-puk-code-description": "A senha de 6 dígitos foi bloqueada. \n Digite o código PUK para desbloquear a senha.", + "enter-recipient-address-or-username": "Digite o endereço ou nome de usuário do destinatário", + "enter-seed-phrase": "Inserir frase-semente", + "enter-url": "Inserir URL", + "enter-user-pk": "Insira a chave pública do usuário", + "enter-watch-account-address": "Digitalize um código QR \nou \ncoloque o endereço para monitorar", + "enter-word": "Digite a palavra", + "enter-your-code": "Digite sua senha de 6 dígitos", + "enter-your-password": "Coloque sua senha", + "error": "Erro", + "error-unable-to-get-balance": "Não foi possível obter o saldo", + "error-unable-to-get-prices": "Erro de conversão de moeda. Atualize sua tela para tentar novamente.", + "error-unable-to-get-token-balance": "Não foi possível obter o saldo do token", + "errors": "Erros", + "eth": "ETH", + "ethereum-account": "Conta Ethereum", + "ethereum-address": "Endereço Ethereum", + "ethereum-node-started-incorrectly-description": "O nó Ethereum foi iniciado com a configuração incorreta; o aplicativo será parado para se recuperar dessa condição. ID de rede configurado = {{network-id}} , real = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Nó Ethereum iniciado incorretamente", + "etherscan-lookup": "Procura no Etherscan", + "everyone": "Todos", + "export-account": "Exportar conta", + "export-key": "Exportar chave privada", + "external-storage-denied": "Acesso ao armazenamento externo negado", + "failed": "Falhou", + "faq": "Perguntas frequentes", + "favourite": "Favoritos", + "favourite-description": "Seus sites favoritos aparecerão aqui", + "favourites": "Favoritos", + "favourites-empty": "Os endereços adicionados aos favoritos aparecerão aqui", + "fetch-messages": "↓ Buscar mensagens", + "fetch-timeline": "↓ Buscar", + "find": "Encontrar", + "finish": "Terminar", + "finishing-card-setup": "Finalizando configurações do cartão", + "fleet": "Frota", + "fleet-settings": "Configurações de frota", + "follow": "Seguir", + "follow-your-interests": "Entre em uma conversa pública e conheça novas pessoas", + "free": "↓ Grátis", + "from": "De", + "gas-limit": "Limite de gás", + "gas-price": "Preço do gás", + "gas-used": "Gás usado", + "generate-a-key": "Gerar chaves", + "generate-a-new-account": "Gerar uma nova conta", + "generate-a-new-key": "Gere uma nova chave", + "generate-account": "Gerar chaves", + "generate-an-account": "Gerar uma nova conta", + "generate-new-key": "Gerar chaves", + "generating-codes-for-pairing": "> Download do software do produto no cartão \n> Gerando códigos de desbloqueio e emparelhamento", + "generating-keys": "Gerando chaves...", + "generating-mnemonic": "Gerando frase-semente", + "get-a-keycard": "Obtenha um Keycard", + "get-started": "Começar", + "get-status-at": "Obter status em http:\/\/status.im", + "get-stickers": "Obter Adesivos", + "give-a-short-description": "Dê uma breve descrição", + "give-a-short-description-community": "Dê uma breve descrição", + "give-permissions-camera": "Dar permissão\npara acessar a câmera", + "glossary": "Glossário", + "go-to-settings": "Vá para as configurações...", + "got-it": "Entendido", + "grant-face-id-permissions": "Para conceder a permissão de Face ID necessária, vá para as configurações do seu sistema e certifique-se de que Status > Face ID está selecionado", + "group-chat": "Conversa em grupo", + "group-chat-admin": "Administrador", + "group-chat-admin-added": "**{{member}}** foi adicionado como administrador", + "group-chat-all-contacts-invited": "Todos os seus contatos já estão no grupo", + "group-chat-created": "**{{member}}** criou o grupo **{{name}}**", + "group-chat-decline-invitation": "Recusar o convite", + "group-chat-member-added": "**{{member}}** foi convidado", + "group-chat-member-joined": "**{{member}}** acessou o grupo", + "group-chat-member-removed": "**{{member}}** saiu do grupo", + "group-chat-members-count": "{{selected}}\/{{max}} membros", + "group-chat-name-changed": "**{{member}}** mudou o nome do grupo para **{{name}}**", + "group-chat-no-contacts": "Você não possui contatos.\nAdicione contatos para iniciar a conversa.", + "group-info": "Informações do grupo", + "group-invite": "Convite para o grupo", + "group-invite-link": "Link de convite do grupo", + "group-membership-request": "Pedido para entrar no grupo", + "gwei": "Gwei", + "has-permissions": "tem permissão para acessar", + "hash": "Hash", + "help": "ajuda", + "help-capitalized": "Ajuda", + "help-center": "Centro de ajuda", + "hide-content-when-switching-apps": "Bloquear capturas de tela", + "hide-content-when-switching-apps-ios": "Ocultar visualização", + "history": "Histórico", + "history-nodes": "Nós de mensagem da Status", + "hold-card": "Segure o cartão na parte de trás \n do seu telefone", + "home": "Início", + "hooks": "Hooks", + "how-it-works": "Como funciona", + "http-gateway-error": "Ops, a solicitação falhou!", + "identifier": "Identificador", + "image": "Imagem", + "image-remove-current": "Remover foto atual", + "image-source-gallery": "Escolher na galeria", + "image-source-make-photo": "Tirar foto", + "image-source-title": "Editar imagem", + "import": "Importar", + "import-community": "Importar uma comunidade", + "import-community-title": "Importar uma comunidade", + "in-contacts": "Em contatos", + "incoming": "Entrada", + "incoming-transaction": "Transação recebida", + "incorrect-code": [ + "str", + "Desculpe, o código estava incorreto. Por favor, digite novamente" + ], + "increase-gas": "Aumentar o gás", + "initialization": "Inicialização", + "install": "Instalar", + "intro-message1": "Bem-vindo à Status\nToque nesta mensagem para definir sua senha e começar!", + "intro-privacy-policy-note1": "A Status não coleta ou lucra com seus dados pessoais. Ao continuar, você concorda com a ", + "intro-privacy-policy-note2": "política de privacidade", + "intro-text": "Status é a sua porta de entrada para a web descentralizada", + "intro-text1": "Converse em uma rede criptografada ponto a ponto, onde as mensagens não podem ser censuradas ou invadidas", + "intro-text2": "Envie e receba ativos digitais em qualquer lugar do mundo - não é necessária uma conta bancária", + "intro-text3": "Explore jogos, trocas e redes sociais onde você é o proprietário dos seus dados", + "intro-title1": "Comunicação verdadeiramente privada", + "intro-title2": "Carteira criptográfica segura", + "intro-title3": "Aplicativos descentralizados", + "intro-wizard-text1": "Um conjunto de chaves controla sua conta. Suas chaves estão no seu telefone, então somente você pode usá-las", + "intro-wizard-text2": "Uma chave é para conversar. Ele vem com um nome legível que não pode ser alterado.", + "intro-wizard-text3": "Se você possui um Keycard, guarde lá as suas chaves para maior segurança.", + "intro-wizard-text4": "Proteja e criptografe suas chaves", + "intro-wizard-text6": "A Status o notificará sobre novas mensagens. Você pode editar suas preferências de notificação posteriormente nas configurações", + "intro-wizard-title-alt4": "Crie uma senha", + "intro-wizard-title-alt5": "Confirme sua senha", + "intro-wizard-title1": "Pegue suas chaves", + "intro-wizard-title2": "Escolha um nome de bate-papo", + "intro-wizard-title3": "Escolha o armazenamento de chaves", + "intro-wizard-title4": "Crie uma senha de 6 dígitos", + "intro-wizard-title5": "Confirme a senha", + "intro-wizard-title6": "Ativar notificações", + "introduce-yourself": "Apresente-se com uma breve mensagem", + "invalid-address-qr-code": "O código QR digitalizado não contém um endereço válido", + "invalid-format": "Formato Inválido \n Deve ser {{format}}", + "invalid-key-confirm": "Aplicar", + "invalid-key-content": "O banco de dados não pode ser criptografado porque um arquivo está corrompido. Seus fundos e a chave de bate-papo estão seguros. Outros dados, como seus chats e contatos, não podem ser restaurados. O botão \"{{erase-multiaccounts-data-button-text}}\" removerá todos os outros dados e permitirá que você acesse seus fundos e envie mensagens", + "invalid-number": "Número inválido", + "invalid-pairing-password": "Senha de emparelhamento inválida", + "invalid-public-chat-topic": "Tópico de chat público inválido", + "invalid-range": "Formato inválido, deve estar entre {{min}} e {{max}}", + "invite": "Convidar", + "invite-button": "Convidar", + "invite-chat-accept": "Aceitar", + "invite-chat-accept-join": "Aceitar e Participar", + "invite-chat-intro": "Você foi indicado por um amigo para juntar-se na Status. Aqui estão algumas criptomoedas para você começar! Use-o para registrar um nome ENS ou comprar um pacote de adesivos", + "invite-chat-name": "Indicação de amigo", + "invite-chat-pending": "Pendente", + "invite-chat-rule": "Aceitar também recompensará o seu amigo com um bônus de indicação criptografado", + "invite-chat-starter-pack": "Pacote Inicial", + "invite-friends": "Convide amigos", + "invite-instruction-fifth": "Você pode optar por resgatar seu bônus de indicação a qualquer momento.", + "invite-instruction-first": "Você envia um link de convite exclusivo para seu amigo fazer o download e entrar na Status", + "invite-instruction-fourth": "Você recebe seu bônus de indicação e seu amigo o Pacote inicial", + "invite-instruction-second": "Seu amigo baixa a Status e cria uma conta (no Android)", + "invite-instruction-third": "É iniciado um bate-papo com seu amigo, onde ele confirma sua indicação", + "invite-people": "Convide pessoas", + "invite-privacy-policy-public": "Você instalou o Status por meio de um link de indicação. Ao entrar neste chat, você atribui ao seu referenciador e concorda com os", + "invite-privacy-policy1": "Ao aceitar você concorda com o programa de indicação", + "invite-privacy-policy2": "Termos e Condições.", + "invite-public-chat-home": "Convite de indicação", + "invite-public-chat-intro": "Aqui estão algumas criptomoedas para você começar! Use-as para registrar um nome ENS ou comprar um pacote de adesivos", + "invite-receive-account": "Conta para receber o seu bónus de indicação", + "invite-reward": "Ganhe criptomoedas para cada amigo que você convidar!", + "invite-reward-friend": "Amigo:", + "invite-reward-friend-description": "Seu amigo receberá um Pacote Inicial que consiste em alguns {{reward}} para começar", + "invite-reward-friend-name": "Pacote Inicial", + "invite-reward-you": "Você: ", + "invite-reward-you-description": "Convide um amigo e receba {{reward}} como um símbolo de nossa gratidão. Utilize para comprar figurinhas, um nome ENS e testar alguns DApps.", + "invite-reward-you-name": "Bónus de indicação", + "invite-select-account": "Selecione uma conta para receber o seu bônus de indicação", + "invite-warning": "Esta promoção é válida apenas para usuários de um dispositivo Android, que não sejam residentes dos EUA. O amigo precisa confirmar a indicação dentro de 7 dias", + "invited": "convidou", + "join": "Junte-se", + "join-a-community": "ou junte-se a uma comunidade", + "join-group-chat": "Acessar grupo", + "join-group-chat-description": "{{username}} convidou você para ingressar no grupo {{group-name}}", + "join-me": "Hey, junte-se a mim na Status: {{url}}", + "join-new-private-chat": "Iniciar uma nova conversa privada", + "join-new-public-chat": "Participar do bate-papo público", + "joined": "Entrou", + "joined-group-chat-description": "Você ingressou no {{group-name}} do convite por {{username}}", + "key": "Chave", + "key-managment": "Gerenciamento de chave", + "key-on-device": "A chave privada está salva neste dispositivo", + "keycard": "Keycard", + "keycard-access-reset": "O acesso ao Keycard foi redefinido", + "keycard-applet-install-instructions": "Para instalar o applet, siga as instruções em https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Tente mover o cartão para encontrar o leitor NFC em seu dispositivo", + "keycard-awaiting-title": "Ainda procurando…", + "keycard-blocked": "O cartão-chave foi bloqueado. \nVocê precisa redefinir o cartão para continuar usando-o.", + "keycard-can-use-with-new-passcode": "Você pode usar este cartão com sua nova senha de acesso", + "keycard-cancel-setup-text": "Isso cancelará a configuração do Keycard. É altamente recomendável concluir a configuração para usar o cartão-chave. Deseja mesmo cancelar?", + "keycard-cancel-setup-title": "Operação perigosa", + "keycard-connected-description": "Tente manter o cartão parado", + "keycard-connected-title": "Conectado", + "keycard-desc": "Tem um Keycard? Guarde as suas chaves nele; vai precisar dele para as transações", + "keycard-dont-ask-card": "Não peça o cartão para entrar", + "keycard-enter-new-passcode": "Digite a nova senha {{step}} \/ 2", + "keycard-error-description": "Conecte o cartão novamente para continuar", + "keycard-error-title": "Conexão perdida", + "keycard-free-pairing-slots": "O Keycard tem {{n}} portas de emparelhamento livres", + "keycard-has-multiaccount-on-it": "Este cartão está cheio. Cada cartão pode conter um par de chaves principal", + "keycard-init-description": "Coloque o cartão na parte de trás do telefone para continuar", + "keycard-init-title": "Procurando por cartões…", + "keycard-is-blocked-details": "Você não pode mais usar este cartão para acessar ou assinar esta conta. Houve muitas tentativas de falhadas de senha e PUK.", + "keycard-is-blocked-instructions": "Para acessar sua conta, reinstale a Status e use um novo Keycard, use uma carteira diferente ou redefina o Keycard manualmente.", + "keycard-is-blocked-title": "O Keycard está bloqueado", + "keycard-is-frozen-details": "Para proteger os seus bens, o seu cartão está congelado. Reinicie o acesso ao cartão para desbloquear chaves e enviar transacções. Crie uma nova senha e digite seu PUK para acessar sua(s) conta(s) neste cartão.", + "keycard-is-frozen-reset": "Redefinir acesso ao cartão", + "keycard-is-frozen-title": "O Keycard está congelado", + "keycard-onboarding-finishing-header": "Finalizando", + "keycard-onboarding-intro-header": "Armazene suas chaves no Keycard", + "keycard-onboarding-intro-text": "Prepare-se, isso pode levar alguns minutos, mas é importante proteger sua conta", + "keycard-onboarding-pairing-header": "Emparelhando o cartão...", + "keycard-onboarding-preparing-header": "Preparando o cartão...", + "keycard-onboarding-puk-code-header": "Anote os códigos e\n guarde de forma segura", + "keycard-onboarding-recovery-phrase-description": "Você precisa dessa frase-smenete para recuperar sua chave. Anote-a. Mantenha-a segura, offline e separado deste dispositivo.", + "keycard-onboarding-recovery-phrase-header": "Fazer backup da frase-semente", + "keycard-onboarding-recovery-phrase-text": "Somente para você ver. Esta é a frase-semente mágica usada para gerar sua chave.", + "keycard-onboarding-start-header": "Segure o cartão na parte de trás \n do seu telefone para começar", + "keycard-onboarding-start-step1": "Crie uma senha", + "keycard-onboarding-start-step1-text": "Em torno de 1 minuto. Crie uma senha de 6 dígitos para criptografar suas chaves", + "keycard-onboarding-start-step2": "Anote o código PUK e o código de emparelhamento", + "keycard-onboarding-start-step2-text": "Em torno de 1 minuto. Você vai precisar de um pedaço de papel e um lápis para isso", + "keycard-onboarding-start-step3": "Faça backup da frase-semente", + "keycard-onboarding-start-step3-text": "Em torno de 1 minuto. Também um pedaço de papel e um lápis são necessários", + "keycard-onboarding-start-text": "E manter o cartão em contato com o telefone\n durante a configuração. A configuração levará cerca de 4 minutos", + "keycard-processing-description": "Tente manter o cartão parado", + "keycard-processing-title": "Processando…", + "keycard-recovery-intro-button-text": "Iniciar a recuperação", + "keycard-recovery-intro-header": "Recuperar chaves armazenadas no Keycard", + "keycard-recovery-intro-text": "Se você gerou chaves usando um Keycard antes e agora quer usar estas chaves neste dispositivo", + "keycard-recovery-no-key-header": "Não há nada para \n recuperar aqui.", + "keycard-recovery-no-key-text": "O seu Keycard não tem nenhuma chave armazenada nele. Para o utilizar, gere uma nova chave e escolha o seu Keycard para guardar a chave", + "keycard-recovery-phrase-confirm-header": "Confirmar frase-semente", + "keycard-recovery-phrase-confirmation-text": "Você não terá uma segunda chance! Se você perder o acesso, por exemplo ao perder seu Keycard, só poderá acessar suas chaves com sua frase-semente. Ninguém, mas você tem sua frase-semente. Anotá-la. Mantenha-o seguro.", + "keycard-recovery-phrase-confirmation-title": "Anotou a frase-semente?", + "keycard-recovery-success-header": "Suas chaves foram \n recuperadas com sucesso", + "keycard-redeem-title": "Resgatar para", + "keycard-redeem-tx": "Resgatar bens", + "keycard-redeem-tx-desc": "Toque no cartão para assinar e receber bens", + "keycard-reset-passcode": "Redefinir senha", + "keycard-success-description": "Você pode remover o cartão agora", + "keycard-success-title": "Sucesso", + "keycard-unauthorized-operation": "Você não está autorizado a realizar esta operação. \n Por favor, toque no cartão válido e tente novamente.", + "keycard-upsell-subtitle": "Sua carteira hardware portátil e fácil de usar", + "language": "Idioma", + "learn-more": "Aprender mais", + "learn-more-about-keycard": "Saiba mais sobre o Keycard ", + "leave": "Sair", + "leave-chat": "Sair da conversa", + "leave-chat-confirmation": "Tem certeza que quer sair da conversa?", + "leave-community": "Deixar a comunidade", + "leave-confirmation": "Sair da conversa", + "leave-group": "Deixar o grupo", + "left": "saiu", + "les-ulc": "LES\/ULC", + "lets-go": "Vamos lá", + "light": "Claro", + "linked-on": "Vinculado em {{date}}", + "load-messages-before": "antes de {{date}}", + "load-more-messages": "↓ Buscar mais mensagens", + "load-more-timeline": "↓ Buscar mais", + "loading": "Carregando...", + "local-notifications": "Notificações locais", + "lock-app-with": "Bloquear aplicativo com", + "log-level": "Nível de registro", + "log-level-settings": "Configurações de nível de registo", + "logging": "Registros", + "logging-enabled": "Registros ativados?", + "login-pin-description": "Digite sua senha de 6 dígitos para desbloquear suas chaves", + "logout": "Sair", + "logout-app-content": "A conta será desconectada. Quando você a desbloquear novamente, a rede selecionada será usada", + "logout-are-you-sure": "Você tem certeza que quer \n sair?", + "logout-title": "Sair?", + "looking-for-cards": "Procurando por cartões…", + "lost-connection": "Conexão perdida", + "mail-should-be-configured": "O cliente de correio deve ser configurado", + "mailserver-address": "Endereço do nó da Status", + "mailserver-automatic": "Seleção automática", + "mailserver-automatic-switch-explanation": "Escolha o nó da Status mais rápido disponível", + "mailserver-connection-error": "Não foi possível conectar ao nó da Status", + "mailserver-content": "Um nó na rede Status que roteia e armazena mensagens por até 30 dias.", + "mailserver-details": "Detalhes do nó da Status", + "mailserver-error-content": "O nó da Status selecionado não pôde ser localizado.", + "mailserver-error-title": "Erro ao conectar ao nó da Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Escolha outro nó da Status", + "mailserver-reconnect": "Não foi possível conectar ao nó da Status. Toque para reconectar", + "mailserver-request-error-content": "O seguinte erro foi retornado pelo nó da Status: {{error}}", + "mailserver-request-error-status": "Um erro ocorreu ao buscar o histórico, verifique os registros para obter detalhes", + "mailserver-request-error-title": "Erro de comunicação com o nó da Status", + "mailserver-request-retry": "Solicitar nova tentativa", + "mailserver-retry": "Tente novamente", + "mailserver-title": "Nó de mensagem da Status", + "main-currency": "Moeda principal", + "main-networks": "Redes principais", + "main-wallet": "Carteira principal", + "mainnet-network": "Rede principal", + "make-admin": "Tornar-se administrador", + "manage-keys-and-storage": "Gerenciar chaves e armazenamento", + "mark-all-read": "Marcar todas como lidas", + "master-account": "Conta mestre", + "maybe-later": "Talvez mais tarde", + "member-kick": "Expulsar membro", + "members": { + "one": "1 membro", + "other": "{{count}} membros" + }, + "members-active": { + "one": "1 membro, 1 ativo", + "other": "{{count}} membros, {{count}} ativo" + }, + "members-active-none": "sem membros", + "members-count": "{{count}} membros", + "members-label": "Membros", + "members-limit-reached": "Limite de membros atingido", + "members-title": "Membros", + "membership-approval": "Exigir aprovação", + "membership-approval-description": "Sua comunidade é livre para participar, mas novos membros são obrigados a ser aprovados pelo criador da comunidade primeiro", + "membership-button": "Requisitos de participação", + "membership-declined": "O pedido de entrada no grupo foi recusado", + "membership-description": "A entrada no grupo exige que você seja aceito pelo administrador do grupo", + "membership-ens": "Exigir nome de usuário ENS", + "membership-ens-description": "Sua comunidade requer um nome de usuário ENS para poder participar", + "membership-free": "Nenhuma exigência", + "membership-free-description": "Sua comunidade é livre para que qualquer pessoa possa participar", + "membership-invite": "Exigir convite de outro membro", + "membership-invite-description": "Sua comunidade só pode ser acessada por convite de membros existentes da comunidade", + "membership-none": "Nenhum", + "membership-none-placeholder": "Você pode exigir que novos membros atendam a certos critérios antes que eles possam participar. Essa configuração pode ser alterada a qualquer momento", + "membership-request-pending": "Pedido de participação pendente", + "membership-requests": "Pedidos de participação", + "membership-title": "Requisitos de participação", + "message": "Mensagem", + "message-not-sent": "Mensagem não enviada", + "message-options-cancel": "Cancelar", + "message-reply": "Responder", + "messages": "Mensagens", + "might-break": "Pode quebrar alguns ÐApps", + "migrations-failed-content": "{{message}} \n versão do esquema: inicial {{initial-version}} , atual {{current-version}} , última {{last-version}} \n\nOcorreu um erro no banco de dados. Seus fundos e chave de bate-papo estão seguros. Outros dados, como bate-papos e contatos, não podem ser restaurados. O {{erase-multiaccounts-data-button-text}} \" {{erase-multiaccounts-data-button-text}} \" irá remover todos os outros dados e permitir que você acesse seus fundos e envie mensagens.", + "mobile-network-ask-me": "Pergunte-me quando usar a rede móvel", + "mobile-network-continue-syncing": "Continuar a sincronizar", + "mobile-network-continue-syncing-details": "Você pode alterá-lo mais tarde nas configurações", + "mobile-network-go-to-settings": "Ir para configurações", + "mobile-network-settings": "Dados móveis", + "mobile-network-sheet-configure": "Você pode configurar a sincronização em mais \ndetalhe em", + "mobile-network-sheet-offline": "Sem Wi-Fi, sincronização de mensagens desativada.", + "mobile-network-sheet-offline-details": "A sincronização com a rede móvel está desativada", + "mobile-network-sheet-remember-choice": "Lembrar a minha escolha", + "mobile-network-sheet-settings": "configurações", + "mobile-network-start-syncing": "Iniciar sincronização", + "mobile-network-stop-syncing": "Parar de sincronizar", + "mobile-network-stop-syncing-details": "Está conectado ao Wi-Fi?", + "mobile-network-use-mobile": "Utilizar dados móveis", + "mobile-network-use-mobile-data": "A Status utiliza bastante dados ao sincronizar os chats e a carteira.", + "mobile-network-use-wifi": "Somente Wi-Fi", + "mobile-syncing-sheet-details": "A Status usa bastante dados ao sincronizar os chats e a carteira.", + "mobile-syncing-sheet-title": "Sincronizar usando dados móveis?", + "more": "mais", + "move-and-reset": "Mover e reiniciar", + "move-keystore-file": "Mover arquivo de armazenamento de chaves", + "move-keystore-file-to-keycard": "Mover arquivo de armazenamento de chaves para o keycard?", + "multiaccount-exists-content": "Já existem chaves para esta conta e não podem ser adicionadas novamente. Se você perdeu sua senha, código de acesso ou cartão-chave, desinstale o aplicativo, reinstale e acesse suas chaves inserindo sua frase-semente", + "multiaccount-exists-title": "Já existem chaves para esta conta", + "multiaccounts-recover-enter-phrase-text": "Digite 12, 15, 18, 21 ou 24 palavras. \nSepare as palavras por um único espaço.", + "multiaccounts-recover-enter-phrase-title": "Digite sua frase-semente", + "mute": "Silenciar", + "my-accounts": "Minhas contas", + "my-accounts-empty": "Suas contas disponíveis aparecerão aqui", + "my-status": "Meu status", + "name": "Nome", + "name-of-token": "O nome do seu símbolo", + "name-optional": "Nome (opcional)", + "name-your-channel": "Dê um nome ao seu canal", + "name-your-channel-placeholder": "Nome do canal", + "name-your-community": "Dê um nome para sua comunidade", + "name-your-community-placeholder": "Um nome atraente", + "need-help": "Precisa de ajuda?", + "network": "Rede", + "network-chain": "Cadeia de rede", + "network-details": "Detalhes da rede", + "network-fee": "Taxa de rede", + "network-id": "ID de rede", + "network-info": "Informações da rede", + "network-invalid-network-id": "O ID da rede especificado não corresponde ao ID da rede pelo URL RPC", + "network-invalid-status-code": "Código de status inválido: {{code}}", + "network-invalid-url": "URL da rede é inválida", + "network-settings": "Configurações de rede", + "new": "Novo", + "new-chat": "Nova conversa", + "new-community-title": "Nova comunidade", + "new-contact": "Novo contato", + "new-contract": "Novo Contrato", + "new-favourite": "Novo favorito", + "new-group": "Novo grupo", + "new-group-chat": "Novo grupo de conversa", + "new-network": "Nova rede", + "new-pin-description": "Digite a nova senha de 6 dígitos", + "new-public-group-chat": "Participar de bate-papo público", + "new-status": "Novo status", + "new-tab": "Nova aba", + "next": "Próximo", + "nickname": "Apelido", + "nickname-description": "Os apelidos ajudam você a identificar as pessoas na Status. \n Só você pode ver os apelidos você criou", + "no": "Não", + "no-collectibles": "Não há colecionáveis disponíveis", + "no-contacts": "Você ainda não tem contatos", + "no-keycard-applet-on-card": "Não há applet Keycard no cartão", + "no-messages": "Nenhuma mensagem", + "no-pairing-slots-available": "Este cartão já está emparelhado com 5 dispositivos e não pode ser emparelhado com este. Use um dos dispositivos emparelhados, faça login com este cartão e libere uma porta de emparelhamento no cartão", + "no-result": "Sem resultados", + "no-tokens-found": "Nenhum token encontrado", + "node-info": "Informação do nó", + "node-version": "Versão do nó", + "nodes-disabled": "Nós da Status desativados", + "non-archival-node": "O endpoint RPC não oferece suporte a solicitações de arquivamento. Seu histórico de transferências locais pode estar incompleto.", + "nonce": "Nonce", + "none": "Nenhum", + "not-applicable": "Não aplicável a transações não assinadas", + "not-connected-nodes": "Não conectado a um nó da Status", + "not-connected-to-peers": "Não conectado a nenhum par", + "not-enough-snt": "SNT insuficiente", + "not-keycard-text": "O cartão que você usou não é um Keycard. Você precisa comprar um Keycard para usá-lo", + "not-keycard-title": "Não é um Keycard", + "not-registered": "não cadastrado", + "notification-settings": "Notificações", + "notifications": "Notificações", + "notifications-non-contacts": "Notificações de não contatos", + "notifications-preferences": "Preferências de notificação", + "notifications-servers": "Servidores de notificação", + "notifications-switch": "Mostrar notificações", + "notifications-transactions": "Transações da carteira", + "notify": "Notificar", + "now": "Agora", + "off": "Desativado", + "off-status-tree": "Fora da árvore da Status", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Ative os nós da Status para buscar mensagens enviadas enquanto o aplicativo está fechado. Quando ativado, um nó de histórico obtém seu endereço IP. Quando desativado, você não receberá mensagens quando o aplicativo for fechado e não as verá quando abrir o app mais tarde.", + "offline-messaging-use-history-nodes": "Usar nós de mensagem da Status", + "ok": "OK", + "ok-continue": "Ok, continuar", + "ok-got-it": "Ok, entendido", + "ok-save-pass": "OK, salvar senha", + "okay": "OK", + "on": "Em", + "on-status-tree": "Na árvore Status", + "once-enabled-share-metadata": "Uma vez habilitados, os links postados no chat podem compartilhar seus metadados com o site", + "open": "Abrir", + "open-chat": "Abrir chat", + "open-dapp": "Abrir ÐApp", + "open-dapp-store": "Descubra ÐApps", + "open-home": "Abrir...", + "open-in-new-tab": "Abrir em nova aba", + "open-membership": "Participação aberta", + "open-nfc-settings": "Abrir configurações do NFC", + "opening-buy-crypto": "Abrindo", + "optional": "Opcional", + "or": "OU", + "outgoing": "Saída", + "outgoing-transaction": "Transação enviada", + "page-camera-request-blocked": "Solicitações de câmera bloqueadas. Para habilitar as solicitações de câmera, vá em Configurações", + "page-would-like-to-use-camera": "gostaria de usar sua câmera", + "pair": "Sincronizar dispositivos", + "pair-card": "Sincronizar para este dispositivo", + "pair-code": "Código de par", + "pair-code-explanation": "Emparelha o cartão com um dispositivo diferente (até 5) para desbloquear as chaves e assinar transações com o mesmo Keycard", + "pair-code-placeholder": "Emparelhar código…", + "pair-this-card": "Emparelhar este cartão", + "pair-this-device": "Anunciar dispositivo", + "pair-this-device-description": "Emparelhe seus dispositivos para sincronizar contatos e bate-papos entre eles", + "paired-devices": "Dispositivos sincronizados", + "pairing": "Emparelhamento", + "pairing-card": "Cartão de emparelhamento", + "pairing-go-to-installation": "Ir para as configurações de sincronização", + "pairing-maximum-number-reached-content": "Desative um dos seus dispositivos antes de ativar um novo.", + "pairing-maximum-number-reached-title": "Número máximo de dispositivos atingidos", + "pairing-new-installation-detected-content": "Um novo dispositivo foi detectado. \nPara usar seus dispositivos corretamente, é importante emparelhá-los e ativá-los antes de usá-los. \nPor favor, vá para a seção de dispositivos em configurações para parear seus dispositivos.", + "pairing-new-installation-detected-title": "Novo dispositivo detectado", + "pairing-no-info": "Sem informações", + "pairing-please-set-a-name": "Por favor, atribua um nome para o seu dispositivo.", + "passphrase": "Frase-semente", + "password": "Senha", + "password-description": "Pelo menos 6 caracteres. Sua senha protege suas chaves. Você precisa desbloquear a Status e fazer transações.", + "password-placeholder": "Senha…", + "password-placeholder2": "Confirme sua senha", + "password_error1": "As senhas não correspondem.", + "paste": "Colar", + "paste-json": "Colar JSON", + "pay-to-chat": "Pagar para conversar", + "peer-content": "Um dispositivo conectado à rede de bate-papo Status. Cada usuário pode representar um ou mais pares, dependendo do número de dispositivos.", + "peer-title": "Par", + "peer-to-peer": "P2P", + "peers": "Pares", + "pending": "Pendente", + "pending-confirmation": "Confirmação pendente...", + "pending-invitations": "Pedidos de entrada pendentes", + "permissions": "Permissões", + "phone-e164": "Internacional 1", + "photos": "Fotos", + "photos-access-error": "Para conceder a permissão de fotos necessária, acesse as configurações do sistema e verifique se Status > Fotos está selecionado.", + "pin-changed": "A senha de 6 dígitos foi alterada", + "pin-code": "Senha de 6 dígitos", + "pin-mismatch": "Senha incorreta", + "pin-one-attempt": "uma tentativa", + "pin-one-attempt-blocked-after": "antes que o seu Keycard seja bloqueado", + "pin-one-attempt-blocked-before": "Cuidado, você só tem", + "pin-one-attempt-frozen-after": "antes que o seu Keycard seja congelado", + "pin-one-attempt-frozen-before": "Cuidado, você só tem", + "pin-retries-left": "{{number}} tentativas restantes", + "preference": "Preferência", + "preview-privacy": "Visualizar modo de privacidade", + "previewing-may-share-metadata": "A visualização de links desses sites pode compartilhar seus metadados com seus proprietários", + "privacy": "Privacidade", + "privacy-and-security": "Privacidade e segurança", + "privacy-policy": "Política de privacidade", + "private-key": "Chave privada", + "private-notifications": "Notificações particulares", + "private-notifications-descr": "A Status notificará sobre novas mensagens. Você pode editar suas preferências de notificação nas configurações.", + "processing": "Apenas um momento", + "product-information": "Informações do produto", + "profile": "Perfil", + "profile-deleted-content": "O seu perfil foi excluído com sucesso", + "profile-deleted-title": "Perfil excluído", + "profile-details": "Detalhes do perfil", + "profile-pic-pick": "Escolher na galeria", + "profile-pic-remove": "Remover foto", + "profile-pic-take": "Tirar foto", + "public-chat": "Conversa pública", + "public-chat-description": "Participe de bate-papos públicos sobre seus interesses! Qualquer um pode começar uma nova sala.", + "public-chats": "Conversas públicas", + "public-group-status": "Público", + "public-group-topic": "Assunto", + "public-key": "Chave pública", + "puk-and-pairing-codes-displayed": "Códigos PUK e de emparelhamento exibidos", + "puk-code": "Código PUK", + "puk-code-explanation": "Se você esquecer sua senha de 6 dígitos ou inseri-la incorretamente três vezes, precisará desse código para desbloquear seu cartão.", + "puk-mismatch": "Código PUK errado", + "push-failed-transaction": "Sua transação falhou", + "push-failed-transaction-body": "{{value}} {{currency}} para {{to}}", + "push-inbound-transaction": "Você recebeu {{value}} {{currency}}", + "push-inbound-transaction-body": "De {{from}} para {{to}}", + "push-notifications-server-enabled": "Servidor habilitado", + "push-notifications-servers": "Servidores de notificação push", + "push-outbound-transaction": "Você enviou {{value}} {{currency}}", + "push-outbound-transaction-body": "De {{from}} para {{to}}", + "quiet-days": "{{quiet-days}} dias", + "quiet-hours": "{{quiet-hours}} horas", + "re-encrypt-key": "Recriptografe suas chaves", + "receive": "Receber", + "receive-transaction": "Receber transação", + "recent": "Recentes", + "recent-empty": "Os endereços usados recentemente aparecerão aqui", + "recent-recipients": "Contatos", + "recently-used-stickers": "Os adesivos usados recentemente aparecerão aqui", + "recipient": "Destinatário", + "recipient-code": "Digite o endereço do destinatário", + "recipient-code-placeholder": "0x… ou nome.dominio.eth", + "recover": "Recuperar", + "recover-key": "Acessar chaves existentes", + "recover-keycard-multiaccount-not-supported": "Já existem chaves para esta conta e não podem ser adicionadas novamente. Se você perdeu sua senha, código de acesso ou cartão-chave, desinstale o aplicativo, reinstale e acesse suas chaves inserindo sua frase-semente", + "recover-with-keycard": "Recuperar com Keycard", + "recover-with-seed-phrase": "Recupere com a frase semente", + "recovering-key": "Acessando chaves…", + "recovery-confirm-phrase": "Confirmar frase-semente", + "recovery-phrase": "Frase-semente", + "recovery-success-text": "Você terá que definir um novo código ou senha para recriptografar suas chaves", + "recovery-typo-dialog-description": "Observe que sua frase-semente deve usar exatamente as mesmas palavras e ordem em que você a recebeu", + "recovery-typo-dialog-title": "A frase-semente está correta?", + "redeem-amount": "{{quantity}} bônus disponíveis", + "redeem-now": "Resgatar agora", + "redeem-success": "Resgate de bônus com sucesso!", + "refresh": "Atualizar", + "registered": "cadastrado", + "remember-me": "Lembrar-me", + "remind-me-later": "Mostrar isso novamente", + "remove": "Remover", + "remove-favourite": "Remover favorito", + "remove-from-chat": "Remover do chat", + "remove-from-contacts": "Remover dos contatos", + "remove-from-contacts-text": "Ao remover um usuário da sua lista de contatos, você não esconde o endereço da sua carteira dele", + "remove-group": "Remover grupo", + "remove-network": "Remover rede", + "remove-token": "Remover token", + "removed": "removeu", + "repeat-pin": "Repita os 6 novos dígitos da senha", + "report-bug-email-template": "1. Descrição da Edição \n (Descreva o recurso que você deseja ou resuma brevemente o bug e o que você fez, o que esperava que acontecesse e o que realmente acontece. Seções abaixo) \n\n\n 2. Passos para reproduzir \n (Descreva como podemos replicar o bug passo a passo.) \n - Abrir Status\n - ... \n - Etapa 3, etc. \n\n\n 3. Comportamento esperado \n (Descreva o que você esperava que acontecesse.) \n\n\n 4. Comportamento real \n (Descreva o que realmente aconteceu.) \n\n\n 5. Por favor, anexe capturas de tela que possam demonstrar o problema \n", + "request-access": "Solicitar acesso", + "request-feature": "Solicitar um recurso", + "request-membership": "Solicitar para entrar", + "request-pending": "Pedido pendente…", + "request-transaction": "Solicitar transação", + "required-field": "Campo obrigatório", + "resend-message": "Reenviar", + "reset-card": "Redefinir cartão", + "reset-card-description": "Esta operação redefinirá o cartão para o estado inicial. Ele apagará todos os dados do cartão, incluindo chaves privadas. A operação não é reversível.", + "reset-database": "Reiniciar base de dados", + "reset-database-warning": "Exclua chats, contatos e configurações. Obrigatório quando você perder sua senha", + "restore-defaults": "Restaurar padrões", + "retry": "Tentar de novo", + "revoke-access": "Recuperar acesso", + "rpc-url": "RPC URL", + "rpc-usage-copy": "Copiar", + "rpc-usage-get-stats": "Atualizar", + "save": "Salvar", + "save-password": "Salvar senha", + "save-password-unavailable": "Defina a senha do dispositivo para salvar a senha", + "save-password-unavailable-android": "Salvar senha não está disponível: seu dispositivo pode estar \"rooteado\" ou não possui os recursos de segurança necessários.", + "scan-qr": "Escanear QR", + "scan-qr-code": "Digitalize um código QR com um endereço de carteira", + "scan-tokens": "Escanear tokens", + "search": "Procurar", + "search-no-chat-found": "Sem resultados. Você quer dizer", + "secret-keys-confirmation-text": "Você vai precisar deles para continuar a usar o seu Keycard no caso de perder o seu celular.", + "secret-keys-confirmation-title": "Já anotou os códigos?", + "security": "Segurança", + "see-details": "Ver detalhes", + "see-it-again": "VEJA NOVAMENTE", + "seed-key-uid-mismatch": "Semente não combina", + "seed-key-uid-mismatch-desc-1": "A frase-semente que você inseriu não corresponde a {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Para gerenciar as chaves para esta conta, verifique sua frase-semente e tente novamente.", + "seed-phrase-content": "Um conjunto de palavras fáceis de ler, selecionadas aleatoriamente na lista padrão BIP39 e usadas para recuperar ou acessar sua conta Ethereum em outras carteiras e dispositivos. Também chamada de \"frase mnemônica\", \"frase de recuperação\" ou \"backup de carteira\" em todo o ecossistema Ethereum. A maioria dos aplicativos de Ethereum usa esse mesmo padrão para gerar contas.", + "seed-phrase-placeholder": "Frase-semente…", + "seed-phrase-title": "Frase semente", + "select": "Selecione", + "select-account": "Selecionar conta", + "select-account-dapp": "Selecione a conta que você deseja usar com Dapps", + "select-account-first": "Selecione uma conta primeiro", + "select-chat": "Selecione bate-papo para iniciar o envio de mensagens", + "select-new-location-for-keys": "Selecione um novo local para salvar sua (s) chave (s) privada (s)", + "selected": "Selecionado", + "send-logs": "Relatar uma falha", + "send-logs-to": "Relatar um falha para {{email}}", + "send-message": "Enviar mensagem", + "send-push-notifications-description": "Quando desativado, a pessoa que recebe suas mensagens não será notificada de suas novas mensagens", + "send-request": "Enviar pedido", + "send-request-amount": "Quantidade", + "send-request-amount-max-decimals": "O número máximo de decimais é {{asset-decimals}}", + "send-request-unknown-token": "Token desconhecido - {{asset}}", + "send-sending-to": "para {{recipient-name}}", + "send-transaction": "Enviar transação", + "sending": "Enviando.", + "sent-at": "Enviado a", + "server": "Servidor", + "set-a-topic": "Criar um tópico", + "set-currency": "Definir moeda", + "set-custom-fee": "Alterar taxa", + "set-dapp-access-permissions": "Definir permissões de acesso do DApp", + "set-max": "Definir máximo", + "settings": "Configurações", + "share": "Compartilhar", + "share-address": "Compartilhar endereço", + "share-chat": "Compartilhar bate-papo", + "share-contact-code": "Compartilhar minha chave de conversa", + "share-dapp-text": "Confira este DApp que estou usando na Status: {{link}}", + "share-link": "Compartilhar link", + "share-my-profile": "Compartilhar meu perfil", + "share-profile": "Compartilhar perfil", + "share-profile-link": "Compartilhar link do perfil", + "share-public-chat-text": "Confira este bate-papo público no app Status: {{link}}", + "shared": "Compartilhado", + "sharing-copied-to-clipboard": "Copiado", + "sharing-copy-to-clipboard": "Copiar", + "sharing-share": "Compartilhar", + "show-less": "Mostre menos", + "show-more": "Mostrar mais", + "show-notifications": "Mostrar notificações", + "show-profile-pictures": "Mostrar fotos de perfil de", + "show-qr": "Mostrar código QR", + "show-transaction-data": "Mostrar dados da transação", + "sign-and-send": "Assinar e enviar", + "sign-anyway": "Assinar mesmo assim", + "sign-in": "Entrar", + "sign-message": "Assinar Mensagem", + "sign-out": "Sair da sessão", + "sign-request-failed": "Não foi possível assinar a mensagem", + "sign-with": "Cadastre-se com", + "sign-with-password": "Entra com a senha", + "sign-you-in": "Iniciando sessão...", + "signing": "Assinatura", + "signing-a-message": "Assinando uma mensagem", + "signing-phrase": "Frase de assinatura", + "something-went-wrong": "Algo deu errado", + "soon": "Em breve", + "specify-address": "Especificar endereço", + "specify-name": "Especifique um nome", + "specify-network-id": "Especifique o ID da rede", + "specify-rpc-url": "Especifique um URL RPC", + "specify-server-public-key": "Digite a chave pública do servidor", + "start-chat": "Iniciar bate-papo", + "start-conversation": "Iniciar conversa", + "start-group-chat": "Novo grupo de conversa", + "start-new-chat": "Iniciar conversa particular", + "starter-pack-coming": "Pacote de iniciante vindo até você", + "starter-pack-coming-description": "Pode levar de alguns minutos a horas", + "starter-pack-received": "Pacote inicial recebido", + "starter-pack-received-description": "Aqui estão algumas criptomoedas para você começar! Use-as para obter stickers, um nome ENS e testar alguns dapps", + "status": "Status", + "status-confirmed": "Confirmado", + "status-hardwallet": "Status hardwallet", + "status-keycard": "Status Keycard", + "status-mobile-descr": "O Status usa muitos dados ao sincronizar as conversas. Você pode optar por não sincronizar quando na rede móvel", + "status-not-sent-click": "Não confirmado. Clique para opções", + "status-not-sent-tap": "Não confirmado. Toque para opções", + "status-pending": "Pendente", + "status-sent": "Enviado", + "status-tx-not-found": "TX não encontrado", + "status-updates-descr": "As atualizações de status aparecerão aqui. Adicione este perfil como um contato para receber atualizações em sua linha do tempo.", + "statuses-descr": "Compartilhe o que você está pensando e fique atualizado com seus contatos", + "statuses-my-status-descr": "Compartilhe o que você está pensando. Qualquer pessoa que visitar seu perfil poderá ver seu status. As pessoas que adicionam você como contato receberão suas atualizações na linha do tempo delas", + "step-i-of-n": "Etapa {{step}} de {{number}}", + "sticker-market": "Mercado de adesivos", + "storage": "Armazenamento", + "submit": "Enviar", + "submit-bug": "Relatar uma falha", + "success": "Sucesso", + "symbol": "Símbolo", + "sync-all-devices": "Sincronizar todos dispositivos", + "sync-in-progress": "Sincronizando...", + "sync-settings": "Configurações de sincronização", + "sync-synced": "Sincronizado", + "syncing-devices": "Sincronizando...", + "system": "Sistema", + "tabs": "Abas", + "tag-was-lost": "A tag foi perdida", + "tap-card-again": "Aproxime o cartão na parte de trás do seu telefone novamente", + "test-networks": "Redes de teste", + "text-input-disabled": "Por favor, espere um momento…", + "this-device": "Este dispositivo", + "this-device-desc": "Suas chaves serão criptografadas e armazenadas com segurança no seu dispositivo", + "this-is-you-signing": "Esta é a sua frase de assinatura", + "this-will-take-few-seconds": "Isso levará alguns segundos", + "three-words-description": "Você deve ver essas três palavras antes de assinar cada transação", + "three-words-description-2": "Se você ver uma combinação diferente, cancele a transação e saia", + "timeline": "Linha do tempo", + "to": "Para", + "to-block": "Bloquear ", + "to-enable-biometric": "Para ativar o {{bio-type-label}}, você deve salvar sua senha na tela de desbloqueio", + "to-encrypt-enter-password": "Para criptografar a conta, digite sua senha", + "to-see-this-message": "Para ver esta mensagem,", + "token-auto-validate-decimals-error": "Decimais incorretos para o token {{symbol}} no endereço {{address}} - definido como {{expected}} mas detectado como {{actual}}", + "token-auto-validate-name-error": "Nome incorreto para o token {{symbol}} no endereço {{address}} - definido como {{expected}} mas detectado como {{actual}}", + "token-auto-validate-symbol-error": "Símbolo incorreto para o token {{symbol}} no endereço {{address}} - definido como {{expected}} mas detectado como {{actual}}", + "token-details": "Detalhes do Token", + "topic-name-error": "Use apenas letras minúsculas (a a z), números e traços (-). Não use teclas de bate-papo", + "transaction": "Transação", + "transaction-data": "Dados da transação", + "transaction-declined": "Transação recusada", + "transaction-description": "Considere-o completo após 12 confirmações na rede.", + "transaction-details": "Detalhes da transação", + "transaction-failed": "Falha na transação", + "transaction-history": "Histórico de transações", + "transaction-request": "Solicitação de transação", + "transaction-sent": "Transação enviada", + "transaction-signed": "A transação foi assinada com sucesso", + "transactions": "Transações", + "transactions-filter-select-all": "Selecionar tudo", + "transactions-filter-title": "Filtrar histórico", + "transactions-history": "Histórico de transações", + "transactions-history-empty": "Ainda não há transações no seu histórico", + "transactions-history-loading": "Carregando histórico de transações. Isso pode demorar um pouco.", + "transactions-load-more": "Carregar mais", + "transactions-sign": "Assinar", + "transfer-ma-unknown-error-desc-1": "Parece que sua conta múltipla não foi excluída. O banco de dados pode ter sido redefinido", + "transfer-ma-unknown-error-desc-2": "Verifique sua lista de contas e tente novamente. Se a conta não estiver listada vá para Acessar as chaves existentes para recuperar com a frase semente", + "transfers-fetching-failure": "O histórico de transferências não pôde ser atualizado. Verifique sua conexão e puxe para baixo para tentar novamente", + "tribute-required-by-multiaccount": "{{multiaccount-name}} precisa de SNT para começar uma conversa.", + "tribute-state-paid": "Tributo pago", + "tribute-state-pending": "Tributo pendente", + "tribute-state-required": "Requer {{snt-amount}} tributos em SNT", + "tribute-to-talk": "Tribute to Talk", + "tribute-to-talk-add-friends": "Adicione amigos como um contato para permitir bate-papos sem pagamento de tributo.", + "tribute-to-talk-are-you-friends": "Vocês são amigos?", + "tribute-to-talk-ask-to-be-added": "Peça para ser adicionado como um contato", + "tribute-to-talk-contact-received-your-tribute": " recebeu seu tributo. Agora vocês podem conversar com segurança entre si.", + "tribute-to-talk-desc": "Monetize sua atenção exigindo pagamento de SNT para novas pessoas iniciem um bate-papo", + "tribute-to-talk-disabled": "Tribute to Talk desativado", + "tribute-to-talk-disabled-note": "A partir de agora, novas pessoas podem iniciar um bate-papo com você sem enviar o SNT.", + "tribute-to-talk-enabled": "Você tem o Tribute to Talk ativado.", + "tribute-to-talk-finish-desc": "A partir de agora, você receberá apenas conversas de contatos, e as pessoas que pagaram ", + "tribute-to-talk-learn-more-1": "Seu tempo e atenção são seus ativos mais valiosos. O Tribute to Talk permite definir uma quantidade de SNT necessária para novas pessoas iniciarem um bate-papo com você.", + "tribute-to-talk-learn-more-2": "Qualquer pessoa que não esteja na sua lista de contatos será solicitada a pagar e você poderá responder assim que tiverem.", + "tribute-to-talk-learn-more-3": "Você sempre pode enviar o dinheiro de volta, mas para garantir que os amigos possam entrar em contato com você livremente, adicione-os primeiro como contato.", + "tribute-to-talk-paywall-learn-more-1": "Nosso tempo e atenção são nossos ativos mais valiosos. O Tribute to Talk permite que você entre em contato com novas pessoas em troca de um pagamento SNT.", + "tribute-to-talk-paywall-learn-more-2": "Para iniciar um bate-papo com alguém que tenha um conjunto de tributo, basta pagar o SNT necessário e você será adicionado como um contato.", + "tribute-to-talk-paywall-learn-more-3": "Se você os conhece, pode compartilhar seu perfil fora da Status para ser adicionado gratuitamente.", + "tribute-to-talk-pending": "Tributo pendente de confirmação", + "tribute-to-talk-pending-note": "A transação de tributo está pendente de confirmação na rede. Você pode verificar seu status no histórico de transações", + "tribute-to-talk-removing-note": "A remoção do Tribute to Talk permitirá que novas pessoas iniciem um bate-papo sem enviar o SNT. Requer que uma transação seja feita.", + "tribute-to-talk-set-snt-amount": "Defina a quantidade de SNT necessária para novas pessoas iniciarem uma conversa", + "tribute-to-talk-signing": "Aguardando para assinar a transação", + "tribute-to-talk-transaction-failed-note": "A transação falhou e as configurações do Tribute to Talk não foram alteradas", + "tribute-to-talk-tribute-received1": "Tributo recebido. Você e", + "tribute-to-talk-tribute-received2": "agora são contatos e podem conversar com segurança.", + "tribute-to-talk-you-require-snt": "Você requer SNT para novas pessoas para iniciar um bate-papo.", + "try-again": "Tente novamente", + "try-keeping-the-card-still": "Tente manter o cartão parado", + "turn-nfc-description": "NFC está desativado no seu dispositivo. Você pode habilitá-lo nas configurações", + "turn-nfc-on": "Ligue o NFC para continuar", + "tx-fail-description1": "É provável que esta transação falhe. Assine por sua conta em risco usando uma taxa de rede personalizada.", + "tx-fail-description2": "É provável que esta transação falhe. Defina uma taxa de rede maior para assinar por sua conta em risco.", + "type": "Tipo", + "type-a-message": "Mensagem", + "ulc-enabled": "ULC ativado", + "unable-to-fetch": "Não foi possível obter o histórico do chat", + "unable-to-read-this-code": "Não foi possível ler este código", + "unable-to-send-messages": "Incapaz de enviar e receber mensagens", + "unblock": "Desbloquear", + "unblock-contact": "Desbloquear este usuário", + "unknown-status-go-error": "Erro desconhecido de status-go", + "unlock": "Desbloquear", + "unmute": "Ativar som", + "unpair-card": "Desemparelhar cartão", + "unpair-card-confirmation": "Esta operação desemparelhará o cartão do dispositivo atual. Requer autorização de senha de 6 dígitos. Você quer prosseguir?", + "unpaired-keycard-text": "O cartão-chave que você selecionou não está associado a este telefone ", + "unpaired-keycard-title": "Parece que seu cartão não foi pareado.", + "update": "Atualizar", + "update-to-listen-audio": "Atualize para a versão mais recente para ouvir uma mensagem de áudio aqui!", + "update-to-see-image": "Atualize para a versão mais recente para ver uma imagem aqui!", + "update-to-see-sticker": "Atualize para a versão mais recente para ver um belo adesivo aqui!", + "url": "URL", + "usd-currency": "USD", + "use-valid-contact-code": "Digite ou verifique uma chave de bate-papo ou nome de usuário válidos", + "validation-amount-invalid-number": "Quantidade não é um número válido", + "validation-amount-is-too-precise": "O valor é muito preciso. O número máximo de casas decimais é {{decimals}} .", + "version": "Versão do aplicativo", + "view": "Visualizar", + "view-cryptokitties": "Ver em CryptoKitties", + "view-cryptostrikers": "Ver em CryptoStrikers", + "view-details": "Ver Detalhes", + "view-etheremon": "Ver no Etheremon", + "view-gitcoin": "Ver no Gitcoin", + "view-profile": "Ver perfil", + "view-signing": "Ver frase de assinatura", + "view-superrare": "Ver no SuperRare", + "waiting-for-wifi": "Sem Wi-Fi, sincronização de mensagens desativada.", + "waiting-for-wifi-change": "Configurações", + "waiting-to-sign": "Aguardando para assinar a transação", + "waiting-wi-fi": "Aguardando Wi-Fi", + "waku-bloom-filter-mode": "Modo de filtro Waku bloom", + "wallet": "Carteira", + "wallet-address": "Endereço da carteira", + "wallet-asset": "Ativo", + "wallet-assets": "Ativos", + "wallet-backup-recovery-title": "Faça uma cópia de segurança da sua frase-semente", + "wallet-choose-recipient": "Escolher destinatário", + "wallet-collectibles": "Colecionáveis", + "wallet-insufficient-funds": "Fundos insuficientes", + "wallet-insufficient-gas": "Não há ETH suficiente para o gás", + "wallet-invalid-address": "Endereço inválido: \n {{data}}", + "wallet-invalid-address-checksum": "Erro no endereço:{{data}}", + "wallet-invalid-chain-id": "A rede não corresponde: \n {{data}} mas a cadeia atual é {{chain}}", + "wallet-key-content": "Um endereço hexadecimal de 64 caracteres baseado no padrão Ethereum e começando com 0x. Para o público, o endereço da sua conta é compartilhado com outras pessoas quando você deseja receber fundos. Também chamado de \"endereço Ethereum\" ou \"endereço da carteira\".", + "wallet-key-title": "Endereço da conta", + "wallet-manage-assets": "Gerenciar ativos", + "wallet-request": "Solicitação", + "wallet-send": "Enviar", + "wallet-send-min-units": "Mínimo 21000 unidades", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Configurações da carteira", + "wallet-total-value": "Valor total", + "wallet-transaction-total-fee": "Taxa total", + "wants-to-access-profile": "quer acessar seu perfil", + "warning": "Atenção", + "warning-message": "Desculpe, limitamos o envio de várias mensagens em rápida sucessão para evitar spam. Por favor, tente novamente daqui a pouco", + "warning-sending-to-contract-descr": "O endereço que você inseriu é um contrato inteligente. O envio de fundos para este endereço pode resultar em perda de fundos. Para interagir com um DApp, abra o DApp no navegador da Status.", + "watch-only": "Somente exibição", + "web-view-error": "Ops, erro", + "websites": "Websites", + "webview-camera-permission-requests": "Solicitações de permissão de câmera do Webview", + "webview-camera-permission-requests-subtitle": "Quando habilitado, sites e dapps podem pedir para usar sua câmera", + "welcome-blank-community-message": "Suas comunidades aparecerão aqui.", + "welcome-blank-message": "Suas conversas serão exibidas aqui. Para iniciar novos bate-papos, pressione o botão ⊕", + "welcome-community-blank-message": "Suas conversas aparecerão aqui. Para iniciar novos bate-papos clique nos 3 pontos acima e selecione \"Criar um canal\"", + "welcome-screen-text": "Configure sua carteira, convide seus amigos para conversar e navegue pelos populares dapps!", + "welcome-to-status": "Bem-vindo à Status!", + "welcome-to-status-description": "Configure sua carteira criptográfica, convide amigos para conversar e navegar por aplicativos descentralizados", + "whats-on-your-mind": "O que você está pensando...", + "word-count": "Contagem de palavras", + "word-n": "Palavra #{{number}}", + "word-n-description": "Para verificar se você fez o backup da sua frase-semente corretamente, digite a palavra #{{number}} acima.", + "words-n": { + "one": "1 palavra", + "other": "{{count}} palavras" + }, + "write-down-and-store-securely": "Anote os códigos &\n guarde-os de forma segura", + "wrong-address": "Endereço errado", + "wrong-card": "Cartão errado", + "wrong-card-text": "O cartão tocado não corresponde às chaves que você selecionou", + "wrong-contract": "Contrato errado", + "wrong-keycard-text": "O Keycard que você selecionou não está associado a este telefone", + "wrong-keycard-title": "Parece que você selecionou\n um Keycard errado.", + "wrong-password": "Senha incorreta", + "wrong-word": "Palavra errada", + "yes": "Sim", + "you": "você", + "you-already-have-an-asset": "Você já tem um ativo {{value}}", + "you-are-all-set": "Está tudo pronto!", + "you-are-all-set-description": "Se você perder seu telefone, você pode acessar seus fundos e a chave do bate-papo agora usando sua frase-semente", + "you-can-change-account": "Você pode mudar o nome da conta e a cor para o que desejar", + "you-can-choose-preview-websites": "Você pode escolher quais dos seguintes sites podem visualizar o link de descrições e imagens nos bate-papos", + "you-can-fetch": "Você pode buscar o histórico do chat", + "you-dont-have-contacts": "Você ainda não possui contatos.", + "you-dont-have-stickers": "Você ainda não tem nenhum adesivo", + "you-will-need-this-code": "Você precisará desse código para abrir a Status e assinar transações", + "you-will-start-from-scratch": "Você começará do zero com um novo conjunto de chaves", + "your-card-is-frozen": "O seu Keycard está congelado. Redefinir acesso ao cartão", + "your-contact-code": "A concessão de acesso autoriza este DApp a recuperar a sua chave de chat", + "your-data-belongs-to-you": "Se você perder a sua frase-semente, você perde os seus dados e fundos", + "your-data-belongs-to-you-description": "Se perder o acesso, por exemplo, ao perder seu telefone, você só pode acessar suas chaves com sua frase-semente. Somente você tem a sua frase-semente. Escreva-a. Guarde-a em segurança", + "your-keys": "Suas chaves", + "your-recovery-phrase": "Sua frase-semente", + "your-recovery-phrase-description": "Esta é sua frase-semente. Você o usa para provar que esta é sua carteira. Você só pode vê-lo uma vez! Escreva no papel e mantenha em um lugar seguro. Você vai precisar dele se você perder ou reinstalar sua carteira.", + "youre-on-mobile-network": "Você está em uma rede móvel" +} diff --git a/translations/pt_BR.json b/translations/pt_BR.json new file mode 100644 index 00000000000..1df27a8680a --- /dev/null +++ b/translations/pt_BR.json @@ -0,0 +1,1896 @@ +{ + "K": "K", + "M": "M", + "NFT": "NFT", + "You": "Você", + "about-app": "Sobre", + "about-key-storage-content": "O Status nunca acessará sua chave privada. Não esqueça de salvar sua frase-semente. Se você perder seu telefone, essa é a única maneira de acessar suas chaves.", + "about-key-storage-title": "Sobre o armazenamento de chaves", + "about-names-content": "Ninguém pode fingir ser você! Você é anônimo por padrão e nunca precisa revelar seu nome real. Você pode cadastrar um nome personalizado por uma pequena taxa.", + "about-names-title": "Os nomes não podem ser alterados", + "about-sharing-data": "Sobre o compartilhamento de dados", + "accept": "Aceitar", + "accept-and-add": "Aceitar e adicionar", + "accept-and-continue": "Aceitar e continuar", + "accept-and-share-address": "Aceitar e compartilhar endereço", + "accept-community-rules": "Eu concordo com as regras da comunidade", + "accept-new-chats-from": "Aceitar novos bate-papos de", + "accept-status-tos-prefix": "Eu aceito o Status", + "accepted": "Aceito", + "access-existing-keys": "Acessar chaves existentes", + "access-key": "Chave de acesso", + "account-added": "Conta adicionada", + "account-color": "Cor da conta", + "account-content": "Você pode comparar contas no Status com contas bancárias. Como uma conta bancária, uma conta normalmente possui um endereço e um saldo. Você usa essa conta para realizar transações no Ethereum. Você pode ter várias contas na sua carteira. Todas acessadas desbloqueando o Status.", + "account-exists-title": "A conta já existe", + "account-is-used": "A conta está sendo usada com os Dapps no navegador.", + "account-name": "Nome da conta", + "account-settings": "Configurações da conta", + "account-title": "Conta", + "accounts": "Contas", + "actions": "Ações", + "active-members": "Membros ativos", + "active-online": "Online", + "active-unknown": "Desconhecido", + "activity": "Atividade", + "add": "Adicionar", + "add-a-watch-account": "Adicione um endereço somente leitura", + "add-account": "Adicione uma conta", + "add-account-description": "Você pode importar qualquer tipo de conta Ethereum para adicioná-lo à sua carteira Status", + "add-account-incorrect-password": "A senha parece estar incorreta. Digite a senha que você usa para desbloquear o aplicativo.", + "add-an-account": "Adicione uma conta", + "add-bootnode": "Adicionar nó de inicialização", + "add-contact": "Adicionar contato", + "add-custom-token": "Adicionar token personalizado", + "add-favourite": "Adicionar favorito", + "add-mailserver": "Adicionar nó do Status", + "add-members": "Adicionar membros", + "add-network": "Adicionar rede", + "add-new-contact": "Adicionar novo contato", + "add-nickname": "Adicionar um apelido (opcional)", + "add-node": "Adicionar nó", + "add-private-key-account": "Adicionar conta a partir de chave privada", + "add-seed-account": "Adicionar conta com uma frase-semente", + "add-text": "Adicionar texto", + "add-to-contacts": "Adicionar aos contatos", + "add-to-contacts-text": "Ao adicionar um usuário à sua lista de contatos, você compartilha o endereço da sua carteira", + "add-to-favourites": "Adicionar aos favoritos", + "add-watch-account": "Adicionar uma conta somente para exibição", + "added": "adicionado", + "address": "Endereço", + "address-or-ens-name": "Endereço ou nome ENS", + "address-received": "Endereço recebido", + "address-request-accepted": "Solicitação de endereço aceita", + "address-requested": "Endereço solicitado", + "admin": "Administrador", + "advanced": "Avançado", + "advanced-settings": "Configurações avançadas", + "advertiser-description": "Você descobriu o Status graças a um parceiro. Você se importa se o Status verificar seu endereço IP uma vez para que ele seja recompensado? Esta informação não será usada para mais nada e será removida completamente após 7 dias.", + "advertiser-starter-pack-accept": "Aceitar", + "advertiser-starter-pack-decline": "Recusar", + "advertiser-starter-pack-description": "Aqui estão algumas criptografias para você começar! Use-as para obter adesivos, um nome ENS e experimentar os dapps", + "advertiser-starter-pack-title": "Pacote Inicial", + "advertiser-title": "Privacidade por padrão", + "agree-by-continuing": "Ao continuar, você concorda \n com nosso", + "all": "Todos", + "all-connections": "Todas as Conexões", + "allow": "Permitir", + "allow-and-send": "Permitir e enviar", + "allow-mention-notifications": "Mostrar @ menções", + "allowing-authorizes-this-dapp": "Permitir que este DApp recupere o endereço da sua carteira e ative o Web3", + "alphabetically": "Alfabeticamente", + "already-have-asset": "Você já tem este ativo", + "amount": "Quantia", + "and": "e", + "anyone": "Qualquer um", + "app-commit": "Commit do aplicativo", + "app-connections": "Conexões de aplicativos", + "appearance": "Aparência", + "apply": "Aplicar", + "approve": "Aprovar", + "approve-limit": "Aprovar limite", + "approve-token": "Aprovar token", + "approve-token-contract-desc": "Aprovar um token com um contrato permite que ele gaste seu saldo de tokens. Se você julgar que um projeto não é confiável, não aprove o token com eles ou aprove apenas o valor que você usará com eles.", + "are-you-sure": "Você tem certeza?", + "are-you-sure-description": "Você não poderá ver toda a frase-semente de novo", + "are-you-sure-to-cancel": "Você tem certeza que deseja cancelar?", + "are-you-sure?": "Você tem certeza?", + "ask-in-status": "Faça uma pergunta ou relate um bug", + "at": "em", + "attribution-received": "{{attrib}} de {{max}} bônus recebidos", + "audio": "Áudio", + "audio-message": "Mensagem de áudio", + "audio-recorder": "Gravador", + "audio-recorder-error": "Erro no gravador", + "audio-recorder-max-ms-reached": "Tempo máximo de gravação atingido", + "audio-recorder-permissions-error": "Você precisa dar permissão para enviar mensagens de áudio", + "authorize": "Autorizar", + "available": "Disponível", + "available-participants": { + "one": "Você pode selecionar mais um participante", + "other": "Você pode selecionar mais {{count}} participantes" + }, + "back": "Voltar", + "back-up": "Cópia de segurança", + "back-up-seed-phrase": "Fazer backup da frase-semente", + "back-up-your-seed-phrase": "Fazer backup da frase-semente", + "backing-up": "Realizando cópia de segurança...", + "backup-disabled": "Desativado", + "backup-enabled": "Habilitado", + "backup-recovery-phrase": "Cópia de segurança da frase-semente", + "backup-settings": "Configurações de cópia de segurança", + "backup-through-waku": "Realizar cópia de segurança através do Waku", + "bad-fees-description": "Sua taxa de prioridade está abaixo de nossos parâmetros sugeridos.", + "balance": "Saldo", + "begin-set-up": "Iniciar configuração", + "below-base-fee": "taxa máxima abaixo da taxa base", + "biometric-auth-android-sensor-desc": "Toque no sensor", + "biometric-auth-android-sensor-error-desc": "Falhou", + "biometric-auth-android-title": "Autenticação necessária", + "biometric-auth-confirm-logout": "Faça login novamente", + "biometric-auth-confirm-message": "É necessária autenticação biométrica para continuar. Se não for possível, desbloqueie suas chaves com sua senha ou código de acesso", + "biometric-auth-confirm-title": "Você deve autenticar!", + "biometric-auth-confirm-try-again": "Tente novamente", + "biometric-auth-error": "Não foi possível executar a autenticação biométrica ({{code}})", + "biometric-auth-login-error-title": "Erro de autenticação biométrica", + "biometric-auth-login-ios-fallback-label": "Digite a senha", + "biometric-auth-reason-login": "Login no Status", + "biometric-auth-reason-verify": "Verificar autenticação", + "biometric-disable-bioauth": "desativar {{bio-type-label}}", + "biometric-disable-password-description": "Se você desativar isso, você também irá ", + "biometric-disable-password-title": "Desativar senha salva", + "biometric-enable": "Se você não quiser inserir sua senha todas as vezes para acessar o aplicativo, ative o início de sessão por {{bio-type-label}}", + "biometric-enable-button": "Ativar {{bio-type-label}}", + "biometric-enable-keycard": "Se você não quiser usar seu Keycard cada vez que acessar o aplicativo, ative {{bio-type-label}} para iniciar a sessão", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Impressão digital", + "biometric-secure-with": "Proteger com {{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "Senha BIP39", + "biu": "BIU", + "blank-contacts-text": "Seus contatos estarão aqui", + "blank-keycard-text": "Você pode continuar com o seu keycard depois de gerar as chaves e o nome", + "blank-keycard-title": "Parece que você utilizou\num Keycard em branco.", + "blank-messages-text": "Suas mensagens estarão aqui", + "block": "Bloquear ", + "block-contact": "Bloquear esse usuário", + "block-contact-details": "O bloqueio excluirá as mensagens anteriores deste usuário e impedirá que novas cheguem até você", + "block-user": "Bloquear Usuário", + "block-user?": "Bloquear Usuário?", + "blocked-users": "Usuários bloqueados", + "bold": "Negrito", + "bootnode-address": "Endereço do nó de inicialização", + "bootnode-details": "Detalhes do nó de inicialização", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Nós de inicialização", + "bootnodes-enabled": "Nós de inicialização ativados", + "bootnodes-settings": "Configurações de nós de inicialização", + "browsed-websites": "O histórico do navegador aparecerá aqui", + "browser": "Navegador", + "browser-not-secure": "A conexão não é segura! Não assine transações nem envie dados pessoais neste site.", + "browser-secure": "A conexão é segura. Certifique-se de confiar neste site antes de assinar transações ou inserir dados pessoais.", + "browsers": "Navegadores", + "browsing-cancel": "Cancelar", + "browsing-open-in-android-web-browser": "Abrir no Android", + "browsing-open-in-ios-web-browser": "Abrir no iOS", + "browsing-open-in-status": "Abrir no Status", + "browsing-site-blocked-description1": "Detectamos atividades maliciosas em potencial neste endereço. Para proteger você e sua carteira, estamos impedindo mais navegação. \n\nSe você acha que isso é um erro, informe-nos no", + "browsing-site-blocked-description2": " conversa pública.", + "browsing-site-blocked-go-back": "Voltar", + "browsing-site-blocked-title": "Este site está bloqueado", + "browsing-title": "Buscar", + "bug-report": "Relatar um bug", + "bug-report-description": "Descrição", + "bug-report-description-placeholder": "Obrigatório, não pode ser vazio", + "bug-report-steps": "Passos para reproduzir", + "bug-report-steps-placeholder": "- abra a app\n- faça algo\n- e então faça outra coisa", + "bug-report-submit-email": "Enviar por e-mail com arquivo de logs", + "bug-report-submit-gh-issue": "Enviar um problema do GitHub sem logs", + "bug-report-too-short-description": "Descrição muito curta", + "build-yourself": "Para usar o aplicativo sem estes Termos de Serviço, você pode construir sua própria versão", + "buy-crypto": "Comprar criptomoedas", + "buy-crypto-choose-a-service": "Escolha um serviço que você gostaria de utilizar para comprar criptomoedas", + "buy-crypto-description": "Encontre um dapp para comprar criptomoedas agora", + "buy-crypto-leaving": "Você está saindo do Status e entrando em um site de terceiros para concluir sua compra", + "buy-crypto-title": "Parece que sua carteira está vazia", + "camera-access-error": "Para conceder a permissão de câmera necessária, vá para as configurações do sistema e verifique se Status > Câmera está selecionado.", + "can-not-add-yourself": "É você. Para iniciar um bate-papo, escolha outra pessoa", + "can-send-messages": "Você pode enviar e receber novas mensagens", + "cancel": "Cancelar", + "cancel-keycard-setup": "Cancelar a configuração do Keycard ", + "cancelling": "Cancelando", + "cannot-pin-desc": "Você pode fixar no máximo 3 mensagens.\n Desafixe pelo menos uma para fixar uma nova.", + "cannot-pin-title": "Você não pode fixar esta mensagem!", + "cannot-read-card": "Não é possível ler o cartão. Por favor, segure-o na parte de trás do seu telefone", + "cannot-use-default-pin": "O código de acesso 000000 não é permitido. \nPor favor, use outro número", + "cant-open-public-chat": "Não é possível abrir este chat público", + "cant-report-bug": "Não é possível relatar um bug", + "card-is-blank": "Este cartão está em branco", + "card-reseted": "O cartão foi redefinido", + "card-unpaired": "O cartão foi desemparelhado do dispositivo atual", + "category": "Categoria", + "category-title": "Nome da categoria", + "change-fleet": "Mude a frota para {{fleet}}", + "change-group-privacy": "Alterar a privacidade do grupo", + "change-logging-enabled": "Tem certeza de que deseja {{enable}} o registro?", + "change-pairing": "Alterar o código de emparelhamento", + "change-pairing-description": "Alterar o código de emparelhamento não afeta os emparelhamentos atuais. No entanto, qualquer novo emparelhamento exigirá o novo código.", + "change-pairing-title": "Criar um novo código de emparelhamento", + "change-passcode": "Alterar código de acesso", + "change-password": "Alterar senha", + "change-pin": "Alterar código de acesso de 6 dígitos", + "change-puk": "Alterar PUK de 12 dígitos", + "change-tip": "Mudar gorjeta", + "changed-amount-warning": "O valor foi alterado de {{old}} para {{new}}", + "changed-asset-warning": "O ativo foi alterado de {{old}} para {{new}}", + "chaos-mode": "Modo caos", + "chaos-unicorn-day": "Dia do Caos do Unicórnio", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Bate-papo", + "chat-and-transact": "Converse e faça transações de forma privada com amigos", + "chat-is-a-contact": "Contato", + "chat-is-not-a-contact": "Não é um contato", + "chat-key": "Chave de bate-papo", + "chat-key-content": "As mensagens no protocolo de bate-papo Status são enviadas e recebidas usando chaves de criptografia. A chave de bate-papo público é uma sequência de caracteres que você compartilha com outras pessoas para que elas possam enviar mensagens para você no Status.", + "chat-key-title": "Chave de bate-papo", + "chat-link-previews": "Visualizações de links de chat", + "chat-name": "Nome do chat", + "chat-name-content": "Três palavras aleatórias, derivadas algoritmicamente da sua chave de bate-papo e usadas como pseudônimo padrão no bate-papo. Os nomes de bate-papo são completamente únicos; nenhum outro usuário pode ter as mesmas três palavras.", + "chat-name-title": "Nome no chat", + "chat-notification-preferences": "Configurações de notificação", + "chat-settings": "Configurações do chat", + "chats": "Chats", + "check-on-block-explorer": "Verifique no explorador de blocos", + "check-on-opensea": "Confira no opensea", + "check-your-recovery-phrase": "Confira sua frase-semente", + "choose-actions": "Escolha ações", + "choose-authentication-method": "Escolha um método de autenticação", + "choose-new-location-for-keystore": "Escolha um novo local para salvar seu arquivo de armazenamento de chaves", + "choose-storage": "Escolha o armazenamento", + "clear": "Apagar", + "clear-all": "Apagar tudo", + "clear-history": "Apagar histórico", + "clear-history-action": "Apagar", + "clear-history-confirmation": "Apagar o histórico?", + "clear-history-confirmation-content": "Tem certeza de que deseja apagar este histórico de bate-papo?", + "clear-history-title": "Apagar o histórico?", + "clear-history?": "Limpar Histórico?", + "cleared-chat-description-public": "Tem estado muito quieto por aqui. Inicie a conversa ou ", + "close": "Fechar", + "close-all": "Fechar tudo", + "close-app-button": "Confirmar", + "close-app-content": "O aplicativo irá parar e fechar. Quando você reabri-lo, a rede selecionada será usada", + "close-app-title": "Atenção!", + "collectibles-leak-metadata": "Você pode exibir seus NFTs aqui. Se você fizer isso, você compartilhará sua carteira e endereço IP", + "command-button-send": "Enviar", + "communities": "Comunidades", + "communities-alpha": "Comunidades (alfa)", + "communities-enabled": "Comunidades habilitadas", + "communities-verified": "✓ Comunidade do Status verificada", + "community": "Comunidade", + "community-channel": "Canal da comunidade", + "community-channel-read-requirements-met": "Requisitos de apenas visualizar atendidos", + "community-channel-read-requirements-not-met": "Requisitos de apenas visualizar não atendidos", + "community-channel-write-requirements-met": "Requisitos de visualização e postagem atendidos", + "community-channel-write-requirements-not-met": "Requisitos de visualização e postagem não atendidos", + "community-color": "Cor da comunidade", + "community-color-placeholder": "Escolha uma cor", + "community-edit-title": "Editar comunidade", + "community-emoji-thumbnail-title": "Thumbnail", + "community-enter-channel-info": "Entrar no canal revelará seus endereços públicos ao proprietário do nó", + "community-image-delete": "Vazio", + "community-image-pick": "Escolha uma imagem", + "community-image-remove": "Remover", + "community-image-take": "Tirar foto", + "community-info": "Informação da comunidade", + "community-info-not-found": "Informação da comunidade não encontrada", + "community-invite-title": "Convidar", + "community-join-requirements-changed": "Os requisitos de token mudaram", + "community-join-requirements-met": "Requisitos de participação atendidos", + "community-join-requirements-not-met": "Requisitos de participação não atendidos", + "community-join-requirements-tokens-lost": "Você não tem mais os tokens necessários", + "community-key": "Chave privada da comunidade", + "community-key-placeholder": "Digite a chave privada da sua comunidade", + "community-link": "Link da comunidade", + "community-members": { + "one": "{{count}} membro", + "other": "{{count}} membros" + }, + "community-members-title": "Membros", + "community-message-preview": "Convite para participar do(a) {{community-name}}", + "community-notification-settings": "Configurações de notificação", + "community-private-key": "Chave privada da comunidade", + "community-requests-to-join-title": "Pedidos de participação", + "community-roles": "Funções", + "community-rules": "Regras da comunidade", + "community-share-title": "Compartilhar", + "community-thumbnail-image": "Imagem em miniatura", + "community-thumbnail-upload": "Carregar", + "complete-hardwallet-setup": "Este cartão agora está vinculado. Você precisa dele para assinar transações e desbloquear suas chaves", + "completed": "Concluído", + "confirm": "Confirmar", + "confirm-new-password": "Confirmar nova senha", + "confirm-pairing-code-placeholder": "Confirme o seu código de emparelhamento...", + "confirm-password-placeholder": "Confirme sua senha…", + "confirmation-request": "Pedido de confirmação", + "confirmations": "Confirmações", + "confirmations-helper-text": "Quando a transação tiver 12 confirmações, você poderá considerá-la liquidada.", + "connect": "Conectar", + "connect-mailserver-content": "Conectar-se a {{name}} ?", + "connect-wallet": "Conectar carteira", + "connected": "Conectado", + "connected-to": "Conectado a", + "connecting": "Conectando...", + "connecting-requires-login": "Conectar-se a outra rede requer login", + "connection-request": "Solicitação de conexão", + "connection-status": "Status de conexão", + "connection-with-the-card-lost": "A conexão com o cartão \n foi perdida", + "connection-with-the-card-lost-setup-text": "Para retomar a configuração, segure o cartão na\n parte de trás do seu telefone e mantenha \n o cartão em contato com o telefone", + "connection-with-the-card-lost-text": "Para continuar, segure o cartão na parte de trás do seu telefone", + "contact-code": "Chave de bate-papo", + "contact-request": "Requisição de contato", + "contact-request-accepted": "Aceito ✓", + "contact-request-declined": "Recusado ⓧ", + "contact-request-header": "👋 Solicitações de contato", + "contact-request-pending": "Pendente...", + "contact-request-sent": "pedido de contato enviado", + "contact-requests": "Solicitações de Contato", + "contact-s": { + "one": "contato", + "other": "contatos" + }, + "contacts": "Contatos", + "contacts-descr": "Seus contatos aparecerão aqui. Você receberá atualizações de status de qualquer pessoa que adicionar como contato", + "contacts-empty": "Contatos com nomes ENS aparecerão aqui", + "continue": "Continuar", + "continue-anyway": "Continuar mesmo assim", + "contract-address": "Endereço do contrato", + "contract-interaction": "Interação contratual", + "contract-isnt-supported": "Contrato não suportado", + "copy-info": "Copiar informação", + "copy-qr": "Copiar código", + "copy-text": "Copiar texto", + "copy-to-clipboard": "Copiar", + "copy-transaction-hash": "Copiar ID da transação", + "cost-fee": "Custo\/Taxa", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Criar", + "create-a-pin": "Crie um código de acesso de 6 dígitos", + "create-a-puk": "Crie um PUK de 12 dígitos", + "create-category": "Criar categoria", + "create-channel": "Criar um canal", + "create-channel-title": "Novo canal", + "create-community": "Criar uma comunidade", + "create-group-chat": "Criar bate-papo do grupo", + "create-multiaccount": "Gerar chaves", + "create-new-key": "Obter novas chaves", + "create-pin": "Criar código de acesso de 6 dígitos", + "create-pin-description": "Você precisará do seu cartão + este código de acesso de 6 dígitos para desbloquear o Status e confirmar transações", + "created-group-chat-description": "Você criou o grupo {{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Moeda", + "currency-display-name-aed": "Dirham dos Emiratos Árabes Unidos", + "currency-display-name-afn": "Afegane afegão", + "currency-display-name-ars": "Peso argentino", + "currency-display-name-aud": "Dólar australiano", + "currency-display-name-bbd": "Dólar de Barbados", + "currency-display-name-bdt": "Taka de Bangladesh", + "currency-display-name-bgn": "Lev búlgaro", + "currency-display-name-bhd": "Dinar do Bahrein", + "currency-display-name-bnd": "Dólar de Brunei", + "currency-display-name-bob": "Boliviano", + "currency-display-name-brl": "Real do Brasil", + "currency-display-name-btn": "Ngultrum do Butão", + "currency-display-name-cad": "Dólar canadense", + "currency-display-name-chf": "Franco suíço", + "currency-display-name-clp": "Peso chileno", + "currency-display-name-cny": "Renminbi yaun chinês", + "currency-display-name-cop": "Peso colombiano", + "currency-display-name-crc": "Cólon da Costa Rica", + "currency-display-name-czk": "Coroa checa", + "currency-display-name-dkk": "Coroa dinamarquesa", + "currency-display-name-dop": "Peso da República Dominicana", + "currency-display-name-egp": "Libra egipcia", + "currency-display-name-etb": "Birr etíope", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Libra esterlina", + "currency-display-name-gel": "Lari georgiano", + "currency-display-name-ghs": "Cedi ganês", + "currency-display-name-hkd": "Dólar de Hong Kong", + "currency-display-name-hrk": "Kuna da Croácia", + "currency-display-name-huf": "Florim húngaro", + "currency-display-name-idr": "Rupia indonésia", + "currency-display-name-ils": "Shekel de Israel", + "currency-display-name-inr": "Rupia indiana", + "currency-display-name-isk": "Coroa islandesa", + "currency-display-name-jmd": "Dólar jamaicano", + "currency-display-name-jpy": "Yen japonês", + "currency-display-name-kes": "Xelim queniano", + "currency-display-name-krw": "Won sul-coreano", + "currency-display-name-kwd": "Dinar kuwaitianao", + "currency-display-name-kzt": "Tenge do Cazaquistão", + "currency-display-name-lkr": "Rupia do Sri Lanka", + "currency-display-name-mad": "Dirham marroquino", + "currency-display-name-mdl": "Leu moldávo", + "currency-display-name-mur": "Rúpia mauriciana", + "currency-display-name-mwk": "Kwacha do Malawi", + "currency-display-name-mxn": "Peso mexicano", + "currency-display-name-myr": "Ringgit malaio", + "currency-display-name-mzn": "Metical de Moçambique", + "currency-display-name-nad": "Dólar namibiano", + "currency-display-name-ngn": "Naira da Nigéria", + "currency-display-name-nok": "Coroa norueguesa", + "currency-display-name-npr": "Rupia nepalesa", + "currency-display-name-nzd": "Dólar da Nova Zelândia", + "currency-display-name-omr": "Rial de Omã", + "currency-display-name-pen": "Sol peruano", + "currency-display-name-pgk": "Kina da Papua-Nova Guiné", + "currency-display-name-php": "Peso das Filipinas", + "currency-display-name-pkr": "Rupia paquistanesa", + "currency-display-name-pln": "Zloty polonês", + "currency-display-name-pyg": "Guarani do Paraguai", + "currency-display-name-qar": "Rial do Qatar", + "currency-display-name-ron": "Leu romeno", + "currency-display-name-rsd": "Dinar sérvio", + "currency-display-name-rub": "Rublo russo", + "currency-display-name-sar": "Rial da Arábia Saudita", + "currency-display-name-sek": "Coroa da Suécia", + "currency-display-name-sgd": "Dólar de Singapura", + "currency-display-name-thb": "Baht da Tailândia", + "currency-display-name-try": "Lira turca", + "currency-display-name-ttd": "Dólar de Trinidad e Tobago", + "currency-display-name-twd": "Novo dólar taiwanês", + "currency-display-name-tzs": "Xelim tanzaniano", + "currency-display-name-uah": "Grívnia da Ucrânia", + "currency-display-name-ugx": "Xelim ugandês", + "currency-display-name-usd": "Dólar dos Estados Unidos", + "currency-display-name-uyu": "Peso uruguaio", + "currency-display-name-vef": "Bolívar venezuelano", + "currency-display-name-vnd": "Vietnam Dong", + "currency-display-name-zar": "Rand sul-africano", + "current": "Atual", + "current-average": "Média atual", + "current-average-tip": "Gorjeta média atual", + "current-base": "Base atual", + "current-base-fee": "Taxa base atual", + "current-minimum-tip": "Gorjeta mínima atual", + "current-network": "Rede atual", + "current-password": "Senha atual", + "current-pin": "Digite o código de acesso de 6 dígitos", + "current-pin-description": "Digite seu código de acesso de 6 dígitos para continuar", + "custom": "Personalizado", + "custom-networks": "Redes personalizadas", + "custom-node": "Você está usando um endpoint RPC personalizado. Seu histórico de transferências locais pode estar incompleto.", + "custom-seed-phrase": "Frase-semente inválida", + "custom-seed-phrase-text-1": "Esta frase-semente não é compatível com o nosso dicionário. Verifique se há palavras com erros ortográficos.", + "cut": "Cortar", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Aceitar e Abrir", + "dapp-starter-pack-description": "Aqui estão algumas criptografias para você começar! Use-o para obter adesivos, um nome ENS e experimente os dapps", + "dapp-starter-pack-title": "Pacote Inicial", + "dapp-would-like-to-connect-wallet": "gostaria de se conectar a", + "dapps": "ÐApps", + "dapps-permissions": "Permissões do DApp", + "dark": "Escuro", + "data": "Dados", + "data-collected": "Dados coletados", + "data-collected-subtitle": "A tabela abaixo mostra os dados exatos que estão armazenados e serão enviados. Os dados são validados de acordo com as regras públicas para garantir que nenhum dado sensível seja enviado. Não confie, verifique.", + "data-syncing": "Sincronização de dados", + "database-reset-content": "Conversas, contatos e configurações foram excluídos. Você pode usar sua conta com seu Keycard", + "database-reset-title": "Redefinição do banco de dados", + "database-reset-warning": "O banco de dados será redefinido. Conversas, contatos e configurações serão excluídos", + "datetime-ago": "atrás", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "dia", + "other": "dias" + }, + "datetime-day-short": { + "one": "D", + "other": "D" + }, + "datetime-hour": { + "one": "hora", + "other": "horas" + }, + "datetime-hour-short": { + "one": "H", + "other": "H" + }, + "datetime-minute": { + "one": "minuto", + "other": "minutos" + }, + "datetime-minute-short": { + "one": "M", + "other": "M" + }, + "datetime-second": { + "one": "segundo", + "other": "segundos" + }, + "datetime-second-short": { + "one": "S", + "other": "S" + }, + "datetime-today": "hoje", + "datetime-yesterday": "Ontem", + "decimals": "Decimais", + "decline": "Recusar", + "declined": "Recusado", + "decryption-failed-content": "Ocorreu um erro ao descriptografar seus dados. Você pode precisar apagar seus dados antigos e gerar uma nova conta. Toque em “Aplicar” para apagar ou “Cancelar” para tentar novamente", + "default": "Padrão", + "default-assets": "ERC20 e ERC721 padrão", + "default-sync-period": "Histórico de sincronização para", + "delete": "Excluir", + "delete-account": "Remover conta", + "delete-and-leave-group": "Excluir e sair do grupo", + "delete-bootnode": "Excluir nó de inicialização", + "delete-bootnode-are-you-sure": "Tem certeza de que deseja excluir este nó de inicialização?", + "delete-bootnode-title": "Eliminar nó de inicialização", + "delete-category-confirmation": "Tem certeza de que deseja excluir esta categoria?", + "close-chat": "Excluir conversa", + "close-chat-confirmation": "Tem certeza de que deseja excluir este bate-papo?", + "delete-confirmation": "Excluir?", + "delete-for-everyone": "Excluir para todos", + "delete-for-me": "Excluir para mim", + "delete-keys-keycard": "Excluir chaves do Keycard", + "delete-mailserver": "Excluir nó do Status", + "delete-mailserver-are-you-sure": "Tem certeza de que deseja excluir este nó do Status?", + "delete-mailserver-title": "Excluir nó do Status", + "delete-message": "Apagar mensagem", + "delete-my-account": "Excluir minha conta", + "delete-my-profile": "Excluir meu perfil", + "delete-network-confirmation": "Tem certeza de que deseja excluir esta rede?", + "delete-network-error": "Conecte-se a uma rede diferente antes de excluir esta", + "delete-network-title": "Excluir rede?", + "delete-node": "Excluir nó", + "delete-node-are-you-sure": "Você tem certeza que deseja excluir esse nó?", + "delete-node-title": "Excluir nó", + "delete-profile": "Excluir perfil", + "delete-profile-warning": "Aviso: se você não tiver sua frase-semente escrita, você perderá o acesso aos seus fundos depois de excluir seu perfil", + "deny": "Negar", + "derivation-path": "Caminho de derivação", + "describe-channel": "Descreva o canal", + "description": "descrição", + "dev-mode": "Modo de desenvolvimento", + "dev-mode-settings": "Configurações do modo de desenvolvimento", + "device-syncing": "Sincronização de dispositivos", + "devices": "Dispositivos", + "disable": "desativar", + "disable-all": "Desativar todos", + "disable-later-in-settings": "Você pode desativar isso mais tarde nas Configurações", + "disabled": "Desativado", + "disconnect": "Desconectar", + "disconnected": "Bate-papo offline", + "discover": "Descobrir", + "discover-communities": "Descubra Comunidades", + "dismiss": "Ignorar", + "display-collectibles": "Exibir colecionáveis", + "done": "Concluído", + "dont-ask": "Não me pergunte novamente", + "edit": "Editar", + "edit-categories": "Editar categorias", + "edit-channel-title": "Editar canal", + "edit-chats": "Editar bate-papos", + "edit-community": "Editar comunidade", + "edit-favourite": "Editar favorito", + "edit-group": "Editar grupo", + "edit-message": "Editar mensagem", + "edit-name-and-image": "Editar nome e imagem", + "edit-name-image": "Editar nome e imagem", + "edit-nickname": "Editar apelido", + "edit-profile": "Editar perfil", + "edited": "Editado", + "editing-message": "Editando mensagem", + "empty-activity-center": "Suas notificações de bate-papo\n aparecerão aqui", + "empty-chat-description": "Ainda não há mensagens \n neste bate-papo", + "empty-chat-description-community": "Tem estado quieto aqui nas últimas {{quiet-hours}}.", + "empty-chat-description-one-to-one": "Todas as mensagens que você envia aqui são criptografadas e só podem ser lidas por você e ", + "empty-chat-description-public": "Está quieto aqui nas últimas {{quiet-hours}}. Inicie a conversa ou ", + "empty-chat-description-public-share-this": "compartilhe este bate-papo.", + "empty-keycard-required": "Requer um Keycard vazio", + "empty-pending-invitations-descr": "Pessoas que desejam entrar no grupo \n através de um link de convite aparecerão aqui", + "empty-tab": "Aba vazia", + "enable": "habilitar", + "enable-all": "Habilitar todos", + "enable-link-previews": "Ativar visualizações de link no chat?", + "encrypt-with-password": "Criptografar com senha", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Adicionar nome de usuário ", + "ens-agree-to": "Concordo com", + "ens-chat-settings": "Configurações de bate-papo", + "ens-custom-domain": "Domínio personalizado", + "ens-custom-username-hints": "Digite o nome de usuário inteiro, incluindo o domínio personalizado, como nomedeusuario.domínio.eth", + "ens-custom-username-taken": "O nome de usuário não pertence a você :(", + "ens-deposit": "Depósito", + "ens-dismiss-message": "Clique aqui para fechar", + "ens-displayed-with": "Suas mensagens são exibidas para outras pessoas com", + "ens-get-name": "Obter um nome universal", + "ens-got-it": "Ok, entendi.", + "ens-locked": "Nome de usuário bloqueado. Você não poderá liberá-lo até {{date}}", + "ens-name-content": "Pseudônimo personalizado para sua chave de bate-papo que você pode cadastrar usando o Ethereum Name Service. Os nomes do ENS são nomes de usuário descentralizados.", + "ens-name-not-found": "Não é possível resolver o nome ENS", + "ens-name-title": "Nome ENS", + "ens-network-restriction": "Disponível apenas na Mainnet", + "ens-no-usernames": "Você não tem nenhum nome de usuário conectado", + "ens-powered-by": "Desenvolvido com Ethereum Name Services", + "ens-primary-username": "Nome de usuário principal", + "ens-register": "Cadastro", + "ens-registration-failed": "Para cadastrar o nome de usuário, tente novamente.", + "ens-registration-failed-title": "Falha na transação", + "ens-registration-failure": "Erro ao cadastrar", + "ens-registration-in-progress": "Cadastro em andamento…", + "ens-release-username": "Liberar nome de usuário", + "ens-remove-hints": "A remoção desanexará o nome de usuário da sua chave.", + "ens-remove-username": "Remover nome de usuário", + "ens-saved": " agora está conectado com sua chave de bate-papo e pode ser usado no Status.", + "ens-saved-title": "Nome de usuário adicionado", + "ens-show-username": "Mostrar meu nome de usuário ENS em bate-papos", + "ens-terms-header": "Termos de cadastro de nome", + "ens-terms-point-1": "Os fundos são depositados por 1 ano. Seu SNT será bloqueado, mas não será gasto.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Registro ENS).", + "ens-terms-point-2": "Após 1 ano, você pode liberar o nome e receber seu depósito de volta ou não fazer nenhuma ação para manter o nome.", + "ens-terms-point-3": "Se os termos do contrato mudarem — por exemplo, o Status fizer atualizações do contrato — o usuário tem o direito de liberar o nome de usuário, independentemente do tempo de espera.", + "ens-terms-point-4": "O controlador do contrato não pode acessar seus fundos depositados. Eles só podem ser movidos de volta para o endereço que os enviou.", + "ens-terms-point-5": "Seus endereços serão publicamente associados ao seu nome no ENS.", + "ens-terms-point-6": "Os nomes de usuários são criados como nós de subdomínio stateofus.eth e estão sujeitos aos termos do contrato inteligente do ENS.", + "ens-terms-point-7": "Você autoriza o contrato a transferir o SNT em seu nome. Isso só pode ocorrer quando você aprovar uma transação para autorizar a transferência.", + "ens-terms-point-8": "Estes termos são garantidos pela lógica do contrato inteligente nos endereços:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "Termos de cadastro do nome.", + "ens-test-message": "Olá", + "ens-transaction-pending": "Transação pendente ...", + "ens-understand": "Entendo que o endereço da minha carteira estará publicamente conectado ao meu nome de usuário.", + "ens-username": "Nome de usuário ENS", + "ens-username-already-added": "O nome de usuário já está conectado à sua chave de bate-papo e pode ser usado dentro do Status.", + "ens-username-available": "✓ Nome de usuário disponível!", + "ens-username-connected": "Esse nome de usuário é de sua propriedade e conectado à sua chave de bate-papo.", + "ens-username-connected-continue": "Continuar configurando \"Mostrar meu nome de usuário do ENS nos bate-papos\".", + "ens-username-connected-with-different-key": "Continuar exigirá uma transação para conectar o nome de usuário com sua chave de bate-papo atual.", + "ens-username-connection-confirmation": "{{username}} será conectado assim que a transação for concluída.", + "ens-username-hints": "Pelo menos 4 caracteres. Usando letras minúsculas e números.", + "ens-username-invalid": "Apenas letras e números.", + "ens-username-invalid-name-warning": "O processo de registro de um dos seus nomes ens terminou em estado inválido. NÃO USE o nome para transações de carteira e contate nosso suporte em support@status.im", + "ens-username-owned": "✓ O nome de usuário é seu.", + "ens-username-owned-continue": "Continuar conectará esse nome de usuário com sua chave de bate-papo.", + "ens-username-registration-confirmation": "Ótimo! Você será o proprietário do {{username}} assim que a transação for concluída.", + "ens-username-registration-invalid": "Aviso! O processo de registro terminou em estado inválido. NÃO USE o nome para transações de carteira e contate nosso suporte em support@status.im", + "ens-username-taken": "Nome de usuário já utilizado :(", + "ens-username-you-can-follow-progress": "Você pode acompanhar o progresso na seção Histórico de Transações da sua carteira.", + "ens-usernames": "Nomes de usuário ENS", + "ens-usernames-details": "Cadastre um nome de usuário universal para ser facilmente reconhecido por outros usuários", + "ens-want-custom-domain": "Eu possuo um nome em outro domínio", + "ens-want-domain": "Eu quero um domínio stateofus.eth", + "ens-welcome-hints": "Os nomes do ENS transformam esses endereços compridos e malucos em nomes de usuário exclusivos.", + "ens-welcome-point-customize": "Um nome ENS pode substituir seu nome aleatório de três palavras no chat. Seja @seunome em vez de {{name}}.", + "ens-welcome-point-customize-title": "Personalize seu nome de chat", + "ens-welcome-point-receive": "Outras pessoas podem enviar fundos para você pelo chat em um único passo.", + "ens-welcome-point-receive-title": "Receba transações no chat", + "ens-welcome-point-register": "Cadastre-se uma vez para manter o nome para sempre. Após 1 ano, você pode liberar o nome e recuperar seu SNT.", + "ens-welcome-point-register-title": "10 SNT para se cadastrar", + "ens-welcome-point-simplify": "Você pode receber fundos em um nome ENS fácil de compartilhar, em vez do seu endereço hexadecimal (0x ...).", + "ens-welcome-point-simplify-title": "Simplifique seu endereço ETH", + "ens-welcome-point-verify": "Você pode verificar e adicionar qualquer nome de usuário que você possui nos próximos passos.", + "ens-welcome-point-verify-title": "Já possui um nome de usuário?", + "ens-your-username": "Seu nome de usuário", + "ens-your-usernames": "Seus nomes de usuário", + "ens-your-your-name": "Seu nome ENS", + "enter-12-words": "Digite as 12 palavras da sua frase-semente, separadas por um espaço", + "enter-a-private-key": "Digite uma chave privada", + "enter-a-seed-phrase": "Digite uma frase-semente", + "enter-address": "Inserir endereço", + "enter-channel": "Entrar no canal", + "enter-contact-code": "ENS (vitalik94) ou chave de bate-papo (0x04 ...)", + "enter-pair-code": "Digite seu código de emparelhamento", + "enter-pair-code-description": "O código de emparelhamento pode ser definido a partir de um cliente Status já emparelhado", + "enter-password": "Digite a senha", + "enter-password-migration-prompt": "Digite sua senha para mover contatos, bate-papos e configurações com suas chaves", + "enter-pin": "Digite o código de acesso de 6 dígitos", + "enter-puk-code": "Digite o código PUK", + "enter-puk-code-description": "O código de acesso de 6 dígitos foi bloqueado. \n Digite o código PUK para desbloquear o código de acesso.", + "enter-recipient-address-or-username": "Digite o endereço ou nome de usuário do destinatário", + "enter-seed-phrase": "Inserir frase-semente", + "enter-url": "Inserir URL", + "enter-user-pk": "Insira a chave pública do usuário", + "enter-watch-account-address": "Escaneie um código QR \nou \ndigite o endereço para visualizar", + "enter-word": "Digite a palavra", + "enter-your-code": "Digite sua senha de 6 dígitos", + "enter-your-password": "Digite sua senha", + "error": "Erro", + "error-unable-to-get-balance": "Não foi possível obter o saldo", + "error-unable-to-get-prices": "Erro de conversão de moeda. Atualize sua tela para tentar novamente.", + "error-unable-to-get-token-balance": "Não foi possível obter o saldo de token", + "errors": "Erros", + "eth": "ETH", + "ethereum-address": "Endereço Ethereum", + "ethereum-node-started-incorrectly-description": "O nó Ethereum foi iniciado com a configuração incorreta. O aplicativo será interrompido para se recuperar dessa condição. ID de rede configurado = {{network-id}}, real = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Nó Ethereum iniciado incorretamente", + "etherscan-lookup": "Procurar no Etherscan", + "everyone": "Todos", + "expand-all": "Expandir todos", + "export-account": "Exportar conta", + "export-key": "Exportar chave privada", + "external-link": "Link externo", + "external-storage-denied": "Acesso ao armazenamento externo negado", + "failed": "Falhou", + "faq": "Perguntas frequentes", + "fast": "Rápido", + "favourite": "Favoritos", + "favourite-description": "Seus sites favoritos aparecerão aqui", + "favourites": "Favoritos", + "favourites-empty": "Os endereços adicionados aos favoritos aparecerão aqui", + "featured": "Destaque", + "fee-cap": "Limite de taxa", + "fee-explanation": "Preço total máximo para a transação. Se a taxa base do bloco exceder esse valor, ela será incluída em um bloco seguinte com uma taxa base mais baixa.", + "fee-options": "Opções de taxas sugeridas", + "fetch-community": "Buscar comunidade", + "fetch-messages": "↓ Buscar mensagens", + "fetch-timeline": "↓ Buscar", + "fetching-community": "Buscando comunidade…", + "find": "Encontrar", + "finish": "Terminar", + "finishing-card-setup": "Finalizando configurações do cartão", + "fleet": "Frota", + "fleet-settings": "Configurações de frota", + "follow": "Seguir", + "follow-your-interests": "Entre em um bate-papo público e conheça novas pessoas", + "free": "↓ Grátis", + "from": "De", + "gas-amount-limit": "Limite de quantidade de Gas", + "gas-limit": "Limite de gás", + "gas-price": "Preço do gás", + "gas-used": "Gás usado", + "gated": "Fechado", + "generate-a-key": "Gerar chaves", + "generate-a-new-account": "Gerar uma conta", + "generate-a-new-key": "Gere uma nova chave", + "generate-account": "Gerar chaves", + "generate-an-account": "Gerar uma conta", + "generate-new-key": "Gerar chaves", + "generating-codes-for-pairing": "> Download do software do produto no cartão \n> Gerando códigos de desbloqueio e emparelhamento", + "generating-keys": "Gerando chaves...", + "generating-mnemonic": "Gerando frase-semente", + "get-a-keycard": "Obtenha um Keycard", + "get-started": "Começar", + "get-status-at": "Obter o Status em http:\/\/status.im", + "get-stickers": "Obter Adesivos", + "gif": "GIF", + "give-a-short-description": "Dê uma breve descrição", + "give-a-short-description-community": "Dê uma breve descrição", + "give-permissions-camera": "Dar permissão\npara acessar a câmera", + "glossary": "Glossário", + "go-to-settings": "Vá para as configurações...", + "got-it": "Entendido", + "grant-face-id-permissions": "Para conceder a permissão de Face ID necessária, vá para as configurações do seu sistema e certifique-se de que Status > Face ID está selecionado", + "group-chat": "Conversa em grupo", + "group-chat-admin": "Administrador", + "group-chat-admin-added": "**{{member}}** foi adicionado como administrador", + "group-chat-all-contacts-invited": "Todos os seus contatos já estão no grupo", + "group-chat-created": "**{{member}}** criou o grupo **{{name}}**", + "group-chat-decline-invitation": "Recusar o convite", + "group-chat-member-added": "**{{member}}** foi convidado", + "group-chat-member-joined": "**{{member}}** acessou o grupo", + "group-chat-member-removed": "**{{member}}** saiu do grupo", + "group-chat-members-count": "{{selected}}\/{{max}} membros", + "group-chat-name-changed": "**{{member}}** mudou o nome do grupo para **{{name}}**", + "group-chat-no-contacts": "Você ainda não possui nenhum contato.\nConvide seus amigos para começar a conversar.", + "group-details": "Detalhes do grupo", + "group-info": "Informações do grupo", + "group-invite": "Convite para o grupo", + "group-invite-link": "Link de convite do grupo", + "group-membership-request": "Pedido para entrar no grupo", + "groups": "Grupos", + "gwei": "Gwei", + "has-permissions": "tem permissão para acessar", + "hash": "Hash", + "help": "ajuda", + "help-capitalized": "Ajuda", + "help-center": "Centro de ajuda", + "help-improve-status": "Ajude a melhorar o Status", + "hide": "Esconder", + "hide-content-when-switching-apps": "Bloquear capturas de tela", + "hide-content-when-switching-apps-ios": "Ocultar visualização", + "history": "Histórico", + "history-nodes": "Nós de mensagem da Status", + "hold-card": "Segure o cartão na parte de trás \n do seu telefone", + "home": "Início", + "hooks": "Hooks", + "how-it-works": "Como funciona", + "http-gateway-error": "Opa, falha na solicitação!", + "identifier": "Identificador", + "identity-verification": "Verificação de identidade", + "identity-verification-request": "Verificação de identidade", + "identity-verification-request-sent": "pergunta", + "image": "Imagem", + "image-remove-current": "Remover foto atual", + "image-source-gallery": "Escolher na galeria", + "image-source-make-photo": "Tirar foto", + "image-source-title": "Editar imagem", + "import": "Importar", + "import-community": "Importar uma comunidade", + "import-community-title": "Importar uma comunidade", + "in-contacts": "Em contatos", + "include": "Incluir", + "incoming": "Recebidos", + "incoming-transaction": "Transação recebida", + "incorrect-code": [ + "str", + "Desculpe, o código estava incorreto. Por favor, digite novamente" + ], + "increase-gas": "Aumentar o gás", + "initialization": "Inicialização", + "install": "Instalar", + "insufficient-balance-to-cover-fee": "saldo insuficiente para cobrir a taxa de transação", + "intro-message1": "Bem-vindo ao Status\nToque nesta mensagem para definir sua senha e começar!", + "intro-privacy-policy-note1": "O Status não coleta ou lucra com seus dados pessoais. Ao continuar, você concorda com a ", + "intro-privacy-policy-note2": "política de privacidade", + "intro-text": "Status é a sua porta de entrada para a web descentralizada", + "intro-text1": "Converse em uma rede criptografada ponto a ponto, onde as mensagens não podem ser censuradas ou invadidas", + "intro-text2": "Envie e receba ativos digitais em qualquer lugar do mundo - não é necessária uma conta bancária", + "intro-text3": "Explore jogos, trocas e redes sociais onde só você é o proprietário dos seus dados", + "intro-title1": "Comunicação verdadeiramente privada", + "intro-title2": "Carteira criptográfica segura", + "intro-title3": "Aplicativos descentralizados", + "intro-wizard-text1": "Um conjunto de chaves controla sua conta. Suas chaves estão no seu telefone, então somente você pode usá-las", + "intro-wizard-text2": "Uma chave é para o bate-papo. Ela vem com um nome legível que não pode ser alterado.", + "intro-wizard-text3": "Se você possui um Keycard, armazene suas chaves nele para maior segurança.", + "intro-wizard-text4": "Proteja e criptografe suas chaves", + "intro-wizard-text6": "O Status notificará você sobre novas mensagens. Você pode editar suas preferências de notificação mais tarde nas configurações", + "intro-wizard-title-alt4": "Crie uma senha", + "intro-wizard-title-alt5": "Confirme sua senha", + "intro-wizard-title1": "Obtenha suas chaves", + "intro-wizard-title2": "Escolha um nome de bate-papo", + "intro-wizard-title3": "Escolha o armazenamento de chaves", + "intro-wizard-title4": "Crie uma senha de 6 dígitos", + "intro-wizard-title5": "Confirme a senha", + "intro-wizard-title6": "Ativar notificações", + "introduce-yourself": "Apresente-se com uma breve mensagem", + "invalid-address-qr-code": "O código QR escaneado não contém um endereço válido", + "invalid-format": "Formato Inválido \n Deve ser {{format}}", + "invalid-key-confirm": "Aplicar", + "invalid-key-content": "O banco de dados não pode ser criptografado porque um arquivo está corrompido. Seus fundos e a chave de bate-papo estão seguros. Outros dados, como suas conversas e contatos, não podem ser restaurados. O botão \"{{erase-multiaccounts-data-button-text}}\" removerá todos os outros dados e permitirá que você acesse seus fundos e envie mensagens", + "invalid-number": "Número inválido", + "invalid-pairing-password": "Senha de emparelhamento inválida", + "invalid-public-chat-topic": "Tópico de chat público inválido", + "invalid-range": "Formato inválido, deve estar entre {{min}} e {{max}}", + "invalid-username-or-key": "Nome de usuário ou chave de bate-papo inválida", + "invite": "Convidar", + "invite-button": "Convidar", + "invite-chat-accept": "Aceitar", + "invite-chat-accept-join": "Aceitar e participar", + "invite-chat-intro": "Você foi indicado por um amigo para participar do Status. Aqui estão algumas criptomoedas para você começar! Use-as para cadastrar um nome ENS ou comprar um pacote de adesivos", + "invite-chat-name": "Indicação de amigo", + "invite-chat-pending": "Pendente", + "invite-chat-rule": "Aceitar também recompensará o seu amigo com um bônus de criptomoeda pela indicação", + "invite-chat-starter-pack": "Pacote Inicial", + "invite-contacts": "Convidar pessoas da lista de contatos", + "invite-friends": "Convide amigos", + "invite-friends-to-status": "Convidar amigos ao Status", + "invite-instruction-fifth": "Você pode optar por resgatar seu bônus de indicação a qualquer momento.", + "invite-instruction-first": "Você envia um link de convite exclusivo para seu amigo fazer o download e entrar no Status", + "invite-instruction-fourth": "Você recebe seu bônus de indicação e seu amigo recebe o Pacote inicial", + "invite-instruction-second": "Seu amigo baixa o Status e cria uma conta (no Android)", + "invite-instruction-third": "É iniciada uma conversa com seu amigo, onde ele confirma sua indicação", + "invite-people": "Convide pessoas", + "invite-people-from-contacts": "Convidar pessoas da lista de contatos", + "invite-privacy-policy-public": "Você instalou o Status por meio de um link de indicação. Ao entrar nesta conversa, você confirma seu referenciador e concorda com os", + "invite-privacy-policy1": "Ao aceitar você concorda com o programa de indicação", + "invite-privacy-policy2": "Termos e Condições.", + "invite-public-chat-home": "Convite de indicação", + "invite-public-chat-intro": "Aqui estão algumas criptomoedas para você começar! Use-as para registrar um nome ENS ou comprar um pacote de adesivos", + "invite-receive-account": "Conta para receber o seu bônus de indicação", + "invite-reward": "Ganhe criptomoedas para cada amigo que você convidar!", + "invite-reward-friend": "Amigo:", + "invite-reward-friend-description": "Seu amigo receberá um Pacote Inicial que consiste em {{reward}} para começar", + "invite-reward-friend-name": "Pacote Inicial", + "invite-reward-you": "Você: ", + "invite-reward-you-description": "Convide um amigo e receba {{reward}} como bônus pela indicação. Utilize para comprar adesivos, um nome ENS e testar alguns DApps.", + "invite-reward-you-name": "Bônus de indicação", + "invite-select-account": "Selecione uma conta para receber o seu bônus de indicação", + "invite-warning": "Esta promoção é válida apenas para usuários de um dispositivo Android, que não sejam residentes dos EUA. O amigo precisa confirmar a indicação dentro de 7 dias", + "invited": "convidou", + "italic": "Itálico", + "join": "Participar", + "join-a-community": "ou junte-se a uma comunidade", + "join-group-chat": "Acessar grupo", + "join-group-chat-description": "{{username}} convidou você para ingressar no grupo {{group-name}}", + "join-me": "Junte-se a mim no Status: {{url}}", + "join-new-private-chat": "Iniciar uma nova conversa privada", + "join-new-public-chat": "Participar do chat público", + "join-open-community": "Junte-se à Comunidade", + "joined": "Entrou", + "joined-group-chat-description": "Você ingressou no {{group-name}} por convite de {{username}}", + "key": "Chave", + "key-managment": "Gerenciamento de chave", + "key-on-device": "A chave privada está salva neste dispositivo", + "keycard": "Keycard", + "keycard-access-reset": "O acesso ao Keycard foi redefinido", + "keycard-applet-install-instructions": "Para instalar o applet, siga as instruções em https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Tente mover o cartão para encontrar o leitor NFC em seu dispositivo", + "keycard-awaiting-title": "Ainda procurando…", + "keycard-backup": "Crie um Keycard de backup", + "keycard-backup-success-body": "Cartão de backup criado com sucesso. Agora você pode usá-lo com sua conta, assim como o cartão principal.", + "keycard-backup-success-title": "Backup bem-sucedido", + "keycard-blocked": "O keycard foi bloqueado. \nVocê precisa redefinir o cartão para continuar usando.", + "keycard-can-use-with-new-passcode": "Você pode usar este cartão com sua nova senha", + "keycard-cancel-setup-text": "Isso cancelará a configuração do Keycard. É altamente recomendável concluir a configuração para usar o keycard. Deseja mesmo cancelar?", + "keycard-cancel-setup-title": "Operação perigosa", + "keycard-connected-description": "Tente manter o cartão parado", + "keycard-connected-title": "Conectado", + "keycard-desc": "Tem um Keycard? Armazene suas chaves nele. Você precisará delas para as transações", + "keycard-dont-ask-card": "Não peça o cartão para entrar", + "keycard-enter-new-passcode": "Digite a nova senha {{step}} \/ 2", + "keycard-error-description": "Conecte o cartão novamente para continuar", + "keycard-error-title": "Conexão perdida", + "keycard-factory-reset": "Retornar cartão para as configurações de fábrica", + "keycard-factory-reset-text": "Fazer isso excluirá qualquer frase mnemônica armazenada no cartão. Certifique-se de ter um backup da frase mnemônica que está usando com este Keycard.", + "keycard-factory-reset-title": "Tem certeza de que deseja redefinir os padrões de fábrica?", + "keycard-free-pairing-slots": "O Keycard tem {{n}} portas de emparelhamento livres", + "keycard-has-multiaccount-on-it": "Este cartão está cheio. Cada cartão pode conter um par de chaves principal", + "keycard-init-description": "Coloque o cartão na parte de trás do telefone para continuar", + "keycard-init-title": "Procurando cartões…", + "keycard-is-blocked-details": "Você não pode mais usar este cartão para acessar ou assinar esta conta. Houve muitas tentativas falhas de senha e PUK.", + "keycard-is-blocked-instructions": "Para acessar sua conta, você precisará redefinir seu cartão para os padrões de fábrica. Toque no botão abaixo para iniciar o procedimento. Você precisará da sua frase-semente.", + "keycard-is-blocked-title": "O Keycard está bloqueado", + "keycard-is-frozen-details": "Para proteger os seus bens, o seu cartão travou. Redefina o acesso ao cartão para desbloquear e enviar transações. Você pode fazer isso com seu PUK ou seu mnemônico.", + "keycard-is-frozen-factory-reset": "Redefinir com mnemônica", + "keycard-is-frozen-reset": "Reiniciar com o PUK", + "keycard-is-frozen-title": "O Keycard travou", + "keycard-onboarding-finishing-header": "Finalizando", + "keycard-onboarding-intro-header": "Armazene suas chaves no Keycard", + "keycard-onboarding-intro-text": "Prepare-se, isso pode levar alguns minutos, mas é importante proteger sua conta", + "keycard-onboarding-mnemonic-text": "Você também precisará de um pedaço de papel e um lápis para escrever sua frase-semente.", + "keycard-onboarding-pairing-header": "Emparelhando o cartão...", + "keycard-onboarding-pin-text": "Você precisará criar uma senha de 6 dígitos que será usada para proteger o acesso ao seu Keycard.", + "keycard-onboarding-preparing-header": "Preparando o cartão...", + "keycard-onboarding-puk-code-header": "Anote os códigos e\n guarde de forma segura", + "keycard-onboarding-recovery-phrase-description": "Você precisa dessa frase-semente para recuperar sua chave. Anote e mantenha sua frase-semente offline em segurança, separada deste dispositivo.", + "keycard-onboarding-recovery-phrase-header": "Fazer backup da frase-semente", + "keycard-onboarding-recovery-phrase-text": "Somente para você ver. Esta é a frase-semente mágica usada para gerar sua chave.", + "keycard-onboarding-start-header": "Segure o cartão na parte de trás \n do seu telefone para começar", + "keycard-onboarding-start-step1": "Crie uma senha", + "keycard-onboarding-start-step1-text": "Em torno de 1 minuto. Crie uma senha de 6 dígitos para criptografar suas chaves", + "keycard-onboarding-start-step2": "Anote o PUK e o código de emparelhamento", + "keycard-onboarding-start-step2-text": "Em torno de 1 minuto. Você vai precisar de um pedaço de papel e um lápis para isso", + "keycard-onboarding-start-step3": "Faça backup da frase-semente", + "keycard-onboarding-start-step3-text": "Em torno de 1 minuto. Também são necessários um pedaço de papel e um lápis", + "keycard-onboarding-start-text": "E mantenha o cartão em contato com o telefone\n durante a configuração. A configuração levará cerca de 4 minutos", + "keycard-processing-description": "Tente manter o cartão parado", + "keycard-processing-title": "Processando…", + "keycard-recover": "cartão perdido ou travado?", + "keycard-recover-text": "Se você tem sua frase mnemônica, poderá criar um novo Keycard associado a esta conta. Você pode usar um novo Keycard ou redefinir os padrões de fábrica em um cartão travado.", + "keycard-recover-title": "Criar um novo cartão para esta conta?", + "keycard-recovery-intro-button-text": "Iniciar a recuperação", + "keycard-recovery-intro-header": "Recuperar chaves armazenadas no keycard", + "keycard-recovery-intro-text": "Se você gerou chaves usando um keycard antes e agora quer usar estas chaves neste dispositivo", + "keycard-recovery-no-key-header": "Não há nada para \n recuperar aqui.", + "keycard-recovery-no-key-text": "Seu Keycard não tem nenhuma chave armazenada. Para usá-lo, gere uma nova chave e escolha seu Keycard para armazenar a chave", + "keycard-recovery-phrase-confirm-header": "Confirmar frase-semente", + "keycard-recovery-phrase-confirmation-text": "Você não terá uma segunda chance! Se você perder o acesso, por exemplo ao perder seu keycard, só poderá acessar suas chaves com sua frase-semente. Somente você tem sua frase-semente. Anote e mantenha sua frase-semente em segurança.", + "keycard-recovery-phrase-confirmation-title": "Anotou a frase-semente?", + "keycard-recovery-success-header": "Suas chaves foram \n recuperadas com sucesso", + "keycard-redeem-title": "Resgatar para", + "keycard-redeem-tx": "Resgatar bens", + "keycard-redeem-tx-desc": "Toque no cartão para assinar e receber bens", + "keycard-reset-passcode": "Redefinir senha", + "keycard-success-description": "Você pode remover o cartão agora", + "keycard-success-title": "Sucesso", + "keycard-unauthorized-operation": "Você não está autorizado a realizar esta operação. \n Por favor, utilize o cartão válido e tente novamente.", + "keycard-upsell-subtitle": "Maior segurança e facilidade", + "kicked": "Cutucado", + "language": "Idioma", + "last-backup-performed": "Última cópia de segurança executada", + "last-transaction": "Última transação", + "learn-more": "Saiba mais", + "learn-more-about-keycard": "Saiba mais sobre o Keycard ", + "leave": "Sair", + "leave-chat": "Sair da conversa", + "leave-chat-confirmation": "O histórico do chat será removido do seu dispositivo. Depois de voltar você não será capaz de recuperar seu histórico.", + "leave-community": "Deixar a comunidade", + "leave-community-message": "Ficaremos tristes em vê-lo partir, mas lembre-se, você pode voltar a qualquer momento!", + "leave-community?": "Sair da comunidade?", + "leave-confirmation": "Sair de {{chat-name}}", + "leave-group": "Sair do grupo", + "leave-group?": "Deixar o Grupo?", + "left": "saiu", + "les-ulc": "LES\/ULC", + "lets-go": "Vamos lá", + "lifestyle": "Lifestyle", + "light": "Claro", + "limit": "Limite", + "link-to-community": "Link para a comunidade", + "linked-on": "Vinculado em {{date}}", + "load-messages-before": "antes de {{date}}", + "load-more-messages": "↓ Buscar mais mensagens", + "load-more-timeline": "↓ Buscar mais", + "loading": "Carregando...", + "local-notifications": "Notificações locais", + "local-notifications-subtitle": "Ativar serviço em segundo plano", + "lock-app-with": "Bloquear aplicativo com", + "log-level": "Nível de registro", + "log-level-settings": "Configurações de nível de registro", + "logging": "Registro", + "logging-enabled": "Registro ativado?", + "login-pin-description": "Digite seu código de acesso de 6 dígitos para desbloquear suas chaves", + "logout": "Sair", + "logout-app-content": "A conta será desconectada. Quando você a desbloquear novamente, a rede selecionada será usada", + "logout-are-you-sure": "Você tem certeza que quer \n sair?", + "logout-key-management": "Você precisa se desconectar para acessar o gerenciamento de chaves.", + "logout-title": "Sair?", + "looking-for-cards": "Procurando cartões…", + "lost-connection": "Conexão perdida", + "low-tip": "a gorjeta é muito baixa", + "lower-than-average-tip": "gorjeta inferior à média", + "mail-should-be-configured": "O cliente de e-mail deve ser configurado", + "mailserver-address": "Endereço do nó do Status", + "mailserver-automatic": "Seleção automática", + "mailserver-automatic-switch-explanation": "Escolha o nó da Status mais rápido disponível", + "mailserver-connection-error": "Não foi possível conectar ao nó do Status", + "mailserver-content": "Um nó na rede Status que encaminha e armazena mensagens por até 30 dias.", + "mailserver-details": "Detalhes do nó do Status", + "mailserver-error-content": "O nó do Status selecionado não pôde ser localizado.", + "mailserver-error-title": "Erro ao conectar o nó do Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Escolha outro nó do Status", + "mailserver-reconnect": "Não foi possível conectar ao nó do Status. Toque para reconectar", + "mailserver-request-error-content": "O seguinte erro foi retornado pelo nó do Status: {{error}}", + "mailserver-request-error-status": "Um erro ocorreu ao buscar o histórico, verifique os registros para obter detalhes", + "mailserver-request-error-title": "Erro de comunicação com o nó da Status", + "mailserver-request-retry": "Solicitar nova tentativa", + "mailserver-retry": "Tente novamente", + "mailserver-title": "Nó de mensagem Status", + "main-account": "Conta principal", + "main-currency": "Moeda principal", + "main-networks": "Redes principais", + "main-wallet": "Carteira principal", + "mainnet-network": "Rede principal", + "make-admin": "Tornar-se administrador", + "make-moderator": "Tornar moderador", + "manage-connections": "Gerenciar conexões de dentro do Application Connections", + "manage-keys-and-storage": "Gerenciar chaves e armazenamento", + "manage-members": "Gerenciar membros", + "mark-all-read": "Marcar todas como lidas", + "mark-as-read": "Marcar como lido", + "mark-untrustworthy": "Marcar como Não Confiável", + "mark-user-untrustworthy": "Marcar {{username}} como não confiável", + "master-account": "Conta mestre", + "max-fee": "Taxa máxima", + "max-priority-fee": "Taxa máxima de prioridade", + "maximum-fee": "Taxa máxima", + "maximum-fee-desc": "Preço total máximo para a transação. Se a taxa base do bloco atual exceder isso, sua transação será incluída em um bloco seguinte com uma taxa base mais baixa.", + "maybe-later": "Talvez mais tarde", + "member-ban": "Banir membro", + "member-kick": "Expulsar membro", + "members": { + "one": "1 membro", + "other": "{{count}} membros" + }, + "members-active": { + "one": "1 membro", + "other": "{{count}} membros" + }, + "members-active-none": "sem membros", + "members-count": "{{count}} membros", + "members-label": "Membros", + "members-limit-reached": "Limite de membros atingido", + "members-title": "Membros", + "membership": "Adesão", + "membership-approval": "Exigir aprovação", + "membership-approval-description": "Sua comunidade é livre para participar, mas novos membros são obrigados a ser aprovados pelo criador da comunidade primeiro", + "membership-button": "Requisitos de participação", + "membership-declined": "O pedido de entrada no grupo foi recusado", + "membership-description": "A entrada no grupo exige que você seja aceito pelo administrador do grupo", + "membership-ens": "Exigir nome de usuário ENS", + "membership-ens-description": "Sua comunidade requer um nome de usuário ENS para poder participar", + "membership-free": "Nenhuma exigência", + "membership-free-description": "Sua comunidade é livre para que qualquer pessoa possa participar", + "membership-invite": "Exigir convite de outro membro", + "membership-invite-description": "Sua comunidade só pode ser acessada por convite de membros existentes da comunidade", + "membership-none": "Nenhum", + "membership-none-placeholder": "Você pode exigir que novos membros atendam a certos critérios antes que eles possam participar. Isso pode ser alterado a qualquer momento", + "membership-request-denied": "Pedido de participação negado", + "membership-request-pending": "Pedido de participação pendente", + "membership-requests": "Pedidos de participação", + "membership-title": "Requisitos de participação", + "mention": "Menção", + "mentions": "Menções", + "message": "Mensagem", + "message-deleted": "Mensagem apagada", + "message-deleted-for-everyone": "Mensagem apagada para todos", + "message-deleted-for-you": "Mensagem excluída para você", + "message-not-sent": "Mensagem não enviada", + "message-options-cancel": "Cancelar", + "message-reply": "Responder", + "messages": "Mensagens", + "messages-from-contacts-only-subtitle": "Apenas as pessoas que você adicionou como contatos podem iniciar um novo bate-papo com você ou convidá-lo para um grupo", + "messages-gap-warning": "Algumas mensagens podem estar faltando", + "might-break": "Pode quebrar alguns ÐApps", + "migration-successful": "Migração realizada com sucesso", + "migration-successful-text": "Conta migrada com sucesso para o Keycard", + "migrations-failed-content": "{{message}} \n versão do esquema: inicial {{initial-version}} , atual {{current-version}} , última {{last-version}} \n\nOcorreu um erro no banco de dados. Seus fundos e chave de bate-papo estão seguros. Outros dados, como bate-papos e contatos, não podem ser restaurados. O botão \"{{erase-multiaccounts-data-button-text}}\" irá remover todos os outros dados e permitir que você acesse seus fundos e envie mensagens.", + "miners-higher-fee": "Os mineradores provavelmente incluirão sua transação mais cedo se você pagar uma taxa mais alta.", + "minimum-received": "Mínimo recebido", + "mobile-network-ask-me": "Pergunte-me quando estiver na rede móvel", + "mobile-network-continue-syncing": "Continuar a sincronizar", + "mobile-network-continue-syncing-details": "Você pode alterar isso mais tarde nas configurações", + "mobile-network-go-to-settings": "Ir para configurações", + "mobile-network-settings": "Dados móveis", + "mobile-network-sheet-configure": "Você pode configurar a sincronização com mais \ndetalhes em", + "mobile-network-sheet-offline": "Sem Wi-Fi, sincronização de mensagens desativada.", + "mobile-network-sheet-offline-details": "A sincronização usando a rede móvel está desativada", + "mobile-network-sheet-remember-choice": "Lembrar a minha escolha", + "mobile-network-sheet-settings": "configurações", + "mobile-network-start-syncing": "Iniciar sincronização", + "mobile-network-stop-syncing": "Parar de sincronizar", + "mobile-network-stop-syncing-details": "Está conectado ao Wi-Fi?", + "mobile-network-use-mobile": "Utilizar dados móveis", + "mobile-network-use-mobile-data": "O Status usa muitos dados ao sincronizar bate-papos e carteira.", + "mobile-network-use-wifi": "Somente Wi-Fi", + "mobile-syncing-sheet-details": "O Status usa muitos dados ao sincronizar bate-papos e carteira.", + "mobile-syncing-sheet-title": "Sincronizar usando dados móveis?", + "more": "mais", + "move-and-reset": "Mover e reiniciar", + "move-keystore-file": "Mover arquivo de armazenamento de chaves", + "move-keystore-file-to-keycard": "Mover arquivo de armazenamento de chaves para o Keycard?", + "multiaccount-exists-content": "Já existem chaves para esta conta e não podem ser adicionadas novamente. Se você perdeu sua senha, código de acesso ou Keycard, desinstale o aplicativo, reinstale e acesse suas chaves inserindo sua frase-semente", + "multiaccount-exists-title": "Já existem chaves para esta conta", + "multiaccounts-recover-enter-phrase-text": "Digite 12, 15, 18, 21 ou 24 palavras. \nSepare as palavras por um único espaço.", + "multiaccounts-recover-enter-phrase-title": "Digite sua frase-semente", + "music": "Música", + "mutal-contacts": "Contatos mútuos", + "mute": "Silenciar", + "mute-chat": "Silenciar chat", + "mute-community": "Silenciar comunidade", + "mute-group": "Silenciar grupo", + "mutual-contact-requests": "Solicitações de contato mútuo", + "my-accounts": "Minhas contas", + "my-accounts-empty": "Suas contas disponíveis aparecerão aqui", + "my-profile": "Meu perfil", + "my-status": "Meu status", + "n-photos": "{{count}} fotos", + "name": "Nome", + "name-of-token": "O nome do seu token", + "name-optional": "Nome (opcional)", + "name-your-channel": "Dê um nome ao seu canal", + "name-your-channel-placeholder": "Nome do canal", + "name-your-community": "Dê um nome para sua comunidade", + "name-your-community-placeholder": "Um nome atraente", + "need-help": "Precisa de ajuda?", + "negative": "Negativo", + "network": "Rede", + "network-chain": "Cadeia de rede", + "network-details": "Detalhes da rede", + "network-fee": "Taxa de rede", + "network-id": "ID de rede", + "network-info": "Informações da rede", + "network-invalid-network-id": "O ID da rede especificado não corresponde ao ID da rede pelo URL RPC", + "network-invalid-status-code": "Código de status inválido: {{code}}", + "network-invalid-url": "URL da rede é inválida", + "network-settings": "Configurações de rede", + "never": "Nunca", + "new": "Novo", + "new-category": "Nova categoria", + "new-chat": "Nova conversa", + "new-community-title": "Nova comunidade", + "new-contact": "Novo contato", + "new-contract": "Novo Contrato", + "new-favourite": "Novo favorito", + "new-group": "Novo grupo", + "new-group-chat": "Novo grupo de bate-papo", + "new-messages-header": "Novas Mensagens", + "new-network": "Nova rede", + "new-password": "Nova Senha", + "new-pin-description": "Digite o novo código de acesso de 6 dígitos", + "new-public-group-chat": "Participar do bate-papo público", + "new-puk-description": "Insira o novo PUK de 12 dígitos", + "new-status": "Novo status", + "new-tab": "Nova aba", + "new-ui": "Nova UI", + "next": "Próximo", + "nickname": "Apelido", + "nickname-description": "Os apelidos ajudam você a identificar as pessoas no Status. \n Só você pode ver os apelidos que adicionou", + "no": "Não", + "no-collectibles": "Não há colecionáveis disponíveis", + "no-contacts": "Você ainda não tem contatos", + "no-keycard-applet-on-card": "Não há applet Keycard no cartão", + "no-messages": "Nenhuma mensagem", + "no-pairing-slots-available": "Este cartão já está emparelhado com 5 dispositivos e não pode ser emparelhado com este. Use um dos dispositivos emparelhados, faça login com este cartão e libere uma porta de emparelhamento no cartão", + "no-pinned-messages": "Nenhuma mensagem fixada", + "no-pinned-messages-community-desc": "Este canal não tem mensagens fixadas.", + "no-pinned-messages-desc": "Este chat não tem mensagens fixadas.", + "no-result": "Nenhum resultado", + "no-thanks": "Não, obrigado", + "no-tokens-found": "Nenhum token encontrado", + "node-address": "Endereço do nó", + "node-details": "Detalhes do nó", + "node-info": "Informação do nó", + "node-version": "Versão do nó", + "nodes-disabled": "Nós do Status desativados", + "non-archival-node": "O endpoint RPC não oferece suporte a solicitações de arquivamento. Seu histórico de transferências locais pode estar incompleto.", + "non-contacts": "Sem contatos", + "nonce": "Nonce", + "none": "Nenhum", + "normal": "Normal", + "not-applicable": "Não aplicável a transações não assinadas", + "not-connected-nodes": "Não conectado a um nó do Status", + "not-connected-to-peers": "Não conectado a nenhum par", + "not-enough-snt": "SNT insuficiente", + "not-found": "Não encontrado", + "not-keycard-text": "O cartão que você usou não é um Keycard. Você precisa comprar um Keycard para usá-lo", + "not-keycard-title": "Não é um Keycard", + "not-registered": "não cadastrado", + "notification-settings": "Notificações", + "notifications": "Notificações", + "notifications-non-contacts": "Notificações de não contatos", + "notifications-preferences": "Preferências de notificação", + "notifications-servers": "Servidores de notificação", + "notifications-switch": "Mostrar notificações", + "notifications-transactions": "Transações da carteira", + "notify": "Notificar", + "now": "agora", + "off": "Desativado", + "off-status-tree": "Árvore Status desativada", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Ative os nós da Status para buscar mensagens enviadas enquanto o aplicativo está fechado. Quando ativado, um nó de histórico obtém seu endereço IP. Quando desativado, você não receberá mensagens quando o aplicativo for fechado e não as verá quando abrir o app mais tarde.", + "offline-messaging-use-history-nodes": "Usar nós de mensagem da Status", + "ok": "OK", + "ok-continue": "Ok, continuar", + "ok-got-it": "Ok, entendido", + "ok-save-pass": "OK, salvar senha", + "okay": "OK", + "on": "Ativado", + "on-status-tree": "Árvore Status ativada", + "once-enabled-share-metadata": "Uma vez habilitados, os links postados no chat podem compartilhar seus metadados com o site", + "one-day": "Um dia", + "one-month": "Um mês", + "one-week": "Uma semana", + "online": "Online", + "only-mentions": "Apenas @menções", + "open": "Abrir", + "open-chat": "Abrir chat", + "open-dapp": "Abrir ÐApp", + "open-dapp-store": "Descubra ÐApps", + "open-home": "Abrir...", + "open-in-new-tab": "Abrir em nova aba", + "open-membership": "Participação aberta", + "open-nfc-settings": "Abrir configurações do NFC", + "open-on-block-explorer": "Abrir no explorador de blocos", + "opened": "Aberto", + "opening-buy-crypto": "Abrindo {{site}}...", + "optimal": "Ótimo", + "optional": "opcional", + "or": "ou", + "outgoing": "Enviados", + "outgoing-transaction": "Transação enviada", + "owner": "Proprietário", + "page-camera-request-blocked": "Solicitações de câmera bloqueadas. Para habilitar as solicitações de câmera, vá em Configurações", + "page-would-like-to-use-camera": "gostaria de usar sua câmera", + "pair": "Sincronizar dispositivos", + "pair-card": "Sincronizar para este dispositivo", + "pair-code": "Código de par", + "pair-code-explanation": "Emparelha o cartão com um dispositivo diferente (até 5) para desbloquear as chaves e assinar transações com o mesmo Keycard", + "pair-code-placeholder": "Código de emparelhamento…", + "pair-this-card": "Emparelhar este cartão", + "pair-this-device": "Anunciar dispositivo", + "pair-this-device-description": "Emparelhe seus dispositivos para sincronizar contatos e bate-papos entre eles", + "paired-devices": "Dispositivos sincronizados", + "pairing": "Emparelhamento", + "pairing-card": "Cartão de emparelhamento", + "pairing-changed": "Código de emparelhamento foi alterado", + "pairing-code-placeholder": "Código de emparelhamento...", + "pairing-code_error1": "Os códigos de emparelhamento não correspondem.", + "pairing-go-to-installation": "Ir para as configurações de sincronização", + "pairing-maximum-number-reached-content": "Desative um dos seus dispositivos antes de ativar um novo.", + "pairing-maximum-number-reached-title": "Atingido o número máximo de dispositivos", + "pairing-new-installation-detected-content": "Um novo dispositivo foi detectado. \nPara usar seus dispositivos corretamente, é importante emparelhá-los e ativá-los antes de usar. \nAcesse a seção configurações dos dispositivos para parear seus dispositivos.", + "pairing-new-installation-detected-title": "Novo dispositivo detectado", + "pairing-no-info": "Sem informações", + "pairing-please-set-a-name": "Defina um nome para o seu dispositivo.", + "passphrase": "Frase-semente", + "password": "Senha", + "password-description": "Pelo menos 6 caracteres. Sua senha protege suas chaves. Você precisa dela para desbloquear o Status e fazer transações.", + "password-mismatch": "A nova senha e a confirmação não correspondem", + "password-placeholder": "Senha…", + "password-placeholder2": "Confirme sua senha", + "password-reset-in-progress": "Alterando senha...", + "password-reset-success": "Redefinição de senha", + "password-reset-success-message": "Você precisará entrar novamente", + "password_error1": "As senhas não correspondem.", + "paste": "Colar", + "paste-json": "Colar JSON", + "pay-to-chat": "Pagar para conversar", + "peer-content": "Um dispositivo conectado à rede de bate-papo Status. Cada usuário pode representar um ou mais pares, dependendo do número de dispositivos.", + "peer-title": "Par", + "peer-to-peer": "P2P", + "peers": "Pares", + "peers-count": "Contagem de pares", + "peers-stats": "Estatísticas de pares", + "pending": "Pendente", + "pending-confirmation": "Confirmação pendente...", + "pending-invitations": "Pedidos de entrada pendentes", + "pending-requests": "Solicitações pendentes", + "per-gas-price-limit": "Limite de preço por gás", + "per-gas-tip-limit": "Limite de gorjeta\npor gás", + "perform-backup": "Fazer cópia de segurança", + "permissions": "Permissões", + "phone-e164": "Internacional 1", + "photos": "Fotos", + "photos-access-error": "Para conceder a permissão de fotos necessária, acesse as configurações do sistema e verifique se Status > Fotos está selecionado.", + "pin": "Fixar", + "pin-changed": "O código de acesso de 6 dígitos foi alterado", + "pin-code": "Código de acesso de 6 dígitos", + "pin-limit-reached": "Limite de mensagens fixadas atingido. Desafixe uma mensagem anterior primeiro.", + "pin-mismatch": "Código de acesso incorreto", + "pin-one-attempt": "uma tentativa", + "pin-one-attempt-blocked-after": "antes que o seu Keycard seja bloqueado", + "pin-one-attempt-blocked-before": "Cuidado, você só tem", + "pin-one-attempt-frozen-after": "antes que o seu Keycard trave", + "pin-one-attempt-frozen-before": "Cuidado, você só tem", + "pin-retries-left": "{{number}} tentativas restantes", + "pin-to-channel": "Fixar ao canal", + "pin-to-chat": "Fixar na conversa", + "pinned-a-message": "fixou uma mensagem", + "pinned-by": "Fixado por", + "pinned-messages": "Mensagens fixadas", + "pinned-messages-2": "Mensagens\n fixadas", + "pinned-messages-count": { + "one": "1 mensagem fixada", + "other": "{{count}} mensagens fixadas" + }, + "pinned-messages-empty": "As mensagens fixadas aparecerão aqui. Para fixar uma mensagem, pressione e segure e toque em `Fixar`", + "podcasts": "Podcasts", + "positive": "Positivo", + "powered-by-paraswap": "Desenvolvido por Paraswap", + "preference": "Preferência", + "preview-privacy": "Visualizar modo de privacidade", + "previewing-may-share-metadata": "Visualizar links desses sites pode compartilhar seus metadados com os proprietários", + "price-impact": "Impacto do preço", + "price-impact-desc": "Impacto de preço estimado para esta transação. Se a taxa base do bloco atual exceder isso, sua transação será incluída em um bloco seguinte com uma taxa base mais baixa.", + "principles": "Princípios", + "priority": "Prioridade", + "privacy": "Privacidade", + "privacy-and-security": "Privacidade e segurança", + "privacy-photos": "Privacidade da Foto de Perfil", + "privacy-policy": "Política de privacidade", + "privacy-show-to-warning": "As pessoas que já viram sua foto de perfil continuarão vendo", + "private-key": "Chave privada", + "private-notifications": "Notificações privadas", + "private-notifications-descr": "O Status notificará você sobre novas mensagens. Você pode editar suas preferências de notificação nas configurações.", + "processing": "Aguarde um momento", + "product-information": "Informações do produto", + "profile": "Perfil", + "profile-deleted-content": "O seu perfil foi excluído com sucesso", + "profile-deleted-keycard": "Agora você pode restaurar outro par de chaves no seu Keycard", + "profile-deleted-title": "Perfil excluído", + "profile-details": "Detalhes do perfil", + "profile-not-found": "Perfil não encontrado", + "profile-pic-pick": "Escolher na galeria", + "profile-pic-remove": "Remover foto", + "profile-pic-take": "Tirar foto", + "profile-picture-updated": "Foto do perfil atualizada", + "public": "Público", + "public-channel": "Canal público", + "public-chat": "Conversa pública", + "public-chat-description": "Participe de bate-papos públicos sobre seus interesses! Qualquer um pode começar uma nova sala.", + "public-chats": "Conversas públicas", + "public-group-status": "Público", + "public-group-topic": "Assunto", + "public-key": "Chave pública", + "puk-and-pairing-codes-displayed": "Códigos PUK e de emparelhamento exibidos", + "puk-changed": "PUK de 12 dígitos foi alterado", + "puk-code": "Código PUK", + "puk-code-explanation": "Se você esquecer seu código de acesso de 6 dígitos ou inseri-lo incorretamente três vezes, precisará desse código para desbloquear seu cartão.", + "puk-mismatch": "Código PUK errado", + "push-failed-transaction": "Falha na sua transação", + "push-failed-transaction-body": "{{value}} {{currency}} para {{to}}", + "push-inbound-transaction": "Você recebeu {{value}} {{currency}}", + "push-inbound-transaction-body": "De {{from}} para {{to}}", + "push-notifications-server-enabled": "Servidor habilitado", + "push-notifications-servers": "Servidores de notificação push", + "push-outbound-transaction": "Você enviou {{value}} {{currency}}", + "push-outbound-transaction-body": "De {{from}} para {{to}}", + "quiet-days": "{{quiet-days}} dias", + "quiet-hours": "{{quiet-hours}} horas", + "re-encrypt-key": "Recriptografe suas chaves", + "rearrange-categories": "Reorganizar Categorias", + "receive": "Receber", + "receive-transaction": "Receber transação", + "received": "Recebido", + "recent": "Recentes", + "recent-empty": "Os endereços usados recentemente aparecerão aqui", + "recent-recipients": "Contatos", + "recently-used-stickers": "Os adesivos usados recentemente aparecerão aqui", + "recipient": "Destinatário", + "recipient-code": "Digite o endereço do destinatário", + "recipient-code-placeholder": "0x… ou nome.dominio.eth", + "recover": "Recuperar", + "recover-key": "Acessar chaves existentes", + "recover-keycard-multiaccount-not-supported": "Já existem chaves para esta conta e não podem ser adicionadas novamente. Se você perdeu sua senha, código de acesso ou Keycard, desinstale o aplicativo, reinstale e acesse suas chaves inserindo sua frase-semente", + "recover-with-keycard": "Recuperar com Keycard", + "recover-with-seed-phrase": "Recupere com a frase-semente", + "recovering-key": "Acessando chaves…", + "recovery-confirm-phrase": "Confirmar frase-semente", + "recovery-phrase": "Frase-semente", + "recovery-success-text": "Você terá que definir um novo código ou senha para recriptografar suas chaves", + "recovery-typo-dialog-description": "Observe que sua frase-semente deve usar exatamente as mesmas palavras e ordem que você recebeu", + "recovery-typo-dialog-title": "A frase-semente está correta?", + "redeem-amount": "{{quantity}} bônus disponíveis", + "redeem-now": "Resgatar agora", + "redeem-success": "Resgate de bônus com sucesso!", + "reduced-tip": "a gorjeta prioritária será reduzida", + "refresh": "Atualizar", + "registered": "cadastrado", + "reject": "Rejeitar", + "reject-and-delete": "Rejeitar e apagar", + "remember-me": "Lembrar-me", + "remind-me-later": "Mostrar isso novamente", + "remote-notifications": "Notificações remotas", + "remote-notifications-subtitle": "Ativar notificações push do google", + "remove": "Remover", + "remove-favourite": "Remover favorito", + "remove-from-chat": "Remover do chat", + "remove-from-contacts": "Remover dos contatos", + "remove-from-contacts-text": "Ao remover um usuário da sua lista de contatos, você não esconde dele o endereço da sua carteira", + "remove-group": "Remover grupo", + "remove-network": "Remover rede", + "remove-token": "Remover token", + "remove-user-from-group": "Remover {{username}} do grupo", + "removed": "removeu", + "removed-from-contacts": "Removido dos contatos", + "rename": "Renomear", + "repeat-pin": "Repita os 6 novos dígitos do código de acesso", + "repeat-puk": "Repita o novo PUK de 12 dígitos", + "replied": "Respondeu", + "replies": "Respostas", + "replying-to": "Respondendo a {{author}}", + "report-bug-email-template": "1. Descrição do problema \n (Descreva o recurso que você deseja ou resuma brevemente o bug e o que você fez, o que esperava que acontecesse e o que realmente acontece. Seções abaixo) \n\n\n 2. Passos para reproduzir \n (Descreva como podemos replicar o bug passo a passo.) \n - Abrir Status\n - ... \n - Etapa 3, etc. \n\n\n 3. Comportamento esperado \n (Descreva o que você esperava que acontecesse.) \n\n\n 4. Comportamento real \n (Descreva o que realmente aconteceu.) \n\n\n 5. Por favor, anexe capturas de tela que possam demonstrar o problema", + "request-access": "Solicitar acesso", + "request-feature": "Solicitar um recurso", + "request-membership": "Solicitar para entrar", + "request-pending": "Pedido pendente…", + "request-to-join": "Solicitar para participar", + "request-transaction": "Solicitar transação", + "required-field": "Campo obrigatório", + "resend-message": "Reenviar", + "reset-card": "Redefinir cartão", + "reset-card-description": "Esta operação redefinirá o cartão para o estado inicial. Ele apagará todos os dados do cartão, incluindo chaves privadas. A operação não é reversível.", + "reset-database": "Reiniciar base de dados", + "reset-database-warning": "Excluir conversas, contatos e configurações. Obrigatório quando você perder sua senha", + "reset-database-warning-keycard": "Excluir conversas, contatos e configurações.", + "reset-password": "Redefinir senha", + "restore-defaults": "Restaurar padrões", + "retry": "Tentar de novo", + "revoke-access": "Recuperar acesso", + "rpc-url": "RPC URL", + "rpc-usage-copy": "Copiar", + "rpc-usage-filter": "Métodos de filtro", + "rpc-usage-filter-methods": "Métodos de filtro", + "rpc-usage-filtered-total": "{{filtered-total}} de {{total}}", + "rpc-usage-get-stats": "Atualizar", + "rpc-usage-info": "Estatísticas de uso de RPC", + "rpc-usage-reset": "Redefinir", + "rpc-usage-total": "Total", + "safe-estimate": "Estimativa segura", + "save": "Salvar", + "save-image-library": "Salvar imagem na biblioteca", + "save-password": "Salvar senha", + "save-password-unavailable": "Defina o código de acesso do dispositivo para salvar a senha", + "save-password-unavailable-android": "Salvar senha não está disponível: seu dispositivo pode estar \"rooteado\" ou não possui os recursos de segurança necessários.", + "say-hi": "Diga oi", + "scan-qr": "Escanear código QR", + "scan-qr-code": "Escaneie um código QR com um endereço de carteira", + "scan-tokens": "Escanear tokens", + "search": "Procurar", + "search-contacts": "Buscar contatos", + "search-discover-communities": "Pesquisar comunidades ou categorias", + "search-no-chat-found": "Sem resultados. Você quer dizer", + "secret-keys-confirmation-text": "Você vai precisar deles para continuar a usar o seu Keycard no caso de perder o seu celular.", + "secret-keys-confirmation-title": "Já anotou os códigos?", + "security": "Segurança", + "see-details": "Ver detalhes", + "see-it-again": "VEJA NOVAMENTE", + "see-sticker-set": "Veja o conjunto completo de adesivos", + "see-suggestions": "Veja sugestões", + "seed-key-uid-mismatch": "Semente não corresponde", + "seed-key-uid-mismatch-desc-1": "A frase-semente que você inseriu não corresponde a {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Para gerenciar as chaves para esta conta, verifique sua frase-semente e tente novamente.", + "seed-phrase-content": "Um conjunto de palavras fáceis de ler, selecionadas aleatoriamente na lista padrão BIP39 e usadas para recuperar ou acessar sua conta Ethereum em outras carteiras e dispositivos. Também chamada de \"frase mnemônica\", \"frase de recuperação\" ou \"backup de carteira\" em todo o ecossistema de criptomoeda. A maioria dos aplicativos de criptomoeda usa esse mesmo padrão para gerar contas.", + "seed-phrase-placeholder": "Frase-semente…", + "seed-phrase-title": "Frase-semente", + "select": "Selecionar", + "select-account": "Selecionar conta", + "select-account-dapp": "Selecione a conta que você deseja usar com Dapps", + "select-account-first": "Selecione uma conta primeiro", + "select-chat": "Selecionar bate-papo para iniciar o envio de mensagens", + "select-new-location-for-keys": "Selecione um novo local para salvar sua(s) chave(s) privada(s)", + "select-token-to-receive": "Selecione o token para receber", + "select-token-to-swap": "Selecione o token para trocar", + "selected": "Selecionado", + "send": "Enviar", + "send-contact-request-message": "Para iniciar um chat, você precisa se tornar um contato", + "send-logs": "Relatar um bug", + "send-logs-to": "Relatar um bug para {{email}}", + "send-message": "Enviar mensagem", + "send-push-notifications": "Enviar Notificações Push", + "send-push-notifications-description": "Quando desativado, a pessoa que recebe suas mensagens não será notificada de suas novas mensagens", + "send-reply": "Enviar resposta", + "send-request": "Enviar pedido", + "send-request-amount": "Quantidade", + "send-request-amount-max-decimals": "O número máximo de decimais é {{asset-decimals}}", + "send-request-unknown-token": "Token desconhecido - {{asset}}", + "send-sending-to": "para {{recipient-name}}", + "send-transaction": "Enviar transação", + "sending": "Enviando", + "sent": "Enviado", + "sent-at": "Enviado em", + "server": "Servidor", + "set-a-topic": "Criar um tópico", + "set-currency": "Definir moeda", + "set-custom-fee": "Definir taxa personalizada", + "set-dapp-access-permissions": "Definir permissões de acesso do DApp", + "set-max": "Definir máximo", + "settings": "Configurações", + "share": "Compartilhar", + "share-address": "Compartilhar endereço", + "share-chat": "Compartilhar bate-papo", + "share-community": "Compartilhar comunidade", + "share-contact-code": "Compartilhar minha chave de bate-papo", + "share-dapp-text": "Confira este DApp que estou usando no Status: {{link}}", + "share-image": "Compartilhar imagem", + "share-invite-link": "Compartilhar um link de convite", + "share-link": "Compartilhar link", + "share-logs": "Compartilhar logs", + "share-my-profile": "Compartilhar meu perfil", + "share-profile": "Compartilhar perfil", + "share-profile-link": "Compartilhar link do perfil", + "share-public-chat-text": "Confira este bate-papo público no app Status: {{link}}", + "shared": "Compartilhado", + "sharing-copied-to-clipboard": "Copiado", + "sharing-copy-to-clipboard": "Copiar", + "sharing-data-desc-1": "Os dados são validados de acordo com as regras públicas para garantir que nenhum dado sensível seja enviado. Não confie, verifique.", + "sharing-data-desc-2": "Os dados de uso são enviados criptografados de ponta a ponta pela rede ponto a ponto (P2P) do Status", + "sharing-data-desc-3": "Em vez de sua chave de bate-papo normal, uma chave de uso único é utilizada", + "sharing-data-desc-4": "Os dados de uso não podem ser associados ao seu endereço IP", + "sharing-data-desc-5": "Os dados acumulados de todos os usuários estão disponíveis publicamente", + "sharing-data-desc-6": "Os dados são removidos do seu telefone após serem enviados", + "sharing-share": "Compartilhar", + "shell-placeholder-subtitle": "Abra abas de suas comunidades, mensagens,\n conta da carteira e janelas do navegador", + "shell-placeholder-title": "Seus aplicativos serão executados aqui", + "show-less": "Mostre menos", + "show-more": "Mostrar mais", + "show-notifications": "Mostrar notificações", + "show-profile-pictures": "Mostrar fotos de perfil de", + "show-profile-pictures-to": "Mostre sua foto de perfil para", + "show-qr": "Mostrar código QR", + "show-transaction-data": "Mostrar dados da transação", + "sign-and-send": "Assinar e enviar", + "sign-anyway": "Assinar mesmo assim", + "sign-in": "Entrar", + "sign-message": "Assinar Mensagem", + "sign-out": "Sair da sessão", + "sign-request-failed": "Não foi possível assinar a mensagem", + "sign-with": "Assinar com", + "sign-with-password": "Assinar com senha", + "sign-you-in": "Iniciando sessão...", + "signing": "Assinatura", + "signing-a-message": "Assinando uma mensagem", + "signing-phrase": "Frase de assinatura", + "skip": "Pular", + "slippage": "Derrapagem", + "slow": "Lento", + "something-went-wrong": "Algo deu errado", + "soon": "Em breve", + "sort-communities": "Classificar comunidades", + "specify-address": "Especificar endereço", + "specify-name": "Especifique um nome", + "specify-network-id": "Especifique o ID da rede", + "specify-rpc-url": "Especifique um URL RPC", + "specify-server-public-key": "Digite a chave pública do servidor", + "specify-symbol": "Especifique um símbolo", + "start-chat": "Iniciar bate-papo", + "start-conversation": "Iniciar conversa", + "start-group-chat": "Iniciar conversa em grupo", + "start-new-chat": "Iniciar nova conversa", + "starter-pack-coming": "Pacote de iniciante vindo até você", + "starter-pack-coming-description": "Pode levar de alguns minutos a horas", + "starter-pack-received": "Pacote inicial recebido", + "starter-pack-received-description": "Aqui estão algumas criptomoedas para você começar! Use-as para obter adesivos, um nome ENS e testar alguns dapps", + "status": "Status", + "status-always-online": "Sempre Online", + "status-automatic": "Automático", + "status-automatic-subtitle": "Definir status automaticamente", + "status-confirmed": "Confirmado", + "status-dnd": "Não perturbe", + "status-dnd-subtitle": "Silencia todas as notificações", + "status-hardwallet": "Hardwallet Status", + "status-inactive": "Inativo", + "status-inactive-subtitle": "Oculta seu status online", + "status-is-open-source": "O status é de código aberto", + "status-keycard": "Keycard Status", + "status-mobile-descr": "O Status usa muitos dados ao sincronizar conversas. Você pode optar por não sincronizar quando na rede móvel", + "status-not-sent-click": "Não confirmado. Clique para opções", + "status-not-sent-tap": "Não confirmado. Toque para opções", + "status-pending": "Pendente", + "status-sent": "Enviado", + "status-tx-not-found": "TX não encontrado", + "status-updates-descr": "As atualizações de status aparecerão aqui. Adicione este perfil como um contato para receber atualizações em sua linha do tempo.", + "statuses-descr": "Compartilhe o que você está pensando e fique atualizado com seus contatos", + "statuses-my-status-descr": "Compartilhe o que você está pensando. Qualquer pessoa que visitar seu perfil poderá ver seu status. As pessoas que adicionam você como contato receberão suas atualizações na linha do tempo delas", + "step-i-of-n": "Etapa {{step}} de {{number}}", + "sticker": "Sticker", + "sticker-market": "Mercado de adesivos", + "storage": "Armazenamento", + "strikethrough": "Riscado", + "submit": "Enviar", + "submit-bug": "Relatar um bug", + "success": "Sucesso", + "successful-connection": "Conexão Bem-sucedida", + "suggested-min-tip": "Gorjeta mínima sugerida", + "suggested-price-limit": "Limite de preço sugerido", + "swap": "Troca", + "swap-details": "Trocar detalhes", + "switch-to-simple-interface": "Mudar para interface simples", + "symbol": "Símbolo", + "sync-all-devices": "Sincronizar todos dispositivos", + "sync-in-progress": "Sincronizando...", + "sync-settings": "Configurações de sincronização", + "sync-synced": "Sincronizado", + "syncing-devices": "Sincronizando...", + "system": "Sistema", + "tabs": "Abas", + "tag-was-lost": "A tag foi perdida", + "tap-card-again": "Aproxime o cartão na parte de trás do seu telefone novamente", + "terms-of-service": "Termos de uso", + "test-networks": "Redes de teste", + "text-input-disabled": "Aguarde um momento…", + "thank-you": "Obrigado", + "this-device": "Este dispositivo", + "this-device-desc": "Suas chaves serão criptografadas e armazenadas com segurança no seu dispositivo", + "this-is-you-signing": "Esta é a sua frase de assinatura", + "this-will-take-few-seconds": "Isso levará alguns segundos", + "three-days": "Três dias", + "three-words-description": "Você deve ver essas três palavras antes de assinar cada transação", + "three-words-description-2": "Se você ver uma combinação diferente, cancele a transação e finalize a sessão", + "timeline": "Linha do tempo", + "tip-cap": "Limite de gorjeta", + "to": "Para", + "to-block": "Bloquear ", + "to-enable-biometric": "Para ativar o {{bio-type-label}}, você deve salvar sua senha na tela de desbloqueio", + "to-encrypt-enter-password": "Para criptografar a conta, digite sua senha", + "to-see-this-message": "Para ver esta mensagem,", + "token": "Token", + "token-auto-validate-decimals-error": "Decimais incorretos para o token {{symbol}} no endereço {{address}} - definido como {{expected}} mas detectado como {{actual}}", + "token-auto-validate-name-error": "Nome incorreto para o token {{symbol}} no endereço {{address}} - definido como {{expected}} mas detectado como {{actual}}", + "token-auto-validate-symbol-error": "Símbolo incorreto para o token {{symbol}} no endereço {{address}} - definido como {{expected}} mas detectado como {{actual}}", + "token-details": "Detalhes do Token", + "token-price": "Preço do Token", + "topic-name-error": "Use apenas letras minúsculas (a a z), números e traços (-). Não use teclas de bate-papo", + "total-gas": "Gas total", + "total-members": "Total de membros", + "transaction": "Transação", + "transaction-data": "Dados da transação", + "transaction-declined": "Transação recusada", + "transaction-description": "Considere-o completo após 12 confirmações na rede.", + "transaction-details": "Detalhes da transação", + "transaction-failed": "Falha na transação", + "transaction-fee": "Taxa de transação", + "transaction-history": "Histórico de transações", + "transaction-request": "Solicitação de transação", + "transaction-sent": "Transação enviada", + "transaction-signed": "A transação foi assinada com sucesso", + "transactions": "Transações", + "transactions-filter-select-all": "Selecionar tudo", + "transactions-filter-title": "Filtrar histórico", + "transactions-history": "Histórico de transações", + "transactions-history-empty": "Ainda não há transações no seu histórico", + "transactions-history-loading": "Carregando histórico de transações. Isso pode demorar um pouco.", + "transactions-load-more": "Carregar mais", + "transactions-management-enabled": "Gerenciamento de transações (alfa)", + "transactions-sign": "Assinar", + "transfer-ma-unknown-error-desc-1": "Parece que sua conta múltipla não foi excluída. O banco de dados pode ter sido redefinido", + "transfer-ma-unknown-error-desc-2": "Verifique sua lista de contas e tente novamente. Se a conta não estiver listada, vá para Acessar as chaves existentes para recuperar com a frase-semente", + "transfers-fetching-failure": "O histórico de transferências não pôde ser atualizado. Verifique sua conexão e puxe para baixo para tentar novamente", + "tribute-required-by-multiaccount": "{{multiaccount-name}} precisa de SNT para começar uma conversa.", + "tribute-state-paid": "Tributo pago", + "tribute-state-pending": "Tributo pendente", + "tribute-state-required": "Requer {{snt-amount}} tributos em SNT", + "tribute-to-talk": "Tribute to Talk", + "tribute-to-talk-add-friends": "Adicione amigos como um contato para permitir bate-papos sem pagamento de tributo.", + "tribute-to-talk-are-you-friends": "Vocês são amigos?", + "tribute-to-talk-ask-to-be-added": "Peça para ser adicionado como um contato", + "tribute-to-talk-contact-received-your-tribute": " recebeu seu tributo. Agora vocês podem conversar com segurança.", + "tribute-to-talk-desc": "Monetize sua atenção exigindo pagamento de SNT para novas pessoas iniciem um bate-papo", + "tribute-to-talk-disabled": "Tribute to Talk desativado", + "tribute-to-talk-disabled-note": "A partir de agora, novas pessoas podem iniciar um bate-papo com você sem enviar o SNT.", + "tribute-to-talk-enabled": "Você tem o Tribute to Talk ativado.", + "tribute-to-talk-finish-desc": "A partir de agora, você receberá apenas conversas de contatos e pessoas que pagaram ", + "tribute-to-talk-learn-more-1": "Seu tempo e atenção são seus ativos mais valiosos. O Tribute to Talk permite definir uma quantidade de SNT necessária para novas pessoas iniciarem um bate-papo com você.", + "tribute-to-talk-learn-more-2": "Qualquer pessoa que não esteja na sua lista de contatos será solicitada a pagar e você pode responder assim que ela efetuar o pagamento.", + "tribute-to-talk-learn-more-3": "Você sempre pode enviar o dinheiro de volta, mas para garantir que os amigos possam entrar em contato com você gratuitamente, adicione-os primeiro como contato.", + "tribute-to-talk-paywall-learn-more-1": "Nosso tempo e atenção são nossos ativos mais valiosos. O Tribute to Talk permite que você entre em contato com novas pessoas em troca de um pagamento SNT.", + "tribute-to-talk-paywall-learn-more-2": "Para iniciar um bate-papo com alguém que tenha um conjunto de tributos, basta pagar o SNT necessário e você será adicionado como um contato.", + "tribute-to-talk-paywall-learn-more-3": "Se você os conhece, pode compartilhar seu perfil fora do Status para ser adicionado gratuitamente.", + "tribute-to-talk-pending": "Tributo pendente de confirmação", + "tribute-to-talk-pending-note": "A transação de tributo está pendente de confirmação na rede. Você pode verificar seu status no histórico de transações", + "tribute-to-talk-removing-note": "A remoção do Tribute to Talk permitirá que novas pessoas iniciem um bate-papo sem enviar o SNT. Requer que uma transação seja feita.", + "tribute-to-talk-set-snt-amount": "Defina a quantidade de SNT necessária para novas pessoas iniciarem um bate-papo", + "tribute-to-talk-signing": "Aguardando para assinar a transação", + "tribute-to-talk-transaction-failed-note": "A transação falhou e as configurações do Tribute to Talk não foram alteradas", + "tribute-to-talk-tribute-received1": "Tributo recebido. Você e", + "tribute-to-talk-tribute-received2": "agora são contatos e podem conversar com segurança.", + "tribute-to-talk-you-require-snt": "Você requer SNT para novas pessoas iniciarem um bate-papo.", + "try-again": "Tente novamente", + "try-keeping-the-card-still": "Tente manter o cartão parado", + "turn-nfc-description": "NFC está desativado no seu dispositivo. Você pode habilitá-lo nas configurações", + "turn-nfc-on": "Ative a NFC para continuar", + "two-minutes": "dois minutos", + "tx-fail-description1": "É provável que esta transação falhe. Assine por sua conta e risco usando uma taxa de rede personalizada.", + "tx-fail-description2": "É provável que esta transação falhe. Defina uma taxa de rede personalizada para assinar por sua conta e risco.", + "type": "Tipo", + "type-a-message": "Mensagem", + "type-something": "Digite algo", + "ulc-enabled": "ULC ativado", + "unable-to-fetch": "Não foi possível obter o histórico de conversas", + "unable-to-read-this-code": "Não foi possível ler este código", + "unable-to-send-messages": "Incapaz de enviar e receber mensagens", + "unblock": "Desbloquear", + "unblock-contact": "Desbloquear este usuário", + "undo": "Desfazer", + "unknown-status-go-error": "Erro desconhecido de status-go", + "unlimited": "Ilimitado", + "unlock": "Desbloquear", + "unmute": "Ativar som", + "unmute-chat": "Ativar som do chat", + "unmute-community": "Ativar o som da comunidade", + "unmute-group": "Ativar som do grupo", + "unpair-card": "Desemparelhar cartão", + "unpair-card-confirmation": "Esta operação desemparelhará o cartão do dispositivo atual. Requer autorização de código de acesso de 6 dígitos. Deseja continuar?", + "unpair-keycard": "Desparear o Keycard deste telefone", + "unpair-keycard-warning": "Seu código de emparelhamento\/PUK & PIN permanecem inalterados", + "unpaired-keycard-text": "O Keycard que você utilizou não está associado a este telefone", + "unpaired-keycard-title": "Parece que seu cartão não foi pareado.", + "unpin": "Desafixar", + "unpin-from-channel": "Desafixar do canal", + "unpin-from-chat": "Desafixar da conversa", + "untrustworthy": "Não Confiável", + "update": "Atualizar", + "update-to-listen-audio": "Atualize para a versão mais recente para ouvir uma mensagem de áudio aqui!", + "update-to-see-image": "Atualize para a versão mais recente para ver uma bela imagem aqui!", + "update-to-see-sticker": "Atualize para a versão mais recente para ver um belo adesivo aqui!", + "updates-to-tos": "Atualizações nos Termos de Serviço", + "updates-to-tos-desc": "Antes de continuar, leia os Termos de Serviço e confirme se você assume total responsabilidade pelo modo como usa o aplicativo.", + "url": "URL", + "usd-currency": "USD", + "use-as-profile-picture": "Usar como foto de perfil", + "use-valid-contact-code": "Digite ou escaneie uma chave de bate-papo ou nome de usuário válidos", + "validation-amount-invalid-number": "A quantidade não é um número válido", + "validation-amount-is-too-precise": "O valor é muito preciso. O número máximo de casas decimais é {{decimals}} .", + "verified-community": "✓ Comunidade verificada", + "version": "Versão do aplicativo", + "view": "Visualizar", + "view-community-rules": "Ver Regras da Comunidade", + "view-cryptokitties": "Ver em CryptoKitties", + "view-cryptostrikers": "Ver no CryptoStrikers", + "view-data": "Ver dados", + "view-details": "Ver Detalhes", + "view-etheremon": "Ver no Etheremon", + "view-gitcoin": "Ver no Gitcoin", + "view-members": "Ver membros", + "view-on-opensea": "Ver no OpenSea", + "view-pinned-messages": "Ver mensagens fixadas", + "view-profile": "Ver perfil", + "view-public-dashboard": "Ver painel de controle público", + "view-rules": "Ver regras", + "view-signing": "Ver frase de assinatura", + "view-superrare": "Ver no SuperRare", + "vote-to-feature": "Votar para destacar esta comunidade", + "waiting-for-wifi": "Sem Wi-Fi, sincronização de mensagens desativada.", + "waiting-for-wifi-change": "Configurações", + "waiting-to-sign": "Aguardando para assinar a transação...", + "waiting-wi-fi": "Aguardando Wi-Fi...", + "waku-bloom-filter-mode": "Modo de filtro Waku bloom", + "wakuv2-change-nodes": "Tem certeza de que deseja modificar os nós do Wakuv2?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "Configurações Waku v2", + "wallet": "Carteira", + "wallet-address": "Endereço da carteira", + "wallet-asset": "Ativo", + "wallet-assets": "Ativos", + "wallet-backup-recovery-title": "Faça backup da sua frase-semente", + "wallet-choose-recipient": "Escolher destinatário", + "wallet-collectibles": "Colecionáveis", + "wallet-connect": "Wallet Connect", + "wallet-connect-2.0": "Wallet Connect 2.0", + "wallet-connect-app-connected": "está conectado", + "wallet-connect-go-back": "Volte para o seu navegador ou dapp", + "wallet-connect-proposal-description": "Ao conectar você permite que {{name}} recupere o endereço da sua conta e ative o Web3", + "wallet-connect-proposal-title": "Gostaria de se conectar com sua carteira", + "wallet-insufficient-funds": "Fundos insuficientes", + "wallet-insufficient-gas": "Não há ETH suficiente para o gás", + "wallet-invalid-address": "Endereço inválido: \n {{data}}", + "wallet-invalid-address-checksum": "Erro no endereço:{{data}}", + "wallet-invalid-chain-id": "A rede não corresponde: \n {{data}} mas a cadeia atual é {{chain}}", + "wallet-key-content": "Um endereço hexadecimal de 64 caracteres baseado no padrão Ethereum e começando com 0x. Para o público, o endereço da sua conta é compartilhado com outras pessoas quando você deseja receber fundos. Também chamado de \"endereço Ethereum\" ou \"endereço da carteira\".", + "wallet-key-title": "Endereço da conta", + "wallet-manage-accounts": "Gerenciar contas", + "wallet-manage-app-connections": "Gerenciar conexões de aplicativos", + "wallet-manage-assets": "Gerenciar ativos", + "wallet-request": "Solicitação", + "wallet-send": "Enviar", + "wallet-send-min-units": "Mínimo 21000 unidades", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Configurações da carteira", + "wallet-total-value": "Valor total", + "wallet-transaction-total-fee": "Taxa total", + "wants-to-access-profile": "quer acessar seu perfil", + "warning": "Atenção", + "warning-message": "Desculpe, limitamos o envio de várias mensagens em rápida sucessão para evitar spam. Por favor, tente novamente daqui a pouco", + "warning-sending-to-contract-descr": "O endereço que você inseriu é um contrato inteligente. O envio de fundos para este endereço pode resultar em perda de fundos. Para interagir com um DApp, abra o DApp no navegador da Status.", + "watch-only": "Somente exibição", + "wc-brand-guide": "Orientação sobre o uso de marcas, como marcas registradas e logotipos", + "wc-disclaimer": "Exonerações de responsabilidade (incluindo fornecedores terceirizados), garantias e liberações legais", + "wc-dispute": "Disposições de resolução de disputas", + "wc-how-to-use-status-app": "Como usar o aplicativo Status, incluindo privacidade e segurança", + "wc-new-tos-based-on-principles-prefix": "Novos Termos de Serviço desenvolvidos com base em nossos", + "web-view-error": "Não foi possível carregar a página", + "websites": "Sites", + "webview-camera-permission-requests": "Solicitações de permissão de câmera do Webview", + "webview-camera-permission-requests-subtitle": "Quando habilitado, sites e dapps podem pedir para usar sua câmera", + "welcome-blank-community-message": "Suas comunidades aparecerão aqui.", + "welcome-blank-message": "Suas conversas serão exibidas aqui. Para iniciar novos bate-papos, pressione o botão ⊕", + "welcome-community-blank-message": "Seus canais aparecerão aqui. Para criar um novo canal, clique no botão ⊕ e selecione \"Criar um canal\"", + "welcome-community-blank-message-edit-chats": "Seus canais aparecerão aqui. Para criar um novo canal, volte para a tela da comunidade, clique no botão ⊕ e selecione \"Criar um canal\"", + "welcome-screen-text": "Configure sua carteira, convide seus amigos para bater papo e navegue pelos dapps mais usados!", + "welcome-to-status": "Bem-vindo ao Status!", + "welcome-to-status-description": "Configure sua carteira criptográfica, convide amigos para conversar e navegue por aplicativos descentralizados", + "what-changed": "O que mudou", + "what-is-shared": "O que é compartilhado", + "whats-on-your-mind": "O que você está pensando...", + "whats-trending": "Veja o que está em alta", + "word-count": "Contagem de palavras", + "word-n": "Palavra #{{number}}", + "word-n-description": "Para verificar se você fez o backup da sua frase-semente corretamente, digite a palavra #{{number}} acima.", + "words-n": { + "one": "1 palavra", + "other": "{{count}} palavras" + }, + "write-down-and-store-securely": "Anote os códigos e\n guarde-os de forma segura", + "wrong-address": "Endereço errado", + "wrong-card": "Cartão errado", + "wrong-card-text": "O cartão utilizado não corresponde às chaves que você selecionou", + "wrong-contract": "Contrato errado", + "wrong-keycard-text": "O Keycard que você utilizou não está associado a este telefone", + "wrong-keycard-title": "Parece que você utilizou\n um Keycard errado.", + "wrong-password": "Senha incorreta", + "wrong-word": "Palavra errada", + "yes": "Sim", + "you": "você", + "you-already-have-an-asset": "Você já tem um ativo {{value}}", + "you-are-all-set": "Está tudo pronto!", + "you-are-all-set-description": "Se você perder seu telefone, agora você pode acessar seus fundos e a chave de bate-papo usando sua frase-semente", + "you-can-change-account": "Você pode mudar o nome da conta e a cor para o que desejar", + "you-can-choose-preview-websites": "Você pode escolher quais dos seguintes sites podem visualizar o link de descrições e imagens nas conversas", + "you-can-fetch": "Você pode obter o histórico de conversas", + "you-dont-have-contacts": "Você ainda não possui contatos.", + "you-dont-have-contacts-invite-friends": "Você ainda não tem contatos.\n Convide seus amigos para começar a conversar.", + "you-dont-have-stickers": "Você ainda não tem nenhum adesivo", + "you-must-always-hold": "Você deve sempre segurar:", + "you-must-hold": "Você deve segurar:", + "you-must-now-hold": "Agora você deve segurar:", + "you-will-need-this-code": "Você precisará desse código para abrir o Status e assinar transações", + "you-will-start-from-scratch": "Você começará do zero com um novo conjunto de chaves", + "your-answer": "Sua resposta", + "your-card-is-frozen": "Seu Keycard travou. Redefinir acesso ao cartão", + "your-contact-code": "A concessão de acesso autoriza este DApp a recuperar sua chave de bate-papo", + "your-data-belongs-to-you": "Se você perder a sua frase-semente, você perde seus dados e fundos", + "your-data-belongs-to-you-description": "Se perder o acesso, por exemplo, ao perder seu telefone, você só pode acessar suas chaves com sua frase-semente. Só você tem a sua frase-semente. Anote e mantenha sua frase-semente em segurança", + "your-keys": "Suas chaves", + "your-price-limit": "Seu limite de preço", + "your-recovery-phrase": "Sua frase-semente", + "your-recovery-phrase-description": "Esta é sua frase-semente. Você a usa para provar que esta é sua carteira. Você só pode vê-la uma vez! Escreva no papel e mantenha em um lugar seguro. Você vai precisar dela se perder ou reinstalar sua carteira.", + "your-tip-limit": "Seu limite de gorjeta", + "youre-on-mobile-network": "Você está em uma rede móvel" +} diff --git a/translations/pt_pt.json b/translations/pt_pt.json new file mode 100644 index 00000000000..448e5cef647 --- /dev/null +++ b/translations/pt_pt.json @@ -0,0 +1,127 @@ +{ + "confirm": "Confirmar", + "amount": "Montante", + "members-active": { + "one": "1 membro, 1 ativo", + "other": "{{count}} membros, {{count}} ativos", + "zero": "sem membros" + }, + "chat-name": "Nome no chat", + "phew-here-is-your-passphrase": "*Ufa* foi complicado, aqui está a sua frase-chave, *anote-a e mantenha-a em segurança!* Vai precisar dela para recuperar a sua conta.", + "public-group-topic": "Tema", + "chat-settings": "Definições de chat", + "offline": "Offline", + "invited": "convidado", + "address": "Endereço", + "new-public-group-chat": "Juntar-se ao chat público", + "datetime-hour": { + "one": "hora", + "other": "horas" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "Remover", + "add-members": "Adicionar Membros", + "done": "Concluído", + "close-chat": "Eliminar o chat", + "new-group-chat": "Novo chat em grupo", + "sign-in": "Entrar", + "datetime-yesterday": "ontem", + "create-new-account": "Criar nova conta", + "datetime-ago": "atrás", + "contacts": "Contactos", + "got-it": "Entendido", + "active-online": "Online", + "password": "Palavra-passe", + "browsing-title": "Procurar", + "discover": "Descoberta", + "browsing-cancel": "Cancelar", + "intro-status": "Fale comigo para configurar a sua conta e alterar as suas definições!", + "name": "Nome", + "show-qr": "Mostrar QR", + "connect": "Ligar", + "edit": "Editar", + "account-generation-message": "Dê-me um segundo, tenho de fazer alguns cálculos doidos para gerar a sua conta!", + "no-messages": "Sem mensagens", + "passphrase": "Frase-chave", + "recipient": "Destinatário", + "members-title": "Membros", + "new-group": "Novo grupo", + "phone-e164": "Internacional 1", + "settings": "Definições", + "chats": "Chats", + "transaction": "Transação", + "public-group-status": "Público", + "image-source-make-photo": "Capturar", + "start-conversation": "Iniciar a conversa", + "save": "Guardar", + "sharing-copy-to-clipboard": "Copiar para a área de transferência", + "sync-in-progress": "A sincronizar...", + "send-transaction": "Enviar a transação", + "incorrect-code": [ + "str", + "Desculpe, o código estava incorreto. Por favor, volte a digitar" + ], + "image-source-gallery": "Selecionar a partir da galeria", + "sync-synced": "Em sincronização", + "status-pending": "Pendente", + "datetime-day": { + "one": "dia", + "other": "dias" + }, + "scan-qr": "Digitalizar QR", + "contact-s": { + "one": "contacto", + "other": "Contactos" + }, + "next": "Próximo", + "recent": "Recente", + "status": "Estado", + "from": "De", + "wrong-password": "Palavra-passe errada", + "in-contacts": "Nos contactos", + "sharing-share": "Partilhar...", + "type-a-message": "Digitar uma mensagem...", + "clear-history": "Limpar o histórico", + "no-contacts": "Ainda sem contactos", + "datetime-today": "hoje", + "web-view-error": "ups, erro", + "error": "Erro", + "more": "mais", + "cancel": "Cancelar", + "can-not-add-yourself": "Não pode adicionar-se a si mesmo", + "add-to-contacts": "Adicionar aos contactos", + "available": "Disponível", + "You": "Você", + "main-wallet": "Carteira Principal", + "members": { + "one": "1 membro", + "other": "{{count}} membros", + "zero": "sem membros" + }, + "intro-message1": "Bem-vindo ao Status\nToque nesta mensagem para definir a sua palavra-passe e começar!", + "new-contact": "Novo Contacto", + "datetime-second": { + "one": "segundo", + "other": "segundos" + }, + "recover": "Recuperar", + "datetime-minute": { + "one": "minuto", + "other": "minutos" + }, + "browsing-open-in-android-web-browser": "Abrir no navegador Web", + "browsing-open-in-ios-web-browser": "Abrir no navegador Web", + "edit-profile": "Editar perfil", + "active-unknown": "Desconhecido", + "public-key": "Chave Pública", + "profile": "Perfil", + "none": "Nenhum", + "removed": "removido", + "message": "Mensagem", + "here-is-your-passphrase": "Aqui está a sua frase-chave, *anote-a e mantenha-a em segurança!* Vai precisar dela para recuperar a sua conta.", + "image-source-title": "Imagem de perfil", + "left": "saiu", + "to": "Para", + "status-sent": "Enviado", + "data": "Dados" +} diff --git a/translations/ro.json b/translations/ro.json new file mode 100644 index 00000000000..b222c063e7d --- /dev/null +++ b/translations/ro.json @@ -0,0 +1,127 @@ +{ + "confirm": "Confirmare", + "amount": "Sumă", + "members-active": { + "one": "1 membru, 1 activ", + "other": "{{count}} membri, {{count}} activi", + "zero": "nici un membru" + }, + "chat-name": "Nume chat", + "phew-here-is-your-passphrase": "*Pfui* a fost greu, iată fraza ta de acces, *noteaz-o și păstreaz-o în siguranță!* Vei avea nevoie de ea pentru a-ți redobândi accesul la cont.", + "public-group-topic": "Subiect", + "chat-settings": "Setări chat", + "offline": "Offline", + "invited": "invitat", + "address": "Adresă", + "new-public-group-chat": "Alăturați-vă chat-ului public", + "datetime-hour": { + "one": "oră", + "other": "ore" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "Elimină", + "add-members": "Adăugare membri", + "done": "Gata", + "close-chat": "Ștergere chat", + "new-group-chat": "Grup nou de chat", + "sign-in": "Conectare", + "datetime-yesterday": "ieri", + "create-new-account": "Creați un cont nou", + "datetime-ago": "în urmă", + "contacts": "Contacte", + "got-it": "Am înțeles", + "active-online": "Online", + "password": "Parolă", + "browsing-title": "Navigare", + "discover": "Descoperire", + "browsing-cancel": "Anulare", + "intro-status": "Hai să vorbim pe chat pentru a-ți seta contul și modifica setările!", + "name": "Nume", + "show-qr": "Afișează QR", + "connect": "Conectare", + "edit": "Editare", + "account-generation-message": "O secundă, trebuie să fac niște calcule complicate ca să-ți generez contul!", + "no-messages": "Nici un mesaj", + "passphrase": "Fraza de acces", + "recipient": "Beneficiar", + "members-title": "Membri", + "new-group": "Grup nou", + "phone-e164": "Internațional 1", + "settings": "Setări", + "chats": "Discuții pe chat", + "transaction": "Tranzacție", + "public-group-status": "Public", + "image-source-make-photo": "Captează", + "start-conversation": "Începeți o conversație", + "save": "Salvează", + "sharing-copy-to-clipboard": "Copiere în clipboard", + "sync-in-progress": "Se sincronizează…", + "send-transaction": "Trimitere tranzacție", + "incorrect-code": [ + "str", + "Ne pare rău, dar codul este greșit, te rugăm să-l introduci încă o dată" + ], + "image-source-gallery": "Selectează din galerie", + "sync-synced": "Sincronizat", + "status-pending": "În așteptare", + "datetime-day": { + "one": "zi", + "other": "zile" + }, + "scan-qr": "Scanează QR", + "contact-s": { + "one": "contact", + "other": "contacte" + }, + "next": "Mai departe", + "recent": "Recente", + "status": "Status", + "from": "De la", + "wrong-password": "Parola greșită", + "in-contacts": "În contacte", + "sharing-share": "Partajare...", + "type-a-message": "Tastați un mesaj...", + "clear-history": "Șterge istoricul", + "no-contacts": "Nici un contact deocamdată", + "datetime-today": "azi", + "web-view-error": "ups, eroare", + "error": "Eroare", + "more": "mai mult", + "cancel": "Anulare", + "can-not-add-yourself": "Nu te poți adăuga pe tine", + "add-to-contacts": "Adaugă la contacte", + "available": "Disponibil", + "You": "Tu", + "main-wallet": "Portofelul principal", + "members": { + "one": "1 membru", + "other": "{{count}} membri", + "zero": "nici un membru" + }, + "intro-message1": "Bine ai vent în Status\nApasă pe acest mesaj pentru a-ți seta parola și a începe!", + "new-contact": "Contact nou", + "datetime-second": { + "one": "secundă", + "other": "secunde" + }, + "recover": "Recuperează", + "datetime-minute": { + "one": "minut", + "other": "minute" + }, + "browsing-open-in-android-web-browser": "Deschide în browser web", + "browsing-open-in-ios-web-browser": "Deschide în browser web", + "edit-profile": "Editare profil", + "active-unknown": "Necunoscut", + "public-key": "Cheie publică", + "profile": "Profil", + "none": "Niciuna", + "removed": "eliminat", + "message": "Mesaj", + "here-is-your-passphrase": "Iată fraza ta de acces, *noteaz-o și păstreaz-o în siguranță!* Vei avea nevoie de ea pentru a-ți redobândi accesul la cont.", + "image-source-title": "Imagine de profil", + "left": "a plecat", + "to": "Către", + "status-sent": "Trimis", + "data": "Date" +} diff --git a/translations/ru.json b/translations/ru.json new file mode 100644 index 00000000000..165ba5946a6 --- /dev/null +++ b/translations/ru.json @@ -0,0 +1,1820 @@ +{ + "K": "К", + "M": "М", + "NFT": "НФТ", + "You": "Вы", + "about-app": "О приложении", + "about-key-storage-content": "У Status никогда не будет доступа к вашему приватному ключу. Обязательно сделайте резервную копию своей сид-фразы. Если вы потеряете телефон, сид-фраза — единственный способ получить доступ к своим ключам.", + "about-key-storage-title": "О хранилище ключей", + "about-names-content": "Никто не сможет выдать себя за вас! По умолчанию вы анонимны, и вам никогда не нужно раскрывать свое настоящее имя. За небольшую плату вы можете зарегистрировать пользовательское имя.", + "about-names-title": "Имена нельзя изменить", + "about-sharing-data": "Об обмене данными", + "accept": "Принять", + "accept-and-add": "Принять и добавить", + "accept-and-continue": "Принять и продолжить", + "accept-and-share-address": "Принять и поделиться адресом", + "accept-new-chats-from": "Принимать новые чаты от", + "accept-status-tos-prefix": "Я принимаю Status", + "accepted": "Принято", + "access-existing-keys": "Доступ к существующим ключам", + "access-key": "Ключ доступа", + "account-added": "Аккаунт добавлен", + "account-color": "Цвет аккаунта", + "account-content": "Аккаунт Status подобен банковскому счету — у аккаунта есть адрес и баланс, аккаунт можно использовать для совершения транзакций в блокчейне Ethereum. В рамках одного кошелька вы можете использовать несколько аккаунтов. Эти функции доступны при разблокировании приложения.", + "account-exists-title": "Аккаунт уже существует", + "account-is-used": "Учетная запись используется в Dapps в браузере.", + "account-name": "Название аккаунта", + "account-settings": "Настройки аккаунта", + "account-title": "Аккаунт", + "accounts": "Аккаунты", + "actions": "Действия", + "active-members": "Активные участники", + "active-online": "В сети", + "active-unknown": "Неизвестно", + "activity": "Активность", + "add": "Добавить", + "add-a-watch-account": "Добавить адрес только для просмотра", + "add-account": "Добавить аккаунт", + "add-account-description": "Вы можете импортировать любой тип аккаунта сети Ethereum, чтобы добавить его в свой кошелек Status.", + "add-account-incorrect-password": "Неверный пароль. Введите пароль, который вы используете для разблокировки приложения.", + "add-an-account": "Добавить аккаунт", + "add-bootnode": "Добавить bootnode", + "add-contact": "Добавить контакт", + "add-custom-token": "Добавить пользовательский токен", + "add-favourite": "Добавить в избранное", + "add-mailserver": "Добавить ноду Status", + "add-members": "Добавить участников", + "add-network": "Добавить сеть", + "add-new-contact": "Добавить новый контакт", + "add-nickname": "Добавить псевдоним (необязательно)", + "add-node": "Добавить ноду", + "add-private-key-account": "Добавить аккаунт из приватного ключа", + "add-seed-account": "Добавить аккаунт с сид-фразой", + "add-to-contacts": "Добавить в контакты", + "add-to-contacts-text": "Добавляя пользователя в список контактов, вы делитесь адресом своего кошелька", + "add-to-favourites": "Добавить в избранное", + "add-watch-account": "Добавить аккаунт только для просмотра", + "address": "Адрес", + "address-or-ens-name": "Адрес или ENS-имя", + "address-received": "Адрес получен", + "address-request-accepted": "Запрос адреса принят", + "address-requested": "Адрес запрошен", + "advanced": "Расширенные настройки", + "advanced-settings": "Расширенные настройки", + "advertiser-description": "Вы узнали о Status благодаря другу. Вы не возражаете, если Status проверит ваш IP-адрес один раз, чтобы друг получил вознаграждение? Эта информация не будет использоваться ни для чего другого и будет полностью удалена через 7 дней.", + "advertiser-starter-pack-accept": "Принять", + "advertiser-starter-pack-decline": "Отклонить", + "advertiser-starter-pack-description": "Вот немного криптовалюты для старта! Используйте её, чтобы получить ENS-имя, стикеры и доступ к децентрализованным приложениям", + "advertiser-starter-pack-title": "Стартовый набор", + "advertiser-title": "Конфиденциальность по умолчанию", + "agree-by-continuing": "Продолжая, вы соглашаетесь \nс нашими ", + "all": "Все", + "all-connections": "Все подключения", + "allow": "Разрешить", + "allow-and-send": "Разрешить и отправить", + "allow-mention-notifications": "Отображать упоминания с @", + "allowing-authorizes-this-dapp": "Соглашаясь, вы разрешаете DApp получить адрес вашего кошелька и включить Web3", + "alphabetically": "В алфавитном порядке", + "already-have-asset": "У вас есть актив с таким адресом", + "amount": "Сумма", + "anyone": "Любой", + "app-commit": "Фиксация приложения", + "app-connections": "Подключения приложений", + "appearance": "Внешний вид", + "apply": "Применить", + "approve": "Одобрить", + "approve-limit": "Одобрить лимит", + "approve-token": "Одобрить токен", + "approve-token-contract-desc": "Утверждение токена с контрактом позволяет ему потратить ваш баланс токена. Если вы чувствуете, что проект не заслуживает доверия, не одобряйте токен с ними или одобряйте только ту сумму, которую вы будете использовать с ними.", + "are-you-sure": "Вы уверены?", + "are-you-sure-description": "Вы не сможете увидеть всю сид-фразу снова", + "are-you-sure-to-cancel": "Вы точно хотите отменить?", + "are-you-sure?": "Вы уверены?", + "ask-in-status": "Задайте вопрос или сообщите об ошибке", + "at": "в", + "attribution-received": "Получено бонусов: {{attrib}} из {{max}}", + "audio": "Аудио", + "audio-recorder": "Запись", + "audio-recorder-error": "Ошибка записи", + "audio-recorder-max-ms-reached": "Достигнуто максимальное время записи", + "audio-recorder-permissions-error": "Вы должны дать разрешение на отправку голосовых сообщений", + "authorize": "Разрешить", + "available": "Доступно", + "available-participants": { + "zero": "Вы больше не можете выбирать участников", + "one": "Вы можете выбрать еще одного участника", + "few": "Вы можете выбрать еще несколько участников", + "many": "Вы можете выбрать еще много участников", + "other": "Доступное для выбора количество участников: {{count}}" + }, + "back": "Назад", + "back-up": "Резервная копия", + "back-up-seed-phrase": "Резервное копирование сид-фразы", + "back-up-your-seed-phrase": "Резервное копирование сид-фразы", + "backing-up": "Резервное копирование...", + "backup-disabled": "Отключен", + "backup-enabled": "Включено", + "backup-recovery-phrase": "Резервное копирование сид-фразы", + "backup-settings": "Настройки резервного копирования", + "backup-through-waku": "Резервное копирование через waku", + "bad-fees-description": "Ваш приоритет по комиссии ниже рекомендуемых нами параметров.", + "balance": "Баланс", + "begin-set-up": "Начать настройку", + "below-base-fee": "максимальная комиссия ниже базовой ставки", + "biometric-auth-android-sensor-desc": "Коснитесь сканера отпечатков пальцев", + "biometric-auth-android-sensor-error-desc": "Ошибка", + "biometric-auth-android-title": "Требуется аутентификация", + "biometric-auth-confirm-logout": "Повторный вход", + "biometric-auth-confirm-message": "Для продолжения необходима биометрическая аутентификация, если это невозможно, разблокируйте ваши ключи с помощью пароля или кода доступа", + "biometric-auth-confirm-title": "Вы должны пройти аутентификацию!", + "biometric-auth-confirm-try-again": "Попробуйте снова", + "biometric-auth-error": "Невозможно выполнить биометрическую аутентификацию ({{code}})", + "biometric-auth-login-error-title": "Ошибка биометрической аутентификации", + "biometric-auth-login-ios-fallback-label": "Введите пароль", + "biometric-auth-reason-login": "Войти в Status", + "biometric-auth-reason-verify": "Проверка аутентификации", + "biometric-disable-bioauth": "отключить {{bio-type-label}}", + "biometric-disable-password-description": "Если вы отключите это, вы также ", + "biometric-disable-password-title": "Отключить сохранение пароля", + "biometric-enable": "Если вы не хотите каждый раз вводить пароль для доступа к приложению, включите функцию «{{bio-type-label}}»", + "biometric-enable-button": "Включить функцию «{{bio-type-label}}»", + "biometric-enable-keycard": "Если вы не хотите каждый раз для доступа к приложению использовать свой Keycard, включите функцию «{{bio-type-label}}» для каждого входа", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Отпечаток пальца", + "biometric-secure-with": "Повысить безопасность с помощью функции «{{bio-type-label}}»", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "Пароль BIP39", + "blank-keycard-text": "Вы можете приступить к работе с Keycard после того, как будут сгенерированы ключи и имя", + "blank-keycard-title": "Похоже, вы используете \nпустой Keycard", + "block": "Блокировать", + "block-contact": "Заблокировать этого пользователя", + "block-contact-details": "Блокировка удалит предыдущие сообщения этого пользователя и заблокирует новые сообщения", + "blocked-users": "Заблокированные пользователи", + "bootnode-address": "Адрес bootnode", + "bootnode-details": "Детали Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes включены", + "bootnodes-settings": "Настройки bootnode", + "browsed-websites": "Просмотренные сайты появятся здесь", + "browser": "Браузер", + "browser-not-secure": "Соединение не защищено! Не подписывайте транзакции на этом сайте и не отправляйте с него личные данные.", + "browser-secure": "Соединение безопасно. Убедитесь в том, что вы доверяете этому сайту перед подписанием транзакций или вводом личных данных.", + "browsers": "Браузеры", + "browsing-cancel": "Отменить", + "browsing-open-in-android-web-browser": "Открыть в Android", + "browsing-open-in-ios-web-browser": "Открыть в iOS", + "browsing-open-in-status": "Открыть в Status", + "browsing-site-blocked-description1": "Мы обнаружили потенциальную вредоносную активность по этому адресу. Чтобы защитить вас и ваш кошелек, мы предотвращаем дальнейшую навигацию.\n\nЕсли вы считаете, что это ошибка, сообщите нам об этом в ", + "browsing-site-blocked-description2": " публичном чате.", + "browsing-site-blocked-go-back": "Назад", + "browsing-site-blocked-title": "Этот сайт заблокирован", + "browsing-title": "Просмотреть", + "bug-report": "Сообщить об ошибке", + "bug-report-description": "* Описание", + "bug-report-description-placeholder": "Требуется, не может быть пустым", + "bug-report-steps": "Действия по воспроизведению", + "bug-report-steps-placeholder": "- открыть приложение\n- сделать что-нибудь\n- а потом еще что-то...", + "bug-report-submit-email": "Отправить по электронной почте с архивом логов", + "bug-report-submit-gh-issue": "Отправить сообщение о проблеме на GitHub без логов", + "bug-report-too-short-description": "Описание слишком короткое", + "build-yourself": "Чтобы использовать приложение без этих Условий использования, вы можете создать свою собственную версию", + "buy-crypto": "Купить криптовалюту", + "buy-crypto-choose-a-service": "Выберите сервис, который вы хотели бы использовать для покупки криптовалюты", + "buy-crypto-description": "Найти децентрализованное приложение для покупки криптовалюты", + "buy-crypto-leaving": "Вы покидаете Status и перенаправляетесь на сторонний сайт для завершения покупки", + "buy-crypto-title": "Похоже, ваш кошелек пуст", + "camera-access-error": "Чтобы предоставить необходимое разрешение для камеры, перейдите в настройки системы и убедитесь, что выбрана опция Status > Камера.", + "can-not-add-yourself": "Это вы, чтобы начать чат, выберите собеседника", + "can-send-messages": "Вы можете отправлять и получать новые сообщения", + "cancel": "Отменить", + "cancel-keycard-setup": "Отменить настройку Keycard", + "cancelling": "Отмена", + "cannot-read-card": "Невозможно прочитать карту. \nПрислоните карту к задней панели телефона", + "cannot-use-default-pin": "Код доступа 000000 запрещен. \nИспользуйте другой набор символов", + "cant-open-public-chat": "Не удается открыть публичный чат", + "cant-report-bug": "Не удается сообщить об ошибке", + "card-is-blank": "Эта карта пуста", + "card-reseted": "Настройки карты были сброшены", + "card-unpaired": "Сопряжение карты с текущим устройством отменено", + "category": "Категория", + "category-title": "Название категории", + "change-fleet": "Изменение fleet на {{fleet}}", + "change-logging-enabled": "В точно хотите {{enable}} ведение журнала?", + "change-pairing": "Изменение кода сопряжения", + "change-pairing-description": "Изменение кода сопряжения не влияет на текущие сопряжения. Однако для любого нового сопряжения потребуется новый код.", + "change-pairing-title": "Создайте новый код сопряжения", + "change-passcode": "Изменить код доступа", + "change-password": "Изменить пароль", + "change-pin": "Изменить 6-значный код доступа", + "change-puk": "Изменить 12-значный PUK", + "change-tip": "Изменить чаевые", + "changed-amount-warning": "Сумма была изменена с {{old}} на {{new}}", + "changed-asset-warning": "Актив был изменен с {{old}} на {{new}}", + "chaos-mode": "Режим хаоса", + "chaos-unicorn-day": "День единорога хаоса", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Чат", + "chat-and-transact": "Полная приватность переписки и осуществляемых транзакций", + "chat-is-a-contact": "Есть в контактах", + "chat-is-not-a-contact": "Отсутствует в контактах", + "chat-key": "Код контакта", + "chat-key-content": "Протокол работы чатов приложения Status формирует прием и отправку сообщений с использованием ключей шифрования. Публичный код контакта – это строка символов, которой вы делитесь с другими пользователями, чтобы они имели возможность отправить вам сообщение в приложении.", + "chat-key-title": "Ключ чата", + "chat-link-previews": "Предварительный просмотр ссылок в чате", + "chat-name": "Имя чата", + "chat-name-content": "Набор из трех случайных слов, генерируется алгоритмическим путем на основе вашего кода контакта. Полученное имя используется по умолчанию в качестве псевдонима. Имена полностью уникальны и не могут состоять из трех одинаковых слов.", + "chat-name-title": "Имя в чате", + "chat-notification-preferences": "Настройки уведомлений", + "chat-settings": "Настройки чата", + "chats": "Чаты", + "check-on-block-explorer": "Проверьте в обозревателе блоков", + "check-on-opensea": "Проверить на OpenSea", + "check-your-recovery-phrase": "Проверьте свою сид-фразу", + "choose-actions": "Выбор действия", + "choose-authentication-method": "Выберите метод аутентификации", + "choose-new-location-for-keystore": "Выберите новое место для размещения файла хранилища ключей", + "choose-storage": "Выбор хранилища", + "clear": "Очистить", + "clear-all": "Очистить все", + "clear-history": "Очистить историю", + "clear-history-action": "Очистить", + "clear-history-confirmation": "Очистить историю?", + "clear-history-confirmation-content": "Вы действительно хотите удалить историю этого чата?", + "clear-history-title": "Очистить историю?", + "cleared-chat-description-public": "Здесь было тихо. Начать разговор или", + "close": "Закрыть", + "close-all": "Закрыть все", + "close-app-button": "Подтвердить", + "close-app-content": "Приложение остановится и закроется. При повторном открытии будет использоваться выбранная сеть", + "close-app-title": "Предупреждение!", + "collectibles-leak-metadata": "Здесь вы можете показать свои НФТ. Если вы это сделаете, то покажете также свой кошелек и IP адрес", + "command-button-send": "Отправить", + "communities": "Сообщества", + "communities-alpha": "Сообщества (альфа-версия)", + "communities-enabled": "Сообщества включены", + "communities-verified": "✓ Верифицированное сообщество", + "community": "Сообщество", + "community-color": "Цвет сообщества", + "community-color-placeholder": "Выберите цвет", + "community-edit-title": "Изменение сообщества", + "community-emoji-thumbnail-title": "Черновик", + "community-image-delete": "Пустой", + "community-image-pick": "Выберите изображение", + "community-image-remove": "Удалить", + "community-image-take": "Сделать фото", + "community-info": "Информация о сообществе", + "community-info-not-found": "Информация о сообществе не найдена", + "community-invite-title": "Пригласить", + "community-key": "Приватный ключ сообщества", + "community-key-placeholder": "Введите приватный ключ вашего сообщества", + "community-link": "Ссылка на сообщество", + "community-members": { + "zero": "{{count}} участников", + "one": "{{count}} участник", + "few": "{{count}} участника", + "many": "{{count}} участников", + "other": "Участников: {{count}}" + }, + "community-members-title": "Участники", + "community-message-preview": "Приглашение присоединиться к сообществу {{community-name}}", + "community-private-key": "Приватный ключ сообщества", + "community-requests-to-join-title": "Запросы на участие", + "community-roles": "Роли", + "community-share-title": "Поделиться", + "community-thumbnail-image": "Уменьшенное изображение", + "community-thumbnail-upload": "Загрузить", + "complete-hardwallet-setup": "Теперь эта карта привязана. Она нужна для подписания транзакций и разблокировки ключей.", + "completed": "Завершено", + "confirm": "Подтвердить", + "confirm-new-password": "Подтвердите новый пароль", + "confirm-pairing-code-placeholder": "Подтвердите код сопряжения...", + "confirm-password-placeholder": "Подтвердите пароль...", + "confirmation-request": "Запрос подтверждения", + "confirmations": "Подтверждения", + "confirmations-helper-text": "После получения 12 подтверждений транзакция считается завершенной.", + "connect": "Подключиться", + "connect-mailserver-content": "Подключиться к {{name}}?", + "connect-wallet": "Подключить кошелек", + "connected": "Подключен", + "connected-to": "Установлено подключение к", + "connecting": "Подключение...", + "connecting-requires-login": "Подключение к другой сети требует входа в систему", + "connection-request": "Запрос на подключение", + "connection-status": "Состояние подключения", + "connection-with-the-card-lost": "Соединение с картой потеряно", + "connection-with-the-card-lost-setup-text": "Для возобновления настройки приложите \nкарту к задней панели телефона и удерживайте \nее в таком состоянии", + "connection-with-the-card-lost-text": "Чтобы продолжить, приложите карту к задней панели телефона.", + "contact-code": "Код контакта", + "contact-request": "Запрос контакта", + "contact-request-accepted": "Принято", + "contact-request-declined": "Отклонено ⓧ", + "contact-request-header": "👋 Запросы на контакты", + "contact-request-pending": "В ожидании", + "contact-s": { + "zero": "контактов", + "one": "контакт", + "few": "контакта", + "many": "контактов", + "other": "контактов" + }, + "contacts": "Контакты", + "contacts-descr": "Ваши контакты появятся здесь. Вы будете получать обновления статуса от всех, кого добавите в контакты.", + "contacts-empty": "Контакты с ENS-именами появятся здесь", + "continue": "Продолжить", + "continue-anyway": "Продолжить в любом случае", + "contract-address": "Адрес контракта", + "contract-interaction": "Взаимодействие с контрактом", + "contract-isnt-supported": "Контракт не поддерживается", + "copy-info": "Скопировать информацию", + "copy-qr": "Копировать код", + "copy-to-clipboard": "Копировать", + "copy-transaction-hash": "Копировать ID транзакции", + "cost-fee": "Стоимость\/Комиссия", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Создать", + "create-a-pin": "Создать 6-значный код доступа", + "create-a-puk": "Создать 12-значный PUK", + "create-category": "Создать категорию", + "create-channel": "Создать канал", + "create-channel-title": "Новый канал", + "create-community": "Создать сообщество", + "create-group-chat": "Создать групповой чат", + "create-multiaccount": "Сгенерировать ключи", + "create-new-key": "Получить новые ключи", + "create-pin": "Создать 6-значный код доступа", + "create-pin-description": "Для разблокировки Status и подтверждения транзакций вам понадобится ваша карта + этот 6-значный код доступа", + "created-group-chat-description": "Вы создали группу {{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Валюта", + "currency-display-name-aed": "Дирхам ОАЭ", + "currency-display-name-afn": "Афганистанский афгани", + "currency-display-name-ars": "Аргентинское песо", + "currency-display-name-aud": "Австралийский доллар", + "currency-display-name-bbd": "Барбадосский доллар", + "currency-display-name-bdt": "Бангладешская така", + "currency-display-name-bgn": "Болгарский лев", + "currency-display-name-bhd": "Бахрейнский динар", + "currency-display-name-bnd": "Брунейский доллар", + "currency-display-name-bob": "Боливийский боливиано", + "currency-display-name-brl": "Бразильский реал", + "currency-display-name-btn": "Бутанский нгултрум", + "currency-display-name-cad": "Канадский доллар", + "currency-display-name-chf": "Швейцарский франк", + "currency-display-name-clp": "Чилийское песо", + "currency-display-name-cny": "Китайский юань рэнминби", + "currency-display-name-cop": "Колумбийское песо", + "currency-display-name-crc": "Коста-риканский колон", + "currency-display-name-czk": "Чешская крона", + "currency-display-name-dkk": "Датская крона", + "currency-display-name-dop": "Доминиканское песо", + "currency-display-name-egp": "Египетский фунт", + "currency-display-name-etb": "Эфиопский быр", + "currency-display-name-eur": "Евро", + "currency-display-name-gbp": "Британский фунт", + "currency-display-name-gel": "Грузинский лари", + "currency-display-name-ghs": "Ганский седи", + "currency-display-name-hkd": "Гонконгский доллар", + "currency-display-name-hrk": "Хорватская куна", + "currency-display-name-huf": "Венгерский форинт", + "currency-display-name-idr": "Индонезийская рупия", + "currency-display-name-ils": "Израильский шекель", + "currency-display-name-inr": "Индийская рупия", + "currency-display-name-isk": "Исландская крона", + "currency-display-name-jmd": "Ямайский доллар", + "currency-display-name-jpy": "Японская иена", + "currency-display-name-kes": "Кенийский шиллинг", + "currency-display-name-krw": "Южнокорейская вона", + "currency-display-name-kwd": "Кувейтский динар", + "currency-display-name-kzt": "Казахстанский тенге", + "currency-display-name-lkr": "Шри–ланкийская рупия", + "currency-display-name-mad": "Марокканский дирхам", + "currency-display-name-mdl": "Молдавский лей", + "currency-display-name-mur": "Маврикийская рупия", + "currency-display-name-mwk": "Малавийская квача", + "currency-display-name-mxn": "Мексиканское песо", + "currency-display-name-myr": "Малайзийский ринггит", + "currency-display-name-mzn": "Мозамбикский метикал", + "currency-display-name-nad": "Намибийский доллар", + "currency-display-name-ngn": "Нигерийская найра", + "currency-display-name-nok": "Норвежская крона", + "currency-display-name-npr": "Непальская рупия", + "currency-display-name-nzd": "Новозеландский доллар", + "currency-display-name-omr": "Оманский риал", + "currency-display-name-pen": "Перуанский соль", + "currency-display-name-pgk": "Кина Папуа – Новой Гвинеи", + "currency-display-name-php": "Филиппинское песо", + "currency-display-name-pkr": "Пакистанская рупия", + "currency-display-name-pln": "Польский злотый", + "currency-display-name-pyg": "Парагвайский гуарани", + "currency-display-name-qar": "Катарский риал", + "currency-display-name-ron": "Румынский лей", + "currency-display-name-rsd": "Сербский динар", + "currency-display-name-rub": "Российский рубль", + "currency-display-name-sar": "Саудовский риял", + "currency-display-name-sek": "Шведская крона", + "currency-display-name-sgd": "Сингапурский доллар", + "currency-display-name-thb": "Тайский бат", + "currency-display-name-try": "Турецкая лира", + "currency-display-name-ttd": "Доллар Тринидада и Тобаго", + "currency-display-name-twd": "Новый Тайваньский доллар", + "currency-display-name-tzs": "Танзанийский шиллинг", + "currency-display-name-uah": "Украинская гривна", + "currency-display-name-ugx": "Угандийский шиллинг", + "currency-display-name-usd": "Доллар США", + "currency-display-name-uyu": "Уругвайское песо", + "currency-display-name-vef": "Венесуэльский боливар", + "currency-display-name-vnd": "Вьетнамский донг", + "currency-display-name-zar": "Южноафриканский рэнд", + "current": "Текущее", + "current-average": "Текущее среднее", + "current-average-tip": "Текущие средние чаевые", + "current-base": "Текущая база", + "current-base-fee": "Текущая базовая комиссия", + "current-minimum-tip": "Текущие минимальные чаевые", + "current-network": "Текущая сеть", + "current-password": "Текущий пароль", + "current-pin": "Введите 6-значный код доступа", + "current-pin-description": "Для продолжения введите 6-значный код доступа", + "custom": "Пользовательские", + "custom-networks": "Пользовательские сети", + "custom-node": "Вы используете пользовательскую конечную точку RPC. История ваших локальных переводов может быть неполной.", + "custom-seed-phrase": "Неверная сид-фраза", + "custom-seed-phrase-text-1": "Данная сид-фраза не соответствует поддерживаемому словарю. Проверьте введенные вами слова.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Принять и открыть", + "dapp-starter-pack-description": "Вот немного криптовалюты для старта! Используйте её, чтобы получить ENS-имя, стикеры и доступ к децентрализованным приложениям", + "dapp-starter-pack-title": "Стартовый набор", + "dapp-would-like-to-connect-wallet": "хочет подключиться к", + "dapps": "ÐApps", + "dapps-permissions": "Разрешения DApp", + "dark": "Темная", + "data": "Данные", + "data-collected": "Собранные данные", + "data-collected-subtitle": "В приведенной ниже таблице указаны точные данные, которые хранятся и будут отправлены. Данные проверяются на соответствие общедоступным правилам, чтобы гарантировать, что конфиденциальные данные не будут отправлены. Не доверяйте, проверяйте.", + "data-syncing": "Синхронизация данных", + "database-reset-content": "Чаты, контакты и настройки были удалены. Вы можете использовать аккаунт со своим Keycard.", + "database-reset-title": "Очистить базы данных", + "database-reset-warning": "База данных будет очищена. Чаты, контакты и настройки будут удалены.", + "datetime-ago": "назад", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "zero": "дней", + "one": "день", + "few": "дня", + "many": "дней", + "other": "дн." + }, + "datetime-day-short": { + "zero": "дн.", + "one": "дн.", + "few": "дн.", + "many": "дн.", + "other": "дн." + }, + "datetime-hour": { + "zero": "часов", + "one": "час", + "few": "часа", + "many": "часов", + "other": "ч." + }, + "datetime-hour-short": { + "zero": "ч.", + "one": "ч.", + "few": "ч.", + "many": "ч.", + "other": "ч." + }, + "datetime-minute": { + "zero": "минут", + "one": "минута", + "few": "минуты", + "many": "минут", + "other": "мин." + }, + "datetime-minute-short": { + "zero": "мин.", + "one": "мин.", + "few": "мин.", + "many": "мин.", + "other": "мин." + }, + "datetime-second": { + "zero": "секунд", + "one": "секунда", + "few": "секунды", + "many": "секунд", + "other": "сек." + }, + "datetime-second-short": { + "zero": "сек.", + "one": "сек.", + "few": "сек.", + "many": "сек.", + "other": "сек." + }, + "datetime-today": "сегодня", + "datetime-yesterday": "вчера", + "decimals": "Количество знаков после запятой", + "decline": "Отклонить", + "declined": "Отклонено", + "decryption-failed-content": "При расшифровке ваших данных произошла ошибка. Возможно, вам потребуется стереть старые данные и создать новый аккаунт. Нажмите «Применить» для удаления или «Отмена» для повторной попытки.", + "default": "По умолчанию", + "default-assets": "Стандарты токенов ERC20 и ERC721", + "default-sync-period": "Синхронизировать историю за", + "delete": "Удалить", + "delete-account": "Удалить аккаунт", + "delete-and-leave-group": "Удалить и выйти из группы", + "delete-bootnode": "Удалить bootnode", + "delete-bootnode-are-you-sure": "Вы действительно хотите удалить этот bootnode?", + "delete-bootnode-title": "Удалить bootnode", + "delete-category-confirmation": "Вы уверены, что хотите удалить эту категорию?", + "close-chat": "Удалить чат", + "close-chat-confirmation": "Вы точно хотите удалить этот чат?", + "delete-confirmation": "Удалить?", + "delete-keys-keycard": "Удалить ключи с Keycard", + "delete-mailserver": "Удалить ноду Status", + "delete-mailserver-are-you-sure": "Вы действительно хотите удалить эту ноду Status?", + "delete-mailserver-title": "Удалить ноду Status", + "delete-message": "Удалить сообщение", + "delete-my-account": "Удалить мой аккаунт", + "delete-my-profile": "Удалить мой профиль", + "delete-network-confirmation": "Вы действительно хотите удалить эту сеть?", + "delete-network-error": "Подключитесь к другой сети перед удалением этой", + "delete-network-title": "Удалить сеть?", + "delete-node": "Удалить ноду", + "delete-node-are-you-sure": "Вы уверены, что хотите удалить эту ноду?", + "delete-node-title": "Удалить ноду", + "delete-profile": "Удалить профиль", + "delete-profile-warning": "Внимание: если вы не записали сид-фразу, то потеряете доступ к своим средствам сразу после удаления профиля.", + "deny": "Отказать", + "derivation-path": "Путь вывода", + "describe-channel": "Опишите канал", + "description": "описание", + "dev-mode": "Режим разработки", + "dev-mode-settings": "Настройки режима разработки", + "device-syncing": "Синхронизация устройств", + "devices": "Устройства", + "disable": "отключить", + "disable-all": "Отключить все", + "disable-later-in-settings": "Вы сможете отключить это потом в Настройках ", + "disabled": "Отключен", + "disconnect": "Отключить", + "disconnected": "Чат не в сети", + "discover": "Откройте для себя", + "discover-communities": "Откройте для себя сообщества", + "dismiss": "Отклонить", + "display-collectibles": "Показать коллекции", + "done": "Готово", + "dont-ask": "Больше не спрашивать", + "edit": "Изменить", + "edit-categories": "Изменить категории", + "edit-channel-title": "Редактировать канал", + "edit-chats": "Редактировать чаты", + "edit-community": "Редактировать сообщество", + "edit-favourite": "Редактировать избранное", + "edit-group": "Изменить группу", + "edit-profile": "Редактировать профиль", + "edited": "Отредактировано", + "editing-message": "Редактирование сообщения", + "empty-activity-center": "Ваши уведомления в чате\nбудут появляться здесь", + "empty-chat-description": "В этом чате пока \nнет сообщений", + "empty-chat-description-community": "Здесь было тихо уже в течение {{quiet-hours}}", + "empty-chat-description-one-to-one": "Вся переписка в этом чате зашифрована и может быть прочитаны только вами и ", + "empty-chat-description-public": "Здесь было тихо уже в течение {{quiet-hours}}. Начать разговор или ", + "empty-chat-description-public-share-this": "поделиться этим чатом.", + "empty-keycard-required": "Требуется пустая карта Keycard", + "empty-pending-invitations-descr": "Все желающие вступить в группу\nчерез ссылку приглашения появятся здесь", + "empty-tab": "Пустая вкладка", + "enable": "включить", + "enable-all": "Разрешить все", + "enable-link-previews": "Разрешить предварительный просмотр ссылок в чате?", + "encrypt-with-password": "Зашифровать с помощью пароля", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Добавить имя пользователя", + "ens-agree-to": "Согласиться с ", + "ens-chat-settings": "Настройки чата", + "ens-custom-domain": "Пользовательский домен", + "ens-custom-username-hints": "Введите полное имя пользователя, включая пользовательский домен, например username.domain.eth", + "ens-custom-username-taken": "Имя пользователя вам не принадлежит :(", + "ens-deposit": "Депозит", + "ens-dismiss-message": "Нажмите здесь, чтобы отклонить", + "ens-displayed-with": "Ваши сообщения отображаются для других с", + "ens-get-name": "Получить универсальное имя пользователя", + "ens-got-it": "Понятно", + "ens-locked": "Имя пользователя заблокировано. Вы не сможете разблокировать его до {{date}}", + "ens-name-content": "Псевдоним пользователя для вашего кода контакта. Вы можете зарегистрировать псевдоним с помощью Ethereum Name Service. ENS-имя – это уникальное децентрализованное имя пользователя.", + "ens-name-not-found": "Невозможно определить ENS-имя", + "ens-name-title": "ENS-имя", + "ens-network-restriction": "Доступно только на Mainnet", + "ens-no-usernames": "У вас нет присоединенного имени пользователя", + "ens-powered-by": "При поддержке Ethereum Name Services", + "ens-primary-username": "Основное имя пользователя", + "ens-register": "Зарегистрировать", + "ens-registration-failed": "Повторите попытку регистрации имя пользователя.", + "ens-registration-failed-title": "Не удалось выполнить транзакцию", + "ens-registration-failure": "Сбой регистрации", + "ens-registration-in-progress": "Регистрация в процессе...", + "ens-release-username": "Освободить имя пользователя", + "ens-remove-hints": "Удаление имени пользователя отсоединит его от вашего ключа.", + "ens-remove-username": "Удалить имя пользователя", + "ens-saved": " теперь связано с вашим кодом контакта и может быть использовано в Status.", + "ens-saved-title": "Имя пользователя добавлено", + "ens-show-username": "Показывать мое ENS-имя в чатах", + "ens-terms-header": "Условия регистрации имени", + "ens-terms-point-1": "Средства вносятся на 1 год. Ваши SNT будут заблокированы, но не потрачены.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS-регистр).", + "ens-terms-point-2": "Через 1 год вы можете освободить имя и получить свой депозит обратно или ничего не делать и тогда имя останется вашим.", + "ens-terms-point-3": "Если условия контракта меняются (например, Status обновляет контракт), то пользователь имеет право освободить имя пользователя без учета времени, прошедшего с момента его регистрации.", + "ens-terms-point-4": "Контролер контракта не может получить доступ к внесенным вами средствам. Их можно переместить обратно только на тот адрес, с которого они были отправлены.", + "ens-terms-point-5": "Ваш(и) адрес(а) будет(ут) публично связан(ы) с вашим ENS-именем.", + "ens-terms-point-6": "Имена пользователей создаются в качестве нод субдоменов stateofus.eth и подчиняются условиям смарт-контракта ENS.", + "ens-terms-point-7": "Вы даете разрешение контракту перевести SNT от вашего имени. Это может произойти только если вы одобрите транзакцию для авторизации перевода.", + "ens-terms-point-8": "Эти условия гарантируются смарт-контрактом по адресам:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar) ", + "ens-terms-registration": "Условия регистрации имени.", + "ens-test-message": "Привет", + "ens-transaction-pending": "Транзакция в ожидании подтверждения...", + "ens-understand": "Я понимаю, что мой адрес кошелька будет публично связан с моим именем пользователя.", + "ens-username": "ENS-имя", + "ens-username-already-added": "Имя пользователя уже связано с вашим кодом контакта и может быть использовано в Status.", + "ens-username-available": "✓ Имя пользователя доступно!", + "ens-username-connected": "Это имя пользователя принадлежит вам и связано с вашим кодом контакта.", + "ens-username-connected-continue": "Продолжить настройку: «Показывать мое ENS-имя в чатах».", + "ens-username-connected-with-different-key": "Для продолжения потребуется транзакция, чтобы связать имя пользователя с текущим кодом контакта.", + "ens-username-connection-confirmation": "{{username}} будет подключено, как только транзакция будет завершена.", + "ens-username-hints": "Как минимум 4 символа. Только латинские буквы, цифры и строчные буквы.", + "ens-username-invalid": "Только буквы и цифры.", + "ens-username-invalid-name-warning": "Процесс регистрации одного из ваших имен ens завершился в недействительном состоянии. НЕ ИСПОЛЬЗУЙТЕ это имя для операций с кошельком и обратитесь в нашу службу поддержки по адресу support@status.im.", + "ens-username-owned": "✓ Имя пользователя принадлежит вам.", + "ens-username-owned-continue": "Если вы продолжите, это имя пользователя будет привязано к вашему коду контакта.", + "ens-username-registration-confirmation": "Отлично! Вы станете владельцем {{username}}, как только транзакция будет завершена.", + "ens-username-registration-invalid": "Внимание! Процесс регистрации завершился в недействительном состоянии. НЕ ИСПОЛЬЗУЙТЕ это имя для операций с кошельком и обратитесь в нашу службу поддержки по адресу support@status.im.", + "ens-username-taken": "Это имя пользователя уже занято :(", + "ens-username-you-can-follow-progress": "Вы можете следить за ходом выполнения в разделе вашего кошелька «История транзакций».", + "ens-usernames": "ENS-имена", + "ens-usernames-details": "Зарегистрируйте уникальное имя пользователя для лучшей узнаваемости", + "ens-want-custom-domain": "У меня есть имя на другом домене", + "ens-want-domain": "Я хочу имя на домене stateofus.eth", + "ens-welcome-hints": "ENS-имена преобразуют эти невероятно длинные адреса в уникальные имена пользователей", + "ens-welcome-point-customize": "ENS-имя может заменить в чате ваше случайно сгенерированное имя из трех слов. Будьте @yourname вместо {{name}}.", + "ens-welcome-point-customize-title": "Настройте свое имя в чате", + "ens-welcome-point-receive": "Вам могут отправлять средства через чат одним простым действием.", + "ens-welcome-point-receive-title": "Получайте транзакции в чате", + "ens-welcome-point-register": "Зарегистрировавшись один раз, вы получаете имя навсегда. Через год вы можете освободить имя и вернуть свои токены SNT.", + "ens-welcome-point-register-title": "10 SNT за регистрацию", + "ens-welcome-point-simplify": "Получайте средства, используя свое ENS-имя вместо публичного адреса (0x...)", + "ens-welcome-point-simplify-title": "Упростите свой адрес ETH", + "ens-welcome-point-verify": "Далее вы можете проверить любое имя пользователя и зарегистрировать его, если оно свободно.", + "ens-welcome-point-verify-title": "Уже есть имя пользователя?", + "ens-your-username": "Ваше имя пользователя", + "ens-your-usernames": "Ваши имена пользователей", + "ens-your-your-name": "Ваше ENS-имя", + "enter-12-words": "Введите 12 слов своей сид-фразы через одиночные пробелы", + "enter-a-private-key": "Введите приватный ключ", + "enter-a-seed-phrase": "Введите сид-фразу", + "enter-address": "Введите адрес", + "enter-contact-code": "ENS (vitalik94) или код контакта (0x04...)", + "enter-pair-code": "Введите код сопряжения", + "enter-pair-code-description": "Можно использовать код сопряжения с уже подключенного клиента Status", + "enter-password": "Введите пароль", + "enter-password-migration-prompt": "Введите свой пароль для перемещения контактов, чатов и настроек вместе с ключами", + "enter-pin": "Введите 6-значный код доступа", + "enter-puk-code": "Введите код PUK", + "enter-puk-code-description": "6-значный код доступа был заблокирован. \n Введите код PUK, чтобы разблокировать код доступа.", + "enter-recipient-address-or-username": "Введите адрес или имя пользователя получателя", + "enter-seed-phrase": "Введите сид-фразу", + "enter-url": "Введите URL-адрес", + "enter-user-pk": "Введите публичный ключ пользователя", + "enter-watch-account-address": "Отсканируйте QR-код\nили\nвведите адрес для просмотра", + "enter-word": "Введите слово", + "enter-your-code": "Введите свой 6-значный код доступа", + "enter-your-password": "Введите свой пароль", + "error": "Ошибка", + "error-unable-to-get-balance": "Не удалось получить баланс", + "error-unable-to-get-prices": "Ошибка при конвертации валюты. Обновите содержимое экрана, чтобы повторить попытку.", + "error-unable-to-get-token-balance": "Не удалось получить баланс токенов", + "errors": "Ошибки", + "eth": "ETH", + "ethereum-address": "Адрес Ethereum", + "ethereum-node-started-incorrectly-description": "Нода Ethereum была запущена с неверной конфигурацией, приложение будет остановлено для восстановления после этого состояния. Сконфигурированный идентификатор сети = {{network-id}}, фактический = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Нода Ethereum была запущена неправильно", + "etherscan-lookup": "Посмотреть на Etherscan", + "everyone": "Все", + "expand-all": "Развернуть все", + "export-account": "Экспортировать аккаунт", + "export-key": "Экспортировать приватный ключ", + "external-storage-denied": "Доступ к внешнему накопителю запрещен", + "failed": "Не удалось", + "faq": "Часто задаваемые вопросы", + "fast": "Быстрый", + "favourite": "Избранное", + "favourite-description": "Здесь будут находиться сайты, добавленные в избранное", + "favourites": "Избранное", + "favourites-empty": "Здесь будут отображены адреса, добавленные в избранное.", + "featured": "Избранное", + "fee-cap": "Максимальный размер комиссии", + "fee-explanation": "Максимальная общая цена сделки. Если базовая плата за блок превышает это значение, она будет включена в следующий блок с более низкой базовой комиссией.", + "fee-options": "Предлагаемые варианты оплаты", + "fetch-community": "Найденные сообщества", + "fetch-messages": "↓ Получить сообщения", + "fetch-timeline": "↓ Обновить", + "fetching-community": "Поиск сообщества...", + "find": "Найти", + "finish": "Завершить", + "finishing-card-setup": "Завершение настройки карты", + "fleet": "Fleet", + "fleet-settings": "Настройки fleet", + "follow": "Следите за", + "follow-your-interests": "Присоединяйтесь к публичному чату и знакомьтесь с новыми людьми", + "free": "↓ Бесплатно", + "from": "От", + "gas-amount-limit": "Лимит газа", + "gas-limit": "Лимит газа", + "gas-price": "Цена на газ", + "gas-used": "Использованный газ", + "gated": "Pакрытый", + "generate-a-key": "Сгенерировать ключи", + "generate-a-new-account": "Создать аккаунт", + "generate-a-new-key": "Создать новый ключ", + "generate-account": "Сгенерировать ключи", + "generate-an-account": "Создать аккаунт", + "generate-new-key": "Сгенерировать ключи", + "generating-codes-for-pairing": "> Скачивание программного обеспечения на карту\n> Генерация кодов разблокирования и привязки", + "generating-keys": "Создание ключей...", + "generating-mnemonic": "Генерация сид-фразы", + "get-a-keycard": "Получить Keycard", + "get-started": "Начать", + "get-status-at": "Получить Status по адресу http:\/\/status.im", + "get-stickers": "Получить стикеры", + "give-a-short-description": "Дайте краткое описание", + "give-a-short-description-community": "Дайте краткое описание", + "give-permissions-camera": "Предоставьте разрешение для\nдоступа к камере", + "glossary": "Глоссарий", + "go-to-settings": "Перейти в настройки...", + "got-it": "Понятно", + "grant-face-id-permissions": "Чтобы предоставить необходимое разрешение для Face ID, перейдите в настройки системы и убедитесь, что выбрана опция Status > Face ID.", + "group-chat": "Групповой чат", + "group-chat-admin": "Администратор", + "group-chat-admin-added": "**{{member}}** присвоена роль администратора", + "group-chat-all-contacts-invited": "Все ваши контакты уже в группе", + "group-chat-created": "Пользователь **{{member}}** создал группу **{{name}}**", + "group-chat-decline-invitation": "Отказаться от приглашения", + "group-chat-member-added": "Пользователя **{{member}}** пригласили в группу", + "group-chat-member-joined": "Пользователь **{{member}}** присоединился к группе", + "group-chat-member-removed": "Пользователь **{{member}}** покинул группу", + "group-chat-members-count": "{{selected}}\/{{max}} участников", + "group-chat-name-changed": "Пользователь **{{member}}** изменил имя группы на **{{name}}**", + "group-chat-no-contacts": "У вас еще нет контактов.\nПригласите своих друзей для общения.", + "group-info": "Информация о группе", + "group-invite": "Приглашение в группу", + "group-invite-link": "Ссылка на приглашение в группу", + "group-membership-request": "Запрос на вступление в группу", + "gwei": "Gwei", + "has-permissions": "имеет разрешение на доступ", + "hash": "Хеш", + "help": "помощь", + "help-capitalized": "Помощь", + "help-center": "Справочный центр", + "help-improve-status": "Помогите улучшить Status", + "hide": "Скрыть", + "hide-content-when-switching-apps": "Блокировать снимки экрана", + "hide-content-when-switching-apps-ios": "Скрыть предпросмотр", + "history": "История", + "history-nodes": "Ноды Status", + "hold-card": "Приложите карту к\nзадней панели телефона", + "home": "Главная", + "hooks": "Перехватчики", + "how-it-works": "Как это работает", + "http-gateway-error": "К сожалению, не удалось выполнить запрос.", + "identifier": "Идентификатор", + "image": "Изображение", + "image-remove-current": "Удалить текущее фото", + "image-source-gallery": "Выбрать из галереи", + "image-source-make-photo": "Сделать снимок", + "image-source-title": "Изменить изображение", + "import": "Импорт", + "import-community": "Импортировать сообщество", + "import-community-title": "Импортировать сообщество", + "in-contacts": "В контактах", + "include": "Включает", + "incoming": "Входящие", + "incoming-transaction": "Входящая транзакция", + "incorrect-code": [ + "str", + "Вы ввели неверный код. Попробуйте еще раз." + ], + "increase-gas": "Увеличить газ", + "initialization": "Инициализация", + "install": "↓ Установить", + "insufficient-balance-to-cover-fee": "недостаточно средств для покрытия комиссии за транзакцию", + "intro-message1": "Добро пожаловать в Status!\nНажмите на это сообщение, чтобы задать пароль и начать.", + "intro-privacy-policy-note1": "Status не собирает ваши личные данные и не извлекает из них выгоду . Продолжая, вы соглашаетесь с", + "intro-privacy-policy-note2": " нашей политикой конфиденциальности", + "intro-text": "Status – это ваш пропуск в децентрализованный Интернет", + "intro-text1": "Общайтесь через одноранговую зашифрованную сеть, где сообщения не могут быть подвергнуты цензуре или взлому", + "intro-text2": "Отправляйте и получайте цифровые активы в любой точке мира, без необходимости использовать банковский счет.", + "intro-text3": "Откройте для себя мир игр, бирж и социальных сетей, в которых все ваши данные принадлежат только вам", + "intro-title1": "Действительно приватное общение", + "intro-title2": "Безопасный криптокошелек", + "intro-title3": "Децентрализованные приложения", + "intro-wizard-text1": "Управление вашим аккаунтом осуществляется на основе сгенерированных ключей. Эти ключи хранятся на вашем устройстве, поэтому только вы имеете к ним доступ.", + "intro-wizard-text2": "Один из ключей — это ваш код контакта. Он генерируется вместе с базовым неизменным именем.", + "intro-wizard-text3": "Если у вас есть Keycard, мы рекомендуем хранить ключи только на данном аппаратном кошельке в целях повышения безопасности.", + "intro-wizard-text4": "Защитите и зашифруйте свои ключи", + "intro-wizard-text6": "Приложение Status будет отправлять вам уведомления о новых сообщениях. Вы можете изменить параметры уведомлений в настройках.", + "intro-wizard-title-alt4": "Создайте пароль", + "intro-wizard-title-alt5": "Подтвердите пароль", + "intro-wizard-title1": "Сгенерируйте свои ключи", + "intro-wizard-title2": "Выберите имя в чате", + "intro-wizard-title3": "Выберите хранилище ключей", + "intro-wizard-title4": "Создайте 6-значный код доступа", + "intro-wizard-title5": "Подтвердите код доступа", + "intro-wizard-title6": "Включите уведомления", + "introduce-yourself": "Представьтесь и коротко расскажите о себе", + "invalid-address-qr-code": "Отсканированный QR-код не содержит действительный адрес", + "invalid-format": "Недопустимый формат \nДолжно быть {{format}}", + "invalid-key-confirm": "Применить", + "invalid-key-content": "База данных не может быть зашифрована, так как файл поврежден. Ваши средства и код контакта в безопасности. Другие данные, например, информацию о ваших чатах и контактах, нельзя восстановить. Кнопка «{{erase-multiaccounts-data-button-text}}» удалит все остальные данные и позволит вам получить доступ к своим средствам, а также отправлять сообщения", + "invalid-number": "Недействительный номер", + "invalid-pairing-password": "Неверный пароль сопряжения", + "invalid-public-chat-topic": "Неправильная тема публичного чата", + "invalid-range": "Неверный формат, должно быть от {{min}} и до {{max}}", + "invalid-username-or-key": "Неверное имя пользователя или код контакта", + "invite": "Пригласить", + "invite-button": "Пригласить", + "invite-chat-accept": "Принять", + "invite-chat-accept-join": "Принять и присоединиться", + "invite-chat-intro": "Ваш друг пригласил вас присоединиться к Status. Вот немного криптовалюты для старта! Используй ее, чтобы зарегистрировать ENS-имя или купить набор стикеров.", + "invite-chat-name": "Реферал", + "invite-chat-pending": "В ожидании", + "invite-chat-rule": "Если вы примете приглашение, ваш друг получит реферальный бонус в криптовалюте", + "invite-chat-starter-pack": "Стартовый набор", + "invite-friends": "Пригласить друзей", + "invite-instruction-fifth": "Вы можете использовать свой реферальный бонус в любое время.", + "invite-instruction-first": "Вы отправляете своему другу уникальную ссылку-приглашение, с помощью которой можно скачать Status и присоединиться к нашему сообществу.", + "invite-instruction-fourth": "Вы получите реферальный бонус, а ваш друг – стартовый набор.", + "invite-instruction-second": "Ваш друг скачивает приложение Status для Android и создаёт аккаунт.", + "invite-instruction-third": "Создается чат с вашим другом, в котором он подтверждает приглашение.", + "invite-people": "Пригласить друзей", + "invite-privacy-policy-public": "Вы установили приложение Status по реферальной ссылке. Присоединяясь к этому чату, вы указываете того, кто вас пригласил и соглашаетесь с", + "invite-privacy-policy1": "Принимая стартовый набор, вы соглашаетесь с", + "invite-privacy-policy2": "правилами и положениями реферальной программы.", + "invite-public-chat-home": "Приглашение реферала", + "invite-public-chat-intro": "Вот немного криптовалюты для старта! Используйте её, чтобы зарегистрировать ENS-имя или купить набор стикеров", + "invite-receive-account": "Аккаунт для получения вашего реферального бонуса", + "invite-reward": "Зарабатывайте криптовалюту за каждого приглашенного друга!", + "invite-reward-friend": "Друг: ", + "invite-reward-friend-description": "Ваш друг получит стартовый набор, состоящий из {{reward}}", + "invite-reward-friend-name": "Стартовый набор", + "invite-reward-you": "Вы: ", + "invite-reward-you-description": "Пригласите друга и получите {{reward}} в качестве реферального бонуса. Используйте награду, чтобы получить ENS-имя, стикеры или доступ к децентрализованным приложениям", + "invite-reward-you-name": "Реферальный бонус", + "invite-select-account": "Выберите аккаунт для получения вашего реферального бонуса", + "invite-warning": "Данное предложение актуально только для пользователей Android-устройств, не являющихся резидентами США. В течение 7 дней друг должен подтвердить, что получил приглашение от вас.", + "invited": "приглашен(а)", + "join": "Присоединиться", + "join-a-community": "или присоединиться к сообществу", + "join-group-chat": "Вступить в группу", + "join-group-chat-description": "Пользователь {{username}} пригласил вас присоединиться к группе {{group-name}}", + "join-me": "Привет! Присоединяйтесь ко мне в Status: {{url}}", + "join-new-private-chat": "Создать новый приватный чат", + "join-new-public-chat": "Присоединиться к публичному чату", + "joined": "В группе", + "joined-group-chat-description": "Вы присоединились к {{group-name}} по приглашению {{username}}", + "key": "Ключ", + "key-managment": "Управление ключами", + "key-on-device": "Приватный ключ сохраняется на этом устройстве", + "keycard": "Keycard", + "keycard-access-reset": "Доступ к Keycard был сброшен", + "keycard-applet-install-instructions": "Чтобы установить апплет, следуйте инструкциям на https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Попробуйте переместить карту в диапазон действия NFC-сканера вашего устройства", + "keycard-awaiting-title": "Идет поиск...", + "keycard-backup": "Создать резервную копию Keycard", + "keycard-backup-success-body": "Резервная карта создана успешно. Теперь вы можете использовать ее со своим аккаунтом так же, как и основную карту.", + "keycard-backup-success-title": "Резервное копирование успешно", + "keycard-blocked": "Кошелек Keycard заблокирован. \nЧтобы возобновить его работу, необходимо сбросить настройки карты.", + "keycard-can-use-with-new-passcode": "Вы можете использовать эту карту с вашим новым кодом доступа", + "keycard-cancel-setup-text": "Это отменит настройку Keycard. Настоятельно рекомендуем завершить настройку карты, чтобы ее можно было использовать в дальнейшем. Вы действительно хотите отменить?", + "keycard-cancel-setup-title": "Опасная операция", + "keycard-connected-description": "Держите карту неподвижно", + "keycard-connected-title": "Подключен", + "keycard-desc": "Владеете Keycard? Храните безопасно свои ключи на Keycard, а также подписывайте транзакции с использованием данного аппаратного кошелька", + "keycard-dont-ask-card": "Не спрашивать карту для входа", + "keycard-enter-new-passcode": "Введите новый код доступа {{step}}\/2", + "keycard-error-description": "Подключите карту еще раз, чтобы продолжить", + "keycard-error-title": "Соединение потеряно", + "keycard-factory-reset": "Возврат карты к заводским настройкам", + "keycard-factory-reset-text": "Это приведет к удалению всех мнемонических фраз, хранящихся на карте. Убедитесь, что у вас есть резервная копия мнемонической фразы, которую вы использовали с этим кошельком Keycard.", + "keycard-factory-reset-title": "Вы точно хотите выполнить сброс параметров?", + "keycard-free-pairing-slots": "У Keycard есть свободные слоты ({{n}}) для сопряжения", + "keycard-has-multiaccount-on-it": "Эта карта заполнена. Каждая карта может содержать одну основную пару ключей", + "keycard-init-description": "Приложите карту к задней панели телефона, чтобы продолжить", + "keycard-init-title": "Ищем карту...", + "keycard-is-blocked-details": "Вы больше не можете использовать эту карту для доступа к вашему аккаунту или подписи. Было сделано слишком много неудачных попыток ввода кода доступа и PUK-кода.", + "keycard-is-blocked-instructions": "Чтобы получить доступ к своему аккаунту, необходимо выполнить сброс карты. Нажмите кнопку ниже, чтобы начать процедуру, вам понадобится мнемоника.", + "keycard-is-blocked-title": "Keycard заблокирован", + "keycard-is-frozen-details": "В целях защиты активов ваша карта заморожена. Сбросьте параметры карты, чтобы разморозить ее и получить возможность осуществлять транзакции. Сброс можно выполнить с помощью кода PUK или мнемоники.", + "keycard-is-frozen-factory-reset": "Сброс с мнемоникой", + "keycard-is-frozen-reset": "Сброс c PUK", + "keycard-is-frozen-title": "Keycard заморожен", + "keycard-onboarding-finishing-header": "Завершение", + "keycard-onboarding-intro-header": "Храните ваши ключи на Keycard", + "keycard-onboarding-intro-text": "Это займет несколько минут. Помните: данные действия нужны для защиты вашего аккаунта", + "keycard-onboarding-mnemonic-text": "Вам также понадобятся лист бумаги и ручка, чтобы записать кодовую фразу.", + "keycard-onboarding-pairing-header": "Привязка карты...", + "keycard-onboarding-pin-text": "Вам нужно будет создать 6-значный пароль, который будет использоваться для защиты доступа к вашей карточке-ключу.", + "keycard-onboarding-preparing-header": "Подготовка карты ...", + "keycard-onboarding-puk-code-header": "Запишите коды\nи храните их в надежном месте", + "keycard-onboarding-recovery-phrase-description": "Сид-фраза необходима для того, чтобы вернуть доступ к своим ключам. Запишите ее и храните в надежном и безопасном месте (не рекомендуется хранить слова на мобильных устройствах и документах, размещенных в сети Интернет).", + "keycard-onboarding-recovery-phrase-header": "Резервное копирование сид-фразы", + "keycard-onboarding-recovery-phrase-text": "Только для вашего приватного ознакомления. Эти волшебные слова используются для создания ваших ключей.", + "keycard-onboarding-start-header": "Приложите карту к задней \nпанели телефона, чтобы начать", + "keycard-onboarding-start-step1": "Создать код доступа", + "keycard-onboarding-start-step1-text": "Это займет около минуты. Создайте 6-значный код доступа для шифрования своих ключей", + "keycard-onboarding-start-step2": "Запишите PUK и код сопряжения", + "keycard-onboarding-start-step2-text": "Это займет около минуты. Вам понадобится лист бумаги и ручка", + "keycard-onboarding-start-step3": "Сделайте резервную копию сид-фразы", + "keycard-onboarding-start-step3-text": "Это займет около минуты. Вам также понадобится лист бумаги и карандаш", + "keycard-onboarding-start-text": "Удерживайте карту плотно прижатой к телефону\n во время установки. Она займет около 4-x минут", + "keycard-processing-description": "Держите карту неподвижно", + "keycard-processing-title": "Обработка...", + "keycard-recover": "потерянная или замороженная карта?", + "keycard-recover-text": "Если у вас есть ваша мнемоническая фраза, вы можете создать новый кошелек Keycard, связанный с этим аккаунтом. Можно использовать либо новый Keycard, либо выполнить сброс параметров для замороженного Keycard.", + "keycard-recover-title": "Создать новую карту для этого аккаунта?", + "keycard-recovery-intro-button-text": "Восстановить", + "keycard-recovery-intro-header": "Восстановить ключи, \nхранящиеся на Keycard", + "keycard-recovery-intro-text": "Если ранее вы создали ключи с помощью Keycard и теперь хотите использовать их на своем устройстве", + "keycard-recovery-no-key-header": "Отсутствует информация\nдля восстановления", + "keycard-recovery-no-key-text": "На вашем Keycard отсутствует ключ. Чтобы использовать аппаратный кошелек, сгенерируйте новый ключ и выберите Keycard в качестве хранилища ключей", + "keycard-recovery-phrase-confirm-header": "Подтвердить сид-фразу", + "keycard-recovery-phrase-confirmation-text": "У вас не будет второго шанса! Например, потеряв свой Keycard, вы сможете восстановить доступ к своим ключам только с помощью сид-фразы. Никто, кроме вас, не должен иметь доступ к вашей сид-фразе. Запишите ее и храните в надежном месте.", + "keycard-recovery-phrase-confirmation-title": "Записали свою сид-фразу?", + "keycard-recovery-success-header": "Ваши ключи\nуспешно восстановлены", + "keycard-redeem-title": "Отправить", + "keycard-redeem-tx": "Использовать активы", + "keycard-redeem-tx-desc": "Коснитесь карты для подтверждения вашей подписи, чтобы получить активы", + "keycard-reset-passcode": "Сброс кода доступа", + "keycard-success-description": "Теперь карту можно убрать", + "keycard-success-title": "Успешно", + "keycard-unauthorized-operation": "Вы не авторизованы для выполнения этой операции. \nИспользуйте действительную карту и попробуйте снова.", + "keycard-upsell-subtitle": "Повышенная безопасность и удобство", + "language": "Язык", + "last-backup-performed": "Последнее резервное копирование было выполнено:", + "last-transaction": "Последняя транзакция", + "learn-more": "Узнать больше", + "learn-more-about-keycard": "Узнайте больше о Keycard", + "leave": "Выйти", + "leave-chat": "Выйти из чата", + "leave-chat-confirmation": "История чата будет удалена с вашего устройства. После повторного присоединения вы не сможете восстановить свою историю.", + "leave-community": "Покинуть сообщество", + "leave-confirmation": "Выйти из чата", + "leave-group": "Покинуть группу", + "left": "осталось", + "les-ulc": "LES\/ULC", + "lets-go": "Начать", + "lifestyle": "Образ жизни", + "light": "Светлая", + "limit": "Ограничение", + "linked-on": "Привязано {{date}}", + "load-messages-before": "до {{date}}", + "load-more-messages": "↓ Открыть больше сообщений", + "load-more-timeline": "↓ Показать больше", + "loading": "Загрузка...", + "local-notifications": "Местные уведомления", + "local-notifications-subtitle": "Включить фоновые службы", + "lock-app-with": "Блокировать приложение с помощью", + "log-level": "Уровень ведения журнала", + "log-level-settings": "Настройки уровня ведения журнала", + "logging": "Ведение журнала", + "logging-enabled": "Ведение журнала включено?", + "login-pin-description": "Введите 6-значный код доступа для разблокировки ваших ключей", + "logout": "Выйти", + "logout-app-content": "Вы выйдете из системы. При повторной разблокировке будет использоваться выбранная сеть.", + "logout-are-you-sure": "Вы действительно \nхотите выйти?", + "logout-key-management": "Для доступа к управлению ключами необходимо выйти из системы.", + "logout-title": "Выйти из системы?", + "looking-for-cards": "Ищем карту...", + "lost-connection": "Соединение прервано", + "low-tip": "чаевые слишком маленькие", + "lower-than-average-tip": "чаевые ниже среднего уровня", + "mail-should-be-configured": "Необходимо настроить почтовый клиент", + "mailserver-address": "Адрес ноды Status", + "mailserver-automatic": "Автоматический выбор", + "mailserver-automatic-switch-explanation": "Выберите самую быструю ноду Status среди доступных", + "mailserver-connection-error": "Не удалось подключиться к ноде Status", + "mailserver-content": "Нода в сети Status отвечает за маршрутизацию и хранение сообщений сроком до 30 дней.", + "mailserver-details": "Сведения о ноде Status", + "mailserver-error-content": "Выбранная вами нода Status недоступна.", + "mailserver-error-title": "Ошибка подключения к ноде Status", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Выберите другую ноду Status", + "mailserver-reconnect": "Не удалось подключиться к ноде Status. Нажмите для повторного подключения.", + "mailserver-request-error-content": "Нода Status вернула следующую ошибку: {{error}}", + "mailserver-request-error-status": "При получении истории произошла ошибка, проверьте подробности в журналах", + "mailserver-request-error-title": "Ошибка запроса к ноде Status", + "mailserver-request-retry": "Повторить запрос", + "mailserver-retry": "Повторить", + "mailserver-title": "Нода Status", + "main-account": "Основной аккаунт", + "main-currency": "Основная валюта", + "main-networks": "Основные сети", + "main-wallet": "Основной кошелек", + "mainnet-network": "Основная сеть", + "make-admin": "Сделать администратором", + "manage-connections": "Управление подключениями из приложений", + "manage-keys-and-storage": "Управление ключами и хранилищем", + "mark-all-read": "Отметить всё как прочитанное", + "master-account": "Главный аккаунт", + "max-fee": "Максимальная комиссия", + "max-priority-fee": "Максимальная приоритетная комиссия", + "maximum-fee": "Максимальная комиссия", + "maximum-fee-desc": "Максимальная общая цена сделки. Если текущая базовая комиссия за блок превышает это значение, ваша транзакция будет включена в следующий блок с более низкой базовой комиссией.", + "maybe-later": "Возможно, позже", + "member-ban": "Забанить участника", + "member-kick": "Удалить участника", + "members": { + "zero": "участников", + "one": "1 участник", + "few": "{{count}} участника", + "many": "{{count}} участников", + "other": "Участников: {{count}}" + }, + "members-active": { + "zero": "участников", + "one": "1 участник", + "few": "{{count}} участника", + "many": "{{count}} участников", + "other": "Участников: {{count}}" + }, + "members-active-none": "нет участников", + "members-count": "Участников: {{count}}", + "members-label": "Участники", + "members-limit-reached": "Достигнут лимит участников", + "members-title": "Участники", + "membership-approval": "Требуется одобрение", + "membership-approval-description": "Сообщество открытое, но новые участники должны быть одобрены создателем сообщества", + "membership-button": "Требование к участию", + "membership-declined": "Заявка на вступление в группу была отклонена", + "membership-description": "Для вступления в группу ваша заявка должна быть одобрена администратором", + "membership-ens": "Требуется ENS-имя", + "membership-ens-description": "Для присоединения к сообществу требуется ENS-имя", + "membership-free": "Не требуется", + "membership-free-description": "Сообщество открыто для всех желающих", + "membership-invite": "Требуется приглашение от другого участника", + "membership-invite-description": "К сообществу можно присоединиться только по приглашению существующих членов сообщества", + "membership-none": "Нет", + "membership-none-placeholder": "Вы можете потребовать, чтобы новые участники соответствовали определенным критериям. Это условие можно изменить в любое время", + "membership-request-pending": "Запрос на участие находится на рассмотрении", + "membership-requests": "Запросы на участие", + "membership-title": "Требование к участию", + "message": "Сообщение", + "message-not-sent": "Сообщение не отправлено", + "message-options-cancel": "Отменить", + "message-reply": "Ответить", + "messages": "Сообщения", + "messages-from-contacts-only-subtitle": "Только люди, которых вы добавили в свои контакты, могут начать с вами новый чат или пригласить в группу", + "might-break": "Некоторые ÐApp станут недоступными", + "migration-successful": "Перенос прошел успешно", + "migration-successful-text": "Аккаунт успешно перенесен на Keycard", + "migrations-failed-content": "{{message}}\nверсия схемы: начальная {{initial-version}}, текущая {{current-version}}, последняя {{last-version}}\n\nПроизошла ошибка базы данных. Ваши средства и код контакта в безопасности. Другие данные, например, информацию о ваших чатах и контактах, нельзя восстановить. Кнопка «{{erase-multiaccounts-data-button-text}}» удалит все остальные данные и позволит вам получить доступ к своим средствам, а также отправлять сообщения.", + "miners-higher-fee": "Майнеры, скорее всего, проведут вашу транзакцию раньше, если вы заплатите более высокую комиссию.", + "minimum-received": "Полученный минимум", + "mobile-network-ask-me": "Спросить меня при использовании мобильной сети", + "mobile-network-continue-syncing": "Продолжить синхронизацию", + "mobile-network-continue-syncing-details": "Вы можете изменить это позже в настройках", + "mobile-network-go-to-settings": "Перейти в настройки", + "mobile-network-settings": "Мобильные данные", + "mobile-network-sheet-configure": "Вы можете настроить синхронизацию\nболее подробно в", + "mobile-network-sheet-offline": "Wi-Fi не подключен, синхронизация сообщений отключена.", + "mobile-network-sheet-offline-details": "Синхронизация с использованием мобильной сети отключена", + "mobile-network-sheet-remember-choice": "Запомнить мой выбор", + "mobile-network-sheet-settings": "настройках", + "mobile-network-start-syncing": "Начать синхронизацию", + "mobile-network-stop-syncing": "Прервать синхронизацию", + "mobile-network-stop-syncing-details": "До подключения к Wi-Fi?", + "mobile-network-use-mobile": "Использовать мобильные данные", + "mobile-network-use-mobile-data": "Status может быстро расходовать мобильный трафик при синхронизации чатов и кошелька.", + "mobile-network-use-wifi": "Только Wi-Fi", + "mobile-syncing-sheet-details": "Status может быстро расходовать мобильный трафик при синхронизации чатов и кошелька.", + "mobile-syncing-sheet-title": "Синхронизация с использованием мобильных данных?", + "more": "больше", + "move-and-reset": "Переместить и сбросить", + "move-keystore-file": "Переместить файл хранилища ключей", + "move-keystore-file-to-keycard": "Переместить файл хранилища ключей на Keycard?", + "multiaccount-exists-content": "Ключи для этого аккаунта уже существуют, и их нельзя добавить повторно. Если вы потеряли пароль, код доступа или Keycard, удалите приложение, переустановите его и восстановите доступ к ключам, введя сид-фразу.", + "multiaccount-exists-title": "Ключи для этого аккаунта уже существуют", + "multiaccounts-recover-enter-phrase-text": "Введите 12, 15, 18, 21 или 24 слова. \nРазделяйте слова одним пробелом.", + "multiaccounts-recover-enter-phrase-title": "Введите свою сид-фразу", + "music": "Музыка", + "mutal-contacts": "Взаимные контакты", + "mute": "Без звука", + "mutual-contact-requests": "Запросы на взаимные контакты", + "my-accounts": "Мои аккаунты", + "my-accounts-empty": "Доступные аккаунты появятся здесь", + "my-profile": "Мой профиль", + "my-status": "Мой статус", + "name": "Имя", + "name-of-token": "Название вашего токена", + "name-optional": "Имя (необязательно)", + "name-your-channel": "Назовите свой канал", + "name-your-channel-placeholder": "Название канала", + "name-your-community": "Назовите ваше сообщество", + "name-your-community-placeholder": "Запоминающееся имя", + "need-help": "Нужна помощь?", + "negative": "Отрицательный", + "network": "Сеть", + "network-chain": "Тип сети", + "network-details": "Сведения о сети", + "network-fee": "Комиссия сети", + "network-id": "Идентификатор сети", + "network-info": "Сведения о сети", + "network-invalid-network-id": "Указанный идентификатор сети не соответствует идентификатору сети по URL-адресу RPC", + "network-invalid-status-code": "Неверный код состояния: {{code}}", + "network-invalid-url": "Сетевой URL недействителен", + "network-settings": "Настройки сети", + "never": "Никогда", + "new": "Новый", + "new-category": "Новая категория", + "new-chat": "Новый чат", + "new-community-title": "Новое сообщество", + "new-contact": "Новый контакт", + "new-contract": "Новый контракт", + "new-favourite": "Новый фаворит", + "new-group": "Новая группа", + "new-group-chat": "Новый групповой чат", + "new-network": "Новая сеть", + "new-password": "Новый пароль", + "new-pin-description": "Введите новый 6-значный код доступа", + "new-public-group-chat": "Присоединиться к публичному чату", + "new-puk-description": "Введите новый 12-значный PUK", + "new-status": "Новый статус", + "new-tab": "Новая вкладка", + "new-ui": "Новый интерфейс", + "next": "Далее", + "nickname": "Псевдоним", + "nickname-description": "Псевдонимы помогут вам идентифицировать других в Status.\nТолько вы можете видеть псевдонимы, добавленные вами.", + "no": "Нет", + "no-collectibles": "Нет доступных коллекций", + "no-contacts": "Пока нет контактов", + "no-keycard-applet-on-card": "На карте не установлен апплет Keycard", + "no-messages": "Нет сообщений", + "no-pairing-slots-available": "Карта уже привязана к 5-ти устройствам, и ее нельзя привязать к ещё одному. Используйте одно из привязанных устройств, войдите с помощью этой карты и освободите слоты привязки.", + "no-pinned-messages": "Нет закрепленных сообщений", + "no-result": "Нет результатов", + "no-thanks": "Нет, спасибо", + "no-tokens-found": "Токены не найдены", + "node-address": "Адрес ноды", + "node-details": "Сведения о ноде", + "node-info": "Информация о ноде", + "node-version": "Версия ноды", + "nodes-disabled": "Ноды Status отключены", + "non-archival-node": "Конечная точка RPC не поддерживает запросы архивации. История ваших локальных переводов может быть неполной.", + "non-contacts": "Нет контактов", + "nonce": "Nonce", + "none": "Нет", + "normal": "Обычный", + "not-applicable": "Неприменимо для неподписанных транзакций", + "not-connected-nodes": "Нет подключения к ноде Status", + "not-connected-to-peers": "Нет подключения к узлам", + "not-enough-snt": "Недостаточно SNT", + "not-found": "Не найдено", + "not-keycard-text": "Карта, которую вы использовали, не является Keycard. Вам нужно купить Keycard, чтобы пользоваться данной функцией", + "not-keycard-title": "Это не Keycard", + "not-registered": "не зарегистрированный", + "notification-settings": "Уведомления", + "notifications": "Уведомления", + "notifications-non-contacts": "Уведомления от тех, кого нет в ваших контактах", + "notifications-preferences": "Параметры уведомлений", + "notifications-servers": "Серверы уведомлений", + "notifications-switch": "Просмотр уведомлений", + "notifications-transactions": "Операции с кошельком", + "notify": "Уведомлять", + "now": "сейчас", + "off": "Выкл.", + "off-status-tree": "Выключить дерево Status", + "offline": "Не в сети", + "offline-messaging-use-history-explanation": "Включите ноды Status для получения сообщений, которые отправляются вам при закрытом приложении. Если эта функция включена, нода Status получает ваш IP-адрес. В противном случае вы не будете получать сообщения, когда приложение закрыто, и не увидите их при последующем открытии приложения.", + "offline-messaging-use-history-nodes": "Использовать ноды Status", + "ok": "OK", + "ok-continue": "Хорошо, продолжить", + "ok-got-it": "Понятно", + "ok-save-pass": "Хорошо, сохранить пароль", + "okay": "Хорошо", + "on": "Вкл.", + "on-status-tree": "Включить дерево Status", + "once-enabled-share-metadata": "Если эта функция включена, ссылки, размещенные в чате, могут передавать ваши метаданные сайту", + "one-day": "Один день", + "one-month": "Один месяц", + "one-week": "Одну неделю", + "open": "Открыть", + "open-chat": "Создать чат", + "open-dapp": "Открыть ÐApp", + "open-dapp-store": "Откройте для себя мир ÐApp-приложений", + "open-home": "Открыть...", + "open-in-new-tab": "Открыть в новой вкладке", + "open-membership": "Открытое участие", + "open-nfc-settings": "Открыть настройки NFC", + "open-on-block-explorer": "Открыть в обозревателе блоков", + "opened": "Открытый", + "opening-buy-crypto": "Открытие {{site}}...", + "optimal": "Оптимальный", + "optional": "опционально", + "or": "или", + "outgoing": "Исходящие", + "outgoing-transaction": "Исходящая транзакция", + "page-camera-request-blocked": "Доступ к камере отклонен. Для включения камеры необходимо изменить настройки доступа на вашем устройстве.", + "page-would-like-to-use-camera": "хотите использовать камеру?", + "pair": "Связать устройства", + "pair-card": "Установить сопряжение с этим устройством", + "pair-code": "Код сопряжения", + "pair-code-explanation": "Устанавливает сопряжение карты с другим устройством (до 5) для разблокировки ключей и подписания транзакций с помощью одного и того же Keycard", + "pair-code-placeholder": "Код сопряжения...", + "pair-this-card": "Привязать эту карту", + "pair-this-device": "Установить сопряжение", + "pair-this-device-description": "Свяжите устройства для синхронизации контактов и чатов между ними", + "paired-devices": "Связанные устройства", + "pairing": "Сопряжение", + "pairing-card": "Привязка карты", + "pairing-changed": "Код сопряжения был изменен", + "pairing-code-placeholder": "Код сопряжения...", + "pairing-code_error1": "Коды сопряжения не совпадают.", + "pairing-go-to-installation": "Перейти к параметрам сопряжения", + "pairing-maximum-number-reached-content": "Отвяжите одно из своих устройств перед добавлением нового.", + "pairing-maximum-number-reached-title": "Достигнуто максимальное количество устройств", + "pairing-new-installation-detected-content": "Обнаружено новое устройство.\nДля того, чтобы правильно использовать устройства, важно выполнить сопряжение и включить их перед использованием.\nДля сопряжения устройств перейдите в раздел «Устройства» в настройках.", + "pairing-new-installation-detected-title": "Обнаружено новое устройство", + "pairing-no-info": "Нет информации", + "pairing-please-set-a-name": "Задайте название вашего устройства.", + "passphrase": "Сид-фраза", + "password": "Пароль", + "password-description": "Введите не менее 6 символов. Данный пароль оберегает ваши ключи. Он нужен для разблокировки приложения и совершения транзакций.", + "password-mismatch": "Новый пароль и его подтверждение не совпадают", + "password-placeholder": "Пароль...", + "password-placeholder2": "Подтвердите пароль", + "password-reset-in-progress": "Изменение пароля...", + "password-reset-success": "Пароль изменен", + "password-reset-success-message": "Вам нужно будет снова войти в систему", + "password_error1": "Пароли не совпадают.", + "paste": "Вставить", + "paste-json": "Вставить JSON", + "pay-to-chat": "Оплатить для чата", + "peer-content": "Устройство, подключенное к сети чатов приложения Status. Каждый пользователь может представлять один или несколько одноранговых узлов, в зависимости от количества устройств.", + "peer-title": "Одноранговый узел", + "peer-to-peer": "Одноранговая сеть", + "peers": "Одноранговые узлы", + "pending": "В ожидании", + "pending-confirmation": "Ожидание подтверждения...", + "pending-invitations": "Нерассмотренные заявки на вступление", + "per-gas-price-limit": "Предел цены за газ", + "per-gas-tip-limit": "Предел чаевых за газ", + "perform-backup": "Сделать резервную копию", + "permissions": "Разрешения", + "phone-e164": "Международный 1", + "photos": "Фотографии", + "photos-access-error": "Чтобы предоставить необходимое разрешение для фото, перейдите в настройки системы и убедитесь, что выбрана опция Status > Фотографии.", + "pin": "Закрепить", + "pin-changed": "6-значный код доступа был изменен", + "pin-code": "6-значный код доступа", + "pin-limit-reached": "Лимит закрепленных сообщений достигнут. Сначала открепите предыдущее сообщение.", + "pin-mismatch": "Неверный код доступа", + "pin-one-attempt": " одна попытка ", + "pin-one-attempt-blocked-after": "до того, как ваш Keycard будет заблокирован", + "pin-one-attempt-blocked-before": "Будьте осторожны, у вас есть только", + "pin-one-attempt-frozen-after": "до того, как ваш Keycard будет заморожен", + "pin-one-attempt-frozen-before": "Будьте осторожны, у вас есть только", + "pin-retries-left": "Осталось попыток: {{number}}", + "pinned-by": "Закреплено", + "pinned-messages": "Закрепленные сообщения", + "pinned-messages-count": { + "zero": "ноль", + "one": "одно", + "few": "несколько", + "many": "много", + "other": "другое" + }, + "pinned-messages-empty": "Закрепленные сообщения появятся здесь. Чтобы закрепить сообщение, нажмите и удерживайте его, а затем нажмите «Закрепить».", + "podcasts": "Подкасты", + "positive": "Положительный", + "powered-by-paraswap": "При поддержке Paraswap", + "preference": "Темы", + "preview-privacy": "Предпросмотр в приватном режиме", + "previewing-may-share-metadata": "При предварительном просмотре ссылок с этих сайтов ваши метаданные могут предоставляться владельцам сайтов", + "price-impact": "Влияние цены", + "price-impact-desc": "Предполагаемое влияние цены на эту транзакцию. Если текущая базовая комиссия за блок превышает это значение, ваша транзакция будет включена в следующий блок с более низкой базовой комиссией.", + "principles": "Наши принципы", + "priority": "Приоритет", + "privacy": "Конфиденциальность", + "privacy-and-security": "Конфиденциальность и безопасность", + "privacy-photos": "Конфиденциальность фото профиля", + "privacy-policy": "Политика конфиденциальности", + "privacy-show-to-warning": "Люди, которые уже видели ваше изображение профиля, будут продолжать его видеть", + "private-key": "Приватный ключ", + "private-notifications": "Приватные уведомления", + "private-notifications-descr": "Приложение Status будет отправлять вам уведомления о новых сообщениях. Вы можете изменить параметры уведомлений в настройках.", + "processing": "Минутку...", + "product-information": "Информация о продукте", + "profile": "Профиль", + "profile-deleted-content": "Ваш профиль был успешно удалён", + "profile-deleted-keycard": "Теперь вы можете восстановить другую пару ключей на вашем Keycard", + "profile-deleted-title": "Профиль удален", + "profile-details": "Детали профиля", + "profile-not-found": "Профиль не найден", + "profile-pic-pick": "Выбрать из галереи", + "profile-pic-remove": "Удалить фото", + "profile-pic-take": "Сделать фото", + "profile-picture-updated": "Фото профиля обновлено", + "public": "Публичный", + "public-channel": "Публичный канал", + "public-chat": "Публичный чат", + "public-chat-description": "Присоединяйтесь к публичным чатам по интересам. Любой пользователь может создать новый чат.", + "public-chats": "Публичные чаты", + "public-group-status": "Публичный", + "public-group-topic": "Тема", + "public-key": "Публичный ключ", + "puk-and-pairing-codes-displayed": "Показаны PUK и код сопряжения", + "puk-changed": "12-значный PUK был изменен", + "puk-code": "PUK-код", + "puk-code-explanation": "Если вы забыли 6-значный код доступа или ввели его неправильно 3 раза, то вам понадобится этот PUK-код для разблокировки карты.", + "puk-mismatch": "Неверный PUK-код", + "push-failed-transaction": "Сбой транзакции", + "push-failed-transaction-body": "{{value}}{{currency}} на {{to}}", + "push-inbound-transaction": "Вы получили {{value}} {{currency}}", + "push-inbound-transaction-body": "От {{from}} на {{to}}", + "push-notifications-server-enabled": "Сервер подключен", + "push-notifications-servers": "Серверы push-уведомлений", + "push-outbound-transaction": "Вы отправили {{value}}{{currency}}", + "push-outbound-transaction-body": "От {{from}} на {{to}}", + "quiet-days": "{{quiet-days}} дн.", + "quiet-hours": "{{quiet-hours}} ч.", + "re-encrypt-key": "Зашифруйте свои ключи повторно", + "rearrange-categories": "Изменение порядка категорий", + "receive": "Получить", + "receive-transaction": "Получить транзакцию", + "recent": "Последние", + "recent-empty": "Недавно использованные адреса появятся здесь", + "recent-recipients": "Контакты", + "recently-used-stickers": "Недавно использованные стикеры появятся здесь", + "recipient": "Получатель", + "recipient-code": "Введите адрес получателя", + "recipient-code-placeholder": "0x... или username.domain.eth", + "recover": "Восстановить", + "recover-key": "Доступ к существующим ключам", + "recover-keycard-multiaccount-not-supported": "Ключи для этого аккаунта уже существуют, и их нельзя добавить повторно. Если вы потеряли пароль, 6-значный код доступа или Keycard, удалите приложение, переустановите его и восстановите доступ к ключам, введя сид-фразу.", + "recover-with-keycard": "Восстановить с помощью Keycard", + "recover-with-seed-phrase": "Восстановить с помощью сид-фразы", + "recovering-key": "Доступ к ключам...", + "recovery-confirm-phrase": "Подтвердите сид-фразу", + "recovery-phrase": "Сид-фраза", + "recovery-success-text": "Вам нужно будет создать новый код или пароль, чтобы повторно зашифровать свои ключи", + "recovery-typo-dialog-description": "Обратите внимание: в сид-фразе должны использоваться те же слова и в том же порядке, в котором вы их получили", + "recovery-typo-dialog-title": "Эта сид-фраза верна?", + "redeem-amount": "Доступных бонусов: {{quantity}}", + "redeem-now": "Использовать сейчас", + "redeem-success": "Бонус успешно получен!", + "reduced-tip": "приоритет чаевых будет уменьшен", + "refresh": "Обновить", + "registered": "зарегистрированный", + "reject": "Отклонить", + "reject-and-delete": "Отклонить и удалить", + "remember-me": "Запомнить меня", + "remind-me-later": "Показать еще раз", + "remote-notifications": "Удаленные уведомления", + "remote-notifications-subtitle": "Включите push-уведомления Google", + "remove": "Удалить", + "remove-favourite": "Удалить из избранного", + "remove-from-chat": "Удалить из чата", + "remove-from-contacts": "Удалить из контактов", + "remove-from-contacts-text": "Удаляя пользователя из списка контактов, вы не скрываете от него адрес своего кошелька", + "remove-group": "Удалить группу", + "remove-network": "Удалить сеть", + "remove-token": "Удалить токен", + "removed": "удалено", + "removed-from-contacts": "Удалено из контактов", + "repeat-pin": "Повторите новый 6-значный код доступа", + "repeat-puk": "Повторите новый 12-значный PUK", + "replying-to": "Ответ {{author}}", + "report-bug-email-template": "1. Описание проблемы \n(Опишите функцию, которую вы хотели бы иметь, или кратко опишите ошибку: ваши действия, ожидания и полученный результат, изложив информацию в соответствующих разделах ниже)\n\n\n2. Действия для повторения ошибки:\n(Опишите, как мы можем повторить ошибку шаг за шагом) \nНапример:\n- Открыть Status \n- ... \n- Шаг 3 и т. д.\n\n\n3. Ожидаемый результат:\n(Опишите, что вы ожидали получить в результате своих действий) \n\n\n4. Фактический результат:\n(Опишите, что произошло на самом деле) \n\n\n5. Приложите скриншоты, на которых видно проблему.\n", + "request-access": "Запрос доступа", + "request-feature": "Запросить новую функцию", + "request-membership": "Отправить запрос на участие", + "request-pending": "На рассмотрении...", + "request-transaction": "Запросить транзакцию", + "required-field": "Обязательное поле", + "resend-message": "Отправить повторно", + "reset-card": "Сбросить параметры карты", + "reset-card-description": "Эта операция вернет карту в начальное состояние. Все данные, включая приватные ключи, будут уничтожены. Операция необратима.", + "reset-database": "Очистить базу данных", + "reset-database-warning": "Будут удалены чаты, контакты и настройки. Требуется при потере пароля.", + "reset-database-warning-keycard": "Будут удалены чаты, контакты и настройки.", + "reset-password": "Сброс пароля", + "restore-defaults": "Восстановить настройки по умолчанию", + "retry": "Повторить", + "revoke-access": "Отменить доступ", + "rpc-url": "URL-адрес RPC", + "rpc-usage-copy": "Копировать", + "rpc-usage-filter": "Методы фильтрации", + "rpc-usage-filter-methods": "Методы фильтрации", + "rpc-usage-filtered-total": "{{filtered-total}} из {{total}}", + "rpc-usage-get-stats": "Обновить", + "rpc-usage-info": "Статистика использования RPC", + "rpc-usage-reset": "Сброс", + "rpc-usage-total": "Всего", + "safe-estimate": "Безопасная оценка", + "save": "Сохранить", + "save-password": "Сохранить пароль", + "save-password-unavailable": "Установите код доступа на ваше устройство, чтобы сохранить пароль", + "save-password-unavailable-android": "Сохранить пароль невозможно: устройство было подвергнуто рутингу (rooting) или не имеет необходимых функций безопасности.", + "say-hi": "Поздороваться", + "scan-qr": "Сканировать QR-код", + "scan-qr-code": "Сканировать QR-код с адресом кошелька", + "scan-tokens": "Сканировать токены", + "search": "Поиск", + "search-no-chat-found": "По вашему запросу ничего не найдено. Возможно, вы имели в виду:", + "secret-keys-confirmation-text": "Они понадобятся вам, чтобы продолжать использовать Keycard в случае утери телефона.", + "secret-keys-confirmation-title": "Вы записали эти коды?", + "security": "Безопасность", + "see-details": "Смотрите подробности", + "see-it-again": "ПОСМОТРЕТЬ ЕЩЕ РАЗ", + "see-suggestions": "Посмотреть предложения", + "seed-key-uid-mismatch": "Сид-фраза введена неправильно", + "seed-key-uid-mismatch-desc-1": "Введенная вами сид-фраза не соответствует фразе для {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Для управления ключами этого аккаунта проверьте сид-фразу и попробуйте снова.", + "seed-phrase-content": "Фраза из удобочитаемых слов, выбранных случайным образом из списка стандарта BIP39. Сид-фраза используется для восстановления аккаунта Ethereum или предоставления доступа к нему на других кошельках и устройствах. Встречаются также другие названия сид-фразы: «мнемоническая фраза», «фраза восстановления» или «резервная копия кошелька». Большинство криптоприложений используют этот же стандарт при генерации новых аккаунтов.", + "seed-phrase-placeholder": "Сид-фраза...", + "seed-phrase-title": "Сид-фраза", + "select": "Выбрать", + "select-account": "Выберите аккаунт", + "select-account-dapp": "Выберите аккаунт, который хотите использовать с Dapps", + "select-account-first": "Сначала выберите аккаунт", + "select-chat": "Выберите чат, чтобы начать общение", + "select-new-location-for-keys": "Выберите новое хранилище для приватных ключей", + "select-token-to-receive": "Выберите токен для получения", + "select-token-to-swap": "Выберите токен для обмена", + "selected": "Выбранные", + "send-contact-request-message": "Для начала чата вам нужно стать контактами", + "send-logs": "Сообщить об ошибке", + "send-logs-to": "Сообщить об ошибке по адресу электронной почты {{email}}", + "send-message": "Отправить сообщение", + "send-push-notifications": "Отправлять push-уведомления", + "send-push-notifications-description": "Если данная функция отключена, пользователь не получит уведомление о новом сообщении.", + "send-request": "Послать запрос", + "send-request-amount": "Сумма", + "send-request-amount-max-decimals": "Максимальное количество знаков после запятой — {{asset-decimals}}", + "send-request-unknown-token": "Неизвестный токен — {{asset}}", + "send-sending-to": "Получатель: {{recipient-name}}", + "send-transaction": "Отправить транзакцию", + "sending": "Отправка", + "sent-at": "Отправлено", + "server": "Сервер", + "set-a-topic": "Создать тему", + "set-currency": "Установить валюту", + "set-custom-fee": "Установить комиссию", + "set-dapp-access-permissions": "Установить права доступа к DApp", + "set-max": "Установить максимум", + "settings": "Настройки", + "share": "Поделиться", + "share-address": "Поделиться адресом", + "share-chat": "Поделиться чатом", + "share-contact-code": "Поделиться своим кодом контакта", + "share-dapp-text": "Попробуйте это DApp, которое я использую в приложении Status: {{link}}", + "share-link": "Поделиться ссылкой", + "share-logs": "Поделиться логами", + "share-my-profile": "Поделиться моим профилем", + "share-profile": "Поделиться профилем", + "share-profile-link": "Поделиться ссылкой на профиль", + "share-public-chat-text": "Присоединяйтесь к публичному чату в приложении Status: {{link}}", + "shared": "Общий", + "sharing-copied-to-clipboard": "Скопировано", + "sharing-copy-to-clipboard": "Копировать", + "sharing-data-desc-1": "Данные проверяются на соответствие общедоступным правилам, чтобы исключить отправку конфиденциальных данных. Не доверяйте, проверяйте.", + "sharing-data-desc-2": "Данные об использовании отправляются сквозным шифрованием по одноранговой сети Status", + "sharing-data-desc-3": "Вместо обычного ключа чата используется одноразовый ключ.", + "sharing-data-desc-4": "Данные об использовании не могут быть связаны с вашим IP-адресом", + "sharing-data-desc-5": "Совокупные данные всех пользователей общедоступны", + "sharing-data-desc-6": "Данные удаляются с вашего телефона после их отправки", + "sharing-share": "Поделиться", + "show-less": "Скрыть", + "show-more": "Показать больше", + "show-notifications": "Просмотр уведомлений", + "show-profile-pictures": "Показывать фотографии профиля", + "show-profile-pictures-to": "Показать ваше фото профиля", + "show-qr": "Показать QR-код", + "show-transaction-data": "Показать детали транзакции", + "sign-and-send": "Подписать и отправить", + "sign-anyway": "Подписать в любом случае", + "sign-in": "Войти", + "sign-message": "Подписать сообщение", + "sign-out": "Выход", + "sign-request-failed": "Не удалось подписать сообщение", + "sign-with": "Подписать с помощью", + "sign-with-password": "Подписать с паролем", + "sign-you-in": "Выполняется вход...", + "signing": "Подписание", + "signing-a-message": "Подписание сообщения", + "signing-phrase": "Фраза для подписи", + "skip": "Пропустить", + "slippage": "Проскальзывание", + "slow": "Медленный", + "something-went-wrong": "Что-то пошло не так", + "soon": "Скоро", + "sort-communities": "Сортировать сообщества", + "specify-address": "Укажите адрес", + "specify-name": "Укажите имя", + "specify-network-id": "Укажите идентификатор сети", + "specify-rpc-url": "Укажите URL-адрес RPC", + "specify-server-public-key": "Введите публичный ключ сервера", + "specify-symbol": "Укажите символ", + "start-chat": "Начать общение в чате", + "start-conversation": "Начать разговор", + "start-group-chat": "Начать общение в групповом чате", + "start-new-chat": "Новый чат", + "starter-pack-coming": "Стартовый набор скоро будет у вас", + "starter-pack-coming-description": "Время ожидания: от нескольких минут до нескольких часов", + "starter-pack-received": "Стартовый набор получен", + "starter-pack-received-description": "Вот немного криптовалюты для старта! Используйте её, чтобы получить ENS-имя, стикеры и доступ к децентрализованным приложениям", + "status": "Status", + "status-always-online": "Всегда онлайн", + "status-automatic": "Автоматический", + "status-automatic-subtitle": "Установить статус автоматически", + "status-confirmed": "Подтверждено", + "status-dnd": "Не беспокоить", + "status-dnd-subtitle": "Отключить все уведомления", + "status-hardwallet": "Status hardwallet", + "status-inactive": "Неактивен", + "status-inactive-subtitle": "Скрыть ваш онлайн статус", + "status-is-open-source": "Статус - проект с открытым исходным кодом", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status задействует большой объем данных при синхронизации чатов. Вы можете отказаться от синхронизации, если используется мобильный трафик.", + "status-not-sent-click": "Не подтверждено. Нажмите для просмотра параметров.", + "status-not-sent-tap": "Не подтверждено. Нажмите для просмотра параметров.", + "status-pending": "В ожидании", + "status-sent": "Отправлено", + "status-tx-not-found": "Транзакция не найдена", + "status-updates-descr": "Обновления статуса будут отображаться здесь. Добавьте профиль в контакты для получения обновлений с привязкой ко времени.", + "statuses-descr": "Делитесь тем, что у вас на уме, и будьте в курсе последних изменений у ваших контактов", + "statuses-my-status-descr": "Делитесь тем, что у вас на уме. Любой, кто посетит вашу страницу, сможет увидеть ваш статус. Люди, добавившие вас в свои контакты, будут получать изменения вашего статуса с привязкой ко времени.", + "step-i-of-n": "Шаг {{step}} из {{number}}", + "sticker": "Стикер", + "sticker-market": "Магазин стикеров", + "storage": "Хранилище", + "submit": "Подтвердить", + "submit-bug": "Сообщить об ошибке", + "success": "Успешно", + "successful-connection": "Успешное подключение", + "suggested-min-tip": "Рекомендуемые минимальные чаевые", + "suggested-price-limit": "Рекомендуемый ценовой лимит", + "swap": "Обмен", + "swap-details": "Поменять детали", + "switch-to-simple-interface": "Переключиться на простой интерфейс", + "symbol": "Символ", + "sync-all-devices": "Синхронизировать все устройства", + "sync-in-progress": "Синхронизация...", + "sync-settings": "Настройки синхронизации", + "sync-synced": "Синхронизируется", + "syncing-devices": "Синхронизация...", + "system": "Системная", + "tabs": "Вкладки", + "tag-was-lost": "Тег потерян", + "tap-card-again": "Приложите карту к задней стороне телефона еще раз", + "terms-of-service": "Условия использования", + "test-networks": "Тестовые сети", + "text-input-disabled": "Подождите...", + "thank-you": "Спасибо", + "this-device": "Это устройство", + "this-device-desc": "Ключи будут зашифрованы и безопасно сохранены на вашем устройстве", + "this-is-you-signing": "Это ваша фраза для подписи транзакций", + "this-will-take-few-seconds": "Это займет несколько секунд", + "three-days": "Три дня", + "three-words-description": "Вы должны увидеть эти 3 слова перед подписанием каждой транзакции", + "three-words-description-2": "Если вы видите другую комбинацию слов, отмените транзакцию и выйдите из системы", + "timeline": "Временная шкала", + "tip-cap": "Максимальный размер чаевых", + "to": "Кому", + "to-block": "Блокировать", + "to-enable-biometric": "Чтобы включить {{bio-type-label}}, необходимо сохранить пароль экрана разблокировки.", + "to-encrypt-enter-password": "Чтобы зашифровать аккаунт, введите свой пароль", + "to-see-this-message": "Чтобы увидеть это сообщение,", + "token": "Токен", + "token-auto-validate-decimals-error": "Неверное количество знаков после запятой для токена {{symbol}} по адресу {{address}} — указано как {{expected}}, но обнаружено как {{actual}}", + "token-auto-validate-name-error": "Неправильное имя токена {{symbol}} по адресу {{address}} — указано как {{expected}}, но обнаружено как {{actual}}", + "token-auto-validate-symbol-error": "Неправильный символ токена {{symbol}} по адресу {{address}} — указан как {{expected}}, но обнаружен как {{actual}}", + "token-details": "Детали токена", + "token-price": "Цена токена", + "topic-name-error": "Используйте только строчные латинские буквы (от a до z), цифры и тире (-). Не используйте коды контактов", + "total-gas": "Общий газ", + "total-members": "Всего участников", + "transaction": "Транзакция", + "transaction-data": "Детали транзакции", + "transaction-declined": "Транзакция отклонена", + "transaction-description": "Транзакция считается завершенной после получения 12 подтверждений в сети.", + "transaction-details": "Детали транзакции", + "transaction-failed": "Не удалось выполнить транзакцию", + "transaction-fee": "Транзакционный сбор", + "transaction-history": "История транзакций", + "transaction-request": "Запрос транзакции", + "transaction-sent": "Транзакция отправлена", + "transaction-signed": "Транзакция успешно подписана", + "transactions": "Транзакции", + "transactions-filter-select-all": "Выбрать все", + "transactions-filter-title": "Фильтр истории", + "transactions-history": "История транзакций", + "transactions-history-empty": "В вашей истории еще нет транзакций", + "transactions-history-loading": "Идет загрузка истории транзакций. Это может занять некоторое время.", + "transactions-load-more": "Загрузить больше", + "transactions-management-enabled": "Управление транзакциями (альфа-версия)", + "transactions-sign": "Подписать", + "transfer-ma-unknown-error-desc-1": "Похоже, что ваш мультиаккаунт не был удален. Возможно, база данных была очищена.", + "transfer-ma-unknown-error-desc-2": "Проверьте свой список аккаунтов и попробуйте еще раз. Если аккаунта нет в списке, перейдите в раздел «Доступ к существующим ключам для восстановления с помощью сид-фразы».", + "transfers-fetching-failure": "Не удалось обновить историю переводов. Проверьте подключение к Интернету и потяните вниз, чтобы повторить попытку.", + "tribute-required-by-multiaccount": "Для начала общения с {{multiaccount-name}} требуются SNT.", + "tribute-state-paid": "«Дань уважения» оплачена", + "tribute-state-pending": "Ожидается оплата «дани уважения»", + "tribute-state-required": "Требуется {{snt-amount}} SNT для оплаты «дани уважения»", + "tribute-to-talk": "Дань уважения", + "tribute-to-talk-add-friends": "Добавьте друзей в список контактов, чтобы разрешить чаты без оплаты «дани уважения».", + "tribute-to-talk-are-you-friends": "Вы друзья?", + "tribute-to-talk-ask-to-be-added": "Попросите добавить вас в список контактов", + "tribute-to-talk-contact-received-your-tribute": " получил(-а) от вас «дань уважения». Теперь вы можете безопасно общаться друг с другом.", + "tribute-to-talk-desc": "Поставьте спам-фильтр на основе криптовалюты, запрашивая SNT от новых людей для начала общения", + "tribute-to-talk-disabled": "Функция «Дань уважения» отключена", + "tribute-to-talk-disabled-note": "С этого момента новые люди смогут начать общаться с вами в чате, не отправляя SNT.", + "tribute-to-talk-enabled": "Функция «Дань уважения» включена.", + "tribute-to-talk-finish-desc": "Теперь вы будете получать запросы на общение только от людей, которые заплатили ", + "tribute-to-talk-learn-more-1": "Время и внимание — это самые ценные ваши активы. Функция «Дань уважения» позволяет установить количество SNT, которое пользователи должны заплатить для начала общения с вами.", + "tribute-to-talk-learn-more-2": "Всем, кого нет в вашем списке контактов, попросят заплатить, и вы сможете ответить им, как только оплата будет получена.", + "tribute-to-talk-learn-more-3": "Вы всегда сможете вернуть деньги отправителю, но чтобы друзья могли беспрепятственно с вами связаться, добавьте их в список контактов.", + "tribute-to-talk-paywall-learn-more-1": "Время и внимание – это самые ценные активы. Функция «Дань уважения» предоставляет возможность связываться с новыми людьми в обмен на оплату в SNT.", + "tribute-to-talk-paywall-learn-more-2": "Чтобы начать чат с пользователем, у которого включена функция «Дань уважения», просто оплатите необходимую сумму в SNT, и вы будете добавлены в качестве контакта.", + "tribute-to-talk-paywall-learn-more-3": "Если вы знаете того, с кем хотите начать общение, поделиться своим профилем за пределами Status, чтобы вас добавили в список контактов бесплатно.", + "tribute-to-talk-pending": "Оплата «дани уважения» ожидает подтверждения", + "tribute-to-talk-pending-note": "Транзакция «дань уважения» ожидает подтверждения в сети. Вы сможете проверить её статус в истории транзакций.", + "tribute-to-talk-removing-note": "Отключение функции «Дань уважения» позволит новым людям начинать общение, не посылая SNT. Для этого нужно осуществить транзакцию.", + "tribute-to-talk-set-snt-amount": "Установите количество SNT, которое новые люди должны заплатить для начала общения с вами", + "tribute-to-talk-signing": "Ожидание подписания транзакции", + "tribute-to-talk-transaction-failed-note": "Сбой транзакции. Параметры функции «Дань уважения» не были изменены", + "tribute-to-talk-tribute-received1": "«Дань уважения» получена. Вы и ", + "tribute-to-talk-tribute-received2": " теперь в списках контактов друг друга и можете безопасно начать общение.", + "tribute-to-talk-you-require-snt": "Новые люди должны будут перевести SNT, чтобы начать общение с вами.", + "try-again": "Попробуйте снова", + "try-keeping-the-card-still": "Держите карту неподвижно", + "turn-nfc-description": "Функция NFC на вашем устройстве отключена. Вы можете активировать ее в настройках.", + "turn-nfc-on": "Для продолжения включите NFC", + "two-minutes": "две минуты", + "tx-fail-description1": "Скорее всего, эта транзакция не получит подтверждения. Установите другую комиссию за использование сети для подписания транзакции.", + "tx-fail-description2": "Скорее всего, эта транзакция не получит подтверждения. Установите другую комиссию за использование сети для подписания транзакции.", + "type": "Тип", + "type-a-message": "Сообщение", + "ulc-enabled": "ULC включен", + "unable-to-fetch": "Не удалось получить историю чата", + "unable-to-read-this-code": "Не удалось прочитать этот код", + "unable-to-send-messages": "Не удается отправлять и получать сообщения", + "unblock": "Разблокировать", + "unblock-contact": "Разблокировать этого пользователя", + "unknown-status-go-error": "Неизвестная ошибка status-go", + "unlimited": "Неограниченный", + "unlock": "Разблокировать", + "unmute": "Включить звук", + "unpair-card": "Отменить привязку карты", + "unpair-card-confirmation": "Эта операция отменит сопряжение карты с текущим устройством. Для авторизации требуется 6-значный код доступа. Вы хотите продолжить?", + "unpair-keycard": "Отменить сопряжение Keycard с этим телефоном", + "unpair-keycard-warning": "Ваш код сопряжения, PUK и PIN остаются неизменными.", + "unpaired-keycard-text": "Keycard, который вы только что использовали, не привязан к этому телефону", + "unpaired-keycard-title": "Похоже, сопряжение с вашей картой было потеряно", + "unpin": "Открепить", + "update": "Обновить", + "update-to-listen-audio": "Обновитесь до последней версии, чтобы прослушать здесь звуковое сообщение!", + "update-to-see-image": "Обновитесь до последней версии, чтобы увидеть здесь красивое изображение!", + "update-to-see-sticker": "Обновитесь до последней версии, чтобы увидеть здесь красивый стикер!", + "updates-to-tos": "Обновление Условий использования", + "updates-to-tos-desc": "Прежде чем продолжить, пожалуйста, ознакомьтесь с Условиями использования и подтвердите, что вы берете на себя полную ответственность за использование приложения.", + "url": "URL-адрес", + "usd-currency": "USD", + "use-as-profile-picture": "Использовать как фото профиля", + "use-valid-contact-code": "Введите или отсканируйте действительный код контакта или имя пользователя", + "validation-amount-invalid-number": "Сумма не является допустимым числом", + "validation-amount-is-too-precise": "Слишком много чисел после запятой. Максимально допустимое количество знаков после запятой — {{decimals}}.", + "verified-community": "✓ Проверенное сообщество", + "version": "Версия приложения", + "view": "Посмотреть", + "view-cryptokitties": "Посмотреть в CryptoKitties", + "view-cryptostrikers": "Посмотреть в CryptoStrikers", + "view-data": "Просмотреть данные", + "view-details": "Посмотреть детали", + "view-etheremon": "Посмотреть в Etheremon", + "view-gitcoin": "Посмотреть в Gitcoin", + "view-on-opensea": "Посмотреть на OpenSea", + "view-profile": "Просмотреть профиль", + "view-public-dashboard": "Посмотреть общедоступную панель управления", + "view-rules": "Посмотреть правила", + "view-signing": "Посмотреть фразу для подписи", + "view-superrare": "Посмотреть в SuperRare", + "waiting-for-wifi": "Wi-Fi не подключен, синхронизация сообщений отключена.", + "waiting-for-wifi-change": "Настройки", + "waiting-to-sign": "Ожидание подписания транзакции...", + "waiting-wi-fi": "В ожидании Wi-Fi...", + "waku-bloom-filter-mode": "Режим фильтра Waku bloom", + "wakuv2-change-nodes": "Вы уверены, что хотите поменять ноды Wakuv2?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "Настройки Waku v2", + "wallet": "Кошелек", + "wallet-address": "Адрес кошелька", + "wallet-asset": "Актив", + "wallet-assets": "Активы", + "wallet-backup-recovery-title": "Сделайте резервную копию вашей сид-фразы", + "wallet-choose-recipient": "Выбрать получателя", + "wallet-collectibles": "Коллекции", + "wallet-connect": "Подключить кошелек", + "wallet-connect-2.0": "Подключить кошелек 2.0", + "wallet-connect-app-connected": "подключено", + "wallet-connect-go-back": "Вернитесь в свой браузер или децентрализованное приложение", + "wallet-connect-proposal-description": "Подключившись, вы позволяете {{name}} получить адрес вашей учетной записи и включить Web3.", + "wallet-connect-proposal-title": "Хочу подключиться к вашему кошельку", + "wallet-insufficient-funds": "Недостаточно средств", + "wallet-insufficient-gas": "Недостаточно ETH, чтобы заплатить за газ", + "wallet-invalid-address": "Недействительный адрес: \n {{data}}", + "wallet-invalid-address-checksum": "Ошибка в адресе: \n {{data}}", + "wallet-invalid-chain-id": "Несоответствующая сеть: \n {{data}}, в то время как текущая цепочка — {{chain}}", + "wallet-key-content": "Шестнадцатеричный адрес из 64 знаков, начинающийся с «0x». Данный тип адресов основывается на блокчейне Ethereum. Это общедоступный адрес вашего аккаунта, который вы можете сообщать другим пользователям, чтобы получать от них средства в криптовалюте, функционирующей на аналогичном блокчейне. Альтернативные наименования: «адрес Ethereum» и «адрес кошелька ERC-20».", + "wallet-key-title": "Адрес аккаунта", + "wallet-manage-accounts": "Управлять аккаунтами", + "wallet-manage-app-connections": "Управление подключениями приложений", + "wallet-manage-assets": "Управлять активами", + "wallet-request": "Запрос", + "wallet-send": "Отправить", + "wallet-send-min-units": "Мин. 21000 единиц", + "wallet-send-min-wei": "Мин. 1 Gwei", + "wallet-settings": "Настройки кошелька", + "wallet-total-value": "Общая сумма", + "wallet-transaction-total-fee": "Итоговая комиссия", + "wants-to-access-profile": "хочет получить доступ к вашему профилю", + "warning": "Предупреждение", + "warning-message": "Извините, мы ограничиваем быструю отправку нескольких сообщений друг за другом для предотвращения спама. Попробуйте снова через пару секунд.", + "warning-sending-to-contract-descr": "Введенный вами адрес является смарт-контрактом. Отправка средств на этот адрес может привести к их потере. Для взаимодействия с DApp откройте его в DApp-браузере Status.", + "watch-only": "Только для просмотра", + "wc-brand-guide": "Руководство по использованию брендинга, такого как торговые марки и логотипы", + "wc-disclaimer": "Отказ от ответственности (включая сторонних поставщиков), гарантии и юридические релизы", + "wc-dispute": "Положения о разрешении споров", + "wc-how-to-use-status-app": "Как использовать приложение Status, включая конфиденциальность и безопасность", + "wc-new-tos-based-on-principles-prefix": "Новые Условия использования, разработанные на основе наших", + "web-view-error": "Не удалось загрузить страницу", + "websites": "Веб-сайты", + "webview-camera-permission-requests": "Запросы на доступ к камере", + "webview-camera-permission-requests-subtitle": "Если эта функция включена, сайты и децентрализованные приложения могут запрашивать доступ к камере устройства.", + "welcome-blank-community-message": "Ваши сообщества появятся здесь.", + "welcome-blank-message": "Ваши чаты появятся здесь. Чтобы создать новые чаты, нажмите кнопку ⊕", + "welcome-community-blank-message": "Ваши каналы появятся здесь. Чтобы создать новый канал, нажмите кнопку ⊕ и выберите «Создать канал».", + "welcome-community-blank-message-edit-chats": "Ваши каналы появятся здесь. Чтобы создать новый канал, вернитесь на экран сообщества, нажмите кнопку ⊕ и выберите «Создать канал».", + "welcome-screen-text": "Настройте кошелек, приглаcите друзей общаться в чатах \nи пользуйтесь популярными dapps!", + "welcome-to-status": "Добро пожаловать в Status!", + "welcome-to-status-description": "Настройте криптокошелек, пригласите друзей в чаты и знакомьтесь с возможностями децентрализованных приложений", + "what-changed": "Изменения", + "what-is-shared": "Что является общим", + "whats-on-your-mind": "О чём вы думаете...", + "whats-trending": "Узнайте, что сейчас в тренде", + "word-count": "Количество слов", + "word-n": "Слово №{{number}}", + "word-n-description": "Чтобы проверить, правильно ли вы создали резервную копию сид-фразы, введите слово № {{number}} выше.", + "words-n": { + "zero": "слов", + "one": "1 слово", + "few": "{{count}} слова", + "many": "{{count}} слов", + "other": "Слов: {{count}}" + }, + "write-down-and-store-securely": "Запишите коды\n и храните их в надежном месте.", + "wrong-address": "Неправильный адрес", + "wrong-card": "Неправильная карта", + "wrong-card-text": "Используемая карта не соответствует выбранным вами ключам", + "wrong-contract": "Неправильный контракт", + "wrong-keycard-text": "Keycard, который вы использовали, не привязан к этому телефону", + "wrong-keycard-title": "Похоже, вы используете \nнеправильный Keycard", + "wrong-password": "Неверный пароль", + "wrong-word": "Неверное слово", + "yes": "Да", + "you": "вы", + "you-already-have-an-asset": "У вас уже есть актив {{value}}", + "you-are-all-set": "Готово!", + "you-are-all-set-description": "При потере телефона вы сможете получить доступ к своим средствам и коду контакта с помощью сид-фразы", + "you-can-change-account": "Вы можете изменить имя и цвет аккаунта по своему усмотрению", + "you-can-choose-preview-websites": "Вы можете выбрать, для каких из следующих сайтов будет работать предварительный просмотр ссылок и изображений в чатах", + "you-can-fetch": "Вы можете получить историю чата", + "you-dont-have-contacts": "У вас пока нет контактов.", + "you-dont-have-contacts-invite-friends": "У вас еще нет контактов.\nПригласите своих друзей для общения.", + "you-dont-have-stickers": "У вас еще нет стикеров", + "you-will-need-this-code": "Вам понадобится этот код, чтобы открывать Status и подписывать транзакции", + "you-will-start-from-scratch": "Вы начнете с начала с новым набором ключей", + "your-card-is-frozen": "Ваш Keycard заморожен. Сбросьте настройки карты для новой активации.", + "your-contact-code": "Предоставление доступа разрешает этому DApp получать ваш код контакта", + "your-data-belongs-to-you": "Если вы потеряете сид-фразу, то уже не сможете получить доступ к данным и средствам на этом аккаунте", + "your-data-belongs-to-you-description": "В случае, если вы потеряете свой ключ или пароль от аккаунта, вы сможете восстановить доступ к нему только с помощью сид-фразы, закрепленной за данным аккаунтом. Никто, кроме вас не должен иметь доступ к вашей сид-фразе. Запишите ее и храните в надежном месте.", + "your-keys": "Ваши ключи", + "your-price-limit": "Ваш ценовой лимит", + "your-recovery-phrase": "Ваша сид-фраза", + "your-recovery-phrase-description": "Это ваша сид-фраза. Она используется для доказательства владения кошельком. Вы увидите ее только один раз! Запишите ее на бумаге и храните в надежном месте. Она понадобится вам, если вы потеряете или переустановите кошелек.", + "your-tip-limit": "Ваш лимит чаевых", + "youre-on-mobile-network": "Вы используете мобильную сеть" +} diff --git a/translations/sl.json b/translations/sl.json new file mode 100644 index 00000000000..4d40f75b139 --- /dev/null +++ b/translations/sl.json @@ -0,0 +1,127 @@ +{ + "confirm": "Potrdi", + "amount": "Vsota", + "members-active": { + "one": "1 član, 1 aktiven", + "other": "{{count}} članov, {{count}} aktivnih", + "zero": "ni članov" + }, + "chat-name": "Ime za klepet", + "phew-here-is-your-passphrase": "*Pfuu* to pa je bilo težko, tukaj je tvoje šifrirno geslo, *zapiši si ga in shrani na varno mesto!* Potreboval/-a ga boš, da obnoviš svoj račun.", + "public-group-topic": "Tema", + "chat-settings": "Nastavitve klepeta", + "offline": "Nedosegljiv/-a", + "invited": "povabil/-a", + "address": "Naslov", + "new-public-group-chat": "Pridruži se javnemu klepetu", + "datetime-hour": { + "one": "ura", + "other": "ur" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "Odstrani", + "add-members": "Dodaj člane", + "done": "Končano", + "close-chat": "Izbriši klepet", + "new-group-chat": "Nov skupinski klepet", + "sign-in": "Prijava", + "datetime-yesterday": "včeraj", + "create-new-account": "Ustvari nov račun", + "datetime-ago": "nazaj", + "contacts": "Stiki", + "got-it": "Razumem", + "active-online": "Dosegljiv/-a", + "password": "Geslo", + "browsing-title": "Brskaj", + "discover": "Odkrivanje", + "browsing-cancel": "Prekliči", + "intro-status": "Klepetaj z mano in nastavi svoj račun ter spremeni svoje nastavitve!", + "name": "Ime", + "show-qr": "Prikaži QR", + "connect": "Poveži", + "edit": "Uredi", + "account-generation-message": "Počakaj sekundo, opraviti moram noro računico, da ustvarim tvoj račun!", + "no-messages": "Ni sporočil", + "passphrase": "Šifrirno geslo", + "recipient": "Prejemnik", + "members-title": "Člani", + "new-group": "Nova skupina", + "phone-e164": "Mednarodno 1", + "settings": "Nastavitve", + "chats": "Klepeti", + "transaction": "Transakcija", + "public-group-status": "Javno", + "image-source-make-photo": "Zajemi", + "start-conversation": "Začni pogovor", + "save": "Shrani", + "sharing-copy-to-clipboard": "Kopiraj v odložišče", + "sync-in-progress": "Sinhronizacija...", + "send-transaction": "Pošlji transakcijo", + "incorrect-code": [ + "str", + "Koda na žalost ni bila pravilna, prosimo, da jo ponovno vneseš" + ], + "image-source-gallery": "Izberi iz galerije", + "sync-synced": "Sinhronizirano", + "status-pending": "V teku", + "datetime-day": { + "one": "dan", + "other": "dni" + }, + "scan-qr": "Skeniraj QR", + "contact-s": { + "one": "stik", + "other": "stiki" + }, + "next": "Naprej", + "recent": "Nedavno", + "status": "Status", + "from": "Od", + "wrong-password": "Napačno geslo", + "in-contacts": "Med stiki", + "sharing-share": "Deli...", + "type-a-message": "Napiši sporočilo...", + "clear-history": "Počisti zgodovino", + "no-contacts": "Zaenkrat še ni stikov", + "datetime-today": "danes", + "web-view-error": "ups, napaka", + "error": "Napaka", + "more": "več", + "cancel": "Prekliči", + "can-not-add-yourself": "Sebe ni mogoče dodati", + "add-to-contacts": "Dodaj med stike", + "available": "Na voljo", + "You": "Ti", + "main-wallet": "Glavna denarnica", + "members": { + "one": "1 član", + "other": "{{count}} članov", + "zero": "ni članov" + }, + "intro-message1": "Dobrodošel/-la v status\nPritisni to sporočilo in nastavi svoje geslo ter začni!", + "new-contact": "Nov stik", + "datetime-second": { + "one": "sekunda", + "other": "sekund" + }, + "recover": "Povrni", + "datetime-minute": { + "one": "minuta", + "other": "minut" + }, + "browsing-open-in-android-web-browser": "Odpri v spletnem brskalniku", + "browsing-open-in-ios-web-browser": "Odpri v spletnem brskalniku", + "edit-profile": "Uredi profil", + "active-unknown": "Neznano", + "public-key": "Javni ključ", + "profile": "Profil", + "none": "Brez", + "removed": "odstranil/-a", + "message": "Sporočilo", + "here-is-your-passphrase": "Tukaj je tvoje geslo, *zapiši si ga in shrani na varno mesto!* Potreboval/-a ga boš, da obnoviš svoj račun.", + "image-source-title": "Fotografija profila", + "left": "preostane", + "to": "Za", + "status-sent": "Poslano", + "data": "Podatki" +} diff --git a/translations/sr_rs_cyrl.json b/translations/sr_rs_cyrl.json new file mode 100644 index 00000000000..e7ce04e8fe8 --- /dev/null +++ b/translations/sr_rs_cyrl.json @@ -0,0 +1,195 @@ +{ + "validation-amount-invalid-number": "Износ није исправан број", + "transaction-details": "Детаљи трансакције", + "confirm": "Потврди", + "description": "Опис", + "amount": "Износ", + "open": "Отвори", + "close-app-title": "Упозорење!", + "members-active": { + "one": "1 члан", + "other": "{{count}} чланови", + "zero": "нема чланова" + }, + "chat-name": "Назив разговора", + "phew-here-is-your-passphrase": "Уф, то је било тешко. Ово је ваша лозинка, *запишите је и чувајте на сигурном!* Требаће вам за обнову налога.", + "public-group-topic": "Тема", + "chat-settings": "Подешавања разговора", + "offline": "Ван мреже", + "invited": "позвани", + "address": "Адреса", + "new-public-group-chat": "Придружите се јавном разговору", + "datetime-hour": { + "one": "сат", + "other": "сати" + }, + "wallet-settings": "Подешавања новчаника", + "datetime-ago-format": "{{ago}} {{number}} {{time-intervals}}", + "close-app-button": "Потврди", + "block": "Блокирај", + "camera-access-error": "Да би сте дозволили приступ камери, молимо вас да у системским подешавањима селектујете Статус > Камера.", + "wallet-invalid-address": "Неисправна адреса: \n {{data}}", + "remove": "Уклони", + "add-members": "Додај чланове", + "yes": "Да", + "dapps": "ÐApps", + "network-settings": "Подешавања мреже", + "photos-access-error": "Да би сте дозволили приступ фотографијама, молимо вас у системским подешаванјима селектујете Статус > Албум.", + "hash": "Hash", + "done": "Урађено", + "close-chat": "Обришите разговор", + "new-group-chat": "Нова група за разговор", + "wallet": "Новчаник", + "wallet-request": "Захтев", + "sign-in": "Улогујте се", + "datetime-yesterday": "јуче", + "create-new-account": "Направи нови налог", + "datetime-ago": "пре", + "contacts": "Контакти", + "got-it": "ОК", + "public-chats": "Јавни разговори", + "not-applicable": "Није примењиво за непотписане трансакције", + "active-online": "На мрежи", + "password": "Лозинка", + "send-request": "Пошаљите захтев", + "paste-json": "Налепи JSON", + "browsing-title": "Претражи", + "transactions-history-empty": "Још увек немате трансакције у историји", + "discover": "Открића", + "browsing-cancel": "Одустани", + "intro-status": "Разговарајте са мном да бисте подесили свој налог.", + "name": "Име", + "gas-price": "Цена гаса", + "validation-amount-is-too-precise": "Износ има превише децимала. Најмања јединица коју можете послати је 1 Wei (1x10^-18 ETH)", + "copy-transaction-hash": "Копирај hash трансакције", + "show-qr": "Прикажи QR code", + "connect": "Повежи", + "edit": "Уреди", + "account-generation-message": "Само тренутак, одрађујем неку жешћу математику око креирања вашег налога!", + "remove-network": "Уклони мрежу", + "no-messages": "Нема порука", + "passphrase": "Тајна фраза", + "recipient": "Прималац", + "members-title": "Чланови", + "new-group": "Нова група", + "phone-e164": "Међународни 1", + "connected": "Повезано", + "rpc-url": "RPC URL", + "settings": "Подешавања", + "specify-rpc-url": "Дефиниши RPC URL", + "gas-limit": "Лимит гаса", + "delete": "Обриши", + "chats": "Разговори", + "transaction-sent": "Трансакција послата", + "transaction": "Трансакција", + "public-group-status": "Јавно", + "image-source-make-photo": "Сликај", + "chat": "Разговор", + "start-conversation": "Започни разговор", + "save": "Сачувај", + "all": "Све", + "confirmations-helper-text": "Молимо вас сачекајте најмање 12 потврда како бисте били сигурни да је ваша трансакција безбедно обрађена", + "sharing-copy-to-clipboard": "Копирај", + "sync-in-progress": "Синхронизација...", + "enter-password": "Унесите лозинку", + "send-transaction": "Пошаљи трансакцију", + "confirmations": "Потврде", + "incorrect-code": [ + "str", + "Жао нам је код је неисправан, молимо вас покушајте поново." + ], + "image-source-gallery": "Изабери из галерије", + "sync-synced": "Синхронизовано", + "currency": "Валута", + "status-pending": "Обрађује се", + "connecting-requires-login": "Повезивање на другу мрежу захтева да се поново улогујете", + "datetime-day": { + "one": "дан", + "other": "дана" + }, + "request-transaction": "Захтевајте трансакцију", + "wallet-send": "Пошаљи", + "scan-qr": "Скенирај QR код", + "contact-s": { + "one": "контакт", + "other": "контаката" + }, + "gas-used": "Искоришћени гас", + "type": "Тип", + "next": "Следећи", + "recent": "Нови статуси", + "open-on-etherscan": "Отвори Etherscan.io", + "share": "Подели", + "status": "Статус", + "from": "Од", + "wrong-password": "Погрешна лозинка", + "in-contacts": "У контактима", + "transactions-sign": "Додели", + "sharing-share": "Подели...", + "type-a-message": "Укуцајте поруку...", + "usd-currency": "USD", + "url": "УРЛ", + "add-network": "Додај мрежу", + "unknown-status-go-error": "Непозната status-go грешка", + "clear-history": "Очисти историју", + "no-contacts": "Још увек без контаката", + "soon": "Ускоро", + "close-app-content": "Ова апликација ће да буде рестартована. Када је поново отворите, биће коришћена изабрана мрежа", + "datetime-today": "данас", + "web-view-error": "упс, грешка", + "error": "Грешка", + "more": "још", + "cancel": "Откажи", + "can-not-add-yourself": "Не можете да додате самог себе", + "transaction-description": "Молимо вас сачекајте најмање 12 потврда да бисте потврдили да је ваша трансакција успешно обрађена", + "add-to-contacts": "Додај у контакте", + "available": "Доступан", + "You": "Ви", + "main-wallet": "Главни новчаник", + "transactions": "Трансакције", + "members": { + "one": "1 члан", + "other": "{{count}} чланова", + "zero": "нема чланова" + }, + "intro-message1": "Добродошли у Статус!\nЗа почетак кликните на ову поруку да поставите своју шифру.", + "eth": "ETH", + "transactions-history": "Историја", + "new-contact": "Нови контакт", + "datetime-second": { + "one": "секунда", + "other": "секунди" + }, + "recover": "Обнова налога", + "wallet-total-value": "Укупна вредност", + "nonce": "Nonce", + "new-network": "Нова мрежа", + "datetime-minute": { + "one": "минут", + "other": "минута" + }, + "browsing-open-in-android-web-browser": "Отвори у интернет претраживачу", + "browsing-open-in-ios-web-browser": "Отвори у интернет претраживачу", + "wallet-insufficient-funds": "Недовољно средстава", + "edit-profile": "Ажурирајте профил", + "active-unknown": "Непознат", + "transaction-failed": "Неуспела трансакција", + "public-key": "Јавни кључ", + "profile": "Профил", + "wallet-choose-recipient": "Изабери примаоца", + "none": "Ништа", + "removed": "уклоњени", + "no": "Нe", + "transactions-filter-select-all": "Изабери све", + "transactions-filter-title": "Филтрирај историју", + "message": "Порука", + "here-is-your-passphrase": "Ово је ваша лозинка, *Запишите је и чувајте на сигурном!* Требаће вам за обнову налога.", + "wallet-assets": "Средства", + "image-source-title": "Профилна слика", + "current-network": "Тренутна мрежа", + "left": "остати", + "to": "Коме", + "data": "Подаци", + "status-sent": "Послато", + "cost-fee": "Цена" +} diff --git a/translations/sr_rs_latn.json b/translations/sr_rs_latn.json new file mode 100644 index 00000000000..5800db81eea --- /dev/null +++ b/translations/sr_rs_latn.json @@ -0,0 +1,192 @@ +{ + "validation-amount-invalid-number": "Iznos nije ispravan broj", + "transaction-details": "Detalji transakcije", + "confirm": "Potvrdi", + "description": "Opis", + "amount": "Iznos", + "open": "Otvori", + "close-app-title": "Upozorenje!", + "members-active": { + "one": "1 član", + "other": "{{count}} članovi", + "zero": "nema članova" + }, + "chat-name": "Naziv razgovora", + "phew-here-is-your-passphrase": "Uf, to je bilo teško. Ovo je vaša lozinka, *zapišite je i čuvajte na sigurnom!* Trebaće vam za obnovu naloga.", + "public-group-topic": "Tema", + "chat-settings": "Podešavanja razgovora", + "offline": "Van mreže", + "invited": "pozvani", + "address": "Adresa", + "new-public-group-chat": "Pridružite se javnom razgovoru", + "datetime-hour": { + "one": "sat", + "other": "sati" + }, + "wallet-settings": "Podešavanja novčanika", + "datetime-ago-format": "{{ago}} {{number}} {{time-intervals}}", + "close-app-button": "Potvrdi", + "block": "Blokiraj", + "camera-access-error": "Da bi ste dozvolili pristup kameri, molimo vas da u sistemskim podešavanjima selektujete Status > Kamera.", + "wallet-invalid-address": "Neispravna adresa: \n {{data}}", + "remove": "Ukloni", + "add-members": "Dodaj članove", + "yes": "Da", + "dapps": "ÐApps", + "network-settings": "Podešavanja mreže", + "photos-access-error": "Da bi ste dozvolili pristup fotografijama, molimo vas u sistemskim podešavanjima selektujete Status > Album.", + "hash": "Hash", + "done": "Urađeno", + "close-chat": "Obrišite razgovor", + "new-group-chat": "Nova grupa za razgovor", + "wallet": "Novčanik", + "wallet-request": "Zahtev", + "sign-in": "Ulogujte se", + "datetime-yesterday": "juče", + "create-new-account": "Napravi novi nalog", + "datetime-ago": "pre", + "contacts": "Kontakti", + "got-it": "OK", + "public-chats": "Javni razgovori", + "not-applicable": "Nije primenjivo za nepotpisane transakcije", + "active-online": "Na mreži", + "password": "Lozinka", + "send-request": "Pošaljite zahtev", + "paste-json": "Nalepi JSON", + "browsing-title": "Pretraži", + "transactions-history-empty": "Još uvek nemate transakcije u istoriji", + "discover": "Otkrića", + "browsing-cancel": "Odustani", + "intro-status": "Razgovarajte sa mnom da biste podesili svoj nalog.", + "name": "Ime", + "gas-price": "Cena gasa", + "validation-amount-is-too-precise": "Iznos ima previše decimala. Najmanja jedinica koju možete poslati je 1 Wei (1x10^-18 ETH)", + "copy-transaction-hash": "Kopiraj hash transakcije", + "show-qr": "Prikaži QR code", + "connect": "Poveži", + "edit": "Uredi", + "account-generation-message": "Samo trenutak, odrađujem neku žešću matematiku oko kreiranja vašeg naloga!", + "remove-network": "Ukloni mrežu", + "no-messages": "Nema poruka", + "passphrase": "Tajna fraza", + "recipient": "Primalac", + "members-title": "Članovi", + "new-group": "Nova grupa", + "phone-e164": "Međunarodni 1", + "connected": "Povezano", + "rpc-url": "RPC URL", + "settings": "Podešavanja", + "specify-rpc-url": "Definiši RPC URL", + "gas-limit": "Limit gasa", + "delete": "Obriši", + "chats": "Razgovori", + "transaction-sent": "Transakcija poslata", + "transaction": "Transakcija", + "public-group-status": "Javno", + "image-source-make-photo": "Slikaj", + "chat": "Razgovor", + "start-conversation": "Započni razgovor", + "save": "Sačuvaj", + "all": "Sve", + "confirmations-helper-text": "Molimo vas sačekajte najmanje 12 potvrda kako biste bili sigurni da je vaša transakcija bezbedno obrađena", + "sharing-copy-to-clipboard": "Kopiraj", + "sync-in-progress": "Sinhronizacija...", + "enter-password": "Unesite lozinku", + "send-transaction": "Pošalji transakciju", + "confirmations": "Potvrde", + "incorrect-code": null, + "image-source-gallery": "Izaberi iz galerije", + "sync-synced": "Sinhronizovano", + "currency": "Valuta", + "status-pending": "Obrađuje se", + "connecting-requires-login": "Povezivanje na drugu mrežu zahteva da se ponovo ulogujete", + "datetime-day": { + "one": "dan", + "other": "dana" + }, + "request-transaction": "Zahtevajte transakciju", + "wallet-send": "Pošalji", + "scan-qr": "Skeniraj QR kod", + "contact-s": { + "one": "kontakt", + "other": "kontakata" + }, + "gas-used": "Iskorišćeni gas", + "type": "Tip", + "next": "Sledeći", + "recent": "Novi statusi", + "open-on-etherscan": "Otvori Etherscan.io", + "share": "Podeli", + "status": "Status", + "from": "Od", + "wrong-password": "Pogrešna lozinka", + "in-contacts": "U kontaktima", + "transactions-sign": "Dodeli", + "sharing-share": "Podeli...", + "type-a-message": "Ukucajte poruku...", + "usd-currency": "USD", + "url": "URL", + "add-network": "Dodaj mrežu", + "unknown-status-go-error": "Nepoznata status-go greška", + "clear-history": "Očisti istoriju", + "no-contacts": "Još uvek bez kontakata", + "soon": "Uskoro", + "close-app-content": "Ova aplikacija će da bude restartovana. Kada je ponovo otvorite, biće korišćena izabrana mreža", + "datetime-today": "danas", + "web-view-error": "ups, greška", + "error": "Greška", + "more": "još", + "cancel": "Otkaži", + "can-not-add-yourself": "Ne možete da dodate samog sebe", + "transaction-description": "Molimo vas sačekajte najmanje 12 potvrda da biste potvrdili da je vaša transakcija uspešno obrađena", + "add-to-contacts": "Dodaj u kontakte", + "available": "Dostupan", + "You": "Vi", + "main-wallet": "Glavni novčanik", + "transactions": "Transakcije", + "members": { + "one": "1 član", + "other": "{{count}} članova", + "zero": "nema članova" + }, + "intro-message1": "Dobrodošli u Status!\nZa početak kliknite na ovu poruku da postavite svoju šifru.", + "eth": "ETH", + "transactions-history": "Istorija", + "new-contact": "Novi kontakt", + "datetime-second": { + "one": "sekunda", + "other": "sekundi" + }, + "recover": "Obnova naloga", + "wallet-total-value": "Ukupna vrednost", + "nonce": "Nonce", + "new-network": "Nova mreža", + "datetime-minute": { + "one": "minut", + "other": "minuta" + }, + "browsing-open-in-android-web-browser": "Otvori u internet pretraživaču", + "browsing-open-in-ios-web-browser": "Otvori u internet pretraživaču", + "wallet-insufficient-funds": "Nedovoljno sredstava", + "edit-profile": "Ažurirajte profil", + "active-unknown": "Nepoznat", + "transaction-failed": "Neuspela transakcija", + "public-key": "Javni ključ", + "profile": "Profil", + "wallet-choose-recipient": "Izaberi primaoca", + "none": "Ništa", + "removed": "uklonjeni", + "no": "Ne", + "transactions-filter-select-all": "Izaberi sve", + "transactions-filter-title": "Filtriraj istoriju", + "message": "Poruka", + "here-is-your-passphrase": "Ovo je vaša lozinka, *Zapišite je i čuvajte na sigurnom!* Trebaće vam za obnovu naloga.", + "wallet-assets": "Sredstva", + "image-source-title": "Profilna slika", + "current-network": "Trenutna mreža", + "left": "ostati", + "to": "Kome", + "data": "Podaci", + "status-sent": "Poslato", + "cost-fee": "Cena" +} diff --git a/translations/sv.json b/translations/sv.json new file mode 100644 index 00000000000..095414e18d4 --- /dev/null +++ b/translations/sv.json @@ -0,0 +1,200 @@ +{ + "validation-amount-invalid-number": "Beloppet är inte ett giltigt tal", + "transaction-details": "Transaktiondetaljer", + "confirm": "Bekräfta", + "description": "Beskrivning", + "amount": "Belopp", + "open": "Öppna", + "close-app-title": "Varning!", + "members-active": { + "one": "1 medlem", + "other": "{{count}} medlemmar", + "zero": "inga medlemmar" + }, + "chat-name": "Chattnamn", + "phew-here-is-your-passphrase": "*Pust* det var svårt, här är din lösenordsfras, *skriv ner den och förvara den säkert!* Du kommer att behöva den för att återställa ditt konto.", + "public-group-topic": "Rubrik", + "chat-settings": "Chattinställningar", + "offline": "Offline", + "invited": "inbjudna", + "address": "Adress", + "new-public-group-chat": "Gå med i offentlig chatt", + "datetime-hour": { + "one": "timma", + "other": "timmar" + }, + "wallet-settings": "Plånboksinställningar", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "close-app-button": "Bekräfta", + "block": "Block", + "camera-access-error": "För att bevilja nödvändinga kamerarättigheter, gå till systeminställningarna och kontrollera att Status > Kamera är vald.", + "wallet-invalid-address": "Ogiltig address: \n {{data}}", + "remove": "Ta bort", + "add-members": "Lägg till medlemmar", + "yes": "Ja", + "dapps": "ÐApps", + "network-settings": "Nätverksinställningar", + "photos-access-error": "För att bevilja nödvändinga fotorättigheter, gå till systeminställiningarna och kontrollera att Status > Foton är vald.", + "hash": "Hash", + "done": "Klar", + "close-chat": "Ta bort chatt", + "new-group-chat": "Ny gruppchatt", + "wallet": "Plånbok", + "wallet-request": "Begär", + "sign-in": "Logga in", + "datetime-yesterday": "igår", + "create-new-account": "Skapa nytt konto", + "datetime-ago": "sedan", + "contacts": "Kontakter", + "got-it": "OK", + "public-chats": "Offentliga chattar", + "not-applicable": "Otillämplig på ej underskrivna transaktioner", + "active-online": "Online", + "password": "Lösenord", + "send-request": "Skicka begäran", + "paste-json": "Klistra in JSON", + "browsing-title": "Webbläsning", + "transactions-history-empty": "Inga transaktioner i din historik ännu", + "discover": "Upptäck", + "browsing-cancel": "Avbryt", + "intro-status": "Chatta med mig för att konfigurera ditt konto och ändra dina inställningar!", + "name": "Namn", + "gas-price": "Gas pris", + "validation-amount-is-too-precise": "Summan är för precis Den minsta summan du kan skicka är 1 Wei (1x10^-18 ETH)", + "pending": "Pågående", + "copy-transaction-hash": "Kopiera transaktionshash", + "show-qr": "Visa QR kod", + "connect": "Anslut", + "edit": "Redigera", + "account-generation-message": "Vänta ett ögonblick, jag måste göra lite galna beräkningar för att generera ditt konto!", + "remove-network": "Ta bort nätverk", + "no-messages": "Inga meddelanden", + "passphrase": "Lösenordsfras", + "recipient": "Mottagare", + "members-title": "Medlemmar", + "new-group": "Ny grupp", + "phone-e164": "Internationell 1", + "connected": "Ansluten", + "rpc-url": "RPC URL", + "settings": "Inställningar", + "specify-rpc-url": "Ange RPC URL", + "gas-limit": "Gas begränsning", + "delete": "Ta bort", + "chats": "Chattar", + "transaction-sent": "Transaktionen skickad", + "transaction": "Transaktion", + "public-group-status": "Offentlig", + "image-source-make-photo": "Fånga", + "chat": "Chatta", + "start-conversation": "Börja en konversation", + "save": "Spara", + "all": "Alla", + "confirmations-helper-text": "Var god och vänta på minst 12 bekräftelser för att försäkra dig om att transaktionen gått igenom", + "sharing-copy-to-clipboard": "Kopiera till urklipp", + "sync-in-progress": "Synkroniserar...", + "enter-password": "Ge lösenord", + "send-transaction": "Sänd en transaktion", + "confirmations": "Bekräftelser", + "incorrect-code": [ + "str", + "Tyvärr var koden felaktig, var god ange den igen" + ], + "image-source-gallery": "Välj från galleri", + "sync-synced": "Synkroniserad", + "currency": "Valuta", + "status-pending": "Avvaktan", + "connecting-requires-login": "Du måste logga in för att kunna ansluta till ett annat nätverk", + "datetime-day": { + "one": "dag", + "other": "dagar" + }, + "request-transaction": "Begär transaktion", + "wallet-send": "Sänd", + "scan-qr": "Skanna QR kod", + "contact-s": { + "one": "kontakt", + "other": "kontakter" + }, + "gas-used": "Gas använt", + "type": "Typ", + "next": "Nästa", + "recent": "Senaste", + "open-on-etherscan": "Öppna på Etherscan.io", + "share": "Dela", + "status": "Status", + "from": "Från", + "wrong-password": "Fel lösenord", + "in-contacts": "I kontakterna", + "transactions-sign": "Skriv under", + "sharing-share": "Dela...", + "type-a-message": "Skriv ett meddelande...", + "usd-currency": "USD", + "url": "URL", + "add-network": "Lägg till nätverk", + "unknown-status-go-error": "Okänt status-go fel", + "clear-history": "Rensa historik", + "wallet-manage-assets": "Hantera tillgångar", + "no-contacts": "Inga kontakter ännu", + "soon": "Snart", + "close-app-content": "Denna app kommer att avslutas. När du öppnar den på nytt kommer det valda nätverket att användas", + "datetime-today": "idag", + "web-view-error": "hoppsan, fel", + "error": "Ett fel uppstod", + "more": "mer", + "cancel": "Avbryt", + "can-not-add-yourself": "Du kan inte lägga till dig själv", + "transaction-description": "Var god och vänta på minst 12 bekräftelser för att försäkra dig om att transaktionen gått igenom", + "add-to-contacts": "Lägg till i kontakter", + "available": "Tillgänglig", + "You": "Du", + "main-wallet": "Huvudplånbok", + "transactions": "Transaktioner", + "members": { + "one": "1 medlem", + "other": "{{count}} medlemmar", + "zero": "inga medlemmar" + }, + "intro-message1": "Välkommen till Status\nTryck på detta meddelande för att ange ett lösenord och komma igång!", + "eth": "ETH", + "transactions-history": "Historik", + "new-contact": "Ny kontakt", + "datetime-second": { + "one": "sekund", + "other": "sekunder" + }, + "recover": "Återställ", + "wallet-total-value": "Totalt värde", + "nonce": "Nonce", + "new-network": "Nytt nätverk", + "datetime-minute": { + "one": "minut", + "other": "minuter" + }, + "browsing-open-in-android-web-browser": "Öppna in webbläsare", + "browsing-open-in-ios-web-browser": "Öppna in webbläsare", + "wallet-insufficient-funds": "Otillräckligt kapital", + "edit-profile": "Redigera profil", + "active-unknown": "Okänd", + "wallet-invalid-chain-id": "Felaktigt nätverk: \n {{data}}", + "transaction-failed": "Transaktionen misslyckades", + "public-key": "Offentlig nyckel", + "incoming": "Inkommande", + "profile": "Profil", + "wallet-choose-recipient": "Välj mottagare", + "none": "Inga", + "removed": "borttagna", + "no": "Nej", + "transactions-filter-select-all": "Välj alla", + "transactions-filter-title": "Filtrera historik", + "message": "Meddelande", + "here-is-your-passphrase": "Här är din lösenordsfras, *skriv ner den och förvara den säkert!* Du kommer att behöva den för att återställa ditt konto.", + "wallet-assets": "Tillgångar", + "image-source-title": "Profilbild", + "current-network": "Nuvarande nätverk", + "outgoing": "Utgående", + "left": "lämnade", + "to": "Till", + "data": "Data", + "status-sent": "Skickad", + "cost-fee": "Kostnad/Avgift" +} diff --git a/translations/sw.json b/translations/sw.json new file mode 100644 index 00000000000..180a6eca47c --- /dev/null +++ b/translations/sw.json @@ -0,0 +1,95 @@ +{ + "amount": "Kiasi", + "members-active": { + "one": "Mwanachama 1, Aliyewajibika 1", + "other": "{{count}} wanachama, {{count}} waliowajibika", + "zero": "hakuna wanachama" + }, + "chat-name": "Jina la gumzo", + "phew-here-is-your-passphrase": "*Phew* hiyo ilikuwa ngumu, hapa ni kaulisiri yako, *iandike na uiweke salama!* Utaihitaji kwa ajili ya kufufua akaunti yako.", + "chat-settings": "Mipangilio ya gumzo", + "offline": "Nje ya mtandao", + "invited": "ulialikwa", + "address": "Anwani", + "datetime-hour": { + "one": "saa", + "other": "masaa" + }, + "remove": "Ondoa", + "add-members": "Ongeza wanachama", + "done": "Imefanyika", + "new-group-chat": "Gumzo mpya ya kikundi", + "datetime-yesterday": "jana", + "datetime-ago": "iliyopita", + "contacts": "Mawasiliano", + "active-online": "Mtandaoni", + "password": "Nenosiri", + "discover": "Ugunduzi", + "intro-status": "Ongea nami kuanzisha akaunti yako na kubadilisha mipangilio yako!", + "name": "Jina", + "show-qr": "Onyesha QR", + "connect": "Unganisha", + "edit": "Hariri", + "account-generation-message": "Nipe sekunde, naenda kufanya baadhi ya hisabati kutengeneza akaunti yako!", + "no-messages": "Hakuna ujumbe", + "passphrase": "Kaulisiri", + "recipient": "Mpokeaji", + "members-title": "Wanachama", + "settings": "Mipangilio", + "chats": "Gumzo", + "image-source-make-photo": "Chukua picha", + "save": "Hifadhi", + "sharing-copy-to-clipboard": "Kopiera", + "sync-in-progress": "Kulandanisha...", + "incorrect-code": [ + "str", + "Samahani kificho hakikuwa sahihi, tafadhali ingiza tena" + ], + "image-source-gallery": "Chagua kutoka nyumba ya sanaa", + "sync-synced": "Katika ulandanishaji", + "status-pending": "Inasubiri", + "datetime-day": { + "one": "siku", + "other": "siku" + }, + "scan-qr": "Piga picha QR", + "recent": "Hivi karibuni", + "status": "Hali", + "wrong-password": "Nenosiri sio halali", + "sharing-share": "Dela...", + "clear-history": "Futa historia", + "no-contacts": "Bado hakuna mawasiliano", + "datetime-today": "leo", + "web-view-error": "nadhani, hitilafu", + "can-not-add-yourself": "Huwezi kujiongeza mwenyewe", + "add-to-contacts": "Ongeza kwa mawasiliano", + "available": "Wanapatikana", + "You": "Wewe", + "main-wallet": "Mkoba Mkuu", + "members": { + "one": "Mwanachama 1", + "other": "{{count}} wanachama", + "zero": "hakuna wanachama" + }, + "intro-message1": "Karibu kwa Hali na Ubofye ujumbe huu ili kuweka nenosiri lako na uanze!", + "new-contact": "Mawasiliano mapya", + "datetime-second": { + "one": "sekunde", + "other": "sekunde" + }, + "recover": "Okoa", + "datetime-minute": { + "one": "dakika", + "other": "dakika" + }, + "active-unknown": "Hawajulikani", + "public-key": "Ufunguo wa Umma", + "profile": "Profaili", + "none": "Hakuna", + "removed": "uliondolewa", + "message": "Ujumbe", + "here-is-your-passphrase": "Hapa ni kaulisiri yako, *iandike na uiweke salama!* Utaihitaji kwa ajili ya kufufua akaunti yako.", + "image-source-title": "Picha ya profaili", + "status-sent": "Tuma", + "left": "uliondoka" +} diff --git a/translations/th.json b/translations/th.json new file mode 100644 index 00000000000..00d577c0d8f --- /dev/null +++ b/translations/th.json @@ -0,0 +1,108 @@ +{ + "accept-and-share-address": "ยอมรับและแชร์ Address", + "account-exists-title": "บัญชีมีอยู่แล้ว", + "add-private-key-account": "เพิ่มบัญชีจาก Private key", + "add-seed-account": "เพิ่มบัญชีด้วย Seed phrase", + "address-received": "Address ผู้รับ", + "balance": "คงเหลือ", + "biometric-disable-bioauth": "ปิดการใช้งาน {{bio-type-label}}", + "biometric-disable-password-title": "ปิดใช้งานการบันทึกรหัสผ่าน", + "cant-report-bug": "ไม่สามารถรายงานข้อผิดพลาดได้", + "check-on-etherscan": "ตรวจสอบใน etherscan", + "clear-all": "ลบทั้งหมด", + "decimals": "ทศนิยม", + "decline": "ยกเลิก", + "default": "ค่าเริ่มต้น", + "delete-account": "สร้างบัญชี", + "delete-and-leave-group": "ลบและออกจากกลุ่ม", + "delete-my-account": "ลบบัญชีของฉัน", + "dev-mode-settings": "การตั้งค่าโหมดการพัฒนา", + "ens-10-SNT": "10 SNT", + "ens-add-username": "เพิ่มชื่อผู้ใช้", + "ens-custom-username-taken": "ชื่อผู้ใช้ไม่ได้เป็นของคุณ :(", + "ens-deposit": "เงินฝาก", + "ens-got-it": "ตกลงเข้าใจแล้ว", + "ens-name-title": "ชื่อ ENS", + "ens-register": "ลงทะเบียน", + "ens-registration-failed-title": "การทำธุรกรรมล้มเหลว", + "ens-remove-username": "ลบชื่อผู้ใช้", + "ens-saved-title": "เพิ่มชื่อผู้ใช้แล้ว", + "ens-test-message": "เฮ้", + "ens-username-owned": "✓ชื่อผู้ใช้เป็นของคุณอยู่แล้ว", + "ens-username-taken": "ชื่อผู้ใช้ถูกใช้ไปแล้ว :(", + "ens-usernames": "ชื่อผู้ใช้ ENS", + "ens-your-username": "ชื่อผู้ใช้ของคุณ", + "ens-your-your-name": "ENS ของคุณ", + "enter-watch-account-address": "สแกน QR code\nหรือ\nป้อน Address เพิ่มดู", + "enter-your-password": "ป้อนรหัสผ่านของคุณ", + "export-account": "ส่งออกบัญชี", + "free": "↓ฟรี", + "generate-an-account": "สร้างบัญชี", + "history": "ประวัติ", + "incoming-transaction": "ธุรกรรมขาเข้า", + "intro-privacy-policy-note2": "นโยบายความเป็นส่วนตัว", + "intro-title3": "Decentralized แอป", + "intro-wizard-text4": "รักษาความปลอดภัยและเข้ารหัสคีย์ของคุณ", + "intro-wizard-text6": "Status จะแจ้งให้คุณทราบเกี่ยวกับข้อความใหม่ คุณสามารถแก้ไขการตั้งค่าการแจ้งเตือนภายหลังในการตั้งค่า", + "intro-wizard-title-alt4": "สร้างรหัสผ่าน", + "intro-wizard-title-alt5": "ยืนยันรหัสผ่านของคุณ", + "intro-wizard-title1": "รับคีย์ของคุณ", + "intro-wizard-title2": "เลือกชื่อแชท", + "intro-wizard-title4": "สร้างรหัสผ่าน 6 หลัก", + "intro-wizard-title5": "ยืนยันรหัสผ่าน", + "intro-wizard-title6": "เปิดใช้งานการแจ้งเตือน", + "key": "คีย์", + "keycard-awaiting-title": "กำลังมองหา...", + "keycard-connected-title": "เชื่อมต่อ", + "keycard-error-description": "เชื่อมต่อบัตรอีกครั้งเพื่อดำเนินการต่อ", + "keycard-error-title": "การเชื่อมต่อล้มเหลว", + "keycard-onboarding-intro-header": "เก็บคีย์ของคุณไว้ใน Keycard", + "keycard-onboarding-start-step1": "สร้างรหัสผ่าน 6 หลัก", + "keycard-processing-title": "กำลังประมวลผล...", + "keycard-success-title": "สำเร็จ", + "language": "ภาษา", + "learn-more-about-keycard": "เรียนรู้เพิ่มเติมเกี่ยวกับ Keycard", + "les-ulc": "LES\/ULC", + "load-messages-before": "ก่อน {{date}}", + "mail-should-be-configured": "ควรกำหนดค่าเมลไคลเอ็นต์", + "mark-all-read": "ทำเครื่องหมายอ่านทั้งหมด", + "mobile-network-go-to-settings": "ไปยังการตั้งค่า", + "mobile-network-sheet-settings": "การตั้งค่า", + "mobile-network-start-syncing": "เริ่มการซิงค์", + "mobile-network-stop-syncing": "หยุดการซิงค์", + "node-version": "เวอร์ชันโหนด", + "ok-save-pass": "ตกลงบันทึกรหัสผ่าน", + "open-nfc-settings": "เปิดการตั้งค่า NFC", + "outgoing-transaction": "ธุรกรรมขาออก", + "paste": "วาง", + "privacy": "ความเป็นส่วนตัว", + "private-key": "Private key", + "processing": "รอสักครู่", + "quiet-days": "{{quiet-days}} วัน", + "quiet-hours": "{{quiet-hours}} ชั่วโมง", + "retry": "ลองใหม่", + "security": "ความปลอดภัย", + "select": "เลือก", + "select-account": "เลือกบัญชี", + "select-account-first": "เลือกบัญชีก่อน", + "set-currency": "ตั้งค่าสกุลเงินเริ่มต้น", + "share-address": "แชร์ Address", + "share-profile": "แชร์โปรไฟล์", + "shared": "แชร์", + "sign-and-send": "ลงชื่อและส่ง", + "sign-out": "ออกจากระบบ", + "sign-with": "ลงชื่อด้วย", + "submit": "ตกลง", + "symbol": "ตัวย่อ", + "sync-settings": "การตั้งค่าการซิงค์", + "this-device": "อุปกรณ์นี้", + "transactions-load-more": "โหลดเพิ่มเติม", + "turn-nfc-description": "NFC ปิดการใช้งานอยู่ คุณสามารถเปิดใช้งานได้ในการตั้งค่า", + "ulc-enabled": "ULC ที่เปิดใช้งาน", + "unlock": "ปลดล็อค", + "user-not-found": "ไม่พบชื่อผู้ใช้", + "waiting-for-wifi-change": "เปลี่ยน", + "watch-only": "ดูอย่างเดียว", + "wrong-address": "Address ไม่ถูกต้อง", + "your-keys": "คีย์ของคุณ" +} diff --git a/translations/tl.json b/translations/tl.json new file mode 100644 index 00000000000..d3d882c2cde --- /dev/null +++ b/translations/tl.json @@ -0,0 +1,1528 @@ +{ + "about-app": "Tungkol sa", + "about-key-storage-content": "Ang Status ay hindi kailanman mai-access ang iyong pribadong key. Siguraduhing i-backup ang iyong seen phrase. Kung nawala mo ang iyong telepono ito ang tanging paraan upang ma-access ang iyong mga keys.", + "about-key-storage-title": "Tungkol sa pangunahing imbakan", + "about-names-content": "Walang sinuman ang maaaring magpanggap na ikaw! Hindi ka nagpapakilala sa pamamagitan ng default at hindi mo kailangang ihayag ang iyong tunay na pangalan. Maaari kang magrehistro ng isang pasadyang pangalan para sa isang maliit na bayad.", + "about-names-title": "Hindi mababago ang mga pangalan", + "accept": "Tanggapin", + "accept-and-share-address": "Tanggapin at ibahagi ang address", + "access-existing-keys": "I-access ang umiiral na mga key", + "access-key": "I-access ang key", + "account-added": "Idinagdag ang account", + "account-color": "Kulay ng account", + "account-content": "Maaari mong ihambing ang mga account sa Status at sa bank accounts. Tulad ng isang account sa bangko, isang account ay karaniwang mayroong address at isang balanse; Ginagamit mo ang account na ito upang mag-transact sa Ethereum. Maaari kang magkaroon ng maraming mga account sa iyong pitaka o wallet. Lahat ng na-access sa pamamagitan ng pag-unlock ng Status.", + "account-exists-title": "Mayroon nang account", + "account-name": "Pangalan ng account", + "account-settings": "Mga setting ng account", + "account-title": "Account", + "accounts": "Accounts", + "actions": "Mga kilos", + "active-online": "Online", + "active-unknown": "Hindi kilala", + "add": "Idagdag", + "add-a-watch-account": "Magdagdag ng isang address na panonood lamang", + "add-account": "Magdagdag ng account", + "add-account-description": "Maaari kang mag-import ng anumang uri ng Ethereum account para ma-idagdag ito para sa iyong Status Wallet", + "add-account-incorrect-password": "Tila hindi tama ang password. Ilagay ang password na iyong ginagamit para mabuksan ang app.", + "add-an-account": "Magdagdag ng isang account", + "add-bootnode": "Idagdag ang bootnode", + "add-contact": "Magdagdag ng contact", + "add-custom-token": "Magdagdag ng custom token", + "add-favourite": "Magdagdag ng paborito", + "add-mailserver": "Magdagdag ng Status node", + "add-members": "Magdagdag ng mga miyembro", + "add-network": "Magdagdag ng network", + "add-new-contact": "Magdagdag ng bagong contact", + "add-nickname": "Magdagdag ng palayaw (opsyonal)", + "add-private-key-account": "Magdagdag ng account mula sa pribadong key", + "add-seed-account": "Magdagdag ng account para sa seed phrase", + "add-to-contacts": "Idagdag sa mga contact", + "add-to-contacts-text": "Sa pamamagitan ng pagdaragdag ng isang user sa iyong listahan ng contact, ibinahagi mo ang iyong wallet address", + "add-to-favourites": "Idagdag sa mga Paborito", + "add-watch-account": "Magdagdag ng isang account na panonood lamang", + "address": "Address", + "address-or-ens-name": "Address o pangalan ng ENS", + "address-received": "Natanggap ang address", + "address-request-accepted": "Tinanggap ang kahilingan sa address", + "address-requested": "Hiniling ang address", + "advanced": "Advanced", + "advanced-settings": "Mga advanced na setting", + "advertiser-description": "Natuklasan mo ang Status salamat sa isang kasosyo. Naisip mo ba kung susuriin ng Status ang iyong IP address nang isang beses para makakuha sila ng gantimpala? Ang impormasyong ito ay hindi gagamitin para sa anumang bagay at ito ay lubos na aalisin pagkatapos ng 7 araw.", + "advertiser-starter-pack-accept": "Tanggapin", + "advertiser-starter-pack-decline": "Tanggihan", + "advertiser-starter-pack-description": "Narito ang ilang mga crypto upang makapagsimula ka! Gamitin ito upang makakuha ng mga sticker, isang pangalan ng ENS at subukan ang mga dapps", + "advertiser-starter-pack-title": "Starter Pack", + "advertiser-title": "Default sa privacy", + "agree-by-continuing": "Sa pamamagitan ng pagpapatuloy sumasang-ayon ka\nsa aming", + "all": "Lahat", + "allow": "Payagan", + "allow-mention-notifications": "Ipakita @ banggitin", + "allowing-authorizes-this-dapp": "Pinapayagan ang nagpapahintulot sa DApp na ito na makuha ang iyong address ng pitaka at paganahin ang Web3", + "already-have-asset": "Mayroon ka nang asset na ito", + "amount": "Halaga", + "appearance": "Hitsura", + "apply": "Mag-apply", + "are-you-sure-description": "Hindi mo makikita ang kabuuang seed phrase muli", + "are-you-sure-to-cancel": "Sigurado ka bang gusto mong kanselahin?", + "are-you-sure?": "Sigurado ka ba?", + "ask-in-status": "Magtanong ng isang katanungan o mag-ulat ng isang bug", + "at": "sa", + "attribution-received": "{{attrib}} mula sa {{max}} bonus na natanggap", + "audio": "Audio", + "audio-recorder": "Recorder", + "audio-recorder-error": "Error sa recorder", + "audio-recorder-max-ms-reached": "Naabot ang maximum na oras ng pag-record", + "audio-recorder-permissions-error": "Kailangan mong magbigay ng pahintulot upang magpadala ng mga audio message", + "authorize": "Awtorisado", + "available": "Available", + "available-participants": { + "zero": "wala", + "one": "isa", + "two": "dalawa", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "back": "Balik", + "back-up": "I-back up", + "back-up-seed-phrase": "Back up ng seed phrase", + "back-up-your-seed-phrase": "I-back up ang iyong seed phrase", + "backup-recovery-phrase": "Back up seed phrase", + "balance": "Balanse", + "begin-set-up": "Simulan ang pag-setup", + "biometric-auth-android-sensor-desc": "Pindutin ang sensor", + "biometric-auth-android-sensor-error-desc": "Nabigo", + "biometric-auth-android-title": "Kinakailangan ang pagpapatunay", + "biometric-auth-confirm-logout": "Relogin", + "biometric-auth-confirm-message": "Kinakailangan ang Biometric authentication para magpatuloy, kung hindi pwede mangyaring i-unlock ang iyong keys gamit ang iyong password or passcode", + "biometric-auth-confirm-title": "Dapat mong patunayan!", + "biometric-auth-confirm-try-again": "Subukan muli", + "biometric-auth-error": "Hindi ma-perform ang pagpapatunay ng biometric ({{code}})", + "biometric-auth-login-error-title": "Error sa authentication ng biometric", + "biometric-auth-login-ios-fallback-label": "Ilagay ang password", + "biometric-auth-reason-login": "Mag-login sa Status", + "biometric-auth-reason-verify": "Patunayan ang pagpapatunay", + "biometric-disable-bioauth": "hindi paganahin{{bio-type-label}}", + "biometric-disable-password-description": "Kung hindi mo paganahin ito, magagawa mo rin", + "biometric-disable-password-title": "Huwag paganahin ang pag-save ng password", + "biometric-enable": "Kung hindi mo nais na ipasok ang iyong password sa bawat oras upang ma-access ang app, paganahin{{bio-type-label}} mag sign-in", + "biometric-enable-button": "Paganahin{{bio-type-label}}", + "biometric-enable-keycard": "Kung ayaw mong gamitin ang iyong Keycard sa bawat oras na ma-access ang app, paganahin{{bio-type-label}}mag-sign in", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Fingerprint", + "biometric-secure-with": "Secure sa{{bio-type-label}}", + "biometric-touchid": "Touch ID", + "blank-keycard-text": "Maaari kang magpatuloy sa iyong keycard kapag nabuo mo ang iyong mga keys at pangalan", + "blank-keycard-title": "Mukhang naka-tap ka ng\nisang blangkong keycard", + "block": "Block", + "block-contact": "I-block ang gumagamit na ito", + "block-contact-details": "Ang pag-block ay tatanggalin ang mga naunang mensahe ng gumagamit na ito at pipigilan ang mga bago na maabot sa iyo.", + "blocked-users": "Na-block ang mga gumagamit", + "bootnode-address": "Bootnode address", + "bootnode-details": "Mga detalye sa Bootnode", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes enabled", + "bootnodes-settings": "Bootnodes settings", + "browsed-websites": "Ang kasaysayan ng browser ay lilitaw dito", + "browser": "Browser", + "browser-not-secure": "Hindi secure ang koneksyon! Huwag mag-sign ng transaksyon o magpadala ng personal na data sa site na ito.", + "browser-secure": "Ang koneksyon ay ligtas. Siguraduhin na talagang pinagkakatiwalaan mo ang site na ito bago mag-sign ng mga transaksyon o pagpasok ng personal na data.", + "browsers": "Mga Browser", + "browsing-cancel": "Pagkansela", + "browsing-open-in-android-web-browser": "Buksan sa Android", + "browsing-open-in-ios-web-browser": "Buksan sa iOS", + "browsing-open-in-status": "Buksan ang Status", + "browsing-site-blocked-description1": "Nakita namin ang mga potensyal na nakakahamak na aktibidad mula sa address na ito. Upang maprotektahan ka at ang iyong pitaka, pinipigilan namin ang karagdagang pag-navigate.\n\nKung sa palagay mo ito ay isang error, ipaalam sa amin sa", + "browsing-site-blocked-description2": "pampublikong chat.", + "browsing-site-blocked-go-back": "Bumalik ka", + "browsing-site-blocked-title": "Na-block ang site na ito", + "browsing-title": "Browse", + "buy-crypto": "Bumili ng crypto", + "buy-crypto-choose-a-service": "Pumili ng serbisyo na nais mong gamitin upang bumili ng crypto", + "buy-crypto-description": "Maghanap ng dapp upang bumili ng crypto ngayon", + "buy-crypto-leaving": "Ikaw ay umaalis sa Status at pumapasok sa isang third party na website upang makumpleto ang iyong pagbili", + "buy-crypto-title": "Mukhang walang laman ang iyong wallet", + "camera-access-error": "Upang bigyan ang kinakailangang pahintulot ng camera, mangyaring pumunta sa iyong mga setting ng system at tiyakin na ang Status > Napili ang camera.", + "can-not-add-yourself": "Ikaw yan, upang magsimula ng isang chat pumili ng ibang tao", + "can-send-messages": "Maaari kang magpadala at makatanggap ng mga bagong mensahe", + "cancel": "kansela", + "cancel-keycard-setup": "Ikansela ang pag-setup ng Keycard", + "cancelling": "Pagkansela", + "cannot-read-card": "Hindi mabasa ang card.\nMangyaring hawakan ito sa likod ng iyong telepono", + "cannot-use-default-pin": "Hindi pinapayagan ang passcode 000000.\nMangyaring gumamit ng iba pang numero", + "cant-open-public-chat": "Hindi mabuksan ang pampublikong chat", + "cant-report-bug": "Hindi maiulat ang isang bug", + "card-is-blank": "Blangko ang card na ito", + "card-reseted": "Natapos na ang card", + "card-unpaired": "Ang card ay walang bayad mula sa kasalukuyang aparato", + "change-fleet": "Baguhin ang fleet sa{{fleet}}", + "change-logging-enabled": "Sigurado ka bang gusto mo{{enable}}logging?", + "change-passcode": "Baguhin ang Passcode", + "change-password": "Palitan ang password", + "change-pin": "Baguhin ang 6-digit na Passcode", + "changed-amount-warning": "Ang halaga ay binago mula sa{{old}} hangang sa{{new}}", + "changed-asset-warning": "Binago si Asset mula sa{{old}} hangang sa{{new}}", + "chaos-mode": "Chaos mode", + "chaos-unicorn-day": "Araw ng Chaos Unicorn", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Chat", + "chat-and-transact": "Makipag-chat at makipag-transaksyon nang pribado sa mga kaibigan", + "chat-is-a-contact": "Makipag-ugnay", + "chat-is-not-a-contact": "Hindi isang contact", + "chat-key": "Chat key", + "chat-key-content": "Mensahe sa Status Chat Protocol ay ipinapadala at natanggap gamit ang mga encryption keys. Mga public chat key ay isang string ng mga character na ibinabahagi mo sa iba upang maaari silang magpadala sa iyo ng mga mensahe sa Status.", + "chat-key-title": "Chat Key", + "chat-link-previews": "Mga preview ng chat link", + "chat-name": "Pangalan ng chat", + "chat-name-content": "Tatlong random na salita, nagmula algorithmically galing sa iyong chat key at gamitin mo ito para sa iyong default na alyas sa chat. Pangalan ng Chat ay ganap na natatangi o kakaiba, walang ibang user ang maaring magkapareho sa tatlong salita.", + "chat-name-title": "Chat Name", + "chat-notification-preferences": "Mga setting ng notification", + "chat-settings": "Mga setting ng chat", + "chats": "Chats", + "check-on-etherscan": "Tingnan sa etherscan", + "check-on-opensea": "Tingnan sa opensea", + "check-your-recovery-phrase": "Suriin ang iyong seed phrase", + "choose-actions": "Pumili ng mga aksyon", + "choose-authentication-method": "Pumili ng isang paraan ng pagpapatunay", + "choose-new-location-for-keystore": "Pumili ng isang bagong lokasyon upang i-save ang iyong keystore file", + "choose-storage": "Pumili ng imbakan", + "clear": "Malinaw", + "clear-all": "Alisin lahat", + "clear-history": "Alisin ang history", + "clear-history-action": "Malinaw", + "clear-history-confirmation": "Alisin ang history?", + "clear-history-confirmation-content": "Sigurado ka bang nais mong limasin ang kasaysayan ng chat na ito?", + "clear-history-title": "Alisin ang history?", + "close": "Isara", + "close-all": "Isara lahat", + "close-app-button": "Kumpirma", + "close-app-content": "Ang app ay titigil at isara. Kapag binuksan mo ito, gagamitin ang napiling network", + "close-app-title": "Babala!", + "command-button-send": "Ipadala", + "communities": "Mga Komunidad", + "communities-alpha": "Mga Komunidad (alpha)", + "communities-verified": "✓ Na-verify na Komunidad ng Status", + "community-color": "Kulay ng pamayanan", + "community-color-placeholder": "Pumili ng kulay", + "community-edit-title": "I-edit ang pamayanan", + "community-image-pick": "Pumili ng imahe", + "community-image-take": "Kunan ng litrato", + "community-invite-title": "Mag-anyaya", + "community-key": "Pribadong key ng komunidad", + "community-key-placeholder": "I-type ang iyong community private key", + "community-members": { + "zero": "sero", + "one": "isa", + "two": "dalawa", + "few": "konti", + "many": "marami", + "other": "iba" + }, + "community-members-title": "Mga kasapi", + "community-private-key": "Pribadong key ng komunidad", + "community-requests-to-join-title": "Mga kahilingan sa pagiging miyembro", + "community-roles": "Mga Tungkulin", + "community-share-title": "Ibahagi", + "community-thumbnail-image": "Thumbnail na imahe", + "community-thumbnail-upload": "I-upload", + "complete-hardwallet-setup": "Ang card na ito ay naka-link ngayon. Kailangan mo ito upang mag-sign ng transaksyon at para ma-unlock ang iyong keys", + "completed": "Nakumpleto", + "confirm": "Kumpirma", + "confirm-password-placeholder": "Kumpirmahin ang iyong password...", + "confirmation-request": "Kahilingan sa Pagkumpirma", + "confirmations": "Pagkumpirma", + "confirmations-helper-text": "Kapag ang transaksyon ay may 12 kumpirmasyon maaari mong isaalang-alang na naayos ito.", + "connect": "Kumonekta", + "connect-mailserver-content": "kumonekta sa {{name}}?", + "connect-wallet": "Ikonekta ang pitaka", + "connected": "Nakakonekta", + "connected-to": "Nakakonekta sa", + "connecting": "Pagkonekta ...", + "connecting-requires-login": "Ang pagkonekta sa isa pang network ay nangangailangan ng pag-login", + "connection-status": "Koneksyon ng Status", + "connection-with-the-card-lost": "Ang iyong koneksyon sa iyong Card ay nawala", + "connection-with-the-card-lost-setup-text": "Para ipagpatuloy ang pag-setup hawakan ang card sa\nlikod ng iyong phone at panatilihin\nang card sa contact sa telepono", + "connection-with-the-card-lost-text": "Para magpatuloy hawakan ang card sa likod ng iyong telepono", + "contact-code": "Chat key", + "contact-s": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunting", + "many": "maraming", + "other": "iba pang" + }, + "contacts": "Mga contact", + "contacts-descr": "Lilitaw ang iyong mga contact dito. Makakatanggap ka ng mga pag-update ng Status mula sa sinumang idagdag mo bilang isang contact", + "contacts-empty": "komontak gamit ang pangalan ng ENS na lilitaw dito", + "continue": "Magpatuloy", + "continue-anyway": "Magpatuloy pa rin", + "contract-address": "Contract address", + "contract-interaction": "Pakikipag-ugnayan sa kontrata", + "copy-info": "Kopyahin ang impormasyon", + "copy-qr": "Kopyahin ang code", + "copy-to-clipboard": "Kopya", + "copy-transaction-hash": "Kopyahin ang transaksyon ID", + "cost-fee": "Gastos \/ Bayad", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Lumikha", + "create-a-pin": "Gumawa ng 6-digit passcode", + "create-channel": "Lumikha ng channel", + "create-channel-title": "Bagong channel", + "create-community": "Lumikha ng isang pamayanan", + "create-group-chat": "Gumawa ng group chat", + "create-multiaccount": "Lumikha ng Keys", + "create-new-key": "Kumuha ng mga bagong keys", + "create-pin": "Lumikha ng 6-digit na passcode", + "create-pin-description": "kailangan mo ang iyong card + ang 6-digit na passcode na ito upang mai-unlock ang Status at upang kumpirmahin ang mga transaksyon.", + "created-group-chat-description": "Ikaw ay nakagawa ng grupo{{group-name}}", + "cryptokitty-name": "CryptoKitty #{{id}}", + "currency": "Pera", + "currency-display-name-aed": "Emirati Dirham", + "currency-display-name-afn": "Afghanistan Afghani", + "currency-display-name-ars": "Argentine Peso", + "currency-display-name-aud": "Australian Dollar", + "currency-display-name-bbd": "Barbados Dollar", + "currency-display-name-bdt": "Bangladeshi Taka", + "currency-display-name-bgn": "Bulgarian Lev", + "currency-display-name-bhd": "Bahraini Dinar", + "currency-display-name-bnd": "Brunei Darussalam Dollar", + "currency-display-name-bob": "Bolivia Bolíviano", + "currency-display-name-brl": "Brazil Real", + "currency-display-name-btn": "Bhutanese Ngultrum", + "currency-display-name-cad": "Canada Dollar", + "currency-display-name-chf": "Switzerland Franc", + "currency-display-name-clp": "Chile Peso", + "currency-display-name-cny": "China Yuan Renminbi", + "currency-display-name-cop": "Colombia Peso", + "currency-display-name-crc": "Costa Rica Colon", + "currency-display-name-czk": "Czech Koruna", + "currency-display-name-dkk": "Denmark Krone", + "currency-display-name-dop": "Dominican Republic Peso", + "currency-display-name-egp": "Egypt Pound", + "currency-display-name-etb": "Ethiopian Birr", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "British Pound", + "currency-display-name-gel": "Georgian Lari", + "currency-display-name-ghs": "Ghana Cedi", + "currency-display-name-hkd": "Hong Kong Dollar", + "currency-display-name-hrk": "Croatia Kuna", + "currency-display-name-huf": "Hungary Forint", + "currency-display-name-idr": "Indonesia Rupiah", + "currency-display-name-ils": "Israel Shekel", + "currency-display-name-inr": "India Rupee", + "currency-display-name-isk": "Iceland Krona", + "currency-display-name-jmd": "Jamaica Dollar", + "currency-display-name-jpy": "Japanese Yen", + "currency-display-name-kes": "Kenyan Shilling", + "currency-display-name-krw": "Korea (South) Won", + "currency-display-name-kwd": "Kuwaiti Dinar", + "currency-display-name-kzt": "Kazakhstan Tenge", + "currency-display-name-lkr": "Sri Lanka Rupee", + "currency-display-name-mad": "Moroccan Dirham", + "currency-display-name-mdl": "Moldovan Leu", + "currency-display-name-mur": "Mauritius Rupee", + "currency-display-name-mwk": "Malawian Kwacha", + "currency-display-name-mxn": "Mexico Peso", + "currency-display-name-myr": "Malaysia Ringgit", + "currency-display-name-mzn": "Mozambique Metical", + "currency-display-name-nad": "Namibia Dollar", + "currency-display-name-ngn": "Nigeria Naira", + "currency-display-name-nok": "Norway Krone", + "currency-display-name-npr": "Nepal Rupee", + "currency-display-name-nzd": "New Zealand Dollar", + "currency-display-name-omr": "Oman Rial", + "currency-display-name-pen": "Peru Sol", + "currency-display-name-pgk": "Papua New Guinean Kina", + "currency-display-name-php": "Philippines Peso", + "currency-display-name-pkr": "Pakistan Rupee", + "currency-display-name-pln": "Poland Zloty", + "currency-display-name-pyg": "Paraguay Guarani", + "currency-display-name-qar": "Qatar Riyal", + "currency-display-name-ron": "Romania Leu", + "currency-display-name-rsd": "Serbia Dinar", + "currency-display-name-rub": "Russia Ruble", + "currency-display-name-sar": "Saudi Arabia Riyal", + "currency-display-name-sek": "Sweden Krona", + "currency-display-name-sgd": "Singapore Dollar", + "currency-display-name-thb": "Thailand Baht", + "currency-display-name-try": "Turkish Lira", + "currency-display-name-ttd": "Trinidad at Tobago Dollar", + "currency-display-name-twd": "Taiwan New Dollar", + "currency-display-name-tzs": "Tanzanian Shilling", + "currency-display-name-uah": "walang mga kasapi", + "currency-display-name-ugx": "Ugandan Shilling", + "currency-display-name-usd": "United States Dollar", + "currency-display-name-uyu": "Uruguay Peso", + "currency-display-name-vef": "Venezuela Bolívar", + "currency-display-name-vnd": "Viet Nam Dong", + "currency-display-name-zar": "South Africa Rand", + "current": "Kasalukuyan", + "current-network": "Kasalukuyang network", + "current-pin": "Ilagay ang 6-digit na passcode", + "current-pin-description": "Ilagay ang iyong 6-digit na passcode upang magpatuloy", + "custom": "Pasadyang", + "custom-networks": "Pasadyang mga network", + "custom-node": "Ikaw ay gumagamit ng custom RPC endpoint. Ang iyong lokal na kasaysayan ng paglilipat ay maaring hindi kumpleto", + "custom-seed-phrase": "Hindi wastong seed phrase", + "custom-seed-phrase-text-1": "Ang seed phrase ay hindi tumutugma sa aming suportadong diksyunaryo. Suriin para sa mga maling salita.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Tanggapin at Buksan", + "dapp-starter-pack-description": "Narito ang ilang mga crypto upang makapagsimula ka! Gamitin ito upang makakuha ng mga sticker, isang pangalan ng ENS at subukan ang mga dapps", + "dapp-starter-pack-title": "Starter Pack", + "dapp-would-like-to-connect-wallet": "nais kumonekta sa", + "dapps": "ÐApps", + "dapps-permissions": "Mga pahintulot ng DApp", + "dark": "Madilim", + "data": "Data", + "data-syncing": "Pag-sync ng data", + "database-reset-warning": "Mare-reset ang database. Ang mga chat, contact at setting ay tatanggalin", + "datetime-ago": "nakaraan", + "datetime-ago-format": "{{number}}{{time-intervals}}{{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunting", + "many": "maraming", + "other": "iba pang" + }, + "datetime-day-short": { + "zero": "walang araw", + "one": "isang araw", + "two": "dalawang araw", + "few": "ilang araw", + "many": "maraming araw", + "other": "ibang araw" + }, + "datetime-hour": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "datetime-hour-short": { + "zero": "walang oras", + "one": "isang oras", + "two": "dalawang oras", + "few": "kaunting oras", + "many": "maraming oras", + "other": "ibang oras" + }, + "datetime-minute": { + "zero": "minuto", + "one": "isang", + "two": "dalawa", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "datetime-minute-short": { + "zero": "walang minuto", + "one": "isang minuto", + "two": "dalawang minuto", + "few": "ilang minuto", + "many": "maraming minuto", + "other": "ibang minuto" + }, + "datetime-second": { + "zero": "zero", + "one": "isa", + "two": "dalawa", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "datetime-second-short": { + "zero": "walang segundo", + "one": "isang segundo", + "two": "dalawang segundo", + "few": "ilang segundo", + "many": "maraming segundo", + "other": "iba pang mga segundo" + }, + "datetime-today": "ngayon", + "datetime-yesterday": "kahapon", + "decimals": "Decimals", + "decline": "Tanggihan", + "decryption-failed-content": "Isang error na naganap sa pag-decrypting ng iyong data. Maaaring kailanganin mong burahin ang iyong dating data at makabuo ng isang bagong account. Tapikin ang \"Mag-apply\" upang burahin o \"Ikansela\" upang subukang muli", + "default": "Default", + "default-assets": "Default na ERC20 at ERC721", + "delete": "Tanggalin", + "delete-account": "Tanggalin ang account", + "delete-and-leave-group": "Tanggalin at iwanan ang pangkat", + "delete-bootnode": "Tanggalin ang bootnode", + "delete-bootnode-are-you-sure": "Sigurado ka bang gusto mong tanggalin ang bootnode na ito?", + "delete-bootnode-title": "Tanggalin ang bootnode", + "close-chat": "Tanggalin ang chat", + "close-chat-confirmation": "Sigurado ka bang gusto mong tanggalin ang chat na ito?", + "delete-confirmation": "Tanggalin?", + "delete-mailserver": "Tanggalin ang Status node", + "delete-mailserver-are-you-sure": "Sigurado ka bang nais mong burahin ang Status node?", + "delete-mailserver-title": "Burahin ang Status node", + "delete-message": "Tanggalin ang mensahe", + "delete-my-account": "Tanggalin ang aking account", + "delete-my-profile": "Tanggalin ang aking profile", + "delete-network-confirmation": "Sigurado ka bang gusto mong tanggalin ang network na ito?", + "delete-network-error": "Mangyaring kumonekta sa ibang network bago matanggal ang isang ito", + "delete-network-title": "Tanggalin ang network?", + "delete-profile": "Tanggalin ang profile", + "delete-profile-warning": "Babala: Kung hindi mo naisulat ang iyong seed phrase, mawawalan ka ng pag-access sa iyong mga pondo matapos mong tanggalin ang iyong profile", + "deny": "Deny", + "derivation-path": "Landas ng dereksyon", + "description": "Paglalarawan", + "dev-mode": "Mode ng pag-unlad", + "dev-mode-settings": "Pag-unlad ng mga setting ng mode", + "device-syncing": "Pag-sync ng aparato", + "devices": "Aparato", + "disable": "huwag paganahin", + "disabled": "Disabled", + "disconnected": "Chat offline", + "discover": "tuklasin", + "dismiss": "Iwaksi", + "done": "Tapos na", + "dont-ask": "Huwag mo ulit akong tanungin", + "edit": "Edit", + "edit-community": "I-edit ang pamayanan", + "edit-favourite": "baguhin ang mga paborito", + "edit-group": "I-edit ang grupo", + "edit-profile": "Ibahin ang profile", + "empty-chat-description": "Walang mga mensahe\nsa chat na ito", + "empty-chat-description-community": "Tahimik dito sa huling {{quiet-hours}} .", + "empty-chat-description-one-to-one": "Ang anumang mga mensahe na ipinadala mo dito ay naka-encrypt at maaari mo lamang itong basahin at", + "empty-chat-description-public": "Masyadong tahimik dito para sa huling{{quiet-hours}}Simulan ang pag-uusap o", + "empty-chat-description-public-share-this": "ibahagi ang chat na ito.", + "empty-keycard-required": "Nangangailangan ng isang walang laman na Keycard", + "empty-pending-invitations-descr": "Ang mga taong nais sumali sa pangkat \n sa pamamagitan ng isang link ng imbitasyon ay lilitaw dito", + "empty-tab": "Walang laman na tab", + "enable": "Paganahin", + "enable-all": "Paganahin lahat", + "enable-link-previews": "Paganahin ang mga link previews sa chat?", + "encrypt-with-password": "I-encrypt gamit ang password", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Magdagdag ng username", + "ens-agree-to": "Sang-ayon sa", + "ens-chat-settings": "Mga setting ng chat", + "ens-custom-domain": "Custom domain", + "ens-custom-username-hints": "I-type ang buong username kabilang ang pasadyang domain tulad ng username.domain.eth", + "ens-custom-username-taken": "Hindi kabilang sa iyo ang Username :(", + "ens-deposit": "Deposito", + "ens-dismiss-message": "I-click dito upang idespatsa", + "ens-displayed-with": "Ang iyong mga mensahe ay ipinapakita sa iba kasama", + "ens-get-name": "Kumuha ng isang unibersal na username", + "ens-got-it": "Ok, nakuha ito", + "ens-locked": "Naka-lock ang Username. Hindi mo mai-release ito hanggang{{date}}", + "ens-name-content": "Pasadyang alyas para sa iyong susi sa chat na maaari iparehistro gamit ang serbisyong pangalan ng Ethereum. Ang mga pangalan ng ENS ay desentralisado na mga username.", + "ens-name-not-found": "Hindi malutas ang pangalan ng ENS", + "ens-name-title": "Pangalan ng ENS", + "ens-network-restriction": "Magagamit lamang sa Mainnet", + "ens-no-usernames": "Wala kang koneksyon sa anumang username", + "ens-powered-by": "Pinapagana ng Ethereum Name Services", + "ens-primary-username": "Pangunahing username", + "ens-register": "Magrehistro", + "ens-registration-failed": "Para mairehistro ang username, mangyaring subukang muli.", + "ens-registration-failed-title": "Nabigo ang transaksyon", + "ens-registration-failure": "Nabigo ang pagrehistro", + "ens-registration-in-progress": "Pino-progreso ang Rehistro", + "ens-release-username": "Bitawan ang username", + "ens-remove-hints": "Ang pagtanggal ay tatanggalin ang username mula sa iyong key.", + "ens-remove-username": "Alisin ang username", + "ens-saved": "ay konektado ngayon sa iyong chat key at maaaring magamit sa Status.", + "ens-saved-title": "Idinagdag ang username", + "ens-show-username": "Ipakita ang aking pangalan ng ENS sa mga chat", + "ens-terms-header": "Mga tuntunin ng pagpaparehistro ng pangalan", + "ens-terms-point-1": "Ang mga pondo ay idineposito para sa 1 taon. Ang iyong SNT ay mai-lock, ngunit hindi ginugol.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS Registry).", + "ens-terms-point-2": "Matapos ang 1 taon, maaari mong palabasin ang pangalan at ibabalik ang iyong deposito, o walang pagkilos upang mapanatili ang pangalan.", + "ens-terms-point-3": "Kung ang mga termino ng pagbabago sa kontrata - e.g Status gumagawa ng mga pag-upgrade ng kontrata - May karapatan ang gumagamit na palabasin ang username anuman ang gaganapin sa oras.", + "ens-terms-point-4": "Hindi ma-access ng Controller ng kontrata ang iyong mga na-deposito na pondo. Maaari lamang silang ilipat sa address na nagpadala sa kanila.", + "ens-terms-point-5": "Ang iyong address (es) ay maiugnay sa publiko sa iyong pangalan ng ENS.", + "ens-terms-point-6": "Ang mga username ay nilikha bilang mga subdomain node ng stateofus.eth at napapailalim sa ENS smart contract sa mga terms.", + "ens-terms-point-7": "Pinapayagan mo ang kontrata para ilipat ang SNT sa iyong ngalan. Magagawa lamang ito kapag inaprubahan mo ang isang transaksyon upang pahintulutan ang paglipat.", + "ens-terms-point-8": "Ang mga salitang ito ay ginagarantiyahan ng smart contract logic sa address:", + "ens-terms-point-9": "{{address}}(Status UsernameRegistrar)", + "ens-terms-registration": "Mga tuntunin ng pagpaparehistro ng pangalan.", + "ens-test-message": "Hey", + "ens-transaction-pending": "Naka-pending ang transaksyon ...", + "ens-understand": "Naiintindihan ko na ang aking wallet address ay maiugnay sa publiko sa aking username.", + "ens-username": "Username ng ENS", + "ens-username-already-added": "Nakakonekta na ang Username sa iyong chat key at maaaring magamit sa loob ng Status.", + "ens-username-available": "Ang username ay magagamit", + "ens-username-connected": "Ang pangalan ng gumagamit na ito ay pagmamay-ari mo at konektado sa iyong chat key.", + "ens-username-connected-continue": "Ipagpapatuloy na itakda ang aking pangalan sa ENS sa chat.", + "ens-username-connected-with-different-key": "Ang pagpapatuloy ay mangangailangan ng isang transaksyon upang ikonekta ang username sa iyong chat key.", + "ens-username-connection-confirmation": "{{username}} ay konektado sa sandaling kumpleto na ang transaksyon.", + "ens-username-hints": "Hindi bababa sa 4 na character. Mga letrang Latin, numero, at maliit na titik lamang.", + "ens-username-invalid": "Mga letra at mga numero lang.", + "ens-username-owned": "✓ Ang username ay pagmamay-ari mo.", + "ens-username-owned-continue": "Ang pagpapatuloy ay magkakokonekta sa username na ito sa iyong chat key.", + "ens-username-registration-confirmation": "Magaling! Pag-aari mo ang{{username}}sa sandaling kumpleto na ang transaksyon", + "ens-username-taken": "Nakuha na ang username :(", + "ens-username-you-can-follow-progress": "Maaari mong sundin ang pag-unlad sa seksyon ng Kasaysayan ng Transaksyon ng iyong wallet.", + "ens-usernames": "Mga pangalan ng ENS", + "ens-usernames-details": "Magrehistro ng isang unibersal na username upang madaling makilala ng ibang mga gumagamit", + "ens-want-custom-domain": "Nagmamay-ari ako ng isang pangalan sa isa pang domain", + "ens-want-domain": "Gusto ko ng isang stateofus.eth domain", + "ens-welcome-hints": "Binago ni ENS ang mga pangalan na sobrang habang address sa mga natatanging pangalan.", + "ens-welcome-point-customize": "Ang pangalan ng ENS ay maaaring palitan ang iyong 3 random word na pangalan sa chat. Maging @iyongpangalan sa halip na {{name}}.", + "ens-welcome-point-customize-title": "I-customize ang iyong pangalan ng chat", + "ens-welcome-point-receive": "Ang iba ay maaaring magpadala sa iyo ng mga pondo sa pamamagitan ng chat sa isang simpleng hakbang.", + "ens-welcome-point-receive-title": "Tumanggap ng mga transaksyon sa chat", + "ens-welcome-point-register": "Magrehistro ng isang beses upang mapanatili ang pangalan magpakailanman. Matapos ang 1 taon maaari mong pakawalan ang pangalan at maibalik ang iyong SNT.", + "ens-welcome-point-register-title": "10 SNT upang magparehistro", + "ens-welcome-point-simplify": "Maaari kang makatanggap ng mga pondo sa iyong easy-to-share ENS name kaysa sa iyong hexadecimal hash (0x...).", + "ens-welcome-point-simplify-title": "Pasimplehin ang iyong ETH address", + "ens-welcome-point-verify": "Maaari mong i-verify at magdagdag ng anumang mga username na pagmamay-ari mo sa susunod na mga hakbang.", + "ens-welcome-point-verify-title": "Mayroon ka bang isang username?", + "ens-your-username": "Ang iyong username", + "ens-your-usernames": "Ang iyong mga username", + "ens-your-your-name": "Ang iyong pangalan ng ENS", + "enter-12-words": "Ilagay ang 12 words ng iyong seed phrase, pinaghiwalay ng mga solong puwang", + "enter-a-private-key": "Ilagay ang pribadong susi", + "enter-a-seed-phrase": "Ilagay ang seed phrase", + "enter-address": "Ilagay ang address", + "enter-contact-code": "ilagay ang ENS username o chat key", + "enter-pair-code": "ilagay ang iyong pares na code", + "enter-pair-code-description": "Ang ipinares na code ay ipinakita sa iyo sa pag-setup ng Keycard", + "enter-password": "Ilagay ang password", + "enter-pin": "Ilagay ang iyong 6-digit na passcode", + "enter-puk-code": "Ilagay ang PUK code", + "enter-puk-code-description": "Na-block ang 6-digit na passcode.\nMangyaring ilagay ang PUK code upang i-unblock ang passcode.", + "enter-recipient-address-or-username": "Ilagay ang address o username ng tatanggap", + "enter-seed-phrase": "Ilagay ang seed phrase", + "enter-url": "Ilagay ang URL", + "enter-user-pk": "Ilagay ang user public key", + "enter-watch-account-address": "Scan ang QR code o i-enter ang address para makita", + "enter-word": "ilagay ang word", + "enter-your-code": "Ilagay ang iyong 6-digit na passcode", + "enter-your-password": "Ilagay ang iyong password", + "error": "Error", + "error-unable-to-get-balance": "Hindi makakakuha ng balanse", + "error-unable-to-get-prices": "Error sa conversion ng Pera. I-refresh ang iyong screen upang subukang muli.", + "error-unable-to-get-token-balance": "hindi makukuha ang balance ng token", + "errors": "Mga Pagkakamali", + "eth": "ETH", + "ethereum-account": "Ethereum account", + "ethereum-address": "Address ng Ethereum", + "ethereum-node-started-incorrectly-description": "Ang Ethereum node ay sinimulan ng maling pagsasaayos, titigil ang aplikasyon upang mabawi mula sa kondisyong iyon. Na-configure ang network id ={{network-id}}, aktwal ={{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Ang Ethereum node ay nagsimula nang hindi wasto", + "etherscan-lookup": "Tumingin sa Etherscan", + "everyone": "Lahat", + "export-account": "I-export ang account", + "export-key": "I-export ang pribadong key", + "external-storage-denied": "Tinanggihan ang pag-access sa external storage", + "failed": "Nabigo", + "faq": "Mga madalas na tinatanong", + "favourite": "Mga Paborito", + "favourite-description": "Lahat ng paboritong website ay lalabas dito", + "favourites": "Mga paborito", + "favourites-empty": "Lilitaw dito ang mga address na idinagdag sa mga paborito", + "fetch-messages": "↓ Kumuha ng mga mensahe", + "fetch-timeline": "↓ Fetch", + "find": "Maghanap", + "finish": "Tapos na", + "finishing-card-setup": "Pag-setup ng card", + "fleet": "Fleet", + "fleet-settings": "Fleet settings", + "follow": "Sundan", + "follow-your-interests": "Sumali sa isang pampublikong chat at makilala ang mga bagong tao", + "free": "↓ Libre", + "from": "Mula sa", + "gas-limit": "Gas limit", + "gas-price": "Presyo ng Gas", + "gas-used": "Ginamit na gas", + "generate-a-key": "Bumuo ng mga keys", + "generate-a-new-account": "Lumikha ng account", + "generate-a-new-key": "Lumikha ng isang bagong key", + "generate-account": "Bumuo ng mga keys", + "generate-an-account": "lumikha ng account", + "generate-new-key": "Lumikha ng bagong keys", + "generating-codes-for-pairing": "> Pag-download ng software ng produkto sa card\n> Bumubuo ng pag-unlock at mga pagpapares code", + "generating-keys": "Lumilikha ng keys", + "generating-mnemonic": "Bumubuo ng seed phrase", + "get-a-keycard": "Kumuha ng isang Keycard", + "get-started": "Magsimula", + "get-status-at": "Kumuha ng Status sa http:\/\/status.im", + "get-stickers": "Kumuha ng Sticker", + "give-a-short-description": "Magbigay ng maikling deskripsyon", + "give-a-short-description-community": "Bigyan ito ng isang maikling paglalarawan", + "give-permissions-camera": "Magbigay ng pahintulot\nupang ma-access ang camera", + "glossary": "Glossary", + "go-to-settings": "Pumunta sa Mga Setting ...", + "got-it": "Nakuha ko", + "grant-face-id-permissions": "Upang mabigyan ang kinakailangang pahintulot ng ID gamit ng iyong mukha, mangyaring pumunta sa iyong mga setting ng system at tiyaking napili ang Status > Face ID", + "group-chat": "Group chat", + "group-chat-admin": "Admin", + "group-chat-admin-added": "**{{member}}** ay ginawang admin", + "group-chat-all-contacts-invited": "Ang lahat ng iyong mga contact ay nasa grupo na", + "group-chat-created": "**{{member}}**nilikha ang pangkat**{{name}}**", + "group-chat-decline-invitation": "Tanggihan ang paanyaya", + "group-chat-member-added": "**{{member}}** ay inimbitahan", + "group-chat-member-joined": "**{{member}}** ay sumali sa group", + "group-chat-member-removed": "**{{member}}** umalis sa group", + "group-chat-members-count": "{{selected}}\/{{max}}miyembro", + "group-chat-name-changed": "**{{member}}** Palitan ang pangalan ng group sa **{{name}}**", + "group-chat-no-contacts": "Wala ka pang mga contact.\nAnyayahan ang iyong mga kaibigan na simulan ang pakikipag-chat", + "group-info": "Impormasyon sa grupo", + "group-invite": "Paanyaya sa pangkat", + "group-invite-link": "Link ng pangkat na anyayahan", + "group-membership-request": "Kahilingan sa pagiging kasapi ng pangkat", + "gwei": "Gwei", + "has-permissions": "ay may pahintulot sa pag access", + "hash": "Hash", + "help": "tumulong", + "help-capitalized": "Tulong", + "help-center": "Sentro ng tulong", + "hide-content-when-switching-apps": "Block screenshots", + "hide-content-when-switching-apps-ios": "Itago ang preview", + "history": "Kasaysayan", + "history-nodes": "Status nodes", + "hold-card": "Hold card sa likod\nng iyong telepono", + "home": "Home", + "hooks": "Hooks", + "http-gateway-error": "Oops, nabigo ang kahilingan!", + "identifier": "Identifier", + "image": "Imahe", + "image-remove-current": "Alisin ang kasalukuyang larawan", + "image-source-gallery": "Pumili mula sa gallery", + "image-source-make-photo": "Kumuha", + "image-source-title": "I-edit ang larawan", + "import": "angkat", + "import-community": "Mag-import ng isang pamayanan", + "import-community-title": "Mag-import ng isang pamayanan", + "in-contacts": "Sa mga contact", + "incoming": "Paparating", + "incoming-transaction": "Papasok na transaksyon", + "incorrect-code": [ + "str", + "Paumanhin hindi tama ang code, mangyaring ilagay muli" + ], + "increase-gas": "Taasan ang Gas", + "initialization": "Pagsisimula", + "install": "↓ Install", + "intro-message1": "Maligayang pagdating sa Status!\nTapikin ang mensaheng ito upang itakda ang iyong password at magsimula.", + "intro-privacy-policy-note1": "Ang Status ay hindi kinukolekta o kumikita sa iyong personal na data. Sa pamamagitan ng pagpapatuloy, sumasang-ayon ka sa", + "intro-privacy-policy-note2": "patakaran sa privacy", + "intro-text": "Status ay ang iyong gateway sa desentralisadong web", + "intro-text1": "Makipag-chat sa isang peer-to-peer, naka-encrypt na network kung saan ang mga mensahe ay hindi mai-censor o mai-hack", + "intro-text2": "Magpadala at tumanggap ng mga digital na assets sa kahit saan sa mundo — walang kinakailangang bank account", + "intro-text3": "Galugarin ang mga laro, palitan at mga social network kung saan nag-iisa kang nagmamay-ari ng iyong data", + "intro-title1": "Tunay na pribadong komunikasyon", + "intro-title2": "Secure ang crypto wallet", + "intro-title3": "Desentralisadong apps", + "intro-wizard-text1": "Kinokontrol ng isang hanay ng mga keys ang iyong account. Nakatira ang iyong mga keys sa iyong telepono, kaya maaari mo lamang itong magamit", + "intro-wizard-text2": "Ang isang key ay para sa chat. Ito ay may nabasang pangalan na hindi mababago.", + "intro-wizard-text3": "Kung nagmamay-ari ka ng isang Keycard, itago ang iyong mga keys doon para sa pinahusay na seguridad.", + "intro-wizard-text4": "I-secure at i-encrypt ang iyong mga keys", + "intro-wizard-text6": "Ang Status ay magbabatid sa iyo tungkol sa mga bagong mensahe. Maaari mong mai-edit ang iyong mga kagustuhan sa abiso mamaya sa mga setting", + "intro-wizard-title-alt4": "Gumawa ng password", + "intro-wizard-title-alt5": "Kumpirmahin ang iyong password", + "intro-wizard-title1": "Kunin ang iyong mga keys", + "intro-wizard-title2": "Pumili ng isang pangalan ng chat", + "intro-wizard-title3": "Piliin ang key storage", + "intro-wizard-title4": "Gumawa ng 6-digit passcode", + "intro-wizard-title5": "Kumpirmahin ang passcode", + "intro-wizard-title6": "Paganahin ang mga notification", + "introduce-yourself": "Ipakilala ang iyong sarili sa isang maikling mensahe", + "invalid-address-qr-code": "Ang naka-scan na QR code ay hindi naglalaman ng isang wastong address", + "invalid-format": "Hindi wastong format\nDapat{{format}}", + "invalid-key-confirm": "Apply", + "invalid-key-content": "Ang Database ay hindi mai-encrypt dahil ang file ay corrupt. Ang iyong funds at chat key ay ligtas. Ang iba pang data, katulad ng iyong chats at contacts, hindi na maibabalik. \"{{erase-multiaccounts-data-button-text}}\" button, aalisin ang lahat ng iba pang data at magpapahintulot sa iyo na ma-access ang iyong mga pondo at magpadala ng mga mensahe.", + "invalid-number": "Di-wastong numero", + "invalid-pairing-password": "Di-wastong pagpapares ng password", + "invalid-public-chat-topic": "Hind wastong pampublikong paksa", + "invalid-range": "Hindi wastong format, dapat nasa pagitan{{min}}at{{max}}", + "invite": "Mag-anyaya", + "invite-button": "Mag-anyaya", + "invite-chat-accept": "Tanggapin", + "invite-chat-accept-join": "Tanggapin at Sumali", + "invite-chat-intro": "Ikaw ay ni-refer ng isang kaibigan na sumali sa Status. Narito ang ilang crypto upang makapagsimula ka! Gamitin ito sa pag reghistro ng pangalan ng ENS o bumili ng sticker pack", + "invite-chat-name": "Referral ng kaibigan", + "invite-chat-pending": "Naghihintay", + "invite-chat-rule": "Ang pagtanggap ay gagantimpalaan din ang iyong kaibigan ng isang crypto referral bonus", + "invite-chat-starter-pack": "Starter Pack", + "invite-friends": "Mag-imbita ng mga kaibigan", + "how-it-works": "Paano ito gumagana", + "invite-instruction-fifth": "Maaari kang pumili para i-redeem ang iyong referral bonus anumang oras.", + "invite-instruction-first": "Nagpadala ka ng isang natatanging link ng imbitasyon sa iyong kaibigan upang i-download at sumali sa Status", + "invite-instruction-fourth": "Ikaw ay makakatanggap ng referral bonus at ang iyong kaibigan ng Starter Pack", + "invite-instruction-second": "Ang iyong kaibigan ay nag download ng Status at gumawa ng account (sa Android)", + "invite-instruction-third": "Sinimulan ang isang chat sa iyong kaibigan, kung saan kumpirmahin ang iyong referral", + "invite-people": "Mag-imbita ng mga tao", + "invite-privacy-policy-public": "Nag-install ka ng Status sa pamamagitan ng isang referral na link. Sa pamamagitan ng pagsali sa chat na ito naiugnay mo ang iyong referrer at sumasang-ayon sa", + "invite-privacy-policy1": "Sa pamamagitan ng pagtanggap sumasang-ayon ka sa programa ng referral", + "invite-privacy-policy2": "Mga Tuntunin at Kondisyon.", + "invite-public-chat-home": "Imbitasyon ng referral", + "invite-public-chat-intro": "Narito ang ilang crypto upang makapagsimula ka! Gamitin ito upang magparehistro ng isang pangalan ng ENS o bumili ng isang sticker pack", + "invite-receive-account": "Account upang matanggap ang iyong referral bonus", + "invite-reward": "Kumita ng crypto sa bawat kaibigan na inaanyayahan mo!", + "invite-reward-friend": "Kaibigan: ", + "invite-reward-friend-description": "Ang iyong kaibigan ay makakatanggap ng Starter Pack na binubuo ng ilang {{reward}} upang makapagsimula", + "invite-reward-friend-name": "Starter Pack", + "invite-reward-you": "Ikaw:", + "invite-reward-you-description": "Mag-imbita ng mga kaibigan at tanggapin ang {{reward}} bilang isang referral bonus. Gamitin ito para sa stickers, pangalan ng ENS at subukan ang iba pang dapps", + "invite-reward-you-name": "Referral bonus", + "invite-select-account": "Pumili ng isang account para matanggap ang iyong referral bonus", + "invite-warning": "Ang promosyon na ito ay may bisa lamang para sa mga gumagamit ng Android device, na hindi residente ng US. Kailangang kumpirmahin ng kaibigan ang referral sa loob ng 7 araw", + "invited": "inimbitahan", + "join": "Sumali", + "join-a-community": "o sumali sa isang pamayanan", + "join-group-chat": "Sumali sa group", + "join-group-chat-description": "{{username}}inanyayahan kang sumali sa grupo{{group-name}}", + "join-me": "Hali ka, samahan mo ako sa Status: {{url}}", + "join-new-private-chat": "Magsimula ng bagong pribadong chat", + "join-new-public-chat": "Sumali sa pampublikong chat", + "joined": "Sumali", + "joined-group-chat-description": "Sumali ka sa{{group-name}}mula sa paanyaya ni{{username}}", + "key": "Key", + "key-managment": "pangunahing pamamahala", + "key-on-device": "Ang pribadong key ay nai-save sa device na ito", + "keycard": "Keycard", + "keycard-access-reset": "Ang pag-access sa Keycard ay naka-reset", + "keycard-applet-install-instructions": "Para mai-install ang applet mangyaring sundin ang mga tagubilin sa https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "subukang galawin ang card paikot para makita ng NFC at mabasa sa iyong aparato.", + "keycard-awaiting-title": "Naghahanap pa rin...", + "keycard-blocked": "Na-block ang Keycard.\nKailangan mong i-reset ang card upang magpatuloy sa paggamit nito.", + "keycard-can-use-with-new-passcode": "Maaari mong gamitin ang card na ito sa iyong bagong passcode", + "keycard-cancel-setup-text": "Kanselahin nito ang pag-setup ng keycard. Ito'y lubos na inirerekumenda na tapusin ang pag-setup upang magamit ang keycard. Nais mo bang kanselahin?", + "keycard-cancel-setup-title": "Mapanganib na operasyon", + "keycard-connected-description": "Subukang panatilihin pa rin ang card", + "keycard-connected-title": "Nakakonekta", + "keycard-desc": "May-ari ng isang Keycard? Itago ang iyong mga keys dito; kakailanganin mo ito para sa mga transaksyon", + "keycard-dont-ask-card": "Huwag humingi ng card upang mag-sign in", + "keycard-enter-new-passcode": "Ilagay ang bagong passcode {{step}}\/2", + "keycard-error-description": "i-konek ang card para makapag simula ulit", + "keycard-error-title": "Nawala ang koneksyon", + "keycard-free-pairing-slots": "Ang Keycard ay mayroong {{n}} libreng pagpapares ng mga puwang", + "keycard-has-multiaccount-on-it": "Ang card na ito ay puno na. Ang bawat card ay maaaring humawak ng isang pangunahing keypair", + "keycard-init-description": "Ilagay ang card sa likod ng iyong telepono upang magpatuloy", + "keycard-init-title": "Naghahanap ng mga kards", + "keycard-is-blocked-details": "Hindi mo na magagamit ang card na ito upang ma-access o mag-sign para sa account na ito. Napakaraming nabigo na passcode at mga pagtatangka ng PUK.", + "keycard-is-blocked-instructions": "Upang ma-access ang iyong account maaring i-reinstall ang Status at gumamit ng bagong Keycard, gumamit ng ibang wallet o i-reset ang keycard ng manu-mano.", + "keycard-is-blocked-title": "Na-block ang Keycard", + "keycard-is-frozen-details": "Upang maprotektahan ang iyong mga assets, ang iyong card ay frozen. I-reset ang access sa card upang i-unlock ang mga key at magpadala ng mga transaksyon. Lumikha ng isang bagong passcode at ipasok ang iyong PUK upang ma-access ang iyong (mga) account sa card na ito.", + "keycard-is-frozen-reset": "I-reset ang pag-access sa card", + "keycard-is-frozen-title": "Ang Keycard ay frozen", + "keycard-onboarding-finishing-header": "Tinatapos na", + "keycard-onboarding-intro-header": "Itago ang iyong mga keys sa Keycard", + "keycard-onboarding-intro-text": "Maghanda, maaaring tumagal ito ng ilang minuto, ngunit mahalaga na ma-secure ang iyong account", + "keycard-onboarding-pairing-header": "Pagpapares ng card ...", + "keycard-onboarding-preparing-header": "Paghahanda ng card....", + "keycard-onboarding-puk-code-header": "Isulat ang mga code\n at itago ang mga ito nang ligtas", + "keycard-onboarding-recovery-phrase-description": "Kailangan mo ang seed phrase para mabalik ang iyong key. Isulat mo. Panatilihing ligtas, offline, at paghiwalayin sa aparatong ito.", + "keycard-onboarding-recovery-phrase-header": "Back up seed phrase", + "keycard-onboarding-recovery-phrase-text": "Para sa iyong mga mata lamang. Ito ang magical seed na ginamit upang makabuo ng iyong key.", + "keycard-onboarding-start-header": "I-hold ang card sa likod\n ng iyong telepono upang magsimula", + "keycard-onboarding-start-step1": "Gumawa ng passcode", + "keycard-onboarding-start-step1-text": "Halos 1 minuto. Lumikha ng isang 6-digit na passcode upang i-encrypt ang iyong mga key", + "keycard-onboarding-start-step2": "Isulat ang PUK at ang pairing code", + "keycard-onboarding-start-step2-text": "Halos 1 minuto. Kakailanganin mo ang isang piraso ng papel at isang lapis para doon", + "keycard-onboarding-start-step3": "I-back up ang seed phrase", + "keycard-onboarding-start-step3-text": "Halos 1 minuto. Gayundin ang isang piraso ng papel at isang lapis ay kinakailangan", + "keycard-onboarding-start-text": "At panatilihin ang card sa contact sa telepono\n sa pag-setup. Ang pag-setup ay aabutin ng halos 4 na minuto", + "keycard-processing-description": "Subukang panatilihin pa rin ang card", + "keycard-processing-title": "Nagproproseso", + "keycard-recovery-intro-button-text": "Simulan ang pagbawi", + "keycard-recovery-intro-header": "Ibalik ang mga key na naka-imbak sa keycard", + "keycard-recovery-intro-text": "Kung ikaw ay bumuo ng mga keys gamit ang keycard nuon at ngayon gusto mo gamitin ang mga keys sa mga aparato.", + "keycard-recovery-no-key-header": "Wala naman\nma recover dito", + "keycard-recovery-no-key-text": "Ang iyong Keycard ay walang nakaimbak na key dito. Upang magamit ito, bumuo ng isang bagong key at piliin ang iyong Keycard upang maiimbak ang key", + "keycard-recovery-phrase-confirm-header": "Kumpirmahin ang seed phrase", + "keycard-recovery-phrase-confirmation-text": "Hindi ka magkakaroon ng pangalawang pagkakataon! Kung nawalan ka ng access, halimbawa sa pagkawala ng iyong keycard, maaari mo lamang ma access ang iyong keys kasama ang iyong seed phrase. Walang sinuman, kundi ikaw ay mayroon ng iyong seed phrase. Isulat mo ito at panatilihing ligtas.", + "keycard-recovery-phrase-confirmation-title": "Isinulat ang seed phrase?", + "keycard-recovery-success-header": "Ang iyong mga keys ay\nmatagumpay na nabawi", + "keycard-redeem-title": "I-Redeem sa", + "keycard-redeem-tx": "Mag Redeem ng Assets", + "keycard-redeem-tx-desc": "Tapikin ang card upang mag-sign at makatanggap ng mga assets", + "keycard-reset-passcode": "Reset passcode", + "keycard-success-description": "maari mo ng alisin ang card ngayon", + "keycard-success-title": "Tagumpay", + "keycard-unauthorized-operation": "Hindi ka awtorisado upang maisagawa ang operasyong ito.\nMangyaring i-tap ang wastong card at subukang muli.", + "keycard-upsell-subtitle": "Ang iyong portable, madaling gamitin na hardware wallet", + "language": "Lengwahe", + "learn-more": "Dagdagan ang nalalaman", + "learn-more-about-keycard": "Matuto nang higit pa tungkol sa Keycard", + "leave": "Umalis", + "leave-chat": "Umalis sa chat", + "leave-chat-confirmation": "Sigurado ka bang nais mong iwanan ang chat?", + "leave-community": "Umalis sa pamayanan", + "leave-confirmation": "aalis sa chat", + "leave-group": "Umalis sa grupo", + "left": "Umalis", + "les-ulc": "LES\/ULC", + "lets-go": "Tara na", + "light": "Mailaw", + "linked-on": "Naka-link {{date}}", + "load-messages-before": "bago ang {{date}}", + "load-more-messages": "↓ Kumuha ng higit pang mga mensahe", + "load-more-timeline": "↓ Dagdagan pa", + "loading": "Loading...", + "local-notifications": "Mga lokal na abiso", + "lock-app-with": "I-lock ang app na may", + "log-level": "Log level", + "log-level-settings": "Mga setting ng antas ng log", + "logging": "Logging", + "logging-enabled": "Paganahin ang logging?", + "login-pin-description": "Ilagay ang 6-digit passcode para ma-unlock ang iyong keys", + "logout": "Log out", + "logout-app-content": "Ang account ay mai-log out. Kapag binuksan mo ulit ito, gagamitin ang napiling network", + "logout-are-you-sure": "Sigurado ka bang gusto mo\nmag-log out?", + "logout-title": "Log out?", + "looking-for-cards": "Naghahanap ng mga cards", + "lost-connection": "Nawala ang koneksyon", + "mail-should-be-configured": "Ang mail client ay dapat isaayos", + "mailserver-address": "Status node address", + "mailserver-automatic": "Awtomatikong pagpili", + "mailserver-automatic-switch-explanation": "Piliin ang pinakamabilis na available sa Status node", + "mailserver-connection-error": "Hindi makakonekta sa Status node", + "mailserver-content": "Ang isang node sa Status network na ruta at nag-iimbak ng mga mensahe, hanggang sa 30 araw.", + "mailserver-details": "Mga detalye ng Status node", + "mailserver-error-content": "Ang pinili mong Status node ay hindi maabot", + "mailserver-error-title": "Error sa pagkonekta sa Status node", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "Pumili ng iba pang Status node", + "mailserver-reconnect": "Hindi makakonekta sa Status node. I-tap upang muling kumonekta", + "mailserver-request-error-content": "Ang sumusunod na error ay naibalik ng Status node: {{error}}", + "mailserver-request-error-status": "Isang error na naganap habang kumukuha ng kasaysayan, suriin ang mga tala para sa mga detalye", + "mailserver-request-error-title": "Ang paghiling ng Status node ay error", + "mailserver-request-retry": "Muling hilingin", + "mailserver-retry": "Ulitin", + "mailserver-title": "Status node", + "main-currency": "Pangunahing pera", + "main-networks": "pangunahing networks", + "main-wallet": "Pangunahing Wallet", + "mainnet-network": "Pangunahing network", + "make-admin": "Gumawa ng admin", + "manage-keys-and-storage": "Pamahalaan ang mga key at imbakan", + "mark-all-read": "Markahan ang lahat ng nabasa", + "master-account": "Master account", + "maybe-later": "Baka mamaya", + "member-kick": "Kick member", + "members": { + "zero": "wala", + "one": "isang", + "two": "dalawang", + "few": "kakaunti", + "many": "marami", + "other": "iba pa" + }, + "members-active": { + "zero": "sero", + "one": "isang", + "two": "dalawang", + "few": "kakaunting", + "many": "maraming", + "other": "iba pang" + }, + "members-active-none": "walang mga miyembro", + "members-count": "{{count}} miyembro", + "members-label": "Mga kasapi", + "members-limit-reached": "Naabot na ang limitasyon ng mga miyembro", + "members-title": "Mga kasapi", + "membership-approval": "Mangangailangan ng pag-apruba", + "membership-approval-description": "Ang iyong komunidad ay malayang sumali, ngunit ang mga bagong kasapi ay kinakailangang maaprubahan muna ng lumikha ng komunidad", + "membership-button": "Kahilingan sa pagiging kasapi ng pangkat", + "membership-declined": "Tinanggihan ang kahilingan sa pagiging miyembro", + "membership-description": "Ang pagiging kasapi ng pangkat ay kinakailangan kang tanggapin ng admin", + "membership-ens": "Kailangan ng ENS username", + "membership-ens-description": "Ang iyong komunidad ay nangangailangan ng isang ENS username upang makasali", + "membership-free": "Walang kinakailangan", + "membership-free-description": "Bukas ang iyong pamayanan para sumali ang sinuman", + "membership-invite": "Humiling ng paanyaya mula sa ibang miyembro", + "membership-invite-description": "Ang iyong komunidad ay maaari lamang sumali sa pamamagitan ng isang paanyaya mula sa existing na miyembro ng komunidad", + "membership-none": "Wala", + "membership-none-placeholder": "Maaari kang mangailangan ng mga bagong kasapi upang matugunan ang ilang mga pamantayan bago sila sumali. Maaari itong mabago anumang oras", + "membership-request-pending": "Nakabinbin ang kahilingan sa pagiging miyembro", + "membership-requests": "Mga kahilingan sa pagiging miyembro", + "membership-title": "Kahilingan sa pagiging kasapi ng pangkat", + "message": "Mensahe", + "message-not-sent": "Hindi ipinadala ang mensahe", + "message-options-cancel": "Pagkansela", + "message-reply": "Sagot", + "messages": "mensahe", + "might-break": "Maaaring masira ang ilang ÐApps", + "migrations-failed-content": "{{message}}\nschema version: paunang{{initial-version}},kasalukuyang{{current-version}},huli{{last-version}}\n\nNagkaroon ng isang error sa database. Ang iyong funds at chat key ay ligtas. Ang iba pang data, kagaya ng iyong chats at mga contacts, ay hindi maibabalik. \"{{erase-multiaccounts-data-button-text}}\" button, aalisin ang lahat ng iba pang data at magpapahintulot sa iyo na ma-access ang iyong mga pondo at magpadala ng mga mensahe.", + "mobile-network-ask-me": "Tanungin mo ako sa mobile network", + "mobile-network-continue-syncing": "Magpatuloy sa pag-sync", + "mobile-network-continue-syncing-details": "Maaari mong baguhin ito sa ibang pagkakataon sa mga setting", + "mobile-network-go-to-settings": "Pumunta sa mga setting", + "mobile-network-settings": "Mobile data", + "mobile-network-sheet-configure": "Maaari mong i-configure ang pag-sync sa higit pang\ndetalye sa", + "mobile-network-sheet-offline": "Walang Wi-fi, hindi pinagana ang pag-sync ng mensahe.", + "mobile-network-sheet-offline-details": "Ang pag-sync gamit ang mobile network ay naka-off", + "mobile-network-sheet-remember-choice": "Tandaan ang aking pinili", + "mobile-network-sheet-settings": "settings", + "mobile-network-start-syncing": "Simulan ang pag-sync", + "mobile-network-stop-syncing": "Itigil ang pag-sync", + "mobile-network-stop-syncing-details": "Hanggang sa konektado sa Wi-Fi?", + "mobile-network-use-mobile": "Gumamit ng mobile data", + "mobile-network-use-mobile-data": "Ang Status ay gumagamit ng maraming data kapag nag-sync sa mga chats at wallet.", + "mobile-network-use-wifi": "Wi-Fi lang", + "mobile-syncing-sheet-details": "Ang Status ay gumagamit ng maraming data kapag nag-syng sa chats at wallet.", + "mobile-syncing-sheet-title": "Pag-sync gamit ang mobile data?", + "more": "higit pa", + "move-and-reset": "Ilipat at I-reset", + "move-keystore-file": "Ilipat ang keystore file", + "move-keystore-file-to-keycard": "Ilipat ang keystore file sa keycard?", + "multiaccount-exists-content": "Ang Keys sa account na ito ay mayroon na at hindi maaaring idagdag muli. Kung nawala ang iyong password, passcode o Keycard, i-uninstall ang app, reinstall at i-access ang iyong key sa pamamagitan ng iyong seed phrase", + "multiaccount-exists-title": "Ang Keys sa account na ito ay mayroon na", + "multiaccounts-recover-enter-phrase-text": "Ilagay ang 12, 15, 18, 21 o 24 na mga salita.\nPaghiwalayin ang mga salita sa pamamagitan ng isang puwang.", + "multiaccounts-recover-enter-phrase-title": "Ilagay ang seed phrase", + "mute": "Mute", + "my-accounts": "Ang aking accounts", + "my-accounts-empty": "Lilitaw dito ang iyong mga magagamit na account", + "my-status": "Ang status ko", + "name": "Pangalan", + "name-of-token": "Ang pangalan ng iyong token", + "name-optional": "Pangalan (opsyonal)", + "name-your-channel": "Pangalanan ang iyong channel", + "name-your-channel-placeholder": "Pangalan ng channel", + "name-your-community": "Pangalanan ang iyong pamayanan", + "name-your-community-placeholder": "Isang kapansin pansin na pangalan", + "need-help": "Kailangan ng tulong?", + "network": "Network", + "network-chain": "Network chain", + "network-details": "Mga detalye ng network", + "network-fee": "Bayad sa network", + "network-id": "Network ID", + "network-info": "Impormasyon sa network", + "network-invalid-network-id": "Ang tinukoy na id ng network ay hindi tumutugma sa id ng network ng RPC url", + "network-invalid-status-code": "Hindi wastong katayuan ng status:{{code}}", + "network-invalid-url": "Hindi wasto ang URL ng Network", + "network-settings": "Mga setting ng network", + "new": "bago", + "new-chat": "Bagong chat", + "new-community-title": "Bagong komunidad", + "new-contact": "Bagong kontak", + "new-contract": "Bagong Kontrata", + "new-favourite": "Bagong paborito", + "new-group": "Bagong grupo", + "new-group-chat": "Panibagong group chat", + "new-network": "Bagong network", + "new-pin-description": "Ilagay ang baong 6-digit na passcode", + "new-public-group-chat": "Sumali sa pampublikong Chat", + "new-status": "Bagong Status", + "new-tab": "Bagong tab", + "next": "Sunod", + "nickname": "Palayaw", + "nickname-description": "Ang mga palayaw ay tinutulungan kang makilala ang iba sa Status. \n Ikaw lang ang makakakita ng mga palayaw na idinagdag mo", + "no": "Hindi", + "no-collectibles": "Walang magagamit na mga koleksyon", + "no-contacts": "Wala pang contact", + "no-keycard-applet-on-card": "Walang applet Keycard sa card", + "no-messages": "Walang mensahe", + "no-pairing-slots-available": "Ang kard na ito ay nakapares na sa 5 na aparato at hindi maaaring ipares sa isang ito. Mangyaring gumamit ng isa sa mga ipinares na aparato, mag-log in gamit ang card na ito at libre ang pagpapares ng mga puwang sa card", + "no-result": "walang resulta", + "no-tokens-found": "Walang nahanap na mga token", + "node-info": "Node info", + "node-version": "Bersyon ng nod", + "nodes-disabled": "Hindi pinapagana ang Status nodes", + "non-archival-node": "Ang RPC endpoint ay hindi suportado ng archival request. Ang iyong lokal na kasaysayan ng paglilipat ay maaring hindi makumpleto.", + "nonce": "Nonce", + "none": "Wala", + "not-applicable": "Hindi naaangkop para sa mga hindi naka -ignign na transaksyon", + "not-connected-nodes": "Hindi nakakonekta sa isang status node", + "not-connected-to-peers": "Hindi konektado sa anumang mga kapantay", + "not-enough-snt": "Hindi sapat ang iyong SNT", + "not-keycard-text": "Ang card na iyong ginamit ay hindi isang Keycard. Kailangan mong bumili ng isang Keycard upang magamit ito.", + "not-keycard-title": "Hindi isang Keycard", + "not-registered": "hindi rehistrado", + "notification-settings": "Mga Abiso", + "notifications": "Mga notification", + "notifications-non-contacts": "Mga notification mula sa mga hindi contact", + "notifications-preferences": "Mga kagustuhan sa abiso", + "notifications-servers": "Notification servers", + "notifications-switch": "Ipakita ang mga notification", + "notifications-transactions": "Mga transaksyon sa wallet", + "notify": "Ipaalam", + "now": "Ngayon", + "off": "Off", + "off-status-tree": "Off Status tree", + "offline": "Offline", + "offline-messaging-use-history-explanation": "Paganahin ang Status node upang kumuha ng mga mensahe na ipinadala habang ang app ay sarado. Kapag pinagana, ang status node ay kukunin ang iyong IP address. Kapag hindi pinagana hindi ka makakatanggap ng mga mensahe kapag ang app ay sarado at hindi makikita ang mga ito kapag binuksan mo ang app sa paglaon.", + "offline-messaging-use-history-nodes": "Gumamit ng Status nodes", + "ok": "OK", + "ok-continue": "Okay, magpatuloy", + "ok-got-it": "Okay, nakuha ko", + "ok-save-pass": "OK, i-save ang password", + "okay": "Okay", + "on": "On", + "on-status-tree": "Sa Puno ng Status", + "once-enabled-share-metadata": "Kapag pinagana, ang mga link na nai-post sa chat ay maaaring ibahagi ang iyong metadata sa site", + "open": "bukas", + "open-chat": "Buksan ang chat", + "open-dapp": "Buksan ang ÐApp", + "open-dapp-store": "Tuklasin ang ÐApps", + "open-home": "Buksan ...", + "open-in-new-tab": "Magbukas ng bagong tab", + "open-membership": "Bukas na miyembro", + "open-nfc-settings": "Buksan ang settings ng NFC", + "open-on-etherscan": "bukas ang Etherscan.io", + "opening-buy-crypto": "Pagbubukas{{site}}", + "optional": "opsyonal", + "or": "O", + "outgoing": "Papalabas", + "outgoing-transaction": "Papalabas na transaksyon", + "page-camera-request-blocked": "hinarangan ang mga kahilingan sa camera. Upang paganahin ang mga kahilingan sa camera pumunta sa Mga Setting", + "page-would-like-to-use-camera": "nais mo bang gamitin ang iyong camera", + "pair": "I-Pares ang aparato", + "pair-card": "Ipares sa aparatong ito", + "pair-code": "Pair codePares ng code", + "pair-code-explanation": "Pagpapares card sa ibang aparato (hanggang sa 5) upang i-unlock ang mga key at mag-sign ng mga transaksyon sa parehong Keycard", + "pair-code-placeholder": "pares ng code", + "pair-this-card": "Ipares ang card na ito", + "pair-this-device": "Mag-advertise ng aparato", + "pair-this-device-description": "Ipares ang iyong mga aparato upang i-sync ang mga contact at chat sa pagitan nila", + "paired-devices": "Mga aparato na ipinares", + "pairing": "Pagpapares", + "pairing-card": "Pagpapares card", + "pairing-go-to-installation": "Pumunta sa mga setting ng pagpapares", + "pairing-maximum-number-reached-content": "Mangyaring huwag paganahin ang isa sa iyong mga aparato bago paganahin ang bago.", + "pairing-maximum-number-reached-title": "Naabot ang maximum na bilang ng mga aparato", + "pairing-new-installation-detected-content": "Ang isang bagong aparato ay napansin.\nUpang magamit nang tama ang iyong mga aparato, mahalaga na ipares at paganahin ang mga ito bago gamitin.\nMangyaring pumunta sa seksyon ng aparato sa ilalim ng mga setting upang ipares ang iyong mga aparato.", + "pairing-new-installation-detected-title": "May nakitang bagong device\/aparato", + "pairing-no-info": "Walang impormasyon", + "pairing-please-set-a-name": "Mangyaring magtakda ng isang pangalan para sa iyong aparato.", + "passphrase": "Passphrase", + "password": "Password", + "password_error1": "Hindi tumutugma ang mga password.", + "password-description": "Ng hindi bababa sa 6 na mga character. Pinoprotektahan ng iyong password ang iyong mga susi. Kailangan mo ito upang i-unlock ang Status at lumipat.", + "password-placeholder": "Password...", + "password-placeholder2": "Kumpirmahin ang iyong password", + "paste": "Paste", + "paste-json": "Idikit JSON", + "pay-to-chat": "Magbayad sa chat", + "peer-content": "Ang isang aparato na konektado sa Status chat network. Ang bawat gumagamit ay maaaring kumatawan sa isa o higit pang mga kapantay depende sa kanilang bilang ng mga aparato.", + "peer-title": "Peer", + "peer-to-peer": "Peer to peer", + "peers": "Peers", + "pending": "Naghihintay", + "pending-confirmation": "Pending na kumpirmasyon ...", + "pending-invitations": "Nakabinbin ang mga kahilingan sa pagiging miyembro", + "permissions": "Pahintulot", + "phone-e164": "International 1", + "photos": "Litrato", + "photos-access-error": "Upang bigyan ang kinakailangang pahintulot ng mga larawan, mangyaring pumunta sa mga setting ng iyong system at tiyakin na ang Status >Napiling mga larawan.", + "pin-changed": "Binago ang 6-digit na passcode", + "pin-code": "6-digit passcode", + "pin-mismatch": "Maling passcode", + "pin-one-attempt": "isang pagtatangka", + "pin-one-attempt-blocked-after": "bago maharang ang iyong Keycard", + "pin-one-attempt-blocked-before": "Mag-ingat, mayroon ka lamang", + "pin-one-attempt-frozen-after": "bago mag-frozen ang iyong Keycard", + "pin-one-attempt-frozen-before": "Mag-ingat, mayroon ka lamang", + "pin-retries-left": "{{number}}para mag retry", + "preference": "Kagustuhan", + "preview-privacy": "I-preview ang mode ng privacy", + "previewing-may-share-metadata": "Ang pag-preview ng mga link mula sa mga website na ito ay maaaring ibahagi ang iyong metadata sa kanilang mga may-ari", + "privacy": "Pagkapribado", + "privacy-and-security": "Pagkapribado at seguridad", + "privacy-policy": "Patakaran sa pagkapribado", + "private-key": "Pribadong key", + "private-notifications": "Pribadong mga abiso", + "private-notifications-descr": "Aabisuhan ka ng Status tungkol sa mga bagong mensahe. Maaari mong mai-edit ang iyong mga kagustuhan sa notification mamaya sa mga setting.", + "processing": "Sandali lang", + "product-information": "Impormasyon sa Produkto", + "profile": "Profile", + "profile-deleted-content": "Matagumpay na tinanggal ang iyong profile", + "profile-deleted-title": "Natanggal ang Profile", + "profile-details": "Mga detalye ng profile", + "profile-pic-pick": "Pumili mula sa gallery", + "profile-pic-remove": "Alisin ang larawan", + "profile-pic-take": "Kunan ng litrato", + "public-chat": "Pampublikong chat", + "public-chat-description": "Sumali sa mga pampublikong chat para sa iyong mga interest! Kahit sino ay maaaring magsimula ng bago.", + "public-chats": "Mga pampublikong chat", + "public-group-status": "Pampubliko", + "public-group-topic": "Paksa", + "public-key": "Public key", + "puk-and-pairing-codes-displayed": "Ipinapakita ang mga PUK at pagpapares ng mga code", + "puk-code": "PUK code", + "puk-code-explanation": "Kung nakalimutan mo ang iyong 6-digit na passcode o hindi tama nang ipinasok nang 3 beses, kakailanganin mo ang code na ito upang i-unlock ang iyong card.", + "puk-mismatch": "Mali ang PUK code", + "push-failed-transaction": "Nabigo ang iyong transaksyon", + "push-failed-transaction-body": "{{value}} {{currency}} hanggang {{to}}", + "push-inbound-transaction": "Nakatanggap ka ng {{value}} {{currency}}", + "push-inbound-transaction-body": "Mula sa {{from}} hanggang sa {{to}}", + "push-notifications-server-enabled": "Pinagana ang server", + "push-notifications-servers": "Push notification servers", + "push-outbound-transaction": "Nagpadala ka ng {{value}} {{currency}}", + "push-outbound-transaction-body": "Mula sa {{from}} hanggang sa {{to}}", + "quiet-days": "{{quiet-days}}araw", + "quiet-hours": "{{quiet-hours}}oras", + "re-encrypt-key": "I-encrypt muli ang iyong mga keys", + "receive": "Tumanggap", + "receive-transaction": "Tumanggap ng transaksyon", + "recent": "Kamakailan", + "recent-empty": "Ang mga nagamit na addresses ay lalabas dito", + "recent-recipients": "Contacts", + "recently-used-stickers": "Ang mga kamakailang ginamit na sticker ay lilitaw dito", + "recipient": "Tagatanggap", + "recipient-code": "ilagay ang address ng tatanggap", + "recipient-code-placeholder": "0x.. o username.domain.eth", + "recover": "Bawiin", + "recover-key": "I-access ang umiiral na mga key", + "recover-keycard-multiaccount-not-supported": "Ang keys sa account na ito ay mayroon na at hindi na maaring idagdag muli. Kung nawala ang iyong password, passcode o Keycard, i-uninstall ang app, reinstall at i-access ang iyong key sa pamamagitan ng iyong seed phrase", + "recover-with-keycard": "I-recover gamit ang Keycard", + "recover-with-seed-phrase": "Bawiin gamit ang seed phrase", + "recovering-key": "Pag-access sa mga key ...", + "recovery-confirm-phrase": "kumpirmahin ang seed phrase", + "recovery-phrase": "Seed phrase", + "recovery-success-text": "Kailangan mong lumikha ng isang bagong code o password upang muling i-encrypt ang iyong mga key", + "recovery-typo-dialog-description": "Paalala, ang iyong seed phrase dapat eksaktong, parehong mga salita at pagkakasunud-sunod habang natanggap mo ito", + "recovery-typo-dialog-title": "ang seed phrase ay tama?", + "redeem-amount": "{{quantity}} makukuhang mga bonus", + "redeem-now": "Kunin ngayon", + "redeem-success": "Kunin ang tagumpay ng bonus!", + "refresh": "Refresh", + "registered": "rehistrado", + "remember-me": "Tandaan mo ako", + "remind-me-later": "Ipakita mo sa akin ito muli", + "remove": "Alisin", + "remove-favourite": "Mag-alis ng paborito", + "remove-from-chat": "Alisin sa chat", + "remove-from-contacts": "Tanggalin sa mga kontak", + "remove-from-contacts-text": "Sa pamamagitan ng pag-alis ng isang user sa iyong listahan ng kontak hindi mo ma itago ang iyong wallet address sa kanila.", + "remove-group": "Alisin ang pangkat", + "remove-network": "Alisin ang network", + "remove-token": "Alisin ang token", + "removed": "alisin", + "repeat-pin": "Ulitin ang bagong 6-digit na passcode", + "report-bug-email-template": "1. Paglalarawan ng Isyu\{{description}}\n\n\n2. Mga hakbang upang makalikha\n {{steps}}\n\n\n3. Ikabit ang mga screenshot na maaaring mag-demo sa problema, kung maaari sana", + "request-access": "Humiling ng pag-access", + "request-feature": "Humiling ng isang tampok", + "request-membership": "Humiling ng pagiging miyembro", + "request-pending": "Nakabinbin ang kahilingan…", + "request-transaction": "Humiling ng transaksyon", + "required-field": "Required field", + "resend-message": "I-resend", + "reset-card": "I-reset ang card", + "reset-card-description": "Ang operasyon na ito ay i-reset ang card sa paunang estado. Tatanggalin nito ang lahat ng data ng kard kasama ang mga pribadong key. Hindi maibabalik ang operasyon.", + "reset-database": "I-reset ang database", + "reset-database-warning": "Tanggalin ang mga chats, contacts at settings. Kinakailangan kapag nawala mo ang iyong password", + "restore-defaults": "Ibalik sa dating ayos", + "retry": "Ulitin", + "revoke-access": "Bawiin ang access", + "rpc-url": "RPC URL", + "save": "I-save", + "save-password": "I-save ang password", + "save-password-unavailable": "Itakda ang passcode ng aparato upang i-save ang password", + "save-password-unavailable-android": "Hindi magagamit ang pag-save ng password: ang iyong aparato ay maaaring ma-root o walang kinakailangang mga tampok ng seguridad.", + "scan-qr": "I-scan ang QR code", + "scan-qr-code": "I-scan ang isang QR code na may isang address ng pitaka", + "scan-tokens": "I-scan ang tokens", + "search": "Paghahanap", + "search-no-chat-found": "Walang resulta. Ang ibig mong sabihin ay", + "secret-keys-confirmation-text": "Kakailanganin mo ang mga ito upang magpatuloy na gamitin ang iyong Keycard kung sakaling mawala mo ang iyong telepono.", + "secret-keys-confirmation-title": "Nasulat mo na ang mga codes?", + "security": "Seguridad", + "see-details": "Tignan ang detalye", + "see-it-again": "Tingnan muli", + "seed-key-uid-mismatch": "Hindi tumutugma ang seed", + "seed-key-uid-mismatch-desc-1": "Ang seed phrase na ipinasok mo ay hindi tumutugma sa {{multiaccount-name}}", + "seed-key-uid-mismatch-desc-2": "Upang maisagawa ang mga keys para sa account na ito, i-verify ang iyong seed phrase at subukang muli.", + "seed-phrase-content": "Isang hanay ng mga friendly-to-read na salita, at sapalarang napili mula sa BIP39 ay karaniwang listahan at ginagamit para ma access ang iyong Ethereum account sa iba pang wallets at devices. Tinukoy din bilang a “mnemonic phrase,” “recovery phrase” o “wallet backup” sa buong crypto ecosystem. Karamihan sa crypto apps ginagamit ang parehong standard to generate accounts.", + "seed-phrase-placeholder": "Seed phrase...", + "seed-phrase-title": "Seed Phrase", + "select": "Piliin", + "select-account": "Piliin ang account", + "select-account-dapp": "Piliin ang account na nais mong gamitin sa mga Dapp", + "select-account-first": "Piliin muna ang isang account", + "select-chat": "Piliin ang chat upang simulan ang pagmemensahe", + "select-new-location-for-keys": "Pumili ng isang bagong lokasyon upang i-save ang iyong (mga) pribadong keys", + "selected": "Napili", + "send-logs": "I-report ang bug", + "send-logs-to": "Mag-ulat ng isang bug sa{{email}}", + "send-message": "Magpadala ng Mensahe", + "send-push-notifications-description": "Kapag naka disabled, ang taong tumatanggap ng iyong mga mensahe ay hindi aabisuhan tungkol sa pagdating nito.", + "send-request": "Magpadala ng kahilingan", + "send-request-amount": "Halaga", + "send-request-amount-max-decimals": "Ang Max Number ng desimal ay{{asset-decimals}}", + "send-request-unknown-token": "Hindi kilalang token -{{asset}}", + "send-sending-to": "para kay{{recipient-name}}", + "send-transaction": "Magpadala ng transaksyon", + "sending": "Nagpapadala", + "sent-at": "Ipinadala sa", + "server": "Server", + "set-a-topic": "gumawa ng topic", + "set-currency": "Itakda ang pera", + "set-custom-fee": "Itakda ang pasadyang bayad", + "set-dapp-access-permissions": "Itakda ang mga pahintulot sa pag-access ng DApp", + "set-max": "Itakda ang max", + "settings": "Settings", + "share": "Ibahagi", + "share-address": "Share address", + "share-chat": "Ibahagi ang chat", + "share-contact-code": "Ibahagi ang aking susi sa chat", + "share-dapp-text": "Tingnan ang DApp na ginagamit ko sa Status:{{link}}", + "share-link": "Ibahagi ang Link", + "share-my-profile": "Ibahagi ang link sa profile", + "share-profile": "Ibahagi ang profile", + "share-profile-link": "Ibahagi ang link sa profile", + "share-public-chat-text": "Tingnan ang pampublikong chat na ito sa Status app:{{link}}", + "shared": "Ibinahagi", + "sharing-copied-to-clipboard": "Kinopya", + "sharing-copy-to-clipboard": "Kopya", + "sharing-share": "Ibahagi", + "show-less": "Magpakita ng mas kaunti", + "show-more": "Magpakita pa", + "show-notifications": "Ipakita ang mga notification", + "show-profile-pictures": "Ipakita ang mga larawan sa profile ng", + "show-qr": "Ipakita ang QR code", + "show-transaction-data": "Ipakita ang data ng transaksyon", + "sign-and-send": "Mag-sign at ipadala", + "sign-anyway": "Mag-sign pa rin", + "sign-in": "mag sign-in", + "sign-message": "Mag-sign Message", + "sign-out": "Mag-sign out", + "sign-request-failed": "Hindi ma-sign na mensahe", + "sign-with": "Mag-sign in", + "sign-with-password": "Mag-sign in gamit ang password", + "sign-you-in": "Nag-sign in ka ...", + "signing": "Pag-sign", + "signing-a-message": "Pag-sign ng isang mensahe", + "signing-phrase": "Pag sign ng phrase", + "something-went-wrong": "May nangyari na mali", + "soon": "Sa lalong madaling panahon", + "specify-address": "Tukuyin ang address", + "specify-name": "Tukuyin ang isang pangalan", + "specify-network-id": "Tukuyin ang id ng network", + "specify-rpc-url": "Tukuyin ang isang URL ng RPC", + "specify-server-public-key": "Ilagay ang server public key", + "start-chat": "Simulan ang chat", + "start-conversation": "Simulan ang pag-uusap", + "start-group-chat": "Simulan ang group chat", + "start-new-chat": "Magsimula ng bagong chat", + "starter-pack-coming": "Darating sa inyo ang starter pack", + "starter-pack-coming-description": "Maaaring tumagal ng ilang minuto hanggang oras", + "starter-pack-received": "Natanggap ang Starter Pack", + "starter-pack-received-description": "Narito ang ilang mga crypto upang makapagsimula ka! Gamitin ito upang makakuha ng mga sticker, isang pangalan ng ENS at subukan ang mga dapps", + "status": "Status", + "status-confirmed": "Nakumpirma", + "status-hardwallet": "Status hardwallet", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Ang Status ay may kaugaliang gumamit ng maraming data kapag nagsi-sync ng mga chat. Maaari mong piliing hindi mag-sync kapag nasa mobile network", + "status-not-sent-click": "Hindi kumpirmado. Mag-click para sa mga pagpipilian", + "status-not-sent-tap": "Hindi kumpirmado. I-tap para sa mga pagpipilian", + "status-pending": "Naghihintay", + "status-sent": "Ipinadala", + "status-tx-not-found": "Hindi nahanap ang TX", + "status-updates-descr": "Ang Status updates ay lalabas dito. Idagdag ang profile bilang isang contact upang makatanggap ng mga update sa iyong timeline.", + "statuses-descr": "Ibahagi kung ano ang nasa isip mo at manatiling nai-update sa iyong mga contact", + "statuses-my-status-descr": "Ibahagi kung ano ang nasa isip mo. Ang sinumang bumibisita sa iyong profile ay makakakita ng iyong kalagayan. Ang mga taong in-add ka bilang kanilang contact ay makakatanggap ng iyong mga update sa kanilang timeline", + "step-i-of-n": "gabay{{step}}sa{{number}}", + "sticker-market": "Sticker market", + "storage": "Imbakan", + "submit": "Ipasa", + "submit-bug": "Magsumite ng isang bug", + "success": "Tagumpay", + "symbol": "Simbolo", + "sync-all-devices": "Sync lahat ng aparato", + "sync-in-progress": "Syncing...", + "sync-settings": "Mga setting ng pag-sync", + "sync-synced": "Sa pag-sync", + "syncing-devices": "Syncing...", + "system": "System", + "tabs": "tabs", + "tag-was-lost": "nawala ang tag", + "tap-card-again": "Tapikin muli ang card sa likod ng iyong telepono", + "test-networks": "Mga network ng pagsubok", + "text-input-disabled": "Mangyaring maghintay sandali ...", + "this-device": "Ang aparato na ito", + "this-device-desc": "Ang iyong mga keys ay naka-encrypt at ligtas na nakaimbak sa iyong aparato", + "this-is-you-signing": "Ito ang iyong signing phrase", + "this-will-take-few-seconds": "Ito ay tatagal ng ilang segundo", + "three-words-description": "Dapat mong makita ang mga 3 salita bago mag-sign sa bawat transaksyon", + "three-words-description-2": "Kung nakakita ka ng ibang kombinasyon, kanselahin ang transaksyon at mag-sign out", + "timeline": "Timeline", + "to": "Para kay", + "to-block": "Block", + "to-enable-biometric": "Para paganahin{{bio-type-label}} kailangan mong i-save ang iyong password para sa unlock screen", + "to-encrypt-enter-password": "Para ma encrypt ang account ilagay ang iyong password", + "to-see-this-message": "Upang makita ang mensaheng ito,", + "token-auto-validate-decimals-error": "Maling mga decimals para sa token {{symbol}} sa address {{address}} - itakda sa {{expected}} ngunit nakita bilang {{actual}}", + "token-auto-validate-name-error": "Maling pangalan para sa token {{symbol}} sa address {{address}} - itakda sa {{expected}} ngunit nakita bilang {{actual}}", + "token-auto-validate-symbol-error": "Maling simbolo para sa token{{symbol}}sa address{{address}}- itakda sa{{expected}}ngunit nakita bilang{{actual}}", + "token-details": "detalye ng token", + "topic-name-error": "Gumamit lamang ng mga maliliit na titik (a to z), numbero at dashes (-) Huwag gumamit ng chat keys", + "transaction": "Transaksyon", + "transaction-data": "Data ng transaksyon", + "transaction-declined": "Hindi tinanggap ang transaksyon", + "transaction-description": "Isaalang-alang itong kumpleto pagkatapos ng 12 kumpirmasyon sa network.", + "transaction-details": "Mga detalye ng transaksyon", + "transaction-failed": "Nabigo ang transaksyon", + "transaction-history": "Kasaysayan ng Transaksyon", + "transaction-request": "Kahilingan sa Transaksyon", + "transaction-sent": "Ipinadala ang transaksyon", + "transaction-signed": "Ang transaksyon ay matagumpay na naka-sign", + "transactions": "Transaksyon", + "transactions-filter-select-all": "Piliin lahat", + "transactions-filter-title": "Filter ng kasaysayan", + "transactions-history": "Kasaysayan ng Transaksyon", + "transactions-history-empty": "Wala pang transaksyon sa iyong kasaysayan", + "transactions-history-loading": "Naglo-load ng kasaysayan ng transaksyon. Maaaring tumagal ito ng ilang sandali.", + "transactions-load-more": "Mag-load pa", + "transactions-sign": "Sign", + "transfer-ma-unknown-error-desc-1": "Mukhang hindi tinanggal ang iyong multiaccount. Maaaring na-reset ang database", + "transfer-ma-unknown-error-desc-2": "Mangyaring suriin ang iyong listahan ng account at subukang muli. Kung ang account ay hindi nakalista pumunta sa Access existing keys upang mabawi gamit ang seed phrase", + "transfers-fetching-failure": "Hindi ma-update ang kasaysayan ng paglilipat. Suriin ang iyong koneksyon at hilahin pababa upang subukang muli", + "tribute-required-by-multiaccount": "{{multiaccount-name}}nangangailangan ng SNT para magsimula ng isang chat.", + "tribute-state-paid": "Bayad ang nai-ambag", + "tribute-state-pending": "Pending na tribute", + "tribute-state-required": "Nangangailangan{{snt-amount}}SNT tribute", + "tribute-to-talk": "Tribute to talk", + "tribute-to-talk-add-friends": "Magdagdag ng mga kaibigan bilang isang contact upang payagan ang mga chat nang walang bayad sa pagkilala.", + "tribute-to-talk-are-you-friends": "Magkaibigan ba kayo?", + "tribute-to-talk-ask-to-be-added": "Hilingin na maidagdag bilang isang contact", + "tribute-to-talk-contact-received-your-tribute": "natanggap ang iyong parangal. Maaari ka nang ligtas na makipag-chat sa bawat isa.", + "tribute-to-talk-desc": "I-monetize iyong pansin sa pamamagitan ng pag-uutos sa SNT para sa mga bagong tao upang magsimula ng isang chat", + "tribute-to-talk-disabled": "Ang Tribute to Talk ay di pinagana", + "tribute-to-talk-disabled-note": "Simula ngayon, ang mga baguhan ay maaaring magsimula ng isang chat sa iyo nang hindi nagpapadala ng SNT.", + "tribute-to-talk-enabled": "Pinapagana mo ang Tribute to Talk", + "tribute-to-talk-finish-desc": "Mula ngayon, makakatanggap ka lamang ng mga chat mula sa mga contact, at mga taong nagbayad", + "tribute-to-talk-learn-more-1": "Ang iyong oras at atensyon ay ang iyong pinakamahalagang pag-aari. Hinahayaan ka ng Tribute to Talk na magtakda ka ng isang halaga ng SNT na kinakailangan para sa mga bagong tao upang magsimula ng isang chat sa iyo.", + "tribute-to-talk-learn-more-2": "Ang sinumang wala sa iyong listahan ng contact ay hihilingin na bayaran, at maaari kang tumugon sa sandaling mayroon sila.", + "tribute-to-talk-learn-more-3": "Maaari mong palaging maibalik ang pera, ngunit upang matiyak na ang mga kaibigan ay maaaring maabot mo nang malayang, idagdag ang mga ito bilang isang contact muna.", + "tribute-to-talk-paywall-learn-more-1": "Ang aming oras at pansin ay ang aming pinakamahalagang pag-aari. Hinahayaan ka ng Tribute to Talk na makipag-ugnay sa mga bagong tao kapalit ng pagbabayad sa SNT.", + "tribute-to-talk-paywall-learn-more-2": "Para magsimula ng isang chat sa isang taong may set ng pagkilala, bayaran lamang ang kinakailangang SNT at idadagdag ka bilang isang contact.", + "tribute-to-talk-paywall-learn-more-3": "Kung kilala mo ang mga ito, maaari mong ibahagi ang iyong profile sa labas ng Status na idaragdag nang libre.", + "tribute-to-talk-pending": "nakabinbing kumpirmasyon ng Tribute", + "tribute-to-talk-pending-note": "Tribute transaction ay nakabinbing kumpirmasyon sa network. Maaari mong suriin ito sa Status sa kasaysayan ng transaksyon", + "tribute-to-talk-removing-note": "Ang pag-alis ng Tribute to tak ay papayagan ang mga baguhan na magsimula ng isang chat nang hindi nagpapadala ng SNT. Nangangailangan ng isang transaksyon na gagawin.", + "tribute-to-talk-set-snt-amount": "Itakda ang halaga ng SNT na kinakailangan para sa mga bagong tao upang magsimula ng isang chat", + "tribute-to-talk-signing": "Naghihintay na mag-sign transaksyon", + "tribute-to-talk-transaction-failed-note": "Nabigo ang transaksyon at ang iyong mga setting ng Tribute to Talk ay hindi nabago", + "tribute-to-talk-tribute-received1": "Natanggap ang ambag. Ikaw at", + "tribute-to-talk-tribute-received2": "ngayon ay mga contact at maaaring ligtas na makipag-chat sa bawat isa.", + "tribute-to-talk-you-require-snt": "Kailangan mo ng SNT para sa mga bagong tao na magsimula ng isang chat.", + "try-again": "Subukan muli", + "try-keeping-the-card-still": "Subukang panatilihin pa rin ang card", + "turn-nfc-description": "NFC ay hindi pinagana sa iyong aparato. Maaari mo itong paganahin sa mga setting", + "turn-nfc-on": "I-on ang NFC upang magpatuloy", + "tx-fail-description1": "Ang transaksyon na ito ay malamang na mabigo. Mag-sign sa iyong sariling panganib gamit ang pasadyang bayad sa network.", + "tx-fail-description2": "Ang transaksyon na ito ay malamang na mabigo. Magtakda ng isang pasadyang bayad sa network upang mag-sign sa iyong sariling peligro.", + "type": "Uri", + "type-a-message": "Mensahe", + "ulc-enabled": "Pinapagana ang ULC", + "unable-to-fetch": "Hindi makuha ang kasaysayan ng chat", + "unable-to-read-this-code": "Hindi mabasa ang code na ito", + "unable-to-send-messages": "Hindi makapadala at makatanggap ng mga mensahe", + "unblock": "I-unblock", + "unblock-contact": "I-unblock ang gumagamit na ito", + "unknown-status-go-error": "hindi kinikilala status-go error", + "unlock": "I-unlock", + "unmute": "Unmute", + "unpair-card": "Ang kawalan ng pag-asa card", + "unpair-card-confirmation": "Ang operasyon na ito ay mawawalan ng pag-asa ang card mula sa kasalukuyang aparato. Nangangailangan ng pahintulot ng 6-digit na passcode. Gusto mo bang magpatuloy?", + "unpaired-keycard-text": "Ang Keycard na iyong tinapik ay hindi nauugnay sa teleponong ito", + "unpaired-keycard-title": "Mukhang hindi naipares ang iyong card", + "update": "Update", + "update-to-listen-audio": "I-update sa pinakabagong bersyon upang makinig sa isang audio message dito!", + "update-to-see-image": "I-update sa pinakabagong bersyon upang makita ang isang magandang imahe dito!", + "update-to-see-sticker": "I-update sa pinakabagong bersyon upang makita ang isang magandang sticker dito!", + "url": "URL", + "usd-currency": "USD", + "use-valid-contact-code": "Mangyaring mag-enter o mag-scan ng isang wastong chat key o username", + "user-not-found": "Hindi nahanap ang gumagamit", + "validation-amount-invalid-number": "Ang halaga ay hindi isang wastong numero", + "validation-amount-is-too-precise": "Masyadong tumpak ang halaga. Sobrang bilang ng mga decimals ay{{decimals}}", + "version": "Bersyon ng app", + "view": "Tingnan", + "view-cryptokitties": "Tingnan sa CryptoKitties", + "view-cryptostrikers": "Tingnan sa CryptoStrikers", + "view-details": "Tingnan ang mga detalye", + "view-etheremon": "Tingnan sa Etheremon", + "view-gitcoin": "Tingnan sa Gitcoin", + "view-profile": "Tingnan ang Profile", + "view-signing": "Tingnan ang signing phrase", + "view-superrare": "Tingnan sa SuperRare", + "waiting-for-wifi": "Walang Wi-fi, hindi pinagana ang pag-sync ng mensahe.", + "waiting-for-wifi-change": "Mga settings", + "waiting-to-sign": "Naghihintay na mag-sign ng transaksyon ...", + "waiting-wi-fi": "Naghihintay para sa Wi-Fi…", + "waku-bloom-filter-mode": "Waku Bloom mode ng filter", + "wallet": "Pitaka", + "wallet-address": "Address ng Wallet", + "wallet-asset": "Asset", + "wallet-assets": "Assets", + "wallet-backup-recovery-title": "Back up mo ang seed phrase", + "wallet-choose-recipient": "Piliin ang Tumatanggap", + "wallet-collectibles": "Mga Kolektibo", + "wallet-insufficient-funds": "Hindi sapat na pondo", + "wallet-insufficient-gas": "hindi sapat ang gas ng ETH mo", + "wallet-invalid-address": "Di-wastong address::\n{{data}}", + "wallet-invalid-address-checksum": "Error sa address:\n{{data}}", + "wallet-invalid-chain-id": "Hindi tumutugma ang network:\n{{data}}ngunit ang kasalukuyang chain ay{{chain}}", + "wallet-key-content": "Ang isang 64 character na hex address batay sa pamantayan ng Ethereum at nagsisimula sa 0x. na Humaharap sa publiko, ang iyong account address ay na maibahagi sa iba kung gusto mo matanggap ang pondo. Tinukoy din bilang isang \"Ethereum address\" o \"wallet address.\"", + "wallet-key-title": "Address ng account", + "wallet-manage-assets": "Pamahalaan ang mga assets", + "wallet-request": "Kahilingan", + "wallet-send": "Ipadala", + "wallet-send-min-units": "Min 21000 units", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Settings ng Pitaka", + "wallet-total-value": "Kabuuang halaga", + "wallet-transaction-total-fee": "Kabuuang Bayad", + "wants-to-access-profile": "nais na ma-access sa iyong profile", + "warning": "Warning", + "warning-message": "Paumanhin, nililimitahan namin ang pagpapadala ng maraming mga mensahe nang mabilis na magkakasunod upang maiwasan ang spam. Mangyaring subukang muli sa isang sandali", + "warning-sending-to-contract-descr": "Ang address na iyong pinasok ay isang smart contract, magpadala ng pondo sa address na ito ay pwedeng magresulta ng pagkawala ng mga pondo. Makipag-ugnay sa DApp, bukas ang DApp sa anumang Status DApp Browser.", + "watch-only": "panoorin lamang", + "web-view-error": "Hindi ma-load ang pahina", + "websites": "Websites", + "webview-camera-permission-requests": "Mga kahilingan sa pahintulot sa Webview camera", + "webview-camera-permission-requests-subtitle": "Kapag pinagana, ang mga website at dapps ay maaaring hilingin na gamitin ang iyong camera", + "welcome-blank-community-message": "Lilitaw ang iyong mga komunidad dito.", + "welcome-blank-message": "Ang iyong mga chat ay lilitaw dito. Upang simulan ang mga bagong chat pindutin ang button ⊕", + "welcome-community-blank-message": "Lilitaw ang iyong mga chat dito. Para simulan ang mga bagong pag-chat mag-click sa 3 mga tuldok sa itaas at piliin ang \"Lumikha ng isang channel\"", + "welcome-screen-text": "I-set up ang iyong wallet, anyayahan ang mga kaibigan na mag-chat\nat mag-browse ng mga sikat na dapps!", + "welcome-to-status": "Maligayang pagdating sa Status!", + "welcome-to-status-description": "I-set up ang iyong crypto wallet, anyayahan ang mga kaibigan na mag-chat at mag-browse sa mga desentralisadong apps", + "whats-on-your-mind": "Ano ang nasa isip mo…", + "word-count": "Bilang ng salita", + "word-n": "Word #{{number}}", + "word-n-description": "Upang masuri kung nai-back up ang iyong seed phrase ng tama, ilagay ang word#{{number}}sa itaas.", + "words-n": { + "zero": "", + "one": "Isang salita", + "two": "", + "few": "", + "many": "", + "other": "mga salita" + }, + "write-down-and-store-securely": "Isulat ang mga code\nat itago sila ng ligtas", + "wrong-address": "Maling address", + "wrong-card": "maling card", + "wrong-card-text": "Nag-tap ka ng isang kard na hindi tumutugma sa mga keys na iyong napili", + "wrong-contract": "Maling contract", + "wrong-keycard-text": "Ang Keycard na iyong tinapik ay hindi nauugnay sa teleponong ito", + "wrong-keycard-title": "Mukhang naka-tap ka ng\nisang maling keycard", + "wrong-password": "Maling password", + "wrong-word": "Maling salita", + "yes": "Oo", + "You": "ikaw", + "you": "ikaw", + "you-already-have-an-asset": "Mayroon ka nang isang asset na {{value}}", + "you-are-all-set": "Handa ka na!", + "you-are-all-set-description": "Kung mawala ang iyong telepono, maaari mo na ngayong ma-access ang iyong mga pondo at chat key gamit ang iyong seed phrase", + "you-can-change-account": "Maaari mong baguhin ang pangalan at kulay ng account sa nais mo", + "you-can-choose-preview-websites": "Maaari kang pumili kung alin sa mga sumusunod na website ang maaaring mag-preview ng link ng mga paglalarawan at larawan sa mga chat", + "you-can-fetch": "Maaari mong makuha ang kasaysayan ng chat", + "you-dont-have-contacts": "Wala ka pang mga contact.", + "you-dont-have-stickers": "Wala ka pang sticker", + "you-will-need-this-code": "Kakailanganin mo ang code na ito upang buksan ang Status at mga transaksyon sa pag-sign", + "you-will-start-from-scratch": "Magsisimula ka mula sa simula ng isang bagong hanay", + "your-card-is-frozen": "Ang iyong Keycard ay naka frozen. I-reset ang card access", + "your-contact-code": "Pinapahintulutan ng pagbibigay ng pag-access ang DApp na makuha ang iyong susi sa chat", + "your-data-belongs-to-you": "Kung mawala mo ang iyong seed phrase mawawala mo rin, ang iyong data at pondo", + "your-data-belongs-to-you-description": "Kung nawalan ka ng pag-access, halimbawa sa pagkawala ng iyong telepono, maaari mo lamang ma-access ang iyong mga key gamit ang iyong seed phrase. Walang sinuman dapat nakaalam ng iyong seed phrase kung hindi ikaw lamang, kung mangyaring Isulat mo at Panatilihing ligtas", + "your-keys": "Ang iyong susi", + "your-recovery-phrase": "Ang iyong seed phrase", + "your-recovery-phrase-description": "ito ang iyong seed phrase. Ginagamit mo ito upang patunayan na ito ang iyong pitaka. ikaw lamang makita ito nang isang beses! Isulat ito sa papel at itago ito sa isang ligtas na lugar. Kakailanganin mo ito kung nawala o muling i-install ang iyong pitaka.", + "youre-on-mobile-network": "Ikaw ay nasa mobile network" +} diff --git a/translations/tr.json b/translations/tr.json new file mode 100644 index 00000000000..662b03a5cd6 --- /dev/null +++ b/translations/tr.json @@ -0,0 +1,1688 @@ +{ + "You": "Sen", + "about-app": "Hakkında", + "about-key-storage-content": "Status hiçbir zaman özel anahtarınıza erişmez. Kelime imzanızı yedeklediğinizden emin olun. Telefonunuzu kaybederseniz, anahtarlarınıza erişmenin tek yolu budur.", + "about-key-storage-title": "Anahtar saklama hakkında", + "about-names-content": "Kimse senmişsin gibi davranamaz! Varsayılan olarak anonimsiniz ve asla gerçek adınızı açıklamanız gerekmez. Küçük bir ücret karşılığında özel bir ad kaydedebilirsiniz.", + "about-names-title": "İsimler değiştirilemez", + "about-sharing-data": "Veri paylaşımı hakkında", + "accept": "Kabul et", + "accept-and-add": "Kabul et ve ekle", + "accept-and-continue": "Kabul et ve devam et", + "accept-and-share-address": "Adresi kabul et ve paylaş", + "accept-new-chats-from": "Yeni sohbetleri kabul et", + "accept-status-tos-prefix": "Statusu kabul ediyorum", + "access-existing-keys": "Mevcut anahtarlara erişin", + "access-key": "Erişim anahtarı", + "account-added": "Hesap eklendi", + "account-color": "Hesap rengi", + "account-content": "Status'daki hesapları banka hesaplarıyla karşılaştırabilirsiniz.Bir hesabın genellikle bir adresi ve bir bakiyesi vardır; Bu hesabı Ethereum üzerinde işlem yapmak için kullanırsınız. Cüzdanınızda birden fazla hesabınız olabilir. Tümüne Status kilidini açarak erişebilirsiniz.", + "account-exists-title": "Bu Hesap Zaten Mevcut", + "account-is-used": "Hesap, tarayıcıda Dapps ile kullanılıyor.", + "account-name": "Hesap adı", + "account-settings": "Hesap ayarları", + "account-title": "Hesap", + "accounts": "Hesaplar", + "actions": "Hareketler", + "active-online": "Aktif", + "active-unknown": "Bilinmeyen", + "activity": "Aktivite", + "add": "Ekle", + "add-a-watch-account": "Yalnızca izleme adresi ekleyin", + "add-account": "Bir hesap ekle", + "add-account-description": "Status cüzdanınıza eklemek için herhangi bir Ethereum hesabını içe aktarabilirsiniz.", + "add-account-incorrect-password": "Şifre yanlış görünüyor. Uygulamanın kilidini açmak için kullandığınız şifreyi girin.", + "add-an-account": "Bir hesap ekle", + "add-bootnode": "Bootnode ekle", + "add-contact": "Kişi ekle", + "add-custom-token": "Özel token ekle", + "add-favourite": "Favori ekle", + "add-mailserver": "Status nodesi ekle", + "add-members": "Üye ekle", + "add-network": "Ağ ekle", + "add-new-contact": "Yeni kişi ekle", + "add-nickname": "Bir takma ad ekleyin (isteğe bağlı)", + "add-node": "Node ekle", + "add-private-key-account": "Özel Anahtarı Hesabına Ekle", + "add-seed-account": "Kelime imzasıyla hesap ekle", + "add-to-contacts": "Rehbere ekle", + "add-to-contacts-text": "Kişi listenize bir kullanıcı ekleyerek cüzdan adresinizi paylaşabilirsiniz.", + "add-to-favourites": "Favorilere ekle", + "add-watch-account": "Yalnızca izlenen bir hesap ekleyin", + "address": "Adres", + "address-or-ens-name": "Adres veya ENS adı", + "address-received": "Adres alındı", + "address-request-accepted": "Adres isteği kabul edildi", + "address-requested": "Adrese talep edildi.", + "advanced": "Gelişmiş", + "advanced-settings": "Gelişmiş Ayarlar", + "advertiser-description": "Bir ortak sayesinde Statusu keşfettiniz. Durumun IP adresinizi bir kez kontrol etmesini ve böylece ödüllendirilmelerinin bir sakıncası var mı? Bu bilgiler başka hiçbir amaçla kullanılmayacak ve 7 gün sonra tamamen silinecektir.", + "advertiser-starter-pack-accept": "Kabul et", + "advertiser-starter-pack-decline": "Reddet", + "advertiser-starter-pack-description": "İşte başlamanız için bazı kriptolar! Çıkartmalar, bir ENS adı almak ve dapp'leri denemek için kullanın", + "advertiser-starter-pack-title": "Başlangıç paketi", + "advertiser-title": "Varsayılan olarak gizlilik", + "agree-by-continuing": "Devam ederek kabül etmiş sayılırsınız", + "all": "Hepsi", + "allow": "İzin ver", + "allow-and-send": "İzin ver ve gönder", + "allow-mention-notifications": "@ bahsetmeyi göster", + "allowing-authorizes-this-dapp": "Bu DApp'e cüzdan adresinizi alma ve Web3'ü etkinleştirme yetkisi verin.", + "already-have-asset": "Bu öğeye zaten sahipsiniz", + "amount": "Miktar", + "anyone": "Kimse", + "appearance": "Görünüm", + "apply": "Uygula", + "approve": "Kabul et", + "are-you-sure": "Emin misiniz?", + "are-you-sure-description": "kelime imzanızın tamamını bir daha göremeyeceksiniz", + "are-you-sure-to-cancel": "İptal etmek istediğinden emin misin?", + "are-you-sure?": "Emin misiniz?", + "ask-in-status": "Soru sorun veya hata bildirin", + "at": "üzere", + "attribution-received": "{{attrib}}dışında {{max}}bonuslar alındı.", + "audio": "Ses", + "audio-recorder": "Kaydedici", + "audio-recorder-error": "Kaydedici hatası", + "audio-recorder-max-ms-reached": "Maksimum kayıt süresine ulaşıldı", + "audio-recorder-permissions-error": "Sesli mesaj gönderme izni vermelisiniz", + "authorize": "Yetkilendir", + "available": "Mevcut", + "available-participants": { + "one": "Bir katılımcı daha seçebilirsiniz", + "other": "Daha fazla {{Count}} katılımcı seçebilirsiniz" + }, + "back": "Geri", + "back-up": "Yedekleme", + "back-up-seed-phrase": "Kelime imzanızı yedekleyin", + "back-up-your-seed-phrase": "İmza kelimenizi yedekleyin", + "backing-up": "Yedekleme...", + "backup-disabled": "Devre dışı", + "backup-enabled": "Etkinleştirilmiş", + "backup-recovery-phrase": "Kelime imzalarınızı yedekleyin.", + "backup-settings": "Yedekleme ayarları", + "backup-through-waku": "waku aracılığıyla yedekle", + "bad-fees-description": "Öncelik ücretiniz önerilen parametrelerimizin altındadır.", + "balance": "Bakiye", + "begin-set-up": "Kuruluma başlayın", + "biometric-auth-android-sensor-desc": "Dokunmatik sensörü", + "biometric-auth-android-sensor-error-desc": "Başarısız", + "biometric-auth-android-title": "Kimlik Doğrulaması Gerekiyor", + "biometric-auth-confirm-logout": "Tekrar giriş yapın", + "biometric-auth-confirm-message": "Devam etmek için biyometrik kimlik doğrulaması gerekiyor, eğer mümkün değilse, lütfen şifrenizi kullanarak anahtarlarınızın kilidini açın", + "biometric-auth-confirm-title": "Kimlik doğrulaması yapmalısınız!", + "biometric-auth-confirm-try-again": "Tekrar deneyin", + "biometric-auth-error": "Biyometrik kimlik doğrulaması gerçekleştirilemiyor ( {{code}} )", + "biometric-auth-login-error-title": "Biyometrik kimlik doğrulama hatası", + "biometric-auth-login-ios-fallback-label": "Şifrenizi girin", + "biometric-auth-reason-login": "Status'ta Oturum Aç", + "biometric-auth-reason-verify": "Kimlik doğrulamasını doğrulayın", + "biometric-disable-bioauth": "devre dışı {{bio-type-label}}", + "biometric-disable-password-description": "Bunu devre dışı bırakırsanız", + "biometric-disable-password-title": "Şifre kaydetmeyi devre dışı bırak", + "biometric-enable": "Uygulamayı her hesap açtığınızda şifre girmek istemiyorsanız, {{bio-type-label}} ile oturum açmayı etkinleştirin", + "biometric-enable-button": "{{bio-type-label}} etkinleştir", + "biometric-enable-keycard": "Uygulamaya erişmek için Keycard'ınızı her seferinde kullanmak istemiyorsanız, {{bio-type-label}} oturum açmayı etkinleştirin.", + "biometric-faceid": "Yüz Kimliği", + "biometric-fingerprint": "Parmak izi", + "biometric-secure-with": "{{bio-type-label}} ile güvenli", + "biometric-touchid": "Dokunmatik kimlik", + "bip39-password-placeholder": "BIP39 şifresi", + "blank-keycard-text": "Anahtarlarınızı ve adınızı oluşturduktan sonra Keycard ile devam edebilirsiniz", + "blank-keycard-title": "Dokunduğunuz anlaşılıyor \nBoş bir Keycard", + "block": "Engelle", + "block-contact": "Bu kullanıcıyı engelle", + "block-contact-details": "Engelleme, bu kullanıcının önceki iletilerini siler ve yenilerinin size ulaşmasını engeller", + "blocked-users": "Engellenmiş kullanıcılar", + "bootnode-address": "Bootnode adresi", + "bootnode-details": "Bootnode detayları", + "bootnode-format": "enode: \/\/ {enode-id} @ {ip-address} : {port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes etkin", + "bootnodes-settings": "Bootnodes ayarları", + "browsed-websites": "Tarayıcı geçmişiniz burada görüntülenir", + "browser": "Tarayıcı", + "browser-not-secure": "Bağlantı güvenli değil! Bu sitede işlem imzalamayın veya kişisel veri göndermeyin.", + "browser-secure": "Bağlantı güvenli. İşlemleri imzalamadan veya kişisel verilerizi girmeden önce bu siteye gerçekten güvendiğinizden emin olun.", + "browsers": "Tarayıcılar", + "browsing-cancel": "İptal", + "browsing-open-in-android-web-browser": "Android'de aç", + "browsing-open-in-ios-web-browser": "İOS'ta aç", + "browsing-open-in-status": "Statusda aç", + "browsing-site-blocked-description1": "Bu adresten potansiyel kötü amaçlı etkinlik tespit ettik. Sizi ve cüzdanınızı korumak için daha fazla gezinmenizi engelliyoruz. \n\n Bunun bir hata olduğunu düşünüyorsanız,Bize bildirin.", + "browsing-site-blocked-description2": "genel sohbet.", + "browsing-site-blocked-go-back": "Geri dön", + "browsing-site-blocked-title": "Bu site engellendi", + "browsing-title": "Gözat", + "bug-report": "Hata bildirin", + "bug-report-description": "* Açıklama", + "bug-report-description-placeholder": "Gerekli, boş olamaz", + "bug-report-steps": "Yeniden üretme adımları", + "bug-report-steps-placeholder": "- uygulamayı aç\n - bir şeyler yap\n - ve sonra başka bir şey...", + "bug-report-submit-email": "Günlük arşivi e-posta ile gönder", + "bug-report-too-short-description": "Açıklama çok kısa", + "build-yourself": "Uygulamayı bu Hizmet Koşulları olmadan kullanmak için kendi sürümünüzü oluşturabilirsiniz", + "buy-crypto": "Kripto satın alın", + "buy-crypto-choose-a-service": "Kripto satın almak için kullanmak istediğiniz bir hizmet seçin", + "buy-crypto-description": "Şimdi kripto satın almak için bir dapp bulun", + "buy-crypto-leaving": "Status'tan ayrılıyorsunuz ve satın alma işleminizi tamamlamak için üçüncü taraf web sitesine giriyorsunuz", + "buy-crypto-title": "Görünüşe göre cüzdanınız boş", + "camera-access-error": "Gerekli kamera iznini vermek için lütfen sistem ayarlarınıza gidin ve Status > Kamerasının seçili olduğundan emin olun.", + "can-not-add-yourself": "Bu sensin, sohbet başlatmak için başka birini seçin.", + "can-send-messages": "Yeni mesaj gönderebilir ve alabilirsiniz", + "cancel": "İptal", + "cancel-keycard-setup": "Keycard kurulumlarını iptal et", + "cancelling": "İptal", + "cannot-read-card": "Kart okunamıyor. \n Lütfen telefonunuzun arkasına tutun.", + "cannot-use-default-pin": "000000 parolasına izin verilmiyor. \n Lütfen başka bir numara kullanın", + "cant-open-public-chat": "Herkese açık sohbeti açamıyorum", + "cant-report-bug": "Hata bildirilemiyor", + "card-is-blank": "Bu kart boş", + "card-reseted": "Kart sıfırlandı", + "card-unpaired": "Kart mevcut cihazla eşleştirilmedi", + "category": "Kategori", + "category-title": "Kategori başlığı", + "change-fleet": "Fleet'i {{fleet}} olarak değiştir.", + "change-logging-enabled": "Günlüğü {{enable}} günlüğe kaydetmek istediğinizden emin misiniz?", + "change-pairing": "Eşleştirme kodunu değiştir", + "change-pairing-description": "Eşleştirme kodunun değiştirilmesi mevcut eşleştirmeleri etkilemez. Ancak, herhangi bir yeni eşleştirme için yeni kod gerektirir.", + "change-pairing-title": "Yeni bir eşleştirme kodu oluşturun", + "change-passcode": "Parolayı Değiştir", + "change-password": "Şifreni değiştir", + "change-pin": "6 basamaklı şifrenizi değiştirin.", + "change-puk": "12 haneli PUK'u değiştir", + "changed-amount-warning": "Tutar {{old}} den {{new}} olarak değiştirildi.", + "changed-asset-warning": "Varlık{{old}} den {{new}} olarak değiştirildi.", + "chaos-mode": "Kaos Modu", + "chaos-unicorn-day": "Kaos Unicorn Günü", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Sohbet", + "chat-and-transact": "Arkadaşlarınızla özel olarak sohbet edin ve işlem yapın", + "chat-is-a-contact": "İletişim", + "chat-is-not-a-contact": "Ekli Değil", + "chat-key": "Sohbet anahtarı", + "chat-key-content": "Status sohbet protokolündeki mesajlar şifreleme anahtarları kullanılarak gönderilir ve alınır. Genel sohbet anahtarları sizin Status ile mesaj gönderilmesi için başkalarıyla paylaştığınız bir karakter dizisidir.", + "chat-key-title": "Sohbet Anahtarı", + "chat-link-previews": "Sohbet bağlantısı önizlemeleri", + "chat-name": "Sohbet Adı", + "chat-name-content": "Sohbet anahtarınızdan algoritmik olarak türetilen ve sohbette varsayılan takma adınız olarak kullanılan üç rastgele kelimedir. Sohbet adları tamamen benzersizdir; başka hiçbir kullanıcı aynı üç kelimeye sahip olamaz.", + "chat-name-title": "Sohbet Adı", + "chat-notification-preferences": "Bildirim ayarları", + "chat-settings": "Sohbet ayarları", + "chats": "Sohbetler", + "check-on-opensea": "Opensea'yı kontrol edin", + "check-your-recovery-phrase": "Kelime imzalarınızı kontrol edin", + "choose-actions": "Hareketleri seçin", + "choose-authentication-method": "Bir kimlik doğrulama yöntemi seçin.", + "choose-new-location-for-keystore": "Anahtar deposu dosyanızı kaydetmek için yeni bir konum seçin", + "choose-storage": "Depolamayı seçin", + "clear": "Temizle", + "clear-all": "Hepsini temizle", + "clear-history": "Geçmişi temizle", + "clear-history-action": "Temizle", + "clear-history-confirmation": "Geçmiş temizlensin mi?", + "clear-history-confirmation-content": "Bu sohbet geçmişini silmek istediğinizden emin misiniz?", + "clear-history-title": "Geçmiş temizlensin mi?", + "close": "Kapat", + "close-all": "Hepsini kapat", + "close-app-button": "Onayla", + "close-app-content": "Uygulama durur ve kapanır. Yeniden açtığınızda, seçilen ağ kullanılır", + "close-app-title": "Uyarı!", + "collectibles-leak-metadata": "NFT'lerinizi burada görüntüleyebilirsiniz. Bunu yaparsanız, cüzdanınızı ve IP adresinizi paylaşacaksınız.", + "command-button-send": "Gönder", + "communities": "Topluluklar", + "communities-alpha": "Topluluklar (alfa)", + "communities-enabled": "Topluluklar etkinleştirildi", + "communities-verified": "✓ Doğrulanmış Status Topluluğu", + "community": "Topluluk", + "community-color": "Topluluk rengi", + "community-color-placeholder": "Bir renk seçin", + "community-edit-title": "Topluluğu düzenleyin", + "community-emoji-thumbnail-title": "Küçük resim", + "community-image-pick": "Bir resim seçin", + "community-image-remove": "Kaldır", + "community-image-take": "Fotoğraf çek", + "community-info": "Topluluk bilgileri", + "community-info-not-found": "Topluluk bilgileri bulunamadı", + "community-invite-title": "Davet et", + "community-key": "Topluluk özel anahtarı", + "community-key-placeholder": "Topluluk özel anahtarınızı yazın", + "community-link": "Topluluk bağlantısı", + "community-members": { + "one": "Üye", + "other": "Üyeler" + }, + "community-members-title": "Üyeler", + "community-message-preview": "{{community-name}} katılma daveti", + "community-private-key": "Topluluk özel anahtarı", + "community-requests-to-join-title": "Üyelik talepleri", + "community-roles": "Roller", + "community-share-title": "Paylaş", + "community-thumbnail-image": "Küçük resim", + "community-thumbnail-upload": "Yükle", + "complete-hardwallet-setup": "Bu kart şimdi bağlandı. İşlemleri imzalamak ve anahtarlarınızın kilidini açmak için buna ihtiyacınız var", + "completed": "Tamamlandı", + "confirm": "Onayla", + "confirm-new-password": "Yeni şifreyi onayla", + "confirm-pairing-code-placeholder": "Eşleştirme kodunuzu onaylayın...", + "confirm-password-placeholder": "Parolanızı onaylayın...", + "confirmation-request": "Onay talebi", + "confirmations": "Onaylar", + "confirmations-helper-text": "İşlemin 12 onayı olduğunda, işlemin tamamlanmış olduğunu düşünebilirsiniz.", + "connect": "Bağlan", + "connect-mailserver-content": "{{name}}'e bağlan ?", + "connect-wallet": "Cüzdanı bağlayın", + "connected": "Bağlandı", + "connected-to": "Bağlı", + "connecting": "Bağlanıyor...", + "connecting-requires-login": "Başka bir ağa bağlanmak için oturum açmak gerekir", + "connection-request": "Bağlantı İsteği", + "connection-status": "Bağlantı durumu", + "connection-with-the-card-lost": "Kart ile bağlantı \n kayboldu", + "connection-with-the-card-lost-setup-text": "Kuruluma devam etmek için kartı \n telefonunuzun arkasına tutun.Telefon ile iletişime geçecek.", + "connection-with-the-card-lost-text": "Devam etmek için kartı telefonunuzun arkasına tutun", + "contact-code": "Sohbet anahtarı", + "contact-s": { + "one": "Kişi", + "other": "Kişiler" + }, + "contacts": "Kişiler", + "contacts-descr": "Kişileriniz burada görünecek. Kişi olarak eklediğiniz herkesten status güncellemeleri alacaksınız", + "contacts-empty": "ENS adlarına sahip kişiler burada görünecek", + "continue": "Devam et", + "continue-anyway": "Yine de devam edin", + "contract-address": "Sözleşme adresi", + "contract-interaction": "Sözleşme etkileşimi", + "contract-isnt-supported": "Sözleşme desteklenmiyor", + "copy-info": "Kopya bilgisi", + "copy-qr": "Kodu kopyala", + "copy-to-clipboard": "Kopyala", + "copy-transaction-hash": "İşlem kimliğini kopyala", + "cost-fee": "Maliyet \/ Ücret", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Oluştur", + "create-a-pin": "6 haneli bir şifre oluşturun", + "create-a-puk": "12 basamaklı bir PUK oluşturun", + "create-category": "Kategori oluştur", + "create-channel": "Kanal oluştur", + "create-channel-title": "Yeni kanal", + "create-community": "Bir topluluk oluşturun", + "create-group-chat": "Grup sohbeti oluştur", + "create-multiaccount": "Anahtar üret", + "create-new-key": "Yeni anahtarlar edinin", + "create-pin": "6 basamaklı şifre oluştur", + "create-pin-description": "Status kilidini açmak ve işlemleri onaylamak için kartınıza ve bu 6 haneli şifreye ihtiyacınız olacak.", + "created-group-chat-description": "{{group-name}} oluşturdunuz", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Para birimi", + "currency-display-name-aed": "Emirati Dirhemi", + "currency-display-name-afn": "Afganistan Afganisi", + "currency-display-name-ars": "Arjantin Pezosu", + "currency-display-name-aud": "Avustralya Doları", + "currency-display-name-bbd": "Barbados Doları", + "currency-display-name-bdt": "Bangladeş Takası", + "currency-display-name-bgn": "Bulgar Levası", + "currency-display-name-bhd": "Bahreyn Dinarı", + "currency-display-name-bnd": "Brunei Sultanlığı Doları", + "currency-display-name-bob": "Bolivya Bolivianosu", + "currency-display-name-brl": "Brezilya Reali", + "currency-display-name-btn": "Bhutan Ngultrumu", + "currency-display-name-cad": "Kanada Doları", + "currency-display-name-chf": "İsviçre Frangı", + "currency-display-name-clp": "Şili Pezosu", + "currency-display-name-cny": " Çin Yuanı", + "currency-display-name-cop": "Kolombiya Pezosu", + "currency-display-name-crc": "Kosta Rika Kolonu", + "currency-display-name-czk": "Çek Korunası", + "currency-display-name-dkk": "Danimarka Kronu", + "currency-display-name-dop": "Dominik Cumhuriyeti Pezosu", + "currency-display-name-egp": "Mısır Lirası", + "currency-display-name-etb": "Etiyopya Birri", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "İngiliz Sterlini", + "currency-display-name-gel": "Gürcü Larisi", + "currency-display-name-ghs": "Gana Cedi", + "currency-display-name-hkd": "Hong Kong Doları", + "currency-display-name-hrk": "Hırvatistan Kunası", + "currency-display-name-huf": "Macaristan Forinti", + "currency-display-name-idr": "Endonezya Rupiahı", + "currency-display-name-ils": "İsrail Şekeli", + "currency-display-name-inr": "Hindistan Rupisi", + "currency-display-name-isk": "İzlanda Kronu", + "currency-display-name-jmd": "Jamaika Doları", + "currency-display-name-jpy": "Japon Yeni", + "currency-display-name-kes": "Kenya Şilini", + "currency-display-name-krw": "Güney Kore Wonu", + "currency-display-name-kwd": "Küveyt Dinarı", + "currency-display-name-kzt": "Kazakistan Tengesi", + "currency-display-name-lkr": "Sri Lanka Rupisi", + "currency-display-name-mad": "Fas Dirhemi", + "currency-display-name-mdl": "Moldova Leyi", + "currency-display-name-mur": "Mauritius Rupisi", + "currency-display-name-mwk": "Malavi Kvaçası", + "currency-display-name-mxn": "Meksika Pezosu", + "currency-display-name-myr": "Malezya Ringiti", + "currency-display-name-mzn": "Mozambik Metikali", + "currency-display-name-nad": "Namibya Doları", + "currency-display-name-ngn": "Nijerya Nairası", + "currency-display-name-nok": "Norveç Kronu", + "currency-display-name-npr": "Nepal Rupisi", + "currency-display-name-nzd": "Yeni Zellanda Doları", + "currency-display-name-omr": "Umman Riyali", + "currency-display-name-pen": "Peru Solu", + "currency-display-name-pgk": "Papua Yeni Gine Kinası", + "currency-display-name-php": "Filipinler Pezosu", + "currency-display-name-pkr": "Pakistan Rupisi", + "currency-display-name-pln": "Polonya Zlotisi", + "currency-display-name-pyg": "Paraguay Guarani", + "currency-display-name-qar": "Katar Riyali", + "currency-display-name-ron": "Rumen Leyi", + "currency-display-name-rsd": "Sırbistan Dinarı", + "currency-display-name-rub": "Rusya Rublesi", + "currency-display-name-sar": "Suudi Arabistan Riyali", + "currency-display-name-sek": "İsveç Kronu", + "currency-display-name-sgd": "Singapur doları", + "currency-display-name-thb": "Tayland Bahtı", + "currency-display-name-try": "Türk Lirası", + "currency-display-name-ttd": "Trinidad ve Tobago Doları", + "currency-display-name-twd": "Tayvan Yeni Doları", + "currency-display-name-tzs": "Tanzanya Şilini", + "currency-display-name-uah": "Ukrayna Grivnası", + "currency-display-name-ugx": "Uganda Şilini", + "currency-display-name-usd": "Amerikan Doları", + "currency-display-name-uyu": "Uruguay Pezosu", + "currency-display-name-vef": "Venezuela Bolívarı", + "currency-display-name-vnd": "Vietnam Dongu", + "currency-display-name-zar": "Güney Afrika Randı", + "current": "Şu anki", + "current-base": "Mevcut taban ücret", + "current-base-fee": "Mevcut taban ücret", + "current-network": "Mevcut ağ", + "current-password": "Şimdiki Şifre", + "current-pin": "6 haneli şifreyi girin", + "current-pin-description": "Devam etmek için 6 haneli şifrenizi girin", + "custom": "Özel", + "custom-networks": "Özel ağlar", + "custom-node": "Özel RPC uç noktası kullanıyorsunuz. Yerel transfer geçmişiniz eksik olabilir.", + "custom-seed-phrase": "Geçersiz kelime imzası", + "custom-seed-phrase-text-1": "Bu kelime imzası desteklenebilen kelimeler ile eşleşmiyor. Yanlış yazılmış kelimeleri kontrol edin.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Kabul Et ve Aç", + "dapp-starter-pack-description": "İşte başlamanız için bazı kriptolar! Çıkartmalar, bir ENS adı almak ve dapp'leri denemek için kullanın.", + "dapp-starter-pack-title": "Başlangıç paketi", + "dapp-would-like-to-connect-wallet": "bağlanmak istiyor", + "dapps": "ÐApps", + "dapps-permissions": "DApp izinleri", + "dark": "Karanlık", + "data": "Veri", + "data-collected": "Toplanan veri", + "data-collected-subtitle": "Aşağıdaki tablo, saklanan ve gönderilecek olan verileri tam olarak göstermektedir. Hassas verilerin gönderilmediğinden emin olmak için veriler genel kurallara göre doğrulanır. Güvenmeyin, onaylayın.", + "data-syncing": "Veri senkronizasyonu", + "database-reset-content": "Sohbetler, kişiler ve ayarlar silindi. Hesabınızı Keycardınızla kullanabilirsiniz", + "database-reset-title": "Veritabanı sıfırla", + "database-reset-warning": "Veritabanı sıfırlanacak. Sohbetler, kişiler ve ayarlar silinecek", + "datetime-ago": "Önce", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "one": "gün", + "other": "günler" + }, + "datetime-day-short": { + "one": "gün", + "other": "gün" + }, + "datetime-hour": { + "one": "saat", + "other": "saatler" + }, + "datetime-hour-short": { + "one": "saat", + "other": "saat" + }, + "datetime-minute": { + "one": "dakika", + "other": "dakikalar" + }, + "datetime-minute-short": { + "one": "dakika", + "other": "dakika" + }, + "datetime-second": { + "one": "Saniye", + "other": "Saniyeler" + }, + "datetime-second-short": { + "one": "saniye", + "other": "saniye" + }, + "datetime-today": "bugün", + "datetime-yesterday": "Dün", + "decimals": "Basamaklı", + "decline": "Reddet", + "decryption-failed-content": "Verileriniz şifresi çözülürken bir hata oluştu. Eski verilerinizi silmeniz ve yeni bir hesap oluşturmanız gerekebilir. Silmek için \"Uygula\" ya da tekrar denemek için \"İptal\" e hafifçe dokunun.", + "default": "Varsayılan", + "default-assets": "Varsayılan ERC20 ve ERC721", + "default-sync-period": "Senkronizasyon geçmişi", + "delete": "Sil", + "delete-account": "Hesabı sil", + "delete-and-leave-group": "Grubu sil ve gruptan ayrıl", + "delete-bootnode": "Bootnode sil", + "delete-bootnode-are-you-sure": "Bu bootnode silmek istediğinizden emin misiniz?", + "delete-bootnode-title": "Bootnode silme", + "delete-category-confirmation": "Bu kategoriyi silmek istediğinizden emin misiniz?", + "close-chat": "Sohbeti sil", + "close-chat-confirmation": "Bu sohbeti silmek istediğinizden emin misiniz?", + "delete-confirmation": "Sil?", + "delete-keys-keycard": "Keycardından anahtarları sil", + "delete-mailserver": "Geçmiş Status nodesini silin.", + "delete-mailserver-are-you-sure": "Bu Status nodesini silmek istediğinizden emin misiniz?", + "delete-mailserver-title": "Status nodesini silin", + "delete-message": "Mesajı sil", + "delete-my-account": "Hesabımı sil", + "delete-my-profile": "Profilimi sil", + "delete-network-confirmation": "Bu ağı silmek istediğinizden emin misiniz?", + "delete-network-error": "Lütfen bunu silmeden önce farklı bir ağa bağlanın.", + "delete-network-title": "Ağ silinsin mi?", + "delete-node": "Node sil", + "delete-node-are-you-sure": "Bu nodeyi silmek istediğinizden emin misiniz?", + "delete-node-title": "Node sil", + "delete-profile": "Profili sil", + "delete-profile-warning": "Uyarı: Kelime imzanız yazılmamışsa, profilinizi sildikten sonra parasal varlıklarınıza erişiminizi kaybedersiniz", + "deny": "Reddet", + "derivation-path": "Türetme yolu", + "describe-channel": "Kanalı açıklayın", + "description": "Açıklama", + "dev-mode": "Geliştirici Modu", + "dev-mode-settings": "Geliştirme modu ayarları", + "device-syncing": "Cihaz senkronizasyonu", + "devices": "Cihazlar", + "disable": "Devre dışı bırak", + "disable-all": "Hepsini etkisiz hale getir", + "disable-later-in-settings": "Bunu daha sonra ayarlardan değiştirebilirsiniz", + "disabled": "Devre dışı", + "disconnected": "Sohbet Çevrimdışı", + "discover": "Keşfedin", + "dismiss": "Reddet", + "display-collectibles": "Koleksiyon ürünlerini göster", + "done": "Bitti", + "dont-ask": "Bir daha sorma", + "edit": "Düzenle", + "edit-channel-title": "Kanalı düzenle", + "edit-chats": "Sohbetleri düzenle", + "edit-community": "Topluluğu düzenleyin", + "edit-favourite": "Favoriyi düzenle", + "edit-group": "Grubu düzenle", + "edit-profile": "Profili Düzenle", + "edited": "Düzenlendi", + "editing-message": "Mesajı düzenle", + "empty-activity-center": "Sohbet bildirimleriniz\n burada görünecek", + "empty-chat-description": "Bu sohbette henüz hiç mesaj yok.", + "empty-chat-description-community": "Burada son kez sessiz kaldı {{quiet-hours}}", + "empty-chat-description-one-to-one": "Buraya gönderdiğiniz tüm iletiler şifrelenir ve yalnızca sizin tarafınızdan okunabilir.", + "empty-chat-description-public": "Burada son {{quiet-hours}} konuşmaları. Konuşmayı başlatın veya", + "empty-chat-description-public-share-this": "Bu sohbeti paylaş.", + "empty-keycard-required": "Boş bir Keycard gerektirir", + "empty-pending-invitations-descr": "Gruba katılmak isteyenler\n bir davet bağlantısı aracılığıyla burada görünecek", + "empty-tab": "Boş sekme", + "enable": "Etkinleştir", + "enable-all": "Hepsini etkinleştir", + "enable-link-previews": "Sohbette bağlantı önizlemeleri etkinleştirilsin mi?", + "encrypt-with-password": "Şifrenizi şifreleyin.", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Kullanıcı adı ekle", + "ens-agree-to": "İçin kabul", + "ens-chat-settings": "Sohbet ayarları", + "ens-custom-domain": "Özel alan adı", + "ens-custom-username-hints": "Username.domain.eth gibi özel alan adı dahil olmak üzere tüm kullanıcı adınızı yazın", + "ens-custom-username-taken": "Kullanıcı adı size ait değil :(", + "ens-deposit": "Depozito", + "ens-dismiss-message": "Kapatmak için burayı tıklayın", + "ens-displayed-with": "İletileriniz şu kullanıcılara gösterilir.", + "ens-get-name": "Evrensel bir kullanıcı adı edinin", + "ens-got-it": "Tamam anladım", + "ens-locked": "Kullanıcı adınız kilitlendi. {{date}} kadar yayın yapamazsınız.", + "ens-name-content": "Ethereum Name Service'i kullanarak kaydedebileceğiniz sohbet anahtarınız için özel takma ad. ENS isimleri merkezi olmayan kullanıcı isimleridir.", + "ens-name-not-found": "ENS adı çözülemiyor", + "ens-name-title": "ENS Adı", + "ens-network-restriction": "Yalnızca Mainnet'te kullanılabilir", + "ens-no-usernames": "Bağlı kullanıcı adınız yok", + "ens-powered-by": "Powered by Ethereum İsim Hizmetleri", + "ens-primary-username": "Birincil kullanıcı adı", + "ens-register": "Kayıt ol", + "ens-registration-failed": "Kullanıcı adını kaydetmek için lütfen tekrar deneyin.", + "ens-registration-failed-title": "İşlem başarısız oldu", + "ens-registration-failure": "Kayıt başarısız oldu", + "ens-registration-in-progress": "Kayıt devam ediyor ...", + "ens-release-username": "Kullanıcı adınımı yayınla", + "ens-remove-hints": "Kaldırma işlemiyle birlikte, kullanıcı adınınız anahtarınızdan ayıracaktır.", + "ens-remove-username": "Kullanıcı adını kaldırma", + "ens-saved": "Artık sohbet anahtarınızla bağlantılıdır ve Status'da kullanılabilir.", + "ens-saved-title": "Kullanıcı adı eklendi", + "ens-show-username": "Sohbetlerde ENS kullanıcı adımı göster", + "ens-terms-header": "İsim kaydı şartları", + "ens-terms-point-1": "Fonlar 1 yıl süreyle yatırılır. SNT'niz kilitlenecek, ancak harcanmayacak.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS Kaydı).", + "ens-terms-point-2": "1 yıl sonra adınızdan vazgeçebilirsiniz ve depozitonuzu geri alabilirsiniz.Veya isterseniz adınızın kayıtta olması için herhangi bir işlem yapmanıza gerek yok.", + "ens-terms-point-3": "Sözleşme şartları değiştiğinde (örneğin, Status sözleşmede güncellemeler yapıyorsa )Kullanıcı tutulmuş zaman her ne olursa olsun kullanıcı adını serbest bırakmak için hakka sahiptir.", + "ens-terms-point-4": "Sözleşme denetleyicisi yatırdığınız paraya erişemez. Yalnızca onları gönderen adrese geri taşıyabilir.", + "ens-terms-point-5": "Adresleriniz ENS adınızla herkese açık olarak ilişkilendirilecektir.", + "ens-terms-point-6": "Kullanıcı adları stateofus.eth alt etki alanı düğümleri olarak oluşturulur ve ENS akıllı sözleşme koşullarına tabidir.", + "ens-terms-point-7": "Sizin adınıza SNT'yi transfer etmek için sözleşmeye yetki verirsiniz. Bu yalnızca aktarıma yetkilendirmek üzere bir işlemi onayladığınızda oluşabilir.", + "ens-terms-point-8": "Bu koşullar, adreslerdeki akıllı sözleşme mantığı ile garanti edilir:", + "ens-terms-point-9": "{{address}}Status Kullanıcı Adı Kayıtı", + "ens-terms-registration": "İsim kayıt şartları", + "ens-test-message": "Hey", + "ens-transaction-pending": "İşlem bekleniyor ...", + "ens-understand": "Cüzdan adresimin kullanıcı adımla herkese açık olarak bağlanacağını anlıyorum.", + "ens-username": "ENS kullanıcı adı", + "ens-username-already-added": "Kullanıcı adı sohbet anahtarınıza zaten bağlı ve Status içinde kullanılabilir.", + "ens-username-available": "✓ Kullanıcı adı kullanılabilir!", + "ens-username-connected": "Bu kullanıcı adı size aittir ve sohbet anahtarınızla bağlantılıdır.", + "ens-username-connected-continue": "`ENS kullanıcı adımı sohbette göster 'ayarını yapmaya devam edin.", + "ens-username-connected-with-different-key": "Devam etmek için kullanıcı adını mevcut sohbet anahtarınıza bağlamaki çin işlem yapmanız gerekiyor.", + "ens-username-connection-confirmation": "İşlem tamamlandığında {{username}} bağlanacak.", + "ens-username-hints": "En az 4 karakter. Yalnızca Latin harfleri, sayılar ve küçük harfler", + "ens-username-invalid": "Sadece harfler ve rakamlar.", + "ens-username-owned": "✓ Bu kullanıcı adı size ait.", + "ens-username-owned-continue": "Devam ederseniz, bu kullanıcı adı sohbet anahtarınıza bağlanır.", + "ens-username-registration-confirmation": "{{username}}", + "ens-username-taken": "Bu kullanıcı adı daha önce zaten alınmış :(", + "ens-username-you-can-follow-progress": "İlerlemeyi cüzdanınızın '' İşlem Geçmişi '' bölümünden takip edebilirsiniz.", + "ens-usernames": "ENS kullanıcı adlarınız", + "ens-usernames-details": "Diğer kullanıcılar tarafından kolayca tanınabilmesi için evrensel bir kullanıcı adı kaydedin", + "ens-want-custom-domain": "Başka bir alan adında bir isme sahibim ", + "ens-want-domain": "Stateofus.eth alan adı istiyorum", + "ens-welcome-hints": "ENS adları bu çılgın adresleri benzersiz kullanıcı adlarına dönüştürür.", + "ens-welcome-point-customize": "Bir ENS adı, sohbet sırasında rastgele 3 kelimelik adınızın yerini alabilir. {{name}} yerine @ adınız olur.", + "ens-welcome-point-customize-title": "Sohbet adınızı özelleştirin", + "ens-welcome-point-receive": "Diğer herkes size tek bir adımda para gönderebilir.", + "ens-welcome-point-receive-title": "Sohbette işlem alın", + "ens-welcome-point-register": "Sadece bir kez kayıt olun ve o ad sizin olsun.1 yıl sonra isterseniz adınızdan vazgeçebilir ve Sntlerinizi geri alabilirsiniz.", + "ens-welcome-point-register-title": "Kayıt için 10 snt gerekli", + "ens-welcome-point-simplify": "Onaltılık karma (0x ...) yerine paylaşması kolay ENS adınıza para alabilirsiniz.", + "ens-welcome-point-simplify-title": "ETH adresinizi basitleştirin", + "ens-welcome-point-verify": "Sonraki adımlarda sahip olduğunuz kullanıcı adlarınızı doğrulayabilir ve ekleyebilirsiniz.", + "ens-welcome-point-verify-title": "Zaten bir kullanıcı adınız var mı?", + "ens-your-username": "Kullanıcı adınız", + "ens-your-usernames": "Kullanıcı adlarınız", + "ens-your-your-name": "ENS adınız", + "enter-12-words": "Tek boşluk ile ayrılmış kelime imzanızın 12 kelimesini girin", + "enter-a-private-key": "Özel bir anahtar girin", + "enter-a-seed-phrase": "Kelime imzası girin", + "enter-address": "Adresi girin", + "enter-contact-code": "ENS (vitalik94) veya sohbet anahtarı (0x04…)", + "enter-pair-code": "Eşleme kodunuzu girin", + "enter-pair-code-description": "Eşleştirme kodu, önceden eşleştirilmiş bir Status istemcisinden ayarlanabilir", + "enter-password": "Şifre girin", + "enter-password-migration-prompt": "Anahtarlarınızla birlikte kişileri, sohbetleri ve ayarları taşımak için şifrenizi girin", + "enter-pin": "6 haneli şifreyi girin", + "enter-puk-code": "PUK kodunu girin", + "enter-puk-code-description": "6 haneli şifreniz engellendi. \n Şifre engelinizi kaldırmak için lütfen PUK kodunu girin.", + "enter-recipient-address-or-username": "Alıcının adresini veya kullanıcı adını girin", + "enter-seed-phrase": "Kelime imzanızı girin", + "enter-url": "URL girin", + "enter-user-pk": "Kullanıcı genel anahtarını girin", + "enter-watch-account-address": "QR kodunu tarayın \n veya \n izlenecek adresi girin", + "enter-word": "Kelime girin", + "enter-your-code": "6 haneli şifrenizi girin", + "enter-your-password": "Şifrenizi girin", + "error": "Hata", + "error-unable-to-get-balance": "Bakiye alınamıyor.", + "error-unable-to-get-prices": "Para birimi dönüştürme hatası. Tekrar denemek için ekranınızı yenileyin.", + "error-unable-to-get-token-balance": "Token bakiyesi alınamıyor.", + "errors": "Hatalar", + "eth": "ETH", + "ethereum-account": "Ethereum hesabı", + "ethereum-address": "Ethereum adresi", + "ethereum-node-started-incorrectly-description": "Ethereum düğümü yanlış yapılandırmayla başlatıld ve bu durumu düzeltmek için uygulama durdurulur. Yapılandırılmış ağ kimliği = {{network-id}} , gerçek = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Ethereum düğümü yanlış başladı", + "etherscan-lookup": "Etherscan'a bak.", + "everyone": "Herkes", + "expand-all": "Tümünü genişlet", + "export-account": "Hesabı dışa aktar", + "export-key": "Özel anahtarı dışa aktar", + "external-storage-denied": "Harici depolamaya erişim reddedildi", + "failed": "Başarısız", + "faq": "Sıkça Sorulan Sorular", + "fast": "Hızlı", + "favourite": "Favori", + "favourite-description": "Favori web siteleriniz burada görünecek", + "favourites": "Favoriler", + "favourites-empty": "Sık kullanılanlara eklenen adresler burada görünecek", + "fee-cap": "Ücret sınırı", + "fee-explanation": "İşlem için maksimum toplam fiyat. Blok taban ücreti bunu aşarsa, daha düşük taban ücretle bir sonraki bloğa dahil edilir.", + "fee-options": "Önerilen ücret seçenekleri", + "fetch-community": "topluluğu getir", + "fetch-messages": "Mesajları getir", + "fetch-timeline": "↓ Getir", + "fetching-community": "Topluluk getiriliyor...", + "find": "Bul", + "finish": "Bitti", + "finishing-card-setup": "Kart kurulumu tamamlandı", + "fleet": "Fleet", + "fleet-settings": "Fleet ayarları", + "follow": "Takip et", + "follow-your-interests": "Herkese açık bir sohbete katılın ve yeni insanlarla tanışın", + "free": "↓ Ücretsiz", + "from": "Kaynak", + "gas-amount-limit": "Gaz miktarı limiti", + "gas-limit": "Gas limiti", + "gas-price": "Gaz fiyatı", + "gas-used": "Gas kullanılıyor", + "generate-a-key": "Anahtar üret", + "generate-a-new-account": "Bir hesap oluştur", + "generate-a-new-key": "Yeni bir anahtar oluşturun", + "generate-account": "Anahtar üret", + "generate-an-account": "Bir hesap oluşturun", + "generate-new-key": "Anahtar üret", + "generating-codes-for-pairing": "> Ürün yazılımını karta indirme \n > Kilit açma ve eşleme kodları oluşturma", + "generating-keys": "Anahtarlar oluşturuluyor ...", + "generating-mnemonic": "Kelime imzanız oluşturuluyor", + "get-a-keycard": "Keycard alın", + "get-started": "Başlayın", + "get-status-at": "Statusu http:\/\/status.im adresinden edinin.", + "get-stickers": "Çıkartma Al", + "give-a-short-description": "Kısa bir açıklama yapın", + "give-a-short-description-community": "Kısa bir açıklama yapın", + "give-permissions-camera": "Kameraya erişmek için izin verin", + "glossary": "Terimler sözlüğü", + "go-to-settings": "Ayarlar'a gidin...", + "got-it": "Anladım", + "grant-face-id-permissions": "Gerekli Face ID iznini vermek için lütfen sistem ayarlarınıza gidin ve Status > Face ID'nin seçili olduğundan emin olun", + "group-chat": "Grup sohbeti", + "group-chat-admin": "Yönetici", + "group-chat-admin-added": "** {{member}} ** yönetici yapıldı", + "group-chat-all-contacts-invited": "Tüm kişileriniz zaten grupta", + "group-chat-created": "** {{member}} ** grup oluşturdu ** {{name}} **", + "group-chat-decline-invitation": "Daveti reddet", + "group-chat-member-added": "** {{member}} ** davet edildi", + "group-chat-member-joined": "** {{member}} ** gruba katıldı", + "group-chat-member-removed": "** {{member}} ** gruptan ayrıldı", + "group-chat-members-count": "{{selected}} \/ {{max}} üye", + "group-chat-name-changed": "** {{member}} ** grubun adını ** {{name}} ** olarak değiştirdi", + "group-chat-no-contacts": "Henüz hiç kişiniz yok. \n Arkadaşlarınızı sohbete davet edin", + "group-info": "Grup bilgisi", + "group-invite": "Grup daveti", + "group-invite-link": "Grup davet bağlantısı", + "group-membership-request": "Grup üyeliği isteği", + "gwei": "Gwei", + "has-permissions": "erişim izni istiyor", + "hash": "Hash", + "help": "Yardım", + "help-capitalized": "Yardım", + "help-center": "Yardım Merkezi", + "help-improve-status": "Statusu geliştirmeye yardımcı olun", + "hide": "Gizle ", + "hide-content-when-switching-apps": "Ekran görüntüsü alınmasını engelle", + "hide-content-when-switching-apps-ios": "Önizlemeyi gizle", + "history": "Geçmiş", + "history-nodes": "Status nodeleri", + "hold-card": "Kartı telefonunuzun arkasında tutun", + "home": "Ev", + "hooks": "Kancalama", + "how-it-works": "Nasıl çalışır", + "http-gateway-error": "Hay aksi, istek başarısız oldu!", + "identifier": "Tanımlayıcı", + "image": "Resim", + "image-remove-current": "Mevcut fotoğrafı kaldır", + "image-source-gallery": "Galeriden seç", + "image-source-make-photo": "Yakala", + "image-source-title": "Resmi düzenle", + "import": "İçe Aktar", + "import-community": "Bir topluluğu içe aktar", + "import-community-title": "Bir topluluğu içe aktar", + "in-contacts": "Kişilerde", + "include": "Kapsar", + "incoming": "Gelen", + "incoming-transaction": "Gelen işlem", + "incorrect-code": [ + "str", + "Maalesef kod yanlış, lütfen tekrar girin" + ], + "increase-gas": "Gazı Artırın", + "initialization": "Başlatma", + "install": "◊ Yükle", + "intro-message1": "Status'a Hoş Geldiniz! \n Şifrenizi ayarlayabilmeniz için bu mesaja dokunun.", + "intro-privacy-policy-note1": "Status kişisel verilerinizi toplamaz veya bunlardan kar sağlamaz. Devam ederek kabul etmiş sayılırsınız.", + "intro-privacy-policy-note2": "Gizlilik Politikası", + "intro-text": "Status, merkezi olmayan web'e giriş kapısıdır", + "intro-text1": "İletilerin sansürlenemeyeceği veya saldırıya uğratılamadığı şifreli bir ağ üzerinden sohbet edin.", + "intro-text2": "Dijital varlıkları dünyanın herhangi bir yerine gönderin veya alın; banka hesabı gerektirmez.", + "intro-text3": "Yalnızca verilerinizin sahibi olduğunuz oyunları, borsaları ve sosyal ağları keşfedin", + "intro-title1": "Gerçekten özel iletişim", + "intro-title2": "Güvenli kripto cüzdanı", + "intro-title3": "Merkezi olmayan uygulamalar", + "intro-wizard-text1": "Bu anahtarlar hesabınızı kontrol eder. Anahtarlarınız sadece telefonunuzda yayınlanır.Böylece yalnızca siz kullanabilirsiniz", + "intro-wizard-text2": "Bu anahtar sohbet içindir. Değiştirilemeyen ve okunabilir bir adla birlikte gelir.", + "intro-wizard-text3": "Bir Keycard Kartınız var ise, gelişmiş güvenlik için anahtarlarınızı orada saklayın.", + "intro-wizard-text4": "Anahtarlarınızı güvenceye alın ve şifreleyin", + "intro-wizard-text6": "Status yeni mesajlar hakkında sizi bilgilendirir. Bildirim tercihlerinizi daha sonra ayarlardan düzenleyebilirsiniz", + "intro-wizard-title-alt4": "Şifre oluştur", + "intro-wizard-title-alt5": "Parolanızı doğrulayın", + "intro-wizard-title1": "Anahtarlarını al", + "intro-wizard-title2": "Bir sohbet adı seçin", + "intro-wizard-title3": "Anahtar depolama alanını seçin", + "intro-wizard-title4": "6 haneli bir şifre oluşturun", + "intro-wizard-title5": "Parolayı onaylayın", + "intro-wizard-title6": "Bildirimleri etkinleştirin", + "introduce-yourself": "Kendinizi kısa bir mesajla tanıtın", + "invalid-address-qr-code": "Taranan QR kodu geçerli bir adres içermiyor", + "invalid-format": "Geçersiz format \n {{format}} olmalı", + "invalid-key-confirm": "Uygula", + "invalid-key-content": "Bir dosya bozuk olduğundan veritabanı şifrelenemiyor. Paranız ve sohbet anahtarınız güvende. Sohbetleriniz ve kişileriniz gibi diğer veriler geri yüklenemez. “ {{erase-multiaccounts-data-button-text}} ” düğmesi, diğer tüm verileri kaldırır ve paranıza erişmenize ve mesaj göndermenize olanak tanır", + "invalid-number": "Geçersiz numara", + "invalid-pairing-password": "Geçersiz eşleştirme şifresi", + "invalid-public-chat-topic": "Geçersiz genel sohbet konusu", + "invalid-range": "Geçersiz biçim, {{min}} ve {{max}} arasında olmalıdır", + "invalid-username-or-key": "Geçersiz kullanıcı adı veya sohbet anahtarı", + "invite": "Davet et", + "invite-button": "Davet et", + "invite-chat-accept": "Kabul et", + "invite-chat-accept-join": "Kabul Et ve Katıl", + "invite-chat-intro": "Bir arkadaşınız tarafından Statusa katılmanız için yönlendirildiniz. İşte başlamanız için bazı kriptolar! Bir ENS adını kaydetmek veya bir çıkartma paketi satın almak için kullanın", + "invite-chat-name": "Arkadaş tavsiyesi", + "invite-chat-pending": "Bekliyor", + "invite-chat-rule": "Kabul ettiğinizde,arkadaşınızı bir kripto tavsiye bonusu ile de ödüllendirilecek", + "invite-chat-starter-pack": "Başlangıç paketi", + "invite-friends": "Arkadaşlarınızı davet edin", + "invite-instruction-fifth": "Tavsiye bonusunuzu istediğiniz zaman kullanmayı seçebilirsiniz.", + "invite-instruction-first": "Statusu indirmek ve katılmak için arkadaşınıza benzersiz bir davet bağlantısı gönderirsiniz", + "invite-instruction-fourth": "Tavsiye bonusu ve arkadaşınıza Başlangıç Paketini alırsınız", + "invite-instruction-second": "Arkadaşınız Statusu indirir ve bir hesap oluşturur (Android'de)", + "invite-instruction-third": "Arkadaşınızla, tavsiyenizi onayladıkları bir sohbet başlatılır", + "invite-people": "İnsanları davet et", + "invite-privacy-policy-public": "Statusu bir referans bağlantısı aracılığıyla yüklediniz. Bu sohbete katılarak size referans verene atıfta bulunursunuz ve şunu kabul edersiniz:", + "invite-privacy-policy1": "Kabul ederek tavsiye programını kabul etmiş olursunuz", + "invite-privacy-policy2": "Şartlar ve koşullar.", + "invite-public-chat-home": "Referans daveti", + "invite-public-chat-intro": "İşte başlamanız için bazı kriptolar! Bir ENS adını kaydetmek veya bir çıkartma paketi satın almak için kullanın", + "invite-receive-account": "Tavsiye bonusunuzu alacak hesap", + "invite-reward": "Davet ettiğiniz her arkadaşınız için kripto kazanın!", + "invite-reward-friend": "Arkadaş:", + "invite-reward-friend-description": "Arkadaşınız, başlamak için bazı {{reward}} içeren bir Başlangıç Paketi alacak", + "invite-reward-friend-name": "Başlangıç paketi", + "invite-reward-you": "Sen:", + "invite-reward-you-description": "Bir arkadaşınızı davet edin ve tavsiye bonusu olarak {{reward}} kazanın. Çıkartmalar, bir ENS adı almak ve dapp'leri denemek için kullanın", + "invite-reward-you-name": "Tavsiye bonusu", + "invite-select-account": "Tavsiye bonusunuzu almak için bir hesap seçin", + "invite-warning": "Bu promosyon yalnızca ABD'de ikamet etmeyen bir Android cihaz kullanıcıları için geçerlidir. Arkadaşın tavsiyeyi 7 gün içinde onaylaması gerekiyor", + "invited": "davet", + "join": "Katılın", + "join-a-community": "veya bir topluluğa katılın", + "join-group-chat": "Gruba katıl", + "join-group-chat-description": "{{username}} sizi {{group-name}} katılmaya davet etti", + "join-me": "Hey Statusda bana katılın: {{url}}", + "join-new-private-chat": "Yeni bir özel sohbet başlat", + "join-new-public-chat": "Herkese açık bir sohbete katılın", + "joined": "Katıldı", + "joined-group-chat-description": "{{username}} tarafından gönderilen davetiye {{group-name}} {{username}}", + "key": "Anahtar", + "key-managment": "Anahtar yönetimi", + "key-on-device": "Özel anahtar bu cihaza kaydedildi", + "keycard": "Keycard", + "keycard-access-reset": "Keycarda erişim sıfırlandı", + "keycard-applet-install-instructions": "Uygulamayı yüklemek için lütfen https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation adresindeki talimatları izleyin.", + "keycard-awaiting-description": "Cihazınızdaki nfc okuyucuyu bulmak için kartı hareket ettirmeyi deneyin", + "keycard-awaiting-title": "Arıyor...", + "keycard-backup": "Yedek Keycardı oluşturun", + "keycard-backup-success-body": "Yedek kart başarıyla oluşturuldu. Artık bunu tıpkı birincil kart gibi hesabınızla kullanabilirsiniz.", + "keycard-backup-success-title": "Yedekleme başarılı", + "keycard-blocked": "Keycard engellendi. \n Kullanmaya devam etmek için kartı sıfırlamanız gerekir.", + "keycard-can-use-with-new-passcode": "Bu kartı yeni şifrenizle kullanabilirsiniz", + "keycard-cancel-setup-text": "Bu, keycard kurulumunu iptal eder. Keycardı kullanmak için kurulumun tamamlanması önemle tavsiye edilir. Gerçekten iptal etmek istiyor musunuz?", + "keycard-cancel-setup-title": "Tehlikeli operasyon", + "keycard-connected-description": "Kartı sabit tutmayı deneyin", + "keycard-connected-title": "Bağlandı", + "keycard-desc": "Keycardınız mı var? Anahtarlarınızı üzerinde saklayın; işlemler için buna ihtiyacın olacak", + "keycard-dont-ask-card": "Oturum açmak için kart sorma", + "keycard-enter-new-passcode": "Yeni parola girin {{step}}\/2", + "keycard-error-description": "Devam etmek için kartı yeniden bağlayın", + "keycard-error-title": "Bağlantı koptu", + "keycard-factory-reset": "Kartı fabrika ayarlarına döndür", + "keycard-factory-reset-text": "Bunu yapmak, kartta depolanan tüm anımsatıcı ifadeleri siler. Bu Keycard ile kullandığınız anımsatıcı ifadenin bir yedeğine sahip olduğunuzdan emin olun.", + "keycard-factory-reset-title": "Fabrika ayarlarına sıfırlama yapmak istediğinizden emin misiniz?", + "keycard-free-pairing-slots": "Kaycardında {{n}} boş eşleme yuvası var", + "keycard-has-multiaccount-on-it": "Bu kart dolu. Her kart bir ana anahtar çifti tutabilir", + "keycard-init-description": "Devam edebilmeniz için kartı telefonunuzun arkasına koyun", + "keycard-init-title": "Kart aranıyor ...", + "keycard-is-blocked-details": "Artık bu kartı, bu hesaba erişmek veya bu hesapta oturum açmak için kullanamazsınız. Çok fazla başarısız şifre ve PUK denemesi yapıldı.", + "keycard-is-blocked-instructions": "Hesabınıza erişmek için kartınızı fabrika ayarlarına sıfırlamanız gerekir. Prosedürü başlatmak için aşağıdaki düğmeye dokunun, anımsatıcınıza ihtiyacınız olacak.", + "keycard-is-blocked-title": "Keycard Engellendi", + "keycard-is-frozen-details": "Varlıklarınızı korumak için kartınız dondurulur. Çözmek ve işlem gönderebilmek için kartınızı sıfırlayın. Bunu PUK'unuz veya anımsatıcınızla yapabilirsiniz.", + "keycard-is-frozen-factory-reset": "Anımsatıcı ile sıfırla", + "keycard-is-frozen-reset": "PUK ile Sıfırla", + "keycard-is-frozen-title": "Keycard donduruldu", + "keycard-onboarding-finishing-header": "Bitirin", + "keycard-onboarding-intro-header": "Anahtarlarınızı Keycardınızda saklayın.", + "keycard-onboarding-intro-text": "Hazırlanın, bu birkaç dakika sürebilir, ancak hesabınızın güvenliğini sağlamak önemlidir.", + "keycard-onboarding-mnemonic-text": "Ayrıca imza kelimelerinizi yazmak için bir parça kağıda ve bir kaleme ihtiyacınız olacak.", + "keycard-onboarding-pairing-header": "Kart eşleştiriliyor ...", + "keycard-onboarding-pin-text": "Keycardınıza erişimi korumak için kullanılacak 6 haneli bir şifre oluşturmanız gerekecektir.", + "keycard-onboarding-preparing-header": "Kart hazırlanıyor ...", + "keycard-onboarding-puk-code-header": "Kodları yaz \n ve güvenli bir şekilde saklayın", + "keycard-onboarding-recovery-phrase-description": "Anahtarınızı geri almak için bu kelime imzanıza ihtiyacınız var.Güvenli bir yere yazın ve saklayın.", + "keycard-onboarding-recovery-phrase-header": "İmza kelimelerinizi yedekleyin.", + "keycard-onboarding-recovery-phrase-text": "Gözlerinle iyi bak. Bu, anahtarınızı oluşturmak için kullanılan büyülü kelime imzalarınız.", + "keycard-onboarding-start-header": "Başlamak için kartı telefonunuzun arkasında tutun.", + "keycard-onboarding-start-step1": "Parola oluştur", + "keycard-onboarding-start-step1-text": "Yaklaşık 1 dakika. Anahtarlarınızı şifrelemek için 6 haneli bir şifre oluşturun", + "keycard-onboarding-start-step2": "PUK ve eşleme kodunu yazın", + "keycard-onboarding-start-step2-text": "Yaklaşık 1 dakika sürecek.Bir parça kağıda ve kaleme ihtiyacınız olacak", + "keycard-onboarding-start-step3": "İmza kelimelerinizi yedekleyin", + "keycard-onboarding-start-step3-text": "Yaklaşık 1 dakika sürecek. Ayrıca bir parça kağıt ve kalem gereklidir", + "keycard-onboarding-start-text": "Kurulum sırasında kart ile telefon temasını sürdürün.Kurulum yaklaşık 4 dakika sürecek", + "keycard-processing-description": "Kartı sabit tutmayı deneyin", + "keycard-processing-title": "İşleniyor...", + "keycard-recover": "kayıp veya dondurulmuş kart?", + "keycard-recover-text": "Anımsatıcı ifadeniz varsa, bu hesapla ilişkili yeni bir Keycard oluşturabilirsiniz. Yeni bir Keycardı kullanabilir veya donmuş bir Keycardınızda fabrika ayarlarına sıfırlama yapabilirsiniz.", + "keycard-recover-title": "Bu hesap için yeni bir kart oluşturulsun mu?", + "keycard-recovery-intro-button-text": "Kurtarmayı başlat", + "keycard-recovery-intro-header": "Keycardınızda bulunan anahtarlarınızı kurtarın", + "keycard-recovery-intro-text": "Daha önce bir Keycard kullanarak anahtarlar oluşturduysanız ve şimdi bu anahtarları bu cihazda kullanmak mı istiyorsun", + "keycard-recovery-no-key-header": "Yapacak bir şey yok. \nburadan kurtarın", + "keycard-recovery-no-key-text": "Keycardınızda depolanmış bir anahtar yok. Kullanmak için yeni bir anahtar oluşturun ve anahtarı saklamak için Keycardınızı seçin", + "keycard-recovery-phrase-confirm-header": "İmza Kelimesini onaylayın", + "keycard-recovery-phrase-confirmation-text": "İkinci bir şansın olmayacak! Cihazınızı kaybederseniz veya değiştirirseniz, kelime imzalarınız yoksa Keycardınıza veya onunla ilişkili hiç bir şeye erişemezsiniz.Kelime imzanı bir yere yaz.Ve onu güvende tut.", + "keycard-recovery-phrase-confirmation-title": "Kelime imzanızı yazdınız mı?", + "keycard-recovery-success-header": "Anahtarlarınız \n başarıyla kurtarıldı", + "keycard-redeem-title": "Kullan", + "keycard-redeem-tx": "Varlıkları kullanın", + "keycard-redeem-tx-desc": "Varlıkları imzalamak ve almak için karta hafifçe dokunun", + "keycard-reset-passcode": "Parolayı sıfırla", + "keycard-success-description": "Kartı şimdi kaldırabilirsiniz", + "keycard-success-title": "Başarılı", + "keycard-unauthorized-operation": "Bu işlemi gerçekleştirme yetkiniz yok. \n Lütfen geçerli karta hafifçe vurun ve tekrar deneyin.", + "keycard-upsell-subtitle": "Gelişmiş güvenlik ve rahatlık", + "language": "Dil", + "last-backup-performed": "Gerçekleştirilen son yedekleme:", + "last-transaction": "Son islem", + "learn-more": "Daha fazla bilgi edinin", + "learn-more-about-keycard": "Keycard hakkında daha fazla bilgi edinin", + "leave": "Ayrıl", + "leave-chat": "Sohbetten ayrıl", + "leave-chat-confirmation": "Sohbetten ayrılmak istediğinizden emin misiniz?", + "leave-community": "Topluluktan ayrıl", + "leave-confirmation": "Sohbetten ayrıl", + "leave-group": "Gruptan ayrıl", + "left": "ayrıldı", + "les-ulc": "LES \/ ULC", + "lets-go": "Hadi gidelim", + "light": "Aydınlık", + "limit": "Limit", + "linked-on": "bağlandı {{date}}", + "load-messages-before": "{{date}} önce", + "load-more-messages": "Daha fazla mesaj getir", + "load-more-timeline": "↓ Daha fazlasını getir", + "loading": "Yükleniyor...", + "local-notifications": "Yerel bildirimler", + "lock-app-with": "Uygulamayı şununla kilitle:", + "log-level": "Günlük seviyesi", + "log-level-settings": "Günlük seviyesi ayarları", + "logging": "Günlükler", + "logging-enabled": "Günlük kaydı etkin mi?", + "login-pin-description": "Anahtarlarınızın kilidini açmak için 6 haneli şifrenizi girin", + "logout": "Çıkış Yap", + "logout-app-content": "Hesap oturumu kapatılacak. Tekrar kilidini açtığınızda, seçilen ağ kullanılır", + "logout-are-you-sure": "Çıkış yapmak istediğinden emin misin ?", + "logout-key-management": "Anahtar yönetimine erişmek için oturumu kapatmanız gerekir.", + "logout-title": "Çıkış Yap?", + "looking-for-cards": "Kart aranıyor ...", + "lost-connection": "Bağlantı koptu", + "mail-should-be-configured": "Mail istemcisi yapılandırılmalıdır", + "mailserver-address": "Status node adresi", + "mailserver-automatic": "Otomatik seçim", + "mailserver-automatic-switch-explanation": "Mevcut en hızlı Status nodesini seçin", + "mailserver-connection-error": "Status nodesine bağlanılamadı", + "mailserver-content": "Status ağında iletileri 30 güne kadar yönlendiren ve depolayan bir node.", + "mailserver-details": "Status nodesi ayrıntıları", + "mailserver-error-content": "Seçtiğiniz Status nodesine ulaşılamadı.", + "mailserver-error-title": "Status nodesine bağlanırken hata oluştu", + "mailserver-format": "enode: \/\/ {enode-id} @ {ip-address} : {port}", + "mailserver-pick-another": "Başka bir Status nodesi seçin", + "mailserver-reconnect": "Status nodesine bağlanılamadı. Yeniden bağlanmak için dokunun", + "mailserver-request-error-content": "Status nodesi tarafından şu hata oluştu: {{error}}", + "mailserver-request-error-status": "Geçmiş getirilirken bir hata oluştu, ayrıntılar için günlükleri kontrol edin", + "mailserver-request-error-title": "Status node isteği hatası", + "mailserver-request-retry": "İsteği yeniden deneyin", + "mailserver-retry": "Yeniden Dene", + "mailserver-title": "Status nodesi", + "main-currency": "Ana para birimi", + "main-networks": "Ana ağlar", + "main-wallet": "Ana Cüzdan", + "mainnet-network": "Ana ağ", + "make-admin": "Admin yap", + "manage-keys-and-storage": "Anahtarları ve depolamayı yönetin", + "mark-all-read": "Tümünü okundu olarak işaretle", + "master-account": "Ana hesap", + "max-fee": "Maksimum ücret", + "max-priority-fee": "Maksimum öncelik ücreti", + "maximum-fee": "Maksimum ücret", + "maximum-fee-desc": "Bu işlem için maksimum toplam ucret. Mevcut blok taban ücreti bunu aşars işleminiz daha düşük taban ücretle bir sonraki bloğa dahil edilecektir.", + "maybe-later": "Belki sonra", + "member-ban": "Üyeyi yasakla", + "member-kick": "Üyeyi at", + "members": { + "one": "1 üye", + "other": "{{count}} üyeler" + }, + "members-active": { + "one": "Üye", + "other": "{{count}}Üyeler" + }, + "members-active-none": "Üye yok", + "members-count": "{{count}} üyeler", + "members-label": "Üyeler", + "members-limit-reached": "Üye sınırına ulaşıldı", + "members-title": "Üyeler", + "membership-approval": "Onay gerekli", + "membership-approval-description": "Topluluğunuza katılmak ücretsizdir, ancak yeni üyelerin önce topluluk oluşturucu tarafından onaylanması gerekir", + "membership-button": "Üyelik şartı", + "membership-declined": "Üyelik talebi reddedildi", + "membership-description": "Grup üyeliği, grup yöneticisi tarafından kabul edilmenizi gerektirir", + "membership-ens": "ENS kullanıcı adı gerekli", + "membership-ens-description": "Topluluğunuzun katılabilmesi için bir ENS kullanıcı adı gerekiyor", + "membership-free": "Gerek yok", + "membership-free-description": "Topluluğunuz herkesin katılması için ücretsizdir", + "membership-invite": "Başka bir üyeden davet iste", + "membership-invite-description": "Topluluğunuza yalnızca mevcut topluluk üyelerinden gelen bir davetle katılabilir", + "membership-none": "Hiçbiri", + "membership-none-placeholder": "Yeni üyelerin katılmadan önce belirli kriterleri karşılamasını zorunlu kılabilirsiniz. Bu herhangi bir zamanda değiştirilebilir", + "membership-request-pending": "Üyelik talebi beklemede", + "membership-requests": "Üyelik talepleri", + "membership-title": "Üyelik şartı", + "message": "Mesaj", + "message-not-sent": "Mesaj gönderilmedi", + "message-options-cancel": "İptal", + "message-reply": "Cevap", + "messages": "Mesajlar", + "messages-from-contacts-only-subtitle": "Yalnızca kişi olarak eklediğiniz kişiler sizinle yeni bir sohbet başlatabilir veya sizi bir gruba davet edebilir", + "might-break": "Biraz ÐApps'ı kırabilir ", + "migration-successful": "Taşıma başarılı", + "migration-successful-text": "Hesap başarıyla Keycard'a taşındı", + "migrations-failed-content": "{{message}} \n şema sürümü: ilk {{initial-version}} , geçerli {{current-version}} , son {{last-version}} \n\n Bir veritabanı hatası oluştu. Paranız ve sohbet anahtarınız güvende. Sohbetleriniz ve kişileriniz gibi diğer veriler geri yüklenemez. \" {{erase-multiaccounts-data-button-text}} \" \"düğmesi diğer tüm verileri kaldırır ve paranıza erişmenize ve mesaj göndermenize olanak tanır.", + "miners-higher-fee": "Daha yüksek bir ücret öderseniz, madenciler muhtemelen işleminizi daha erkene dahil edeceklerdir.", + "mobile-network-ask-me": "Mobil ağdayken bana sor", + "mobile-network-continue-syncing": "Senkronizasyona devam et", + "mobile-network-continue-syncing-details": "Bunu daha sonra ayarlardan değiştirebilirsiniz", + "mobile-network-go-to-settings": "Ayarlara git", + "mobile-network-settings": "Mobil veri", + "mobile-network-sheet-configure": "Senkronizasyonu daha fazla yapılandırabilirsiniz .\n Detaylar burada", + "mobile-network-sheet-offline": "Wi-fi kapalı, mesaj senkronizasyonu devre dışı.", + "mobile-network-sheet-offline-details": "Mobil ağ kullanarak senkronizasyon kapalı", + "mobile-network-sheet-remember-choice": "Seçimimi hatırla", + "mobile-network-sheet-settings": "Ayarlar", + "mobile-network-start-syncing": "Senkronizasyonu başlat", + "mobile-network-stop-syncing": "Senkronizasyonu durdur", + "mobile-network-stop-syncing-details": "Kablosuz ağa bağlanana kadar?", + "mobile-network-use-mobile": "Mobil veri kullan", + "mobile-network-use-mobile-data": "Status, sohbetleri ve cüzdanı senkronize ederken çok fazla veri kullanır.", + "mobile-network-use-wifi": "Yalnızca kablosuz", + "mobile-syncing-sheet-details": "Status, sohbetleri ve cüzdanı senkronize ederken çok fazla veri kullanır.", + "mobile-syncing-sheet-title": "Mobil veri kullanarak senkronize edilsin mi?", + "more": "Daha", + "move-and-reset": "Taşı ve Sıfırla", + "move-keystore-file": "Anahtar deposu dosyasını taşı", + "move-keystore-file-to-keycard": "Anahtar deposu dosyası keycadınıza taşınsın mı?", + "multiaccount-exists-content": "Bu hesabın anahtarları zaten var ve tekrar eklenemiyor.Parolanızı, şifrenizi veya keycardınızı kaybettiyseniz, uygulamayı kaldırın, kelime imzanızı girerek anahtarlarınızı yeniden yükleyin ve erişin", + "multiaccount-exists-title": "Bu hesabın anahtarları zaten var", + "multiaccounts-recover-enter-phrase-text": "12, 15, 18, 21 veya 24 uzunluğunda kelime girin. \n Kelimeleri tek bir boşlukla ayırın.", + "multiaccounts-recover-enter-phrase-title": "kelime imzanızı girin", + "mute": "Sessiz", + "my-accounts": "Hesaplarım", + "my-accounts-empty": "Mevcut hesaplarınız burada görünecek", + "my-profile": "Benim profilim", + "my-status": "Durumum", + "name": "Ad", + "name-of-token": "Tokeninizin Adı", + "name-optional": "İsim: (İsteğe bağlı)", + "name-your-channel": "Kanalınızı adlandırın", + "name-your-channel-placeholder": "Kanal ismi", + "name-your-community": "Topluluğunuzu adlandırın", + "name-your-community-placeholder": "Akılda kalıcı bir isim", + "need-help": "Yardıma mı ihtiyacınız var?", + "network": "Ağ", + "network-chain": "Ağ zinciri", + "network-details": "Ağ ayrıntıları", + "network-fee": "Ağ ücreti", + "network-id": "Ağ kimliği", + "network-info": "Ağ bilgisi", + "network-invalid-network-id": "Belirtilen ağ kimliği, RPC URL'sinin ağ kimliğine karşılık gelmiyor", + "network-invalid-status-code": "Geçersiz status kodu: {{code}}", + "network-invalid-url": "Ağ URL'si geçersiz", + "network-settings": "Ağ ayarları", + "never": "Hiçbir zaman", + "new": "Yeni", + "new-category": "Yeni kategori", + "new-chat": "Yeni sohbet", + "new-community-title": "Yeni topluluk", + "new-contact": "Yeni iletişim", + "new-contract": "Yeni sözleşme", + "new-favourite": "Yeni favori", + "new-group": "Yeni Grup", + "new-group-chat": "Yeni grup sohbeti", + "new-network": "Yeni ağ", + "new-password": "Yeni Şifre", + "new-pin-description": "Yeni 6 haneli şifre girin", + "new-public-group-chat": "Herkese açık sohbete katılın", + "new-puk-description": "Yeni 12 haneli PUK girin", + "new-status": "Yeni Status", + "new-tab": "Yeni sekme", + "next": "Sonraki", + "nickname": "Takma ad", + "nickname-description": "Takma adlar, Status'da başkalarını tanımanıza yardımcı olur.\n Eklediğiniz takma adlarını yalnızca siz görebilirsiniz", + "no": "Hayır", + "no-collectibles": "Mevcut koleksiyonunuz yok", + "no-contacts": "Henüz kişi yok", + "no-keycard-applet-on-card": "Kartta Keycard uygulaması yok", + "no-messages": "Mesaj yok", + "no-pairing-slots-available": "Bu kart zaten 5 cihazla eşleştirildi ve bu kartla eşleştirilemiyor. Lütfen eşleştirilmiş cihazlardan birini kullanın, bu kartla giriş yapın ve karttaki eşleştirme yuvalarını boşaltın", + "no-pinned-messages": "Sabitlenmiş mesaj yok", + "no-result": "Sonuç yok", + "no-thanks": "Hayır, Teşekkürler", + "no-tokens-found": "Token bulunamadı.", + "node-address": "Node adresi", + "node-details": "Node ayrıntıları", + "node-info": "Node bilgisi", + "node-version": "Düğüm sürümü", + "nodes-disabled": "Status nodeleri devre dışı bırakıldı", + "non-archival-node": "RPC uç noktası arşiv isteklerini desteklemez. Yerel transfer geçmişiniz eksik olabilir.", + "non-contacts": "Kişi olmayanlar", + "nonce": "nonce", + "none": "Hiçbiri", + "normal": "Normal", + "not-applicable": "İmzasız işlemler için geçerli değildir", + "not-connected-nodes": "Bir status nodesine bağlı değil", + "not-connected-to-peers": "Herhangi bir eşe bağlı değil", + "not-enough-snt": "Yeterli SNT yok", + "not-found": "Bulunamadı", + "not-keycard-text": "Kullandığınız kart bir Keycard değil. Kullanmak için bir Keycard satın almanız gerekiyor", + "not-keycard-title": "Keycard değil", + "not-registered": "kayıtlı değil", + "notification-settings": "Bildirimler", + "notifications": "Bildirimler", + "notifications-non-contacts": "Kişi olmayanlardan gelen bildirimler", + "notifications-preferences": "Bildirim Tercihleri", + "notifications-servers": "Bildirim sunucuları", + "notifications-switch": "Bildirimleri göster", + "notifications-transactions": "Cüzdan işlemleri", + "notify": "Bildirim", + "now": "Şimdi", + "off": "Kapalı", + "off-status-tree": "Staus ağacı kapalı", + "offline": "Çevrimdışı", + "offline-messaging-use-history-explanation": "Uygulama kapalıyken gönderilen mesajları almak için Status nodelerini etkinleştirin. Etkinleştirildiğinde, bir Status nodesi ip adresinizi alır. Devre dışı bırakıldığında, uygulama kapatıldığında mesaj almayacaksınız ve uygulamayı daha sonra açtığınızda bunları görmeyeceksiniz.", + "offline-messaging-use-history-nodes": "Status nodelerini kullan", + "ok": "tamam", + "ok-continue": "Tamam, devam et", + "ok-got-it": "Tamam anladım", + "ok-save-pass": "Tamam, şifreyi kaydet", + "okay": "Tamam", + "on": "Aç", + "on-status-tree": "Status ağacında", + "once-enabled-share-metadata": "Etkinleştirildikten sonra, sohbette yayınlanan bağlantılar ve meta verileriniz siteyle paylaşılır.", + "one-day": "Bir gün", + "one-month": "Bir ay", + "one-week": "Bir hafta", + "open": "Açık", + "open-chat": "Sohbeti aç", + "open-dapp": "ÐApp'ı aç", + "open-dapp-store": "ÐApps'ı Keşfedin", + "open-home": "Açık...", + "open-in-new-tab": "Yeni sekmede aç", + "open-membership": "Üyelik açık", + "open-nfc-settings": "NFC ayarlarını aç", + "opening-buy-crypto": "{{site}} açılıyor ...", + "optimal": "En uygun", + "optional": "isteğe bağlı", + "or": "Veya", + "outgoing": "Giden", + "outgoing-transaction": "Giden işlem", + "page-camera-request-blocked": "Kamera istekleri engellendi. Kamera isteklerini etkinleştirmek için Ayarlar'a gidin", + "page-would-like-to-use-camera": "Kameranı kullanmak ister misin", + "pair": "Cihazları eşleştir", + "pair-card": "Bu cihazla eşleştir", + "pair-code": "Eşleme kodu", + "pair-code-explanation": "Anahtarları açmak ve aynı Keycard ile işlemleri imzalamak için kartı farklı bir cihaza (5'e kadar) eşleştirir", + "pair-code-placeholder": "Kodu eşle ...", + "pair-this-card": "Bu kartı eşleştir", + "pair-this-device": "Diğer cihazların telefonunuzu bulmasına izin verin", + "pair-this-device-description": "Kişileri ve sohbetleri aralarında senkronize etmek için cihazlarınızı eşleyin", + "paired-devices": "Eşleşmiş cihazlar", + "pairing": "Eşleştirme", + "pairing-card": "Eşleşen kart", + "pairing-changed": "Eşleştirme kodu değiştirildi", + "pairing-code-placeholder": "Eşleştirme kodu...", + "pairing-code_error1": "Eşleştirme kodları eşleşmiyor.", + "pairing-go-to-installation": "Eşleme ayarlarına gidin", + "pairing-maximum-number-reached-content": "Lütfen yeni bir cihazı etkinleştirmeden önce cihazlarınızdan birini devre dışı bırakın.", + "pairing-maximum-number-reached-title": "Maksimum cihaz sayısına ulaşıldı", + "pairing-new-installation-detected-content": "Yeni bir cihaz algılandı. \n Cihazlarınızı doğru bir şekilde kullanmak için kullanmadan önce eşleştirmek ve etkinleştirmek önemlidir. \n Cihazlarınızı eşleştirmek için lütfen ayarlar altındaki cihaz bölümüne gidin.", + "pairing-new-installation-detected-title": "Yeni cihaz algılandı", + "pairing-no-info": "Bilgi yok", + "pairing-please-set-a-name": "Lütfen cihazınız için bir ad belirleyin.", + "passphrase": "Parola", + "password": "Şifre", + "password-description": "En az 6 karakter. Parolanız anahtarlarınızı korur. Statusun kilidini açmak ve işlem yapmak için buna ihtiyacınız var.", + "password-mismatch": "Yeni şifre ve onay eşleşmiyor", + "password-placeholder": "Parola...", + "password-placeholder2": "Parolanızı doğrulayın", + "password-reset-in-progress": "Şifre değiştiriliyor...", + "password-reset-success": "Parola değiştirildi", + "password-reset-success-message": "Tekrar oturum açmanız gerekecek", + "password_error1": "Parolalar uyuşmuyor.", + "paste": "Yapıştır", + "paste-json": "JSON'u yapıştır", + "pay-to-chat": "Sohbete öde", + "peer-content": "Status sohbet ağına bağlı bir cihaz. Her kullanıcı, cihaz sayısına bağlı olarak bir veya daha fazla eş temsil edebilir.", + "peer-title": "Eş", + "peer-to-peer": "Eşler arası", + "peers": "Eşler", + "pending": "Bekliyor", + "pending-confirmation": "Onay bekleniyor...", + "pending-invitations": "Bekleyen üyelik istekleri", + "per-gas-price-limit": "Gaz başına fiyat limiti", + "per-gas-tip-limit": "Gaz başına uç sınır", + "perform-backup": "Yedeklemeyi gerçekleştir", + "permissions": "İzinler", + "phone-e164": "Uluslararası 1", + "photos": "Fotoğraflar", + "photos-access-error": "Gerekli fotoğraflara izin vermek için lütfen sistem ayarlarınıza gidin ve Status > Fotoğraflar'ın seçili olduğundan emin olun.", + "pin": "Sabit", + "pin-changed": "6 haneli şifre değiştirildi", + "pin-code": "6 haneli şifre", + "pin-limit-reached": "Pin sınırına ulaşıldı. Önce bir önceki mesajın sabitlemesini kaldırın.", + "pin-mismatch": "Yanlış şifre", + "pin-one-attempt": "bir deneme", + "pin-one-attempt-blocked-after": "Keycardınız engellenmeden önce", + "pin-one-attempt-blocked-before": "Sadece, Dikkatli Ol", + "pin-one-attempt-frozen-after": "Keycardınız dondurulmadan önce", + "pin-one-attempt-frozen-before": "Sadece, Dikkatli Ol", + "pin-retries-left": "{{number}} kaldı", + "pinned-by": "tarafından sabitlendi", + "pinned-messages": "Sabitlenmiş mesajlar", + "pinned-messages-count": { + "one": "1 sabitlenmiş mesaj", + "other": "{{count}} sabitlenmiş mesajlar" + }, + "pinned-messages-empty": "Sabitlenmiş mesajlar burada görünecektir. Bir mesajı sabitlemek için, basılı tutun ve \"Sabitle\"ye dokunun", + "preference": "Tecihler", + "preview-privacy": "Gizlilik modunu önizleme", + "previewing-may-share-metadata": "Bu web sitelerinden gelen bağlantılarını önizleyebilir, meta verilerinizi sahipleriyle paylaşabilir.", + "principles": "Prensipler", + "privacy": "Gizlilik", + "privacy-and-security": "Gizlilik ve güvenlik", + "privacy-photos": "Profil fotoğrafı gizliliği", + "privacy-policy": "Gizlilik Politikası", + "privacy-show-to-warning": "Profil resminizi daha önce görmüş olan kişiler bunu yapmaya devam edecek", + "private-key": "Özel anahtar", + "private-notifications": "Özel bildirimler", + "private-notifications-descr": "Status yeni mesajlar hakkında sizi bilgilendirir. Bildirim tercihlerinizi daha sonra ayarlardan düzenleyebilirsiniz.", + "processing": "Sadece bir dakika", + "product-information": "Ürün Bilgisi", + "profile": "Profil", + "profile-deleted-content": "Profiliniz başarıyla silindi", + "profile-deleted-keycard": "Artık Keycardınıza başka bir anahtar çiftini geri yükleyebilirsiniz.", + "profile-deleted-title": "Profil silindi", + "profile-details": "Profil detayları", + "profile-not-found": "Profil bulunamadı", + "profile-pic-pick": "Galeriden seç", + "profile-pic-remove": "Fotoğrafı kaldır", + "profile-pic-take": "Fotoğraf çek", + "profile-picture-updated": "Profil resmi güncellendi", + "public-channel": "Herkese açık kanal", + "public-chat": "Genel sohbet", + "public-chat-description": "İlgi alanlarınız için herkese açık sohbetlere katılın! Herkes yeni bir tane başlatabilir.", + "public-chats": "Genel sohbetler", + "public-group-status": "Genel", + "public-group-topic": "Konu", + "public-key": "Genel anahtar", + "puk-and-pairing-codes-displayed": "PUK ve eşleştirme kodlarını görüntüle", + "puk-changed": "12 haneli PUK değiştirildi", + "puk-code": "PUK kodu", + "puk-code-explanation": "6 basamaklı şifrenizi unutursanız veya 3 kez yanlış girerseniz, kartınızın kilidini açmak için bu koda ihtiyacınız olacak", + "puk-mismatch": "Yanlış PUK kodu", + "push-failed-transaction": "İşleminiz başarısız oldu", + "push-failed-transaction-body": "{{value}} {{currency}} için {{to}}", + "push-inbound-transaction": "{{value}} {{currency}} aldınız", + "push-inbound-transaction-body": "{{from}} gönderen {{from}} - {{to}}", + "push-notifications-server-enabled": "Sunucu etkinleştirildi", + "push-notifications-servers": "Push bildirim sunucuları", + "push-outbound-transaction": "{{value}} {{currency}} gönderdiniz", + "push-outbound-transaction-body": "{{from}} gönderen {{from}} - {{to}}", + "quiet-days": "{{quiet-days}} gün", + "quiet-hours": "{{quiet-hours}} saat", + "re-encrypt-key": "Anahtarlarınızı yeniden şifreleyin", + "rearrange-categories": "Kategorileri yeniden düzenle", + "receive": "Al", + "receive-transaction": "İşlem al", + "recent": "Son", + "recent-empty": "Son kullanılan adresler burada görünecek", + "recent-recipients": "İletişim", + "recently-used-stickers": "Son kullanılan etiketler burada görünecek", + "recipient": "alıcı", + "recipient-code": "Alıcı adresini girin", + "recipient-code-placeholder": "0x ... veya kullanıcıadı.domain.eth", + "recover": "Kurtarmak", + "recover-key": "Mevcut anahtarlara erişin", + "recover-keycard-multiaccount-not-supported": "Bu hesabın anahtarları zaten var ve tekrar eklenemiyor.Parolanızı, şifrenizi veya keycardınızı kaybettiyseniz, uygulamayı kaldırın, kelime imzanızı girerek anahtarlarınızı yeniden yükleyin ve erişin", + "recover-with-keycard": "Keycard ile kurtar", + "recover-with-seed-phrase": "İmza kelimeniz ile kurtar", + "recovering-key": "Anahtarlara erişiliyor ...", + "recovery-confirm-phrase": "Kelime imzanızı onaylayın.", + "recovery-phrase": "Kelime imzanız", + "recovery-success-text": "Anahtarlarınızı yeniden şifrelemek için yeni bir kod veya şifre oluşturmanız gerekecek", + "recovery-typo-dialog-description": "Lütfen dikkat, kelime imzanız için seçtiğiniz kelimelerin her sırası aynı kelimelerden oluşmalıdır.", + "recovery-typo-dialog-title": "Kelime imzası doğru mu?", + "redeem-amount": "{{quantity}}bonusları kullanılabilir", + "redeem-now": "Şimdi kullan", + "redeem-success": "Bonusu başarıyla kullandın!", + "refresh": "Yenile", + "registered": "kayıtlı", + "reject-and-delete": "Reddet ve sil", + "remember-me": "Beni hatırla", + "remind-me-later": "Bunu tekrar göster", + "remote-notifications-subtitle": "Google push bildirimlerini etkinleştirin", + "remove": "Kaldır", + "remove-favourite": "Favoriyi kaldır", + "remove-from-chat": "Sohbetten kaldır", + "remove-from-contacts": "Kişilerden kaldır", + "remove-from-contacts-text": "Bir kullanıcıyı kişi listenizden kaldırsanız bile cüzdan adresinizi onlardan gizleyemezsiniz.", + "remove-group": "Grubu kaldır", + "remove-network": "Ağı kaldır", + "remove-token": "Tokeni Sil", + "removed": "kaldırıldı", + "repeat-pin": "Yeni 6 haneli şifreyi tekrarlayın", + "repeat-puk": "Yeni 12 haneli PUK'u tekrarlayın", + "replying-to": "{{author}} yanıtlanıyor", + "report-bug-email-template": "1. Sorun Açıklaması\n( İstediğiniz özelliği açıklayın veya hatayı ve ne yaptığınızı, ne olmasını beklediğinizi ve gerçekte ne olduğunu kısaca özetleyin. Aşağıdaki bölümler )\n2. Birincil adım\n( Hatayı adım adım nasıl kopyalayabileceğimizi açıklayın. )\nStatus açın\n3. Adım vb.\n3. Beklenen davranış\n( Ne olmasını beklediğinizi açıklayın. )\n4. Gerçek davranış\n( Gerçekte ne olduğunu açıklayın. )\n5. sorunu demo olabilir ekran görüntüleri ekleyin, lütfen", + "request-access": "Erişim talep et", + "request-feature": "Bir özellik isteyin", + "request-membership": "Üyelik iste", + "request-pending": "Bekleyen istek…", + "request-transaction": "İşlem iste", + "required-field": "gerekli alan", + "resend-message": "Yeniden gönder", + "reset-card": "Kartı sıfırla", + "reset-card-description": "Bu işlem kartı başlangıç durumuna sıfırlar. Özel anahtarlar dahil tüm kart verilerini silecektir. İşlem geri döndürülemez.", + "reset-database": "Veritabanını sıfırla", + "reset-database-warning": "Sohbetleri, kişileri ve ayarları silin. Şifrenizi kaybettiğinizde gereklidir", + "reset-database-warning-keycard": "Sohbetleri, kişileri ve ayarları silin.", + "reset-password": "Şifreyi yenile", + "restore-defaults": "Varsayılanları Geri Yükle", + "retry": "Yeniden Dene", + "revoke-access": "Erişimi iptal et", + "rpc-url": "RPC URL'si", + "rpc-usage-copy": "Kopyala", + "rpc-usage-filter": "Filtre yöntemleri", + "rpc-usage-get-stats": "Yenile", + "rpc-usage-info": "RPC kullanım istatistikleri", + "rpc-usage-reset": "Sıfırla", + "safe-estimate": "Güvenli tahmin", + "save": "Kaydet", + "save-password": "Şifreyi kaydet", + "save-password-unavailable": "Şifreyi kaydetmek için cihaz şifresini ayarlayın", + "save-password-unavailable-android": "Şifreyi kaydet kullanılamıyor: cihazınız rootlanmış olabilir veya gerekli güvenlik izinlerine sahip olmayabilir.", + "scan-qr": "QR kodunu tara", + "scan-qr-code": "Cüz adresininin QR ile tarayın.", + "scan-tokens": "Tokenleri tara", + "search": "Arama", + "search-no-chat-found": "Arama sonucu bulunamadı. Ne aramak istedin ?", + "secret-keys-confirmation-text": "Telefonunuzu kaybetmeniz durumunda Keycardınızı kullanmaya devam etmeniz için onlara ihtiyacınız olacaktır.", + "secret-keys-confirmation-title": "Kodları yazdınız mı?", + "security": "Güvenlik", + "see-details": "Ayrıntılara bakınız", + "see-it-again": "TEKRAR GÖR", + "see-suggestions": "Önerileri görün", + "seed-key-uid-mismatch": "İmza kelimeniz eşleşmiyor", + "seed-key-uid-mismatch-desc-1": "Girdiğiniz kelime imza öbeği {{multiaccount-name}} eşleşmiyor", + "seed-key-uid-mismatch-desc-2": "Bu hesabın anahtarlarını yönetmek için kelime imzalarınızı doğrulayın ve tekrar deneyin.", + "seed-phrase-content": "BIP39 standart listesinden rastgele seçilen ve Ethereum hesabınızı diğer cüzdanlarda ve cihazlarda kurtarmak veya erişmek için kullanılan bir dizi okunması kolay kelimeler. Kripto ekosisteminde “anımsatıcı ifade”, “kurtarma ifadesi” veya “cüzdan yedeklemesi” olarak da adlandırılır. Çoğu kripto uygulaması hesap oluşturmak için aynı standardı kullanır.", + "seed-phrase-placeholder": "Kelime imzası...", + "seed-phrase-title": "Kelime imzanız", + "select": "Seçin", + "select-account": "Hesabı seçin", + "select-account-dapp": "Dapps kullanmak istediğiniz hesabı seçiniz.", + "select-account-first": "Önce bir hesap seçin", + "select-chat": "Mesajlaşmaya başlamak için bir sohbet seçin", + "select-new-location-for-keys": "Özel anahtarlarınızı kaydetmek için yeni bir konum seçin", + "selected": "Seçili", + "send-logs": "Hata bildirin", + "send-logs-to": "Bu hatayı {{email}} adresine bildir", + "send-message": "Mesaj gönder", + "send-push-notifications-description": "Devre dışı bırakıldığında, mesajlarınızı alan kişiye mesajın geldiği bildirilmez.", + "send-request": "İstek gönder", + "send-request-amount": "Miktar", + "send-request-amount-max-decimals": "Maksimum ondalık sayı {{asset-decimals}}", + "send-request-unknown-token": "Bilinmeyen token {{asset}}", + "send-sending-to": "{{recipient-name}}için", + "send-transaction": "İşlem gönder", + "sending": "Gönderiyorsun", + "sent-at": "Gönderme tarihi", + "server": "Sunucu", + "set-a-topic": "Konu oluştur", + "set-currency": "Para birimini ayarla", + "set-custom-fee": "Özel ücret belirle", + "set-dapp-access-permissions": "DApp erişim izinlerini ayarla", + "set-max": "Maksimum ayarlayın", + "settings": "Ayarlar", + "share": "Paylaş", + "share-address": "Adresi paylaş", + "share-chat": "Sohbeti paylaş", + "share-contact-code": "Sohbet anahtarımı paylaş", + "share-dapp-text": "Status'da kullandığım şu DApp'e göz atın: {{link}}", + "share-link": "Bağlantıyı paylaş", + "share-logs": "Günlükleri paylaş", + "share-my-profile": "Profilimi paylaş", + "share-profile": "Profili paylaş", + "share-profile-link": "Profil bağlantısını paylaş", + "share-public-chat-text": "Status uygulamasında şu genel sohbete göz atın: {{link}}", + "shared": "Paylaşıldı", + "sharing-copied-to-clipboard": "Kopyalandı", + "sharing-copy-to-clipboard": "Kopyala", + "sharing-data-desc-1": "Hassas verilerin gönderilmediğinden emin olmak için veriler genel kurallara göre doğrulanır. Güvenmeyin, onaylayın.", + "sharing-data-desc-2": "Kullanım verileri, Status eşler arası ağ üzerinden uçtan uca şifreli olarak gönderilir", + "sharing-data-desc-3": "Normal sohbet anahtarınız yerine tek kullanımlık bir anahtar kullanılır", + "sharing-data-desc-4": "Kullanım verileri IP adresinizle ilişkilendirilemez", + "sharing-data-desc-5": "Tüm kullanıcıların toplu verileri herkese açık", + "sharing-data-desc-6": "Veriler gönderildikten sonra telefonunuzdan kaldırılır", + "sharing-share": "Paylaş", + "show-less": "Daha az göster", + "show-more": "Daha fazla göster", + "show-notifications": "Bildirimleri göster", + "show-profile-pictures": "Profil resimlerine bakın", + "show-profile-pictures-to": "Profil resmini göster", + "show-qr": "QR kodunu göster", + "show-transaction-data": "İşlem verilerini göster", + "sign-and-send": "İmzala ve gönder", + "sign-anyway": "Yine de imzala", + "sign-in": "Oturum Aç", + "sign-message": "Mesajı İmzala", + "sign-out": "Oturumu kapat", + "sign-request-failed": "Mesaj imzalanamadı", + "sign-with": "İle imzala", + "sign-with-password": "Şifre ile oturum açın", + "sign-you-in": "Oturumunuz açılıyor…", + "signing": "İmzala", + "signing-a-message": "Mesaj imzalama", + "signing-phrase": "Kelime imzası", + "skip": "Atla", + "slow": "Yavaş", + "something-went-wrong": "Bir şeyler yanlış gitti", + "soon": "Yakında", + "specify-address": "Adresi belirtin", + "specify-name": "Bir ad belirtin", + "specify-network-id": "Ağ kimliğini belirtin", + "specify-rpc-url": "Bir RPC URL'si belirtin", + "specify-server-public-key": "Sunucu genel anahtarını girin", + "specify-symbol": "Bir sembol belirtin", + "start-chat": "Sohbeti başlat", + "start-conversation": "Görüşmeye başla", + "start-group-chat": "Grup sohbeti başlat", + "start-new-chat": "Yeni sohbet başlat", + "starter-pack-coming": "Başlangıç Paketi yolunuzu geliyor", + "starter-pack-coming-description": "Birkaç dakika veya saat sürebilir", + "starter-pack-received": "Başlangıç Paketi alındı", + "starter-pack-received-description": "İşte başlamanız için bazı kriptolar! Çıkartmalar, bir ENS adı almak ve dapp'leri denemek için kullanın", + "status": "Status", + "status-always-online": "Her zaman çevrimiçi", + "status-automatic": "Otomatik", + "status-automatic-subtitle": "Durumu otomatik olarak ayarla", + "status-confirmed": "Onaylandı", + "status-dnd": "Rahatsız etmeyin", + "status-dnd-subtitle": "Tüm bildirimleri sessize al", + "status-hardwallet": "Status hardwallet", + "status-inactive": "etkin değil", + "status-inactive-subtitle": "Çevrimiçi durumunuzu gizler", + "status-is-open-source": "Status açık kaynaklıdır", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status, sohbetleri senkronize ederken çok fazla veri kullanabilir. Mobil ağdayken senkronize etmemeyi seçebilirsiniz", + "status-not-sent-click": "Onaylanmadı. Seçenekler için tıklayın", + "status-not-sent-tap": "Onaylanamadı. Seçenekler için hafifçe dokunun.", + "status-pending": "Bekliyor", + "status-sent": "Gönderildi", + "status-tx-not-found": "TX bulunamadı", + "status-updates-descr": "Status güncellemeleri burada görünecektir. Zaman çizelgenizle ilgili güncellemeleri almak için profili kişi olarak ekleyin.", + "statuses-descr": "Aklınızdakileri paylaşın ve kişilerinizle güncel kalın", + "statuses-my-status-descr": "Aklından geçenleri paylaş. Profilinizi ziyaret eden herkes durumunuzu görebilir. Sizi kişi olarak ekleyen kişiler güncelleştirmelerinizi zaman çizelgelerine göre alır", + "step-i-of-n": "Adım {{step}}{{number}}", + "sticker": "Etiket", + "sticker-market": "Sticker marketi", + "storage": "Depolama", + "submit": "Gönder", + "submit-bug": "Bir hata gönderin", + "success": "Başarılı", + "suggested-min-tip": "Önerilen min.", + "suggested-price-limit": "Önerilen fiyat limiti", + "symbol": "Sembol", + "sync-all-devices": "Tüm cihazları senkronize et", + "sync-in-progress": "Senkronize ediliyor ...", + "sync-settings": "Senkronizasyon ayarları", + "sync-synced": "Senkronize edildi", + "syncing-devices": "Senkronize ediliyor ...", + "system": "Sistem", + "tabs": "Sekmeler", + "tag-was-lost": "Etiket kayboldu", + "tap-card-again": "Telefonunuzun arkasındaki karta hafifçe dokunun.", + "terms-of-service": "Kullanım Şartları", + "test-networks": "Test ağları", + "text-input-disabled": "Lütfen bir dakika bekleyin...", + "thank-you": "Teşekkür ederim", + "this-device": "Bu cihaz", + "this-device-desc": "Anahtarlarınız şifrelenecek ve cihazınızda güvenli bir şekilde saklanacak", + "this-is-you-signing": "Bu sizin kelime imzanız", + "this-will-take-few-seconds": "Bu birkaç saniye sürecek", + "three-days": "Üç gün", + "three-words-description": "Her bir işlemi imzalamadan önce bu 3 kelimeyi görmelisiniz", + "three-words-description-2": "Farklı bir kombinasyon görürseniz işlemi iptal edin ve oturumu kapatın", + "timeline": "Zaman çizelgesi", + "to": "için", + "to-block": "Engelle", + "to-enable-biometric": "{{bio-type-label}} özelliğini etkinleştirmek için kilit açma ekranında şifrenizi kaydetmelisiniz", + "to-encrypt-enter-password": "Hesabızı şifrelemek için lütfen şifrenizi girin.", + "to-see-this-message": "Bu mesajı görmek için,", + "token-auto-validate-decimals-error": "{{address}} adresindeki {{symbol}} belirtecinin yanlış ondalıkları, {{expected}} olarak ayarlandı ancak {{actual}} olarak algılandı", + "token-auto-validate-name-error": "{{address}} adresindeki {{symbol}} belirtecinin yanlış ondalıkları, {{expected}} olarak ayarlandı ancak {{actual}} olarak algılandı", + "token-auto-validate-symbol-error": "{{address}} adresindeki {{symbol}} belirtecinin yanlış ondalıkları, {{expected}} olarak ayarlandı ancak {{actual}} olarak algılandı", + "token-details": "Token detayları", + "topic-name-error": "Yalnızca küçük harfleri (a'dan z'ye), sayılar ve tireler (-) kullanın. Sohbet tuşlarını kullanmayın", + "total-gas": "Toplam gas ucreti", + "transaction": "işlem", + "transaction-data": "Işlem verileri", + "transaction-declined": "İşlem reddedildi", + "transaction-description": "Ağdaki 12 onaydan sonra tamamlanmış olarak düşünün.", + "transaction-details": "Ödeme detayları", + "transaction-failed": "İşlem başarısız oldu.", + "transaction-history": "İşlem geçmişi", + "transaction-request": "İşlem İsteği", + "transaction-sent": "İşlem gönderildi", + "transaction-signed": "İşlem başarıyla imzalandı", + "transactions": "işlemler", + "transactions-filter-select-all": "Hepsini seç", + "transactions-filter-title": "Geçmişi filtrele", + "transactions-history": "İşlem geçmişi", + "transactions-history-empty": "Geçmişinizde henüz işlem yok", + "transactions-history-loading": "İşlem geçmişi yükleniyor. Bu biraz zaman alabilir.", + "transactions-load-more": "Daha fazla yükle", + "transactions-management-enabled": "İşlem yönetimi (alfa)", + "transactions-sign": "İmzala", + "transfer-ma-unknown-error-desc-1": "Görünüşe göre çoklu hesabınız silinmemiş. Veritabanı sıfırlanmış olabilir", + "transfer-ma-unknown-error-desc-2": "Lütfen hesap listenizi kontrol edin ve tekrar deneyin. Hesap listelenmemişse, imza kelimeniz ile kurtarmak için mevcut anahtarlara erişin bölümüne gidin.", + "transfers-fetching-failure": "Transfer geçmişi güncellenemedi. Bağlantınızı kontrol edin ve tekrar denemek için aşağı elinizle kaydırın.", + "tribute-required-by-multiaccount": "{{multiaccount-name}} sohbeti başlatmak için SNT gerekiyor", + "tribute-state-paid": "Ücret ödendi", + "tribute-state-pending": "Ücret bekleniyor", + "tribute-state-required": "{{snt-amount}}Snt ödemesi gerektirir.", + "tribute-to-talk": "Tribute to Talk", + "tribute-to-talk-add-friends": "Ödeme yapmadan sohbetlere izin vermek için arkadaşlarınızı kişi olarak ekleyin.", + "tribute-to-talk-are-you-friends": "Siz arkadaş mısınız?", + "tribute-to-talk-ask-to-be-added": "Kişi olarak eklenmesini isteyin", + "tribute-to-talk-contact-received-your-tribute": "Ücreti aldı. Artık birbirinizle güvenli bir şekilde sohbet edebilirsiniz.", + "tribute-to-talk-desc": "Yeni kişilerin sohbet başlatması için SNT talep ederek dikkatinizi paraya çevirin", + "tribute-to-talk-disabled": "Tribute to Talk devre dışı", + "tribute-to-talk-disabled-note": "Artık yeni kişiler SNT göndermeden sizinle sohbet etmeye başlayabilir.", + "tribute-to-talk-enabled": "Tribute to Talk'u etkinleştirdiniz.", + "tribute-to-talk-finish-desc": "Şu andan itibaren, yalnızca kişilerden ve ödeme yapan kişilerden sohbetler alacaksınız", + "tribute-to-talk-learn-more-1": "Zamanınız ve dikkatiniz en değerli varlıklarınızdır. Tribute to Talk, yeni kişilerin sizinle sohbet etmeye başlaması için gereken miktarda SNT ayarlamanızı sağlar.", + "tribute-to-talk-learn-more-2": "Kişi listenizde olmayan herkesin ödeme yapması istenir ve ödeme yaptıktan sonra yanıt verebilirsiniz.", + "tribute-to-talk-learn-more-3": "Parayı her zaman geri gönderebilirsin, ancak arkadaşlarınızın size özgürce ulaşabilmesini sağlamak için, önce bir kişi olarak ekleyin.", + "tribute-to-talk-paywall-learn-more-1": "Zamanınız ve dikkatiniz en değerli varlıklarınızdır. Tribute to Talk, yeni kişilerin sizinle sohbet etmeye başlaması için gereken miktarda SNT ayarlamanızı sağlar.", + "tribute-to-talk-paywall-learn-more-2": "Ödemesi olan biriyle sohbet etmek için, gerekli SNT'yi ödemeniz yeterlidir ve bir kişi olarak eklenirsiniz.", + "tribute-to-talk-paywall-learn-more-3": "Bunu biliyor muydunuz ? Ücretsiz olarak eklenecek profilinizi Status dışında paylaşabilirsiniz.", + "tribute-to-talk-pending": "Ücret onaylanması bekleniyor", + "tribute-to-talk-pending-note": "Ödeme işlemi ağda onay bekliyor. İşlem geçmişinde durumunu kontrol edebilirsiniz", + "tribute-to-talk-removing-note": "Tribute to Talk'un kaldırılması, yeni kişilerin SNT göndermeden sohbet başlatmasına izin verir.İşlem gerektirir.", + "tribute-to-talk-set-snt-amount": "Yeni kişilerin sohbet başlatabilmesi için gereken SNT miktarını ayarla", + "tribute-to-talk-signing": "İşlem imzalanmayı bekliyor ...", + "tribute-to-talk-transaction-failed-note": "İşlem başarısız oldu ve Tribute to Talk, ayarlarınız değiştirilmedi", + "tribute-to-talk-tribute-received1": "Ücret alındı. Sen ve", + "tribute-to-talk-tribute-received2": "Şimdi kişiler birbirleriyle güvenli bir şekilde sohbet edebilirler.", + "tribute-to-talk-you-require-snt": "Yeni kişiler ile sohbete başlamak için SNT'ye ihtiyacınız var.", + "try-again": "Tekrar deneyin", + "try-keeping-the-card-still": "Kartı sabit tutmayı deneyin", + "turn-nfc-description": "Cihazınızda NFC devre dışı. Ayarlardan etkinleştirebilirsiniz.", + "turn-nfc-on": "Devam etmek için NFC'yi açın", + "tx-fail-description1": "Bu işlem büyük olasılıkla başarısız olacaktır. Özel ağ ücreti kullanarak kendi sorumluluğunuzda işlemi imzalayın.", + "tx-fail-description2": "Bu işlem büyük olasılıkla başarısız olacaktır. Kendi sorumluluğunuzdadır.İşlemi imzalamak için özel bir ağ ücreti belirleyin.", + "type": "Türü", + "type-a-message": "Mesaj", + "ulc-enabled": "ULC etkin", + "unable-to-fetch": "Sohbet geçmişi getirilemiyor", + "unable-to-read-this-code": "Bu kod okunamıyor", + "unable-to-send-messages": "Mesaj gönderilemiyor ve alınamıyor", + "unblock": "Engeli kaldır", + "unblock-contact": "Bu kullanıcının engelini kaldır", + "unknown-status-go-error": "Bilinmeyen status-go hatası", + "unlimited": "Limitsiz", + "unlock": "Kilidini aç", + "unmute": "Sessize Al", + "unpair-card": "Eşleştirilmemiş kart", + "unpair-card-confirmation": "Bu işlem kartın eşleştirmesini mevcut cihazınızdan kaldıracaktır. 6 basamaklı parola yetkilendirmesi gerektirir. Devam etmek istiyor musunuz?", + "unpair-keycard": "Keycardını bu telefondan eşlemesini kaldır", + "unpair-keycard-warning": "Eşleştirme kodunuz\/PUK ve PIN'iniz değişmeden kalır", + "unpaired-keycard-text": "Dokunduğunuz Keycard bu telefonla ilişkili değil", + "unpaired-keycard-title": "Kartınız eşleştirilmemiş gibi görünüyor", + "unpin": "Sabitlemeyi kaldır", + "update": "Güncelleme", + "update-to-listen-audio": "Sesli mesajı dinlemek için en son sürüme güncelleyin!", + "update-to-see-image": "Burada güzel bir görüntü görmek için en son sürüme güncelleyin!", + "update-to-see-sticker": "Burada güzel bir etiket görmek için en son sürüme güncelleyin!", + "updates-to-tos": "Hizmet Şartları Güncellemeleri", + "updates-to-tos-desc": "Devam etmeden önce lütfen Hizmet Şartlarını gözden geçirin ve uygulamayı nasıl kullandığınızla ilgili tüm sorumluluğu üstlendiğinizi onaylayın.", + "url": "URL", + "usd-currency": "Usd", + "use-as-profile-picture": "Profil resmi olarak kullan", + "use-valid-contact-code": "Lütfen geçerli bir sohbet anahtarı veya kullanıcı adı girin veya tarayın", + "validation-amount-invalid-number": "Tutar geçerli bir sayı değil", + "validation-amount-is-too-precise": "Tutar çok hassas. Maksimum ondalık sayı {{decimals}}", + "verified-community": "✓ Doğrulanmış topluluk", + "version": "Uygulama Versiyonu", + "view": "Görünüm", + "view-cryptokitties": "CryptoKitties'te görüntüle", + "view-cryptostrikers": "CryptoStrikers'da görüntüle", + "view-data": "Veriyi gör", + "view-details": "Detayları göster", + "view-etheremon": "Etheremon'da görüntüle", + "view-gitcoin": "Gitcoin'de görüntüle", + "view-on-opensea": "Opensea'de görüntüle", + "view-profile": "Profili Görüntüle", + "view-public-dashboard": "Herkese açık paneli görüntüle.", + "view-rules": "Kuralları görüntüle", + "view-signing": "Kelime imzanızı görüntüle", + "view-superrare": "SuperRare'de görüntüle", + "waiting-for-wifi": "Wi-fi kapalı, mesaj senkronizasyonu devre dışı.", + "waiting-for-wifi-change": "Ayarlar", + "waiting-to-sign": "İşlem imzalanmayı bekliyor ...", + "waiting-wi-fi": "Kablosuz bağlantı bekleniyor…", + "waku-bloom-filter-mode": "Waku Bloom Filtre Modu", + "wakuv2-change-nodes": "Waku v2 nodelerini değiştirmek istediğinden emin misiniz? ", + "wakuv2-node-format": "\/ip4\/ {node-ip} \/tcp\/ {port} \/p2p\/ {id}", + "wakuv2-settings": "Waku v2 ayarları", + "wallet": "Cüzdan", + "wallet-address": "Cüzdan adresi", + "wallet-asset": "Varlık", + "wallet-assets": "Varlıklar", + "wallet-backup-recovery-title": "İmza kelimelerinizi yedekleyin", + "wallet-choose-recipient": "Alıcı'yı Seçin", + "wallet-collectibles": "Koleksiyon", + "wallet-connect": "Cüzdan Bağla", + "wallet-insufficient-funds": "Yetersiz bakiye", + "wallet-insufficient-gas": "Gas için yeterli ETH yok", + "wallet-invalid-address": "Geçersiz adres: \n {{data}}", + "wallet-invalid-address-checksum": "Adres hatası: \n {{data}}", + "wallet-invalid-chain-id": "Ağ eşleşmiyor: \n {{data}} ancak mevcut zincir {{chain}}", + "wallet-key-content": "Ethereum standardını temel alan ve 0x ile başlayan 64 karakterlik onaltılık adres. Herkese açık olarak, para almak istediğinizde hesap adresiniz başkalarıyla paylaşılır. \"Ethereum adresi\" veya \"cüzdan adresi\" olarak da bilinir.", + "wallet-key-title": "Hesap adresi", + "wallet-manage-accounts": "Hesapları yönet", + "wallet-manage-assets": "Varlıkları yönetin", + "wallet-request": "İstek", + "wallet-send": "Gönder", + "wallet-send-min-units": "Min. 21000 adet", + "wallet-send-min-wei": "En az 1 wei", + "wallet-settings": "Cüzdan ayarları", + "wallet-total-value": "Toplam değer", + "wallet-transaction-total-fee": "Toplam ücret", + "wants-to-access-profile": "profilinize erişmek istiyor", + "warning": "Uyarı", + "warning-message": "Maalesef, spam'ı önlemek için hızlı bir şekilde birkaç mesaj göndermeyi sınırlandırıyoruz. Lütfen bir süre sonra tekrar deneyin", + "warning-sending-to-contract-descr": "Girdiğiniz adres akıllı bir sözleşmedir, bu adrese para göndermek para kaybına neden olabilir. Bir DApp ile etkileşim kurmak için, Status DApp Tarayıcısında DApp'ı açın.", + "watch-only": "Yalnızca İzle", + "wc-brand-guide": "Ticari markalar ve logolar gibi markalama kullanımına ilişkin rehberlik", + "wc-disclaimer": "Yasal uyarılar (üçüncü taraf sağlayıcılar dahil), garantiler ve yasal yayınlar", + "wc-dispute": "Uyuşmazlık çözümü hükümleri", + "wc-how-to-use-status-app": "Gizlilik ve güvenlik dahil Status uygulaması nasıl kullanılır?", + "wc-new-tos-based-on-principles-prefix": "Yeni Hizmet Şartlarımız temel alınarak tasarlanmış", + "web-view-error": "Sayfa yüklenemedi", + "websites": "Web siteleri", + "webview-camera-permission-requests": "Web görünümü kamera izin istekleri", + "webview-camera-permission-requests-subtitle": "Etkinleştirildiğinde, web siteleri ve dapp'ler kameranızı kullanmak isteyebilir", + "welcome-blank-community-message": "Topluluklarınız burada görünecek.", + "welcome-blank-message": "Sohbetleriniz burada görünecek. Yeni sohbetler başlatmak için ⊕ düğmesine basın", + "welcome-community-blank-message": "Sohbetleriniz burada görünecek. Yeni sohbetler başlatmak için yukarıdaki 3 noktayı tıklayın ve \"Kanal oluştur\" u seçin", + "welcome-community-blank-message-edit-chats": "Kanallarınız burada görünecek. Yeni bir kanal oluşturmak için topluluk ekranına geri dönün, ⊕ düğmesine tıklayın ve \"Bir kanal oluştur\"u seçin", + "welcome-screen-text": "Cüzdanınızı ayarlayın, arkadaşlarınızı sohbete davet edin \n ve popüler dappslere göz atın!", + "welcome-to-status": "Status'a Hoş Geldiniz!", + "welcome-to-status-description": "Kripto cüzdanınızı ayarlayın, arkadaşlarınızı sohbet etmeye ve merkezi olmayan uygulamalara göz atmaya davet edin", + "what-changed": "Ne değişti", + "what-is-shared": "Paylaşılanlar", + "whats-on-your-mind": "Aklınızdan ne geçiyor…", + "word-count": "Sözcük sayısı", + "word-n": "Kelime # {{number}}", + "word-n-description": "Kelime imzanızı doğru bir şekilde yedekleyip yedeklemediğinizi kontrol etmek için, yukarıdaki # {{number}} kelimesini girin.", + "words-n": { + "one": "kelime", + "other": "kelimeler" + }, + "write-down-and-store-securely": "Kodları yazın ve güvenli bir şekilde saklayın.", + "wrong-address": "Yanlış adres", + "wrong-card": "Yanlış kart", + "wrong-card-text": "Yüklenen kart seçtiğiniz tuşlara karşılık gelmiyor", + "wrong-contract": "Yanlış sözleşme", + "wrong-keycard-text": "Dokunduğunuz Keycard bu telefonla ilişkili değil", + "wrong-keycard-title": "Dokunduğunuz anlaşılıyor \nBu yanlış kaycard", + "wrong-password": "Yanlış şifre", + "wrong-word": "Yanlış kelime", + "yes": "Evet", + "you": "sen", + "you-already-have-an-asset": "Zaten bir {{value}} varlığınız var", + "you-are-all-set": "Hazırsınız!", + "you-are-all-set-description": "Telefonunuzu kaybederseniz, bakiyenize ve sohbet anahtarınıza kelime imzanızı kullanarak erişebilirsiniz", + "you-can-change-account": "Hesap adını ve rengini istediğiniz şekilde değiştirebilirsiniz", + "you-can-choose-preview-websites": "Aşağıdaki web sitelerinden hangilerinin sohbetlerdeki açıklamalarını ve resimlerininin bağlantılarını önizleyebileceğini seçebilirsiniz", + "you-can-fetch": "Sohbet geçmişini getirebilirsiniz", + "you-dont-have-contacts": "Henüz hiç kişiniz yok.", + "you-dont-have-contacts-invite-friends": "Henüz herhangi bir kişiniz yok.\n Arkadaşlarınızı sohbet etmeye davet edin.", + "you-dont-have-stickers": "Henüz çıkartmanız yok", + "you-will-need-this-code": "Statusu açmak ve işlemleri imzalamak için bu koda ihtiyacınız olacaktır", + "you-will-start-from-scratch": "Sıfırdan yeni bir anahtar seti ile başlayacaksınız.", + "your-card-is-frozen": "Keycardınız dondurulmuş. Kart erişimini sıfırla", + "your-contact-code": "Erişim izni vermek, DApp'e sohbet anahtarınızı alma yetkisi verir", + "your-data-belongs-to-you": "Kelime imzanızı kaybederseniz verilerinizi ve bakiyenizide kaybedersiniz.", + "your-data-belongs-to-you-description": "Erişiminizi kaybederseniz, örneğin telefonunuzu kaybederek, anahtarlarınıza yalnızca kelime imzanızla erişebilirsiniz. Ama senin kelime imzan var. Bir yere yaz. Güvende tut.", + "your-keys": "Senin anahtarların", + "your-price-limit": "Fiyat limitiniz", + "your-recovery-phrase": "Kelime imzanız", + "your-recovery-phrase-description": "Bu senin kelime imzan. Bunu senin cüzdanın olduğunu kanıtlamak için kullanıyorsun. Sadece bir kez görüyorsunuz! Kağıda yazın ve güvenli bir yerde saklayın. Cüzdanınızı kaybederseniz veya uygulamayı yeniden yüklerseniz ihtiyacınız olacak.", + "youre-on-mobile-network": "Mobil ağ üzerindesiniz" +} diff --git a/translations/uk.json b/translations/uk.json new file mode 100644 index 00000000000..c24b7d92465 --- /dev/null +++ b/translations/uk.json @@ -0,0 +1,202 @@ +{ + "validation-amount-invalid-number": "Сума недійсна", + "transaction-details": "Деталі транзакції", + "confirm": "Підтвердити", + "description": "Опис", + "amount": "Сума", + "open": "Відкрити", + "close-app-title": "Попередження!", + "members-active": { + "one": "1 контакт", + "other": "{{count}} контакти", + "zero": "немає контактів" + }, + "chat-name": "Назва чату", + "phew-here-is-your-passphrase": "*Уф*, це було непросто, ось ваша парольна фраза *запишіть її і зберігайте в надійному місці!* Вона буде потрібна вам для відновлення облікового запису.", + "public-group-topic": "Тема", + "chat-settings": "Налаштування чату", + "offline": "Оффлайн", + "invited": "запрошений", + "address": "Адреса", + "new-public-group-chat": "Приєднатися до публічного чату", + "datetime-hour": { + "one": "година", + "other": "години" + }, + "wallet-settings": "Налаштування гаманця", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "close-app-button": "Підтвердити", + "block": "Блок", + "camera-access-error": "Щоб надати необхідний дозвіл для камери, будь ласка, перейдіть в налаштування системи і переконайтеся, що вибрано Status > Камера.", + "wallet-invalid-address": "Недійсна адреса: \n {{data}}", + "remove": "Видалити", + "add-members": "Додати учасників", + "yes": "Так", + "dapps": "ÐApps", + "network-settings": "Налаштування мережі", + "photos-access-error": "Щоб надати необхідний дозвіл для фото, будь ласка, перейдіть в налаштування системи і переконайтеся, що вибрано Status > Фотографії.", + "hash": "Хеш", + "done": "Готово", + "close-chat": "Видалити чат", + "new-group-chat": "Новий груповий чат", + "wallet": "Гаманець", + "wallet-request": "Запросити", + "sign-in": "Увійти", + "datetime-yesterday": "вчора", + "create-new-account": "Створити новий обліковий запис", + "datetime-ago": "назад", + "contacts": "Контакти", + "got-it": "Зрозуміло", + "public-chats": "Публічні чати", + "not-applicable": "Не можна застосувати для непідписаних транзакцій", + "active-online": "Онлайн", + "password": "Пароль", + "send-request": "Надіслати запит", + "paste-json": "Вставити JSON", + "browsing-title": "Переглядати", + "transactions-history-empty": "У вашій історії ще немає транзакцій", + "discover": "Пошук", + "browsing-cancel": "Скасувати", + "intro-status": "Поспілкуйтеся зі мною в чаті, щоб налаштувати свій аккаунт і змінити свої налаштування!", + "name": "Ім'я", + "gas-price": "Ціна Газу", + "validation-amount-is-too-precise": "Занадто багато цифр після десяткової коми. Найменша сума, яку ви можете надіслати це 1 Wei (1x10^-18 ETH)", + "pending": "В очікуванні", + "copy-transaction-hash": "Копіювати хеш транзакції", + "show-qr": "Показати QR код", + "connect": "Підключитися", + "edit": "Редагувати", + "account-generation-message": "Секундочку, мені потрібно виконати шалено складні розрахунки для створення вашого аккаунта!", + "remove-network": "Видалити мережу", + "no-messages": "Немає повідомлень", + "passphrase": "Парольна фраза", + "recipient": "Одержувач", + "members-title": "Учасники", + "new-group": "Нова група", + "phone-e164": "Міжнародний 1", + "connected": "Підключений", + "rpc-url": "RPC URL-адреса", + "settings": "Налаштування", + "specify-rpc-url": "Вкажіть RPC URL-адресу", + "gas-limit": "Ліміт Газу", + "delete": "Видалити", + "chats": "Чати", + "transaction-sent": "Транзакція відправлена", + "transaction": "Транзакція", + "public-group-status": "Публічний", + "image-source-make-photo": "Сфотографувати", + "chat": "Чат", + "start-conversation": "Почати розмову", + "save": "Зберегти", + "all": "Всі", + "confirmations-helper-text": "Будь ласка, зачекайте як мінімум 12-ти підтверджень, щоб переконатися, що транзакція оброблена безпечно", + "sharing-copy-to-clipboard": "Скопіювати", + "sync-in-progress": "Синхронізація...", + "enter-password": "Введіть пароль", + "send-transaction": "Відправити транзакцію", + "confirmations": "Підтвердження", + "incorrect-code": [ + "str", + "Вибачте, код неправильний, введіть ще раз" + ], + "image-source-gallery": "Вибрати з галереї", + "sync-synced": "Синхронізовано", + "currency": "Валюта", + "status-pending": "В очікуванні", + "connecting-requires-login": "Підключення до мережі вимагає входу в систему", + "datetime-day": { + "one": "день", + "other": "дні" + }, + "request-transaction": "Запит транзакції", + "wallet-send": "Відправити", + "scan-qr": "Сканувати QR код", + "contact-s": { + "one": "контакт", + "other": "контакти" + }, + "gas-used": "Використаний Газ", + "type": "Тип", + "next": "Далі", + "recent": "Останні статуси", + "open-on-etherscan": "Відкрити посилання на Etherscan.io", + "offline-messaging-settings": "Параметри оффлайн повідомленнь", + "share": "Поділитися", + "status": "Status", + "from": "Від", + "wrong-password": "Невірний пароль", + "in-contacts": "В контактах", + "transactions-sign": "Підписати", + "sharing-share": "Поділитися...", + "type-a-message": "Написати повідомлення...", + "usd-currency": "USD", + "url": "URL-адреса", + "add-network": "Додати мережу", + "unknown-status-go-error": "Невідома status-go помилка", + "clear-history": "Видалити історію", + "wallet-manage-assets": "Управління Активами", + "no-contacts": "Поки що немає контактів", + "soon": "Незабаром", + "close-app-content": "Додаток зупиниться і закриється. При повторному відкритті, буде використовуватися обрана мережа", + "datetime-today": "сьогодні", + "web-view-error": "ой, помилка", + "error": "Помилка", + "more": "більше", + "cancel": "Скасувати", + "can-not-add-yourself": "Ви не можете додати себе", + "transaction-description": "Будь ласка, почекайте як мінімум 12 підтверджень, щоб переконатися, що транзакція оброблена безпечно", + "add-to-contacts": "Додати до контактів", + "available": "Доступно", + "You": "Ви", + "main-wallet": "Основний Гаманець", + "transactions": "Транзакції", + "members": { + "one": "1 контакт", + "other": "{{count}} контакти", + "zero": "немає контактів" + }, + "intro-message1": "Ласкаво просимо в Status\nнатисніть на це повідомлення, щоб встановити пароль і почати!", + "eth": "ETH", + "transactions-history": "Історія", + "new-contact": "Новий контакт", + "datetime-second": { + "one": "секунда", + "other": "секунди" + }, + "recover": "Відновити", + "wallet-total-value": "Загальна вартість", + "nonce": "Nonce", + "new-network": "Нова мережа", + "datetime-minute": { + "one": "хвилина", + "other": "хвилин" + }, + "browsing-open-in-android-web-browser": "Відкрити у веб-браузері", + "browsing-open-in-ios-web-browser": "Відкрити у веб-браузері", + "wallet-insufficient-funds": "Недостатньо коштів", + "edit-profile": "Редагувати профіль", + "active-unknown": "Невідомо", + "wallet-invalid-chain-id": "Мережа не відповідає: \n {{data}}", + "transaction-failed": "Не вдалося виконати транзакцію", + "public-key": "Публічний ключ", + "incoming": "Вхідні", + "profile": "Профіль", + "wallet-choose-recipient": "Виберіть Одержувача", + "add-mailserver": "Додати поштовий сервер", + "none": "Жоден", + "removed": "видалений", + "no": "Ні", + "transactions-filter-select-all": "Вибрати все", + "transactions-filter-title": "Фільтри історії", + "message": "Повідомлення", + "here-is-your-passphrase": "Ось ваша парольна фраза *запишіть її і зберігайте в надійному місці!* Вона буде потрібна вам для відновлення облікового запису.", + "wallet-assets": "Активи", + "image-source-title": "Зображення профілю", + "current-network": "Поточна мережа", + "outgoing": "Вихідні", + "left": "вийшов", + "to": "Кому", + "data": "Дані", + "status-sent": "Відправлено", + "cost-fee": "Вартість/Комісія" +} diff --git a/translations/ur.json b/translations/ur.json new file mode 100644 index 00000000000..aa6fb9da9b3 --- /dev/null +++ b/translations/ur.json @@ -0,0 +1,127 @@ +{ + "confirm": "توثیق کریں", + "amount": "رقم", + "members-active": { + "one": "1 ممبر, 1 فعال", + "other": "{{count}} ممبران, {count}} فعال", + "zero": "کوئی ممبر نہیں" + }, + "chat-name": "چیٹ کا نام", + "phew-here-is-your-passphrase": "*اف* یہ مشکل تھا, یہ آپ کی passphrase ہے۔, *اسے لکھ لیں اور اپنے پاس محفوظ کر لیں۔* آپ کو اپنا اکاؤنٹ بحال کرنے کے لیے اس کی ضرورت ہو گی۔", + "public-group-topic": "موضوع", + "chat-settings": "چیٹ کی سیٹنگز", + "offline": "آف لائن", + "invited": "مدعو", + "address": "پتہ", + "new-public-group-chat": "عوامی چیٹ میں شامل ہوں", + "datetime-hour": { + "one": "گھنٹہ", + "other": "گھنٹے" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "remove": "نکالیں", + "add-members": "ممبران شامل کریں", + "done": "ہو گیا", + "close-chat": "چیٹ کو حذف کریں", + "new-group-chat": "نئی گروپ چیٹ", + "sign-in": "سائن اِن کریں", + "datetime-yesterday": "کل", + "create-new-account": "نیا اکاؤنٹ تخلیق کریں", + "datetime-ago": "قبل", + "contacts": "رابطے", + "got-it": "سمجھ گیا", + "active-online": "آن لائن", + "password": "پاسورڈ", + "browsing-title": "براؤز", + "discover": "دریافت", + "browsing-cancel": "منسوخ کریں", + "intro-status": "اپنا اکاؤنٹ سیٹ اپ کرنے اور سیٹنگز تبدیل کرنے کے لیے مجھ سے چیٹ کریں!", + "name": "نام", + "show-qr": "دکھائیں QR", + "connect": "ملیں", + "edit": "تصحیح", + "account-generation-message": "مجھے ایک سیکنڈ دیں، اپ کا اکاؤنٹ تشکیل دینے کے لیے مجھے کچھ ریاضی سے کام کرنا ہے۔!", + "no-messages": "کوئی پیغام نہیں", + "passphrase": "Passphrase", + "recipient": "وصول کنندہ", + "members-title": "ممبران", + "new-group": "نیا گروپ", + "phone-e164": "بین الاقوامی 1", + "settings": "سیٹنگز", + "chats": "چیٹ", + "transaction": "لین دین", + "public-group-status": "عوامی", + "image-source-make-photo": "کھینچیں", + "start-conversation": "بات چیت شروع کریں", + "save": "محفوظ کریں", + "sharing-copy-to-clipboard": "کلپ بورڈ پر نقل کیا گیا", + "sync-in-progress": "سنکرونائز ہو رہا ہے", + "send-transaction": "لین دین بھیجیں", + "incorrect-code": [ + "str", + "معذرت، کوڈ غلط تھا، برائے مہربانی دوبارہ درج کریں" + ], + "image-source-gallery": "گیلری سے چنیں", + "sync-synced": "دوران سنکرونائز", + "status-pending": "زیر غور", + "datetime-day": { + "one": "دن", + "other": "دن" + }, + "scan-qr": "سکین کریں QR", + "contact-s": { + "one": "رابطہ", + "other": "روابط" + }, + "next": "اگلا", + "recent": "حالیہ", + "status": "سٹیٹس", + "from": "سے", + "wrong-password": "غلط پاسورڈ", + "in-contacts": "روابط میں", + "sharing-share": "اشتراک کریں.....", + "type-a-message": "کوئی پیغام ٹائپ کریں....", + "clear-history": "تاریخ مٹا دیں", + "no-contacts": "ابھی یہاں کوئی کنٹیکٹ نہیں", + "datetime-today": "آج", + "web-view-error": "اوہ، غلطی ہو گئی", + "error": "خرابی", + "more": "مزید", + "cancel": "منسوخ کریں", + "can-not-add-yourself": "آپ خود کو شامل نہیں کر سکتے", + "add-to-contacts": "اپنے رابطوں میں درج کریں", + "available": "دستیاب", + "You": "آپ", + "main-wallet": "مرکزی والٹ", + "members": { + "one": "1 ممبر", + "other": "{{count}} ممبران", + "zero": "کوئی ممبر نہیں" + }, + "intro-message1": "سٹیٹس میں خوش آمدید اور اپنا پاسورڈ سیٹ کرنے کے لیے اس کو ٹیپ کریں اور شروعات کریں۔", + "new-contact": "نئے رابطے", + "datetime-second": { + "one": "سیکنڈ", + "other": "سیکنڈ" + }, + "recover": "بحال کریں", + "datetime-minute": { + "one": "منٹ", + "other": "منٹ" + }, + "browsing-open-in-android-web-browser": "ویب براؤزر میں کھولیں", + "browsing-open-in-ios-web-browser": "ویب براؤزر میں کھولیں", + "edit-profile": "پروفائل میں ترمیم کریں", + "active-unknown": "نامعلوم", + "public-key": "Public Key", + "profile": "پروفائل", + "none": "کوئی نہیں", + "removed": "نکال دیا گیا", + "message": "پیغام", + "here-is-your-passphrase": "یہ آپ کی passphrase ہے۔, *ا اکاؤنٹ بحال کرنے کے لیے اس کی ضرورت * ہے۔, *اسے لکھ لیں اور اپنے پاس محفوظ کر لیں۔* آپ کو ا", + "image-source-title": "پروفائل تصویر", + "left": "چھوڑ دیا", + "to": "بجانب", + "status-sent": "بھیج دیا گیا", + "data": "ڈیٹا" +} diff --git a/translations/vi.json b/translations/vi.json new file mode 100644 index 00000000000..4fe537a56bc --- /dev/null +++ b/translations/vi.json @@ -0,0 +1,1327 @@ +{ + "You": "Bạn", + "about-app": "Trong khoảng", + "about-key-storage-content": "Trạng thái sẽ không bao giờ truy cập khóa cá nhân của bạn. Đảm bảo sao lưu cụm từ hạt giống của bạn. Nếu bạn bị mất điện thoại, đó là cách duy nhất để truy cập vào chìa khóa của bạn.", + "about-key-storage-title": "Giới thiệu về bộ nhớ khóa", + "about-names-content": "Không ai có thể giả vờ là bạn! Bạn ẩn danh theo mặc định và không bao giờ phải tiết lộ tên thật của mình. Bạn có thể đăng ký một tên tùy chỉnh với một khoản phí nhỏ.", + "about-names-title": "Không thể thay đổi tên", + "accept": "Chấp nhận", + "accept-and-share-address": "Chấp nhận và chia sẻ địa chỉ", + "access-existing-keys": "Truy cập các khóa hiện có", + "access-key": "Khóa truy cập", + "account-added": "Đã thêm tài khoản", + "account-color": "Màu tài khoản", + "account-content": "Bạn có thể so sánh tài khoản ở Trạng thái với tài khoản ngân hàng. Giống như tài khoản ngân hàng, tài khoản thường có địa chỉ và số dư; Bạn sử dụng tài khoản này để giao dịch trên Ethereum. Bạn có thể có nhiều tài khoản trong ví của mình. Tất cả được truy cập bằng trạng thái mở khóa.", + "account-exists-title": "Tài khoản đã tồn tại", + "account-name": "Tên tài khoản", + "account-settings": "Cài đặt tài khoản", + "account-title": "Tài khoản", + "accounts": "Tài khoản", + "active-online": "Trực tuyến", + "active-unknown": "không xác định", + "add": "Thêm vào", + "add-a-watch-account": "Thêm địa chỉ chỉ xem", + "add-account": "Thêm một tài khoản", + "add-account-description": "Bạn có thể nhập bất kỳ loại tài khoản Ethereum nào để thêm vào ví Trạng thái của mình", + "add-account-incorrect-password": "Mật khẩu dường như không chính xác. Nhập mật khẩu bạn sử dụng để mở khóa ứng dụng.", + "add-an-account": "Thêm một tài khoản", + "add-bootnode": "Thêm bootnode", + "add-contact": "Thêm liên hệ", + "add-custom-token": "Thêm mã thông báo tùy chỉnh", + "add-mailserver": "Thêm nút lịch sử", + "add-members": "Thêm thành viên", + "add-network": "Thêm mạng", + "add-new-contact": "Thêm liên hệ mới", + "add-nickname": "Thêm biệt hiệu (tùy chọn)", + "add-private-key-account": "Thêm tài khoản từ khóa cá nhân", + "add-seed-account": "Thêm tài khoản bằng cụm từ hạt giống", + "add-to-contacts": "Thêm vào liên lạc", + "add-to-contacts-text": "Bằng cách thêm một người dùng vào danh sách liên hệ của bạn, bạn chia sẻ địa chỉ ví của mình", + "add-to-favourites": "Thêm vào mục ưa thích", + "add-watch-account": "Thêm tài khoản chỉ xem", + "address": "Địa chỉ", + "address-or-ens-name": "Địa chỉ hoặc tên ENS", + "address-received": "Địa chỉ đã nhận", + "address-request-accepted": "Yêu cầu địa chỉ được chấp nhận", + "address-requested": "Địa chỉ được yêu cầu", + "advanced": "Nâng cao", + "advanced-settings": "Cài đặt nâng cao", + "advertiser-description": "Bạn đã khám phá ra Trạng thái nhờ một đối tác. Bạn có phiền nếu Trạng thái kiểm tra địa chỉ IP của bạn một lần để họ nhận phần thưởng không? Thông tin này sẽ không được sử dụng cho bất cứ việc gì khác và nó sẽ bị xóa hoàn toàn sau 7 ngày.", + "advertiser-starter-pack-accept": "Chấp nhận", + "advertiser-starter-pack-decline": "Từ chối", + "advertiser-starter-pack-description": "Đây là một số tiền điện tử để bạn bắt đầu! Sử dụng nó để nhận nhãn dán, tên ENS và thử dapp", + "advertiser-starter-pack-title": "Gói khởi động", + "advertiser-title": "Quyền riêng tư theo mặc định", + "agree-by-continuing": "Bằng cách tiếp tục, bạn đồng ý\n cho chúng tôi", + "all": "Tất cả", + "allow": "Cho phép", + "allow-mention-notifications": "Hiển thị @ đề cập", + "allowing-authorizes-this-dapp": "Việc cho phép cho phép DApp này truy xuất địa chỉ ví của bạn và kích hoạt Web3", + "already-have-asset": "Bạn đã có tài sản này", + "amount": "Số tiền", + "appearance": "Xuất hiện", + "apply": "Ứng dụng", + "are-you-sure": "Bạn có chắc không?", + "are-you-sure-description": "Bạn sẽ không thể xem lại toàn bộ cụm từ hạt giống", + "are-you-sure-to-cancel": "Bạn có chắc là muốn hủy bỏ?", + "are-you-sure?": "Bạn có chắc không?", + "ask-in-status": "Đặt câu hỏi hoặc báo cáo lỗi", + "at": "tại", + "attribution-received": "{{attrib}} trong số {{max}} nhận được", + "audio": "Âm thanh", + "audio-recorder": "Máy ghi âm", + "audio-recorder-error": "Lỗi máy ghi âm", + "audio-recorder-max-ms-reached": "Đã đạt được thời gian ghi tối đa", + "audio-recorder-permissions-error": "Bạn phải cấp quyền để gửi tin nhắn âm thanh", + "authorize": "Ủy quyền", + "available": "Có sẵn", + "available-participants": { + "other": "Bạn có thể chọn thêm {{count}} người tham gia" + }, + "back": "Trở lại", + "back-up-seed-phrase": "Sao lưu cụm từ hạt giống", + "back-up-your-seed-phrase": "Sao lưu cụm từ hạt giống của bạn", + "backup-disabled": "Tàn tật", + "backup-recovery-phrase": "Sao lưu cụm từ hạt giống", + "balance": "Thăng bằng", + "begin-set-up": "Bắt đầu thiết lập", + "biometric-auth-android-sensor-desc": "Cảm biến chạm", + "biometric-auth-android-sensor-error-desc": "Thất bại", + "biometric-auth-android-title": "Yêu cầu xác thực", + "biometric-auth-confirm-logout": "Đăng nhập lại", + "biometric-auth-confirm-message": "Xác thực sinh trắc học là bắt buộc để tiếp tục, nếu không thể, vui lòng mở khóa bằng mật khẩu hoặc mật mã của bạn", + "biometric-auth-confirm-title": "Bạn phải xác thực!", + "biometric-auth-confirm-try-again": "Thử lại", + "biometric-auth-error": "Không thể thực hiện xác thực sinh trắc học ( {{code}} )", + "biometric-auth-login-error-title": "Lỗi xác thực sinh trắc học", + "biometric-auth-login-ios-fallback-label": "Nhập mật khẩu", + "biometric-auth-reason-login": "Đăng nhập trong trạng thái", + "biometric-auth-reason-verify": "Xác minh xác thực", + "biometric-disable-bioauth": "tắt {{bio-type-label}}", + "biometric-disable-password-description": "Nếu bạn tắt tính năng này, bạn cũng sẽ", + "biometric-disable-password-title": "Tắt tính năng lưu mật khẩu", + "biometric-enable": "Nếu bạn không muốn nhập mật khẩu của mình mỗi lần để truy cập ứng dụng, hãy bật đăng nhập {{bio-type-label}}", + "biometric-enable-button": "Bật {{bio-type-label}}", + "biometric-enable-keycard": "Nếu bạn không muốn sử dụng Thẻ khóa của mình mỗi lần để truy cập ứng dụng, hãy bật đăng nhập {{bio-type-label}}", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "Vân tay", + "biometric-secure-with": "Bảo mật với {{bio-type-label}}", + "biometric-touchid": "ID cảm ứng", + "blank-keycard-text": "Bạn có thể tiếp tục với thẻ khóa của mình sau khi bạn đã tạo khóa và tên của mình", + "blank-keycard-title": "Có vẻ như bạn đã nhấn vào\n một thẻ khóa trống", + "block": "Khối", + "block-contact": "Chặn người dùng này", + "block-contact-details": "Việc chặn sẽ xóa các tin nhắn trước đây của người dùng này và ngăn những tin nhắn mới tiếp cận bạn", + "blocked-users": "Người dùng bị chặn", + "bootnode-address": "Địa chỉ Bootnode", + "bootnode-details": "Chi tiết Bootnode", + "bootnode-format": "enode: \/\/ {enode-id} @ {ip-address} : {port}", + "bootnodes": "Bootnodes", + "bootnodes-enabled": "Bootnodes đã được kích hoạt", + "bootnodes-settings": "Cài đặt Bootnodes", + "browsed-websites": "Lịch sử trình duyệt sẽ xuất hiện ở đây", + "browser": "Trình duyệt", + "browser-not-secure": "Kết nối không an toàn! Không ký kết các giao dịch hoặc gửi dữ liệu cá nhân trên trang web này.", + "browser-secure": "Kết nối an toàn. Hãy chắc chắn rằng bạn thực sự tin tưởng trang web này trước khi ký kết các giao dịch hoặc nhập dữ liệu cá nhân.", + "browsers": "Các trình duyệt", + "browsing-cancel": "Huỷ bỏ", + "browsing-open-in-android-web-browser": "Mở trong Android", + "browsing-open-in-ios-web-browser": "Mở trong iOS", + "browsing-open-in-status": "Mở ở trạng thái", + "browsing-site-blocked-description1": "Chúng tôi đã phát hiện hoạt động độc hại tiềm ẩn từ địa chỉ này. Để bảo vệ bạn và ví của bạn, chúng tôi đang ngăn điều hướng thêm. \n\n Nếu bạn cho rằng đây là lỗi, hãy cho chúng tôi biết trong", + "browsing-site-blocked-description2": "trò chuyện công khai.", + "browsing-site-blocked-go-back": "Quay lại", + "browsing-site-blocked-title": "Trang web này bị chặn", + "browsing-title": "Duyệt qua", + "bug-report": "Báo cáo lỗi", + "camera-access-error": "Để cấp quyền đối với máy ảnh cần thiết, vui lòng chuyển đến cài đặt hệ thống của bạn và đảm bảo rằng Trạng thái > Máy ảnh được chọn.", + "can-not-add-yourself": "Đó là bạn, để bắt đầu một cuộc trò chuyện, hãy chọn một người khác", + "cancel": "Huỷ bỏ", + "cancel-keycard-setup": "Hủy thiết lập Thẻ khóa", + "cancelling": "Đang hủy", + "cannot-read-card": "Không thể đọc thẻ.\n Vui lòng giữ nó vào mặt sau điện thoại của bạn", + "cannot-use-default-pin": "Mật mã 000000 không được phép.\n Vui lòng sử dụng số khác", + "cant-open-public-chat": "Không thể mở cuộc trò chuyện công khai", + "cant-report-bug": "Không thể báo cáo lỗi", + "card-is-blank": "Thẻ này trống", + "card-reseted": "Thẻ đã được đặt lại", + "card-unpaired": "Thẻ đã được hủy ghép nối khỏi thiết bị hiện tại", + "change-fleet": "Thay đổi hạm đội thành {{fleet}}", + "change-logging-enabled": "Bạn có chắc chắn muốn {{enable}} ghi nhật ký không?", + "change-passcode": "Đổi mật khẩu", + "change-password": "Đổi mật khẩu", + "change-pin": "Thay đổi mật mã 6 chữ số", + "changed-amount-warning": "Số tiền đã được thay đổi từ {{old}} thành {{new}}", + "changed-asset-warning": "Nội dung đã được thay đổi từ {{old}} thành {{new}}", + "chaos-mode": "Chaos Mode", + "chaos-unicorn-day": "Ngày kỳ lân hỗn loạn", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "Trò chuyện", + "chat-and-transact": "Trò chuyện và giao dịch riêng tư với bạn bè", + "chat-is-a-contact": "Tiếp xúc", + "chat-is-not-a-contact": "Không phải là liên hệ", + "chat-key": "Phím trò chuyện", + "chat-key-content": "Tin nhắn trên giao thức Trò chuyện trạng thái được gửi và nhận bằng các khóa mã hóa. Khóa trò chuyện công khai là một chuỗi ký tự bạn chia sẻ với người khác để họ có thể gửi tin nhắn cho bạn trong Trạng thái.", + "chat-key-title": "Khóa trò chuyện", + "chat-link-previews": "Xem trước liên kết trò chuyện", + "chat-name": "Tên trò chuyện", + "chat-name-content": "Ba từ ngẫu nhiên, được lấy theo thuật toán từ khóa trò chuyện của bạn và được sử dụng làm bí danh mặc định của bạn trong trò chuyện. Tên trò chuyện là hoàn toàn duy nhất; không người dùng nào khác có thể có ba từ giống nhau.", + "chat-name-title": "Tên trò chuyện", + "chat-settings": "Cài đặt trò chuyện", + "chats": "Trò chuyện", + "check-on-opensea": "Kiểm tra opensea", + "check-your-recovery-phrase": "Kiểm tra cụm từ hạt giống của bạn", + "choose-authentication-method": "Chọn một phương thức xác thực", + "clear": "Thông thoáng", + "clear-all": "Quet sạch tât cả", + "clear-history": "Xóa lịch sử", + "clear-history-action": "Thông thoáng", + "clear-history-confirmation": "Xóa lịch sử?", + "clear-history-confirmation-content": "Bạn có chắc chắn muốn xóa lịch sử trò chuyện này không?", + "clear-history-title": "Xóa lịch sử?", + "close": "Đóng", + "close-app-button": "Xác nhận", + "close-app-content": "Ứng dụng sẽ dừng và đóng. Khi bạn mở lại, mạng đã chọn sẽ được sử dụng", + "close-app-title": "Cảnh báo!", + "command-button-send": "Gửi", + "community-image-remove": "Tẩy", + "community-invite-title": "Mời gọi", + "community-members-title": "Các thành viên", + "community-share-title": "Chia sẻ", + "complete-hardwallet-setup": "Thẻ này hiện đã được liên kết. Bạn cần nó để ký giao dịch và mở khóa", + "completed": "Đã hoàn thành", + "confirm": "Xác nhận", + "confirm-password-placeholder": "Xác nhận mật khẩu của bạn...", + "confirmation-request": "Đề nghị đơn xác nhận", + "confirmations": "Xác nhận", + "confirmations-helper-text": "Khi giao dịch có 12 xác nhận, bạn có thể coi là nó đã được giải quyết.", + "connect": "Kết nối", + "connect-mailserver-content": "Kết nối với {{name}} ?", + "connected": "Đã kết nối", + "connecting": "Đang kết nối...", + "connecting-requires-login": "Kết nối với mạng khác yêu cầu đăng nhập", + "connection-with-the-card-lost": "Kết nối với thẻ\n đã bị mất", + "connection-with-the-card-lost-setup-text": "Để tiếp tục thiết lập, hãy giữ thẻ để\n mặt sau của điện thoại của bạn và duy trì\n thẻ liên hệ điện thoại", + "connection-with-the-card-lost-text": "Để tiếp tục giữ thẻ vào mặt sau điện thoại của bạn", + "contact-code": "Phím trò chuyện", + "contact-s": { + "other": "liên lạc" + }, + "contacts": "Liên lạc", + "contacts-descr": "Danh bạ của bạn sẽ xuất hiện ở đây. Bạn sẽ nhận được cập nhật trạng thái từ bất kỳ ai mà bạn thêm làm địa chỉ liên hệ", + "contacts-empty": "Các liên hệ có tên ENS sẽ xuất hiện ở đây", + "continue": "Tiếp tục", + "continue-anyway": "Vẫn tiếp tục", + "contract-address": "Địa chỉ hợp đồng", + "contract-interaction": "Hợp đồng tương tác", + "copy-info": "Sao chép thông tin", + "copy-qr": "Sao chép mã", + "copy-to-clipboard": "Sao chép", + "copy-transaction-hash": "Sao chép ID giao dịch", + "cost-fee": "Chi phí \/ Phí", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "Tạo nên", + "create-a-pin": "Tạo mật mã gồm 6 chữ số", + "create-group-chat": "Tạo cuộc trò chuyện nhóm", + "create-multiaccount": "Tạo khóa", + "create-new-key": "Nhận chìa khóa mới", + "create-pin": "Tạo mật mã 6 chữ số", + "create-pin-description": "Bạn sẽ cần thẻ của mình + mật mã gồm 6 chữ số này để mở khóa Trạng thái và xác nhận giao dịch", + "created-group-chat-description": "Bạn đã tạo nhóm {{group-name}}", + "cryptokitty-name": "CryptoKitty # {{id}}", + "currency": "Tiền tệ", + "currency-display-name-aed": "Emirati Dirham", + "currency-display-name-afn": "Afghanistan Người Afghanistan", + "currency-display-name-ars": "Đồng peso của Argentina", + "currency-display-name-aud": "Đô la Úc", + "currency-display-name-bbd": "Đô la Barbados", + "currency-display-name-bdt": "Taka Bangladesh", + "currency-display-name-bgn": "Lev Bungari", + "currency-display-name-bhd": "Dinar Bahrain", + "currency-display-name-bnd": "Đô la Brunei Darussalam", + "currency-display-name-bob": "Bolivia Bolíviano", + "currency-display-name-brl": "Brazil Real", + "currency-display-name-btn": "Ngultrum của người Bhutan", + "currency-display-name-cad": "Đô la Canada", + "currency-display-name-chf": "Franc Thụy Sĩ", + "currency-display-name-clp": "Peso Chile", + "currency-display-name-cny": "Nhân dân tệ Trung Quốc", + "currency-display-name-cop": "Colombia Peso", + "currency-display-name-crc": "Costa Rica Colon", + "currency-display-name-czk": "Koruna Séc", + "currency-display-name-dkk": "Đan Mạch Krone", + "currency-display-name-dop": "Đồng Peso của Cộng hòa Dominica", + "currency-display-name-egp": "Bảng Ai Cập", + "currency-display-name-etb": "Ethiopia Birr", + "currency-display-name-eur": "Euro", + "currency-display-name-gbp": "Đồng bảng anh", + "currency-display-name-gel": "Gruzia Lari", + "currency-display-name-ghs": "Ghana Cedi", + "currency-display-name-hkd": "Đôla Hong Kong", + "currency-display-name-hrk": "Croatia Kuna", + "currency-display-name-huf": "Hungary Forint", + "currency-display-name-idr": "Đồng Rupiah Indonesia", + "currency-display-name-ils": "Shekel Israel", + "currency-display-name-inr": "Rupee Ấn Độ", + "currency-display-name-isk": "Iceland Krona", + "currency-display-name-jmd": "Đô la Jamaica", + "currency-display-name-jpy": "Yen Nhật", + "currency-display-name-kes": "Shilling Kenya", + "currency-display-name-krw": "Hàn Quốc (Hàn Quốc) Won", + "currency-display-name-kwd": "Đồng Dinar của Kuwait", + "currency-display-name-kzt": "Kazakhstan Tenge", + "currency-display-name-lkr": "Rupee Sri Lanka", + "currency-display-name-mad": "Đồng Dirham của Maroc", + "currency-display-name-mdl": "Moldova Leu", + "currency-display-name-mur": "Mauritius Rupee", + "currency-display-name-mwk": "Malawian Kwacha", + "currency-display-name-mxn": "Peso Mexico", + "currency-display-name-myr": "Ringgit Malaysia", + "currency-display-name-mzn": "Mô-dăm-bích Metical", + "currency-display-name-nad": "Đô la Namibia", + "currency-display-name-ngn": "Nigeria Naira", + "currency-display-name-nok": "Na Uy Krone", + "currency-display-name-npr": "Rupee Nepal", + "currency-display-name-nzd": "Đô la New Zealand", + "currency-display-name-omr": "Oman Rial", + "currency-display-name-pen": "Peru Sol", + "currency-display-name-pgk": "Papua New Guinean Kina", + "currency-display-name-php": "Peso Philippines", + "currency-display-name-pkr": "Đồng Rupee Pakistan", + "currency-display-name-pln": "Zloty Ba Lan", + "currency-display-name-pyg": "Guarani của Paraguay", + "currency-display-name-qar": "Riyal Qatar", + "currency-display-name-ron": "Romania Leu", + "currency-display-name-rsd": "Đồng Dinar của Serbia", + "currency-display-name-rub": "Đồng rúp Nga", + "currency-display-name-sar": "Riyal Ả Rập Xê Út", + "currency-display-name-sek": "Krona Thụy Điển", + "currency-display-name-sgd": "Đô la Singapore", + "currency-display-name-thb": "Bạt Thái Lan", + "currency-display-name-try": "Lira Thổ Nhĩ Kỳ", + "currency-display-name-ttd": "Đô la Trinidad và Tobago", + "currency-display-name-twd": "Đô la mới của Đài Loan", + "currency-display-name-tzs": "Đồng Shilling của Tanzania", + "currency-display-name-uah": "Ukraina Hryvnia", + "currency-display-name-ugx": "Đồng Shilling của Ugandan", + "currency-display-name-usd": "Đô la Mĩ", + "currency-display-name-uyu": "Peso Uruguay", + "currency-display-name-vef": "Bolívar Venezuela", + "currency-display-name-vnd": "Việt Nam đồng", + "currency-display-name-zar": "Rand Nam Phi", + "current-network": "Mạng hiện hành", + "current-pin": "Nhập mật mã gồm 6 chữ số", + "current-pin-description": "Nhập mật mã gồm 6 chữ số của bạn để tiếp tục", + "custom": "Tập quán", + "custom-networks": "Mạng tùy chỉnh", + "custom-seed-phrase": "Cụm từ hạt giống không hợp lệ", + "custom-seed-phrase-text-1": "Cụm từ hạt giống này không khớp với từ điển được hỗ trợ của chúng tôi. Kiểm tra các từ sai chính tả.", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "Chấp nhận và mở", + "dapp-starter-pack-description": "Đây là một số tiền điện tử để bạn bắt đầu! Sử dụng nó để nhận nhãn dán, tên ENS và thử dapp", + "dapp-starter-pack-title": "Gói khởi động", + "dapp-would-like-to-connect-wallet": "muốn kết nối với", + "dapps": "ÐApps", + "dapps-permissions": "Quyền DApp", + "dark": "Tối", + "data": "Dữ liệu", + "data-syncing": "Đồng bộ hóa dữ liệu", + "datetime-ago": "trước đây", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "other": "ngày" + }, + "datetime-day-short": { + "other": "D" + }, + "datetime-hour": { + "other": "giờ" + }, + "datetime-hour-short": { + "other": "H" + }, + "datetime-minute": { + "other": "phút" + }, + "datetime-minute-short": { + "other": "M" + }, + "datetime-second": { + "other": "giây" + }, + "datetime-second-short": { + "other": "S" + }, + "datetime-today": "hôm nay", + "datetime-yesterday": "hôm qua", + "decimals": "Số thập phân", + "decline": "Từ chối", + "decryption-failed-content": "Đã xảy ra lỗi khi giải mã dữ liệu của bạn. Bạn có thể cần xóa dữ liệu cũ của mình và tạo tài khoản mới. Nhấn vào “Áp dụng” để xóa hoặc “Hủy” để thử lại", + "default": "Mặc định", + "default-assets": "ERC20 và ERC721 mặc định", + "delete": "Xóa bỏ", + "delete-account": "Xóa tài khoản", + "delete-and-leave-group": "Xóa và rời khỏi nhóm", + "delete-bootnode": "Xóa bootnode", + "delete-bootnode-are-you-sure": "Bạn có chắc chắn muốn xóa bootnode này không?", + "delete-bootnode-title": "Xóa bootnode", + "close-chat": "Xóa trò chuyện", + "close-chat-confirmation": "Bạn có chắc chắn muốn xóa cuộc trò chuyện này không?", + "delete-confirmation": "Xóa bỏ?", + "delete-mailserver": "Xóa nút lịch sử", + "delete-mailserver-are-you-sure": "Bạn có chắc chắn muốn xóa nút lịch sử này không?", + "delete-mailserver-title": "Xóa nút lịch sử", + "delete-message": "Xóa tin nhắn", + "delete-my-account": "Xóa tài khoản của tôi đi", + "delete-my-profile": "Xóa hồ sơ của tôi", + "delete-network-confirmation": "Bạn có chắc chắn muốn xóa mạng này không?", + "delete-network-error": "Vui lòng kết nối với một mạng khác trước khi xóa mạng này", + "delete-network-title": "Xóa mạng?", + "delete-profile": "Xóa hồ sơ", + "delete-profile-warning": "Cảnh báo: Nếu bạn không viết lại cụm từ gốc, bạn sẽ mất quyền truy cập vào tiền của mình sau khi xóa hồ sơ của mình", + "deny": "Phủ nhận", + "derivation-path": "Đường dẫn xuất phát", + "description": "Sự miêu tả", + "dev-mode": "Chế độ phát triển", + "dev-mode-settings": "Cài đặt chế độ phát triển", + "device-syncing": "Đồng bộ hóa thiết bị", + "devices": "Thiết bị", + "disable": "vô hiệu hóa", + "disabled": "Tàn tật", + "disconnected": "Trò chuyện ngoại tuyến", + "discover": "Khám phá", + "dismiss": "Bỏ qua", + "done": "Làm xong", + "dont-ask": "Đừng hỏi tôi nữa", + "edit": "Biên tập", + "edit-group": "Chỉnh sửa nhóm", + "edit-profile": "Chỉnh sửa hồ sơ", + "empty-chat-description": "Không có tin nhắn nào\n trong cuộc trò chuyện này chưa", + "empty-chat-description-one-to-one": "Mọi thư bạn gửi ở đây đều được mã hóa và chỉ bạn mới có thể đọc và", + "empty-chat-description-public": "Ở đây yên tĩnh trong {{quiet-hours}} . Bắt đầu cuộc trò chuyện hoặc", + "empty-chat-description-public-share-this": "chia sẻ cuộc trò chuyện này.", + "empty-pending-invitations-descr": "Những người muốn tham gia nhóm\n thông qua một liên kết mời sẽ xuất hiện ở đây", + "enable": "Kích hoạt", + "enable-all": "Cho phép tất cả", + "enable-link-previews": "Bật xem trước liên kết trong trò chuyện?", + "encrypt-with-password": "Mã hóa bằng mật khẩu", + "ens-10-SNT": "10 SNT", + "ens-add-username": "Thêm tên người dùng", + "ens-agree-to": "Đồng ý với", + "ens-chat-settings": "Cài đặt trò chuyện", + "ens-custom-domain": "Tên miền tùy chọn", + "ens-custom-username-hints": "Nhập toàn bộ tên người dùng bao gồm cả miền tùy chỉnh như username.domain.eth", + "ens-custom-username-taken": "Tên người dùng không thuộc về bạn :(", + "ens-deposit": "Tiền gửi", + "ens-dismiss-message": "Bấm vào đây để loại bỏ", + "ens-displayed-with": "Tin nhắn của bạn được hiển thị cho những người khác với", + "ens-get-name": "Nhận một tên người dùng chung", + "ens-got-it": "OK đã nhận nó", + "ens-locked": "Tên người dùng đã bị khóa. Bạn sẽ không thể phát hành nó cho đến {{date}}", + "ens-name-content": "Bí danh tùy chỉnh cho khóa trò chuyện của bạn mà bạn có thể đăng ký bằng Dịch vụ tên Ethereum. Tên ENS là tên người dùng phi tập trung.", + "ens-name-not-found": "Không thể phân giải tên ENS", + "ens-name-title": "Tên ENS", + "ens-network-restriction": "Chỉ có trên Mainnet", + "ens-no-usernames": "Bạn không có bất kỳ tên người dùng nào được kết nối", + "ens-powered-by": "Được hỗ trợ bởi Dịch vụ tên Ethereum", + "ens-primary-username": "Tên người dùng chính", + "ens-register": "Đăng ký", + "ens-registration-failed": "Để đăng ký tên người dùng, vui lòng thử lại.", + "ens-registration-failed-title": "giao dịch không thành công", + "ens-registration-failure": "Đăng ký không thành công", + "ens-registration-in-progress": "Đang đăng ký ...", + "ens-release-username": "Giải phóng tên người dùng", + "ens-remove-hints": "Xóa sẽ tách tên người dùng khỏi khóa của bạn.", + "ens-remove-username": "Xóa tên người dùng", + "ens-saved": "hiện được kết nối với khóa trò chuyện của bạn và có thể được sử dụng trong Trạng thái.", + "ens-saved-title": "Đã thêm tên người dùng", + "ens-show-username": "Hiển thị tên người dùng ENS của tôi trong các cuộc trò chuyện", + "ens-terms-header": "Điều khoản đăng ký tên", + "ens-terms-point-1": "Tiền được gửi trong 1 năm. SNT của bạn sẽ bị khóa, nhưng không được chi tiêu.", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (Hệ thống đăng ký ENS).", + "ens-terms-point-2": "Sau 1 năm, bạn có thể sang tên và nhận lại tiền đặt cọc hoặc không cần thực hiện hành động nào để giữ tên.", + "ens-terms-point-3": "Nếu các điều khoản của hợp đồng thay đổi - ví dụ: Trạng thái thực hiện nâng cấp hợp đồng - người dùng có quyền phát hành tên người dùng bất kể thời gian được giữ.", + "ens-terms-point-4": "Người kiểm soát hợp đồng không thể truy cập số tiền đã ký gửi của bạn. Chúng chỉ có thể được chuyển trở lại địa chỉ đã gửi chúng.", + "ens-terms-point-5": "(Các) địa chỉ của bạn sẽ được liên kết công khai với tên ENS của bạn.", + "ens-terms-point-6": "Tên người dùng được tạo dưới dạng các nút miền phụ của stateofus.eth và tuân theo các điều khoản hợp đồng thông minh ENS.", + "ens-terms-point-7": "Bạn ủy quyền hợp đồng để chuyển giao SNT thay cho bạn. Điều này chỉ có thể xảy ra khi bạn chấp thuận một giao dịch để cho phép chuyển.", + "ens-terms-point-8": "Các điều khoản này được đảm bảo bởi logic hợp đồng thông minh tại các địa chỉ:", + "ens-terms-point-9": "{{address}} (Trạng thái UsernameRegistrar)", + "ens-terms-registration": "Điều khoản đăng ký tên.", + "ens-test-message": "Chào", + "ens-transaction-pending": "Giao dịch đang chờ xử lý ...", + "ens-understand": "Tôi hiểu rằng địa chỉ ví của tôi sẽ được kết nối công khai với tên người dùng của tôi.", + "ens-username": "Tên người dùng ENS", + "ens-username-already-added": "Tên người dùng đã được kết nối với khóa trò chuyện của bạn và có thể được sử dụng trong Trạng thái.", + "ens-username-available": "✓ Tên người dùng có sẵn!", + "ens-username-connected": "Tên người dùng này do bạn sở hữu và được kết nối với khóa trò chuyện của bạn.", + "ens-username-connected-continue": "Tiếp tục đặt `Hiển thị tên người dùng ENS của tôi trong cuộc trò chuyện`.", + "ens-username-connected-with-different-key": "Tiếp tục sẽ yêu cầu một giao dịch để kết nối tên người dùng với khóa trò chuyện hiện tại của bạn.", + "ens-username-connection-confirmation": "{{username}} sẽ được kết nối sau khi giao dịch hoàn tất.", + "ens-username-hints": "Ít nhất 4 ký tự. Chỉ các chữ cái Latinh, số và chữ thường.", + "ens-username-invalid": "Chỉ ký tự và số.", + "ens-username-owned": "✓ Tên người dùng do bạn sở hữu.", + "ens-username-owned-continue": "Tiếp tục sẽ kết nối tên người dùng này với khóa trò chuyện của bạn.", + "ens-username-registration-confirmation": "Đẹp! Bạn sở hữu {{username}} sau khi giao dịch hoàn tất.", + "ens-username-taken": "Tên người dùng đã được sử dụng :(", + "ens-username-you-can-follow-progress": "Bạn có thể theo dõi tiến trình trong phần Lịch sử giao dịch của ví.", + "ens-usernames": "Tên người dùng ENS", + "ens-usernames-details": "Đăng ký tên người dùng chung để người dùng khác dễ dàng nhận ra", + "ens-want-custom-domain": "Tôi sở hữu một tên miền khác", + "ens-want-domain": "Tôi muốn một miền stateofus.eth", + "ens-welcome-hints": "Tên ENS biến đổi những địa chỉ dài ngoằng đó thành tên người dùng duy nhất.", + "ens-welcome-point-customize": "Tên ENS có thể thay thế tên 3 từ ngẫu nhiên của bạn trong cuộc trò chuyện. Hãy là @yourname thay vì {{name}} .", + "ens-welcome-point-customize-title": "Tùy chỉnh tên trò chuyện của bạn", + "ens-welcome-point-receive": "Những người khác có thể gửi tiền cho bạn qua trò chuyện trong một bước đơn giản.", + "ens-welcome-point-receive-title": "Nhận giao dịch trong trò chuyện", + "ens-welcome-point-register": "Đăng ký một lần để giữ tên mãi mãi. Sau 1 năm, bạn có thể phát hành tên và lấy lại SNT của mình.", + "ens-welcome-point-register-title": "10 SNT để đăng ký", + "ens-welcome-point-simplify": "Bạn có thể nhận tiền bằng tên ENS dễ chia sẻ của mình hơn là hàm băm thập lục phân (0x ...).", + "ens-welcome-point-simplify-title": "Đơn giản hóa địa chỉ ETH của bạn", + "ens-welcome-point-verify": "Bạn có thể xác minh và thêm bất kỳ tên người dùng nào bạn sở hữu trong các bước tiếp theo.", + "ens-welcome-point-verify-title": "Đã sở hữu tên người dùng?", + "ens-your-username": "Tên người dùng của bạn", + "ens-your-usernames": "Tên người dùng của bạn", + "ens-your-your-name": "Tên ENS của bạn", + "enter-12-words": "Nhập 12 từ của cụm từ hạt giống của bạn, được phân tách bằng dấu cách đơn", + "enter-a-private-key": "Nhập khóa cá nhân", + "enter-a-seed-phrase": "Nhập một cụm từ hạt giống", + "enter-address": "Nhập địa chỉ", + "enter-contact-code": "Nhập tên người dùng ENS hoặc khóa trò chuyện", + "enter-pair-code": "Nhập mã ghép nối của bạn", + "enter-pair-code-description": "Mã ghép nối được hiển thị cho bạn trong quá trình thiết lập Thẻ khóa", + "enter-password": "Nhập mật khẩu", + "enter-pin": "Nhập mật mã gồm 6 chữ số", + "enter-puk-code": "Nhập mã PUK", + "enter-puk-code-description": "Mật mã 6 chữ số đã bị chặn.\n Vui lòng nhập mã PUK để mở khóa mật mã.", + "enter-recipient-address-or-username": "Nhập địa chỉ hoặc tên người dùng của người nhận", + "enter-seed-phrase": "Nhập cụm từ hạt giống", + "enter-url": "Nhập URL", + "enter-watch-account-address": "Quét mã QR\n hoặc là\n nhập địa chỉ để xem", + "enter-word": "Nhập từ", + "enter-your-code": "Nhập mật mã gồm 6 chữ số của bạn", + "enter-your-password": "Nhập mật khẩu của bạn", + "error": "lỗi", + "error-unable-to-get-balance": "Không thể lấy số dư", + "error-unable-to-get-prices": "Lỗi chuyển đổi tiền tệ. Làm mới màn hình của bạn để thử lại.", + "error-unable-to-get-token-balance": "Không thể lấy số dư mã thông báo", + "errors": "Lỗi", + "eth": "ETH", + "ethereum-account": "Tài khoản Ethereum", + "ethereum-address": "Địa chỉ Ethereum", + "ethereum-node-started-incorrectly-description": "Nút Ethereum đã được khởi động với cấu hình không chính xác, ứng dụng sẽ bị dừng để khôi phục từ tình trạng đó. Id mạng đã định cấu hình = {{network-id}} , thực tế = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "Nút Ethereum bắt đầu không chính xác", + "etherscan-lookup": "Tra cứu trên Etherscan", + "export-account": "Xuất tài khoản", + "external-storage-denied": "Quyền truy cập vào bộ nhớ ngoài bị từ chối", + "failed": "Thất bại", + "faq": "Các câu hỏi thường gặp", + "favourites": "Yêu thích", + "favourites-empty": "Địa chỉ được thêm vào danh sách yêu thích sẽ xuất hiện ở đây", + "fetch-messages": "↓ Tìm nạp tin nhắn", + "find": "Tìm thấy", + "finish": "Hoàn thành", + "finishing-card-setup": "Hoàn tất thiết lập thẻ", + "fleet": "Hạm đội", + "fleet-settings": "Cài đặt hạm đội", + "follow-your-interests": "Tham gia cuộc trò chuyện công khai và gặp gỡ những người mới", + "free": "↓ Miễn phí", + "from": "Từ", + "gas-limit": "Giới hạn khí", + "gas-price": "Giá xăng", + "gas-used": "Khí sử dụng", + "generate-a-key": "Tạo khóa", + "generate-a-new-account": "Tạo tài khoản", + "generate-a-new-key": "Tạo khóa mới", + "generate-account": "Tạo khóa", + "generate-an-account": "Tạo tài khoản", + "generate-new-key": "Tạo khóa", + "generating-codes-for-pairing": "> Tải phần mềm sản phẩm xuống thẻ\n > Tạo mã mở khóa và ghép nối", + "generating-keys": "Đang tạo khóa ...", + "generating-mnemonic": "Tạo cụm từ hạt giống", + "get-started": "Bắt đầu", + "get-status-at": "Nhận trạng thái tại http:\/\/status.im", + "get-stickers": "Nhận hình dán", + "give-permissions-camera": "Cho phép\n truy cập máy ảnh", + "glossary": "Bảng chú giải", + "go-to-settings": "Đi tới Cài đặt ...", + "got-it": "Hiểu rồi", + "grant-face-id-permissions": "Để cấp quyền Face ID cần thiết, vui lòng chuyển đến cài đặt hệ thống của bạn và đảm bảo rằng Trạng thái > Face ID được chọn", + "group-chat": "Trò chuyện nhóm", + "group-chat-admin": "Admin", + "group-chat-admin-added": "** {{member}} ** đã được làm quản trị viên", + "group-chat-all-contacts-invited": "Tất cả địa chỉ liên hệ của bạn đã có trong nhóm", + "group-chat-created": "** {{member}} ** đã tạo nhóm ** {{name}} **", + "group-chat-decline-invitation": "Từ chối lời mời", + "group-chat-member-added": "** {{member}} ** đã được mời", + "group-chat-member-joined": "** {{member}} ** đã tham gia nhóm", + "group-chat-member-removed": "** {{member}} ** đã rời khỏi nhóm", + "group-chat-members-count": "{{selected}} \/ {{max}} thành viên", + "group-chat-name-changed": "** {{member}} ** đã đổi tên nhóm thành ** {{name}} **", + "group-chat-no-contacts": "Bạn chưa có bất kỳ liên hệ nào.\n Mời bạn bè của bạn bắt đầu trò chuyện", + "group-info": "Thông tin nhóm", + "group-invite": "Mời nhóm", + "group-invite-link": "Liên kết mời nhóm", + "group-membership-request": "Yêu cầu thành viên nhóm", + "gwei": "Gwei", + "hash": "Băm", + "help": "Cứu giúp", + "help-capitalized": "Cứu giúp", + "help-center": "Trung tâm trợ giúp", + "hide-content-when-switching-apps": "Chặn ảnh chụp màn hình", + "hide-content-when-switching-apps-ios": "Ẩn bản xem trước", + "history": "Lịch sử", + "history-nodes": "Các nút lịch sử", + "hold-card": "Giữ thẻ ở mặt sau\n điện thoại của bạn", + "home": "Trang Chủ", + "hooks": "Móc", + "how-it-works": "Làm thế nào nó hoạt động", + "http-gateway-error": "Rất tiếc, yêu cầu không thành công!", + "identifier": "Định danh", + "image": "Hình ảnh", + "image-remove-current": "Loại bỏ hình ảnh hiện tại", + "image-source-gallery": "Chọn từ thư viện", + "image-source-make-photo": "Chiếm lấy", + "image-source-title": "Chỉnh sửa hình ảnh", + "in-contacts": "Trong danh bạ", + "incoming": "Mới đến", + "incoming-transaction": "Giao dịch đến", + "incorrect-code": [ + "str", + "Xin lỗi, mã không chính xác, vui lòng nhập lại" + ], + "increase-gas": "Tăng khí", + "initialization": "Khởi tạo", + "install": "↓ Cài đặt", + "intro-message1": "Chào mừng đến với Trạng thái!\n Nhấn vào tin nhắn này để đặt mật khẩu của bạn và bắt đầu.", + "intro-privacy-policy-note1": "Trạng thái không thu thập hoặc thu lợi từ dữ liệu cá nhân của bạn. Bằng cách tiếp tục, bạn đồng ý với", + "intro-privacy-policy-note2": "Chính sách bảo mật", + "intro-text": "Trạng thái là cửa ngõ của bạn vào web phi tập trung", + "intro-text1": "Trò chuyện qua mạng ngang hàng, được mã hóa nơi tin nhắn không thể bị kiểm duyệt hoặc bị tấn công", + "intro-text2": "Gửi và nhận tài sản kỹ thuật số ở mọi nơi trên thế giới — không cần tài khoản ngân hàng", + "intro-text3": "Khám phá các trò chơi, sàn giao dịch và mạng xã hội nơi bạn chỉ sở hữu dữ liệu của mình", + "intro-title1": "Giao tiếp thực sự riêng tư", + "intro-title2": "Ví tiền điện tử an toàn", + "intro-title3": "Ứng dụng phi tập trung", + "intro-wizard-text1": "Một bộ khóa kiểm soát tài khoản của bạn. Chìa khóa của bạn có trên điện thoại của bạn, vì vậy chỉ bạn mới có thể sử dụng chúng", + "intro-wizard-text2": "Một chìa khóa là để trò chuyện. Nó đi kèm với một cái tên dễ đọc và không thể thay đổi.", + "intro-wizard-text3": "Nếu bạn sở hữu Thẻ khóa, hãy cất chìa khóa của bạn ở đó để tăng cường bảo mật.", + "intro-wizard-text4": "Bảo mật và mã hóa khóa của bạn", + "intro-wizard-text6": "Trạng thái sẽ thông báo cho bạn về tin nhắn mới. Bạn có thể chỉnh sửa tùy chọn thông báo của mình sau trong cài đặt", + "intro-wizard-title-alt4": "Tạo một mật khẩu", + "intro-wizard-title-alt5": "Xác nhận mật khẩu của bạn", + "intro-wizard-title1": "Lấy chìa khóa của bạn", + "intro-wizard-title2": "Chọn tên trò chuyện", + "intro-wizard-title3": "Chọn bộ nhớ chính", + "intro-wizard-title4": "Tạo mật mã gồm 6 chữ số", + "intro-wizard-title5": "Xác nhận mật mã", + "intro-wizard-title6": "Bật thông báo", + "introduce-yourself": "Giới thiệu bản thân bằng một tin nhắn ngắn gọn", + "invalid-address-qr-code": "Mã QR được quét không chứa địa chỉ hợp lệ", + "invalid-format": "Định dạng không hợp lệ\n Phải là {{format}}", + "invalid-key-confirm": "Ứng dụng", + "invalid-key-content": "Không thể mã hóa cơ sở dữ liệu vì tệp bị hỏng. Tiền và khóa trò chuyện của bạn được an toàn. Không thể khôi phục dữ liệu khác, như các cuộc trò chuyện và danh bạ của bạn. {{erase-multiaccounts-data-button-text}} “ {{erase-multiaccounts-data-button-text}} ”, sẽ xóa tất cả dữ liệu khác và cho phép bạn truy cập vào tiền của mình và gửi tin nhắn", + "invalid-number": "Số không hợp lệ", + "invalid-pairing-password": "Mật khẩu ghép nối không hợp lệ", + "invalid-public-chat-topic": "Chủ đề trò chuyện công khai không hợp lệ", + "invalid-range": "Định dạng không hợp lệ, phải từ {{min}} đến {{max}}", + "invite": "Mời gọi", + "invite-button": "Mời gọi", + "invite-chat-accept": "Chấp nhận", + "invite-chat-intro": "Bạn được một người bạn giới thiệu tham gia Trạng thái. Đây là một số tiền điện tử để bạn bắt đầu! Sử dụng nó để đăng ký tên ENS hoặc mua một gói hình dán", + "invite-chat-name": "Giới thiệu bạn bè", + "invite-chat-pending": "Đang chờ xử lý", + "invite-chat-rule": "Việc chấp nhận cũng sẽ thưởng cho bạn bè của bạn một phần thưởng giới thiệu tiền điện tử", + "invite-chat-starter-pack": "Gói khởi động", + "invite-friends": "Mời bạn bè", + "invite-instruction-fifth": "Bạn có thể chọn đổi phần thưởng giới thiệu của mình bất cứ lúc nào.", + "invite-instruction-first": "Bạn gửi một liên kết mời duy nhất đến bạn bè của mình để tải xuống và tham gia Trạng thái", + "invite-instruction-fourth": "Bạn nhận được phần thưởng giới thiệu của mình và bạn bè của bạn Gói Khởi đầu", + "invite-instruction-second": "Bạn bè của bạn tải xuống Trạng thái và tạo tài khoản (trên Android)", + "invite-instruction-third": "Trò chuyện với bạn bè của bạn được bắt đầu, nơi họ xác nhận sự giới thiệu của bạn", + "invite-privacy-policy1": "Bằng cách chấp nhận, bạn đồng ý với chương trình giới thiệu", + "invite-privacy-policy2": "Các điều khoản và điều kiện.", + "invite-receive-account": "Tài khoản để nhận tiền thưởng giới thiệu của bạn", + "invite-reward": "Kiếm tiền điện tử cho mọi người bạn mà bạn mời!", + "invite-reward-friend": "Bạn:", + "invite-reward-friend-description": "Bạn của bạn sẽ nhận được Gói khởi đầu bao gồm một số {{reward}} để bắt đầu", + "invite-reward-friend-name": "Gói khởi động", + "invite-reward-you": "Bạn:", + "invite-reward-you-description": "Mời bạn bè và nhận {{reward}} dưới dạng phần thưởng giới thiệu. Sử dụng nó để nhận nhãn dán, tên ENS và thử dapp", + "invite-reward-you-name": "Tiền thưởng giới thiệu", + "invite-select-account": "Chọn một tài khoản để nhận phần thưởng giới thiệu của bạn", + "invite-warning": "Khuyến mãi này chỉ có hiệu lực cho người dùng thiết bị Android không phải là cư dân của Hoa Kỳ. Bạn bè cần xác nhận giới thiệu trong vòng 7 ngày", + "invited": "được mời", + "join": "Tham gia", + "join-group-chat": "Tham gia nhóm", + "join-group-chat-description": "{{username}} đã mời bạn tham gia nhóm {{group-name}}", + "join-me": "Xin hãy tham gia với tôi trên Trạng thái: {{url}}", + "join-new-private-chat": "Bắt đầu một cuộc trò chuyện riêng tư mới", + "join-new-public-chat": "Tham gia cuộc trò chuyện công khai", + "joined-group-chat-description": "Bạn đã tham gia {{group-name}} từ lời mời của {{username}}", + "key": "Chìa khóa", + "keycard": "Thẻ chìa khóa", + "keycard-access-reset": "Quyền truy cập thẻ khóa được đặt lại", + "keycard-applet-install-instructions": "Để cài đặt applet, vui lòng làm theo hướng dẫn trên https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation", + "keycard-awaiting-description": "Thử di chuyển thẻ xung quanh để tìm đầu đọc NFC trên thiết bị của bạn", + "keycard-awaiting-title": "Vẫn đang tìm kiếm...", + "keycard-blocked": "Thẻ khóa đã bị chặn.\n Bạn cần đặt lại thẻ để tiếp tục sử dụng nó.", + "keycard-can-use-with-new-passcode": "Bạn có thể sử dụng thẻ này với mật mã mới của mình", + "keycard-cancel-setup-text": "Thao tác này sẽ hủy thiết lập thẻ khóa. Bạn nên hoàn tất quá trình thiết lập để sử dụng thẻ khóa. Bạn có thực sự muốn hủy không?", + "keycard-cancel-setup-title": "Hoạt động nguy hiểm", + "keycard-connected-description": "Thử giữ yên thẻ", + "keycard-connected-title": "Đã kết nối", + "keycard-desc": "Sở hữu một Thẻ khóa? Lưu trữ chìa khóa của bạn trên đó; bạn sẽ cần nó cho các giao dịch", + "keycard-dont-ask-card": "Không yêu cầu thẻ để đăng nhập", + "keycard-enter-new-passcode": "Nhập mật mã mới {{step}} \/ 2", + "keycard-error-description": "Kết nối lại thẻ để tiếp tục", + "keycard-error-title": "Kết nối bị mất", + "keycard-free-pairing-slots": "Keycard có {{n}} vị trí ghép nối miễn phí", + "keycard-has-multiaccount-on-it": "Thẻ này đã đầy. Mỗi thẻ có thể chứa một cặp khóa chính", + "keycard-init-description": "Đặt thẻ vào mặt sau điện thoại của bạn để tiếp tục", + "keycard-init-title": "Đang tìm thẻ ...", + "keycard-is-blocked-details": "Bạn không thể sử dụng thẻ này để truy cập hoặc đăng nhập vào tài khoản này nữa. Đã có quá nhiều lần nhập mật mã và mã PUK không thành công.", + "keycard-is-blocked-instructions": "Để truy cập vào tài khoản của bạn, hãy cài đặt lại Trạng thái và sử dụng Thẻ khóa mới, hãy sử dụng ví khác hoặc đặt lại Thẻ khóa theo cách thủ công.", + "keycard-is-blocked-title": "Thẻ khóa bị chặn", + "keycard-is-frozen-details": "Để bảo vệ tài sản của bạn, thẻ của bạn đã bị đóng băng. Đặt lại quyền truy cập thẻ để mở khóa và gửi giao dịch. Tạo mật mã mới và nhập PUK của bạn để truy cập (các) tài khoản của bạn trên thẻ này", + "keycard-is-frozen-reset": "Đặt lại quyền truy cập thẻ", + "keycard-is-frozen-title": "Thẻ khóa bị đóng băng", + "keycard-onboarding-finishing-header": "Kết thúc", + "keycard-onboarding-intro-header": "Lưu trữ chìa khóa của bạn trên Keycard", + "keycard-onboarding-intro-text": "Hãy sẵn sàng, quá trình này có thể mất vài phút, nhưng điều quan trọng là phải bảo mật tài khoản của bạn", + "keycard-onboarding-pairing-header": "Ghép nối thẻ ...", + "keycard-onboarding-preparing-header": "Đang chuẩn bị thẻ ...", + "keycard-onboarding-puk-code-header": "Viết mã xuống\n và lưu trữ chúng một cách an toàn", + "keycard-onboarding-recovery-phrase-description": "Bạn cần cụm từ hạt giống này để lấy lại chìa khóa của mình. Viết nó ra. Giữ nó an toàn, ngoại tuyến và tách biệt với thiết bị này.", + "keycard-onboarding-recovery-phrase-header": "Sao lưu cụm từ hạt giống", + "keycard-onboarding-recovery-phrase-text": "Chỉ dung cho măt. Đây là hạt giống kỳ diệu được sử dụng để tạo ra khóa của bạn.", + "keycard-onboarding-start-header": "Giữ thẻ ở mặt sau\n của điện thoại của bạn để bắt đầu", + "keycard-onboarding-start-step1": "Tạo mật mã", + "keycard-onboarding-start-step1-text": "Khoảng 1 phút. Tạo mật mã gồm 6 chữ số để mã hóa khóa của bạn", + "keycard-onboarding-start-step2": "Viết ra PUK và mã ghép nối", + "keycard-onboarding-start-step2-text": "Khoảng 1 phút. Bạn sẽ cần một mảnh giấy và một cây bút chì cho việc đó", + "keycard-onboarding-start-step3": "Sao lưu cụm từ hạt giống", + "keycard-onboarding-start-step3-text": "Khoảng 1 phút. Ngoài ra, một mảnh giấy và một cây bút chì là cần thiết", + "keycard-onboarding-start-text": "Và duy trì thẻ liên hệ qua điện thoại\n trong quá trình thiết lập. Quá trình thiết lập sẽ mất khoảng 4 phút", + "keycard-processing-description": "Thử giữ yên thẻ", + "keycard-processing-title": "Chế biến...", + "keycard-recovery-intro-button-text": "Bắt đầu khôi phục", + "keycard-recovery-intro-header": "Khôi phục các khóa được lưu trữ trên thẻ khóa", + "keycard-recovery-intro-text": "Nếu bạn đã tạo khóa bằng thẻ khóa trước đây và bây giờ muốn sử dụng các khóa này trên thiết bị này", + "keycard-recovery-no-key-header": "Không có gì để\n phục hồi ở đây", + "keycard-recovery-no-key-text": "Thẻ Keycard của bạn không có khóa nào được lưu trên đó. Để sử dụng nó, hãy tạo một khóa mới và chọn Thẻ khóa của bạn để lưu trữ khóa", + "keycard-recovery-phrase-confirm-header": "Xác nhận cụm từ hạt giống", + "keycard-recovery-phrase-confirmation-text": "Bạn sẽ không có cơ hội thứ hai! Nếu bạn mất quyền truy cập, chẳng hạn như mất thẻ khóa, bạn chỉ có thể truy cập vào các khóa của mình bằng cụm từ hạt giống. Không ai, nhưng bạn có cụm từ hạt giống của bạn. Viết nó ra. Giữ nó an toàn.", + "keycard-recovery-phrase-confirmation-title": "Đã viết cụm từ hạt giống xuống?", + "keycard-recovery-success-header": "Chìa khóa của bạn đã được\n phục hồi thành công", + "keycard-redeem-title": "Đổi thành", + "keycard-redeem-tx": "Đổi tài sản", + "keycard-redeem-tx-desc": "Nhấn vào thẻ để ký và nhận nội dung", + "keycard-reset-passcode": "Đặt lại mật mã", + "keycard-success-description": "Bạn có thể xóa thẻ ngay bây giờ", + "keycard-success-title": "Sự thành công", + "keycard-unauthorized-operation": "Bạn không được phép thực hiện thao tác này.\n Vui lòng nhấn vào thẻ hợp lệ và thử lại.", + "language": "Ngôn ngữ", + "learn-more": "Tìm hiểu thêm", + "learn-more-about-keycard": "Tìm hiểu thêm về Keycard", + "leave": "Rời khỏi", + "leave-chat": "Rời khỏi trò chuyện", + "leave-chat-confirmation": "Bạn có chắc chắn muốn rời khỏi cuộc trò chuyện không?", + "leave-confirmation": "Rời khỏi trò chuyện", + "leave-group": "Rời khỏi nhóm", + "left": "trái", + "les-ulc": "LES \/ ULC", + "lets-go": "Đi nào", + "light": "Ánh sáng", + "linked-on": "Được liên kết vào {{date}}", + "load-messages-before": "trước {{date}}", + "load-more-messages": "↓ Tìm nạp thêm tin nhắn", + "loading": "Đang tải...", + "local-notifications": "Thông báo địa phương", + "lock-app-with": "Khóa ứng dụng bằng", + "log-level": "Mức đăng nhập", + "log-level-settings": "Cài đặt cấp độ nhật ký", + "logging": "Ghi nhật ký", + "logging-enabled": "Ghi nhật ký được bật?", + "login-pin-description": "Nhập mật mã gồm 6 chữ số của bạn để mở khóa", + "logout": "Đăng xuất", + "logout-app-content": "Tài khoản sẽ được đăng xuất. Khi bạn mở khóa lại, mạng đã chọn sẽ được sử dụng", + "logout-are-you-sure": "Bạn có chắc bạn muốn\n thoát ra?", + "logout-title": "Đăng xuất?", + "looking-for-cards": "Đang tìm thẻ ...", + "lost-connection": "Mất kết nối", + "mail-should-be-configured": "Ứng dụng thư phải được định cấu hình", + "mailserver-address": "Địa chỉ nút lịch sử", + "mailserver-automatic": "Lựa chọn tự động", + "mailserver-automatic-switch-explanation": "Chọn nút lịch sử nhanh nhất hiện có", + "mailserver-connection-error": "Không thể kết nối với nút lịch sử", + "mailserver-content": "Một nút trong mạng Trạng thái định tuyến và lưu trữ thông báo, trong tối đa 30 ngày.", + "mailserver-details": "Chi tiết nút lịch sử", + "mailserver-error-content": "Không thể truy cập vào nút lịch sử bạn đã chọn.", + "mailserver-error-title": "Lỗi khi kết nối với nút lịch sử", + "mailserver-format": "enode: \/\/ {enode-id} : {password} @ {ip-address} : {port}", + "mailserver-pick-another": "Chọn một nút lịch sử khác", + "mailserver-reconnect": "Không thể kết nối với nút lịch sử. Nhấn để kết nối lại", + "mailserver-request-error-content": "Nút lịch sử trả về lỗi sau: {{error}}", + "mailserver-request-error-status": "Đã xảy ra lỗi khi tìm nạp lịch sử, hãy kiểm tra nhật ký để biết chi tiết", + "mailserver-request-error-title": "Lỗi yêu cầu nút lịch sử", + "mailserver-request-retry": "Yêu cầu thử lại", + "mailserver-retry": "Thử lại", + "mailserver-title": "Nút lịch sử", + "main-currency": "Tiền tệ chính", + "main-networks": "Các mạng chính", + "main-wallet": "Ví chính", + "mainnet-network": "Mạng chính", + "make-admin": "Làm quản trị viên", + "mark-all-read": "đánh dấu tất cả đã đọc", + "maybe-later": "Có lẽ sau", + "members": { + "other": "{{count}} thành viên" + }, + "members-active": { + "other": "{{count}} thành viên" + }, + "members-active-none": "không có thành viên", + "members-count": "{{count}} thành viên", + "members-label": "Các thành viên", + "members-limit-reached": "Đã đạt đến giới hạn thành viên", + "members-title": "Các thành viên", + "membership-declined": "Yêu cầu tư cách thành viên đã bị từ chối", + "membership-description": "Tư cách thành viên nhóm yêu cầu bạn phải được quản trị viên nhóm chấp nhận", + "membership-none": "không ai", + "message": "Thông điệp", + "message-not-sent": "Tin nhắn chưa được gửi", + "message-options-cancel": "Huỷ bỏ", + "message-reply": "Đáp lại", + "messages": "Tin nhắn", + "might-break": "Có thể phá vỡ một số ÐApps", + "migrations-failed-content": "{{message}}\n phiên bản giản đồ: ban đầu {{initial-version}} , {{current-version}} , {{last-version}} \n\n Đã xảy ra lỗi cơ sở dữ liệu. Tiền và khóa trò chuyện của bạn được an toàn. Không thể khôi phục các dữ liệu khác như cuộc trò chuyện và danh bạ của bạn. {{erase-multiaccounts-data-button-text}} \" {{erase-multiaccounts-data-button-text}} \", sẽ xóa tất cả dữ liệu khác và cho phép bạn truy cập vào tiền của mình và gửi tin nhắn.", + "mobile-network-ask-me": "Hỏi tôi khi ở trên mạng di động", + "mobile-network-continue-syncing": "Tiếp tục đồng bộ hóa", + "mobile-network-continue-syncing-details": "Bạn có thể thay đổi điều này sau trong cài đặt", + "mobile-network-go-to-settings": "Đi tới cài đặt", + "mobile-network-settings": "Dữ liệu di động", + "mobile-network-sheet-configure": "Bạn có thể định cấu hình đồng bộ hóa trong nhiều hơn nữa\n chi tiết trong", + "mobile-network-sheet-offline": "Không có Wi-Fi, đồng bộ hóa tin nhắn bị tắt.", + "mobile-network-sheet-offline-details": "Đồng bộ hóa bằng mạng di động bị tắt", + "mobile-network-sheet-remember-choice": "Hãy nhớ sự lựa chọn của tôi", + "mobile-network-sheet-settings": "cài đặt", + "mobile-network-start-syncing": "Bắt đầu đồng bộ hóa", + "mobile-network-stop-syncing": "Dừng đồng bộ hóa", + "mobile-network-stop-syncing-details": "Cho đến khi kết nối với Wi-Fi?", + "mobile-network-use-mobile": "Sử dụng dữ liệu di động", + "mobile-network-use-mobile-data": "Trạng thái sử dụng nhiều dữ liệu khi đồng bộ hóa cuộc trò chuyện và ví.", + "mobile-network-use-wifi": "Chỉ wifi", + "mobile-syncing-sheet-details": "Trạng thái sử dụng nhiều dữ liệu khi đồng bộ hóa cuộc trò chuyện và ví.", + "mobile-syncing-sheet-title": "Đồng bộ hóa bằng dữ liệu di động?", + "more": "hơn", + "multiaccount-exists-content": "Các khóa cho tài khoản này đã tồn tại và không thể thêm lại. Nếu bạn bị mất mật khẩu, mật mã hoặc Thẻ khóa, hãy gỡ cài đặt ứng dụng, cài đặt lại và truy cập các khóa của bạn bằng cách nhập cụm từ hạt giống của bạn", + "multiaccount-exists-title": "Các khóa cho tài khoản này đã tồn tại", + "multiaccounts-recover-enter-phrase-text": "Nhập 12, 15, 18, 21 hoặc 24 từ.\n Phân cách các từ bằng một khoảng trắng.", + "multiaccounts-recover-enter-phrase-title": "Nhập cụm từ hạt giống của bạn", + "mute": "Tắt tiếng", + "my-accounts": "Tài khoản của tôi", + "my-accounts-empty": "Các tài khoản khả dụng của bạn sẽ xuất hiện ở đây", + "my-status": "Trạng thái của tôi", + "name": "Tên", + "name-of-token": "Tên mã thông báo của bạn", + "name-optional": "Tên (tùy chọn)", + "need-help": "Cần giúp đỡ?", + "network": "Mạng lưới", + "network-chain": "Chuỗi mạng", + "network-details": "Chi tiết mạng", + "network-fee": "Phí mạng", + "network-id": "ID mạng", + "network-info": "Thông tin mạng", + "network-invalid-network-id": "Id mạng đã chỉ định không tương ứng với id mạng theo url RPC", + "network-invalid-status-code": "Mã trạng thái không hợp lệ: {{code}}", + "network-invalid-url": "URL mạng không hợp lệ", + "network-settings": "Thiết lạp mạng lưới", + "new": "Mới", + "new-chat": "Cuộc trò truyện mới", + "new-contact": "Địa chỉ liên lạc mới", + "new-contract": "Liên hệ mới", + "new-favourite": "Yêu thích mới", + "new-group": "Nhóm mới", + "new-group-chat": "Trò chuyện nhóm mới", + "new-network": "Mạng mới", + "new-pin-description": "Nhập mật mã 6 chữ số mới", + "new-public-group-chat": "Tham gia trò chuyện công khai", + "new-status": "Trạng thái mới", + "next": "Kế tiếp", + "nickname": "Tên nick", + "nickname-description": "Biệt hiệu giúp bạn xác định những người khác trong Trạng thái.\n Chỉ bạn mới có thể xem các biệt hiệu bạn đã thêm", + "no": "Không", + "no-collectibles": "Không có sẵn sưu tầm", + "no-contacts": "Chưa có liên hệ", + "no-keycard-applet-on-card": "Không có applet Keycard trên thẻ", + "no-messages": "Không có tin nhắn", + "no-pairing-slots-available": "Thẻ này đã được ghép nối với 5 thiết bị và không thể ghép nối với thiết bị này. Vui lòng sử dụng một trong các thiết bị được ghép nối, đăng nhập bằng thẻ này và giải phóng các khe ghép nối trên thẻ", + "no-result": "Ko có kết quả", + "no-tokens-found": "Không tìm thấy mã thông báo", + "node-info": "Thông tin nút", + "node-version": "Phiên bản nút", + "nonce": "Nonce", + "none": "không ai", + "not-applicable": "Không áp dụng cho các giao dịch chưa ký", + "not-enough-snt": "Không đủ SNT", + "not-keycard-text": "Thẻ bạn đã sử dụng không phải là Thẻ khóa. Bạn cần mua Thẻ khóa để sử dụng", + "not-keycard-title": "Không phải thẻ khóa", + "not-registered": "chưa đăng ký", + "notification-settings": "Thông báo", + "notifications": "Thông báo", + "notifications-non-contacts": "Thông báo từ những người không liên hệ", + "notifications-preferences": "Tùy chọn thông báo", + "notifications-servers": "Máy chủ thông báo", + "notifications-switch": "Hiển thị thông báo", + "notifications-transactions": "Giao dịch ví", + "notify": "Thông báo", + "now": "Hiện nay", + "off": "Tắt", + "off-status-tree": "Cây trạng thái tắt", + "offline": "Ngoại tuyến", + "offline-messaging-use-history-explanation": "Bật các nút lịch sử để tìm nạp các tin nhắn đã được gửi trong khi ứng dụng bị đóng. Khi được bật, nút lịch sử sẽ lấy địa chỉ IP của bạn. Khi bị vô hiệu hóa, bạn sẽ không nhận được tin nhắn khi ứng dụng bị đóng và sẽ không nhìn thấy chúng khi bạn mở ứng dụng sau đó.", + "offline-messaging-use-history-nodes": "Sử dụng các nút lịch sử", + "ok": "đồng ý", + "ok-continue": "Được rồi, tiếp tục", + "ok-got-it": "Được rồi hiểu rồi", + "ok-save-pass": "OK, lưu mật khẩu", + "okay": "Được chứ", + "on": "Trên", + "on-status-tree": "Trên cây trạng thái", + "once-enabled-share-metadata": "Sau khi được bật, các liên kết được đăng trong cuộc trò chuyện có thể chia sẻ siêu dữ liệu của bạn với trang web", + "open": "Mở", + "open-dapp": "Mở ÐApp", + "open-dapp-store": "Khám phá ÐApps", + "open-home": "Mở...", + "open-nfc-settings": "Mở cài đặt NFC", + "optional": "không bắt buộc", + "or": "HOẶC LÀ", + "outgoing": "Hướng ngoaị", + "outgoing-transaction": "Giao dịch gửi đi", + "page-camera-request-blocked": "yêu cầu máy ảnh bị chặn. Để kích hoạt yêu cầu máy ảnh, hãy đi tới Cài đặt", + "page-would-like-to-use-camera": "muốn sử dụng máy ảnh của bạn", + "pair": "Ghép nối thiết bị", + "pair-card": "Ghép nối với thiết bị này", + "pair-code": "Ghép nối mã", + "pair-code-explanation": "Ghép nối thẻ với một thiết bị khác (tối đa 5) để mở khóa và ký giao dịch bằng cùng một Thẻ khóa", + "pair-code-placeholder": "Ghép nối mã ...", + "pair-this-card": "Ghép nối thẻ này", + "pair-this-device": "Quảng cáo thiết bị", + "pair-this-device-description": "Ghép nối các thiết bị của bạn để đồng bộ hóa danh bạ và cuộc trò chuyện giữa chúng", + "paired-devices": "Các thiết bị được ghép nối", + "pairing": "Ghép nối", + "pairing-card": "Thẻ ghép nối", + "pairing-go-to-installation": "Đi tới cài đặt ghép nối", + "pairing-maximum-number-reached-content": "Vui lòng tắt một trong các thiết bị của bạn trước khi bật thiết bị mới.", + "pairing-maximum-number-reached-title": "Đã đạt đến số lượng thiết bị tối đa", + "pairing-new-installation-detected-content": "Một thiết bị mới đã được phát hiện.\n Để sử dụng thiết bị của bạn một cách chính xác, điều quan trọng là phải ghép nối và bật chúng trước khi sử dụng.\n Vui lòng chuyển đến phần thiết bị trong cài đặt để ghép nối các thiết bị của bạn.", + "pairing-new-installation-detected-title": "Đã phát hiện thiết bị mới", + "pairing-no-info": "Không có thông tin", + "pairing-please-set-a-name": "Vui lòng đặt tên cho thiết bị của bạn.", + "passphrase": "Cụm mật khẩu", + "password": "Mật khẩu", + "password-description": "Ít nhất 6 kí tự. Mật khẩu của bạn bảo vệ các khóa của bạn. Bạn cần nó để mở khóa Trạng thái và giao dịch.", + "password-placeholder": "Mật khẩu...", + "password-placeholder2": "Xác nhận mật khẩu của bạn", + "password_error1": "Mật khẩu không khớp.", + "paste": "Dán", + "paste-json": "Dán JSON", + "pay-to-chat": "Trả tiền để trò chuyện", + "peer-content": "Một thiết bị được kết nối với mạng trò chuyện Trạng thái. Mỗi người dùng có thể đại diện cho một hoặc nhiều đồng nghiệp, tùy thuộc vào số lượng thiết bị của họ.", + "peer-title": "Ngang nhau", + "peers": "Ngang hàng", + "pending": "Đang chờ xử lý", + "pending-confirmation": "Chờ xác nhận...", + "pending-invitations": "Yêu cầu thành viên đang chờ xử lý", + "permissions": "Quyền", + "phone-e164": "Quốc tế 1", + "photos": "Ảnh", + "photos-access-error": "Để cấp quyền cho ảnh cần thiết, vui lòng chuyển đến cài đặt hệ thống của bạn và đảm bảo rằng Trạng thái > Ảnh được chọn.", + "pin-changed": "Mật mã 6 chữ số đã được thay đổi", + "pin-code": "Mật mã 6 chữ số", + "pin-mismatch": "Mật mã sai", + "pin-one-attempt": "một lần thử", + "pin-one-attempt-blocked-after": "trước khi Thẻ khóa của bạn bị chặn", + "pin-one-attempt-blocked-before": "Hãy cẩn thận, bạn chỉ có", + "pin-one-attempt-frozen-after": "trước khi thẻ Keycard của bạn bị đóng băng", + "pin-one-attempt-frozen-before": "Hãy cẩn thận, bạn chỉ có", + "pin-retries-left": "{{number}} còn lại", + "preference": "Sở thích", + "preview-privacy": "Xem trước chế độ bảo mật", + "previewing-may-share-metadata": "Xem trước các liên kết từ các trang web này có thể chia sẻ siêu dữ liệu của bạn với chủ sở hữu của chúng", + "privacy": "Riêng tư", + "privacy-and-security": "Quyền riêng tư và bảo mật", + "privacy-policy": "Chính sách bảo mật", + "private-key": "Khóa cá nhân", + "private-notifications": "Thông báo riêng tư", + "private-notifications-descr": "Trạng thái sẽ thông báo cho bạn về tin nhắn mới. Bạn có thể chỉnh sửa tùy chọn thông báo của mình sau trong cài đặt.", + "processing": "Chờ một chút", + "product-information": "Thông tin sản phẩm", + "profile": "Hồ sơ", + "profile-deleted-content": "Hồ sơ của bạn đã được xóa thành công", + "profile-deleted-title": "Hồ sơ đã bị xóa", + "profile-details": "Chi tiết hồ sơ", + "profile-pic-pick": "Chọn từ thư viện", + "public-chat": "Trò chuyện công khai", + "public-chat-description": "Tham gia các cuộc trò chuyện công khai vì sở thích của bạn! Bất kỳ ai cũng có thể bắt đầu một cái mới.", + "public-chats": "Trò chuyện công khai", + "public-group-status": "Công cộng", + "public-group-topic": "Đề tài", + "public-key": "Khóa công khai", + "puk-and-pairing-codes-displayed": "Mã PUK và mã ghép nối được hiển thị", + "puk-code": "Mã PUK", + "puk-code-explanation": "Nếu bạn quên mật mã gồm 6 chữ số hoặc nhập sai 3 lần, bạn sẽ cần mã này để mở khóa thẻ của mình.", + "puk-mismatch": "Mã PUK sai", + "push-failed-transaction": "Giao dịch của bạn không thành công", + "push-failed-transaction-body": "{{value}} {{currency}} thành {{to}}", + "push-inbound-transaction": "Bạn đã nhận được {{value}} {{currency}}", + "push-inbound-transaction-body": "Từ {{from}} đến {{to}}", + "push-notifications-server-enabled": "Máy chủ được kích hoạt", + "push-notifications-servers": "Máy chủ thông báo đẩy", + "push-outbound-transaction": "Bạn đã gửi {{value}} {{currency}}", + "push-outbound-transaction-body": "Từ {{from}} đến {{to}}", + "quiet-days": "{{quiet-days}} ngày", + "quiet-hours": "{{quiet-hours}} giờ", + "re-encrypt-key": "Mã hóa lại khóa của bạn", + "receive": "Nhận được", + "receive-transaction": "Nhận giao dịch", + "recent": "Gần đây", + "recent-empty": "Các địa chỉ được sử dụng gần đây sẽ xuất hiện ở đây", + "recent-recipients": "Liên lạc", + "recently-used-stickers": "Các nhãn dán được sử dụng gần đây sẽ xuất hiện ở đây", + "recipient": "Người nhận", + "recipient-code": "Nhập địa chỉ người nhận", + "recipient-code-placeholder": "0x ... hoặc username.domain.eth", + "recover": "Bình phục", + "recover-key": "Truy cập các khóa hiện có", + "recover-keycard-multiaccount-not-supported": "Các khóa cho tài khoản này đã tồn tại và không thể thêm lại. Nếu bạn bị mất mật khẩu, mật mã hoặc Thẻ khóa, hãy gỡ cài đặt ứng dụng, cài đặt lại và truy cập các khóa của bạn bằng cách nhập cụm từ hạt giống của bạn", + "recover-with-keycard": "Phục hồi bằng thẻ khóa", + "recovering-key": "Đang truy cập các phím ...", + "recovery-confirm-phrase": "Xác nhận cụm từ hạt giống", + "recovery-phrase": "Cụm từ hạt giống", + "recovery-success-text": "Bạn sẽ phải tạo mã hoặc mật khẩu mới để mã hóa lại khóa của mình", + "recovery-typo-dialog-description": "Xin lưu ý, cụm từ hạt giống của bạn phải sử dụng chính xác các từ và thứ tự như bạn nhận được", + "recovery-typo-dialog-title": "Cụm từ hạt giống có đúng không?", + "redeem-amount": "{{quantity}} có sẵn", + "redeem-now": "Đổi ngay", + "redeem-success": "Đổi tiền thưởng thành công!", + "refresh": "Làm tươi", + "registered": "đã đăng ký", + "remember-me": "Nhớ tôi", + "remind-me-later": "Cho tôi xem lại cái này", + "remove": "Tẩy", + "remove-from-chat": "Xóa khỏi trò chuyện", + "remove-from-contacts": "Xóa khỏi danh bạ", + "remove-from-contacts-text": "Bằng cách xóa một người dùng khỏi danh sách liên hệ của bạn, bạn không ẩn địa chỉ ví của mình với họ", + "remove-group": "Xóa nhóm", + "remove-network": "Xóa mạng", + "remove-token": "Xóa mã thông báo", + "removed": "loại bỏ", + "repeat-pin": "Lặp lại mật mã 6 chữ số mới", + "report-bug-email-template": "1. Mô tả vấn đề\n (Mô tả tính năng bạn muốn hoặc tóm tắt ngắn gọn về lỗi và những gì bạn đã làm, những gì bạn mong đợi sẽ xảy ra và những gì thực sự xảy ra. Các phần bên dưới) \n\n\n 2. Các bước tái tạo\n (Mô tả cách chúng tôi có thể sao chép lỗi từng bước.)\n -Tình trạng mở\n -...\n -Bước 3, v.v. \n\n\n 3. Hành vi mong đợi\n (Mô tả những gì bạn mong đợi sẽ xảy ra.) \n\n\n 4. Hành vi thực tế\n (Mô tả những gì thực sự đã xảy ra.) \n\n\n 5. Đính kèm ảnh chụp màn hình có thể demo vấn đề, vui lòng\n", + "request-feature": "Yêu cầu một tính năng", + "request-membership": "Yêu cầu thành viên", + "request-pending": "Đợi phản hồi…", + "request-transaction": "Yêu cầu giao dịch", + "required-field": "phần bắt buộc", + "resend-message": "Gửi lại", + "reset-card": "Đặt lại thẻ", + "reset-card-description": "Thao tác này sẽ đặt lại thẻ về trạng thái ban đầu. Nó sẽ xóa tất cả dữ liệu thẻ bao gồm cả khóa cá nhân. Hoạt động không thể đảo ngược.", + "retry": "Thử lại", + "revoke-access": "Thu hồi truy cập", + "rpc-url": "URL RPC", + "rpc-usage-copy": "Sao chép", + "rpc-usage-get-stats": "Làm tươi", + "save": "Tiết kiệm", + "save-password": "Lưu mật khẩu", + "save-password-unavailable": "Đặt mật mã thiết bị để lưu mật khẩu", + "save-password-unavailable-android": "Lưu mật khẩu không khả dụng: thiết bị của bạn có thể đã được root hoặc thiếu các tính năng bảo mật cần thiết.", + "scan-qr": "Quét mã QR", + "scan-qr-code": "Quét mã QR bằng địa chỉ ví", + "scan-tokens": "Quét mã thông báo", + "search": "Tìm kiếm", + "search-no-chat-found": "Không có kết quả tìm kiếm. Ý bạn là", + "secret-keys-confirmation-text": "Bạn sẽ cần họ tiếp tục sử dụng Thẻ khóa của mình trong trường hợp bạn bị mất điện thoại.", + "secret-keys-confirmation-title": "Viết mã xuống?", + "security": "Bảo vệ", + "see-details": "Xem chi tiết", + "see-it-again": "XEM LẠI", + "seed-phrase-content": "Một tập hợp các từ dễ đọc, được chọn ngẫu nhiên từ danh sách tiêu chuẩn BIP39 và được sử dụng để khôi phục hoặc truy cập tài khoản Ethereum của bạn trên các ví và thiết bị khác. Còn được gọi là “cụm từ dễ nhớ”, “cụm từ khôi phục” hoặc “sao lưu ví” trong hệ sinh thái tiền điện tử. Hầu hết các ứng dụng tiền điện tử đều sử dụng cùng một tiêu chuẩn này để tạo tài khoản.", + "seed-phrase-placeholder": "Cụm từ hạt giống ...", + "seed-phrase-title": "Cụm từ hạt giống", + "select": "Lựa chọn", + "select-account": "Chọn tài khoản", + "select-account-dapp": "Chọn tài khoản bạn muốn sử dụng với Dapps", + "select-account-first": "Chọn một tài khoản trước tiên", + "select-chat": "Chọn trò chuyện để bắt đầu nhắn tin", + "selected": "Đã chọn", + "send-logs": "Báo cáo lỗi", + "send-logs-to": "Báo cáo lỗi cho {{email}}", + "send-message": "Gửi tin nhắn", + "send-push-notifications-description": "Khi bị vô hiệu hóa, người nhận tin nhắn của bạn sẽ không được thông báo về việc họ đến", + "send-request": "Gửi yêu cầu", + "send-request-amount": "Số tiền", + "send-request-amount-max-decimals": "Số số thập phân tối đa là {{asset-decimals}}", + "send-request-unknown-token": "Mã thông báo không xác định - {{asset}}", + "send-sending-to": "tới {{recipient-name}}", + "send-transaction": "Gửi giao dịch", + "sending": "Gửi", + "sent-at": "Gửi lúc", + "server": "Người phục vụ", + "set-a-topic": "Tạo một chủ đề", + "set-currency": "Đặt tiền tệ", + "set-custom-fee": "Đặt phí tùy chỉnh", + "set-dapp-access-permissions": "Đặt quyền truy cập DApp", + "set-max": "Đặt tối đa", + "settings": "Cài đặt", + "share": "Chia sẻ", + "share-address": "Chia sẻ địa chỉ", + "share-chat": "Chia sẻ trò chuyện", + "share-contact-code": "Chia sẻ khóa trò chuyện của tôi", + "share-dapp-text": "Kiểm tra DApp này mà tôi đang sử dụng trên Trạng thái: {{link}}", + "share-link": "Chia sẻ đường link", + "share-my-profile": "Chia sẻ hồ sơ của tôi", + "share-profile": "Chia sẻ hồ sơ", + "share-profile-link": "Chia sẻ liên kết hồ sơ", + "share-public-chat-text": "Xem cuộc trò chuyện công khai này trên ứng dụng Trạng thái: {{link}}", + "shared": "Được chia sẻ", + "sharing-copied-to-clipboard": "Đã sao chép", + "sharing-copy-to-clipboard": "Sao chép", + "sharing-share": "Chia sẻ", + "show-less": "Hiện ít hơn", + "show-more": "Cho xem nhiều hơn", + "show-notifications": "Hiển thị thông báo", + "show-qr": "Hiển thị mã QR", + "show-transaction-data": "Hiển thị dữ liệu giao dịch", + "sign-and-send": "Ký và gửi", + "sign-anyway": "Vẫn ký", + "sign-in": "Mở khóa", + "sign-message": "Ký tên", + "sign-out": "Đăng xuất", + "sign-request-failed": "Không thể ký tin nhắn", + "sign-with": "Ký với", + "sign-with-password": "Đăng nhập bằng mật khẩu", + "sign-you-in": "Đang đăng nhập bạn…", + "signing": "Đang ký", + "signing-a-message": "Ký một tin nhắn", + "signing-phrase": "Ký tên", + "something-went-wrong": "Đã xảy ra sự cố", + "soon": "Sớm", + "specify-address": "Chỉ định địa chỉ", + "specify-name": "Chỉ định tên", + "specify-network-id": "Chỉ định id mạng", + "specify-rpc-url": "Chỉ định một URL RPC", + "specify-server-public-key": "Nhập khóa công khai của máy chủ", + "start-chat": "Bắt đầu trò chuyện", + "start-conversation": "Bắt đầu cuộc trò chuyện", + "start-group-chat": "Bắt đầu trò chuyện nhóm", + "start-new-chat": "Bắt đầu cuộc trò chuyện mới", + "starter-pack-received": "Gói Khởi đầu đã nhận được", + "starter-pack-received-description": "Đây là một số tiền điện tử để bạn bắt đầu! Sử dụng nó để nhận nhãn dán, tên ENS và thử dapp", + "status": "Trạng thái", + "status-confirmed": "Đã xác nhận", + "status-hardwallet": "Trạng thái cứng", + "status-keycard": "Thẻ trạng thái", + "status-not-sent-click": "Không được xác nhận. Nhấp để có các tùy chọn", + "status-not-sent-tap": "Không được xác nhận. Nhấn để có các tùy chọn", + "status-pending": "Đang chờ xử lý", + "status-sent": "Gởi", + "status-tx-not-found": "TX không tìm thấy", + "status-updates-descr": "Cập nhật trạng thái sẽ xuất hiện ở đây. Thêm hồ sơ làm địa chỉ liên hệ để nhận cập nhật trên dòng thời gian của bạn.", + "statuses-descr": "Chia sẻ những gì bạn nghĩ và cập nhật thông tin liên hệ của bạn", + "statuses-my-status-descr": "Chia sẻ những gì bạn nghĩ. Bất kỳ ai truy cập hồ sơ của bạn sẽ có thể xem trạng thái của bạn. Những người thêm bạn làm liên hệ của họ sẽ nhận được thông tin cập nhật của bạn trên dòng thời gian của họ", + "step-i-of-n": "Bước {{step}} của {{number}}", + "sticker-market": "Thị trường hình dán", + "storage": "Lưu trữ", + "submit": "Gửi đi", + "submit-bug": "Gửi lỗi", + "success": "Sự thành công", + "symbol": "Biểu tượng", + "sync-all-devices": "Đồng bộ hóa tất cả các thiết bị", + "sync-in-progress": "Đang đồng bộ hóa ...", + "sync-settings": "Cài đặt đồng bộ hóa", + "sync-synced": "Đồng bộ", + "syncing-devices": "Đang đồng bộ hóa ...", + "system": "Hệ thống", + "tag-was-lost": "Thẻ đã bị mất", + "tap-card-again": "Nhấn lại thẻ vào mặt sau điện thoại của bạn", + "test-networks": "Kiểm tra mạng", + "text-input-disabled": "Xin vui lòng chờ trong giây lát...", + "this-device": "Thiết bị này", + "this-device-desc": "Khóa của bạn sẽ được mã hóa và lưu trữ an toàn trên thiết bị của bạn", + "this-is-you-signing": "Đây là cụm từ ký của bạn", + "this-will-take-few-seconds": "Quá trình này sẽ mất vài giây", + "three-words-description": "Bạn nên xem 3 từ này trước khi ký mỗi giao dịch", + "three-words-description-2": "Nếu bạn thấy sự kết hợp khác, hãy hủy giao dịch và đăng xuất", + "timeline": "Mốc thời gian", + "to": "Đến", + "to-block": "Khối", + "to-enable-biometric": "Để bật {{bio-type-label}} , bạn phải lưu mật khẩu của mình trên màn hình mở khóa", + "to-encrypt-enter-password": "Để mã hóa tài khoản, vui lòng nhập mật khẩu của bạn", + "to-see-this-message": "Để xem thông báo này,", + "token-auto-validate-decimals-error": "Số thập phân sai cho mã thông báo {{symbol}} tại địa chỉ {{address}} - được đặt thành {{expected}} nhưng được phát hiện là {{actual}}", + "token-auto-validate-name-error": "Tên sai cho mã thông báo {{symbol}} tại địa chỉ {{address}} - được đặt thành {{expected}} nhưng được phát hiện là {{actual}}", + "token-auto-validate-symbol-error": "Ký hiệu sai cho mã thông báo {{symbol}} tại địa chỉ {{address}} - được đặt thành {{expected}} nhưng được phát hiện là {{actual}}", + "token-details": "Chi tiết mã thông báo", + "topic-name-error": "Chỉ sử dụng các chữ cái thường (a đến z), số và dấu gạch ngang (-). Không sử dụng phím trò chuyện", + "transaction": "Giao dịch", + "transaction-data": "Dữ liệu giao dịch", + "transaction-declined": "Giao dịch bị từ chối", + "transaction-description": "Coi như nó hoàn tất sau 12 lần xác nhận trên mạng.", + "transaction-details": "Chi tiết giao dịch", + "transaction-failed": "giao dịch không thành công", + "transaction-history": "Lịch sử giao dịch", + "transaction-request": "Yêu cầu giao dịch", + "transaction-sent": "Giao dịch đã được gửi", + "transaction-signed": "Giao dịch đã được ký thành công", + "transactions": "Giao dịch", + "transactions-filter-select-all": "Chọn tất cả", + "transactions-filter-title": "Lịch sử lọc", + "transactions-history": "Lịch sử giao dịch", + "transactions-history-empty": "Chưa có giao dịch nào trong lịch sử của bạn", + "transactions-history-loading": "Đang tải lịch sử giao dịch. Có lẽ sẽ mất một lúc.", + "transactions-load-more": "Tải thêm", + "transactions-sign": "Ký tên", + "tribute-required-by-multiaccount": "{{multiaccount-name}} yêu cầu SNT bắt đầu trò chuyện.", + "tribute-state-paid": "Cống đã trả", + "tribute-state-pending": "Cống hiến đang chờ xử lý", + "tribute-state-required": "Yêu cầu cống nạp SNT {{snt-amount}}", + "tribute-to-talk": "Tribute để nói chuyện", + "tribute-to-talk-add-friends": "Thêm bạn bè làm địa chỉ liên hệ để cho phép trò chuyện mà không cần trả tiền tri ân.", + "tribute-to-talk-are-you-friends": "Bạn là bạn bè?", + "tribute-to-talk-ask-to-be-added": "Yêu cầu được thêm làm địa chỉ liên hệ", + "tribute-to-talk-contact-received-your-tribute": "đã nhận được cống của bạn. Bây giờ bạn có thể trò chuyện an toàn với nhau.", + "tribute-to-talk-desc": "Kiếm tiền từ sự chú ý của bạn bằng cách yêu cầu SNT để những người mới bắt đầu trò chuyện", + "tribute-to-talk-disabled": "Tribute to Talk bị tắt", + "tribute-to-talk-disabled-note": "Từ bây giờ, những người mới có thể bắt đầu trò chuyện với bạn mà không cần gửi SNT.", + "tribute-to-talk-enabled": "Bạn đã bật Tribute to Talk.", + "tribute-to-talk-finish-desc": "Từ bây giờ, bạn sẽ chỉ nhận được các cuộc trò chuyện từ các liên hệ và những người đã trả tiền", + "tribute-to-talk-learn-more-1": "Thời gian và sự chú ý của bạn là tài sản quý giá nhất của bạn. Tribute to Talk cho phép bạn đặt số lượng SNT cần thiết để những người mới bắt đầu trò chuyện với bạn.", + "tribute-to-talk-learn-more-2": "Bất kỳ ai không có trong danh sách liên hệ của bạn sẽ được yêu cầu thanh toán và bạn có thể trả lời khi họ có.", + "tribute-to-talk-learn-more-3": "Bạn luôn có thể gửi lại tiền, nhưng để đảm bảo rằng bạn bè có thể liên hệ với bạn một cách thoải mái, hãy thêm họ làm địa chỉ liên hệ trước.", + "tribute-to-talk-paywall-learn-more-1": "Thời gian và sự chú ý của chúng tôi là tài sản quý giá nhất của chúng tôi. Tribute to Talk cho phép bạn liên hệ với những người mới để đổi lấy khoản thanh toán SNT.", + "tribute-to-talk-paywall-learn-more-2": "Để bắt đầu trò chuyện với một người nào đó đã đặt dấu ấn, chỉ cần thanh toán SNT được yêu cầu và bạn sẽ được thêm làm địa chỉ liên hệ.", + "tribute-to-talk-paywall-learn-more-3": "Nếu bạn biết họ, bạn có thể chia sẻ hồ sơ của mình bên ngoài Trạng thái để được thêm miễn phí.", + "tribute-to-talk-pending": "Xác nhận cống đang chờ xử lý", + "tribute-to-talk-pending-note": "Giao dịch cống đang chờ xác nhận trên mạng. Bạn có thể kiểm tra trạng thái của nó trong lịch sử giao dịch", + "tribute-to-talk-removing-note": "Xóa Tribute to Talk sẽ cho phép những người mới bắt đầu trò chuyện mà không cần gửi SNT. Yêu cầu một giao dịch được thực hiện.", + "tribute-to-talk-set-snt-amount": "Đặt số lượng SNT cần thiết để những người mới bắt đầu trò chuyện", + "tribute-to-talk-signing": "Đang chờ ký giao dịch", + "tribute-to-talk-transaction-failed-note": "Giao dịch không thành công và cài đặt Tribute to Talk của bạn không được thay đổi", + "tribute-to-talk-tribute-received1": "Đã nhận được cống hiến. Bạn và", + "tribute-to-talk-tribute-received2": "hiện là các địa chỉ liên hệ và có thể trò chuyện an toàn với nhau.", + "tribute-to-talk-you-require-snt": "Bạn yêu cầu SNT để những người mới bắt đầu trò chuyện.", + "try-again": "Thử lại", + "try-keeping-the-card-still": "Thử giữ yên thẻ", + "turn-nfc-description": "NFC bị tắt trên thiết bị của bạn. Bạn có thể bật nó trong cài đặt", + "turn-nfc-on": "Bật NFC để tiếp tục", + "tx-fail-description1": "Giao dịch này có khả năng thất bại. Tự chịu rủi ro khi đăng ký bằng cách sử dụng phí mạng tùy chỉnh.", + "tx-fail-description2": "Giao dịch này có khả năng thất bại. Đặt phí mạng tùy chỉnh để bạn tự chịu rủi ro khi đăng ký.", + "type": "Kiểu", + "type-a-message": "Thông điệp", + "ulc-enabled": "ULC được bật", + "unable-to-read-this-code": "Không thể đọc mã này", + "unblock": "Mở khóa", + "unblock-contact": "Bỏ chặn người dùng này", + "unknown-status-go-error": "Lỗi trạng thái không xác định", + "unlock": "Mở khóa", + "unmute": "Bật tiếng", + "unpair-card": "Hủy ghép nối thẻ", + "unpair-card-confirmation": "Thao tác này sẽ hủy ghép nối thẻ khỏi thiết bị hiện tại. Yêu cầu ủy quyền mật mã 6 chữ số. Bạn có muốn tiếp tục?", + "unpaired-keycard-text": "Thẻ khóa bạn đã nhấn không được liên kết với điện thoại này", + "unpaired-keycard-title": "Có vẻ như thẻ của bạn chưa được ghép nối", + "update": "Cập nhật", + "update-to-listen-audio": "Cập nhật phiên bản mới nhất để nghe tin nhắn âm thanh tại đây!", + "update-to-see-image": "Cập nhật phiên bản mới nhất để xem hình ảnh đẹp tại đây!", + "update-to-see-sticker": "Cập nhật phiên bản mới nhất để xem nhãn dán đẹp tại đây!", + "url": "URL", + "usd-currency": "đô la Mỹ", + "use-valid-contact-code": "Vui lòng nhập hoặc quét một khóa trò chuyện hoặc tên người dùng hợp lệ", + "validation-amount-invalid-number": "Số tiền không phải là số hợp lệ", + "validation-amount-is-too-precise": "Số tiền quá chính xác. Số phần thập phân tối đa là {{decimals}} .", + "version": "Phiên bản ứng dụng", + "view-cryptokitties": "Xem trong CryptoKitties", + "view-cryptostrikers": "Xem trong CryptoStrikers", + "view-details": "Xem chi tiết", + "view-etheremon": "Xem trong Etheremon", + "view-gitcoin": "Xem trong Gitcoin", + "view-profile": "Xem hồ sơ", + "view-signing": "Xem cụm từ ký kết", + "view-superrare": "Xem trong SuperRare", + "waiting-for-wifi": "Không có Wi-Fi, đồng bộ hóa tin nhắn bị tắt.", + "waiting-for-wifi-change": "Cài đặt", + "waiting-to-sign": "Đang chờ ký giao dịch ...", + "waku-bloom-filter-mode": "Chế độ bộ lọc nở Waku", + "wallet": "Ví tiền", + "wallet-address": "Địa chỉ ví", + "wallet-asset": "Tài sản", + "wallet-assets": "Tài sản", + "wallet-backup-recovery-title": "Sao lưu cụm từ hạt giống của bạn", + "wallet-choose-recipient": "Chọn người nhận", + "wallet-collectibles": "Sưu tầm", + "wallet-insufficient-funds": "Không đủ tiền", + "wallet-insufficient-gas": "Không đủ ETH để đổ xăng", + "wallet-invalid-address": "Địa chỉ không hợp lệ:\n {{data}}", + "wallet-invalid-address-checksum": "Lỗi địa chỉ:\n {{data}}", + "wallet-invalid-chain-id": "Mạng không khớp:\n {{data}} nhưng chuỗi hiện tại là {{chain}}", + "wallet-key-content": "Địa chỉ hex 64 ký tự dựa trên tiêu chuẩn Ethereum và bắt đầu bằng 0x. Công khai, địa chỉ tài khoản của bạn được chia sẻ với những người khác khi bạn muốn nhận tiền. Còn được gọi là “địa chỉ Ethereum” hoặc “địa chỉ ví”.", + "wallet-key-title": "Địa chỉ tài khoản", + "wallet-manage-assets": "Quản lý tài sản", + "wallet-request": "Yêu cầu", + "wallet-send": "Gửi", + "wallet-send-min-units": "Tối thiểu 21000 đơn vị", + "wallet-send-min-wei": "Min 1 wei", + "wallet-settings": "Cài đặt ví", + "wallet-total-value": "Tổng giá trị", + "wallet-transaction-total-fee": "Tổng phí", + "wants-to-access-profile": "muốn truy cập vào hồ sơ của bạn", + "warning": "Cảnh báo", + "warning-message": "Xin lỗi, chúng tôi hạn chế gửi nhiều thư liên tiếp để ngăn chặn thư rác. Vui lòng thử lại sau giây lát", + "warning-sending-to-contract-descr": "Địa chỉ bạn đã nhập là hợp đồng thông minh, việc gửi tiền đến địa chỉ này có thể dẫn đến mất tiền. Để tương tác với DApp, hãy mở DApp trong Trình duyệt DApp Trạng thái.", + "watch-only": "Chỉ xem", + "web-view-error": "Không thể tải trang", + "websites": "Trang web", + "webview-camera-permission-requests": "Yêu cầu quyền đối với camera xem qua web", + "webview-camera-permission-requests-subtitle": "Khi được bật, các trang web và dapp có thể yêu cầu sử dụng máy ảnh của bạn", + "welcome-blank-message": "Các cuộc trò chuyện của bạn sẽ xuất hiện ở đây. Để bắt đầu cuộc trò chuyện mới, nhấn nút ⊕", + "welcome-screen-text": "Thiết lập ví của bạn, mời bạn bè trò chuyện\n và duyệt qua các dapp phổ biến!", + "welcome-to-status": "Chào mừng đến với Trạng thái!", + "welcome-to-status-description": "Thiết lập ví tiền điện tử của bạn, mời bạn bè trò chuyện và duyệt qua các ứng dụng phi tập trung", + "whats-on-your-mind": "Bạn đang nghĩ gì vậy…", + "word-count": "Số từ", + "word-n": "Từ # {{number}}", + "word-n-description": "Để kiểm tra xem bạn đã sao lưu đúng cụm từ hạt giống của mình chưa, hãy nhập từ # {{number}} ở trên.", + "words-n": { + "other": "{{count}} từ" + }, + "write-down-and-store-securely": "Viết mã xuống\n & lưu trữ chúng một cách an toàn", + "wrong-address": "Sai địa chỉ", + "wrong-card": "Thẻ sai", + "wrong-card-text": "Thẻ đã nhấn không tương ứng với các phím bạn đã chọn", + "wrong-contract": "Hợp đồng sai", + "wrong-keycard-text": "Thẻ khóa bạn đã nhấn không được liên kết với điện thoại này", + "wrong-keycard-title": "Có vẻ như bạn đã nhấn vào\n một thẻ khóa sai", + "wrong-password": "Sai mật khẩu", + "wrong-word": "Từ sai", + "yes": "Đúng", + "you": "bạn", + "you-already-have-an-asset": "Bạn đã có một tài sản {{value}}", + "you-are-all-set": "Bạn đã sẵn sàng!", + "you-are-all-set-description": "Nếu bạn bị mất điện thoại, bây giờ bạn có thể truy cập vào tiền của mình và khóa trò chuyện bằng cách sử dụng cụm từ hạt giống của bạn", + "you-can-change-account": "Bạn có thể thay đổi tên tài khoản và màu sắc theo ý muốn", + "you-can-choose-preview-websites": "Bạn có thể chọn trang web nào sau đây có thể xem trước liên kết mô tả và hình ảnh trong cuộc trò chuyện", + "you-dont-have-contacts": "Bạn chưa có bất kỳ liên hệ nào.", + "you-dont-have-stickers": "Bạn chưa có bất kỳ nhãn dán nào", + "you-will-need-this-code": "Bạn sẽ cần mã này để mở Trạng thái và ký giao dịch", + "you-will-start-from-scratch": "Bạn sẽ bắt đầu lại từ đầu với một bộ phím mới", + "your-card-is-frozen": "Thẻ Keycard của bạn bị đóng băng. Đặt lại quyền truy cập thẻ", + "your-contact-code": "Việc cấp quyền truy cập cho phép DApp này truy xuất khóa trò chuyện của bạn", + "your-data-belongs-to-you": "Nếu bạn mất cụm từ hạt giống, bạn sẽ mất dữ liệu và tiền của mình", + "your-data-belongs-to-you-description": "Nếu bạn mất quyền truy cập, chẳng hạn như do mất điện thoại, bạn chỉ có thể truy cập các khóa của mình bằng cụm từ hạt giống. Không ai, nhưng bạn có cụm từ hạt giống của bạn. Viết nó ra. Giữ nó an toàn", + "your-keys": "Chìa khóa của bạn", + "your-recovery-phrase": "Cụm từ hạt giống của bạn", + "your-recovery-phrase-description": "Đây là cụm từ hạt giống của bạn. Bạn sử dụng nó để chứng minh rằng đây là ví của bạn. Bạn chỉ có thể xem nó một lần! Viết nó trên giấy và giữ nó ở một nơi an toàn. Bạn sẽ cần nó nếu bạn bị mất hoặc cài đặt lại ví của mình." +} diff --git a/translations/zh.json b/translations/zh.json new file mode 100644 index 00000000000..a12250a5710 --- /dev/null +++ b/translations/zh.json @@ -0,0 +1,1841 @@ +{ + "K": "K", + "M": "M", + "NFT": "NFT", + "You": "您", + "about-app": "关于", + "about-key-storage-content": "Status永远不会访问您的密钥。请务必备份您的助记词。如果您丢失了手机,这是找回密钥的唯一方法。", + "about-key-storage-title": "关于密钥存储", + "about-names-content": "没有人可以冒充您!默认情况下,您是匿名的,永远不必透露您的真实姓名。您只需支付少量费用即可注册自定义名称。", + "about-names-title": "名称无法更改", + "about-sharing-data": "关于共享数据", + "accept": "接受", + "accept-and-add": "接受并添加", + "accept-and-continue": "接受并继续", + "accept-and-share-address": "同意并分享地址", + "accept-community-rules": "我同意社区规则", + "accept-new-chats-from": "接受新聊天,来自", + "accept-status-tos-prefix": "我接受Status", + "accepted": "已接受", + "access-existing-keys": "访问现有密钥", + "access-key": "访问密钥", + "account-added": "帐户已添加", + "account-color": "帐户颜色", + "account-content": "您可以将Status中的帐户比作银行帐户。像银行帐户一样,Status帐户通常具有地址和余额;您使用此帐户在以太坊上进行交易。您的钱包中可以有多个帐户。通过解锁Status可访问所有帐户。", + "account-exists-title": "帐户已存在", + "account-is-used": "该账户连接到浏览器中的Dapp。", + "account-name": "帐户名称", + "account-settings": "帐户设置", + "account-title": "帐户", + "accounts": "帐户", + "actions": "操作", + "active-members": "活跃成员", + "active-online": "在线", + "active-unknown": "未知", + "activity": "通知", + "add": "添加", + "add-a-watch-account": "添加仅限观察的地址", + "add-account": "添加帐户", + "add-account-description": "您可以导入任何类型的以太坊帐户,将其添加到您的Status钱包", + "add-account-incorrect-password": "密码似乎不正确。请输入您用于解锁应用程序的密码。", + "add-an-account": "添加帐户", + "add-bootnode": "添加引导节点", + "add-contact": "添加联系人", + "add-custom-token": "添加自定义代币", + "add-favourite": "添加到收藏夹", + "add-mailserver": "添加Status节点", + "add-members": "添加成员", + "add-network": "添加网络", + "add-new-contact": "添加新联系人", + "add-nickname": "添加昵称(可选)", + "add-node": "添加节点", + "add-private-key-account": "通过私钥添加帐户", + "add-seed-account": "使用助记词添加帐户", + "add-to-contacts": "添加到联系人", + "add-to-contacts-text": "将用户添加到您的联系人列表,您即在向他们分享您的钱包地址", + "add-to-favourites": "添加到收藏夹", + "add-watch-account": "添加仅限观察的帐户", + "added": "添加", + "address": "地址", + "address-or-ens-name": "地址或ENS名称", + "address-received": "已收到地址", + "address-request-accepted": "已接受地址请求", + "address-requested": "已请求地址", + "admin": "管理员", + "advanced": "高级", + "advanced-settings": "高级设置", + "advertiser-description": "感谢您的好友将Status推荐给您。您是否介意Status查看一次您目前的IP地址,以便获得奖励?此信息将不会再用于其他任何用途,并且会在7天后完全删除。", + "advertiser-starter-pack-accept": "接受", + "advertiser-starter-pack-decline": "拒绝", + "advertiser-starter-pack-description": "这是一些加密货币,可以帮助您入门!用其来获取贴纸、ENS名称并尝试DApp", + "advertiser-starter-pack-title": "入门包", + "advertiser-title": "默认为私密", + "agree-by-continuing": "通过继续,即表明您同意我们的", + "all": "全部", + "all-connections": "所有连接", + "allow": "允许", + "allow-and-send": "允许并发送", + "allow-mention-notifications": "显示@提及", + "allowing-authorizes-this-dapp": "允许授权此DApp以检索您的钱包地址, 并启用Web3", + "alphabetically": "按字母顺序排列", + "already-have-asset": "您已有该资产", + "amount": "金额", + "and": "和", + "anyone": "任何人", + "app-commit": "应用程序提交", + "app-connections": "应用程序连接", + "appearance": "显示", + "apply": "应用", + "approve": "批准", + "approve-limit": "批准限额", + "approve-token": "批准代币", + "approve-token-contract-desc": "通过合约批准代币允许其花费您的代币余额。如果您认为某个项目不值得信任,请不要选择批准代币,或者只批准您需要使用到的金额额度。", + "are-you-sure": "确定吗?", + "are-you-sure-description": "您将无法再次查看整个助记词", + "are-you-sure-to-cancel": "确定要取消?", + "are-you-sure?": "确定吗?", + "ask-in-status": "咨询问题或报告错误", + "at": "在", + "attribution-received": "收到{{attrib}}奖励,上限为{{max}}", + "audio": "音频", + "audio-message": "音频消息", + "audio-recorder": "录音器", + "audio-recorder-error": "录音器错误", + "audio-recorder-max-ms-reached": "达到最长录音时间", + "audio-recorder-permissions-error": "您必须授予发送音频消息的权限", + "authorize": "授权", + "available": "可用", + "available-participants": { + "other": "您可以再选择{{count}}个参与者。" + }, + "back": "返回", + "back-up": "备份", + "back-up-seed-phrase": "备份助记词", + "back-up-your-seed-phrase": "备份您的助记词", + "backing-up": "正在备份...", + "backup-disabled": "已禁用", + "backup-enabled": "启用", + "backup-recovery-phrase": "备份助记词", + "backup-settings": "备份设置", + "backup-through-waku": "通过 waku 备份", + "bad-fees-description": "您的首选费用低于我们建议的数值。", + "balance": "余额", + "begin-set-up": "开始设置", + "below-base-fee": "最高费用低于基本费用", + "biometric-auth-android-sensor-desc": "触摸传感器", + "biometric-auth-android-sensor-error-desc": "失败", + "biometric-auth-android-title": "需要进行身份验证", + "biometric-auth-confirm-logout": "重新登录", + "biometric-auth-confirm-message": "要继续请先进行生物识别身份验证,如果不能,请使用密码或通行码解锁密钥", + "biometric-auth-confirm-title": "您必须验证!", + "biometric-auth-confirm-try-again": "再试一次", + "biometric-auth-error": "无法执行生物识别认证 ( {{code}} )", + "biometric-auth-login-error-title": "生物识别身份验证错误", + "biometric-auth-login-ios-fallback-label": "输入密码", + "biometric-auth-reason-login": "登录Status", + "biometric-auth-reason-verify": "验证身份", + "biometric-disable-bioauth": "禁用{{bio-type-label}}", + "biometric-disable-password-description": "如果禁用此功能,您还将", + "biometric-disable-password-title": "禁用密码保存", + "biometric-enable": "如果您不想访问本应用时每次都输入密码,请启用{{bio-type-label}}登录", + "biometric-enable-button": "启用{{bio-type-label}}", + "biometric-enable-keycard": "如果您不想每次都使用Keycard来访问应用程序,请启用{{bio-type-label}}登录", + "biometric-faceid": "脸部ID", + "biometric-fingerprint": "指纹", + "biometric-secure-with": "使用{{bio-type-label}}进行安全保护", + "biometric-touchid": "触控ID", + "bip39-password-placeholder": "BIP39密码", + "blank-keycard-text": "生成密钥和名称后,您可以继续使用Keycard", + "blank-keycard-title": "好像您刷的是一张空白的Keycard", + "blank-messages-text": "您的消息将在这里", + "block": "屏蔽", + "block-contact": "屏蔽此用户", + "block-contact-details": "屏蔽将删除此用户之前的所有消息,并拒收新消息", + "block-user": "屏蔽用户", + "blocked-users": "已屏蔽的用户", + "bold": "粗体", + "bootnode-address": "引导节点地址", + "bootnode-details": "引导节点详细信息", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "引导节点", + "bootnodes-enabled": "引导节点启用", + "bootnodes-settings": "引导节点设置", + "browsed-websites": "浏览过的网站将在此处显示。", + "browser": "浏览器", + "browser-not-secure": "连接不安全!不要在本网站上签名交易或发送个人数据。", + "browser-secure": "连接是安全的。在签署交易或输入个人数据之前, 请确保您确实信任此网站。", + "browsers": "浏览器", + "browsing-cancel": "取消", + "browsing-open-in-android-web-browser": "在Android中打开", + "browsing-open-in-ios-web-browser": "在iOS中打开", + "browsing-open-in-status": "在Status中打开", + "browsing-site-blocked-description1": "我们检测到此地址有潜在的恶意行为。为了保护您和您的钱包,我们阻止了进一步的浏览。 \n\n如果您认为这是一个错误,请通过以下方式告诉我们:", + "browsing-site-blocked-description2": " 公共聊天。", + "browsing-site-blocked-go-back": "返回", + "browsing-site-blocked-title": "此站点被屏蔽", + "browsing-title": "浏览", + "bug-report": "报告错误", + "bug-report-description": "* 描述", + "bug-report-description-placeholder": "必填,不能为空", + "bug-report-steps": "问题重现步骤", + "bug-report-steps-placeholder": "- 打开应用程序\n- 先做了什么操作\n- 然后做了什么操作...", + "bug-report-submit-email": "通过电子邮件提交日志存档", + "bug-report-submit-gh-issue": "提交一个没有日志的GitHub的漏洞", + "bug-report-too-short-description": "描述太短", + "build-yourself": "如果您不同意本使用条款,你可以构建自己的应用程序版本", + "buy-crypto": "购买加密货币", + "buy-crypto-choose-a-service": "选择您想用来购买加密货币的服务", + "buy-crypto-description": "查找能够购买加密货币的DApp", + "buy-crypto-leaving": "您将离开Status并进入第三方网站以完成购买", + "buy-crypto-title": "您的钱包似乎是空的", + "camera-access-error": "要授予所需的相机许可,请转到系统设置,并确保选中 “Status” > “相机”。", + "can-not-add-yourself": "这是您自己,选择一个人开始聊天", + "can-send-messages": "您可以发送和接收新消息", + "cancel": "取消", + "cancel-keycard-setup": "取消Keycard设置", + "cancelling": "取消", + "cannot-pin-desc": "您最多只能置顶 3 条消息。\n至少取消置顶一个消息以置顶其他新消息。", + "cannot-pin-title": "您无法置顶此消息!", + "cannot-read-card": "无法读卡。\n请将卡贴紧您的手机背面", + "cannot-use-default-pin": "不允许密码为000000。 \n请使用其他数字", + "cant-open-public-chat": "无法开启公共聊天", + "cant-report-bug": "无法报告错误", + "card-is-blank": "这是一张空白卡", + "card-reseted": "卡已被重置", + "card-unpaired": "卡与当前设备的配对已取消", + "category": "类别", + "category-title": "类别标题", + "change-fleet": "变更组别到{{fleet}}", + "change-group-privacy": "更改群组隐私", + "change-logging-enabled": "确定要{{enable}}日志记录吗?", + "change-pairing": "更改配对码", + "change-pairing-description": "更改配对码不会影响当前的配对。但是,任何新的配对都将需要新的配对码。", + "change-pairing-title": "创建一个新的配对码", + "change-passcode": "更改密码", + "change-password": "更改密码", + "change-pin": "更改6位数字密码", + "change-puk": "更改12位PUK码", + "change-tip": "变更手续费", + "changed-amount-warning": "金额已从{{old}}更改为{{new}}", + "changed-asset-warning": "资产已从{{old}}更改为{{new}}", + "chaos-mode": "混乱模式", + "chaos-unicorn-day": "Chaos Unicorn Day", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "聊天", + "chat-and-transact": "与朋友开启隐私聊天和交易", + "chat-is-a-contact": "联系人", + "chat-is-not-a-contact": "非联系人", + "chat-key": "聊天码", + "chat-key-content": "在Status聊天协议中使用加密密钥发送和接收消息。公共聊天码是您与他人分享的一串字符,在Status中用于其他人向您发送消息。", + "chat-key-title": "聊天码", + "chat-link-previews": "聊天中的链接预览", + "chat-name": "聊天名称", + "chat-name-content": "三个随机的词,通过算法从您的聊天码中派生,并用作您在聊天中的默认别名。聊天名称是独一无二的;没有任何其他用户可以拥有相同的三个词。", + "chat-name-title": "聊天名称", + "chat-notification-preferences": "通知设置", + "chat-settings": "聊天设置", + "chats": "聊天", + "check-on-block-explorer": "在区块浏览器中确认", + "check-on-opensea": "在OpenSea中查看", + "check-your-recovery-phrase": "查看您的助记词", + "choose-actions": "选择操作", + "choose-authentication-method": "选择身份验证方式", + "choose-new-location-for-keystore": "选择一个新位置以保存您的密钥存储文件", + "choose-storage": "选择存储位置", + "clear": "清空", + "clear-all": "全部清除", + "clear-history": "清空历史记录", + "clear-history-action": "清空", + "clear-history-confirmation": "清空历史记录?", + "clear-history-confirmation-content": "确定要清除此聊天记录?", + "clear-history-title": "清空历史记录?", + "cleared-chat-description-public": "太安静了,开始交谈或者.", + "close": "关闭", + "close-all": "全部关闭", + "close-app-button": "确认", + "close-app-content": "应用将停止并关闭。再次打开时,将使用选中的网络", + "close-app-title": "警告!", + "collectibles-leak-metadata": "您可以在此处显示您的 NFT。如果这样做,您将共享您的钱包和 IP 地址", + "command-button-send": "发送", + "communities": "社区", + "communities-alpha": "社区(alpha)", + "communities-enabled": "启用社区功能", + "communities-verified": "✓已验证的Status社区", + "community": "社区", + "community-channel": "社区频道", + "community-color": "社区颜色", + "community-color-placeholder": "选择颜色", + "community-edit-title": "编辑社区", + "community-emoji-thumbnail-title": "缩略图", + "community-image-pick": "选择图片", + "community-image-remove": "删除", + "community-image-take": "拍照", + "community-info": "社区信息", + "community-info-not-found": "未找到社区信息", + "community-invite-title": "邀请", + "community-key": "社区私钥", + "community-key-placeholder": "输入您的社区私钥", + "community-link": "社区链接", + "community-members": { + "other": "{{count}}位成员" + }, + "community-members-title": "成员", + "community-message-preview": "邀请加入{{community-name}}", + "community-notification-settings": "通知设置", + "community-private-key": "社区私钥", + "community-requests-to-join-title": "加入申请", + "community-roles": "角色", + "community-rules": "社区规则", + "community-share-title": "分享", + "community-thumbnail-image": "缩略图", + "community-thumbnail-upload": "上传", + "complete-hardwallet-setup": "该卡现在已连接。您需要它来签署交易并解锁密钥", + "completed": "已完成", + "confirm": "确认", + "confirm-new-password": "确认新密码", + "confirm-pairing-code-placeholder": "确认您的配对码...", + "confirm-password-placeholder": "确认您的密码...", + "confirmation-request": "确认请求", + "confirmations": "确认", + "confirmations-helper-text": "当交易达到 12 个确认时,您可以认为交易已成功。", + "connect": "连接", + "connect-mailserver-content": "连接到{{name}} ?", + "connect-wallet": "连接钱包", + "connected": "已连接", + "connected-to": "连接到", + "connecting": "正在连接...", + "connecting-requires-login": "连接其它网络需要登录", + "connection-request": "连接请求", + "connection-status": "连接状态", + "connection-with-the-card-lost": "与卡的连接\n 已丢失", + "connection-with-the-card-lost-setup-text": "要恢复设置,请将卡放到\n手机背面并保持\n卡与手机接触", + "connection-with-the-card-lost-text": "要继续,请将卡片贴近手机背面", + "contact-code": "聊天码", + "contact-request": "请求加为好友", + "contact-request-accepted": "已接受 ✓", + "contact-request-declined": "已拒绝 (x)", + "contact-request-header": "请求加为好友", + "contact-request-pending": "待定...", + "contact-request-sent": "已发送好友申请", + "contact-requests": "好友请求", + "contact-s": { + "other": "联系人" + }, + "contacts": "联系人", + "contacts-descr": "您的联系人将显示在此处。您将收到添加为联系人的任何人的状态更新", + "contacts-empty": "具有ENS名称的联系人将显示在此处", + "continue": "继续", + "continue-anyway": "仍然继续", + "contract-address": "合约地址", + "contract-interaction": "合约交互", + "contract-isnt-supported": "不支持合约", + "copy-info": "复制信息", + "copy-qr": "复制代码", + "copy-text": "复制文本", + "copy-to-clipboard": "复制", + "copy-transaction-hash": "复制交易ID", + "cost-fee": "成本\/费用", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "创建", + "create-a-pin": "创建6位数字密码", + "create-a-puk": "创建一个12位的PUK码", + "create-category": "创建分类", + "create-channel": "创建频道", + "create-channel-title": "新频道", + "create-community": "创建社区", + "create-group-chat": "创建群聊", + "create-multiaccount": "生成密钥", + "create-new-key": "获取新密钥", + "create-pin": "创建6位数字密码", + "create-pin-description": "您需要卡和6位数字密码才能解锁Status并确认交易", + "created-group-chat-description": "您创建了群{{group-name}}", + "cryptokitty-name": "CryptoKitty# {{id}}", + "currency": "货币", + "currency-display-name-aed": "阿联酋迪拉姆", + "currency-display-name-afn": "阿富汗尼", + "currency-display-name-ars": "阿根廷比索", + "currency-display-name-aud": "澳元", + "currency-display-name-bbd": "巴巴多斯元", + "currency-display-name-bdt": "孟加拉国塔卡", + "currency-display-name-bgn": "保加利亚列弗", + "currency-display-name-bhd": "巴林第纳尔", + "currency-display-name-bnd": "文莱元", + "currency-display-name-bob": "玻利维亚玻利维亚诺", + "currency-display-name-brl": "巴西雷亚尔", + "currency-display-name-btn": "不丹努扎姆", + "currency-display-name-cad": "加拿大元", + "currency-display-name-chf": "瑞士法郎", + "currency-display-name-clp": "智利比索", + "currency-display-name-cny": "中国人民币", + "currency-display-name-cop": "哥伦比亚比索", + "currency-display-name-crc": "哥斯达黎加科郎", + "currency-display-name-czk": "捷克克朗", + "currency-display-name-dkk": "丹麦克朗", + "currency-display-name-dop": "多米尼加共和国比索", + "currency-display-name-egp": "埃及镑", + "currency-display-name-etb": "埃塞俄比亚比尔", + "currency-display-name-eur": "欧元", + "currency-display-name-gbp": "英镑", + "currency-display-name-gel": "格鲁吉亚拉里", + "currency-display-name-ghs": "加纳塞地", + "currency-display-name-hkd": "港币", + "currency-display-name-hrk": "克罗地亚库纳", + "currency-display-name-huf": "匈牙利福林", + "currency-display-name-idr": "印度尼西亚卢比", + "currency-display-name-ils": "以色列谢克尔", + "currency-display-name-inr": "印度卢比", + "currency-display-name-isk": "冰岛克朗", + "currency-display-name-jmd": "牙买加元", + "currency-display-name-jpy": "日元", + "currency-display-name-kes": "肯尼亚先令", + "currency-display-name-krw": "韩元", + "currency-display-name-kwd": "科威特第纳尔", + "currency-display-name-kzt": "哈萨克斯坦腾格", + "currency-display-name-lkr": "斯里兰卡卢比", + "currency-display-name-mad": "摩洛哥迪拉姆", + "currency-display-name-mdl": "摩尔多瓦列伊", + "currency-display-name-mur": "毛里求斯卢比", + "currency-display-name-mwk": "马拉维克瓦查", + "currency-display-name-mxn": "墨西哥比索", + "currency-display-name-myr": "马来西亚林吉特", + "currency-display-name-mzn": "莫桑比克梅蒂卡尔", + "currency-display-name-nad": "纳米比亚元", + "currency-display-name-ngn": "尼日利亚奈拉", + "currency-display-name-nok": "挪威克朗", + "currency-display-name-npr": "尼泊尔卢比", + "currency-display-name-nzd": "新西兰元", + "currency-display-name-omr": "阿曼里亚尔", + "currency-display-name-pen": "秘鲁索尔", + "currency-display-name-pgk": "巴布亚新几内亚基那", + "currency-display-name-php": "菲律宾比索", + "currency-display-name-pkr": "巴基斯坦卢比", + "currency-display-name-pln": "波兰兹罗提", + "currency-display-name-pyg": "巴拉圭瓜拉尼", + "currency-display-name-qar": "卡塔尔里亚尔", + "currency-display-name-ron": "罗马尼亚列伊", + "currency-display-name-rsd": "突尼斯第纳尔", + "currency-display-name-rub": "俄罗斯卢布", + "currency-display-name-sar": "沙特阿拉伯里亚尔", + "currency-display-name-sek": "瑞典克朗", + "currency-display-name-sgd": "新加坡元", + "currency-display-name-thb": "泰国泰铢", + "currency-display-name-try": "土耳其里拉", + "currency-display-name-ttd": "特立尼达和多巴哥元", + "currency-display-name-twd": "新台币", + "currency-display-name-tzs": "坦桑尼亚先令", + "currency-display-name-uah": "乌克兰格里夫尼亚", + "currency-display-name-ugx": "乌干达先令", + "currency-display-name-usd": "美元", + "currency-display-name-uyu": "乌拉圭比索", + "currency-display-name-vef": "委内瑞拉博利瓦", + "currency-display-name-vnd": "越南盾", + "currency-display-name-zar": "南非兰特", + "current": "当前", + "current-average": "目前的平均值", + "current-average-tip": "当前平均费用", + "current-base": "目前的基础", + "current-base-fee": "当前基本费用", + "current-minimum-tip": "当前最低费用", + "current-network": "当前网络", + "current-password": "当前密码", + "current-pin": "输入6位数字密码", + "current-pin-description": "输入您的6位数字密码以继续", + "custom": "自定义", + "custom-networks": "自定义网络", + "custom-node": "您正在使用自定义RPC端点。您的本地交易记录可能不完整。", + "custom-seed-phrase": "助记词无效", + "custom-seed-phrase-text-1": "该助记词与我们支持的词库不匹配。请检查是否存在拼写错误。", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "接受并打开", + "dapp-starter-pack-description": "这是一些加密货币,可以帮助您入门!用其来获取贴纸、ENS名称并尝试DApp", + "dapp-starter-pack-title": "入门包", + "dapp-would-like-to-connect-wallet": "想要连接到", + "dapps": "ÐApp", + "dapps-permissions": "DApp权限", + "dark": "夜晚模式", + "data": "数据", + "data-collected": "收集的数据", + "data-collected-subtitle": "下表显示了存储和将发送的确切数据。数据根据公共规则进行验证,以确保不会发送敏感数据。应随时保持怀疑,并进行验证。", + "data-syncing": "数据同步中", + "database-reset-content": "已删除聊天记录、联系人和设置。您的帐户可与Keycard配合使用", + "database-reset-title": "数据库重置", + "database-reset-warning": "数据库将被重置。聊天记录、联系人和设置将被删除", + "datetime-ago": "之前", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-ago-format-short": "{{number}} {{time-intervals}}", + "datetime-day": { + "other": "天" + }, + "datetime-day-short": { + "other": "天" + }, + "datetime-hour": { + "other": "小时" + }, + "datetime-hour-short": { + "other": "小时" + }, + "datetime-minute": { + "other": "分钟" + }, + "datetime-minute-short": { + "other": "分" + }, + "datetime-second": { + "other": "秒" + }, + "datetime-second-short": { + "other": "秒" + }, + "datetime-today": "今天", + "datetime-yesterday": "昨天", + "decimals": "小数点", + "decline": "拒绝", + "declined": "拒绝", + "decryption-failed-content": "解密数据时发生错误。您可能需要擦除旧数据并生成一个新帐户。点击“应用”以擦除,或点击“取消”重试", + "default": "默认", + "default-assets": "默认的ERC20和ERC721代币", + "default-sync-period": "同步历史记录", + "delete": "刪除", + "delete-account": "删除帐户", + "delete-and-leave-group": "删除并离开群组", + "delete-bootnode": "删除引导节点", + "delete-bootnode-are-you-sure": "确定要删除此引导节点吗?", + "delete-bootnode-title": "删除引导节点", + "delete-category-confirmation": "您确定要删除此类别吗?", + "close-chat": "刪除聊天", + "close-chat-confirmation": "确定要删除此聊天?", + "delete-confirmation": "刪除?", + "delete-for-everyone": "为所有人删除", + "delete-for-me": "为我删除", + "delete-keys-keycard": "从Keycard中删除密钥", + "delete-mailserver": "删除Status节点", + "delete-mailserver-are-you-sure": "确定要删除此Status节点吗?", + "delete-mailserver-title": "删除Status节点", + "delete-message": "删除消息", + "delete-my-account": "删除我的帐户", + "delete-my-profile": "删除我的个人资料", + "delete-network-confirmation": "确定要删除此网络吗?", + "delete-network-error": "请在删除此网络之前连接到其他网络", + "delete-network-title": "删除网络?", + "delete-node": "删除节点", + "delete-node-are-you-sure": "您确定要删除此节点吗?", + "delete-node-title": "删除节点", + "delete-profile": "删除个人资料", + "delete-profile-warning": "警告:如果您没有记下助记词, 在删除个人资料后,您将失去对资金的访问权限", + "deny": "拒绝", + "derivation-path": "派生路径", + "describe-channel": "渠道简介", + "description": "描述", + "dev-mode": "开发者模式", + "dev-mode-settings": "开发模式设置", + "device-syncing": "设备同步", + "devices": "设备", + "disable": "禁用", + "disable-all": "全部禁用", + "disable-later-in-settings": "您可以稍后在“设置”中禁用此功能", + "disabled": "已禁用", + "disconnect": "断开连接", + "disconnected": "离线聊天", + "discover": "发现", + "discover-communities": "发现社区", + "dismiss": "取消", + "display-collectibles": "展示收藏品", + "done": "完成", + "dont-ask": "不再询问我", + "edit": "编辑", + "edit-categories": "编辑类别", + "edit-channel-title": "编辑频道", + "edit-chats": "编辑聊天", + "edit-community": "编辑社区", + "edit-favourite": "编辑收藏夹", + "edit-group": "编辑群组", + "edit-message": "编辑消息", + "edit-name-and-image": "编辑名称和图像", + "edit-name-image": "编辑名称和图像", + "edit-nickname": "修改昵称", + "edit-profile": "编辑个人资料", + "edited": "已编辑", + "editing-message": "编辑消息", + "empty-activity-center": "您的聊天通知\n会在这里显示", + "empty-chat-description": "此聊天中\n还没有消息", + "empty-chat-description-community": "在过去{{quiet-hours}}没有新的消息。", + "empty-chat-description-one-to-one": "您在这里发送的任何消息都会加密, 只能由您读取,还有", + "empty-chat-description-public": "最近{{quiet-hours}}这里没有消息 。开始聊天或", + "empty-chat-description-public-share-this": "分享此聊天。", + "empty-keycard-required": "需要空白的Keycard", + "empty-pending-invitations-descr": "希望通过邀请链接加入群组的人将显示在此处", + "empty-tab": "空选项卡", + "enable": "启用", + "enable-all": "全部启用", + "enable-link-previews": "在聊天中启用链接预览?", + "encrypt-with-password": "用密码加密", + "ens-10-SNT": "10 SNT", + "ens-add-username": "添加用户名", + "ens-agree-to": "同意", + "ens-chat-settings": "聊天设置", + "ens-custom-domain": "自定义域名", + "ens-custom-username-hints": "输入完整的用户名,包括自定义域,例如username.domain.eth", + "ens-custom-username-taken": "用户名不属于您 :(", + "ens-deposit": "存款", + "ens-dismiss-message": "点击此处关闭", + "ens-displayed-with": "您的消息会显示给其他人", + "ens-get-name": "获取通用用户名", + "ens-got-it": "好的, 知道了", + "ens-locked": "用户名已锁定。您必须等到{{date}}才能将其释放", + "ens-name-content": "您可以使用以太坊名称服务(ENS)为您的聊天码注册的自定义别名。ENS名称是去中心化的用户名。", + "ens-name-not-found": "无法解析ENS名称", + "ens-name-title": "ENS名称", + "ens-network-restriction": "仅适用于主网", + "ens-no-usernames": "您没有关联任何用户名", + "ens-powered-by": "由以太坊名称服务提供支持", + "ens-primary-username": "主用户名", + "ens-register": "注册", + "ens-registration-failed": "要注册用户名,请重试。", + "ens-registration-failed-title": "交易失败", + "ens-registration-failure": "注册失败", + "ens-registration-in-progress": "正在注册...", + "ens-release-username": "释放用户名", + "ens-remove-hints": "删除将从密钥中分离用户名。", + "ens-remove-username": "删除用户名", + "ens-saved": " 现在已与您的聊天码相关联,可以在Status中使用了。", + "ens-saved-title": "用户名已添加", + "ens-show-username": "在聊天中显示我的ENS用户名", + "ens-terms-header": "名称注册条款", + "ens-terms-point-1": "资金存入1年。您的SNT将被锁定,但不会被花费。", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e(ENS注册表)。", + "ens-terms-point-2": "1年后,您可以释放名称并赎回您的押金,或者不采取任何措施保留该名称。", + "ens-terms-point-3": "如果合约条款发生变化(例如,Status升级合约),用户有权释放用户名,无论持有时间长短。", + "ens-terms-point-4": "合约控制者无法访问您缴纳的押金。它们只能原路返回发送它们的钱包地址。", + "ens-terms-point-5": "您的地址将与您的ENS名称公开关联。", + "ens-terms-point-6": "用户名创建为stateofus.eth的子域节点,并受ENS智能合约条款的约束。", + "ens-terms-point-7": "您授权合约代表您转发SNT。仅当您批准交易以授权转发时,才会出现这种情况。", + "ens-terms-point-8": "这些条款由下列地址的智能合约逻辑保证:", + "ens-terms-point-9": "{{address}} (Status用户名注册器)", + "ens-terms-registration": "名称注册条款。", + "ens-test-message": "嘿", + "ens-transaction-pending": "交易待确定…", + "ens-understand": "我知道我的钱包地址将公开连接到我的用户名。", + "ens-username": "ENS用户名", + "ens-username-already-added": "用户名已与您的聊天码关联,可以在Status中使用。", + "ens-username-available": "✓用户名可用!", + "ens-username-connected": "此用户名归您所有,并与您的聊天码相关联。", + "ens-username-connected-continue": "继续以设置“在聊天中显示我的ENS用户名”。", + "ens-username-connected-with-different-key": "如果继续,将需要通过一笔交易将用户名与您当前的聊天码关联起来。", + "ens-username-connection-confirmation": "交易一旦完成, {{username}}将被关联。", + "ens-username-hints": "至少4个字符。仅限拉丁字母、数字和小写字母。", + "ens-username-invalid": "仅限字母和数字。", + "ens-username-invalid-name-warning": "您的一个 ens 名称的注册过程已在无效状态下完成。请勿使用该名称进行钱包交易,并通过 support@status.im 获得我们的支持", + "ens-username-owned": "✓用户名归您所有。", + "ens-username-owned-continue": "如果继续,会将此用户名与您的聊天码关联。", + "ens-username-registration-confirmation": "恭喜!交易完成后,您即可拥有{{username}} 。", + "ens-username-registration-invalid": "警告!注册过程已在无效状态下完成。请勿使用该名称进行钱包交易,并通过 support@status.im 联系我们并获得支持", + "ens-username-taken": "用户名已被占用 :(", + "ens-username-you-can-follow-progress": "您可以在钱包的\"交易历史记录\"中关注进度。", + "ens-usernames": "ENS用户名", + "ens-usernames-details": "注册一个通用用户名,以方便其他用户轻松找到您", + "ens-want-custom-domain": "我在其他域上拥有名称", + "ens-want-domain": "我想要一个stateofus.eth域名", + "ens-welcome-hints": "ENS名称将那些冗长的地址转换为独一无二的用户名。", + "ens-welcome-point-customize": "ENS名称可以替换聊天中随机产生的由3个单词组成的名称。显示为“@您的名称”而不是{{name}} 。", + "ens-welcome-point-customize-title": "自定义聊天名称", + "ens-welcome-point-receive": "其他人可以在聊天中通过一个简单的操作向您汇款。", + "ens-welcome-point-receive-title": "在聊天中接收交易", + "ens-welcome-point-register": "注册一次就可以永久保留该用户名。1年后,您可以解绑用户名,并赎回您的SNT。", + "ens-welcome-point-register-title": "注册需要10个SNT", + "ens-welcome-point-simplify": "您可以使用易于共享的ENS名称而不是十六进制哈希(0x ...)来接收资金。", + "ens-welcome-point-simplify-title": "简化您的ETH地址", + "ens-welcome-point-verify": "您可以在后续步骤中验证并添加您拥有的任何用户名。", + "ens-welcome-point-verify-title": "已经拥有用户名?", + "ens-your-username": "您的用户名", + "ens-your-usernames": "您的用户名", + "ens-your-your-name": "您的ENS名称", + "enter-12-words": "输入您的12个助记词,并使用单个空格隔开", + "enter-a-private-key": "输入私钥", + "enter-a-seed-phrase": "输入助记词", + "enter-address": "输入地址", + "enter-contact-code": "ENS(vitalik94)或聊天码(0x04…)", + "enter-pair-code": "输入配对码", + "enter-pair-code-description": "可以从已配对的Status客户端设置配对码", + "enter-password": "输入密码", + "enter-password-migration-prompt": "输入密码以移动与您的密钥相关的好友名单、聊天和设置", + "enter-pin": "输入6位数字密码", + "enter-puk-code": "输入PUK码", + "enter-puk-code-description": "6位数字密码已锁定。 \n请输入PUK码以解锁密码。", + "enter-recipient-address-or-username": "输入接收人的地址或用户名", + "enter-seed-phrase": "输入助记词", + "enter-url": "输入网址", + "enter-user-pk": "输入用户公钥", + "enter-watch-account-address": "扫描二维码\n或\n输入要观察的地址", + "enter-word": "输入单词", + "enter-your-code": "输入您的6位密码", + "enter-your-password": "输入您的密码", + "error": "错误", + "error-unable-to-get-balance": "无法获取余额", + "error-unable-to-get-prices": "货币转换错误,请刷新屏幕以重试。", + "error-unable-to-get-token-balance": "无法获取代币余额", + "errors": "错误", + "eth": "ETH", + "ethereum-address": "以太坊地址", + "ethereum-node-started-incorrectly-description": "以太坊节点启动配置不正确,应用程序将停止从该条件恢复。已配置的网络ID = {{network-id}} ,实际网络ID = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "以太坊节点启动不正确", + "etherscan-lookup": "在Etherscan上查找", + "everyone": "所有人", + "expand-all": "展开全部", + "export-account": "导出帐户", + "export-key": "导出私钥", + "external-link": "外部链接", + "external-storage-denied": "对外部存储的访问遭到拒绝", + "failed": "失败", + "faq": "常见问题", + "fast": "快", + "favourite": "收藏夹", + "favourite-description": "您收藏的网站将显示在此处", + "favourites": "收藏夹", + "favourites-empty": "添加到收藏夹的地址将显示在此处", + "featured": "精选", + "fee-cap": "手续费上限", + "fee-explanation": "交易的最高费用。如果区块基本费用超过这个数值,则交易将被打包进基础费用较低的下一个区块中。", + "fee-options": "推荐的费用方案", + "fetch-community": "获取社区", + "fetch-messages": "↓获取消息", + "fetch-timeline": "↓获取", + "fetching-community": "正在获取社区...", + "find": "查找", + "finish": "完成", + "finishing-card-setup": "卡设置完成", + "fleet": "组别", + "fleet-settings": "组别设置", + "follow": "追踪", + "follow-your-interests": "进入公共聊天并结识新朋友", + "free": "↓免费", + "from": "从", + "gas-amount-limit": "gas数量上限", + "gas-limit": "Gas限制", + "gas-price": "Gas价格", + "gas-used": "使用的Gas", + "gated": " 有门槛", + "generate-a-key": "生成密钥", + "generate-a-new-account": "生成帐户", + "generate-a-new-key": "生成新密钥", + "generate-account": "生成密钥", + "generate-an-account": "生成帐户", + "generate-new-key": "生成密钥", + "generating-codes-for-pairing": "> 下载产品软件至卡\n > 生成解锁和配对码", + "generating-keys": "正在生成密钥…", + "generating-mnemonic": "生成助记词", + "get-a-keycard": "获取Keycard", + "get-started": "开始", + "get-status-at": "在http:\/\/status.im上获取Status", + "get-stickers": "获取贴纸", + "gif": "GIF", + "give-a-short-description": "请简短描述", + "give-a-short-description-community": "请简短描述", + "give-permissions-camera": "授予访问\n相机的权限", + "glossary": "词汇", + "go-to-settings": "前往设置…", + "got-it": "知道了", + "grant-face-id-permissions": "要授予脸部ID权限,请转到系统设置,确保选中“Status > 脸部ID”", + "group-chat": "群聊", + "group-chat-admin": "管理员", + "group-chat-admin-added": "** {{member}} **已成为管理员", + "group-chat-all-contacts-invited": "您的所有联系人都已在群组中", + "group-chat-created": "** {{member}} **创建了群** {{name}} **", + "group-chat-decline-invitation": "拒绝邀请", + "group-chat-member-added": "已邀请** {{member}} **", + "group-chat-member-joined": "** {{member}} **加入了群", + "group-chat-member-removed": "** {{member}} **离开了群", + "group-chat-members-count": "{{selected}} \/ {{max}}成员", + "group-chat-name-changed": "** {{member}} **将群名称更改为了** {{name}} **", + "group-chat-no-contacts": "您还没有任何联系人。 \n邀请您的好友开始聊天", + "group-details": "群信息", + "group-info": "群信息", + "group-invite": "群组邀请", + "group-invite-link": "群组邀请链接", + "group-membership-request": "加群请求", + "groups": "群组", + "gwei": "GWEI", + "has-permissions": "有权访问", + "hash": "哈希", + "help": "帮助", + "help-capitalized": "帮助", + "help-center": "帮助中心", + "help-improve-status": "帮助改善Status", + "hide": "隐藏", + "hide-content-when-switching-apps": "禁止屏幕截图", + "hide-content-when-switching-apps-ios": "隐藏预览", + "history": "历史记录", + "history-nodes": "Status节点", + "hold-card": "将卡贴放在手机背面", + "home": "主页", + "hooks": "挂钩", + "how-it-works": "使用方法", + "http-gateway-error": "糟糕,请求失败!", + "identifier": "标识符", + "identity-verification": "身份验证", + "identity-verification-request": "身份验证", + "image": "图像", + "image-remove-current": "删除当前照片", + "image-source-gallery": "从图库中选择", + "image-source-make-photo": "拍摄", + "image-source-title": "编辑图片", + "import": "导入", + "import-community": "导入社区", + "import-community-title": "导入社区", + "in-contacts": "在联系人中", + "include": "包括", + "incoming": "转入", + "incoming-transaction": "转入交易", + "incorrect-code": [ + "str", + "抱歉,此码不正确,请重新输入" + ], + "increase-gas": "增加Gas", + "initialization": "初始化", + "install": "↓安装", + "insufficient-balance-to-cover-fee": "余额不足以支付交易费", + "intro-message1": "欢迎使用Status!\n点击此消息设置您的密码并开始。", + "intro-privacy-policy-note1": "Status不会收集您的个人数据或从中获利。通过继续,即表示您同意", + "intro-privacy-policy-note2": "隐私政策", + "intro-text": "Status是您通向去中心化网络的门户", + "intro-text1": "通过点对点加密网络进行聊天,消息不会受到审查或黑客入侵", + "intro-text2": "在世界任何地方发送和接收数字资产 - 无需银行帐户", + "intro-text3": "探索您独自拥有自己数据的游戏、交易所和社交网络", + "intro-title1": "真正的私密交流", + "intro-title2": "安全加密钱包", + "intro-title3": "去中心化应用程序", + "intro-wizard-text1": "您的帐户由一组密钥控制。您的密钥存储在您的手机上,只有您可以使用它们", + "intro-wizard-text2": "一个密钥用于聊天。它有一个无法更改的可读名称。", + "intro-wizard-text3": "如果您有Keycard,将您的密钥存储在其上可以增强安全性。", + "intro-wizard-text4": "保护并加密您的密钥", + "intro-wizard-text6": "有新消息时Status将通知您。您稍后可在设置中编辑您的通知首选项", + "intro-wizard-title-alt4": "创建密码", + "intro-wizard-title-alt5": "确认密码", + "intro-wizard-title1": "获取密钥", + "intro-wizard-title2": "选择一个聊天名称", + "intro-wizard-title3": "选择密钥存储", + "intro-wizard-title4": "创建一个6位密码", + "intro-wizard-title5": "确认密码", + "intro-wizard-title6": "启用通知", + "introduce-yourself": "请简要介绍您自己", + "invalid-address-qr-code": "扫描的二维码不包含有效地址", + "invalid-format": "无效的格式\n必须是{{format}}", + "invalid-key-confirm": "应用", + "invalid-key-content": "由于文件损坏,无法加密数据库。您的资金和聊天码安全。其他数据(例如您的聊天和联系人)无法恢复。“{{erase-multiaccounts-data-button-text}}”按钮将删除所有其他数据,并允许您访问资金和发送消息", + "invalid-number": "无效数字", + "invalid-pairing-password": "配对密码无效", + "invalid-public-chat-topic": "公共聊天主题无效", + "invalid-range": "格式无效\n必须在{{min}}与{{max}}之间", + "invalid-username-or-key": "用户名或聊天码无效", + "invite": "邀请", + "invite-button": "邀请", + "invite-chat-accept": "接受", + "invite-chat-accept-join": "接受并加入", + "invite-chat-intro": "您被朋友推荐加入Status。这是一些加密货币,可以帮助您入门!用其注册一个ENS名称或购买贴纸包", + "invite-chat-name": "朋友推荐", + "invite-chat-pending": "等待中", + "invite-chat-rule": "接受将让您的朋友也获得推荐奖励", + "invite-chat-starter-pack": "入门包", + "invite-contacts": "邀请好友加入社区", + "invite-friends": "邀请好友", + "invite-friends-to-status": "邀请好友到Status", + "invite-instruction-fifth": "您可以选择随时兑换您的推荐奖金。", + "invite-instruction-first": "您向朋友发送一个专属的邀请链接,以下载并加入Status", + "invite-instruction-fourth": "您将收到推荐奖金,您的朋友将收到入门包", + "invite-instruction-second": "您的朋友下载Status并创建帐户(在安卓上)", + "invite-instruction-third": "开始与您的朋友聊天,让他们可以确认您的推荐", + "invite-people": "邀请其他人", + "invite-privacy-policy-public": "您已通过推荐链接安装了Status。通过加入此聊天,即表明您将归于您的推荐人并同意", + "invite-privacy-policy1": "接受即表示您同意推荐计划", + "invite-privacy-policy2": "条款和条件。", + "invite-public-chat-home": "推荐邀请", + "invite-public-chat-intro": "这里有一些加密货币能帮助您更快上手!用其注册ENS名称或购买贴纸包", + "invite-receive-account": "接收推荐奖励的帐户", + "invite-reward": "您每邀请一个朋友都可以赚取加密货币", + "invite-reward-friend": "朋友:", + "invite-reward-friend-description": "您的朋友将收到包含一些{{reward}}的入门包,以开始使用Status", + "invite-reward-friend-name": "入门包", + "invite-reward-you": "您:", + "invite-reward-you-description": "邀请朋友并获得{{reward}}作为推荐奖金。用其来购买贴纸、ENS名称并尝试DApp", + "invite-reward-you-name": "推荐奖励", + "invite-select-account": "选择一个帐户来接收您的推荐奖励", + "invite-warning": "此推广活动仅适用于非美国居民的安卓设备用户。朋友需要在7天内确认推荐", + "invited": "已邀请", + "italic": "斜体", + "join": "加入", + "join-a-community": "或加入社区", + "join-group-chat": "加入群", + "join-group-chat-description": "{{username}}邀请您加入群组{{group-name}}", + "join-me": "您好,请在Status上加入我: {{url}}", + "join-new-private-chat": "开始新的私密聊天", + "join-new-public-chat": "加入公共聊天", + "join-open-community": "加入社区", + "joined": "已加入", + "joined-group-chat-description": "您通过{{username}}的邀请加入{{group-name}}", + "key": "密钥", + "key-managment": "密钥管理", + "key-on-device": "私钥已保存于此设备", + "keycard": "Keycard", + "keycard-access-reset": "Keycard访问权限已重置", + "keycard-applet-install-instructions": "要安装小程序,请按照https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation上的说明进行操作。", + "keycard-awaiting-description": "尝试移动卡以找到设备上的NFC读卡器", + "keycard-awaiting-title": "仍在寻找...", + "keycard-backup": "创建备份Keycard", + "keycard-backup-success-body": "备份卡创建成功。现在,您可以像主卡一样在帐户中使用。", + "keycard-backup-success-title": "备份成功", + "keycard-blocked": "Keycard已被锁定。 \n您需要重置卡才能继续使用它。", + "keycard-can-use-with-new-passcode": "您可以通过新密码使用此卡", + "keycard-cancel-setup-text": "这将取消Keycard设置。强烈建议完成此设置以使用Keycard。确定要取消吗?", + "keycard-cancel-setup-title": "危险操作", + "keycard-connected-description": "保持卡不动", + "keycard-connected-title": "已连接", + "keycard-desc": "拥有Keycard?将密钥存储在其上;进行交易时,您将需要用到它", + "keycard-dont-ask-card": "无卡登录", + "keycard-enter-new-passcode": "输入新的密码{{step}} \/ 2", + "keycard-error-description": "再次连接卡以继续", + "keycard-error-title": "连接丢失", + "keycard-factory-reset": "将卡恢复为出厂设置", + "keycard-factory-reset-text": "执行此操作将删除卡上存储的所有助记词。确保您已备份了此Keycard上存储的助记词。", + "keycard-factory-reset-title": "确定要恢复出厂设置吗?", + "keycard-free-pairing-slots": "Keycard有{{n}}个空闲配对位", + "keycard-has-multiaccount-on-it": "此卡已满。每张卡可存储一个主密钥对", + "keycard-init-description": "请将卡放在手机背面以继续", + "keycard-init-title": "正在寻找卡片...", + "keycard-is-blocked-details": "您无法再使用此卡访问或签名此帐户。密码和PUK尝试失败次数过多。", + "keycard-is-blocked-instructions": "要访问您的帐户,您需要将卡恢复出厂设置。点击下面的按钮开始,您将需要助记词。", + "keycard-is-blocked-title": "Keycard已被锁定", + "keycard-is-frozen-details": "为了保护您的资产,您的卡已被冻结。请重置以解冻您的卡并恢复发送交易功能。您可以使用PUK或助记词来执行此操作。", + "keycard-is-frozen-factory-reset": "用助记词重置", + "keycard-is-frozen-reset": "用PUK重置", + "keycard-is-frozen-title": "Keycard被冻结", + "keycard-onboarding-finishing-header": "完成", + "keycard-onboarding-intro-header": "将密钥存储在keycard上", + "keycard-onboarding-intro-text": "已经准备好,可能需要几分钟,但这对保护您的帐户非常重要", + "keycard-onboarding-mnemonic-text": "您还需要一张纸和一支笔写下您的助记词。", + "keycard-onboarding-pairing-header": "正在配对卡...", + "keycard-onboarding-pin-text": "您将需要创建一个6位数的密码,用于保护对Keycard的访问。", + "keycard-onboarding-preparing-header": "正在准备密钥卡", + "keycard-onboarding-puk-code-header": "写下密码\n并安全地保存它们", + "keycard-onboarding-recovery-phrase-description": "您需要此助记词来找回密钥。请抄写下来,安全离线保存,并与本设备分开。", + "keycard-onboarding-recovery-phrase-header": "备份助记词", + "keycard-onboarding-recovery-phrase-text": "只用于查看。这是用于生成密钥的神奇种子。", + "keycard-onboarding-start-header": "将Keycard贴近手机背面以开始", + "keycard-onboarding-start-step1": "创建密码", + "keycard-onboarding-start-step1-text": "大约1分钟。创建一个6位密码来加密您的密钥", + "keycard-onboarding-start-step2": "写下PUK码和配对码", + "keycard-onboarding-start-step2-text": "大约1分钟。您需要准备纸和笔", + "keycard-onboarding-start-step3": "备份助记词", + "keycard-onboarding-start-step3-text": "大约1分钟。还需要准备笔和纸", + "keycard-onboarding-start-text": "设置过程中保持卡与手机接触。设置大约需要4分钟", + "keycard-processing-description": "保持卡不动", + "keycard-processing-title": "处理中...", + "keycard-recover": "卡丢失或锁定?", + "keycard-recover-text": "如果您有助记词,则可以创建与此帐户关联的新Keycard。您可以使用新的Keycard,也可以将锁定的Keycard恢复出厂设置。", + "keycard-recover-title": "为此帐户创建一张新卡?", + "keycard-recovery-intro-button-text": "开始恢复", + "keycard-recovery-intro-header": "恢复存储在Keycard上的密钥", + "keycard-recovery-intro-text": "如果您之前使用Keycard生成了密钥,现在想在本设备上使用该密钥", + "keycard-recovery-no-key-header": "没有要恢复的内容", + "keycard-recovery-no-key-text": "您的Keycard上没有存储密钥。要使用它,请生成一个新密钥并选择存储到Keycard", + "keycard-recovery-phrase-confirm-header": "确认助记词", + "keycard-recovery-phrase-confirmation-text": "您将没有第二次机会!如果您失去访问权限(例如,丢失了Keycard),则只能使用助记词访问密钥。只有您自己拥有您的助记词。请务必将助记词写到纸上并安全保存。", + "keycard-recovery-phrase-confirmation-title": "已抄写下助记词了吗?", + "keycard-recovery-success-header": "您的密钥已成功恢复", + "keycard-redeem-title": "赎回到", + "keycard-redeem-tx": "赎回资产", + "keycard-redeem-tx-desc": "贴近卡以完成签名和接收资产", + "keycard-reset-passcode": "重置密码", + "keycard-success-description": "您现在可以移除卡", + "keycard-success-title": "成功", + "keycard-unauthorized-operation": "您未经授权执行此操作。 \n请选择有效卡,然后重试。", + "keycard-upsell-subtitle": "增强的安全性和便利性", + "kicked": "已踢", + "language": "语言", + "last-backup-performed": "上次执行的备份:", + "last-transaction": "上一笔交易", + "learn-more": "了解更多", + "learn-more-about-keycard": "了解有关Keycard的更多信息", + "leave": "离开", + "leave-chat": "退出聊天", + "leave-chat-confirmation": "聊天记录将从您的设备中删除。重新加入后,您将无法查看您的任何历史记录。", + "leave-community": "退出社区", + "leave-community-message": "很遗憾你的离开,但请记住,你可以随时回来!", + "leave-community?": "离开社区?", + "leave-confirmation": "退出{{chat-name}}", + "leave-group": "退群", + "left": "剩余", + "les-ulc": "LES\/ULC", + "lets-go": "开始", + "lifestyle": "生活", + "light": "白天模式", + "limit": "限价", + "link-to-community": "链接到社区", + "linked-on": "已与{{date}}关联", + "load-messages-before": "在{{date}} 之前", + "load-more-messages": "↓获取更多消息", + "load-more-timeline": "↓获取更多", + "loading": "载入中...", + "local-notifications": "本地通知", + "local-notifications-subtitle": "启用后台服务", + "lock-app-with": "锁定应用程序方式", + "log-level": "日志级别", + "log-level-settings": "日志级别设置", + "logging": "日志记录", + "logging-enabled": "启用日志记录?", + "login-pin-description": "输入您的6位数字密码来解锁密钥", + "logout": "注销", + "logout-app-content": "该帐户将被注销。再次解锁时,将使用所选的网络", + "logout-are-you-sure": "确定要注销吗?", + "logout-key-management": "您需要先注销才能访问密钥管理。", + "logout-title": "注销?", + "looking-for-cards": "正在查找卡...", + "lost-connection": "连接中断", + "low-tip": "手续费太低", + "lower-than-average-tip": "低于平均手续费", + "mail-should-be-configured": "应配置邮件客户端", + "mailserver-address": "Status节点地址", + "mailserver-automatic": "自动选择", + "mailserver-automatic-switch-explanation": "选择可用的最快Status节点", + "mailserver-connection-error": "无法连接到Status节点", + "mailserver-content": "Status网络中路由和存储消息的节点,最多存储30天的消息。", + "mailserver-details": "Status节点详细信息", + "mailserver-error-content": "无法访问您选择的Status节点。", + "mailserver-error-title": "连接到Status节点时出错", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "选择其他Status节点", + "mailserver-reconnect": "无法连接到Status节点。点击重新连接", + "mailserver-request-error-content": "Status节点返回了以下错误:{{error}}", + "mailserver-request-error-status": "获取历史消息时发生错误,请查看日志以获取详细信息", + "mailserver-request-error-title": "Status节点请求错误", + "mailserver-request-retry": "重试请求", + "mailserver-retry": "重试", + "mailserver-title": "Status节点", + "main-account": "主账户", + "main-currency": "主要货币", + "main-networks": "主网", + "main-wallet": "主钱包", + "mainnet-network": "主网", + "make-admin": "设置管理员", + "manage-connections": "管理来自应用连接中的连接", + "manage-keys-and-storage": "管理密钥和存储", + "manage-members": "管理成员", + "mark-all-read": "标记全部为已读", + "mark-as-read": "标记为已读", + "mark-untrustworthy": "标记为不可信", + "master-account": "主帐户", + "max-fee": "最高交易费用", + "max-priority-fee": "最高手续费", + "maximum-fee": "最高费用上限", + "maximum-fee-desc": "交易的最高手续费。如果当前区块基础费用超过此值,您的交易将包含在下一个基础费用较低的区块中。", + "maybe-later": "以后再说", + "member-ban": "禁止成员", + "member-kick": "删除成员", + "members": { + "other": "{{count}} 个成员" + }, + "members-active": { + "other": "{{count}} 个成员" + }, + "members-active-none": "没有成员", + "members-count": "{{count}}位成员", + "members-label": "成员", + "members-limit-reached": "达到成员人数上限", + "members-title": "成员", + "membership": "成员资格", + "membership-approval": "需要取得同意", + "membership-approval-description": "您的社区可以自由加入,但是新成员首次必须先获得社区创建者的同意", + "membership-button": "加入条件", + "membership-declined": "进群申请被拒绝", + "membership-description": "进群需要群管理员同意", + "membership-ens": "需要ENS用户名", + "membership-ens-description": "您的社区需要有ENS用户名才能加入", + "membership-free": "无要求", + "membership-free-description": "任何人都可以自由加入您的社区", + "membership-invite": "需要其他成员的邀请", + "membership-invite-description": "您的社区只能通过现有社区成员的邀请才能加入", + "membership-none": "无", + "membership-none-placeholder": "您可以要求新成员在加入前满足某些条件。这些条件可以随时更改", + "membership-request-pending": "加入申请待通过", + "membership-requests": "加入申请", + "membership-title": "加入条件", + "mentions": "提及", + "message": "消息", + "message-deleted": "消息已删除", + "message-deleted-for-you": "为您删除的消息", + "message-not-sent": "消息未发送", + "message-options-cancel": "取消", + "message-reply": "回复", + "messages": "消息", + "messages-from-contacts-only-subtitle": "只有您添加为联系人的人可以与您开始新的聊天或邀请您加入群组", + "messages-gap-warning": "某些消息可能丢失", + "might-break": "可能会破坏一些ÐApp", + "migration-successful": "迁移成功", + "migration-successful-text": "帐号成功迁移到 Keycard", + "migrations-failed-content": "{{message}} \n架构版本:初始{{initial-version}} ,当前{{current-version}} ,最新{{last-version}} \n\n发生数据库错误。您的资金和聊天码安全。其他数据(例如您的聊天和联系人)无法恢复。 “{{erase-multiaccounts-data-button-text}}” 按钮将删除所有其他数据,并允许您访问资金并发送消息。", + "miners-higher-fee": "如果您支付更高的费用,矿工可能会更早地打包您的交易。", + "minimum-received": "最低收到", + "mobile-network-ask-me": "当在移动网络上时询问我", + "mobile-network-continue-syncing": "继续同步", + "mobile-network-continue-syncing-details": "您可以稍后在设置中更改此设置", + "mobile-network-go-to-settings": "前往设置", + "mobile-network-settings": "移动数据", + "mobile-network-sheet-configure": "您可以配置更多同步选项,请到", + "mobile-network-sheet-offline": "无Wi-Fi,消息同步已禁用。", + "mobile-network-sheet-offline-details": "使用移动网络同步已关闭", + "mobile-network-sheet-remember-choice": "记住我的选择", + "mobile-network-sheet-settings": "设置", + "mobile-network-start-syncing": "开始同步", + "mobile-network-stop-syncing": "停止同步", + "mobile-network-stop-syncing-details": "直到连接到Wi-Fi?", + "mobile-network-use-mobile": "使用移动数据", + "mobile-network-use-mobile-data": "同步聊天和钱包时,Status会消耗大量数据流量。", + "mobile-network-use-wifi": "仅限Wi-Fi", + "mobile-syncing-sheet-details": "同步聊天和钱包时,Status会消耗大量数据流量。", + "mobile-syncing-sheet-title": "使用移动数据同步?", + "more": "更多", + "move-and-reset": "迁移并重置", + "move-keystore-file": "迁移密钥存储文件", + "move-keystore-file-to-keycard": "将密钥存储文件迁移到Keycard?", + "multiaccount-exists-content": "此帐户的密钥已存在,无法再次添加。如果您忘记了密码、通行码或keycard,请卸载此应用程序,然后重新安装并且通过输入助记词的方式访问您的密钥。", + "multiaccount-exists-title": "此帐户的密钥已经存在", + "multiaccounts-recover-enter-phrase-text": "输入12、15、18、21或24个单词。 \n单词之间用单个空格分隔。", + "multiaccounts-recover-enter-phrase-title": "输入您的助记词", + "music": "音乐", + "mutal-contacts": "互为好友", + "mute": "静音", + "mute-chat": "静音聊天", + "mute-community": "静音社区", + "mutual-contact-requests": "请求加为好友", + "my-accounts": "我的帐户", + "my-accounts-empty": "您的可用帐户将显示在此处", + "my-profile": "我的个人资料", + "my-status": "我的状态", + "n-photos": "{{count}}照片", + "name": "名称", + "name-of-token": "代币的名称", + "name-optional": "名称(可选)", + "name-your-channel": "命名您的频道", + "name-your-channel-placeholder": "频道名称", + "name-your-community": "命名您的社区", + "name-your-community-placeholder": "容易记住的名称", + "need-help": "需要帮助?", + "negative": "负的", + "network": "网络", + "network-chain": "网链", + "network-details": "网络详细信息", + "network-fee": "网络费", + "network-id": "网络ID", + "network-info": "网络信息", + "network-invalid-network-id": "指定的网络ID与RPC URL的网络ID不对应", + "network-invalid-status-code": "无效状态码: {{code}}", + "network-invalid-url": "网络URL无效", + "network-settings": "网络设置", + "never": "从不", + "new": "新", + "new-category": "新类别", + "new-chat": "新聊天", + "new-community-title": "新社区", + "new-contact": "新联系人", + "new-contract": "新合约", + "new-favourite": "新的收藏", + "new-group": "新群组", + "new-group-chat": "新的群聊", + "new-messages-header": "新消息", + "new-network": "新网络", + "new-password": "新密码", + "new-pin-description": "输入新的6位数字密码", + "new-public-group-chat": "加入公共聊天", + "new-puk-description": "输入新的12位PUK码", + "new-status": "新状态", + "new-tab": "新选项卡", + "new-ui": "新的用户界面", + "next": "下一步", + "nickname": "昵称", + "nickname-description": "昵称可帮助您识别Status中的其他人。 \n只有您可以看到您所添加的昵称", + "no": "否", + "no-collectibles": "没有可用的收藏物", + "no-contacts": "还没有联系人", + "no-keycard-applet-on-card": "卡上没有Keycard小程序", + "no-messages": "无消息", + "no-pairing-slots-available": "此卡已与5台设备配对,无法与此设备配对。请使用此卡登录已配对的其中一台设备,并释放卡上的配对插槽", + "no-pinned-messages": "没有置顶的消息", + "no-pinned-messages-community-desc": "此频道没有任何置顶的消息。", + "no-pinned-messages-desc": "此聊天没有任何置顶消息。", + "no-result": "没有结果", + "no-thanks": "不,谢谢", + "no-tokens-found": "没有找到代币", + "node-address": "节点地址", + "node-details": "节点详情", + "node-info": "节点信息", + "node-version": "节点版本", + "nodes-disabled": "Status节点已禁用", + "non-archival-node": "RPC端点不支持存档请求。您的本地交易记录可能不完整。", + "non-contacts": "非联系人", + "nonce": "随机数", + "none": "无", + "normal": "正常", + "not-applicable": "对未签名交易不可用", + "not-connected-nodes": "未连接到Status节点", + "not-connected-to-peers": "没有连接到任何对等点", + "not-enough-snt": "SNT不足", + "not-found": "未找到", + "not-keycard-text": "您使用的卡不是Keycard。您需要购买Keycard才能使用", + "not-keycard-title": "不是Keycard", + "not-registered": "未注册", + "notification-settings": "通知", + "notifications": "通知", + "notifications-non-contacts": "来自非联系人的通知", + "notifications-preferences": "通知首选项", + "notifications-servers": "通知服务器", + "notifications-switch": "显示通知", + "notifications-transactions": "钱包交易", + "notify": "通知", + "now": "现在", + "off": "关闭", + "off-status-tree": "不在Status树上", + "offline": "离线", + "offline-messaging-use-history-explanation": "启用Status节点以获取应用程序离线期间的消息。启用后,Status节点将获得您的IP地址。禁用后,您将不会收到应用程序离线时的消息,并且您在上线之后也不会收到离线期间的消息。", + "offline-messaging-use-history-nodes": "使用Status节点", + "ok": "确定", + "ok-continue": "好的,继续", + "ok-got-it": "好的, 知道了", + "ok-save-pass": "好的,保存密码", + "okay": "确定", + "on": "开启", + "on-status-tree": "在Status树上", + "once-enabled-share-metadata": "启用后,聊天中发送的链接可能会与网站共享您的元数据", + "one-day": "一天", + "one-month": "一个月", + "one-week": "一周", + "online": "在线", + "open": "打开", + "open-chat": "开启聊天", + "open-dapp": "打开ÐApp", + "open-dapp-store": "发现ÐApp", + "open-home": "打开...", + "open-in-new-tab": "在新选项卡中打开", + "open-membership": "开放加入", + "open-nfc-settings": "打开NFC设置", + "open-on-block-explorer": "在区块浏览器上打开", + "opened": "已开放", + "opening-buy-crypto": "打开{{site}} ...", + "optimal": "最佳", + "optional": "可选", + "or": "或", + "outgoing": "转出", + "outgoing-transaction": "转出交易", + "page-camera-request-blocked": "相机权限被拒绝。要启用相机权限,请前往“设置”", + "page-would-like-to-use-camera": "需要使用您的相机", + "pair": "配对设备", + "pair-card": "与此设备配对", + "pair-code": "配对码", + "pair-code-explanation": "将卡与其他设备配对(最多5个)以解锁密钥并使用同一keycard签署交易", + "pair-code-placeholder": "配对码...", + "pair-this-card": "配对此卡", + "pair-this-device": "广告设备", + "pair-this-device-description": "配对您的设备,以在它们之间同步联系人和聊天", + "paired-devices": "已配对设备", + "pairing": "配对", + "pairing-card": "配对卡", + "pairing-changed": "配对码已更改", + "pairing-code-placeholder": "配对码...", + "pairing-code_error1": "配对码不匹配。", + "pairing-go-to-installation": "前往配对设置", + "pairing-maximum-number-reached-content": "请在启用新设备之前禁用您的其中一个设备。", + "pairing-maximum-number-reached-title": "达到最大设备数", + "pairing-new-installation-detected-content": "已检测到新设备。 \n为了正确使用您的设备,请在使用前务必配对并启用它们。 \n请在设置中的设备部分配对您的设备。", + "pairing-new-installation-detected-title": "检测到新设备", + "pairing-no-info": "没有信息", + "pairing-please-set-a-name": "请为您的设备设置名称。", + "passphrase": "密码短语", + "password": "密码", + "password-description": "至少6个字符。您的密码可以保护您的密钥。您需要它来解锁Status并进行交易。", + "password-mismatch": "新密码和确认不一致", + "password-placeholder": "密码...", + "password-placeholder2": "确认您的密码", + "password-reset-in-progress": "更改密码中...", + "password-reset-success": "密码已重置", + "password-reset-success-message": "您需要重新登录", + "password_error1": "密码错误。", + "paste": "粘贴", + "paste-json": "粘贴JSON", + "pay-to-chat": "付费聊天", + "peer-content": "连接到Status聊天网络的设备。每个用户可以代表一个或多个对等点,具体取决于他们的设备数量。", + "peer-title": "对等点", + "peer-to-peer": "点对点", + "peers": "对等点", + "pending": "等待中", + "pending-confirmation": "待确认...", + "pending-invitations": "待处理的加群请求", + "pending-requests": "待处理的请求", + "per-gas-price-limit": "每次gas价格限制", + "per-gas-tip-limit": "每次gas手续费用限制", + "perform-backup": "进行备份", + "permissions": "权限", + "phone-e164": "国际1", + "photos": "照片", + "photos-access-error": "要授予所需的照片权限,请前往系统设置,并确保选中 “Status” > “照片”。", + "pin": "置顶", + "pin-changed": "6位数字密码已更改", + "pin-code": "6位数字密码", + "pin-limit-reached": "已达到置顶数上限。请先取消一条之前置顶的消息。", + "pin-mismatch": "密码错误", + "pin-one-attempt": "一次尝试,", + "pin-one-attempt-blocked-after": "之后您的Keycard将被锁定", + "pin-one-attempt-blocked-before": "请注意,您只有", + "pin-one-attempt-frozen-after": "在您的Keycard被冻结之前", + "pin-one-attempt-frozen-before": "请注意,您只有", + "pin-retries-left": "您还剩{{number}}次尝试机会", + "pin-to-channel": "在频道内置顶消息", + "pin-to-chat": "在聊天中置顶", + "pinned-a-message": "置顶消息", + "pinned-by": "被置顶", + "pinned-messages": "置顶的消息", + "pinned-messages-count": { + "other": " {{count}} 条置顶消息" + }, + "pinned-messages-empty": "置顶的消息将显示在此处。要置顶消息,请按住它并点击“置顶”", + "podcasts": "播客", + "positive": "正的", + "powered-by-paraswap": "由 Paraswap 提供支持", + "preference": "偏好", + "preview-privacy": "预览隐私模式", + "previewing-may-share-metadata": "预览这些网站的链接可能会与网站所有者共享您的元数据", + "price-impact": "价格变化", + "price-impact-desc": "此交易的估算价格。如果当前区块基础费用超过此值,您的交易将包含在下一个基础费用较低的区块中。", + "principles": "原则", + "priority": "优先程度", + "privacy": "隐私", + "privacy-and-security": "隐私和安全", + "privacy-photos": "个人资料照片隐私", + "privacy-policy": "隐私政策", + "privacy-show-to-warning": "已经看过你头像的人会继续可以看到", + "private-key": "私钥", + "private-notifications": "隐私通知", + "private-notifications-descr": "有新消息时Status将通知您。您稍后可在设置中编辑您的通知首选项。", + "processing": "稍等片刻", + "product-information": "产品信息", + "profile": "个人资料", + "profile-deleted-content": "您的个人资料已成功删除", + "profile-deleted-keycard": "现在,您可以在Keycard上恢复另一个密钥对", + "profile-deleted-title": "个人资料已删除", + "profile-details": "个人资料详细信息", + "profile-not-found": "找不到个人资料", + "profile-pic-pick": "从图库中选择", + "profile-pic-remove": "删除照片", + "profile-pic-take": "拍摄照片", + "profile-picture-updated": "头像图片已更新", + "public": "公共", + "public-channel": "公共频道", + "public-chat": "公共聊天", + "public-chat-description": "加入您感兴趣的公共聊天!任何人都可以创建一个新的公共聊天频道。", + "public-chats": "公共聊天", + "public-group-status": "公共", + "public-group-topic": "主题", + "public-key": "公钥", + "puk-and-pairing-codes-displayed": "显示PUK码和配对码", + "puk-changed": "12位PUK码已更改", + "puk-code": "PUK码", + "puk-code-explanation": "如果您忘记了6位数字密码或输入错误3次,则需要使用此码解锁您的卡。", + "puk-mismatch": "PUK码错误", + "push-failed-transaction": "您的交易失败", + "push-failed-transaction-body": "{{value}} {{currency}}至{{to}}", + "push-inbound-transaction": "您收到了{{value}} {{currency}}", + "push-inbound-transaction-body": "从{{from}}到{{to}}", + "push-notifications-server-enabled": "服务器已启用", + "push-notifications-servers": "推送通知服务器", + "push-outbound-transaction": "您发送了{{value}} {{currency}}", + "push-outbound-transaction-body": "从{{from}}到{{to}}", + "quiet-days": "{{quiet-days}}天", + "quiet-hours": "{{quiet-hours}}小时", + "re-encrypt-key": "重新加密密钥", + "rearrange-categories": "重新排列类别", + "receive": "接收", + "receive-transaction": "接收交易", + "received": "已收到", + "recent": "最近", + "recent-empty": "最近使用的地址将显示在此处", + "recent-recipients": "联系人", + "recently-used-stickers": "最近使用的贴纸将显示在此处", + "recipient": "接收入", + "recipient-code": "输入接收人地址", + "recipient-code-placeholder": "0x ...或用户名.域名.eth", + "recover": "恢复", + "recover-key": "访问现有密钥", + "recover-keycard-multiaccount-not-supported": "此帐户的密钥已存在,无法再次添加。如果您忘记了密码、通行码或Keycard,请卸载此应用程序,然后重新安装并且通过输入助记词的方式访问您的密钥。", + "recover-with-keycard": "使用Keycard恢复", + "recover-with-seed-phrase": "用助记词恢复", + "recovering-key": "正在访问密钥...", + "recovery-confirm-phrase": "确认助记词", + "recovery-phrase": "助记词", + "recovery-success-text": "您必须创建新的代码或密码才能重新加密密钥", + "recovery-typo-dialog-description": "请注意,您的助记词必须使用与您收到的词组完全相同的单词和顺序", + "recovery-typo-dialog-title": "此助记词正确吗?", + "redeem-amount": "{{quantity}}个待领取奖金", + "redeem-now": "立即领取", + "redeem-success": "领取奖励成功!", + "reduced-tip": "手续费将被减少", + "refresh": "刷新", + "registered": "已注册", + "reject": "拒绝", + "reject-and-delete": "拒绝并删除", + "remember-me": "记住我", + "remind-me-later": "给我再显示", + "remote-notifications": "远程通知", + "remote-notifications-subtitle": "启用谷歌推送通知", + "remove": "删除", + "remove-favourite": "删除收藏夹", + "remove-from-chat": "从聊天中删除", + "remove-from-contacts": "从联系人中删除", + "remove-from-contacts-text": "从联系人列表中删除用户,也无法对他们隐藏您的钱包地址", + "remove-group": "删除群", + "remove-network": "删除网络", + "remove-token": "删除代币", + "removed": "已删除", + "removed-from-contacts": "从联系人中删除", + "rename": "重命名", + "repeat-pin": "重复新的6位数字密码", + "repeat-puk": "重复新的12位PUK码", + "replies": "回复", + "replying-to": "回复{{author}}", + "report-bug-email-template": "1. 问题描述\n{{description}}\n \n\n 2. 重现步骤\n{{steps}}\n \n\n 3.请附上可以演示问题的截图\n", + "request-access": "申请加入", + "request-feature": "请求功能", + "request-membership": "申请进群", + "request-pending": "请求待处理...", + "request-to-join": "申请加入", + "request-transaction": "请求交易", + "required-field": "必填字段", + "resend-message": "重新发送", + "reset-card": "重置卡", + "reset-card-description": "此操作会将卡片重置为初始状态。它将清除所有的卡数据,包括私钥。操作不可逆。", + "reset-database": "重置数据库", + "reset-database-warning": "删除聊天记录、联系人和设置。当您的密码丢失时才需要执行此步骤", + "reset-database-warning-keycard": "删除聊天记录、联系人和设置。", + "reset-password": "重置密码", + "restore-defaults": "恢复默认值", + "retry": "重试", + "revoke-access": "撤消访问权限", + "rpc-url": "RPC URL", + "rpc-usage-copy": "复制", + "rpc-usage-filter": "过滤方式", + "rpc-usage-filter-methods": "过滤方式", + "rpc-usage-filtered-total": "第{{filtered-total}} ,共 {{total}}", + "rpc-usage-get-stats": "刷新", + "rpc-usage-info": "RPC使用情况统计", + "rpc-usage-reset": "重置", + "rpc-usage-total": "共计", + "safe-estimate": "安全估计", + "save": "保存", + "save-image-library": "将图片保存到库", + "save-password": "保存密码", + "save-password-unavailable": "设置设备码以保存密码", + "save-password-unavailable-android": "保存密码不可用:您的设备可能已被root或缺少必要的安全功能。", + "say-hi": "打个招呼", + "scan-qr": "扫描二维码", + "scan-qr-code": "扫描钱包地址的二维码", + "scan-tokens": "扫描代币", + "search": "搜索", + "search-contacts": "搜索好友", + "search-discover-communities": "搜索社区或类别", + "search-no-chat-found": "没有搜索结果。您是想:", + "secret-keys-confirmation-text": "万一您丢失了手机,您将需要它们才能继续使用您的Keycard。", + "secret-keys-confirmation-title": "将密码抄写下来了吗?", + "security": "安全", + "see-details": "查看详细信息", + "see-it-again": "再次核对", + "see-sticker-set": "查看完整的贴纸集", + "see-suggestions": "查看建议", + "seed-key-uid-mismatch": "助记词不匹配", + "seed-key-uid-mismatch-desc-1": "您输入的助记词与{{multiaccount-name}}不匹配", + "seed-key-uid-mismatch-desc-2": "要管理此帐户的密钥,请验证您的助记词,然后重试。", + "seed-phrase-content": "从BIP39标准列表中随机选择的一组易于阅读的单词,用于在其他钱包和设备上恢复或访问您的以太坊帐户。在整个加密生态系统中也称为“助记词”,“恢复短语”或“钱包备份”。大多数加密应用程序使用相同的标准来生成帐户。", + "seed-phrase-placeholder": "助记词...", + "seed-phrase-title": "助记词", + "select": "选择", + "select-account": "选择帐号", + "select-account-dapp": "选择您要用于DApp的帐户", + "select-account-first": "首先选择一个帐户", + "select-chat": "选择一个聊天开始发送消息", + "select-new-location-for-keys": "选择一个新位置以保存您的私钥", + "select-token-to-receive": "选择要接收的代币", + "select-token-to-swap": "选择要交换的代币", + "selected": "已选择", + "send": "发送", + "send-contact-request-message": "要开始聊天,您们需要成为联系人", + "send-logs": "报告错误", + "send-logs-to": "向{{email}}报告错误", + "send-message": "发送消息", + "send-push-notifications": "发送推送通知", + "send-push-notifications-description": "禁用此功能后,接收您消息的人不会收到通知", + "send-request": "发送请求", + "send-request-amount": "金额", + "send-request-amount-max-decimals": "最大小数位数为{{asset-decimals}}", + "send-request-unknown-token": "未知代币 - {{asset}}", + "send-sending-to": "至{{recipient-name}}", + "send-transaction": "发送交易", + "sending": "发送", + "sent": "已发送", + "sent-at": "发送于", + "server": "服务器", + "set-a-topic": "创建主题", + "set-currency": "设置货币", + "set-custom-fee": "设置自定义费用", + "set-dapp-access-permissions": "设置DApp访问权限", + "set-max": "设至上限", + "settings": "设置", + "share": "分享", + "share-address": "分享地址", + "share-chat": "分享聊天", + "share-community": "分享社区", + "share-contact-code": "分享我的聊天码", + "share-dapp-text": "查看我在Status上使用的这个DApp:{{link}}", + "share-image": "分享图片", + "share-invite-link": "分享邀请链接", + "share-link": "分享链接", + "share-logs": "分享日志", + "share-my-profile": "分享我的个人资料", + "share-profile": "分享个人资料", + "share-profile-link": "分享个人资料链接", + "share-public-chat-text": "在Status应用中查看此公共聊天:{{link}}", + "shared": "已分享", + "sharing-copied-to-clipboard": "已复制", + "sharing-copy-to-clipboard": "复制", + "sharing-data-desc-1": "数据根据公共规则进行验证,以确保不会发送敏感数据。应随时保持怀疑,并进行验证。", + "sharing-data-desc-2": "使用数据通过 Status 的对等网络进行端到端加密发送", + "sharing-data-desc-3": "使用一次性密钥而不是您的常规聊天密钥", + "sharing-data-desc-4": "使用数据无法与您的 IP 地址相关联", + "sharing-data-desc-5": "所有用户的累积数据公开可用", + "sharing-data-desc-6": "数据发送后会从您的手机中删除", + "sharing-share": "分享", + "shell-placeholder-subtitle": "打开您的社区、\n钱包帐户、浏览器窗口和发送消息", + "shell-placeholder-title": "你的应用程序将在这里运行", + "show-less": "收起", + "show-more": "显示更多", + "show-notifications": "显示通知", + "show-profile-pictures": "要显示个人头像的组为", + "show-profile-pictures-to": "将您的个人头像图片显示给", + "show-qr": "显示二维码", + "show-transaction-data": "显示交易数据", + "sign-and-send": "签名并发送", + "sign-anyway": "仍要签名", + "sign-in": "登录", + "sign-message": "签名消息", + "sign-out": "注销", + "sign-request-failed": "无法签署消息", + "sign-with": "签名方式", + "sign-with-password": "使用密码签署", + "sign-you-in": "登录…", + "signing": "签名", + "signing-a-message": "签署消息", + "signing-phrase": "签名短语", + "skip": "跳过", + "slippage": "滑点", + "slow": "慢", + "something-went-wrong": "出错", + "soon": "很快", + "sort-communities": "对社区进行排序", + "specify-address": "指定地址", + "specify-name": "指定名称", + "specify-network-id": "指定网络ID", + "specify-rpc-url": "指定一个RPC URL", + "specify-server-public-key": "输入服务器公钥", + "specify-symbol": "指定符号", + "start-chat": "开始聊天", + "start-conversation": "开始聊天", + "start-group-chat": "开始群聊", + "start-new-chat": "开始新聊天", + "starter-pack-coming": "入门包来了", + "starter-pack-coming-description": "可能需要几分钟到几小时", + "starter-pack-received": "入门包已收到", + "starter-pack-received-description": "这是一些加密货币,可以帮助您入门!用其来获取贴纸、ENS名称并尝试DApp", + "status": "Status", + "status-always-online": "始终在线", + "status-automatic": "自动", + "status-automatic-subtitle": "自动设置状态", + "status-confirmed": "已确认", + "status-dnd": "请勿打扰", + "status-dnd-subtitle": "静音所有通知", + "status-hardwallet": "Status硬件钱包", + "status-inactive": "不活跃", + "status-inactive-subtitle": "隐藏您的在线状态", + "status-is-open-source": "Status是开源项目", + "status-keycard": "Status Keycard", + "status-mobile-descr": "Status同步聊天时往往会消耗大量数据。您可以选择在移动网络上时不同步。", + "status-not-sent-click": "未确认。点击选项", + "status-not-sent-tap": "未确认。点击选项", + "status-pending": "等待中", + "status-sent": "已发送", + "status-tx-not-found": "未找到TX", + "status-updates-descr": "Status更新将显示在此处。将个人资料添加为联系人,以在您的时间轴上接收更新。", + "statuses-descr": "分享您的想法,并更新到您的联系人", + "statuses-my-status-descr": "分享您的想法。访问您个人资料的任何人都可以看到您的状态。将您添加为联系人的用户,将在时间轴上收到您的更新", + "step-i-of-n": "步骤{{step}}\/{{number}}", + "sticker": "贴纸", + "sticker-market": "贴纸市场", + "storage": "存储", + "strikethrough": "删除线", + "submit": "提交", + "submit-bug": "提交错误", + "success": "成功", + "successful-connection": "连接成功", + "suggested-min-tip": "建议的最低手续费", + "suggested-price-limit": "建议的价格", + "swap": "交换", + "swap-details": "交换详情", + "switch-to-simple-interface": "切换到简单界面", + "symbol": "符号", + "sync-all-devices": "同步所有设备", + "sync-in-progress": "正在同步...", + "sync-settings": "同步设置", + "sync-synced": "同步中", + "syncing-devices": "正在同步...", + "system": "智能模式", + "tabs": "选项卡", + "tag-was-lost": "标签丢失", + "tap-card-again": "再次将卡贴近手机背面", + "terms-of-service": "使用条款", + "test-networks": "测试网络", + "text-input-disabled": "请稍候...", + "thank-you": "谢谢", + "this-device": "本设备", + "this-device-desc": "您的密钥将被加密并安全地存储在您的设备上", + "this-is-you-signing": "这是您的签名短语", + "this-will-take-few-seconds": "这将需要几秒钟", + "three-days": "三天", + "three-words-description": "在签署每笔交易之前, 您应看到这3个单词", + "three-words-description-2": "如果您看到不同的组合, 请取消交易并注销", + "timeline": "时间轴", + "tip-cap": "小费上限", + "to": "到", + "to-block": "屏蔽", + "to-enable-biometric": "要启用{{bio-type-label}} ,您必须在解锁界面勾选保存密码", + "to-encrypt-enter-password": "请输入密码以对您的帐户加密", + "to-see-this-message": "要查看此消息,", + "token": "代币", + "token-auto-validate-decimals-error": "地址{{address}}中的代币{{symbol}}小数位错误 - 设置为{{expected}}但检测为{{actual}}", + "token-auto-validate-name-error": "地址{{address}}中的代币{{symbol}}名称错误 - 设置为{{expected}}但检测为{{actual}}", + "token-auto-validate-symbol-error": "地址{{address}}中的代币{{symbol}}符号错误 - 设置为{{expected}}但检测为{{actual}}", + "token-details": "代币详细信息", + "token-price": "代币价格", + "topic-name-error": "仅使用小写字母(a到z)、数字和短划线( - )。不要使用聊天码", + "total-gas": "总交易手续费", + "total-members": "成员总数", + "transaction": "交易", + "transaction-data": "交易数据", + "transaction-declined": "交易被拒绝", + "transaction-description": "网络上达到12个确认将被认为交易完成。", + "transaction-details": "交易明细", + "transaction-failed": "交易失败", + "transaction-fee": "交易费用", + "transaction-history": "交易记录", + "transaction-request": "交易请求", + "transaction-sent": "交易已发送", + "transaction-signed": "交易已成功签署", + "transactions": "交易", + "transactions-filter-select-all": "全选", + "transactions-filter-title": "筛选记录", + "transactions-history": "交易记录", + "transactions-history-empty": "您的历史记录中尚未有交易", + "transactions-history-loading": "正在载入交易记录。这可能需要一点时间。", + "transactions-load-more": "加载更多", + "transactions-management-enabled": "交易管理 (alpha)", + "transactions-sign": "签名", + "transfer-ma-unknown-error-desc-1": "您的多帐户好像没有被删除。数据库可能已被重置", + "transfer-ma-unknown-error-desc-2": "请检查您的帐户列表,然后重试。如果未列出该帐户,请访问现有密钥并使用助记词进行恢复", + "transfers-fetching-failure": "交易历史记录无法更新。请检查您的网络连接,然后下拉以重试", + "tribute-required-by-multiaccount": "{{multiaccount-name}}需要收到SNT才能开始聊天", + "tribute-state-paid": "朝贡已支付", + "tribute-state-pending": "朝贡等待中", + "tribute-state-required": "需要朝贡{{snt-amount}}SNT", + "tribute-to-talk": "聊天收益模式", + "tribute-to-talk-add-friends": "添加好友为联系人就可以允许他们无需支付朝贡便可以聊天。", + "tribute-to-talk-are-you-friends": "你们是朋友吗?", + "tribute-to-talk-ask-to-be-added": "请求添加为联系人", + "tribute-to-talk-contact-received-your-tribute": " 已收到您的朝贡。现在可以安全地互相聊天了。", + "tribute-to-talk-desc": "通过收取SNT来开启新聊天,从而可以货币化您的注意力。", + "tribute-to-talk-disabled": "付费聊天模式关闭", + "tribute-to-talk-disabled-note": "从现在开始,新用户可以在不发送SNT的情况下与您聊天。", + "tribute-to-talk-enabled": "您已启用朝贡。", + "tribute-to-talk-finish-desc": "从现在开始,您只会收到来自联系人和付费用户的聊天信息 ", + "tribute-to-talk-learn-more-1": "您的时间和精力是您最宝贵的资产。 朝贡可让您设置陌生人加您为好友所需的SNT数量。", + "tribute-to-talk-learn-more-2": "任何不在您的联系人列表中的人都会被要求付款,您可以在收款后回复他们。", + "tribute-to-talk-learn-more-3": "您可以随时把钱退回,但为了确保好友可以随时联系您,请先将其添加为联系人。", + "tribute-to-talk-paywall-learn-more-1": "我们的时间和精力是我们最宝贵的资产。朝贡让您添加新朋友以换取SNT付款。", + "tribute-to-talk-paywall-learn-more-2": "要与设置了朝贡的人开始聊天,只需支付所需的SNT,您就会被添加为联系人。", + "tribute-to-talk-paywall-learn-more-3": "如果您认识他们,则可以在Status之外分享您的个人资料,以便其他人可自由添加您。", + "tribute-to-talk-pending": "朝贡等待确认", + "tribute-to-talk-pending-note": "朝贡交易正在等待网络确认。您可以在交易历史记录中查看其状态", + "tribute-to-talk-removing-note": "删除朝贡将允许陌生人在不发送SNT的情况下开始聊天。需要进行交易才能生效。", + "tribute-to-talk-set-snt-amount": "设置开始新聊天所需的SNT数量", + "tribute-to-talk-signing": "正在等待签署交易", + "tribute-to-talk-transaction-failed-note": "交易失败,您的朝贡设置未更改", + "tribute-to-talk-tribute-received1": "朝贡收到了。您和", + "tribute-to-talk-tribute-received2": " 现在是联系人,可以安全互聊了。", + "tribute-to-talk-you-require-snt": "您需要收到 SNT 才能开启新聊天。", + "try-again": "再试一次", + "try-keeping-the-card-still": "保持卡不动", + "turn-nfc-description": "您的设备已禁用NFC,请先在设置中启用NFC", + "turn-nfc-on": "开启NFC以继续", + "two-minutes": "2分钟", + "tx-fail-description1": "此交易可能失败。请根据自身风险承担能力设置自定义网络费。", + "tx-fail-description2": "此交易可能失败。请根据自身风险承担能力设置自定义网络费。", + "type": "类型", + "type-a-message": "消息", + "ulc-enabled": "ULC已启用", + "unable-to-fetch": "无法获取聊天记录", + "unable-to-read-this-code": "无法读取此代码", + "unable-to-send-messages": "无法发送和接收消息", + "unblock": "解锁", + "unblock-contact": "取消屏蔽此用户", + "undo": "撤消", + "unknown-status-go-error": "未知的status-go错误", + "unlimited": "无限制", + "unlock": "解锁", + "unmute": "取消静音", + "unmute-chat": "取消聊天静音", + "unmute-community": "取消静音社区", + "unpair-card": "卡未配对", + "unpair-card-confirmation": "此操作将取消卡与当前设备的配对。需要6位数字密码授权。要继续吗?", + "unpair-keycard": "取消此手机与Keycard的配对", + "unpair-keycard-warning": "您的配对码\/PUK和PIN保持不变", + "unpaired-keycard-text": "您刷的Keycard与此手机无关联", + "unpaired-keycard-title": "您的卡似乎已取消配对", + "unpin": "取消置顶", + "unpin-from-channel": "取消频道内置顶消息", + "unpin-from-chat": "从聊天中取消置顶", + "untrustworthy": "不可信", + "update": "更新", + "update-to-listen-audio": "更新到最新版本,收听音频消息!", + "update-to-see-image": "更新到最新版本,欣赏漂亮的图像!", + "update-to-see-sticker": "更新到最新版本,查看精美的贴纸!", + "updates-to-tos": "使用条款更新", + "updates-to-tos-desc": "在继续之前,请查看服务条款,并确认您对使用此应用程序的方式承担全部责任。", + "url": "URL", + "usd-currency": "美元", + "use-as-profile-picture": "用作个人头像图片", + "use-valid-contact-code": "请输入或扫描有效的聊天码或用户名", + "validation-amount-invalid-number": "金额不是一个有效的数字", + "validation-amount-is-too-precise": "金额太精确了。最大小数位数为{{decimals}} 。", + "verified-community": "✓通过验证的社区", + "version": "应用版本", + "view": "查看", + "view-community-rules": "查看社区规则", + "view-cryptokitties": "在CryptoKitties中查看", + "view-cryptostrikers": "在CryptoStrikers中查看", + "view-data": "查看数据", + "view-details": "查看详细信息", + "view-etheremon": "在Etheremon中查看", + "view-gitcoin": "在Gitcoin中查看", + "view-members": "查看成员", + "view-on-opensea": "在opensea中查看", + "view-pinned-messages": "查看置顶消息", + "view-profile": "查看个人资料", + "view-public-dashboard": "查看公共仪表板", + "view-rules": "查看规则", + "view-signing": "查看签名短语", + "view-superrare": "在SuperRare中查看", + "vote-to-feature": "投票推荐此社区", + "waiting-for-wifi": "无Wi-Fi,消息同步已禁用。", + "waiting-for-wifi-change": "设置", + "waiting-to-sign": "正在等待签名交易…", + "waiting-wi-fi": "正在等待Wi-Fi…", + "waku-bloom-filter-mode": "Waku Bloom过滤器模式", + "wakuv2-change-nodes": "您确定要切换 Wakuv2 节点吗?", + "wakuv2-node-format": "\/ip4\/ {node-ip} \/tcp\/ {port} \/p2p\/ {id}", + "wakuv2-settings": "Waku v2设置", + "wallet": "钱包", + "wallet-address": "钱包地址", + "wallet-asset": "资产", + "wallet-assets": "资产", + "wallet-backup-recovery-title": "备份助记词", + "wallet-choose-recipient": "选择接收人", + "wallet-collectibles": "收藏物", + "wallet-connect": "钱包连接", + "wallet-connect-2.0": "钱包连接2.0", + "wallet-connect-app-connected": "已连接", + "wallet-connect-go-back": "回到你的浏览器或dapp", + "wallet-connect-proposal-description": "通过连接,你允许{{name}}获取你的钱包地址和启用Web3", + "wallet-connect-proposal-title": "愿意连接你的钱包", + "wallet-insufficient-funds": "资金不足", + "wallet-insufficient-gas": "ETH不足以支付gas费", + "wallet-invalid-address": "无效的地址:{{data}}", + "wallet-invalid-address-checksum": "地址错误: \n {{data}}", + "wallet-invalid-chain-id": "网络不匹配: \n {{data}}而当前链是{{chain}}", + "wallet-key-content": "基于以太坊标准并以0x开头的用64个字符表示的十六进制地址。面向公众,当您想接收资金时,您的帐户地址将共享给其他人。该地址也称为“以太坊地址”或“钱包地址”。", + "wallet-key-title": "帐户地址", + "wallet-manage-accounts": "管理帐户", + "wallet-manage-app-connections": "管理app连接", + "wallet-manage-assets": "管理资产", + "wallet-request": "请求", + "wallet-send": "发送", + "wallet-send-min-units": "最少21000个单位", + "wallet-send-min-wei": "最小1 Gwei", + "wallet-settings": "钱包设置", + "wallet-total-value": "总价值", + "wallet-transaction-total-fee": "总费用", + "wants-to-access-profile": "想要访问您的个人资料", + "warning": "警告", + "warning-message": "抱歉,我们限制快速连续发送多条消息,以防滥发。请稍后再试", + "warning-sending-to-contract-descr": "您输入的地址是智能合约地址,将资金发送到此地址可能会导致资金损失。若要与DApp交互,请在Status DApp浏览器中打开DApp。", + "watch-only": "仅限观察", + "wc-brand-guide": "有关使用商标和徽标等品牌的指南", + "wc-disclaimer": "免责声明(包括第三方提供商)、保证和法律声明", + "wc-dispute": "争议解决条款", + "wc-how-to-use-status-app": "如何使用 Status 应用程序,包括隐私和安全", + "wc-new-tos-based-on-principles-prefix": "使用条款的设计基于我们", + "web-view-error": "无法加载页面", + "websites": "网站", + "webview-camera-permission-requests": "手机相机权限请求", + "webview-camera-permission-requests-subtitle": "启用后,网站和DApp可请求使用您的相机", + "welcome-blank-community-message": "您的社区将显示在此处。", + "welcome-blank-message": "您的聊天记录将在此处显示。要开始新的聊天,请点击⊕按钮", + "welcome-community-blank-message": "您的频道将显示在此处。要创建新频道,请单击⊕按钮,然后选择\"创建频道\"", + "welcome-community-blank-message-edit-chats": "您的频道将显示在此处。要创建新频道,请返回社区界面,单击⊕按钮,然后选择\"创建频道\"", + "welcome-screen-text": "设置您的钱包,邀请好友聊天\n并浏览广受欢迎的DApp!", + "welcome-to-status": "欢迎使用Status!", + "welcome-to-status-description": "设置您的加密钱包、邀请好友聊天、浏览去中心化应用", + "what-changed": "更改的内容", + "what-is-shared": "共享的内容", + "whats-on-your-mind": "您想到了些什么…", + "whats-trending": "查看热点", + "word-count": "字数", + "word-n": "{{number}}#单词", + "word-n-description": "为了确认您是否正确备份了助记词,请在上方输入{{number}}#单词。", + "words-n": { + "other": "{{count}} 个单词" + }, + "write-down-and-store-securely": "写下来并安全的存储起来", + "wrong-address": "地址错误", + "wrong-card": "错误的卡", + "wrong-card-text": "卡与选择的密钥不对应", + "wrong-contract": "合约错误", + "wrong-keycard-text": "您使用的Keycard与此手机无关联", + "wrong-keycard-title": "好像您刷了一张错误的Keycard", + "wrong-password": "密码错误", + "wrong-word": "错误的单词", + "yes": "是", + "you": "您", + "you-already-have-an-asset": "您已拥有资产{{value}}", + "you-are-all-set": "设置完成!", + "you-are-all-set-description": "如果您丢了手机,现在可以使用助记词访问您的资金和聊天码", + "you-can-change-account": "您可以根据自己的喜好更改帐户的名称和颜色", + "you-can-choose-preview-websites": "您可以选择以下哪些网站可以在聊天中预览链接中的描述和图片", + "you-can-fetch": "您可以获取聊天记录", + "you-dont-have-contacts": "您还没有任何联系人。", + "you-dont-have-contacts-invite-friends": "您还没有任何联系人。\n邀请您的朋友开始聊天。", + "you-dont-have-stickers": "您还没有任何贴纸", + "you-will-need-this-code": "您需要此密码才能打开应用程序并签署交易。", + "you-will-start-from-scratch": "您将从零开始使用一组新的密钥", + "your-card-is-frozen": "您的Keycard已冻结。重置卡访问权限", + "your-contact-code": "授予此DApp访问权限以查看您的聊天码", + "your-data-belongs-to-you": "如果您丢失了您的助记词, 您也同时失去了您的数据和资金", + "your-data-belongs-to-you-description": "如果您失去了访问权限(例如,丢失了手机),则只能使用助记词访问密钥。除了您没有任何其他人有您的助记词。请将其写下来并安全保存", + "your-keys": "您的密钥", + "your-price-limit": "您的设定价格", + "your-recovery-phrase": "您的助记词", + "your-recovery-phrase-description": "这是您的助记词。您需要用它来证明这个钱包属于您。您只能查看一次!请将其写在纸上并保存在安全的地方。如果丢失或重新安装钱包,您将需要用到这些助记词。", + "your-tip-limit": "您的手续费上限", + "youre-on-mobile-network": "您正在使用移动网络" +} diff --git a/translations/zh_Hans_CN.json b/translations/zh_Hans_CN.json new file mode 100644 index 00000000000..440f7e7d3f6 --- /dev/null +++ b/translations/zh_Hans_CN.json @@ -0,0 +1,967 @@ +{ + "about-app": "关于", + "about-key-storage-content": "Status永远不会访问您的私钥。请务必备份您的助记词。如果您丢失了手机,这是找回密钥的唯一方法。", + "about-key-storage-title": "关于密钥存储", + "about-names-content": "您的身份在设计上是安全且私密的。您将获得本地生成的加密密钥对。名称和图像是此秘钥的可视形式。它们是唯一的。没有人能假装成你。除非你提供你自己的名称,否则没有人看到你的名称。", + "about-names-title": "关于名称", + "access-key": "访问密钥", + "account-added": "帐户已添加", + "account-color": "账户颜色", + "account-name": "帐户名称", + "account-settings": "帐号设定", + "accounts": "帐户", + "active-online": "在线", + "active-unknown": "未知", + "add": "添加", + "add-a-watch-account": "添加一个观察帐户", + "add-account": "添加帐号", + "add-account-description": "您可以导入任何类型的以太坊帐户,将其添加到你的Status钱包", + "add-account-incorrect-password": "密码不正确。输入用于解锁应用程序的密码。", + "add-an-account": "添加一个帐号", + "add-bootnode": "添加引导节点", + "add-contact": "增加联系人", + "add-custom-token": "添加自定义代币", + "add-mailserver": "添加邮件服务器", + "add-members": "添加成员", + "add-network": "添加网络", + "add-to-contacts": "添加到联系人", + "address": "地址", + "advanced": "高级", + "advanced-settings": "高级设置", + "agree-by-continuing": "继续授权给我们", + "all": "全部", + "allow": "允许", + "allowing-authorizes-this-dapp": "允许授权此 DApp 检索您的钱包地址, 并启用 Web3", + "already-have-asset": "你已经有了这种资产", + "amount": "金额", + "are-you-sure-description": "您将无法再次看到整个助记词", + "are-you-sure?": "你确定?", + "ask-in-status": "咨询问题或报告错误", + "at": "在", + "authorize": "授权", + "available": "可用", + "available-participants": { + "one": "", + "other": "你可以选择{{count}} 参与者。" + }, + "back": "返回", + "back-up-seed-phrase": "备份助记词", + "back-up-your-seed-phrase": "备份你的助记词", + "backup-recovery-phrase": "备份助记词", + "balance": "余额", + "begin-set-up": "开始设置", + "biometric-auth-android-sensor-desc": "触摸传感器", + "biometric-auth-android-sensor-error-desc": "失败", + "biometric-auth-android-title": "需要进行身份验证", + "biometric-auth-confirm-logout": "重新登录", + "biometric-auth-confirm-message": "生物识别身份验证需要连续,如果不能完成,请使用帐户密码重新登录", + "biometric-auth-confirm-title": "你必须验证!", + "biometric-auth-confirm-try-again": "再试一次", + "biometric-auth-error": "无法执行生物识别身份验证( {{code}} )", + "biometric-auth-login-error-title": "生物识别身份验证错误", + "biometric-auth-login-ios-fallback-label": "输入密码", + "biometric-auth-reason-login": "登录Status", + "biometric-auth-reason-verify": "验证身份", + "blank-keycard-text": "生成密钥和名称后,您可以继续使用密钥卡", + "blank-keycard-title": "看起来你已经点击了\n一张空白的钥匙卡", + "block": "块", + "block-contact": "屏蔽联系人", + "block-contact-details": "屏蔽将删除此用户之前的所有消息,并拒收新消息", + "blocked-users": "被屏蔽的用户", + "bootnode-address": "引导节点地址", + "bootnode-details": "引导节点详细信息", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "引导节点", + "bootnodes-enabled": "引导节点启用", + "bootnodes-settings": "引导节点设置", + "browsed-websites": "浏览过的网站将在此处显示。", + "browser": "浏览器", + "browser-not-secure": "连接不安全!不要在本网站上签署交易或发送个人数据。", + "browser-secure": "连接是安全的。在签署交易或输入个人数据之前, 请确保您确实信任此网站。", + "browsers": "浏览器", + "browsing-cancel": "取消", + "browsing-open-in-android-web-browser": "在网络浏览器中打开", + "browsing-open-in-ios-web-browser": "在网络浏览器中打开", + "browsing-open-in-status": "在Status中打开", + "browsing-site-blocked-description1": "我们从此地址检测到潜在的恶意活动。为了保护您和您的钱包,我们阻止了进一步的引导。 \n\n如果您认为这是一个错误,请告诉我们", + "browsing-site-blocked-description2": "公聊", + "browsing-site-blocked-go-back": "返回", + "browsing-site-blocked-title": "此站点被阻止", + "browsing-title": "浏览", + "camera-access-error": "要授予所需的摄像机许可,请转到系统设置,并确定选中了 “Status” > “摄像机”。", + "can-not-add-yourself": "不能添加自己", + "cancel": "取消", + "cancel-keycard-setup": "取消密钥卡设置", + "cannot-read-card": "无法读卡。\n请把卡放在你的手机后面", + "cannot-use-default-pin": "密码000000是无效的。 \n请使用其他数字", + "card-is-blank": "这是一张空白卡", + "card-reseted": "卡片已被重置", + "card-unpaired": "卡片已从当前设备删除配对", + "change-fleet": "变更组别到{{fleet}}", + "change-logging-enabled": "您确定要{{enable}}日志记录吗?", + "change-passcode": "更改密码", + "change-password": "更改密码", + "change-pin": "更改6位数字密码", + "changed-amount-warning": "数量已从{{old}}更改为{{new}}", + "changed-asset-warning": "资产已从{{old}}更改为{{new}}", + "chaos-mode": "混乱模式", + "chaos-unicorn-day": "混乱独角兽日", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "聊天", + "chat-key": "聊天密钥", + "chat-name": "聊天名称", + "chat-settings": "对话设置", + "chats": "聊天", + "check-your-recovery-phrase": "检查您的助记词", + "choose-authentication-method": "选择一种身份验证方法来保护您的帐户", + "clear": "清空", + "clear-history": "清空历史记录", + "clear-history-action": "清空", + "clear-history-confirmation": "清空历史记录?", + "clear-history-confirmation-content": "您确定要清除此聊天记录吗?", + "clear-history-title": "清空历史记录?", + "close-app-button": "确认", + "close-app-content": "应用将会停止并关闭。当你重新打开应用,选中的网络将会被使用", + "close-app-title": "警告!", + "command-button-send": "发送", + "complete-hardwallet-setup": "现在此卡是您帐户安全的重要组成部分。没有它就无法发送交易。", + "completed": "已完成", + "confirm": "确认", + "confirmations": "确认", + "confirmations-helper-text": "请等待达到至少12个确认,来确保交易已被安全地处理。", + "connect": "连接", + "connect-mailserver-content": "连接到{{name}} ?", + "connected": "已连接", + "connecting": "正在连接...", + "connecting-requires-login": "连接其它网络需要登录", + "connection-with-the-card-lost": "与卡的连接\n 已丢失", + "connection-with-the-card-lost-setup-text": "要恢复设置,请将卡放到\n你的手机背面并保持\n卡与手机的连接", + "connection-with-the-card-lost-text": "要继续,请将卡片放在手机背面", + "contact-code": "聊天码", + "contact-s": { + "one": "", + "other": "联系人" + }, + "contacts": "联系人", + "continue": "继续", + "contract-address": "合约地址", + "contract-interaction": "合约交互", + "copy-info": "复制信息", + "copy-qr": "复制代码", + "copy-to-clipboard": "复制", + "copy-transaction-hash": "复制交易哈希", + "cost-fee": "成本\/费用", + "counter-9-plus": "9+", + "create": "创建", + "create-a-pin": "创建6位数字密码", + "create-group-chat": "创建群聊", + "create-multiaccount": "创建帐号", + "create-new-key": "创建一个新密钥", + "create-pin": "创建6位数字密码", + "create-pin-description": "你需要卡+本6位数字密码才能解锁Status和确认交易", + "created-group-chat-description": "您创建了群{{group-name}}", + "cryptokitty-name": "CryptoKitty# {{id}}", + "currency": "货币", + "currency-display-name-aed": "阿联酋迪拉姆", + "currency-display-name-afn": "阿富汗尼", + "currency-display-name-ars": "阿根廷比索", + "currency-display-name-aud": "澳元", + "currency-display-name-bbd": "巴巴多斯元", + "currency-display-name-bdt": "孟加拉国塔卡", + "currency-display-name-bgn": "保加利亚列弗", + "currency-display-name-bhd": "巴林第纳尔", + "currency-display-name-bnd": "文莱元", + "currency-display-name-bob": "玻利维亚玻利维亚诺", + "currency-display-name-brl": "巴西雷亚尔", + "currency-display-name-btn": "不丹努扎姆", + "currency-display-name-cad": "加拿大元", + "currency-display-name-chf": "瑞士法郎", + "currency-display-name-clp": "智利比索", + "currency-display-name-cny": "人民币", + "currency-display-name-cop": "哥伦比亚比索", + "currency-display-name-crc": "哥斯达黎加科郎", + "currency-display-name-czk": "捷克克朗", + "currency-display-name-dkk": "丹麦克朗", + "currency-display-name-dop": "多米尼加比索", + "currency-display-name-egp": "埃及镑", + "currency-display-name-etb": "埃塞俄比亚比尔", + "currency-display-name-eur": "欧元", + "currency-display-name-gbp": "英镑", + "currency-display-name-gel": "格鲁吉亚拉里", + "currency-display-name-ghs": "加纳塞地", + "currency-display-name-hkd": "港币", + "currency-display-name-hrk": "克罗地亚库纳", + "currency-display-name-huf": "匈牙利福林", + "currency-display-name-idr": "印度尼西亚卢比", + "currency-display-name-ils": "以色列新谢克尔", + "currency-display-name-inr": "印度卢比", + "currency-display-name-isk": "冰岛克朗", + "currency-display-name-jmd": "牙买加元", + "currency-display-name-jpy": "日元", + "currency-display-name-kes": "肯尼亚先令", + "currency-display-name-krw": "韩元", + "currency-display-name-kwd": "科威特第纳尔", + "currency-display-name-kzt": "哈萨克斯坦腾格", + "currency-display-name-lkr": "斯里兰卡卢比", + "currency-display-name-mad": "摩洛哥迪拉姆", + "currency-display-name-mdl": "摩尔多瓦列伊", + "currency-display-name-mur": "毛里求斯卢比", + "currency-display-name-mwk": "马拉维克瓦查", + "currency-display-name-mxn": "墨西哥比索", + "currency-display-name-myr": "马来西亚林吉特", + "currency-display-name-mzn": "莫桑比克美提卡", + "currency-display-name-nad": "纳米比亚元", + "currency-display-name-ngn": "尼日利亚奈拉", + "currency-display-name-nok": "挪威克朗", + "currency-display-name-npr": "尼泊尔卢比", + "currency-display-name-nzd": "新西兰元", + "currency-display-name-omr": "阿曼里亚尔", + "currency-display-name-pen": "秘鲁新索尔", + "currency-display-name-pgk": "巴布亚新几内亚基那", + "currency-display-name-php": "菲律宾比索", + "currency-display-name-pkr": "巴基斯坦卢比", + "currency-display-name-pln": "波兰兹罗提", + "currency-display-name-pyg": "巴拉圭瓜拉尼", + "currency-display-name-qar": "卡塔尔里亚尔", + "currency-display-name-ron": "罗马尼亚列伊", + "currency-display-name-rsd": "突尼斯第纳尔", + "currency-display-name-rub": "俄罗斯卢布", + "currency-display-name-sar": "沙特阿拉伯里亚尔", + "currency-display-name-sek": "瑞典克朗", + "currency-display-name-sgd": "新加坡元", + "currency-display-name-thb": "泰国泰铢", + "currency-display-name-try": "土耳其里拉", + "currency-display-name-ttd": "特立尼达和多巴哥元", + "currency-display-name-twd": "新台币", + "currency-display-name-tzs": "坦桑尼亚先令", + "currency-display-name-uah": "乌克兰格里夫纳", + "currency-display-name-ugx": "乌干达先令", + "currency-display-name-usd": "美元", + "currency-display-name-uyu": "乌拉圭比索", + "currency-display-name-vef": "委内瑞拉玻利瓦尔", + "currency-display-name-vnd": "越南盾", + "currency-display-name-zar": "南非兰德", + "current-network": "当前网络", + "current-pin": "输入6位数字密码", + "current-pin-description": "输入您的6位数字密码以继续", + "custom": "自 定义", + "custom-networks": "自定义网络", + "dapp": "ÐApp", + "dapp-would-like-to-connect-wallet": "想连接到您的钱包吗", + "dapps": "ÐApps", + "dapps-permissions": "DApp权限", + "data": "数据", + "datetime-ago": "前", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-day": { + "one": "", + "other": "天" + }, + "datetime-hour": { + "one": "", + "other": "小时" + }, + "datetime-minute": { + "one": "", + "other": "分钟" + }, + "datetime-second": { + "one": "", + "other": "秒" + }, + "datetime-today": "今天", + "datetime-yesterday": "昨天", + "decimals": "小数点", + "decryption-failed-content": "我们无法解密您的数据,您可能需要创建新帐户并且点击 “应用” 来清除旧数据。如果点击 “取消”,系统将再尝试一次。", + "default": "默认", + "delete": "刪除", + "delete-and-leave-group": "删除并离开群组", + "delete-bootnode": "删除引导节点", + "delete-bootnode-are-you-sure": "您确定要删除这个引导节点吗?", + "delete-bootnode-title": "删除 bootnode", + "close-chat": "刪除聊天", + "close-chat-confirmation": "您确定要删除此聊天记录吗?", + "delete-confirmation": "刪除", + "delete-mailserver": "删除邮件服务器", + "delete-mailserver-are-you-sure": "您确定要删除此邮件服务器吗?", + "delete-mailserver-title": "删除邮件服务器", + "delete-message": "删除消息", + "delete-my-account": "删除我的账户", + "delete-network-confirmation": "您确定要删除此网络吗?", + "delete-network-error": "请在删除此网络之前连接到其他网络", + "delete-network-title": "删除网络?", + "deny": "拒绝", + "description": "简介", + "dev-mode": "开发模式", + "dev-mode-settings": "开发模式设置", + "device-syncing": "设备同步", + "devices": "设备", + "disable": "禁用", + "disabled": "禁用", + "disconnected": "正在连接到对等方..。", + "discover": "发现", + "dismiss": "解雇", + "done": "完成", + "edit": "编辑", + "edit-profile": "编辑个人资料", + "empty-chat-description": "此聊天中还没有消息", + "empty-chat-description-one-to-one": "您在这里发送的任何邮件都是加密的, 只能由您读取并", + "empty-chat-description-public": "最近{{quiet-hours}}这里没有消息 。开始对话或", + "empty-chat-description-public-share-this": "分享此聊天。", + "enable": "启用", + "encrypt-with-password": "用密码加密", + "ens-10-SNT": "10 SNT", + "ens-add-username": "添加用户名", + "ens-agree-to": "同意", + "ens-chat-settings": "聊天设置", + "ens-custom-domain": "自定义域名", + "ens-custom-username-hints": "输入完整用户名,包括自定义域,例如username.domain.eth", + "ens-custom-username-taken": "用户名不属于您:(", + "ens-deposit": "充值", + "ens-displayed-with": "您的消息会显示给其他人", + "ens-get-name": "获取通用用户名", + "ens-got-it": "好的, 明白了", + "ens-locked": "用户名已锁定。您将无法在{{date}}之前释放它", + "ens-network-restriction": "仅适用于主网或ropsten", + "ens-no-usernames": "您没有关联任何用户名", + "ens-powered-by": "由以太坊名称服务提供支持", + "ens-primary-username": "主用户名", + "ens-register": "注册", + "ens-registration-failed": "要注册用户名,请重试。", + "ens-registration-failed-title": "交易失败", + "ens-release-username": "释放用户名", + "ens-remove-hints": "删除将从密钥中分离用户名。", + "ens-remove-username": "删除用户名", + "ens-saved": " 现在已与您的密钥相关联,可以在Status中使用了。", + "ens-saved-title": "用户名已添加", + "ens-show-username": "在聊天中显示我的ENS用户名", + "ens-terms-header": "名称注册条款", + "ens-terms-point-1": "资金存入1年。您的 snt 将被锁定, 但不会被花费。", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e(ENS注册表)。", + "ens-terms-point-2": "1 年后,您可以释放名称并赎回您的押金,或者不采取任何措施保留该名称。", + "ens-terms-point-3": "如果合约条款发生变化(例如,Status升级合约),用户有权释放用户名,无论持有时间长短。", + "ens-terms-point-4": "合约控制者无法访问您缴纳的押金。它们只能原路返回发送它们的钱包地址。", + "ens-terms-point-5": "您的地址将与您的ENS名称公开关联。", + "ens-terms-point-6": "用户名创建为stateofus.eth的子节点,并受ENS智能合约条款的约束。", + "ens-terms-point-7": "您授权合约代表您转发 SNT。仅当您批准交易以授权转移时,才会出现这种情况。", + "ens-terms-point-8": "这些条款由地址的智能合约逻辑保证:", + "ens-terms-point-9": "{{address}} (Status用户名注册器)", + "ens-terms-registration": "名称注册条款。", + "ens-test-message": "嘿", + "ens-transaction-pending": "交易待确定......", + "ens-understand": "我明白我的钱包地址将公开连接到我的用户名。", + "ens-username": "ENS用户名", + "ens-username-connected": "此用户名归您所有,并与您的聊天密钥相关联。", + "ens-username-hints": "至少 4 个字符。仅限拉丁字母、数字和小写字母。", + "ens-username-invalid": "只限字母和数字", + "ens-username-owned": "✓用户名归您所有。", + "ens-username-available": "✓用户名可用!", + "ens-username-taken": "用户名已被占用:(", + "ens-usernames": "ENS用户名", + "ens-usernames-details": "注册通用用户名,以便其他用户轻松找到你", + "wallet-address": "钱包地址", + "ens-want-custom-domain": "我在另一个域名上拥有一个名称", + "ens-want-domain": "我想要一个stateofus.eth域名", + "ens-welcome-hints": "ENS名称将那些冗长的地址转换为唯一的用户名。", + "ens-your-username": "您的用户名", + "ens-your-usernames": "您的用户名", + "ens-your-your-name": "您的ENS名称", + "enter-12-words": "输入您的12个助记词,并使用单个空格隔开", + "enter-contact-code": "输入联系人代码或用户名", + "enter-pair-code": "输入配对码", + "enter-pair-code-description": "在Keycard设置期间显示配对代码", + "enter-password": "输入密码", + "enter-pin": "输入6位数字密码", + "enter-puk-code": "输入PUK码", + "enter-puk-code-description": "6位数字密码已锁定。 \n请输入PUK码以解锁密码。", + "enter-seed-phrase": "输入助记词", + "enter-url": "输入网址", + "enter-word": "输入单词", + "enter-your-code": "输入你的6位数密码", + "enter-your-password": "输入你的密码......", + "error": "错误", + "error-unable-to-get-balance": "无法获取余额", + "error-unable-to-get-prices": "货币转换错误,请刷新屏幕以重试。", + "error-unable-to-get-token-balance": "无法获取代币余额", + "errors": "错误", + "eth": "ETH", + "ethereum-node-started-incorrectly-description": "以太网节点启动配置不正确,应用程序将停止从该条件恢复。已配置的网络ID = {{network-id}} ,实际网络ID = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "以太坊节点启动不正确", + "etherscan-lookup": "在Etherscan上查看", + "export-account": "导出帐户", + "failed": "失败", + "faq": "常见问题", + "fetch-messages": "↓获取消息", + "find": "查找", + "finish": "完成", + "finishing-card-setup": "卡片设置完成", + "fleet": "组别", + "fleet-settings": "组别设置", + "from": "从", + "gas-limit": "Gas 限制", + "gas-price": "Gas 价格", + "gas-used": "使用的 Gas", + "generate-a-key": "生成一个新密钥", + "generate-a-new-account": "生成一个新密钥", + "generate-a-new-key": "生成一个新密钥", + "generate-account": "生成密钥", + "generate-new-key": "生成一个新密钥", + "generating-codes-for-pairing": "正在生成与您的Status帐户配对的代码。", + "generating-keys": "正在生成密钥......", + "generating-mnemonic": "生成助记词", + "get-started": "开始", + "get-status-at": "在http:\/\/status.im获取Status", + "get-stickers": "获取贴纸", + "go-to-settings": "前往设置", + "got-it": "知道了", + "group-chat": "群聊", + "group-chat-admin": "管理员", + "group-chat-admin-added": "* {{member}} *已成为管理员", + "group-chat-created": "* {{member}} *创建了群* {{name}} *", + "group-chat-decline-invitation": "拒绝邀请", + "group-chat-member-added": "* {{member}} *加入了群", + "group-chat-member-joined": "* {{member}} *已加入该群组", + "group-chat-member-removed": "* {{member}} *离开了群", + "group-chat-members-count": "{{selected}} \/ {{max}}成员", + "group-chat-name-changed": "* {{member}} *将群名称更改为* {{name}} *", + "group-chat-no-contacts": "您还没有任何联系人。 \n邀请您的朋友开始聊天", + "group-info": "群信息", + "gwei": "GWEI", + "hash": "哈希", + "help": "帮助", + "help-capitalized": "帮助", + "help-center": "帮助中心", + "hide-content-when-switching-apps": "切换应用时隐藏内容", + "history": "历史记录", + "hold-card": "将卡放在手机背面", + "home": "消息", + "hooks": "挂钩", + "identifier": "识别码", + "image-remove-current": "删除当前照片", + "image-source-gallery": "从图库中选择", + "image-source-make-photo": "拍摄", + "image-source-title": "个人资料图片", + "in-contacts": "已添加联系人", + "incoming": "呼入", + "incorrect-code": [ + "str", + "抱歉,代码不正确,请重新输入" + ], + "initialization": "初始化", + "install": "安装", + "intro-message1": "欢迎来到Status\n点击该消息来设置你的密码并开始!", + "intro-privacy-policy-note1": "Status不会收集,共享或出售任何个人数据。你会继续同意", + "intro-privacy-policy-note2": "隐私政策", + "intro-text": "Status 是您通向分散网络的门户", + "intro-text1": "在以太坊区块链上安全地聊天和交易的开源平台", + "intro-text2": "您的帐户已设置完毕。请不要忘记在个人资料中备份助记词", + "intro-text3": "您的帐户已设置完毕。请不要忘记在个人资料中备份助记词", + "intro-title1": "私密,安全的沟通", + "intro-title2": "安全加密钱包", + "intro-title3": "去中心化应用程序", + "intro-wizard-text1": "您的身份在设计上是安全的。您将获得本地生成的加密密钥对", + "intro-wizard-text2": "此名称是您在Status中的身份。一旦选择,就无法更改。", + "intro-wizard-text3": "您的密钥存储在本地。没有副本。只有你有权访问。", + "intro-wizard-text4": "保护并加密您的密钥", + "intro-wizard-text6": "有新消息Status将通知您。您可以稍后在设置中编辑通知偏好设置", + "intro-wizard-title-alt4": "创建密码", + "intro-wizard-title-alt5": "确认你的密码", + "intro-wizard-title1": "先获取一个秘钥", + "intro-wizard-title2": "选择私钥和名称", + "intro-wizard-title3": "选择密钥存储", + "intro-wizard-title4": "创建一个6位数字密码", + "intro-wizard-title5": "确认密码", + "intro-wizard-title6": "启用通知", + "invalid-format": "无效的格式\n必须是{{format}}", + "invalid-key-confirm": "应用", + "invalid-key-content": "{{message}} \n \n 多账户的数据库无法加密,因为文件已损坏。没有办法恢复它。如果按“取消”按钮,则不会发生任何事情。如果按“ {{erase-multiaccounts-data-button-text}} ”按钮,将删除多账户的数据库,您将能够解锁多帐户。所有multiaccount的数据都将丢失。", + "invalid-number": "无效号码", + "invalid-pairing-password": "配对密码无效", + "invalid-range": "无效的格式\n必须是在{{min}}与{{max}}之间", + "invite-friends": "邀请朋友", + "invited": "已邀请", + "join-group-chat": "加入群", + "join-group-chat-description": "{{username}}邀请您加入群组{{group-name}}", + "joined-group-chat-description": "您通过来自{{username}}的邀请加入{{group-name}}", + "key": "密钥", + "keycard": "Keycard", + "keycard-applet-install-instructions": "要安装applet,请按照https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation上的说明进行操作。", + "keycard-blocked": "Keycard已被冻结。 \n您需要重置卡片才能继续使用它。", + "keycard-cancel-setup-text": "这将取消Keycard设置。强烈建议完成设置并使用Keycard。你真的想取消吗?", + "keycard-cancel-setup-title": "危险操作", + "keycard-desc": "仅适用于Android系统。您需要先获得一张Keycard", + "keycard-has-multiaccount-on-it": "此卡上已有帐户。如果您想更改它,请先登录并重置您的卡。如果您要导入Keycard帐户,请使用“添加现有帐户”", + "keycard-onboarding-finishing-header": "完成", + "keycard-onboarding-intro-header": "将密钥存储在keycard上", + "keycard-onboarding-intro-text": "准备好了,这可能需要几分钟,但保护您的帐户非常重要", + "keycard-onboarding-pairing-header": "正在配对卡片...", + "keycard-onboarding-preparing-header": "正在准备密钥卡", + "keycard-onboarding-puk-code-header": "写下代码\n并安全地存储它们", + "keycard-onboarding-recovery-phrase-description": "使用此助记词,您可以随时获取密钥。写下助记词。保持安全,离线存储并与此设备分开。", + "keycard-onboarding-recovery-phrase-header": "备份“种子”短语", + "keycard-onboarding-recovery-phrase-text": "这是用来生成密钥的种子。", + "keycard-onboarding-start-header": "把卡贴到手机后面并开始", + "keycard-onboarding-start-step1": "创建密码", + "keycard-onboarding-start-step1-text": "大约1分钟。创建一个6位数密码来加密您的密钥", + "keycard-onboarding-start-step2": "记下 PUK 码和配对码", + "keycard-onboarding-start-step2-text": "大约1分钟。你需要纸和笔", + "keycard-onboarding-start-step3": "备份助记词", + "keycard-onboarding-start-step3-text": "大约1分钟。还需要准备笔和纸", + "keycard-onboarding-start-text": "并在设置过程中保持卡与手机的接触。设置大约需要4分钟", + "keycard-recovery-intro-button-text": "开始恢复", + "keycard-recovery-intro-header": "恢复存储在密钥卡上的密钥", + "keycard-recovery-intro-text": "如果您之前使用密钥卡生成了密钥,现在想在此设备上使用此密钥", + "keycard-recovery-no-key-header": "无需恢复", + "keycard-recovery-no-key-text": "您的密钥卡上没有密钥存储。要使用它,请生成一个新密钥并选择您的密钥卡来存储密钥", + "keycard-recovery-phrase-confirm-header": "确认“种子”短语", + "keycard-recovery-phrase-confirmation-text": "以后无法再操作。如果丢失设备并且没有助记词,您将无法访问密钥或与之关联的任何资产。", + "keycard-recovery-phrase-confirmation-title": "已写下助记词?", + "keycard-recovery-success-header": "你的私钥已经成功恢复", + "keycard-unauthorized-operation": "您未经授权执行此操作。 \n请选择有效卡,然后重试。", + "language": "语言", + "learn-more": "学到更多", + "learn-more-about-keycard": "了解有关Keycard的更多信息", + "leave": "离开", + "leave-group": "退群", + "left": "剩余", + "les-ulc": "轻以太坊子协议\/超轻客户端", + "linked-on": "已与{{date}}关联", + "load-messages-before": "{{date}} 之前", + "load-more-messages": "↓获取更多消息", + "loading": "载入中...", + "log-level": "日志级别", + "log-level-settings": "日志级别设置", + "logging": "日志记录", + "logging-enabled": "启用日志记录吗?", + "login-pin-description": "输入您的6位数字密码并登录\n到您的帐户", + "logout": "登出", + "logout-app-content": "该帐户将被注销。再次登录时,将使用所选的网络", + "logout-are-you-sure": "您确定您要登出吗?", + "logout-title": "登出?", + "mailserver-address": "邮件服务器地址", + "mailserver-automatic": "自动选择", + "mailserver-connection-error": "无法连接到邮件服务器", + "mailserver-details": "邮件服务器详细信息", + "mailserver-error-content": "无法访问您选择的邮件服务器。", + "mailserver-error-title": "连接到邮件服务器时出错", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "选择其他邮件服务器", + "mailserver-reconnect": "无法连接到邮件服务器,请点击重新连接", + "mailserver-request-error-content": "邮件服务器返回以下错误: {{error}}", + "mailserver-request-error-status": "获取历史消息时发生错误,请检查日志以获取详细信息", + "mailserver-request-error-title": "邮件服务器请求错误", + "mailserver-request-retry": "重试请求", + "mailserver-retry": "重试", + "main-currency": "主要货币", + "main-networks": "主网", + "main-wallet": "主钱包", + "mainnet-network": "主网", + "make-admin": "设置管理员", + "members": { + "one": "", + "other": "{{count}} 个成员" + }, + "members-active": { + "one": "", + "other": "{{count}} 个成员" + }, + "members-active-none": "没有成员", + "members-title": "成员", + "message": "信息", + "message-not-sent": "信息未发送", + "message-options-cancel": "取消", + "message-reply": "回复", + "message-syncing": "消息同步", + "messages": "消息", + "might-break": "可能会破坏一些ÐApp", + "migrations-failed-content": "{{message}} \n架构版本:初始{{initial-version}} ,当前{{current-version}} ,最后{{last-version}} \n\n请在#status公共聊天中告诉我们此问题。如果按“取消”按钮,则不会产生任何后果。如果按“ {{erase-multiaccounts-data-button-text}} ”按钮,帐户的数据库将被删除,您将能够解锁帐户。所有帐户的数据都将丢失。", + "mobile-network-ask-me": "当在移动网络上时询问我", + "mobile-network-continue-syncing": "继续同步", + "mobile-network-continue-syncing-details": "您可以稍后在设置中更改此设置", + "mobile-network-go-to-settings": "前往设置", + "mobile-network-settings": "移动数据", + "mobile-network-sheet-configure": "您可以配置更多同步\n细节", + "mobile-network-sheet-offline": "离线,等待Wi-Fi", + "mobile-network-sheet-offline-details": "使用移动网络同步已关闭", + "mobile-network-sheet-remember-choice": "记住我的选择", + "mobile-network-sheet-settings": "设置", + "mobile-network-start-syncing": "开始同步", + "mobile-network-stop-syncing": "停止同步", + "mobile-network-stop-syncing-details": "直到连接到Wi-Fi?", + "mobile-network-use-mobile": "使用移动数据", + "mobile-network-use-mobile-data": "在同步聊天时,Status 会使用大量数据流量。您可以选择不在移动网络上同步", + "mobile-network-use-wifi": "仅限Wi-Fi", + "mobile-syncing-sheet-details": "在同步聊天时,Status 会使用大量数据流量。您可以选择不在移动网络上同步", + "mobile-syncing-sheet-title": "使用移动数据同步", + "more": "更多", + "multiaccounts-recover-enter-phrase-text": "输入12,15,18,21或24个单词。 \n用单个空格分隔单词。", + "multiaccounts-recover-enter-phrase-title": "输入您的助记词", + "name": "名称", + "name-of-token": "代币的名称", + "need-help": "需要帮助?", + "network": "网络", + "network-chain": "网络链", + "network-details": "网络详细信息", + "network-fee": "网络费", + "network-id": "网络 ID", + "network-invalid-network-id": "指定的网络ID与RPC url的网络ID不对应", + "network-invalid-status-code": "无效状态码: {{code}}", + "network-invalid-url": "无效网络网址", + "network-settings": "网络设置", + "new": "新建", + "new-chat": "新的聊天", + "new-contact": "新的联系人", + "new-contract": "新合约", + "new-group": "新增群组", + "new-group-chat": "新的群聊", + "new-network": "新建网络", + "new-pin-description": "输入新的6位数字密码", + "new-public-group-chat": "加入公共聊天", + "next": "下一个", + "no": "否", + "no-collectibles": "没有可用的收藏品", + "no-contacts": "还没有联系人", + "no-keycard-applet-on-card": "卡上没有Keycard小程序", + "no-messages": "无消息", + "no-pairing-slots-available": "没有可用配对位。 \n你可以解除已经与卡配对的设备配对", + "no-result": "没有结果", + "no-tokens-found": "没有找到代币", + "node-info": "节点信息", + "nonce": "随机数", + "none": "无", + "not-applicable": "对未签名交易不可用", + "not-keycard-text": "您使用的卡不是密钥卡。您需要购买密钥卡才能使用它", + "not-keycard-title": "不是一个密钥卡", + "notifications": "通知", + "notify": "通知", + "off": "关闭", + "offline": "离线", + "offline-messaging": "邮件服务器", + "offline-messaging-settings": "邮件服务器设置", + "ok": "确定", + "ok-continue": "好的,继续", + "ok-got-it": "好的, 明白了", + "okay": "确定", + "on": "开启", + "open": "打开", + "open-dapp": "打开ÐApp", + "open-dapp-store": "发现ÐApps", + "open-nfc-settings": "打开NFC设置", + "open-on-etherscan": "打开 Etherscan.io", + "optional": "可选", + "outgoing": "呼出", + "pair": "配对设备", + "pair-card": "配对卡", + "pair-code": "配对码", + "pair-code-explanation": "将卡与另一台具有相同Status多帐户的设备配对。", + "pair-this-card": "配对此卡", + "pair-this-device": "配对此设备", + "pair-this-device-description": "将您的设备配对以同步联系人并与他们聊天", + "paired-devices": "配对设备", + "pairing": "配对", + "pairing-card": "配对卡", + "pairing-go-to-installation": "配对设置", + "pairing-maximum-number-reached-content": "请在启用新设备之前禁用其中一个设备。", + "pairing-maximum-number-reached-title": "达到最大设备数", + "pairing-new-installation-detected-content": "已检测到新设备。 \n为了正确使用您的设备,在使用之前配对并启用它们。 \n请找到设置菜单下的设备项以配对您的设备。", + "pairing-new-installation-detected-title": "检测到新设备", + "pairing-no-info": "没有信息", + "pairing-please-set-a-name": "请为您的设备设置名称。", + "passphrase": "密码", + "password": "密码", + "password_error1": "密码不匹配。", + "password-description": "至少6个字符。您需要此密码才能打开Status并确认交易", + "password-placeholder2": "确认你的密码", + "paste": "粘贴", + "paste-json": "粘贴 JSON", + "pay-to-chat": "付钱聊天", + "peers": "对等方", + "pending": "等待中", + "pending-confirmation": "待确认...", + "permissions": "权限", + "phone-e164": "国际1", + "photos-access-error": "要授予所需的照片许可,请转到系统设置,并确保选中了 “Status” > “照片”。", + "pin-changed": "6位数字密码已更改", + "pin-code": "6位数字密码", + "pin-mismatch": "6位数字密码不匹配", + "pin-retries-left": "您还剩{{number}}次重试机会", + "preview-privacy": "预览隐私模式", + "privacy": "隐私", + "privacy-and-security": "隐私和安全", + "privacy-policy": "隐私权政策", + "processing": "处理中", + "product-information": "产品信息", + "profile": "个人资料", + "public-chat": "公聊", + "public-chats": "公共聊天", + "public-group-status": "公共", + "public-group-topic": "话题", + "public-key": "公钥", + "puk-and-pairing-codes-displayed": "显示PUK和配对码", + "puk-code": "PUK码", + "puk-code-explanation": "如果您忘记了6位数字密码或输入错误3次,则需要使用此代码解锁您的卡。", + "puk-mismatch": "PUK码不匹配", + "quiet-days": "{{quiet-days}}天", + "quiet-hours": "{{quiet-hours}}小时", + "re-encrypt-key": "重新加密密钥", + "receive": "接收", + "receive-transaction": "接收交易", + "recent": "最近", + "recent-recipients": "联系人", + "recently-used-stickers": "最近使用的贴纸将出现在这里", + "recipient": "接收方", + "recipient-code": "输入收件人地址", + "recover": "恢复", + "recover-key": "恢复密钥", + "recover-keycard-multiaccount-not-supported": "不支持使用密码恢复keycard帐户", + "recover-with-keycard": "使用Keycard恢复", + "recovering-key": "正在恢复密钥...", + "recovery-confirm-phrase": "确认密码", + "recovery-phrase": "助记词", + "recovery-success-text": "您必须创建新的代码或密码才能重新加密密钥", + "recovery-typo-dialog-description": "如果输入错误的单词,您将创建一个新帐号,而不是恢复旧帐号。", + "recovery-typo-dialog-title": "此助记词正确吗?", + "remember-me": "记住我的选择", + "remind-me-later": "请稍后提示我", + "remove": "删除", + "remove-from-chat": "从聊天中删除", + "remove-network": "移除网络", + "remove-token": "删除代币", + "removed": "已删除", + "repeat-pin": "重复你的新的6位数字密码", + "report-bug-email-template": "1.问题描述\n {{description}} \n\n\n 2.重现的步骤\n {{steps}} \n\n\n 3.附上可以演示问题的屏幕截图\n", + "request-transaction": "请求交易", + "required-field": "必填字段", + "resend-message": "重新发送", + "reset-card": "重置卡片", + "reset-card-description": "此操作将卡片重置为初始状态。它将清除所有数据,包括私钥。操作不可逆。", + "retry": "重试", + "rpc-url": "RPC 链接", + "save": "保存", + "save-password": "保存密码", + "save-password-unavailable": "设置设备码以保存密码", + "save-password-unavailable-android": "保存密码不可用:您的设备可能已被root或缺少必要的安全功能。", + "scan-qr": "扫描二维码", + "scan-qr-code": "扫描钱包地址的 QR 码", + "search": "搜索", + "secret-keys-confirmation-text": "万一您丢失了手机,您将需要它们以继续使用您的Keycard。", + "secret-keys-confirmation-title": "你抄写下来了吗?", + "security": "安全", + "see-details": "查看详细信息", + "see-it-again": "再次核对", + "select-chat": "选择一个聊天对象开始聊天", + "selected": "选择", + "send-logs": "报告一个错误", + "send-logs-to": "向{{email}}报告错误", + "send-message": "发送消息", + "send-request": "发送请求", + "send-request-amount": "金额", + "send-request-amount-max-decimals": "最大小数位数为{{asset-decimals}}", + "send-request-unknown-token": "未知代币 - {{asset}}", + "send-sending-to": "到{{recipient-name}}", + "send-transaction": "发送交易", + "sending": "正在发送...", + "sent-at": "发送于", + "set-a-topic": "创建主题", + "set-currency": "设置货币", + "set-dapp-access-permissions": "设置DApp访问权限", + "settings": "设置", + "share": "分享", + "share-address": "分享地址", + "share-chat": "共享聊天", + "share-contact-code": "共享我的联系人代码", + "share-dapp-text": "查看我在Status上使用的这个DApp: {{link}}", + "share-link": "共享链接", + "share-my-profile": "分享我的个人资料", + "share-profile": "分享个人资料", + "share-profile-link": "共享配置文件链接", + "share-public-chat-text": "在Status应用中查看此公共聊天: {{link}}", + "sharing-copied-to-clipboard": "复制到剪贴板", + "sharing-copy-to-clipboard": "复制到剪贴板", + "sharing-share": "分享...", + "show-less": "收起", + "show-more": "显示更多", + "show-qr": "显示二维码", + "sign-in": "登录", + "sign-message": "签名消息", + "sign-out": "注销", + "sign-with": "签名", + "sign-with-password": "使用密码签名", + "sign-you-in": "签入..。", + "signing": "签名", + "signing-a-message": "输入讯息...", + "signing-phrase": "签名短语", + "something-went-wrong": "出错", + "soon": "不久", + "specify-address": "指定地址", + "specify-name": "指定名称", + "specify-network-id": "指定网络ID", + "specify-rpc-url": "指定一个 RPC 链接", + "start-chat": "开始聊天", + "start-conversation": "开始对话", + "start-group-chat": "开始群聊", + "start-new-chat": "开始新对话", + "status": "状态", + "status-confirmed": "确定", + "status-hardwallet": "Status硬件钱包", + "status-keycard": "Status Keycard", + "status-not-sent-click": "未发送", + "status-not-sent-tap": "未发送。点击选项", + "status-pending": "等待中", + "status-sent": "已发送", + "status-tx-not-found": "未找到 TX", + "step-i-of-n": "步骤{{step}}\/{{number}}", + "sticker-market": "贴纸市场", + "submit": "提交", + "submit-bug": "提交错误", + "success": "成功", + "symbol": "符号", + "sync-all-devices": "同步所有设备", + "sync-in-progress": "正在同步...", + "sync-settings": "同步设置", + "sync-synced": "已同步", + "syncing-devices": "正在同步...", + "tag-was-lost": "标签丢失", + "test-networks": "测试网络", + "text-input-disabled": "请稍候...", + "this-device": "此设备", + "this-device-desc": "您的私钥将被加密并安全存储", + "this-is-you-signing": "这是你的签名短语", + "this-will-take-few-seconds": "这将需要几秒钟", + "three-words-description": "在每笔交易签名之前, 您可以看到这三个词语", + "three-words-description-2": "如果您看到不同的组合, 请取消交易并注销。", + "to": "到", + "to-block": "屏蔽", + "to-encrypt-enter-password": "请输入密码以对您的账户加密", + "to-see-this-message": "查看此消息,", + "token-auto-validate-decimals-error": "地址{{address}}中的代币 {{symbol}}小数位错误 - 设置为{{expected}}但检测为{{actual}}", + "token-auto-validate-name-error": "地址{{address}}中的代币 {{symbol}}名称错误 - 设置为{{expected}}但检测为{{actual}}", + "token-auto-validate-symbol-error": "地址{{address}}中的代币 {{symbol}}符号错误 - 设置为{{expected}}但检测为{{actual}}", + "token-details": "代币详细信息", + "topic-name-error": "仅使用小写字母(a到z),数字和短划线( - )。不要使用聊天码", + "transaction": "交易", + "transaction-description": "请等待达到至少12个确认,来确保交易已被安全地处理。", + "transaction-details": "交易明细", + "transaction-failed": "交易失败", + "transaction-history": "交易记录", + "transaction-request": "交易请求", + "transaction-sent": "发送的交易", + "transactions": "交易", + "transactions-filter-select-all": "全部选中", + "transactions-filter-title": "过滤历史记录", + "type": "类型", + "transactions-history": "历史记录", + "transactions-history-empty": "暂无交易历史记录", + "transactions-sign": "签名", + "tribute-required-by-multiaccount": "{{multiaccount-name}}要求SNT开始聊天。", + "tribute-state-paid": "Tribute已付款", + "tribute-state-pending": "见面礼等待中", + "tribute-state-required": "需要{{snt-amount}} SNT致敬", + "tribute-to-talk": "见面礼对话", + "tribute-to-talk-add-friends": "添加朋友作为联系人以允许聊天而无需支付费用。", + "tribute-to-talk-are-you-friends": "你们是朋友吗?", + "tribute-to-talk-ask-to-be-added": "请求添加为联系人", + "tribute-to-talk-contact-received-your-tribute": "收到你的见面礼。你可以安全地互相聊天。", + "tribute-to-talk-desc": "通过收取陌生人首次与你聊天时支付的SNT,来货币化你的注意力。", + "tribute-to-talk-disabled": "见面礼对话未启用", + "tribute-to-talk-disabled-note": "从现在开始,新人可以在不发送SNT的情况下与您聊天。", + "tribute-to-talk-enabled": "您已启用Tribute to Talk。", + "tribute-to-talk-finish-desc": "从现在开始,您只会收到来自联系人和付款人的聊天信息 ", + "tribute-to-talk-learn-more-1": "您的时间和精力是您最宝贵的资产。 Tribute to Talk可让您设置陌生人与您开始聊天所需的SNT数量。", + "tribute-to-talk-learn-more-2": "任何不在您的联系人列表中的人都会被要求付款,您可以在他们付款后回复。", + "tribute-to-talk-learn-more-3": "您可以随时退款,但为了确保朋友可以自由联系您,请先将其添加为联系人。", + "tribute-to-talk-paywall-learn-more-1": "我们的时间和精力是我们最宝贵的资产。 Tribute to Talk让您联系新朋友以换取SNT付款。", + "tribute-to-talk-paywall-learn-more-2": "要与设置了Tribute的人开始聊天,只需支付所需的SNT,您就会被添加为联系人。", + "tribute-to-talk-paywall-learn-more-3": "如果你认识他们,则可以在Status之外分享您的个人资料以免费添加好友。", + "tribute-to-talk-pending": "见面礼等待确认", + "tribute-to-talk-pending-note": "见面礼交易正在等待网络确认。您可以在交易历史记录中查看其状态", + "tribute-to-talk-removing-note": "删除Tribute to Talk将允许陌生人在不发送SNT的情况下开始聊天。需要进行交易才能生效。", + "tribute-to-talk-set-snt-amount": "设置陌生人与你开始聊天所需的SNT数量", + "tribute-to-talk-signing": "正在等待签名交易", + "tribute-to-talk-transaction-failed-note": "交易失败,你的见面礼谈话设置未更改", + "tribute-to-talk-tribute-received1": "Tribute收到了. You and ", + "tribute-to-talk-tribute-received2": "现在是联系人,可以安全地互相聊天。", + "tribute-to-talk-you-require-snt": "您需要收到 SNT 才能允许新人与您聊天。", + "try-again": "再试一次", + "turn-nfc-on": "开启NFC", + "type-a-message": "输入讯息...", + "ulc-enabled": "超轻客户端启用", + "unable-to-read-this-code": "无法读取此代码", + "unblock-contact": "取消屏蔽联系人", + "unknown-status-go-error": "未知的 status-go 错误", + "unlock": "解锁", + "unpair-card": "未配对卡片", + "unpair-card-confirmation": "此操作将取消当前设备的卡。需要6位数字密码授权。你想继续吗?", + "unpaired-keycard-text": "您点按的Keycard与此手机无关联", + "unpaired-keycard-title": "卡片已从当前设备删除配对", + "update": "更新", + "url": "链接", + "usd-currency": "USD", + "use-valid-contact-code": "请输入或扫描有效的联系人代码或用户名", + "validation-amount-invalid-number": "金额不是一个有效的数字", + "validation-amount-is-too-precise": "金额太精确了。最大小数位数为{{decimals}} 。", + "version": "版本", + "view-cryptokitties": "在CryptoKitties中查看", + "view-cryptostrikers": "在 CryptoStrikers 中查看", + "view-etheremon": "在Etheremon中查看", + "view-gitcoin": "在Gitcoin中查看", + "view-profile": "查看个人资料", + "view-signing": "查看签名短语", + "view-superrare": "在 SuperRare 中查看", + "waiting-for-wifi": "历史记录同步离线, 等待 Wi-Fi。", + "waiting-for-wifi-change": "更改", + "waiting-to-sign": "正在等待签名交易......", + "wallet": "钱包", + "wallet-asset": "资产", + "wallet-assets": "资产", + "wallet-backup-recovery-title": "备份助记词", + "wallet-choose-recipient": "选择接收方", + "wallet-collectibles": "收藏品", + "wallet-insufficient-funds": "资金不足", + "wallet-insufficient-gas": "ETH不足以支付燃料费", + "wallet-invalid-address": "无效的地址: \n {{data}}", + "wallet-invalid-address-checksum": "地址错误: \n {{data}}", + "wallet-invalid-chain-id": "网络不匹配: \n {{data}}而当前链是{{chain}}", + "wallet-manage-assets": "管理资产", + "wallet-request": "请求", + "wallet-send": "发送", + "wallet-send-min-wei": "最小1wei", + "wallet-settings": "钱包设置", + "wallet-total-value": "总值", + "wallet-transaction-total-fee": "总费用", + "wants-to-access-profile": "想要访问您的个人资料", + "warning": "警告", + "warning-message": "抱歉,我们限制快速连续发送多条消息。请稍后再试", + "web-view-error": "糟糕,出错了", + "welcome-screen-text": "设置你的钱包,邀请朋友聊天\n并浏览流行的dapps!", + "welcome-to-status": "欢迎来到Status", + "welcome-to-status-description": "点按加号(+)按钮即可开始使用", + "word-n": "字# {{number}}", + "word-n-description": "为了确认您是否正确备份助记词,请在上方输入单词 #{{number}} 。", + "words-n": { + "one": "1个单词", + "other": "{{count}} 个单词" + }, + "write-down-and-store-securely": "写下来并安全的存储起来", + "wrong-address": "错误的地址", + "wrong-card": "错误的卡片", + "wrong-card-text": "您已点击了与您选择的多帐户不对应的卡片。请再试一次。", + "wrong-contract": "错误的合约", + "wrong-keycard-text": "您点按的Keycard与此手机无关联", + "wrong-keycard-title": "看起来你已经点击了\n一张空白的密钥卡", + "wrong-password": "密码错误", + "wrong-word": "错误的单词", + "yes": "是", + "You": "你", + "you": "你", + "you-already-have-an-asset": "您已拥有资产{{value}}", + "you-are-all-set": "你已完成所有设置", + "you-are-all-set-description": "现在,如果您丢失了手机,则可以使用助记词恢复您的帐号和钱包。", + "you-can-change-account": "您可以根据自己的喜好更改帐户的名称和颜色", + "you-dont-have-stickers": "你还没有任何贴纸", + "your-contact-code": "授予此DApp访问权限以查看您的聊天码", + "your-data-belongs-to-you": "如果你丢失了你的助记词, 你也同时失去了你的数据和资金", + "your-data-belongs-to-you-description": "如果您丢失了助记词,Status无法帮助您恢复多帐户。您需对数据的安全性负责,备份助记词是最佳保障。", + "your-recovery-phrase": "您的助记词", + "your-recovery-phrase-description": "这是你的助记词。已此来证明这是你的钱包。你只能查看一次!请将其写在纸上并保存在安全的地方。如果丢失或重新安装钱包,您将需要用到这些助记词。" +} diff --git a/translations/zh_TW.json b/translations/zh_TW.json new file mode 100644 index 00000000000..6f7c34b8e71 --- /dev/null +++ b/translations/zh_TW.json @@ -0,0 +1,1834 @@ +{ + "K": "K", + "M": "M", + "NFT": "NFT", + "You": "您", + "about-app": "關於", + "about-key-storage-content": "Status永遠不會詢問您的私鑰。確保備份您的種子詞組。如果丟失手機,這是取回金鑰的唯一方法。", + "about-key-storage-title": "關於金鑰保存", + "about-names-content": "沒有人可以冒充您!在預設值中,您是匿名的,永遠不必透露您的真實姓名。您只需支付一些費用即可註冊自定義名稱。", + "about-names-title": "名稱無法更改", + "about-sharing-data": "關於共享資料", + "accept": "接受", + "accept-and-add": "接受並新增", + "accept-and-continue": "接受並繼續", + "accept-and-share-address": "接受並分享地址", + "accept-community-rules": "我同意社区规则", + "accept-new-chats-from": "接受來自的新聊天", + "accept-status-tos-prefix": "我接受Status", + "accepted": "已接受", + "access-existing-keys": "使用既有金鑰", + "access-key": "使用金鑰", + "account-added": "帳戶已新增", + "account-color": "帳戶顏色", + "account-content": "您可以將 Status 中的帳號與銀行帳戶進行比較。像銀行帳戶一樣,帳號通常具有位址和餘額;您將使用此帳號在乙太坊上進行交易。您的錢包中可以有多個帳號。解鎖 Status 後即可進行存取。", + "account-exists-title": "帳號已存在", + "account-is-used": "該帳戶正在瀏覽器中與 Dapps 一起使用。", + "account-name": "帳號名稱", + "account-settings": "帳號設定", + "account-title": "帳號", + "accounts": "帳號", + "actions": "操作", + "active-members": "活跃成员", + "active-online": "線上", + "active-unknown": "未知", + "activity": "活動", + "add": "新增", + "add-a-watch-account": "新增監看地址", + "add-account": "新增收款地址", + "add-account-description": "您可以導入任何類型的乙太坊賬戶以將其新增到您的狀態錢包中", + "add-account-incorrect-password": "密碼似乎不正確。請輸入密碼來解鎖應用程式。", + "add-an-account": "新增帳號", + "add-bootnode": "新增引導節點", + "add-contact": "新增好友", + "add-custom-token": "新增自訂代幣", + "add-favourite": "加入書籤", + "add-mailserver": "新增Status節點", + "add-members": "新增成員", + "add-network": "新增網路", + "add-new-contact": "新增好友", + "add-nickname": "新增暱稱(選填)", + "add-node": "新增節點", + "add-private-key-account": "通過私鑰新增帳戶", + "add-seed-account": "使用種子詞組來新增帳號", + "add-to-contacts": "新增到好友名單", + "add-to-contacts-text": "通過將用戶新增到您的好友名單,您的錢包位址將顯示給對方", + "add-to-favourites": "加入收藏", + "add-watch-account": "新增監視帳號", + "added": "添加", + "address": "地址", + "address-or-ens-name": "地址或ENS名稱", + "address-received": "接收位址", + "address-request-accepted": "地址請求已接受", + "address-requested": "地址請求", + "admin": "管理員", + "advanced": "進階", + "advanced-settings": "進階設定", + "advertiser-description": "感謝您的好友將Status推薦給您。您是否介意Status檢查您目前的IP地址,以便獲得獎勵?該資訊將不會用於其他任何用途,並會在7天後將其完全刪除。", + "advertiser-starter-pack-accept": "接受", + "advertiser-starter-pack-decline": "拒絕", + "advertiser-starter-pack-description": "這是一些加密貨幣,可以幫助您入門!用它來獲得貼紙,ENS名稱並嘗試dapps", + "advertiser-starter-pack-title": "入門包", + "advertiser-title": "預設為隱密", + "agree-by-continuing": "繼續,表示您同意我們", + "all": "所有", + "all-connections": "所有连接", + "allow": "允許", + "allow-and-send": "允許並傳送", + "allow-mention-notifications": "顯示@", + "allowing-authorizes-this-dapp": "允許授權此DApp檢索您的錢包地址並啟用Web3", + "alphabetically": "按字母顺序排列", + "already-have-asset": "您已經擁有此資產", + "amount": "數目", + "and": "和", + "anyone": "任何人", + "app-commit": "App提交", + "app-connections": "应用程序连接", + "appearance": "版面外觀", + "apply": "執行", + "approve": "核准", + "approve-limit": "核准限額", + "approve-token": "核准代幣", + "approve-token-contract-desc": "核准代幣合約將會允許它花費您的代幣餘額。如果您覺得某個項目不值得信賴,請不要選擇核准代幣,或者只核准您需要使用的金額額度。", + "are-you-sure": "您確定嗎?", + "are-you-sure-description": "您將無法再看到完整的種子詞組", + "are-you-sure-to-cancel": "您確定要取消?", + "are-you-sure?": "您確定嗎?", + "ask-in-status": "提出問題或報告錯誤", + "at": "在", + "attribution-received": "收到{{attrib}}獎勵,上限為{{max}}", + "audio": "音效", + "audio-message": "音频消息", + "audio-recorder": "錄音機", + "audio-recorder-error": "錄音機錯誤", + "audio-recorder-max-ms-reached": "已達最長錄音時間", + "audio-recorder-permissions-error": "您必須授予發送語音訊息的權限", + "authorize": "授權", + "available": "可使用", + "available-participants": { + "other": "您還可以選擇 {{count}} 位參與者" + }, + "back": "返回", + "back-up": "備份", + "back-up-seed-phrase": "備份種子詞組", + "back-up-your-seed-phrase": "備份您的種子詞組", + "backing-up": "備份中...", + "backup-disabled": "禁用", + "backup-enabled": "啟用", + "backup-recovery-phrase": "備份種子詞組", + "backup-settings": "備份設定", + "backup-through-waku": "透過 waku 備份", + "bad-fees-description": "您的手續費用低於我們建議的參數。", + "balance": "帳戶餘額", + "begin-set-up": "開始設定", + "below-base-fee": "最高交易費用低於基本費用", + "biometric-auth-android-sensor-desc": "觸摸感應器", + "biometric-auth-android-sensor-error-desc": "失敗", + "biometric-auth-android-title": "需要驗證", + "biometric-auth-confirm-logout": "重新登入", + "biometric-auth-confirm-message": "需要進行生物識別身份驗證才能繼續,如果無法使用,請使用密碼來解鎖您的金鑰", + "biometric-auth-confirm-title": "您必須驗證!", + "biometric-auth-confirm-try-again": "再試一次", + "biometric-auth-error": "無法執行生物特徵認證( {{code}} )", + "biometric-auth-login-error-title": "生物特徵辨識錯誤", + "biometric-auth-login-ios-fallback-label": "輸入密碼", + "biometric-auth-reason-login": "登入Status", + "biometric-auth-reason-verify": "需要驗證", + "biometric-disable-bioauth": "禁用{{bio-type-label}}", + "biometric-disable-password-description": "如果禁用此功能,您還將", + "biometric-disable-password-title": "禁用密碼儲存", + "biometric-enable": "如果您不想每次都輸入密碼來啟動應用程式,請啟用{{bio-type-label}}登錄", + "biometric-enable-button": "啟用{{bio-type-label}}", + "biometric-enable-keycard": "如果您不想每次都使用金鑰卡來啟動應用程式,請啟用{{bio-type-label}}登錄", + "biometric-faceid": "Face ID", + "biometric-fingerprint": "指紋", + "biometric-secure-with": "使用{{bio-type-label}}", + "biometric-touchid": "Touch ID", + "bip39-password-placeholder": "BIP39密碼", + "blank-keycard-text": "生成金鑰和名稱後,即可繼續使用金鑰卡", + "blank-keycard-title": "您似乎使用\n空白的金鑰卡", + "blank-messages-text": "您的消息将在这里", + "block": "區塊", + "block-contact": "封鎖此用戶", + "block-contact-details": "封鎖將刪除該用戶以前的訊息,並阻擋傳送新訊息給您", + "block-user": "屏蔽用户", + "blocked-users": "已封鎖的黑名單", + "bootnode-address": "引導節點地址", + "bootnode-details": "引導節點詳細資訊", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "引導節點", + "bootnodes-enabled": "引導節點已啟用", + "bootnodes-settings": "引導節點設定", + "browsed-websites": "瀏覽器歷史記錄將顯示在此處", + "browser": "瀏覽器", + "browser-not-secure": "連線不安全!不要在此網站上簽署交易或發送個人數據。", + "browser-secure": "連線是安全的。在簽署交易或輸入個人資訊之前,請確保您確實信任此網站。", + "browsers": "瀏覽器", + "browsing-cancel": "取消", + "browsing-open-in-android-web-browser": "在Android中開啟", + "browsing-open-in-ios-web-browser": "在iOS中開啟", + "browsing-open-in-status": "在Status中開啟", + "browsing-site-blocked-description1": "我們從該地址偵測到潛在的惡意活動。為了保護您和您的錢包,我們已阻止進一步的瀏覽動作。 \n\n如果您認為這是一個錯誤,請回報至", + "browsing-site-blocked-description2": "開放的聊天", + "browsing-site-blocked-go-back": "返回", + "browsing-site-blocked-title": "該網站已被封鎖", + "browsing-title": "瀏覽", + "bug-report": "報告錯誤", + "bug-report-description": "* 描述", + "bug-report-description-placeholder": "必填,不能為空", + "bug-report-steps": "故障重現步驟", + "bug-report-steps-placeholder": "- 打開應用程序\n- 做了什麼點選\n- 然後是其他的...", + "bug-report-submit-email": "透過電子郵件傳送日誌存檔", + "bug-report-submit-gh-issue": "提送一個沒有日誌紀錄的 GitHub漏洞", + "bug-report-too-short-description": "描述太短", + "build-yourself": "若您不同意這些服務條款,您可以考慮建立自己的應用程式版本", + "buy-crypto": "購買加密貨幣", + "buy-crypto-choose-a-service": "選擇您想用來購買加密貨幣的服務", + "buy-crypto-description": "尋找能購買加密貨幣的dapp", + "buy-crypto-leaving": "您即將離開Status並進入第三方網站以完成購買", + "buy-crypto-title": "您的錢包似乎是空的", + "camera-access-error": "要開放所需的相機權限,請進入系統設定,並在應用程式Status>相機中進行變更。", + "can-not-add-yourself": "這是您,要開始聊天,請選擇其他人", + "can-send-messages": "您可以傳送和接收新訊息", + "cancel": "取消", + "cancel-keycard-setup": "取消金鑰卡設定", + "cancelling": "取消", + "cannot-pin-desc": "您最多只能置顶 3 条消息。\n至少取消置顶一个消息以置顶其他新消息。", + "cannot-pin-title": "您无法置顶此消息!", + "cannot-read-card": "無法讀取卡片。 \n請把它放在手機感應處", + "cannot-use-default-pin": "不允許輸入密碼000000\n請使用其他號碼", + "cant-open-public-chat": "無法開啟公共聊天", + "cant-report-bug": "無法報告錯誤", + "card-is-blank": "空白卡片", + "card-reseted": "卡片已重設", + "card-unpaired": "卡片已與當前設備取消配對", + "category": "類別", + "category-title": "類別標題", + "change-fleet": "將其更改為{{fleet}}", + "change-group-privacy": "更改群组隐私", + "change-logging-enabled": "您確定要{{enable}}登入嗎?", + "change-pairing": "更改配對碼", + "change-pairing-description": "更改配對代碼不會影響當前的配對。但是,往後任何新增配對都將需要新的代碼。", + "change-pairing-title": "建立一個新的配對代碼", + "change-passcode": "更改啟動密碼", + "change-password": "更改密碼", + "change-pin": "更改六位數密碼", + "change-puk": "更改12位數PUK", + "change-tip": "變更手續費", + "changed-amount-warning": "金額從{{old}}更改為{{new}}", + "changed-asset-warning": "資產已從{{old}}更改為{{new}}", + "chaos-mode": "混沌模式", + "chaos-unicorn-day": "Chaos Unicorn Day", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "聊天室", + "chat-and-transact": "與朋友私下聊天和交易", + "chat-is-a-contact": "聯絡人", + "chat-is-not-a-contact": "不是聯絡人", + "chat-key": "聊天金鑰", + "chat-key-content": "\"Status\"聊天協議使用加密金鑰傳送和接收有關的訊息。聊天公鑰是您與他人共用的字串,讓他們可以在“Status”中向您傳送訊息。", + "chat-key-title": "聊天金鑰", + "chat-link-previews": "聊天連結預覽", + "chat-name": "聊天名称", + "chat-name-content": "從聊天鍵中演算法得出的三個隨機詞,會當作聊天中的默認別名。聊天名稱是完全唯一的;沒有其他用戶可以使用相同的三個詞。", + "chat-name-title": "聊天名稱", + "chat-notification-preferences": "通知设置", + "chat-settings": "聊天設定", + "chats": "聊天室", + "check-on-block-explorer": "在區塊瀏覽器查看", + "check-on-opensea": "在OpenSea查看", + "check-your-recovery-phrase": "檢查您的種子詞組", + "choose-actions": "選擇操作", + "choose-authentication-method": "選擇一種身份驗證方法", + "choose-new-location-for-keystore": "選擇一個新位置以儲存您的密鑰檔案", + "choose-storage": "選擇保存", + "clear": "清除", + "clear-all": "全部清除", + "clear-history": "清除歷史記錄", + "clear-history-action": "清除", + "clear-history-confirmation": "清除歷史記錄?", + "clear-history-confirmation-content": "您確定要清除此聊天記錄嗎?", + "clear-history-title": "清除歷史記錄?", + "cleared-chat-description-public": "太安静了,开始交谈或者.", + "close": "關閉", + "close-all": "全部關閉", + "close-app-button": "確認", + "close-app-content": "應用程式即將中止並關閉。重新啟動時,將使用所選定的網路", + "close-app-title": "警告!", + "collectibles-leak-metadata": "您可以在此處顯示您的 NFT。如果這樣做,您將會分享您的錢包和 IP 地址", + "command-button-send": "發送", + "communities": "社群", + "communities-alpha": "社群(alpha)", + "communities-enabled": "啟用社群", + "communities-verified": "✓已驗證的Status社群", + "community": "社群", + "community-channel": "社区频道", + "community-color": "社群顏色", + "community-color-placeholder": "選擇一種顏色", + "community-edit-title": "編輯社群", + "community-emoji-thumbnail-title": "圖片縮圖", + "community-image-pick": "選擇一張圖片", + "community-image-remove": "移除", + "community-image-take": "拍攝照片", + "community-info": "社群資訊", + "community-info-not-found": "找不到社群資訊", + "community-invite-title": "邀請", + "community-key": "社群管理私鑰", + "community-key-placeholder": "輸入您的社群管理私鑰", + "community-link": "社群連接", + "community-members": { + "other": "{{count}}位成員" + }, + "community-members-title": "成員", + "community-message-preview": "邀請加入{{community-name}}", + "community-notification-settings": "通知设置", + "community-private-key": "社群管理私鑰", + "community-requests-to-join-title": "會員申請", + "community-roles": "角色", + "community-rules": "社区规则", + "community-share-title": "分享", + "community-thumbnail-image": "相片縮圖", + "community-thumbnail-upload": "上傳", + "complete-hardwallet-setup": "這張卡片已連結。您將需要它來簽署交易與解鎖金鑰。", + "completed": "完成", + "confirm": "確認", + "confirm-new-password": "確認新密碼", + "confirm-pairing-code-placeholder": "確認您的配對代碼...", + "confirm-password-placeholder": "確認密碼...", + "confirmation-request": "要求確認", + "confirmations": "確認", + "confirmations-helper-text": "當交易經過12次確認後,您可以視為已結算完成。", + "connect": "連結", + "connect-mailserver-content": "連結到{{name}}嗎?", + "connect-wallet": "連接錢包", + "connected": "已連結", + "connected-to": "連結到", + "connecting": "正在連線...", + "connecting-requires-login": "需要登入以連結到另一個網路", + "connection-request": "連結請求", + "connection-status": "連結狀態", + "connection-with-the-card-lost": "無法與卡片取得連結", + "connection-with-the-card-lost-setup-text": "要恢復設定,\n請將卡置於手機感應處\n並維持不動", + "connection-with-the-card-lost-text": "要繼續操作,請將卡保持在手機感應處", + "contact-code": "聊天金鑰", + "contact-request": "联络请求", + "contact-request-accepted": "已接受 ✓", + "contact-request-declined": "已拒绝 (x)", + "contact-request-header": "请求加为好友", + "contact-request-pending": "待定...", + "contact-request-sent": "已发送好友申请", + "contact-requests": "好友请求", + "contact-s": { + "other": "好友名單" + }, + "contacts": "好友名單", + "contacts-descr": "您的好友名單將出現在這裡。您將收到添加為好友的所有狀態更新", + "contacts-empty": "具有ENS名稱的聯絡人將顯示在此處", + "continue": "繼續", + "continue-anyway": "仍然繼續", + "contract-address": "合約地址", + "contract-interaction": "合約交互", + "contract-isnt-supported": "不支援該合约", + "copy-info": "複製訊息", + "copy-qr": "複製", + "copy-text": "复制文本", + "copy-to-clipboard": "複製", + "copy-transaction-hash": "複製交易編號", + "cost-fee": "支出\/交易費用", + "counter-9-plus": "9+", + "counter-99-plus": "99+", + "create": "建立", + "create-a-pin": "創建一個六位數密碼", + "create-a-puk": "建立一個12位數的PUK", + "create-category": "建立分類", + "create-channel": "建立頻道", + "create-channel-title": "新頻道", + "create-community": "建立社群", + "create-group-chat": "建立聊天室", + "create-multiaccount": "產生金鑰", + "create-new-key": "獲取新金鑰", + "create-pin": "建立六位數的密碼", + "create-pin-description": "將需要您的卡片和此六位元數密碼來解鎖Status以及確認交易", + "created-group-chat-description": "您新建了群組{{group-name}}", + "cryptokitty-name": "謎戀貓 #{{id}}", + "currency": "貨幣", + "currency-display-name-aed": "阿聯迪拉姆", + "currency-display-name-afn": "阿富汗幣", + "currency-display-name-ars": "阿根廷披索", + "currency-display-name-aud": "澳元", + "currency-display-name-bbd": "巴貝多元", + "currency-display-name-bdt": "孟加拉塔卡", + "currency-display-name-bgn": "保加利亞幣", + "currency-display-name-bhd": "巴林第納爾", + "currency-display-name-bnd": "汶萊元", + "currency-display-name-bob": "玻利維亞幣", + "currency-display-name-brl": "巴西幣", + "currency-display-name-btn": "不丹幣", + "currency-display-name-cad": "加拿大元", + "currency-display-name-chf": "瑞士法郎", + "currency-display-name-clp": "智利披索", + "currency-display-name-cny": "人民幣", + "currency-display-name-cop": "哥倫比亞披索", + "currency-display-name-crc": "哥斯大黎加科朗", + "currency-display-name-czk": "捷克克朗", + "currency-display-name-dkk": "丹麥克朗", + "currency-display-name-dop": "多明尼加共和國披索", + "currency-display-name-egp": "埃及鎊", + "currency-display-name-etb": "衣索比亞比爾", + "currency-display-name-eur": "歐元", + "currency-display-name-gbp": "英鎊", + "currency-display-name-gel": "喬治亞拉里", + "currency-display-name-ghs": "迦納幣", + "currency-display-name-hkd": "港元", + "currency-display-name-hrk": "克羅埃西亞庫納", + "currency-display-name-huf": "匈牙利福林", + "currency-display-name-idr": "印尼盾", + "currency-display-name-ils": "以色列謝克爾", + "currency-display-name-inr": "印度盧比", + "currency-display-name-isk": "冰島克朗", + "currency-display-name-jmd": "牙買加元", + "currency-display-name-jpy": "日圓", + "currency-display-name-kes": "肯亞先令", + "currency-display-name-krw": "韓元", + "currency-display-name-kwd": "科威特第納爾", + "currency-display-name-kzt": "哈薩克堅戈", + "currency-display-name-lkr": "斯里蘭卡盧比", + "currency-display-name-mad": "摩洛哥迪拉姆", + "currency-display-name-mdl": "摩爾多瓦列伊", + "currency-display-name-mur": "模里西斯盧比", + "currency-display-name-mwk": "馬拉威克瓦查", + "currency-display-name-mxn": "墨西哥披索", + "currency-display-name-myr": "馬來西亞令吉", + "currency-display-name-mzn": "莫三比克梅蒂卡爾", + "currency-display-name-nad": "納米比亞元", + "currency-display-name-ngn": "奈及利亞奈拉", + "currency-display-name-nok": "挪威克朗", + "currency-display-name-npr": "尼泊爾盧比", + "currency-display-name-nzd": "紐西蘭元", + "currency-display-name-omr": "阿曼里亞爾", + "currency-display-name-pen": "秘魯幣", + "currency-display-name-pgk": "巴布亞新幾內亞幣", + "currency-display-name-php": "菲律賓披索", + "currency-display-name-pkr": "巴基斯坦盧比", + "currency-display-name-pln": "波蘭幣", + "currency-display-name-pyg": "巴拉圭幣", + "currency-display-name-qar": "卡達里亞爾", + "currency-display-name-ron": "羅馬尼亞列伊", + "currency-display-name-rsd": "塞爾維亞第納爾", + "currency-display-name-rub": "俄羅斯盧布", + "currency-display-name-sar": "沙烏地阿拉伯里亞爾", + "currency-display-name-sek": "瑞典克朗", + "currency-display-name-sgd": "新加坡元", + "currency-display-name-thb": "泰銖", + "currency-display-name-try": "土耳其里拉", + "currency-display-name-ttd": "千里達及托巴哥元", + "currency-display-name-twd": "新台幣", + "currency-display-name-tzs": "坦尚尼亞先令", + "currency-display-name-uah": "烏克蘭格里夫納", + "currency-display-name-ugx": "烏干達先令", + "currency-display-name-usd": "美金", + "currency-display-name-uyu": "烏拉圭披索", + "currency-display-name-vef": "委內瑞拉幣", + "currency-display-name-vnd": "越南盾", + "currency-display-name-zar": "南非幣", + "current": "目前", + "current-average": "目前平均值", + "current-average-tip": "目前平均手續費用", + "current-base": "目前基礎", + "current-base-fee": "目前基本費用", + "current-minimum-tip": "目前最低手續費用", + "current-network": "目前網路", + "current-password": "目前密碼", + "current-pin": "輸入六位數密碼", + "current-pin-description": "輸入您的六位數密碼以繼續", + "custom": "自訂", + "custom-networks": "自訂網路", + "custom-node": "您正在使用自訂RPC端點。您的本機交易記錄可能不完整。", + "custom-seed-phrase": "無效的種子詞組", + "custom-seed-phrase-text-1": "該種子詞組與我們支持的辭典不相符。請檢查拼寫錯誤的單字。", + "dapp": "ÐApp", + "dapp-starter-pack-accept": "接受並開啟", + "dapp-starter-pack-description": "這是一些加密貨幣,可以幫助您入門!用它來獲得貼紙,ENS名稱並嘗試Ðapps", + "dapp-starter-pack-title": "入門包", + "dapp-would-like-to-connect-wallet": "請求連結到", + "dapps": "ÐApps", + "dapps-permissions": "ÐApp權限", + "dark": "夜晚", + "data": "數據", + "data-collected": "資料已收集", + "data-collected-subtitle": "下表顯示將儲存和傳送的確切資料。資料根據公共規則進行驗證,以確保不會發送敏感資料。應隨時保持懷疑,進行驗證。", + "data-syncing": "資料同步中", + "database-reset-content": "對話紀錄、好友名單和設定已經被刪除。您可以將您的帳戶與金鑰卡一起使用", + "database-reset-title": "資料庫重置", + "database-reset-warning": "資料庫將被重置。現有聊天、好友名單和設定將被刪除", + "datetime-ago": "前", + "datetime-ago-format": "{{number}}{{time-intervals}}{{ago}}", + "datetime-ago-format-short": "{{number}}{{time-intervals}}", + "datetime-day": { + "other": "天" + }, + "datetime-day-short": { + "other": "天" + }, + "datetime-hour": { + "other": "小時" + }, + "datetime-hour-short": { + "other": "小時" + }, + "datetime-minute": { + "other": "分" + }, + "datetime-minute-short": { + "other": "分" + }, + "datetime-second": { + "other": "秒" + }, + "datetime-second-short": { + "other": "秒" + }, + "datetime-today": "今天", + "datetime-yesterday": "昨天", + "decimals": "小數點", + "decline": "拒絕", + "declined": "拒绝", + "decryption-failed-content": "解密數據時出現錯誤,您可能需要清除舊資料,並建立新的帳戶,請點選“同意”來清除舊資料,或點選“取消”以重試。", + "default": "預設", + "default-assets": "預設兼容ERC20和ERC721", + "default-sync-period": "同步歷史記錄", + "delete": "刪除", + "delete-account": "刪除帳號", + "delete-and-leave-group": "刪除並離開群組", + "delete-bootnode": "刪除引導節點", + "delete-bootnode-are-you-sure": "您確定要刪除該引導節點嗎?", + "delete-bootnode-title": "刪除引導節點", + "delete-category-confirmation": "您確定要刪除此類別嗎?", + "close-chat": "刪除聊天", + "close-chat-confirmation": "您確定要刪除此對話嗎?", + "delete-confirmation": "刪除?", + "delete-for-everyone": "为所有人删除", + "delete-for-me": "为我删除", + "delete-keys-keycard": "從金鑰卡中刪除私鑰", + "delete-mailserver": "刪除Status節點", + "delete-mailserver-are-you-sure": "您確定要刪除此Status節點嗎?", + "delete-mailserver-title": "刪除Status節點", + "delete-message": "刪除留言", + "delete-my-account": "刪除我的賬號", + "delete-my-profile": "刪除我的個人資料", + "delete-network-confirmation": "您確定要刪除此網路嗎?", + "delete-network-error": "請先連結到其他網路,然後再刪除這個網路", + "delete-network-title": "刪除網路?", + "delete-node": "刪除節點", + "delete-node-are-you-sure": "您確定要刪除此節點嗎?", + "delete-node-title": "刪除節點", + "delete-profile": "刪除個人資料", + "delete-profile-warning": "警告:如果您沒有記下種子詞組,在刪除個人資料後,您將失去對資金的訪問權限", + "deny": "拒絕", + "derivation-path": "推導路徑", + "describe-channel": "頻道簡介", + "description": "描述", + "dev-mode": "開發人員模式", + "dev-mode-settings": "開發模式設定", + "device-syncing": "設備同步", + "devices": "裝置", + "disable": "禁用", + "disable-all": "全部停用", + "disable-later-in-settings": "您可以稍後在“設定”中禁用此功能", + "disabled": "禁用", + "disconnect": "中斷連結", + "disconnected": "離線聊天", + "discover": "發現", + "discover-communities": "发现社区", + "dismiss": "返回", + "display-collectibles": "展示收藏", + "done": "完成", + "dont-ask": "不再詢問我", + "edit": "編輯", + "edit-categories": "編輯分類", + "edit-channel-title": "編輯頻道", + "edit-chats": "編輯聊天", + "edit-community": "編輯社群", + "edit-favourite": "編輯書籤", + "edit-group": "編輯群組", + "edit-message": "編輯訊息", + "edit-name-and-image": "編輯名稱和圖像", + "edit-name-image": "編輯名稱和圖像", + "edit-nickname": "修改昵称", + "edit-profile": "編輯個人資料", + "edited": "已編輯", + "editing-message": "編輯訊息", + "empty-activity-center": "您的聊天通知會出現在此", + "empty-chat-description": "聊天室中沒有任何訊息", + "empty-chat-description-community": "在過去{{quiet-hours}}中沒有新的留言。", + "empty-chat-description-one-to-one": "您在此發送的所有訊息都是經過加密的,只有您和", + "empty-chat-description-public": "過去{{quiet-hours}}中,這裡沒有新留言。開始對話或", + "empty-chat-description-public-share-this": "分享此對話。", + "empty-keycard-required": "需要空白的金鑰卡", + "empty-pending-invitations-descr": "透過邀請連結申請加入群組的人員將顯示在此", + "empty-tab": "空白分頁", + "enable": "啟用", + "enable-all": "全部啟用", + "enable-link-previews": "啟用聊天中的連結預覽?", + "encrypt-with-password": "用密碼加密", + "ens-10-SNT": "10 SNT", + "ens-add-username": "新增用戶名稱", + "ens-agree-to": "同意", + "ens-chat-settings": "聊天設定", + "ens-custom-domain": "自訂網域", + "ens-custom-username-hints": "輸入整個用戶名,包括自訂網域,如username.domain.eth", + "ens-custom-username-taken": "用戶名稱不屬於您:(", + "ens-deposit": "存款", + "ens-dismiss-message": "點選此處關閉", + "ens-displayed-with": "您的訊息透過", + "ens-get-name": "獲取專屬用戶名稱", + "ens-got-it": "好的,我知道了", + "ens-locked": "用戶名已鎖定。您必須等到{{date}}才能將其釋放", + "ens-name-content": "您可以使用乙太坊名稱服務(Ethereum Name Service, ENS) 註冊自訂的聊天別名。ENS名稱是分散式的用戶名。", + "ens-name-not-found": "無法解析ENS名稱", + "ens-name-title": "ENS名稱", + "ens-network-restriction": "僅在主網上可用", + "ens-no-usernames": "您沒有任何用戶名", + "ens-powered-by": "由Ethereum Name Services提供", + "ens-primary-username": "主要使用者名稱", + "ens-register": "註冊", + "ens-registration-failed": "要註冊用戶名稱,請重試。", + "ens-registration-failed-title": "交易失敗", + "ens-registration-failure": "註冊失敗", + "ens-registration-in-progress": "正在註冊...", + "ens-release-username": "釋出用戶名稱", + "ens-remove-hints": "刪除會將用戶名稱從您的金鑰中分離。", + "ens-remove-username": "刪除用戶名稱", + "ens-saved": "現在已與您的聊天金鑰連結,可以在\"Status\"中使用。", + "ens-saved-title": "用戶名已新增", + "ens-show-username": "在聊天中顯示我的ENS用戶名", + "ens-terms-header": "名稱註冊條款", + "ens-terms-point-1": "資金存入後,您該筆SNT將被鎖定一整年,無法被花費。", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e(ENS註冊表)", + "ens-terms-point-2": "一年後,您可以釋放名稱並取回押金,或者不採取任何措施來保留名稱。", + "ens-terms-point-3": "如果合約條款發生更改(例如\"Status\"變更了合約),則用戶有權釋放用戶名,而不管其持有時間為何。", + "ens-terms-point-4": "合約控制者無法取用您的存款。他們只能將存款返回發送位址。", + "ens-terms-point-5": "您的地址將與您的ENS名稱產生公開連結。", + "ens-terms-point-6": "用戶名稱將成為stateofus.eth的子網域節點,並接受ENS智慧合約條款的約束。", + "ens-terms-point-7": "您授權合約代表您同意轉讓SNT。僅當您核准交易以授權轉移時,才會出現此提示。", + "ens-terms-point-8": "這些條款由以下地址的智慧合約邏輯保證:", + "ens-terms-point-9": "{{address}} (Status UsernameRegistrar)", + "ens-terms-registration": "名稱註冊條款。", + "ens-test-message": "嘿", + "ens-transaction-pending": "交易待處理...", + "ens-understand": "我瞭解我的錢包地址將公開連結到我的用戶名。", + "ens-username": "ENS用戶名稱", + "ens-username-already-added": "用戶名稱已經與您的聊天密鑰連結,可以在\"Status\"中使用。", + "ens-username-available": "✓用戶名稱可以使用!", + "ens-username-connected": "該用戶名由您擁有,並與您的聊天金鑰連結。", + "ens-username-connected-continue": "繼續以設定“在聊天中顯示我的ENS用戶名”。", + "ens-username-connected-with-different-key": "繼續進行將產生一筆交易來將用戶名稱與您當前的聊天金鑰連結。", + "ens-username-connection-confirmation": "交易完成後, {{username}}將被連結。", + "ens-username-hints": "至少4個字元。僅拉丁字母,數字和小寫字母。", + "ens-username-invalid": "只能使用字母和數字。", + "ens-username-invalid-name-warning": "您的一個 ens 域名在註冊過程中被視為無效狀態。請勿使用該名稱進行錢包交易,並通過 support@status.im 聯繫我們以取得協助", + "ens-username-owned": "✓用戶名歸您所有。", + "ens-username-owned-continue": "繼續以將此用戶名稱與您的聊天金鑰連結。", + "ens-username-registration-confirmation": "太好了!交易完成後,您將擁有{{username}} 。", + "ens-username-registration-invalid": "警告! 該域名在註冊過程中被視為無效。請勿使用該名稱進行錢包交易,並通過 support@status.im 聯繫我們以取得協助", + "ens-username-taken": "用戶名稱已被使用:(", + "ens-username-you-can-follow-progress": "您可以在錢包的“交易歷史記錄”部分中追蹤進度。", + "ens-usernames": "ENS用戶名稱", + "ens-usernames-details": "註冊專屬用戶名,以便其他用戶輕鬆辨認", + "ens-want-custom-domain": "我在另一個網域上擁有註冊名稱", + "ens-want-domain": "我想要一個stateofus.eth網域", + "ens-welcome-hints": "ENS名稱將那些瘋狂的長串地址轉換為唯一的用戶名。", + "ens-welcome-point-customize": "ENS名稱可以替換聊天中的隨機3字名稱,改為 @yourname 而不是{{name}} 。", + "ens-welcome-point-customize-title": "自定義您的聊天名稱", + "ens-welcome-point-receive": "其他人可以在聊天中透過一個簡單的操作匯款給您。", + "ens-welcome-point-receive-title": "在聊天中接收交易", + "ens-welcome-point-register": "註冊一次就可以永久保留該用戶名。1年後,您可以解綁用戶名,並贖回您的SNT。", + "ens-welcome-point-register-title": "用10 SNT進行註冊", + "ens-welcome-point-simplify": "您可以使用易於共用的ENS名稱而不是十六進制雜湊(0x ...)來接收資金。…", + "ens-welcome-point-simplify-title": "簡化您的ETH地址", + "ens-welcome-point-verify": "您可以在接下來的步驟中驗證並新增您擁有的任何用戶名。", + "ens-welcome-point-verify-title": "已擁有用戶名?", + "ens-your-username": "您的用戶名稱", + "ens-your-usernames": "您的用戶名稱", + "ens-your-your-name": "您的ENS名稱", + "enter-12-words": "請輸入您種子詞組的12個單詞,並以單空格分開", + "enter-a-private-key": "輸入私鑰", + "enter-a-seed-phrase": "輸入種子詞組", + "enter-address": "輸入地址", + "enter-contact-code": "輸入ENS用戶名或聊天密鑰", + "enter-pair-code": "輸入您的配對碼", + "enter-pair-code-description": "配對碼可從已完成配對的Status用戶端設定", + "enter-password": "輸入密碼", + "enter-password-migration-prompt": "輸入您的密碼以移動好友名單、聊天和您的密鑰設定", + "enter-pin": "輸入六位數密碼", + "enter-puk-code": "輸入PUK碼", + "enter-puk-code-description": "六位數密碼已被鎖定, \n請輸入PUK碼以解鎖。", + "enter-recipient-address-or-username": "輸入收件人的地址或用戶名", + "enter-seed-phrase": "輸入種子詞組", + "enter-url": "輸入網址", + "enter-user-pk": "輸入用戶公鑰", + "enter-watch-account-address": "掃描QR碼或輸入要監控的地址", + "enter-word": "輸入密碼", + "enter-your-code": "輸入您的6位數密碼", + "enter-your-password": "輸入密碼", + "error": "錯誤", + "error-unable-to-get-balance": "無法取得帳戶餘額", + "error-unable-to-get-prices": "貨幣換算錯誤。請刷新頁面後重試。", + "error-unable-to-get-token-balance": "無法獲得代幣餘額", + "errors": "錯誤", + "eth": "ETH", + "ethereum-address": "以太坊地址", + "ethereum-node-started-incorrectly-description": "乙太坊節點以錯誤的配置啟動,應用程式即將停止。\n已配置的網路ID = {{network-id}} ,實際= {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "乙太坊節點啟動錯誤", + "etherscan-lookup": "至Etherscan上查詢", + "everyone": "所有人", + "expand-all": "展開全部", + "export-account": "匯出帳號", + "export-key": "匯出私鑰", + "external-link": "外部链接", + "external-storage-denied": "存取外部儲存空間被拒絕", + "failed": "失敗", + "faq": "常見問題", + "fast": "快速", + "favourite": "書籤", + "favourite-description": "您已加入書籤的網站將出現在這裡", + "favourites": "收藏夾", + "favourites-empty": "添加到收藏夾的地址將顯示在此處", + "featured": "精选", + "fee-cap": "費用上限", + "fee-explanation": "交易的最高價格。如果區塊基本費用超過此值,它將會延遲到下一個價格較低的區塊中處理。", + "fee-options": "建議的費用選項", + "fetch-community": "徵詢社群", + "fetch-messages": "↓取得訊息", + "fetch-timeline": "↓獲取", + "fetching-community": "正在徵詢社群...", + "find": "搜尋", + "finish": "完成", + "finishing-card-setup": "設定即將完成", + "fleet": "機群(Fleet)", + "fleet-settings": "機群設定", + "follow": "追蹤", + "follow-your-interests": "進入公共聊天並結識新朋友", + "free": "↓免費", + "from": "從", + "gas-amount-limit": "Gas數量上限", + "gas-limit": "Gas上限", + "gas-price": "Gas價格", + "gas-used": "Gas用量", + "gated": " 有门槛", + "generate-a-key": "產生金鑰", + "generate-a-new-account": "新增帳號", + "generate-a-new-key": "產生新的金鑰", + "generate-account": "產生金鑰", + "generate-an-account": "產生一個帳號", + "generate-new-key": "產生金鑰", + "generating-codes-for-pairing": ">將軟體程式下載到卡片上\n >產生解鎖碼和配對碼", + "generating-keys": "產生金鑰中...", + "generating-mnemonic": "產生種子詞組", + "get-a-keycard": "取得金鑰卡", + "get-started": "開始吧", + "get-status-at": "前往https:\/\/status.im取得Status", + "get-stickers": "取得貼圖", + "gif": "GIF", + "give-a-short-description": "簡短說明", + "give-a-short-description-community": "請簡短描述", + "give-permissions-camera": "給予使用相機的權限", + "glossary": "詞彙表", + "go-to-settings": "前往設定...", + "got-it": "知道了", + "grant-face-id-permissions": "要授予所需的Face ID權限,請進入系統設定,並確認選擇了Status > Face ID", + "group-chat": "群組聊天", + "group-chat-admin": "管理員", + "group-chat-admin-added": "** {{member}} **已成為管理員", + "group-chat-all-contacts-invited": "所有好友都已經在群組中", + "group-chat-created": "** {{member}} **新建了群組** {{name}} **", + "group-chat-decline-invitation": "拒絕邀請", + "group-chat-member-added": "** {{member}} **已受邀", + "group-chat-member-joined": "** {{member}} **已加入該群組", + "group-chat-member-removed": "** {{member}} **離開群組", + "group-chat-members-count": "{{selected}} \/ {{max}}位成員", + "group-chat-name-changed": "** {{member}} **將群組名稱更改為** {{name}} **", + "group-chat-no-contacts": "您還沒有任何好友。 \n邀請您的朋友開始聊天", + "group-details": "群信息", + "group-info": "群組資訊", + "group-invite": "群組邀請", + "group-invite-link": "群組邀請連結", + "group-membership-request": "請求加入群組成員", + "groups": "群组", + "gwei": "Gwei", + "has-permissions": "請求地址存取授權", + "hash": "雜湊", + "help": "幫助", + "help-capitalized": "幫助", + "help-center": "幫助中心", + "help-improve-status": "幫助改善Status", + "hide": "隱藏", + "hide-content-when-switching-apps": "禁止螢幕截圖", + "hide-content-when-switching-apps-ios": "隱藏預覽", + "history": "歷史紀錄", + "history-nodes": "Status節點", + "hold-card": "請將卡片保持在手機感應處", + "home": "首頁", + "hooks": "Hooks", + "how-it-works": "如何運作", + "http-gateway-error": "糟糕,請求失敗!", + "identifier": "識別碼", + "identity-verification": "身份验证", + "image": "圖片", + "image-remove-current": "刪除當前照片", + "image-source-gallery": "從圖庫中選擇", + "image-source-make-photo": "拍攝", + "image-source-title": "編輯圖片", + "import": "匯入", + "import-community": "匯入社群", + "import-community-title": "匯入社群", + "in-contacts": "好友名單", + "include": "包括", + "incoming": "等待中", + "incoming-transaction": "等待中交易", + "incorrect-code": [ + "str", + "抱歉,密碼不正確,請重新輸入" + ], + "increase-gas": "增加Gas", + "initialization": "初始化", + "install": "↓安裝", + "insufficient-balance-to-cover-fee": "餘額不足以支付交易費用", + "intro-message1": "歡迎來到Status! \n點選此訊息以設定密碼並開始使用。", + "intro-privacy-policy-note1": "\"Status\"不會收集您的個人數據或從中獲利。按下繼續,即表示您同意", + "intro-privacy-policy-note2": "隱私權政策", + "intro-text": "Status是您前進分散式網路的入口", + "intro-text1": "透過點對點加密網路進行聊天,在該網路中,郵件不會受到審查或駭客入侵", + "intro-text2": "在世界任何地方發送和接收數字資產-無需銀行帳戶", + "intro-text3": "探索遊戲,交易所和社群網路,只有您掌握自己的數據", + "intro-title1": "真正的隱密交流", + "intro-title2": "安全的加密錢包", + "intro-title3": "去中心化應用", + "intro-wizard-text1": "此組金鑰能控制您的帳戶。您的金鑰只存放在手機中,因此只有您可以使用它們", + "intro-wizard-text2": "此金鑰是聊天使用。它帶有一個無法更改的名稱。", + "intro-wizard-text3": "如果您擁有金鑰卡,請將金鑰儲存在此處以增強安全性。", + "intro-wizard-text4": "保護並加密您的金鑰", + "intro-wizard-text6": "\"Status\"將通知您更新訊息。您稍後可以在設定中的通知選項進行變更", + "intro-wizard-title-alt4": "建立密碼", + "intro-wizard-title-alt5": "確認您的密碼", + "intro-wizard-title1": "獲取您的金鑰", + "intro-wizard-title2": "選擇一個聊天名稱", + "intro-wizard-title3": "選擇金鑰保存", + "intro-wizard-title4": "創建一個六位數的密碼", + "intro-wizard-title5": "確認密碼", + "intro-wizard-title6": "啟用通知", + "introduce-yourself": "請簡單地介紹您自己", + "invalid-address-qr-code": "掃描的QR碼不包含有效位址", + "invalid-format": "格式錯誤\n必須為{{format}}", + "invalid-key-confirm": "執行", + "invalid-key-content": "帳戶資料庫無法加密,因為檔案已損壞。您的資產與聊天金鑰仍然安全。其他資料。像是對話紀錄與好友名單,將無法復原。使用“ {{erase-multiaccounts-data-button-text}} ”按鈕,將會刪除這些其他資料,並讓您重新存取資產與傳送訊息。", + "invalid-number": "無效號碼", + "invalid-pairing-password": "配對密碼無效", + "invalid-public-chat-topic": "無效的公開聊天主題", + "invalid-range": "格式無效,必須介於{{min}}和{{max}}", + "invalid-username-or-key": "無效的用戶名或聊天密鑰", + "invite": "邀请", + "invite-button": "邀請", + "invite-chat-accept": "接受", + "invite-chat-accept-join": "接受並加入", + "invite-chat-intro": "您推薦的一位朋友加入了Status。這裡是一些加密貨幣,可以幫助您入門!用它來註冊一個ENS名稱或購買貼紙包", + "invite-chat-name": "朋友推薦", + "invite-chat-pending": "等待中", + "invite-chat-rule": "接受將讓您的朋友也獲得推薦獎勵", + "invite-chat-starter-pack": "入門包", + "invite-contacts": "邀请好友加入社区", + "invite-friends": "邀請朋友", + "invite-friends-to-status": "邀請朋友", + "invite-instruction-fifth": "您隨時都可以來兌換推薦獎勵。", + "invite-instruction-first": "向您的朋友發送一個專屬的邀請,用來下載並加入Status", + "invite-instruction-fourth": "您的朋友取得入門包時,您可以獲得推薦獎勵", + "invite-instruction-second": "您的朋友下載了Status並建立帳戶(在Android上)", + "invite-instruction-third": "開始與您的朋友聊天,讓他們可以確認您的推薦", + "invite-people": "邀請其他人", + "invite-privacy-policy-public": "您已透過推薦連結安裝Status。現在加入此聊天頻道,您可以取得自己的推薦碼並同意", + "invite-privacy-policy1": "繼續,表示您同意參加推薦獎勵", + "invite-privacy-policy2": "條款及細則", + "invite-public-chat-home": "推薦邀請", + "invite-public-chat-intro": "這裡有一些可用的加密貨幣能幫助您更快上手!用它註冊一個ENS名稱或購買貼圖組合", + "invite-receive-account": "獲得推薦獎勵的賬戶", + "invite-reward": "為您邀請的每個朋友取得加密貨幣!", + "invite-reward-friend": "朋友:", + "invite-reward-friend-description": "您的朋友將收到包含一些{{reward}}的入門包,以開始使用", + "invite-reward-friend-name": "入門包", + "invite-reward-you": "您:", + "invite-reward-you-description": "邀請朋友並獲得{{reward}}推薦獎勵。可用它來取得貼圖、ENS名稱並嘗試dapps", + "invite-reward-you-name": "推薦獎勵", + "invite-select-account": "選擇一個帳戶以獲得推薦獎勵", + "invite-warning": "此推廣活動僅適用於非美國居民的Android設備用戶。好友需要在7天內確認推薦。", + "invited": "邀請", + "join": "加入", + "join-a-community": "或加入社群", + "join-group-chat": "加入群組", + "join-group-chat-description": "{{username}}邀請您加入群組{{group-name}}", + "join-me": "嗨,快來加我為Status好友:{{url}}", + "join-new-private-chat": "開始新的個人聊天", + "join-new-public-chat": "加入公共聊天", + "join-open-community": "加入社区", + "joined": "已加入", + "joined-group-chat-description": "您已透過{{username}}邀請加入{{group-name}}", + "key": "金鑰", + "key-managment": "密鑰管理", + "key-on-device": "私鑰已保存在此裝置上", + "keycard": "金鑰卡", + "keycard-access-reset": "金鑰卡的訪問權限被重置", + "keycard-applet-install-instructions": "要安裝小工具,請按照https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation上的說明進行操作", + "keycard-awaiting-description": "嘗試四處移動卡以在設備上找到NFC讀卡器", + "keycard-awaiting-title": "尋找中...", + "keycard-backup": "建立一個備份金鑰卡", + "keycard-backup-success-body": "備份卡建立成功。現在,您可以像原本卡片一樣在帳戶中使用它。", + "keycard-backup-success-title": "備份成功", + "keycard-blocked": "金鑰卡已被鎖定, \n您需要重設卡片才能繼續使用。", + "keycard-can-use-with-new-passcode": "您可以將此卡與新密碼一起使用", + "keycard-cancel-setup-text": "這將取消金鑰卡設定。強烈建議您完成設定以使用金鑰卡。您真的要取消嗎?", + "keycard-cancel-setup-title": "危險操作", + "keycard-connected-description": "嘗試保持卡靜止", + "keycard-connected-title": "已連結", + "keycard-desc": "擁有金鑰卡?將金鑰儲存在上面;進行交易時您將需要它", + "keycard-dont-ask-card": "不使用卡片進行登入", + "keycard-enter-new-passcode": "輸入新密碼{{step}}\/2", + "keycard-error-description": "再次連結卡以繼續", + "keycard-error-title": "連結丟失", + "keycard-factory-reset": "將卡片恢復為初始設定", + "keycard-factory-reset-text": "執行此操作將刪除卡上儲存的所有記憶詞組。請確保您已備份了與此鑰匙卡一起使用的助記詞。", + "keycard-factory-reset-title": "您確定要恢復至初始設定?", + "keycard-free-pairing-slots": "金鑰卡有{{n}}自由配對插槽", + "keycard-has-multiaccount-on-it": "卡片已滿。每張卡只可容納一個主金鑰配對", + "keycard-init-description": "將卡放在手機背面即可繼續", + "keycard-init-title": "尋找卡片...", + "keycard-is-blocked-details": "您不能再使用此卡存取或為該帳戶簽署。因為密碼和PUK嘗試失敗的次數過多。", + "keycard-is-blocked-instructions": "要存取您的帳戶,您需要將卡恢復初始設定。請點擊下方按鈕開始執行,您將需要使用到您的助記詞。", + "keycard-is-blocked-title": "金鑰卡已被封鎖", + "keycard-is-frozen-details": "為保護您的資產,您的卡已被凍結。請重置您的卡將其解凍回復後,才能夠發送交易。您需要使用PUK或助記詞來執行此操作。", + "keycard-is-frozen-factory-reset": "使用助記詞重設", + "keycard-is-frozen-reset": "使用PUK重置", + "keycard-is-frozen-title": "金鑰卡被凍結", + "keycard-onboarding-finishing-header": "即將完成", + "keycard-onboarding-intro-header": "將金鑰儲存到金鑰卡上", + "keycard-onboarding-intro-text": "做好準備,這可能需要幾分鐘,但確保帳戶安全很重要", + "keycard-onboarding-mnemonic-text": "您還需要一張紙和一支筆來寫下您的助記詞。", + "keycard-onboarding-pairing-header": "卡片配對中...", + "keycard-onboarding-pin-text": "您需要建立一個6位數的密碼,用於保護金鑰卡的存取權限。", + "keycard-onboarding-preparing-header": "正在準備卡片...", + "keycard-onboarding-puk-code-header": "寫下代碼並保存在安全之處", + "keycard-onboarding-recovery-phrase-description": "您需要此種子詞組來找回金鑰。請寫下來,存放在安全處,離線並與該設備分開。", + "keycard-onboarding-recovery-phrase-header": "備份種子詞組", + "keycard-onboarding-recovery-phrase-text": "最高機密。這是用於生成金鑰的神奇種子。", + "keycard-onboarding-start-header": "要繼續操作,\n請將卡保持在手機感應處", + "keycard-onboarding-start-step1": "創建密碼", + "keycard-onboarding-start-step1-text": "大約1分鐘。創建一個六位數的密碼來加密您的金鑰", + "keycard-onboarding-start-step2": "寫下PUK和配對代碼", + "keycard-onboarding-start-step2-text": "大約要1分鐘。您將需要一張紙和一支鉛筆", + "keycard-onboarding-start-step3": "備份種子詞組", + "keycard-onboarding-start-step3-text": "大約要1分鐘,您還需要準備一張紙和一支鉛筆", + "keycard-onboarding-start-text": "請保持卡片與手機能感應,\n在安裝過程中。設定大約需要4分鐘", + "keycard-processing-description": "嘗試保持卡靜止", + "keycard-processing-title": "處理中...", + "keycard-recover": "卡片遺失或被凍結?", + "keycard-recover-text": "如果您有助記詞,就能夠建立與此帳戶連結的新金鑰卡。您可以使用全新的金鑰卡,也可以在凍結的金鑰卡上進行重置。", + "keycard-recover-title": "為此帳戶建立一張新金鑰卡?", + "keycard-recovery-intro-button-text": "開始恢復", + "keycard-recovery-intro-header": "使用金鑰卡復原", + "keycard-recovery-intro-text": "如果您以前使用金鑰卡生成過金鑰,並想在此設備上使用這些金鑰", + "keycard-recovery-no-key-header": "無法恢復", + "keycard-recovery-no-key-text": "您的金鑰卡上沒有任何資料。想使用它,請生成一個新金鑰,並選擇您的金鑰卡來存放金鑰", + "keycard-recovery-phrase-confirm-header": "確認種子詞組", + "keycard-recovery-phrase-confirmation-text": "您將無法挽回!如果您遺失存取裝置,例如您的金鑰卡,您只能透過種子詞組來存取帳戶。切勿讓他人知道您的種子詞組,立刻寫下種子詞組,並存放在安全之處。", + "keycard-recovery-phrase-confirmation-title": "把種子詞組寫下來了嗎?", + "keycard-recovery-success-header": "您的金鑰\n已經成功恢復", + "keycard-redeem-title": "兌換至", + "keycard-redeem-tx": "兌換資產", + "keycard-redeem-tx-desc": "靠近卡片以完成簽署和接收資產", + "keycard-reset-passcode": "重置通行密碼", + "keycard-success-description": "您現在可以取出卡了", + "keycard-success-title": "成功", + "keycard-unauthorized-operation": "您無權執行此操作。 \n請點擊有效的卡,然後重試。", + "keycard-upsell-subtitle": "增強的安全性和便利性", + "kicked": "已踢", + "language": "語言Lang", + "last-backup-performed": "上次建立備份:", + "last-transaction": "最後一筆交易", + "learn-more": "瞭解更多", + "learn-more-about-keycard": "瞭解有關金鑰卡的更多訊息", + "leave": "離開", + "leave-chat": "退出聊天", + "leave-chat-confirmation": "聊天記錄將從您的設備中刪除。重新加入後,您將無法取回先前的歷史記錄。", + "leave-community": "退出社群", + "leave-community-message": "很遗憾你的离开,但请记住,你可以随时回来!", + "leave-community?": "离开社区?", + "leave-confirmation": "退出{{chat-name}}", + "leave-group": "退出群組", + "left": "剩下", + "les-ulc": "LES\/ULC", + "lets-go": "出發吧", + "lifestyle": "生活", + "light": "白天", + "limit": "限額", + "link-to-community": "链接到社区", + "linked-on": "已於{{date}}鏈結", + "load-messages-before": "在{{date}}之前", + "load-more-messages": "↓取得更多訊息", + "load-more-timeline": "↓取得更多", + "loading": "載入中...", + "local-notifications": "本地通知", + "local-notifications-subtitle": "啟用後端服務", + "lock-app-with": "鎖定應用", + "log-level": "日誌級別", + "log-level-settings": "紀錄日誌設定", + "logging": "登入中", + "logging-enabled": "啟用登入?", + "login-pin-description": "輸入您的六位數密碼以解鎖您的密鑰", + "logout": "登出", + "logout-app-content": "該帳戶即將登出。再次解鎖時,將使用前次選定的網路", + "logout-are-you-sure": "您確定要登出?", + "logout-key-management": "您需要先登出才能進行密鑰管理。", + "logout-title": "登出?", + "looking-for-cards": "尋找卡片...", + "lost-connection": "連線中斷", + "low-tip": "手續費用太低", + "lower-than-average-tip": "低於平均手續費用", + "mail-should-be-configured": "郵件客戶端應配置", + "mailserver-address": "Status節點地址", + "mailserver-automatic": "自動選擇", + "mailserver-automatic-switch-explanation": "選擇最快的可用Status節點", + "mailserver-connection-error": "無法連結到Status節點", + "mailserver-content": "\"Status\"網路中路由和儲存訊息的節點,最多30天。", + "mailserver-details": "Status節點詳細資訊", + "mailserver-error-content": "無法訪問您選擇的Status節點。", + "mailserver-error-title": "連結到Status節點時錯誤", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "選擇另一個Status節點", + "mailserver-reconnect": "無法連結到Status節點。點選即可重試", + "mailserver-request-error-content": "Status節點返回了以下錯誤: {{error}}", + "mailserver-request-error-status": "提取歷史記錄時發生錯誤,請查看日誌以獲取詳細訊息", + "mailserver-request-error-title": "Status節點請求錯誤", + "mailserver-request-retry": "重試", + "mailserver-retry": "重試", + "mailserver-title": "Status節點", + "main-account": "主账户", + "main-currency": "主要貨幣", + "main-networks": "主網路", + "main-wallet": "主錢包", + "mainnet-network": "主網路", + "make-admin": "設為管理員", + "manage-connections": "從應用程式連結內管理連線", + "manage-keys-and-storage": "管理密鑰和儲存", + "manage-members": "管理成员", + "mark-all-read": "將所有內容標記為已讀", + "mark-as-read": "标记为已读", + "mark-untrustworthy": "标记为不可信", + "master-account": "主控賬戶", + "max-fee": "最高交易費用", + "max-priority-fee": "最高手續費用", + "maximum-fee": "最高費用上限", + "maximum-fee-desc": "交易的最高價格。如果區塊處理費用超過此值,它將會延遲至稍後價格較低的區塊中處理。", + "maybe-later": "稍後再說", + "member-ban": "封鎖成員", + "member-kick": "強制退出成員", + "members": { + "other": " {{count}} 位成員" + }, + "members-active": { + "other": "{{count}} 位成員" + }, + "members-active-none": "沒有成員", + "members-count": "{{count}}名成員", + "members-label": "成員", + "members-limit-reached": "達到會員人數上限", + "members-title": "成員", + "membership": "成员资格", + "membership-approval": "需要取得同意", + "membership-approval-description": "您的社群可以自由加入,但是新成員必須先取得社群創建者的同意", + "membership-button": "入會要求", + "membership-declined": "會員申請被拒絕", + "membership-description": "會員資格需要經過群組管理員同意", + "membership-ens": "需要具備ENS用戶名", + "membership-ens-description": "您的社群需要擁有ENS用戶名才能加入", + "membership-free": "沒有要求", + "membership-free-description": "您的社群是任何人都能自由加入", + "membership-invite": "需要其他成員的邀請", + "membership-invite-description": "您的社群只能透過現有社群成員邀請才能加入", + "membership-none": "沒有", + "membership-none-placeholder": "您可以要求新成員滿足某些條件才能加入。可以隨時更改", + "membership-request-pending": "會員申請處理中", + "membership-requests": "會員申請", + "membership-title": "入會要求", + "mentions": "提及", + "message": "留言", + "message-deleted": "消息已删除", + "message-deleted-for-you": "为您删除的消息", + "message-not-sent": "留言未傳送", + "message-options-cancel": "取消", + "message-reply": "回覆", + "messages": "留言", + "messages-from-contacts-only-subtitle": "只有您加入好友名單的成員,才能與您開啟新聊天,或邀請您加入群組", + "messages-gap-warning": "某些消息可能丢失", + "might-break": "可能導致某些ÐApps失效", + "migration-successful": "遷移成功", + "migration-successful-text": "帳號已成功遷移到金鑰卡", + "migrations-failed-content": "{{message}} \n架構版本:初始{{initial-version}} ,當前{{current-version}} ,前次{{last-version}} \n\n資料庫發生錯誤,您的資產與聊天金鑰仍然安全,但其他數據,像是您的對話紀錄與好友名單,將無法回復。\n“{{erase-multiaccounts-data-button-text}}”按鈕,將會刪除這些數據,讓您能夠重新存取帳戶資產和傳送訊息。", + "miners-higher-fee": "如果您支付更高的費用,礦工可能會更快處理您的交易。", + "minimum-received": "最低收到", + "mobile-network-ask-me": "使用行動網路時詢問", + "mobile-network-continue-syncing": "同步中", + "mobile-network-continue-syncing-details": "您稍後可以在設定中進行更改", + "mobile-network-go-to-settings": "前往設定", + "mobile-network-settings": "行動數據", + "mobile-network-sheet-configure": "您可以更改其他同步選項,請至", + "mobile-network-sheet-offline": "沒有Wi-fi,無法更新訊息。", + "mobile-network-sheet-offline-details": "使用行動網路同步已關閉", + "mobile-network-sheet-remember-choice": "記住我的選擇", + "mobile-network-sheet-settings": "設定", + "mobile-network-start-syncing": "開始同步", + "mobile-network-stop-syncing": "停止同步", + "mobile-network-stop-syncing-details": "僅透過Wi-Fi進行同步", + "mobile-network-use-mobile": "使用行動數據", + "mobile-network-use-mobile-data": "進行聊天和錢包同步時,Status會佔用相當的數據傳輸量。", + "mobile-network-use-wifi": "僅限Wi-Fi", + "mobile-syncing-sheet-details": "進行聊天和錢包同步時,Status會佔用相當的數據傳輸量。", + "mobile-syncing-sheet-title": "使用行動數據進行同步?", + "more": "更多", + "move-and-reset": "移動並重置", + "move-keystore-file": "移動密鑰檔案", + "move-keystore-file-to-keycard": "要將密鑰檔案移動到金鑰卡?", + "multiaccount-exists-content": "此帳號的密鑰已存在,無法再次新增。如果您已經忘記您的密碼、通行密碼、或是遺失金鑰卡,請移除應用程式,然後重新安裝,並使用您的種子詞組取回密鑰。", + "multiaccount-exists-title": "此帳號的密鑰已存在", + "multiaccounts-recover-enter-phrase-text": "輸入12、15、18、21或24個字詞。 \n用單個空格分隔單詞。", + "multiaccounts-recover-enter-phrase-title": "輸入您的種子詞組", + "music": "音乐", + "mutal-contacts": "互为好友", + "mute": "靜音", + "mute-chat": "靜音聊天", + "mute-community": "静音社区", + "mutual-contact-requests": "好友请求", + "my-accounts": "我的帳戶", + "my-accounts-empty": "您的可用帳戶將顯示在此處", + "my-profile": "我的個人資料", + "my-status": "我的狀態", + "n-photos": "{{count}}照片", + "name": "名稱", + "name-of-token": "代幣名稱", + "name-optional": "名稱(選填)", + "name-your-channel": "請為您的頻道命名", + "name-your-channel-placeholder": "頻道名稱", + "name-your-community": "請為您的社群命名", + "name-your-community-placeholder": "容易記憶的名稱", + "need-help": "需要幫忙?", + "negative": "负的", + "network": "網路", + "network-chain": "網路鏈", + "network-details": "網路詳細資訊", + "network-fee": "網路費用", + "network-id": "網路ID", + "network-info": "網路狀態", + "network-invalid-network-id": "指定的網路ID與RPC網址所對應的網路ID不符", + "network-invalid-status-code": "無效的狀態碼: {{code}}", + "network-invalid-url": "網址無效", + "network-settings": "網路設定", + "never": "從未", + "new": "新", + "new-category": "新類別", + "new-chat": "新聊天", + "new-community-title": "新社群", + "new-contact": "新好友", + "new-contract": "新合約", + "new-favourite": "新的收藏", + "new-group": "新群組", + "new-group-chat": "新的群組聊天", + "new-messages-header": "新消息", + "new-network": "新網路", + "new-password": "新密碼", + "new-pin-description": "輸入新的六位數密碼", + "new-public-group-chat": "加入公共聊天", + "new-puk-description": "輸入新的12位數PUK碼", + "new-status": "新增狀態", + "new-tab": "新分頁", + "new-ui": "新的用户界面", + "next": "下一個", + "nickname": "暱稱", + "nickname-description": "暱稱可以幫助您識別Status中的其他人。 \n只有您才能看到已新增的暱稱", + "no": "不", + "no-collectibles": "沒有可用的收藏品", + "no-contacts": "好友名單空白", + "no-keycard-applet-on-card": "卡上沒有金鑰卡程式", + "no-messages": "無訊息", + "no-pairing-slots-available": "此卡片已經與5台裝置配對,因此無法再與此裝置配對。請使用已配對的裝置,或登入卡片並解除先前的裝置配對", + "no-pinned-messages": "沒有釘選的訊息", + "no-pinned-messages-community-desc": "此频道没有任何置顶的消息。", + "no-pinned-messages-desc": "此聊天没有任何置顶消息。", + "no-result": "沒有結果", + "no-thanks": "不,謝謝", + "no-tokens-found": "找不到代幣", + "node-address": "節點地址", + "node-details": "節點詳情", + "node-info": "節點資訊", + "node-version": "節點版本", + "nodes-disabled": "Status節點已停用", + "non-archival-node": "RPC端點不支援存檔請求。您的本機交易記錄可能不完整。", + "non-contacts": "未知聯絡人", + "nonce": "Nonce", + "none": "沒有", + "normal": "普通", + "not-applicable": "不適用於未簽名的交易", + "not-connected-nodes": "未連結到Status節點", + "not-connected-to-peers": "沒有連結到任何端點", + "not-enough-snt": "SNT不足", + "not-found": "未找到", + "not-keycard-text": "您使用的不是金鑰卡。您需要購買金鑰卡才能使用它", + "not-keycard-title": "不是金鑰卡", + "not-registered": "未註冊", + "notification-settings": "通知設定", + "notifications": "通知", + "notifications-non-contacts": "來自未知聯絡人的通知", + "notifications-preferences": "通知首選項", + "notifications-servers": "通知伺服器", + "notifications-switch": "顯示通知", + "notifications-transactions": "錢包交易", + "notify": "通知", + "now": "現在", + "off": "關", + "off-status-tree": "關閉\"Status\"", + "offline": "離線", + "offline-messaging-use-history-explanation": "啟用Status節點以存取應用程式關閉時傳送的訊息。啟用時,Status節點將獲取您的IP地址。禁用後,您將不會在關閉應用程式時收到訊息,並且往後打開應用程式時也不會看到訊息。", + "offline-messaging-use-history-nodes": "使用Status節點", + "ok": "確定", + "ok-continue": "好,繼續", + "ok-got-it": "好,知道了", + "ok-save-pass": "OK,保存密碼", + "okay": "好的", + "on": "開", + "on-status-tree": "在\"Status\"上", + "once-enabled-share-metadata": "啟用後,聊天中發布的連結可能會與網站共享您的中繼資料", + "one-day": "一天", + "one-month": "一個月", + "one-week": "一週", + "online": "線上", + "open": "打開", + "open-chat": "開啟聊天", + "open-dapp": "啟動ÐApp", + "open-dapp-store": "搜尋ÐApps", + "open-home": "打開...", + "open-in-new-tab": "用新分頁開啟", + "open-membership": "開放會員", + "open-nfc-settings": "打開NFC設定", + "open-on-block-explorer": "在區塊瀏覽器上開啟", + "opened": "已开放", + "opening-buy-crypto": "開啟{{site}} ...", + "optimal": "最佳", + "optional": "選填", + "or": "或", + "outgoing": "外部", + "outgoing-transaction": "外部交易", + "page-camera-request-blocked": "相機權限被拒絕。要啟用相機,請進入手機設定", + "page-would-like-to-use-camera": "想使用你的相機", + "pair": "配對裝置", + "pair-card": "與此裝置配對", + "pair-code": "配對碼", + "pair-code-explanation": "將卡片與不同的設備(最多5個)進行配對,就能使用同一張卡片簽署交易。", + "pair-code-placeholder": "配對碼", + "pair-this-card": "配對這張卡", + "pair-this-device": "其他通知裝置", + "pair-this-device-description": "配對裝置以同步好友名單和聊天", + "paired-devices": "已配對裝置", + "pairing": "配對中", + "pairing-card": "配對卡片", + "pairing-changed": "配對代碼已更改", + "pairing-code-placeholder": "配對代碼...", + "pairing-code_error1": "配對碼不符合", + "pairing-go-to-installation": "進入配對設定", + "pairing-maximum-number-reached-content": "在啟用新裝置之前,請先禁用其中一台。", + "pairing-maximum-number-reached-title": "達到最大裝置數", + "pairing-new-installation-detected-content": "偵測到新裝置。 \n為了正確使用您的裝置,需要在使用之前配對並啟用。 \n請進到設定下的裝置部分,以與您的裝置配對。", + "pairing-new-installation-detected-title": "偵測到新裝置", + "pairing-no-info": "暫無資訊", + "pairing-please-set-a-name": "請為您的裝置設定名稱。", + "passphrase": "密碼詞組", + "password": "密碼", + "password-description": "最少要有6個字元。您的密碼可以保護您的金鑰。您需要它來解鎖Status並進行交易。", + "password-mismatch": "新密碼與確認不一致", + "password-placeholder": "密碼...", + "password-placeholder2": "確認您的密碼", + "password-reset-in-progress": "更改密碼中...", + "password-reset-success": "密碼已重設", + "password-reset-success-message": "您將需要重新登入", + "password_error1": "密碼不符", + "paste": "貼上", + "paste-json": "貼上JSON", + "pay-to-chat": "付費聊天", + "peer-content": "連結到\"Status\"聊天網路的設備。每個用戶可以代表一個或多個端點,具體取決於他們的設備數量。", + "peer-title": "端點", + "peer-to-peer": "點對點", + "peers": "端點", + "pending": "等待中", + "pending-confirmation": "正在確認...", + "pending-invitations": "待處理的會員申請", + "pending-requests": "申請處理中", + "per-gas-price-limit": "每次gas價格限制", + "per-gas-tip-limit": "每次gas手續費用限制", + "perform-backup": "建立備份", + "permissions": "權限", + "phone-e164": "International 1", + "photos": "相片", + "photos-access-error": "要開放相簿的使用權限,請進入系統設定,並在應用程式Status>相簿中進行變更。", + "pin": "釘選", + "pin-changed": "六位數密碼已更改", + "pin-code": "六位數密碼", + "pin-limit-reached": "達到釘選限制數。請先取消釘選上一則訊息。", + "pin-mismatch": "密碼錯誤", + "pin-one-attempt": "一次嘗試", + "pin-one-attempt-blocked-after": "在您的金鑰卡被封鎖之前", + "pin-one-attempt-blocked-before": "小心,您只有", + "pin-one-attempt-frozen-after": "在您的金鑰卡被凍結之前", + "pin-one-attempt-frozen-before": "小心,您只有", + "pin-retries-left": "還剩{{number}}次", + "pin-to-channel": "在频道内置顶消息", + "pin-to-chat": "在聊天中置顶", + "pinned-a-message": "置顶消息", + "pinned-by": "設為釘選", + "pinned-messages": "釘選的訊息", + "pinned-messages-count": { + "other": " {{count}} 則釘選訊息" + }, + "pinned-messages-empty": "釘選的訊息將顯示在此。要釘選訊息,請長按該訊息並確認“釘選”", + "podcasts": "播客", + "positive": "正的", + "powered-by-paraswap": "由 Paraswap 提供", + "preference": "選擇偏好", + "preview-privacy": "預覽隱私模式", + "previewing-may-share-metadata": "這些網站的預覽連結可能會與其所有者共享您的中繼資料", + "price-impact": "價格影響", + "price-impact-desc": "本次交易的價格影響估計。如果當前的區塊處理費用超過此值,您的交易將包含在稍後處理費用較低的區塊中。", + "principles": "原則", + "priority": "優先程度", + "privacy": "隱私", + "privacy-and-security": "隱私權與安全性", + "privacy-photos": "個人資料照片隱私", + "privacy-policy": "隱私權政策", + "privacy-show-to-warning": "已經看過你頭像的人能繼續取得更新", + "private-key": "私鑰", + "private-notifications": "私人通知", + "private-notifications-descr": "Status將通知您更新訊息。您可以稍後在設定中的通知選項進行變更", + "processing": "請稍等", + "product-information": "產品資訊", + "profile": "個人資料", + "profile-deleted-content": "您的個人資料已成功刪除", + "profile-deleted-keycard": "現在您可以在金鑰卡上還原另一個密鑰對", + "profile-deleted-title": "個人資料已刪除", + "profile-details": "個人資料詳細訊息", + "profile-not-found": "找不到個人資料", + "profile-pic-pick": "從圖庫中選擇", + "profile-pic-remove": "移除照片", + "profile-pic-take": "拍攝照片", + "profile-picture-updated": "個人資料圖片已更新", + "public": "公開", + "public-channel": "公共頻道", + "public-chat": "公開的聊天", + "public-chat-description": "加入您感興趣的公開聊天!任何人都可以建立一個新的。", + "public-chats": "公共聊天室", + "public-group-status": "公開", + "public-group-topic": "話題", + "public-key": "公鑰", + "puk-and-pairing-codes-displayed": "顯示PUK和配對碼", + "puk-changed": "12位數PUK已更改", + "puk-code": "PUK碼", + "puk-code-explanation": "如果您忘記了六位數的密碼,或輸入錯誤三次,就需要此密碼來解鎖卡片。", + "puk-mismatch": "錯誤的PUK碼", + "push-failed-transaction": "您的交易失敗", + "push-failed-transaction-body": "{{value}} {{currency}}至{{to}}", + "push-inbound-transaction": "您收到了{{value}} {{currency}}", + "push-inbound-transaction-body": "從{{from}}到{{to}}", + "push-notifications-server-enabled": "伺服器已啟用", + "push-notifications-servers": "推送通知伺服器", + "push-outbound-transaction": "您發送了{{value}} {{currency}}", + "push-outbound-transaction-body": "從{{from}}到{{to}}", + "quiet-days": "{{quiet-days}}天", + "quiet-hours": "{{quiet-hours}}小時", + "re-encrypt-key": "重新加密您的金鑰", + "rearrange-categories": "重新排序類別", + "receive": "接收", + "receive-transaction": "接收交易", + "received": "已接收", + "recent": "最近", + "recent-empty": "最近使用的地址將顯示在此處", + "recent-recipients": "好友名單", + "recently-used-stickers": "最近使用過的貼圖會出現在這裡", + "recipient": "收款人", + "recipient-code": "輸入收款地址", + "recipient-code-placeholder": "0x ...或username.domain.eth", + "recover": "恢復", + "recover-key": "使用既有金鑰", + "recover-keycard-multiaccount-not-supported": "此帳號的密鑰已存在,無法再次新增。如果您已經忘記您的密碼、通行密碼、或是遺失金鑰卡,請移除應用程式,然後重新安裝,並使用您的種子詞組取回密鑰。", + "recover-with-keycard": "使用金鑰卡復原", + "recover-with-seed-phrase": "使用種子詞組恢復", + "recovering-key": "正在訪問金鑰...", + "recovery-confirm-phrase": "確認種子詞組", + "recovery-phrase": "種子詞組", + "recovery-success-text": "您將必須創建新的代碼或密碼來重新加密金鑰", + "recovery-typo-dialog-description": "請注意,您輸入種子詞組的單字與順序,都必須與最初的詞組一致。", + "recovery-typo-dialog-title": "種子詞組正確嗎?", + "redeem-amount": "{{quantity}}待領取的獎勵", + "redeem-now": "立即領取", + "redeem-success": "領取獎勵完成!", + "reduced-tip": "手續費將會減少", + "refresh": "重新整理", + "registered": "註冊", + "reject": "拒絕", + "reject-and-delete": "拒絕並刪除", + "remember-me": "記住賬號", + "remind-me-later": "再讓我看一次", + "remote-notifications": "遠端通知", + "remote-notifications-subtitle": "啟用google推送通知", + "remove": "移除", + "remove-favourite": "刪除書籤", + "remove-from-chat": "從聊天中刪除", + "remove-from-contacts": "從好友名單中刪除", + "remove-from-contacts-text": "即使從好友名單中刪除用戶,您仍無法向他們隱藏已顯示過的錢包地址", + "remove-group": "移除群組", + "remove-network": "移除網路", + "remove-token": "刪除代幣", + "removed": "已移除", + "removed-from-contacts": "从联系人中删除", + "rename": "重命名", + "repeat-pin": "重複新的六位數密碼", + "repeat-puk": "再次確認新的12位數PUK", + "replies": "回复", + "replying-to": "回覆給{{author}}", + "report-bug-email-template": "1. 問題說明\n {{description}}\n \n 2. 操作步驟\n {{steps}}\n\n 3. 請附上可以顯示問題的螢幕截圖", + "request-access": "申請加入", + "request-feature": "要求功能", + "request-membership": "申請會員", + "request-pending": "申請處理中...", + "request-transaction": "請求交易", + "required-field": "必填項目", + "resend-message": "重新傳送", + "reset-card": "卡片重置", + "reset-card-description": "此操作會將卡重置為初始狀態。它將刪除所有卡片資料,包括金鑰。操作將無法回復。", + "reset-database": "重置資料庫", + "reset-database-warning": "刪除聊天、好友名單和設定。當您遺失密碼時才需要進行此步驟", + "reset-database-warning-keycard": "刪除對話紀錄、好友名單和設定。當您遺失密碼時才需要進行此步驟", + "reset-password": "重設密碼", + "restore-defaults": "恢復預設值", + "retry": "重試", + "revoke-access": "解除訪問權限", + "rpc-url": "RPC URL", + "rpc-usage-copy": "複製", + "rpc-usage-filter": "過濾方式", + "rpc-usage-filter-methods": "過濾方式", + "rpc-usage-filtered-total": "第{{filtered-total}} ,共 {{total}}", + "rpc-usage-get-stats": "重新整理", + "rpc-usage-info": "RPC使用情況統計", + "rpc-usage-reset": "重置", + "rpc-usage-total": "共计", + "safe-estimate": "安全估計", + "save": "儲存", + "save-image-library": "将图像保存到库", + "save-password": "保存密碼", + "save-password-unavailable": "設定裝置密碼以儲存密碼", + "save-password-unavailable-android": "無法使用“儲存密碼”:您的設備可能已開啟Root權限,或缺少必要的安全功能。", + "say-hi": "打个招呼", + "scan-qr": "掃描QR碼", + "scan-qr-code": "掃描含有錢包地址的QR碼", + "scan-tokens": "掃描代幣", + "search": "搜尋", + "search-discover-communities": "搜索社区或类别", + "search-no-chat-found": "沒有搜尋結果。您的意思是", + "secret-keys-confirmation-text": "萬一您遺失手機,您將需要此密碼才能使用金鑰卡。", + "secret-keys-confirmation-title": "把密碼寫下來了嗎?", + "security": "安全性", + "see-details": "詳細資訊", + "see-it-again": "再看一遍", + "see-sticker-set": "查看完整的贴纸集", + "see-suggestions": "查看建議", + "seed-key-uid-mismatch": "種子詞組不符合", + "seed-key-uid-mismatch-desc-1": "您輸入的種子詞組與{{multiaccount-name}}不一致", + "seed-key-uid-mismatch-desc-2": "要管理此帳戶的密鑰,請驗證您的種子詞組,然後重試。", + "seed-phrase-content": "從BIP39標準列表中隨機選擇的一組易於閱讀的單詞,用於在其他錢包和設備上恢復或訪問您的以太坊帳戶。在整個加密生態系統中也稱為“助記詞”,“恢復詞”或“錢包備份”。大多數加密應用程式使用相同地標準來生成帳戶。", + "seed-phrase-placeholder": "助記詞", + "seed-phrase-title": "輸入種子詞組", + "select": "選擇", + "select-account": "選擇帳號", + "select-account-dapp": "選擇您希望用於Dapps的帳戶", + "select-account-first": "首先選擇一個帳號", + "select-chat": "選擇聊天以開始發送訊息", + "select-new-location-for-keys": "選擇一個新位置以保存您的私鑰", + "select-token-to-receive": "選擇要接收的代幣", + "select-token-to-swap": "選擇要交換的代幣", + "selected": "已選", + "send": "發送", + "send-contact-request-message": "要开始聊天,您需要成为联系人", + "send-logs": "報告錯誤", + "send-logs-to": "向{{email}}報告錯誤", + "send-message": "發訊息", + "send-push-notifications": "傳送推送通知", + "send-push-notifications-description": "禁用此功能後,收到您訊息的人將不會收到通知", + "send-request": "發送請求", + "send-request-amount": "數目", + "send-request-amount-max-decimals": "小數點後的位數最多為{{asset-decimals}}", + "send-request-unknown-token": "未知代幣-{{asset}}", + "send-sending-to": "致{{recipient-name}}", + "send-transaction": "發送交易", + "sending": "正在發送", + "sent": "已發送", + "sent-at": "發送於", + "server": "伺服器", + "set-a-topic": "新增話題", + "set-currency": "設定默認貨幣", + "set-custom-fee": "設定自訂費用", + "set-dapp-access-permissions": "設定DApp訪問權限", + "set-max": "設至最大", + "settings": "設定", + "share": "分享", + "share-address": "分享地址", + "share-chat": "分享聊天", + "share-community": "分享社区", + "share-contact-code": "分享聊天金鑰", + "share-dapp-text": "在Status上查看這個使用中的ÐApp: {{link}}", + "share-image": "分享图片", + "share-invite-link": "分享邀請連結", + "share-link": "分享連結", + "share-logs": "分享程式紀錄日誌", + "share-my-profile": "分享我的個人檔案", + "share-profile": "分享個人資料", + "share-profile-link": "分享資料連結", + "share-public-chat-text": "在Status應用程式上查看此公開聊天: {{link}}", + "shared": "共用", + "sharing-copied-to-clipboard": "已複製", + "sharing-copy-to-clipboard": "複製", + "sharing-data-desc-1": "資料根據公共規則進行驗證,以確保不會發送敏感資料。請隨時保持懷疑,進行驗證。", + "sharing-data-desc-2": "使用資料是通過 Status 的對等網路進行端到端加密發送", + "sharing-data-desc-3": "使用一次性密鑰而不是您個人的聊天密鑰", + "sharing-data-desc-4": "使用資料無法與您的 IP 地址相關聯", + "sharing-data-desc-5": "所有用戶的總計資料均公開可得", + "sharing-data-desc-6": "資料傳送後會從您的手機中刪除", + "sharing-share": "分享", + "shell-placeholder-subtitle": "打開您的社群、訊息、\n錢包帳戶和瀏覽器視窗", + "shell-placeholder-title": "您的應用程式將在此執行", + "show-less": "顯示較少", + "show-more": "顯示更多", + "show-notifications": "顯示通知", + "show-profile-pictures": "展示個人大頭貼", + "show-profile-pictures-to": "將您的個人頭像顯示給", + "show-qr": "顯示QR碼", + "show-transaction-data": "顯示交易明細", + "sign-and-send": "簽名並傳送", + "sign-anyway": "仍要簽名", + "sign-in": "登入", + "sign-message": "簽署訊息", + "sign-out": "登出", + "sign-request-failed": "無法簽署訊息", + "sign-with": "簽名", + "sign-with-password": "輸入密碼確認", + "sign-you-in": "正在登錄…", + "signing": "簽署", + "signing-a-message": "簽署訊息", + "signing-phrase": "簽署詞組", + "skip": "略過", + "slippage": "滑價", + "slow": "慢", + "something-went-wrong": "發生錯誤", + "soon": "即將", + "sort-communities": "对社区进行排序", + "specify-address": "指定地址", + "specify-name": "指定一個名稱", + "specify-network-id": "指定網路ID", + "specify-rpc-url": "指定一個RPC網址", + "specify-server-public-key": "輸入伺服器公開金鑰", + "specify-symbol": "指定標示", + "start-chat": "開始聊天", + "start-conversation": "開始對話", + "start-group-chat": "開始群組聊天", + "start-new-chat": "開始新聊天", + "starter-pack-coming": "新手組合禮包在此", + "starter-pack-coming-description": "可能需要幾分鐘到幾個小時", + "starter-pack-received": "入門包已收到", + "starter-pack-received-description": "這是一些加密貨幣,可以幫助您入門!用它來獲得貼紙,ENS名稱並嘗試dapps", + "status": "Status", + "status-always-online": "總是在線上", + "status-automatic": "自動", + "status-automatic-subtitle": "自動設定個人狀態", + "status-confirmed": "已確認", + "status-dnd": "請勿打擾", + "status-dnd-subtitle": "靜音所有通知", + "status-hardwallet": "Status硬體錢包", + "status-inactive": "關閉", + "status-inactive-subtitle": "隱藏您的線上狀態", + "status-is-open-source": "Status是開源項目", + "status-keycard": "Status金鑰卡", + "status-mobile-descr": "同步對話時,Status會使用大量數據傳輸。您可以選擇在行動網路上關閉同步", + "status-not-sent-click": "尚未確認,查看其他選項", + "status-not-sent-tap": "尚未確認,查看其他選項", + "status-pending": "等待中", + "status-sent": "已發送", + "status-tx-not-found": "找不到TX", + "status-updates-descr": "狀態更新將顯示在此處。將個人資料添加為聯絡人,以在您的時間軸上接收更新。", + "statuses-descr": "分享您的想法,並更新到您的好友名單中", + "statuses-my-status-descr": "分享您的想法。任何瀏覽您個人資料的使用者,都可以看到您的狀態。而將您添加為聯絡人的使用者,會在時間軸上收到您的更新", + "step-i-of-n": "步驟{{step}}\/{{number}}", + "sticker": "貼圖", + "sticker-market": "貼圖市集", + "storage": "儲存", + "submit": "提交", + "submit-bug": "回報錯誤", + "success": "成功", + "successful-connection": "连接成功", + "suggested-min-tip": "建議最低手續費", + "suggested-price-limit": "建議價格", + "swap": "交換", + "swap-details": "交換詳情", + "switch-to-simple-interface": "切換到簡易界面", + "symbol": "符號", + "sync-all-devices": "同步所有裝置", + "sync-in-progress": "同步中...", + "sync-settings": "同步設定", + "sync-synced": "同步中", + "syncing-devices": "同步中...", + "system": "系統預設", + "tabs": "分頁", + "tag-was-lost": "標籤丟失", + "tap-card-again": "請將卡片再次靠近手機感應處", + "terms-of-service": "使用條款", + "test-networks": "測試網路", + "text-input-disabled": "請稍等", + "thank-you": "感謝您", + "this-device": "這個設備", + "this-device-desc": "您的金鑰將被加密並安全地儲存在設備上", + "this-is-you-signing": "這是您的簽署字詞", + "this-will-take-few-seconds": "這需要幾秒鐘", + "three-days": "三天", + "three-words-description": "這三個字詞證明交易是安全的。", + "three-words-description-2": "在簽署每筆交易之前,您應該看到這些單詞,反之請取消並登出。", + "timeline": "時間軸", + "tip-cap": "費用上限", + "to": "到", + "to-block": "區塊", + "to-enable-biometric": "要啟用{{bio-type-label}} ,您必須在解鎖螢幕上勾選保存密碼", + "to-encrypt-enter-password": "要加密此帳號,請輸入密碼", + "to-see-this-message": "要查看此訊息,", + "token": "代幣", + "token-auto-validate-decimals-error": "地址{{address}}上{{symbol}}代幣的小數位數錯誤-設定為{{expected}}但檢測為{{actual}}", + "token-auto-validate-name-error": "地址{{address}}上{{symbol}}代幣名稱錯誤-設定為{{expected}}但檢測為{{actual}}", + "token-auto-validate-symbol-error": "地址{{address}}上{{symbol}}代幣代碼錯誤-設定為{{expected}}但檢測為{{actual}}", + "token-details": "代幣詳細訊息", + "token-price": "代币价格", + "topic-name-error": "僅能使用小寫字母(a到z)、數字和破折號(-),請勿使用聊天金鑰", + "total-gas": "總交易手續費", + "total-members": "成员总数", + "transaction": "交易", + "transaction-data": "交易明細", + "transaction-declined": "交易被拒絕", + "transaction-description": "將在網路上經過12次確認後完成", + "transaction-details": "交易明細", + "transaction-failed": "交易失敗", + "transaction-fee": "交易費用", + "transaction-history": "交易記錄", + "transaction-request": "交易請求", + "transaction-sent": "交易已發送", + "transaction-signed": "交易已成功簽署", + "transactions": "交易", + "transactions-filter-select-all": "全選", + "transactions-filter-title": "篩選記錄", + "transactions-history": "交易記錄", + "transactions-history-empty": "您的歷史記錄中還沒有交易", + "transactions-history-loading": "正在載入交易記錄。這可能需要一點時間。", + "transactions-load-more": "讀取更多", + "transactions-management-enabled": "交易管理 (alpha)", + "transactions-sign": "確認", + "transfer-ma-unknown-error-desc-1": "您的多帳戶似乎沒有被刪除。但資料庫可能已經重置", + "transfer-ma-unknown-error-desc-2": "請檢查您的帳戶列表,然後重試。如果未列出該帳戶,請選擇使用既有金鑰,並以種子詞組進行恢復", + "transfers-fetching-failure": "傳輸歷史記錄無法更新。請檢查您的網路,然後下拉頁面來重試", + "tribute-required-by-multiaccount": "{{multiaccount-name}}需要SNT才能開始聊天。", + "tribute-state-paid": "付款完成", + "tribute-state-pending": "付款待處理", + "tribute-state-required": "需要提供{{snt-amount}} SNT", + "tribute-to-talk": "聊天收益模式", + "tribute-to-talk-add-friends": "將朋友新增為好友,允許聊天且無需支付費用。", + "tribute-to-talk-are-you-friends": "這是您認識的人嗎?", + "tribute-to-talk-ask-to-be-added": "申請成為好友", + "tribute-to-talk-contact-received-your-tribute": "收到您的付款。您現在可以安全地相互聊天。", + "tribute-to-talk-desc": "通過要求SNT來開啟新聊天,讓您的知名度可以轉換成收益", + "tribute-to-talk-disabled": "關閉Tribute to Talk", + "tribute-to-talk-disabled-note": "現在開始,新使用者可以在不發送SNT的情況下與您聊天。", + "tribute-to-talk-enabled": "您已啟用聊天收益模式。", + "tribute-to-talk-finish-desc": "從現在開始,您將只收到來自好友和付費用戶的聊天訊息", + "tribute-to-talk-learn-more-1": "您的時間和精力是您最寶貴的資產。所以聊天收益模式允許您設定新朋友開始與您聊天所需的SNT數量。", + "tribute-to-talk-learn-more-2": "不在您的好友列表中的人都將被要求付款,並且您可以在他們確認後開始聊天。", + "tribute-to-talk-learn-more-3": "您可以隨時將錢匯回,但是要確保朋友們能與您聯繫,請先將他們新增為好友。", + "tribute-to-talk-paywall-learn-more-1": "時間和精力是我們最寶貴的資產。通過Tribute to Talk,您可以聯繫新朋友以換取SNT付款。", + "tribute-to-talk-paywall-learn-more-2": "要與通過認證的人開始聊天,只需支付所需的SNT,您就會被新增為好友。", + "tribute-to-talk-paywall-learn-more-3": "如果您認識他們,可以自由地在外部應用分享您在\"Status\"的個人資料連結。", + "tribute-to-talk-pending": "尚待確認", + "tribute-to-talk-pending-note": "交易正在網路上等待確認,您可以在交易記錄中查看其狀態", + "tribute-to-talk-removing-note": "關閉聊天收益模式,將使新朋友無需發送SNT即可開始聊天,需要完成交易確認。", + "tribute-to-talk-set-snt-amount": "設定開始新聊天所需的SNT數量", + "tribute-to-talk-signing": "等待簽署交易", + "tribute-to-talk-transaction-failed-note": "交易失敗,您的“Tribute to Talk”設定並未更改", + "tribute-to-talk-tribute-received1": "接收已完成,您和", + "tribute-to-talk-tribute-received2": "現在是好友,可以安全地彼此聊天。", + "tribute-to-talk-you-require-snt": "您需要SNT才能與新朋友開始聊天。", + "try-again": "再試一次", + "try-keeping-the-card-still": "嘗試保持卡靜止", + "turn-nfc-description": "您的設備上禁用了NFC。您可以在設定中啟用它", + "turn-nfc-on": "開啟NFC以繼續", + "two-minutes": "2分鐘", + "tx-fail-description1": "此交易很可能失敗。使用自定網路費需自擔風險。", + "tx-fail-description2": "此交易很可能失敗。請根據自身風險承擔能力設定自訂網路費。", + "type": "類型", + "type-a-message": "訊息", + "ulc-enabled": "啟用ULC", + "unable-to-fetch": "無法取得對話記錄", + "unable-to-read-this-code": "無法讀取此代碼", + "unable-to-send-messages": "無法傳送和接收訊息", + "unblock": "解除封鎖", + "unblock-contact": "取消封鎖此用戶", + "undo": "撤消", + "unknown-status-go-error": "status-go發生未知錯誤", + "unlimited": "無上限", + "unlock": "解鎖", + "unmute": "取消靜音", + "unmute-chat": "取消聊天静音", + "unmute-community": "取消静音社区", + "unpair-card": "卡片配對取消", + "unpair-card-confirmation": "此操作將取消與當前設備的卡配對。需要6位元密碼授權。您要繼續嗎?", + "unpair-keycard": "取消與此手機的金鑰卡配對", + "unpair-keycard-warning": "您的配對代碼\/PUK和PIN維持不變", + "unpaired-keycard-text": "您使用的金鑰卡與此手機不相符", + "unpaired-keycard-title": "看來您的卡已取消配對", + "unpin": "取消釘選", + "unpin-from-channel": "取消频道内置顶消息", + "unpin-from-chat": "从聊天中取消置顶", + "update": "更新", + "update-to-listen-audio": "更新到最新版本,收聽語音訊息!", + "update-to-see-image": "更新到最新版本,欣賞精美圖像!", + "update-to-see-sticker": "更新到最新版本,查看精美貼圖!", + "updates-to-tos": "使用條款更新", + "updates-to-tos-desc": "在繼續之前,請查看使用條款並確認您對於此應用程式的使用方式承擔全部責任。", + "url": "網址", + "usd-currency": "美元", + "use-as-profile-picture": "設為個人頭像", + "use-valid-contact-code": "請輸入或掃描有效的聊天金鑰或用戶名", + "validation-amount-invalid-number": "金額不是有效數字", + "validation-amount-is-too-precise": "金額太精確了。小數點後的位數最多為{{decimals}}", + "verified-community": "✓經過驗證的社群", + "version": "應用程式版本", + "view": "查看", + "view-community-rules": "查看社区规则", + "view-cryptokitties": "在謎戀貓中查看", + "view-cryptostrikers": "在CryptoStrikers中查看", + "view-data": "查看資料", + "view-details": "查看詳情", + "view-etheremon": "在Etheremon中查看", + "view-gitcoin": "在Gitcoin中查看", + "view-members": "查看成员", + "view-on-opensea": "在OpenSea中查看", + "view-pinned-messages": "查看置顶消息", + "view-profile": "查看資料", + "view-public-dashboard": "查看公共統計資料面板", + "view-rules": "查看規則", + "view-signing": "查看簽署詞組", + "view-superrare": "在SuperRare中查看", + "vote-to-feature": "投票推荐此社区", + "waiting-for-wifi": "沒有Wi-fi,無法更新訊息。", + "waiting-for-wifi-change": "設定", + "waiting-to-sign": "正在等待簽署交易...", + "waiting-wi-fi": "正在等待Wi-Fi…", + "waku-bloom-filter-mode": "Waku Bloom過濾器模式", + "wakuv2-change-nodes": "您確定要更改 Wakuv2 節點嗎?", + "wakuv2-node-format": "\/ip4\/{node-ip}\/tcp\/{port}\/p2p\/{id}", + "wakuv2-settings": "Waku v2 設定", + "wallet": "錢包", + "wallet-address": "錢包地址", + "wallet-asset": "資產", + "wallet-assets": "資產", + "wallet-backup-recovery-title": "備份您的種子詞組", + "wallet-choose-recipient": "選擇收款人", + "wallet-collectibles": "收藏", + "wallet-connect": "錢包連結", + "wallet-connect-2.0": "錢包連結2.0", + "wallet-connect-app-connected": "已連結", + "wallet-connect-go-back": "返回您的瀏覽器或dapp", + "wallet-connect-proposal-description": "連結後,將授權{{name}}檢索您的錢包地址並啟用Web3", + "wallet-connect-proposal-title": "請求連結到您的錢包", + "wallet-insufficient-funds": "餘額不足", + "wallet-insufficient-gas": "ETH不足以支付gas", + "wallet-invalid-address": "地址錯誤: \n {{data}}", + "wallet-invalid-address-checksum": "地址錯誤: \n {{data}}", + "wallet-invalid-chain-id": "網路不符: \n {{data}}但目前使用{{chain}}", + "wallet-key-content": "基於乙太坊標準並以0x開頭的64個字元的十六進制位址。向大眾公開,當您想接收資金時,您的帳戶位址會與其他人共用。也稱為“乙太坊位址”或“錢包地址”。", + "wallet-key-title": "帳號地址", + "wallet-manage-accounts": "帳戶管理", + "wallet-manage-app-connections": "管理應用程式連結", + "wallet-manage-assets": "管理資產", + "wallet-request": "請求", + "wallet-send": "發送", + "wallet-send-min-units": "最少21000 units", + "wallet-send-min-wei": "最少1 wei", + "wallet-settings": "錢包設定", + "wallet-total-value": "總值", + "wallet-transaction-total-fee": "總交易費用", + "wants-to-access-profile": "想要訪問您的個人資料", + "warning": "警告", + "warning-message": "抱歉,我們設定了連續發送上限以防止垃圾郵件。請稍後重試", + "warning-sending-to-contract-descr": "您輸入的地址是智能合約,向該地址發送資金可能會導致損失。如要操作DApp功能,請在Status瀏覽器中打開DApp。", + "watch-only": "監看", + "wc-brand-guide": "商標和品牌圖像的使用指引", + "wc-disclaimer": "免責聲明(包括第三方提供商)、保固和法律聲明", + "wc-dispute": "爭議解決條款", + "wc-how-to-use-status-app": "如何使用Status應用程式,並涵蓋隱私和安全", + "wc-new-tos-based-on-principles-prefix": "基於設計原則而更新的使用條款", + "web-view-error": "無法讀取頁面", + "websites": "網站", + "webview-camera-permission-requests": "Webview相機權限請求", + "webview-camera-permission-requests-subtitle": "啟用後,網站和dapp可能會要求使用您的相機", + "welcome-blank-community-message": "您的社群列表將出現在這裡。", + "welcome-blank-message": "您的聊天記錄將顯示在此處。要開始新聊天,請按⊕按鈕", + "welcome-community-blank-message": "您的頻道將顯示在此處。要建立新頻道,請單擊 ⊕ 按鈕並選擇“建立頻道”", + "welcome-community-blank-message-edit-chats": "您的頻道將顯示在此處。要建立新頻道,請返回社群畫面,單擊 ⊕ 按鈕並選擇“建立頻道”", + "welcome-screen-text": "設定錢包,邀請朋友聊天\n並瀏覽流行的dapp!", + "welcome-to-status": "歡迎來到Status!", + "welcome-to-status-description": "設定您的加密錢包,邀請朋友聊天和瀏覽分散式應用程式", + "what-changed": "變動更新內容", + "what-is-shared": "共享的内容", + "whats-on-your-mind": "你想到了些什麼…", + "whats-trending": "查看热点", + "word-count": "字數", + "word-n": "單字#{{number}}", + "word-n-description": "為檢查您是否已正確備份種子詞組,請在上方輸入編號# {{number}}的單字。", + "words-n": { + "other": " {{count}} words " + }, + "write-down-and-store-securely": "寫下代碼\n並存放在安全的位置", + "wrong-address": "地址錯誤", + "wrong-card": "卡片錯誤", + "wrong-card-text": "您使用的卡與您選擇的帳戶金鑰不符。", + "wrong-contract": "合約錯誤", + "wrong-keycard-text": "您使用的金鑰卡與此手機不相符", + "wrong-keycard-title": "您似乎使用\n錯誤的金鑰卡", + "wrong-password": "密碼錯誤", + "wrong-word": "輸入錯誤", + "yes": "是", + "you": "您", + "you-already-have-an-asset": "您已經擁有{{value}}", + "you-are-all-set": "設定完成!", + "you-are-all-set-description": "如果您遺失了手機,您現在可以使用種子詞組來重新存取您的資產與聊天金鑰。", + "you-can-change-account": "您可以根據需要更改帳戶名稱和顏色", + "you-can-choose-preview-websites": "您可以選擇以下哪個網站可以預覽聊天中的敘述和圖片連結", + "you-can-fetch": "您可以取得對話記錄", + "you-dont-have-contacts": "您還沒有任何好友。", + "you-dont-have-contacts-invite-friends": "您還沒有任何好友。\n邀請您的朋友開始聊天。", + "you-dont-have-stickers": "您還沒有任何貼圖", + "you-will-need-this-code": "您需要此代碼才能打開狀態並簽署交易", + "you-will-start-from-scratch": "您將從頭開始,使用一組新的金鑰", + "your-card-is-frozen": "您的金鑰卡已被凍結,請重置卡片存取權限", + "your-contact-code": "同意授權此DApp檢索您的聊天金鑰", + "your-data-belongs-to-you": "如果您遺失種子詞組,您將失去所有的數據資料和資金", + "your-data-belongs-to-you-description": "如果您遺失存取裝置,例如:您的手機,您將只能使用種子詞組取回個人密鑰。切勿讓他人得知您的種子詞組,請立刻寫下這些字詞,並妥善維護其安全。", + "your-keys": "您的金鑰", + "your-price-limit": "您的設定價格", + "your-recovery-phrase": "您的種子詞組", + "your-recovery-phrase-description": "這是您的種子詞組,能用它來證明這是您的錢包。您只會看到一次!請將其寫在紙上,並存放在安全的地方。如果裝置遺失或想重新安裝錢包,將需要它。", + "your-tip-limit": "您的手續費上限", + "youre-on-mobile-network": "您正在使用行動網路" +} diff --git a/translations/zh_hans.json b/translations/zh_hans.json new file mode 100644 index 00000000000..db6cea6405f --- /dev/null +++ b/translations/zh_hans.json @@ -0,0 +1,900 @@ +{ + "about-app": "关于", + "about-key-storage-content": "", + "about-key-storage-title": "", + "about-names-content": "", + "about-names-title": "", + "access-key": "", + "multiaccount-generation-message": "给我一点时间,我得疯狂地计算一下,才能生成你的帐户!", + "account-settings": "帐号设定", + "active-online": "在线", + "active-unknown": "未知", + "add": "添加", + "add-a-watch-account": "添加一个观察帐户", + "add-account": "添加帐号", + "add-an-account": "添加一个帐号", + "add-bootnode": "添加引导节点", + "add-contact": "增加联系人", + "add-custom-token": "添加自定义代币", + "add-existing-account": "添加已有帐号", + "add-mailserver": "添加邮件服务器", + "add-members": "添加成员", + "add-network": "添加网络", + "add-to-contacts": "添加到联系人", + "address": "地址", + "advanced": "", + "advanced-settings": "高级设置", + "agree-by-continuing": "继续授权给我们", + "all": "全部", + "allow": "允许", + "allowing-authorizes-this-dapp": "允许授权此 DApp 检索您的钱包地址, 并启用 Web3", + "already-have-asset": "你已经有了这种资产", + "amount": "金额", + "are-you-sure-description": "您将无法再次看到整个助记词", + "are-you-sure?": "你确定?", + "ask-in-status": "咨询问题或报告错误", + "at": "在", + "authorize": "授权", + "available": "可用", + "available-participants": { + "other": "你可以选择{{count}} 参与者。" + }, + "back": "返回", + "back-up-your-seed-phrase": "备份助记词", + "backup-recovery-phrase": "备份助记词", + "balance": "余额", + "begin-set-up": "开始设置", + "biometric-auth-android-sensor-desc": "触摸传感器", + "biometric-auth-android-sensor-error-desc": "失败", + "biometric-auth-android-title": "需要进行身份验证", + "biometric-auth-confirm-logout": "重新登录", + "biometric-auth-confirm-message": "生物识别身份验证需要连续,如果不能完成,请使用帐户密码重新登录", + "biometric-auth-confirm-title": "你必须验证!", + "biometric-auth-confirm-try-again": "再试一次", + "biometric-auth-error": "无法执行生物识别身份验证( {{code}} )", + "biometric-auth-login-error-title": "生物识别身份验证错误", + "biometric-auth-login-ios-fallback-label": "输入密码", + "biometric-auth-reason-login": "登录Status", + "biometric-auth-reason-verify": "验证身份", + "block": "块", + "block-contact": "屏蔽联系人", + "block-contact-details": "屏蔽将删除此用户之前的所有消息,并拒收新消息", + "blocked-users": "被屏蔽的用户", + "bootnode-address": "引导节点地址", + "bootnode-details": "引导节点详细信息", + "bootnode-format": "enode:\/\/{enode-id}@{ip-address}:{port}", + "bootnodes": "引导节点", + "bootnodes-enabled": "引导节点启用", + "bootnodes-settings": "引导节点设置", + "browsed-websites": "浏览过的网站将在此处显示。", + "browser": "浏览器", + "browser-not-secure": "连接不安全!不要在本网站上签署交易或发送个人数据。", + "browser-secure": "连接是安全的。在签署交易或输入个人数据之前, 请确保您确实信任此网站。", + "browsers": "浏览器", + "browsing-cancel": "取消", + "browsing-open-in-android-web-browser": "在网络浏览器中打开", + "browsing-open-in-ios-web-browser": "在网络浏览器中打开", + "browsing-open-in-status": "在Status中打开", + "browsing-site-blocked-description1": "我们从此地址检测到潜在的恶意活动。为了保护您和您的钱包,我们阻止了进一步的引导。 \n\n如果您认为这是一个错误,请告诉我们", + "browsing-site-blocked-description2": "公聊", + "browsing-site-blocked-go-back": "返回", + "browsing-site-blocked-title": "此站点被阻止", + "browsing-title": "浏览", + "camera-access-error": "要授予所需的摄像机许可,请转到系统设置,并确定选中了 “Status” > “摄像机”。", + "can-not-add-yourself": "不能添加自己", + "cancel": "取消", + "cancel-keycard-setup": "", + "cannot-read-card": "无法读卡。\n请把卡放在你的手机后面", + "cannot-use-default-pin": "PIN 码000000是无效的。 \n请使用其他码", + "card-is-blank": "这是一张空白卡", + "card-reseted": "卡片已被重置", + "card-unpaired": "卡片已从当前设备删除配对", + "change-fleet": "变更组别到{{fleet}}", + "change-logging-enabled": "您确定要{{enable}}日志记录吗?", + "change-pin": "更改PIN码", + "changed-amount-warning": "数量已从{{old}}更改为{{new}}", + "changed-asset-warning": "资产已从{{old}}更改为{{new}}", + "chaos-mode": "混乱模式", + "chaos-unicorn-day": "混乱独角兽日", + "chaos-unicorn-day-details": "🦄🦄🦄🦄🦄🦄🦄🚀!", + "chat": "聊天", + "chat-name": "聊天名称", + "chat-settings": "对话设置", + "chats": "聊天", + "check-your-recovery-phrase": "检查您的助记词", + "choose-authentication-method": "选择一种身份验证方法来保护您的帐户", + "clear": "清空", + "clear-history": "清空历史记录", + "clear-history-action": "清空", + "clear-history-confirmation": "清空历史记录?", + "clear-history-confirmation-content": "您确定要清除此聊天记录吗?", + "clear-history-title": "清空历史记录?", + "close-app-button": "确认", + "close-app-content": "应用将会停止并关闭。当你重新打开应用,选中的网络将会被使用", + "close-app-title": "警告!", + "command-button-send": "发送", + "complete-hardwallet-setup": "现在此卡是您帐户安全的重要组成部分。没有它就无法发送交易。", + "completed": "已完成", + "confirm": "确认", + "confirmations": "确认", + "confirmations-helper-text": "请等待达到至少12个确认,来确保交易已被安全地处理。", + "connect": "连接", + "connect-mailserver-content": "连接到{{name}} ?", + "connected": "已连接", + "connecting": "正在连接...", + "connecting-requires-login": "连接其它网络需要登录", + "connection-with-the-card-lost": "", + "connection-with-the-card-lost-text": "", + "contact-code": "联系码", + "contact-s": { + "other": "联系人" + }, + "contacts": "联系人", + "continue": "继续", + "contract-address": "合约地址", + "contract-interaction": "合约交互", + "copy-info": "复制信息", + "copy-qr": "复制代码", + "copy-to-clipboard": "复制", + "copy-transaction-hash": "复制交易哈希", + "cost-fee": "成本\/费用", + "counter-9-plus": "9+", + "create": "创建", + "create-a-pin": "创建PIN码", + "create-multiaccount": "创建帐号", + "create-group-chat": "创建群聊", + "create-pin": "创建PIN码", + "create-pin-description": "你需要卡+本PIN码才能登录和确认交易", + "created-group-chat-description": "您创建了群{{group-name}}", + "cryptokitty-name": "CryptoKitty# {{id}}", + "currency": "货币", + "currency-display-name-aed": "阿联酋迪拉姆", + "currency-display-name-afn": "阿富汗尼", + "currency-display-name-ars": "阿根廷比索", + "currency-display-name-aud": "澳元", + "currency-display-name-bbd": "巴巴多斯元", + "currency-display-name-bdt": "孟加拉国塔卡", + "currency-display-name-bgn": "保加利亚列弗", + "currency-display-name-bhd": "巴林第纳尔", + "currency-display-name-bnd": "文莱元", + "currency-display-name-bob": "玻利维亚玻利维亚诺", + "currency-display-name-brl": "巴西雷亚尔", + "currency-display-name-btn": "不丹努扎姆", + "currency-display-name-cad": "加拿大元", + "currency-display-name-chf": "瑞士法郎", + "currency-display-name-clp": "智利比索", + "currency-display-name-cny": "人民币", + "currency-display-name-cop": "哥伦比亚比索", + "currency-display-name-crc": "哥斯达黎加科郎", + "currency-display-name-czk": "捷克克朗", + "currency-display-name-dkk": "丹麦克朗", + "currency-display-name-dop": "多米尼加比索", + "currency-display-name-egp": "埃及镑", + "currency-display-name-etb": "埃塞俄比亚比尔", + "currency-display-name-eur": "欧元", + "currency-display-name-gbp": "英镑", + "currency-display-name-gel": "格鲁吉亚拉里", + "currency-display-name-ghs": "加纳塞地", + "currency-display-name-hkd": "港币", + "currency-display-name-hrk": "克罗地亚库纳", + "currency-display-name-huf": "匈牙利福林", + "currency-display-name-idr": "印度尼西亚卢比", + "currency-display-name-ils": "以色列新谢克尔", + "currency-display-name-inr": "印度卢比", + "currency-display-name-isk": "冰岛克朗", + "currency-display-name-jmd": "牙买加元", + "currency-display-name-jpy": "日元", + "currency-display-name-kes": "肯尼亚先令", + "currency-display-name-krw": "韩元", + "currency-display-name-kwd": "科威特第纳尔", + "currency-display-name-kzt": "哈萨克斯坦腾格", + "currency-display-name-lkr": "斯里兰卡卢比", + "currency-display-name-mad": "摩洛哥迪拉姆", + "currency-display-name-mdl": "摩尔多瓦列伊", + "currency-display-name-mur": "毛里求斯卢比", + "currency-display-name-mwk": "马拉维克瓦查", + "currency-display-name-mxn": "墨西哥比索", + "currency-display-name-myr": "马来西亚林吉特", + "currency-display-name-mzn": "莫桑比克美提卡", + "currency-display-name-nad": "纳米比亚元", + "currency-display-name-ngn": "尼日利亚奈拉", + "currency-display-name-nok": "挪威克朗", + "currency-display-name-npr": "尼泊尔卢比", + "currency-display-name-nzd": "新西兰元", + "currency-display-name-omr": "阿曼里亚尔", + "currency-display-name-pen": "秘鲁新索尔", + "currency-display-name-pgk": "巴布亚新几内亚基那", + "currency-display-name-php": "菲律宾比索", + "currency-display-name-pkr": "巴基斯坦卢比", + "currency-display-name-pln": "波兰兹罗提", + "currency-display-name-pyg": "巴拉圭瓜拉尼", + "currency-display-name-qar": "卡塔尔里亚尔", + "currency-display-name-ron": "罗马尼亚列伊", + "currency-display-name-rsd": "突尼斯第纳尔", + "currency-display-name-rub": "俄罗斯卢布", + "currency-display-name-sar": "沙特阿拉伯里亚尔", + "currency-display-name-sek": "瑞典克朗", + "currency-display-name-sgd": "新加坡元", + "currency-display-name-thb": "泰国泰铢", + "currency-display-name-try": "土耳其里拉", + "currency-display-name-ttd": "特立尼达和多巴哥元", + "currency-display-name-twd": "新台币", + "currency-display-name-tzs": "坦桑尼亚先令", + "currency-display-name-uah": "乌克兰格里夫纳", + "currency-display-name-ugx": "乌干达先令", + "currency-display-name-usd": "美元", + "currency-display-name-uyu": "乌拉圭比索", + "currency-display-name-vef": "委内瑞拉玻利瓦尔", + "currency-display-name-vnd": "越南盾", + "currency-display-name-zar": "南非兰德", + "current-network": "当前网络", + "current-pin": "输入PIN码", + "current-pin-description": "输入您的PIN码以继续", + "custom": "自 定义", + "custom-networks": "自定义网络", + "dapp": "ÐApp", + "dapp-would-like-to-connect-wallet": "想连接到您的钱包吗", + "dapps": "ÐApps", + "dapps-permissions": "DApp权限", + "data": "数据", + "datetime-ago": "前", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "datetime-day": { + "other": "天" + }, + "datetime-hour": { + "other": "小时" + }, + "datetime-minute": { + "other": "分钟" + }, + "datetime-second": { + "other": "秒" + }, + "datetime-today": "今天", + "datetime-yesterday": "昨天", + "decimals": "小数点", + "decryption-failed-content": "我们无法解密您的数据,您可能需要创建新帐户并且点击 “套用” 来清除旧数据。如果点击 “取消”,系统将再尝试一次。", + "default": "默认", + "delete": "刪除", + "delete-and-leave-group": "删除并离开群组", + "delete-bootnode": "删除引导节点", + "delete-bootnode-are-you-sure": "您确定要删除这个引导节点吗?", + "delete-bootnode-title": "删除 bootnode", + "close-chat": "刪除聊天", + "close-chat-confirmation": "您确定要删除此聊天记录吗?", + "delete-confirmation": "刪除", + "delete-mailserver": "删除邮件服务器", + "delete-mailserver-are-you-sure": "您确定要删除此邮件服务器吗?", + "delete-mailserver-title": "删除邮件服务器", + "delete-message": "删除消息", + "delete-network-confirmation": "您确定要删除此网络吗?", + "delete-network-error": "请在删除此网络之前连接到其他网络", + "delete-network-title": "删除网络?", + "deny": "拒绝", + "description": "简介", + "dev-mode": "开发模式", + "devices": "设备", + "disable": "禁用", + "disabled": "禁用", + "disconnected": "正在连接到对等方..。", + "discover": "发现", + "done": "完成", + "edit": "编辑", + "edit-profile": "编辑个人资料", + "empty-chat-description": "此聊天中还没有消息", + "empty-chat-description-one-to-one": "您在这里发送的任何邮件都是加密的, 只能由您读取并", + "empty-chat-description-public": "最后{{quiet-hours}}在这里{{quiet-hours}} 。开始对话或", + "empty-chat-description-public-share-this": "分享此聊天。", + "enable": "启用", + "encrypt-with-password": "", + "ens-10-SNT": "10 SNT", + "ens-add-username": "添加用户名", + "ens-agree-to": "同意", + "ens-chat-settings": "聊天设置", + "ens-custom-domain": "自定义域名", + "ens-custom-username-hints": "输入完整用户名,包括自定义域,例如username.domain.eth", + "ens-custom-username-taken": "用户名不属于您:(", + "ens-deposit": "充值", + "ens-displayed-with": "您的消息会显示给其他人", + "ens-get-name": "获取通用用户名", + "ens-got-it": "好的, 明白了", + "ens-locked": "用户名已锁定。您将无法在{{date}}之前释放它", + "ens-network-restriction": "仅适用于主网或ropsten", + "ens-no-usernames": "您没有关联任何用户名", + "ens-powered-by": "由以太坊名称服务提供支持", + "ens-primary-username": "主用户名", + "ens-register": "注册", + "ens-registration-failed": "要注册用户名,请重试。", + "ens-registration-failed-title": "交易失败", + "ens-release-username": "释放用户名", + "ens-remove-hints": "删除将从密钥中分离用户名。", + "ens-remove-username": "删除用户名", + "ens-saved": " 现在已与您的密钥相关联,可以在Status中使用了。", + "ens-saved-title": "用户名已添加", + "ens-show-username": "在聊天中显示我的ENS用户名", + "ens-terms-header": "名称注册条款", + "ens-terms-point-1": "资金存入1年。您的 snt 将被锁定, 但不会被花费。", + "ens-terms-point-10": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e(ENS注册表)。", + "ens-terms-point-2": "1 年后,您可以释放名称并赎回您的押金,或者不采取任何措施保留该名称。", + "ens-terms-point-3": "如果合约条款发生变化(例如,Status升级合约),用户有权释放用户名,无论持有时间长短。", + "ens-terms-point-4": "合约控制者无法访问您缴纳的押金。它们只能原路返回发送它们的钱包地址。", + "ens-terms-point-5": "您的地址将与您的ENS名称公开关联。", + "ens-terms-point-6": "用户名创建为stateofus.eth的子节点,并受ENS智能合约条款的约束。", + "ens-terms-point-7": "您授权合约代表您转发 SNT。仅当您批准交易以授权转移时,才会出现这种情况。", + "ens-terms-point-8": "这些条款由地址的智能合约逻辑保证:", + "ens-terms-point-9": "{{address}} (Status用户名注册器)", + "ens-terms-registration": "名称注册条款。", + "ens-test-message": "嘿", + "ens-transaction-pending": "交易待确定......", + "ens-understand": "我明白我的钱包地址将公开连接到我的用户名。", + "ens-username-connected": "此用户名归您所有,并与您的聊天密钥相关联。", + "ens-username-hints": "至少 4 个字符。仅限拉丁字母、数字和小写字母。", + "ens-username-invalid": "只限字母和数字", + "ens-username-owned": "✓用户名归您所有。", + "ens-username-available": "✓用户名可用!", + "ens-username-taken": "用户名已被占用:(", + "ens-usernames": "ENS用户名", + "ens-usernames-details": "注册通用用户名,以便其他用户轻松找到你", + "wallet-address": "钱包地址", + "ens-want-custom-domain": "我在另一个域名上拥有一个名称", + "ens-want-domain": "我想要一个stateofus.eth域名", + "ens-welcome-hints": "ENS名称将那些冗长的地址转换为唯一的用户名。", + "ens-your-username": "您的用户名", + "ens-your-usernames": "您的用户名", + "ens-your-your-name": "您的ENS名称", + "enter-12-words": "输入您的助记词中的12个单词,并使用单个空格隔开", + "enter-contact-code": "输入联系人代码或用户名", + "enter-pair-code": "输入配对码", + "enter-pair-code-description": "需要将你的卡连接到此设备", + "enter-password": "输入密码", + "enter-pin": "输入PIN码", + "enter-puk-code": "输入PUK码", + "enter-puk-code-description": "PIN码已锁定。 \n请输入PUK码以解锁PIN码。", + "enter-url": "输入网址", + "enter-word": "输入单词", + "enter-your-password": "", + "error": "错误", + "error-unable-to-get-balance": "无法获取余额", + "error-unable-to-get-prices": "货币转换错误,请刷新屏幕以重试。", + "error-unable-to-get-token-balance": "无法获取代币余额", + "errors": "错误", + "eth": "ETH", + "ethereum-node-started-incorrectly-description": "以太网节点启动配置不正确,应用程序将停止从该条件恢复。已配置的网络ID = {{network-id}} ,实际网络ID = {{fetched-network-id}}", + "ethereum-node-started-incorrectly-title": "以太坊节点启动不正确", + "etherscan-lookup": "在Etherscan上查看", + "export-account": "导出帐户", + "failed": "失败", + "faq": "常见问题", + "fetch-messages": "↓获取消息", + "find": "查找", + "finishing-card-setup": "卡片设置完成", + "fleet": "组别", + "fleet-settings": "组别设置", + "from": "从", + "gas-limit": "Gas 限制", + "gas-price": "Gas 价格", + "gas-used": "使用的 Gas", + "generate-a-key": "", + "generate-a-new-key": "", + "generating-codes-for-pairing": "正在生成与您的Status帐户配对的代码。", + "generating-keys": "", + "generating-mnemonic": "生成助记词", + "get-started": "开始", + "get-status-at": "在http:\/\/status.im获取Status", + "get-stickers": "获取贴纸", + "go-to-settings": "前往设置", + "got-it": "知道了", + "group-chat": "群聊", + "group-chat-admin": "管理员", + "group-chat-admin-added": "* {{member}} *已成为管理员", + "group-chat-created": "* {{member}} *创建了群* {{name}} *", + "group-chat-decline-invitation": "拒绝邀请", + "group-chat-member-added": "* {{member}} *加入了群", + "group-chat-member-joined": "* {{member}} *已加入该群组", + "group-chat-member-removed": "* {{member}} *离开了群", + "group-chat-members-count": "{{selected}} \/ {{max}}成员", + "group-chat-name-changed": "* {{member}} *将群名称更改为* {{name}} *", + "group-chat-no-contacts": "您还没有任何联系人。 \n邀请您的朋友开始聊天", + "group-info": "群信息", + "gwei": "GWEI", + "hash": "哈希", + "help": "帮助", + "help-capitalized": "帮助", + "help-center": "帮助中心", + "history": "历史记录", + "hold-card": "将卡放在手机背面", + "home": "消息", + "hooks": "挂钩", + "identifier": "识别码", + "image-remove-current": "删除当前照片", + "image-source-gallery": "从图库中选择", + "image-source-make-photo": "拍摄", + "image-source-title": "个人资料图片", + "importing-keycard-account": "导入keycard帐户", + "in-contacts": "已添加联系人", + "incoming": "呼入", + "incorrect-code": [ + "str", + "抱歉,代码不正确,请重新输入" + ], + "initialization": "初始化", + "install": "安装", + "intro-message1": "欢迎来到Status\n点击该消息来设置你的密码并开始!", + "intro-privacy-policy-note1": "", + "intro-privacy-policy-note2": "", + "intro-text": "Status 是您通向分散网络的门户", + "intro-text1": "", + "intro-text2": "", + "intro-text3": "", + "intro-title1": "", + "intro-title2": "", + "intro-title3": "", + "intro-wizard-text1": "", + "intro-wizard-text2": "", + "intro-wizard-text3": "", + "intro-wizard-text4": "", + "intro-wizard-text6": "", + "intro-wizard-title-alt4": "", + "intro-wizard-title-alt5": "", + "intro-wizard-title1": "", + "intro-wizard-title2": "", + "intro-wizard-title3": "", + "intro-wizard-title4": "", + "intro-wizard-title5": "", + "intro-wizard-title6": "", + "invalid-format": "无效的格式\n必须是{{format}}", + "invalid-key-confirm": "应用", + "invalid-key-content": "为了保护自己,您需要创建新帐号并通过点击“应用”来清除旧数据。如果您已有帐号并想保存助记词,请选择“取消”,备份并重新启动应用程序。我们强烈建议您创建新帐号,因为旧帐号未加密存储。", + "invalid-number": "无效号码", + "invalid-pairing-password": "配对密码无效", + "invalid-range": "无效的格式\n必须是在{{min}}与{{max}}之间", + "invite-friends": "邀请朋友", + "invited": "已邀请", + "join-group-chat": "加入群", + "join-group-chat-description": "{{username}}邀请您加入群组{{group-name}}", + "joined-group-chat-description": "您通过来自{{username}}的邀请加入{{group-name}}", + "key": "密钥", + "keycard": "Keycard", + "keycard-applet-install-instructions": "要安装applet,请按照https:\/\/github.com\/status-im\/keycard-cli#keycard-applet-installation上的说明进行操作。", + "keycard-blocked": "Keycard已被冻结。 \n您需要重置卡片才能继续使用它。", + "keycard-cancel-setup-text": "", + "keycard-cancel-setup-title": "", + "keycard-desc": "", + "keycard-has-multiaccount-on-it": "此卡上已有帐户。如果您想更改它,请先登录并重置您的卡。如果您要导入Keycard帐户,请使用“添加现有帐户”", + "keycard-onboarding-finishing-header": "", + "keycard-onboarding-intro-header": "", + "keycard-onboarding-intro-text": "", + "keycard-onboarding-pairing-header": "", + "keycard-onboarding-preparing-header": "", + "keycard-onboarding-puk-code-header": "", + "keycard-onboarding-recovery-phrase-description": "", + "keycard-onboarding-recovery-phrase-header": "", + "keycard-onboarding-recovery-phrase-text": "", + "keycard-onboarding-start-header": "", + "keycard-onboarding-start-step1": "", + "keycard-onboarding-start-step1-text": "", + "keycard-onboarding-start-step2": "", + "keycard-onboarding-start-step2-text": "", + "keycard-onboarding-start-step3": "", + "keycard-onboarding-start-step3-text": "", + "keycard-onboarding-start-text": "", + "keycard-recovery-phrase-confirm-header": "", + "keycard-recovery-phrase-confirmation-text": "", + "keycard-recovery-phrase-confirmation-title": "", + "keycard-unauthorized-operation": "您未经授权执行此操作。 \n请选择有效卡,然后重试。", + "learn-more": "学到更多", + "learn-more-about-keycard": "", + "leave": "离开", + "leave-group": "退群", + "left": "剩余", + "linked-on": "已与{{date}}关联", + "load-messages-before": "{{date}} 之前", + "load-more-messages": "↓获取更多消息", + "loading": "载入中...", + "log-level": "日志级别", + "log-level-settings": "日志级别设置", + "logging": "日志记录", + "logging-enabled": "启用日志记录吗?", + "login-pin-description": "输入您的PIN码并登录\n到您的帐户", + "logout": "登出", + "logout-app-content": "该帐户将被注销。再次登录时,将使用所选的网络", + "logout-are-you-sure": "您确定您要登出吗?", + "logout-title": "登出?", + "mailserver-address": "邮件服务器地址", + "mailserver-automatic": "自动选择", + "mailserver-connection-error": "无法连接到邮件服务器", + "mailserver-details": "邮件服务器详细信息", + "mailserver-error-content": "无法访问您选择的邮件服务器。", + "mailserver-error-title": "连接到邮件服务器时出错", + "mailserver-format": "enode:\/\/{enode-id}:{password}@{ip-address}:{port}", + "mailserver-pick-another": "选择其他邮件服务器", + "mailserver-reconnect": "无法连接到邮件服务器,请点击重新连接", + "mailserver-request-error-content": "邮件服务器返回以下错误: {{error}}", + "mailserver-request-error-status": "获取历史消息时发生错误,请检查日志以获取详细信息", + "mailserver-request-error-title": "邮件服务器请求错误", + "mailserver-request-retry": "重试请求", + "mailserver-retry": "重试", + "main-currency": "主要货币", + "main-networks": "主网", + "main-wallet": "主钱包", + "mainnet-network": "主网", + "make-admin": "设置管理员", + "members": { + "other": "{{count}} 个成员" + }, + "members-active": { + "other": "{{count}} 个成员" + }, + "members-active-none": "没有成员", + "members-title": "成员", + "message": "信息", + "message-not-sent": "信息未发送", + "message-options-cancel": "取消", + "message-reply": "回复", + "messages": "消息", + "might-break": "可能会破坏一些ÐApp", + "migrations-failed-content": "{{message}} \n架构版本:初始{{initial-version}} ,当前{{current-version}} ,最后{{last-version}} \n\n请在#status公共聊天中告诉我们此问题。如果按“取消”按钮,则不会产生任何后果。如果按“ {{erase-accounts-data-button-text}} ”按钮,帐户的数据库将被删除,您将能够解锁帐户。所有帐户的数据都将丢失。", + "mobile-network-ask-me": "当在移动网络上时询问我", + "mobile-network-continue-syncing": "继续同步", + "mobile-network-continue-syncing-details": "您可以稍后在设置中更改此设置", + "mobile-network-go-to-settings": "前往设置", + "mobile-network-settings": "移动数据", + "mobile-network-sheet-configure": "您可以配置更多同步\n细节", + "mobile-network-sheet-offline": "离线,等待Wi-Fi", + "mobile-network-sheet-offline-details": "使用移动网络同步已关闭", + "mobile-network-sheet-remember-choice": "记住我的选择", + "mobile-network-sheet-settings": "设置", + "mobile-network-start-syncing": "开始同步", + "mobile-network-stop-syncing": "停止同步", + "mobile-network-stop-syncing-details": "直到连接到Wi-Fi?", + "mobile-network-use-mobile": "使用移动数据", + "mobile-network-use-mobile-data": "在同步聊天时,Status 会使用大量数据流量。您可以选择不在移动网络上同步", + "mobile-syncing-sheet-details": "在同步聊天时,Status 会使用大量数据流量。您可以选择不在移动网络上同步", + "mobile-syncing-sheet-title": "使用移动数据同步", + "more": "更多", + "name": "名称", + "name-of-token": "代币的名称", + "need-help": "需要帮助?", + "network": "网络", + "network-chain": "网络链", + "network-details": "网络详细信息", + "network-fee": "网络费", + "network-id": "网络 ID", + "network-invalid-network-id": "指定的网络ID与RPC url的网络ID不对应", + "network-invalid-status-code": "无效状态码: {{code}}", + "network-invalid-url": "无效网络网址", + "network-settings": "网络设置", + "new": "新建", + "new-chat": "新的聊天", + "new-contact": "新的联系人", + "new-contract": "新合约", + "new-group": "新增群组", + "new-group-chat": "新的群聊", + "new-network": "新建网络", + "new-pin-description": "输入新的PIN码", + "new-public-group-chat": "加入公共聊天", + "next": "下一个", + "no": "否", + "no-collectibles": "没有可用的收藏品", + "no-contacts": "还没有联系人", + "no-keycard-applet-on-card": "卡上没有Keycard小程序", + "no-messages": "无消息", + "no-pairing-slots-available": "没有可用配对位。 \n你可以解除已经与卡配对的设备配对", + "no-result": "没有结果", + "no-tokens-found": "没有找到代币", + "node-info": "节点信息", + "nonce": "随机数", + "none": "无", + "not-applicable": "对未签名交易不可用", + "notifications": "通知", + "notify": "通知", + "off": "关闭", + "offline": "离线", + "offline-messaging": "邮件服务器", + "offline-messaging-settings": "邮件服务器设置", + "ok": "确定", + "ok-continue": "好的,继续", + "ok-got-it": "好的, 明白了", + "okay": "确定", + "on": "开启", + "open": "打开", + "open-dapp": "打开ÐApp", + "open-dapp-store": "发现ÐApps", + "open-nfc-settings": "", + "open-on-etherscan": "打开 Etherscan.io", + "optional": "可选", + "other-accounts": "其他帐号", + "outgoing": "呼出", + "pair": "配对设备", + "pair-card": "配对卡", + "pair-code": "配对码", + "pair-code-explanation": "将卡与另一台具有相同Status帐户的设备配对", + "pair-this-device": "配对此设备", + "pair-this-device-description": "将您的设备配对以同步联系人并与他们聊天", + "paired-devices": "配对设备", + "pairing": "配对", + "pairing-card": "配对卡", + "pairing-go-to-installation": "配对设置", + "pairing-maximum-number-reached-content": "请在启用新设备之前禁用其中一个设备。", + "pairing-maximum-number-reached-title": "达到最大设备数", + "pairing-new-installation-detected-content": "已检测到新设备。 \n为了正确使用您的设备,在使用之前配对并启用它们。 \n请找到设置菜单下的设备项以配对您的设备。", + "pairing-new-installation-detected-title": "检测到新设备", + "pairing-no-info": "没有信息", + "pairing-please-set-a-name": "请为您的设备设置名称。", + "passphrase": "密码", + "password": "密码", + "password_error1": "密码不匹配。", + "password-description": "您需要此密码才能启用应用程序并确认交易。", + "password-placeholder2": "确认你的密码", + "paste": "粘贴", + "paste-json": "粘贴 JSON", + "pay-to-chat": "付钱聊天", + "peers": "对等方", + "pending": "等待中", + "pending-confirmation": "待确认...", + "permissions": "权限", + "phone-e164": "国际1", + "photos-access-error": "要授予所需的照片许可,请转到系统设置,并确保选中了 “Status” > “照片”。", + "pin-changed": "PIN码已更改", + "pin-code": "PIN 码", + "pin-mismatch": "PIN码不匹配", + "pin-retries-left": "您还剩{{number}}次重试机会", + "preview-privacy": "预览隐私模式", + "privacy-policy": "隐私权政策", + "processing": "处理中", + "product-information": "产品信息", + "profile": "个人资料", + "public-chat": "公聊", + "public-chats": "公共聊天", + "public-group-status": "公共", + "public-group-topic": "话题", + "public-key": "公钥", + "puk-and-pairing-codes-displayed": "显示PUK和配对码", + "puk-code": "PUK码", + "puk-code-explanation": "如果您失去访问权限,请解锁卡", + "puk-mismatch": "PUK码不匹配", + "quiet-days": "{{quiet-days}}天", + "quiet-hours": "{{quiet-hours}}小时", + "receive": "接收", + "receive-transaction": "接收交易", + "recent": "最近", + "recent-recipients": "联系人", + "recently-used-stickers": "最近使用的贴纸将出现在这里", + "recipient": "接收方", + "recipient-code": "输入收件人地址", + "recover": "恢复", + "recover-key": "", + "recover-keycard-multiaccount-not-supported": "不支持使用密码恢复keycard帐户", + "recovery-confirm-phrase": "确认密码", + "recovery-phrase": "恢复短语", + "recovery-typo-dialog-description": "如果输入错误的单词,您将创建一个新帐号,而不是恢复旧帐号。", + "recovery-typo-dialog-title": "此助记词正确吗?", + "remind-me-later": "", + "remove": "删除", + "remove-from-chat": "从聊天中删除", + "remove-network": "移除网络", + "remove-token": "删除代币", + "removed": "已删除", + "repeat-pin": "重复你的PIN码", + "report-bug-email-template": "1.问题描述\n {{description}} \n\n\n 2.重现的步骤\n {{steps}} \n\n\n 3.附上可以演示问题的屏幕截图\n", + "request-transaction": "请求交易", + "required-field": "必填字段", + "resend-message": "重新发送", + "reset-card": "重置卡片", + "reset-card-description": "此操作将卡片重置为初始状态。它将清除所有数据,包括私钥。操作不可逆。", + "retry": "重试", + "revoke-access": "撤消访问权限", + "rpc-url": "RPC 链接", + "save": "保存", + "save-password": "保存密码直到注销", + "save-password-unavailable": "设置设备码以保存密码", + "save-password-unavailable-android": "保存密码不可用:您的设备可能已被root或缺少必要的安全功能。", + "scan-qr": "扫描二维码", + "scan-qr-code": "扫描钱包地址的 QR 码", + "search": "搜索", + "secret-keys-confirmation-text": "请立即记录,你无法再一次查看本页面", + "secret-keys-confirmation-title": "你抄写下来了吗?", + "see-details": "查看详细信息", + "see-it-again": "再次核对", + "select-chat": "", + "selected": "选择", + "send-logs": "报告一个错误", + "send-logs-to": "向{{email}}报告错误", + "send-message": "发送消息", + "send-request": "发送请求", + "send-request-amount": "金额", + "send-request-amount-max-decimals": "最大小数位数为{{asset-decimals}}", + "send-request-unknown-token": "未知代币 - {{asset}}", + "send-sending-to": "到", + "send-transaction": "发送交易", + "sending": "正在发送...", + "sent-at": "发送于", + "set-a-topic": "创建主题", + "set-currency": "设置货币", + "settings": "设置", + "share": "分享", + "share-address": "", + "share-chat": "共享聊天", + "share-contact-code": "共享我的联系人代码", + "share-dapp-text": "查看我在Status上使用的这个DApp: {{link}}", + "share-link": "共享链接", + "share-my-profile": "分享我的个人资料", + "share-profile": "分享个人资料", + "share-profile-link": "共享配置文件链接", + "share-public-chat-text": "在Status应用中查看此公共聊天: {{link}}", + "sharing-copied-to-clipboard": "复制到剪贴板", + "sharing-copy-to-clipboard": "复制到剪贴板", + "sharing-share": "分享...", + "show-less": "收起", + "show-more": "显示更多", + "show-qr": "显示二维码", + "sign-in": "登录", + "sign-message": "签名消息", + "sign-with": "", + "sign-with-password": "使用密码签名", + "sign-you-in": "签入..。", + "signing": "签名", + "signing-a-message": "输入讯息...", + "signing-phrase": "签名短语", + "something-went-wrong": "出错", + "soon": "不久", + "specify-address": "指定地址", + "specify-name": "指定名称", + "specify-network-id": "指定网络ID", + "specify-rpc-url": "指定一个 RPC 链接", + "start-chat": "开始聊天", + "start-conversation": "开始对话", + "start-group-chat": "开始群聊", + "start-new-chat": "开始新对话", + "status": "状态", + "status-confirmed": "确定", + "status-hardwallet": "Status硬件钱包", + "status-keycard": "Status Keycard", + "status-not-sent-click": "未发送", + "status-not-sent-tap": "未发送。点击选项", + "status-pending": "等待中", + "status-sent": "已发送", + "status-tx-not-found": "未找到 TX", + "step-i-of-n": "步骤{{step}}\/{{number}}", + "sticker-market": "贴纸市场", + "submit": "", + "submit-bug": "提交错误", + "success": "成功", + "symbol": "符号", + "sync-all-devices": "同步所有设备", + "sync-in-progress": "正在同步...", + "sync-synced": "已同步", + "syncing-devices": "正在同步...", + "tag-was-lost": "标签丢失", + "test-networks": "测试网络", + "text-input-disabled": "请稍候...", + "this-device": "", + "this-device-desc": "", + "this-is-you-signing": "", + "this-will-take-few-seconds": "这将需要几秒钟", + "three-words-description": "", + "three-words-description-2": "", + "to": "到", + "to-block": "屏蔽", + "to-see-this-message": "查看此消息,", + "token-auto-validate-decimals-error": "地址{{address}}中的代币 {{symbol}}小数位错误 - 设置为{{expected}}但检测为{{actual}}", + "token-auto-validate-name-error": "地址{{address}}中的代币 {{symbol}}名称错误 - 设置为{{expected}}但检测为{{actual}}", + "token-auto-validate-symbol-error": "地址{{address}}中的代币 {{symbol}}符号错误 - 设置为{{expected}}但检测为{{actual}}", + "token-details": "代币详细信息", + "topic-name-error": "主题名称仅可使用小写字母( a 到 z )和短划线( - )", + "transaction": "交易", + "transaction-description": "请等待达到至少12个确认,来确保交易已被安全地处理。", + "transaction-details": "交易明细", + "transaction-failed": "交易失败", + "transaction-history": "交易记录", + "transaction-request": "交易请求", + "transaction-sent": "发送的交易", + "transactions": "交易", + "transactions-filter-select-all": "全部选中", + "transactions-filter-title": "过滤历史记录", + "type": "类型", + "transactions-history": "历史记录", + "transactions-history-empty": "暂无交易历史记录", + "transactions-sign": "签名", + "tribute-required-by-multiaccount": "{{multiaccount-name}}要求SNT开始聊天。", + "tribute-state-paid": "Tribute已付款", + "tribute-state-pending": "见面礼等待中", + "tribute-state-required": "需要{{snt-amount}} SNT致敬", + "tribute-to-talk": "见面礼对话", + "tribute-to-talk-add-friends": "添加朋友作为联系人以允许聊天而无需支付费用。", + "tribute-to-talk-are-you-friends": "你们是朋友吗?", + "tribute-to-talk-ask-to-be-added": "请求添加为联系人", + "tribute-to-talk-contact-received-your-tribute": "收到你的见面礼。你可以安全地互相聊天。", + "tribute-to-talk-desc": "通过收取陌生人首次与你聊天时支付的SNT,来货币化你的注意力。", + "tribute-to-talk-disabled": "见面礼对话未启用", + "tribute-to-talk-disabled-note": "从现在开始,新人可以在不发送SNT的情况下与您聊天。", + "tribute-to-talk-enabled": "您已启用Tribute to Talk。", + "tribute-to-talk-finish-desc": "从现在开始,您只会收到来自联系人和付款人的聊天信息 ", + "tribute-to-talk-learn-more-1": "您的时间和精力是您最宝贵的资产。 Tribute to Talk可让您设置陌生人与您开始聊天所需的SNT数量。", + "tribute-to-talk-learn-more-2": "任何不在您的联系人列表中的人都会被要求付款,您可以在他们付款后回复。", + "tribute-to-talk-learn-more-3": "您可以随时退款,但为了确保朋友可以自由联系您,请先将其添加为联系人。", + "tribute-to-talk-paywall-learn-more-1": "我们的时间和精力是我们最宝贵的资产。 Tribute to Talk让您联系新朋友以换取SNT付款。", + "tribute-to-talk-paywall-learn-more-2": "要与设置了Tribute的人开始聊天,只需支付所需的SNT,您就会被添加为联系人。", + "tribute-to-talk-paywall-learn-more-3": "如果你认识他们,则可以在Status之外分享您的个人资料以免费添加好友。", + "tribute-to-talk-pending": "见面礼等待确认", + "tribute-to-talk-pending-note": "见面礼交易正在等待网络确认。您可以在交易历史记录中查看其状态", + "tribute-to-talk-removing-note": "删除Tribute to Talk将允许陌生人在不发送SNT的情况下开始聊天。需要进行交易才能生效。", + "tribute-to-talk-set-snt-amount": "设置陌生人与你开始聊天所需的SNT数量", + "tribute-to-talk-signing": "正在等待签名交易", + "tribute-to-talk-transaction-failed-note": "交易失败,你的见面礼谈话设置未更改", + "tribute-to-talk-tribute-received1": "Tribute收到了. You and ", + "tribute-to-talk-tribute-received2": "现在是联系人,可以安全地互相聊天。", + "tribute-to-talk-you-require-snt": "您需要收到 SNT 才能允许新人与您聊天。", + "try-again": "再试一次", + "turn-nfc-on": "开启NFC", + "type-a-message": "输入讯息...", + "unable-to-read-this-code": "无法读取此代码", + "unblock-contact": "取消屏蔽联系人", + "unknown-status-go-error": "未知的 status-go 错误", + "unlock": "", + "unpair-card": "未配对卡片", + "unpair-card-confirmation": "此操作将取消当前设备的卡。需要PIN授权。你想继续吗?", + "update": "更新", + "url": "链接", + "usd-currency": "USD", + "use-valid-contact-code": "请输入或扫描有效的联系人代码或用户名", + "validation-amount-invalid-number": "金额不是一个有效的数字", + "validation-amount-is-too-precise": "金额过于精确,所能发送的最小单位是1 Wei (1x10^-18 ETH)", + "version": "版本", + "view-cryptokitties": "在CryptoKitties中查看", + "view-cryptostrikers": "在 CryptoStrikers 中查看", + "view-etheremon": "在Etheremon中查看", + "view-gitcoin": "在Gitcoin中查看", + "view-profile": "查看个人资料", + "view-signing": "查看签名短语", + "view-superrare": "在 SuperRare 中查看", + "waiting-for-wifi": "历史记录同步离线, 等待 Wi-Fi。", + "waiting-for-wifi-change": "更改", + "waiting-to-sign": "正在等待签名交易......", + "wallet": "钱包", + "wallet-asset": "资产", + "wallet-assets": "资产", + "wallet-backup-recovery-title": "备份助记词", + "wallet-choose-recipient": "选择接收方", + "wallet-collectibles": "收藏品", + "wallet-insufficient-funds": "资金不足", + "wallet-insufficient-gas": "ETH不足以支付燃料费", + "wallet-invalid-address": "无效的地址: \n {{data}}", + "wallet-invalid-address-checksum": "地址错误: \n {{data}}", + "wallet-invalid-chain-id": "网络不匹配: \n {{data}}而当前链是{{chain}}", + "wallet-manage-assets": "管理资产", + "wallet-request": "请求", + "wallet-send": "发送", + "wallet-send-min-wei": "最小1wei", + "wallet-settings": "钱包设置", + "wallet-total-value": "总值", + "wallet-transaction-total-fee": "总费用", + "wants-to-access-profile": "想要访问您的个人资料", + "warning": "警告", + "warning-message": "抱歉,我们限制快速连续发送多条消息。请稍后再试", + "web-view-error": "糟糕,出错了", + "welcome-to-status": "欢迎来到Status", + "welcome-to-status-description": "点按加号(+)按钮即可开始使用", + "word-n": "字# {{number}}", + "word-n-description": "为了确认您是否正确备份助记词,请在上方输入单词 #{{number}} 。", + "write-down-and-store-securely": "写下来并安全的存储起来", + "wrong-address": "错误的地址", + "wrong-card": "错误的卡片", + "wrong-card-text": "您已点击了与您选择的帐户不对应的卡片。请再试一次。", + "wrong-contract": "错误的合约", + "wrong-password": "密码错误", + "wrong-word": "错误的单词", + "yes": "是", + "You": "你", + "you": "你", + "you-already-have-an-asset": "您已拥有资产{{value}}", + "you-are-all-set": "你已完成所有设置", + "you-are-all-set-description": "现在,如果您丢失了手机,则可以使用助记词恢复您的帐号和钱包。", + "you-dont-have-stickers": "你还没有任何贴纸", + "your-contact-code": "授予此DApp访问权限以查看您的联系人代码", + "your-data-belongs-to-you": "如果你丢失了你的助记词, 你也同时失去了你的数据和资金", + "your-data-belongs-to-you-description": "如果丢失了您的助记词,Status 将无法帮助您恢复您的帐户。您最好的保障是您自行负责数据安全性,并且备份助记词。", + "your-recovery-phrase": "您的助记词", + "your-recovery-phrase-description": "这是你的助记词。已此来证明这是你的钱包。你只能查看一次!请将其写在纸上并保存在安全的地方。如果丢失或重新安装钱包,您将需要用到这些助记词。" +} diff --git a/translations/zh_hant.json b/translations/zh_hant.json new file mode 100644 index 00000000000..1315517c36d --- /dev/null +++ b/translations/zh_hant.json @@ -0,0 +1,195 @@ +{ + "validation-amount-invalid-number": "金額不是一個有效的數字", + "transaction-details": "交易明細", + "confirm": "確認", + "description": "簡介", + "amount": "金額", + "open": "打開", + "close-app-title": "警告!", + "members-active": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "chat-name": "聊天名稱", + "phew-here-is-your-passphrase": "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。", + "public-group-topic": "話題", + "chat-settings": "對話設置", + "offline": "離線", + "invited": "已邀請", + "address": "地址", + "new-public-group-chat": "加入公共聊天", + "datetime-hour": { + "one": "小時", + "other": "小時" + }, + "wallet-settings": "錢包設置", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "close-app-button": "確認", + "block": "塊", + "camera-access-error": "要授予所需的攝像機許可,請轉到系統設置,並確定選中了 “Status” > “攝像機”。", + "wallet-invalid-address": "無效的地址: \n {{data}}", + "remove": "刪除", + "add-members": "添加成員", + "yes": "是", + "dapps": "ÐApps", + "network-settings": "網絡設置", + "photos-access-error": "要授予所需的照片許可,請轉到系統設置,並確保選中了 “Status” > “照片”。", + "hash": "哈希", + "done": "完成", + "close-chat": "刪除聊天", + "new-group-chat": "新的羣聊", + "wallet": "錢包", + "wallet-request": "請求", + "sign-in": "登錄", + "datetime-yesterday": "昨天", + "create-new-account": "創建新賬户", + "datetime-ago": "前", + "contacts": "聯繫人", + "got-it": "知道了", + "public-chats": "公共聊天", + "not-applicable": "對未簽名交易不可用", + "active-online": "在線", + "password": "密碼", + "send-request": "發送請求", + "paste-json": "粘貼 JSON", + "browsing-title": "瀏覽", + "transactions-history-empty": "暫無交易歷史記錄", + "discover": "發現", + "browsing-cancel": "取消", + "intro-status": "跟我聊天,以設置你的帳户並更改你的設置!", + "name": "名稱", + "gas-price": "Gas價格", + "validation-amount-is-too-precise": "金額過於精確,所能發送的最小單位是1 Wei (1x10^-18 ETH)", + "copy-transaction-hash": "複製交易哈希", + "show-qr": "顯示二維碼", + "connect": "連接", + "edit": "編輯", + "account-generation-message": "給我一點時間,我得瘋狂地計算一下,才能生成你的帳户!", + "remove-network": "移除網絡", + "no-messages": "無消息", + "passphrase": "密碼", + "recipient": "接收方", + "members-title": "成員", + "new-group": "新增羣組", + "phone-e164": "國際1", + "connected": "已連接", + "rpc-url": "RPC 鏈接", + "settings": "設置", + "specify-rpc-url": "指定一個 RPC 鏈接", + "gas-limit": "Gas限制", + "delete": "刪除", + "chats": "聊天", + "transaction-sent": "發送的交易", + "transaction": "交易", + "public-group-status": "公共", + "image-source-make-photo": "拍攝", + "chat": "聊天", + "start-conversation": "開始對話", + "save": "保存", + "all": "全部", + "confirmations-helper-text": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "sharing-copy-to-clipboard": "複製到剪貼板", + "sync-in-progress": "正在同步...", + "enter-password": "輸入密碼", + "send-transaction": "發送交易", + "confirmations": "確認", + "incorrect-code": [ + "str", + "抱歉,代碼不正確,請重新輸入" + ], + "image-source-gallery": "從圖庫中選擇", + "sync-synced": "已同步", + "currency": "貨幣", + "status-pending": "等待中", + "connecting-requires-login": "連接其它網絡需要登錄", + "datetime-day": { + "one": "天", + "other": "天" + }, + "request-transaction": "請求交易", + "wallet-send": "發送", + "scan-qr": "掃描二維碼", + "contact-s": { + "one": "聯繫人", + "other": "聯繫人" + }, + "gas-used": "使用的Gas", + "type": "類型", + "next": "下一個", + "recent": "最近", + "open-on-etherscan": "打開 Etherscan.io", + "share": "分享", + "status": "狀態", + "from": "從", + "wrong-password": "密碼錯誤", + "in-contacts": "已添加聯繫人", + "transactions-sign": "簽名", + "sharing-share": "分享...", + "type-a-message": "輸入訊息...", + "usd-currency": "USD", + "url": "鏈接", + "add-network": "添加網絡", + "unknown-status-go-error": "未知的 status-go 錯誤", + "clear-history": "清空歷史記錄", + "no-contacts": "還沒有聯繫人", + "soon": "不久", + "close-app-content": "應用將會停止並關閉。當你重新打開應用,選中的網絡將會被使用", + "datetime-today": "今天", + "web-view-error": "糟糕,出錯了", + "error": "錯誤", + "more": "更多", + "cancel": "取消", + "can-not-add-yourself": "不能添加自己", + "transaction-description": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "add-to-contacts": "添加到聯繫人", + "available": "可用", + "You": "你", + "main-wallet": "主錢包", + "transactions": "交易", + "members": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "intro-message1": "歡迎來到 Status\n點擊該消息來設置你的密碼並開始!", + "eth": "ETH", + "transactions-history": "歷史記錄", + "new-contact": "新的聯繫人", + "datetime-second": { + "one": "秒", + "other": "秒" + }, + "recover": "恢復", + "wallet-total-value": "總值", + "nonce": "隨機數", + "new-network": "新建網絡", + "datetime-minute": { + "one": "分鐘", + "other": "分鐘" + }, + "browsing-open-in-android-web-browser": "在網絡瀏覽器中打開", + "browsing-open-in-ios-web-browser": "在網絡瀏覽器中打開", + "wallet-insufficient-funds": "資金不足", + "edit-profile": "編輯個人資料", + "active-unknown": "未知", + "transaction-failed": "交易失敗", + "public-key": "公鑰", + "profile": "個人資料", + "wallet-choose-recipient": "選擇接收方", + "none": "無", + "removed": "已刪除", + "no": "否", + "transactions-filter-select-all": "全部選中", + "transactions-filter-title": "過濾歷史記錄", + "message": "信息", + "here-is-your-passphrase": "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。", + "wallet-assets": "資產", + "image-source-title": "個人資料圖片", + "current-network": "當前網絡", + "left": "剩餘", + "to": "到", + "data": "數據", + "status-sent": "已發送", + "cost-fee": "成本/費用" +} diff --git a/translations/zh_hant_hk.json b/translations/zh_hant_hk.json new file mode 100644 index 00000000000..1315517c36d --- /dev/null +++ b/translations/zh_hant_hk.json @@ -0,0 +1,195 @@ +{ + "validation-amount-invalid-number": "金額不是一個有效的數字", + "transaction-details": "交易明細", + "confirm": "確認", + "description": "簡介", + "amount": "金額", + "open": "打開", + "close-app-title": "警告!", + "members-active": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "chat-name": "聊天名稱", + "phew-here-is-your-passphrase": "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。", + "public-group-topic": "話題", + "chat-settings": "對話設置", + "offline": "離線", + "invited": "已邀請", + "address": "地址", + "new-public-group-chat": "加入公共聊天", + "datetime-hour": { + "one": "小時", + "other": "小時" + }, + "wallet-settings": "錢包設置", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "close-app-button": "確認", + "block": "塊", + "camera-access-error": "要授予所需的攝像機許可,請轉到系統設置,並確定選中了 “Status” > “攝像機”。", + "wallet-invalid-address": "無效的地址: \n {{data}}", + "remove": "刪除", + "add-members": "添加成員", + "yes": "是", + "dapps": "ÐApps", + "network-settings": "網絡設置", + "photos-access-error": "要授予所需的照片許可,請轉到系統設置,並確保選中了 “Status” > “照片”。", + "hash": "哈希", + "done": "完成", + "close-chat": "刪除聊天", + "new-group-chat": "新的羣聊", + "wallet": "錢包", + "wallet-request": "請求", + "sign-in": "登錄", + "datetime-yesterday": "昨天", + "create-new-account": "創建新賬户", + "datetime-ago": "前", + "contacts": "聯繫人", + "got-it": "知道了", + "public-chats": "公共聊天", + "not-applicable": "對未簽名交易不可用", + "active-online": "在線", + "password": "密碼", + "send-request": "發送請求", + "paste-json": "粘貼 JSON", + "browsing-title": "瀏覽", + "transactions-history-empty": "暫無交易歷史記錄", + "discover": "發現", + "browsing-cancel": "取消", + "intro-status": "跟我聊天,以設置你的帳户並更改你的設置!", + "name": "名稱", + "gas-price": "Gas價格", + "validation-amount-is-too-precise": "金額過於精確,所能發送的最小單位是1 Wei (1x10^-18 ETH)", + "copy-transaction-hash": "複製交易哈希", + "show-qr": "顯示二維碼", + "connect": "連接", + "edit": "編輯", + "account-generation-message": "給我一點時間,我得瘋狂地計算一下,才能生成你的帳户!", + "remove-network": "移除網絡", + "no-messages": "無消息", + "passphrase": "密碼", + "recipient": "接收方", + "members-title": "成員", + "new-group": "新增羣組", + "phone-e164": "國際1", + "connected": "已連接", + "rpc-url": "RPC 鏈接", + "settings": "設置", + "specify-rpc-url": "指定一個 RPC 鏈接", + "gas-limit": "Gas限制", + "delete": "刪除", + "chats": "聊天", + "transaction-sent": "發送的交易", + "transaction": "交易", + "public-group-status": "公共", + "image-source-make-photo": "拍攝", + "chat": "聊天", + "start-conversation": "開始對話", + "save": "保存", + "all": "全部", + "confirmations-helper-text": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "sharing-copy-to-clipboard": "複製到剪貼板", + "sync-in-progress": "正在同步...", + "enter-password": "輸入密碼", + "send-transaction": "發送交易", + "confirmations": "確認", + "incorrect-code": [ + "str", + "抱歉,代碼不正確,請重新輸入" + ], + "image-source-gallery": "從圖庫中選擇", + "sync-synced": "已同步", + "currency": "貨幣", + "status-pending": "等待中", + "connecting-requires-login": "連接其它網絡需要登錄", + "datetime-day": { + "one": "天", + "other": "天" + }, + "request-transaction": "請求交易", + "wallet-send": "發送", + "scan-qr": "掃描二維碼", + "contact-s": { + "one": "聯繫人", + "other": "聯繫人" + }, + "gas-used": "使用的Gas", + "type": "類型", + "next": "下一個", + "recent": "最近", + "open-on-etherscan": "打開 Etherscan.io", + "share": "分享", + "status": "狀態", + "from": "從", + "wrong-password": "密碼錯誤", + "in-contacts": "已添加聯繫人", + "transactions-sign": "簽名", + "sharing-share": "分享...", + "type-a-message": "輸入訊息...", + "usd-currency": "USD", + "url": "鏈接", + "add-network": "添加網絡", + "unknown-status-go-error": "未知的 status-go 錯誤", + "clear-history": "清空歷史記錄", + "no-contacts": "還沒有聯繫人", + "soon": "不久", + "close-app-content": "應用將會停止並關閉。當你重新打開應用,選中的網絡將會被使用", + "datetime-today": "今天", + "web-view-error": "糟糕,出錯了", + "error": "錯誤", + "more": "更多", + "cancel": "取消", + "can-not-add-yourself": "不能添加自己", + "transaction-description": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "add-to-contacts": "添加到聯繫人", + "available": "可用", + "You": "你", + "main-wallet": "主錢包", + "transactions": "交易", + "members": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "intro-message1": "歡迎來到 Status\n點擊該消息來設置你的密碼並開始!", + "eth": "ETH", + "transactions-history": "歷史記錄", + "new-contact": "新的聯繫人", + "datetime-second": { + "one": "秒", + "other": "秒" + }, + "recover": "恢復", + "wallet-total-value": "總值", + "nonce": "隨機數", + "new-network": "新建網絡", + "datetime-minute": { + "one": "分鐘", + "other": "分鐘" + }, + "browsing-open-in-android-web-browser": "在網絡瀏覽器中打開", + "browsing-open-in-ios-web-browser": "在網絡瀏覽器中打開", + "wallet-insufficient-funds": "資金不足", + "edit-profile": "編輯個人資料", + "active-unknown": "未知", + "transaction-failed": "交易失敗", + "public-key": "公鑰", + "profile": "個人資料", + "wallet-choose-recipient": "選擇接收方", + "none": "無", + "removed": "已刪除", + "no": "否", + "transactions-filter-select-all": "全部選中", + "transactions-filter-title": "過濾歷史記錄", + "message": "信息", + "here-is-your-passphrase": "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳户。", + "wallet-assets": "資產", + "image-source-title": "個人資料圖片", + "current-network": "當前網絡", + "left": "剩餘", + "to": "到", + "data": "數據", + "status-sent": "已發送", + "cost-fee": "成本/費用" +} diff --git a/translations/zh_hant_sg.json b/translations/zh_hant_sg.json new file mode 100644 index 00000000000..6a3bdef8ed4 --- /dev/null +++ b/translations/zh_hant_sg.json @@ -0,0 +1,195 @@ +{ + "validation-amount-invalid-number": "金額不是一個有效的數字", + "transaction-details": "交易明細", + "confirm": "確認", + "description": "簡介", + "amount": "金額", + "open": "開啟", + "close-app-title": "警告!", + "members-active": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "chat-name": "聊天名稱", + "phew-here-is-your-passphrase": "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。", + "public-group-topic": "話題", + "chat-settings": "對話設定", + "offline": "離線", + "invited": "已邀請", + "address": "地址", + "new-public-group-chat": "加入公共聊天", + "datetime-hour": { + "one": "小時", + "other": "小時" + }, + "wallet-settings": "錢包設定", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "close-app-button": "確認", + "block": "塊", + "camera-access-error": "要授予所需的攝像機許可,請轉到系統設定,並確定選中了 “Status” > “攝像機”。", + "wallet-invalid-address": "無效的地址: \n {{data}}", + "remove": "刪除", + "add-members": "新增成員", + "yes": "是", + "dapps": "ÐApps", + "network-settings": "網路設定", + "photos-access-error": "要授予所需的照片許可,請轉到系統設定,並確保選中了 “Status” > “照片”。", + "hash": "雜湊", + "done": "完成", + "close-chat": "刪除聊天", + "new-group-chat": "新的群聊", + "wallet": "錢包", + "wallet-request": "請求", + "sign-in": "登入", + "datetime-yesterday": "昨天", + "create-new-account": "建立新賬戶", + "datetime-ago": "前", + "contacts": "聯絡人", + "got-it": "知道了", + "public-chats": "公共聊天", + "not-applicable": "對未簽名交易不可用", + "active-online": "線上", + "password": "密碼", + "send-request": "傳送請求", + "paste-json": "貼上 JSON", + "browsing-title": "瀏覽", + "transactions-history-empty": "暫無交易歷史記錄", + "discover": "發現", + "browsing-cancel": "取消", + "intro-status": "跟我聊天,以設定你的帳戶並更改你的設定!", + "name": "名稱", + "gas-price": "Gas 價格", + "validation-amount-is-too-precise": "金額過於精確,所能傳送的最小單位是1 Wei (1x10^-18 ETH)", + "copy-transaction-hash": "複製交易雜湊", + "show-qr": "顯示二維碼", + "connect": "連線", + "edit": "編輯", + "account-generation-message": "給我一點時間,我得瘋狂地計算一下,才能生成你的帳戶!", + "remove-network": "移除網路", + "no-messages": "無訊息", + "passphrase": "密碼", + "recipient": "接收方", + "members-title": "成員", + "new-group": "新增群組", + "phone-e164": "國際1", + "connected": "已連線", + "rpc-url": "RPC 連結", + "settings": "設定", + "specify-rpc-url": "指定一個 RPC 連結", + "gas-limit": "Gas 限制", + "delete": "刪除", + "chats": "聊天", + "transaction-sent": "傳送的交易", + "transaction": "交易", + "public-group-status": "公共", + "image-source-make-photo": "拍攝", + "chat": "聊天", + "start-conversation": "開始對話", + "save": "儲存", + "all": "全部", + "confirmations-helper-text": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "sharing-copy-to-clipboard": "複製到剪貼簿", + "sync-in-progress": "正在同步...", + "enter-password": "輸入密碼", + "send-transaction": "傳送交易", + "confirmations": "確認", + "incorrect-code": [ + "str", + "抱歉,程式碼不正確,請重新輸入" + ], + "image-source-gallery": "從相簿中選擇", + "sync-synced": "已同步", + "currency": "貨幣", + "status-pending": "等待中", + "connecting-requires-login": "連線其它網路需要登入", + "datetime-day": { + "one": "天", + "other": "天" + }, + "request-transaction": "請求交易", + "wallet-send": "傳送", + "scan-qr": "掃描二維碼", + "contact-s": { + "one": "聯絡人", + "other": "聯絡人" + }, + "gas-used": "使用的 Gas", + "type": "型別", + "next": "下一個", + "recent": "最近", + "open-on-etherscan": "開啟 Etherscan.io", + "share": "分享", + "status": "狀態", + "from": "從", + "wrong-password": "密碼錯誤", + "in-contacts": "已新增聯絡人", + "transactions-sign": "簽名", + "sharing-share": "分享...", + "type-a-message": "輸入訊息...", + "usd-currency": "USD", + "url": "連結", + "add-network": "新增網路", + "unknown-status-go-error": "未知的 status-go 錯誤", + "clear-history": "清空歷史記錄", + "no-contacts": "還沒有聯絡人", + "soon": "不久", + "close-app-content": "應用將會停止並關閉。當你重新開啟應用,選中的網路將會被使用", + "datetime-today": "今天", + "web-view-error": "糟糕,出錯了", + "error": "錯誤", + "more": "更多", + "cancel": "取消", + "can-not-add-yourself": "不能新增自己", + "transaction-description": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "add-to-contacts": "新增到聯絡人", + "available": "可用", + "You": "你", + "main-wallet": "主錢包", + "transactions": "交易", + "members": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "intro-message1": "歡迎來到 Status\n點選該訊息來設定你的密碼並開始!", + "eth": "ETH", + "transactions-history": "歷史記錄", + "new-contact": "新的聯絡人", + "datetime-second": { + "one": "秒", + "other": "秒" + }, + "recover": "恢復", + "wallet-total-value": "總值", + "nonce": "隨機數", + "new-network": "新建網路", + "datetime-minute": { + "one": "分鐘", + "other": "分鐘" + }, + "browsing-open-in-android-web-browser": "在網路瀏覽器中開啟", + "browsing-open-in-ios-web-browser": "在網路瀏覽器中開啟", + "wallet-insufficient-funds": "資金不足", + "edit-profile": "編輯個人資料", + "active-unknown": "未知", + "transaction-failed": "交易失敗", + "public-key": "公鑰", + "profile": "個人資料", + "wallet-choose-recipient": "選擇接收方", + "none": "無", + "removed": "已刪除", + "no": "否", + "transactions-filter-select-all": "全部選中", + "transactions-filter-title": "過濾歷史記錄", + "message": "資訊", + "here-is-your-passphrase": "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。", + "wallet-assets": "資產", + "image-source-title": "個人資料圖片", + "current-network": "當前網路", + "left": "剩餘", + "to": "到", + "data": "資料", + "status-sent": "已傳送", + "cost-fee": "成本/費用" +} diff --git a/translations/zh_hant_tw.json b/translations/zh_hant_tw.json new file mode 100644 index 00000000000..6a3bdef8ed4 --- /dev/null +++ b/translations/zh_hant_tw.json @@ -0,0 +1,195 @@ +{ + "validation-amount-invalid-number": "金額不是一個有效的數字", + "transaction-details": "交易明細", + "confirm": "確認", + "description": "簡介", + "amount": "金額", + "open": "開啟", + "close-app-title": "警告!", + "members-active": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "chat-name": "聊天名稱", + "phew-here-is-your-passphrase": "嗨,真不容易。這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。", + "public-group-topic": "話題", + "chat-settings": "對話設定", + "offline": "離線", + "invited": "已邀請", + "address": "地址", + "new-public-group-chat": "加入公共聊天", + "datetime-hour": { + "one": "小時", + "other": "小時" + }, + "wallet-settings": "錢包設定", + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "close-app-button": "確認", + "block": "塊", + "camera-access-error": "要授予所需的攝像機許可,請轉到系統設定,並確定選中了 “Status” > “攝像機”。", + "wallet-invalid-address": "無效的地址: \n {{data}}", + "remove": "刪除", + "add-members": "新增成員", + "yes": "是", + "dapps": "ÐApps", + "network-settings": "網路設定", + "photos-access-error": "要授予所需的照片許可,請轉到系統設定,並確保選中了 “Status” > “照片”。", + "hash": "雜湊", + "done": "完成", + "close-chat": "刪除聊天", + "new-group-chat": "新的群聊", + "wallet": "錢包", + "wallet-request": "請求", + "sign-in": "登入", + "datetime-yesterday": "昨天", + "create-new-account": "建立新賬戶", + "datetime-ago": "前", + "contacts": "聯絡人", + "got-it": "知道了", + "public-chats": "公共聊天", + "not-applicable": "對未簽名交易不可用", + "active-online": "線上", + "password": "密碼", + "send-request": "傳送請求", + "paste-json": "貼上 JSON", + "browsing-title": "瀏覽", + "transactions-history-empty": "暫無交易歷史記錄", + "discover": "發現", + "browsing-cancel": "取消", + "intro-status": "跟我聊天,以設定你的帳戶並更改你的設定!", + "name": "名稱", + "gas-price": "Gas 價格", + "validation-amount-is-too-precise": "金額過於精確,所能傳送的最小單位是1 Wei (1x10^-18 ETH)", + "copy-transaction-hash": "複製交易雜湊", + "show-qr": "顯示二維碼", + "connect": "連線", + "edit": "編輯", + "account-generation-message": "給我一點時間,我得瘋狂地計算一下,才能生成你的帳戶!", + "remove-network": "移除網路", + "no-messages": "無訊息", + "passphrase": "密碼", + "recipient": "接收方", + "members-title": "成員", + "new-group": "新增群組", + "phone-e164": "國際1", + "connected": "已連線", + "rpc-url": "RPC 連結", + "settings": "設定", + "specify-rpc-url": "指定一個 RPC 連結", + "gas-limit": "Gas 限制", + "delete": "刪除", + "chats": "聊天", + "transaction-sent": "傳送的交易", + "transaction": "交易", + "public-group-status": "公共", + "image-source-make-photo": "拍攝", + "chat": "聊天", + "start-conversation": "開始對話", + "save": "儲存", + "all": "全部", + "confirmations-helper-text": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "sharing-copy-to-clipboard": "複製到剪貼簿", + "sync-in-progress": "正在同步...", + "enter-password": "輸入密碼", + "send-transaction": "傳送交易", + "confirmations": "確認", + "incorrect-code": [ + "str", + "抱歉,程式碼不正確,請重新輸入" + ], + "image-source-gallery": "從相簿中選擇", + "sync-synced": "已同步", + "currency": "貨幣", + "status-pending": "等待中", + "connecting-requires-login": "連線其它網路需要登入", + "datetime-day": { + "one": "天", + "other": "天" + }, + "request-transaction": "請求交易", + "wallet-send": "傳送", + "scan-qr": "掃描二維碼", + "contact-s": { + "one": "聯絡人", + "other": "聯絡人" + }, + "gas-used": "使用的 Gas", + "type": "型別", + "next": "下一個", + "recent": "最近", + "open-on-etherscan": "開啟 Etherscan.io", + "share": "分享", + "status": "狀態", + "from": "從", + "wrong-password": "密碼錯誤", + "in-contacts": "已新增聯絡人", + "transactions-sign": "簽名", + "sharing-share": "分享...", + "type-a-message": "輸入訊息...", + "usd-currency": "USD", + "url": "連結", + "add-network": "新增網路", + "unknown-status-go-error": "未知的 status-go 錯誤", + "clear-history": "清空歷史記錄", + "no-contacts": "還沒有聯絡人", + "soon": "不久", + "close-app-content": "應用將會停止並關閉。當你重新開啟應用,選中的網路將會被使用", + "datetime-today": "今天", + "web-view-error": "糟糕,出錯了", + "error": "錯誤", + "more": "更多", + "cancel": "取消", + "can-not-add-yourself": "不能新增自己", + "transaction-description": "請等待達到至少12個確認,來確保交易已被安全地處理。", + "add-to-contacts": "新增到聯絡人", + "available": "可用", + "You": "你", + "main-wallet": "主錢包", + "transactions": "交易", + "members": { + "one": "1 個成員", + "other": "{{count}} 個成員", + "zero": "無成員" + }, + "intro-message1": "歡迎來到 Status\n點選該訊息來設定你的密碼並開始!", + "eth": "ETH", + "transactions-history": "歷史記錄", + "new-contact": "新的聯絡人", + "datetime-second": { + "one": "秒", + "other": "秒" + }, + "recover": "恢復", + "wallet-total-value": "總值", + "nonce": "隨機數", + "new-network": "新建網路", + "datetime-minute": { + "one": "分鐘", + "other": "分鐘" + }, + "browsing-open-in-android-web-browser": "在網路瀏覽器中開啟", + "browsing-open-in-ios-web-browser": "在網路瀏覽器中開啟", + "wallet-insufficient-funds": "資金不足", + "edit-profile": "編輯個人資料", + "active-unknown": "未知", + "transaction-failed": "交易失敗", + "public-key": "公鑰", + "profile": "個人資料", + "wallet-choose-recipient": "選擇接收方", + "none": "無", + "removed": "已刪除", + "no": "否", + "transactions-filter-select-all": "全部選中", + "transactions-filter-title": "過濾歷史記錄", + "message": "資訊", + "here-is-your-passphrase": "這是你的密碼,*寫下來並好好保管它!* 你需要用它來恢復你的帳戶。", + "wallet-assets": "資產", + "image-source-title": "個人資料圖片", + "current-network": "當前網路", + "left": "剩餘", + "to": "到", + "data": "資料", + "status-sent": "已傳送", + "cost-fee": "成本/費用" +} diff --git a/translations/zh_wuu.json b/translations/zh_wuu.json new file mode 100644 index 00000000000..19b1a17f3cb --- /dev/null +++ b/translations/zh_wuu.json @@ -0,0 +1,131 @@ +{ + "confirm": "确认", + "amount": "金额", + "members-active": { + "one": "1个会员,1个活跃成员", + "other": "{{count}} 会员, {{count}} 活跃成员", + "zero": "无会员" + }, + "chat-name": "聊天名称", + "phew-here-is-your-passphrase": "*唷* 是很难的,这是您的口令短语,*写下来并保证安全!* 您将需要它来恢复您的帐户。", + "public-group-topic": "主题", + "chat-settings": "聊天设置", + "offline": "离线", + "invited": "已邀请", + "address": "地址", + "new-public-group-chat": "加入公开聊天", + "datetime-hour": { + "one": "小时", + "other": "小时" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "要授予所需的摄像机许可,请转到系统设置,并确定选择了“状态”>“摄像机”。", + "remove": "移动", + "add-members": "添加会员", + "photos-access-error": "要授予所需的照片许可,请转到系统设置,并确保选择“状态“>“照片“。", + "done": "完成", + "close-chat": "刪除聊天", + "new-group-chat": "新的群聊", + "sign-in": "登录", + "datetime-yesterday": "昨天", + "create-new-account": "建立新帐户", + "datetime-ago": "之前", + "contacts": "联系人", + "got-it": "得到了", + "active-online": "在线", + "password": "密码", + "browsing-title": "浏览", + "discover": "发现", + "browsing-cancel": "取消", + "intro-status": "与我聊天设置您的帐户,并更改您的设置!", + "name": "名称", + "show-qr": "显示QR", + "connect": "连接", + "edit": "编辑", + "account-generation-message": "给我一秒,我要做一些疯狂的计算生成您的帐号!", + "no-messages": "无信息", + "passphrase": "口令短语", + "recipient": "收件人", + "members-title": "会员", + "new-group": "新增群組", + "phone-e164": "国际1", + "settings": "设置", + "delete": "刪除", + "chats": "聊天", + "transaction": "交易", + "public-group-status": "公共", + "image-source-make-photo": "截图", + "start-conversation": "开始对话", + "save": "保存", + "sharing-copy-to-clipboard": "复制到剪贴板", + "sync-in-progress": "同步中...", + "send-transaction": "发送交易", + "incorrect-code": [ + "str", + "对不起,代码不正确,请再输入一次" + ], + "image-source-gallery": "从相册中选择", + "sync-synced": "同步", + "status-pending": "待定", + "datetime-day": { + "one": "日", + "other": "日" + }, + "scan-qr": "扫描QR", + "contact-s": { + "one": "contact", + "other": "contacts" + }, + "next": "下一个", + "recent": "最近", + "status": "状态", + "from": "从", + "wrong-password": "错误的密码", + "in-contacts": "在联系人", + "sharing-share": "分享...", + "type-a-message": "输入讯息...", + "clear-history": "清除历史", + "no-contacts": "暂无联系人", + "datetime-today": "今天", + "web-view-error": "啊哦,错误", + "error": "错误", + "more": "更多", + "cancel": "取消", + "can-not-add-yourself": "您不能添加您自己", + "add-to-contacts": "添加到联系人", + "available": "可用", + "You": "您", + "main-wallet": "主要钱包", + "members": { + "one": "1人", + "other": "{{count}}会员", + "zero": "无会员" + }, + "intro-message1": "欢迎来到\n点击这一消息设置您的密码并开始!", + "new-contact": "新的联系人", + "datetime-second": { + "one": "秒", + "other": "秒" + }, + "recover": "恢复", + "datetime-minute": { + "one": "分钟", + "other": "分钟" + }, + "browsing-open-in-android-web-browser": "在网络浏览器中打开", + "browsing-open-in-ios-web-browser": "在网络浏览器中打开", + "edit-profile": "编辑个人资料", + "active-unknown": "未知", + "public-key": "公共密钥", + "profile": "个人资料", + "none": "无", + "removed": "已移除", + "message": "消息", + "here-is-your-passphrase": "这是您的口令短语,*写下来并保证安全!* 您将需要它来恢复您的帐户。", + "image-source-title": "个人资料照片", + "current-network": "当前网络", + "left": "离开", + "to": "至", + "status-sent": "已发送", + "data": "数据" +} diff --git a/translations/zh_yue.json b/translations/zh_yue.json new file mode 100644 index 00000000000..eb8b81ad78d --- /dev/null +++ b/translations/zh_yue.json @@ -0,0 +1,131 @@ +{ + "confirm": "確認", + "amount": "金額", + "members-active": { + "one": "1位成員, 1位活躍", + "other": "{{count}}位成員, {{count}}位活躍", + "zero": "暫無成員" + }, + "chat-name": "用戶名稱", + "phew-here-is-your-passphrase": "*啊* 那可真不簡單。 這是你的臨時登入碼。 *請記錄並保存於安全地方!* 你將會需要它,以恢復你的帳戶。", + "public-group-topic": "主題", + "chat-settings": "聊天設置", + "offline": "離線", + "invited": "邀請", + "address": "地址", + "new-public-group-chat": "加入公開聊天", + "datetime-hour": { + "one": "小時", + "other": "小時" + }, + "datetime-ago-format": "{{number}} {{time-intervals}} {{ago}}", + "camera-access-error": "要授予所需的攝像機許可,請轉到系統設置,並確定選擇了“狀態”>“攝像機”。", + "remove": "刪除", + "add-members": "增添成員", + "photos-access-error": "要授予所需的照片許可,請轉到系統設置,並確保選擇“狀態“>“照片“。", + "done": "完成", + "close-chat": "刪除對話", + "new-group-chat": "新增群聊", + "sign-in": "登入", + "datetime-yesterday": "昨天", + "create-new-account": "建立新帳號", + "datetime-ago": "之前", + "contacts": "聯絡人", + "got-it": "得到了", + "active-online": "在線", + "password": "密碼", + "browsing-title": "瀏覽", + "discover": "新發現", + "browsing-cancel": "取消", + "intro-status": "如需設置新帳戶或更改現有設置,請與我聊天!", + "name": "名稱", + "show-qr": "暫無聯絡人", + "connect": "連接", + "edit": "編輯", + "account-generation-message": "請給我一秒鐘。我正在瘋狂地運算,以啟動你的帳戶!", + "no-messages": "沒有新訊息", + "passphrase": "臨時登入碼", + "recipient": "收件人", + "members-title": "會員", + "new-group": "新增群組", + "phone-e164": "國際1", + "settings": "設置", + "delete": "刪除", + "chats": "聊天史", + "transaction": "交易", + "public-group-status": "公共", + "image-source-make-photo": "捕獲照片", + "start-conversation": "開始對話", + "save": "儲存", + "sharing-copy-to-clipboard": "複製到剪貼板", + "sync-in-progress": "正在同步...", + "send-transaction": "發送交易", + "incorrect-code": [ + "str", + "對不起,代碼不正確,請重新輸入" + ], + "image-source-gallery": "從圖庫中選取", + "sync-synced": "已同步", + "status-pending": "待定", + "datetime-day": { + "one": "天", + "other": "天" + }, + "scan-qr": "掃描 QR", + "contact-s": { + "one": "contact", + "other": "contacts" + }, + "next": "下一個", + "recent": "最近發現", + "status": "狀態", + "from": "從", + "wrong-password": "密碼錯誤", + "in-contacts": "在聯絡資訊", + "sharing-share": "分享...", + "type-a-message": "輸入訊息...", + "clear-history": "清除歷史記錄", + "no-contacts": "No contacts yet", + "datetime-today": "今天", + "web-view-error": "抱歉,錯誤", + "error": "錯誤", + "more": "更多", + "cancel": "取消", + "can-not-add-yourself": "你不能添加自己", + "add-to-contacts": "添加到通訊錄", + "available": "有空", + "You": "你", + "main-wallet": "主錢包", + "members": { + "one": "1位成員", + "other": "{{count}} 位成員", + "zero": "暫無成員" + }, + "intro-message1": "歡迎來到Status\n請點擊此短訊,以設置你的密碼及開始!", + "new-contact": "新增聯絡", + "datetime-second": { + "one": "秒", + "other": "秒" + }, + "recover": "還原", + "datetime-minute": { + "one": "分鐘", + "other": "分鐘" + }, + "browsing-open-in-android-web-browser": "在網絡瀏覽器中打開", + "browsing-open-in-ios-web-browser": "在網絡瀏覽器中打開", + "edit-profile": "編輯個人資料", + "active-unknown": "未知", + "public-key": "公共鑰匙", + "profile": "用戶簡介", + "none": "不存在", + "removed": "刪除", + "message": "短訊", + "here-is-your-passphrase": "這是你的臨時登入碼。 *請記錄並保存於安全地方!* 你將會需要它,以恢復你的帳戶。", + "image-source-title": "封面照片", + "current-network": "當前網絡", + "left": "離開", + "to": "至", + "status-sent": "發送成功", + "data": "數據" +} diff --git a/ubuntu-server.js b/ubuntu-server.js deleted file mode 100755 index d4b18aa8caf..00000000000 --- a/ubuntu-server.js +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env node - -/** - * Copyright (C) 2016, Canonical Ltd. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - */ - - console.debug = console.log; - - -var net = require('net'); -var repl = require('repl'); -var vm = require('vm'); -var util = require('util'); -var Buffer = require('buffer').Buffer; -var realmConstructor = require("./node_modules/realm/lib/index.js"); -console.log("Loaded realmConstructor: " + realmConstructor); - -var DEBUG = 1; - -function rnUbuntuServer(readable, writable) { - console.reportErrorsAsExceptions = false; // XXX: - var sandbox = { console: console, util: util, outerRealmConstructor: realmConstructor }; - vm.createContext(sandbox); - - var state = 'start'; - var length = 0; - var buffer = new Buffer(0); - - var internalEval = function(code) { - DEBUG > 3 && console.error("-- internalEval: executing script(length=" + code.length + "): " + code.slice(0, 80) + " ... " + code.slice(-80)); - DEBUG > 3 && console.error("-- before sandbox=" + util.inspect(sandbox, { colors: true, depth: null })); - var result = vm.runInContext(code, sandbox); - DEBUG > 3 && console.error("-- internalEval: result = " + result); - DEBUG > 3 && console.error("-- after sandbox=" + util.inspect(sandbox, { colors: true, depth: null })); - return result; - }; - - var sendResponse = function(result) { - function sendResponsePacket(response) { - const sizeBuf = new Buffer(4); - const dataBuf = new Buffer(response); - sizeBuf.writeUInt32LE(dataBuf.length, 0); - writable.write(sizeBuf); - writable.write(dataBuf); - } - - var stringifiedResult = JSON.stringify(result); - DEBUG > 3 && console.error("-- sending result=" + stringifiedResult); - if (stringifiedResult === undefined) { - sendResponsePacket('undefined'); - return; - } - sendResponsePacket(stringifiedResult); - } - - readable.on('error', function (exc) { - console.warn("ignoring exception: " + exc); - }); - - readable.on('data', function(chunk) { - DEBUG > 2 && console.error("-- Data received from RN Client: state = " + state) - DEBUG > 2 && console.error("-- chunk length: " + chunk.length) - DEBUG > 2 && console.error("-- buffer length(original): " + buffer.length) - - if (chunk == null || state === 'eof') - return; - - buffer = Buffer.concat([buffer, chunk]); - DEBUG > 2 && console.error("-- buffer length(concat): " + buffer.length) - - while(true) { - if (state === 'start') { - if (buffer.length < 4) - return; - length = buffer.readUInt32LE(0); - DEBUG > 2 && console.error("-- New Packet: length=" + length); - - if (buffer.length >= length + 4) { - var result = internalEval(buffer.toString('utf8', 4, length + 4)); - var tmpBuffer = new Buffer(buffer.length - 4 - length); - buffer.copy(tmpBuffer, 0, length + 4, buffer.length); - buffer = tmpBuffer; - sendResponse(result); - } else { - state = 'script'; - } - } - - if (state === 'script') { - DEBUG > 2 && console.error("-- Packet length: " + length); - if (buffer.length >= length + 4) { - var result = internalEval(buffer.toString('utf8', 4, length + 4)); - var tmpBuffer = new Buffer(buffer.length - 4 - length); - buffer.copy(tmpBuffer, 0, length + 4, buffer.length); - buffer = tmpBuffer; - state = 'start'; - sendResponse(result); - } else { - return; - } - } - } - }); - - readable.on('end', function() { - state = 'eof'; - DEBUG && console.error("-- Session ended"); - }); -} - -if (process.argv.indexOf('--pipe') != -1) { - console.log = console.error - rnUbuntuServer(process.stdin, process.stdout); -} else { - var server = net.createServer((sock) => { - DEBUG && console.error("-- Connection from RN client"); - rnUbuntuServer(sock, sock); - }).listen(5000, function() { console.error("-- Server starting") }); -} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000000..714506024fa --- /dev/null +++ b/yarn.lock @@ -0,0 +1,11860 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.0.tgz#c241dc454e5b5917e40d37e525e2f4530c399298" + integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g== + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" + integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.12.tgz#b4eb2d7ebc3449b062381644c93050db545b70ee" + integrity sha512-44ODe6O1IVz9s2oJE3rZ4trNNKTX9O7KpQpfAP4t8QII/zwrVRHL7i2pxhqtcY7tqMLrrKfMlBKnm1QlrRFs5w== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.12" + "@babel/helper-compilation-targets" "^7.17.10" + "@babel/helper-module-transforms" "^7.17.12" + "@babel/helpers" "^7.17.9" + "@babel/parser" "^7.17.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.12" + "@babel/types" "^7.17.12" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/core@^7.20.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.0.tgz#f8259ae0e52a123eb40f552551e647b506a94d83" + integrity sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.0" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.0" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.17.12", "@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/generator@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== + dependencies: + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + +"@babel/generator@^7.20.0", "@babel/generator@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.15.tgz#1564189c7ec94cb8f77b5e8a90c4d200d21b2339" + integrity sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA== + dependencies: + "@babel/types" "^7.22.15" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/generator@^7.21.3": + version "7.21.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.3.tgz#232359d0874b392df04045d72ce2fd9bb5045fce" + integrity sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA== + dependencies: + "@babel/types" "^7.21.3" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/generator@^7.4.0", "@babel/generator@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" + integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ== + dependencies: + "@babel/types" "^7.9.5" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/generator@^7.7.2": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" + integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-annotate-as-pure@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" + integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-annotate-as-pure@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" + integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-builder-react-jsx@^7.20.0": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.22.10.tgz#591b1b44a80eb16123b46ad55e9ba42335d6f103" + integrity sha512-cZr0nzCwrMp7Z8owt+YN8OncOqhG1eZLp/aRT5ftBnkLJTCB3Dnq/t52vSfWc6sGRWdDeawbksuh3pYQGCzVwA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/types" "^7.22.10" + +"@babel/helper-compilation-targets@^7.17.10", "@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.15" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" + integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.0.tgz#fac430912606331cb075ea8d82f9a4c145a4da19" + integrity sha512-Kh8zTGR9de3J63e5nS0rQUdRs/kbtwoeQQ0sriS0lItjC96u8XXZN6lKpuyWd2coKSU13py/y+LTmThLuVX0Pg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-member-expression-to-functions" "^7.17.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz#64f49ecb0020532f19b1d014b03bccaa1ab85fb9" + integrity sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-member-expression-to-functions" "^7.21.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/helper-split-export-declaration" "^7.18.6" + +"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" + integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.6.tgz#b04d915ce92ce363666f816a884cdcfc9be04953" + integrity sha512-cBXU1vZni/CpGF29iTu4YRbOZt3Wat6zCoMDxRF1MayiEc4URxOj31tT65HUM0CRpMowA3HCJaAOVOUnMf96cw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-member-expression-to-functions" "^7.23.0" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.20" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.20.5": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz#40411a8ab134258ad2cf3a3d987ec6aa0723cee5" + integrity sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" + integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.8.3": + version "7.8.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" + integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-regex" "^7.8.3" + regexpu-core "^4.7.0" + +"@babel/helper-define-polyfill-provider@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" + integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== + dependencies: + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-define-polyfill-provider@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" + integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" + integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== + dependencies: + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-function-name@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" + integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.9.5" + +"@babel/helper-get-function-arity@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" + integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== + dependencies: + "@babel/types" "^7.17.0" + +"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" + integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== + dependencies: + "@babel/types" "^7.21.0" + +"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz#b95a144896f6d491ca7863576f820f3628818621" + integrity sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-member-expression-to-functions@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-transforms@^7.17.12", "@babel/helper-module-transforms@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" + integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-module-transforms@^7.21.2": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" + integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.2" + "@babel/types" "^7.21.2" + +"@babel/helper-module-transforms@^7.22.15", "@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": + version "7.22.17" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.17.tgz#7edf129097a51ccc12443adbc6320e90eab76693" + integrity sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.15" + +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-optimise-call-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" + integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-optimise-call-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" + integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" + integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== + +"@babel/helper-plugin-utils@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + +"@babel/helper-plugin-utils@^7.12.13": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" + integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== + +"@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" + integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== + +"@babel/helper-plugin-utils@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" + integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA== + +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-regex@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" + integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== + dependencies: + lodash "^4.17.13" + +"@babel/helper-remap-async-to-generator@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" + integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-wrap-function" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": + version "7.22.17" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.17.tgz#dabaa50622b3b4670bd6546fc8db23eb12d89da0" + integrity sha512-bxH77R5gjH3Nkde6/LuncQoLaP16THYPscurp1S8z7S9ZgezCyV3G8Hc+TZiCmY8pz4fp8CvKSgtJMW0FkLAxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-wrap-function" "^7.22.17" + +"@babel/helper-replace-supers@^7.16.7": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.2.tgz#41fdfcc9abaf900e18ba6e5931816d9062a7b2e0" + integrity sha512-XzAIyxx+vFnrOxiQrToSUOzUOn0e1J2Li40ntddek1Y69AXUTXoDJ40/D5RdjFu7s7qHiaeoTiempZcbuVXh2Q== + dependencies: + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-member-expression-to-functions" "^7.17.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + +"@babel/helper-replace-supers@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" + integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.20.7" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/helper-replace-supers@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" + integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" + integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" + integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== + dependencies: + "@babel/types" "^7.20.2" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" + integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== + dependencies: + "@babel/types" "^7.20.0" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" + integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-identifier@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz#601fa28e4cc06786c18912dca138cec73b882044" + integrity sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-identifier@^7.9.0": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" + integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helper-validator-option@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" + integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== + +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== + +"@babel/helper-wrap-function@^7.18.9": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" + integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== + dependencies: + "@babel/helper-function-name" "^7.19.0" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" + +"@babel/helper-wrap-function@^7.22.17": + version "7.22.17" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.17.tgz#222ac3ff9cc8f9b617cc1e5db75c0b538e722801" + integrity sha512-nAhoheCMlrqU41tAojw9GpVEKDlTS8r3lzFmF0lP52LwblCPbuFSO7nGIZoIcoU5NIm1ABrna0cJExE4Ay6l2Q== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.17" + +"@babel/helpers@^7.17.9", "@babel/helpers@^7.23.0": + version "7.23.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.1.tgz#44e981e8ce2b9e99f8f0b703f3326a4636c16d15" + integrity sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA== + dependencies: + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.0" + "@babel/types" "^7.23.0" + +"@babel/highlight@^7.16.7": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" + integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/highlight@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" + integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/highlight@^7.8.3": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" + integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== + dependencies: + "@babel/helper-validator-identifier" "^7.9.0" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": + version "7.9.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" + integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== + +"@babel/parser@^7.13.16": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" + integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== + +"@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.22.15", "@babel/parser@^7.22.16": + version "7.22.16" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.16.tgz#180aead7f247305cce6551bea2720934e2fa2c95" + integrity sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA== + +"@babel/parser@^7.16.7", "@babel/parser@^7.18.0": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" + integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== + +"@babel/parser@^7.17.12", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + +"@babel/parser@^7.18.10": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" + integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== + +"@babel/parser@^7.20.7", "@babel/parser@^7.21.3": + version "7.21.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.3.tgz#1d285d67a19162ff9daa358d4cb41d50c06220b3" + integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" + integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f" + integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.15" + +"@babel/plugin-proposal-async-generator-functions@^7.0.0": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" + integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.13.0", "@babel/plugin-proposal-class-properties@^7.18.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-export-default-from@^7.0.0": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.10.tgz#091f4794dbce4027c03cf4ebc64d3fb96b75c206" + integrity sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-default-from" "^7.18.6" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator@^7.18.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.0.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.20.0": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.7" + +"@babel/plugin-proposal-optional-catch-binding@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" + integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + +"@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.20.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" + integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7" + integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-default-from@^7.0.0", "@babel/plugin-syntax-export-default-from@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.18.6.tgz#8df076711a4818c4ce4f23e61d622b0ba2ff84bc" + integrity sha512-Kr//z3ujSVNx6E9z9ih5xXXMqK07VVTuqPmqGe6Mss/zW5XPeLZeSDZoP9ab/hT4wPKqAgjl2PnhPrcpk8Seew== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-flow@^7.12.1", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz#163b820b9e7696ce134df3ee716d9c0c98035859" + integrity sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-flow@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1" + integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-import-assertions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" + integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-jsx@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2" + integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.7.2": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" + integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897" + integrity sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.0.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" + integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.0.0", "@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.12.tgz#b54fc3be6de734a56b87508f99d6428b5b605a7b" + integrity sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw== + dependencies: + "@babel/helper-plugin-utils" "^7.17.12" + +"@babel/plugin-syntax-typescript@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" + integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-syntax-typescript@^7.23.3", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz#24f460c85dbbc983cd2b9c4994178bcc01df958f" + integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" + integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-arrow-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz#3b153af4a6b779f340d5b80d3f634f55820aefa3" + integrity sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.20.0", "@babel/plugin-transform-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" + integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-block-scoping@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" + integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + lodash "^4.17.13" + +"@babel/plugin-transform-block-scoping@^7.20.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz#8744d02c6c264d82e1a4bc5d2d501fd8aff6f022" + integrity sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-block-scoping@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz#494eb82b87b5f8b1d8f6f28ea74078ec0a10a841" + integrity sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" + integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.0.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665" + integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + +"@babel/plugin-transform-classes@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b" + integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" + integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-computed-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + +"@babel/plugin-transform-destructuring@^7.20.0", "@babel/plugin-transform-destructuring@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz#e7404ea5bb3387073b9754be654eecb578324694" + integrity sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dotall-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" + integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-duplicate-keys@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" + integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" + integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" + integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" + integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-flow-strip-types@^7.20.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.22.5.tgz#0bb17110c7bf5b35a60754b2f00c58302381dee2" + integrity sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-flow" "^7.22.5" + +"@babel/plugin-transform-flow-strip-types@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz#6aeca0adcb81dc627c8986e770bfaa4d9812aff5" + integrity sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-flow" "^7.18.6" + +"@babel/plugin-transform-for-of@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29" + integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.0.0": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" + integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== + dependencies: + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" + integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1" + integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" + integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-amd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" + integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.21.2": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7" + integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== + dependencies: + "@babel/helper-module-transforms" "^7.21.2" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-simple-access" "^7.20.2" + +"@babel/plugin-transform-modules-commonjs@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz#b11810117ed4ee7691b29bd29fd9f3f98276034f" + integrity sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg== + dependencies: + "@babel/helper-module-transforms" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-commonjs@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4" + integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1" + integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + +"@babel/plugin-transform-modules-umd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" + integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.0.0": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" + integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.20.5" + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" + integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" + integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" + integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-assign@^7.16.7": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.23.3.tgz#64177e8cf943460c7f0e1c410277546804f59625" + integrity sha512-TPJ6O7gVC2rlQH2hvQGRH273G1xdoloCj9Pc07Q7JbIZYDi+Sv5gaE2fu+r5E7qK4zyt6vj0FbZaZTRU5C3OMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-object-rest-spread@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f" + integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.15" + +"@babel/plugin-transform-object-super@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" + integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz#d7a5996c2f7ca4ad2ad16dbb74444e5c4385b1ba" + integrity sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.0.0": + version "7.21.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz#18fc4e797cf6d6d972cb8c411dbe8a809fa157db" + integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114" + integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" + integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" + integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-display-name@^7.0.0", "@babel/plugin-transform-react-display-name@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415" + integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-jsx-development@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5" + integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.18.6" + +"@babel/plugin-transform-react-jsx-self@^7.0.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.21.0.tgz#ec98d4a9baafc5a1eb398da4cf94afbb40254a54" + integrity sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-react-jsx-source@^7.0.0": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz#88578ae8331e5887e8ce28e4c9dc83fb29da0b86" + integrity sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-transform-react-jsx@^7.0.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz#656b42c2fdea0a6d8762075d58ef9d4e3c4ab8a2" + integrity sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.21.0" + +"@babel/plugin-transform-react-jsx@^7.18.6": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz#b3cbb7c3a00b92ec8ae1027910e331ba5c500eb9" + integrity sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.19.0" + +"@babel/plugin-transform-react-pure-annotations@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz#561af267f19f3e5d59291f9950fd7b9663d0d844" + integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-regenerator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" + integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" + integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-runtime@^7.0.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.0.tgz#2a884f29556d0a68cd3d152dcc9e6c71dfb6eee8" + integrity sha512-ReY6pxwSzEU0b3r2/T/VhqMKg/AkceBT19X0UptA3/tYi5Pe2eXgEUH+NNMC5nok6c6XQz5tyVTUpuezRfSMSg== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + semver "^6.3.0" + +"@babel/plugin-transform-shorthand-properties@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" + integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-shorthand-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-spread@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" + integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100" + integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-regex" "^7.8.3" + +"@babel/plugin-transform-sticky-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" + integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-template-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typeof-symbol@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" + integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typescript@^7.17.12": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.4.tgz#587eaf6a39edb8c06215e550dc939faeadd750bf" + integrity sha512-l4vHuSLUajptpHNEOUDEGsnpl9pfRLsN1XUoDQDD/YBuXTM+v37SHGS+c6n4jdcZy96QtuUuSvZYMLSSsjH8Mw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.0" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/plugin-syntax-typescript" "^7.17.12" + +"@babel/plugin-transform-typescript@^7.21.3", "@babel/plugin-transform-typescript@^7.5.0": + version "7.21.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz#316c5be579856ea890a57ebc5116c5d064658f2b" + integrity sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.21.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-typescript" "^7.20.0" + +"@babel/plugin-transform-typescript@^7.23.3": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz#aa36a94e5da8d94339ae3a4e22d40ed287feb34c" + integrity sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.23.6" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-typescript" "^7.23.3" + +"@babel/plugin-transform-unicode-escapes@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" + integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" + integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-regex@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" + integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-unicode-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" + integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" + integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/preset-env@^7.20.0": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.15.tgz#142716f8e00bc030dae5b2ac6a46fbd8b3e18ff8" + integrity sha512-tZFHr54GBkHk6hQuVA8w4Fmq+MSPsfvMG0vPnOYyTnJpyfMqybL8/MbNCPRT9zc2KBO2pe4tq15g6Uno4Jpoag== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.22.15" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.22.15" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.11" + "@babel/plugin-transform-classes" "^7.22.15" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.22.15" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.11" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.11" + "@babel/plugin-transform-for-of" "^7.22.15" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.11" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.15" + "@babel/plugin-transform-modules-systemjs" "^7.22.11" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" + "@babel/plugin-transform-numeric-separator" "^7.22.11" + "@babel/plugin-transform-object-rest-spread" "^7.22.15" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.11" + "@babel/plugin-transform-optional-chaining" "^7.22.15" + "@babel/plugin-transform-parameters" "^7.22.15" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.11" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.10" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.10" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "0.1.6-no-external-plugins" + "@babel/types" "^7.22.15" + babel-plugin-polyfill-corejs2 "^0.4.5" + babel-plugin-polyfill-corejs3 "^0.8.3" + babel-plugin-polyfill-regenerator "^0.5.2" + core-js-compat "^3.31.0" + semver "^6.3.1" + +"@babel/preset-flow@^7.13.13": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.21.4.tgz#a5de2a1cafa61f0e0b3af9b30ff0295d38d3608f" + integrity sha512-F24cSq4DIBmhq4OzK3dE63NHagb27OPE3eWR+HLekt4Z3Y5MzIIUGF3LlLgV0gN8vzbDViSY7HnrReNVCJXTeA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.21.0" + "@babel/plugin-transform-flow-strip-types" "^7.21.0" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d" + integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-transform-react-display-name" "^7.18.6" + "@babel/plugin-transform-react-jsx" "^7.18.6" + "@babel/plugin-transform-react-jsx-development" "^7.18.6" + "@babel/plugin-transform-react-pure-annotations" "^7.18.6" + +"@babel/preset-typescript@^7.13.0": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.4.tgz#b913ac8e6aa8932e47c21b01b4368d8aa239a529" + integrity sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.21.0" + "@babel/plugin-syntax-jsx" "^7.21.4" + "@babel/plugin-transform-modules-commonjs" "^7.21.2" + "@babel/plugin-transform-typescript" "^7.21.3" + +"@babel/preset-typescript@^7.16.7": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz#14534b34ed5b6d435aa05f1ae1c5e7adcc01d913" + integrity sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-syntax-jsx" "^7.23.3" + "@babel/plugin-transform-modules-commonjs" "^7.23.3" + "@babel/plugin-transform-typescript" "^7.23.3" + +"@babel/preset-typescript@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.17.12.tgz#40269e0a0084d56fc5731b6c40febe1c9a4a3e8c" + integrity sha512-S1ViF8W2QwAKUGJXxP9NAfNaqGDdEBJKpYkxHf5Yy2C4NPPzXGeR3Lhk7G8xJaaLcFTRfNjVbtbVtm8Gb0mqvg== + dependencies: + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-typescript" "^7.17.12" + +"@babel/register@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.0.0.tgz#fa634bae1bfa429f60615b754fc1f1d745edd827" + integrity sha512-f/+CRmaCe7rVEvcvPvxeA8j5aJhHC3aJie7YuqcMDhUOuyWLA7J/aNrTaHIzoWPEhpHA54mec4Mm8fv8KBlv3g== + dependencies: + core-js "^2.5.7" + find-cache-dir "^1.0.0" + home-or-tmp "^3.0.0" + lodash "^4.17.10" + mkdirp "^0.5.1" + pirates "^4.0.0" + source-map-support "^0.5.9" + +"@babel/register@^7.13.16": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.21.0.tgz#c97bf56c2472e063774f31d344c592ebdcefa132" + integrity sha512-9nKsPmYDi5DidAqJaQooxIhsLJiNMkGr8ypQ8Uic7cIox7UCDsM7HuUGxdGT7mSDTYbqzIdsOWzfBton/YJrMw== + dependencies: + clone-deep "^4.0.1" + find-cache-dir "^2.0.0" + make-dir "^2.1.0" + pirates "^4.0.5" + source-map-support "^0.5.16" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.0.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/runtime@^7.3.1": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.0.tgz#824a9ef325ffde6f78056059db3168c08785e24a" + integrity sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q== + dependencies: + regenerator-runtime "^0.13.10" + +"@babel/runtime@^7.8.4": + version "7.9.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" + integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/template@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/template@^7.22.15", "@babel/template@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/template@^7.3.3": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + +"@babel/template@^7.4.0", "@babel/template@^7.8.3": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" + integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + +"@babel/traverse@^7.17.12", "@babel/traverse@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.0.tgz#18196ddfbcf4ccea324b7f6d3ada00d8c5a99c53" + integrity sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.2.tgz#b77a52604b5cc836a9e1e08dca01cba67a12d2e8" + integrity sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.0" + "@babel/types" "^7.18.2" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.20.0": + version "7.22.17" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.17.tgz#b23c203ab3707e3be816043081b4a994fcacec44" + integrity sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.22.15" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.16" + "@babel/types" "^7.22.17" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.2": + version "7.21.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.3.tgz#4747c5e7903d224be71f90788b06798331896f67" + integrity sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.21.3" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.21.3" + "@babel/types" "^7.21.3" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.4.3": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" + integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.9.5" + "@babel/helper-function-name" "^7.9.5" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.9.0" + "@babel/types" "^7.9.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.17.12", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.5": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.12.tgz#1210690a516489c0200f355d87619157fbbd69a0" + integrity sha512-rH8i29wcZ6x9xjzI5ILHL/yZkbQnCERdHlogKuIb4PUr7do4iT8DPekrTbBLWTnRQm6U0GYABbTMSzijmEqlAg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@babel/types@^7.18.10", "@babel/types@^7.18.2", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.3", "@babel/types@^7.22.10", "@babel/types@^7.22.15", "@babel/types@^7.22.17", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@babel/types@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" + integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@egjs/hammerjs@^2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" + integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A== + dependencies: + "@types/hammerjs" "^2.0.36" + +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" + integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + +"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + +"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + +"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + +"@ethersproject/contracts@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" + integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + +"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" + integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" + integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + aes-js "3.0.0" + scrypt-js "3.0.1" + +"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + +"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + +"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" + integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + +"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/providers@5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bech32 "1.1.4" + ws "7.4.6" + +"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" + integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + hash.js "1.1.7" + +"@ethersproject/shims@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/shims/-/shims-5.7.0.tgz#ee32e543418595774029c5ea6123ea8995e7e154" + integrity sha512-WeDptc6oAprov5CCN2LJ/6/+dC9gTonnkdAtLepm/7P5Z+3PRxS5NpfVWmOMs1yE4Vitl2cU8bOPWC0GvGSbVg== + +"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + +"@ethersproject/solidity@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" + integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + +"@ethersproject/units@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" + integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/wallet@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" + integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/json-wallets" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" + integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@firebase/analytics-compat@0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.17.tgz#c3cfc8ffb863d574ec26d86f9c8344d752832995" + integrity sha512-SJNVOeTvzdqZQvXFzj7yAirXnYcLDxh57wBFROfeowq/kRN1AqOw1tG6U4OiFOEhqi7s3xLze/LMkZatk2IEww== + dependencies: + "@firebase/analytics" "0.10.11" + "@firebase/analytics-types" "0.8.3" + "@firebase/component" "0.6.12" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.3.tgz#d08cd39a6209693ca2039ba7a81570dfa6c1518f" + integrity sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg== + +"@firebase/analytics@0.10.11": + version "0.10.11" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.11.tgz#6896413e92613573af775c45050af889a43676da" + integrity sha512-zwuPiRE0+hgcS95JZbJ6DFQN4xYFO8IyGxpeePTV51YJMwCf3lkBa6FnZ/iXIqDKcBPMgMuuEZozI0BJWaLEYg== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/installations" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.3.18": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.18.tgz#abe63858fca86b61ea431e0d9e58ccb8bac1b275" + integrity sha512-qjozwnwYmAIdrsVGrJk+hnF1WBois54IhZR6gO0wtZQoTvWL/GtiA2F31TIgAhF0ayUiZhztOv1RfC7YyrZGDQ== + dependencies: + "@firebase/app-check" "0.8.11" + "@firebase/app-check-types" "0.5.3" + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz#ed9c4a4f48d1395ef378f007476db3940aa5351a" + integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A== + +"@firebase/app-check-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.3.tgz#38ba954acf4bffe451581a32fffa20337f11d8e5" + integrity sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng== + +"@firebase/app-check@0.8.11": + version "0.8.11" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.11.tgz#3c67148046fea0a0a9a1eecf1a17fdc31a76eda7" + integrity sha512-42zIfRI08/7bQqczAy7sY2JqZYEv3a1eNa4fLFdtJ54vNevbBIRSEA3fZgRqWFNHalh5ohsBXdrYgFqaRIuCcQ== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/app-compat@0.2.48": + version "0.2.48" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.48.tgz#4cc013dc53b12c9c2ebda6369bbeb99f3cc59975" + integrity sha512-wVNU1foBIaJncUmiALyRxhHHHC3ZPMLIETTAk+2PG87eP9B/IDBsYUiTpHyboDPEI8CgBPat/zN2v+Snkz6lBw== + dependencies: + "@firebase/app" "0.10.18" + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/app-types@0.9.3": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.3.tgz#8408219eae9b1fb74f86c24e7150a148460414ad" + integrity sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw== + +"@firebase/app@0.10.18": + version "0.10.18" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.10.18.tgz#219d897beedcc833ab6d7bdc4ea810ece9e32df6" + integrity sha512-VuqEwD/QRisKd/zsFsqgvSAx34mZ3WEF47i97FD6Vw4GWAhdjepYf0Hmi6K0b4QMSgWcv/x0C30Slm5NjjERXg== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.5.17": + version "0.5.17" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.5.17.tgz#60d9222dae734fb8740ac565e342dc11f92d2392" + integrity sha512-Shi6rqLqzU9KLXnUCmlLvVByq1kiG3oe7Wpbf5m1CgS7NiRx2pSSn0HLaRRozdkaizNzMGGj+3oHmNYQ7kU6xA== + dependencies: + "@firebase/auth" "1.8.2" + "@firebase/auth-types" "0.12.3" + "@firebase/component" "0.6.12" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/auth-interop-types@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz#176a08686b0685596ff03d7879b7e4115af53de0" + integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA== + +"@firebase/auth-types@0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.3.tgz#650e54a36060b5ea012075ddbd3cb26566334c41" + integrity sha512-Zq9zI0o5hqXDtKg6yDkSnvMCMuLU6qAVS51PANQx+ZZX5xnzyNLEBO3GZgBUPsV5qIMFhjhqmLDxUqCbnAYy2A== + +"@firebase/auth@1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.8.2.tgz#4559dfabe40bc7a0605fb2a73f401d83cb32fe80" + integrity sha512-q+071y2LWe0bVnjqaX3BscqZwzdP0GKN2YBKapLq4bV88MPfCtWwGKmDhNDEDUmioOjudGXkUY5cvvKqk3mlUg== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/component@0.6.12": + version "0.6.12" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.12.tgz#08905a534e9b769164e7e1b1e80f6e7611eb67f3" + integrity sha512-YnxqjtohLbnb7raXt2YuA44cC1wA9GiehM/cmxrsoxKlFxBLy2V0OkRSj9gpngAE0UoJ421Wlav9ycO7lTPAUw== + dependencies: + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/data-connect@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@firebase/data-connect/-/data-connect-0.2.0.tgz#7133cb40096466dc17ad01a50a5f2c383605df20" + integrity sha512-7OrZtQoLSk2fiGijhIdUnTSqEFti3h1EMhw9nNiSZ6jJGduw4Pz6jrVvxjpZJtGH/JiljbMkBnPBS2h8CTRKEw== + dependencies: + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/database-compat@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-2.0.2.tgz#9ddf474b593766a41ea576185cdf115e28f0cb50" + integrity sha512-5zvdnMsfDHvrQAVM6jBS7CkBpu+z3YbpFdhxRsrK1FP45IEfxlzpeuEUb17D/tpM10vfq4Ok0x5akIBaCv7gfA== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/database" "1.0.11" + "@firebase/database-types" "1.0.8" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/database-types@1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.8.tgz#eddcce594be118bf9aebb043b5a6d51cfb6de620" + integrity sha512-6lPWIGeufhUq1heofZULyVvWFhD01TUrkkB9vyhmksjZ4XF7NaivQp9rICMk7QNhqwa+uDCaj4j+Q8qqcSVZ9g== + dependencies: + "@firebase/app-types" "0.9.3" + "@firebase/util" "1.10.3" + +"@firebase/database@1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.11.tgz#5b9960a07a0d49361f67fc69affc779cae4e07e3" + integrity sha512-gLrw/XeioswWUXgpVKCPAzzoOuvYNqK5fRUeiJTzO7Mlp9P6ylFEyPJlRBl1djqYye641r3MX6AmIeMXwjgwuQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.3.41": + version "0.3.41" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.41.tgz#434d86fd603b5ebcde19b5695b9b2a53bb23be8b" + integrity sha512-J/PgWKEt0yugETOE7lOabT16hsV21cLzSxERD7ZhaiwBQkBTSf0Mx9RhjZRT0Ttqe4weM90HGZFyUBqYA73fVA== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/firestore" "4.7.6" + "@firebase/firestore-types" "3.0.3" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.3.tgz#7d0c3dd8850c0193d8f5ee0cc8f11961407742c1" + integrity sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q== + +"@firebase/firestore@4.7.6": + version "4.7.6" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.7.6.tgz#877a37b615d86c61ac9ba7ddd0a967feb99380ac" + integrity sha512-aVDboR+upR/44qZDLR4tnZ9pepSOFBbDJnwk7eWzmTyQq2nZAVG+HIhrqpQawmUVcDRkuJv2K2UT2+oqR8F8TA== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + "@firebase/webchannel-wrapper" "1.0.3" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + +"@firebase/functions-compat@0.3.18": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.18.tgz#c6a4f6b0762c6990db0aab244420c3e1240fda2b" + integrity sha512-N7+RN5GVus2ORB8cqfSNhfSn4iaYws6F8uCCfn4mtjC7zYS/KH6muzNAhZUdUqlv5YazbVmvxlAoYYF39i8Qzg== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/functions" "0.12.1" + "@firebase/functions-types" "0.6.3" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.3.tgz#f5faf770248b13f45d256f614230da6a11bfb654" + integrity sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg== + +"@firebase/functions@0.12.1": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.12.1.tgz#09ca7386619b0c50c535f6695e5be0712d3e4730" + integrity sha512-QucRiFrvMMmIGTRhL7ZK2IeBnAWP7lAmfFREMpEtX47GjVqDqGxdFs+Mg7XBzxSc9UjDO4Rxf+aE9xJHU6bGwg== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.12" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/installations-compat@0.2.12": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.12.tgz#ee6396f3cc787c0dd4fc5dd87fec1db9dbb40c97" + integrity sha512-RhcGknkxmFu92F6Jb3rXxv6a4sytPjJGifRZj8MSURPuv2Xu+/AispCXEfY1ZraobhEHTG5HLGsP6R4l9qB5aA== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/installations" "0.6.12" + "@firebase/installations-types" "0.5.3" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.3.tgz#cac8a14dd49f09174da9df8ae453f9b359c3ef2f" + integrity sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA== + +"@firebase/installations@0.6.12": + version "0.6.12" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.12.tgz#6d9ad14e60caa8fae4ec0120c0e46ceb9d6fbdae" + integrity sha512-ES/WpuAV2k2YtBTvdaknEo7IY8vaGjIjS3zhnHSAIvY9KwTR8XZFXOJoZ3nSkjN1A5R4MtEh+07drnzPDg9vaw== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/util" "1.10.3" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.4.tgz#29e8379d20fd1149349a195ee6deee4573a86f48" + integrity sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.16": + version "0.2.16" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.16.tgz#533af4542a54b932146d175d5687aedd428be972" + integrity sha512-9HZZ88Ig3zQ0ok/Pwt4gQcNsOhoEy8hDHoGsV1am6ulgMuGuDVD2gl11Lere2ksL+msM12Lddi2x/7TCqmODZw== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/messaging" "0.12.16" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz#e647c9cd1beecfe6a6e82018a6eec37555e4da3e" + integrity sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q== + +"@firebase/messaging@0.12.16": + version "0.12.16" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.16.tgz#bd8a768274bdc4368396bd9eaa356bffb998bef2" + integrity sha512-VJ8sCEIeP3+XkfbJA7410WhYGHdloYFZXoHe/vt+vNVDGw8JQPTQSVTRvjrUprEf5I4Tbcnpr2H34lS6zhCHSA== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/installations" "0.6.12" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.10.3" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.12": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.12.tgz#069284005e3f29339b570ee517b813d9bdbc0a89" + integrity sha512-DyCbDTIwtBTGsEiQxTz/TD23a0na2nrDozceQ5kVkszyFYvliB0YK/9el0wAGIG91SqgTG9pxHtYErzfZc0VWw== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/performance" "0.6.12" + "@firebase/performance-types" "0.2.3" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.3.tgz#5ce64e90fa20ab5561f8b62a305010cf9fab86fb" + integrity sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ== + +"@firebase/performance@0.6.12": + version "0.6.12" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.6.12.tgz#58763cbbe31673351e1494875a5c10c6b68e1322" + integrity sha512-8mYL4z2jRlKXAi2hjk4G7o2sQLnJCCuTbyvti/xmHf5ZvOIGB01BZec0aDuBIXO+H1MLF62dbye/k91Fr+yc8g== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/installations" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/remote-config-compat@0.2.12": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.12.tgz#ae0b597b3228deef0e3c6b2c6e631f19213eca4c" + integrity sha512-91jLWPtubIuPBngg9SzwvNCWzhMLcyBccmt7TNZP+y1cuYFNOWWHKUXQ3IrxCLB7WwLqQaEu7fTDAjHsTyBsSw== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/remote-config" "0.5.0" + "@firebase/remote-config-types" "0.4.0" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz#91b9a836d5ca30ced68c1516163b281fbb544537" + integrity sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg== + +"@firebase/remote-config@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.5.0.tgz#30212fa77adba8a62fc6408eb32122147ae80790" + integrity sha512-weiEbpBp5PBJTHUWR4GwI7ZacaAg68BKha5QnZ8Go65W4oQjEWqCW/rfskABI/OkrGijlL3CUmCB/SA6mVo0qA== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/installations" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/storage-compat@0.3.15": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.15.tgz#02a18e0e1866849206dba7b075a4dcd99489aae7" + integrity sha512-Z9afjrK2O9o1ZHWCpprCGZ1BTc3BbvpZvi6tkSteC8H3W/fMM6x+RoSunlzD3hEVV5bkbwdJIqNClLMchvyoPA== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/storage" "0.13.5" + "@firebase/storage-types" "0.8.3" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.3.tgz#2531ef593a3452fc12c59117195d6485c6632d3d" + integrity sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg== + +"@firebase/storage@0.13.5": + version "0.13.5" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.13.5.tgz#108c86c9cd359aebd306882eb61ce6a8b1deb417" + integrity sha512-sB/7HNuW0N9tITyD0RxVLNCROuCXkml5i/iPqjwOGKC0xiUfpCOjBE+bb0ABMoN1qYZfqk0y9IuI2TdomjmkNw== + dependencies: + "@firebase/component" "0.6.12" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/util@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.10.3.tgz#63fc5fea7b36236219c4875731597494416678d1" + integrity sha512-wfoF5LTy0m2ufUapV0ZnpcGQvuavTbJ5Qr1Ze9OJGL70cSMvhDyjS4w2121XdA3lGZSTOsDOyGhpoDtYwck85A== + dependencies: + tslib "^2.1.0" + +"@firebase/vertexai@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@firebase/vertexai/-/vertexai-1.0.3.tgz#815efed8c16105676ea090daed963be72a01fb68" + integrity sha512-SQHg/RPb3LwQs/xiLcvAZYz9NXyDSZUIIwvgsKh6e4wdULAfyPCZIu6Y2ZYIhZLfk9Q44cKZ+++7RPTaqQJdYA== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/component" "0.6.12" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.10.3" + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz#a73bab8eb491d7b8b7be2f0e6c310647835afe83" + integrity sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ== + +"@grpc/grpc-js@~1.9.0": + version "1.9.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" + integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@isaacs/ttlcache@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" + integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" + integrity sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/create-cache-key-function@^29.6.3": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz#793be38148fab78e65f40ae30c36785f4ad859f0" + integrity sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA== + dependencies: + "@jest/types" "^29.6.3" + +"@jest/environment@^25.5.0": + version "25.5.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.5.0.tgz#aa33b0c21a716c65686638e7ef816c0e3a0c7b37" + integrity sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA== + dependencies: + "@jest/fake-timers" "^25.5.0" + "@jest/types" "^25.5.0" + jest-mock "^25.5.0" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.3.tgz#58561ce5db7cd253a7edddbc051fb39dda50f525" + integrity sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA== + dependencies: + jest-get-type "^28.0.2" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^25.5.0": + version "25.5.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.5.0.tgz#46352e00533c024c90c2bc2ad9f2959f7f114185" + integrity sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ== + dependencies: + "@jest/types" "^25.5.0" + jest-message-util "^25.5.0" + jest-mock "^25.5.0" + jest-util "^25.5.0" + lolex "^5.0.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^25.1.0": + version "25.5.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-25.5.2.tgz#5e45e9de8d228716af3257eeb3991cc2e162ca88" + integrity sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA== + dependencies: + "@jest/environment" "^25.5.0" + "@jest/types" "^25.5.0" + expect "^25.5.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/schemas@^29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" + integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^25.5.0": + version "25.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" + integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@jest/types@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" + integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== + dependencies: + "@jest/schemas" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" + integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.12": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@jridgewell/trace-mapping@^0.3.18": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" + integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@json-rpc-tools/types@^1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@json-rpc-tools/types/-/types-1.7.6.tgz#5abd5fde01364a130c46093b501715bcce5bdc0e" + integrity sha512-nDSqmyRNEqEK9TZHtM15uNnDljczhCUdBmRhpNZ95bIPKEDQ+nTDmGMFd2lLin3upc5h2VVVd9tkTDdbXUhDIQ== + dependencies: + keyvaluestorage-interface "^1.0.0" + +"@json-rpc-tools/utils@^1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@json-rpc-tools/utils/-/utils-1.7.6.tgz#67f04987dbaa2e7adb6adff1575367b75a9a9ba1" + integrity sha512-HjA8x/U/Q78HRRe19yh8HVKoZ+Iaoo3YZjakJYxR+rw52NHo6jM+VE9b8+7ygkCFXl/EHID5wh/MkXaE/jGyYw== + dependencies: + "@json-rpc-tools/types" "^1.7.6" + "@pedrouid/environment" "^1.0.1" + +"@mapbox/node-pre-gyp@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@parcel/watcher-android-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" + integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg== + +"@parcel/watcher-darwin-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz#c817c7a3b4f3a79c1535bfe54a1c2818d9ffdc34" + integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA== + +"@parcel/watcher-darwin-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020" + integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg== + +"@parcel/watcher-freebsd-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8" + integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w== + +"@parcel/watcher-linux-arm-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d" + integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA== + +"@parcel/watcher-linux-arm64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7" + integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA== + +"@parcel/watcher-linux-arm64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635" + integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA== + +"@parcel/watcher-linux-x64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39" + integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg== + +"@parcel/watcher-linux-x64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16" + integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ== + +"@parcel/watcher-wasm@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-wasm/-/watcher-wasm-2.4.1.tgz#c4353e4fdb96ee14389856f7f6f6d21b7dcef9e1" + integrity sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA== + dependencies: + is-glob "^4.0.3" + micromatch "^4.0.5" + napi-wasm "^1.1.0" + +"@parcel/watcher-win32-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc" + integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg== + +"@parcel/watcher-win32-ia32@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7" + integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw== + +"@parcel/watcher-win32-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf" + integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A== + +"@parcel/watcher@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.1.tgz#a50275151a1bb110879c6123589dba90c19f1bf8" + integrity sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.4.1" + "@parcel/watcher-darwin-arm64" "2.4.1" + "@parcel/watcher-darwin-x64" "2.4.1" + "@parcel/watcher-freebsd-x64" "2.4.1" + "@parcel/watcher-linux-arm-glibc" "2.4.1" + "@parcel/watcher-linux-arm64-glibc" "2.4.1" + "@parcel/watcher-linux-arm64-musl" "2.4.1" + "@parcel/watcher-linux-x64-glibc" "2.4.1" + "@parcel/watcher-linux-x64-musl" "2.4.1" + "@parcel/watcher-win32-arm64" "2.4.1" + "@parcel/watcher-win32-ia32" "2.4.1" + "@parcel/watcher-win32-x64" "2.4.1" + +"@pedrouid/environment@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@pedrouid/environment/-/environment-1.0.1.tgz#858f0f8a057340e0b250398b75ead77d6f4342ec" + integrity sha512-HaW78NszGzRZd9SeoI3JD11JqY+lubnaOx7Pewj5pfjqWXOEATpeKIFb9Z4t2WBUK2iryiXX3lzWwmYWgUL0Ug== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@react-native-async-storage/async-storage@1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.19.3.tgz#ad5fe3ed0a82d4624aa4500321c1e09c02daeb46" + integrity sha512-CwGfoHCWdPOTPS+2fW6YRE1fFBpT9++ahLEroX5hkgwyoQ+TkmjOaUxixdEIoVua9Pz5EF2pGOIJzqOTMWfBlA== + dependencies: + merge-options "^3.0.4" + +"@react-native-camera-roll/camera-roll@7.5.2": + version "7.5.2" + resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-7.5.2.tgz#2b248a835fbb8b53d04fc0c2957adba1474d9c99" + integrity sha512-XiVIrW17EFXrFzqB48q6cQOaYeVnw0iC3tH+Jhl+MAHDYGLJp+ulzxCNNwngaMvnVAA5Q2mUMzRocUiJPy8q0g== + +"@react-native-clipboard/clipboard@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.13.2.tgz#28adcfc43ed2addddf79a59198ec1b25087c115e" + integrity sha512-uVM55oEGc6a6ZmSATDeTcMm55A/C1km5X47g0xaoF0Zagv7N/8RGvLceA5L/izPwflIy78t7XQeJUcnGSib0nA== + +"@react-native-community/audio-toolkit@git+https://github.com/tbenr/react-native-audio-toolkit.git#refs/tags/v2.0.3-status-v6": + version "2.0.3" + resolved "git+https://github.com/tbenr/react-native-audio-toolkit.git#7ae9055cf6169b30f5089bda7bfcfc1c40a715e5" + dependencies: + async "^2.6.3" + eventemitter3 "^1.2.0" + lodash "^4.17.15" + +"@react-native-community/blur@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.4.1.tgz#72cbc0be5a84022c33091683ec6888925ebcca6e" + integrity sha512-XBSsRiYxE/MOEln2ayunShfJtWztHwUxLFcSL20o+HNNRnuUDv+GXkF6FmM2zE8ZUfrnhQ/zeTqvnuDPGw6O8A== + +"@react-native-community/cli-clean@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-12.3.6.tgz#e8a7910bebc97266fd5068649013a03958021fc4" + integrity sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig== + dependencies: + "@react-native-community/cli-tools" "12.3.6" + chalk "^4.1.2" + execa "^5.0.0" + +"@react-native-community/cli-config@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-12.3.6.tgz#5f0be68270217908a739c32e3155a0e354773251" + integrity sha512-JGWSYQ9EAK6m2v0abXwFLEfsqJ1zkhzZ4CV261QZF9MoUNB6h57a274h1MLQR9mG6Tsh38wBUuNfEPUvS1vYew== + dependencies: + "@react-native-community/cli-tools" "12.3.6" + chalk "^4.1.2" + cosmiconfig "^5.1.0" + deepmerge "^4.3.0" + glob "^7.1.3" + joi "^17.2.1" + +"@react-native-community/cli-debugger-ui@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-12.3.6.tgz#418027a1ae76850079684d309a732eb378c7f690" + integrity sha512-SjUKKsx5FmcK9G6Pb6UBFT0s9JexVStK5WInmANw75Hm7YokVvHEgtprQDz2Uvy5znX5g2ujzrkIU//T15KQzA== + dependencies: + serve-static "^1.13.1" + +"@react-native-community/cli-doctor@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-12.3.6.tgz#f68b51bbc6554ff4837269d98e9e405044e6f1b9" + integrity sha512-fvBDv2lTthfw4WOQKkdTop2PlE9GtfrlNnpjB818MhcdEnPjfQw5YaTUcnNEGsvGomdCs1MVRMgYXXwPSN6OvQ== + dependencies: + "@react-native-community/cli-config" "12.3.6" + "@react-native-community/cli-platform-android" "12.3.6" + "@react-native-community/cli-platform-ios" "12.3.6" + "@react-native-community/cli-tools" "12.3.6" + chalk "^4.1.2" + command-exists "^1.2.8" + deepmerge "^4.3.0" + envinfo "^7.10.0" + execa "^5.0.0" + hermes-profile-transformer "^0.0.6" + node-stream-zip "^1.9.1" + ora "^5.4.1" + semver "^7.5.2" + strip-ansi "^5.2.0" + wcwidth "^1.0.1" + yaml "^2.2.1" + +"@react-native-community/cli-hermes@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-12.3.6.tgz#5ac2c9ee26c69e1ce6b5047ba0f399984a6dea16" + integrity sha512-sNGwfOCl8OAIjWCkwuLpP8NZbuO0dhDI/2W7NeOGDzIBsf4/c4MptTrULWtGIH9okVPLSPX0NnRyGQ+mSwWyuQ== + dependencies: + "@react-native-community/cli-platform-android" "12.3.6" + "@react-native-community/cli-tools" "12.3.6" + chalk "^4.1.2" + hermes-profile-transformer "^0.0.6" + +"@react-native-community/cli-platform-android@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-12.3.6.tgz#e1103692c659ff0b72ee6f00b7c72578db7376ec" + integrity sha512-DeDDAB8lHpuGIAPXeeD9Qu2+/wDTFPo99c8uSW49L0hkmZJixzvvvffbGQAYk32H0TmaI7rzvzH+qzu7z3891g== + dependencies: + "@react-native-community/cli-tools" "12.3.6" + chalk "^4.1.2" + execa "^5.0.0" + fast-xml-parser "^4.2.4" + glob "^7.1.3" + logkitty "^0.7.1" + +"@react-native-community/cli-platform-ios@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-12.3.6.tgz#e7decb5ee764f5fdc7a6ad1ba5e15de8929d54a5" + integrity sha512-3eZ0jMCkKUO58wzPWlvAPRqezVKm9EPZyaPyHbRPWU8qw7JqkvnRlWIaYDGpjCJgVW4k2hKsEursLtYKb188tg== + dependencies: + "@react-native-community/cli-tools" "12.3.6" + chalk "^4.1.2" + execa "^5.0.0" + fast-xml-parser "^4.0.12" + glob "^7.1.3" + ora "^5.4.1" + +"@react-native-community/cli-plugin-metro@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-12.3.6.tgz#ae62de18e998478db60a3fe10dc746162c272dbd" + integrity sha512-3jxSBQt4fkS+KtHCPSyB5auIT+KKIrPCv9Dk14FbvOaEh9erUWEm/5PZWmtboW1z7CYeNbFMeXm9fM2xwtVOpg== + +"@react-native-community/cli-server-api@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-12.3.6.tgz#cd78122954a02d22c7821c365938635b51ddd1bd" + integrity sha512-80NIMzo8b2W+PL0Jd7NjiJW9mgaT8Y8wsIT/lh6mAvYH7mK0ecDJUYUTAAv79Tbo1iCGPAr3T295DlVtS8s4yQ== + dependencies: + "@react-native-community/cli-debugger-ui" "12.3.6" + "@react-native-community/cli-tools" "12.3.6" + compression "^1.7.1" + connect "^3.6.5" + errorhandler "^1.5.1" + nocache "^3.0.1" + pretty-format "^26.6.2" + serve-static "^1.13.1" + ws "^7.5.1" + +"@react-native-community/cli-tools@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-12.3.6.tgz#c39965982347635dfaf1daa7b3c0133b3bd45e64" + integrity sha512-FPEvZn19UTMMXUp/piwKZSh8cMEfO8G3KDtOwo53O347GTcwNrKjgZGtLSPELBX2gr+YlzEft3CoRv2Qmo83fQ== + dependencies: + appdirsjs "^1.2.4" + chalk "^4.1.2" + find-up "^5.0.0" + mime "^2.4.1" + node-fetch "^2.6.0" + open "^6.2.0" + ora "^5.4.1" + semver "^7.5.2" + shell-quote "^1.7.3" + sudo-prompt "^9.0.0" + +"@react-native-community/cli-types@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-types/-/cli-types-12.3.6.tgz#239de348800fe1ffba3eb1fe0edbeb9306981e57" + integrity sha512-xPqTgcUtZowQ8WKOkI9TLGBwH2bGggOC4d2FFaIRST3gTcjrEeGRNeR5aXCzJFIgItIft8sd7p2oKEdy90+01Q== + dependencies: + joi "^17.2.1" + +"@react-native-community/cli@12.3.6": + version "12.3.6" + resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-12.3.6.tgz#7a323b78725b959bb8a31cca1145918263ff3c8d" + integrity sha512-647OSi6xBb8FbwFqX9zsJxOzu685AWtrOUWHfOkbKD+5LOpGORw+GQo0F9rWZnB68rLQyfKUZWJeaD00pGv5fw== + dependencies: + "@react-native-community/cli-clean" "12.3.6" + "@react-native-community/cli-config" "12.3.6" + "@react-native-community/cli-debugger-ui" "12.3.6" + "@react-native-community/cli-doctor" "12.3.6" + "@react-native-community/cli-hermes" "12.3.6" + "@react-native-community/cli-plugin-metro" "12.3.6" + "@react-native-community/cli-server-api" "12.3.6" + "@react-native-community/cli-tools" "12.3.6" + "@react-native-community/cli-types" "12.3.6" + chalk "^4.1.2" + commander "^9.4.1" + deepmerge "^4.3.0" + execa "^5.0.0" + find-up "^4.1.0" + fs-extra "^8.1.0" + graceful-fs "^4.1.3" + prompts "^2.4.2" + semver "^7.5.2" + +"@react-native-community/hooks@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-3.0.0.tgz#af5f2ca32eea59b792ce9e3d9a4cf0354f9b195f" + integrity sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg== + +"@react-native-community/masked-view@^0.1.6": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.9.tgz#383aca2fb053e3e14405c99cce2d5805df730821" + integrity sha512-nUtzbiLeXU0K9oVed6rc/WVrjGJwDSL4q1RTDkpZYU4j0FeovfuzcNUIDesD2r728LYfIop+uAgQdm+6qBOCug== + +"@react-native-community/netinfo@^4.4.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-4.7.0.tgz#7482d36836cac69d0a0ae25581f65bc472639930" + integrity sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA== + +"@react-native-community/push-notification-ios@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@react-native-community/push-notification-ios/-/push-notification-ios-1.4.1.tgz#02b46fb793926f34aa275605ed49f03015e893b3" + integrity sha512-Y+4LS10R+yc17wu54tlDcxgW/SugEAz2dNjmil9I7KUtaZIOc0hTvAE8dUvYTEDvYQ9uYrXI+OqdElTE3FJ3FA== + dependencies: + invariant "^2.2.4" + +"@react-native-community/slider@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.0.tgz#ffbf78689fc0572fb5c1e2ccb61b2ef074d3dcd2" + integrity sha512-deNc3JHBHz24YN+0DTAocXfrYFIFc1DvsIriMJSsJlR/MvsLzoq2+qwaEN+0/LJ37pstv85wZWY0pNugk4e41g== + +"@react-native-firebase/app@^21.7.2": + version "21.7.2" + resolved "https://registry.yarnpkg.com/@react-native-firebase/app/-/app-21.7.2.tgz#f45c237522d11165ac0cf283031155f043f8920f" + integrity sha512-9lwuRSKdS5Zv5iaVu2HIbgdOrVozC3J7xW8xlcQOBGU/iygDWtt0wXA12UqeFO96qPFYFrmFPO8CU15FTsM/Hw== + dependencies: + firebase "11.2.0" + +"@react-native-firebase/messaging@^21.7.2": + version "21.7.2" + resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-21.7.2.tgz#12f751d83071880e731d14c77c4082e6b3a3f8fb" + integrity sha512-Kapksse0VjitdCm3gS5aGoCnYb6tEyqOVQYdZg8Sx/eT+iNDZo5fv6+aeV0zALGLjRSZ8GMzTPTFVd5Cu78nEA== + +"@react-native/assets-registry@0.73.1": + version "0.73.1" + resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.73.1.tgz#e2a6b73b16c183a270f338dc69c36039b3946e85" + integrity sha512-2FgAbU7uKM5SbbW9QptPPZx8N9Ke2L7bsHb+EhAanZjFZunA9PaYtyjUQ1s7HD+zDVqOQIvjkpXSv7Kejd2tqg== + +"@react-native/babel-plugin-codegen@0.73.4": + version "0.73.4" + resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.73.4.tgz#8a2037d5585b41877611498ae66adbf1dddfec1b" + integrity sha512-XzRd8MJGo4Zc5KsphDHBYJzS1ryOHg8I2gOZDAUCGcwLFhdyGu1zBNDJYH2GFyDrInn9TzAbRIf3d4O+eltXQQ== + dependencies: + "@react-native/codegen" "0.73.3" + +"@react-native/babel-preset@0.73.21": + version "0.73.21" + resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.73.21.tgz#174c16493fa4e311b2f5f0c58d4f3c6a5a68bbea" + integrity sha512-WlFttNnySKQMeujN09fRmrdWqh46QyJluM5jdtDNrkl/2Hx6N4XeDUGhABvConeK95OidVO7sFFf7sNebVXogA== + dependencies: + "@babel/core" "^7.20.0" + "@babel/plugin-proposal-async-generator-functions" "^7.0.0" + "@babel/plugin-proposal-class-properties" "^7.18.0" + "@babel/plugin-proposal-export-default-from" "^7.0.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.0" + "@babel/plugin-proposal-numeric-separator" "^7.0.0" + "@babel/plugin-proposal-object-rest-spread" "^7.20.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" + "@babel/plugin-proposal-optional-chaining" "^7.20.0" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-default-from" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.18.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-syntax-optional-chaining" "^7.0.0" + "@babel/plugin-transform-arrow-functions" "^7.0.0" + "@babel/plugin-transform-async-to-generator" "^7.20.0" + "@babel/plugin-transform-block-scoping" "^7.0.0" + "@babel/plugin-transform-classes" "^7.0.0" + "@babel/plugin-transform-computed-properties" "^7.0.0" + "@babel/plugin-transform-destructuring" "^7.20.0" + "@babel/plugin-transform-flow-strip-types" "^7.20.0" + "@babel/plugin-transform-function-name" "^7.0.0" + "@babel/plugin-transform-literals" "^7.0.0" + "@babel/plugin-transform-modules-commonjs" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.0.0" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.11" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" + "@babel/plugin-transform-runtime" "^7.0.0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0" + "@babel/plugin-transform-spread" "^7.0.0" + "@babel/plugin-transform-sticky-regex" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.5.0" + "@babel/plugin-transform-unicode-regex" "^7.0.0" + "@babel/template" "^7.0.0" + "@react-native/babel-plugin-codegen" "0.73.4" + babel-plugin-transform-flow-enums "^0.0.2" + react-refresh "^0.14.0" + +"@react-native/codegen@0.73.2": + version "0.73.2" + resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.73.2.tgz#58af4e4c3098f0e6338e88ec64412c014dd51519" + integrity sha512-lfy8S7umhE3QLQG5ViC4wg5N1Z+E6RnaeIw8w1voroQsXXGPB72IBozh8dAHR3+ceTxIU0KX3A8OpJI8e1+HpQ== + dependencies: + "@babel/parser" "^7.20.0" + flow-parser "^0.206.0" + glob "^7.1.1" + invariant "^2.2.4" + jscodeshift "^0.14.0" + mkdirp "^0.5.1" + nullthrows "^1.1.1" + +"@react-native/codegen@0.73.3": + version "0.73.3" + resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.73.3.tgz#cc984a8b17334d986cc600254a0d4b7fa7d68a94" + integrity sha512-sxslCAAb8kM06vGy9Jyh4TtvjhcP36k/rvj2QE2Jdhdm61KvfafCATSIsOfc0QvnduWFcpXUPvAVyYwuv7PYDg== + dependencies: + "@babel/parser" "^7.20.0" + flow-parser "^0.206.0" + glob "^7.1.1" + invariant "^2.2.4" + jscodeshift "^0.14.0" + mkdirp "^0.5.1" + nullthrows "^1.1.1" + +"@react-native/community-cli-plugin@0.73.17": + version "0.73.17" + resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.73.17.tgz#37b381a8b503a3296eaa6727e0c52ea8835add28" + integrity sha512-F3PXZkcHg+1ARIr6FRQCQiB7ZAA+MQXGmq051metRscoLvgYJwj7dgC8pvgy0kexzUkHu5BNKrZeySzUft3xuQ== + dependencies: + "@react-native-community/cli-server-api" "12.3.6" + "@react-native-community/cli-tools" "12.3.6" + "@react-native/dev-middleware" "0.73.8" + "@react-native/metro-babel-transformer" "0.73.15" + chalk "^4.0.0" + execa "^5.1.1" + metro "^0.80.3" + metro-config "^0.80.3" + metro-core "^0.80.3" + node-fetch "^2.2.0" + readline "^1.3.0" + +"@react-native/debugger-frontend@0.73.3": + version "0.73.3" + resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.73.3.tgz#033757614d2ada994c68a1deae78c1dd2ad33c2b" + integrity sha512-RgEKnWuoo54dh7gQhV7kvzKhXZEhpF9LlMdZolyhGxHsBqZ2gXdibfDlfcARFFifPIiaZ3lXuOVVa4ei+uPgTw== + +"@react-native/dev-middleware@0.73.8": + version "0.73.8" + resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.73.8.tgz#2e43722a00c7b8db753f747f40267cbad6caba4d" + integrity sha512-oph4NamCIxkMfUL/fYtSsE+JbGOnrlawfQ0kKtDQ5xbOjPKotKoXqrs1eGwozNKv7FfQ393stk1by9a6DyASSg== + dependencies: + "@isaacs/ttlcache" "^1.4.1" + "@react-native/debugger-frontend" "0.73.3" + chrome-launcher "^0.15.2" + chromium-edge-launcher "^1.0.0" + connect "^3.6.5" + debug "^2.2.0" + node-fetch "^2.2.0" + open "^7.0.3" + serve-static "^1.13.1" + temp-dir "^2.0.0" + ws "^6.2.2" + +"@react-native/gradle-plugin@0.73.4": + version "0.73.4" + resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.73.4.tgz#aa55784a8c2b471aa89934db38c090d331baf23b" + integrity sha512-PMDnbsZa+tD55Ug+W8CfqXiGoGneSSyrBZCMb5JfiB3AFST3Uj5e6lw8SgI/B6SKZF7lG0BhZ6YHZsRZ5MlXmg== + +"@react-native/js-polyfills@0.73.1": + version "0.73.1" + resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.73.1.tgz#730b0a7aaab947ae6f8e5aa9d995e788977191ed" + integrity sha512-ewMwGcumrilnF87H4jjrnvGZEaPFCAC4ebraEK+CurDDmwST/bIicI4hrOAv+0Z0F7DEK4O4H7r8q9vH7IbN4g== + +"@react-native/metro-babel-transformer@0.73.15": + version "0.73.15" + resolved "https://registry.yarnpkg.com/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.73.15.tgz#c516584dde62d65a46668074084359c03e6a50f1" + integrity sha512-LlkSGaXCz+xdxc9819plmpsl4P4gZndoFtpjN3GMBIu6f7TBV0GVbyJAU4GE8fuAWPVSVL5ArOcdkWKSbI1klw== + dependencies: + "@babel/core" "^7.20.0" + "@react-native/babel-preset" "0.73.21" + hermes-parser "0.15.0" + nullthrows "^1.1.1" + +"@react-native/metro-config@0.73.5": + version "0.73.5" + resolved "https://registry.yarnpkg.com/@react-native/metro-config/-/metro-config-0.73.5.tgz#791242ca93057d7299ce18379ea11d3bdb368ea9" + integrity sha512-3bNWoHzOzP/+qoLJtRhOVXrnxKmSY3i4y5PXyMQlIvvOI/GQbXulPpEZxK/yUrf1MmeXHLLFufFbQWlfDEDoxA== + dependencies: + "@react-native/js-polyfills" "0.73.1" + "@react-native/metro-babel-transformer" "0.73.15" + metro-config "^0.80.3" + metro-runtime "^0.80.3" + +"@react-native/normalize-color@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.1.0.tgz#939b87a9849e81687d3640c5efa2a486ac266f91" + integrity sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA== + +"@react-native/normalize-colors@0.73.2", "@react-native/normalize-colors@^0.73.0": + version "0.73.2" + resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.73.2.tgz#cc8e48fbae2bbfff53e12f209369e8d2e4cf34ec" + integrity sha512-bRBcb2T+I88aG74LMVHaKms2p/T8aQd8+BZ7LuuzXlRfog1bMWWn/C5i0HVuvW4RPtXQYgIlGiXVDy9Ir1So/w== + +"@react-native/virtualized-lists@0.73.4": + version "0.73.4" + resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.73.4.tgz#640e594775806f63685435b5d9c3d05c378ccd8c" + integrity sha512-HpmLg1FrEiDtrtAbXiwCgXFYyloK/dOIPIuWW3fsqukwJEWAiTzm1nXGJ7xPU5XTHiWZ4sKup5Ebaj8z7iyWog== + dependencies: + invariant "^2.2.4" + nullthrows "^1.1.1" + +"@reown/walletkit@^1.0.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@reown/walletkit/-/walletkit-1.1.2.tgz#6c2a61b483ef50090d0b7c07fb086407f80904c2" + integrity sha512-JcrO8d7FLMG8R99TeUTDpm7lhZjD2oqKuIs6TWK2obLrnpXtD0/S+FdoJoBXffjDzhv38npCMqYs0QGUu1Fo0A== + dependencies: + "@walletconnect/core" "2.17.3" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "2.1.2" + "@walletconnect/sign-client" "2.17.3" + "@walletconnect/types" "2.17.3" + "@walletconnect/utils" "2.17.3" + +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinclair/typebox@^0.24.1": + version "0.24.51" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" + integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@sinonjs/commons@^1.7.0": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" + integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw== + dependencies: + type-detect "4.0.8" + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@stablelib/aead@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" + integrity sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg== + +"@stablelib/binary@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f" + integrity sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q== + dependencies: + "@stablelib/int" "^1.0.1" + +"@stablelib/bytes@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/bytes/-/bytes-1.0.1.tgz#0f4aa7b03df3080b878c7dea927d01f42d6a20d8" + integrity sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ== + +"@stablelib/chacha20poly1305@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/chacha20poly1305/-/chacha20poly1305-1.0.1.tgz#de6b18e283a9cb9b7530d8767f99cde1fec4c2ee" + integrity sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA== + dependencies: + "@stablelib/aead" "^1.0.1" + "@stablelib/binary" "^1.0.1" + "@stablelib/chacha" "^1.0.1" + "@stablelib/constant-time" "^1.0.1" + "@stablelib/poly1305" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/chacha@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/chacha/-/chacha-1.0.1.tgz#deccfac95083e30600c3f92803a3a1a4fa761371" + integrity sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/constant-time@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/constant-time/-/constant-time-1.0.1.tgz#bde361465e1cf7b9753061b77e376b0ca4c77e35" + integrity sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg== + +"@stablelib/ed25519@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@stablelib/ed25519/-/ed25519-1.0.3.tgz#f8fdeb6f77114897c887bb6a3138d659d3f35996" + integrity sha512-puIMWaX9QlRsbhxfDc5i+mNPMY+0TmQEskunY1rZEBPi1acBCVQAhnsk/1Hk50DGPtVsZtAWQg4NHGlVaO9Hqg== + dependencies: + "@stablelib/random" "^1.0.2" + "@stablelib/sha512" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/hash@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/hash/-/hash-1.0.1.tgz#3c944403ff2239fad8ebb9015e33e98444058bc5" + integrity sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg== + +"@stablelib/hkdf@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/hkdf/-/hkdf-1.0.1.tgz#b4efd47fd56fb43c6a13e8775a54b354f028d98d" + integrity sha512-SBEHYE16ZXlHuaW5RcGk533YlBj4grMeg5TooN80W3NpcHRtLZLLXvKyX0qcRFxf+BGDobJLnwkvgEwHIDBR6g== + dependencies: + "@stablelib/hash" "^1.0.1" + "@stablelib/hmac" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/hmac@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/hmac/-/hmac-1.0.1.tgz#3d4c1b8cf194cb05d28155f0eed8a299620a07ec" + integrity sha512-V2APD9NSnhVpV/QMYgCVMIYKiYG6LSqw1S65wxVoirhU/51ACio6D4yDVSwMzuTJXWZoVHbDdINioBwKy5kVmA== + dependencies: + "@stablelib/constant-time" "^1.0.1" + "@stablelib/hash" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/int@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/int/-/int-1.0.1.tgz#75928cc25d59d73d75ae361f02128588c15fd008" + integrity sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w== + +"@stablelib/keyagreement@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/keyagreement/-/keyagreement-1.0.1.tgz#4612efb0a30989deb437cd352cee637ca41fc50f" + integrity sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg== + dependencies: + "@stablelib/bytes" "^1.0.1" + +"@stablelib/poly1305@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/poly1305/-/poly1305-1.0.1.tgz#93bfb836c9384685d33d70080718deae4ddef1dc" + integrity sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA== + dependencies: + "@stablelib/constant-time" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/random@1.0.2", "@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.2.tgz#2dece393636489bf7e19c51229dd7900eddf742c" + integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/sha256@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/sha256/-/sha256-1.0.1.tgz#77b6675b67f9b0ea081d2e31bda4866297a3ae4f" + integrity sha512-GIIH3e6KH+91FqGV42Kcj71Uefd/QEe7Dy42sBTeqppXV95ggCcxLTk39bEr+lZfJmp+ghsR07J++ORkRELsBQ== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/hash" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/sha512@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/sha512/-/sha512-1.0.1.tgz#6da700c901c2c0ceacbd3ae122a38ac57c72145f" + integrity sha512-13gl/iawHV9zvDKciLo1fQ8Bgn2Pvf7OV6amaRVKiq3pjQ3UmEpXxWiAfV8tYjUpeZroBxtyrwtdooQT/i3hzw== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/hash" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/wipe@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36" + integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== + +"@stablelib/x25519@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@stablelib/x25519/-/x25519-1.0.3.tgz#13c8174f774ea9f3e5e42213cbf9fc68a3c7b7fd" + integrity sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw== + dependencies: + "@stablelib/keyagreement" "^1.0.1" + "@stablelib/random" "^1.0.2" + "@stablelib/wipe" "^1.0.1" + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@testing-library/jest-native@^5.4.3": + version "5.4.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-native/-/jest-native-5.4.3.tgz#9334c68eaf45db9eb20d0876728cc5d7fc2c3ea2" + integrity sha512-/sSDGaOuE+PJ1Z9Kp4u7PQScSVVXGud59I/qsBFFJvIbcn4P6yYw6cBnBmbPF+X9aRIsTJRDl6gzw5ZkJNm66w== + dependencies: + chalk "^4.1.2" + jest-diff "^29.0.1" + jest-matcher-utils "^29.0.1" + pretty-format "^29.0.3" + redent "^3.0.0" + +"@testing-library/react-native@^12.4.2": + version "12.4.2" + resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-12.4.2.tgz#1f3e200351ac664ae78169c754c54fe36009a0ef" + integrity sha512-6Uu3M+ga1iDgleS55mGBqI1GHjvKu1T0aESSHkT8dL+M3nIZkW8jZ9HqDSj2UohkMA1hlb+7PRxS+EjwllyANA== + dependencies: + jest-matcher-utils "^29.7.0" + pretty-format "^29.7.0" + redent "^3.0.0" + +"@tsconfig/react-native@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/react-native/-/react-native-3.0.2.tgz#f7db242eee4820f5a3d0edcc86c920bb7d9ec0f2" + integrity sha512-F7IoHEqf741lut4Z2K+IkWQRvXAhBiZMeY5L7BysG7Z2Z3MlIyFR+AagD8jQ/CqC1vowGnRwfLjeuwIpaeoJxA== + +"@types/babel__core@^7.1.14": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b" + integrity sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04" + integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.10" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.10.tgz#d9a99f017317d9b3d1abc2ced45d3bca68df0daf" + integrity sha512-74fNdUGrWsgIB/V9kTO5FGHPWYY6Eqn+3Z7L6Hc4e/BxjYV7puvBqp5HwsVYYfLm6iURYBNCx4Ut37OF9yitCw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/graceful-fs@^4.1.3": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + dependencies: + "@types/node" "*" + +"@types/hammerjs@^2.0.36": + version "2.0.36" + resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.36.tgz#17ce0a235e9ffbcdcdf5095646b374c2bf615a4c" + integrity sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ== + +"@types/hast@^2.0.0": + version "2.3.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" + integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g== + dependencies: + "@types/unist" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^28.1.6": + version "28.1.8" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.8.tgz#6936409f3c9724ea431efd412ea0238a0f03b09b" + integrity sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw== + dependencies: + expect "^28.0.0" + pretty-format "^28.0.0" + +"@types/node@*": + version "18.11.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" + integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== + +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "22.13.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33" + integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew== + dependencies: + undici-types "~6.20.0" + +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-test-renderer@^18.0.0": + version "18.0.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.1.tgz#d3f308912fcc4491e4fbc134b906bb65bdee73f6" + integrity sha512-LjEF+jTUCjzd+Qq4eWqsmZvEWPA/l4L0my+YWN5US8Fo3wZOMiyrpBshHDFbkO8usjdO1B430mEWNU/i1MF7Qg== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "18.2.21" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" + integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/react@^18.2.6": + version "18.2.45" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.45.tgz#253f4fac288e7e751ab3dc542000fb687422c15c" + integrity sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.3" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" + integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== + +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/unist@*": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" + integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== + +"@types/yargs-parser@*": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + +"@types/yargs@^15.0.0": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" + integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg== + dependencies: + "@types/yargs-parser" "*" + +"@types/yargs@^17.0.8": + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" + integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== + dependencies: + "@types/yargs-parser" "*" + +"@walletconnect/core@2.17.3": + version "2.17.3" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.17.3.tgz#e59045a666951e9fc2e8420130c4f93221bd2492" + integrity sha512-57uv0FW4L6H/tmkb1kS2nG41MDguyDgZbGR58nkDUd1TO/HydyiTByVOhFzIxgN331cnY/1G1rMaKqncgdnOFA== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.16" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.17.3" + "@walletconnect/utils" "2.17.3" + "@walletconnect/window-getters" "1.0.1" + events "3.3.0" + lodash.isequal "4.5.0" + uint8arrays "3.1.0" + +"@walletconnect/environment@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.1.tgz#1d7f82f0009ab821a2ba5ad5e5a7b8ae3b214cd7" + integrity sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg== + dependencies: + tslib "1.14.1" + +"@walletconnect/events@1.0.1", "@walletconnect/events@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@walletconnect/events/-/events-1.0.1.tgz#2b5f9c7202019e229d7ccae1369a9e86bda7816c" + integrity sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ== + dependencies: + keyvaluestorage-interface "^1.0.0" + tslib "1.14.1" + +"@walletconnect/heartbeat@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz#e8dc5179db7769950c6f9cf59b23516d9b95227d" + integrity sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw== + dependencies: + "@walletconnect/events" "^1.0.1" + "@walletconnect/time" "^1.0.2" + events "^3.3.0" + +"@walletconnect/jsonrpc-provider@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz#696f3e3b6d728b361f2e8b853cfc6afbdf2e4e3e" + integrity sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow== + dependencies: + "@walletconnect/jsonrpc-utils" "^1.0.8" + "@walletconnect/safe-json" "^1.0.2" + events "^3.3.0" + +"@walletconnect/jsonrpc-types@1.0.4", "@walletconnect/jsonrpc-types@^1.0.2", "@walletconnect/jsonrpc-types@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz#ce1a667d79eadf2a2d9d002c152ceb68739c230c" + integrity sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ== + dependencies: + events "^3.3.0" + keyvaluestorage-interface "^1.0.0" + +"@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz#82d0cc6a5d6ff0ecc277cb35f71402c91ad48d72" + integrity sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw== + dependencies: + "@walletconnect/environment" "^1.0.1" + "@walletconnect/jsonrpc-types" "^1.0.3" + tslib "1.14.1" + +"@walletconnect/jsonrpc-ws-connection@1.0.16": + version "1.0.16" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz#666bb13fbf32a2d4f7912d5b4d0bdef26a1d057b" + integrity sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q== + dependencies: + "@walletconnect/jsonrpc-utils" "^1.0.6" + "@walletconnect/safe-json" "^1.0.2" + events "^3.3.0" + ws "^7.5.1" + +"@walletconnect/keyvaluestorage@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz#dd2caddabfbaf80f6b8993a0704d8b83115a1842" + integrity sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA== + dependencies: + "@walletconnect/safe-json" "^1.0.1" + idb-keyval "^6.2.1" + unstorage "^1.9.0" + +"@walletconnect/logger@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.1.2.tgz#813c9af61b96323a99f16c10089bfeb525e2a272" + integrity sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw== + dependencies: + "@walletconnect/safe-json" "^1.0.2" + pino "7.11.0" + +"@walletconnect/react-native-compat@2.17.3": + version "2.17.3" + resolved "https://registry.yarnpkg.com/@walletconnect/react-native-compat/-/react-native-compat-2.17.3.tgz#e26d3f584176d3111f247dfaeea14687e8c2c195" + integrity sha512-lHKwXKoB0rdDH1ukxUx7o86xosWbttWIHYMZ8tgAQC1k9VH3CZZCoBcHOAAX8iBzyb0n0UP3/9zRrOcJE5nz7Q== + dependencies: + events "3.3.0" + fast-text-encoding "1.0.6" + react-native-url-polyfill "2.0.0" + +"@walletconnect/relay-api@1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.11.tgz#80ab7ef2e83c6c173be1a59756f95e515fb63224" + integrity sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q== + dependencies: + "@walletconnect/jsonrpc-types" "^1.0.2" + +"@walletconnect/relay-auth@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@walletconnect/relay-auth/-/relay-auth-1.0.4.tgz#0b5c55c9aa3b0ef61f526ce679f3ff8a5c4c2c7c" + integrity sha512-kKJcS6+WxYq5kshpPaxGHdwf5y98ZwbfuS4EE/NkQzqrDFm5Cj+dP8LofzWvjrrLkZq7Afy7WrQMXdLy8Sx7HQ== + dependencies: + "@stablelib/ed25519" "^1.0.2" + "@stablelib/random" "^1.0.1" + "@walletconnect/safe-json" "^1.0.1" + "@walletconnect/time" "^1.0.2" + tslib "1.14.1" + uint8arrays "^3.0.0" + +"@walletconnect/safe-json@1.0.2", "@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.2.tgz#7237e5ca48046e4476154e503c6d3c914126fa77" + integrity sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA== + dependencies: + tslib "1.14.1" + +"@walletconnect/sign-client@2.17.3": + version "2.17.3" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.17.3.tgz#86c116bc927946bffa8415ca8d92d3ef412082e1" + integrity sha512-OzOWxRTfVGCHU3OOF6ibPkgPfDpivFJjuknfcOUt9PYWpTAv6YKOmT4cyfBPhc7llruyHpV44fYbykMcLIvEcg== + dependencies: + "@walletconnect/core" "2.17.3" + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "2.1.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.17.3" + "@walletconnect/utils" "2.17.3" + events "3.3.0" + +"@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@walletconnect/time/-/time-1.0.2.tgz#6c5888b835750ecb4299d28eecc5e72c6d336523" + integrity sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g== + dependencies: + tslib "1.14.1" + +"@walletconnect/types@2.17.3": + version "2.17.3" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.17.3.tgz#906f25cf0c9691704b9161eaa305262b0e7626d0" + integrity sha512-5eFxnbZGJJx0IQyCS99qz+OvozpLJJYfVG96dEHGgbzZMd+C9V1eitYqVClx26uX6V+WQVqVwjpD2Dyzie++Wg== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + events "3.3.0" + +"@walletconnect/utils@2.17.3": + version "2.17.3" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.17.3.tgz#a22938567febc3e3771efae8eb351adf3d499a8d" + integrity sha512-tG77UpZNeLYgeOwViwWnifpyBatkPlpKSSayhN0gcjY1lZAUNqtYslpm4AdTxlrA3pL61MnyybXgWYT5eZjarw== + dependencies: + "@ethersproject/hash" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "1.0.3" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.17.3" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + detect-browser "5.3.0" + elliptic "6.6.1" + query-string "7.1.3" + uint8arrays "3.1.0" + +"@walletconnect/window-getters@1.0.1", "@walletconnect/window-getters@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" + integrity sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q== + dependencies: + tslib "1.14.1" + +"@walletconnect/window-metadata@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz#2124f75447b7e989e4e4e1581d55d25bc75f7be5" + integrity sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA== + dependencies: + "@walletconnect/window-getters" "^1.0.1" + tslib "1.14.1" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +abs-svg-path@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf" + integrity sha1-32Acjo0roQ1KdtYl4japo5wnI78= + +accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn@^8.11.3: + version "8.12.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" + integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== + +acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +aes-js@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +anser@^1.4.9: + version "1.4.10" + resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" + integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" + +ansi-fragments@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-fragments/-/ansi-fragments-0.2.1.tgz#24409c56c4cc37817c3d7caa99d8969e2de5a05e" + integrity sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w== + dependencies: + colorette "^1.0.7" + slice-ansi "^2.0.0" + strip-ansi "^5.0.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +anymatch@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +appdirsjs@^1.2.4: + version "1.2.7" + resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.7.tgz#50b4b7948a26ba6090d4aede2ae2dc2b051be3b3" + integrity sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw== + +append-transform@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" + integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== + dependencies: + default-require-extensions "^2.0.0" + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +assert@^1.1.1, assert@^1.4.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +ast-types@0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d" + integrity sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg== + dependencies: + tslib "^2.0.1" + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + +babel-core@^7.0.0-bridge.0: + version "7.0.0-bridge.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" + integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== + +babel-jest@^29.6.3, babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-polyfill-corejs2@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" + integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-define-polyfill-provider" "^0.3.3" + semver "^6.1.1" + +babel-plugin-polyfill-corejs2@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" + integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.2" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" + integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + core-js-compat "^3.25.1" + +babel-plugin-polyfill-corejs3@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" + integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + core-js-compat "^3.31.0" + +babel-plugin-polyfill-regenerator@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" + integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + +babel-plugin-polyfill-regenerator@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" + integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + +babel-plugin-transform-flow-enums@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz#d1d0cc9bdc799c850ca110d0ddc9f21b9ec3ef25" + integrity sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ== + dependencies: + "@babel/plugin-syntax-flow" "^7.12.1" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base-64@0.1.0, base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= + +base-64@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" + integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg== + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base64-js@^1.3.1, base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + +big-integer@1.6.51: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + +"bignumber.js@git+https://github.com/status-im/bignumber.js.git#refs/tags/v4.0.2-status": + version "4.0.2" + resolved "git+https://github.com/status-im/bignumber.js.git#cc066a0a3d6bfe0c436c9957f4ea8344bf963c89" + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" + integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== + +bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +boxen@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +brorand@^1.0.1, brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0, browserify-sign@^4.0.4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" + integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + dependencies: + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.3" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@^4.21.10, browserslist@^4.21.9: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + +browserslist@^4.21.3, browserslist@^4.21.5: + version "4.21.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" + integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== + dependencies: + caniuse-lite "^1.0.30001449" + electron-to-chromium "^1.4.284" + node-releases "^2.0.8" + update-browserslist-db "^1.0.10" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0, buffer@^4.9.1: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +buffer@^5.4.3, buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bufferutil@^4.0.1: + version "4.0.8" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" + integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== + dependencies: + node-gyp-build "^4.3.0" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +caching-transform@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-3.0.2.tgz#601d46b91eca87687a281e71cef99791b0efca70" + integrity sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w== + dependencies: + hasha "^3.0.0" + make-dir "^2.0.0" + package-hash "^3.0.0" + write-file-atomic "^2.4.2" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001449: + version "1.0.30001469" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz#3dd505430c8522fdc9f94b4a19518e330f5c945a" + integrity sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g== + +caniuse-lite@^1.0.30001517: + version "1.0.30001532" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz#c6a4d5d2da6d2b967f0ee5e12e7f680db6ad2fca" + integrity sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw== + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chance@^1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.4.tgz#d8743bf8e40bb05e024c305ca1ff441195eb23db" + integrity sha512-pXPDSu3knKlb6H7ahQfpq//J9mSOxYK8SMtp8MV/nRJh8aLRDIl0ipLH8At8+nVogVwtvPZzyIzY/EbcY/cLuQ== + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +character-entities-legacy@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== + +character-entities@^1.0.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== + +character-reference-invalid@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== + +chokidar@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chrome-launcher@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" + integrity sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^1.0.0" + +chromium-edge-launcher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chromium-edge-launcher/-/chromium-edge-launcher-1.0.0.tgz#0443083074715a13c669530b35df7bfea33b1509" + integrity sha512-pgtgjNKZ7i5U++1g1PWv75umkHvhVTDOQIZ+sjeUX9483S7Y6MUvO0lrd7ShGlQlFHMN4SwKTCq/X8hWrbv2KA== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^1.0.0" + mkdirp "^1.0.4" + rimraf "^3.0.2" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +ci-info@^3.2.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.2.tgz#362ea15378f1c39378ba786affbc1c9ef015ecfd" + integrity sha512-lVZdhvbEudris15CLytp2u6Y0p5EKfztae9Fqa189MfNmln9F33XuH69v5fvNfiRN5/0eAUz2yJL3mo+nhaRKg== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +citty@^0.1.5, citty@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== + dependencies: + consola "^3.2.3" + +cjs-module-lexer@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.8.0.tgz#e97a3e2bd00e6d85aa0c13d7f9e3ce236f7787fc" + integrity sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ== + +clipboardy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-4.0.0.tgz#e73ced93a76d19dd379ebf1f297565426dffdca1" + integrity sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w== + dependencies: + execa "^8.0.1" + is-wsl "^3.1.0" + is64bit "^2.0.0" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q== + dependencies: + mimic-response "^1.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colord@2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" + integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== + +colorette@^1.0.7: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +comma-separated-tokens@^1.0.0: + version "1.0.8" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" + integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== + +command-exists@^1.2.8: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^9.4.1: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.1: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concurrently@^7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.6.0.tgz#531a6f5f30cf616f355a4afb8f8fcb2bba65a49a" + integrity sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw== + dependencies: + chalk "^4.1.0" + date-fns "^2.29.1" + lodash "^4.17.21" + rxjs "^7.0.0" + shell-quote "^1.7.3" + spawn-command "^0.0.2-1" + supports-color "^8.1.0" + tree-kill "^1.2.2" + yargs "^17.3.1" + +confbox@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" + integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +connect@^3.6.5: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +consola@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" + integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.1.0.tgz#68f8d9f48aeb5a534f3896f80e792760d3d20def" + integrity sha512-L2rLOcK0wzWSfSDA33YR+PUHDG10a8px7rUHKWbGLP4YfbsMed2KFUw5fczvDPbT98DDe3LEzviswl810apTEw== + +core-js-compat@^3.25.1: + version "3.29.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.29.1.tgz#15c0fb812ea27c973c18d425099afa50b934b41b" + integrity sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA== + dependencies: + browserslist "^4.21.5" + +core-js-compat@^3.31.0: + version "3.32.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.2.tgz#8047d1a8b3ac4e639f0d4f66d4431aa3b16e004c" + integrity sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ== + dependencies: + browserslist "^4.21.10" + +core-js@^2.5.7: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cosmiconfig@^5.0.5, cosmiconfig@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cp-file@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" + integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== + dependencies: + graceful-fs "^4.1.2" + make-dir "^2.0.0" + nested-error-stacks "^2.0.0" + pify "^4.0.1" + safe-buffer "^5.0.1" + +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +cross-spawn@^4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA== + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crossws@^0.2.0, crossws@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.2.4.tgz#82a8b518bff1018ab1d21ced9e35ffbe1681ad03" + integrity sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg== + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +crypto-js@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +csstype@^3.0.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +date-fns@^2.29.1: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + +dayjs@^1.8.15: + version "1.11.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" + integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + +debug@2.6.9, debug@^2.2.0, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +dedent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.6.0.tgz#0e6da8f0ce52838ef5cec5c8f9396b0c1b64a3cb" + integrity sha512-cSfRWjXJtZQeRuZGVvDrJroCR5V2UvBNUMHsPCdNYzuAG8b9V8aAy3KUcdQrGQPXs17Y+ojbPh1aOCplg9YR9g== + +dedent@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" + integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +deepmerge@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +default-require-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" + integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= + dependencies: + strip-bom "^3.0.0" + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +defu@^6.1.3, defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +denodeify@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" + integrity sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +deprecated-react-native-prop-types@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz#c10c6ee75ff2b6de94bb127f142b814e6e08d9ab" + integrity sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA== + dependencies: + "@react-native/normalize-color" "*" + invariant "*" + prop-types "*" + +deprecated-react-native-prop-types@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-5.0.0.tgz#02a12f090da7bd9e8c3ac53c31cf786a1315d302" + integrity sha512-cIK8KYiiGVOFsKdPMmm1L3tA/Gl+JopXL6F5+C7x39MyPsQYnP57Im/D6bNUzcborD7fcMwiwZqcBdBXXZucYQ== + dependencies: + "@react-native/normalize-colors" "^0.73.0" + invariant "^2.2.4" + prop-types "^15.8.1" + +des.js@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destr@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449" + integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-browser@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca" + integrity sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w== + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + +diff-sequences@^28.1.1: + version "28.1.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" + integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== + +diff-sequences@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" + integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +duplexify@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.284: + version "1.4.340" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.340.tgz#3a6d7414c1fc2dbf84b6f7af3ec24270606c85b8" + integrity sha512-zx8hqumOqltKsv/MF50yvdAlPF9S/4PXbyfzJS6ZGhbddGkRegdwImmfSVqCkEziYzrIGZ/TlrzBND4FysfkDg== + +electron-to-chromium@^1.4.477: + version "1.4.513" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz#41a50bf749aa7d8058ffbf7a131fc3327a7b1675" + integrity sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw== + +elliptic@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +elliptic@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" + integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +elliptic@^6.5.3: + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojilib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" + integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +envinfo@^7.10.0: + version "7.11.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.11.0.tgz#c3793f44284a55ff8c82faf1ffd91bc6478ea01f" + integrity sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +errorhandler@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.5.1.tgz#b9ba5d17cf90744cd1e851357a6e75bf806a9a91" + integrity sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A== + dependencies: + accepts "~1.3.7" + escape-html "~1.0.3" + +es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.63, es5-ext@^0.10.64, es5-ext@~0.10.14: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-error@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +esprima@^4.0.0, esprima@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eth-phishing-detect@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eth-phishing-detect/-/eth-phishing-detect-1.2.0.tgz#11b357776b2d1b98a9ac594a1343e5184fc26bf0" + integrity sha512-+M7D4dhu5tkSA9b5eiBwDeJCjwy+7Lv49nuTEw8fNZIZUAVZC3d2XHatBq1MOW7J8kxNGbBdgBuIf65opI7Tkg== + dependencies: + fast-levenshtein "^2.0.6" + +ethers@5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" + integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== + dependencies: + "@ethersproject/abi" "5.7.0" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/address" "5.7.0" + "@ethersproject/base64" "5.7.0" + "@ethersproject/basex" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@ethersproject/bytes" "5.7.0" + "@ethersproject/constants" "5.7.0" + "@ethersproject/contracts" "5.7.0" + "@ethersproject/hash" "5.7.0" + "@ethersproject/hdnode" "5.7.0" + "@ethersproject/json-wallets" "5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/logger" "5.7.0" + "@ethersproject/networks" "5.7.1" + "@ethersproject/pbkdf2" "5.7.0" + "@ethersproject/properties" "5.7.0" + "@ethersproject/providers" "5.7.2" + "@ethersproject/random" "5.7.0" + "@ethersproject/rlp" "5.7.0" + "@ethersproject/sha2" "5.7.0" + "@ethersproject/signing-key" "5.7.0" + "@ethersproject/solidity" "5.7.0" + "@ethersproject/strings" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@ethersproject/units" "5.7.0" + "@ethersproject/wallet" "5.7.0" + "@ethersproject/web" "5.7.1" + "@ethersproject/wordlists" "5.7.0" + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +event-target-shim@^5.0.0, event-target-shim@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter3@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg= + +events@3.3.0, events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + +events@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^5.0.0, execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expect@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-25.5.0.tgz#f07f848712a2813bb59167da3fb828ca21f58bba" + integrity sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA== + dependencies: + "@jest/types" "^25.5.0" + ansi-styles "^4.0.0" + jest-get-type "^25.2.6" + jest-matcher-utils "^25.5.0" + jest-message-util "^25.5.0" + jest-regex-util "^25.2.6" + +expect@^28.0.0: + version "28.1.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.3.tgz#90a7c1a124f1824133dd4533cce2d2bdcb6603ec" + integrity sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g== + dependencies: + "@jest/expect-utils" "^28.1.3" + jest-get-type "^28.0.2" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + +fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-redact@^3.0.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + +fast-text-encoding@1.0.6, fast-text-encoding@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + +fast-xml-parser@^4.0.12: + version "4.2.7" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz#871f2ca299dc4334b29f8da3658c164e68395167" + integrity sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig== + dependencies: + strnum "^1.0.5" + +fast-xml-parser@^4.2.4: + version "4.3.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz#761e641260706d6e13251c4ef8e3f5694d4b0d79" + integrity sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg== + dependencies: + strnum "^1.0.5" + +fault@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" + integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== + dependencies: + format "^0.2.0" + +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + integrity sha512-46TFiBOzX7xq/PcSWfFwkyjpemdRnMe31UQF+os0y+1W3k95f6R4SEt02Hj4p3X0Mir9gfrkmOtshFidS0VPUg== + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +firebase@11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-11.2.0.tgz#843de48382fcaf8050a4a278f35d094fb9960fda" + integrity sha512-ztwPhBLAZMVNZjBeQzzTM4rk2rsRXmdFYcnvjAXh+StbiFVshHKaPO9VRGMUzF48du4Mkz6jN1wkmYCuUJPxLA== + dependencies: + "@firebase/analytics" "0.10.11" + "@firebase/analytics-compat" "0.2.17" + "@firebase/app" "0.10.18" + "@firebase/app-check" "0.8.11" + "@firebase/app-check-compat" "0.3.18" + "@firebase/app-compat" "0.2.48" + "@firebase/app-types" "0.9.3" + "@firebase/auth" "1.8.2" + "@firebase/auth-compat" "0.5.17" + "@firebase/data-connect" "0.2.0" + "@firebase/database" "1.0.11" + "@firebase/database-compat" "2.0.2" + "@firebase/firestore" "4.7.6" + "@firebase/firestore-compat" "0.3.41" + "@firebase/functions" "0.12.1" + "@firebase/functions-compat" "0.3.18" + "@firebase/installations" "0.6.12" + "@firebase/installations-compat" "0.2.12" + "@firebase/messaging" "0.12.16" + "@firebase/messaging-compat" "0.2.16" + "@firebase/performance" "0.6.12" + "@firebase/performance-compat" "0.2.12" + "@firebase/remote-config" "0.5.0" + "@firebase/remote-config-compat" "0.2.12" + "@firebase/storage" "0.13.5" + "@firebase/storage-compat" "0.3.15" + "@firebase/util" "1.10.3" + "@firebase/vertexai" "1.0.3" + +flow-enums-runtime@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787" + integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== + +flow-parser@0.*: + version "0.202.1" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.202.1.tgz#3f87222e6910f6790bb2ce4648c20e1b87d6f9d7" + integrity sha512-IA8mhyNEUtzAKh+lj1yNDLFiUr1NSwPC+exQgghQNARFU/DeWGpoNmuYYzMDFIYsOdVdDoTJTxRc+/cS9CVvNg== + +flow-parser@^0.206.0: + version "0.206.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.206.0.tgz#f4f794f8026535278393308e01ea72f31000bfef" + integrity sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w== + +foreground-child@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" + integrity sha1-T9ca0t/elnibmApcCilZN8svXOk= + dependencies: + cross-spawn "^4" + signal-exit "^3.0.0" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-port-please@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.1.2.tgz#502795e56217128e4183025c89a48c71652f4e49" + integrity sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ== + +get-stdin@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" + integrity sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA== + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.1: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3, glob@^7.1.4: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.6: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== + dependencies: + ini "2.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +glur@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689" + integrity sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA== + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +graceful-fs@^4.1.3, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +h3@^1.10.2, h3@^1.11.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.12.0.tgz#9d7f05f08a997d263e484b02436cb027df3026d8" + integrity sha512-Zi/CcNeWBXDrFNlV0hUBJQR9F7a96RjMeAZweW/ZWkR9fuXrMcvKnSA63f/zZ9l0GgQOZDVHGvXivNN9PWOwhA== + dependencies: + cookie-es "^1.1.0" + crossws "^0.2.4" + defu "^6.1.4" + destr "^2.0.3" + iron-webcrypto "^1.1.1" + ohash "^1.1.3" + radix3 "^1.1.2" + ufo "^1.5.3" + uncrypto "^0.1.3" + unenv "^1.9.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hasha@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-3.0.0.tgz#52a32fab8569d41ca69a61ff1a214f8eb7c8bd39" + integrity sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk= + dependencies: + is-stream "^1.0.1" + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +hast-util-parse-selector@^2.0.0: + version "2.2.5" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" + integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== + +hastscript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" + integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.0.0" + property-information "^5.0.0" + space-separated-tokens "^1.0.0" + +hermes-estree@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.15.0.tgz#e32f6210ab18c7b705bdcb375f7700f2db15d6ba" + integrity sha512-lLYvAd+6BnOqWdnNbP/Q8xfl8LOGw4wVjfrNd9Gt8eoFzhNBRVD95n4l2ksfMVOoxuVyegs85g83KS9QOsxbVQ== + +hermes-estree@0.18.2: + version "0.18.2" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.18.2.tgz#fd450fa1659cf074ceaa2ddeeb21674f3b2342f3" + integrity sha512-KoLsoWXJ5o81nit1wSyEZnWUGy9cBna9iYMZBR7skKh7okYAYKqQ9/OczwpMHn/cH0hKDyblulGsJ7FknlfVxQ== + +hermes-parser@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.15.0.tgz#f611a297c2a2dbbfbce8af8543242254f604c382" + integrity sha512-Q1uks5rjZlE9RjMMjSUCkGrEIPI5pKJILeCtK1VmTj7U4pf3wVPoo+cxfu+s4cBAPy2JzikIIdCZgBoR6x7U1Q== + dependencies: + hermes-estree "0.15.0" + +hermes-parser@0.18.2: + version "0.18.2" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.18.2.tgz#50f15e2fcd559a48c68cd7af259d4292298bd14d" + integrity sha512-1eQfvib+VPpgBZ2zYKQhpuOjw1tH+Emuib6QmjkJWJMhyjM8xnXMvA+76o9LhF0zOAJDZgPfQhg43cyXEyl5Ew== + dependencies: + hermes-estree "0.18.2" + +hermes-profile-transformer@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/hermes-profile-transformer/-/hermes-profile-transformer-0.0.6.tgz#bd0f5ecceda80dd0ddaae443469ab26fb38fc27b" + integrity sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ== + dependencies: + source-map "^0.7.3" + +highlight.js@^10.4.1, highlight.js@~10.7.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +hmac-drbg@^1.0.0, hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoist-non-react-statics@3.x.x, hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +home-or-tmp@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-3.0.0.tgz#57a8fe24cf33cdd524860a15821ddc25c86671fb" + integrity sha512-pj6ktgQDedSIbzwrC108F9aZadnF2ZJ8mP8QbGq2nLPOmqpdqKWMoU4JPJaFEzpoTl6X/jOwE8BlkTOfVPR98A== + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-parser-js@>=0.5.1: + version "0.5.9" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.9.tgz#b817b3ca0edea6236225000d795378707c169cec" + integrity sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw== + +http-shutdown@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f" + integrity sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw== + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + +i18n-js@^3.3.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/i18n-js/-/i18n-js-3.5.1.tgz#9787450894059bec1af791123231e59898eb97c1" + integrity sha512-nJgbE5Vj9qzOQfjdVd/uoMoO8ppVaB/3LB6KOmMfD8IQ1vNNh307iHyQLK8ZnLYWkAszfPvVpYmUt1Le/RuHMQ== + +idb-keyval@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" + integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + +idb@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + +image-size@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.2.tgz#d778b6d0ab75b2737c1556dd631652eb963bc486" + integrity sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg== + dependencies: + queue "6.0.2" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +import-local@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" + integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +invariant@*, invariant@2.2.4, invariant@^2.2.2, invariant@^2.2.4, invariant@^2.2.x: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +iron-webcrypto@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz#aa60ff2aa10550630f4c0b11fd2442becdb35a6f" + integrity sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg== + +is-alphabetical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== + +is-alphanumerical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-decimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== + +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-hexadecimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-npm@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" + integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw== + +is-wsl@^2.1.1, is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +is64bit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is64bit/-/is64bit-2.0.0.tgz#198c627cbcb198bbec402251f88e5e1a51236c07" + integrity sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw== + dependencies: + system-architecture "^0.1.0" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-coverage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== + +istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-hook@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz#c95695f383d4f8f60df1f04252a9550e15b5b133" + integrity sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA== + dependencies: + append-transform "^1.0.0" + +istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" + integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" + integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^2.2.4: + version "2.2.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931" + integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== + dependencies: + html-escaper "^2.0.0" + +istanbul-reports@^3.1.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a" + integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + +jest-diff@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.3.tgz#948a192d86f4e7a64c5264ad4da4877133d8792f" + integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== + dependencies: + chalk "^4.0.0" + diff-sequences "^28.1.1" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-diff@^29.0.1, jest-diff@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.3.1.tgz#d8215b72fed8f1e647aed2cae6c752a89e757527" + integrity sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.3.1" + jest-get-type "^29.2.0" + pretty-format "^29.3.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-node@^29.6.3, jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + +jest-get-type@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" + integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== + +jest-get-type@^29.2.0: + version "29.2.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" + integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-image-snapshot@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/jest-image-snapshot/-/jest-image-snapshot-5.2.0.tgz#4af046935b465f0460aa73e890717bbc25d431e9" + integrity sha512-msKQqsxr4ZS8S3FQ6ot1SPlDKc4pCfyKY3SxU9LEoASj1zoEfglDYjmxNX53pxpNf7Fp7CJZvwP4xkNXVQgEXA== + dependencies: + chalk "^1.1.3" + get-stdin "^5.0.1" + glur "^1.1.2" + lodash "^4.17.4" + mkdirp "^0.5.1" + pixelmatch "^5.1.0" + pngjs "^3.4.0" + rimraf "^2.6.2" + ssim.js "^3.1.1" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz#fbc98a12d730e5d2453d7f1ed4a4d948e34b7867" + integrity sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw== + dependencies: + chalk "^3.0.0" + jest-diff "^25.5.0" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + +jest-matcher-utils@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz#5a77f1c129dd5ba3b4d7fc20728806c78893146e" + integrity sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw== + dependencies: + chalk "^4.0.0" + jest-diff "^28.1.3" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-matcher-utils@^29.0.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz#6e7f53512f80e817dfa148672bd2d5d04914a572" + integrity sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ== + dependencies: + chalk "^4.0.0" + jest-diff "^29.3.1" + jest-get-type "^29.2.0" + pretty-format "^29.3.1" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.5.0.tgz#ea11d93204cc7ae97456e1d8716251185b8880ea" + integrity sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^25.5.0" + "@types/stack-utils" "^1.0.1" + chalk "^3.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + slash "^3.0.0" + stack-utils "^1.0.1" + +jest-message-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.3.tgz#232def7f2e333f1eecc90649b5b94b0055e7c43d" + integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^28.1.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^28.1.3" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.5.0.tgz#a91a54dabd14e37ecd61665d6b6e06360a55387a" + integrity sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA== + dependencies: + "@jest/types" "^25.5.0" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.2.6.tgz#d847d38ba15d2118d3b06390056028d0f2fd3964" + integrity sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-silent-reporter@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jest-silent-reporter/-/jest-silent-reporter-0.5.0.tgz#5fd8ccd61665227e3bf19d908b7350719d06ff38" + integrity sha512-epdLt8Oj0a1AyRiR6F8zx/1SVT1Mi7VU3y4wB2uOBHs/ohIquC7v2eeja7UN54uRPyHInIKWdL+RdG228n5pJQ== + dependencies: + chalk "^4.0.0" + jest-util "^26.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0" + integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA== + dependencies: + "@jest/types" "^25.5.0" + chalk "^3.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + make-dir "^3.0.0" + +jest-util@^26.0.0: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + +jest-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.3.tgz#f4f932aa0074f0679943220ff9cbba7e497028b0" + integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.6.3, jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^29.6.3, jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +jiti@^1.21.0: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + +joi@^17.2.1: + version "17.9.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.1.tgz#74899b9fa3646904afa984a11df648eca66c9018" + integrity sha512-FariIi9j6QODKATGBrEX7HZcja8Bsh3rfdGYy/Sb65sGlZWK/QWesU1ghk7aJWDj95knjXlQfSmzFSPPkLVsfw== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsc-android@^250231.0.0: + version "250231.0.0" + resolved "https://registry.yarnpkg.com/jsc-android/-/jsc-android-250231.0.0.tgz#91720f8df382a108872fa4b3f558f33ba5e95262" + integrity sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw== + +jsc-safe-url@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz#141c14fbb43791e88d5dc64e85a374575a83477a" + integrity sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q== + +jscodeshift@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.14.0.tgz#7542e6715d6d2e8bde0b4e883f0ccea358b46881" + integrity sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA== + dependencies: + "@babel/core" "^7.13.16" + "@babel/parser" "^7.13.16" + "@babel/plugin-proposal-class-properties" "^7.13.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.13.8" + "@babel/plugin-proposal-optional-chaining" "^7.13.12" + "@babel/plugin-transform-modules-commonjs" "^7.13.8" + "@babel/preset-flow" "^7.13.13" + "@babel/preset-typescript" "^7.13.0" + "@babel/register" "^7.13.16" + babel-core "^7.0.0-bridge.0" + chalk "^4.1.2" + flow-parser "0.*" + graceful-fs "^4.2.4" + micromatch "^4.0.4" + neo-async "^2.5.0" + node-dir "^0.1.17" + recast "^0.21.0" + temp "^0.8.4" + write-file-atomic "^2.3.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json5@^2.2.1, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +keyvaluestorage-interface@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz#13ebdf71f5284ad54be94bd1ad9ed79adad515ff" + integrity sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +latest-version@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lighthouse-logger@^1.0.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz#aef90f9e97cd81db367c7634292ee22079280aaa" + integrity sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g== + dependencies: + debug "^2.6.9" + marky "^1.2.2" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +listhen@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.7.2.tgz#66b81740692269d5d8cafdc475020f2fc51afbae" + integrity sha512-7/HamOm5YD9Wb7CFgAZkKgVPA96WwhcTQoqtm2VTZGVbVVn3IWKRBTgrU7cchA3Q8k9iCsG8Osoi9GX4JsGM9g== + dependencies: + "@parcel/watcher" "^2.4.1" + "@parcel/watcher-wasm" "^2.4.1" + citty "^0.1.6" + clipboardy "^4.0.0" + consola "^3.2.3" + crossws "^0.2.0" + defu "^6.1.4" + get-port-please "^3.1.2" + h3 "^1.10.2" + http-shutdown "^1.2.2" + jiti "^1.21.0" + mlly "^1.6.1" + node-forge "^1.3.1" + pathe "^1.1.2" + std-env "^3.7.0" + ufo "^1.4.0" + untun "^0.1.3" + uqr "^0.1.2" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= + +lodash.isequal@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +lodash@4.17.x, lodash@^4.17.10, lodash@^4.17.21, lodash@^4.17.4: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +logkitty@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/logkitty/-/logkitty-0.7.1.tgz#8e8d62f4085a826e8d38987722570234e33c6aa7" + integrity sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ== + dependencies: + ansi-fragments "^0.2.1" + dayjs "^1.8.15" + yargs "^15.1.0" + +lolex@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" + integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== + dependencies: + "@sinonjs/commons" "^1.7.0" + +long@^5.0.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.4.tgz#ee651d5c7c25901cfca5e67220ae9911695e99b2" + integrity sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lottie-ios@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/lottie-ios/-/lottie-ios-3.2.3.tgz#d5a029ccce611603d178ea7ba725c1446f2310b4" + integrity sha512-mubYMN6+1HXa8z3EJKBvNBkl4UoVM4McjESeB2PgvRMSngmJtC5yUMRdhbbrIAn5Liu3hFGao/14s5hQIgtkRQ== + +lottie-react-native@^5.1.3: + version "5.1.6" + resolved "https://registry.yarnpkg.com/lottie-react-native/-/lottie-react-native-5.1.6.tgz#6ae72829c3c0b95952dddd3d78c043f4dfbcb487" + integrity sha512-vhdeZstXMfuVKwnddYWjJgQ/1whGL58IJEJu/iSf0XQ5gAb4pp/+vy91mdYQLezlb8Aw4Vu3fKnqErJL2hwchg== + dependencies: + invariant "^2.2.2" + react-native-safe-modules "^1.0.3" + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lowlight@^1.17.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== + dependencies: + fault "^1.0.0" + highlight.js "~10.7.0" + +lru-cache@^10.2.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.3.0.tgz#4a4aaf10c84658ab70f79a85a9a3f1e1fb11196b" + integrity sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ== + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" + integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== + dependencies: + semver "^6.0.0" + +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +marky@^1.2.2: + version "1.2.5" + resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" + integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +memoize-one@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +metro-babel-transformer@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.80.4.tgz#67dd300dd794d35ce24e22c17f317750669dd2b2" + integrity sha512-QP1kjYLap4O3w9tA4bYO8iyuNpR65If5Z97Ku37O4CwQPAwQaTmg67g4OdABS4BVK10fsxdExKp+fC37XirPow== + dependencies: + "@babel/core" "^7.20.0" + hermes-parser "0.18.2" + nullthrows "^1.1.1" + +metro-cache-key@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.80.4.tgz#dc92ca7aa251b9f6ed232fef98a4649fcc5d614e" + integrity sha512-okOOSRFou7Mxaaigoi+KxdFIU/ZJtvDCC6l8BYKsdMx86JDlVdvtIgFU4tFrY1yEkv0wnn7WH0X3xSz4mHKwoQ== + +metro-cache@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.80.4.tgz#3bfe8176353dd1e44fef4361339bd8ee992d5900" + integrity sha512-Dj+GoYt4PvsnnE4GdXhqV9PxEF7GPilY5NPeoTgptWZLlaDuTT2+cJQoDOOit1SfRjnF0zqABtVvB6GGBWdtaQ== + dependencies: + metro-core "0.80.4" + rimraf "^3.0.2" + +metro-config@0.80.4, metro-config@^0.80.3: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.80.4.tgz#f14fe1465bf8812cd9a930f9a1667350161050cf" + integrity sha512-X3/3tleFYB4SdoxXg8uJ+qc8eITKiLnXs3Ev6pihM4jIM5JD89riwUsSLKVsovfZs8ETqKtjevzfe6jQ2O5NtQ== + dependencies: + connect "^3.6.5" + cosmiconfig "^5.0.5" + jest-validate "^29.6.3" + metro "0.80.4" + metro-cache "0.80.4" + metro-core "0.80.4" + metro-runtime "0.80.4" + +metro-core@0.80.4, metro-core@^0.80.3: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.80.4.tgz#1421e432f2f9ec69d82ea1f19832a0544a3ce294" + integrity sha512-HRb+zydAhI7QyLpK4D6ARZsKjaBwEn+kCrJEjnVFij8wjJxIIHVilgNCETgg9NWvKJFUoZZCG7ewHkxQ9Qpd8Q== + dependencies: + lodash.throttle "^4.1.1" + metro-resolver "0.80.4" + +metro-file-map@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.80.4.tgz#22d2e1fc1110490ab1a6c3ab4de4c21fef1951af" + integrity sha512-EvBC31JI5vsyebeQ8PWpGENuAWy2Ka7sLqEW7OInW+aLVWmBq02h0BNl33xRgAMz0gwvMf2nKie82hmefYF6ew== + dependencies: + anymatch "^3.0.3" + debug "^2.2.0" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + invariant "^2.2.4" + jest-worker "^29.6.3" + micromatch "^4.0.4" + node-abort-controller "^3.1.1" + nullthrows "^1.1.1" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + +metro-minify-terser@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.80.4.tgz#008a4874f6167a4da5d24c90d4281b56f09ba684" + integrity sha512-cuxfRZWDWGKjh+Z6t4KJkrvmV4JUKXfvQuAX7Pa7U0Mf1YJdLtoGQ5iVOu/6MkfYGXbppqGk2qmFECrRGRh0cA== + dependencies: + terser "^5.15.0" + +metro-resolver@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.80.4.tgz#c04f2bf3995e11ee0484a067b7a51904ccee9964" + integrity sha512-PCiVWN+d3gtWlobf8jPypwKx9T1QrZmhLJAyqIWLoOsZbpSfj1dn5h0ajCr8rYi9LNzIHm58GGYJK8VFHNn8Cw== + +metro-runtime@0.80.4, metro-runtime@^0.80.3: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.80.4.tgz#24fe3e332cfbe303f944fc6d5f0c28bef6704ee1" + integrity sha512-CWIvf0zmL4jKHSj81zjUAbEwjTqFQmETI0NIQvN4JNwTSHiz50WPOuHnUUcmwM6Dye/ta6KNTELnERp0tKEYYg== + dependencies: + "@babel/runtime" "^7.0.0" + +metro-source-map@0.80.4, metro-source-map@^0.80.3: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.80.4.tgz#809d7d0eb36ccf912eecb4dc80ab50177f9fb5e1" + integrity sha512-x+0By55ml6IcGqY9x9HE0hyU0S+uDssrTQ0bPvuydG+iKCX85DzGnlT8k0Vs+EYgZl3KMWcvQ9TpGHW4LRL4GQ== + dependencies: + "@babel/traverse" "^7.20.0" + "@babel/types" "^7.20.0" + invariant "^2.2.4" + metro-symbolicate "0.80.4" + nullthrows "^1.1.1" + ob1 "0.80.4" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-symbolicate@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.80.4.tgz#1c72c5c7b29941ecd95e53f2a25570e61dfd4eec" + integrity sha512-UmtH96G5TrcAgbIqdE4xA8MBS9fbZW9Pln+n7eJ0tQ0Fw0M/jzdpiZzhx3bIB2zzqbdm6Nv/kB1+aEo0WvXdyg== + dependencies: + invariant "^2.2.4" + metro-source-map "0.80.4" + nullthrows "^1.1.1" + source-map "^0.5.6" + through2 "^2.0.1" + vlq "^1.0.0" + +metro-transform-plugins@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.80.4.tgz#fd76d62f080d556a8626ec6a92f55d033aa64283" + integrity sha512-cvmTLBA9ET64h+tgHt6prHlvOq98zBA1Glc9+wLZihPJo+Qmu9i3nQ1g4O+4aUnHivDlp+4C00BMNC+aC/buRQ== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/template" "^7.0.0" + "@babel/traverse" "^7.20.0" + nullthrows "^1.1.1" + +metro-transform-worker@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.80.4.tgz#33cab53b0cc527b627f7f7ef9c07a41dd15682d3" + integrity sha512-hLCrlxXyyaV64XQNSiyY/0jMVvGXrgXMkpJ4KwH2t4clxbxyt6TBW+4TqmgAeU9WGclY0OuQ0HzfvIZiONcUOw== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/parser" "^7.20.0" + "@babel/types" "^7.20.0" + metro "0.80.4" + metro-babel-transformer "0.80.4" + metro-cache "0.80.4" + metro-cache-key "0.80.4" + metro-source-map "0.80.4" + metro-transform-plugins "0.80.4" + nullthrows "^1.1.1" + +metro@0.80.4, metro@^0.80.3: + version "0.80.4" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.80.4.tgz#5f8cd8f7b730418ec6e7b377611caf620be33158" + integrity sha512-fBhZKU1z44KdhS6sH6Sk97595A66EOniH+jI9OjKDu6piH1SIEqQgdWAuWfJJMzgBHcJceRRvJY1zzsOT/Zx0g== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/parser" "^7.20.0" + "@babel/template" "^7.0.0" + "@babel/traverse" "^7.20.0" + "@babel/types" "^7.20.0" + accepts "^1.3.7" + chalk "^4.0.0" + ci-info "^2.0.0" + connect "^3.6.5" + debug "^2.2.0" + denodeify "^1.2.1" + error-stack-parser "^2.0.6" + graceful-fs "^4.2.4" + hermes-parser "0.18.2" + image-size "^1.0.2" + invariant "^2.2.4" + jest-worker "^29.6.3" + jsc-safe-url "^0.2.2" + lodash.throttle "^4.1.1" + metro-babel-transformer "0.80.4" + metro-cache "0.80.4" + metro-cache-key "0.80.4" + metro-config "0.80.4" + metro-core "0.80.4" + metro-file-map "0.80.4" + metro-minify-terser "0.80.4" + metro-resolver "0.80.4" + metro-runtime "0.80.4" + metro-source-map "0.80.4" + metro-symbolicate "0.80.4" + metro-transform-plugins "0.80.4" + metro-transform-worker "0.80.4" + mime-types "^2.1.27" + node-fetch "^2.2.0" + nullthrows "^1.1.1" + rimraf "^3.0.2" + serialize-error "^2.1.0" + source-map "^0.5.6" + strip-ansi "^6.0.0" + throat "^5.0.0" + ws "^7.5.1" + yargs "^17.6.2" + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +micromatch@^4.0.5: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +minimatch@^3.0.2, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass@^3.0.0: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732" + integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mlly@^1.6.1, mlly@^1.7.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" + integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== + dependencies: + acorn "^8.11.3" + pathe "^1.1.2" + pkg-types "^1.1.1" + ufo "^1.5.3" + +mri@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multiformats@^9.4.2: + version "9.9.0" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== + +napi-wasm@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.0.tgz#bbe617823765ae9c1bc12ff5942370eae7b2ba4e" + integrity sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.5.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nested-error-stacks@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" + integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +nocache@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79" + integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== + +node-abort-controller@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + +node-addon-api@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.0.tgz#71f609369379c08e251c558527a107107b5e0fdb" + integrity sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g== + +node-dir@^0.1.17: + version "0.1.17" + resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" + integrity sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg== + dependencies: + minimatch "^3.0.2" + +node-fetch-native@^1.6.1, node-fetch-native@^1.6.2, node-fetch-native@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" + integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== + +node-fetch@^2.2.0, node-fetch@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-libs-react-native@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/node-libs-react-native/-/node-libs-react-native-1.2.1.tgz#c9fd1d1b2cd2573d3e75633cea8bccd53b334f24" + integrity sha512-2LkPntNVa5MInMxtP7fW9F/uGo7KZCURI5/YoNn9/qOGIQpxEqnsndQWAPipsw8+ZVrIxPGkKEgJ2sSdvz0NKQ== + dependencies: + assert "^1.4.1" + base-64 "^0.1.0" + browserify-zlib "^0.2.0" + buffer "^6.0.3" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.0" + process "^0.11.0" + punycode "^2.1.0" + querystring-es3 "^0.2.0" + react-native-crypto "^2.0.1" + react-native-randombytes "^3.5.1" + readable-stream "^2.2.9" + stream-http "^2.3.1" + string_decoder "^1.0.3" + timers-browserify "^2.0.2" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +node-releases@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" + integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== + +node-stream-zip@^1.9.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" + integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== + +nodemon@^2.0.16: + version "2.0.16" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.16.tgz#d71b31bfdb226c25de34afea53486c8ef225fdef" + integrity sha512-zsrcaOfTWRuUzBn3P44RDliLlp263Z/76FPoHFr3cFFkOz0lTPAcIw8dCzfdVIx/t3AtDYCZRCDkoCojJqaG3w== + dependencies: + chokidar "^3.5.2" + debug "^3.2.7" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.8" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + update-notifier "^5.1.0" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-svg-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/normalize-svg-path/-/normalize-svg-path-1.0.1.tgz#6f729ad6b70bb4ca4eff2fe4b107489efe1d56fe" + integrity sha1-b3Ka1rcLtMpO/y/ksQdInv4dVv4= + dependencies: + svg-arc-to-cubic-bezier "^3.0.0" + +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +nullthrows@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== + +nyc@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-14.1.1.tgz#151d64a6a9f9f5908a1b73233931e4a0a3075eeb" + integrity sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw== + dependencies: + archy "^1.0.0" + caching-transform "^3.0.2" + convert-source-map "^1.6.0" + cp-file "^6.2.0" + find-cache-dir "^2.1.0" + find-up "^3.0.0" + foreground-child "^1.5.6" + glob "^7.1.3" + istanbul-lib-coverage "^2.0.5" + istanbul-lib-hook "^2.0.7" + istanbul-lib-instrument "^3.3.0" + istanbul-lib-report "^2.0.8" + istanbul-lib-source-maps "^3.0.6" + istanbul-reports "^2.2.4" + js-yaml "^3.13.1" + make-dir "^2.1.0" + merge-source-map "^1.1.0" + resolve-from "^4.0.0" + rimraf "^2.6.3" + signal-exit "^3.0.2" + spawn-wrap "^1.4.2" + test-exclude "^5.2.3" + uuid "^3.3.2" + yargs "^13.2.2" + yargs-parser "^13.0.0" + +ob1@0.80.4: + version "0.80.4" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.80.4.tgz#a2e77e2dbe144c76356c834b994e147e19bb472f" + integrity sha512-Lku8OBpq+fhF1ZdKUjbPnTNeqG+3OL0psGAEVJ8zcUiCB5/DPGR/rm3kLcjKDylzC9Rfv540/7I08+oImzfrhw== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +ofetch@^1.3.3: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.4.tgz#7ea65ced3c592ec2b9906975ae3fe1d26a56f635" + integrity sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw== + dependencies: + destr "^2.0.3" + node-fetch-native "^1.6.3" + ufo "^1.5.3" + +ohash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.3.tgz#f12c3c50bfe7271ce3fd1097d42568122ccdcf07" + integrity sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw== + +on-exit-leak-free@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209" + integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +open@^6.2.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" + integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg== + dependencies: + is-wsl "^1.1.0" + +open@^7.0.3: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-3.0.0.tgz#50183f2d36c9e3e528ea0a8605dff57ce976f88e" + integrity sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA== + dependencies: + graceful-fs "^4.1.15" + hasha "^3.0.0" + lodash.flattendeep "^4.4.0" + release-zalgo "^1.0.0" + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parse-asn1@^5.0.0, parse-asn1@^5.1.5: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-svg-path@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb" + integrity sha1-en7A0esG+lMlx9PgCbhZoJtdSes= + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo= + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +pathe@^1.1.1, pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pbkdf2@3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.8.tgz#2f8abf16ebecc82277945d748aba1d78761f61e2" + integrity sha1-L4q/FuvsyCJ3lF10irodeHYfYeI= + dependencies: + create-hmac "^1.1.2" + +pbkdf2@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" + integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.0.5: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pino-abstract-transport@v0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz#4b54348d8f73713bfd14e3dc44228739aa13d9c0" + integrity sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ== + dependencies: + duplexify "^4.1.2" + split2 "^4.0.0" + +pino-std-serializers@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1791ccd2539c091ae49ce9993205e2cd5dbba1e2" + integrity sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q== + +pino@7.11.0: + version "7.11.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-7.11.0.tgz#0f0ea5c4683dc91388081d44bff10c83125066f6" + integrity sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.0.0" + on-exit-leak-free "^0.2.0" + pino-abstract-transport v0.5.0 + pino-std-serializers "^4.0.0" + process-warning "^1.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.1.0" + safe-stable-stringify "^2.1.0" + sonic-boom "^2.2.1" + thread-stream "^0.15.1" + +pirates@^4.0.0, pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pirates@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pixelmatch@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a" + integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q== + dependencies: + pngjs "^6.0.0" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha512-ojakdnUgL5pzJYWw2AIDEupaQCX5OPbM688ZevubICjdIX01PRSYKqm33fJoCOJBRseYCTUlQRnBNX+Pchaejw== + dependencies: + find-up "^2.1.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-types@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.1.tgz#07b626880749beb607b0c817af63aac1845a73f2" + integrity sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ== + dependencies: + confbox "^0.1.7" + mlly "^1.7.0" + pathe "^1.1.2" + +pngjs@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + +pngjs@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" + integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg== + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +prettier-plugin-sort-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.0.0.tgz#b72a44b57183dd7f8cef5c07a0614f21d29a0e4a" + integrity sha512-zV5g+bWFD2zAqyQ8gCkwUTC49o9FxslaUdirwivt5GZHcf57hCocavykuyYqbExoEsuBOg8IU36OY7zmVEMOWA== + +prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== + +pretty-format@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" + integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== + dependencies: + "@jest/types" "^25.5.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + +pretty-format@^26.5.2, pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + +pretty-format@^28.0.0, pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.0.3, pretty-format@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.3.1.tgz#1841cac822b02b4da8971dacb03e8a871b4722da" + integrity sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg== + dependencies: + "@jest/schemas" "^29.0.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +prismjs@^1.27.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + +prismjs@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process-warning@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" + integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q== + +process@0.11.10, process@^0.11.0, process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +promise@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== + dependencies: + asap "~2.0.6" + +prompts@^2.0.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068" + integrity sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.4" + +prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types@*, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +prop-types@15.x.x, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +property-information@^5.0.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" + integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== + dependencies: + xtend "^4.0.0" + +protobufjs@^7.2.5: + version "7.4.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" + integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pupa@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + +pure-rand@^6.0.0: + version "6.0.4" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" + integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== + +query-string@7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== + dependencies: + decode-uri-component "^0.2.2" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +queue@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + +radix3@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.2.tgz#fd27d2af3896c6bf4bcdfab6427c69c2afc69ec0" + integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA== + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-devtools-core@^4.27.7: + version "4.28.5" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.5.tgz#c8442b91f068cdf0c899c543907f7f27d79c2508" + integrity sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA== + dependencies: + shell-quote "^1.6.1" + ws "^7" + +react-dom@18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023" + integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.21.0" + +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.1.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-lifecycles-compat@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-2.0.0.tgz#71d9c4cde47114c4102454f76da055c2bc48c948" + integrity sha512-txfpPCQYiazVdcbMRhatqWKcAxJweUu2wDXvts5/7Wyp6+Y9cHojqXHsLPEckzutfHlxZhG8Oiundbmp8Fd6eQ== + +react-native-background-timer@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/react-native-background-timer/-/react-native-background-timer-2.2.0.tgz#ff82d30899209b924983cc00e6ce174b8de5054a" + integrity sha512-Y7N6diSFko/FCJPMmB0PoBlmY1kEcma7qDVwn8t7zi12GLqpe/Vwls97onkSD8/QL+BR33BygUHOrLTPwgeKfg== + +react-native-biometrics@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/react-native-biometrics/-/react-native-biometrics-3.0.1.tgz#23c5a0bdbae1fcb1e08b22936223fe0fc4af846e" + integrity sha512-Ru80gXRa9KG04sl5AB9HyjLjVbduhqZVjA+AiOSGqr+fNqCDmCu9y5WEksnjbnniNLmq1yGcw+qcLXmR1ddLDQ== + +react-native-blob-util@^0.13.18: + version "0.13.18" + resolved "https://registry.yarnpkg.com/react-native-blob-util/-/react-native-blob-util-0.13.18.tgz#7b924d55607bda380d6e9862b621ed1ac0c007ec" + integrity sha512-f9NyydQKMd24QiyW+LZmDg2rjCTi4GBff3UJzP1BDgEQ8aoHMs4MZmPs5erNH4E577qhEFp5v7HGzXVRQL8SnQ== + dependencies: + base-64 "0.1.0" + glob "^7.1.6" + +"react-native-camera-kit@git+https://github.com/status-im/react-native-camera-kit.git#refs/tags/v14.0.0-beta15-status-v1": + version "14.0.0-beta15" + resolved "git+https://github.com/status-im/react-native-camera-kit.git#90821bc45c950c98119acc896651997a5357dd4f" + +react-native-config@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/react-native-config/-/react-native-config-1.5.0.tgz#ff5a78fbbc2c2a0525788e5f3c86101110651ba4" + integrity sha512-slecooA/0tCwhb+RuWEbwLqtKirGh9vWPRpgDfH7uPAraCciqHNH2XjS9ylW+Spn4FUrHg5KWTqUGs9BdBADHg== + +react-native-crypto@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/react-native-crypto/-/react-native-crypto-2.2.0.tgz#c999ed7c96064f830e1f958687f53d0c44025770" + integrity sha512-eZu9Y8pa8BN9FU2pIex7MLRAi+Cd1Y6bsxfiufKh7sfraAACJvjQTeW7/zcQAT93WMfM+D0OVk+bubvkrbrUkw== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.4" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "3.0.8" + public-encrypt "^4.0.0" + randomfill "^1.0.3" + +react-native-dialogs@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/react-native-dialogs/-/react-native-dialogs-1.1.2.tgz#28f6173e381c9354002b1c7839aa23dec4c5f71b" + integrity sha512-W0y22JpiywLNVYny32Z85h2zETkY9Neh48zndHJ4Byx/vS96Ex/0fhZ55hRS7XER7SBH3SHo5+TBTuX7ECHrLw== + +react-native-draggable-flatlist@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.1.tgz#2f027d387ba4b8f3eb0907340e32cb85e6460df2" + integrity sha512-ZO1QUTNx64KZfXGXeXcBfql67l38X7kBcJ3rxUVZzPHt5r035GnGzIC0F8rqSXp6zgnwgUYMfB6zQc5PKmPL9Q== + dependencies: + "@babel/preset-typescript" "^7.17.12" + +react-native-fast-image@^8.5.11: + version "8.5.11" + resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.5.11.tgz#e3dc969d0e4e8df026646bf18194465aa55cbc2b" + integrity sha512-cNW4bIJg3nvKaheG8vGMfqCt5LMWX9MS5+wMudgKIHbGO51spRr4sgnlhVgwHLcZ5aeNOVJ8CPRxDIWKRq/0QA== + +react-native-fs@^2.14.1: + version "2.16.6" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.16.6.tgz#2901789a43210a35a0ef0a098019bbef3af395fd" + integrity sha512-ZWOooD1AuFoAGY3HS2GY7Qx2LZo4oIg6AK0wbC68detxwvX75R/q9lRqThXNKP6vIo2VHWa0fYUo/SrLw80E8w== + dependencies: + base-64 "^0.1.0" + utf8 "^3.0.0" + +react-native-gesture-handler@2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.14.1.tgz#930640231024b7921435ab476aa501dd4a6b2e01" + integrity sha512-YiM1BApV4aKeuwsM6O4C2ufwewYEKk6VMXOt0YqEZFMwABBFWhXLySFZYjBSNRU2USGppJbfHP1q1DfFQpKhdA== + dependencies: + "@egjs/hammerjs" "^2.0.17" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + lodash "^4.17.21" + prop-types "^15.7.2" + +react-native-get-random-values@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz#1ca70d1271f4b08af92958803b89dccbda78728d" + integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ== + dependencies: + fast-base64-decode "^1.0.0" + +react-native-gifted-charts@^1.3.2: + version "1.3.6" + resolved "https://registry.yarnpkg.com/react-native-gifted-charts/-/react-native-gifted-charts-1.3.6.tgz#dc2d527bf46d7abdc1672fcdd86bfd54bed9edb1" + integrity sha512-jU3ikdxltRtpzcxRmCQGuovQ8dWnai8S+xScU0TgMMpfDuyLNHAFOi2Vu//7TPDWwtSWR2sIOHTtJLKJTqrriw== + +react-native-hole-view@^3.0.0-alpha4: + version "3.0.0-alpha4" + resolved "https://registry.yarnpkg.com/react-native-hole-view/-/react-native-hole-view-3.0.0-alpha4.tgz#b5f54df380636c3b4dfacd5c297117f068fa4531" + integrity sha512-pAIQCA13Dn42xLFsVziFXHRtTgRJ13SBrb1vwFG7cIFslMjmBhPdFlUrqYcN+wMTt2+OnZhRxgM4Yaf4+gARDg== + +react-native-image-crop-picker@0.41.2: + version "0.41.2" + resolved "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.41.2.tgz#824fa8fee8391fbb3e0b5ae2973221a2dff0cafb" + integrity sha512-GcDu/adXU/1y/MrxsbOfqcGRGWC2pTttt5VGy/jyRJ6GXfoC29fTQf8SG5kGtc5schSR6K+mKYO4uW6eJPljlQ== + +react-native-image-resizer@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/react-native-image-resizer/-/react-native-image-resizer-1.2.3.tgz#21cb2b158ff950e55a0fc01c2cb61375bd5a03ba" + integrity sha512-RDPNJglRmWDZ/7hvu+CbpsKYl6AQmseL8cWX4UkLAHxQWNc5ZdYhP/9avC5xCfyiwkRw+Zmkmv78HO0kt0ewhQ== + +react-native-keychain@8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.1.2.tgz#34291ae472878e5124d081211af5ede7d810e64f" + integrity sha512-bhHEui+yMp3Us41NMoRGtnWEJiBE0g8tw5VFpq4mpmXAx6XJYahuM6K3WN5CsUeUl83hYysSL9oFZNKSTPSvYw== + +react-native-linear-gradient@3.0.0-alpha.1: + version "3.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-3.0.0-alpha.1.tgz#fa1914462536f052faa211c34240be3ab1856bdd" + integrity sha512-vo9ks7m2VTLtbdAIMqwk944SB65+rc0U1a2R1CAQetT282DnH4rI3GKenaa+Gj2gPngF1l7Mc3Jxk9+6IZDraA== + +react-native-lottie-splash-screen@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/react-native-lottie-splash-screen/-/react-native-lottie-splash-screen-1.1.2.tgz#7d044cb6aede33dc30611da7a7d9edcb14ee005f" + integrity sha512-5V4sk46UlU2MbRDlncT0O3qtBdqjxKfcW5/VXceU82CPcO0z0ItcaX34L74Zt5fqkR80OW1VCccrsIGly1y0lw== + dependencies: + lottie-ios "3.2.3" + lottie-react-native "^5.1.3" + +react-native-mmkv@2.12.2: + version "2.12.2" + resolved "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee" + integrity sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg== + +react-native-navigation@7.43.0: + version "7.43.0" + resolved "https://registry.yarnpkg.com/react-native-navigation/-/react-native-navigation-7.43.0.tgz#533920672578b43f6a1884894c89584bc5452e61" + integrity sha512-OZe6yRrO91xLDeCRWqNkXYz2dQyCipmYicybQkjfUtdZEQDmOANLa3BoQOpOCRv3d38PX4qiGEleglJm16yUBQ== + dependencies: + hoist-non-react-statics "3.x.x" + lodash "4.17.x" + prop-types "15.x.x" + react-lifecycles-compat "2.0.0" + tslib "1.9.3" + +react-native-orientation-locker@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/react-native-orientation-locker/-/react-native-orientation-locker-1.5.0.tgz#324853937eed4835ecd1c8613ab2206135d908ac" + integrity sha512-4XOCGmNN4BXg5JUFjUuXpsfhPJmbA3LkQilJO1ed+6vL97teTdG2w5IEevKiqL9/hPjeWE8YYtX/YW+yp53hkg== + +react-native-pdf@^6.7.5: + version "6.7.5" + resolved "https://registry.yarnpkg.com/react-native-pdf/-/react-native-pdf-6.7.5.tgz#1bc5479f2161189f938912b4080ea9eef8a9d84d" + integrity sha512-d1S76p2Vwax2iG+kTnjINiUMvpjtJJvtMiYwHRbgGczT8GJjtXH49YCWOd+HfnUAU29cB+knzsKGYoZBMQM8Ow== + dependencies: + crypto-js "4.2.0" + deprecated-react-native-prop-types "^2.3.0" + +react-native-permissions@4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-4.1.5.tgz#db4d1ddbf076570043f4fd4168f54bb6020aec92" + integrity sha512-r6VMRacASmtRHS+GZ+5HQCp9p9kiE+UU9magHOZCXZLTJitdTuVHWZRrb4v4oqZGU+zAp3mZhTQftuMMv+WLUg== + +react-native-randombytes@^3.5.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/react-native-randombytes/-/react-native-randombytes-3.6.1.tgz#cac578093b5ca38e3e085becffdc6cbcf6f0d654" + integrity sha512-qxkwMbOZ0Hff1V7VqpaWrR6ItkA+oF6bnI79Qp9F3Tk8WBsdKDi6m1mi3dEdFWePoRLrhJ2L03rU0yabst1tVw== + dependencies: + buffer "^4.9.1" + sjcl "^1.0.3" + +react-native-reanimated@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz#5add41efafac6d0befd9786e752e7f26dbe903b7" + integrity sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg== + dependencies: + "@babel/plugin-transform-object-assign" "^7.16.7" + "@babel/preset-typescript" "^7.16.7" + convert-source-map "^2.0.0" + invariant "^2.2.4" + +react-native-redash@18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-native-redash/-/react-native-redash-18.1.0.tgz#ebebace19d0d2ed877cb6836b8c824b49354eb93" + integrity sha512-bdCFl/ZB7Rf2raIlU6SLV+Dc/rL6UXsQNjEVwTGBukHMeSKp1zs4zVtWaGimbN8P22N4qYvb9Jmw/K94ZWYG0Q== + dependencies: + abs-svg-path "^0.1.1" + normalize-svg-path "^1.0.1" + parse-svg-path "^0.1.2" + +react-native-safe-area-context@4.14.1: + version "4.14.1" + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.14.1.tgz#980c3c9db02a2314fa8ceb07f614728802320367" + integrity sha512-+tUhT5WBl8nh5+P+chYhAjR470iCByf9z5EYdCEbPaAK3Yfzw+o8VRPnUgmPAKlSccOgQBxx3NOl/Wzckn9ujg== + +react-native-safe-modules@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/react-native-safe-modules/-/react-native-safe-modules-1.0.3.tgz#f5f29bb9d09d17581193843d4173ad3054f74890" + integrity sha512-DUxti4Z+AgJ/ZsO5U7p3uSCUBko8JT8GvFlCeOXk9bMd+4qjpoDvMYpfbixXKgL88M+HwmU/KI1YFN6gsQZyBA== + dependencies: + dedent "^0.6.0" + +react-native-shadow-2@^7.0.8: + version "7.0.8" + resolved "https://registry.yarnpkg.com/react-native-shadow-2/-/react-native-shadow-2-7.0.8.tgz#df365ba1d8d22b7c63f1afc66d75308c3d6af4e1" + integrity sha512-6QlmbJvHCmDGa85jwtfrVxhro1oGm/ezTlciom0A+i0UFAzxlvmRiMHyKXg1hEB2PQkKPoG6ZpANju4mpqPqGA== + dependencies: + colord "2.9.2" + +react-native-shake@^3.3.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/react-native-shake/-/react-native-shake-3.4.0.tgz#3fa8f682651104b39c0b6c199bfa2ab10b36ce28" + integrity sha512-jOPrCC3VW/f8xYL6Yrv4XlSklVepMNF/ZdZ/5e0RzRPY+fexuNr0/Z1bQsPpaGjBd75Tk4WKpS/I/+Bj9oVXxg== + dependencies: + invariant "^2.2.x" + +react-native-share@10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/react-native-share/-/react-native-share-10.0.2.tgz#ba517d20a3bf20385eeeea32d9be9b41395f4dc7" + integrity sha512-EZs4MtsyauAI1zP8xXT1hIFB/pXOZJNDCKcgCpEfTZFXgCUzz8MDVbI1ocP2hA59XHRSkqAQdbJ0BFTpjxOBlg== + +"react-native-status-keycard@git+https://github.com/status-im/react-native-status-keycard.git#refs/tags/v2.6.6": + version "2.6.6" + resolved "git+https://github.com/status-im/react-native-status-keycard.git#c27b8059b53312f60281bd7ae782173be3ac2c74" + +react-native-svg@13.10.0: + version "13.10.0" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-13.10.0.tgz#d3c6222ea9cc1e21e2af0fd59dfbeafe7a3d0dc1" + integrity sha512-D/oYTmUi5nsA/2Nw4WYlF1UUi3vZqhpESpiEhpYCIFB/EMd6vz4A/uq3tIzZFcfa5z2oAdGSxRU1TaYr8IcPlQ== + dependencies: + css-select "^5.1.0" + css-tree "^1.1.3" + +react-native-system-navigation-bar@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/react-native-system-navigation-bar/-/react-native-system-navigation-bar-2.6.4.tgz#34edee7051dea01531ff2be95dc14f9fa8a540b7" + integrity sha512-4pysgADW53PiuHv+2glzNLJnHSxqDszZvLoitLFI5os4D+gCDfxmR36VSET4EnXkzSf8X9mbeFkHYDypDHJyZA== + +react-native-url-polyfill@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589" + integrity sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA== + dependencies: + whatwg-url-without-unicode "8.0.0-3" + +react-native-webview@13.6.3: + version "13.6.3" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.6.3.tgz#f3d26e942ef5cc5a07547f2e47903aa81a68e25e" + integrity sha512-IApO0JSj0uAWsBGKWagyfgDYoX29piiMYLmkHtRjKeL1rIVjLoR/bMe7KJ/0X47y86b//a6u3cYQtphyay+F2A== + dependencies: + escape-string-regexp "2.0.0" + invariant "2.2.4" + +react-native@0.73.5: + version "0.73.5" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.73.5.tgz#724fd1ae8ec8fee1dcf619c82bdd1695d3cff463" + integrity sha512-iHgDArmF4CrhL0qTj+Rn+CBN5pZWUL9lUGl8ub+V9Hwu/vnzQQh8rTMVSwVd2sV6N76KjpE5a4TfIAHkpIHhKg== + dependencies: + "@jest/create-cache-key-function" "^29.6.3" + "@react-native-community/cli" "12.3.6" + "@react-native-community/cli-platform-android" "12.3.6" + "@react-native-community/cli-platform-ios" "12.3.6" + "@react-native/assets-registry" "0.73.1" + "@react-native/codegen" "0.73.3" + "@react-native/community-cli-plugin" "0.73.17" + "@react-native/gradle-plugin" "0.73.4" + "@react-native/js-polyfills" "0.73.1" + "@react-native/normalize-colors" "0.73.2" + "@react-native/virtualized-lists" "0.73.4" + abort-controller "^3.0.0" + anser "^1.4.9" + ansi-regex "^5.0.0" + base64-js "^1.5.1" + chalk "^4.0.0" + deprecated-react-native-prop-types "^5.0.0" + event-target-shim "^5.0.1" + flow-enums-runtime "^0.0.6" + invariant "^2.2.4" + jest-environment-node "^29.6.3" + jsc-android "^250231.0.0" + memoize-one "^5.0.0" + metro-runtime "^0.80.3" + metro-source-map "^0.80.3" + mkdirp "^0.5.1" + nullthrows "^1.1.1" + pretty-format "^26.5.2" + promise "^8.3.0" + react-devtools-core "^4.27.7" + react-refresh "^0.14.0" + react-shallow-renderer "^16.15.0" + regenerator-runtime "^0.13.2" + scheduler "0.24.0-canary-efb381bbf-20230505" + stacktrace-parser "^0.1.10" + whatwg-fetch "^3.0.0" + ws "^6.2.2" + yargs "^17.6.2" + +react-refresh@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" + integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== + +react-shallow-renderer@^16.15.0: + version "16.15.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" + +react-syntax-highlighter@^15.5.0: + version "15.5.0" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" + integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg== + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "^10.4.1" + lowlight "^1.17.0" + prismjs "^1.27.0" + refractor "^3.6.0" + +react-test-renderer@18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.1.0.tgz#35b75754834cf9ab517b6813db94aee0a6b545c3" + integrity sha512-OfuueprJFW7h69GN+kr4Ywin7stcuqaYAt1g7airM5cUgP0BoF5G5CXsPGmXeDeEkncb2fqYNECO4y18sSqphg== + dependencies: + react-is "^18.1.0" + react-shallow-renderer "^16.15.0" + scheduler "^0.22.0" + +react@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +readable-stream@^2.0.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +readline-sync@^1.4.7: + version "1.4.10" + resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" + integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== + +readline@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c" + integrity sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg== + +real-require@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381" + integrity sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg== + +recast@^0.21.0: + version "0.21.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.21.5.tgz#e8cd22bb51bcd6130e54f87955d33a2b2e57b495" + integrity sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg== + dependencies: + ast-types "0.15.2" + esprima "~4.0.0" + source-map "~0.6.1" + tslib "^2.0.1" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +refractor@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" + integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== + dependencies: + hastscript "^6.0.0" + parse-entities "^2.0.0" + prismjs "~1.27.0" + +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate-unicode-properties@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" + integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== + dependencies: + regenerate "^1.4.0" + +regenerate@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.10: + version "0.13.10" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" + integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== + +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.2: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexpu-core@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" + integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.2.0" + regjsgen "^0.5.1" + regjsparser "^0.6.4" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.2.0" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +registry-auth-token@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" + integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== + dependencies: + rc "^1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +regjsgen@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" + integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== + +regjsparser@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" + integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== + dependencies: + jsesc "~0.5.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +release-zalgo@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" + integrity sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA= + dependencies: + es6-error "^4.0.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + +resolve@^1.10.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.16.1.tgz#49fac5d8bacf1fd53f200fa51247ae736175832c" + integrity sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig== + dependencies: + path-parse "^1.0.6" + +resolve@^1.14.2: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +rimraf@^2.6.2, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@~2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rxjs@^7.0.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.1.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + +safer-buffer@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +scheduler@0.24.0-canary-efb381bbf-20230505: + version "0.24.0-canary-efb381bbf-20230505" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz#5dddc60e29f91cd7f8b983d7ce4a99c2202d178f" + integrity sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA== + dependencies: + loose-envify "^1.1.0" + +scheduler@^0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" + integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== + dependencies: + loose-envify "^1.1.0" + +scheduler@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8" + integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ== + dependencies: + loose-envify "^1.1.0" + +scrypt-js@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" + integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.4, semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + +semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" + integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== + +serve-static@^1.13.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shadow-cljs-jar@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.4.tgz#0939d91c468b4bc5eab5a958f79e7ef5696fdf62" + integrity sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA== + +shadow-cljs@2.26.2: + version "2.26.2" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.26.2.tgz#c9d443940d679c33696009598b01e02a176f6a67" + integrity sha512-xBJxBxSpfoVQLSDA+WN+ZgtnyA5qYf3EE4ARZpov0JOz0YBTdIQajnNYMs5+5OzCbbNfhWGLybyu/Pj4dIwsWw== + dependencies: + node-libs-browser "^2.2.1" + readline-sync "^1.4.7" + shadow-cljs-jar "1.3.4" + source-map-support "^0.4.15" + which "^1.3.1" + ws "^7.4.6" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.6.1: + version "1.8.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba" + integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== + +shell-quote@^1.7.3: + version "1.7.4" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.4.tgz#33fe15dee71ab2a81fcbd3a52106c5cfb9fb75d8" + integrity sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +sisteransi@^1.0.4, sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +sjcl@^1.0.3: + version "1.0.8" + resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a" + integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +sonic-boom@^2.2.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611" + integrity sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg== + dependencies: + atomic-sleep "^1.0.0" + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== + dependencies: + source-map "^0.5.6" + +source-map-support@^0.5.16, source-map-support@^0.5.9, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.5.0, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +space-separated-tokens@^1.0.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" + integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== + +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" + integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg== + +spawn-wrap@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.3.tgz#81b7670e170cca247d80bf5faf0cfb713bdcf848" + integrity sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw== + dependencies: + foreground-child "^1.5.6" + mkdirp "^0.5.0" + os-homedir "^1.0.1" + rimraf "^2.6.2" + signal-exit "^3.0.2" + which "^1.3.0" + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +ssim.js@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df" + integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g== + +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +stacktrace-parser@^0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" + integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== + dependencies: + type-fest "^0.7.1" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +std-env@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" + integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-http@^2.3.1, stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@^1.0.0, string_decoder@^1.0.3, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +sudo-prompt@^9.0.0: + version "9.2.1" + resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd" + integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw== + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== + +supports-color@^5.3.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0, supports-color@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-arc-to-cubic-bezier@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6" + integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g== + +system-architecture@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d" + integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== + +tar@^6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +temp@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2" + integrity sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg== + dependencies: + rimraf "~2.6.2" + +terser@^5.15.0: + version "5.19.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.4.tgz#941426fa482bf9b40a0308ab2b3cd0cf7c775ebd" + integrity sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +thread-stream@^0.15.1: + version "0.15.2" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.15.2.tgz#fb95ad87d2f1e28f07116eb23d85aba3bc0425f4" + integrity sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA== + dependencies: + real-require "^0.1.0" + +throat@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" + integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== + +through2@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +timers-browserify@^2.0.2: + version "2.0.12" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" + integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== + dependencies: + setimmediate "^1.0.4" + +timers-browserify@^2.0.4: + version "2.0.11" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" + integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== + dependencies: + setimmediate "^1.0.4" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +tslib@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + +tslib@^2.0.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + +tslib@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" + integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +ufo@^1.4.0, ufo@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" + integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== + +uint8arrays@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.0.tgz#8186b8eafce68f28bd29bd29d683a311778901e2" + integrity sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog== + dependencies: + multiformats "^9.4.2" + +uint8arrays@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" + integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== + dependencies: + multiformats "^9.4.2" + +uncrypto@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" + integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +unenv@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.9.0.tgz#469502ae85be1bd3a6aa60f810972b1a904ca312" + integrity sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g== + dependencies: + consola "^3.2.3" + defu "^6.1.3" + mime "^3.0.0" + node-fetch-native "^1.6.1" + pathe "^1.1.1" + +unicode-canonical-property-names-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" + integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" + integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== + dependencies: + unicode-canonical-property-names-ecmascript "^1.0.4" + unicode-property-aliases-ecmascript "^1.0.4" + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" + integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" + integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unstorage@^1.9.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.10.2.tgz#fb7590ada8b30e83be9318f85100158b02a76dae" + integrity sha512-cULBcwDqrS8UhlIysUJs2Dk0Mmt8h7B0E6mtR+relW9nZvsf/u4SkAYyNliPiPW7XtFNb5u3IUMkxGxFTTRTgQ== + dependencies: + anymatch "^3.1.3" + chokidar "^3.6.0" + destr "^2.0.3" + h3 "^1.11.1" + listhen "^1.7.2" + lru-cache "^10.2.0" + mri "^1.2.0" + node-fetch-native "^1.6.2" + ofetch "^1.3.3" + ufo "^1.4.0" + +untun@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/untun/-/untun-0.1.3.tgz#5d10dee37a3a5737ff03d158be877dae0a0e58a6" + integrity sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ== + dependencies: + citty "^0.1.5" + consola "^3.2.3" + pathe "^1.1.1" + +update-browserslist-db@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +update-notifier@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" + integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== + dependencies: + boxen "^5.0.0" + chalk "^4.1.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.4.0" + is-npm "^5.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.1.0" + pupa "^2.1.1" + semver "^7.3.4" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + +uqr@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/uqr/-/uqr-0.1.2.tgz#5c6cd5dcff9581f9bb35b982cb89e2c483a41d7d" + integrity sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA== + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + +utf8@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +v8-to-istanbul@^9.0.1: + version "9.2.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" + integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vlq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" + integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== + +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +walker@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +websocket@^1.0.35: + version "1.0.35" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885" + integrity sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.63" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + +whatwg-fetch@^3.0.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + +whatwg-url-without-unicode@8.0.0-3: + version "8.0.0-3" + resolved "https://registry.yarnpkg.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b" + integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig== + dependencies: + buffer "^5.4.3" + punycode "^2.1.1" + webidl-conversions "^5.0.0" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9, which@^1.3.0, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.3.0, write-file-atomic@^2.4.2: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +ws@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + dependencies: + async-limiter "~1.0.0" + +ws@^7, ws@^7.4.6: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + +ws@^7.5.1: + version "7.5.5" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" + integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^2.2.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144" + integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg== + +yargs-parser@^13.0.0, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^13.2.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yargs@^15.1.0: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + +yargs@^17.3.1: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yargs@^17.6.2, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==